From dc662849d47f3acb5e5aa2d383e9574d014966eb Mon Sep 17 00:00:00 2001 From: FlightControl Date: Fri, 27 May 2016 09:45:58 +0200 Subject: [PATCH 1/5] Sets first version --- Moose Development/Moose/Set.lua | 472 ++++--------------------- Moose Development/Moose/UnitSet.lua | 516 ++++++++++++++++++++++++++++ 2 files changed, 588 insertions(+), 400 deletions(-) create mode 100644 Moose Development/Moose/UnitSet.lua diff --git a/Moose Development/Moose/Set.lua b/Moose Development/Moose/Set.lua index 063ec2fe0..0ac45ae1a 100644 --- a/Moose Development/Moose/Set.lua +++ b/Moose Development/Moose/Set.lua @@ -141,7 +141,7 @@ local _DATABASECategory = -- @usage -- -- Define a new SET Object. This DBObject will contain a reference to all Group and Unit Templates defined within the ME and the DCSRTE. -- DBObject = SET:New() -function SET:New() +function SET:New( Database ) -- Inherits from BASE local self = BASE:Inherit( self, BASE:New() ) @@ -156,189 +156,46 @@ function SET:New() -- Follow alive players and clients _EVENTDISPATCHER:OnPlayerEnterUnit( self._EventOnPlayerEnterUnit, self ) _EVENTDISPATCHER:OnPlayerLeaveUnit( self._EventOnPlayerLeaveUnit, self ) - - + + self.Collection = Database + + self:_RegisterSet() + self:_RegisterPlayers() + return self end ---- Finds a Unit based on the Unit Name. +--- Finds an Object based on the Object Name. -- @param #SET self --- @param #string UnitName --- @return Unit#UNIT The found Unit. -function SET:FindUnit( UnitName ) +-- @param #string ObjectName +-- @return #table The Object found. +function SET:_Find( ObjectName ) - local UnitFound = self.Units[UnitName] - return UnitFound + local ObjectFound = self.Set[ObjectName] + return ObjectFound end ---- Finds a Unit based on the Unit Name. +--- Adds a Object based on the Object Name. -- @param #SET self --- @param Unit#UNIT UnitToAdd --- @return Unit#UNIT The added Unit. -function SET:AddUnit( UnitToAdd ) +-- @param #string ObjectName +-- @param #table Object +-- @return #table The added Object. +function SET:_Add( ObjectName, Object ) - self.Units[UnitToAdd.UnitName] = UnitToAdd - return self.Units[UnitToAdd.UnitName] + self.Set[ObjectName] = Object end - - ---- Builds a set of units of coalitons. --- Possible current coalitions are red, blue and neutral. --- @param #SET self --- @param #string Coalitions Can take the following values: "red", "blue", "neutral". --- @return #SET self -function SET: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 self --- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship". --- @return #SET self -function SET: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 self --- @param #string Types Can take those type strings known within DCS world. --- @return #SET self -function SET: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 self --- @param #string Countries Can take those country strings known within DCS world. --- @return #SET self -function SET: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 self --- @param #string Prefixes The prefix of which the unit name starts with. --- @return #SET self -function SET:FilterUnitPrefixes( 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 of defined group prefixes. --- All the units starting with the given group prefixes will be included within the set. --- @param #SET self --- @param #string Prefixes The prefix of which the group name where the unit belongs to starts with. --- @return #SET self -function SET:FilterGroupPrefixes( 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. +--- Starts the filtering for the defined collection. -- @param #SET self -- @return #SET self -function SET:FilterStart() +function SET:_FilterStart() - if _DATABASE then - -- OK, we have a _DATABASE - -- Now use the different filters to build the set. - -- We first take ALL of the Units of the _DATABASE. - - self:E( { "Adding Set Datapoints with filters" } ) - for DCSUnitName, DCSUnit in pairs( _DATABASE.DCSUnits ) do + for ObjectName, Object in pairs( self.Collection ) do - if self:_IsIncludeDCSUnit( DCSUnit ) then - - self:E( { "Adding Unit:", DCSUnitName } ) - self.DCSUnits[DCSUnitName] = _DATABASE.DCSUnits[DCSUnitName] - self.Units[DCSUnitName] = _DATABASE:FindUnit( DCSUnitName ) - - if _DATABASE.DCSUnitsAlive[DCSUnitName] then - self.DCSUnitsAlive[DCSUnitName] = _DATABASE.DCSUnitsAlive[DCSUnitName] - self.UnitsAlive[DCSUnitName] = _DATABASE.UnitsAlive[DCSUnitName] - end - - end + if self:IsIncludeObject( Object ) then + self:E( { "Adding Object:", ObjectName } ) + self:_Add( ObjectName, Object ) end - - for DCSGroupName, DCSGroup in pairs( _DATABASE.DCSGroups ) do - - --if self:_IsIncludeDCSGroup( DCSGroup ) then - self:E( { "Adding Group:", DCSGroupName } ) - self.DCSGroups[DCSGroupName] = _DATABASE.DCSGroups[DCSGroupName] - self.Groups[DCSGroupName] = _DATABASE:FindGroups( DCSGroupName ) - --end - - if _DATABASE.DCSGroupsAlive[DCSGroupName] then - self.DCSGroupsAlive[DCSGroupName] = _DATABASE.DCSGroupsAlive[DCSGroupName] - self.GroupsAlive[DCSGroupName] = _DATABASE.GroupsAlive[DCSGroupName] - end - end - - for DCSUnitName, Client in pairs( _DATABASE.CLIENTS ) do - self:E( { "Adding Client for Unit:", DCSUnitName } ) - self.Clients[DCSUnitName] = _DATABASE.Clients[DCSUnitName] - end - - else - self:E( "There is a structural error in MOOSE. No _DATABASE has been defined! Cannot build this custom SET." ) end return self @@ -368,78 +225,19 @@ function SET:_RegisterPlayers() return self end ---- Private method that registers all datapoints within in the mission. --- @param #SET self --- @return #SET self -function SET:_RegisterDatabase() - - local CoalitionsData = { AlivePlayersRed = coalition.getGroups( coalition.side.RED ), AlivePlayersBlue = 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:", DCSGroup, DCSGroupName } ) - self.DCSGroups[DCSGroupName] = DCSGroup - self.Groups[DCSGroupName] = GROUP:New( DCSGroup ) - - if self:_IsAliveDCSGroup(DCSGroup) then - self:E( { "Register Alive Group:", DCSGroup, DCSGroupName } ) - self.DCSGroupsAlive[DCSGroupName] = DCSGroup - self.GroupsAlive[DCSGroupName] = self.Groups[DCSGroupName] - end - - for DCSUnitId, DCSUnit in pairs( DCSGroup:getUnits() ) do - - local DCSUnitName = DCSUnit:getName() - self:E( { "Register Unit:", DCSUnit, DCSUnitName } ) - - self.DCSUnits[DCSUnitName] = DCSUnit - self:AddUnit( UNIT:Find( DCSUnit ) ) - --self.Units[DCSUnitName] = UNIT:Register( DCSUnit ) - - if self:_IsAliveDCSUnit(DCSUnit) then - self:E( { "Register Alive Unit:", DCSUnit, DCSUnitName } ) - self.DCSUnitsAlive[DCSUnitName] = DCSUnit - self.UnitsAlive[DCSUnitName] = self.Units[DCSUnitName] - end - end - else - self:E( "Group does not exist: " .. DCSGroup ) - end - - for ClientName, ClientTemplate in pairs( self.Templates.ClientsByName ) do - self.Clients[ClientName] = CLIENT:Find( ClientName ) - end - end - end - - return self -end - - --- Events ---- Handles the OnBirth event for the alive units set. +--- Handles the OnBirth event for the Set. -- @param #SET self -- @param Event#EVENTDATA Event function SET:_EventOnBirth( Event ) self:F( { Event } ) if Event.IniDCSUnit then - if self:_IsIncludeDCSUnit( Event.IniDCSUnit ) then - self.DCSUnits[Event.IniDCSUnitName] = Event.IniDCSUnit - self.DCSUnitsAlive[Event.IniDCSUnitName] = Event.IniDCSUnit - self:AddUnit( UNIT:Register( Event.IniDCSUnit ) ) - --self.Units[Event.IniDCSUnitName] = UNIT:Register( Event.IniDCSUnit ) - - --if not self.DCSGroups[Event.IniDCSGroupName] then - -- self.DCSGroups[Event.IniDCSGroupName] = Event.IniDCSGroupName - -- self.DCSGroupsAlive[Event.IniDCSGroupName] = Event.IniDCSGroupName - -- self.Groups[Event.IniDCSGroupName] = GROUP:New( Event.IniDCSGroup ) - --end - self:_EventOnPlayerEnterUnit( Event ) + local ObjectName, Object = self:AddInDatabase( Event ) + if self:IsIncludeObject( Object ) then + self:_Add( ObjectName, Object ) + --self:_EventOnPlayerEnterUnit( Event ) end end end @@ -451,46 +249,46 @@ function SET:_EventOnDeadOrCrash( Event ) self:F( { Event } ) if Event.IniDCSUnit then - if self.DCSUnitsAlive[Event.IniDCSUnitName] then - self.DCSUnits[Event.IniDCSUnitName] = nil - self.DCSUnitsAlive[Event.IniDCSUnitName] = nil + local ObjectName, Object = self:FindInDatabase( Event ) + if ObjectName and Object then + self:_Delete( ObjectName ) end end end ---- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). --- @param #SET self --- @param Event#EVENTDATA Event -function SET:_EventOnPlayerEnterUnit( Event ) - self:F( { Event } ) - - if Event.IniDCSUnit then - if self:_IsIncludeDCSUnit( Event.IniDCSUnit ) then - if not self.PlayersAlive[Event.IniDCSUnitName] then - self:E( { "Add player for unit:", Event.IniDCSUnitName, Event.IniDCSUnit:getPlayerName() } ) - self.PlayersAlive[Event.IniDCSUnitName] = Event.IniDCSUnit:getPlayerName() - self.ClientsAlive[Event.IniDCSUnitName] = _DATABASE.Clients[ Event.IniDCSUnitName ] - end - end - end -end - ---- Handles the OnPlayerLeaveUnit event to clean the active players table. --- @param #SET self --- @param Event#EVENTDATA Event -function SET:_EventOnPlayerLeaveUnit( Event ) - self:F( { Event } ) - - if Event.IniDCSUnit then - if self:_IsIncludeDCSUnit( Event.IniDCSUnit ) then - if self.PlayersAlive[Event.IniDCSUnitName] then - self:E( { "Cleaning player for unit:", Event.IniDCSUnitName, Event.IniDCSUnit:getPlayerName() } ) - self.PlayersAlive[Event.IniDCSUnitName] = nil - self.ClientsAlive[Event.IniDCSUnitName] = nil - end - end - end -end +----- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). +---- @param #SET self +---- @param Event#EVENTDATA Event +--function SET:_EventOnPlayerEnterUnit( Event ) +-- self:F( { Event } ) +-- +-- if Event.IniDCSUnit then +-- if self:IsIncludeObject( Event.IniDCSUnit ) then +-- if not self.PlayersAlive[Event.IniDCSUnitName] then +-- self:E( { "Add player for unit:", Event.IniDCSUnitName, Event.IniDCSUnit:getPlayerName() } ) +-- self.PlayersAlive[Event.IniDCSUnitName] = Event.IniDCSUnit:getPlayerName() +-- self.ClientsAlive[Event.IniDCSUnitName] = _DATABASE.Clients[ Event.IniDCSUnitName ] +-- end +-- end +-- end +--end +-- +----- Handles the OnPlayerLeaveUnit event to clean the active players table. +---- @param #SET self +---- @param Event#EVENTDATA Event +--function SET:_EventOnPlayerLeaveUnit( Event ) +-- self:F( { Event } ) +-- +-- if Event.IniDCSUnit then +-- if self:IsIncludeObject( Event.IniDCSUnit ) then +-- if self.PlayersAlive[Event.IniDCSUnitName] then +-- self:E( { "Cleaning player for unit:", Event.IniDCSUnitName, Event.IniDCSUnit:getPlayerName() } ) +-- self.PlayersAlive[Event.IniDCSUnitName] = nil +-- self.ClientsAlive[Event.IniDCSUnitName] = nil +-- end +-- end +-- end +--end --- Iterators @@ -575,143 +373,17 @@ function SET:ForEachClient( IteratorFunction, ... ) end -function SET:ScanEnvironment() - self:F() - - self.Navpoints = {} - self.Units = {} - --Build routines.db.units and self.Navpoints - 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 - --self.Units[coa_name] = {} - - ---------------------------------------------- - -- build nav points DB - self.Navpoints[coa_name] = {} - 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[coa_name][nav_ind] = routines.utils.deepCopy(nav_data) - - self.Navpoints[coa_name][nav_ind]['name'] = nav_data.callsignStr -- name is a little bit more self-explanatory. - self.Navpoints[coa_name][nav_ind]['point'] = {} -- point is used by SSE, support it. - self.Navpoints[coa_name][nav_ind]['point']['x'] = nav_data.x - self.Navpoints[coa_name][nav_ind]['point']['y'] = 0 - self.Navpoints[coa_name][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.lower(cntry_data.name) - --self.Units[coa_name][countryName] = {} - --self.Units[coa_name][countryName]["countryId"] = cntry_data.id - - if type(cntry_data) == 'table' then --just making sure - - for obj_type_name, obj_type_data in pairs(cntry_data) do - - if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then --should be an unncessary check - - local category = 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:_RegisterGroup( GroupTemplate ) - 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 - - self:_RegisterDatabase() - self:_RegisterPlayers() - - return self -end - - ---- +--- Decides whether to include the Object -- @param #SET self --- @param DCSUnit#Unit DCSUnit +-- @param #table Object -- @return #SET self -function SET:_IsIncludeDCSUnit( DCSUnit ) - self:F( DCSUnit ) - local DCSUnitInclude = true - - if self.Filter.Coalitions then - local DCSUnitCoalition = false - for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do - self:T( { "Coalition:", DCSUnit:getCoalition(), self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) - if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == DCSUnit:getCoalition() then - DCSUnitCoalition = true - end - end - DCSUnitInclude = DCSUnitInclude and DCSUnitCoalition - end +function SET:IsIncludeObject( Object ) + self:F( Object ) - if self.Filter.Categories then - local DCSUnitCategory = false - for CategoryID, CategoryName in pairs( self.Filter.Categories ) do - self:T( { "Category:", DCSUnit:getDesc().category, self.FilterMeta.Categories[CategoryName], CategoryName } ) - if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == DCSUnit:getDesc().category then - DCSUnitCategory = true - end - end - DCSUnitInclude = DCSUnitInclude and DCSUnitCategory - end - - if self.Filter.Types then - local DCSUnitType = false - for TypeID, TypeName in pairs( self.Filter.Types ) do - self:T( { "Type:", DCSUnit:getTypeName(), TypeName } ) - if TypeName == DCSUnit:getTypeName() then - DCSUnitType = true - end - end - DCSUnitInclude = DCSUnitInclude and DCSUnitType - end - - if self.Filter.Countries then - local DCSUnitCountry = false - for CountryID, CountryName in pairs( self.Filter.Countries ) do - self:T( { "Country:", DCSUnit:getCountry(), CountryName } ) - if country.id[CountryName] == DCSUnit:getCountry() then - DCSUnitCountry = true - end - end - DCSUnitInclude = DCSUnitInclude and DCSUnitCountry - end - - if self.Filter.UnitPrefixes then - local DCSUnitPrefix = false - for UnitPrefixId, UnitPrefix in pairs( self.Filter.UnitPrefixes ) do - self:T( { "Unit Prefix:", string.find( DCSUnit:getName(), UnitPrefix, 1 ), UnitPrefix } ) - if string.find( DCSUnit:getName(), UnitPrefix, 1 ) then - DCSUnitPrefix = true - end - end - DCSUnitInclude = DCSUnitInclude and DCSUnitPrefix - end - - self:T( DCSUnitInclude ) - return DCSUnitInclude + return true end + --- -- @param #SET self -- @param DCSUnit#Unit DCSUnit diff --git a/Moose Development/Moose/UnitSet.lua b/Moose Development/Moose/UnitSet.lua new file mode 100644 index 000000000..188d59a73 --- /dev/null +++ b/Moose Development/Moose/UnitSet.lua @@ -0,0 +1,516 @@ +--- Create and manage a set of units. +-- +-- @{#UNITSET} class +-- ================== +-- Mission designers can use the UNITSET class to build sets of units belonging to certain: +-- +-- * Coalitions +-- * Categories +-- * Countries +-- * Unit types +-- * Starting with certain prefix strings. +-- +-- This list will grow over time. Planned developments are to include filters and iterators. +-- Additional filters will be added around @{Zone#ZONEs}, Radiuses, Active players, ... +-- More iterators will be implemented in the near future ... +-- +-- Administers the Initial Sets of the Mission Templates as defined within the Mission Editor. +-- +-- UNITSET construction methods: +-- ================================= +-- Create a new UNITSET object with the @{#UNITSET.New} method: +-- +-- * @{#UNITSET.New}: Creates a new UNITSET object. +-- +-- +-- UNITSET filter criteria: +-- ========================= +-- You can set filter criteria to define the set of units within the UNITSET. +-- Filter criteria are defined by: +-- +-- * @{#UNITSET.FilterCoalitions}: Builds the UNITSET with the units belonging to the coalition(s). +-- * @{#UNITSET.FilterCategories}: Builds the UNITSET with the units belonging to the category(ies). +-- * @{#UNITSET.FilterTypes}: Builds the UNITSET with the units belonging to the unit type(s). +-- * @{#UNITSET.FilterCountries}: Builds the UNITSET with the units belonging to the country(ies). +-- * @{#UNITSET.FilterPrefixes}: Builds the UNITSET with the units starting with the same prefix string(s). +-- +-- Once the filter criteria have been set for the UNITSET, you can start filtering using: +-- +-- * @{#UNITSET.FilterStart}: Starts the filtering of the units within the UNITSET. +-- +-- Planned filter criteria within development are (so these are not yet available): +-- +-- * @{#UNITSET.FilterZones}: Builds the UNITSET with the units within a @{Zone#ZONE}. +-- +-- +-- UNITSET iterators: +-- =================== +-- Once the filters have been defined and the UNITSET has been built, you can iterate the UNITSET with the available iterator methods. +-- The iterator methods will walk the UNITSET set, and call for each element within the set a function that you provide. +-- The following iterator methods are currently available within the UNITSET: +-- +-- * @{#UNITSET.ForEachUnit}: Calls a function for each alive unit it finds within the UNITSET. +-- +-- Planned iterators methods in development are (so these are not yet available): +-- +-- * @{#UNITSET.ForEachUnitInGroup}: Calls a function for each group contained within the UNITSET. +-- * @{#UNITSET.ForEachUnitInZone}: Calls a function for each unit within a certain zone contained within the UNITSET. +-- +-- @module Set +-- @author FlightControl + +Include.File( "Routines" ) +Include.File( "Base" ) +Include.File( "Menu" ) +Include.File( "Group" ) +Include.File( "Unit" ) +Include.File( "Event" ) +Include.File( "Client" ) + + +--- UNITSET class +-- @type UNITSET +-- @extends Set#SET +UNITSET = { + ClassName = "UNITSET", + 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, + }, + }, +} + +local _DATABASECoalition = + { + [1] = "Red", + [2] = "Blue", + } + +local _DATABASECategory = + { + [Unit.Category.AIRPLANE] = "Plane", + [Unit.Category.HELICOPTER] = "Helicopter", + [Unit.Category.GROUND_UNIT] = "Vehicle", + [Unit.Category.SHIP] = "Ship", + [Unit.Category.STRUCTURE] = "Structure", + } + + +--- Creates a new UNITSET object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. +-- @param #UNITSET self +-- @return #UNITSET +-- @usage +-- -- Define a new UNITSET Object. This DBObject will contain a reference to all alive Units. +-- DBObject = UNITSET:New() +function UNITSET:New() + + -- Inherits from BASE + local self = BASE:Inherit( self, SET:New( _DATABASE.Units ) ) + + + +-- -- Follow alive players and clients +-- _EVENTDISPATCHER:OnPlayerEnterUnit( self._EventOnPlayerEnterUnit, self ) +-- _EVENTDISPATCHER:OnPlayerLeaveUnit( self._EventOnPlayerLeaveUnit, self ) + +-- self:_RegisterPlayers() + + return self +end + +--- Finds a Unit based on the Unit Name. +-- @param #UNITSET self +-- @param #string UnitName +-- @return Unit#UNIT The found Unit. +function UNITSET:FindUnit( UnitName ) + + local UnitFound = self.Units[UnitName] + return UnitFound +end + +--- Finds a Unit based on the Unit Name. +-- @param #UNITSET self +-- @param Unit#UNIT UnitName +-- @param Unit#UNIT UnitData +-- @return Unit#UNIT The added Unit. +function UNITSET:_AddUnit( UnitName, UnitData ) + + self.Units[UnitName] = _DATABASE:FindUnit( UnitName ) +end + + + +--- Builds a set of units of coalitons. +-- Possible current coalitions are red, blue and neutral. +-- @param #UNITSET self +-- @param #string Coalitions Can take the following values: "red", "blue", "neutral". +-- @return #UNITSET self +function UNITSET: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 #UNITSET self +-- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship". +-- @return #UNITSET self +function UNITSET: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 #UNITSET self +-- @param #string Types Can take those type strings known within DCS world. +-- @return #UNITSET self +function UNITSET: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 #UNITSET self +-- @param #string Countries Can take those country strings known within DCS world. +-- @return #UNITSET self +function UNITSET: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 #UNITSET self +-- @param #string Prefixes The prefix of which the unit name starts with. +-- @return #UNITSET self +function UNITSET: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 of defined group prefixes. +---- All the units starting with the given group prefixes will be included within the set. +---- @param #UNITSET self +---- @param #string Prefixes The prefix of which the group name where the unit belongs to starts with. +---- @return #UNITSET self +--function UNITSET:FilterGroupPrefixes( 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 #UNITSET self +-- @return #UNITSET self +function UNITSET:FilterStart() + + if _DATABASE then + self:_FilterStart( self.DatabaseCollection ) + end + + FollowEventBirth( ) + + _EVENTDISPATCHER:OnBirth( self._EventOnBirth, self ) + _EVENTDISPATCHER:OnDead( self._EventOnDeadOrCrash, self ) + _EVENTDISPATCHER:OnCrash( self._EventOnDeadOrCrash, self ) + + return self +end + +--- Events + +--- Handles the OnBirth event for the alive units set. +-- @param #UNITSET self +-- @param Event#EVENTDATA Event +function UNITSET:_EventOnBirth( Event ) + self:F( { Event } ) + + if Event.IniDCSUnit then + if self:_IsIncludeUnit( Event.IniDCSUnit ) then + self.DCSUnits[Event.IniDCSUnitName] = Event.IniDCSUnit + self.DCSUnitsAlive[Event.IniDCSUnitName] = Event.IniDCSUnit + self:_AddUnit( UNIT:Register( Event.IniDCSUnit ) ) + --self.Units[Event.IniDCSUnitName] = UNIT:Register( Event.IniDCSUnit ) + + --if not self.DCSGroups[Event.IniDCSGroupName] then + -- self.DCSGroups[Event.IniDCSGroupName] = Event.IniDCSGroupName + -- self.DCSGroupsAlive[Event.IniDCSGroupName] = Event.IniDCSGroupName + -- self.Groups[Event.IniDCSGroupName] = GROUP:New( Event.IniDCSGroup ) + --end + self:_EventOnPlayerEnterUnit( Event ) + end + end +end + +--- Handles the OnDead or OnCrash event for alive units set. +-- @param #UNITSET self +-- @param Event#EVENTDATA Event +function UNITSET:_EventOnDeadOrCrash( Event ) + self:F( { Event } ) + + if Event.IniDCSUnit then + if self.DCSUnitsAlive[Event.IniDCSUnitName] then + self.DCSUnits[Event.IniDCSUnitName] = nil + self.DCSUnitsAlive[Event.IniDCSUnitName] = nil + end + end +end + +--- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). +-- @param #UNITSET self +-- @param Event#EVENTDATA Event +function UNITSET:_EventOnPlayerEnterUnit( Event ) + self:F( { Event } ) + + if Event.IniDCSUnit then + if self:_IsIncludeUnit( Event.IniDCSUnit ) then + if not self.PlayersAlive[Event.IniDCSUnitName] then + self:E( { "Add player for unit:", Event.IniDCSUnitName, Event.IniDCSUnit:getPlayerName() } ) + self.PlayersAlive[Event.IniDCSUnitName] = Event.IniDCSUnit:getPlayerName() + self.ClientsAlive[Event.IniDCSUnitName] = _DATABASE.Clients[ Event.IniDCSUnitName ] + end + end + end +end + +--- Handles the OnPlayerLeaveUnit event to clean the active players table. +-- @param #UNITSET self +-- @param Event#EVENTDATA Event +function UNITSET:_EventOnPlayerLeaveUnit( Event ) + self:F( { Event } ) + + if Event.IniDCSUnit then + if self:_IsIncludeUnit( Event.IniDCSUnit ) then + if self.PlayersAlive[Event.IniDCSUnitName] then + self:E( { "Cleaning player for unit:", Event.IniDCSUnitName, Event.IniDCSUnit:getPlayerName() } ) + self.PlayersAlive[Event.IniDCSUnitName] = nil + self.ClientsAlive[Event.IniDCSUnitName] = nil + end + end + end +end + +--- Iterators + +--- Interate the UNITSET and call an interator function for the given set, providing the Object for each element within the set and optional parameters. +-- @param #UNITSET self +-- @param #function IteratorFunction The function that will be called when there is an alive player in the UNITSET. +-- @return #UNITSET self +function UNITSET:ForEach( IteratorFunction, arg, Set ) + self:F( 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 % 10 == 0 then + coroutine.yield( false ) + end + end + return true + end + + local co = coroutine.create( CoRoutine ) + + local function Schedule() + + local status, res = coroutine.resume( co ) + self:T( { status, res } ) + + if status == false then + error( res ) + end + if res == false then + return true -- resume next time the loop + end + + return false + end + + local Scheduler = SCHEDULER:New( self, Schedule, {}, 0.001, 0.001, 0 ) + + return self +end + + +--- Interate the UNITSET and call an interator function for each **alive** unit, providing the Unit and optional parameters. +-- @param #UNITSET self +-- @param #function IteratorFunction The function that will be called when there is an alive unit in the UNITSET. The function needs to accept a UNIT parameter. +-- @return #UNITSET self +function UNITSET:ForEachDCSUnitAlive( IteratorFunction, ... ) + self:F( arg ) + + self:ForEach( IteratorFunction, arg, self.DCSUnitsAlive ) + + return self +end + + +--- Interate the UNITSET and call an interator function for each **alive** player, providing the Unit of the player and optional parameters. +-- @param #UNITSET self +-- @param #function IteratorFunction The function that will be called when there is an alive player in the UNITSET. The function needs to accept a UNIT parameter. +-- @return #UNITSET self +function UNITSET:ForEachPlayer( IteratorFunction, ... ) + self:F( arg ) + + self:ForEach( IteratorFunction, arg, self.PlayersAlive ) + + return self +end + + +--- Interate the UNITSET and call an interator function for each client, providing the Client to the function and optional parameters. +-- @param #UNITSET self +-- @param #function IteratorFunction The function that will be called when there is an alive player in the UNITSET. The function needs to accept a CLIENT parameter. +-- @return #UNITSET self +function UNITSET:ForEachClient( IteratorFunction, ... ) + self:F( arg ) + + self:ForEach( IteratorFunction, arg, self.Clients ) + + return self +end + + +--- +-- @param #UNITSET self +-- @param Unit#UNIT MUnit +-- @return #UNITSET self +function UNITSET:IsIncludeObject( MUnit ) + self:F( MUnit ) + local MUnitInclude = true + + if self.Filter.Coalitions then + local MUnitCoalition = false + for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do + self:T( { "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:T( { "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:T( { "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:T( { "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:T( { "Unit Prefix:", string.find( MUnit:GetName(), UnitPrefix, 1 ), UnitPrefix } ) + if string.find( MUnit:GetName(), UnitPrefix, 1 ) then + MUnitPrefix = true + end + end + MUnitInclude = MUnitInclude and MUnitPrefix + end + + self:T( MUnitInclude ) + return MUnitInclude +end + + From 645d074a7dfd010eb77260bfc5d7b2155434d5cd Mon Sep 17 00:00:00 2001 From: FlightControl Date: Fri, 27 May 2016 11:32:49 +0200 Subject: [PATCH 2/5] GROUPSET and UNITSET are working now !! - GROUPSET and UNITSET inherit SET - DATABASE optimized - Tracing levels tuned. --- Moose Development/Moose/Database.lua | 68 +- Moose Development/Moose/Event.lua | 50 +- Moose Development/Moose/Group.lua | 17 + Moose Development/Moose/GroupSet.lua | 327 + Moose Development/Moose/Set.lua | 267 +- Moose Development/Moose/UnitSet.lua | 275 +- .../l10n/DEFAULT/Moose.lua | 16724 +--------------- Moose Mission Setup/Moose.lua | 16724 +--------------- .../Moose_Test_CLEANUP/Moose_Test_CLEANUP.miz | Bin 98334 -> 98334 bytes .../Moose_Test_DATABASE.lua | 63 +- .../Moose_Test_DATABASE.miz | Bin 27828 -> 29075 bytes .../Moose_Test_DESTROY/MOOSE_Test_DESTROY.miz | Bin 33518 -> 33518 bytes .../Moose_Test_ESCORT/MOOSE_Test_ESCORT.miz | Bin 59173 -> 59173 bytes .../Moose_Test_MISSILETRAINER.miz | Bin 118467 -> 118467 bytes .../Moose_Test_SEAD/MOOSE_Test_SEAD.miz | Bin 25771 -> 25771 bytes .../Moose_Test_SPAWN/MOOSE_Test_SPAWN.miz | Bin 51188 -> 51188 bytes .../MOOSE_Test_SPAWN_Repeat.miz | Bin 25332 -> 25332 bytes .../MOOSE_Test_TASK_Pickup_and_Deploy.miz | Bin 31933 -> 31933 bytes .../Moose_Test_WRAPPER/Moose_Test_WRAPPER.miz | Bin 40952 -> 40952 bytes 19 files changed, 634 insertions(+), 33881 deletions(-) create mode 100644 Moose Development/Moose/GroupSet.lua diff --git a/Moose Development/Moose/Database.lua b/Moose Development/Moose/Database.lua index 1814af91f..b0fff9b76 100644 --- a/Moose Development/Moose/Database.lua +++ b/Moose Development/Moose/Database.lua @@ -226,9 +226,9 @@ end -- @param #table SpawnTemplate -- @return #DATABASE self function DATABASE:Spawn( SpawnTemplate ) - self:F( SpawnTemplate.name ) + self:F2( SpawnTemplate.name ) - self:T( { SpawnTemplate.SpawnCountryID, SpawnTemplate.SpawnCategoryID } ) + self:T2( { SpawnTemplate.SpawnCountryID, SpawnTemplate.SpawnCategoryID } ) -- Copy the spawn variables of the template in temporary storage, nullify, and restore the spawn variables. local SpawnCoalitionID = SpawnTemplate.SpawnCoalitionID @@ -240,7 +240,7 @@ function DATABASE:Spawn( SpawnTemplate ) SpawnTemplate.SpawnCountryID = nil SpawnTemplate.SpawnCategoryID = nil - self:_RegisterGroup( SpawnTemplate ) + self:_RegisterTemplate( SpawnTemplate ) coalition.addGroup( SpawnCountryID, SpawnCategoryID, SpawnTemplate ) -- Restore @@ -256,7 +256,7 @@ end --- Set a status to a Group within the Database, this to check crossing events for example. function DATABASE:SetStatusGroup( GroupName, Status ) - self:F( Status ) + self:F2( Status ) self.Templates.Groups[GroupName].Status = Status end @@ -264,7 +264,7 @@ end --- Get a status to a Group within the Database, this to check crossing events for example. function DATABASE:GetStatusGroup( GroupName ) - self:F( Status ) + self:F2( Status ) if self.Templates.Groups[GroupName] then return self.Templates.Groups[GroupName].Status @@ -277,7 +277,7 @@ end -- @param #DATABASE self -- @param #table GroupTemplate -- @return #DATABASE self -function DATABASE:_RegisterGroup( GroupTemplate ) +function DATABASE:_RegisterTemplate( GroupTemplate ) local GroupTemplateName = env.getValueDictByKey(GroupTemplate.name) @@ -297,7 +297,7 @@ function DATABASE:_RegisterGroup( GroupTemplate ) self.Templates.Groups[GroupTemplateName].UnitCount = #GroupTemplate.units self.Templates.Groups[GroupTemplateName].Units = GroupTemplate.units - self:T( { "Group", self.Templates.Groups[GroupTemplateName].GroupName, self.Templates.Groups[GroupTemplateName].UnitCount } ) + self:T2( { "Group", self.Templates.Groups[GroupTemplateName].GroupName, self.Templates.Groups[GroupTemplateName].UnitCount } ) for unit_num, UnitTemplate in pairs( GroupTemplate.units ) do @@ -381,7 +381,7 @@ end -- @param #DATABASE self -- @param Event#EVENTDATA Event function DATABASE:_EventOnBirth( Event ) - self:F( { Event } ) + self:F2( { Event } ) if Event.IniDCSUnit then if self:_IsIncludeDCSUnit( Event.IniDCSUnit ) then @@ -396,7 +396,7 @@ end -- @param #DATABASE self -- @param Event#EVENTDATA Event function DATABASE:_EventOnDeadOrCrash( Event ) - self:F( { Event } ) + self:F2( { Event } ) if Event.IniDCSUnit then if self.DCSUnits[Event.IniDCSUnitName] then @@ -410,7 +410,7 @@ end -- @param #DATABASE self -- @param Event#EVENTDATA Event function DATABASE:_EventOnPlayerEnterUnit( Event ) - self:F( { Event } ) + self:F2( { Event } ) if Event.IniDCSUnit then if self:_IsIncludeDCSUnit( Event.IniDCSUnit ) then @@ -427,7 +427,7 @@ end -- @param #DATABASE self -- @param Event#EVENTDATA Event function DATABASE:_EventOnPlayerLeaveUnit( Event ) - self:F( { Event } ) + self:F2( { Event } ) if Event.IniDCSUnit then if self:_IsIncludeDCSUnit( Event.IniDCSUnit ) then @@ -447,7 +447,7 @@ end -- @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, arg, Set ) - self:F( arg ) + self:F2( arg ) local function CoRoutine() local Count = 0 @@ -467,7 +467,7 @@ function DATABASE:ForEach( IteratorFunction, arg, Set ) local function Schedule() local status, res = coroutine.resume( co ) - self:T( { status, res } ) + self:T2( { status, res } ) if status == false then error( res ) @@ -490,7 +490,7 @@ end -- @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:ForEachDCSUnit( IteratorFunction, ... ) - self:F( arg ) + self:F2( arg ) self:ForEach( IteratorFunction, arg, self.DCSUnits ) @@ -502,7 +502,7 @@ end -- @param #function IteratorFunction The function that will be called when there is an alive player in the database. The function needs to accept a UNIT parameter. -- @return #DATABASE self function DATABASE:ForEachPlayer( IteratorFunction, ... ) - self:F( arg ) + self:F2( arg ) self:ForEach( IteratorFunction, arg, self.PlayersAlive ) @@ -515,7 +515,7 @@ end -- @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:F( arg ) + self:F2( arg ) self:ForEach( IteratorFunction, arg, self.CLIENTS ) @@ -524,7 +524,7 @@ end function DATABASE:ScanEnvironment() - self:F() + self:F2() self.Navpoints = {} self.UNITS = {} @@ -574,7 +574,7 @@ function DATABASE:ScanEnvironment() 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:_RegisterGroup( GroupTemplate ) + self:_RegisterTemplate( GroupTemplate ) 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 @@ -598,13 +598,13 @@ end -- @param DCSUnit#Unit DCSUnit -- @return #DATABASE self function DATABASE:_IsIncludeDCSUnit( DCSUnit ) - self:F( DCSUnit ) + self:F2( DCSUnit ) local DCSUnitInclude = true if self.Filter.Coalitions then local DCSUnitCoalition = false for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do - self:T( { "Coalition:", DCSUnit:getCoalition(), self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) + self:T2( { "Coalition:", DCSUnit:getCoalition(), self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == DCSUnit:getCoalition() then DCSUnitCoalition = true end @@ -615,7 +615,7 @@ function DATABASE:_IsIncludeDCSUnit( DCSUnit ) if self.Filter.Categories then local DCSUnitCategory = false for CategoryID, CategoryName in pairs( self.Filter.Categories ) do - self:T( { "Category:", DCSUnit:getDesc().category, self.FilterMeta.Categories[CategoryName], CategoryName } ) + self:T2( { "Category:", DCSUnit:getDesc().category, self.FilterMeta.Categories[CategoryName], CategoryName } ) if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == DCSUnit:getDesc().category then DCSUnitCategory = true end @@ -626,7 +626,7 @@ function DATABASE:_IsIncludeDCSUnit( DCSUnit ) if self.Filter.Types then local DCSUnitType = false for TypeID, TypeName in pairs( self.Filter.Types ) do - self:T( { "Type:", DCSUnit:getTypeName(), TypeName } ) + self:T2( { "Type:", DCSUnit:getTypeName(), TypeName } ) if TypeName == DCSUnit:getTypeName() then DCSUnitType = true end @@ -637,7 +637,7 @@ function DATABASE:_IsIncludeDCSUnit( DCSUnit ) if self.Filter.Countries then local DCSUnitCountry = false for CountryID, CountryName in pairs( self.Filter.Countries ) do - self:T( { "Country:", DCSUnit:getCountry(), CountryName } ) + self:T2( { "Country:", DCSUnit:getCountry(), CountryName } ) if country.id[CountryName] == DCSUnit:getCountry() then DCSUnitCountry = true end @@ -648,7 +648,7 @@ function DATABASE:_IsIncludeDCSUnit( DCSUnit ) if self.Filter.UnitPrefixes then local DCSUnitPrefix = false for UnitPrefixId, UnitPrefix in pairs( self.Filter.UnitPrefixes ) do - self:T( { "Unit Prefix:", string.find( DCSUnit:getName(), UnitPrefix, 1 ), UnitPrefix } ) + self:T2( { "Unit Prefix:", string.find( DCSUnit:getName(), UnitPrefix, 1 ), UnitPrefix } ) if string.find( DCSUnit:getName(), UnitPrefix, 1 ) then DCSUnitPrefix = true end @@ -656,7 +656,7 @@ function DATABASE:_IsIncludeDCSUnit( DCSUnit ) DCSUnitInclude = DCSUnitInclude and DCSUnitPrefix end - self:T( DCSUnitInclude ) + self:T2( DCSUnitInclude ) return DCSUnitInclude end @@ -665,14 +665,14 @@ end -- @param DCSUnit#Unit DCSUnit -- @return #DATABASE self function DATABASE:_IsAliveDCSUnit( DCSUnit ) - self:F( DCSUnit ) + self:F2( DCSUnit ) local DCSUnitAlive = false if DCSUnit and DCSUnit:isExist() and DCSUnit:isActive() then if self.DCSUnits[DCSUnit:getName()] then DCSUnitAlive = true end end - self:T( DCSUnitAlive ) + self:T2( DCSUnitAlive ) return DCSUnitAlive end @@ -681,25 +681,15 @@ end -- @param DCSGroup#Group DCSGroup -- @return #DATABASE self function DATABASE:_IsAliveDCSGroup( DCSGroup ) - self:F( DCSGroup ) + self:F2( DCSGroup ) local DCSGroupAlive = false if DCSGroup and DCSGroup:isExist() then if self.DCSGroups[DCSGroup:getName()] then DCSGroupAlive = true end end - self:T( DCSGroupAlive ) + self:T2( DCSGroupAlive ) return DCSGroupAlive end ---- Traces the current database contents in the log ... (for debug reasons). --- @param #DATABASE self --- @return #DATABASE self -function DATABASE:TraceDatabase() - self:F() - - self:T( { "DCSUnits:", self.DCSUnits } ) -end - - diff --git a/Moose Development/Moose/Event.lua b/Moose Development/Moose/Event.lua index f7c2a0649..75cfca6ff 100644 --- a/Moose Development/Moose/Event.lua +++ b/Moose Development/Moose/Event.lua @@ -64,7 +64,7 @@ local _EVENTCODES = { function EVENT:New() local self = BASE:Inherit( self, BASE:New() ) - self:F() + self:F2() self.EventHandler = world.addEventHandler( self ) return self end @@ -154,7 +154,7 @@ end -- @param EventSelf The self instance of the class for which the event is. -- @return #EVENT function EVENT:OnBirthForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F( EventTemplate.name ) + self:F2( EventTemplate.name ) self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnBirthForUnit ) @@ -167,7 +167,7 @@ end -- @param Base#BASE EventSelf -- @return #EVENT function EVENT:OnBirth( EventFunction, EventSelf ) - self:F() + self:F2() self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_BIRTH ) @@ -181,7 +181,7 @@ end -- @param Base#BASE EventSelf -- @return #EVENT function EVENT:OnBirthForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F( EventDCSUnitName ) + self:F2( EventDCSUnitName ) self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_BIRTH ) @@ -195,7 +195,7 @@ end -- @param EventSelf The self instance of the class for which the event is. -- @return #EVENT function EVENT:OnCrashForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F( EventTemplate.name ) + self:F2( EventTemplate.name ) self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnCrashForUnit ) @@ -208,7 +208,7 @@ end -- @param Base#BASE EventSelf -- @return #EVENT function EVENT:OnCrash( EventFunction, EventSelf ) - self:F() + self:F2() self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_CRASH ) @@ -222,7 +222,7 @@ end -- @param Base#BASE EventSelf The self instance of the class for which the event is. -- @return #EVENT function EVENT:OnCrashForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F( EventDCSUnitName ) + self:F2( EventDCSUnitName ) self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_CRASH ) @@ -236,7 +236,7 @@ end -- @param EventSelf The self instance of the class for which the event is. -- @return #EVENT function EVENT:OnDeadForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F( EventTemplate.name ) + self:F2( EventTemplate.name ) self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnDeadForUnit ) @@ -249,7 +249,7 @@ end -- @param Base#BASE EventSelf -- @return #EVENT function EVENT:OnDead( EventFunction, EventSelf ) - self:F() + self:F2() self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_DEAD ) @@ -264,7 +264,7 @@ end -- @param Base#BASE EventSelf The self instance of the class for which the event is. -- @return #EVENT function EVENT:OnDeadForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F( EventDCSUnitName ) + self:F2( EventDCSUnitName ) self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_DEAD ) @@ -278,7 +278,7 @@ end -- @param Base#BASE EventSelf The self instance of the class for which the event is. -- @return #EVENT function EVENT:OnPilotDeadForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F( EventDCSUnitName ) + self:F2( EventDCSUnitName ) self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_PILOT_DEAD ) @@ -292,7 +292,7 @@ end -- @param EventSelf The self instance of the class for which the event is. -- @return #EVENT function EVENT:OnLandForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F( EventTemplate.name ) + self:F2( EventTemplate.name ) self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnLandForUnit ) @@ -306,7 +306,7 @@ end -- @param Base#BASE EventSelf The self instance of the class for which the event is. -- @return #EVENT function EVENT:OnLandForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F( EventDCSUnitName ) + self:F2( EventDCSUnitName ) self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_LAND ) @@ -320,7 +320,7 @@ end -- @param EventSelf The self instance of the class for which the event is. -- @return #EVENT function EVENT:OnTakeOffForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F( EventTemplate.name ) + self:F2( EventTemplate.name ) self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnTakeOffForUnit ) @@ -334,7 +334,7 @@ end -- @param Base#BASE EventSelf The self instance of the class for which the event is. -- @return #EVENT function EVENT:OnTakeOffForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F( EventDCSUnitName ) + self:F2( EventDCSUnitName ) self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_TAKEOFF ) @@ -348,7 +348,7 @@ end -- @param EventSelf The self instance of the class for which the event is. -- @return #EVENT function EVENT:OnEngineShutDownForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F( EventTemplate.name ) + self:F2( EventTemplate.name ) self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnEngineShutDownForUnit ) @@ -362,7 +362,7 @@ end -- @param Base#BASE EventSelf The self instance of the class for which the event is. -- @return #EVENT function EVENT:OnEngineShutDownForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F( EventDCSUnitName ) + self:F2( EventDCSUnitName ) self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_ENGINE_SHUTDOWN ) @@ -376,7 +376,7 @@ end -- @param Base#BASE EventSelf The self instance of the class for which the event is. -- @return #EVENT function EVENT:OnEngineStartUpForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F( EventDCSUnitName ) + self:F2( EventDCSUnitName ) self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_ENGINE_STARTUP ) @@ -389,7 +389,7 @@ end -- @param Base#BASE EventSelf The self instance of the class for which the event is. -- @return #EVENT function EVENT:OnShot( EventFunction, EventSelf ) - self:F() + self:F2() self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_SHOT ) @@ -403,7 +403,7 @@ end -- @param Base#BASE EventSelf The self instance of the class for which the event is. -- @return #EVENT function EVENT:OnShotForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F( EventDCSUnitName ) + self:F2( EventDCSUnitName ) self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_SHOT ) @@ -416,7 +416,7 @@ end -- @param Base#BASE EventSelf The self instance of the class for which the event is. -- @return #EVENT function EVENT:OnHit( EventFunction, EventSelf ) - self:F() + self:F2() self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_HIT ) @@ -430,7 +430,7 @@ end -- @param Base#BASE EventSelf The self instance of the class for which the event is. -- @return #EVENT function EVENT:OnHitForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F( EventDCSUnitName ) + self:F2( EventDCSUnitName ) self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_HIT ) @@ -443,7 +443,7 @@ end -- @param Base#BASE EventSelf The self instance of the class for which the event is. -- @return #EVENT function EVENT:OnPlayerEnterUnit( EventFunction, EventSelf ) - self:F() + self:F2() self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_PLAYER_ENTER_UNIT ) @@ -456,7 +456,7 @@ end -- @param Base#BASE EventSelf The self instance of the class for which the event is. -- @return #EVENT function EVENT:OnPlayerLeaveUnit( EventFunction, EventSelf ) - self:F() + self:F2() self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_PLAYER_LEAVE_UNIT ) @@ -466,7 +466,7 @@ end function EVENT:onEvent( Event ) - self:F( { _EVENTCODES[Event.id], Event } ) + self:F2( { _EVENTCODES[Event.id], Event } ) if self and self.Events and self.Events[Event.id] then if Event.initiator and Event.initiator:getCategory() == Object.Category.UNIT then diff --git a/Moose Development/Moose/Group.lua b/Moose Development/Moose/Group.lua index 89cf288b8..27e972843 100644 --- a/Moose Development/Moose/Group.lua +++ b/Moose Development/Moose/Group.lua @@ -198,6 +198,23 @@ function GROUP:GetCoalition() return nil end +--- Returns the country of the DCS Group. +-- @param #GROUP self +-- @return DCScountry#country.id The country identifier. +-- @return #nil The DCS Group is not existing or alive. +function GROUP:GetCountry() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + local GroupCountry = DCSGroup:getUnit(1):getCountry() + self:T3( GroupCountry ) + return GroupCountry + end + + return nil +end + --- Returns the name of the DCS Group. -- @param #GROUP self -- @return #string The DCS Group name. diff --git a/Moose Development/Moose/GroupSet.lua b/Moose Development/Moose/GroupSet.lua new file mode 100644 index 000000000..fb3f45fc5 --- /dev/null +++ b/Moose Development/Moose/GroupSet.lua @@ -0,0 +1,327 @@ +--- Create and manage a set of groups. +-- +-- @{#GROUPSET} class +-- ================== +-- Mission designers can use the GROUPSET class to build sets of groups belonging to certain: +-- +-- * Coalitions +-- * Categories +-- * Countries +-- * Starting with certain prefix strings. +-- +-- GROUPSET construction methods: +-- ================================= +-- Create a new GROUPSET object with the @{#GROUPSET.New} method: +-- +-- * @{#GROUPSET.New}: Creates a new GROUPSET object. +-- +-- +-- GROUPSET filter criteria: +-- ========================= +-- You can set filter criteria to define the set of groups within the GROUPSET. +-- Filter criteria are defined by: +-- +-- * @{#GROUPSET.FilterCoalitions}: Builds the GROUPSET with the groups belonging to the coalition(s). +-- * @{#GROUPSET.FilterCategories}: Builds the GROUPSET with the groups belonging to the category(ies). +-- * @{#GROUPSET.FilterTypes}: Builds the GROUPSET with the groups belonging to the unit type(s). +-- * @{#GROUPSET.FilterPrefixes}: Builds the GROUPSET with the groups starting with the same prefix string(s). +-- +-- Once the filter criteria have been set for the GROUPSET, you can start filtering using: +-- +-- * @{#GROUPSET.FilterStart}: Starts the filtering of the groups within the GROUPSET. +-- +-- Planned filter criteria within development are (so these are not yet available): +-- +-- * @{#GROUPSET.FilterZones}: Builds the GROUPSET with the groups within a @{Zone#ZONE}. +-- +-- +-- GROUPSET iterators: +-- =================== +-- Once the filters have been defined and the GROUPSET has been built, you can iterate the GROUPSET with the available iterator methods. +-- The iterator methods will walk the GROUPSET set, and call for each element within the set a function that you provide. +-- The following iterator methods are currently available within the GROUPSET: +-- +-- * @{#GROUPSET.ForEachGroup}: Calls a function for each alive unit it finds within the GROUPSET. +-- +-- Planned iterators methods in development are (so these are not yet available): +-- +-- * @{#GROUPSET.ForEachUnitInGroup}: Calls a function for each group contained within the GROUPSET. +-- * @{#GROUPSET.ForEachUnitInZone}: Calls a function for each unit within a certain zone contained within the GROUPSET. +-- +-- @module GroupSet +-- @author FlightControl + +Include.File( "Routines" ) +Include.File( "Base" ) +Include.File( "Group" ) +Include.File( "Set" ) + + +--- GROUPSET class +-- @type GROUPSET +-- @extends Set#SET +GROUPSET = { + ClassName = "GROUPSET", + Units = {}, + 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 GROUPSET object, building a set of groups belonging to a coalitions, categories, countries, types or with defined prefix names. +-- @param #GROUPSET self +-- @return #GROUPSET +-- @usage +-- -- Define a new GROUPSET Object. This DBObject will contain a reference to all alive GROUPS. +-- DBObject = GROUPSET:New() +function GROUPSET:New() + + -- Inherits from BASE + local self = BASE:Inherit( self, SET:New( _DATABASE.GROUPS ) ) + + return self +end + + +--- Finds a Unit based on the Unit Name. +-- @param #GROUPSET self +-- @param #string GroupName +-- @return Group#GROUP The found Unit. +function GROUPSET:FindUnit( 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 #GROUPSET self +-- @param #string Coalitions Can take the following values: "red", "blue", "neutral". +-- @return #GROUPSET self +function GROUPSET: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 #GROUPSET self +-- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship". +-- @return #GROUPSET self +function GROUPSET: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 #GROUPSET self +-- @param #string Countries Can take those country strings known within DCS world. +-- @return #GROUPSET self +function GROUPSET: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 unit prefixes. +-- All the groups starting with the given prefixes will be included within the set. +-- @param #GROUPSET self +-- @param #string Prefixes The prefix of which the group name starts with. +-- @return #GROUPSET self +function GROUPSET: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 #GROUPSET self +-- @return #GROUPSET self +function GROUPSET: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 birth event! +-- @param #GROUPSET self +-- @param Event#EVENTDATA Event +-- @return #string The name of the GROUP +-- @return #table The GROUP +function GROUPSET:AddInDatabase( Event ) + self:F3( { Event } ) + + if not self.Database[Event.IniDCSGroupName] then + self.Database[Event.IniDCSGroupName] = GROUP:Register( Event.IniDCSGroupName ) + self:T3( self.Database[Event.IniDCSGroupName] ) + end + + return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName] +end + +--- Handles the Database to check on any event that Object exists in the Database. +-- This is required, because sometimes the _DATABASE event gets called later than the SET event or vise versa! +-- @param #GROUPSET self +-- @param Event#EVENTDATA Event +-- @return #string The name of the GROUP +-- @return #table The GROUP +function GROUPSET:FindInDatabase( Event ) + self:F3( { Event } ) + + return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName] +end + +--- Interate the GROUPSET and call an interator function for each **alive** GROUP, providing the GROUP and optional parameters. +-- @param #GROUPSET self +-- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the GROUPSET. The function needs to accept a GROUP parameter. +-- @return #GROUPSET self +function GROUPSET:ForEachUnit( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.Set ) + + return self +end + + +----- Interate the GROUPSET and call an interator function for each **alive** player, providing the Unit of the player and optional parameters. +---- @param #GROUPSET self +---- @param #function IteratorFunction The function that will be called when there is an alive player in the GROUPSET. The function needs to accept a GROUP parameter. +---- @return #GROUPSET self +--function GROUPSET:ForEachPlayer( IteratorFunction, ... ) +-- self:F2( arg ) +-- +-- self:ForEach( IteratorFunction, arg, self.PlayersAlive ) +-- +-- return self +--end +-- +-- +----- Interate the GROUPSET and call an interator function for each client, providing the Client to the function and optional parameters. +---- @param #GROUPSET self +---- @param #function IteratorFunction The function that will be called when there is an alive player in the GROUPSET. The function needs to accept a CLIENT parameter. +---- @return #GROUPSET self +--function GROUPSET:ForEachClient( IteratorFunction, ... ) +-- self:F2( arg ) +-- +-- self:ForEach( IteratorFunction, arg, self.Clients ) +-- +-- return self +--end + + +--- +-- @param #GROUPSET self +-- @param Group#GROUP MooseGroup +-- @return #GROUPSET self +function GROUPSET: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 + + diff --git a/Moose Development/Moose/Set.lua b/Moose Development/Moose/Set.lua index 0ac45ae1a..6b0ba0260 100644 --- a/Moose Development/Moose/Set.lua +++ b/Moose Development/Moose/Set.lua @@ -75,66 +75,10 @@ Include.File( "Client" ) -- @extends Base#BASE SET = { ClassName = "SET", - Templates = { - Units = {}, - Groups = {}, - ClientsByName = {}, - ClientsByID = {}, - }, - DCSUnits = {}, - DCSUnitsAlive = {}, - DCSGroups = {}, - DCSGroupsAlive = {}, - Units = {}, - UnitsAlive = {}, - Groups = {}, - GroupsAlive = {}, - NavPoints = {}, - Statics = {}, - Players = {}, - PlayersAlive = {}, - Clients = {}, - ClientsAlive = {}, - Filter = { - Coalitions = nil, - Categories = nil, - Types = nil, - Countries = nil, - UnitPrefixes = nil, - GroupPrefixes = 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, - }, - }, + Set = {}, + Database = {}, } -local _DATABASECoalition = - { - [1] = "Red", - [2] = "Blue", - } - -local _DATABASECategory = - { - [Unit.Category.AIRPLANE] = "Plane", - [Unit.Category.HELICOPTER] = "Helicopter", - [Unit.Category.GROUND_UNIT] = "Vehicle", - [Unit.Category.SHIP] = "Ship", - [Unit.Category.STRUCTURE] = "Structure", - } - - --- Creates a new SET object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. -- @param #SET self -- @return #SET @@ -146,21 +90,7 @@ function SET:New( Database ) -- Inherits from BASE local self = BASE:Inherit( self, BASE:New() ) - _EVENTDISPATCHER:OnBirth( self._EventOnBirth, self ) - _EVENTDISPATCHER:OnDead( self._EventOnDeadOrCrash, self ) - _EVENTDISPATCHER:OnCrash( self._EventOnDeadOrCrash, self ) - - - -- Add SET with registered clients and already alive players - - -- Follow alive players and clients - _EVENTDISPATCHER:OnPlayerEnterUnit( self._EventOnPlayerEnterUnit, self ) - _EVENTDISPATCHER:OnPlayerLeaveUnit( self._EventOnPlayerLeaveUnit, self ) - - self.Collection = Database - - self:_RegisterSet() - self:_RegisterPlayers() + self.Database = Database return self end @@ -190,7 +120,7 @@ end -- @return #SET self function SET:_FilterStart() - for ObjectName, Object in pairs( self.Collection ) do + for ObjectName, Object in pairs( self.Database ) do if self:IsIncludeObject( Object ) then self:E( { "Adding Object:", ObjectName } ) @@ -198,43 +128,53 @@ function SET:_FilterStart() end end - return self -end - - - ---- Private method that registers all alive players in the mission. --- @param #SET self --- @return #SET self -function SET:_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 + _EVENTDISPATCHER:OnBirth( self._EventOnBirth, self ) + _EVENTDISPATCHER:OnDead( self._EventOnDeadOrCrash, self ) + _EVENTDISPATCHER:OnCrash( self._EventOnDeadOrCrash, self ) + + -- Follow alive players and clients +-- _EVENTDISPATCHER:OnPlayerEnterUnit( self._EventOnPlayerEnterUnit, self ) +-- _EVENTDISPATCHER:OnPlayerLeaveUnit( self._EventOnPlayerLeaveUnit, self ) + return self end + + +----- Private method that registers all alive players in the mission. +---- @param #SET self +---- @return #SET self +--function SET:_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 self -- @param Event#EVENTDATA Event function SET:_EventOnBirth( Event ) - self:F( { 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 ) @@ -246,7 +186,7 @@ end -- @param #SET self -- @param Event#EVENTDATA Event function SET:_EventOnDeadOrCrash( Event ) - self:F( { Event } ) + self:F3( { Event } ) if Event.IniDCSUnit then local ObjectName, Object = self:FindInDatabase( Event ) @@ -260,7 +200,7 @@ end ---- @param #SET self ---- @param Event#EVENTDATA Event --function SET:_EventOnPlayerEnterUnit( Event ) --- self:F( { Event } ) +-- self:F3( { Event } ) -- -- if Event.IniDCSUnit then -- if self:IsIncludeObject( Event.IniDCSUnit ) then @@ -277,7 +217,7 @@ end ---- @param #SET self ---- @param Event#EVENTDATA Event --function SET:_EventOnPlayerLeaveUnit( Event ) --- self:F( { Event } ) +-- self:F3( { Event } ) -- -- if Event.IniDCSUnit then -- if self:IsIncludeObject( Event.IniDCSUnit ) then @@ -297,7 +237,7 @@ end -- @param #function IteratorFunction The function that will be called when there is an alive player in the SET. -- @return #SET self function SET:ForEach( IteratorFunction, arg, Set ) - self:F( arg ) + self:F3( arg ) local function CoRoutine() local Count = 0 @@ -317,7 +257,7 @@ function SET:ForEach( IteratorFunction, arg, Set ) local function Schedule() local status, res = coroutine.resume( co ) - self:T( { status, res } ) + self:T3( { status, res } ) if status == false then error( res ) @@ -335,42 +275,42 @@ function SET:ForEach( IteratorFunction, arg, Set ) end ---- Interate the SET and call an interator function for each **alive** unit, providing the Unit and optional parameters. --- @param #SET self --- @param #function IteratorFunction The function that will be called when there is an alive unit in the SET. The function needs to accept a UNIT parameter. --- @return #SET self -function SET:ForEachDCSUnitAlive( IteratorFunction, ... ) - self:F( arg ) - - self:ForEach( IteratorFunction, arg, self.DCSUnitsAlive ) - - return self -end - ---- Interate the SET and call an interator function for each **alive** player, providing the Unit of the player and optional parameters. --- @param #SET self --- @param #function IteratorFunction The function that will be called when there is an alive player in the SET. The function needs to accept a UNIT parameter. --- @return #SET self -function SET:ForEachPlayer( IteratorFunction, ... ) - self:F( arg ) - - self:ForEach( IteratorFunction, arg, self.PlayersAlive ) - - return self -end - - ---- Interate the SET and call an interator function for each client, providing the Client to the function and optional parameters. --- @param #SET self --- @param #function IteratorFunction The function that will be called when there is an alive player in the SET. The function needs to accept a CLIENT parameter. --- @return #SET self -function SET:ForEachClient( IteratorFunction, ... ) - self:F( arg ) - - self:ForEach( IteratorFunction, arg, self.Clients ) - - return self -end +----- Interate the SET and call an interator function for each **alive** unit, providing the Unit and optional parameters. +---- @param #SET self +---- @param #function IteratorFunction The function that will be called when there is an alive unit in the SET. The function needs to accept a UNIT parameter. +---- @return #SET self +--function SET:ForEachDCSUnitAlive( IteratorFunction, ... ) +-- self:F3( arg ) +-- +-- self:ForEach( IteratorFunction, arg, self.DCSUnitsAlive ) +-- +-- return self +--end +-- +----- Interate the SET and call an interator function for each **alive** player, providing the Unit of the player and optional parameters. +---- @param #SET self +---- @param #function IteratorFunction The function that will be called when there is an alive player in the SET. The function needs to accept a UNIT parameter. +---- @return #SET self +--function SET:ForEachPlayer( IteratorFunction, ... ) +-- self:F3( arg ) +-- +-- self:ForEach( IteratorFunction, arg, self.PlayersAlive ) +-- +-- return self +--end +-- +-- +----- Interate the SET and call an interator function for each client, providing the Client to the function and optional parameters. +---- @param #SET self +---- @param #function IteratorFunction The function that will be called when there is an alive player in the SET. The function needs to accept a CLIENT parameter. +---- @return #SET self +--function SET:ForEachClient( IteratorFunction, ... ) +-- self:F3( arg ) +-- +-- self:ForEach( IteratorFunction, arg, self.Clients ) +-- +-- return self +--end --- Decides whether to include the Object @@ -378,53 +318,22 @@ end -- @param #table Object -- @return #SET self function SET:IsIncludeObject( Object ) - self:F( Object ) + self:F3( Object ) return true end - ---- +--- Flushes the current SET contents in the log ... (for debug reasons). -- @param #SET self --- @param DCSUnit#Unit DCSUnit -- @return #SET self -function SET:_IsAliveDCSUnit( DCSUnit ) - self:F( DCSUnit ) - local DCSUnitAlive = false - if DCSUnit and DCSUnit:isExist() and DCSUnit:isActive() then - if self.DCSUnits[DCSUnit:getName()] then - DCSUnitAlive = true - end +function SET:Flush() + self:F3() + + local ObjectNames = "" + for ObjectName, Object in pairs( self.Set ) do + ObjectNames = ObjectNames .. ObjectName .. ", " end - self:T( DCSUnitAlive ) - return DCSUnitAlive -end - ---- --- @param #SET self --- @param DCSGroup#Group DCSGroup --- @return #SET self -function SET:_IsAliveDCSGroup( DCSGroup ) - self:F( DCSGroup ) - local DCSGroupAlive = false - if DCSGroup and DCSGroup:isExist() then - if self.DCSGroups[DCSGroup:getName()] then - DCSGroupAlive = true - end - end - self:T( DCSGroupAlive ) - return DCSGroupAlive -end - - ---- Traces the current SET contents in the log ... (for debug reasons). --- @param #SET self --- @return #SET self -function SET:TraceDatabase() - self:F() - - self:T( { "DCSUnits:", self.DCSUnits } ) - self:T( { "DCSUnitsAlive:", self.DCSUnitsAlive } ) + self:T( { "Objects in Set:", ObjectNames } ) end diff --git a/Moose Development/Moose/UnitSet.lua b/Moose Development/Moose/UnitSet.lua index 188d59a73..5bd542727 100644 --- a/Moose Development/Moose/UnitSet.lua +++ b/Moose Development/Moose/UnitSet.lua @@ -10,12 +10,6 @@ -- * Unit types -- * Starting with certain prefix strings. -- --- This list will grow over time. Planned developments are to include filters and iterators. --- Additional filters will be added around @{Zone#ZONEs}, Radiuses, Active players, ... --- More iterators will be implemented in the near future ... --- --- Administers the Initial Sets of the Mission Templates as defined within the Mission Editor. --- -- UNITSET construction methods: -- ================================= -- Create a new UNITSET object with the @{#UNITSET.New} method: @@ -56,16 +50,13 @@ -- * @{#UNITSET.ForEachUnitInGroup}: Calls a function for each group contained within the UNITSET. -- * @{#UNITSET.ForEachUnitInZone}: Calls a function for each unit within a certain zone contained within the UNITSET. -- --- @module Set +-- @module UnitSet -- @author FlightControl Include.File( "Routines" ) Include.File( "Base" ) -Include.File( "Menu" ) -Include.File( "Group" ) Include.File( "Unit" ) -Include.File( "Event" ) -Include.File( "Client" ) +Include.File( "Set" ) --- UNITSET class @@ -97,21 +88,6 @@ UNITSET = { }, } -local _DATABASECoalition = - { - [1] = "Red", - [2] = "Blue", - } - -local _DATABASECategory = - { - [Unit.Category.AIRPLANE] = "Plane", - [Unit.Category.HELICOPTER] = "Helicopter", - [Unit.Category.GROUND_UNIT] = "Vehicle", - [Unit.Category.SHIP] = "Ship", - [Unit.Category.STRUCTURE] = "Structure", - } - --- Creates a new UNITSET object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. -- @param #UNITSET self @@ -122,42 +98,25 @@ local _DATABASECategory = function UNITSET:New() -- Inherits from BASE - local self = BASE:Inherit( self, SET:New( _DATABASE.Units ) ) - - - --- -- Follow alive players and clients --- _EVENTDISPATCHER:OnPlayerEnterUnit( self._EventOnPlayerEnterUnit, self ) --- _EVENTDISPATCHER:OnPlayerLeaveUnit( self._EventOnPlayerLeaveUnit, self ) - --- self:_RegisterPlayers() + local self = BASE:Inherit( self, SET:New( _DATABASE.UNITS ) ) return self end + --- Finds a Unit based on the Unit Name. -- @param #UNITSET self -- @param #string UnitName -- @return Unit#UNIT The found Unit. function UNITSET:FindUnit( UnitName ) - local UnitFound = self.Units[UnitName] + local UnitFound = self.Set[UnitName] return UnitFound end ---- Finds a Unit based on the Unit Name. --- @param #UNITSET self --- @param Unit#UNIT UnitName --- @param Unit#UNIT UnitData --- @return Unit#UNIT The added Unit. -function UNITSET:_AddUnit( UnitName, UnitData ) - - self.Units[UnitName] = _DATABASE:FindUnit( UnitName ) -end - ---- Builds a set of units of coalitons. +--- Builds a set of units of coalitions. -- Possible current coalitions are red, blue and neutral. -- @param #UNITSET self -- @param #string Coalitions Can take the following values: "red", "blue", "neutral". @@ -252,23 +211,6 @@ function UNITSET:FilterPrefixes( Prefixes ) end ------ Builds a set of units of defined group prefixes. ----- All the units starting with the given group prefixes will be included within the set. ----- @param #UNITSET self ----- @param #string Prefixes The prefix of which the group name where the unit belongs to starts with. ----- @return #UNITSET self ---function UNITSET:FilterGroupPrefixes( 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. @@ -277,173 +219,78 @@ end function UNITSET:FilterStart() if _DATABASE then - self:_FilterStart( self.DatabaseCollection ) + self:_FilterStart() end - FollowEventBirth( ) - - _EVENTDISPATCHER:OnBirth( self._EventOnBirth, self ) - _EVENTDISPATCHER:OnDead( self._EventOnDeadOrCrash, self ) - _EVENTDISPATCHER:OnCrash( self._EventOnDeadOrCrash, self ) - return self end ---- Events - ---- Handles the OnBirth event for the alive units set. +--- 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 birth event! -- @param #UNITSET self -- @param Event#EVENTDATA Event -function UNITSET:_EventOnBirth( Event ) - self:F( { Event } ) +-- @return #string The name of the UNIT +-- @return #table The UNIT +function UNITSET:AddInDatabase( Event ) + self:F3( { Event } ) - if Event.IniDCSUnit then - if self:_IsIncludeUnit( Event.IniDCSUnit ) then - self.DCSUnits[Event.IniDCSUnitName] = Event.IniDCSUnit - self.DCSUnitsAlive[Event.IniDCSUnitName] = Event.IniDCSUnit - self:_AddUnit( UNIT:Register( Event.IniDCSUnit ) ) - --self.Units[Event.IniDCSUnitName] = UNIT:Register( Event.IniDCSUnit ) - - --if not self.DCSGroups[Event.IniDCSGroupName] then - -- self.DCSGroups[Event.IniDCSGroupName] = Event.IniDCSGroupName - -- self.DCSGroupsAlive[Event.IniDCSGroupName] = Event.IniDCSGroupName - -- self.Groups[Event.IniDCSGroupName] = GROUP:New( Event.IniDCSGroup ) - --end - self:_EventOnPlayerEnterUnit( Event ) - end + if not self.Database[Event.IniDCSUnitName] then + self.Database[Event.IniDCSUnitName] = UNIT:Register( Event.IniDCSUnitName ) + self:T3( self.Database[Event.IniDCSUnitName] ) end + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] end ---- Handles the OnDead or OnCrash event for alive units set. +--- 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 event or vise versa! -- @param #UNITSET self -- @param Event#EVENTDATA Event -function UNITSET:_EventOnDeadOrCrash( Event ) - self:F( { Event } ) +-- @return #string The name of the UNIT +-- @return #table The UNIT +function UNITSET:FindInDatabase( Event ) + self:F3( { Event } ) - if Event.IniDCSUnit then - if self.DCSUnitsAlive[Event.IniDCSUnitName] then - self.DCSUnits[Event.IniDCSUnitName] = nil - self.DCSUnitsAlive[Event.IniDCSUnitName] = nil - end - end + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] end ---- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). +--- Interate the UNITSET and call an interator function for each **alive** UNIT, providing the UNIT and optional parameters. -- @param #UNITSET self --- @param Event#EVENTDATA Event -function UNITSET:_EventOnPlayerEnterUnit( Event ) - self:F( { Event } ) - - if Event.IniDCSUnit then - if self:_IsIncludeUnit( Event.IniDCSUnit ) then - if not self.PlayersAlive[Event.IniDCSUnitName] then - self:E( { "Add player for unit:", Event.IniDCSUnitName, Event.IniDCSUnit:getPlayerName() } ) - self.PlayersAlive[Event.IniDCSUnitName] = Event.IniDCSUnit:getPlayerName() - self.ClientsAlive[Event.IniDCSUnitName] = _DATABASE.Clients[ Event.IniDCSUnitName ] - end - end - end -end - ---- Handles the OnPlayerLeaveUnit event to clean the active players table. --- @param #UNITSET self --- @param Event#EVENTDATA Event -function UNITSET:_EventOnPlayerLeaveUnit( Event ) - self:F( { Event } ) - - if Event.IniDCSUnit then - if self:_IsIncludeUnit( Event.IniDCSUnit ) then - if self.PlayersAlive[Event.IniDCSUnitName] then - self:E( { "Cleaning player for unit:", Event.IniDCSUnitName, Event.IniDCSUnit:getPlayerName() } ) - self.PlayersAlive[Event.IniDCSUnitName] = nil - self.ClientsAlive[Event.IniDCSUnitName] = nil - end - end - end -end - ---- Iterators - ---- Interate the UNITSET and call an interator function for the given set, providing the Object for each element within the set and optional parameters. --- @param #UNITSET self --- @param #function IteratorFunction The function that will be called when there is an alive player in the UNITSET. +-- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the UNITSET. The function needs to accept a UNIT parameter. -- @return #UNITSET self -function UNITSET:ForEach( IteratorFunction, arg, Set ) - self:F( arg ) +function UNITSET:ForEachUnit( IteratorFunction, ... ) + 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 % 10 == 0 then - coroutine.yield( false ) - end - end - return true - end - - local co = coroutine.create( CoRoutine ) - - local function Schedule() - - local status, res = coroutine.resume( co ) - self:T( { status, res } ) - - if status == false then - error( res ) - end - if res == false then - return true -- resume next time the loop - end - - return false - end - - local Scheduler = SCHEDULER:New( self, Schedule, {}, 0.001, 0.001, 0 ) - - return self -end - - ---- Interate the UNITSET and call an interator function for each **alive** unit, providing the Unit and optional parameters. --- @param #UNITSET self --- @param #function IteratorFunction The function that will be called when there is an alive unit in the UNITSET. The function needs to accept a UNIT parameter. --- @return #UNITSET self -function UNITSET:ForEachDCSUnitAlive( IteratorFunction, ... ) - self:F( arg ) - - self:ForEach( IteratorFunction, arg, self.DCSUnitsAlive ) + self:ForEach( IteratorFunction, arg, self.Set ) return self end ---- Interate the UNITSET and call an interator function for each **alive** player, providing the Unit of the player and optional parameters. --- @param #UNITSET self --- @param #function IteratorFunction The function that will be called when there is an alive player in the UNITSET. The function needs to accept a UNIT parameter. --- @return #UNITSET self -function UNITSET:ForEachPlayer( IteratorFunction, ... ) - self:F( arg ) - - self:ForEach( IteratorFunction, arg, self.PlayersAlive ) - - return self -end - - ---- Interate the UNITSET and call an interator function for each client, providing the Client to the function and optional parameters. --- @param #UNITSET self --- @param #function IteratorFunction The function that will be called when there is an alive player in the UNITSET. The function needs to accept a CLIENT parameter. --- @return #UNITSET self -function UNITSET:ForEachClient( IteratorFunction, ... ) - self:F( arg ) - - self:ForEach( IteratorFunction, arg, self.Clients ) - - return self -end +----- Interate the UNITSET and call an interator function for each **alive** player, providing the Unit of the player and optional parameters. +---- @param #UNITSET self +---- @param #function IteratorFunction The function that will be called when there is an alive player in the UNITSET. The function needs to accept a UNIT parameter. +---- @return #UNITSET self +--function UNITSET:ForEachPlayer( IteratorFunction, ... ) +-- self:F2( arg ) +-- +-- self:ForEach( IteratorFunction, arg, self.PlayersAlive ) +-- +-- return self +--end +-- +-- +----- Interate the UNITSET and call an interator function for each client, providing the Client to the function and optional parameters. +---- @param #UNITSET self +---- @param #function IteratorFunction The function that will be called when there is an alive player in the UNITSET. The function needs to accept a CLIENT parameter. +---- @return #UNITSET self +--function UNITSET:ForEachClient( IteratorFunction, ... ) +-- self:F2( arg ) +-- +-- self:ForEach( IteratorFunction, arg, self.Clients ) +-- +-- return self +--end --- @@ -451,13 +298,13 @@ end -- @param Unit#UNIT MUnit -- @return #UNITSET self function UNITSET:IsIncludeObject( MUnit ) - self:F( 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:T( { "Coalition:", MUnit:GetCoalition(), self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) + 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 @@ -468,7 +315,7 @@ function UNITSET:IsIncludeObject( MUnit ) if self.Filter.Categories then local MUnitCategory = false for CategoryID, CategoryName in pairs( self.Filter.Categories ) do - self:T( { "Category:", MUnit:GetDesc().category, self.FilterMeta.Categories[CategoryName], CategoryName } ) + 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 @@ -479,7 +326,7 @@ function UNITSET:IsIncludeObject( MUnit ) if self.Filter.Types then local MUnitType = false for TypeID, TypeName in pairs( self.Filter.Types ) do - self:T( { "Type:", MUnit:GetTypeName(), TypeName } ) + self:T3( { "Type:", MUnit:GetTypeName(), TypeName } ) if TypeName == MUnit:GetTypeName() then MUnitType = true end @@ -490,7 +337,7 @@ function UNITSET:IsIncludeObject( MUnit ) if self.Filter.Countries then local MUnitCountry = false for CountryID, CountryName in pairs( self.Filter.Countries ) do - self:T( { "Country:", MUnit:GetCountry(), CountryName } ) + self:T3( { "Country:", MUnit:GetCountry(), CountryName } ) if country.id[CountryName] == MUnit:GetCountry() then MUnitCountry = true end @@ -501,7 +348,7 @@ function UNITSET:IsIncludeObject( MUnit ) if self.Filter.UnitPrefixes then local MUnitPrefix = false for UnitPrefixId, UnitPrefix in pairs( self.Filter.UnitPrefixes ) do - self:T( { "Unit Prefix:", string.find( MUnit:GetName(), UnitPrefix, 1 ), UnitPrefix } ) + self:T3( { "Prefix:", string.find( MUnit:GetName(), UnitPrefix, 1 ), UnitPrefix } ) if string.find( MUnit:GetName(), UnitPrefix, 1 ) then MUnitPrefix = true end @@ -509,7 +356,7 @@ function UNITSET:IsIncludeObject( MUnit ) MUnitInclude = MUnitInclude and MUnitPrefix end - self:T( MUnitInclude ) + self:T2( MUnitInclude ) return MUnitInclude end diff --git a/Moose Mission Setup/Moose Mission Update/l10n/DEFAULT/Moose.lua b/Moose Mission Setup/Moose Mission Update/l10n/DEFAULT/Moose.lua index 8f1d04ce8..d4c566d6b 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,6 @@ -env.info( '*** MOOSE STATIC INCLUDE START *** ' ) -env.info( 'Moose Generation Timestamp: 20160526_1413' ) +env.info( '*** MOOSE DYNAMIC INCLUDE START *** ' ) +env.info( 'Moose Generation Timestamp: 20160527_1003' ) + local base = _G env.info("Loading MOOSE " .. base.timer.getAbsTime() ) @@ -11,9 +12,27 @@ Include.Path = function() end Include.File = function( IncludeFile ) + if not Include.Files[ IncludeFile ] then + Include.Files[IncludeFile] = IncludeFile + env.info( "Include:" .. IncludeFile .. " from " .. Include.ProgramPath ) + local f = assert( base.loadfile( Include.ProgramPath .. IncludeFile .. ".lua" ) ) + if f == nil then + env.info( "Include:" .. IncludeFile .. " from " .. Include.MissionPath ) + local f = assert( base.loadfile( Include.MissionPath .. IncludeFile .. ".lua" ) ) + if f == nil then + error ("Could not load MOOSE file " .. IncludeFile .. ".lua" ) + else + env.info( "Include:" .. IncludeFile .. " loaded from " .. Include.MissionPath ) + return f() + end + else + env.info( "Include:" .. IncludeFile .. " loaded from " .. Include.ProgramPath ) + return f() + end + end end -Include.ProgramPath = "Scripts/Moose/Moose/" +Include.ProgramPath = "Scripts/Moose/" Include.MissionPath = Include.Path() env.info( "Include.ProgramPath = " .. Include.ProgramPath) @@ -23,16701 +42,4 @@ Include.Files = {} Include.File( "Moose" ) -env.info("Loaded MOOSE Include Engine") ---- Various routines --- @module routines --- @author Flightcontrol - ---Include.File( "Trace" ) ---Include.File( "Message" ) - - -env.setErrorMessageBoxEnabled(false) - ---- Extract of MIST functions. --- @author Grimes - -routines = {} - - --- don't change these -routines.majorVersion = 3 -routines.minorVersion = 3 -routines.build = 22 - ------------------------------------------------------------------------------------------------------------------ - ----------------------------------------------------------------------------------------------- --- Utils- conversion, Lua utils, etc. -routines.utils = {} - ---from http://lua-users.org/wiki/CopyTable -routines.utils.deepCopy = function(object) - local lookup_table = {} - local function _copy(object) - if type(object) ~= "table" then - return object - elseif lookup_table[object] then - return lookup_table[object] - end - local new_table = {} - lookup_table[object] = new_table - for index, value in pairs(object) do - new_table[_copy(index)] = _copy(value) - end - return setmetatable(new_table, getmetatable(object)) - end - local objectreturn = _copy(object) - return objectreturn -end - - --- porting in Slmod's serialize_slmod2 -routines.utils.oneLineSerialize = function(tbl) -- serialization of a table all on a single line, no comments, made to replace old get_table_string function - - lookup_table = {} - - local function _Serialize( tbl ) - - if type(tbl) == 'table' then --function only works for tables! - - if lookup_table[tbl] then - return lookup_table[object] - end - - local tbl_str = {} - - lookup_table[tbl] = tbl_str - - tbl_str[#tbl_str + 1] = '{' - - for ind,val in pairs(tbl) do -- serialize its fields - local ind_str = {} - if type(ind) == "number" then - ind_str[#ind_str + 1] = '[' - ind_str[#ind_str + 1] = tostring(ind) - ind_str[#ind_str + 1] = ']=' - else --must be a string - ind_str[#ind_str + 1] = '[' - ind_str[#ind_str + 1] = routines.utils.basicSerialize(ind) - ind_str[#ind_str + 1] = ']=' - end - - local val_str = {} - if ((type(val) == 'number') or (type(val) == 'boolean')) then - val_str[#val_str + 1] = tostring(val) - val_str[#val_str + 1] = ',' - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - elseif type(val) == 'string' then - val_str[#val_str + 1] = routines.utils.basicSerialize(val) - val_str[#val_str + 1] = ',' - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - elseif type(val) == 'nil' then -- won't ever happen, right? - val_str[#val_str + 1] = 'nil,' - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - elseif type(val) == 'table' then - if ind == "__index" then - -- tbl_str[#tbl_str + 1] = "__index" - -- tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it - else - - val_str[#val_str + 1] = _Serialize(val) - val_str[#val_str + 1] = ',' --I think this is right, I just added it - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - end - elseif type(val) == 'function' then - -- tbl_str[#tbl_str + 1] = "function " .. tostring(ind) - -- tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it - else --- env.info('unable to serialize value type ' .. routines.utils.basicSerialize(type(val)) .. ' at index ' .. tostring(ind)) --- env.info( debug.traceback() ) - end - - end - tbl_str[#tbl_str + 1] = '}' - return table.concat(tbl_str) - else - return tostring(tbl) - end - end - - local objectreturn = _Serialize(tbl) - return objectreturn -end - ---porting in Slmod's "safestring" basic serialize -routines.utils.basicSerialize = function(s) - if s == nil then - return "\"\"" - else - if ((type(s) == 'number') or (type(s) == 'boolean') or (type(s) == 'function') or (type(s) == 'table') or (type(s) == 'userdata') ) then - return tostring(s) - elseif type(s) == 'string' then - s = string.format('%q', s) - return s - end - end -end - - -routines.utils.toDegree = function(angle) - return angle*180/math.pi -end - -routines.utils.toRadian = function(angle) - return angle*math.pi/180 -end - -routines.utils.metersToNM = function(meters) - return meters/1852 -end - -routines.utils.metersToFeet = function(meters) - return meters/0.3048 -end - -routines.utils.NMToMeters = function(NM) - return NM*1852 -end - -routines.utils.feetToMeters = function(feet) - return feet*0.3048 -end - -routines.utils.mpsToKnots = function(mps) - return mps*3600/1852 -end - -routines.utils.mpsToKmph = function(mps) - return mps*3.6 -end - -routines.utils.knotsToMps = function(knots) - return knots*1852/3600 -end - -routines.utils.kmphToMps = function(kmph) - return kmph/3.6 -end - -function routines.utils.makeVec2(Vec3) - if Vec3.z then - return {x = Vec3.x, y = Vec3.z} - else - return {x = Vec3.x, y = Vec3.y} -- it was actually already vec2. - end -end - -function routines.utils.makeVec3(Vec2, y) - if not Vec2.z then - if not y then - y = 0 - end - return {x = Vec2.x, y = y, z = Vec2.y} - else - return {x = Vec2.x, y = Vec2.y, z = Vec2.z} -- it was already Vec3, actually. - end -end - -function routines.utils.makeVec3GL(Vec2, offset) - local adj = offset or 0 - - if not Vec2.z then - return {x = Vec2.x, y = (land.getHeight(Vec2) + adj), z = Vec2.y} - else - return {x = Vec2.x, y = (land.getHeight({x = Vec2.x, y = Vec2.z}) + adj), z = Vec2.z} - end -end - -routines.utils.zoneToVec3 = function(zone) - local new = {} - if type(zone) == 'table' and zone.point then - new.x = zone.point.x - new.y = zone.point.y - new.z = zone.point.z - return new - elseif type(zone) == 'string' then - zone = trigger.misc.getZone(zone) - if zone then - new.x = zone.point.x - new.y = zone.point.y - new.z = zone.point.z - return new - end - end -end - --- gets heading-error corrected direction from point along vector vec. -function routines.utils.getDir(vec, point) - local dir = math.atan2(vec.z, vec.x) - dir = dir + routines.getNorthCorrection(point) - if dir < 0 then - dir = dir + 2*math.pi -- put dir in range of 0 to 2*pi - end - return dir -end - --- gets distance in meters between two points (2 dimensional) -function routines.utils.get2DDist(point1, point2) - point1 = routines.utils.makeVec3(point1) - point2 = routines.utils.makeVec3(point2) - return routines.vec.mag({x = point1.x - point2.x, y = 0, z = point1.z - point2.z}) -end - --- gets distance in meters between two points (3 dimensional) -function routines.utils.get3DDist(point1, point2) - return routines.vec.mag({x = point1.x - point2.x, y = point1.y - point2.y, z = point1.z - point2.z}) -end - - - --- From http://lua-users.org/wiki/SimpleRound --- use negative idp for rounding ahead of decimal place, positive for rounding after decimal place -routines.utils.round = function(num, idp) - local mult = 10^(idp or 0) - return math.floor(num * mult + 0.5) / mult -end - --- porting in Slmod's dostring -routines.utils.dostring = function(s) - local f, err = loadstring(s) - if f then - return true, f() - else - return false, err - end -end - - ---3D Vector manipulation -routines.vec = {} - -routines.vec.add = function(vec1, vec2) - return {x = vec1.x + vec2.x, y = vec1.y + vec2.y, z = vec1.z + vec2.z} -end - -routines.vec.sub = function(vec1, vec2) - return {x = vec1.x - vec2.x, y = vec1.y - vec2.y, z = vec1.z - vec2.z} -end - -routines.vec.scalarMult = function(vec, mult) - return {x = vec.x*mult, y = vec.y*mult, z = vec.z*mult} -end - -routines.vec.scalar_mult = routines.vec.scalarMult - -routines.vec.dp = function(vec1, vec2) - return vec1.x*vec2.x + vec1.y*vec2.y + vec1.z*vec2.z -end - -routines.vec.cp = function(vec1, vec2) - return { x = vec1.y*vec2.z - vec1.z*vec2.y, y = vec1.z*vec2.x - vec1.x*vec2.z, z = vec1.x*vec2.y - vec1.y*vec2.x} -end - -routines.vec.mag = function(vec) - return (vec.x^2 + vec.y^2 + vec.z^2)^0.5 -end - -routines.vec.getUnitVec = function(vec) - local mag = routines.vec.mag(vec) - return { x = vec.x/mag, y = vec.y/mag, z = vec.z/mag } -end - -routines.vec.rotateVec2 = function(vec2, theta) - return { x = vec2.x*math.cos(theta) - vec2.y*math.sin(theta), y = vec2.x*math.sin(theta) + vec2.y*math.cos(theta)} -end ---------------------------------------------------------------------------------------------------------------------------- - - - - --- acc- the accuracy of each easting/northing. 0, 1, 2, 3, 4, or 5. -routines.tostringMGRS = function(MGRS, acc) - if acc == 0 then - return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph - else - return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph .. ' ' .. string.format('%0' .. acc .. 'd', routines.utils.round(MGRS.Easting/(10^(5-acc)), 0)) - .. ' ' .. string.format('%0' .. acc .. 'd', routines.utils.round(MGRS.Northing/(10^(5-acc)), 0)) - end -end - ---[[acc: -in DM: decimal point of minutes. -In DMS: decimal point of seconds. -position after the decimal of the least significant digit: -So: -42.32 - acc of 2. -]] -routines.tostringLL = function(lat, lon, acc, DMS) - - local latHemi, lonHemi - if lat > 0 then - latHemi = 'N' - else - latHemi = 'S' - end - - if lon > 0 then - lonHemi = 'E' - else - lonHemi = 'W' - end - - lat = math.abs(lat) - lon = math.abs(lon) - - local latDeg = math.floor(lat) - local latMin = (lat - latDeg)*60 - - local lonDeg = math.floor(lon) - local lonMin = (lon - lonDeg)*60 - - if DMS then -- degrees, minutes, and seconds. - local oldLatMin = latMin - latMin = math.floor(latMin) - local latSec = routines.utils.round((oldLatMin - latMin)*60, acc) - - local oldLonMin = lonMin - lonMin = math.floor(lonMin) - local lonSec = routines.utils.round((oldLonMin - lonMin)*60, acc) - - if latSec == 60 then - latSec = 0 - latMin = latMin + 1 - end - - if lonSec == 60 then - lonSec = 0 - lonMin = lonMin + 1 - end - - local secFrmtStr -- create the formatting string for the seconds place - if acc <= 0 then -- no decimal place. - secFrmtStr = '%02d' - else - local width = 3 + acc -- 01.310 - that's a width of 6, for example. - secFrmtStr = '%0' .. width .. '.' .. acc .. 'f' - end - - return string.format('%02d', latDeg) .. ' ' .. string.format('%02d', latMin) .. '\' ' .. string.format(secFrmtStr, latSec) .. '"' .. latHemi .. ' ' - .. string.format('%02d', lonDeg) .. ' ' .. string.format('%02d', lonMin) .. '\' ' .. string.format(secFrmtStr, lonSec) .. '"' .. lonHemi - - else -- degrees, decimal minutes. - latMin = routines.utils.round(latMin, acc) - lonMin = routines.utils.round(lonMin, acc) - - if latMin == 60 then - latMin = 0 - latDeg = latDeg + 1 - end - - if lonMin == 60 then - lonMin = 0 - lonDeg = lonDeg + 1 - end - - local minFrmtStr -- create the formatting string for the minutes place - if acc <= 0 then -- no decimal place. - minFrmtStr = '%02d' - else - local width = 3 + acc -- 01.310 - that's a width of 6, for example. - minFrmtStr = '%0' .. width .. '.' .. acc .. 'f' - end - - return string.format('%02d', latDeg) .. ' ' .. string.format(minFrmtStr, latMin) .. '\'' .. latHemi .. ' ' - .. string.format('%02d', lonDeg) .. ' ' .. string.format(minFrmtStr, lonMin) .. '\'' .. lonHemi - - end -end - ---[[ required: az - radian - required: dist - meters - optional: alt - meters (set to false or nil if you don't want to use it). - optional: metric - set true to get dist and alt in km and m. - precision will always be nearest degree and NM or km.]] -routines.tostringBR = function(az, dist, alt, metric) - az = routines.utils.round(routines.utils.toDegree(az), 0) - - if metric then - dist = routines.utils.round(dist/1000, 2) - else - dist = routines.utils.round(routines.utils.metersToNM(dist), 2) - end - - local s = string.format('%03d', az) .. ' for ' .. dist - - if alt then - if metric then - s = s .. ' at ' .. routines.utils.round(alt, 0) - else - s = s .. ' at ' .. routines.utils.round(routines.utils.metersToFeet(alt), 0) - end - end - return s -end - -routines.getNorthCorrection = function(point) --gets the correction needed for true north - if not point.z then --Vec2; convert to Vec3 - point.z = point.y - point.y = 0 - end - local lat, lon = coord.LOtoLL(point) - local north_posit = coord.LLtoLO(lat + 1, lon) - return math.atan2(north_posit.z - point.z, north_posit.x - point.x) -end - - --- the main area -do - -- THE MAIN FUNCTION -- Accessed 100 times/sec. - routines.main = function() - timer.scheduleFunction(routines.main, {}, timer.getTime() + 2) --reschedule first in case of Lua error - ---------------------------------------------------------------------------------------------------------- - --area to add new stuff in - - routines.do_scheduled_functions() - end -- end of routines.main - - timer.scheduleFunction(routines.main, {}, timer.getTime() + 2) - -end - - -do - local idNum = 0 - - --Simplified event handler - routines.addEventHandler = function(f) --id is optional! - local handler = {} - idNum = idNum + 1 - handler.id = idNum - handler.f = f - handler.onEvent = function(self, event) - self.f(event) - end - world.addEventHandler(handler) - end - - routines.removeEventHandler = function(id) - for key, handler in pairs(world.eventHandlers) do - if handler.id and handler.id == id then - world.eventHandlers[key] = nil - return true - end - end - return false - end -end - --- need to return a Vec3 or Vec2? -function routines.getRandPointInCircle(point, radius, innerRadius) - local theta = 2*math.pi*math.random() - local rad = math.random() + math.random() - if rad > 1 then - rad = 2 - rad - end - - local radMult - if innerRadius and innerRadius <= radius then - radMult = (radius - innerRadius)*rad + innerRadius - else - radMult = radius*rad - end - - if not point.z then --might as well work with vec2/3 - point.z = point.y - end - - local rndCoord - if radius > 0 then - rndCoord = {x = math.cos(theta)*radMult + point.x, y = math.sin(theta)*radMult + point.z} - else - rndCoord = {x = point.x, y = point.z} - end - return rndCoord -end - -routines.goRoute = function(group, path) - local misTask = { - id = 'Mission', - params = { - route = { - points = routines.utils.deepCopy(path), - }, - }, - } - if type(group) == 'string' then - group = Group.getByName(group) - end - local groupCon = group:getController() - if groupCon then - groupCon:setTask(misTask) - return true - end - - Controller.setTask(groupCon, misTask) - return false -end - - --- Useful atomic functions from mist, ported. - -routines.ground = {} -routines.fixedWing = {} -routines.heli = {} - -routines.ground.buildWP = function(point, overRideForm, overRideSpeed) - - local wp = {} - wp.x = point.x - - if point.z then - wp.y = point.z - else - wp.y = point.y - end - local form, speed - - if point.speed and not overRideSpeed then - wp.speed = point.speed - elseif type(overRideSpeed) == 'number' then - wp.speed = overRideSpeed - else - wp.speed = routines.utils.kmphToMps(20) - end - - if point.form and not overRideForm then - form = point.form - else - form = overRideForm - end - - if not form then - wp.action = 'Cone' - else - form = string.lower(form) - if form == 'off_road' or form == 'off road' then - wp.action = 'Off Road' - elseif form == 'on_road' or form == 'on road' then - wp.action = 'On Road' - elseif form == 'rank' or form == 'line_abrest' or form == 'line abrest' or form == 'lineabrest'then - wp.action = 'Rank' - elseif form == 'cone' then - wp.action = 'Cone' - elseif form == 'diamond' then - wp.action = 'Diamond' - elseif form == 'vee' then - wp.action = 'Vee' - elseif form == 'echelon_left' or form == 'echelon left' or form == 'echelonl' then - wp.action = 'EchelonL' - elseif form == 'echelon_right' or form == 'echelon right' or form == 'echelonr' then - wp.action = 'EchelonR' - else - wp.action = 'Cone' -- if nothing matched - end - end - - wp.type = 'Turning Point' - - return wp - -end - -routines.fixedWing.buildWP = function(point, WPtype, speed, alt, altType) - - local wp = {} - wp.x = point.x - - if point.z then - wp.y = point.z - else - wp.y = point.y - end - - if alt and type(alt) == 'number' then - wp.alt = alt - else - wp.alt = 2000 - end - - if altType then - altType = string.lower(altType) - if altType == 'radio' or 'agl' then - wp.alt_type = 'RADIO' - elseif altType == 'baro' or 'asl' then - wp.alt_type = 'BARO' - end - else - wp.alt_type = 'RADIO' - end - - if point.speed then - speed = point.speed - end - - if point.type then - WPtype = point.type - end - - if not speed then - wp.speed = routines.utils.kmphToMps(500) - else - wp.speed = speed - end - - if not WPtype then - wp.action = 'Turning Point' - else - WPtype = string.lower(WPtype) - if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then - wp.action = 'Fly Over Point' - elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then - wp.action = 'Turning Point' - else - wp.action = 'Turning Point' - end - end - - wp.type = 'Turning Point' - return wp -end - -routines.heli.buildWP = function(point, WPtype, speed, alt, altType) - - local wp = {} - wp.x = point.x - - if point.z then - wp.y = point.z - else - wp.y = point.y - end - - if alt and type(alt) == 'number' then - wp.alt = alt - else - wp.alt = 500 - end - - if altType then - altType = string.lower(altType) - if altType == 'radio' or 'agl' then - wp.alt_type = 'RADIO' - elseif altType == 'baro' or 'asl' then - wp.alt_type = 'BARO' - end - else - wp.alt_type = 'RADIO' - end - - if point.speed then - speed = point.speed - end - - if point.type then - WPtype = point.type - end - - if not speed then - wp.speed = routines.utils.kmphToMps(200) - else - wp.speed = speed - end - - if not WPtype then - wp.action = 'Turning Point' - else - WPtype = string.lower(WPtype) - if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then - wp.action = 'Fly Over Point' - elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then - wp.action = 'Turning Point' - else - wp.action = 'Turning Point' - end - end - - wp.type = 'Turning Point' - return wp -end - -routines.groupToRandomPoint = function(vars) - local group = vars.group --Required - local point = vars.point --required - local radius = vars.radius or 0 - local innerRadius = vars.innerRadius - local form = vars.form or 'Cone' - local heading = vars.heading or math.random()*2*math.pi - local headingDegrees = vars.headingDegrees - local speed = vars.speed or routines.utils.kmphToMps(20) - - - local useRoads - if not vars.disableRoads then - useRoads = true - else - useRoads = false - end - - local path = {} - - if headingDegrees then - heading = headingDegrees*math.pi/180 - end - - if heading >= 2*math.pi then - heading = heading - 2*math.pi - end - - local rndCoord = routines.getRandPointInCircle(point, radius, innerRadius) - - local offset = {} - local posStart = routines.getLeadPos(group) - - offset.x = routines.utils.round(math.sin(heading - (math.pi/2)) * 50 + rndCoord.x, 3) - offset.z = routines.utils.round(math.cos(heading + (math.pi/2)) * 50 + rndCoord.y, 3) - path[#path + 1] = routines.ground.buildWP(posStart, form, speed) - - - if useRoads == true and ((point.x - posStart.x)^2 + (point.z - posStart.z)^2)^0.5 > radius * 1.3 then - path[#path + 1] = routines.ground.buildWP({['x'] = posStart.x + 11, ['z'] = posStart.z + 11}, 'off_road', speed) - path[#path + 1] = routines.ground.buildWP(posStart, 'on_road', speed) - path[#path + 1] = routines.ground.buildWP(offset, 'on_road', speed) - else - path[#path + 1] = routines.ground.buildWP({['x'] = posStart.x + 25, ['z'] = posStart.z + 25}, form, speed) - end - - path[#path + 1] = routines.ground.buildWP(offset, form, speed) - path[#path + 1] = routines.ground.buildWP(rndCoord, form, speed) - - routines.goRoute(group, path) - - return -end - -routines.groupRandomDistSelf = function(gpData, dist, form, heading, speed) - local pos = routines.getLeadPos(gpData) - local fakeZone = {} - fakeZone.radius = dist or math.random(300, 1000) - fakeZone.point = {x = pos.x, y, pos.y, z = pos.z} - routines.groupToRandomZone(gpData, fakeZone, form, heading, speed) - - return -end - -routines.groupToRandomZone = function(gpData, zone, form, heading, speed) - if type(gpData) == 'string' then - gpData = Group.getByName(gpData) - end - - if type(zone) == 'string' then - zone = trigger.misc.getZone(zone) - elseif type(zone) == 'table' and not zone.radius then - zone = trigger.misc.getZone(zone[math.random(1, #zone)]) - end - - if speed then - speed = routines.utils.kmphToMps(speed) - end - - local vars = {} - vars.group = gpData - vars.radius = zone.radius - vars.form = form - vars.headingDegrees = heading - vars.speed = speed - vars.point = routines.utils.zoneToVec3(zone) - - routines.groupToRandomPoint(vars) - - return -end - -routines.isTerrainValid = function(coord, terrainTypes) -- vec2/3 and enum or table of acceptable terrain types - if coord.z then - coord.y = coord.z - end - local typeConverted = {} - - if type(terrainTypes) == 'string' then -- if its a string it does this check - for constId, constData in pairs(land.SurfaceType) do - if string.lower(constId) == string.lower(terrainTypes) or string.lower(constData) == string.lower(terrainTypes) then - table.insert(typeConverted, constId) - end - end - elseif type(terrainTypes) == 'table' then -- if its a table it does this check - for typeId, typeData in pairs(terrainTypes) do - for constId, constData in pairs(land.SurfaceType) do - if string.lower(constId) == string.lower(typeData) or string.lower(constData) == string.lower(typeId) then - table.insert(typeConverted, constId) - end - end - end - end - for validIndex, validData in pairs(typeConverted) do - if land.getSurfaceType(coord) == land.SurfaceType[validData] then - return true - end - end - return false -end - -routines.groupToPoint = function(gpData, point, form, heading, speed, useRoads) - if type(point) == 'string' then - point = trigger.misc.getZone(point) - end - if speed then - speed = routines.utils.kmphToMps(speed) - end - - local vars = {} - vars.group = gpData - vars.form = form - vars.headingDegrees = heading - vars.speed = speed - vars.disableRoads = useRoads - vars.point = routines.utils.zoneToVec3(point) - routines.groupToRandomPoint(vars) - - return -end - - -routines.getLeadPos = function(group) - if type(group) == 'string' then -- group name - group = Group.getByName(group) - end - - local units = group:getUnits() - - local leader = units[1] - if not leader then -- SHOULD be good, but if there is a bug, this code future-proofs it then. - local lowestInd = math.huge - for ind, unit in pairs(units) do - if ind < lowestInd then - lowestInd = ind - leader = unit - end - end - end - if leader and Unit.isExist(leader) then -- maybe a little too paranoid now... - return leader:getPosition().p - end -end - ---[[ vars for routines.getMGRSString: -vars.units - table of unit names (NOT unitNameTable- maybe this should change). -vars.acc - integer between 0 and 5, inclusive -]] -routines.getMGRSString = function(vars) - local units = vars.units - local acc = vars.acc or 5 - local avgPos = routines.getAvgPos(units) - if avgPos then - return routines.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(avgPos)), acc) - end -end - ---[[ vars for routines.getLLString -vars.units - table of unit names (NOT unitNameTable- maybe this should change). -vars.acc - integer, number of numbers after decimal place -vars.DMS - if true, output in degrees, minutes, seconds. Otherwise, output in degrees, minutes. - - -]] -routines.getLLString = function(vars) - local units = vars.units - local acc = vars.acc or 3 - local DMS = vars.DMS - local avgPos = routines.getAvgPos(units) - if avgPos then - local lat, lon = coord.LOtoLL(avgPos) - return routines.tostringLL(lat, lon, acc, DMS) - end -end - ---[[ -vars.zone - table of a zone name. -vars.ref - vec3 ref point, maybe overload for vec2 as well? -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -]] -routines.getBRStringZone = function(vars) - local zone = trigger.misc.getZone( vars.zone ) - local ref = routines.utils.makeVec3(vars.ref, 0) -- turn it into Vec3 if it is not already. - local alt = vars.alt - local metric = vars.metric - if zone then - local vec = {x = zone.point.x - ref.x, y = zone.point.y - ref.y, z = zone.point.z - ref.z} - local dir = routines.utils.getDir(vec, ref) - local dist = routines.utils.get2DDist(zone.point, ref) - if alt then - alt = zone.y - end - return routines.tostringBR(dir, dist, alt, metric) - else - env.info( 'routines.getBRStringZone: error: zone is nil' ) - end -end - ---[[ -vars.units- table of unit names (NOT unitNameTable- maybe this should change). -vars.ref - vec3 ref point, maybe overload for vec2 as well? -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -]] -routines.getBRString = function(vars) - local units = vars.units - local ref = routines.utils.makeVec3(vars.ref, 0) -- turn it into Vec3 if it is not already. - local alt = vars.alt - local metric = vars.metric - local avgPos = routines.getAvgPos(units) - if avgPos then - local vec = {x = avgPos.x - ref.x, y = avgPos.y - ref.y, z = avgPos.z - ref.z} - local dir = routines.utils.getDir(vec, ref) - local dist = routines.utils.get2DDist(avgPos, ref) - if alt then - alt = avgPos.y - end - return routines.tostringBR(dir, dist, alt, metric) - end -end - - --- Returns the Vec3 coordinates of the average position of the concentration of units most in the heading direction. ---[[ vars for routines.getLeadingPos: -vars.units - table of unit names -vars.heading - direction -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees -]] -routines.getLeadingPos = function(vars) - local units = vars.units - local heading = vars.heading - local radius = vars.radius - if vars.headingDegrees then - heading = routines.utils.toRadian(vars.headingDegrees) - end - - local unitPosTbl = {} - for i = 1, #units do - local unit = Unit.getByName(units[i]) - if unit and unit:isExist() then - unitPosTbl[#unitPosTbl + 1] = unit:getPosition().p - end - end - if #unitPosTbl > 0 then -- one more more units found. - -- first, find the unit most in the heading direction - local maxPos = -math.huge - - local maxPosInd -- maxPos - the furthest in direction defined by heading; maxPosInd = - for i = 1, #unitPosTbl do - local rotatedVec2 = routines.vec.rotateVec2(routines.utils.makeVec2(unitPosTbl[i]), heading) - if (not maxPos) or maxPos < rotatedVec2.x then - maxPos = rotatedVec2.x - maxPosInd = i - end - end - - --now, get all the units around this unit... - local avgPos - if radius then - local maxUnitPos = unitPosTbl[maxPosInd] - local avgx, avgy, avgz, totNum = 0, 0, 0, 0 - for i = 1, #unitPosTbl do - if routines.utils.get2DDist(maxUnitPos, unitPosTbl[i]) <= radius then - avgx = avgx + unitPosTbl[i].x - avgy = avgy + unitPosTbl[i].y - avgz = avgz + unitPosTbl[i].z - totNum = totNum + 1 - end - end - avgPos = { x = avgx/totNum, y = avgy/totNum, z = avgz/totNum} - else - avgPos = unitPosTbl[maxPosInd] - end - - return avgPos - end -end - - ---[[ vars for routines.getLeadingMGRSString: -vars.units - table of unit names -vars.heading - direction -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees -vars.acc - number, 0 to 5. -]] -routines.getLeadingMGRSString = function(vars) - local pos = routines.getLeadingPos(vars) - if pos then - local acc = vars.acc or 5 - return routines.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(pos)), acc) - end -end - ---[[ vars for routines.getLeadingLLString: -vars.units - table of unit names -vars.heading - direction, number -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees -vars.acc - number of digits after decimal point (can be negative) -vars.DMS - boolean, true if you want DMS. -]] -routines.getLeadingLLString = function(vars) - local pos = routines.getLeadingPos(vars) - if pos then - local acc = vars.acc or 3 - local DMS = vars.DMS - local lat, lon = coord.LOtoLL(pos) - return routines.tostringLL(lat, lon, acc, DMS) - end -end - - - ---[[ vars for routines.getLeadingBRString: -vars.units - table of unit names -vars.heading - direction, number -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees -vars.metric - boolean, if true, use km instead of NM. -vars.alt - boolean, if true, include altitude. -vars.ref - vec3/vec2 reference point. -]] -routines.getLeadingBRString = function(vars) - local pos = routines.getLeadingPos(vars) - if pos then - local ref = vars.ref - local alt = vars.alt - local metric = vars.metric - - local vec = {x = pos.x - ref.x, y = pos.y - ref.y, z = pos.z - ref.z} - local dir = routines.utils.getDir(vec, ref) - local dist = routines.utils.get2DDist(pos, ref) - if alt then - alt = pos.y - end - return routines.tostringBR(dir, dist, alt, metric) - end -end - ---[[ vars for routines.message.add - vars.text = 'Hello World' - vars.displayTime = 20 - vars.msgFor = {coa = {'red'}, countries = {'Ukraine', 'Georgia'}, unitTypes = {'A-10C'}} - -]] - ---[[ vars for routines.msgMGRS -vars.units - table of unit names (NOT unitNameTable- maybe this should change). -vars.acc - integer between 0 and 5, inclusive -vars.text - text in the message -vars.displayTime - self explanatory -vars.msgFor - scope -]] -routines.msgMGRS = function(vars) - local units = vars.units - local acc = vars.acc - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getMGRSString{units = units, acc = acc} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } -end - ---[[ vars for routines.msgLL -vars.units - table of unit names (NOT unitNameTable- maybe this should change) (Yes). -vars.acc - integer, number of numbers after decimal place -vars.DMS - if true, output in degrees, minutes, seconds. Otherwise, output in degrees, minutes. -vars.text - text in the message -vars.displayTime - self explanatory -vars.msgFor - scope -]] -routines.msgLL = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local acc = vars.acc - local DMS = vars.DMS - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getLLString{units = units, acc = acc, DMS = DMS} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } - -end - - ---[[ -vars.units- table of unit names (NOT unitNameTable- maybe this should change). -vars.ref - vec3 ref point, maybe overload for vec2 as well? -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] -routines.msgBR = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local ref = vars.ref -- vec2/vec3 will be handled in routines.getBRString - local alt = vars.alt - local metric = vars.metric - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getBRString{units = units, ref = ref, alt = alt, metric = metric} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } - -end - - --------------------------------------------------------------------------------------------- --- basically, just sub-types of routines.msgBR... saves folks the work of getting the ref point. ---[[ -vars.units- table of unit names (NOT unitNameTable- maybe this should change). -vars.ref - string red, blue -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] -routines.msgBullseye = function(vars) - if string.lower(vars.ref) == 'red' then - vars.ref = routines.DBs.missionData.bullseye.red - routines.msgBR(vars) - elseif string.lower(vars.ref) == 'blue' then - vars.ref = routines.DBs.missionData.bullseye.blue - routines.msgBR(vars) - end -end - ---[[ -vars.units- table of unit names (NOT unitNameTable- maybe this should change). -vars.ref - unit name of reference point -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] - -routines.msgBRA = function(vars) - if Unit.getByName(vars.ref) then - vars.ref = Unit.getByName(vars.ref):getPosition().p - if not vars.alt then - vars.alt = true - end - routines.msgBR(vars) - end -end --------------------------------------------------------------------------------------------- - ---[[ vars for routines.msgLeadingMGRS: -vars.units - table of unit names -vars.heading - direction -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees (optional) -vars.acc - number, 0 to 5. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] -routines.msgLeadingMGRS = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local heading = vars.heading - local radius = vars.radius - local headingDegrees = vars.headingDegrees - local acc = vars.acc - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getLeadingMGRSString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } - - -end ---[[ vars for routines.msgLeadingLL: -vars.units - table of unit names -vars.heading - direction, number -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees (optional) -vars.acc - number of digits after decimal point (can be negative) -vars.DMS - boolean, true if you want DMS. (optional) -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] -routines.msgLeadingLL = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local heading = vars.heading - local radius = vars.radius - local headingDegrees = vars.headingDegrees - local acc = vars.acc - local DMS = vars.DMS - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getLeadingLLString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc, DMS = DMS} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } - -end - ---[[ -vars.units - table of unit names -vars.heading - direction, number -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees (optional) -vars.metric - boolean, if true, use km instead of NM. (optional) -vars.alt - boolean, if true, include altitude. (optional) -vars.ref - vec3/vec2 reference point. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] -routines.msgLeadingBR = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local heading = vars.heading - local radius = vars.radius - local headingDegrees = vars.headingDegrees - local metric = vars.metric - local alt = vars.alt - local ref = vars.ref -- vec2/vec3 will be handled in routines.getBRString - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getLeadingBRString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, metric = metric, alt = alt, ref = ref} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } -end - - -function spairs(t, order) - -- collect the keys - local keys = {} - for k in pairs(t) do keys[#keys+1] = k end - - -- if order function given, sort by it by passing the table and keys a, b, - -- otherwise just sort the keys - if order then - table.sort(keys, function(a,b) return order(t, a, b) end) - else - table.sort(keys) - end - - -- return the iterator function - local i = 0 - return function() - i = i + 1 - if keys[i] then - return keys[i], t[keys[i]] - end - end -end - - -function routines.IsPartOfGroupInZones( CargoGroup, LandingZones ) ---trace.f() - - local CurrentZoneID = nil - - if CargoGroup then - local CargoUnits = CargoGroup:getUnits() - for CargoUnitID, CargoUnit in pairs( CargoUnits ) do - if CargoUnit and CargoUnit:getLife() >= 1.0 then - CurrentZoneID = routines.IsUnitInZones( CargoUnit, LandingZones ) - if CurrentZoneID then - break - end - end - end - end - ---trace.r( "", "", { CurrentZoneID } ) - return CurrentZoneID -end - - - -function routines.IsUnitInZones( TransportUnit, LandingZones ) ---trace.f("", "routines.IsUnitInZones" ) - - local TransportZoneResult = nil - local TransportZonePos = nil - local TransportZone = nil - - -- fill-up some local variables to support further calculations to determine location of units within the zone. - if TransportUnit then - local TransportUnitPos = TransportUnit:getPosition().p - if type( LandingZones ) == "table" then - for LandingZoneID, LandingZoneName in pairs( LandingZones ) do - TransportZone = trigger.misc.getZone( LandingZoneName ) - if TransportZone then - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then - TransportZoneResult = LandingZoneID - break - end - end - end - else - TransportZone = trigger.misc.getZone( LandingZones ) - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then - TransportZoneResult = 1 - end - end - if TransportZoneResult then - --trace.i( "routines", "TransportZone:" .. TransportZoneResult ) - else - --trace.i( "routines", "TransportZone:nil logic" ) - end - return TransportZoneResult - else - --trace.i( "routines", "TransportZone:nil hard" ) - return nil - end -end - -function routines.IsUnitNearZonesRadius( TransportUnit, LandingZones, ZoneRadius ) ---trace.f("", "routines.IsUnitInZones" ) - - local TransportZoneResult = nil - local TransportZonePos = nil - local TransportZone = nil - - -- fill-up some local variables to support further calculations to determine location of units within the zone. - if TransportUnit then - local TransportUnitPos = TransportUnit:getPosition().p - if type( LandingZones ) == "table" then - for LandingZoneID, LandingZoneName in pairs( LandingZones ) do - TransportZone = trigger.misc.getZone( LandingZoneName ) - if TransportZone then - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= ZoneRadius ) then - TransportZoneResult = LandingZoneID - break - end - end - end - else - TransportZone = trigger.misc.getZone( LandingZones ) - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= ZoneRadius ) then - TransportZoneResult = 1 - end - end - if TransportZoneResult then - --trace.i( "routines", "TransportZone:" .. TransportZoneResult ) - else - --trace.i( "routines", "TransportZone:nil logic" ) - end - return TransportZoneResult - else - --trace.i( "routines", "TransportZone:nil hard" ) - return nil - end -end - - -function routines.IsStaticInZones( TransportStatic, LandingZones ) ---trace.f() - - local TransportZoneResult = nil - local TransportZonePos = nil - local TransportZone = nil - - -- fill-up some local variables to support further calculations to determine location of units within the zone. - local TransportStaticPos = TransportStatic:getPosition().p - if type( LandingZones ) == "table" then - for LandingZoneID, LandingZoneName in pairs( LandingZones ) do - TransportZone = trigger.misc.getZone( LandingZoneName ) - if TransportZone then - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportStaticPos.x - TransportZonePos.x)^2 + (TransportStaticPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then - TransportZoneResult = LandingZoneID - break - end - end - end - else - TransportZone = trigger.misc.getZone( LandingZones ) - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportStaticPos.x - TransportZonePos.x)^2 + (TransportStaticPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then - TransportZoneResult = 1 - end - end - ---trace.r( "", "", { TransportZoneResult } ) - return TransportZoneResult -end - - -function routines.IsUnitInRadius( CargoUnit, ReferencePosition, Radius ) ---trace.f() - - local Valid = true - - -- fill-up some local variables to support further calculations to determine location of units within the zone. - local CargoPos = CargoUnit:getPosition().p - local ReferenceP = ReferencePosition.p - - if (((CargoPos.x - ReferenceP.x)^2 + (CargoPos.z - ReferenceP.z)^2)^0.5 <= Radius) then - else - Valid = false - end - - return Valid -end - -function routines.IsPartOfGroupInRadius( CargoGroup, ReferencePosition, Radius ) ---trace.f() - - local Valid = true - - Valid = routines.ValidateGroup( CargoGroup, "CargoGroup", Valid ) - - -- fill-up some local variables to support further calculations to determine location of units within the zone - local CargoUnits = CargoGroup:getUnits() - for CargoUnitId, CargoUnit in pairs( CargoUnits ) do - local CargoUnitPos = CargoUnit:getPosition().p --- env.info( 'routines.IsPartOfGroupInRadius: CargoUnitPos.x = ' .. CargoUnitPos.x .. ' CargoUnitPos.z = ' .. CargoUnitPos.z ) - local ReferenceP = ReferencePosition.p --- env.info( 'routines.IsPartOfGroupInRadius: ReferenceGroupPos.x = ' .. ReferenceGroupPos.x .. ' ReferenceGroupPos.z = ' .. ReferenceGroupPos.z ) - - if ((( CargoUnitPos.x - ReferenceP.x)^2 + (CargoUnitPos.z - ReferenceP.z)^2)^0.5 <= Radius) then - else - Valid = false - break - end - end - - return Valid -end - - -function routines.ValidateString( Variable, VariableName, Valid ) ---trace.f() - - if type( Variable ) == "string" then - if Variable == "" then - error( "routines.ValidateString: error: " .. VariableName .. " must be filled out!" ) - Valid = false - end - else - error( "routines.ValidateString: error: " .. VariableName .. " is not a string." ) - Valid = false - end - ---trace.r( "", "", { Valid } ) - return Valid -end - -function routines.ValidateNumber( Variable, VariableName, Valid ) ---trace.f() - - if type( Variable ) == "number" then - else - error( "routines.ValidateNumber: error: " .. VariableName .. " is not a number." ) - Valid = false - end - ---trace.r( "", "", { Valid } ) - return Valid - -end - -function routines.ValidateGroup( Variable, VariableName, Valid ) ---trace.f() - - if Variable == nil then - error( "routines.ValidateGroup: error: " .. VariableName .. " is a nil value!" ) - Valid = false - end - ---trace.r( "", "", { Valid } ) - return Valid -end - -function routines.ValidateZone( LandingZones, VariableName, Valid ) ---trace.f() - - if LandingZones == nil then - error( "routines.ValidateGroup: error: " .. VariableName .. " is a nil value!" ) - Valid = false - end - - if type( LandingZones ) == "table" then - for LandingZoneID, LandingZoneName in pairs( LandingZones ) do - if trigger.misc.getZone( LandingZoneName ) == nil then - error( "routines.ValidateGroup: error: Zone " .. LandingZoneName .. " does not exist!" ) - Valid = false - break - end - end - else - if trigger.misc.getZone( LandingZones ) == nil then - error( "routines.ValidateGroup: error: Zone " .. LandingZones .. " does not exist!" ) - Valid = false - end - end - ---trace.r( "", "", { Valid } ) - return Valid -end - -function routines.ValidateEnumeration( Variable, VariableName, Enum, Valid ) ---trace.f() - - local ValidVariable = false - - for EnumId, EnumData in pairs( Enum ) do - if Variable == EnumData then - ValidVariable = true - break - end - end - - if ValidVariable then - else - error( 'TransportValidateEnum: " .. VariableName .. " is not a valid type.' .. Variable ) - Valid = false - end - ---trace.r( "", "", { Valid } ) - return Valid -end - -function routines.getGroupRoute(groupIdent, task) -- same as getGroupPoints but returns speed and formation type along with vec2 of point} - -- refactor to search by groupId and allow groupId and groupName as inputs - local gpId = groupIdent - if type(groupIdent) == 'string' and not tonumber(groupIdent) then - gpId = _DATABASE.Templates.Groups[groupIdent].groupId - end - - for coa_name, coa_data in pairs(env.mission.coalition) do - if (coa_name == 'red' or coa_name == 'blue') and type(coa_data) == 'table' then - if coa_data.country then --there is a country table - for cntry_id, cntry_data in pairs(coa_data.country) do - for obj_type_name, obj_type_data in pairs(cntry_data) do - if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" then -- only these types have points - if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! - for group_num, group_data in pairs(obj_type_data.group) do - if group_data and group_data.groupId == gpId then -- this is the group we are looking for - if group_data.route and group_data.route.points and #group_data.route.points > 0 then - local points = {} - - for point_num, point in pairs(group_data.route.points) do - local routeData = {} - if not point.point then - routeData.x = point.x - routeData.y = point.y - else - routeData.point = point.point --it's possible that the ME could move to the point = Vec2 notation. - end - routeData.form = point.action - routeData.speed = point.speed - routeData.alt = point.alt - routeData.alt_type = point.alt_type - routeData.airdromeId = point.airdromeId - routeData.helipadId = point.helipadId - routeData.type = point.type - routeData.action = point.action - if task then - routeData.task = point.task - end - points[point_num] = routeData - end - - return points - end - return - end --if group_data and group_data.name and group_data.name == 'groupname' - end --for group_num, group_data in pairs(obj_type_data.group) do - end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then - end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then - end --for obj_type_name, obj_type_data in pairs(cntry_data) do - end --for cntry_id, cntry_data in pairs(coa_data.country) do - end --if coa_data.country then --there is a country table - end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then - end --for coa_name, coa_data in pairs(mission.coalition) do -end - -routines.ground.patrolRoute = function(vars) - - - local tempRoute = {} - local useRoute = {} - local gpData = vars.gpData - if type(gpData) == 'string' then - gpData = Group.getByName(gpData) - end - - local useGroupRoute - if not vars.useGroupRoute then - useGroupRoute = vars.gpData - else - useGroupRoute = vars.useGroupRoute - end - local routeProvided = false - if not vars.route then - if useGroupRoute then - tempRoute = routines.getGroupRoute(useGroupRoute) - end - else - useRoute = vars.route - local posStart = routines.getLeadPos(gpData) - useRoute[1] = routines.ground.buildWP(posStart, useRoute[1].action, useRoute[1].speed) - routeProvided = true - end - - - local overRideSpeed = vars.speed or 'default' - local pType = vars.pType - local offRoadForm = vars.offRoadForm or 'default' - local onRoadForm = vars.onRoadForm or 'default' - - if routeProvided == false and #tempRoute > 0 then - local posStart = routines.getLeadPos(gpData) - - - useRoute[#useRoute + 1] = routines.ground.buildWP(posStart, offRoadForm, overRideSpeed) - for i = 1, #tempRoute do - local tempForm = tempRoute[i].action - local tempSpeed = tempRoute[i].speed - - if offRoadForm == 'default' then - tempForm = tempRoute[i].action - end - if onRoadForm == 'default' then - onRoadForm = 'On Road' - end - if (string.lower(tempRoute[i].action) == 'on road' or string.lower(tempRoute[i].action) == 'onroad' or string.lower(tempRoute[i].action) == 'on_road') then - tempForm = onRoadForm - 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, "Message", MsgTime, MsgName ):ToCoalition( coalition.side.RED ):ToCoalition( coalition.side.BLUE ) -end - -function MessageToRed( MsgText, MsgTime, MsgName ) ---trace.f() - - MESSAGE:New( MsgText, "To Red Coalition", MsgTime, MsgName ):ToCoalition( coalition.side.RED ) -end - -function MessageToBlue( MsgText, MsgTime, MsgName ) ---trace.f() - - MESSAGE:New( MsgText, "To Blue Coalition", MsgTime, MsgName ):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' )) - ---- BASE classes. --- --- @{#BASE} class --- ============== --- The @{#BASE} class is the super class for most of the classes defined within MOOSE. --- --- It handles: --- --- * The construction and inheritance of child classes. --- * The tracing of objects during mission execution within the DCS.log file (under saved games folder). --- --- Note: Normally you would not use the BASE class unless you are extending the MOOSE framework with new classes. --- --- BASE Trace functionality --- ======================== --- The BASE class contains trace methods to trace progress within a mission execution of a certain object. --- Note that these trace methods are inherited by each MOOSE class interiting BASE. --- As such, each object created from derived class from BASE can use the tracing functions to trace its execution. --- --- Trace a function call --- --------------------- --- There are basically 3 types of tracing methods available within BASE: --- --- * @{#BASE.F}: Trace the beginning of a function and its given parameters. --- * @{#BASE.T}: Trace further logic within a function giving optional variables or parameters. --- * @{#BASE.E}: Trace an execption within a function giving optional variables or parameters. An exception will always be traced. --- --- 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. --- --- BASE Inheritance support --- ======================== --- The following methods are available to support inheritance: --- --- * @{#BASE.Inherit}: Inherits from a class. --- * @{#BASE.Inherited}: Returns the parent class from the class. --- --- Future --- ====== --- Further methods may be added to BASE whenever there is a need to make "overall" functions available within MOOSE. --- --- ==== --- --- @module Base --- @author FlightControl - -Include.File( "Routines" ) - -local _TraceOn = true -local _TraceLevel = 1 -local _TraceClass = { - --DATABASE = true, - --SEAD = true, - --DESTROYBASETASK = true, - --MOVEMENT = true, - --SPAWN = true, - --STAGE = true, - --ZONE = true, - --GROUP = true, - --UNIT = true, - --CLIENT = true, - --CARGO = true, - --CARGO_GROUP = true, - --CARGO_PACKAGE = true, - --CARGO_SLINGLOAD = true, - --CARGO_ZONE = true, - --CLEANUP = true, - --MENU_CLIENT = true, - --MENU_CLIENT_COMMAND = true, - --ESCORT = true, - } -local _TraceClassMethod = {} - ---- The BASE Class --- @type BASE --- @field ClassName The name of the class. --- @field ClassID The ID number of the class. -BASE = { - ClassName = "BASE", - ClassID = 0, - Events = {} -} - ---- The Formation Class --- @type FORMATION --- @field Cone A cone formation. -FORMATION = { - Cone = "Cone" -} - - - ---- The base constructor. This is the top top class of all classed defined within the MOOSE. --- Any new class needs to be derived from this class for proper inheritance. --- @param #BASE self --- @return #BASE The new instance of the BASE class. --- @usage --- function TASK:New() --- --- local self = BASE:Inherit( self, BASE:New() ) --- --- -- assign Task default values during construction --- self.TaskBriefing = "Task: No Task." --- self.Time = timer.getTime() --- self.ExecuteStage = _TransportExecuteStage.NONE --- --- return self --- end --- @todo need to investigate if the deepCopy is really needed... Don't think so. -function BASE:New() - local Child = routines.utils.deepCopy( self ) - local Parent = {} - setmetatable( Child, Parent ) - Child.__index = Child - self.ClassID = self.ClassID + 1 - Child.ClassID = self.ClassID - --Child.AddEvent( Child, S_EVENT_BIRTH, Child.EventBirth ) - return Child -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 ) - if Child ~= nil then - setmetatable( Child, Parent ) - Child.__index = Child - end - --Child.ClassName = Child.ClassName .. '.' .. Child.ClassID - self:T( 'Inherited from ' .. Parent.ClassName ) - return Child -end - ---- This is the worker method to retrieve the Parent class. --- @param #BASE self --- @param #BASE Child is the Child class from which the Parent class needs to be retrieved. --- @return #BASE -function BASE:Inherited( Child ) - local Parent = getmetatable( Child ) --- env.info('Inherited class of ' .. Child.ClassName .. ' is ' .. Parent.ClassName ) - return Parent -end - ---- Get the ClassName + ClassID of the class instance. --- The ClassName + ClassID is formatted as '%s#%09d'. --- @param #BASE self --- @return #string The ClassName + ClassID of the class instance. -function BASE:GetClassNameAndID() - return string.format( '%s#%09d', self:GetClassName(), self:GetClassID() ) -end - ---- Get the ClassName of the class instance. --- @param #BASE self --- @return #string The ClassName of the class instance. -function BASE:GetClassName() - return self.ClassName -end - ---- Get the ClassID of the class instance. --- @param #BASE self --- @return #string The ClassID of the class instance. -function BASE:GetClassID() - return self.ClassID -end - ---- Set a new listener for the class. --- @param self --- @param DCSTypes#Event Event --- @param #function EventFunction --- @return #BASE -function BASE:AddEvent( Event, EventFunction ) - self:F( Event ) - - self.Events[#self.Events+1] = {} - self.Events[#self.Events].Event = Event - self.Events[#self.Events].EventFunction = EventFunction - self.Events[#self.Events].EventEnabled = false - - return self -end - ---- Returns the event dispatcher --- @param #BASE self --- @return Event#EVENT -function BASE:Event() - - return _EVENTDISPATCHER -end - - - - - ---- Enable the event listeners for the class. --- @param #BASE self --- @return #BASE -function BASE:EnableEvents() - self:F( #self.Events ) - - for EventID, Event in pairs( self.Events ) do - Event.Self = self - Event.EventEnabled = true - end - self.Events.Handler = world.addEventHandler( self ) - - return self -end - - ---- Disable the event listeners for the class. --- @param #BASE self --- @return #BASE -function BASE:DisableEvents() - self:F() - - world.removeEventHandler( self ) - for EventID, Event in pairs( self.Events ) do - Event.Self = nil - Event.EventEnabled = false - end - - return self -end - - -local BaseEventCodes = { - "S_EVENT_SHOT", - "S_EVENT_HIT", - "S_EVENT_TAKEOFF", - "S_EVENT_LAND", - "S_EVENT_CRASH", - "S_EVENT_EJECTION", - "S_EVENT_REFUELING", - "S_EVENT_DEAD", - "S_EVENT_PILOT_DEAD", - "S_EVENT_BASE_CAPTURED", - "S_EVENT_MISSION_START", - "S_EVENT_MISSION_END", - "S_EVENT_TOOK_CONTROL", - "S_EVENT_REFUELING_STOP", - "S_EVENT_BIRTH", - "S_EVENT_HUMAN_FAILURE", - "S_EVENT_ENGINE_STARTUP", - "S_EVENT_ENGINE_SHUTDOWN", - "S_EVENT_PLAYER_ENTER_UNIT", - "S_EVENT_PLAYER_LEAVE_UNIT", - "S_EVENT_PLAYER_COMMENT", - "S_EVENT_SHOOTING_START", - "S_EVENT_SHOOTING_END", - "S_EVENT_MAX", -} - ---onEvent( {[1]="S_EVENT_BIRTH",[2]={["subPlace"]=5,["time"]=0,["initiator"]={["id_"]=16884480,},["place"]={["id_"]=5000040,},["id"]=15,["IniUnitName"]="US F-15C@RAMP-Air Support Mountains#001-01",},} --- Event = { --- id = enum world.event, --- time = Time, --- initiator = Unit, --- target = Unit, --- place = Unit, --- subPlace = enum world.BirthPlace, --- weapon = Weapon --- } - ---- Creation of a Birth Event. --- @param #BASE self --- @param DCSTypes#Time EventTime The time stamp of the event. --- @param DCSObject#Object Initiator The initiating object of the event. --- @param #string IniUnitName The initiating unit name. --- @param place --- @param subplace -function BASE:CreateEventBirth( EventTime, Initiator, IniUnitName, place, subplace ) - self:F( { EventTime, Initiator, IniUnitName, place, subplace } ) - - local Event = { - id = world.event.S_EVENT_BIRTH, - time = EventTime, - initiator = Initiator, - IniUnitName = IniUnitName, - place = place, - subplace = subplace - } - - world.onEvent( Event ) -end - ---- Creation of a Crash Event. --- @param #BASE self --- @param DCSTypes#Time EventTime The time stamp of the event. --- @param DCSObject#Object Initiator The initiating object of the event. -function BASE:CreateEventCrash( EventTime, Initiator ) - self:F( { EventTime, Initiator } ) - - local Event = { - id = world.event.S_EVENT_CRASH, - time = EventTime, - initiator = Initiator, - } - - world.onEvent( Event ) -end - --- TODO: Complete DCSTypes#Event structure. ---- The main event handling function... This function captures all events generated for the class. --- @param #BASE self --- @param DCSTypes#Event event -function BASE:onEvent(event) - --self:F( { BaseEventCodes[event.id], event } ) - - if self then - for EventID, EventObject in pairs( self.Events ) do - if EventObject.EventEnabled then - --env.info( 'onEvent Table EventObject.Self = ' .. tostring(EventObject.Self) ) - --env.info( 'onEvent event.id = ' .. tostring(event.id) ) - --env.info( 'onEvent EventObject.Event = ' .. tostring(EventObject.Event) ) - if event.id == EventObject.Event then - if self == EventObject.Self then - if event.initiator and event.initiator:isExist() then - event.IniUnitName = event.initiator:getName() - end - if event.target and event.target:isExist() then - event.TgtUnitName = event.target:getName() - end - --self:T( { BaseEventCodes[event.id], event } ) - --EventObject.EventFunction( self, event ) - end - end - end - end - end -end - --- Trace section - --- Log a trace (only shown when trace is on) --- TODO: Make trace function using variable parameters. - ---- Set trace level --- @param #BASE self --- @param #number Level -function BASE:TraceLevel( Level ) - _TraceLevel = Level - self:E( "Tracing level " .. Level ) -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. Must be at the beginning of the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:F( Arguments ) - - if _TraceOn and ( _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName] ) then - - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - local Function = "function" - if DebugInfoCurrent.name then - Function = DebugInfoCurrent.name - end - - if _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName].Method[Function] then - local LineCurrent = DebugInfoCurrent.currentline - 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 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 _TraceLevel >= 2 then - self:F( Arguments ) - 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 _TraceLevel >= 3 then - self:F( Arguments ) - end - -end - ---- Trace a function logic. Can be anywhere within the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:T( Arguments ) - - if _TraceOn and ( _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName] ) then - - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - local Function = "function" - if DebugInfoCurrent.name then - Function = DebugInfoCurrent.name - end - - if _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName].Method[Function] then - local LineCurrent = DebugInfoCurrent.currentline - 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 2. Can be anywhere within the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:T2( Arguments ) - - if _TraceLevel >= 2 then - self:T( Arguments ) - 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 _TraceLevel >= 3 then - self:T( Arguments ) - end - -end - ---- Log an exception which will be traced always. Can be anywhere within the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:E( Arguments ) - - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - local Function = "function" - if DebugInfoCurrent.name then - Function = DebugInfoCurrent.name - end - - local LineCurrent = DebugInfoCurrent.currentline - local LineFrom = DebugInfoFrom.currentline - - env.info( string.format( "%6d(%6d)/%1s:%20s%05d.%s(%s)" , LineCurrent, LineFrom, "E", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) -end - - - ---- Models time events calling event handing functions. --- --- @{SCHEDULER} class --- =================== --- The @{SCHEDULER} class models time events calling given event handling functions. --- --- SCHEDULER constructor --- ===================== --- The SCHEDULER class is quite easy to use: --- --- * @{#SCHEDULER.New}: Setup a new scheduler and start it with the specified parameters. --- --- SCHEDULER timer methods --- ======================= --- The SCHEDULER can be stopped and restarted with the following methods: --- --- * @{#SCHEDULER.Start}: (Re-)Start the scheduler. --- * @{#SCHEDULER.Start}: Stop the scheduler. --- --- @module Scheduler --- @author FlightControl - -Include.File( "Routines" ) -Include.File( "Base" ) - - ---- The SCHEDULER class --- @type SCHEDULER --- @extends Base#BASE -SCHEDULER = { - ClassName = "SCHEDULER", -} - - ---- Constructor. --- @param #SCHEDULER self --- @param #table TimeEventObject Specified for which Moose object the timer is setup. If a value of nil is provided, a scheduler will be setup without an object reference. --- @param #function TimeEventFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in TimeEventFunctionArguments. --- @param #table TimeEventFunctionArguments Optional arguments that can be given as part of scheduler. The arguments need to be given as a table { param1, param 2, ... }. --- @param #number StartSeconds Specifies the amount of seconds that will be waited before the scheduling is started, and the event function is called. --- @param #number RepeatSecondsInterval Specifies the interval in seconds when the scheduler will call the event function. --- @param #number RandomizationFactor Specifies a randomization factor between 0 and 1 to randomize the RepeatSecondsInterval. --- @param #number StopSeconds Specifies the amount of seconds when the scheduler will be stopped. --- @return #SCHEDULER self -function SCHEDULER:New( TimeEventObject, TimeEventFunction, TimeEventFunctionArguments, StartSeconds, RepeatSecondsInterval, RandomizationFactor, StopSeconds ) - local self = BASE:Inherit( self, BASE:New() ) - self:F2( { TimeEventObject, TimeEventFunction, TimeEventFunctionArguments, StartSeconds, RepeatSecondsInterval, RandomizationFactor, StopSeconds } ) - - self.TimeEventObject = TimeEventObject - self.TimeEventFunction = TimeEventFunction - self.TimeEventFunctionArguments = TimeEventFunctionArguments - self.StartSeconds = StartSeconds - - if RepeatSecondsInterval then - self.RepeatSecondsInterval = RepeatSecondsInterval - else - self.RepeatSecondsInterval = 0 - end - - if RandomizationFactor then - self.RandomizationFactor = RandomizationFactor - else - self.RandomizationFactor = 0 - end - - if StopSeconds then - self.StopSeconds = StopSeconds - end - - self.Repeat = false - - self.StartTime = timer.getTime() - - self:Start() - - return self -end - ---- (Re-)Starts the scheduler. --- @param #SCHEDULER self --- @return #SCHEDULER self -function SCHEDULER:Start() - self:F2( self.TimeEventObject ) - - self.Repeat = true - timer.scheduleFunction( self._Scheduler, self, timer.getTime() + self.StartSeconds + .01 ) - - return self -end - ---- Stops the scheduler. --- @param #SCHEDULER self --- @return #SCHEDULER self -function SCHEDULER:Stop() - self:F2( self.TimeEventObject ) - - self.Repeat = false - - return self -end - --- Private Functions - -function SCHEDULER:_Scheduler() - self:F2( self.TimeEventFunctionArguments ) - - local ErrorHandler = function( errmsg ) - - env.info( "Error in SCHEDULER function:" .. errmsg ) - env.info( debug.traceback() ) - - return errmsg - end - - local Status, Result - if self.TimeEventObject then - Status, Result = xpcall( function() return self.TimeEventFunction( self.TimeEventObject, unpack( self.TimeEventFunctionArguments ) ) end, ErrorHandler ) - else - Status, Result = xpcall( function() return self.TimeEventFunction( unpack( self.TimeEventFunctionArguments ) ) end, ErrorHandler ) - end - - self:T( { Status, Result } ) - - if Status and Status == true and Result and Result == true then - if self.Repeat and ( not self.StopSeconds or ( self.StopSeconds and timer.getTime() <= self.StartTime + self.StopSeconds ) ) then - timer.scheduleFunction( - self._Scheduler, - self, - timer.getTime() + self.RepeatSecondsInterval + math.random( - ( self.RandomizationFactor * self.RepeatSecondsInterval / 2 ), ( self.RandomizationFactor * self.RepeatSecondsInterval / 2 ) ) + 0.01 - ) - end - end - -end - - - - - - - - ---- The EVENT class models an efficient event handling process between other classes and its units, weapons. --- @module Event --- @author FlightControl - -Include.File( "Routines" ) -Include.File( "Base" ) - ---- The EVENT structure --- @type EVENT --- @field #EVENT.Events Events -EVENT = { - ClassName = "EVENT", - ClassID = 0, -} - -local _EVENTCODES = { - "S_EVENT_SHOT", - "S_EVENT_HIT", - "S_EVENT_TAKEOFF", - "S_EVENT_LAND", - "S_EVENT_CRASH", - "S_EVENT_EJECTION", - "S_EVENT_REFUELING", - "S_EVENT_DEAD", - "S_EVENT_PILOT_DEAD", - "S_EVENT_BASE_CAPTURED", - "S_EVENT_MISSION_START", - "S_EVENT_MISSION_END", - "S_EVENT_TOOK_CONTROL", - "S_EVENT_REFUELING_STOP", - "S_EVENT_BIRTH", - "S_EVENT_HUMAN_FAILURE", - "S_EVENT_ENGINE_STARTUP", - "S_EVENT_ENGINE_SHUTDOWN", - "S_EVENT_PLAYER_ENTER_UNIT", - "S_EVENT_PLAYER_LEAVE_UNIT", - "S_EVENT_PLAYER_COMMENT", - "S_EVENT_SHOOTING_START", - "S_EVENT_SHOOTING_END", - "S_EVENT_MAX", -} - ---- The Event structure --- @type EVENTDATA --- @field id --- @field initiator --- @field target --- @field weapon --- @field IniDCSUnit --- @field IniDCSUnitName --- @field IniDCSGroup --- @field IniDCSGroupName --- @field TgtDCSUnit --- @field TgtDCSUnitName --- @field TgtDCSGroup --- @field TgtDCSGroupName --- @field Weapon --- @field WeaponName --- @field WeaponTgtDCSUnit - ---- The Events structure --- @type EVENT.Events --- @field #number IniUnit - -function EVENT:New() - local self = BASE:Inherit( self, BASE:New() ) - self:F() - self.EventHandler = world.addEventHandler( self ) - return self -end - -function EVENT:EventText( EventID ) - - local EventText = _EVENTCODES[EventID] - - return EventText -end - - ---- Initializes the Events structure for the event --- @param #EVENT self --- @param DCSWorld#world.event EventID --- @param #string EventClass --- @return #EVENT.Events -function EVENT:Init( EventID, EventClass ) - self:F3( { _EVENTCODES[EventID], EventClass } ) - if not self.Events[EventID] then - self.Events[EventID] = {} - end - if not self.Events[EventID][EventClass] then - self.Events[EventID][EventClass] = {} - end - return self.Events[EventID][EventClass] -end - - ---- Create an OnDead event handler for a group --- @param #EVENT self --- @param #table EventTemplate --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param EventSelf The self instance of the class for which the event is. --- @param #function OnEventFunction --- @return #EVENT -function EVENT:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, OnEventFunction ) - self:F2( EventTemplate.name ) - - for EventUnitID, EventUnit in pairs( EventTemplate.units ) do - OnEventFunction( self, EventUnit.name, EventFunction, EventSelf ) - end - return self -end - ---- Set a new listener for an S_EVENT_X event independent from a unit or a weapon. --- @param #EVENT self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @param EventID --- @return #EVENT -function EVENT:OnEventGeneric( EventFunction, EventSelf, EventID ) - self:F2( { EventID } ) - - local Event = self:Init( EventID, EventSelf:GetClassNameAndID() ) - Event.EventFunction = EventFunction - Event.EventSelf = EventSelf - return self -end - - ---- Set a new listener for an S_EVENT_X event --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @param EventID --- @return #EVENT -function EVENT:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, EventID ) - self:F2( EventDCSUnitName ) - - local Event = self:Init( EventID, EventSelf:GetClassNameAndID() ) - if not Event.IniUnit then - Event.IniUnit = {} - end - Event.IniUnit[EventDCSUnitName] = {} - Event.IniUnit[EventDCSUnitName].EventFunction = EventFunction - Event.IniUnit[EventDCSUnitName].EventSelf = EventSelf - return self -end - - ---- Create an OnBirth event handler for a group --- @param #EVENT self --- @param Group#GROUP EventGroup --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnBirthForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnBirthForUnit ) - - return self -end - ---- Set a new listener for an S_EVENT_BIRTH event, and registers the unit born. --- @param #EVENT self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf --- @return #EVENT -function EVENT:OnBirth( EventFunction, EventSelf ) - self:F() - - self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_BIRTH ) - - return self -end - ---- Set a new listener for an S_EVENT_BIRTH event. --- @param #EVENT self --- @param #string EventDCSUnitName The id of the unit for the event to be handled. --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf --- @return #EVENT -function EVENT:OnBirthForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_BIRTH ) - - return self -end - ---- Create an OnCrash event handler for a group --- @param #EVENT self --- @param Group#GROUP EventGroup --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnCrashForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnCrashForUnit ) - - return self -end - ---- Set a new listener for an S_EVENT_CRASH event. --- @param #EVENT self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf --- @return #EVENT -function EVENT:OnCrash( EventFunction, EventSelf ) - self:F() - - self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_CRASH ) - - return self -end - ---- Set a new listener for an S_EVENT_CRASH event. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnCrashForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_CRASH ) - - return self -end - ---- Create an OnDead event handler for a group --- @param #EVENT self --- @param Group#GROUP EventGroup --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnDeadForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnDeadForUnit ) - - return self -end - ---- Set a new listener for an S_EVENT_DEAD event. --- @param #EVENT self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf --- @return #EVENT -function EVENT:OnDead( EventFunction, EventSelf ) - self:F() - - self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_DEAD ) - - return self -end - - ---- Set a new listener for an S_EVENT_DEAD event. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnDeadForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_DEAD ) - - return self -end - ---- Set a new listener for an S_EVENT_PILOT_DEAD event. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnPilotDeadForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_PILOT_DEAD ) - - return self -end - ---- Create an OnDead event handler for a group --- @param #EVENT self --- @param #table EventTemplate --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnLandForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnLandForUnit ) - - return self -end - ---- Set a new listener for an S_EVENT_LAND event. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnLandForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_LAND ) - - return self -end - ---- Create an OnDead event handler for a group --- @param #EVENT self --- @param #table EventTemplate --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnTakeOffForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnTakeOffForUnit ) - - return self -end - ---- Set a new listener for an S_EVENT_TAKEOFF event. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnTakeOffForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_TAKEOFF ) - - return self -end - ---- Create an OnDead event handler for a group --- @param #EVENT self --- @param #table EventTemplate --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnEngineShutDownForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnEngineShutDownForUnit ) - - return self -end - ---- Set a new listener for an S_EVENT_ENGINE_SHUTDOWN event. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnEngineShutDownForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_ENGINE_SHUTDOWN ) - - return self -end - ---- Set a new listener for an S_EVENT_ENGINE_STARTUP event. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnEngineStartUpForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_ENGINE_STARTUP ) - - return self -end - ---- Set a new listener for an S_EVENT_SHOT event. --- @param #EVENT self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnShot( EventFunction, EventSelf ) - self:F() - - self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_SHOT ) - - return self -end - ---- Set a new listener for an S_EVENT_SHOT event for a unit. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnShotForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_SHOT ) - - return self -end - ---- Set a new listener for an S_EVENT_HIT event. --- @param #EVENT self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnHit( EventFunction, EventSelf ) - self:F() - - self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_HIT ) - - return self -end - ---- Set a new listener for an S_EVENT_HIT event. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnHitForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_HIT ) - - return self -end - ---- Set a new listener for an S_EVENT_PLAYER_ENTER_UNIT event. --- @param #EVENT self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnPlayerEnterUnit( EventFunction, EventSelf ) - self:F() - - self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_PLAYER_ENTER_UNIT ) - - return self -end - ---- Set a new listener for an S_EVENT_PLAYER_LEAVE_UNIT event. --- @param #EVENT self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnPlayerLeaveUnit( EventFunction, EventSelf ) - self:F() - - self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_PLAYER_LEAVE_UNIT ) - - return self -end - - - -function EVENT:onEvent( Event ) - self:F( { _EVENTCODES[Event.id], Event } ) - - if self and self.Events and self.Events[Event.id] then - if Event.initiator and Event.initiator:getCategory() == Object.Category.UNIT then - Event.IniDCSUnit = Event.initiator - Event.IniDCSGroup = Event.IniDCSUnit:getGroup() - Event.IniDCSUnitName = Event.IniDCSUnit:getName() - Event.IniDCSGroupName = "" - if Event.IniDCSGroup and Event.IniDCSGroup:isExist() then - Event.IniDCSGroupName = Event.IniDCSGroup:getName() - end - end - if Event.target then - if Event.target and Event.target:getCategory() == Object.Category.UNIT then - Event.TgtDCSUnit = Event.target - Event.TgtDCSGroup = Event.TgtDCSUnit:getGroup() - Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() - Event.TgtDCSGroupName = "" - if Event.TgtDCSGroup and Event.TgtDCSGroup:isExist() then - Event.TgtDCSGroupName = Event.TgtDCSGroup:getName() - end - end - end - if Event.weapon then - Event.Weapon = Event.weapon - Event.WeaponName = Event.Weapon:getTypeName() - --Event.WeaponTgtDCSUnit = Event.Weapon:getTarget() - end - self:E( { _EVENTCODES[Event.id], Event } ) - for ClassName, EventData in pairs( self.Events[Event.id] ) do - if Event.IniDCSUnitName and EventData.IniUnit and EventData.IniUnit[Event.IniDCSUnitName] then - self:T2( { "Calling event function for class ", ClassName, " unit ", Event.IniDCSUnitName } ) - EventData.IniUnit[Event.IniDCSUnitName].EventFunction( EventData.IniUnit[Event.IniDCSUnitName].EventSelf, Event ) - else - if Event.IniDCSUnit and not EventData.IniUnit then - self:T2( { "Calling event function for class ", ClassName } ) - EventData.EventFunction( EventData.EventSelf, Event ) - end - end - end - end -end - ---- Encapsulation of DCS World Menu system in a set of MENU classes. --- @module Menu - -Include.File( "Routines" ) -Include.File( "Base" ) - ---- The MENU class --- @type MENU --- @extends Base#BASE -MENU = { - ClassName = "MENU", - MenuPath = nil, - MenuText = "", - MenuParentPath = nil -} - ---- -function MENU:New( MenuText, MenuParentPath ) - - -- Arrange meta tables - local Child = BASE:Inherit( self, BASE:New() ) - - Child.MenuPath = nil - Child.MenuText = MenuText - Child.MenuParentPath = MenuParentPath - return Child -end - ---- The COMMANDMENU class --- @type COMMANDMENU --- @extends Menu#MENU -COMMANDMENU = { - ClassName = "COMMANDMENU", - CommandMenuFunction = nil, - CommandMenuArgument = nil -} - -function COMMANDMENU:New( MenuText, ParentMenu, CommandMenuFunction, CommandMenuArgument ) - - -- Arrange meta tables - - local MenuParentPath = nil - if ParentMenu ~= nil then - MenuParentPath = ParentMenu.MenuPath - end - - local Child = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) - - Child.MenuPath = missionCommands.addCommand( MenuText, MenuParentPath, CommandMenuFunction, CommandMenuArgument ) - Child.CommandMenuFunction = CommandMenuFunction - Child.CommandMenuArgument = CommandMenuArgument - return Child -end - ---- The SUBMENU class --- @type SUBMENU --- @extends Menu#MENU -SUBMENU = { - ClassName = "SUBMENU" -} - -function SUBMENU:New( MenuText, ParentMenu ) - - -- Arrange meta tables - local MenuParentPath = nil - if ParentMenu ~= nil then - MenuParentPath = ParentMenu.MenuPath - end - - local Child = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) - - Child.MenuPath = missionCommands.addSubMenu( MenuText, MenuParentPath ) - return Child -end - --- This local variable is used to cache the menus registered under clients. --- Menus don't dissapear when clients are destroyed and restarted. --- So every menu for a client created must be tracked so that program logic accidentally does not create --- the same menus twice during initialization logic. --- These menu classes are handling this logic with this variable. -local _MENUCLIENTS = {} - ---- The MENU_CLIENT class --- @type MENU_CLIENT --- @extends Menu#MENU -MENU_CLIENT = { - ClassName = "MENU_CLIENT" -} - ---- Creates a new menu item for a group --- @param self --- @param Client#CLIENT MenuClient The Client owning the menu. --- @param #string MenuText The text for the menu. --- @param #table ParentMenu The parent menu. --- @return #MENU_CLIENT self -function MENU_CLIENT:New( MenuClient, MenuText, ParentMenu ) - - -- Arrange meta tables - local MenuParentPath = {} - if ParentMenu ~= nil then - MenuParentPath = ParentMenu.MenuPath - end - - local self = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) - self:F( { MenuClient, MenuText, ParentMenu } ) - - self.MenuClient = MenuClient - self.MenuClientGroupID = MenuClient:GetClientGroupID() - self.MenuParentPath = MenuParentPath - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - self.Menus = {} - - if not _MENUCLIENTS[self.MenuClientGroupID] then - _MENUCLIENTS[self.MenuClientGroupID] = {} - end - - local MenuPath = _MENUCLIENTS[self.MenuClientGroupID] - - self:T( { MenuClient:GetClientGroupName(), MenuPath[table.concat(MenuParentPath)], MenuParentPath, MenuText } ) - - local MenuPathID = table.concat(MenuParentPath) .. "/" .. MenuText - if MenuPath[MenuPathID] then - missionCommands.removeItemForGroup( self.MenuClient:GetClientGroupID(), MenuPath[MenuPathID] ) - end - - self.MenuPath = missionCommands.addSubMenuForGroup( self.MenuClient:GetClientGroupID(), MenuText, MenuParentPath ) - MenuPath[MenuPathID] = self.MenuPath - - self:T( { MenuClient:GetClientGroupName(), self.MenuPath } ) - - if ParentMenu and ParentMenu.Menus then - ParentMenu.Menus[self.MenuPath] = self - end - return self -end - ---- Removes the sub menus recursively of this MENU_CLIENT. --- @param #MENU_CLIENT self --- @return #MENU_CLIENT self -function MENU_CLIENT:RemoveSubMenus() - self:F( self.MenuPath ) - - for MenuID, Menu in pairs( self.Menus ) do - Menu:Remove() - end - -end - ---- Removes the sub menus recursively of this MENU_CLIENT. --- @param #MENU_CLIENT self --- @return #MENU_CLIENT self -function MENU_CLIENT:Remove() - self:F( self.MenuPath ) - - self:RemoveSubMenus() - - if not _MENUCLIENTS[self.MenuClientGroupID] then - _MENUCLIENTS[self.MenuClientGroupID] = {} - end - - local MenuPath = _MENUCLIENTS[self.MenuClientGroupID] - - if MenuPath[table.concat(self.MenuParentPath) .. "/" .. self.MenuText] then - MenuPath[table.concat(self.MenuParentPath) .. "/" .. self.MenuText] = nil - end - - missionCommands.removeItemForGroup( self.MenuClient:GetClientGroupID(), self.MenuPath ) - self.ParentMenu.Menus[self.MenuPath] = nil - return nil -end - - ---- The MENU_CLIENT_COMMAND class --- @type MENU_CLIENT_COMMAND --- @extends Menu#MENU -MENU_CLIENT_COMMAND = { - ClassName = "MENU_CLIENT_COMMAND" -} - ---- Creates a new radio command item for a group --- @param self --- @param Client#CLIENT MenuClient The Client owning the menu. --- @param MenuText The text for the menu. --- @param ParentMenu The parent menu. --- @param CommandMenuFunction A function that is called when the menu key is pressed. --- @param CommandMenuArgument An argument for the function. --- @return Menu#MENU_CLIENT_COMMAND self -function MENU_CLIENT_COMMAND:New( MenuClient, MenuText, ParentMenu, CommandMenuFunction, CommandMenuArgument ) - - -- Arrange meta tables - - local MenuParentPath = {} - if ParentMenu ~= nil then - MenuParentPath = ParentMenu.MenuPath - end - - local self = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) - - self.MenuClient = MenuClient - self.MenuClientGroupID = MenuClient:GetClientGroupID() - self.MenuParentPath = MenuParentPath - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - if not _MENUCLIENTS[self.MenuClientGroupID] then - _MENUCLIENTS[self.MenuClientGroupID] = {} - end - - local MenuPath = _MENUCLIENTS[self.MenuClientGroupID] - - self:T( { MenuClient:GetClientGroupName(), MenuPath[table.concat(MenuParentPath)], MenuParentPath, MenuText, CommandMenuFunction, CommandMenuArgument } ) - - local MenuPathID = table.concat(MenuParentPath) .. "/" .. MenuText - if MenuPath[MenuPathID] then - missionCommands.removeItemForGroup( self.MenuClient:GetClientGroupID(), MenuPath[MenuPathID] ) - end - - self.MenuPath = missionCommands.addCommandForGroup( self.MenuClient:GetClientGroupID(), MenuText, MenuParentPath, CommandMenuFunction, CommandMenuArgument ) - MenuPath[MenuPathID] = self.MenuPath - - self.CommandMenuFunction = CommandMenuFunction - self.CommandMenuArgument = CommandMenuArgument - - ParentMenu.Menus[self.MenuPath] = self - - return self -end - -function MENU_CLIENT_COMMAND:Remove() - self:F( self.MenuPath ) - - if not _MENUCLIENTS[self.MenuClientGroupID] then - _MENUCLIENTS[self.MenuClientGroupID] = {} - end - - local MenuPath = _MENUCLIENTS[self.MenuClientGroupID] - - if MenuPath[table.concat(self.MenuParentPath) .. "/" .. self.MenuText] then - MenuPath[table.concat(self.MenuParentPath) .. "/" .. self.MenuText] = nil - end - - missionCommands.removeItemForGroup( self.MenuClient:GetClientGroupID(), self.MenuPath ) - self.ParentMenu.Menus[self.MenuPath] = nil - return nil -end - - ---- The MENU_COALITION class --- @type MENU_COALITION --- @extends Menu#MENU -MENU_COALITION = { - ClassName = "MENU_COALITION" -} - ---- Creates a new coalition menu item --- @param #MENU_COALITION self --- @param DCSCoalition#coalition.side MenuCoalition The coalition owning the menu. --- @param #string MenuText The text for the menu. --- @param #table ParentMenu The parent menu. --- @return #MENU_COALITION self -function MENU_COALITION:New( MenuCoalition, MenuText, ParentMenu ) - - -- Arrange meta tables - local MenuParentPath = {} - if ParentMenu ~= nil then - MenuParentPath = ParentMenu.MenuPath - end - - local self = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) - self:F( { MenuCoalition, MenuText, ParentMenu } ) - - self.MenuCoalition = MenuCoalition - self.MenuParentPath = MenuParentPath - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - self.Menus = {} - - self:T( { MenuParentPath, MenuText } ) - - self.MenuPath = missionCommands.addSubMenuForCoalition( self.MenuCoalition, MenuText, MenuParentPath ) - - self:T( { self.MenuPath } ) - - if ParentMenu and ParentMenu.Menus then - ParentMenu.Menus[self.MenuPath] = self - end - return self -end - ---- Removes the sub menus recursively of this MENU_COALITION. --- @param #MENU_COALITION self --- @return #MENU_COALITION self -function MENU_COALITION:RemoveSubMenus() - self:F( self.MenuPath ) - - for MenuID, Menu in pairs( self.Menus ) do - Menu:Remove() - end - -end - ---- Removes the sub menus recursively of this MENU_COALITION. --- @param #MENU_COALITION self --- @return #MENU_COALITION self -function MENU_COALITION:Remove() - self:F( self.MenuPath ) - - self:RemoveSubMenus() - missionCommands.removeItemForCoalition( self.MenuCoalition, self.MenuPath ) - self.ParentMenu.Menus[self.MenuPath] = nil - - return nil -end - - ---- The MENU_COALITION_COMMAND class --- @type MENU_COALITION_COMMAND --- @extends Menu#MENU -MENU_COALITION_COMMAND = { - ClassName = "MENU_COALITION_COMMAND" -} - ---- Creates a new radio command item for a group --- @param #MENU_COALITION_COMMAND self --- @param DCSCoalition#coalition.side MenuCoalition The coalition owning the menu. --- @param MenuText The text for the menu. --- @param ParentMenu The parent menu. --- @param CommandMenuFunction A function that is called when the menu key is pressed. --- @param CommandMenuArgument An argument for the function. --- @return #MENU_COALITION_COMMAND self -function MENU_COALITION_COMMAND:New( MenuCoalition, MenuText, ParentMenu, CommandMenuFunction, CommandMenuArgument ) - - -- Arrange meta tables - - local MenuParentPath = {} - if ParentMenu ~= nil then - MenuParentPath = ParentMenu.MenuPath - end - - local self = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) - - self.MenuCoalition = MenuCoalition - self.MenuParentPath = MenuParentPath - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - self:T( { MenuParentPath, MenuText, CommandMenuFunction, CommandMenuArgument } ) - - self.MenuPath = missionCommands.addCommandForCoalition( self.MenuCoalition, MenuText, MenuParentPath, CommandMenuFunction, CommandMenuArgument ) - - self.CommandMenuFunction = CommandMenuFunction - self.CommandMenuArgument = CommandMenuArgument - - ParentMenu.Menus[self.MenuPath] = self - - return self -end - ---- Removes a radio command item for a coalition --- @param #MENU_COALITION_COMMAND self --- @return #MENU_COALITION_COMMAND self -function MENU_COALITION_COMMAND:Remove() - self:F( self.MenuPath ) - - missionCommands.removeItemForCoalition( self.MenuCoalition, self.MenuPath ) - self.ParentMenu.Menus[self.MenuPath] = nil - return nil -end ---- GROUP class. --- --- @{GROUP} class --- ============== --- The @{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. --- --- --- 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. --- --- IMPORTANT: ONE SHOULD NEVER SANATIZE these GROUP OBJECT REFERENCES! (make the GROUP object references nil). --- @module Group --- @author FlightControl - -Include.File( "Routines" ) -Include.File( "Base" ) -Include.File( "Message" ) -Include.File( "Unit" ) - ---- The GROUP class --- @type GROUP --- @extends Base#BASE --- @field DCSGroup#Group DCSGroup The DCS group class. --- @field #string GroupName The name of the group. -GROUP = { - ClassName = "GROUP", - GroupName = "", - GroupID = 0, - Controller = nil, - DCSGroup = nil, - WayPointFunctions = {}, - } - ---- A DCSGroup --- @type DCSGroup --- @field id_ The ID of the group in DCS - ---- Create a new GROUP from a DCSGroup --- @param #GROUP self --- @param DCSGroup#Group GroupName The DCS Group name --- @return #GROUP self -function GROUP:Register( GroupName ) - local self = BASE:Inherit( self, BASE:New() ) - self:F2( GroupName ) - self.GroupName = GroupName - return self -end - --- Reference methods. - ---- Find the GROUP wrapper class instance using the DCS Group. --- @param #GROUP self --- @param DCSGroup#Group DCSGroup The DCS Group. --- @return #GROUP The GROUP. -function GROUP:Find( DCSGroup ) - - local GroupName = DCSGroup:getName() -- Group#GROUP - local GroupFound = _DATABASE:FindGroup( GroupName ) - return GroupFound -end - ---- Find the created GROUP using the DCS Group Name. --- @param #GROUP self --- @param #string GroupName The DCS Group Name. --- @return #GROUP The GROUP. -function GROUP:FindByName( GroupName ) - - local GroupFound = _DATABASE:FindGroup( GroupName ) - return GroupFound -end - --- DCS Group methods support. - ---- Returns the DCS Group. --- @param #GROUP self --- @return DCSGroup#Group The DCS Group. -function GROUP:GetDCSGroup() - local DCSGroup = Group.getByName( self.GroupName ) - - if DCSGroup then - return DCSGroup - end - - return nil -end - - ---- Returns if the DCS Group is alive. --- When the group exists at run-time, this method will return true, otherwise false. --- @param #GROUP self --- @return #boolean true if the DCS Group is alive. -function GROUP:IsAlive() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSGroup() - - if DCSGroup then - local GroupIsAlive = DCSGroup:isExist() - self:T3( GroupIsAlive ) - return GroupIsAlive - end - - return nil -end - ---- Destroys the DCS Group and all of its DCS Units. --- Note that this destroy method also raises a destroy event at run-time. --- So all event listeners will catch the destroy event of this DCS Group. --- @param #GROUP self -function GROUP:Destroy() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSGroup() - - if DCSGroup then - for Index, UnitData in pairs( DCSGroup:getUnits() ) do - self:CreateEventCrash( timer.getTime(), UnitData ) - end - DCSGroup:destroy() - DCSGroup = nil - end - - return nil -end - ---- Returns category of the DCS Group. --- @param #GROUP self --- @return DCSGroup#Group.Category The category ID -function GROUP:GetCategory() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup then - local GroupCategory = DCSGroup:getCategory() - self:T3( GroupCategory ) - return GroupCategory - end - - return nil -end - ---- Returns the category name of the DCS Group. --- @param #GROUP self --- @return #string Category name = Helicopter, Airplane, Ground Unit, Ship -function GROUP:GetCategoryName() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup then - local CategoryNames = { - [Group.Category.AIRPLANE] = "Airplane", - [Group.Category.HELICOPTER] = "Helicopter", - [Group.Category.GROUND] = "Ground Unit", - [Group.Category.SHIP] = "Ship", - } - local GroupCategory = DCSGroup:getCategory() - self:T3( GroupCategory ) - - return CategoryNames[GroupCategory] - end - - return nil -end - - ---- Returns the coalition of the DCS Group. --- @param #GROUP self --- @return DCSCoalitionObject#coalition.side The coalition side of the DCS Group. -function GROUP:GetCoalition() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup then - local GroupCoalition = DCSGroup:getCoalition() - self:T3( GroupCoalition ) - return GroupCoalition - end - - return nil -end - ---- Returns the name of the DCS Group. --- @param #GROUP self --- @return #string The DCS Group name. -function GROUP:GetName() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSGroup() - - if DCSGroup then - local GroupName = DCSGroup:getName() - self:T3( GroupName ) - return GroupName - end - - return nil -end - ---- Returns the DCS Group identifier. --- @param #GROUP self --- @return #number The identifier of the DCS Group. -function GROUP:GetID() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSGroup() - - if DCSGroup then - local GroupID = DCSGroup:getID() - self:T3( GroupID ) - return GroupID - end - - return nil -end - ---- Returns the UNIT wrapper class with number UnitNumber. --- If the underlying DCS Unit does not exist, the method will return nil. . --- @param #GROUP self --- @param #number UnitNumber The number of the UNIT wrapper class to be returned. --- @return Unit#UNIT The UNIT wrapper class. -function GROUP:GetUnit( UnitNumber ) - self:F2( { self.GroupName, UnitNumber } ) - - local DCSGroup = self:GetDCSGroup() - - if DCSGroup then - local UnitFound = UNIT:Find( DCSGroup:getUnit( UnitNumber ) ) - self:T3( UnitFound.UnitName ) - self:T2( UnitFound ) - return UnitFound - end - - return nil -end - ---- Returns the DCS Unit with number UnitNumber. --- If the underlying DCS Unit does not exist, the method will return nil. . --- @param #GROUP self --- @param #number UnitNumber The number of the DCS Unit to be returned. --- @return DCSUnit#Unit The DCS Unit. -function GROUP:GetDCSUnit( UnitNumber ) - self:F2( { self.GroupName, UnitNumber } ) - - local DCSGroup = self:GetDCSGroup() - - 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:GetDCSGroup() - - 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:GetDCSGroup() - - if DCSGroup then - local GroupInitialSize = DCSGroup:getInitialSize() - self:T3( GroupInitialSize ) - return GroupInitialSize - end - - return nil -end - ---- Returns the UNITs wrappers of the DCS Units of the DCS Group. --- @param #GROUP self --- @return #table The UNITs wrappers. -function GROUP:GetUnits() - self:F2( { self.GroupName } ) - local DCSGroup = self:GetDCSGroup() - - if DCSGroup then - local DCSUnits = DCSGroup:getUnits() - local Units = {} - for Index, UnitData in pairs( DCSUnits ) do - Units[#Units+1] = UNIT:Find( UnitData ) - end - self:T3( Units ) - return Units - end - - return nil -end - - ---- Returns the DCS Units of the DCS Group. --- @param #GROUP self --- @return #table The DCS Units. -function GROUP:GetDCSUnits() - self:F2( { self.GroupName } ) - local DCSGroup = self:GetDCSGroup() - - if DCSGroup then - local DCSUnits = DCSGroup:getUnits() - self:T3( DCSUnits ) - return DCSUnits - end - - return nil -end - ---- Get the controller for the GROUP. --- @param #GROUP self --- @return DCSController#Controller -function GROUP:_GetController() - self:F2( { self.GroupName } ) - local DCSGroup = self:GetDCSGroup() - - if DCSGroup then - local GroupController = DCSGroup:getController() - self:T3( GroupController ) - return GroupController - end - - return nil -end - - ---- Retrieve the group mission and allow to place function hooks within the mission waypoint plan. --- Use the method @{Group#GROUP:WayPointFunction} to define the hook functions for specific waypoints. --- Use the method @{Group@GROUP:WayPointExecute) to start the execution of the new mission plan. --- Note that when WayPointInitialize is called, the Mission of the group is RESTARTED! --- @param #GROUP self --- @return #GROUP -function GROUP:WayPointInitialize() - - self.WayPoints = self:GetTaskRoute() - - return self -end - - ---- Registers a waypoint function that will be executed when the group moves over the WayPoint. --- @param #GROUP 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 group moves over the waypoint. The waypoint function takes variable parameters. --- @return #GROUP -function GROUP: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 GROUP:TaskFunction( WayPoint, WayPointIndex, FunctionString, FunctionArguments ) - - local DCSTask - - local DCSScript = {} - DCSScript[#DCSScript+1] = "local MissionGroup = GROUP:Find( ... ) " - - if FunctionArguments.n > 0 then - DCSScript[#DCSScript+1] = FunctionString .. "( MissionGroup, " .. table.concat( FunctionArguments, "," ) .. ")" - else - DCSScript[#DCSScript+1] = FunctionString .. "( MissionGroup )" - 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 group will be 1! --- @param #GROUP self --- @param #number WayPoint The WayPoint from where to execute the mission. --- @param #WaitTime The amount seconds to wait before initiating the mission. --- @return #GROUP -function GROUP:WayPointExecute( WayPoint, WaitTime ) - - if not WayPoint then - WayPoint = 1 - end - - -- When starting the mission from a certain point, the TaskPoints need to be deleted before the given WayPoint. - for TaskPointID = 1, WayPoint - 1 do - table.remove( self.WayPoints, 1 ) - end - - self:T3( self.WayPoints ) - - self:SetTask( self:TaskRoute( self.WayPoints ), WaitTime ) - - return self -end - - ---- Activates a GROUP. --- @param #GROUP self -function GROUP:Activate() - self:F2( { self.GroupName } ) - trigger.action.activateGroup( self:GetDCSGroup() ) - return self:GetDCSGroup() -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:GetDCSGroup() - - 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:GetDCSGroup() - - if DCSGroup then - local GroupCallSign = DCSGroup:getUnit(1):getCallsign() - self:T3( GroupCallSign ) - return GroupCallSign - end - - return nil -end - ---- Returns the current point (Vec2 vector) of the first DCS Unit in the DCS Group. --- @return DCSTypes#Vec2 Current Vec2 point of the first DCS Unit of the DCS Group. -function GROUP:GetPointVec2() - self:F2( self.GroupName ) - - local GroupPointVec2 = self:GetUnit(1):GetPointVec2() - self:T3( GroupPointVec2 ) - return GroupPointVec2 -end - ---- Returns the current point (Vec3 vector) of the first DCS Unit in the DCS Group. --- @return DCSTypes#Vec3 Current Vec3 point of the first DCS Unit of the DCS Group. -function GROUP:GetPointVec3() - self:F2( self.GroupName ) - - local GroupPointVec3 = self:GetUnit(1):GetPointVec3() - self:T3( GroupPointVec3 ) - return GroupPointVec3 -end - - - --- Is Functions - ---- 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:GetDCSGroup() - - 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:GetDCSGroup() - - 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:GetDCSGroup() - - 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:GetDCSGroup() - - 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:GetDCSGroup() - - 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:GetDCSGroup() - - if DCSGroup then - local AllOnGroundResult = true - - for Index, UnitData in pairs( DCSGroup:getUnits() ) do - if UnitData:inAir() then - AllOnGroundResult = false - end - end - - self:T3( AllOnGroundResult ) - return AllOnGroundResult - end - - return nil -end - ---- Returns the current maximum velocity of the group. --- Each unit within the group gets evaluated, and the maximum velocity (= the unit which is going the fastest) is returned. --- @param #GROUP self --- @return #number Maximum velocity found. -function GROUP:GetMaxVelocity() - self:F2() - - local DCSGroup = self:GetDCSGroup() - - if DCSGroup then - local MaxVelocity = 0 - - for Index, UnitData in pairs( DCSGroup:getUnits() ) do - - local Velocity = UnitData:getVelocity() - local VelocityTotal = math.abs( Velocity.x ) + math.abs( Velocity.y ) + math.abs( Velocity.z ) - - if VelocityTotal < MaxVelocity then - MaxVelocity = VelocityTotal - end - end - - return MaxVelocity - end - - return nil -end - ---- Returns the current minimum height of the group. --- Each unit within the group gets evaluated, and the minimum height (= the unit which is the lowest elevated) is returned. --- @param #GROUP self --- @return #number Minimum height found. -function GROUP:GetMinHeight() - self:F2() - -end - ---- Returns the current maximum height of the group. --- Each unit within the group gets evaluated, and the maximum height (= the unit which is the highest elevated) is returned. --- @param #GROUP self --- @return #number Maximum height found. -function GROUP:GetMaxHeight() - self:F2() - -end - --- Tasks - ---- Popping current Task from the group. --- @param #GROUP self --- @return Group#GROUP self -function GROUP:PopCurrentTask() - self:F2() - - local DCSGroup = self:GetDCSGroup() - - if DCSGroup then - local Controller = self:_GetController() - Controller:popTask() - return self - end - - return nil -end - ---- Pushing Task on the queue from the group. --- @param #GROUP self --- @return Group#GROUP self -function GROUP:PushTask( DCSTask, WaitTime ) - self:F2() - - local DCSGroup = self:GetDCSGroup() - - if DCSGroup then - local Controller = self:_GetController() - - -- When a group SPAWNs, it takes about a second to get the group in the simulator. Setting tasks to unspawned groups provides unexpected results. - -- Therefore we schedule the functions to set the mission and options for the Group. - -- Controller:pushTask( DCSTask ) - - if WaitTime then - --routines.scheduleFunction( Controller.pushTask, { Controller, DCSTask }, timer.getTime() + WaitTime ) - SCHEDULER:New( Controller, Controller.pushTask, { DCSTask }, WaitTime ) - else - Controller:pushTask( DCSTask ) - end - - return self - end - - return nil -end - ---- Clearing the Task Queue and Setting the Task on the queue from the group. --- @param #GROUP self --- @return Group#GROUP self -function GROUP:SetTask( DCSTask, WaitTime ) - self:F2( { DCSTask } ) - - local DCSGroup = self:GetDCSGroup() - - if DCSGroup then - - local Controller = self:_GetController() - - -- When a group SPAWNs, it takes about a second to get the group in the simulator. Setting tasks to unspawned groups provides unexpected results. - -- Therefore we schedule the functions to set the mission and options for the Group. - -- Controller.setTask( Controller, DCSTask ) - - if not WaitTime then - WaitTime = 1 - end - --routines.scheduleFunction( Controller.setTask, { Controller, DCSTask }, timer.getTime() + WaitTime ) - SCHEDULER:New( Controller, Controller.setTask, { DCSTask }, WaitTime ) - - return self - end - - return nil -end - - ---- Return a condition section for a controlled task --- @param #GROUP self --- @param DCSTime#Time time --- @param #string userFlag --- @param #boolean userFlagValue --- @param #string condition --- @param DCSTime#Time duration --- @param #number lastWayPoint --- return DCSTask#Task -function GROUP: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 #GROUP self --- @param DCSTask#Task DCSTask --- @param #DCSStopCondition DCSStopCondition --- @return DCSTask#Task -function GROUP: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 #GROUP self --- @param #list DCSTasks --- @return DCSTask#Task -function GROUP:TaskCombo( DCSTasks ) - self:F2( { DCSTasks } ) - - local DCSTaskCombo - - DCSTaskCombo = { - id = 'ComboTask', - params = { - tasks = DCSTasks - } - } - - self:T3( { DCSTaskCombo } ) - return DCSTaskCombo -end - ---- Return a WrappedAction Task taking a Command --- @param #GROUP self --- @param DCSCommand#Command DCSCommand --- @return DCSTask#Task -function GROUP: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 #GROUP self --- @param DCSCommand#Command DCSCommand --- @return #GROUP self -function GROUP:SetCommand( DCSCommand ) - self:F2( DCSCommand ) - - local DCSGroup = self:GetDCSGroup() - - if DCSGroup then - local Controller = self:_GetController() - Controller:setCommand( DCSCommand ) - return self - end - - return nil -end - ---- Perform a switch waypoint command --- @param #GROUP self --- @param #number FromWayPoint --- @param #number ToWayPoint --- @return DCSTask#Task -function GROUP:CommandSwitchWayPoint( FromWayPoint, ToWayPoint, Index ) - self:F2( { FromWayPoint, ToWayPoint, Index } ) - - local CommandSwitchWayPoint = { - id = 'SwitchWaypoint', - params = { - fromWaypointIndex = FromWayPoint, - goToWaypointIndex = ToWayPoint, - }, - } - - self:T3( { CommandSwitchWayPoint } ) - return CommandSwitchWayPoint -end - - ---- Orbit at a specified position at a specified alititude during a specified duration with a specified speed. --- @param #GROUP self --- @param DCSTypes#Vec2 Point The point to hold the position. --- @param #number Altitude The altitude to hold the position. --- @param #number Speed The speed flying when holding the position. --- @return #GROUP self -function GROUP:TaskOrbitCircleAtVec2( Point, Altitude, Speed ) - self:F2( { self.GroupName, 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 - ---- Orbit at the current position of the first unit of the group at a specified alititude --- @param #GROUP self --- @param #number Altitude The altitude to hold the position. --- @param #number Speed The speed flying when holding the position. --- @return #GROUP self -function GROUP:TaskOrbitCircle( Altitude, Speed ) - self:F2( { self.GroupName, Altitude, Speed } ) - - local DCSGroup = self:GetDCSGroup() - - if DCSGroup then - local GroupPoint = self:GetPointVec2() - return self:TaskOrbitCircleAtVec2( GroupPoint, Altitude, Speed ) - end - - return nil -end - - - ---- Hold position at the current position of the first unit of the group. --- @param #GROUP self --- @param #number Duration The maximum duration in seconds to hold the position. --- @return #GROUP self -function GROUP:TaskHoldPosition() - self:F2( { self.GroupName } ) - - return self:TaskOrbitCircle( 30, 10 ) -end - - ---- Land the group at a Vec2Point. --- @param #GROUP self --- @param DCSTypes#Vec2 Point The point where to land. --- @param #number Duration The duration in seconds to stay on the ground. --- @return #GROUP self -function GROUP:TaskLandAtVec2( Point, Duration ) - self:F2( { self.GroupName, Point, Duration } ) - - 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 - ---- Land the group at a @{Zone#ZONE). --- @param #GROUP self --- @param Zone#ZONE Zone The zone where to land. --- @param #number Duration The duration in seconds to stay on the ground. --- @return #GROUP self -function GROUP:TaskLandAtZone( Zone, Duration, RandomPoint ) - self:F2( { self.GroupName, Zone, Duration, RandomPoint } ) - - local Point - if RandomPoint then - Point = Zone:GetRandomPointVec2() - else - Point = Zone:GetPointVec2() - end - - local DCSTask = self:TaskLandAtVec2( Point, Duration ) - - self:T3( DCSTask ) - return DCSTask -end - - ---- Attack the Unit. --- @param #GROUP self --- @param Unit#UNIT The unit. --- @return DCSTask#Task The DCS task structure. -function GROUP:TaskAttackUnit( AttackUnit ) - self:F2( { self.GroupName, AttackUnit } ) - --- AttackUnit = { --- id = 'AttackUnit', --- params = { --- unitId = Unit.ID, --- weaponType = number, --- expend = enum AI.Task.WeaponExpend --- attackQty = number, --- direction = Azimuth, --- attackQtyLimit = boolean, --- groupAttack = boolean, --- } --- } - - local DCSTask - DCSTask = { id = 'AttackUnit', - params = { unitId = AttackUnit:GetID(), - expend = AI.Task.WeaponExpend.TWO, - groupAttack = true, - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - ---- Attack a Group. --- @param #GROUP self --- @param Group#GROUP AttackGroup The Group to be attacked. --- @return DCSTask#Task The DCS task structure. -function GROUP:TaskAttackGroup( AttackGroup ) - self:F2( { self.GroupName, AttackGroup } ) - --- 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 DCSTask - DCSTask = { id = 'AttackGroup', - params = { groupId = AttackGroup:GetID(), - expend = AI.Task.WeaponExpend.TWO, - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - ---- Fires at a VEC2 point. --- @param #GROUP self --- @param DCSTypes#Vec2 The point to fire at. --- @param DCSTypes#Distance Radius The radius of the zone to deploy the fire at. --- @return DCSTask#Task The DCS task structure. -function GROUP:TaskFireAtPoint( PointVec2, Radius ) - self:F2( { self.GroupName, PointVec2, Radius } ) - --- FireAtPoint = { --- id = 'FireAtPoint', --- params = { --- point = Vec2, --- radius = Distance, --- } --- } - - local DCSTask - DCSTask = { id = 'FireAtPoint', - params = { point = PointVec2, - radius = Radius, - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - - ---- Move the group to a Vec2 Point, wait for a defined duration and embark a group. --- @param #GROUP self --- @param DCSTypes#Vec2 Point The point where to wait. --- @param #number Duration The duration in seconds to wait. --- @param #GROUP EmbarkingGroup The group to be embarked. --- @return DCSTask#Task The DCS task structure -function GROUP:TaskEmbarkingAtVec2( Point, Duration, EmbarkingGroup ) - self:F2( { self.GroupName, Point, Duration, EmbarkingGroup.DCSGroup } ) - - local DCSTask - DCSTask = { id = 'Embarking', - params = { x = Point.x, - y = Point.y, - duration = Duration, - groupsForEmbarking = { EmbarkingGroup.GroupID }, - durationFlag = true, - distributionFlag = false, - distribution = {}, - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - ---- Move to a defined Vec2 Point, and embark to a group when arrived within a defined Radius. --- @param #GROUP self --- @param DCSTypes#Vec2 Point The point where to wait. --- @param #number Radius The radius of the embarking zone around the Point. --- @return DCSTask#Task The DCS task structure. -function GROUP:TaskEmbarkToTransportAtVec2( Point, Radius ) - self:F2( { self.GroupName, Point, Radius } ) - - local DCSTask --DCSTask#Task - DCSTask = { id = 'EmbarkToTransport', - params = { x = Point.x, - y = Point.y, - zoneRadius = Radius, - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - ---- Return a Misson task from a mission template. --- @param #GROUP self --- @param #table TaskMission A table containing the mission task. --- @return DCSTask#Task -function GROUP: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 #GROUP self --- @param #table Points A table of route points. --- @return DCSTask#Task -function GROUP:TaskRoute( Points ) - self:F2( Points ) - - local DCSTask - DCSTask = { id = 'Mission', params = { route = { points = Points, }, }, } - - self:T3( { DCSTask } ) - return DCSTask -end - ---- Make the DCS Group to fly to a given point and hover. --- @param #GROUP self --- @param DCSTypes#Vec3 Point The destination point in Vec3 format. --- @param #number Speed The speed to travel. --- @return #GROUP self -function GROUP:TaskRouteToVec2( Point, Speed ) - self:F2( { Point, Speed } ) - - local GroupPoint = self:GetUnit( 1 ):GetPointVec2() - - local PointFrom = {} - PointFrom.x = GroupPoint.x - PointFrom.y = GroupPoint.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 - ---- Make the DCS Group to fly to a given point and hover. --- @param #GROUP self --- @param DCSTypes#Vec3 Point The destination point in Vec3 format. --- @param #number Speed The speed to travel. --- @return #GROUP self -function GROUP:TaskRouteToVec3( Point, Speed ) - self:F2( { Point, Speed } ) - - local GroupPoint = self:GetUnit( 1 ):GetPointVec3() - - local PointFrom = {} - PointFrom.x = GroupPoint.x - PointFrom.y = GroupPoint.z - PointFrom.alt = GroupPoint.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 group to follow a given route. --- @param #GROUP self --- @param #table GoPoints A table of Route Points. --- @return #GROUP self -function GROUP:Route( GoPoints ) - self:F2( GoPoints ) - - local DCSGroup = self:GetDCSGroup() - - if DCSGroup then - local Points = routines.utils.deepCopy( GoPoints ) - local MissionTask = { id = 'Mission', params = { route = { points = Points, }, }, } - local Controller = self:_GetController() - --Controller.setTask( Controller, MissionTask ) - --routines.scheduleFunction( Controller.setTask, { Controller, MissionTask}, timer.getTime() + 1 ) - SCHEDULER:New( Controller, Controller.setTask, { MissionTask }, 1 ) - return self - end - - return nil -end - - - ---- Route the group to a given zone. --- The group final destination point can be randomized. --- A speed can be given in km/h. --- A given formation can be given. --- @param #GROUP self --- @param Zone#ZONE Zone The zone where to route to. --- @param #boolean Randomize Defines whether to target point gets randomized within the Zone. --- @param #number Speed The speed. --- @param Base#FORMATION Formation The formation string. -function GROUP:TaskRouteToZone( Zone, Randomize, Speed, Formation ) - self:F2( Zone ) - - local DCSGroup = self:GetDCSGroup() - - if DCSGroup then - - local GroupPoint = self:GetPointVec2() - - local PointFrom = {} - PointFrom.x = GroupPoint.x - PointFrom.y = GroupPoint.y - PointFrom.type = "Turning Point" - PointFrom.action = "Cone" - PointFrom.speed = 20 / 1.6 - - - local PointTo = {} - local ZonePoint - - if Randomize then - ZonePoint = Zone:GetRandomPointVec2() - else - ZonePoint = Zone:GetPointVec2() - end - - PointTo.x = ZonePoint.x - PointTo.y = ZonePoint.y - PointTo.type = "Turning Point" - - if Formation then - PointTo.action = Formation - else - PointTo.action = "Cone" - end - - if Speed then - PointTo.speed = Speed - else - PointTo.speed = 20 / 1.6 - end - - local Points = { PointFrom, PointTo } - - self:T3( Points ) - - self:Route( Points ) - - return self - end - - return nil -end - --- Commands - ---- Do Script command --- @param #GROUP self --- @param #string DoScript --- @return #DCSCommand -function GROUP:CommandDoScript( DoScript ) - - local DCSDoScript = { - id = "Script", - params = { - command = DoScript, - }, - } - - self:T3( DCSDoScript ) - return DCSDoScript -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 - end - - return nil -end - - -function GROUP:GetDetectedTargets() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup then - return self:_GetController():getDetectedTargets() - end - - return nil -end - -function GROUP:IsTargetDetected( DCSObject ) - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup 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 GROUP hold their weapons? --- @param #GROUP self --- @return #boolean -function GROUP:OptionROEHoldFirePossible() - self:F2( { self.GroupName } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup then - if self:IsAir() or self:IsGround() or self:IsShip() then - return true - end - - return false - end - - return nil -end - ---- Holding weapons. --- @param Group#GROUP self --- @return Group#GROUP self -function GROUP:OptionROEHoldFire() - self:F2( { self.GroupName } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup 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 GROUP attack returning on enemy fire? --- @param #GROUP self --- @return #boolean -function GROUP:OptionROEReturnFirePossible() - self:F2( { self.GroupName } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup then - if self:IsAir() or self:IsGround() or self:IsShip() then - return true - end - - return false - end - - return nil -end - ---- Return fire. --- @param #GROUP self --- @return #GROUP self -function GROUP:OptionROEReturnFire() - self:F2( { self.GroupName } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup 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 GROUP attack designated targets? --- @param #GROUP self --- @return #boolean -function GROUP:OptionROEOpenFirePossible() - self:F2( { self.GroupName } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup then - if self:IsAir() or self:IsGround() or self:IsShip() then - return true - end - - return false - end - - return nil -end - ---- Openfire. --- @param #GROUP self --- @return #GROUP self -function GROUP:OptionROEOpenFire() - self:F2( { self.GroupName } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup 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 GROUP attack targets of opportunity? --- @param #GROUP self --- @return #boolean -function GROUP:OptionROEWeaponFreePossible() - self:F2( { self.GroupName } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup then - if self:IsAir() then - return true - end - - return false - end - - return nil -end - ---- Weapon free. --- @param #GROUP self --- @return #GROUP self -function GROUP:OptionROEWeaponFree() - self:F2( { self.GroupName } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup 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 GROUP ignore enemy fire? --- @param #GROUP self --- @return #boolean -function GROUP:OptionROTNoReactionPossible() - self:F2( { self.GroupName } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup then - if self:IsAir() then - return true - end - - return false - end - - return nil -end - - ---- No evasion on enemy threats. --- @param #GROUP self --- @return #GROUP self -function GROUP:OptionROTNoReaction() - self:F2( { self.GroupName } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup 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 GROUP evade using passive defenses? --- @param #GROUP self --- @return #boolean -function GROUP:OptionROTPassiveDefensePossible() - self:F2( { self.GroupName } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup then - if self:IsAir() then - return true - end - - return false - end - - return nil -end - ---- Evasion passive defense. --- @param #GROUP self --- @return #GROUP self -function GROUP:OptionROTPassiveDefense() - self:F2( { self.GroupName } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup 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 GROUP evade on enemy fire? --- @param #GROUP self --- @return #boolean -function GROUP:OptionROTEvadeFirePossible() - self:F2( { self.GroupName } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup then - if self:IsAir() then - return true - end - - return false - end - - return nil -end - - ---- Evade on fire. --- @param #GROUP self --- @return #GROUP self -function GROUP:OptionROTEvadeFire() - self:F2( { self.GroupName } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup 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 GROUP evade on fire using vertical manoeuvres? --- @param #GROUP self --- @return #boolean -function GROUP:OptionROTVerticalPossible() - self:F2( { self.GroupName } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup then - if self:IsAir() then - return true - end - - return false - end - - return nil -end - - ---- Evade on fire using vertical manoeuvres. --- @param #GROUP self --- @return #GROUP self -function GROUP:OptionROTVertical() - self:F2( { self.GroupName } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup 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 - --- Message APIs - ---- Returns a message for a coalition or a client. --- @param #GROUP self --- @param #string Message The message text --- @param #Duration Duration The duration of the message. --- @return Message#MESSAGE -function GROUP:Message( Message, Duration ) - self:F2( { Message, Duration } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup then - return MESSAGE:New( Message, self:GetCallsign() .. " (" .. self:GetTypeName() .. ")", Duration, self:GetClassNameAndID() ) - end - - return nil -end - ---- Send a message to all coalitions. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #GROUP self --- @param #string Message The message text --- @param #Duration Duration The duration of the message. -function GROUP:MessageToAll( Message, Duration ) - self:F2( { Message, Duration } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup then - self:Message( Message, Duration ):ToAll() - end - - return nil -end - ---- Send a message to the red coalition. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #GROUP self --- @param #string Message The message text --- @param #Duration Duration The duration of the message. -function GROUP:MessageToRed( Message, Duration ) - self:F2( { Message, Duration } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup then - self:Message( Message, Duration ):ToRed() - end - - return nil -end - ---- Send a message to the blue coalition. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #GROUP self --- @param #string Message The message text --- @param #Duration Duration The duration of the message. -function GROUP:MessageToBlue( Message, Duration ) - self:F2( { Message, Duration } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup then - self:Message( Message, Duration ):ToBlue() - end - - return nil -end - ---- Send a message to a client. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #GROUP self --- @param #string Message The message text --- @param #Duration Duration The duration of the message. --- @param Client#CLIENT Client The client object receiving the message. -function GROUP:MessageToClient( Message, Duration, Client ) - self:F2( { Message, Duration } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup then - self:Message( Message, Duration ):ToClient( Client ) - end - - return nil -end ---- UNIT Class --- --- @{UNIT} class --- ============== --- 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. --- --- --- 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). --- --- DCS UNIT APIs --- ============= --- The DCS Unit APIs are used extensively within MOOSE. The UNIT class has for each DCS Unit API a corresponding method. --- To be able to distinguish easily in your code the difference between a UNIT API call and a DCS Unit API call, --- the first letter of the method is also capitalized. So, by example, the DCS Unit method @{DCSUnit#Unit.getName}() --- is implemented in the UNIT class as @{#UNIT.GetName}(). --- --- Additional UNIT APIs --- ==================== --- The UNIT class comes with additional methods. Find below a summary. --- --- 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. --- --- Position, Point --- --------------- --- The UNIT class provides methods to obtain the current point or position of the DCS Unit. --- The @{#UNIT.GetPointVec2}(), @{#UNIT.GetPointVec3}() will obtain the current location of the DCS Unit in a Vec2 (2D) or a 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. --- --- Alive --- ----- --- The @{#UNIT.IsAlive}(), @{#UNIT.IsActive}() methods determines if the DCS Unit is alive, meaning, it is existing and active. --- --- Test for other units in radius --- ------------------------------ --- One can test if another DCS Unit is within a given radius of the current DCS Unit, by using the @{#UNIT.OtherUnitInRadius}() method. --- --- More functions will be added --- ---------------------------- --- During the MOOSE development, more functions will be added. A complete list of the current functions is below. --- --- --- --- --- @module Unit --- @author FlightControl - -Include.File( "Routines" ) -Include.File( "Base" ) -Include.File( "Message" ) - ---- The UNIT class --- @type UNIT --- @extends Base#BASE --- @field #UNIT.FlareColor FlareColor --- @field #UNIT.SmokeColor SmokeColor -UNIT = { - ClassName="UNIT", - CategoryName = { - [Unit.Category.AIRPLANE] = "Airplane", - [Unit.Category.HELICOPTER] = "Helicoper", - [Unit.Category.GROUND_UNIT] = "Ground Unit", - [Unit.Category.SHIP] = "Ship", - [Unit.Category.STRUCTURE] = "Structure", - }, - FlareColor = { - Green = trigger.flareColor.Green, - Red = trigger.flareColor.Red, - White = trigger.flareColor.White, - Yellow = trigger.flareColor.Yellow - }, - SmokeColor = { - Green = trigger.smokeColor.Green, - Red = trigger.smokeColor.Red, - White = trigger.smokeColor.White, - Orange = trigger.smokeColor.Orange, - Blue = trigger.smokeColor.Blue - }, - } - ---- FlareColor --- @type UNIT.FlareColor --- @field Green --- @field Red --- @field White --- @field Yellow - ---- SmokeColor --- @type UNIT.SmokeColor --- @field Green --- @field Red --- @field White --- @field Orange --- @field Blue - --- Registration. - ---- Create a new UNIT from DCSUnit. --- @param #UNIT self --- @param DCSUnit#Unit DCSUnit --- @param Database#DATABASE Database --- @return Unit#UNIT -function UNIT:Register( UnitName ) - - local self = BASE:Inherit( self, BASE:New() ) - self:F2( UnitName ) - self.UnitName = UnitName - return self -end - --- Reference methods. - ---- Finds a UNIT from the _DATABASE using a DCSUnit object. --- @param #UNIT self --- @param DCSUnit#Unit DCSUnit An existing DCS Unit object reference. --- @return Unit#UNIT self -function UNIT:Find( DCSUnit ) - - local UnitName = DCSUnit:getName() - local UnitFound = _DATABASE:FindUnit( UnitName ) - return UnitFound -end - ---- Find a UNIT in the _DATABASE using the name of an existing DCS Unit. --- @param #UNIT self --- @param #string UnitName The Unit Name. --- @return Unit#UNIT self -function UNIT:FindByName( UnitName ) - - local UnitFound = _DATABASE:FindUnit( UnitName ) - return UnitFound -end - -function UNIT:GetDCSUnit() - local DCSUnit = Unit.getByName( self.UnitName ) - - if DCSUnit then - return DCSUnit - end - - return nil -end - ---- Returns coalition of the Unit. --- @param Unit#UNIT self --- @return DCSCoalitionObject#coalition.side The side of the coalition. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetCoalition() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitCoalition = DCSUnit:getCoalition() - self:T3( UnitCoalition ) - return UnitCoalition - end - - return nil -end - ---- Returns country of the Unit. --- @param Unit#UNIT self --- @return DCScountry#country.id The country identifier. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetCountry() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitCountry = DCSUnit:getCountry() - self:T3( UnitCountry ) - return UnitCountry - end - - return nil -end - - ---- Returns DCS Unit object name. --- The function provides access to non-activated units too. --- @param Unit#UNIT self --- @return #string The name of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetName() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitName = self.UnitName - return UnitName - end - - return nil -end - - ---- Returns if the unit is alive. --- @param Unit#UNIT self --- @return #boolean true if Unit is alive. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:IsAlive() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitIsAlive = DCSUnit:isExist() - return UnitIsAlive - end - - return false -end - ---- Returns if the unit is activated. --- @param Unit#UNIT self --- @return #boolean true if Unit is activated. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:IsActive() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - - local UnitIsActive = DCSUnit:isActive() - return UnitIsActive - end - - return nil -end - ---- Returns name of the player that control the unit or nil if the unit is controlled by A.I. --- @param Unit#UNIT self --- @return #string Player Name --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetPlayerName() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - - local PlayerName = DCSUnit:getPlayerName() - if PlayerName == nil then - PlayerName = "" - end - return PlayerName - end - - return nil -end - ---- Returns the unit's unique identifier. --- @param Unit#UNIT self --- @return DCSUnit#Unit.ID Unit ID --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetID() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitID = DCSUnit:getID() - return UnitID - end - - return nil -end - ---- Returns the unit's number in the group. --- The number is the same number the unit has in ME. --- It may not be changed during the mission. --- If any unit in the group is destroyed, the numbers of another units will not be changed. --- @param Unit#UNIT self --- @return #number The Unit number. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetNumber() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitNumber = DCSUnit:getNumber() - return UnitNumber - end - - return nil -end - ---- Returns the unit's group if it exist and nil otherwise. --- @param Unit#UNIT self --- @return Group#GROUP The Group of the Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetGroup() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitGroup = DCSUnit:getGroup() - return UnitGroup - end - - return nil -end - - ---- Returns the unit's callsign - the localized string. --- @param Unit#UNIT self --- @return #string The Callsign of the Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetCallSign() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitCallSign = DCSUnit:getCallsign() - return UnitCallSign - end - - return nil -end - ---- Returns the unit's health. Dead units has health <= 1.0. --- @param Unit#UNIT self --- @return #number The Unit's health value. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetLife() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitLife = DCSUnit:getLife() - return UnitLife - end - - return nil -end - ---- Returns the Unit's initial health. --- @param Unit#UNIT self --- @return #number The Unit's initial health value. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetLife0() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitLife0 = DCSUnit:getLife0() - return UnitLife0 - end - - return nil -end - ---- Returns relative amount of fuel (from 0.0 to 1.0) the unit has in its internal tanks. If there are additional fuel tanks the value may be greater than 1.0. --- @param Unit#UNIT self --- @return #number The relative amount of fuel (from 0.0 to 1.0). --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetFuel() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitFuel = DCSUnit:getFuel() - return UnitFuel - end - - return nil -end - ---- Returns the Unit's ammunition. --- @param Unit#UNIT self --- @return DCSUnit#Unit.Ammo --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetAmmo() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitAmmo = DCSUnit:getAmmo() - return UnitAmmo - end - - return nil -end - ---- Returns the unit sensors. --- @param Unit#UNIT self --- @return DCSUnit#Unit.Sensors --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetSensors() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitSensors = DCSUnit:getSensors() - return UnitSensors - end - - return nil -end - --- Need to add here a function per sensortype --- unit:hasSensors(Unit.SensorType.RADAR, Unit.RadarType.AS) - ---- Returns two values: --- --- * First value indicates if at least one of the unit's radar(s) is on. --- * Second value is the object of the radar's interest. Not nil only if at least one radar of the unit is tracking a target. --- @param Unit#UNIT self --- @return #boolean Indicates if at least one of the unit's radar(s) is on. --- @return DCSObject#Object The object of the radar's interest. Not nil only if at least one radar of the unit is tracking a target. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetRadar() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitRadarOn, UnitRadarObject = DCSUnit:getRadar() - return UnitRadarOn, UnitRadarObject - end - - return nil, nil -end - --- Need to add here functions to check if radar is on and which object etc. - ---- Returns unit descriptor. Descriptor type depends on unit category. --- @param Unit#UNIT self --- @return DCSUnit#Unit.Desc The Unit descriptor. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetDesc() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitDesc = DCSUnit:getDesc() - return UnitDesc - end - - return nil -end - - ---- Returns the type name of the DCS Unit. --- @param Unit#UNIT self --- @return #string The type name of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetTypeName() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitTypeName = DCSUnit:getTypeName() - self:T3( UnitTypeName ) - return UnitTypeName - end - - return nil -end - - - ---- Returns the prefix name of the DCS Unit. A prefix name is a part of the name before a '#'-sign. --- DCS Units spawned with the @{SPAWN} class contain a '#'-sign to indicate the end of the (base) DCS Unit name. --- The spawn sequence number and unit number are contained within the name after the '#' sign. --- @param Unit#UNIT self --- @return #string The name of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetPrefix() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitPrefix = string.match( self.UnitName, ".*#" ):sub( 1, -2 ) - self:T3( UnitPrefix ) - return UnitPrefix - end - - return nil -end - - - ---- Returns the @{DCSTypes#Vec2} vector indicating the point in 2D of the DCS Unit within the mission. --- @param Unit#UNIT self --- @return DCSTypes#Vec2 The 2D point vector of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetPointVec2() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitPointVec3 = DCSUnit:getPosition().p - - local UnitPointVec2 = {} - UnitPointVec2.x = UnitPointVec3.x - UnitPointVec2.y = UnitPointVec3.z - - self:T3( UnitPointVec2 ) - return UnitPointVec2 - end - - return nil -end - - ---- Returns the @{DCSTypes#Vec3} vector indicating the point in 3D of the DCS Unit within the mission. --- @param Unit#UNIT self --- @return DCSTypes#Vec3 The 3D point vector of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetPointVec3() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitPointVec3 = DCSUnit:getPosition().p - self:T3( UnitPointVec3 ) - return UnitPointVec3 - end - - return nil -end - ---- Returns the @{DCSTypes#Position3} position vectors indicating the point and direction vectors in 3D of the DCS Unit within the mission. --- @param Unit#UNIT self --- @return DCSTypes#Position The 3D position vectors of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetPositionVec3() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitPosition = DCSUnit:getPosition() - self:T3( UnitPosition ) - return UnitPosition - end - - return nil -end - ---- Returns the DCS Unit velocity vector. --- @param Unit#UNIT self --- @return DCSTypes#Vec3 The velocity vector --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetVelocity() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitVelocityVec3 = DCSUnit:getVelocity() - self:T3( UnitVelocityVec3 ) - return UnitVelocityVec3 - end - - return nil -end - ---- Returns true if the DCS Unit is in the air. --- @param Unit#UNIT self --- @return #boolean true if in the air. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:InAir() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitInAir = DCSUnit:inAir() - self:T3( UnitInAir ) - return UnitInAir - end - - return nil -end - ---- Returns the altitude of the DCS Unit. --- @param Unit#UNIT self --- @return DCSTypes#Distance The altitude of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetAltitude() - self:F2() - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitPointVec3 = DCSUnit:getPoint() --DCSTypes#Vec3 - return UnitPointVec3.y - end - - return nil -end - ---- Returns true if there is an **other** DCS Unit within a radius of the current 2D point of the DCS Unit. --- @param Unit#UNIT self --- @param Unit#UNIT AwaitUnit The other UNIT wrapper object. --- @param Radius The radius in meters with the DCS Unit in the centre. --- @return true If the other DCS Unit is within the radius of the 2D point of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:OtherUnitInRadius( AwaitUnit, Radius ) - self:F2( { self.UnitName, AwaitUnit.UnitName, Radius } ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitPos = self:GetPointVec3() - local AwaitUnitPos = AwaitUnit:GetPointVec3() - - if (((UnitPos.x - AwaitUnitPos.x)^2 + (UnitPos.z - AwaitUnitPos.z)^2)^0.5 <= Radius) then - self:T3( "true" ) - return true - else - self:T3( "false" ) - return false - end - end - - return nil -end - ---- Returns the DCS Unit category name as defined within the DCS Unit Descriptor. --- @param Unit#UNIT self --- @return #string The DCS Unit Category Name -function UNIT:GetCategoryName() - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitCategoryName = self.CategoryName[ self:GetDesc().category ] - return UnitCategoryName - end - - return nil -end - ---- Signal a flare at the position of the UNIT. --- @param #UNIT self -function UNIT:Flare( FlareColor ) - self:F2() - trigger.action.signalFlare( self:GetPointVec3(), FlareColor , 0 ) -end - ---- Signal a white flare at the position of the UNIT. --- @param #UNIT self -function UNIT:FlareWhite() - self:F2() - trigger.action.signalFlare( self:GetPointVec3(), trigger.flareColor.White , 0 ) -end - ---- Signal a yellow flare at the position of the UNIT. --- @param #UNIT self -function UNIT:FlareYellow() - self:F2() - trigger.action.signalFlare( self:GetPointVec3(), trigger.flareColor.Yellow , 0 ) -end - ---- Signal a green flare at the position of the UNIT. --- @param #UNIT self -function UNIT:FlareGreen() - self:F2() - trigger.action.signalFlare( self:GetPointVec3(), trigger.flareColor.Green , 0 ) -end - ---- Signal a red flare at the position of the UNIT. --- @param #UNIT self -function UNIT:FlareRed() - self:F2() - trigger.action.signalFlare( self:GetPointVec3(), trigger.flareColor.Red, 0 ) -end - ---- Smoke the UNIT. --- @param #UNIT self -function UNIT:Smoke( SmokeColor ) - self:F2() - trigger.action.smoke( self:GetPointVec3(), SmokeColor ) -end - ---- Smoke the UNIT Green. --- @param #UNIT self -function UNIT:SmokeGreen() - self:F2() - trigger.action.smoke( self:GetPointVec3(), trigger.smokeColor.Green ) -end - ---- Smoke the UNIT Red. --- @param #UNIT self -function UNIT:SmokeRed() - self:F2() - trigger.action.smoke( self:GetPointVec3(), trigger.smokeColor.Red ) -end - ---- Smoke the UNIT White. --- @param #UNIT self -function UNIT:SmokeWhite() - self:F2() - trigger.action.smoke( self:GetPointVec3(), trigger.smokeColor.White ) -end - ---- Smoke the UNIT Orange. --- @param #UNIT self -function UNIT:SmokeOrange() - self:F2() - trigger.action.smoke( self:GetPointVec3(), trigger.smokeColor.Orange ) -end - ---- Smoke the UNIT Blue. --- @param #UNIT self -function UNIT:SmokeBlue() - self:F2() - trigger.action.smoke( self:GetPointVec3(), trigger.smokeColor.Blue ) -end - --- Is methods - ---- Returns if the unit is of an air category. --- If the unit is a helicopter or a plane, then this method will return true, otherwise false. --- @param #UNIT self --- @return #boolean Air category evaluation result. -function UNIT:IsAir() - self:F2() - - local UnitDescriptor = self.DCSUnit:getDesc() - self:T3( { UnitDescriptor.category, Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } ) - - local IsAirResult = ( UnitDescriptor.category == Unit.Category.AIRPLANE ) or ( UnitDescriptor.category == Unit.Category.HELICOPTER ) - - self:T3( IsAirResult ) - return IsAirResult -end - ---- ZONE Classes --- @module Zone - -Include.File( "Routines" ) -Include.File( "Base" ) -Include.File( "Message" ) - ---- The ZONE class --- @type ZONE --- @Extends Base#BASE -ZONE = { - ClassName="ZONE", - } - -function ZONE:New( ZoneName ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( ZoneName ) - - local Zone = trigger.misc.getZone( ZoneName ) - - if not Zone then - error( "Zone " .. ZoneName .. " does not exist." ) - return nil - end - - self.Zone = Zone - self.ZoneName = ZoneName - - return self -end - -function ZONE:GetPointVec2() - self:F( self.ZoneName ) - - local Zone = trigger.misc.getZone( self.ZoneName ) - local Point = { x = Zone.point.x, y = Zone.point.z } - - self:T( { Zone, Point } ) - - return Point -end - -function ZONE:GetPointVec3( Height ) - self:F( self.ZoneName ) - - local Zone = trigger.misc.getZone( self.ZoneName ) - local Point = { x = Zone.point.x, y = land.getHeight( self:GetPointVec2() ) + Height, z = Zone.point.z } - - self:T( { Zone, Point } ) - - return Point -end - -function ZONE:GetRandomPointVec2() - self:F( self.ZoneName ) - - local Point = {} - - local Zone = trigger.misc.getZone( self.ZoneName ) - - local angle = math.random() * math.pi*2; - Point.x = Zone.point.x + math.cos( angle ) * math.random() * Zone.radius; - Point.y = Zone.point.z + math.sin( angle ) * math.random() * Zone.radius; - - self:T( { Zone, Point } ) - - return Point -end - -function ZONE:GetRadius() - self:F( self.ZoneName ) - - local Zone = trigger.misc.getZone( self.ZoneName ) - - self:T( { Zone } ) - - return Zone.radius -end - ---- The CLIENT models client units in multi player missions. --- --- @{#CLIENT} class --- ================ --- Clients are those **Units** defined within the Mission Editor that have the skillset defined as __Client__ or __Player__. --- Note that clients are NOT the same as Units, they are NOT necessarily alive. --- The @{CLIENT} class is a wrapper class to handle the DCS Unit objects that have the skillset defined as __Client__ or __Player__: --- --- * 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. --- --- CLIENT reference methods --- ======================= --- For each DCS Unit having skill level Player or Client, a CLIENT wrapper object (instance) will be created within the _@{DATABASE} object. --- This is done at the beginning of the mission (when the mission starts). --- --- The CLIENT class does not contain a :New() method, rather it provides :Find() methods to retrieve the object reference --- using the DCS Unit or the DCS UnitName. --- --- Another thing to know is that CLIENT objects do not "contain" the DCS Unit object. --- The CLIENT methods will reference the DCS Unit object by name when it is needed during API execution. --- If the DCS Unit object does not exist or is nil, the CLIENT methods will return nil and log an exception in the DCS.log file. --- --- The CLIENT class provides the following functions to retrieve quickly the relevant CLIENT instance: --- --- * @{#CLIENT.Find}(): Find a CLIENT instance from the _DATABASE object using a DCS Unit object. --- * @{#CLIENT.FindByName}(): Find a CLIENT instance from the _DATABASE object using a DCS Unit name. --- --- IMPORTANT: ONE SHOULD NEVER SANATIZE these CLIENT OBJECT REFERENCES! (make the CLIENT object references nil). --- --- @module Client --- @author FlightControl - -Include.File( "Routines" ) -Include.File( "Base" ) -Include.File( "Cargo" ) -Include.File( "Message" ) - - ---- The CLIENT class --- @type CLIENT --- @extends Unit#UNIT -CLIENT = { - ONBOARDSIDE = { - NONE = 0, - LEFT = 1, - RIGHT = 2, - BACK = 3, - FRONT = 4 - }, - ClassName = "CLIENT", - ClientName = nil, - ClientAlive = false, - ClientTransport = false, - ClientBriefingShown = false, - _Menus = {}, - _Tasks = {}, - Messages = { - } -} - - ---- Finds a CLIENT from the _DATABASE using the relevant DCS Unit. --- @param #CLIENT self --- @param #string ClientName Name of the DCS **Unit** as defined within the Mission Editor. --- @param #string ClientBriefing Text that describes the briefing of the mission when a Player logs into the Client. --- @return #CLIENT --- @usage --- -- Create new Clients. --- local Mission = MISSIONSCHEDULER.AddMission( 'Russia Transport Troops SA-6', 'Operational', 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', 'Russia' ) --- Mission:AddGoal( DeploySA6TroopsGoal ) --- --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 1' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 3' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 2' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 4' ):Transport() ) -function CLIENT:Find( DCSUnit ) - local ClientName = DCSUnit:getName() - local ClientFound = _DATABASE:FindClient( ClientName ) - - if ClientFound then - ClientFound:F( ClientName ) - return ClientFound - end - - error( "CLIENT not found for: " .. ClientName ) -end - - ---- Finds a CLIENT from the _DATABASE using the relevant Client Unit Name. --- As an optional parameter, a briefing text can be given also. --- @param #CLIENT self --- @param #string ClientName Name of the DCS **Unit** as defined within the Mission Editor. --- @param #string ClientBriefing Text that describes the briefing of the mission when a Player logs into the Client. --- @return #CLIENT --- @usage --- -- Create new Clients. --- local Mission = MISSIONSCHEDULER.AddMission( 'Russia Transport Troops SA-6', 'Operational', 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', 'Russia' ) --- Mission:AddGoal( DeploySA6TroopsGoal ) --- --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 1' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 3' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 2' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 4' ):Transport() ) -function CLIENT:FindByName( ClientName, ClientBriefing ) - local ClientFound = _DATABASE:FindClient( ClientName ) - - if ClientFound then - ClientFound:F( { ClientName, ClientBriefing } ) - ClientFound:AddBriefing( ClientBriefing ) - ClientFound.MessageSwitch = true - - return ClientFound - end - - error( "CLIENT not found for: " .. ClientName ) -end - -function CLIENT:Register( ClientName ) - local self = BASE:Inherit( self, UNIT:Register( ClientName ) ) - - self:F( ClientName ) - self.ClientName = ClientName - self.MessageSwitch = true - self.ClientAlive2 = false - - --self.AliveCheckScheduler = routines.scheduleFunction( self._AliveCheckScheduler, { self }, timer.getTime() + 1, 5 ) - self.AliveCheckScheduler = SCHEDULER:New( self, self._AliveCheckScheduler, {}, 1, 5 ) - - 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, self.ClientName .. '/ClientBriefing', "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, self.ClientName .. '/MissionBriefing', "Mission Briefing" ) - end - - return self -end - - - ---- Resets a CLIENT. --- @param #CLIENT self --- @param #string ClientName Name of the Group as defined within the Mission Editor. The Group must have a Unit with the type Client. -function CLIENT:Reset( ClientName ) - self:F() - self._Menus = {} -end - --- Is Functions - ---- Checks if the CLIENT is a multi-seated UNIT. --- @param #CLIENT self --- @return #boolean true if multi-seated. -function CLIENT:IsMultiSeated() - self:F( self.ClientName ) - - local ClientMultiSeatedTypes = { - ["Mi-8MT"] = "Mi-8MT", - ["UH-1H"] = "UH-1H", - ["P-51B"] = "P-51B" - } - - if self:IsAlive() then - local ClientTypeName = self:GetClientGroupUnit():GetTypeName() - if ClientMultiSeatedTypes[ClientTypeName] then - return true - end - end - - return false -end - ---- Checks for a client alive event and calls a function on a continuous basis. --- @param #CLIENT self --- @param #function CallBack Function. --- @return #CLIENT -function CLIENT:Alive( CallBack, ... ) - self:F() - - self.ClientCallBack = CallBack - self.ClientParameters = arg - - return self -end - ---- @param #CLIENT self -function CLIENT:_AliveCheckScheduler() - self:F( { self.ClientName, self.ClientAlive2, self.ClientBriefingShown } ) - - if self:IsAlive() then -- Polymorphic call of UNIT - if self.ClientAlive2 == false then - self:ShowBriefing() - if self.ClientCallBack then - self:T("Calling Callback function") - self.ClientCallBack( self, unpack( self.ClientParameters ) ) - end - self.ClientAlive2 = true - end - else - if self.ClientAlive2 == true then - self.ClientAlive2 = false - end - end - - return true -end - ---- Return the DCSGroup of a Client. --- This function is modified to deal with a couple of bugs in DCS 1.5.3 --- @param #CLIENT self --- @return DCSGroup#Group -function CLIENT:GetDCSGroup() - self:F3() - --- local ClientData = Group.getByName( self.ClientName ) --- if ClientData and ClientData:isExist() then --- self:T( self.ClientName .. " : group found!" ) --- return ClientData --- else --- return nil --- end - - local ClientUnit = Unit.getByName( self.ClientName ) - - local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ) } - for CoalitionId, CoalitionData in pairs( CoalitionsData ) do - self:T3( { "CoalitionData:", CoalitionData } ) - for UnitId, UnitData in pairs( CoalitionData ) do - self:T3( { "UnitData:", UnitData } ) - if UnitData and UnitData:isExist() then - - --self:E(self.ClientName) - if ClientUnit then - local ClientGroup = ClientUnit:getGroup() - if ClientGroup then - self:T3( "ClientGroup = " .. self.ClientName ) - if ClientGroup:isExist() and UnitData:getGroup():isExist() then - if ClientGroup:getID() == UnitData:getGroup():getID() then - self:T3( "Normal logic" ) - self:T3( self.ClientName .. " : group found!" ) - self.ClientGroupID = ClientGroup:getID() - self.ClientGroupName = ClientGroup:getName() - return ClientGroup - end - else - -- Now we need to resolve the bugs in DCS 1.5 ... - -- Consult the database for the units of the Client Group. (ClientGroup:getUnits() returns nil) - self:T3( "Bug 1.5 logic" ) - local ClientGroupTemplate = _DATABASE.Templates.Units[self.ClientName].GroupTemplate - self.ClientGroupID = ClientGroupTemplate.groupId - self.ClientGroupName = _DATABASE.Templates.Units[self.ClientName].GroupName - self:T3( self.ClientName .. " : group found in bug 1.5 resolvement logic!" ) - return ClientGroup - end - -- else - -- error( "Client " .. self.ClientName .. " not found!" ) - end - else - --self:E( { "Client not found!", self.ClientName } ) - end - end - end - end - - -- For non player clients - if ClientUnit then - local ClientGroup = ClientUnit:getGroup() - if ClientGroup then - self:T3( "ClientGroup = " .. self.ClientName ) - if ClientGroup:isExist() then - self:T3( "Normal logic" ) - self:T3( self.ClientName .. " : group found!" ) - return ClientGroup - end - end - end - - self.ClientGroupID = nil - self.ClientGroupUnit = nil - - return nil -end - - --- TODO: Check DCSTypes#Group.ID ---- Get the group ID of the client. --- @param #CLIENT self --- @return DCSTypes#Group.ID -function CLIENT:GetClientGroupID() - - local ClientGroup = self:GetDCSGroup() - - --self:E( self.ClientGroupID ) -- Determined in GetDCSGroup() - return self.ClientGroupID -end - - ---- Get the name of the group of the client. --- @param #CLIENT self --- @return #string -function CLIENT:GetClientGroupName() - - local ClientGroup = self:GetDCSGroup() - - self:T( self.ClientGroupName ) -- Determined in GetDCSGroup() - return self.ClientGroupName -end - ---- Returns the UNIT of the CLIENT. --- @param #CLIENT self --- @return Unit#UNIT -function CLIENT:GetClientGroupUnit() - self:F2() - - local ClientDCSUnit = Unit.getByName( self.ClientName ) - - self:T( self.ClientDCSUnit ) - if ClientDCSUnit and ClientDCSUnit:isExist() then - local ClientUnit = _DATABASE:FindUnit( self.ClientName ) - self:T2( ClientUnit ) - return ClientUnit - end -end - ---- Returns the DCSUnit of the CLIENT. --- @param #CLIENT self --- @return DCSTypes#Unit -function CLIENT:GetClientGroupDCSUnit() - self:F2() - - local ClientDCSUnit = Unit.getByName( self.ClientName ) - - if ClientDCSUnit and ClientDCSUnit:isExist() then - self:T2( ClientDCSUnit ) - return ClientDCSUnit - end -end - - ---- Evaluates if the CLIENT is a transport. --- @param #CLIENT self --- @return #boolean true is a transport. -function CLIENT:IsTransport() - self:F() - return self.ClientTransport -end - ---- Shows the @{Cargo#CARGO} contained within the CLIENT to the player as a message. --- The @{Cargo#CARGO} is shown using the @{Message#MESSAGE} distribution system. --- @param #CLIENT self -function CLIENT:ShowCargo() - self:F() - - local CargoMsg = "" - - for CargoName, Cargo in pairs( CARGOS ) do - if self == Cargo:IsLoadedInClient() then - CargoMsg = CargoMsg .. Cargo.CargoName .. " Type:" .. Cargo.CargoType .. " Weight: " .. Cargo.CargoWeight .. "\n" - end - end - - if CargoMsg == "" then - CargoMsg = "empty" - end - - self:Message( CargoMsg, 15, self.ClientName .. "/Cargo", "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 MessageId is a text identifying the Message in the MessageQueue. The Message system overwrites Messages with the same MessageId --- @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. -function CLIENT:Message( Message, MessageDuration, MessageId, MessageCategory, MessageInterval ) - self:F( { Message, MessageDuration, MessageId, MessageCategory, MessageInterval } ) - - if not self.MenuMessages then - if self:GetClientGroupID() then - self.MenuMessages = MENU_CLIENT:New( self, 'Messages' ) - self.MenuRouteMessageOn = MENU_CLIENT_COMMAND:New( self, 'Messages On', self.MenuMessages, CLIENT.SwitchMessages, { self, true } ) - self.MenuRouteMessageOff = MENU_CLIENT_COMMAND:New( self,'Messages Off', self.MenuMessages, CLIENT.SwitchMessages, { self, false } ) - end - end - - if self.MessageSwitch == true then - if MessageCategory == nil then - MessageCategory = "Messages" - end - if 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, MessageCategory, MessageDuration, MessageId ):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, MessageCategory, MessageDuration, MessageId ):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, MessageCategory, MessageDuration, MessageId ):ToClient( self ) - self.Messages[MessageId].MessageTime = timer.getTime() - end - end - end - end -end ---- Manage sets of units and groups. --- --- @{#Database} class --- ================== --- Mission designers can use the DATABASE class to build sets of units belonging to certain: --- --- * Coalitions --- * Categories --- * Countries --- * Unit types --- * Starting with certain prefix strings. --- --- This list will grow over time. Planned developments are to include filters and iterators. --- Additional filters will be added around @{Zone#ZONEs}, Radiuses, Active players, ... --- More iterators will be implemented in the near future ... --- --- Administers the Initial Sets of the Mission Templates as defined within the Mission Editor. --- --- DATABASE construction methods: --- ================================= --- Create a new DATABASE object with the @{#DATABASE.New} method: --- --- * @{#DATABASE.New}: Creates a new DATABASE object. --- --- --- DATABASE filter criteria: --- ========================= --- You can set filter criteria to define the set of units within the database. --- Filter criteria are defined by: --- --- * @{#DATABASE.FilterCoalitions}: Builds the DATABASE with the units belonging to the coalition(s). --- * @{#DATABASE.FilterCategories}: Builds the DATABASE with the units belonging to the category(ies). --- * @{#DATABASE.FilterTypes}: Builds the DATABASE with the units belonging to the unit type(s). --- * @{#DATABASE.FilterCountries}: Builds the DATABASE with the units belonging to the country(ies). --- * @{#DATABASE.FilterUnitPrefixes}: Builds the DATABASE with the units starting with the same prefix string(s). --- --- Once the filter criteria have been set for the DATABASE, you can start filtering using: --- --- * @{#DATABASE.FilterStart}: Starts the filtering of the units within the database. --- --- Planned filter criteria within development are (so these are not yet available): --- --- * @{#DATABASE.FilterGroupPrefixes}: Builds the DATABASE with the groups of the units starting with the same prefix string(s). --- * @{#DATABASE.FilterZones}: Builds the DATABASE with the units within a @{Zone#ZONE}. --- --- --- DATABASE iterators: --- =================== --- Once the filters have been defined and the DATABASE has been built, 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.ForEachAliveUnit}: Calls a function for each alive unit it finds within the DATABASE. --- --- Planned iterators methods in development are (so these are not yet available): --- --- * @{#DATABASE.ForEachUnit}: Calls a function for each unit contained within the DATABASE. --- * @{#DATABASE.ForEachGroup}: Calls a function for each group contained within the DATABASE. --- * @{#DATABASE.ForEachUnitInZone}: Calls a function for each unit within a certain zone contained within the DATABASE. --- --- ==== --- @module Database --- @author FlightControl - -Include.File( "Routines" ) -Include.File( "Base" ) -Include.File( "Menu" ) -Include.File( "Group" ) -Include.File( "Unit" ) -Include.File( "Event" ) -Include.File( "Client" ) - ---- DATABASE class --- @type DATABASE --- @extends Base#BASE -DATABASE = { - ClassName = "DATABASE", - Templates = { - Units = {}, - Groups = {}, - ClientsByName = {}, - ClientsByID = {}, - }, - DCSUnits = {}, - DCSGroups = {}, - UNITS = {}, - GROUPS = {}, - NavPoints = {}, - Statics = {}, - Players = {}, - PlayersAlive = {}, - CLIENTS = {}, - ClientsAlive = {}, - Filter = { - Coalitions = nil, - Categories = nil, - Types = nil, - Countries = nil, - UnitPrefixes = nil, - GroupPrefixes = 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, - }, - }, -} - -local _DATABASECoalition = - { - [1] = "Red", - [2] = "Blue", - } - -local _DATABASECategory = - { - [Unit.Category.AIRPLANE] = "Plane", - [Unit.Category.HELICOPTER] = "Helicopter", - [Unit.Category.GROUND_UNIT] = "Vehicle", - [Unit.Category.SHIP] = "Ship", - [Unit.Category.STRUCTURE] = "Structure", - } - - ---- Creates a new DATABASE object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. --- @param #DATABASE self --- @return #DATABASE --- @usage --- -- Define a new DATABASE Object. This DBObject will contain a reference to all Group and Unit Templates defined within the ME and the DCSRTE. --- DBObject = DATABASE:New() -function DATABASE:New() - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) - - _EVENTDISPATCHER:OnBirth( self._EventOnBirth, self ) - _EVENTDISPATCHER:OnDead( self._EventOnDeadOrCrash, self ) - _EVENTDISPATCHER:OnCrash( self._EventOnDeadOrCrash, self ) - - - -- Add database with registered clients and already alive players - - -- Follow alive players and clients - _EVENTDISPATCHER:OnPlayerEnterUnit( self._EventOnPlayerEnterUnit, self ) - _EVENTDISPATCHER:OnPlayerLeaveUnit( self._EventOnPlayerLeaveUnit, self ) - - - return self -end - ---- Finds a Unit based on the Unit Name. --- @param #DATABASE self --- @param #string UnitName --- @return Unit#UNIT The found Unit. -function DATABASE:FindUnit( UnitName ) - - local UnitFound = self.UNITS[UnitName] - return UnitFound -end - ---- Adds a Unit based on the Unit Name in the DATABASE. --- @param #DATABASE self -function DATABASE:AddUnit( DCSUnit, DCSUnitName ) - - self.DCSUnits[DCSUnitName] = DCSUnit - self.UNITS[DCSUnitName] = UNIT:Register( DCSUnitName ) -end - ---- Deletes a Unit from the DATABASE based on the Unit Name. --- @param #DATABASE self -function DATABASE:DeleteUnit( DCSUnitName ) - - self.DCSUnits[DCSUnitName] = nil -end - ---- Finds a CLIENT based on the ClientName. --- @param #DATABASE self --- @param #string ClientName --- @return Client#CLIENT The found CLIENT. -function DATABASE:FindClient( ClientName ) - - local ClientFound = self.CLIENTS[ClientName] - return ClientFound -end - ---- Adds a CLIENT based on the ClientName in the DATABASE. --- @param #DATABASE self -function DATABASE:AddClient( ClientName ) - - self.CLIENTS[ClientName] = CLIENT:Register( ClientName ) - self:E( self.CLIENTS[ClientName]:GetClassNameAndID() ) -end - ---- Finds a GROUP based on the GroupName. --- @param #DATABASE self --- @param #string GroupName --- @return Group#GROUP The found GROUP. -function DATABASE:FindGroup( GroupName ) - - local GroupFound = self.GROUPS[GroupName] - return GroupFound -end - ---- Adds a GROUP based on the GroupName in the DATABASE. --- @param #DATABASE self -function DATABASE:AddGroup( DCSGroup, GroupName ) - - self.DCSGroups[GroupName] = DCSGroup - self.GROUPS[GroupName] = GROUP:Register( GroupName ) -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.SpawnCoalitionID - local SpawnCountryID = SpawnTemplate.SpawnCountryID - local SpawnCategoryID = SpawnTemplate.SpawnCategoryID - - -- Nullify - SpawnTemplate.SpawnCoalitionID = nil - SpawnTemplate.SpawnCountryID = nil - SpawnTemplate.SpawnCategoryID = nil - - self:_RegisterGroup( SpawnTemplate ) - coalition.addGroup( SpawnCountryID, SpawnCategoryID, SpawnTemplate ) - - -- Restore - SpawnTemplate.SpawnCoalitionID = SpawnCoalitionID - SpawnTemplate.SpawnCountryID = SpawnCountryID - SpawnTemplate.SpawnCategoryID = SpawnCategoryID - - - local SpawnGroup = GROUP:Register( 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:F( 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:F( 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:_RegisterGroup( GroupTemplate ) - - local GroupTemplateName = env.getValueDictByKey(GroupTemplate.name) - - if not self.Templates.Groups[GroupTemplateName] then - self.Templates.Groups[GroupTemplateName] = {} - self.Templates.Groups[GroupTemplateName].Status = nil - end - - -- Delete the spans from the route, it is not needed and takes memory. - if GroupTemplate.route and GroupTemplate.route.spans then - GroupTemplate.route.spans = nil - end - - self.Templates.Groups[GroupTemplateName].GroupName = GroupTemplateName - self.Templates.Groups[GroupTemplateName].Template = GroupTemplate - self.Templates.Groups[GroupTemplateName].groupId = GroupTemplate.groupId - self.Templates.Groups[GroupTemplateName].UnitCount = #GroupTemplate.units - self.Templates.Groups[GroupTemplateName].Units = GroupTemplate.units - - self:T( { "Group", self.Templates.Groups[GroupTemplateName].GroupName, self.Templates.Groups[GroupTemplateName].UnitCount } ) - - for unit_num, UnitTemplate in pairs( GroupTemplate.units ) do - - local UnitTemplateName = env.getValueDictByKey(UnitTemplate.name) - self.Templates.Units[UnitTemplateName] = {} - self.Templates.Units[UnitTemplateName].UnitName = UnitTemplateName - self.Templates.Units[UnitTemplateName].Template = UnitTemplate - self.Templates.Units[UnitTemplateName].GroupName = GroupTemplateName - self.Templates.Units[UnitTemplateName].GroupTemplate = GroupTemplate - self.Templates.Units[UnitTemplateName].GroupId = GroupTemplate.groupId - self:E( {"skill",UnitTemplate.skill}) - if UnitTemplate.skill and (UnitTemplate.skill == "Client" or UnitTemplate.skill == "Player") then - self.Templates.ClientsByName[UnitTemplateName] = UnitTemplate - self.Templates.ClientsByID[UnitTemplate.unitId] = UnitTemplate - end - self:E( { "Unit", self.Templates.Units[UnitTemplateName].UnitName } ) - end -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() - 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 - ---- Private method that registers all datapoints within in the mission. --- @param #DATABASE self --- @return #DATABASE self -function DATABASE:_RegisterDatabase() - - 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:", DCSGroup, DCSGroupName } ) - self:AddGroup( DCSGroup, DCSGroupName ) - - for DCSUnitId, DCSUnit in pairs( DCSGroup:getUnits() ) do - - local DCSUnitName = DCSUnit:getName() - self:E( { "Register Unit:", DCSUnit, DCSUnitName } ) - self:AddUnit( DCSUnit, DCSUnitName ) - end - else - self:E( { "Group does not exist: ", DCSGroup } ) - end - - end - end - - for ClientName, ClientTemplate in pairs( self.Templates.ClientsByName ) do - self:E( { "Adding Client:", ClientName } ) - self:AddClient( ClientName ) - end - - return self -end - ---- Events - ---- Handles the OnBirth event for the alive units set. --- @param #DATABASE self --- @param Event#EVENTDATA Event -function DATABASE:_EventOnBirth( Event ) - self:F( { Event } ) - - if Event.IniDCSUnit then - if self:_IsIncludeDCSUnit( Event.IniDCSUnit ) then - self:AddUnit( Event.IniDCSUnit, Event.IniDCSUnitName ) - self:AddGroup( Event.IniDCSGroup, Event.IniDCSGroupName ) - self:_EventOnPlayerEnterUnit( Event ) - end - end -end - ---- Handles the OnDead or OnCrash event for alive units set. --- @param #DATABASE self --- @param Event#EVENTDATA Event -function DATABASE:_EventOnDeadOrCrash( Event ) - self:F( { Event } ) - - if Event.IniDCSUnit then - if self.DCSUnits[Event.IniDCSUnitName] then - self:DeleteUnit( Event.IniDCSUnitName ) - -- add logic to correctly remove a group once all units are destroyed... - end - end -end - ---- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). --- @param #DATABASE self --- @param Event#EVENTDATA Event -function DATABASE:_EventOnPlayerEnterUnit( Event ) - self:F( { Event } ) - - if Event.IniDCSUnit then - if self:_IsIncludeDCSUnit( Event.IniDCSUnit ) then - if not self.PlayersAlive[Event.IniDCSUnitName] then - self:E( { "Add player for unit:", Event.IniDCSUnitName, Event.IniDCSUnit:getPlayerName() } ) - self.PlayersAlive[Event.IniDCSUnitName] = Event.IniDCSUnit:getPlayerName() - self.ClientsAlive[Event.IniDCSUnitName] = self.CLIENTS[ Event.IniDCSUnitName ] - end - end - end -end - ---- Handles the OnPlayerLeaveUnit event to clean the active players table. --- @param #DATABASE self --- @param Event#EVENTDATA Event -function DATABASE:_EventOnPlayerLeaveUnit( Event ) - self:F( { Event } ) - - if Event.IniDCSUnit then - if self:_IsIncludeDCSUnit( Event.IniDCSUnit ) then - if self.PlayersAlive[Event.IniDCSUnitName] then - self:E( { "Cleaning player for unit:", Event.IniDCSUnitName, Event.IniDCSUnit:getPlayerName() } ) - self.PlayersAlive[Event.IniDCSUnitName] = nil - self.ClientsAlive[Event.IniDCSUnitName] = nil - end - end - end -end - ---- Iterators - ---- Interate the DATABASE and call an interator 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, arg, Set ) - self:F( 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 % 10 == 0 then - coroutine.yield( false ) - end - end - return true - end - - local co = coroutine.create( CoRoutine ) - - local function Schedule() - - local status, res = coroutine.resume( co ) - self:T( { status, res } ) - - if status == false then - error( res ) - end - if res == false then - return true -- resume next time the loop - end - - return false - end - - local Scheduler = SCHEDULER:New( self, Schedule, {}, 0.001, 0.001, 0 ) - - return self -end - - ---- Interate the DATABASE and call an interator 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:ForEachDCSUnit( IteratorFunction, ... ) - self:F( arg ) - - self:ForEach( IteratorFunction, arg, self.DCSUnits ) - - return self -end - ---- Interate the DATABASE and call an interator function for each **alive** player, providing the Unit of the player 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 UNIT parameter. --- @return #DATABASE self -function DATABASE:ForEachPlayer( IteratorFunction, ... ) - self:F( arg ) - - self:ForEach( IteratorFunction, arg, self.PlayersAlive ) - - return self -end - - ---- Interate the DATABASE and call an interator 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:F( arg ) - - self:ForEach( IteratorFunction, arg, self.CLIENTS ) - - return self -end - - -function DATABASE:ScanEnvironment() - self:F() - - self.Navpoints = {} - self.UNITS = {} - --Build routines.db.units and self.Navpoints - 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 - --self.Units[coa_name] = {} - - ---------------------------------------------- - -- build nav points DB - self.Navpoints[coa_name] = {} - 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[coa_name][nav_ind] = routines.utils.deepCopy(nav_data) - - self.Navpoints[coa_name][nav_ind]['name'] = nav_data.callsignStr -- name is a little bit more self-explanatory. - self.Navpoints[coa_name][nav_ind]['point'] = {} -- point is used by SSE, support it. - self.Navpoints[coa_name][nav_ind]['point']['x'] = nav_data.x - self.Navpoints[coa_name][nav_ind]['point']['y'] = 0 - self.Navpoints[coa_name][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.lower(cntry_data.name) - --self.Units[coa_name][countryName] = {} - --self.Units[coa_name][countryName]["countryId"] = cntry_data.id - - if type(cntry_data) == 'table' then --just making sure - - for obj_type_name, obj_type_data in pairs(cntry_data) do - - if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then --should be an unncessary check - - local category = 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:_RegisterGroup( GroupTemplate ) - 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 - - self:_RegisterDatabase() - self:_RegisterPlayers() - - return self -end - - ---- --- @param #DATABASE self --- @param DCSUnit#Unit DCSUnit --- @return #DATABASE self -function DATABASE:_IsIncludeDCSUnit( DCSUnit ) - self:F( DCSUnit ) - local DCSUnitInclude = true - - if self.Filter.Coalitions then - local DCSUnitCoalition = false - for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do - self:T( { "Coalition:", DCSUnit:getCoalition(), self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) - if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == DCSUnit:getCoalition() then - DCSUnitCoalition = true - end - end - DCSUnitInclude = DCSUnitInclude and DCSUnitCoalition - end - - if self.Filter.Categories then - local DCSUnitCategory = false - for CategoryID, CategoryName in pairs( self.Filter.Categories ) do - self:T( { "Category:", DCSUnit:getDesc().category, self.FilterMeta.Categories[CategoryName], CategoryName } ) - if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == DCSUnit:getDesc().category then - DCSUnitCategory = true - end - end - DCSUnitInclude = DCSUnitInclude and DCSUnitCategory - end - - if self.Filter.Types then - local DCSUnitType = false - for TypeID, TypeName in pairs( self.Filter.Types ) do - self:T( { "Type:", DCSUnit:getTypeName(), TypeName } ) - if TypeName == DCSUnit:getTypeName() then - DCSUnitType = true - end - end - DCSUnitInclude = DCSUnitInclude and DCSUnitType - end - - if self.Filter.Countries then - local DCSUnitCountry = false - for CountryID, CountryName in pairs( self.Filter.Countries ) do - self:T( { "Country:", DCSUnit:getCountry(), CountryName } ) - if country.id[CountryName] == DCSUnit:getCountry() then - DCSUnitCountry = true - end - end - DCSUnitInclude = DCSUnitInclude and DCSUnitCountry - end - - if self.Filter.UnitPrefixes then - local DCSUnitPrefix = false - for UnitPrefixId, UnitPrefix in pairs( self.Filter.UnitPrefixes ) do - self:T( { "Unit Prefix:", string.find( DCSUnit:getName(), UnitPrefix, 1 ), UnitPrefix } ) - if string.find( DCSUnit:getName(), UnitPrefix, 1 ) then - DCSUnitPrefix = true - end - end - DCSUnitInclude = DCSUnitInclude and DCSUnitPrefix - end - - self:T( DCSUnitInclude ) - return DCSUnitInclude -end - ---- --- @param #DATABASE self --- @param DCSUnit#Unit DCSUnit --- @return #DATABASE self -function DATABASE:_IsAliveDCSUnit( DCSUnit ) - self:F( DCSUnit ) - local DCSUnitAlive = false - if DCSUnit and DCSUnit:isExist() and DCSUnit:isActive() then - if self.DCSUnits[DCSUnit:getName()] then - DCSUnitAlive = true - end - end - self:T( DCSUnitAlive ) - return DCSUnitAlive -end - ---- --- @param #DATABASE self --- @param DCSGroup#Group DCSGroup --- @return #DATABASE self -function DATABASE:_IsAliveDCSGroup( DCSGroup ) - self:F( DCSGroup ) - local DCSGroupAlive = false - if DCSGroup and DCSGroup:isExist() then - if self.DCSGroups[DCSGroup:getName()] then - DCSGroupAlive = true - end - end - self:T( DCSGroupAlive ) - return DCSGroupAlive -end - - ---- Traces the current database contents in the log ... (for debug reasons). --- @param #DATABASE self --- @return #DATABASE self -function DATABASE:TraceDatabase() - self:F() - - self:T( { "DCSUnits:", self.DCSUnits } ) -end - - ---- The main include file for the MOOSE system. - -Include.File( "Routines" ) -Include.File( "Base" ) -Include.File( "Database" ) -Include.File( "Event" ) - --- The order of the declarations is important here. Don't touch it. - ---- Declare the event dispatcher based on the EVENT class -_EVENTDISPATCHER = EVENT:New() -- #EVENT - ---- Declare the main database object, which is used internally by the MOOSE classes. -_DATABASE = DATABASE:New():ScanEnvironment() -- Database#DATABASE - ---- Scoring system for MOOSE. --- This scoring class calculates the hits and kills that players make within a simulation session. --- Scoring is calculated using a defined algorithm. --- With a small change in MissionScripting.lua, the scoring can also be logged in a CSV file, that can then be uploaded --- to a database or a BI tool to publish the scoring results to the player community. --- @module Scoring --- @author FlightControl - - -Include.File( "Routines" ) -Include.File( "Base" ) -Include.File( "Menu" ) -Include.File( "Group" ) -Include.File( "Event" ) - - ---- The Scoring class --- @type SCORING --- @field Players A collection of the current players that have joined the game. --- @extends Base#BASE -SCORING = { - ClassName = "SCORING", - ClassID = 0, - Players = {}, -} - -local _SCORINGCoalition = - { - [1] = "Red", - [2] = "Blue", - } - -local _SCORINGCategory = - { - [Unit.Category.AIRPLANE] = "Plane", - [Unit.Category.HELICOPTER] = "Helicopter", - [Unit.Category.GROUND_UNIT] = "Vehicle", - [Unit.Category.SHIP] = "Ship", - [Unit.Category.STRUCTURE] = "Structure", - } - ---- Creates a new SCORING object to administer the scoring achieved by players. --- @param #SCORING self --- @param #string GameName The name of the game. This name is also logged in the CSV score file. --- @return #SCORING self --- @usage --- -- Define a new scoring object for the mission Gori Valley. --- ScoringObject = SCORING:New( "Gori Valley" ) -function SCORING:New( GameName ) - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) - - if GameName then - self.GameName = GameName - else - error( "A game name must be given to register the scoring results" ) - end - - - _EVENTDISPATCHER:OnDead( self._EventOnDeadOrCrash, self ) - _EVENTDISPATCHER:OnCrash( self._EventOnDeadOrCrash, self ) - _EVENTDISPATCHER:OnHit( self._EventOnHit, self ) - - --self.SchedulerId = routines.scheduleFunction( SCORING._FollowPlayersScheduled, { self }, 0, 5 ) - self.SchedulerId = SCHEDULER:New( self, self._FollowPlayersScheduled, {}, 0, 5 ) - - self:ScoreMenu() - - return self - -end - ---- Creates a score radio menu. Can be accessed using Radio -> F10. --- @param #SCORING self --- @return #SCORING self -function SCORING:ScoreMenu() - self.Menu = SUBMENU:New( 'Scoring' ) - self.AllScoresMenu = COMMANDMENU:New( 'Score All Active Players', self.Menu, SCORING.ReportScoreAll, self ) - --- = COMMANDMENU:New('Your Current Score', ReportScore, SCORING.ReportScorePlayer, self ) - return self -end - ---- Follows new players entering Clients within the DCSRTE. --- TODO: Need to see if i can catch this also with an event. It will eliminate the schedule ... -function SCORING:_FollowPlayersScheduled() - self:F3( "_FollowPlayersScheduled" ) - - local ClientUnit = 0 - local CoalitionsData = { AlivePlayersRed = coalition.getPlayers(coalition.side.RED), AlivePlayersBlue = coalition.getPlayers(coalition.side.BLUE) } - local unitId - local unitData - local AlivePlayerUnits = {} - - for CoalitionId, CoalitionData in pairs( CoalitionsData ) do - self:T3( { "_FollowPlayersScheduled", CoalitionData } ) - for UnitId, UnitData in pairs( CoalitionData ) do - self:_AddPlayerFromUnit( UnitData ) - end - end - - return true -end - - ---- Track DEAD or CRASH events for the scoring. --- @param #SCORING self --- @param Event#EVENTDATA Event -function SCORING:_EventOnDeadOrCrash( Event ) - self:F( { Event } ) - - local TargetUnit = nil - local TargetGroup = nil - local TargetUnitName = "" - local TargetGroupName = "" - local TargetPlayerName = "" - local TargetCoalition = nil - local TargetCategory = nil - local TargetType = nil - local TargetUnitCoalition = nil - local TargetUnitCategory = nil - local TargetUnitType = nil - - if Event.IniDCSUnit then - - TargetUnit = Event.IniDCSUnit - TargetUnitName = Event.IniDCSUnitName - TargetGroup = Event.IniDCSGroup - TargetGroupName = Event.IniDCSGroupName - TargetPlayerName = TargetUnit:getPlayerName() - - TargetCoalition = TargetUnit:getCoalition() - --TargetCategory = TargetUnit:getCategory() - TargetCategory = TargetUnit:getDesc().category -- Workaround - TargetType = TargetUnit:getTypeName() - - TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] - TargetUnitCategory = _SCORINGCategory[TargetCategory] - TargetUnitType = TargetType - - self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType } ) - end - - for PlayerName, PlayerData in pairs( self.Players ) do - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Something got killed" ) - - -- Some variables - local InitUnitName = PlayerData.UnitName - local InitUnitType = PlayerData.UnitType - local InitCoalition = PlayerData.UnitCoalition - local InitCategory = PlayerData.UnitCategory - local InitUnitCoalition = _SCORINGCoalition[InitCoalition] - local InitUnitCategory = _SCORINGCategory[InitCategory] - - self:T( { InitUnitName, InitUnitType, InitUnitCoalition, InitCoalition, InitUnitCategory, InitCategory } ) - - -- What is he hitting? - if TargetCategory then - if PlayerData and PlayerData.Hit and PlayerData.Hit[TargetCategory] and PlayerData.Hit[TargetCategory][TargetUnitName] then -- Was there a hit for this unit for this player before registered??? - if not PlayerData.Kill[TargetCategory] then - PlayerData.Kill[TargetCategory] = {} - end - if not PlayerData.Kill[TargetCategory][TargetType] then - PlayerData.Kill[TargetCategory][TargetType] = {} - PlayerData.Kill[TargetCategory][TargetType].Score = 0 - PlayerData.Kill[TargetCategory][TargetType].ScoreKill = 0 - PlayerData.Kill[TargetCategory][TargetType].Penalty = 0 - PlayerData.Kill[TargetCategory][TargetType].PenaltyKill = 0 - end - - if InitCoalition == TargetCoalition then - PlayerData.Penalty = PlayerData.Penalty + 25 - PlayerData.Kill[TargetCategory][TargetType].Penalty = PlayerData.Kill[TargetCategory][TargetType].Penalty + 25 - PlayerData.Kill[TargetCategory][TargetType].PenaltyKill = PlayerData.Kill[TargetCategory][TargetType].PenaltyKill + 1 - MESSAGE:New( "Player '" .. PlayerName .. "' killed a friendly " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - PlayerData.Kill[TargetCategory][TargetType].PenaltyKill .. " times. Penalty: -" .. PlayerData.Kill[TargetCategory][TargetType].Penalty .. - ". Score Total:" .. PlayerData.Score - PlayerData.Penalty, - "", 5, "/PENALTY" .. PlayerName .. "/" .. InitUnitName ):ToAll() - self:ScoreCSV( PlayerName, "KILL_PENALTY", 1, -125, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - else - PlayerData.Score = PlayerData.Score + 10 - PlayerData.Kill[TargetCategory][TargetType].Score = PlayerData.Kill[TargetCategory][TargetType].Score + 10 - PlayerData.Kill[TargetCategory][TargetType].ScoreKill = PlayerData.Kill[TargetCategory][TargetType].ScoreKill + 1 - MESSAGE:New( "Player '" .. PlayerName .. "' killed an enemy " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - PlayerData.Kill[TargetCategory][TargetType].ScoreKill .. " times. Score: " .. PlayerData.Kill[TargetCategory][TargetType].Score .. - ". Score Total:" .. PlayerData.Score - PlayerData.Penalty, - "", 5, "/SCORE" .. PlayerName .. "/" .. InitUnitName ):ToAll() - self:ScoreCSV( PlayerName, "KILL_SCORE", 1, 10, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - end - end - end - end - end -end - - - ---- Add a new player entering a Unit. -function SCORING:_AddPlayerFromUnit( UnitData ) - self:F( UnitData ) - - if UnitData and UnitData:isExist() then - local UnitName = UnitData:getName() - local PlayerName = UnitData:getPlayerName() - local UnitDesc = UnitData:getDesc() - local UnitCategory = UnitDesc.category - local UnitCoalition = UnitData:getCoalition() - local UnitTypeName = UnitData:getTypeName() - - self:T( { PlayerName, UnitName, UnitCategory, UnitCoalition, UnitTypeName } ) - - if self.Players[PlayerName] == nil then -- I believe this is the place where a Player gets a life in a mission when he enters a unit ... - self.Players[PlayerName] = {} - self.Players[PlayerName].Hit = {} - self.Players[PlayerName].Kill = {} - self.Players[PlayerName].Mission = {} - - -- for CategoryID, CategoryName in pairs( SCORINGCategory ) do - -- self.Players[PlayerName].Hit[CategoryID] = {} - -- self.Players[PlayerName].Kill[CategoryID] = {} - -- end - self.Players[PlayerName].HitPlayers = {} - self.Players[PlayerName].HitUnits = {} - self.Players[PlayerName].Score = 0 - self.Players[PlayerName].Penalty = 0 - self.Players[PlayerName].PenaltyCoalition = 0 - self.Players[PlayerName].PenaltyWarning = 0 - end - - if not self.Players[PlayerName].UnitCoalition then - self.Players[PlayerName].UnitCoalition = UnitCoalition - else - if self.Players[PlayerName].UnitCoalition ~= UnitCoalition then - self.Players[PlayerName].Penalty = self.Players[PlayerName].Penalty + 50 - self.Players[PlayerName].PenaltyCoalition = self.Players[PlayerName].PenaltyCoalition + 1 - MESSAGE:New( "Player '" .. PlayerName .. "' changed coalition from " .. _SCORINGCoalition[self.Players[PlayerName].UnitCoalition] .. " to " .. _SCORINGCoalition[UnitCoalition] .. - "(changed " .. self.Players[PlayerName].PenaltyCoalition .. " times the coalition). 50 Penalty points added.", - "", - 2, - "/PENALTYCOALITION" .. PlayerName - ):ToAll() - self:ScoreCSV( PlayerName, "COALITION_PENALTY", 1, -50, self.Players[PlayerName].UnitName, _SCORINGCoalition[self.Players[PlayerName].UnitCoalition], _SCORINGCategory[self.Players[PlayerName].UnitCategory], self.Players[PlayerName].UnitType, - UnitName, _SCORINGCoalition[UnitCoalition], _SCORINGCategory[UnitCategory], UnitData:getTypeName() ) - end - end - self.Players[PlayerName].UnitName = UnitName - self.Players[PlayerName].UnitCoalition = UnitCoalition - self.Players[PlayerName].UnitCategory = UnitCategory - self.Players[PlayerName].UnitType = UnitTypeName - - if self.Players[PlayerName].Penalty > 100 then - if self.Players[PlayerName].PenaltyWarning < 1 then - MESSAGE:New( "Player '" .. PlayerName .. "': WARNING! If you continue to commit FRATRICIDE and have a PENALTY score higher than 150, you will be COURT MARTIALED and DISMISSED from this mission! \nYour total penalty is: " .. self.Players[PlayerName].Penalty, - "", - 30, - "/PENALTYCOALITION" .. PlayerName - ):ToAll() - self.Players[PlayerName].PenaltyWarning = self.Players[PlayerName].PenaltyWarning + 1 - end - end - - if self.Players[PlayerName].Penalty > 150 then - ClientGroup = GROUP:NewFromDCSUnit( UnitData ) - ClientGroup:Destroy() - MESSAGE:New( "Player '" .. PlayerName .. "' committed FRATRICIDE, he will be COURT MARTIALED and is DISMISSED from this mission!", - "", - 10, - "/PENALTYCOALITION" .. PlayerName - ):ToAll() - end - - end -end - - ---- Registers Scores the players completing a Mission Task. -function SCORING:_AddMissionTaskScore( PlayerUnit, MissionName, Score ) - self:F( { PlayerUnit, MissionName, Score } ) - - local PlayerName = PlayerUnit:getPlayerName() - - if not self.Players[PlayerName].Mission[MissionName] then - self.Players[PlayerName].Mission[MissionName] = {} - self.Players[PlayerName].Mission[MissionName].ScoreTask = 0 - self.Players[PlayerName].Mission[MissionName].ScoreMission = 0 - end - - self:T( PlayerName ) - self:T( self.Players[PlayerName].Mission[MissionName] ) - - self.Players[PlayerName].Score = self.Players[PlayerName].Score + Score - self.Players[PlayerName].Mission[MissionName].ScoreTask = self.Players[PlayerName].Mission[MissionName].ScoreTask + Score - - MESSAGE:New( "Player '" .. PlayerName .. "' has finished another Task in Mission '" .. MissionName .. "'. " .. - Score .. " Score points added.", - "", 20, "/SCORETASK" .. PlayerName ):ToAll() - - self:ScoreCSV( PlayerName, "TASK_" .. MissionName:gsub( ' ', '_' ), 1, Score, PlayerUnit:getName() ) -end - - ---- Registers Mission Scores for possible multiple players that contributed in the Mission. -function SCORING:_AddMissionScore( MissionName, Score ) - self:F( { MissionName, Score } ) - - for PlayerName, PlayerData in pairs( self.Players ) do - - if PlayerData.Mission[MissionName] then - PlayerData.Score = PlayerData.Score + Score - PlayerData.Mission[MissionName].ScoreMission = PlayerData.Mission[MissionName].ScoreMission + Score - MESSAGE:New( "Player '" .. PlayerName .. "' has finished Mission '" .. MissionName .. "'. " .. - Score .. " Score points added.", - "", 20, "/SCOREMISSION" .. PlayerName ):ToAll() - self:ScoreCSV( PlayerName, "MISSION_" .. MissionName:gsub( ' ', '_' ), 1, Score ) - end - end -end - ---- Handles the OnHit event for the scoring. --- @param #SCORING self --- @param Event#EVENTDATA Event -function SCORING:_EventOnHit( Event ) - self:F( { Event } ) - - local InitUnit = nil - local InitUnitName = "" - local InitGroup = nil - local InitGroupName = "" - local InitPlayerName = nil - - local InitCoalition = nil - local InitCategory = nil - local InitType = nil - local InitUnitCoalition = nil - local InitUnitCategory = nil - local InitUnitType = nil - - local TargetUnit = nil - local TargetUnitName = "" - local TargetGroup = nil - local TargetGroupName = "" - local TargetPlayerName = "" - - local TargetCoalition = nil - local TargetCategory = nil - local TargetType = nil - local TargetUnitCoalition = nil - local TargetUnitCategory = nil - local TargetUnitType = nil - - if Event.IniDCSUnit then - - InitUnit = Event.IniDCSUnit - InitUnitName = Event.IniDCSUnitName - InitGroup = Event.IniDCSGroup - InitGroupName = Event.IniDCSGroupName - InitPlayerName = InitUnit:getPlayerName() - - InitCoalition = InitUnit:getCoalition() - --TODO: Workaround Client DCS Bug - --InitCategory = InitUnit:getCategory() - InitCategory = InitUnit:getDesc().category - InitType = InitUnit:getTypeName() - - InitUnitCoalition = _SCORINGCoalition[InitCoalition] - InitUnitCategory = _SCORINGCategory[InitCategory] - InitUnitType = InitType - - self:T( { InitUnitName, InitGroupName, InitPlayerName, InitCoalition, InitCategory, InitType , InitUnitCoalition, InitUnitCategory, InitUnitType } ) - end - - - if Event.TgtDCSUnit then - - TargetUnit = Event.TgtDCSUnit - TargetUnitName = Event.TgtDCSUnitName - TargetGroup = Event.TgtDCSGroup - TargetGroupName = Event.TgtDCSGroupName - TargetPlayerName = TargetUnit:getPlayerName() - - TargetCoalition = TargetUnit:getCoalition() - --TODO: Workaround Client DCS Bug - --TargetCategory = TargetUnit:getCategory() - TargetCategory = TargetUnit:getDesc().category - TargetType = TargetUnit:getTypeName() - - TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] - TargetUnitCategory = _SCORINGCategory[TargetCategory] - TargetUnitType = TargetType - - self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType, TargetUnitCoalition, TargetUnitCategory, TargetUnitType } ) - end - - if InitPlayerName ~= nil then -- It is a player that is hitting something - self:_AddPlayerFromUnit( InitUnit ) - if self.Players[InitPlayerName] then -- This should normally not happen, but i'll test it anyway. - if TargetPlayerName ~= nil then -- It is a player hitting another player ... - self:_AddPlayerFromUnit( TargetUnit ) - self.Players[InitPlayerName].HitPlayers = self.Players[InitPlayerName].HitPlayers + 1 - end - - self:T( "Hitting Something" ) - -- What is he hitting? - if TargetCategory then - if not self.Players[InitPlayerName].Hit[TargetCategory] then - self.Players[InitPlayerName].Hit[TargetCategory] = {} - end - if not self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName] then - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName] = {} - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score = 0 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty = 0 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit = 0 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit = 0 - end - local Score = 0 - if InitCoalition == TargetCoalition then - self.Players[InitPlayerName].Penalty = self.Players[InitPlayerName].Penalty + 10 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty + 10 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit + 1 - MESSAGE:New( "Player '" .. InitPlayerName .. "' hit a friendly " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit .. " times. Penalty: -" .. self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty .. - ". Score Total:" .. self.Players[InitPlayerName].Score - self.Players[InitPlayerName].Penalty, - "", - 2, - "/PENALTY" .. InitPlayerName .. "/" .. InitUnitName - ):ToAll() - self:ScoreCSV( InitPlayerName, "HIT_PENALTY", 1, -25, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - else - self.Players[InitPlayerName].Score = self.Players[InitPlayerName].Score + 10 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score + 1 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit + 1 - MESSAGE:New( "Player '" .. InitPlayerName .. "' hit a target " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit .. " times. Score: " .. self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score .. - ". Score Total:" .. self.Players[InitPlayerName].Score - self.Players[InitPlayerName].Penalty, - "", - 2, - "/SCORE" .. InitPlayerName .. "/" .. InitUnitName - ):ToAll() - self:ScoreCSV( InitPlayerName, "HIT_SCORE", 1, 1, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - end - end - end - elseif InitPlayerName == nil then -- It is an AI hitting a player??? - - end -end - - -function SCORING:ReportScoreAll() - - env.info( "Hello World " ) - - local ScoreMessage = "" - local PlayerMessage = "" - - self:T( "Score Report" ) - - for PlayerName, PlayerData in pairs( self.Players ) do - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local PlayerScore = 0 - local PlayerPenalty = 0 - - ScoreMessage = ":\n" - - local ScoreMessageHits = "" - - for CategoryID, CategoryName in pairs( _SCORINGCategory ) do - self:T( CategoryName ) - if PlayerData.Hit[CategoryID] then - local Score = 0 - local ScoreHit = 0 - local Penalty = 0 - local PenaltyHit = 0 - self:T( "Hit scores exist for player " .. PlayerName ) - for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do - Score = Score + UnitData.Score - ScoreHit = ScoreHit + UnitData.ScoreHit - Penalty = Penalty + UnitData.Penalty - PenaltyHit = UnitData.PenaltyHit - end - local ScoreMessageHit = string.format( "%s:%d ", CategoryName, Score - Penalty ) - self:T( ScoreMessageHit ) - ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageHits ~= "" then - ScoreMessage = ScoreMessage .. " Hits: " .. ScoreMessageHits .. "\n" - end - - local ScoreMessageKills = "" - for CategoryID, CategoryName in pairs( _SCORINGCategory ) do - self:T( "Kill scores exist for player " .. PlayerName ) - if PlayerData.Kill[CategoryID] then - local Score = 0 - local ScoreKill = 0 - local Penalty = 0 - local PenaltyKill = 0 - - for UnitName, UnitData in pairs( PlayerData.Kill[CategoryID] ) do - Score = Score + UnitData.Score - ScoreKill = ScoreKill + UnitData.ScoreKill - Penalty = Penalty + UnitData.Penalty - PenaltyKill = PenaltyKill + UnitData.PenaltyKill - end - - local ScoreMessageKill = string.format( " %s:%d ", CategoryName, Score - Penalty ) - self:T( ScoreMessageKill ) - ScoreMessageKills = ScoreMessageKills .. ScoreMessageKill - - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageKills = ScoreMessageKills .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageKills ~= "" then - ScoreMessage = ScoreMessage .. " Kills: " .. ScoreMessageKills .. "\n" - end - - local ScoreMessageCoalitionChangePenalties = "" - if PlayerData.PenaltyCoalition ~= 0 then - ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) - PlayerPenalty = PlayerPenalty + PlayerData.Penalty - end - if ScoreMessageCoalitionChangePenalties ~= "" then - ScoreMessage = ScoreMessage .. " Coalition Penalties: " .. ScoreMessageCoalitionChangePenalties .. "\n" - end - - local ScoreMessageMission = "" - local ScoreMission = 0 - local ScoreTask = 0 - for MissionName, MissionData in pairs( PlayerData.Mission ) do - ScoreMission = ScoreMission + MissionData.ScoreMission - ScoreTask = ScoreTask + MissionData.ScoreTask - ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " - end - PlayerScore = PlayerScore + ScoreMission + ScoreTask - - if ScoreMessageMission ~= "" then - ScoreMessage = ScoreMessage .. " Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ")\n" - end - - PlayerMessage = PlayerMessage .. string.format( "Player '%s' Score:%d (%d Score -%d Penalties)%s", PlayerName, PlayerScore - PlayerPenalty, PlayerScore, PlayerPenalty, ScoreMessage ) - end - end - MESSAGE:New( PlayerMessage, "Player Scores", 30, "AllPlayerScores"):ToAll() -end - - -function SCORING:ReportScorePlayer() - - env.info( "Hello World " ) - - local ScoreMessage = "" - local PlayerMessage = "" - - self:T( "Score Report" ) - - for PlayerName, PlayerData in pairs( self.Players ) do - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local PlayerScore = 0 - local PlayerPenalty = 0 - - ScoreMessage = "" - - local ScoreMessageHits = "" - - for CategoryID, CategoryName in pairs( _SCORINGCategory ) do - self:T( CategoryName ) - if PlayerData.Hit[CategoryID] then - local Score = 0 - local ScoreHit = 0 - local Penalty = 0 - local PenaltyHit = 0 - self:T( "Hit scores exist for player " .. PlayerName ) - for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do - Score = Score + UnitData.Score - ScoreHit = ScoreHit + UnitData.ScoreHit - Penalty = Penalty + UnitData.Penalty - PenaltyHit = UnitData.PenaltyHit - end - local ScoreMessageHit = string.format( "\n %s = %d score(%d;-%d) hits(#%d;#-%d)", CategoryName, Score - Penalty, Score, Penalty, ScoreHit, PenaltyHit ) - self:T( ScoreMessageHit ) - ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageHits ~= "" then - ScoreMessage = ScoreMessage .. "\n Hits: " .. ScoreMessageHits .. " " - end - - local ScoreMessageKills = "" - for CategoryID, CategoryName in pairs( _SCORINGCategory ) do - self:T( "Kill scores exist for player " .. PlayerName ) - if PlayerData.Kill[CategoryID] then - local Score = 0 - local ScoreKill = 0 - local Penalty = 0 - local PenaltyKill = 0 - - for UnitName, UnitData in pairs( PlayerData.Kill[CategoryID] ) do - Score = Score + UnitData.Score - ScoreKill = ScoreKill + UnitData.ScoreKill - Penalty = Penalty + UnitData.Penalty - PenaltyKill = PenaltyKill + UnitData.PenaltyKill - end - - local ScoreMessageKill = string.format( "\n %s = %d score(%d;-%d) hits(#%d;#-%d)", CategoryName, Score - Penalty, Score, Penalty, ScoreKill, PenaltyKill ) - self:T( ScoreMessageKill ) - ScoreMessageKills = ScoreMessageKills .. ScoreMessageKill - - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageKills = ScoreMessageKills .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageKills ~= "" then - ScoreMessage = ScoreMessage .. "\n Kills: " .. ScoreMessageKills .. " " - end - - local ScoreMessageCoalitionChangePenalties = "" - if PlayerData.PenaltyCoalition ~= 0 then - ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) - PlayerPenalty = PlayerPenalty + PlayerData.Penalty - end - if ScoreMessageCoalitionChangePenalties ~= "" then - ScoreMessage = ScoreMessage .. "\n Coalition: " .. ScoreMessageCoalitionChangePenalties .. " " - end - - local ScoreMessageMission = "" - local ScoreMission = 0 - local ScoreTask = 0 - for MissionName, MissionData in pairs( PlayerData.Mission ) do - ScoreMission = ScoreMission + MissionData.ScoreMission - ScoreTask = ScoreTask + MissionData.ScoreTask - ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " - end - PlayerScore = PlayerScore + ScoreMission + ScoreTask - - if ScoreMessageMission ~= "" then - ScoreMessage = ScoreMessage .. "\n Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ") " - end - - PlayerMessage = PlayerMessage .. string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties ):%s", PlayerName, PlayerScore - PlayerPenalty, PlayerScore, PlayerPenalty, ScoreMessage ) - end - end - MESSAGE:New( PlayerMessage, "Player Scores", 30, "AllPlayerScores"):ToAll() - -end - - -function SCORING:SecondsToClock(sSeconds) - local nSeconds = sSeconds - if nSeconds == 0 then - --return nil; - return "00:00:00"; - else - nHours = string.format("%02.f", math.floor(nSeconds/3600)); - nMins = string.format("%02.f", math.floor(nSeconds/60 - (nHours*60))); - nSecs = string.format("%02.f", math.floor(nSeconds - nHours*3600 - nMins *60)); - return nHours..":"..nMins..":"..nSecs - end -end - ---- Opens a score CSV file to log the scores. --- @param #SCORING self --- @param #string ScoringCSV --- @return #SCORING self --- @usage --- -- Open a new CSV file to log the scores of the game Gori Valley. Let the name of the CSV file begin with "Player Scores". --- ScoringObject = SCORING:New( "Gori Valley" ) --- ScoringObject:OpenCSV( "Player Scores" ) -function SCORING:OpenCSV( ScoringCSV ) - self:F( ScoringCSV ) - - if lfs and io and os then - if ScoringCSV then - self.ScoringCSV = ScoringCSV - local fdir = lfs.writedir() .. [[Logs\]] .. self.ScoringCSV .. " " .. os.date( "%Y-%m-%d %H-%M-%S" ) .. ".csv" - - self.CSVFile, self.err = io.open( fdir, "w+" ) - if not self.CSVFile then - error( "Error: Cannot open CSV file in " .. lfs.writedir() ) - end - - self.CSVFile:write( '"GameName","RunTime","Time","PlayerName","ScoreType","PlayerUnitCoaltion","PlayerUnitCategory","PlayerUnitType","PlayerUnitName","TargetUnitCoalition","TargetUnitCategory","TargetUnitType","TargetUnitName","Times","Score"\n' ) - - self.RunTime = os.date("%y-%m-%d_%H-%M-%S") - else - error( "A string containing the CSV file name must be given." ) - end - else - self:E( "The MissionScripting.lua file has not been changed to allow lfs, io and os modules to be used..." ) - end - return self -end - - ---- Registers a score for a player. --- @param #SCORING self --- @param #string PlayerName The name of the player. --- @param #string ScoreType The type of the score. --- @param #string ScoreTimes The amount of scores achieved. --- @param #string ScoreAmount The score given. --- @param #string PlayerUnitName The unit name of the player. --- @param #string PlayerUnitCoalition The coalition of the player unit. --- @param #string PlayerUnitCategory The category of the player unit. --- @param #string PlayerUnitType The type of the player unit. --- @param #string TargetUnitName The name of the target unit. --- @param #string TargetUnitCoalition The coalition of the target unit. --- @param #string TargetUnitCategory The category of the target unit. --- @param #string TargetUnitType The type of the target unit. --- @return #SCORING self -function SCORING:ScoreCSV( PlayerName, ScoreType, ScoreTimes, ScoreAmount, PlayerUnitName, PlayerUnitCoalition, PlayerUnitCategory, PlayerUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - --write statistic information to file - local ScoreTime = self:SecondsToClock( timer.getTime() ) - PlayerName = PlayerName:gsub( '"', '_' ) - - if PlayerUnitName and PlayerUnitName ~= '' then - local PlayerUnit = Unit.getByName( PlayerUnitName ) - - if PlayerUnit then - if not PlayerUnitCategory then - --PlayerUnitCategory = SCORINGCategory[PlayerUnit:getCategory()] - PlayerUnitCategory = _SCORINGCategory[PlayerUnit:getDesc().category] - end - - if not PlayerUnitCoalition then - PlayerUnitCoalition = _SCORINGCoalition[PlayerUnit:getCoalition()] - end - - if not PlayerUnitType then - PlayerUnitType = PlayerUnit:getTypeName() - end - else - PlayerUnitName = '' - PlayerUnitCategory = '' - PlayerUnitCoalition = '' - PlayerUnitType = '' - end - else - PlayerUnitName = '' - PlayerUnitCategory = '' - PlayerUnitCoalition = '' - PlayerUnitType = '' - end - - if not TargetUnitCoalition then - TargetUnitCoalition = '' - end - - if not TargetUnitCategory then - TargetUnitCategory = '' - end - - if not TargetUnitType then - TargetUnitType = '' - end - - if not TargetUnitName then - TargetUnitName = '' - end - - if lfs and io and os then - self.CSVFile:write( - '"' .. self.GameName .. '"' .. ',' .. - '"' .. self.RunTime .. '"' .. ',' .. - '' .. ScoreTime .. '' .. ',' .. - '"' .. PlayerName .. '"' .. ',' .. - '"' .. ScoreType .. '"' .. ',' .. - '"' .. PlayerUnitCoalition .. '"' .. ',' .. - '"' .. PlayerUnitCategory .. '"' .. ',' .. - '"' .. PlayerUnitType .. '"' .. ',' .. - '"' .. PlayerUnitName .. '"' .. ',' .. - '"' .. TargetUnitCoalition .. '"' .. ',' .. - '"' .. TargetUnitCategory .. '"' .. ',' .. - '"' .. TargetUnitType .. '"' .. ',' .. - '"' .. TargetUnitName .. '"' .. ',' .. - '' .. ScoreTimes .. '' .. ',' .. - '' .. ScoreAmount - ) - - self.CSVFile:write( "\n" ) - end -end - - -function SCORING:CloseCSV() - if lfs and io and os then - self.CSVFile:close() - end -end - ---- CARGO Classes --- @module CARGO - -Include.File( "Routines" ) -Include.File( "Base" ) -Include.File( "Message" ) -Include.File( "Scheduler" ) - - ---- Clients are those Groups defined within the Mission Editor that have the skillset defined as "Client" or "Player". --- These clients are defined within the Mission Orchestration Framework (MOF) - -CARGOS = {} - - -CARGO_ZONE = { - ClassName="CARGO_ZONE", - CargoZoneName = '', - CargoHostUnitName = '', - SIGNAL = { - TYPE = { - SMOKE = { ID = 1, TEXT = "smoke" }, - FLARE = { ID = 2, TEXT = "flare" } - }, - COLOR = { - GREEN = { ID = 1, TRIGGERCOLOR = trigger.smokeColor.Green, TEXT = "A green" }, - RED = { ID = 2, TRIGGERCOLOR = trigger.smokeColor.Red, TEXT = "A red" }, - WHITE = { ID = 3, TRIGGERCOLOR = trigger.smokeColor.White, TEXT = "A white" }, - ORANGE = { ID = 4, TRIGGERCOLOR = trigger.smokeColor.Orange, TEXT = "An orange" }, - BLUE = { ID = 5, TRIGGERCOLOR = trigger.smokeColor.Blue, TEXT = "A blue" }, - YELLOW = { ID = 6, TRIGGERCOLOR = trigger.flareColor.Yellow, TEXT = "A yellow" } - } - } -} - ---- Creates a new zone where cargo can be collected or deployed. --- The zone functionality is useful to smoke or indicate routes for cargo pickups or deployments. --- Provide the zone name as declared in the mission file into the CargoZoneName in the :New method. --- An optional parameter is the CargoHostName, which is a Group declared with Late Activation switched on in the mission file. --- The CargoHostName is the "host" of the cargo zone: --- --- * It will smoke the zone position when a client is approaching the zone. --- * Depending on the cargo type, it will assist in the delivery of the cargo by driving to and from the client. --- --- @param #CARGO_ZONE self --- @param #string CargoZoneName The name of the zone as declared within the mission editor. --- @param #string CargoHostName The name of the Group "hosting" the zone. The Group MUST NOT be a static, and must be a "mobile" unit. -function CARGO_ZONE:New( CargoZoneName, CargoHostName ) local self = BASE:Inherit( self, ZONE:New( CargoZoneName ) ) - self:F( { CargoZoneName, CargoHostName } ) - - self.CargoZoneName = CargoZoneName - self.SignalHeight = 2 - --self.CargoZone = trigger.misc.getZone( CargoZoneName ) - - - if CargoHostName then - self.CargoHostName = CargoHostName - end - - self:T( self.CargoZoneName ) - - return self -end - -function CARGO_ZONE:Spawn() - self:F( self.CargoHostName ) - - if self.CargoHostName then -- Only spawn a host in the zone when there is one given as a parameter in the New function. - if self.CargoHostSpawn then - local CargoHostGroup = self.CargoHostSpawn:GetGroupFromIndex() - if CargoHostGroup and CargoHostGroup:IsAlive() then - else - self.CargoHostSpawn:ReSpawn( 1 ) - end - else - self:T( "Initialize CargoHostSpawn" ) - self.CargoHostSpawn = SPAWN:New( self.CargoHostName ):Limit( 1, 1 ) - self.CargoHostSpawn:ReSpawn( 1 ) - end - end - - return self -end - -function CARGO_ZONE:GetHostUnit() - self:F( self ) - - if self.CargoHostName then - - -- A Host has been given, signal the host - local CargoHostGroup = self.CargoHostSpawn:GetGroupFromIndex() - local CargoHostUnit - if CargoHostGroup and CargoHostGroup:IsAlive() then - CargoHostUnit = CargoHostGroup:GetUnit(1) - else - CargoHostUnit = StaticObject.getByName( self.CargoHostName ) - end - - return CargoHostUnit - end - - return nil -end - -function CARGO_ZONE:ReportCargosToClient( Client, CargoType ) - self:F() - - local SignalUnit = self:GetHostUnit() - - if SignalUnit then - - local SignalUnitTypeName = SignalUnit:getTypeName() - - local HostMessage = "" - - local IsCargo = false - for CargoID, Cargo in pairs( CARGOS ) do - if Cargo.CargoType == Task.CargoType then - if Cargo:IsStatusNone() then - HostMessage = HostMessage .. " - " .. Cargo.CargoName .. " - " .. Cargo.CargoType .. " (" .. Cargo.Weight .. "kg)" .. "\n" - IsCargo = true - end - end - end - - if not IsCargo then - HostMessage = "No Cargo Available." - end - - Client:Message( HostMessage, 20, Mission.Name .. "/StageHosts." .. SignalUnitTypeName, SignalUnitTypeName .. ": Reporting Cargo", 10 ) - end -end - - -function CARGO_ZONE:Signal() - self:F() - - local Signalled = false - - if self.SignalType then - - if self.CargoHostName then - - -- A Host has been given, signal the host - - local SignalUnit = self:GetHostUnit() - - if SignalUnit then - - self:T( 'Signalling Unit' ) - local SignalVehiclePos = SignalUnit:GetPointVec3() - SignalVehiclePos.y = SignalVehiclePos.y + 2 - - if self.SignalType.ID == CARGO_ZONE.SIGNAL.TYPE.SMOKE.ID then - - trigger.action.smoke( SignalVehiclePos, self.SignalColor.TRIGGERCOLOR ) - Signalled = true - - elseif self.SignalType.ID == CARGO_ZONE.SIGNAL.TYPE.FLARE.ID then - - trigger.action.signalFlare( SignalVehiclePos, self.SignalColor.TRIGGERCOLOR , 0 ) - Signalled = false - - end - end - - else - - local ZonePointVec3 = self:GetPointVec3( self.SignalHeight ) -- Get the zone position + the landheight + 2 meters - - if self.SignalType.ID == CARGO_ZONE.SIGNAL.TYPE.SMOKE.ID then - - trigger.action.smoke( ZonePointVec3, self.SignalColor.TRIGGERCOLOR ) - Signalled = true - - elseif self.SignalType.ID == CARGO_ZONE.SIGNAL.TYPE.FLARE.ID then - trigger.action.signalFlare( ZonePointVec3, self.SignalColor.TRIGGERCOLOR, 0 ) - Signalled = false - - end - end - end - - return Signalled - -end - -function CARGO_ZONE:WhiteSmoke( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.SMOKE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.WHITE - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - -function CARGO_ZONE:BlueSmoke( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.SMOKE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.BLUE - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - -function CARGO_ZONE:RedSmoke( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.SMOKE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.RED - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - -function CARGO_ZONE:OrangeSmoke( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.SMOKE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.ORANGE - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - -function CARGO_ZONE:GreenSmoke( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.SMOKE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.GREEN - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - - -function CARGO_ZONE:WhiteFlare( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.FLARE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.WHITE - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - -function CARGO_ZONE:RedFlare( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.FLARE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.RED - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - -function CARGO_ZONE:GreenFlare( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.FLARE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.GREEN - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - -function CARGO_ZONE:YellowFlare( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.FLARE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.YELLOW - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - - -function CARGO_ZONE:GetCargoHostUnit() - self:F( self ) - - if self.CargoHostSpawn then - local CargoHostGroup = self.CargoHostSpawn:GetGroupFromIndex(1) - if CargoHostGroup and CargoHostGroup:IsAlive() then - local CargoHostUnit = CargoHostGroup:GetUnit(1) - if CargoHostUnit and CargoHostUnit:IsAlive() then - return CargoHostUnit - end - end - end - - return nil -end - -function CARGO_ZONE:GetCargoZoneName() - self:F() - - return self.CargoZoneName -end - -CARGO = { - ClassName = "CARGO", - STATUS = { - NONE = 0, - LOADED = 1, - UNLOADED = 2, - LOADING = 3 - }, - CargoClient = nil -} - ---- Add Cargo to the mission... Cargo functionality needs to be reworked a bit, so this is still under construction. I need to make a CARGO Class... -function CARGO:New( CargoType, CargoName, CargoWeight ) local self = BASE:Inherit( self, BASE:New() ) - self:F( { CargoType, CargoName, CargoWeight } ) - - - self.CargoType = CargoType - self.CargoName = CargoName - self.CargoWeight = CargoWeight - - self:StatusNone() - - return self -end - -function CARGO:Spawn( Client ) - self:F() - - return self - -end - -function CARGO:IsNear( Client, LandingZone ) - self:F() - - local Near = true - - return Near - -end - - -function CARGO:IsLoadingToClient() - self:F() - - if self:IsStatusLoading() then - return self.CargoClient - end - - return nil - -end - - -function CARGO:IsLoadedInClient() - self:F() - - if self:IsStatusLoaded() then - return self.CargoClient - end - - return nil - -end - - -function CARGO:UnLoad( Client, TargetZoneName ) - self:F() - - self:StatusUnLoaded() - - return self -end - -function CARGO:OnBoard( Client, LandingZone ) - self:F() - - local Valid = true - - self.CargoClient = Client - local ClientUnit = Client:GetClientGroupDCSUnit() - - return Valid -end - -function CARGO:OnBoarded( Client, LandingZone ) - self:F() - - local OnBoarded = true - - return OnBoarded -end - -function CARGO:Load( Client ) - self:F() - - self:StatusLoaded( Client ) - - return self -end - -function CARGO:IsLandingRequired() - self:F() - return true -end - -function CARGO:IsSlingLoad() - self:F() - return false -end - - -function CARGO:StatusNone() - self:F() - - self.CargoClient = nil - self.CargoStatus = CARGO.STATUS.NONE - - return self -end - -function CARGO:StatusLoading( Client ) - self:F() - - self.CargoClient = Client - self.CargoStatus = CARGO.STATUS.LOADING - self:T( "Cargo " .. self.CargoName .. " loading to Client: " .. self.CargoClient:GetClientGroupName() ) - - return self -end - -function CARGO:StatusLoaded( Client ) - self:F() - - self.CargoClient = Client - self.CargoStatus = CARGO.STATUS.LOADED - self:T( "Cargo " .. self.CargoName .. " loaded in Client: " .. self.CargoClient:GetClientGroupName() ) - - return self -end - -function CARGO:StatusUnLoaded() - self:F() - - self.CargoClient = nil - self.CargoStatus = CARGO.STATUS.UNLOADED - - return self -end - - -function CARGO:IsStatusNone() - self:F() - - return self.CargoStatus == CARGO.STATUS.NONE -end - -function CARGO:IsStatusLoading() - self:F() - - return self.CargoStatus == CARGO.STATUS.LOADING -end - -function CARGO:IsStatusLoaded() - self:F() - - return self.CargoStatus == CARGO.STATUS.LOADED -end - -function CARGO:IsStatusUnLoaded() - self:F() - - return self.CargoStatus == CARGO.STATUS.UNLOADED -end - - -CARGO_GROUP = { - ClassName = "CARGO_GROUP" -} - - -function CARGO_GROUP:New( CargoType, CargoName, CargoWeight, CargoGroupTemplate, CargoZone ) local self = BASE:Inherit( self, CARGO:New( CargoType, CargoName, CargoWeight ) ) - self:F( { CargoType, CargoName, CargoWeight, CargoGroupTemplate, CargoZone } ) - - self.CargoSpawn = SPAWN:NewWithAlias( CargoGroupTemplate, CargoName ) - self.CargoZone = CargoZone - - CARGOS[self.CargoName] = self - - return self - -end - -function CARGO_GROUP:Spawn( Client ) - self:F( { Client } ) - - local SpawnCargo = true - - if self:IsStatusNone() then - local CargoGroup = Group.getByName( self.CargoName ) - if CargoGroup and CargoGroup:isExist() then - SpawnCargo = false - end - - elseif self:IsStatusLoading() then - - local Client = self:IsLoadingToClient() - if Client and Client:GetDCSGroup() then - SpawnCargo = false - else - local CargoGroup = Group.getByName( self.CargoName ) - if CargoGroup and CargoGroup:isExist() then - SpawnCargo = false - end - end - - elseif self:IsStatusLoaded() then - - local ClientLoaded = self:IsLoadedInClient() - -- Now test if another Client is alive (not this one), and it has the CARGO, then this cargo does not need to be initialized and spawned. - if ClientLoaded and ClientLoaded ~= Client then - local ClientGroup = Client:GetDCSGroup() - if ClientLoaded:GetClientGroupDCSUnit() and ClientLoaded:GetClientGroupDCSUnit():isExist() then - SpawnCargo = false - else - self:StatusNone() - end - else - -- Same Client, but now in initialize, so set back the status to None. - self:StatusNone() - end - - elseif self:IsStatusUnLoaded() then - - SpawnCargo = false - - end - - if SpawnCargo then - if self.CargoZone:GetCargoHostUnit() then - --- ReSpawn the Cargo from the CargoHost - self.CargoGroupName = self.CargoSpawn:SpawnFromUnit( self.CargoZone:GetCargoHostUnit(), 60, 30, 1 ):GetName() - else - --- ReSpawn the Cargo in the CargoZone without a host ... - self:T( self.CargoZone ) - self.CargoGroupName = self.CargoSpawn:SpawnInZone( self.CargoZone, true, 1 ):GetName() - end - self:StatusNone() - end - - self:T( { self.CargoGroupName, CARGOS[self.CargoName].CargoGroupName } ) - - return self -end - -function CARGO_GROUP:IsNear( Client, LandingZone ) - self:F() - - local Near = false - - if self.CargoGroupName then - local CargoGroup = Group.getByName( self.CargoGroupName ) - if routines.IsPartOfGroupInRadius( CargoGroup, Client:GetPositionVec3(), 250 ) then - Near = true - end - end - - return Near - -end - - -function CARGO_GROUP:OnBoard( Client, LandingZone, OnBoardSide ) - self:F() - - local Valid = true - - local ClientUnit = Client:GetClientGroupDCSUnit() - - local CarrierPos = ClientUnit:getPoint() - local CarrierPosMove = ClientUnit:getPoint() - local CarrierPosOnBoard = ClientUnit:getPoint() - - local CargoGroup = Group.getByName( self.CargoGroupName ) - - local CargoUnit = CargoGroup:getUnit(1) - local CargoPos = CargoUnit:getPoint() - - self.CargoInAir = CargoUnit:inAir() - - self:T( self.CargoInAir ) - - -- Only move the group to the carrier when the cargo is not in the air - -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea). - if not self.CargoInAir then - - local Points = {} - - self:T( 'CargoPos x = ' .. CargoPos.x .. " z = " .. CargoPos.z ) - self:T( 'CarrierPosMove x = ' .. CarrierPosMove.x .. " z = " .. CarrierPosMove.z ) - - Points[#Points+1] = routines.ground.buildWP( CargoPos, "Cone", 10 ) - - self:T( 'Points[1] x = ' .. Points[1].x .. " y = " .. Points[1].y ) - - if OnBoardSide == nil then - OnBoardSide = CLIENT.ONBOARDSIDE.NONE - end - - if OnBoardSide == CLIENT.ONBOARDSIDE.LEFT then - - self:T( "TransportCargoOnBoard: Onboarding LEFT" ) - CarrierPosMove.z = CarrierPosMove.z - 25 - CarrierPosOnBoard.z = CarrierPosOnBoard.z - 5 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - - elseif OnBoardSide == CLIENT.ONBOARDSIDE.RIGHT then - - self:T( "TransportCargoOnBoard: Onboarding RIGHT" ) - CarrierPosMove.z = CarrierPosMove.z + 25 - CarrierPosOnBoard.z = CarrierPosOnBoard.z + 5 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - - elseif OnBoardSide == CLIENT.ONBOARDSIDE.BACK then - - self:T( "TransportCargoOnBoard: Onboarding BACK" ) - CarrierPosMove.x = CarrierPosMove.x - 25 - CarrierPosOnBoard.x = CarrierPosOnBoard.x - 5 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - - elseif OnBoardSide == CLIENT.ONBOARDSIDE.FRONT then - - self:T( "TransportCargoOnBoard: Onboarding FRONT" ) - CarrierPosMove.x = CarrierPosMove.x + 25 - CarrierPosOnBoard.x = CarrierPosOnBoard.x + 5 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - - elseif OnBoardSide == CLIENT.ONBOARDSIDE.NONE then - - self:T( "TransportCargoOnBoard: Onboarding CENTRAL" ) - Points[#Points+1] = routines.ground.buildWP( CarrierPos, "Cone", 10 ) - - end - self:T( "TransportCargoOnBoard: Routing " .. self.CargoGroupName ) - - --routines.scheduleFunction( routines.goRoute, { self.CargoGroupName, Points}, timer.getTime() + 4 ) - SCHEDULER:New( self, routines.goRoute, { self.CargoGroupName, Points}, 4 ) - end - - self:StatusLoading( Client ) - - return Valid - -end - - -function CARGO_GROUP:OnBoarded( Client, LandingZone ) - self:F() - - local OnBoarded = false - - local CargoGroup = Group.getByName( self.CargoGroupName ) - - if not self.CargoInAir then - if routines.IsPartOfGroupInRadius( CargoGroup, Client:GetPositionVec3(), 25 ) then - CargoGroup:destroy() - self:StatusLoaded( Client ) - OnBoarded = true - end - else - CargoGroup:destroy() - self:StatusLoaded( Client ) - OnBoarded = true - end - - return OnBoarded -end - - -function CARGO_GROUP:UnLoad( Client, TargetZoneName ) - self:F() - - self:T( 'self.CargoName = ' .. self.CargoName ) - - local CargoGroup = self.CargoSpawn:SpawnFromUnit( Client:GetClientGroupUnit(), 60, 30 ) - - self.CargoGroupName = CargoGroup:GetName() - self:T( 'self.CargoGroupName = ' .. self.CargoGroupName ) - - CargoGroup:TaskRouteToZone( ZONE:New( TargetZoneName ), true ) - - self:StatusUnLoaded() - - return self -end - - -CARGO_PACKAGE = { - ClassName = "CARGO_PACKAGE" -} - - -function CARGO_PACKAGE:New( CargoType, CargoName, CargoWeight, CargoClient ) local self = BASE:Inherit( self, CARGO:New( CargoType, CargoName, CargoWeight ) ) - self:F( { CargoType, CargoName, CargoWeight, CargoClient } ) - - self.CargoClient = CargoClient - - CARGOS[self.CargoName] = self - - return self - -end - - -function CARGO_PACKAGE:Spawn( Client ) - self:F( { self, Client } ) - - -- this needs to be checked thoroughly - - local CargoClientGroup = self.CargoClient:GetDCSGroup() - if not CargoClientGroup then - if not self.CargoClientSpawn then - self.CargoClientSpawn = SPAWN:New( self.CargoClient:GetClientGroupName() ):Limit( 1, 1 ) - end - self.CargoClientSpawn:ReSpawn( 1 ) - end - - local SpawnCargo = true - - if self:IsStatusNone() then - - elseif self:IsStatusLoading() or self:IsStatusLoaded() then - - local CargoClientLoaded = self:IsLoadedInClient() - if CargoClientLoaded and CargoClientLoaded:GetDCSGroup() then - SpawnCargo = false - end - - elseif self:IsStatusUnLoaded() then - - SpawnCargo = false - - else - - end - - if SpawnCargo then - self:StatusLoaded( self.CargoClient ) - end - - return self -end - - -function CARGO_PACKAGE:IsNear( Client, LandingZone ) - self:F() - - local Near = false - - if self.CargoClient and self.CargoClient:GetDCSGroup() then - self:T( self.CargoClient.ClientName ) - self:T( 'Client Exists.' ) - - if routines.IsUnitInRadius( self.CargoClient:GetClientGroupDCSUnit(), Client:GetPositionVec3(), 150 ) then - Near = true - end - end - - return Near - -end - - -function CARGO_PACKAGE:OnBoard( Client, LandingZone, OnBoardSide ) - self:F() - - local Valid = true - - local ClientUnit = Client:GetClientGroupDCSUnit() - - local CarrierPos = ClientUnit:getPoint() - local CarrierPosMove = ClientUnit:getPoint() - local CarrierPosOnBoard = ClientUnit:getPoint() - local CarrierPosMoveAway = ClientUnit:getPoint() - - local CargoHostGroup = self.CargoClient:GetDCSGroup() - local CargoHostName = self.CargoClient:GetDCSGroup():getName() - - local CargoHostUnits = CargoHostGroup:getUnits() - local CargoPos = CargoHostUnits[1]:getPoint() - - local Points = {} - - self:T( 'CargoPos x = ' .. CargoPos.x .. " z = " .. CargoPos.z ) - self:T( 'CarrierPosMove x = ' .. CarrierPosMove.x .. " z = " .. CarrierPosMove.z ) - - Points[#Points+1] = routines.ground.buildWP( CargoPos, "Cone", 10 ) - - self:T( 'Points[1] x = ' .. Points[1].x .. " y = " .. Points[1].y ) - - if OnBoardSide == nil then - OnBoardSide = CLIENT.ONBOARDSIDE.NONE - end - - if OnBoardSide == CLIENT.ONBOARDSIDE.LEFT then - - self:T( "TransportCargoOnBoard: Onboarding LEFT" ) - CarrierPosMove.z = CarrierPosMove.z - 25 - CarrierPosOnBoard.z = CarrierPosOnBoard.z - 5 - CarrierPosMoveAway.z = CarrierPosMoveAway.z - 20 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosMoveAway, "Cone", 10 ) - - elseif OnBoardSide == CLIENT.ONBOARDSIDE.RIGHT then - - self:T( "TransportCargoOnBoard: Onboarding RIGHT" ) - CarrierPosMove.z = CarrierPosMove.z + 25 - CarrierPosOnBoard.z = CarrierPosOnBoard.z + 5 - CarrierPosMoveAway.z = CarrierPosMoveAway.z + 20 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosMoveAway, "Cone", 10 ) - - elseif OnBoardSide == CLIENT.ONBOARDSIDE.BACK then - - self:T( "TransportCargoOnBoard: Onboarding BACK" ) - CarrierPosMove.x = CarrierPosMove.x - 25 - CarrierPosOnBoard.x = CarrierPosOnBoard.x - 5 - CarrierPosMoveAway.x = CarrierPosMoveAway.x - 20 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosMoveAway, "Cone", 10 ) - - elseif OnBoardSide == CLIENT.ONBOARDSIDE.FRONT then - - self:T( "TransportCargoOnBoard: Onboarding FRONT" ) - CarrierPosMove.x = CarrierPosMove.x + 25 - CarrierPosOnBoard.x = CarrierPosOnBoard.x + 5 - CarrierPosMoveAway.x = CarrierPosMoveAway.x + 20 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosMoveAway, "Cone", 10 ) - - elseif OnBoardSide == CLIENT.ONBOARDSIDE.NONE then - - self:T( "TransportCargoOnBoard: Onboarding FRONT" ) - CarrierPosMove.x = CarrierPosMove.x + 25 - CarrierPosOnBoard.x = CarrierPosOnBoard.x + 5 - CarrierPosMoveAway.x = CarrierPosMoveAway.x + 20 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosMoveAway, "Cone", 10 ) - - end - self:T( "Routing " .. CargoHostName ) - - --routines.scheduleFunction( routines.goRoute, { CargoHostName, Points}, timer.getTime() + 4 ) - SCHEDULER:New( self, routines.goRoute, { CargoHostName, Points }, 4 ) - - return Valid - -end - - -function CARGO_PACKAGE:OnBoarded( Client, LandingZone ) - self:F() - - local OnBoarded = false - - if self.CargoClient and self.CargoClient:GetDCSGroup() then - if routines.IsUnitInRadius( self.CargoClient:GetClientGroupDCSUnit(), self.CargoClient:GetPositionVec3(), 10 ) then - - -- Switch Cargo from self.CargoClient to Client ... Each cargo can have only one client. So assigning the new client for the cargo is enough. - self:StatusLoaded( Client ) - - -- All done, onboarded the Cargo to the new Client. - OnBoarded = true - end - end - - return OnBoarded -end - - -function CARGO_PACKAGE:UnLoad( Client, TargetZoneName ) - self:F() - - self:T( 'self.CargoName = ' .. self.CargoName ) - --self:T( 'self.CargoHostName = ' .. self.CargoHostName ) - - --self.CargoSpawn:FromCarrier( Client:GetDCSGroup(), TargetZoneName, self.CargoHostName ) - self:StatusUnLoaded() - - return Cargo -end - - -CARGO_SLINGLOAD = { - ClassName = "CARGO_SLINGLOAD" -} - - -function CARGO_SLINGLOAD:New( CargoType, CargoName, CargoWeight, CargoZone, CargoHostName, CargoCountryID ) - local self = BASE:Inherit( self, CARGO:New( CargoType, CargoName, CargoWeight ) ) - self:F( { CargoType, CargoName, CargoWeight, CargoZone, CargoHostName, CargoCountryID } ) - - self.CargoHostName = CargoHostName - - -- Cargo will be initialized around the CargoZone position. - self.CargoZone = CargoZone - - self.CargoCount = 0 - self.CargoStaticName = string.format( "%s#%03d", self.CargoName, self.CargoCount ) - - -- The country ID needs to be correctly set. - self.CargoCountryID = CargoCountryID - - CARGOS[self.CargoName] = self - - return self - -end - - -function CARGO_SLINGLOAD:IsLandingRequired() - self:F() - return false -end - - -function CARGO_SLINGLOAD:IsSlingLoad() - self:F() - return true -end - - -function CARGO_SLINGLOAD:Spawn( Client ) - self:F( { self, Client } ) - - local Zone = trigger.misc.getZone( self.CargoZone ) - - local ZonePos = {} - ZonePos.x = Zone.point.x + math.random( Zone.radius / 2 * -1, Zone.radius / 2 ) - ZonePos.y = Zone.point.z + math.random( Zone.radius / 2 * -1, Zone.radius / 2 ) - - self:T( "Cargo Location = " .. ZonePos.x .. ", " .. ZonePos.y ) - - --[[ - -- This does not work in 1.5.2. - CargoStatic = StaticObject.getByName( self.CargoName ) - if CargoStatic then - CargoStatic:destroy() - end - --]] - - CargoStatic = StaticObject.getByName( self.CargoStaticName ) - - if CargoStatic and CargoStatic:isExist() then - CargoStatic:destroy() - end - - -- I need to make every time a new cargo due to bugs in 1.5.2. - - self.CargoCount = self.CargoCount + 1 - self.CargoStaticName = string.format( "%s#%03d", self.CargoName, self.CargoCount ) - - local CargoTemplate = { - ["category"] = "Cargo", - ["shape_name"] = "ab-212_cargo", - ["type"] = "Cargo1", - ["x"] = ZonePos.x, - ["y"] = ZonePos.y, - ["mass"] = self.CargoWeight, - ["name"] = self.CargoStaticName, - ["canCargo"] = true, - ["heading"] = 0, - } - - coalition.addStaticObject( self.CargoCountryID, CargoTemplate ) - --- end - - return self -end - - -function CARGO_SLINGLOAD:IsNear( Client, LandingZone ) - self:F() - - local Near = false - - return Near -end - - -function CARGO_SLINGLOAD:IsInLandingZone( Client, LandingZone ) - self:F() - - local Near = false - - local CargoStaticUnit = StaticObject.getByName( self.CargoName ) - if CargoStaticUnit then - if routines.IsStaticInZones( CargoStaticUnit, LandingZone ) then - Near = true - end - end - - return Near -end - - -function CARGO_SLINGLOAD:OnBoard( Client, LandingZone, OnBoardSide ) - self:F() - - local Valid = true - - - return Valid -end - - -function CARGO_SLINGLOAD:OnBoarded( Client, LandingZone ) - self:F() - - local OnBoarded = false - - local CargoStaticUnit = StaticObject.getByName( self.CargoName ) - if CargoStaticUnit then - if not routines.IsStaticInZones( CargoStaticUnit, LandingZone ) then - OnBoarded = true - end - end - - return OnBoarded -end - - -function CARGO_SLINGLOAD:UnLoad( Client, TargetZoneName ) - self:F() - - self:T( 'self.CargoName = ' .. self.CargoName ) - self:T( 'self.CargoGroupName = ' .. self.CargoGroupName ) - - self:StatusUnLoaded() - - return Cargo -end ---- Message System to display Messages for Clients and Coalitions or All. --- Messages are grouped on the display panel per Category to improve readability for the players. --- Messages are shown on the display panel for an amount of seconds, and will then disappear. --- Messages are identified by an ID. The messages with the same ID belonging to the same category will be overwritten if they were still being displayed on the display panel. --- Messages are created with MESSAGE:@{New}(). --- Messages are sent to Clients with MESSAGE:@{ToClient}(). --- Messages are sent to Coalitions with MESSAGE:@{ToCoalition}(). --- Messages are sent to All Players with MESSAGE:@{ToAll}(). --- @module Message - -Include.File( "Base" ) - ---- The MESSAGE class --- @type MESSAGE -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 #string MessageCategory is a string expressing the Category of the Message. Messages are grouped on the display panel per Category to improve readability. --- @param #number MessageDuration is a number in seconds of how long the MESSAGE should be shown on the display panel. --- @param #string MessageID is a string expressing the ID of the Message. --- @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!", "End of Mission", 25, "Win" ) --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) --- 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" ) -function MESSAGE:New( MessageText, MessageCategory, MessageDuration, MessageID ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( { MessageText, MessageCategory, MessageDuration, MessageID } ) - - -- When no messagecategory is given, we don't show it as a title... - if MessageCategory and MessageCategory ~= "" then - self.MessageCategory = MessageCategory .. ": " - else - self.MessageCategory = "" - end - - self.MessageDuration = MessageDuration - self.MessageID = MessageID - self.MessageTime = timer.getTime() - self.MessageText = MessageText - - self.MessageSent = false - self.MessageGroup = false - self.MessageCoalition = false - - return self -end - ---- Sends a MESSAGE to a Client Group. Note that the Group needs to be defined within the ME with the skillset "Client" or "Player". --- @param #MESSAGE self --- @param Client#CLIENT Client is the Group of the Client. --- @return #MESSAGE --- @usage --- -- Send the 2 messages created with the @{New} method to the Client Group. --- -- Note that the Message of MessageClient2 is overwriting the Message of MessageClient1. --- ClientGroup = Group.getByName( "ClientGroup" ) --- --- MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ):ToClient( ClientGroup ) --- MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ):ToClient( ClientGroup ) --- or --- MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ):ToClient( ClientGroup ) --- MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ):ToClient( ClientGroup ) --- or --- MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ) --- MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ) --- MessageClient1:ToClient( ClientGroup ) --- MessageClient2:ToClient( ClientGroup ) -function MESSAGE:ToClient( Client ) - self:F( Client ) - - if Client and Client:GetClientGroupID() then - - local ClientGroupID = Client:GetClientGroupID() - self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) - trigger.action.outTextForGroup( ClientGroupID, self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration ) - end - - return self -end - ---- Sends a MESSAGE to the Blue coalition. --- @param #MESSAGE self --- @return #MESSAGE --- @usage --- -- Send a message created with the @{New} method to the BLUE coalition. --- MessageBLUE = MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToBlue() --- or --- MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToBlue() --- or --- MessageBLUE = MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) --- MessageBLUE:ToBlue() -function MESSAGE:ToBlue() - self:F() - - self:ToCoalition( coalition.side.BLUE ) - - return self -end - ---- Sends a MESSAGE to the Red Coalition. --- @param #MESSAGE self --- @return #MESSAGE --- @usage --- -- Send a message created with the @{New} method to the RED coalition. --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToRed() --- or --- MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToRed() --- or --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) --- MessageRED:ToRed() -function MESSAGE:ToRed( ) - self:F() - - self:ToCoalition( coalition.side.RED ) - - return self -end - ---- Sends a MESSAGE to a Coalition. --- @param #MESSAGE self --- @param CoalitionSide needs to be filled out by the defined structure of the standard scripting engine @{coalition.side}. --- @return #MESSAGE --- @usage --- -- Send a message created with the @{New} method to the RED coalition. --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToCoalition( coalition.side.RED ) --- or --- MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToCoalition( coalition.side.RED ) --- or --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) --- MessageRED:ToCoalition( coalition.side.RED ) -function MESSAGE:ToCoalition( CoalitionSide ) - self:F( CoalitionSide ) - - if CoalitionSide then - self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) - trigger.action.outTextForCoalition( CoalitionSide, self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration ) - end - - return self -end - ---- Sends a MESSAGE to all players. --- @param #MESSAGE self --- @return #MESSAGE --- @usage --- -- Send a message created to all players. --- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ):ToAll() --- or --- MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ):ToAll() --- or --- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ) --- MessageAll:ToAll() -function MESSAGE:ToAll() - self:F() - - self:ToCoalition( coalition.side.RED ) - self:ToCoalition( coalition.side.BLUE ) - - return self -end - - - ---- The MESSAGEQUEUE class --- @type MESSAGEQUEUE -MESSAGEQUEUE = { - ClientGroups = {}, - CoalitionSides = {} -} - -function MESSAGEQUEUE:New( RefreshInterval ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( { RefreshInterval } ) - - self.RefreshInterval = RefreshInterval - - --self.DisplayFunction = routines.scheduleFunction( self._DisplayMessages, { self }, 0, RefreshInterval ) - self.DisplayFunction = SCHEDULER:New( self, self._DisplayMessages, {}, 0, RefreshInterval ) - - return self -end - ---- This function is called automatically by the MESSAGEQUEUE scheduler. -function MESSAGEQUEUE:_DisplayMessages() - - -- First we display all messages that a coalition needs to receive... Also those who are not in a client (CA module clients...). - for CoalitionSideID, CoalitionSideData in pairs( self.CoalitionSides ) do - for MessageID, MessageData in pairs( CoalitionSideData.Messages ) do - if MessageData.MessageSent == false then - --trigger.action.outTextForCoalition( CoalitionSideID, MessageData.MessageCategory .. '\n' .. MessageData.MessageText:gsub("\n$",""):gsub("\n$",""), MessageData.MessageDuration ) - MessageData.MessageSent = true - end - local MessageTimeLeft = ( MessageData.MessageTime + MessageData.MessageDuration ) - timer.getTime() - if MessageTimeLeft <= 0 then - MessageData = nil - end - end - end - - -- Then we send the messages for each individual client, but also to be included are those Coalition messages for the Clients who belong to a coalition. - -- Because the Client messages will overwrite the Coalition messages (for that Client). - for ClientGroupName, ClientGroupData in pairs( self.ClientGroups ) do - for MessageID, MessageData in pairs( ClientGroupData.Messages ) do - if MessageData.MessageGroup == false then - trigger.action.outTextForGroup( Group.getByName(ClientGroupName):getID(), MessageData.MessageCategory .. '\n' .. MessageData.MessageText:gsub("\n$",""):gsub("\n$",""), MessageData.MessageDuration ) - MessageData.MessageGroup = true - end - local MessageTimeLeft = ( MessageData.MessageTime + MessageData.MessageDuration ) - timer.getTime() - if MessageTimeLeft <= 0 then - MessageData = nil - end - end - - -- Now check if the Client also has messages that belong to the Coalition of the Client... - for CoalitionSideID, CoalitionSideData in pairs( self.CoalitionSides ) do - for MessageID, MessageData in pairs( CoalitionSideData.Messages ) do - local CoalitionGroup = Group.getByName( ClientGroupName ) - if CoalitionGroup and CoalitionGroup:getCoalition() == CoalitionSideID then - if MessageData.MessageCoalition == false then - trigger.action.outTextForGroup( Group.getByName(ClientGroupName):getID(), MessageData.MessageCategory .. '\n' .. MessageData.MessageText:gsub("\n$",""):gsub("\n$",""), MessageData.MessageDuration ) - MessageData.MessageCoalition = true - end - end - local MessageTimeLeft = ( MessageData.MessageTime + MessageData.MessageDuration ) - timer.getTime() - if MessageTimeLeft <= 0 then - MessageData = nil - end - end - end - end - - return true -end - ---- The _MessageQueue object is created when the MESSAGE class module is loaded. ---_MessageQueue = MESSAGEQUEUE:New( 0.5 ) - ---- Stages within a @{TASK} within a @{MISSION}. All of the STAGE functionality is considered internally administered and not to be used by any Mission designer. --- @module STAGE --- @author Flightcontrol - -Include.File( "Routines" ) -Include.File( "Base" ) -Include.File( "Mission" ) -Include.File( "Client" ) -Include.File( "Task" ) - ---- The STAGE class --- @type -STAGE = { - ClassName = "STAGE", - MSG = { ID = "None", TIME = 10 }, - FREQUENCY = { NONE = 0, ONCE = 1, REPEAT = -1 }, - - Name = "NoStage", - StageType = '', - WaitTime = 1, - Frequency = 1, - MessageCount = 0, - MessageInterval = 15, - MessageShown = {}, - MessageShow = false, - MessageFlash = false -} - - -function STAGE:New() - local self = BASE:Inherit( self, BASE:New() ) - self:F() - return self -end - -function STAGE:Execute( Mission, Client, Task ) - - local Valid = true - - return Valid -end - -function STAGE:Executing( Mission, Client, Task ) - -end - -function STAGE:Validate( Mission, Client, Task ) - local Valid = true - - return Valid -end - - -STAGEBRIEF = { - ClassName = "BRIEF", - MSG = { ID = "Brief", TIME = 1 }, - Name = "Brief", - StageBriefingTime = 0, - StageBriefingDuration = 1 -} - -function STAGEBRIEF:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - ---- Execute --- @param #STAGEBRIEF self --- @param Mission#MISSION Mission --- @param Client#CLIENT Client --- @param Task#TASK Task --- @return #boolean -function STAGEBRIEF:Execute( Mission, Client, Task ) - local Valid = BASE:Inherited(self):Execute( Mission, Client, Task ) - self:F() - Client:ShowMissionBriefing( Mission.MissionBriefing ) - self.StageBriefingTime = timer.getTime() - return Valid -end - -function STAGEBRIEF:Validate( Mission, Client, Task ) - local Valid = STAGE:Validate( Mission, Client, Task ) - self:T() - - if timer.getTime() - self.StageBriefingTime <= self.StageBriefingDuration then - return 0 - else - self.StageBriefingTime = timer.getTime() - return 1 - end - -end - - -STAGESTART = { - ClassName = "START", - MSG = { ID = "Start", TIME = 1 }, - Name = "Start", - StageStartTime = 0, - StageStartDuration = 1 -} - -function STAGESTART:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - -function STAGESTART:Execute( Mission, Client, Task ) - self:F() - local Valid = BASE:Inherited(self):Execute( Mission, Client, Task ) - if Task.TaskBriefing then - Client:Message( Task.TaskBriefing, 30, Mission.Name .. "/Stage", "Command" ) - else - Client:Message( 'Task ' .. Task.TaskNumber .. '.', 30, Mission.Name .. "/Stage", "Command" ) - end - self.StageStartTime = timer.getTime() - return Valid -end - -function STAGESTART:Validate( Mission, Client, Task ) - self:F() - local Valid = STAGE:Validate( Mission, Client, Task ) - - if timer.getTime() - self.StageStartTime <= self.StageStartDuration then - return 0 - else - self.StageStartTime = timer.getTime() - return 1 - end - - return 1 - -end - -STAGE_CARGO_LOAD = { - ClassName = "STAGE_CARGO_LOAD" -} - -function STAGE_CARGO_LOAD:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - -function STAGE_CARGO_LOAD:Execute( Mission, Client, Task ) - self:F() - local Valid = BASE:Inherited(self):Execute( Mission, Client, Task ) - - for LoadCargoID, LoadCargo in pairs( Task.Cargos.LoadCargos ) do - LoadCargo:Load( Client ) - end - - if Mission.MissionReportFlash and Client:IsTransport() then - Client:ShowCargo() - end - - return Valid -end - -function STAGE_CARGO_LOAD:Validate( Mission, Client, Task ) - self:F() - local Valid = STAGE:Validate( Mission, Client, Task ) - - return 1 -end - - -STAGE_CARGO_INIT = { - ClassName = "STAGE_CARGO_INIT" -} - -function STAGE_CARGO_INIT:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - -function STAGE_CARGO_INIT:Execute( Mission, Client, Task ) - self:F() - local Valid = BASE:Inherited(self):Execute( Mission, Client, Task ) - - for InitLandingZoneID, InitLandingZone in pairs( Task.LandingZones.LandingZones ) do - self:T( InitLandingZone ) - InitLandingZone:Spawn() - end - - - self:T( Task.Cargos.InitCargos ) - for InitCargoID, InitCargoData in pairs( Task.Cargos.InitCargos ) do - self:T( { InitCargoData } ) - InitCargoData:Spawn( Client ) - end - - return Valid -end - - -function STAGE_CARGO_INIT:Validate( Mission, Client, Task ) - self:F() - local Valid = STAGE:Validate( Mission, Client, Task ) - - return 1 -end - - - -STAGEROUTE = { - ClassName = "STAGEROUTE", - MSG = { ID = "Route", TIME = 5 }, - Frequency = STAGE.FREQUENCY.REPEAT, - Name = "Route" -} - -function STAGEROUTE:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - self.MessageSwitch = true - return self -end - - ---- Execute the routing. --- @param #STAGEROUTE self --- @param Mission#MISSION Mission --- @param Client#CLIENT Client --- @param Task#TASK Task -function STAGEROUTE:Execute( Mission, Client, Task ) - self:F() - local Valid = BASE:Inherited(self):Execute( Mission, Client, Task ) - - local RouteMessage = "Fly to: " - self:T( Task.LandingZones ) - for LandingZoneID, LandingZoneName in pairs( Task.LandingZones.LandingZoneNames ) do - RouteMessage = RouteMessage .. "\n " .. LandingZoneName .. ' at ' .. routines.getBRStringZone( { zone = LandingZoneName, ref = Client:GetClientGroupDCSUnit():getPoint(), true, true } ) .. ' km.' - end - - if Client:IsMultiSeated() then - Client:Message( RouteMessage, self.MSG.TIME, Mission.Name .. "/StageRoute", "Co-Pilot", 20 ) - else - Client:Message( RouteMessage, self.MSG.TIME, Mission.Name .. "/StageRoute", "Command", 20 ) - end - - - if Mission.MissionReportFlash and Client:IsTransport() then - Client:ShowCargo() - end - - return Valid -end - -function STAGEROUTE:Validate( Mission, Client, Task ) - self:F() - local Valid = STAGE:Validate( Mission, Client, Task ) - - -- check if the Client is in the landing zone - self:T( Task.LandingZones.LandingZoneNames ) - Task.CurrentLandingZoneName = routines.IsUnitNearZonesRadius( Client:GetClientGroupDCSUnit(), Task.LandingZones.LandingZoneNames, 500 ) - - if Task.CurrentLandingZoneName then - - Task.CurrentLandingZone = Task.LandingZones.LandingZones[Task.CurrentLandingZoneName].CargoZone - Task.CurrentCargoZone = Task.LandingZones.LandingZones[Task.CurrentLandingZoneName] - - if Task.CurrentCargoZone then - if not Task.Signalled then - Task.Signalled = Task.CurrentCargoZone:Signal() - end - end - - self:T( 1 ) - return 1 - end - - self:T( 0 ) - return 0 -end - - - -STAGELANDING = { - ClassName = "STAGELANDING", - MSG = { ID = "Landing", TIME = 10 }, - Name = "Landing", - Signalled = false -} - -function STAGELANDING:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - ---- Execute the landing coordination. --- @param #STAGELANDING self --- @param Mission#MISSION Mission --- @param Client#CLIENT Client --- @param Task#TASK Task -function STAGELANDING:Execute( Mission, Client, Task ) - self:F() - - if Client:IsMultiSeated() then - Client:Message( "We have arrived at the landing zone.", self.MSG.TIME, Mission.Name .. "/StageArrived", "Co-Pilot", 10 ) - else - Client:Message( "You have arrived at the landing zone.", self.MSG.TIME, Mission.Name .. "/StageArrived", "Command", 10 ) - end - - Task.HostUnit = Task.CurrentCargoZone:GetHostUnit() - - self:T( { Task.HostUnit } ) - - if Task.HostUnit then - - Task.HostUnitName = Task.HostUnit:GetPrefix() - Task.HostUnitTypeName = Task.HostUnit:GetTypeName() - - local HostMessage = "" - Task.CargoNames = "" - - local IsFirst = true - - for CargoID, Cargo in pairs( CARGOS ) do - if Cargo.CargoType == Task.CargoType then - - if Cargo:IsLandingRequired() then - self:T( "Task for cargo " .. Cargo.CargoType .. " requires landing.") - Task.IsLandingRequired = true - end - - if Cargo:IsSlingLoad() then - self:T( "Task for cargo " .. Cargo.CargoType .. " is a slingload.") - Task.IsSlingLoad = true - end - - if IsFirst then - IsFirst = false - Task.CargoNames = Task.CargoNames .. Cargo.CargoName .. "( " .. Cargo.CargoWeight .. " )" - else - Task.CargoNames = Task.CargoNames .. "; " .. Cargo.CargoName .. "( " .. Cargo.CargoWeight .. " )" - end - end - end - - if Task.IsLandingRequired then - HostMessage = "Land the helicopter to " .. Task.TEXT[1] .. " " .. Task.CargoNames .. "." - else - HostMessage = "Use the Radio menu and F6 to find the cargo, then fly or land near the cargo and " .. Task.TEXT[1] .. " " .. Task.CargoNames .. "." - end - - local Host = "Command" - if Task.HostUnitName then - Host = Task.HostUnitName .. " (" .. Task.HostUnitTypeName .. ")" - else - if Client:IsMultiSeated() then - Host = "Co-Pilot" - end - end - - Client:Message( HostMessage, self.MSG.TIME, Mission.Name .. "/STAGELANDING.EXEC." .. Host, Host, 10 ) - - end -end - -function STAGELANDING:Validate( Mission, Client, Task ) - self:F() - - Task.CurrentLandingZoneName = routines.IsUnitNearZonesRadius( Client:GetClientGroupDCSUnit(), Task.LandingZones.LandingZoneNames, 500 ) - if Task.CurrentLandingZoneName then - - -- Client is in de landing zone. - self:T( Task.CurrentLandingZoneName ) - - Task.CurrentLandingZone = Task.LandingZones.LandingZones[Task.CurrentLandingZoneName].CargoZone - Task.CurrentCargoZone = Task.LandingZones.LandingZones[Task.CurrentLandingZoneName] - - if Task.CurrentCargoZone then - if not Task.Signalled then - Task.Signalled = Task.CurrentCargoZone:Signal() - end - end - else - if Task.CurrentLandingZone then - Task.CurrentLandingZone = nil - end - if Task.CurrentCargoZone then - Task.CurrentCargoZone = nil - end - Task.Signalled = false - Task:RemoveCargoMenus( Client ) - self:T( -1 ) - return -1 - end - - - local DCSUnitVelocityVec3 = Client:GetClientGroupDCSUnit():getVelocity() - local DCSUnitVelocity = ( DCSUnitVelocityVec3.x ^2 + DCSUnitVelocityVec3.y ^2 + DCSUnitVelocityVec3.z ^2 ) ^ 0.5 - - local DCSUnitPointVec3 = Client:GetClientGroupDCSUnit():getPoint() - local LandHeight = land.getHeight( { x = DCSUnitPointVec3.x, y = DCSUnitPointVec3.z } ) - local DCSUnitHeight = DCSUnitPointVec3.y - LandHeight - - self:T( { Task.IsLandingRequired, Client:GetClientGroupDCSUnit():inAir() } ) - if Task.IsLandingRequired and not Client:GetClientGroupDCSUnit():inAir() then - self:T( 1 ) - Task.IsInAirTestRequired = true - return 1 - end - - self:T( { DCSUnitVelocity, DCSUnitHeight, LandHeight, Task.CurrentCargoZone.SignalHeight } ) - if Task.IsLandingRequired and DCSUnitVelocity <= 0.05 and DCSUnitHeight <= Task.CurrentCargoZone.SignalHeight then - self:T( 1 ) - Task.IsInAirTestRequired = false - return 1 - end - - self:T( 0 ) - return 0 -end - -STAGELANDED = { - ClassName = "STAGELANDED", - MSG = { ID = "Land", TIME = 10 }, - Name = "Landed", - MenusAdded = false -} - -function STAGELANDED:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - -function STAGELANDED:Execute( Mission, Client, Task ) - self:F() - - if Task.IsLandingRequired then - - local Host = "Command" - if Task.HostUnitName then - Host = Task.HostUnitName .. " (" .. Task.HostUnitTypeName .. ")" - else - if Client:IsMultiSeated() then - Host = "Co-Pilot" - end - end - - Client:Message( 'You have landed within the landing zone. Use the radio menu (F10) to ' .. Task.TEXT[1] .. ' the ' .. Task.CargoType .. '.', - self.MSG.TIME, Mission.Name .. "/STAGELANDED.EXEC" .. Host, Host ) - - if not self.MenusAdded then - Task.Cargo = nil - Task:RemoveCargoMenus( Client ) - Task:AddCargoMenus( Client, CARGOS, 250 ) - end - end -end - - - -function STAGELANDED:Validate( Mission, Client, Task ) - self:F() - - if not routines.IsUnitNearZonesRadius( Client:GetClientGroupDCSUnit(), Task.CurrentLandingZoneName, 500 ) then - self:T( "Client is not anymore in the landing zone, go back to stage Route, and remove cargo menus." ) - Task.Signalled = false - Task:RemoveCargoMenus( Client ) - self:T( -2 ) - return -2 - end - - local DCSUnitVelocityVec3 = Client:GetClientGroupDCSUnit():getVelocity() - local DCSUnitVelocity = ( DCSUnitVelocityVec3.x ^2 + DCSUnitVelocityVec3.y ^2 + DCSUnitVelocityVec3.z ^2 ) ^ 0.5 - - local DCSUnitPointVec3 = Client:GetClientGroupDCSUnit():getPoint() - local LandHeight = land.getHeight( { x = DCSUnitPointVec3.x, y = DCSUnitPointVec3.z } ) - local DCSUnitHeight = DCSUnitPointVec3.y - LandHeight - - self:T( { Task.IsLandingRequired, Client:GetClientGroupDCSUnit():inAir() } ) - if Task.IsLandingRequired and Task.IsInAirTestRequired == true and Client:GetClientGroupDCSUnit():inAir() then - self:T( "Client went back in the air. Go back to stage Landing." ) - self:T( -1 ) - return -1 - end - - self:T( { DCSUnitVelocity, DCSUnitHeight, LandHeight, Task.CurrentCargoZone.SignalHeight } ) - if Task.IsLandingRequired and Task.IsInAirTestRequired == false and DCSUnitVelocity >= 2 and DCSUnitHeight >= Task.CurrentCargoZone.SignalHeight then - self:T( "It seems the Client went back in the air and over the boundary limits. Go back to stage Landing." ) - self:T( -1 ) - return -1 - end - - -- Wait until cargo is selected from the menu. - if Task.IsLandingRequired then - if not Task.Cargo then - self:T( 0 ) - return 0 - end - end - - self:T( 1 ) - return 1 -end - -STAGEUNLOAD = { - ClassName = "STAGEUNLOAD", - MSG = { ID = "Unload", TIME = 10 }, - Name = "Unload" -} - -function STAGEUNLOAD:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - ---- Coordinate UnLoading --- @param #STAGEUNLOAD self --- @param Mission#MISSION Mission --- @param Client#CLIENT Client --- @param Task#TASK Task -function STAGEUNLOAD:Execute( Mission, Client, Task ) - self:F() - - if Client:IsMultiSeated() then - Client:Message( 'The ' .. Task.CargoType .. ' are being ' .. Task.TEXT[2] .. ' within the landing zone. Wait until the helicopter is ' .. Task.TEXT[3] .. '.', - self.MSG.TIME, Mission.Name .. "/StageUnLoad", "Co-Pilot" ) - else - Client:Message( 'You are unloading the ' .. Task.CargoType .. ' ' .. Task.TEXT[2] .. ' within the landing zone. Wait until the helicopter is ' .. Task.TEXT[3] .. '.', - self.MSG.TIME, Mission.Name .. "/StageUnLoad", "Command" ) - end - Task:RemoveCargoMenus( Client ) -end - -function STAGEUNLOAD:Executing( Mission, Client, Task ) - self:F() - env.info( 'STAGEUNLOAD:Executing() Task.Cargo.CargoName = ' .. Task.Cargo.CargoName ) - - local TargetZoneName - - if Task.TargetZoneName then - TargetZoneName = Task.TargetZoneName - else - TargetZoneName = Task.CurrentLandingZoneName - end - - if Task.Cargo:UnLoad( Client, TargetZoneName ) then - Task.ExecuteStage = _TransportExecuteStage.SUCCESS - if Mission.MissionReportFlash then - Client:ShowCargo() - end - end -end - ---- Validate UnLoading --- @param #STAGEUNLOAD self --- @param Mission#MISSION Mission --- @param Client#CLIENT Client --- @param Task#TASK Task -function STAGEUNLOAD:Validate( Mission, Client, Task ) - self:F() - env.info( 'STAGEUNLOAD:Validate()' ) - - if routines.IsUnitNearZonesRadius( Client:GetClientGroupDCSUnit(), Task.CurrentLandingZoneName, 500 ) then - else - Task.ExecuteStage = _TransportExecuteStage.FAILED - Task:RemoveCargoMenus( Client ) - if Client:IsMultiSeated() then - Client:Message( 'The ' .. Task.CargoType .. " haven't been successfully " .. Task.TEXT[3] .. ' within the landing zone. Task and mission has failed.', - _TransportStageMsgTime.DONE, Mission.Name .. "/StageFailure", "Co-Pilot" ) - else - Client:Message( 'The ' .. Task.CargoType .. " haven't been successfully " .. Task.TEXT[3] .. ' within the landing zone. Task and mission has failed.', - _TransportStageMsgTime.DONE, Mission.Name .. "/StageFailure", "Command" ) - end - return 1 - end - - if not Client:GetClientGroupDCSUnit():inAir() then - else - Task.ExecuteStage = _TransportExecuteStage.FAILED - Task:RemoveCargoMenus( Client ) - if Client:IsMultiSeated() then - Client:Message( 'The ' .. Task.CargoType .. " haven't been successfully " .. Task.TEXT[3] .. ' within the landing zone. Task and mission has failed.', - _TransportStageMsgTime.DONE, Mission.Name .. "/StageFailure", "Co-Pilot" ) - else - Client:Message( 'The ' .. Task.CargoType .. " haven't been successfully " .. Task.TEXT[3] .. ' within the landing zone. Task and mission has failed.', - _TransportStageMsgTime.DONE, Mission.Name .. "/StageFailure", "Command" ) - end - return 1 - end - - if Task.ExecuteStage == _TransportExecuteStage.SUCCESS then - if Client:IsMultiSeated() then - Client:Message( 'The ' .. Task.CargoType .. ' have been sucessfully ' .. Task.TEXT[3] .. ' within the landing zone.', _TransportStageMsgTime.DONE, Mission.Name .. "/Stage", "Co-Pilot" ) - else - Client:Message( 'The ' .. Task.CargoType .. ' have been sucessfully ' .. Task.TEXT[3] .. ' within the landing zone.', _TransportStageMsgTime.DONE, Mission.Name .. "/Stage", "Command" ) - end - Task:RemoveCargoMenus( Client ) - Task.MissionTask:AddGoalCompletion( Task.MissionTask.GoalVerb, Task.CargoName, 1 ) -- We set the cargo as one more goal completed in the mission. - return 1 - end - - return 1 -end - -STAGELOAD = { - ClassName = "STAGELOAD", - MSG = { ID = "Load", TIME = 10 }, - Name = "Load" -} - -function STAGELOAD:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - -function STAGELOAD:Execute( Mission, Client, Task ) - self:F() - - if not Task.IsSlingLoad then - - local Host = "Command" - if Task.HostUnitName then - Host = Task.HostUnitName .. " (" .. Task.HostUnitTypeName .. ")" - else - if Client:IsMultiSeated() then - Host = "Co-Pilot" - end - end - - Client:Message( 'The ' .. Task.CargoType .. ' are being ' .. Task.TEXT[2] .. ' within the landing zone. Wait until the helicopter is ' .. Task.TEXT[3] .. '.', - _TransportStageMsgTime.EXECUTING, Mission.Name .. "/STAGELOAD.EXEC." .. Host, Host ) - - -- Route the cargo to the Carrier - - Task.Cargo:OnBoard( Client, Task.CurrentCargoZone, Task.OnBoardSide ) - Task.ExecuteStage = _TransportExecuteStage.EXECUTING - else - Task.ExecuteStage = _TransportExecuteStage.EXECUTING - end -end - -function STAGELOAD:Executing( Mission, Client, Task ) - self:F() - - -- If the Cargo is ready to be loaded, load it into the Client. - - local Host = "Command" - if Task.HostUnitName then - Host = Task.HostUnitName .. " (" .. Task.HostUnitTypeName .. ")" - else - if Client:IsMultiSeated() then - Host = "Co-Pilot" - end - end - - if not Task.IsSlingLoad then - self:T( Task.Cargo.CargoName) - - if Task.Cargo:OnBoarded( Client, Task.CurrentCargoZone ) then - - -- Load the Cargo onto the Client - Task.Cargo:Load( Client ) - - -- Message to the pilot that cargo has been loaded. - Client:Message( "The cargo " .. Task.Cargo.CargoName .. " has been loaded in our helicopter.", - 20, Mission.Name .. "/STAGELANDING.LOADING1." .. Host, Host ) - Task.ExecuteStage = _TransportExecuteStage.SUCCESS - - Client:ShowCargo() - end - else - Client:Message( "Hook the " .. Task.CargoNames .. " onto the helicopter " .. Task.TEXT[3] .. " within the landing zone.", - _TransportStageMsgTime.EXECUTING, Mission.Name .. "/STAGELOAD.LOADING.1." .. Host, Host , 10 ) - for CargoID, Cargo in pairs( CARGOS ) do - self:T( "Cargo.CargoName = " .. Cargo.CargoName ) - - if Cargo:IsSlingLoad() then - local CargoStatic = StaticObject.getByName( Cargo.CargoStaticName ) - if CargoStatic then - self:T( "Cargo is found in the DCS simulator.") - local CargoStaticPosition = CargoStatic:getPosition().p - self:T( "Cargo Position x = " .. CargoStaticPosition.x .. ", y = " .. CargoStaticPosition.y .. ", z = " .. CargoStaticPosition.z ) - local CargoStaticHeight = routines.GetUnitHeight( CargoStatic ) - if CargoStaticHeight > 5 then - self:T( "Cargo is airborne.") - Cargo:StatusLoaded() - Task.Cargo = Cargo - Client:Message( 'The Cargo has been successfully hooked onto the helicopter and is now being sling loaded. Fly outside the landing zone.', - self.MSG.TIME, Mission.Name .. "/STAGELANDING.LOADING.2." .. Host, Host ) - Task.ExecuteStage = _TransportExecuteStage.SUCCESS - break - end - else - self:T( "Cargo not found in the DCS simulator." ) - end - end - end - end - -end - -function STAGELOAD:Validate( Mission, Client, Task ) - self:F() - - self:T( "Task.CurrentLandingZoneName = " .. Task.CurrentLandingZoneName ) - - local Host = "Command" - if Task.HostUnitName then - Host = Task.HostUnitName .. " (" .. Task.HostUnitTypeName .. ")" - else - if Client:IsMultiSeated() then - Host = "Co-Pilot" - end - end - - if not Task.IsSlingLoad then - if not routines.IsUnitNearZonesRadius( Client:GetClientGroupDCSUnit(), Task.CurrentLandingZoneName, 500 ) then - Task:RemoveCargoMenus( Client ) - Task.ExecuteStage = _TransportExecuteStage.FAILED - Task.CargoName = nil - Client:Message( "The " .. Task.CargoType .. " loading has been aborted. You flew outside the pick-up zone while loading. ", - self.MSG.TIME, Mission.Name .. "/STAGELANDING.VALIDATE.1." .. Host, Host ) - self:T( -1 ) - return -1 - end - - local DCSUnitVelocityVec3 = Client:GetClientGroupDCSUnit():getVelocity() - local DCSUnitVelocity = ( DCSUnitVelocityVec3.x ^2 + DCSUnitVelocityVec3.y ^2 + DCSUnitVelocityVec3.z ^2 ) ^ 0.5 - - local DCSUnitPointVec3 = Client:GetClientGroupDCSUnit():getPoint() - local LandHeight = land.getHeight( { x = DCSUnitPointVec3.x, y = DCSUnitPointVec3.z } ) - local DCSUnitHeight = DCSUnitPointVec3.y - LandHeight - - self:T( { Task.IsLandingRequired, Client:GetClientGroupDCSUnit():inAir() } ) - if Task.IsLandingRequired and Task.IsInAirTestRequired == true and Client:GetClientGroupDCSUnit():inAir() then - Task:RemoveCargoMenus( Client ) - Task.ExecuteStage = _TransportExecuteStage.FAILED - Task.CargoName = nil - Client:Message( "The " .. Task.CargoType .. " loading has been aborted. Re-start the " .. Task.TEXT[3] .. " process. Don't fly outside the pick-up zone.", - self.MSG.TIME, Mission.Name .. "/STAGELANDING.VALIDATE.1." .. Host, Host ) - self:T( -1 ) - return -1 - end - - self:T( { DCSUnitVelocity, DCSUnitHeight, LandHeight, Task.CurrentCargoZone.SignalHeight } ) - if Task.IsLandingRequired and Task.IsInAirTestRequired == false and DCSUnitVelocity >= 2 and DCSUnitHeight >= Task.CurrentCargoZone.SignalHeight then - Task:RemoveCargoMenus( Client ) - Task.ExecuteStage = _TransportExecuteStage.FAILED - Task.CargoName = nil - Client:Message( "The " .. Task.CargoType .. " loading has been aborted. Re-start the " .. Task.TEXT[3] .. " process. Don't fly outside the pick-up zone.", - self.MSG.TIME, Mission.Name .. "/STAGELANDING.VALIDATE.1." .. Host, Host ) - self:T( -1 ) - return -1 - end - - if Task.ExecuteStage == _TransportExecuteStage.SUCCESS then - Task:RemoveCargoMenus( Client ) - Client:Message( "Good Job. The " .. Task.CargoType .. " has been sucessfully " .. Task.TEXT[3] .. " within the landing zone.", - self.MSG.TIME, Mission.Name .. "/STAGELANDING.VALIDATE.3." .. Host, Host ) - Task.MissionTask:AddGoalCompletion( Task.MissionTask.GoalVerb, Task.CargoName, 1 ) - self:T( 1 ) - return 1 - end - - else - if Task.ExecuteStage == _TransportExecuteStage.SUCCESS then - CargoStatic = StaticObject.getByName( Task.Cargo.CargoStaticName ) - if CargoStatic and not routines.IsStaticInZones( CargoStatic, Task.CurrentLandingZoneName ) then - Client:Message( "Good Job. The " .. Task.CargoType .. " has been sucessfully " .. Task.TEXT[3] .. " and flown outside of the landing zone.", - self.MSG.TIME, Mission.Name .. "/STAGELANDING.VALIDATE.4." .. Host, Host ) - Task.MissionTask:AddGoalCompletion( Task.MissionTask.GoalVerb, Task.Cargo.CargoName, 1 ) - self:T( 1 ) - return 1 - end - end - - end - - - self:T( 0 ) - return 0 -end - - -STAGEDONE = { - ClassName = "STAGEDONE", - MSG = { ID = "Done", TIME = 10 }, - Name = "Done" -} - -function STAGEDONE:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'AI' - return self -end - -function STAGEDONE:Execute( Mission, Client, Task ) - self:F() - -end - -function STAGEDONE:Validate( Mission, Client, Task ) - self:F() - - Task:Done() - - return 0 -end - -STAGEARRIVE = { - ClassName = "STAGEARRIVE", - MSG = { ID = "Arrive", TIME = 10 }, - Name = "Arrive" -} - -function STAGEARRIVE:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - - ---- Execute Arrival --- @param #STAGEARRIVE self --- @param Mission#MISSION Mission --- @param Client#CLIENT Client --- @param Task#TASK Task -function STAGEARRIVE:Execute( Mission, Client, Task ) - self:F() - - if Client:IsMultiSeated() then - Client:Message( 'We have arrived at ' .. Task.CurrentLandingZoneName .. ".", self.MSG.TIME, Mission.Name .. "/Stage", "Co-Pilot" ) - else - Client:Message( 'We have arrived at ' .. Task.CurrentLandingZoneName .. ".", self.MSG.TIME, Mission.Name .. "/Stage", "Command" ) - end - -end - -function STAGEARRIVE:Validate( Mission, Client, Task ) - self:F() - - Task.CurrentLandingZoneID = routines.IsUnitInZones( Client:GetClientGroupDCSUnit(), Task.LandingZones ) - if ( Task.CurrentLandingZoneID ) then - else - return -1 - end - - return 1 -end - -STAGEGROUPSDESTROYED = { - ClassName = "STAGEGROUPSDESTROYED", - DestroyGroupSize = -1, - Frequency = STAGE.FREQUENCY.REPEAT, - MSG = { ID = "DestroyGroup", TIME = 10 }, - Name = "GroupsDestroyed" -} - -function STAGEGROUPSDESTROYED:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'AI' - return self -end - ---function STAGEGROUPSDESTROYED:Execute( Mission, Client, Task ) --- --- Client:Message( 'Task: Still ' .. DestroyGroupSize .. " of " .. Task.DestroyGroupCount .. " " .. Task.DestroyGroupType .. " to be destroyed!", self.MSG.TIME, Mission.Name .. "/Stage" ) --- ---end - -function STAGEGROUPSDESTROYED:Validate( Mission, Client, Task ) - self:F() - - if Task.MissionTask:IsGoalReached() then - return 1 - else - return 0 - end -end - -function STAGEGROUPSDESTROYED:Execute( Mission, Client, Task ) - self:F() - self:T( { Task.ClassName, Task.Destroyed } ) - --env.info( 'Event Table Task = ' .. tostring(Task) ) - -end - - - - - - - - - - - - - ---[[ - _TransportStage: Defines the different stages of which of transport missions can be in. This table is internal and is used to control the sequence of messages, actions and flow. - - - _TransportStage.START - - _TransportStage.ROUTE - - _TransportStage.LAND - - _TransportStage.EXECUTE - - _TransportStage.DONE - - _TransportStage.REMOVE ---]] -_TransportStage = { - HOLD = "HOLD", - START = "START", - ROUTE = "ROUTE", - LANDING = "LANDING", - LANDED = "LANDED", - EXECUTING = "EXECUTING", - LOAD = "LOAD", - UNLOAD = "UNLOAD", - DONE = "DONE", - NEXT = "NEXT" -} - -_TransportStageMsgTime = { - HOLD = 10, - START = 60, - ROUTE = 5, - LANDING = 10, - LANDED = 30, - EXECUTING = 30, - LOAD = 30, - UNLOAD = 30, - DONE = 30, - NEXT = 0 -} - -_TransportStageTime = { - HOLD = 10, - START = 5, - ROUTE = 5, - LANDING = 1, - LANDED = 1, - EXECUTING = 5, - LOAD = 5, - UNLOAD = 5, - DONE = 1, - NEXT = 0 -} - -_TransportStageAction = { - REPEAT = -1, - NONE = 0, - ONCE = 1 -} ---- The TASK Classes define major end-to-end activities within a MISSION. The TASK Class is the Master Class to orchestrate these activities. From this class, many concrete TASK classes are inherited. --- @module TASK - -Include.File( "Routines" ) -Include.File( "Base" ) -Include.File( "Mission" ) -Include.File( "Client" ) -Include.File( "Stage" ) - ---- The TASK class --- @type TASK --- @extends Base#BASE -TASK = { - - -- Defines the different signal types with a Task. - SIGNAL = { - COLOR = { - RED = { ID = 1, COLOR = trigger.smokeColor.Red, TEXT = "A red" }, - GREEN = { ID = 2, COLOR = trigger.smokeColor.Green, TEXT = "A green" }, - BLUE = { ID = 3, COLOR = trigger.smokeColor.Blue, TEXT = "A blue" }, - WHITE = { ID = 4, COLOR = trigger.smokeColor.White, TEXT = "A white" }, - ORANGE = { ID = 5, COLOR = trigger.smokeColor.Orange, TEXT = "An orange" } - }, - TYPE = { - SMOKE = { ID = 1, TEXT = "smoke" }, - FLARE = { ID = 2, TEXT = "flare" } - } - }, - ClassName = "TASK", - Mission = {}, -- Owning mission of the Task - Name = '', - Stages = {}, - Stage = {}, - Cargos = { - InitCargos = {}, - LoadCargos = {} - }, - LandingZones = { - LandingZoneNames = {}, - LandingZones = {} - }, - ActiveStage = 0, - TaskDone = false, - TaskFailed = false, - GoalTasks = {} -} - ---- Instantiates a new TASK Base. Should never be used. Interface Class. --- @return TASK -function TASK:New() - local self = BASE:Inherit( self, BASE:New() ) - self:F() - - -- assign Task default values during construction - self.TaskBriefing = "Task: No Task." - self.Time = timer.getTime() - self.ExecuteStage = _TransportExecuteStage.NONE - - return self -end - -function TASK:SetStage( StageSequenceIncrement ) - self:F( { StageSequenceIncrement } ) - - local Valid = false - if StageSequenceIncrement ~= 0 then - self.ActiveStage = self.ActiveStage + StageSequenceIncrement - if 1 <= self.ActiveStage and self.ActiveStage <= #self.Stages then - self.Stage = self.Stages[self.ActiveStage] - self:T( { self.Stage.Name } ) - self.Frequency = self.Stage.Frequency - Valid = true - else - Valid = false - env.info( "TASK:SetStage() self.ActiveStage is smaller or larger than self.Stages array. self.ActiveStage = " .. self.ActiveStage ) - end - end - self.Time = timer.getTime() - return Valid -end - -function TASK:Init() - self:F() - self.ActiveStage = 0 - self:SetStage(1) - self.TaskDone = false - self.TaskFailed = false -end - - ---- Get progress of a TASK. --- @return string GoalsText -function TASK:GetGoalProgress() - self:F2() - - local GoalsText = "" - for GoalVerb, GoalVerbData in pairs( self.GoalTasks ) do - local Goals = self:GetGoalCompletion( GoalVerb ) - if Goals and Goals ~= "" then - Goals = '(' .. Goals .. ')' - else - Goals = '( - )' - end - GoalsText = GoalsText .. GoalVerb .. ': ' .. self:GetGoalCount(GoalVerb) .. ' goals ' .. Goals .. ' of ' .. self:GetGoalTotal(GoalVerb) .. ' goals completed (' .. self:GetGoalPercentage(GoalVerb) .. '%); ' - end - - if GoalsText == "" then - GoalsText = "( - )" - end - - return GoalsText -end - ---- Show progress of a TASK. --- @param MISSION Mission Group structure describing the Mission. --- @param CLIENT Client Group structure describing the Client. -function TASK:ShowGoalProgress( Mission, Client ) - self:F2() - - local GoalsText = "" - for GoalVerb, GoalVerbData in pairs( self.GoalTasks ) do - if Mission:IsCompleted() then - else - local Goals = self:GetGoalCompletion( GoalVerb ) - if Goals and Goals ~= "" then - else - Goals = "-" - end - GoalsText = GoalsText .. self:GetGoalProgress() - end - end - - if Mission.MissionReportFlash or Mission.MissionReportShow then - Client:Message( GoalsText, 10, "/TASKPROGRESS" .. self.ClassName, "Mission Command: Task Status", 30 ) - end -end - ---- Sets a TASK to status Done. -function TASK:Done() - self:F2() - self.TaskDone = true -end - ---- Returns if a TASK is done. --- @return bool -function TASK:IsDone() - self:F2( self.TaskDone ) - return self.TaskDone -end - ---- Sets a TASK to status failed. -function TASK:Failed() - self:F() - self.TaskFailed = true -end - ---- Returns if a TASk has failed. --- @return bool -function TASK:IsFailed() - self:F2( self.TaskFailed ) - return self.TaskFailed -end - -function TASK:Reset( Mission, Client ) - self:F2() - self.ExecuteStage = _TransportExecuteStage.NONE -end - ---- Returns the Goals of a TASK --- @return @table Goals -function TASK:GetGoals() - return self.GoalTasks -end - ---- Returns if a TASK has Goal(s). --- @param #TASK self --- @param #string GoalVerb is the name of the Goal of the TASK. --- @return bool -function TASK:Goal( GoalVerb ) - self:F2( { GoalVerb } ) - if not GoalVerb then - GoalVerb = self.GoalVerb - end - self:T2( {self.GoalTasks[GoalVerb] } ) - if self.GoalTasks[GoalVerb] and self.GoalTasks[GoalVerb].GoalTotal > 0 then - return true - else - return false - end -end - ---- Sets the total Goals to be achieved of the Goal Name --- @param number GoalTotal is the number of times the GoalVerb needs to be achieved. --- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. -function TASK:SetGoalTotal( GoalTotal, GoalVerb ) - self:F2( { GoalTotal, GoalVerb } ) - - if not GoalVerb then - GoalVerb = self.GoalVerb - end - self.GoalTasks[GoalVerb] = {} - self.GoalTasks[GoalVerb].Goals = {} - self.GoalTasks[GoalVerb].GoalTotal = GoalTotal - self.GoalTasks[GoalVerb].GoalCount = 0 - return self -end - ---- Gets the total of Goals to be achieved within the TASK of the GoalVerb. --- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. -function TASK:GetGoalTotal( GoalVerb ) - self:F2( { GoalVerb } ) - if not GoalVerb then - GoalVerb = self.GoalVerb - end - if self:Goal( GoalVerb ) then - return self.GoalTasks[GoalVerb].GoalTotal - else - return 0 - end -end - ---- Sets the total of Goals currently achieved within the TASK of the GoalVerb. --- @param number GoalCount is the total number of Goals achieved within the TASK. --- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. --- @return TASK -function TASK:SetGoalCount( GoalCount, GoalVerb ) - self:F2() - if not GoalVerb then - GoalVerb = self.GoalVerb - end - if self:Goal( GoalVerb) then - self.GoalTasks[GoalVerb].GoalCount = GoalCount - end - return self -end - ---- Increments the total of Goals currently achieved within the TASK of the GoalVerb, with the given GoalCountIncrease. --- @param number GoalCountIncrease is the number of new Goals achieved within the TASK. --- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. --- @return TASK -function TASK:IncreaseGoalCount( GoalCountIncrease, GoalVerb ) - self:F2( { GoalCountIncrease, GoalVerb } ) - if not GoalVerb then - GoalVerb = self.GoalVerb - end - if self:Goal( GoalVerb) then - self.GoalTasks[GoalVerb].GoalCount = self.GoalTasks[GoalVerb].GoalCount + GoalCountIncrease - end - return self -end - ---- Gets the total of Goals currently achieved within the TASK of the GoalVerb. --- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. --- @return TASK -function TASK:GetGoalCount( GoalVerb ) - self:F2() - if not GoalVerb then - GoalVerb = self.GoalVerb - end - if self:Goal( GoalVerb ) then - return self.GoalTasks[GoalVerb].GoalCount - else - return 0 - end -end - ---- Gets the percentage of Goals currently achieved within the TASK of the GoalVerb. --- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. --- @return TASK -function TASK:GetGoalPercentage( GoalVerb ) - self:F2() - if not GoalVerb then - GoalVerb = self.GoalVerb - end - if self:Goal( GoalVerb ) then - return math.floor( self:GetGoalCount( GoalVerb ) / self:GetGoalTotal( GoalVerb ) * 100 + .5 ) - else - return 100 - end -end - ---- Returns if all the Goals of the TASK were achieved. --- @return bool -function TASK:IsGoalReached() - self:F2() - - local GoalReached = true - - for GoalVerb, Goals in pairs( self.GoalTasks ) do - self:T2( { "GoalVerb", GoalVerb } ) - if self:Goal( GoalVerb ) then - local GoalToDo = self:GetGoalTotal( GoalVerb ) - self:GetGoalCount( GoalVerb ) - self:T2( "GoalToDo = " .. GoalToDo ) - if GoalToDo <= 0 then - else - GoalReached = false - break - end - else - break - end - end - - self:T( { GoalReached, self.GoalTasks } ) - return GoalReached -end - ---- Adds an Additional Goal for the TASK to be achieved. --- @param string GoalVerb is the name of the Goal of the TASK. --- @param string GoalTask is a text describing the Goal of the TASK to be achieved. --- @param number GoalIncrease is a number by which the Goal achievement is increasing. -function TASK:AddGoalCompletion( GoalVerb, GoalTask, GoalIncrease ) - self:F2( { GoalVerb, GoalTask, GoalIncrease } ) - - if self:Goal( GoalVerb ) then - self.GoalTasks[GoalVerb].Goals[#self.GoalTasks[GoalVerb].Goals+1] = GoalTask - self.GoalTasks[GoalVerb].GoalCount = self.GoalTasks[GoalVerb].GoalCount + GoalIncrease - end - return self -end - ---- Returns if the additional Goal for the TASK was completed. --- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. --- @return string Goals -function TASK:GetGoalCompletion( GoalVerb ) - self:F2( { GoalVerb } ) - - if self:Goal( GoalVerb ) then - local Goals = "" - for GoalID, GoalName in pairs( self.GoalTasks[GoalVerb].Goals ) do Goals = Goals .. GoalName .. " + " end - return Goals:gsub(" + $", ""), self.GoalTasks[GoalVerb].GoalCount - end -end - -function TASK.MenuAction( Parameter ) - Parameter.ReferenceTask.ExecuteStage = _TransportExecuteStage.EXECUTING - Parameter.ReferenceTask.Cargo = Parameter.CargoTask -end - -function TASK:StageExecute() - self:F() - - local Execute = false - - if self.Frequency == STAGE.FREQUENCY.REPEAT then - Execute = true - elseif self.Frequency == STAGE.FREQUENCY.NONE then - Execute = false - elseif self.Frequency >= 0 then - Execute = true - self.Frequency = self.Frequency - 1 - end - - return Execute - -end - ---- Work function to set signal events within a TASK. -function TASK:AddSignal( SignalUnitNames, SignalType, SignalColor, SignalHeight ) - self:F() - - local Valid = true - - if Valid then - if type( SignalUnitNames ) == "table" then - self.LandingZoneSignalUnitNames = SignalUnitNames - else - self.LandingZoneSignalUnitNames = { SignalUnitNames } - end - self.LandingZoneSignalType = SignalType - self.LandingZoneSignalColor = SignalColor - self.Signalled = false - if SignalHeight ~= nil then - self.LandingZoneSignalHeight = SignalHeight - else - self.LandingZoneSignalHeight = 0 - end - - if self.TaskBriefing then - self.TaskBriefing = self.TaskBriefing .. " " .. SignalColor.TEXT .. " " .. SignalType.TEXT .. " will be fired when entering the landing zone." - end - end - - return Valid -end - ---- When the CLIENT is approaching the landing zone, a RED SMOKE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddSmokeRed( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.SMOKE, TASK.SIGNAL.COLOR.RED, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, a GREEN SMOKE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddSmokeGreen( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.SMOKE, TASK.SIGNAL.COLOR.GREEN, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, a BLUE SMOKE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddSmokeBlue( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.SMOKE, TASK.SIGNAL.COLOR.BLUE, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, a WHITE SMOKE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddSmokeWhite( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.SMOKE, TASK.SIGNAL.COLOR.WHITE, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, an ORANGE SMOKE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddSmokeOrange( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.SMOKE, TASK.SIGNAL.COLOR.ORANGE, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, a RED FLARE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddFlareRed( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.FLARE, TASK.SIGNAL.COLOR.RED, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, a GREEN FLARE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddFlareGreen( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.FLARE, TASK.SIGNAL.COLOR.GREEN, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, a BLUE FLARE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddFlareBlue( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.FLARE, TASK.SIGNAL.COLOR.BLUE, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, a WHITE FLARE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddFlareWhite( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.FLARE, TASK.SIGNAL.COLOR.WHITE, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, an ORANGE FLARE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddFlareOrange( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.FLARE, TASK.SIGNAL.COLOR.ORANGE, SignalHeight ) -end ---- A GOHOMETASK orchestrates the travel back to the home base, which is a specific zone defined within the ME. --- @module GOHOMETASK - -Include.File("Task") - ---- The GOHOMETASK class --- @type -GOHOMETASK = { - ClassName = "GOHOMETASK", -} - ---- Creates a new GOHOMETASK. --- @param table{string,...}|string LandingZones Table of Landing Zone names where Home(s) are located. --- @return GOHOMETASK -function GOHOMETASK:New( LandingZones ) - local self = BASE:Inherit( self, TASK:New() ) - self:F( { LandingZones } ) - local Valid = true - - Valid = routines.ValidateZone( LandingZones, "LandingZones", Valid ) - - if Valid then - self.Name = 'Fly Home' - self.TaskBriefing = "Task: Fly back to your home base. Your co-pilot will provide you with the directions (required flight angle in degrees) and the distance (in km) to your home base." - if type( LandingZones ) == "table" then - self.LandingZones = LandingZones - else - self.LandingZones = { LandingZones } - end - self.Stages = { STAGEBRIEF:New(), STAGESTART:New(), STAGEROUTE:New(), STAGEARRIVE:New(), STAGEDONE:New() } - self.SetStage( self, 1 ) - end - - return self -end ---- A DESTROYBASETASK will monitor the destruction of Groups and Units. This is a BASE class, other classes are derived from this class. --- @module DESTROYBASETASK --- @see DESTROYGROUPSTASK --- @see DESTROYUNITTYPESTASK --- @see DESTROY_RADARS_TASK - -Include.File("Task") - ---- The DESTROYBASETASK class --- @type DESTROYBASETASK -DESTROYBASETASK = { - ClassName = "DESTROYBASETASK", - Destroyed = 0, - GoalVerb = "Destroy", - DestroyPercentage = 100, -} - ---- Creates a new DESTROYBASETASK. --- @param #DESTROYBASETASK self --- @param #string DestroyGroupType Text describing the group to be destroyed. f.e. "Radar Installations", "Ships", "Vehicles", "Command Centers". --- @param #string DestroyUnitType Text describing the unit types to be destroyed. f.e. "SA-6", "Row Boats", "Tanks", "Tents". --- @param #list<#string> DestroyGroupPrefixes Table of Prefixes of the Groups to be destroyed before task is completed. --- @param #number DestroyPercentage defines the %-tage that needs to be destroyed to achieve mission success. eg. If in the Group there are 10 units, then a value of 75 would require 8 units to be destroyed from the Group to complete the @{TASK}. --- @return DESTROYBASETASK -function DESTROYBASETASK:New( DestroyGroupType, DestroyUnitType, DestroyGroupPrefixes, DestroyPercentage ) - local self = BASE:Inherit( self, TASK:New() ) - self:F() - - self.Name = 'Destroy' - self.Destroyed = 0 - self.DestroyGroupPrefixes = DestroyGroupPrefixes - self.DestroyGroupType = DestroyGroupType - self.DestroyUnitType = DestroyUnitType - if DestroyPercentage then - self.DestroyPercentage = DestroyPercentage - end - self.TaskBriefing = "Task: Destroy " .. DestroyGroupType .. "." - self.Stages = { STAGEBRIEF:New(), STAGESTART:New(), STAGEGROUPSDESTROYED:New(), STAGEDONE:New() } - self.SetStage( self, 1 ) - - return self -end - ---- Handle the S_EVENT_DEAD events to validate the destruction of units for the task monitoring. --- @param #DESTROYBASETASK self --- @param Event#EVENTDATA Event structure of MOOSE. -function DESTROYBASETASK:EventDead( Event ) - self:F( { Event } ) - - if Event.IniDCSUnit then - local DestroyUnit = Event.IniDCSUnit - local DestroyUnitName = Event.IniDCSUnitName - local DestroyGroup = Event.IniDCSGroup - local DestroyGroupName = Event.IniDCSGroupName - - --TODO: I need to fix here if 2 groups in the mission have a similar name with GroupPrefix equal, then i should differentiate for which group the goal was reached! - --I may need to test if for the goalverb that group goal was reached or something. Need to think about it a bit more ... - local UnitsDestroyed = 0 - for DestroyGroupPrefixID, DestroyGroupPrefix in pairs( self.DestroyGroupPrefixes ) do - self:T( DestroyGroupPrefix ) - if string.find( DestroyGroupName, DestroyGroupPrefix, 1, true ) then - self:T( BASE:Inherited(self).ClassName ) - UnitsDestroyed = self:ReportGoalProgress( DestroyGroup, DestroyUnit ) - self:T( UnitsDestroyed ) - end - end - - self:T( { UnitsDestroyed } ) - self:IncreaseGoalCount( UnitsDestroyed, self.GoalVerb ) - end - -end - ---- Validate task completeness of DESTROYBASETASK. --- @param DestroyGroup Group structure describing the group to be evaluated. --- @param DestroyUnit Unit structure describing the Unit to be evaluated. -function DESTROYBASETASK:ReportGoalProgress( DestroyGroup, DestroyUnit ) - self:F() - - return 0 -end ---- DESTROYGROUPSTASK --- @module DESTROYGROUPSTASK - -Include.File("DestroyBaseTask") - ---- The DESTROYGROUPSTASK class --- @type -DESTROYGROUPSTASK = { - ClassName = "DESTROYGROUPSTASK", - GoalVerb = "Destroy Groups", -} - ---- Creates a new DESTROYGROUPSTASK. --- @param #DESTROYGROUPSTASK self --- @param #string DestroyGroupType String describing the group to be destroyed. --- @param #string DestroyUnitType String describing the unit to be destroyed. --- @param #list<#string> DestroyGroupNames Table of string containing the name of the groups to be destroyed before task is completed. --- @param #number DestroyPercentage defines the %-tage that needs to be destroyed to achieve mission success. eg. If in the Group there are 10 units, then a value of 75 would require 8 units to be destroyed from the Group to complete the @{TASK}. ----@return DESTROYGROUPSTASK -function DESTROYGROUPSTASK:New( DestroyGroupType, DestroyUnitType, DestroyGroupNames, DestroyPercentage ) - local self = BASE:Inherit( self, DESTROYBASETASK:New( DestroyGroupType, DestroyUnitType, DestroyGroupNames, DestroyPercentage ) ) - self:F() - - self.Name = 'Destroy Groups' - self.GoalVerb = "Destroy " .. DestroyGroupType - - _EVENTDISPATCHER:OnDead( self.EventDead , self ) - _EVENTDISPATCHER:OnCrash( self.EventDead , self ) - - return self -end - ---- Report Goal Progress. --- @param #DESTROYGROUPSTASK self --- @param DCSGroup#Group DestroyGroup Group structure describing the group to be evaluated. --- @param DCSUnit#Unit DestroyUnit Unit structure describing the Unit to be evaluated. --- @return #number The DestroyCount reflecting the amount of units destroyed within the group. -function DESTROYGROUPSTASK:ReportGoalProgress( DestroyGroup, DestroyUnit ) - self:F( { DestroyGroup, DestroyUnit, self.DestroyPercentage } ) - - local DestroyGroupSize = DestroyGroup:getSize() - 1 -- When a DEAD event occurs, the getSize is still one larger than the destroyed unit. - local DestroyGroupInitialSize = DestroyGroup:getInitialSize() - self:T( { DestroyGroupSize, DestroyGroupInitialSize - ( DestroyGroupInitialSize * self.DestroyPercentage / 100 ) } ) - - local DestroyCount = 0 - if DestroyGroup then - if DestroyGroupSize <= DestroyGroupInitialSize - ( DestroyGroupInitialSize * self.DestroyPercentage / 100 ) then - DestroyCount = 1 - end - else - DestroyCount = 1 - end - - self:T( DestroyCount ) - - return DestroyCount -end ---- Task class to destroy radar installations. --- @module DESTROYRADARSTASK - -Include.File("DestroyBaseTask") - ---- The DESTROYRADARS class --- @type -DESTROYRADARSTASK = { - ClassName = "DESTROYRADARSTASK", - GoalVerb = "Destroy Radars" -} - ---- Creates a new DESTROYRADARSTASK. --- @param table{string,...} DestroyGroupNames Table of string containing the group names of which the radars are be destroyed. --- @return DESTROYRADARSTASK -function DESTROYRADARSTASK:New( DestroyGroupNames ) - local self = BASE:Inherit( self, DESTROYGROUPSTASK:New( 'radar installations', 'radars', DestroyGroupNames ) ) - self:F() - - self.Name = 'Destroy Radars' - - _EVENTDISPATCHER:OnDead( self.EventDead , self ) - - return self -end - ---- Report Goal Progress. --- @param Group DestroyGroup Group structure describing the group to be evaluated. --- @param Unit DestroyUnit Unit structure describing the Unit to be evaluated. -function DESTROYRADARSTASK:ReportGoalProgress( DestroyGroup, DestroyUnit ) - self:F( { DestroyGroup, DestroyUnit } ) - - local DestroyCount = 0 - if DestroyUnit and DestroyUnit:hasSensors( Unit.SensorType.RADAR, Unit.RadarType.AS ) then - if DestroyUnit and DestroyUnit:getLife() <= 1.0 then - self:T( 'Destroyed a radar' ) - DestroyCount = 1 - end - end - return DestroyCount -end ---- Set TASK to destroy certain unit types. --- @module DESTROYUNITTYPESTASK - -Include.File("DestroyBaseTask") - ---- The DESTROYUNITTYPESTASK class --- @type -DESTROYUNITTYPESTASK = { - ClassName = "DESTROYUNITTYPESTASK", - GoalVerb = "Destroy", -} - ---- Creates a new DESTROYUNITTYPESTASK. --- @param string DestroyGroupType String describing the group to be destroyed. f.e. "Radar Installations", "Fleet", "Batallion", "Command Centers". --- @param string DestroyUnitType String describing the unit to be destroyed. f.e. "radars", "ships", "tanks", "centers". --- @param table{string,...} DestroyGroupNames Table of string containing the group names of which the radars are be destroyed. --- @param string DestroyUnitTypes Table of string containing the type names of the units to achieve mission success. --- @return DESTROYUNITTYPESTASK -function DESTROYUNITTYPESTASK:New( DestroyGroupType, DestroyUnitType, DestroyGroupNames, DestroyUnitTypes ) - local self = BASE:Inherit( self, DESTROYBASETASK:New( DestroyGroupType, DestroyUnitType, DestroyGroupNames ) ) - self:F( { DestroyGroupType, DestroyUnitType, DestroyGroupNames, DestroyUnitTypes } ) - - if type(DestroyUnitTypes) == 'table' then - self.DestroyUnitTypes = DestroyUnitTypes - else - self.DestroyUnitTypes = { DestroyUnitTypes } - end - - self.Name = 'Destroy Unit Types' - self.GoalVerb = "Destroy " .. DestroyGroupType - - _EVENTDISPATCHER:OnDead( self.EventDead , self ) - - return self -end - ---- Report Goal Progress. --- @param Group DestroyGroup Group structure describing the group to be evaluated. --- @param Unit DestroyUnit Unit structure describing the Unit to be evaluated. -function DESTROYUNITTYPESTASK:ReportGoalProgress( DestroyGroup, DestroyUnit ) - self:F( { DestroyGroup, DestroyUnit } ) - - local DestroyCount = 0 - for UnitTypeID, UnitType in pairs( self.DestroyUnitTypes ) do - if DestroyUnit and DestroyUnit:getTypeName() == UnitType then - if DestroyUnit and DestroyUnit:getLife() <= 1.0 then - DestroyCount = DestroyCount + 1 - end - end - end - return DestroyCount -end ---- A PICKUPTASK orchestrates the loading of CARGO at a specific landing zone. --- @module PICKUPTASK --- @parent TASK - -Include.File("Task") -Include.File("Cargo") - ---- The PICKUPTASK class --- @type -PICKUPTASK = { - ClassName = "PICKUPTASK", - TEXT = { "Pick-Up", "picked-up", "loaded" }, - GoalVerb = "Pick-Up" -} - ---- Creates a new PICKUPTASK. --- @param table{string,...}|string LandingZones Table of Zone names where Cargo is to be loaded. --- @param CARGO_TYPE CargoType Type of the Cargo. The type must be of the following Enumeration:.. --- @param number OnBoardSide Reflects from which side the cargo Group will be on-boarded on the Carrier. -function PICKUPTASK:New( CargoType, OnBoardSide ) - local self = BASE:Inherit( self, TASK:New() ) - self:F() - - -- self holds the inherited instance of the PICKUPTASK Class to the BASE class. - - local Valid = true - - if Valid then - self.Name = 'Pickup Cargo' - self.TaskBriefing = "Task: Fly to the indicated landing zones and pickup " .. CargoType .. ". Your co-pilot will provide you with the directions (required flight angle in degrees) and the distance (in km) to the pickup zone." - self.CargoType = CargoType - self.GoalVerb = CargoType .. " " .. self.GoalVerb - self.OnBoardSide = OnBoardSide - self.IsLandingRequired = true -- required to decide whether the client needs to land or not - self.IsSlingLoad = false -- Indicates whether the cargo is a sling load cargo - self.Stages = { STAGE_CARGO_INIT:New(), STAGE_CARGO_LOAD:New(), STAGEBRIEF:New(), STAGESTART:New(), STAGEROUTE:New(), STAGELANDING:New(), STAGELANDED:New(), STAGELOAD:New(), STAGEDONE:New() } - self.SetStage( self, 1 ) - end - - return self -end - -function PICKUPTASK:FromZone( LandingZone ) - self:F() - - self.LandingZones.LandingZoneNames[LandingZone.CargoZoneName] = LandingZone.CargoZoneName - self.LandingZones.LandingZones[LandingZone.CargoZoneName] = LandingZone - - return self -end - -function PICKUPTASK:InitCargo( InitCargos ) - self:F( { InitCargos } ) - - if type( InitCargos ) == "table" then - self.Cargos.InitCargos = InitCargos - else - self.Cargos.InitCargos = { InitCargos } - end - - return self -end - -function PICKUPTASK:LoadCargo( LoadCargos ) - self:F( { LoadCargos } ) - - if type( LoadCargos ) == "table" then - self.Cargos.LoadCargos = LoadCargos - else - self.Cargos.LoadCargos = { LoadCargos } - end - - return self -end - -function PICKUPTASK:AddCargoMenus( Client, Cargos, TransportRadius ) - self:F() - - for CargoID, Cargo in pairs( Cargos ) do - - self:T( { Cargo.ClassName, Cargo.CargoName, Cargo.CargoType, Cargo:IsStatusNone(), Cargo:IsStatusLoaded(), Cargo:IsStatusLoading(), Cargo:IsStatusUnLoaded() } ) - - -- If the Cargo has no status, allow the menu option. - if Cargo:IsStatusNone() or ( Cargo:IsStatusLoading() and Client == Cargo:IsLoadingToClient() ) then - - local MenuAdd = false - if Cargo:IsNear( Client, self.CurrentCargoZone ) then - MenuAdd = true - end - - if MenuAdd then - if Client._Menus[Cargo.CargoType] == nil then - Client._Menus[Cargo.CargoType] = {} - end - - if not Client._Menus[Cargo.CargoType].PickupMenu then - Client._Menus[Cargo.CargoType].PickupMenu = missionCommands.addSubMenuForGroup( - Client:GetClientGroupID(), - self.TEXT[1] .. " " .. Cargo.CargoType, - nil - ) - self:T( 'Added PickupMenu: ' .. self.TEXT[1] .. " " .. Cargo.CargoType ) - end - - if Client._Menus[Cargo.CargoType].PickupSubMenus == nil then - Client._Menus[Cargo.CargoType].PickupSubMenus = {} - end - - Client._Menus[Cargo.CargoType].PickupSubMenus[ #Client._Menus[Cargo.CargoType].PickupSubMenus + 1 ] = missionCommands.addCommandForGroup( - Client:GetClientGroupID(), - Cargo.CargoName .. " ( " .. Cargo.CargoWeight .. "kg )", - Client._Menus[Cargo.CargoType].PickupMenu, - self.MenuAction, - { ReferenceTask = self, CargoTask = Cargo } - ) - self:T( 'Added PickupSubMenu' .. Cargo.CargoType .. ":" .. Cargo.CargoName .. " ( " .. Cargo.CargoWeight .. "kg )" ) - end - end - end - -end - -function PICKUPTASK:RemoveCargoMenus( Client ) - self:F() - - for MenuID, MenuData in pairs( Client._Menus ) do - for SubMenuID, SubMenuData in pairs( MenuData.PickupSubMenus ) do - missionCommands.removeItemForGroup( Client:GetClientGroupID(), SubMenuData ) - self:T( "Removed PickupSubMenu " ) - SubMenuData = nil - end - if MenuData.PickupMenu then - missionCommands.removeItemForGroup( Client:GetClientGroupID(), MenuData.PickupMenu ) - self:T( "Removed PickupMenu " ) - MenuData.PickupMenu = nil - end - end - - for CargoID, Cargo in pairs( CARGOS ) do - self:T( { Cargo.ClassName, Cargo.CargoName, Cargo.CargoType, Cargo:IsStatusNone(), Cargo:IsStatusLoaded(), Cargo:IsStatusLoading(), Cargo:IsStatusUnLoaded() } ) - if Cargo:IsStatusLoading() and Client == Cargo:IsLoadingToClient() then - Cargo:StatusNone() - end - end - -end - - - -function PICKUPTASK:HasFailed( ClientDead ) - self:F() - - local TaskHasFailed = self.TaskFailed - return TaskHasFailed -end - ---- A DEPLOYTASK orchestrates the deployment of CARGO within a specific landing zone. --- @module DEPLOYTASK - -Include.File( "Task" ) - ---- A DeployTask --- @type DEPLOYTASK -DEPLOYTASK = { - ClassName = "DEPLOYTASK", - TEXT = { "Deploy", "deployed", "unloaded" }, - GoalVerb = "Deployment" -} - - ---- Creates a new DEPLOYTASK object, which models the sequence of STAGEs to unload a cargo. --- @function [parent=#DEPLOYTASK] New --- @param #string CargoType Type of the Cargo. --- @return #DEPLOYTASK The created DeployTask -function DEPLOYTASK:New( CargoType ) - local self = BASE:Inherit( self, TASK:New() ) - self:F() - - local Valid = true - - if Valid then - self.Name = 'Deploy Cargo' - self.TaskBriefing = "Fly to one of the indicated landing zones and deploy " .. CargoType .. ". Your co-pilot will provide you with the directions (required flight angle in degrees) and the distance (in km) to the deployment zone." - self.CargoType = CargoType - self.GoalVerb = CargoType .. " " .. self.GoalVerb - self.Stages = { STAGE_CARGO_INIT:New(), STAGE_CARGO_LOAD:New(), STAGEBRIEF:New(), STAGESTART:New(), STAGEROUTE:New(), STAGELANDING:New(), STAGELANDED:New(), STAGEUNLOAD:New(), STAGEDONE:New() } - self.SetStage( self, 1 ) - end - - return self -end - -function DEPLOYTASK:ToZone( LandingZone ) - self:F() - - self.LandingZones.LandingZoneNames[LandingZone.CargoZoneName] = LandingZone.CargoZoneName - self.LandingZones.LandingZones[LandingZone.CargoZoneName] = LandingZone - - return self -end - - -function DEPLOYTASK:InitCargo( InitCargos ) - self:F( { InitCargos } ) - - if type( InitCargos ) == "table" then - self.Cargos.InitCargos = InitCargos - else - self.Cargos.InitCargos = { InitCargos } - end - - return self -end - - -function DEPLOYTASK:LoadCargo( LoadCargos ) - self:F( { LoadCargos } ) - - if type( LoadCargos ) == "table" then - self.Cargos.LoadCargos = LoadCargos - else - self.Cargos.LoadCargos = { LoadCargos } - end - - return self -end - - ---- When the cargo is unloaded, it will move to the target zone name. --- @param string TargetZoneName Name of the Zone to where the Cargo should move after unloading. -function DEPLOYTASK:SetCargoTargetZoneName( TargetZoneName ) - self:F() - - local Valid = true - - Valid = routines.ValidateString( TargetZoneName, "TargetZoneName", Valid ) - - if Valid then - self.TargetZoneName = TargetZoneName - end - - return Valid - -end - -function DEPLOYTASK:AddCargoMenus( Client, Cargos, TransportRadius ) - self:F() - - local ClientGroupID = Client:GetClientGroupID() - - self:T( ClientGroupID ) - - for CargoID, Cargo in pairs( Cargos ) do - - self:T( { Cargo.ClassName, Cargo.CargoName, Cargo.CargoType, Cargo.CargoWeight } ) - - if Cargo:IsStatusLoaded() and Client == Cargo:IsLoadedInClient() then - - if Client._Menus[Cargo.CargoType] == nil then - Client._Menus[Cargo.CargoType] = {} - end - - if not Client._Menus[Cargo.CargoType].DeployMenu then - Client._Menus[Cargo.CargoType].DeployMenu = missionCommands.addSubMenuForGroup( - ClientGroupID, - self.TEXT[1] .. " " .. Cargo.CargoType, - nil - ) - self:T( 'Added DeployMenu ' .. self.TEXT[1] ) - end - - if Client._Menus[Cargo.CargoType].DeploySubMenus == nil then - Client._Menus[Cargo.CargoType].DeploySubMenus = {} - end - - if Client._Menus[Cargo.CargoType].DeployMenu == nil then - self:T( 'deploymenu is nil' ) - end - - Client._Menus[Cargo.CargoType].DeploySubMenus[ #Client._Menus[Cargo.CargoType].DeploySubMenus + 1 ] = missionCommands.addCommandForGroup( - ClientGroupID, - Cargo.CargoName .. " ( " .. Cargo.CargoWeight .. "kg )", - Client._Menus[Cargo.CargoType].DeployMenu, - self.MenuAction, - { ReferenceTask = self, CargoTask = Cargo } - ) - self:T( 'Added DeploySubMenu ' .. Cargo.CargoType .. ":" .. Cargo.CargoName .. " ( " .. Cargo.CargoWeight .. "kg )" ) - end - end - -end - -function DEPLOYTASK:RemoveCargoMenus( Client ) - self:F() - - local ClientGroupID = Client:GetClientGroupID() - self:T( ClientGroupID ) - - for MenuID, MenuData in pairs( Client._Menus ) do - if MenuData.DeploySubMenus ~= nil then - for SubMenuID, SubMenuData in pairs( MenuData.DeploySubMenus ) do - missionCommands.removeItemForGroup( ClientGroupID, SubMenuData ) - self:T( "Removed DeploySubMenu " ) - SubMenuData = nil - end - end - if MenuData.DeployMenu then - missionCommands.removeItemForGroup( ClientGroupID, MenuData.DeployMenu ) - self:T( "Removed DeployMenu " ) - MenuData.DeployMenu = nil - end - end - -end ---- A NOTASK is a dummy activity... But it will show a Mission Briefing... --- @module NOTASK - -Include.File("Task") - ---- The NOTASK class --- @type -NOTASK = { - ClassName = "NOTASK", -} - ---- Creates a new NOTASK. -function NOTASK:New() - local self = BASE:Inherit( self, TASK:New() ) - self:F() - - local Valid = true - - if Valid then - self.Name = 'Nothing' - self.TaskBriefing = "Task: Execute your mission." - self.Stages = { STAGEBRIEF:New(), STAGESTART:New(), STAGEDONE:New() } - self.SetStage( self, 1 ) - end - - return self -end ---- A ROUTETASK orchestrates the travel to a specific zone defined within the ME. --- @module ROUTETASK - ---- The ROUTETASK class --- @type -ROUTETASK = { - ClassName = "ROUTETASK", - GoalVerb = "Route", -} - ---- Creates a new ROUTETASK. --- @param table{sring,...}|string LandingZones Table of Zone Names where the target is located. --- @param string TaskBriefing (optional) Defines a text describing the briefing of the task. --- @return ROUTETASK -function ROUTETASK:New( LandingZones, TaskBriefing ) - local self = BASE:Inherit( self, TASK:New() ) - self:F( { LandingZones, TaskBriefing } ) - - local Valid = true - - Valid = routines.ValidateZone( LandingZones, "LandingZones", Valid ) - - if Valid then - self.Name = 'Route To Zone' - if TaskBriefing then - self.TaskBriefing = TaskBriefing .. " Your co-pilot will provide you with the directions (required flight angle in degrees) and the distance (in km) to the target objective." - else - self.TaskBriefing = "Task: Fly to specified zone(s). Your co-pilot will provide you with the directions (required flight angle in degrees) and the distance (in km) to the target objective." - end - if type( LandingZones ) == "table" then - self.LandingZones = LandingZones - else - self.LandingZones = { LandingZones } - end - self.Stages = { STAGEBRIEF:New(), STAGESTART:New(), STAGEROUTE:New(), STAGEARRIVE:New(), STAGEDONE:New() } - self.SetStage( self, 1 ) - end - - return self -end - ---- A MISSION is the main owner of a Mission orchestration within MOOSE . The Mission framework orchestrates @{CLIENT}s, @{TASK}s, @{STAGE}s etc. --- A @{CLIENT} needs to be registered within the @{MISSION} through the function @{AddClient}. A @{TASK} needs to be registered within the @{MISSION} through the function @{AddTask}. --- @module Mission - -Include.File( "Routines" ) -Include.File( "Base" ) -Include.File( "Client" ) -Include.File( "Task" ) - ---- The MISSION class --- @type MISSION --- @extends Base#BASE --- @field #MISSION.Clients _Clients --- @field #string MissionBriefing -MISSION = { - ClassName = "MISSION", - Name = "", - MissionStatus = "PENDING", - _Clients = {}, - _Tasks = {}, - _ActiveTasks = {}, - GoalFunction = nil, - MissionReportTrigger = 0, - MissionProgressTrigger = 0, - MissionReportShow = false, - MissionReportFlash = false, - MissionTimeInterval = 0, - MissionCoalition = "", - SUCCESS = 1, - FAILED = 2, - REPEAT = 3, - _GoalTasks = {} -} - ---- @type MISSION.Clients --- @list - -function MISSION:Meta() - - local self = BASE:Inherit( self, BASE:New() ) - self:F() - - return self -end - ---- This is the main MISSION declaration method. Each Mission is like the master or a Mission orchestration between, Clients, Tasks, Stages etc. --- @param string MissionName is the name of the mission. This name will be used to reference the status of each mission by the players. --- @param string MissionPriority is a string indicating the "priority" of the Mission. f.e. "Primary", "Secondary" or "First", "Second". It is free format and up to the Mission designer to choose. There are no rules behind this field. --- @param string MissionBriefing is a string indicating the mission briefing to be shown when a player joins a @{CLIENT}. --- @param string MissionCoalition is a string indicating the coalition or party to which this mission belongs to. It is free format and can be chosen freely by the mission designer. Note that this field is not to be confused with the coalition concept of the ME. Examples of a Mission Coalition could be "NATO", "CCCP", "Intruders", "Terrorists"... --- @return MISSION --- @usage --- -- Declare a few missions. --- local Mission = MISSIONSCHEDULER.AddMission( 'Russia Transport Troops SA-6', 'Operational', 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', 'Russia' ) --- local Mission = MISSIONSCHEDULER.AddMission( 'Patriots', 'Primary', 'Our intelligence reports that 3 Patriot SAM defense batteries are located near Ruisi, Kvarhiti and Gori.', 'Russia' ) --- local Mission = MISSIONSCHEDULER.AddMission( 'Package Delivery', 'Operational', 'In order to be in full control of the situation, we need you to deliver a very important package at a secret location. Fly undetected through the NATO defenses and deliver the secret package. The secret agent is located at waypoint 4.', 'Russia' ) --- local Mission = MISSIONSCHEDULER.AddMission( 'Rescue General', 'Tactical', 'Our intelligence has received a remote signal behind Gori. We believe it is a very important Russian General that was captured by Georgia. Go out there and rescue him! Ensure you stay out of the battle zone, keep south. Waypoint 4 is the location of our Russian General.', 'Russia' ) --- local Mission = MISSIONSCHEDULER.AddMission( 'NATO Transport Troops', 'Operational', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.', 'NATO' ) --- local Mission = MISSIONSCHEDULER.AddMission( 'SA-6 SAMs', 'Primary', 'Our intelligence reports that 3 SA-6 SAM defense batteries are located near Didmukha, Khetagurov and Berula. Eliminate the Russian SAMs.', 'NATO' ) --- local Mission = MISSIONSCHEDULER.AddMission( 'NATO Sling Load', 'Operational', 'Fly to the cargo pickup zone at Dzegvi or Kaspi, and sling the cargo to Soganlug airbase.', 'NATO' ) --- local Mission = MISSIONSCHEDULER.AddMission( 'Rescue secret agent', 'Tactical', 'In order to be in full control of the situation, we need you to rescue a secret agent from the woods behind enemy lines. Avoid the Russian defenses and rescue the agent. Keep south until Khasuri, and keep your eyes open for any SAM presence. The agent is located at waypoint 4 on your kneeboard.', 'NATO' ) -function MISSION:New( MissionName, MissionPriority, MissionBriefing, MissionCoalition ) - - self = MISSION:Meta() - self:T({ MissionName, MissionPriority, MissionBriefing, MissionCoalition }) - - local Valid = true - - Valid = routines.ValidateString( MissionName, "MissionName", Valid ) - Valid = routines.ValidateString( MissionPriority, "MissionPriority", Valid ) - Valid = routines.ValidateString( MissionBriefing, "MissionBriefing", Valid ) - Valid = routines.ValidateString( MissionCoalition, "MissionCoalition", Valid ) - - if Valid then - self.Name = MissionName - self.MissionPriority = MissionPriority - self.MissionBriefing = MissionBriefing - self.MissionCoalition = MissionCoalition - end - - return self -end - ---- Returns if a Mission has completed. --- @return bool -function MISSION:IsCompleted() - self:F() - return self.MissionStatus == "ACCOMPLISHED" -end - ---- Set a Mission to completed. -function MISSION:Completed() - self:F() - self.MissionStatus = "ACCOMPLISHED" - self:StatusToClients() -end - ---- Returns if a Mission is ongoing. --- treturn bool -function MISSION:IsOngoing() - self:F() - return self.MissionStatus == "ONGOING" -end - ---- Set a Mission to ongoing. -function MISSION:Ongoing() - self:F() - self.MissionStatus = "ONGOING" - --self:StatusToClients() -end - ---- Returns if a Mission is pending. --- treturn bool -function MISSION:IsPending() - self:F() - return self.MissionStatus == "PENDING" -end - ---- Set a Mission to pending. -function MISSION:Pending() - self:F() - self.MissionStatus = "PENDING" - self:StatusToClients() -end - ---- Returns if a Mission has failed. --- treturn bool -function MISSION:IsFailed() - self:F() - return self.MissionStatus == "FAILED" -end - ---- Set a Mission to failed. -function MISSION:Failed() - self:F() - self.MissionStatus = "FAILED" - self:StatusToClients() -end - ---- Send the status of the MISSION to all Clients. -function MISSION:StatusToClients() - self:F() - if self.MissionReportFlash then - for ClientID, Client in pairs( self._Clients ) do - Client:Message( self.MissionCoalition .. ' "' .. self.Name .. '": ' .. self.MissionStatus .. '! ( ' .. self.MissionPriority .. ' mission ) ', 10, self.Name .. '/Status', "Mission Command: Mission Status") - end - end -end - ---- Handles the reporting. After certain time intervals, a MISSION report MESSAGE will be shown to All Players. -function MISSION:ReportTrigger() - self:F() - - if self.MissionReportShow == true then - self.MissionReportShow = false - return true - else - if self.MissionReportFlash == true then - if timer.getTime() >= self.MissionReportTrigger then - self.MissionReportTrigger = timer.getTime() + self.MissionTimeInterval - return true - else - return false - end - else - return false - end - end -end - ---- Report the status of all MISSIONs to all active Clients. -function MISSION:ReportToAll() - self:F() - - local AlivePlayers = '' - for ClientID, Client in pairs( self._Clients ) do - if Client:GetDCSGroup() then - if Client:GetClientGroupDCSUnit() then - if Client:GetClientGroupDCSUnit():getLife() > 0.0 then - if AlivePlayers == '' then - AlivePlayers = ' Players: ' .. Client:GetClientGroupDCSUnit():getPlayerName() - else - AlivePlayers = AlivePlayers .. ' / ' .. Client:GetClientGroupDCSUnit():getPlayerName() - end - end - end - end - end - local Tasks = self:GetTasks() - local TaskText = "" - for TaskID, TaskData in pairs( Tasks ) do - TaskText = TaskText .. " - Task " .. TaskID .. ": " .. TaskData.Name .. ": " .. TaskData:GetGoalProgress() .. "\n" - end - MESSAGE:New( self.MissionCoalition .. ' "' .. self.Name .. '": ' .. self.MissionStatus .. ' ( ' .. self.MissionPriority .. ' mission )' .. AlivePlayers .. "\n" .. TaskText:gsub("\n$",""), "Mission Command: Mission Report", 10, self.Name .. '/Status'):ToAll() -end - - ---- Add a goal function to a MISSION. Goal functions are called when a @{TASK} within a mission has been completed. --- @param function GoalFunction is the function defined by the mission designer to evaluate whether a certain goal has been reached after a @{TASK} finishes within the @{MISSION}. A GoalFunction must accept 2 parameters: Mission, Client, which contains the current MISSION object and the current CLIENT object respectively. --- @usage --- PatriotActivation = { --- { "US SAM Patriot Zerti", false }, --- { "US SAM Patriot Zegduleti", false }, --- { "US SAM Patriot Gvleti", false } --- } --- --- function DeployPatriotTroopsGoal( Mission, Client ) --- --- --- -- Check if the cargo is all deployed for mission success. --- for CargoID, CargoData in pairs( Mission._Cargos ) do --- if Group.getByName( CargoData.CargoGroupName ) then --- CargoGroup = Group.getByName( CargoData.CargoGroupName ) --- if CargoGroup then --- -- Check if the cargo is ready to activate --- CurrentLandingZoneID = routines.IsUnitInZones( CargoGroup:getUnits()[1], Mission:GetTask( 2 ).LandingZones ) -- The second task is the Deploytask to measure mission success upon --- if CurrentLandingZoneID then --- if PatriotActivation[CurrentLandingZoneID][2] == false then --- -- Now check if this is a new Mission Task to be completed... --- trigger.action.setGroupAIOn( Group.getByName( PatriotActivation[CurrentLandingZoneID][1] ) ) --- PatriotActivation[CurrentLandingZoneID][2] = true --- MessageToBlue( "Mission Command: Message to all airborne units! The " .. PatriotActivation[CurrentLandingZoneID][1] .. " is armed. Our air defenses are now stronger.", 60, "BLUE/PatriotDefense" ) --- MessageToRed( "Mission Command: Our satellite systems are detecting additional NATO air defenses. To all airborne units: Take care!!!", 60, "RED/PatriotDefense" ) --- Mission:GetTask( 2 ):AddGoalCompletion( "Patriots activated", PatriotActivation[CurrentLandingZoneID][1], 1 ) -- Register Patriot activation as part of mission goal. --- end --- end --- end --- end --- end --- end --- --- local Mission = MISSIONSCHEDULER.AddMission( 'NATO Transport Troops', 'Operational', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.', 'NATO' ) --- Mission:AddGoalFunction( DeployPatriotTroopsGoal ) -function MISSION:AddGoalFunction( GoalFunction ) - self:F() - self.GoalFunction = GoalFunction -end - ---- Register a new @{CLIENT} to participate within the mission. --- @param CLIENT Client is the @{CLIENT} object. The object must have been instantiated with @{CLIENT:New}. --- @return CLIENT --- @usage --- Add a number of Client objects to the Mission. --- Mission:AddClient( CLIENT:FindByName( 'US UH-1H*HOT-Deploy Troops 1', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'US UH-1H*RAMP-Deploy Troops 3', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'US UH-1H*HOT-Deploy Troops 2', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'US UH-1H*RAMP-Deploy Troops 4', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.' ):Transport() ) -function MISSION:AddClient( Client ) - self:F( { Client } ) - - local Valid = true - - if Valid then - self._Clients[Client.ClientName] = Client - end - - return Client -end - ---- Find a @{CLIENT} object within the @{MISSION} by its ClientName. --- @param CLIENT ClientName is a string defining the Client Group as defined within the ME. --- @return CLIENT --- @usage --- -- Seach for Client "Bomber" within the Mission. --- local BomberClient = Mission:FindClient( "Bomber" ) -function MISSION:FindClient( ClientName ) - self:F( { self._Clients[ClientName] } ) - return self._Clients[ClientName] -end - - ---- Register a @{TASK} to be completed within the @{MISSION}. Note that there can be multiple @{TASK}s registered to be completed. Each TASK can be set a certain Goal. The MISSION will not be completed until all Goals are reached. --- @param TASK Task is the @{TASK} object. The object must have been instantiated with @{TASK:New} or any of its inherited @{TASK}s. --- @param number TaskNumber is the sequence number of the TASK within the MISSION. This number does have to be chronological. --- @return TASK --- @usage --- -- Define a few tasks for the Mission. --- PickupZones = { "NATO Gold Pickup Zone", "NATO Titan Pickup Zone" } --- PickupSignalUnits = { "NATO Gold Coordination Center", "NATO Titan Coordination Center" } --- --- -- Assign the Pickup Task --- local PickupTask = PICKUPTASK:New( PickupZones, CARGO_TYPE.ENGINEERS, CLIENT.ONBOARDSIDE.LEFT ) --- PickupTask:AddSmokeBlue( PickupSignalUnits ) --- PickupTask:SetGoalTotal( 3 ) --- Mission:AddTask( PickupTask, 1 ) --- --- -- Assign the Deploy Task --- local PatriotActivationZones = { "US Patriot Battery 1 Activation", "US Patriot Battery 2 Activation", "US Patriot Battery 3 Activation" } --- local PatriotActivationZonesSmokeUnits = { "US SAM Patriot - Battery 1 Control", "US SAM Patriot - Battery 2 Control", "US SAM Patriot - Battery 3 Control" } --- local DeployTask = DEPLOYTASK:New( PatriotActivationZones, CARGO_TYPE.ENGINEERS ) --- --DeployTask:SetCargoTargetZoneName( 'US Troops Attack ' .. math.random(2) ) --- DeployTask:AddSmokeBlue( PatriotActivationZonesSmokeUnits ) --- DeployTask:SetGoalTotal( 3 ) --- DeployTask:SetGoalTotal( 3, "Patriots activated" ) --- Mission:AddTask( DeployTask, 2 ) - -function MISSION:AddTask( Task, TaskNumber ) - self:F() - - self._Tasks[TaskNumber] = Task - self._Tasks[TaskNumber]:EnableEvents() - self._Tasks[TaskNumber].ID = TaskNumber - - return Task - end - ---- Get the TASK idenified by the TaskNumber from the Mission. This function is useful in GoalFunctions. --- @param number TaskNumber is the number of the @{TASK} within the @{MISSION}. --- @return TASK --- @usage --- -- Get Task 2 from the Mission. --- Task2 = Mission:GetTask( 2 ) - -function MISSION:GetTask( TaskNumber ) - self:F() - - local Valid = true - - local Task = nil - - if type(TaskNumber) ~= "number" then - Valid = false - end - - if Valid then - Task = self._Tasks[TaskNumber] - end - - return Task -end - ---- Get all the TASKs from the Mission. This function is useful in GoalFunctions. --- @return {TASK,...} Structure of TASKS with the @{TASK} number as the key. --- @usage --- -- Get Tasks from the Mission. --- Tasks = Mission:GetTasks() --- env.info( "Task 2 Completion = " .. Tasks[2]:GetGoalPercentage() .. "%" ) -function MISSION:GetTasks() - self:F() - - return self._Tasks -end - - ---[[ - _TransportExecuteStage: Defines the different stages of Transport unload/load execution. This table is internal and is used to control the validity of Transport load/unload timing. - - - _TransportExecuteStage.EXECUTING - - _TransportExecuteStage.SUCCESS - - _TransportExecuteStage.FAILED - ---]] -_TransportExecuteStage = { - NONE = 0, - EXECUTING = 1, - SUCCESS = 2, - FAILED = 3 -} - - ---- The MISSIONSCHEDULER is an OBJECT and is the main scheduler of ALL active MISSIONs registered within this scheduler. It's workings are considered internal and is automatically created when the Mission.lua file is included. --- @type MISSIONSCHEDULER --- @field #MISSIONSCHEDULER.MISSIONS Missions -MISSIONSCHEDULER = { - Missions = {}, - MissionCount = 0, - TimeIntervalCount = 0, - TimeIntervalShow = 150, - TimeSeconds = 14400, - TimeShow = 5 -} - ---- @type MISSIONSCHEDULER.MISSIONS --- @list <#MISSION> Mission - ---- This is the main MISSIONSCHEDULER Scheduler function. It is considered internal and is automatically created when the Mission.lua file is included. -function MISSIONSCHEDULER.Scheduler() - - - -- loop through the missions in the TransportTasks - for MissionName, MissionData in pairs( MISSIONSCHEDULER.Missions ) do - - local Mission = MissionData -- #MISSION - - if not Mission:IsCompleted() then - - -- This flag will monitor if for this mission, there are clients alive. If this flag is still false at the end of the loop, the mission status will be set to Pending (if not Failed or Completed). - local ClientsAlive = false - - for ClientID, ClientData in pairs( Mission._Clients ) do - - local Client = ClientData -- Client#CLIENT - - if Client:IsAlive() then - - -- There is at least one Client that is alive... So the Mission status is set to Ongoing. - ClientsAlive = true - - -- If this Client was not registered as Alive before: - -- 1. We register the Client as Alive. - -- 2. We initialize the Client Tasks and make a link to the original Mission Task. - -- 3. We initialize the Cargos. - -- 4. We flag the Mission as Ongoing. - if not Client.ClientAlive then - Client.ClientAlive = true - Client.ClientBriefingShown = false - for TaskNumber, Task in pairs( Mission._Tasks ) do - -- Note that this a deepCopy. Each client must have their own Tasks with own Stages!!! - Client._Tasks[TaskNumber] = routines.utils.deepCopy( Mission._Tasks[TaskNumber] ) - -- Each MissionTask must point to the original Mission. - Client._Tasks[TaskNumber].MissionTask = Mission._Tasks[TaskNumber] - Client._Tasks[TaskNumber].Cargos = Mission._Tasks[TaskNumber].Cargos - Client._Tasks[TaskNumber].LandingZones = Mission._Tasks[TaskNumber].LandingZones - end - - Mission:Ongoing() - end - - - -- For each Client, check for each Task the state and evolve the mission. - -- This flag will indicate if the Task of the Client is Complete. - local TaskComplete = false - - for TaskNumber, Task in pairs( Client._Tasks ) do - - if not Task.Stage then - Task:SetStage( 1 ) - end - - - local TransportTime = timer.getTime() - - if not Task:IsDone() then - - if Task:Goal() then - Task:ShowGoalProgress( Mission, Client ) - end - - --env.info( 'Scheduler: Mission = ' .. Mission.Name .. ' / Client = ' .. Client.ClientName .. ' / Task = ' .. Task.Name .. ' / Stage = ' .. Task.ActiveStage .. ' - ' .. Task.Stage.Name .. ' - ' .. Task.Stage.StageType ) - - -- Action - if Task:StageExecute() then - Task.Stage:Execute( Mission, Client, Task ) - end - - -- Wait until execution is finished - if Task.ExecuteStage == _TransportExecuteStage.EXECUTING then - Task.Stage:Executing( Mission, Client, Task ) - end - - -- Validate completion or reverse to earlier stage - if Task.Time + Task.Stage.WaitTime <= TransportTime then - Task:SetStage( Task.Stage:Validate( Mission, Client, Task ) ) - end - - if Task:IsDone() then - --env.info( 'Scheduler: Mission '.. Mission.Name .. ' Task ' .. Task.Name .. ' Stage ' .. Task.Stage.Name .. ' done. TaskComplete = ' .. string.format ( "%s", TaskComplete and "true" or "false" ) ) - TaskComplete = true -- when a task is not yet completed, a mission cannot be completed - - else - -- break only if this task is not yet done, so that future task are not yet activated. - TaskComplete = false -- when a task is not yet completed, a mission cannot be completed - --env.info( 'Scheduler: Mission "'.. Mission.Name .. '" Task "' .. Task.Name .. '" Stage "' .. Task.Stage.Name .. '" break. TaskComplete = ' .. string.format ( "%s", TaskComplete and "true" or "false" ) ) - break - end - - if TaskComplete then - - if Mission.GoalFunction ~= nil then - Mission.GoalFunction( Mission, Client ) - end - if MISSIONSCHEDULER.Scoring then - MISSIONSCHEDULER.Scoring:_AddMissionTaskScore( Client:GetClientGroupDCSUnit(), Mission.Name, 25 ) - end - --- if not Mission:IsCompleted() then --- end - end - end - end - - local MissionComplete = true - for TaskNumber, Task in pairs( Mission._Tasks ) do - if Task:Goal() then --- Task:ShowGoalProgress( Mission, Client ) - if Task:IsGoalReached() then - else - MissionComplete = false - end - else - MissionComplete = false -- If there is no goal, the mission should never be ended. The goal status will be set somewhere else. - end - end - - if MissionComplete then - Mission:Completed() - if MISSIONSCHEDULER.Scoring then - MISSIONSCHEDULER.Scoring:_AddMissionScore( Mission.Name, 100 ) - end - else - if TaskComplete then - -- Reset for new tasking of active client - Client.ClientAlive = false -- Reset the client tasks. - end - end - - - else - if Client.ClientAlive then - env.info( 'Scheduler: Client "' .. Client.ClientName .. '" is inactive.' ) - Client.ClientAlive = false - - -- This is tricky. If we sanitize Client._Tasks before sanitizing Client._Tasks[TaskNumber].MissionTask, then the original MissionTask will be sanitized, and will be lost within the garbage collector. - -- So first sanitize Client._Tasks[TaskNumber].MissionTask, after that, sanitize only the whole _Tasks structure... - --Client._Tasks[TaskNumber].MissionTask = nil - --Client._Tasks = nil - end - end - end - - -- If all Clients of this Mission are not activated, then the Mission status needs to be put back into Pending status. - -- But only if the Mission was Ongoing. In case the Mission is Completed or Failed, the Mission status may not be changed. In these cases, this will be the last run of this Mission in the Scheduler. - if ClientsAlive == false then - if Mission:IsOngoing() then - -- Mission status back to pending... - Mission:Pending() - end - end - end - - Mission:StatusToClients() - - if Mission:ReportTrigger() then - Mission:ReportToAll() - end - end - - return true -end - ---- Start the MISSIONSCHEDULER. -function MISSIONSCHEDULER.Start() - if MISSIONSCHEDULER ~= nil then - --MISSIONSCHEDULER.SchedulerId = routines.scheduleFunction( MISSIONSCHEDULER.Scheduler, { }, 0, 2 ) - MISSIONSCHEDULER.SchedulerId = SCHEDULER:New( nil, MISSIONSCHEDULER.Scheduler, { }, 0, 2 ) - end -end - ---- Stop the MISSIONSCHEDULER. -function MISSIONSCHEDULER.Stop() - if MISSIONSCHEDULER.SchedulerId then - routines.removeFunction(MISSIONSCHEDULER.SchedulerId) - MISSIONSCHEDULER.SchedulerId = nil - end -end - ---- This is the main MISSION declaration method. Each Mission is like the master or a Mission orchestration between, Clients, Tasks, Stages etc. --- @param Mission is the MISSION object instantiated by @{MISSION:New}. --- @return MISSION --- @usage --- -- Declare a mission. --- Mission = MISSION:New( 'Russia Transport Troops SA-6', --- 'Operational', --- 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', --- 'Russia' ) --- MISSIONSCHEDULER:AddMission( Mission ) -function MISSIONSCHEDULER.AddMission( Mission ) - MISSIONSCHEDULER.Missions[Mission.Name] = Mission - MISSIONSCHEDULER.MissionCount = MISSIONSCHEDULER.MissionCount + 1 - -- Add an overall AI Client for the AI tasks... This AI Client will facilitate the Events in the background for each Task. - --MissionAdd:AddClient( CLIENT:Register( 'AI' ) ) - - return Mission -end - ---- Remove a MISSION from the MISSIONSCHEDULER. --- @param MissionName is the name of the MISSION given at declaration using @{AddMission}. --- @usage --- -- Declare a mission. --- Mission = MISSION:New( 'Russia Transport Troops SA-6', --- 'Operational', --- 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', --- 'Russia' ) --- MISSIONSCHEDULER:AddMission( Mission ) --- --- -- Now remove the Mission. --- MISSIONSCHEDULER:RemoveMission( 'Russia Transport Troops SA-6' ) -function MISSIONSCHEDULER.RemoveMission( MissionName ) - MISSIONSCHEDULER.Missions[MissionName] = nil - MISSIONSCHEDULER.MissionCount = MISSIONSCHEDULER.MissionCount - 1 -end - ---- Find a MISSION within the MISSIONSCHEDULER. --- @param MissionName is the name of the MISSION given at declaration using @{AddMission}. --- @return MISSION --- @usage --- -- Declare a mission. --- Mission = MISSION:New( 'Russia Transport Troops SA-6', --- 'Operational', --- 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', --- 'Russia' ) --- MISSIONSCHEDULER:AddMission( Mission ) --- --- -- Now find the Mission. --- MissionFind = MISSIONSCHEDULER:FindMission( 'Russia Transport Troops SA-6' ) -function MISSIONSCHEDULER.FindMission( MissionName ) - return MISSIONSCHEDULER.Missions[MissionName] -end - --- Internal function used by the MISSIONSCHEDULER menu. -function MISSIONSCHEDULER.ReportMissionsShow( ) - for MissionName, Mission in pairs( MISSIONSCHEDULER.Missions ) do - Mission.MissionReportShow = true - Mission.MissionReportFlash = false - end -end - --- Internal function used by the MISSIONSCHEDULER menu. -function MISSIONSCHEDULER.ReportMissionsFlash( TimeInterval ) - local Count = 0 - for MissionName, Mission in pairs( MISSIONSCHEDULER.Missions ) do - Mission.MissionReportShow = false - Mission.MissionReportFlash = true - Mission.MissionReportTrigger = timer.getTime() + Count * TimeInterval - Mission.MissionTimeInterval = MISSIONSCHEDULER.MissionCount * TimeInterval - env.info( "TimeInterval = " .. Mission.MissionTimeInterval ) - Count = Count + 1 - end -end - --- Internal function used by the MISSIONSCHEDULER menu. -function MISSIONSCHEDULER.ReportMissionsHide( Prm ) - for MissionName, Mission in pairs( MISSIONSCHEDULER.Missions ) do - Mission.MissionReportShow = false - Mission.MissionReportFlash = false - end -end - ---- Enables a MENU option in the communications menu under F10 to control the status of the active missions. --- This function should be called only once when starting the MISSIONSCHEDULER. -function MISSIONSCHEDULER.ReportMenu() - local ReportMenu = SUBMENU:New( 'Status' ) - local ReportMenuShow = COMMANDMENU:New( 'Show Report Missions', ReportMenu, MISSIONSCHEDULER.ReportMissionsShow, 0 ) - local ReportMenuFlash = COMMANDMENU:New('Flash Report Missions', ReportMenu, MISSIONSCHEDULER.ReportMissionsFlash, 120 ) - local ReportMenuHide = COMMANDMENU:New( 'Hide Report Missions', ReportMenu, MISSIONSCHEDULER.ReportMissionsHide, 0 ) -end - ---- Show the remaining mission time. -function MISSIONSCHEDULER:TimeShow() - self.TimeIntervalCount = self.TimeIntervalCount + 1 - if self.TimeIntervalCount >= self.TimeTriggerShow then - local TimeMsg = string.format("%00d", ( self.TimeSeconds / 60 ) - ( timer.getTime() / 60 )) .. ' minutes left until mission reload.' - MESSAGE:New( TimeMsg, "Mission time", self.TimeShow, '/TimeMsg' ):ToAll() - self.TimeIntervalCount = 0 - end -end - -function MISSIONSCHEDULER:Time( TimeSeconds, TimeIntervalShow, TimeShow ) - - self.TimeIntervalCount = 0 - self.TimeSeconds = TimeSeconds - self.TimeIntervalShow = TimeIntervalShow - self.TimeShow = TimeShow -end - ---- Adds a mission scoring to the game. -function MISSIONSCHEDULER:Scoring( Scoring ) - - self.Scoring = Scoring -end - ---- The CLEANUP class keeps an area clean of crashing or colliding airplanes. It also prevents airplanes from firing within this area. --- @module CleanUp --- @author Flightcontrol - -Include.File( "Routines" ) -Include.File( "Base" ) -Include.File( "Mission" ) -Include.File( "Client" ) -Include.File( "Task" ) - ---- The CLEANUP class. --- @type CLEANUP --- @extends Base#BASE -CLEANUP = { - ClassName = "CLEANUP", - ZoneNames = {}, - TimeInterval = 300, - CleanUpList = {}, -} - ---- Creates the main object which is handling the cleaning of the debris within the given Zone Names. --- @param #CLEANUP self --- @param #table ZoneNames Is a table of zone names where the debris should be cleaned. Also a single string can be passed with one zone name. --- @param #number TimeInterval The interval in seconds when the clean activity takes place. The default is 300 seconds, thus every 5 minutes. --- @return #CLEANUP --- @usage --- -- Clean these Zones. --- CleanUpAirports = CLEANUP:New( { 'CLEAN Tbilisi', 'CLEAN Kutaisi' }, 150 ) --- or --- CleanUpTbilisi = CLEANUP:New( 'CLEAN Tbilisi', 150 ) --- CleanUpKutaisi = CLEANUP:New( 'CLEAN Kutaisi', 600 ) -function CLEANUP:New( ZoneNames, TimeInterval ) local self = BASE:Inherit( self, BASE:New() ) - self:F( { ZoneNames, TimeInterval } ) - - if type( ZoneNames ) == 'table' then - self.ZoneNames = ZoneNames - else - self.ZoneNames = { ZoneNames } - end - if TimeInterval then - self.TimeInterval = TimeInterval - end - - _EVENTDISPATCHER:OnBirth( self._OnEventBirth, self ) - - --self.CleanUpScheduler = routines.scheduleFunction( self._CleanUpScheduler, { self }, timer.getTime() + 1, TimeInterval ) - self.CleanUpScheduler = SCHEDULER:New( self, self._CleanUpScheduler, {}, 1, TimeInterval ) - - return self -end - - ---- Destroys a group from the simulator, but checks first if it is still existing! --- @param #CLEANUP self --- @param DCSGroup#Group GroupObject The object to be destroyed. --- @param #string CleanUpGroupName The groupname... -function CLEANUP:_DestroyGroup( GroupObject, CleanUpGroupName ) - self:F( { GroupObject, CleanUpGroupName } ) - - if GroupObject then -- and GroupObject:isExist() then - --MESSAGE:New( "Destroy Group " .. CleanUpGroupName, CleanUpGroupName, 1, CleanUpGroupName ):ToAll() - trigger.action.deactivateGroup(GroupObject) - self:T( { "GroupObject Destroyed", GroupObject } ) - end -end - ---- Destroys a @{DCSUnit#Unit} from the simulator, but checks first if it is still existing! --- @param #CLEANUP self --- @param DCSUnit#Unit CleanUpUnit The object to be destroyed. --- @param #string CleanUpUnitName The Unit name ... -function CLEANUP:_DestroyUnit( CleanUpUnit, CleanUpUnitName ) - self:F( { CleanUpUnit, CleanUpUnitName } ) - - if CleanUpUnit then - --MESSAGE:New( "Destroy " .. CleanUpUnitName, CleanUpUnitName, 1, CleanUpUnitName ):ToAll() - local CleanUpGroup = Unit.getGroup(CleanUpUnit) - -- TODO Client bug in 1.5.3 - if CleanUpGroup and CleanUpGroup:isExist() then - local CleanUpGroupUnits = CleanUpGroup:getUnits() - if #CleanUpGroupUnits == 1 then - local CleanUpGroupName = CleanUpGroup:getName() - --self:CreateEventCrash( timer.getTime(), CleanUpUnit ) - CleanUpGroup:destroy() - self:T( { "Destroyed Group:", CleanUpGroupName } ) - else - CleanUpUnit:destroy() - self:T( { "Destroyed Unit:", CleanUpUnitName } ) - end - self.CleanUpList[CleanUpUnitName] = nil -- Cleaning from the list - CleanUpUnit = nil - end - end -end - --- TODO check DCSTypes#Weapon ---- Destroys a missile from the simulator, but checks first if it is still existing! --- @param #CLEANUP self --- @param DCSTypes#Weapon MissileObject -function CLEANUP:_DestroyMissile( MissileObject ) - self:F( { MissileObject } ) - - if MissileObject and MissileObject:isExist() then - MissileObject:destroy() - self:T( "MissileObject Destroyed") - end -end - -function CLEANUP:_OnEventBirth( Event ) - self:F( { Event } ) - - self.CleanUpList[Event.IniDCSUnitName] = {} - self.CleanUpList[Event.IniDCSUnitName].CleanUpUnit = Event.IniDCSUnit - self.CleanUpList[Event.IniDCSUnitName].CleanUpGroup = Event.IniDCSGroup - self.CleanUpList[Event.IniDCSUnitName].CleanUpGroupName = Event.IniDCSGroupName - self.CleanUpList[Event.IniDCSUnitName].CleanUpUnitName = Event.IniDCSUnitName - - _EVENTDISPATCHER:OnEngineShutDownForUnit( Event.IniDCSUnitName, self._EventAddForCleanUp, self ) - _EVENTDISPATCHER:OnEngineStartUpForUnit( Event.IniDCSUnitName, self._EventAddForCleanUp, self ) - _EVENTDISPATCHER:OnHitForUnit( Event.IniDCSUnitName, self._EventAddForCleanUp, self ) - _EVENTDISPATCHER:OnPilotDeadForUnit( Event.IniDCSUnitName, self._EventCrash, self ) - _EVENTDISPATCHER:OnDeadForUnit( Event.IniDCSUnitName, self._EventCrash, self ) - _EVENTDISPATCHER:OnCrashForUnit( Event.IniDCSUnitName, self._EventCrash, self ) - _EVENTDISPATCHER:OnShotForUnit( Event.IniDCSUnitName, self._EventShot, self ) - - --self:AddEvent( world.event.S_EVENT_ENGINE_SHUTDOWN, self._EventAddForCleanUp ) - --self:AddEvent( world.event.S_EVENT_ENGINE_STARTUP, self._EventAddForCleanUp ) --- self:AddEvent( world.event.S_EVENT_HIT, self._EventAddForCleanUp ) -- , self._EventHitCleanUp ) --- self:AddEvent( world.event.S_EVENT_CRASH, self._EventCrash ) -- , self._EventHitCleanUp ) --- --self:AddEvent( world.event.S_EVENT_DEAD, self._EventCrash ) --- self:AddEvent( world.event.S_EVENT_SHOT, self._EventShot ) --- --- self:EnableEvents() - - -end - ---- Detects if a crash event occurs. --- Crashed units go into a CleanUpList for removal. --- @param #CLEANUP self --- @param DCSTypes#Event event -function CLEANUP:_EventCrash( Event ) - self:F( { Event } ) - - --TODO: This stuff is not working due to a DCS bug. Burning units cannot be destroyed. - --MESSAGE:New( "Crash ", "Crash", 10, "Crash" ):ToAll() - -- self:T("before getGroup") - -- local _grp = Unit.getGroup(event.initiator)-- Identify the group that fired - -- self:T("after getGroup") - -- _grp:destroy() - -- self:T("after deactivateGroup") - -- event.initiator:destroy() - - self.CleanUpList[Event.IniDCSUnitName] = {} - self.CleanUpList[Event.IniDCSUnitName].CleanUpUnit = Event.IniDCSUnit - self.CleanUpList[Event.IniDCSUnitName].CleanUpGroup = Event.IniDCSGroup - self.CleanUpList[Event.IniDCSUnitName].CleanUpGroupName = Event.IniDCSGroupName - self.CleanUpList[Event.IniDCSUnitName].CleanUpUnitName = Event.IniDCSUnitName - -end - ---- Detects if a unit shoots a missile. --- If this occurs within one of the zones, then the weapon used must be destroyed. --- @param #CLEANUP self --- @param DCSTypes#Event event -function CLEANUP:_EventShot( Event ) - self:F( { Event } ) - - -- Test if the missile was fired within one of the CLEANUP.ZoneNames. - local CurrentLandingZoneID = 0 - CurrentLandingZoneID = routines.IsUnitInZones( Event.IniDCSUnit, self.ZoneNames ) - if ( CurrentLandingZoneID ) then - -- Okay, the missile was fired within the CLEANUP.ZoneNames, destroy the fired weapon. - --_SEADmissile:destroy() - --routines.scheduleFunction( CLEANUP._DestroyMissile, { self, Event.Weapon }, timer.getTime() + 0.1) - SCHEDULER:New( self, CLEANUP._DestroyMissile, { Event.Weapon }, 0.1 ) - end -end - - ---- Detects if the Unit has an S_EVENT_HIT within the given ZoneNames. If this is the case, destroy the unit. --- @param #CLEANUP self --- @param DCSTypes#Event event -function CLEANUP:_EventHitCleanUp( Event ) - self:F( { Event } ) - - if Event.IniDCSUnit then - if routines.IsUnitInZones( Event.IniDCSUnit, self.ZoneNames ) ~= nil then - self:T( { "Life: ", Event.IniDCSUnitName, ' = ', Event.IniDCSUnit:getLife(), "/", Event.IniDCSUnit:getLife0() } ) - if Event.IniDCSUnit:getLife() < Event.IniDCSUnit:getLife0() then - self:T( "CleanUp: Destroy: " .. Event.IniDCSUnitName ) - --routines.scheduleFunction( CLEANUP._DestroyUnit, { self, Event.IniDCSUnit }, timer.getTime() + 0.1) - SCHEDULER:New( self, CLEANUP._DestroyUnit, { Event.IniDCSUnit }, 0.1 ) - end - end - end - - if Event.TgtDCSUnit then - if routines.IsUnitInZones( Event.TgtDCSUnit, self.ZoneNames ) ~= nil then - self:T( { "Life: ", Event.TgtDCSUnitName, ' = ', Event.TgtDCSUnit:getLife(), "/", Event.TgtDCSUnit:getLife0() } ) - if Event.TgtDCSUnit:getLife() < Event.TgtDCSUnit:getLife0() then - self:T( "CleanUp: Destroy: " .. Event.TgtDCSUnitName ) - --routines.scheduleFunction( CLEANUP._DestroyUnit, { self, Event.TgtDCSUnit }, timer.getTime() + 0.1 ) - SCHEDULER:New( self, CLEANUP._DestroyUnit, { Event.TgtDCSUnit }, 0.1 ) - end - end - end -end - ---- Add the @{DCSUnit#Unit} to the CleanUpList for CleanUp. -function CLEANUP:_AddForCleanUp( CleanUpUnit, CleanUpUnitName ) - self:F( { CleanUpUnit, CleanUpUnitName } ) - - self.CleanUpList[CleanUpUnitName] = {} - self.CleanUpList[CleanUpUnitName].CleanUpUnit = CleanUpUnit - self.CleanUpList[CleanUpUnitName].CleanUpUnitName = CleanUpUnitName - self.CleanUpList[CleanUpUnitName].CleanUpGroup = Unit.getGroup(CleanUpUnit) - self.CleanUpList[CleanUpUnitName].CleanUpGroupName = Unit.getGroup(CleanUpUnit):getName() - self.CleanUpList[CleanUpUnitName].CleanUpTime = timer.getTime() - self.CleanUpList[CleanUpUnitName].CleanUpMoved = false - - self:T( { "CleanUp: Add to CleanUpList: ", Unit.getGroup(CleanUpUnit):getName(), CleanUpUnitName } ) - -end - ---- Detects if the Unit has an S_EVENT_ENGINE_SHUTDOWN or an S_EVENT_HIT within the given ZoneNames. If this is the case, add the Group to the CLEANUP List. --- @param #CLEANUP self --- @param DCSTypes#Event event -function CLEANUP:_EventAddForCleanUp( Event ) - - if Event.IniDCSUnit then - if self.CleanUpList[Event.IniDCSUnitName] == nil then - if routines.IsUnitInZones( Event.IniDCSUnit, self.ZoneNames ) ~= nil then - self:_AddForCleanUp( Event.IniDCSUnit, Event.IniDCSUnitName ) - end - end - end - - if Event.TgtDCSUnit then - if self.CleanUpList[Event.TgtDCSUnitName] == nil then - if routines.IsUnitInZones( Event.TgtDCSUnit, self.ZoneNames ) ~= nil then - self:_AddForCleanUp( Event.TgtDCSUnit, Event.TgtDCSUnitName ) - end - end - end - -end - -local CleanUpSurfaceTypeText = { - "LAND", - "SHALLOW_WATER", - "WATER", - "ROAD", - "RUNWAY" - } - ---- At the defined time interval, CleanUp the Groups within the CleanUpList. --- @param #CLEANUP self -function CLEANUP:_CleanUpScheduler() - self:F( { "CleanUp Scheduler" } ) - - local CleanUpCount = 0 - for CleanUpUnitName, UnitData in pairs( self.CleanUpList ) do - CleanUpCount = CleanUpCount + 1 - - self:T( { CleanUpUnitName, UnitData } ) - local CleanUpUnit = Unit.getByName(UnitData.CleanUpUnitName) - local CleanUpGroupName = UnitData.CleanUpGroupName - local CleanUpUnitName = UnitData.CleanUpUnitName - if CleanUpUnit then - self:T( { "CleanUp Scheduler", "Checking:", CleanUpUnitName } ) - if _DATABASE:GetStatusGroup( CleanUpGroupName ) ~= "ReSpawn" then - local CleanUpUnitVec3 = CleanUpUnit:getPoint() - --self:T( CleanUpUnitVec3 ) - local CleanUpUnitVec2 = {} - CleanUpUnitVec2.x = CleanUpUnitVec3.x - CleanUpUnitVec2.y = CleanUpUnitVec3.z - --self:T( CleanUpUnitVec2 ) - local CleanUpSurfaceType = land.getSurfaceType(CleanUpUnitVec2) - --self:T( CleanUpSurfaceType ) - --MESSAGE:New( "Surface " .. CleanUpUnitName .. " = " .. CleanUpSurfaceTypeText[CleanUpSurfaceType], CleanUpUnitName, 10, CleanUpUnitName ):ToAll() - - 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 - --MESSAGE:New( "Moved " .. CleanUpUnitName, CleanUpUnitName, 10, CleanUpUnitName ):ToAll() - 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 - ---- Dynamic spawning of groups (and units). --- --- @{#SPAWN} class --- =============== --- The @{#SPAWN} class allows to spawn dynamically new groups, based on pre-defined initialization settings, modifying the behaviour when groups are spawned. --- For each group to be spawned, within the mission editor, a group has to be created with the "late activation flag" set. We call this group the *"Spawn Template"* of the SPAWN object. --- A reference to this Spawn Template needs to be provided when constructing the SPAWN object, by indicating the name of the group within the mission editor in the constructor methods. --- --- Within the SPAWN object, there is an internal index that keeps track of which group from the internal group list was spawned. --- When new groups get spawned by using the SPAWN functions (see below), it will be validated whether the Limits (@{#SPAWN.Limit}) of the SPAWN object are not reached. --- When all is valid, a new group will be created by the spawning methods, and the internal index will be increased with 1. --- --- Regarding the name of new spawned groups, a _SpawnPrefix_ will be assigned for each new group created. --- If you want to have the Spawn Template name to be used as the _SpawnPrefix_ name, use the @{#SPAWN.New} constructor. --- However, when the @{#SPAWN.NewWithAlias} constructor was used, the Alias name will define the _SpawnPrefix_ name. --- Groups will follow the following naming structure when spawned at run-time: --- --- 1. Spawned groups will have the name _SpawnPrefix_#ggg, where ggg is a counter from 0 to 999. --- 2. Spawned units will have the name _SpawnPrefix_#ggg-uu, where uu is a counter from 0 to 99 for each new spawned unit belonging to the group. --- --- Some additional notes that need to be remembered: --- --- * Templates are actually groups defined within the mission editor, with the flag "Late Activation" set. As such, these groups are never used within the mission, but are used by the @{#SPAWN} module. --- * It is important to defined BEFORE you spawn new groups, a proper initialization of the SPAWN instance is done with the options you want to use. --- * When designing a mission, NEVER name groups using a "#" within the name of the group Spawn Template(s), or the SPAWN module logic won't work anymore. --- --- SPAWN construction methods: --- =========================== --- Create a new SPAWN object with the @{#SPAWN.New} or the @{#SPAWN.NewWithAlias} methods: --- --- * @{#SPAWN.New}: Creates a new SPAWN object taking the name of the group that functions as the Template. --- --- It is important to understand how the SPAWN class works internally. The SPAWN object created will contain internally a list of groups that will be spawned and that are already spawned. --- The initialization functions will modify this list of groups so that when a group gets spawned, ALL information is already prepared when spawning. This is done for performance reasons. --- So in principle, the group list will contain all parameters and configurations after initialization, and when groups get actually spawned, this spawning can be done quickly and efficient. --- --- SPAWN initialization methods: --- ============================= --- A spawn object will behave differently based on the usage of initialization methods: --- --- * @{#SPAWN.Limit}: Limits the amount of groups that can be alive at the same time and that can be dynamically spawned. --- * @{#SPAWN.RandomizeRoute}: Randomize the routes of spawned groups. --- * @{#SPAWN.RandomizeTemplate}: Randomize the group templates so that when a new group is spawned, a random group template is selected from one of the templates defined. --- * @{#SPAWN.Uncontrolled}: Spawn plane groups uncontrolled. --- * @{#SPAWN.Array}: Make groups visible before they are actually activated, and order these groups like a batallion in an array. --- * @{#SPAWN.InitRepeat}: Re-spawn groups when they land at the home base. Similar functions are @{#SPAWN.InitRepeatOnLanding} and @{#SPAWN.InitRepeatOnEngineShutDown}. --- --- SPAWN spawning methods: --- ======================= --- Groups can be spawned at different times and methods: --- --- * @{#SPAWN.Spawn}: Spawn one new group based on the last spawned index. --- * @{#SPAWN.ReSpawn}: Re-spawn a group based on a given index. --- * @{#SPAWN.SpawnScheduled}: Spawn groups at scheduled but randomized intervals. You can use @{#SPAWN.SpawnScheduleStart} and @{#SPAWN.SpawnScheduleStop} to start and stop the schedule respectively. --- * @{#SPAWN.SpawnFromUnit}: Spawn a new group taking the position of a @{UNIT}. --- * @{#SPAWN.SpawnInZone}: Spawn a new group in a @{ZONE}. --- --- Note that @{#SPAWN.Spawn} and @{#SPAWN.ReSpawn} return a @{GROUP#GROUP.New} object, that contains a reference to the DCSGroup object. --- You can use the @{GROUP} object to do further actions with the DCSGroup. --- --- SPAWN object cleaning: --- ========================= --- Sometimes, it will occur during a mission run-time, that ground or especially air objects get damaged, and will while being damged stop their activities, while remaining alive. --- In such cases, the SPAWN object will just sit there and wait until that group gets destroyed, but most of the time it won't, --- and it may occur that no new groups are or can be spawned as limits are reached. --- To prevent this, a @{#SPAWN.CleanUp} initialization method has been defined that will silently monitor the status of each spawned group. --- Once a group has a velocity = 0, and has been waiting for a defined interval, that group will be cleaned or removed from run-time. --- There is a catch however :-) If a damaged group has returned to an airbase within the coalition, that group will not be considered as "lost"... --- In such a case, when the inactive group is cleaned, a new group will Re-spawned automatically. --- This models AI that has succesfully returned to their airbase, to restart their combat activities. --- Check the @{#SPAWN.CleanUp} for further info. --- --- ==== --- @module Spawn --- @author FlightControl - -Include.File( "Routines" ) -Include.File( "Base" ) -Include.File( "Database" ) -Include.File( "Group" ) -Include.File( "Zone" ) -Include.File( "Event" ) -Include.File( "Scheduler" ) - ---- SPAWN Class --- @type SPAWN --- @extends Base#BASE --- @field ClassName --- @field #string SpawnTemplatePrefix --- @field #string SpawnAliasPrefix -SPAWN = { - ClassName = "SPAWN", - SpawnTemplatePrefix = nil, - SpawnAliasPrefix = nil, -} - - - ---- Creates the main object to spawn a GROUP defined in the DCS ME. --- @param #SPAWN self --- @param #string SpawnTemplatePrefix is the name of the Group in the ME that defines the Template. Each new group will have the name starting with SpawnTemplatePrefix. --- @return #SPAWN --- @usage --- -- NATO helicopters engaging in the battle field. --- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ) --- @usage local Plane = SPAWN:New( "Plane" ) -- Creates a new local variable that can initiate new planes with the name "Plane#ddd" using the template "Plane" as defined within the ME. -function SPAWN:New( SpawnTemplatePrefix ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( { SpawnTemplatePrefix } ) - - local TemplateGroup = Group.getByName( SpawnTemplatePrefix ) - if TemplateGroup then - self.SpawnTemplatePrefix = SpawnTemplatePrefix - self.SpawnIndex = 0 - self.SpawnCount = 0 -- The internal counter of the amount of spawning the has happened since SpawnStart. - self.AliveUnits = 0 -- Contains the counter how many units are currently alive - self.SpawnIsScheduled = false -- Reflects if the spawning for this SpawnTemplatePrefix is going to be scheduled or not. - self.SpawnTemplate = self._GetTemplate( self, SpawnTemplatePrefix ) -- Contains the template structure for a Group Spawn from the Mission Editor. Note that this group must have lateActivation always on!!! - self.Repeat = false -- Don't repeat the group from Take-Off till Landing and back Take-Off by ReSpawning. - self.UnControlled = false -- When working in UnControlled mode, all planes are Spawned in UnControlled mode before the scheduler starts. - self.SpawnMaxUnitsAlive = 0 -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. - self.SpawnMaxGroups = 0 -- The maximum amount of groups that can be spawned. - self.SpawnRandomize = false -- Sets the randomization flag of new Spawned units to false. - self.SpawnVisible = false -- Flag that indicates if all the Groups of the SpawnGroup need to be visible when Spawned. - - self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. - else - error( "SPAWN:New: There is no group declared in the mission editor with SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" ) - end - - return self -end - ---- Creates a new SPAWN instance to create new groups based on the defined template and using a new alias for each new group. --- @param #SPAWN self --- @param #string SpawnTemplatePrefix is the name of the Group in the ME that defines the Template. --- @param #string SpawnAliasPrefix is the name that will be given to the Group at runtime. --- @return #SPAWN --- @usage --- -- NATO helicopters engaging in the battle field. --- Spawn_BE_KA50 = SPAWN:NewWithAlias( 'BE KA-50@RAMP-Ground Defense', 'Helicopter Attacking a City' ) --- @usage local PlaneWithAlias = SPAWN:NewWithAlias( "Plane", "Bomber" ) -- Creates a new local variable that can instantiate new planes with the name "Bomber#ddd" using the template "Plane" as defined within the ME. -function SPAWN:NewWithAlias( SpawnTemplatePrefix, SpawnAliasPrefix ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( { SpawnTemplatePrefix, SpawnAliasPrefix } ) - - local TemplateGroup = Group.getByName( SpawnTemplatePrefix ) - if TemplateGroup then - self.SpawnTemplatePrefix = SpawnTemplatePrefix - self.SpawnAliasPrefix = SpawnAliasPrefix - self.SpawnIndex = 0 - self.SpawnCount = 0 -- The internal counter of the amount of spawning the has happened since SpawnStart. - self.AliveUnits = 0 -- Contains the counter how many units are currently alive - self.SpawnIsScheduled = false -- Reflects if the spawning for this SpawnTemplatePrefix is going to be scheduled or not. - self.SpawnTemplate = self._GetTemplate( self, SpawnTemplatePrefix ) -- Contains the template structure for a Group Spawn from the Mission Editor. Note that this group must have lateActivation always on!!! - self.Repeat = false -- Don't repeat the group from Take-Off till Landing and back Take-Off by ReSpawning. - self.UnControlled = false -- When working in UnControlled mode, all planes are Spawned in UnControlled mode before the scheduler starts. - self.SpawnMaxUnitsAlive = 0 -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. - self.SpawnMaxGroups = 0 -- The maximum amount of groups that can be spawned. - self.SpawnRandomize = false -- Sets the randomization flag of new Spawned units to false. - self.SpawnVisible = false -- Flag that indicates if all the Groups of the SpawnGroup need to be visible when Spawned. - - self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. - else - error( "SPAWN:New: There is no group declared in the mission editor with SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" ) - end - - return self -end - - ---- Limits the Maximum amount of Units that can be alive at the same time, and the maximum amount of groups that can be spawned. --- Note that this method is exceptionally important to balance the performance of the mission. Depending on the machine etc, a mission can only process a maximum amount of units. --- If the time interval must be short, but there should not be more Units or Groups alive than a maximum amount of units, then this function should be used... --- When a @{#SPAWN.New} is executed and the limit of the amount of units alive is reached, then no new spawn will happen of the group, until some of these units of the spawn object will be destroyed. --- @param #SPAWN self --- @param #number SpawnMaxUnitsAlive The maximum amount of units that can be alive at runtime. --- @param #number SpawnMaxGroups The maximum amount of groups that can be spawned. When the limit is reached, then no more actual spawns will happen of the group. --- This parameter is useful to define a maximum amount of airplanes, ground troops, helicopters, ships etc within a supply area. --- This parameter accepts the value 0, which defines that there are no maximum group limits, but there are limits on the maximum of units that can be alive at the same time. --- @return #SPAWN self --- @usage --- -- NATO helicopters engaging in the battle field. --- -- This helicopter group consists of one Unit. So, this group will SPAWN maximum 2 groups simultaneously within the DCSRTE. --- -- There will be maximum 24 groups spawned during the whole mission lifetime. --- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):Limit( 2, 24 ) -function SPAWN:Limit( SpawnMaxUnitsAlive, SpawnMaxGroups ) - self:F( { self.SpawnTemplatePrefix, SpawnMaxUnitsAlive, SpawnMaxGroups } ) - - self.SpawnMaxUnitsAlive = SpawnMaxUnitsAlive -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. - self.SpawnMaxGroups = SpawnMaxGroups -- The maximum amount of groups that can be spawned. - - for SpawnGroupID = 1, self.SpawnMaxGroups do - self:_InitializeSpawnGroups( SpawnGroupID ) - end - - return self -end - - ---- Randomizes the defined route of the SpawnTemplatePrefix group in the ME. This is very useful to define extra variation of the behaviour of groups. --- @param #SPAWN self --- @param #number SpawnStartPoint is the waypoint where the randomization begins. --- Note that the StartPoint = 0 equaling the point where the group is spawned. --- @param #number SpawnEndPoint is the waypoint where the randomization ends counting backwards. --- This parameter is useful to avoid randomization to end at a waypoint earlier than the last waypoint on the route. --- @param #number SpawnRadius is the radius in meters in which the randomization of the new waypoints, with the original waypoint of the original template located in the middle ... --- @return #SPAWN --- @usage --- -- NATO helicopters engaging in the battle field. --- -- The KA-50 has waypoints Start point ( =0 or SP ), 1, 2, 3, 4, End point (= 5 or DP). --- -- Waypoints 2 and 3 will only be randomized. The others will remain on their original position with each new spawn of the helicopter. --- -- The randomization of waypoint 2 and 3 will take place within a radius of 2000 meters. --- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):RandomizeRoute( 2, 2, 2000 ) -function SPAWN:RandomizeRoute( SpawnStartPoint, SpawnEndPoint, SpawnRadius ) - self:F( { self.SpawnTemplatePrefix, SpawnStartPoint, SpawnEndPoint, SpawnRadius } ) - - self.SpawnRandomizeRoute = true - self.SpawnRandomizeRouteStartPoint = SpawnStartPoint - self.SpawnRandomizeRouteEndPoint = SpawnEndPoint - self.SpawnRandomizeRouteRadius = SpawnRadius - - for GroupID = 1, self.SpawnMaxGroups do - self:_RandomizeRoute( GroupID ) - end - - return self -end - - ---- This function is rather complicated to understand. But I'll try to explain. --- This function becomes useful when you need to spawn groups with random templates of groups defined within the mission editor, --- but they will all follow the same Template route and have the same prefix name. --- In other words, this method randomizes between a defined set of groups the template to be used for each new spawn of a group. --- @param #SPAWN self --- @param #string SpawnTemplatePrefixTable A table with the names of the groups defined within the mission editor, from which one will be choosen when a new group will be spawned. --- @return #SPAWN --- @usage --- -- NATO Tank Platoons invading Gori. --- -- Choose between 13 different 'US Tank Platoon' configurations for each new SPAWN the Group to be spawned for the --- -- 'US Tank Platoon Left', 'US Tank Platoon Middle' and 'US Tank Platoon Right' SpawnTemplatePrefixes. --- -- Each new SPAWN will randomize the route, with a defined time interval of 200 seconds with 40% time variation (randomization) and --- -- with a limit set of maximum 12 Units alive simulteneously and 150 Groups to be spawned during the whole mission. --- Spawn_US_Platoon = { 'US Tank Platoon 1', 'US Tank Platoon 2', 'US Tank Platoon 3', 'US Tank Platoon 4', 'US Tank Platoon 5', --- 'US Tank Platoon 6', 'US Tank Platoon 7', 'US Tank Platoon 8', 'US Tank Platoon 9', 'US Tank Platoon 10', --- 'US Tank Platoon 11', 'US Tank Platoon 12', 'US Tank Platoon 13' } --- Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):Limit( 12, 150 ):Schedule( 200, 0.4 ):RandomizeTemplate( Spawn_US_Platoon ):RandomizeRoute( 3, 3, 2000 ) --- Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):Limit( 12, 150 ):Schedule( 200, 0.4 ):RandomizeTemplate( Spawn_US_Platoon ):RandomizeRoute( 3, 3, 2000 ) --- Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):Limit( 12, 150 ):Schedule( 200, 0.4 ):RandomizeTemplate( Spawn_US_Platoon ):RandomizeRoute( 3, 3, 2000 ) -function SPAWN:RandomizeTemplate( SpawnTemplatePrefixTable ) - self:F( { self.SpawnTemplatePrefix, SpawnTemplatePrefixTable } ) - - self.SpawnTemplatePrefixTable = SpawnTemplatePrefixTable - self.SpawnRandomizeTemplate = true - - for SpawnGroupID = 1, self.SpawnMaxGroups do - self:_RandomizeTemplate( SpawnGroupID ) - end - - return self -end - - - - - ---- For planes and helicopters, when these groups go home and land on their home airbases and farps, they normally would taxi to the parking spot, shut-down their engines and wait forever until the Group is removed by the runtime environment. --- This function is used to re-spawn automatically (so no extra call is needed anymore) the same group after it has landed. --- This will enable a spawned group to be re-spawned after it lands, until it is destroyed... --- Note: When the group is respawned, it will re-spawn from the original airbase where it took off. --- So ensure that the routes for groups that respawn, always return to the original airbase, or players may get confused ... --- @param #SPAWN self --- @return #SPAWN self --- @usage --- -- RU Su-34 - AI Ship Attack --- -- Re-SPAWN the Group(s) after each landing and Engine Shut-Down automatically. --- SpawnRU_SU34 = SPAWN:New( 'TF1 RU Su-34 Krymsk@AI - Attack Ships' ):Schedule( 2, 3, 1800, 0.4 ):SpawnUncontrolled():RandomizeRoute( 1, 1, 3000 ):RepeatOnEngineShutDown() -function SPAWN:InitRepeat() - self:F( { self.SpawnTemplatePrefix, self.SpawnIndex } ) - - self.Repeat = true - self.RepeatOnEngineShutDown = false - self.RepeatOnLanding = true - - return self -end - ---- Respawn group after landing. --- @param #SPAWN self --- @return #SPAWN self -function SPAWN:InitRepeatOnLanding() - self:F( { self.SpawnTemplatePrefix } ) - - self:InitRepeat() - self.RepeatOnEngineShutDown = false - self.RepeatOnLanding = true - - return self -end - - ---- Respawn after landing when its engines have shut down. --- @param #SPAWN self --- @return #SPAWN self -function SPAWN:InitRepeatOnEngineShutDown() - self:F( { self.SpawnTemplatePrefix } ) - - self:InitRepeat() - self.RepeatOnEngineShutDown = true - self.RepeatOnLanding = false - - return self -end - - ---- CleanUp groups when they are still alive, but inactive. --- When groups are still alive and have become inactive due to damage and are unable to contribute anything, then this group will be removed at defined intervals in seconds. --- @param #SPAWN self --- @param #string SpawnCleanUpInterval The interval to check for inactive groups within seconds. --- @return #SPAWN self --- @usage Spawn_Helicopter:CleanUp( 20 ) -- CleanUp the spawning of the helicopters every 20 seconds when they become inactive. -function SPAWN:CleanUp( SpawnCleanUpInterval ) - self:F( { self.SpawnTemplatePrefix, SpawnCleanUpInterval } ) - - self.SpawnCleanUpInterval = SpawnCleanUpInterval - self.SpawnCleanUpTimeStamps = {} - --self.CleanUpFunction = routines.scheduleFunction( self._SpawnCleanUpScheduler, { self }, timer.getTime() + 1, SpawnCleanUpInterval ) - self.CleanUpScheduler = SCHEDULER:New( self, self._SpawnCleanUpScheduler, {}, 1, SpawnCleanUpInterval, 0.2 ) - return self -end - - - ---- Makes the groups visible before start (like a batallion). --- The method will take the position of the group as the first position in the array. --- @param #SPAWN self --- @param #number SpawnAngle The angle in degrees how the groups and each unit of the group will be positioned. --- @param #number SpawnWidth The amount of Groups that will be positioned on the X axis. --- @param #number SpawnDeltaX The space between each Group on the X-axis. --- @param #number SpawnDeltaY The space between each Group on the Y-axis. --- @return #SPAWN self --- @usage --- -- Define an array of Groups. --- Spawn_BE_Ground = SPAWN:New( 'BE Ground' ):Limit( 2, 24 ):Visible( 90, "Diamond", 10, 100, 50 ) -function SPAWN:Array( SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY ) - self:F( { self.SpawnTemplatePrefix, SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY } ) - - self.SpawnVisible = true -- When the first Spawn executes, all the Groups need to be made visible before start. - - local SpawnX = 0 - local SpawnY = 0 - local SpawnXIndex = 0 - local SpawnYIndex = 0 - - for SpawnGroupID = 1, self.SpawnMaxGroups do - self:T( { SpawnX, SpawnY, SpawnXIndex, SpawnYIndex } ) - - self.SpawnGroups[SpawnGroupID].Visible = true - self.SpawnGroups[SpawnGroupID].Spawned = false - - SpawnXIndex = SpawnXIndex + 1 - if SpawnWidth and SpawnWidth ~= 0 then - if SpawnXIndex >= SpawnWidth then - SpawnXIndex = 0 - SpawnYIndex = SpawnYIndex + 1 - end - end - - local SpawnRootX = self.SpawnGroups[SpawnGroupID].SpawnTemplate.x - local SpawnRootY = self.SpawnGroups[SpawnGroupID].SpawnTemplate.y - - self:_TranslateRotate( SpawnGroupID, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle ) - - self.SpawnGroups[SpawnGroupID].SpawnTemplate.lateActivation = true - self.SpawnGroups[SpawnGroupID].SpawnTemplate.visible = true - - self.SpawnGroups[SpawnGroupID].Visible = true - - _EVENTDISPATCHER:OnBirthForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnBirth, self ) - _EVENTDISPATCHER:OnCrashForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnDeadOrCrash, self ) - _EVENTDISPATCHER:OnDeadForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnDeadOrCrash, self ) - - if self.Repeat then - _EVENTDISPATCHER:OnTakeOffForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnTakeOff, self ) - _EVENTDISPATCHER:OnLandForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnLand, self ) - end - if self.RepeatOnEngineShutDown then - _EVENTDISPATCHER:OnEngineShutDownForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnEngineShutDown, self ) - end - - self.SpawnGroups[SpawnGroupID].Group = _DATABASE:Spawn( self.SpawnGroups[SpawnGroupID].SpawnTemplate ) - - SpawnX = SpawnXIndex * SpawnDeltaX - SpawnY = SpawnYIndex * SpawnDeltaY - end - - return self -end - - - ---- Will spawn a group based on the internal index. --- Note: Uses @{DATABASE} module defined in MOOSE. --- @param #SPAWN self --- @return Group#GROUP The group that was spawned. You can use this group for further actions. -function SPAWN:Spawn() - self:F( { self.SpawnTemplatePrefix, self.SpawnIndex } ) - - return self:SpawnWithIndex( self.SpawnIndex + 1 ) -end - ---- Will re-spawn a group based on a given index. --- Note: Uses @{DATABASE} module defined in MOOSE. --- @param #SPAWN self --- @param #string SpawnIndex The index of the group to be spawned. --- @return Group#GROUP The group that was spawned. You can use this group for further actions. -function SPAWN:ReSpawn( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) - - if not SpawnIndex then - SpawnIndex = 1 - end - --- TODO: This logic makes DCS crash and i don't know why (yet). - local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) - if SpawnGroup then - local SpawnDCSGroup = SpawnGroup:GetDCSGroup() - if SpawnDCSGroup then - SpawnGroup:Destroy() - end - end - - return self:SpawnWithIndex( SpawnIndex ) -end - ---- Will spawn a group with a specified index number. --- Uses @{DATABASE} global object defined in MOOSE. --- @param #SPAWN self --- @return Group#GROUP The group that was spawned. You can use this group for further actions. -function SPAWN:SpawnWithIndex( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnMaxGroups } ) - - if self:_GetSpawnIndex( SpawnIndex ) then - - if self.SpawnGroups[self.SpawnIndex].Visible then - self.SpawnGroups[self.SpawnIndex].Group:Activate() - else - self:T( self.SpawnGroups[self.SpawnIndex].SpawnTemplate ) - _EVENTDISPATCHER:OnBirthForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnBirth, self ) - _EVENTDISPATCHER:OnCrashForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnDeadOrCrash, self ) - _EVENTDISPATCHER:OnDeadForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnDeadOrCrash, self ) - - if self.Repeat then - _EVENTDISPATCHER:OnTakeOffForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnTakeOff, self ) - _EVENTDISPATCHER:OnLandForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnLand, self ) - end - if self.RepeatOnEngineShutDown then - _EVENTDISPATCHER:OnEngineShutDownForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnEngineShutDown, self ) - end - - self:T( self.SpawnGroups[self.SpawnIndex].SpawnTemplate ) - - self.SpawnGroups[self.SpawnIndex].Group = _DATABASE:Spawn( self.SpawnGroups[self.SpawnIndex].SpawnTemplate ) - - -- If there is a SpawnFunction hook defined, call it. - if self.SpawnFunctionHook then - self.SpawnFunctionHook( self.SpawnGroups[self.SpawnIndex].Group, unpack( self.SpawnFunctionArguments ) ) - end - -- TODO: Need to fix this by putting an "R" in the name of the group when the group repeats. - --if self.Repeat then - -- _DATABASE:SetStatusGroup( SpawnTemplate.name, "ReSpawn" ) - --end - end - - self.SpawnGroups[self.SpawnIndex].Spawned = true - return self.SpawnGroups[self.SpawnIndex].Group - else - --self:E( { self.SpawnTemplatePrefix, "No more Groups to Spawn:", SpawnIndex, self.SpawnMaxGroups } ) - end - - return nil -end - ---- Spawns new groups at varying time intervals. --- This is useful if you want to have continuity within your missions of certain (AI) groups to be present (alive) within your missions. --- @param #SPAWN self --- @param #number SpawnTime The time interval defined in seconds between each new spawn of new groups. --- @param #number SpawnTimeVariation The variation to be applied on the defined time interval between each new spawn. --- The variation is a number between 0 and 1, representing the %-tage of variation to be applied on the time interval. --- @return #SPAWN self --- @usage --- -- NATO helicopters engaging in the battle field. --- -- The time interval is set to SPAWN new helicopters between each 600 seconds, with a time variation of 50%. --- -- The time variation in this case will be between 450 seconds and 750 seconds. --- -- This is calculated as follows: --- -- Low limit: 600 * ( 1 - 0.5 / 2 ) = 450 --- -- High limit: 600 * ( 1 + 0.5 / 2 ) = 750 --- -- Between these two values, a random amount of seconds will be choosen for each new spawn of the helicopters. --- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):Schedule( 600, 0.5 ) -function SPAWN:SpawnScheduled( SpawnTime, SpawnTimeVariation ) - self:F( { SpawnTime, SpawnTimeVariation } ) - - if SpawnTime ~= nil and SpawnTimeVariation ~= nil then - self.SpawnScheduler = SCHEDULER:New( self, self._Scheduler, {}, 1, SpawnTime, SpawnTimeVariation ) - end - - return self -end - ---- Will re-start the spawning scheduler. --- Note: This function is only required to be called when the schedule was stopped. -function SPAWN:SpawnScheduleStart() - self:F( { self.SpawnTemplatePrefix } ) - - self.SpawnScheduler:Start() -end - ---- Will stop the scheduled spawning scheduler. -function SPAWN:SpawnScheduleStop() - self:F( { self.SpawnTemplatePrefix } ) - - self.SpawnScheduler:Stop() -end - - ---- Allows to place a CallFunction hook when a new group spawns. --- The provided function will be called when a new group is spawned, including its given parameters. --- The first parameter of the SpawnFunction is the @{Group#GROUP} that was spawned. --- @param #SPAWN self --- @param #function SpawnFunctionHook The function to be called when a group spawns. --- @param SpawnFunctionArguments A random amount of arguments to be provided to the function when the group spawns. --- @return #SPAWN -function SPAWN:SpawnFunction( SpawnFunctionHook, ... ) - self:F( SpawnFunction ) - - self.SpawnFunctionHook = SpawnFunctionHook - self.SpawnFunctionArguments = {} - if arg then - self.SpawnFunctionArguments = arg - end - - return self -end - - - - ---- Will spawn a group from a hosting unit. This function is mostly advisable to be used if you want to simulate spawning from air units, like helicopters, which are dropping infantry into a defined Landing Zone. --- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. --- You can use the returned group to further define the route to be followed. --- @param #SPAWN self --- @param Unit#UNIT HostUnit The air or ground unit dropping or unloading the group. --- @param #number OuterRadius The outer radius in meters where the new group will be spawned. --- @param #number InnerRadius The inner radius in meters where the new group will NOT be spawned. --- @param #number SpawnIndex (Optional) The index which group to spawn within the given zone. --- @return Group#GROUP that was spawned. --- @return #nil Nothing was spawned. -function SPAWN:SpawnFromUnit( HostUnit, OuterRadius, InnerRadius, SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, HostUnit, OuterRadius, InnerRadius, SpawnIndex } ) - - if HostUnit and HostUnit:IsAlive() then -- and HostUnit:getUnit(1):inAir() == false then - - if SpawnIndex then - else - SpawnIndex = self.SpawnIndex + 1 - end - - if self:_GetSpawnIndex( SpawnIndex ) then - - local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate - - if SpawnTemplate then - - local UnitPoint = HostUnit:GetPointVec2() - - self:T( { "Current point of ", self.SpawnTemplatePrefix, UnitPoint } ) - - --for PointID, Point in pairs( SpawnTemplate.route.points ) do - --Point.x = UnitPoint.x - --Point.y = UnitPoint.y - --Point.alt = nil - --Point.alt_type = nil - --end - - SpawnTemplate.route.points[1].x = UnitPoint.x - SpawnTemplate.route.points[1].y = UnitPoint.y - - if not InnerRadius then - InnerRadius = 10 - end - - if not OuterRadius then - OuterRadius = 50 - end - - -- Apply SpawnFormation - for UnitID = 1, #SpawnTemplate.units do - if InnerRadius == 0 then - SpawnTemplate.units[UnitID].x = UnitPoint.x - SpawnTemplate.units[UnitID].y = UnitPoint.y - else - local CirclePos = routines.getRandPointInCircle( UnitPoint, OuterRadius, InnerRadius ) - SpawnTemplate.units[UnitID].x = CirclePos.x - SpawnTemplate.units[UnitID].y = CirclePos.y - end - self:T( 'SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) - end - - local SpawnPos = routines.getRandPointInCircle( UnitPoint, OuterRadius, InnerRadius ) - local Point = {} - Point.type = "Turning Point" - Point.x = SpawnPos.x - Point.y = SpawnPos.y - Point.action = "Cone" - Point.speed = 5 - - table.insert( SpawnTemplate.route.points, 2, Point ) - - return self:SpawnWithIndex( self.SpawnIndex ) - end - end - end - - return nil -end - ---- Will spawn a Group within a given @{Zone#ZONE}. --- Once the group is spawned within the zone, it will continue on its route. --- The first waypoint (where the group is spawned) is replaced with the zone coordinates. --- @param #SPAWN self --- @param Zone#ZONE Zone The zone where the group is to be spawned. --- @param #number ZoneRandomize (Optional) Set to true if you want to randomize the starting point in the zone. --- @param #number SpawnIndex (Optional) The index which group to spawn within the given zone. --- @return Group#GROUP that was spawned. --- @return #nil when nothing was spawned. -function SPAWN:SpawnInZone( Zone, ZoneRandomize, SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, Zone, ZoneRandomize, SpawnIndex } ) - - if Zone then - - if SpawnIndex then - else - SpawnIndex = self.SpawnIndex + 1 - end - - if self:_GetSpawnIndex( SpawnIndex ) then - - local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate - - if SpawnTemplate then - - local ZonePoint - - if ZoneRandomize == true then - ZonePoint = Zone:GetRandomPointVec2() - else - ZonePoint = Zone:GetPointVec2() - end - - SpawnTemplate.route.points[1].x = ZonePoint.x - SpawnTemplate.route.points[1].y = ZonePoint.y - - -- Apply SpawnFormation - for UnitID = 1, #SpawnTemplate.units do - local ZonePointUnit = Zone:GetRandomPointVec2() - SpawnTemplate.units[UnitID].x = ZonePointUnit.x - SpawnTemplate.units[UnitID].y = ZonePointUnit.y - self:T( 'SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) - end - - return self:SpawnWithIndex( self.SpawnIndex ) - end - end - end - - return nil -end - - - - ---- Will spawn a plane group in uncontrolled mode... --- This will be similar to the uncontrolled flag setting in the ME. --- @return #SPAWN self -function SPAWN:UnControlled() - self:F( { self.SpawnTemplatePrefix } ) - - self.SpawnUnControlled = true - - for SpawnGroupID = 1, self.SpawnMaxGroups do - self.SpawnGroups[SpawnGroupID].UnControlled = true - end - - return self -end - - - ---- Will return the SpawnGroupName either with with a specific count number or without any count. --- @param #SPAWN self --- @param #number SpawnIndex Is the number of the Group that is to be spawned. --- @return #string SpawnGroupName -function SPAWN:SpawnGroupName( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) - - local SpawnPrefix = self.SpawnTemplatePrefix - if self.SpawnAliasPrefix then - SpawnPrefix = self.SpawnAliasPrefix - end - - if SpawnIndex then - local SpawnName = string.format( '%s#%03d', SpawnPrefix, SpawnIndex ) - self:T( SpawnName ) - return SpawnName - else - self:T( SpawnPrefix ) - return SpawnPrefix - end - -end - ---- Find the first alive group. --- @param #SPAWN self --- @param #number SpawnCursor A number holding the index from where to find the first group from. --- @return Group#GROUP, #number The group found, the new index where the group was found. --- @return #nil, #nil When no group is found, #nil is returned. -function SPAWN:GetFirstAliveGroup( SpawnCursor ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnCursor } ) - - for SpawnIndex = 1, self.SpawnCount do - local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) - if SpawnGroup and SpawnGroup:IsAlive() then - SpawnCursor = SpawnIndex - return SpawnGroup, SpawnCursor - end - end - - return nil, nil -end - - ---- Find the next alive group. --- @param #SPAWN self --- @param #number SpawnCursor A number holding the last found previous index. --- @return Group#GROUP, #number The group found, the new index where the group was found. --- @return #nil, #nil When no group is found, #nil is returned. -function SPAWN:GetNextAliveGroup( SpawnCursor ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnCursor } ) - - SpawnCursor = SpawnCursor + 1 - for SpawnIndex = SpawnCursor, self.SpawnCount do - local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) - if SpawnGroup and SpawnGroup:IsAlive() then - SpawnCursor = SpawnIndex - return SpawnGroup, SpawnCursor - end - end - - return nil, nil -end - ---- Find the last alive group during runtime. -function SPAWN:GetLastAliveGroup() - self:F( { self.SpawnTemplatePrefixself.SpawnAliasPrefix } ) - - self.SpawnIndex = self:_GetLastIndex() - for SpawnIndex = self.SpawnIndex, 1, -1 do - local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) - if SpawnGroup and SpawnGroup:IsAlive() then - self.SpawnIndex = SpawnIndex - return SpawnGroup - end - end - - self.SpawnIndex = nil - return nil -end - - - ---- Get the group from an index. --- Returns the group from the SpawnGroups list. --- If no index is given, it will return the first group in the list. --- @param #SPAWN self --- @param #number SpawnIndex The index of the group to return. --- @return Group#GROUP -function SPAWN:GetGroupFromIndex( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndex } ) - - if not SpawnIndex then - SpawnIndex = 1 - end - - if self.SpawnGroups and self.SpawnGroups[SpawnIndex] then - local SpawnGroup = self.SpawnGroups[SpawnIndex].Group - return SpawnGroup - else - return nil - end -end - ---- Get the group index from a DCSUnit. --- The method will search for a #-mark, and will return the index behind the #-mark of the DCSUnit. --- It will return nil of no prefix was found. --- @param #SPAWN self --- @param DCSUnit The DCS unit to be searched. --- @return #string The prefix --- @return #nil Nothing found -function SPAWN:_GetGroupIndexFromDCSUnit( DCSUnit ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, DCSUnit } ) - - if DCSUnit and DCSUnit:getName() then - local IndexString = string.match( DCSUnit:getName(), "#.*-" ):sub( 2, -2 ) - self:T( IndexString ) - - if IndexString then - local Index = tonumber( IndexString ) - self:T( { "Index:", IndexString, Index } ) - return Index - end - end - - return nil -end - ---- Return the prefix of a DCSUnit. --- The method will search for a #-mark, and will return the text before the #-mark. --- It will return nil of no prefix was found. --- @param #SPAWN self --- @param DCSUnit The DCS unit to be searched. --- @return #string The prefix --- @return #nil Nothing found -function SPAWN:_GetPrefixFromDCSUnit( DCSUnit ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, DCSUnit } ) - - if DCSUnit and DCSUnit:getName() then - local SpawnPrefix = string.match( DCSUnit:getName(), ".*#" ) - if SpawnPrefix then - SpawnPrefix = SpawnPrefix:sub( 1, -2 ) - end - self:T( SpawnPrefix ) - return SpawnPrefix - end - - return nil -end - ---- Return the group within the SpawnGroups collection with input a DCSUnit. -function SPAWN:_GetGroupFromDCSUnit( DCSUnit ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, DCSUnit } ) - - if DCSUnit then - local SpawnPrefix = self:_GetPrefixFromDCSUnit( DCSUnit ) - - if self.SpawnTemplatePrefix == SpawnPrefix or ( self.SpawnAliasPrefix and self.SpawnAliasPrefix == SpawnPrefix ) then - local SpawnGroupIndex = self:_GetGroupIndexFromDCSUnit( DCSUnit ) - local SpawnGroup = self.SpawnGroups[SpawnGroupIndex].Group - self:T( SpawnGroup ) - return SpawnGroup - end - end - - return nil -end - - ---- Get the index from a given group. --- The function will search the name of the group for a #, and will return the number behind the #-mark. -function SPAWN:GetSpawnIndexFromGroup( SpawnGroup ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnGroup } ) - - local IndexString = string.match( SpawnGroup:GetName(), "#.*$" ):sub( 2 ) - local Index = tonumber( IndexString ) - - self:T( IndexString, Index ) - return Index - -end - ---- Return the last maximum index that can be used. -function SPAWN:_GetLastIndex() - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) - - return self.SpawnMaxGroups -end - ---- Initalize the SpawnGroups collection. -function SPAWN:_InitializeSpawnGroups( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndex } ) - - if not self.SpawnGroups[SpawnIndex] then - self.SpawnGroups[SpawnIndex] = {} - self.SpawnGroups[SpawnIndex].Visible = false - self.SpawnGroups[SpawnIndex].Spawned = false - self.SpawnGroups[SpawnIndex].UnControlled = false - self.SpawnGroups[SpawnIndex].SpawnTime = 0 - - self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix = self.SpawnTemplatePrefix - self.SpawnGroups[SpawnIndex].SpawnTemplate = self:_Prepare( self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix, SpawnIndex ) - end - - self:_RandomizeTemplate( SpawnIndex ) - self:_RandomizeRoute( SpawnIndex ) - --self:_TranslateRotate( SpawnIndex ) - - return self.SpawnGroups[SpawnIndex] -end - - - ---- Gets the CategoryID of the Group with the given SpawnPrefix -function SPAWN:_GetGroupCategoryID( SpawnPrefix ) - local TemplateGroup = Group.getByName( SpawnPrefix ) - - if TemplateGroup then - return TemplateGroup:getCategory() - else - return nil - end -end - ---- Gets the CoalitionID of the Group with the given SpawnPrefix -function SPAWN:_GetGroupCoalitionID( SpawnPrefix ) - local TemplateGroup = Group.getByName( SpawnPrefix ) - - if TemplateGroup then - return TemplateGroup:getCoalition() - else - return nil - end -end - ---- Gets the CountryID of the Group with the given SpawnPrefix -function SPAWN:_GetGroupCountryID( SpawnPrefix ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnPrefix } ) - - local TemplateGroup = Group.getByName( SpawnPrefix ) - - if TemplateGroup then - local TemplateUnits = TemplateGroup:getUnits() - return TemplateUnits[1]:getCountry() - else - return nil - end -end - ---- Gets the Group Template from the ME environment definition. --- This method used the @{DATABASE} object, which contains ALL initial and new spawned object in MOOSE. --- @param #SPAWN self --- @param #string SpawnTemplatePrefix --- @return @SPAWN self -function SPAWN:_GetTemplate( SpawnTemplatePrefix ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnTemplatePrefix } ) - - local SpawnTemplate = nil - - SpawnTemplate = routines.utils.deepCopy( _DATABASE.Templates.Groups[SpawnTemplatePrefix].Template ) - - if SpawnTemplate == nil then - error( 'No Template returned for SpawnTemplatePrefix = ' .. SpawnTemplatePrefix ) - end - - SpawnTemplate.SpawnCoalitionID = self:_GetGroupCoalitionID( SpawnTemplatePrefix ) - SpawnTemplate.SpawnCategoryID = self:_GetGroupCategoryID( SpawnTemplatePrefix ) - SpawnTemplate.SpawnCountryID = self:_GetGroupCountryID( SpawnTemplatePrefix ) - - self:T( { SpawnTemplate } ) - return SpawnTemplate -end - ---- Prepares the new Group Template. --- @param #SPAWN self --- @param #string SpawnTemplatePrefix --- @param #number SpawnIndex --- @return #SPAWN self -function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) - - local SpawnTemplate = self:_GetTemplate( SpawnTemplatePrefix ) - SpawnTemplate.name = self:SpawnGroupName( SpawnIndex ) - - SpawnTemplate.groupId = nil - SpawnTemplate.lateActivation = false - - if SpawnTemplate.SpawnCategoryID == Group.Category.GROUND then - self:T( "For ground units, visible needs to be false..." ) - SpawnTemplate.visible = false - end - - if SpawnTemplate.SpawnCategoryID == Group.Category.HELICOPTER or SpawnTemplate.SpawnCategoryID == Group.Category.AIRPLANE then - SpawnTemplate.uncontrolled = false - end - - for UnitID = 1, #SpawnTemplate.units do - SpawnTemplate.units[UnitID].name = string.format( SpawnTemplate.name .. '-%02d', UnitID ) - SpawnTemplate.units[UnitID].unitId = nil - SpawnTemplate.units[UnitID].x = SpawnTemplate.route.points[1].x - SpawnTemplate.units[UnitID].y = SpawnTemplate.route.points[1].y - end - - self:T( { "Template:", SpawnTemplate } ) - return SpawnTemplate - -end - ---- Private method randomizing the routes. --- @param #SPAWN self --- @param #number SpawnIndex The index of the group to be spawned. --- @return #SPAWN -function SPAWN:_RandomizeRoute( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeRoute, self.SpawnRandomizeRouteStartPoint, self.SpawnRandomizeRouteEndPoint, self.SpawnRandomizeRouteRadius } ) - - if self.SpawnRandomizeRoute then - local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate - local RouteCount = #SpawnTemplate.route.points - - for t = self.SpawnRandomizeRouteStartPoint + 1, ( RouteCount - self.SpawnRandomizeRouteEndPoint ) do - SpawnTemplate.route.points[t].x = SpawnTemplate.route.points[t].x + math.random( self.SpawnRandomizeRouteRadius * -1, self.SpawnRandomizeRouteRadius ) - SpawnTemplate.route.points[t].y = SpawnTemplate.route.points[t].y + math.random( self.SpawnRandomizeRouteRadius * -1, self.SpawnRandomizeRouteRadius ) - -- TODO: manage altitude for airborne units ... - SpawnTemplate.route.points[t].alt = nil - --SpawnGroup.route.points[t].alt_type = nil - self:T( 'SpawnTemplate.route.points[' .. t .. '].x = ' .. SpawnTemplate.route.points[t].x .. ', SpawnTemplate.route.points[' .. t .. '].y = ' .. SpawnTemplate.route.points[t].y ) - end - end - - return self -end - ---- Private method that randomizes the template of the group. --- @param #SPAWN self --- @param #number SpawnIndex --- @return #SPAWN self -function SPAWN:_RandomizeTemplate( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) - - if self.SpawnRandomizeTemplate then - self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix = self.SpawnTemplatePrefixTable[ math.random( 1, #self.SpawnTemplatePrefixTable ) ] - self.SpawnGroups[SpawnIndex].SpawnTemplate = self:_Prepare( self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix, SpawnIndex ) - self.SpawnGroups[SpawnIndex].SpawnTemplate.route = routines.utils.deepCopy( self.SpawnTemplate.route ) - self.SpawnGroups[SpawnIndex].SpawnTemplate.x = self.SpawnTemplate.x - self.SpawnGroups[SpawnIndex].SpawnTemplate.y = self.SpawnTemplate.y - self.SpawnGroups[SpawnIndex].SpawnTemplate.start_time = self.SpawnTemplate.start_time - for UnitID = 1, #self.SpawnGroups[SpawnIndex].SpawnTemplate.units do - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].heading = self.SpawnTemplate.units[1].heading - end - end - - self:_RandomizeRoute( SpawnIndex ) - - return self -end - -function SPAWN:_TranslateRotate( SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle } ) - - -- Translate - local TranslatedX = SpawnX - local TranslatedY = SpawnY - - -- Rotate - -- From Wikipedia: https://en.wikipedia.org/wiki/Rotation_matrix#Common_rotations - -- x' = x \cos \theta - y \sin \theta\ - -- y' = x \sin \theta + y \cos \theta\ - local RotatedX = - TranslatedX * math.cos( math.rad( SpawnAngle ) ) - + TranslatedY * math.sin( math.rad( SpawnAngle ) ) - local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) - + TranslatedY * math.cos( math.rad( SpawnAngle ) ) - - -- Assign - self.SpawnGroups[SpawnIndex].SpawnTemplate.x = SpawnRootX - RotatedX - self.SpawnGroups[SpawnIndex].SpawnTemplate.y = SpawnRootY + RotatedY - - - local SpawnUnitCount = table.getn( self.SpawnGroups[SpawnIndex].SpawnTemplate.units ) - for u = 1, SpawnUnitCount do - - -- Translate - local TranslatedX = SpawnX - local TranslatedY = SpawnY - 10 * ( u - 1 ) - - -- Rotate - local RotatedX = - TranslatedX * math.cos( math.rad( SpawnAngle ) ) - + TranslatedY * math.sin( math.rad( SpawnAngle ) ) - local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) - + TranslatedY * math.cos( math.rad( SpawnAngle ) ) - - -- Assign - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].x = SpawnRootX - RotatedX - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].y = SpawnRootY + RotatedY - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].heading = self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].heading + math.rad( SpawnAngle ) - end - - return self -end - ---- Get the next index of the groups to be spawned. This function is complicated, as it is used at several spaces. -function SPAWN:_GetSpawnIndex( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive, self.AliveUnits, #self.SpawnTemplate.units } ) - - - if ( self.SpawnMaxGroups == 0 ) or ( SpawnIndex <= self.SpawnMaxGroups ) then - if ( self.SpawnMaxUnitsAlive == 0 ) or ( self.AliveUnits < self.SpawnMaxUnitsAlive * #self.SpawnTemplate.units ) or self.UnControlled then - if SpawnIndex and SpawnIndex >= self.SpawnCount + 1 then - self.SpawnCount = self.SpawnCount + 1 - SpawnIndex = self.SpawnCount - end - self.SpawnIndex = SpawnIndex - if not self.SpawnGroups[self.SpawnIndex] then - self:_InitializeSpawnGroups( self.SpawnIndex ) - end - else - return nil - end - else - return nil - end - - return self.SpawnIndex -end - - --- TODO Need to delete this... _DATABASE does this now ... -function SPAWN:_OnBirth( event ) - - if timer.getTime0() < timer.getAbsTime() then -- dont need to add units spawned in at the start of the mission if mist is loaded in init line - if event.initiator and event.initiator:getName() then - local EventPrefix = self:_GetPrefixFromDCSUnit( event.initiator ) - if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then - self:T( { "Birth event: " .. event.initiator:getName(), event } ) - --MessageToAll( "Mission command: unit " .. SpawnTemplatePrefix .. " spawned." , 5, EventPrefix .. '/Event') - self.AliveUnits = self.AliveUnits + 1 - self:T( "Alive Units: " .. self.AliveUnits ) - end - end - end - -end - ---- Obscolete --- @todo Need to delete this... _DATABASE does this now ... -function SPAWN:_OnDeadOrCrash( event ) - self:F( self.SpawnTemplatePrefix, event ) - - if event.initiator and event.initiator:getName() then - local EventPrefix = self:_GetPrefixFromDCSUnit( event.initiator ) - if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then - self:T( { "Dead event: " .. event.initiator:getName(), event } ) --- local DestroyedUnit = Unit.getByName( EventPrefix ) --- if DestroyedUnit and DestroyedUnit.getLife() <= 1.0 then - --MessageToAll( "Mission command: unit " .. SpawnTemplatePrefix .. " crashed." , 5, EventPrefix .. '/Event') - self.AliveUnits = self.AliveUnits - 1 - self:T( "Alive Units: " .. self.AliveUnits ) --- end - end - end -end - ---- Will detect AIR Units taking off... When the event takes place, the spawned Group is registered as airborne... --- This is needed to ensure that Re-SPAWNing only is done for landed AIR Groups. --- @todo Need to test for AIR Groups only... -function SPAWN:_OnTakeOff( event ) - self:F( self.SpawnTemplatePrefix, event ) - - if event.initiator and event.initiator:getName() then - local SpawnGroup = self:_GetGroupFromDCSUnit( event.initiator ) - if SpawnGroup then - self:T( { "TakeOff event: " .. event.initiator:getName(), event } ) - self:T( "self.Landed = false" ) - self.Landed = false - end - end -end - ---- Will detect AIR Units landing... When the event takes place, the spawned Group is registered as landed. --- This is needed to ensure that Re-SPAWNing is only done for landed AIR Groups. --- @todo Need to test for AIR Groups only... -function SPAWN:_OnLand( event ) - self:F( self.SpawnTemplatePrefix, event ) - - local SpawnUnit = event.initiator - if SpawnUnit and SpawnUnit:isExist() and Object.getCategory(SpawnUnit) == Object.Category.UNIT then - local SpawnGroup = self:_GetGroupFromDCSUnit( SpawnUnit ) - if SpawnGroup then - self:T( { "Landed event:" .. SpawnUnit:getName(), event } ) - self.Landed = true - self:T( "self.Landed = true" ) - if self.Landed and self.RepeatOnLanding then - local SpawnGroupIndex = self:GetSpawnIndexFromGroup( SpawnGroup ) - self:T( { "Landed:", "ReSpawn:", SpawnGroup:GetName(), SpawnGroupIndex } ) - self:ReSpawn( SpawnGroupIndex ) - end - end - end -end - ---- Will detect AIR Units shutting down their engines ... --- When the event takes place, and the method @{RepeatOnEngineShutDown} was called, the spawned Group will Re-SPAWN. --- But only when the Unit was registered to have landed. --- @param #SPAWN self --- @see _OnTakeOff --- @see _OnLand --- @todo Need to test for AIR Groups only... -function SPAWN:_OnEngineShutDown( event ) - self:F( self.SpawnTemplatePrefix, event ) - - local SpawnUnit = event.initiator - if SpawnUnit and SpawnUnit:isExist() and Object.getCategory(SpawnUnit) == Object.Category.UNIT then - local SpawnGroup = self:_GetGroupFromDCSUnit( SpawnUnit ) - if SpawnGroup then - self:T( { "EngineShutDown event: " .. SpawnUnit:getName(), event } ) - if self.Landed and self.RepeatOnEngineShutDown then - local SpawnGroupIndex = self:GetSpawnIndexFromGroup( SpawnGroup ) - self:T( { "EngineShutDown: ", "ReSpawn:", SpawnGroup:GetName(), SpawnGroupIndex } ) - self:ReSpawn( SpawnGroupIndex ) - end - end - end -end - ---- This function is called automatically by the Spawning scheduler. --- It is the internal worker method SPAWNing new Groups on the defined time intervals. -function SPAWN:_Scheduler() - self:F( { "_Scheduler", self.SpawnTemplatePrefix, self.SpawnAliasPrefix, self.SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive } ) - - -- Validate if there are still groups left in the batch... - self:Spawn() - - return true -end - -function SPAWN:_SpawnCleanUpScheduler() - self:F( { "CleanUp Scheduler:", self.SpawnTemplatePrefix } ) - - local SpawnCursor - local SpawnGroup, SpawnCursor = self:GetFirstAliveGroup( SpawnCursor ) - - self:T( { "CleanUp Scheduler:", SpawnGroup } ) - - while SpawnGroup do - - if SpawnGroup:AllOnGround() and SpawnGroup:GetMaxVelocity() < 1 then - if not self.SpawnCleanUpTimeStamps[SpawnGroup:GetName()] then - self.SpawnCleanUpTimeStamps[SpawnGroup:GetName()] = timer.getTime() - else - if self.SpawnCleanUpTimeStamps[SpawnGroup:GetName()] + self.SpawnCleanUpInterval < timer.getTime() then - self:T( { "CleanUp Scheduler:", "Cleaning:", SpawnGroup } ) - SpawnGroup:Destroy() - end - end - else - self.SpawnCleanUpTimeStamps[SpawnGroup:GetName()] = nil - end - - SpawnGroup, SpawnCursor = self:GetNextAliveGroup( SpawnCursor ) - - self:T( { "CleanUp Scheduler:", SpawnGroup } ) - - end - - return true -- Repeat - -end ---- Limit the simultaneous movement of Groups within a running Mission. --- This module is defined to improve the performance in missions, and to bring additional realism for GROUND vehicles. --- Performance: If in a DCSRTE there are a lot of moving GROUND units, then in a multi player mission, this WILL create lag if --- the main DCS execution core of your CPU is fully utilized. So, this class will limit the amount of simultaneous moving GROUND units --- on defined intervals (currently every minute). --- @module MOVEMENT - -Include.File( "Routines" ) - ---- the MOVEMENT class --- @type -MOVEMENT = { - ClassName = "MOVEMENT", -} - ---- Creates the main object which is handling the GROUND forces movement. --- @param table{string,...}|string MovePrefixes is a table of the Prefixes (names) of the GROUND Groups that need to be controlled by the MOVEMENT Object. --- @param number MoveMaximum is a number that defines the maximum amount of GROUND Units to be moving during one minute. --- @return MOVEMENT --- @usage --- -- Limit the amount of simultaneous moving units on the ground to prevent lag. --- Movement_US_Platoons = MOVEMENT:New( { 'US Tank Platoon Left', 'US Tank Platoon Middle', 'US Tank Platoon Right', 'US CH-47D Troops' }, 15 ) - -function MOVEMENT:New( MovePrefixes, MoveMaximum ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( { MovePrefixes, MoveMaximum } ) - - if type( MovePrefixes ) == 'table' then - self.MovePrefixes = MovePrefixes - else - self.MovePrefixes = { MovePrefixes } - end - self.MoveCount = 0 -- The internal counter of the amount of Moveing the has happened since MoveStart. - self.MoveMaximum = MoveMaximum -- Contains the Maximum amount of units that are allowed to move... - self.AliveUnits = 0 -- Contains the counter how many units are currently alive - self.MoveUnits = {} -- Reflects if the Moving for this MovePrefixes is going to be scheduled or not. - - _EVENTDISPATCHER:OnBirth( self.OnBirth, self ) - --- self:AddEvent( world.event.S_EVENT_BIRTH, self.OnBirth ) --- --- self:EnableEvents() - - self:ScheduleStart() - - return self -end - ---- Call this function to start the MOVEMENT scheduling. -function MOVEMENT:ScheduleStart() - self:F() - --self.MoveFunction = routines.scheduleFunction( self._Scheduler, { self }, timer.getTime() + 1, 120 ) - self.MoveFunction = SCHEDULER:New( self, self._Scheduler, {}, 1, 120 ) -end - ---- Call this function to stop the MOVEMENT scheduling. --- @todo need to implement it ... Forgot. -function MOVEMENT:ScheduleStop() - self:F() - -end - ---- Captures the birth events when new Units were spawned. --- @todo This method should become obsolete. The new @{DATABASE} class will handle the collection administration. -function MOVEMENT:OnBirth( Event ) - self:F( { Event } ) - - if timer.getTime0() < timer.getAbsTime() then -- dont need to add units spawned in at the start of the mission if mist is loaded in init line - if Event.IniDCSUnit then - self:T( "Birth object : " .. Event.IniDCSUnitName ) - if Event.IniDCSGroup and Event.IniDCSGroup:isExist() then - for MovePrefixID, MovePrefix in pairs( self.MovePrefixes ) do - if string.find( Event.IniDCSUnitName, MovePrefix, 1, true ) then - self.AliveUnits = self.AliveUnits + 1 - self.MoveUnits[Event.IniDCSUnitName] = Event.IniDCSGroupName - self:T( self.AliveUnits ) - end - end - end - end - _EVENTDISPATCHER:OnCrashForUnit( Event.IniDCSUnitName, self.OnDeadOrCrash, self ) - _EVENTDISPATCHER:OnDeadForUnit( Event.IniDCSUnitName, self.OnDeadOrCrash, self ) - end - -end - ---- Captures the Dead or Crash events when Units crash or are destroyed. --- @todo This method should become obsolete. The new @{DATABASE} class will handle the collection administration. -function MOVEMENT:OnDeadOrCrash( Event ) - self:F( { Event } ) - - if Event.IniDCSUnit then - self:T( "Dead object : " .. Event.IniDCSUnitName ) - for MovePrefixID, MovePrefix in pairs( self.MovePrefixes ) do - if string.find( Event.IniDCSUnitName, MovePrefix, 1, true ) then - self.AliveUnits = self.AliveUnits - 1 - self.MoveUnits[Event.IniDCSUnitName] = nil - self:T( self.AliveUnits ) - end - end - end -end - ---- This function is called automatically by the MOVEMENT scheduler. A new function is scheduled when MoveScheduled is true. -function MOVEMENT:_Scheduler() - self:F( { self.MovePrefixes, self.MoveMaximum, self.AliveUnits, self.MovementGroups } ) - - if self.AliveUnits > 0 then - local MoveProbability = ( self.MoveMaximum * 100 ) / self.AliveUnits - self:T( 'Move Probability = ' .. MoveProbability ) - - for MovementUnitName, MovementGroupName in pairs( self.MoveUnits ) do - local MovementGroup = Group.getByName( MovementGroupName ) - if MovementGroup and MovementGroup:isExist() then - local MoveOrStop = math.random( 1, 100 ) - self:T( 'MoveOrStop = ' .. MoveOrStop ) - if MoveOrStop <= MoveProbability then - self:T( 'Group continues moving = ' .. MovementGroupName ) - trigger.action.groupContinueMoving( MovementGroup ) - else - self:T( 'Group stops moving = ' .. MovementGroupName ) - trigger.action.groupStopMoving( MovementGroup ) - end - else - self.MoveUnits[MovementUnitName] = nil - end - end - end - return true -end ---- Provides defensive behaviour to a set of SAM sites within a running Mission. --- @module Sead --- @author to be searched on the forum --- @author (co) Flightcontrol (Modified and enriched with functionality) - -Include.File( "Routines" ) -Include.File( "Event" ) -Include.File( "Base" ) -Include.File( "Mission" ) -Include.File( "Client" ) -Include.File( "Task" ) - ---- The SEAD class --- @type SEAD --- @extends Base#BASE -SEAD = { - ClassName = "SEAD", - TargetSkill = { - Average = { Evade = 50, DelayOff = { 10, 25 }, DelayOn = { 10, 30 } } , - Good = { Evade = 30, DelayOff = { 8, 20 }, DelayOn = { 20, 40 } } , - High = { Evade = 15, DelayOff = { 5, 17 }, DelayOn = { 30, 50 } } , - Excellent = { Evade = 10, DelayOff = { 3, 10 }, DelayOn = { 30, 60 } } - }, - SEADGroupPrefixes = {} -} - ---- Creates the main object which is handling defensive actions for SA sites or moving SA vehicles. --- When an anti radiation missile is fired (KH-58, KH-31P, KH-31A, KH-25MPU, HARM missiles), the SA will shut down their radars and will take evasive actions... --- Chances are big that the missile will miss. --- @param table{string,...}|string SEADGroupPrefixes which is a table of Prefixes of the SA Groups in the DCSRTE on which evasive actions need to be taken. --- @return SEAD --- @usage --- -- CCCP SEAD Defenses --- -- Defends the Russian SA installations from SEAD attacks. --- SEAD_RU_SAM_Defenses = SEAD:New( { 'RU SA-6 Kub', 'RU SA-6 Defenses', 'RU MI-26 Troops', 'RU Attack Gori' } ) -function SEAD:New( SEADGroupPrefixes ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( SEADGroupPrefixes ) - if type( SEADGroupPrefixes ) == 'table' then - for SEADGroupPrefixID, SEADGroupPrefix in pairs( SEADGroupPrefixes ) do - self.SEADGroupPrefixes[SEADGroupPrefix] = SEADGroupPrefix - end - else - self.SEADGroupNames[SEADGroupPrefixes] = SEADGroupPrefixes - end - _EVENTDISPATCHER:OnShot( self.EventShot, self ) - - return self -end - ---- Detects if an SA site was shot with an anti radiation missile. In this case, take evasive actions based on the skill level set within the ME. --- @see SEAD -function SEAD:EventShot( Event ) - self:F( { Event } ) - - local SEADUnit = Event.IniDCSUnit - local SEADUnitName = Event.IniDCSUnitName - local SEADWeapon = Event.Weapon -- Identify the weapon fired - local SEADWeaponName = Event.WeaponName -- return weapon type - --trigger.action.outText( string.format("Alerte, depart missile " ..string.format(SEADWeaponName)), 20) --debug message - -- Start of the 2nd loop - self:T( "Missile Launched = " .. SEADWeaponName ) - if SEADWeaponName == "KH-58" or SEADWeaponName == "KH-25MPU" or SEADWeaponName == "AGM-88" or SEADWeaponName == "KH-31A" or SEADWeaponName == "KH-31P" then -- Check if the missile is a SEAD - local _evade = math.random (1,100) -- random number for chance of evading action - local _targetMim = Event.Weapon:getTarget() -- Identify target - local _targetMimname = Unit.getName(_targetMim) - local _targetMimgroup = Unit.getGroup(Weapon.getTarget(SEADWeapon)) - local _targetMimgroupName = _targetMimgroup:getName() - local _targetMimcont= _targetMimgroup:getController() - local _targetskill = _DATABASE.Templates.Units[_targetMimname].Template.skill - self:T( self.SEADGroupPrefixes ) - self:T( _targetMimgroupName ) - local SEADGroupFound = false - for SEADGroupPrefixID, SEADGroupPrefix in pairs( self.SEADGroupPrefixes ) do - if string.find( _targetMimgroupName, SEADGroupPrefix, 1, true ) then - SEADGroupFound = true - self:T( 'Group Found' ) - break - end - end - if SEADGroupFound == true then - if _targetskill == "Random" then -- when skill is random, choose a skill - local Skills = { "Average", "Good", "High", "Excellent" } - _targetskill = Skills[ math.random(1,4) ] - end - self:T( _targetskill ) -- debug message for skill check - if self.TargetSkill[_targetskill] then - if (_evade > self.TargetSkill[_targetskill].Evade) then - self:T( string.format("Evading, target skill " ..string.format(_targetskill)) ) --debug message - local _targetMim = Weapon.getTarget(SEADWeapon) - local _targetMimname = Unit.getName(_targetMim) - local _targetMimgroup = Unit.getGroup(Weapon.getTarget(SEADWeapon)) - local _targetMimcont= _targetMimgroup:getController() - routines.groupRandomDistSelf(_targetMimgroup,300,'Diamond',250,20) -- move randomly - local SuppressedGroups1 = {} -- unit suppressed radar off for a random time - local function SuppressionEnd1(id) - id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) - SuppressedGroups1[id.groupName] = nil - end - local id = { - groupName = _targetMimgroup, - ctrl = _targetMimcont - } - local delay1 = math.random(self.TargetSkill[_targetskill].DelayOff[1], self.TargetSkill[_targetskill].DelayOff[2]) - if SuppressedGroups1[id.groupName] == nil then - SuppressedGroups1[id.groupName] = { - SuppressionEndTime1 = timer.getTime() + delay1, - SuppressionEndN1 = SuppressionEndCounter1 --Store instance of SuppressionEnd() scheduled function - } - Controller.setOption(_targetMimcont, AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) - timer.scheduleFunction(SuppressionEnd1, id, SuppressedGroups1[id.groupName].SuppressionEndTime1) --Schedule the SuppressionEnd() function - --trigger.action.outText( string.format("Radar Off " ..string.format(delay1)), 20) - end - - local SuppressedGroups = {} - local function SuppressionEnd(id) - id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.RED) - SuppressedGroups[id.groupName] = nil - end - local id = { - groupName = _targetMimgroup, - ctrl = _targetMimcont - } - local delay = math.random(self.TargetSkill[_targetskill].DelayOn[1], self.TargetSkill[_targetskill].DelayOn[2]) - if SuppressedGroups[id.groupName] == nil then - SuppressedGroups[id.groupName] = { - SuppressionEndTime = timer.getTime() + delay, - SuppressionEndN = SuppressionEndCounter --Store instance of SuppressionEnd() scheduled function - } - timer.scheduleFunction(SuppressionEnd, id, SuppressedGroups[id.groupName].SuppressionEndTime) --Schedule the SuppressionEnd() function - --trigger.action.outText( string.format("Radar On " ..string.format(delay)), 20) - end - end - end - end - end -end ---- Taking the lead of AI escorting your flight. --- --- @{#ESCORT} class --- ================ --- The @{#ESCORT} class allows you to interact with escorting AI on your flight and take the lead. --- Each escorting group can be commanded with a whole set of radio commands (radio menu in your flight, and then F10). --- --- The radio commands will vary according the category of the group. The richest set of commands are with Helicopters and AirPlanes. --- Ships and Ground troops will have a more limited set, but they can provide support through the bombing of targets designated by the other escorts. --- --- RADIO MENUs that can be created: --- ================================ --- Find a summary below of the current available commands: --- --- Navigation ...: --- --------------- --- Escort group navigation functions: --- --- * **"Join-Up and Follow at x meters":** The escort group fill follow you at about x meters, and they will follow you. --- * **"Flare":** Provides menu commands to let the escort group shoot a flare in the air in a color. --- * **"Smoke":** Provides menu commands to let the escort group smoke the air in a color. Note that smoking is only available for ground and naval troops. --- --- Hold position ...: --- ------------------ --- Escort group navigation functions: --- --- * **"At current location":** Stops the escort group and they will hover 30 meters above the ground at the position they stopped. --- * **"At client location":** Stops the escort group and they will hover 30 meters above the ground at the position they stopped. --- --- Report targets ...: --- ------------------- --- Report targets will make the escort group to report any target that it identifies within a 8km range. Any detected target can be attacked using the 4. Attack nearby targets function. (see below). --- --- * **"Report now":** Will report the current detected targets. --- * **"Report targets on":** Will make the escort group to report detected targets and will fill the "Attack nearby targets" menu list. --- * **"Report targets off":** Will stop detecting targets. --- --- Scan targets ...: --- ----------------- --- Menu items to pop-up the escort group for target scanning. After scanning, the escort group will resume with the mission or defined task. --- --- * **"Scan targets 30 seconds":** Scan 30 seconds for targets. --- * **"Scan targets 60 seconds":** Scan 60 seconds for targets. --- --- Attack targets ...: --- ------------------- --- This menu item will list all detected targets within a 15km range. Depending on the level of detection (known/unknown) and visuality, the targets type will also be listed. --- --- Request assistance from ...: --- ---------------------------- --- This menu item will list all detected targets within a 15km range, as with the menu item **Attack Targets**. --- This menu item allows to request attack support from other escorts supporting the current client group. --- eg. the function allows a player to request support from the Ship escort to attack a target identified by the Plane escort with its Tomahawk missiles. --- eg. the function allows a player to request support from other Planes escorting to bomb the unit with illumination missiles or bombs, so that the main plane escort can attack the area. --- --- ROE ...: --- -------- --- Sets the Rules of Engagement (ROE) of the escort group when in flight. --- --- * **"Hold Fire":** The escort group will hold fire. --- * **"Return Fire":** The escort group will return fire. --- * **"Open Fire":** The escort group will open fire on designated targets. --- * **"Weapon Free":** The escort group will engage with any target. --- --- Evasion ...: --- ------------ --- Will define the evasion techniques that the escort group will perform during flight or combat. --- --- * **"Fight until death":** The escort group will have no reaction to threats. --- * **"Use flares, chaff and jammers":** The escort group will use passive defense using flares and jammers. No evasive manoeuvres are executed. --- * **"Evade enemy fire":** The rescort group will evade enemy fire before firing. --- * **"Go below radar and evade fire":** The escort group will perform evasive vertical manoeuvres. --- --- Resume Mission ...: --- ------------------- --- Escort groups can have their own mission. This menu item will allow the escort group to resume their Mission from a given waypoint. --- Note that this is really fantastic, as you now have the dynamic of taking control of the escort groups, and allowing them to resume their path or mission. --- --- ESCORT construction methods. --- ============================ --- Create a new SPAWN object with the @{#ESCORT.New} method: --- --- * @{#ESCORT.New}: Creates a new ESCORT object from a @{Group#GROUP} for a @{Client#CLIENT}, with an optional briefing text. --- --- ESCORT initialization methods. --- ============================== --- The following menus are created within the RADIO MENU of an active unit hosted by a player: --- --- * @{#ESCORT.MenuFollowAt}: Creates a menu to make the escort follow the client. --- * @{#ESCORT.MenuHoldAtEscortPosition}: Creates a menu to hold the escort at its current position. --- * @{#ESCORT.MenuHoldAtLeaderPosition}: Creates a menu to hold the escort at the client position. --- * @{#ESCORT.MenuScanForTargets}: Creates a menu so that the escort scans targets. --- * @{#ESCORT.MenuFlare}: Creates a menu to disperse flares. --- * @{#ESCORT.MenuSmoke}: Creates a menu to disparse smoke. --- * @{#ESCORT.MenuReportTargets}: Creates a menu so that the escort reports targets. --- * @{#ESCORT.MenuReportPosition}: Creates a menu so that the escort reports its current position from bullseye. --- * @{#ESCORT.MenuAssistedAttack: Creates a menu so that the escort supportes assisted attack from other escorts with the client. --- * @{#ESCORT.MenuROE: Creates a menu structure to set the rules of engagement of the escort. --- * @{#ESCORT.MenuEvasion: Creates a menu structure to set the evasion techniques when the escort is under threat. --- * @{#ESCORT.MenuResumeMission}: Creates a menu structure so that the escort can resume from a waypoint. --- --- @module Escort --- @author FlightControl - -Include.File( "Routines" ) -Include.File( "Base" ) -Include.File( "Database" ) -Include.File( "Group" ) -Include.File( "Zone" ) - ---- --- @type ESCORT --- @extends Base#BASE --- @field Client#CLIENT EscortClient --- @field Group#GROUP EscortGroup --- @field #string EscortName --- @field #ESCORT.MODE EscortMode The mode the escort is in. --- @field #number FollowScheduler The id of the _FollowScheduler function. --- @field #boolean ReportTargets If true, nearby targets are reported. --- @Field DCSTypes#AI.Option.Air.val.ROE OptionROE Which ROE is set to the EscortGroup. --- @field DCSTypes#AI.Option.Air.val.REACTION_ON_THREAT OptionReactionOnThreat Which REACTION_ON_THREAT is set to the EscortGroup. --- @field Menu#MENU_CLIENT EscortMenuResumeMission -ESCORT = { - ClassName = "ESCORT", - EscortName = nil, -- The Escort Name - EscortClient = nil, - EscortGroup = nil, - EscortMode = nil, - 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, - TaskPoints = {} -} - ---- ESCORT.Mode class --- @type ESCORT.MODE --- @field #number FOLLOW --- @field #number MISSION - ---- MENUPARAM type --- @type MENUPARAM --- @field #ESCORT ParamSelf --- @field #Distance ParamDistance --- @field #function ParamFunction --- @field #string ParamMessage - ---- ESCORT class constructor for an AI group --- @param #ESCORT self --- @param Client#CLIENT EscortClient The client escorted by the EscortGroup. --- @param Group#GROUP EscortGroup The group AI escorting the EscortClient. --- @param #string EscortName Name of the escort. --- @return #ESCORT self -function ESCORT:New( EscortClient, EscortGroup, EscortName, EscortBriefing ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( { EscortClient, EscortGroup, EscortName } ) - - self.EscortClient = EscortClient -- Client#CLIENT - self.EscortGroup = EscortGroup -- Group#GROUP - self.EscortName = EscortName - self.EscortBriefing = EscortBriefing - - self:T( EscortGroup:GetClassNameAndID() ) - - -- Set EscortGroup known at EscortClient. - if not self.EscortClient._EscortGroups then - self.EscortClient._EscortGroups = {} - end - - if not self.EscortClient._EscortGroups[EscortGroup:GetName()] then - self.EscortClient._EscortGroups[EscortGroup:GetName()] = {} - self.EscortClient._EscortGroups[EscortGroup:GetName()].EscortGroup = self.EscortGroup - self.EscortClient._EscortGroups[EscortGroup:GetName()].EscortName = self.EscortName - self.EscortClient._EscortGroups[EscortGroup:GetName()].Targets = {} - self.EscortMode = ESCORT.MODE.FOLLOW - end - - - self.EscortMenu = MENU_CLIENT:New( self.EscortClient, self.EscortName ) - - self.EscortGroup:WayPointInitialize(1) - - self.EscortGroup:OptionROTVertical() - self.EscortGroup:OptionROEOpenFire() - - EscortGroup:MessageToClient( EscortGroup:GetCategoryName() .. " '" .. EscortName .. "' (" .. EscortGroup:GetCallsign() .. ") reporting! " .. - "We're escorting your flight. " .. - "Use the Radio Menu and F10 and use the options under + " .. EscortName .. "\n", - 60, EscortClient - ) - - return self -end - - ---- Defines the default menus --- @param #ESCORT self --- @return #ESCORT -function ESCORT:Menus() - self:F() - - self:MenuFollowAt( 100 ) - self:MenuFollowAt( 200 ) - self:MenuFollowAt( 300 ) - self:MenuFollowAt( 400 ) - - self:MenuScanForTargets( 100, 60 ) - - self:MenuHoldAtEscortPosition( 30 ) - self:MenuHoldAtLeaderPosition( 30 ) - - self:MenuFlare() - self:MenuSmoke() - - self:MenuReportTargets( 60 ) - self:MenuAssistedAttack() - self:MenuROE() - self:MenuEvasion() - self:MenuResumeMission() - - return self -end - - - ---- Defines a menu slot to let the escort Join and Follow you at a certain distance. --- This menu will appear under **Navigation**. --- @param #ESCORT self --- @param DCSTypes#Distance Distance The distance in meters that the escort needs to follow the client. --- @return #ESCORT -function ESCORT:MenuFollowAt( Distance ) - self:F(Distance) - - if self.EscortGroup:IsAir() then - if not self.EscortMenuReportNavigation then - self.EscortMenuReportNavigation = MENU_CLIENT:New( self.EscortClient, "Navigation", self.EscortMenu ) - end - - if not self.EscortMenuJoinUpAndFollow then - self.EscortMenuJoinUpAndFollow = {} - end - - self.EscortMenuJoinUpAndFollow[#self.EscortMenuJoinUpAndFollow+1] = MENU_CLIENT_COMMAND:New( self.EscortClient, "Join-Up and Follow at " .. Distance, self.EscortMenuReportNavigation, ESCORT._JoinUpAndFollow, { ParamSelf = self, ParamDistance = Distance } ) - - self.EscortMode = ESCORT.MODE.FOLLOW - end - - return self -end - ---- Defines a menu slot to let the escort hold at their current position and stay low with a specified height during a specified time in seconds. --- This menu will appear under **Hold position**. --- @param #ESCORT self --- @param DCSTypes#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. --- @param DCSTypes#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. --- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. --- @return #ESCORT --- TODO: Implement Seconds parameter. Challenge is to first develop the "continue from last activity" function. -function ESCORT:MenuHoldAtEscortPosition( Height, Seconds, MenuTextFormat ) - self:F( { Height, Seconds, MenuTextFormat } ) - - if self.EscortGroup:IsAir() then - - if not self.EscortMenuHold then - self.EscortMenuHold = MENU_CLIENT:New( self.EscortClient, "Hold position", self.EscortMenu ) - end - - if not Height then - Height = 30 - end - - if not Seconds then - Seconds = 0 - end - - local MenuText = "" - if not MenuTextFormat then - if Seconds == 0 then - MenuText = string.format( "Hold at %d meter", Height ) - else - MenuText = string.format( "Hold at %d meter for %d seconds", Height, Seconds ) - end - else - if Seconds == 0 then - MenuText = string.format( MenuTextFormat, Height ) - else - MenuText = string.format( MenuTextFormat, Height, Seconds ) - end - end - - if not self.EscortMenuHoldPosition then - self.EscortMenuHoldPosition = {} - end - - self.EscortMenuHoldPosition[#self.EscortMenuHoldPosition+1] = MENU_CLIENT_COMMAND - :New( - self.EscortClient, - MenuText, - self.EscortMenuHold, - ESCORT._HoldPosition, - { ParamSelf = self, - ParamOrbitGroup = self.EscortGroup, - ParamHeight = Height, - ParamSeconds = Seconds - } - ) - end - - return self -end - - ---- Defines a menu slot to let the escort hold at the client position and stay low with a specified height during a specified time in seconds. --- This menu will appear under **Navigation**. --- @param #ESCORT self --- @param DCSTypes#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. --- @param DCSTypes#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. --- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain one or two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. --- @return #ESCORT --- TODO: Implement Seconds parameter. Challenge is to first develop the "continue from last activity" function. -function ESCORT:MenuHoldAtLeaderPosition( Height, Seconds, MenuTextFormat ) - self:F( { Height, Seconds, MenuTextFormat } ) - - if self.EscortGroup:IsAir() then - - if not self.EscortMenuHold then - self.EscortMenuHold = MENU_CLIENT:New( self.EscortClient, "Hold position", self.EscortMenu ) - end - - if not Height then - Height = 30 - end - - if not Seconds then - Seconds = 0 - end - - local MenuText = "" - if not MenuTextFormat then - if Seconds == 0 then - MenuText = string.format( "Rejoin and hold at %d meter", Height ) - else - MenuText = string.format( "Rejoin and hold at %d meter for %d seconds", Height, Seconds ) - end - else - if Seconds == 0 then - MenuText = string.format( MenuTextFormat, Height ) - else - MenuText = string.format( MenuTextFormat, Height, Seconds ) - end - end - - if not self.EscortMenuHoldAtLeaderPosition then - self.EscortMenuHoldAtLeaderPosition = {} - end - - self.EscortMenuHoldAtLeaderPosition[#self.EscortMenuHoldAtLeaderPosition+1] = MENU_CLIENT_COMMAND - :New( - self.EscortClient, - MenuText, - self.EscortMenuHold, - ESCORT._HoldPosition, - { ParamSelf = self, - ParamOrbitGroup = self.EscortClient, - ParamHeight = Height, - ParamSeconds = Seconds - } - ) - end - - return self -end - ---- Defines a menu slot to let the escort scan for targets at a certain height for a certain time in seconds. --- This menu will appear under **Scan targets**. --- @param #ESCORT self --- @param DCSTypes#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. --- @param DCSTypes#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. --- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain one or two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. --- @return #ESCORT -function ESCORT:MenuScanForTargets( Height, Seconds, MenuTextFormat ) - self:F( { Height, Seconds, MenuTextFormat } ) - - if self.EscortGroup:IsAir() then - if not self.EscortMenuScan then - self.EscortMenuScan = MENU_CLIENT:New( self.EscortClient, "Scan for targets", self.EscortMenu ) - end - - if not Height then - Height = 100 - end - - if not Seconds then - Seconds = 30 - end - - local MenuText = "" - if not MenuTextFormat then - if Seconds == 0 then - MenuText = string.format( "At %d meter", Height ) - else - MenuText = string.format( "At %d meter for %d seconds", Height, Seconds ) - end - else - if Seconds == 0 then - MenuText = string.format( MenuTextFormat, Height ) - else - MenuText = string.format( MenuTextFormat, Height, Seconds ) - end - end - - if not self.EscortMenuScanForTargets then - self.EscortMenuScanForTargets = {} - end - - self.EscortMenuScanForTargets[#self.EscortMenuScanForTargets+1] = MENU_CLIENT_COMMAND - :New( - self.EscortClient, - MenuText, - self.EscortMenuScan, - ESCORT._ScanTargets, - { ParamSelf = self, - ParamScanDuration = 30 - } - ) - end - - return self -end - - - ---- Defines a menu slot to let the escort disperse a flare in a certain color. --- This menu will appear under **Navigation**. --- The flare will be fired from the first unit in the group. --- @param #ESCORT self --- @param #string MenuTextFormat Optional parameter that shows the menu option text. If no text is given, the default text will be displayed. --- @return #ESCORT -function ESCORT:MenuFlare( MenuTextFormat ) - self:F() - - if not self.EscortMenuReportNavigation then - self.EscortMenuReportNavigation = MENU_CLIENT:New( self.EscortClient, "Navigation", self.EscortMenu ) - end - - local MenuText = "" - if not MenuTextFormat then - MenuText = "Flare" - else - MenuText = MenuTextFormat - end - - if not self.EscortMenuFlare then - self.EscortMenuFlare = MENU_CLIENT:New( self.EscortClient, MenuText, self.EscortMenuReportNavigation, ESCORT._Flare, { ParamSelf = self } ) - self.EscortMenuFlareGreen = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release green flare", self.EscortMenuFlare, ESCORT._Flare, { ParamSelf = self, ParamColor = UNIT.FlareColor.Green, ParamMessage = "Released a green flare!" } ) - self.EscortMenuFlareRed = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release red flare", self.EscortMenuFlare, ESCORT._Flare, { ParamSelf = self, ParamColor = UNIT.FlareColor.Red, ParamMessage = "Released a red flare!" } ) - self.EscortMenuFlareWhite = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release white flare", self.EscortMenuFlare, ESCORT._Flare, { ParamSelf = self, ParamColor = UNIT.FlareColor.White, ParamMessage = "Released a white flare!" } ) - self.EscortMenuFlareYellow = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release yellow flare", self.EscortMenuFlare, ESCORT._Flare, { ParamSelf = self, ParamColor = UNIT.FlareColor.Yellow, ParamMessage = "Released a yellow flare!" } ) - end - - return self -end - ---- Defines a menu slot to let the escort disperse a smoke in a certain color. --- This menu will appear under **Navigation**. --- Note that smoke menu options will only be displayed for ships and ground units. Not for air units. --- The smoke will be fired from the first unit in the group. --- @param #ESCORT self --- @param #string MenuTextFormat Optional parameter that shows the menu option text. If no text is given, the default text will be displayed. --- @return #ESCORT -function ESCORT:MenuSmoke( MenuTextFormat ) - self:F() - - if not self.EscortGroup:IsAir() then - if not self.EscortMenuReportNavigation then - self.EscortMenuReportNavigation = MENU_CLIENT:New( self.EscortClient, "Navigation", self.EscortMenu ) - end - - local MenuText = "" - if not MenuTextFormat then - MenuText = "Smoke" - else - MenuText = MenuTextFormat - end - - if not self.EscortMenuSmoke then - self.EscortMenuSmoke = MENU_CLIENT:New( self.EscortClient, "Smoke", self.EscortMenuReportNavigation, ESCORT._Smoke, { ParamSelf = self } ) - self.EscortMenuSmokeGreen = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release green smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.Green, ParamMessage = "Releasing green smoke!" } ) - self.EscortMenuSmokeRed = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release red smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.Red, ParamMessage = "Releasing red smoke!" } ) - self.EscortMenuSmokeWhite = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release white smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.White, ParamMessage = "Releasing white smoke!" } ) - self.EscortMenuSmokeOrange = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release orange smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.Orange, ParamMessage = "Releasing orange smoke!" } ) - self.EscortMenuSmokeBlue = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release blue smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.Blue, ParamMessage = "Releasing blue smoke!" } ) - end - end - - return self -end - ---- Defines a menu slot to let the escort report their current detected targets with a specified time interval in seconds. --- This menu will appear under **Report targets**. --- Note that if a report targets menu is not specified, no targets will be detected by the escort, and the attack and assisted attack menus will not be displayed. --- @param #ESCORT self --- @param DCSTypes#Time Seconds Optional parameter that lets the escort report their current detected targets after specified time interval in seconds. The default time is 30 seconds. --- @return #ESCORT -function ESCORT:MenuReportTargets( Seconds ) - self:F( { Seconds } ) - - if not self.EscortMenuReportNearbyTargets then - self.EscortMenuReportNearbyTargets = MENU_CLIENT:New( self.EscortClient, "Report targets", self.EscortMenu ) - end - - if not Seconds then - Seconds = 30 - end - - -- Report Targets - self.EscortMenuReportNearbyTargetsNow = MENU_CLIENT_COMMAND:New( self.EscortClient, "Report targets now!", self.EscortMenuReportNearbyTargets, ESCORT._ReportNearbyTargetsNow, { ParamSelf = self } ) - self.EscortMenuReportNearbyTargetsOn = MENU_CLIENT_COMMAND:New( self.EscortClient, "Report targets on", self.EscortMenuReportNearbyTargets, ESCORT._SwitchReportNearbyTargets, { ParamSelf = self, ParamReportTargets = true } ) - self.EscortMenuReportNearbyTargetsOff = MENU_CLIENT_COMMAND:New( self.EscortClient, "Report targets off", self.EscortMenuReportNearbyTargets, ESCORT._SwitchReportNearbyTargets, { ParamSelf = self, ParamReportTargets = false, } ) - - -- Attack Targets - self.EscortMenuAttackNearbyTargets = MENU_CLIENT:New( self.EscortClient, "Attack targets", self.EscortMenu ) - - - --self.ReportTargetsScheduler = routines.scheduleFunction( self._ReportTargetsScheduler, { self }, timer.getTime() + 1, Seconds ) - self.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, {}, 1, Seconds ) - - return self -end - ---- Defines a menu slot to let the escort attack its detected targets using assisted attack from another escort joined also with the client. --- This menu will appear under **Request assistance from**. --- Note that this method needs to be preceded with the method MenuReportTargets. --- @param #ESCORT self --- @return #ESCORT -function ESCORT:MenuAssistedAttack() - self:F() - - -- Request assistance from other escorts. - -- This is very useful to let f.e. an escorting ship attack a target detected by an escorting plane... - self.EscortMenuTargetAssistance = MENU_CLIENT:New( self.EscortClient, "Request assistance from", self.EscortMenu ) - - return self -end - ---- Defines a menu to let the escort set its rules of engagement. --- All rules of engagement will appear under the menu **ROE**. --- @param #ESCORT self --- @return #ESCORT -function ESCORT:MenuROE( MenuTextFormat ) - self:F( MenuTextFormat ) - - if not self.EscortMenuROE then - -- Rules of Engagement - self.EscortMenuROE = MENU_CLIENT:New( self.EscortClient, "ROE", self.EscortMenu ) - if self.EscortGroup:OptionROEHoldFirePossible() then - self.EscortMenuROEHoldFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Hold Fire", self.EscortMenuROE, ESCORT._ROE, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROEHoldFire(), ParamMessage = "Holding weapons!" } ) - end - if self.EscortGroup:OptionROEReturnFirePossible() then - self.EscortMenuROEReturnFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Return Fire", self.EscortMenuROE, ESCORT._ROE, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROEReturnFire(), ParamMessage = "Returning fire!" } ) - end - if self.EscortGroup:OptionROEOpenFirePossible() then - self.EscortMenuROEOpenFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Open Fire", self.EscortMenuROE, ESCORT._ROE, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROEOpenFire(), ParamMessage = "Opening fire on designated targets!!" } ) - end - if self.EscortGroup:OptionROEWeaponFreePossible() then - self.EscortMenuROEWeaponFree = MENU_CLIENT_COMMAND:New( self.EscortClient, "Weapon Free", self.EscortMenuROE, ESCORT._ROE, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROEWeaponFree(), ParamMessage = "Opening fire on targets of opportunity!" } ) - end - end - - return self -end - - ---- Defines a menu to let the escort set its evasion when under threat. --- All rules of engagement will appear under the menu **Evasion**. --- @param #ESCORT self --- @return #ESCORT -function ESCORT:MenuEvasion( MenuTextFormat ) - self:F( MenuTextFormat ) - - if self.EscortGroup:IsAir() then - if not self.EscortMenuEvasion then - -- Reaction to Threats - self.EscortMenuEvasion = MENU_CLIENT:New( self.EscortClient, "Evasion", self.EscortMenu ) - if self.EscortGroup:OptionROTNoReactionPossible() then - self.EscortMenuEvasionNoReaction = MENU_CLIENT_COMMAND:New( self.EscortClient, "Fight until death", self.EscortMenuEvasion, ESCORT._ROT, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROTNoReaction(), ParamMessage = "Fighting until death!" } ) - end - if self.EscortGroup:OptionROTPassiveDefensePossible() then - self.EscortMenuEvasionPassiveDefense = MENU_CLIENT_COMMAND:New( self.EscortClient, "Use flares, chaff and jammers", self.EscortMenuEvasion, ESCORT._ROT, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROTPassiveDefense(), ParamMessage = "Defending using jammers, chaff and flares!" } ) - end - if self.EscortGroup:OptionROTEvadeFirePossible() then - self.EscortMenuEvasionEvadeFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Evade enemy fire", self.EscortMenuEvasion, ESCORT._ROT, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROTEvadeFire(), ParamMessage = "Evading on enemy fire!" } ) - end - if self.EscortGroup:OptionROTVerticalPossible() then - self.EscortMenuOptionEvasionVertical = MENU_CLIENT_COMMAND:New( self.EscortClient, "Go below radar and evade fire", self.EscortMenuEvasion, ESCORT._ROT, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROTVertical(), ParamMessage = "Evading on enemy fire with vertical manoeuvres!" } ) - end - end - end - - return self -end - ---- Defines a menu to let the escort resume its mission from a waypoint on its route. --- All rules of engagement will appear under the menu **Resume mission from**. --- @param #ESCORT self --- @return #ESCORT -function ESCORT:MenuResumeMission() - self:F() - - if not self.EscortMenuResumeMission then - -- Mission Resume Menu Root - self.EscortMenuResumeMission = MENU_CLIENT:New( self.EscortClient, "Resume mission from", self.EscortMenu ) - end - - return self -end - - ---- @param #MENUPARAM MenuParam -function ESCORT._HoldPosition( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - local OrbitGroup = MenuParam.ParamOrbitGroup -- Group#GROUP - local OrbitUnit = OrbitGroup:GetUnit(1) -- Unit#UNIT - local OrbitHeight = MenuParam.ParamHeight - local OrbitSeconds = MenuParam.ParamSeconds -- Not implemented yet - - routines.removeFunction( self.FollowScheduler ) - - local PointFrom = {} - local GroupPoint = EscortGroup:GetUnit(1):GetPointVec3() - PointFrom = {} - PointFrom.x = GroupPoint.x - PointFrom.y = GroupPoint.z - PointFrom.speed = 250 - PointFrom.type = AI.Task.WaypointType.TURNING_POINT - PointFrom.alt = GroupPoint.y - PointFrom.alt_type = AI.Task.AltitudeType.BARO - - local OrbitPoint = OrbitUnit:GetPointVec2() - local PointTo = {} - PointTo.x = OrbitPoint.x - PointTo.y = OrbitPoint.y - PointTo.speed = 250 - PointTo.type = AI.Task.WaypointType.TURNING_POINT - PointTo.alt = OrbitHeight - PointTo.alt_type = AI.Task.AltitudeType.BARO - PointTo.task = EscortGroup:TaskOrbitCircleAtVec2( OrbitPoint, OrbitHeight, 0 ) - - local Points = { PointFrom, PointTo } - - EscortGroup:OptionROEHoldFire() - EscortGroup:OptionROTPassiveDefense() - - EscortGroup:SetTask( EscortGroup:TaskRoute( Points ) ) - EscortGroup:MessageToClient( "Orbiting at location.", 10, EscortClient ) -end - ---- @param #MENUPARAM MenuParam -function ESCORT._JoinUpAndFollow( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - self.Distance = MenuParam.ParamDistance - - self:JoinUpAndFollow( EscortGroup, EscortClient, self.Distance ) -end - ---- JoinsUp and Follows a CLIENT. --- @param Escort#ESCORT self --- @param Group#GROUP EscortGroup --- @param Client#CLIENT EscortClient --- @param DCSTypes#Distance Distance -function ESCORT:JoinUpAndFollow( EscortGroup, EscortClient, Distance ) - self:F( { EscortGroup, EscortClient, Distance } ) - - if self.FollowScheduler then - routines.removeFunction( self.FollowScheduler ) - end - - EscortGroup:OptionROEHoldFire() - EscortGroup:OptionROTPassiveDefense() - - self.EscortMode = ESCORT.MODE.FOLLOW - - self.CT1 = 0 - self.GT1 = 0 - --self.FollowScheduler = routines.scheduleFunction( self._FollowScheduler, { self, Distance }, timer.getTime() + 1, .5 ) - self.FollowScheduler = SCHEDULER:New( self, self._FollowScheduler, { Distance }, 1, .5, .1 ) - EscortGroup:MessageToClient( "Rejoining and Following at " .. Distance .. "!", 30, EscortClient ) -end - ---- @param #MENUPARAM MenuParam -function ESCORT._Flare( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - local Color = MenuParam.ParamColor - local Message = MenuParam.ParamMessage - - EscortGroup:GetUnit(1):Flare( Color ) - EscortGroup:MessageToClient( Message, 10, EscortClient ) -end - ---- @param #MENUPARAM MenuParam -function ESCORT._Smoke( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - local Color = MenuParam.ParamColor - local Message = MenuParam.ParamMessage - - EscortGroup:GetUnit(1):Smoke( Color ) - EscortGroup:MessageToClient( Message, 10, EscortClient ) -end - - ---- @param #MENUPARAM MenuParam -function ESCORT._ReportNearbyTargetsNow( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - self:_ReportTargetsScheduler() - -end - -function ESCORT._SwitchReportNearbyTargets( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - self.ReportTargets = MenuParam.ParamReportTargets - - if self.ReportTargets then - if not self.ReportTargetsScheduler then - --self.ReportTargetsScheduler = routines.scheduleFunction( self._ReportTargetsScheduler, { self }, timer.getTime() + 1, 30 ) - self.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, {}, 1, 30 ) - end - else - routines.removeFunction( self.ReportTargetsScheduler ) - self.ReportTargetsScheduler = nil - end -end - ---- @param #MENUPARAM MenuParam -function ESCORT._ScanTargets( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - local ScanDuration = MenuParam.ParamScanDuration - - if self.FollowScheduler then - routines.removeFunction( self.FollowScheduler ) - end - - self:T( { "FollowScheduler after removefunction: ", self.FollowScheduler } ) - - if EscortGroup:IsHelicopter() then - SCHEDULER:New( EscortGroup, EscortGroup.PushTask, - { EscortGroup:TaskControlled( - EscortGroup:TaskOrbitCircle( 200, 20 ), - EscortGroup:TaskCondition( nil, nil, nil, nil, ScanDuration, nil ) - ) - }, - 1 - ) - elseif EscortGroup:IsAirPlane() then - SCHEDULER:New( EscortGroup, EscortGroup.PushTask, - { EscortGroup:TaskControlled( - EscortGroup:TaskOrbitCircle( 1000, 500 ), - EscortGroup:TaskCondition( nil, nil, nil, nil, ScanDuration, nil ) - ) - }, - 1 - ) - end - - EscortGroup:MessageToClient( "Scanning targets for " .. ScanDuration .. " seconds.", ScanDuration, EscortClient ) - - if self.EscortMode == ESCORT.MODE.FOLLOW then - --self.FollowScheduler = routines.scheduleFunction( self._FollowScheduler, { self, Distance }, timer.getTime() + ScanDuration, 1 ) - self.FollowScheduler:Start() - end - -end - -function _Resume( EscortGroup ) - env.info( '_Resume' ) - - local Escort = EscortGroup.Escort -- #ESCORT - env.info( "EscortMode = " .. Escort.EscortMode ) - if Escort.EscortMode == ESCORT.MODE.FOLLOW then - Escort:JoinUpAndFollow( EscortGroup, Escort.EscortClient, Escort.Distance ) - end - -end - ---- @param #MENUPARAM MenuParam -function ESCORT._AttackTarget( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - local AttackUnit = MenuParam.ParamUnit -- Unit#UNIT - - if self.FollowScheduler then - routines.removeFunction( self.FollowScheduler ) - end - - self:T( AttackUnit ) - - if EscortGroup:IsAir() then - EscortGroup:OptionROEOpenFire() - EscortGroup:OptionROTPassiveDefense() - EscortGroup.Escort = self -- Need to do this trick to get the reference for the escort in the _Resume function. --- routines.scheduleFunction( --- EscortGroup.PushTask, --- { EscortGroup, --- EscortGroup:TaskCombo( --- { EscortGroup:TaskAttackUnit( AttackUnit ), --- EscortGroup:TaskFunction( 1, 2, "_Resume", {"''"} ) --- } --- ) --- }, timer.getTime() + 10 --- ) - SCHEDULER:New( EscortGroup, - EscortGroup.PushTask, - { EscortGroup:TaskCombo( - { EscortGroup:TaskAttackUnit( AttackUnit ), - EscortGroup:TaskFunction( 1, 2, "_Resume", {"''"} ) - } - ) - }, 10 - ) - else --- routines.scheduleFunction( --- EscortGroup.PushTask, --- { EscortGroup, --- EscortGroup:TaskCombo( --- { EscortGroup:TaskFireAtPoint( AttackUnit:GetPointVec2(), 50 ) --- } --- ) --- }, timer.getTime() + 10 --- ) - SCHEDULER:New( EscortGroup, - EscortGroup.PushTask, - { EscortGroup:TaskCombo( - { EscortGroup:TaskFireAtPoint( AttackUnit:GetPointVec2(), 50 ) - } - ) - }, 10 - ) - end - EscortGroup:MessageToClient( "Engaging Designated Unit!", 10, EscortClient ) - - -end - ---- @param #MENUPARAM MenuParam -function ESCORT._AssistTarget( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - local EscortGroupAttack = MenuParam.ParamEscortGroup - local AttackUnit = MenuParam.ParamUnit -- Unit#UNIT - - if self.FollowScheduler then - routines.removeFunction( self.FollowScheduler ) - end - - - self:T( AttackUnit ) - - if EscortGroupAttack:IsAir() then - EscortGroupAttack:OptionROEOpenFire() - EscortGroupAttack:OptionROTVertical() --- routines.scheduleFunction( --- EscortGroupAttack.PushTask, --- { EscortGroupAttack, --- EscortGroupAttack:TaskCombo( --- { EscortGroupAttack:TaskAttackUnit( AttackUnit ), --- EscortGroupAttack:TaskOrbitCircle( 500, 350 ) --- } --- ) --- }, timer.getTime() + 10 --- ) - SCHDULER:New( EscortGroupAttack, - EscortGroupAttack.PushTask, - { EscortGroupAttack:TaskCombo( - { EscortGroupAttack:TaskAttackUnit( AttackUnit ), - EscortGroupAttack:TaskOrbitCircle( 500, 350 ) - } - ) - }, 10 - ) - else --- routines.scheduleFunction( --- EscortGroupAttack.PushTask, --- { EscortGroupAttack, --- EscortGroupAttack:TaskCombo( --- { EscortGroupAttack:TaskFireAtPoint( AttackUnit:GetPointVec2(), 50 ) --- } --- ) --- }, timer.getTime() + 10 --- ) - SCHEDULER:New( EscortGroupAttack, - EscortGroupAttack.PushTask, - { EscortGroupAttack:TaskCombo( - { EscortGroupAttack:TaskFireAtPoint( AttackUnit:GetPointVec2(), 50 ) - } - ) - }, 10 - ) - end - EscortGroupAttack:MessageToClient( "Assisting with the destroying the enemy unit!", 10, EscortClient ) - -end - ---- @param #MENUPARAM MenuParam -function ESCORT._ROE( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - local EscortROEFunction = MenuParam.ParamFunction - local EscortROEMessage = MenuParam.ParamMessage - - pcall( function() EscortROEFunction() end ) - EscortGroup:MessageToClient( EscortROEMessage, 10, EscortClient ) -end - ---- @param #MENUPARAM MenuParam -function ESCORT._ROT( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - local EscortROTFunction = MenuParam.ParamFunction - local EscortROTMessage = MenuParam.ParamMessage - - pcall( function() EscortROTFunction() end ) - EscortGroup:MessageToClient( EscortROTMessage, 10, EscortClient ) -end - ---- @param #MENUPARAM MenuParam -function ESCORT._ResumeMission( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - local WayPoint = MenuParam.ParamWayPoint - - routines.removeFunction( self.FollowScheduler ) - self.FollowScheduler = nil - - local WayPoints = EscortGroup:GetTaskRoute() - self:T( WayPoint, WayPoints ) - - for WayPointIgnore = 1, WayPoint do - table.remove( WayPoints, 1 ) - end - - --routines.scheduleFunction( EscortGroup.SetTask, {EscortGroup, EscortGroup:TaskRoute( WayPoints ) }, timer.getTime() + 1 ) - SCHEDULER:New( EscortGroup, EscortGroup.SetTask, { EscortGroup:TaskRoute( WayPoints ) }, 1 ) - - EscortGroup:MessageToClient( "Resuming mission from waypoint " .. WayPoint .. ".", 10, EscortClient ) -end - ---- Registers the waypoints --- @param #ESCORT self --- @return #table -function ESCORT:RegisterRoute() - self:F() - - local EscortGroup = self.EscortGroup -- Group#GROUP - - local TaskPoints = EscortGroup:GetTaskRoute() - - self:T( TaskPoints ) - - return TaskPoints -end - ---- @param Escort#ESCORT self -function ESCORT:_FollowScheduler( FollowDistance ) - self:F( { FollowDistance }) - - if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then - - local ClientUnit = self.EscortClient:GetClientGroupUnit() - local GroupUnit = self.EscortGroup:GetUnit( 1 ) - - if self.CT1 == 0 and self.GT1 == 0 then - self.CV1 = ClientUnit:GetPointVec3() - self.CT1 = timer.getTime() - self.GV1 = GroupUnit:GetPointVec3() - self.GT1 = timer.getTime() - else - local CT1 = self.CT1 - local CT2 = timer.getTime() - local CV1 = self.CV1 - local CV2 = ClientUnit:GetPointVec3() - self.CT1 = CT2 - self.CV1 = CV2 - - local CD = ( ( CV2.x - CV1.x )^2 + ( CV2.y - CV1.y )^2 + ( CV2.z - CV1.z )^2 ) ^ 0.5 - local CT = CT2 - CT1 - - local CS = ( 3600 / CT ) * ( CD / 1000 ) - - self:T2( { "Client:", CS, CD, CT, CV2, CV1, CT2, CT1 } ) - - local GT1 = self.GT1 - local GT2 = timer.getTime() - local GV1 = self.GV1 - local GV2 = GroupUnit:GetPointVec3() - self.GT1 = GT2 - self.GV1 = GV2 - - local GD = ( ( GV2.x - GV1.x )^2 + ( GV2.y - GV1.y )^2 + ( GV2.z - GV1.z )^2 ) ^ 0.5 - local GT = GT2 - GT1 - - local GS = ( 3600 / GT ) * ( GD / 1000 ) - - self:T2( { "Group:", GS, GD, GT, GV2, GV1, GT2, GT1 } ) - - -- Calculate the group direction vector - local GV = { x = GV2.x - CV2.x, y = GV2.y - CV2.y, z = GV2.z - CV2.z } - - -- Calculate GH2, GH2 with the same height as CV2. - local GH2 = { x = GV2.x, y = CV2.y, z = GV2.z } - - -- Calculate the angle of GV to the orthonormal plane - local alpha = math.atan2( GV.z, GV.x ) - - -- Now we calculate the intersecting vector between the circle around CV2 with radius FollowDistance and GH2. - -- From the GeoGebra model: CVI = (x(CV2) + FollowDistance cos(alpha), y(GH2) + FollowDistance sin(alpha), z(CV2)) - local CVI = { x = CV2.x + FollowDistance * math.cos(alpha), - y = GH2.y, - z = CV2.z + FollowDistance * math.sin(alpha), - } - - -- Calculate the direction vector DV of the escort group. We use CVI as the base and CV2 as the direction. - local DV = { x = CV2.x - CVI.x, y = CV2.y - CVI.y, z = CV2.z - CVI.z } - - -- We now calculate the unary direction vector DVu, so that we can multiply DVu with the speed, which is expressed in meters / s. - -- We need to calculate this vector to predict the point the escort group needs to fly to according its speed. - -- The distance of the destination point should be far enough not to have the aircraft starting to swipe left to right... - local DVu = { x = DV.x / FollowDistance, y = DV.y / FollowDistance, z = DV.z / FollowDistance } - - -- Now we can calculate the group destination vector GDV. - local GDV = { x = DVu.x * CS * 8 + CVI.x, y = CVI.y, z = DVu.z * CS * 8 + CVI.z } - - --trigger.action.smoke( GDV, trigger.smokeColor.Red ) - 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, FlyDistance, Time:", CS, GS, Speed, Distance, Time } ) - - -- Now route the escort to the desired point with the desired speed. - self.EscortGroup:TaskRouteToVec3( GDV, Speed / 3.6 ) -- DCS models speed in Mps (Miles per second) - end - return true - end - - return false -end - - ---- Report Targets Scheduler. --- @param #ESCORT self -function ESCORT:_ReportTargetsScheduler() - self:F( self.EscortGroup:GetName() ) - - if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then - local EscortGroupName = self.EscortGroup:GetName() - local EscortTargets = self.EscortGroup:GetDetectedTargets() - - local ClientEscortTargets = self.EscortClient._EscortGroups[EscortGroupName].Targets - - local EscortTargetMessages = "" - for EscortTargetID, EscortTarget in pairs( EscortTargets ) do - local EscortObject = EscortTarget.object - self:T( EscortObject ) - if EscortObject and EscortObject:isExist() and EscortObject.id_ < 50000000 then - - local EscortTargetUnit = UNIT:Find( EscortObject ) - local EscortTargetUnitName = EscortTargetUnit:GetName() - - - - -- local EscortTargetIsDetected, - -- EscortTargetIsVisible, - -- EscortTargetLastTime, - -- EscortTargetKnowType, - -- EscortTargetKnowDistance, - -- EscortTargetLastPos, - -- EscortTargetLastVelocity - -- = self.EscortGroup:IsTargetDetected( EscortObject ) - -- - -- self:T( { EscortTargetIsDetected, - -- EscortTargetIsVisible, - -- EscortTargetLastTime, - -- EscortTargetKnowType, - -- EscortTargetKnowDistance, - -- EscortTargetLastPos, - -- EscortTargetLastVelocity } ) - - - local EscortTargetUnitPositionVec3 = EscortTargetUnit:GetPointVec3() - local EscortPositionVec3 = self.EscortGroup:GetPointVec3() - local Distance = ( ( EscortTargetUnitPositionVec3.x - EscortPositionVec3.x )^2 + - ( EscortTargetUnitPositionVec3.y - EscortPositionVec3.y )^2 + - ( EscortTargetUnitPositionVec3.z - EscortPositionVec3.z )^2 - ) ^ 0.5 / 1000 - - self:T( { self.EscortGroup:GetName(), EscortTargetUnit:GetName(), Distance, EscortTarget } ) - - if Distance <= 15 then - - if not ClientEscortTargets[EscortTargetUnitName] then - ClientEscortTargets[EscortTargetUnitName] = {} - end - ClientEscortTargets[EscortTargetUnitName].AttackUnit = EscortTargetUnit - ClientEscortTargets[EscortTargetUnitName].visible = EscortTarget.visible - ClientEscortTargets[EscortTargetUnitName].type = EscortTarget.type - ClientEscortTargets[EscortTargetUnitName].distance = EscortTarget.distance - else - if ClientEscortTargets[EscortTargetUnitName] then - ClientEscortTargets[EscortTargetUnitName] = nil - end - end - end - end - - self:T( { "Sorting Targets Table:", ClientEscortTargets } ) - table.sort( ClientEscortTargets, function( a, b ) return a.Distance < b.Distance end ) - self:T( { "Sorted Targets Table:", ClientEscortTargets } ) - - -- Remove the sub menus of the Attack menu of the Escort for the EscortGroup. - self.EscortMenuAttackNearbyTargets:RemoveSubMenus() - - if self.EscortMenuTargetAssistance then - self.EscortMenuTargetAssistance:RemoveSubMenus() - end - - --for MenuIndex = 1, #self.EscortMenuAttackTargets do - -- self:T( { "Remove Menu:", self.EscortMenuAttackTargets[MenuIndex] } ) - -- self.EscortMenuAttackTargets[MenuIndex] = self.EscortMenuAttackTargets[MenuIndex]:Remove() - --end - - - if ClientEscortTargets then - for ClientEscortTargetUnitName, ClientEscortTargetData in pairs( ClientEscortTargets ) do - - for ClientEscortGroupName, EscortGroupData in pairs( self.EscortClient._EscortGroups ) do - - if ClientEscortTargetData and ClientEscortTargetData.AttackUnit:IsAlive() then - - local EscortTargetMessage = "" - local EscortTargetCategoryName = ClientEscortTargetData.AttackUnit:GetCategoryName() - local EscortTargetCategoryType = ClientEscortTargetData.AttackUnit:GetTypeName() - if ClientEscortTargetData.type then - EscortTargetMessage = EscortTargetMessage .. EscortTargetCategoryName .. " (" .. EscortTargetCategoryType .. ") at " - else - EscortTargetMessage = EscortTargetMessage .. "Unknown target at " - end - - local EscortTargetUnitPositionVec3 = ClientEscortTargetData.AttackUnit:GetPointVec3() - local EscortPositionVec3 = self.EscortGroup:GetPointVec3() - local Distance = ( ( EscortTargetUnitPositionVec3.x - EscortPositionVec3.x )^2 + - ( EscortTargetUnitPositionVec3.y - EscortPositionVec3.y )^2 + - ( EscortTargetUnitPositionVec3.z - EscortPositionVec3.z )^2 - ) ^ 0.5 / 1000 - - self:T( { self.EscortGroup:GetName(), ClientEscortTargetData.AttackUnit:GetName(), Distance, ClientEscortTargetData.AttackUnit } ) - if ClientEscortTargetData.visible == false then - EscortTargetMessage = EscortTargetMessage .. string.format( "%.2f", Distance ) .. " estimated km" - else - EscortTargetMessage = EscortTargetMessage .. string.format( "%.2f", Distance ) .. " km" - end - - if ClientEscortTargetData.visible then - EscortTargetMessage = EscortTargetMessage .. ", visual" - end - - if ClientEscortGroupName == EscortGroupName then - - MENU_CLIENT_COMMAND:New( self.EscortClient, - EscortTargetMessage, - self.EscortMenuAttackNearbyTargets, - ESCORT._AttackTarget, - { ParamSelf = self, - ParamUnit = ClientEscortTargetData.AttackUnit - } - ) - EscortTargetMessages = EscortTargetMessages .. "\n - " .. EscortTargetMessage - else - if self.EscortMenuTargetAssistance then - local MenuTargetAssistance = MENU_CLIENT:New( self.EscortClient, EscortGroupData.EscortName, self.EscortMenuTargetAssistance ) - MENU_CLIENT_COMMAND:New( self.EscortClient, - EscortTargetMessage, - MenuTargetAssistance, - ESCORT._AssistTarget, - { ParamSelf = self, - ParamEscortGroup = EscortGroupData.EscortGroup, - ParamUnit = ClientEscortTargetData.AttackUnit - } - ) - end - end - else - ClientEscortTargetData = nil - end - end - end - - if EscortTargetMessages ~= "" and self.ReportTargets == true then - self.EscortGroup:MessageToClient( "Detected targets within 15 km range:" .. EscortTargetMessages:gsub("\n$",""), 20, self.EscortClient ) - else - self.EscortGroup:MessageToClient( "No targets detected!", 20, self.EscortClient ) - end - end - - if self.EscortMenuResumeMission then - self.EscortMenuResumeMission:RemoveSubMenus() - - -- if self.EscortMenuResumeWayPoints then - -- for MenuIndex = 1, #self.EscortMenuResumeWayPoints do - -- self:T( { "Remove Menu:", self.EscortMenuResumeWayPoints[MenuIndex] } ) - -- self.EscortMenuResumeWayPoints[MenuIndex] = self.EscortMenuResumeWayPoints[MenuIndex]:Remove() - -- end - -- end - - local TaskPoints = self:RegisterRoute() - for WayPointID, WayPoint in pairs( TaskPoints ) do - local EscortPositionVec3 = self.EscortGroup:GetPointVec3() - local Distance = ( ( WayPoint.x - EscortPositionVec3.x )^2 + - ( WayPoint.y - EscortPositionVec3.z )^2 - ) ^ 0.5 / 1000 - MENU_CLIENT_COMMAND:New( self.EscortClient, "Waypoint " .. WayPointID .. " at " .. string.format( "%.2f", Distance ).. "km", self.EscortMenuResumeMission, ESCORT._ResumeMission, { ParamSelf = self, ParamWayPoint = WayPointID } ) - end - end - return true - end - - return false -end ---- Provides missile training functions. --- --- @{#MISSILETRAINER} class --- ======================== --- 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. --- --- --- 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. --- --- 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. --- --- @module MissileTrainer --- @author FlightControl - - -Include.File( "Client" ) -Include.File( "Scheduler" ) - ---- The MISSILETRAINER class --- @type MISSILETRAINER --- @extends Base#BASE -MISSILETRAINER = { - ClassName = "MISSILETRAINER", -} - ---- Creates the main object which is handling missile tracking. --- When a missile is fired a SCHEDULER is set off that follows the missile. When near a certain a client player, the missile will be destroyed. --- @param #MISSILETRAINER self --- @param #number Distance The distance in meters when a tracked missile needs to be destroyed when close to a player. --- @param #string Briefing (Optional) Will show a text to the players when starting their mission. Can be used for briefing purposes. --- @return #MISSILETRAINER -function MISSILETRAINER:New( Distance, Briefing ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( Distance ) - - if Briefing then - self.Briefing = Briefing - end - - self.Schedulers = {} - self.SchedulerID = 0 - - self.MessageInterval = 2 - self.MessageLastTime = timer.getTime() - - self.Distance = Distance / 1000 - - _EVENTDISPATCHER:OnShot( self._EventShot, self ) - - self.DB = DATABASE:New():FilterStart() - self.DBClients = self.DB.Clients - self.DBUnits = self.DB.Units - - for ClientID, Client in pairs( self.DBClients ) do - - local function _Alive( Client ) - - if self.Briefing then - Client:Message( self.Briefing, 15, "HELLO WORLD", "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, "MENU", "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 - - Client:Alive( _Alive ) - - end - --- self.DB:ForEachClient( --- --- @param Client#CLIENT Client --- function( Client ) --- --- ... actions ... --- --- end --- ) - - self.MessagesOnOff = true - - self.TrackingToAll = false - self.TrackingOnOff = true - self.TrackingFrequency = 3 - - self.AlertsToAll = true - self.AlertsHitsOnOff = true - self.AlertsLaunchesOnOff = true - - self.DetailsRangeOnOff = true - self.DetailsBearingOnOff = true - - self.MenusOnOff = true - - self.TrackingMissiles = {} - - self.TrackingScheduler = SCHEDULER:New( self, self._TrackMissiles, {}, 0.5, 0.05, 0 ) - - return self -end - --- Initialization methods. - - ---- Sets by default the display of any message to be ON or OFF. --- @param #MISSILETRAINER self --- @param #boolean MessagesOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitMessagesOnOff( MessagesOnOff ) - self:F( MessagesOnOff ) - - self.MessagesOnOff = MessagesOnOff - if self.MessagesOnOff == true then - MESSAGE:New( "Messages ON", "Menu", 15, "ID" ):ToAll() - else - MESSAGE:New( "Messages OFF", "Menu", 15, "ID" ):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", "Menu", 15, "ID" ):ToAll() - else - MESSAGE:New( "Missile tracking to all players OFF", "Menu", 15, "ID" ):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", "Menu", 15, "ID" ):ToAll() - else - MESSAGE:New( "Missile tracking OFF", "Menu", 15, "ID" ):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.", "Menu", 15, "ID" ):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", "Menu", 15, "ID" ):ToAll() - else - MESSAGE:New( "Alerts to all players OFF", "Menu", 15, "ID" ):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", "Menu", 15, "ID" ):ToAll() - else - MESSAGE:New( "Alerts Hits OFF", "Menu", 15, "ID" ):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", "Menu", 15, "ID" ):ToAll() - else - MESSAGE:New( "Alerts Launches OFF", "Menu", 15, "ID" ):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", "Menu", 15, "ID" ):ToAll() - else - MESSAGE:New( "Range display OFF", "Menu", 15, "ID" ):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", "Menu", 15, "ID" ):ToAll() - else - MESSAGE:New( "Bearing display OFF", "Menu", 15, "ID" ):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)", "Menu", 15, "ID" ):ToAll() - else - MESSAGE:New( "Menus are DISABLED", "Menu", 15, "ID" ):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", "Menu", 15, "ID" ):ToAll() - end - -end - ---- Detects if an SA site was shot with an anti radiation missile. In this case, take evasive actions based on the skill level set within the ME. --- @param #MISSILETRAINER self --- @param Event#EVENTDATA Event -function MISSILETRAINER:_EventShot( Event ) - self:F( { Event } ) - - local TrainerSourceDCSUnit = Event.IniDCSUnit - local TrainerSourceDCSUnitName = Event.IniDCSUnitName - local TrainerWeapon = Event.Weapon -- Identify the weapon fired - local TrainerWeaponName = Event.WeaponName -- return weapon type - - self:T( "Missile Launched = " .. TrainerWeaponName ) - - local TrainerTargetDCSUnit = TrainerWeapon:getTarget() -- Identify target - local TrainerTargetDCSUnitName = Unit.getName( TrainerTargetDCSUnit ) - local TrainerTargetSkill = _DATABASE.Templates.Units[TrainerTargetDCSUnitName].Template.skill - - self:T(TrainerTargetDCSUnitName ) - - local Client = self.DBClients[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 ),"Launch Alert", 5, "ID" ) - - if self.AlertsToAll then - Message:ToAll() - else - Message:ToClient( Client ) - end - end - - local ClientID = Client:GetID() - local MissileData = {} - MissileData.TrainerSourceUnit = TrainerSourceUnit - MissileData.TrainerWeapon = TrainerWeapon - MissileData.TrainerTargetUnit = TrainerTargetUnit - MissileData.TrainerWeaponTypeName = TrainerWeapon:getTypeName() - MissileData.TrainerWeaponLaunched = true - table.insert( self.TrackingMissiles[ClientID].MissileData, MissileData ) - --self:T( self.TrackingMissiles ) - end -end - -function MISSILETRAINER:_AddRange( Client, TrainerWeapon ) - - local RangeText = "" - - if self.DetailsRangeOnOff then - - local PositionMissile = TrainerWeapon:getPoint() - local PositionTarget = Client:GetPointVec3() - - local Range = ( ( PositionMissile.x - PositionTarget.x )^2 + - ( PositionMissile.y - PositionTarget.y )^2 + - ( PositionMissile.z - PositionTarget.z )^2 - ) ^ 0.5 / 1000 - - RangeText = string.format( ", at %4.2fkm", Range ) - end - - return RangeText -end - -function MISSILETRAINER:_AddBearing( Client, TrainerWeapon ) - - local BearingText = "" - - if self.DetailsBearingOnOff then - - local PositionMissile = TrainerWeapon:getPoint() - local PositionTarget = Client:GetPointVec3() - - self:T2( { PositionTarget, PositionMissile }) - - local DirectionVector = { x = PositionMissile.x - PositionTarget.x, y = PositionMissile.y - PositionTarget.y, z = PositionMissile.z - PositionTarget.z } - local DirectionRadians = math.atan2( DirectionVector.z, DirectionVector.x ) - --DirectionRadians = DirectionRadians + routines.getNorthCorrection( PositionTarget ) - if DirectionRadians < 0 then - DirectionRadians = DirectionRadians + 2 * math.pi - end - local DirectionDegrees = DirectionRadians * 180 / math.pi - - BearingText = string.format( ", %d degrees", DirectionDegrees ) - end - - return BearingText -end - - -function MISSILETRAINER:_TrackMissiles() - self:F2() - - - local ShowMessages = false - if self.MessagesOnOff and self.MessageLastTime + self.TrackingFrequency <= timer.getTime() then - self.MessageLastTime = timer.getTime() - ShowMessages = true - end - - -- ALERTS PART - - -- Loop for all Player Clients to check the alerts and deletion of missiles. - for ClientDataID, ClientData in pairs( self.TrackingMissiles ) do - - local Client = ClientData.Client - self:T2( { Client:GetName() } ) - - for MissileDataID, MissileData in pairs( ClientData.MissileData ) do - self:T3( MissileDataID ) - - local TrainerSourceUnit = MissileData.TrainerSourceUnit - local TrainerWeapon = MissileData.TrainerWeapon - local TrainerTargetUnit = MissileData.TrainerTargetUnit - local TrainerWeaponTypeName = MissileData.TrainerWeaponTypeName - local TrainerWeaponLaunched = MissileData.TrainerWeaponLaunched - - if Client and Client:IsAlive() and TrainerSourceUnit and TrainerSourceUnit:IsAlive() and TrainerWeapon and TrainerWeapon:isExist() and TrainerTargetUnit and TrainerTargetUnit:IsAlive() then - local PositionMissile = TrainerWeapon:getPosition().p - local PositionTarget = Client:GetPointVec3() - - local Distance = ( ( PositionMissile.x - PositionTarget.x )^2 + - ( PositionMissile.y - PositionTarget.y )^2 + - ( PositionMissile.z - PositionTarget.z )^2 - ) ^ 0.5 / 1000 - - if Distance <= self.Distance then - -- Hit alert - TrainerWeapon:destroy() - if self.MessagesOnOff == true and self.AlertsHitsOnOff == true then - - self:T( "killed" ) - - local Message = MESSAGE:New( - string.format( "%s launched by %s killed %s", - TrainerWeapon:getTypeName(), - TrainerSourceUnit:GetTypeName(), - TrainerTargetUnit:GetPlayerName() - ),"Hit Alert", 15, "ID" ) - - 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() - ),"Tracking", 5, "ID" ) - - 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, "Tracking", 1, "ID" ):ToClient( Client ) - end - end - end - - return true -end -env.info( '*** MOOSE INCLUDE END *** ' ) +env.info("Loaded MOOSE Include Engine")env.info( '*** MOOSE INCLUDE END *** ' ) diff --git a/Moose Mission Setup/Moose.lua b/Moose Mission Setup/Moose.lua index 8f1d04ce8..d4c566d6b 100644 --- a/Moose Mission Setup/Moose.lua +++ b/Moose Mission Setup/Moose.lua @@ -1,5 +1,6 @@ -env.info( '*** MOOSE STATIC INCLUDE START *** ' ) -env.info( 'Moose Generation Timestamp: 20160526_1413' ) +env.info( '*** MOOSE DYNAMIC INCLUDE START *** ' ) +env.info( 'Moose Generation Timestamp: 20160527_1003' ) + local base = _G env.info("Loading MOOSE " .. base.timer.getAbsTime() ) @@ -11,9 +12,27 @@ Include.Path = function() end Include.File = function( IncludeFile ) + if not Include.Files[ IncludeFile ] then + Include.Files[IncludeFile] = IncludeFile + env.info( "Include:" .. IncludeFile .. " from " .. Include.ProgramPath ) + local f = assert( base.loadfile( Include.ProgramPath .. IncludeFile .. ".lua" ) ) + if f == nil then + env.info( "Include:" .. IncludeFile .. " from " .. Include.MissionPath ) + local f = assert( base.loadfile( Include.MissionPath .. IncludeFile .. ".lua" ) ) + if f == nil then + error ("Could not load MOOSE file " .. IncludeFile .. ".lua" ) + else + env.info( "Include:" .. IncludeFile .. " loaded from " .. Include.MissionPath ) + return f() + end + else + env.info( "Include:" .. IncludeFile .. " loaded from " .. Include.ProgramPath ) + return f() + end + end end -Include.ProgramPath = "Scripts/Moose/Moose/" +Include.ProgramPath = "Scripts/Moose/" Include.MissionPath = Include.Path() env.info( "Include.ProgramPath = " .. Include.ProgramPath) @@ -23,16701 +42,4 @@ Include.Files = {} Include.File( "Moose" ) -env.info("Loaded MOOSE Include Engine") ---- Various routines --- @module routines --- @author Flightcontrol - ---Include.File( "Trace" ) ---Include.File( "Message" ) - - -env.setErrorMessageBoxEnabled(false) - ---- Extract of MIST functions. --- @author Grimes - -routines = {} - - --- don't change these -routines.majorVersion = 3 -routines.minorVersion = 3 -routines.build = 22 - ------------------------------------------------------------------------------------------------------------------ - ----------------------------------------------------------------------------------------------- --- Utils- conversion, Lua utils, etc. -routines.utils = {} - ---from http://lua-users.org/wiki/CopyTable -routines.utils.deepCopy = function(object) - local lookup_table = {} - local function _copy(object) - if type(object) ~= "table" then - return object - elseif lookup_table[object] then - return lookup_table[object] - end - local new_table = {} - lookup_table[object] = new_table - for index, value in pairs(object) do - new_table[_copy(index)] = _copy(value) - end - return setmetatable(new_table, getmetatable(object)) - end - local objectreturn = _copy(object) - return objectreturn -end - - --- porting in Slmod's serialize_slmod2 -routines.utils.oneLineSerialize = function(tbl) -- serialization of a table all on a single line, no comments, made to replace old get_table_string function - - lookup_table = {} - - local function _Serialize( tbl ) - - if type(tbl) == 'table' then --function only works for tables! - - if lookup_table[tbl] then - return lookup_table[object] - end - - local tbl_str = {} - - lookup_table[tbl] = tbl_str - - tbl_str[#tbl_str + 1] = '{' - - for ind,val in pairs(tbl) do -- serialize its fields - local ind_str = {} - if type(ind) == "number" then - ind_str[#ind_str + 1] = '[' - ind_str[#ind_str + 1] = tostring(ind) - ind_str[#ind_str + 1] = ']=' - else --must be a string - ind_str[#ind_str + 1] = '[' - ind_str[#ind_str + 1] = routines.utils.basicSerialize(ind) - ind_str[#ind_str + 1] = ']=' - end - - local val_str = {} - if ((type(val) == 'number') or (type(val) == 'boolean')) then - val_str[#val_str + 1] = tostring(val) - val_str[#val_str + 1] = ',' - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - elseif type(val) == 'string' then - val_str[#val_str + 1] = routines.utils.basicSerialize(val) - val_str[#val_str + 1] = ',' - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - elseif type(val) == 'nil' then -- won't ever happen, right? - val_str[#val_str + 1] = 'nil,' - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - elseif type(val) == 'table' then - if ind == "__index" then - -- tbl_str[#tbl_str + 1] = "__index" - -- tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it - else - - val_str[#val_str + 1] = _Serialize(val) - val_str[#val_str + 1] = ',' --I think this is right, I just added it - tbl_str[#tbl_str + 1] = table.concat(ind_str) - tbl_str[#tbl_str + 1] = table.concat(val_str) - end - elseif type(val) == 'function' then - -- tbl_str[#tbl_str + 1] = "function " .. tostring(ind) - -- tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it - else --- env.info('unable to serialize value type ' .. routines.utils.basicSerialize(type(val)) .. ' at index ' .. tostring(ind)) --- env.info( debug.traceback() ) - end - - end - tbl_str[#tbl_str + 1] = '}' - return table.concat(tbl_str) - else - return tostring(tbl) - end - end - - local objectreturn = _Serialize(tbl) - return objectreturn -end - ---porting in Slmod's "safestring" basic serialize -routines.utils.basicSerialize = function(s) - if s == nil then - return "\"\"" - else - if ((type(s) == 'number') or (type(s) == 'boolean') or (type(s) == 'function') or (type(s) == 'table') or (type(s) == 'userdata') ) then - return tostring(s) - elseif type(s) == 'string' then - s = string.format('%q', s) - return s - end - end -end - - -routines.utils.toDegree = function(angle) - return angle*180/math.pi -end - -routines.utils.toRadian = function(angle) - return angle*math.pi/180 -end - -routines.utils.metersToNM = function(meters) - return meters/1852 -end - -routines.utils.metersToFeet = function(meters) - return meters/0.3048 -end - -routines.utils.NMToMeters = function(NM) - return NM*1852 -end - -routines.utils.feetToMeters = function(feet) - return feet*0.3048 -end - -routines.utils.mpsToKnots = function(mps) - return mps*3600/1852 -end - -routines.utils.mpsToKmph = function(mps) - return mps*3.6 -end - -routines.utils.knotsToMps = function(knots) - return knots*1852/3600 -end - -routines.utils.kmphToMps = function(kmph) - return kmph/3.6 -end - -function routines.utils.makeVec2(Vec3) - if Vec3.z then - return {x = Vec3.x, y = Vec3.z} - else - return {x = Vec3.x, y = Vec3.y} -- it was actually already vec2. - end -end - -function routines.utils.makeVec3(Vec2, y) - if not Vec2.z then - if not y then - y = 0 - end - return {x = Vec2.x, y = y, z = Vec2.y} - else - return {x = Vec2.x, y = Vec2.y, z = Vec2.z} -- it was already Vec3, actually. - end -end - -function routines.utils.makeVec3GL(Vec2, offset) - local adj = offset or 0 - - if not Vec2.z then - return {x = Vec2.x, y = (land.getHeight(Vec2) + adj), z = Vec2.y} - else - return {x = Vec2.x, y = (land.getHeight({x = Vec2.x, y = Vec2.z}) + adj), z = Vec2.z} - end -end - -routines.utils.zoneToVec3 = function(zone) - local new = {} - if type(zone) == 'table' and zone.point then - new.x = zone.point.x - new.y = zone.point.y - new.z = zone.point.z - return new - elseif type(zone) == 'string' then - zone = trigger.misc.getZone(zone) - if zone then - new.x = zone.point.x - new.y = zone.point.y - new.z = zone.point.z - return new - end - end -end - --- gets heading-error corrected direction from point along vector vec. -function routines.utils.getDir(vec, point) - local dir = math.atan2(vec.z, vec.x) - dir = dir + routines.getNorthCorrection(point) - if dir < 0 then - dir = dir + 2*math.pi -- put dir in range of 0 to 2*pi - end - return dir -end - --- gets distance in meters between two points (2 dimensional) -function routines.utils.get2DDist(point1, point2) - point1 = routines.utils.makeVec3(point1) - point2 = routines.utils.makeVec3(point2) - return routines.vec.mag({x = point1.x - point2.x, y = 0, z = point1.z - point2.z}) -end - --- gets distance in meters between two points (3 dimensional) -function routines.utils.get3DDist(point1, point2) - return routines.vec.mag({x = point1.x - point2.x, y = point1.y - point2.y, z = point1.z - point2.z}) -end - - - --- From http://lua-users.org/wiki/SimpleRound --- use negative idp for rounding ahead of decimal place, positive for rounding after decimal place -routines.utils.round = function(num, idp) - local mult = 10^(idp or 0) - return math.floor(num * mult + 0.5) / mult -end - --- porting in Slmod's dostring -routines.utils.dostring = function(s) - local f, err = loadstring(s) - if f then - return true, f() - else - return false, err - end -end - - ---3D Vector manipulation -routines.vec = {} - -routines.vec.add = function(vec1, vec2) - return {x = vec1.x + vec2.x, y = vec1.y + vec2.y, z = vec1.z + vec2.z} -end - -routines.vec.sub = function(vec1, vec2) - return {x = vec1.x - vec2.x, y = vec1.y - vec2.y, z = vec1.z - vec2.z} -end - -routines.vec.scalarMult = function(vec, mult) - return {x = vec.x*mult, y = vec.y*mult, z = vec.z*mult} -end - -routines.vec.scalar_mult = routines.vec.scalarMult - -routines.vec.dp = function(vec1, vec2) - return vec1.x*vec2.x + vec1.y*vec2.y + vec1.z*vec2.z -end - -routines.vec.cp = function(vec1, vec2) - return { x = vec1.y*vec2.z - vec1.z*vec2.y, y = vec1.z*vec2.x - vec1.x*vec2.z, z = vec1.x*vec2.y - vec1.y*vec2.x} -end - -routines.vec.mag = function(vec) - return (vec.x^2 + vec.y^2 + vec.z^2)^0.5 -end - -routines.vec.getUnitVec = function(vec) - local mag = routines.vec.mag(vec) - return { x = vec.x/mag, y = vec.y/mag, z = vec.z/mag } -end - -routines.vec.rotateVec2 = function(vec2, theta) - return { x = vec2.x*math.cos(theta) - vec2.y*math.sin(theta), y = vec2.x*math.sin(theta) + vec2.y*math.cos(theta)} -end ---------------------------------------------------------------------------------------------------------------------------- - - - - --- acc- the accuracy of each easting/northing. 0, 1, 2, 3, 4, or 5. -routines.tostringMGRS = function(MGRS, acc) - if acc == 0 then - return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph - else - return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph .. ' ' .. string.format('%0' .. acc .. 'd', routines.utils.round(MGRS.Easting/(10^(5-acc)), 0)) - .. ' ' .. string.format('%0' .. acc .. 'd', routines.utils.round(MGRS.Northing/(10^(5-acc)), 0)) - end -end - ---[[acc: -in DM: decimal point of minutes. -In DMS: decimal point of seconds. -position after the decimal of the least significant digit: -So: -42.32 - acc of 2. -]] -routines.tostringLL = function(lat, lon, acc, DMS) - - local latHemi, lonHemi - if lat > 0 then - latHemi = 'N' - else - latHemi = 'S' - end - - if lon > 0 then - lonHemi = 'E' - else - lonHemi = 'W' - end - - lat = math.abs(lat) - lon = math.abs(lon) - - local latDeg = math.floor(lat) - local latMin = (lat - latDeg)*60 - - local lonDeg = math.floor(lon) - local lonMin = (lon - lonDeg)*60 - - if DMS then -- degrees, minutes, and seconds. - local oldLatMin = latMin - latMin = math.floor(latMin) - local latSec = routines.utils.round((oldLatMin - latMin)*60, acc) - - local oldLonMin = lonMin - lonMin = math.floor(lonMin) - local lonSec = routines.utils.round((oldLonMin - lonMin)*60, acc) - - if latSec == 60 then - latSec = 0 - latMin = latMin + 1 - end - - if lonSec == 60 then - lonSec = 0 - lonMin = lonMin + 1 - end - - local secFrmtStr -- create the formatting string for the seconds place - if acc <= 0 then -- no decimal place. - secFrmtStr = '%02d' - else - local width = 3 + acc -- 01.310 - that's a width of 6, for example. - secFrmtStr = '%0' .. width .. '.' .. acc .. 'f' - end - - return string.format('%02d', latDeg) .. ' ' .. string.format('%02d', latMin) .. '\' ' .. string.format(secFrmtStr, latSec) .. '"' .. latHemi .. ' ' - .. string.format('%02d', lonDeg) .. ' ' .. string.format('%02d', lonMin) .. '\' ' .. string.format(secFrmtStr, lonSec) .. '"' .. lonHemi - - else -- degrees, decimal minutes. - latMin = routines.utils.round(latMin, acc) - lonMin = routines.utils.round(lonMin, acc) - - if latMin == 60 then - latMin = 0 - latDeg = latDeg + 1 - end - - if lonMin == 60 then - lonMin = 0 - lonDeg = lonDeg + 1 - end - - local minFrmtStr -- create the formatting string for the minutes place - if acc <= 0 then -- no decimal place. - minFrmtStr = '%02d' - else - local width = 3 + acc -- 01.310 - that's a width of 6, for example. - minFrmtStr = '%0' .. width .. '.' .. acc .. 'f' - end - - return string.format('%02d', latDeg) .. ' ' .. string.format(minFrmtStr, latMin) .. '\'' .. latHemi .. ' ' - .. string.format('%02d', lonDeg) .. ' ' .. string.format(minFrmtStr, lonMin) .. '\'' .. lonHemi - - end -end - ---[[ required: az - radian - required: dist - meters - optional: alt - meters (set to false or nil if you don't want to use it). - optional: metric - set true to get dist and alt in km and m. - precision will always be nearest degree and NM or km.]] -routines.tostringBR = function(az, dist, alt, metric) - az = routines.utils.round(routines.utils.toDegree(az), 0) - - if metric then - dist = routines.utils.round(dist/1000, 2) - else - dist = routines.utils.round(routines.utils.metersToNM(dist), 2) - end - - local s = string.format('%03d', az) .. ' for ' .. dist - - if alt then - if metric then - s = s .. ' at ' .. routines.utils.round(alt, 0) - else - s = s .. ' at ' .. routines.utils.round(routines.utils.metersToFeet(alt), 0) - end - end - return s -end - -routines.getNorthCorrection = function(point) --gets the correction needed for true north - if not point.z then --Vec2; convert to Vec3 - point.z = point.y - point.y = 0 - end - local lat, lon = coord.LOtoLL(point) - local north_posit = coord.LLtoLO(lat + 1, lon) - return math.atan2(north_posit.z - point.z, north_posit.x - point.x) -end - - --- the main area -do - -- THE MAIN FUNCTION -- Accessed 100 times/sec. - routines.main = function() - timer.scheduleFunction(routines.main, {}, timer.getTime() + 2) --reschedule first in case of Lua error - ---------------------------------------------------------------------------------------------------------- - --area to add new stuff in - - routines.do_scheduled_functions() - end -- end of routines.main - - timer.scheduleFunction(routines.main, {}, timer.getTime() + 2) - -end - - -do - local idNum = 0 - - --Simplified event handler - routines.addEventHandler = function(f) --id is optional! - local handler = {} - idNum = idNum + 1 - handler.id = idNum - handler.f = f - handler.onEvent = function(self, event) - self.f(event) - end - world.addEventHandler(handler) - end - - routines.removeEventHandler = function(id) - for key, handler in pairs(world.eventHandlers) do - if handler.id and handler.id == id then - world.eventHandlers[key] = nil - return true - end - end - return false - end -end - --- need to return a Vec3 or Vec2? -function routines.getRandPointInCircle(point, radius, innerRadius) - local theta = 2*math.pi*math.random() - local rad = math.random() + math.random() - if rad > 1 then - rad = 2 - rad - end - - local radMult - if innerRadius and innerRadius <= radius then - radMult = (radius - innerRadius)*rad + innerRadius - else - radMult = radius*rad - end - - if not point.z then --might as well work with vec2/3 - point.z = point.y - end - - local rndCoord - if radius > 0 then - rndCoord = {x = math.cos(theta)*radMult + point.x, y = math.sin(theta)*radMult + point.z} - else - rndCoord = {x = point.x, y = point.z} - end - return rndCoord -end - -routines.goRoute = function(group, path) - local misTask = { - id = 'Mission', - params = { - route = { - points = routines.utils.deepCopy(path), - }, - }, - } - if type(group) == 'string' then - group = Group.getByName(group) - end - local groupCon = group:getController() - if groupCon then - groupCon:setTask(misTask) - return true - end - - Controller.setTask(groupCon, misTask) - return false -end - - --- Useful atomic functions from mist, ported. - -routines.ground = {} -routines.fixedWing = {} -routines.heli = {} - -routines.ground.buildWP = function(point, overRideForm, overRideSpeed) - - local wp = {} - wp.x = point.x - - if point.z then - wp.y = point.z - else - wp.y = point.y - end - local form, speed - - if point.speed and not overRideSpeed then - wp.speed = point.speed - elseif type(overRideSpeed) == 'number' then - wp.speed = overRideSpeed - else - wp.speed = routines.utils.kmphToMps(20) - end - - if point.form and not overRideForm then - form = point.form - else - form = overRideForm - end - - if not form then - wp.action = 'Cone' - else - form = string.lower(form) - if form == 'off_road' or form == 'off road' then - wp.action = 'Off Road' - elseif form == 'on_road' or form == 'on road' then - wp.action = 'On Road' - elseif form == 'rank' or form == 'line_abrest' or form == 'line abrest' or form == 'lineabrest'then - wp.action = 'Rank' - elseif form == 'cone' then - wp.action = 'Cone' - elseif form == 'diamond' then - wp.action = 'Diamond' - elseif form == 'vee' then - wp.action = 'Vee' - elseif form == 'echelon_left' or form == 'echelon left' or form == 'echelonl' then - wp.action = 'EchelonL' - elseif form == 'echelon_right' or form == 'echelon right' or form == 'echelonr' then - wp.action = 'EchelonR' - else - wp.action = 'Cone' -- if nothing matched - end - end - - wp.type = 'Turning Point' - - return wp - -end - -routines.fixedWing.buildWP = function(point, WPtype, speed, alt, altType) - - local wp = {} - wp.x = point.x - - if point.z then - wp.y = point.z - else - wp.y = point.y - end - - if alt and type(alt) == 'number' then - wp.alt = alt - else - wp.alt = 2000 - end - - if altType then - altType = string.lower(altType) - if altType == 'radio' or 'agl' then - wp.alt_type = 'RADIO' - elseif altType == 'baro' or 'asl' then - wp.alt_type = 'BARO' - end - else - wp.alt_type = 'RADIO' - end - - if point.speed then - speed = point.speed - end - - if point.type then - WPtype = point.type - end - - if not speed then - wp.speed = routines.utils.kmphToMps(500) - else - wp.speed = speed - end - - if not WPtype then - wp.action = 'Turning Point' - else - WPtype = string.lower(WPtype) - if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then - wp.action = 'Fly Over Point' - elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then - wp.action = 'Turning Point' - else - wp.action = 'Turning Point' - end - end - - wp.type = 'Turning Point' - return wp -end - -routines.heli.buildWP = function(point, WPtype, speed, alt, altType) - - local wp = {} - wp.x = point.x - - if point.z then - wp.y = point.z - else - wp.y = point.y - end - - if alt and type(alt) == 'number' then - wp.alt = alt - else - wp.alt = 500 - end - - if altType then - altType = string.lower(altType) - if altType == 'radio' or 'agl' then - wp.alt_type = 'RADIO' - elseif altType == 'baro' or 'asl' then - wp.alt_type = 'BARO' - end - else - wp.alt_type = 'RADIO' - end - - if point.speed then - speed = point.speed - end - - if point.type then - WPtype = point.type - end - - if not speed then - wp.speed = routines.utils.kmphToMps(200) - else - wp.speed = speed - end - - if not WPtype then - wp.action = 'Turning Point' - else - WPtype = string.lower(WPtype) - if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then - wp.action = 'Fly Over Point' - elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then - wp.action = 'Turning Point' - else - wp.action = 'Turning Point' - end - end - - wp.type = 'Turning Point' - return wp -end - -routines.groupToRandomPoint = function(vars) - local group = vars.group --Required - local point = vars.point --required - local radius = vars.radius or 0 - local innerRadius = vars.innerRadius - local form = vars.form or 'Cone' - local heading = vars.heading or math.random()*2*math.pi - local headingDegrees = vars.headingDegrees - local speed = vars.speed or routines.utils.kmphToMps(20) - - - local useRoads - if not vars.disableRoads then - useRoads = true - else - useRoads = false - end - - local path = {} - - if headingDegrees then - heading = headingDegrees*math.pi/180 - end - - if heading >= 2*math.pi then - heading = heading - 2*math.pi - end - - local rndCoord = routines.getRandPointInCircle(point, radius, innerRadius) - - local offset = {} - local posStart = routines.getLeadPos(group) - - offset.x = routines.utils.round(math.sin(heading - (math.pi/2)) * 50 + rndCoord.x, 3) - offset.z = routines.utils.round(math.cos(heading + (math.pi/2)) * 50 + rndCoord.y, 3) - path[#path + 1] = routines.ground.buildWP(posStart, form, speed) - - - if useRoads == true and ((point.x - posStart.x)^2 + (point.z - posStart.z)^2)^0.5 > radius * 1.3 then - path[#path + 1] = routines.ground.buildWP({['x'] = posStart.x + 11, ['z'] = posStart.z + 11}, 'off_road', speed) - path[#path + 1] = routines.ground.buildWP(posStart, 'on_road', speed) - path[#path + 1] = routines.ground.buildWP(offset, 'on_road', speed) - else - path[#path + 1] = routines.ground.buildWP({['x'] = posStart.x + 25, ['z'] = posStart.z + 25}, form, speed) - end - - path[#path + 1] = routines.ground.buildWP(offset, form, speed) - path[#path + 1] = routines.ground.buildWP(rndCoord, form, speed) - - routines.goRoute(group, path) - - return -end - -routines.groupRandomDistSelf = function(gpData, dist, form, heading, speed) - local pos = routines.getLeadPos(gpData) - local fakeZone = {} - fakeZone.radius = dist or math.random(300, 1000) - fakeZone.point = {x = pos.x, y, pos.y, z = pos.z} - routines.groupToRandomZone(gpData, fakeZone, form, heading, speed) - - return -end - -routines.groupToRandomZone = function(gpData, zone, form, heading, speed) - if type(gpData) == 'string' then - gpData = Group.getByName(gpData) - end - - if type(zone) == 'string' then - zone = trigger.misc.getZone(zone) - elseif type(zone) == 'table' and not zone.radius then - zone = trigger.misc.getZone(zone[math.random(1, #zone)]) - end - - if speed then - speed = routines.utils.kmphToMps(speed) - end - - local vars = {} - vars.group = gpData - vars.radius = zone.radius - vars.form = form - vars.headingDegrees = heading - vars.speed = speed - vars.point = routines.utils.zoneToVec3(zone) - - routines.groupToRandomPoint(vars) - - return -end - -routines.isTerrainValid = function(coord, terrainTypes) -- vec2/3 and enum or table of acceptable terrain types - if coord.z then - coord.y = coord.z - end - local typeConverted = {} - - if type(terrainTypes) == 'string' then -- if its a string it does this check - for constId, constData in pairs(land.SurfaceType) do - if string.lower(constId) == string.lower(terrainTypes) or string.lower(constData) == string.lower(terrainTypes) then - table.insert(typeConverted, constId) - end - end - elseif type(terrainTypes) == 'table' then -- if its a table it does this check - for typeId, typeData in pairs(terrainTypes) do - for constId, constData in pairs(land.SurfaceType) do - if string.lower(constId) == string.lower(typeData) or string.lower(constData) == string.lower(typeId) then - table.insert(typeConverted, constId) - end - end - end - end - for validIndex, validData in pairs(typeConverted) do - if land.getSurfaceType(coord) == land.SurfaceType[validData] then - return true - end - end - return false -end - -routines.groupToPoint = function(gpData, point, form, heading, speed, useRoads) - if type(point) == 'string' then - point = trigger.misc.getZone(point) - end - if speed then - speed = routines.utils.kmphToMps(speed) - end - - local vars = {} - vars.group = gpData - vars.form = form - vars.headingDegrees = heading - vars.speed = speed - vars.disableRoads = useRoads - vars.point = routines.utils.zoneToVec3(point) - routines.groupToRandomPoint(vars) - - return -end - - -routines.getLeadPos = function(group) - if type(group) == 'string' then -- group name - group = Group.getByName(group) - end - - local units = group:getUnits() - - local leader = units[1] - if not leader then -- SHOULD be good, but if there is a bug, this code future-proofs it then. - local lowestInd = math.huge - for ind, unit in pairs(units) do - if ind < lowestInd then - lowestInd = ind - leader = unit - end - end - end - if leader and Unit.isExist(leader) then -- maybe a little too paranoid now... - return leader:getPosition().p - end -end - ---[[ vars for routines.getMGRSString: -vars.units - table of unit names (NOT unitNameTable- maybe this should change). -vars.acc - integer between 0 and 5, inclusive -]] -routines.getMGRSString = function(vars) - local units = vars.units - local acc = vars.acc or 5 - local avgPos = routines.getAvgPos(units) - if avgPos then - return routines.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(avgPos)), acc) - end -end - ---[[ vars for routines.getLLString -vars.units - table of unit names (NOT unitNameTable- maybe this should change). -vars.acc - integer, number of numbers after decimal place -vars.DMS - if true, output in degrees, minutes, seconds. Otherwise, output in degrees, minutes. - - -]] -routines.getLLString = function(vars) - local units = vars.units - local acc = vars.acc or 3 - local DMS = vars.DMS - local avgPos = routines.getAvgPos(units) - if avgPos then - local lat, lon = coord.LOtoLL(avgPos) - return routines.tostringLL(lat, lon, acc, DMS) - end -end - ---[[ -vars.zone - table of a zone name. -vars.ref - vec3 ref point, maybe overload for vec2 as well? -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -]] -routines.getBRStringZone = function(vars) - local zone = trigger.misc.getZone( vars.zone ) - local ref = routines.utils.makeVec3(vars.ref, 0) -- turn it into Vec3 if it is not already. - local alt = vars.alt - local metric = vars.metric - if zone then - local vec = {x = zone.point.x - ref.x, y = zone.point.y - ref.y, z = zone.point.z - ref.z} - local dir = routines.utils.getDir(vec, ref) - local dist = routines.utils.get2DDist(zone.point, ref) - if alt then - alt = zone.y - end - return routines.tostringBR(dir, dist, alt, metric) - else - env.info( 'routines.getBRStringZone: error: zone is nil' ) - end -end - ---[[ -vars.units- table of unit names (NOT unitNameTable- maybe this should change). -vars.ref - vec3 ref point, maybe overload for vec2 as well? -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -]] -routines.getBRString = function(vars) - local units = vars.units - local ref = routines.utils.makeVec3(vars.ref, 0) -- turn it into Vec3 if it is not already. - local alt = vars.alt - local metric = vars.metric - local avgPos = routines.getAvgPos(units) - if avgPos then - local vec = {x = avgPos.x - ref.x, y = avgPos.y - ref.y, z = avgPos.z - ref.z} - local dir = routines.utils.getDir(vec, ref) - local dist = routines.utils.get2DDist(avgPos, ref) - if alt then - alt = avgPos.y - end - return routines.tostringBR(dir, dist, alt, metric) - end -end - - --- Returns the Vec3 coordinates of the average position of the concentration of units most in the heading direction. ---[[ vars for routines.getLeadingPos: -vars.units - table of unit names -vars.heading - direction -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees -]] -routines.getLeadingPos = function(vars) - local units = vars.units - local heading = vars.heading - local radius = vars.radius - if vars.headingDegrees then - heading = routines.utils.toRadian(vars.headingDegrees) - end - - local unitPosTbl = {} - for i = 1, #units do - local unit = Unit.getByName(units[i]) - if unit and unit:isExist() then - unitPosTbl[#unitPosTbl + 1] = unit:getPosition().p - end - end - if #unitPosTbl > 0 then -- one more more units found. - -- first, find the unit most in the heading direction - local maxPos = -math.huge - - local maxPosInd -- maxPos - the furthest in direction defined by heading; maxPosInd = - for i = 1, #unitPosTbl do - local rotatedVec2 = routines.vec.rotateVec2(routines.utils.makeVec2(unitPosTbl[i]), heading) - if (not maxPos) or maxPos < rotatedVec2.x then - maxPos = rotatedVec2.x - maxPosInd = i - end - end - - --now, get all the units around this unit... - local avgPos - if radius then - local maxUnitPos = unitPosTbl[maxPosInd] - local avgx, avgy, avgz, totNum = 0, 0, 0, 0 - for i = 1, #unitPosTbl do - if routines.utils.get2DDist(maxUnitPos, unitPosTbl[i]) <= radius then - avgx = avgx + unitPosTbl[i].x - avgy = avgy + unitPosTbl[i].y - avgz = avgz + unitPosTbl[i].z - totNum = totNum + 1 - end - end - avgPos = { x = avgx/totNum, y = avgy/totNum, z = avgz/totNum} - else - avgPos = unitPosTbl[maxPosInd] - end - - return avgPos - end -end - - ---[[ vars for routines.getLeadingMGRSString: -vars.units - table of unit names -vars.heading - direction -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees -vars.acc - number, 0 to 5. -]] -routines.getLeadingMGRSString = function(vars) - local pos = routines.getLeadingPos(vars) - if pos then - local acc = vars.acc or 5 - return routines.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(pos)), acc) - end -end - ---[[ vars for routines.getLeadingLLString: -vars.units - table of unit names -vars.heading - direction, number -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees -vars.acc - number of digits after decimal point (can be negative) -vars.DMS - boolean, true if you want DMS. -]] -routines.getLeadingLLString = function(vars) - local pos = routines.getLeadingPos(vars) - if pos then - local acc = vars.acc or 3 - local DMS = vars.DMS - local lat, lon = coord.LOtoLL(pos) - return routines.tostringLL(lat, lon, acc, DMS) - end -end - - - ---[[ vars for routines.getLeadingBRString: -vars.units - table of unit names -vars.heading - direction, number -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees -vars.metric - boolean, if true, use km instead of NM. -vars.alt - boolean, if true, include altitude. -vars.ref - vec3/vec2 reference point. -]] -routines.getLeadingBRString = function(vars) - local pos = routines.getLeadingPos(vars) - if pos then - local ref = vars.ref - local alt = vars.alt - local metric = vars.metric - - local vec = {x = pos.x - ref.x, y = pos.y - ref.y, z = pos.z - ref.z} - local dir = routines.utils.getDir(vec, ref) - local dist = routines.utils.get2DDist(pos, ref) - if alt then - alt = pos.y - end - return routines.tostringBR(dir, dist, alt, metric) - end -end - ---[[ vars for routines.message.add - vars.text = 'Hello World' - vars.displayTime = 20 - vars.msgFor = {coa = {'red'}, countries = {'Ukraine', 'Georgia'}, unitTypes = {'A-10C'}} - -]] - ---[[ vars for routines.msgMGRS -vars.units - table of unit names (NOT unitNameTable- maybe this should change). -vars.acc - integer between 0 and 5, inclusive -vars.text - text in the message -vars.displayTime - self explanatory -vars.msgFor - scope -]] -routines.msgMGRS = function(vars) - local units = vars.units - local acc = vars.acc - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getMGRSString{units = units, acc = acc} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } -end - ---[[ vars for routines.msgLL -vars.units - table of unit names (NOT unitNameTable- maybe this should change) (Yes). -vars.acc - integer, number of numbers after decimal place -vars.DMS - if true, output in degrees, minutes, seconds. Otherwise, output in degrees, minutes. -vars.text - text in the message -vars.displayTime - self explanatory -vars.msgFor - scope -]] -routines.msgLL = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local acc = vars.acc - local DMS = vars.DMS - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getLLString{units = units, acc = acc, DMS = DMS} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } - -end - - ---[[ -vars.units- table of unit names (NOT unitNameTable- maybe this should change). -vars.ref - vec3 ref point, maybe overload for vec2 as well? -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] -routines.msgBR = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local ref = vars.ref -- vec2/vec3 will be handled in routines.getBRString - local alt = vars.alt - local metric = vars.metric - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getBRString{units = units, ref = ref, alt = alt, metric = metric} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } - -end - - --------------------------------------------------------------------------------------------- --- basically, just sub-types of routines.msgBR... saves folks the work of getting the ref point. ---[[ -vars.units- table of unit names (NOT unitNameTable- maybe this should change). -vars.ref - string red, blue -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] -routines.msgBullseye = function(vars) - if string.lower(vars.ref) == 'red' then - vars.ref = routines.DBs.missionData.bullseye.red - routines.msgBR(vars) - elseif string.lower(vars.ref) == 'blue' then - vars.ref = routines.DBs.missionData.bullseye.blue - routines.msgBR(vars) - end -end - ---[[ -vars.units- table of unit names (NOT unitNameTable- maybe this should change). -vars.ref - unit name of reference point -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] - -routines.msgBRA = function(vars) - if Unit.getByName(vars.ref) then - vars.ref = Unit.getByName(vars.ref):getPosition().p - if not vars.alt then - vars.alt = true - end - routines.msgBR(vars) - end -end --------------------------------------------------------------------------------------------- - ---[[ vars for routines.msgLeadingMGRS: -vars.units - table of unit names -vars.heading - direction -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees (optional) -vars.acc - number, 0 to 5. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] -routines.msgLeadingMGRS = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local heading = vars.heading - local radius = vars.radius - local headingDegrees = vars.headingDegrees - local acc = vars.acc - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getLeadingMGRSString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } - - -end ---[[ vars for routines.msgLeadingLL: -vars.units - table of unit names -vars.heading - direction, number -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees (optional) -vars.acc - number of digits after decimal point (can be negative) -vars.DMS - boolean, true if you want DMS. (optional) -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] -routines.msgLeadingLL = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local heading = vars.heading - local radius = vars.radius - local headingDegrees = vars.headingDegrees - local acc = vars.acc - local DMS = vars.DMS - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getLeadingLLString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc, DMS = DMS} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } - -end - ---[[ -vars.units - table of unit names -vars.heading - direction, number -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees (optional) -vars.metric - boolean, if true, use km instead of NM. (optional) -vars.alt - boolean, if true, include altitude. (optional) -vars.ref - vec3/vec2 reference point. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] -routines.msgLeadingBR = function(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local heading = vars.heading - local radius = vars.radius - local headingDegrees = vars.headingDegrees - local metric = vars.metric - local alt = vars.alt - local ref = vars.ref -- vec2/vec3 will be handled in routines.getBRString - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = routines.getLeadingBRString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, metric = metric, alt = alt, ref = ref} - local newText - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else -- else, just append to the end. - newText = text .. s - end - - routines.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } -end - - -function spairs(t, order) - -- collect the keys - local keys = {} - for k in pairs(t) do keys[#keys+1] = k end - - -- if order function given, sort by it by passing the table and keys a, b, - -- otherwise just sort the keys - if order then - table.sort(keys, function(a,b) return order(t, a, b) end) - else - table.sort(keys) - end - - -- return the iterator function - local i = 0 - return function() - i = i + 1 - if keys[i] then - return keys[i], t[keys[i]] - end - end -end - - -function routines.IsPartOfGroupInZones( CargoGroup, LandingZones ) ---trace.f() - - local CurrentZoneID = nil - - if CargoGroup then - local CargoUnits = CargoGroup:getUnits() - for CargoUnitID, CargoUnit in pairs( CargoUnits ) do - if CargoUnit and CargoUnit:getLife() >= 1.0 then - CurrentZoneID = routines.IsUnitInZones( CargoUnit, LandingZones ) - if CurrentZoneID then - break - end - end - end - end - ---trace.r( "", "", { CurrentZoneID } ) - return CurrentZoneID -end - - - -function routines.IsUnitInZones( TransportUnit, LandingZones ) ---trace.f("", "routines.IsUnitInZones" ) - - local TransportZoneResult = nil - local TransportZonePos = nil - local TransportZone = nil - - -- fill-up some local variables to support further calculations to determine location of units within the zone. - if TransportUnit then - local TransportUnitPos = TransportUnit:getPosition().p - if type( LandingZones ) == "table" then - for LandingZoneID, LandingZoneName in pairs( LandingZones ) do - TransportZone = trigger.misc.getZone( LandingZoneName ) - if TransportZone then - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then - TransportZoneResult = LandingZoneID - break - end - end - end - else - TransportZone = trigger.misc.getZone( LandingZones ) - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then - TransportZoneResult = 1 - end - end - if TransportZoneResult then - --trace.i( "routines", "TransportZone:" .. TransportZoneResult ) - else - --trace.i( "routines", "TransportZone:nil logic" ) - end - return TransportZoneResult - else - --trace.i( "routines", "TransportZone:nil hard" ) - return nil - end -end - -function routines.IsUnitNearZonesRadius( TransportUnit, LandingZones, ZoneRadius ) ---trace.f("", "routines.IsUnitInZones" ) - - local TransportZoneResult = nil - local TransportZonePos = nil - local TransportZone = nil - - -- fill-up some local variables to support further calculations to determine location of units within the zone. - if TransportUnit then - local TransportUnitPos = TransportUnit:getPosition().p - if type( LandingZones ) == "table" then - for LandingZoneID, LandingZoneName in pairs( LandingZones ) do - TransportZone = trigger.misc.getZone( LandingZoneName ) - if TransportZone then - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= ZoneRadius ) then - TransportZoneResult = LandingZoneID - break - end - end - end - else - TransportZone = trigger.misc.getZone( LandingZones ) - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= ZoneRadius ) then - TransportZoneResult = 1 - end - end - if TransportZoneResult then - --trace.i( "routines", "TransportZone:" .. TransportZoneResult ) - else - --trace.i( "routines", "TransportZone:nil logic" ) - end - return TransportZoneResult - else - --trace.i( "routines", "TransportZone:nil hard" ) - return nil - end -end - - -function routines.IsStaticInZones( TransportStatic, LandingZones ) ---trace.f() - - local TransportZoneResult = nil - local TransportZonePos = nil - local TransportZone = nil - - -- fill-up some local variables to support further calculations to determine location of units within the zone. - local TransportStaticPos = TransportStatic:getPosition().p - if type( LandingZones ) == "table" then - for LandingZoneID, LandingZoneName in pairs( LandingZones ) do - TransportZone = trigger.misc.getZone( LandingZoneName ) - if TransportZone then - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportStaticPos.x - TransportZonePos.x)^2 + (TransportStaticPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then - TransportZoneResult = LandingZoneID - break - end - end - end - else - TransportZone = trigger.misc.getZone( LandingZones ) - TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} - if ((( TransportStaticPos.x - TransportZonePos.x)^2 + (TransportStaticPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then - TransportZoneResult = 1 - end - end - ---trace.r( "", "", { TransportZoneResult } ) - return TransportZoneResult -end - - -function routines.IsUnitInRadius( CargoUnit, ReferencePosition, Radius ) ---trace.f() - - local Valid = true - - -- fill-up some local variables to support further calculations to determine location of units within the zone. - local CargoPos = CargoUnit:getPosition().p - local ReferenceP = ReferencePosition.p - - if (((CargoPos.x - ReferenceP.x)^2 + (CargoPos.z - ReferenceP.z)^2)^0.5 <= Radius) then - else - Valid = false - end - - return Valid -end - -function routines.IsPartOfGroupInRadius( CargoGroup, ReferencePosition, Radius ) ---trace.f() - - local Valid = true - - Valid = routines.ValidateGroup( CargoGroup, "CargoGroup", Valid ) - - -- fill-up some local variables to support further calculations to determine location of units within the zone - local CargoUnits = CargoGroup:getUnits() - for CargoUnitId, CargoUnit in pairs( CargoUnits ) do - local CargoUnitPos = CargoUnit:getPosition().p --- env.info( 'routines.IsPartOfGroupInRadius: CargoUnitPos.x = ' .. CargoUnitPos.x .. ' CargoUnitPos.z = ' .. CargoUnitPos.z ) - local ReferenceP = ReferencePosition.p --- env.info( 'routines.IsPartOfGroupInRadius: ReferenceGroupPos.x = ' .. ReferenceGroupPos.x .. ' ReferenceGroupPos.z = ' .. ReferenceGroupPos.z ) - - if ((( CargoUnitPos.x - ReferenceP.x)^2 + (CargoUnitPos.z - ReferenceP.z)^2)^0.5 <= Radius) then - else - Valid = false - break - end - end - - return Valid -end - - -function routines.ValidateString( Variable, VariableName, Valid ) ---trace.f() - - if type( Variable ) == "string" then - if Variable == "" then - error( "routines.ValidateString: error: " .. VariableName .. " must be filled out!" ) - Valid = false - end - else - error( "routines.ValidateString: error: " .. VariableName .. " is not a string." ) - Valid = false - end - ---trace.r( "", "", { Valid } ) - return Valid -end - -function routines.ValidateNumber( Variable, VariableName, Valid ) ---trace.f() - - if type( Variable ) == "number" then - else - error( "routines.ValidateNumber: error: " .. VariableName .. " is not a number." ) - Valid = false - end - ---trace.r( "", "", { Valid } ) - return Valid - -end - -function routines.ValidateGroup( Variable, VariableName, Valid ) ---trace.f() - - if Variable == nil then - error( "routines.ValidateGroup: error: " .. VariableName .. " is a nil value!" ) - Valid = false - end - ---trace.r( "", "", { Valid } ) - return Valid -end - -function routines.ValidateZone( LandingZones, VariableName, Valid ) ---trace.f() - - if LandingZones == nil then - error( "routines.ValidateGroup: error: " .. VariableName .. " is a nil value!" ) - Valid = false - end - - if type( LandingZones ) == "table" then - for LandingZoneID, LandingZoneName in pairs( LandingZones ) do - if trigger.misc.getZone( LandingZoneName ) == nil then - error( "routines.ValidateGroup: error: Zone " .. LandingZoneName .. " does not exist!" ) - Valid = false - break - end - end - else - if trigger.misc.getZone( LandingZones ) == nil then - error( "routines.ValidateGroup: error: Zone " .. LandingZones .. " does not exist!" ) - Valid = false - end - end - ---trace.r( "", "", { Valid } ) - return Valid -end - -function routines.ValidateEnumeration( Variable, VariableName, Enum, Valid ) ---trace.f() - - local ValidVariable = false - - for EnumId, EnumData in pairs( Enum ) do - if Variable == EnumData then - ValidVariable = true - break - end - end - - if ValidVariable then - else - error( 'TransportValidateEnum: " .. VariableName .. " is not a valid type.' .. Variable ) - Valid = false - end - ---trace.r( "", "", { Valid } ) - return Valid -end - -function routines.getGroupRoute(groupIdent, task) -- same as getGroupPoints but returns speed and formation type along with vec2 of point} - -- refactor to search by groupId and allow groupId and groupName as inputs - local gpId = groupIdent - if type(groupIdent) == 'string' and not tonumber(groupIdent) then - gpId = _DATABASE.Templates.Groups[groupIdent].groupId - end - - for coa_name, coa_data in pairs(env.mission.coalition) do - if (coa_name == 'red' or coa_name == 'blue') and type(coa_data) == 'table' then - if coa_data.country then --there is a country table - for cntry_id, cntry_data in pairs(coa_data.country) do - for obj_type_name, obj_type_data in pairs(cntry_data) do - if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" then -- only these types have points - if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! - for group_num, group_data in pairs(obj_type_data.group) do - if group_data and group_data.groupId == gpId then -- this is the group we are looking for - if group_data.route and group_data.route.points and #group_data.route.points > 0 then - local points = {} - - for point_num, point in pairs(group_data.route.points) do - local routeData = {} - if not point.point then - routeData.x = point.x - routeData.y = point.y - else - routeData.point = point.point --it's possible that the ME could move to the point = Vec2 notation. - end - routeData.form = point.action - routeData.speed = point.speed - routeData.alt = point.alt - routeData.alt_type = point.alt_type - routeData.airdromeId = point.airdromeId - routeData.helipadId = point.helipadId - routeData.type = point.type - routeData.action = point.action - if task then - routeData.task = point.task - end - points[point_num] = routeData - end - - return points - end - return - end --if group_data and group_data.name and group_data.name == 'groupname' - end --for group_num, group_data in pairs(obj_type_data.group) do - end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then - end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then - end --for obj_type_name, obj_type_data in pairs(cntry_data) do - end --for cntry_id, cntry_data in pairs(coa_data.country) do - end --if coa_data.country then --there is a country table - end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then - end --for coa_name, coa_data in pairs(mission.coalition) do -end - -routines.ground.patrolRoute = function(vars) - - - local tempRoute = {} - local useRoute = {} - local gpData = vars.gpData - if type(gpData) == 'string' then - gpData = Group.getByName(gpData) - end - - local useGroupRoute - if not vars.useGroupRoute then - useGroupRoute = vars.gpData - else - useGroupRoute = vars.useGroupRoute - end - local routeProvided = false - if not vars.route then - if useGroupRoute then - tempRoute = routines.getGroupRoute(useGroupRoute) - end - else - useRoute = vars.route - local posStart = routines.getLeadPos(gpData) - useRoute[1] = routines.ground.buildWP(posStart, useRoute[1].action, useRoute[1].speed) - routeProvided = true - end - - - local overRideSpeed = vars.speed or 'default' - local pType = vars.pType - local offRoadForm = vars.offRoadForm or 'default' - local onRoadForm = vars.onRoadForm or 'default' - - if routeProvided == false and #tempRoute > 0 then - local posStart = routines.getLeadPos(gpData) - - - useRoute[#useRoute + 1] = routines.ground.buildWP(posStart, offRoadForm, overRideSpeed) - for i = 1, #tempRoute do - local tempForm = tempRoute[i].action - local tempSpeed = tempRoute[i].speed - - if offRoadForm == 'default' then - tempForm = tempRoute[i].action - end - if onRoadForm == 'default' then - onRoadForm = 'On Road' - end - if (string.lower(tempRoute[i].action) == 'on road' or string.lower(tempRoute[i].action) == 'onroad' or string.lower(tempRoute[i].action) == 'on_road') then - tempForm = onRoadForm - 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, "Message", MsgTime, MsgName ):ToCoalition( coalition.side.RED ):ToCoalition( coalition.side.BLUE ) -end - -function MessageToRed( MsgText, MsgTime, MsgName ) ---trace.f() - - MESSAGE:New( MsgText, "To Red Coalition", MsgTime, MsgName ):ToCoalition( coalition.side.RED ) -end - -function MessageToBlue( MsgText, MsgTime, MsgName ) ---trace.f() - - MESSAGE:New( MsgText, "To Blue Coalition", MsgTime, MsgName ):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' )) - ---- BASE classes. --- --- @{#BASE} class --- ============== --- The @{#BASE} class is the super class for most of the classes defined within MOOSE. --- --- It handles: --- --- * The construction and inheritance of child classes. --- * The tracing of objects during mission execution within the DCS.log file (under saved games folder). --- --- Note: Normally you would not use the BASE class unless you are extending the MOOSE framework with new classes. --- --- BASE Trace functionality --- ======================== --- The BASE class contains trace methods to trace progress within a mission execution of a certain object. --- Note that these trace methods are inherited by each MOOSE class interiting BASE. --- As such, each object created from derived class from BASE can use the tracing functions to trace its execution. --- --- Trace a function call --- --------------------- --- There are basically 3 types of tracing methods available within BASE: --- --- * @{#BASE.F}: Trace the beginning of a function and its given parameters. --- * @{#BASE.T}: Trace further logic within a function giving optional variables or parameters. --- * @{#BASE.E}: Trace an execption within a function giving optional variables or parameters. An exception will always be traced. --- --- 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. --- --- BASE Inheritance support --- ======================== --- The following methods are available to support inheritance: --- --- * @{#BASE.Inherit}: Inherits from a class. --- * @{#BASE.Inherited}: Returns the parent class from the class. --- --- Future --- ====== --- Further methods may be added to BASE whenever there is a need to make "overall" functions available within MOOSE. --- --- ==== --- --- @module Base --- @author FlightControl - -Include.File( "Routines" ) - -local _TraceOn = true -local _TraceLevel = 1 -local _TraceClass = { - --DATABASE = true, - --SEAD = true, - --DESTROYBASETASK = true, - --MOVEMENT = true, - --SPAWN = true, - --STAGE = true, - --ZONE = true, - --GROUP = true, - --UNIT = true, - --CLIENT = true, - --CARGO = true, - --CARGO_GROUP = true, - --CARGO_PACKAGE = true, - --CARGO_SLINGLOAD = true, - --CARGO_ZONE = true, - --CLEANUP = true, - --MENU_CLIENT = true, - --MENU_CLIENT_COMMAND = true, - --ESCORT = true, - } -local _TraceClassMethod = {} - ---- The BASE Class --- @type BASE --- @field ClassName The name of the class. --- @field ClassID The ID number of the class. -BASE = { - ClassName = "BASE", - ClassID = 0, - Events = {} -} - ---- The Formation Class --- @type FORMATION --- @field Cone A cone formation. -FORMATION = { - Cone = "Cone" -} - - - ---- The base constructor. This is the top top class of all classed defined within the MOOSE. --- Any new class needs to be derived from this class for proper inheritance. --- @param #BASE self --- @return #BASE The new instance of the BASE class. --- @usage --- function TASK:New() --- --- local self = BASE:Inherit( self, BASE:New() ) --- --- -- assign Task default values during construction --- self.TaskBriefing = "Task: No Task." --- self.Time = timer.getTime() --- self.ExecuteStage = _TransportExecuteStage.NONE --- --- return self --- end --- @todo need to investigate if the deepCopy is really needed... Don't think so. -function BASE:New() - local Child = routines.utils.deepCopy( self ) - local Parent = {} - setmetatable( Child, Parent ) - Child.__index = Child - self.ClassID = self.ClassID + 1 - Child.ClassID = self.ClassID - --Child.AddEvent( Child, S_EVENT_BIRTH, Child.EventBirth ) - return Child -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 ) - if Child ~= nil then - setmetatable( Child, Parent ) - Child.__index = Child - end - --Child.ClassName = Child.ClassName .. '.' .. Child.ClassID - self:T( 'Inherited from ' .. Parent.ClassName ) - return Child -end - ---- This is the worker method to retrieve the Parent class. --- @param #BASE self --- @param #BASE Child is the Child class from which the Parent class needs to be retrieved. --- @return #BASE -function BASE:Inherited( Child ) - local Parent = getmetatable( Child ) --- env.info('Inherited class of ' .. Child.ClassName .. ' is ' .. Parent.ClassName ) - return Parent -end - ---- Get the ClassName + ClassID of the class instance. --- The ClassName + ClassID is formatted as '%s#%09d'. --- @param #BASE self --- @return #string The ClassName + ClassID of the class instance. -function BASE:GetClassNameAndID() - return string.format( '%s#%09d', self:GetClassName(), self:GetClassID() ) -end - ---- Get the ClassName of the class instance. --- @param #BASE self --- @return #string The ClassName of the class instance. -function BASE:GetClassName() - return self.ClassName -end - ---- Get the ClassID of the class instance. --- @param #BASE self --- @return #string The ClassID of the class instance. -function BASE:GetClassID() - return self.ClassID -end - ---- Set a new listener for the class. --- @param self --- @param DCSTypes#Event Event --- @param #function EventFunction --- @return #BASE -function BASE:AddEvent( Event, EventFunction ) - self:F( Event ) - - self.Events[#self.Events+1] = {} - self.Events[#self.Events].Event = Event - self.Events[#self.Events].EventFunction = EventFunction - self.Events[#self.Events].EventEnabled = false - - return self -end - ---- Returns the event dispatcher --- @param #BASE self --- @return Event#EVENT -function BASE:Event() - - return _EVENTDISPATCHER -end - - - - - ---- Enable the event listeners for the class. --- @param #BASE self --- @return #BASE -function BASE:EnableEvents() - self:F( #self.Events ) - - for EventID, Event in pairs( self.Events ) do - Event.Self = self - Event.EventEnabled = true - end - self.Events.Handler = world.addEventHandler( self ) - - return self -end - - ---- Disable the event listeners for the class. --- @param #BASE self --- @return #BASE -function BASE:DisableEvents() - self:F() - - world.removeEventHandler( self ) - for EventID, Event in pairs( self.Events ) do - Event.Self = nil - Event.EventEnabled = false - end - - return self -end - - -local BaseEventCodes = { - "S_EVENT_SHOT", - "S_EVENT_HIT", - "S_EVENT_TAKEOFF", - "S_EVENT_LAND", - "S_EVENT_CRASH", - "S_EVENT_EJECTION", - "S_EVENT_REFUELING", - "S_EVENT_DEAD", - "S_EVENT_PILOT_DEAD", - "S_EVENT_BASE_CAPTURED", - "S_EVENT_MISSION_START", - "S_EVENT_MISSION_END", - "S_EVENT_TOOK_CONTROL", - "S_EVENT_REFUELING_STOP", - "S_EVENT_BIRTH", - "S_EVENT_HUMAN_FAILURE", - "S_EVENT_ENGINE_STARTUP", - "S_EVENT_ENGINE_SHUTDOWN", - "S_EVENT_PLAYER_ENTER_UNIT", - "S_EVENT_PLAYER_LEAVE_UNIT", - "S_EVENT_PLAYER_COMMENT", - "S_EVENT_SHOOTING_START", - "S_EVENT_SHOOTING_END", - "S_EVENT_MAX", -} - ---onEvent( {[1]="S_EVENT_BIRTH",[2]={["subPlace"]=5,["time"]=0,["initiator"]={["id_"]=16884480,},["place"]={["id_"]=5000040,},["id"]=15,["IniUnitName"]="US F-15C@RAMP-Air Support Mountains#001-01",},} --- Event = { --- id = enum world.event, --- time = Time, --- initiator = Unit, --- target = Unit, --- place = Unit, --- subPlace = enum world.BirthPlace, --- weapon = Weapon --- } - ---- Creation of a Birth Event. --- @param #BASE self --- @param DCSTypes#Time EventTime The time stamp of the event. --- @param DCSObject#Object Initiator The initiating object of the event. --- @param #string IniUnitName The initiating unit name. --- @param place --- @param subplace -function BASE:CreateEventBirth( EventTime, Initiator, IniUnitName, place, subplace ) - self:F( { EventTime, Initiator, IniUnitName, place, subplace } ) - - local Event = { - id = world.event.S_EVENT_BIRTH, - time = EventTime, - initiator = Initiator, - IniUnitName = IniUnitName, - place = place, - subplace = subplace - } - - world.onEvent( Event ) -end - ---- Creation of a Crash Event. --- @param #BASE self --- @param DCSTypes#Time EventTime The time stamp of the event. --- @param DCSObject#Object Initiator The initiating object of the event. -function BASE:CreateEventCrash( EventTime, Initiator ) - self:F( { EventTime, Initiator } ) - - local Event = { - id = world.event.S_EVENT_CRASH, - time = EventTime, - initiator = Initiator, - } - - world.onEvent( Event ) -end - --- TODO: Complete DCSTypes#Event structure. ---- The main event handling function... This function captures all events generated for the class. --- @param #BASE self --- @param DCSTypes#Event event -function BASE:onEvent(event) - --self:F( { BaseEventCodes[event.id], event } ) - - if self then - for EventID, EventObject in pairs( self.Events ) do - if EventObject.EventEnabled then - --env.info( 'onEvent Table EventObject.Self = ' .. tostring(EventObject.Self) ) - --env.info( 'onEvent event.id = ' .. tostring(event.id) ) - --env.info( 'onEvent EventObject.Event = ' .. tostring(EventObject.Event) ) - if event.id == EventObject.Event then - if self == EventObject.Self then - if event.initiator and event.initiator:isExist() then - event.IniUnitName = event.initiator:getName() - end - if event.target and event.target:isExist() then - event.TgtUnitName = event.target:getName() - end - --self:T( { BaseEventCodes[event.id], event } ) - --EventObject.EventFunction( self, event ) - end - end - end - end - end -end - --- Trace section - --- Log a trace (only shown when trace is on) --- TODO: Make trace function using variable parameters. - ---- Set trace level --- @param #BASE self --- @param #number Level -function BASE:TraceLevel( Level ) - _TraceLevel = Level - self:E( "Tracing level " .. Level ) -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. Must be at the beginning of the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:F( Arguments ) - - if _TraceOn and ( _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName] ) then - - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - local Function = "function" - if DebugInfoCurrent.name then - Function = DebugInfoCurrent.name - end - - if _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName].Method[Function] then - local LineCurrent = DebugInfoCurrent.currentline - 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 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 _TraceLevel >= 2 then - self:F( Arguments ) - 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 _TraceLevel >= 3 then - self:F( Arguments ) - end - -end - ---- Trace a function logic. Can be anywhere within the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:T( Arguments ) - - if _TraceOn and ( _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName] ) then - - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - local Function = "function" - if DebugInfoCurrent.name then - Function = DebugInfoCurrent.name - end - - if _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName].Method[Function] then - local LineCurrent = DebugInfoCurrent.currentline - 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 2. Can be anywhere within the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:T2( Arguments ) - - if _TraceLevel >= 2 then - self:T( Arguments ) - 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 _TraceLevel >= 3 then - self:T( Arguments ) - end - -end - ---- Log an exception which will be traced always. Can be anywhere within the function logic. --- @param #BASE self --- @param Arguments A #table or any field. -function BASE:E( Arguments ) - - local DebugInfoCurrent = debug.getinfo( 2, "nl" ) - local DebugInfoFrom = debug.getinfo( 3, "l" ) - - local Function = "function" - if DebugInfoCurrent.name then - Function = DebugInfoCurrent.name - end - - local LineCurrent = DebugInfoCurrent.currentline - local LineFrom = DebugInfoFrom.currentline - - env.info( string.format( "%6d(%6d)/%1s:%20s%05d.%s(%s)" , LineCurrent, LineFrom, "E", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) -end - - - ---- Models time events calling event handing functions. --- --- @{SCHEDULER} class --- =================== --- The @{SCHEDULER} class models time events calling given event handling functions. --- --- SCHEDULER constructor --- ===================== --- The SCHEDULER class is quite easy to use: --- --- * @{#SCHEDULER.New}: Setup a new scheduler and start it with the specified parameters. --- --- SCHEDULER timer methods --- ======================= --- The SCHEDULER can be stopped and restarted with the following methods: --- --- * @{#SCHEDULER.Start}: (Re-)Start the scheduler. --- * @{#SCHEDULER.Start}: Stop the scheduler. --- --- @module Scheduler --- @author FlightControl - -Include.File( "Routines" ) -Include.File( "Base" ) - - ---- The SCHEDULER class --- @type SCHEDULER --- @extends Base#BASE -SCHEDULER = { - ClassName = "SCHEDULER", -} - - ---- Constructor. --- @param #SCHEDULER self --- @param #table TimeEventObject Specified for which Moose object the timer is setup. If a value of nil is provided, a scheduler will be setup without an object reference. --- @param #function TimeEventFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in TimeEventFunctionArguments. --- @param #table TimeEventFunctionArguments Optional arguments that can be given as part of scheduler. The arguments need to be given as a table { param1, param 2, ... }. --- @param #number StartSeconds Specifies the amount of seconds that will be waited before the scheduling is started, and the event function is called. --- @param #number RepeatSecondsInterval Specifies the interval in seconds when the scheduler will call the event function. --- @param #number RandomizationFactor Specifies a randomization factor between 0 and 1 to randomize the RepeatSecondsInterval. --- @param #number StopSeconds Specifies the amount of seconds when the scheduler will be stopped. --- @return #SCHEDULER self -function SCHEDULER:New( TimeEventObject, TimeEventFunction, TimeEventFunctionArguments, StartSeconds, RepeatSecondsInterval, RandomizationFactor, StopSeconds ) - local self = BASE:Inherit( self, BASE:New() ) - self:F2( { TimeEventObject, TimeEventFunction, TimeEventFunctionArguments, StartSeconds, RepeatSecondsInterval, RandomizationFactor, StopSeconds } ) - - self.TimeEventObject = TimeEventObject - self.TimeEventFunction = TimeEventFunction - self.TimeEventFunctionArguments = TimeEventFunctionArguments - self.StartSeconds = StartSeconds - - if RepeatSecondsInterval then - self.RepeatSecondsInterval = RepeatSecondsInterval - else - self.RepeatSecondsInterval = 0 - end - - if RandomizationFactor then - self.RandomizationFactor = RandomizationFactor - else - self.RandomizationFactor = 0 - end - - if StopSeconds then - self.StopSeconds = StopSeconds - end - - self.Repeat = false - - self.StartTime = timer.getTime() - - self:Start() - - return self -end - ---- (Re-)Starts the scheduler. --- @param #SCHEDULER self --- @return #SCHEDULER self -function SCHEDULER:Start() - self:F2( self.TimeEventObject ) - - self.Repeat = true - timer.scheduleFunction( self._Scheduler, self, timer.getTime() + self.StartSeconds + .01 ) - - return self -end - ---- Stops the scheduler. --- @param #SCHEDULER self --- @return #SCHEDULER self -function SCHEDULER:Stop() - self:F2( self.TimeEventObject ) - - self.Repeat = false - - return self -end - --- Private Functions - -function SCHEDULER:_Scheduler() - self:F2( self.TimeEventFunctionArguments ) - - local ErrorHandler = function( errmsg ) - - env.info( "Error in SCHEDULER function:" .. errmsg ) - env.info( debug.traceback() ) - - return errmsg - end - - local Status, Result - if self.TimeEventObject then - Status, Result = xpcall( function() return self.TimeEventFunction( self.TimeEventObject, unpack( self.TimeEventFunctionArguments ) ) end, ErrorHandler ) - else - Status, Result = xpcall( function() return self.TimeEventFunction( unpack( self.TimeEventFunctionArguments ) ) end, ErrorHandler ) - end - - self:T( { Status, Result } ) - - if Status and Status == true and Result and Result == true then - if self.Repeat and ( not self.StopSeconds or ( self.StopSeconds and timer.getTime() <= self.StartTime + self.StopSeconds ) ) then - timer.scheduleFunction( - self._Scheduler, - self, - timer.getTime() + self.RepeatSecondsInterval + math.random( - ( self.RandomizationFactor * self.RepeatSecondsInterval / 2 ), ( self.RandomizationFactor * self.RepeatSecondsInterval / 2 ) ) + 0.01 - ) - end - end - -end - - - - - - - - ---- The EVENT class models an efficient event handling process between other classes and its units, weapons. --- @module Event --- @author FlightControl - -Include.File( "Routines" ) -Include.File( "Base" ) - ---- The EVENT structure --- @type EVENT --- @field #EVENT.Events Events -EVENT = { - ClassName = "EVENT", - ClassID = 0, -} - -local _EVENTCODES = { - "S_EVENT_SHOT", - "S_EVENT_HIT", - "S_EVENT_TAKEOFF", - "S_EVENT_LAND", - "S_EVENT_CRASH", - "S_EVENT_EJECTION", - "S_EVENT_REFUELING", - "S_EVENT_DEAD", - "S_EVENT_PILOT_DEAD", - "S_EVENT_BASE_CAPTURED", - "S_EVENT_MISSION_START", - "S_EVENT_MISSION_END", - "S_EVENT_TOOK_CONTROL", - "S_EVENT_REFUELING_STOP", - "S_EVENT_BIRTH", - "S_EVENT_HUMAN_FAILURE", - "S_EVENT_ENGINE_STARTUP", - "S_EVENT_ENGINE_SHUTDOWN", - "S_EVENT_PLAYER_ENTER_UNIT", - "S_EVENT_PLAYER_LEAVE_UNIT", - "S_EVENT_PLAYER_COMMENT", - "S_EVENT_SHOOTING_START", - "S_EVENT_SHOOTING_END", - "S_EVENT_MAX", -} - ---- The Event structure --- @type EVENTDATA --- @field id --- @field initiator --- @field target --- @field weapon --- @field IniDCSUnit --- @field IniDCSUnitName --- @field IniDCSGroup --- @field IniDCSGroupName --- @field TgtDCSUnit --- @field TgtDCSUnitName --- @field TgtDCSGroup --- @field TgtDCSGroupName --- @field Weapon --- @field WeaponName --- @field WeaponTgtDCSUnit - ---- The Events structure --- @type EVENT.Events --- @field #number IniUnit - -function EVENT:New() - local self = BASE:Inherit( self, BASE:New() ) - self:F() - self.EventHandler = world.addEventHandler( self ) - return self -end - -function EVENT:EventText( EventID ) - - local EventText = _EVENTCODES[EventID] - - return EventText -end - - ---- Initializes the Events structure for the event --- @param #EVENT self --- @param DCSWorld#world.event EventID --- @param #string EventClass --- @return #EVENT.Events -function EVENT:Init( EventID, EventClass ) - self:F3( { _EVENTCODES[EventID], EventClass } ) - if not self.Events[EventID] then - self.Events[EventID] = {} - end - if not self.Events[EventID][EventClass] then - self.Events[EventID][EventClass] = {} - end - return self.Events[EventID][EventClass] -end - - ---- Create an OnDead event handler for a group --- @param #EVENT self --- @param #table EventTemplate --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param EventSelf The self instance of the class for which the event is. --- @param #function OnEventFunction --- @return #EVENT -function EVENT:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, OnEventFunction ) - self:F2( EventTemplate.name ) - - for EventUnitID, EventUnit in pairs( EventTemplate.units ) do - OnEventFunction( self, EventUnit.name, EventFunction, EventSelf ) - end - return self -end - ---- Set a new listener for an S_EVENT_X event independent from a unit or a weapon. --- @param #EVENT self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @param EventID --- @return #EVENT -function EVENT:OnEventGeneric( EventFunction, EventSelf, EventID ) - self:F2( { EventID } ) - - local Event = self:Init( EventID, EventSelf:GetClassNameAndID() ) - Event.EventFunction = EventFunction - Event.EventSelf = EventSelf - return self -end - - ---- Set a new listener for an S_EVENT_X event --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @param EventID --- @return #EVENT -function EVENT:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, EventID ) - self:F2( EventDCSUnitName ) - - local Event = self:Init( EventID, EventSelf:GetClassNameAndID() ) - if not Event.IniUnit then - Event.IniUnit = {} - end - Event.IniUnit[EventDCSUnitName] = {} - Event.IniUnit[EventDCSUnitName].EventFunction = EventFunction - Event.IniUnit[EventDCSUnitName].EventSelf = EventSelf - return self -end - - ---- Create an OnBirth event handler for a group --- @param #EVENT self --- @param Group#GROUP EventGroup --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnBirthForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnBirthForUnit ) - - return self -end - ---- Set a new listener for an S_EVENT_BIRTH event, and registers the unit born. --- @param #EVENT self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf --- @return #EVENT -function EVENT:OnBirth( EventFunction, EventSelf ) - self:F() - - self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_BIRTH ) - - return self -end - ---- Set a new listener for an S_EVENT_BIRTH event. --- @param #EVENT self --- @param #string EventDCSUnitName The id of the unit for the event to be handled. --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf --- @return #EVENT -function EVENT:OnBirthForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_BIRTH ) - - return self -end - ---- Create an OnCrash event handler for a group --- @param #EVENT self --- @param Group#GROUP EventGroup --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnCrashForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnCrashForUnit ) - - return self -end - ---- Set a new listener for an S_EVENT_CRASH event. --- @param #EVENT self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf --- @return #EVENT -function EVENT:OnCrash( EventFunction, EventSelf ) - self:F() - - self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_CRASH ) - - return self -end - ---- Set a new listener for an S_EVENT_CRASH event. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnCrashForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_CRASH ) - - return self -end - ---- Create an OnDead event handler for a group --- @param #EVENT self --- @param Group#GROUP EventGroup --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnDeadForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnDeadForUnit ) - - return self -end - ---- Set a new listener for an S_EVENT_DEAD event. --- @param #EVENT self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf --- @return #EVENT -function EVENT:OnDead( EventFunction, EventSelf ) - self:F() - - self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_DEAD ) - - return self -end - - ---- Set a new listener for an S_EVENT_DEAD event. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnDeadForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_DEAD ) - - return self -end - ---- Set a new listener for an S_EVENT_PILOT_DEAD event. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnPilotDeadForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_PILOT_DEAD ) - - return self -end - ---- Create an OnDead event handler for a group --- @param #EVENT self --- @param #table EventTemplate --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnLandForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnLandForUnit ) - - return self -end - ---- Set a new listener for an S_EVENT_LAND event. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnLandForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_LAND ) - - return self -end - ---- Create an OnDead event handler for a group --- @param #EVENT self --- @param #table EventTemplate --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnTakeOffForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnTakeOffForUnit ) - - return self -end - ---- Set a new listener for an S_EVENT_TAKEOFF event. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnTakeOffForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_TAKEOFF ) - - return self -end - ---- Create an OnDead event handler for a group --- @param #EVENT self --- @param #table EventTemplate --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnEngineShutDownForTemplate( EventTemplate, EventFunction, EventSelf ) - self:F( EventTemplate.name ) - - self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnEngineShutDownForUnit ) - - return self -end - ---- Set a new listener for an S_EVENT_ENGINE_SHUTDOWN event. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnEngineShutDownForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_ENGINE_SHUTDOWN ) - - return self -end - ---- Set a new listener for an S_EVENT_ENGINE_STARTUP event. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnEngineStartUpForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_ENGINE_STARTUP ) - - return self -end - ---- Set a new listener for an S_EVENT_SHOT event. --- @param #EVENT self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnShot( EventFunction, EventSelf ) - self:F() - - self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_SHOT ) - - return self -end - ---- Set a new listener for an S_EVENT_SHOT event for a unit. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnShotForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_SHOT ) - - return self -end - ---- Set a new listener for an S_EVENT_HIT event. --- @param #EVENT self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnHit( EventFunction, EventSelf ) - self:F() - - self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_HIT ) - - return self -end - ---- Set a new listener for an S_EVENT_HIT event. --- @param #EVENT self --- @param #string EventDCSUnitName --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnHitForUnit( EventDCSUnitName, EventFunction, EventSelf ) - self:F( EventDCSUnitName ) - - self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_HIT ) - - return self -end - ---- Set a new listener for an S_EVENT_PLAYER_ENTER_UNIT event. --- @param #EVENT self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnPlayerEnterUnit( EventFunction, EventSelf ) - self:F() - - self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_PLAYER_ENTER_UNIT ) - - return self -end - ---- Set a new listener for an S_EVENT_PLAYER_LEAVE_UNIT event. --- @param #EVENT self --- @param #function EventFunction The function to be called when the event occurs for the unit. --- @param Base#BASE EventSelf The self instance of the class for which the event is. --- @return #EVENT -function EVENT:OnPlayerLeaveUnit( EventFunction, EventSelf ) - self:F() - - self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_PLAYER_LEAVE_UNIT ) - - return self -end - - - -function EVENT:onEvent( Event ) - self:F( { _EVENTCODES[Event.id], Event } ) - - if self and self.Events and self.Events[Event.id] then - if Event.initiator and Event.initiator:getCategory() == Object.Category.UNIT then - Event.IniDCSUnit = Event.initiator - Event.IniDCSGroup = Event.IniDCSUnit:getGroup() - Event.IniDCSUnitName = Event.IniDCSUnit:getName() - Event.IniDCSGroupName = "" - if Event.IniDCSGroup and Event.IniDCSGroup:isExist() then - Event.IniDCSGroupName = Event.IniDCSGroup:getName() - end - end - if Event.target then - if Event.target and Event.target:getCategory() == Object.Category.UNIT then - Event.TgtDCSUnit = Event.target - Event.TgtDCSGroup = Event.TgtDCSUnit:getGroup() - Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() - Event.TgtDCSGroupName = "" - if Event.TgtDCSGroup and Event.TgtDCSGroup:isExist() then - Event.TgtDCSGroupName = Event.TgtDCSGroup:getName() - end - end - end - if Event.weapon then - Event.Weapon = Event.weapon - Event.WeaponName = Event.Weapon:getTypeName() - --Event.WeaponTgtDCSUnit = Event.Weapon:getTarget() - end - self:E( { _EVENTCODES[Event.id], Event } ) - for ClassName, EventData in pairs( self.Events[Event.id] ) do - if Event.IniDCSUnitName and EventData.IniUnit and EventData.IniUnit[Event.IniDCSUnitName] then - self:T2( { "Calling event function for class ", ClassName, " unit ", Event.IniDCSUnitName } ) - EventData.IniUnit[Event.IniDCSUnitName].EventFunction( EventData.IniUnit[Event.IniDCSUnitName].EventSelf, Event ) - else - if Event.IniDCSUnit and not EventData.IniUnit then - self:T2( { "Calling event function for class ", ClassName } ) - EventData.EventFunction( EventData.EventSelf, Event ) - end - end - end - end -end - ---- Encapsulation of DCS World Menu system in a set of MENU classes. --- @module Menu - -Include.File( "Routines" ) -Include.File( "Base" ) - ---- The MENU class --- @type MENU --- @extends Base#BASE -MENU = { - ClassName = "MENU", - MenuPath = nil, - MenuText = "", - MenuParentPath = nil -} - ---- -function MENU:New( MenuText, MenuParentPath ) - - -- Arrange meta tables - local Child = BASE:Inherit( self, BASE:New() ) - - Child.MenuPath = nil - Child.MenuText = MenuText - Child.MenuParentPath = MenuParentPath - return Child -end - ---- The COMMANDMENU class --- @type COMMANDMENU --- @extends Menu#MENU -COMMANDMENU = { - ClassName = "COMMANDMENU", - CommandMenuFunction = nil, - CommandMenuArgument = nil -} - -function COMMANDMENU:New( MenuText, ParentMenu, CommandMenuFunction, CommandMenuArgument ) - - -- Arrange meta tables - - local MenuParentPath = nil - if ParentMenu ~= nil then - MenuParentPath = ParentMenu.MenuPath - end - - local Child = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) - - Child.MenuPath = missionCommands.addCommand( MenuText, MenuParentPath, CommandMenuFunction, CommandMenuArgument ) - Child.CommandMenuFunction = CommandMenuFunction - Child.CommandMenuArgument = CommandMenuArgument - return Child -end - ---- The SUBMENU class --- @type SUBMENU --- @extends Menu#MENU -SUBMENU = { - ClassName = "SUBMENU" -} - -function SUBMENU:New( MenuText, ParentMenu ) - - -- Arrange meta tables - local MenuParentPath = nil - if ParentMenu ~= nil then - MenuParentPath = ParentMenu.MenuPath - end - - local Child = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) - - Child.MenuPath = missionCommands.addSubMenu( MenuText, MenuParentPath ) - return Child -end - --- This local variable is used to cache the menus registered under clients. --- Menus don't dissapear when clients are destroyed and restarted. --- So every menu for a client created must be tracked so that program logic accidentally does not create --- the same menus twice during initialization logic. --- These menu classes are handling this logic with this variable. -local _MENUCLIENTS = {} - ---- The MENU_CLIENT class --- @type MENU_CLIENT --- @extends Menu#MENU -MENU_CLIENT = { - ClassName = "MENU_CLIENT" -} - ---- Creates a new menu item for a group --- @param self --- @param Client#CLIENT MenuClient The Client owning the menu. --- @param #string MenuText The text for the menu. --- @param #table ParentMenu The parent menu. --- @return #MENU_CLIENT self -function MENU_CLIENT:New( MenuClient, MenuText, ParentMenu ) - - -- Arrange meta tables - local MenuParentPath = {} - if ParentMenu ~= nil then - MenuParentPath = ParentMenu.MenuPath - end - - local self = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) - self:F( { MenuClient, MenuText, ParentMenu } ) - - self.MenuClient = MenuClient - self.MenuClientGroupID = MenuClient:GetClientGroupID() - self.MenuParentPath = MenuParentPath - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - self.Menus = {} - - if not _MENUCLIENTS[self.MenuClientGroupID] then - _MENUCLIENTS[self.MenuClientGroupID] = {} - end - - local MenuPath = _MENUCLIENTS[self.MenuClientGroupID] - - self:T( { MenuClient:GetClientGroupName(), MenuPath[table.concat(MenuParentPath)], MenuParentPath, MenuText } ) - - local MenuPathID = table.concat(MenuParentPath) .. "/" .. MenuText - if MenuPath[MenuPathID] then - missionCommands.removeItemForGroup( self.MenuClient:GetClientGroupID(), MenuPath[MenuPathID] ) - end - - self.MenuPath = missionCommands.addSubMenuForGroup( self.MenuClient:GetClientGroupID(), MenuText, MenuParentPath ) - MenuPath[MenuPathID] = self.MenuPath - - self:T( { MenuClient:GetClientGroupName(), self.MenuPath } ) - - if ParentMenu and ParentMenu.Menus then - ParentMenu.Menus[self.MenuPath] = self - end - return self -end - ---- Removes the sub menus recursively of this MENU_CLIENT. --- @param #MENU_CLIENT self --- @return #MENU_CLIENT self -function MENU_CLIENT:RemoveSubMenus() - self:F( self.MenuPath ) - - for MenuID, Menu in pairs( self.Menus ) do - Menu:Remove() - end - -end - ---- Removes the sub menus recursively of this MENU_CLIENT. --- @param #MENU_CLIENT self --- @return #MENU_CLIENT self -function MENU_CLIENT:Remove() - self:F( self.MenuPath ) - - self:RemoveSubMenus() - - if not _MENUCLIENTS[self.MenuClientGroupID] then - _MENUCLIENTS[self.MenuClientGroupID] = {} - end - - local MenuPath = _MENUCLIENTS[self.MenuClientGroupID] - - if MenuPath[table.concat(self.MenuParentPath) .. "/" .. self.MenuText] then - MenuPath[table.concat(self.MenuParentPath) .. "/" .. self.MenuText] = nil - end - - missionCommands.removeItemForGroup( self.MenuClient:GetClientGroupID(), self.MenuPath ) - self.ParentMenu.Menus[self.MenuPath] = nil - return nil -end - - ---- The MENU_CLIENT_COMMAND class --- @type MENU_CLIENT_COMMAND --- @extends Menu#MENU -MENU_CLIENT_COMMAND = { - ClassName = "MENU_CLIENT_COMMAND" -} - ---- Creates a new radio command item for a group --- @param self --- @param Client#CLIENT MenuClient The Client owning the menu. --- @param MenuText The text for the menu. --- @param ParentMenu The parent menu. --- @param CommandMenuFunction A function that is called when the menu key is pressed. --- @param CommandMenuArgument An argument for the function. --- @return Menu#MENU_CLIENT_COMMAND self -function MENU_CLIENT_COMMAND:New( MenuClient, MenuText, ParentMenu, CommandMenuFunction, CommandMenuArgument ) - - -- Arrange meta tables - - local MenuParentPath = {} - if ParentMenu ~= nil then - MenuParentPath = ParentMenu.MenuPath - end - - local self = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) - - self.MenuClient = MenuClient - self.MenuClientGroupID = MenuClient:GetClientGroupID() - self.MenuParentPath = MenuParentPath - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - if not _MENUCLIENTS[self.MenuClientGroupID] then - _MENUCLIENTS[self.MenuClientGroupID] = {} - end - - local MenuPath = _MENUCLIENTS[self.MenuClientGroupID] - - self:T( { MenuClient:GetClientGroupName(), MenuPath[table.concat(MenuParentPath)], MenuParentPath, MenuText, CommandMenuFunction, CommandMenuArgument } ) - - local MenuPathID = table.concat(MenuParentPath) .. "/" .. MenuText - if MenuPath[MenuPathID] then - missionCommands.removeItemForGroup( self.MenuClient:GetClientGroupID(), MenuPath[MenuPathID] ) - end - - self.MenuPath = missionCommands.addCommandForGroup( self.MenuClient:GetClientGroupID(), MenuText, MenuParentPath, CommandMenuFunction, CommandMenuArgument ) - MenuPath[MenuPathID] = self.MenuPath - - self.CommandMenuFunction = CommandMenuFunction - self.CommandMenuArgument = CommandMenuArgument - - ParentMenu.Menus[self.MenuPath] = self - - return self -end - -function MENU_CLIENT_COMMAND:Remove() - self:F( self.MenuPath ) - - if not _MENUCLIENTS[self.MenuClientGroupID] then - _MENUCLIENTS[self.MenuClientGroupID] = {} - end - - local MenuPath = _MENUCLIENTS[self.MenuClientGroupID] - - if MenuPath[table.concat(self.MenuParentPath) .. "/" .. self.MenuText] then - MenuPath[table.concat(self.MenuParentPath) .. "/" .. self.MenuText] = nil - end - - missionCommands.removeItemForGroup( self.MenuClient:GetClientGroupID(), self.MenuPath ) - self.ParentMenu.Menus[self.MenuPath] = nil - return nil -end - - ---- The MENU_COALITION class --- @type MENU_COALITION --- @extends Menu#MENU -MENU_COALITION = { - ClassName = "MENU_COALITION" -} - ---- Creates a new coalition menu item --- @param #MENU_COALITION self --- @param DCSCoalition#coalition.side MenuCoalition The coalition owning the menu. --- @param #string MenuText The text for the menu. --- @param #table ParentMenu The parent menu. --- @return #MENU_COALITION self -function MENU_COALITION:New( MenuCoalition, MenuText, ParentMenu ) - - -- Arrange meta tables - local MenuParentPath = {} - if ParentMenu ~= nil then - MenuParentPath = ParentMenu.MenuPath - end - - local self = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) - self:F( { MenuCoalition, MenuText, ParentMenu } ) - - self.MenuCoalition = MenuCoalition - self.MenuParentPath = MenuParentPath - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - self.Menus = {} - - self:T( { MenuParentPath, MenuText } ) - - self.MenuPath = missionCommands.addSubMenuForCoalition( self.MenuCoalition, MenuText, MenuParentPath ) - - self:T( { self.MenuPath } ) - - if ParentMenu and ParentMenu.Menus then - ParentMenu.Menus[self.MenuPath] = self - end - return self -end - ---- Removes the sub menus recursively of this MENU_COALITION. --- @param #MENU_COALITION self --- @return #MENU_COALITION self -function MENU_COALITION:RemoveSubMenus() - self:F( self.MenuPath ) - - for MenuID, Menu in pairs( self.Menus ) do - Menu:Remove() - end - -end - ---- Removes the sub menus recursively of this MENU_COALITION. --- @param #MENU_COALITION self --- @return #MENU_COALITION self -function MENU_COALITION:Remove() - self:F( self.MenuPath ) - - self:RemoveSubMenus() - missionCommands.removeItemForCoalition( self.MenuCoalition, self.MenuPath ) - self.ParentMenu.Menus[self.MenuPath] = nil - - return nil -end - - ---- The MENU_COALITION_COMMAND class --- @type MENU_COALITION_COMMAND --- @extends Menu#MENU -MENU_COALITION_COMMAND = { - ClassName = "MENU_COALITION_COMMAND" -} - ---- Creates a new radio command item for a group --- @param #MENU_COALITION_COMMAND self --- @param DCSCoalition#coalition.side MenuCoalition The coalition owning the menu. --- @param MenuText The text for the menu. --- @param ParentMenu The parent menu. --- @param CommandMenuFunction A function that is called when the menu key is pressed. --- @param CommandMenuArgument An argument for the function. --- @return #MENU_COALITION_COMMAND self -function MENU_COALITION_COMMAND:New( MenuCoalition, MenuText, ParentMenu, CommandMenuFunction, CommandMenuArgument ) - - -- Arrange meta tables - - local MenuParentPath = {} - if ParentMenu ~= nil then - MenuParentPath = ParentMenu.MenuPath - end - - local self = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) - - self.MenuCoalition = MenuCoalition - self.MenuParentPath = MenuParentPath - self.MenuText = MenuText - self.ParentMenu = ParentMenu - - self:T( { MenuParentPath, MenuText, CommandMenuFunction, CommandMenuArgument } ) - - self.MenuPath = missionCommands.addCommandForCoalition( self.MenuCoalition, MenuText, MenuParentPath, CommandMenuFunction, CommandMenuArgument ) - - self.CommandMenuFunction = CommandMenuFunction - self.CommandMenuArgument = CommandMenuArgument - - ParentMenu.Menus[self.MenuPath] = self - - return self -end - ---- Removes a radio command item for a coalition --- @param #MENU_COALITION_COMMAND self --- @return #MENU_COALITION_COMMAND self -function MENU_COALITION_COMMAND:Remove() - self:F( self.MenuPath ) - - missionCommands.removeItemForCoalition( self.MenuCoalition, self.MenuPath ) - self.ParentMenu.Menus[self.MenuPath] = nil - return nil -end ---- GROUP class. --- --- @{GROUP} class --- ============== --- The @{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. --- --- --- 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. --- --- IMPORTANT: ONE SHOULD NEVER SANATIZE these GROUP OBJECT REFERENCES! (make the GROUP object references nil). --- @module Group --- @author FlightControl - -Include.File( "Routines" ) -Include.File( "Base" ) -Include.File( "Message" ) -Include.File( "Unit" ) - ---- The GROUP class --- @type GROUP --- @extends Base#BASE --- @field DCSGroup#Group DCSGroup The DCS group class. --- @field #string GroupName The name of the group. -GROUP = { - ClassName = "GROUP", - GroupName = "", - GroupID = 0, - Controller = nil, - DCSGroup = nil, - WayPointFunctions = {}, - } - ---- A DCSGroup --- @type DCSGroup --- @field id_ The ID of the group in DCS - ---- Create a new GROUP from a DCSGroup --- @param #GROUP self --- @param DCSGroup#Group GroupName The DCS Group name --- @return #GROUP self -function GROUP:Register( GroupName ) - local self = BASE:Inherit( self, BASE:New() ) - self:F2( GroupName ) - self.GroupName = GroupName - return self -end - --- Reference methods. - ---- Find the GROUP wrapper class instance using the DCS Group. --- @param #GROUP self --- @param DCSGroup#Group DCSGroup The DCS Group. --- @return #GROUP The GROUP. -function GROUP:Find( DCSGroup ) - - local GroupName = DCSGroup:getName() -- Group#GROUP - local GroupFound = _DATABASE:FindGroup( GroupName ) - return GroupFound -end - ---- Find the created GROUP using the DCS Group Name. --- @param #GROUP self --- @param #string GroupName The DCS Group Name. --- @return #GROUP The GROUP. -function GROUP:FindByName( GroupName ) - - local GroupFound = _DATABASE:FindGroup( GroupName ) - return GroupFound -end - --- DCS Group methods support. - ---- Returns the DCS Group. --- @param #GROUP self --- @return DCSGroup#Group The DCS Group. -function GROUP:GetDCSGroup() - local DCSGroup = Group.getByName( self.GroupName ) - - if DCSGroup then - return DCSGroup - end - - return nil -end - - ---- Returns if the DCS Group is alive. --- When the group exists at run-time, this method will return true, otherwise false. --- @param #GROUP self --- @return #boolean true if the DCS Group is alive. -function GROUP:IsAlive() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSGroup() - - if DCSGroup then - local GroupIsAlive = DCSGroup:isExist() - self:T3( GroupIsAlive ) - return GroupIsAlive - end - - return nil -end - ---- Destroys the DCS Group and all of its DCS Units. --- Note that this destroy method also raises a destroy event at run-time. --- So all event listeners will catch the destroy event of this DCS Group. --- @param #GROUP self -function GROUP:Destroy() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSGroup() - - if DCSGroup then - for Index, UnitData in pairs( DCSGroup:getUnits() ) do - self:CreateEventCrash( timer.getTime(), UnitData ) - end - DCSGroup:destroy() - DCSGroup = nil - end - - return nil -end - ---- Returns category of the DCS Group. --- @param #GROUP self --- @return DCSGroup#Group.Category The category ID -function GROUP:GetCategory() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup then - local GroupCategory = DCSGroup:getCategory() - self:T3( GroupCategory ) - return GroupCategory - end - - return nil -end - ---- Returns the category name of the DCS Group. --- @param #GROUP self --- @return #string Category name = Helicopter, Airplane, Ground Unit, Ship -function GROUP:GetCategoryName() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup then - local CategoryNames = { - [Group.Category.AIRPLANE] = "Airplane", - [Group.Category.HELICOPTER] = "Helicopter", - [Group.Category.GROUND] = "Ground Unit", - [Group.Category.SHIP] = "Ship", - } - local GroupCategory = DCSGroup:getCategory() - self:T3( GroupCategory ) - - return CategoryNames[GroupCategory] - end - - return nil -end - - ---- Returns the coalition of the DCS Group. --- @param #GROUP self --- @return DCSCoalitionObject#coalition.side The coalition side of the DCS Group. -function GROUP:GetCoalition() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup then - local GroupCoalition = DCSGroup:getCoalition() - self:T3( GroupCoalition ) - return GroupCoalition - end - - return nil -end - ---- Returns the name of the DCS Group. --- @param #GROUP self --- @return #string The DCS Group name. -function GROUP:GetName() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSGroup() - - if DCSGroup then - local GroupName = DCSGroup:getName() - self:T3( GroupName ) - return GroupName - end - - return nil -end - ---- Returns the DCS Group identifier. --- @param #GROUP self --- @return #number The identifier of the DCS Group. -function GROUP:GetID() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSGroup() - - if DCSGroup then - local GroupID = DCSGroup:getID() - self:T3( GroupID ) - return GroupID - end - - return nil -end - ---- Returns the UNIT wrapper class with number UnitNumber. --- If the underlying DCS Unit does not exist, the method will return nil. . --- @param #GROUP self --- @param #number UnitNumber The number of the UNIT wrapper class to be returned. --- @return Unit#UNIT The UNIT wrapper class. -function GROUP:GetUnit( UnitNumber ) - self:F2( { self.GroupName, UnitNumber } ) - - local DCSGroup = self:GetDCSGroup() - - if DCSGroup then - local UnitFound = UNIT:Find( DCSGroup:getUnit( UnitNumber ) ) - self:T3( UnitFound.UnitName ) - self:T2( UnitFound ) - return UnitFound - end - - return nil -end - ---- Returns the DCS Unit with number UnitNumber. --- If the underlying DCS Unit does not exist, the method will return nil. . --- @param #GROUP self --- @param #number UnitNumber The number of the DCS Unit to be returned. --- @return DCSUnit#Unit The DCS Unit. -function GROUP:GetDCSUnit( UnitNumber ) - self:F2( { self.GroupName, UnitNumber } ) - - local DCSGroup = self:GetDCSGroup() - - 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:GetDCSGroup() - - 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:GetDCSGroup() - - if DCSGroup then - local GroupInitialSize = DCSGroup:getInitialSize() - self:T3( GroupInitialSize ) - return GroupInitialSize - end - - return nil -end - ---- Returns the UNITs wrappers of the DCS Units of the DCS Group. --- @param #GROUP self --- @return #table The UNITs wrappers. -function GROUP:GetUnits() - self:F2( { self.GroupName } ) - local DCSGroup = self:GetDCSGroup() - - if DCSGroup then - local DCSUnits = DCSGroup:getUnits() - local Units = {} - for Index, UnitData in pairs( DCSUnits ) do - Units[#Units+1] = UNIT:Find( UnitData ) - end - self:T3( Units ) - return Units - end - - return nil -end - - ---- Returns the DCS Units of the DCS Group. --- @param #GROUP self --- @return #table The DCS Units. -function GROUP:GetDCSUnits() - self:F2( { self.GroupName } ) - local DCSGroup = self:GetDCSGroup() - - if DCSGroup then - local DCSUnits = DCSGroup:getUnits() - self:T3( DCSUnits ) - return DCSUnits - end - - return nil -end - ---- Get the controller for the GROUP. --- @param #GROUP self --- @return DCSController#Controller -function GROUP:_GetController() - self:F2( { self.GroupName } ) - local DCSGroup = self:GetDCSGroup() - - if DCSGroup then - local GroupController = DCSGroup:getController() - self:T3( GroupController ) - return GroupController - end - - return nil -end - - ---- Retrieve the group mission and allow to place function hooks within the mission waypoint plan. --- Use the method @{Group#GROUP:WayPointFunction} to define the hook functions for specific waypoints. --- Use the method @{Group@GROUP:WayPointExecute) to start the execution of the new mission plan. --- Note that when WayPointInitialize is called, the Mission of the group is RESTARTED! --- @param #GROUP self --- @return #GROUP -function GROUP:WayPointInitialize() - - self.WayPoints = self:GetTaskRoute() - - return self -end - - ---- Registers a waypoint function that will be executed when the group moves over the WayPoint. --- @param #GROUP 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 group moves over the waypoint. The waypoint function takes variable parameters. --- @return #GROUP -function GROUP: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 GROUP:TaskFunction( WayPoint, WayPointIndex, FunctionString, FunctionArguments ) - - local DCSTask - - local DCSScript = {} - DCSScript[#DCSScript+1] = "local MissionGroup = GROUP:Find( ... ) " - - if FunctionArguments.n > 0 then - DCSScript[#DCSScript+1] = FunctionString .. "( MissionGroup, " .. table.concat( FunctionArguments, "," ) .. ")" - else - DCSScript[#DCSScript+1] = FunctionString .. "( MissionGroup )" - 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 group will be 1! --- @param #GROUP self --- @param #number WayPoint The WayPoint from where to execute the mission. --- @param #WaitTime The amount seconds to wait before initiating the mission. --- @return #GROUP -function GROUP:WayPointExecute( WayPoint, WaitTime ) - - if not WayPoint then - WayPoint = 1 - end - - -- When starting the mission from a certain point, the TaskPoints need to be deleted before the given WayPoint. - for TaskPointID = 1, WayPoint - 1 do - table.remove( self.WayPoints, 1 ) - end - - self:T3( self.WayPoints ) - - self:SetTask( self:TaskRoute( self.WayPoints ), WaitTime ) - - return self -end - - ---- Activates a GROUP. --- @param #GROUP self -function GROUP:Activate() - self:F2( { self.GroupName } ) - trigger.action.activateGroup( self:GetDCSGroup() ) - return self:GetDCSGroup() -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:GetDCSGroup() - - 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:GetDCSGroup() - - if DCSGroup then - local GroupCallSign = DCSGroup:getUnit(1):getCallsign() - self:T3( GroupCallSign ) - return GroupCallSign - end - - return nil -end - ---- Returns the current point (Vec2 vector) of the first DCS Unit in the DCS Group. --- @return DCSTypes#Vec2 Current Vec2 point of the first DCS Unit of the DCS Group. -function GROUP:GetPointVec2() - self:F2( self.GroupName ) - - local GroupPointVec2 = self:GetUnit(1):GetPointVec2() - self:T3( GroupPointVec2 ) - return GroupPointVec2 -end - ---- Returns the current point (Vec3 vector) of the first DCS Unit in the DCS Group. --- @return DCSTypes#Vec3 Current Vec3 point of the first DCS Unit of the DCS Group. -function GROUP:GetPointVec3() - self:F2( self.GroupName ) - - local GroupPointVec3 = self:GetUnit(1):GetPointVec3() - self:T3( GroupPointVec3 ) - return GroupPointVec3 -end - - - --- Is Functions - ---- 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:GetDCSGroup() - - 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:GetDCSGroup() - - 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:GetDCSGroup() - - 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:GetDCSGroup() - - 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:GetDCSGroup() - - 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:GetDCSGroup() - - if DCSGroup then - local AllOnGroundResult = true - - for Index, UnitData in pairs( DCSGroup:getUnits() ) do - if UnitData:inAir() then - AllOnGroundResult = false - end - end - - self:T3( AllOnGroundResult ) - return AllOnGroundResult - end - - return nil -end - ---- Returns the current maximum velocity of the group. --- Each unit within the group gets evaluated, and the maximum velocity (= the unit which is going the fastest) is returned. --- @param #GROUP self --- @return #number Maximum velocity found. -function GROUP:GetMaxVelocity() - self:F2() - - local DCSGroup = self:GetDCSGroup() - - if DCSGroup then - local MaxVelocity = 0 - - for Index, UnitData in pairs( DCSGroup:getUnits() ) do - - local Velocity = UnitData:getVelocity() - local VelocityTotal = math.abs( Velocity.x ) + math.abs( Velocity.y ) + math.abs( Velocity.z ) - - if VelocityTotal < MaxVelocity then - MaxVelocity = VelocityTotal - end - end - - return MaxVelocity - end - - return nil -end - ---- Returns the current minimum height of the group. --- Each unit within the group gets evaluated, and the minimum height (= the unit which is the lowest elevated) is returned. --- @param #GROUP self --- @return #number Minimum height found. -function GROUP:GetMinHeight() - self:F2() - -end - ---- Returns the current maximum height of the group. --- Each unit within the group gets evaluated, and the maximum height (= the unit which is the highest elevated) is returned. --- @param #GROUP self --- @return #number Maximum height found. -function GROUP:GetMaxHeight() - self:F2() - -end - --- Tasks - ---- Popping current Task from the group. --- @param #GROUP self --- @return Group#GROUP self -function GROUP:PopCurrentTask() - self:F2() - - local DCSGroup = self:GetDCSGroup() - - if DCSGroup then - local Controller = self:_GetController() - Controller:popTask() - return self - end - - return nil -end - ---- Pushing Task on the queue from the group. --- @param #GROUP self --- @return Group#GROUP self -function GROUP:PushTask( DCSTask, WaitTime ) - self:F2() - - local DCSGroup = self:GetDCSGroup() - - if DCSGroup then - local Controller = self:_GetController() - - -- When a group SPAWNs, it takes about a second to get the group in the simulator. Setting tasks to unspawned groups provides unexpected results. - -- Therefore we schedule the functions to set the mission and options for the Group. - -- Controller:pushTask( DCSTask ) - - if WaitTime then - --routines.scheduleFunction( Controller.pushTask, { Controller, DCSTask }, timer.getTime() + WaitTime ) - SCHEDULER:New( Controller, Controller.pushTask, { DCSTask }, WaitTime ) - else - Controller:pushTask( DCSTask ) - end - - return self - end - - return nil -end - ---- Clearing the Task Queue and Setting the Task on the queue from the group. --- @param #GROUP self --- @return Group#GROUP self -function GROUP:SetTask( DCSTask, WaitTime ) - self:F2( { DCSTask } ) - - local DCSGroup = self:GetDCSGroup() - - if DCSGroup then - - local Controller = self:_GetController() - - -- When a group SPAWNs, it takes about a second to get the group in the simulator. Setting tasks to unspawned groups provides unexpected results. - -- Therefore we schedule the functions to set the mission and options for the Group. - -- Controller.setTask( Controller, DCSTask ) - - if not WaitTime then - WaitTime = 1 - end - --routines.scheduleFunction( Controller.setTask, { Controller, DCSTask }, timer.getTime() + WaitTime ) - SCHEDULER:New( Controller, Controller.setTask, { DCSTask }, WaitTime ) - - return self - end - - return nil -end - - ---- Return a condition section for a controlled task --- @param #GROUP self --- @param DCSTime#Time time --- @param #string userFlag --- @param #boolean userFlagValue --- @param #string condition --- @param DCSTime#Time duration --- @param #number lastWayPoint --- return DCSTask#Task -function GROUP: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 #GROUP self --- @param DCSTask#Task DCSTask --- @param #DCSStopCondition DCSStopCondition --- @return DCSTask#Task -function GROUP: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 #GROUP self --- @param #list DCSTasks --- @return DCSTask#Task -function GROUP:TaskCombo( DCSTasks ) - self:F2( { DCSTasks } ) - - local DCSTaskCombo - - DCSTaskCombo = { - id = 'ComboTask', - params = { - tasks = DCSTasks - } - } - - self:T3( { DCSTaskCombo } ) - return DCSTaskCombo -end - ---- Return a WrappedAction Task taking a Command --- @param #GROUP self --- @param DCSCommand#Command DCSCommand --- @return DCSTask#Task -function GROUP: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 #GROUP self --- @param DCSCommand#Command DCSCommand --- @return #GROUP self -function GROUP:SetCommand( DCSCommand ) - self:F2( DCSCommand ) - - local DCSGroup = self:GetDCSGroup() - - if DCSGroup then - local Controller = self:_GetController() - Controller:setCommand( DCSCommand ) - return self - end - - return nil -end - ---- Perform a switch waypoint command --- @param #GROUP self --- @param #number FromWayPoint --- @param #number ToWayPoint --- @return DCSTask#Task -function GROUP:CommandSwitchWayPoint( FromWayPoint, ToWayPoint, Index ) - self:F2( { FromWayPoint, ToWayPoint, Index } ) - - local CommandSwitchWayPoint = { - id = 'SwitchWaypoint', - params = { - fromWaypointIndex = FromWayPoint, - goToWaypointIndex = ToWayPoint, - }, - } - - self:T3( { CommandSwitchWayPoint } ) - return CommandSwitchWayPoint -end - - ---- Orbit at a specified position at a specified alititude during a specified duration with a specified speed. --- @param #GROUP self --- @param DCSTypes#Vec2 Point The point to hold the position. --- @param #number Altitude The altitude to hold the position. --- @param #number Speed The speed flying when holding the position. --- @return #GROUP self -function GROUP:TaskOrbitCircleAtVec2( Point, Altitude, Speed ) - self:F2( { self.GroupName, 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 - ---- Orbit at the current position of the first unit of the group at a specified alititude --- @param #GROUP self --- @param #number Altitude The altitude to hold the position. --- @param #number Speed The speed flying when holding the position. --- @return #GROUP self -function GROUP:TaskOrbitCircle( Altitude, Speed ) - self:F2( { self.GroupName, Altitude, Speed } ) - - local DCSGroup = self:GetDCSGroup() - - if DCSGroup then - local GroupPoint = self:GetPointVec2() - return self:TaskOrbitCircleAtVec2( GroupPoint, Altitude, Speed ) - end - - return nil -end - - - ---- Hold position at the current position of the first unit of the group. --- @param #GROUP self --- @param #number Duration The maximum duration in seconds to hold the position. --- @return #GROUP self -function GROUP:TaskHoldPosition() - self:F2( { self.GroupName } ) - - return self:TaskOrbitCircle( 30, 10 ) -end - - ---- Land the group at a Vec2Point. --- @param #GROUP self --- @param DCSTypes#Vec2 Point The point where to land. --- @param #number Duration The duration in seconds to stay on the ground. --- @return #GROUP self -function GROUP:TaskLandAtVec2( Point, Duration ) - self:F2( { self.GroupName, Point, Duration } ) - - 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 - ---- Land the group at a @{Zone#ZONE). --- @param #GROUP self --- @param Zone#ZONE Zone The zone where to land. --- @param #number Duration The duration in seconds to stay on the ground. --- @return #GROUP self -function GROUP:TaskLandAtZone( Zone, Duration, RandomPoint ) - self:F2( { self.GroupName, Zone, Duration, RandomPoint } ) - - local Point - if RandomPoint then - Point = Zone:GetRandomPointVec2() - else - Point = Zone:GetPointVec2() - end - - local DCSTask = self:TaskLandAtVec2( Point, Duration ) - - self:T3( DCSTask ) - return DCSTask -end - - ---- Attack the Unit. --- @param #GROUP self --- @param Unit#UNIT The unit. --- @return DCSTask#Task The DCS task structure. -function GROUP:TaskAttackUnit( AttackUnit ) - self:F2( { self.GroupName, AttackUnit } ) - --- AttackUnit = { --- id = 'AttackUnit', --- params = { --- unitId = Unit.ID, --- weaponType = number, --- expend = enum AI.Task.WeaponExpend --- attackQty = number, --- direction = Azimuth, --- attackQtyLimit = boolean, --- groupAttack = boolean, --- } --- } - - local DCSTask - DCSTask = { id = 'AttackUnit', - params = { unitId = AttackUnit:GetID(), - expend = AI.Task.WeaponExpend.TWO, - groupAttack = true, - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - ---- Attack a Group. --- @param #GROUP self --- @param Group#GROUP AttackGroup The Group to be attacked. --- @return DCSTask#Task The DCS task structure. -function GROUP:TaskAttackGroup( AttackGroup ) - self:F2( { self.GroupName, AttackGroup } ) - --- 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 DCSTask - DCSTask = { id = 'AttackGroup', - params = { groupId = AttackGroup:GetID(), - expend = AI.Task.WeaponExpend.TWO, - }, - }, - - self:T3( { DCSTask } ) - return DCSTask -end - ---- Fires at a VEC2 point. --- @param #GROUP self --- @param DCSTypes#Vec2 The point to fire at. --- @param DCSTypes#Distance Radius The radius of the zone to deploy the fire at. --- @return DCSTask#Task The DCS task structure. -function GROUP:TaskFireAtPoint( PointVec2, Radius ) - self:F2( { self.GroupName, PointVec2, Radius } ) - --- FireAtPoint = { --- id = 'FireAtPoint', --- params = { --- point = Vec2, --- radius = Distance, --- } --- } - - local DCSTask - DCSTask = { id = 'FireAtPoint', - params = { point = PointVec2, - radius = Radius, - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - - - ---- Move the group to a Vec2 Point, wait for a defined duration and embark a group. --- @param #GROUP self --- @param DCSTypes#Vec2 Point The point where to wait. --- @param #number Duration The duration in seconds to wait. --- @param #GROUP EmbarkingGroup The group to be embarked. --- @return DCSTask#Task The DCS task structure -function GROUP:TaskEmbarkingAtVec2( Point, Duration, EmbarkingGroup ) - self:F2( { self.GroupName, Point, Duration, EmbarkingGroup.DCSGroup } ) - - local DCSTask - DCSTask = { id = 'Embarking', - params = { x = Point.x, - y = Point.y, - duration = Duration, - groupsForEmbarking = { EmbarkingGroup.GroupID }, - durationFlag = true, - distributionFlag = false, - distribution = {}, - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - ---- Move to a defined Vec2 Point, and embark to a group when arrived within a defined Radius. --- @param #GROUP self --- @param DCSTypes#Vec2 Point The point where to wait. --- @param #number Radius The radius of the embarking zone around the Point. --- @return DCSTask#Task The DCS task structure. -function GROUP:TaskEmbarkToTransportAtVec2( Point, Radius ) - self:F2( { self.GroupName, Point, Radius } ) - - local DCSTask --DCSTask#Task - DCSTask = { id = 'EmbarkToTransport', - params = { x = Point.x, - y = Point.y, - zoneRadius = Radius, - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - ---- Return a Misson task from a mission template. --- @param #GROUP self --- @param #table TaskMission A table containing the mission task. --- @return DCSTask#Task -function GROUP: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 #GROUP self --- @param #table Points A table of route points. --- @return DCSTask#Task -function GROUP:TaskRoute( Points ) - self:F2( Points ) - - local DCSTask - DCSTask = { id = 'Mission', params = { route = { points = Points, }, }, } - - self:T3( { DCSTask } ) - return DCSTask -end - ---- Make the DCS Group to fly to a given point and hover. --- @param #GROUP self --- @param DCSTypes#Vec3 Point The destination point in Vec3 format. --- @param #number Speed The speed to travel. --- @return #GROUP self -function GROUP:TaskRouteToVec2( Point, Speed ) - self:F2( { Point, Speed } ) - - local GroupPoint = self:GetUnit( 1 ):GetPointVec2() - - local PointFrom = {} - PointFrom.x = GroupPoint.x - PointFrom.y = GroupPoint.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 - ---- Make the DCS Group to fly to a given point and hover. --- @param #GROUP self --- @param DCSTypes#Vec3 Point The destination point in Vec3 format. --- @param #number Speed The speed to travel. --- @return #GROUP self -function GROUP:TaskRouteToVec3( Point, Speed ) - self:F2( { Point, Speed } ) - - local GroupPoint = self:GetUnit( 1 ):GetPointVec3() - - local PointFrom = {} - PointFrom.x = GroupPoint.x - PointFrom.y = GroupPoint.z - PointFrom.alt = GroupPoint.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 group to follow a given route. --- @param #GROUP self --- @param #table GoPoints A table of Route Points. --- @return #GROUP self -function GROUP:Route( GoPoints ) - self:F2( GoPoints ) - - local DCSGroup = self:GetDCSGroup() - - if DCSGroup then - local Points = routines.utils.deepCopy( GoPoints ) - local MissionTask = { id = 'Mission', params = { route = { points = Points, }, }, } - local Controller = self:_GetController() - --Controller.setTask( Controller, MissionTask ) - --routines.scheduleFunction( Controller.setTask, { Controller, MissionTask}, timer.getTime() + 1 ) - SCHEDULER:New( Controller, Controller.setTask, { MissionTask }, 1 ) - return self - end - - return nil -end - - - ---- Route the group to a given zone. --- The group final destination point can be randomized. --- A speed can be given in km/h. --- A given formation can be given. --- @param #GROUP self --- @param Zone#ZONE Zone The zone where to route to. --- @param #boolean Randomize Defines whether to target point gets randomized within the Zone. --- @param #number Speed The speed. --- @param Base#FORMATION Formation The formation string. -function GROUP:TaskRouteToZone( Zone, Randomize, Speed, Formation ) - self:F2( Zone ) - - local DCSGroup = self:GetDCSGroup() - - if DCSGroup then - - local GroupPoint = self:GetPointVec2() - - local PointFrom = {} - PointFrom.x = GroupPoint.x - PointFrom.y = GroupPoint.y - PointFrom.type = "Turning Point" - PointFrom.action = "Cone" - PointFrom.speed = 20 / 1.6 - - - local PointTo = {} - local ZonePoint - - if Randomize then - ZonePoint = Zone:GetRandomPointVec2() - else - ZonePoint = Zone:GetPointVec2() - end - - PointTo.x = ZonePoint.x - PointTo.y = ZonePoint.y - PointTo.type = "Turning Point" - - if Formation then - PointTo.action = Formation - else - PointTo.action = "Cone" - end - - if Speed then - PointTo.speed = Speed - else - PointTo.speed = 20 / 1.6 - end - - local Points = { PointFrom, PointTo } - - self:T3( Points ) - - self:Route( Points ) - - return self - end - - return nil -end - --- Commands - ---- Do Script command --- @param #GROUP self --- @param #string DoScript --- @return #DCSCommand -function GROUP:CommandDoScript( DoScript ) - - local DCSDoScript = { - id = "Script", - params = { - command = DoScript, - }, - } - - self:T3( DCSDoScript ) - return DCSDoScript -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 - end - - return nil -end - - -function GROUP:GetDetectedTargets() - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup then - return self:_GetController():getDetectedTargets() - end - - return nil -end - -function GROUP:IsTargetDetected( DCSObject ) - self:F2( self.GroupName ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup 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 GROUP hold their weapons? --- @param #GROUP self --- @return #boolean -function GROUP:OptionROEHoldFirePossible() - self:F2( { self.GroupName } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup then - if self:IsAir() or self:IsGround() or self:IsShip() then - return true - end - - return false - end - - return nil -end - ---- Holding weapons. --- @param Group#GROUP self --- @return Group#GROUP self -function GROUP:OptionROEHoldFire() - self:F2( { self.GroupName } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup 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 GROUP attack returning on enemy fire? --- @param #GROUP self --- @return #boolean -function GROUP:OptionROEReturnFirePossible() - self:F2( { self.GroupName } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup then - if self:IsAir() or self:IsGround() or self:IsShip() then - return true - end - - return false - end - - return nil -end - ---- Return fire. --- @param #GROUP self --- @return #GROUP self -function GROUP:OptionROEReturnFire() - self:F2( { self.GroupName } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup 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 GROUP attack designated targets? --- @param #GROUP self --- @return #boolean -function GROUP:OptionROEOpenFirePossible() - self:F2( { self.GroupName } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup then - if self:IsAir() or self:IsGround() or self:IsShip() then - return true - end - - return false - end - - return nil -end - ---- Openfire. --- @param #GROUP self --- @return #GROUP self -function GROUP:OptionROEOpenFire() - self:F2( { self.GroupName } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup 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 GROUP attack targets of opportunity? --- @param #GROUP self --- @return #boolean -function GROUP:OptionROEWeaponFreePossible() - self:F2( { self.GroupName } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup then - if self:IsAir() then - return true - end - - return false - end - - return nil -end - ---- Weapon free. --- @param #GROUP self --- @return #GROUP self -function GROUP:OptionROEWeaponFree() - self:F2( { self.GroupName } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup 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 GROUP ignore enemy fire? --- @param #GROUP self --- @return #boolean -function GROUP:OptionROTNoReactionPossible() - self:F2( { self.GroupName } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup then - if self:IsAir() then - return true - end - - return false - end - - return nil -end - - ---- No evasion on enemy threats. --- @param #GROUP self --- @return #GROUP self -function GROUP:OptionROTNoReaction() - self:F2( { self.GroupName } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup 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 GROUP evade using passive defenses? --- @param #GROUP self --- @return #boolean -function GROUP:OptionROTPassiveDefensePossible() - self:F2( { self.GroupName } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup then - if self:IsAir() then - return true - end - - return false - end - - return nil -end - ---- Evasion passive defense. --- @param #GROUP self --- @return #GROUP self -function GROUP:OptionROTPassiveDefense() - self:F2( { self.GroupName } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup 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 GROUP evade on enemy fire? --- @param #GROUP self --- @return #boolean -function GROUP:OptionROTEvadeFirePossible() - self:F2( { self.GroupName } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup then - if self:IsAir() then - return true - end - - return false - end - - return nil -end - - ---- Evade on fire. --- @param #GROUP self --- @return #GROUP self -function GROUP:OptionROTEvadeFire() - self:F2( { self.GroupName } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup 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 GROUP evade on fire using vertical manoeuvres? --- @param #GROUP self --- @return #boolean -function GROUP:OptionROTVerticalPossible() - self:F2( { self.GroupName } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup then - if self:IsAir() then - return true - end - - return false - end - - return nil -end - - ---- Evade on fire using vertical manoeuvres. --- @param #GROUP self --- @return #GROUP self -function GROUP:OptionROTVertical() - self:F2( { self.GroupName } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup 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 - --- Message APIs - ---- Returns a message for a coalition or a client. --- @param #GROUP self --- @param #string Message The message text --- @param #Duration Duration The duration of the message. --- @return Message#MESSAGE -function GROUP:Message( Message, Duration ) - self:F2( { Message, Duration } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup then - return MESSAGE:New( Message, self:GetCallsign() .. " (" .. self:GetTypeName() .. ")", Duration, self:GetClassNameAndID() ) - end - - return nil -end - ---- Send a message to all coalitions. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #GROUP self --- @param #string Message The message text --- @param #Duration Duration The duration of the message. -function GROUP:MessageToAll( Message, Duration ) - self:F2( { Message, Duration } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup then - self:Message( Message, Duration ):ToAll() - end - - return nil -end - ---- Send a message to the red coalition. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #GROUP self --- @param #string Message The message text --- @param #Duration Duration The duration of the message. -function GROUP:MessageToRed( Message, Duration ) - self:F2( { Message, Duration } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup then - self:Message( Message, Duration ):ToRed() - end - - return nil -end - ---- Send a message to the blue coalition. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #GROUP self --- @param #string Message The message text --- @param #Duration Duration The duration of the message. -function GROUP:MessageToBlue( Message, Duration ) - self:F2( { Message, Duration } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup then - self:Message( Message, Duration ):ToBlue() - end - - return nil -end - ---- Send a message to a client. --- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. --- @param #GROUP self --- @param #string Message The message text --- @param #Duration Duration The duration of the message. --- @param Client#CLIENT Client The client object receiving the message. -function GROUP:MessageToClient( Message, Duration, Client ) - self:F2( { Message, Duration } ) - - local DCSGroup = self:GetDCSGroup() - if DCSGroup then - self:Message( Message, Duration ):ToClient( Client ) - end - - return nil -end ---- UNIT Class --- --- @{UNIT} class --- ============== --- 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. --- --- --- 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). --- --- DCS UNIT APIs --- ============= --- The DCS Unit APIs are used extensively within MOOSE. The UNIT class has for each DCS Unit API a corresponding method. --- To be able to distinguish easily in your code the difference between a UNIT API call and a DCS Unit API call, --- the first letter of the method is also capitalized. So, by example, the DCS Unit method @{DCSUnit#Unit.getName}() --- is implemented in the UNIT class as @{#UNIT.GetName}(). --- --- Additional UNIT APIs --- ==================== --- The UNIT class comes with additional methods. Find below a summary. --- --- 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. --- --- Position, Point --- --------------- --- The UNIT class provides methods to obtain the current point or position of the DCS Unit. --- The @{#UNIT.GetPointVec2}(), @{#UNIT.GetPointVec3}() will obtain the current location of the DCS Unit in a Vec2 (2D) or a 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. --- --- Alive --- ----- --- The @{#UNIT.IsAlive}(), @{#UNIT.IsActive}() methods determines if the DCS Unit is alive, meaning, it is existing and active. --- --- Test for other units in radius --- ------------------------------ --- One can test if another DCS Unit is within a given radius of the current DCS Unit, by using the @{#UNIT.OtherUnitInRadius}() method. --- --- More functions will be added --- ---------------------------- --- During the MOOSE development, more functions will be added. A complete list of the current functions is below. --- --- --- --- --- @module Unit --- @author FlightControl - -Include.File( "Routines" ) -Include.File( "Base" ) -Include.File( "Message" ) - ---- The UNIT class --- @type UNIT --- @extends Base#BASE --- @field #UNIT.FlareColor FlareColor --- @field #UNIT.SmokeColor SmokeColor -UNIT = { - ClassName="UNIT", - CategoryName = { - [Unit.Category.AIRPLANE] = "Airplane", - [Unit.Category.HELICOPTER] = "Helicoper", - [Unit.Category.GROUND_UNIT] = "Ground Unit", - [Unit.Category.SHIP] = "Ship", - [Unit.Category.STRUCTURE] = "Structure", - }, - FlareColor = { - Green = trigger.flareColor.Green, - Red = trigger.flareColor.Red, - White = trigger.flareColor.White, - Yellow = trigger.flareColor.Yellow - }, - SmokeColor = { - Green = trigger.smokeColor.Green, - Red = trigger.smokeColor.Red, - White = trigger.smokeColor.White, - Orange = trigger.smokeColor.Orange, - Blue = trigger.smokeColor.Blue - }, - } - ---- FlareColor --- @type UNIT.FlareColor --- @field Green --- @field Red --- @field White --- @field Yellow - ---- SmokeColor --- @type UNIT.SmokeColor --- @field Green --- @field Red --- @field White --- @field Orange --- @field Blue - --- Registration. - ---- Create a new UNIT from DCSUnit. --- @param #UNIT self --- @param DCSUnit#Unit DCSUnit --- @param Database#DATABASE Database --- @return Unit#UNIT -function UNIT:Register( UnitName ) - - local self = BASE:Inherit( self, BASE:New() ) - self:F2( UnitName ) - self.UnitName = UnitName - return self -end - --- Reference methods. - ---- Finds a UNIT from the _DATABASE using a DCSUnit object. --- @param #UNIT self --- @param DCSUnit#Unit DCSUnit An existing DCS Unit object reference. --- @return Unit#UNIT self -function UNIT:Find( DCSUnit ) - - local UnitName = DCSUnit:getName() - local UnitFound = _DATABASE:FindUnit( UnitName ) - return UnitFound -end - ---- Find a UNIT in the _DATABASE using the name of an existing DCS Unit. --- @param #UNIT self --- @param #string UnitName The Unit Name. --- @return Unit#UNIT self -function UNIT:FindByName( UnitName ) - - local UnitFound = _DATABASE:FindUnit( UnitName ) - return UnitFound -end - -function UNIT:GetDCSUnit() - local DCSUnit = Unit.getByName( self.UnitName ) - - if DCSUnit then - return DCSUnit - end - - return nil -end - ---- Returns coalition of the Unit. --- @param Unit#UNIT self --- @return DCSCoalitionObject#coalition.side The side of the coalition. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetCoalition() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitCoalition = DCSUnit:getCoalition() - self:T3( UnitCoalition ) - return UnitCoalition - end - - return nil -end - ---- Returns country of the Unit. --- @param Unit#UNIT self --- @return DCScountry#country.id The country identifier. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetCountry() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitCountry = DCSUnit:getCountry() - self:T3( UnitCountry ) - return UnitCountry - end - - return nil -end - - ---- Returns DCS Unit object name. --- The function provides access to non-activated units too. --- @param Unit#UNIT self --- @return #string The name of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetName() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitName = self.UnitName - return UnitName - end - - return nil -end - - ---- Returns if the unit is alive. --- @param Unit#UNIT self --- @return #boolean true if Unit is alive. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:IsAlive() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitIsAlive = DCSUnit:isExist() - return UnitIsAlive - end - - return false -end - ---- Returns if the unit is activated. --- @param Unit#UNIT self --- @return #boolean true if Unit is activated. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:IsActive() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - - local UnitIsActive = DCSUnit:isActive() - return UnitIsActive - end - - return nil -end - ---- Returns name of the player that control the unit or nil if the unit is controlled by A.I. --- @param Unit#UNIT self --- @return #string Player Name --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetPlayerName() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - - local PlayerName = DCSUnit:getPlayerName() - if PlayerName == nil then - PlayerName = "" - end - return PlayerName - end - - return nil -end - ---- Returns the unit's unique identifier. --- @param Unit#UNIT self --- @return DCSUnit#Unit.ID Unit ID --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetID() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitID = DCSUnit:getID() - return UnitID - end - - return nil -end - ---- Returns the unit's number in the group. --- The number is the same number the unit has in ME. --- It may not be changed during the mission. --- If any unit in the group is destroyed, the numbers of another units will not be changed. --- @param Unit#UNIT self --- @return #number The Unit number. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetNumber() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitNumber = DCSUnit:getNumber() - return UnitNumber - end - - return nil -end - ---- Returns the unit's group if it exist and nil otherwise. --- @param Unit#UNIT self --- @return Group#GROUP The Group of the Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetGroup() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitGroup = DCSUnit:getGroup() - return UnitGroup - end - - return nil -end - - ---- Returns the unit's callsign - the localized string. --- @param Unit#UNIT self --- @return #string The Callsign of the Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetCallSign() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitCallSign = DCSUnit:getCallsign() - return UnitCallSign - end - - return nil -end - ---- Returns the unit's health. Dead units has health <= 1.0. --- @param Unit#UNIT self --- @return #number The Unit's health value. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetLife() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitLife = DCSUnit:getLife() - return UnitLife - end - - return nil -end - ---- Returns the Unit's initial health. --- @param Unit#UNIT self --- @return #number The Unit's initial health value. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetLife0() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitLife0 = DCSUnit:getLife0() - return UnitLife0 - end - - return nil -end - ---- Returns relative amount of fuel (from 0.0 to 1.0) the unit has in its internal tanks. If there are additional fuel tanks the value may be greater than 1.0. --- @param Unit#UNIT self --- @return #number The relative amount of fuel (from 0.0 to 1.0). --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetFuel() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitFuel = DCSUnit:getFuel() - return UnitFuel - end - - return nil -end - ---- Returns the Unit's ammunition. --- @param Unit#UNIT self --- @return DCSUnit#Unit.Ammo --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetAmmo() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitAmmo = DCSUnit:getAmmo() - return UnitAmmo - end - - return nil -end - ---- Returns the unit sensors. --- @param Unit#UNIT self --- @return DCSUnit#Unit.Sensors --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetSensors() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitSensors = DCSUnit:getSensors() - return UnitSensors - end - - return nil -end - --- Need to add here a function per sensortype --- unit:hasSensors(Unit.SensorType.RADAR, Unit.RadarType.AS) - ---- Returns two values: --- --- * First value indicates if at least one of the unit's radar(s) is on. --- * Second value is the object of the radar's interest. Not nil only if at least one radar of the unit is tracking a target. --- @param Unit#UNIT self --- @return #boolean Indicates if at least one of the unit's radar(s) is on. --- @return DCSObject#Object The object of the radar's interest. Not nil only if at least one radar of the unit is tracking a target. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetRadar() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitRadarOn, UnitRadarObject = DCSUnit:getRadar() - return UnitRadarOn, UnitRadarObject - end - - return nil, nil -end - --- Need to add here functions to check if radar is on and which object etc. - ---- Returns unit descriptor. Descriptor type depends on unit category. --- @param Unit#UNIT self --- @return DCSUnit#Unit.Desc The Unit descriptor. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetDesc() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitDesc = DCSUnit:getDesc() - return UnitDesc - end - - return nil -end - - ---- Returns the type name of the DCS Unit. --- @param Unit#UNIT self --- @return #string The type name of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetTypeName() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitTypeName = DCSUnit:getTypeName() - self:T3( UnitTypeName ) - return UnitTypeName - end - - return nil -end - - - ---- Returns the prefix name of the DCS Unit. A prefix name is a part of the name before a '#'-sign. --- DCS Units spawned with the @{SPAWN} class contain a '#'-sign to indicate the end of the (base) DCS Unit name. --- The spawn sequence number and unit number are contained within the name after the '#' sign. --- @param Unit#UNIT self --- @return #string The name of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetPrefix() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitPrefix = string.match( self.UnitName, ".*#" ):sub( 1, -2 ) - self:T3( UnitPrefix ) - return UnitPrefix - end - - return nil -end - - - ---- Returns the @{DCSTypes#Vec2} vector indicating the point in 2D of the DCS Unit within the mission. --- @param Unit#UNIT self --- @return DCSTypes#Vec2 The 2D point vector of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetPointVec2() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitPointVec3 = DCSUnit:getPosition().p - - local UnitPointVec2 = {} - UnitPointVec2.x = UnitPointVec3.x - UnitPointVec2.y = UnitPointVec3.z - - self:T3( UnitPointVec2 ) - return UnitPointVec2 - end - - return nil -end - - ---- Returns the @{DCSTypes#Vec3} vector indicating the point in 3D of the DCS Unit within the mission. --- @param Unit#UNIT self --- @return DCSTypes#Vec3 The 3D point vector of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetPointVec3() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitPointVec3 = DCSUnit:getPosition().p - self:T3( UnitPointVec3 ) - return UnitPointVec3 - end - - return nil -end - ---- Returns the @{DCSTypes#Position3} position vectors indicating the point and direction vectors in 3D of the DCS Unit within the mission. --- @param Unit#UNIT self --- @return DCSTypes#Position The 3D position vectors of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetPositionVec3() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitPosition = DCSUnit:getPosition() - self:T3( UnitPosition ) - return UnitPosition - end - - return nil -end - ---- Returns the DCS Unit velocity vector. --- @param Unit#UNIT self --- @return DCSTypes#Vec3 The velocity vector --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetVelocity() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitVelocityVec3 = DCSUnit:getVelocity() - self:T3( UnitVelocityVec3 ) - return UnitVelocityVec3 - end - - return nil -end - ---- Returns true if the DCS Unit is in the air. --- @param Unit#UNIT self --- @return #boolean true if in the air. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:InAir() - self:F2( self.UnitName ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitInAir = DCSUnit:inAir() - self:T3( UnitInAir ) - return UnitInAir - end - - return nil -end - ---- Returns the altitude of the DCS Unit. --- @param Unit#UNIT self --- @return DCSTypes#Distance The altitude of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:GetAltitude() - self:F2() - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitPointVec3 = DCSUnit:getPoint() --DCSTypes#Vec3 - return UnitPointVec3.y - end - - return nil -end - ---- Returns true if there is an **other** DCS Unit within a radius of the current 2D point of the DCS Unit. --- @param Unit#UNIT self --- @param Unit#UNIT AwaitUnit The other UNIT wrapper object. --- @param Radius The radius in meters with the DCS Unit in the centre. --- @return true If the other DCS Unit is within the radius of the 2D point of the DCS Unit. --- @return #nil The DCS Unit is not existing or alive. -function UNIT:OtherUnitInRadius( AwaitUnit, Radius ) - self:F2( { self.UnitName, AwaitUnit.UnitName, Radius } ) - - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitPos = self:GetPointVec3() - local AwaitUnitPos = AwaitUnit:GetPointVec3() - - if (((UnitPos.x - AwaitUnitPos.x)^2 + (UnitPos.z - AwaitUnitPos.z)^2)^0.5 <= Radius) then - self:T3( "true" ) - return true - else - self:T3( "false" ) - return false - end - end - - return nil -end - ---- Returns the DCS Unit category name as defined within the DCS Unit Descriptor. --- @param Unit#UNIT self --- @return #string The DCS Unit Category Name -function UNIT:GetCategoryName() - local DCSUnit = self:GetDCSUnit() - - if DCSUnit then - local UnitCategoryName = self.CategoryName[ self:GetDesc().category ] - return UnitCategoryName - end - - return nil -end - ---- Signal a flare at the position of the UNIT. --- @param #UNIT self -function UNIT:Flare( FlareColor ) - self:F2() - trigger.action.signalFlare( self:GetPointVec3(), FlareColor , 0 ) -end - ---- Signal a white flare at the position of the UNIT. --- @param #UNIT self -function UNIT:FlareWhite() - self:F2() - trigger.action.signalFlare( self:GetPointVec3(), trigger.flareColor.White , 0 ) -end - ---- Signal a yellow flare at the position of the UNIT. --- @param #UNIT self -function UNIT:FlareYellow() - self:F2() - trigger.action.signalFlare( self:GetPointVec3(), trigger.flareColor.Yellow , 0 ) -end - ---- Signal a green flare at the position of the UNIT. --- @param #UNIT self -function UNIT:FlareGreen() - self:F2() - trigger.action.signalFlare( self:GetPointVec3(), trigger.flareColor.Green , 0 ) -end - ---- Signal a red flare at the position of the UNIT. --- @param #UNIT self -function UNIT:FlareRed() - self:F2() - trigger.action.signalFlare( self:GetPointVec3(), trigger.flareColor.Red, 0 ) -end - ---- Smoke the UNIT. --- @param #UNIT self -function UNIT:Smoke( SmokeColor ) - self:F2() - trigger.action.smoke( self:GetPointVec3(), SmokeColor ) -end - ---- Smoke the UNIT Green. --- @param #UNIT self -function UNIT:SmokeGreen() - self:F2() - trigger.action.smoke( self:GetPointVec3(), trigger.smokeColor.Green ) -end - ---- Smoke the UNIT Red. --- @param #UNIT self -function UNIT:SmokeRed() - self:F2() - trigger.action.smoke( self:GetPointVec3(), trigger.smokeColor.Red ) -end - ---- Smoke the UNIT White. --- @param #UNIT self -function UNIT:SmokeWhite() - self:F2() - trigger.action.smoke( self:GetPointVec3(), trigger.smokeColor.White ) -end - ---- Smoke the UNIT Orange. --- @param #UNIT self -function UNIT:SmokeOrange() - self:F2() - trigger.action.smoke( self:GetPointVec3(), trigger.smokeColor.Orange ) -end - ---- Smoke the UNIT Blue. --- @param #UNIT self -function UNIT:SmokeBlue() - self:F2() - trigger.action.smoke( self:GetPointVec3(), trigger.smokeColor.Blue ) -end - --- Is methods - ---- Returns if the unit is of an air category. --- If the unit is a helicopter or a plane, then this method will return true, otherwise false. --- @param #UNIT self --- @return #boolean Air category evaluation result. -function UNIT:IsAir() - self:F2() - - local UnitDescriptor = self.DCSUnit:getDesc() - self:T3( { UnitDescriptor.category, Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } ) - - local IsAirResult = ( UnitDescriptor.category == Unit.Category.AIRPLANE ) or ( UnitDescriptor.category == Unit.Category.HELICOPTER ) - - self:T3( IsAirResult ) - return IsAirResult -end - ---- ZONE Classes --- @module Zone - -Include.File( "Routines" ) -Include.File( "Base" ) -Include.File( "Message" ) - ---- The ZONE class --- @type ZONE --- @Extends Base#BASE -ZONE = { - ClassName="ZONE", - } - -function ZONE:New( ZoneName ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( ZoneName ) - - local Zone = trigger.misc.getZone( ZoneName ) - - if not Zone then - error( "Zone " .. ZoneName .. " does not exist." ) - return nil - end - - self.Zone = Zone - self.ZoneName = ZoneName - - return self -end - -function ZONE:GetPointVec2() - self:F( self.ZoneName ) - - local Zone = trigger.misc.getZone( self.ZoneName ) - local Point = { x = Zone.point.x, y = Zone.point.z } - - self:T( { Zone, Point } ) - - return Point -end - -function ZONE:GetPointVec3( Height ) - self:F( self.ZoneName ) - - local Zone = trigger.misc.getZone( self.ZoneName ) - local Point = { x = Zone.point.x, y = land.getHeight( self:GetPointVec2() ) + Height, z = Zone.point.z } - - self:T( { Zone, Point } ) - - return Point -end - -function ZONE:GetRandomPointVec2() - self:F( self.ZoneName ) - - local Point = {} - - local Zone = trigger.misc.getZone( self.ZoneName ) - - local angle = math.random() * math.pi*2; - Point.x = Zone.point.x + math.cos( angle ) * math.random() * Zone.radius; - Point.y = Zone.point.z + math.sin( angle ) * math.random() * Zone.radius; - - self:T( { Zone, Point } ) - - return Point -end - -function ZONE:GetRadius() - self:F( self.ZoneName ) - - local Zone = trigger.misc.getZone( self.ZoneName ) - - self:T( { Zone } ) - - return Zone.radius -end - ---- The CLIENT models client units in multi player missions. --- --- @{#CLIENT} class --- ================ --- Clients are those **Units** defined within the Mission Editor that have the skillset defined as __Client__ or __Player__. --- Note that clients are NOT the same as Units, they are NOT necessarily alive. --- The @{CLIENT} class is a wrapper class to handle the DCS Unit objects that have the skillset defined as __Client__ or __Player__: --- --- * 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. --- --- CLIENT reference methods --- ======================= --- For each DCS Unit having skill level Player or Client, a CLIENT wrapper object (instance) will be created within the _@{DATABASE} object. --- This is done at the beginning of the mission (when the mission starts). --- --- The CLIENT class does not contain a :New() method, rather it provides :Find() methods to retrieve the object reference --- using the DCS Unit or the DCS UnitName. --- --- Another thing to know is that CLIENT objects do not "contain" the DCS Unit object. --- The CLIENT methods will reference the DCS Unit object by name when it is needed during API execution. --- If the DCS Unit object does not exist or is nil, the CLIENT methods will return nil and log an exception in the DCS.log file. --- --- The CLIENT class provides the following functions to retrieve quickly the relevant CLIENT instance: --- --- * @{#CLIENT.Find}(): Find a CLIENT instance from the _DATABASE object using a DCS Unit object. --- * @{#CLIENT.FindByName}(): Find a CLIENT instance from the _DATABASE object using a DCS Unit name. --- --- IMPORTANT: ONE SHOULD NEVER SANATIZE these CLIENT OBJECT REFERENCES! (make the CLIENT object references nil). --- --- @module Client --- @author FlightControl - -Include.File( "Routines" ) -Include.File( "Base" ) -Include.File( "Cargo" ) -Include.File( "Message" ) - - ---- The CLIENT class --- @type CLIENT --- @extends Unit#UNIT -CLIENT = { - ONBOARDSIDE = { - NONE = 0, - LEFT = 1, - RIGHT = 2, - BACK = 3, - FRONT = 4 - }, - ClassName = "CLIENT", - ClientName = nil, - ClientAlive = false, - ClientTransport = false, - ClientBriefingShown = false, - _Menus = {}, - _Tasks = {}, - Messages = { - } -} - - ---- Finds a CLIENT from the _DATABASE using the relevant DCS Unit. --- @param #CLIENT self --- @param #string ClientName Name of the DCS **Unit** as defined within the Mission Editor. --- @param #string ClientBriefing Text that describes the briefing of the mission when a Player logs into the Client. --- @return #CLIENT --- @usage --- -- Create new Clients. --- local Mission = MISSIONSCHEDULER.AddMission( 'Russia Transport Troops SA-6', 'Operational', 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', 'Russia' ) --- Mission:AddGoal( DeploySA6TroopsGoal ) --- --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 1' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 3' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 2' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 4' ):Transport() ) -function CLIENT:Find( DCSUnit ) - local ClientName = DCSUnit:getName() - local ClientFound = _DATABASE:FindClient( ClientName ) - - if ClientFound then - ClientFound:F( ClientName ) - return ClientFound - end - - error( "CLIENT not found for: " .. ClientName ) -end - - ---- Finds a CLIENT from the _DATABASE using the relevant Client Unit Name. --- As an optional parameter, a briefing text can be given also. --- @param #CLIENT self --- @param #string ClientName Name of the DCS **Unit** as defined within the Mission Editor. --- @param #string ClientBriefing Text that describes the briefing of the mission when a Player logs into the Client. --- @return #CLIENT --- @usage --- -- Create new Clients. --- local Mission = MISSIONSCHEDULER.AddMission( 'Russia Transport Troops SA-6', 'Operational', 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', 'Russia' ) --- Mission:AddGoal( DeploySA6TroopsGoal ) --- --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 1' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 3' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 2' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 4' ):Transport() ) -function CLIENT:FindByName( ClientName, ClientBriefing ) - local ClientFound = _DATABASE:FindClient( ClientName ) - - if ClientFound then - ClientFound:F( { ClientName, ClientBriefing } ) - ClientFound:AddBriefing( ClientBriefing ) - ClientFound.MessageSwitch = true - - return ClientFound - end - - error( "CLIENT not found for: " .. ClientName ) -end - -function CLIENT:Register( ClientName ) - local self = BASE:Inherit( self, UNIT:Register( ClientName ) ) - - self:F( ClientName ) - self.ClientName = ClientName - self.MessageSwitch = true - self.ClientAlive2 = false - - --self.AliveCheckScheduler = routines.scheduleFunction( self._AliveCheckScheduler, { self }, timer.getTime() + 1, 5 ) - self.AliveCheckScheduler = SCHEDULER:New( self, self._AliveCheckScheduler, {}, 1, 5 ) - - 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, self.ClientName .. '/ClientBriefing', "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, self.ClientName .. '/MissionBriefing', "Mission Briefing" ) - end - - return self -end - - - ---- Resets a CLIENT. --- @param #CLIENT self --- @param #string ClientName Name of the Group as defined within the Mission Editor. The Group must have a Unit with the type Client. -function CLIENT:Reset( ClientName ) - self:F() - self._Menus = {} -end - --- Is Functions - ---- Checks if the CLIENT is a multi-seated UNIT. --- @param #CLIENT self --- @return #boolean true if multi-seated. -function CLIENT:IsMultiSeated() - self:F( self.ClientName ) - - local ClientMultiSeatedTypes = { - ["Mi-8MT"] = "Mi-8MT", - ["UH-1H"] = "UH-1H", - ["P-51B"] = "P-51B" - } - - if self:IsAlive() then - local ClientTypeName = self:GetClientGroupUnit():GetTypeName() - if ClientMultiSeatedTypes[ClientTypeName] then - return true - end - end - - return false -end - ---- Checks for a client alive event and calls a function on a continuous basis. --- @param #CLIENT self --- @param #function CallBack Function. --- @return #CLIENT -function CLIENT:Alive( CallBack, ... ) - self:F() - - self.ClientCallBack = CallBack - self.ClientParameters = arg - - return self -end - ---- @param #CLIENT self -function CLIENT:_AliveCheckScheduler() - self:F( { self.ClientName, self.ClientAlive2, self.ClientBriefingShown } ) - - if self:IsAlive() then -- Polymorphic call of UNIT - if self.ClientAlive2 == false then - self:ShowBriefing() - if self.ClientCallBack then - self:T("Calling Callback function") - self.ClientCallBack( self, unpack( self.ClientParameters ) ) - end - self.ClientAlive2 = true - end - else - if self.ClientAlive2 == true then - self.ClientAlive2 = false - end - end - - return true -end - ---- Return the DCSGroup of a Client. --- This function is modified to deal with a couple of bugs in DCS 1.5.3 --- @param #CLIENT self --- @return DCSGroup#Group -function CLIENT:GetDCSGroup() - self:F3() - --- local ClientData = Group.getByName( self.ClientName ) --- if ClientData and ClientData:isExist() then --- self:T( self.ClientName .. " : group found!" ) --- return ClientData --- else --- return nil --- end - - local ClientUnit = Unit.getByName( self.ClientName ) - - local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ) } - for CoalitionId, CoalitionData in pairs( CoalitionsData ) do - self:T3( { "CoalitionData:", CoalitionData } ) - for UnitId, UnitData in pairs( CoalitionData ) do - self:T3( { "UnitData:", UnitData } ) - if UnitData and UnitData:isExist() then - - --self:E(self.ClientName) - if ClientUnit then - local ClientGroup = ClientUnit:getGroup() - if ClientGroup then - self:T3( "ClientGroup = " .. self.ClientName ) - if ClientGroup:isExist() and UnitData:getGroup():isExist() then - if ClientGroup:getID() == UnitData:getGroup():getID() then - self:T3( "Normal logic" ) - self:T3( self.ClientName .. " : group found!" ) - self.ClientGroupID = ClientGroup:getID() - self.ClientGroupName = ClientGroup:getName() - return ClientGroup - end - else - -- Now we need to resolve the bugs in DCS 1.5 ... - -- Consult the database for the units of the Client Group. (ClientGroup:getUnits() returns nil) - self:T3( "Bug 1.5 logic" ) - local ClientGroupTemplate = _DATABASE.Templates.Units[self.ClientName].GroupTemplate - self.ClientGroupID = ClientGroupTemplate.groupId - self.ClientGroupName = _DATABASE.Templates.Units[self.ClientName].GroupName - self:T3( self.ClientName .. " : group found in bug 1.5 resolvement logic!" ) - return ClientGroup - end - -- else - -- error( "Client " .. self.ClientName .. " not found!" ) - end - else - --self:E( { "Client not found!", self.ClientName } ) - end - end - end - end - - -- For non player clients - if ClientUnit then - local ClientGroup = ClientUnit:getGroup() - if ClientGroup then - self:T3( "ClientGroup = " .. self.ClientName ) - if ClientGroup:isExist() then - self:T3( "Normal logic" ) - self:T3( self.ClientName .. " : group found!" ) - return ClientGroup - end - end - end - - self.ClientGroupID = nil - self.ClientGroupUnit = nil - - return nil -end - - --- TODO: Check DCSTypes#Group.ID ---- Get the group ID of the client. --- @param #CLIENT self --- @return DCSTypes#Group.ID -function CLIENT:GetClientGroupID() - - local ClientGroup = self:GetDCSGroup() - - --self:E( self.ClientGroupID ) -- Determined in GetDCSGroup() - return self.ClientGroupID -end - - ---- Get the name of the group of the client. --- @param #CLIENT self --- @return #string -function CLIENT:GetClientGroupName() - - local ClientGroup = self:GetDCSGroup() - - self:T( self.ClientGroupName ) -- Determined in GetDCSGroup() - return self.ClientGroupName -end - ---- Returns the UNIT of the CLIENT. --- @param #CLIENT self --- @return Unit#UNIT -function CLIENT:GetClientGroupUnit() - self:F2() - - local ClientDCSUnit = Unit.getByName( self.ClientName ) - - self:T( self.ClientDCSUnit ) - if ClientDCSUnit and ClientDCSUnit:isExist() then - local ClientUnit = _DATABASE:FindUnit( self.ClientName ) - self:T2( ClientUnit ) - return ClientUnit - end -end - ---- Returns the DCSUnit of the CLIENT. --- @param #CLIENT self --- @return DCSTypes#Unit -function CLIENT:GetClientGroupDCSUnit() - self:F2() - - local ClientDCSUnit = Unit.getByName( self.ClientName ) - - if ClientDCSUnit and ClientDCSUnit:isExist() then - self:T2( ClientDCSUnit ) - return ClientDCSUnit - end -end - - ---- Evaluates if the CLIENT is a transport. --- @param #CLIENT self --- @return #boolean true is a transport. -function CLIENT:IsTransport() - self:F() - return self.ClientTransport -end - ---- Shows the @{Cargo#CARGO} contained within the CLIENT to the player as a message. --- The @{Cargo#CARGO} is shown using the @{Message#MESSAGE} distribution system. --- @param #CLIENT self -function CLIENT:ShowCargo() - self:F() - - local CargoMsg = "" - - for CargoName, Cargo in pairs( CARGOS ) do - if self == Cargo:IsLoadedInClient() then - CargoMsg = CargoMsg .. Cargo.CargoName .. " Type:" .. Cargo.CargoType .. " Weight: " .. Cargo.CargoWeight .. "\n" - end - end - - if CargoMsg == "" then - CargoMsg = "empty" - end - - self:Message( CargoMsg, 15, self.ClientName .. "/Cargo", "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 MessageId is a text identifying the Message in the MessageQueue. The Message system overwrites Messages with the same MessageId --- @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. -function CLIENT:Message( Message, MessageDuration, MessageId, MessageCategory, MessageInterval ) - self:F( { Message, MessageDuration, MessageId, MessageCategory, MessageInterval } ) - - if not self.MenuMessages then - if self:GetClientGroupID() then - self.MenuMessages = MENU_CLIENT:New( self, 'Messages' ) - self.MenuRouteMessageOn = MENU_CLIENT_COMMAND:New( self, 'Messages On', self.MenuMessages, CLIENT.SwitchMessages, { self, true } ) - self.MenuRouteMessageOff = MENU_CLIENT_COMMAND:New( self,'Messages Off', self.MenuMessages, CLIENT.SwitchMessages, { self, false } ) - end - end - - if self.MessageSwitch == true then - if MessageCategory == nil then - MessageCategory = "Messages" - end - if 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, MessageCategory, MessageDuration, MessageId ):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, MessageCategory, MessageDuration, MessageId ):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, MessageCategory, MessageDuration, MessageId ):ToClient( self ) - self.Messages[MessageId].MessageTime = timer.getTime() - end - end - end - end -end ---- Manage sets of units and groups. --- --- @{#Database} class --- ================== --- Mission designers can use the DATABASE class to build sets of units belonging to certain: --- --- * Coalitions --- * Categories --- * Countries --- * Unit types --- * Starting with certain prefix strings. --- --- This list will grow over time. Planned developments are to include filters and iterators. --- Additional filters will be added around @{Zone#ZONEs}, Radiuses, Active players, ... --- More iterators will be implemented in the near future ... --- --- Administers the Initial Sets of the Mission Templates as defined within the Mission Editor. --- --- DATABASE construction methods: --- ================================= --- Create a new DATABASE object with the @{#DATABASE.New} method: --- --- * @{#DATABASE.New}: Creates a new DATABASE object. --- --- --- DATABASE filter criteria: --- ========================= --- You can set filter criteria to define the set of units within the database. --- Filter criteria are defined by: --- --- * @{#DATABASE.FilterCoalitions}: Builds the DATABASE with the units belonging to the coalition(s). --- * @{#DATABASE.FilterCategories}: Builds the DATABASE with the units belonging to the category(ies). --- * @{#DATABASE.FilterTypes}: Builds the DATABASE with the units belonging to the unit type(s). --- * @{#DATABASE.FilterCountries}: Builds the DATABASE with the units belonging to the country(ies). --- * @{#DATABASE.FilterUnitPrefixes}: Builds the DATABASE with the units starting with the same prefix string(s). --- --- Once the filter criteria have been set for the DATABASE, you can start filtering using: --- --- * @{#DATABASE.FilterStart}: Starts the filtering of the units within the database. --- --- Planned filter criteria within development are (so these are not yet available): --- --- * @{#DATABASE.FilterGroupPrefixes}: Builds the DATABASE with the groups of the units starting with the same prefix string(s). --- * @{#DATABASE.FilterZones}: Builds the DATABASE with the units within a @{Zone#ZONE}. --- --- --- DATABASE iterators: --- =================== --- Once the filters have been defined and the DATABASE has been built, 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.ForEachAliveUnit}: Calls a function for each alive unit it finds within the DATABASE. --- --- Planned iterators methods in development are (so these are not yet available): --- --- * @{#DATABASE.ForEachUnit}: Calls a function for each unit contained within the DATABASE. --- * @{#DATABASE.ForEachGroup}: Calls a function for each group contained within the DATABASE. --- * @{#DATABASE.ForEachUnitInZone}: Calls a function for each unit within a certain zone contained within the DATABASE. --- --- ==== --- @module Database --- @author FlightControl - -Include.File( "Routines" ) -Include.File( "Base" ) -Include.File( "Menu" ) -Include.File( "Group" ) -Include.File( "Unit" ) -Include.File( "Event" ) -Include.File( "Client" ) - ---- DATABASE class --- @type DATABASE --- @extends Base#BASE -DATABASE = { - ClassName = "DATABASE", - Templates = { - Units = {}, - Groups = {}, - ClientsByName = {}, - ClientsByID = {}, - }, - DCSUnits = {}, - DCSGroups = {}, - UNITS = {}, - GROUPS = {}, - NavPoints = {}, - Statics = {}, - Players = {}, - PlayersAlive = {}, - CLIENTS = {}, - ClientsAlive = {}, - Filter = { - Coalitions = nil, - Categories = nil, - Types = nil, - Countries = nil, - UnitPrefixes = nil, - GroupPrefixes = 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, - }, - }, -} - -local _DATABASECoalition = - { - [1] = "Red", - [2] = "Blue", - } - -local _DATABASECategory = - { - [Unit.Category.AIRPLANE] = "Plane", - [Unit.Category.HELICOPTER] = "Helicopter", - [Unit.Category.GROUND_UNIT] = "Vehicle", - [Unit.Category.SHIP] = "Ship", - [Unit.Category.STRUCTURE] = "Structure", - } - - ---- Creates a new DATABASE object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. --- @param #DATABASE self --- @return #DATABASE --- @usage --- -- Define a new DATABASE Object. This DBObject will contain a reference to all Group and Unit Templates defined within the ME and the DCSRTE. --- DBObject = DATABASE:New() -function DATABASE:New() - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) - - _EVENTDISPATCHER:OnBirth( self._EventOnBirth, self ) - _EVENTDISPATCHER:OnDead( self._EventOnDeadOrCrash, self ) - _EVENTDISPATCHER:OnCrash( self._EventOnDeadOrCrash, self ) - - - -- Add database with registered clients and already alive players - - -- Follow alive players and clients - _EVENTDISPATCHER:OnPlayerEnterUnit( self._EventOnPlayerEnterUnit, self ) - _EVENTDISPATCHER:OnPlayerLeaveUnit( self._EventOnPlayerLeaveUnit, self ) - - - return self -end - ---- Finds a Unit based on the Unit Name. --- @param #DATABASE self --- @param #string UnitName --- @return Unit#UNIT The found Unit. -function DATABASE:FindUnit( UnitName ) - - local UnitFound = self.UNITS[UnitName] - return UnitFound -end - ---- Adds a Unit based on the Unit Name in the DATABASE. --- @param #DATABASE self -function DATABASE:AddUnit( DCSUnit, DCSUnitName ) - - self.DCSUnits[DCSUnitName] = DCSUnit - self.UNITS[DCSUnitName] = UNIT:Register( DCSUnitName ) -end - ---- Deletes a Unit from the DATABASE based on the Unit Name. --- @param #DATABASE self -function DATABASE:DeleteUnit( DCSUnitName ) - - self.DCSUnits[DCSUnitName] = nil -end - ---- Finds a CLIENT based on the ClientName. --- @param #DATABASE self --- @param #string ClientName --- @return Client#CLIENT The found CLIENT. -function DATABASE:FindClient( ClientName ) - - local ClientFound = self.CLIENTS[ClientName] - return ClientFound -end - ---- Adds a CLIENT based on the ClientName in the DATABASE. --- @param #DATABASE self -function DATABASE:AddClient( ClientName ) - - self.CLIENTS[ClientName] = CLIENT:Register( ClientName ) - self:E( self.CLIENTS[ClientName]:GetClassNameAndID() ) -end - ---- Finds a GROUP based on the GroupName. --- @param #DATABASE self --- @param #string GroupName --- @return Group#GROUP The found GROUP. -function DATABASE:FindGroup( GroupName ) - - local GroupFound = self.GROUPS[GroupName] - return GroupFound -end - ---- Adds a GROUP based on the GroupName in the DATABASE. --- @param #DATABASE self -function DATABASE:AddGroup( DCSGroup, GroupName ) - - self.DCSGroups[GroupName] = DCSGroup - self.GROUPS[GroupName] = GROUP:Register( GroupName ) -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.SpawnCoalitionID - local SpawnCountryID = SpawnTemplate.SpawnCountryID - local SpawnCategoryID = SpawnTemplate.SpawnCategoryID - - -- Nullify - SpawnTemplate.SpawnCoalitionID = nil - SpawnTemplate.SpawnCountryID = nil - SpawnTemplate.SpawnCategoryID = nil - - self:_RegisterGroup( SpawnTemplate ) - coalition.addGroup( SpawnCountryID, SpawnCategoryID, SpawnTemplate ) - - -- Restore - SpawnTemplate.SpawnCoalitionID = SpawnCoalitionID - SpawnTemplate.SpawnCountryID = SpawnCountryID - SpawnTemplate.SpawnCategoryID = SpawnCategoryID - - - local SpawnGroup = GROUP:Register( 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:F( 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:F( 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:_RegisterGroup( GroupTemplate ) - - local GroupTemplateName = env.getValueDictByKey(GroupTemplate.name) - - if not self.Templates.Groups[GroupTemplateName] then - self.Templates.Groups[GroupTemplateName] = {} - self.Templates.Groups[GroupTemplateName].Status = nil - end - - -- Delete the spans from the route, it is not needed and takes memory. - if GroupTemplate.route and GroupTemplate.route.spans then - GroupTemplate.route.spans = nil - end - - self.Templates.Groups[GroupTemplateName].GroupName = GroupTemplateName - self.Templates.Groups[GroupTemplateName].Template = GroupTemplate - self.Templates.Groups[GroupTemplateName].groupId = GroupTemplate.groupId - self.Templates.Groups[GroupTemplateName].UnitCount = #GroupTemplate.units - self.Templates.Groups[GroupTemplateName].Units = GroupTemplate.units - - self:T( { "Group", self.Templates.Groups[GroupTemplateName].GroupName, self.Templates.Groups[GroupTemplateName].UnitCount } ) - - for unit_num, UnitTemplate in pairs( GroupTemplate.units ) do - - local UnitTemplateName = env.getValueDictByKey(UnitTemplate.name) - self.Templates.Units[UnitTemplateName] = {} - self.Templates.Units[UnitTemplateName].UnitName = UnitTemplateName - self.Templates.Units[UnitTemplateName].Template = UnitTemplate - self.Templates.Units[UnitTemplateName].GroupName = GroupTemplateName - self.Templates.Units[UnitTemplateName].GroupTemplate = GroupTemplate - self.Templates.Units[UnitTemplateName].GroupId = GroupTemplate.groupId - self:E( {"skill",UnitTemplate.skill}) - if UnitTemplate.skill and (UnitTemplate.skill == "Client" or UnitTemplate.skill == "Player") then - self.Templates.ClientsByName[UnitTemplateName] = UnitTemplate - self.Templates.ClientsByID[UnitTemplate.unitId] = UnitTemplate - end - self:E( { "Unit", self.Templates.Units[UnitTemplateName].UnitName } ) - end -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() - 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 - ---- Private method that registers all datapoints within in the mission. --- @param #DATABASE self --- @return #DATABASE self -function DATABASE:_RegisterDatabase() - - 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:", DCSGroup, DCSGroupName } ) - self:AddGroup( DCSGroup, DCSGroupName ) - - for DCSUnitId, DCSUnit in pairs( DCSGroup:getUnits() ) do - - local DCSUnitName = DCSUnit:getName() - self:E( { "Register Unit:", DCSUnit, DCSUnitName } ) - self:AddUnit( DCSUnit, DCSUnitName ) - end - else - self:E( { "Group does not exist: ", DCSGroup } ) - end - - end - end - - for ClientName, ClientTemplate in pairs( self.Templates.ClientsByName ) do - self:E( { "Adding Client:", ClientName } ) - self:AddClient( ClientName ) - end - - return self -end - ---- Events - ---- Handles the OnBirth event for the alive units set. --- @param #DATABASE self --- @param Event#EVENTDATA Event -function DATABASE:_EventOnBirth( Event ) - self:F( { Event } ) - - if Event.IniDCSUnit then - if self:_IsIncludeDCSUnit( Event.IniDCSUnit ) then - self:AddUnit( Event.IniDCSUnit, Event.IniDCSUnitName ) - self:AddGroup( Event.IniDCSGroup, Event.IniDCSGroupName ) - self:_EventOnPlayerEnterUnit( Event ) - end - end -end - ---- Handles the OnDead or OnCrash event for alive units set. --- @param #DATABASE self --- @param Event#EVENTDATA Event -function DATABASE:_EventOnDeadOrCrash( Event ) - self:F( { Event } ) - - if Event.IniDCSUnit then - if self.DCSUnits[Event.IniDCSUnitName] then - self:DeleteUnit( Event.IniDCSUnitName ) - -- add logic to correctly remove a group once all units are destroyed... - end - end -end - ---- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). --- @param #DATABASE self --- @param Event#EVENTDATA Event -function DATABASE:_EventOnPlayerEnterUnit( Event ) - self:F( { Event } ) - - if Event.IniDCSUnit then - if self:_IsIncludeDCSUnit( Event.IniDCSUnit ) then - if not self.PlayersAlive[Event.IniDCSUnitName] then - self:E( { "Add player for unit:", Event.IniDCSUnitName, Event.IniDCSUnit:getPlayerName() } ) - self.PlayersAlive[Event.IniDCSUnitName] = Event.IniDCSUnit:getPlayerName() - self.ClientsAlive[Event.IniDCSUnitName] = self.CLIENTS[ Event.IniDCSUnitName ] - end - end - end -end - ---- Handles the OnPlayerLeaveUnit event to clean the active players table. --- @param #DATABASE self --- @param Event#EVENTDATA Event -function DATABASE:_EventOnPlayerLeaveUnit( Event ) - self:F( { Event } ) - - if Event.IniDCSUnit then - if self:_IsIncludeDCSUnit( Event.IniDCSUnit ) then - if self.PlayersAlive[Event.IniDCSUnitName] then - self:E( { "Cleaning player for unit:", Event.IniDCSUnitName, Event.IniDCSUnit:getPlayerName() } ) - self.PlayersAlive[Event.IniDCSUnitName] = nil - self.ClientsAlive[Event.IniDCSUnitName] = nil - end - end - end -end - ---- Iterators - ---- Interate the DATABASE and call an interator 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, arg, Set ) - self:F( 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 % 10 == 0 then - coroutine.yield( false ) - end - end - return true - end - - local co = coroutine.create( CoRoutine ) - - local function Schedule() - - local status, res = coroutine.resume( co ) - self:T( { status, res } ) - - if status == false then - error( res ) - end - if res == false then - return true -- resume next time the loop - end - - return false - end - - local Scheduler = SCHEDULER:New( self, Schedule, {}, 0.001, 0.001, 0 ) - - return self -end - - ---- Interate the DATABASE and call an interator 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:ForEachDCSUnit( IteratorFunction, ... ) - self:F( arg ) - - self:ForEach( IteratorFunction, arg, self.DCSUnits ) - - return self -end - ---- Interate the DATABASE and call an interator function for each **alive** player, providing the Unit of the player 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 UNIT parameter. --- @return #DATABASE self -function DATABASE:ForEachPlayer( IteratorFunction, ... ) - self:F( arg ) - - self:ForEach( IteratorFunction, arg, self.PlayersAlive ) - - return self -end - - ---- Interate the DATABASE and call an interator 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:F( arg ) - - self:ForEach( IteratorFunction, arg, self.CLIENTS ) - - return self -end - - -function DATABASE:ScanEnvironment() - self:F() - - self.Navpoints = {} - self.UNITS = {} - --Build routines.db.units and self.Navpoints - 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 - --self.Units[coa_name] = {} - - ---------------------------------------------- - -- build nav points DB - self.Navpoints[coa_name] = {} - 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[coa_name][nav_ind] = routines.utils.deepCopy(nav_data) - - self.Navpoints[coa_name][nav_ind]['name'] = nav_data.callsignStr -- name is a little bit more self-explanatory. - self.Navpoints[coa_name][nav_ind]['point'] = {} -- point is used by SSE, support it. - self.Navpoints[coa_name][nav_ind]['point']['x'] = nav_data.x - self.Navpoints[coa_name][nav_ind]['point']['y'] = 0 - self.Navpoints[coa_name][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.lower(cntry_data.name) - --self.Units[coa_name][countryName] = {} - --self.Units[coa_name][countryName]["countryId"] = cntry_data.id - - if type(cntry_data) == 'table' then --just making sure - - for obj_type_name, obj_type_data in pairs(cntry_data) do - - if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then --should be an unncessary check - - local category = 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:_RegisterGroup( GroupTemplate ) - 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 - - self:_RegisterDatabase() - self:_RegisterPlayers() - - return self -end - - ---- --- @param #DATABASE self --- @param DCSUnit#Unit DCSUnit --- @return #DATABASE self -function DATABASE:_IsIncludeDCSUnit( DCSUnit ) - self:F( DCSUnit ) - local DCSUnitInclude = true - - if self.Filter.Coalitions then - local DCSUnitCoalition = false - for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do - self:T( { "Coalition:", DCSUnit:getCoalition(), self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) - if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == DCSUnit:getCoalition() then - DCSUnitCoalition = true - end - end - DCSUnitInclude = DCSUnitInclude and DCSUnitCoalition - end - - if self.Filter.Categories then - local DCSUnitCategory = false - for CategoryID, CategoryName in pairs( self.Filter.Categories ) do - self:T( { "Category:", DCSUnit:getDesc().category, self.FilterMeta.Categories[CategoryName], CategoryName } ) - if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == DCSUnit:getDesc().category then - DCSUnitCategory = true - end - end - DCSUnitInclude = DCSUnitInclude and DCSUnitCategory - end - - if self.Filter.Types then - local DCSUnitType = false - for TypeID, TypeName in pairs( self.Filter.Types ) do - self:T( { "Type:", DCSUnit:getTypeName(), TypeName } ) - if TypeName == DCSUnit:getTypeName() then - DCSUnitType = true - end - end - DCSUnitInclude = DCSUnitInclude and DCSUnitType - end - - if self.Filter.Countries then - local DCSUnitCountry = false - for CountryID, CountryName in pairs( self.Filter.Countries ) do - self:T( { "Country:", DCSUnit:getCountry(), CountryName } ) - if country.id[CountryName] == DCSUnit:getCountry() then - DCSUnitCountry = true - end - end - DCSUnitInclude = DCSUnitInclude and DCSUnitCountry - end - - if self.Filter.UnitPrefixes then - local DCSUnitPrefix = false - for UnitPrefixId, UnitPrefix in pairs( self.Filter.UnitPrefixes ) do - self:T( { "Unit Prefix:", string.find( DCSUnit:getName(), UnitPrefix, 1 ), UnitPrefix } ) - if string.find( DCSUnit:getName(), UnitPrefix, 1 ) then - DCSUnitPrefix = true - end - end - DCSUnitInclude = DCSUnitInclude and DCSUnitPrefix - end - - self:T( DCSUnitInclude ) - return DCSUnitInclude -end - ---- --- @param #DATABASE self --- @param DCSUnit#Unit DCSUnit --- @return #DATABASE self -function DATABASE:_IsAliveDCSUnit( DCSUnit ) - self:F( DCSUnit ) - local DCSUnitAlive = false - if DCSUnit and DCSUnit:isExist() and DCSUnit:isActive() then - if self.DCSUnits[DCSUnit:getName()] then - DCSUnitAlive = true - end - end - self:T( DCSUnitAlive ) - return DCSUnitAlive -end - ---- --- @param #DATABASE self --- @param DCSGroup#Group DCSGroup --- @return #DATABASE self -function DATABASE:_IsAliveDCSGroup( DCSGroup ) - self:F( DCSGroup ) - local DCSGroupAlive = false - if DCSGroup and DCSGroup:isExist() then - if self.DCSGroups[DCSGroup:getName()] then - DCSGroupAlive = true - end - end - self:T( DCSGroupAlive ) - return DCSGroupAlive -end - - ---- Traces the current database contents in the log ... (for debug reasons). --- @param #DATABASE self --- @return #DATABASE self -function DATABASE:TraceDatabase() - self:F() - - self:T( { "DCSUnits:", self.DCSUnits } ) -end - - ---- The main include file for the MOOSE system. - -Include.File( "Routines" ) -Include.File( "Base" ) -Include.File( "Database" ) -Include.File( "Event" ) - --- The order of the declarations is important here. Don't touch it. - ---- Declare the event dispatcher based on the EVENT class -_EVENTDISPATCHER = EVENT:New() -- #EVENT - ---- Declare the main database object, which is used internally by the MOOSE classes. -_DATABASE = DATABASE:New():ScanEnvironment() -- Database#DATABASE - ---- Scoring system for MOOSE. --- This scoring class calculates the hits and kills that players make within a simulation session. --- Scoring is calculated using a defined algorithm. --- With a small change in MissionScripting.lua, the scoring can also be logged in a CSV file, that can then be uploaded --- to a database or a BI tool to publish the scoring results to the player community. --- @module Scoring --- @author FlightControl - - -Include.File( "Routines" ) -Include.File( "Base" ) -Include.File( "Menu" ) -Include.File( "Group" ) -Include.File( "Event" ) - - ---- The Scoring class --- @type SCORING --- @field Players A collection of the current players that have joined the game. --- @extends Base#BASE -SCORING = { - ClassName = "SCORING", - ClassID = 0, - Players = {}, -} - -local _SCORINGCoalition = - { - [1] = "Red", - [2] = "Blue", - } - -local _SCORINGCategory = - { - [Unit.Category.AIRPLANE] = "Plane", - [Unit.Category.HELICOPTER] = "Helicopter", - [Unit.Category.GROUND_UNIT] = "Vehicle", - [Unit.Category.SHIP] = "Ship", - [Unit.Category.STRUCTURE] = "Structure", - } - ---- Creates a new SCORING object to administer the scoring achieved by players. --- @param #SCORING self --- @param #string GameName The name of the game. This name is also logged in the CSV score file. --- @return #SCORING self --- @usage --- -- Define a new scoring object for the mission Gori Valley. --- ScoringObject = SCORING:New( "Gori Valley" ) -function SCORING:New( GameName ) - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) - - if GameName then - self.GameName = GameName - else - error( "A game name must be given to register the scoring results" ) - end - - - _EVENTDISPATCHER:OnDead( self._EventOnDeadOrCrash, self ) - _EVENTDISPATCHER:OnCrash( self._EventOnDeadOrCrash, self ) - _EVENTDISPATCHER:OnHit( self._EventOnHit, self ) - - --self.SchedulerId = routines.scheduleFunction( SCORING._FollowPlayersScheduled, { self }, 0, 5 ) - self.SchedulerId = SCHEDULER:New( self, self._FollowPlayersScheduled, {}, 0, 5 ) - - self:ScoreMenu() - - return self - -end - ---- Creates a score radio menu. Can be accessed using Radio -> F10. --- @param #SCORING self --- @return #SCORING self -function SCORING:ScoreMenu() - self.Menu = SUBMENU:New( 'Scoring' ) - self.AllScoresMenu = COMMANDMENU:New( 'Score All Active Players', self.Menu, SCORING.ReportScoreAll, self ) - --- = COMMANDMENU:New('Your Current Score', ReportScore, SCORING.ReportScorePlayer, self ) - return self -end - ---- Follows new players entering Clients within the DCSRTE. --- TODO: Need to see if i can catch this also with an event. It will eliminate the schedule ... -function SCORING:_FollowPlayersScheduled() - self:F3( "_FollowPlayersScheduled" ) - - local ClientUnit = 0 - local CoalitionsData = { AlivePlayersRed = coalition.getPlayers(coalition.side.RED), AlivePlayersBlue = coalition.getPlayers(coalition.side.BLUE) } - local unitId - local unitData - local AlivePlayerUnits = {} - - for CoalitionId, CoalitionData in pairs( CoalitionsData ) do - self:T3( { "_FollowPlayersScheduled", CoalitionData } ) - for UnitId, UnitData in pairs( CoalitionData ) do - self:_AddPlayerFromUnit( UnitData ) - end - end - - return true -end - - ---- Track DEAD or CRASH events for the scoring. --- @param #SCORING self --- @param Event#EVENTDATA Event -function SCORING:_EventOnDeadOrCrash( Event ) - self:F( { Event } ) - - local TargetUnit = nil - local TargetGroup = nil - local TargetUnitName = "" - local TargetGroupName = "" - local TargetPlayerName = "" - local TargetCoalition = nil - local TargetCategory = nil - local TargetType = nil - local TargetUnitCoalition = nil - local TargetUnitCategory = nil - local TargetUnitType = nil - - if Event.IniDCSUnit then - - TargetUnit = Event.IniDCSUnit - TargetUnitName = Event.IniDCSUnitName - TargetGroup = Event.IniDCSGroup - TargetGroupName = Event.IniDCSGroupName - TargetPlayerName = TargetUnit:getPlayerName() - - TargetCoalition = TargetUnit:getCoalition() - --TargetCategory = TargetUnit:getCategory() - TargetCategory = TargetUnit:getDesc().category -- Workaround - TargetType = TargetUnit:getTypeName() - - TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] - TargetUnitCategory = _SCORINGCategory[TargetCategory] - TargetUnitType = TargetType - - self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType } ) - end - - for PlayerName, PlayerData in pairs( self.Players ) do - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Something got killed" ) - - -- Some variables - local InitUnitName = PlayerData.UnitName - local InitUnitType = PlayerData.UnitType - local InitCoalition = PlayerData.UnitCoalition - local InitCategory = PlayerData.UnitCategory - local InitUnitCoalition = _SCORINGCoalition[InitCoalition] - local InitUnitCategory = _SCORINGCategory[InitCategory] - - self:T( { InitUnitName, InitUnitType, InitUnitCoalition, InitCoalition, InitUnitCategory, InitCategory } ) - - -- What is he hitting? - if TargetCategory then - if PlayerData and PlayerData.Hit and PlayerData.Hit[TargetCategory] and PlayerData.Hit[TargetCategory][TargetUnitName] then -- Was there a hit for this unit for this player before registered??? - if not PlayerData.Kill[TargetCategory] then - PlayerData.Kill[TargetCategory] = {} - end - if not PlayerData.Kill[TargetCategory][TargetType] then - PlayerData.Kill[TargetCategory][TargetType] = {} - PlayerData.Kill[TargetCategory][TargetType].Score = 0 - PlayerData.Kill[TargetCategory][TargetType].ScoreKill = 0 - PlayerData.Kill[TargetCategory][TargetType].Penalty = 0 - PlayerData.Kill[TargetCategory][TargetType].PenaltyKill = 0 - end - - if InitCoalition == TargetCoalition then - PlayerData.Penalty = PlayerData.Penalty + 25 - PlayerData.Kill[TargetCategory][TargetType].Penalty = PlayerData.Kill[TargetCategory][TargetType].Penalty + 25 - PlayerData.Kill[TargetCategory][TargetType].PenaltyKill = PlayerData.Kill[TargetCategory][TargetType].PenaltyKill + 1 - MESSAGE:New( "Player '" .. PlayerName .. "' killed a friendly " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - PlayerData.Kill[TargetCategory][TargetType].PenaltyKill .. " times. Penalty: -" .. PlayerData.Kill[TargetCategory][TargetType].Penalty .. - ". Score Total:" .. PlayerData.Score - PlayerData.Penalty, - "", 5, "/PENALTY" .. PlayerName .. "/" .. InitUnitName ):ToAll() - self:ScoreCSV( PlayerName, "KILL_PENALTY", 1, -125, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - else - PlayerData.Score = PlayerData.Score + 10 - PlayerData.Kill[TargetCategory][TargetType].Score = PlayerData.Kill[TargetCategory][TargetType].Score + 10 - PlayerData.Kill[TargetCategory][TargetType].ScoreKill = PlayerData.Kill[TargetCategory][TargetType].ScoreKill + 1 - MESSAGE:New( "Player '" .. PlayerName .. "' killed an enemy " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - PlayerData.Kill[TargetCategory][TargetType].ScoreKill .. " times. Score: " .. PlayerData.Kill[TargetCategory][TargetType].Score .. - ". Score Total:" .. PlayerData.Score - PlayerData.Penalty, - "", 5, "/SCORE" .. PlayerName .. "/" .. InitUnitName ):ToAll() - self:ScoreCSV( PlayerName, "KILL_SCORE", 1, 10, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - end - end - end - end - end -end - - - ---- Add a new player entering a Unit. -function SCORING:_AddPlayerFromUnit( UnitData ) - self:F( UnitData ) - - if UnitData and UnitData:isExist() then - local UnitName = UnitData:getName() - local PlayerName = UnitData:getPlayerName() - local UnitDesc = UnitData:getDesc() - local UnitCategory = UnitDesc.category - local UnitCoalition = UnitData:getCoalition() - local UnitTypeName = UnitData:getTypeName() - - self:T( { PlayerName, UnitName, UnitCategory, UnitCoalition, UnitTypeName } ) - - if self.Players[PlayerName] == nil then -- I believe this is the place where a Player gets a life in a mission when he enters a unit ... - self.Players[PlayerName] = {} - self.Players[PlayerName].Hit = {} - self.Players[PlayerName].Kill = {} - self.Players[PlayerName].Mission = {} - - -- for CategoryID, CategoryName in pairs( SCORINGCategory ) do - -- self.Players[PlayerName].Hit[CategoryID] = {} - -- self.Players[PlayerName].Kill[CategoryID] = {} - -- end - self.Players[PlayerName].HitPlayers = {} - self.Players[PlayerName].HitUnits = {} - self.Players[PlayerName].Score = 0 - self.Players[PlayerName].Penalty = 0 - self.Players[PlayerName].PenaltyCoalition = 0 - self.Players[PlayerName].PenaltyWarning = 0 - end - - if not self.Players[PlayerName].UnitCoalition then - self.Players[PlayerName].UnitCoalition = UnitCoalition - else - if self.Players[PlayerName].UnitCoalition ~= UnitCoalition then - self.Players[PlayerName].Penalty = self.Players[PlayerName].Penalty + 50 - self.Players[PlayerName].PenaltyCoalition = self.Players[PlayerName].PenaltyCoalition + 1 - MESSAGE:New( "Player '" .. PlayerName .. "' changed coalition from " .. _SCORINGCoalition[self.Players[PlayerName].UnitCoalition] .. " to " .. _SCORINGCoalition[UnitCoalition] .. - "(changed " .. self.Players[PlayerName].PenaltyCoalition .. " times the coalition). 50 Penalty points added.", - "", - 2, - "/PENALTYCOALITION" .. PlayerName - ):ToAll() - self:ScoreCSV( PlayerName, "COALITION_PENALTY", 1, -50, self.Players[PlayerName].UnitName, _SCORINGCoalition[self.Players[PlayerName].UnitCoalition], _SCORINGCategory[self.Players[PlayerName].UnitCategory], self.Players[PlayerName].UnitType, - UnitName, _SCORINGCoalition[UnitCoalition], _SCORINGCategory[UnitCategory], UnitData:getTypeName() ) - end - end - self.Players[PlayerName].UnitName = UnitName - self.Players[PlayerName].UnitCoalition = UnitCoalition - self.Players[PlayerName].UnitCategory = UnitCategory - self.Players[PlayerName].UnitType = UnitTypeName - - if self.Players[PlayerName].Penalty > 100 then - if self.Players[PlayerName].PenaltyWarning < 1 then - MESSAGE:New( "Player '" .. PlayerName .. "': WARNING! If you continue to commit FRATRICIDE and have a PENALTY score higher than 150, you will be COURT MARTIALED and DISMISSED from this mission! \nYour total penalty is: " .. self.Players[PlayerName].Penalty, - "", - 30, - "/PENALTYCOALITION" .. PlayerName - ):ToAll() - self.Players[PlayerName].PenaltyWarning = self.Players[PlayerName].PenaltyWarning + 1 - end - end - - if self.Players[PlayerName].Penalty > 150 then - ClientGroup = GROUP:NewFromDCSUnit( UnitData ) - ClientGroup:Destroy() - MESSAGE:New( "Player '" .. PlayerName .. "' committed FRATRICIDE, he will be COURT MARTIALED and is DISMISSED from this mission!", - "", - 10, - "/PENALTYCOALITION" .. PlayerName - ):ToAll() - end - - end -end - - ---- Registers Scores the players completing a Mission Task. -function SCORING:_AddMissionTaskScore( PlayerUnit, MissionName, Score ) - self:F( { PlayerUnit, MissionName, Score } ) - - local PlayerName = PlayerUnit:getPlayerName() - - if not self.Players[PlayerName].Mission[MissionName] then - self.Players[PlayerName].Mission[MissionName] = {} - self.Players[PlayerName].Mission[MissionName].ScoreTask = 0 - self.Players[PlayerName].Mission[MissionName].ScoreMission = 0 - end - - self:T( PlayerName ) - self:T( self.Players[PlayerName].Mission[MissionName] ) - - self.Players[PlayerName].Score = self.Players[PlayerName].Score + Score - self.Players[PlayerName].Mission[MissionName].ScoreTask = self.Players[PlayerName].Mission[MissionName].ScoreTask + Score - - MESSAGE:New( "Player '" .. PlayerName .. "' has finished another Task in Mission '" .. MissionName .. "'. " .. - Score .. " Score points added.", - "", 20, "/SCORETASK" .. PlayerName ):ToAll() - - self:ScoreCSV( PlayerName, "TASK_" .. MissionName:gsub( ' ', '_' ), 1, Score, PlayerUnit:getName() ) -end - - ---- Registers Mission Scores for possible multiple players that contributed in the Mission. -function SCORING:_AddMissionScore( MissionName, Score ) - self:F( { MissionName, Score } ) - - for PlayerName, PlayerData in pairs( self.Players ) do - - if PlayerData.Mission[MissionName] then - PlayerData.Score = PlayerData.Score + Score - PlayerData.Mission[MissionName].ScoreMission = PlayerData.Mission[MissionName].ScoreMission + Score - MESSAGE:New( "Player '" .. PlayerName .. "' has finished Mission '" .. MissionName .. "'. " .. - Score .. " Score points added.", - "", 20, "/SCOREMISSION" .. PlayerName ):ToAll() - self:ScoreCSV( PlayerName, "MISSION_" .. MissionName:gsub( ' ', '_' ), 1, Score ) - end - end -end - ---- Handles the OnHit event for the scoring. --- @param #SCORING self --- @param Event#EVENTDATA Event -function SCORING:_EventOnHit( Event ) - self:F( { Event } ) - - local InitUnit = nil - local InitUnitName = "" - local InitGroup = nil - local InitGroupName = "" - local InitPlayerName = nil - - local InitCoalition = nil - local InitCategory = nil - local InitType = nil - local InitUnitCoalition = nil - local InitUnitCategory = nil - local InitUnitType = nil - - local TargetUnit = nil - local TargetUnitName = "" - local TargetGroup = nil - local TargetGroupName = "" - local TargetPlayerName = "" - - local TargetCoalition = nil - local TargetCategory = nil - local TargetType = nil - local TargetUnitCoalition = nil - local TargetUnitCategory = nil - local TargetUnitType = nil - - if Event.IniDCSUnit then - - InitUnit = Event.IniDCSUnit - InitUnitName = Event.IniDCSUnitName - InitGroup = Event.IniDCSGroup - InitGroupName = Event.IniDCSGroupName - InitPlayerName = InitUnit:getPlayerName() - - InitCoalition = InitUnit:getCoalition() - --TODO: Workaround Client DCS Bug - --InitCategory = InitUnit:getCategory() - InitCategory = InitUnit:getDesc().category - InitType = InitUnit:getTypeName() - - InitUnitCoalition = _SCORINGCoalition[InitCoalition] - InitUnitCategory = _SCORINGCategory[InitCategory] - InitUnitType = InitType - - self:T( { InitUnitName, InitGroupName, InitPlayerName, InitCoalition, InitCategory, InitType , InitUnitCoalition, InitUnitCategory, InitUnitType } ) - end - - - if Event.TgtDCSUnit then - - TargetUnit = Event.TgtDCSUnit - TargetUnitName = Event.TgtDCSUnitName - TargetGroup = Event.TgtDCSGroup - TargetGroupName = Event.TgtDCSGroupName - TargetPlayerName = TargetUnit:getPlayerName() - - TargetCoalition = TargetUnit:getCoalition() - --TODO: Workaround Client DCS Bug - --TargetCategory = TargetUnit:getCategory() - TargetCategory = TargetUnit:getDesc().category - TargetType = TargetUnit:getTypeName() - - TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] - TargetUnitCategory = _SCORINGCategory[TargetCategory] - TargetUnitType = TargetType - - self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType, TargetUnitCoalition, TargetUnitCategory, TargetUnitType } ) - end - - if InitPlayerName ~= nil then -- It is a player that is hitting something - self:_AddPlayerFromUnit( InitUnit ) - if self.Players[InitPlayerName] then -- This should normally not happen, but i'll test it anyway. - if TargetPlayerName ~= nil then -- It is a player hitting another player ... - self:_AddPlayerFromUnit( TargetUnit ) - self.Players[InitPlayerName].HitPlayers = self.Players[InitPlayerName].HitPlayers + 1 - end - - self:T( "Hitting Something" ) - -- What is he hitting? - if TargetCategory then - if not self.Players[InitPlayerName].Hit[TargetCategory] then - self.Players[InitPlayerName].Hit[TargetCategory] = {} - end - if not self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName] then - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName] = {} - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score = 0 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty = 0 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit = 0 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit = 0 - end - local Score = 0 - if InitCoalition == TargetCoalition then - self.Players[InitPlayerName].Penalty = self.Players[InitPlayerName].Penalty + 10 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty + 10 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit + 1 - MESSAGE:New( "Player '" .. InitPlayerName .. "' hit a friendly " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit .. " times. Penalty: -" .. self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty .. - ". Score Total:" .. self.Players[InitPlayerName].Score - self.Players[InitPlayerName].Penalty, - "", - 2, - "/PENALTY" .. InitPlayerName .. "/" .. InitUnitName - ):ToAll() - self:ScoreCSV( InitPlayerName, "HIT_PENALTY", 1, -25, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - else - self.Players[InitPlayerName].Score = self.Players[InitPlayerName].Score + 10 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score + 1 - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit + 1 - MESSAGE:New( "Player '" .. InitPlayerName .. "' hit a target " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit .. " times. Score: " .. self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score .. - ". Score Total:" .. self.Players[InitPlayerName].Score - self.Players[InitPlayerName].Penalty, - "", - 2, - "/SCORE" .. InitPlayerName .. "/" .. InitUnitName - ):ToAll() - self:ScoreCSV( InitPlayerName, "HIT_SCORE", 1, 1, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - end - end - end - elseif InitPlayerName == nil then -- It is an AI hitting a player??? - - end -end - - -function SCORING:ReportScoreAll() - - env.info( "Hello World " ) - - local ScoreMessage = "" - local PlayerMessage = "" - - self:T( "Score Report" ) - - for PlayerName, PlayerData in pairs( self.Players ) do - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local PlayerScore = 0 - local PlayerPenalty = 0 - - ScoreMessage = ":\n" - - local ScoreMessageHits = "" - - for CategoryID, CategoryName in pairs( _SCORINGCategory ) do - self:T( CategoryName ) - if PlayerData.Hit[CategoryID] then - local Score = 0 - local ScoreHit = 0 - local Penalty = 0 - local PenaltyHit = 0 - self:T( "Hit scores exist for player " .. PlayerName ) - for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do - Score = Score + UnitData.Score - ScoreHit = ScoreHit + UnitData.ScoreHit - Penalty = Penalty + UnitData.Penalty - PenaltyHit = UnitData.PenaltyHit - end - local ScoreMessageHit = string.format( "%s:%d ", CategoryName, Score - Penalty ) - self:T( ScoreMessageHit ) - ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageHits ~= "" then - ScoreMessage = ScoreMessage .. " Hits: " .. ScoreMessageHits .. "\n" - end - - local ScoreMessageKills = "" - for CategoryID, CategoryName in pairs( _SCORINGCategory ) do - self:T( "Kill scores exist for player " .. PlayerName ) - if PlayerData.Kill[CategoryID] then - local Score = 0 - local ScoreKill = 0 - local Penalty = 0 - local PenaltyKill = 0 - - for UnitName, UnitData in pairs( PlayerData.Kill[CategoryID] ) do - Score = Score + UnitData.Score - ScoreKill = ScoreKill + UnitData.ScoreKill - Penalty = Penalty + UnitData.Penalty - PenaltyKill = PenaltyKill + UnitData.PenaltyKill - end - - local ScoreMessageKill = string.format( " %s:%d ", CategoryName, Score - Penalty ) - self:T( ScoreMessageKill ) - ScoreMessageKills = ScoreMessageKills .. ScoreMessageKill - - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageKills = ScoreMessageKills .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageKills ~= "" then - ScoreMessage = ScoreMessage .. " Kills: " .. ScoreMessageKills .. "\n" - end - - local ScoreMessageCoalitionChangePenalties = "" - if PlayerData.PenaltyCoalition ~= 0 then - ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) - PlayerPenalty = PlayerPenalty + PlayerData.Penalty - end - if ScoreMessageCoalitionChangePenalties ~= "" then - ScoreMessage = ScoreMessage .. " Coalition Penalties: " .. ScoreMessageCoalitionChangePenalties .. "\n" - end - - local ScoreMessageMission = "" - local ScoreMission = 0 - local ScoreTask = 0 - for MissionName, MissionData in pairs( PlayerData.Mission ) do - ScoreMission = ScoreMission + MissionData.ScoreMission - ScoreTask = ScoreTask + MissionData.ScoreTask - ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " - end - PlayerScore = PlayerScore + ScoreMission + ScoreTask - - if ScoreMessageMission ~= "" then - ScoreMessage = ScoreMessage .. " Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ")\n" - end - - PlayerMessage = PlayerMessage .. string.format( "Player '%s' Score:%d (%d Score -%d Penalties)%s", PlayerName, PlayerScore - PlayerPenalty, PlayerScore, PlayerPenalty, ScoreMessage ) - end - end - MESSAGE:New( PlayerMessage, "Player Scores", 30, "AllPlayerScores"):ToAll() -end - - -function SCORING:ReportScorePlayer() - - env.info( "Hello World " ) - - local ScoreMessage = "" - local PlayerMessage = "" - - self:T( "Score Report" ) - - for PlayerName, PlayerData in pairs( self.Players ) do - if PlayerData then -- This should normally not happen, but i'll test it anyway. - self:T( "Score Player: " .. PlayerName ) - - -- Some variables - local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] - local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] - local InitUnitType = PlayerData.UnitType - local InitUnitName = PlayerData.UnitName - - local PlayerScore = 0 - local PlayerPenalty = 0 - - ScoreMessage = "" - - local ScoreMessageHits = "" - - for CategoryID, CategoryName in pairs( _SCORINGCategory ) do - self:T( CategoryName ) - if PlayerData.Hit[CategoryID] then - local Score = 0 - local ScoreHit = 0 - local Penalty = 0 - local PenaltyHit = 0 - self:T( "Hit scores exist for player " .. PlayerName ) - for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do - Score = Score + UnitData.Score - ScoreHit = ScoreHit + UnitData.ScoreHit - Penalty = Penalty + UnitData.Penalty - PenaltyHit = UnitData.PenaltyHit - end - local ScoreMessageHit = string.format( "\n %s = %d score(%d;-%d) hits(#%d;#-%d)", CategoryName, Score - Penalty, Score, Penalty, ScoreHit, PenaltyHit ) - self:T( ScoreMessageHit ) - ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageHits ~= "" then - ScoreMessage = ScoreMessage .. "\n Hits: " .. ScoreMessageHits .. " " - end - - local ScoreMessageKills = "" - for CategoryID, CategoryName in pairs( _SCORINGCategory ) do - self:T( "Kill scores exist for player " .. PlayerName ) - if PlayerData.Kill[CategoryID] then - local Score = 0 - local ScoreKill = 0 - local Penalty = 0 - local PenaltyKill = 0 - - for UnitName, UnitData in pairs( PlayerData.Kill[CategoryID] ) do - Score = Score + UnitData.Score - ScoreKill = ScoreKill + UnitData.ScoreKill - Penalty = Penalty + UnitData.Penalty - PenaltyKill = PenaltyKill + UnitData.PenaltyKill - end - - local ScoreMessageKill = string.format( "\n %s = %d score(%d;-%d) hits(#%d;#-%d)", CategoryName, Score - Penalty, Score, Penalty, ScoreKill, PenaltyKill ) - self:T( ScoreMessageKill ) - ScoreMessageKills = ScoreMessageKills .. ScoreMessageKill - - PlayerScore = PlayerScore + Score - PlayerPenalty = PlayerPenalty + Penalty - else - --ScoreMessageKills = ScoreMessageKills .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) - end - end - if ScoreMessageKills ~= "" then - ScoreMessage = ScoreMessage .. "\n Kills: " .. ScoreMessageKills .. " " - end - - local ScoreMessageCoalitionChangePenalties = "" - if PlayerData.PenaltyCoalition ~= 0 then - ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) - PlayerPenalty = PlayerPenalty + PlayerData.Penalty - end - if ScoreMessageCoalitionChangePenalties ~= "" then - ScoreMessage = ScoreMessage .. "\n Coalition: " .. ScoreMessageCoalitionChangePenalties .. " " - end - - local ScoreMessageMission = "" - local ScoreMission = 0 - local ScoreTask = 0 - for MissionName, MissionData in pairs( PlayerData.Mission ) do - ScoreMission = ScoreMission + MissionData.ScoreMission - ScoreTask = ScoreTask + MissionData.ScoreTask - ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " - end - PlayerScore = PlayerScore + ScoreMission + ScoreTask - - if ScoreMessageMission ~= "" then - ScoreMessage = ScoreMessage .. "\n Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ") " - end - - PlayerMessage = PlayerMessage .. string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties ):%s", PlayerName, PlayerScore - PlayerPenalty, PlayerScore, PlayerPenalty, ScoreMessage ) - end - end - MESSAGE:New( PlayerMessage, "Player Scores", 30, "AllPlayerScores"):ToAll() - -end - - -function SCORING:SecondsToClock(sSeconds) - local nSeconds = sSeconds - if nSeconds == 0 then - --return nil; - return "00:00:00"; - else - nHours = string.format("%02.f", math.floor(nSeconds/3600)); - nMins = string.format("%02.f", math.floor(nSeconds/60 - (nHours*60))); - nSecs = string.format("%02.f", math.floor(nSeconds - nHours*3600 - nMins *60)); - return nHours..":"..nMins..":"..nSecs - end -end - ---- Opens a score CSV file to log the scores. --- @param #SCORING self --- @param #string ScoringCSV --- @return #SCORING self --- @usage --- -- Open a new CSV file to log the scores of the game Gori Valley. Let the name of the CSV file begin with "Player Scores". --- ScoringObject = SCORING:New( "Gori Valley" ) --- ScoringObject:OpenCSV( "Player Scores" ) -function SCORING:OpenCSV( ScoringCSV ) - self:F( ScoringCSV ) - - if lfs and io and os then - if ScoringCSV then - self.ScoringCSV = ScoringCSV - local fdir = lfs.writedir() .. [[Logs\]] .. self.ScoringCSV .. " " .. os.date( "%Y-%m-%d %H-%M-%S" ) .. ".csv" - - self.CSVFile, self.err = io.open( fdir, "w+" ) - if not self.CSVFile then - error( "Error: Cannot open CSV file in " .. lfs.writedir() ) - end - - self.CSVFile:write( '"GameName","RunTime","Time","PlayerName","ScoreType","PlayerUnitCoaltion","PlayerUnitCategory","PlayerUnitType","PlayerUnitName","TargetUnitCoalition","TargetUnitCategory","TargetUnitType","TargetUnitName","Times","Score"\n' ) - - self.RunTime = os.date("%y-%m-%d_%H-%M-%S") - else - error( "A string containing the CSV file name must be given." ) - end - else - self:E( "The MissionScripting.lua file has not been changed to allow lfs, io and os modules to be used..." ) - end - return self -end - - ---- Registers a score for a player. --- @param #SCORING self --- @param #string PlayerName The name of the player. --- @param #string ScoreType The type of the score. --- @param #string ScoreTimes The amount of scores achieved. --- @param #string ScoreAmount The score given. --- @param #string PlayerUnitName The unit name of the player. --- @param #string PlayerUnitCoalition The coalition of the player unit. --- @param #string PlayerUnitCategory The category of the player unit. --- @param #string PlayerUnitType The type of the player unit. --- @param #string TargetUnitName The name of the target unit. --- @param #string TargetUnitCoalition The coalition of the target unit. --- @param #string TargetUnitCategory The category of the target unit. --- @param #string TargetUnitType The type of the target unit. --- @return #SCORING self -function SCORING:ScoreCSV( PlayerName, ScoreType, ScoreTimes, ScoreAmount, PlayerUnitName, PlayerUnitCoalition, PlayerUnitCategory, PlayerUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) - --write statistic information to file - local ScoreTime = self:SecondsToClock( timer.getTime() ) - PlayerName = PlayerName:gsub( '"', '_' ) - - if PlayerUnitName and PlayerUnitName ~= '' then - local PlayerUnit = Unit.getByName( PlayerUnitName ) - - if PlayerUnit then - if not PlayerUnitCategory then - --PlayerUnitCategory = SCORINGCategory[PlayerUnit:getCategory()] - PlayerUnitCategory = _SCORINGCategory[PlayerUnit:getDesc().category] - end - - if not PlayerUnitCoalition then - PlayerUnitCoalition = _SCORINGCoalition[PlayerUnit:getCoalition()] - end - - if not PlayerUnitType then - PlayerUnitType = PlayerUnit:getTypeName() - end - else - PlayerUnitName = '' - PlayerUnitCategory = '' - PlayerUnitCoalition = '' - PlayerUnitType = '' - end - else - PlayerUnitName = '' - PlayerUnitCategory = '' - PlayerUnitCoalition = '' - PlayerUnitType = '' - end - - if not TargetUnitCoalition then - TargetUnitCoalition = '' - end - - if not TargetUnitCategory then - TargetUnitCategory = '' - end - - if not TargetUnitType then - TargetUnitType = '' - end - - if not TargetUnitName then - TargetUnitName = '' - end - - if lfs and io and os then - self.CSVFile:write( - '"' .. self.GameName .. '"' .. ',' .. - '"' .. self.RunTime .. '"' .. ',' .. - '' .. ScoreTime .. '' .. ',' .. - '"' .. PlayerName .. '"' .. ',' .. - '"' .. ScoreType .. '"' .. ',' .. - '"' .. PlayerUnitCoalition .. '"' .. ',' .. - '"' .. PlayerUnitCategory .. '"' .. ',' .. - '"' .. PlayerUnitType .. '"' .. ',' .. - '"' .. PlayerUnitName .. '"' .. ',' .. - '"' .. TargetUnitCoalition .. '"' .. ',' .. - '"' .. TargetUnitCategory .. '"' .. ',' .. - '"' .. TargetUnitType .. '"' .. ',' .. - '"' .. TargetUnitName .. '"' .. ',' .. - '' .. ScoreTimes .. '' .. ',' .. - '' .. ScoreAmount - ) - - self.CSVFile:write( "\n" ) - end -end - - -function SCORING:CloseCSV() - if lfs and io and os then - self.CSVFile:close() - end -end - ---- CARGO Classes --- @module CARGO - -Include.File( "Routines" ) -Include.File( "Base" ) -Include.File( "Message" ) -Include.File( "Scheduler" ) - - ---- Clients are those Groups defined within the Mission Editor that have the skillset defined as "Client" or "Player". --- These clients are defined within the Mission Orchestration Framework (MOF) - -CARGOS = {} - - -CARGO_ZONE = { - ClassName="CARGO_ZONE", - CargoZoneName = '', - CargoHostUnitName = '', - SIGNAL = { - TYPE = { - SMOKE = { ID = 1, TEXT = "smoke" }, - FLARE = { ID = 2, TEXT = "flare" } - }, - COLOR = { - GREEN = { ID = 1, TRIGGERCOLOR = trigger.smokeColor.Green, TEXT = "A green" }, - RED = { ID = 2, TRIGGERCOLOR = trigger.smokeColor.Red, TEXT = "A red" }, - WHITE = { ID = 3, TRIGGERCOLOR = trigger.smokeColor.White, TEXT = "A white" }, - ORANGE = { ID = 4, TRIGGERCOLOR = trigger.smokeColor.Orange, TEXT = "An orange" }, - BLUE = { ID = 5, TRIGGERCOLOR = trigger.smokeColor.Blue, TEXT = "A blue" }, - YELLOW = { ID = 6, TRIGGERCOLOR = trigger.flareColor.Yellow, TEXT = "A yellow" } - } - } -} - ---- Creates a new zone where cargo can be collected or deployed. --- The zone functionality is useful to smoke or indicate routes for cargo pickups or deployments. --- Provide the zone name as declared in the mission file into the CargoZoneName in the :New method. --- An optional parameter is the CargoHostName, which is a Group declared with Late Activation switched on in the mission file. --- The CargoHostName is the "host" of the cargo zone: --- --- * It will smoke the zone position when a client is approaching the zone. --- * Depending on the cargo type, it will assist in the delivery of the cargo by driving to and from the client. --- --- @param #CARGO_ZONE self --- @param #string CargoZoneName The name of the zone as declared within the mission editor. --- @param #string CargoHostName The name of the Group "hosting" the zone. The Group MUST NOT be a static, and must be a "mobile" unit. -function CARGO_ZONE:New( CargoZoneName, CargoHostName ) local self = BASE:Inherit( self, ZONE:New( CargoZoneName ) ) - self:F( { CargoZoneName, CargoHostName } ) - - self.CargoZoneName = CargoZoneName - self.SignalHeight = 2 - --self.CargoZone = trigger.misc.getZone( CargoZoneName ) - - - if CargoHostName then - self.CargoHostName = CargoHostName - end - - self:T( self.CargoZoneName ) - - return self -end - -function CARGO_ZONE:Spawn() - self:F( self.CargoHostName ) - - if self.CargoHostName then -- Only spawn a host in the zone when there is one given as a parameter in the New function. - if self.CargoHostSpawn then - local CargoHostGroup = self.CargoHostSpawn:GetGroupFromIndex() - if CargoHostGroup and CargoHostGroup:IsAlive() then - else - self.CargoHostSpawn:ReSpawn( 1 ) - end - else - self:T( "Initialize CargoHostSpawn" ) - self.CargoHostSpawn = SPAWN:New( self.CargoHostName ):Limit( 1, 1 ) - self.CargoHostSpawn:ReSpawn( 1 ) - end - end - - return self -end - -function CARGO_ZONE:GetHostUnit() - self:F( self ) - - if self.CargoHostName then - - -- A Host has been given, signal the host - local CargoHostGroup = self.CargoHostSpawn:GetGroupFromIndex() - local CargoHostUnit - if CargoHostGroup and CargoHostGroup:IsAlive() then - CargoHostUnit = CargoHostGroup:GetUnit(1) - else - CargoHostUnit = StaticObject.getByName( self.CargoHostName ) - end - - return CargoHostUnit - end - - return nil -end - -function CARGO_ZONE:ReportCargosToClient( Client, CargoType ) - self:F() - - local SignalUnit = self:GetHostUnit() - - if SignalUnit then - - local SignalUnitTypeName = SignalUnit:getTypeName() - - local HostMessage = "" - - local IsCargo = false - for CargoID, Cargo in pairs( CARGOS ) do - if Cargo.CargoType == Task.CargoType then - if Cargo:IsStatusNone() then - HostMessage = HostMessage .. " - " .. Cargo.CargoName .. " - " .. Cargo.CargoType .. " (" .. Cargo.Weight .. "kg)" .. "\n" - IsCargo = true - end - end - end - - if not IsCargo then - HostMessage = "No Cargo Available." - end - - Client:Message( HostMessage, 20, Mission.Name .. "/StageHosts." .. SignalUnitTypeName, SignalUnitTypeName .. ": Reporting Cargo", 10 ) - end -end - - -function CARGO_ZONE:Signal() - self:F() - - local Signalled = false - - if self.SignalType then - - if self.CargoHostName then - - -- A Host has been given, signal the host - - local SignalUnit = self:GetHostUnit() - - if SignalUnit then - - self:T( 'Signalling Unit' ) - local SignalVehiclePos = SignalUnit:GetPointVec3() - SignalVehiclePos.y = SignalVehiclePos.y + 2 - - if self.SignalType.ID == CARGO_ZONE.SIGNAL.TYPE.SMOKE.ID then - - trigger.action.smoke( SignalVehiclePos, self.SignalColor.TRIGGERCOLOR ) - Signalled = true - - elseif self.SignalType.ID == CARGO_ZONE.SIGNAL.TYPE.FLARE.ID then - - trigger.action.signalFlare( SignalVehiclePos, self.SignalColor.TRIGGERCOLOR , 0 ) - Signalled = false - - end - end - - else - - local ZonePointVec3 = self:GetPointVec3( self.SignalHeight ) -- Get the zone position + the landheight + 2 meters - - if self.SignalType.ID == CARGO_ZONE.SIGNAL.TYPE.SMOKE.ID then - - trigger.action.smoke( ZonePointVec3, self.SignalColor.TRIGGERCOLOR ) - Signalled = true - - elseif self.SignalType.ID == CARGO_ZONE.SIGNAL.TYPE.FLARE.ID then - trigger.action.signalFlare( ZonePointVec3, self.SignalColor.TRIGGERCOLOR, 0 ) - Signalled = false - - end - end - end - - return Signalled - -end - -function CARGO_ZONE:WhiteSmoke( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.SMOKE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.WHITE - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - -function CARGO_ZONE:BlueSmoke( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.SMOKE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.BLUE - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - -function CARGO_ZONE:RedSmoke( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.SMOKE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.RED - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - -function CARGO_ZONE:OrangeSmoke( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.SMOKE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.ORANGE - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - -function CARGO_ZONE:GreenSmoke( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.SMOKE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.GREEN - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - - -function CARGO_ZONE:WhiteFlare( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.FLARE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.WHITE - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - -function CARGO_ZONE:RedFlare( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.FLARE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.RED - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - -function CARGO_ZONE:GreenFlare( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.FLARE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.GREEN - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - -function CARGO_ZONE:YellowFlare( SignalHeight ) - self:F() - - self.SignalType = CARGO_ZONE.SIGNAL.TYPE.FLARE - self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.YELLOW - - if SignalHeight then - self.SignalHeight = SignalHeight - end - - return self -end - - -function CARGO_ZONE:GetCargoHostUnit() - self:F( self ) - - if self.CargoHostSpawn then - local CargoHostGroup = self.CargoHostSpawn:GetGroupFromIndex(1) - if CargoHostGroup and CargoHostGroup:IsAlive() then - local CargoHostUnit = CargoHostGroup:GetUnit(1) - if CargoHostUnit and CargoHostUnit:IsAlive() then - return CargoHostUnit - end - end - end - - return nil -end - -function CARGO_ZONE:GetCargoZoneName() - self:F() - - return self.CargoZoneName -end - -CARGO = { - ClassName = "CARGO", - STATUS = { - NONE = 0, - LOADED = 1, - UNLOADED = 2, - LOADING = 3 - }, - CargoClient = nil -} - ---- Add Cargo to the mission... Cargo functionality needs to be reworked a bit, so this is still under construction. I need to make a CARGO Class... -function CARGO:New( CargoType, CargoName, CargoWeight ) local self = BASE:Inherit( self, BASE:New() ) - self:F( { CargoType, CargoName, CargoWeight } ) - - - self.CargoType = CargoType - self.CargoName = CargoName - self.CargoWeight = CargoWeight - - self:StatusNone() - - return self -end - -function CARGO:Spawn( Client ) - self:F() - - return self - -end - -function CARGO:IsNear( Client, LandingZone ) - self:F() - - local Near = true - - return Near - -end - - -function CARGO:IsLoadingToClient() - self:F() - - if self:IsStatusLoading() then - return self.CargoClient - end - - return nil - -end - - -function CARGO:IsLoadedInClient() - self:F() - - if self:IsStatusLoaded() then - return self.CargoClient - end - - return nil - -end - - -function CARGO:UnLoad( Client, TargetZoneName ) - self:F() - - self:StatusUnLoaded() - - return self -end - -function CARGO:OnBoard( Client, LandingZone ) - self:F() - - local Valid = true - - self.CargoClient = Client - local ClientUnit = Client:GetClientGroupDCSUnit() - - return Valid -end - -function CARGO:OnBoarded( Client, LandingZone ) - self:F() - - local OnBoarded = true - - return OnBoarded -end - -function CARGO:Load( Client ) - self:F() - - self:StatusLoaded( Client ) - - return self -end - -function CARGO:IsLandingRequired() - self:F() - return true -end - -function CARGO:IsSlingLoad() - self:F() - return false -end - - -function CARGO:StatusNone() - self:F() - - self.CargoClient = nil - self.CargoStatus = CARGO.STATUS.NONE - - return self -end - -function CARGO:StatusLoading( Client ) - self:F() - - self.CargoClient = Client - self.CargoStatus = CARGO.STATUS.LOADING - self:T( "Cargo " .. self.CargoName .. " loading to Client: " .. self.CargoClient:GetClientGroupName() ) - - return self -end - -function CARGO:StatusLoaded( Client ) - self:F() - - self.CargoClient = Client - self.CargoStatus = CARGO.STATUS.LOADED - self:T( "Cargo " .. self.CargoName .. " loaded in Client: " .. self.CargoClient:GetClientGroupName() ) - - return self -end - -function CARGO:StatusUnLoaded() - self:F() - - self.CargoClient = nil - self.CargoStatus = CARGO.STATUS.UNLOADED - - return self -end - - -function CARGO:IsStatusNone() - self:F() - - return self.CargoStatus == CARGO.STATUS.NONE -end - -function CARGO:IsStatusLoading() - self:F() - - return self.CargoStatus == CARGO.STATUS.LOADING -end - -function CARGO:IsStatusLoaded() - self:F() - - return self.CargoStatus == CARGO.STATUS.LOADED -end - -function CARGO:IsStatusUnLoaded() - self:F() - - return self.CargoStatus == CARGO.STATUS.UNLOADED -end - - -CARGO_GROUP = { - ClassName = "CARGO_GROUP" -} - - -function CARGO_GROUP:New( CargoType, CargoName, CargoWeight, CargoGroupTemplate, CargoZone ) local self = BASE:Inherit( self, CARGO:New( CargoType, CargoName, CargoWeight ) ) - self:F( { CargoType, CargoName, CargoWeight, CargoGroupTemplate, CargoZone } ) - - self.CargoSpawn = SPAWN:NewWithAlias( CargoGroupTemplate, CargoName ) - self.CargoZone = CargoZone - - CARGOS[self.CargoName] = self - - return self - -end - -function CARGO_GROUP:Spawn( Client ) - self:F( { Client } ) - - local SpawnCargo = true - - if self:IsStatusNone() then - local CargoGroup = Group.getByName( self.CargoName ) - if CargoGroup and CargoGroup:isExist() then - SpawnCargo = false - end - - elseif self:IsStatusLoading() then - - local Client = self:IsLoadingToClient() - if Client and Client:GetDCSGroup() then - SpawnCargo = false - else - local CargoGroup = Group.getByName( self.CargoName ) - if CargoGroup and CargoGroup:isExist() then - SpawnCargo = false - end - end - - elseif self:IsStatusLoaded() then - - local ClientLoaded = self:IsLoadedInClient() - -- Now test if another Client is alive (not this one), and it has the CARGO, then this cargo does not need to be initialized and spawned. - if ClientLoaded and ClientLoaded ~= Client then - local ClientGroup = Client:GetDCSGroup() - if ClientLoaded:GetClientGroupDCSUnit() and ClientLoaded:GetClientGroupDCSUnit():isExist() then - SpawnCargo = false - else - self:StatusNone() - end - else - -- Same Client, but now in initialize, so set back the status to None. - self:StatusNone() - end - - elseif self:IsStatusUnLoaded() then - - SpawnCargo = false - - end - - if SpawnCargo then - if self.CargoZone:GetCargoHostUnit() then - --- ReSpawn the Cargo from the CargoHost - self.CargoGroupName = self.CargoSpawn:SpawnFromUnit( self.CargoZone:GetCargoHostUnit(), 60, 30, 1 ):GetName() - else - --- ReSpawn the Cargo in the CargoZone without a host ... - self:T( self.CargoZone ) - self.CargoGroupName = self.CargoSpawn:SpawnInZone( self.CargoZone, true, 1 ):GetName() - end - self:StatusNone() - end - - self:T( { self.CargoGroupName, CARGOS[self.CargoName].CargoGroupName } ) - - return self -end - -function CARGO_GROUP:IsNear( Client, LandingZone ) - self:F() - - local Near = false - - if self.CargoGroupName then - local CargoGroup = Group.getByName( self.CargoGroupName ) - if routines.IsPartOfGroupInRadius( CargoGroup, Client:GetPositionVec3(), 250 ) then - Near = true - end - end - - return Near - -end - - -function CARGO_GROUP:OnBoard( Client, LandingZone, OnBoardSide ) - self:F() - - local Valid = true - - local ClientUnit = Client:GetClientGroupDCSUnit() - - local CarrierPos = ClientUnit:getPoint() - local CarrierPosMove = ClientUnit:getPoint() - local CarrierPosOnBoard = ClientUnit:getPoint() - - local CargoGroup = Group.getByName( self.CargoGroupName ) - - local CargoUnit = CargoGroup:getUnit(1) - local CargoPos = CargoUnit:getPoint() - - self.CargoInAir = CargoUnit:inAir() - - self:T( self.CargoInAir ) - - -- Only move the group to the carrier when the cargo is not in the air - -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea). - if not self.CargoInAir then - - local Points = {} - - self:T( 'CargoPos x = ' .. CargoPos.x .. " z = " .. CargoPos.z ) - self:T( 'CarrierPosMove x = ' .. CarrierPosMove.x .. " z = " .. CarrierPosMove.z ) - - Points[#Points+1] = routines.ground.buildWP( CargoPos, "Cone", 10 ) - - self:T( 'Points[1] x = ' .. Points[1].x .. " y = " .. Points[1].y ) - - if OnBoardSide == nil then - OnBoardSide = CLIENT.ONBOARDSIDE.NONE - end - - if OnBoardSide == CLIENT.ONBOARDSIDE.LEFT then - - self:T( "TransportCargoOnBoard: Onboarding LEFT" ) - CarrierPosMove.z = CarrierPosMove.z - 25 - CarrierPosOnBoard.z = CarrierPosOnBoard.z - 5 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - - elseif OnBoardSide == CLIENT.ONBOARDSIDE.RIGHT then - - self:T( "TransportCargoOnBoard: Onboarding RIGHT" ) - CarrierPosMove.z = CarrierPosMove.z + 25 - CarrierPosOnBoard.z = CarrierPosOnBoard.z + 5 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - - elseif OnBoardSide == CLIENT.ONBOARDSIDE.BACK then - - self:T( "TransportCargoOnBoard: Onboarding BACK" ) - CarrierPosMove.x = CarrierPosMove.x - 25 - CarrierPosOnBoard.x = CarrierPosOnBoard.x - 5 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - - elseif OnBoardSide == CLIENT.ONBOARDSIDE.FRONT then - - self:T( "TransportCargoOnBoard: Onboarding FRONT" ) - CarrierPosMove.x = CarrierPosMove.x + 25 - CarrierPosOnBoard.x = CarrierPosOnBoard.x + 5 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - - elseif OnBoardSide == CLIENT.ONBOARDSIDE.NONE then - - self:T( "TransportCargoOnBoard: Onboarding CENTRAL" ) - Points[#Points+1] = routines.ground.buildWP( CarrierPos, "Cone", 10 ) - - end - self:T( "TransportCargoOnBoard: Routing " .. self.CargoGroupName ) - - --routines.scheduleFunction( routines.goRoute, { self.CargoGroupName, Points}, timer.getTime() + 4 ) - SCHEDULER:New( self, routines.goRoute, { self.CargoGroupName, Points}, 4 ) - end - - self:StatusLoading( Client ) - - return Valid - -end - - -function CARGO_GROUP:OnBoarded( Client, LandingZone ) - self:F() - - local OnBoarded = false - - local CargoGroup = Group.getByName( self.CargoGroupName ) - - if not self.CargoInAir then - if routines.IsPartOfGroupInRadius( CargoGroup, Client:GetPositionVec3(), 25 ) then - CargoGroup:destroy() - self:StatusLoaded( Client ) - OnBoarded = true - end - else - CargoGroup:destroy() - self:StatusLoaded( Client ) - OnBoarded = true - end - - return OnBoarded -end - - -function CARGO_GROUP:UnLoad( Client, TargetZoneName ) - self:F() - - self:T( 'self.CargoName = ' .. self.CargoName ) - - local CargoGroup = self.CargoSpawn:SpawnFromUnit( Client:GetClientGroupUnit(), 60, 30 ) - - self.CargoGroupName = CargoGroup:GetName() - self:T( 'self.CargoGroupName = ' .. self.CargoGroupName ) - - CargoGroup:TaskRouteToZone( ZONE:New( TargetZoneName ), true ) - - self:StatusUnLoaded() - - return self -end - - -CARGO_PACKAGE = { - ClassName = "CARGO_PACKAGE" -} - - -function CARGO_PACKAGE:New( CargoType, CargoName, CargoWeight, CargoClient ) local self = BASE:Inherit( self, CARGO:New( CargoType, CargoName, CargoWeight ) ) - self:F( { CargoType, CargoName, CargoWeight, CargoClient } ) - - self.CargoClient = CargoClient - - CARGOS[self.CargoName] = self - - return self - -end - - -function CARGO_PACKAGE:Spawn( Client ) - self:F( { self, Client } ) - - -- this needs to be checked thoroughly - - local CargoClientGroup = self.CargoClient:GetDCSGroup() - if not CargoClientGroup then - if not self.CargoClientSpawn then - self.CargoClientSpawn = SPAWN:New( self.CargoClient:GetClientGroupName() ):Limit( 1, 1 ) - end - self.CargoClientSpawn:ReSpawn( 1 ) - end - - local SpawnCargo = true - - if self:IsStatusNone() then - - elseif self:IsStatusLoading() or self:IsStatusLoaded() then - - local CargoClientLoaded = self:IsLoadedInClient() - if CargoClientLoaded and CargoClientLoaded:GetDCSGroup() then - SpawnCargo = false - end - - elseif self:IsStatusUnLoaded() then - - SpawnCargo = false - - else - - end - - if SpawnCargo then - self:StatusLoaded( self.CargoClient ) - end - - return self -end - - -function CARGO_PACKAGE:IsNear( Client, LandingZone ) - self:F() - - local Near = false - - if self.CargoClient and self.CargoClient:GetDCSGroup() then - self:T( self.CargoClient.ClientName ) - self:T( 'Client Exists.' ) - - if routines.IsUnitInRadius( self.CargoClient:GetClientGroupDCSUnit(), Client:GetPositionVec3(), 150 ) then - Near = true - end - end - - return Near - -end - - -function CARGO_PACKAGE:OnBoard( Client, LandingZone, OnBoardSide ) - self:F() - - local Valid = true - - local ClientUnit = Client:GetClientGroupDCSUnit() - - local CarrierPos = ClientUnit:getPoint() - local CarrierPosMove = ClientUnit:getPoint() - local CarrierPosOnBoard = ClientUnit:getPoint() - local CarrierPosMoveAway = ClientUnit:getPoint() - - local CargoHostGroup = self.CargoClient:GetDCSGroup() - local CargoHostName = self.CargoClient:GetDCSGroup():getName() - - local CargoHostUnits = CargoHostGroup:getUnits() - local CargoPos = CargoHostUnits[1]:getPoint() - - local Points = {} - - self:T( 'CargoPos x = ' .. CargoPos.x .. " z = " .. CargoPos.z ) - self:T( 'CarrierPosMove x = ' .. CarrierPosMove.x .. " z = " .. CarrierPosMove.z ) - - Points[#Points+1] = routines.ground.buildWP( CargoPos, "Cone", 10 ) - - self:T( 'Points[1] x = ' .. Points[1].x .. " y = " .. Points[1].y ) - - if OnBoardSide == nil then - OnBoardSide = CLIENT.ONBOARDSIDE.NONE - end - - if OnBoardSide == CLIENT.ONBOARDSIDE.LEFT then - - self:T( "TransportCargoOnBoard: Onboarding LEFT" ) - CarrierPosMove.z = CarrierPosMove.z - 25 - CarrierPosOnBoard.z = CarrierPosOnBoard.z - 5 - CarrierPosMoveAway.z = CarrierPosMoveAway.z - 20 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosMoveAway, "Cone", 10 ) - - elseif OnBoardSide == CLIENT.ONBOARDSIDE.RIGHT then - - self:T( "TransportCargoOnBoard: Onboarding RIGHT" ) - CarrierPosMove.z = CarrierPosMove.z + 25 - CarrierPosOnBoard.z = CarrierPosOnBoard.z + 5 - CarrierPosMoveAway.z = CarrierPosMoveAway.z + 20 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosMoveAway, "Cone", 10 ) - - elseif OnBoardSide == CLIENT.ONBOARDSIDE.BACK then - - self:T( "TransportCargoOnBoard: Onboarding BACK" ) - CarrierPosMove.x = CarrierPosMove.x - 25 - CarrierPosOnBoard.x = CarrierPosOnBoard.x - 5 - CarrierPosMoveAway.x = CarrierPosMoveAway.x - 20 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosMoveAway, "Cone", 10 ) - - elseif OnBoardSide == CLIENT.ONBOARDSIDE.FRONT then - - self:T( "TransportCargoOnBoard: Onboarding FRONT" ) - CarrierPosMove.x = CarrierPosMove.x + 25 - CarrierPosOnBoard.x = CarrierPosOnBoard.x + 5 - CarrierPosMoveAway.x = CarrierPosMoveAway.x + 20 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosMoveAway, "Cone", 10 ) - - elseif OnBoardSide == CLIENT.ONBOARDSIDE.NONE then - - self:T( "TransportCargoOnBoard: Onboarding FRONT" ) - CarrierPosMove.x = CarrierPosMove.x + 25 - CarrierPosOnBoard.x = CarrierPosOnBoard.x + 5 - CarrierPosMoveAway.x = CarrierPosMoveAway.x + 20 - Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) - Points[#Points+1] = routines.ground.buildWP( CarrierPosMoveAway, "Cone", 10 ) - - end - self:T( "Routing " .. CargoHostName ) - - --routines.scheduleFunction( routines.goRoute, { CargoHostName, Points}, timer.getTime() + 4 ) - SCHEDULER:New( self, routines.goRoute, { CargoHostName, Points }, 4 ) - - return Valid - -end - - -function CARGO_PACKAGE:OnBoarded( Client, LandingZone ) - self:F() - - local OnBoarded = false - - if self.CargoClient and self.CargoClient:GetDCSGroup() then - if routines.IsUnitInRadius( self.CargoClient:GetClientGroupDCSUnit(), self.CargoClient:GetPositionVec3(), 10 ) then - - -- Switch Cargo from self.CargoClient to Client ... Each cargo can have only one client. So assigning the new client for the cargo is enough. - self:StatusLoaded( Client ) - - -- All done, onboarded the Cargo to the new Client. - OnBoarded = true - end - end - - return OnBoarded -end - - -function CARGO_PACKAGE:UnLoad( Client, TargetZoneName ) - self:F() - - self:T( 'self.CargoName = ' .. self.CargoName ) - --self:T( 'self.CargoHostName = ' .. self.CargoHostName ) - - --self.CargoSpawn:FromCarrier( Client:GetDCSGroup(), TargetZoneName, self.CargoHostName ) - self:StatusUnLoaded() - - return Cargo -end - - -CARGO_SLINGLOAD = { - ClassName = "CARGO_SLINGLOAD" -} - - -function CARGO_SLINGLOAD:New( CargoType, CargoName, CargoWeight, CargoZone, CargoHostName, CargoCountryID ) - local self = BASE:Inherit( self, CARGO:New( CargoType, CargoName, CargoWeight ) ) - self:F( { CargoType, CargoName, CargoWeight, CargoZone, CargoHostName, CargoCountryID } ) - - self.CargoHostName = CargoHostName - - -- Cargo will be initialized around the CargoZone position. - self.CargoZone = CargoZone - - self.CargoCount = 0 - self.CargoStaticName = string.format( "%s#%03d", self.CargoName, self.CargoCount ) - - -- The country ID needs to be correctly set. - self.CargoCountryID = CargoCountryID - - CARGOS[self.CargoName] = self - - return self - -end - - -function CARGO_SLINGLOAD:IsLandingRequired() - self:F() - return false -end - - -function CARGO_SLINGLOAD:IsSlingLoad() - self:F() - return true -end - - -function CARGO_SLINGLOAD:Spawn( Client ) - self:F( { self, Client } ) - - local Zone = trigger.misc.getZone( self.CargoZone ) - - local ZonePos = {} - ZonePos.x = Zone.point.x + math.random( Zone.radius / 2 * -1, Zone.radius / 2 ) - ZonePos.y = Zone.point.z + math.random( Zone.radius / 2 * -1, Zone.radius / 2 ) - - self:T( "Cargo Location = " .. ZonePos.x .. ", " .. ZonePos.y ) - - --[[ - -- This does not work in 1.5.2. - CargoStatic = StaticObject.getByName( self.CargoName ) - if CargoStatic then - CargoStatic:destroy() - end - --]] - - CargoStatic = StaticObject.getByName( self.CargoStaticName ) - - if CargoStatic and CargoStatic:isExist() then - CargoStatic:destroy() - end - - -- I need to make every time a new cargo due to bugs in 1.5.2. - - self.CargoCount = self.CargoCount + 1 - self.CargoStaticName = string.format( "%s#%03d", self.CargoName, self.CargoCount ) - - local CargoTemplate = { - ["category"] = "Cargo", - ["shape_name"] = "ab-212_cargo", - ["type"] = "Cargo1", - ["x"] = ZonePos.x, - ["y"] = ZonePos.y, - ["mass"] = self.CargoWeight, - ["name"] = self.CargoStaticName, - ["canCargo"] = true, - ["heading"] = 0, - } - - coalition.addStaticObject( self.CargoCountryID, CargoTemplate ) - --- end - - return self -end - - -function CARGO_SLINGLOAD:IsNear( Client, LandingZone ) - self:F() - - local Near = false - - return Near -end - - -function CARGO_SLINGLOAD:IsInLandingZone( Client, LandingZone ) - self:F() - - local Near = false - - local CargoStaticUnit = StaticObject.getByName( self.CargoName ) - if CargoStaticUnit then - if routines.IsStaticInZones( CargoStaticUnit, LandingZone ) then - Near = true - end - end - - return Near -end - - -function CARGO_SLINGLOAD:OnBoard( Client, LandingZone, OnBoardSide ) - self:F() - - local Valid = true - - - return Valid -end - - -function CARGO_SLINGLOAD:OnBoarded( Client, LandingZone ) - self:F() - - local OnBoarded = false - - local CargoStaticUnit = StaticObject.getByName( self.CargoName ) - if CargoStaticUnit then - if not routines.IsStaticInZones( CargoStaticUnit, LandingZone ) then - OnBoarded = true - end - end - - return OnBoarded -end - - -function CARGO_SLINGLOAD:UnLoad( Client, TargetZoneName ) - self:F() - - self:T( 'self.CargoName = ' .. self.CargoName ) - self:T( 'self.CargoGroupName = ' .. self.CargoGroupName ) - - self:StatusUnLoaded() - - return Cargo -end ---- Message System to display Messages for Clients and Coalitions or All. --- Messages are grouped on the display panel per Category to improve readability for the players. --- Messages are shown on the display panel for an amount of seconds, and will then disappear. --- Messages are identified by an ID. The messages with the same ID belonging to the same category will be overwritten if they were still being displayed on the display panel. --- Messages are created with MESSAGE:@{New}(). --- Messages are sent to Clients with MESSAGE:@{ToClient}(). --- Messages are sent to Coalitions with MESSAGE:@{ToCoalition}(). --- Messages are sent to All Players with MESSAGE:@{ToAll}(). --- @module Message - -Include.File( "Base" ) - ---- The MESSAGE class --- @type MESSAGE -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 #string MessageCategory is a string expressing the Category of the Message. Messages are grouped on the display panel per Category to improve readability. --- @param #number MessageDuration is a number in seconds of how long the MESSAGE should be shown on the display panel. --- @param #string MessageID is a string expressing the ID of the Message. --- @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!", "End of Mission", 25, "Win" ) --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) --- 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" ) -function MESSAGE:New( MessageText, MessageCategory, MessageDuration, MessageID ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( { MessageText, MessageCategory, MessageDuration, MessageID } ) - - -- When no messagecategory is given, we don't show it as a title... - if MessageCategory and MessageCategory ~= "" then - self.MessageCategory = MessageCategory .. ": " - else - self.MessageCategory = "" - end - - self.MessageDuration = MessageDuration - self.MessageID = MessageID - self.MessageTime = timer.getTime() - self.MessageText = MessageText - - self.MessageSent = false - self.MessageGroup = false - self.MessageCoalition = false - - return self -end - ---- Sends a MESSAGE to a Client Group. Note that the Group needs to be defined within the ME with the skillset "Client" or "Player". --- @param #MESSAGE self --- @param Client#CLIENT Client is the Group of the Client. --- @return #MESSAGE --- @usage --- -- Send the 2 messages created with the @{New} method to the Client Group. --- -- Note that the Message of MessageClient2 is overwriting the Message of MessageClient1. --- ClientGroup = Group.getByName( "ClientGroup" ) --- --- MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ):ToClient( ClientGroup ) --- MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ):ToClient( ClientGroup ) --- or --- MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ):ToClient( ClientGroup ) --- MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ):ToClient( ClientGroup ) --- or --- MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ) --- MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ) --- MessageClient1:ToClient( ClientGroup ) --- MessageClient2:ToClient( ClientGroup ) -function MESSAGE:ToClient( Client ) - self:F( Client ) - - if Client and Client:GetClientGroupID() then - - local ClientGroupID = Client:GetClientGroupID() - self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) - trigger.action.outTextForGroup( ClientGroupID, self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration ) - end - - return self -end - ---- Sends a MESSAGE to the Blue coalition. --- @param #MESSAGE self --- @return #MESSAGE --- @usage --- -- Send a message created with the @{New} method to the BLUE coalition. --- MessageBLUE = MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToBlue() --- or --- MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToBlue() --- or --- MessageBLUE = MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) --- MessageBLUE:ToBlue() -function MESSAGE:ToBlue() - self:F() - - self:ToCoalition( coalition.side.BLUE ) - - return self -end - ---- Sends a MESSAGE to the Red Coalition. --- @param #MESSAGE self --- @return #MESSAGE --- @usage --- -- Send a message created with the @{New} method to the RED coalition. --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToRed() --- or --- MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToRed() --- or --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) --- MessageRED:ToRed() -function MESSAGE:ToRed( ) - self:F() - - self:ToCoalition( coalition.side.RED ) - - return self -end - ---- Sends a MESSAGE to a Coalition. --- @param #MESSAGE self --- @param CoalitionSide needs to be filled out by the defined structure of the standard scripting engine @{coalition.side}. --- @return #MESSAGE --- @usage --- -- Send a message created with the @{New} method to the RED coalition. --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToCoalition( coalition.side.RED ) --- or --- MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToCoalition( coalition.side.RED ) --- or --- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) --- MessageRED:ToCoalition( coalition.side.RED ) -function MESSAGE:ToCoalition( CoalitionSide ) - self:F( CoalitionSide ) - - if CoalitionSide then - self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) - trigger.action.outTextForCoalition( CoalitionSide, self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration ) - end - - return self -end - ---- Sends a MESSAGE to all players. --- @param #MESSAGE self --- @return #MESSAGE --- @usage --- -- Send a message created to all players. --- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ):ToAll() --- or --- MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ):ToAll() --- or --- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ) --- MessageAll:ToAll() -function MESSAGE:ToAll() - self:F() - - self:ToCoalition( coalition.side.RED ) - self:ToCoalition( coalition.side.BLUE ) - - return self -end - - - ---- The MESSAGEQUEUE class --- @type MESSAGEQUEUE -MESSAGEQUEUE = { - ClientGroups = {}, - CoalitionSides = {} -} - -function MESSAGEQUEUE:New( RefreshInterval ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( { RefreshInterval } ) - - self.RefreshInterval = RefreshInterval - - --self.DisplayFunction = routines.scheduleFunction( self._DisplayMessages, { self }, 0, RefreshInterval ) - self.DisplayFunction = SCHEDULER:New( self, self._DisplayMessages, {}, 0, RefreshInterval ) - - return self -end - ---- This function is called automatically by the MESSAGEQUEUE scheduler. -function MESSAGEQUEUE:_DisplayMessages() - - -- First we display all messages that a coalition needs to receive... Also those who are not in a client (CA module clients...). - for CoalitionSideID, CoalitionSideData in pairs( self.CoalitionSides ) do - for MessageID, MessageData in pairs( CoalitionSideData.Messages ) do - if MessageData.MessageSent == false then - --trigger.action.outTextForCoalition( CoalitionSideID, MessageData.MessageCategory .. '\n' .. MessageData.MessageText:gsub("\n$",""):gsub("\n$",""), MessageData.MessageDuration ) - MessageData.MessageSent = true - end - local MessageTimeLeft = ( MessageData.MessageTime + MessageData.MessageDuration ) - timer.getTime() - if MessageTimeLeft <= 0 then - MessageData = nil - end - end - end - - -- Then we send the messages for each individual client, but also to be included are those Coalition messages for the Clients who belong to a coalition. - -- Because the Client messages will overwrite the Coalition messages (for that Client). - for ClientGroupName, ClientGroupData in pairs( self.ClientGroups ) do - for MessageID, MessageData in pairs( ClientGroupData.Messages ) do - if MessageData.MessageGroup == false then - trigger.action.outTextForGroup( Group.getByName(ClientGroupName):getID(), MessageData.MessageCategory .. '\n' .. MessageData.MessageText:gsub("\n$",""):gsub("\n$",""), MessageData.MessageDuration ) - MessageData.MessageGroup = true - end - local MessageTimeLeft = ( MessageData.MessageTime + MessageData.MessageDuration ) - timer.getTime() - if MessageTimeLeft <= 0 then - MessageData = nil - end - end - - -- Now check if the Client also has messages that belong to the Coalition of the Client... - for CoalitionSideID, CoalitionSideData in pairs( self.CoalitionSides ) do - for MessageID, MessageData in pairs( CoalitionSideData.Messages ) do - local CoalitionGroup = Group.getByName( ClientGroupName ) - if CoalitionGroup and CoalitionGroup:getCoalition() == CoalitionSideID then - if MessageData.MessageCoalition == false then - trigger.action.outTextForGroup( Group.getByName(ClientGroupName):getID(), MessageData.MessageCategory .. '\n' .. MessageData.MessageText:gsub("\n$",""):gsub("\n$",""), MessageData.MessageDuration ) - MessageData.MessageCoalition = true - end - end - local MessageTimeLeft = ( MessageData.MessageTime + MessageData.MessageDuration ) - timer.getTime() - if MessageTimeLeft <= 0 then - MessageData = nil - end - end - end - end - - return true -end - ---- The _MessageQueue object is created when the MESSAGE class module is loaded. ---_MessageQueue = MESSAGEQUEUE:New( 0.5 ) - ---- Stages within a @{TASK} within a @{MISSION}. All of the STAGE functionality is considered internally administered and not to be used by any Mission designer. --- @module STAGE --- @author Flightcontrol - -Include.File( "Routines" ) -Include.File( "Base" ) -Include.File( "Mission" ) -Include.File( "Client" ) -Include.File( "Task" ) - ---- The STAGE class --- @type -STAGE = { - ClassName = "STAGE", - MSG = { ID = "None", TIME = 10 }, - FREQUENCY = { NONE = 0, ONCE = 1, REPEAT = -1 }, - - Name = "NoStage", - StageType = '', - WaitTime = 1, - Frequency = 1, - MessageCount = 0, - MessageInterval = 15, - MessageShown = {}, - MessageShow = false, - MessageFlash = false -} - - -function STAGE:New() - local self = BASE:Inherit( self, BASE:New() ) - self:F() - return self -end - -function STAGE:Execute( Mission, Client, Task ) - - local Valid = true - - return Valid -end - -function STAGE:Executing( Mission, Client, Task ) - -end - -function STAGE:Validate( Mission, Client, Task ) - local Valid = true - - return Valid -end - - -STAGEBRIEF = { - ClassName = "BRIEF", - MSG = { ID = "Brief", TIME = 1 }, - Name = "Brief", - StageBriefingTime = 0, - StageBriefingDuration = 1 -} - -function STAGEBRIEF:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - ---- Execute --- @param #STAGEBRIEF self --- @param Mission#MISSION Mission --- @param Client#CLIENT Client --- @param Task#TASK Task --- @return #boolean -function STAGEBRIEF:Execute( Mission, Client, Task ) - local Valid = BASE:Inherited(self):Execute( Mission, Client, Task ) - self:F() - Client:ShowMissionBriefing( Mission.MissionBriefing ) - self.StageBriefingTime = timer.getTime() - return Valid -end - -function STAGEBRIEF:Validate( Mission, Client, Task ) - local Valid = STAGE:Validate( Mission, Client, Task ) - self:T() - - if timer.getTime() - self.StageBriefingTime <= self.StageBriefingDuration then - return 0 - else - self.StageBriefingTime = timer.getTime() - return 1 - end - -end - - -STAGESTART = { - ClassName = "START", - MSG = { ID = "Start", TIME = 1 }, - Name = "Start", - StageStartTime = 0, - StageStartDuration = 1 -} - -function STAGESTART:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - -function STAGESTART:Execute( Mission, Client, Task ) - self:F() - local Valid = BASE:Inherited(self):Execute( Mission, Client, Task ) - if Task.TaskBriefing then - Client:Message( Task.TaskBriefing, 30, Mission.Name .. "/Stage", "Command" ) - else - Client:Message( 'Task ' .. Task.TaskNumber .. '.', 30, Mission.Name .. "/Stage", "Command" ) - end - self.StageStartTime = timer.getTime() - return Valid -end - -function STAGESTART:Validate( Mission, Client, Task ) - self:F() - local Valid = STAGE:Validate( Mission, Client, Task ) - - if timer.getTime() - self.StageStartTime <= self.StageStartDuration then - return 0 - else - self.StageStartTime = timer.getTime() - return 1 - end - - return 1 - -end - -STAGE_CARGO_LOAD = { - ClassName = "STAGE_CARGO_LOAD" -} - -function STAGE_CARGO_LOAD:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - -function STAGE_CARGO_LOAD:Execute( Mission, Client, Task ) - self:F() - local Valid = BASE:Inherited(self):Execute( Mission, Client, Task ) - - for LoadCargoID, LoadCargo in pairs( Task.Cargos.LoadCargos ) do - LoadCargo:Load( Client ) - end - - if Mission.MissionReportFlash and Client:IsTransport() then - Client:ShowCargo() - end - - return Valid -end - -function STAGE_CARGO_LOAD:Validate( Mission, Client, Task ) - self:F() - local Valid = STAGE:Validate( Mission, Client, Task ) - - return 1 -end - - -STAGE_CARGO_INIT = { - ClassName = "STAGE_CARGO_INIT" -} - -function STAGE_CARGO_INIT:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - -function STAGE_CARGO_INIT:Execute( Mission, Client, Task ) - self:F() - local Valid = BASE:Inherited(self):Execute( Mission, Client, Task ) - - for InitLandingZoneID, InitLandingZone in pairs( Task.LandingZones.LandingZones ) do - self:T( InitLandingZone ) - InitLandingZone:Spawn() - end - - - self:T( Task.Cargos.InitCargos ) - for InitCargoID, InitCargoData in pairs( Task.Cargos.InitCargos ) do - self:T( { InitCargoData } ) - InitCargoData:Spawn( Client ) - end - - return Valid -end - - -function STAGE_CARGO_INIT:Validate( Mission, Client, Task ) - self:F() - local Valid = STAGE:Validate( Mission, Client, Task ) - - return 1 -end - - - -STAGEROUTE = { - ClassName = "STAGEROUTE", - MSG = { ID = "Route", TIME = 5 }, - Frequency = STAGE.FREQUENCY.REPEAT, - Name = "Route" -} - -function STAGEROUTE:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - self.MessageSwitch = true - return self -end - - ---- Execute the routing. --- @param #STAGEROUTE self --- @param Mission#MISSION Mission --- @param Client#CLIENT Client --- @param Task#TASK Task -function STAGEROUTE:Execute( Mission, Client, Task ) - self:F() - local Valid = BASE:Inherited(self):Execute( Mission, Client, Task ) - - local RouteMessage = "Fly to: " - self:T( Task.LandingZones ) - for LandingZoneID, LandingZoneName in pairs( Task.LandingZones.LandingZoneNames ) do - RouteMessage = RouteMessage .. "\n " .. LandingZoneName .. ' at ' .. routines.getBRStringZone( { zone = LandingZoneName, ref = Client:GetClientGroupDCSUnit():getPoint(), true, true } ) .. ' km.' - end - - if Client:IsMultiSeated() then - Client:Message( RouteMessage, self.MSG.TIME, Mission.Name .. "/StageRoute", "Co-Pilot", 20 ) - else - Client:Message( RouteMessage, self.MSG.TIME, Mission.Name .. "/StageRoute", "Command", 20 ) - end - - - if Mission.MissionReportFlash and Client:IsTransport() then - Client:ShowCargo() - end - - return Valid -end - -function STAGEROUTE:Validate( Mission, Client, Task ) - self:F() - local Valid = STAGE:Validate( Mission, Client, Task ) - - -- check if the Client is in the landing zone - self:T( Task.LandingZones.LandingZoneNames ) - Task.CurrentLandingZoneName = routines.IsUnitNearZonesRadius( Client:GetClientGroupDCSUnit(), Task.LandingZones.LandingZoneNames, 500 ) - - if Task.CurrentLandingZoneName then - - Task.CurrentLandingZone = Task.LandingZones.LandingZones[Task.CurrentLandingZoneName].CargoZone - Task.CurrentCargoZone = Task.LandingZones.LandingZones[Task.CurrentLandingZoneName] - - if Task.CurrentCargoZone then - if not Task.Signalled then - Task.Signalled = Task.CurrentCargoZone:Signal() - end - end - - self:T( 1 ) - return 1 - end - - self:T( 0 ) - return 0 -end - - - -STAGELANDING = { - ClassName = "STAGELANDING", - MSG = { ID = "Landing", TIME = 10 }, - Name = "Landing", - Signalled = false -} - -function STAGELANDING:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - ---- Execute the landing coordination. --- @param #STAGELANDING self --- @param Mission#MISSION Mission --- @param Client#CLIENT Client --- @param Task#TASK Task -function STAGELANDING:Execute( Mission, Client, Task ) - self:F() - - if Client:IsMultiSeated() then - Client:Message( "We have arrived at the landing zone.", self.MSG.TIME, Mission.Name .. "/StageArrived", "Co-Pilot", 10 ) - else - Client:Message( "You have arrived at the landing zone.", self.MSG.TIME, Mission.Name .. "/StageArrived", "Command", 10 ) - end - - Task.HostUnit = Task.CurrentCargoZone:GetHostUnit() - - self:T( { Task.HostUnit } ) - - if Task.HostUnit then - - Task.HostUnitName = Task.HostUnit:GetPrefix() - Task.HostUnitTypeName = Task.HostUnit:GetTypeName() - - local HostMessage = "" - Task.CargoNames = "" - - local IsFirst = true - - for CargoID, Cargo in pairs( CARGOS ) do - if Cargo.CargoType == Task.CargoType then - - if Cargo:IsLandingRequired() then - self:T( "Task for cargo " .. Cargo.CargoType .. " requires landing.") - Task.IsLandingRequired = true - end - - if Cargo:IsSlingLoad() then - self:T( "Task for cargo " .. Cargo.CargoType .. " is a slingload.") - Task.IsSlingLoad = true - end - - if IsFirst then - IsFirst = false - Task.CargoNames = Task.CargoNames .. Cargo.CargoName .. "( " .. Cargo.CargoWeight .. " )" - else - Task.CargoNames = Task.CargoNames .. "; " .. Cargo.CargoName .. "( " .. Cargo.CargoWeight .. " )" - end - end - end - - if Task.IsLandingRequired then - HostMessage = "Land the helicopter to " .. Task.TEXT[1] .. " " .. Task.CargoNames .. "." - else - HostMessage = "Use the Radio menu and F6 to find the cargo, then fly or land near the cargo and " .. Task.TEXT[1] .. " " .. Task.CargoNames .. "." - end - - local Host = "Command" - if Task.HostUnitName then - Host = Task.HostUnitName .. " (" .. Task.HostUnitTypeName .. ")" - else - if Client:IsMultiSeated() then - Host = "Co-Pilot" - end - end - - Client:Message( HostMessage, self.MSG.TIME, Mission.Name .. "/STAGELANDING.EXEC." .. Host, Host, 10 ) - - end -end - -function STAGELANDING:Validate( Mission, Client, Task ) - self:F() - - Task.CurrentLandingZoneName = routines.IsUnitNearZonesRadius( Client:GetClientGroupDCSUnit(), Task.LandingZones.LandingZoneNames, 500 ) - if Task.CurrentLandingZoneName then - - -- Client is in de landing zone. - self:T( Task.CurrentLandingZoneName ) - - Task.CurrentLandingZone = Task.LandingZones.LandingZones[Task.CurrentLandingZoneName].CargoZone - Task.CurrentCargoZone = Task.LandingZones.LandingZones[Task.CurrentLandingZoneName] - - if Task.CurrentCargoZone then - if not Task.Signalled then - Task.Signalled = Task.CurrentCargoZone:Signal() - end - end - else - if Task.CurrentLandingZone then - Task.CurrentLandingZone = nil - end - if Task.CurrentCargoZone then - Task.CurrentCargoZone = nil - end - Task.Signalled = false - Task:RemoveCargoMenus( Client ) - self:T( -1 ) - return -1 - end - - - local DCSUnitVelocityVec3 = Client:GetClientGroupDCSUnit():getVelocity() - local DCSUnitVelocity = ( DCSUnitVelocityVec3.x ^2 + DCSUnitVelocityVec3.y ^2 + DCSUnitVelocityVec3.z ^2 ) ^ 0.5 - - local DCSUnitPointVec3 = Client:GetClientGroupDCSUnit():getPoint() - local LandHeight = land.getHeight( { x = DCSUnitPointVec3.x, y = DCSUnitPointVec3.z } ) - local DCSUnitHeight = DCSUnitPointVec3.y - LandHeight - - self:T( { Task.IsLandingRequired, Client:GetClientGroupDCSUnit():inAir() } ) - if Task.IsLandingRequired and not Client:GetClientGroupDCSUnit():inAir() then - self:T( 1 ) - Task.IsInAirTestRequired = true - return 1 - end - - self:T( { DCSUnitVelocity, DCSUnitHeight, LandHeight, Task.CurrentCargoZone.SignalHeight } ) - if Task.IsLandingRequired and DCSUnitVelocity <= 0.05 and DCSUnitHeight <= Task.CurrentCargoZone.SignalHeight then - self:T( 1 ) - Task.IsInAirTestRequired = false - return 1 - end - - self:T( 0 ) - return 0 -end - -STAGELANDED = { - ClassName = "STAGELANDED", - MSG = { ID = "Land", TIME = 10 }, - Name = "Landed", - MenusAdded = false -} - -function STAGELANDED:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - -function STAGELANDED:Execute( Mission, Client, Task ) - self:F() - - if Task.IsLandingRequired then - - local Host = "Command" - if Task.HostUnitName then - Host = Task.HostUnitName .. " (" .. Task.HostUnitTypeName .. ")" - else - if Client:IsMultiSeated() then - Host = "Co-Pilot" - end - end - - Client:Message( 'You have landed within the landing zone. Use the radio menu (F10) to ' .. Task.TEXT[1] .. ' the ' .. Task.CargoType .. '.', - self.MSG.TIME, Mission.Name .. "/STAGELANDED.EXEC" .. Host, Host ) - - if not self.MenusAdded then - Task.Cargo = nil - Task:RemoveCargoMenus( Client ) - Task:AddCargoMenus( Client, CARGOS, 250 ) - end - end -end - - - -function STAGELANDED:Validate( Mission, Client, Task ) - self:F() - - if not routines.IsUnitNearZonesRadius( Client:GetClientGroupDCSUnit(), Task.CurrentLandingZoneName, 500 ) then - self:T( "Client is not anymore in the landing zone, go back to stage Route, and remove cargo menus." ) - Task.Signalled = false - Task:RemoveCargoMenus( Client ) - self:T( -2 ) - return -2 - end - - local DCSUnitVelocityVec3 = Client:GetClientGroupDCSUnit():getVelocity() - local DCSUnitVelocity = ( DCSUnitVelocityVec3.x ^2 + DCSUnitVelocityVec3.y ^2 + DCSUnitVelocityVec3.z ^2 ) ^ 0.5 - - local DCSUnitPointVec3 = Client:GetClientGroupDCSUnit():getPoint() - local LandHeight = land.getHeight( { x = DCSUnitPointVec3.x, y = DCSUnitPointVec3.z } ) - local DCSUnitHeight = DCSUnitPointVec3.y - LandHeight - - self:T( { Task.IsLandingRequired, Client:GetClientGroupDCSUnit():inAir() } ) - if Task.IsLandingRequired and Task.IsInAirTestRequired == true and Client:GetClientGroupDCSUnit():inAir() then - self:T( "Client went back in the air. Go back to stage Landing." ) - self:T( -1 ) - return -1 - end - - self:T( { DCSUnitVelocity, DCSUnitHeight, LandHeight, Task.CurrentCargoZone.SignalHeight } ) - if Task.IsLandingRequired and Task.IsInAirTestRequired == false and DCSUnitVelocity >= 2 and DCSUnitHeight >= Task.CurrentCargoZone.SignalHeight then - self:T( "It seems the Client went back in the air and over the boundary limits. Go back to stage Landing." ) - self:T( -1 ) - return -1 - end - - -- Wait until cargo is selected from the menu. - if Task.IsLandingRequired then - if not Task.Cargo then - self:T( 0 ) - return 0 - end - end - - self:T( 1 ) - return 1 -end - -STAGEUNLOAD = { - ClassName = "STAGEUNLOAD", - MSG = { ID = "Unload", TIME = 10 }, - Name = "Unload" -} - -function STAGEUNLOAD:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - ---- Coordinate UnLoading --- @param #STAGEUNLOAD self --- @param Mission#MISSION Mission --- @param Client#CLIENT Client --- @param Task#TASK Task -function STAGEUNLOAD:Execute( Mission, Client, Task ) - self:F() - - if Client:IsMultiSeated() then - Client:Message( 'The ' .. Task.CargoType .. ' are being ' .. Task.TEXT[2] .. ' within the landing zone. Wait until the helicopter is ' .. Task.TEXT[3] .. '.', - self.MSG.TIME, Mission.Name .. "/StageUnLoad", "Co-Pilot" ) - else - Client:Message( 'You are unloading the ' .. Task.CargoType .. ' ' .. Task.TEXT[2] .. ' within the landing zone. Wait until the helicopter is ' .. Task.TEXT[3] .. '.', - self.MSG.TIME, Mission.Name .. "/StageUnLoad", "Command" ) - end - Task:RemoveCargoMenus( Client ) -end - -function STAGEUNLOAD:Executing( Mission, Client, Task ) - self:F() - env.info( 'STAGEUNLOAD:Executing() Task.Cargo.CargoName = ' .. Task.Cargo.CargoName ) - - local TargetZoneName - - if Task.TargetZoneName then - TargetZoneName = Task.TargetZoneName - else - TargetZoneName = Task.CurrentLandingZoneName - end - - if Task.Cargo:UnLoad( Client, TargetZoneName ) then - Task.ExecuteStage = _TransportExecuteStage.SUCCESS - if Mission.MissionReportFlash then - Client:ShowCargo() - end - end -end - ---- Validate UnLoading --- @param #STAGEUNLOAD self --- @param Mission#MISSION Mission --- @param Client#CLIENT Client --- @param Task#TASK Task -function STAGEUNLOAD:Validate( Mission, Client, Task ) - self:F() - env.info( 'STAGEUNLOAD:Validate()' ) - - if routines.IsUnitNearZonesRadius( Client:GetClientGroupDCSUnit(), Task.CurrentLandingZoneName, 500 ) then - else - Task.ExecuteStage = _TransportExecuteStage.FAILED - Task:RemoveCargoMenus( Client ) - if Client:IsMultiSeated() then - Client:Message( 'The ' .. Task.CargoType .. " haven't been successfully " .. Task.TEXT[3] .. ' within the landing zone. Task and mission has failed.', - _TransportStageMsgTime.DONE, Mission.Name .. "/StageFailure", "Co-Pilot" ) - else - Client:Message( 'The ' .. Task.CargoType .. " haven't been successfully " .. Task.TEXT[3] .. ' within the landing zone. Task and mission has failed.', - _TransportStageMsgTime.DONE, Mission.Name .. "/StageFailure", "Command" ) - end - return 1 - end - - if not Client:GetClientGroupDCSUnit():inAir() then - else - Task.ExecuteStage = _TransportExecuteStage.FAILED - Task:RemoveCargoMenus( Client ) - if Client:IsMultiSeated() then - Client:Message( 'The ' .. Task.CargoType .. " haven't been successfully " .. Task.TEXT[3] .. ' within the landing zone. Task and mission has failed.', - _TransportStageMsgTime.DONE, Mission.Name .. "/StageFailure", "Co-Pilot" ) - else - Client:Message( 'The ' .. Task.CargoType .. " haven't been successfully " .. Task.TEXT[3] .. ' within the landing zone. Task and mission has failed.', - _TransportStageMsgTime.DONE, Mission.Name .. "/StageFailure", "Command" ) - end - return 1 - end - - if Task.ExecuteStage == _TransportExecuteStage.SUCCESS then - if Client:IsMultiSeated() then - Client:Message( 'The ' .. Task.CargoType .. ' have been sucessfully ' .. Task.TEXT[3] .. ' within the landing zone.', _TransportStageMsgTime.DONE, Mission.Name .. "/Stage", "Co-Pilot" ) - else - Client:Message( 'The ' .. Task.CargoType .. ' have been sucessfully ' .. Task.TEXT[3] .. ' within the landing zone.', _TransportStageMsgTime.DONE, Mission.Name .. "/Stage", "Command" ) - end - Task:RemoveCargoMenus( Client ) - Task.MissionTask:AddGoalCompletion( Task.MissionTask.GoalVerb, Task.CargoName, 1 ) -- We set the cargo as one more goal completed in the mission. - return 1 - end - - return 1 -end - -STAGELOAD = { - ClassName = "STAGELOAD", - MSG = { ID = "Load", TIME = 10 }, - Name = "Load" -} - -function STAGELOAD:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - -function STAGELOAD:Execute( Mission, Client, Task ) - self:F() - - if not Task.IsSlingLoad then - - local Host = "Command" - if Task.HostUnitName then - Host = Task.HostUnitName .. " (" .. Task.HostUnitTypeName .. ")" - else - if Client:IsMultiSeated() then - Host = "Co-Pilot" - end - end - - Client:Message( 'The ' .. Task.CargoType .. ' are being ' .. Task.TEXT[2] .. ' within the landing zone. Wait until the helicopter is ' .. Task.TEXT[3] .. '.', - _TransportStageMsgTime.EXECUTING, Mission.Name .. "/STAGELOAD.EXEC." .. Host, Host ) - - -- Route the cargo to the Carrier - - Task.Cargo:OnBoard( Client, Task.CurrentCargoZone, Task.OnBoardSide ) - Task.ExecuteStage = _TransportExecuteStage.EXECUTING - else - Task.ExecuteStage = _TransportExecuteStage.EXECUTING - end -end - -function STAGELOAD:Executing( Mission, Client, Task ) - self:F() - - -- If the Cargo is ready to be loaded, load it into the Client. - - local Host = "Command" - if Task.HostUnitName then - Host = Task.HostUnitName .. " (" .. Task.HostUnitTypeName .. ")" - else - if Client:IsMultiSeated() then - Host = "Co-Pilot" - end - end - - if not Task.IsSlingLoad then - self:T( Task.Cargo.CargoName) - - if Task.Cargo:OnBoarded( Client, Task.CurrentCargoZone ) then - - -- Load the Cargo onto the Client - Task.Cargo:Load( Client ) - - -- Message to the pilot that cargo has been loaded. - Client:Message( "The cargo " .. Task.Cargo.CargoName .. " has been loaded in our helicopter.", - 20, Mission.Name .. "/STAGELANDING.LOADING1." .. Host, Host ) - Task.ExecuteStage = _TransportExecuteStage.SUCCESS - - Client:ShowCargo() - end - else - Client:Message( "Hook the " .. Task.CargoNames .. " onto the helicopter " .. Task.TEXT[3] .. " within the landing zone.", - _TransportStageMsgTime.EXECUTING, Mission.Name .. "/STAGELOAD.LOADING.1." .. Host, Host , 10 ) - for CargoID, Cargo in pairs( CARGOS ) do - self:T( "Cargo.CargoName = " .. Cargo.CargoName ) - - if Cargo:IsSlingLoad() then - local CargoStatic = StaticObject.getByName( Cargo.CargoStaticName ) - if CargoStatic then - self:T( "Cargo is found in the DCS simulator.") - local CargoStaticPosition = CargoStatic:getPosition().p - self:T( "Cargo Position x = " .. CargoStaticPosition.x .. ", y = " .. CargoStaticPosition.y .. ", z = " .. CargoStaticPosition.z ) - local CargoStaticHeight = routines.GetUnitHeight( CargoStatic ) - if CargoStaticHeight > 5 then - self:T( "Cargo is airborne.") - Cargo:StatusLoaded() - Task.Cargo = Cargo - Client:Message( 'The Cargo has been successfully hooked onto the helicopter and is now being sling loaded. Fly outside the landing zone.', - self.MSG.TIME, Mission.Name .. "/STAGELANDING.LOADING.2." .. Host, Host ) - Task.ExecuteStage = _TransportExecuteStage.SUCCESS - break - end - else - self:T( "Cargo not found in the DCS simulator." ) - end - end - end - end - -end - -function STAGELOAD:Validate( Mission, Client, Task ) - self:F() - - self:T( "Task.CurrentLandingZoneName = " .. Task.CurrentLandingZoneName ) - - local Host = "Command" - if Task.HostUnitName then - Host = Task.HostUnitName .. " (" .. Task.HostUnitTypeName .. ")" - else - if Client:IsMultiSeated() then - Host = "Co-Pilot" - end - end - - if not Task.IsSlingLoad then - if not routines.IsUnitNearZonesRadius( Client:GetClientGroupDCSUnit(), Task.CurrentLandingZoneName, 500 ) then - Task:RemoveCargoMenus( Client ) - Task.ExecuteStage = _TransportExecuteStage.FAILED - Task.CargoName = nil - Client:Message( "The " .. Task.CargoType .. " loading has been aborted. You flew outside the pick-up zone while loading. ", - self.MSG.TIME, Mission.Name .. "/STAGELANDING.VALIDATE.1." .. Host, Host ) - self:T( -1 ) - return -1 - end - - local DCSUnitVelocityVec3 = Client:GetClientGroupDCSUnit():getVelocity() - local DCSUnitVelocity = ( DCSUnitVelocityVec3.x ^2 + DCSUnitVelocityVec3.y ^2 + DCSUnitVelocityVec3.z ^2 ) ^ 0.5 - - local DCSUnitPointVec3 = Client:GetClientGroupDCSUnit():getPoint() - local LandHeight = land.getHeight( { x = DCSUnitPointVec3.x, y = DCSUnitPointVec3.z } ) - local DCSUnitHeight = DCSUnitPointVec3.y - LandHeight - - self:T( { Task.IsLandingRequired, Client:GetClientGroupDCSUnit():inAir() } ) - if Task.IsLandingRequired and Task.IsInAirTestRequired == true and Client:GetClientGroupDCSUnit():inAir() then - Task:RemoveCargoMenus( Client ) - Task.ExecuteStage = _TransportExecuteStage.FAILED - Task.CargoName = nil - Client:Message( "The " .. Task.CargoType .. " loading has been aborted. Re-start the " .. Task.TEXT[3] .. " process. Don't fly outside the pick-up zone.", - self.MSG.TIME, Mission.Name .. "/STAGELANDING.VALIDATE.1." .. Host, Host ) - self:T( -1 ) - return -1 - end - - self:T( { DCSUnitVelocity, DCSUnitHeight, LandHeight, Task.CurrentCargoZone.SignalHeight } ) - if Task.IsLandingRequired and Task.IsInAirTestRequired == false and DCSUnitVelocity >= 2 and DCSUnitHeight >= Task.CurrentCargoZone.SignalHeight then - Task:RemoveCargoMenus( Client ) - Task.ExecuteStage = _TransportExecuteStage.FAILED - Task.CargoName = nil - Client:Message( "The " .. Task.CargoType .. " loading has been aborted. Re-start the " .. Task.TEXT[3] .. " process. Don't fly outside the pick-up zone.", - self.MSG.TIME, Mission.Name .. "/STAGELANDING.VALIDATE.1." .. Host, Host ) - self:T( -1 ) - return -1 - end - - if Task.ExecuteStage == _TransportExecuteStage.SUCCESS then - Task:RemoveCargoMenus( Client ) - Client:Message( "Good Job. The " .. Task.CargoType .. " has been sucessfully " .. Task.TEXT[3] .. " within the landing zone.", - self.MSG.TIME, Mission.Name .. "/STAGELANDING.VALIDATE.3." .. Host, Host ) - Task.MissionTask:AddGoalCompletion( Task.MissionTask.GoalVerb, Task.CargoName, 1 ) - self:T( 1 ) - return 1 - end - - else - if Task.ExecuteStage == _TransportExecuteStage.SUCCESS then - CargoStatic = StaticObject.getByName( Task.Cargo.CargoStaticName ) - if CargoStatic and not routines.IsStaticInZones( CargoStatic, Task.CurrentLandingZoneName ) then - Client:Message( "Good Job. The " .. Task.CargoType .. " has been sucessfully " .. Task.TEXT[3] .. " and flown outside of the landing zone.", - self.MSG.TIME, Mission.Name .. "/STAGELANDING.VALIDATE.4." .. Host, Host ) - Task.MissionTask:AddGoalCompletion( Task.MissionTask.GoalVerb, Task.Cargo.CargoName, 1 ) - self:T( 1 ) - return 1 - end - end - - end - - - self:T( 0 ) - return 0 -end - - -STAGEDONE = { - ClassName = "STAGEDONE", - MSG = { ID = "Done", TIME = 10 }, - Name = "Done" -} - -function STAGEDONE:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'AI' - return self -end - -function STAGEDONE:Execute( Mission, Client, Task ) - self:F() - -end - -function STAGEDONE:Validate( Mission, Client, Task ) - self:F() - - Task:Done() - - return 0 -end - -STAGEARRIVE = { - ClassName = "STAGEARRIVE", - MSG = { ID = "Arrive", TIME = 10 }, - Name = "Arrive" -} - -function STAGEARRIVE:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'CLIENT' - return self -end - - ---- Execute Arrival --- @param #STAGEARRIVE self --- @param Mission#MISSION Mission --- @param Client#CLIENT Client --- @param Task#TASK Task -function STAGEARRIVE:Execute( Mission, Client, Task ) - self:F() - - if Client:IsMultiSeated() then - Client:Message( 'We have arrived at ' .. Task.CurrentLandingZoneName .. ".", self.MSG.TIME, Mission.Name .. "/Stage", "Co-Pilot" ) - else - Client:Message( 'We have arrived at ' .. Task.CurrentLandingZoneName .. ".", self.MSG.TIME, Mission.Name .. "/Stage", "Command" ) - end - -end - -function STAGEARRIVE:Validate( Mission, Client, Task ) - self:F() - - Task.CurrentLandingZoneID = routines.IsUnitInZones( Client:GetClientGroupDCSUnit(), Task.LandingZones ) - if ( Task.CurrentLandingZoneID ) then - else - return -1 - end - - return 1 -end - -STAGEGROUPSDESTROYED = { - ClassName = "STAGEGROUPSDESTROYED", - DestroyGroupSize = -1, - Frequency = STAGE.FREQUENCY.REPEAT, - MSG = { ID = "DestroyGroup", TIME = 10 }, - Name = "GroupsDestroyed" -} - -function STAGEGROUPSDESTROYED:New() - local self = BASE:Inherit( self, STAGE:New() ) - self:F() - self.StageType = 'AI' - return self -end - ---function STAGEGROUPSDESTROYED:Execute( Mission, Client, Task ) --- --- Client:Message( 'Task: Still ' .. DestroyGroupSize .. " of " .. Task.DestroyGroupCount .. " " .. Task.DestroyGroupType .. " to be destroyed!", self.MSG.TIME, Mission.Name .. "/Stage" ) --- ---end - -function STAGEGROUPSDESTROYED:Validate( Mission, Client, Task ) - self:F() - - if Task.MissionTask:IsGoalReached() then - return 1 - else - return 0 - end -end - -function STAGEGROUPSDESTROYED:Execute( Mission, Client, Task ) - self:F() - self:T( { Task.ClassName, Task.Destroyed } ) - --env.info( 'Event Table Task = ' .. tostring(Task) ) - -end - - - - - - - - - - - - - ---[[ - _TransportStage: Defines the different stages of which of transport missions can be in. This table is internal and is used to control the sequence of messages, actions and flow. - - - _TransportStage.START - - _TransportStage.ROUTE - - _TransportStage.LAND - - _TransportStage.EXECUTE - - _TransportStage.DONE - - _TransportStage.REMOVE ---]] -_TransportStage = { - HOLD = "HOLD", - START = "START", - ROUTE = "ROUTE", - LANDING = "LANDING", - LANDED = "LANDED", - EXECUTING = "EXECUTING", - LOAD = "LOAD", - UNLOAD = "UNLOAD", - DONE = "DONE", - NEXT = "NEXT" -} - -_TransportStageMsgTime = { - HOLD = 10, - START = 60, - ROUTE = 5, - LANDING = 10, - LANDED = 30, - EXECUTING = 30, - LOAD = 30, - UNLOAD = 30, - DONE = 30, - NEXT = 0 -} - -_TransportStageTime = { - HOLD = 10, - START = 5, - ROUTE = 5, - LANDING = 1, - LANDED = 1, - EXECUTING = 5, - LOAD = 5, - UNLOAD = 5, - DONE = 1, - NEXT = 0 -} - -_TransportStageAction = { - REPEAT = -1, - NONE = 0, - ONCE = 1 -} ---- The TASK Classes define major end-to-end activities within a MISSION. The TASK Class is the Master Class to orchestrate these activities. From this class, many concrete TASK classes are inherited. --- @module TASK - -Include.File( "Routines" ) -Include.File( "Base" ) -Include.File( "Mission" ) -Include.File( "Client" ) -Include.File( "Stage" ) - ---- The TASK class --- @type TASK --- @extends Base#BASE -TASK = { - - -- Defines the different signal types with a Task. - SIGNAL = { - COLOR = { - RED = { ID = 1, COLOR = trigger.smokeColor.Red, TEXT = "A red" }, - GREEN = { ID = 2, COLOR = trigger.smokeColor.Green, TEXT = "A green" }, - BLUE = { ID = 3, COLOR = trigger.smokeColor.Blue, TEXT = "A blue" }, - WHITE = { ID = 4, COLOR = trigger.smokeColor.White, TEXT = "A white" }, - ORANGE = { ID = 5, COLOR = trigger.smokeColor.Orange, TEXT = "An orange" } - }, - TYPE = { - SMOKE = { ID = 1, TEXT = "smoke" }, - FLARE = { ID = 2, TEXT = "flare" } - } - }, - ClassName = "TASK", - Mission = {}, -- Owning mission of the Task - Name = '', - Stages = {}, - Stage = {}, - Cargos = { - InitCargos = {}, - LoadCargos = {} - }, - LandingZones = { - LandingZoneNames = {}, - LandingZones = {} - }, - ActiveStage = 0, - TaskDone = false, - TaskFailed = false, - GoalTasks = {} -} - ---- Instantiates a new TASK Base. Should never be used. Interface Class. --- @return TASK -function TASK:New() - local self = BASE:Inherit( self, BASE:New() ) - self:F() - - -- assign Task default values during construction - self.TaskBriefing = "Task: No Task." - self.Time = timer.getTime() - self.ExecuteStage = _TransportExecuteStage.NONE - - return self -end - -function TASK:SetStage( StageSequenceIncrement ) - self:F( { StageSequenceIncrement } ) - - local Valid = false - if StageSequenceIncrement ~= 0 then - self.ActiveStage = self.ActiveStage + StageSequenceIncrement - if 1 <= self.ActiveStage and self.ActiveStage <= #self.Stages then - self.Stage = self.Stages[self.ActiveStage] - self:T( { self.Stage.Name } ) - self.Frequency = self.Stage.Frequency - Valid = true - else - Valid = false - env.info( "TASK:SetStage() self.ActiveStage is smaller or larger than self.Stages array. self.ActiveStage = " .. self.ActiveStage ) - end - end - self.Time = timer.getTime() - return Valid -end - -function TASK:Init() - self:F() - self.ActiveStage = 0 - self:SetStage(1) - self.TaskDone = false - self.TaskFailed = false -end - - ---- Get progress of a TASK. --- @return string GoalsText -function TASK:GetGoalProgress() - self:F2() - - local GoalsText = "" - for GoalVerb, GoalVerbData in pairs( self.GoalTasks ) do - local Goals = self:GetGoalCompletion( GoalVerb ) - if Goals and Goals ~= "" then - Goals = '(' .. Goals .. ')' - else - Goals = '( - )' - end - GoalsText = GoalsText .. GoalVerb .. ': ' .. self:GetGoalCount(GoalVerb) .. ' goals ' .. Goals .. ' of ' .. self:GetGoalTotal(GoalVerb) .. ' goals completed (' .. self:GetGoalPercentage(GoalVerb) .. '%); ' - end - - if GoalsText == "" then - GoalsText = "( - )" - end - - return GoalsText -end - ---- Show progress of a TASK. --- @param MISSION Mission Group structure describing the Mission. --- @param CLIENT Client Group structure describing the Client. -function TASK:ShowGoalProgress( Mission, Client ) - self:F2() - - local GoalsText = "" - for GoalVerb, GoalVerbData in pairs( self.GoalTasks ) do - if Mission:IsCompleted() then - else - local Goals = self:GetGoalCompletion( GoalVerb ) - if Goals and Goals ~= "" then - else - Goals = "-" - end - GoalsText = GoalsText .. self:GetGoalProgress() - end - end - - if Mission.MissionReportFlash or Mission.MissionReportShow then - Client:Message( GoalsText, 10, "/TASKPROGRESS" .. self.ClassName, "Mission Command: Task Status", 30 ) - end -end - ---- Sets a TASK to status Done. -function TASK:Done() - self:F2() - self.TaskDone = true -end - ---- Returns if a TASK is done. --- @return bool -function TASK:IsDone() - self:F2( self.TaskDone ) - return self.TaskDone -end - ---- Sets a TASK to status failed. -function TASK:Failed() - self:F() - self.TaskFailed = true -end - ---- Returns if a TASk has failed. --- @return bool -function TASK:IsFailed() - self:F2( self.TaskFailed ) - return self.TaskFailed -end - -function TASK:Reset( Mission, Client ) - self:F2() - self.ExecuteStage = _TransportExecuteStage.NONE -end - ---- Returns the Goals of a TASK --- @return @table Goals -function TASK:GetGoals() - return self.GoalTasks -end - ---- Returns if a TASK has Goal(s). --- @param #TASK self --- @param #string GoalVerb is the name of the Goal of the TASK. --- @return bool -function TASK:Goal( GoalVerb ) - self:F2( { GoalVerb } ) - if not GoalVerb then - GoalVerb = self.GoalVerb - end - self:T2( {self.GoalTasks[GoalVerb] } ) - if self.GoalTasks[GoalVerb] and self.GoalTasks[GoalVerb].GoalTotal > 0 then - return true - else - return false - end -end - ---- Sets the total Goals to be achieved of the Goal Name --- @param number GoalTotal is the number of times the GoalVerb needs to be achieved. --- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. -function TASK:SetGoalTotal( GoalTotal, GoalVerb ) - self:F2( { GoalTotal, GoalVerb } ) - - if not GoalVerb then - GoalVerb = self.GoalVerb - end - self.GoalTasks[GoalVerb] = {} - self.GoalTasks[GoalVerb].Goals = {} - self.GoalTasks[GoalVerb].GoalTotal = GoalTotal - self.GoalTasks[GoalVerb].GoalCount = 0 - return self -end - ---- Gets the total of Goals to be achieved within the TASK of the GoalVerb. --- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. -function TASK:GetGoalTotal( GoalVerb ) - self:F2( { GoalVerb } ) - if not GoalVerb then - GoalVerb = self.GoalVerb - end - if self:Goal( GoalVerb ) then - return self.GoalTasks[GoalVerb].GoalTotal - else - return 0 - end -end - ---- Sets the total of Goals currently achieved within the TASK of the GoalVerb. --- @param number GoalCount is the total number of Goals achieved within the TASK. --- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. --- @return TASK -function TASK:SetGoalCount( GoalCount, GoalVerb ) - self:F2() - if not GoalVerb then - GoalVerb = self.GoalVerb - end - if self:Goal( GoalVerb) then - self.GoalTasks[GoalVerb].GoalCount = GoalCount - end - return self -end - ---- Increments the total of Goals currently achieved within the TASK of the GoalVerb, with the given GoalCountIncrease. --- @param number GoalCountIncrease is the number of new Goals achieved within the TASK. --- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. --- @return TASK -function TASK:IncreaseGoalCount( GoalCountIncrease, GoalVerb ) - self:F2( { GoalCountIncrease, GoalVerb } ) - if not GoalVerb then - GoalVerb = self.GoalVerb - end - if self:Goal( GoalVerb) then - self.GoalTasks[GoalVerb].GoalCount = self.GoalTasks[GoalVerb].GoalCount + GoalCountIncrease - end - return self -end - ---- Gets the total of Goals currently achieved within the TASK of the GoalVerb. --- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. --- @return TASK -function TASK:GetGoalCount( GoalVerb ) - self:F2() - if not GoalVerb then - GoalVerb = self.GoalVerb - end - if self:Goal( GoalVerb ) then - return self.GoalTasks[GoalVerb].GoalCount - else - return 0 - end -end - ---- Gets the percentage of Goals currently achieved within the TASK of the GoalVerb. --- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. --- @return TASK -function TASK:GetGoalPercentage( GoalVerb ) - self:F2() - if not GoalVerb then - GoalVerb = self.GoalVerb - end - if self:Goal( GoalVerb ) then - return math.floor( self:GetGoalCount( GoalVerb ) / self:GetGoalTotal( GoalVerb ) * 100 + .5 ) - else - return 100 - end -end - ---- Returns if all the Goals of the TASK were achieved. --- @return bool -function TASK:IsGoalReached() - self:F2() - - local GoalReached = true - - for GoalVerb, Goals in pairs( self.GoalTasks ) do - self:T2( { "GoalVerb", GoalVerb } ) - if self:Goal( GoalVerb ) then - local GoalToDo = self:GetGoalTotal( GoalVerb ) - self:GetGoalCount( GoalVerb ) - self:T2( "GoalToDo = " .. GoalToDo ) - if GoalToDo <= 0 then - else - GoalReached = false - break - end - else - break - end - end - - self:T( { GoalReached, self.GoalTasks } ) - return GoalReached -end - ---- Adds an Additional Goal for the TASK to be achieved. --- @param string GoalVerb is the name of the Goal of the TASK. --- @param string GoalTask is a text describing the Goal of the TASK to be achieved. --- @param number GoalIncrease is a number by which the Goal achievement is increasing. -function TASK:AddGoalCompletion( GoalVerb, GoalTask, GoalIncrease ) - self:F2( { GoalVerb, GoalTask, GoalIncrease } ) - - if self:Goal( GoalVerb ) then - self.GoalTasks[GoalVerb].Goals[#self.GoalTasks[GoalVerb].Goals+1] = GoalTask - self.GoalTasks[GoalVerb].GoalCount = self.GoalTasks[GoalVerb].GoalCount + GoalIncrease - end - return self -end - ---- Returns if the additional Goal for the TASK was completed. --- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. --- @return string Goals -function TASK:GetGoalCompletion( GoalVerb ) - self:F2( { GoalVerb } ) - - if self:Goal( GoalVerb ) then - local Goals = "" - for GoalID, GoalName in pairs( self.GoalTasks[GoalVerb].Goals ) do Goals = Goals .. GoalName .. " + " end - return Goals:gsub(" + $", ""), self.GoalTasks[GoalVerb].GoalCount - end -end - -function TASK.MenuAction( Parameter ) - Parameter.ReferenceTask.ExecuteStage = _TransportExecuteStage.EXECUTING - Parameter.ReferenceTask.Cargo = Parameter.CargoTask -end - -function TASK:StageExecute() - self:F() - - local Execute = false - - if self.Frequency == STAGE.FREQUENCY.REPEAT then - Execute = true - elseif self.Frequency == STAGE.FREQUENCY.NONE then - Execute = false - elseif self.Frequency >= 0 then - Execute = true - self.Frequency = self.Frequency - 1 - end - - return Execute - -end - ---- Work function to set signal events within a TASK. -function TASK:AddSignal( SignalUnitNames, SignalType, SignalColor, SignalHeight ) - self:F() - - local Valid = true - - if Valid then - if type( SignalUnitNames ) == "table" then - self.LandingZoneSignalUnitNames = SignalUnitNames - else - self.LandingZoneSignalUnitNames = { SignalUnitNames } - end - self.LandingZoneSignalType = SignalType - self.LandingZoneSignalColor = SignalColor - self.Signalled = false - if SignalHeight ~= nil then - self.LandingZoneSignalHeight = SignalHeight - else - self.LandingZoneSignalHeight = 0 - end - - if self.TaskBriefing then - self.TaskBriefing = self.TaskBriefing .. " " .. SignalColor.TEXT .. " " .. SignalType.TEXT .. " will be fired when entering the landing zone." - end - end - - return Valid -end - ---- When the CLIENT is approaching the landing zone, a RED SMOKE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddSmokeRed( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.SMOKE, TASK.SIGNAL.COLOR.RED, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, a GREEN SMOKE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddSmokeGreen( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.SMOKE, TASK.SIGNAL.COLOR.GREEN, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, a BLUE SMOKE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddSmokeBlue( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.SMOKE, TASK.SIGNAL.COLOR.BLUE, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, a WHITE SMOKE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddSmokeWhite( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.SMOKE, TASK.SIGNAL.COLOR.WHITE, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, an ORANGE SMOKE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddSmokeOrange( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.SMOKE, TASK.SIGNAL.COLOR.ORANGE, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, a RED FLARE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddFlareRed( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.FLARE, TASK.SIGNAL.COLOR.RED, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, a GREEN FLARE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddFlareGreen( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.FLARE, TASK.SIGNAL.COLOR.GREEN, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, a BLUE FLARE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddFlareBlue( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.FLARE, TASK.SIGNAL.COLOR.BLUE, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, a WHITE FLARE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddFlareWhite( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.FLARE, TASK.SIGNAL.COLOR.WHITE, SignalHeight ) -end - ---- When the CLIENT is approaching the landing zone, an ORANGE FLARE will be fired by an optional SignalUnitNames. --- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. --- @param number SignalHeight Altitude that the Signal should be fired... -function TASK:AddFlareOrange( SignalUnitNames, SignalHeight ) - self:F() - self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.FLARE, TASK.SIGNAL.COLOR.ORANGE, SignalHeight ) -end ---- A GOHOMETASK orchestrates the travel back to the home base, which is a specific zone defined within the ME. --- @module GOHOMETASK - -Include.File("Task") - ---- The GOHOMETASK class --- @type -GOHOMETASK = { - ClassName = "GOHOMETASK", -} - ---- Creates a new GOHOMETASK. --- @param table{string,...}|string LandingZones Table of Landing Zone names where Home(s) are located. --- @return GOHOMETASK -function GOHOMETASK:New( LandingZones ) - local self = BASE:Inherit( self, TASK:New() ) - self:F( { LandingZones } ) - local Valid = true - - Valid = routines.ValidateZone( LandingZones, "LandingZones", Valid ) - - if Valid then - self.Name = 'Fly Home' - self.TaskBriefing = "Task: Fly back to your home base. Your co-pilot will provide you with the directions (required flight angle in degrees) and the distance (in km) to your home base." - if type( LandingZones ) == "table" then - self.LandingZones = LandingZones - else - self.LandingZones = { LandingZones } - end - self.Stages = { STAGEBRIEF:New(), STAGESTART:New(), STAGEROUTE:New(), STAGEARRIVE:New(), STAGEDONE:New() } - self.SetStage( self, 1 ) - end - - return self -end ---- A DESTROYBASETASK will monitor the destruction of Groups and Units. This is a BASE class, other classes are derived from this class. --- @module DESTROYBASETASK --- @see DESTROYGROUPSTASK --- @see DESTROYUNITTYPESTASK --- @see DESTROY_RADARS_TASK - -Include.File("Task") - ---- The DESTROYBASETASK class --- @type DESTROYBASETASK -DESTROYBASETASK = { - ClassName = "DESTROYBASETASK", - Destroyed = 0, - GoalVerb = "Destroy", - DestroyPercentage = 100, -} - ---- Creates a new DESTROYBASETASK. --- @param #DESTROYBASETASK self --- @param #string DestroyGroupType Text describing the group to be destroyed. f.e. "Radar Installations", "Ships", "Vehicles", "Command Centers". --- @param #string DestroyUnitType Text describing the unit types to be destroyed. f.e. "SA-6", "Row Boats", "Tanks", "Tents". --- @param #list<#string> DestroyGroupPrefixes Table of Prefixes of the Groups to be destroyed before task is completed. --- @param #number DestroyPercentage defines the %-tage that needs to be destroyed to achieve mission success. eg. If in the Group there are 10 units, then a value of 75 would require 8 units to be destroyed from the Group to complete the @{TASK}. --- @return DESTROYBASETASK -function DESTROYBASETASK:New( DestroyGroupType, DestroyUnitType, DestroyGroupPrefixes, DestroyPercentage ) - local self = BASE:Inherit( self, TASK:New() ) - self:F() - - self.Name = 'Destroy' - self.Destroyed = 0 - self.DestroyGroupPrefixes = DestroyGroupPrefixes - self.DestroyGroupType = DestroyGroupType - self.DestroyUnitType = DestroyUnitType - if DestroyPercentage then - self.DestroyPercentage = DestroyPercentage - end - self.TaskBriefing = "Task: Destroy " .. DestroyGroupType .. "." - self.Stages = { STAGEBRIEF:New(), STAGESTART:New(), STAGEGROUPSDESTROYED:New(), STAGEDONE:New() } - self.SetStage( self, 1 ) - - return self -end - ---- Handle the S_EVENT_DEAD events to validate the destruction of units for the task monitoring. --- @param #DESTROYBASETASK self --- @param Event#EVENTDATA Event structure of MOOSE. -function DESTROYBASETASK:EventDead( Event ) - self:F( { Event } ) - - if Event.IniDCSUnit then - local DestroyUnit = Event.IniDCSUnit - local DestroyUnitName = Event.IniDCSUnitName - local DestroyGroup = Event.IniDCSGroup - local DestroyGroupName = Event.IniDCSGroupName - - --TODO: I need to fix here if 2 groups in the mission have a similar name with GroupPrefix equal, then i should differentiate for which group the goal was reached! - --I may need to test if for the goalverb that group goal was reached or something. Need to think about it a bit more ... - local UnitsDestroyed = 0 - for DestroyGroupPrefixID, DestroyGroupPrefix in pairs( self.DestroyGroupPrefixes ) do - self:T( DestroyGroupPrefix ) - if string.find( DestroyGroupName, DestroyGroupPrefix, 1, true ) then - self:T( BASE:Inherited(self).ClassName ) - UnitsDestroyed = self:ReportGoalProgress( DestroyGroup, DestroyUnit ) - self:T( UnitsDestroyed ) - end - end - - self:T( { UnitsDestroyed } ) - self:IncreaseGoalCount( UnitsDestroyed, self.GoalVerb ) - end - -end - ---- Validate task completeness of DESTROYBASETASK. --- @param DestroyGroup Group structure describing the group to be evaluated. --- @param DestroyUnit Unit structure describing the Unit to be evaluated. -function DESTROYBASETASK:ReportGoalProgress( DestroyGroup, DestroyUnit ) - self:F() - - return 0 -end ---- DESTROYGROUPSTASK --- @module DESTROYGROUPSTASK - -Include.File("DestroyBaseTask") - ---- The DESTROYGROUPSTASK class --- @type -DESTROYGROUPSTASK = { - ClassName = "DESTROYGROUPSTASK", - GoalVerb = "Destroy Groups", -} - ---- Creates a new DESTROYGROUPSTASK. --- @param #DESTROYGROUPSTASK self --- @param #string DestroyGroupType String describing the group to be destroyed. --- @param #string DestroyUnitType String describing the unit to be destroyed. --- @param #list<#string> DestroyGroupNames Table of string containing the name of the groups to be destroyed before task is completed. --- @param #number DestroyPercentage defines the %-tage that needs to be destroyed to achieve mission success. eg. If in the Group there are 10 units, then a value of 75 would require 8 units to be destroyed from the Group to complete the @{TASK}. ----@return DESTROYGROUPSTASK -function DESTROYGROUPSTASK:New( DestroyGroupType, DestroyUnitType, DestroyGroupNames, DestroyPercentage ) - local self = BASE:Inherit( self, DESTROYBASETASK:New( DestroyGroupType, DestroyUnitType, DestroyGroupNames, DestroyPercentage ) ) - self:F() - - self.Name = 'Destroy Groups' - self.GoalVerb = "Destroy " .. DestroyGroupType - - _EVENTDISPATCHER:OnDead( self.EventDead , self ) - _EVENTDISPATCHER:OnCrash( self.EventDead , self ) - - return self -end - ---- Report Goal Progress. --- @param #DESTROYGROUPSTASK self --- @param DCSGroup#Group DestroyGroup Group structure describing the group to be evaluated. --- @param DCSUnit#Unit DestroyUnit Unit structure describing the Unit to be evaluated. --- @return #number The DestroyCount reflecting the amount of units destroyed within the group. -function DESTROYGROUPSTASK:ReportGoalProgress( DestroyGroup, DestroyUnit ) - self:F( { DestroyGroup, DestroyUnit, self.DestroyPercentage } ) - - local DestroyGroupSize = DestroyGroup:getSize() - 1 -- When a DEAD event occurs, the getSize is still one larger than the destroyed unit. - local DestroyGroupInitialSize = DestroyGroup:getInitialSize() - self:T( { DestroyGroupSize, DestroyGroupInitialSize - ( DestroyGroupInitialSize * self.DestroyPercentage / 100 ) } ) - - local DestroyCount = 0 - if DestroyGroup then - if DestroyGroupSize <= DestroyGroupInitialSize - ( DestroyGroupInitialSize * self.DestroyPercentage / 100 ) then - DestroyCount = 1 - end - else - DestroyCount = 1 - end - - self:T( DestroyCount ) - - return DestroyCount -end ---- Task class to destroy radar installations. --- @module DESTROYRADARSTASK - -Include.File("DestroyBaseTask") - ---- The DESTROYRADARS class --- @type -DESTROYRADARSTASK = { - ClassName = "DESTROYRADARSTASK", - GoalVerb = "Destroy Radars" -} - ---- Creates a new DESTROYRADARSTASK. --- @param table{string,...} DestroyGroupNames Table of string containing the group names of which the radars are be destroyed. --- @return DESTROYRADARSTASK -function DESTROYRADARSTASK:New( DestroyGroupNames ) - local self = BASE:Inherit( self, DESTROYGROUPSTASK:New( 'radar installations', 'radars', DestroyGroupNames ) ) - self:F() - - self.Name = 'Destroy Radars' - - _EVENTDISPATCHER:OnDead( self.EventDead , self ) - - return self -end - ---- Report Goal Progress. --- @param Group DestroyGroup Group structure describing the group to be evaluated. --- @param Unit DestroyUnit Unit structure describing the Unit to be evaluated. -function DESTROYRADARSTASK:ReportGoalProgress( DestroyGroup, DestroyUnit ) - self:F( { DestroyGroup, DestroyUnit } ) - - local DestroyCount = 0 - if DestroyUnit and DestroyUnit:hasSensors( Unit.SensorType.RADAR, Unit.RadarType.AS ) then - if DestroyUnit and DestroyUnit:getLife() <= 1.0 then - self:T( 'Destroyed a radar' ) - DestroyCount = 1 - end - end - return DestroyCount -end ---- Set TASK to destroy certain unit types. --- @module DESTROYUNITTYPESTASK - -Include.File("DestroyBaseTask") - ---- The DESTROYUNITTYPESTASK class --- @type -DESTROYUNITTYPESTASK = { - ClassName = "DESTROYUNITTYPESTASK", - GoalVerb = "Destroy", -} - ---- Creates a new DESTROYUNITTYPESTASK. --- @param string DestroyGroupType String describing the group to be destroyed. f.e. "Radar Installations", "Fleet", "Batallion", "Command Centers". --- @param string DestroyUnitType String describing the unit to be destroyed. f.e. "radars", "ships", "tanks", "centers". --- @param table{string,...} DestroyGroupNames Table of string containing the group names of which the radars are be destroyed. --- @param string DestroyUnitTypes Table of string containing the type names of the units to achieve mission success. --- @return DESTROYUNITTYPESTASK -function DESTROYUNITTYPESTASK:New( DestroyGroupType, DestroyUnitType, DestroyGroupNames, DestroyUnitTypes ) - local self = BASE:Inherit( self, DESTROYBASETASK:New( DestroyGroupType, DestroyUnitType, DestroyGroupNames ) ) - self:F( { DestroyGroupType, DestroyUnitType, DestroyGroupNames, DestroyUnitTypes } ) - - if type(DestroyUnitTypes) == 'table' then - self.DestroyUnitTypes = DestroyUnitTypes - else - self.DestroyUnitTypes = { DestroyUnitTypes } - end - - self.Name = 'Destroy Unit Types' - self.GoalVerb = "Destroy " .. DestroyGroupType - - _EVENTDISPATCHER:OnDead( self.EventDead , self ) - - return self -end - ---- Report Goal Progress. --- @param Group DestroyGroup Group structure describing the group to be evaluated. --- @param Unit DestroyUnit Unit structure describing the Unit to be evaluated. -function DESTROYUNITTYPESTASK:ReportGoalProgress( DestroyGroup, DestroyUnit ) - self:F( { DestroyGroup, DestroyUnit } ) - - local DestroyCount = 0 - for UnitTypeID, UnitType in pairs( self.DestroyUnitTypes ) do - if DestroyUnit and DestroyUnit:getTypeName() == UnitType then - if DestroyUnit and DestroyUnit:getLife() <= 1.0 then - DestroyCount = DestroyCount + 1 - end - end - end - return DestroyCount -end ---- A PICKUPTASK orchestrates the loading of CARGO at a specific landing zone. --- @module PICKUPTASK --- @parent TASK - -Include.File("Task") -Include.File("Cargo") - ---- The PICKUPTASK class --- @type -PICKUPTASK = { - ClassName = "PICKUPTASK", - TEXT = { "Pick-Up", "picked-up", "loaded" }, - GoalVerb = "Pick-Up" -} - ---- Creates a new PICKUPTASK. --- @param table{string,...}|string LandingZones Table of Zone names where Cargo is to be loaded. --- @param CARGO_TYPE CargoType Type of the Cargo. The type must be of the following Enumeration:.. --- @param number OnBoardSide Reflects from which side the cargo Group will be on-boarded on the Carrier. -function PICKUPTASK:New( CargoType, OnBoardSide ) - local self = BASE:Inherit( self, TASK:New() ) - self:F() - - -- self holds the inherited instance of the PICKUPTASK Class to the BASE class. - - local Valid = true - - if Valid then - self.Name = 'Pickup Cargo' - self.TaskBriefing = "Task: Fly to the indicated landing zones and pickup " .. CargoType .. ". Your co-pilot will provide you with the directions (required flight angle in degrees) and the distance (in km) to the pickup zone." - self.CargoType = CargoType - self.GoalVerb = CargoType .. " " .. self.GoalVerb - self.OnBoardSide = OnBoardSide - self.IsLandingRequired = true -- required to decide whether the client needs to land or not - self.IsSlingLoad = false -- Indicates whether the cargo is a sling load cargo - self.Stages = { STAGE_CARGO_INIT:New(), STAGE_CARGO_LOAD:New(), STAGEBRIEF:New(), STAGESTART:New(), STAGEROUTE:New(), STAGELANDING:New(), STAGELANDED:New(), STAGELOAD:New(), STAGEDONE:New() } - self.SetStage( self, 1 ) - end - - return self -end - -function PICKUPTASK:FromZone( LandingZone ) - self:F() - - self.LandingZones.LandingZoneNames[LandingZone.CargoZoneName] = LandingZone.CargoZoneName - self.LandingZones.LandingZones[LandingZone.CargoZoneName] = LandingZone - - return self -end - -function PICKUPTASK:InitCargo( InitCargos ) - self:F( { InitCargos } ) - - if type( InitCargos ) == "table" then - self.Cargos.InitCargos = InitCargos - else - self.Cargos.InitCargos = { InitCargos } - end - - return self -end - -function PICKUPTASK:LoadCargo( LoadCargos ) - self:F( { LoadCargos } ) - - if type( LoadCargos ) == "table" then - self.Cargos.LoadCargos = LoadCargos - else - self.Cargos.LoadCargos = { LoadCargos } - end - - return self -end - -function PICKUPTASK:AddCargoMenus( Client, Cargos, TransportRadius ) - self:F() - - for CargoID, Cargo in pairs( Cargos ) do - - self:T( { Cargo.ClassName, Cargo.CargoName, Cargo.CargoType, Cargo:IsStatusNone(), Cargo:IsStatusLoaded(), Cargo:IsStatusLoading(), Cargo:IsStatusUnLoaded() } ) - - -- If the Cargo has no status, allow the menu option. - if Cargo:IsStatusNone() or ( Cargo:IsStatusLoading() and Client == Cargo:IsLoadingToClient() ) then - - local MenuAdd = false - if Cargo:IsNear( Client, self.CurrentCargoZone ) then - MenuAdd = true - end - - if MenuAdd then - if Client._Menus[Cargo.CargoType] == nil then - Client._Menus[Cargo.CargoType] = {} - end - - if not Client._Menus[Cargo.CargoType].PickupMenu then - Client._Menus[Cargo.CargoType].PickupMenu = missionCommands.addSubMenuForGroup( - Client:GetClientGroupID(), - self.TEXT[1] .. " " .. Cargo.CargoType, - nil - ) - self:T( 'Added PickupMenu: ' .. self.TEXT[1] .. " " .. Cargo.CargoType ) - end - - if Client._Menus[Cargo.CargoType].PickupSubMenus == nil then - Client._Menus[Cargo.CargoType].PickupSubMenus = {} - end - - Client._Menus[Cargo.CargoType].PickupSubMenus[ #Client._Menus[Cargo.CargoType].PickupSubMenus + 1 ] = missionCommands.addCommandForGroup( - Client:GetClientGroupID(), - Cargo.CargoName .. " ( " .. Cargo.CargoWeight .. "kg )", - Client._Menus[Cargo.CargoType].PickupMenu, - self.MenuAction, - { ReferenceTask = self, CargoTask = Cargo } - ) - self:T( 'Added PickupSubMenu' .. Cargo.CargoType .. ":" .. Cargo.CargoName .. " ( " .. Cargo.CargoWeight .. "kg )" ) - end - end - end - -end - -function PICKUPTASK:RemoveCargoMenus( Client ) - self:F() - - for MenuID, MenuData in pairs( Client._Menus ) do - for SubMenuID, SubMenuData in pairs( MenuData.PickupSubMenus ) do - missionCommands.removeItemForGroup( Client:GetClientGroupID(), SubMenuData ) - self:T( "Removed PickupSubMenu " ) - SubMenuData = nil - end - if MenuData.PickupMenu then - missionCommands.removeItemForGroup( Client:GetClientGroupID(), MenuData.PickupMenu ) - self:T( "Removed PickupMenu " ) - MenuData.PickupMenu = nil - end - end - - for CargoID, Cargo in pairs( CARGOS ) do - self:T( { Cargo.ClassName, Cargo.CargoName, Cargo.CargoType, Cargo:IsStatusNone(), Cargo:IsStatusLoaded(), Cargo:IsStatusLoading(), Cargo:IsStatusUnLoaded() } ) - if Cargo:IsStatusLoading() and Client == Cargo:IsLoadingToClient() then - Cargo:StatusNone() - end - end - -end - - - -function PICKUPTASK:HasFailed( ClientDead ) - self:F() - - local TaskHasFailed = self.TaskFailed - return TaskHasFailed -end - ---- A DEPLOYTASK orchestrates the deployment of CARGO within a specific landing zone. --- @module DEPLOYTASK - -Include.File( "Task" ) - ---- A DeployTask --- @type DEPLOYTASK -DEPLOYTASK = { - ClassName = "DEPLOYTASK", - TEXT = { "Deploy", "deployed", "unloaded" }, - GoalVerb = "Deployment" -} - - ---- Creates a new DEPLOYTASK object, which models the sequence of STAGEs to unload a cargo. --- @function [parent=#DEPLOYTASK] New --- @param #string CargoType Type of the Cargo. --- @return #DEPLOYTASK The created DeployTask -function DEPLOYTASK:New( CargoType ) - local self = BASE:Inherit( self, TASK:New() ) - self:F() - - local Valid = true - - if Valid then - self.Name = 'Deploy Cargo' - self.TaskBriefing = "Fly to one of the indicated landing zones and deploy " .. CargoType .. ". Your co-pilot will provide you with the directions (required flight angle in degrees) and the distance (in km) to the deployment zone." - self.CargoType = CargoType - self.GoalVerb = CargoType .. " " .. self.GoalVerb - self.Stages = { STAGE_CARGO_INIT:New(), STAGE_CARGO_LOAD:New(), STAGEBRIEF:New(), STAGESTART:New(), STAGEROUTE:New(), STAGELANDING:New(), STAGELANDED:New(), STAGEUNLOAD:New(), STAGEDONE:New() } - self.SetStage( self, 1 ) - end - - return self -end - -function DEPLOYTASK:ToZone( LandingZone ) - self:F() - - self.LandingZones.LandingZoneNames[LandingZone.CargoZoneName] = LandingZone.CargoZoneName - self.LandingZones.LandingZones[LandingZone.CargoZoneName] = LandingZone - - return self -end - - -function DEPLOYTASK:InitCargo( InitCargos ) - self:F( { InitCargos } ) - - if type( InitCargos ) == "table" then - self.Cargos.InitCargos = InitCargos - else - self.Cargos.InitCargos = { InitCargos } - end - - return self -end - - -function DEPLOYTASK:LoadCargo( LoadCargos ) - self:F( { LoadCargos } ) - - if type( LoadCargos ) == "table" then - self.Cargos.LoadCargos = LoadCargos - else - self.Cargos.LoadCargos = { LoadCargos } - end - - return self -end - - ---- When the cargo is unloaded, it will move to the target zone name. --- @param string TargetZoneName Name of the Zone to where the Cargo should move after unloading. -function DEPLOYTASK:SetCargoTargetZoneName( TargetZoneName ) - self:F() - - local Valid = true - - Valid = routines.ValidateString( TargetZoneName, "TargetZoneName", Valid ) - - if Valid then - self.TargetZoneName = TargetZoneName - end - - return Valid - -end - -function DEPLOYTASK:AddCargoMenus( Client, Cargos, TransportRadius ) - self:F() - - local ClientGroupID = Client:GetClientGroupID() - - self:T( ClientGroupID ) - - for CargoID, Cargo in pairs( Cargos ) do - - self:T( { Cargo.ClassName, Cargo.CargoName, Cargo.CargoType, Cargo.CargoWeight } ) - - if Cargo:IsStatusLoaded() and Client == Cargo:IsLoadedInClient() then - - if Client._Menus[Cargo.CargoType] == nil then - Client._Menus[Cargo.CargoType] = {} - end - - if not Client._Menus[Cargo.CargoType].DeployMenu then - Client._Menus[Cargo.CargoType].DeployMenu = missionCommands.addSubMenuForGroup( - ClientGroupID, - self.TEXT[1] .. " " .. Cargo.CargoType, - nil - ) - self:T( 'Added DeployMenu ' .. self.TEXT[1] ) - end - - if Client._Menus[Cargo.CargoType].DeploySubMenus == nil then - Client._Menus[Cargo.CargoType].DeploySubMenus = {} - end - - if Client._Menus[Cargo.CargoType].DeployMenu == nil then - self:T( 'deploymenu is nil' ) - end - - Client._Menus[Cargo.CargoType].DeploySubMenus[ #Client._Menus[Cargo.CargoType].DeploySubMenus + 1 ] = missionCommands.addCommandForGroup( - ClientGroupID, - Cargo.CargoName .. " ( " .. Cargo.CargoWeight .. "kg )", - Client._Menus[Cargo.CargoType].DeployMenu, - self.MenuAction, - { ReferenceTask = self, CargoTask = Cargo } - ) - self:T( 'Added DeploySubMenu ' .. Cargo.CargoType .. ":" .. Cargo.CargoName .. " ( " .. Cargo.CargoWeight .. "kg )" ) - end - end - -end - -function DEPLOYTASK:RemoveCargoMenus( Client ) - self:F() - - local ClientGroupID = Client:GetClientGroupID() - self:T( ClientGroupID ) - - for MenuID, MenuData in pairs( Client._Menus ) do - if MenuData.DeploySubMenus ~= nil then - for SubMenuID, SubMenuData in pairs( MenuData.DeploySubMenus ) do - missionCommands.removeItemForGroup( ClientGroupID, SubMenuData ) - self:T( "Removed DeploySubMenu " ) - SubMenuData = nil - end - end - if MenuData.DeployMenu then - missionCommands.removeItemForGroup( ClientGroupID, MenuData.DeployMenu ) - self:T( "Removed DeployMenu " ) - MenuData.DeployMenu = nil - end - end - -end ---- A NOTASK is a dummy activity... But it will show a Mission Briefing... --- @module NOTASK - -Include.File("Task") - ---- The NOTASK class --- @type -NOTASK = { - ClassName = "NOTASK", -} - ---- Creates a new NOTASK. -function NOTASK:New() - local self = BASE:Inherit( self, TASK:New() ) - self:F() - - local Valid = true - - if Valid then - self.Name = 'Nothing' - self.TaskBriefing = "Task: Execute your mission." - self.Stages = { STAGEBRIEF:New(), STAGESTART:New(), STAGEDONE:New() } - self.SetStage( self, 1 ) - end - - return self -end ---- A ROUTETASK orchestrates the travel to a specific zone defined within the ME. --- @module ROUTETASK - ---- The ROUTETASK class --- @type -ROUTETASK = { - ClassName = "ROUTETASK", - GoalVerb = "Route", -} - ---- Creates a new ROUTETASK. --- @param table{sring,...}|string LandingZones Table of Zone Names where the target is located. --- @param string TaskBriefing (optional) Defines a text describing the briefing of the task. --- @return ROUTETASK -function ROUTETASK:New( LandingZones, TaskBriefing ) - local self = BASE:Inherit( self, TASK:New() ) - self:F( { LandingZones, TaskBriefing } ) - - local Valid = true - - Valid = routines.ValidateZone( LandingZones, "LandingZones", Valid ) - - if Valid then - self.Name = 'Route To Zone' - if TaskBriefing then - self.TaskBriefing = TaskBriefing .. " Your co-pilot will provide you with the directions (required flight angle in degrees) and the distance (in km) to the target objective." - else - self.TaskBriefing = "Task: Fly to specified zone(s). Your co-pilot will provide you with the directions (required flight angle in degrees) and the distance (in km) to the target objective." - end - if type( LandingZones ) == "table" then - self.LandingZones = LandingZones - else - self.LandingZones = { LandingZones } - end - self.Stages = { STAGEBRIEF:New(), STAGESTART:New(), STAGEROUTE:New(), STAGEARRIVE:New(), STAGEDONE:New() } - self.SetStage( self, 1 ) - end - - return self -end - ---- A MISSION is the main owner of a Mission orchestration within MOOSE . The Mission framework orchestrates @{CLIENT}s, @{TASK}s, @{STAGE}s etc. --- A @{CLIENT} needs to be registered within the @{MISSION} through the function @{AddClient}. A @{TASK} needs to be registered within the @{MISSION} through the function @{AddTask}. --- @module Mission - -Include.File( "Routines" ) -Include.File( "Base" ) -Include.File( "Client" ) -Include.File( "Task" ) - ---- The MISSION class --- @type MISSION --- @extends Base#BASE --- @field #MISSION.Clients _Clients --- @field #string MissionBriefing -MISSION = { - ClassName = "MISSION", - Name = "", - MissionStatus = "PENDING", - _Clients = {}, - _Tasks = {}, - _ActiveTasks = {}, - GoalFunction = nil, - MissionReportTrigger = 0, - MissionProgressTrigger = 0, - MissionReportShow = false, - MissionReportFlash = false, - MissionTimeInterval = 0, - MissionCoalition = "", - SUCCESS = 1, - FAILED = 2, - REPEAT = 3, - _GoalTasks = {} -} - ---- @type MISSION.Clients --- @list - -function MISSION:Meta() - - local self = BASE:Inherit( self, BASE:New() ) - self:F() - - return self -end - ---- This is the main MISSION declaration method. Each Mission is like the master or a Mission orchestration between, Clients, Tasks, Stages etc. --- @param string MissionName is the name of the mission. This name will be used to reference the status of each mission by the players. --- @param string MissionPriority is a string indicating the "priority" of the Mission. f.e. "Primary", "Secondary" or "First", "Second". It is free format and up to the Mission designer to choose. There are no rules behind this field. --- @param string MissionBriefing is a string indicating the mission briefing to be shown when a player joins a @{CLIENT}. --- @param string MissionCoalition is a string indicating the coalition or party to which this mission belongs to. It is free format and can be chosen freely by the mission designer. Note that this field is not to be confused with the coalition concept of the ME. Examples of a Mission Coalition could be "NATO", "CCCP", "Intruders", "Terrorists"... --- @return MISSION --- @usage --- -- Declare a few missions. --- local Mission = MISSIONSCHEDULER.AddMission( 'Russia Transport Troops SA-6', 'Operational', 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', 'Russia' ) --- local Mission = MISSIONSCHEDULER.AddMission( 'Patriots', 'Primary', 'Our intelligence reports that 3 Patriot SAM defense batteries are located near Ruisi, Kvarhiti and Gori.', 'Russia' ) --- local Mission = MISSIONSCHEDULER.AddMission( 'Package Delivery', 'Operational', 'In order to be in full control of the situation, we need you to deliver a very important package at a secret location. Fly undetected through the NATO defenses and deliver the secret package. The secret agent is located at waypoint 4.', 'Russia' ) --- local Mission = MISSIONSCHEDULER.AddMission( 'Rescue General', 'Tactical', 'Our intelligence has received a remote signal behind Gori. We believe it is a very important Russian General that was captured by Georgia. Go out there and rescue him! Ensure you stay out of the battle zone, keep south. Waypoint 4 is the location of our Russian General.', 'Russia' ) --- local Mission = MISSIONSCHEDULER.AddMission( 'NATO Transport Troops', 'Operational', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.', 'NATO' ) --- local Mission = MISSIONSCHEDULER.AddMission( 'SA-6 SAMs', 'Primary', 'Our intelligence reports that 3 SA-6 SAM defense batteries are located near Didmukha, Khetagurov and Berula. Eliminate the Russian SAMs.', 'NATO' ) --- local Mission = MISSIONSCHEDULER.AddMission( 'NATO Sling Load', 'Operational', 'Fly to the cargo pickup zone at Dzegvi or Kaspi, and sling the cargo to Soganlug airbase.', 'NATO' ) --- local Mission = MISSIONSCHEDULER.AddMission( 'Rescue secret agent', 'Tactical', 'In order to be in full control of the situation, we need you to rescue a secret agent from the woods behind enemy lines. Avoid the Russian defenses and rescue the agent. Keep south until Khasuri, and keep your eyes open for any SAM presence. The agent is located at waypoint 4 on your kneeboard.', 'NATO' ) -function MISSION:New( MissionName, MissionPriority, MissionBriefing, MissionCoalition ) - - self = MISSION:Meta() - self:T({ MissionName, MissionPriority, MissionBriefing, MissionCoalition }) - - local Valid = true - - Valid = routines.ValidateString( MissionName, "MissionName", Valid ) - Valid = routines.ValidateString( MissionPriority, "MissionPriority", Valid ) - Valid = routines.ValidateString( MissionBriefing, "MissionBriefing", Valid ) - Valid = routines.ValidateString( MissionCoalition, "MissionCoalition", Valid ) - - if Valid then - self.Name = MissionName - self.MissionPriority = MissionPriority - self.MissionBriefing = MissionBriefing - self.MissionCoalition = MissionCoalition - end - - return self -end - ---- Returns if a Mission has completed. --- @return bool -function MISSION:IsCompleted() - self:F() - return self.MissionStatus == "ACCOMPLISHED" -end - ---- Set a Mission to completed. -function MISSION:Completed() - self:F() - self.MissionStatus = "ACCOMPLISHED" - self:StatusToClients() -end - ---- Returns if a Mission is ongoing. --- treturn bool -function MISSION:IsOngoing() - self:F() - return self.MissionStatus == "ONGOING" -end - ---- Set a Mission to ongoing. -function MISSION:Ongoing() - self:F() - self.MissionStatus = "ONGOING" - --self:StatusToClients() -end - ---- Returns if a Mission is pending. --- treturn bool -function MISSION:IsPending() - self:F() - return self.MissionStatus == "PENDING" -end - ---- Set a Mission to pending. -function MISSION:Pending() - self:F() - self.MissionStatus = "PENDING" - self:StatusToClients() -end - ---- Returns if a Mission has failed. --- treturn bool -function MISSION:IsFailed() - self:F() - return self.MissionStatus == "FAILED" -end - ---- Set a Mission to failed. -function MISSION:Failed() - self:F() - self.MissionStatus = "FAILED" - self:StatusToClients() -end - ---- Send the status of the MISSION to all Clients. -function MISSION:StatusToClients() - self:F() - if self.MissionReportFlash then - for ClientID, Client in pairs( self._Clients ) do - Client:Message( self.MissionCoalition .. ' "' .. self.Name .. '": ' .. self.MissionStatus .. '! ( ' .. self.MissionPriority .. ' mission ) ', 10, self.Name .. '/Status', "Mission Command: Mission Status") - end - end -end - ---- Handles the reporting. After certain time intervals, a MISSION report MESSAGE will be shown to All Players. -function MISSION:ReportTrigger() - self:F() - - if self.MissionReportShow == true then - self.MissionReportShow = false - return true - else - if self.MissionReportFlash == true then - if timer.getTime() >= self.MissionReportTrigger then - self.MissionReportTrigger = timer.getTime() + self.MissionTimeInterval - return true - else - return false - end - else - return false - end - end -end - ---- Report the status of all MISSIONs to all active Clients. -function MISSION:ReportToAll() - self:F() - - local AlivePlayers = '' - for ClientID, Client in pairs( self._Clients ) do - if Client:GetDCSGroup() then - if Client:GetClientGroupDCSUnit() then - if Client:GetClientGroupDCSUnit():getLife() > 0.0 then - if AlivePlayers == '' then - AlivePlayers = ' Players: ' .. Client:GetClientGroupDCSUnit():getPlayerName() - else - AlivePlayers = AlivePlayers .. ' / ' .. Client:GetClientGroupDCSUnit():getPlayerName() - end - end - end - end - end - local Tasks = self:GetTasks() - local TaskText = "" - for TaskID, TaskData in pairs( Tasks ) do - TaskText = TaskText .. " - Task " .. TaskID .. ": " .. TaskData.Name .. ": " .. TaskData:GetGoalProgress() .. "\n" - end - MESSAGE:New( self.MissionCoalition .. ' "' .. self.Name .. '": ' .. self.MissionStatus .. ' ( ' .. self.MissionPriority .. ' mission )' .. AlivePlayers .. "\n" .. TaskText:gsub("\n$",""), "Mission Command: Mission Report", 10, self.Name .. '/Status'):ToAll() -end - - ---- Add a goal function to a MISSION. Goal functions are called when a @{TASK} within a mission has been completed. --- @param function GoalFunction is the function defined by the mission designer to evaluate whether a certain goal has been reached after a @{TASK} finishes within the @{MISSION}. A GoalFunction must accept 2 parameters: Mission, Client, which contains the current MISSION object and the current CLIENT object respectively. --- @usage --- PatriotActivation = { --- { "US SAM Patriot Zerti", false }, --- { "US SAM Patriot Zegduleti", false }, --- { "US SAM Patriot Gvleti", false } --- } --- --- function DeployPatriotTroopsGoal( Mission, Client ) --- --- --- -- Check if the cargo is all deployed for mission success. --- for CargoID, CargoData in pairs( Mission._Cargos ) do --- if Group.getByName( CargoData.CargoGroupName ) then --- CargoGroup = Group.getByName( CargoData.CargoGroupName ) --- if CargoGroup then --- -- Check if the cargo is ready to activate --- CurrentLandingZoneID = routines.IsUnitInZones( CargoGroup:getUnits()[1], Mission:GetTask( 2 ).LandingZones ) -- The second task is the Deploytask to measure mission success upon --- if CurrentLandingZoneID then --- if PatriotActivation[CurrentLandingZoneID][2] == false then --- -- Now check if this is a new Mission Task to be completed... --- trigger.action.setGroupAIOn( Group.getByName( PatriotActivation[CurrentLandingZoneID][1] ) ) --- PatriotActivation[CurrentLandingZoneID][2] = true --- MessageToBlue( "Mission Command: Message to all airborne units! The " .. PatriotActivation[CurrentLandingZoneID][1] .. " is armed. Our air defenses are now stronger.", 60, "BLUE/PatriotDefense" ) --- MessageToRed( "Mission Command: Our satellite systems are detecting additional NATO air defenses. To all airborne units: Take care!!!", 60, "RED/PatriotDefense" ) --- Mission:GetTask( 2 ):AddGoalCompletion( "Patriots activated", PatriotActivation[CurrentLandingZoneID][1], 1 ) -- Register Patriot activation as part of mission goal. --- end --- end --- end --- end --- end --- end --- --- local Mission = MISSIONSCHEDULER.AddMission( 'NATO Transport Troops', 'Operational', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.', 'NATO' ) --- Mission:AddGoalFunction( DeployPatriotTroopsGoal ) -function MISSION:AddGoalFunction( GoalFunction ) - self:F() - self.GoalFunction = GoalFunction -end - ---- Register a new @{CLIENT} to participate within the mission. --- @param CLIENT Client is the @{CLIENT} object. The object must have been instantiated with @{CLIENT:New}. --- @return CLIENT --- @usage --- Add a number of Client objects to the Mission. --- Mission:AddClient( CLIENT:FindByName( 'US UH-1H*HOT-Deploy Troops 1', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'US UH-1H*RAMP-Deploy Troops 3', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'US UH-1H*HOT-Deploy Troops 2', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.' ):Transport() ) --- Mission:AddClient( CLIENT:FindByName( 'US UH-1H*RAMP-Deploy Troops 4', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.' ):Transport() ) -function MISSION:AddClient( Client ) - self:F( { Client } ) - - local Valid = true - - if Valid then - self._Clients[Client.ClientName] = Client - end - - return Client -end - ---- Find a @{CLIENT} object within the @{MISSION} by its ClientName. --- @param CLIENT ClientName is a string defining the Client Group as defined within the ME. --- @return CLIENT --- @usage --- -- Seach for Client "Bomber" within the Mission. --- local BomberClient = Mission:FindClient( "Bomber" ) -function MISSION:FindClient( ClientName ) - self:F( { self._Clients[ClientName] } ) - return self._Clients[ClientName] -end - - ---- Register a @{TASK} to be completed within the @{MISSION}. Note that there can be multiple @{TASK}s registered to be completed. Each TASK can be set a certain Goal. The MISSION will not be completed until all Goals are reached. --- @param TASK Task is the @{TASK} object. The object must have been instantiated with @{TASK:New} or any of its inherited @{TASK}s. --- @param number TaskNumber is the sequence number of the TASK within the MISSION. This number does have to be chronological. --- @return TASK --- @usage --- -- Define a few tasks for the Mission. --- PickupZones = { "NATO Gold Pickup Zone", "NATO Titan Pickup Zone" } --- PickupSignalUnits = { "NATO Gold Coordination Center", "NATO Titan Coordination Center" } --- --- -- Assign the Pickup Task --- local PickupTask = PICKUPTASK:New( PickupZones, CARGO_TYPE.ENGINEERS, CLIENT.ONBOARDSIDE.LEFT ) --- PickupTask:AddSmokeBlue( PickupSignalUnits ) --- PickupTask:SetGoalTotal( 3 ) --- Mission:AddTask( PickupTask, 1 ) --- --- -- Assign the Deploy Task --- local PatriotActivationZones = { "US Patriot Battery 1 Activation", "US Patriot Battery 2 Activation", "US Patriot Battery 3 Activation" } --- local PatriotActivationZonesSmokeUnits = { "US SAM Patriot - Battery 1 Control", "US SAM Patriot - Battery 2 Control", "US SAM Patriot - Battery 3 Control" } --- local DeployTask = DEPLOYTASK:New( PatriotActivationZones, CARGO_TYPE.ENGINEERS ) --- --DeployTask:SetCargoTargetZoneName( 'US Troops Attack ' .. math.random(2) ) --- DeployTask:AddSmokeBlue( PatriotActivationZonesSmokeUnits ) --- DeployTask:SetGoalTotal( 3 ) --- DeployTask:SetGoalTotal( 3, "Patriots activated" ) --- Mission:AddTask( DeployTask, 2 ) - -function MISSION:AddTask( Task, TaskNumber ) - self:F() - - self._Tasks[TaskNumber] = Task - self._Tasks[TaskNumber]:EnableEvents() - self._Tasks[TaskNumber].ID = TaskNumber - - return Task - end - ---- Get the TASK idenified by the TaskNumber from the Mission. This function is useful in GoalFunctions. --- @param number TaskNumber is the number of the @{TASK} within the @{MISSION}. --- @return TASK --- @usage --- -- Get Task 2 from the Mission. --- Task2 = Mission:GetTask( 2 ) - -function MISSION:GetTask( TaskNumber ) - self:F() - - local Valid = true - - local Task = nil - - if type(TaskNumber) ~= "number" then - Valid = false - end - - if Valid then - Task = self._Tasks[TaskNumber] - end - - return Task -end - ---- Get all the TASKs from the Mission. This function is useful in GoalFunctions. --- @return {TASK,...} Structure of TASKS with the @{TASK} number as the key. --- @usage --- -- Get Tasks from the Mission. --- Tasks = Mission:GetTasks() --- env.info( "Task 2 Completion = " .. Tasks[2]:GetGoalPercentage() .. "%" ) -function MISSION:GetTasks() - self:F() - - return self._Tasks -end - - ---[[ - _TransportExecuteStage: Defines the different stages of Transport unload/load execution. This table is internal and is used to control the validity of Transport load/unload timing. - - - _TransportExecuteStage.EXECUTING - - _TransportExecuteStage.SUCCESS - - _TransportExecuteStage.FAILED - ---]] -_TransportExecuteStage = { - NONE = 0, - EXECUTING = 1, - SUCCESS = 2, - FAILED = 3 -} - - ---- The MISSIONSCHEDULER is an OBJECT and is the main scheduler of ALL active MISSIONs registered within this scheduler. It's workings are considered internal and is automatically created when the Mission.lua file is included. --- @type MISSIONSCHEDULER --- @field #MISSIONSCHEDULER.MISSIONS Missions -MISSIONSCHEDULER = { - Missions = {}, - MissionCount = 0, - TimeIntervalCount = 0, - TimeIntervalShow = 150, - TimeSeconds = 14400, - TimeShow = 5 -} - ---- @type MISSIONSCHEDULER.MISSIONS --- @list <#MISSION> Mission - ---- This is the main MISSIONSCHEDULER Scheduler function. It is considered internal and is automatically created when the Mission.lua file is included. -function MISSIONSCHEDULER.Scheduler() - - - -- loop through the missions in the TransportTasks - for MissionName, MissionData in pairs( MISSIONSCHEDULER.Missions ) do - - local Mission = MissionData -- #MISSION - - if not Mission:IsCompleted() then - - -- This flag will monitor if for this mission, there are clients alive. If this flag is still false at the end of the loop, the mission status will be set to Pending (if not Failed or Completed). - local ClientsAlive = false - - for ClientID, ClientData in pairs( Mission._Clients ) do - - local Client = ClientData -- Client#CLIENT - - if Client:IsAlive() then - - -- There is at least one Client that is alive... So the Mission status is set to Ongoing. - ClientsAlive = true - - -- If this Client was not registered as Alive before: - -- 1. We register the Client as Alive. - -- 2. We initialize the Client Tasks and make a link to the original Mission Task. - -- 3. We initialize the Cargos. - -- 4. We flag the Mission as Ongoing. - if not Client.ClientAlive then - Client.ClientAlive = true - Client.ClientBriefingShown = false - for TaskNumber, Task in pairs( Mission._Tasks ) do - -- Note that this a deepCopy. Each client must have their own Tasks with own Stages!!! - Client._Tasks[TaskNumber] = routines.utils.deepCopy( Mission._Tasks[TaskNumber] ) - -- Each MissionTask must point to the original Mission. - Client._Tasks[TaskNumber].MissionTask = Mission._Tasks[TaskNumber] - Client._Tasks[TaskNumber].Cargos = Mission._Tasks[TaskNumber].Cargos - Client._Tasks[TaskNumber].LandingZones = Mission._Tasks[TaskNumber].LandingZones - end - - Mission:Ongoing() - end - - - -- For each Client, check for each Task the state and evolve the mission. - -- This flag will indicate if the Task of the Client is Complete. - local TaskComplete = false - - for TaskNumber, Task in pairs( Client._Tasks ) do - - if not Task.Stage then - Task:SetStage( 1 ) - end - - - local TransportTime = timer.getTime() - - if not Task:IsDone() then - - if Task:Goal() then - Task:ShowGoalProgress( Mission, Client ) - end - - --env.info( 'Scheduler: Mission = ' .. Mission.Name .. ' / Client = ' .. Client.ClientName .. ' / Task = ' .. Task.Name .. ' / Stage = ' .. Task.ActiveStage .. ' - ' .. Task.Stage.Name .. ' - ' .. Task.Stage.StageType ) - - -- Action - if Task:StageExecute() then - Task.Stage:Execute( Mission, Client, Task ) - end - - -- Wait until execution is finished - if Task.ExecuteStage == _TransportExecuteStage.EXECUTING then - Task.Stage:Executing( Mission, Client, Task ) - end - - -- Validate completion or reverse to earlier stage - if Task.Time + Task.Stage.WaitTime <= TransportTime then - Task:SetStage( Task.Stage:Validate( Mission, Client, Task ) ) - end - - if Task:IsDone() then - --env.info( 'Scheduler: Mission '.. Mission.Name .. ' Task ' .. Task.Name .. ' Stage ' .. Task.Stage.Name .. ' done. TaskComplete = ' .. string.format ( "%s", TaskComplete and "true" or "false" ) ) - TaskComplete = true -- when a task is not yet completed, a mission cannot be completed - - else - -- break only if this task is not yet done, so that future task are not yet activated. - TaskComplete = false -- when a task is not yet completed, a mission cannot be completed - --env.info( 'Scheduler: Mission "'.. Mission.Name .. '" Task "' .. Task.Name .. '" Stage "' .. Task.Stage.Name .. '" break. TaskComplete = ' .. string.format ( "%s", TaskComplete and "true" or "false" ) ) - break - end - - if TaskComplete then - - if Mission.GoalFunction ~= nil then - Mission.GoalFunction( Mission, Client ) - end - if MISSIONSCHEDULER.Scoring then - MISSIONSCHEDULER.Scoring:_AddMissionTaskScore( Client:GetClientGroupDCSUnit(), Mission.Name, 25 ) - end - --- if not Mission:IsCompleted() then --- end - end - end - end - - local MissionComplete = true - for TaskNumber, Task in pairs( Mission._Tasks ) do - if Task:Goal() then --- Task:ShowGoalProgress( Mission, Client ) - if Task:IsGoalReached() then - else - MissionComplete = false - end - else - MissionComplete = false -- If there is no goal, the mission should never be ended. The goal status will be set somewhere else. - end - end - - if MissionComplete then - Mission:Completed() - if MISSIONSCHEDULER.Scoring then - MISSIONSCHEDULER.Scoring:_AddMissionScore( Mission.Name, 100 ) - end - else - if TaskComplete then - -- Reset for new tasking of active client - Client.ClientAlive = false -- Reset the client tasks. - end - end - - - else - if Client.ClientAlive then - env.info( 'Scheduler: Client "' .. Client.ClientName .. '" is inactive.' ) - Client.ClientAlive = false - - -- This is tricky. If we sanitize Client._Tasks before sanitizing Client._Tasks[TaskNumber].MissionTask, then the original MissionTask will be sanitized, and will be lost within the garbage collector. - -- So first sanitize Client._Tasks[TaskNumber].MissionTask, after that, sanitize only the whole _Tasks structure... - --Client._Tasks[TaskNumber].MissionTask = nil - --Client._Tasks = nil - end - end - end - - -- If all Clients of this Mission are not activated, then the Mission status needs to be put back into Pending status. - -- But only if the Mission was Ongoing. In case the Mission is Completed or Failed, the Mission status may not be changed. In these cases, this will be the last run of this Mission in the Scheduler. - if ClientsAlive == false then - if Mission:IsOngoing() then - -- Mission status back to pending... - Mission:Pending() - end - end - end - - Mission:StatusToClients() - - if Mission:ReportTrigger() then - Mission:ReportToAll() - end - end - - return true -end - ---- Start the MISSIONSCHEDULER. -function MISSIONSCHEDULER.Start() - if MISSIONSCHEDULER ~= nil then - --MISSIONSCHEDULER.SchedulerId = routines.scheduleFunction( MISSIONSCHEDULER.Scheduler, { }, 0, 2 ) - MISSIONSCHEDULER.SchedulerId = SCHEDULER:New( nil, MISSIONSCHEDULER.Scheduler, { }, 0, 2 ) - end -end - ---- Stop the MISSIONSCHEDULER. -function MISSIONSCHEDULER.Stop() - if MISSIONSCHEDULER.SchedulerId then - routines.removeFunction(MISSIONSCHEDULER.SchedulerId) - MISSIONSCHEDULER.SchedulerId = nil - end -end - ---- This is the main MISSION declaration method. Each Mission is like the master or a Mission orchestration between, Clients, Tasks, Stages etc. --- @param Mission is the MISSION object instantiated by @{MISSION:New}. --- @return MISSION --- @usage --- -- Declare a mission. --- Mission = MISSION:New( 'Russia Transport Troops SA-6', --- 'Operational', --- 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', --- 'Russia' ) --- MISSIONSCHEDULER:AddMission( Mission ) -function MISSIONSCHEDULER.AddMission( Mission ) - MISSIONSCHEDULER.Missions[Mission.Name] = Mission - MISSIONSCHEDULER.MissionCount = MISSIONSCHEDULER.MissionCount + 1 - -- Add an overall AI Client for the AI tasks... This AI Client will facilitate the Events in the background for each Task. - --MissionAdd:AddClient( CLIENT:Register( 'AI' ) ) - - return Mission -end - ---- Remove a MISSION from the MISSIONSCHEDULER. --- @param MissionName is the name of the MISSION given at declaration using @{AddMission}. --- @usage --- -- Declare a mission. --- Mission = MISSION:New( 'Russia Transport Troops SA-6', --- 'Operational', --- 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', --- 'Russia' ) --- MISSIONSCHEDULER:AddMission( Mission ) --- --- -- Now remove the Mission. --- MISSIONSCHEDULER:RemoveMission( 'Russia Transport Troops SA-6' ) -function MISSIONSCHEDULER.RemoveMission( MissionName ) - MISSIONSCHEDULER.Missions[MissionName] = nil - MISSIONSCHEDULER.MissionCount = MISSIONSCHEDULER.MissionCount - 1 -end - ---- Find a MISSION within the MISSIONSCHEDULER. --- @param MissionName is the name of the MISSION given at declaration using @{AddMission}. --- @return MISSION --- @usage --- -- Declare a mission. --- Mission = MISSION:New( 'Russia Transport Troops SA-6', --- 'Operational', --- 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', --- 'Russia' ) --- MISSIONSCHEDULER:AddMission( Mission ) --- --- -- Now find the Mission. --- MissionFind = MISSIONSCHEDULER:FindMission( 'Russia Transport Troops SA-6' ) -function MISSIONSCHEDULER.FindMission( MissionName ) - return MISSIONSCHEDULER.Missions[MissionName] -end - --- Internal function used by the MISSIONSCHEDULER menu. -function MISSIONSCHEDULER.ReportMissionsShow( ) - for MissionName, Mission in pairs( MISSIONSCHEDULER.Missions ) do - Mission.MissionReportShow = true - Mission.MissionReportFlash = false - end -end - --- Internal function used by the MISSIONSCHEDULER menu. -function MISSIONSCHEDULER.ReportMissionsFlash( TimeInterval ) - local Count = 0 - for MissionName, Mission in pairs( MISSIONSCHEDULER.Missions ) do - Mission.MissionReportShow = false - Mission.MissionReportFlash = true - Mission.MissionReportTrigger = timer.getTime() + Count * TimeInterval - Mission.MissionTimeInterval = MISSIONSCHEDULER.MissionCount * TimeInterval - env.info( "TimeInterval = " .. Mission.MissionTimeInterval ) - Count = Count + 1 - end -end - --- Internal function used by the MISSIONSCHEDULER menu. -function MISSIONSCHEDULER.ReportMissionsHide( Prm ) - for MissionName, Mission in pairs( MISSIONSCHEDULER.Missions ) do - Mission.MissionReportShow = false - Mission.MissionReportFlash = false - end -end - ---- Enables a MENU option in the communications menu under F10 to control the status of the active missions. --- This function should be called only once when starting the MISSIONSCHEDULER. -function MISSIONSCHEDULER.ReportMenu() - local ReportMenu = SUBMENU:New( 'Status' ) - local ReportMenuShow = COMMANDMENU:New( 'Show Report Missions', ReportMenu, MISSIONSCHEDULER.ReportMissionsShow, 0 ) - local ReportMenuFlash = COMMANDMENU:New('Flash Report Missions', ReportMenu, MISSIONSCHEDULER.ReportMissionsFlash, 120 ) - local ReportMenuHide = COMMANDMENU:New( 'Hide Report Missions', ReportMenu, MISSIONSCHEDULER.ReportMissionsHide, 0 ) -end - ---- Show the remaining mission time. -function MISSIONSCHEDULER:TimeShow() - self.TimeIntervalCount = self.TimeIntervalCount + 1 - if self.TimeIntervalCount >= self.TimeTriggerShow then - local TimeMsg = string.format("%00d", ( self.TimeSeconds / 60 ) - ( timer.getTime() / 60 )) .. ' minutes left until mission reload.' - MESSAGE:New( TimeMsg, "Mission time", self.TimeShow, '/TimeMsg' ):ToAll() - self.TimeIntervalCount = 0 - end -end - -function MISSIONSCHEDULER:Time( TimeSeconds, TimeIntervalShow, TimeShow ) - - self.TimeIntervalCount = 0 - self.TimeSeconds = TimeSeconds - self.TimeIntervalShow = TimeIntervalShow - self.TimeShow = TimeShow -end - ---- Adds a mission scoring to the game. -function MISSIONSCHEDULER:Scoring( Scoring ) - - self.Scoring = Scoring -end - ---- The CLEANUP class keeps an area clean of crashing or colliding airplanes. It also prevents airplanes from firing within this area. --- @module CleanUp --- @author Flightcontrol - -Include.File( "Routines" ) -Include.File( "Base" ) -Include.File( "Mission" ) -Include.File( "Client" ) -Include.File( "Task" ) - ---- The CLEANUP class. --- @type CLEANUP --- @extends Base#BASE -CLEANUP = { - ClassName = "CLEANUP", - ZoneNames = {}, - TimeInterval = 300, - CleanUpList = {}, -} - ---- Creates the main object which is handling the cleaning of the debris within the given Zone Names. --- @param #CLEANUP self --- @param #table ZoneNames Is a table of zone names where the debris should be cleaned. Also a single string can be passed with one zone name. --- @param #number TimeInterval The interval in seconds when the clean activity takes place. The default is 300 seconds, thus every 5 minutes. --- @return #CLEANUP --- @usage --- -- Clean these Zones. --- CleanUpAirports = CLEANUP:New( { 'CLEAN Tbilisi', 'CLEAN Kutaisi' }, 150 ) --- or --- CleanUpTbilisi = CLEANUP:New( 'CLEAN Tbilisi', 150 ) --- CleanUpKutaisi = CLEANUP:New( 'CLEAN Kutaisi', 600 ) -function CLEANUP:New( ZoneNames, TimeInterval ) local self = BASE:Inherit( self, BASE:New() ) - self:F( { ZoneNames, TimeInterval } ) - - if type( ZoneNames ) == 'table' then - self.ZoneNames = ZoneNames - else - self.ZoneNames = { ZoneNames } - end - if TimeInterval then - self.TimeInterval = TimeInterval - end - - _EVENTDISPATCHER:OnBirth( self._OnEventBirth, self ) - - --self.CleanUpScheduler = routines.scheduleFunction( self._CleanUpScheduler, { self }, timer.getTime() + 1, TimeInterval ) - self.CleanUpScheduler = SCHEDULER:New( self, self._CleanUpScheduler, {}, 1, TimeInterval ) - - return self -end - - ---- Destroys a group from the simulator, but checks first if it is still existing! --- @param #CLEANUP self --- @param DCSGroup#Group GroupObject The object to be destroyed. --- @param #string CleanUpGroupName The groupname... -function CLEANUP:_DestroyGroup( GroupObject, CleanUpGroupName ) - self:F( { GroupObject, CleanUpGroupName } ) - - if GroupObject then -- and GroupObject:isExist() then - --MESSAGE:New( "Destroy Group " .. CleanUpGroupName, CleanUpGroupName, 1, CleanUpGroupName ):ToAll() - trigger.action.deactivateGroup(GroupObject) - self:T( { "GroupObject Destroyed", GroupObject } ) - end -end - ---- Destroys a @{DCSUnit#Unit} from the simulator, but checks first if it is still existing! --- @param #CLEANUP self --- @param DCSUnit#Unit CleanUpUnit The object to be destroyed. --- @param #string CleanUpUnitName The Unit name ... -function CLEANUP:_DestroyUnit( CleanUpUnit, CleanUpUnitName ) - self:F( { CleanUpUnit, CleanUpUnitName } ) - - if CleanUpUnit then - --MESSAGE:New( "Destroy " .. CleanUpUnitName, CleanUpUnitName, 1, CleanUpUnitName ):ToAll() - local CleanUpGroup = Unit.getGroup(CleanUpUnit) - -- TODO Client bug in 1.5.3 - if CleanUpGroup and CleanUpGroup:isExist() then - local CleanUpGroupUnits = CleanUpGroup:getUnits() - if #CleanUpGroupUnits == 1 then - local CleanUpGroupName = CleanUpGroup:getName() - --self:CreateEventCrash( timer.getTime(), CleanUpUnit ) - CleanUpGroup:destroy() - self:T( { "Destroyed Group:", CleanUpGroupName } ) - else - CleanUpUnit:destroy() - self:T( { "Destroyed Unit:", CleanUpUnitName } ) - end - self.CleanUpList[CleanUpUnitName] = nil -- Cleaning from the list - CleanUpUnit = nil - end - end -end - --- TODO check DCSTypes#Weapon ---- Destroys a missile from the simulator, but checks first if it is still existing! --- @param #CLEANUP self --- @param DCSTypes#Weapon MissileObject -function CLEANUP:_DestroyMissile( MissileObject ) - self:F( { MissileObject } ) - - if MissileObject and MissileObject:isExist() then - MissileObject:destroy() - self:T( "MissileObject Destroyed") - end -end - -function CLEANUP:_OnEventBirth( Event ) - self:F( { Event } ) - - self.CleanUpList[Event.IniDCSUnitName] = {} - self.CleanUpList[Event.IniDCSUnitName].CleanUpUnit = Event.IniDCSUnit - self.CleanUpList[Event.IniDCSUnitName].CleanUpGroup = Event.IniDCSGroup - self.CleanUpList[Event.IniDCSUnitName].CleanUpGroupName = Event.IniDCSGroupName - self.CleanUpList[Event.IniDCSUnitName].CleanUpUnitName = Event.IniDCSUnitName - - _EVENTDISPATCHER:OnEngineShutDownForUnit( Event.IniDCSUnitName, self._EventAddForCleanUp, self ) - _EVENTDISPATCHER:OnEngineStartUpForUnit( Event.IniDCSUnitName, self._EventAddForCleanUp, self ) - _EVENTDISPATCHER:OnHitForUnit( Event.IniDCSUnitName, self._EventAddForCleanUp, self ) - _EVENTDISPATCHER:OnPilotDeadForUnit( Event.IniDCSUnitName, self._EventCrash, self ) - _EVENTDISPATCHER:OnDeadForUnit( Event.IniDCSUnitName, self._EventCrash, self ) - _EVENTDISPATCHER:OnCrashForUnit( Event.IniDCSUnitName, self._EventCrash, self ) - _EVENTDISPATCHER:OnShotForUnit( Event.IniDCSUnitName, self._EventShot, self ) - - --self:AddEvent( world.event.S_EVENT_ENGINE_SHUTDOWN, self._EventAddForCleanUp ) - --self:AddEvent( world.event.S_EVENT_ENGINE_STARTUP, self._EventAddForCleanUp ) --- self:AddEvent( world.event.S_EVENT_HIT, self._EventAddForCleanUp ) -- , self._EventHitCleanUp ) --- self:AddEvent( world.event.S_EVENT_CRASH, self._EventCrash ) -- , self._EventHitCleanUp ) --- --self:AddEvent( world.event.S_EVENT_DEAD, self._EventCrash ) --- self:AddEvent( world.event.S_EVENT_SHOT, self._EventShot ) --- --- self:EnableEvents() - - -end - ---- Detects if a crash event occurs. --- Crashed units go into a CleanUpList for removal. --- @param #CLEANUP self --- @param DCSTypes#Event event -function CLEANUP:_EventCrash( Event ) - self:F( { Event } ) - - --TODO: This stuff is not working due to a DCS bug. Burning units cannot be destroyed. - --MESSAGE:New( "Crash ", "Crash", 10, "Crash" ):ToAll() - -- self:T("before getGroup") - -- local _grp = Unit.getGroup(event.initiator)-- Identify the group that fired - -- self:T("after getGroup") - -- _grp:destroy() - -- self:T("after deactivateGroup") - -- event.initiator:destroy() - - self.CleanUpList[Event.IniDCSUnitName] = {} - self.CleanUpList[Event.IniDCSUnitName].CleanUpUnit = Event.IniDCSUnit - self.CleanUpList[Event.IniDCSUnitName].CleanUpGroup = Event.IniDCSGroup - self.CleanUpList[Event.IniDCSUnitName].CleanUpGroupName = Event.IniDCSGroupName - self.CleanUpList[Event.IniDCSUnitName].CleanUpUnitName = Event.IniDCSUnitName - -end - ---- Detects if a unit shoots a missile. --- If this occurs within one of the zones, then the weapon used must be destroyed. --- @param #CLEANUP self --- @param DCSTypes#Event event -function CLEANUP:_EventShot( Event ) - self:F( { Event } ) - - -- Test if the missile was fired within one of the CLEANUP.ZoneNames. - local CurrentLandingZoneID = 0 - CurrentLandingZoneID = routines.IsUnitInZones( Event.IniDCSUnit, self.ZoneNames ) - if ( CurrentLandingZoneID ) then - -- Okay, the missile was fired within the CLEANUP.ZoneNames, destroy the fired weapon. - --_SEADmissile:destroy() - --routines.scheduleFunction( CLEANUP._DestroyMissile, { self, Event.Weapon }, timer.getTime() + 0.1) - SCHEDULER:New( self, CLEANUP._DestroyMissile, { Event.Weapon }, 0.1 ) - end -end - - ---- Detects if the Unit has an S_EVENT_HIT within the given ZoneNames. If this is the case, destroy the unit. --- @param #CLEANUP self --- @param DCSTypes#Event event -function CLEANUP:_EventHitCleanUp( Event ) - self:F( { Event } ) - - if Event.IniDCSUnit then - if routines.IsUnitInZones( Event.IniDCSUnit, self.ZoneNames ) ~= nil then - self:T( { "Life: ", Event.IniDCSUnitName, ' = ', Event.IniDCSUnit:getLife(), "/", Event.IniDCSUnit:getLife0() } ) - if Event.IniDCSUnit:getLife() < Event.IniDCSUnit:getLife0() then - self:T( "CleanUp: Destroy: " .. Event.IniDCSUnitName ) - --routines.scheduleFunction( CLEANUP._DestroyUnit, { self, Event.IniDCSUnit }, timer.getTime() + 0.1) - SCHEDULER:New( self, CLEANUP._DestroyUnit, { Event.IniDCSUnit }, 0.1 ) - end - end - end - - if Event.TgtDCSUnit then - if routines.IsUnitInZones( Event.TgtDCSUnit, self.ZoneNames ) ~= nil then - self:T( { "Life: ", Event.TgtDCSUnitName, ' = ', Event.TgtDCSUnit:getLife(), "/", Event.TgtDCSUnit:getLife0() } ) - if Event.TgtDCSUnit:getLife() < Event.TgtDCSUnit:getLife0() then - self:T( "CleanUp: Destroy: " .. Event.TgtDCSUnitName ) - --routines.scheduleFunction( CLEANUP._DestroyUnit, { self, Event.TgtDCSUnit }, timer.getTime() + 0.1 ) - SCHEDULER:New( self, CLEANUP._DestroyUnit, { Event.TgtDCSUnit }, 0.1 ) - end - end - end -end - ---- Add the @{DCSUnit#Unit} to the CleanUpList for CleanUp. -function CLEANUP:_AddForCleanUp( CleanUpUnit, CleanUpUnitName ) - self:F( { CleanUpUnit, CleanUpUnitName } ) - - self.CleanUpList[CleanUpUnitName] = {} - self.CleanUpList[CleanUpUnitName].CleanUpUnit = CleanUpUnit - self.CleanUpList[CleanUpUnitName].CleanUpUnitName = CleanUpUnitName - self.CleanUpList[CleanUpUnitName].CleanUpGroup = Unit.getGroup(CleanUpUnit) - self.CleanUpList[CleanUpUnitName].CleanUpGroupName = Unit.getGroup(CleanUpUnit):getName() - self.CleanUpList[CleanUpUnitName].CleanUpTime = timer.getTime() - self.CleanUpList[CleanUpUnitName].CleanUpMoved = false - - self:T( { "CleanUp: Add to CleanUpList: ", Unit.getGroup(CleanUpUnit):getName(), CleanUpUnitName } ) - -end - ---- Detects if the Unit has an S_EVENT_ENGINE_SHUTDOWN or an S_EVENT_HIT within the given ZoneNames. If this is the case, add the Group to the CLEANUP List. --- @param #CLEANUP self --- @param DCSTypes#Event event -function CLEANUP:_EventAddForCleanUp( Event ) - - if Event.IniDCSUnit then - if self.CleanUpList[Event.IniDCSUnitName] == nil then - if routines.IsUnitInZones( Event.IniDCSUnit, self.ZoneNames ) ~= nil then - self:_AddForCleanUp( Event.IniDCSUnit, Event.IniDCSUnitName ) - end - end - end - - if Event.TgtDCSUnit then - if self.CleanUpList[Event.TgtDCSUnitName] == nil then - if routines.IsUnitInZones( Event.TgtDCSUnit, self.ZoneNames ) ~= nil then - self:_AddForCleanUp( Event.TgtDCSUnit, Event.TgtDCSUnitName ) - end - end - end - -end - -local CleanUpSurfaceTypeText = { - "LAND", - "SHALLOW_WATER", - "WATER", - "ROAD", - "RUNWAY" - } - ---- At the defined time interval, CleanUp the Groups within the CleanUpList. --- @param #CLEANUP self -function CLEANUP:_CleanUpScheduler() - self:F( { "CleanUp Scheduler" } ) - - local CleanUpCount = 0 - for CleanUpUnitName, UnitData in pairs( self.CleanUpList ) do - CleanUpCount = CleanUpCount + 1 - - self:T( { CleanUpUnitName, UnitData } ) - local CleanUpUnit = Unit.getByName(UnitData.CleanUpUnitName) - local CleanUpGroupName = UnitData.CleanUpGroupName - local CleanUpUnitName = UnitData.CleanUpUnitName - if CleanUpUnit then - self:T( { "CleanUp Scheduler", "Checking:", CleanUpUnitName } ) - if _DATABASE:GetStatusGroup( CleanUpGroupName ) ~= "ReSpawn" then - local CleanUpUnitVec3 = CleanUpUnit:getPoint() - --self:T( CleanUpUnitVec3 ) - local CleanUpUnitVec2 = {} - CleanUpUnitVec2.x = CleanUpUnitVec3.x - CleanUpUnitVec2.y = CleanUpUnitVec3.z - --self:T( CleanUpUnitVec2 ) - local CleanUpSurfaceType = land.getSurfaceType(CleanUpUnitVec2) - --self:T( CleanUpSurfaceType ) - --MESSAGE:New( "Surface " .. CleanUpUnitName .. " = " .. CleanUpSurfaceTypeText[CleanUpSurfaceType], CleanUpUnitName, 10, CleanUpUnitName ):ToAll() - - 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 - --MESSAGE:New( "Moved " .. CleanUpUnitName, CleanUpUnitName, 10, CleanUpUnitName ):ToAll() - 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 - ---- Dynamic spawning of groups (and units). --- --- @{#SPAWN} class --- =============== --- The @{#SPAWN} class allows to spawn dynamically new groups, based on pre-defined initialization settings, modifying the behaviour when groups are spawned. --- For each group to be spawned, within the mission editor, a group has to be created with the "late activation flag" set. We call this group the *"Spawn Template"* of the SPAWN object. --- A reference to this Spawn Template needs to be provided when constructing the SPAWN object, by indicating the name of the group within the mission editor in the constructor methods. --- --- Within the SPAWN object, there is an internal index that keeps track of which group from the internal group list was spawned. --- When new groups get spawned by using the SPAWN functions (see below), it will be validated whether the Limits (@{#SPAWN.Limit}) of the SPAWN object are not reached. --- When all is valid, a new group will be created by the spawning methods, and the internal index will be increased with 1. --- --- Regarding the name of new spawned groups, a _SpawnPrefix_ will be assigned for each new group created. --- If you want to have the Spawn Template name to be used as the _SpawnPrefix_ name, use the @{#SPAWN.New} constructor. --- However, when the @{#SPAWN.NewWithAlias} constructor was used, the Alias name will define the _SpawnPrefix_ name. --- Groups will follow the following naming structure when spawned at run-time: --- --- 1. Spawned groups will have the name _SpawnPrefix_#ggg, where ggg is a counter from 0 to 999. --- 2. Spawned units will have the name _SpawnPrefix_#ggg-uu, where uu is a counter from 0 to 99 for each new spawned unit belonging to the group. --- --- Some additional notes that need to be remembered: --- --- * Templates are actually groups defined within the mission editor, with the flag "Late Activation" set. As such, these groups are never used within the mission, but are used by the @{#SPAWN} module. --- * It is important to defined BEFORE you spawn new groups, a proper initialization of the SPAWN instance is done with the options you want to use. --- * When designing a mission, NEVER name groups using a "#" within the name of the group Spawn Template(s), or the SPAWN module logic won't work anymore. --- --- SPAWN construction methods: --- =========================== --- Create a new SPAWN object with the @{#SPAWN.New} or the @{#SPAWN.NewWithAlias} methods: --- --- * @{#SPAWN.New}: Creates a new SPAWN object taking the name of the group that functions as the Template. --- --- It is important to understand how the SPAWN class works internally. The SPAWN object created will contain internally a list of groups that will be spawned and that are already spawned. --- The initialization functions will modify this list of groups so that when a group gets spawned, ALL information is already prepared when spawning. This is done for performance reasons. --- So in principle, the group list will contain all parameters and configurations after initialization, and when groups get actually spawned, this spawning can be done quickly and efficient. --- --- SPAWN initialization methods: --- ============================= --- A spawn object will behave differently based on the usage of initialization methods: --- --- * @{#SPAWN.Limit}: Limits the amount of groups that can be alive at the same time and that can be dynamically spawned. --- * @{#SPAWN.RandomizeRoute}: Randomize the routes of spawned groups. --- * @{#SPAWN.RandomizeTemplate}: Randomize the group templates so that when a new group is spawned, a random group template is selected from one of the templates defined. --- * @{#SPAWN.Uncontrolled}: Spawn plane groups uncontrolled. --- * @{#SPAWN.Array}: Make groups visible before they are actually activated, and order these groups like a batallion in an array. --- * @{#SPAWN.InitRepeat}: Re-spawn groups when they land at the home base. Similar functions are @{#SPAWN.InitRepeatOnLanding} and @{#SPAWN.InitRepeatOnEngineShutDown}. --- --- SPAWN spawning methods: --- ======================= --- Groups can be spawned at different times and methods: --- --- * @{#SPAWN.Spawn}: Spawn one new group based on the last spawned index. --- * @{#SPAWN.ReSpawn}: Re-spawn a group based on a given index. --- * @{#SPAWN.SpawnScheduled}: Spawn groups at scheduled but randomized intervals. You can use @{#SPAWN.SpawnScheduleStart} and @{#SPAWN.SpawnScheduleStop} to start and stop the schedule respectively. --- * @{#SPAWN.SpawnFromUnit}: Spawn a new group taking the position of a @{UNIT}. --- * @{#SPAWN.SpawnInZone}: Spawn a new group in a @{ZONE}. --- --- Note that @{#SPAWN.Spawn} and @{#SPAWN.ReSpawn} return a @{GROUP#GROUP.New} object, that contains a reference to the DCSGroup object. --- You can use the @{GROUP} object to do further actions with the DCSGroup. --- --- SPAWN object cleaning: --- ========================= --- Sometimes, it will occur during a mission run-time, that ground or especially air objects get damaged, and will while being damged stop their activities, while remaining alive. --- In such cases, the SPAWN object will just sit there and wait until that group gets destroyed, but most of the time it won't, --- and it may occur that no new groups are or can be spawned as limits are reached. --- To prevent this, a @{#SPAWN.CleanUp} initialization method has been defined that will silently monitor the status of each spawned group. --- Once a group has a velocity = 0, and has been waiting for a defined interval, that group will be cleaned or removed from run-time. --- There is a catch however :-) If a damaged group has returned to an airbase within the coalition, that group will not be considered as "lost"... --- In such a case, when the inactive group is cleaned, a new group will Re-spawned automatically. --- This models AI that has succesfully returned to their airbase, to restart their combat activities. --- Check the @{#SPAWN.CleanUp} for further info. --- --- ==== --- @module Spawn --- @author FlightControl - -Include.File( "Routines" ) -Include.File( "Base" ) -Include.File( "Database" ) -Include.File( "Group" ) -Include.File( "Zone" ) -Include.File( "Event" ) -Include.File( "Scheduler" ) - ---- SPAWN Class --- @type SPAWN --- @extends Base#BASE --- @field ClassName --- @field #string SpawnTemplatePrefix --- @field #string SpawnAliasPrefix -SPAWN = { - ClassName = "SPAWN", - SpawnTemplatePrefix = nil, - SpawnAliasPrefix = nil, -} - - - ---- Creates the main object to spawn a GROUP defined in the DCS ME. --- @param #SPAWN self --- @param #string SpawnTemplatePrefix is the name of the Group in the ME that defines the Template. Each new group will have the name starting with SpawnTemplatePrefix. --- @return #SPAWN --- @usage --- -- NATO helicopters engaging in the battle field. --- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ) --- @usage local Plane = SPAWN:New( "Plane" ) -- Creates a new local variable that can initiate new planes with the name "Plane#ddd" using the template "Plane" as defined within the ME. -function SPAWN:New( SpawnTemplatePrefix ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( { SpawnTemplatePrefix } ) - - local TemplateGroup = Group.getByName( SpawnTemplatePrefix ) - if TemplateGroup then - self.SpawnTemplatePrefix = SpawnTemplatePrefix - self.SpawnIndex = 0 - self.SpawnCount = 0 -- The internal counter of the amount of spawning the has happened since SpawnStart. - self.AliveUnits = 0 -- Contains the counter how many units are currently alive - self.SpawnIsScheduled = false -- Reflects if the spawning for this SpawnTemplatePrefix is going to be scheduled or not. - self.SpawnTemplate = self._GetTemplate( self, SpawnTemplatePrefix ) -- Contains the template structure for a Group Spawn from the Mission Editor. Note that this group must have lateActivation always on!!! - self.Repeat = false -- Don't repeat the group from Take-Off till Landing and back Take-Off by ReSpawning. - self.UnControlled = false -- When working in UnControlled mode, all planes are Spawned in UnControlled mode before the scheduler starts. - self.SpawnMaxUnitsAlive = 0 -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. - self.SpawnMaxGroups = 0 -- The maximum amount of groups that can be spawned. - self.SpawnRandomize = false -- Sets the randomization flag of new Spawned units to false. - self.SpawnVisible = false -- Flag that indicates if all the Groups of the SpawnGroup need to be visible when Spawned. - - self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. - else - error( "SPAWN:New: There is no group declared in the mission editor with SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" ) - end - - return self -end - ---- Creates a new SPAWN instance to create new groups based on the defined template and using a new alias for each new group. --- @param #SPAWN self --- @param #string SpawnTemplatePrefix is the name of the Group in the ME that defines the Template. --- @param #string SpawnAliasPrefix is the name that will be given to the Group at runtime. --- @return #SPAWN --- @usage --- -- NATO helicopters engaging in the battle field. --- Spawn_BE_KA50 = SPAWN:NewWithAlias( 'BE KA-50@RAMP-Ground Defense', 'Helicopter Attacking a City' ) --- @usage local PlaneWithAlias = SPAWN:NewWithAlias( "Plane", "Bomber" ) -- Creates a new local variable that can instantiate new planes with the name "Bomber#ddd" using the template "Plane" as defined within the ME. -function SPAWN:NewWithAlias( SpawnTemplatePrefix, SpawnAliasPrefix ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( { SpawnTemplatePrefix, SpawnAliasPrefix } ) - - local TemplateGroup = Group.getByName( SpawnTemplatePrefix ) - if TemplateGroup then - self.SpawnTemplatePrefix = SpawnTemplatePrefix - self.SpawnAliasPrefix = SpawnAliasPrefix - self.SpawnIndex = 0 - self.SpawnCount = 0 -- The internal counter of the amount of spawning the has happened since SpawnStart. - self.AliveUnits = 0 -- Contains the counter how many units are currently alive - self.SpawnIsScheduled = false -- Reflects if the spawning for this SpawnTemplatePrefix is going to be scheduled or not. - self.SpawnTemplate = self._GetTemplate( self, SpawnTemplatePrefix ) -- Contains the template structure for a Group Spawn from the Mission Editor. Note that this group must have lateActivation always on!!! - self.Repeat = false -- Don't repeat the group from Take-Off till Landing and back Take-Off by ReSpawning. - self.UnControlled = false -- When working in UnControlled mode, all planes are Spawned in UnControlled mode before the scheduler starts. - self.SpawnMaxUnitsAlive = 0 -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. - self.SpawnMaxGroups = 0 -- The maximum amount of groups that can be spawned. - self.SpawnRandomize = false -- Sets the randomization flag of new Spawned units to false. - self.SpawnVisible = false -- Flag that indicates if all the Groups of the SpawnGroup need to be visible when Spawned. - - self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. - else - error( "SPAWN:New: There is no group declared in the mission editor with SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" ) - end - - return self -end - - ---- Limits the Maximum amount of Units that can be alive at the same time, and the maximum amount of groups that can be spawned. --- Note that this method is exceptionally important to balance the performance of the mission. Depending on the machine etc, a mission can only process a maximum amount of units. --- If the time interval must be short, but there should not be more Units or Groups alive than a maximum amount of units, then this function should be used... --- When a @{#SPAWN.New} is executed and the limit of the amount of units alive is reached, then no new spawn will happen of the group, until some of these units of the spawn object will be destroyed. --- @param #SPAWN self --- @param #number SpawnMaxUnitsAlive The maximum amount of units that can be alive at runtime. --- @param #number SpawnMaxGroups The maximum amount of groups that can be spawned. When the limit is reached, then no more actual spawns will happen of the group. --- This parameter is useful to define a maximum amount of airplanes, ground troops, helicopters, ships etc within a supply area. --- This parameter accepts the value 0, which defines that there are no maximum group limits, but there are limits on the maximum of units that can be alive at the same time. --- @return #SPAWN self --- @usage --- -- NATO helicopters engaging in the battle field. --- -- This helicopter group consists of one Unit. So, this group will SPAWN maximum 2 groups simultaneously within the DCSRTE. --- -- There will be maximum 24 groups spawned during the whole mission lifetime. --- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):Limit( 2, 24 ) -function SPAWN:Limit( SpawnMaxUnitsAlive, SpawnMaxGroups ) - self:F( { self.SpawnTemplatePrefix, SpawnMaxUnitsAlive, SpawnMaxGroups } ) - - self.SpawnMaxUnitsAlive = SpawnMaxUnitsAlive -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. - self.SpawnMaxGroups = SpawnMaxGroups -- The maximum amount of groups that can be spawned. - - for SpawnGroupID = 1, self.SpawnMaxGroups do - self:_InitializeSpawnGroups( SpawnGroupID ) - end - - return self -end - - ---- Randomizes the defined route of the SpawnTemplatePrefix group in the ME. This is very useful to define extra variation of the behaviour of groups. --- @param #SPAWN self --- @param #number SpawnStartPoint is the waypoint where the randomization begins. --- Note that the StartPoint = 0 equaling the point where the group is spawned. --- @param #number SpawnEndPoint is the waypoint where the randomization ends counting backwards. --- This parameter is useful to avoid randomization to end at a waypoint earlier than the last waypoint on the route. --- @param #number SpawnRadius is the radius in meters in which the randomization of the new waypoints, with the original waypoint of the original template located in the middle ... --- @return #SPAWN --- @usage --- -- NATO helicopters engaging in the battle field. --- -- The KA-50 has waypoints Start point ( =0 or SP ), 1, 2, 3, 4, End point (= 5 or DP). --- -- Waypoints 2 and 3 will only be randomized. The others will remain on their original position with each new spawn of the helicopter. --- -- The randomization of waypoint 2 and 3 will take place within a radius of 2000 meters. --- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):RandomizeRoute( 2, 2, 2000 ) -function SPAWN:RandomizeRoute( SpawnStartPoint, SpawnEndPoint, SpawnRadius ) - self:F( { self.SpawnTemplatePrefix, SpawnStartPoint, SpawnEndPoint, SpawnRadius } ) - - self.SpawnRandomizeRoute = true - self.SpawnRandomizeRouteStartPoint = SpawnStartPoint - self.SpawnRandomizeRouteEndPoint = SpawnEndPoint - self.SpawnRandomizeRouteRadius = SpawnRadius - - for GroupID = 1, self.SpawnMaxGroups do - self:_RandomizeRoute( GroupID ) - end - - return self -end - - ---- This function is rather complicated to understand. But I'll try to explain. --- This function becomes useful when you need to spawn groups with random templates of groups defined within the mission editor, --- but they will all follow the same Template route and have the same prefix name. --- In other words, this method randomizes between a defined set of groups the template to be used for each new spawn of a group. --- @param #SPAWN self --- @param #string SpawnTemplatePrefixTable A table with the names of the groups defined within the mission editor, from which one will be choosen when a new group will be spawned. --- @return #SPAWN --- @usage --- -- NATO Tank Platoons invading Gori. --- -- Choose between 13 different 'US Tank Platoon' configurations for each new SPAWN the Group to be spawned for the --- -- 'US Tank Platoon Left', 'US Tank Platoon Middle' and 'US Tank Platoon Right' SpawnTemplatePrefixes. --- -- Each new SPAWN will randomize the route, with a defined time interval of 200 seconds with 40% time variation (randomization) and --- -- with a limit set of maximum 12 Units alive simulteneously and 150 Groups to be spawned during the whole mission. --- Spawn_US_Platoon = { 'US Tank Platoon 1', 'US Tank Platoon 2', 'US Tank Platoon 3', 'US Tank Platoon 4', 'US Tank Platoon 5', --- 'US Tank Platoon 6', 'US Tank Platoon 7', 'US Tank Platoon 8', 'US Tank Platoon 9', 'US Tank Platoon 10', --- 'US Tank Platoon 11', 'US Tank Platoon 12', 'US Tank Platoon 13' } --- Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):Limit( 12, 150 ):Schedule( 200, 0.4 ):RandomizeTemplate( Spawn_US_Platoon ):RandomizeRoute( 3, 3, 2000 ) --- Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):Limit( 12, 150 ):Schedule( 200, 0.4 ):RandomizeTemplate( Spawn_US_Platoon ):RandomizeRoute( 3, 3, 2000 ) --- Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):Limit( 12, 150 ):Schedule( 200, 0.4 ):RandomizeTemplate( Spawn_US_Platoon ):RandomizeRoute( 3, 3, 2000 ) -function SPAWN:RandomizeTemplate( SpawnTemplatePrefixTable ) - self:F( { self.SpawnTemplatePrefix, SpawnTemplatePrefixTable } ) - - self.SpawnTemplatePrefixTable = SpawnTemplatePrefixTable - self.SpawnRandomizeTemplate = true - - for SpawnGroupID = 1, self.SpawnMaxGroups do - self:_RandomizeTemplate( SpawnGroupID ) - end - - return self -end - - - - - ---- For planes and helicopters, when these groups go home and land on their home airbases and farps, they normally would taxi to the parking spot, shut-down their engines and wait forever until the Group is removed by the runtime environment. --- This function is used to re-spawn automatically (so no extra call is needed anymore) the same group after it has landed. --- This will enable a spawned group to be re-spawned after it lands, until it is destroyed... --- Note: When the group is respawned, it will re-spawn from the original airbase where it took off. --- So ensure that the routes for groups that respawn, always return to the original airbase, or players may get confused ... --- @param #SPAWN self --- @return #SPAWN self --- @usage --- -- RU Su-34 - AI Ship Attack --- -- Re-SPAWN the Group(s) after each landing and Engine Shut-Down automatically. --- SpawnRU_SU34 = SPAWN:New( 'TF1 RU Su-34 Krymsk@AI - Attack Ships' ):Schedule( 2, 3, 1800, 0.4 ):SpawnUncontrolled():RandomizeRoute( 1, 1, 3000 ):RepeatOnEngineShutDown() -function SPAWN:InitRepeat() - self:F( { self.SpawnTemplatePrefix, self.SpawnIndex } ) - - self.Repeat = true - self.RepeatOnEngineShutDown = false - self.RepeatOnLanding = true - - return self -end - ---- Respawn group after landing. --- @param #SPAWN self --- @return #SPAWN self -function SPAWN:InitRepeatOnLanding() - self:F( { self.SpawnTemplatePrefix } ) - - self:InitRepeat() - self.RepeatOnEngineShutDown = false - self.RepeatOnLanding = true - - return self -end - - ---- Respawn after landing when its engines have shut down. --- @param #SPAWN self --- @return #SPAWN self -function SPAWN:InitRepeatOnEngineShutDown() - self:F( { self.SpawnTemplatePrefix } ) - - self:InitRepeat() - self.RepeatOnEngineShutDown = true - self.RepeatOnLanding = false - - return self -end - - ---- CleanUp groups when they are still alive, but inactive. --- When groups are still alive and have become inactive due to damage and are unable to contribute anything, then this group will be removed at defined intervals in seconds. --- @param #SPAWN self --- @param #string SpawnCleanUpInterval The interval to check for inactive groups within seconds. --- @return #SPAWN self --- @usage Spawn_Helicopter:CleanUp( 20 ) -- CleanUp the spawning of the helicopters every 20 seconds when they become inactive. -function SPAWN:CleanUp( SpawnCleanUpInterval ) - self:F( { self.SpawnTemplatePrefix, SpawnCleanUpInterval } ) - - self.SpawnCleanUpInterval = SpawnCleanUpInterval - self.SpawnCleanUpTimeStamps = {} - --self.CleanUpFunction = routines.scheduleFunction( self._SpawnCleanUpScheduler, { self }, timer.getTime() + 1, SpawnCleanUpInterval ) - self.CleanUpScheduler = SCHEDULER:New( self, self._SpawnCleanUpScheduler, {}, 1, SpawnCleanUpInterval, 0.2 ) - return self -end - - - ---- Makes the groups visible before start (like a batallion). --- The method will take the position of the group as the first position in the array. --- @param #SPAWN self --- @param #number SpawnAngle The angle in degrees how the groups and each unit of the group will be positioned. --- @param #number SpawnWidth The amount of Groups that will be positioned on the X axis. --- @param #number SpawnDeltaX The space between each Group on the X-axis. --- @param #number SpawnDeltaY The space between each Group on the Y-axis. --- @return #SPAWN self --- @usage --- -- Define an array of Groups. --- Spawn_BE_Ground = SPAWN:New( 'BE Ground' ):Limit( 2, 24 ):Visible( 90, "Diamond", 10, 100, 50 ) -function SPAWN:Array( SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY ) - self:F( { self.SpawnTemplatePrefix, SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY } ) - - self.SpawnVisible = true -- When the first Spawn executes, all the Groups need to be made visible before start. - - local SpawnX = 0 - local SpawnY = 0 - local SpawnXIndex = 0 - local SpawnYIndex = 0 - - for SpawnGroupID = 1, self.SpawnMaxGroups do - self:T( { SpawnX, SpawnY, SpawnXIndex, SpawnYIndex } ) - - self.SpawnGroups[SpawnGroupID].Visible = true - self.SpawnGroups[SpawnGroupID].Spawned = false - - SpawnXIndex = SpawnXIndex + 1 - if SpawnWidth and SpawnWidth ~= 0 then - if SpawnXIndex >= SpawnWidth then - SpawnXIndex = 0 - SpawnYIndex = SpawnYIndex + 1 - end - end - - local SpawnRootX = self.SpawnGroups[SpawnGroupID].SpawnTemplate.x - local SpawnRootY = self.SpawnGroups[SpawnGroupID].SpawnTemplate.y - - self:_TranslateRotate( SpawnGroupID, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle ) - - self.SpawnGroups[SpawnGroupID].SpawnTemplate.lateActivation = true - self.SpawnGroups[SpawnGroupID].SpawnTemplate.visible = true - - self.SpawnGroups[SpawnGroupID].Visible = true - - _EVENTDISPATCHER:OnBirthForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnBirth, self ) - _EVENTDISPATCHER:OnCrashForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnDeadOrCrash, self ) - _EVENTDISPATCHER:OnDeadForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnDeadOrCrash, self ) - - if self.Repeat then - _EVENTDISPATCHER:OnTakeOffForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnTakeOff, self ) - _EVENTDISPATCHER:OnLandForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnLand, self ) - end - if self.RepeatOnEngineShutDown then - _EVENTDISPATCHER:OnEngineShutDownForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnEngineShutDown, self ) - end - - self.SpawnGroups[SpawnGroupID].Group = _DATABASE:Spawn( self.SpawnGroups[SpawnGroupID].SpawnTemplate ) - - SpawnX = SpawnXIndex * SpawnDeltaX - SpawnY = SpawnYIndex * SpawnDeltaY - end - - return self -end - - - ---- Will spawn a group based on the internal index. --- Note: Uses @{DATABASE} module defined in MOOSE. --- @param #SPAWN self --- @return Group#GROUP The group that was spawned. You can use this group for further actions. -function SPAWN:Spawn() - self:F( { self.SpawnTemplatePrefix, self.SpawnIndex } ) - - return self:SpawnWithIndex( self.SpawnIndex + 1 ) -end - ---- Will re-spawn a group based on a given index. --- Note: Uses @{DATABASE} module defined in MOOSE. --- @param #SPAWN self --- @param #string SpawnIndex The index of the group to be spawned. --- @return Group#GROUP The group that was spawned. You can use this group for further actions. -function SPAWN:ReSpawn( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) - - if not SpawnIndex then - SpawnIndex = 1 - end - --- TODO: This logic makes DCS crash and i don't know why (yet). - local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) - if SpawnGroup then - local SpawnDCSGroup = SpawnGroup:GetDCSGroup() - if SpawnDCSGroup then - SpawnGroup:Destroy() - end - end - - return self:SpawnWithIndex( SpawnIndex ) -end - ---- Will spawn a group with a specified index number. --- Uses @{DATABASE} global object defined in MOOSE. --- @param #SPAWN self --- @return Group#GROUP The group that was spawned. You can use this group for further actions. -function SPAWN:SpawnWithIndex( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnMaxGroups } ) - - if self:_GetSpawnIndex( SpawnIndex ) then - - if self.SpawnGroups[self.SpawnIndex].Visible then - self.SpawnGroups[self.SpawnIndex].Group:Activate() - else - self:T( self.SpawnGroups[self.SpawnIndex].SpawnTemplate ) - _EVENTDISPATCHER:OnBirthForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnBirth, self ) - _EVENTDISPATCHER:OnCrashForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnDeadOrCrash, self ) - _EVENTDISPATCHER:OnDeadForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnDeadOrCrash, self ) - - if self.Repeat then - _EVENTDISPATCHER:OnTakeOffForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnTakeOff, self ) - _EVENTDISPATCHER:OnLandForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnLand, self ) - end - if self.RepeatOnEngineShutDown then - _EVENTDISPATCHER:OnEngineShutDownForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnEngineShutDown, self ) - end - - self:T( self.SpawnGroups[self.SpawnIndex].SpawnTemplate ) - - self.SpawnGroups[self.SpawnIndex].Group = _DATABASE:Spawn( self.SpawnGroups[self.SpawnIndex].SpawnTemplate ) - - -- If there is a SpawnFunction hook defined, call it. - if self.SpawnFunctionHook then - self.SpawnFunctionHook( self.SpawnGroups[self.SpawnIndex].Group, unpack( self.SpawnFunctionArguments ) ) - end - -- TODO: Need to fix this by putting an "R" in the name of the group when the group repeats. - --if self.Repeat then - -- _DATABASE:SetStatusGroup( SpawnTemplate.name, "ReSpawn" ) - --end - end - - self.SpawnGroups[self.SpawnIndex].Spawned = true - return self.SpawnGroups[self.SpawnIndex].Group - else - --self:E( { self.SpawnTemplatePrefix, "No more Groups to Spawn:", SpawnIndex, self.SpawnMaxGroups } ) - end - - return nil -end - ---- Spawns new groups at varying time intervals. --- This is useful if you want to have continuity within your missions of certain (AI) groups to be present (alive) within your missions. --- @param #SPAWN self --- @param #number SpawnTime The time interval defined in seconds between each new spawn of new groups. --- @param #number SpawnTimeVariation The variation to be applied on the defined time interval between each new spawn. --- The variation is a number between 0 and 1, representing the %-tage of variation to be applied on the time interval. --- @return #SPAWN self --- @usage --- -- NATO helicopters engaging in the battle field. --- -- The time interval is set to SPAWN new helicopters between each 600 seconds, with a time variation of 50%. --- -- The time variation in this case will be between 450 seconds and 750 seconds. --- -- This is calculated as follows: --- -- Low limit: 600 * ( 1 - 0.5 / 2 ) = 450 --- -- High limit: 600 * ( 1 + 0.5 / 2 ) = 750 --- -- Between these two values, a random amount of seconds will be choosen for each new spawn of the helicopters. --- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):Schedule( 600, 0.5 ) -function SPAWN:SpawnScheduled( SpawnTime, SpawnTimeVariation ) - self:F( { SpawnTime, SpawnTimeVariation } ) - - if SpawnTime ~= nil and SpawnTimeVariation ~= nil then - self.SpawnScheduler = SCHEDULER:New( self, self._Scheduler, {}, 1, SpawnTime, SpawnTimeVariation ) - end - - return self -end - ---- Will re-start the spawning scheduler. --- Note: This function is only required to be called when the schedule was stopped. -function SPAWN:SpawnScheduleStart() - self:F( { self.SpawnTemplatePrefix } ) - - self.SpawnScheduler:Start() -end - ---- Will stop the scheduled spawning scheduler. -function SPAWN:SpawnScheduleStop() - self:F( { self.SpawnTemplatePrefix } ) - - self.SpawnScheduler:Stop() -end - - ---- Allows to place a CallFunction hook when a new group spawns. --- The provided function will be called when a new group is spawned, including its given parameters. --- The first parameter of the SpawnFunction is the @{Group#GROUP} that was spawned. --- @param #SPAWN self --- @param #function SpawnFunctionHook The function to be called when a group spawns. --- @param SpawnFunctionArguments A random amount of arguments to be provided to the function when the group spawns. --- @return #SPAWN -function SPAWN:SpawnFunction( SpawnFunctionHook, ... ) - self:F( SpawnFunction ) - - self.SpawnFunctionHook = SpawnFunctionHook - self.SpawnFunctionArguments = {} - if arg then - self.SpawnFunctionArguments = arg - end - - return self -end - - - - ---- Will spawn a group from a hosting unit. This function is mostly advisable to be used if you want to simulate spawning from air units, like helicopters, which are dropping infantry into a defined Landing Zone. --- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. --- You can use the returned group to further define the route to be followed. --- @param #SPAWN self --- @param Unit#UNIT HostUnit The air or ground unit dropping or unloading the group. --- @param #number OuterRadius The outer radius in meters where the new group will be spawned. --- @param #number InnerRadius The inner radius in meters where the new group will NOT be spawned. --- @param #number SpawnIndex (Optional) The index which group to spawn within the given zone. --- @return Group#GROUP that was spawned. --- @return #nil Nothing was spawned. -function SPAWN:SpawnFromUnit( HostUnit, OuterRadius, InnerRadius, SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, HostUnit, OuterRadius, InnerRadius, SpawnIndex } ) - - if HostUnit and HostUnit:IsAlive() then -- and HostUnit:getUnit(1):inAir() == false then - - if SpawnIndex then - else - SpawnIndex = self.SpawnIndex + 1 - end - - if self:_GetSpawnIndex( SpawnIndex ) then - - local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate - - if SpawnTemplate then - - local UnitPoint = HostUnit:GetPointVec2() - - self:T( { "Current point of ", self.SpawnTemplatePrefix, UnitPoint } ) - - --for PointID, Point in pairs( SpawnTemplate.route.points ) do - --Point.x = UnitPoint.x - --Point.y = UnitPoint.y - --Point.alt = nil - --Point.alt_type = nil - --end - - SpawnTemplate.route.points[1].x = UnitPoint.x - SpawnTemplate.route.points[1].y = UnitPoint.y - - if not InnerRadius then - InnerRadius = 10 - end - - if not OuterRadius then - OuterRadius = 50 - end - - -- Apply SpawnFormation - for UnitID = 1, #SpawnTemplate.units do - if InnerRadius == 0 then - SpawnTemplate.units[UnitID].x = UnitPoint.x - SpawnTemplate.units[UnitID].y = UnitPoint.y - else - local CirclePos = routines.getRandPointInCircle( UnitPoint, OuterRadius, InnerRadius ) - SpawnTemplate.units[UnitID].x = CirclePos.x - SpawnTemplate.units[UnitID].y = CirclePos.y - end - self:T( 'SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) - end - - local SpawnPos = routines.getRandPointInCircle( UnitPoint, OuterRadius, InnerRadius ) - local Point = {} - Point.type = "Turning Point" - Point.x = SpawnPos.x - Point.y = SpawnPos.y - Point.action = "Cone" - Point.speed = 5 - - table.insert( SpawnTemplate.route.points, 2, Point ) - - return self:SpawnWithIndex( self.SpawnIndex ) - end - end - end - - return nil -end - ---- Will spawn a Group within a given @{Zone#ZONE}. --- Once the group is spawned within the zone, it will continue on its route. --- The first waypoint (where the group is spawned) is replaced with the zone coordinates. --- @param #SPAWN self --- @param Zone#ZONE Zone The zone where the group is to be spawned. --- @param #number ZoneRandomize (Optional) Set to true if you want to randomize the starting point in the zone. --- @param #number SpawnIndex (Optional) The index which group to spawn within the given zone. --- @return Group#GROUP that was spawned. --- @return #nil when nothing was spawned. -function SPAWN:SpawnInZone( Zone, ZoneRandomize, SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, Zone, ZoneRandomize, SpawnIndex } ) - - if Zone then - - if SpawnIndex then - else - SpawnIndex = self.SpawnIndex + 1 - end - - if self:_GetSpawnIndex( SpawnIndex ) then - - local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate - - if SpawnTemplate then - - local ZonePoint - - if ZoneRandomize == true then - ZonePoint = Zone:GetRandomPointVec2() - else - ZonePoint = Zone:GetPointVec2() - end - - SpawnTemplate.route.points[1].x = ZonePoint.x - SpawnTemplate.route.points[1].y = ZonePoint.y - - -- Apply SpawnFormation - for UnitID = 1, #SpawnTemplate.units do - local ZonePointUnit = Zone:GetRandomPointVec2() - SpawnTemplate.units[UnitID].x = ZonePointUnit.x - SpawnTemplate.units[UnitID].y = ZonePointUnit.y - self:T( 'SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) - end - - return self:SpawnWithIndex( self.SpawnIndex ) - end - end - end - - return nil -end - - - - ---- Will spawn a plane group in uncontrolled mode... --- This will be similar to the uncontrolled flag setting in the ME. --- @return #SPAWN self -function SPAWN:UnControlled() - self:F( { self.SpawnTemplatePrefix } ) - - self.SpawnUnControlled = true - - for SpawnGroupID = 1, self.SpawnMaxGroups do - self.SpawnGroups[SpawnGroupID].UnControlled = true - end - - return self -end - - - ---- Will return the SpawnGroupName either with with a specific count number or without any count. --- @param #SPAWN self --- @param #number SpawnIndex Is the number of the Group that is to be spawned. --- @return #string SpawnGroupName -function SPAWN:SpawnGroupName( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) - - local SpawnPrefix = self.SpawnTemplatePrefix - if self.SpawnAliasPrefix then - SpawnPrefix = self.SpawnAliasPrefix - end - - if SpawnIndex then - local SpawnName = string.format( '%s#%03d', SpawnPrefix, SpawnIndex ) - self:T( SpawnName ) - return SpawnName - else - self:T( SpawnPrefix ) - return SpawnPrefix - end - -end - ---- Find the first alive group. --- @param #SPAWN self --- @param #number SpawnCursor A number holding the index from where to find the first group from. --- @return Group#GROUP, #number The group found, the new index where the group was found. --- @return #nil, #nil When no group is found, #nil is returned. -function SPAWN:GetFirstAliveGroup( SpawnCursor ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnCursor } ) - - for SpawnIndex = 1, self.SpawnCount do - local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) - if SpawnGroup and SpawnGroup:IsAlive() then - SpawnCursor = SpawnIndex - return SpawnGroup, SpawnCursor - end - end - - return nil, nil -end - - ---- Find the next alive group. --- @param #SPAWN self --- @param #number SpawnCursor A number holding the last found previous index. --- @return Group#GROUP, #number The group found, the new index where the group was found. --- @return #nil, #nil When no group is found, #nil is returned. -function SPAWN:GetNextAliveGroup( SpawnCursor ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnCursor } ) - - SpawnCursor = SpawnCursor + 1 - for SpawnIndex = SpawnCursor, self.SpawnCount do - local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) - if SpawnGroup and SpawnGroup:IsAlive() then - SpawnCursor = SpawnIndex - return SpawnGroup, SpawnCursor - end - end - - return nil, nil -end - ---- Find the last alive group during runtime. -function SPAWN:GetLastAliveGroup() - self:F( { self.SpawnTemplatePrefixself.SpawnAliasPrefix } ) - - self.SpawnIndex = self:_GetLastIndex() - for SpawnIndex = self.SpawnIndex, 1, -1 do - local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) - if SpawnGroup and SpawnGroup:IsAlive() then - self.SpawnIndex = SpawnIndex - return SpawnGroup - end - end - - self.SpawnIndex = nil - return nil -end - - - ---- Get the group from an index. --- Returns the group from the SpawnGroups list. --- If no index is given, it will return the first group in the list. --- @param #SPAWN self --- @param #number SpawnIndex The index of the group to return. --- @return Group#GROUP -function SPAWN:GetGroupFromIndex( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndex } ) - - if not SpawnIndex then - SpawnIndex = 1 - end - - if self.SpawnGroups and self.SpawnGroups[SpawnIndex] then - local SpawnGroup = self.SpawnGroups[SpawnIndex].Group - return SpawnGroup - else - return nil - end -end - ---- Get the group index from a DCSUnit. --- The method will search for a #-mark, and will return the index behind the #-mark of the DCSUnit. --- It will return nil of no prefix was found. --- @param #SPAWN self --- @param DCSUnit The DCS unit to be searched. --- @return #string The prefix --- @return #nil Nothing found -function SPAWN:_GetGroupIndexFromDCSUnit( DCSUnit ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, DCSUnit } ) - - if DCSUnit and DCSUnit:getName() then - local IndexString = string.match( DCSUnit:getName(), "#.*-" ):sub( 2, -2 ) - self:T( IndexString ) - - if IndexString then - local Index = tonumber( IndexString ) - self:T( { "Index:", IndexString, Index } ) - return Index - end - end - - return nil -end - ---- Return the prefix of a DCSUnit. --- The method will search for a #-mark, and will return the text before the #-mark. --- It will return nil of no prefix was found. --- @param #SPAWN self --- @param DCSUnit The DCS unit to be searched. --- @return #string The prefix --- @return #nil Nothing found -function SPAWN:_GetPrefixFromDCSUnit( DCSUnit ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, DCSUnit } ) - - if DCSUnit and DCSUnit:getName() then - local SpawnPrefix = string.match( DCSUnit:getName(), ".*#" ) - if SpawnPrefix then - SpawnPrefix = SpawnPrefix:sub( 1, -2 ) - end - self:T( SpawnPrefix ) - return SpawnPrefix - end - - return nil -end - ---- Return the group within the SpawnGroups collection with input a DCSUnit. -function SPAWN:_GetGroupFromDCSUnit( DCSUnit ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, DCSUnit } ) - - if DCSUnit then - local SpawnPrefix = self:_GetPrefixFromDCSUnit( DCSUnit ) - - if self.SpawnTemplatePrefix == SpawnPrefix or ( self.SpawnAliasPrefix and self.SpawnAliasPrefix == SpawnPrefix ) then - local SpawnGroupIndex = self:_GetGroupIndexFromDCSUnit( DCSUnit ) - local SpawnGroup = self.SpawnGroups[SpawnGroupIndex].Group - self:T( SpawnGroup ) - return SpawnGroup - end - end - - return nil -end - - ---- Get the index from a given group. --- The function will search the name of the group for a #, and will return the number behind the #-mark. -function SPAWN:GetSpawnIndexFromGroup( SpawnGroup ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnGroup } ) - - local IndexString = string.match( SpawnGroup:GetName(), "#.*$" ):sub( 2 ) - local Index = tonumber( IndexString ) - - self:T( IndexString, Index ) - return Index - -end - ---- Return the last maximum index that can be used. -function SPAWN:_GetLastIndex() - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) - - return self.SpawnMaxGroups -end - ---- Initalize the SpawnGroups collection. -function SPAWN:_InitializeSpawnGroups( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndex } ) - - if not self.SpawnGroups[SpawnIndex] then - self.SpawnGroups[SpawnIndex] = {} - self.SpawnGroups[SpawnIndex].Visible = false - self.SpawnGroups[SpawnIndex].Spawned = false - self.SpawnGroups[SpawnIndex].UnControlled = false - self.SpawnGroups[SpawnIndex].SpawnTime = 0 - - self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix = self.SpawnTemplatePrefix - self.SpawnGroups[SpawnIndex].SpawnTemplate = self:_Prepare( self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix, SpawnIndex ) - end - - self:_RandomizeTemplate( SpawnIndex ) - self:_RandomizeRoute( SpawnIndex ) - --self:_TranslateRotate( SpawnIndex ) - - return self.SpawnGroups[SpawnIndex] -end - - - ---- Gets the CategoryID of the Group with the given SpawnPrefix -function SPAWN:_GetGroupCategoryID( SpawnPrefix ) - local TemplateGroup = Group.getByName( SpawnPrefix ) - - if TemplateGroup then - return TemplateGroup:getCategory() - else - return nil - end -end - ---- Gets the CoalitionID of the Group with the given SpawnPrefix -function SPAWN:_GetGroupCoalitionID( SpawnPrefix ) - local TemplateGroup = Group.getByName( SpawnPrefix ) - - if TemplateGroup then - return TemplateGroup:getCoalition() - else - return nil - end -end - ---- Gets the CountryID of the Group with the given SpawnPrefix -function SPAWN:_GetGroupCountryID( SpawnPrefix ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnPrefix } ) - - local TemplateGroup = Group.getByName( SpawnPrefix ) - - if TemplateGroup then - local TemplateUnits = TemplateGroup:getUnits() - return TemplateUnits[1]:getCountry() - else - return nil - end -end - ---- Gets the Group Template from the ME environment definition. --- This method used the @{DATABASE} object, which contains ALL initial and new spawned object in MOOSE. --- @param #SPAWN self --- @param #string SpawnTemplatePrefix --- @return @SPAWN self -function SPAWN:_GetTemplate( SpawnTemplatePrefix ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnTemplatePrefix } ) - - local SpawnTemplate = nil - - SpawnTemplate = routines.utils.deepCopy( _DATABASE.Templates.Groups[SpawnTemplatePrefix].Template ) - - if SpawnTemplate == nil then - error( 'No Template returned for SpawnTemplatePrefix = ' .. SpawnTemplatePrefix ) - end - - SpawnTemplate.SpawnCoalitionID = self:_GetGroupCoalitionID( SpawnTemplatePrefix ) - SpawnTemplate.SpawnCategoryID = self:_GetGroupCategoryID( SpawnTemplatePrefix ) - SpawnTemplate.SpawnCountryID = self:_GetGroupCountryID( SpawnTemplatePrefix ) - - self:T( { SpawnTemplate } ) - return SpawnTemplate -end - ---- Prepares the new Group Template. --- @param #SPAWN self --- @param #string SpawnTemplatePrefix --- @param #number SpawnIndex --- @return #SPAWN self -function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) - - local SpawnTemplate = self:_GetTemplate( SpawnTemplatePrefix ) - SpawnTemplate.name = self:SpawnGroupName( SpawnIndex ) - - SpawnTemplate.groupId = nil - SpawnTemplate.lateActivation = false - - if SpawnTemplate.SpawnCategoryID == Group.Category.GROUND then - self:T( "For ground units, visible needs to be false..." ) - SpawnTemplate.visible = false - end - - if SpawnTemplate.SpawnCategoryID == Group.Category.HELICOPTER or SpawnTemplate.SpawnCategoryID == Group.Category.AIRPLANE then - SpawnTemplate.uncontrolled = false - end - - for UnitID = 1, #SpawnTemplate.units do - SpawnTemplate.units[UnitID].name = string.format( SpawnTemplate.name .. '-%02d', UnitID ) - SpawnTemplate.units[UnitID].unitId = nil - SpawnTemplate.units[UnitID].x = SpawnTemplate.route.points[1].x - SpawnTemplate.units[UnitID].y = SpawnTemplate.route.points[1].y - end - - self:T( { "Template:", SpawnTemplate } ) - return SpawnTemplate - -end - ---- Private method randomizing the routes. --- @param #SPAWN self --- @param #number SpawnIndex The index of the group to be spawned. --- @return #SPAWN -function SPAWN:_RandomizeRoute( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeRoute, self.SpawnRandomizeRouteStartPoint, self.SpawnRandomizeRouteEndPoint, self.SpawnRandomizeRouteRadius } ) - - if self.SpawnRandomizeRoute then - local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate - local RouteCount = #SpawnTemplate.route.points - - for t = self.SpawnRandomizeRouteStartPoint + 1, ( RouteCount - self.SpawnRandomizeRouteEndPoint ) do - SpawnTemplate.route.points[t].x = SpawnTemplate.route.points[t].x + math.random( self.SpawnRandomizeRouteRadius * -1, self.SpawnRandomizeRouteRadius ) - SpawnTemplate.route.points[t].y = SpawnTemplate.route.points[t].y + math.random( self.SpawnRandomizeRouteRadius * -1, self.SpawnRandomizeRouteRadius ) - -- TODO: manage altitude for airborne units ... - SpawnTemplate.route.points[t].alt = nil - --SpawnGroup.route.points[t].alt_type = nil - self:T( 'SpawnTemplate.route.points[' .. t .. '].x = ' .. SpawnTemplate.route.points[t].x .. ', SpawnTemplate.route.points[' .. t .. '].y = ' .. SpawnTemplate.route.points[t].y ) - end - end - - return self -end - ---- Private method that randomizes the template of the group. --- @param #SPAWN self --- @param #number SpawnIndex --- @return #SPAWN self -function SPAWN:_RandomizeTemplate( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) - - if self.SpawnRandomizeTemplate then - self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix = self.SpawnTemplatePrefixTable[ math.random( 1, #self.SpawnTemplatePrefixTable ) ] - self.SpawnGroups[SpawnIndex].SpawnTemplate = self:_Prepare( self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix, SpawnIndex ) - self.SpawnGroups[SpawnIndex].SpawnTemplate.route = routines.utils.deepCopy( self.SpawnTemplate.route ) - self.SpawnGroups[SpawnIndex].SpawnTemplate.x = self.SpawnTemplate.x - self.SpawnGroups[SpawnIndex].SpawnTemplate.y = self.SpawnTemplate.y - self.SpawnGroups[SpawnIndex].SpawnTemplate.start_time = self.SpawnTemplate.start_time - for UnitID = 1, #self.SpawnGroups[SpawnIndex].SpawnTemplate.units do - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].heading = self.SpawnTemplate.units[1].heading - end - end - - self:_RandomizeRoute( SpawnIndex ) - - return self -end - -function SPAWN:_TranslateRotate( SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle } ) - - -- Translate - local TranslatedX = SpawnX - local TranslatedY = SpawnY - - -- Rotate - -- From Wikipedia: https://en.wikipedia.org/wiki/Rotation_matrix#Common_rotations - -- x' = x \cos \theta - y \sin \theta\ - -- y' = x \sin \theta + y \cos \theta\ - local RotatedX = - TranslatedX * math.cos( math.rad( SpawnAngle ) ) - + TranslatedY * math.sin( math.rad( SpawnAngle ) ) - local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) - + TranslatedY * math.cos( math.rad( SpawnAngle ) ) - - -- Assign - self.SpawnGroups[SpawnIndex].SpawnTemplate.x = SpawnRootX - RotatedX - self.SpawnGroups[SpawnIndex].SpawnTemplate.y = SpawnRootY + RotatedY - - - local SpawnUnitCount = table.getn( self.SpawnGroups[SpawnIndex].SpawnTemplate.units ) - for u = 1, SpawnUnitCount do - - -- Translate - local TranslatedX = SpawnX - local TranslatedY = SpawnY - 10 * ( u - 1 ) - - -- Rotate - local RotatedX = - TranslatedX * math.cos( math.rad( SpawnAngle ) ) - + TranslatedY * math.sin( math.rad( SpawnAngle ) ) - local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) - + TranslatedY * math.cos( math.rad( SpawnAngle ) ) - - -- Assign - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].x = SpawnRootX - RotatedX - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].y = SpawnRootY + RotatedY - self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].heading = self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].heading + math.rad( SpawnAngle ) - end - - return self -end - ---- Get the next index of the groups to be spawned. This function is complicated, as it is used at several spaces. -function SPAWN:_GetSpawnIndex( SpawnIndex ) - self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive, self.AliveUnits, #self.SpawnTemplate.units } ) - - - if ( self.SpawnMaxGroups == 0 ) or ( SpawnIndex <= self.SpawnMaxGroups ) then - if ( self.SpawnMaxUnitsAlive == 0 ) or ( self.AliveUnits < self.SpawnMaxUnitsAlive * #self.SpawnTemplate.units ) or self.UnControlled then - if SpawnIndex and SpawnIndex >= self.SpawnCount + 1 then - self.SpawnCount = self.SpawnCount + 1 - SpawnIndex = self.SpawnCount - end - self.SpawnIndex = SpawnIndex - if not self.SpawnGroups[self.SpawnIndex] then - self:_InitializeSpawnGroups( self.SpawnIndex ) - end - else - return nil - end - else - return nil - end - - return self.SpawnIndex -end - - --- TODO Need to delete this... _DATABASE does this now ... -function SPAWN:_OnBirth( event ) - - if timer.getTime0() < timer.getAbsTime() then -- dont need to add units spawned in at the start of the mission if mist is loaded in init line - if event.initiator and event.initiator:getName() then - local EventPrefix = self:_GetPrefixFromDCSUnit( event.initiator ) - if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then - self:T( { "Birth event: " .. event.initiator:getName(), event } ) - --MessageToAll( "Mission command: unit " .. SpawnTemplatePrefix .. " spawned." , 5, EventPrefix .. '/Event') - self.AliveUnits = self.AliveUnits + 1 - self:T( "Alive Units: " .. self.AliveUnits ) - end - end - end - -end - ---- Obscolete --- @todo Need to delete this... _DATABASE does this now ... -function SPAWN:_OnDeadOrCrash( event ) - self:F( self.SpawnTemplatePrefix, event ) - - if event.initiator and event.initiator:getName() then - local EventPrefix = self:_GetPrefixFromDCSUnit( event.initiator ) - if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then - self:T( { "Dead event: " .. event.initiator:getName(), event } ) --- local DestroyedUnit = Unit.getByName( EventPrefix ) --- if DestroyedUnit and DestroyedUnit.getLife() <= 1.0 then - --MessageToAll( "Mission command: unit " .. SpawnTemplatePrefix .. " crashed." , 5, EventPrefix .. '/Event') - self.AliveUnits = self.AliveUnits - 1 - self:T( "Alive Units: " .. self.AliveUnits ) --- end - end - end -end - ---- Will detect AIR Units taking off... When the event takes place, the spawned Group is registered as airborne... --- This is needed to ensure that Re-SPAWNing only is done for landed AIR Groups. --- @todo Need to test for AIR Groups only... -function SPAWN:_OnTakeOff( event ) - self:F( self.SpawnTemplatePrefix, event ) - - if event.initiator and event.initiator:getName() then - local SpawnGroup = self:_GetGroupFromDCSUnit( event.initiator ) - if SpawnGroup then - self:T( { "TakeOff event: " .. event.initiator:getName(), event } ) - self:T( "self.Landed = false" ) - self.Landed = false - end - end -end - ---- Will detect AIR Units landing... When the event takes place, the spawned Group is registered as landed. --- This is needed to ensure that Re-SPAWNing is only done for landed AIR Groups. --- @todo Need to test for AIR Groups only... -function SPAWN:_OnLand( event ) - self:F( self.SpawnTemplatePrefix, event ) - - local SpawnUnit = event.initiator - if SpawnUnit and SpawnUnit:isExist() and Object.getCategory(SpawnUnit) == Object.Category.UNIT then - local SpawnGroup = self:_GetGroupFromDCSUnit( SpawnUnit ) - if SpawnGroup then - self:T( { "Landed event:" .. SpawnUnit:getName(), event } ) - self.Landed = true - self:T( "self.Landed = true" ) - if self.Landed and self.RepeatOnLanding then - local SpawnGroupIndex = self:GetSpawnIndexFromGroup( SpawnGroup ) - self:T( { "Landed:", "ReSpawn:", SpawnGroup:GetName(), SpawnGroupIndex } ) - self:ReSpawn( SpawnGroupIndex ) - end - end - end -end - ---- Will detect AIR Units shutting down their engines ... --- When the event takes place, and the method @{RepeatOnEngineShutDown} was called, the spawned Group will Re-SPAWN. --- But only when the Unit was registered to have landed. --- @param #SPAWN self --- @see _OnTakeOff --- @see _OnLand --- @todo Need to test for AIR Groups only... -function SPAWN:_OnEngineShutDown( event ) - self:F( self.SpawnTemplatePrefix, event ) - - local SpawnUnit = event.initiator - if SpawnUnit and SpawnUnit:isExist() and Object.getCategory(SpawnUnit) == Object.Category.UNIT then - local SpawnGroup = self:_GetGroupFromDCSUnit( SpawnUnit ) - if SpawnGroup then - self:T( { "EngineShutDown event: " .. SpawnUnit:getName(), event } ) - if self.Landed and self.RepeatOnEngineShutDown then - local SpawnGroupIndex = self:GetSpawnIndexFromGroup( SpawnGroup ) - self:T( { "EngineShutDown: ", "ReSpawn:", SpawnGroup:GetName(), SpawnGroupIndex } ) - self:ReSpawn( SpawnGroupIndex ) - end - end - end -end - ---- This function is called automatically by the Spawning scheduler. --- It is the internal worker method SPAWNing new Groups on the defined time intervals. -function SPAWN:_Scheduler() - self:F( { "_Scheduler", self.SpawnTemplatePrefix, self.SpawnAliasPrefix, self.SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive } ) - - -- Validate if there are still groups left in the batch... - self:Spawn() - - return true -end - -function SPAWN:_SpawnCleanUpScheduler() - self:F( { "CleanUp Scheduler:", self.SpawnTemplatePrefix } ) - - local SpawnCursor - local SpawnGroup, SpawnCursor = self:GetFirstAliveGroup( SpawnCursor ) - - self:T( { "CleanUp Scheduler:", SpawnGroup } ) - - while SpawnGroup do - - if SpawnGroup:AllOnGround() and SpawnGroup:GetMaxVelocity() < 1 then - if not self.SpawnCleanUpTimeStamps[SpawnGroup:GetName()] then - self.SpawnCleanUpTimeStamps[SpawnGroup:GetName()] = timer.getTime() - else - if self.SpawnCleanUpTimeStamps[SpawnGroup:GetName()] + self.SpawnCleanUpInterval < timer.getTime() then - self:T( { "CleanUp Scheduler:", "Cleaning:", SpawnGroup } ) - SpawnGroup:Destroy() - end - end - else - self.SpawnCleanUpTimeStamps[SpawnGroup:GetName()] = nil - end - - SpawnGroup, SpawnCursor = self:GetNextAliveGroup( SpawnCursor ) - - self:T( { "CleanUp Scheduler:", SpawnGroup } ) - - end - - return true -- Repeat - -end ---- Limit the simultaneous movement of Groups within a running Mission. --- This module is defined to improve the performance in missions, and to bring additional realism for GROUND vehicles. --- Performance: If in a DCSRTE there are a lot of moving GROUND units, then in a multi player mission, this WILL create lag if --- the main DCS execution core of your CPU is fully utilized. So, this class will limit the amount of simultaneous moving GROUND units --- on defined intervals (currently every minute). --- @module MOVEMENT - -Include.File( "Routines" ) - ---- the MOVEMENT class --- @type -MOVEMENT = { - ClassName = "MOVEMENT", -} - ---- Creates the main object which is handling the GROUND forces movement. --- @param table{string,...}|string MovePrefixes is a table of the Prefixes (names) of the GROUND Groups that need to be controlled by the MOVEMENT Object. --- @param number MoveMaximum is a number that defines the maximum amount of GROUND Units to be moving during one minute. --- @return MOVEMENT --- @usage --- -- Limit the amount of simultaneous moving units on the ground to prevent lag. --- Movement_US_Platoons = MOVEMENT:New( { 'US Tank Platoon Left', 'US Tank Platoon Middle', 'US Tank Platoon Right', 'US CH-47D Troops' }, 15 ) - -function MOVEMENT:New( MovePrefixes, MoveMaximum ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( { MovePrefixes, MoveMaximum } ) - - if type( MovePrefixes ) == 'table' then - self.MovePrefixes = MovePrefixes - else - self.MovePrefixes = { MovePrefixes } - end - self.MoveCount = 0 -- The internal counter of the amount of Moveing the has happened since MoveStart. - self.MoveMaximum = MoveMaximum -- Contains the Maximum amount of units that are allowed to move... - self.AliveUnits = 0 -- Contains the counter how many units are currently alive - self.MoveUnits = {} -- Reflects if the Moving for this MovePrefixes is going to be scheduled or not. - - _EVENTDISPATCHER:OnBirth( self.OnBirth, self ) - --- self:AddEvent( world.event.S_EVENT_BIRTH, self.OnBirth ) --- --- self:EnableEvents() - - self:ScheduleStart() - - return self -end - ---- Call this function to start the MOVEMENT scheduling. -function MOVEMENT:ScheduleStart() - self:F() - --self.MoveFunction = routines.scheduleFunction( self._Scheduler, { self }, timer.getTime() + 1, 120 ) - self.MoveFunction = SCHEDULER:New( self, self._Scheduler, {}, 1, 120 ) -end - ---- Call this function to stop the MOVEMENT scheduling. --- @todo need to implement it ... Forgot. -function MOVEMENT:ScheduleStop() - self:F() - -end - ---- Captures the birth events when new Units were spawned. --- @todo This method should become obsolete. The new @{DATABASE} class will handle the collection administration. -function MOVEMENT:OnBirth( Event ) - self:F( { Event } ) - - if timer.getTime0() < timer.getAbsTime() then -- dont need to add units spawned in at the start of the mission if mist is loaded in init line - if Event.IniDCSUnit then - self:T( "Birth object : " .. Event.IniDCSUnitName ) - if Event.IniDCSGroup and Event.IniDCSGroup:isExist() then - for MovePrefixID, MovePrefix in pairs( self.MovePrefixes ) do - if string.find( Event.IniDCSUnitName, MovePrefix, 1, true ) then - self.AliveUnits = self.AliveUnits + 1 - self.MoveUnits[Event.IniDCSUnitName] = Event.IniDCSGroupName - self:T( self.AliveUnits ) - end - end - end - end - _EVENTDISPATCHER:OnCrashForUnit( Event.IniDCSUnitName, self.OnDeadOrCrash, self ) - _EVENTDISPATCHER:OnDeadForUnit( Event.IniDCSUnitName, self.OnDeadOrCrash, self ) - end - -end - ---- Captures the Dead or Crash events when Units crash or are destroyed. --- @todo This method should become obsolete. The new @{DATABASE} class will handle the collection administration. -function MOVEMENT:OnDeadOrCrash( Event ) - self:F( { Event } ) - - if Event.IniDCSUnit then - self:T( "Dead object : " .. Event.IniDCSUnitName ) - for MovePrefixID, MovePrefix in pairs( self.MovePrefixes ) do - if string.find( Event.IniDCSUnitName, MovePrefix, 1, true ) then - self.AliveUnits = self.AliveUnits - 1 - self.MoveUnits[Event.IniDCSUnitName] = nil - self:T( self.AliveUnits ) - end - end - end -end - ---- This function is called automatically by the MOVEMENT scheduler. A new function is scheduled when MoveScheduled is true. -function MOVEMENT:_Scheduler() - self:F( { self.MovePrefixes, self.MoveMaximum, self.AliveUnits, self.MovementGroups } ) - - if self.AliveUnits > 0 then - local MoveProbability = ( self.MoveMaximum * 100 ) / self.AliveUnits - self:T( 'Move Probability = ' .. MoveProbability ) - - for MovementUnitName, MovementGroupName in pairs( self.MoveUnits ) do - local MovementGroup = Group.getByName( MovementGroupName ) - if MovementGroup and MovementGroup:isExist() then - local MoveOrStop = math.random( 1, 100 ) - self:T( 'MoveOrStop = ' .. MoveOrStop ) - if MoveOrStop <= MoveProbability then - self:T( 'Group continues moving = ' .. MovementGroupName ) - trigger.action.groupContinueMoving( MovementGroup ) - else - self:T( 'Group stops moving = ' .. MovementGroupName ) - trigger.action.groupStopMoving( MovementGroup ) - end - else - self.MoveUnits[MovementUnitName] = nil - end - end - end - return true -end ---- Provides defensive behaviour to a set of SAM sites within a running Mission. --- @module Sead --- @author to be searched on the forum --- @author (co) Flightcontrol (Modified and enriched with functionality) - -Include.File( "Routines" ) -Include.File( "Event" ) -Include.File( "Base" ) -Include.File( "Mission" ) -Include.File( "Client" ) -Include.File( "Task" ) - ---- The SEAD class --- @type SEAD --- @extends Base#BASE -SEAD = { - ClassName = "SEAD", - TargetSkill = { - Average = { Evade = 50, DelayOff = { 10, 25 }, DelayOn = { 10, 30 } } , - Good = { Evade = 30, DelayOff = { 8, 20 }, DelayOn = { 20, 40 } } , - High = { Evade = 15, DelayOff = { 5, 17 }, DelayOn = { 30, 50 } } , - Excellent = { Evade = 10, DelayOff = { 3, 10 }, DelayOn = { 30, 60 } } - }, - SEADGroupPrefixes = {} -} - ---- Creates the main object which is handling defensive actions for SA sites or moving SA vehicles. --- When an anti radiation missile is fired (KH-58, KH-31P, KH-31A, KH-25MPU, HARM missiles), the SA will shut down their radars and will take evasive actions... --- Chances are big that the missile will miss. --- @param table{string,...}|string SEADGroupPrefixes which is a table of Prefixes of the SA Groups in the DCSRTE on which evasive actions need to be taken. --- @return SEAD --- @usage --- -- CCCP SEAD Defenses --- -- Defends the Russian SA installations from SEAD attacks. --- SEAD_RU_SAM_Defenses = SEAD:New( { 'RU SA-6 Kub', 'RU SA-6 Defenses', 'RU MI-26 Troops', 'RU Attack Gori' } ) -function SEAD:New( SEADGroupPrefixes ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( SEADGroupPrefixes ) - if type( SEADGroupPrefixes ) == 'table' then - for SEADGroupPrefixID, SEADGroupPrefix in pairs( SEADGroupPrefixes ) do - self.SEADGroupPrefixes[SEADGroupPrefix] = SEADGroupPrefix - end - else - self.SEADGroupNames[SEADGroupPrefixes] = SEADGroupPrefixes - end - _EVENTDISPATCHER:OnShot( self.EventShot, self ) - - return self -end - ---- Detects if an SA site was shot with an anti radiation missile. In this case, take evasive actions based on the skill level set within the ME. --- @see SEAD -function SEAD:EventShot( Event ) - self:F( { Event } ) - - local SEADUnit = Event.IniDCSUnit - local SEADUnitName = Event.IniDCSUnitName - local SEADWeapon = Event.Weapon -- Identify the weapon fired - local SEADWeaponName = Event.WeaponName -- return weapon type - --trigger.action.outText( string.format("Alerte, depart missile " ..string.format(SEADWeaponName)), 20) --debug message - -- Start of the 2nd loop - self:T( "Missile Launched = " .. SEADWeaponName ) - if SEADWeaponName == "KH-58" or SEADWeaponName == "KH-25MPU" or SEADWeaponName == "AGM-88" or SEADWeaponName == "KH-31A" or SEADWeaponName == "KH-31P" then -- Check if the missile is a SEAD - local _evade = math.random (1,100) -- random number for chance of evading action - local _targetMim = Event.Weapon:getTarget() -- Identify target - local _targetMimname = Unit.getName(_targetMim) - local _targetMimgroup = Unit.getGroup(Weapon.getTarget(SEADWeapon)) - local _targetMimgroupName = _targetMimgroup:getName() - local _targetMimcont= _targetMimgroup:getController() - local _targetskill = _DATABASE.Templates.Units[_targetMimname].Template.skill - self:T( self.SEADGroupPrefixes ) - self:T( _targetMimgroupName ) - local SEADGroupFound = false - for SEADGroupPrefixID, SEADGroupPrefix in pairs( self.SEADGroupPrefixes ) do - if string.find( _targetMimgroupName, SEADGroupPrefix, 1, true ) then - SEADGroupFound = true - self:T( 'Group Found' ) - break - end - end - if SEADGroupFound == true then - if _targetskill == "Random" then -- when skill is random, choose a skill - local Skills = { "Average", "Good", "High", "Excellent" } - _targetskill = Skills[ math.random(1,4) ] - end - self:T( _targetskill ) -- debug message for skill check - if self.TargetSkill[_targetskill] then - if (_evade > self.TargetSkill[_targetskill].Evade) then - self:T( string.format("Evading, target skill " ..string.format(_targetskill)) ) --debug message - local _targetMim = Weapon.getTarget(SEADWeapon) - local _targetMimname = Unit.getName(_targetMim) - local _targetMimgroup = Unit.getGroup(Weapon.getTarget(SEADWeapon)) - local _targetMimcont= _targetMimgroup:getController() - routines.groupRandomDistSelf(_targetMimgroup,300,'Diamond',250,20) -- move randomly - local SuppressedGroups1 = {} -- unit suppressed radar off for a random time - local function SuppressionEnd1(id) - id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) - SuppressedGroups1[id.groupName] = nil - end - local id = { - groupName = _targetMimgroup, - ctrl = _targetMimcont - } - local delay1 = math.random(self.TargetSkill[_targetskill].DelayOff[1], self.TargetSkill[_targetskill].DelayOff[2]) - if SuppressedGroups1[id.groupName] == nil then - SuppressedGroups1[id.groupName] = { - SuppressionEndTime1 = timer.getTime() + delay1, - SuppressionEndN1 = SuppressionEndCounter1 --Store instance of SuppressionEnd() scheduled function - } - Controller.setOption(_targetMimcont, AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) - timer.scheduleFunction(SuppressionEnd1, id, SuppressedGroups1[id.groupName].SuppressionEndTime1) --Schedule the SuppressionEnd() function - --trigger.action.outText( string.format("Radar Off " ..string.format(delay1)), 20) - end - - local SuppressedGroups = {} - local function SuppressionEnd(id) - id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.RED) - SuppressedGroups[id.groupName] = nil - end - local id = { - groupName = _targetMimgroup, - ctrl = _targetMimcont - } - local delay = math.random(self.TargetSkill[_targetskill].DelayOn[1], self.TargetSkill[_targetskill].DelayOn[2]) - if SuppressedGroups[id.groupName] == nil then - SuppressedGroups[id.groupName] = { - SuppressionEndTime = timer.getTime() + delay, - SuppressionEndN = SuppressionEndCounter --Store instance of SuppressionEnd() scheduled function - } - timer.scheduleFunction(SuppressionEnd, id, SuppressedGroups[id.groupName].SuppressionEndTime) --Schedule the SuppressionEnd() function - --trigger.action.outText( string.format("Radar On " ..string.format(delay)), 20) - end - end - end - end - end -end ---- Taking the lead of AI escorting your flight. --- --- @{#ESCORT} class --- ================ --- The @{#ESCORT} class allows you to interact with escorting AI on your flight and take the lead. --- Each escorting group can be commanded with a whole set of radio commands (radio menu in your flight, and then F10). --- --- The radio commands will vary according the category of the group. The richest set of commands are with Helicopters and AirPlanes. --- Ships and Ground troops will have a more limited set, but they can provide support through the bombing of targets designated by the other escorts. --- --- RADIO MENUs that can be created: --- ================================ --- Find a summary below of the current available commands: --- --- Navigation ...: --- --------------- --- Escort group navigation functions: --- --- * **"Join-Up and Follow at x meters":** The escort group fill follow you at about x meters, and they will follow you. --- * **"Flare":** Provides menu commands to let the escort group shoot a flare in the air in a color. --- * **"Smoke":** Provides menu commands to let the escort group smoke the air in a color. Note that smoking is only available for ground and naval troops. --- --- Hold position ...: --- ------------------ --- Escort group navigation functions: --- --- * **"At current location":** Stops the escort group and they will hover 30 meters above the ground at the position they stopped. --- * **"At client location":** Stops the escort group and they will hover 30 meters above the ground at the position they stopped. --- --- Report targets ...: --- ------------------- --- Report targets will make the escort group to report any target that it identifies within a 8km range. Any detected target can be attacked using the 4. Attack nearby targets function. (see below). --- --- * **"Report now":** Will report the current detected targets. --- * **"Report targets on":** Will make the escort group to report detected targets and will fill the "Attack nearby targets" menu list. --- * **"Report targets off":** Will stop detecting targets. --- --- Scan targets ...: --- ----------------- --- Menu items to pop-up the escort group for target scanning. After scanning, the escort group will resume with the mission or defined task. --- --- * **"Scan targets 30 seconds":** Scan 30 seconds for targets. --- * **"Scan targets 60 seconds":** Scan 60 seconds for targets. --- --- Attack targets ...: --- ------------------- --- This menu item will list all detected targets within a 15km range. Depending on the level of detection (known/unknown) and visuality, the targets type will also be listed. --- --- Request assistance from ...: --- ---------------------------- --- This menu item will list all detected targets within a 15km range, as with the menu item **Attack Targets**. --- This menu item allows to request attack support from other escorts supporting the current client group. --- eg. the function allows a player to request support from the Ship escort to attack a target identified by the Plane escort with its Tomahawk missiles. --- eg. the function allows a player to request support from other Planes escorting to bomb the unit with illumination missiles or bombs, so that the main plane escort can attack the area. --- --- ROE ...: --- -------- --- Sets the Rules of Engagement (ROE) of the escort group when in flight. --- --- * **"Hold Fire":** The escort group will hold fire. --- * **"Return Fire":** The escort group will return fire. --- * **"Open Fire":** The escort group will open fire on designated targets. --- * **"Weapon Free":** The escort group will engage with any target. --- --- Evasion ...: --- ------------ --- Will define the evasion techniques that the escort group will perform during flight or combat. --- --- * **"Fight until death":** The escort group will have no reaction to threats. --- * **"Use flares, chaff and jammers":** The escort group will use passive defense using flares and jammers. No evasive manoeuvres are executed. --- * **"Evade enemy fire":** The rescort group will evade enemy fire before firing. --- * **"Go below radar and evade fire":** The escort group will perform evasive vertical manoeuvres. --- --- Resume Mission ...: --- ------------------- --- Escort groups can have their own mission. This menu item will allow the escort group to resume their Mission from a given waypoint. --- Note that this is really fantastic, as you now have the dynamic of taking control of the escort groups, and allowing them to resume their path or mission. --- --- ESCORT construction methods. --- ============================ --- Create a new SPAWN object with the @{#ESCORT.New} method: --- --- * @{#ESCORT.New}: Creates a new ESCORT object from a @{Group#GROUP} for a @{Client#CLIENT}, with an optional briefing text. --- --- ESCORT initialization methods. --- ============================== --- The following menus are created within the RADIO MENU of an active unit hosted by a player: --- --- * @{#ESCORT.MenuFollowAt}: Creates a menu to make the escort follow the client. --- * @{#ESCORT.MenuHoldAtEscortPosition}: Creates a menu to hold the escort at its current position. --- * @{#ESCORT.MenuHoldAtLeaderPosition}: Creates a menu to hold the escort at the client position. --- * @{#ESCORT.MenuScanForTargets}: Creates a menu so that the escort scans targets. --- * @{#ESCORT.MenuFlare}: Creates a menu to disperse flares. --- * @{#ESCORT.MenuSmoke}: Creates a menu to disparse smoke. --- * @{#ESCORT.MenuReportTargets}: Creates a menu so that the escort reports targets. --- * @{#ESCORT.MenuReportPosition}: Creates a menu so that the escort reports its current position from bullseye. --- * @{#ESCORT.MenuAssistedAttack: Creates a menu so that the escort supportes assisted attack from other escorts with the client. --- * @{#ESCORT.MenuROE: Creates a menu structure to set the rules of engagement of the escort. --- * @{#ESCORT.MenuEvasion: Creates a menu structure to set the evasion techniques when the escort is under threat. --- * @{#ESCORT.MenuResumeMission}: Creates a menu structure so that the escort can resume from a waypoint. --- --- @module Escort --- @author FlightControl - -Include.File( "Routines" ) -Include.File( "Base" ) -Include.File( "Database" ) -Include.File( "Group" ) -Include.File( "Zone" ) - ---- --- @type ESCORT --- @extends Base#BASE --- @field Client#CLIENT EscortClient --- @field Group#GROUP EscortGroup --- @field #string EscortName --- @field #ESCORT.MODE EscortMode The mode the escort is in. --- @field #number FollowScheduler The id of the _FollowScheduler function. --- @field #boolean ReportTargets If true, nearby targets are reported. --- @Field DCSTypes#AI.Option.Air.val.ROE OptionROE Which ROE is set to the EscortGroup. --- @field DCSTypes#AI.Option.Air.val.REACTION_ON_THREAT OptionReactionOnThreat Which REACTION_ON_THREAT is set to the EscortGroup. --- @field Menu#MENU_CLIENT EscortMenuResumeMission -ESCORT = { - ClassName = "ESCORT", - EscortName = nil, -- The Escort Name - EscortClient = nil, - EscortGroup = nil, - EscortMode = nil, - 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, - TaskPoints = {} -} - ---- ESCORT.Mode class --- @type ESCORT.MODE --- @field #number FOLLOW --- @field #number MISSION - ---- MENUPARAM type --- @type MENUPARAM --- @field #ESCORT ParamSelf --- @field #Distance ParamDistance --- @field #function ParamFunction --- @field #string ParamMessage - ---- ESCORT class constructor for an AI group --- @param #ESCORT self --- @param Client#CLIENT EscortClient The client escorted by the EscortGroup. --- @param Group#GROUP EscortGroup The group AI escorting the EscortClient. --- @param #string EscortName Name of the escort. --- @return #ESCORT self -function ESCORT:New( EscortClient, EscortGroup, EscortName, EscortBriefing ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( { EscortClient, EscortGroup, EscortName } ) - - self.EscortClient = EscortClient -- Client#CLIENT - self.EscortGroup = EscortGroup -- Group#GROUP - self.EscortName = EscortName - self.EscortBriefing = EscortBriefing - - self:T( EscortGroup:GetClassNameAndID() ) - - -- Set EscortGroup known at EscortClient. - if not self.EscortClient._EscortGroups then - self.EscortClient._EscortGroups = {} - end - - if not self.EscortClient._EscortGroups[EscortGroup:GetName()] then - self.EscortClient._EscortGroups[EscortGroup:GetName()] = {} - self.EscortClient._EscortGroups[EscortGroup:GetName()].EscortGroup = self.EscortGroup - self.EscortClient._EscortGroups[EscortGroup:GetName()].EscortName = self.EscortName - self.EscortClient._EscortGroups[EscortGroup:GetName()].Targets = {} - self.EscortMode = ESCORT.MODE.FOLLOW - end - - - self.EscortMenu = MENU_CLIENT:New( self.EscortClient, self.EscortName ) - - self.EscortGroup:WayPointInitialize(1) - - self.EscortGroup:OptionROTVertical() - self.EscortGroup:OptionROEOpenFire() - - EscortGroup:MessageToClient( EscortGroup:GetCategoryName() .. " '" .. EscortName .. "' (" .. EscortGroup:GetCallsign() .. ") reporting! " .. - "We're escorting your flight. " .. - "Use the Radio Menu and F10 and use the options under + " .. EscortName .. "\n", - 60, EscortClient - ) - - return self -end - - ---- Defines the default menus --- @param #ESCORT self --- @return #ESCORT -function ESCORT:Menus() - self:F() - - self:MenuFollowAt( 100 ) - self:MenuFollowAt( 200 ) - self:MenuFollowAt( 300 ) - self:MenuFollowAt( 400 ) - - self:MenuScanForTargets( 100, 60 ) - - self:MenuHoldAtEscortPosition( 30 ) - self:MenuHoldAtLeaderPosition( 30 ) - - self:MenuFlare() - self:MenuSmoke() - - self:MenuReportTargets( 60 ) - self:MenuAssistedAttack() - self:MenuROE() - self:MenuEvasion() - self:MenuResumeMission() - - return self -end - - - ---- Defines a menu slot to let the escort Join and Follow you at a certain distance. --- This menu will appear under **Navigation**. --- @param #ESCORT self --- @param DCSTypes#Distance Distance The distance in meters that the escort needs to follow the client. --- @return #ESCORT -function ESCORT:MenuFollowAt( Distance ) - self:F(Distance) - - if self.EscortGroup:IsAir() then - if not self.EscortMenuReportNavigation then - self.EscortMenuReportNavigation = MENU_CLIENT:New( self.EscortClient, "Navigation", self.EscortMenu ) - end - - if not self.EscortMenuJoinUpAndFollow then - self.EscortMenuJoinUpAndFollow = {} - end - - self.EscortMenuJoinUpAndFollow[#self.EscortMenuJoinUpAndFollow+1] = MENU_CLIENT_COMMAND:New( self.EscortClient, "Join-Up and Follow at " .. Distance, self.EscortMenuReportNavigation, ESCORT._JoinUpAndFollow, { ParamSelf = self, ParamDistance = Distance } ) - - self.EscortMode = ESCORT.MODE.FOLLOW - end - - return self -end - ---- Defines a menu slot to let the escort hold at their current position and stay low with a specified height during a specified time in seconds. --- This menu will appear under **Hold position**. --- @param #ESCORT self --- @param DCSTypes#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. --- @param DCSTypes#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. --- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. --- @return #ESCORT --- TODO: Implement Seconds parameter. Challenge is to first develop the "continue from last activity" function. -function ESCORT:MenuHoldAtEscortPosition( Height, Seconds, MenuTextFormat ) - self:F( { Height, Seconds, MenuTextFormat } ) - - if self.EscortGroup:IsAir() then - - if not self.EscortMenuHold then - self.EscortMenuHold = MENU_CLIENT:New( self.EscortClient, "Hold position", self.EscortMenu ) - end - - if not Height then - Height = 30 - end - - if not Seconds then - Seconds = 0 - end - - local MenuText = "" - if not MenuTextFormat then - if Seconds == 0 then - MenuText = string.format( "Hold at %d meter", Height ) - else - MenuText = string.format( "Hold at %d meter for %d seconds", Height, Seconds ) - end - else - if Seconds == 0 then - MenuText = string.format( MenuTextFormat, Height ) - else - MenuText = string.format( MenuTextFormat, Height, Seconds ) - end - end - - if not self.EscortMenuHoldPosition then - self.EscortMenuHoldPosition = {} - end - - self.EscortMenuHoldPosition[#self.EscortMenuHoldPosition+1] = MENU_CLIENT_COMMAND - :New( - self.EscortClient, - MenuText, - self.EscortMenuHold, - ESCORT._HoldPosition, - { ParamSelf = self, - ParamOrbitGroup = self.EscortGroup, - ParamHeight = Height, - ParamSeconds = Seconds - } - ) - end - - return self -end - - ---- Defines a menu slot to let the escort hold at the client position and stay low with a specified height during a specified time in seconds. --- This menu will appear under **Navigation**. --- @param #ESCORT self --- @param DCSTypes#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. --- @param DCSTypes#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. --- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain one or two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. --- @return #ESCORT --- TODO: Implement Seconds parameter. Challenge is to first develop the "continue from last activity" function. -function ESCORT:MenuHoldAtLeaderPosition( Height, Seconds, MenuTextFormat ) - self:F( { Height, Seconds, MenuTextFormat } ) - - if self.EscortGroup:IsAir() then - - if not self.EscortMenuHold then - self.EscortMenuHold = MENU_CLIENT:New( self.EscortClient, "Hold position", self.EscortMenu ) - end - - if not Height then - Height = 30 - end - - if not Seconds then - Seconds = 0 - end - - local MenuText = "" - if not MenuTextFormat then - if Seconds == 0 then - MenuText = string.format( "Rejoin and hold at %d meter", Height ) - else - MenuText = string.format( "Rejoin and hold at %d meter for %d seconds", Height, Seconds ) - end - else - if Seconds == 0 then - MenuText = string.format( MenuTextFormat, Height ) - else - MenuText = string.format( MenuTextFormat, Height, Seconds ) - end - end - - if not self.EscortMenuHoldAtLeaderPosition then - self.EscortMenuHoldAtLeaderPosition = {} - end - - self.EscortMenuHoldAtLeaderPosition[#self.EscortMenuHoldAtLeaderPosition+1] = MENU_CLIENT_COMMAND - :New( - self.EscortClient, - MenuText, - self.EscortMenuHold, - ESCORT._HoldPosition, - { ParamSelf = self, - ParamOrbitGroup = self.EscortClient, - ParamHeight = Height, - ParamSeconds = Seconds - } - ) - end - - return self -end - ---- Defines a menu slot to let the escort scan for targets at a certain height for a certain time in seconds. --- This menu will appear under **Scan targets**. --- @param #ESCORT self --- @param DCSTypes#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. --- @param DCSTypes#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. --- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain one or two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. --- @return #ESCORT -function ESCORT:MenuScanForTargets( Height, Seconds, MenuTextFormat ) - self:F( { Height, Seconds, MenuTextFormat } ) - - if self.EscortGroup:IsAir() then - if not self.EscortMenuScan then - self.EscortMenuScan = MENU_CLIENT:New( self.EscortClient, "Scan for targets", self.EscortMenu ) - end - - if not Height then - Height = 100 - end - - if not Seconds then - Seconds = 30 - end - - local MenuText = "" - if not MenuTextFormat then - if Seconds == 0 then - MenuText = string.format( "At %d meter", Height ) - else - MenuText = string.format( "At %d meter for %d seconds", Height, Seconds ) - end - else - if Seconds == 0 then - MenuText = string.format( MenuTextFormat, Height ) - else - MenuText = string.format( MenuTextFormat, Height, Seconds ) - end - end - - if not self.EscortMenuScanForTargets then - self.EscortMenuScanForTargets = {} - end - - self.EscortMenuScanForTargets[#self.EscortMenuScanForTargets+1] = MENU_CLIENT_COMMAND - :New( - self.EscortClient, - MenuText, - self.EscortMenuScan, - ESCORT._ScanTargets, - { ParamSelf = self, - ParamScanDuration = 30 - } - ) - end - - return self -end - - - ---- Defines a menu slot to let the escort disperse a flare in a certain color. --- This menu will appear under **Navigation**. --- The flare will be fired from the first unit in the group. --- @param #ESCORT self --- @param #string MenuTextFormat Optional parameter that shows the menu option text. If no text is given, the default text will be displayed. --- @return #ESCORT -function ESCORT:MenuFlare( MenuTextFormat ) - self:F() - - if not self.EscortMenuReportNavigation then - self.EscortMenuReportNavigation = MENU_CLIENT:New( self.EscortClient, "Navigation", self.EscortMenu ) - end - - local MenuText = "" - if not MenuTextFormat then - MenuText = "Flare" - else - MenuText = MenuTextFormat - end - - if not self.EscortMenuFlare then - self.EscortMenuFlare = MENU_CLIENT:New( self.EscortClient, MenuText, self.EscortMenuReportNavigation, ESCORT._Flare, { ParamSelf = self } ) - self.EscortMenuFlareGreen = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release green flare", self.EscortMenuFlare, ESCORT._Flare, { ParamSelf = self, ParamColor = UNIT.FlareColor.Green, ParamMessage = "Released a green flare!" } ) - self.EscortMenuFlareRed = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release red flare", self.EscortMenuFlare, ESCORT._Flare, { ParamSelf = self, ParamColor = UNIT.FlareColor.Red, ParamMessage = "Released a red flare!" } ) - self.EscortMenuFlareWhite = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release white flare", self.EscortMenuFlare, ESCORT._Flare, { ParamSelf = self, ParamColor = UNIT.FlareColor.White, ParamMessage = "Released a white flare!" } ) - self.EscortMenuFlareYellow = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release yellow flare", self.EscortMenuFlare, ESCORT._Flare, { ParamSelf = self, ParamColor = UNIT.FlareColor.Yellow, ParamMessage = "Released a yellow flare!" } ) - end - - return self -end - ---- Defines a menu slot to let the escort disperse a smoke in a certain color. --- This menu will appear under **Navigation**. --- Note that smoke menu options will only be displayed for ships and ground units. Not for air units. --- The smoke will be fired from the first unit in the group. --- @param #ESCORT self --- @param #string MenuTextFormat Optional parameter that shows the menu option text. If no text is given, the default text will be displayed. --- @return #ESCORT -function ESCORT:MenuSmoke( MenuTextFormat ) - self:F() - - if not self.EscortGroup:IsAir() then - if not self.EscortMenuReportNavigation then - self.EscortMenuReportNavigation = MENU_CLIENT:New( self.EscortClient, "Navigation", self.EscortMenu ) - end - - local MenuText = "" - if not MenuTextFormat then - MenuText = "Smoke" - else - MenuText = MenuTextFormat - end - - if not self.EscortMenuSmoke then - self.EscortMenuSmoke = MENU_CLIENT:New( self.EscortClient, "Smoke", self.EscortMenuReportNavigation, ESCORT._Smoke, { ParamSelf = self } ) - self.EscortMenuSmokeGreen = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release green smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.Green, ParamMessage = "Releasing green smoke!" } ) - self.EscortMenuSmokeRed = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release red smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.Red, ParamMessage = "Releasing red smoke!" } ) - self.EscortMenuSmokeWhite = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release white smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.White, ParamMessage = "Releasing white smoke!" } ) - self.EscortMenuSmokeOrange = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release orange smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.Orange, ParamMessage = "Releasing orange smoke!" } ) - self.EscortMenuSmokeBlue = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release blue smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.Blue, ParamMessage = "Releasing blue smoke!" } ) - end - end - - return self -end - ---- Defines a menu slot to let the escort report their current detected targets with a specified time interval in seconds. --- This menu will appear under **Report targets**. --- Note that if a report targets menu is not specified, no targets will be detected by the escort, and the attack and assisted attack menus will not be displayed. --- @param #ESCORT self --- @param DCSTypes#Time Seconds Optional parameter that lets the escort report their current detected targets after specified time interval in seconds. The default time is 30 seconds. --- @return #ESCORT -function ESCORT:MenuReportTargets( Seconds ) - self:F( { Seconds } ) - - if not self.EscortMenuReportNearbyTargets then - self.EscortMenuReportNearbyTargets = MENU_CLIENT:New( self.EscortClient, "Report targets", self.EscortMenu ) - end - - if not Seconds then - Seconds = 30 - end - - -- Report Targets - self.EscortMenuReportNearbyTargetsNow = MENU_CLIENT_COMMAND:New( self.EscortClient, "Report targets now!", self.EscortMenuReportNearbyTargets, ESCORT._ReportNearbyTargetsNow, { ParamSelf = self } ) - self.EscortMenuReportNearbyTargetsOn = MENU_CLIENT_COMMAND:New( self.EscortClient, "Report targets on", self.EscortMenuReportNearbyTargets, ESCORT._SwitchReportNearbyTargets, { ParamSelf = self, ParamReportTargets = true } ) - self.EscortMenuReportNearbyTargetsOff = MENU_CLIENT_COMMAND:New( self.EscortClient, "Report targets off", self.EscortMenuReportNearbyTargets, ESCORT._SwitchReportNearbyTargets, { ParamSelf = self, ParamReportTargets = false, } ) - - -- Attack Targets - self.EscortMenuAttackNearbyTargets = MENU_CLIENT:New( self.EscortClient, "Attack targets", self.EscortMenu ) - - - --self.ReportTargetsScheduler = routines.scheduleFunction( self._ReportTargetsScheduler, { self }, timer.getTime() + 1, Seconds ) - self.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, {}, 1, Seconds ) - - return self -end - ---- Defines a menu slot to let the escort attack its detected targets using assisted attack from another escort joined also with the client. --- This menu will appear under **Request assistance from**. --- Note that this method needs to be preceded with the method MenuReportTargets. --- @param #ESCORT self --- @return #ESCORT -function ESCORT:MenuAssistedAttack() - self:F() - - -- Request assistance from other escorts. - -- This is very useful to let f.e. an escorting ship attack a target detected by an escorting plane... - self.EscortMenuTargetAssistance = MENU_CLIENT:New( self.EscortClient, "Request assistance from", self.EscortMenu ) - - return self -end - ---- Defines a menu to let the escort set its rules of engagement. --- All rules of engagement will appear under the menu **ROE**. --- @param #ESCORT self --- @return #ESCORT -function ESCORT:MenuROE( MenuTextFormat ) - self:F( MenuTextFormat ) - - if not self.EscortMenuROE then - -- Rules of Engagement - self.EscortMenuROE = MENU_CLIENT:New( self.EscortClient, "ROE", self.EscortMenu ) - if self.EscortGroup:OptionROEHoldFirePossible() then - self.EscortMenuROEHoldFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Hold Fire", self.EscortMenuROE, ESCORT._ROE, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROEHoldFire(), ParamMessage = "Holding weapons!" } ) - end - if self.EscortGroup:OptionROEReturnFirePossible() then - self.EscortMenuROEReturnFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Return Fire", self.EscortMenuROE, ESCORT._ROE, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROEReturnFire(), ParamMessage = "Returning fire!" } ) - end - if self.EscortGroup:OptionROEOpenFirePossible() then - self.EscortMenuROEOpenFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Open Fire", self.EscortMenuROE, ESCORT._ROE, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROEOpenFire(), ParamMessage = "Opening fire on designated targets!!" } ) - end - if self.EscortGroup:OptionROEWeaponFreePossible() then - self.EscortMenuROEWeaponFree = MENU_CLIENT_COMMAND:New( self.EscortClient, "Weapon Free", self.EscortMenuROE, ESCORT._ROE, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROEWeaponFree(), ParamMessage = "Opening fire on targets of opportunity!" } ) - end - end - - return self -end - - ---- Defines a menu to let the escort set its evasion when under threat. --- All rules of engagement will appear under the menu **Evasion**. --- @param #ESCORT self --- @return #ESCORT -function ESCORT:MenuEvasion( MenuTextFormat ) - self:F( MenuTextFormat ) - - if self.EscortGroup:IsAir() then - if not self.EscortMenuEvasion then - -- Reaction to Threats - self.EscortMenuEvasion = MENU_CLIENT:New( self.EscortClient, "Evasion", self.EscortMenu ) - if self.EscortGroup:OptionROTNoReactionPossible() then - self.EscortMenuEvasionNoReaction = MENU_CLIENT_COMMAND:New( self.EscortClient, "Fight until death", self.EscortMenuEvasion, ESCORT._ROT, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROTNoReaction(), ParamMessage = "Fighting until death!" } ) - end - if self.EscortGroup:OptionROTPassiveDefensePossible() then - self.EscortMenuEvasionPassiveDefense = MENU_CLIENT_COMMAND:New( self.EscortClient, "Use flares, chaff and jammers", self.EscortMenuEvasion, ESCORT._ROT, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROTPassiveDefense(), ParamMessage = "Defending using jammers, chaff and flares!" } ) - end - if self.EscortGroup:OptionROTEvadeFirePossible() then - self.EscortMenuEvasionEvadeFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Evade enemy fire", self.EscortMenuEvasion, ESCORT._ROT, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROTEvadeFire(), ParamMessage = "Evading on enemy fire!" } ) - end - if self.EscortGroup:OptionROTVerticalPossible() then - self.EscortMenuOptionEvasionVertical = MENU_CLIENT_COMMAND:New( self.EscortClient, "Go below radar and evade fire", self.EscortMenuEvasion, ESCORT._ROT, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROTVertical(), ParamMessage = "Evading on enemy fire with vertical manoeuvres!" } ) - end - end - end - - return self -end - ---- Defines a menu to let the escort resume its mission from a waypoint on its route. --- All rules of engagement will appear under the menu **Resume mission from**. --- @param #ESCORT self --- @return #ESCORT -function ESCORT:MenuResumeMission() - self:F() - - if not self.EscortMenuResumeMission then - -- Mission Resume Menu Root - self.EscortMenuResumeMission = MENU_CLIENT:New( self.EscortClient, "Resume mission from", self.EscortMenu ) - end - - return self -end - - ---- @param #MENUPARAM MenuParam -function ESCORT._HoldPosition( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - local OrbitGroup = MenuParam.ParamOrbitGroup -- Group#GROUP - local OrbitUnit = OrbitGroup:GetUnit(1) -- Unit#UNIT - local OrbitHeight = MenuParam.ParamHeight - local OrbitSeconds = MenuParam.ParamSeconds -- Not implemented yet - - routines.removeFunction( self.FollowScheduler ) - - local PointFrom = {} - local GroupPoint = EscortGroup:GetUnit(1):GetPointVec3() - PointFrom = {} - PointFrom.x = GroupPoint.x - PointFrom.y = GroupPoint.z - PointFrom.speed = 250 - PointFrom.type = AI.Task.WaypointType.TURNING_POINT - PointFrom.alt = GroupPoint.y - PointFrom.alt_type = AI.Task.AltitudeType.BARO - - local OrbitPoint = OrbitUnit:GetPointVec2() - local PointTo = {} - PointTo.x = OrbitPoint.x - PointTo.y = OrbitPoint.y - PointTo.speed = 250 - PointTo.type = AI.Task.WaypointType.TURNING_POINT - PointTo.alt = OrbitHeight - PointTo.alt_type = AI.Task.AltitudeType.BARO - PointTo.task = EscortGroup:TaskOrbitCircleAtVec2( OrbitPoint, OrbitHeight, 0 ) - - local Points = { PointFrom, PointTo } - - EscortGroup:OptionROEHoldFire() - EscortGroup:OptionROTPassiveDefense() - - EscortGroup:SetTask( EscortGroup:TaskRoute( Points ) ) - EscortGroup:MessageToClient( "Orbiting at location.", 10, EscortClient ) -end - ---- @param #MENUPARAM MenuParam -function ESCORT._JoinUpAndFollow( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - self.Distance = MenuParam.ParamDistance - - self:JoinUpAndFollow( EscortGroup, EscortClient, self.Distance ) -end - ---- JoinsUp and Follows a CLIENT. --- @param Escort#ESCORT self --- @param Group#GROUP EscortGroup --- @param Client#CLIENT EscortClient --- @param DCSTypes#Distance Distance -function ESCORT:JoinUpAndFollow( EscortGroup, EscortClient, Distance ) - self:F( { EscortGroup, EscortClient, Distance } ) - - if self.FollowScheduler then - routines.removeFunction( self.FollowScheduler ) - end - - EscortGroup:OptionROEHoldFire() - EscortGroup:OptionROTPassiveDefense() - - self.EscortMode = ESCORT.MODE.FOLLOW - - self.CT1 = 0 - self.GT1 = 0 - --self.FollowScheduler = routines.scheduleFunction( self._FollowScheduler, { self, Distance }, timer.getTime() + 1, .5 ) - self.FollowScheduler = SCHEDULER:New( self, self._FollowScheduler, { Distance }, 1, .5, .1 ) - EscortGroup:MessageToClient( "Rejoining and Following at " .. Distance .. "!", 30, EscortClient ) -end - ---- @param #MENUPARAM MenuParam -function ESCORT._Flare( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - local Color = MenuParam.ParamColor - local Message = MenuParam.ParamMessage - - EscortGroup:GetUnit(1):Flare( Color ) - EscortGroup:MessageToClient( Message, 10, EscortClient ) -end - ---- @param #MENUPARAM MenuParam -function ESCORT._Smoke( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - local Color = MenuParam.ParamColor - local Message = MenuParam.ParamMessage - - EscortGroup:GetUnit(1):Smoke( Color ) - EscortGroup:MessageToClient( Message, 10, EscortClient ) -end - - ---- @param #MENUPARAM MenuParam -function ESCORT._ReportNearbyTargetsNow( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - self:_ReportTargetsScheduler() - -end - -function ESCORT._SwitchReportNearbyTargets( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - self.ReportTargets = MenuParam.ParamReportTargets - - if self.ReportTargets then - if not self.ReportTargetsScheduler then - --self.ReportTargetsScheduler = routines.scheduleFunction( self._ReportTargetsScheduler, { self }, timer.getTime() + 1, 30 ) - self.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, {}, 1, 30 ) - end - else - routines.removeFunction( self.ReportTargetsScheduler ) - self.ReportTargetsScheduler = nil - end -end - ---- @param #MENUPARAM MenuParam -function ESCORT._ScanTargets( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - local ScanDuration = MenuParam.ParamScanDuration - - if self.FollowScheduler then - routines.removeFunction( self.FollowScheduler ) - end - - self:T( { "FollowScheduler after removefunction: ", self.FollowScheduler } ) - - if EscortGroup:IsHelicopter() then - SCHEDULER:New( EscortGroup, EscortGroup.PushTask, - { EscortGroup:TaskControlled( - EscortGroup:TaskOrbitCircle( 200, 20 ), - EscortGroup:TaskCondition( nil, nil, nil, nil, ScanDuration, nil ) - ) - }, - 1 - ) - elseif EscortGroup:IsAirPlane() then - SCHEDULER:New( EscortGroup, EscortGroup.PushTask, - { EscortGroup:TaskControlled( - EscortGroup:TaskOrbitCircle( 1000, 500 ), - EscortGroup:TaskCondition( nil, nil, nil, nil, ScanDuration, nil ) - ) - }, - 1 - ) - end - - EscortGroup:MessageToClient( "Scanning targets for " .. ScanDuration .. " seconds.", ScanDuration, EscortClient ) - - if self.EscortMode == ESCORT.MODE.FOLLOW then - --self.FollowScheduler = routines.scheduleFunction( self._FollowScheduler, { self, Distance }, timer.getTime() + ScanDuration, 1 ) - self.FollowScheduler:Start() - end - -end - -function _Resume( EscortGroup ) - env.info( '_Resume' ) - - local Escort = EscortGroup.Escort -- #ESCORT - env.info( "EscortMode = " .. Escort.EscortMode ) - if Escort.EscortMode == ESCORT.MODE.FOLLOW then - Escort:JoinUpAndFollow( EscortGroup, Escort.EscortClient, Escort.Distance ) - end - -end - ---- @param #MENUPARAM MenuParam -function ESCORT._AttackTarget( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - local AttackUnit = MenuParam.ParamUnit -- Unit#UNIT - - if self.FollowScheduler then - routines.removeFunction( self.FollowScheduler ) - end - - self:T( AttackUnit ) - - if EscortGroup:IsAir() then - EscortGroup:OptionROEOpenFire() - EscortGroup:OptionROTPassiveDefense() - EscortGroup.Escort = self -- Need to do this trick to get the reference for the escort in the _Resume function. --- routines.scheduleFunction( --- EscortGroup.PushTask, --- { EscortGroup, --- EscortGroup:TaskCombo( --- { EscortGroup:TaskAttackUnit( AttackUnit ), --- EscortGroup:TaskFunction( 1, 2, "_Resume", {"''"} ) --- } --- ) --- }, timer.getTime() + 10 --- ) - SCHEDULER:New( EscortGroup, - EscortGroup.PushTask, - { EscortGroup:TaskCombo( - { EscortGroup:TaskAttackUnit( AttackUnit ), - EscortGroup:TaskFunction( 1, 2, "_Resume", {"''"} ) - } - ) - }, 10 - ) - else --- routines.scheduleFunction( --- EscortGroup.PushTask, --- { EscortGroup, --- EscortGroup:TaskCombo( --- { EscortGroup:TaskFireAtPoint( AttackUnit:GetPointVec2(), 50 ) --- } --- ) --- }, timer.getTime() + 10 --- ) - SCHEDULER:New( EscortGroup, - EscortGroup.PushTask, - { EscortGroup:TaskCombo( - { EscortGroup:TaskFireAtPoint( AttackUnit:GetPointVec2(), 50 ) - } - ) - }, 10 - ) - end - EscortGroup:MessageToClient( "Engaging Designated Unit!", 10, EscortClient ) - - -end - ---- @param #MENUPARAM MenuParam -function ESCORT._AssistTarget( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - local EscortGroupAttack = MenuParam.ParamEscortGroup - local AttackUnit = MenuParam.ParamUnit -- Unit#UNIT - - if self.FollowScheduler then - routines.removeFunction( self.FollowScheduler ) - end - - - self:T( AttackUnit ) - - if EscortGroupAttack:IsAir() then - EscortGroupAttack:OptionROEOpenFire() - EscortGroupAttack:OptionROTVertical() --- routines.scheduleFunction( --- EscortGroupAttack.PushTask, --- { EscortGroupAttack, --- EscortGroupAttack:TaskCombo( --- { EscortGroupAttack:TaskAttackUnit( AttackUnit ), --- EscortGroupAttack:TaskOrbitCircle( 500, 350 ) --- } --- ) --- }, timer.getTime() + 10 --- ) - SCHDULER:New( EscortGroupAttack, - EscortGroupAttack.PushTask, - { EscortGroupAttack:TaskCombo( - { EscortGroupAttack:TaskAttackUnit( AttackUnit ), - EscortGroupAttack:TaskOrbitCircle( 500, 350 ) - } - ) - }, 10 - ) - else --- routines.scheduleFunction( --- EscortGroupAttack.PushTask, --- { EscortGroupAttack, --- EscortGroupAttack:TaskCombo( --- { EscortGroupAttack:TaskFireAtPoint( AttackUnit:GetPointVec2(), 50 ) --- } --- ) --- }, timer.getTime() + 10 --- ) - SCHEDULER:New( EscortGroupAttack, - EscortGroupAttack.PushTask, - { EscortGroupAttack:TaskCombo( - { EscortGroupAttack:TaskFireAtPoint( AttackUnit:GetPointVec2(), 50 ) - } - ) - }, 10 - ) - end - EscortGroupAttack:MessageToClient( "Assisting with the destroying the enemy unit!", 10, EscortClient ) - -end - ---- @param #MENUPARAM MenuParam -function ESCORT._ROE( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - local EscortROEFunction = MenuParam.ParamFunction - local EscortROEMessage = MenuParam.ParamMessage - - pcall( function() EscortROEFunction() end ) - EscortGroup:MessageToClient( EscortROEMessage, 10, EscortClient ) -end - ---- @param #MENUPARAM MenuParam -function ESCORT._ROT( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - local EscortROTFunction = MenuParam.ParamFunction - local EscortROTMessage = MenuParam.ParamMessage - - pcall( function() EscortROTFunction() end ) - EscortGroup:MessageToClient( EscortROTMessage, 10, EscortClient ) -end - ---- @param #MENUPARAM MenuParam -function ESCORT._ResumeMission( MenuParam ) - - local self = MenuParam.ParamSelf - local EscortGroup = self.EscortGroup - local EscortClient = self.EscortClient - - local WayPoint = MenuParam.ParamWayPoint - - routines.removeFunction( self.FollowScheduler ) - self.FollowScheduler = nil - - local WayPoints = EscortGroup:GetTaskRoute() - self:T( WayPoint, WayPoints ) - - for WayPointIgnore = 1, WayPoint do - table.remove( WayPoints, 1 ) - end - - --routines.scheduleFunction( EscortGroup.SetTask, {EscortGroup, EscortGroup:TaskRoute( WayPoints ) }, timer.getTime() + 1 ) - SCHEDULER:New( EscortGroup, EscortGroup.SetTask, { EscortGroup:TaskRoute( WayPoints ) }, 1 ) - - EscortGroup:MessageToClient( "Resuming mission from waypoint " .. WayPoint .. ".", 10, EscortClient ) -end - ---- Registers the waypoints --- @param #ESCORT self --- @return #table -function ESCORT:RegisterRoute() - self:F() - - local EscortGroup = self.EscortGroup -- Group#GROUP - - local TaskPoints = EscortGroup:GetTaskRoute() - - self:T( TaskPoints ) - - return TaskPoints -end - ---- @param Escort#ESCORT self -function ESCORT:_FollowScheduler( FollowDistance ) - self:F( { FollowDistance }) - - if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then - - local ClientUnit = self.EscortClient:GetClientGroupUnit() - local GroupUnit = self.EscortGroup:GetUnit( 1 ) - - if self.CT1 == 0 and self.GT1 == 0 then - self.CV1 = ClientUnit:GetPointVec3() - self.CT1 = timer.getTime() - self.GV1 = GroupUnit:GetPointVec3() - self.GT1 = timer.getTime() - else - local CT1 = self.CT1 - local CT2 = timer.getTime() - local CV1 = self.CV1 - local CV2 = ClientUnit:GetPointVec3() - self.CT1 = CT2 - self.CV1 = CV2 - - local CD = ( ( CV2.x - CV1.x )^2 + ( CV2.y - CV1.y )^2 + ( CV2.z - CV1.z )^2 ) ^ 0.5 - local CT = CT2 - CT1 - - local CS = ( 3600 / CT ) * ( CD / 1000 ) - - self:T2( { "Client:", CS, CD, CT, CV2, CV1, CT2, CT1 } ) - - local GT1 = self.GT1 - local GT2 = timer.getTime() - local GV1 = self.GV1 - local GV2 = GroupUnit:GetPointVec3() - self.GT1 = GT2 - self.GV1 = GV2 - - local GD = ( ( GV2.x - GV1.x )^2 + ( GV2.y - GV1.y )^2 + ( GV2.z - GV1.z )^2 ) ^ 0.5 - local GT = GT2 - GT1 - - local GS = ( 3600 / GT ) * ( GD / 1000 ) - - self:T2( { "Group:", GS, GD, GT, GV2, GV1, GT2, GT1 } ) - - -- Calculate the group direction vector - local GV = { x = GV2.x - CV2.x, y = GV2.y - CV2.y, z = GV2.z - CV2.z } - - -- Calculate GH2, GH2 with the same height as CV2. - local GH2 = { x = GV2.x, y = CV2.y, z = GV2.z } - - -- Calculate the angle of GV to the orthonormal plane - local alpha = math.atan2( GV.z, GV.x ) - - -- Now we calculate the intersecting vector between the circle around CV2 with radius FollowDistance and GH2. - -- From the GeoGebra model: CVI = (x(CV2) + FollowDistance cos(alpha), y(GH2) + FollowDistance sin(alpha), z(CV2)) - local CVI = { x = CV2.x + FollowDistance * math.cos(alpha), - y = GH2.y, - z = CV2.z + FollowDistance * math.sin(alpha), - } - - -- Calculate the direction vector DV of the escort group. We use CVI as the base and CV2 as the direction. - local DV = { x = CV2.x - CVI.x, y = CV2.y - CVI.y, z = CV2.z - CVI.z } - - -- We now calculate the unary direction vector DVu, so that we can multiply DVu with the speed, which is expressed in meters / s. - -- We need to calculate this vector to predict the point the escort group needs to fly to according its speed. - -- The distance of the destination point should be far enough not to have the aircraft starting to swipe left to right... - local DVu = { x = DV.x / FollowDistance, y = DV.y / FollowDistance, z = DV.z / FollowDistance } - - -- Now we can calculate the group destination vector GDV. - local GDV = { x = DVu.x * CS * 8 + CVI.x, y = CVI.y, z = DVu.z * CS * 8 + CVI.z } - - --trigger.action.smoke( GDV, trigger.smokeColor.Red ) - 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, FlyDistance, Time:", CS, GS, Speed, Distance, Time } ) - - -- Now route the escort to the desired point with the desired speed. - self.EscortGroup:TaskRouteToVec3( GDV, Speed / 3.6 ) -- DCS models speed in Mps (Miles per second) - end - return true - end - - return false -end - - ---- Report Targets Scheduler. --- @param #ESCORT self -function ESCORT:_ReportTargetsScheduler() - self:F( self.EscortGroup:GetName() ) - - if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then - local EscortGroupName = self.EscortGroup:GetName() - local EscortTargets = self.EscortGroup:GetDetectedTargets() - - local ClientEscortTargets = self.EscortClient._EscortGroups[EscortGroupName].Targets - - local EscortTargetMessages = "" - for EscortTargetID, EscortTarget in pairs( EscortTargets ) do - local EscortObject = EscortTarget.object - self:T( EscortObject ) - if EscortObject and EscortObject:isExist() and EscortObject.id_ < 50000000 then - - local EscortTargetUnit = UNIT:Find( EscortObject ) - local EscortTargetUnitName = EscortTargetUnit:GetName() - - - - -- local EscortTargetIsDetected, - -- EscortTargetIsVisible, - -- EscortTargetLastTime, - -- EscortTargetKnowType, - -- EscortTargetKnowDistance, - -- EscortTargetLastPos, - -- EscortTargetLastVelocity - -- = self.EscortGroup:IsTargetDetected( EscortObject ) - -- - -- self:T( { EscortTargetIsDetected, - -- EscortTargetIsVisible, - -- EscortTargetLastTime, - -- EscortTargetKnowType, - -- EscortTargetKnowDistance, - -- EscortTargetLastPos, - -- EscortTargetLastVelocity } ) - - - local EscortTargetUnitPositionVec3 = EscortTargetUnit:GetPointVec3() - local EscortPositionVec3 = self.EscortGroup:GetPointVec3() - local Distance = ( ( EscortTargetUnitPositionVec3.x - EscortPositionVec3.x )^2 + - ( EscortTargetUnitPositionVec3.y - EscortPositionVec3.y )^2 + - ( EscortTargetUnitPositionVec3.z - EscortPositionVec3.z )^2 - ) ^ 0.5 / 1000 - - self:T( { self.EscortGroup:GetName(), EscortTargetUnit:GetName(), Distance, EscortTarget } ) - - if Distance <= 15 then - - if not ClientEscortTargets[EscortTargetUnitName] then - ClientEscortTargets[EscortTargetUnitName] = {} - end - ClientEscortTargets[EscortTargetUnitName].AttackUnit = EscortTargetUnit - ClientEscortTargets[EscortTargetUnitName].visible = EscortTarget.visible - ClientEscortTargets[EscortTargetUnitName].type = EscortTarget.type - ClientEscortTargets[EscortTargetUnitName].distance = EscortTarget.distance - else - if ClientEscortTargets[EscortTargetUnitName] then - ClientEscortTargets[EscortTargetUnitName] = nil - end - end - end - end - - self:T( { "Sorting Targets Table:", ClientEscortTargets } ) - table.sort( ClientEscortTargets, function( a, b ) return a.Distance < b.Distance end ) - self:T( { "Sorted Targets Table:", ClientEscortTargets } ) - - -- Remove the sub menus of the Attack menu of the Escort for the EscortGroup. - self.EscortMenuAttackNearbyTargets:RemoveSubMenus() - - if self.EscortMenuTargetAssistance then - self.EscortMenuTargetAssistance:RemoveSubMenus() - end - - --for MenuIndex = 1, #self.EscortMenuAttackTargets do - -- self:T( { "Remove Menu:", self.EscortMenuAttackTargets[MenuIndex] } ) - -- self.EscortMenuAttackTargets[MenuIndex] = self.EscortMenuAttackTargets[MenuIndex]:Remove() - --end - - - if ClientEscortTargets then - for ClientEscortTargetUnitName, ClientEscortTargetData in pairs( ClientEscortTargets ) do - - for ClientEscortGroupName, EscortGroupData in pairs( self.EscortClient._EscortGroups ) do - - if ClientEscortTargetData and ClientEscortTargetData.AttackUnit:IsAlive() then - - local EscortTargetMessage = "" - local EscortTargetCategoryName = ClientEscortTargetData.AttackUnit:GetCategoryName() - local EscortTargetCategoryType = ClientEscortTargetData.AttackUnit:GetTypeName() - if ClientEscortTargetData.type then - EscortTargetMessage = EscortTargetMessage .. EscortTargetCategoryName .. " (" .. EscortTargetCategoryType .. ") at " - else - EscortTargetMessage = EscortTargetMessage .. "Unknown target at " - end - - local EscortTargetUnitPositionVec3 = ClientEscortTargetData.AttackUnit:GetPointVec3() - local EscortPositionVec3 = self.EscortGroup:GetPointVec3() - local Distance = ( ( EscortTargetUnitPositionVec3.x - EscortPositionVec3.x )^2 + - ( EscortTargetUnitPositionVec3.y - EscortPositionVec3.y )^2 + - ( EscortTargetUnitPositionVec3.z - EscortPositionVec3.z )^2 - ) ^ 0.5 / 1000 - - self:T( { self.EscortGroup:GetName(), ClientEscortTargetData.AttackUnit:GetName(), Distance, ClientEscortTargetData.AttackUnit } ) - if ClientEscortTargetData.visible == false then - EscortTargetMessage = EscortTargetMessage .. string.format( "%.2f", Distance ) .. " estimated km" - else - EscortTargetMessage = EscortTargetMessage .. string.format( "%.2f", Distance ) .. " km" - end - - if ClientEscortTargetData.visible then - EscortTargetMessage = EscortTargetMessage .. ", visual" - end - - if ClientEscortGroupName == EscortGroupName then - - MENU_CLIENT_COMMAND:New( self.EscortClient, - EscortTargetMessage, - self.EscortMenuAttackNearbyTargets, - ESCORT._AttackTarget, - { ParamSelf = self, - ParamUnit = ClientEscortTargetData.AttackUnit - } - ) - EscortTargetMessages = EscortTargetMessages .. "\n - " .. EscortTargetMessage - else - if self.EscortMenuTargetAssistance then - local MenuTargetAssistance = MENU_CLIENT:New( self.EscortClient, EscortGroupData.EscortName, self.EscortMenuTargetAssistance ) - MENU_CLIENT_COMMAND:New( self.EscortClient, - EscortTargetMessage, - MenuTargetAssistance, - ESCORT._AssistTarget, - { ParamSelf = self, - ParamEscortGroup = EscortGroupData.EscortGroup, - ParamUnit = ClientEscortTargetData.AttackUnit - } - ) - end - end - else - ClientEscortTargetData = nil - end - end - end - - if EscortTargetMessages ~= "" and self.ReportTargets == true then - self.EscortGroup:MessageToClient( "Detected targets within 15 km range:" .. EscortTargetMessages:gsub("\n$",""), 20, self.EscortClient ) - else - self.EscortGroup:MessageToClient( "No targets detected!", 20, self.EscortClient ) - end - end - - if self.EscortMenuResumeMission then - self.EscortMenuResumeMission:RemoveSubMenus() - - -- if self.EscortMenuResumeWayPoints then - -- for MenuIndex = 1, #self.EscortMenuResumeWayPoints do - -- self:T( { "Remove Menu:", self.EscortMenuResumeWayPoints[MenuIndex] } ) - -- self.EscortMenuResumeWayPoints[MenuIndex] = self.EscortMenuResumeWayPoints[MenuIndex]:Remove() - -- end - -- end - - local TaskPoints = self:RegisterRoute() - for WayPointID, WayPoint in pairs( TaskPoints ) do - local EscortPositionVec3 = self.EscortGroup:GetPointVec3() - local Distance = ( ( WayPoint.x - EscortPositionVec3.x )^2 + - ( WayPoint.y - EscortPositionVec3.z )^2 - ) ^ 0.5 / 1000 - MENU_CLIENT_COMMAND:New( self.EscortClient, "Waypoint " .. WayPointID .. " at " .. string.format( "%.2f", Distance ).. "km", self.EscortMenuResumeMission, ESCORT._ResumeMission, { ParamSelf = self, ParamWayPoint = WayPointID } ) - end - end - return true - end - - return false -end ---- Provides missile training functions. --- --- @{#MISSILETRAINER} class --- ======================== --- 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. --- --- --- 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. --- --- 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. --- --- @module MissileTrainer --- @author FlightControl - - -Include.File( "Client" ) -Include.File( "Scheduler" ) - ---- The MISSILETRAINER class --- @type MISSILETRAINER --- @extends Base#BASE -MISSILETRAINER = { - ClassName = "MISSILETRAINER", -} - ---- Creates the main object which is handling missile tracking. --- When a missile is fired a SCHEDULER is set off that follows the missile. When near a certain a client player, the missile will be destroyed. --- @param #MISSILETRAINER self --- @param #number Distance The distance in meters when a tracked missile needs to be destroyed when close to a player. --- @param #string Briefing (Optional) Will show a text to the players when starting their mission. Can be used for briefing purposes. --- @return #MISSILETRAINER -function MISSILETRAINER:New( Distance, Briefing ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( Distance ) - - if Briefing then - self.Briefing = Briefing - end - - self.Schedulers = {} - self.SchedulerID = 0 - - self.MessageInterval = 2 - self.MessageLastTime = timer.getTime() - - self.Distance = Distance / 1000 - - _EVENTDISPATCHER:OnShot( self._EventShot, self ) - - self.DB = DATABASE:New():FilterStart() - self.DBClients = self.DB.Clients - self.DBUnits = self.DB.Units - - for ClientID, Client in pairs( self.DBClients ) do - - local function _Alive( Client ) - - if self.Briefing then - Client:Message( self.Briefing, 15, "HELLO WORLD", "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, "MENU", "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 - - Client:Alive( _Alive ) - - end - --- self.DB:ForEachClient( --- --- @param Client#CLIENT Client --- function( Client ) --- --- ... actions ... --- --- end --- ) - - self.MessagesOnOff = true - - self.TrackingToAll = false - self.TrackingOnOff = true - self.TrackingFrequency = 3 - - self.AlertsToAll = true - self.AlertsHitsOnOff = true - self.AlertsLaunchesOnOff = true - - self.DetailsRangeOnOff = true - self.DetailsBearingOnOff = true - - self.MenusOnOff = true - - self.TrackingMissiles = {} - - self.TrackingScheduler = SCHEDULER:New( self, self._TrackMissiles, {}, 0.5, 0.05, 0 ) - - return self -end - --- Initialization methods. - - ---- Sets by default the display of any message to be ON or OFF. --- @param #MISSILETRAINER self --- @param #boolean MessagesOnOff true or false --- @return #MISSILETRAINER self -function MISSILETRAINER:InitMessagesOnOff( MessagesOnOff ) - self:F( MessagesOnOff ) - - self.MessagesOnOff = MessagesOnOff - if self.MessagesOnOff == true then - MESSAGE:New( "Messages ON", "Menu", 15, "ID" ):ToAll() - else - MESSAGE:New( "Messages OFF", "Menu", 15, "ID" ):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", "Menu", 15, "ID" ):ToAll() - else - MESSAGE:New( "Missile tracking to all players OFF", "Menu", 15, "ID" ):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", "Menu", 15, "ID" ):ToAll() - else - MESSAGE:New( "Missile tracking OFF", "Menu", 15, "ID" ):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.", "Menu", 15, "ID" ):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", "Menu", 15, "ID" ):ToAll() - else - MESSAGE:New( "Alerts to all players OFF", "Menu", 15, "ID" ):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", "Menu", 15, "ID" ):ToAll() - else - MESSAGE:New( "Alerts Hits OFF", "Menu", 15, "ID" ):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", "Menu", 15, "ID" ):ToAll() - else - MESSAGE:New( "Alerts Launches OFF", "Menu", 15, "ID" ):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", "Menu", 15, "ID" ):ToAll() - else - MESSAGE:New( "Range display OFF", "Menu", 15, "ID" ):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", "Menu", 15, "ID" ):ToAll() - else - MESSAGE:New( "Bearing display OFF", "Menu", 15, "ID" ):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)", "Menu", 15, "ID" ):ToAll() - else - MESSAGE:New( "Menus are DISABLED", "Menu", 15, "ID" ):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", "Menu", 15, "ID" ):ToAll() - end - -end - ---- Detects if an SA site was shot with an anti radiation missile. In this case, take evasive actions based on the skill level set within the ME. --- @param #MISSILETRAINER self --- @param Event#EVENTDATA Event -function MISSILETRAINER:_EventShot( Event ) - self:F( { Event } ) - - local TrainerSourceDCSUnit = Event.IniDCSUnit - local TrainerSourceDCSUnitName = Event.IniDCSUnitName - local TrainerWeapon = Event.Weapon -- Identify the weapon fired - local TrainerWeaponName = Event.WeaponName -- return weapon type - - self:T( "Missile Launched = " .. TrainerWeaponName ) - - local TrainerTargetDCSUnit = TrainerWeapon:getTarget() -- Identify target - local TrainerTargetDCSUnitName = Unit.getName( TrainerTargetDCSUnit ) - local TrainerTargetSkill = _DATABASE.Templates.Units[TrainerTargetDCSUnitName].Template.skill - - self:T(TrainerTargetDCSUnitName ) - - local Client = self.DBClients[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 ),"Launch Alert", 5, "ID" ) - - if self.AlertsToAll then - Message:ToAll() - else - Message:ToClient( Client ) - end - end - - local ClientID = Client:GetID() - local MissileData = {} - MissileData.TrainerSourceUnit = TrainerSourceUnit - MissileData.TrainerWeapon = TrainerWeapon - MissileData.TrainerTargetUnit = TrainerTargetUnit - MissileData.TrainerWeaponTypeName = TrainerWeapon:getTypeName() - MissileData.TrainerWeaponLaunched = true - table.insert( self.TrackingMissiles[ClientID].MissileData, MissileData ) - --self:T( self.TrackingMissiles ) - end -end - -function MISSILETRAINER:_AddRange( Client, TrainerWeapon ) - - local RangeText = "" - - if self.DetailsRangeOnOff then - - local PositionMissile = TrainerWeapon:getPoint() - local PositionTarget = Client:GetPointVec3() - - local Range = ( ( PositionMissile.x - PositionTarget.x )^2 + - ( PositionMissile.y - PositionTarget.y )^2 + - ( PositionMissile.z - PositionTarget.z )^2 - ) ^ 0.5 / 1000 - - RangeText = string.format( ", at %4.2fkm", Range ) - end - - return RangeText -end - -function MISSILETRAINER:_AddBearing( Client, TrainerWeapon ) - - local BearingText = "" - - if self.DetailsBearingOnOff then - - local PositionMissile = TrainerWeapon:getPoint() - local PositionTarget = Client:GetPointVec3() - - self:T2( { PositionTarget, PositionMissile }) - - local DirectionVector = { x = PositionMissile.x - PositionTarget.x, y = PositionMissile.y - PositionTarget.y, z = PositionMissile.z - PositionTarget.z } - local DirectionRadians = math.atan2( DirectionVector.z, DirectionVector.x ) - --DirectionRadians = DirectionRadians + routines.getNorthCorrection( PositionTarget ) - if DirectionRadians < 0 then - DirectionRadians = DirectionRadians + 2 * math.pi - end - local DirectionDegrees = DirectionRadians * 180 / math.pi - - BearingText = string.format( ", %d degrees", DirectionDegrees ) - end - - return BearingText -end - - -function MISSILETRAINER:_TrackMissiles() - self:F2() - - - local ShowMessages = false - if self.MessagesOnOff and self.MessageLastTime + self.TrackingFrequency <= timer.getTime() then - self.MessageLastTime = timer.getTime() - ShowMessages = true - end - - -- ALERTS PART - - -- Loop for all Player Clients to check the alerts and deletion of missiles. - for ClientDataID, ClientData in pairs( self.TrackingMissiles ) do - - local Client = ClientData.Client - self:T2( { Client:GetName() } ) - - for MissileDataID, MissileData in pairs( ClientData.MissileData ) do - self:T3( MissileDataID ) - - local TrainerSourceUnit = MissileData.TrainerSourceUnit - local TrainerWeapon = MissileData.TrainerWeapon - local TrainerTargetUnit = MissileData.TrainerTargetUnit - local TrainerWeaponTypeName = MissileData.TrainerWeaponTypeName - local TrainerWeaponLaunched = MissileData.TrainerWeaponLaunched - - if Client and Client:IsAlive() and TrainerSourceUnit and TrainerSourceUnit:IsAlive() and TrainerWeapon and TrainerWeapon:isExist() and TrainerTargetUnit and TrainerTargetUnit:IsAlive() then - local PositionMissile = TrainerWeapon:getPosition().p - local PositionTarget = Client:GetPointVec3() - - local Distance = ( ( PositionMissile.x - PositionTarget.x )^2 + - ( PositionMissile.y - PositionTarget.y )^2 + - ( PositionMissile.z - PositionTarget.z )^2 - ) ^ 0.5 / 1000 - - if Distance <= self.Distance then - -- Hit alert - TrainerWeapon:destroy() - if self.MessagesOnOff == true and self.AlertsHitsOnOff == true then - - self:T( "killed" ) - - local Message = MESSAGE:New( - string.format( "%s launched by %s killed %s", - TrainerWeapon:getTypeName(), - TrainerSourceUnit:GetTypeName(), - TrainerTargetUnit:GetPlayerName() - ),"Hit Alert", 15, "ID" ) - - 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() - ),"Tracking", 5, "ID" ) - - 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, "Tracking", 1, "ID" ):ToClient( Client ) - end - end - end - - return true -end -env.info( '*** MOOSE INCLUDE END *** ' ) +env.info("Loaded MOOSE Include Engine")env.info( '*** MOOSE INCLUDE END *** ' ) diff --git a/Moose Test Missions/Moose_Test_CLEANUP/Moose_Test_CLEANUP.miz b/Moose Test Missions/Moose_Test_CLEANUP/Moose_Test_CLEANUP.miz index 928c0524215e3cdb6eca2e24fcf10a61ffe5f0b6..c8858385e1d3314424ec4d99caa97951791d6e01 100644 GIT binary patch delta 281 zcmV+!0p|XmfCip`2C!fn2z*exNVnrR)01c#H3DrClb;$Ge_q=OU}#!_`Vz(Vx#ygF zuYH;L8*!0J3^(m|8y5HXs~JpxFUO1d1m?@h-LL5kR_pQ4HMsK|AaH#Z1TCc03~rfV ziW)8jta-t-p~dqM2EG2*-j~6*r+%;Zxr)5L%4JM**iaiaf~VVn4BbhZaFNv}5JZv3 zMaEL9C}U>46VZ+y6CjRiF5@m#cOG8iTMLx3C-m fnOO*YP`gOC<2TcnQFZ}51$uRvQ0}*qb^(kDz@>o> delta 281 zcmV+!0p|XmfCip`2C!fn2((GMNL%F<9Fu4oH3C%{lb;$Ge?Hp@U}#!_`Vz(Vx#ygF zuYH;L6LFDB3^(m|8x{`_s~JpxEys)b1m?@h{m46VZ+y6CjRiF5!8-a*lSvyuDj!h?brEriB%zCMJZGT`s1N+U zg|~PLLK+D1`u7p)3CxxW3vC^P0?d<>vm2QK#gk1OLk9j2P)h>@m#cOG8iTMLx3C-m fnOO+5NxDc|(arARf1pb^(kDeg1t@ diff --git a/Moose Test Missions/Moose_Test_DATABASE/Moose_Test_DATABASE.lua b/Moose Test Missions/Moose_Test_DATABASE/Moose_Test_DATABASE.lua index 88f76d82a..cbea27ae9 100644 --- a/Moose Test Missions/Moose_Test_DATABASE/Moose_Test_DATABASE.lua +++ b/Moose Test Missions/Moose_Test_DATABASE/Moose_Test_DATABASE.lua @@ -1,39 +1,52 @@ -Include.File( 'Database' ) + +Include.File( 'UnitSet' ) +Include.File( 'GroupSet' ) Include.File( 'Spawn' ) -DBBluePlanes = DATABASE:New() +DBBluePlanes = UNITSET:New() :FilterCoalitions( "blue" ) :FilterCategories( "plane" ) :FilterStart() -DBRedVehicles = DATABASE:New() +DBRedVehicles = UNITSET:New() :FilterCoalitions( "red" ) :FilterCategories( "ground" ) :FilterStart() -DBShips = DATABASE:New() +DBShips = UNITSET:New() :FilterCategories( "ship" ) :FilterStart() -DBBelgium = DATABASE:New() +DBBelgium = UNITSET:New() :FilterCategories( "helicopter" ) :FilterCountries( "BELGIUM" ) :FilterStart() -DBNorthKorea = DATABASE:New() +DBNorthKorea = UNITSET:New() :FilterCountries( "NORTH_KOREA" ) :FilterStart() -DBKA50Vinson = DATABASE:New() +DBKA50Vinson = UNITSET:New() :FilterTypes( { "Ka-50", "VINSON" } ) :FilterStart() + +DBBluePlanesGroup = GROUPSET:New() + :FilterCoalitions( "blue" ) + :FilterCategories( "plane" ) + :FilterStart() + +DBNorthKoreaGroup = GROUPSET:New() + :FilterCountries( "NORTH_KOREA" ) + :FilterStart() -DBBluePlanes:TraceDatabase() -DBRedVehicles:TraceDatabase() -DBShips:TraceDatabase() -DBBelgium:TraceDatabase() -DBNorthKorea:TraceDatabase() -DBKA50Vinson:TraceDatabase() +DBBluePlanes:Flush() +DBRedVehicles:Flush() +DBShips:Flush() +DBBelgium:Flush() +DBNorthKorea:Flush() +DBKA50Vinson:Flush() +DBBluePlanesGroup:Flush() +DBNorthKoreaGroup:Flush() SpawnUS_Plane = SPAWN:New( 'Database Spawn Test USA Plane') @@ -54,21 +67,27 @@ GroupRU_Ship = SpawnRU_Ship:Spawn() SpawnUS_AttackVehicle = SPAWN:New( 'Database Spawn Test USA Attack Vehicle' ) SpawnRU_AttackVehicle = SPAWN:New( 'Database Spawn Test RUSSIA Attack Vehicle' ) -for i = 1, 10 do +for i = 1, 2 do GroupRU_AttackVehicle = SpawnRU_AttackVehicle:SpawnInZone( ZONE:New("Spawn Zone RU"), true) GroupUS_AttackVehicle = SpawnUS_AttackVehicle:SpawnInZone( ZONE:New("Spawn Zone US"), true) end --DBBlue:TraceDatabase() -routines.scheduleFunction( DBBluePlanes.TraceDatabase, { DBBluePlanes }, 1 ) -routines.scheduleFunction( DBRedVehicles.TraceDatabase, { DBRedVehicles }, 1 ) -routines.scheduleFunction( DBShips.TraceDatabase, { DBShips }, 1 ) -routines.scheduleFunction( DBBelgium.TraceDatabase, { DBBelgium }, 1 ) -routines.scheduleFunction( DBNorthKorea.TraceDatabase, { DBNorthKorea }, 1 ) -routines.scheduleFunction( DBKA50Vinson.TraceDatabase, { DBKA50Vinson }, 1 ) +SCHEDULER:New( DBBluePlanes, DBBluePlanes.Flush, { }, 1 ) +SCHEDULER:New( DBRedVehicles, DBRedVehicles.Flush, { }, 1 ) +SCHEDULER:New( DBShips, DBShips.Flush, { }, 1 ) +SCHEDULER:New( DBBelgium, DBBelgium.Flush, { }, 1 ) +SCHEDULER:New( DBNorthKorea, DBNorthKorea.Flush, { }, 1 ) +SCHEDULER:New( DBKA50Vinson, DBKA50Vinson.Flush, { }, 1 ) + +SCHEDULER:New( DBBluePlanesGroup, DBBluePlanesGroup.Flush, { }, 1 ) +SCHEDULER:New( DBNorthKoreaGroup, DBNorthKoreaGroup.Flush, { }, 1 ) DBRedVehicles - :ForEachAliveUnit( function( DCSUnit ) - DBRedVehicles:T( DCSUnit:getName() ) + :ForEachUnit( function( MooseUnit ) + DBRedVehicles:T( MooseUnit:GetName() ) end ) + + + diff --git a/Moose Test Missions/Moose_Test_DATABASE/Moose_Test_DATABASE.miz b/Moose Test Missions/Moose_Test_DATABASE/Moose_Test_DATABASE.miz index 0bdbd3f057ae7e8b9ab85d17dab9b1da91ec05ea..7f9c38b8f44683700581f2ac62ab650541703abc 100644 GIT binary patch delta 27361 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 delta 26049 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 M&;WpJ`#<^r4{T?}T>t<8 diff --git a/Moose Test Missions/Moose_Test_DESTROY/MOOSE_Test_DESTROY.miz b/Moose Test Missions/Moose_Test_DESTROY/MOOSE_Test_DESTROY.miz index 670f8d468b2ba70862fa12569993207514b00123..5d1f7a1b2485548e55ff2da197103fa218d945db 100644 GIT binary patch delta 266 zcmV+l0rmdwg#zw{046VZ+y6CjRiF546VZ+y6CjRiF5!8-a*lZYxnDj!h?brEriB%zCMJZGT`s1N+U zg|~PLLK+D1`u7p)3CxxW3vC^P0?d<>@+z4D#gmLHLjwK}v(YORQwX$4x=35)6&#c9 Qemn)@!!#fsvoC+Y11NEP5C8xG diff --git a/Moose Test Missions/Moose_Test_ESCORT/MOOSE_Test_ESCORT.miz b/Moose Test Missions/Moose_Test_ESCORT/MOOSE_Test_ESCORT.miz index 912b3d1cffe1f62ec4b27fa9f0cb4b238791d1e5..d80475c06c2e55b3d8890031c5313528df38e87f 100644 GIT binary patch delta 266 zcmV+l0rmc+&I6^+1F)`c2z*exNVnrR)04JtH3DrClkIL8e_q=OU}#!_`Vz(Vx#ygF zuYH;L8*!0J3^(m|8y5HXs~JpxFUO1d1m?@h-LL5kR_pQ4HMsK|AaH#Z1TCc03~rfV ziW)8jta-t-p~dqM2EG2*-j~6*r+%;Zxr)5L%4JM**iaiaf~VVn4BbhZaFNv}5JZv3 zMaEL9C}U>46VZ+y6CjRiF546VZ+y6CjRiF5!8-a*lb3HmDj!h?brEriB%zCMJZGT`s1N+U zg|~PLLK+D1`u7p)3CxxW3vC^P0?d<>0dScC#gm(GLjwK}v*2(YaR{_Yx=35)6&#ZV Q;yeZ7!!#fsvqs|I1Tf-#n*aa+ diff --git a/Moose Test Missions/Moose_Test_MISSILETRAINER/Moose_Test_MISSILETRAINER.miz b/Moose Test Missions/Moose_Test_MISSILETRAINER/Moose_Test_MISSILETRAINER.miz index 69faae2dfeccb9769d5d34772b3e8629f522829c..52cae96f7dc46732a12dcc03ad0e4091db239fc9 100644 GIT binary patch delta 275 zcmV+u0qp+6od?662e5P=2z*exNVnrR)02E2HGgdq5)TMig;eM?*u%6|l}VgnDX}Zt z*~_&5UfT&^Xj*~#62+#PuxbqtzaD5d7Eu_>8Zkb?;8ZHH_dBL=y#q$sbz5dtUm%+EEey{hrioCwcWlVF} zPzf6~f~VVn4BbhSIUgPdzyHBH`azRjA3!P|PzQApafu|Mi|;&Vp$n)F{Jw>^cnU%q z2=V&Q5$X}lmI(`O9fJbQqm#!UnE~08VIV^WegRNR0|b{W%>f#N#2~lCAOQxf2z*ex ZNVnrR)0fiB0XzkIb(v7^w-e0)umLs5fXM&= delta 275 zcmV+u0qp+6od?662e5P=2((GMNL%F<9Fu$=HGfqa5)TMig;eM?*u%6|l}VgnDX}Zt z*~_&5KHCXkXj*~#62+z2@xbqtzaD5d7Eu_>8?wDYT8ZHH_dBL=y#mf)|z5Z?Q%i#98_qG4IioCwcWlVF} zPzf6~g6F${4BbnUIUgPd-+#e6`c9KwA3!P}Q3rJqafu|Mi*Gz^cnU%q z2=V&&5$XxdmI(`O9fJbQlat3EnE}Of#N#2~lCAOQxf2((GM ZNL%F<9GB9}0Xzlb!!#fsw-e0)umK8)eGUKs diff --git a/Moose Test Missions/Moose_Test_SEAD/MOOSE_Test_SEAD.miz b/Moose Test Missions/Moose_Test_SEAD/MOOSE_Test_SEAD.miz index dbe71769b56c938b237c60fe99d0c8cc44a081b8..48d70c1058e4294120e4d02ccbaf524929e71d1b 100644 GIT binary patch delta 268 zcmV+n0rUQ=$pNd$0kFmw2z*exNVnrR)055@HGgdq5)TMig;eM?*u%6|l}VgnDX}Zt z*~_&5UfT&^Xj*~#62+#PuxbqtzaD5d7Eu_>8Zkb?;8ZHH_dBL=y#q$sbz5dtUm%+EEey{hrioCwcWlVF} zPzf6~f~VVn4BbhSix?gTzyHBH`azSg7(gl>PzQApafu|Mi|;&Vp$n)F{Jw>^cnU%q z2=V&Q5$X}lmI(`O9fJbQqmvmKnE~08vl&AMegRNR0|b*EV;Zv-8m34Hd{Dbcx8pa{ Slf+^?1$uRvQ0}t@W2OO!`+h?J delta 268 zcmV+n0rUQ=$pNd$0kFmw2((GMNL%F<9Fxu$HGfqa5)TMig;eM?*u%6|l}VgnDX}Zt z*~_&5KHCXkXj*~#62+z2@xbqtzaD5d7Eu_>8?wDYT8ZHH_dBL=y#mf)|z5Z?Q%i#98_qG4IioCwcWlVF} zPzf6~g6F${4BbnUix?gT-+#e6`c9Lt7(gl?Q3rJqafu|Mi*Gz^cnU%q z2=V&&5$XxdmI(`O9fJbQlam=4nE}O(arARe;?W2OOauX>08 diff --git a/Moose Test Missions/Moose_Test_SPAWN/MOOSE_Test_SPAWN.miz b/Moose Test Missions/Moose_Test_SPAWN/MOOSE_Test_SPAWN.miz index 3a6bcd9e665e044b158358fc53ae8b9e5cf442aa..cf86dece2888efeee19a1c58c078aebe40e023a7 100644 GIT binary patch delta 259 zcmV+e0sQ{-j|23N1F$+#PuxbqtzaD5d7Eu_>8Zkb?;8ZHH_dBL=y#q$sbz5dtUm%+EEey{hrioCwcWlVF} zPzf6~f~VVn4BbhRA07t3|G_%?L6amOKq?+k2Xzr~i6o(m?>uLr3#bqLzJ<4V3PKtP z@%qmZ>JiMA2@7ola3#m0oju%AVUIv0kdr&jim^DP`gOC<2Tcj0>(TAdUcsl J?z2P2u>np-du9Lt delta 259 zcmV+e0sQ{-j|23N1F$+z2@xbqtzaD5d7Eu_>8?wDYT8ZHH_dBL=y#mf)|z5Z?Q%i#98_qG4IioCwcWlVF} zPzf6~g6F${4BbnTA07tZf5AHXPLm`bKq?>Iux22@7o(TA;=?o` J9nQ|cdGyZ diff --git a/Moose Test Missions/Moose_Test_SPAWN_Repeat/MOOSE_Test_SPAWN_Repeat.miz b/Moose Test Missions/Moose_Test_SPAWN_Repeat/MOOSE_Test_SPAWN_Repeat.miz index 63198ee5a4306d1ac658a6402a1db825c8234151..8899cab3200d9bb6025a9ddcdac781202d45c2dc 100644 GIT binary patch delta 260 zcmV+f0sH>+#R2ri0kF0h2z*exNVnrR)04g!HGgdq5)TMig;eM?*u%6|l}VgnDX}Zt z*~_&5UfT&^Xj*~#62+#PuxbqtzaD5d7Eu_>8Zkb?;8ZHH_dBL=y#q$sbz5dtUm%+EEey{hrioCwcWlVF} zPzf6~f~VVn4BbhSdl?=EzyHBH`azSQ89*u?PzQApafu|Mi|;&Vp$n)F{Jw>^cnU%q z2=V&Q5$X}lmI(`O9fJbQqmv05nE~08qZ&g3egU)S8d^pOd{Dbcx8pa{lS*Mc1$uRv KQ0}vfVQB$3N__7C delta 260 zcmV+f0sH>+#R2ri0kF0h2((GMNL%F<9Fx8nHGfqa5)TMig;eM?*u%6|l}VgnDX}Zt z*~_&5KHCXkXj*~#62+z2@xbqtzaD5d7Eu_>8?wDYT8ZHH_dBL=y#mf)|z5Z?Q%i#98_qG4IioCwcWlVF} zPzf6~g6F${4BbnUdl?=E-+#e6`c9Ld89*u@Q3rJqafu|Mi*Gz^cnU%q z2=V&&5$XxdmI(`O9fJbQlamP=nE}O(ar KARe=eVQB#^MtL*< diff --git a/Moose Test Missions/Moose_Test_TASK_Pickup_and_Deploy/MOOSE_Test_TASK_Pickup_and_Deploy.miz b/Moose Test Missions/Moose_Test_TASK_Pickup_and_Deploy/MOOSE_Test_TASK_Pickup_and_Deploy.miz index c909c341bcab3b20e029a3918dbcfaf42b1381ae..eec12788c51e57659aef5a7c4ec1a2d7101e1c43 100644 GIT binary patch delta 265 zcmV+k0rvjA`2oH80kEPO2z*exNVnrR)03(hH3DrCli?W{e_q=OU}#!_`Vz(Vx#ygF zuYH;L8*!0J3^(m|8y5HXs~JpxFUO1d1m?@h-LL5kR_pQ4HMsK|AaH#Z1TCc03~rfV ziW)8jta-t-p~dqM2EG2*-j~6*r+%;Zxr)5L%4JM**iaiaf~VVn4BbhZaFNv}5JZv3 zMaEL9C}U>4lXw~p2EYHoI{HDAiyA;G9#98$5pjtmp^NW4XQ2zI5B$D`w|ELd8VK?F z&k^bo%$5lYZ5@LG%%hX_8kqsvlaCui27UohO9KRx&UqTM@*8?x2z*exNVnrR)01?0 PJOz4nnNaStwRs2w&OCkJ delta 265 zcmV+k0rvjA`2oH80kEPO2((GMNL%F<9FwXUH3C%{li?W{e?Hp@U}#!_`Vz(Vx#ygF zuYH;L6LFDB3^(m|8x{`_s~JpxEys)b1m?@h{m4lXw~p2H$_dI{HqNiyA;GA5jN&5pjtmp^I-kXQ2zI5B$D`w|ELd8VK?F z_Yvv|%$5lYZ5@LG%#)M#8kqsblaCui2L2CFO9KRx&UqTM@*8?x2((GMNL%F<9Fuf; PJO$#zG$046VZ+y6CjRiF5O4DjrY=brEriB%zD%JZGT`s1N+U zg|~PLLK+D1`p*&S5zLke3vC^P0?eb6qD`3r*^@j@LjryQvw==+PY8TayGXa=H`9~$ Qoje74b(v7^vp1f>0Xrjqx&QzG delta 266 zcmV+l0rmd)zXJHb046VZ+y6CjRiF5!8-a*lQ>O4Dj!h?brEriB%zCMJZGT`s1N+U zg|~PLLK+D1`u7p)3CxxW3vC^P0?d<>qD`3r#gjZvLjwK}vw==+PYAS0x=35)6&#cH Qoje8N!!#fsvp1f>0U5Y^{{R30 From 7efbd60b9ed5cf983f4a89c64676ca1b62219dfd Mon Sep 17 00:00:00 2001 From: FlightControl Date: Fri, 27 May 2016 13:21:39 +0200 Subject: [PATCH 3/5] DATABASE now also follows correctly the players. --- Moose Development/Moose/Base.lua | 5 +- Moose Development/Moose/Database.lua | 343 +++++++----------- Moose Development/Moose/Moose.lua | 2 +- Moose Development/Moose/Set.lua | 4 +- .../Moose_Test_DATABASE.lua | 15 + .../Moose_Test_DATABASE.miz | Bin 29075 -> 38047 bytes 6 files changed, 157 insertions(+), 212 deletions(-) diff --git a/Moose Development/Moose/Base.lua b/Moose Development/Moose/Base.lua index e2196c8ba..3586673d1 100644 --- a/Moose Development/Moose/Base.lua +++ b/Moose Development/Moose/Base.lua @@ -487,7 +487,10 @@ function BASE:E( Arguments ) end local LineCurrent = DebugInfoCurrent.currentline - local LineFrom = DebugInfoFrom.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 diff --git a/Moose Development/Moose/Database.lua b/Moose Development/Moose/Database.lua index b0fff9b76..66febcae9 100644 --- a/Moose Development/Moose/Database.lua +++ b/Moose Development/Moose/Database.lua @@ -1,64 +1,34 @@ ---- Manage sets of units and groups. +--- Manage the mission database. -- --- @{#Database} class +-- @{#DATABASE} class -- ================== --- Mission designers can use the DATABASE class to build sets of units belonging to certain: +-- Mission designers can use the DATABASE class to refer to: -- --- * Coalitions --- * Categories --- * Countries --- * Unit types --- * Starting with certain prefix strings. +-- * UNITS +-- * GROUPS +-- * players +-- * alive players +-- * CLIENTS +-- * alive CLIENTS -- --- This list will grow over time. Planned developments are to include filters and iterators. --- Additional filters will be added around @{Zone#ZONEs}, Radiuses, Active players, ... --- More iterators will be implemented in the near future ... --- --- Administers the Initial Sets of the Mission Templates as defined within the Mission Editor. --- --- DATABASE construction methods: --- ================================= --- Create a new DATABASE object with the @{#DATABASE.New} method: --- --- * @{#DATABASE.New}: Creates a new DATABASE object. --- --- --- DATABASE filter criteria: --- ========================= --- You can set filter criteria to define the set of units within the database. --- Filter criteria are defined by: --- --- * @{#DATABASE.FilterCoalitions}: Builds the DATABASE with the units belonging to the coalition(s). --- * @{#DATABASE.FilterCategories}: Builds the DATABASE with the units belonging to the category(ies). --- * @{#DATABASE.FilterTypes}: Builds the DATABASE with the units belonging to the unit type(s). --- * @{#DATABASE.FilterCountries}: Builds the DATABASE with the units belonging to the country(ies). --- * @{#DATABASE.FilterUnitPrefixes}: Builds the DATABASE with the units starting with the same prefix string(s). --- --- Once the filter criteria have been set for the DATABASE, you can start filtering using: --- --- * @{#DATABASE.FilterStart}: Starts the filtering of the units within the database. --- --- Planned filter criteria within development are (so these are not yet available): --- --- * @{#DATABASE.FilterGroupPrefixes}: Builds the DATABASE with the groups of the units starting with the same prefix string(s). --- * @{#DATABASE.FilterZones}: Builds the DATABASE with the units within a @{Zone#ZONE}. +-- On top, for internal MOOSE administration purposes, the DATBASE administers the Unit and Gruop 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. -- -- DATABASE iterators: -- =================== --- Once the filters have been defined and the DATABASE has been built, you can iterate the database with the available iterator methods. +-- 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.ForEachAliveUnit}: Calls a function for each alive unit it finds 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 player it finds within the DATABASE. +-- * @{#DATABASE.ForEachPlayerAlive}: Calls a function for each alive 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. -- --- Planned iterators methods in development are (so these are not yet available): --- --- * @{#DATABASE.ForEachUnit}: Calls a function for each unit contained within the DATABASE. --- * @{#DATABASE.ForEachGroup}: Calls a function for each group contained within the DATABASE. --- * @{#DATABASE.ForEachUnitInZone}: Calls a function for each unit within a certain zone contained within the DATABASE. --- --- ==== -- @module Database -- @author FlightControl @@ -70,6 +40,7 @@ Include.File( "Unit" ) Include.File( "Event" ) Include.File( "Client" ) + --- DATABASE class -- @type DATABASE -- @extends Base#BASE @@ -85,34 +56,11 @@ DATABASE = { DCSGroups = {}, UNITS = {}, GROUPS = {}, - NavPoints = {}, - Statics = {}, - Players = {}, - PlayersAlive = {}, + PLAYERS = {}, + PLAYERSALIVE = {}, CLIENTS = {}, - ClientsAlive = {}, - Filter = { - Coalitions = nil, - Categories = nil, - Types = nil, - Countries = nil, - UnitPrefixes = nil, - GroupPrefixes = 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, - }, - }, + CLIENTSALIVE = {}, + NavPoints = {}, } local _DATABASECoalition = @@ -147,12 +95,13 @@ function DATABASE:New() _EVENTDISPATCHER:OnCrash( self._EventOnDeadOrCrash, self ) - -- Add database with registered clients and already alive players - -- Follow alive players and clients _EVENTDISPATCHER:OnPlayerEnterUnit( self._EventOnPlayerEnterUnit, self ) _EVENTDISPATCHER:OnPlayerLeaveUnit( self._EventOnPlayerLeaveUnit, self ) + self:_RegisterTemplates() + self:_RegisterDatabase() + self:_RegisterPlayers() return self end @@ -167,6 +116,7 @@ function DATABASE:FindUnit( UnitName ) return UnitFound end + --- Adds a Unit based on the Unit Name in the DATABASE. -- @param #DATABASE self function DATABASE:AddUnit( DCSUnit, DCSUnitName ) @@ -175,6 +125,7 @@ function DATABASE:AddUnit( DCSUnit, DCSUnitName ) self.UNITS[DCSUnitName] = UNIT:Register( DCSUnitName ) end + --- Deletes a Unit from the DATABASE based on the Unit Name. -- @param #DATABASE self function DATABASE:DeleteUnit( DCSUnitName ) @@ -182,6 +133,7 @@ function DATABASE:DeleteUnit( DCSUnitName ) self.DCSUnits[DCSUnitName] = nil end + --- Finds a CLIENT based on the ClientName. -- @param #DATABASE self -- @param #string ClientName @@ -192,6 +144,7 @@ function DATABASE:FindClient( ClientName ) return ClientFound end + --- Adds a CLIENT based on the ClientName in the DATABASE. -- @param #DATABASE self function DATABASE:AddClient( ClientName ) @@ -200,6 +153,7 @@ function DATABASE:AddClient( ClientName ) self:E( self.CLIENTS[ClientName]:GetClassNameAndID() ) end + --- Finds a GROUP based on the GroupName. -- @param #DATABASE self -- @param #string GroupName @@ -210,6 +164,7 @@ function DATABASE:FindGroup( GroupName ) return GroupFound end + --- Adds a GROUP based on the GroupName in the DATABASE. -- @param #DATABASE self function DATABASE:AddGroup( DCSGroup, GroupName ) @@ -218,6 +173,30 @@ function DATABASE:AddGroup( DCSGroup, GroupName ) self.GROUPS[GroupName] = GROUP:Register( 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] = PlayerName + self.PLAYERSALIVE[PlayerName] = PlayerName + self.CLIENTSALIVE[PlayerName] = self:FindClient( UnitName ) + 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.PLAYERSALIVE[PlayerName] = nil + self.CLIENTSALIVE[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 @@ -253,7 +232,6 @@ function DATABASE:Spawn( SpawnTemplate ) 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 ) @@ -261,7 +239,6 @@ function DATABASE:SetStatusGroup( GroupName, 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 ) @@ -273,6 +250,7 @@ function DATABASE:GetStatusGroup( GroupName ) end end + --- Private method that registers new Group Templates within the DATABASE Object. -- @param #DATABASE self -- @param #table GroupTemplate @@ -317,6 +295,7 @@ function DATABASE:_RegisterTemplate( GroupTemplate ) end end + --- Private method that registers all alive players in the mission. -- @param #DATABASE self -- @return #DATABASE self @@ -328,9 +307,10 @@ function DATABASE:_RegisterPlayers() 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() + 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 @@ -339,6 +319,7 @@ function DATABASE:_RegisterPlayers() return self end + --- Private method that registers all datapoints within in the mission. -- @param #DATABASE self -- @return #DATABASE self @@ -384,14 +365,13 @@ function DATABASE:_EventOnBirth( Event ) self:F2( { Event } ) if Event.IniDCSUnit then - if self:_IsIncludeDCSUnit( Event.IniDCSUnit ) then - self:AddUnit( Event.IniDCSUnit, Event.IniDCSUnitName ) - self:AddGroup( Event.IniDCSGroup, Event.IniDCSGroupName ) - self:_EventOnPlayerEnterUnit( Event ) - end + self:AddUnit( Event.IniDCSUnit, Event.IniDCSUnitName ) + self:AddGroup( Event.IniDCSGroup, Event.IniDCSGroupName ) + self:_EventOnPlayerEnterUnit( Event ) end end + --- Handles the OnDead or OnCrash event for alive units set. -- @param #DATABASE self -- @param Event#EVENTDATA Event @@ -406,6 +386,7 @@ function DATABASE:_EventOnDeadOrCrash( Event ) end end + --- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). -- @param #DATABASE self -- @param Event#EVENTDATA Event @@ -413,16 +394,14 @@ function DATABASE:_EventOnPlayerEnterUnit( Event ) self:F2( { Event } ) if Event.IniDCSUnit then - if self:_IsIncludeDCSUnit( Event.IniDCSUnit ) then - if not self.PlayersAlive[Event.IniDCSUnitName] then - self:E( { "Add player for unit:", Event.IniDCSUnitName, Event.IniDCSUnit:getPlayerName() } ) - self.PlayersAlive[Event.IniDCSUnitName] = Event.IniDCSUnit:getPlayerName() - self.ClientsAlive[Event.IniDCSUnitName] = self.CLIENTS[ Event.IniDCSUnitName ] - end + local PlayerName = Event.IniDCSUnit:getPlayerName() + if not self.PLAYERSALIVE[PlayerName] then + self:AddPlayer( Event.IniDCSUnitName, PlayerName ) end end end + --- Handles the OnPlayerLeaveUnit event to clean the active players table. -- @param #DATABASE self -- @param Event#EVENTDATA Event @@ -430,19 +409,16 @@ function DATABASE:_EventOnPlayerLeaveUnit( Event ) self:F2( { Event } ) if Event.IniDCSUnit then - if self:_IsIncludeDCSUnit( Event.IniDCSUnit ) then - if self.PlayersAlive[Event.IniDCSUnitName] then - self:E( { "Cleaning player for unit:", Event.IniDCSUnitName, Event.IniDCSUnit:getPlayerName() } ) - self.PlayersAlive[Event.IniDCSUnitName] = nil - self.ClientsAlive[Event.IniDCSUnitName] = nil - end + local PlayerName = Event.IniDCSUnit:getPlayerName() + if self.PLAYERSALIVE[PlayerName] then + self:DeletePlayer( PlayerName ) end end end --- Iterators ---- Interate the DATABASE and call an interator function for the given set, providing the Object for each element within the set and optional parameters. +--- 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 @@ -485,9 +461,9 @@ function DATABASE:ForEach( IteratorFunction, arg, Set ) end ---- Interate the DATABASE and call an interator function for each **alive** unit, providing the Unit and optional parameters. +--- Iterate the DATABASE and call an iterator function for each **alive** unit, providing the DCSUnit 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 when there is an alive unit in the database. The function needs to accept a DCSUnit parameter. -- @return #DATABASE self function DATABASE:ForEachDCSUnit( IteratorFunction, ... ) self:F2( arg ) @@ -497,20 +473,58 @@ function DATABASE:ForEachDCSUnit( IteratorFunction, ... ) return self end ---- Interate the DATABASE and call an interator function for each **alive** player, providing the Unit of the player and optional parameters. + +--- 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 player in the database. The function needs to accept a UNIT parameter. +-- @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, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, 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 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.PlayersAlive ) + self:ForEach( IteratorFunction, arg, self.PLAYERS ) return self end ---- Interate the DATABASE and call an interator function for each client, providing the Client to the function and optional parameters. +--- Iterate the DATABASE and call an iterator function for each **alive** player, providing the Unit of the player 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 UNIT parameter. +-- @return #DATABASE self +function DATABASE:ForEachPlayerAlive( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.PLAYERSALIVE ) + + 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 @@ -522,8 +536,20 @@ function DATABASE:ForEachClient( IteratorFunction, ... ) return self end +--- Iterate the DATABASE and call an iterator function for each **ALIVE** 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 CLIENT in the database. The function needs to accept a CLIENT parameter. +-- @return #DATABASE self +function DATABASE:ForEachClientAlive( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.CLIENTSALIVE ) -function DATABASE:ScanEnvironment() + return self +end + + +function DATABASE:_RegisterTemplates() self:F2() self.Navpoints = {} @@ -586,110 +612,9 @@ function DATABASE:ScanEnvironment() 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 - self:_RegisterDatabase() - self:_RegisterPlayers() - return self end ---- --- @param #DATABASE self --- @param DCSUnit#Unit DCSUnit --- @return #DATABASE self -function DATABASE:_IsIncludeDCSUnit( DCSUnit ) - self:F2( DCSUnit ) - local DCSUnitInclude = true - - if self.Filter.Coalitions then - local DCSUnitCoalition = false - for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do - self:T2( { "Coalition:", DCSUnit:getCoalition(), self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) - if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == DCSUnit:getCoalition() then - DCSUnitCoalition = true - end - end - DCSUnitInclude = DCSUnitInclude and DCSUnitCoalition - end - - if self.Filter.Categories then - local DCSUnitCategory = false - for CategoryID, CategoryName in pairs( self.Filter.Categories ) do - self:T2( { "Category:", DCSUnit:getDesc().category, self.FilterMeta.Categories[CategoryName], CategoryName } ) - if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == DCSUnit:getDesc().category then - DCSUnitCategory = true - end - end - DCSUnitInclude = DCSUnitInclude and DCSUnitCategory - end - - if self.Filter.Types then - local DCSUnitType = false - for TypeID, TypeName in pairs( self.Filter.Types ) do - self:T2( { "Type:", DCSUnit:getTypeName(), TypeName } ) - if TypeName == DCSUnit:getTypeName() then - DCSUnitType = true - end - end - DCSUnitInclude = DCSUnitInclude and DCSUnitType - end - - if self.Filter.Countries then - local DCSUnitCountry = false - for CountryID, CountryName in pairs( self.Filter.Countries ) do - self:T2( { "Country:", DCSUnit:getCountry(), CountryName } ) - if country.id[CountryName] == DCSUnit:getCountry() then - DCSUnitCountry = true - end - end - DCSUnitInclude = DCSUnitInclude and DCSUnitCountry - end - - if self.Filter.UnitPrefixes then - local DCSUnitPrefix = false - for UnitPrefixId, UnitPrefix in pairs( self.Filter.UnitPrefixes ) do - self:T2( { "Unit Prefix:", string.find( DCSUnit:getName(), UnitPrefix, 1 ), UnitPrefix } ) - if string.find( DCSUnit:getName(), UnitPrefix, 1 ) then - DCSUnitPrefix = true - end - end - DCSUnitInclude = DCSUnitInclude and DCSUnitPrefix - end - - self:T2( DCSUnitInclude ) - return DCSUnitInclude -end - ---- --- @param #DATABASE self --- @param DCSUnit#Unit DCSUnit --- @return #DATABASE self -function DATABASE:_IsAliveDCSUnit( DCSUnit ) - self:F2( DCSUnit ) - local DCSUnitAlive = false - if DCSUnit and DCSUnit:isExist() and DCSUnit:isActive() then - if self.DCSUnits[DCSUnit:getName()] then - DCSUnitAlive = true - end - end - self:T2( DCSUnitAlive ) - return DCSUnitAlive -end - ---- --- @param #DATABASE self --- @param DCSGroup#Group DCSGroup --- @return #DATABASE self -function DATABASE:_IsAliveDCSGroup( DCSGroup ) - self:F2( DCSGroup ) - local DCSGroupAlive = false - if DCSGroup and DCSGroup:isExist() then - if self.DCSGroups[DCSGroup:getName()] then - DCSGroupAlive = true - end - end - self:T2( DCSGroupAlive ) - return DCSGroupAlive -end diff --git a/Moose Development/Moose/Moose.lua b/Moose Development/Moose/Moose.lua index b9df585ce..123f51481 100644 --- a/Moose Development/Moose/Moose.lua +++ b/Moose Development/Moose/Moose.lua @@ -11,5 +11,5 @@ Include.File( "Event" ) _EVENTDISPATCHER = EVENT:New() -- #EVENT --- Declare the main database object, which is used internally by the MOOSE classes. -_DATABASE = DATABASE:New():ScanEnvironment() -- Database#DATABASE +_DATABASE = DATABASE:New() -- Database#DATABASE diff --git a/Moose Development/Moose/Set.lua b/Moose Development/Moose/Set.lua index 6b0ba0260..feff204a4 100644 --- a/Moose Development/Moose/Set.lua +++ b/Moose Development/Moose/Set.lua @@ -325,7 +325,7 @@ end --- Flushes the current SET contents in the log ... (for debug reasons). -- @param #SET self --- @return #SET self +-- @return #string A string with the names of the objects. function SET:Flush() self:F3() @@ -334,6 +334,8 @@ function SET:Flush() ObjectNames = ObjectNames .. ObjectName .. ", " end self:T( { "Objects in Set:", ObjectNames } ) + + return ObjectNames end diff --git a/Moose Test Missions/Moose_Test_DATABASE/Moose_Test_DATABASE.lua b/Moose Test Missions/Moose_Test_DATABASE/Moose_Test_DATABASE.lua index cbea27ae9..d299d3501 100644 --- a/Moose Test Missions/Moose_Test_DATABASE/Moose_Test_DATABASE.lua +++ b/Moose Test Missions/Moose_Test_DATABASE/Moose_Test_DATABASE.lua @@ -88,6 +88,21 @@ DBRedVehicles :ForEachUnit( function( MooseUnit ) DBRedVehicles:T( MooseUnit:GetName() ) end ) + +local function FlushPlayers() + + _DATABASE:E( "FlushPlayers" ) + _DATABASE + :ForEachPlayerAlive( function( Player ) + _DATABASE:E( Player ) + MESSAGE:New( Player, "Test", 5, "Player Test" ):ToAll() + return true + end ) + return true +end + +_DATABASE:E( "Schedule" ) +local PlayerShow = SCHEDULER:New( nil, FlushPlayers, {}, 1, 10 ) diff --git a/Moose Test Missions/Moose_Test_DATABASE/Moose_Test_DATABASE.miz b/Moose Test Missions/Moose_Test_DATABASE/Moose_Test_DATABASE.miz index 7f9c38b8f44683700581f2ac62ab650541703abc..35621d8d47379b72dda92f0f740f4a5442d94ba8 100644 GIT binary patch delta 24800 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 delta 15804 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 Date: Fri, 27 May 2016 13:34:25 +0200 Subject: [PATCH 4/5] DATABASE and SETS publish first version. --- .../l10n/DEFAULT/Moose.lua | 16659 +++++++++++++++- Moose Mission Setup/Moose.lua | 16659 +++++++++++++++- .../Moose_Test_CLEANUP/Moose_Test_CLEANUP.miz | Bin 98334 -> 199246 bytes .../Moose_Test_DATABASE.miz | Bin 38047 -> 138994 bytes .../Moose_Test_DESTROY/MOOSE_Test_DESTROY.miz | Bin 33518 -> 134430 bytes .../Moose_Test_ESCORT/MOOSE_Test_ESCORT.miz | Bin 59173 -> 160085 bytes .../Moose_Test_MISSILETRAINER.miz | Bin 118467 -> 219379 bytes .../Moose_Test_SEAD/MOOSE_Test_SEAD.miz | Bin 25771 -> 126683 bytes .../Moose_Test_SPAWN/MOOSE_Test_SPAWN.miz | Bin 51188 -> 152100 bytes .../MOOSE_Test_SPAWN_Repeat.miz | Bin 25332 -> 126244 bytes .../MOOSE_Test_TASK_Pickup_and_Deploy.miz | Bin 31933 -> 132845 bytes .../Moose_Test_WRAPPER/Moose_Test_WRAPPER.miz | Bin 40952 -> 141864 bytes 12 files changed, 33272 insertions(+), 46 deletions(-) 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 d4c566d6b..6339b2e01 100644 --- a/Moose Mission Setup/Moose Mission Update/l10n/DEFAULT/Moose.lua +++ b/Moose Mission Setup/Moose Mission Update/l10n/DEFAULT/Moose.lua @@ -1,6 +1,5 @@ -env.info( '*** MOOSE DYNAMIC INCLUDE START *** ' ) -env.info( 'Moose Generation Timestamp: 20160527_1003' ) - +env.info( '*** MOOSE STATIC INCLUDE START *** ' ) +env.info( 'Moose Generation Timestamp: 20160527_1334' ) local base = _G env.info("Loading MOOSE " .. base.timer.getAbsTime() ) @@ -12,27 +11,9 @@ Include.Path = function() end Include.File = function( IncludeFile ) - if not Include.Files[ IncludeFile ] then - Include.Files[IncludeFile] = IncludeFile - env.info( "Include:" .. IncludeFile .. " from " .. Include.ProgramPath ) - local f = assert( base.loadfile( Include.ProgramPath .. IncludeFile .. ".lua" ) ) - if f == nil then - env.info( "Include:" .. IncludeFile .. " from " .. Include.MissionPath ) - local f = assert( base.loadfile( Include.MissionPath .. IncludeFile .. ".lua" ) ) - if f == nil then - error ("Could not load MOOSE file " .. IncludeFile .. ".lua" ) - else - env.info( "Include:" .. IncludeFile .. " loaded from " .. Include.MissionPath ) - return f() - end - else - env.info( "Include:" .. IncludeFile .. " loaded from " .. Include.ProgramPath ) - return f() - end - end end -Include.ProgramPath = "Scripts/Moose/" +Include.ProgramPath = "Scripts/Moose/Moose/" Include.MissionPath = Include.Path() env.info( "Include.ProgramPath = " .. Include.ProgramPath) @@ -42,4 +23,16636 @@ Include.Files = {} Include.File( "Moose" ) -env.info("Loaded MOOSE Include Engine")env.info( '*** MOOSE INCLUDE END *** ' ) +env.info("Loaded MOOSE Include Engine") +--- Various routines +-- @module routines +-- @author Flightcontrol + +--Include.File( "Trace" ) +--Include.File( "Message" ) + + +env.setErrorMessageBoxEnabled(false) + +--- Extract of MIST functions. +-- @author Grimes + +routines = {} + + +-- don't change these +routines.majorVersion = 3 +routines.minorVersion = 3 +routines.build = 22 + +----------------------------------------------------------------------------------------------------------------- + +---------------------------------------------------------------------------------------------- +-- Utils- conversion, Lua utils, etc. +routines.utils = {} + +--from http://lua-users.org/wiki/CopyTable +routines.utils.deepCopy = function(object) + local lookup_table = {} + local function _copy(object) + if type(object) ~= "table" then + return object + elseif lookup_table[object] then + return lookup_table[object] + end + local new_table = {} + lookup_table[object] = new_table + for index, value in pairs(object) do + new_table[_copy(index)] = _copy(value) + end + return setmetatable(new_table, getmetatable(object)) + end + local objectreturn = _copy(object) + return objectreturn +end + + +-- porting in Slmod's serialize_slmod2 +routines.utils.oneLineSerialize = function(tbl) -- serialization of a table all on a single line, no comments, made to replace old get_table_string function + + lookup_table = {} + + local function _Serialize( tbl ) + + if type(tbl) == 'table' then --function only works for tables! + + if lookup_table[tbl] then + return lookup_table[object] + end + + local tbl_str = {} + + lookup_table[tbl] = tbl_str + + tbl_str[#tbl_str + 1] = '{' + + for ind,val in pairs(tbl) do -- serialize its fields + local ind_str = {} + if type(ind) == "number" then + ind_str[#ind_str + 1] = '[' + ind_str[#ind_str + 1] = tostring(ind) + ind_str[#ind_str + 1] = ']=' + else --must be a string + ind_str[#ind_str + 1] = '[' + ind_str[#ind_str + 1] = routines.utils.basicSerialize(ind) + ind_str[#ind_str + 1] = ']=' + end + + local val_str = {} + if ((type(val) == 'number') or (type(val) == 'boolean')) then + val_str[#val_str + 1] = tostring(val) + val_str[#val_str + 1] = ',' + tbl_str[#tbl_str + 1] = table.concat(ind_str) + tbl_str[#tbl_str + 1] = table.concat(val_str) + elseif type(val) == 'string' then + val_str[#val_str + 1] = routines.utils.basicSerialize(val) + val_str[#val_str + 1] = ',' + tbl_str[#tbl_str + 1] = table.concat(ind_str) + tbl_str[#tbl_str + 1] = table.concat(val_str) + elseif type(val) == 'nil' then -- won't ever happen, right? + val_str[#val_str + 1] = 'nil,' + tbl_str[#tbl_str + 1] = table.concat(ind_str) + tbl_str[#tbl_str + 1] = table.concat(val_str) + elseif type(val) == 'table' then + if ind == "__index" then + -- tbl_str[#tbl_str + 1] = "__index" + -- tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it + else + + val_str[#val_str + 1] = _Serialize(val) + val_str[#val_str + 1] = ',' --I think this is right, I just added it + tbl_str[#tbl_str + 1] = table.concat(ind_str) + tbl_str[#tbl_str + 1] = table.concat(val_str) + end + elseif type(val) == 'function' then + -- tbl_str[#tbl_str + 1] = "function " .. tostring(ind) + -- tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it + else +-- env.info('unable to serialize value type ' .. routines.utils.basicSerialize(type(val)) .. ' at index ' .. tostring(ind)) +-- env.info( debug.traceback() ) + end + + end + tbl_str[#tbl_str + 1] = '}' + return table.concat(tbl_str) + else + return tostring(tbl) + end + end + + local objectreturn = _Serialize(tbl) + return objectreturn +end + +--porting in Slmod's "safestring" basic serialize +routines.utils.basicSerialize = function(s) + if s == nil then + return "\"\"" + else + if ((type(s) == 'number') or (type(s) == 'boolean') or (type(s) == 'function') or (type(s) == 'table') or (type(s) == 'userdata') ) then + return tostring(s) + elseif type(s) == 'string' then + s = string.format('%q', s) + return s + end + end +end + + +routines.utils.toDegree = function(angle) + return angle*180/math.pi +end + +routines.utils.toRadian = function(angle) + return angle*math.pi/180 +end + +routines.utils.metersToNM = function(meters) + return meters/1852 +end + +routines.utils.metersToFeet = function(meters) + return meters/0.3048 +end + +routines.utils.NMToMeters = function(NM) + return NM*1852 +end + +routines.utils.feetToMeters = function(feet) + return feet*0.3048 +end + +routines.utils.mpsToKnots = function(mps) + return mps*3600/1852 +end + +routines.utils.mpsToKmph = function(mps) + return mps*3.6 +end + +routines.utils.knotsToMps = function(knots) + return knots*1852/3600 +end + +routines.utils.kmphToMps = function(kmph) + return kmph/3.6 +end + +function routines.utils.makeVec2(Vec3) + if Vec3.z then + return {x = Vec3.x, y = Vec3.z} + else + return {x = Vec3.x, y = Vec3.y} -- it was actually already vec2. + end +end + +function routines.utils.makeVec3(Vec2, y) + if not Vec2.z then + if not y then + y = 0 + end + return {x = Vec2.x, y = y, z = Vec2.y} + else + return {x = Vec2.x, y = Vec2.y, z = Vec2.z} -- it was already Vec3, actually. + end +end + +function routines.utils.makeVec3GL(Vec2, offset) + local adj = offset or 0 + + if not Vec2.z then + return {x = Vec2.x, y = (land.getHeight(Vec2) + adj), z = Vec2.y} + else + return {x = Vec2.x, y = (land.getHeight({x = Vec2.x, y = Vec2.z}) + adj), z = Vec2.z} + end +end + +routines.utils.zoneToVec3 = function(zone) + local new = {} + if type(zone) == 'table' and zone.point then + new.x = zone.point.x + new.y = zone.point.y + new.z = zone.point.z + return new + elseif type(zone) == 'string' then + zone = trigger.misc.getZone(zone) + if zone then + new.x = zone.point.x + new.y = zone.point.y + new.z = zone.point.z + return new + end + end +end + +-- gets heading-error corrected direction from point along vector vec. +function routines.utils.getDir(vec, point) + local dir = math.atan2(vec.z, vec.x) + dir = dir + routines.getNorthCorrection(point) + if dir < 0 then + dir = dir + 2*math.pi -- put dir in range of 0 to 2*pi + end + return dir +end + +-- gets distance in meters between two points (2 dimensional) +function routines.utils.get2DDist(point1, point2) + point1 = routines.utils.makeVec3(point1) + point2 = routines.utils.makeVec3(point2) + return routines.vec.mag({x = point1.x - point2.x, y = 0, z = point1.z - point2.z}) +end + +-- gets distance in meters between two points (3 dimensional) +function routines.utils.get3DDist(point1, point2) + return routines.vec.mag({x = point1.x - point2.x, y = point1.y - point2.y, z = point1.z - point2.z}) +end + + + +-- From http://lua-users.org/wiki/SimpleRound +-- use negative idp for rounding ahead of decimal place, positive for rounding after decimal place +routines.utils.round = function(num, idp) + local mult = 10^(idp or 0) + return math.floor(num * mult + 0.5) / mult +end + +-- porting in Slmod's dostring +routines.utils.dostring = function(s) + local f, err = loadstring(s) + if f then + return true, f() + else + return false, err + end +end + + +--3D Vector manipulation +routines.vec = {} + +routines.vec.add = function(vec1, vec2) + return {x = vec1.x + vec2.x, y = vec1.y + vec2.y, z = vec1.z + vec2.z} +end + +routines.vec.sub = function(vec1, vec2) + return {x = vec1.x - vec2.x, y = vec1.y - vec2.y, z = vec1.z - vec2.z} +end + +routines.vec.scalarMult = function(vec, mult) + return {x = vec.x*mult, y = vec.y*mult, z = vec.z*mult} +end + +routines.vec.scalar_mult = routines.vec.scalarMult + +routines.vec.dp = function(vec1, vec2) + return vec1.x*vec2.x + vec1.y*vec2.y + vec1.z*vec2.z +end + +routines.vec.cp = function(vec1, vec2) + return { x = vec1.y*vec2.z - vec1.z*vec2.y, y = vec1.z*vec2.x - vec1.x*vec2.z, z = vec1.x*vec2.y - vec1.y*vec2.x} +end + +routines.vec.mag = function(vec) + return (vec.x^2 + vec.y^2 + vec.z^2)^0.5 +end + +routines.vec.getUnitVec = function(vec) + local mag = routines.vec.mag(vec) + return { x = vec.x/mag, y = vec.y/mag, z = vec.z/mag } +end + +routines.vec.rotateVec2 = function(vec2, theta) + return { x = vec2.x*math.cos(theta) - vec2.y*math.sin(theta), y = vec2.x*math.sin(theta) + vec2.y*math.cos(theta)} +end +--------------------------------------------------------------------------------------------------------------------------- + + + + +-- acc- the accuracy of each easting/northing. 0, 1, 2, 3, 4, or 5. +routines.tostringMGRS = function(MGRS, acc) + if acc == 0 then + return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph + else + return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph .. ' ' .. string.format('%0' .. acc .. 'd', routines.utils.round(MGRS.Easting/(10^(5-acc)), 0)) + .. ' ' .. string.format('%0' .. acc .. 'd', routines.utils.round(MGRS.Northing/(10^(5-acc)), 0)) + end +end + +--[[acc: +in DM: decimal point of minutes. +In DMS: decimal point of seconds. +position after the decimal of the least significant digit: +So: +42.32 - acc of 2. +]] +routines.tostringLL = function(lat, lon, acc, DMS) + + local latHemi, lonHemi + if lat > 0 then + latHemi = 'N' + else + latHemi = 'S' + end + + if lon > 0 then + lonHemi = 'E' + else + lonHemi = 'W' + end + + lat = math.abs(lat) + lon = math.abs(lon) + + local latDeg = math.floor(lat) + local latMin = (lat - latDeg)*60 + + local lonDeg = math.floor(lon) + local lonMin = (lon - lonDeg)*60 + + if DMS then -- degrees, minutes, and seconds. + local oldLatMin = latMin + latMin = math.floor(latMin) + local latSec = routines.utils.round((oldLatMin - latMin)*60, acc) + + local oldLonMin = lonMin + lonMin = math.floor(lonMin) + local lonSec = routines.utils.round((oldLonMin - lonMin)*60, acc) + + if latSec == 60 then + latSec = 0 + latMin = latMin + 1 + end + + if lonSec == 60 then + lonSec = 0 + lonMin = lonMin + 1 + end + + local secFrmtStr -- create the formatting string for the seconds place + if acc <= 0 then -- no decimal place. + secFrmtStr = '%02d' + else + local width = 3 + acc -- 01.310 - that's a width of 6, for example. + secFrmtStr = '%0' .. width .. '.' .. acc .. 'f' + end + + return string.format('%02d', latDeg) .. ' ' .. string.format('%02d', latMin) .. '\' ' .. string.format(secFrmtStr, latSec) .. '"' .. latHemi .. ' ' + .. string.format('%02d', lonDeg) .. ' ' .. string.format('%02d', lonMin) .. '\' ' .. string.format(secFrmtStr, lonSec) .. '"' .. lonHemi + + else -- degrees, decimal minutes. + latMin = routines.utils.round(latMin, acc) + lonMin = routines.utils.round(lonMin, acc) + + if latMin == 60 then + latMin = 0 + latDeg = latDeg + 1 + end + + if lonMin == 60 then + lonMin = 0 + lonDeg = lonDeg + 1 + end + + local minFrmtStr -- create the formatting string for the minutes place + if acc <= 0 then -- no decimal place. + minFrmtStr = '%02d' + else + local width = 3 + acc -- 01.310 - that's a width of 6, for example. + minFrmtStr = '%0' .. width .. '.' .. acc .. 'f' + end + + return string.format('%02d', latDeg) .. ' ' .. string.format(minFrmtStr, latMin) .. '\'' .. latHemi .. ' ' + .. string.format('%02d', lonDeg) .. ' ' .. string.format(minFrmtStr, lonMin) .. '\'' .. lonHemi + + end +end + +--[[ required: az - radian + required: dist - meters + optional: alt - meters (set to false or nil if you don't want to use it). + optional: metric - set true to get dist and alt in km and m. + precision will always be nearest degree and NM or km.]] +routines.tostringBR = function(az, dist, alt, metric) + az = routines.utils.round(routines.utils.toDegree(az), 0) + + if metric then + dist = routines.utils.round(dist/1000, 2) + else + dist = routines.utils.round(routines.utils.metersToNM(dist), 2) + end + + local s = string.format('%03d', az) .. ' for ' .. dist + + if alt then + if metric then + s = s .. ' at ' .. routines.utils.round(alt, 0) + else + s = s .. ' at ' .. routines.utils.round(routines.utils.metersToFeet(alt), 0) + end + end + return s +end + +routines.getNorthCorrection = function(point) --gets the correction needed for true north + if not point.z then --Vec2; convert to Vec3 + point.z = point.y + point.y = 0 + end + local lat, lon = coord.LOtoLL(point) + local north_posit = coord.LLtoLO(lat + 1, lon) + return math.atan2(north_posit.z - point.z, north_posit.x - point.x) +end + + +-- the main area +do + -- THE MAIN FUNCTION -- Accessed 100 times/sec. + routines.main = function() + timer.scheduleFunction(routines.main, {}, timer.getTime() + 2) --reschedule first in case of Lua error + ---------------------------------------------------------------------------------------------------------- + --area to add new stuff in + + routines.do_scheduled_functions() + end -- end of routines.main + + timer.scheduleFunction(routines.main, {}, timer.getTime() + 2) + +end + + +do + local idNum = 0 + + --Simplified event handler + routines.addEventHandler = function(f) --id is optional! + local handler = {} + idNum = idNum + 1 + handler.id = idNum + handler.f = f + handler.onEvent = function(self, event) + self.f(event) + end + world.addEventHandler(handler) + end + + routines.removeEventHandler = function(id) + for key, handler in pairs(world.eventHandlers) do + if handler.id and handler.id == id then + world.eventHandlers[key] = nil + return true + end + end + return false + end +end + +-- need to return a Vec3 or Vec2? +function routines.getRandPointInCircle(point, radius, innerRadius) + local theta = 2*math.pi*math.random() + local rad = math.random() + math.random() + if rad > 1 then + rad = 2 - rad + end + + local radMult + if innerRadius and innerRadius <= radius then + radMult = (radius - innerRadius)*rad + innerRadius + else + radMult = radius*rad + end + + if not point.z then --might as well work with vec2/3 + point.z = point.y + end + + local rndCoord + if radius > 0 then + rndCoord = {x = math.cos(theta)*radMult + point.x, y = math.sin(theta)*radMult + point.z} + else + rndCoord = {x = point.x, y = point.z} + end + return rndCoord +end + +routines.goRoute = function(group, path) + local misTask = { + id = 'Mission', + params = { + route = { + points = routines.utils.deepCopy(path), + }, + }, + } + if type(group) == 'string' then + group = Group.getByName(group) + end + local groupCon = group:getController() + if groupCon then + groupCon:setTask(misTask) + return true + end + + Controller.setTask(groupCon, misTask) + return false +end + + +-- Useful atomic functions from mist, ported. + +routines.ground = {} +routines.fixedWing = {} +routines.heli = {} + +routines.ground.buildWP = function(point, overRideForm, overRideSpeed) + + local wp = {} + wp.x = point.x + + if point.z then + wp.y = point.z + else + wp.y = point.y + end + local form, speed + + if point.speed and not overRideSpeed then + wp.speed = point.speed + elseif type(overRideSpeed) == 'number' then + wp.speed = overRideSpeed + else + wp.speed = routines.utils.kmphToMps(20) + end + + if point.form and not overRideForm then + form = point.form + else + form = overRideForm + end + + if not form then + wp.action = 'Cone' + else + form = string.lower(form) + if form == 'off_road' or form == 'off road' then + wp.action = 'Off Road' + elseif form == 'on_road' or form == 'on road' then + wp.action = 'On Road' + elseif form == 'rank' or form == 'line_abrest' or form == 'line abrest' or form == 'lineabrest'then + wp.action = 'Rank' + elseif form == 'cone' then + wp.action = 'Cone' + elseif form == 'diamond' then + wp.action = 'Diamond' + elseif form == 'vee' then + wp.action = 'Vee' + elseif form == 'echelon_left' or form == 'echelon left' or form == 'echelonl' then + wp.action = 'EchelonL' + elseif form == 'echelon_right' or form == 'echelon right' or form == 'echelonr' then + wp.action = 'EchelonR' + else + wp.action = 'Cone' -- if nothing matched + end + end + + wp.type = 'Turning Point' + + return wp + +end + +routines.fixedWing.buildWP = function(point, WPtype, speed, alt, altType) + + local wp = {} + wp.x = point.x + + if point.z then + wp.y = point.z + else + wp.y = point.y + end + + if alt and type(alt) == 'number' then + wp.alt = alt + else + wp.alt = 2000 + end + + if altType then + altType = string.lower(altType) + if altType == 'radio' or 'agl' then + wp.alt_type = 'RADIO' + elseif altType == 'baro' or 'asl' then + wp.alt_type = 'BARO' + end + else + wp.alt_type = 'RADIO' + end + + if point.speed then + speed = point.speed + end + + if point.type then + WPtype = point.type + end + + if not speed then + wp.speed = routines.utils.kmphToMps(500) + else + wp.speed = speed + end + + if not WPtype then + wp.action = 'Turning Point' + else + WPtype = string.lower(WPtype) + if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then + wp.action = 'Fly Over Point' + elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then + wp.action = 'Turning Point' + else + wp.action = 'Turning Point' + end + end + + wp.type = 'Turning Point' + return wp +end + +routines.heli.buildWP = function(point, WPtype, speed, alt, altType) + + local wp = {} + wp.x = point.x + + if point.z then + wp.y = point.z + else + wp.y = point.y + end + + if alt and type(alt) == 'number' then + wp.alt = alt + else + wp.alt = 500 + end + + if altType then + altType = string.lower(altType) + if altType == 'radio' or 'agl' then + wp.alt_type = 'RADIO' + elseif altType == 'baro' or 'asl' then + wp.alt_type = 'BARO' + end + else + wp.alt_type = 'RADIO' + end + + if point.speed then + speed = point.speed + end + + if point.type then + WPtype = point.type + end + + if not speed then + wp.speed = routines.utils.kmphToMps(200) + else + wp.speed = speed + end + + if not WPtype then + wp.action = 'Turning Point' + else + WPtype = string.lower(WPtype) + if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then + wp.action = 'Fly Over Point' + elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then + wp.action = 'Turning Point' + else + wp.action = 'Turning Point' + end + end + + wp.type = 'Turning Point' + return wp +end + +routines.groupToRandomPoint = function(vars) + local group = vars.group --Required + local point = vars.point --required + local radius = vars.radius or 0 + local innerRadius = vars.innerRadius + local form = vars.form or 'Cone' + local heading = vars.heading or math.random()*2*math.pi + local headingDegrees = vars.headingDegrees + local speed = vars.speed or routines.utils.kmphToMps(20) + + + local useRoads + if not vars.disableRoads then + useRoads = true + else + useRoads = false + end + + local path = {} + + if headingDegrees then + heading = headingDegrees*math.pi/180 + end + + if heading >= 2*math.pi then + heading = heading - 2*math.pi + end + + local rndCoord = routines.getRandPointInCircle(point, radius, innerRadius) + + local offset = {} + local posStart = routines.getLeadPos(group) + + offset.x = routines.utils.round(math.sin(heading - (math.pi/2)) * 50 + rndCoord.x, 3) + offset.z = routines.utils.round(math.cos(heading + (math.pi/2)) * 50 + rndCoord.y, 3) + path[#path + 1] = routines.ground.buildWP(posStart, form, speed) + + + if useRoads == true and ((point.x - posStart.x)^2 + (point.z - posStart.z)^2)^0.5 > radius * 1.3 then + path[#path + 1] = routines.ground.buildWP({['x'] = posStart.x + 11, ['z'] = posStart.z + 11}, 'off_road', speed) + path[#path + 1] = routines.ground.buildWP(posStart, 'on_road', speed) + path[#path + 1] = routines.ground.buildWP(offset, 'on_road', speed) + else + path[#path + 1] = routines.ground.buildWP({['x'] = posStart.x + 25, ['z'] = posStart.z + 25}, form, speed) + end + + path[#path + 1] = routines.ground.buildWP(offset, form, speed) + path[#path + 1] = routines.ground.buildWP(rndCoord, form, speed) + + routines.goRoute(group, path) + + return +end + +routines.groupRandomDistSelf = function(gpData, dist, form, heading, speed) + local pos = routines.getLeadPos(gpData) + local fakeZone = {} + fakeZone.radius = dist or math.random(300, 1000) + fakeZone.point = {x = pos.x, y, pos.y, z = pos.z} + routines.groupToRandomZone(gpData, fakeZone, form, heading, speed) + + return +end + +routines.groupToRandomZone = function(gpData, zone, form, heading, speed) + if type(gpData) == 'string' then + gpData = Group.getByName(gpData) + end + + if type(zone) == 'string' then + zone = trigger.misc.getZone(zone) + elseif type(zone) == 'table' and not zone.radius then + zone = trigger.misc.getZone(zone[math.random(1, #zone)]) + end + + if speed then + speed = routines.utils.kmphToMps(speed) + end + + local vars = {} + vars.group = gpData + vars.radius = zone.radius + vars.form = form + vars.headingDegrees = heading + vars.speed = speed + vars.point = routines.utils.zoneToVec3(zone) + + routines.groupToRandomPoint(vars) + + return +end + +routines.isTerrainValid = function(coord, terrainTypes) -- vec2/3 and enum or table of acceptable terrain types + if coord.z then + coord.y = coord.z + end + local typeConverted = {} + + if type(terrainTypes) == 'string' then -- if its a string it does this check + for constId, constData in pairs(land.SurfaceType) do + if string.lower(constId) == string.lower(terrainTypes) or string.lower(constData) == string.lower(terrainTypes) then + table.insert(typeConverted, constId) + end + end + elseif type(terrainTypes) == 'table' then -- if its a table it does this check + for typeId, typeData in pairs(terrainTypes) do + for constId, constData in pairs(land.SurfaceType) do + if string.lower(constId) == string.lower(typeData) or string.lower(constData) == string.lower(typeId) then + table.insert(typeConverted, constId) + end + end + end + end + for validIndex, validData in pairs(typeConverted) do + if land.getSurfaceType(coord) == land.SurfaceType[validData] then + return true + end + end + return false +end + +routines.groupToPoint = function(gpData, point, form, heading, speed, useRoads) + if type(point) == 'string' then + point = trigger.misc.getZone(point) + end + if speed then + speed = routines.utils.kmphToMps(speed) + end + + local vars = {} + vars.group = gpData + vars.form = form + vars.headingDegrees = heading + vars.speed = speed + vars.disableRoads = useRoads + vars.point = routines.utils.zoneToVec3(point) + routines.groupToRandomPoint(vars) + + return +end + + +routines.getLeadPos = function(group) + if type(group) == 'string' then -- group name + group = Group.getByName(group) + end + + local units = group:getUnits() + + local leader = units[1] + if not leader then -- SHOULD be good, but if there is a bug, this code future-proofs it then. + local lowestInd = math.huge + for ind, unit in pairs(units) do + if ind < lowestInd then + lowestInd = ind + leader = unit + end + end + end + if leader and Unit.isExist(leader) then -- maybe a little too paranoid now... + return leader:getPosition().p + end +end + +--[[ vars for routines.getMGRSString: +vars.units - table of unit names (NOT unitNameTable- maybe this should change). +vars.acc - integer between 0 and 5, inclusive +]] +routines.getMGRSString = function(vars) + local units = vars.units + local acc = vars.acc or 5 + local avgPos = routines.getAvgPos(units) + if avgPos then + return routines.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(avgPos)), acc) + end +end + +--[[ vars for routines.getLLString +vars.units - table of unit names (NOT unitNameTable- maybe this should change). +vars.acc - integer, number of numbers after decimal place +vars.DMS - if true, output in degrees, minutes, seconds. Otherwise, output in degrees, minutes. + + +]] +routines.getLLString = function(vars) + local units = vars.units + local acc = vars.acc or 3 + local DMS = vars.DMS + local avgPos = routines.getAvgPos(units) + if avgPos then + local lat, lon = coord.LOtoLL(avgPos) + return routines.tostringLL(lat, lon, acc, DMS) + end +end + +--[[ +vars.zone - table of a zone name. +vars.ref - vec3 ref point, maybe overload for vec2 as well? +vars.alt - boolean, if used, includes altitude in string +vars.metric - boolean, gives distance in km instead of NM. +]] +routines.getBRStringZone = function(vars) + local zone = trigger.misc.getZone( vars.zone ) + local ref = routines.utils.makeVec3(vars.ref, 0) -- turn it into Vec3 if it is not already. + local alt = vars.alt + local metric = vars.metric + if zone then + local vec = {x = zone.point.x - ref.x, y = zone.point.y - ref.y, z = zone.point.z - ref.z} + local dir = routines.utils.getDir(vec, ref) + local dist = routines.utils.get2DDist(zone.point, ref) + if alt then + alt = zone.y + end + return routines.tostringBR(dir, dist, alt, metric) + else + env.info( 'routines.getBRStringZone: error: zone is nil' ) + end +end + +--[[ +vars.units- table of unit names (NOT unitNameTable- maybe this should change). +vars.ref - vec3 ref point, maybe overload for vec2 as well? +vars.alt - boolean, if used, includes altitude in string +vars.metric - boolean, gives distance in km instead of NM. +]] +routines.getBRString = function(vars) + local units = vars.units + local ref = routines.utils.makeVec3(vars.ref, 0) -- turn it into Vec3 if it is not already. + local alt = vars.alt + local metric = vars.metric + local avgPos = routines.getAvgPos(units) + if avgPos then + local vec = {x = avgPos.x - ref.x, y = avgPos.y - ref.y, z = avgPos.z - ref.z} + local dir = routines.utils.getDir(vec, ref) + local dist = routines.utils.get2DDist(avgPos, ref) + if alt then + alt = avgPos.y + end + return routines.tostringBR(dir, dist, alt, metric) + end +end + + +-- Returns the Vec3 coordinates of the average position of the concentration of units most in the heading direction. +--[[ vars for routines.getLeadingPos: +vars.units - table of unit names +vars.heading - direction +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees +]] +routines.getLeadingPos = function(vars) + local units = vars.units + local heading = vars.heading + local radius = vars.radius + if vars.headingDegrees then + heading = routines.utils.toRadian(vars.headingDegrees) + end + + local unitPosTbl = {} + for i = 1, #units do + local unit = Unit.getByName(units[i]) + if unit and unit:isExist() then + unitPosTbl[#unitPosTbl + 1] = unit:getPosition().p + end + end + if #unitPosTbl > 0 then -- one more more units found. + -- first, find the unit most in the heading direction + local maxPos = -math.huge + + local maxPosInd -- maxPos - the furthest in direction defined by heading; maxPosInd = + for i = 1, #unitPosTbl do + local rotatedVec2 = routines.vec.rotateVec2(routines.utils.makeVec2(unitPosTbl[i]), heading) + if (not maxPos) or maxPos < rotatedVec2.x then + maxPos = rotatedVec2.x + maxPosInd = i + end + end + + --now, get all the units around this unit... + local avgPos + if radius then + local maxUnitPos = unitPosTbl[maxPosInd] + local avgx, avgy, avgz, totNum = 0, 0, 0, 0 + for i = 1, #unitPosTbl do + if routines.utils.get2DDist(maxUnitPos, unitPosTbl[i]) <= radius then + avgx = avgx + unitPosTbl[i].x + avgy = avgy + unitPosTbl[i].y + avgz = avgz + unitPosTbl[i].z + totNum = totNum + 1 + end + end + avgPos = { x = avgx/totNum, y = avgy/totNum, z = avgz/totNum} + else + avgPos = unitPosTbl[maxPosInd] + end + + return avgPos + end +end + + +--[[ vars for routines.getLeadingMGRSString: +vars.units - table of unit names +vars.heading - direction +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees +vars.acc - number, 0 to 5. +]] +routines.getLeadingMGRSString = function(vars) + local pos = routines.getLeadingPos(vars) + if pos then + local acc = vars.acc or 5 + return routines.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(pos)), acc) + end +end + +--[[ vars for routines.getLeadingLLString: +vars.units - table of unit names +vars.heading - direction, number +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees +vars.acc - number of digits after decimal point (can be negative) +vars.DMS - boolean, true if you want DMS. +]] +routines.getLeadingLLString = function(vars) + local pos = routines.getLeadingPos(vars) + if pos then + local acc = vars.acc or 3 + local DMS = vars.DMS + local lat, lon = coord.LOtoLL(pos) + return routines.tostringLL(lat, lon, acc, DMS) + end +end + + + +--[[ vars for routines.getLeadingBRString: +vars.units - table of unit names +vars.heading - direction, number +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees +vars.metric - boolean, if true, use km instead of NM. +vars.alt - boolean, if true, include altitude. +vars.ref - vec3/vec2 reference point. +]] +routines.getLeadingBRString = function(vars) + local pos = routines.getLeadingPos(vars) + if pos then + local ref = vars.ref + local alt = vars.alt + local metric = vars.metric + + local vec = {x = pos.x - ref.x, y = pos.y - ref.y, z = pos.z - ref.z} + local dir = routines.utils.getDir(vec, ref) + local dist = routines.utils.get2DDist(pos, ref) + if alt then + alt = pos.y + end + return routines.tostringBR(dir, dist, alt, metric) + end +end + +--[[ vars for routines.message.add + vars.text = 'Hello World' + vars.displayTime = 20 + vars.msgFor = {coa = {'red'}, countries = {'Ukraine', 'Georgia'}, unitTypes = {'A-10C'}} + +]] + +--[[ vars for routines.msgMGRS +vars.units - table of unit names (NOT unitNameTable- maybe this should change). +vars.acc - integer between 0 and 5, inclusive +vars.text - text in the message +vars.displayTime - self explanatory +vars.msgFor - scope +]] +routines.msgMGRS = function(vars) + local units = vars.units + local acc = vars.acc + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = routines.getMGRSString{units = units, acc = acc} + local newText + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end + + routines.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } +end + +--[[ vars for routines.msgLL +vars.units - table of unit names (NOT unitNameTable- maybe this should change) (Yes). +vars.acc - integer, number of numbers after decimal place +vars.DMS - if true, output in degrees, minutes, seconds. Otherwise, output in degrees, minutes. +vars.text - text in the message +vars.displayTime - self explanatory +vars.msgFor - scope +]] +routines.msgLL = function(vars) + local units = vars.units -- technically, I don't really need to do this, but it helps readability. + local acc = vars.acc + local DMS = vars.DMS + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = routines.getLLString{units = units, acc = acc, DMS = DMS} + local newText + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end + + routines.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } + +end + + +--[[ +vars.units- table of unit names (NOT unitNameTable- maybe this should change). +vars.ref - vec3 ref point, maybe overload for vec2 as well? +vars.alt - boolean, if used, includes altitude in string +vars.metric - boolean, gives distance in km instead of NM. +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] +routines.msgBR = function(vars) + local units = vars.units -- technically, I don't really need to do this, but it helps readability. + local ref = vars.ref -- vec2/vec3 will be handled in routines.getBRString + local alt = vars.alt + local metric = vars.metric + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = routines.getBRString{units = units, ref = ref, alt = alt, metric = metric} + local newText + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end + + routines.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } + +end + + +-------------------------------------------------------------------------------------------- +-- basically, just sub-types of routines.msgBR... saves folks the work of getting the ref point. +--[[ +vars.units- table of unit names (NOT unitNameTable- maybe this should change). +vars.ref - string red, blue +vars.alt - boolean, if used, includes altitude in string +vars.metric - boolean, gives distance in km instead of NM. +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] +routines.msgBullseye = function(vars) + if string.lower(vars.ref) == 'red' then + vars.ref = routines.DBs.missionData.bullseye.red + routines.msgBR(vars) + elseif string.lower(vars.ref) == 'blue' then + vars.ref = routines.DBs.missionData.bullseye.blue + routines.msgBR(vars) + end +end + +--[[ +vars.units- table of unit names (NOT unitNameTable- maybe this should change). +vars.ref - unit name of reference point +vars.alt - boolean, if used, includes altitude in string +vars.metric - boolean, gives distance in km instead of NM. +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] + +routines.msgBRA = function(vars) + if Unit.getByName(vars.ref) then + vars.ref = Unit.getByName(vars.ref):getPosition().p + if not vars.alt then + vars.alt = true + end + routines.msgBR(vars) + end +end +-------------------------------------------------------------------------------------------- + +--[[ vars for routines.msgLeadingMGRS: +vars.units - table of unit names +vars.heading - direction +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees (optional) +vars.acc - number, 0 to 5. +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] +routines.msgLeadingMGRS = function(vars) + local units = vars.units -- technically, I don't really need to do this, but it helps readability. + local heading = vars.heading + local radius = vars.radius + local headingDegrees = vars.headingDegrees + local acc = vars.acc + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = routines.getLeadingMGRSString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc} + local newText + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end + + routines.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } + + +end +--[[ vars for routines.msgLeadingLL: +vars.units - table of unit names +vars.heading - direction, number +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees (optional) +vars.acc - number of digits after decimal point (can be negative) +vars.DMS - boolean, true if you want DMS. (optional) +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] +routines.msgLeadingLL = function(vars) + local units = vars.units -- technically, I don't really need to do this, but it helps readability. + local heading = vars.heading + local radius = vars.radius + local headingDegrees = vars.headingDegrees + local acc = vars.acc + local DMS = vars.DMS + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = routines.getLeadingLLString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc, DMS = DMS} + local newText + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end + + routines.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } + +end + +--[[ +vars.units - table of unit names +vars.heading - direction, number +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees (optional) +vars.metric - boolean, if true, use km instead of NM. (optional) +vars.alt - boolean, if true, include altitude. (optional) +vars.ref - vec3/vec2 reference point. +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] +routines.msgLeadingBR = function(vars) + local units = vars.units -- technically, I don't really need to do this, but it helps readability. + local heading = vars.heading + local radius = vars.radius + local headingDegrees = vars.headingDegrees + local metric = vars.metric + local alt = vars.alt + local ref = vars.ref -- vec2/vec3 will be handled in routines.getBRString + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = routines.getLeadingBRString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, metric = metric, alt = alt, ref = ref} + local newText + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end + + routines.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } +end + + +function spairs(t, order) + -- collect the keys + local keys = {} + for k in pairs(t) do keys[#keys+1] = k end + + -- if order function given, sort by it by passing the table and keys a, b, + -- otherwise just sort the keys + if order then + table.sort(keys, function(a,b) return order(t, a, b) end) + else + table.sort(keys) + end + + -- return the iterator function + local i = 0 + return function() + i = i + 1 + if keys[i] then + return keys[i], t[keys[i]] + end + end +end + + +function routines.IsPartOfGroupInZones( CargoGroup, LandingZones ) +--trace.f() + + local CurrentZoneID = nil + + if CargoGroup then + local CargoUnits = CargoGroup:getUnits() + for CargoUnitID, CargoUnit in pairs( CargoUnits ) do + if CargoUnit and CargoUnit:getLife() >= 1.0 then + CurrentZoneID = routines.IsUnitInZones( CargoUnit, LandingZones ) + if CurrentZoneID then + break + end + end + end + end + +--trace.r( "", "", { CurrentZoneID } ) + return CurrentZoneID +end + + + +function routines.IsUnitInZones( TransportUnit, LandingZones ) +--trace.f("", "routines.IsUnitInZones" ) + + local TransportZoneResult = nil + local TransportZonePos = nil + local TransportZone = nil + + -- fill-up some local variables to support further calculations to determine location of units within the zone. + if TransportUnit then + local TransportUnitPos = TransportUnit:getPosition().p + if type( LandingZones ) == "table" then + for LandingZoneID, LandingZoneName in pairs( LandingZones ) do + TransportZone = trigger.misc.getZone( LandingZoneName ) + if TransportZone then + TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} + if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then + TransportZoneResult = LandingZoneID + break + end + end + end + else + TransportZone = trigger.misc.getZone( LandingZones ) + TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} + if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then + TransportZoneResult = 1 + end + end + if TransportZoneResult then + --trace.i( "routines", "TransportZone:" .. TransportZoneResult ) + else + --trace.i( "routines", "TransportZone:nil logic" ) + end + return TransportZoneResult + else + --trace.i( "routines", "TransportZone:nil hard" ) + return nil + end +end + +function routines.IsUnitNearZonesRadius( TransportUnit, LandingZones, ZoneRadius ) +--trace.f("", "routines.IsUnitInZones" ) + + local TransportZoneResult = nil + local TransportZonePos = nil + local TransportZone = nil + + -- fill-up some local variables to support further calculations to determine location of units within the zone. + if TransportUnit then + local TransportUnitPos = TransportUnit:getPosition().p + if type( LandingZones ) == "table" then + for LandingZoneID, LandingZoneName in pairs( LandingZones ) do + TransportZone = trigger.misc.getZone( LandingZoneName ) + if TransportZone then + TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} + if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= ZoneRadius ) then + TransportZoneResult = LandingZoneID + break + end + end + end + else + TransportZone = trigger.misc.getZone( LandingZones ) + TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} + if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= ZoneRadius ) then + TransportZoneResult = 1 + end + end + if TransportZoneResult then + --trace.i( "routines", "TransportZone:" .. TransportZoneResult ) + else + --trace.i( "routines", "TransportZone:nil logic" ) + end + return TransportZoneResult + else + --trace.i( "routines", "TransportZone:nil hard" ) + return nil + end +end + + +function routines.IsStaticInZones( TransportStatic, LandingZones ) +--trace.f() + + local TransportZoneResult = nil + local TransportZonePos = nil + local TransportZone = nil + + -- fill-up some local variables to support further calculations to determine location of units within the zone. + local TransportStaticPos = TransportStatic:getPosition().p + if type( LandingZones ) == "table" then + for LandingZoneID, LandingZoneName in pairs( LandingZones ) do + TransportZone = trigger.misc.getZone( LandingZoneName ) + if TransportZone then + TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} + if ((( TransportStaticPos.x - TransportZonePos.x)^2 + (TransportStaticPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then + TransportZoneResult = LandingZoneID + break + end + end + end + else + TransportZone = trigger.misc.getZone( LandingZones ) + TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} + if ((( TransportStaticPos.x - TransportZonePos.x)^2 + (TransportStaticPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then + TransportZoneResult = 1 + end + end + +--trace.r( "", "", { TransportZoneResult } ) + return TransportZoneResult +end + + +function routines.IsUnitInRadius( CargoUnit, ReferencePosition, Radius ) +--trace.f() + + local Valid = true + + -- fill-up some local variables to support further calculations to determine location of units within the zone. + local CargoPos = CargoUnit:getPosition().p + local ReferenceP = ReferencePosition.p + + if (((CargoPos.x - ReferenceP.x)^2 + (CargoPos.z - ReferenceP.z)^2)^0.5 <= Radius) then + else + Valid = false + end + + return Valid +end + +function routines.IsPartOfGroupInRadius( CargoGroup, ReferencePosition, Radius ) +--trace.f() + + local Valid = true + + Valid = routines.ValidateGroup( CargoGroup, "CargoGroup", Valid ) + + -- fill-up some local variables to support further calculations to determine location of units within the zone + local CargoUnits = CargoGroup:getUnits() + for CargoUnitId, CargoUnit in pairs( CargoUnits ) do + local CargoUnitPos = CargoUnit:getPosition().p +-- env.info( 'routines.IsPartOfGroupInRadius: CargoUnitPos.x = ' .. CargoUnitPos.x .. ' CargoUnitPos.z = ' .. CargoUnitPos.z ) + local ReferenceP = ReferencePosition.p +-- env.info( 'routines.IsPartOfGroupInRadius: ReferenceGroupPos.x = ' .. ReferenceGroupPos.x .. ' ReferenceGroupPos.z = ' .. ReferenceGroupPos.z ) + + if ((( CargoUnitPos.x - ReferenceP.x)^2 + (CargoUnitPos.z - ReferenceP.z)^2)^0.5 <= Radius) then + else + Valid = false + break + end + end + + return Valid +end + + +function routines.ValidateString( Variable, VariableName, Valid ) +--trace.f() + + if type( Variable ) == "string" then + if Variable == "" then + error( "routines.ValidateString: error: " .. VariableName .. " must be filled out!" ) + Valid = false + end + else + error( "routines.ValidateString: error: " .. VariableName .. " is not a string." ) + Valid = false + end + +--trace.r( "", "", { Valid } ) + return Valid +end + +function routines.ValidateNumber( Variable, VariableName, Valid ) +--trace.f() + + if type( Variable ) == "number" then + else + error( "routines.ValidateNumber: error: " .. VariableName .. " is not a number." ) + Valid = false + end + +--trace.r( "", "", { Valid } ) + return Valid + +end + +function routines.ValidateGroup( Variable, VariableName, Valid ) +--trace.f() + + if Variable == nil then + error( "routines.ValidateGroup: error: " .. VariableName .. " is a nil value!" ) + Valid = false + end + +--trace.r( "", "", { Valid } ) + return Valid +end + +function routines.ValidateZone( LandingZones, VariableName, Valid ) +--trace.f() + + if LandingZones == nil then + error( "routines.ValidateGroup: error: " .. VariableName .. " is a nil value!" ) + Valid = false + end + + if type( LandingZones ) == "table" then + for LandingZoneID, LandingZoneName in pairs( LandingZones ) do + if trigger.misc.getZone( LandingZoneName ) == nil then + error( "routines.ValidateGroup: error: Zone " .. LandingZoneName .. " does not exist!" ) + Valid = false + break + end + end + else + if trigger.misc.getZone( LandingZones ) == nil then + error( "routines.ValidateGroup: error: Zone " .. LandingZones .. " does not exist!" ) + Valid = false + end + end + +--trace.r( "", "", { Valid } ) + return Valid +end + +function routines.ValidateEnumeration( Variable, VariableName, Enum, Valid ) +--trace.f() + + local ValidVariable = false + + for EnumId, EnumData in pairs( Enum ) do + if Variable == EnumData then + ValidVariable = true + break + end + end + + if ValidVariable then + else + error( 'TransportValidateEnum: " .. VariableName .. " is not a valid type.' .. Variable ) + Valid = false + end + +--trace.r( "", "", { Valid } ) + return Valid +end + +function routines.getGroupRoute(groupIdent, task) -- same as getGroupPoints but returns speed and formation type along with vec2 of point} + -- refactor to search by groupId and allow groupId and groupName as inputs + local gpId = groupIdent + if type(groupIdent) == 'string' and not tonumber(groupIdent) then + gpId = _DATABASE.Templates.Groups[groupIdent].groupId + end + + for coa_name, coa_data in pairs(env.mission.coalition) do + if (coa_name == 'red' or coa_name == 'blue') and type(coa_data) == 'table' then + if coa_data.country then --there is a country table + for cntry_id, cntry_data in pairs(coa_data.country) do + for obj_type_name, obj_type_data in pairs(cntry_data) do + if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" then -- only these types have points + if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! + for group_num, group_data in pairs(obj_type_data.group) do + if group_data and group_data.groupId == gpId then -- this is the group we are looking for + if group_data.route and group_data.route.points and #group_data.route.points > 0 then + local points = {} + + for point_num, point in pairs(group_data.route.points) do + local routeData = {} + if not point.point then + routeData.x = point.x + routeData.y = point.y + else + routeData.point = point.point --it's possible that the ME could move to the point = Vec2 notation. + end + routeData.form = point.action + routeData.speed = point.speed + routeData.alt = point.alt + routeData.alt_type = point.alt_type + routeData.airdromeId = point.airdromeId + routeData.helipadId = point.helipadId + routeData.type = point.type + routeData.action = point.action + if task then + routeData.task = point.task + end + points[point_num] = routeData + end + + return points + end + return + end --if group_data and group_data.name and group_data.name == 'groupname' + end --for group_num, group_data in pairs(obj_type_data.group) do + end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then + end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then + end --for obj_type_name, obj_type_data in pairs(cntry_data) do + end --for cntry_id, cntry_data in pairs(coa_data.country) do + end --if coa_data.country then --there is a country table + end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then + end --for coa_name, coa_data in pairs(mission.coalition) do +end + +routines.ground.patrolRoute = function(vars) + + + local tempRoute = {} + local useRoute = {} + local gpData = vars.gpData + if type(gpData) == 'string' then + gpData = Group.getByName(gpData) + end + + local useGroupRoute + if not vars.useGroupRoute then + useGroupRoute = vars.gpData + else + useGroupRoute = vars.useGroupRoute + end + local routeProvided = false + if not vars.route then + if useGroupRoute then + tempRoute = routines.getGroupRoute(useGroupRoute) + end + else + useRoute = vars.route + local posStart = routines.getLeadPos(gpData) + useRoute[1] = routines.ground.buildWP(posStart, useRoute[1].action, useRoute[1].speed) + routeProvided = true + end + + + local overRideSpeed = vars.speed or 'default' + local pType = vars.pType + local offRoadForm = vars.offRoadForm or 'default' + local onRoadForm = vars.onRoadForm or 'default' + + if routeProvided == false and #tempRoute > 0 then + local posStart = routines.getLeadPos(gpData) + + + useRoute[#useRoute + 1] = routines.ground.buildWP(posStart, offRoadForm, overRideSpeed) + for i = 1, #tempRoute do + local tempForm = tempRoute[i].action + local tempSpeed = tempRoute[i].speed + + if offRoadForm == 'default' then + tempForm = tempRoute[i].action + end + if onRoadForm == 'default' then + onRoadForm = 'On Road' + end + if (string.lower(tempRoute[i].action) == 'on road' or string.lower(tempRoute[i].action) == 'onroad' or string.lower(tempRoute[i].action) == 'on_road') then + tempForm = onRoadForm + 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, "Message", MsgTime, MsgName ):ToCoalition( coalition.side.RED ):ToCoalition( coalition.side.BLUE ) +end + +function MessageToRed( MsgText, MsgTime, MsgName ) +--trace.f() + + MESSAGE:New( MsgText, "To Red Coalition", MsgTime, MsgName ):ToCoalition( coalition.side.RED ) +end + +function MessageToBlue( MsgText, MsgTime, MsgName ) +--trace.f() + + MESSAGE:New( MsgText, "To Blue Coalition", MsgTime, MsgName ):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' )) + +--- BASE classes. +-- +-- @{#BASE} class +-- ============== +-- The @{#BASE} class is the super class for most of the classes defined within MOOSE. +-- +-- It handles: +-- +-- * The construction and inheritance of child classes. +-- * The tracing of objects during mission execution within the DCS.log file (under saved games folder). +-- +-- Note: Normally you would not use the BASE class unless you are extending the MOOSE framework with new classes. +-- +-- BASE Trace functionality +-- ======================== +-- The BASE class contains trace methods to trace progress within a mission execution of a certain object. +-- Note that these trace methods are inherited by each MOOSE class interiting BASE. +-- As such, each object created from derived class from BASE can use the tracing functions to trace its execution. +-- +-- Trace a function call +-- --------------------- +-- There are basically 3 types of tracing methods available within BASE: +-- +-- * @{#BASE.F}: Trace the beginning of a function and its given parameters. +-- * @{#BASE.T}: Trace further logic within a function giving optional variables or parameters. +-- * @{#BASE.E}: Trace an execption within a function giving optional variables or parameters. An exception will always be traced. +-- +-- 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. +-- +-- BASE Inheritance support +-- ======================== +-- The following methods are available to support inheritance: +-- +-- * @{#BASE.Inherit}: Inherits from a class. +-- * @{#BASE.Inherited}: Returns the parent class from the class. +-- +-- Future +-- ====== +-- Further methods may be added to BASE whenever there is a need to make "overall" functions available within MOOSE. +-- +-- ==== +-- +-- @module Base +-- @author FlightControl + +Include.File( "Routines" ) + +local _TraceOn = true +local _TraceLevel = 1 +local _TraceClass = { + --DATABASE = true, + --SEAD = true, + --DESTROYBASETASK = true, + --MOVEMENT = true, + --SPAWN = true, + --STAGE = true, + --ZONE = true, + --GROUP = true, + --UNIT = true, + --CLIENT = true, + --CARGO = true, + --CARGO_GROUP = true, + --CARGO_PACKAGE = true, + --CARGO_SLINGLOAD = true, + --CARGO_ZONE = true, + --CLEANUP = true, + --MENU_CLIENT = true, + --MENU_CLIENT_COMMAND = true, + --ESCORT = true, + } +local _TraceClassMethod = {} + +--- The BASE Class +-- @type BASE +-- @field ClassName The name of the class. +-- @field ClassID The ID number of the class. +BASE = { + ClassName = "BASE", + ClassID = 0, + Events = {} +} + +--- The Formation Class +-- @type FORMATION +-- @field Cone A cone formation. +FORMATION = { + Cone = "Cone" +} + + + +--- The base constructor. This is the top top class of all classed defined within the MOOSE. +-- Any new class needs to be derived from this class for proper inheritance. +-- @param #BASE self +-- @return #BASE The new instance of the BASE class. +-- @usage +-- function TASK:New() +-- +-- local self = BASE:Inherit( self, BASE:New() ) +-- +-- -- assign Task default values during construction +-- self.TaskBriefing = "Task: No Task." +-- self.Time = timer.getTime() +-- self.ExecuteStage = _TransportExecuteStage.NONE +-- +-- return self +-- end +-- @todo need to investigate if the deepCopy is really needed... Don't think so. +function BASE:New() + local Child = routines.utils.deepCopy( self ) + local Parent = {} + setmetatable( Child, Parent ) + Child.__index = Child + self.ClassID = self.ClassID + 1 + Child.ClassID = self.ClassID + --Child.AddEvent( Child, S_EVENT_BIRTH, Child.EventBirth ) + return Child +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 ) + if Child ~= nil then + setmetatable( Child, Parent ) + Child.__index = Child + end + --Child.ClassName = Child.ClassName .. '.' .. Child.ClassID + self:T( 'Inherited from ' .. Parent.ClassName ) + return Child +end + +--- This is the worker method to retrieve the Parent class. +-- @param #BASE self +-- @param #BASE Child is the Child class from which the Parent class needs to be retrieved. +-- @return #BASE +function BASE:Inherited( Child ) + local Parent = getmetatable( Child ) +-- env.info('Inherited class of ' .. Child.ClassName .. ' is ' .. Parent.ClassName ) + return Parent +end + +--- Get the ClassName + ClassID of the class instance. +-- The ClassName + ClassID is formatted as '%s#%09d'. +-- @param #BASE self +-- @return #string The ClassName + ClassID of the class instance. +function BASE:GetClassNameAndID() + return string.format( '%s#%09d', self:GetClassName(), self:GetClassID() ) +end + +--- Get the ClassName of the class instance. +-- @param #BASE self +-- @return #string The ClassName of the class instance. +function BASE:GetClassName() + return self.ClassName +end + +--- Get the ClassID of the class instance. +-- @param #BASE self +-- @return #string The ClassID of the class instance. +function BASE:GetClassID() + return self.ClassID +end + +--- Set a new listener for the class. +-- @param self +-- @param DCSTypes#Event Event +-- @param #function EventFunction +-- @return #BASE +function BASE:AddEvent( Event, EventFunction ) + self:F( Event ) + + self.Events[#self.Events+1] = {} + self.Events[#self.Events].Event = Event + self.Events[#self.Events].EventFunction = EventFunction + self.Events[#self.Events].EventEnabled = false + + return self +end + +--- Returns the event dispatcher +-- @param #BASE self +-- @return Event#EVENT +function BASE:Event() + + return _EVENTDISPATCHER +end + + + + + +--- Enable the event listeners for the class. +-- @param #BASE self +-- @return #BASE +function BASE:EnableEvents() + self:F( #self.Events ) + + for EventID, Event in pairs( self.Events ) do + Event.Self = self + Event.EventEnabled = true + end + self.Events.Handler = world.addEventHandler( self ) + + return self +end + + +--- Disable the event listeners for the class. +-- @param #BASE self +-- @return #BASE +function BASE:DisableEvents() + self:F() + + world.removeEventHandler( self ) + for EventID, Event in pairs( self.Events ) do + Event.Self = nil + Event.EventEnabled = false + end + + return self +end + + +local BaseEventCodes = { + "S_EVENT_SHOT", + "S_EVENT_HIT", + "S_EVENT_TAKEOFF", + "S_EVENT_LAND", + "S_EVENT_CRASH", + "S_EVENT_EJECTION", + "S_EVENT_REFUELING", + "S_EVENT_DEAD", + "S_EVENT_PILOT_DEAD", + "S_EVENT_BASE_CAPTURED", + "S_EVENT_MISSION_START", + "S_EVENT_MISSION_END", + "S_EVENT_TOOK_CONTROL", + "S_EVENT_REFUELING_STOP", + "S_EVENT_BIRTH", + "S_EVENT_HUMAN_FAILURE", + "S_EVENT_ENGINE_STARTUP", + "S_EVENT_ENGINE_SHUTDOWN", + "S_EVENT_PLAYER_ENTER_UNIT", + "S_EVENT_PLAYER_LEAVE_UNIT", + "S_EVENT_PLAYER_COMMENT", + "S_EVENT_SHOOTING_START", + "S_EVENT_SHOOTING_END", + "S_EVENT_MAX", +} + +--onEvent( {[1]="S_EVENT_BIRTH",[2]={["subPlace"]=5,["time"]=0,["initiator"]={["id_"]=16884480,},["place"]={["id_"]=5000040,},["id"]=15,["IniUnitName"]="US F-15C@RAMP-Air Support Mountains#001-01",},} +-- Event = { +-- id = enum world.event, +-- time = Time, +-- initiator = Unit, +-- target = Unit, +-- place = Unit, +-- subPlace = enum world.BirthPlace, +-- weapon = Weapon +-- } + +--- Creation of a Birth Event. +-- @param #BASE self +-- @param DCSTypes#Time EventTime The time stamp of the event. +-- @param DCSObject#Object Initiator The initiating object of the event. +-- @param #string IniUnitName The initiating unit name. +-- @param place +-- @param subplace +function BASE:CreateEventBirth( EventTime, Initiator, IniUnitName, place, subplace ) + self:F( { EventTime, Initiator, IniUnitName, place, subplace } ) + + local Event = { + id = world.event.S_EVENT_BIRTH, + time = EventTime, + initiator = Initiator, + IniUnitName = IniUnitName, + place = place, + subplace = subplace + } + + world.onEvent( Event ) +end + +--- Creation of a Crash Event. +-- @param #BASE self +-- @param DCSTypes#Time EventTime The time stamp of the event. +-- @param DCSObject#Object Initiator The initiating object of the event. +function BASE:CreateEventCrash( EventTime, Initiator ) + self:F( { EventTime, Initiator } ) + + local Event = { + id = world.event.S_EVENT_CRASH, + time = EventTime, + initiator = Initiator, + } + + world.onEvent( Event ) +end + +-- TODO: Complete DCSTypes#Event structure. +--- The main event handling function... This function captures all events generated for the class. +-- @param #BASE self +-- @param DCSTypes#Event event +function BASE:onEvent(event) + --self:F( { BaseEventCodes[event.id], event } ) + + if self then + for EventID, EventObject in pairs( self.Events ) do + if EventObject.EventEnabled then + --env.info( 'onEvent Table EventObject.Self = ' .. tostring(EventObject.Self) ) + --env.info( 'onEvent event.id = ' .. tostring(event.id) ) + --env.info( 'onEvent EventObject.Event = ' .. tostring(EventObject.Event) ) + if event.id == EventObject.Event then + if self == EventObject.Self then + if event.initiator and event.initiator:isExist() then + event.IniUnitName = event.initiator:getName() + end + if event.target and event.target:isExist() then + event.TgtUnitName = event.target:getName() + end + --self:T( { BaseEventCodes[event.id], event } ) + --EventObject.EventFunction( self, event ) + end + end + end + end + end +end + +-- Trace section + +-- Log a trace (only shown when trace is on) +-- TODO: Make trace function using variable parameters. + +--- Set trace level +-- @param #BASE self +-- @param #number Level +function BASE:TraceLevel( Level ) + _TraceLevel = Level + self:E( "Tracing level " .. Level ) +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. Must be at the beginning of the function logic. +-- @param #BASE self +-- @param Arguments A #table or any field. +function BASE:F( Arguments ) + + if _TraceOn and ( _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName] ) then + + local DebugInfoCurrent = debug.getinfo( 2, "nl" ) + local DebugInfoFrom = debug.getinfo( 3, "l" ) + + local Function = "function" + if DebugInfoCurrent.name then + Function = DebugInfoCurrent.name + end + + if _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName].Method[Function] then + local LineCurrent = DebugInfoCurrent.currentline + 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 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 _TraceLevel >= 2 then + self:F( Arguments ) + 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 _TraceLevel >= 3 then + self:F( Arguments ) + end + +end + +--- Trace a function logic. Can be anywhere within the function logic. +-- @param #BASE self +-- @param Arguments A #table or any field. +function BASE:T( Arguments ) + + if _TraceOn and ( _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName] ) then + + local DebugInfoCurrent = debug.getinfo( 2, "nl" ) + local DebugInfoFrom = debug.getinfo( 3, "l" ) + + local Function = "function" + if DebugInfoCurrent.name then + Function = DebugInfoCurrent.name + end + + if _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName].Method[Function] then + local LineCurrent = DebugInfoCurrent.currentline + 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 2. Can be anywhere within the function logic. +-- @param #BASE self +-- @param Arguments A #table or any field. +function BASE:T2( Arguments ) + + if _TraceLevel >= 2 then + self:T( Arguments ) + 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 _TraceLevel >= 3 then + self:T( Arguments ) + end + +end + +--- Log an exception which will be traced always. Can be anywhere within the function logic. +-- @param #BASE self +-- @param Arguments A #table or any field. +function BASE:E( Arguments ) + + local DebugInfoCurrent = debug.getinfo( 2, "nl" ) + local DebugInfoFrom = debug.getinfo( 3, "l" ) + + local Function = "function" + if DebugInfoCurrent.name then + Function = DebugInfoCurrent.name + end + + local LineCurrent = DebugInfoCurrent.currentline + local LineFrom = -1 + if DebugInfoFrom then + LineFrom = DebugInfoFrom.currentline + end + + env.info( string.format( "%6d(%6d)/%1s:%20s%05d.%s(%s)" , LineCurrent, LineFrom, "E", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) +end + + + +--- Models time events calling event handing functions. +-- +-- @{SCHEDULER} class +-- =================== +-- The @{SCHEDULER} class models time events calling given event handling functions. +-- +-- SCHEDULER constructor +-- ===================== +-- The SCHEDULER class is quite easy to use: +-- +-- * @{#SCHEDULER.New}: Setup a new scheduler and start it with the specified parameters. +-- +-- SCHEDULER timer methods +-- ======================= +-- The SCHEDULER can be stopped and restarted with the following methods: +-- +-- * @{#SCHEDULER.Start}: (Re-)Start the scheduler. +-- * @{#SCHEDULER.Start}: Stop the scheduler. +-- +-- @module Scheduler +-- @author FlightControl + +Include.File( "Routines" ) +Include.File( "Base" ) + + +--- The SCHEDULER class +-- @type SCHEDULER +-- @extends Base#BASE +SCHEDULER = { + ClassName = "SCHEDULER", +} + + +--- Constructor. +-- @param #SCHEDULER self +-- @param #table TimeEventObject Specified for which Moose object the timer is setup. If a value of nil is provided, a scheduler will be setup without an object reference. +-- @param #function TimeEventFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in TimeEventFunctionArguments. +-- @param #table TimeEventFunctionArguments Optional arguments that can be given as part of scheduler. The arguments need to be given as a table { param1, param 2, ... }. +-- @param #number StartSeconds Specifies the amount of seconds that will be waited before the scheduling is started, and the event function is called. +-- @param #number RepeatSecondsInterval Specifies the interval in seconds when the scheduler will call the event function. +-- @param #number RandomizationFactor Specifies a randomization factor between 0 and 1 to randomize the RepeatSecondsInterval. +-- @param #number StopSeconds Specifies the amount of seconds when the scheduler will be stopped. +-- @return #SCHEDULER self +function SCHEDULER:New( TimeEventObject, TimeEventFunction, TimeEventFunctionArguments, StartSeconds, RepeatSecondsInterval, RandomizationFactor, StopSeconds ) + local self = BASE:Inherit( self, BASE:New() ) + self:F2( { TimeEventObject, TimeEventFunction, TimeEventFunctionArguments, StartSeconds, RepeatSecondsInterval, RandomizationFactor, StopSeconds } ) + + self.TimeEventObject = TimeEventObject + self.TimeEventFunction = TimeEventFunction + self.TimeEventFunctionArguments = TimeEventFunctionArguments + self.StartSeconds = StartSeconds + + if RepeatSecondsInterval then + self.RepeatSecondsInterval = RepeatSecondsInterval + else + self.RepeatSecondsInterval = 0 + end + + if RandomizationFactor then + self.RandomizationFactor = RandomizationFactor + else + self.RandomizationFactor = 0 + end + + if StopSeconds then + self.StopSeconds = StopSeconds + end + + self.Repeat = false + + self.StartTime = timer.getTime() + + self:Start() + + return self +end + +--- (Re-)Starts the scheduler. +-- @param #SCHEDULER self +-- @return #SCHEDULER self +function SCHEDULER:Start() + self:F2( self.TimeEventObject ) + + self.Repeat = true + timer.scheduleFunction( self._Scheduler, self, timer.getTime() + self.StartSeconds + .01 ) + + return self +end + +--- Stops the scheduler. +-- @param #SCHEDULER self +-- @return #SCHEDULER self +function SCHEDULER:Stop() + self:F2( self.TimeEventObject ) + + self.Repeat = false + + return self +end + +-- Private Functions + +function SCHEDULER:_Scheduler() + self:F2( self.TimeEventFunctionArguments ) + + local ErrorHandler = function( errmsg ) + + env.info( "Error in SCHEDULER function:" .. errmsg ) + env.info( debug.traceback() ) + + return errmsg + end + + local Status, Result + if self.TimeEventObject then + Status, Result = xpcall( function() return self.TimeEventFunction( self.TimeEventObject, unpack( self.TimeEventFunctionArguments ) ) end, ErrorHandler ) + else + Status, Result = xpcall( function() return self.TimeEventFunction( unpack( self.TimeEventFunctionArguments ) ) end, ErrorHandler ) + end + + self:T( { Status, Result } ) + + if Status and Status == true and Result and Result == true then + if self.Repeat and ( not self.StopSeconds or ( self.StopSeconds and timer.getTime() <= self.StartTime + self.StopSeconds ) ) then + timer.scheduleFunction( + self._Scheduler, + self, + timer.getTime() + self.RepeatSecondsInterval + math.random( - ( self.RandomizationFactor * self.RepeatSecondsInterval / 2 ), ( self.RandomizationFactor * self.RepeatSecondsInterval / 2 ) ) + 0.01 + ) + end + end + +end + + + + + + + + +--- The EVENT class models an efficient event handling process between other classes and its units, weapons. +-- @module Event +-- @author FlightControl + +Include.File( "Routines" ) +Include.File( "Base" ) + +--- The EVENT structure +-- @type EVENT +-- @field #EVENT.Events Events +EVENT = { + ClassName = "EVENT", + ClassID = 0, +} + +local _EVENTCODES = { + "S_EVENT_SHOT", + "S_EVENT_HIT", + "S_EVENT_TAKEOFF", + "S_EVENT_LAND", + "S_EVENT_CRASH", + "S_EVENT_EJECTION", + "S_EVENT_REFUELING", + "S_EVENT_DEAD", + "S_EVENT_PILOT_DEAD", + "S_EVENT_BASE_CAPTURED", + "S_EVENT_MISSION_START", + "S_EVENT_MISSION_END", + "S_EVENT_TOOK_CONTROL", + "S_EVENT_REFUELING_STOP", + "S_EVENT_BIRTH", + "S_EVENT_HUMAN_FAILURE", + "S_EVENT_ENGINE_STARTUP", + "S_EVENT_ENGINE_SHUTDOWN", + "S_EVENT_PLAYER_ENTER_UNIT", + "S_EVENT_PLAYER_LEAVE_UNIT", + "S_EVENT_PLAYER_COMMENT", + "S_EVENT_SHOOTING_START", + "S_EVENT_SHOOTING_END", + "S_EVENT_MAX", +} + +--- The Event structure +-- @type EVENTDATA +-- @field id +-- @field initiator +-- @field target +-- @field weapon +-- @field IniDCSUnit +-- @field IniDCSUnitName +-- @field IniDCSGroup +-- @field IniDCSGroupName +-- @field TgtDCSUnit +-- @field TgtDCSUnitName +-- @field TgtDCSGroup +-- @field TgtDCSGroupName +-- @field Weapon +-- @field WeaponName +-- @field WeaponTgtDCSUnit + +--- The Events structure +-- @type EVENT.Events +-- @field #number IniUnit + +function EVENT:New() + local self = BASE:Inherit( self, BASE:New() ) + self:F2() + self.EventHandler = world.addEventHandler( self ) + return self +end + +function EVENT:EventText( EventID ) + + local EventText = _EVENTCODES[EventID] + + return EventText +end + + +--- Initializes the Events structure for the event +-- @param #EVENT self +-- @param DCSWorld#world.event EventID +-- @param #string EventClass +-- @return #EVENT.Events +function EVENT:Init( EventID, EventClass ) + self:F3( { _EVENTCODES[EventID], EventClass } ) + if not self.Events[EventID] then + self.Events[EventID] = {} + end + if not self.Events[EventID][EventClass] then + self.Events[EventID][EventClass] = {} + end + return self.Events[EventID][EventClass] +end + + +--- Create an OnDead event handler for a group +-- @param #EVENT self +-- @param #table EventTemplate +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param EventSelf The self instance of the class for which the event is. +-- @param #function OnEventFunction +-- @return #EVENT +function EVENT:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, OnEventFunction ) + self:F2( EventTemplate.name ) + + for EventUnitID, EventUnit in pairs( EventTemplate.units ) do + OnEventFunction( self, EventUnit.name, EventFunction, EventSelf ) + end + return self +end + +--- Set a new listener for an S_EVENT_X event independent from a unit or a weapon. +-- @param #EVENT self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @param EventID +-- @return #EVENT +function EVENT:OnEventGeneric( EventFunction, EventSelf, EventID ) + self:F2( { EventID } ) + + local Event = self:Init( EventID, EventSelf:GetClassNameAndID() ) + Event.EventFunction = EventFunction + Event.EventSelf = EventSelf + return self +end + + +--- Set a new listener for an S_EVENT_X event +-- @param #EVENT self +-- @param #string EventDCSUnitName +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @param EventID +-- @return #EVENT +function EVENT:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, EventID ) + self:F2( EventDCSUnitName ) + + local Event = self:Init( EventID, EventSelf:GetClassNameAndID() ) + if not Event.IniUnit then + Event.IniUnit = {} + end + Event.IniUnit[EventDCSUnitName] = {} + Event.IniUnit[EventDCSUnitName].EventFunction = EventFunction + Event.IniUnit[EventDCSUnitName].EventSelf = EventSelf + return self +end + + +--- Create an OnBirth event handler for a group +-- @param #EVENT self +-- @param Group#GROUP EventGroup +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnBirthForTemplate( EventTemplate, EventFunction, EventSelf ) + self:F2( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnBirthForUnit ) + + return self +end + +--- Set a new listener for an S_EVENT_BIRTH event, and registers the unit born. +-- @param #EVENT self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf +-- @return #EVENT +function EVENT:OnBirth( EventFunction, EventSelf ) + self:F2() + + self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_BIRTH ) + + return self +end + +--- Set a new listener for an S_EVENT_BIRTH event. +-- @param #EVENT self +-- @param #string EventDCSUnitName The id of the unit for the event to be handled. +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf +-- @return #EVENT +function EVENT:OnBirthForUnit( EventDCSUnitName, EventFunction, EventSelf ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_BIRTH ) + + return self +end + +--- Create an OnCrash event handler for a group +-- @param #EVENT self +-- @param Group#GROUP EventGroup +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnCrashForTemplate( EventTemplate, EventFunction, EventSelf ) + self:F2( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnCrashForUnit ) + + return self +end + +--- Set a new listener for an S_EVENT_CRASH event. +-- @param #EVENT self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf +-- @return #EVENT +function EVENT:OnCrash( EventFunction, EventSelf ) + self:F2() + + self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_CRASH ) + + return self +end + +--- Set a new listener for an S_EVENT_CRASH event. +-- @param #EVENT self +-- @param #string EventDCSUnitName +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnCrashForUnit( EventDCSUnitName, EventFunction, EventSelf ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_CRASH ) + + return self +end + +--- Create an OnDead event handler for a group +-- @param #EVENT self +-- @param Group#GROUP EventGroup +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnDeadForTemplate( EventTemplate, EventFunction, EventSelf ) + self:F2( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnDeadForUnit ) + + return self +end + +--- Set a new listener for an S_EVENT_DEAD event. +-- @param #EVENT self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf +-- @return #EVENT +function EVENT:OnDead( EventFunction, EventSelf ) + self:F2() + + self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_DEAD ) + + return self +end + + +--- Set a new listener for an S_EVENT_DEAD event. +-- @param #EVENT self +-- @param #string EventDCSUnitName +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnDeadForUnit( EventDCSUnitName, EventFunction, EventSelf ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_DEAD ) + + return self +end + +--- Set a new listener for an S_EVENT_PILOT_DEAD event. +-- @param #EVENT self +-- @param #string EventDCSUnitName +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnPilotDeadForUnit( EventDCSUnitName, EventFunction, EventSelf ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_PILOT_DEAD ) + + return self +end + +--- Create an OnDead event handler for a group +-- @param #EVENT self +-- @param #table EventTemplate +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnLandForTemplate( EventTemplate, EventFunction, EventSelf ) + self:F2( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnLandForUnit ) + + return self +end + +--- Set a new listener for an S_EVENT_LAND event. +-- @param #EVENT self +-- @param #string EventDCSUnitName +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnLandForUnit( EventDCSUnitName, EventFunction, EventSelf ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_LAND ) + + return self +end + +--- Create an OnDead event handler for a group +-- @param #EVENT self +-- @param #table EventTemplate +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnTakeOffForTemplate( EventTemplate, EventFunction, EventSelf ) + self:F2( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnTakeOffForUnit ) + + return self +end + +--- Set a new listener for an S_EVENT_TAKEOFF event. +-- @param #EVENT self +-- @param #string EventDCSUnitName +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnTakeOffForUnit( EventDCSUnitName, EventFunction, EventSelf ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_TAKEOFF ) + + return self +end + +--- Create an OnDead event handler for a group +-- @param #EVENT self +-- @param #table EventTemplate +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnEngineShutDownForTemplate( EventTemplate, EventFunction, EventSelf ) + self:F2( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnEngineShutDownForUnit ) + + return self +end + +--- Set a new listener for an S_EVENT_ENGINE_SHUTDOWN event. +-- @param #EVENT self +-- @param #string EventDCSUnitName +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnEngineShutDownForUnit( EventDCSUnitName, EventFunction, EventSelf ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_ENGINE_SHUTDOWN ) + + return self +end + +--- Set a new listener for an S_EVENT_ENGINE_STARTUP event. +-- @param #EVENT self +-- @param #string EventDCSUnitName +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnEngineStartUpForUnit( EventDCSUnitName, EventFunction, EventSelf ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_ENGINE_STARTUP ) + + return self +end + +--- Set a new listener for an S_EVENT_SHOT event. +-- @param #EVENT self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnShot( EventFunction, EventSelf ) + self:F2() + + self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_SHOT ) + + return self +end + +--- Set a new listener for an S_EVENT_SHOT event for a unit. +-- @param #EVENT self +-- @param #string EventDCSUnitName +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnShotForUnit( EventDCSUnitName, EventFunction, EventSelf ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_SHOT ) + + return self +end + +--- Set a new listener for an S_EVENT_HIT event. +-- @param #EVENT self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnHit( EventFunction, EventSelf ) + self:F2() + + self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_HIT ) + + return self +end + +--- Set a new listener for an S_EVENT_HIT event. +-- @param #EVENT self +-- @param #string EventDCSUnitName +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnHitForUnit( EventDCSUnitName, EventFunction, EventSelf ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_HIT ) + + return self +end + +--- Set a new listener for an S_EVENT_PLAYER_ENTER_UNIT event. +-- @param #EVENT self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnPlayerEnterUnit( EventFunction, EventSelf ) + self:F2() + + self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_PLAYER_ENTER_UNIT ) + + return self +end + +--- Set a new listener for an S_EVENT_PLAYER_LEAVE_UNIT event. +-- @param #EVENT self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnPlayerLeaveUnit( EventFunction, EventSelf ) + self:F2() + + self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_PLAYER_LEAVE_UNIT ) + + return self +end + + + +function EVENT:onEvent( Event ) + self:F2( { _EVENTCODES[Event.id], Event } ) + + if self and self.Events and self.Events[Event.id] then + if Event.initiator and Event.initiator:getCategory() == Object.Category.UNIT then + Event.IniDCSUnit = Event.initiator + Event.IniDCSGroup = Event.IniDCSUnit:getGroup() + Event.IniDCSUnitName = Event.IniDCSUnit:getName() + Event.IniDCSGroupName = "" + if Event.IniDCSGroup and Event.IniDCSGroup:isExist() then + Event.IniDCSGroupName = Event.IniDCSGroup:getName() + end + end + if Event.target then + if Event.target and Event.target:getCategory() == Object.Category.UNIT then + Event.TgtDCSUnit = Event.target + Event.TgtDCSGroup = Event.TgtDCSUnit:getGroup() + Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() + Event.TgtDCSGroupName = "" + if Event.TgtDCSGroup and Event.TgtDCSGroup:isExist() then + Event.TgtDCSGroupName = Event.TgtDCSGroup:getName() + end + end + end + if Event.weapon then + Event.Weapon = Event.weapon + Event.WeaponName = Event.Weapon:getTypeName() + --Event.WeaponTgtDCSUnit = Event.Weapon:getTarget() + end + self:E( { _EVENTCODES[Event.id], Event } ) + for ClassName, EventData in pairs( self.Events[Event.id] ) do + if Event.IniDCSUnitName and EventData.IniUnit and EventData.IniUnit[Event.IniDCSUnitName] then + self:T2( { "Calling event function for class ", ClassName, " unit ", Event.IniDCSUnitName } ) + EventData.IniUnit[Event.IniDCSUnitName].EventFunction( EventData.IniUnit[Event.IniDCSUnitName].EventSelf, Event ) + else + if Event.IniDCSUnit and not EventData.IniUnit then + self:T2( { "Calling event function for class ", ClassName } ) + EventData.EventFunction( EventData.EventSelf, Event ) + end + end + end + end +end + +--- Encapsulation of DCS World Menu system in a set of MENU classes. +-- @module Menu + +Include.File( "Routines" ) +Include.File( "Base" ) + +--- The MENU class +-- @type MENU +-- @extends Base#BASE +MENU = { + ClassName = "MENU", + MenuPath = nil, + MenuText = "", + MenuParentPath = nil +} + +--- +function MENU:New( MenuText, MenuParentPath ) + + -- Arrange meta tables + local Child = BASE:Inherit( self, BASE:New() ) + + Child.MenuPath = nil + Child.MenuText = MenuText + Child.MenuParentPath = MenuParentPath + return Child +end + +--- The COMMANDMENU class +-- @type COMMANDMENU +-- @extends Menu#MENU +COMMANDMENU = { + ClassName = "COMMANDMENU", + CommandMenuFunction = nil, + CommandMenuArgument = nil +} + +function COMMANDMENU:New( MenuText, ParentMenu, CommandMenuFunction, CommandMenuArgument ) + + -- Arrange meta tables + + local MenuParentPath = nil + if ParentMenu ~= nil then + MenuParentPath = ParentMenu.MenuPath + end + + local Child = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) + + Child.MenuPath = missionCommands.addCommand( MenuText, MenuParentPath, CommandMenuFunction, CommandMenuArgument ) + Child.CommandMenuFunction = CommandMenuFunction + Child.CommandMenuArgument = CommandMenuArgument + return Child +end + +--- The SUBMENU class +-- @type SUBMENU +-- @extends Menu#MENU +SUBMENU = { + ClassName = "SUBMENU" +} + +function SUBMENU:New( MenuText, ParentMenu ) + + -- Arrange meta tables + local MenuParentPath = nil + if ParentMenu ~= nil then + MenuParentPath = ParentMenu.MenuPath + end + + local Child = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) + + Child.MenuPath = missionCommands.addSubMenu( MenuText, MenuParentPath ) + return Child +end + +-- This local variable is used to cache the menus registered under clients. +-- Menus don't dissapear when clients are destroyed and restarted. +-- So every menu for a client created must be tracked so that program logic accidentally does not create +-- the same menus twice during initialization logic. +-- These menu classes are handling this logic with this variable. +local _MENUCLIENTS = {} + +--- The MENU_CLIENT class +-- @type MENU_CLIENT +-- @extends Menu#MENU +MENU_CLIENT = { + ClassName = "MENU_CLIENT" +} + +--- Creates a new menu item for a group +-- @param self +-- @param Client#CLIENT MenuClient The Client owning the menu. +-- @param #string MenuText The text for the menu. +-- @param #table ParentMenu The parent menu. +-- @return #MENU_CLIENT self +function MENU_CLIENT:New( MenuClient, MenuText, ParentMenu ) + + -- Arrange meta tables + local MenuParentPath = {} + if ParentMenu ~= nil then + MenuParentPath = ParentMenu.MenuPath + end + + local self = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) + self:F( { MenuClient, MenuText, ParentMenu } ) + + self.MenuClient = MenuClient + self.MenuClientGroupID = MenuClient:GetClientGroupID() + self.MenuParentPath = MenuParentPath + self.MenuText = MenuText + self.ParentMenu = ParentMenu + + self.Menus = {} + + if not _MENUCLIENTS[self.MenuClientGroupID] then + _MENUCLIENTS[self.MenuClientGroupID] = {} + end + + local MenuPath = _MENUCLIENTS[self.MenuClientGroupID] + + self:T( { MenuClient:GetClientGroupName(), MenuPath[table.concat(MenuParentPath)], MenuParentPath, MenuText } ) + + local MenuPathID = table.concat(MenuParentPath) .. "/" .. MenuText + if MenuPath[MenuPathID] then + missionCommands.removeItemForGroup( self.MenuClient:GetClientGroupID(), MenuPath[MenuPathID] ) + end + + self.MenuPath = missionCommands.addSubMenuForGroup( self.MenuClient:GetClientGroupID(), MenuText, MenuParentPath ) + MenuPath[MenuPathID] = self.MenuPath + + self:T( { MenuClient:GetClientGroupName(), self.MenuPath } ) + + if ParentMenu and ParentMenu.Menus then + ParentMenu.Menus[self.MenuPath] = self + end + return self +end + +--- Removes the sub menus recursively of this MENU_CLIENT. +-- @param #MENU_CLIENT self +-- @return #MENU_CLIENT self +function MENU_CLIENT:RemoveSubMenus() + self:F( self.MenuPath ) + + for MenuID, Menu in pairs( self.Menus ) do + Menu:Remove() + end + +end + +--- Removes the sub menus recursively of this MENU_CLIENT. +-- @param #MENU_CLIENT self +-- @return #MENU_CLIENT self +function MENU_CLIENT:Remove() + self:F( self.MenuPath ) + + self:RemoveSubMenus() + + if not _MENUCLIENTS[self.MenuClientGroupID] then + _MENUCLIENTS[self.MenuClientGroupID] = {} + end + + local MenuPath = _MENUCLIENTS[self.MenuClientGroupID] + + if MenuPath[table.concat(self.MenuParentPath) .. "/" .. self.MenuText] then + MenuPath[table.concat(self.MenuParentPath) .. "/" .. self.MenuText] = nil + end + + missionCommands.removeItemForGroup( self.MenuClient:GetClientGroupID(), self.MenuPath ) + self.ParentMenu.Menus[self.MenuPath] = nil + return nil +end + + +--- The MENU_CLIENT_COMMAND class +-- @type MENU_CLIENT_COMMAND +-- @extends Menu#MENU +MENU_CLIENT_COMMAND = { + ClassName = "MENU_CLIENT_COMMAND" +} + +--- Creates a new radio command item for a group +-- @param self +-- @param Client#CLIENT MenuClient The Client owning the menu. +-- @param MenuText The text for the menu. +-- @param ParentMenu The parent menu. +-- @param CommandMenuFunction A function that is called when the menu key is pressed. +-- @param CommandMenuArgument An argument for the function. +-- @return Menu#MENU_CLIENT_COMMAND self +function MENU_CLIENT_COMMAND:New( MenuClient, MenuText, ParentMenu, CommandMenuFunction, CommandMenuArgument ) + + -- Arrange meta tables + + local MenuParentPath = {} + if ParentMenu ~= nil then + MenuParentPath = ParentMenu.MenuPath + end + + local self = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) + + self.MenuClient = MenuClient + self.MenuClientGroupID = MenuClient:GetClientGroupID() + self.MenuParentPath = MenuParentPath + self.MenuText = MenuText + self.ParentMenu = ParentMenu + + if not _MENUCLIENTS[self.MenuClientGroupID] then + _MENUCLIENTS[self.MenuClientGroupID] = {} + end + + local MenuPath = _MENUCLIENTS[self.MenuClientGroupID] + + self:T( { MenuClient:GetClientGroupName(), MenuPath[table.concat(MenuParentPath)], MenuParentPath, MenuText, CommandMenuFunction, CommandMenuArgument } ) + + local MenuPathID = table.concat(MenuParentPath) .. "/" .. MenuText + if MenuPath[MenuPathID] then + missionCommands.removeItemForGroup( self.MenuClient:GetClientGroupID(), MenuPath[MenuPathID] ) + end + + self.MenuPath = missionCommands.addCommandForGroup( self.MenuClient:GetClientGroupID(), MenuText, MenuParentPath, CommandMenuFunction, CommandMenuArgument ) + MenuPath[MenuPathID] = self.MenuPath + + self.CommandMenuFunction = CommandMenuFunction + self.CommandMenuArgument = CommandMenuArgument + + ParentMenu.Menus[self.MenuPath] = self + + return self +end + +function MENU_CLIENT_COMMAND:Remove() + self:F( self.MenuPath ) + + if not _MENUCLIENTS[self.MenuClientGroupID] then + _MENUCLIENTS[self.MenuClientGroupID] = {} + end + + local MenuPath = _MENUCLIENTS[self.MenuClientGroupID] + + if MenuPath[table.concat(self.MenuParentPath) .. "/" .. self.MenuText] then + MenuPath[table.concat(self.MenuParentPath) .. "/" .. self.MenuText] = nil + end + + missionCommands.removeItemForGroup( self.MenuClient:GetClientGroupID(), self.MenuPath ) + self.ParentMenu.Menus[self.MenuPath] = nil + return nil +end + + +--- The MENU_COALITION class +-- @type MENU_COALITION +-- @extends Menu#MENU +MENU_COALITION = { + ClassName = "MENU_COALITION" +} + +--- Creates a new coalition menu item +-- @param #MENU_COALITION self +-- @param DCSCoalition#coalition.side MenuCoalition The coalition owning the menu. +-- @param #string MenuText The text for the menu. +-- @param #table ParentMenu The parent menu. +-- @return #MENU_COALITION self +function MENU_COALITION:New( MenuCoalition, MenuText, ParentMenu ) + + -- Arrange meta tables + local MenuParentPath = {} + if ParentMenu ~= nil then + MenuParentPath = ParentMenu.MenuPath + end + + local self = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) + self:F( { MenuCoalition, MenuText, ParentMenu } ) + + self.MenuCoalition = MenuCoalition + self.MenuParentPath = MenuParentPath + self.MenuText = MenuText + self.ParentMenu = ParentMenu + + self.Menus = {} + + self:T( { MenuParentPath, MenuText } ) + + self.MenuPath = missionCommands.addSubMenuForCoalition( self.MenuCoalition, MenuText, MenuParentPath ) + + self:T( { self.MenuPath } ) + + if ParentMenu and ParentMenu.Menus then + ParentMenu.Menus[self.MenuPath] = self + end + return self +end + +--- Removes the sub menus recursively of this MENU_COALITION. +-- @param #MENU_COALITION self +-- @return #MENU_COALITION self +function MENU_COALITION:RemoveSubMenus() + self:F( self.MenuPath ) + + for MenuID, Menu in pairs( self.Menus ) do + Menu:Remove() + end + +end + +--- Removes the sub menus recursively of this MENU_COALITION. +-- @param #MENU_COALITION self +-- @return #MENU_COALITION self +function MENU_COALITION:Remove() + self:F( self.MenuPath ) + + self:RemoveSubMenus() + missionCommands.removeItemForCoalition( self.MenuCoalition, self.MenuPath ) + self.ParentMenu.Menus[self.MenuPath] = nil + + return nil +end + + +--- The MENU_COALITION_COMMAND class +-- @type MENU_COALITION_COMMAND +-- @extends Menu#MENU +MENU_COALITION_COMMAND = { + ClassName = "MENU_COALITION_COMMAND" +} + +--- Creates a new radio command item for a group +-- @param #MENU_COALITION_COMMAND self +-- @param DCSCoalition#coalition.side MenuCoalition The coalition owning the menu. +-- @param MenuText The text for the menu. +-- @param ParentMenu The parent menu. +-- @param CommandMenuFunction A function that is called when the menu key is pressed. +-- @param CommandMenuArgument An argument for the function. +-- @return #MENU_COALITION_COMMAND self +function MENU_COALITION_COMMAND:New( MenuCoalition, MenuText, ParentMenu, CommandMenuFunction, CommandMenuArgument ) + + -- Arrange meta tables + + local MenuParentPath = {} + if ParentMenu ~= nil then + MenuParentPath = ParentMenu.MenuPath + end + + local self = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) + + self.MenuCoalition = MenuCoalition + self.MenuParentPath = MenuParentPath + self.MenuText = MenuText + self.ParentMenu = ParentMenu + + self:T( { MenuParentPath, MenuText, CommandMenuFunction, CommandMenuArgument } ) + + self.MenuPath = missionCommands.addCommandForCoalition( self.MenuCoalition, MenuText, MenuParentPath, CommandMenuFunction, CommandMenuArgument ) + + self.CommandMenuFunction = CommandMenuFunction + self.CommandMenuArgument = CommandMenuArgument + + ParentMenu.Menus[self.MenuPath] = self + + return self +end + +--- Removes a radio command item for a coalition +-- @param #MENU_COALITION_COMMAND self +-- @return #MENU_COALITION_COMMAND self +function MENU_COALITION_COMMAND:Remove() + self:F( self.MenuPath ) + + missionCommands.removeItemForCoalition( self.MenuCoalition, self.MenuPath ) + self.ParentMenu.Menus[self.MenuPath] = nil + return nil +end +--- GROUP class. +-- +-- @{GROUP} class +-- ============== +-- The @{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. +-- +-- +-- 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. +-- +-- IMPORTANT: ONE SHOULD NEVER SANATIZE these GROUP OBJECT REFERENCES! (make the GROUP object references nil). +-- @module Group +-- @author FlightControl + +Include.File( "Routines" ) +Include.File( "Base" ) +Include.File( "Message" ) +Include.File( "Unit" ) + +--- The GROUP class +-- @type GROUP +-- @extends Base#BASE +-- @field DCSGroup#Group DCSGroup The DCS group class. +-- @field #string GroupName The name of the group. +GROUP = { + ClassName = "GROUP", + GroupName = "", + GroupID = 0, + Controller = nil, + DCSGroup = nil, + WayPointFunctions = {}, + } + +--- A DCSGroup +-- @type DCSGroup +-- @field id_ The ID of the group in DCS + +--- Create a new GROUP from a DCSGroup +-- @param #GROUP self +-- @param DCSGroup#Group GroupName The DCS Group name +-- @return #GROUP self +function GROUP:Register( GroupName ) + local self = BASE:Inherit( self, BASE:New() ) + self:F2( GroupName ) + self.GroupName = GroupName + return self +end + +-- Reference methods. + +--- Find the GROUP wrapper class instance using the DCS Group. +-- @param #GROUP self +-- @param DCSGroup#Group DCSGroup The DCS Group. +-- @return #GROUP The GROUP. +function GROUP:Find( DCSGroup ) + + local GroupName = DCSGroup:getName() -- Group#GROUP + local GroupFound = _DATABASE:FindGroup( GroupName ) + return GroupFound +end + +--- Find the created GROUP using the DCS Group Name. +-- @param #GROUP self +-- @param #string GroupName The DCS Group Name. +-- @return #GROUP The GROUP. +function GROUP:FindByName( GroupName ) + + local GroupFound = _DATABASE:FindGroup( GroupName ) + return GroupFound +end + +-- DCS Group methods support. + +--- Returns the DCS Group. +-- @param #GROUP self +-- @return DCSGroup#Group The DCS Group. +function GROUP:GetDCSGroup() + local DCSGroup = Group.getByName( self.GroupName ) + + if DCSGroup then + return DCSGroup + end + + return nil +end + + +--- Returns if the DCS Group is alive. +-- When the group exists at run-time, this method will return true, otherwise false. +-- @param #GROUP self +-- @return #boolean true if the DCS Group is alive. +function GROUP:IsAlive() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSGroup() + + if DCSGroup then + local GroupIsAlive = DCSGroup:isExist() + self:T3( GroupIsAlive ) + return GroupIsAlive + end + + return nil +end + +--- Destroys the DCS Group and all of its DCS Units. +-- Note that this destroy method also raises a destroy event at run-time. +-- So all event listeners will catch the destroy event of this DCS Group. +-- @param #GROUP self +function GROUP:Destroy() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSGroup() + + if DCSGroup then + for Index, UnitData in pairs( DCSGroup:getUnits() ) do + self:CreateEventCrash( timer.getTime(), UnitData ) + end + DCSGroup:destroy() + DCSGroup = nil + end + + return nil +end + +--- Returns category of the DCS Group. +-- @param #GROUP self +-- @return DCSGroup#Group.Category The category ID +function GROUP:GetCategory() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + local GroupCategory = DCSGroup:getCategory() + self:T3( GroupCategory ) + return GroupCategory + end + + return nil +end + +--- Returns the category name of the DCS Group. +-- @param #GROUP self +-- @return #string Category name = Helicopter, Airplane, Ground Unit, Ship +function GROUP:GetCategoryName() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + local CategoryNames = { + [Group.Category.AIRPLANE] = "Airplane", + [Group.Category.HELICOPTER] = "Helicopter", + [Group.Category.GROUND] = "Ground Unit", + [Group.Category.SHIP] = "Ship", + } + local GroupCategory = DCSGroup:getCategory() + self:T3( GroupCategory ) + + return CategoryNames[GroupCategory] + end + + return nil +end + + +--- Returns the coalition of the DCS Group. +-- @param #GROUP self +-- @return DCSCoalitionObject#coalition.side The coalition side of the DCS Group. +function GROUP:GetCoalition() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + local GroupCoalition = DCSGroup:getCoalition() + self:T3( GroupCoalition ) + return GroupCoalition + end + + return nil +end + +--- Returns the country of the DCS Group. +-- @param #GROUP self +-- @return DCScountry#country.id The country identifier. +-- @return #nil The DCS Group is not existing or alive. +function GROUP:GetCountry() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + local GroupCountry = DCSGroup:getUnit(1):getCountry() + self:T3( GroupCountry ) + return GroupCountry + end + + return nil +end + +--- Returns the name of the DCS Group. +-- @param #GROUP self +-- @return #string The DCS Group name. +function GROUP:GetName() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSGroup() + + if DCSGroup then + local GroupName = DCSGroup:getName() + self:T3( GroupName ) + return GroupName + end + + return nil +end + +--- Returns the DCS Group identifier. +-- @param #GROUP self +-- @return #number The identifier of the DCS Group. +function GROUP:GetID() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSGroup() + + if DCSGroup then + local GroupID = DCSGroup:getID() + self:T3( GroupID ) + return GroupID + end + + return nil +end + +--- Returns the UNIT wrapper class with number UnitNumber. +-- If the underlying DCS Unit does not exist, the method will return nil. . +-- @param #GROUP self +-- @param #number UnitNumber The number of the UNIT wrapper class to be returned. +-- @return Unit#UNIT The UNIT wrapper class. +function GROUP:GetUnit( UnitNumber ) + self:F2( { self.GroupName, UnitNumber } ) + + local DCSGroup = self:GetDCSGroup() + + if DCSGroup then + local UnitFound = UNIT:Find( DCSGroup:getUnit( UnitNumber ) ) + self:T3( UnitFound.UnitName ) + self:T2( UnitFound ) + return UnitFound + end + + return nil +end + +--- Returns the DCS Unit with number UnitNumber. +-- If the underlying DCS Unit does not exist, the method will return nil. . +-- @param #GROUP self +-- @param #number UnitNumber The number of the DCS Unit to be returned. +-- @return DCSUnit#Unit The DCS Unit. +function GROUP:GetDCSUnit( UnitNumber ) + self:F2( { self.GroupName, UnitNumber } ) + + local DCSGroup = self:GetDCSGroup() + + 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:GetDCSGroup() + + 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:GetDCSGroup() + + if DCSGroup then + local GroupInitialSize = DCSGroup:getInitialSize() + self:T3( GroupInitialSize ) + return GroupInitialSize + end + + return nil +end + +--- Returns the UNITs wrappers of the DCS Units of the DCS Group. +-- @param #GROUP self +-- @return #table The UNITs wrappers. +function GROUP:GetUnits() + self:F2( { self.GroupName } ) + local DCSGroup = self:GetDCSGroup() + + if DCSGroup then + local DCSUnits = DCSGroup:getUnits() + local Units = {} + for Index, UnitData in pairs( DCSUnits ) do + Units[#Units+1] = UNIT:Find( UnitData ) + end + self:T3( Units ) + return Units + end + + return nil +end + + +--- Returns the DCS Units of the DCS Group. +-- @param #GROUP self +-- @return #table The DCS Units. +function GROUP:GetDCSUnits() + self:F2( { self.GroupName } ) + local DCSGroup = self:GetDCSGroup() + + if DCSGroup then + local DCSUnits = DCSGroup:getUnits() + self:T3( DCSUnits ) + return DCSUnits + end + + return nil +end + +--- Get the controller for the GROUP. +-- @param #GROUP self +-- @return DCSController#Controller +function GROUP:_GetController() + self:F2( { self.GroupName } ) + local DCSGroup = self:GetDCSGroup() + + if DCSGroup then + local GroupController = DCSGroup:getController() + self:T3( GroupController ) + return GroupController + end + + return nil +end + + +--- Retrieve the group mission and allow to place function hooks within the mission waypoint plan. +-- Use the method @{Group#GROUP:WayPointFunction} to define the hook functions for specific waypoints. +-- Use the method @{Group@GROUP:WayPointExecute) to start the execution of the new mission plan. +-- Note that when WayPointInitialize is called, the Mission of the group is RESTARTED! +-- @param #GROUP self +-- @return #GROUP +function GROUP:WayPointInitialize() + + self.WayPoints = self:GetTaskRoute() + + return self +end + + +--- Registers a waypoint function that will be executed when the group moves over the WayPoint. +-- @param #GROUP 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 group moves over the waypoint. The waypoint function takes variable parameters. +-- @return #GROUP +function GROUP: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 GROUP:TaskFunction( WayPoint, WayPointIndex, FunctionString, FunctionArguments ) + + local DCSTask + + local DCSScript = {} + DCSScript[#DCSScript+1] = "local MissionGroup = GROUP:Find( ... ) " + + if FunctionArguments.n > 0 then + DCSScript[#DCSScript+1] = FunctionString .. "( MissionGroup, " .. table.concat( FunctionArguments, "," ) .. ")" + else + DCSScript[#DCSScript+1] = FunctionString .. "( MissionGroup )" + 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 group will be 1! +-- @param #GROUP self +-- @param #number WayPoint The WayPoint from where to execute the mission. +-- @param #WaitTime The amount seconds to wait before initiating the mission. +-- @return #GROUP +function GROUP:WayPointExecute( WayPoint, WaitTime ) + + if not WayPoint then + WayPoint = 1 + end + + -- When starting the mission from a certain point, the TaskPoints need to be deleted before the given WayPoint. + for TaskPointID = 1, WayPoint - 1 do + table.remove( self.WayPoints, 1 ) + end + + self:T3( self.WayPoints ) + + self:SetTask( self:TaskRoute( self.WayPoints ), WaitTime ) + + return self +end + + +--- Activates a GROUP. +-- @param #GROUP self +function GROUP:Activate() + self:F2( { self.GroupName } ) + trigger.action.activateGroup( self:GetDCSGroup() ) + return self:GetDCSGroup() +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:GetDCSGroup() + + 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:GetDCSGroup() + + if DCSGroup then + local GroupCallSign = DCSGroup:getUnit(1):getCallsign() + self:T3( GroupCallSign ) + return GroupCallSign + end + + return nil +end + +--- Returns the current point (Vec2 vector) of the first DCS Unit in the DCS Group. +-- @return DCSTypes#Vec2 Current Vec2 point of the first DCS Unit of the DCS Group. +function GROUP:GetPointVec2() + self:F2( self.GroupName ) + + local GroupPointVec2 = self:GetUnit(1):GetPointVec2() + self:T3( GroupPointVec2 ) + return GroupPointVec2 +end + +--- Returns the current point (Vec3 vector) of the first DCS Unit in the DCS Group. +-- @return DCSTypes#Vec3 Current Vec3 point of the first DCS Unit of the DCS Group. +function GROUP:GetPointVec3() + self:F2( self.GroupName ) + + local GroupPointVec3 = self:GetUnit(1):GetPointVec3() + self:T3( GroupPointVec3 ) + return GroupPointVec3 +end + + + +-- Is Functions + +--- 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:GetDCSGroup() + + 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:GetDCSGroup() + + 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:GetDCSGroup() + + 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:GetDCSGroup() + + 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:GetDCSGroup() + + 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:GetDCSGroup() + + if DCSGroup then + local AllOnGroundResult = true + + for Index, UnitData in pairs( DCSGroup:getUnits() ) do + if UnitData:inAir() then + AllOnGroundResult = false + end + end + + self:T3( AllOnGroundResult ) + return AllOnGroundResult + end + + return nil +end + +--- Returns the current maximum velocity of the group. +-- Each unit within the group gets evaluated, and the maximum velocity (= the unit which is going the fastest) is returned. +-- @param #GROUP self +-- @return #number Maximum velocity found. +function GROUP:GetMaxVelocity() + self:F2() + + local DCSGroup = self:GetDCSGroup() + + if DCSGroup then + local MaxVelocity = 0 + + for Index, UnitData in pairs( DCSGroup:getUnits() ) do + + local Velocity = UnitData:getVelocity() + local VelocityTotal = math.abs( Velocity.x ) + math.abs( Velocity.y ) + math.abs( Velocity.z ) + + if VelocityTotal < MaxVelocity then + MaxVelocity = VelocityTotal + end + end + + return MaxVelocity + end + + return nil +end + +--- Returns the current minimum height of the group. +-- Each unit within the group gets evaluated, and the minimum height (= the unit which is the lowest elevated) is returned. +-- @param #GROUP self +-- @return #number Minimum height found. +function GROUP:GetMinHeight() + self:F2() + +end + +--- Returns the current maximum height of the group. +-- Each unit within the group gets evaluated, and the maximum height (= the unit which is the highest elevated) is returned. +-- @param #GROUP self +-- @return #number Maximum height found. +function GROUP:GetMaxHeight() + self:F2() + +end + +-- Tasks + +--- Popping current Task from the group. +-- @param #GROUP self +-- @return Group#GROUP self +function GROUP:PopCurrentTask() + self:F2() + + local DCSGroup = self:GetDCSGroup() + + if DCSGroup then + local Controller = self:_GetController() + Controller:popTask() + return self + end + + return nil +end + +--- Pushing Task on the queue from the group. +-- @param #GROUP self +-- @return Group#GROUP self +function GROUP:PushTask( DCSTask, WaitTime ) + self:F2() + + local DCSGroup = self:GetDCSGroup() + + if DCSGroup then + local Controller = self:_GetController() + + -- When a group SPAWNs, it takes about a second to get the group in the simulator. Setting tasks to unspawned groups provides unexpected results. + -- Therefore we schedule the functions to set the mission and options for the Group. + -- Controller:pushTask( DCSTask ) + + if WaitTime then + --routines.scheduleFunction( Controller.pushTask, { Controller, DCSTask }, timer.getTime() + WaitTime ) + SCHEDULER:New( Controller, Controller.pushTask, { DCSTask }, WaitTime ) + else + Controller:pushTask( DCSTask ) + end + + return self + end + + return nil +end + +--- Clearing the Task Queue and Setting the Task on the queue from the group. +-- @param #GROUP self +-- @return Group#GROUP self +function GROUP:SetTask( DCSTask, WaitTime ) + self:F2( { DCSTask } ) + + local DCSGroup = self:GetDCSGroup() + + if DCSGroup then + + local Controller = self:_GetController() + + -- When a group SPAWNs, it takes about a second to get the group in the simulator. Setting tasks to unspawned groups provides unexpected results. + -- Therefore we schedule the functions to set the mission and options for the Group. + -- Controller.setTask( Controller, DCSTask ) + + if not WaitTime then + WaitTime = 1 + end + --routines.scheduleFunction( Controller.setTask, { Controller, DCSTask }, timer.getTime() + WaitTime ) + SCHEDULER:New( Controller, Controller.setTask, { DCSTask }, WaitTime ) + + return self + end + + return nil +end + + +--- Return a condition section for a controlled task +-- @param #GROUP self +-- @param DCSTime#Time time +-- @param #string userFlag +-- @param #boolean userFlagValue +-- @param #string condition +-- @param DCSTime#Time duration +-- @param #number lastWayPoint +-- return DCSTask#Task +function GROUP: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 #GROUP self +-- @param DCSTask#Task DCSTask +-- @param #DCSStopCondition DCSStopCondition +-- @return DCSTask#Task +function GROUP: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 #GROUP self +-- @param #list DCSTasks +-- @return DCSTask#Task +function GROUP:TaskCombo( DCSTasks ) + self:F2( { DCSTasks } ) + + local DCSTaskCombo + + DCSTaskCombo = { + id = 'ComboTask', + params = { + tasks = DCSTasks + } + } + + self:T3( { DCSTaskCombo } ) + return DCSTaskCombo +end + +--- Return a WrappedAction Task taking a Command +-- @param #GROUP self +-- @param DCSCommand#Command DCSCommand +-- @return DCSTask#Task +function GROUP: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 #GROUP self +-- @param DCSCommand#Command DCSCommand +-- @return #GROUP self +function GROUP:SetCommand( DCSCommand ) + self:F2( DCSCommand ) + + local DCSGroup = self:GetDCSGroup() + + if DCSGroup then + local Controller = self:_GetController() + Controller:setCommand( DCSCommand ) + return self + end + + return nil +end + +--- Perform a switch waypoint command +-- @param #GROUP self +-- @param #number FromWayPoint +-- @param #number ToWayPoint +-- @return DCSTask#Task +function GROUP:CommandSwitchWayPoint( FromWayPoint, ToWayPoint, Index ) + self:F2( { FromWayPoint, ToWayPoint, Index } ) + + local CommandSwitchWayPoint = { + id = 'SwitchWaypoint', + params = { + fromWaypointIndex = FromWayPoint, + goToWaypointIndex = ToWayPoint, + }, + } + + self:T3( { CommandSwitchWayPoint } ) + return CommandSwitchWayPoint +end + + +--- Orbit at a specified position at a specified alititude during a specified duration with a specified speed. +-- @param #GROUP self +-- @param DCSTypes#Vec2 Point The point to hold the position. +-- @param #number Altitude The altitude to hold the position. +-- @param #number Speed The speed flying when holding the position. +-- @return #GROUP self +function GROUP:TaskOrbitCircleAtVec2( Point, Altitude, Speed ) + self:F2( { self.GroupName, 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 + +--- Orbit at the current position of the first unit of the group at a specified alititude +-- @param #GROUP self +-- @param #number Altitude The altitude to hold the position. +-- @param #number Speed The speed flying when holding the position. +-- @return #GROUP self +function GROUP:TaskOrbitCircle( Altitude, Speed ) + self:F2( { self.GroupName, Altitude, Speed } ) + + local DCSGroup = self:GetDCSGroup() + + if DCSGroup then + local GroupPoint = self:GetPointVec2() + return self:TaskOrbitCircleAtVec2( GroupPoint, Altitude, Speed ) + end + + return nil +end + + + +--- Hold position at the current position of the first unit of the group. +-- @param #GROUP self +-- @param #number Duration The maximum duration in seconds to hold the position. +-- @return #GROUP self +function GROUP:TaskHoldPosition() + self:F2( { self.GroupName } ) + + return self:TaskOrbitCircle( 30, 10 ) +end + + +--- Land the group at a Vec2Point. +-- @param #GROUP self +-- @param DCSTypes#Vec2 Point The point where to land. +-- @param #number Duration The duration in seconds to stay on the ground. +-- @return #GROUP self +function GROUP:TaskLandAtVec2( Point, Duration ) + self:F2( { self.GroupName, Point, Duration } ) + + 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 + +--- Land the group at a @{Zone#ZONE). +-- @param #GROUP self +-- @param Zone#ZONE Zone The zone where to land. +-- @param #number Duration The duration in seconds to stay on the ground. +-- @return #GROUP self +function GROUP:TaskLandAtZone( Zone, Duration, RandomPoint ) + self:F2( { self.GroupName, Zone, Duration, RandomPoint } ) + + local Point + if RandomPoint then + Point = Zone:GetRandomPointVec2() + else + Point = Zone:GetPointVec2() + end + + local DCSTask = self:TaskLandAtVec2( Point, Duration ) + + self:T3( DCSTask ) + return DCSTask +end + + +--- Attack the Unit. +-- @param #GROUP self +-- @param Unit#UNIT The unit. +-- @return DCSTask#Task The DCS task structure. +function GROUP:TaskAttackUnit( AttackUnit ) + self:F2( { self.GroupName, AttackUnit } ) + +-- AttackUnit = { +-- id = 'AttackUnit', +-- params = { +-- unitId = Unit.ID, +-- weaponType = number, +-- expend = enum AI.Task.WeaponExpend +-- attackQty = number, +-- direction = Azimuth, +-- attackQtyLimit = boolean, +-- groupAttack = boolean, +-- } +-- } + + local DCSTask + DCSTask = { id = 'AttackUnit', + params = { unitId = AttackUnit:GetID(), + expend = AI.Task.WeaponExpend.TWO, + groupAttack = true, + }, + }, + + self:T3( { DCSTask } ) + return DCSTask +end + +--- Attack a Group. +-- @param #GROUP self +-- @param Group#GROUP AttackGroup The Group to be attacked. +-- @return DCSTask#Task The DCS task structure. +function GROUP:TaskAttackGroup( AttackGroup ) + self:F2( { self.GroupName, AttackGroup } ) + +-- 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 DCSTask + DCSTask = { id = 'AttackGroup', + params = { groupId = AttackGroup:GetID(), + expend = AI.Task.WeaponExpend.TWO, + }, + }, + + self:T3( { DCSTask } ) + return DCSTask +end + +--- Fires at a VEC2 point. +-- @param #GROUP self +-- @param DCSTypes#Vec2 The point to fire at. +-- @param DCSTypes#Distance Radius The radius of the zone to deploy the fire at. +-- @return DCSTask#Task The DCS task structure. +function GROUP:TaskFireAtPoint( PointVec2, Radius ) + self:F2( { self.GroupName, PointVec2, Radius } ) + +-- FireAtPoint = { +-- id = 'FireAtPoint', +-- params = { +-- point = Vec2, +-- radius = Distance, +-- } +-- } + + local DCSTask + DCSTask = { id = 'FireAtPoint', + params = { point = PointVec2, + radius = Radius, + } + } + + self:T3( { DCSTask } ) + return DCSTask +end + + + +--- Move the group to a Vec2 Point, wait for a defined duration and embark a group. +-- @param #GROUP self +-- @param DCSTypes#Vec2 Point The point where to wait. +-- @param #number Duration The duration in seconds to wait. +-- @param #GROUP EmbarkingGroup The group to be embarked. +-- @return DCSTask#Task The DCS task structure +function GROUP:TaskEmbarkingAtVec2( Point, Duration, EmbarkingGroup ) + self:F2( { self.GroupName, Point, Duration, EmbarkingGroup.DCSGroup } ) + + local DCSTask + DCSTask = { id = 'Embarking', + params = { x = Point.x, + y = Point.y, + duration = Duration, + groupsForEmbarking = { EmbarkingGroup.GroupID }, + durationFlag = true, + distributionFlag = false, + distribution = {}, + } + } + + self:T3( { DCSTask } ) + return DCSTask +end + +--- Move to a defined Vec2 Point, and embark to a group when arrived within a defined Radius. +-- @param #GROUP self +-- @param DCSTypes#Vec2 Point The point where to wait. +-- @param #number Radius The radius of the embarking zone around the Point. +-- @return DCSTask#Task The DCS task structure. +function GROUP:TaskEmbarkToTransportAtVec2( Point, Radius ) + self:F2( { self.GroupName, Point, Radius } ) + + local DCSTask --DCSTask#Task + DCSTask = { id = 'EmbarkToTransport', + params = { x = Point.x, + y = Point.y, + zoneRadius = Radius, + } + } + + self:T3( { DCSTask } ) + return DCSTask +end + +--- Return a Misson task from a mission template. +-- @param #GROUP self +-- @param #table TaskMission A table containing the mission task. +-- @return DCSTask#Task +function GROUP: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 #GROUP self +-- @param #table Points A table of route points. +-- @return DCSTask#Task +function GROUP:TaskRoute( Points ) + self:F2( Points ) + + local DCSTask + DCSTask = { id = 'Mission', params = { route = { points = Points, }, }, } + + self:T3( { DCSTask } ) + return DCSTask +end + +--- Make the DCS Group to fly to a given point and hover. +-- @param #GROUP self +-- @param DCSTypes#Vec3 Point The destination point in Vec3 format. +-- @param #number Speed The speed to travel. +-- @return #GROUP self +function GROUP:TaskRouteToVec2( Point, Speed ) + self:F2( { Point, Speed } ) + + local GroupPoint = self:GetUnit( 1 ):GetPointVec2() + + local PointFrom = {} + PointFrom.x = GroupPoint.x + PointFrom.y = GroupPoint.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 + +--- Make the DCS Group to fly to a given point and hover. +-- @param #GROUP self +-- @param DCSTypes#Vec3 Point The destination point in Vec3 format. +-- @param #number Speed The speed to travel. +-- @return #GROUP self +function GROUP:TaskRouteToVec3( Point, Speed ) + self:F2( { Point, Speed } ) + + local GroupPoint = self:GetUnit( 1 ):GetPointVec3() + + local PointFrom = {} + PointFrom.x = GroupPoint.x + PointFrom.y = GroupPoint.z + PointFrom.alt = GroupPoint.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 group to follow a given route. +-- @param #GROUP self +-- @param #table GoPoints A table of Route Points. +-- @return #GROUP self +function GROUP:Route( GoPoints ) + self:F2( GoPoints ) + + local DCSGroup = self:GetDCSGroup() + + if DCSGroup then + local Points = routines.utils.deepCopy( GoPoints ) + local MissionTask = { id = 'Mission', params = { route = { points = Points, }, }, } + local Controller = self:_GetController() + --Controller.setTask( Controller, MissionTask ) + --routines.scheduleFunction( Controller.setTask, { Controller, MissionTask}, timer.getTime() + 1 ) + SCHEDULER:New( Controller, Controller.setTask, { MissionTask }, 1 ) + return self + end + + return nil +end + + + +--- Route the group to a given zone. +-- The group final destination point can be randomized. +-- A speed can be given in km/h. +-- A given formation can be given. +-- @param #GROUP self +-- @param Zone#ZONE Zone The zone where to route to. +-- @param #boolean Randomize Defines whether to target point gets randomized within the Zone. +-- @param #number Speed The speed. +-- @param Base#FORMATION Formation The formation string. +function GROUP:TaskRouteToZone( Zone, Randomize, Speed, Formation ) + self:F2( Zone ) + + local DCSGroup = self:GetDCSGroup() + + if DCSGroup then + + local GroupPoint = self:GetPointVec2() + + local PointFrom = {} + PointFrom.x = GroupPoint.x + PointFrom.y = GroupPoint.y + PointFrom.type = "Turning Point" + PointFrom.action = "Cone" + PointFrom.speed = 20 / 1.6 + + + local PointTo = {} + local ZonePoint + + if Randomize then + ZonePoint = Zone:GetRandomPointVec2() + else + ZonePoint = Zone:GetPointVec2() + end + + PointTo.x = ZonePoint.x + PointTo.y = ZonePoint.y + PointTo.type = "Turning Point" + + if Formation then + PointTo.action = Formation + else + PointTo.action = "Cone" + end + + if Speed then + PointTo.speed = Speed + else + PointTo.speed = 20 / 1.6 + end + + local Points = { PointFrom, PointTo } + + self:T3( Points ) + + self:Route( Points ) + + return self + end + + return nil +end + +-- Commands + +--- Do Script command +-- @param #GROUP self +-- @param #string DoScript +-- @return #DCSCommand +function GROUP:CommandDoScript( DoScript ) + + local DCSDoScript = { + id = "Script", + params = { + command = DoScript, + }, + } + + self:T3( DCSDoScript ) + return DCSDoScript +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 + end + + return nil +end + + +function GROUP:GetDetectedTargets() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + return self:_GetController():getDetectedTargets() + end + + return nil +end + +function GROUP:IsTargetDetected( DCSObject ) + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup 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 GROUP hold their weapons? +-- @param #GROUP self +-- @return #boolean +function GROUP:OptionROEHoldFirePossible() + self:F2( { self.GroupName } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + if self:IsAir() or self:IsGround() or self:IsShip() then + return true + end + + return false + end + + return nil +end + +--- Holding weapons. +-- @param Group#GROUP self +-- @return Group#GROUP self +function GROUP:OptionROEHoldFire() + self:F2( { self.GroupName } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup 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 GROUP attack returning on enemy fire? +-- @param #GROUP self +-- @return #boolean +function GROUP:OptionROEReturnFirePossible() + self:F2( { self.GroupName } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + if self:IsAir() or self:IsGround() or self:IsShip() then + return true + end + + return false + end + + return nil +end + +--- Return fire. +-- @param #GROUP self +-- @return #GROUP self +function GROUP:OptionROEReturnFire() + self:F2( { self.GroupName } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup 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 GROUP attack designated targets? +-- @param #GROUP self +-- @return #boolean +function GROUP:OptionROEOpenFirePossible() + self:F2( { self.GroupName } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + if self:IsAir() or self:IsGround() or self:IsShip() then + return true + end + + return false + end + + return nil +end + +--- Openfire. +-- @param #GROUP self +-- @return #GROUP self +function GROUP:OptionROEOpenFire() + self:F2( { self.GroupName } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup 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 GROUP attack targets of opportunity? +-- @param #GROUP self +-- @return #boolean +function GROUP:OptionROEWeaponFreePossible() + self:F2( { self.GroupName } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + if self:IsAir() then + return true + end + + return false + end + + return nil +end + +--- Weapon free. +-- @param #GROUP self +-- @return #GROUP self +function GROUP:OptionROEWeaponFree() + self:F2( { self.GroupName } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup 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 GROUP ignore enemy fire? +-- @param #GROUP self +-- @return #boolean +function GROUP:OptionROTNoReactionPossible() + self:F2( { self.GroupName } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + if self:IsAir() then + return true + end + + return false + end + + return nil +end + + +--- No evasion on enemy threats. +-- @param #GROUP self +-- @return #GROUP self +function GROUP:OptionROTNoReaction() + self:F2( { self.GroupName } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup 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 GROUP evade using passive defenses? +-- @param #GROUP self +-- @return #boolean +function GROUP:OptionROTPassiveDefensePossible() + self:F2( { self.GroupName } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + if self:IsAir() then + return true + end + + return false + end + + return nil +end + +--- Evasion passive defense. +-- @param #GROUP self +-- @return #GROUP self +function GROUP:OptionROTPassiveDefense() + self:F2( { self.GroupName } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup 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 GROUP evade on enemy fire? +-- @param #GROUP self +-- @return #boolean +function GROUP:OptionROTEvadeFirePossible() + self:F2( { self.GroupName } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + if self:IsAir() then + return true + end + + return false + end + + return nil +end + + +--- Evade on fire. +-- @param #GROUP self +-- @return #GROUP self +function GROUP:OptionROTEvadeFire() + self:F2( { self.GroupName } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup 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 GROUP evade on fire using vertical manoeuvres? +-- @param #GROUP self +-- @return #boolean +function GROUP:OptionROTVerticalPossible() + self:F2( { self.GroupName } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + if self:IsAir() then + return true + end + + return false + end + + return nil +end + + +--- Evade on fire using vertical manoeuvres. +-- @param #GROUP self +-- @return #GROUP self +function GROUP:OptionROTVertical() + self:F2( { self.GroupName } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup 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 + +-- Message APIs + +--- Returns a message for a coalition or a client. +-- @param #GROUP self +-- @param #string Message The message text +-- @param #Duration Duration The duration of the message. +-- @return Message#MESSAGE +function GROUP:Message( Message, Duration ) + self:F2( { Message, Duration } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + return MESSAGE:New( Message, self:GetCallsign() .. " (" .. self:GetTypeName() .. ")", Duration, self:GetClassNameAndID() ) + end + + return nil +end + +--- Send a message to all coalitions. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #GROUP self +-- @param #string Message The message text +-- @param #Duration Duration The duration of the message. +function GROUP:MessageToAll( Message, Duration ) + self:F2( { Message, Duration } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + self:Message( Message, Duration ):ToAll() + end + + return nil +end + +--- Send a message to the red coalition. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #GROUP self +-- @param #string Message The message text +-- @param #Duration Duration The duration of the message. +function GROUP:MessageToRed( Message, Duration ) + self:F2( { Message, Duration } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + self:Message( Message, Duration ):ToRed() + end + + return nil +end + +--- Send a message to the blue coalition. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #GROUP self +-- @param #string Message The message text +-- @param #Duration Duration The duration of the message. +function GROUP:MessageToBlue( Message, Duration ) + self:F2( { Message, Duration } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + self:Message( Message, Duration ):ToBlue() + end + + return nil +end + +--- Send a message to a client. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #GROUP self +-- @param #string Message The message text +-- @param #Duration Duration The duration of the message. +-- @param Client#CLIENT Client The client object receiving the message. +function GROUP:MessageToClient( Message, Duration, Client ) + self:F2( { Message, Duration } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + self:Message( Message, Duration ):ToClient( Client ) + end + + return nil +end +--- UNIT Class +-- +-- @{UNIT} class +-- ============== +-- 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. +-- +-- +-- 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). +-- +-- DCS UNIT APIs +-- ============= +-- The DCS Unit APIs are used extensively within MOOSE. The UNIT class has for each DCS Unit API a corresponding method. +-- To be able to distinguish easily in your code the difference between a UNIT API call and a DCS Unit API call, +-- the first letter of the method is also capitalized. So, by example, the DCS Unit method @{DCSUnit#Unit.getName}() +-- is implemented in the UNIT class as @{#UNIT.GetName}(). +-- +-- Additional UNIT APIs +-- ==================== +-- The UNIT class comes with additional methods. Find below a summary. +-- +-- 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. +-- +-- Position, Point +-- --------------- +-- The UNIT class provides methods to obtain the current point or position of the DCS Unit. +-- The @{#UNIT.GetPointVec2}(), @{#UNIT.GetPointVec3}() will obtain the current location of the DCS Unit in a Vec2 (2D) or a 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. +-- +-- Alive +-- ----- +-- The @{#UNIT.IsAlive}(), @{#UNIT.IsActive}() methods determines if the DCS Unit is alive, meaning, it is existing and active. +-- +-- Test for other units in radius +-- ------------------------------ +-- One can test if another DCS Unit is within a given radius of the current DCS Unit, by using the @{#UNIT.OtherUnitInRadius}() method. +-- +-- More functions will be added +-- ---------------------------- +-- During the MOOSE development, more functions will be added. A complete list of the current functions is below. +-- +-- +-- +-- +-- @module Unit +-- @author FlightControl + +Include.File( "Routines" ) +Include.File( "Base" ) +Include.File( "Message" ) + +--- The UNIT class +-- @type UNIT +-- @extends Base#BASE +-- @field #UNIT.FlareColor FlareColor +-- @field #UNIT.SmokeColor SmokeColor +UNIT = { + ClassName="UNIT", + CategoryName = { + [Unit.Category.AIRPLANE] = "Airplane", + [Unit.Category.HELICOPTER] = "Helicoper", + [Unit.Category.GROUND_UNIT] = "Ground Unit", + [Unit.Category.SHIP] = "Ship", + [Unit.Category.STRUCTURE] = "Structure", + }, + FlareColor = { + Green = trigger.flareColor.Green, + Red = trigger.flareColor.Red, + White = trigger.flareColor.White, + Yellow = trigger.flareColor.Yellow + }, + SmokeColor = { + Green = trigger.smokeColor.Green, + Red = trigger.smokeColor.Red, + White = trigger.smokeColor.White, + Orange = trigger.smokeColor.Orange, + Blue = trigger.smokeColor.Blue + }, + } + +--- FlareColor +-- @type UNIT.FlareColor +-- @field Green +-- @field Red +-- @field White +-- @field Yellow + +--- SmokeColor +-- @type UNIT.SmokeColor +-- @field Green +-- @field Red +-- @field White +-- @field Orange +-- @field Blue + +-- Registration. + +--- Create a new UNIT from DCSUnit. +-- @param #UNIT self +-- @param DCSUnit#Unit DCSUnit +-- @param Database#DATABASE Database +-- @return Unit#UNIT +function UNIT:Register( UnitName ) + + local self = BASE:Inherit( self, BASE:New() ) + self:F2( UnitName ) + self.UnitName = UnitName + return self +end + +-- Reference methods. + +--- Finds a UNIT from the _DATABASE using a DCSUnit object. +-- @param #UNIT self +-- @param DCSUnit#Unit DCSUnit An existing DCS Unit object reference. +-- @return Unit#UNIT self +function UNIT:Find( DCSUnit ) + + local UnitName = DCSUnit:getName() + local UnitFound = _DATABASE:FindUnit( UnitName ) + return UnitFound +end + +--- Find a UNIT in the _DATABASE using the name of an existing DCS Unit. +-- @param #UNIT self +-- @param #string UnitName The Unit Name. +-- @return Unit#UNIT self +function UNIT:FindByName( UnitName ) + + local UnitFound = _DATABASE:FindUnit( UnitName ) + return UnitFound +end + +function UNIT:GetDCSUnit() + local DCSUnit = Unit.getByName( self.UnitName ) + + if DCSUnit then + return DCSUnit + end + + return nil +end + +--- Returns coalition of the Unit. +-- @param Unit#UNIT self +-- @return DCSCoalitionObject#coalition.side The side of the coalition. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetCoalition() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitCoalition = DCSUnit:getCoalition() + self:T3( UnitCoalition ) + return UnitCoalition + end + + return nil +end + +--- Returns country of the Unit. +-- @param Unit#UNIT self +-- @return DCScountry#country.id The country identifier. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetCountry() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitCountry = DCSUnit:getCountry() + self:T3( UnitCountry ) + return UnitCountry + end + + return nil +end + + +--- Returns DCS Unit object name. +-- The function provides access to non-activated units too. +-- @param Unit#UNIT self +-- @return #string The name of the DCS Unit. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetName() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitName = self.UnitName + return UnitName + end + + return nil +end + + +--- Returns if the unit is alive. +-- @param Unit#UNIT self +-- @return #boolean true if Unit is alive. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:IsAlive() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitIsAlive = DCSUnit:isExist() + return UnitIsAlive + end + + return false +end + +--- Returns if the unit is activated. +-- @param Unit#UNIT self +-- @return #boolean true if Unit is activated. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:IsActive() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + + local UnitIsActive = DCSUnit:isActive() + return UnitIsActive + end + + return nil +end + +--- Returns name of the player that control the unit or nil if the unit is controlled by A.I. +-- @param Unit#UNIT self +-- @return #string Player Name +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetPlayerName() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + + local PlayerName = DCSUnit:getPlayerName() + if PlayerName == nil then + PlayerName = "" + end + return PlayerName + end + + return nil +end + +--- Returns the unit's unique identifier. +-- @param Unit#UNIT self +-- @return DCSUnit#Unit.ID Unit ID +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetID() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitID = DCSUnit:getID() + return UnitID + end + + return nil +end + +--- Returns the unit's number in the group. +-- The number is the same number the unit has in ME. +-- It may not be changed during the mission. +-- If any unit in the group is destroyed, the numbers of another units will not be changed. +-- @param Unit#UNIT self +-- @return #number The Unit number. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetNumber() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitNumber = DCSUnit:getNumber() + return UnitNumber + end + + return nil +end + +--- Returns the unit's group if it exist and nil otherwise. +-- @param Unit#UNIT self +-- @return Group#GROUP The Group of the Unit. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetGroup() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitGroup = DCSUnit:getGroup() + return UnitGroup + end + + return nil +end + + +--- Returns the unit's callsign - the localized string. +-- @param Unit#UNIT self +-- @return #string The Callsign of the Unit. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetCallSign() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitCallSign = DCSUnit:getCallsign() + return UnitCallSign + end + + return nil +end + +--- Returns the unit's health. Dead units has health <= 1.0. +-- @param Unit#UNIT self +-- @return #number The Unit's health value. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetLife() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitLife = DCSUnit:getLife() + return UnitLife + end + + return nil +end + +--- Returns the Unit's initial health. +-- @param Unit#UNIT self +-- @return #number The Unit's initial health value. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetLife0() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitLife0 = DCSUnit:getLife0() + return UnitLife0 + end + + return nil +end + +--- Returns relative amount of fuel (from 0.0 to 1.0) the unit has in its internal tanks. If there are additional fuel tanks the value may be greater than 1.0. +-- @param Unit#UNIT self +-- @return #number The relative amount of fuel (from 0.0 to 1.0). +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetFuel() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitFuel = DCSUnit:getFuel() + return UnitFuel + end + + return nil +end + +--- Returns the Unit's ammunition. +-- @param Unit#UNIT self +-- @return DCSUnit#Unit.Ammo +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetAmmo() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitAmmo = DCSUnit:getAmmo() + return UnitAmmo + end + + return nil +end + +--- Returns the unit sensors. +-- @param Unit#UNIT self +-- @return DCSUnit#Unit.Sensors +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetSensors() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitSensors = DCSUnit:getSensors() + return UnitSensors + end + + return nil +end + +-- Need to add here a function per sensortype +-- unit:hasSensors(Unit.SensorType.RADAR, Unit.RadarType.AS) + +--- Returns two values: +-- +-- * First value indicates if at least one of the unit's radar(s) is on. +-- * Second value is the object of the radar's interest. Not nil only if at least one radar of the unit is tracking a target. +-- @param Unit#UNIT self +-- @return #boolean Indicates if at least one of the unit's radar(s) is on. +-- @return DCSObject#Object The object of the radar's interest. Not nil only if at least one radar of the unit is tracking a target. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetRadar() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitRadarOn, UnitRadarObject = DCSUnit:getRadar() + return UnitRadarOn, UnitRadarObject + end + + return nil, nil +end + +-- Need to add here functions to check if radar is on and which object etc. + +--- Returns unit descriptor. Descriptor type depends on unit category. +-- @param Unit#UNIT self +-- @return DCSUnit#Unit.Desc The Unit descriptor. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetDesc() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitDesc = DCSUnit:getDesc() + return UnitDesc + end + + return nil +end + + +--- Returns the type name of the DCS Unit. +-- @param Unit#UNIT self +-- @return #string The type name of the DCS Unit. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetTypeName() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitTypeName = DCSUnit:getTypeName() + self:T3( UnitTypeName ) + return UnitTypeName + end + + return nil +end + + + +--- Returns the prefix name of the DCS Unit. A prefix name is a part of the name before a '#'-sign. +-- DCS Units spawned with the @{SPAWN} class contain a '#'-sign to indicate the end of the (base) DCS Unit name. +-- The spawn sequence number and unit number are contained within the name after the '#' sign. +-- @param Unit#UNIT self +-- @return #string The name of the DCS Unit. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetPrefix() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitPrefix = string.match( self.UnitName, ".*#" ):sub( 1, -2 ) + self:T3( UnitPrefix ) + return UnitPrefix + end + + return nil +end + + + +--- Returns the @{DCSTypes#Vec2} vector indicating the point in 2D of the DCS Unit within the mission. +-- @param Unit#UNIT self +-- @return DCSTypes#Vec2 The 2D point vector of the DCS Unit. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetPointVec2() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitPointVec3 = DCSUnit:getPosition().p + + local UnitPointVec2 = {} + UnitPointVec2.x = UnitPointVec3.x + UnitPointVec2.y = UnitPointVec3.z + + self:T3( UnitPointVec2 ) + return UnitPointVec2 + end + + return nil +end + + +--- Returns the @{DCSTypes#Vec3} vector indicating the point in 3D of the DCS Unit within the mission. +-- @param Unit#UNIT self +-- @return DCSTypes#Vec3 The 3D point vector of the DCS Unit. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetPointVec3() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitPointVec3 = DCSUnit:getPosition().p + self:T3( UnitPointVec3 ) + return UnitPointVec3 + end + + return nil +end + +--- Returns the @{DCSTypes#Position3} position vectors indicating the point and direction vectors in 3D of the DCS Unit within the mission. +-- @param Unit#UNIT self +-- @return DCSTypes#Position The 3D position vectors of the DCS Unit. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetPositionVec3() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitPosition = DCSUnit:getPosition() + self:T3( UnitPosition ) + return UnitPosition + end + + return nil +end + +--- Returns the DCS Unit velocity vector. +-- @param Unit#UNIT self +-- @return DCSTypes#Vec3 The velocity vector +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetVelocity() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitVelocityVec3 = DCSUnit:getVelocity() + self:T3( UnitVelocityVec3 ) + return UnitVelocityVec3 + end + + return nil +end + +--- Returns true if the DCS Unit is in the air. +-- @param Unit#UNIT self +-- @return #boolean true if in the air. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:InAir() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitInAir = DCSUnit:inAir() + self:T3( UnitInAir ) + return UnitInAir + end + + return nil +end + +--- Returns the altitude of the DCS Unit. +-- @param Unit#UNIT self +-- @return DCSTypes#Distance The altitude of the DCS Unit. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetAltitude() + self:F2() + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitPointVec3 = DCSUnit:getPoint() --DCSTypes#Vec3 + return UnitPointVec3.y + end + + return nil +end + +--- Returns true if there is an **other** DCS Unit within a radius of the current 2D point of the DCS Unit. +-- @param Unit#UNIT self +-- @param Unit#UNIT AwaitUnit The other UNIT wrapper object. +-- @param Radius The radius in meters with the DCS Unit in the centre. +-- @return true If the other DCS Unit is within the radius of the 2D point of the DCS Unit. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:OtherUnitInRadius( AwaitUnit, Radius ) + self:F2( { self.UnitName, AwaitUnit.UnitName, Radius } ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitPos = self:GetPointVec3() + local AwaitUnitPos = AwaitUnit:GetPointVec3() + + if (((UnitPos.x - AwaitUnitPos.x)^2 + (UnitPos.z - AwaitUnitPos.z)^2)^0.5 <= Radius) then + self:T3( "true" ) + return true + else + self:T3( "false" ) + return false + end + end + + return nil +end + +--- Returns the DCS Unit category name as defined within the DCS Unit Descriptor. +-- @param Unit#UNIT self +-- @return #string The DCS Unit Category Name +function UNIT:GetCategoryName() + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitCategoryName = self.CategoryName[ self:GetDesc().category ] + return UnitCategoryName + end + + return nil +end + +--- Signal a flare at the position of the UNIT. +-- @param #UNIT self +function UNIT:Flare( FlareColor ) + self:F2() + trigger.action.signalFlare( self:GetPointVec3(), FlareColor , 0 ) +end + +--- Signal a white flare at the position of the UNIT. +-- @param #UNIT self +function UNIT:FlareWhite() + self:F2() + trigger.action.signalFlare( self:GetPointVec3(), trigger.flareColor.White , 0 ) +end + +--- Signal a yellow flare at the position of the UNIT. +-- @param #UNIT self +function UNIT:FlareYellow() + self:F2() + trigger.action.signalFlare( self:GetPointVec3(), trigger.flareColor.Yellow , 0 ) +end + +--- Signal a green flare at the position of the UNIT. +-- @param #UNIT self +function UNIT:FlareGreen() + self:F2() + trigger.action.signalFlare( self:GetPointVec3(), trigger.flareColor.Green , 0 ) +end + +--- Signal a red flare at the position of the UNIT. +-- @param #UNIT self +function UNIT:FlareRed() + self:F2() + trigger.action.signalFlare( self:GetPointVec3(), trigger.flareColor.Red, 0 ) +end + +--- Smoke the UNIT. +-- @param #UNIT self +function UNIT:Smoke( SmokeColor ) + self:F2() + trigger.action.smoke( self:GetPointVec3(), SmokeColor ) +end + +--- Smoke the UNIT Green. +-- @param #UNIT self +function UNIT:SmokeGreen() + self:F2() + trigger.action.smoke( self:GetPointVec3(), trigger.smokeColor.Green ) +end + +--- Smoke the UNIT Red. +-- @param #UNIT self +function UNIT:SmokeRed() + self:F2() + trigger.action.smoke( self:GetPointVec3(), trigger.smokeColor.Red ) +end + +--- Smoke the UNIT White. +-- @param #UNIT self +function UNIT:SmokeWhite() + self:F2() + trigger.action.smoke( self:GetPointVec3(), trigger.smokeColor.White ) +end + +--- Smoke the UNIT Orange. +-- @param #UNIT self +function UNIT:SmokeOrange() + self:F2() + trigger.action.smoke( self:GetPointVec3(), trigger.smokeColor.Orange ) +end + +--- Smoke the UNIT Blue. +-- @param #UNIT self +function UNIT:SmokeBlue() + self:F2() + trigger.action.smoke( self:GetPointVec3(), trigger.smokeColor.Blue ) +end + +-- Is methods + +--- Returns if the unit is of an air category. +-- If the unit is a helicopter or a plane, then this method will return true, otherwise false. +-- @param #UNIT self +-- @return #boolean Air category evaluation result. +function UNIT:IsAir() + self:F2() + + local UnitDescriptor = self.DCSUnit:getDesc() + self:T3( { UnitDescriptor.category, Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } ) + + local IsAirResult = ( UnitDescriptor.category == Unit.Category.AIRPLANE ) or ( UnitDescriptor.category == Unit.Category.HELICOPTER ) + + self:T3( IsAirResult ) + return IsAirResult +end + +--- ZONE Classes +-- @module Zone + +Include.File( "Routines" ) +Include.File( "Base" ) +Include.File( "Message" ) + +--- The ZONE class +-- @type ZONE +-- @Extends Base#BASE +ZONE = { + ClassName="ZONE", + } + +function ZONE:New( ZoneName ) + local self = BASE:Inherit( self, BASE:New() ) + self:F( ZoneName ) + + local Zone = trigger.misc.getZone( ZoneName ) + + if not Zone then + error( "Zone " .. ZoneName .. " does not exist." ) + return nil + end + + self.Zone = Zone + self.ZoneName = ZoneName + + return self +end + +function ZONE:GetPointVec2() + self:F( self.ZoneName ) + + local Zone = trigger.misc.getZone( self.ZoneName ) + local Point = { x = Zone.point.x, y = Zone.point.z } + + self:T( { Zone, Point } ) + + return Point +end + +function ZONE:GetPointVec3( Height ) + self:F( self.ZoneName ) + + local Zone = trigger.misc.getZone( self.ZoneName ) + local Point = { x = Zone.point.x, y = land.getHeight( self:GetPointVec2() ) + Height, z = Zone.point.z } + + self:T( { Zone, Point } ) + + return Point +end + +function ZONE:GetRandomPointVec2() + self:F( self.ZoneName ) + + local Point = {} + + local Zone = trigger.misc.getZone( self.ZoneName ) + + local angle = math.random() * math.pi*2; + Point.x = Zone.point.x + math.cos( angle ) * math.random() * Zone.radius; + Point.y = Zone.point.z + math.sin( angle ) * math.random() * Zone.radius; + + self:T( { Zone, Point } ) + + return Point +end + +function ZONE:GetRadius() + self:F( self.ZoneName ) + + local Zone = trigger.misc.getZone( self.ZoneName ) + + self:T( { Zone } ) + + return Zone.radius +end + +--- The CLIENT models client units in multi player missions. +-- +-- @{#CLIENT} class +-- ================ +-- Clients are those **Units** defined within the Mission Editor that have the skillset defined as __Client__ or __Player__. +-- Note that clients are NOT the same as Units, they are NOT necessarily alive. +-- The @{CLIENT} class is a wrapper class to handle the DCS Unit objects that have the skillset defined as __Client__ or __Player__: +-- +-- * 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. +-- +-- CLIENT reference methods +-- ======================= +-- For each DCS Unit having skill level Player or Client, a CLIENT wrapper object (instance) will be created within the _@{DATABASE} object. +-- This is done at the beginning of the mission (when the mission starts). +-- +-- The CLIENT class does not contain a :New() method, rather it provides :Find() methods to retrieve the object reference +-- using the DCS Unit or the DCS UnitName. +-- +-- Another thing to know is that CLIENT objects do not "contain" the DCS Unit object. +-- The CLIENT methods will reference the DCS Unit object by name when it is needed during API execution. +-- If the DCS Unit object does not exist or is nil, the CLIENT methods will return nil and log an exception in the DCS.log file. +-- +-- The CLIENT class provides the following functions to retrieve quickly the relevant CLIENT instance: +-- +-- * @{#CLIENT.Find}(): Find a CLIENT instance from the _DATABASE object using a DCS Unit object. +-- * @{#CLIENT.FindByName}(): Find a CLIENT instance from the _DATABASE object using a DCS Unit name. +-- +-- IMPORTANT: ONE SHOULD NEVER SANATIZE these CLIENT OBJECT REFERENCES! (make the CLIENT object references nil). +-- +-- @module Client +-- @author FlightControl + +Include.File( "Routines" ) +Include.File( "Base" ) +Include.File( "Cargo" ) +Include.File( "Message" ) + + +--- The CLIENT class +-- @type CLIENT +-- @extends Unit#UNIT +CLIENT = { + ONBOARDSIDE = { + NONE = 0, + LEFT = 1, + RIGHT = 2, + BACK = 3, + FRONT = 4 + }, + ClassName = "CLIENT", + ClientName = nil, + ClientAlive = false, + ClientTransport = false, + ClientBriefingShown = false, + _Menus = {}, + _Tasks = {}, + Messages = { + } +} + + +--- Finds a CLIENT from the _DATABASE using the relevant DCS Unit. +-- @param #CLIENT self +-- @param #string ClientName Name of the DCS **Unit** as defined within the Mission Editor. +-- @param #string ClientBriefing Text that describes the briefing of the mission when a Player logs into the Client. +-- @return #CLIENT +-- @usage +-- -- Create new Clients. +-- local Mission = MISSIONSCHEDULER.AddMission( 'Russia Transport Troops SA-6', 'Operational', 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', 'Russia' ) +-- Mission:AddGoal( DeploySA6TroopsGoal ) +-- +-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 1' ):Transport() ) +-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 3' ):Transport() ) +-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 2' ):Transport() ) +-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 4' ):Transport() ) +function CLIENT:Find( DCSUnit ) + local ClientName = DCSUnit:getName() + local ClientFound = _DATABASE:FindClient( ClientName ) + + if ClientFound then + ClientFound:F( ClientName ) + return ClientFound + end + + error( "CLIENT not found for: " .. ClientName ) +end + + +--- Finds a CLIENT from the _DATABASE using the relevant Client Unit Name. +-- As an optional parameter, a briefing text can be given also. +-- @param #CLIENT self +-- @param #string ClientName Name of the DCS **Unit** as defined within the Mission Editor. +-- @param #string ClientBriefing Text that describes the briefing of the mission when a Player logs into the Client. +-- @return #CLIENT +-- @usage +-- -- Create new Clients. +-- local Mission = MISSIONSCHEDULER.AddMission( 'Russia Transport Troops SA-6', 'Operational', 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', 'Russia' ) +-- Mission:AddGoal( DeploySA6TroopsGoal ) +-- +-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 1' ):Transport() ) +-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 3' ):Transport() ) +-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 2' ):Transport() ) +-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 4' ):Transport() ) +function CLIENT:FindByName( ClientName, ClientBriefing ) + local ClientFound = _DATABASE:FindClient( ClientName ) + + if ClientFound then + ClientFound:F( { ClientName, ClientBriefing } ) + ClientFound:AddBriefing( ClientBriefing ) + ClientFound.MessageSwitch = true + + return ClientFound + end + + error( "CLIENT not found for: " .. ClientName ) +end + +function CLIENT:Register( ClientName ) + local self = BASE:Inherit( self, UNIT:Register( ClientName ) ) + + self:F( ClientName ) + self.ClientName = ClientName + self.MessageSwitch = true + self.ClientAlive2 = false + + --self.AliveCheckScheduler = routines.scheduleFunction( self._AliveCheckScheduler, { self }, timer.getTime() + 1, 5 ) + self.AliveCheckScheduler = SCHEDULER:New( self, self._AliveCheckScheduler, {}, 1, 5 ) + + 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, self.ClientName .. '/ClientBriefing', "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, self.ClientName .. '/MissionBriefing', "Mission Briefing" ) + end + + return self +end + + + +--- Resets a CLIENT. +-- @param #CLIENT self +-- @param #string ClientName Name of the Group as defined within the Mission Editor. The Group must have a Unit with the type Client. +function CLIENT:Reset( ClientName ) + self:F() + self._Menus = {} +end + +-- Is Functions + +--- Checks if the CLIENT is a multi-seated UNIT. +-- @param #CLIENT self +-- @return #boolean true if multi-seated. +function CLIENT:IsMultiSeated() + self:F( self.ClientName ) + + local ClientMultiSeatedTypes = { + ["Mi-8MT"] = "Mi-8MT", + ["UH-1H"] = "UH-1H", + ["P-51B"] = "P-51B" + } + + if self:IsAlive() then + local ClientTypeName = self:GetClientGroupUnit():GetTypeName() + if ClientMultiSeatedTypes[ClientTypeName] then + return true + end + end + + return false +end + +--- Checks for a client alive event and calls a function on a continuous basis. +-- @param #CLIENT self +-- @param #function CallBack Function. +-- @return #CLIENT +function CLIENT:Alive( CallBack, ... ) + self:F() + + self.ClientCallBack = CallBack + self.ClientParameters = arg + + return self +end + +--- @param #CLIENT self +function CLIENT:_AliveCheckScheduler() + self:F( { self.ClientName, self.ClientAlive2, self.ClientBriefingShown } ) + + if self:IsAlive() then -- Polymorphic call of UNIT + if self.ClientAlive2 == false then + self:ShowBriefing() + if self.ClientCallBack then + self:T("Calling Callback function") + self.ClientCallBack( self, unpack( self.ClientParameters ) ) + end + self.ClientAlive2 = true + end + else + if self.ClientAlive2 == true then + self.ClientAlive2 = false + end + end + + return true +end + +--- Return the DCSGroup of a Client. +-- This function is modified to deal with a couple of bugs in DCS 1.5.3 +-- @param #CLIENT self +-- @return DCSGroup#Group +function CLIENT:GetDCSGroup() + self:F3() + +-- local ClientData = Group.getByName( self.ClientName ) +-- if ClientData and ClientData:isExist() then +-- self:T( self.ClientName .. " : group found!" ) +-- return ClientData +-- else +-- return nil +-- end + + local ClientUnit = Unit.getByName( self.ClientName ) + + local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ) } + for CoalitionId, CoalitionData in pairs( CoalitionsData ) do + self:T3( { "CoalitionData:", CoalitionData } ) + for UnitId, UnitData in pairs( CoalitionData ) do + self:T3( { "UnitData:", UnitData } ) + if UnitData and UnitData:isExist() then + + --self:E(self.ClientName) + if ClientUnit then + local ClientGroup = ClientUnit:getGroup() + if ClientGroup then + self:T3( "ClientGroup = " .. self.ClientName ) + if ClientGroup:isExist() and UnitData:getGroup():isExist() then + if ClientGroup:getID() == UnitData:getGroup():getID() then + self:T3( "Normal logic" ) + self:T3( self.ClientName .. " : group found!" ) + self.ClientGroupID = ClientGroup:getID() + self.ClientGroupName = ClientGroup:getName() + return ClientGroup + end + else + -- Now we need to resolve the bugs in DCS 1.5 ... + -- Consult the database for the units of the Client Group. (ClientGroup:getUnits() returns nil) + self:T3( "Bug 1.5 logic" ) + local ClientGroupTemplate = _DATABASE.Templates.Units[self.ClientName].GroupTemplate + self.ClientGroupID = ClientGroupTemplate.groupId + self.ClientGroupName = _DATABASE.Templates.Units[self.ClientName].GroupName + self:T3( self.ClientName .. " : group found in bug 1.5 resolvement logic!" ) + return ClientGroup + end + -- else + -- error( "Client " .. self.ClientName .. " not found!" ) + end + else + --self:E( { "Client not found!", self.ClientName } ) + end + end + end + end + + -- For non player clients + if ClientUnit then + local ClientGroup = ClientUnit:getGroup() + if ClientGroup then + self:T3( "ClientGroup = " .. self.ClientName ) + if ClientGroup:isExist() then + self:T3( "Normal logic" ) + self:T3( self.ClientName .. " : group found!" ) + return ClientGroup + end + end + end + + self.ClientGroupID = nil + self.ClientGroupUnit = nil + + return nil +end + + +-- TODO: Check DCSTypes#Group.ID +--- Get the group ID of the client. +-- @param #CLIENT self +-- @return DCSTypes#Group.ID +function CLIENT:GetClientGroupID() + + local ClientGroup = self:GetDCSGroup() + + --self:E( self.ClientGroupID ) -- Determined in GetDCSGroup() + return self.ClientGroupID +end + + +--- Get the name of the group of the client. +-- @param #CLIENT self +-- @return #string +function CLIENT:GetClientGroupName() + + local ClientGroup = self:GetDCSGroup() + + self:T( self.ClientGroupName ) -- Determined in GetDCSGroup() + return self.ClientGroupName +end + +--- Returns the UNIT of the CLIENT. +-- @param #CLIENT self +-- @return Unit#UNIT +function CLIENT:GetClientGroupUnit() + self:F2() + + local ClientDCSUnit = Unit.getByName( self.ClientName ) + + self:T( self.ClientDCSUnit ) + if ClientDCSUnit and ClientDCSUnit:isExist() then + local ClientUnit = _DATABASE:FindUnit( self.ClientName ) + self:T2( ClientUnit ) + return ClientUnit + end +end + +--- Returns the DCSUnit of the CLIENT. +-- @param #CLIENT self +-- @return DCSTypes#Unit +function CLIENT:GetClientGroupDCSUnit() + self:F2() + + local ClientDCSUnit = Unit.getByName( self.ClientName ) + + if ClientDCSUnit and ClientDCSUnit:isExist() then + self:T2( ClientDCSUnit ) + return ClientDCSUnit + end +end + + +--- Evaluates if the CLIENT is a transport. +-- @param #CLIENT self +-- @return #boolean true is a transport. +function CLIENT:IsTransport() + self:F() + return self.ClientTransport +end + +--- Shows the @{Cargo#CARGO} contained within the CLIENT to the player as a message. +-- The @{Cargo#CARGO} is shown using the @{Message#MESSAGE} distribution system. +-- @param #CLIENT self +function CLIENT:ShowCargo() + self:F() + + local CargoMsg = "" + + for CargoName, Cargo in pairs( CARGOS ) do + if self == Cargo:IsLoadedInClient() then + CargoMsg = CargoMsg .. Cargo.CargoName .. " Type:" .. Cargo.CargoType .. " Weight: " .. Cargo.CargoWeight .. "\n" + end + end + + if CargoMsg == "" then + CargoMsg = "empty" + end + + self:Message( CargoMsg, 15, self.ClientName .. "/Cargo", "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 MessageId is a text identifying the Message in the MessageQueue. The Message system overwrites Messages with the same MessageId +-- @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. +function CLIENT:Message( Message, MessageDuration, MessageId, MessageCategory, MessageInterval ) + self:F( { Message, MessageDuration, MessageId, MessageCategory, MessageInterval } ) + + if not self.MenuMessages then + if self:GetClientGroupID() then + self.MenuMessages = MENU_CLIENT:New( self, 'Messages' ) + self.MenuRouteMessageOn = MENU_CLIENT_COMMAND:New( self, 'Messages On', self.MenuMessages, CLIENT.SwitchMessages, { self, true } ) + self.MenuRouteMessageOff = MENU_CLIENT_COMMAND:New( self,'Messages Off', self.MenuMessages, CLIENT.SwitchMessages, { self, false } ) + end + end + + if self.MessageSwitch == true then + if MessageCategory == nil then + MessageCategory = "Messages" + end + if 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, MessageCategory, MessageDuration, MessageId ):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, MessageCategory, MessageDuration, MessageId ):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, MessageCategory, MessageDuration, MessageId ):ToClient( self ) + self.Messages[MessageId].MessageTime = timer.getTime() + end + end + end + end +end +--- Manage the mission database. +-- +-- @{#DATABASE} class +-- ================== +-- Mission designers can use the DATABASE class to refer to: +-- +-- * UNITS +-- * GROUPS +-- * players +-- * alive players +-- * CLIENTS +-- * alive CLIENTS +-- +-- On top, for internal MOOSE administration purposes, the DATBASE administers the Unit and Gruop 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. +-- +-- 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 player it finds within the DATABASE. +-- * @{#DATABASE.ForEachPlayerAlive}: Calls a function for each alive 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 + +Include.File( "Routines" ) +Include.File( "Base" ) +Include.File( "Menu" ) +Include.File( "Group" ) +Include.File( "Unit" ) +Include.File( "Event" ) +Include.File( "Client" ) + + +--- DATABASE class +-- @type DATABASE +-- @extends Base#BASE +DATABASE = { + ClassName = "DATABASE", + Templates = { + Units = {}, + Groups = {}, + ClientsByName = {}, + ClientsByID = {}, + }, + DCSUnits = {}, + DCSGroups = {}, + UNITS = {}, + GROUPS = {}, + PLAYERS = {}, + PLAYERSALIVE = {}, + CLIENTS = {}, + CLIENTSALIVE = {}, + NavPoints = {}, +} + +local _DATABASECoalition = + { + [1] = "Red", + [2] = "Blue", + } + +local _DATABASECategory = + { + [Unit.Category.AIRPLANE] = "Plane", + [Unit.Category.HELICOPTER] = "Helicopter", + [Unit.Category.GROUND_UNIT] = "Vehicle", + [Unit.Category.SHIP] = "Ship", + [Unit.Category.STRUCTURE] = "Structure", + } + + +--- Creates a new DATABASE object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. +-- @param #DATABASE self +-- @return #DATABASE +-- @usage +-- -- Define a new DATABASE Object. This DBObject will contain a reference to all Group and Unit Templates defined within the ME and the DCSRTE. +-- DBObject = DATABASE:New() +function DATABASE:New() + + -- Inherits from BASE + local self = BASE:Inherit( self, BASE:New() ) + + _EVENTDISPATCHER:OnBirth( self._EventOnBirth, self ) + _EVENTDISPATCHER:OnDead( self._EventOnDeadOrCrash, self ) + _EVENTDISPATCHER:OnCrash( self._EventOnDeadOrCrash, self ) + + + -- Follow alive players and clients + _EVENTDISPATCHER:OnPlayerEnterUnit( self._EventOnPlayerEnterUnit, self ) + _EVENTDISPATCHER:OnPlayerLeaveUnit( self._EventOnPlayerLeaveUnit, self ) + + self:_RegisterTemplates() + self:_RegisterDatabase() + self:_RegisterPlayers() + + return self +end + +--- Finds a Unit based on the Unit Name. +-- @param #DATABASE self +-- @param #string UnitName +-- @return Unit#UNIT The found Unit. +function DATABASE:FindUnit( UnitName ) + + local UnitFound = self.UNITS[UnitName] + return UnitFound +end + + +--- Adds a Unit based on the Unit Name in the DATABASE. +-- @param #DATABASE self +function DATABASE:AddUnit( DCSUnit, DCSUnitName ) + + self.DCSUnits[DCSUnitName] = DCSUnit + self.UNITS[DCSUnitName] = UNIT:Register( DCSUnitName ) +end + + +--- Deletes a Unit from the DATABASE based on the Unit Name. +-- @param #DATABASE self +function DATABASE:DeleteUnit( DCSUnitName ) + + self.DCSUnits[DCSUnitName] = nil +end + + +--- Finds a CLIENT based on the ClientName. +-- @param #DATABASE self +-- @param #string ClientName +-- @return Client#CLIENT The found CLIENT. +function DATABASE:FindClient( ClientName ) + + local ClientFound = self.CLIENTS[ClientName] + return ClientFound +end + + +--- Adds a CLIENT based on the ClientName in the DATABASE. +-- @param #DATABASE self +function DATABASE:AddClient( ClientName ) + + self.CLIENTS[ClientName] = CLIENT:Register( ClientName ) + self:E( self.CLIENTS[ClientName]:GetClassNameAndID() ) +end + + +--- Finds a GROUP based on the GroupName. +-- @param #DATABASE self +-- @param #string GroupName +-- @return Group#GROUP The found GROUP. +function DATABASE:FindGroup( GroupName ) + + local GroupFound = self.GROUPS[GroupName] + return GroupFound +end + + +--- Adds a GROUP based on the GroupName in the DATABASE. +-- @param #DATABASE self +function DATABASE:AddGroup( DCSGroup, GroupName ) + + self.DCSGroups[GroupName] = DCSGroup + self.GROUPS[GroupName] = GROUP:Register( 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] = PlayerName + self.PLAYERSALIVE[PlayerName] = PlayerName + self.CLIENTSALIVE[PlayerName] = self:FindClient( UnitName ) + 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.PLAYERSALIVE[PlayerName] = nil + self.CLIENTSALIVE[PlayerName] = nil + end +end + + +--- Instantiate new Groups within the DCSRTE. +-- This method expects EXACTLY the same structure as a structure within the ME, and needs 2 additional fields defined: +-- SpawnCountryID, SpawnCategoryID +-- This method is used by the SPAWN class. +-- @param #DATABASE self +-- @param #table SpawnTemplate +-- @return #DATABASE self +function DATABASE:Spawn( SpawnTemplate ) + self:F2( SpawnTemplate.name ) + + self:T2( { SpawnTemplate.SpawnCountryID, SpawnTemplate.SpawnCategoryID } ) + + -- Copy the spawn variables of the template in temporary storage, nullify, and restore the spawn variables. + local SpawnCoalitionID = SpawnTemplate.SpawnCoalitionID + local SpawnCountryID = SpawnTemplate.SpawnCountryID + local SpawnCategoryID = SpawnTemplate.SpawnCategoryID + + -- Nullify + SpawnTemplate.SpawnCoalitionID = nil + SpawnTemplate.SpawnCountryID = nil + SpawnTemplate.SpawnCategoryID = nil + + self:_RegisterTemplate( SpawnTemplate ) + coalition.addGroup( SpawnCountryID, SpawnCategoryID, SpawnTemplate ) + + -- Restore + SpawnTemplate.SpawnCoalitionID = SpawnCoalitionID + SpawnTemplate.SpawnCountryID = SpawnCountryID + SpawnTemplate.SpawnCategoryID = SpawnCategoryID + + + local SpawnGroup = GROUP:Register( 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 ) + + local GroupTemplateName = env.getValueDictByKey(GroupTemplate.name) + + if not self.Templates.Groups[GroupTemplateName] then + self.Templates.Groups[GroupTemplateName] = {} + self.Templates.Groups[GroupTemplateName].Status = nil + end + + -- Delete the spans from the route, it is not needed and takes memory. + if GroupTemplate.route and GroupTemplate.route.spans then + GroupTemplate.route.spans = nil + end + + self.Templates.Groups[GroupTemplateName].GroupName = GroupTemplateName + self.Templates.Groups[GroupTemplateName].Template = GroupTemplate + self.Templates.Groups[GroupTemplateName].groupId = GroupTemplate.groupId + self.Templates.Groups[GroupTemplateName].UnitCount = #GroupTemplate.units + self.Templates.Groups[GroupTemplateName].Units = GroupTemplate.units + + self:T2( { "Group", self.Templates.Groups[GroupTemplateName].GroupName, self.Templates.Groups[GroupTemplateName].UnitCount } ) + + for unit_num, UnitTemplate in pairs( GroupTemplate.units ) do + + local UnitTemplateName = env.getValueDictByKey(UnitTemplate.name) + self.Templates.Units[UnitTemplateName] = {} + self.Templates.Units[UnitTemplateName].UnitName = UnitTemplateName + self.Templates.Units[UnitTemplateName].Template = UnitTemplate + self.Templates.Units[UnitTemplateName].GroupName = GroupTemplateName + self.Templates.Units[UnitTemplateName].GroupTemplate = GroupTemplate + self.Templates.Units[UnitTemplateName].GroupId = GroupTemplate.groupId + self:E( {"skill",UnitTemplate.skill}) + if UnitTemplate.skill and (UnitTemplate.skill == "Client" or UnitTemplate.skill == "Player") then + self.Templates.ClientsByName[UnitTemplateName] = UnitTemplate + self.Templates.ClientsByID[UnitTemplate.unitId] = UnitTemplate + end + self:E( { "Unit", self.Templates.Units[UnitTemplateName].UnitName } ) + end +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 datapoints within in the mission. +-- @param #DATABASE self +-- @return #DATABASE self +function DATABASE:_RegisterDatabase() + + 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:", DCSGroup, DCSGroupName } ) + self:AddGroup( DCSGroup, DCSGroupName ) + + for DCSUnitId, DCSUnit in pairs( DCSGroup:getUnits() ) do + + local DCSUnitName = DCSUnit:getName() + self:E( { "Register Unit:", DCSUnit, DCSUnitName } ) + self:AddUnit( DCSUnit, DCSUnitName ) + end + else + self:E( { "Group does not exist: ", DCSGroup } ) + end + + end + end + + for ClientName, ClientTemplate in pairs( self.Templates.ClientsByName ) do + self:E( { "Adding Client:", ClientName } ) + self:AddClient( ClientName ) + end + + return self +end + +--- Events + +--- Handles the OnBirth event for the alive units set. +-- @param #DATABASE self +-- @param Event#EVENTDATA Event +function DATABASE:_EventOnBirth( Event ) + self:F2( { Event } ) + + if Event.IniDCSUnit then + self:AddUnit( Event.IniDCSUnit, Event.IniDCSUnitName ) + self:AddGroup( Event.IniDCSGroup, Event.IniDCSGroupName ) + self:_EventOnPlayerEnterUnit( Event ) + end +end + + +--- Handles the OnDead or OnCrash event for alive units set. +-- @param #DATABASE self +-- @param Event#EVENTDATA Event +function DATABASE:_EventOnDeadOrCrash( Event ) + self:F2( { Event } ) + + if Event.IniDCSUnit then + if self.DCSUnits[Event.IniDCSUnitName] then + self:DeleteUnit( Event.IniDCSUnitName ) + -- add logic to correctly remove a group once all units are destroyed... + end + end +end + + +--- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). +-- @param #DATABASE self +-- @param Event#EVENTDATA Event +function DATABASE:_EventOnPlayerEnterUnit( Event ) + self:F2( { Event } ) + + if Event.IniDCSUnit then + local PlayerName = Event.IniDCSUnit:getPlayerName() + if not self.PLAYERSALIVE[PlayerName] then + self:AddPlayer( Event.IniDCSUnitName, PlayerName ) + end + end +end + + +--- Handles the OnPlayerLeaveUnit event to clean the active players table. +-- @param #DATABASE self +-- @param Event#EVENTDATA Event +function DATABASE:_EventOnPlayerLeaveUnit( Event ) + self:F2( { Event } ) + + if Event.IniDCSUnit then + local PlayerName = Event.IniDCSUnit:getPlayerName() + if self.PLAYERSALIVE[PlayerName] then + self:DeletePlayer( PlayerName ) + end + end +end + +--- Iterators + +--- Iterate the DATABASE and call an iterator function for the given set, providing the Object for each element within the set and optional parameters. +-- @param #DATABASE self +-- @param #function IteratorFunction The function that will be called when there is an alive player in the database. +-- @return #DATABASE self +function DATABASE:ForEach( IteratorFunction, 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 % 10 == 0 then + coroutine.yield( false ) + end + end + return true + end + + local co = coroutine.create( CoRoutine ) + + local function Schedule() + + local status, res = coroutine.resume( co ) + self:T2( { status, res } ) + + if status == false then + error( res ) + end + if res == false then + return true -- resume next time the loop + end + + return false + end + + local Scheduler = SCHEDULER:New( self, Schedule, {}, 0.001, 0.001, 0 ) + + return self +end + + +--- Iterate the DATABASE and call an iterator function for each **alive** unit, providing the DCSUnit 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 DCSUnit parameter. +-- @return #DATABASE self +function DATABASE:ForEachDCSUnit( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.DCSUnits ) + + 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, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, 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 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 **alive** player, providing the Unit of the player 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 UNIT parameter. +-- @return #DATABASE self +function DATABASE:ForEachPlayerAlive( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.PLAYERSALIVE ) + + 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 + +--- Iterate the DATABASE and call an iterator function for each **ALIVE** 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 CLIENT in the database. The function needs to accept a CLIENT parameter. +-- @return #DATABASE self +function DATABASE:ForEachClientAlive( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.CLIENTSALIVE ) + + return self +end + + +function DATABASE:_RegisterTemplates() + self:F2() + + self.Navpoints = {} + self.UNITS = {} + --Build routines.db.units and self.Navpoints + 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 + --self.Units[coa_name] = {} + + ---------------------------------------------- + -- build nav points DB + self.Navpoints[coa_name] = {} + 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[coa_name][nav_ind] = routines.utils.deepCopy(nav_data) + + self.Navpoints[coa_name][nav_ind]['name'] = nav_data.callsignStr -- name is a little bit more self-explanatory. + self.Navpoints[coa_name][nav_ind]['point'] = {} -- point is used by SSE, support it. + self.Navpoints[coa_name][nav_ind]['point']['x'] = nav_data.x + self.Navpoints[coa_name][nav_ind]['point']['y'] = 0 + self.Navpoints[coa_name][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.lower(cntry_data.name) + --self.Units[coa_name][countryName] = {} + --self.Units[coa_name][countryName]["countryId"] = cntry_data.id + + if type(cntry_data) == 'table' then --just making sure + + for obj_type_name, obj_type_data in pairs(cntry_data) do + + if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then --should be an unncessary check + + local category = 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 ) + 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 + + + + +--- The main include file for the MOOSE system. + +Include.File( "Routines" ) +Include.File( "Base" ) +Include.File( "Database" ) +Include.File( "Event" ) + +-- The order of the declarations is important here. Don't touch it. + +--- Declare the event dispatcher based on the EVENT class +_EVENTDISPATCHER = EVENT:New() -- #EVENT + +--- Declare the main database object, which is used internally by the MOOSE classes. +_DATABASE = DATABASE:New() -- Database#DATABASE + +--- Scoring system for MOOSE. +-- This scoring class calculates the hits and kills that players make within a simulation session. +-- Scoring is calculated using a defined algorithm. +-- With a small change in MissionScripting.lua, the scoring can also be logged in a CSV file, that can then be uploaded +-- to a database or a BI tool to publish the scoring results to the player community. +-- @module Scoring +-- @author FlightControl + + +Include.File( "Routines" ) +Include.File( "Base" ) +Include.File( "Menu" ) +Include.File( "Group" ) +Include.File( "Event" ) + + +--- The Scoring class +-- @type SCORING +-- @field Players A collection of the current players that have joined the game. +-- @extends Base#BASE +SCORING = { + ClassName = "SCORING", + ClassID = 0, + Players = {}, +} + +local _SCORINGCoalition = + { + [1] = "Red", + [2] = "Blue", + } + +local _SCORINGCategory = + { + [Unit.Category.AIRPLANE] = "Plane", + [Unit.Category.HELICOPTER] = "Helicopter", + [Unit.Category.GROUND_UNIT] = "Vehicle", + [Unit.Category.SHIP] = "Ship", + [Unit.Category.STRUCTURE] = "Structure", + } + +--- Creates a new SCORING object to administer the scoring achieved by players. +-- @param #SCORING self +-- @param #string GameName The name of the game. This name is also logged in the CSV score file. +-- @return #SCORING self +-- @usage +-- -- Define a new scoring object for the mission Gori Valley. +-- ScoringObject = SCORING:New( "Gori Valley" ) +function SCORING:New( GameName ) + + -- Inherits from BASE + local self = BASE:Inherit( self, BASE:New() ) + + if GameName then + self.GameName = GameName + else + error( "A game name must be given to register the scoring results" ) + end + + + _EVENTDISPATCHER:OnDead( self._EventOnDeadOrCrash, self ) + _EVENTDISPATCHER:OnCrash( self._EventOnDeadOrCrash, self ) + _EVENTDISPATCHER:OnHit( self._EventOnHit, self ) + + --self.SchedulerId = routines.scheduleFunction( SCORING._FollowPlayersScheduled, { self }, 0, 5 ) + self.SchedulerId = SCHEDULER:New( self, self._FollowPlayersScheduled, {}, 0, 5 ) + + self:ScoreMenu() + + return self + +end + +--- Creates a score radio menu. Can be accessed using Radio -> F10. +-- @param #SCORING self +-- @return #SCORING self +function SCORING:ScoreMenu() + self.Menu = SUBMENU:New( 'Scoring' ) + self.AllScoresMenu = COMMANDMENU:New( 'Score All Active Players', self.Menu, SCORING.ReportScoreAll, self ) + --- = COMMANDMENU:New('Your Current Score', ReportScore, SCORING.ReportScorePlayer, self ) + return self +end + +--- Follows new players entering Clients within the DCSRTE. +-- TODO: Need to see if i can catch this also with an event. It will eliminate the schedule ... +function SCORING:_FollowPlayersScheduled() + self:F3( "_FollowPlayersScheduled" ) + + local ClientUnit = 0 + local CoalitionsData = { AlivePlayersRed = coalition.getPlayers(coalition.side.RED), AlivePlayersBlue = coalition.getPlayers(coalition.side.BLUE) } + local unitId + local unitData + local AlivePlayerUnits = {} + + for CoalitionId, CoalitionData in pairs( CoalitionsData ) do + self:T3( { "_FollowPlayersScheduled", CoalitionData } ) + for UnitId, UnitData in pairs( CoalitionData ) do + self:_AddPlayerFromUnit( UnitData ) + end + end + + return true +end + + +--- Track DEAD or CRASH events for the scoring. +-- @param #SCORING self +-- @param Event#EVENTDATA Event +function SCORING:_EventOnDeadOrCrash( Event ) + self:F( { Event } ) + + local TargetUnit = nil + local TargetGroup = nil + local TargetUnitName = "" + local TargetGroupName = "" + local TargetPlayerName = "" + local TargetCoalition = nil + local TargetCategory = nil + local TargetType = nil + local TargetUnitCoalition = nil + local TargetUnitCategory = nil + local TargetUnitType = nil + + if Event.IniDCSUnit then + + TargetUnit = Event.IniDCSUnit + TargetUnitName = Event.IniDCSUnitName + TargetGroup = Event.IniDCSGroup + TargetGroupName = Event.IniDCSGroupName + TargetPlayerName = TargetUnit:getPlayerName() + + TargetCoalition = TargetUnit:getCoalition() + --TargetCategory = TargetUnit:getCategory() + TargetCategory = TargetUnit:getDesc().category -- Workaround + TargetType = TargetUnit:getTypeName() + + TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] + TargetUnitCategory = _SCORINGCategory[TargetCategory] + TargetUnitType = TargetType + + self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType } ) + end + + for PlayerName, PlayerData in pairs( self.Players ) do + if PlayerData then -- This should normally not happen, but i'll test it anyway. + self:T( "Something got killed" ) + + -- Some variables + local InitUnitName = PlayerData.UnitName + local InitUnitType = PlayerData.UnitType + local InitCoalition = PlayerData.UnitCoalition + local InitCategory = PlayerData.UnitCategory + local InitUnitCoalition = _SCORINGCoalition[InitCoalition] + local InitUnitCategory = _SCORINGCategory[InitCategory] + + self:T( { InitUnitName, InitUnitType, InitUnitCoalition, InitCoalition, InitUnitCategory, InitCategory } ) + + -- What is he hitting? + if TargetCategory then + if PlayerData and PlayerData.Hit and PlayerData.Hit[TargetCategory] and PlayerData.Hit[TargetCategory][TargetUnitName] then -- Was there a hit for this unit for this player before registered??? + if not PlayerData.Kill[TargetCategory] then + PlayerData.Kill[TargetCategory] = {} + end + if not PlayerData.Kill[TargetCategory][TargetType] then + PlayerData.Kill[TargetCategory][TargetType] = {} + PlayerData.Kill[TargetCategory][TargetType].Score = 0 + PlayerData.Kill[TargetCategory][TargetType].ScoreKill = 0 + PlayerData.Kill[TargetCategory][TargetType].Penalty = 0 + PlayerData.Kill[TargetCategory][TargetType].PenaltyKill = 0 + end + + if InitCoalition == TargetCoalition then + PlayerData.Penalty = PlayerData.Penalty + 25 + PlayerData.Kill[TargetCategory][TargetType].Penalty = PlayerData.Kill[TargetCategory][TargetType].Penalty + 25 + PlayerData.Kill[TargetCategory][TargetType].PenaltyKill = PlayerData.Kill[TargetCategory][TargetType].PenaltyKill + 1 + MESSAGE:New( "Player '" .. PlayerName .. "' killed a friendly " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. + PlayerData.Kill[TargetCategory][TargetType].PenaltyKill .. " times. Penalty: -" .. PlayerData.Kill[TargetCategory][TargetType].Penalty .. + ". Score Total:" .. PlayerData.Score - PlayerData.Penalty, + "", 5, "/PENALTY" .. PlayerName .. "/" .. InitUnitName ):ToAll() + self:ScoreCSV( PlayerName, "KILL_PENALTY", 1, -125, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + else + PlayerData.Score = PlayerData.Score + 10 + PlayerData.Kill[TargetCategory][TargetType].Score = PlayerData.Kill[TargetCategory][TargetType].Score + 10 + PlayerData.Kill[TargetCategory][TargetType].ScoreKill = PlayerData.Kill[TargetCategory][TargetType].ScoreKill + 1 + MESSAGE:New( "Player '" .. PlayerName .. "' killed an enemy " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. + PlayerData.Kill[TargetCategory][TargetType].ScoreKill .. " times. Score: " .. PlayerData.Kill[TargetCategory][TargetType].Score .. + ". Score Total:" .. PlayerData.Score - PlayerData.Penalty, + "", 5, "/SCORE" .. PlayerName .. "/" .. InitUnitName ):ToAll() + self:ScoreCSV( PlayerName, "KILL_SCORE", 1, 10, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + end + end + end + end + end +end + + + +--- Add a new player entering a Unit. +function SCORING:_AddPlayerFromUnit( UnitData ) + self:F( UnitData ) + + if UnitData and UnitData:isExist() then + local UnitName = UnitData:getName() + local PlayerName = UnitData:getPlayerName() + local UnitDesc = UnitData:getDesc() + local UnitCategory = UnitDesc.category + local UnitCoalition = UnitData:getCoalition() + local UnitTypeName = UnitData:getTypeName() + + self:T( { PlayerName, UnitName, UnitCategory, UnitCoalition, UnitTypeName } ) + + if self.Players[PlayerName] == nil then -- I believe this is the place where a Player gets a life in a mission when he enters a unit ... + self.Players[PlayerName] = {} + self.Players[PlayerName].Hit = {} + self.Players[PlayerName].Kill = {} + self.Players[PlayerName].Mission = {} + + -- for CategoryID, CategoryName in pairs( SCORINGCategory ) do + -- self.Players[PlayerName].Hit[CategoryID] = {} + -- self.Players[PlayerName].Kill[CategoryID] = {} + -- end + self.Players[PlayerName].HitPlayers = {} + self.Players[PlayerName].HitUnits = {} + self.Players[PlayerName].Score = 0 + self.Players[PlayerName].Penalty = 0 + self.Players[PlayerName].PenaltyCoalition = 0 + self.Players[PlayerName].PenaltyWarning = 0 + end + + if not self.Players[PlayerName].UnitCoalition then + self.Players[PlayerName].UnitCoalition = UnitCoalition + else + if self.Players[PlayerName].UnitCoalition ~= UnitCoalition then + self.Players[PlayerName].Penalty = self.Players[PlayerName].Penalty + 50 + self.Players[PlayerName].PenaltyCoalition = self.Players[PlayerName].PenaltyCoalition + 1 + MESSAGE:New( "Player '" .. PlayerName .. "' changed coalition from " .. _SCORINGCoalition[self.Players[PlayerName].UnitCoalition] .. " to " .. _SCORINGCoalition[UnitCoalition] .. + "(changed " .. self.Players[PlayerName].PenaltyCoalition .. " times the coalition). 50 Penalty points added.", + "", + 2, + "/PENALTYCOALITION" .. PlayerName + ):ToAll() + self:ScoreCSV( PlayerName, "COALITION_PENALTY", 1, -50, self.Players[PlayerName].UnitName, _SCORINGCoalition[self.Players[PlayerName].UnitCoalition], _SCORINGCategory[self.Players[PlayerName].UnitCategory], self.Players[PlayerName].UnitType, + UnitName, _SCORINGCoalition[UnitCoalition], _SCORINGCategory[UnitCategory], UnitData:getTypeName() ) + end + end + self.Players[PlayerName].UnitName = UnitName + self.Players[PlayerName].UnitCoalition = UnitCoalition + self.Players[PlayerName].UnitCategory = UnitCategory + self.Players[PlayerName].UnitType = UnitTypeName + + if self.Players[PlayerName].Penalty > 100 then + if self.Players[PlayerName].PenaltyWarning < 1 then + MESSAGE:New( "Player '" .. PlayerName .. "': WARNING! If you continue to commit FRATRICIDE and have a PENALTY score higher than 150, you will be COURT MARTIALED and DISMISSED from this mission! \nYour total penalty is: " .. self.Players[PlayerName].Penalty, + "", + 30, + "/PENALTYCOALITION" .. PlayerName + ):ToAll() + self.Players[PlayerName].PenaltyWarning = self.Players[PlayerName].PenaltyWarning + 1 + end + end + + if self.Players[PlayerName].Penalty > 150 then + ClientGroup = GROUP:NewFromDCSUnit( UnitData ) + ClientGroup:Destroy() + MESSAGE:New( "Player '" .. PlayerName .. "' committed FRATRICIDE, he will be COURT MARTIALED and is DISMISSED from this mission!", + "", + 10, + "/PENALTYCOALITION" .. PlayerName + ):ToAll() + end + + end +end + + +--- Registers Scores the players completing a Mission Task. +function SCORING:_AddMissionTaskScore( PlayerUnit, MissionName, Score ) + self:F( { PlayerUnit, MissionName, Score } ) + + local PlayerName = PlayerUnit:getPlayerName() + + if not self.Players[PlayerName].Mission[MissionName] then + self.Players[PlayerName].Mission[MissionName] = {} + self.Players[PlayerName].Mission[MissionName].ScoreTask = 0 + self.Players[PlayerName].Mission[MissionName].ScoreMission = 0 + end + + self:T( PlayerName ) + self:T( self.Players[PlayerName].Mission[MissionName] ) + + self.Players[PlayerName].Score = self.Players[PlayerName].Score + Score + self.Players[PlayerName].Mission[MissionName].ScoreTask = self.Players[PlayerName].Mission[MissionName].ScoreTask + Score + + MESSAGE:New( "Player '" .. PlayerName .. "' has finished another Task in Mission '" .. MissionName .. "'. " .. + Score .. " Score points added.", + "", 20, "/SCORETASK" .. PlayerName ):ToAll() + + self:ScoreCSV( PlayerName, "TASK_" .. MissionName:gsub( ' ', '_' ), 1, Score, PlayerUnit:getName() ) +end + + +--- Registers Mission Scores for possible multiple players that contributed in the Mission. +function SCORING:_AddMissionScore( MissionName, Score ) + self:F( { MissionName, Score } ) + + for PlayerName, PlayerData in pairs( self.Players ) do + + if PlayerData.Mission[MissionName] then + PlayerData.Score = PlayerData.Score + Score + PlayerData.Mission[MissionName].ScoreMission = PlayerData.Mission[MissionName].ScoreMission + Score + MESSAGE:New( "Player '" .. PlayerName .. "' has finished Mission '" .. MissionName .. "'. " .. + Score .. " Score points added.", + "", 20, "/SCOREMISSION" .. PlayerName ):ToAll() + self:ScoreCSV( PlayerName, "MISSION_" .. MissionName:gsub( ' ', '_' ), 1, Score ) + end + end +end + +--- Handles the OnHit event for the scoring. +-- @param #SCORING self +-- @param Event#EVENTDATA Event +function SCORING:_EventOnHit( Event ) + self:F( { Event } ) + + local InitUnit = nil + local InitUnitName = "" + local InitGroup = nil + local InitGroupName = "" + local InitPlayerName = nil + + local InitCoalition = nil + local InitCategory = nil + local InitType = nil + local InitUnitCoalition = nil + local InitUnitCategory = nil + local InitUnitType = nil + + local TargetUnit = nil + local TargetUnitName = "" + local TargetGroup = nil + local TargetGroupName = "" + local TargetPlayerName = "" + + local TargetCoalition = nil + local TargetCategory = nil + local TargetType = nil + local TargetUnitCoalition = nil + local TargetUnitCategory = nil + local TargetUnitType = nil + + if Event.IniDCSUnit then + + InitUnit = Event.IniDCSUnit + InitUnitName = Event.IniDCSUnitName + InitGroup = Event.IniDCSGroup + InitGroupName = Event.IniDCSGroupName + InitPlayerName = InitUnit:getPlayerName() + + InitCoalition = InitUnit:getCoalition() + --TODO: Workaround Client DCS Bug + --InitCategory = InitUnit:getCategory() + InitCategory = InitUnit:getDesc().category + InitType = InitUnit:getTypeName() + + InitUnitCoalition = _SCORINGCoalition[InitCoalition] + InitUnitCategory = _SCORINGCategory[InitCategory] + InitUnitType = InitType + + self:T( { InitUnitName, InitGroupName, InitPlayerName, InitCoalition, InitCategory, InitType , InitUnitCoalition, InitUnitCategory, InitUnitType } ) + end + + + if Event.TgtDCSUnit then + + TargetUnit = Event.TgtDCSUnit + TargetUnitName = Event.TgtDCSUnitName + TargetGroup = Event.TgtDCSGroup + TargetGroupName = Event.TgtDCSGroupName + TargetPlayerName = TargetUnit:getPlayerName() + + TargetCoalition = TargetUnit:getCoalition() + --TODO: Workaround Client DCS Bug + --TargetCategory = TargetUnit:getCategory() + TargetCategory = TargetUnit:getDesc().category + TargetType = TargetUnit:getTypeName() + + TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] + TargetUnitCategory = _SCORINGCategory[TargetCategory] + TargetUnitType = TargetType + + self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType, TargetUnitCoalition, TargetUnitCategory, TargetUnitType } ) + end + + if InitPlayerName ~= nil then -- It is a player that is hitting something + self:_AddPlayerFromUnit( InitUnit ) + if self.Players[InitPlayerName] then -- This should normally not happen, but i'll test it anyway. + if TargetPlayerName ~= nil then -- It is a player hitting another player ... + self:_AddPlayerFromUnit( TargetUnit ) + self.Players[InitPlayerName].HitPlayers = self.Players[InitPlayerName].HitPlayers + 1 + end + + self:T( "Hitting Something" ) + -- What is he hitting? + if TargetCategory then + if not self.Players[InitPlayerName].Hit[TargetCategory] then + self.Players[InitPlayerName].Hit[TargetCategory] = {} + end + if not self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName] then + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName] = {} + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score = 0 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty = 0 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit = 0 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit = 0 + end + local Score = 0 + if InitCoalition == TargetCoalition then + self.Players[InitPlayerName].Penalty = self.Players[InitPlayerName].Penalty + 10 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty + 10 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit + 1 + MESSAGE:New( "Player '" .. InitPlayerName .. "' hit a friendly " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit .. " times. Penalty: -" .. self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty .. + ". Score Total:" .. self.Players[InitPlayerName].Score - self.Players[InitPlayerName].Penalty, + "", + 2, + "/PENALTY" .. InitPlayerName .. "/" .. InitUnitName + ):ToAll() + self:ScoreCSV( InitPlayerName, "HIT_PENALTY", 1, -25, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + else + self.Players[InitPlayerName].Score = self.Players[InitPlayerName].Score + 10 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score + 1 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit + 1 + MESSAGE:New( "Player '" .. InitPlayerName .. "' hit a target " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit .. " times. Score: " .. self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score .. + ". Score Total:" .. self.Players[InitPlayerName].Score - self.Players[InitPlayerName].Penalty, + "", + 2, + "/SCORE" .. InitPlayerName .. "/" .. InitUnitName + ):ToAll() + self:ScoreCSV( InitPlayerName, "HIT_SCORE", 1, 1, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + end + end + end + elseif InitPlayerName == nil then -- It is an AI hitting a player??? + + end +end + + +function SCORING:ReportScoreAll() + + env.info( "Hello World " ) + + local ScoreMessage = "" + local PlayerMessage = "" + + self:T( "Score Report" ) + + for PlayerName, PlayerData in pairs( self.Players ) do + if PlayerData then -- This should normally not happen, but i'll test it anyway. + self:T( "Score Player: " .. PlayerName ) + + -- Some variables + local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] + local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] + local InitUnitType = PlayerData.UnitType + local InitUnitName = PlayerData.UnitName + + local PlayerScore = 0 + local PlayerPenalty = 0 + + ScoreMessage = ":\n" + + local ScoreMessageHits = "" + + for CategoryID, CategoryName in pairs( _SCORINGCategory ) do + self:T( CategoryName ) + if PlayerData.Hit[CategoryID] then + local Score = 0 + local ScoreHit = 0 + local Penalty = 0 + local PenaltyHit = 0 + self:T( "Hit scores exist for player " .. PlayerName ) + for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do + Score = Score + UnitData.Score + ScoreHit = ScoreHit + UnitData.ScoreHit + Penalty = Penalty + UnitData.Penalty + PenaltyHit = UnitData.PenaltyHit + end + local ScoreMessageHit = string.format( "%s:%d ", CategoryName, Score - Penalty ) + self:T( ScoreMessageHit ) + ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit + PlayerScore = PlayerScore + Score + PlayerPenalty = PlayerPenalty + Penalty + else + --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) + end + end + if ScoreMessageHits ~= "" then + ScoreMessage = ScoreMessage .. " Hits: " .. ScoreMessageHits .. "\n" + end + + local ScoreMessageKills = "" + for CategoryID, CategoryName in pairs( _SCORINGCategory ) do + self:T( "Kill scores exist for player " .. PlayerName ) + if PlayerData.Kill[CategoryID] then + local Score = 0 + local ScoreKill = 0 + local Penalty = 0 + local PenaltyKill = 0 + + for UnitName, UnitData in pairs( PlayerData.Kill[CategoryID] ) do + Score = Score + UnitData.Score + ScoreKill = ScoreKill + UnitData.ScoreKill + Penalty = Penalty + UnitData.Penalty + PenaltyKill = PenaltyKill + UnitData.PenaltyKill + end + + local ScoreMessageKill = string.format( " %s:%d ", CategoryName, Score - Penalty ) + self:T( ScoreMessageKill ) + ScoreMessageKills = ScoreMessageKills .. ScoreMessageKill + + PlayerScore = PlayerScore + Score + PlayerPenalty = PlayerPenalty + Penalty + else + --ScoreMessageKills = ScoreMessageKills .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) + end + end + if ScoreMessageKills ~= "" then + ScoreMessage = ScoreMessage .. " Kills: " .. ScoreMessageKills .. "\n" + end + + local ScoreMessageCoalitionChangePenalties = "" + if PlayerData.PenaltyCoalition ~= 0 then + ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) + PlayerPenalty = PlayerPenalty + PlayerData.Penalty + end + if ScoreMessageCoalitionChangePenalties ~= "" then + ScoreMessage = ScoreMessage .. " Coalition Penalties: " .. ScoreMessageCoalitionChangePenalties .. "\n" + end + + local ScoreMessageMission = "" + local ScoreMission = 0 + local ScoreTask = 0 + for MissionName, MissionData in pairs( PlayerData.Mission ) do + ScoreMission = ScoreMission + MissionData.ScoreMission + ScoreTask = ScoreTask + MissionData.ScoreTask + ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " + end + PlayerScore = PlayerScore + ScoreMission + ScoreTask + + if ScoreMessageMission ~= "" then + ScoreMessage = ScoreMessage .. " Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ")\n" + end + + PlayerMessage = PlayerMessage .. string.format( "Player '%s' Score:%d (%d Score -%d Penalties)%s", PlayerName, PlayerScore - PlayerPenalty, PlayerScore, PlayerPenalty, ScoreMessage ) + end + end + MESSAGE:New( PlayerMessage, "Player Scores", 30, "AllPlayerScores"):ToAll() +end + + +function SCORING:ReportScorePlayer() + + env.info( "Hello World " ) + + local ScoreMessage = "" + local PlayerMessage = "" + + self:T( "Score Report" ) + + for PlayerName, PlayerData in pairs( self.Players ) do + if PlayerData then -- This should normally not happen, but i'll test it anyway. + self:T( "Score Player: " .. PlayerName ) + + -- Some variables + local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] + local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] + local InitUnitType = PlayerData.UnitType + local InitUnitName = PlayerData.UnitName + + local PlayerScore = 0 + local PlayerPenalty = 0 + + ScoreMessage = "" + + local ScoreMessageHits = "" + + for CategoryID, CategoryName in pairs( _SCORINGCategory ) do + self:T( CategoryName ) + if PlayerData.Hit[CategoryID] then + local Score = 0 + local ScoreHit = 0 + local Penalty = 0 + local PenaltyHit = 0 + self:T( "Hit scores exist for player " .. PlayerName ) + for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do + Score = Score + UnitData.Score + ScoreHit = ScoreHit + UnitData.ScoreHit + Penalty = Penalty + UnitData.Penalty + PenaltyHit = UnitData.PenaltyHit + end + local ScoreMessageHit = string.format( "\n %s = %d score(%d;-%d) hits(#%d;#-%d)", CategoryName, Score - Penalty, Score, Penalty, ScoreHit, PenaltyHit ) + self:T( ScoreMessageHit ) + ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit + PlayerScore = PlayerScore + Score + PlayerPenalty = PlayerPenalty + Penalty + else + --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) + end + end + if ScoreMessageHits ~= "" then + ScoreMessage = ScoreMessage .. "\n Hits: " .. ScoreMessageHits .. " " + end + + local ScoreMessageKills = "" + for CategoryID, CategoryName in pairs( _SCORINGCategory ) do + self:T( "Kill scores exist for player " .. PlayerName ) + if PlayerData.Kill[CategoryID] then + local Score = 0 + local ScoreKill = 0 + local Penalty = 0 + local PenaltyKill = 0 + + for UnitName, UnitData in pairs( PlayerData.Kill[CategoryID] ) do + Score = Score + UnitData.Score + ScoreKill = ScoreKill + UnitData.ScoreKill + Penalty = Penalty + UnitData.Penalty + PenaltyKill = PenaltyKill + UnitData.PenaltyKill + end + + local ScoreMessageKill = string.format( "\n %s = %d score(%d;-%d) hits(#%d;#-%d)", CategoryName, Score - Penalty, Score, Penalty, ScoreKill, PenaltyKill ) + self:T( ScoreMessageKill ) + ScoreMessageKills = ScoreMessageKills .. ScoreMessageKill + + PlayerScore = PlayerScore + Score + PlayerPenalty = PlayerPenalty + Penalty + else + --ScoreMessageKills = ScoreMessageKills .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) + end + end + if ScoreMessageKills ~= "" then + ScoreMessage = ScoreMessage .. "\n Kills: " .. ScoreMessageKills .. " " + end + + local ScoreMessageCoalitionChangePenalties = "" + if PlayerData.PenaltyCoalition ~= 0 then + ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) + PlayerPenalty = PlayerPenalty + PlayerData.Penalty + end + if ScoreMessageCoalitionChangePenalties ~= "" then + ScoreMessage = ScoreMessage .. "\n Coalition: " .. ScoreMessageCoalitionChangePenalties .. " " + end + + local ScoreMessageMission = "" + local ScoreMission = 0 + local ScoreTask = 0 + for MissionName, MissionData in pairs( PlayerData.Mission ) do + ScoreMission = ScoreMission + MissionData.ScoreMission + ScoreTask = ScoreTask + MissionData.ScoreTask + ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " + end + PlayerScore = PlayerScore + ScoreMission + ScoreTask + + if ScoreMessageMission ~= "" then + ScoreMessage = ScoreMessage .. "\n Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ") " + end + + PlayerMessage = PlayerMessage .. string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties ):%s", PlayerName, PlayerScore - PlayerPenalty, PlayerScore, PlayerPenalty, ScoreMessage ) + end + end + MESSAGE:New( PlayerMessage, "Player Scores", 30, "AllPlayerScores"):ToAll() + +end + + +function SCORING:SecondsToClock(sSeconds) + local nSeconds = sSeconds + if nSeconds == 0 then + --return nil; + return "00:00:00"; + else + nHours = string.format("%02.f", math.floor(nSeconds/3600)); + nMins = string.format("%02.f", math.floor(nSeconds/60 - (nHours*60))); + nSecs = string.format("%02.f", math.floor(nSeconds - nHours*3600 - nMins *60)); + return nHours..":"..nMins..":"..nSecs + end +end + +--- Opens a score CSV file to log the scores. +-- @param #SCORING self +-- @param #string ScoringCSV +-- @return #SCORING self +-- @usage +-- -- Open a new CSV file to log the scores of the game Gori Valley. Let the name of the CSV file begin with "Player Scores". +-- ScoringObject = SCORING:New( "Gori Valley" ) +-- ScoringObject:OpenCSV( "Player Scores" ) +function SCORING:OpenCSV( ScoringCSV ) + self:F( ScoringCSV ) + + if lfs and io and os then + if ScoringCSV then + self.ScoringCSV = ScoringCSV + local fdir = lfs.writedir() .. [[Logs\]] .. self.ScoringCSV .. " " .. os.date( "%Y-%m-%d %H-%M-%S" ) .. ".csv" + + self.CSVFile, self.err = io.open( fdir, "w+" ) + if not self.CSVFile then + error( "Error: Cannot open CSV file in " .. lfs.writedir() ) + end + + self.CSVFile:write( '"GameName","RunTime","Time","PlayerName","ScoreType","PlayerUnitCoaltion","PlayerUnitCategory","PlayerUnitType","PlayerUnitName","TargetUnitCoalition","TargetUnitCategory","TargetUnitType","TargetUnitName","Times","Score"\n' ) + + self.RunTime = os.date("%y-%m-%d_%H-%M-%S") + else + error( "A string containing the CSV file name must be given." ) + end + else + self:E( "The MissionScripting.lua file has not been changed to allow lfs, io and os modules to be used..." ) + end + return self +end + + +--- Registers a score for a player. +-- @param #SCORING self +-- @param #string PlayerName The name of the player. +-- @param #string ScoreType The type of the score. +-- @param #string ScoreTimes The amount of scores achieved. +-- @param #string ScoreAmount The score given. +-- @param #string PlayerUnitName The unit name of the player. +-- @param #string PlayerUnitCoalition The coalition of the player unit. +-- @param #string PlayerUnitCategory The category of the player unit. +-- @param #string PlayerUnitType The type of the player unit. +-- @param #string TargetUnitName The name of the target unit. +-- @param #string TargetUnitCoalition The coalition of the target unit. +-- @param #string TargetUnitCategory The category of the target unit. +-- @param #string TargetUnitType The type of the target unit. +-- @return #SCORING self +function SCORING:ScoreCSV( PlayerName, ScoreType, ScoreTimes, ScoreAmount, PlayerUnitName, PlayerUnitCoalition, PlayerUnitCategory, PlayerUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + --write statistic information to file + local ScoreTime = self:SecondsToClock( timer.getTime() ) + PlayerName = PlayerName:gsub( '"', '_' ) + + if PlayerUnitName and PlayerUnitName ~= '' then + local PlayerUnit = Unit.getByName( PlayerUnitName ) + + if PlayerUnit then + if not PlayerUnitCategory then + --PlayerUnitCategory = SCORINGCategory[PlayerUnit:getCategory()] + PlayerUnitCategory = _SCORINGCategory[PlayerUnit:getDesc().category] + end + + if not PlayerUnitCoalition then + PlayerUnitCoalition = _SCORINGCoalition[PlayerUnit:getCoalition()] + end + + if not PlayerUnitType then + PlayerUnitType = PlayerUnit:getTypeName() + end + else + PlayerUnitName = '' + PlayerUnitCategory = '' + PlayerUnitCoalition = '' + PlayerUnitType = '' + end + else + PlayerUnitName = '' + PlayerUnitCategory = '' + PlayerUnitCoalition = '' + PlayerUnitType = '' + end + + if not TargetUnitCoalition then + TargetUnitCoalition = '' + end + + if not TargetUnitCategory then + TargetUnitCategory = '' + end + + if not TargetUnitType then + TargetUnitType = '' + end + + if not TargetUnitName then + TargetUnitName = '' + end + + if lfs and io and os then + self.CSVFile:write( + '"' .. self.GameName .. '"' .. ',' .. + '"' .. self.RunTime .. '"' .. ',' .. + '' .. ScoreTime .. '' .. ',' .. + '"' .. PlayerName .. '"' .. ',' .. + '"' .. ScoreType .. '"' .. ',' .. + '"' .. PlayerUnitCoalition .. '"' .. ',' .. + '"' .. PlayerUnitCategory .. '"' .. ',' .. + '"' .. PlayerUnitType .. '"' .. ',' .. + '"' .. PlayerUnitName .. '"' .. ',' .. + '"' .. TargetUnitCoalition .. '"' .. ',' .. + '"' .. TargetUnitCategory .. '"' .. ',' .. + '"' .. TargetUnitType .. '"' .. ',' .. + '"' .. TargetUnitName .. '"' .. ',' .. + '' .. ScoreTimes .. '' .. ',' .. + '' .. ScoreAmount + ) + + self.CSVFile:write( "\n" ) + end +end + + +function SCORING:CloseCSV() + if lfs and io and os then + self.CSVFile:close() + end +end + +--- CARGO Classes +-- @module CARGO + +Include.File( "Routines" ) +Include.File( "Base" ) +Include.File( "Message" ) +Include.File( "Scheduler" ) + + +--- Clients are those Groups defined within the Mission Editor that have the skillset defined as "Client" or "Player". +-- These clients are defined within the Mission Orchestration Framework (MOF) + +CARGOS = {} + + +CARGO_ZONE = { + ClassName="CARGO_ZONE", + CargoZoneName = '', + CargoHostUnitName = '', + SIGNAL = { + TYPE = { + SMOKE = { ID = 1, TEXT = "smoke" }, + FLARE = { ID = 2, TEXT = "flare" } + }, + COLOR = { + GREEN = { ID = 1, TRIGGERCOLOR = trigger.smokeColor.Green, TEXT = "A green" }, + RED = { ID = 2, TRIGGERCOLOR = trigger.smokeColor.Red, TEXT = "A red" }, + WHITE = { ID = 3, TRIGGERCOLOR = trigger.smokeColor.White, TEXT = "A white" }, + ORANGE = { ID = 4, TRIGGERCOLOR = trigger.smokeColor.Orange, TEXT = "An orange" }, + BLUE = { ID = 5, TRIGGERCOLOR = trigger.smokeColor.Blue, TEXT = "A blue" }, + YELLOW = { ID = 6, TRIGGERCOLOR = trigger.flareColor.Yellow, TEXT = "A yellow" } + } + } +} + +--- Creates a new zone where cargo can be collected or deployed. +-- The zone functionality is useful to smoke or indicate routes for cargo pickups or deployments. +-- Provide the zone name as declared in the mission file into the CargoZoneName in the :New method. +-- An optional parameter is the CargoHostName, which is a Group declared with Late Activation switched on in the mission file. +-- The CargoHostName is the "host" of the cargo zone: +-- +-- * It will smoke the zone position when a client is approaching the zone. +-- * Depending on the cargo type, it will assist in the delivery of the cargo by driving to and from the client. +-- +-- @param #CARGO_ZONE self +-- @param #string CargoZoneName The name of the zone as declared within the mission editor. +-- @param #string CargoHostName The name of the Group "hosting" the zone. The Group MUST NOT be a static, and must be a "mobile" unit. +function CARGO_ZONE:New( CargoZoneName, CargoHostName ) local self = BASE:Inherit( self, ZONE:New( CargoZoneName ) ) + self:F( { CargoZoneName, CargoHostName } ) + + self.CargoZoneName = CargoZoneName + self.SignalHeight = 2 + --self.CargoZone = trigger.misc.getZone( CargoZoneName ) + + + if CargoHostName then + self.CargoHostName = CargoHostName + end + + self:T( self.CargoZoneName ) + + return self +end + +function CARGO_ZONE:Spawn() + self:F( self.CargoHostName ) + + if self.CargoHostName then -- Only spawn a host in the zone when there is one given as a parameter in the New function. + if self.CargoHostSpawn then + local CargoHostGroup = self.CargoHostSpawn:GetGroupFromIndex() + if CargoHostGroup and CargoHostGroup:IsAlive() then + else + self.CargoHostSpawn:ReSpawn( 1 ) + end + else + self:T( "Initialize CargoHostSpawn" ) + self.CargoHostSpawn = SPAWN:New( self.CargoHostName ):Limit( 1, 1 ) + self.CargoHostSpawn:ReSpawn( 1 ) + end + end + + return self +end + +function CARGO_ZONE:GetHostUnit() + self:F( self ) + + if self.CargoHostName then + + -- A Host has been given, signal the host + local CargoHostGroup = self.CargoHostSpawn:GetGroupFromIndex() + local CargoHostUnit + if CargoHostGroup and CargoHostGroup:IsAlive() then + CargoHostUnit = CargoHostGroup:GetUnit(1) + else + CargoHostUnit = StaticObject.getByName( self.CargoHostName ) + end + + return CargoHostUnit + end + + return nil +end + +function CARGO_ZONE:ReportCargosToClient( Client, CargoType ) + self:F() + + local SignalUnit = self:GetHostUnit() + + if SignalUnit then + + local SignalUnitTypeName = SignalUnit:getTypeName() + + local HostMessage = "" + + local IsCargo = false + for CargoID, Cargo in pairs( CARGOS ) do + if Cargo.CargoType == Task.CargoType then + if Cargo:IsStatusNone() then + HostMessage = HostMessage .. " - " .. Cargo.CargoName .. " - " .. Cargo.CargoType .. " (" .. Cargo.Weight .. "kg)" .. "\n" + IsCargo = true + end + end + end + + if not IsCargo then + HostMessage = "No Cargo Available." + end + + Client:Message( HostMessage, 20, Mission.Name .. "/StageHosts." .. SignalUnitTypeName, SignalUnitTypeName .. ": Reporting Cargo", 10 ) + end +end + + +function CARGO_ZONE:Signal() + self:F() + + local Signalled = false + + if self.SignalType then + + if self.CargoHostName then + + -- A Host has been given, signal the host + + local SignalUnit = self:GetHostUnit() + + if SignalUnit then + + self:T( 'Signalling Unit' ) + local SignalVehiclePos = SignalUnit:GetPointVec3() + SignalVehiclePos.y = SignalVehiclePos.y + 2 + + if self.SignalType.ID == CARGO_ZONE.SIGNAL.TYPE.SMOKE.ID then + + trigger.action.smoke( SignalVehiclePos, self.SignalColor.TRIGGERCOLOR ) + Signalled = true + + elseif self.SignalType.ID == CARGO_ZONE.SIGNAL.TYPE.FLARE.ID then + + trigger.action.signalFlare( SignalVehiclePos, self.SignalColor.TRIGGERCOLOR , 0 ) + Signalled = false + + end + end + + else + + local ZonePointVec3 = self:GetPointVec3( self.SignalHeight ) -- Get the zone position + the landheight + 2 meters + + if self.SignalType.ID == CARGO_ZONE.SIGNAL.TYPE.SMOKE.ID then + + trigger.action.smoke( ZonePointVec3, self.SignalColor.TRIGGERCOLOR ) + Signalled = true + + elseif self.SignalType.ID == CARGO_ZONE.SIGNAL.TYPE.FLARE.ID then + trigger.action.signalFlare( ZonePointVec3, self.SignalColor.TRIGGERCOLOR, 0 ) + Signalled = false + + end + end + end + + return Signalled + +end + +function CARGO_ZONE:WhiteSmoke( SignalHeight ) + self:F() + + self.SignalType = CARGO_ZONE.SIGNAL.TYPE.SMOKE + self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.WHITE + + if SignalHeight then + self.SignalHeight = SignalHeight + end + + return self +end + +function CARGO_ZONE:BlueSmoke( SignalHeight ) + self:F() + + self.SignalType = CARGO_ZONE.SIGNAL.TYPE.SMOKE + self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.BLUE + + if SignalHeight then + self.SignalHeight = SignalHeight + end + + return self +end + +function CARGO_ZONE:RedSmoke( SignalHeight ) + self:F() + + self.SignalType = CARGO_ZONE.SIGNAL.TYPE.SMOKE + self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.RED + + if SignalHeight then + self.SignalHeight = SignalHeight + end + + return self +end + +function CARGO_ZONE:OrangeSmoke( SignalHeight ) + self:F() + + self.SignalType = CARGO_ZONE.SIGNAL.TYPE.SMOKE + self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.ORANGE + + if SignalHeight then + self.SignalHeight = SignalHeight + end + + return self +end + +function CARGO_ZONE:GreenSmoke( SignalHeight ) + self:F() + + self.SignalType = CARGO_ZONE.SIGNAL.TYPE.SMOKE + self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.GREEN + + if SignalHeight then + self.SignalHeight = SignalHeight + end + + return self +end + + +function CARGO_ZONE:WhiteFlare( SignalHeight ) + self:F() + + self.SignalType = CARGO_ZONE.SIGNAL.TYPE.FLARE + self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.WHITE + + if SignalHeight then + self.SignalHeight = SignalHeight + end + + return self +end + +function CARGO_ZONE:RedFlare( SignalHeight ) + self:F() + + self.SignalType = CARGO_ZONE.SIGNAL.TYPE.FLARE + self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.RED + + if SignalHeight then + self.SignalHeight = SignalHeight + end + + return self +end + +function CARGO_ZONE:GreenFlare( SignalHeight ) + self:F() + + self.SignalType = CARGO_ZONE.SIGNAL.TYPE.FLARE + self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.GREEN + + if SignalHeight then + self.SignalHeight = SignalHeight + end + + return self +end + +function CARGO_ZONE:YellowFlare( SignalHeight ) + self:F() + + self.SignalType = CARGO_ZONE.SIGNAL.TYPE.FLARE + self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.YELLOW + + if SignalHeight then + self.SignalHeight = SignalHeight + end + + return self +end + + +function CARGO_ZONE:GetCargoHostUnit() + self:F( self ) + + if self.CargoHostSpawn then + local CargoHostGroup = self.CargoHostSpawn:GetGroupFromIndex(1) + if CargoHostGroup and CargoHostGroup:IsAlive() then + local CargoHostUnit = CargoHostGroup:GetUnit(1) + if CargoHostUnit and CargoHostUnit:IsAlive() then + return CargoHostUnit + end + end + end + + return nil +end + +function CARGO_ZONE:GetCargoZoneName() + self:F() + + return self.CargoZoneName +end + +CARGO = { + ClassName = "CARGO", + STATUS = { + NONE = 0, + LOADED = 1, + UNLOADED = 2, + LOADING = 3 + }, + CargoClient = nil +} + +--- Add Cargo to the mission... Cargo functionality needs to be reworked a bit, so this is still under construction. I need to make a CARGO Class... +function CARGO:New( CargoType, CargoName, CargoWeight ) local self = BASE:Inherit( self, BASE:New() ) + self:F( { CargoType, CargoName, CargoWeight } ) + + + self.CargoType = CargoType + self.CargoName = CargoName + self.CargoWeight = CargoWeight + + self:StatusNone() + + return self +end + +function CARGO:Spawn( Client ) + self:F() + + return self + +end + +function CARGO:IsNear( Client, LandingZone ) + self:F() + + local Near = true + + return Near + +end + + +function CARGO:IsLoadingToClient() + self:F() + + if self:IsStatusLoading() then + return self.CargoClient + end + + return nil + +end + + +function CARGO:IsLoadedInClient() + self:F() + + if self:IsStatusLoaded() then + return self.CargoClient + end + + return nil + +end + + +function CARGO:UnLoad( Client, TargetZoneName ) + self:F() + + self:StatusUnLoaded() + + return self +end + +function CARGO:OnBoard( Client, LandingZone ) + self:F() + + local Valid = true + + self.CargoClient = Client + local ClientUnit = Client:GetClientGroupDCSUnit() + + return Valid +end + +function CARGO:OnBoarded( Client, LandingZone ) + self:F() + + local OnBoarded = true + + return OnBoarded +end + +function CARGO:Load( Client ) + self:F() + + self:StatusLoaded( Client ) + + return self +end + +function CARGO:IsLandingRequired() + self:F() + return true +end + +function CARGO:IsSlingLoad() + self:F() + return false +end + + +function CARGO:StatusNone() + self:F() + + self.CargoClient = nil + self.CargoStatus = CARGO.STATUS.NONE + + return self +end + +function CARGO:StatusLoading( Client ) + self:F() + + self.CargoClient = Client + self.CargoStatus = CARGO.STATUS.LOADING + self:T( "Cargo " .. self.CargoName .. " loading to Client: " .. self.CargoClient:GetClientGroupName() ) + + return self +end + +function CARGO:StatusLoaded( Client ) + self:F() + + self.CargoClient = Client + self.CargoStatus = CARGO.STATUS.LOADED + self:T( "Cargo " .. self.CargoName .. " loaded in Client: " .. self.CargoClient:GetClientGroupName() ) + + return self +end + +function CARGO:StatusUnLoaded() + self:F() + + self.CargoClient = nil + self.CargoStatus = CARGO.STATUS.UNLOADED + + return self +end + + +function CARGO:IsStatusNone() + self:F() + + return self.CargoStatus == CARGO.STATUS.NONE +end + +function CARGO:IsStatusLoading() + self:F() + + return self.CargoStatus == CARGO.STATUS.LOADING +end + +function CARGO:IsStatusLoaded() + self:F() + + return self.CargoStatus == CARGO.STATUS.LOADED +end + +function CARGO:IsStatusUnLoaded() + self:F() + + return self.CargoStatus == CARGO.STATUS.UNLOADED +end + + +CARGO_GROUP = { + ClassName = "CARGO_GROUP" +} + + +function CARGO_GROUP:New( CargoType, CargoName, CargoWeight, CargoGroupTemplate, CargoZone ) local self = BASE:Inherit( self, CARGO:New( CargoType, CargoName, CargoWeight ) ) + self:F( { CargoType, CargoName, CargoWeight, CargoGroupTemplate, CargoZone } ) + + self.CargoSpawn = SPAWN:NewWithAlias( CargoGroupTemplate, CargoName ) + self.CargoZone = CargoZone + + CARGOS[self.CargoName] = self + + return self + +end + +function CARGO_GROUP:Spawn( Client ) + self:F( { Client } ) + + local SpawnCargo = true + + if self:IsStatusNone() then + local CargoGroup = Group.getByName( self.CargoName ) + if CargoGroup and CargoGroup:isExist() then + SpawnCargo = false + end + + elseif self:IsStatusLoading() then + + local Client = self:IsLoadingToClient() + if Client and Client:GetDCSGroup() then + SpawnCargo = false + else + local CargoGroup = Group.getByName( self.CargoName ) + if CargoGroup and CargoGroup:isExist() then + SpawnCargo = false + end + end + + elseif self:IsStatusLoaded() then + + local ClientLoaded = self:IsLoadedInClient() + -- Now test if another Client is alive (not this one), and it has the CARGO, then this cargo does not need to be initialized and spawned. + if ClientLoaded and ClientLoaded ~= Client then + local ClientGroup = Client:GetDCSGroup() + if ClientLoaded:GetClientGroupDCSUnit() and ClientLoaded:GetClientGroupDCSUnit():isExist() then + SpawnCargo = false + else + self:StatusNone() + end + else + -- Same Client, but now in initialize, so set back the status to None. + self:StatusNone() + end + + elseif self:IsStatusUnLoaded() then + + SpawnCargo = false + + end + + if SpawnCargo then + if self.CargoZone:GetCargoHostUnit() then + --- ReSpawn the Cargo from the CargoHost + self.CargoGroupName = self.CargoSpawn:SpawnFromUnit( self.CargoZone:GetCargoHostUnit(), 60, 30, 1 ):GetName() + else + --- ReSpawn the Cargo in the CargoZone without a host ... + self:T( self.CargoZone ) + self.CargoGroupName = self.CargoSpawn:SpawnInZone( self.CargoZone, true, 1 ):GetName() + end + self:StatusNone() + end + + self:T( { self.CargoGroupName, CARGOS[self.CargoName].CargoGroupName } ) + + return self +end + +function CARGO_GROUP:IsNear( Client, LandingZone ) + self:F() + + local Near = false + + if self.CargoGroupName then + local CargoGroup = Group.getByName( self.CargoGroupName ) + if routines.IsPartOfGroupInRadius( CargoGroup, Client:GetPositionVec3(), 250 ) then + Near = true + end + end + + return Near + +end + + +function CARGO_GROUP:OnBoard( Client, LandingZone, OnBoardSide ) + self:F() + + local Valid = true + + local ClientUnit = Client:GetClientGroupDCSUnit() + + local CarrierPos = ClientUnit:getPoint() + local CarrierPosMove = ClientUnit:getPoint() + local CarrierPosOnBoard = ClientUnit:getPoint() + + local CargoGroup = Group.getByName( self.CargoGroupName ) + + local CargoUnit = CargoGroup:getUnit(1) + local CargoPos = CargoUnit:getPoint() + + self.CargoInAir = CargoUnit:inAir() + + self:T( self.CargoInAir ) + + -- Only move the group to the carrier when the cargo is not in the air + -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea). + if not self.CargoInAir then + + local Points = {} + + self:T( 'CargoPos x = ' .. CargoPos.x .. " z = " .. CargoPos.z ) + self:T( 'CarrierPosMove x = ' .. CarrierPosMove.x .. " z = " .. CarrierPosMove.z ) + + Points[#Points+1] = routines.ground.buildWP( CargoPos, "Cone", 10 ) + + self:T( 'Points[1] x = ' .. Points[1].x .. " y = " .. Points[1].y ) + + if OnBoardSide == nil then + OnBoardSide = CLIENT.ONBOARDSIDE.NONE + end + + if OnBoardSide == CLIENT.ONBOARDSIDE.LEFT then + + self:T( "TransportCargoOnBoard: Onboarding LEFT" ) + CarrierPosMove.z = CarrierPosMove.z - 25 + CarrierPosOnBoard.z = CarrierPosOnBoard.z - 5 + Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) + Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) + + elseif OnBoardSide == CLIENT.ONBOARDSIDE.RIGHT then + + self:T( "TransportCargoOnBoard: Onboarding RIGHT" ) + CarrierPosMove.z = CarrierPosMove.z + 25 + CarrierPosOnBoard.z = CarrierPosOnBoard.z + 5 + Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) + Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) + + elseif OnBoardSide == CLIENT.ONBOARDSIDE.BACK then + + self:T( "TransportCargoOnBoard: Onboarding BACK" ) + CarrierPosMove.x = CarrierPosMove.x - 25 + CarrierPosOnBoard.x = CarrierPosOnBoard.x - 5 + Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) + Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) + + elseif OnBoardSide == CLIENT.ONBOARDSIDE.FRONT then + + self:T( "TransportCargoOnBoard: Onboarding FRONT" ) + CarrierPosMove.x = CarrierPosMove.x + 25 + CarrierPosOnBoard.x = CarrierPosOnBoard.x + 5 + Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) + Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) + + elseif OnBoardSide == CLIENT.ONBOARDSIDE.NONE then + + self:T( "TransportCargoOnBoard: Onboarding CENTRAL" ) + Points[#Points+1] = routines.ground.buildWP( CarrierPos, "Cone", 10 ) + + end + self:T( "TransportCargoOnBoard: Routing " .. self.CargoGroupName ) + + --routines.scheduleFunction( routines.goRoute, { self.CargoGroupName, Points}, timer.getTime() + 4 ) + SCHEDULER:New( self, routines.goRoute, { self.CargoGroupName, Points}, 4 ) + end + + self:StatusLoading( Client ) + + return Valid + +end + + +function CARGO_GROUP:OnBoarded( Client, LandingZone ) + self:F() + + local OnBoarded = false + + local CargoGroup = Group.getByName( self.CargoGroupName ) + + if not self.CargoInAir then + if routines.IsPartOfGroupInRadius( CargoGroup, Client:GetPositionVec3(), 25 ) then + CargoGroup:destroy() + self:StatusLoaded( Client ) + OnBoarded = true + end + else + CargoGroup:destroy() + self:StatusLoaded( Client ) + OnBoarded = true + end + + return OnBoarded +end + + +function CARGO_GROUP:UnLoad( Client, TargetZoneName ) + self:F() + + self:T( 'self.CargoName = ' .. self.CargoName ) + + local CargoGroup = self.CargoSpawn:SpawnFromUnit( Client:GetClientGroupUnit(), 60, 30 ) + + self.CargoGroupName = CargoGroup:GetName() + self:T( 'self.CargoGroupName = ' .. self.CargoGroupName ) + + CargoGroup:TaskRouteToZone( ZONE:New( TargetZoneName ), true ) + + self:StatusUnLoaded() + + return self +end + + +CARGO_PACKAGE = { + ClassName = "CARGO_PACKAGE" +} + + +function CARGO_PACKAGE:New( CargoType, CargoName, CargoWeight, CargoClient ) local self = BASE:Inherit( self, CARGO:New( CargoType, CargoName, CargoWeight ) ) + self:F( { CargoType, CargoName, CargoWeight, CargoClient } ) + + self.CargoClient = CargoClient + + CARGOS[self.CargoName] = self + + return self + +end + + +function CARGO_PACKAGE:Spawn( Client ) + self:F( { self, Client } ) + + -- this needs to be checked thoroughly + + local CargoClientGroup = self.CargoClient:GetDCSGroup() + if not CargoClientGroup then + if not self.CargoClientSpawn then + self.CargoClientSpawn = SPAWN:New( self.CargoClient:GetClientGroupName() ):Limit( 1, 1 ) + end + self.CargoClientSpawn:ReSpawn( 1 ) + end + + local SpawnCargo = true + + if self:IsStatusNone() then + + elseif self:IsStatusLoading() or self:IsStatusLoaded() then + + local CargoClientLoaded = self:IsLoadedInClient() + if CargoClientLoaded and CargoClientLoaded:GetDCSGroup() then + SpawnCargo = false + end + + elseif self:IsStatusUnLoaded() then + + SpawnCargo = false + + else + + end + + if SpawnCargo then + self:StatusLoaded( self.CargoClient ) + end + + return self +end + + +function CARGO_PACKAGE:IsNear( Client, LandingZone ) + self:F() + + local Near = false + + if self.CargoClient and self.CargoClient:GetDCSGroup() then + self:T( self.CargoClient.ClientName ) + self:T( 'Client Exists.' ) + + if routines.IsUnitInRadius( self.CargoClient:GetClientGroupDCSUnit(), Client:GetPositionVec3(), 150 ) then + Near = true + end + end + + return Near + +end + + +function CARGO_PACKAGE:OnBoard( Client, LandingZone, OnBoardSide ) + self:F() + + local Valid = true + + local ClientUnit = Client:GetClientGroupDCSUnit() + + local CarrierPos = ClientUnit:getPoint() + local CarrierPosMove = ClientUnit:getPoint() + local CarrierPosOnBoard = ClientUnit:getPoint() + local CarrierPosMoveAway = ClientUnit:getPoint() + + local CargoHostGroup = self.CargoClient:GetDCSGroup() + local CargoHostName = self.CargoClient:GetDCSGroup():getName() + + local CargoHostUnits = CargoHostGroup:getUnits() + local CargoPos = CargoHostUnits[1]:getPoint() + + local Points = {} + + self:T( 'CargoPos x = ' .. CargoPos.x .. " z = " .. CargoPos.z ) + self:T( 'CarrierPosMove x = ' .. CarrierPosMove.x .. " z = " .. CarrierPosMove.z ) + + Points[#Points+1] = routines.ground.buildWP( CargoPos, "Cone", 10 ) + + self:T( 'Points[1] x = ' .. Points[1].x .. " y = " .. Points[1].y ) + + if OnBoardSide == nil then + OnBoardSide = CLIENT.ONBOARDSIDE.NONE + end + + if OnBoardSide == CLIENT.ONBOARDSIDE.LEFT then + + self:T( "TransportCargoOnBoard: Onboarding LEFT" ) + CarrierPosMove.z = CarrierPosMove.z - 25 + CarrierPosOnBoard.z = CarrierPosOnBoard.z - 5 + CarrierPosMoveAway.z = CarrierPosMoveAway.z - 20 + Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) + Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) + Points[#Points+1] = routines.ground.buildWP( CarrierPosMoveAway, "Cone", 10 ) + + elseif OnBoardSide == CLIENT.ONBOARDSIDE.RIGHT then + + self:T( "TransportCargoOnBoard: Onboarding RIGHT" ) + CarrierPosMove.z = CarrierPosMove.z + 25 + CarrierPosOnBoard.z = CarrierPosOnBoard.z + 5 + CarrierPosMoveAway.z = CarrierPosMoveAway.z + 20 + Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) + Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) + Points[#Points+1] = routines.ground.buildWP( CarrierPosMoveAway, "Cone", 10 ) + + elseif OnBoardSide == CLIENT.ONBOARDSIDE.BACK then + + self:T( "TransportCargoOnBoard: Onboarding BACK" ) + CarrierPosMove.x = CarrierPosMove.x - 25 + CarrierPosOnBoard.x = CarrierPosOnBoard.x - 5 + CarrierPosMoveAway.x = CarrierPosMoveAway.x - 20 + Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) + Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) + Points[#Points+1] = routines.ground.buildWP( CarrierPosMoveAway, "Cone", 10 ) + + elseif OnBoardSide == CLIENT.ONBOARDSIDE.FRONT then + + self:T( "TransportCargoOnBoard: Onboarding FRONT" ) + CarrierPosMove.x = CarrierPosMove.x + 25 + CarrierPosOnBoard.x = CarrierPosOnBoard.x + 5 + CarrierPosMoveAway.x = CarrierPosMoveAway.x + 20 + Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) + Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) + Points[#Points+1] = routines.ground.buildWP( CarrierPosMoveAway, "Cone", 10 ) + + elseif OnBoardSide == CLIENT.ONBOARDSIDE.NONE then + + self:T( "TransportCargoOnBoard: Onboarding FRONT" ) + CarrierPosMove.x = CarrierPosMove.x + 25 + CarrierPosOnBoard.x = CarrierPosOnBoard.x + 5 + CarrierPosMoveAway.x = CarrierPosMoveAway.x + 20 + Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) + Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) + Points[#Points+1] = routines.ground.buildWP( CarrierPosMoveAway, "Cone", 10 ) + + end + self:T( "Routing " .. CargoHostName ) + + --routines.scheduleFunction( routines.goRoute, { CargoHostName, Points}, timer.getTime() + 4 ) + SCHEDULER:New( self, routines.goRoute, { CargoHostName, Points }, 4 ) + + return Valid + +end + + +function CARGO_PACKAGE:OnBoarded( Client, LandingZone ) + self:F() + + local OnBoarded = false + + if self.CargoClient and self.CargoClient:GetDCSGroup() then + if routines.IsUnitInRadius( self.CargoClient:GetClientGroupDCSUnit(), self.CargoClient:GetPositionVec3(), 10 ) then + + -- Switch Cargo from self.CargoClient to Client ... Each cargo can have only one client. So assigning the new client for the cargo is enough. + self:StatusLoaded( Client ) + + -- All done, onboarded the Cargo to the new Client. + OnBoarded = true + end + end + + return OnBoarded +end + + +function CARGO_PACKAGE:UnLoad( Client, TargetZoneName ) + self:F() + + self:T( 'self.CargoName = ' .. self.CargoName ) + --self:T( 'self.CargoHostName = ' .. self.CargoHostName ) + + --self.CargoSpawn:FromCarrier( Client:GetDCSGroup(), TargetZoneName, self.CargoHostName ) + self:StatusUnLoaded() + + return Cargo +end + + +CARGO_SLINGLOAD = { + ClassName = "CARGO_SLINGLOAD" +} + + +function CARGO_SLINGLOAD:New( CargoType, CargoName, CargoWeight, CargoZone, CargoHostName, CargoCountryID ) + local self = BASE:Inherit( self, CARGO:New( CargoType, CargoName, CargoWeight ) ) + self:F( { CargoType, CargoName, CargoWeight, CargoZone, CargoHostName, CargoCountryID } ) + + self.CargoHostName = CargoHostName + + -- Cargo will be initialized around the CargoZone position. + self.CargoZone = CargoZone + + self.CargoCount = 0 + self.CargoStaticName = string.format( "%s#%03d", self.CargoName, self.CargoCount ) + + -- The country ID needs to be correctly set. + self.CargoCountryID = CargoCountryID + + CARGOS[self.CargoName] = self + + return self + +end + + +function CARGO_SLINGLOAD:IsLandingRequired() + self:F() + return false +end + + +function CARGO_SLINGLOAD:IsSlingLoad() + self:F() + return true +end + + +function CARGO_SLINGLOAD:Spawn( Client ) + self:F( { self, Client } ) + + local Zone = trigger.misc.getZone( self.CargoZone ) + + local ZonePos = {} + ZonePos.x = Zone.point.x + math.random( Zone.radius / 2 * -1, Zone.radius / 2 ) + ZonePos.y = Zone.point.z + math.random( Zone.radius / 2 * -1, Zone.radius / 2 ) + + self:T( "Cargo Location = " .. ZonePos.x .. ", " .. ZonePos.y ) + + --[[ + -- This does not work in 1.5.2. + CargoStatic = StaticObject.getByName( self.CargoName ) + if CargoStatic then + CargoStatic:destroy() + end + --]] + + CargoStatic = StaticObject.getByName( self.CargoStaticName ) + + if CargoStatic and CargoStatic:isExist() then + CargoStatic:destroy() + end + + -- I need to make every time a new cargo due to bugs in 1.5.2. + + self.CargoCount = self.CargoCount + 1 + self.CargoStaticName = string.format( "%s#%03d", self.CargoName, self.CargoCount ) + + local CargoTemplate = { + ["category"] = "Cargo", + ["shape_name"] = "ab-212_cargo", + ["type"] = "Cargo1", + ["x"] = ZonePos.x, + ["y"] = ZonePos.y, + ["mass"] = self.CargoWeight, + ["name"] = self.CargoStaticName, + ["canCargo"] = true, + ["heading"] = 0, + } + + coalition.addStaticObject( self.CargoCountryID, CargoTemplate ) + +-- end + + return self +end + + +function CARGO_SLINGLOAD:IsNear( Client, LandingZone ) + self:F() + + local Near = false + + return Near +end + + +function CARGO_SLINGLOAD:IsInLandingZone( Client, LandingZone ) + self:F() + + local Near = false + + local CargoStaticUnit = StaticObject.getByName( self.CargoName ) + if CargoStaticUnit then + if routines.IsStaticInZones( CargoStaticUnit, LandingZone ) then + Near = true + end + end + + return Near +end + + +function CARGO_SLINGLOAD:OnBoard( Client, LandingZone, OnBoardSide ) + self:F() + + local Valid = true + + + return Valid +end + + +function CARGO_SLINGLOAD:OnBoarded( Client, LandingZone ) + self:F() + + local OnBoarded = false + + local CargoStaticUnit = StaticObject.getByName( self.CargoName ) + if CargoStaticUnit then + if not routines.IsStaticInZones( CargoStaticUnit, LandingZone ) then + OnBoarded = true + end + end + + return OnBoarded +end + + +function CARGO_SLINGLOAD:UnLoad( Client, TargetZoneName ) + self:F() + + self:T( 'self.CargoName = ' .. self.CargoName ) + self:T( 'self.CargoGroupName = ' .. self.CargoGroupName ) + + self:StatusUnLoaded() + + return Cargo +end +--- Message System to display Messages for Clients and Coalitions or All. +-- Messages are grouped on the display panel per Category to improve readability for the players. +-- Messages are shown on the display panel for an amount of seconds, and will then disappear. +-- Messages are identified by an ID. The messages with the same ID belonging to the same category will be overwritten if they were still being displayed on the display panel. +-- Messages are created with MESSAGE:@{New}(). +-- Messages are sent to Clients with MESSAGE:@{ToClient}(). +-- Messages are sent to Coalitions with MESSAGE:@{ToCoalition}(). +-- Messages are sent to All Players with MESSAGE:@{ToAll}(). +-- @module Message + +Include.File( "Base" ) + +--- The MESSAGE class +-- @type MESSAGE +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 #string MessageCategory is a string expressing the Category of the Message. Messages are grouped on the display panel per Category to improve readability. +-- @param #number MessageDuration is a number in seconds of how long the MESSAGE should be shown on the display panel. +-- @param #string MessageID is a string expressing the ID of the Message. +-- @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!", "End of Mission", 25, "Win" ) +-- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) +-- 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" ) +function MESSAGE:New( MessageText, MessageCategory, MessageDuration, MessageID ) + local self = BASE:Inherit( self, BASE:New() ) + self:F( { MessageText, MessageCategory, MessageDuration, MessageID } ) + + -- When no messagecategory is given, we don't show it as a title... + if MessageCategory and MessageCategory ~= "" then + self.MessageCategory = MessageCategory .. ": " + else + self.MessageCategory = "" + end + + self.MessageDuration = MessageDuration + self.MessageID = MessageID + self.MessageTime = timer.getTime() + self.MessageText = MessageText + + self.MessageSent = false + self.MessageGroup = false + self.MessageCoalition = false + + return self +end + +--- Sends a MESSAGE to a Client Group. Note that the Group needs to be defined within the ME with the skillset "Client" or "Player". +-- @param #MESSAGE self +-- @param Client#CLIENT Client is the Group of the Client. +-- @return #MESSAGE +-- @usage +-- -- Send the 2 messages created with the @{New} method to the Client Group. +-- -- Note that the Message of MessageClient2 is overwriting the Message of MessageClient1. +-- ClientGroup = Group.getByName( "ClientGroup" ) +-- +-- MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ):ToClient( ClientGroup ) +-- MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ):ToClient( ClientGroup ) +-- or +-- MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ):ToClient( ClientGroup ) +-- MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ):ToClient( ClientGroup ) +-- or +-- MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ) +-- MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ) +-- MessageClient1:ToClient( ClientGroup ) +-- MessageClient2:ToClient( ClientGroup ) +function MESSAGE:ToClient( Client ) + self:F( Client ) + + if Client and Client:GetClientGroupID() then + + local ClientGroupID = Client:GetClientGroupID() + self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) + trigger.action.outTextForGroup( ClientGroupID, self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration ) + end + + return self +end + +--- Sends a MESSAGE to the Blue coalition. +-- @param #MESSAGE self +-- @return #MESSAGE +-- @usage +-- -- Send a message created with the @{New} method to the BLUE coalition. +-- MessageBLUE = MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToBlue() +-- or +-- MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToBlue() +-- or +-- MessageBLUE = MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) +-- MessageBLUE:ToBlue() +function MESSAGE:ToBlue() + self:F() + + self:ToCoalition( coalition.side.BLUE ) + + return self +end + +--- Sends a MESSAGE to the Red Coalition. +-- @param #MESSAGE self +-- @return #MESSAGE +-- @usage +-- -- Send a message created with the @{New} method to the RED coalition. +-- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToRed() +-- or +-- MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToRed() +-- or +-- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) +-- MessageRED:ToRed() +function MESSAGE:ToRed( ) + self:F() + + self:ToCoalition( coalition.side.RED ) + + return self +end + +--- Sends a MESSAGE to a Coalition. +-- @param #MESSAGE self +-- @param CoalitionSide needs to be filled out by the defined structure of the standard scripting engine @{coalition.side}. +-- @return #MESSAGE +-- @usage +-- -- Send a message created with the @{New} method to the RED coalition. +-- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToCoalition( coalition.side.RED ) +-- or +-- MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToCoalition( coalition.side.RED ) +-- or +-- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) +-- MessageRED:ToCoalition( coalition.side.RED ) +function MESSAGE:ToCoalition( CoalitionSide ) + self:F( CoalitionSide ) + + if CoalitionSide then + self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) + trigger.action.outTextForCoalition( CoalitionSide, self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration ) + end + + return self +end + +--- Sends a MESSAGE to all players. +-- @param #MESSAGE self +-- @return #MESSAGE +-- @usage +-- -- Send a message created to all players. +-- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ):ToAll() +-- or +-- MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ):ToAll() +-- or +-- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ) +-- MessageAll:ToAll() +function MESSAGE:ToAll() + self:F() + + self:ToCoalition( coalition.side.RED ) + self:ToCoalition( coalition.side.BLUE ) + + return self +end + + + +--- The MESSAGEQUEUE class +-- @type MESSAGEQUEUE +MESSAGEQUEUE = { + ClientGroups = {}, + CoalitionSides = {} +} + +function MESSAGEQUEUE:New( RefreshInterval ) + local self = BASE:Inherit( self, BASE:New() ) + self:F( { RefreshInterval } ) + + self.RefreshInterval = RefreshInterval + + --self.DisplayFunction = routines.scheduleFunction( self._DisplayMessages, { self }, 0, RefreshInterval ) + self.DisplayFunction = SCHEDULER:New( self, self._DisplayMessages, {}, 0, RefreshInterval ) + + return self +end + +--- This function is called automatically by the MESSAGEQUEUE scheduler. +function MESSAGEQUEUE:_DisplayMessages() + + -- First we display all messages that a coalition needs to receive... Also those who are not in a client (CA module clients...). + for CoalitionSideID, CoalitionSideData in pairs( self.CoalitionSides ) do + for MessageID, MessageData in pairs( CoalitionSideData.Messages ) do + if MessageData.MessageSent == false then + --trigger.action.outTextForCoalition( CoalitionSideID, MessageData.MessageCategory .. '\n' .. MessageData.MessageText:gsub("\n$",""):gsub("\n$",""), MessageData.MessageDuration ) + MessageData.MessageSent = true + end + local MessageTimeLeft = ( MessageData.MessageTime + MessageData.MessageDuration ) - timer.getTime() + if MessageTimeLeft <= 0 then + MessageData = nil + end + end + end + + -- Then we send the messages for each individual client, but also to be included are those Coalition messages for the Clients who belong to a coalition. + -- Because the Client messages will overwrite the Coalition messages (for that Client). + for ClientGroupName, ClientGroupData in pairs( self.ClientGroups ) do + for MessageID, MessageData in pairs( ClientGroupData.Messages ) do + if MessageData.MessageGroup == false then + trigger.action.outTextForGroup( Group.getByName(ClientGroupName):getID(), MessageData.MessageCategory .. '\n' .. MessageData.MessageText:gsub("\n$",""):gsub("\n$",""), MessageData.MessageDuration ) + MessageData.MessageGroup = true + end + local MessageTimeLeft = ( MessageData.MessageTime + MessageData.MessageDuration ) - timer.getTime() + if MessageTimeLeft <= 0 then + MessageData = nil + end + end + + -- Now check if the Client also has messages that belong to the Coalition of the Client... + for CoalitionSideID, CoalitionSideData in pairs( self.CoalitionSides ) do + for MessageID, MessageData in pairs( CoalitionSideData.Messages ) do + local CoalitionGroup = Group.getByName( ClientGroupName ) + if CoalitionGroup and CoalitionGroup:getCoalition() == CoalitionSideID then + if MessageData.MessageCoalition == false then + trigger.action.outTextForGroup( Group.getByName(ClientGroupName):getID(), MessageData.MessageCategory .. '\n' .. MessageData.MessageText:gsub("\n$",""):gsub("\n$",""), MessageData.MessageDuration ) + MessageData.MessageCoalition = true + end + end + local MessageTimeLeft = ( MessageData.MessageTime + MessageData.MessageDuration ) - timer.getTime() + if MessageTimeLeft <= 0 then + MessageData = nil + end + end + end + end + + return true +end + +--- The _MessageQueue object is created when the MESSAGE class module is loaded. +--_MessageQueue = MESSAGEQUEUE:New( 0.5 ) + +--- Stages within a @{TASK} within a @{MISSION}. All of the STAGE functionality is considered internally administered and not to be used by any Mission designer. +-- @module STAGE +-- @author Flightcontrol + +Include.File( "Routines" ) +Include.File( "Base" ) +Include.File( "Mission" ) +Include.File( "Client" ) +Include.File( "Task" ) + +--- The STAGE class +-- @type +STAGE = { + ClassName = "STAGE", + MSG = { ID = "None", TIME = 10 }, + FREQUENCY = { NONE = 0, ONCE = 1, REPEAT = -1 }, + + Name = "NoStage", + StageType = '', + WaitTime = 1, + Frequency = 1, + MessageCount = 0, + MessageInterval = 15, + MessageShown = {}, + MessageShow = false, + MessageFlash = false +} + + +function STAGE:New() + local self = BASE:Inherit( self, BASE:New() ) + self:F() + return self +end + +function STAGE:Execute( Mission, Client, Task ) + + local Valid = true + + return Valid +end + +function STAGE:Executing( Mission, Client, Task ) + +end + +function STAGE:Validate( Mission, Client, Task ) + local Valid = true + + return Valid +end + + +STAGEBRIEF = { + ClassName = "BRIEF", + MSG = { ID = "Brief", TIME = 1 }, + Name = "Brief", + StageBriefingTime = 0, + StageBriefingDuration = 1 +} + +function STAGEBRIEF:New() + local self = BASE:Inherit( self, STAGE:New() ) + self:F() + self.StageType = 'CLIENT' + return self +end + +--- Execute +-- @param #STAGEBRIEF self +-- @param Mission#MISSION Mission +-- @param Client#CLIENT Client +-- @param Task#TASK Task +-- @return #boolean +function STAGEBRIEF:Execute( Mission, Client, Task ) + local Valid = BASE:Inherited(self):Execute( Mission, Client, Task ) + self:F() + Client:ShowMissionBriefing( Mission.MissionBriefing ) + self.StageBriefingTime = timer.getTime() + return Valid +end + +function STAGEBRIEF:Validate( Mission, Client, Task ) + local Valid = STAGE:Validate( Mission, Client, Task ) + self:T() + + if timer.getTime() - self.StageBriefingTime <= self.StageBriefingDuration then + return 0 + else + self.StageBriefingTime = timer.getTime() + return 1 + end + +end + + +STAGESTART = { + ClassName = "START", + MSG = { ID = "Start", TIME = 1 }, + Name = "Start", + StageStartTime = 0, + StageStartDuration = 1 +} + +function STAGESTART:New() + local self = BASE:Inherit( self, STAGE:New() ) + self:F() + self.StageType = 'CLIENT' + return self +end + +function STAGESTART:Execute( Mission, Client, Task ) + self:F() + local Valid = BASE:Inherited(self):Execute( Mission, Client, Task ) + if Task.TaskBriefing then + Client:Message( Task.TaskBriefing, 30, Mission.Name .. "/Stage", "Command" ) + else + Client:Message( 'Task ' .. Task.TaskNumber .. '.', 30, Mission.Name .. "/Stage", "Command" ) + end + self.StageStartTime = timer.getTime() + return Valid +end + +function STAGESTART:Validate( Mission, Client, Task ) + self:F() + local Valid = STAGE:Validate( Mission, Client, Task ) + + if timer.getTime() - self.StageStartTime <= self.StageStartDuration then + return 0 + else + self.StageStartTime = timer.getTime() + return 1 + end + + return 1 + +end + +STAGE_CARGO_LOAD = { + ClassName = "STAGE_CARGO_LOAD" +} + +function STAGE_CARGO_LOAD:New() + local self = BASE:Inherit( self, STAGE:New() ) + self:F() + self.StageType = 'CLIENT' + return self +end + +function STAGE_CARGO_LOAD:Execute( Mission, Client, Task ) + self:F() + local Valid = BASE:Inherited(self):Execute( Mission, Client, Task ) + + for LoadCargoID, LoadCargo in pairs( Task.Cargos.LoadCargos ) do + LoadCargo:Load( Client ) + end + + if Mission.MissionReportFlash and Client:IsTransport() then + Client:ShowCargo() + end + + return Valid +end + +function STAGE_CARGO_LOAD:Validate( Mission, Client, Task ) + self:F() + local Valid = STAGE:Validate( Mission, Client, Task ) + + return 1 +end + + +STAGE_CARGO_INIT = { + ClassName = "STAGE_CARGO_INIT" +} + +function STAGE_CARGO_INIT:New() + local self = BASE:Inherit( self, STAGE:New() ) + self:F() + self.StageType = 'CLIENT' + return self +end + +function STAGE_CARGO_INIT:Execute( Mission, Client, Task ) + self:F() + local Valid = BASE:Inherited(self):Execute( Mission, Client, Task ) + + for InitLandingZoneID, InitLandingZone in pairs( Task.LandingZones.LandingZones ) do + self:T( InitLandingZone ) + InitLandingZone:Spawn() + end + + + self:T( Task.Cargos.InitCargos ) + for InitCargoID, InitCargoData in pairs( Task.Cargos.InitCargos ) do + self:T( { InitCargoData } ) + InitCargoData:Spawn( Client ) + end + + return Valid +end + + +function STAGE_CARGO_INIT:Validate( Mission, Client, Task ) + self:F() + local Valid = STAGE:Validate( Mission, Client, Task ) + + return 1 +end + + + +STAGEROUTE = { + ClassName = "STAGEROUTE", + MSG = { ID = "Route", TIME = 5 }, + Frequency = STAGE.FREQUENCY.REPEAT, + Name = "Route" +} + +function STAGEROUTE:New() + local self = BASE:Inherit( self, STAGE:New() ) + self:F() + self.StageType = 'CLIENT' + self.MessageSwitch = true + return self +end + + +--- Execute the routing. +-- @param #STAGEROUTE self +-- @param Mission#MISSION Mission +-- @param Client#CLIENT Client +-- @param Task#TASK Task +function STAGEROUTE:Execute( Mission, Client, Task ) + self:F() + local Valid = BASE:Inherited(self):Execute( Mission, Client, Task ) + + local RouteMessage = "Fly to: " + self:T( Task.LandingZones ) + for LandingZoneID, LandingZoneName in pairs( Task.LandingZones.LandingZoneNames ) do + RouteMessage = RouteMessage .. "\n " .. LandingZoneName .. ' at ' .. routines.getBRStringZone( { zone = LandingZoneName, ref = Client:GetClientGroupDCSUnit():getPoint(), true, true } ) .. ' km.' + end + + if Client:IsMultiSeated() then + Client:Message( RouteMessage, self.MSG.TIME, Mission.Name .. "/StageRoute", "Co-Pilot", 20 ) + else + Client:Message( RouteMessage, self.MSG.TIME, Mission.Name .. "/StageRoute", "Command", 20 ) + end + + + if Mission.MissionReportFlash and Client:IsTransport() then + Client:ShowCargo() + end + + return Valid +end + +function STAGEROUTE:Validate( Mission, Client, Task ) + self:F() + local Valid = STAGE:Validate( Mission, Client, Task ) + + -- check if the Client is in the landing zone + self:T( Task.LandingZones.LandingZoneNames ) + Task.CurrentLandingZoneName = routines.IsUnitNearZonesRadius( Client:GetClientGroupDCSUnit(), Task.LandingZones.LandingZoneNames, 500 ) + + if Task.CurrentLandingZoneName then + + Task.CurrentLandingZone = Task.LandingZones.LandingZones[Task.CurrentLandingZoneName].CargoZone + Task.CurrentCargoZone = Task.LandingZones.LandingZones[Task.CurrentLandingZoneName] + + if Task.CurrentCargoZone then + if not Task.Signalled then + Task.Signalled = Task.CurrentCargoZone:Signal() + end + end + + self:T( 1 ) + return 1 + end + + self:T( 0 ) + return 0 +end + + + +STAGELANDING = { + ClassName = "STAGELANDING", + MSG = { ID = "Landing", TIME = 10 }, + Name = "Landing", + Signalled = false +} + +function STAGELANDING:New() + local self = BASE:Inherit( self, STAGE:New() ) + self:F() + self.StageType = 'CLIENT' + return self +end + +--- Execute the landing coordination. +-- @param #STAGELANDING self +-- @param Mission#MISSION Mission +-- @param Client#CLIENT Client +-- @param Task#TASK Task +function STAGELANDING:Execute( Mission, Client, Task ) + self:F() + + if Client:IsMultiSeated() then + Client:Message( "We have arrived at the landing zone.", self.MSG.TIME, Mission.Name .. "/StageArrived", "Co-Pilot", 10 ) + else + Client:Message( "You have arrived at the landing zone.", self.MSG.TIME, Mission.Name .. "/StageArrived", "Command", 10 ) + end + + Task.HostUnit = Task.CurrentCargoZone:GetHostUnit() + + self:T( { Task.HostUnit } ) + + if Task.HostUnit then + + Task.HostUnitName = Task.HostUnit:GetPrefix() + Task.HostUnitTypeName = Task.HostUnit:GetTypeName() + + local HostMessage = "" + Task.CargoNames = "" + + local IsFirst = true + + for CargoID, Cargo in pairs( CARGOS ) do + if Cargo.CargoType == Task.CargoType then + + if Cargo:IsLandingRequired() then + self:T( "Task for cargo " .. Cargo.CargoType .. " requires landing.") + Task.IsLandingRequired = true + end + + if Cargo:IsSlingLoad() then + self:T( "Task for cargo " .. Cargo.CargoType .. " is a slingload.") + Task.IsSlingLoad = true + end + + if IsFirst then + IsFirst = false + Task.CargoNames = Task.CargoNames .. Cargo.CargoName .. "( " .. Cargo.CargoWeight .. " )" + else + Task.CargoNames = Task.CargoNames .. "; " .. Cargo.CargoName .. "( " .. Cargo.CargoWeight .. " )" + end + end + end + + if Task.IsLandingRequired then + HostMessage = "Land the helicopter to " .. Task.TEXT[1] .. " " .. Task.CargoNames .. "." + else + HostMessage = "Use the Radio menu and F6 to find the cargo, then fly or land near the cargo and " .. Task.TEXT[1] .. " " .. Task.CargoNames .. "." + end + + local Host = "Command" + if Task.HostUnitName then + Host = Task.HostUnitName .. " (" .. Task.HostUnitTypeName .. ")" + else + if Client:IsMultiSeated() then + Host = "Co-Pilot" + end + end + + Client:Message( HostMessage, self.MSG.TIME, Mission.Name .. "/STAGELANDING.EXEC." .. Host, Host, 10 ) + + end +end + +function STAGELANDING:Validate( Mission, Client, Task ) + self:F() + + Task.CurrentLandingZoneName = routines.IsUnitNearZonesRadius( Client:GetClientGroupDCSUnit(), Task.LandingZones.LandingZoneNames, 500 ) + if Task.CurrentLandingZoneName then + + -- Client is in de landing zone. + self:T( Task.CurrentLandingZoneName ) + + Task.CurrentLandingZone = Task.LandingZones.LandingZones[Task.CurrentLandingZoneName].CargoZone + Task.CurrentCargoZone = Task.LandingZones.LandingZones[Task.CurrentLandingZoneName] + + if Task.CurrentCargoZone then + if not Task.Signalled then + Task.Signalled = Task.CurrentCargoZone:Signal() + end + end + else + if Task.CurrentLandingZone then + Task.CurrentLandingZone = nil + end + if Task.CurrentCargoZone then + Task.CurrentCargoZone = nil + end + Task.Signalled = false + Task:RemoveCargoMenus( Client ) + self:T( -1 ) + return -1 + end + + + local DCSUnitVelocityVec3 = Client:GetClientGroupDCSUnit():getVelocity() + local DCSUnitVelocity = ( DCSUnitVelocityVec3.x ^2 + DCSUnitVelocityVec3.y ^2 + DCSUnitVelocityVec3.z ^2 ) ^ 0.5 + + local DCSUnitPointVec3 = Client:GetClientGroupDCSUnit():getPoint() + local LandHeight = land.getHeight( { x = DCSUnitPointVec3.x, y = DCSUnitPointVec3.z } ) + local DCSUnitHeight = DCSUnitPointVec3.y - LandHeight + + self:T( { Task.IsLandingRequired, Client:GetClientGroupDCSUnit():inAir() } ) + if Task.IsLandingRequired and not Client:GetClientGroupDCSUnit():inAir() then + self:T( 1 ) + Task.IsInAirTestRequired = true + return 1 + end + + self:T( { DCSUnitVelocity, DCSUnitHeight, LandHeight, Task.CurrentCargoZone.SignalHeight } ) + if Task.IsLandingRequired and DCSUnitVelocity <= 0.05 and DCSUnitHeight <= Task.CurrentCargoZone.SignalHeight then + self:T( 1 ) + Task.IsInAirTestRequired = false + return 1 + end + + self:T( 0 ) + return 0 +end + +STAGELANDED = { + ClassName = "STAGELANDED", + MSG = { ID = "Land", TIME = 10 }, + Name = "Landed", + MenusAdded = false +} + +function STAGELANDED:New() + local self = BASE:Inherit( self, STAGE:New() ) + self:F() + self.StageType = 'CLIENT' + return self +end + +function STAGELANDED:Execute( Mission, Client, Task ) + self:F() + + if Task.IsLandingRequired then + + local Host = "Command" + if Task.HostUnitName then + Host = Task.HostUnitName .. " (" .. Task.HostUnitTypeName .. ")" + else + if Client:IsMultiSeated() then + Host = "Co-Pilot" + end + end + + Client:Message( 'You have landed within the landing zone. Use the radio menu (F10) to ' .. Task.TEXT[1] .. ' the ' .. Task.CargoType .. '.', + self.MSG.TIME, Mission.Name .. "/STAGELANDED.EXEC" .. Host, Host ) + + if not self.MenusAdded then + Task.Cargo = nil + Task:RemoveCargoMenus( Client ) + Task:AddCargoMenus( Client, CARGOS, 250 ) + end + end +end + + + +function STAGELANDED:Validate( Mission, Client, Task ) + self:F() + + if not routines.IsUnitNearZonesRadius( Client:GetClientGroupDCSUnit(), Task.CurrentLandingZoneName, 500 ) then + self:T( "Client is not anymore in the landing zone, go back to stage Route, and remove cargo menus." ) + Task.Signalled = false + Task:RemoveCargoMenus( Client ) + self:T( -2 ) + return -2 + end + + local DCSUnitVelocityVec3 = Client:GetClientGroupDCSUnit():getVelocity() + local DCSUnitVelocity = ( DCSUnitVelocityVec3.x ^2 + DCSUnitVelocityVec3.y ^2 + DCSUnitVelocityVec3.z ^2 ) ^ 0.5 + + local DCSUnitPointVec3 = Client:GetClientGroupDCSUnit():getPoint() + local LandHeight = land.getHeight( { x = DCSUnitPointVec3.x, y = DCSUnitPointVec3.z } ) + local DCSUnitHeight = DCSUnitPointVec3.y - LandHeight + + self:T( { Task.IsLandingRequired, Client:GetClientGroupDCSUnit():inAir() } ) + if Task.IsLandingRequired and Task.IsInAirTestRequired == true and Client:GetClientGroupDCSUnit():inAir() then + self:T( "Client went back in the air. Go back to stage Landing." ) + self:T( -1 ) + return -1 + end + + self:T( { DCSUnitVelocity, DCSUnitHeight, LandHeight, Task.CurrentCargoZone.SignalHeight } ) + if Task.IsLandingRequired and Task.IsInAirTestRequired == false and DCSUnitVelocity >= 2 and DCSUnitHeight >= Task.CurrentCargoZone.SignalHeight then + self:T( "It seems the Client went back in the air and over the boundary limits. Go back to stage Landing." ) + self:T( -1 ) + return -1 + end + + -- Wait until cargo is selected from the menu. + if Task.IsLandingRequired then + if not Task.Cargo then + self:T( 0 ) + return 0 + end + end + + self:T( 1 ) + return 1 +end + +STAGEUNLOAD = { + ClassName = "STAGEUNLOAD", + MSG = { ID = "Unload", TIME = 10 }, + Name = "Unload" +} + +function STAGEUNLOAD:New() + local self = BASE:Inherit( self, STAGE:New() ) + self:F() + self.StageType = 'CLIENT' + return self +end + +--- Coordinate UnLoading +-- @param #STAGEUNLOAD self +-- @param Mission#MISSION Mission +-- @param Client#CLIENT Client +-- @param Task#TASK Task +function STAGEUNLOAD:Execute( Mission, Client, Task ) + self:F() + + if Client:IsMultiSeated() then + Client:Message( 'The ' .. Task.CargoType .. ' are being ' .. Task.TEXT[2] .. ' within the landing zone. Wait until the helicopter is ' .. Task.TEXT[3] .. '.', + self.MSG.TIME, Mission.Name .. "/StageUnLoad", "Co-Pilot" ) + else + Client:Message( 'You are unloading the ' .. Task.CargoType .. ' ' .. Task.TEXT[2] .. ' within the landing zone. Wait until the helicopter is ' .. Task.TEXT[3] .. '.', + self.MSG.TIME, Mission.Name .. "/StageUnLoad", "Command" ) + end + Task:RemoveCargoMenus( Client ) +end + +function STAGEUNLOAD:Executing( Mission, Client, Task ) + self:F() + env.info( 'STAGEUNLOAD:Executing() Task.Cargo.CargoName = ' .. Task.Cargo.CargoName ) + + local TargetZoneName + + if Task.TargetZoneName then + TargetZoneName = Task.TargetZoneName + else + TargetZoneName = Task.CurrentLandingZoneName + end + + if Task.Cargo:UnLoad( Client, TargetZoneName ) then + Task.ExecuteStage = _TransportExecuteStage.SUCCESS + if Mission.MissionReportFlash then + Client:ShowCargo() + end + end +end + +--- Validate UnLoading +-- @param #STAGEUNLOAD self +-- @param Mission#MISSION Mission +-- @param Client#CLIENT Client +-- @param Task#TASK Task +function STAGEUNLOAD:Validate( Mission, Client, Task ) + self:F() + env.info( 'STAGEUNLOAD:Validate()' ) + + if routines.IsUnitNearZonesRadius( Client:GetClientGroupDCSUnit(), Task.CurrentLandingZoneName, 500 ) then + else + Task.ExecuteStage = _TransportExecuteStage.FAILED + Task:RemoveCargoMenus( Client ) + if Client:IsMultiSeated() then + Client:Message( 'The ' .. Task.CargoType .. " haven't been successfully " .. Task.TEXT[3] .. ' within the landing zone. Task and mission has failed.', + _TransportStageMsgTime.DONE, Mission.Name .. "/StageFailure", "Co-Pilot" ) + else + Client:Message( 'The ' .. Task.CargoType .. " haven't been successfully " .. Task.TEXT[3] .. ' within the landing zone. Task and mission has failed.', + _TransportStageMsgTime.DONE, Mission.Name .. "/StageFailure", "Command" ) + end + return 1 + end + + if not Client:GetClientGroupDCSUnit():inAir() then + else + Task.ExecuteStage = _TransportExecuteStage.FAILED + Task:RemoveCargoMenus( Client ) + if Client:IsMultiSeated() then + Client:Message( 'The ' .. Task.CargoType .. " haven't been successfully " .. Task.TEXT[3] .. ' within the landing zone. Task and mission has failed.', + _TransportStageMsgTime.DONE, Mission.Name .. "/StageFailure", "Co-Pilot" ) + else + Client:Message( 'The ' .. Task.CargoType .. " haven't been successfully " .. Task.TEXT[3] .. ' within the landing zone. Task and mission has failed.', + _TransportStageMsgTime.DONE, Mission.Name .. "/StageFailure", "Command" ) + end + return 1 + end + + if Task.ExecuteStage == _TransportExecuteStage.SUCCESS then + if Client:IsMultiSeated() then + Client:Message( 'The ' .. Task.CargoType .. ' have been sucessfully ' .. Task.TEXT[3] .. ' within the landing zone.', _TransportStageMsgTime.DONE, Mission.Name .. "/Stage", "Co-Pilot" ) + else + Client:Message( 'The ' .. Task.CargoType .. ' have been sucessfully ' .. Task.TEXT[3] .. ' within the landing zone.', _TransportStageMsgTime.DONE, Mission.Name .. "/Stage", "Command" ) + end + Task:RemoveCargoMenus( Client ) + Task.MissionTask:AddGoalCompletion( Task.MissionTask.GoalVerb, Task.CargoName, 1 ) -- We set the cargo as one more goal completed in the mission. + return 1 + end + + return 1 +end + +STAGELOAD = { + ClassName = "STAGELOAD", + MSG = { ID = "Load", TIME = 10 }, + Name = "Load" +} + +function STAGELOAD:New() + local self = BASE:Inherit( self, STAGE:New() ) + self:F() + self.StageType = 'CLIENT' + return self +end + +function STAGELOAD:Execute( Mission, Client, Task ) + self:F() + + if not Task.IsSlingLoad then + + local Host = "Command" + if Task.HostUnitName then + Host = Task.HostUnitName .. " (" .. Task.HostUnitTypeName .. ")" + else + if Client:IsMultiSeated() then + Host = "Co-Pilot" + end + end + + Client:Message( 'The ' .. Task.CargoType .. ' are being ' .. Task.TEXT[2] .. ' within the landing zone. Wait until the helicopter is ' .. Task.TEXT[3] .. '.', + _TransportStageMsgTime.EXECUTING, Mission.Name .. "/STAGELOAD.EXEC." .. Host, Host ) + + -- Route the cargo to the Carrier + + Task.Cargo:OnBoard( Client, Task.CurrentCargoZone, Task.OnBoardSide ) + Task.ExecuteStage = _TransportExecuteStage.EXECUTING + else + Task.ExecuteStage = _TransportExecuteStage.EXECUTING + end +end + +function STAGELOAD:Executing( Mission, Client, Task ) + self:F() + + -- If the Cargo is ready to be loaded, load it into the Client. + + local Host = "Command" + if Task.HostUnitName then + Host = Task.HostUnitName .. " (" .. Task.HostUnitTypeName .. ")" + else + if Client:IsMultiSeated() then + Host = "Co-Pilot" + end + end + + if not Task.IsSlingLoad then + self:T( Task.Cargo.CargoName) + + if Task.Cargo:OnBoarded( Client, Task.CurrentCargoZone ) then + + -- Load the Cargo onto the Client + Task.Cargo:Load( Client ) + + -- Message to the pilot that cargo has been loaded. + Client:Message( "The cargo " .. Task.Cargo.CargoName .. " has been loaded in our helicopter.", + 20, Mission.Name .. "/STAGELANDING.LOADING1." .. Host, Host ) + Task.ExecuteStage = _TransportExecuteStage.SUCCESS + + Client:ShowCargo() + end + else + Client:Message( "Hook the " .. Task.CargoNames .. " onto the helicopter " .. Task.TEXT[3] .. " within the landing zone.", + _TransportStageMsgTime.EXECUTING, Mission.Name .. "/STAGELOAD.LOADING.1." .. Host, Host , 10 ) + for CargoID, Cargo in pairs( CARGOS ) do + self:T( "Cargo.CargoName = " .. Cargo.CargoName ) + + if Cargo:IsSlingLoad() then + local CargoStatic = StaticObject.getByName( Cargo.CargoStaticName ) + if CargoStatic then + self:T( "Cargo is found in the DCS simulator.") + local CargoStaticPosition = CargoStatic:getPosition().p + self:T( "Cargo Position x = " .. CargoStaticPosition.x .. ", y = " .. CargoStaticPosition.y .. ", z = " .. CargoStaticPosition.z ) + local CargoStaticHeight = routines.GetUnitHeight( CargoStatic ) + if CargoStaticHeight > 5 then + self:T( "Cargo is airborne.") + Cargo:StatusLoaded() + Task.Cargo = Cargo + Client:Message( 'The Cargo has been successfully hooked onto the helicopter and is now being sling loaded. Fly outside the landing zone.', + self.MSG.TIME, Mission.Name .. "/STAGELANDING.LOADING.2." .. Host, Host ) + Task.ExecuteStage = _TransportExecuteStage.SUCCESS + break + end + else + self:T( "Cargo not found in the DCS simulator." ) + end + end + end + end + +end + +function STAGELOAD:Validate( Mission, Client, Task ) + self:F() + + self:T( "Task.CurrentLandingZoneName = " .. Task.CurrentLandingZoneName ) + + local Host = "Command" + if Task.HostUnitName then + Host = Task.HostUnitName .. " (" .. Task.HostUnitTypeName .. ")" + else + if Client:IsMultiSeated() then + Host = "Co-Pilot" + end + end + + if not Task.IsSlingLoad then + if not routines.IsUnitNearZonesRadius( Client:GetClientGroupDCSUnit(), Task.CurrentLandingZoneName, 500 ) then + Task:RemoveCargoMenus( Client ) + Task.ExecuteStage = _TransportExecuteStage.FAILED + Task.CargoName = nil + Client:Message( "The " .. Task.CargoType .. " loading has been aborted. You flew outside the pick-up zone while loading. ", + self.MSG.TIME, Mission.Name .. "/STAGELANDING.VALIDATE.1." .. Host, Host ) + self:T( -1 ) + return -1 + end + + local DCSUnitVelocityVec3 = Client:GetClientGroupDCSUnit():getVelocity() + local DCSUnitVelocity = ( DCSUnitVelocityVec3.x ^2 + DCSUnitVelocityVec3.y ^2 + DCSUnitVelocityVec3.z ^2 ) ^ 0.5 + + local DCSUnitPointVec3 = Client:GetClientGroupDCSUnit():getPoint() + local LandHeight = land.getHeight( { x = DCSUnitPointVec3.x, y = DCSUnitPointVec3.z } ) + local DCSUnitHeight = DCSUnitPointVec3.y - LandHeight + + self:T( { Task.IsLandingRequired, Client:GetClientGroupDCSUnit():inAir() } ) + if Task.IsLandingRequired and Task.IsInAirTestRequired == true and Client:GetClientGroupDCSUnit():inAir() then + Task:RemoveCargoMenus( Client ) + Task.ExecuteStage = _TransportExecuteStage.FAILED + Task.CargoName = nil + Client:Message( "The " .. Task.CargoType .. " loading has been aborted. Re-start the " .. Task.TEXT[3] .. " process. Don't fly outside the pick-up zone.", + self.MSG.TIME, Mission.Name .. "/STAGELANDING.VALIDATE.1." .. Host, Host ) + self:T( -1 ) + return -1 + end + + self:T( { DCSUnitVelocity, DCSUnitHeight, LandHeight, Task.CurrentCargoZone.SignalHeight } ) + if Task.IsLandingRequired and Task.IsInAirTestRequired == false and DCSUnitVelocity >= 2 and DCSUnitHeight >= Task.CurrentCargoZone.SignalHeight then + Task:RemoveCargoMenus( Client ) + Task.ExecuteStage = _TransportExecuteStage.FAILED + Task.CargoName = nil + Client:Message( "The " .. Task.CargoType .. " loading has been aborted. Re-start the " .. Task.TEXT[3] .. " process. Don't fly outside the pick-up zone.", + self.MSG.TIME, Mission.Name .. "/STAGELANDING.VALIDATE.1." .. Host, Host ) + self:T( -1 ) + return -1 + end + + if Task.ExecuteStage == _TransportExecuteStage.SUCCESS then + Task:RemoveCargoMenus( Client ) + Client:Message( "Good Job. The " .. Task.CargoType .. " has been sucessfully " .. Task.TEXT[3] .. " within the landing zone.", + self.MSG.TIME, Mission.Name .. "/STAGELANDING.VALIDATE.3." .. Host, Host ) + Task.MissionTask:AddGoalCompletion( Task.MissionTask.GoalVerb, Task.CargoName, 1 ) + self:T( 1 ) + return 1 + end + + else + if Task.ExecuteStage == _TransportExecuteStage.SUCCESS then + CargoStatic = StaticObject.getByName( Task.Cargo.CargoStaticName ) + if CargoStatic and not routines.IsStaticInZones( CargoStatic, Task.CurrentLandingZoneName ) then + Client:Message( "Good Job. The " .. Task.CargoType .. " has been sucessfully " .. Task.TEXT[3] .. " and flown outside of the landing zone.", + self.MSG.TIME, Mission.Name .. "/STAGELANDING.VALIDATE.4." .. Host, Host ) + Task.MissionTask:AddGoalCompletion( Task.MissionTask.GoalVerb, Task.Cargo.CargoName, 1 ) + self:T( 1 ) + return 1 + end + end + + end + + + self:T( 0 ) + return 0 +end + + +STAGEDONE = { + ClassName = "STAGEDONE", + MSG = { ID = "Done", TIME = 10 }, + Name = "Done" +} + +function STAGEDONE:New() + local self = BASE:Inherit( self, STAGE:New() ) + self:F() + self.StageType = 'AI' + return self +end + +function STAGEDONE:Execute( Mission, Client, Task ) + self:F() + +end + +function STAGEDONE:Validate( Mission, Client, Task ) + self:F() + + Task:Done() + + return 0 +end + +STAGEARRIVE = { + ClassName = "STAGEARRIVE", + MSG = { ID = "Arrive", TIME = 10 }, + Name = "Arrive" +} + +function STAGEARRIVE:New() + local self = BASE:Inherit( self, STAGE:New() ) + self:F() + self.StageType = 'CLIENT' + return self +end + + +--- Execute Arrival +-- @param #STAGEARRIVE self +-- @param Mission#MISSION Mission +-- @param Client#CLIENT Client +-- @param Task#TASK Task +function STAGEARRIVE:Execute( Mission, Client, Task ) + self:F() + + if Client:IsMultiSeated() then + Client:Message( 'We have arrived at ' .. Task.CurrentLandingZoneName .. ".", self.MSG.TIME, Mission.Name .. "/Stage", "Co-Pilot" ) + else + Client:Message( 'We have arrived at ' .. Task.CurrentLandingZoneName .. ".", self.MSG.TIME, Mission.Name .. "/Stage", "Command" ) + end + +end + +function STAGEARRIVE:Validate( Mission, Client, Task ) + self:F() + + Task.CurrentLandingZoneID = routines.IsUnitInZones( Client:GetClientGroupDCSUnit(), Task.LandingZones ) + if ( Task.CurrentLandingZoneID ) then + else + return -1 + end + + return 1 +end + +STAGEGROUPSDESTROYED = { + ClassName = "STAGEGROUPSDESTROYED", + DestroyGroupSize = -1, + Frequency = STAGE.FREQUENCY.REPEAT, + MSG = { ID = "DestroyGroup", TIME = 10 }, + Name = "GroupsDestroyed" +} + +function STAGEGROUPSDESTROYED:New() + local self = BASE:Inherit( self, STAGE:New() ) + self:F() + self.StageType = 'AI' + return self +end + +--function STAGEGROUPSDESTROYED:Execute( Mission, Client, Task ) +-- +-- Client:Message( 'Task: Still ' .. DestroyGroupSize .. " of " .. Task.DestroyGroupCount .. " " .. Task.DestroyGroupType .. " to be destroyed!", self.MSG.TIME, Mission.Name .. "/Stage" ) +-- +--end + +function STAGEGROUPSDESTROYED:Validate( Mission, Client, Task ) + self:F() + + if Task.MissionTask:IsGoalReached() then + return 1 + else + return 0 + end +end + +function STAGEGROUPSDESTROYED:Execute( Mission, Client, Task ) + self:F() + self:T( { Task.ClassName, Task.Destroyed } ) + --env.info( 'Event Table Task = ' .. tostring(Task) ) + +end + + + + + + + + + + + + + +--[[ + _TransportStage: Defines the different stages of which of transport missions can be in. This table is internal and is used to control the sequence of messages, actions and flow. + + - _TransportStage.START + - _TransportStage.ROUTE + - _TransportStage.LAND + - _TransportStage.EXECUTE + - _TransportStage.DONE + - _TransportStage.REMOVE +--]] +_TransportStage = { + HOLD = "HOLD", + START = "START", + ROUTE = "ROUTE", + LANDING = "LANDING", + LANDED = "LANDED", + EXECUTING = "EXECUTING", + LOAD = "LOAD", + UNLOAD = "UNLOAD", + DONE = "DONE", + NEXT = "NEXT" +} + +_TransportStageMsgTime = { + HOLD = 10, + START = 60, + ROUTE = 5, + LANDING = 10, + LANDED = 30, + EXECUTING = 30, + LOAD = 30, + UNLOAD = 30, + DONE = 30, + NEXT = 0 +} + +_TransportStageTime = { + HOLD = 10, + START = 5, + ROUTE = 5, + LANDING = 1, + LANDED = 1, + EXECUTING = 5, + LOAD = 5, + UNLOAD = 5, + DONE = 1, + NEXT = 0 +} + +_TransportStageAction = { + REPEAT = -1, + NONE = 0, + ONCE = 1 +} +--- The TASK Classes define major end-to-end activities within a MISSION. The TASK Class is the Master Class to orchestrate these activities. From this class, many concrete TASK classes are inherited. +-- @module TASK + +Include.File( "Routines" ) +Include.File( "Base" ) +Include.File( "Mission" ) +Include.File( "Client" ) +Include.File( "Stage" ) + +--- The TASK class +-- @type TASK +-- @extends Base#BASE +TASK = { + + -- Defines the different signal types with a Task. + SIGNAL = { + COLOR = { + RED = { ID = 1, COLOR = trigger.smokeColor.Red, TEXT = "A red" }, + GREEN = { ID = 2, COLOR = trigger.smokeColor.Green, TEXT = "A green" }, + BLUE = { ID = 3, COLOR = trigger.smokeColor.Blue, TEXT = "A blue" }, + WHITE = { ID = 4, COLOR = trigger.smokeColor.White, TEXT = "A white" }, + ORANGE = { ID = 5, COLOR = trigger.smokeColor.Orange, TEXT = "An orange" } + }, + TYPE = { + SMOKE = { ID = 1, TEXT = "smoke" }, + FLARE = { ID = 2, TEXT = "flare" } + } + }, + ClassName = "TASK", + Mission = {}, -- Owning mission of the Task + Name = '', + Stages = {}, + Stage = {}, + Cargos = { + InitCargos = {}, + LoadCargos = {} + }, + LandingZones = { + LandingZoneNames = {}, + LandingZones = {} + }, + ActiveStage = 0, + TaskDone = false, + TaskFailed = false, + GoalTasks = {} +} + +--- Instantiates a new TASK Base. Should never be used. Interface Class. +-- @return TASK +function TASK:New() + local self = BASE:Inherit( self, BASE:New() ) + self:F() + + -- assign Task default values during construction + self.TaskBriefing = "Task: No Task." + self.Time = timer.getTime() + self.ExecuteStage = _TransportExecuteStage.NONE + + return self +end + +function TASK:SetStage( StageSequenceIncrement ) + self:F( { StageSequenceIncrement } ) + + local Valid = false + if StageSequenceIncrement ~= 0 then + self.ActiveStage = self.ActiveStage + StageSequenceIncrement + if 1 <= self.ActiveStage and self.ActiveStage <= #self.Stages then + self.Stage = self.Stages[self.ActiveStage] + self:T( { self.Stage.Name } ) + self.Frequency = self.Stage.Frequency + Valid = true + else + Valid = false + env.info( "TASK:SetStage() self.ActiveStage is smaller or larger than self.Stages array. self.ActiveStage = " .. self.ActiveStage ) + end + end + self.Time = timer.getTime() + return Valid +end + +function TASK:Init() + self:F() + self.ActiveStage = 0 + self:SetStage(1) + self.TaskDone = false + self.TaskFailed = false +end + + +--- Get progress of a TASK. +-- @return string GoalsText +function TASK:GetGoalProgress() + self:F2() + + local GoalsText = "" + for GoalVerb, GoalVerbData in pairs( self.GoalTasks ) do + local Goals = self:GetGoalCompletion( GoalVerb ) + if Goals and Goals ~= "" then + Goals = '(' .. Goals .. ')' + else + Goals = '( - )' + end + GoalsText = GoalsText .. GoalVerb .. ': ' .. self:GetGoalCount(GoalVerb) .. ' goals ' .. Goals .. ' of ' .. self:GetGoalTotal(GoalVerb) .. ' goals completed (' .. self:GetGoalPercentage(GoalVerb) .. '%); ' + end + + if GoalsText == "" then + GoalsText = "( - )" + end + + return GoalsText +end + +--- Show progress of a TASK. +-- @param MISSION Mission Group structure describing the Mission. +-- @param CLIENT Client Group structure describing the Client. +function TASK:ShowGoalProgress( Mission, Client ) + self:F2() + + local GoalsText = "" + for GoalVerb, GoalVerbData in pairs( self.GoalTasks ) do + if Mission:IsCompleted() then + else + local Goals = self:GetGoalCompletion( GoalVerb ) + if Goals and Goals ~= "" then + else + Goals = "-" + end + GoalsText = GoalsText .. self:GetGoalProgress() + end + end + + if Mission.MissionReportFlash or Mission.MissionReportShow then + Client:Message( GoalsText, 10, "/TASKPROGRESS" .. self.ClassName, "Mission Command: Task Status", 30 ) + end +end + +--- Sets a TASK to status Done. +function TASK:Done() + self:F2() + self.TaskDone = true +end + +--- Returns if a TASK is done. +-- @return bool +function TASK:IsDone() + self:F2( self.TaskDone ) + return self.TaskDone +end + +--- Sets a TASK to status failed. +function TASK:Failed() + self:F() + self.TaskFailed = true +end + +--- Returns if a TASk has failed. +-- @return bool +function TASK:IsFailed() + self:F2( self.TaskFailed ) + return self.TaskFailed +end + +function TASK:Reset( Mission, Client ) + self:F2() + self.ExecuteStage = _TransportExecuteStage.NONE +end + +--- Returns the Goals of a TASK +-- @return @table Goals +function TASK:GetGoals() + return self.GoalTasks +end + +--- Returns if a TASK has Goal(s). +-- @param #TASK self +-- @param #string GoalVerb is the name of the Goal of the TASK. +-- @return bool +function TASK:Goal( GoalVerb ) + self:F2( { GoalVerb } ) + if not GoalVerb then + GoalVerb = self.GoalVerb + end + self:T2( {self.GoalTasks[GoalVerb] } ) + if self.GoalTasks[GoalVerb] and self.GoalTasks[GoalVerb].GoalTotal > 0 then + return true + else + return false + end +end + +--- Sets the total Goals to be achieved of the Goal Name +-- @param number GoalTotal is the number of times the GoalVerb needs to be achieved. +-- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. +function TASK:SetGoalTotal( GoalTotal, GoalVerb ) + self:F2( { GoalTotal, GoalVerb } ) + + if not GoalVerb then + GoalVerb = self.GoalVerb + end + self.GoalTasks[GoalVerb] = {} + self.GoalTasks[GoalVerb].Goals = {} + self.GoalTasks[GoalVerb].GoalTotal = GoalTotal + self.GoalTasks[GoalVerb].GoalCount = 0 + return self +end + +--- Gets the total of Goals to be achieved within the TASK of the GoalVerb. +-- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. +function TASK:GetGoalTotal( GoalVerb ) + self:F2( { GoalVerb } ) + if not GoalVerb then + GoalVerb = self.GoalVerb + end + if self:Goal( GoalVerb ) then + return self.GoalTasks[GoalVerb].GoalTotal + else + return 0 + end +end + +--- Sets the total of Goals currently achieved within the TASK of the GoalVerb. +-- @param number GoalCount is the total number of Goals achieved within the TASK. +-- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. +-- @return TASK +function TASK:SetGoalCount( GoalCount, GoalVerb ) + self:F2() + if not GoalVerb then + GoalVerb = self.GoalVerb + end + if self:Goal( GoalVerb) then + self.GoalTasks[GoalVerb].GoalCount = GoalCount + end + return self +end + +--- Increments the total of Goals currently achieved within the TASK of the GoalVerb, with the given GoalCountIncrease. +-- @param number GoalCountIncrease is the number of new Goals achieved within the TASK. +-- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. +-- @return TASK +function TASK:IncreaseGoalCount( GoalCountIncrease, GoalVerb ) + self:F2( { GoalCountIncrease, GoalVerb } ) + if not GoalVerb then + GoalVerb = self.GoalVerb + end + if self:Goal( GoalVerb) then + self.GoalTasks[GoalVerb].GoalCount = self.GoalTasks[GoalVerb].GoalCount + GoalCountIncrease + end + return self +end + +--- Gets the total of Goals currently achieved within the TASK of the GoalVerb. +-- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. +-- @return TASK +function TASK:GetGoalCount( GoalVerb ) + self:F2() + if not GoalVerb then + GoalVerb = self.GoalVerb + end + if self:Goal( GoalVerb ) then + return self.GoalTasks[GoalVerb].GoalCount + else + return 0 + end +end + +--- Gets the percentage of Goals currently achieved within the TASK of the GoalVerb. +-- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. +-- @return TASK +function TASK:GetGoalPercentage( GoalVerb ) + self:F2() + if not GoalVerb then + GoalVerb = self.GoalVerb + end + if self:Goal( GoalVerb ) then + return math.floor( self:GetGoalCount( GoalVerb ) / self:GetGoalTotal( GoalVerb ) * 100 + .5 ) + else + return 100 + end +end + +--- Returns if all the Goals of the TASK were achieved. +-- @return bool +function TASK:IsGoalReached() + self:F2() + + local GoalReached = true + + for GoalVerb, Goals in pairs( self.GoalTasks ) do + self:T2( { "GoalVerb", GoalVerb } ) + if self:Goal( GoalVerb ) then + local GoalToDo = self:GetGoalTotal( GoalVerb ) - self:GetGoalCount( GoalVerb ) + self:T2( "GoalToDo = " .. GoalToDo ) + if GoalToDo <= 0 then + else + GoalReached = false + break + end + else + break + end + end + + self:T( { GoalReached, self.GoalTasks } ) + return GoalReached +end + +--- Adds an Additional Goal for the TASK to be achieved. +-- @param string GoalVerb is the name of the Goal of the TASK. +-- @param string GoalTask is a text describing the Goal of the TASK to be achieved. +-- @param number GoalIncrease is a number by which the Goal achievement is increasing. +function TASK:AddGoalCompletion( GoalVerb, GoalTask, GoalIncrease ) + self:F2( { GoalVerb, GoalTask, GoalIncrease } ) + + if self:Goal( GoalVerb ) then + self.GoalTasks[GoalVerb].Goals[#self.GoalTasks[GoalVerb].Goals+1] = GoalTask + self.GoalTasks[GoalVerb].GoalCount = self.GoalTasks[GoalVerb].GoalCount + GoalIncrease + end + return self +end + +--- Returns if the additional Goal for the TASK was completed. +-- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. +-- @return string Goals +function TASK:GetGoalCompletion( GoalVerb ) + self:F2( { GoalVerb } ) + + if self:Goal( GoalVerb ) then + local Goals = "" + for GoalID, GoalName in pairs( self.GoalTasks[GoalVerb].Goals ) do Goals = Goals .. GoalName .. " + " end + return Goals:gsub(" + $", ""), self.GoalTasks[GoalVerb].GoalCount + end +end + +function TASK.MenuAction( Parameter ) + Parameter.ReferenceTask.ExecuteStage = _TransportExecuteStage.EXECUTING + Parameter.ReferenceTask.Cargo = Parameter.CargoTask +end + +function TASK:StageExecute() + self:F() + + local Execute = false + + if self.Frequency == STAGE.FREQUENCY.REPEAT then + Execute = true + elseif self.Frequency == STAGE.FREQUENCY.NONE then + Execute = false + elseif self.Frequency >= 0 then + Execute = true + self.Frequency = self.Frequency - 1 + end + + return Execute + +end + +--- Work function to set signal events within a TASK. +function TASK:AddSignal( SignalUnitNames, SignalType, SignalColor, SignalHeight ) + self:F() + + local Valid = true + + if Valid then + if type( SignalUnitNames ) == "table" then + self.LandingZoneSignalUnitNames = SignalUnitNames + else + self.LandingZoneSignalUnitNames = { SignalUnitNames } + end + self.LandingZoneSignalType = SignalType + self.LandingZoneSignalColor = SignalColor + self.Signalled = false + if SignalHeight ~= nil then + self.LandingZoneSignalHeight = SignalHeight + else + self.LandingZoneSignalHeight = 0 + end + + if self.TaskBriefing then + self.TaskBriefing = self.TaskBriefing .. " " .. SignalColor.TEXT .. " " .. SignalType.TEXT .. " will be fired when entering the landing zone." + end + end + + return Valid +end + +--- When the CLIENT is approaching the landing zone, a RED SMOKE will be fired by an optional SignalUnitNames. +-- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. +-- @param number SignalHeight Altitude that the Signal should be fired... +function TASK:AddSmokeRed( SignalUnitNames, SignalHeight ) + self:F() + self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.SMOKE, TASK.SIGNAL.COLOR.RED, SignalHeight ) +end + +--- When the CLIENT is approaching the landing zone, a GREEN SMOKE will be fired by an optional SignalUnitNames. +-- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. +-- @param number SignalHeight Altitude that the Signal should be fired... +function TASK:AddSmokeGreen( SignalUnitNames, SignalHeight ) + self:F() + self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.SMOKE, TASK.SIGNAL.COLOR.GREEN, SignalHeight ) +end + +--- When the CLIENT is approaching the landing zone, a BLUE SMOKE will be fired by an optional SignalUnitNames. +-- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. +-- @param number SignalHeight Altitude that the Signal should be fired... +function TASK:AddSmokeBlue( SignalUnitNames, SignalHeight ) + self:F() + self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.SMOKE, TASK.SIGNAL.COLOR.BLUE, SignalHeight ) +end + +--- When the CLIENT is approaching the landing zone, a WHITE SMOKE will be fired by an optional SignalUnitNames. +-- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. +-- @param number SignalHeight Altitude that the Signal should be fired... +function TASK:AddSmokeWhite( SignalUnitNames, SignalHeight ) + self:F() + self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.SMOKE, TASK.SIGNAL.COLOR.WHITE, SignalHeight ) +end + +--- When the CLIENT is approaching the landing zone, an ORANGE SMOKE will be fired by an optional SignalUnitNames. +-- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. +-- @param number SignalHeight Altitude that the Signal should be fired... +function TASK:AddSmokeOrange( SignalUnitNames, SignalHeight ) + self:F() + self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.SMOKE, TASK.SIGNAL.COLOR.ORANGE, SignalHeight ) +end + +--- When the CLIENT is approaching the landing zone, a RED FLARE will be fired by an optional SignalUnitNames. +-- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. +-- @param number SignalHeight Altitude that the Signal should be fired... +function TASK:AddFlareRed( SignalUnitNames, SignalHeight ) + self:F() + self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.FLARE, TASK.SIGNAL.COLOR.RED, SignalHeight ) +end + +--- When the CLIENT is approaching the landing zone, a GREEN FLARE will be fired by an optional SignalUnitNames. +-- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. +-- @param number SignalHeight Altitude that the Signal should be fired... +function TASK:AddFlareGreen( SignalUnitNames, SignalHeight ) + self:F() + self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.FLARE, TASK.SIGNAL.COLOR.GREEN, SignalHeight ) +end + +--- When the CLIENT is approaching the landing zone, a BLUE FLARE will be fired by an optional SignalUnitNames. +-- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. +-- @param number SignalHeight Altitude that the Signal should be fired... +function TASK:AddFlareBlue( SignalUnitNames, SignalHeight ) + self:F() + self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.FLARE, TASK.SIGNAL.COLOR.BLUE, SignalHeight ) +end + +--- When the CLIENT is approaching the landing zone, a WHITE FLARE will be fired by an optional SignalUnitNames. +-- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. +-- @param number SignalHeight Altitude that the Signal should be fired... +function TASK:AddFlareWhite( SignalUnitNames, SignalHeight ) + self:F() + self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.FLARE, TASK.SIGNAL.COLOR.WHITE, SignalHeight ) +end + +--- When the CLIENT is approaching the landing zone, an ORANGE FLARE will be fired by an optional SignalUnitNames. +-- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. +-- @param number SignalHeight Altitude that the Signal should be fired... +function TASK:AddFlareOrange( SignalUnitNames, SignalHeight ) + self:F() + self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.FLARE, TASK.SIGNAL.COLOR.ORANGE, SignalHeight ) +end +--- A GOHOMETASK orchestrates the travel back to the home base, which is a specific zone defined within the ME. +-- @module GOHOMETASK + +Include.File("Task") + +--- The GOHOMETASK class +-- @type +GOHOMETASK = { + ClassName = "GOHOMETASK", +} + +--- Creates a new GOHOMETASK. +-- @param table{string,...}|string LandingZones Table of Landing Zone names where Home(s) are located. +-- @return GOHOMETASK +function GOHOMETASK:New( LandingZones ) + local self = BASE:Inherit( self, TASK:New() ) + self:F( { LandingZones } ) + local Valid = true + + Valid = routines.ValidateZone( LandingZones, "LandingZones", Valid ) + + if Valid then + self.Name = 'Fly Home' + self.TaskBriefing = "Task: Fly back to your home base. Your co-pilot will provide you with the directions (required flight angle in degrees) and the distance (in km) to your home base." + if type( LandingZones ) == "table" then + self.LandingZones = LandingZones + else + self.LandingZones = { LandingZones } + end + self.Stages = { STAGEBRIEF:New(), STAGESTART:New(), STAGEROUTE:New(), STAGEARRIVE:New(), STAGEDONE:New() } + self.SetStage( self, 1 ) + end + + return self +end +--- A DESTROYBASETASK will monitor the destruction of Groups and Units. This is a BASE class, other classes are derived from this class. +-- @module DESTROYBASETASK +-- @see DESTROYGROUPSTASK +-- @see DESTROYUNITTYPESTASK +-- @see DESTROY_RADARS_TASK + +Include.File("Task") + +--- The DESTROYBASETASK class +-- @type DESTROYBASETASK +DESTROYBASETASK = { + ClassName = "DESTROYBASETASK", + Destroyed = 0, + GoalVerb = "Destroy", + DestroyPercentage = 100, +} + +--- Creates a new DESTROYBASETASK. +-- @param #DESTROYBASETASK self +-- @param #string DestroyGroupType Text describing the group to be destroyed. f.e. "Radar Installations", "Ships", "Vehicles", "Command Centers". +-- @param #string DestroyUnitType Text describing the unit types to be destroyed. f.e. "SA-6", "Row Boats", "Tanks", "Tents". +-- @param #list<#string> DestroyGroupPrefixes Table of Prefixes of the Groups to be destroyed before task is completed. +-- @param #number DestroyPercentage defines the %-tage that needs to be destroyed to achieve mission success. eg. If in the Group there are 10 units, then a value of 75 would require 8 units to be destroyed from the Group to complete the @{TASK}. +-- @return DESTROYBASETASK +function DESTROYBASETASK:New( DestroyGroupType, DestroyUnitType, DestroyGroupPrefixes, DestroyPercentage ) + local self = BASE:Inherit( self, TASK:New() ) + self:F() + + self.Name = 'Destroy' + self.Destroyed = 0 + self.DestroyGroupPrefixes = DestroyGroupPrefixes + self.DestroyGroupType = DestroyGroupType + self.DestroyUnitType = DestroyUnitType + if DestroyPercentage then + self.DestroyPercentage = DestroyPercentage + end + self.TaskBriefing = "Task: Destroy " .. DestroyGroupType .. "." + self.Stages = { STAGEBRIEF:New(), STAGESTART:New(), STAGEGROUPSDESTROYED:New(), STAGEDONE:New() } + self.SetStage( self, 1 ) + + return self +end + +--- Handle the S_EVENT_DEAD events to validate the destruction of units for the task monitoring. +-- @param #DESTROYBASETASK self +-- @param Event#EVENTDATA Event structure of MOOSE. +function DESTROYBASETASK:EventDead( Event ) + self:F( { Event } ) + + if Event.IniDCSUnit then + local DestroyUnit = Event.IniDCSUnit + local DestroyUnitName = Event.IniDCSUnitName + local DestroyGroup = Event.IniDCSGroup + local DestroyGroupName = Event.IniDCSGroupName + + --TODO: I need to fix here if 2 groups in the mission have a similar name with GroupPrefix equal, then i should differentiate for which group the goal was reached! + --I may need to test if for the goalverb that group goal was reached or something. Need to think about it a bit more ... + local UnitsDestroyed = 0 + for DestroyGroupPrefixID, DestroyGroupPrefix in pairs( self.DestroyGroupPrefixes ) do + self:T( DestroyGroupPrefix ) + if string.find( DestroyGroupName, DestroyGroupPrefix, 1, true ) then + self:T( BASE:Inherited(self).ClassName ) + UnitsDestroyed = self:ReportGoalProgress( DestroyGroup, DestroyUnit ) + self:T( UnitsDestroyed ) + end + end + + self:T( { UnitsDestroyed } ) + self:IncreaseGoalCount( UnitsDestroyed, self.GoalVerb ) + end + +end + +--- Validate task completeness of DESTROYBASETASK. +-- @param DestroyGroup Group structure describing the group to be evaluated. +-- @param DestroyUnit Unit structure describing the Unit to be evaluated. +function DESTROYBASETASK:ReportGoalProgress( DestroyGroup, DestroyUnit ) + self:F() + + return 0 +end +--- DESTROYGROUPSTASK +-- @module DESTROYGROUPSTASK + +Include.File("DestroyBaseTask") + +--- The DESTROYGROUPSTASK class +-- @type +DESTROYGROUPSTASK = { + ClassName = "DESTROYGROUPSTASK", + GoalVerb = "Destroy Groups", +} + +--- Creates a new DESTROYGROUPSTASK. +-- @param #DESTROYGROUPSTASK self +-- @param #string DestroyGroupType String describing the group to be destroyed. +-- @param #string DestroyUnitType String describing the unit to be destroyed. +-- @param #list<#string> DestroyGroupNames Table of string containing the name of the groups to be destroyed before task is completed. +-- @param #number DestroyPercentage defines the %-tage that needs to be destroyed to achieve mission success. eg. If in the Group there are 10 units, then a value of 75 would require 8 units to be destroyed from the Group to complete the @{TASK}. +---@return DESTROYGROUPSTASK +function DESTROYGROUPSTASK:New( DestroyGroupType, DestroyUnitType, DestroyGroupNames, DestroyPercentage ) + local self = BASE:Inherit( self, DESTROYBASETASK:New( DestroyGroupType, DestroyUnitType, DestroyGroupNames, DestroyPercentage ) ) + self:F() + + self.Name = 'Destroy Groups' + self.GoalVerb = "Destroy " .. DestroyGroupType + + _EVENTDISPATCHER:OnDead( self.EventDead , self ) + _EVENTDISPATCHER:OnCrash( self.EventDead , self ) + + return self +end + +--- Report Goal Progress. +-- @param #DESTROYGROUPSTASK self +-- @param DCSGroup#Group DestroyGroup Group structure describing the group to be evaluated. +-- @param DCSUnit#Unit DestroyUnit Unit structure describing the Unit to be evaluated. +-- @return #number The DestroyCount reflecting the amount of units destroyed within the group. +function DESTROYGROUPSTASK:ReportGoalProgress( DestroyGroup, DestroyUnit ) + self:F( { DestroyGroup, DestroyUnit, self.DestroyPercentage } ) + + local DestroyGroupSize = DestroyGroup:getSize() - 1 -- When a DEAD event occurs, the getSize is still one larger than the destroyed unit. + local DestroyGroupInitialSize = DestroyGroup:getInitialSize() + self:T( { DestroyGroupSize, DestroyGroupInitialSize - ( DestroyGroupInitialSize * self.DestroyPercentage / 100 ) } ) + + local DestroyCount = 0 + if DestroyGroup then + if DestroyGroupSize <= DestroyGroupInitialSize - ( DestroyGroupInitialSize * self.DestroyPercentage / 100 ) then + DestroyCount = 1 + end + else + DestroyCount = 1 + end + + self:T( DestroyCount ) + + return DestroyCount +end +--- Task class to destroy radar installations. +-- @module DESTROYRADARSTASK + +Include.File("DestroyBaseTask") + +--- The DESTROYRADARS class +-- @type +DESTROYRADARSTASK = { + ClassName = "DESTROYRADARSTASK", + GoalVerb = "Destroy Radars" +} + +--- Creates a new DESTROYRADARSTASK. +-- @param table{string,...} DestroyGroupNames Table of string containing the group names of which the radars are be destroyed. +-- @return DESTROYRADARSTASK +function DESTROYRADARSTASK:New( DestroyGroupNames ) + local self = BASE:Inherit( self, DESTROYGROUPSTASK:New( 'radar installations', 'radars', DestroyGroupNames ) ) + self:F() + + self.Name = 'Destroy Radars' + + _EVENTDISPATCHER:OnDead( self.EventDead , self ) + + return self +end + +--- Report Goal Progress. +-- @param Group DestroyGroup Group structure describing the group to be evaluated. +-- @param Unit DestroyUnit Unit structure describing the Unit to be evaluated. +function DESTROYRADARSTASK:ReportGoalProgress( DestroyGroup, DestroyUnit ) + self:F( { DestroyGroup, DestroyUnit } ) + + local DestroyCount = 0 + if DestroyUnit and DestroyUnit:hasSensors( Unit.SensorType.RADAR, Unit.RadarType.AS ) then + if DestroyUnit and DestroyUnit:getLife() <= 1.0 then + self:T( 'Destroyed a radar' ) + DestroyCount = 1 + end + end + return DestroyCount +end +--- Set TASK to destroy certain unit types. +-- @module DESTROYUNITTYPESTASK + +Include.File("DestroyBaseTask") + +--- The DESTROYUNITTYPESTASK class +-- @type +DESTROYUNITTYPESTASK = { + ClassName = "DESTROYUNITTYPESTASK", + GoalVerb = "Destroy", +} + +--- Creates a new DESTROYUNITTYPESTASK. +-- @param string DestroyGroupType String describing the group to be destroyed. f.e. "Radar Installations", "Fleet", "Batallion", "Command Centers". +-- @param string DestroyUnitType String describing the unit to be destroyed. f.e. "radars", "ships", "tanks", "centers". +-- @param table{string,...} DestroyGroupNames Table of string containing the group names of which the radars are be destroyed. +-- @param string DestroyUnitTypes Table of string containing the type names of the units to achieve mission success. +-- @return DESTROYUNITTYPESTASK +function DESTROYUNITTYPESTASK:New( DestroyGroupType, DestroyUnitType, DestroyGroupNames, DestroyUnitTypes ) + local self = BASE:Inherit( self, DESTROYBASETASK:New( DestroyGroupType, DestroyUnitType, DestroyGroupNames ) ) + self:F( { DestroyGroupType, DestroyUnitType, DestroyGroupNames, DestroyUnitTypes } ) + + if type(DestroyUnitTypes) == 'table' then + self.DestroyUnitTypes = DestroyUnitTypes + else + self.DestroyUnitTypes = { DestroyUnitTypes } + end + + self.Name = 'Destroy Unit Types' + self.GoalVerb = "Destroy " .. DestroyGroupType + + _EVENTDISPATCHER:OnDead( self.EventDead , self ) + + return self +end + +--- Report Goal Progress. +-- @param Group DestroyGroup Group structure describing the group to be evaluated. +-- @param Unit DestroyUnit Unit structure describing the Unit to be evaluated. +function DESTROYUNITTYPESTASK:ReportGoalProgress( DestroyGroup, DestroyUnit ) + self:F( { DestroyGroup, DestroyUnit } ) + + local DestroyCount = 0 + for UnitTypeID, UnitType in pairs( self.DestroyUnitTypes ) do + if DestroyUnit and DestroyUnit:getTypeName() == UnitType then + if DestroyUnit and DestroyUnit:getLife() <= 1.0 then + DestroyCount = DestroyCount + 1 + end + end + end + return DestroyCount +end +--- A PICKUPTASK orchestrates the loading of CARGO at a specific landing zone. +-- @module PICKUPTASK +-- @parent TASK + +Include.File("Task") +Include.File("Cargo") + +--- The PICKUPTASK class +-- @type +PICKUPTASK = { + ClassName = "PICKUPTASK", + TEXT = { "Pick-Up", "picked-up", "loaded" }, + GoalVerb = "Pick-Up" +} + +--- Creates a new PICKUPTASK. +-- @param table{string,...}|string LandingZones Table of Zone names where Cargo is to be loaded. +-- @param CARGO_TYPE CargoType Type of the Cargo. The type must be of the following Enumeration:.. +-- @param number OnBoardSide Reflects from which side the cargo Group will be on-boarded on the Carrier. +function PICKUPTASK:New( CargoType, OnBoardSide ) + local self = BASE:Inherit( self, TASK:New() ) + self:F() + + -- self holds the inherited instance of the PICKUPTASK Class to the BASE class. + + local Valid = true + + if Valid then + self.Name = 'Pickup Cargo' + self.TaskBriefing = "Task: Fly to the indicated landing zones and pickup " .. CargoType .. ". Your co-pilot will provide you with the directions (required flight angle in degrees) and the distance (in km) to the pickup zone." + self.CargoType = CargoType + self.GoalVerb = CargoType .. " " .. self.GoalVerb + self.OnBoardSide = OnBoardSide + self.IsLandingRequired = true -- required to decide whether the client needs to land or not + self.IsSlingLoad = false -- Indicates whether the cargo is a sling load cargo + self.Stages = { STAGE_CARGO_INIT:New(), STAGE_CARGO_LOAD:New(), STAGEBRIEF:New(), STAGESTART:New(), STAGEROUTE:New(), STAGELANDING:New(), STAGELANDED:New(), STAGELOAD:New(), STAGEDONE:New() } + self.SetStage( self, 1 ) + end + + return self +end + +function PICKUPTASK:FromZone( LandingZone ) + self:F() + + self.LandingZones.LandingZoneNames[LandingZone.CargoZoneName] = LandingZone.CargoZoneName + self.LandingZones.LandingZones[LandingZone.CargoZoneName] = LandingZone + + return self +end + +function PICKUPTASK:InitCargo( InitCargos ) + self:F( { InitCargos } ) + + if type( InitCargos ) == "table" then + self.Cargos.InitCargos = InitCargos + else + self.Cargos.InitCargos = { InitCargos } + end + + return self +end + +function PICKUPTASK:LoadCargo( LoadCargos ) + self:F( { LoadCargos } ) + + if type( LoadCargos ) == "table" then + self.Cargos.LoadCargos = LoadCargos + else + self.Cargos.LoadCargos = { LoadCargos } + end + + return self +end + +function PICKUPTASK:AddCargoMenus( Client, Cargos, TransportRadius ) + self:F() + + for CargoID, Cargo in pairs( Cargos ) do + + self:T( { Cargo.ClassName, Cargo.CargoName, Cargo.CargoType, Cargo:IsStatusNone(), Cargo:IsStatusLoaded(), Cargo:IsStatusLoading(), Cargo:IsStatusUnLoaded() } ) + + -- If the Cargo has no status, allow the menu option. + if Cargo:IsStatusNone() or ( Cargo:IsStatusLoading() and Client == Cargo:IsLoadingToClient() ) then + + local MenuAdd = false + if Cargo:IsNear( Client, self.CurrentCargoZone ) then + MenuAdd = true + end + + if MenuAdd then + if Client._Menus[Cargo.CargoType] == nil then + Client._Menus[Cargo.CargoType] = {} + end + + if not Client._Menus[Cargo.CargoType].PickupMenu then + Client._Menus[Cargo.CargoType].PickupMenu = missionCommands.addSubMenuForGroup( + Client:GetClientGroupID(), + self.TEXT[1] .. " " .. Cargo.CargoType, + nil + ) + self:T( 'Added PickupMenu: ' .. self.TEXT[1] .. " " .. Cargo.CargoType ) + end + + if Client._Menus[Cargo.CargoType].PickupSubMenus == nil then + Client._Menus[Cargo.CargoType].PickupSubMenus = {} + end + + Client._Menus[Cargo.CargoType].PickupSubMenus[ #Client._Menus[Cargo.CargoType].PickupSubMenus + 1 ] = missionCommands.addCommandForGroup( + Client:GetClientGroupID(), + Cargo.CargoName .. " ( " .. Cargo.CargoWeight .. "kg )", + Client._Menus[Cargo.CargoType].PickupMenu, + self.MenuAction, + { ReferenceTask = self, CargoTask = Cargo } + ) + self:T( 'Added PickupSubMenu' .. Cargo.CargoType .. ":" .. Cargo.CargoName .. " ( " .. Cargo.CargoWeight .. "kg )" ) + end + end + end + +end + +function PICKUPTASK:RemoveCargoMenus( Client ) + self:F() + + for MenuID, MenuData in pairs( Client._Menus ) do + for SubMenuID, SubMenuData in pairs( MenuData.PickupSubMenus ) do + missionCommands.removeItemForGroup( Client:GetClientGroupID(), SubMenuData ) + self:T( "Removed PickupSubMenu " ) + SubMenuData = nil + end + if MenuData.PickupMenu then + missionCommands.removeItemForGroup( Client:GetClientGroupID(), MenuData.PickupMenu ) + self:T( "Removed PickupMenu " ) + MenuData.PickupMenu = nil + end + end + + for CargoID, Cargo in pairs( CARGOS ) do + self:T( { Cargo.ClassName, Cargo.CargoName, Cargo.CargoType, Cargo:IsStatusNone(), Cargo:IsStatusLoaded(), Cargo:IsStatusLoading(), Cargo:IsStatusUnLoaded() } ) + if Cargo:IsStatusLoading() and Client == Cargo:IsLoadingToClient() then + Cargo:StatusNone() + end + end + +end + + + +function PICKUPTASK:HasFailed( ClientDead ) + self:F() + + local TaskHasFailed = self.TaskFailed + return TaskHasFailed +end + +--- A DEPLOYTASK orchestrates the deployment of CARGO within a specific landing zone. +-- @module DEPLOYTASK + +Include.File( "Task" ) + +--- A DeployTask +-- @type DEPLOYTASK +DEPLOYTASK = { + ClassName = "DEPLOYTASK", + TEXT = { "Deploy", "deployed", "unloaded" }, + GoalVerb = "Deployment" +} + + +--- Creates a new DEPLOYTASK object, which models the sequence of STAGEs to unload a cargo. +-- @function [parent=#DEPLOYTASK] New +-- @param #string CargoType Type of the Cargo. +-- @return #DEPLOYTASK The created DeployTask +function DEPLOYTASK:New( CargoType ) + local self = BASE:Inherit( self, TASK:New() ) + self:F() + + local Valid = true + + if Valid then + self.Name = 'Deploy Cargo' + self.TaskBriefing = "Fly to one of the indicated landing zones and deploy " .. CargoType .. ". Your co-pilot will provide you with the directions (required flight angle in degrees) and the distance (in km) to the deployment zone." + self.CargoType = CargoType + self.GoalVerb = CargoType .. " " .. self.GoalVerb + self.Stages = { STAGE_CARGO_INIT:New(), STAGE_CARGO_LOAD:New(), STAGEBRIEF:New(), STAGESTART:New(), STAGEROUTE:New(), STAGELANDING:New(), STAGELANDED:New(), STAGEUNLOAD:New(), STAGEDONE:New() } + self.SetStage( self, 1 ) + end + + return self +end + +function DEPLOYTASK:ToZone( LandingZone ) + self:F() + + self.LandingZones.LandingZoneNames[LandingZone.CargoZoneName] = LandingZone.CargoZoneName + self.LandingZones.LandingZones[LandingZone.CargoZoneName] = LandingZone + + return self +end + + +function DEPLOYTASK:InitCargo( InitCargos ) + self:F( { InitCargos } ) + + if type( InitCargos ) == "table" then + self.Cargos.InitCargos = InitCargos + else + self.Cargos.InitCargos = { InitCargos } + end + + return self +end + + +function DEPLOYTASK:LoadCargo( LoadCargos ) + self:F( { LoadCargos } ) + + if type( LoadCargos ) == "table" then + self.Cargos.LoadCargos = LoadCargos + else + self.Cargos.LoadCargos = { LoadCargos } + end + + return self +end + + +--- When the cargo is unloaded, it will move to the target zone name. +-- @param string TargetZoneName Name of the Zone to where the Cargo should move after unloading. +function DEPLOYTASK:SetCargoTargetZoneName( TargetZoneName ) + self:F() + + local Valid = true + + Valid = routines.ValidateString( TargetZoneName, "TargetZoneName", Valid ) + + if Valid then + self.TargetZoneName = TargetZoneName + end + + return Valid + +end + +function DEPLOYTASK:AddCargoMenus( Client, Cargos, TransportRadius ) + self:F() + + local ClientGroupID = Client:GetClientGroupID() + + self:T( ClientGroupID ) + + for CargoID, Cargo in pairs( Cargos ) do + + self:T( { Cargo.ClassName, Cargo.CargoName, Cargo.CargoType, Cargo.CargoWeight } ) + + if Cargo:IsStatusLoaded() and Client == Cargo:IsLoadedInClient() then + + if Client._Menus[Cargo.CargoType] == nil then + Client._Menus[Cargo.CargoType] = {} + end + + if not Client._Menus[Cargo.CargoType].DeployMenu then + Client._Menus[Cargo.CargoType].DeployMenu = missionCommands.addSubMenuForGroup( + ClientGroupID, + self.TEXT[1] .. " " .. Cargo.CargoType, + nil + ) + self:T( 'Added DeployMenu ' .. self.TEXT[1] ) + end + + if Client._Menus[Cargo.CargoType].DeploySubMenus == nil then + Client._Menus[Cargo.CargoType].DeploySubMenus = {} + end + + if Client._Menus[Cargo.CargoType].DeployMenu == nil then + self:T( 'deploymenu is nil' ) + end + + Client._Menus[Cargo.CargoType].DeploySubMenus[ #Client._Menus[Cargo.CargoType].DeploySubMenus + 1 ] = missionCommands.addCommandForGroup( + ClientGroupID, + Cargo.CargoName .. " ( " .. Cargo.CargoWeight .. "kg )", + Client._Menus[Cargo.CargoType].DeployMenu, + self.MenuAction, + { ReferenceTask = self, CargoTask = Cargo } + ) + self:T( 'Added DeploySubMenu ' .. Cargo.CargoType .. ":" .. Cargo.CargoName .. " ( " .. Cargo.CargoWeight .. "kg )" ) + end + end + +end + +function DEPLOYTASK:RemoveCargoMenus( Client ) + self:F() + + local ClientGroupID = Client:GetClientGroupID() + self:T( ClientGroupID ) + + for MenuID, MenuData in pairs( Client._Menus ) do + if MenuData.DeploySubMenus ~= nil then + for SubMenuID, SubMenuData in pairs( MenuData.DeploySubMenus ) do + missionCommands.removeItemForGroup( ClientGroupID, SubMenuData ) + self:T( "Removed DeploySubMenu " ) + SubMenuData = nil + end + end + if MenuData.DeployMenu then + missionCommands.removeItemForGroup( ClientGroupID, MenuData.DeployMenu ) + self:T( "Removed DeployMenu " ) + MenuData.DeployMenu = nil + end + end + +end +--- A NOTASK is a dummy activity... But it will show a Mission Briefing... +-- @module NOTASK + +Include.File("Task") + +--- The NOTASK class +-- @type +NOTASK = { + ClassName = "NOTASK", +} + +--- Creates a new NOTASK. +function NOTASK:New() + local self = BASE:Inherit( self, TASK:New() ) + self:F() + + local Valid = true + + if Valid then + self.Name = 'Nothing' + self.TaskBriefing = "Task: Execute your mission." + self.Stages = { STAGEBRIEF:New(), STAGESTART:New(), STAGEDONE:New() } + self.SetStage( self, 1 ) + end + + return self +end +--- A ROUTETASK orchestrates the travel to a specific zone defined within the ME. +-- @module ROUTETASK + +--- The ROUTETASK class +-- @type +ROUTETASK = { + ClassName = "ROUTETASK", + GoalVerb = "Route", +} + +--- Creates a new ROUTETASK. +-- @param table{sring,...}|string LandingZones Table of Zone Names where the target is located. +-- @param string TaskBriefing (optional) Defines a text describing the briefing of the task. +-- @return ROUTETASK +function ROUTETASK:New( LandingZones, TaskBriefing ) + local self = BASE:Inherit( self, TASK:New() ) + self:F( { LandingZones, TaskBriefing } ) + + local Valid = true + + Valid = routines.ValidateZone( LandingZones, "LandingZones", Valid ) + + if Valid then + self.Name = 'Route To Zone' + if TaskBriefing then + self.TaskBriefing = TaskBriefing .. " Your co-pilot will provide you with the directions (required flight angle in degrees) and the distance (in km) to the target objective." + else + self.TaskBriefing = "Task: Fly to specified zone(s). Your co-pilot will provide you with the directions (required flight angle in degrees) and the distance (in km) to the target objective." + end + if type( LandingZones ) == "table" then + self.LandingZones = LandingZones + else + self.LandingZones = { LandingZones } + end + self.Stages = { STAGEBRIEF:New(), STAGESTART:New(), STAGEROUTE:New(), STAGEARRIVE:New(), STAGEDONE:New() } + self.SetStage( self, 1 ) + end + + return self +end + +--- A MISSION is the main owner of a Mission orchestration within MOOSE . The Mission framework orchestrates @{CLIENT}s, @{TASK}s, @{STAGE}s etc. +-- A @{CLIENT} needs to be registered within the @{MISSION} through the function @{AddClient}. A @{TASK} needs to be registered within the @{MISSION} through the function @{AddTask}. +-- @module Mission + +Include.File( "Routines" ) +Include.File( "Base" ) +Include.File( "Client" ) +Include.File( "Task" ) + +--- The MISSION class +-- @type MISSION +-- @extends Base#BASE +-- @field #MISSION.Clients _Clients +-- @field #string MissionBriefing +MISSION = { + ClassName = "MISSION", + Name = "", + MissionStatus = "PENDING", + _Clients = {}, + _Tasks = {}, + _ActiveTasks = {}, + GoalFunction = nil, + MissionReportTrigger = 0, + MissionProgressTrigger = 0, + MissionReportShow = false, + MissionReportFlash = false, + MissionTimeInterval = 0, + MissionCoalition = "", + SUCCESS = 1, + FAILED = 2, + REPEAT = 3, + _GoalTasks = {} +} + +--- @type MISSION.Clients +-- @list + +function MISSION:Meta() + + local self = BASE:Inherit( self, BASE:New() ) + self:F() + + return self +end + +--- This is the main MISSION declaration method. Each Mission is like the master or a Mission orchestration between, Clients, Tasks, Stages etc. +-- @param string MissionName is the name of the mission. This name will be used to reference the status of each mission by the players. +-- @param string MissionPriority is a string indicating the "priority" of the Mission. f.e. "Primary", "Secondary" or "First", "Second". It is free format and up to the Mission designer to choose. There are no rules behind this field. +-- @param string MissionBriefing is a string indicating the mission briefing to be shown when a player joins a @{CLIENT}. +-- @param string MissionCoalition is a string indicating the coalition or party to which this mission belongs to. It is free format and can be chosen freely by the mission designer. Note that this field is not to be confused with the coalition concept of the ME. Examples of a Mission Coalition could be "NATO", "CCCP", "Intruders", "Terrorists"... +-- @return MISSION +-- @usage +-- -- Declare a few missions. +-- local Mission = MISSIONSCHEDULER.AddMission( 'Russia Transport Troops SA-6', 'Operational', 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', 'Russia' ) +-- local Mission = MISSIONSCHEDULER.AddMission( 'Patriots', 'Primary', 'Our intelligence reports that 3 Patriot SAM defense batteries are located near Ruisi, Kvarhiti and Gori.', 'Russia' ) +-- local Mission = MISSIONSCHEDULER.AddMission( 'Package Delivery', 'Operational', 'In order to be in full control of the situation, we need you to deliver a very important package at a secret location. Fly undetected through the NATO defenses and deliver the secret package. The secret agent is located at waypoint 4.', 'Russia' ) +-- local Mission = MISSIONSCHEDULER.AddMission( 'Rescue General', 'Tactical', 'Our intelligence has received a remote signal behind Gori. We believe it is a very important Russian General that was captured by Georgia. Go out there and rescue him! Ensure you stay out of the battle zone, keep south. Waypoint 4 is the location of our Russian General.', 'Russia' ) +-- local Mission = MISSIONSCHEDULER.AddMission( 'NATO Transport Troops', 'Operational', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.', 'NATO' ) +-- local Mission = MISSIONSCHEDULER.AddMission( 'SA-6 SAMs', 'Primary', 'Our intelligence reports that 3 SA-6 SAM defense batteries are located near Didmukha, Khetagurov and Berula. Eliminate the Russian SAMs.', 'NATO' ) +-- local Mission = MISSIONSCHEDULER.AddMission( 'NATO Sling Load', 'Operational', 'Fly to the cargo pickup zone at Dzegvi or Kaspi, and sling the cargo to Soganlug airbase.', 'NATO' ) +-- local Mission = MISSIONSCHEDULER.AddMission( 'Rescue secret agent', 'Tactical', 'In order to be in full control of the situation, we need you to rescue a secret agent from the woods behind enemy lines. Avoid the Russian defenses and rescue the agent. Keep south until Khasuri, and keep your eyes open for any SAM presence. The agent is located at waypoint 4 on your kneeboard.', 'NATO' ) +function MISSION:New( MissionName, MissionPriority, MissionBriefing, MissionCoalition ) + + self = MISSION:Meta() + self:T({ MissionName, MissionPriority, MissionBriefing, MissionCoalition }) + + local Valid = true + + Valid = routines.ValidateString( MissionName, "MissionName", Valid ) + Valid = routines.ValidateString( MissionPriority, "MissionPriority", Valid ) + Valid = routines.ValidateString( MissionBriefing, "MissionBriefing", Valid ) + Valid = routines.ValidateString( MissionCoalition, "MissionCoalition", Valid ) + + if Valid then + self.Name = MissionName + self.MissionPriority = MissionPriority + self.MissionBriefing = MissionBriefing + self.MissionCoalition = MissionCoalition + end + + return self +end + +--- Returns if a Mission has completed. +-- @return bool +function MISSION:IsCompleted() + self:F() + return self.MissionStatus == "ACCOMPLISHED" +end + +--- Set a Mission to completed. +function MISSION:Completed() + self:F() + self.MissionStatus = "ACCOMPLISHED" + self:StatusToClients() +end + +--- Returns if a Mission is ongoing. +-- treturn bool +function MISSION:IsOngoing() + self:F() + return self.MissionStatus == "ONGOING" +end + +--- Set a Mission to ongoing. +function MISSION:Ongoing() + self:F() + self.MissionStatus = "ONGOING" + --self:StatusToClients() +end + +--- Returns if a Mission is pending. +-- treturn bool +function MISSION:IsPending() + self:F() + return self.MissionStatus == "PENDING" +end + +--- Set a Mission to pending. +function MISSION:Pending() + self:F() + self.MissionStatus = "PENDING" + self:StatusToClients() +end + +--- Returns if a Mission has failed. +-- treturn bool +function MISSION:IsFailed() + self:F() + return self.MissionStatus == "FAILED" +end + +--- Set a Mission to failed. +function MISSION:Failed() + self:F() + self.MissionStatus = "FAILED" + self:StatusToClients() +end + +--- Send the status of the MISSION to all Clients. +function MISSION:StatusToClients() + self:F() + if self.MissionReportFlash then + for ClientID, Client in pairs( self._Clients ) do + Client:Message( self.MissionCoalition .. ' "' .. self.Name .. '": ' .. self.MissionStatus .. '! ( ' .. self.MissionPriority .. ' mission ) ', 10, self.Name .. '/Status', "Mission Command: Mission Status") + end + end +end + +--- Handles the reporting. After certain time intervals, a MISSION report MESSAGE will be shown to All Players. +function MISSION:ReportTrigger() + self:F() + + if self.MissionReportShow == true then + self.MissionReportShow = false + return true + else + if self.MissionReportFlash == true then + if timer.getTime() >= self.MissionReportTrigger then + self.MissionReportTrigger = timer.getTime() + self.MissionTimeInterval + return true + else + return false + end + else + return false + end + end +end + +--- Report the status of all MISSIONs to all active Clients. +function MISSION:ReportToAll() + self:F() + + local AlivePlayers = '' + for ClientID, Client in pairs( self._Clients ) do + if Client:GetDCSGroup() then + if Client:GetClientGroupDCSUnit() then + if Client:GetClientGroupDCSUnit():getLife() > 0.0 then + if AlivePlayers == '' then + AlivePlayers = ' Players: ' .. Client:GetClientGroupDCSUnit():getPlayerName() + else + AlivePlayers = AlivePlayers .. ' / ' .. Client:GetClientGroupDCSUnit():getPlayerName() + end + end + end + end + end + local Tasks = self:GetTasks() + local TaskText = "" + for TaskID, TaskData in pairs( Tasks ) do + TaskText = TaskText .. " - Task " .. TaskID .. ": " .. TaskData.Name .. ": " .. TaskData:GetGoalProgress() .. "\n" + end + MESSAGE:New( self.MissionCoalition .. ' "' .. self.Name .. '": ' .. self.MissionStatus .. ' ( ' .. self.MissionPriority .. ' mission )' .. AlivePlayers .. "\n" .. TaskText:gsub("\n$",""), "Mission Command: Mission Report", 10, self.Name .. '/Status'):ToAll() +end + + +--- Add a goal function to a MISSION. Goal functions are called when a @{TASK} within a mission has been completed. +-- @param function GoalFunction is the function defined by the mission designer to evaluate whether a certain goal has been reached after a @{TASK} finishes within the @{MISSION}. A GoalFunction must accept 2 parameters: Mission, Client, which contains the current MISSION object and the current CLIENT object respectively. +-- @usage +-- PatriotActivation = { +-- { "US SAM Patriot Zerti", false }, +-- { "US SAM Patriot Zegduleti", false }, +-- { "US SAM Patriot Gvleti", false } +-- } +-- +-- function DeployPatriotTroopsGoal( Mission, Client ) +-- +-- +-- -- Check if the cargo is all deployed for mission success. +-- for CargoID, CargoData in pairs( Mission._Cargos ) do +-- if Group.getByName( CargoData.CargoGroupName ) then +-- CargoGroup = Group.getByName( CargoData.CargoGroupName ) +-- if CargoGroup then +-- -- Check if the cargo is ready to activate +-- CurrentLandingZoneID = routines.IsUnitInZones( CargoGroup:getUnits()[1], Mission:GetTask( 2 ).LandingZones ) -- The second task is the Deploytask to measure mission success upon +-- if CurrentLandingZoneID then +-- if PatriotActivation[CurrentLandingZoneID][2] == false then +-- -- Now check if this is a new Mission Task to be completed... +-- trigger.action.setGroupAIOn( Group.getByName( PatriotActivation[CurrentLandingZoneID][1] ) ) +-- PatriotActivation[CurrentLandingZoneID][2] = true +-- MessageToBlue( "Mission Command: Message to all airborne units! The " .. PatriotActivation[CurrentLandingZoneID][1] .. " is armed. Our air defenses are now stronger.", 60, "BLUE/PatriotDefense" ) +-- MessageToRed( "Mission Command: Our satellite systems are detecting additional NATO air defenses. To all airborne units: Take care!!!", 60, "RED/PatriotDefense" ) +-- Mission:GetTask( 2 ):AddGoalCompletion( "Patriots activated", PatriotActivation[CurrentLandingZoneID][1], 1 ) -- Register Patriot activation as part of mission goal. +-- end +-- end +-- end +-- end +-- end +-- end +-- +-- local Mission = MISSIONSCHEDULER.AddMission( 'NATO Transport Troops', 'Operational', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.', 'NATO' ) +-- Mission:AddGoalFunction( DeployPatriotTroopsGoal ) +function MISSION:AddGoalFunction( GoalFunction ) + self:F() + self.GoalFunction = GoalFunction +end + +--- Register a new @{CLIENT} to participate within the mission. +-- @param CLIENT Client is the @{CLIENT} object. The object must have been instantiated with @{CLIENT:New}. +-- @return CLIENT +-- @usage +-- Add a number of Client objects to the Mission. +-- Mission:AddClient( CLIENT:FindByName( 'US UH-1H*HOT-Deploy Troops 1', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.' ):Transport() ) +-- Mission:AddClient( CLIENT:FindByName( 'US UH-1H*RAMP-Deploy Troops 3', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.' ):Transport() ) +-- Mission:AddClient( CLIENT:FindByName( 'US UH-1H*HOT-Deploy Troops 2', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.' ):Transport() ) +-- Mission:AddClient( CLIENT:FindByName( 'US UH-1H*RAMP-Deploy Troops 4', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.' ):Transport() ) +function MISSION:AddClient( Client ) + self:F( { Client } ) + + local Valid = true + + if Valid then + self._Clients[Client.ClientName] = Client + end + + return Client +end + +--- Find a @{CLIENT} object within the @{MISSION} by its ClientName. +-- @param CLIENT ClientName is a string defining the Client Group as defined within the ME. +-- @return CLIENT +-- @usage +-- -- Seach for Client "Bomber" within the Mission. +-- local BomberClient = Mission:FindClient( "Bomber" ) +function MISSION:FindClient( ClientName ) + self:F( { self._Clients[ClientName] } ) + return self._Clients[ClientName] +end + + +--- Register a @{TASK} to be completed within the @{MISSION}. Note that there can be multiple @{TASK}s registered to be completed. Each TASK can be set a certain Goal. The MISSION will not be completed until all Goals are reached. +-- @param TASK Task is the @{TASK} object. The object must have been instantiated with @{TASK:New} or any of its inherited @{TASK}s. +-- @param number TaskNumber is the sequence number of the TASK within the MISSION. This number does have to be chronological. +-- @return TASK +-- @usage +-- -- Define a few tasks for the Mission. +-- PickupZones = { "NATO Gold Pickup Zone", "NATO Titan Pickup Zone" } +-- PickupSignalUnits = { "NATO Gold Coordination Center", "NATO Titan Coordination Center" } +-- +-- -- Assign the Pickup Task +-- local PickupTask = PICKUPTASK:New( PickupZones, CARGO_TYPE.ENGINEERS, CLIENT.ONBOARDSIDE.LEFT ) +-- PickupTask:AddSmokeBlue( PickupSignalUnits ) +-- PickupTask:SetGoalTotal( 3 ) +-- Mission:AddTask( PickupTask, 1 ) +-- +-- -- Assign the Deploy Task +-- local PatriotActivationZones = { "US Patriot Battery 1 Activation", "US Patriot Battery 2 Activation", "US Patriot Battery 3 Activation" } +-- local PatriotActivationZonesSmokeUnits = { "US SAM Patriot - Battery 1 Control", "US SAM Patriot - Battery 2 Control", "US SAM Patriot - Battery 3 Control" } +-- local DeployTask = DEPLOYTASK:New( PatriotActivationZones, CARGO_TYPE.ENGINEERS ) +-- --DeployTask:SetCargoTargetZoneName( 'US Troops Attack ' .. math.random(2) ) +-- DeployTask:AddSmokeBlue( PatriotActivationZonesSmokeUnits ) +-- DeployTask:SetGoalTotal( 3 ) +-- DeployTask:SetGoalTotal( 3, "Patriots activated" ) +-- Mission:AddTask( DeployTask, 2 ) + +function MISSION:AddTask( Task, TaskNumber ) + self:F() + + self._Tasks[TaskNumber] = Task + self._Tasks[TaskNumber]:EnableEvents() + self._Tasks[TaskNumber].ID = TaskNumber + + return Task + end + +--- Get the TASK idenified by the TaskNumber from the Mission. This function is useful in GoalFunctions. +-- @param number TaskNumber is the number of the @{TASK} within the @{MISSION}. +-- @return TASK +-- @usage +-- -- Get Task 2 from the Mission. +-- Task2 = Mission:GetTask( 2 ) + +function MISSION:GetTask( TaskNumber ) + self:F() + + local Valid = true + + local Task = nil + + if type(TaskNumber) ~= "number" then + Valid = false + end + + if Valid then + Task = self._Tasks[TaskNumber] + end + + return Task +end + +--- Get all the TASKs from the Mission. This function is useful in GoalFunctions. +-- @return {TASK,...} Structure of TASKS with the @{TASK} number as the key. +-- @usage +-- -- Get Tasks from the Mission. +-- Tasks = Mission:GetTasks() +-- env.info( "Task 2 Completion = " .. Tasks[2]:GetGoalPercentage() .. "%" ) +function MISSION:GetTasks() + self:F() + + return self._Tasks +end + + +--[[ + _TransportExecuteStage: Defines the different stages of Transport unload/load execution. This table is internal and is used to control the validity of Transport load/unload timing. + + - _TransportExecuteStage.EXECUTING + - _TransportExecuteStage.SUCCESS + - _TransportExecuteStage.FAILED + +--]] +_TransportExecuteStage = { + NONE = 0, + EXECUTING = 1, + SUCCESS = 2, + FAILED = 3 +} + + +--- The MISSIONSCHEDULER is an OBJECT and is the main scheduler of ALL active MISSIONs registered within this scheduler. It's workings are considered internal and is automatically created when the Mission.lua file is included. +-- @type MISSIONSCHEDULER +-- @field #MISSIONSCHEDULER.MISSIONS Missions +MISSIONSCHEDULER = { + Missions = {}, + MissionCount = 0, + TimeIntervalCount = 0, + TimeIntervalShow = 150, + TimeSeconds = 14400, + TimeShow = 5 +} + +--- @type MISSIONSCHEDULER.MISSIONS +-- @list <#MISSION> Mission + +--- This is the main MISSIONSCHEDULER Scheduler function. It is considered internal and is automatically created when the Mission.lua file is included. +function MISSIONSCHEDULER.Scheduler() + + + -- loop through the missions in the TransportTasks + for MissionName, MissionData in pairs( MISSIONSCHEDULER.Missions ) do + + local Mission = MissionData -- #MISSION + + if not Mission:IsCompleted() then + + -- This flag will monitor if for this mission, there are clients alive. If this flag is still false at the end of the loop, the mission status will be set to Pending (if not Failed or Completed). + local ClientsAlive = false + + for ClientID, ClientData in pairs( Mission._Clients ) do + + local Client = ClientData -- Client#CLIENT + + if Client:IsAlive() then + + -- There is at least one Client that is alive... So the Mission status is set to Ongoing. + ClientsAlive = true + + -- If this Client was not registered as Alive before: + -- 1. We register the Client as Alive. + -- 2. We initialize the Client Tasks and make a link to the original Mission Task. + -- 3. We initialize the Cargos. + -- 4. We flag the Mission as Ongoing. + if not Client.ClientAlive then + Client.ClientAlive = true + Client.ClientBriefingShown = false + for TaskNumber, Task in pairs( Mission._Tasks ) do + -- Note that this a deepCopy. Each client must have their own Tasks with own Stages!!! + Client._Tasks[TaskNumber] = routines.utils.deepCopy( Mission._Tasks[TaskNumber] ) + -- Each MissionTask must point to the original Mission. + Client._Tasks[TaskNumber].MissionTask = Mission._Tasks[TaskNumber] + Client._Tasks[TaskNumber].Cargos = Mission._Tasks[TaskNumber].Cargos + Client._Tasks[TaskNumber].LandingZones = Mission._Tasks[TaskNumber].LandingZones + end + + Mission:Ongoing() + end + + + -- For each Client, check for each Task the state and evolve the mission. + -- This flag will indicate if the Task of the Client is Complete. + local TaskComplete = false + + for TaskNumber, Task in pairs( Client._Tasks ) do + + if not Task.Stage then + Task:SetStage( 1 ) + end + + + local TransportTime = timer.getTime() + + if not Task:IsDone() then + + if Task:Goal() then + Task:ShowGoalProgress( Mission, Client ) + end + + --env.info( 'Scheduler: Mission = ' .. Mission.Name .. ' / Client = ' .. Client.ClientName .. ' / Task = ' .. Task.Name .. ' / Stage = ' .. Task.ActiveStage .. ' - ' .. Task.Stage.Name .. ' - ' .. Task.Stage.StageType ) + + -- Action + if Task:StageExecute() then + Task.Stage:Execute( Mission, Client, Task ) + end + + -- Wait until execution is finished + if Task.ExecuteStage == _TransportExecuteStage.EXECUTING then + Task.Stage:Executing( Mission, Client, Task ) + end + + -- Validate completion or reverse to earlier stage + if Task.Time + Task.Stage.WaitTime <= TransportTime then + Task:SetStage( Task.Stage:Validate( Mission, Client, Task ) ) + end + + if Task:IsDone() then + --env.info( 'Scheduler: Mission '.. Mission.Name .. ' Task ' .. Task.Name .. ' Stage ' .. Task.Stage.Name .. ' done. TaskComplete = ' .. string.format ( "%s", TaskComplete and "true" or "false" ) ) + TaskComplete = true -- when a task is not yet completed, a mission cannot be completed + + else + -- break only if this task is not yet done, so that future task are not yet activated. + TaskComplete = false -- when a task is not yet completed, a mission cannot be completed + --env.info( 'Scheduler: Mission "'.. Mission.Name .. '" Task "' .. Task.Name .. '" Stage "' .. Task.Stage.Name .. '" break. TaskComplete = ' .. string.format ( "%s", TaskComplete and "true" or "false" ) ) + break + end + + if TaskComplete then + + if Mission.GoalFunction ~= nil then + Mission.GoalFunction( Mission, Client ) + end + if MISSIONSCHEDULER.Scoring then + MISSIONSCHEDULER.Scoring:_AddMissionTaskScore( Client:GetClientGroupDCSUnit(), Mission.Name, 25 ) + end + +-- if not Mission:IsCompleted() then +-- end + end + end + end + + local MissionComplete = true + for TaskNumber, Task in pairs( Mission._Tasks ) do + if Task:Goal() then +-- Task:ShowGoalProgress( Mission, Client ) + if Task:IsGoalReached() then + else + MissionComplete = false + end + else + MissionComplete = false -- If there is no goal, the mission should never be ended. The goal status will be set somewhere else. + end + end + + if MissionComplete then + Mission:Completed() + if MISSIONSCHEDULER.Scoring then + MISSIONSCHEDULER.Scoring:_AddMissionScore( Mission.Name, 100 ) + end + else + if TaskComplete then + -- Reset for new tasking of active client + Client.ClientAlive = false -- Reset the client tasks. + end + end + + + else + if Client.ClientAlive then + env.info( 'Scheduler: Client "' .. Client.ClientName .. '" is inactive.' ) + Client.ClientAlive = false + + -- This is tricky. If we sanitize Client._Tasks before sanitizing Client._Tasks[TaskNumber].MissionTask, then the original MissionTask will be sanitized, and will be lost within the garbage collector. + -- So first sanitize Client._Tasks[TaskNumber].MissionTask, after that, sanitize only the whole _Tasks structure... + --Client._Tasks[TaskNumber].MissionTask = nil + --Client._Tasks = nil + end + end + end + + -- If all Clients of this Mission are not activated, then the Mission status needs to be put back into Pending status. + -- But only if the Mission was Ongoing. In case the Mission is Completed or Failed, the Mission status may not be changed. In these cases, this will be the last run of this Mission in the Scheduler. + if ClientsAlive == false then + if Mission:IsOngoing() then + -- Mission status back to pending... + Mission:Pending() + end + end + end + + Mission:StatusToClients() + + if Mission:ReportTrigger() then + Mission:ReportToAll() + end + end + + return true +end + +--- Start the MISSIONSCHEDULER. +function MISSIONSCHEDULER.Start() + if MISSIONSCHEDULER ~= nil then + --MISSIONSCHEDULER.SchedulerId = routines.scheduleFunction( MISSIONSCHEDULER.Scheduler, { }, 0, 2 ) + MISSIONSCHEDULER.SchedulerId = SCHEDULER:New( nil, MISSIONSCHEDULER.Scheduler, { }, 0, 2 ) + end +end + +--- Stop the MISSIONSCHEDULER. +function MISSIONSCHEDULER.Stop() + if MISSIONSCHEDULER.SchedulerId then + routines.removeFunction(MISSIONSCHEDULER.SchedulerId) + MISSIONSCHEDULER.SchedulerId = nil + end +end + +--- This is the main MISSION declaration method. Each Mission is like the master or a Mission orchestration between, Clients, Tasks, Stages etc. +-- @param Mission is the MISSION object instantiated by @{MISSION:New}. +-- @return MISSION +-- @usage +-- -- Declare a mission. +-- Mission = MISSION:New( 'Russia Transport Troops SA-6', +-- 'Operational', +-- 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', +-- 'Russia' ) +-- MISSIONSCHEDULER:AddMission( Mission ) +function MISSIONSCHEDULER.AddMission( Mission ) + MISSIONSCHEDULER.Missions[Mission.Name] = Mission + MISSIONSCHEDULER.MissionCount = MISSIONSCHEDULER.MissionCount + 1 + -- Add an overall AI Client for the AI tasks... This AI Client will facilitate the Events in the background for each Task. + --MissionAdd:AddClient( CLIENT:Register( 'AI' ) ) + + return Mission +end + +--- Remove a MISSION from the MISSIONSCHEDULER. +-- @param MissionName is the name of the MISSION given at declaration using @{AddMission}. +-- @usage +-- -- Declare a mission. +-- Mission = MISSION:New( 'Russia Transport Troops SA-6', +-- 'Operational', +-- 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', +-- 'Russia' ) +-- MISSIONSCHEDULER:AddMission( Mission ) +-- +-- -- Now remove the Mission. +-- MISSIONSCHEDULER:RemoveMission( 'Russia Transport Troops SA-6' ) +function MISSIONSCHEDULER.RemoveMission( MissionName ) + MISSIONSCHEDULER.Missions[MissionName] = nil + MISSIONSCHEDULER.MissionCount = MISSIONSCHEDULER.MissionCount - 1 +end + +--- Find a MISSION within the MISSIONSCHEDULER. +-- @param MissionName is the name of the MISSION given at declaration using @{AddMission}. +-- @return MISSION +-- @usage +-- -- Declare a mission. +-- Mission = MISSION:New( 'Russia Transport Troops SA-6', +-- 'Operational', +-- 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', +-- 'Russia' ) +-- MISSIONSCHEDULER:AddMission( Mission ) +-- +-- -- Now find the Mission. +-- MissionFind = MISSIONSCHEDULER:FindMission( 'Russia Transport Troops SA-6' ) +function MISSIONSCHEDULER.FindMission( MissionName ) + return MISSIONSCHEDULER.Missions[MissionName] +end + +-- Internal function used by the MISSIONSCHEDULER menu. +function MISSIONSCHEDULER.ReportMissionsShow( ) + for MissionName, Mission in pairs( MISSIONSCHEDULER.Missions ) do + Mission.MissionReportShow = true + Mission.MissionReportFlash = false + end +end + +-- Internal function used by the MISSIONSCHEDULER menu. +function MISSIONSCHEDULER.ReportMissionsFlash( TimeInterval ) + local Count = 0 + for MissionName, Mission in pairs( MISSIONSCHEDULER.Missions ) do + Mission.MissionReportShow = false + Mission.MissionReportFlash = true + Mission.MissionReportTrigger = timer.getTime() + Count * TimeInterval + Mission.MissionTimeInterval = MISSIONSCHEDULER.MissionCount * TimeInterval + env.info( "TimeInterval = " .. Mission.MissionTimeInterval ) + Count = Count + 1 + end +end + +-- Internal function used by the MISSIONSCHEDULER menu. +function MISSIONSCHEDULER.ReportMissionsHide( Prm ) + for MissionName, Mission in pairs( MISSIONSCHEDULER.Missions ) do + Mission.MissionReportShow = false + Mission.MissionReportFlash = false + end +end + +--- Enables a MENU option in the communications menu under F10 to control the status of the active missions. +-- This function should be called only once when starting the MISSIONSCHEDULER. +function MISSIONSCHEDULER.ReportMenu() + local ReportMenu = SUBMENU:New( 'Status' ) + local ReportMenuShow = COMMANDMENU:New( 'Show Report Missions', ReportMenu, MISSIONSCHEDULER.ReportMissionsShow, 0 ) + local ReportMenuFlash = COMMANDMENU:New('Flash Report Missions', ReportMenu, MISSIONSCHEDULER.ReportMissionsFlash, 120 ) + local ReportMenuHide = COMMANDMENU:New( 'Hide Report Missions', ReportMenu, MISSIONSCHEDULER.ReportMissionsHide, 0 ) +end + +--- Show the remaining mission time. +function MISSIONSCHEDULER:TimeShow() + self.TimeIntervalCount = self.TimeIntervalCount + 1 + if self.TimeIntervalCount >= self.TimeTriggerShow then + local TimeMsg = string.format("%00d", ( self.TimeSeconds / 60 ) - ( timer.getTime() / 60 )) .. ' minutes left until mission reload.' + MESSAGE:New( TimeMsg, "Mission time", self.TimeShow, '/TimeMsg' ):ToAll() + self.TimeIntervalCount = 0 + end +end + +function MISSIONSCHEDULER:Time( TimeSeconds, TimeIntervalShow, TimeShow ) + + self.TimeIntervalCount = 0 + self.TimeSeconds = TimeSeconds + self.TimeIntervalShow = TimeIntervalShow + self.TimeShow = TimeShow +end + +--- Adds a mission scoring to the game. +function MISSIONSCHEDULER:Scoring( Scoring ) + + self.Scoring = Scoring +end + +--- The CLEANUP class keeps an area clean of crashing or colliding airplanes. It also prevents airplanes from firing within this area. +-- @module CleanUp +-- @author Flightcontrol + +Include.File( "Routines" ) +Include.File( "Base" ) +Include.File( "Mission" ) +Include.File( "Client" ) +Include.File( "Task" ) + +--- The CLEANUP class. +-- @type CLEANUP +-- @extends Base#BASE +CLEANUP = { + ClassName = "CLEANUP", + ZoneNames = {}, + TimeInterval = 300, + CleanUpList = {}, +} + +--- Creates the main object which is handling the cleaning of the debris within the given Zone Names. +-- @param #CLEANUP self +-- @param #table ZoneNames Is a table of zone names where the debris should be cleaned. Also a single string can be passed with one zone name. +-- @param #number TimeInterval The interval in seconds when the clean activity takes place. The default is 300 seconds, thus every 5 minutes. +-- @return #CLEANUP +-- @usage +-- -- Clean these Zones. +-- CleanUpAirports = CLEANUP:New( { 'CLEAN Tbilisi', 'CLEAN Kutaisi' }, 150 ) +-- or +-- CleanUpTbilisi = CLEANUP:New( 'CLEAN Tbilisi', 150 ) +-- CleanUpKutaisi = CLEANUP:New( 'CLEAN Kutaisi', 600 ) +function CLEANUP:New( ZoneNames, TimeInterval ) local self = BASE:Inherit( self, BASE:New() ) + self:F( { ZoneNames, TimeInterval } ) + + if type( ZoneNames ) == 'table' then + self.ZoneNames = ZoneNames + else + self.ZoneNames = { ZoneNames } + end + if TimeInterval then + self.TimeInterval = TimeInterval + end + + _EVENTDISPATCHER:OnBirth( self._OnEventBirth, self ) + + --self.CleanUpScheduler = routines.scheduleFunction( self._CleanUpScheduler, { self }, timer.getTime() + 1, TimeInterval ) + self.CleanUpScheduler = SCHEDULER:New( self, self._CleanUpScheduler, {}, 1, TimeInterval ) + + return self +end + + +--- Destroys a group from the simulator, but checks first if it is still existing! +-- @param #CLEANUP self +-- @param DCSGroup#Group GroupObject The object to be destroyed. +-- @param #string CleanUpGroupName The groupname... +function CLEANUP:_DestroyGroup( GroupObject, CleanUpGroupName ) + self:F( { GroupObject, CleanUpGroupName } ) + + if GroupObject then -- and GroupObject:isExist() then + --MESSAGE:New( "Destroy Group " .. CleanUpGroupName, CleanUpGroupName, 1, CleanUpGroupName ):ToAll() + trigger.action.deactivateGroup(GroupObject) + self:T( { "GroupObject Destroyed", GroupObject } ) + end +end + +--- Destroys a @{DCSUnit#Unit} from the simulator, but checks first if it is still existing! +-- @param #CLEANUP self +-- @param DCSUnit#Unit CleanUpUnit The object to be destroyed. +-- @param #string CleanUpUnitName The Unit name ... +function CLEANUP:_DestroyUnit( CleanUpUnit, CleanUpUnitName ) + self:F( { CleanUpUnit, CleanUpUnitName } ) + + if CleanUpUnit then + --MESSAGE:New( "Destroy " .. CleanUpUnitName, CleanUpUnitName, 1, CleanUpUnitName ):ToAll() + local CleanUpGroup = Unit.getGroup(CleanUpUnit) + -- TODO Client bug in 1.5.3 + if CleanUpGroup and CleanUpGroup:isExist() then + local CleanUpGroupUnits = CleanUpGroup:getUnits() + if #CleanUpGroupUnits == 1 then + local CleanUpGroupName = CleanUpGroup:getName() + --self:CreateEventCrash( timer.getTime(), CleanUpUnit ) + CleanUpGroup:destroy() + self:T( { "Destroyed Group:", CleanUpGroupName } ) + else + CleanUpUnit:destroy() + self:T( { "Destroyed Unit:", CleanUpUnitName } ) + end + self.CleanUpList[CleanUpUnitName] = nil -- Cleaning from the list + CleanUpUnit = nil + end + end +end + +-- TODO check DCSTypes#Weapon +--- Destroys a missile from the simulator, but checks first if it is still existing! +-- @param #CLEANUP self +-- @param DCSTypes#Weapon MissileObject +function CLEANUP:_DestroyMissile( MissileObject ) + self:F( { MissileObject } ) + + if MissileObject and MissileObject:isExist() then + MissileObject:destroy() + self:T( "MissileObject Destroyed") + end +end + +function CLEANUP:_OnEventBirth( Event ) + self:F( { Event } ) + + self.CleanUpList[Event.IniDCSUnitName] = {} + self.CleanUpList[Event.IniDCSUnitName].CleanUpUnit = Event.IniDCSUnit + self.CleanUpList[Event.IniDCSUnitName].CleanUpGroup = Event.IniDCSGroup + self.CleanUpList[Event.IniDCSUnitName].CleanUpGroupName = Event.IniDCSGroupName + self.CleanUpList[Event.IniDCSUnitName].CleanUpUnitName = Event.IniDCSUnitName + + _EVENTDISPATCHER:OnEngineShutDownForUnit( Event.IniDCSUnitName, self._EventAddForCleanUp, self ) + _EVENTDISPATCHER:OnEngineStartUpForUnit( Event.IniDCSUnitName, self._EventAddForCleanUp, self ) + _EVENTDISPATCHER:OnHitForUnit( Event.IniDCSUnitName, self._EventAddForCleanUp, self ) + _EVENTDISPATCHER:OnPilotDeadForUnit( Event.IniDCSUnitName, self._EventCrash, self ) + _EVENTDISPATCHER:OnDeadForUnit( Event.IniDCSUnitName, self._EventCrash, self ) + _EVENTDISPATCHER:OnCrashForUnit( Event.IniDCSUnitName, self._EventCrash, self ) + _EVENTDISPATCHER:OnShotForUnit( Event.IniDCSUnitName, self._EventShot, self ) + + --self:AddEvent( world.event.S_EVENT_ENGINE_SHUTDOWN, self._EventAddForCleanUp ) + --self:AddEvent( world.event.S_EVENT_ENGINE_STARTUP, self._EventAddForCleanUp ) +-- self:AddEvent( world.event.S_EVENT_HIT, self._EventAddForCleanUp ) -- , self._EventHitCleanUp ) +-- self:AddEvent( world.event.S_EVENT_CRASH, self._EventCrash ) -- , self._EventHitCleanUp ) +-- --self:AddEvent( world.event.S_EVENT_DEAD, self._EventCrash ) +-- self:AddEvent( world.event.S_EVENT_SHOT, self._EventShot ) +-- +-- self:EnableEvents() + + +end + +--- Detects if a crash event occurs. +-- Crashed units go into a CleanUpList for removal. +-- @param #CLEANUP self +-- @param DCSTypes#Event event +function CLEANUP:_EventCrash( Event ) + self:F( { Event } ) + + --TODO: This stuff is not working due to a DCS bug. Burning units cannot be destroyed. + --MESSAGE:New( "Crash ", "Crash", 10, "Crash" ):ToAll() + -- self:T("before getGroup") + -- local _grp = Unit.getGroup(event.initiator)-- Identify the group that fired + -- self:T("after getGroup") + -- _grp:destroy() + -- self:T("after deactivateGroup") + -- event.initiator:destroy() + + self.CleanUpList[Event.IniDCSUnitName] = {} + self.CleanUpList[Event.IniDCSUnitName].CleanUpUnit = Event.IniDCSUnit + self.CleanUpList[Event.IniDCSUnitName].CleanUpGroup = Event.IniDCSGroup + self.CleanUpList[Event.IniDCSUnitName].CleanUpGroupName = Event.IniDCSGroupName + self.CleanUpList[Event.IniDCSUnitName].CleanUpUnitName = Event.IniDCSUnitName + +end + +--- Detects if a unit shoots a missile. +-- If this occurs within one of the zones, then the weapon used must be destroyed. +-- @param #CLEANUP self +-- @param DCSTypes#Event event +function CLEANUP:_EventShot( Event ) + self:F( { Event } ) + + -- Test if the missile was fired within one of the CLEANUP.ZoneNames. + local CurrentLandingZoneID = 0 + CurrentLandingZoneID = routines.IsUnitInZones( Event.IniDCSUnit, self.ZoneNames ) + if ( CurrentLandingZoneID ) then + -- Okay, the missile was fired within the CLEANUP.ZoneNames, destroy the fired weapon. + --_SEADmissile:destroy() + --routines.scheduleFunction( CLEANUP._DestroyMissile, { self, Event.Weapon }, timer.getTime() + 0.1) + SCHEDULER:New( self, CLEANUP._DestroyMissile, { Event.Weapon }, 0.1 ) + end +end + + +--- Detects if the Unit has an S_EVENT_HIT within the given ZoneNames. If this is the case, destroy the unit. +-- @param #CLEANUP self +-- @param DCSTypes#Event event +function CLEANUP:_EventHitCleanUp( Event ) + self:F( { Event } ) + + if Event.IniDCSUnit then + if routines.IsUnitInZones( Event.IniDCSUnit, self.ZoneNames ) ~= nil then + self:T( { "Life: ", Event.IniDCSUnitName, ' = ', Event.IniDCSUnit:getLife(), "/", Event.IniDCSUnit:getLife0() } ) + if Event.IniDCSUnit:getLife() < Event.IniDCSUnit:getLife0() then + self:T( "CleanUp: Destroy: " .. Event.IniDCSUnitName ) + --routines.scheduleFunction( CLEANUP._DestroyUnit, { self, Event.IniDCSUnit }, timer.getTime() + 0.1) + SCHEDULER:New( self, CLEANUP._DestroyUnit, { Event.IniDCSUnit }, 0.1 ) + end + end + end + + if Event.TgtDCSUnit then + if routines.IsUnitInZones( Event.TgtDCSUnit, self.ZoneNames ) ~= nil then + self:T( { "Life: ", Event.TgtDCSUnitName, ' = ', Event.TgtDCSUnit:getLife(), "/", Event.TgtDCSUnit:getLife0() } ) + if Event.TgtDCSUnit:getLife() < Event.TgtDCSUnit:getLife0() then + self:T( "CleanUp: Destroy: " .. Event.TgtDCSUnitName ) + --routines.scheduleFunction( CLEANUP._DestroyUnit, { self, Event.TgtDCSUnit }, timer.getTime() + 0.1 ) + SCHEDULER:New( self, CLEANUP._DestroyUnit, { Event.TgtDCSUnit }, 0.1 ) + end + end + end +end + +--- Add the @{DCSUnit#Unit} to the CleanUpList for CleanUp. +function CLEANUP:_AddForCleanUp( CleanUpUnit, CleanUpUnitName ) + self:F( { CleanUpUnit, CleanUpUnitName } ) + + self.CleanUpList[CleanUpUnitName] = {} + self.CleanUpList[CleanUpUnitName].CleanUpUnit = CleanUpUnit + self.CleanUpList[CleanUpUnitName].CleanUpUnitName = CleanUpUnitName + self.CleanUpList[CleanUpUnitName].CleanUpGroup = Unit.getGroup(CleanUpUnit) + self.CleanUpList[CleanUpUnitName].CleanUpGroupName = Unit.getGroup(CleanUpUnit):getName() + self.CleanUpList[CleanUpUnitName].CleanUpTime = timer.getTime() + self.CleanUpList[CleanUpUnitName].CleanUpMoved = false + + self:T( { "CleanUp: Add to CleanUpList: ", Unit.getGroup(CleanUpUnit):getName(), CleanUpUnitName } ) + +end + +--- Detects if the Unit has an S_EVENT_ENGINE_SHUTDOWN or an S_EVENT_HIT within the given ZoneNames. If this is the case, add the Group to the CLEANUP List. +-- @param #CLEANUP self +-- @param DCSTypes#Event event +function CLEANUP:_EventAddForCleanUp( Event ) + + if Event.IniDCSUnit then + if self.CleanUpList[Event.IniDCSUnitName] == nil then + if routines.IsUnitInZones( Event.IniDCSUnit, self.ZoneNames ) ~= nil then + self:_AddForCleanUp( Event.IniDCSUnit, Event.IniDCSUnitName ) + end + end + end + + if Event.TgtDCSUnit then + if self.CleanUpList[Event.TgtDCSUnitName] == nil then + if routines.IsUnitInZones( Event.TgtDCSUnit, self.ZoneNames ) ~= nil then + self:_AddForCleanUp( Event.TgtDCSUnit, Event.TgtDCSUnitName ) + end + end + end + +end + +local CleanUpSurfaceTypeText = { + "LAND", + "SHALLOW_WATER", + "WATER", + "ROAD", + "RUNWAY" + } + +--- At the defined time interval, CleanUp the Groups within the CleanUpList. +-- @param #CLEANUP self +function CLEANUP:_CleanUpScheduler() + self:F( { "CleanUp Scheduler" } ) + + local CleanUpCount = 0 + for CleanUpUnitName, UnitData in pairs( self.CleanUpList ) do + CleanUpCount = CleanUpCount + 1 + + self:T( { CleanUpUnitName, UnitData } ) + local CleanUpUnit = Unit.getByName(UnitData.CleanUpUnitName) + local CleanUpGroupName = UnitData.CleanUpGroupName + local CleanUpUnitName = UnitData.CleanUpUnitName + if CleanUpUnit then + self:T( { "CleanUp Scheduler", "Checking:", CleanUpUnitName } ) + if _DATABASE:GetStatusGroup( CleanUpGroupName ) ~= "ReSpawn" then + local CleanUpUnitVec3 = CleanUpUnit:getPoint() + --self:T( CleanUpUnitVec3 ) + local CleanUpUnitVec2 = {} + CleanUpUnitVec2.x = CleanUpUnitVec3.x + CleanUpUnitVec2.y = CleanUpUnitVec3.z + --self:T( CleanUpUnitVec2 ) + local CleanUpSurfaceType = land.getSurfaceType(CleanUpUnitVec2) + --self:T( CleanUpSurfaceType ) + --MESSAGE:New( "Surface " .. CleanUpUnitName .. " = " .. CleanUpSurfaceTypeText[CleanUpSurfaceType], CleanUpUnitName, 10, CleanUpUnitName ):ToAll() + + 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 + --MESSAGE:New( "Moved " .. CleanUpUnitName, CleanUpUnitName, 10, CleanUpUnitName ):ToAll() + 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 + +--- Dynamic spawning of groups (and units). +-- +-- @{#SPAWN} class +-- =============== +-- The @{#SPAWN} class allows to spawn dynamically new groups, based on pre-defined initialization settings, modifying the behaviour when groups are spawned. +-- For each group to be spawned, within the mission editor, a group has to be created with the "late activation flag" set. We call this group the *"Spawn Template"* of the SPAWN object. +-- A reference to this Spawn Template needs to be provided when constructing the SPAWN object, by indicating the name of the group within the mission editor in the constructor methods. +-- +-- Within the SPAWN object, there is an internal index that keeps track of which group from the internal group list was spawned. +-- When new groups get spawned by using the SPAWN functions (see below), it will be validated whether the Limits (@{#SPAWN.Limit}) of the SPAWN object are not reached. +-- When all is valid, a new group will be created by the spawning methods, and the internal index will be increased with 1. +-- +-- Regarding the name of new spawned groups, a _SpawnPrefix_ will be assigned for each new group created. +-- If you want to have the Spawn Template name to be used as the _SpawnPrefix_ name, use the @{#SPAWN.New} constructor. +-- However, when the @{#SPAWN.NewWithAlias} constructor was used, the Alias name will define the _SpawnPrefix_ name. +-- Groups will follow the following naming structure when spawned at run-time: +-- +-- 1. Spawned groups will have the name _SpawnPrefix_#ggg, where ggg is a counter from 0 to 999. +-- 2. Spawned units will have the name _SpawnPrefix_#ggg-uu, where uu is a counter from 0 to 99 for each new spawned unit belonging to the group. +-- +-- Some additional notes that need to be remembered: +-- +-- * Templates are actually groups defined within the mission editor, with the flag "Late Activation" set. As such, these groups are never used within the mission, but are used by the @{#SPAWN} module. +-- * It is important to defined BEFORE you spawn new groups, a proper initialization of the SPAWN instance is done with the options you want to use. +-- * When designing a mission, NEVER name groups using a "#" within the name of the group Spawn Template(s), or the SPAWN module logic won't work anymore. +-- +-- SPAWN construction methods: +-- =========================== +-- Create a new SPAWN object with the @{#SPAWN.New} or the @{#SPAWN.NewWithAlias} methods: +-- +-- * @{#SPAWN.New}: Creates a new SPAWN object taking the name of the group that functions as the Template. +-- +-- It is important to understand how the SPAWN class works internally. The SPAWN object created will contain internally a list of groups that will be spawned and that are already spawned. +-- The initialization functions will modify this list of groups so that when a group gets spawned, ALL information is already prepared when spawning. This is done for performance reasons. +-- So in principle, the group list will contain all parameters and configurations after initialization, and when groups get actually spawned, this spawning can be done quickly and efficient. +-- +-- SPAWN initialization methods: +-- ============================= +-- A spawn object will behave differently based on the usage of initialization methods: +-- +-- * @{#SPAWN.Limit}: Limits the amount of groups that can be alive at the same time and that can be dynamically spawned. +-- * @{#SPAWN.RandomizeRoute}: Randomize the routes of spawned groups. +-- * @{#SPAWN.RandomizeTemplate}: Randomize the group templates so that when a new group is spawned, a random group template is selected from one of the templates defined. +-- * @{#SPAWN.Uncontrolled}: Spawn plane groups uncontrolled. +-- * @{#SPAWN.Array}: Make groups visible before they are actually activated, and order these groups like a batallion in an array. +-- * @{#SPAWN.InitRepeat}: Re-spawn groups when they land at the home base. Similar functions are @{#SPAWN.InitRepeatOnLanding} and @{#SPAWN.InitRepeatOnEngineShutDown}. +-- +-- SPAWN spawning methods: +-- ======================= +-- Groups can be spawned at different times and methods: +-- +-- * @{#SPAWN.Spawn}: Spawn one new group based on the last spawned index. +-- * @{#SPAWN.ReSpawn}: Re-spawn a group based on a given index. +-- * @{#SPAWN.SpawnScheduled}: Spawn groups at scheduled but randomized intervals. You can use @{#SPAWN.SpawnScheduleStart} and @{#SPAWN.SpawnScheduleStop} to start and stop the schedule respectively. +-- * @{#SPAWN.SpawnFromUnit}: Spawn a new group taking the position of a @{UNIT}. +-- * @{#SPAWN.SpawnInZone}: Spawn a new group in a @{ZONE}. +-- +-- Note that @{#SPAWN.Spawn} and @{#SPAWN.ReSpawn} return a @{GROUP#GROUP.New} object, that contains a reference to the DCSGroup object. +-- You can use the @{GROUP} object to do further actions with the DCSGroup. +-- +-- SPAWN object cleaning: +-- ========================= +-- Sometimes, it will occur during a mission run-time, that ground or especially air objects get damaged, and will while being damged stop their activities, while remaining alive. +-- In such cases, the SPAWN object will just sit there and wait until that group gets destroyed, but most of the time it won't, +-- and it may occur that no new groups are or can be spawned as limits are reached. +-- To prevent this, a @{#SPAWN.CleanUp} initialization method has been defined that will silently monitor the status of each spawned group. +-- Once a group has a velocity = 0, and has been waiting for a defined interval, that group will be cleaned or removed from run-time. +-- There is a catch however :-) If a damaged group has returned to an airbase within the coalition, that group will not be considered as "lost"... +-- In such a case, when the inactive group is cleaned, a new group will Re-spawned automatically. +-- This models AI that has succesfully returned to their airbase, to restart their combat activities. +-- Check the @{#SPAWN.CleanUp} for further info. +-- +-- ==== +-- @module Spawn +-- @author FlightControl + +Include.File( "Routines" ) +Include.File( "Base" ) +Include.File( "Database" ) +Include.File( "Group" ) +Include.File( "Zone" ) +Include.File( "Event" ) +Include.File( "Scheduler" ) + +--- SPAWN Class +-- @type SPAWN +-- @extends Base#BASE +-- @field ClassName +-- @field #string SpawnTemplatePrefix +-- @field #string SpawnAliasPrefix +SPAWN = { + ClassName = "SPAWN", + SpawnTemplatePrefix = nil, + SpawnAliasPrefix = nil, +} + + + +--- Creates the main object to spawn a GROUP defined in the DCS ME. +-- @param #SPAWN self +-- @param #string SpawnTemplatePrefix is the name of the Group in the ME that defines the Template. Each new group will have the name starting with SpawnTemplatePrefix. +-- @return #SPAWN +-- @usage +-- -- NATO helicopters engaging in the battle field. +-- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ) +-- @usage local Plane = SPAWN:New( "Plane" ) -- Creates a new local variable that can initiate new planes with the name "Plane#ddd" using the template "Plane" as defined within the ME. +function SPAWN:New( SpawnTemplatePrefix ) + local self = BASE:Inherit( self, BASE:New() ) + self:F( { SpawnTemplatePrefix } ) + + local TemplateGroup = Group.getByName( SpawnTemplatePrefix ) + if TemplateGroup then + self.SpawnTemplatePrefix = SpawnTemplatePrefix + self.SpawnIndex = 0 + self.SpawnCount = 0 -- The internal counter of the amount of spawning the has happened since SpawnStart. + self.AliveUnits = 0 -- Contains the counter how many units are currently alive + self.SpawnIsScheduled = false -- Reflects if the spawning for this SpawnTemplatePrefix is going to be scheduled or not. + self.SpawnTemplate = self._GetTemplate( self, SpawnTemplatePrefix ) -- Contains the template structure for a Group Spawn from the Mission Editor. Note that this group must have lateActivation always on!!! + self.Repeat = false -- Don't repeat the group from Take-Off till Landing and back Take-Off by ReSpawning. + self.UnControlled = false -- When working in UnControlled mode, all planes are Spawned in UnControlled mode before the scheduler starts. + self.SpawnMaxUnitsAlive = 0 -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. + self.SpawnMaxGroups = 0 -- The maximum amount of groups that can be spawned. + self.SpawnRandomize = false -- Sets the randomization flag of new Spawned units to false. + self.SpawnVisible = false -- Flag that indicates if all the Groups of the SpawnGroup need to be visible when Spawned. + + self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. + else + error( "SPAWN:New: There is no group declared in the mission editor with SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" ) + end + + return self +end + +--- Creates a new SPAWN instance to create new groups based on the defined template and using a new alias for each new group. +-- @param #SPAWN self +-- @param #string SpawnTemplatePrefix is the name of the Group in the ME that defines the Template. +-- @param #string SpawnAliasPrefix is the name that will be given to the Group at runtime. +-- @return #SPAWN +-- @usage +-- -- NATO helicopters engaging in the battle field. +-- Spawn_BE_KA50 = SPAWN:NewWithAlias( 'BE KA-50@RAMP-Ground Defense', 'Helicopter Attacking a City' ) +-- @usage local PlaneWithAlias = SPAWN:NewWithAlias( "Plane", "Bomber" ) -- Creates a new local variable that can instantiate new planes with the name "Bomber#ddd" using the template "Plane" as defined within the ME. +function SPAWN:NewWithAlias( SpawnTemplatePrefix, SpawnAliasPrefix ) + local self = BASE:Inherit( self, BASE:New() ) + self:F( { SpawnTemplatePrefix, SpawnAliasPrefix } ) + + local TemplateGroup = Group.getByName( SpawnTemplatePrefix ) + if TemplateGroup then + self.SpawnTemplatePrefix = SpawnTemplatePrefix + self.SpawnAliasPrefix = SpawnAliasPrefix + self.SpawnIndex = 0 + self.SpawnCount = 0 -- The internal counter of the amount of spawning the has happened since SpawnStart. + self.AliveUnits = 0 -- Contains the counter how many units are currently alive + self.SpawnIsScheduled = false -- Reflects if the spawning for this SpawnTemplatePrefix is going to be scheduled or not. + self.SpawnTemplate = self._GetTemplate( self, SpawnTemplatePrefix ) -- Contains the template structure for a Group Spawn from the Mission Editor. Note that this group must have lateActivation always on!!! + self.Repeat = false -- Don't repeat the group from Take-Off till Landing and back Take-Off by ReSpawning. + self.UnControlled = false -- When working in UnControlled mode, all planes are Spawned in UnControlled mode before the scheduler starts. + self.SpawnMaxUnitsAlive = 0 -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. + self.SpawnMaxGroups = 0 -- The maximum amount of groups that can be spawned. + self.SpawnRandomize = false -- Sets the randomization flag of new Spawned units to false. + self.SpawnVisible = false -- Flag that indicates if all the Groups of the SpawnGroup need to be visible when Spawned. + + self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. + else + error( "SPAWN:New: There is no group declared in the mission editor with SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" ) + end + + return self +end + + +--- Limits the Maximum amount of Units that can be alive at the same time, and the maximum amount of groups that can be spawned. +-- Note that this method is exceptionally important to balance the performance of the mission. Depending on the machine etc, a mission can only process a maximum amount of units. +-- If the time interval must be short, but there should not be more Units or Groups alive than a maximum amount of units, then this function should be used... +-- When a @{#SPAWN.New} is executed and the limit of the amount of units alive is reached, then no new spawn will happen of the group, until some of these units of the spawn object will be destroyed. +-- @param #SPAWN self +-- @param #number SpawnMaxUnitsAlive The maximum amount of units that can be alive at runtime. +-- @param #number SpawnMaxGroups The maximum amount of groups that can be spawned. When the limit is reached, then no more actual spawns will happen of the group. +-- This parameter is useful to define a maximum amount of airplanes, ground troops, helicopters, ships etc within a supply area. +-- This parameter accepts the value 0, which defines that there are no maximum group limits, but there are limits on the maximum of units that can be alive at the same time. +-- @return #SPAWN self +-- @usage +-- -- NATO helicopters engaging in the battle field. +-- -- This helicopter group consists of one Unit. So, this group will SPAWN maximum 2 groups simultaneously within the DCSRTE. +-- -- There will be maximum 24 groups spawned during the whole mission lifetime. +-- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):Limit( 2, 24 ) +function SPAWN:Limit( SpawnMaxUnitsAlive, SpawnMaxGroups ) + self:F( { self.SpawnTemplatePrefix, SpawnMaxUnitsAlive, SpawnMaxGroups } ) + + self.SpawnMaxUnitsAlive = SpawnMaxUnitsAlive -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. + self.SpawnMaxGroups = SpawnMaxGroups -- The maximum amount of groups that can be spawned. + + for SpawnGroupID = 1, self.SpawnMaxGroups do + self:_InitializeSpawnGroups( SpawnGroupID ) + end + + return self +end + + +--- Randomizes the defined route of the SpawnTemplatePrefix group in the ME. This is very useful to define extra variation of the behaviour of groups. +-- @param #SPAWN self +-- @param #number SpawnStartPoint is the waypoint where the randomization begins. +-- Note that the StartPoint = 0 equaling the point where the group is spawned. +-- @param #number SpawnEndPoint is the waypoint where the randomization ends counting backwards. +-- This parameter is useful to avoid randomization to end at a waypoint earlier than the last waypoint on the route. +-- @param #number SpawnRadius is the radius in meters in which the randomization of the new waypoints, with the original waypoint of the original template located in the middle ... +-- @return #SPAWN +-- @usage +-- -- NATO helicopters engaging in the battle field. +-- -- The KA-50 has waypoints Start point ( =0 or SP ), 1, 2, 3, 4, End point (= 5 or DP). +-- -- Waypoints 2 and 3 will only be randomized. The others will remain on their original position with each new spawn of the helicopter. +-- -- The randomization of waypoint 2 and 3 will take place within a radius of 2000 meters. +-- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):RandomizeRoute( 2, 2, 2000 ) +function SPAWN:RandomizeRoute( SpawnStartPoint, SpawnEndPoint, SpawnRadius ) + self:F( { self.SpawnTemplatePrefix, SpawnStartPoint, SpawnEndPoint, SpawnRadius } ) + + self.SpawnRandomizeRoute = true + self.SpawnRandomizeRouteStartPoint = SpawnStartPoint + self.SpawnRandomizeRouteEndPoint = SpawnEndPoint + self.SpawnRandomizeRouteRadius = SpawnRadius + + for GroupID = 1, self.SpawnMaxGroups do + self:_RandomizeRoute( GroupID ) + end + + return self +end + + +--- This function is rather complicated to understand. But I'll try to explain. +-- This function becomes useful when you need to spawn groups with random templates of groups defined within the mission editor, +-- but they will all follow the same Template route and have the same prefix name. +-- In other words, this method randomizes between a defined set of groups the template to be used for each new spawn of a group. +-- @param #SPAWN self +-- @param #string SpawnTemplatePrefixTable A table with the names of the groups defined within the mission editor, from which one will be choosen when a new group will be spawned. +-- @return #SPAWN +-- @usage +-- -- NATO Tank Platoons invading Gori. +-- -- Choose between 13 different 'US Tank Platoon' configurations for each new SPAWN the Group to be spawned for the +-- -- 'US Tank Platoon Left', 'US Tank Platoon Middle' and 'US Tank Platoon Right' SpawnTemplatePrefixes. +-- -- Each new SPAWN will randomize the route, with a defined time interval of 200 seconds with 40% time variation (randomization) and +-- -- with a limit set of maximum 12 Units alive simulteneously and 150 Groups to be spawned during the whole mission. +-- Spawn_US_Platoon = { 'US Tank Platoon 1', 'US Tank Platoon 2', 'US Tank Platoon 3', 'US Tank Platoon 4', 'US Tank Platoon 5', +-- 'US Tank Platoon 6', 'US Tank Platoon 7', 'US Tank Platoon 8', 'US Tank Platoon 9', 'US Tank Platoon 10', +-- 'US Tank Platoon 11', 'US Tank Platoon 12', 'US Tank Platoon 13' } +-- Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):Limit( 12, 150 ):Schedule( 200, 0.4 ):RandomizeTemplate( Spawn_US_Platoon ):RandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):Limit( 12, 150 ):Schedule( 200, 0.4 ):RandomizeTemplate( Spawn_US_Platoon ):RandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):Limit( 12, 150 ):Schedule( 200, 0.4 ):RandomizeTemplate( Spawn_US_Platoon ):RandomizeRoute( 3, 3, 2000 ) +function SPAWN:RandomizeTemplate( SpawnTemplatePrefixTable ) + self:F( { self.SpawnTemplatePrefix, SpawnTemplatePrefixTable } ) + + self.SpawnTemplatePrefixTable = SpawnTemplatePrefixTable + self.SpawnRandomizeTemplate = true + + for SpawnGroupID = 1, self.SpawnMaxGroups do + self:_RandomizeTemplate( SpawnGroupID ) + end + + return self +end + + + + + +--- For planes and helicopters, when these groups go home and land on their home airbases and farps, they normally would taxi to the parking spot, shut-down their engines and wait forever until the Group is removed by the runtime environment. +-- This function is used to re-spawn automatically (so no extra call is needed anymore) the same group after it has landed. +-- This will enable a spawned group to be re-spawned after it lands, until it is destroyed... +-- Note: When the group is respawned, it will re-spawn from the original airbase where it took off. +-- So ensure that the routes for groups that respawn, always return to the original airbase, or players may get confused ... +-- @param #SPAWN self +-- @return #SPAWN self +-- @usage +-- -- RU Su-34 - AI Ship Attack +-- -- Re-SPAWN the Group(s) after each landing and Engine Shut-Down automatically. +-- SpawnRU_SU34 = SPAWN:New( 'TF1 RU Su-34 Krymsk@AI - Attack Ships' ):Schedule( 2, 3, 1800, 0.4 ):SpawnUncontrolled():RandomizeRoute( 1, 1, 3000 ):RepeatOnEngineShutDown() +function SPAWN:InitRepeat() + self:F( { self.SpawnTemplatePrefix, self.SpawnIndex } ) + + self.Repeat = true + self.RepeatOnEngineShutDown = false + self.RepeatOnLanding = true + + return self +end + +--- Respawn group after landing. +-- @param #SPAWN self +-- @return #SPAWN self +function SPAWN:InitRepeatOnLanding() + self:F( { self.SpawnTemplatePrefix } ) + + self:InitRepeat() + self.RepeatOnEngineShutDown = false + self.RepeatOnLanding = true + + return self +end + + +--- Respawn after landing when its engines have shut down. +-- @param #SPAWN self +-- @return #SPAWN self +function SPAWN:InitRepeatOnEngineShutDown() + self:F( { self.SpawnTemplatePrefix } ) + + self:InitRepeat() + self.RepeatOnEngineShutDown = true + self.RepeatOnLanding = false + + return self +end + + +--- CleanUp groups when they are still alive, but inactive. +-- When groups are still alive and have become inactive due to damage and are unable to contribute anything, then this group will be removed at defined intervals in seconds. +-- @param #SPAWN self +-- @param #string SpawnCleanUpInterval The interval to check for inactive groups within seconds. +-- @return #SPAWN self +-- @usage Spawn_Helicopter:CleanUp( 20 ) -- CleanUp the spawning of the helicopters every 20 seconds when they become inactive. +function SPAWN:CleanUp( SpawnCleanUpInterval ) + self:F( { self.SpawnTemplatePrefix, SpawnCleanUpInterval } ) + + self.SpawnCleanUpInterval = SpawnCleanUpInterval + self.SpawnCleanUpTimeStamps = {} + --self.CleanUpFunction = routines.scheduleFunction( self._SpawnCleanUpScheduler, { self }, timer.getTime() + 1, SpawnCleanUpInterval ) + self.CleanUpScheduler = SCHEDULER:New( self, self._SpawnCleanUpScheduler, {}, 1, SpawnCleanUpInterval, 0.2 ) + return self +end + + + +--- Makes the groups visible before start (like a batallion). +-- The method will take the position of the group as the first position in the array. +-- @param #SPAWN self +-- @param #number SpawnAngle The angle in degrees how the groups and each unit of the group will be positioned. +-- @param #number SpawnWidth The amount of Groups that will be positioned on the X axis. +-- @param #number SpawnDeltaX The space between each Group on the X-axis. +-- @param #number SpawnDeltaY The space between each Group on the Y-axis. +-- @return #SPAWN self +-- @usage +-- -- Define an array of Groups. +-- Spawn_BE_Ground = SPAWN:New( 'BE Ground' ):Limit( 2, 24 ):Visible( 90, "Diamond", 10, 100, 50 ) +function SPAWN:Array( SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY ) + self:F( { self.SpawnTemplatePrefix, SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY } ) + + self.SpawnVisible = true -- When the first Spawn executes, all the Groups need to be made visible before start. + + local SpawnX = 0 + local SpawnY = 0 + local SpawnXIndex = 0 + local SpawnYIndex = 0 + + for SpawnGroupID = 1, self.SpawnMaxGroups do + self:T( { SpawnX, SpawnY, SpawnXIndex, SpawnYIndex } ) + + self.SpawnGroups[SpawnGroupID].Visible = true + self.SpawnGroups[SpawnGroupID].Spawned = false + + SpawnXIndex = SpawnXIndex + 1 + if SpawnWidth and SpawnWidth ~= 0 then + if SpawnXIndex >= SpawnWidth then + SpawnXIndex = 0 + SpawnYIndex = SpawnYIndex + 1 + end + end + + local SpawnRootX = self.SpawnGroups[SpawnGroupID].SpawnTemplate.x + local SpawnRootY = self.SpawnGroups[SpawnGroupID].SpawnTemplate.y + + self:_TranslateRotate( SpawnGroupID, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle ) + + self.SpawnGroups[SpawnGroupID].SpawnTemplate.lateActivation = true + self.SpawnGroups[SpawnGroupID].SpawnTemplate.visible = true + + self.SpawnGroups[SpawnGroupID].Visible = true + + _EVENTDISPATCHER:OnBirthForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnBirth, self ) + _EVENTDISPATCHER:OnCrashForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnDeadOrCrash, self ) + _EVENTDISPATCHER:OnDeadForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnDeadOrCrash, self ) + + if self.Repeat then + _EVENTDISPATCHER:OnTakeOffForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnTakeOff, self ) + _EVENTDISPATCHER:OnLandForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnLand, self ) + end + if self.RepeatOnEngineShutDown then + _EVENTDISPATCHER:OnEngineShutDownForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnEngineShutDown, self ) + end + + self.SpawnGroups[SpawnGroupID].Group = _DATABASE:Spawn( self.SpawnGroups[SpawnGroupID].SpawnTemplate ) + + SpawnX = SpawnXIndex * SpawnDeltaX + SpawnY = SpawnYIndex * SpawnDeltaY + end + + return self +end + + + +--- Will spawn a group based on the internal index. +-- Note: Uses @{DATABASE} module defined in MOOSE. +-- @param #SPAWN self +-- @return Group#GROUP The group that was spawned. You can use this group for further actions. +function SPAWN:Spawn() + self:F( { self.SpawnTemplatePrefix, self.SpawnIndex } ) + + return self:SpawnWithIndex( self.SpawnIndex + 1 ) +end + +--- Will re-spawn a group based on a given index. +-- Note: Uses @{DATABASE} module defined in MOOSE. +-- @param #SPAWN self +-- @param #string SpawnIndex The index of the group to be spawned. +-- @return Group#GROUP The group that was spawned. You can use this group for further actions. +function SPAWN:ReSpawn( SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) + + if not SpawnIndex then + SpawnIndex = 1 + end + +-- TODO: This logic makes DCS crash and i don't know why (yet). + local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) + if SpawnGroup then + local SpawnDCSGroup = SpawnGroup:GetDCSGroup() + if SpawnDCSGroup then + SpawnGroup:Destroy() + end + end + + return self:SpawnWithIndex( SpawnIndex ) +end + +--- Will spawn a group with a specified index number. +-- Uses @{DATABASE} global object defined in MOOSE. +-- @param #SPAWN self +-- @return Group#GROUP The group that was spawned. You can use this group for further actions. +function SPAWN:SpawnWithIndex( SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnMaxGroups } ) + + if self:_GetSpawnIndex( SpawnIndex ) then + + if self.SpawnGroups[self.SpawnIndex].Visible then + self.SpawnGroups[self.SpawnIndex].Group:Activate() + else + self:T( self.SpawnGroups[self.SpawnIndex].SpawnTemplate ) + _EVENTDISPATCHER:OnBirthForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnBirth, self ) + _EVENTDISPATCHER:OnCrashForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnDeadOrCrash, self ) + _EVENTDISPATCHER:OnDeadForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnDeadOrCrash, self ) + + if self.Repeat then + _EVENTDISPATCHER:OnTakeOffForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnTakeOff, self ) + _EVENTDISPATCHER:OnLandForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnLand, self ) + end + if self.RepeatOnEngineShutDown then + _EVENTDISPATCHER:OnEngineShutDownForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnEngineShutDown, self ) + end + + self:T( self.SpawnGroups[self.SpawnIndex].SpawnTemplate ) + + self.SpawnGroups[self.SpawnIndex].Group = _DATABASE:Spawn( self.SpawnGroups[self.SpawnIndex].SpawnTemplate ) + + -- If there is a SpawnFunction hook defined, call it. + if self.SpawnFunctionHook then + self.SpawnFunctionHook( self.SpawnGroups[self.SpawnIndex].Group, unpack( self.SpawnFunctionArguments ) ) + end + -- TODO: Need to fix this by putting an "R" in the name of the group when the group repeats. + --if self.Repeat then + -- _DATABASE:SetStatusGroup( SpawnTemplate.name, "ReSpawn" ) + --end + end + + self.SpawnGroups[self.SpawnIndex].Spawned = true + return self.SpawnGroups[self.SpawnIndex].Group + else + --self:E( { self.SpawnTemplatePrefix, "No more Groups to Spawn:", SpawnIndex, self.SpawnMaxGroups } ) + end + + return nil +end + +--- Spawns new groups at varying time intervals. +-- This is useful if you want to have continuity within your missions of certain (AI) groups to be present (alive) within your missions. +-- @param #SPAWN self +-- @param #number SpawnTime The time interval defined in seconds between each new spawn of new groups. +-- @param #number SpawnTimeVariation The variation to be applied on the defined time interval between each new spawn. +-- The variation is a number between 0 and 1, representing the %-tage of variation to be applied on the time interval. +-- @return #SPAWN self +-- @usage +-- -- NATO helicopters engaging in the battle field. +-- -- The time interval is set to SPAWN new helicopters between each 600 seconds, with a time variation of 50%. +-- -- The time variation in this case will be between 450 seconds and 750 seconds. +-- -- This is calculated as follows: +-- -- Low limit: 600 * ( 1 - 0.5 / 2 ) = 450 +-- -- High limit: 600 * ( 1 + 0.5 / 2 ) = 750 +-- -- Between these two values, a random amount of seconds will be choosen for each new spawn of the helicopters. +-- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):Schedule( 600, 0.5 ) +function SPAWN:SpawnScheduled( SpawnTime, SpawnTimeVariation ) + self:F( { SpawnTime, SpawnTimeVariation } ) + + if SpawnTime ~= nil and SpawnTimeVariation ~= nil then + self.SpawnScheduler = SCHEDULER:New( self, self._Scheduler, {}, 1, SpawnTime, SpawnTimeVariation ) + end + + return self +end + +--- Will re-start the spawning scheduler. +-- Note: This function is only required to be called when the schedule was stopped. +function SPAWN:SpawnScheduleStart() + self:F( { self.SpawnTemplatePrefix } ) + + self.SpawnScheduler:Start() +end + +--- Will stop the scheduled spawning scheduler. +function SPAWN:SpawnScheduleStop() + self:F( { self.SpawnTemplatePrefix } ) + + self.SpawnScheduler:Stop() +end + + +--- Allows to place a CallFunction hook when a new group spawns. +-- The provided function will be called when a new group is spawned, including its given parameters. +-- The first parameter of the SpawnFunction is the @{Group#GROUP} that was spawned. +-- @param #SPAWN self +-- @param #function SpawnFunctionHook The function to be called when a group spawns. +-- @param SpawnFunctionArguments A random amount of arguments to be provided to the function when the group spawns. +-- @return #SPAWN +function SPAWN:SpawnFunction( SpawnFunctionHook, ... ) + self:F( SpawnFunction ) + + self.SpawnFunctionHook = SpawnFunctionHook + self.SpawnFunctionArguments = {} + if arg then + self.SpawnFunctionArguments = arg + end + + return self +end + + + + +--- Will spawn a group from a hosting unit. This function is mostly advisable to be used if you want to simulate spawning from air units, like helicopters, which are dropping infantry into a defined Landing Zone. +-- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. +-- You can use the returned group to further define the route to be followed. +-- @param #SPAWN self +-- @param Unit#UNIT HostUnit The air or ground unit dropping or unloading the group. +-- @param #number OuterRadius The outer radius in meters where the new group will be spawned. +-- @param #number InnerRadius The inner radius in meters where the new group will NOT be spawned. +-- @param #number SpawnIndex (Optional) The index which group to spawn within the given zone. +-- @return Group#GROUP that was spawned. +-- @return #nil Nothing was spawned. +function SPAWN:SpawnFromUnit( HostUnit, OuterRadius, InnerRadius, SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, HostUnit, OuterRadius, InnerRadius, SpawnIndex } ) + + if HostUnit and HostUnit:IsAlive() then -- and HostUnit:getUnit(1):inAir() == false then + + if SpawnIndex then + else + SpawnIndex = self.SpawnIndex + 1 + end + + if self:_GetSpawnIndex( SpawnIndex ) then + + local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate + + if SpawnTemplate then + + local UnitPoint = HostUnit:GetPointVec2() + + self:T( { "Current point of ", self.SpawnTemplatePrefix, UnitPoint } ) + + --for PointID, Point in pairs( SpawnTemplate.route.points ) do + --Point.x = UnitPoint.x + --Point.y = UnitPoint.y + --Point.alt = nil + --Point.alt_type = nil + --end + + SpawnTemplate.route.points[1].x = UnitPoint.x + SpawnTemplate.route.points[1].y = UnitPoint.y + + if not InnerRadius then + InnerRadius = 10 + end + + if not OuterRadius then + OuterRadius = 50 + end + + -- Apply SpawnFormation + for UnitID = 1, #SpawnTemplate.units do + if InnerRadius == 0 then + SpawnTemplate.units[UnitID].x = UnitPoint.x + SpawnTemplate.units[UnitID].y = UnitPoint.y + else + local CirclePos = routines.getRandPointInCircle( UnitPoint, OuterRadius, InnerRadius ) + SpawnTemplate.units[UnitID].x = CirclePos.x + SpawnTemplate.units[UnitID].y = CirclePos.y + end + self:T( 'SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) + end + + local SpawnPos = routines.getRandPointInCircle( UnitPoint, OuterRadius, InnerRadius ) + local Point = {} + Point.type = "Turning Point" + Point.x = SpawnPos.x + Point.y = SpawnPos.y + Point.action = "Cone" + Point.speed = 5 + + table.insert( SpawnTemplate.route.points, 2, Point ) + + return self:SpawnWithIndex( self.SpawnIndex ) + end + end + end + + return nil +end + +--- Will spawn a Group within a given @{Zone#ZONE}. +-- Once the group is spawned within the zone, it will continue on its route. +-- The first waypoint (where the group is spawned) is replaced with the zone coordinates. +-- @param #SPAWN self +-- @param Zone#ZONE Zone The zone where the group is to be spawned. +-- @param #number ZoneRandomize (Optional) Set to true if you want to randomize the starting point in the zone. +-- @param #number SpawnIndex (Optional) The index which group to spawn within the given zone. +-- @return Group#GROUP that was spawned. +-- @return #nil when nothing was spawned. +function SPAWN:SpawnInZone( Zone, ZoneRandomize, SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, Zone, ZoneRandomize, SpawnIndex } ) + + if Zone then + + if SpawnIndex then + else + SpawnIndex = self.SpawnIndex + 1 + end + + if self:_GetSpawnIndex( SpawnIndex ) then + + local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate + + if SpawnTemplate then + + local ZonePoint + + if ZoneRandomize == true then + ZonePoint = Zone:GetRandomPointVec2() + else + ZonePoint = Zone:GetPointVec2() + end + + SpawnTemplate.route.points[1].x = ZonePoint.x + SpawnTemplate.route.points[1].y = ZonePoint.y + + -- Apply SpawnFormation + for UnitID = 1, #SpawnTemplate.units do + local ZonePointUnit = Zone:GetRandomPointVec2() + SpawnTemplate.units[UnitID].x = ZonePointUnit.x + SpawnTemplate.units[UnitID].y = ZonePointUnit.y + self:T( 'SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) + end + + return self:SpawnWithIndex( self.SpawnIndex ) + end + end + end + + return nil +end + + + + +--- Will spawn a plane group in uncontrolled mode... +-- This will be similar to the uncontrolled flag setting in the ME. +-- @return #SPAWN self +function SPAWN:UnControlled() + self:F( { self.SpawnTemplatePrefix } ) + + self.SpawnUnControlled = true + + for SpawnGroupID = 1, self.SpawnMaxGroups do + self.SpawnGroups[SpawnGroupID].UnControlled = true + end + + return self +end + + + +--- Will return the SpawnGroupName either with with a specific count number or without any count. +-- @param #SPAWN self +-- @param #number SpawnIndex Is the number of the Group that is to be spawned. +-- @return #string SpawnGroupName +function SPAWN:SpawnGroupName( SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) + + local SpawnPrefix = self.SpawnTemplatePrefix + if self.SpawnAliasPrefix then + SpawnPrefix = self.SpawnAliasPrefix + end + + if SpawnIndex then + local SpawnName = string.format( '%s#%03d', SpawnPrefix, SpawnIndex ) + self:T( SpawnName ) + return SpawnName + else + self:T( SpawnPrefix ) + return SpawnPrefix + end + +end + +--- Find the first alive group. +-- @param #SPAWN self +-- @param #number SpawnCursor A number holding the index from where to find the first group from. +-- @return Group#GROUP, #number The group found, the new index where the group was found. +-- @return #nil, #nil When no group is found, #nil is returned. +function SPAWN:GetFirstAliveGroup( SpawnCursor ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnCursor } ) + + for SpawnIndex = 1, self.SpawnCount do + local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) + if SpawnGroup and SpawnGroup:IsAlive() then + SpawnCursor = SpawnIndex + return SpawnGroup, SpawnCursor + end + end + + return nil, nil +end + + +--- Find the next alive group. +-- @param #SPAWN self +-- @param #number SpawnCursor A number holding the last found previous index. +-- @return Group#GROUP, #number The group found, the new index where the group was found. +-- @return #nil, #nil When no group is found, #nil is returned. +function SPAWN:GetNextAliveGroup( SpawnCursor ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnCursor } ) + + SpawnCursor = SpawnCursor + 1 + for SpawnIndex = SpawnCursor, self.SpawnCount do + local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) + if SpawnGroup and SpawnGroup:IsAlive() then + SpawnCursor = SpawnIndex + return SpawnGroup, SpawnCursor + end + end + + return nil, nil +end + +--- Find the last alive group during runtime. +function SPAWN:GetLastAliveGroup() + self:F( { self.SpawnTemplatePrefixself.SpawnAliasPrefix } ) + + self.SpawnIndex = self:_GetLastIndex() + for SpawnIndex = self.SpawnIndex, 1, -1 do + local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) + if SpawnGroup and SpawnGroup:IsAlive() then + self.SpawnIndex = SpawnIndex + return SpawnGroup + end + end + + self.SpawnIndex = nil + return nil +end + + + +--- Get the group from an index. +-- Returns the group from the SpawnGroups list. +-- If no index is given, it will return the first group in the list. +-- @param #SPAWN self +-- @param #number SpawnIndex The index of the group to return. +-- @return Group#GROUP +function SPAWN:GetGroupFromIndex( SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndex } ) + + if not SpawnIndex then + SpawnIndex = 1 + end + + if self.SpawnGroups and self.SpawnGroups[SpawnIndex] then + local SpawnGroup = self.SpawnGroups[SpawnIndex].Group + return SpawnGroup + else + return nil + end +end + +--- Get the group index from a DCSUnit. +-- The method will search for a #-mark, and will return the index behind the #-mark of the DCSUnit. +-- It will return nil of no prefix was found. +-- @param #SPAWN self +-- @param DCSUnit The DCS unit to be searched. +-- @return #string The prefix +-- @return #nil Nothing found +function SPAWN:_GetGroupIndexFromDCSUnit( DCSUnit ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, DCSUnit } ) + + if DCSUnit and DCSUnit:getName() then + local IndexString = string.match( DCSUnit:getName(), "#.*-" ):sub( 2, -2 ) + self:T( IndexString ) + + if IndexString then + local Index = tonumber( IndexString ) + self:T( { "Index:", IndexString, Index } ) + return Index + end + end + + return nil +end + +--- Return the prefix of a DCSUnit. +-- The method will search for a #-mark, and will return the text before the #-mark. +-- It will return nil of no prefix was found. +-- @param #SPAWN self +-- @param DCSUnit The DCS unit to be searched. +-- @return #string The prefix +-- @return #nil Nothing found +function SPAWN:_GetPrefixFromDCSUnit( DCSUnit ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, DCSUnit } ) + + if DCSUnit and DCSUnit:getName() then + local SpawnPrefix = string.match( DCSUnit:getName(), ".*#" ) + if SpawnPrefix then + SpawnPrefix = SpawnPrefix:sub( 1, -2 ) + end + self:T( SpawnPrefix ) + return SpawnPrefix + end + + return nil +end + +--- Return the group within the SpawnGroups collection with input a DCSUnit. +function SPAWN:_GetGroupFromDCSUnit( DCSUnit ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, DCSUnit } ) + + if DCSUnit then + local SpawnPrefix = self:_GetPrefixFromDCSUnit( DCSUnit ) + + if self.SpawnTemplatePrefix == SpawnPrefix or ( self.SpawnAliasPrefix and self.SpawnAliasPrefix == SpawnPrefix ) then + local SpawnGroupIndex = self:_GetGroupIndexFromDCSUnit( DCSUnit ) + local SpawnGroup = self.SpawnGroups[SpawnGroupIndex].Group + self:T( SpawnGroup ) + return SpawnGroup + end + end + + return nil +end + + +--- Get the index from a given group. +-- The function will search the name of the group for a #, and will return the number behind the #-mark. +function SPAWN:GetSpawnIndexFromGroup( SpawnGroup ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnGroup } ) + + local IndexString = string.match( SpawnGroup:GetName(), "#.*$" ):sub( 2 ) + local Index = tonumber( IndexString ) + + self:T( IndexString, Index ) + return Index + +end + +--- Return the last maximum index that can be used. +function SPAWN:_GetLastIndex() + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) + + return self.SpawnMaxGroups +end + +--- Initalize the SpawnGroups collection. +function SPAWN:_InitializeSpawnGroups( SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndex } ) + + if not self.SpawnGroups[SpawnIndex] then + self.SpawnGroups[SpawnIndex] = {} + self.SpawnGroups[SpawnIndex].Visible = false + self.SpawnGroups[SpawnIndex].Spawned = false + self.SpawnGroups[SpawnIndex].UnControlled = false + self.SpawnGroups[SpawnIndex].SpawnTime = 0 + + self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix = self.SpawnTemplatePrefix + self.SpawnGroups[SpawnIndex].SpawnTemplate = self:_Prepare( self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix, SpawnIndex ) + end + + self:_RandomizeTemplate( SpawnIndex ) + self:_RandomizeRoute( SpawnIndex ) + --self:_TranslateRotate( SpawnIndex ) + + return self.SpawnGroups[SpawnIndex] +end + + + +--- Gets the CategoryID of the Group with the given SpawnPrefix +function SPAWN:_GetGroupCategoryID( SpawnPrefix ) + local TemplateGroup = Group.getByName( SpawnPrefix ) + + if TemplateGroup then + return TemplateGroup:getCategory() + else + return nil + end +end + +--- Gets the CoalitionID of the Group with the given SpawnPrefix +function SPAWN:_GetGroupCoalitionID( SpawnPrefix ) + local TemplateGroup = Group.getByName( SpawnPrefix ) + + if TemplateGroup then + return TemplateGroup:getCoalition() + else + return nil + end +end + +--- Gets the CountryID of the Group with the given SpawnPrefix +function SPAWN:_GetGroupCountryID( SpawnPrefix ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnPrefix } ) + + local TemplateGroup = Group.getByName( SpawnPrefix ) + + if TemplateGroup then + local TemplateUnits = TemplateGroup:getUnits() + return TemplateUnits[1]:getCountry() + else + return nil + end +end + +--- Gets the Group Template from the ME environment definition. +-- This method used the @{DATABASE} object, which contains ALL initial and new spawned object in MOOSE. +-- @param #SPAWN self +-- @param #string SpawnTemplatePrefix +-- @return @SPAWN self +function SPAWN:_GetTemplate( SpawnTemplatePrefix ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnTemplatePrefix } ) + + local SpawnTemplate = nil + + SpawnTemplate = routines.utils.deepCopy( _DATABASE.Templates.Groups[SpawnTemplatePrefix].Template ) + + if SpawnTemplate == nil then + error( 'No Template returned for SpawnTemplatePrefix = ' .. SpawnTemplatePrefix ) + end + + SpawnTemplate.SpawnCoalitionID = self:_GetGroupCoalitionID( SpawnTemplatePrefix ) + SpawnTemplate.SpawnCategoryID = self:_GetGroupCategoryID( SpawnTemplatePrefix ) + SpawnTemplate.SpawnCountryID = self:_GetGroupCountryID( SpawnTemplatePrefix ) + + self:T( { SpawnTemplate } ) + return SpawnTemplate +end + +--- Prepares the new Group Template. +-- @param #SPAWN self +-- @param #string SpawnTemplatePrefix +-- @param #number SpawnIndex +-- @return #SPAWN self +function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) + + local SpawnTemplate = self:_GetTemplate( SpawnTemplatePrefix ) + SpawnTemplate.name = self:SpawnGroupName( SpawnIndex ) + + SpawnTemplate.groupId = nil + SpawnTemplate.lateActivation = false + + if SpawnTemplate.SpawnCategoryID == Group.Category.GROUND then + self:T( "For ground units, visible needs to be false..." ) + SpawnTemplate.visible = false + end + + if SpawnTemplate.SpawnCategoryID == Group.Category.HELICOPTER or SpawnTemplate.SpawnCategoryID == Group.Category.AIRPLANE then + SpawnTemplate.uncontrolled = false + end + + for UnitID = 1, #SpawnTemplate.units do + SpawnTemplate.units[UnitID].name = string.format( SpawnTemplate.name .. '-%02d', UnitID ) + SpawnTemplate.units[UnitID].unitId = nil + SpawnTemplate.units[UnitID].x = SpawnTemplate.route.points[1].x + SpawnTemplate.units[UnitID].y = SpawnTemplate.route.points[1].y + end + + self:T( { "Template:", SpawnTemplate } ) + return SpawnTemplate + +end + +--- Private method randomizing the routes. +-- @param #SPAWN self +-- @param #number SpawnIndex The index of the group to be spawned. +-- @return #SPAWN +function SPAWN:_RandomizeRoute( SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeRoute, self.SpawnRandomizeRouteStartPoint, self.SpawnRandomizeRouteEndPoint, self.SpawnRandomizeRouteRadius } ) + + if self.SpawnRandomizeRoute then + local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate + local RouteCount = #SpawnTemplate.route.points + + for t = self.SpawnRandomizeRouteStartPoint + 1, ( RouteCount - self.SpawnRandomizeRouteEndPoint ) do + SpawnTemplate.route.points[t].x = SpawnTemplate.route.points[t].x + math.random( self.SpawnRandomizeRouteRadius * -1, self.SpawnRandomizeRouteRadius ) + SpawnTemplate.route.points[t].y = SpawnTemplate.route.points[t].y + math.random( self.SpawnRandomizeRouteRadius * -1, self.SpawnRandomizeRouteRadius ) + -- TODO: manage altitude for airborne units ... + SpawnTemplate.route.points[t].alt = nil + --SpawnGroup.route.points[t].alt_type = nil + self:T( 'SpawnTemplate.route.points[' .. t .. '].x = ' .. SpawnTemplate.route.points[t].x .. ', SpawnTemplate.route.points[' .. t .. '].y = ' .. SpawnTemplate.route.points[t].y ) + end + end + + return self +end + +--- Private method that randomizes the template of the group. +-- @param #SPAWN self +-- @param #number SpawnIndex +-- @return #SPAWN self +function SPAWN:_RandomizeTemplate( SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) + + if self.SpawnRandomizeTemplate then + self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix = self.SpawnTemplatePrefixTable[ math.random( 1, #self.SpawnTemplatePrefixTable ) ] + self.SpawnGroups[SpawnIndex].SpawnTemplate = self:_Prepare( self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix, SpawnIndex ) + self.SpawnGroups[SpawnIndex].SpawnTemplate.route = routines.utils.deepCopy( self.SpawnTemplate.route ) + self.SpawnGroups[SpawnIndex].SpawnTemplate.x = self.SpawnTemplate.x + self.SpawnGroups[SpawnIndex].SpawnTemplate.y = self.SpawnTemplate.y + self.SpawnGroups[SpawnIndex].SpawnTemplate.start_time = self.SpawnTemplate.start_time + for UnitID = 1, #self.SpawnGroups[SpawnIndex].SpawnTemplate.units do + self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].heading = self.SpawnTemplate.units[1].heading + end + end + + self:_RandomizeRoute( SpawnIndex ) + + return self +end + +function SPAWN:_TranslateRotate( SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle } ) + + -- Translate + local TranslatedX = SpawnX + local TranslatedY = SpawnY + + -- Rotate + -- From Wikipedia: https://en.wikipedia.org/wiki/Rotation_matrix#Common_rotations + -- x' = x \cos \theta - y \sin \theta\ + -- y' = x \sin \theta + y \cos \theta\ + local RotatedX = - TranslatedX * math.cos( math.rad( SpawnAngle ) ) + + TranslatedY * math.sin( math.rad( SpawnAngle ) ) + local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) + + TranslatedY * math.cos( math.rad( SpawnAngle ) ) + + -- Assign + self.SpawnGroups[SpawnIndex].SpawnTemplate.x = SpawnRootX - RotatedX + self.SpawnGroups[SpawnIndex].SpawnTemplate.y = SpawnRootY + RotatedY + + + local SpawnUnitCount = table.getn( self.SpawnGroups[SpawnIndex].SpawnTemplate.units ) + for u = 1, SpawnUnitCount do + + -- Translate + local TranslatedX = SpawnX + local TranslatedY = SpawnY - 10 * ( u - 1 ) + + -- Rotate + local RotatedX = - TranslatedX * math.cos( math.rad( SpawnAngle ) ) + + TranslatedY * math.sin( math.rad( SpawnAngle ) ) + local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) + + TranslatedY * math.cos( math.rad( SpawnAngle ) ) + + -- Assign + self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].x = SpawnRootX - RotatedX + self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].y = SpawnRootY + RotatedY + self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].heading = self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].heading + math.rad( SpawnAngle ) + end + + return self +end + +--- Get the next index of the groups to be spawned. This function is complicated, as it is used at several spaces. +function SPAWN:_GetSpawnIndex( SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive, self.AliveUnits, #self.SpawnTemplate.units } ) + + + if ( self.SpawnMaxGroups == 0 ) or ( SpawnIndex <= self.SpawnMaxGroups ) then + if ( self.SpawnMaxUnitsAlive == 0 ) or ( self.AliveUnits < self.SpawnMaxUnitsAlive * #self.SpawnTemplate.units ) or self.UnControlled then + if SpawnIndex and SpawnIndex >= self.SpawnCount + 1 then + self.SpawnCount = self.SpawnCount + 1 + SpawnIndex = self.SpawnCount + end + self.SpawnIndex = SpawnIndex + if not self.SpawnGroups[self.SpawnIndex] then + self:_InitializeSpawnGroups( self.SpawnIndex ) + end + else + return nil + end + else + return nil + end + + return self.SpawnIndex +end + + +-- TODO Need to delete this... _DATABASE does this now ... +function SPAWN:_OnBirth( event ) + + if timer.getTime0() < timer.getAbsTime() then -- dont need to add units spawned in at the start of the mission if mist is loaded in init line + if event.initiator and event.initiator:getName() then + local EventPrefix = self:_GetPrefixFromDCSUnit( event.initiator ) + if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then + self:T( { "Birth event: " .. event.initiator:getName(), event } ) + --MessageToAll( "Mission command: unit " .. SpawnTemplatePrefix .. " spawned." , 5, EventPrefix .. '/Event') + self.AliveUnits = self.AliveUnits + 1 + self:T( "Alive Units: " .. self.AliveUnits ) + end + end + end + +end + +--- Obscolete +-- @todo Need to delete this... _DATABASE does this now ... +function SPAWN:_OnDeadOrCrash( event ) + self:F( self.SpawnTemplatePrefix, event ) + + if event.initiator and event.initiator:getName() then + local EventPrefix = self:_GetPrefixFromDCSUnit( event.initiator ) + if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then + self:T( { "Dead event: " .. event.initiator:getName(), event } ) +-- local DestroyedUnit = Unit.getByName( EventPrefix ) +-- if DestroyedUnit and DestroyedUnit.getLife() <= 1.0 then + --MessageToAll( "Mission command: unit " .. SpawnTemplatePrefix .. " crashed." , 5, EventPrefix .. '/Event') + self.AliveUnits = self.AliveUnits - 1 + self:T( "Alive Units: " .. self.AliveUnits ) +-- end + end + end +end + +--- Will detect AIR Units taking off... When the event takes place, the spawned Group is registered as airborne... +-- This is needed to ensure that Re-SPAWNing only is done for landed AIR Groups. +-- @todo Need to test for AIR Groups only... +function SPAWN:_OnTakeOff( event ) + self:F( self.SpawnTemplatePrefix, event ) + + if event.initiator and event.initiator:getName() then + local SpawnGroup = self:_GetGroupFromDCSUnit( event.initiator ) + if SpawnGroup then + self:T( { "TakeOff event: " .. event.initiator:getName(), event } ) + self:T( "self.Landed = false" ) + self.Landed = false + end + end +end + +--- Will detect AIR Units landing... When the event takes place, the spawned Group is registered as landed. +-- This is needed to ensure that Re-SPAWNing is only done for landed AIR Groups. +-- @todo Need to test for AIR Groups only... +function SPAWN:_OnLand( event ) + self:F( self.SpawnTemplatePrefix, event ) + + local SpawnUnit = event.initiator + if SpawnUnit and SpawnUnit:isExist() and Object.getCategory(SpawnUnit) == Object.Category.UNIT then + local SpawnGroup = self:_GetGroupFromDCSUnit( SpawnUnit ) + if SpawnGroup then + self:T( { "Landed event:" .. SpawnUnit:getName(), event } ) + self.Landed = true + self:T( "self.Landed = true" ) + if self.Landed and self.RepeatOnLanding then + local SpawnGroupIndex = self:GetSpawnIndexFromGroup( SpawnGroup ) + self:T( { "Landed:", "ReSpawn:", SpawnGroup:GetName(), SpawnGroupIndex } ) + self:ReSpawn( SpawnGroupIndex ) + end + end + end +end + +--- Will detect AIR Units shutting down their engines ... +-- When the event takes place, and the method @{RepeatOnEngineShutDown} was called, the spawned Group will Re-SPAWN. +-- But only when the Unit was registered to have landed. +-- @param #SPAWN self +-- @see _OnTakeOff +-- @see _OnLand +-- @todo Need to test for AIR Groups only... +function SPAWN:_OnEngineShutDown( event ) + self:F( self.SpawnTemplatePrefix, event ) + + local SpawnUnit = event.initiator + if SpawnUnit and SpawnUnit:isExist() and Object.getCategory(SpawnUnit) == Object.Category.UNIT then + local SpawnGroup = self:_GetGroupFromDCSUnit( SpawnUnit ) + if SpawnGroup then + self:T( { "EngineShutDown event: " .. SpawnUnit:getName(), event } ) + if self.Landed and self.RepeatOnEngineShutDown then + local SpawnGroupIndex = self:GetSpawnIndexFromGroup( SpawnGroup ) + self:T( { "EngineShutDown: ", "ReSpawn:", SpawnGroup:GetName(), SpawnGroupIndex } ) + self:ReSpawn( SpawnGroupIndex ) + end + end + end +end + +--- This function is called automatically by the Spawning scheduler. +-- It is the internal worker method SPAWNing new Groups on the defined time intervals. +function SPAWN:_Scheduler() + self:F( { "_Scheduler", self.SpawnTemplatePrefix, self.SpawnAliasPrefix, self.SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive } ) + + -- Validate if there are still groups left in the batch... + self:Spawn() + + return true +end + +function SPAWN:_SpawnCleanUpScheduler() + self:F( { "CleanUp Scheduler:", self.SpawnTemplatePrefix } ) + + local SpawnCursor + local SpawnGroup, SpawnCursor = self:GetFirstAliveGroup( SpawnCursor ) + + self:T( { "CleanUp Scheduler:", SpawnGroup } ) + + while SpawnGroup do + + if SpawnGroup:AllOnGround() and SpawnGroup:GetMaxVelocity() < 1 then + if not self.SpawnCleanUpTimeStamps[SpawnGroup:GetName()] then + self.SpawnCleanUpTimeStamps[SpawnGroup:GetName()] = timer.getTime() + else + if self.SpawnCleanUpTimeStamps[SpawnGroup:GetName()] + self.SpawnCleanUpInterval < timer.getTime() then + self:T( { "CleanUp Scheduler:", "Cleaning:", SpawnGroup } ) + SpawnGroup:Destroy() + end + end + else + self.SpawnCleanUpTimeStamps[SpawnGroup:GetName()] = nil + end + + SpawnGroup, SpawnCursor = self:GetNextAliveGroup( SpawnCursor ) + + self:T( { "CleanUp Scheduler:", SpawnGroup } ) + + end + + return true -- Repeat + +end +--- Limit the simultaneous movement of Groups within a running Mission. +-- This module is defined to improve the performance in missions, and to bring additional realism for GROUND vehicles. +-- Performance: If in a DCSRTE there are a lot of moving GROUND units, then in a multi player mission, this WILL create lag if +-- the main DCS execution core of your CPU is fully utilized. So, this class will limit the amount of simultaneous moving GROUND units +-- on defined intervals (currently every minute). +-- @module MOVEMENT + +Include.File( "Routines" ) + +--- the MOVEMENT class +-- @type +MOVEMENT = { + ClassName = "MOVEMENT", +} + +--- Creates the main object which is handling the GROUND forces movement. +-- @param table{string,...}|string MovePrefixes is a table of the Prefixes (names) of the GROUND Groups that need to be controlled by the MOVEMENT Object. +-- @param number MoveMaximum is a number that defines the maximum amount of GROUND Units to be moving during one minute. +-- @return MOVEMENT +-- @usage +-- -- Limit the amount of simultaneous moving units on the ground to prevent lag. +-- Movement_US_Platoons = MOVEMENT:New( { 'US Tank Platoon Left', 'US Tank Platoon Middle', 'US Tank Platoon Right', 'US CH-47D Troops' }, 15 ) + +function MOVEMENT:New( MovePrefixes, MoveMaximum ) + local self = BASE:Inherit( self, BASE:New() ) + self:F( { MovePrefixes, MoveMaximum } ) + + if type( MovePrefixes ) == 'table' then + self.MovePrefixes = MovePrefixes + else + self.MovePrefixes = { MovePrefixes } + end + self.MoveCount = 0 -- The internal counter of the amount of Moveing the has happened since MoveStart. + self.MoveMaximum = MoveMaximum -- Contains the Maximum amount of units that are allowed to move... + self.AliveUnits = 0 -- Contains the counter how many units are currently alive + self.MoveUnits = {} -- Reflects if the Moving for this MovePrefixes is going to be scheduled or not. + + _EVENTDISPATCHER:OnBirth( self.OnBirth, self ) + +-- self:AddEvent( world.event.S_EVENT_BIRTH, self.OnBirth ) +-- +-- self:EnableEvents() + + self:ScheduleStart() + + return self +end + +--- Call this function to start the MOVEMENT scheduling. +function MOVEMENT:ScheduleStart() + self:F() + --self.MoveFunction = routines.scheduleFunction( self._Scheduler, { self }, timer.getTime() + 1, 120 ) + self.MoveFunction = SCHEDULER:New( self, self._Scheduler, {}, 1, 120 ) +end + +--- Call this function to stop the MOVEMENT scheduling. +-- @todo need to implement it ... Forgot. +function MOVEMENT:ScheduleStop() + self:F() + +end + +--- Captures the birth events when new Units were spawned. +-- @todo This method should become obsolete. The new @{DATABASE} class will handle the collection administration. +function MOVEMENT:OnBirth( Event ) + self:F( { Event } ) + + if timer.getTime0() < timer.getAbsTime() then -- dont need to add units spawned in at the start of the mission if mist is loaded in init line + if Event.IniDCSUnit then + self:T( "Birth object : " .. Event.IniDCSUnitName ) + if Event.IniDCSGroup and Event.IniDCSGroup:isExist() then + for MovePrefixID, MovePrefix in pairs( self.MovePrefixes ) do + if string.find( Event.IniDCSUnitName, MovePrefix, 1, true ) then + self.AliveUnits = self.AliveUnits + 1 + self.MoveUnits[Event.IniDCSUnitName] = Event.IniDCSGroupName + self:T( self.AliveUnits ) + end + end + end + end + _EVENTDISPATCHER:OnCrashForUnit( Event.IniDCSUnitName, self.OnDeadOrCrash, self ) + _EVENTDISPATCHER:OnDeadForUnit( Event.IniDCSUnitName, self.OnDeadOrCrash, self ) + end + +end + +--- Captures the Dead or Crash events when Units crash or are destroyed. +-- @todo This method should become obsolete. The new @{DATABASE} class will handle the collection administration. +function MOVEMENT:OnDeadOrCrash( Event ) + self:F( { Event } ) + + if Event.IniDCSUnit then + self:T( "Dead object : " .. Event.IniDCSUnitName ) + for MovePrefixID, MovePrefix in pairs( self.MovePrefixes ) do + if string.find( Event.IniDCSUnitName, MovePrefix, 1, true ) then + self.AliveUnits = self.AliveUnits - 1 + self.MoveUnits[Event.IniDCSUnitName] = nil + self:T( self.AliveUnits ) + end + end + end +end + +--- This function is called automatically by the MOVEMENT scheduler. A new function is scheduled when MoveScheduled is true. +function MOVEMENT:_Scheduler() + self:F( { self.MovePrefixes, self.MoveMaximum, self.AliveUnits, self.MovementGroups } ) + + if self.AliveUnits > 0 then + local MoveProbability = ( self.MoveMaximum * 100 ) / self.AliveUnits + self:T( 'Move Probability = ' .. MoveProbability ) + + for MovementUnitName, MovementGroupName in pairs( self.MoveUnits ) do + local MovementGroup = Group.getByName( MovementGroupName ) + if MovementGroup and MovementGroup:isExist() then + local MoveOrStop = math.random( 1, 100 ) + self:T( 'MoveOrStop = ' .. MoveOrStop ) + if MoveOrStop <= MoveProbability then + self:T( 'Group continues moving = ' .. MovementGroupName ) + trigger.action.groupContinueMoving( MovementGroup ) + else + self:T( 'Group stops moving = ' .. MovementGroupName ) + trigger.action.groupStopMoving( MovementGroup ) + end + else + self.MoveUnits[MovementUnitName] = nil + end + end + end + return true +end +--- Provides defensive behaviour to a set of SAM sites within a running Mission. +-- @module Sead +-- @author to be searched on the forum +-- @author (co) Flightcontrol (Modified and enriched with functionality) + +Include.File( "Routines" ) +Include.File( "Event" ) +Include.File( "Base" ) +Include.File( "Mission" ) +Include.File( "Client" ) +Include.File( "Task" ) + +--- The SEAD class +-- @type SEAD +-- @extends Base#BASE +SEAD = { + ClassName = "SEAD", + TargetSkill = { + Average = { Evade = 50, DelayOff = { 10, 25 }, DelayOn = { 10, 30 } } , + Good = { Evade = 30, DelayOff = { 8, 20 }, DelayOn = { 20, 40 } } , + High = { Evade = 15, DelayOff = { 5, 17 }, DelayOn = { 30, 50 } } , + Excellent = { Evade = 10, DelayOff = { 3, 10 }, DelayOn = { 30, 60 } } + }, + SEADGroupPrefixes = {} +} + +--- Creates the main object which is handling defensive actions for SA sites or moving SA vehicles. +-- When an anti radiation missile is fired (KH-58, KH-31P, KH-31A, KH-25MPU, HARM missiles), the SA will shut down their radars and will take evasive actions... +-- Chances are big that the missile will miss. +-- @param table{string,...}|string SEADGroupPrefixes which is a table of Prefixes of the SA Groups in the DCSRTE on which evasive actions need to be taken. +-- @return SEAD +-- @usage +-- -- CCCP SEAD Defenses +-- -- Defends the Russian SA installations from SEAD attacks. +-- SEAD_RU_SAM_Defenses = SEAD:New( { 'RU SA-6 Kub', 'RU SA-6 Defenses', 'RU MI-26 Troops', 'RU Attack Gori' } ) +function SEAD:New( SEADGroupPrefixes ) + local self = BASE:Inherit( self, BASE:New() ) + self:F( SEADGroupPrefixes ) + if type( SEADGroupPrefixes ) == 'table' then + for SEADGroupPrefixID, SEADGroupPrefix in pairs( SEADGroupPrefixes ) do + self.SEADGroupPrefixes[SEADGroupPrefix] = SEADGroupPrefix + end + else + self.SEADGroupNames[SEADGroupPrefixes] = SEADGroupPrefixes + end + _EVENTDISPATCHER:OnShot( self.EventShot, self ) + + return self +end + +--- Detects if an SA site was shot with an anti radiation missile. In this case, take evasive actions based on the skill level set within the ME. +-- @see SEAD +function SEAD:EventShot( Event ) + self:F( { Event } ) + + local SEADUnit = Event.IniDCSUnit + local SEADUnitName = Event.IniDCSUnitName + local SEADWeapon = Event.Weapon -- Identify the weapon fired + local SEADWeaponName = Event.WeaponName -- return weapon type + --trigger.action.outText( string.format("Alerte, depart missile " ..string.format(SEADWeaponName)), 20) --debug message + -- Start of the 2nd loop + self:T( "Missile Launched = " .. SEADWeaponName ) + if SEADWeaponName == "KH-58" or SEADWeaponName == "KH-25MPU" or SEADWeaponName == "AGM-88" or SEADWeaponName == "KH-31A" or SEADWeaponName == "KH-31P" then -- Check if the missile is a SEAD + local _evade = math.random (1,100) -- random number for chance of evading action + local _targetMim = Event.Weapon:getTarget() -- Identify target + local _targetMimname = Unit.getName(_targetMim) + local _targetMimgroup = Unit.getGroup(Weapon.getTarget(SEADWeapon)) + local _targetMimgroupName = _targetMimgroup:getName() + local _targetMimcont= _targetMimgroup:getController() + local _targetskill = _DATABASE.Templates.Units[_targetMimname].Template.skill + self:T( self.SEADGroupPrefixes ) + self:T( _targetMimgroupName ) + local SEADGroupFound = false + for SEADGroupPrefixID, SEADGroupPrefix in pairs( self.SEADGroupPrefixes ) do + if string.find( _targetMimgroupName, SEADGroupPrefix, 1, true ) then + SEADGroupFound = true + self:T( 'Group Found' ) + break + end + end + if SEADGroupFound == true then + if _targetskill == "Random" then -- when skill is random, choose a skill + local Skills = { "Average", "Good", "High", "Excellent" } + _targetskill = Skills[ math.random(1,4) ] + end + self:T( _targetskill ) -- debug message for skill check + if self.TargetSkill[_targetskill] then + if (_evade > self.TargetSkill[_targetskill].Evade) then + self:T( string.format("Evading, target skill " ..string.format(_targetskill)) ) --debug message + local _targetMim = Weapon.getTarget(SEADWeapon) + local _targetMimname = Unit.getName(_targetMim) + local _targetMimgroup = Unit.getGroup(Weapon.getTarget(SEADWeapon)) + local _targetMimcont= _targetMimgroup:getController() + routines.groupRandomDistSelf(_targetMimgroup,300,'Diamond',250,20) -- move randomly + local SuppressedGroups1 = {} -- unit suppressed radar off for a random time + local function SuppressionEnd1(id) + id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) + SuppressedGroups1[id.groupName] = nil + end + local id = { + groupName = _targetMimgroup, + ctrl = _targetMimcont + } + local delay1 = math.random(self.TargetSkill[_targetskill].DelayOff[1], self.TargetSkill[_targetskill].DelayOff[2]) + if SuppressedGroups1[id.groupName] == nil then + SuppressedGroups1[id.groupName] = { + SuppressionEndTime1 = timer.getTime() + delay1, + SuppressionEndN1 = SuppressionEndCounter1 --Store instance of SuppressionEnd() scheduled function + } + Controller.setOption(_targetMimcont, AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) + timer.scheduleFunction(SuppressionEnd1, id, SuppressedGroups1[id.groupName].SuppressionEndTime1) --Schedule the SuppressionEnd() function + --trigger.action.outText( string.format("Radar Off " ..string.format(delay1)), 20) + end + + local SuppressedGroups = {} + local function SuppressionEnd(id) + id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.RED) + SuppressedGroups[id.groupName] = nil + end + local id = { + groupName = _targetMimgroup, + ctrl = _targetMimcont + } + local delay = math.random(self.TargetSkill[_targetskill].DelayOn[1], self.TargetSkill[_targetskill].DelayOn[2]) + if SuppressedGroups[id.groupName] == nil then + SuppressedGroups[id.groupName] = { + SuppressionEndTime = timer.getTime() + delay, + SuppressionEndN = SuppressionEndCounter --Store instance of SuppressionEnd() scheduled function + } + timer.scheduleFunction(SuppressionEnd, id, SuppressedGroups[id.groupName].SuppressionEndTime) --Schedule the SuppressionEnd() function + --trigger.action.outText( string.format("Radar On " ..string.format(delay)), 20) + end + end + end + end + end +end +--- Taking the lead of AI escorting your flight. +-- +-- @{#ESCORT} class +-- ================ +-- The @{#ESCORT} class allows you to interact with escorting AI on your flight and take the lead. +-- Each escorting group can be commanded with a whole set of radio commands (radio menu in your flight, and then F10). +-- +-- The radio commands will vary according the category of the group. The richest set of commands are with Helicopters and AirPlanes. +-- Ships and Ground troops will have a more limited set, but they can provide support through the bombing of targets designated by the other escorts. +-- +-- RADIO MENUs that can be created: +-- ================================ +-- Find a summary below of the current available commands: +-- +-- Navigation ...: +-- --------------- +-- Escort group navigation functions: +-- +-- * **"Join-Up and Follow at x meters":** The escort group fill follow you at about x meters, and they will follow you. +-- * **"Flare":** Provides menu commands to let the escort group shoot a flare in the air in a color. +-- * **"Smoke":** Provides menu commands to let the escort group smoke the air in a color. Note that smoking is only available for ground and naval troops. +-- +-- Hold position ...: +-- ------------------ +-- Escort group navigation functions: +-- +-- * **"At current location":** Stops the escort group and they will hover 30 meters above the ground at the position they stopped. +-- * **"At client location":** Stops the escort group and they will hover 30 meters above the ground at the position they stopped. +-- +-- Report targets ...: +-- ------------------- +-- Report targets will make the escort group to report any target that it identifies within a 8km range. Any detected target can be attacked using the 4. Attack nearby targets function. (see below). +-- +-- * **"Report now":** Will report the current detected targets. +-- * **"Report targets on":** Will make the escort group to report detected targets and will fill the "Attack nearby targets" menu list. +-- * **"Report targets off":** Will stop detecting targets. +-- +-- Scan targets ...: +-- ----------------- +-- Menu items to pop-up the escort group for target scanning. After scanning, the escort group will resume with the mission or defined task. +-- +-- * **"Scan targets 30 seconds":** Scan 30 seconds for targets. +-- * **"Scan targets 60 seconds":** Scan 60 seconds for targets. +-- +-- Attack targets ...: +-- ------------------- +-- This menu item will list all detected targets within a 15km range. Depending on the level of detection (known/unknown) and visuality, the targets type will also be listed. +-- +-- Request assistance from ...: +-- ---------------------------- +-- This menu item will list all detected targets within a 15km range, as with the menu item **Attack Targets**. +-- This menu item allows to request attack support from other escorts supporting the current client group. +-- eg. the function allows a player to request support from the Ship escort to attack a target identified by the Plane escort with its Tomahawk missiles. +-- eg. the function allows a player to request support from other Planes escorting to bomb the unit with illumination missiles or bombs, so that the main plane escort can attack the area. +-- +-- ROE ...: +-- -------- +-- Sets the Rules of Engagement (ROE) of the escort group when in flight. +-- +-- * **"Hold Fire":** The escort group will hold fire. +-- * **"Return Fire":** The escort group will return fire. +-- * **"Open Fire":** The escort group will open fire on designated targets. +-- * **"Weapon Free":** The escort group will engage with any target. +-- +-- Evasion ...: +-- ------------ +-- Will define the evasion techniques that the escort group will perform during flight or combat. +-- +-- * **"Fight until death":** The escort group will have no reaction to threats. +-- * **"Use flares, chaff and jammers":** The escort group will use passive defense using flares and jammers. No evasive manoeuvres are executed. +-- * **"Evade enemy fire":** The rescort group will evade enemy fire before firing. +-- * **"Go below radar and evade fire":** The escort group will perform evasive vertical manoeuvres. +-- +-- Resume Mission ...: +-- ------------------- +-- Escort groups can have their own mission. This menu item will allow the escort group to resume their Mission from a given waypoint. +-- Note that this is really fantastic, as you now have the dynamic of taking control of the escort groups, and allowing them to resume their path or mission. +-- +-- ESCORT construction methods. +-- ============================ +-- Create a new SPAWN object with the @{#ESCORT.New} method: +-- +-- * @{#ESCORT.New}: Creates a new ESCORT object from a @{Group#GROUP} for a @{Client#CLIENT}, with an optional briefing text. +-- +-- ESCORT initialization methods. +-- ============================== +-- The following menus are created within the RADIO MENU of an active unit hosted by a player: +-- +-- * @{#ESCORT.MenuFollowAt}: Creates a menu to make the escort follow the client. +-- * @{#ESCORT.MenuHoldAtEscortPosition}: Creates a menu to hold the escort at its current position. +-- * @{#ESCORT.MenuHoldAtLeaderPosition}: Creates a menu to hold the escort at the client position. +-- * @{#ESCORT.MenuScanForTargets}: Creates a menu so that the escort scans targets. +-- * @{#ESCORT.MenuFlare}: Creates a menu to disperse flares. +-- * @{#ESCORT.MenuSmoke}: Creates a menu to disparse smoke. +-- * @{#ESCORT.MenuReportTargets}: Creates a menu so that the escort reports targets. +-- * @{#ESCORT.MenuReportPosition}: Creates a menu so that the escort reports its current position from bullseye. +-- * @{#ESCORT.MenuAssistedAttack: Creates a menu so that the escort supportes assisted attack from other escorts with the client. +-- * @{#ESCORT.MenuROE: Creates a menu structure to set the rules of engagement of the escort. +-- * @{#ESCORT.MenuEvasion: Creates a menu structure to set the evasion techniques when the escort is under threat. +-- * @{#ESCORT.MenuResumeMission}: Creates a menu structure so that the escort can resume from a waypoint. +-- +-- @module Escort +-- @author FlightControl + +Include.File( "Routines" ) +Include.File( "Base" ) +Include.File( "Database" ) +Include.File( "Group" ) +Include.File( "Zone" ) + +--- +-- @type ESCORT +-- @extends Base#BASE +-- @field Client#CLIENT EscortClient +-- @field Group#GROUP EscortGroup +-- @field #string EscortName +-- @field #ESCORT.MODE EscortMode The mode the escort is in. +-- @field #number FollowScheduler The id of the _FollowScheduler function. +-- @field #boolean ReportTargets If true, nearby targets are reported. +-- @Field DCSTypes#AI.Option.Air.val.ROE OptionROE Which ROE is set to the EscortGroup. +-- @field DCSTypes#AI.Option.Air.val.REACTION_ON_THREAT OptionReactionOnThreat Which REACTION_ON_THREAT is set to the EscortGroup. +-- @field Menu#MENU_CLIENT EscortMenuResumeMission +ESCORT = { + ClassName = "ESCORT", + EscortName = nil, -- The Escort Name + EscortClient = nil, + EscortGroup = nil, + EscortMode = nil, + 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, + TaskPoints = {} +} + +--- ESCORT.Mode class +-- @type ESCORT.MODE +-- @field #number FOLLOW +-- @field #number MISSION + +--- MENUPARAM type +-- @type MENUPARAM +-- @field #ESCORT ParamSelf +-- @field #Distance ParamDistance +-- @field #function ParamFunction +-- @field #string ParamMessage + +--- ESCORT class constructor for an AI group +-- @param #ESCORT self +-- @param Client#CLIENT EscortClient The client escorted by the EscortGroup. +-- @param Group#GROUP EscortGroup The group AI escorting the EscortClient. +-- @param #string EscortName Name of the escort. +-- @return #ESCORT self +function ESCORT:New( EscortClient, EscortGroup, EscortName, EscortBriefing ) + local self = BASE:Inherit( self, BASE:New() ) + self:F( { EscortClient, EscortGroup, EscortName } ) + + self.EscortClient = EscortClient -- Client#CLIENT + self.EscortGroup = EscortGroup -- Group#GROUP + self.EscortName = EscortName + self.EscortBriefing = EscortBriefing + + self:T( EscortGroup:GetClassNameAndID() ) + + -- Set EscortGroup known at EscortClient. + if not self.EscortClient._EscortGroups then + self.EscortClient._EscortGroups = {} + end + + if not self.EscortClient._EscortGroups[EscortGroup:GetName()] then + self.EscortClient._EscortGroups[EscortGroup:GetName()] = {} + self.EscortClient._EscortGroups[EscortGroup:GetName()].EscortGroup = self.EscortGroup + self.EscortClient._EscortGroups[EscortGroup:GetName()].EscortName = self.EscortName + self.EscortClient._EscortGroups[EscortGroup:GetName()].Targets = {} + self.EscortMode = ESCORT.MODE.FOLLOW + end + + + self.EscortMenu = MENU_CLIENT:New( self.EscortClient, self.EscortName ) + + self.EscortGroup:WayPointInitialize(1) + + self.EscortGroup:OptionROTVertical() + self.EscortGroup:OptionROEOpenFire() + + EscortGroup:MessageToClient( EscortGroup:GetCategoryName() .. " '" .. EscortName .. "' (" .. EscortGroup:GetCallsign() .. ") reporting! " .. + "We're escorting your flight. " .. + "Use the Radio Menu and F10 and use the options under + " .. EscortName .. "\n", + 60, EscortClient + ) + + return self +end + + +--- Defines the default menus +-- @param #ESCORT self +-- @return #ESCORT +function ESCORT:Menus() + self:F() + + self:MenuFollowAt( 100 ) + self:MenuFollowAt( 200 ) + self:MenuFollowAt( 300 ) + self:MenuFollowAt( 400 ) + + self:MenuScanForTargets( 100, 60 ) + + self:MenuHoldAtEscortPosition( 30 ) + self:MenuHoldAtLeaderPosition( 30 ) + + self:MenuFlare() + self:MenuSmoke() + + self:MenuReportTargets( 60 ) + self:MenuAssistedAttack() + self:MenuROE() + self:MenuEvasion() + self:MenuResumeMission() + + return self +end + + + +--- Defines a menu slot to let the escort Join and Follow you at a certain distance. +-- This menu will appear under **Navigation**. +-- @param #ESCORT self +-- @param DCSTypes#Distance Distance The distance in meters that the escort needs to follow the client. +-- @return #ESCORT +function ESCORT:MenuFollowAt( Distance ) + self:F(Distance) + + if self.EscortGroup:IsAir() then + if not self.EscortMenuReportNavigation then + self.EscortMenuReportNavigation = MENU_CLIENT:New( self.EscortClient, "Navigation", self.EscortMenu ) + end + + if not self.EscortMenuJoinUpAndFollow then + self.EscortMenuJoinUpAndFollow = {} + end + + self.EscortMenuJoinUpAndFollow[#self.EscortMenuJoinUpAndFollow+1] = MENU_CLIENT_COMMAND:New( self.EscortClient, "Join-Up and Follow at " .. Distance, self.EscortMenuReportNavigation, ESCORT._JoinUpAndFollow, { ParamSelf = self, ParamDistance = Distance } ) + + self.EscortMode = ESCORT.MODE.FOLLOW + end + + return self +end + +--- Defines a menu slot to let the escort hold at their current position and stay low with a specified height during a specified time in seconds. +-- This menu will appear under **Hold position**. +-- @param #ESCORT self +-- @param DCSTypes#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. +-- @param DCSTypes#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. +-- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. +-- @return #ESCORT +-- TODO: Implement Seconds parameter. Challenge is to first develop the "continue from last activity" function. +function ESCORT:MenuHoldAtEscortPosition( Height, Seconds, MenuTextFormat ) + self:F( { Height, Seconds, MenuTextFormat } ) + + if self.EscortGroup:IsAir() then + + if not self.EscortMenuHold then + self.EscortMenuHold = MENU_CLIENT:New( self.EscortClient, "Hold position", self.EscortMenu ) + end + + if not Height then + Height = 30 + end + + if not Seconds then + Seconds = 0 + end + + local MenuText = "" + if not MenuTextFormat then + if Seconds == 0 then + MenuText = string.format( "Hold at %d meter", Height ) + else + MenuText = string.format( "Hold at %d meter for %d seconds", Height, Seconds ) + end + else + if Seconds == 0 then + MenuText = string.format( MenuTextFormat, Height ) + else + MenuText = string.format( MenuTextFormat, Height, Seconds ) + end + end + + if not self.EscortMenuHoldPosition then + self.EscortMenuHoldPosition = {} + end + + self.EscortMenuHoldPosition[#self.EscortMenuHoldPosition+1] = MENU_CLIENT_COMMAND + :New( + self.EscortClient, + MenuText, + self.EscortMenuHold, + ESCORT._HoldPosition, + { ParamSelf = self, + ParamOrbitGroup = self.EscortGroup, + ParamHeight = Height, + ParamSeconds = Seconds + } + ) + end + + return self +end + + +--- Defines a menu slot to let the escort hold at the client position and stay low with a specified height during a specified time in seconds. +-- This menu will appear under **Navigation**. +-- @param #ESCORT self +-- @param DCSTypes#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. +-- @param DCSTypes#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. +-- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain one or two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. +-- @return #ESCORT +-- TODO: Implement Seconds parameter. Challenge is to first develop the "continue from last activity" function. +function ESCORT:MenuHoldAtLeaderPosition( Height, Seconds, MenuTextFormat ) + self:F( { Height, Seconds, MenuTextFormat } ) + + if self.EscortGroup:IsAir() then + + if not self.EscortMenuHold then + self.EscortMenuHold = MENU_CLIENT:New( self.EscortClient, "Hold position", self.EscortMenu ) + end + + if not Height then + Height = 30 + end + + if not Seconds then + Seconds = 0 + end + + local MenuText = "" + if not MenuTextFormat then + if Seconds == 0 then + MenuText = string.format( "Rejoin and hold at %d meter", Height ) + else + MenuText = string.format( "Rejoin and hold at %d meter for %d seconds", Height, Seconds ) + end + else + if Seconds == 0 then + MenuText = string.format( MenuTextFormat, Height ) + else + MenuText = string.format( MenuTextFormat, Height, Seconds ) + end + end + + if not self.EscortMenuHoldAtLeaderPosition then + self.EscortMenuHoldAtLeaderPosition = {} + end + + self.EscortMenuHoldAtLeaderPosition[#self.EscortMenuHoldAtLeaderPosition+1] = MENU_CLIENT_COMMAND + :New( + self.EscortClient, + MenuText, + self.EscortMenuHold, + ESCORT._HoldPosition, + { ParamSelf = self, + ParamOrbitGroup = self.EscortClient, + ParamHeight = Height, + ParamSeconds = Seconds + } + ) + end + + return self +end + +--- Defines a menu slot to let the escort scan for targets at a certain height for a certain time in seconds. +-- This menu will appear under **Scan targets**. +-- @param #ESCORT self +-- @param DCSTypes#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. +-- @param DCSTypes#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. +-- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain one or two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. +-- @return #ESCORT +function ESCORT:MenuScanForTargets( Height, Seconds, MenuTextFormat ) + self:F( { Height, Seconds, MenuTextFormat } ) + + if self.EscortGroup:IsAir() then + if not self.EscortMenuScan then + self.EscortMenuScan = MENU_CLIENT:New( self.EscortClient, "Scan for targets", self.EscortMenu ) + end + + if not Height then + Height = 100 + end + + if not Seconds then + Seconds = 30 + end + + local MenuText = "" + if not MenuTextFormat then + if Seconds == 0 then + MenuText = string.format( "At %d meter", Height ) + else + MenuText = string.format( "At %d meter for %d seconds", Height, Seconds ) + end + else + if Seconds == 0 then + MenuText = string.format( MenuTextFormat, Height ) + else + MenuText = string.format( MenuTextFormat, Height, Seconds ) + end + end + + if not self.EscortMenuScanForTargets then + self.EscortMenuScanForTargets = {} + end + + self.EscortMenuScanForTargets[#self.EscortMenuScanForTargets+1] = MENU_CLIENT_COMMAND + :New( + self.EscortClient, + MenuText, + self.EscortMenuScan, + ESCORT._ScanTargets, + { ParamSelf = self, + ParamScanDuration = 30 + } + ) + end + + return self +end + + + +--- Defines a menu slot to let the escort disperse a flare in a certain color. +-- This menu will appear under **Navigation**. +-- The flare will be fired from the first unit in the group. +-- @param #ESCORT self +-- @param #string MenuTextFormat Optional parameter that shows the menu option text. If no text is given, the default text will be displayed. +-- @return #ESCORT +function ESCORT:MenuFlare( MenuTextFormat ) + self:F() + + if not self.EscortMenuReportNavigation then + self.EscortMenuReportNavigation = MENU_CLIENT:New( self.EscortClient, "Navigation", self.EscortMenu ) + end + + local MenuText = "" + if not MenuTextFormat then + MenuText = "Flare" + else + MenuText = MenuTextFormat + end + + if not self.EscortMenuFlare then + self.EscortMenuFlare = MENU_CLIENT:New( self.EscortClient, MenuText, self.EscortMenuReportNavigation, ESCORT._Flare, { ParamSelf = self } ) + self.EscortMenuFlareGreen = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release green flare", self.EscortMenuFlare, ESCORT._Flare, { ParamSelf = self, ParamColor = UNIT.FlareColor.Green, ParamMessage = "Released a green flare!" } ) + self.EscortMenuFlareRed = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release red flare", self.EscortMenuFlare, ESCORT._Flare, { ParamSelf = self, ParamColor = UNIT.FlareColor.Red, ParamMessage = "Released a red flare!" } ) + self.EscortMenuFlareWhite = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release white flare", self.EscortMenuFlare, ESCORT._Flare, { ParamSelf = self, ParamColor = UNIT.FlareColor.White, ParamMessage = "Released a white flare!" } ) + self.EscortMenuFlareYellow = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release yellow flare", self.EscortMenuFlare, ESCORT._Flare, { ParamSelf = self, ParamColor = UNIT.FlareColor.Yellow, ParamMessage = "Released a yellow flare!" } ) + end + + return self +end + +--- Defines a menu slot to let the escort disperse a smoke in a certain color. +-- This menu will appear under **Navigation**. +-- Note that smoke menu options will only be displayed for ships and ground units. Not for air units. +-- The smoke will be fired from the first unit in the group. +-- @param #ESCORT self +-- @param #string MenuTextFormat Optional parameter that shows the menu option text. If no text is given, the default text will be displayed. +-- @return #ESCORT +function ESCORT:MenuSmoke( MenuTextFormat ) + self:F() + + if not self.EscortGroup:IsAir() then + if not self.EscortMenuReportNavigation then + self.EscortMenuReportNavigation = MENU_CLIENT:New( self.EscortClient, "Navigation", self.EscortMenu ) + end + + local MenuText = "" + if not MenuTextFormat then + MenuText = "Smoke" + else + MenuText = MenuTextFormat + end + + if not self.EscortMenuSmoke then + self.EscortMenuSmoke = MENU_CLIENT:New( self.EscortClient, "Smoke", self.EscortMenuReportNavigation, ESCORT._Smoke, { ParamSelf = self } ) + self.EscortMenuSmokeGreen = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release green smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.Green, ParamMessage = "Releasing green smoke!" } ) + self.EscortMenuSmokeRed = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release red smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.Red, ParamMessage = "Releasing red smoke!" } ) + self.EscortMenuSmokeWhite = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release white smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.White, ParamMessage = "Releasing white smoke!" } ) + self.EscortMenuSmokeOrange = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release orange smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.Orange, ParamMessage = "Releasing orange smoke!" } ) + self.EscortMenuSmokeBlue = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release blue smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.Blue, ParamMessage = "Releasing blue smoke!" } ) + end + end + + return self +end + +--- Defines a menu slot to let the escort report their current detected targets with a specified time interval in seconds. +-- This menu will appear under **Report targets**. +-- Note that if a report targets menu is not specified, no targets will be detected by the escort, and the attack and assisted attack menus will not be displayed. +-- @param #ESCORT self +-- @param DCSTypes#Time Seconds Optional parameter that lets the escort report their current detected targets after specified time interval in seconds. The default time is 30 seconds. +-- @return #ESCORT +function ESCORT:MenuReportTargets( Seconds ) + self:F( { Seconds } ) + + if not self.EscortMenuReportNearbyTargets then + self.EscortMenuReportNearbyTargets = MENU_CLIENT:New( self.EscortClient, "Report targets", self.EscortMenu ) + end + + if not Seconds then + Seconds = 30 + end + + -- Report Targets + self.EscortMenuReportNearbyTargetsNow = MENU_CLIENT_COMMAND:New( self.EscortClient, "Report targets now!", self.EscortMenuReportNearbyTargets, ESCORT._ReportNearbyTargetsNow, { ParamSelf = self } ) + self.EscortMenuReportNearbyTargetsOn = MENU_CLIENT_COMMAND:New( self.EscortClient, "Report targets on", self.EscortMenuReportNearbyTargets, ESCORT._SwitchReportNearbyTargets, { ParamSelf = self, ParamReportTargets = true } ) + self.EscortMenuReportNearbyTargetsOff = MENU_CLIENT_COMMAND:New( self.EscortClient, "Report targets off", self.EscortMenuReportNearbyTargets, ESCORT._SwitchReportNearbyTargets, { ParamSelf = self, ParamReportTargets = false, } ) + + -- Attack Targets + self.EscortMenuAttackNearbyTargets = MENU_CLIENT:New( self.EscortClient, "Attack targets", self.EscortMenu ) + + + --self.ReportTargetsScheduler = routines.scheduleFunction( self._ReportTargetsScheduler, { self }, timer.getTime() + 1, Seconds ) + self.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, {}, 1, Seconds ) + + return self +end + +--- Defines a menu slot to let the escort attack its detected targets using assisted attack from another escort joined also with the client. +-- This menu will appear under **Request assistance from**. +-- Note that this method needs to be preceded with the method MenuReportTargets. +-- @param #ESCORT self +-- @return #ESCORT +function ESCORT:MenuAssistedAttack() + self:F() + + -- Request assistance from other escorts. + -- This is very useful to let f.e. an escorting ship attack a target detected by an escorting plane... + self.EscortMenuTargetAssistance = MENU_CLIENT:New( self.EscortClient, "Request assistance from", self.EscortMenu ) + + return self +end + +--- Defines a menu to let the escort set its rules of engagement. +-- All rules of engagement will appear under the menu **ROE**. +-- @param #ESCORT self +-- @return #ESCORT +function ESCORT:MenuROE( MenuTextFormat ) + self:F( MenuTextFormat ) + + if not self.EscortMenuROE then + -- Rules of Engagement + self.EscortMenuROE = MENU_CLIENT:New( self.EscortClient, "ROE", self.EscortMenu ) + if self.EscortGroup:OptionROEHoldFirePossible() then + self.EscortMenuROEHoldFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Hold Fire", self.EscortMenuROE, ESCORT._ROE, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROEHoldFire(), ParamMessage = "Holding weapons!" } ) + end + if self.EscortGroup:OptionROEReturnFirePossible() then + self.EscortMenuROEReturnFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Return Fire", self.EscortMenuROE, ESCORT._ROE, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROEReturnFire(), ParamMessage = "Returning fire!" } ) + end + if self.EscortGroup:OptionROEOpenFirePossible() then + self.EscortMenuROEOpenFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Open Fire", self.EscortMenuROE, ESCORT._ROE, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROEOpenFire(), ParamMessage = "Opening fire on designated targets!!" } ) + end + if self.EscortGroup:OptionROEWeaponFreePossible() then + self.EscortMenuROEWeaponFree = MENU_CLIENT_COMMAND:New( self.EscortClient, "Weapon Free", self.EscortMenuROE, ESCORT._ROE, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROEWeaponFree(), ParamMessage = "Opening fire on targets of opportunity!" } ) + end + end + + return self +end + + +--- Defines a menu to let the escort set its evasion when under threat. +-- All rules of engagement will appear under the menu **Evasion**. +-- @param #ESCORT self +-- @return #ESCORT +function ESCORT:MenuEvasion( MenuTextFormat ) + self:F( MenuTextFormat ) + + if self.EscortGroup:IsAir() then + if not self.EscortMenuEvasion then + -- Reaction to Threats + self.EscortMenuEvasion = MENU_CLIENT:New( self.EscortClient, "Evasion", self.EscortMenu ) + if self.EscortGroup:OptionROTNoReactionPossible() then + self.EscortMenuEvasionNoReaction = MENU_CLIENT_COMMAND:New( self.EscortClient, "Fight until death", self.EscortMenuEvasion, ESCORT._ROT, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROTNoReaction(), ParamMessage = "Fighting until death!" } ) + end + if self.EscortGroup:OptionROTPassiveDefensePossible() then + self.EscortMenuEvasionPassiveDefense = MENU_CLIENT_COMMAND:New( self.EscortClient, "Use flares, chaff and jammers", self.EscortMenuEvasion, ESCORT._ROT, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROTPassiveDefense(), ParamMessage = "Defending using jammers, chaff and flares!" } ) + end + if self.EscortGroup:OptionROTEvadeFirePossible() then + self.EscortMenuEvasionEvadeFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Evade enemy fire", self.EscortMenuEvasion, ESCORT._ROT, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROTEvadeFire(), ParamMessage = "Evading on enemy fire!" } ) + end + if self.EscortGroup:OptionROTVerticalPossible() then + self.EscortMenuOptionEvasionVertical = MENU_CLIENT_COMMAND:New( self.EscortClient, "Go below radar and evade fire", self.EscortMenuEvasion, ESCORT._ROT, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROTVertical(), ParamMessage = "Evading on enemy fire with vertical manoeuvres!" } ) + end + end + end + + return self +end + +--- Defines a menu to let the escort resume its mission from a waypoint on its route. +-- All rules of engagement will appear under the menu **Resume mission from**. +-- @param #ESCORT self +-- @return #ESCORT +function ESCORT:MenuResumeMission() + self:F() + + if not self.EscortMenuResumeMission then + -- Mission Resume Menu Root + self.EscortMenuResumeMission = MENU_CLIENT:New( self.EscortClient, "Resume mission from", self.EscortMenu ) + end + + return self +end + + +--- @param #MENUPARAM MenuParam +function ESCORT._HoldPosition( MenuParam ) + + local self = MenuParam.ParamSelf + local EscortGroup = self.EscortGroup + local EscortClient = self.EscortClient + + local OrbitGroup = MenuParam.ParamOrbitGroup -- Group#GROUP + local OrbitUnit = OrbitGroup:GetUnit(1) -- Unit#UNIT + local OrbitHeight = MenuParam.ParamHeight + local OrbitSeconds = MenuParam.ParamSeconds -- Not implemented yet + + routines.removeFunction( self.FollowScheduler ) + + local PointFrom = {} + local GroupPoint = EscortGroup:GetUnit(1):GetPointVec3() + PointFrom = {} + PointFrom.x = GroupPoint.x + PointFrom.y = GroupPoint.z + PointFrom.speed = 250 + PointFrom.type = AI.Task.WaypointType.TURNING_POINT + PointFrom.alt = GroupPoint.y + PointFrom.alt_type = AI.Task.AltitudeType.BARO + + local OrbitPoint = OrbitUnit:GetPointVec2() + local PointTo = {} + PointTo.x = OrbitPoint.x + PointTo.y = OrbitPoint.y + PointTo.speed = 250 + PointTo.type = AI.Task.WaypointType.TURNING_POINT + PointTo.alt = OrbitHeight + PointTo.alt_type = AI.Task.AltitudeType.BARO + PointTo.task = EscortGroup:TaskOrbitCircleAtVec2( OrbitPoint, OrbitHeight, 0 ) + + local Points = { PointFrom, PointTo } + + EscortGroup:OptionROEHoldFire() + EscortGroup:OptionROTPassiveDefense() + + EscortGroup:SetTask( EscortGroup:TaskRoute( Points ) ) + EscortGroup:MessageToClient( "Orbiting at location.", 10, EscortClient ) +end + +--- @param #MENUPARAM MenuParam +function ESCORT._JoinUpAndFollow( MenuParam ) + + local self = MenuParam.ParamSelf + local EscortGroup = self.EscortGroup + local EscortClient = self.EscortClient + + self.Distance = MenuParam.ParamDistance + + self:JoinUpAndFollow( EscortGroup, EscortClient, self.Distance ) +end + +--- JoinsUp and Follows a CLIENT. +-- @param Escort#ESCORT self +-- @param Group#GROUP EscortGroup +-- @param Client#CLIENT EscortClient +-- @param DCSTypes#Distance Distance +function ESCORT:JoinUpAndFollow( EscortGroup, EscortClient, Distance ) + self:F( { EscortGroup, EscortClient, Distance } ) + + if self.FollowScheduler then + routines.removeFunction( self.FollowScheduler ) + end + + EscortGroup:OptionROEHoldFire() + EscortGroup:OptionROTPassiveDefense() + + self.EscortMode = ESCORT.MODE.FOLLOW + + self.CT1 = 0 + self.GT1 = 0 + --self.FollowScheduler = routines.scheduleFunction( self._FollowScheduler, { self, Distance }, timer.getTime() + 1, .5 ) + self.FollowScheduler = SCHEDULER:New( self, self._FollowScheduler, { Distance }, 1, .5, .1 ) + EscortGroup:MessageToClient( "Rejoining and Following at " .. Distance .. "!", 30, EscortClient ) +end + +--- @param #MENUPARAM MenuParam +function ESCORT._Flare( MenuParam ) + + local self = MenuParam.ParamSelf + local EscortGroup = self.EscortGroup + local EscortClient = self.EscortClient + + local Color = MenuParam.ParamColor + local Message = MenuParam.ParamMessage + + EscortGroup:GetUnit(1):Flare( Color ) + EscortGroup:MessageToClient( Message, 10, EscortClient ) +end + +--- @param #MENUPARAM MenuParam +function ESCORT._Smoke( MenuParam ) + + local self = MenuParam.ParamSelf + local EscortGroup = self.EscortGroup + local EscortClient = self.EscortClient + + local Color = MenuParam.ParamColor + local Message = MenuParam.ParamMessage + + EscortGroup:GetUnit(1):Smoke( Color ) + EscortGroup:MessageToClient( Message, 10, EscortClient ) +end + + +--- @param #MENUPARAM MenuParam +function ESCORT._ReportNearbyTargetsNow( MenuParam ) + + local self = MenuParam.ParamSelf + local EscortGroup = self.EscortGroup + local EscortClient = self.EscortClient + + self:_ReportTargetsScheduler() + +end + +function ESCORT._SwitchReportNearbyTargets( MenuParam ) + + local self = MenuParam.ParamSelf + local EscortGroup = self.EscortGroup + local EscortClient = self.EscortClient + + self.ReportTargets = MenuParam.ParamReportTargets + + if self.ReportTargets then + if not self.ReportTargetsScheduler then + --self.ReportTargetsScheduler = routines.scheduleFunction( self._ReportTargetsScheduler, { self }, timer.getTime() + 1, 30 ) + self.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, {}, 1, 30 ) + end + else + routines.removeFunction( self.ReportTargetsScheduler ) + self.ReportTargetsScheduler = nil + end +end + +--- @param #MENUPARAM MenuParam +function ESCORT._ScanTargets( MenuParam ) + + local self = MenuParam.ParamSelf + local EscortGroup = self.EscortGroup + local EscortClient = self.EscortClient + + local ScanDuration = MenuParam.ParamScanDuration + + if self.FollowScheduler then + routines.removeFunction( self.FollowScheduler ) + end + + self:T( { "FollowScheduler after removefunction: ", self.FollowScheduler } ) + + if EscortGroup:IsHelicopter() then + SCHEDULER:New( EscortGroup, EscortGroup.PushTask, + { EscortGroup:TaskControlled( + EscortGroup:TaskOrbitCircle( 200, 20 ), + EscortGroup:TaskCondition( nil, nil, nil, nil, ScanDuration, nil ) + ) + }, + 1 + ) + elseif EscortGroup:IsAirPlane() then + SCHEDULER:New( EscortGroup, EscortGroup.PushTask, + { EscortGroup:TaskControlled( + EscortGroup:TaskOrbitCircle( 1000, 500 ), + EscortGroup:TaskCondition( nil, nil, nil, nil, ScanDuration, nil ) + ) + }, + 1 + ) + end + + EscortGroup:MessageToClient( "Scanning targets for " .. ScanDuration .. " seconds.", ScanDuration, EscortClient ) + + if self.EscortMode == ESCORT.MODE.FOLLOW then + --self.FollowScheduler = routines.scheduleFunction( self._FollowScheduler, { self, Distance }, timer.getTime() + ScanDuration, 1 ) + self.FollowScheduler:Start() + end + +end + +function _Resume( EscortGroup ) + env.info( '_Resume' ) + + local Escort = EscortGroup.Escort -- #ESCORT + env.info( "EscortMode = " .. Escort.EscortMode ) + if Escort.EscortMode == ESCORT.MODE.FOLLOW then + Escort:JoinUpAndFollow( EscortGroup, Escort.EscortClient, Escort.Distance ) + end + +end + +--- @param #MENUPARAM MenuParam +function ESCORT._AttackTarget( MenuParam ) + + local self = MenuParam.ParamSelf + local EscortGroup = self.EscortGroup + local EscortClient = self.EscortClient + local AttackUnit = MenuParam.ParamUnit -- Unit#UNIT + + if self.FollowScheduler then + routines.removeFunction( self.FollowScheduler ) + end + + self:T( AttackUnit ) + + if EscortGroup:IsAir() then + EscortGroup:OptionROEOpenFire() + EscortGroup:OptionROTPassiveDefense() + EscortGroup.Escort = self -- Need to do this trick to get the reference for the escort in the _Resume function. +-- routines.scheduleFunction( +-- EscortGroup.PushTask, +-- { EscortGroup, +-- EscortGroup:TaskCombo( +-- { EscortGroup:TaskAttackUnit( AttackUnit ), +-- EscortGroup:TaskFunction( 1, 2, "_Resume", {"''"} ) +-- } +-- ) +-- }, timer.getTime() + 10 +-- ) + SCHEDULER:New( EscortGroup, + EscortGroup.PushTask, + { EscortGroup:TaskCombo( + { EscortGroup:TaskAttackUnit( AttackUnit ), + EscortGroup:TaskFunction( 1, 2, "_Resume", {"''"} ) + } + ) + }, 10 + ) + else +-- routines.scheduleFunction( +-- EscortGroup.PushTask, +-- { EscortGroup, +-- EscortGroup:TaskCombo( +-- { EscortGroup:TaskFireAtPoint( AttackUnit:GetPointVec2(), 50 ) +-- } +-- ) +-- }, timer.getTime() + 10 +-- ) + SCHEDULER:New( EscortGroup, + EscortGroup.PushTask, + { EscortGroup:TaskCombo( + { EscortGroup:TaskFireAtPoint( AttackUnit:GetPointVec2(), 50 ) + } + ) + }, 10 + ) + end + EscortGroup:MessageToClient( "Engaging Designated Unit!", 10, EscortClient ) + + +end + +--- @param #MENUPARAM MenuParam +function ESCORT._AssistTarget( MenuParam ) + + local self = MenuParam.ParamSelf + local EscortGroup = self.EscortGroup + local EscortClient = self.EscortClient + local EscortGroupAttack = MenuParam.ParamEscortGroup + local AttackUnit = MenuParam.ParamUnit -- Unit#UNIT + + if self.FollowScheduler then + routines.removeFunction( self.FollowScheduler ) + end + + + self:T( AttackUnit ) + + if EscortGroupAttack:IsAir() then + EscortGroupAttack:OptionROEOpenFire() + EscortGroupAttack:OptionROTVertical() +-- routines.scheduleFunction( +-- EscortGroupAttack.PushTask, +-- { EscortGroupAttack, +-- EscortGroupAttack:TaskCombo( +-- { EscortGroupAttack:TaskAttackUnit( AttackUnit ), +-- EscortGroupAttack:TaskOrbitCircle( 500, 350 ) +-- } +-- ) +-- }, timer.getTime() + 10 +-- ) + SCHDULER:New( EscortGroupAttack, + EscortGroupAttack.PushTask, + { EscortGroupAttack:TaskCombo( + { EscortGroupAttack:TaskAttackUnit( AttackUnit ), + EscortGroupAttack:TaskOrbitCircle( 500, 350 ) + } + ) + }, 10 + ) + else +-- routines.scheduleFunction( +-- EscortGroupAttack.PushTask, +-- { EscortGroupAttack, +-- EscortGroupAttack:TaskCombo( +-- { EscortGroupAttack:TaskFireAtPoint( AttackUnit:GetPointVec2(), 50 ) +-- } +-- ) +-- }, timer.getTime() + 10 +-- ) + SCHEDULER:New( EscortGroupAttack, + EscortGroupAttack.PushTask, + { EscortGroupAttack:TaskCombo( + { EscortGroupAttack:TaskFireAtPoint( AttackUnit:GetPointVec2(), 50 ) + } + ) + }, 10 + ) + end + EscortGroupAttack:MessageToClient( "Assisting with the destroying the enemy unit!", 10, EscortClient ) + +end + +--- @param #MENUPARAM MenuParam +function ESCORT._ROE( MenuParam ) + + local self = MenuParam.ParamSelf + local EscortGroup = self.EscortGroup + local EscortClient = self.EscortClient + + local EscortROEFunction = MenuParam.ParamFunction + local EscortROEMessage = MenuParam.ParamMessage + + pcall( function() EscortROEFunction() end ) + EscortGroup:MessageToClient( EscortROEMessage, 10, EscortClient ) +end + +--- @param #MENUPARAM MenuParam +function ESCORT._ROT( MenuParam ) + + local self = MenuParam.ParamSelf + local EscortGroup = self.EscortGroup + local EscortClient = self.EscortClient + + local EscortROTFunction = MenuParam.ParamFunction + local EscortROTMessage = MenuParam.ParamMessage + + pcall( function() EscortROTFunction() end ) + EscortGroup:MessageToClient( EscortROTMessage, 10, EscortClient ) +end + +--- @param #MENUPARAM MenuParam +function ESCORT._ResumeMission( MenuParam ) + + local self = MenuParam.ParamSelf + local EscortGroup = self.EscortGroup + local EscortClient = self.EscortClient + + local WayPoint = MenuParam.ParamWayPoint + + routines.removeFunction( self.FollowScheduler ) + self.FollowScheduler = nil + + local WayPoints = EscortGroup:GetTaskRoute() + self:T( WayPoint, WayPoints ) + + for WayPointIgnore = 1, WayPoint do + table.remove( WayPoints, 1 ) + end + + --routines.scheduleFunction( EscortGroup.SetTask, {EscortGroup, EscortGroup:TaskRoute( WayPoints ) }, timer.getTime() + 1 ) + SCHEDULER:New( EscortGroup, EscortGroup.SetTask, { EscortGroup:TaskRoute( WayPoints ) }, 1 ) + + EscortGroup:MessageToClient( "Resuming mission from waypoint " .. WayPoint .. ".", 10, EscortClient ) +end + +--- Registers the waypoints +-- @param #ESCORT self +-- @return #table +function ESCORT:RegisterRoute() + self:F() + + local EscortGroup = self.EscortGroup -- Group#GROUP + + local TaskPoints = EscortGroup:GetTaskRoute() + + self:T( TaskPoints ) + + return TaskPoints +end + +--- @param Escort#ESCORT self +function ESCORT:_FollowScheduler( FollowDistance ) + self:F( { FollowDistance }) + + if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then + + local ClientUnit = self.EscortClient:GetClientGroupUnit() + local GroupUnit = self.EscortGroup:GetUnit( 1 ) + + if self.CT1 == 0 and self.GT1 == 0 then + self.CV1 = ClientUnit:GetPointVec3() + self.CT1 = timer.getTime() + self.GV1 = GroupUnit:GetPointVec3() + self.GT1 = timer.getTime() + else + local CT1 = self.CT1 + local CT2 = timer.getTime() + local CV1 = self.CV1 + local CV2 = ClientUnit:GetPointVec3() + self.CT1 = CT2 + self.CV1 = CV2 + + local CD = ( ( CV2.x - CV1.x )^2 + ( CV2.y - CV1.y )^2 + ( CV2.z - CV1.z )^2 ) ^ 0.5 + local CT = CT2 - CT1 + + local CS = ( 3600 / CT ) * ( CD / 1000 ) + + self:T2( { "Client:", CS, CD, CT, CV2, CV1, CT2, CT1 } ) + + local GT1 = self.GT1 + local GT2 = timer.getTime() + local GV1 = self.GV1 + local GV2 = GroupUnit:GetPointVec3() + self.GT1 = GT2 + self.GV1 = GV2 + + local GD = ( ( GV2.x - GV1.x )^2 + ( GV2.y - GV1.y )^2 + ( GV2.z - GV1.z )^2 ) ^ 0.5 + local GT = GT2 - GT1 + + local GS = ( 3600 / GT ) * ( GD / 1000 ) + + self:T2( { "Group:", GS, GD, GT, GV2, GV1, GT2, GT1 } ) + + -- Calculate the group direction vector + local GV = { x = GV2.x - CV2.x, y = GV2.y - CV2.y, z = GV2.z - CV2.z } + + -- Calculate GH2, GH2 with the same height as CV2. + local GH2 = { x = GV2.x, y = CV2.y, z = GV2.z } + + -- Calculate the angle of GV to the orthonormal plane + local alpha = math.atan2( GV.z, GV.x ) + + -- Now we calculate the intersecting vector between the circle around CV2 with radius FollowDistance and GH2. + -- From the GeoGebra model: CVI = (x(CV2) + FollowDistance cos(alpha), y(GH2) + FollowDistance sin(alpha), z(CV2)) + local CVI = { x = CV2.x + FollowDistance * math.cos(alpha), + y = GH2.y, + z = CV2.z + FollowDistance * math.sin(alpha), + } + + -- Calculate the direction vector DV of the escort group. We use CVI as the base and CV2 as the direction. + local DV = { x = CV2.x - CVI.x, y = CV2.y - CVI.y, z = CV2.z - CVI.z } + + -- We now calculate the unary direction vector DVu, so that we can multiply DVu with the speed, which is expressed in meters / s. + -- We need to calculate this vector to predict the point the escort group needs to fly to according its speed. + -- The distance of the destination point should be far enough not to have the aircraft starting to swipe left to right... + local DVu = { x = DV.x / FollowDistance, y = DV.y / FollowDistance, z = DV.z / FollowDistance } + + -- Now we can calculate the group destination vector GDV. + local GDV = { x = DVu.x * CS * 8 + CVI.x, y = CVI.y, z = DVu.z * CS * 8 + CVI.z } + + --trigger.action.smoke( GDV, trigger.smokeColor.Red ) + 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, FlyDistance, Time:", CS, GS, Speed, Distance, Time } ) + + -- Now route the escort to the desired point with the desired speed. + self.EscortGroup:TaskRouteToVec3( GDV, Speed / 3.6 ) -- DCS models speed in Mps (Miles per second) + end + return true + end + + return false +end + + +--- Report Targets Scheduler. +-- @param #ESCORT self +function ESCORT:_ReportTargetsScheduler() + self:F( self.EscortGroup:GetName() ) + + if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then + local EscortGroupName = self.EscortGroup:GetName() + local EscortTargets = self.EscortGroup:GetDetectedTargets() + + local ClientEscortTargets = self.EscortClient._EscortGroups[EscortGroupName].Targets + + local EscortTargetMessages = "" + for EscortTargetID, EscortTarget in pairs( EscortTargets ) do + local EscortObject = EscortTarget.object + self:T( EscortObject ) + if EscortObject and EscortObject:isExist() and EscortObject.id_ < 50000000 then + + local EscortTargetUnit = UNIT:Find( EscortObject ) + local EscortTargetUnitName = EscortTargetUnit:GetName() + + + + -- local EscortTargetIsDetected, + -- EscortTargetIsVisible, + -- EscortTargetLastTime, + -- EscortTargetKnowType, + -- EscortTargetKnowDistance, + -- EscortTargetLastPos, + -- EscortTargetLastVelocity + -- = self.EscortGroup:IsTargetDetected( EscortObject ) + -- + -- self:T( { EscortTargetIsDetected, + -- EscortTargetIsVisible, + -- EscortTargetLastTime, + -- EscortTargetKnowType, + -- EscortTargetKnowDistance, + -- EscortTargetLastPos, + -- EscortTargetLastVelocity } ) + + + local EscortTargetUnitPositionVec3 = EscortTargetUnit:GetPointVec3() + local EscortPositionVec3 = self.EscortGroup:GetPointVec3() + local Distance = ( ( EscortTargetUnitPositionVec3.x - EscortPositionVec3.x )^2 + + ( EscortTargetUnitPositionVec3.y - EscortPositionVec3.y )^2 + + ( EscortTargetUnitPositionVec3.z - EscortPositionVec3.z )^2 + ) ^ 0.5 / 1000 + + self:T( { self.EscortGroup:GetName(), EscortTargetUnit:GetName(), Distance, EscortTarget } ) + + if Distance <= 15 then + + if not ClientEscortTargets[EscortTargetUnitName] then + ClientEscortTargets[EscortTargetUnitName] = {} + end + ClientEscortTargets[EscortTargetUnitName].AttackUnit = EscortTargetUnit + ClientEscortTargets[EscortTargetUnitName].visible = EscortTarget.visible + ClientEscortTargets[EscortTargetUnitName].type = EscortTarget.type + ClientEscortTargets[EscortTargetUnitName].distance = EscortTarget.distance + else + if ClientEscortTargets[EscortTargetUnitName] then + ClientEscortTargets[EscortTargetUnitName] = nil + end + end + end + end + + self:T( { "Sorting Targets Table:", ClientEscortTargets } ) + table.sort( ClientEscortTargets, function( a, b ) return a.Distance < b.Distance end ) + self:T( { "Sorted Targets Table:", ClientEscortTargets } ) + + -- Remove the sub menus of the Attack menu of the Escort for the EscortGroup. + self.EscortMenuAttackNearbyTargets:RemoveSubMenus() + + if self.EscortMenuTargetAssistance then + self.EscortMenuTargetAssistance:RemoveSubMenus() + end + + --for MenuIndex = 1, #self.EscortMenuAttackTargets do + -- self:T( { "Remove Menu:", self.EscortMenuAttackTargets[MenuIndex] } ) + -- self.EscortMenuAttackTargets[MenuIndex] = self.EscortMenuAttackTargets[MenuIndex]:Remove() + --end + + + if ClientEscortTargets then + for ClientEscortTargetUnitName, ClientEscortTargetData in pairs( ClientEscortTargets ) do + + for ClientEscortGroupName, EscortGroupData in pairs( self.EscortClient._EscortGroups ) do + + if ClientEscortTargetData and ClientEscortTargetData.AttackUnit:IsAlive() then + + local EscortTargetMessage = "" + local EscortTargetCategoryName = ClientEscortTargetData.AttackUnit:GetCategoryName() + local EscortTargetCategoryType = ClientEscortTargetData.AttackUnit:GetTypeName() + if ClientEscortTargetData.type then + EscortTargetMessage = EscortTargetMessage .. EscortTargetCategoryName .. " (" .. EscortTargetCategoryType .. ") at " + else + EscortTargetMessage = EscortTargetMessage .. "Unknown target at " + end + + local EscortTargetUnitPositionVec3 = ClientEscortTargetData.AttackUnit:GetPointVec3() + local EscortPositionVec3 = self.EscortGroup:GetPointVec3() + local Distance = ( ( EscortTargetUnitPositionVec3.x - EscortPositionVec3.x )^2 + + ( EscortTargetUnitPositionVec3.y - EscortPositionVec3.y )^2 + + ( EscortTargetUnitPositionVec3.z - EscortPositionVec3.z )^2 + ) ^ 0.5 / 1000 + + self:T( { self.EscortGroup:GetName(), ClientEscortTargetData.AttackUnit:GetName(), Distance, ClientEscortTargetData.AttackUnit } ) + if ClientEscortTargetData.visible == false then + EscortTargetMessage = EscortTargetMessage .. string.format( "%.2f", Distance ) .. " estimated km" + else + EscortTargetMessage = EscortTargetMessage .. string.format( "%.2f", Distance ) .. " km" + end + + if ClientEscortTargetData.visible then + EscortTargetMessage = EscortTargetMessage .. ", visual" + end + + if ClientEscortGroupName == EscortGroupName then + + MENU_CLIENT_COMMAND:New( self.EscortClient, + EscortTargetMessage, + self.EscortMenuAttackNearbyTargets, + ESCORT._AttackTarget, + { ParamSelf = self, + ParamUnit = ClientEscortTargetData.AttackUnit + } + ) + EscortTargetMessages = EscortTargetMessages .. "\n - " .. EscortTargetMessage + else + if self.EscortMenuTargetAssistance then + local MenuTargetAssistance = MENU_CLIENT:New( self.EscortClient, EscortGroupData.EscortName, self.EscortMenuTargetAssistance ) + MENU_CLIENT_COMMAND:New( self.EscortClient, + EscortTargetMessage, + MenuTargetAssistance, + ESCORT._AssistTarget, + { ParamSelf = self, + ParamEscortGroup = EscortGroupData.EscortGroup, + ParamUnit = ClientEscortTargetData.AttackUnit + } + ) + end + end + else + ClientEscortTargetData = nil + end + end + end + + if EscortTargetMessages ~= "" and self.ReportTargets == true then + self.EscortGroup:MessageToClient( "Detected targets within 15 km range:" .. EscortTargetMessages:gsub("\n$",""), 20, self.EscortClient ) + else + self.EscortGroup:MessageToClient( "No targets detected!", 20, self.EscortClient ) + end + end + + if self.EscortMenuResumeMission then + self.EscortMenuResumeMission:RemoveSubMenus() + + -- if self.EscortMenuResumeWayPoints then + -- for MenuIndex = 1, #self.EscortMenuResumeWayPoints do + -- self:T( { "Remove Menu:", self.EscortMenuResumeWayPoints[MenuIndex] } ) + -- self.EscortMenuResumeWayPoints[MenuIndex] = self.EscortMenuResumeWayPoints[MenuIndex]:Remove() + -- end + -- end + + local TaskPoints = self:RegisterRoute() + for WayPointID, WayPoint in pairs( TaskPoints ) do + local EscortPositionVec3 = self.EscortGroup:GetPointVec3() + local Distance = ( ( WayPoint.x - EscortPositionVec3.x )^2 + + ( WayPoint.y - EscortPositionVec3.z )^2 + ) ^ 0.5 / 1000 + MENU_CLIENT_COMMAND:New( self.EscortClient, "Waypoint " .. WayPointID .. " at " .. string.format( "%.2f", Distance ).. "km", self.EscortMenuResumeMission, ESCORT._ResumeMission, { ParamSelf = self, ParamWayPoint = WayPointID } ) + end + end + return true + end + + return false +end +--- Provides missile training functions. +-- +-- @{#MISSILETRAINER} class +-- ======================== +-- 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. +-- +-- +-- 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. +-- +-- 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. +-- +-- @module MissileTrainer +-- @author FlightControl + + +Include.File( "Client" ) +Include.File( "Scheduler" ) + +--- The MISSILETRAINER class +-- @type MISSILETRAINER +-- @extends Base#BASE +MISSILETRAINER = { + ClassName = "MISSILETRAINER", +} + +--- Creates the main object which is handling missile tracking. +-- When a missile is fired a SCHEDULER is set off that follows the missile. When near a certain a client player, the missile will be destroyed. +-- @param #MISSILETRAINER self +-- @param #number Distance The distance in meters when a tracked missile needs to be destroyed when close to a player. +-- @param #string Briefing (Optional) Will show a text to the players when starting their mission. Can be used for briefing purposes. +-- @return #MISSILETRAINER +function MISSILETRAINER:New( Distance, Briefing ) + local self = BASE:Inherit( self, BASE:New() ) + self:F( Distance ) + + if Briefing then + self.Briefing = Briefing + end + + self.Schedulers = {} + self.SchedulerID = 0 + + self.MessageInterval = 2 + self.MessageLastTime = timer.getTime() + + self.Distance = Distance / 1000 + + _EVENTDISPATCHER:OnShot( self._EventShot, self ) + + self.DB = DATABASE:New():FilterStart() + self.DBClients = self.DB.Clients + self.DBUnits = self.DB.Units + + for ClientID, Client in pairs( self.DBClients ) do + + local function _Alive( Client ) + + if self.Briefing then + Client:Message( self.Briefing, 15, "HELLO WORLD", "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, "MENU", "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 + + Client:Alive( _Alive ) + + end + +-- self.DB:ForEachClient( +-- --- @param Client#CLIENT Client +-- function( Client ) +-- +-- ... actions ... +-- +-- end +-- ) + + self.MessagesOnOff = true + + self.TrackingToAll = false + self.TrackingOnOff = true + self.TrackingFrequency = 3 + + self.AlertsToAll = true + self.AlertsHitsOnOff = true + self.AlertsLaunchesOnOff = true + + self.DetailsRangeOnOff = true + self.DetailsBearingOnOff = true + + self.MenusOnOff = true + + self.TrackingMissiles = {} + + self.TrackingScheduler = SCHEDULER:New( self, self._TrackMissiles, {}, 0.5, 0.05, 0 ) + + return self +end + +-- Initialization methods. + + +--- Sets by default the display of any message to be ON or OFF. +-- @param #MISSILETRAINER self +-- @param #boolean MessagesOnOff true or false +-- @return #MISSILETRAINER self +function MISSILETRAINER:InitMessagesOnOff( MessagesOnOff ) + self:F( MessagesOnOff ) + + self.MessagesOnOff = MessagesOnOff + if self.MessagesOnOff == true then + MESSAGE:New( "Messages ON", "Menu", 15, "ID" ):ToAll() + else + MESSAGE:New( "Messages OFF", "Menu", 15, "ID" ):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", "Menu", 15, "ID" ):ToAll() + else + MESSAGE:New( "Missile tracking to all players OFF", "Menu", 15, "ID" ):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", "Menu", 15, "ID" ):ToAll() + else + MESSAGE:New( "Missile tracking OFF", "Menu", 15, "ID" ):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.", "Menu", 15, "ID" ):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", "Menu", 15, "ID" ):ToAll() + else + MESSAGE:New( "Alerts to all players OFF", "Menu", 15, "ID" ):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", "Menu", 15, "ID" ):ToAll() + else + MESSAGE:New( "Alerts Hits OFF", "Menu", 15, "ID" ):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", "Menu", 15, "ID" ):ToAll() + else + MESSAGE:New( "Alerts Launches OFF", "Menu", 15, "ID" ):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", "Menu", 15, "ID" ):ToAll() + else + MESSAGE:New( "Range display OFF", "Menu", 15, "ID" ):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", "Menu", 15, "ID" ):ToAll() + else + MESSAGE:New( "Bearing display OFF", "Menu", 15, "ID" ):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)", "Menu", 15, "ID" ):ToAll() + else + MESSAGE:New( "Menus are DISABLED", "Menu", 15, "ID" ):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", "Menu", 15, "ID" ):ToAll() + end + +end + +--- Detects if an SA site was shot with an anti radiation missile. In this case, take evasive actions based on the skill level set within the ME. +-- @param #MISSILETRAINER self +-- @param Event#EVENTDATA Event +function MISSILETRAINER:_EventShot( Event ) + self:F( { Event } ) + + local TrainerSourceDCSUnit = Event.IniDCSUnit + local TrainerSourceDCSUnitName = Event.IniDCSUnitName + local TrainerWeapon = Event.Weapon -- Identify the weapon fired + local TrainerWeaponName = Event.WeaponName -- return weapon type + + self:T( "Missile Launched = " .. TrainerWeaponName ) + + local TrainerTargetDCSUnit = TrainerWeapon:getTarget() -- Identify target + local TrainerTargetDCSUnitName = Unit.getName( TrainerTargetDCSUnit ) + local TrainerTargetSkill = _DATABASE.Templates.Units[TrainerTargetDCSUnitName].Template.skill + + self:T(TrainerTargetDCSUnitName ) + + local Client = self.DBClients[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 ),"Launch Alert", 5, "ID" ) + + if self.AlertsToAll then + Message:ToAll() + else + Message:ToClient( Client ) + end + end + + local ClientID = Client:GetID() + local MissileData = {} + MissileData.TrainerSourceUnit = TrainerSourceUnit + MissileData.TrainerWeapon = TrainerWeapon + MissileData.TrainerTargetUnit = TrainerTargetUnit + MissileData.TrainerWeaponTypeName = TrainerWeapon:getTypeName() + MissileData.TrainerWeaponLaunched = true + table.insert( self.TrackingMissiles[ClientID].MissileData, MissileData ) + --self:T( self.TrackingMissiles ) + end +end + +function MISSILETRAINER:_AddRange( Client, TrainerWeapon ) + + local RangeText = "" + + if self.DetailsRangeOnOff then + + local PositionMissile = TrainerWeapon:getPoint() + local PositionTarget = Client:GetPointVec3() + + local Range = ( ( PositionMissile.x - PositionTarget.x )^2 + + ( PositionMissile.y - PositionTarget.y )^2 + + ( PositionMissile.z - PositionTarget.z )^2 + ) ^ 0.5 / 1000 + + RangeText = string.format( ", at %4.2fkm", Range ) + end + + return RangeText +end + +function MISSILETRAINER:_AddBearing( Client, TrainerWeapon ) + + local BearingText = "" + + if self.DetailsBearingOnOff then + + local PositionMissile = TrainerWeapon:getPoint() + local PositionTarget = Client:GetPointVec3() + + self:T2( { PositionTarget, PositionMissile }) + + local DirectionVector = { x = PositionMissile.x - PositionTarget.x, y = PositionMissile.y - PositionTarget.y, z = PositionMissile.z - PositionTarget.z } + local DirectionRadians = math.atan2( DirectionVector.z, DirectionVector.x ) + --DirectionRadians = DirectionRadians + routines.getNorthCorrection( PositionTarget ) + if DirectionRadians < 0 then + DirectionRadians = DirectionRadians + 2 * math.pi + end + local DirectionDegrees = DirectionRadians * 180 / math.pi + + BearingText = string.format( ", %d degrees", DirectionDegrees ) + end + + return BearingText +end + + +function MISSILETRAINER:_TrackMissiles() + self:F2() + + + local ShowMessages = false + if self.MessagesOnOff and self.MessageLastTime + self.TrackingFrequency <= timer.getTime() then + self.MessageLastTime = timer.getTime() + ShowMessages = true + end + + -- ALERTS PART + + -- Loop for all Player Clients to check the alerts and deletion of missiles. + for ClientDataID, ClientData in pairs( self.TrackingMissiles ) do + + local Client = ClientData.Client + self:T2( { Client:GetName() } ) + + for MissileDataID, MissileData in pairs( ClientData.MissileData ) do + self:T3( MissileDataID ) + + local TrainerSourceUnit = MissileData.TrainerSourceUnit + local TrainerWeapon = MissileData.TrainerWeapon + local TrainerTargetUnit = MissileData.TrainerTargetUnit + local TrainerWeaponTypeName = MissileData.TrainerWeaponTypeName + local TrainerWeaponLaunched = MissileData.TrainerWeaponLaunched + + if Client and Client:IsAlive() and TrainerSourceUnit and TrainerSourceUnit:IsAlive() and TrainerWeapon and TrainerWeapon:isExist() and TrainerTargetUnit and TrainerTargetUnit:IsAlive() then + local PositionMissile = TrainerWeapon:getPosition().p + local PositionTarget = Client:GetPointVec3() + + local Distance = ( ( PositionMissile.x - PositionTarget.x )^2 + + ( PositionMissile.y - PositionTarget.y )^2 + + ( PositionMissile.z - PositionTarget.z )^2 + ) ^ 0.5 / 1000 + + if Distance <= self.Distance then + -- Hit alert + TrainerWeapon:destroy() + if self.MessagesOnOff == true and self.AlertsHitsOnOff == true then + + self:T( "killed" ) + + local Message = MESSAGE:New( + string.format( "%s launched by %s killed %s", + TrainerWeapon:getTypeName(), + TrainerSourceUnit:GetTypeName(), + TrainerTargetUnit:GetPlayerName() + ),"Hit Alert", 15, "ID" ) + + 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() + ),"Tracking", 5, "ID" ) + + 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, "Tracking", 1, "ID" ):ToClient( Client ) + end + end + end + + return true +end +env.info( '*** MOOSE INCLUDE END *** ' ) diff --git a/Moose Mission Setup/Moose.lua b/Moose Mission Setup/Moose.lua index d4c566d6b..6339b2e01 100644 --- a/Moose Mission Setup/Moose.lua +++ b/Moose Mission Setup/Moose.lua @@ -1,6 +1,5 @@ -env.info( '*** MOOSE DYNAMIC INCLUDE START *** ' ) -env.info( 'Moose Generation Timestamp: 20160527_1003' ) - +env.info( '*** MOOSE STATIC INCLUDE START *** ' ) +env.info( 'Moose Generation Timestamp: 20160527_1334' ) local base = _G env.info("Loading MOOSE " .. base.timer.getAbsTime() ) @@ -12,27 +11,9 @@ Include.Path = function() end Include.File = function( IncludeFile ) - if not Include.Files[ IncludeFile ] then - Include.Files[IncludeFile] = IncludeFile - env.info( "Include:" .. IncludeFile .. " from " .. Include.ProgramPath ) - local f = assert( base.loadfile( Include.ProgramPath .. IncludeFile .. ".lua" ) ) - if f == nil then - env.info( "Include:" .. IncludeFile .. " from " .. Include.MissionPath ) - local f = assert( base.loadfile( Include.MissionPath .. IncludeFile .. ".lua" ) ) - if f == nil then - error ("Could not load MOOSE file " .. IncludeFile .. ".lua" ) - else - env.info( "Include:" .. IncludeFile .. " loaded from " .. Include.MissionPath ) - return f() - end - else - env.info( "Include:" .. IncludeFile .. " loaded from " .. Include.ProgramPath ) - return f() - end - end end -Include.ProgramPath = "Scripts/Moose/" +Include.ProgramPath = "Scripts/Moose/Moose/" Include.MissionPath = Include.Path() env.info( "Include.ProgramPath = " .. Include.ProgramPath) @@ -42,4 +23,16636 @@ Include.Files = {} Include.File( "Moose" ) -env.info("Loaded MOOSE Include Engine")env.info( '*** MOOSE INCLUDE END *** ' ) +env.info("Loaded MOOSE Include Engine") +--- Various routines +-- @module routines +-- @author Flightcontrol + +--Include.File( "Trace" ) +--Include.File( "Message" ) + + +env.setErrorMessageBoxEnabled(false) + +--- Extract of MIST functions. +-- @author Grimes + +routines = {} + + +-- don't change these +routines.majorVersion = 3 +routines.minorVersion = 3 +routines.build = 22 + +----------------------------------------------------------------------------------------------------------------- + +---------------------------------------------------------------------------------------------- +-- Utils- conversion, Lua utils, etc. +routines.utils = {} + +--from http://lua-users.org/wiki/CopyTable +routines.utils.deepCopy = function(object) + local lookup_table = {} + local function _copy(object) + if type(object) ~= "table" then + return object + elseif lookup_table[object] then + return lookup_table[object] + end + local new_table = {} + lookup_table[object] = new_table + for index, value in pairs(object) do + new_table[_copy(index)] = _copy(value) + end + return setmetatable(new_table, getmetatable(object)) + end + local objectreturn = _copy(object) + return objectreturn +end + + +-- porting in Slmod's serialize_slmod2 +routines.utils.oneLineSerialize = function(tbl) -- serialization of a table all on a single line, no comments, made to replace old get_table_string function + + lookup_table = {} + + local function _Serialize( tbl ) + + if type(tbl) == 'table' then --function only works for tables! + + if lookup_table[tbl] then + return lookup_table[object] + end + + local tbl_str = {} + + lookup_table[tbl] = tbl_str + + tbl_str[#tbl_str + 1] = '{' + + for ind,val in pairs(tbl) do -- serialize its fields + local ind_str = {} + if type(ind) == "number" then + ind_str[#ind_str + 1] = '[' + ind_str[#ind_str + 1] = tostring(ind) + ind_str[#ind_str + 1] = ']=' + else --must be a string + ind_str[#ind_str + 1] = '[' + ind_str[#ind_str + 1] = routines.utils.basicSerialize(ind) + ind_str[#ind_str + 1] = ']=' + end + + local val_str = {} + if ((type(val) == 'number') or (type(val) == 'boolean')) then + val_str[#val_str + 1] = tostring(val) + val_str[#val_str + 1] = ',' + tbl_str[#tbl_str + 1] = table.concat(ind_str) + tbl_str[#tbl_str + 1] = table.concat(val_str) + elseif type(val) == 'string' then + val_str[#val_str + 1] = routines.utils.basicSerialize(val) + val_str[#val_str + 1] = ',' + tbl_str[#tbl_str + 1] = table.concat(ind_str) + tbl_str[#tbl_str + 1] = table.concat(val_str) + elseif type(val) == 'nil' then -- won't ever happen, right? + val_str[#val_str + 1] = 'nil,' + tbl_str[#tbl_str + 1] = table.concat(ind_str) + tbl_str[#tbl_str + 1] = table.concat(val_str) + elseif type(val) == 'table' then + if ind == "__index" then + -- tbl_str[#tbl_str + 1] = "__index" + -- tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it + else + + val_str[#val_str + 1] = _Serialize(val) + val_str[#val_str + 1] = ',' --I think this is right, I just added it + tbl_str[#tbl_str + 1] = table.concat(ind_str) + tbl_str[#tbl_str + 1] = table.concat(val_str) + end + elseif type(val) == 'function' then + -- tbl_str[#tbl_str + 1] = "function " .. tostring(ind) + -- tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it + else +-- env.info('unable to serialize value type ' .. routines.utils.basicSerialize(type(val)) .. ' at index ' .. tostring(ind)) +-- env.info( debug.traceback() ) + end + + end + tbl_str[#tbl_str + 1] = '}' + return table.concat(tbl_str) + else + return tostring(tbl) + end + end + + local objectreturn = _Serialize(tbl) + return objectreturn +end + +--porting in Slmod's "safestring" basic serialize +routines.utils.basicSerialize = function(s) + if s == nil then + return "\"\"" + else + if ((type(s) == 'number') or (type(s) == 'boolean') or (type(s) == 'function') or (type(s) == 'table') or (type(s) == 'userdata') ) then + return tostring(s) + elseif type(s) == 'string' then + s = string.format('%q', s) + return s + end + end +end + + +routines.utils.toDegree = function(angle) + return angle*180/math.pi +end + +routines.utils.toRadian = function(angle) + return angle*math.pi/180 +end + +routines.utils.metersToNM = function(meters) + return meters/1852 +end + +routines.utils.metersToFeet = function(meters) + return meters/0.3048 +end + +routines.utils.NMToMeters = function(NM) + return NM*1852 +end + +routines.utils.feetToMeters = function(feet) + return feet*0.3048 +end + +routines.utils.mpsToKnots = function(mps) + return mps*3600/1852 +end + +routines.utils.mpsToKmph = function(mps) + return mps*3.6 +end + +routines.utils.knotsToMps = function(knots) + return knots*1852/3600 +end + +routines.utils.kmphToMps = function(kmph) + return kmph/3.6 +end + +function routines.utils.makeVec2(Vec3) + if Vec3.z then + return {x = Vec3.x, y = Vec3.z} + else + return {x = Vec3.x, y = Vec3.y} -- it was actually already vec2. + end +end + +function routines.utils.makeVec3(Vec2, y) + if not Vec2.z then + if not y then + y = 0 + end + return {x = Vec2.x, y = y, z = Vec2.y} + else + return {x = Vec2.x, y = Vec2.y, z = Vec2.z} -- it was already Vec3, actually. + end +end + +function routines.utils.makeVec3GL(Vec2, offset) + local adj = offset or 0 + + if not Vec2.z then + return {x = Vec2.x, y = (land.getHeight(Vec2) + adj), z = Vec2.y} + else + return {x = Vec2.x, y = (land.getHeight({x = Vec2.x, y = Vec2.z}) + adj), z = Vec2.z} + end +end + +routines.utils.zoneToVec3 = function(zone) + local new = {} + if type(zone) == 'table' and zone.point then + new.x = zone.point.x + new.y = zone.point.y + new.z = zone.point.z + return new + elseif type(zone) == 'string' then + zone = trigger.misc.getZone(zone) + if zone then + new.x = zone.point.x + new.y = zone.point.y + new.z = zone.point.z + return new + end + end +end + +-- gets heading-error corrected direction from point along vector vec. +function routines.utils.getDir(vec, point) + local dir = math.atan2(vec.z, vec.x) + dir = dir + routines.getNorthCorrection(point) + if dir < 0 then + dir = dir + 2*math.pi -- put dir in range of 0 to 2*pi + end + return dir +end + +-- gets distance in meters between two points (2 dimensional) +function routines.utils.get2DDist(point1, point2) + point1 = routines.utils.makeVec3(point1) + point2 = routines.utils.makeVec3(point2) + return routines.vec.mag({x = point1.x - point2.x, y = 0, z = point1.z - point2.z}) +end + +-- gets distance in meters between two points (3 dimensional) +function routines.utils.get3DDist(point1, point2) + return routines.vec.mag({x = point1.x - point2.x, y = point1.y - point2.y, z = point1.z - point2.z}) +end + + + +-- From http://lua-users.org/wiki/SimpleRound +-- use negative idp for rounding ahead of decimal place, positive for rounding after decimal place +routines.utils.round = function(num, idp) + local mult = 10^(idp or 0) + return math.floor(num * mult + 0.5) / mult +end + +-- porting in Slmod's dostring +routines.utils.dostring = function(s) + local f, err = loadstring(s) + if f then + return true, f() + else + return false, err + end +end + + +--3D Vector manipulation +routines.vec = {} + +routines.vec.add = function(vec1, vec2) + return {x = vec1.x + vec2.x, y = vec1.y + vec2.y, z = vec1.z + vec2.z} +end + +routines.vec.sub = function(vec1, vec2) + return {x = vec1.x - vec2.x, y = vec1.y - vec2.y, z = vec1.z - vec2.z} +end + +routines.vec.scalarMult = function(vec, mult) + return {x = vec.x*mult, y = vec.y*mult, z = vec.z*mult} +end + +routines.vec.scalar_mult = routines.vec.scalarMult + +routines.vec.dp = function(vec1, vec2) + return vec1.x*vec2.x + vec1.y*vec2.y + vec1.z*vec2.z +end + +routines.vec.cp = function(vec1, vec2) + return { x = vec1.y*vec2.z - vec1.z*vec2.y, y = vec1.z*vec2.x - vec1.x*vec2.z, z = vec1.x*vec2.y - vec1.y*vec2.x} +end + +routines.vec.mag = function(vec) + return (vec.x^2 + vec.y^2 + vec.z^2)^0.5 +end + +routines.vec.getUnitVec = function(vec) + local mag = routines.vec.mag(vec) + return { x = vec.x/mag, y = vec.y/mag, z = vec.z/mag } +end + +routines.vec.rotateVec2 = function(vec2, theta) + return { x = vec2.x*math.cos(theta) - vec2.y*math.sin(theta), y = vec2.x*math.sin(theta) + vec2.y*math.cos(theta)} +end +--------------------------------------------------------------------------------------------------------------------------- + + + + +-- acc- the accuracy of each easting/northing. 0, 1, 2, 3, 4, or 5. +routines.tostringMGRS = function(MGRS, acc) + if acc == 0 then + return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph + else + return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph .. ' ' .. string.format('%0' .. acc .. 'd', routines.utils.round(MGRS.Easting/(10^(5-acc)), 0)) + .. ' ' .. string.format('%0' .. acc .. 'd', routines.utils.round(MGRS.Northing/(10^(5-acc)), 0)) + end +end + +--[[acc: +in DM: decimal point of minutes. +In DMS: decimal point of seconds. +position after the decimal of the least significant digit: +So: +42.32 - acc of 2. +]] +routines.tostringLL = function(lat, lon, acc, DMS) + + local latHemi, lonHemi + if lat > 0 then + latHemi = 'N' + else + latHemi = 'S' + end + + if lon > 0 then + lonHemi = 'E' + else + lonHemi = 'W' + end + + lat = math.abs(lat) + lon = math.abs(lon) + + local latDeg = math.floor(lat) + local latMin = (lat - latDeg)*60 + + local lonDeg = math.floor(lon) + local lonMin = (lon - lonDeg)*60 + + if DMS then -- degrees, minutes, and seconds. + local oldLatMin = latMin + latMin = math.floor(latMin) + local latSec = routines.utils.round((oldLatMin - latMin)*60, acc) + + local oldLonMin = lonMin + lonMin = math.floor(lonMin) + local lonSec = routines.utils.round((oldLonMin - lonMin)*60, acc) + + if latSec == 60 then + latSec = 0 + latMin = latMin + 1 + end + + if lonSec == 60 then + lonSec = 0 + lonMin = lonMin + 1 + end + + local secFrmtStr -- create the formatting string for the seconds place + if acc <= 0 then -- no decimal place. + secFrmtStr = '%02d' + else + local width = 3 + acc -- 01.310 - that's a width of 6, for example. + secFrmtStr = '%0' .. width .. '.' .. acc .. 'f' + end + + return string.format('%02d', latDeg) .. ' ' .. string.format('%02d', latMin) .. '\' ' .. string.format(secFrmtStr, latSec) .. '"' .. latHemi .. ' ' + .. string.format('%02d', lonDeg) .. ' ' .. string.format('%02d', lonMin) .. '\' ' .. string.format(secFrmtStr, lonSec) .. '"' .. lonHemi + + else -- degrees, decimal minutes. + latMin = routines.utils.round(latMin, acc) + lonMin = routines.utils.round(lonMin, acc) + + if latMin == 60 then + latMin = 0 + latDeg = latDeg + 1 + end + + if lonMin == 60 then + lonMin = 0 + lonDeg = lonDeg + 1 + end + + local minFrmtStr -- create the formatting string for the minutes place + if acc <= 0 then -- no decimal place. + minFrmtStr = '%02d' + else + local width = 3 + acc -- 01.310 - that's a width of 6, for example. + minFrmtStr = '%0' .. width .. '.' .. acc .. 'f' + end + + return string.format('%02d', latDeg) .. ' ' .. string.format(minFrmtStr, latMin) .. '\'' .. latHemi .. ' ' + .. string.format('%02d', lonDeg) .. ' ' .. string.format(minFrmtStr, lonMin) .. '\'' .. lonHemi + + end +end + +--[[ required: az - radian + required: dist - meters + optional: alt - meters (set to false or nil if you don't want to use it). + optional: metric - set true to get dist and alt in km and m. + precision will always be nearest degree and NM or km.]] +routines.tostringBR = function(az, dist, alt, metric) + az = routines.utils.round(routines.utils.toDegree(az), 0) + + if metric then + dist = routines.utils.round(dist/1000, 2) + else + dist = routines.utils.round(routines.utils.metersToNM(dist), 2) + end + + local s = string.format('%03d', az) .. ' for ' .. dist + + if alt then + if metric then + s = s .. ' at ' .. routines.utils.round(alt, 0) + else + s = s .. ' at ' .. routines.utils.round(routines.utils.metersToFeet(alt), 0) + end + end + return s +end + +routines.getNorthCorrection = function(point) --gets the correction needed for true north + if not point.z then --Vec2; convert to Vec3 + point.z = point.y + point.y = 0 + end + local lat, lon = coord.LOtoLL(point) + local north_posit = coord.LLtoLO(lat + 1, lon) + return math.atan2(north_posit.z - point.z, north_posit.x - point.x) +end + + +-- the main area +do + -- THE MAIN FUNCTION -- Accessed 100 times/sec. + routines.main = function() + timer.scheduleFunction(routines.main, {}, timer.getTime() + 2) --reschedule first in case of Lua error + ---------------------------------------------------------------------------------------------------------- + --area to add new stuff in + + routines.do_scheduled_functions() + end -- end of routines.main + + timer.scheduleFunction(routines.main, {}, timer.getTime() + 2) + +end + + +do + local idNum = 0 + + --Simplified event handler + routines.addEventHandler = function(f) --id is optional! + local handler = {} + idNum = idNum + 1 + handler.id = idNum + handler.f = f + handler.onEvent = function(self, event) + self.f(event) + end + world.addEventHandler(handler) + end + + routines.removeEventHandler = function(id) + for key, handler in pairs(world.eventHandlers) do + if handler.id and handler.id == id then + world.eventHandlers[key] = nil + return true + end + end + return false + end +end + +-- need to return a Vec3 or Vec2? +function routines.getRandPointInCircle(point, radius, innerRadius) + local theta = 2*math.pi*math.random() + local rad = math.random() + math.random() + if rad > 1 then + rad = 2 - rad + end + + local radMult + if innerRadius and innerRadius <= radius then + radMult = (radius - innerRadius)*rad + innerRadius + else + radMult = radius*rad + end + + if not point.z then --might as well work with vec2/3 + point.z = point.y + end + + local rndCoord + if radius > 0 then + rndCoord = {x = math.cos(theta)*radMult + point.x, y = math.sin(theta)*radMult + point.z} + else + rndCoord = {x = point.x, y = point.z} + end + return rndCoord +end + +routines.goRoute = function(group, path) + local misTask = { + id = 'Mission', + params = { + route = { + points = routines.utils.deepCopy(path), + }, + }, + } + if type(group) == 'string' then + group = Group.getByName(group) + end + local groupCon = group:getController() + if groupCon then + groupCon:setTask(misTask) + return true + end + + Controller.setTask(groupCon, misTask) + return false +end + + +-- Useful atomic functions from mist, ported. + +routines.ground = {} +routines.fixedWing = {} +routines.heli = {} + +routines.ground.buildWP = function(point, overRideForm, overRideSpeed) + + local wp = {} + wp.x = point.x + + if point.z then + wp.y = point.z + else + wp.y = point.y + end + local form, speed + + if point.speed and not overRideSpeed then + wp.speed = point.speed + elseif type(overRideSpeed) == 'number' then + wp.speed = overRideSpeed + else + wp.speed = routines.utils.kmphToMps(20) + end + + if point.form and not overRideForm then + form = point.form + else + form = overRideForm + end + + if not form then + wp.action = 'Cone' + else + form = string.lower(form) + if form == 'off_road' or form == 'off road' then + wp.action = 'Off Road' + elseif form == 'on_road' or form == 'on road' then + wp.action = 'On Road' + elseif form == 'rank' or form == 'line_abrest' or form == 'line abrest' or form == 'lineabrest'then + wp.action = 'Rank' + elseif form == 'cone' then + wp.action = 'Cone' + elseif form == 'diamond' then + wp.action = 'Diamond' + elseif form == 'vee' then + wp.action = 'Vee' + elseif form == 'echelon_left' or form == 'echelon left' or form == 'echelonl' then + wp.action = 'EchelonL' + elseif form == 'echelon_right' or form == 'echelon right' or form == 'echelonr' then + wp.action = 'EchelonR' + else + wp.action = 'Cone' -- if nothing matched + end + end + + wp.type = 'Turning Point' + + return wp + +end + +routines.fixedWing.buildWP = function(point, WPtype, speed, alt, altType) + + local wp = {} + wp.x = point.x + + if point.z then + wp.y = point.z + else + wp.y = point.y + end + + if alt and type(alt) == 'number' then + wp.alt = alt + else + wp.alt = 2000 + end + + if altType then + altType = string.lower(altType) + if altType == 'radio' or 'agl' then + wp.alt_type = 'RADIO' + elseif altType == 'baro' or 'asl' then + wp.alt_type = 'BARO' + end + else + wp.alt_type = 'RADIO' + end + + if point.speed then + speed = point.speed + end + + if point.type then + WPtype = point.type + end + + if not speed then + wp.speed = routines.utils.kmphToMps(500) + else + wp.speed = speed + end + + if not WPtype then + wp.action = 'Turning Point' + else + WPtype = string.lower(WPtype) + if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then + wp.action = 'Fly Over Point' + elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then + wp.action = 'Turning Point' + else + wp.action = 'Turning Point' + end + end + + wp.type = 'Turning Point' + return wp +end + +routines.heli.buildWP = function(point, WPtype, speed, alt, altType) + + local wp = {} + wp.x = point.x + + if point.z then + wp.y = point.z + else + wp.y = point.y + end + + if alt and type(alt) == 'number' then + wp.alt = alt + else + wp.alt = 500 + end + + if altType then + altType = string.lower(altType) + if altType == 'radio' or 'agl' then + wp.alt_type = 'RADIO' + elseif altType == 'baro' or 'asl' then + wp.alt_type = 'BARO' + end + else + wp.alt_type = 'RADIO' + end + + if point.speed then + speed = point.speed + end + + if point.type then + WPtype = point.type + end + + if not speed then + wp.speed = routines.utils.kmphToMps(200) + else + wp.speed = speed + end + + if not WPtype then + wp.action = 'Turning Point' + else + WPtype = string.lower(WPtype) + if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then + wp.action = 'Fly Over Point' + elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then + wp.action = 'Turning Point' + else + wp.action = 'Turning Point' + end + end + + wp.type = 'Turning Point' + return wp +end + +routines.groupToRandomPoint = function(vars) + local group = vars.group --Required + local point = vars.point --required + local radius = vars.radius or 0 + local innerRadius = vars.innerRadius + local form = vars.form or 'Cone' + local heading = vars.heading or math.random()*2*math.pi + local headingDegrees = vars.headingDegrees + local speed = vars.speed or routines.utils.kmphToMps(20) + + + local useRoads + if not vars.disableRoads then + useRoads = true + else + useRoads = false + end + + local path = {} + + if headingDegrees then + heading = headingDegrees*math.pi/180 + end + + if heading >= 2*math.pi then + heading = heading - 2*math.pi + end + + local rndCoord = routines.getRandPointInCircle(point, radius, innerRadius) + + local offset = {} + local posStart = routines.getLeadPos(group) + + offset.x = routines.utils.round(math.sin(heading - (math.pi/2)) * 50 + rndCoord.x, 3) + offset.z = routines.utils.round(math.cos(heading + (math.pi/2)) * 50 + rndCoord.y, 3) + path[#path + 1] = routines.ground.buildWP(posStart, form, speed) + + + if useRoads == true and ((point.x - posStart.x)^2 + (point.z - posStart.z)^2)^0.5 > radius * 1.3 then + path[#path + 1] = routines.ground.buildWP({['x'] = posStart.x + 11, ['z'] = posStart.z + 11}, 'off_road', speed) + path[#path + 1] = routines.ground.buildWP(posStart, 'on_road', speed) + path[#path + 1] = routines.ground.buildWP(offset, 'on_road', speed) + else + path[#path + 1] = routines.ground.buildWP({['x'] = posStart.x + 25, ['z'] = posStart.z + 25}, form, speed) + end + + path[#path + 1] = routines.ground.buildWP(offset, form, speed) + path[#path + 1] = routines.ground.buildWP(rndCoord, form, speed) + + routines.goRoute(group, path) + + return +end + +routines.groupRandomDistSelf = function(gpData, dist, form, heading, speed) + local pos = routines.getLeadPos(gpData) + local fakeZone = {} + fakeZone.radius = dist or math.random(300, 1000) + fakeZone.point = {x = pos.x, y, pos.y, z = pos.z} + routines.groupToRandomZone(gpData, fakeZone, form, heading, speed) + + return +end + +routines.groupToRandomZone = function(gpData, zone, form, heading, speed) + if type(gpData) == 'string' then + gpData = Group.getByName(gpData) + end + + if type(zone) == 'string' then + zone = trigger.misc.getZone(zone) + elseif type(zone) == 'table' and not zone.radius then + zone = trigger.misc.getZone(zone[math.random(1, #zone)]) + end + + if speed then + speed = routines.utils.kmphToMps(speed) + end + + local vars = {} + vars.group = gpData + vars.radius = zone.radius + vars.form = form + vars.headingDegrees = heading + vars.speed = speed + vars.point = routines.utils.zoneToVec3(zone) + + routines.groupToRandomPoint(vars) + + return +end + +routines.isTerrainValid = function(coord, terrainTypes) -- vec2/3 and enum or table of acceptable terrain types + if coord.z then + coord.y = coord.z + end + local typeConverted = {} + + if type(terrainTypes) == 'string' then -- if its a string it does this check + for constId, constData in pairs(land.SurfaceType) do + if string.lower(constId) == string.lower(terrainTypes) or string.lower(constData) == string.lower(terrainTypes) then + table.insert(typeConverted, constId) + end + end + elseif type(terrainTypes) == 'table' then -- if its a table it does this check + for typeId, typeData in pairs(terrainTypes) do + for constId, constData in pairs(land.SurfaceType) do + if string.lower(constId) == string.lower(typeData) or string.lower(constData) == string.lower(typeId) then + table.insert(typeConverted, constId) + end + end + end + end + for validIndex, validData in pairs(typeConverted) do + if land.getSurfaceType(coord) == land.SurfaceType[validData] then + return true + end + end + return false +end + +routines.groupToPoint = function(gpData, point, form, heading, speed, useRoads) + if type(point) == 'string' then + point = trigger.misc.getZone(point) + end + if speed then + speed = routines.utils.kmphToMps(speed) + end + + local vars = {} + vars.group = gpData + vars.form = form + vars.headingDegrees = heading + vars.speed = speed + vars.disableRoads = useRoads + vars.point = routines.utils.zoneToVec3(point) + routines.groupToRandomPoint(vars) + + return +end + + +routines.getLeadPos = function(group) + if type(group) == 'string' then -- group name + group = Group.getByName(group) + end + + local units = group:getUnits() + + local leader = units[1] + if not leader then -- SHOULD be good, but if there is a bug, this code future-proofs it then. + local lowestInd = math.huge + for ind, unit in pairs(units) do + if ind < lowestInd then + lowestInd = ind + leader = unit + end + end + end + if leader and Unit.isExist(leader) then -- maybe a little too paranoid now... + return leader:getPosition().p + end +end + +--[[ vars for routines.getMGRSString: +vars.units - table of unit names (NOT unitNameTable- maybe this should change). +vars.acc - integer between 0 and 5, inclusive +]] +routines.getMGRSString = function(vars) + local units = vars.units + local acc = vars.acc or 5 + local avgPos = routines.getAvgPos(units) + if avgPos then + return routines.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(avgPos)), acc) + end +end + +--[[ vars for routines.getLLString +vars.units - table of unit names (NOT unitNameTable- maybe this should change). +vars.acc - integer, number of numbers after decimal place +vars.DMS - if true, output in degrees, minutes, seconds. Otherwise, output in degrees, minutes. + + +]] +routines.getLLString = function(vars) + local units = vars.units + local acc = vars.acc or 3 + local DMS = vars.DMS + local avgPos = routines.getAvgPos(units) + if avgPos then + local lat, lon = coord.LOtoLL(avgPos) + return routines.tostringLL(lat, lon, acc, DMS) + end +end + +--[[ +vars.zone - table of a zone name. +vars.ref - vec3 ref point, maybe overload for vec2 as well? +vars.alt - boolean, if used, includes altitude in string +vars.metric - boolean, gives distance in km instead of NM. +]] +routines.getBRStringZone = function(vars) + local zone = trigger.misc.getZone( vars.zone ) + local ref = routines.utils.makeVec3(vars.ref, 0) -- turn it into Vec3 if it is not already. + local alt = vars.alt + local metric = vars.metric + if zone then + local vec = {x = zone.point.x - ref.x, y = zone.point.y - ref.y, z = zone.point.z - ref.z} + local dir = routines.utils.getDir(vec, ref) + local dist = routines.utils.get2DDist(zone.point, ref) + if alt then + alt = zone.y + end + return routines.tostringBR(dir, dist, alt, metric) + else + env.info( 'routines.getBRStringZone: error: zone is nil' ) + end +end + +--[[ +vars.units- table of unit names (NOT unitNameTable- maybe this should change). +vars.ref - vec3 ref point, maybe overload for vec2 as well? +vars.alt - boolean, if used, includes altitude in string +vars.metric - boolean, gives distance in km instead of NM. +]] +routines.getBRString = function(vars) + local units = vars.units + local ref = routines.utils.makeVec3(vars.ref, 0) -- turn it into Vec3 if it is not already. + local alt = vars.alt + local metric = vars.metric + local avgPos = routines.getAvgPos(units) + if avgPos then + local vec = {x = avgPos.x - ref.x, y = avgPos.y - ref.y, z = avgPos.z - ref.z} + local dir = routines.utils.getDir(vec, ref) + local dist = routines.utils.get2DDist(avgPos, ref) + if alt then + alt = avgPos.y + end + return routines.tostringBR(dir, dist, alt, metric) + end +end + + +-- Returns the Vec3 coordinates of the average position of the concentration of units most in the heading direction. +--[[ vars for routines.getLeadingPos: +vars.units - table of unit names +vars.heading - direction +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees +]] +routines.getLeadingPos = function(vars) + local units = vars.units + local heading = vars.heading + local radius = vars.radius + if vars.headingDegrees then + heading = routines.utils.toRadian(vars.headingDegrees) + end + + local unitPosTbl = {} + for i = 1, #units do + local unit = Unit.getByName(units[i]) + if unit and unit:isExist() then + unitPosTbl[#unitPosTbl + 1] = unit:getPosition().p + end + end + if #unitPosTbl > 0 then -- one more more units found. + -- first, find the unit most in the heading direction + local maxPos = -math.huge + + local maxPosInd -- maxPos - the furthest in direction defined by heading; maxPosInd = + for i = 1, #unitPosTbl do + local rotatedVec2 = routines.vec.rotateVec2(routines.utils.makeVec2(unitPosTbl[i]), heading) + if (not maxPos) or maxPos < rotatedVec2.x then + maxPos = rotatedVec2.x + maxPosInd = i + end + end + + --now, get all the units around this unit... + local avgPos + if radius then + local maxUnitPos = unitPosTbl[maxPosInd] + local avgx, avgy, avgz, totNum = 0, 0, 0, 0 + for i = 1, #unitPosTbl do + if routines.utils.get2DDist(maxUnitPos, unitPosTbl[i]) <= radius then + avgx = avgx + unitPosTbl[i].x + avgy = avgy + unitPosTbl[i].y + avgz = avgz + unitPosTbl[i].z + totNum = totNum + 1 + end + end + avgPos = { x = avgx/totNum, y = avgy/totNum, z = avgz/totNum} + else + avgPos = unitPosTbl[maxPosInd] + end + + return avgPos + end +end + + +--[[ vars for routines.getLeadingMGRSString: +vars.units - table of unit names +vars.heading - direction +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees +vars.acc - number, 0 to 5. +]] +routines.getLeadingMGRSString = function(vars) + local pos = routines.getLeadingPos(vars) + if pos then + local acc = vars.acc or 5 + return routines.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(pos)), acc) + end +end + +--[[ vars for routines.getLeadingLLString: +vars.units - table of unit names +vars.heading - direction, number +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees +vars.acc - number of digits after decimal point (can be negative) +vars.DMS - boolean, true if you want DMS. +]] +routines.getLeadingLLString = function(vars) + local pos = routines.getLeadingPos(vars) + if pos then + local acc = vars.acc or 3 + local DMS = vars.DMS + local lat, lon = coord.LOtoLL(pos) + return routines.tostringLL(lat, lon, acc, DMS) + end +end + + + +--[[ vars for routines.getLeadingBRString: +vars.units - table of unit names +vars.heading - direction, number +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees +vars.metric - boolean, if true, use km instead of NM. +vars.alt - boolean, if true, include altitude. +vars.ref - vec3/vec2 reference point. +]] +routines.getLeadingBRString = function(vars) + local pos = routines.getLeadingPos(vars) + if pos then + local ref = vars.ref + local alt = vars.alt + local metric = vars.metric + + local vec = {x = pos.x - ref.x, y = pos.y - ref.y, z = pos.z - ref.z} + local dir = routines.utils.getDir(vec, ref) + local dist = routines.utils.get2DDist(pos, ref) + if alt then + alt = pos.y + end + return routines.tostringBR(dir, dist, alt, metric) + end +end + +--[[ vars for routines.message.add + vars.text = 'Hello World' + vars.displayTime = 20 + vars.msgFor = {coa = {'red'}, countries = {'Ukraine', 'Georgia'}, unitTypes = {'A-10C'}} + +]] + +--[[ vars for routines.msgMGRS +vars.units - table of unit names (NOT unitNameTable- maybe this should change). +vars.acc - integer between 0 and 5, inclusive +vars.text - text in the message +vars.displayTime - self explanatory +vars.msgFor - scope +]] +routines.msgMGRS = function(vars) + local units = vars.units + local acc = vars.acc + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = routines.getMGRSString{units = units, acc = acc} + local newText + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end + + routines.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } +end + +--[[ vars for routines.msgLL +vars.units - table of unit names (NOT unitNameTable- maybe this should change) (Yes). +vars.acc - integer, number of numbers after decimal place +vars.DMS - if true, output in degrees, minutes, seconds. Otherwise, output in degrees, minutes. +vars.text - text in the message +vars.displayTime - self explanatory +vars.msgFor - scope +]] +routines.msgLL = function(vars) + local units = vars.units -- technically, I don't really need to do this, but it helps readability. + local acc = vars.acc + local DMS = vars.DMS + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = routines.getLLString{units = units, acc = acc, DMS = DMS} + local newText + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end + + routines.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } + +end + + +--[[ +vars.units- table of unit names (NOT unitNameTable- maybe this should change). +vars.ref - vec3 ref point, maybe overload for vec2 as well? +vars.alt - boolean, if used, includes altitude in string +vars.metric - boolean, gives distance in km instead of NM. +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] +routines.msgBR = function(vars) + local units = vars.units -- technically, I don't really need to do this, but it helps readability. + local ref = vars.ref -- vec2/vec3 will be handled in routines.getBRString + local alt = vars.alt + local metric = vars.metric + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = routines.getBRString{units = units, ref = ref, alt = alt, metric = metric} + local newText + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end + + routines.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } + +end + + +-------------------------------------------------------------------------------------------- +-- basically, just sub-types of routines.msgBR... saves folks the work of getting the ref point. +--[[ +vars.units- table of unit names (NOT unitNameTable- maybe this should change). +vars.ref - string red, blue +vars.alt - boolean, if used, includes altitude in string +vars.metric - boolean, gives distance in km instead of NM. +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] +routines.msgBullseye = function(vars) + if string.lower(vars.ref) == 'red' then + vars.ref = routines.DBs.missionData.bullseye.red + routines.msgBR(vars) + elseif string.lower(vars.ref) == 'blue' then + vars.ref = routines.DBs.missionData.bullseye.blue + routines.msgBR(vars) + end +end + +--[[ +vars.units- table of unit names (NOT unitNameTable- maybe this should change). +vars.ref - unit name of reference point +vars.alt - boolean, if used, includes altitude in string +vars.metric - boolean, gives distance in km instead of NM. +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] + +routines.msgBRA = function(vars) + if Unit.getByName(vars.ref) then + vars.ref = Unit.getByName(vars.ref):getPosition().p + if not vars.alt then + vars.alt = true + end + routines.msgBR(vars) + end +end +-------------------------------------------------------------------------------------------- + +--[[ vars for routines.msgLeadingMGRS: +vars.units - table of unit names +vars.heading - direction +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees (optional) +vars.acc - number, 0 to 5. +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] +routines.msgLeadingMGRS = function(vars) + local units = vars.units -- technically, I don't really need to do this, but it helps readability. + local heading = vars.heading + local radius = vars.radius + local headingDegrees = vars.headingDegrees + local acc = vars.acc + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = routines.getLeadingMGRSString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc} + local newText + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end + + routines.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } + + +end +--[[ vars for routines.msgLeadingLL: +vars.units - table of unit names +vars.heading - direction, number +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees (optional) +vars.acc - number of digits after decimal point (can be negative) +vars.DMS - boolean, true if you want DMS. (optional) +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] +routines.msgLeadingLL = function(vars) + local units = vars.units -- technically, I don't really need to do this, but it helps readability. + local heading = vars.heading + local radius = vars.radius + local headingDegrees = vars.headingDegrees + local acc = vars.acc + local DMS = vars.DMS + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = routines.getLeadingLLString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc, DMS = DMS} + local newText + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end + + routines.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } + +end + +--[[ +vars.units - table of unit names +vars.heading - direction, number +vars.radius - number +vars.headingDegrees - boolean, switches heading to degrees (optional) +vars.metric - boolean, if true, use km instead of NM. (optional) +vars.alt - boolean, if true, include altitude. (optional) +vars.ref - vec3/vec2 reference point. +vars.text - text of the message +vars.displayTime +vars.msgFor - scope +]] +routines.msgLeadingBR = function(vars) + local units = vars.units -- technically, I don't really need to do this, but it helps readability. + local heading = vars.heading + local radius = vars.radius + local headingDegrees = vars.headingDegrees + local metric = vars.metric + local alt = vars.alt + local ref = vars.ref -- vec2/vec3 will be handled in routines.getBRString + local text = vars.text + local displayTime = vars.displayTime + local msgFor = vars.msgFor + + local s = routines.getLeadingBRString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, metric = metric, alt = alt, ref = ref} + local newText + if string.find(text, '%%s') then -- look for %s + newText = string.format(text, s) -- insert the coordinates into the message + else -- else, just append to the end. + newText = text .. s + end + + routines.message.add{ + text = newText, + displayTime = displayTime, + msgFor = msgFor + } +end + + +function spairs(t, order) + -- collect the keys + local keys = {} + for k in pairs(t) do keys[#keys+1] = k end + + -- if order function given, sort by it by passing the table and keys a, b, + -- otherwise just sort the keys + if order then + table.sort(keys, function(a,b) return order(t, a, b) end) + else + table.sort(keys) + end + + -- return the iterator function + local i = 0 + return function() + i = i + 1 + if keys[i] then + return keys[i], t[keys[i]] + end + end +end + + +function routines.IsPartOfGroupInZones( CargoGroup, LandingZones ) +--trace.f() + + local CurrentZoneID = nil + + if CargoGroup then + local CargoUnits = CargoGroup:getUnits() + for CargoUnitID, CargoUnit in pairs( CargoUnits ) do + if CargoUnit and CargoUnit:getLife() >= 1.0 then + CurrentZoneID = routines.IsUnitInZones( CargoUnit, LandingZones ) + if CurrentZoneID then + break + end + end + end + end + +--trace.r( "", "", { CurrentZoneID } ) + return CurrentZoneID +end + + + +function routines.IsUnitInZones( TransportUnit, LandingZones ) +--trace.f("", "routines.IsUnitInZones" ) + + local TransportZoneResult = nil + local TransportZonePos = nil + local TransportZone = nil + + -- fill-up some local variables to support further calculations to determine location of units within the zone. + if TransportUnit then + local TransportUnitPos = TransportUnit:getPosition().p + if type( LandingZones ) == "table" then + for LandingZoneID, LandingZoneName in pairs( LandingZones ) do + TransportZone = trigger.misc.getZone( LandingZoneName ) + if TransportZone then + TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} + if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then + TransportZoneResult = LandingZoneID + break + end + end + end + else + TransportZone = trigger.misc.getZone( LandingZones ) + TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} + if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then + TransportZoneResult = 1 + end + end + if TransportZoneResult then + --trace.i( "routines", "TransportZone:" .. TransportZoneResult ) + else + --trace.i( "routines", "TransportZone:nil logic" ) + end + return TransportZoneResult + else + --trace.i( "routines", "TransportZone:nil hard" ) + return nil + end +end + +function routines.IsUnitNearZonesRadius( TransportUnit, LandingZones, ZoneRadius ) +--trace.f("", "routines.IsUnitInZones" ) + + local TransportZoneResult = nil + local TransportZonePos = nil + local TransportZone = nil + + -- fill-up some local variables to support further calculations to determine location of units within the zone. + if TransportUnit then + local TransportUnitPos = TransportUnit:getPosition().p + if type( LandingZones ) == "table" then + for LandingZoneID, LandingZoneName in pairs( LandingZones ) do + TransportZone = trigger.misc.getZone( LandingZoneName ) + if TransportZone then + TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} + if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= ZoneRadius ) then + TransportZoneResult = LandingZoneID + break + end + end + end + else + TransportZone = trigger.misc.getZone( LandingZones ) + TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} + if ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= ZoneRadius ) then + TransportZoneResult = 1 + end + end + if TransportZoneResult then + --trace.i( "routines", "TransportZone:" .. TransportZoneResult ) + else + --trace.i( "routines", "TransportZone:nil logic" ) + end + return TransportZoneResult + else + --trace.i( "routines", "TransportZone:nil hard" ) + return nil + end +end + + +function routines.IsStaticInZones( TransportStatic, LandingZones ) +--trace.f() + + local TransportZoneResult = nil + local TransportZonePos = nil + local TransportZone = nil + + -- fill-up some local variables to support further calculations to determine location of units within the zone. + local TransportStaticPos = TransportStatic:getPosition().p + if type( LandingZones ) == "table" then + for LandingZoneID, LandingZoneName in pairs( LandingZones ) do + TransportZone = trigger.misc.getZone( LandingZoneName ) + if TransportZone then + TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} + if ((( TransportStaticPos.x - TransportZonePos.x)^2 + (TransportStaticPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then + TransportZoneResult = LandingZoneID + break + end + end + end + else + TransportZone = trigger.misc.getZone( LandingZones ) + TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z} + if ((( TransportStaticPos.x - TransportZonePos.x)^2 + (TransportStaticPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then + TransportZoneResult = 1 + end + end + +--trace.r( "", "", { TransportZoneResult } ) + return TransportZoneResult +end + + +function routines.IsUnitInRadius( CargoUnit, ReferencePosition, Radius ) +--trace.f() + + local Valid = true + + -- fill-up some local variables to support further calculations to determine location of units within the zone. + local CargoPos = CargoUnit:getPosition().p + local ReferenceP = ReferencePosition.p + + if (((CargoPos.x - ReferenceP.x)^2 + (CargoPos.z - ReferenceP.z)^2)^0.5 <= Radius) then + else + Valid = false + end + + return Valid +end + +function routines.IsPartOfGroupInRadius( CargoGroup, ReferencePosition, Radius ) +--trace.f() + + local Valid = true + + Valid = routines.ValidateGroup( CargoGroup, "CargoGroup", Valid ) + + -- fill-up some local variables to support further calculations to determine location of units within the zone + local CargoUnits = CargoGroup:getUnits() + for CargoUnitId, CargoUnit in pairs( CargoUnits ) do + local CargoUnitPos = CargoUnit:getPosition().p +-- env.info( 'routines.IsPartOfGroupInRadius: CargoUnitPos.x = ' .. CargoUnitPos.x .. ' CargoUnitPos.z = ' .. CargoUnitPos.z ) + local ReferenceP = ReferencePosition.p +-- env.info( 'routines.IsPartOfGroupInRadius: ReferenceGroupPos.x = ' .. ReferenceGroupPos.x .. ' ReferenceGroupPos.z = ' .. ReferenceGroupPos.z ) + + if ((( CargoUnitPos.x - ReferenceP.x)^2 + (CargoUnitPos.z - ReferenceP.z)^2)^0.5 <= Radius) then + else + Valid = false + break + end + end + + return Valid +end + + +function routines.ValidateString( Variable, VariableName, Valid ) +--trace.f() + + if type( Variable ) == "string" then + if Variable == "" then + error( "routines.ValidateString: error: " .. VariableName .. " must be filled out!" ) + Valid = false + end + else + error( "routines.ValidateString: error: " .. VariableName .. " is not a string." ) + Valid = false + end + +--trace.r( "", "", { Valid } ) + return Valid +end + +function routines.ValidateNumber( Variable, VariableName, Valid ) +--trace.f() + + if type( Variable ) == "number" then + else + error( "routines.ValidateNumber: error: " .. VariableName .. " is not a number." ) + Valid = false + end + +--trace.r( "", "", { Valid } ) + return Valid + +end + +function routines.ValidateGroup( Variable, VariableName, Valid ) +--trace.f() + + if Variable == nil then + error( "routines.ValidateGroup: error: " .. VariableName .. " is a nil value!" ) + Valid = false + end + +--trace.r( "", "", { Valid } ) + return Valid +end + +function routines.ValidateZone( LandingZones, VariableName, Valid ) +--trace.f() + + if LandingZones == nil then + error( "routines.ValidateGroup: error: " .. VariableName .. " is a nil value!" ) + Valid = false + end + + if type( LandingZones ) == "table" then + for LandingZoneID, LandingZoneName in pairs( LandingZones ) do + if trigger.misc.getZone( LandingZoneName ) == nil then + error( "routines.ValidateGroup: error: Zone " .. LandingZoneName .. " does not exist!" ) + Valid = false + break + end + end + else + if trigger.misc.getZone( LandingZones ) == nil then + error( "routines.ValidateGroup: error: Zone " .. LandingZones .. " does not exist!" ) + Valid = false + end + end + +--trace.r( "", "", { Valid } ) + return Valid +end + +function routines.ValidateEnumeration( Variable, VariableName, Enum, Valid ) +--trace.f() + + local ValidVariable = false + + for EnumId, EnumData in pairs( Enum ) do + if Variable == EnumData then + ValidVariable = true + break + end + end + + if ValidVariable then + else + error( 'TransportValidateEnum: " .. VariableName .. " is not a valid type.' .. Variable ) + Valid = false + end + +--trace.r( "", "", { Valid } ) + return Valid +end + +function routines.getGroupRoute(groupIdent, task) -- same as getGroupPoints but returns speed and formation type along with vec2 of point} + -- refactor to search by groupId and allow groupId and groupName as inputs + local gpId = groupIdent + if type(groupIdent) == 'string' and not tonumber(groupIdent) then + gpId = _DATABASE.Templates.Groups[groupIdent].groupId + end + + for coa_name, coa_data in pairs(env.mission.coalition) do + if (coa_name == 'red' or coa_name == 'blue') and type(coa_data) == 'table' then + if coa_data.country then --there is a country table + for cntry_id, cntry_data in pairs(coa_data.country) do + for obj_type_name, obj_type_data in pairs(cntry_data) do + if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" then -- only these types have points + if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! + for group_num, group_data in pairs(obj_type_data.group) do + if group_data and group_data.groupId == gpId then -- this is the group we are looking for + if group_data.route and group_data.route.points and #group_data.route.points > 0 then + local points = {} + + for point_num, point in pairs(group_data.route.points) do + local routeData = {} + if not point.point then + routeData.x = point.x + routeData.y = point.y + else + routeData.point = point.point --it's possible that the ME could move to the point = Vec2 notation. + end + routeData.form = point.action + routeData.speed = point.speed + routeData.alt = point.alt + routeData.alt_type = point.alt_type + routeData.airdromeId = point.airdromeId + routeData.helipadId = point.helipadId + routeData.type = point.type + routeData.action = point.action + if task then + routeData.task = point.task + end + points[point_num] = routeData + end + + return points + end + return + end --if group_data and group_data.name and group_data.name == 'groupname' + end --for group_num, group_data in pairs(obj_type_data.group) do + end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then + end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then + end --for obj_type_name, obj_type_data in pairs(cntry_data) do + end --for cntry_id, cntry_data in pairs(coa_data.country) do + end --if coa_data.country then --there is a country table + end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then + end --for coa_name, coa_data in pairs(mission.coalition) do +end + +routines.ground.patrolRoute = function(vars) + + + local tempRoute = {} + local useRoute = {} + local gpData = vars.gpData + if type(gpData) == 'string' then + gpData = Group.getByName(gpData) + end + + local useGroupRoute + if not vars.useGroupRoute then + useGroupRoute = vars.gpData + else + useGroupRoute = vars.useGroupRoute + end + local routeProvided = false + if not vars.route then + if useGroupRoute then + tempRoute = routines.getGroupRoute(useGroupRoute) + end + else + useRoute = vars.route + local posStart = routines.getLeadPos(gpData) + useRoute[1] = routines.ground.buildWP(posStart, useRoute[1].action, useRoute[1].speed) + routeProvided = true + end + + + local overRideSpeed = vars.speed or 'default' + local pType = vars.pType + local offRoadForm = vars.offRoadForm or 'default' + local onRoadForm = vars.onRoadForm or 'default' + + if routeProvided == false and #tempRoute > 0 then + local posStart = routines.getLeadPos(gpData) + + + useRoute[#useRoute + 1] = routines.ground.buildWP(posStart, offRoadForm, overRideSpeed) + for i = 1, #tempRoute do + local tempForm = tempRoute[i].action + local tempSpeed = tempRoute[i].speed + + if offRoadForm == 'default' then + tempForm = tempRoute[i].action + end + if onRoadForm == 'default' then + onRoadForm = 'On Road' + end + if (string.lower(tempRoute[i].action) == 'on road' or string.lower(tempRoute[i].action) == 'onroad' or string.lower(tempRoute[i].action) == 'on_road') then + tempForm = onRoadForm + 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, "Message", MsgTime, MsgName ):ToCoalition( coalition.side.RED ):ToCoalition( coalition.side.BLUE ) +end + +function MessageToRed( MsgText, MsgTime, MsgName ) +--trace.f() + + MESSAGE:New( MsgText, "To Red Coalition", MsgTime, MsgName ):ToCoalition( coalition.side.RED ) +end + +function MessageToBlue( MsgText, MsgTime, MsgName ) +--trace.f() + + MESSAGE:New( MsgText, "To Blue Coalition", MsgTime, MsgName ):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' )) + +--- BASE classes. +-- +-- @{#BASE} class +-- ============== +-- The @{#BASE} class is the super class for most of the classes defined within MOOSE. +-- +-- It handles: +-- +-- * The construction and inheritance of child classes. +-- * The tracing of objects during mission execution within the DCS.log file (under saved games folder). +-- +-- Note: Normally you would not use the BASE class unless you are extending the MOOSE framework with new classes. +-- +-- BASE Trace functionality +-- ======================== +-- The BASE class contains trace methods to trace progress within a mission execution of a certain object. +-- Note that these trace methods are inherited by each MOOSE class interiting BASE. +-- As such, each object created from derived class from BASE can use the tracing functions to trace its execution. +-- +-- Trace a function call +-- --------------------- +-- There are basically 3 types of tracing methods available within BASE: +-- +-- * @{#BASE.F}: Trace the beginning of a function and its given parameters. +-- * @{#BASE.T}: Trace further logic within a function giving optional variables or parameters. +-- * @{#BASE.E}: Trace an execption within a function giving optional variables or parameters. An exception will always be traced. +-- +-- 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. +-- +-- BASE Inheritance support +-- ======================== +-- The following methods are available to support inheritance: +-- +-- * @{#BASE.Inherit}: Inherits from a class. +-- * @{#BASE.Inherited}: Returns the parent class from the class. +-- +-- Future +-- ====== +-- Further methods may be added to BASE whenever there is a need to make "overall" functions available within MOOSE. +-- +-- ==== +-- +-- @module Base +-- @author FlightControl + +Include.File( "Routines" ) + +local _TraceOn = true +local _TraceLevel = 1 +local _TraceClass = { + --DATABASE = true, + --SEAD = true, + --DESTROYBASETASK = true, + --MOVEMENT = true, + --SPAWN = true, + --STAGE = true, + --ZONE = true, + --GROUP = true, + --UNIT = true, + --CLIENT = true, + --CARGO = true, + --CARGO_GROUP = true, + --CARGO_PACKAGE = true, + --CARGO_SLINGLOAD = true, + --CARGO_ZONE = true, + --CLEANUP = true, + --MENU_CLIENT = true, + --MENU_CLIENT_COMMAND = true, + --ESCORT = true, + } +local _TraceClassMethod = {} + +--- The BASE Class +-- @type BASE +-- @field ClassName The name of the class. +-- @field ClassID The ID number of the class. +BASE = { + ClassName = "BASE", + ClassID = 0, + Events = {} +} + +--- The Formation Class +-- @type FORMATION +-- @field Cone A cone formation. +FORMATION = { + Cone = "Cone" +} + + + +--- The base constructor. This is the top top class of all classed defined within the MOOSE. +-- Any new class needs to be derived from this class for proper inheritance. +-- @param #BASE self +-- @return #BASE The new instance of the BASE class. +-- @usage +-- function TASK:New() +-- +-- local self = BASE:Inherit( self, BASE:New() ) +-- +-- -- assign Task default values during construction +-- self.TaskBriefing = "Task: No Task." +-- self.Time = timer.getTime() +-- self.ExecuteStage = _TransportExecuteStage.NONE +-- +-- return self +-- end +-- @todo need to investigate if the deepCopy is really needed... Don't think so. +function BASE:New() + local Child = routines.utils.deepCopy( self ) + local Parent = {} + setmetatable( Child, Parent ) + Child.__index = Child + self.ClassID = self.ClassID + 1 + Child.ClassID = self.ClassID + --Child.AddEvent( Child, S_EVENT_BIRTH, Child.EventBirth ) + return Child +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 ) + if Child ~= nil then + setmetatable( Child, Parent ) + Child.__index = Child + end + --Child.ClassName = Child.ClassName .. '.' .. Child.ClassID + self:T( 'Inherited from ' .. Parent.ClassName ) + return Child +end + +--- This is the worker method to retrieve the Parent class. +-- @param #BASE self +-- @param #BASE Child is the Child class from which the Parent class needs to be retrieved. +-- @return #BASE +function BASE:Inherited( Child ) + local Parent = getmetatable( Child ) +-- env.info('Inherited class of ' .. Child.ClassName .. ' is ' .. Parent.ClassName ) + return Parent +end + +--- Get the ClassName + ClassID of the class instance. +-- The ClassName + ClassID is formatted as '%s#%09d'. +-- @param #BASE self +-- @return #string The ClassName + ClassID of the class instance. +function BASE:GetClassNameAndID() + return string.format( '%s#%09d', self:GetClassName(), self:GetClassID() ) +end + +--- Get the ClassName of the class instance. +-- @param #BASE self +-- @return #string The ClassName of the class instance. +function BASE:GetClassName() + return self.ClassName +end + +--- Get the ClassID of the class instance. +-- @param #BASE self +-- @return #string The ClassID of the class instance. +function BASE:GetClassID() + return self.ClassID +end + +--- Set a new listener for the class. +-- @param self +-- @param DCSTypes#Event Event +-- @param #function EventFunction +-- @return #BASE +function BASE:AddEvent( Event, EventFunction ) + self:F( Event ) + + self.Events[#self.Events+1] = {} + self.Events[#self.Events].Event = Event + self.Events[#self.Events].EventFunction = EventFunction + self.Events[#self.Events].EventEnabled = false + + return self +end + +--- Returns the event dispatcher +-- @param #BASE self +-- @return Event#EVENT +function BASE:Event() + + return _EVENTDISPATCHER +end + + + + + +--- Enable the event listeners for the class. +-- @param #BASE self +-- @return #BASE +function BASE:EnableEvents() + self:F( #self.Events ) + + for EventID, Event in pairs( self.Events ) do + Event.Self = self + Event.EventEnabled = true + end + self.Events.Handler = world.addEventHandler( self ) + + return self +end + + +--- Disable the event listeners for the class. +-- @param #BASE self +-- @return #BASE +function BASE:DisableEvents() + self:F() + + world.removeEventHandler( self ) + for EventID, Event in pairs( self.Events ) do + Event.Self = nil + Event.EventEnabled = false + end + + return self +end + + +local BaseEventCodes = { + "S_EVENT_SHOT", + "S_EVENT_HIT", + "S_EVENT_TAKEOFF", + "S_EVENT_LAND", + "S_EVENT_CRASH", + "S_EVENT_EJECTION", + "S_EVENT_REFUELING", + "S_EVENT_DEAD", + "S_EVENT_PILOT_DEAD", + "S_EVENT_BASE_CAPTURED", + "S_EVENT_MISSION_START", + "S_EVENT_MISSION_END", + "S_EVENT_TOOK_CONTROL", + "S_EVENT_REFUELING_STOP", + "S_EVENT_BIRTH", + "S_EVENT_HUMAN_FAILURE", + "S_EVENT_ENGINE_STARTUP", + "S_EVENT_ENGINE_SHUTDOWN", + "S_EVENT_PLAYER_ENTER_UNIT", + "S_EVENT_PLAYER_LEAVE_UNIT", + "S_EVENT_PLAYER_COMMENT", + "S_EVENT_SHOOTING_START", + "S_EVENT_SHOOTING_END", + "S_EVENT_MAX", +} + +--onEvent( {[1]="S_EVENT_BIRTH",[2]={["subPlace"]=5,["time"]=0,["initiator"]={["id_"]=16884480,},["place"]={["id_"]=5000040,},["id"]=15,["IniUnitName"]="US F-15C@RAMP-Air Support Mountains#001-01",},} +-- Event = { +-- id = enum world.event, +-- time = Time, +-- initiator = Unit, +-- target = Unit, +-- place = Unit, +-- subPlace = enum world.BirthPlace, +-- weapon = Weapon +-- } + +--- Creation of a Birth Event. +-- @param #BASE self +-- @param DCSTypes#Time EventTime The time stamp of the event. +-- @param DCSObject#Object Initiator The initiating object of the event. +-- @param #string IniUnitName The initiating unit name. +-- @param place +-- @param subplace +function BASE:CreateEventBirth( EventTime, Initiator, IniUnitName, place, subplace ) + self:F( { EventTime, Initiator, IniUnitName, place, subplace } ) + + local Event = { + id = world.event.S_EVENT_BIRTH, + time = EventTime, + initiator = Initiator, + IniUnitName = IniUnitName, + place = place, + subplace = subplace + } + + world.onEvent( Event ) +end + +--- Creation of a Crash Event. +-- @param #BASE self +-- @param DCSTypes#Time EventTime The time stamp of the event. +-- @param DCSObject#Object Initiator The initiating object of the event. +function BASE:CreateEventCrash( EventTime, Initiator ) + self:F( { EventTime, Initiator } ) + + local Event = { + id = world.event.S_EVENT_CRASH, + time = EventTime, + initiator = Initiator, + } + + world.onEvent( Event ) +end + +-- TODO: Complete DCSTypes#Event structure. +--- The main event handling function... This function captures all events generated for the class. +-- @param #BASE self +-- @param DCSTypes#Event event +function BASE:onEvent(event) + --self:F( { BaseEventCodes[event.id], event } ) + + if self then + for EventID, EventObject in pairs( self.Events ) do + if EventObject.EventEnabled then + --env.info( 'onEvent Table EventObject.Self = ' .. tostring(EventObject.Self) ) + --env.info( 'onEvent event.id = ' .. tostring(event.id) ) + --env.info( 'onEvent EventObject.Event = ' .. tostring(EventObject.Event) ) + if event.id == EventObject.Event then + if self == EventObject.Self then + if event.initiator and event.initiator:isExist() then + event.IniUnitName = event.initiator:getName() + end + if event.target and event.target:isExist() then + event.TgtUnitName = event.target:getName() + end + --self:T( { BaseEventCodes[event.id], event } ) + --EventObject.EventFunction( self, event ) + end + end + end + end + end +end + +-- Trace section + +-- Log a trace (only shown when trace is on) +-- TODO: Make trace function using variable parameters. + +--- Set trace level +-- @param #BASE self +-- @param #number Level +function BASE:TraceLevel( Level ) + _TraceLevel = Level + self:E( "Tracing level " .. Level ) +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. Must be at the beginning of the function logic. +-- @param #BASE self +-- @param Arguments A #table or any field. +function BASE:F( Arguments ) + + if _TraceOn and ( _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName] ) then + + local DebugInfoCurrent = debug.getinfo( 2, "nl" ) + local DebugInfoFrom = debug.getinfo( 3, "l" ) + + local Function = "function" + if DebugInfoCurrent.name then + Function = DebugInfoCurrent.name + end + + if _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName].Method[Function] then + local LineCurrent = DebugInfoCurrent.currentline + 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 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 _TraceLevel >= 2 then + self:F( Arguments ) + 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 _TraceLevel >= 3 then + self:F( Arguments ) + end + +end + +--- Trace a function logic. Can be anywhere within the function logic. +-- @param #BASE self +-- @param Arguments A #table or any field. +function BASE:T( Arguments ) + + if _TraceOn and ( _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName] ) then + + local DebugInfoCurrent = debug.getinfo( 2, "nl" ) + local DebugInfoFrom = debug.getinfo( 3, "l" ) + + local Function = "function" + if DebugInfoCurrent.name then + Function = DebugInfoCurrent.name + end + + if _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName].Method[Function] then + local LineCurrent = DebugInfoCurrent.currentline + 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 2. Can be anywhere within the function logic. +-- @param #BASE self +-- @param Arguments A #table or any field. +function BASE:T2( Arguments ) + + if _TraceLevel >= 2 then + self:T( Arguments ) + 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 _TraceLevel >= 3 then + self:T( Arguments ) + end + +end + +--- Log an exception which will be traced always. Can be anywhere within the function logic. +-- @param #BASE self +-- @param Arguments A #table or any field. +function BASE:E( Arguments ) + + local DebugInfoCurrent = debug.getinfo( 2, "nl" ) + local DebugInfoFrom = debug.getinfo( 3, "l" ) + + local Function = "function" + if DebugInfoCurrent.name then + Function = DebugInfoCurrent.name + end + + local LineCurrent = DebugInfoCurrent.currentline + local LineFrom = -1 + if DebugInfoFrom then + LineFrom = DebugInfoFrom.currentline + end + + env.info( string.format( "%6d(%6d)/%1s:%20s%05d.%s(%s)" , LineCurrent, LineFrom, "E", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) +end + + + +--- Models time events calling event handing functions. +-- +-- @{SCHEDULER} class +-- =================== +-- The @{SCHEDULER} class models time events calling given event handling functions. +-- +-- SCHEDULER constructor +-- ===================== +-- The SCHEDULER class is quite easy to use: +-- +-- * @{#SCHEDULER.New}: Setup a new scheduler and start it with the specified parameters. +-- +-- SCHEDULER timer methods +-- ======================= +-- The SCHEDULER can be stopped and restarted with the following methods: +-- +-- * @{#SCHEDULER.Start}: (Re-)Start the scheduler. +-- * @{#SCHEDULER.Start}: Stop the scheduler. +-- +-- @module Scheduler +-- @author FlightControl + +Include.File( "Routines" ) +Include.File( "Base" ) + + +--- The SCHEDULER class +-- @type SCHEDULER +-- @extends Base#BASE +SCHEDULER = { + ClassName = "SCHEDULER", +} + + +--- Constructor. +-- @param #SCHEDULER self +-- @param #table TimeEventObject Specified for which Moose object the timer is setup. If a value of nil is provided, a scheduler will be setup without an object reference. +-- @param #function TimeEventFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in TimeEventFunctionArguments. +-- @param #table TimeEventFunctionArguments Optional arguments that can be given as part of scheduler. The arguments need to be given as a table { param1, param 2, ... }. +-- @param #number StartSeconds Specifies the amount of seconds that will be waited before the scheduling is started, and the event function is called. +-- @param #number RepeatSecondsInterval Specifies the interval in seconds when the scheduler will call the event function. +-- @param #number RandomizationFactor Specifies a randomization factor between 0 and 1 to randomize the RepeatSecondsInterval. +-- @param #number StopSeconds Specifies the amount of seconds when the scheduler will be stopped. +-- @return #SCHEDULER self +function SCHEDULER:New( TimeEventObject, TimeEventFunction, TimeEventFunctionArguments, StartSeconds, RepeatSecondsInterval, RandomizationFactor, StopSeconds ) + local self = BASE:Inherit( self, BASE:New() ) + self:F2( { TimeEventObject, TimeEventFunction, TimeEventFunctionArguments, StartSeconds, RepeatSecondsInterval, RandomizationFactor, StopSeconds } ) + + self.TimeEventObject = TimeEventObject + self.TimeEventFunction = TimeEventFunction + self.TimeEventFunctionArguments = TimeEventFunctionArguments + self.StartSeconds = StartSeconds + + if RepeatSecondsInterval then + self.RepeatSecondsInterval = RepeatSecondsInterval + else + self.RepeatSecondsInterval = 0 + end + + if RandomizationFactor then + self.RandomizationFactor = RandomizationFactor + else + self.RandomizationFactor = 0 + end + + if StopSeconds then + self.StopSeconds = StopSeconds + end + + self.Repeat = false + + self.StartTime = timer.getTime() + + self:Start() + + return self +end + +--- (Re-)Starts the scheduler. +-- @param #SCHEDULER self +-- @return #SCHEDULER self +function SCHEDULER:Start() + self:F2( self.TimeEventObject ) + + self.Repeat = true + timer.scheduleFunction( self._Scheduler, self, timer.getTime() + self.StartSeconds + .01 ) + + return self +end + +--- Stops the scheduler. +-- @param #SCHEDULER self +-- @return #SCHEDULER self +function SCHEDULER:Stop() + self:F2( self.TimeEventObject ) + + self.Repeat = false + + return self +end + +-- Private Functions + +function SCHEDULER:_Scheduler() + self:F2( self.TimeEventFunctionArguments ) + + local ErrorHandler = function( errmsg ) + + env.info( "Error in SCHEDULER function:" .. errmsg ) + env.info( debug.traceback() ) + + return errmsg + end + + local Status, Result + if self.TimeEventObject then + Status, Result = xpcall( function() return self.TimeEventFunction( self.TimeEventObject, unpack( self.TimeEventFunctionArguments ) ) end, ErrorHandler ) + else + Status, Result = xpcall( function() return self.TimeEventFunction( unpack( self.TimeEventFunctionArguments ) ) end, ErrorHandler ) + end + + self:T( { Status, Result } ) + + if Status and Status == true and Result and Result == true then + if self.Repeat and ( not self.StopSeconds or ( self.StopSeconds and timer.getTime() <= self.StartTime + self.StopSeconds ) ) then + timer.scheduleFunction( + self._Scheduler, + self, + timer.getTime() + self.RepeatSecondsInterval + math.random( - ( self.RandomizationFactor * self.RepeatSecondsInterval / 2 ), ( self.RandomizationFactor * self.RepeatSecondsInterval / 2 ) ) + 0.01 + ) + end + end + +end + + + + + + + + +--- The EVENT class models an efficient event handling process between other classes and its units, weapons. +-- @module Event +-- @author FlightControl + +Include.File( "Routines" ) +Include.File( "Base" ) + +--- The EVENT structure +-- @type EVENT +-- @field #EVENT.Events Events +EVENT = { + ClassName = "EVENT", + ClassID = 0, +} + +local _EVENTCODES = { + "S_EVENT_SHOT", + "S_EVENT_HIT", + "S_EVENT_TAKEOFF", + "S_EVENT_LAND", + "S_EVENT_CRASH", + "S_EVENT_EJECTION", + "S_EVENT_REFUELING", + "S_EVENT_DEAD", + "S_EVENT_PILOT_DEAD", + "S_EVENT_BASE_CAPTURED", + "S_EVENT_MISSION_START", + "S_EVENT_MISSION_END", + "S_EVENT_TOOK_CONTROL", + "S_EVENT_REFUELING_STOP", + "S_EVENT_BIRTH", + "S_EVENT_HUMAN_FAILURE", + "S_EVENT_ENGINE_STARTUP", + "S_EVENT_ENGINE_SHUTDOWN", + "S_EVENT_PLAYER_ENTER_UNIT", + "S_EVENT_PLAYER_LEAVE_UNIT", + "S_EVENT_PLAYER_COMMENT", + "S_EVENT_SHOOTING_START", + "S_EVENT_SHOOTING_END", + "S_EVENT_MAX", +} + +--- The Event structure +-- @type EVENTDATA +-- @field id +-- @field initiator +-- @field target +-- @field weapon +-- @field IniDCSUnit +-- @field IniDCSUnitName +-- @field IniDCSGroup +-- @field IniDCSGroupName +-- @field TgtDCSUnit +-- @field TgtDCSUnitName +-- @field TgtDCSGroup +-- @field TgtDCSGroupName +-- @field Weapon +-- @field WeaponName +-- @field WeaponTgtDCSUnit + +--- The Events structure +-- @type EVENT.Events +-- @field #number IniUnit + +function EVENT:New() + local self = BASE:Inherit( self, BASE:New() ) + self:F2() + self.EventHandler = world.addEventHandler( self ) + return self +end + +function EVENT:EventText( EventID ) + + local EventText = _EVENTCODES[EventID] + + return EventText +end + + +--- Initializes the Events structure for the event +-- @param #EVENT self +-- @param DCSWorld#world.event EventID +-- @param #string EventClass +-- @return #EVENT.Events +function EVENT:Init( EventID, EventClass ) + self:F3( { _EVENTCODES[EventID], EventClass } ) + if not self.Events[EventID] then + self.Events[EventID] = {} + end + if not self.Events[EventID][EventClass] then + self.Events[EventID][EventClass] = {} + end + return self.Events[EventID][EventClass] +end + + +--- Create an OnDead event handler for a group +-- @param #EVENT self +-- @param #table EventTemplate +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param EventSelf The self instance of the class for which the event is. +-- @param #function OnEventFunction +-- @return #EVENT +function EVENT:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, OnEventFunction ) + self:F2( EventTemplate.name ) + + for EventUnitID, EventUnit in pairs( EventTemplate.units ) do + OnEventFunction( self, EventUnit.name, EventFunction, EventSelf ) + end + return self +end + +--- Set a new listener for an S_EVENT_X event independent from a unit or a weapon. +-- @param #EVENT self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @param EventID +-- @return #EVENT +function EVENT:OnEventGeneric( EventFunction, EventSelf, EventID ) + self:F2( { EventID } ) + + local Event = self:Init( EventID, EventSelf:GetClassNameAndID() ) + Event.EventFunction = EventFunction + Event.EventSelf = EventSelf + return self +end + + +--- Set a new listener for an S_EVENT_X event +-- @param #EVENT self +-- @param #string EventDCSUnitName +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @param EventID +-- @return #EVENT +function EVENT:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, EventID ) + self:F2( EventDCSUnitName ) + + local Event = self:Init( EventID, EventSelf:GetClassNameAndID() ) + if not Event.IniUnit then + Event.IniUnit = {} + end + Event.IniUnit[EventDCSUnitName] = {} + Event.IniUnit[EventDCSUnitName].EventFunction = EventFunction + Event.IniUnit[EventDCSUnitName].EventSelf = EventSelf + return self +end + + +--- Create an OnBirth event handler for a group +-- @param #EVENT self +-- @param Group#GROUP EventGroup +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnBirthForTemplate( EventTemplate, EventFunction, EventSelf ) + self:F2( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnBirthForUnit ) + + return self +end + +--- Set a new listener for an S_EVENT_BIRTH event, and registers the unit born. +-- @param #EVENT self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf +-- @return #EVENT +function EVENT:OnBirth( EventFunction, EventSelf ) + self:F2() + + self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_BIRTH ) + + return self +end + +--- Set a new listener for an S_EVENT_BIRTH event. +-- @param #EVENT self +-- @param #string EventDCSUnitName The id of the unit for the event to be handled. +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf +-- @return #EVENT +function EVENT:OnBirthForUnit( EventDCSUnitName, EventFunction, EventSelf ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_BIRTH ) + + return self +end + +--- Create an OnCrash event handler for a group +-- @param #EVENT self +-- @param Group#GROUP EventGroup +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnCrashForTemplate( EventTemplate, EventFunction, EventSelf ) + self:F2( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnCrashForUnit ) + + return self +end + +--- Set a new listener for an S_EVENT_CRASH event. +-- @param #EVENT self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf +-- @return #EVENT +function EVENT:OnCrash( EventFunction, EventSelf ) + self:F2() + + self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_CRASH ) + + return self +end + +--- Set a new listener for an S_EVENT_CRASH event. +-- @param #EVENT self +-- @param #string EventDCSUnitName +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnCrashForUnit( EventDCSUnitName, EventFunction, EventSelf ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_CRASH ) + + return self +end + +--- Create an OnDead event handler for a group +-- @param #EVENT self +-- @param Group#GROUP EventGroup +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnDeadForTemplate( EventTemplate, EventFunction, EventSelf ) + self:F2( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnDeadForUnit ) + + return self +end + +--- Set a new listener for an S_EVENT_DEAD event. +-- @param #EVENT self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf +-- @return #EVENT +function EVENT:OnDead( EventFunction, EventSelf ) + self:F2() + + self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_DEAD ) + + return self +end + + +--- Set a new listener for an S_EVENT_DEAD event. +-- @param #EVENT self +-- @param #string EventDCSUnitName +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnDeadForUnit( EventDCSUnitName, EventFunction, EventSelf ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_DEAD ) + + return self +end + +--- Set a new listener for an S_EVENT_PILOT_DEAD event. +-- @param #EVENT self +-- @param #string EventDCSUnitName +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnPilotDeadForUnit( EventDCSUnitName, EventFunction, EventSelf ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_PILOT_DEAD ) + + return self +end + +--- Create an OnDead event handler for a group +-- @param #EVENT self +-- @param #table EventTemplate +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnLandForTemplate( EventTemplate, EventFunction, EventSelf ) + self:F2( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnLandForUnit ) + + return self +end + +--- Set a new listener for an S_EVENT_LAND event. +-- @param #EVENT self +-- @param #string EventDCSUnitName +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnLandForUnit( EventDCSUnitName, EventFunction, EventSelf ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_LAND ) + + return self +end + +--- Create an OnDead event handler for a group +-- @param #EVENT self +-- @param #table EventTemplate +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnTakeOffForTemplate( EventTemplate, EventFunction, EventSelf ) + self:F2( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnTakeOffForUnit ) + + return self +end + +--- Set a new listener for an S_EVENT_TAKEOFF event. +-- @param #EVENT self +-- @param #string EventDCSUnitName +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnTakeOffForUnit( EventDCSUnitName, EventFunction, EventSelf ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_TAKEOFF ) + + return self +end + +--- Create an OnDead event handler for a group +-- @param #EVENT self +-- @param #table EventTemplate +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnEngineShutDownForTemplate( EventTemplate, EventFunction, EventSelf ) + self:F2( EventTemplate.name ) + + self:OnEventForTemplate( EventTemplate, EventFunction, EventSelf, self.OnEngineShutDownForUnit ) + + return self +end + +--- Set a new listener for an S_EVENT_ENGINE_SHUTDOWN event. +-- @param #EVENT self +-- @param #string EventDCSUnitName +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnEngineShutDownForUnit( EventDCSUnitName, EventFunction, EventSelf ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_ENGINE_SHUTDOWN ) + + return self +end + +--- Set a new listener for an S_EVENT_ENGINE_STARTUP event. +-- @param #EVENT self +-- @param #string EventDCSUnitName +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnEngineStartUpForUnit( EventDCSUnitName, EventFunction, EventSelf ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_ENGINE_STARTUP ) + + return self +end + +--- Set a new listener for an S_EVENT_SHOT event. +-- @param #EVENT self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnShot( EventFunction, EventSelf ) + self:F2() + + self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_SHOT ) + + return self +end + +--- Set a new listener for an S_EVENT_SHOT event for a unit. +-- @param #EVENT self +-- @param #string EventDCSUnitName +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnShotForUnit( EventDCSUnitName, EventFunction, EventSelf ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_SHOT ) + + return self +end + +--- Set a new listener for an S_EVENT_HIT event. +-- @param #EVENT self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnHit( EventFunction, EventSelf ) + self:F2() + + self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_HIT ) + + return self +end + +--- Set a new listener for an S_EVENT_HIT event. +-- @param #EVENT self +-- @param #string EventDCSUnitName +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnHitForUnit( EventDCSUnitName, EventFunction, EventSelf ) + self:F2( EventDCSUnitName ) + + self:OnEventForUnit( EventDCSUnitName, EventFunction, EventSelf, world.event.S_EVENT_HIT ) + + return self +end + +--- Set a new listener for an S_EVENT_PLAYER_ENTER_UNIT event. +-- @param #EVENT self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnPlayerEnterUnit( EventFunction, EventSelf ) + self:F2() + + self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_PLAYER_ENTER_UNIT ) + + return self +end + +--- Set a new listener for an S_EVENT_PLAYER_LEAVE_UNIT event. +-- @param #EVENT self +-- @param #function EventFunction The function to be called when the event occurs for the unit. +-- @param Base#BASE EventSelf The self instance of the class for which the event is. +-- @return #EVENT +function EVENT:OnPlayerLeaveUnit( EventFunction, EventSelf ) + self:F2() + + self:OnEventGeneric( EventFunction, EventSelf, world.event.S_EVENT_PLAYER_LEAVE_UNIT ) + + return self +end + + + +function EVENT:onEvent( Event ) + self:F2( { _EVENTCODES[Event.id], Event } ) + + if self and self.Events and self.Events[Event.id] then + if Event.initiator and Event.initiator:getCategory() == Object.Category.UNIT then + Event.IniDCSUnit = Event.initiator + Event.IniDCSGroup = Event.IniDCSUnit:getGroup() + Event.IniDCSUnitName = Event.IniDCSUnit:getName() + Event.IniDCSGroupName = "" + if Event.IniDCSGroup and Event.IniDCSGroup:isExist() then + Event.IniDCSGroupName = Event.IniDCSGroup:getName() + end + end + if Event.target then + if Event.target and Event.target:getCategory() == Object.Category.UNIT then + Event.TgtDCSUnit = Event.target + Event.TgtDCSGroup = Event.TgtDCSUnit:getGroup() + Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() + Event.TgtDCSGroupName = "" + if Event.TgtDCSGroup and Event.TgtDCSGroup:isExist() then + Event.TgtDCSGroupName = Event.TgtDCSGroup:getName() + end + end + end + if Event.weapon then + Event.Weapon = Event.weapon + Event.WeaponName = Event.Weapon:getTypeName() + --Event.WeaponTgtDCSUnit = Event.Weapon:getTarget() + end + self:E( { _EVENTCODES[Event.id], Event } ) + for ClassName, EventData in pairs( self.Events[Event.id] ) do + if Event.IniDCSUnitName and EventData.IniUnit and EventData.IniUnit[Event.IniDCSUnitName] then + self:T2( { "Calling event function for class ", ClassName, " unit ", Event.IniDCSUnitName } ) + EventData.IniUnit[Event.IniDCSUnitName].EventFunction( EventData.IniUnit[Event.IniDCSUnitName].EventSelf, Event ) + else + if Event.IniDCSUnit and not EventData.IniUnit then + self:T2( { "Calling event function for class ", ClassName } ) + EventData.EventFunction( EventData.EventSelf, Event ) + end + end + end + end +end + +--- Encapsulation of DCS World Menu system in a set of MENU classes. +-- @module Menu + +Include.File( "Routines" ) +Include.File( "Base" ) + +--- The MENU class +-- @type MENU +-- @extends Base#BASE +MENU = { + ClassName = "MENU", + MenuPath = nil, + MenuText = "", + MenuParentPath = nil +} + +--- +function MENU:New( MenuText, MenuParentPath ) + + -- Arrange meta tables + local Child = BASE:Inherit( self, BASE:New() ) + + Child.MenuPath = nil + Child.MenuText = MenuText + Child.MenuParentPath = MenuParentPath + return Child +end + +--- The COMMANDMENU class +-- @type COMMANDMENU +-- @extends Menu#MENU +COMMANDMENU = { + ClassName = "COMMANDMENU", + CommandMenuFunction = nil, + CommandMenuArgument = nil +} + +function COMMANDMENU:New( MenuText, ParentMenu, CommandMenuFunction, CommandMenuArgument ) + + -- Arrange meta tables + + local MenuParentPath = nil + if ParentMenu ~= nil then + MenuParentPath = ParentMenu.MenuPath + end + + local Child = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) + + Child.MenuPath = missionCommands.addCommand( MenuText, MenuParentPath, CommandMenuFunction, CommandMenuArgument ) + Child.CommandMenuFunction = CommandMenuFunction + Child.CommandMenuArgument = CommandMenuArgument + return Child +end + +--- The SUBMENU class +-- @type SUBMENU +-- @extends Menu#MENU +SUBMENU = { + ClassName = "SUBMENU" +} + +function SUBMENU:New( MenuText, ParentMenu ) + + -- Arrange meta tables + local MenuParentPath = nil + if ParentMenu ~= nil then + MenuParentPath = ParentMenu.MenuPath + end + + local Child = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) + + Child.MenuPath = missionCommands.addSubMenu( MenuText, MenuParentPath ) + return Child +end + +-- This local variable is used to cache the menus registered under clients. +-- Menus don't dissapear when clients are destroyed and restarted. +-- So every menu for a client created must be tracked so that program logic accidentally does not create +-- the same menus twice during initialization logic. +-- These menu classes are handling this logic with this variable. +local _MENUCLIENTS = {} + +--- The MENU_CLIENT class +-- @type MENU_CLIENT +-- @extends Menu#MENU +MENU_CLIENT = { + ClassName = "MENU_CLIENT" +} + +--- Creates a new menu item for a group +-- @param self +-- @param Client#CLIENT MenuClient The Client owning the menu. +-- @param #string MenuText The text for the menu. +-- @param #table ParentMenu The parent menu. +-- @return #MENU_CLIENT self +function MENU_CLIENT:New( MenuClient, MenuText, ParentMenu ) + + -- Arrange meta tables + local MenuParentPath = {} + if ParentMenu ~= nil then + MenuParentPath = ParentMenu.MenuPath + end + + local self = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) + self:F( { MenuClient, MenuText, ParentMenu } ) + + self.MenuClient = MenuClient + self.MenuClientGroupID = MenuClient:GetClientGroupID() + self.MenuParentPath = MenuParentPath + self.MenuText = MenuText + self.ParentMenu = ParentMenu + + self.Menus = {} + + if not _MENUCLIENTS[self.MenuClientGroupID] then + _MENUCLIENTS[self.MenuClientGroupID] = {} + end + + local MenuPath = _MENUCLIENTS[self.MenuClientGroupID] + + self:T( { MenuClient:GetClientGroupName(), MenuPath[table.concat(MenuParentPath)], MenuParentPath, MenuText } ) + + local MenuPathID = table.concat(MenuParentPath) .. "/" .. MenuText + if MenuPath[MenuPathID] then + missionCommands.removeItemForGroup( self.MenuClient:GetClientGroupID(), MenuPath[MenuPathID] ) + end + + self.MenuPath = missionCommands.addSubMenuForGroup( self.MenuClient:GetClientGroupID(), MenuText, MenuParentPath ) + MenuPath[MenuPathID] = self.MenuPath + + self:T( { MenuClient:GetClientGroupName(), self.MenuPath } ) + + if ParentMenu and ParentMenu.Menus then + ParentMenu.Menus[self.MenuPath] = self + end + return self +end + +--- Removes the sub menus recursively of this MENU_CLIENT. +-- @param #MENU_CLIENT self +-- @return #MENU_CLIENT self +function MENU_CLIENT:RemoveSubMenus() + self:F( self.MenuPath ) + + for MenuID, Menu in pairs( self.Menus ) do + Menu:Remove() + end + +end + +--- Removes the sub menus recursively of this MENU_CLIENT. +-- @param #MENU_CLIENT self +-- @return #MENU_CLIENT self +function MENU_CLIENT:Remove() + self:F( self.MenuPath ) + + self:RemoveSubMenus() + + if not _MENUCLIENTS[self.MenuClientGroupID] then + _MENUCLIENTS[self.MenuClientGroupID] = {} + end + + local MenuPath = _MENUCLIENTS[self.MenuClientGroupID] + + if MenuPath[table.concat(self.MenuParentPath) .. "/" .. self.MenuText] then + MenuPath[table.concat(self.MenuParentPath) .. "/" .. self.MenuText] = nil + end + + missionCommands.removeItemForGroup( self.MenuClient:GetClientGroupID(), self.MenuPath ) + self.ParentMenu.Menus[self.MenuPath] = nil + return nil +end + + +--- The MENU_CLIENT_COMMAND class +-- @type MENU_CLIENT_COMMAND +-- @extends Menu#MENU +MENU_CLIENT_COMMAND = { + ClassName = "MENU_CLIENT_COMMAND" +} + +--- Creates a new radio command item for a group +-- @param self +-- @param Client#CLIENT MenuClient The Client owning the menu. +-- @param MenuText The text for the menu. +-- @param ParentMenu The parent menu. +-- @param CommandMenuFunction A function that is called when the menu key is pressed. +-- @param CommandMenuArgument An argument for the function. +-- @return Menu#MENU_CLIENT_COMMAND self +function MENU_CLIENT_COMMAND:New( MenuClient, MenuText, ParentMenu, CommandMenuFunction, CommandMenuArgument ) + + -- Arrange meta tables + + local MenuParentPath = {} + if ParentMenu ~= nil then + MenuParentPath = ParentMenu.MenuPath + end + + local self = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) + + self.MenuClient = MenuClient + self.MenuClientGroupID = MenuClient:GetClientGroupID() + self.MenuParentPath = MenuParentPath + self.MenuText = MenuText + self.ParentMenu = ParentMenu + + if not _MENUCLIENTS[self.MenuClientGroupID] then + _MENUCLIENTS[self.MenuClientGroupID] = {} + end + + local MenuPath = _MENUCLIENTS[self.MenuClientGroupID] + + self:T( { MenuClient:GetClientGroupName(), MenuPath[table.concat(MenuParentPath)], MenuParentPath, MenuText, CommandMenuFunction, CommandMenuArgument } ) + + local MenuPathID = table.concat(MenuParentPath) .. "/" .. MenuText + if MenuPath[MenuPathID] then + missionCommands.removeItemForGroup( self.MenuClient:GetClientGroupID(), MenuPath[MenuPathID] ) + end + + self.MenuPath = missionCommands.addCommandForGroup( self.MenuClient:GetClientGroupID(), MenuText, MenuParentPath, CommandMenuFunction, CommandMenuArgument ) + MenuPath[MenuPathID] = self.MenuPath + + self.CommandMenuFunction = CommandMenuFunction + self.CommandMenuArgument = CommandMenuArgument + + ParentMenu.Menus[self.MenuPath] = self + + return self +end + +function MENU_CLIENT_COMMAND:Remove() + self:F( self.MenuPath ) + + if not _MENUCLIENTS[self.MenuClientGroupID] then + _MENUCLIENTS[self.MenuClientGroupID] = {} + end + + local MenuPath = _MENUCLIENTS[self.MenuClientGroupID] + + if MenuPath[table.concat(self.MenuParentPath) .. "/" .. self.MenuText] then + MenuPath[table.concat(self.MenuParentPath) .. "/" .. self.MenuText] = nil + end + + missionCommands.removeItemForGroup( self.MenuClient:GetClientGroupID(), self.MenuPath ) + self.ParentMenu.Menus[self.MenuPath] = nil + return nil +end + + +--- The MENU_COALITION class +-- @type MENU_COALITION +-- @extends Menu#MENU +MENU_COALITION = { + ClassName = "MENU_COALITION" +} + +--- Creates a new coalition menu item +-- @param #MENU_COALITION self +-- @param DCSCoalition#coalition.side MenuCoalition The coalition owning the menu. +-- @param #string MenuText The text for the menu. +-- @param #table ParentMenu The parent menu. +-- @return #MENU_COALITION self +function MENU_COALITION:New( MenuCoalition, MenuText, ParentMenu ) + + -- Arrange meta tables + local MenuParentPath = {} + if ParentMenu ~= nil then + MenuParentPath = ParentMenu.MenuPath + end + + local self = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) + self:F( { MenuCoalition, MenuText, ParentMenu } ) + + self.MenuCoalition = MenuCoalition + self.MenuParentPath = MenuParentPath + self.MenuText = MenuText + self.ParentMenu = ParentMenu + + self.Menus = {} + + self:T( { MenuParentPath, MenuText } ) + + self.MenuPath = missionCommands.addSubMenuForCoalition( self.MenuCoalition, MenuText, MenuParentPath ) + + self:T( { self.MenuPath } ) + + if ParentMenu and ParentMenu.Menus then + ParentMenu.Menus[self.MenuPath] = self + end + return self +end + +--- Removes the sub menus recursively of this MENU_COALITION. +-- @param #MENU_COALITION self +-- @return #MENU_COALITION self +function MENU_COALITION:RemoveSubMenus() + self:F( self.MenuPath ) + + for MenuID, Menu in pairs( self.Menus ) do + Menu:Remove() + end + +end + +--- Removes the sub menus recursively of this MENU_COALITION. +-- @param #MENU_COALITION self +-- @return #MENU_COALITION self +function MENU_COALITION:Remove() + self:F( self.MenuPath ) + + self:RemoveSubMenus() + missionCommands.removeItemForCoalition( self.MenuCoalition, self.MenuPath ) + self.ParentMenu.Menus[self.MenuPath] = nil + + return nil +end + + +--- The MENU_COALITION_COMMAND class +-- @type MENU_COALITION_COMMAND +-- @extends Menu#MENU +MENU_COALITION_COMMAND = { + ClassName = "MENU_COALITION_COMMAND" +} + +--- Creates a new radio command item for a group +-- @param #MENU_COALITION_COMMAND self +-- @param DCSCoalition#coalition.side MenuCoalition The coalition owning the menu. +-- @param MenuText The text for the menu. +-- @param ParentMenu The parent menu. +-- @param CommandMenuFunction A function that is called when the menu key is pressed. +-- @param CommandMenuArgument An argument for the function. +-- @return #MENU_COALITION_COMMAND self +function MENU_COALITION_COMMAND:New( MenuCoalition, MenuText, ParentMenu, CommandMenuFunction, CommandMenuArgument ) + + -- Arrange meta tables + + local MenuParentPath = {} + if ParentMenu ~= nil then + MenuParentPath = ParentMenu.MenuPath + end + + local self = BASE:Inherit( self, MENU:New( MenuText, MenuParentPath ) ) + + self.MenuCoalition = MenuCoalition + self.MenuParentPath = MenuParentPath + self.MenuText = MenuText + self.ParentMenu = ParentMenu + + self:T( { MenuParentPath, MenuText, CommandMenuFunction, CommandMenuArgument } ) + + self.MenuPath = missionCommands.addCommandForCoalition( self.MenuCoalition, MenuText, MenuParentPath, CommandMenuFunction, CommandMenuArgument ) + + self.CommandMenuFunction = CommandMenuFunction + self.CommandMenuArgument = CommandMenuArgument + + ParentMenu.Menus[self.MenuPath] = self + + return self +end + +--- Removes a radio command item for a coalition +-- @param #MENU_COALITION_COMMAND self +-- @return #MENU_COALITION_COMMAND self +function MENU_COALITION_COMMAND:Remove() + self:F( self.MenuPath ) + + missionCommands.removeItemForCoalition( self.MenuCoalition, self.MenuPath ) + self.ParentMenu.Menus[self.MenuPath] = nil + return nil +end +--- GROUP class. +-- +-- @{GROUP} class +-- ============== +-- The @{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. +-- +-- +-- 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. +-- +-- IMPORTANT: ONE SHOULD NEVER SANATIZE these GROUP OBJECT REFERENCES! (make the GROUP object references nil). +-- @module Group +-- @author FlightControl + +Include.File( "Routines" ) +Include.File( "Base" ) +Include.File( "Message" ) +Include.File( "Unit" ) + +--- The GROUP class +-- @type GROUP +-- @extends Base#BASE +-- @field DCSGroup#Group DCSGroup The DCS group class. +-- @field #string GroupName The name of the group. +GROUP = { + ClassName = "GROUP", + GroupName = "", + GroupID = 0, + Controller = nil, + DCSGroup = nil, + WayPointFunctions = {}, + } + +--- A DCSGroup +-- @type DCSGroup +-- @field id_ The ID of the group in DCS + +--- Create a new GROUP from a DCSGroup +-- @param #GROUP self +-- @param DCSGroup#Group GroupName The DCS Group name +-- @return #GROUP self +function GROUP:Register( GroupName ) + local self = BASE:Inherit( self, BASE:New() ) + self:F2( GroupName ) + self.GroupName = GroupName + return self +end + +-- Reference methods. + +--- Find the GROUP wrapper class instance using the DCS Group. +-- @param #GROUP self +-- @param DCSGroup#Group DCSGroup The DCS Group. +-- @return #GROUP The GROUP. +function GROUP:Find( DCSGroup ) + + local GroupName = DCSGroup:getName() -- Group#GROUP + local GroupFound = _DATABASE:FindGroup( GroupName ) + return GroupFound +end + +--- Find the created GROUP using the DCS Group Name. +-- @param #GROUP self +-- @param #string GroupName The DCS Group Name. +-- @return #GROUP The GROUP. +function GROUP:FindByName( GroupName ) + + local GroupFound = _DATABASE:FindGroup( GroupName ) + return GroupFound +end + +-- DCS Group methods support. + +--- Returns the DCS Group. +-- @param #GROUP self +-- @return DCSGroup#Group The DCS Group. +function GROUP:GetDCSGroup() + local DCSGroup = Group.getByName( self.GroupName ) + + if DCSGroup then + return DCSGroup + end + + return nil +end + + +--- Returns if the DCS Group is alive. +-- When the group exists at run-time, this method will return true, otherwise false. +-- @param #GROUP self +-- @return #boolean true if the DCS Group is alive. +function GROUP:IsAlive() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSGroup() + + if DCSGroup then + local GroupIsAlive = DCSGroup:isExist() + self:T3( GroupIsAlive ) + return GroupIsAlive + end + + return nil +end + +--- Destroys the DCS Group and all of its DCS Units. +-- Note that this destroy method also raises a destroy event at run-time. +-- So all event listeners will catch the destroy event of this DCS Group. +-- @param #GROUP self +function GROUP:Destroy() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSGroup() + + if DCSGroup then + for Index, UnitData in pairs( DCSGroup:getUnits() ) do + self:CreateEventCrash( timer.getTime(), UnitData ) + end + DCSGroup:destroy() + DCSGroup = nil + end + + return nil +end + +--- Returns category of the DCS Group. +-- @param #GROUP self +-- @return DCSGroup#Group.Category The category ID +function GROUP:GetCategory() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + local GroupCategory = DCSGroup:getCategory() + self:T3( GroupCategory ) + return GroupCategory + end + + return nil +end + +--- Returns the category name of the DCS Group. +-- @param #GROUP self +-- @return #string Category name = Helicopter, Airplane, Ground Unit, Ship +function GROUP:GetCategoryName() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + local CategoryNames = { + [Group.Category.AIRPLANE] = "Airplane", + [Group.Category.HELICOPTER] = "Helicopter", + [Group.Category.GROUND] = "Ground Unit", + [Group.Category.SHIP] = "Ship", + } + local GroupCategory = DCSGroup:getCategory() + self:T3( GroupCategory ) + + return CategoryNames[GroupCategory] + end + + return nil +end + + +--- Returns the coalition of the DCS Group. +-- @param #GROUP self +-- @return DCSCoalitionObject#coalition.side The coalition side of the DCS Group. +function GROUP:GetCoalition() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + local GroupCoalition = DCSGroup:getCoalition() + self:T3( GroupCoalition ) + return GroupCoalition + end + + return nil +end + +--- Returns the country of the DCS Group. +-- @param #GROUP self +-- @return DCScountry#country.id The country identifier. +-- @return #nil The DCS Group is not existing or alive. +function GROUP:GetCountry() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + local GroupCountry = DCSGroup:getUnit(1):getCountry() + self:T3( GroupCountry ) + return GroupCountry + end + + return nil +end + +--- Returns the name of the DCS Group. +-- @param #GROUP self +-- @return #string The DCS Group name. +function GROUP:GetName() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSGroup() + + if DCSGroup then + local GroupName = DCSGroup:getName() + self:T3( GroupName ) + return GroupName + end + + return nil +end + +--- Returns the DCS Group identifier. +-- @param #GROUP self +-- @return #number The identifier of the DCS Group. +function GROUP:GetID() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSGroup() + + if DCSGroup then + local GroupID = DCSGroup:getID() + self:T3( GroupID ) + return GroupID + end + + return nil +end + +--- Returns the UNIT wrapper class with number UnitNumber. +-- If the underlying DCS Unit does not exist, the method will return nil. . +-- @param #GROUP self +-- @param #number UnitNumber The number of the UNIT wrapper class to be returned. +-- @return Unit#UNIT The UNIT wrapper class. +function GROUP:GetUnit( UnitNumber ) + self:F2( { self.GroupName, UnitNumber } ) + + local DCSGroup = self:GetDCSGroup() + + if DCSGroup then + local UnitFound = UNIT:Find( DCSGroup:getUnit( UnitNumber ) ) + self:T3( UnitFound.UnitName ) + self:T2( UnitFound ) + return UnitFound + end + + return nil +end + +--- Returns the DCS Unit with number UnitNumber. +-- If the underlying DCS Unit does not exist, the method will return nil. . +-- @param #GROUP self +-- @param #number UnitNumber The number of the DCS Unit to be returned. +-- @return DCSUnit#Unit The DCS Unit. +function GROUP:GetDCSUnit( UnitNumber ) + self:F2( { self.GroupName, UnitNumber } ) + + local DCSGroup = self:GetDCSGroup() + + 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:GetDCSGroup() + + 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:GetDCSGroup() + + if DCSGroup then + local GroupInitialSize = DCSGroup:getInitialSize() + self:T3( GroupInitialSize ) + return GroupInitialSize + end + + return nil +end + +--- Returns the UNITs wrappers of the DCS Units of the DCS Group. +-- @param #GROUP self +-- @return #table The UNITs wrappers. +function GROUP:GetUnits() + self:F2( { self.GroupName } ) + local DCSGroup = self:GetDCSGroup() + + if DCSGroup then + local DCSUnits = DCSGroup:getUnits() + local Units = {} + for Index, UnitData in pairs( DCSUnits ) do + Units[#Units+1] = UNIT:Find( UnitData ) + end + self:T3( Units ) + return Units + end + + return nil +end + + +--- Returns the DCS Units of the DCS Group. +-- @param #GROUP self +-- @return #table The DCS Units. +function GROUP:GetDCSUnits() + self:F2( { self.GroupName } ) + local DCSGroup = self:GetDCSGroup() + + if DCSGroup then + local DCSUnits = DCSGroup:getUnits() + self:T3( DCSUnits ) + return DCSUnits + end + + return nil +end + +--- Get the controller for the GROUP. +-- @param #GROUP self +-- @return DCSController#Controller +function GROUP:_GetController() + self:F2( { self.GroupName } ) + local DCSGroup = self:GetDCSGroup() + + if DCSGroup then + local GroupController = DCSGroup:getController() + self:T3( GroupController ) + return GroupController + end + + return nil +end + + +--- Retrieve the group mission and allow to place function hooks within the mission waypoint plan. +-- Use the method @{Group#GROUP:WayPointFunction} to define the hook functions for specific waypoints. +-- Use the method @{Group@GROUP:WayPointExecute) to start the execution of the new mission plan. +-- Note that when WayPointInitialize is called, the Mission of the group is RESTARTED! +-- @param #GROUP self +-- @return #GROUP +function GROUP:WayPointInitialize() + + self.WayPoints = self:GetTaskRoute() + + return self +end + + +--- Registers a waypoint function that will be executed when the group moves over the WayPoint. +-- @param #GROUP 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 group moves over the waypoint. The waypoint function takes variable parameters. +-- @return #GROUP +function GROUP: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 GROUP:TaskFunction( WayPoint, WayPointIndex, FunctionString, FunctionArguments ) + + local DCSTask + + local DCSScript = {} + DCSScript[#DCSScript+1] = "local MissionGroup = GROUP:Find( ... ) " + + if FunctionArguments.n > 0 then + DCSScript[#DCSScript+1] = FunctionString .. "( MissionGroup, " .. table.concat( FunctionArguments, "," ) .. ")" + else + DCSScript[#DCSScript+1] = FunctionString .. "( MissionGroup )" + 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 group will be 1! +-- @param #GROUP self +-- @param #number WayPoint The WayPoint from where to execute the mission. +-- @param #WaitTime The amount seconds to wait before initiating the mission. +-- @return #GROUP +function GROUP:WayPointExecute( WayPoint, WaitTime ) + + if not WayPoint then + WayPoint = 1 + end + + -- When starting the mission from a certain point, the TaskPoints need to be deleted before the given WayPoint. + for TaskPointID = 1, WayPoint - 1 do + table.remove( self.WayPoints, 1 ) + end + + self:T3( self.WayPoints ) + + self:SetTask( self:TaskRoute( self.WayPoints ), WaitTime ) + + return self +end + + +--- Activates a GROUP. +-- @param #GROUP self +function GROUP:Activate() + self:F2( { self.GroupName } ) + trigger.action.activateGroup( self:GetDCSGroup() ) + return self:GetDCSGroup() +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:GetDCSGroup() + + 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:GetDCSGroup() + + if DCSGroup then + local GroupCallSign = DCSGroup:getUnit(1):getCallsign() + self:T3( GroupCallSign ) + return GroupCallSign + end + + return nil +end + +--- Returns the current point (Vec2 vector) of the first DCS Unit in the DCS Group. +-- @return DCSTypes#Vec2 Current Vec2 point of the first DCS Unit of the DCS Group. +function GROUP:GetPointVec2() + self:F2( self.GroupName ) + + local GroupPointVec2 = self:GetUnit(1):GetPointVec2() + self:T3( GroupPointVec2 ) + return GroupPointVec2 +end + +--- Returns the current point (Vec3 vector) of the first DCS Unit in the DCS Group. +-- @return DCSTypes#Vec3 Current Vec3 point of the first DCS Unit of the DCS Group. +function GROUP:GetPointVec3() + self:F2( self.GroupName ) + + local GroupPointVec3 = self:GetUnit(1):GetPointVec3() + self:T3( GroupPointVec3 ) + return GroupPointVec3 +end + + + +-- Is Functions + +--- 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:GetDCSGroup() + + 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:GetDCSGroup() + + 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:GetDCSGroup() + + 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:GetDCSGroup() + + 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:GetDCSGroup() + + 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:GetDCSGroup() + + if DCSGroup then + local AllOnGroundResult = true + + for Index, UnitData in pairs( DCSGroup:getUnits() ) do + if UnitData:inAir() then + AllOnGroundResult = false + end + end + + self:T3( AllOnGroundResult ) + return AllOnGroundResult + end + + return nil +end + +--- Returns the current maximum velocity of the group. +-- Each unit within the group gets evaluated, and the maximum velocity (= the unit which is going the fastest) is returned. +-- @param #GROUP self +-- @return #number Maximum velocity found. +function GROUP:GetMaxVelocity() + self:F2() + + local DCSGroup = self:GetDCSGroup() + + if DCSGroup then + local MaxVelocity = 0 + + for Index, UnitData in pairs( DCSGroup:getUnits() ) do + + local Velocity = UnitData:getVelocity() + local VelocityTotal = math.abs( Velocity.x ) + math.abs( Velocity.y ) + math.abs( Velocity.z ) + + if VelocityTotal < MaxVelocity then + MaxVelocity = VelocityTotal + end + end + + return MaxVelocity + end + + return nil +end + +--- Returns the current minimum height of the group. +-- Each unit within the group gets evaluated, and the minimum height (= the unit which is the lowest elevated) is returned. +-- @param #GROUP self +-- @return #number Minimum height found. +function GROUP:GetMinHeight() + self:F2() + +end + +--- Returns the current maximum height of the group. +-- Each unit within the group gets evaluated, and the maximum height (= the unit which is the highest elevated) is returned. +-- @param #GROUP self +-- @return #number Maximum height found. +function GROUP:GetMaxHeight() + self:F2() + +end + +-- Tasks + +--- Popping current Task from the group. +-- @param #GROUP self +-- @return Group#GROUP self +function GROUP:PopCurrentTask() + self:F2() + + local DCSGroup = self:GetDCSGroup() + + if DCSGroup then + local Controller = self:_GetController() + Controller:popTask() + return self + end + + return nil +end + +--- Pushing Task on the queue from the group. +-- @param #GROUP self +-- @return Group#GROUP self +function GROUP:PushTask( DCSTask, WaitTime ) + self:F2() + + local DCSGroup = self:GetDCSGroup() + + if DCSGroup then + local Controller = self:_GetController() + + -- When a group SPAWNs, it takes about a second to get the group in the simulator. Setting tasks to unspawned groups provides unexpected results. + -- Therefore we schedule the functions to set the mission and options for the Group. + -- Controller:pushTask( DCSTask ) + + if WaitTime then + --routines.scheduleFunction( Controller.pushTask, { Controller, DCSTask }, timer.getTime() + WaitTime ) + SCHEDULER:New( Controller, Controller.pushTask, { DCSTask }, WaitTime ) + else + Controller:pushTask( DCSTask ) + end + + return self + end + + return nil +end + +--- Clearing the Task Queue and Setting the Task on the queue from the group. +-- @param #GROUP self +-- @return Group#GROUP self +function GROUP:SetTask( DCSTask, WaitTime ) + self:F2( { DCSTask } ) + + local DCSGroup = self:GetDCSGroup() + + if DCSGroup then + + local Controller = self:_GetController() + + -- When a group SPAWNs, it takes about a second to get the group in the simulator. Setting tasks to unspawned groups provides unexpected results. + -- Therefore we schedule the functions to set the mission and options for the Group. + -- Controller.setTask( Controller, DCSTask ) + + if not WaitTime then + WaitTime = 1 + end + --routines.scheduleFunction( Controller.setTask, { Controller, DCSTask }, timer.getTime() + WaitTime ) + SCHEDULER:New( Controller, Controller.setTask, { DCSTask }, WaitTime ) + + return self + end + + return nil +end + + +--- Return a condition section for a controlled task +-- @param #GROUP self +-- @param DCSTime#Time time +-- @param #string userFlag +-- @param #boolean userFlagValue +-- @param #string condition +-- @param DCSTime#Time duration +-- @param #number lastWayPoint +-- return DCSTask#Task +function GROUP: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 #GROUP self +-- @param DCSTask#Task DCSTask +-- @param #DCSStopCondition DCSStopCondition +-- @return DCSTask#Task +function GROUP: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 #GROUP self +-- @param #list DCSTasks +-- @return DCSTask#Task +function GROUP:TaskCombo( DCSTasks ) + self:F2( { DCSTasks } ) + + local DCSTaskCombo + + DCSTaskCombo = { + id = 'ComboTask', + params = { + tasks = DCSTasks + } + } + + self:T3( { DCSTaskCombo } ) + return DCSTaskCombo +end + +--- Return a WrappedAction Task taking a Command +-- @param #GROUP self +-- @param DCSCommand#Command DCSCommand +-- @return DCSTask#Task +function GROUP: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 #GROUP self +-- @param DCSCommand#Command DCSCommand +-- @return #GROUP self +function GROUP:SetCommand( DCSCommand ) + self:F2( DCSCommand ) + + local DCSGroup = self:GetDCSGroup() + + if DCSGroup then + local Controller = self:_GetController() + Controller:setCommand( DCSCommand ) + return self + end + + return nil +end + +--- Perform a switch waypoint command +-- @param #GROUP self +-- @param #number FromWayPoint +-- @param #number ToWayPoint +-- @return DCSTask#Task +function GROUP:CommandSwitchWayPoint( FromWayPoint, ToWayPoint, Index ) + self:F2( { FromWayPoint, ToWayPoint, Index } ) + + local CommandSwitchWayPoint = { + id = 'SwitchWaypoint', + params = { + fromWaypointIndex = FromWayPoint, + goToWaypointIndex = ToWayPoint, + }, + } + + self:T3( { CommandSwitchWayPoint } ) + return CommandSwitchWayPoint +end + + +--- Orbit at a specified position at a specified alititude during a specified duration with a specified speed. +-- @param #GROUP self +-- @param DCSTypes#Vec2 Point The point to hold the position. +-- @param #number Altitude The altitude to hold the position. +-- @param #number Speed The speed flying when holding the position. +-- @return #GROUP self +function GROUP:TaskOrbitCircleAtVec2( Point, Altitude, Speed ) + self:F2( { self.GroupName, 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 + +--- Orbit at the current position of the first unit of the group at a specified alititude +-- @param #GROUP self +-- @param #number Altitude The altitude to hold the position. +-- @param #number Speed The speed flying when holding the position. +-- @return #GROUP self +function GROUP:TaskOrbitCircle( Altitude, Speed ) + self:F2( { self.GroupName, Altitude, Speed } ) + + local DCSGroup = self:GetDCSGroup() + + if DCSGroup then + local GroupPoint = self:GetPointVec2() + return self:TaskOrbitCircleAtVec2( GroupPoint, Altitude, Speed ) + end + + return nil +end + + + +--- Hold position at the current position of the first unit of the group. +-- @param #GROUP self +-- @param #number Duration The maximum duration in seconds to hold the position. +-- @return #GROUP self +function GROUP:TaskHoldPosition() + self:F2( { self.GroupName } ) + + return self:TaskOrbitCircle( 30, 10 ) +end + + +--- Land the group at a Vec2Point. +-- @param #GROUP self +-- @param DCSTypes#Vec2 Point The point where to land. +-- @param #number Duration The duration in seconds to stay on the ground. +-- @return #GROUP self +function GROUP:TaskLandAtVec2( Point, Duration ) + self:F2( { self.GroupName, Point, Duration } ) + + 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 + +--- Land the group at a @{Zone#ZONE). +-- @param #GROUP self +-- @param Zone#ZONE Zone The zone where to land. +-- @param #number Duration The duration in seconds to stay on the ground. +-- @return #GROUP self +function GROUP:TaskLandAtZone( Zone, Duration, RandomPoint ) + self:F2( { self.GroupName, Zone, Duration, RandomPoint } ) + + local Point + if RandomPoint then + Point = Zone:GetRandomPointVec2() + else + Point = Zone:GetPointVec2() + end + + local DCSTask = self:TaskLandAtVec2( Point, Duration ) + + self:T3( DCSTask ) + return DCSTask +end + + +--- Attack the Unit. +-- @param #GROUP self +-- @param Unit#UNIT The unit. +-- @return DCSTask#Task The DCS task structure. +function GROUP:TaskAttackUnit( AttackUnit ) + self:F2( { self.GroupName, AttackUnit } ) + +-- AttackUnit = { +-- id = 'AttackUnit', +-- params = { +-- unitId = Unit.ID, +-- weaponType = number, +-- expend = enum AI.Task.WeaponExpend +-- attackQty = number, +-- direction = Azimuth, +-- attackQtyLimit = boolean, +-- groupAttack = boolean, +-- } +-- } + + local DCSTask + DCSTask = { id = 'AttackUnit', + params = { unitId = AttackUnit:GetID(), + expend = AI.Task.WeaponExpend.TWO, + groupAttack = true, + }, + }, + + self:T3( { DCSTask } ) + return DCSTask +end + +--- Attack a Group. +-- @param #GROUP self +-- @param Group#GROUP AttackGroup The Group to be attacked. +-- @return DCSTask#Task The DCS task structure. +function GROUP:TaskAttackGroup( AttackGroup ) + self:F2( { self.GroupName, AttackGroup } ) + +-- 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 DCSTask + DCSTask = { id = 'AttackGroup', + params = { groupId = AttackGroup:GetID(), + expend = AI.Task.WeaponExpend.TWO, + }, + }, + + self:T3( { DCSTask } ) + return DCSTask +end + +--- Fires at a VEC2 point. +-- @param #GROUP self +-- @param DCSTypes#Vec2 The point to fire at. +-- @param DCSTypes#Distance Radius The radius of the zone to deploy the fire at. +-- @return DCSTask#Task The DCS task structure. +function GROUP:TaskFireAtPoint( PointVec2, Radius ) + self:F2( { self.GroupName, PointVec2, Radius } ) + +-- FireAtPoint = { +-- id = 'FireAtPoint', +-- params = { +-- point = Vec2, +-- radius = Distance, +-- } +-- } + + local DCSTask + DCSTask = { id = 'FireAtPoint', + params = { point = PointVec2, + radius = Radius, + } + } + + self:T3( { DCSTask } ) + return DCSTask +end + + + +--- Move the group to a Vec2 Point, wait for a defined duration and embark a group. +-- @param #GROUP self +-- @param DCSTypes#Vec2 Point The point where to wait. +-- @param #number Duration The duration in seconds to wait. +-- @param #GROUP EmbarkingGroup The group to be embarked. +-- @return DCSTask#Task The DCS task structure +function GROUP:TaskEmbarkingAtVec2( Point, Duration, EmbarkingGroup ) + self:F2( { self.GroupName, Point, Duration, EmbarkingGroup.DCSGroup } ) + + local DCSTask + DCSTask = { id = 'Embarking', + params = { x = Point.x, + y = Point.y, + duration = Duration, + groupsForEmbarking = { EmbarkingGroup.GroupID }, + durationFlag = true, + distributionFlag = false, + distribution = {}, + } + } + + self:T3( { DCSTask } ) + return DCSTask +end + +--- Move to a defined Vec2 Point, and embark to a group when arrived within a defined Radius. +-- @param #GROUP self +-- @param DCSTypes#Vec2 Point The point where to wait. +-- @param #number Radius The radius of the embarking zone around the Point. +-- @return DCSTask#Task The DCS task structure. +function GROUP:TaskEmbarkToTransportAtVec2( Point, Radius ) + self:F2( { self.GroupName, Point, Radius } ) + + local DCSTask --DCSTask#Task + DCSTask = { id = 'EmbarkToTransport', + params = { x = Point.x, + y = Point.y, + zoneRadius = Radius, + } + } + + self:T3( { DCSTask } ) + return DCSTask +end + +--- Return a Misson task from a mission template. +-- @param #GROUP self +-- @param #table TaskMission A table containing the mission task. +-- @return DCSTask#Task +function GROUP: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 #GROUP self +-- @param #table Points A table of route points. +-- @return DCSTask#Task +function GROUP:TaskRoute( Points ) + self:F2( Points ) + + local DCSTask + DCSTask = { id = 'Mission', params = { route = { points = Points, }, }, } + + self:T3( { DCSTask } ) + return DCSTask +end + +--- Make the DCS Group to fly to a given point and hover. +-- @param #GROUP self +-- @param DCSTypes#Vec3 Point The destination point in Vec3 format. +-- @param #number Speed The speed to travel. +-- @return #GROUP self +function GROUP:TaskRouteToVec2( Point, Speed ) + self:F2( { Point, Speed } ) + + local GroupPoint = self:GetUnit( 1 ):GetPointVec2() + + local PointFrom = {} + PointFrom.x = GroupPoint.x + PointFrom.y = GroupPoint.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 + +--- Make the DCS Group to fly to a given point and hover. +-- @param #GROUP self +-- @param DCSTypes#Vec3 Point The destination point in Vec3 format. +-- @param #number Speed The speed to travel. +-- @return #GROUP self +function GROUP:TaskRouteToVec3( Point, Speed ) + self:F2( { Point, Speed } ) + + local GroupPoint = self:GetUnit( 1 ):GetPointVec3() + + local PointFrom = {} + PointFrom.x = GroupPoint.x + PointFrom.y = GroupPoint.z + PointFrom.alt = GroupPoint.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 group to follow a given route. +-- @param #GROUP self +-- @param #table GoPoints A table of Route Points. +-- @return #GROUP self +function GROUP:Route( GoPoints ) + self:F2( GoPoints ) + + local DCSGroup = self:GetDCSGroup() + + if DCSGroup then + local Points = routines.utils.deepCopy( GoPoints ) + local MissionTask = { id = 'Mission', params = { route = { points = Points, }, }, } + local Controller = self:_GetController() + --Controller.setTask( Controller, MissionTask ) + --routines.scheduleFunction( Controller.setTask, { Controller, MissionTask}, timer.getTime() + 1 ) + SCHEDULER:New( Controller, Controller.setTask, { MissionTask }, 1 ) + return self + end + + return nil +end + + + +--- Route the group to a given zone. +-- The group final destination point can be randomized. +-- A speed can be given in km/h. +-- A given formation can be given. +-- @param #GROUP self +-- @param Zone#ZONE Zone The zone where to route to. +-- @param #boolean Randomize Defines whether to target point gets randomized within the Zone. +-- @param #number Speed The speed. +-- @param Base#FORMATION Formation The formation string. +function GROUP:TaskRouteToZone( Zone, Randomize, Speed, Formation ) + self:F2( Zone ) + + local DCSGroup = self:GetDCSGroup() + + if DCSGroup then + + local GroupPoint = self:GetPointVec2() + + local PointFrom = {} + PointFrom.x = GroupPoint.x + PointFrom.y = GroupPoint.y + PointFrom.type = "Turning Point" + PointFrom.action = "Cone" + PointFrom.speed = 20 / 1.6 + + + local PointTo = {} + local ZonePoint + + if Randomize then + ZonePoint = Zone:GetRandomPointVec2() + else + ZonePoint = Zone:GetPointVec2() + end + + PointTo.x = ZonePoint.x + PointTo.y = ZonePoint.y + PointTo.type = "Turning Point" + + if Formation then + PointTo.action = Formation + else + PointTo.action = "Cone" + end + + if Speed then + PointTo.speed = Speed + else + PointTo.speed = 20 / 1.6 + end + + local Points = { PointFrom, PointTo } + + self:T3( Points ) + + self:Route( Points ) + + return self + end + + return nil +end + +-- Commands + +--- Do Script command +-- @param #GROUP self +-- @param #string DoScript +-- @return #DCSCommand +function GROUP:CommandDoScript( DoScript ) + + local DCSDoScript = { + id = "Script", + params = { + command = DoScript, + }, + } + + self:T3( DCSDoScript ) + return DCSDoScript +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 + end + + return nil +end + + +function GROUP:GetDetectedTargets() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + return self:_GetController():getDetectedTargets() + end + + return nil +end + +function GROUP:IsTargetDetected( DCSObject ) + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup 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 GROUP hold their weapons? +-- @param #GROUP self +-- @return #boolean +function GROUP:OptionROEHoldFirePossible() + self:F2( { self.GroupName } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + if self:IsAir() or self:IsGround() or self:IsShip() then + return true + end + + return false + end + + return nil +end + +--- Holding weapons. +-- @param Group#GROUP self +-- @return Group#GROUP self +function GROUP:OptionROEHoldFire() + self:F2( { self.GroupName } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup 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 GROUP attack returning on enemy fire? +-- @param #GROUP self +-- @return #boolean +function GROUP:OptionROEReturnFirePossible() + self:F2( { self.GroupName } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + if self:IsAir() or self:IsGround() or self:IsShip() then + return true + end + + return false + end + + return nil +end + +--- Return fire. +-- @param #GROUP self +-- @return #GROUP self +function GROUP:OptionROEReturnFire() + self:F2( { self.GroupName } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup 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 GROUP attack designated targets? +-- @param #GROUP self +-- @return #boolean +function GROUP:OptionROEOpenFirePossible() + self:F2( { self.GroupName } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + if self:IsAir() or self:IsGround() or self:IsShip() then + return true + end + + return false + end + + return nil +end + +--- Openfire. +-- @param #GROUP self +-- @return #GROUP self +function GROUP:OptionROEOpenFire() + self:F2( { self.GroupName } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup 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 GROUP attack targets of opportunity? +-- @param #GROUP self +-- @return #boolean +function GROUP:OptionROEWeaponFreePossible() + self:F2( { self.GroupName } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + if self:IsAir() then + return true + end + + return false + end + + return nil +end + +--- Weapon free. +-- @param #GROUP self +-- @return #GROUP self +function GROUP:OptionROEWeaponFree() + self:F2( { self.GroupName } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup 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 GROUP ignore enemy fire? +-- @param #GROUP self +-- @return #boolean +function GROUP:OptionROTNoReactionPossible() + self:F2( { self.GroupName } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + if self:IsAir() then + return true + end + + return false + end + + return nil +end + + +--- No evasion on enemy threats. +-- @param #GROUP self +-- @return #GROUP self +function GROUP:OptionROTNoReaction() + self:F2( { self.GroupName } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup 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 GROUP evade using passive defenses? +-- @param #GROUP self +-- @return #boolean +function GROUP:OptionROTPassiveDefensePossible() + self:F2( { self.GroupName } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + if self:IsAir() then + return true + end + + return false + end + + return nil +end + +--- Evasion passive defense. +-- @param #GROUP self +-- @return #GROUP self +function GROUP:OptionROTPassiveDefense() + self:F2( { self.GroupName } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup 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 GROUP evade on enemy fire? +-- @param #GROUP self +-- @return #boolean +function GROUP:OptionROTEvadeFirePossible() + self:F2( { self.GroupName } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + if self:IsAir() then + return true + end + + return false + end + + return nil +end + + +--- Evade on fire. +-- @param #GROUP self +-- @return #GROUP self +function GROUP:OptionROTEvadeFire() + self:F2( { self.GroupName } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup 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 GROUP evade on fire using vertical manoeuvres? +-- @param #GROUP self +-- @return #boolean +function GROUP:OptionROTVerticalPossible() + self:F2( { self.GroupName } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + if self:IsAir() then + return true + end + + return false + end + + return nil +end + + +--- Evade on fire using vertical manoeuvres. +-- @param #GROUP self +-- @return #GROUP self +function GROUP:OptionROTVertical() + self:F2( { self.GroupName } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup 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 + +-- Message APIs + +--- Returns a message for a coalition or a client. +-- @param #GROUP self +-- @param #string Message The message text +-- @param #Duration Duration The duration of the message. +-- @return Message#MESSAGE +function GROUP:Message( Message, Duration ) + self:F2( { Message, Duration } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + return MESSAGE:New( Message, self:GetCallsign() .. " (" .. self:GetTypeName() .. ")", Duration, self:GetClassNameAndID() ) + end + + return nil +end + +--- Send a message to all coalitions. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #GROUP self +-- @param #string Message The message text +-- @param #Duration Duration The duration of the message. +function GROUP:MessageToAll( Message, Duration ) + self:F2( { Message, Duration } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + self:Message( Message, Duration ):ToAll() + end + + return nil +end + +--- Send a message to the red coalition. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #GROUP self +-- @param #string Message The message text +-- @param #Duration Duration The duration of the message. +function GROUP:MessageToRed( Message, Duration ) + self:F2( { Message, Duration } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + self:Message( Message, Duration ):ToRed() + end + + return nil +end + +--- Send a message to the blue coalition. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #GROUP self +-- @param #string Message The message text +-- @param #Duration Duration The duration of the message. +function GROUP:MessageToBlue( Message, Duration ) + self:F2( { Message, Duration } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + self:Message( Message, Duration ):ToBlue() + end + + return nil +end + +--- Send a message to a client. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #GROUP self +-- @param #string Message The message text +-- @param #Duration Duration The duration of the message. +-- @param Client#CLIENT Client The client object receiving the message. +function GROUP:MessageToClient( Message, Duration, Client ) + self:F2( { Message, Duration } ) + + local DCSGroup = self:GetDCSGroup() + if DCSGroup then + self:Message( Message, Duration ):ToClient( Client ) + end + + return nil +end +--- UNIT Class +-- +-- @{UNIT} class +-- ============== +-- 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. +-- +-- +-- 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). +-- +-- DCS UNIT APIs +-- ============= +-- The DCS Unit APIs are used extensively within MOOSE. The UNIT class has for each DCS Unit API a corresponding method. +-- To be able to distinguish easily in your code the difference between a UNIT API call and a DCS Unit API call, +-- the first letter of the method is also capitalized. So, by example, the DCS Unit method @{DCSUnit#Unit.getName}() +-- is implemented in the UNIT class as @{#UNIT.GetName}(). +-- +-- Additional UNIT APIs +-- ==================== +-- The UNIT class comes with additional methods. Find below a summary. +-- +-- 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. +-- +-- Position, Point +-- --------------- +-- The UNIT class provides methods to obtain the current point or position of the DCS Unit. +-- The @{#UNIT.GetPointVec2}(), @{#UNIT.GetPointVec3}() will obtain the current location of the DCS Unit in a Vec2 (2D) or a 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. +-- +-- Alive +-- ----- +-- The @{#UNIT.IsAlive}(), @{#UNIT.IsActive}() methods determines if the DCS Unit is alive, meaning, it is existing and active. +-- +-- Test for other units in radius +-- ------------------------------ +-- One can test if another DCS Unit is within a given radius of the current DCS Unit, by using the @{#UNIT.OtherUnitInRadius}() method. +-- +-- More functions will be added +-- ---------------------------- +-- During the MOOSE development, more functions will be added. A complete list of the current functions is below. +-- +-- +-- +-- +-- @module Unit +-- @author FlightControl + +Include.File( "Routines" ) +Include.File( "Base" ) +Include.File( "Message" ) + +--- The UNIT class +-- @type UNIT +-- @extends Base#BASE +-- @field #UNIT.FlareColor FlareColor +-- @field #UNIT.SmokeColor SmokeColor +UNIT = { + ClassName="UNIT", + CategoryName = { + [Unit.Category.AIRPLANE] = "Airplane", + [Unit.Category.HELICOPTER] = "Helicoper", + [Unit.Category.GROUND_UNIT] = "Ground Unit", + [Unit.Category.SHIP] = "Ship", + [Unit.Category.STRUCTURE] = "Structure", + }, + FlareColor = { + Green = trigger.flareColor.Green, + Red = trigger.flareColor.Red, + White = trigger.flareColor.White, + Yellow = trigger.flareColor.Yellow + }, + SmokeColor = { + Green = trigger.smokeColor.Green, + Red = trigger.smokeColor.Red, + White = trigger.smokeColor.White, + Orange = trigger.smokeColor.Orange, + Blue = trigger.smokeColor.Blue + }, + } + +--- FlareColor +-- @type UNIT.FlareColor +-- @field Green +-- @field Red +-- @field White +-- @field Yellow + +--- SmokeColor +-- @type UNIT.SmokeColor +-- @field Green +-- @field Red +-- @field White +-- @field Orange +-- @field Blue + +-- Registration. + +--- Create a new UNIT from DCSUnit. +-- @param #UNIT self +-- @param DCSUnit#Unit DCSUnit +-- @param Database#DATABASE Database +-- @return Unit#UNIT +function UNIT:Register( UnitName ) + + local self = BASE:Inherit( self, BASE:New() ) + self:F2( UnitName ) + self.UnitName = UnitName + return self +end + +-- Reference methods. + +--- Finds a UNIT from the _DATABASE using a DCSUnit object. +-- @param #UNIT self +-- @param DCSUnit#Unit DCSUnit An existing DCS Unit object reference. +-- @return Unit#UNIT self +function UNIT:Find( DCSUnit ) + + local UnitName = DCSUnit:getName() + local UnitFound = _DATABASE:FindUnit( UnitName ) + return UnitFound +end + +--- Find a UNIT in the _DATABASE using the name of an existing DCS Unit. +-- @param #UNIT self +-- @param #string UnitName The Unit Name. +-- @return Unit#UNIT self +function UNIT:FindByName( UnitName ) + + local UnitFound = _DATABASE:FindUnit( UnitName ) + return UnitFound +end + +function UNIT:GetDCSUnit() + local DCSUnit = Unit.getByName( self.UnitName ) + + if DCSUnit then + return DCSUnit + end + + return nil +end + +--- Returns coalition of the Unit. +-- @param Unit#UNIT self +-- @return DCSCoalitionObject#coalition.side The side of the coalition. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetCoalition() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitCoalition = DCSUnit:getCoalition() + self:T3( UnitCoalition ) + return UnitCoalition + end + + return nil +end + +--- Returns country of the Unit. +-- @param Unit#UNIT self +-- @return DCScountry#country.id The country identifier. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetCountry() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitCountry = DCSUnit:getCountry() + self:T3( UnitCountry ) + return UnitCountry + end + + return nil +end + + +--- Returns DCS Unit object name. +-- The function provides access to non-activated units too. +-- @param Unit#UNIT self +-- @return #string The name of the DCS Unit. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetName() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitName = self.UnitName + return UnitName + end + + return nil +end + + +--- Returns if the unit is alive. +-- @param Unit#UNIT self +-- @return #boolean true if Unit is alive. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:IsAlive() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitIsAlive = DCSUnit:isExist() + return UnitIsAlive + end + + return false +end + +--- Returns if the unit is activated. +-- @param Unit#UNIT self +-- @return #boolean true if Unit is activated. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:IsActive() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + + local UnitIsActive = DCSUnit:isActive() + return UnitIsActive + end + + return nil +end + +--- Returns name of the player that control the unit or nil if the unit is controlled by A.I. +-- @param Unit#UNIT self +-- @return #string Player Name +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetPlayerName() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + + local PlayerName = DCSUnit:getPlayerName() + if PlayerName == nil then + PlayerName = "" + end + return PlayerName + end + + return nil +end + +--- Returns the unit's unique identifier. +-- @param Unit#UNIT self +-- @return DCSUnit#Unit.ID Unit ID +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetID() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitID = DCSUnit:getID() + return UnitID + end + + return nil +end + +--- Returns the unit's number in the group. +-- The number is the same number the unit has in ME. +-- It may not be changed during the mission. +-- If any unit in the group is destroyed, the numbers of another units will not be changed. +-- @param Unit#UNIT self +-- @return #number The Unit number. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetNumber() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitNumber = DCSUnit:getNumber() + return UnitNumber + end + + return nil +end + +--- Returns the unit's group if it exist and nil otherwise. +-- @param Unit#UNIT self +-- @return Group#GROUP The Group of the Unit. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetGroup() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitGroup = DCSUnit:getGroup() + return UnitGroup + end + + return nil +end + + +--- Returns the unit's callsign - the localized string. +-- @param Unit#UNIT self +-- @return #string The Callsign of the Unit. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetCallSign() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitCallSign = DCSUnit:getCallsign() + return UnitCallSign + end + + return nil +end + +--- Returns the unit's health. Dead units has health <= 1.0. +-- @param Unit#UNIT self +-- @return #number The Unit's health value. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetLife() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitLife = DCSUnit:getLife() + return UnitLife + end + + return nil +end + +--- Returns the Unit's initial health. +-- @param Unit#UNIT self +-- @return #number The Unit's initial health value. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetLife0() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitLife0 = DCSUnit:getLife0() + return UnitLife0 + end + + return nil +end + +--- Returns relative amount of fuel (from 0.0 to 1.0) the unit has in its internal tanks. If there are additional fuel tanks the value may be greater than 1.0. +-- @param Unit#UNIT self +-- @return #number The relative amount of fuel (from 0.0 to 1.0). +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetFuel() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitFuel = DCSUnit:getFuel() + return UnitFuel + end + + return nil +end + +--- Returns the Unit's ammunition. +-- @param Unit#UNIT self +-- @return DCSUnit#Unit.Ammo +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetAmmo() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitAmmo = DCSUnit:getAmmo() + return UnitAmmo + end + + return nil +end + +--- Returns the unit sensors. +-- @param Unit#UNIT self +-- @return DCSUnit#Unit.Sensors +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetSensors() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitSensors = DCSUnit:getSensors() + return UnitSensors + end + + return nil +end + +-- Need to add here a function per sensortype +-- unit:hasSensors(Unit.SensorType.RADAR, Unit.RadarType.AS) + +--- Returns two values: +-- +-- * First value indicates if at least one of the unit's radar(s) is on. +-- * Second value is the object of the radar's interest. Not nil only if at least one radar of the unit is tracking a target. +-- @param Unit#UNIT self +-- @return #boolean Indicates if at least one of the unit's radar(s) is on. +-- @return DCSObject#Object The object of the radar's interest. Not nil only if at least one radar of the unit is tracking a target. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetRadar() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitRadarOn, UnitRadarObject = DCSUnit:getRadar() + return UnitRadarOn, UnitRadarObject + end + + return nil, nil +end + +-- Need to add here functions to check if radar is on and which object etc. + +--- Returns unit descriptor. Descriptor type depends on unit category. +-- @param Unit#UNIT self +-- @return DCSUnit#Unit.Desc The Unit descriptor. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetDesc() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitDesc = DCSUnit:getDesc() + return UnitDesc + end + + return nil +end + + +--- Returns the type name of the DCS Unit. +-- @param Unit#UNIT self +-- @return #string The type name of the DCS Unit. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetTypeName() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitTypeName = DCSUnit:getTypeName() + self:T3( UnitTypeName ) + return UnitTypeName + end + + return nil +end + + + +--- Returns the prefix name of the DCS Unit. A prefix name is a part of the name before a '#'-sign. +-- DCS Units spawned with the @{SPAWN} class contain a '#'-sign to indicate the end of the (base) DCS Unit name. +-- The spawn sequence number and unit number are contained within the name after the '#' sign. +-- @param Unit#UNIT self +-- @return #string The name of the DCS Unit. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetPrefix() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitPrefix = string.match( self.UnitName, ".*#" ):sub( 1, -2 ) + self:T3( UnitPrefix ) + return UnitPrefix + end + + return nil +end + + + +--- Returns the @{DCSTypes#Vec2} vector indicating the point in 2D of the DCS Unit within the mission. +-- @param Unit#UNIT self +-- @return DCSTypes#Vec2 The 2D point vector of the DCS Unit. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetPointVec2() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitPointVec3 = DCSUnit:getPosition().p + + local UnitPointVec2 = {} + UnitPointVec2.x = UnitPointVec3.x + UnitPointVec2.y = UnitPointVec3.z + + self:T3( UnitPointVec2 ) + return UnitPointVec2 + end + + return nil +end + + +--- Returns the @{DCSTypes#Vec3} vector indicating the point in 3D of the DCS Unit within the mission. +-- @param Unit#UNIT self +-- @return DCSTypes#Vec3 The 3D point vector of the DCS Unit. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetPointVec3() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitPointVec3 = DCSUnit:getPosition().p + self:T3( UnitPointVec3 ) + return UnitPointVec3 + end + + return nil +end + +--- Returns the @{DCSTypes#Position3} position vectors indicating the point and direction vectors in 3D of the DCS Unit within the mission. +-- @param Unit#UNIT self +-- @return DCSTypes#Position The 3D position vectors of the DCS Unit. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetPositionVec3() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitPosition = DCSUnit:getPosition() + self:T3( UnitPosition ) + return UnitPosition + end + + return nil +end + +--- Returns the DCS Unit velocity vector. +-- @param Unit#UNIT self +-- @return DCSTypes#Vec3 The velocity vector +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetVelocity() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitVelocityVec3 = DCSUnit:getVelocity() + self:T3( UnitVelocityVec3 ) + return UnitVelocityVec3 + end + + return nil +end + +--- Returns true if the DCS Unit is in the air. +-- @param Unit#UNIT self +-- @return #boolean true if in the air. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:InAir() + self:F2( self.UnitName ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitInAir = DCSUnit:inAir() + self:T3( UnitInAir ) + return UnitInAir + end + + return nil +end + +--- Returns the altitude of the DCS Unit. +-- @param Unit#UNIT self +-- @return DCSTypes#Distance The altitude of the DCS Unit. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:GetAltitude() + self:F2() + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitPointVec3 = DCSUnit:getPoint() --DCSTypes#Vec3 + return UnitPointVec3.y + end + + return nil +end + +--- Returns true if there is an **other** DCS Unit within a radius of the current 2D point of the DCS Unit. +-- @param Unit#UNIT self +-- @param Unit#UNIT AwaitUnit The other UNIT wrapper object. +-- @param Radius The radius in meters with the DCS Unit in the centre. +-- @return true If the other DCS Unit is within the radius of the 2D point of the DCS Unit. +-- @return #nil The DCS Unit is not existing or alive. +function UNIT:OtherUnitInRadius( AwaitUnit, Radius ) + self:F2( { self.UnitName, AwaitUnit.UnitName, Radius } ) + + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitPos = self:GetPointVec3() + local AwaitUnitPos = AwaitUnit:GetPointVec3() + + if (((UnitPos.x - AwaitUnitPos.x)^2 + (UnitPos.z - AwaitUnitPos.z)^2)^0.5 <= Radius) then + self:T3( "true" ) + return true + else + self:T3( "false" ) + return false + end + end + + return nil +end + +--- Returns the DCS Unit category name as defined within the DCS Unit Descriptor. +-- @param Unit#UNIT self +-- @return #string The DCS Unit Category Name +function UNIT:GetCategoryName() + local DCSUnit = self:GetDCSUnit() + + if DCSUnit then + local UnitCategoryName = self.CategoryName[ self:GetDesc().category ] + return UnitCategoryName + end + + return nil +end + +--- Signal a flare at the position of the UNIT. +-- @param #UNIT self +function UNIT:Flare( FlareColor ) + self:F2() + trigger.action.signalFlare( self:GetPointVec3(), FlareColor , 0 ) +end + +--- Signal a white flare at the position of the UNIT. +-- @param #UNIT self +function UNIT:FlareWhite() + self:F2() + trigger.action.signalFlare( self:GetPointVec3(), trigger.flareColor.White , 0 ) +end + +--- Signal a yellow flare at the position of the UNIT. +-- @param #UNIT self +function UNIT:FlareYellow() + self:F2() + trigger.action.signalFlare( self:GetPointVec3(), trigger.flareColor.Yellow , 0 ) +end + +--- Signal a green flare at the position of the UNIT. +-- @param #UNIT self +function UNIT:FlareGreen() + self:F2() + trigger.action.signalFlare( self:GetPointVec3(), trigger.flareColor.Green , 0 ) +end + +--- Signal a red flare at the position of the UNIT. +-- @param #UNIT self +function UNIT:FlareRed() + self:F2() + trigger.action.signalFlare( self:GetPointVec3(), trigger.flareColor.Red, 0 ) +end + +--- Smoke the UNIT. +-- @param #UNIT self +function UNIT:Smoke( SmokeColor ) + self:F2() + trigger.action.smoke( self:GetPointVec3(), SmokeColor ) +end + +--- Smoke the UNIT Green. +-- @param #UNIT self +function UNIT:SmokeGreen() + self:F2() + trigger.action.smoke( self:GetPointVec3(), trigger.smokeColor.Green ) +end + +--- Smoke the UNIT Red. +-- @param #UNIT self +function UNIT:SmokeRed() + self:F2() + trigger.action.smoke( self:GetPointVec3(), trigger.smokeColor.Red ) +end + +--- Smoke the UNIT White. +-- @param #UNIT self +function UNIT:SmokeWhite() + self:F2() + trigger.action.smoke( self:GetPointVec3(), trigger.smokeColor.White ) +end + +--- Smoke the UNIT Orange. +-- @param #UNIT self +function UNIT:SmokeOrange() + self:F2() + trigger.action.smoke( self:GetPointVec3(), trigger.smokeColor.Orange ) +end + +--- Smoke the UNIT Blue. +-- @param #UNIT self +function UNIT:SmokeBlue() + self:F2() + trigger.action.smoke( self:GetPointVec3(), trigger.smokeColor.Blue ) +end + +-- Is methods + +--- Returns if the unit is of an air category. +-- If the unit is a helicopter or a plane, then this method will return true, otherwise false. +-- @param #UNIT self +-- @return #boolean Air category evaluation result. +function UNIT:IsAir() + self:F2() + + local UnitDescriptor = self.DCSUnit:getDesc() + self:T3( { UnitDescriptor.category, Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } ) + + local IsAirResult = ( UnitDescriptor.category == Unit.Category.AIRPLANE ) or ( UnitDescriptor.category == Unit.Category.HELICOPTER ) + + self:T3( IsAirResult ) + return IsAirResult +end + +--- ZONE Classes +-- @module Zone + +Include.File( "Routines" ) +Include.File( "Base" ) +Include.File( "Message" ) + +--- The ZONE class +-- @type ZONE +-- @Extends Base#BASE +ZONE = { + ClassName="ZONE", + } + +function ZONE:New( ZoneName ) + local self = BASE:Inherit( self, BASE:New() ) + self:F( ZoneName ) + + local Zone = trigger.misc.getZone( ZoneName ) + + if not Zone then + error( "Zone " .. ZoneName .. " does not exist." ) + return nil + end + + self.Zone = Zone + self.ZoneName = ZoneName + + return self +end + +function ZONE:GetPointVec2() + self:F( self.ZoneName ) + + local Zone = trigger.misc.getZone( self.ZoneName ) + local Point = { x = Zone.point.x, y = Zone.point.z } + + self:T( { Zone, Point } ) + + return Point +end + +function ZONE:GetPointVec3( Height ) + self:F( self.ZoneName ) + + local Zone = trigger.misc.getZone( self.ZoneName ) + local Point = { x = Zone.point.x, y = land.getHeight( self:GetPointVec2() ) + Height, z = Zone.point.z } + + self:T( { Zone, Point } ) + + return Point +end + +function ZONE:GetRandomPointVec2() + self:F( self.ZoneName ) + + local Point = {} + + local Zone = trigger.misc.getZone( self.ZoneName ) + + local angle = math.random() * math.pi*2; + Point.x = Zone.point.x + math.cos( angle ) * math.random() * Zone.radius; + Point.y = Zone.point.z + math.sin( angle ) * math.random() * Zone.radius; + + self:T( { Zone, Point } ) + + return Point +end + +function ZONE:GetRadius() + self:F( self.ZoneName ) + + local Zone = trigger.misc.getZone( self.ZoneName ) + + self:T( { Zone } ) + + return Zone.radius +end + +--- The CLIENT models client units in multi player missions. +-- +-- @{#CLIENT} class +-- ================ +-- Clients are those **Units** defined within the Mission Editor that have the skillset defined as __Client__ or __Player__. +-- Note that clients are NOT the same as Units, they are NOT necessarily alive. +-- The @{CLIENT} class is a wrapper class to handle the DCS Unit objects that have the skillset defined as __Client__ or __Player__: +-- +-- * 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. +-- +-- CLIENT reference methods +-- ======================= +-- For each DCS Unit having skill level Player or Client, a CLIENT wrapper object (instance) will be created within the _@{DATABASE} object. +-- This is done at the beginning of the mission (when the mission starts). +-- +-- The CLIENT class does not contain a :New() method, rather it provides :Find() methods to retrieve the object reference +-- using the DCS Unit or the DCS UnitName. +-- +-- Another thing to know is that CLIENT objects do not "contain" the DCS Unit object. +-- The CLIENT methods will reference the DCS Unit object by name when it is needed during API execution. +-- If the DCS Unit object does not exist or is nil, the CLIENT methods will return nil and log an exception in the DCS.log file. +-- +-- The CLIENT class provides the following functions to retrieve quickly the relevant CLIENT instance: +-- +-- * @{#CLIENT.Find}(): Find a CLIENT instance from the _DATABASE object using a DCS Unit object. +-- * @{#CLIENT.FindByName}(): Find a CLIENT instance from the _DATABASE object using a DCS Unit name. +-- +-- IMPORTANT: ONE SHOULD NEVER SANATIZE these CLIENT OBJECT REFERENCES! (make the CLIENT object references nil). +-- +-- @module Client +-- @author FlightControl + +Include.File( "Routines" ) +Include.File( "Base" ) +Include.File( "Cargo" ) +Include.File( "Message" ) + + +--- The CLIENT class +-- @type CLIENT +-- @extends Unit#UNIT +CLIENT = { + ONBOARDSIDE = { + NONE = 0, + LEFT = 1, + RIGHT = 2, + BACK = 3, + FRONT = 4 + }, + ClassName = "CLIENT", + ClientName = nil, + ClientAlive = false, + ClientTransport = false, + ClientBriefingShown = false, + _Menus = {}, + _Tasks = {}, + Messages = { + } +} + + +--- Finds a CLIENT from the _DATABASE using the relevant DCS Unit. +-- @param #CLIENT self +-- @param #string ClientName Name of the DCS **Unit** as defined within the Mission Editor. +-- @param #string ClientBriefing Text that describes the briefing of the mission when a Player logs into the Client. +-- @return #CLIENT +-- @usage +-- -- Create new Clients. +-- local Mission = MISSIONSCHEDULER.AddMission( 'Russia Transport Troops SA-6', 'Operational', 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', 'Russia' ) +-- Mission:AddGoal( DeploySA6TroopsGoal ) +-- +-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 1' ):Transport() ) +-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 3' ):Transport() ) +-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 2' ):Transport() ) +-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 4' ):Transport() ) +function CLIENT:Find( DCSUnit ) + local ClientName = DCSUnit:getName() + local ClientFound = _DATABASE:FindClient( ClientName ) + + if ClientFound then + ClientFound:F( ClientName ) + return ClientFound + end + + error( "CLIENT not found for: " .. ClientName ) +end + + +--- Finds a CLIENT from the _DATABASE using the relevant Client Unit Name. +-- As an optional parameter, a briefing text can be given also. +-- @param #CLIENT self +-- @param #string ClientName Name of the DCS **Unit** as defined within the Mission Editor. +-- @param #string ClientBriefing Text that describes the briefing of the mission when a Player logs into the Client. +-- @return #CLIENT +-- @usage +-- -- Create new Clients. +-- local Mission = MISSIONSCHEDULER.AddMission( 'Russia Transport Troops SA-6', 'Operational', 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', 'Russia' ) +-- Mission:AddGoal( DeploySA6TroopsGoal ) +-- +-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 1' ):Transport() ) +-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 3' ):Transport() ) +-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 2' ):Transport() ) +-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 4' ):Transport() ) +function CLIENT:FindByName( ClientName, ClientBriefing ) + local ClientFound = _DATABASE:FindClient( ClientName ) + + if ClientFound then + ClientFound:F( { ClientName, ClientBriefing } ) + ClientFound:AddBriefing( ClientBriefing ) + ClientFound.MessageSwitch = true + + return ClientFound + end + + error( "CLIENT not found for: " .. ClientName ) +end + +function CLIENT:Register( ClientName ) + local self = BASE:Inherit( self, UNIT:Register( ClientName ) ) + + self:F( ClientName ) + self.ClientName = ClientName + self.MessageSwitch = true + self.ClientAlive2 = false + + --self.AliveCheckScheduler = routines.scheduleFunction( self._AliveCheckScheduler, { self }, timer.getTime() + 1, 5 ) + self.AliveCheckScheduler = SCHEDULER:New( self, self._AliveCheckScheduler, {}, 1, 5 ) + + 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, self.ClientName .. '/ClientBriefing', "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, self.ClientName .. '/MissionBriefing', "Mission Briefing" ) + end + + return self +end + + + +--- Resets a CLIENT. +-- @param #CLIENT self +-- @param #string ClientName Name of the Group as defined within the Mission Editor. The Group must have a Unit with the type Client. +function CLIENT:Reset( ClientName ) + self:F() + self._Menus = {} +end + +-- Is Functions + +--- Checks if the CLIENT is a multi-seated UNIT. +-- @param #CLIENT self +-- @return #boolean true if multi-seated. +function CLIENT:IsMultiSeated() + self:F( self.ClientName ) + + local ClientMultiSeatedTypes = { + ["Mi-8MT"] = "Mi-8MT", + ["UH-1H"] = "UH-1H", + ["P-51B"] = "P-51B" + } + + if self:IsAlive() then + local ClientTypeName = self:GetClientGroupUnit():GetTypeName() + if ClientMultiSeatedTypes[ClientTypeName] then + return true + end + end + + return false +end + +--- Checks for a client alive event and calls a function on a continuous basis. +-- @param #CLIENT self +-- @param #function CallBack Function. +-- @return #CLIENT +function CLIENT:Alive( CallBack, ... ) + self:F() + + self.ClientCallBack = CallBack + self.ClientParameters = arg + + return self +end + +--- @param #CLIENT self +function CLIENT:_AliveCheckScheduler() + self:F( { self.ClientName, self.ClientAlive2, self.ClientBriefingShown } ) + + if self:IsAlive() then -- Polymorphic call of UNIT + if self.ClientAlive2 == false then + self:ShowBriefing() + if self.ClientCallBack then + self:T("Calling Callback function") + self.ClientCallBack( self, unpack( self.ClientParameters ) ) + end + self.ClientAlive2 = true + end + else + if self.ClientAlive2 == true then + self.ClientAlive2 = false + end + end + + return true +end + +--- Return the DCSGroup of a Client. +-- This function is modified to deal with a couple of bugs in DCS 1.5.3 +-- @param #CLIENT self +-- @return DCSGroup#Group +function CLIENT:GetDCSGroup() + self:F3() + +-- local ClientData = Group.getByName( self.ClientName ) +-- if ClientData and ClientData:isExist() then +-- self:T( self.ClientName .. " : group found!" ) +-- return ClientData +-- else +-- return nil +-- end + + local ClientUnit = Unit.getByName( self.ClientName ) + + local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ) } + for CoalitionId, CoalitionData in pairs( CoalitionsData ) do + self:T3( { "CoalitionData:", CoalitionData } ) + for UnitId, UnitData in pairs( CoalitionData ) do + self:T3( { "UnitData:", UnitData } ) + if UnitData and UnitData:isExist() then + + --self:E(self.ClientName) + if ClientUnit then + local ClientGroup = ClientUnit:getGroup() + if ClientGroup then + self:T3( "ClientGroup = " .. self.ClientName ) + if ClientGroup:isExist() and UnitData:getGroup():isExist() then + if ClientGroup:getID() == UnitData:getGroup():getID() then + self:T3( "Normal logic" ) + self:T3( self.ClientName .. " : group found!" ) + self.ClientGroupID = ClientGroup:getID() + self.ClientGroupName = ClientGroup:getName() + return ClientGroup + end + else + -- Now we need to resolve the bugs in DCS 1.5 ... + -- Consult the database for the units of the Client Group. (ClientGroup:getUnits() returns nil) + self:T3( "Bug 1.5 logic" ) + local ClientGroupTemplate = _DATABASE.Templates.Units[self.ClientName].GroupTemplate + self.ClientGroupID = ClientGroupTemplate.groupId + self.ClientGroupName = _DATABASE.Templates.Units[self.ClientName].GroupName + self:T3( self.ClientName .. " : group found in bug 1.5 resolvement logic!" ) + return ClientGroup + end + -- else + -- error( "Client " .. self.ClientName .. " not found!" ) + end + else + --self:E( { "Client not found!", self.ClientName } ) + end + end + end + end + + -- For non player clients + if ClientUnit then + local ClientGroup = ClientUnit:getGroup() + if ClientGroup then + self:T3( "ClientGroup = " .. self.ClientName ) + if ClientGroup:isExist() then + self:T3( "Normal logic" ) + self:T3( self.ClientName .. " : group found!" ) + return ClientGroup + end + end + end + + self.ClientGroupID = nil + self.ClientGroupUnit = nil + + return nil +end + + +-- TODO: Check DCSTypes#Group.ID +--- Get the group ID of the client. +-- @param #CLIENT self +-- @return DCSTypes#Group.ID +function CLIENT:GetClientGroupID() + + local ClientGroup = self:GetDCSGroup() + + --self:E( self.ClientGroupID ) -- Determined in GetDCSGroup() + return self.ClientGroupID +end + + +--- Get the name of the group of the client. +-- @param #CLIENT self +-- @return #string +function CLIENT:GetClientGroupName() + + local ClientGroup = self:GetDCSGroup() + + self:T( self.ClientGroupName ) -- Determined in GetDCSGroup() + return self.ClientGroupName +end + +--- Returns the UNIT of the CLIENT. +-- @param #CLIENT self +-- @return Unit#UNIT +function CLIENT:GetClientGroupUnit() + self:F2() + + local ClientDCSUnit = Unit.getByName( self.ClientName ) + + self:T( self.ClientDCSUnit ) + if ClientDCSUnit and ClientDCSUnit:isExist() then + local ClientUnit = _DATABASE:FindUnit( self.ClientName ) + self:T2( ClientUnit ) + return ClientUnit + end +end + +--- Returns the DCSUnit of the CLIENT. +-- @param #CLIENT self +-- @return DCSTypes#Unit +function CLIENT:GetClientGroupDCSUnit() + self:F2() + + local ClientDCSUnit = Unit.getByName( self.ClientName ) + + if ClientDCSUnit and ClientDCSUnit:isExist() then + self:T2( ClientDCSUnit ) + return ClientDCSUnit + end +end + + +--- Evaluates if the CLIENT is a transport. +-- @param #CLIENT self +-- @return #boolean true is a transport. +function CLIENT:IsTransport() + self:F() + return self.ClientTransport +end + +--- Shows the @{Cargo#CARGO} contained within the CLIENT to the player as a message. +-- The @{Cargo#CARGO} is shown using the @{Message#MESSAGE} distribution system. +-- @param #CLIENT self +function CLIENT:ShowCargo() + self:F() + + local CargoMsg = "" + + for CargoName, Cargo in pairs( CARGOS ) do + if self == Cargo:IsLoadedInClient() then + CargoMsg = CargoMsg .. Cargo.CargoName .. " Type:" .. Cargo.CargoType .. " Weight: " .. Cargo.CargoWeight .. "\n" + end + end + + if CargoMsg == "" then + CargoMsg = "empty" + end + + self:Message( CargoMsg, 15, self.ClientName .. "/Cargo", "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 MessageId is a text identifying the Message in the MessageQueue. The Message system overwrites Messages with the same MessageId +-- @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. +function CLIENT:Message( Message, MessageDuration, MessageId, MessageCategory, MessageInterval ) + self:F( { Message, MessageDuration, MessageId, MessageCategory, MessageInterval } ) + + if not self.MenuMessages then + if self:GetClientGroupID() then + self.MenuMessages = MENU_CLIENT:New( self, 'Messages' ) + self.MenuRouteMessageOn = MENU_CLIENT_COMMAND:New( self, 'Messages On', self.MenuMessages, CLIENT.SwitchMessages, { self, true } ) + self.MenuRouteMessageOff = MENU_CLIENT_COMMAND:New( self,'Messages Off', self.MenuMessages, CLIENT.SwitchMessages, { self, false } ) + end + end + + if self.MessageSwitch == true then + if MessageCategory == nil then + MessageCategory = "Messages" + end + if 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, MessageCategory, MessageDuration, MessageId ):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, MessageCategory, MessageDuration, MessageId ):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, MessageCategory, MessageDuration, MessageId ):ToClient( self ) + self.Messages[MessageId].MessageTime = timer.getTime() + end + end + end + end +end +--- Manage the mission database. +-- +-- @{#DATABASE} class +-- ================== +-- Mission designers can use the DATABASE class to refer to: +-- +-- * UNITS +-- * GROUPS +-- * players +-- * alive players +-- * CLIENTS +-- * alive CLIENTS +-- +-- On top, for internal MOOSE administration purposes, the DATBASE administers the Unit and Gruop 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. +-- +-- 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 player it finds within the DATABASE. +-- * @{#DATABASE.ForEachPlayerAlive}: Calls a function for each alive 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 + +Include.File( "Routines" ) +Include.File( "Base" ) +Include.File( "Menu" ) +Include.File( "Group" ) +Include.File( "Unit" ) +Include.File( "Event" ) +Include.File( "Client" ) + + +--- DATABASE class +-- @type DATABASE +-- @extends Base#BASE +DATABASE = { + ClassName = "DATABASE", + Templates = { + Units = {}, + Groups = {}, + ClientsByName = {}, + ClientsByID = {}, + }, + DCSUnits = {}, + DCSGroups = {}, + UNITS = {}, + GROUPS = {}, + PLAYERS = {}, + PLAYERSALIVE = {}, + CLIENTS = {}, + CLIENTSALIVE = {}, + NavPoints = {}, +} + +local _DATABASECoalition = + { + [1] = "Red", + [2] = "Blue", + } + +local _DATABASECategory = + { + [Unit.Category.AIRPLANE] = "Plane", + [Unit.Category.HELICOPTER] = "Helicopter", + [Unit.Category.GROUND_UNIT] = "Vehicle", + [Unit.Category.SHIP] = "Ship", + [Unit.Category.STRUCTURE] = "Structure", + } + + +--- Creates a new DATABASE object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. +-- @param #DATABASE self +-- @return #DATABASE +-- @usage +-- -- Define a new DATABASE Object. This DBObject will contain a reference to all Group and Unit Templates defined within the ME and the DCSRTE. +-- DBObject = DATABASE:New() +function DATABASE:New() + + -- Inherits from BASE + local self = BASE:Inherit( self, BASE:New() ) + + _EVENTDISPATCHER:OnBirth( self._EventOnBirth, self ) + _EVENTDISPATCHER:OnDead( self._EventOnDeadOrCrash, self ) + _EVENTDISPATCHER:OnCrash( self._EventOnDeadOrCrash, self ) + + + -- Follow alive players and clients + _EVENTDISPATCHER:OnPlayerEnterUnit( self._EventOnPlayerEnterUnit, self ) + _EVENTDISPATCHER:OnPlayerLeaveUnit( self._EventOnPlayerLeaveUnit, self ) + + self:_RegisterTemplates() + self:_RegisterDatabase() + self:_RegisterPlayers() + + return self +end + +--- Finds a Unit based on the Unit Name. +-- @param #DATABASE self +-- @param #string UnitName +-- @return Unit#UNIT The found Unit. +function DATABASE:FindUnit( UnitName ) + + local UnitFound = self.UNITS[UnitName] + return UnitFound +end + + +--- Adds a Unit based on the Unit Name in the DATABASE. +-- @param #DATABASE self +function DATABASE:AddUnit( DCSUnit, DCSUnitName ) + + self.DCSUnits[DCSUnitName] = DCSUnit + self.UNITS[DCSUnitName] = UNIT:Register( DCSUnitName ) +end + + +--- Deletes a Unit from the DATABASE based on the Unit Name. +-- @param #DATABASE self +function DATABASE:DeleteUnit( DCSUnitName ) + + self.DCSUnits[DCSUnitName] = nil +end + + +--- Finds a CLIENT based on the ClientName. +-- @param #DATABASE self +-- @param #string ClientName +-- @return Client#CLIENT The found CLIENT. +function DATABASE:FindClient( ClientName ) + + local ClientFound = self.CLIENTS[ClientName] + return ClientFound +end + + +--- Adds a CLIENT based on the ClientName in the DATABASE. +-- @param #DATABASE self +function DATABASE:AddClient( ClientName ) + + self.CLIENTS[ClientName] = CLIENT:Register( ClientName ) + self:E( self.CLIENTS[ClientName]:GetClassNameAndID() ) +end + + +--- Finds a GROUP based on the GroupName. +-- @param #DATABASE self +-- @param #string GroupName +-- @return Group#GROUP The found GROUP. +function DATABASE:FindGroup( GroupName ) + + local GroupFound = self.GROUPS[GroupName] + return GroupFound +end + + +--- Adds a GROUP based on the GroupName in the DATABASE. +-- @param #DATABASE self +function DATABASE:AddGroup( DCSGroup, GroupName ) + + self.DCSGroups[GroupName] = DCSGroup + self.GROUPS[GroupName] = GROUP:Register( 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] = PlayerName + self.PLAYERSALIVE[PlayerName] = PlayerName + self.CLIENTSALIVE[PlayerName] = self:FindClient( UnitName ) + 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.PLAYERSALIVE[PlayerName] = nil + self.CLIENTSALIVE[PlayerName] = nil + end +end + + +--- Instantiate new Groups within the DCSRTE. +-- This method expects EXACTLY the same structure as a structure within the ME, and needs 2 additional fields defined: +-- SpawnCountryID, SpawnCategoryID +-- This method is used by the SPAWN class. +-- @param #DATABASE self +-- @param #table SpawnTemplate +-- @return #DATABASE self +function DATABASE:Spawn( SpawnTemplate ) + self:F2( SpawnTemplate.name ) + + self:T2( { SpawnTemplate.SpawnCountryID, SpawnTemplate.SpawnCategoryID } ) + + -- Copy the spawn variables of the template in temporary storage, nullify, and restore the spawn variables. + local SpawnCoalitionID = SpawnTemplate.SpawnCoalitionID + local SpawnCountryID = SpawnTemplate.SpawnCountryID + local SpawnCategoryID = SpawnTemplate.SpawnCategoryID + + -- Nullify + SpawnTemplate.SpawnCoalitionID = nil + SpawnTemplate.SpawnCountryID = nil + SpawnTemplate.SpawnCategoryID = nil + + self:_RegisterTemplate( SpawnTemplate ) + coalition.addGroup( SpawnCountryID, SpawnCategoryID, SpawnTemplate ) + + -- Restore + SpawnTemplate.SpawnCoalitionID = SpawnCoalitionID + SpawnTemplate.SpawnCountryID = SpawnCountryID + SpawnTemplate.SpawnCategoryID = SpawnCategoryID + + + local SpawnGroup = GROUP:Register( 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 ) + + local GroupTemplateName = env.getValueDictByKey(GroupTemplate.name) + + if not self.Templates.Groups[GroupTemplateName] then + self.Templates.Groups[GroupTemplateName] = {} + self.Templates.Groups[GroupTemplateName].Status = nil + end + + -- Delete the spans from the route, it is not needed and takes memory. + if GroupTemplate.route and GroupTemplate.route.spans then + GroupTemplate.route.spans = nil + end + + self.Templates.Groups[GroupTemplateName].GroupName = GroupTemplateName + self.Templates.Groups[GroupTemplateName].Template = GroupTemplate + self.Templates.Groups[GroupTemplateName].groupId = GroupTemplate.groupId + self.Templates.Groups[GroupTemplateName].UnitCount = #GroupTemplate.units + self.Templates.Groups[GroupTemplateName].Units = GroupTemplate.units + + self:T2( { "Group", self.Templates.Groups[GroupTemplateName].GroupName, self.Templates.Groups[GroupTemplateName].UnitCount } ) + + for unit_num, UnitTemplate in pairs( GroupTemplate.units ) do + + local UnitTemplateName = env.getValueDictByKey(UnitTemplate.name) + self.Templates.Units[UnitTemplateName] = {} + self.Templates.Units[UnitTemplateName].UnitName = UnitTemplateName + self.Templates.Units[UnitTemplateName].Template = UnitTemplate + self.Templates.Units[UnitTemplateName].GroupName = GroupTemplateName + self.Templates.Units[UnitTemplateName].GroupTemplate = GroupTemplate + self.Templates.Units[UnitTemplateName].GroupId = GroupTemplate.groupId + self:E( {"skill",UnitTemplate.skill}) + if UnitTemplate.skill and (UnitTemplate.skill == "Client" or UnitTemplate.skill == "Player") then + self.Templates.ClientsByName[UnitTemplateName] = UnitTemplate + self.Templates.ClientsByID[UnitTemplate.unitId] = UnitTemplate + end + self:E( { "Unit", self.Templates.Units[UnitTemplateName].UnitName } ) + end +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 datapoints within in the mission. +-- @param #DATABASE self +-- @return #DATABASE self +function DATABASE:_RegisterDatabase() + + 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:", DCSGroup, DCSGroupName } ) + self:AddGroup( DCSGroup, DCSGroupName ) + + for DCSUnitId, DCSUnit in pairs( DCSGroup:getUnits() ) do + + local DCSUnitName = DCSUnit:getName() + self:E( { "Register Unit:", DCSUnit, DCSUnitName } ) + self:AddUnit( DCSUnit, DCSUnitName ) + end + else + self:E( { "Group does not exist: ", DCSGroup } ) + end + + end + end + + for ClientName, ClientTemplate in pairs( self.Templates.ClientsByName ) do + self:E( { "Adding Client:", ClientName } ) + self:AddClient( ClientName ) + end + + return self +end + +--- Events + +--- Handles the OnBirth event for the alive units set. +-- @param #DATABASE self +-- @param Event#EVENTDATA Event +function DATABASE:_EventOnBirth( Event ) + self:F2( { Event } ) + + if Event.IniDCSUnit then + self:AddUnit( Event.IniDCSUnit, Event.IniDCSUnitName ) + self:AddGroup( Event.IniDCSGroup, Event.IniDCSGroupName ) + self:_EventOnPlayerEnterUnit( Event ) + end +end + + +--- Handles the OnDead or OnCrash event for alive units set. +-- @param #DATABASE self +-- @param Event#EVENTDATA Event +function DATABASE:_EventOnDeadOrCrash( Event ) + self:F2( { Event } ) + + if Event.IniDCSUnit then + if self.DCSUnits[Event.IniDCSUnitName] then + self:DeleteUnit( Event.IniDCSUnitName ) + -- add logic to correctly remove a group once all units are destroyed... + end + end +end + + +--- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). +-- @param #DATABASE self +-- @param Event#EVENTDATA Event +function DATABASE:_EventOnPlayerEnterUnit( Event ) + self:F2( { Event } ) + + if Event.IniDCSUnit then + local PlayerName = Event.IniDCSUnit:getPlayerName() + if not self.PLAYERSALIVE[PlayerName] then + self:AddPlayer( Event.IniDCSUnitName, PlayerName ) + end + end +end + + +--- Handles the OnPlayerLeaveUnit event to clean the active players table. +-- @param #DATABASE self +-- @param Event#EVENTDATA Event +function DATABASE:_EventOnPlayerLeaveUnit( Event ) + self:F2( { Event } ) + + if Event.IniDCSUnit then + local PlayerName = Event.IniDCSUnit:getPlayerName() + if self.PLAYERSALIVE[PlayerName] then + self:DeletePlayer( PlayerName ) + end + end +end + +--- Iterators + +--- Iterate the DATABASE and call an iterator function for the given set, providing the Object for each element within the set and optional parameters. +-- @param #DATABASE self +-- @param #function IteratorFunction The function that will be called when there is an alive player in the database. +-- @return #DATABASE self +function DATABASE:ForEach( IteratorFunction, 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 % 10 == 0 then + coroutine.yield( false ) + end + end + return true + end + + local co = coroutine.create( CoRoutine ) + + local function Schedule() + + local status, res = coroutine.resume( co ) + self:T2( { status, res } ) + + if status == false then + error( res ) + end + if res == false then + return true -- resume next time the loop + end + + return false + end + + local Scheduler = SCHEDULER:New( self, Schedule, {}, 0.001, 0.001, 0 ) + + return self +end + + +--- Iterate the DATABASE and call an iterator function for each **alive** unit, providing the DCSUnit 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 DCSUnit parameter. +-- @return #DATABASE self +function DATABASE:ForEachDCSUnit( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.DCSUnits ) + + 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, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, 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 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 **alive** player, providing the Unit of the player 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 UNIT parameter. +-- @return #DATABASE self +function DATABASE:ForEachPlayerAlive( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.PLAYERSALIVE ) + + 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 + +--- Iterate the DATABASE and call an iterator function for each **ALIVE** 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 CLIENT in the database. The function needs to accept a CLIENT parameter. +-- @return #DATABASE self +function DATABASE:ForEachClientAlive( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self.CLIENTSALIVE ) + + return self +end + + +function DATABASE:_RegisterTemplates() + self:F2() + + self.Navpoints = {} + self.UNITS = {} + --Build routines.db.units and self.Navpoints + 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 + --self.Units[coa_name] = {} + + ---------------------------------------------- + -- build nav points DB + self.Navpoints[coa_name] = {} + 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[coa_name][nav_ind] = routines.utils.deepCopy(nav_data) + + self.Navpoints[coa_name][nav_ind]['name'] = nav_data.callsignStr -- name is a little bit more self-explanatory. + self.Navpoints[coa_name][nav_ind]['point'] = {} -- point is used by SSE, support it. + self.Navpoints[coa_name][nav_ind]['point']['x'] = nav_data.x + self.Navpoints[coa_name][nav_ind]['point']['y'] = 0 + self.Navpoints[coa_name][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.lower(cntry_data.name) + --self.Units[coa_name][countryName] = {} + --self.Units[coa_name][countryName]["countryId"] = cntry_data.id + + if type(cntry_data) == 'table' then --just making sure + + for obj_type_name, obj_type_data in pairs(cntry_data) do + + if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then --should be an unncessary check + + local category = 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 ) + 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 + + + + +--- The main include file for the MOOSE system. + +Include.File( "Routines" ) +Include.File( "Base" ) +Include.File( "Database" ) +Include.File( "Event" ) + +-- The order of the declarations is important here. Don't touch it. + +--- Declare the event dispatcher based on the EVENT class +_EVENTDISPATCHER = EVENT:New() -- #EVENT + +--- Declare the main database object, which is used internally by the MOOSE classes. +_DATABASE = DATABASE:New() -- Database#DATABASE + +--- Scoring system for MOOSE. +-- This scoring class calculates the hits and kills that players make within a simulation session. +-- Scoring is calculated using a defined algorithm. +-- With a small change in MissionScripting.lua, the scoring can also be logged in a CSV file, that can then be uploaded +-- to a database or a BI tool to publish the scoring results to the player community. +-- @module Scoring +-- @author FlightControl + + +Include.File( "Routines" ) +Include.File( "Base" ) +Include.File( "Menu" ) +Include.File( "Group" ) +Include.File( "Event" ) + + +--- The Scoring class +-- @type SCORING +-- @field Players A collection of the current players that have joined the game. +-- @extends Base#BASE +SCORING = { + ClassName = "SCORING", + ClassID = 0, + Players = {}, +} + +local _SCORINGCoalition = + { + [1] = "Red", + [2] = "Blue", + } + +local _SCORINGCategory = + { + [Unit.Category.AIRPLANE] = "Plane", + [Unit.Category.HELICOPTER] = "Helicopter", + [Unit.Category.GROUND_UNIT] = "Vehicle", + [Unit.Category.SHIP] = "Ship", + [Unit.Category.STRUCTURE] = "Structure", + } + +--- Creates a new SCORING object to administer the scoring achieved by players. +-- @param #SCORING self +-- @param #string GameName The name of the game. This name is also logged in the CSV score file. +-- @return #SCORING self +-- @usage +-- -- Define a new scoring object for the mission Gori Valley. +-- ScoringObject = SCORING:New( "Gori Valley" ) +function SCORING:New( GameName ) + + -- Inherits from BASE + local self = BASE:Inherit( self, BASE:New() ) + + if GameName then + self.GameName = GameName + else + error( "A game name must be given to register the scoring results" ) + end + + + _EVENTDISPATCHER:OnDead( self._EventOnDeadOrCrash, self ) + _EVENTDISPATCHER:OnCrash( self._EventOnDeadOrCrash, self ) + _EVENTDISPATCHER:OnHit( self._EventOnHit, self ) + + --self.SchedulerId = routines.scheduleFunction( SCORING._FollowPlayersScheduled, { self }, 0, 5 ) + self.SchedulerId = SCHEDULER:New( self, self._FollowPlayersScheduled, {}, 0, 5 ) + + self:ScoreMenu() + + return self + +end + +--- Creates a score radio menu. Can be accessed using Radio -> F10. +-- @param #SCORING self +-- @return #SCORING self +function SCORING:ScoreMenu() + self.Menu = SUBMENU:New( 'Scoring' ) + self.AllScoresMenu = COMMANDMENU:New( 'Score All Active Players', self.Menu, SCORING.ReportScoreAll, self ) + --- = COMMANDMENU:New('Your Current Score', ReportScore, SCORING.ReportScorePlayer, self ) + return self +end + +--- Follows new players entering Clients within the DCSRTE. +-- TODO: Need to see if i can catch this also with an event. It will eliminate the schedule ... +function SCORING:_FollowPlayersScheduled() + self:F3( "_FollowPlayersScheduled" ) + + local ClientUnit = 0 + local CoalitionsData = { AlivePlayersRed = coalition.getPlayers(coalition.side.RED), AlivePlayersBlue = coalition.getPlayers(coalition.side.BLUE) } + local unitId + local unitData + local AlivePlayerUnits = {} + + for CoalitionId, CoalitionData in pairs( CoalitionsData ) do + self:T3( { "_FollowPlayersScheduled", CoalitionData } ) + for UnitId, UnitData in pairs( CoalitionData ) do + self:_AddPlayerFromUnit( UnitData ) + end + end + + return true +end + + +--- Track DEAD or CRASH events for the scoring. +-- @param #SCORING self +-- @param Event#EVENTDATA Event +function SCORING:_EventOnDeadOrCrash( Event ) + self:F( { Event } ) + + local TargetUnit = nil + local TargetGroup = nil + local TargetUnitName = "" + local TargetGroupName = "" + local TargetPlayerName = "" + local TargetCoalition = nil + local TargetCategory = nil + local TargetType = nil + local TargetUnitCoalition = nil + local TargetUnitCategory = nil + local TargetUnitType = nil + + if Event.IniDCSUnit then + + TargetUnit = Event.IniDCSUnit + TargetUnitName = Event.IniDCSUnitName + TargetGroup = Event.IniDCSGroup + TargetGroupName = Event.IniDCSGroupName + TargetPlayerName = TargetUnit:getPlayerName() + + TargetCoalition = TargetUnit:getCoalition() + --TargetCategory = TargetUnit:getCategory() + TargetCategory = TargetUnit:getDesc().category -- Workaround + TargetType = TargetUnit:getTypeName() + + TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] + TargetUnitCategory = _SCORINGCategory[TargetCategory] + TargetUnitType = TargetType + + self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType } ) + end + + for PlayerName, PlayerData in pairs( self.Players ) do + if PlayerData then -- This should normally not happen, but i'll test it anyway. + self:T( "Something got killed" ) + + -- Some variables + local InitUnitName = PlayerData.UnitName + local InitUnitType = PlayerData.UnitType + local InitCoalition = PlayerData.UnitCoalition + local InitCategory = PlayerData.UnitCategory + local InitUnitCoalition = _SCORINGCoalition[InitCoalition] + local InitUnitCategory = _SCORINGCategory[InitCategory] + + self:T( { InitUnitName, InitUnitType, InitUnitCoalition, InitCoalition, InitUnitCategory, InitCategory } ) + + -- What is he hitting? + if TargetCategory then + if PlayerData and PlayerData.Hit and PlayerData.Hit[TargetCategory] and PlayerData.Hit[TargetCategory][TargetUnitName] then -- Was there a hit for this unit for this player before registered??? + if not PlayerData.Kill[TargetCategory] then + PlayerData.Kill[TargetCategory] = {} + end + if not PlayerData.Kill[TargetCategory][TargetType] then + PlayerData.Kill[TargetCategory][TargetType] = {} + PlayerData.Kill[TargetCategory][TargetType].Score = 0 + PlayerData.Kill[TargetCategory][TargetType].ScoreKill = 0 + PlayerData.Kill[TargetCategory][TargetType].Penalty = 0 + PlayerData.Kill[TargetCategory][TargetType].PenaltyKill = 0 + end + + if InitCoalition == TargetCoalition then + PlayerData.Penalty = PlayerData.Penalty + 25 + PlayerData.Kill[TargetCategory][TargetType].Penalty = PlayerData.Kill[TargetCategory][TargetType].Penalty + 25 + PlayerData.Kill[TargetCategory][TargetType].PenaltyKill = PlayerData.Kill[TargetCategory][TargetType].PenaltyKill + 1 + MESSAGE:New( "Player '" .. PlayerName .. "' killed a friendly " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. + PlayerData.Kill[TargetCategory][TargetType].PenaltyKill .. " times. Penalty: -" .. PlayerData.Kill[TargetCategory][TargetType].Penalty .. + ". Score Total:" .. PlayerData.Score - PlayerData.Penalty, + "", 5, "/PENALTY" .. PlayerName .. "/" .. InitUnitName ):ToAll() + self:ScoreCSV( PlayerName, "KILL_PENALTY", 1, -125, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + else + PlayerData.Score = PlayerData.Score + 10 + PlayerData.Kill[TargetCategory][TargetType].Score = PlayerData.Kill[TargetCategory][TargetType].Score + 10 + PlayerData.Kill[TargetCategory][TargetType].ScoreKill = PlayerData.Kill[TargetCategory][TargetType].ScoreKill + 1 + MESSAGE:New( "Player '" .. PlayerName .. "' killed an enemy " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. + PlayerData.Kill[TargetCategory][TargetType].ScoreKill .. " times. Score: " .. PlayerData.Kill[TargetCategory][TargetType].Score .. + ". Score Total:" .. PlayerData.Score - PlayerData.Penalty, + "", 5, "/SCORE" .. PlayerName .. "/" .. InitUnitName ):ToAll() + self:ScoreCSV( PlayerName, "KILL_SCORE", 1, 10, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + end + end + end + end + end +end + + + +--- Add a new player entering a Unit. +function SCORING:_AddPlayerFromUnit( UnitData ) + self:F( UnitData ) + + if UnitData and UnitData:isExist() then + local UnitName = UnitData:getName() + local PlayerName = UnitData:getPlayerName() + local UnitDesc = UnitData:getDesc() + local UnitCategory = UnitDesc.category + local UnitCoalition = UnitData:getCoalition() + local UnitTypeName = UnitData:getTypeName() + + self:T( { PlayerName, UnitName, UnitCategory, UnitCoalition, UnitTypeName } ) + + if self.Players[PlayerName] == nil then -- I believe this is the place where a Player gets a life in a mission when he enters a unit ... + self.Players[PlayerName] = {} + self.Players[PlayerName].Hit = {} + self.Players[PlayerName].Kill = {} + self.Players[PlayerName].Mission = {} + + -- for CategoryID, CategoryName in pairs( SCORINGCategory ) do + -- self.Players[PlayerName].Hit[CategoryID] = {} + -- self.Players[PlayerName].Kill[CategoryID] = {} + -- end + self.Players[PlayerName].HitPlayers = {} + self.Players[PlayerName].HitUnits = {} + self.Players[PlayerName].Score = 0 + self.Players[PlayerName].Penalty = 0 + self.Players[PlayerName].PenaltyCoalition = 0 + self.Players[PlayerName].PenaltyWarning = 0 + end + + if not self.Players[PlayerName].UnitCoalition then + self.Players[PlayerName].UnitCoalition = UnitCoalition + else + if self.Players[PlayerName].UnitCoalition ~= UnitCoalition then + self.Players[PlayerName].Penalty = self.Players[PlayerName].Penalty + 50 + self.Players[PlayerName].PenaltyCoalition = self.Players[PlayerName].PenaltyCoalition + 1 + MESSAGE:New( "Player '" .. PlayerName .. "' changed coalition from " .. _SCORINGCoalition[self.Players[PlayerName].UnitCoalition] .. " to " .. _SCORINGCoalition[UnitCoalition] .. + "(changed " .. self.Players[PlayerName].PenaltyCoalition .. " times the coalition). 50 Penalty points added.", + "", + 2, + "/PENALTYCOALITION" .. PlayerName + ):ToAll() + self:ScoreCSV( PlayerName, "COALITION_PENALTY", 1, -50, self.Players[PlayerName].UnitName, _SCORINGCoalition[self.Players[PlayerName].UnitCoalition], _SCORINGCategory[self.Players[PlayerName].UnitCategory], self.Players[PlayerName].UnitType, + UnitName, _SCORINGCoalition[UnitCoalition], _SCORINGCategory[UnitCategory], UnitData:getTypeName() ) + end + end + self.Players[PlayerName].UnitName = UnitName + self.Players[PlayerName].UnitCoalition = UnitCoalition + self.Players[PlayerName].UnitCategory = UnitCategory + self.Players[PlayerName].UnitType = UnitTypeName + + if self.Players[PlayerName].Penalty > 100 then + if self.Players[PlayerName].PenaltyWarning < 1 then + MESSAGE:New( "Player '" .. PlayerName .. "': WARNING! If you continue to commit FRATRICIDE and have a PENALTY score higher than 150, you will be COURT MARTIALED and DISMISSED from this mission! \nYour total penalty is: " .. self.Players[PlayerName].Penalty, + "", + 30, + "/PENALTYCOALITION" .. PlayerName + ):ToAll() + self.Players[PlayerName].PenaltyWarning = self.Players[PlayerName].PenaltyWarning + 1 + end + end + + if self.Players[PlayerName].Penalty > 150 then + ClientGroup = GROUP:NewFromDCSUnit( UnitData ) + ClientGroup:Destroy() + MESSAGE:New( "Player '" .. PlayerName .. "' committed FRATRICIDE, he will be COURT MARTIALED and is DISMISSED from this mission!", + "", + 10, + "/PENALTYCOALITION" .. PlayerName + ):ToAll() + end + + end +end + + +--- Registers Scores the players completing a Mission Task. +function SCORING:_AddMissionTaskScore( PlayerUnit, MissionName, Score ) + self:F( { PlayerUnit, MissionName, Score } ) + + local PlayerName = PlayerUnit:getPlayerName() + + if not self.Players[PlayerName].Mission[MissionName] then + self.Players[PlayerName].Mission[MissionName] = {} + self.Players[PlayerName].Mission[MissionName].ScoreTask = 0 + self.Players[PlayerName].Mission[MissionName].ScoreMission = 0 + end + + self:T( PlayerName ) + self:T( self.Players[PlayerName].Mission[MissionName] ) + + self.Players[PlayerName].Score = self.Players[PlayerName].Score + Score + self.Players[PlayerName].Mission[MissionName].ScoreTask = self.Players[PlayerName].Mission[MissionName].ScoreTask + Score + + MESSAGE:New( "Player '" .. PlayerName .. "' has finished another Task in Mission '" .. MissionName .. "'. " .. + Score .. " Score points added.", + "", 20, "/SCORETASK" .. PlayerName ):ToAll() + + self:ScoreCSV( PlayerName, "TASK_" .. MissionName:gsub( ' ', '_' ), 1, Score, PlayerUnit:getName() ) +end + + +--- Registers Mission Scores for possible multiple players that contributed in the Mission. +function SCORING:_AddMissionScore( MissionName, Score ) + self:F( { MissionName, Score } ) + + for PlayerName, PlayerData in pairs( self.Players ) do + + if PlayerData.Mission[MissionName] then + PlayerData.Score = PlayerData.Score + Score + PlayerData.Mission[MissionName].ScoreMission = PlayerData.Mission[MissionName].ScoreMission + Score + MESSAGE:New( "Player '" .. PlayerName .. "' has finished Mission '" .. MissionName .. "'. " .. + Score .. " Score points added.", + "", 20, "/SCOREMISSION" .. PlayerName ):ToAll() + self:ScoreCSV( PlayerName, "MISSION_" .. MissionName:gsub( ' ', '_' ), 1, Score ) + end + end +end + +--- Handles the OnHit event for the scoring. +-- @param #SCORING self +-- @param Event#EVENTDATA Event +function SCORING:_EventOnHit( Event ) + self:F( { Event } ) + + local InitUnit = nil + local InitUnitName = "" + local InitGroup = nil + local InitGroupName = "" + local InitPlayerName = nil + + local InitCoalition = nil + local InitCategory = nil + local InitType = nil + local InitUnitCoalition = nil + local InitUnitCategory = nil + local InitUnitType = nil + + local TargetUnit = nil + local TargetUnitName = "" + local TargetGroup = nil + local TargetGroupName = "" + local TargetPlayerName = "" + + local TargetCoalition = nil + local TargetCategory = nil + local TargetType = nil + local TargetUnitCoalition = nil + local TargetUnitCategory = nil + local TargetUnitType = nil + + if Event.IniDCSUnit then + + InitUnit = Event.IniDCSUnit + InitUnitName = Event.IniDCSUnitName + InitGroup = Event.IniDCSGroup + InitGroupName = Event.IniDCSGroupName + InitPlayerName = InitUnit:getPlayerName() + + InitCoalition = InitUnit:getCoalition() + --TODO: Workaround Client DCS Bug + --InitCategory = InitUnit:getCategory() + InitCategory = InitUnit:getDesc().category + InitType = InitUnit:getTypeName() + + InitUnitCoalition = _SCORINGCoalition[InitCoalition] + InitUnitCategory = _SCORINGCategory[InitCategory] + InitUnitType = InitType + + self:T( { InitUnitName, InitGroupName, InitPlayerName, InitCoalition, InitCategory, InitType , InitUnitCoalition, InitUnitCategory, InitUnitType } ) + end + + + if Event.TgtDCSUnit then + + TargetUnit = Event.TgtDCSUnit + TargetUnitName = Event.TgtDCSUnitName + TargetGroup = Event.TgtDCSGroup + TargetGroupName = Event.TgtDCSGroupName + TargetPlayerName = TargetUnit:getPlayerName() + + TargetCoalition = TargetUnit:getCoalition() + --TODO: Workaround Client DCS Bug + --TargetCategory = TargetUnit:getCategory() + TargetCategory = TargetUnit:getDesc().category + TargetType = TargetUnit:getTypeName() + + TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] + TargetUnitCategory = _SCORINGCategory[TargetCategory] + TargetUnitType = TargetType + + self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType, TargetUnitCoalition, TargetUnitCategory, TargetUnitType } ) + end + + if InitPlayerName ~= nil then -- It is a player that is hitting something + self:_AddPlayerFromUnit( InitUnit ) + if self.Players[InitPlayerName] then -- This should normally not happen, but i'll test it anyway. + if TargetPlayerName ~= nil then -- It is a player hitting another player ... + self:_AddPlayerFromUnit( TargetUnit ) + self.Players[InitPlayerName].HitPlayers = self.Players[InitPlayerName].HitPlayers + 1 + end + + self:T( "Hitting Something" ) + -- What is he hitting? + if TargetCategory then + if not self.Players[InitPlayerName].Hit[TargetCategory] then + self.Players[InitPlayerName].Hit[TargetCategory] = {} + end + if not self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName] then + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName] = {} + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score = 0 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty = 0 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit = 0 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit = 0 + end + local Score = 0 + if InitCoalition == TargetCoalition then + self.Players[InitPlayerName].Penalty = self.Players[InitPlayerName].Penalty + 10 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty + 10 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit + 1 + MESSAGE:New( "Player '" .. InitPlayerName .. "' hit a friendly " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].PenaltyHit .. " times. Penalty: -" .. self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Penalty .. + ". Score Total:" .. self.Players[InitPlayerName].Score - self.Players[InitPlayerName].Penalty, + "", + 2, + "/PENALTY" .. InitPlayerName .. "/" .. InitUnitName + ):ToAll() + self:ScoreCSV( InitPlayerName, "HIT_PENALTY", 1, -25, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + else + self.Players[InitPlayerName].Score = self.Players[InitPlayerName].Score + 10 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score + 1 + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit = self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit + 1 + MESSAGE:New( "Player '" .. InitPlayerName .. "' hit a target " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. + self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].ScoreHit .. " times. Score: " .. self.Players[InitPlayerName].Hit[TargetCategory][TargetUnitName].Score .. + ". Score Total:" .. self.Players[InitPlayerName].Score - self.Players[InitPlayerName].Penalty, + "", + 2, + "/SCORE" .. InitPlayerName .. "/" .. InitUnitName + ):ToAll() + self:ScoreCSV( InitPlayerName, "HIT_SCORE", 1, 1, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + end + end + end + elseif InitPlayerName == nil then -- It is an AI hitting a player??? + + end +end + + +function SCORING:ReportScoreAll() + + env.info( "Hello World " ) + + local ScoreMessage = "" + local PlayerMessage = "" + + self:T( "Score Report" ) + + for PlayerName, PlayerData in pairs( self.Players ) do + if PlayerData then -- This should normally not happen, but i'll test it anyway. + self:T( "Score Player: " .. PlayerName ) + + -- Some variables + local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] + local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] + local InitUnitType = PlayerData.UnitType + local InitUnitName = PlayerData.UnitName + + local PlayerScore = 0 + local PlayerPenalty = 0 + + ScoreMessage = ":\n" + + local ScoreMessageHits = "" + + for CategoryID, CategoryName in pairs( _SCORINGCategory ) do + self:T( CategoryName ) + if PlayerData.Hit[CategoryID] then + local Score = 0 + local ScoreHit = 0 + local Penalty = 0 + local PenaltyHit = 0 + self:T( "Hit scores exist for player " .. PlayerName ) + for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do + Score = Score + UnitData.Score + ScoreHit = ScoreHit + UnitData.ScoreHit + Penalty = Penalty + UnitData.Penalty + PenaltyHit = UnitData.PenaltyHit + end + local ScoreMessageHit = string.format( "%s:%d ", CategoryName, Score - Penalty ) + self:T( ScoreMessageHit ) + ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit + PlayerScore = PlayerScore + Score + PlayerPenalty = PlayerPenalty + Penalty + else + --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) + end + end + if ScoreMessageHits ~= "" then + ScoreMessage = ScoreMessage .. " Hits: " .. ScoreMessageHits .. "\n" + end + + local ScoreMessageKills = "" + for CategoryID, CategoryName in pairs( _SCORINGCategory ) do + self:T( "Kill scores exist for player " .. PlayerName ) + if PlayerData.Kill[CategoryID] then + local Score = 0 + local ScoreKill = 0 + local Penalty = 0 + local PenaltyKill = 0 + + for UnitName, UnitData in pairs( PlayerData.Kill[CategoryID] ) do + Score = Score + UnitData.Score + ScoreKill = ScoreKill + UnitData.ScoreKill + Penalty = Penalty + UnitData.Penalty + PenaltyKill = PenaltyKill + UnitData.PenaltyKill + end + + local ScoreMessageKill = string.format( " %s:%d ", CategoryName, Score - Penalty ) + self:T( ScoreMessageKill ) + ScoreMessageKills = ScoreMessageKills .. ScoreMessageKill + + PlayerScore = PlayerScore + Score + PlayerPenalty = PlayerPenalty + Penalty + else + --ScoreMessageKills = ScoreMessageKills .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) + end + end + if ScoreMessageKills ~= "" then + ScoreMessage = ScoreMessage .. " Kills: " .. ScoreMessageKills .. "\n" + end + + local ScoreMessageCoalitionChangePenalties = "" + if PlayerData.PenaltyCoalition ~= 0 then + ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) + PlayerPenalty = PlayerPenalty + PlayerData.Penalty + end + if ScoreMessageCoalitionChangePenalties ~= "" then + ScoreMessage = ScoreMessage .. " Coalition Penalties: " .. ScoreMessageCoalitionChangePenalties .. "\n" + end + + local ScoreMessageMission = "" + local ScoreMission = 0 + local ScoreTask = 0 + for MissionName, MissionData in pairs( PlayerData.Mission ) do + ScoreMission = ScoreMission + MissionData.ScoreMission + ScoreTask = ScoreTask + MissionData.ScoreTask + ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " + end + PlayerScore = PlayerScore + ScoreMission + ScoreTask + + if ScoreMessageMission ~= "" then + ScoreMessage = ScoreMessage .. " Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ")\n" + end + + PlayerMessage = PlayerMessage .. string.format( "Player '%s' Score:%d (%d Score -%d Penalties)%s", PlayerName, PlayerScore - PlayerPenalty, PlayerScore, PlayerPenalty, ScoreMessage ) + end + end + MESSAGE:New( PlayerMessage, "Player Scores", 30, "AllPlayerScores"):ToAll() +end + + +function SCORING:ReportScorePlayer() + + env.info( "Hello World " ) + + local ScoreMessage = "" + local PlayerMessage = "" + + self:T( "Score Report" ) + + for PlayerName, PlayerData in pairs( self.Players ) do + if PlayerData then -- This should normally not happen, but i'll test it anyway. + self:T( "Score Player: " .. PlayerName ) + + -- Some variables + local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition] + local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory] + local InitUnitType = PlayerData.UnitType + local InitUnitName = PlayerData.UnitName + + local PlayerScore = 0 + local PlayerPenalty = 0 + + ScoreMessage = "" + + local ScoreMessageHits = "" + + for CategoryID, CategoryName in pairs( _SCORINGCategory ) do + self:T( CategoryName ) + if PlayerData.Hit[CategoryID] then + local Score = 0 + local ScoreHit = 0 + local Penalty = 0 + local PenaltyHit = 0 + self:T( "Hit scores exist for player " .. PlayerName ) + for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do + Score = Score + UnitData.Score + ScoreHit = ScoreHit + UnitData.ScoreHit + Penalty = Penalty + UnitData.Penalty + PenaltyHit = UnitData.PenaltyHit + end + local ScoreMessageHit = string.format( "\n %s = %d score(%d;-%d) hits(#%d;#-%d)", CategoryName, Score - Penalty, Score, Penalty, ScoreHit, PenaltyHit ) + self:T( ScoreMessageHit ) + ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit + PlayerScore = PlayerScore + Score + PlayerPenalty = PlayerPenalty + Penalty + else + --ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) + end + end + if ScoreMessageHits ~= "" then + ScoreMessage = ScoreMessage .. "\n Hits: " .. ScoreMessageHits .. " " + end + + local ScoreMessageKills = "" + for CategoryID, CategoryName in pairs( _SCORINGCategory ) do + self:T( "Kill scores exist for player " .. PlayerName ) + if PlayerData.Kill[CategoryID] then + local Score = 0 + local ScoreKill = 0 + local Penalty = 0 + local PenaltyKill = 0 + + for UnitName, UnitData in pairs( PlayerData.Kill[CategoryID] ) do + Score = Score + UnitData.Score + ScoreKill = ScoreKill + UnitData.ScoreKill + Penalty = Penalty + UnitData.Penalty + PenaltyKill = PenaltyKill + UnitData.PenaltyKill + end + + local ScoreMessageKill = string.format( "\n %s = %d score(%d;-%d) hits(#%d;#-%d)", CategoryName, Score - Penalty, Score, Penalty, ScoreKill, PenaltyKill ) + self:T( ScoreMessageKill ) + ScoreMessageKills = ScoreMessageKills .. ScoreMessageKill + + PlayerScore = PlayerScore + Score + PlayerPenalty = PlayerPenalty + Penalty + else + --ScoreMessageKills = ScoreMessageKills .. string.format( "%s:%d ", string.format(CategoryName, 1, 1), 0 ) + end + end + if ScoreMessageKills ~= "" then + ScoreMessage = ScoreMessage .. "\n Kills: " .. ScoreMessageKills .. " " + end + + local ScoreMessageCoalitionChangePenalties = "" + if PlayerData.PenaltyCoalition ~= 0 then + ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition ) + PlayerPenalty = PlayerPenalty + PlayerData.Penalty + end + if ScoreMessageCoalitionChangePenalties ~= "" then + ScoreMessage = ScoreMessage .. "\n Coalition: " .. ScoreMessageCoalitionChangePenalties .. " " + end + + local ScoreMessageMission = "" + local ScoreMission = 0 + local ScoreTask = 0 + for MissionName, MissionData in pairs( PlayerData.Mission ) do + ScoreMission = ScoreMission + MissionData.ScoreMission + ScoreTask = ScoreTask + MissionData.ScoreTask + ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; " + end + PlayerScore = PlayerScore + ScoreMission + ScoreTask + + if ScoreMessageMission ~= "" then + ScoreMessage = ScoreMessage .. "\n Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ") " + end + + PlayerMessage = PlayerMessage .. string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties ):%s", PlayerName, PlayerScore - PlayerPenalty, PlayerScore, PlayerPenalty, ScoreMessage ) + end + end + MESSAGE:New( PlayerMessage, "Player Scores", 30, "AllPlayerScores"):ToAll() + +end + + +function SCORING:SecondsToClock(sSeconds) + local nSeconds = sSeconds + if nSeconds == 0 then + --return nil; + return "00:00:00"; + else + nHours = string.format("%02.f", math.floor(nSeconds/3600)); + nMins = string.format("%02.f", math.floor(nSeconds/60 - (nHours*60))); + nSecs = string.format("%02.f", math.floor(nSeconds - nHours*3600 - nMins *60)); + return nHours..":"..nMins..":"..nSecs + end +end + +--- Opens a score CSV file to log the scores. +-- @param #SCORING self +-- @param #string ScoringCSV +-- @return #SCORING self +-- @usage +-- -- Open a new CSV file to log the scores of the game Gori Valley. Let the name of the CSV file begin with "Player Scores". +-- ScoringObject = SCORING:New( "Gori Valley" ) +-- ScoringObject:OpenCSV( "Player Scores" ) +function SCORING:OpenCSV( ScoringCSV ) + self:F( ScoringCSV ) + + if lfs and io and os then + if ScoringCSV then + self.ScoringCSV = ScoringCSV + local fdir = lfs.writedir() .. [[Logs\]] .. self.ScoringCSV .. " " .. os.date( "%Y-%m-%d %H-%M-%S" ) .. ".csv" + + self.CSVFile, self.err = io.open( fdir, "w+" ) + if not self.CSVFile then + error( "Error: Cannot open CSV file in " .. lfs.writedir() ) + end + + self.CSVFile:write( '"GameName","RunTime","Time","PlayerName","ScoreType","PlayerUnitCoaltion","PlayerUnitCategory","PlayerUnitType","PlayerUnitName","TargetUnitCoalition","TargetUnitCategory","TargetUnitType","TargetUnitName","Times","Score"\n' ) + + self.RunTime = os.date("%y-%m-%d_%H-%M-%S") + else + error( "A string containing the CSV file name must be given." ) + end + else + self:E( "The MissionScripting.lua file has not been changed to allow lfs, io and os modules to be used..." ) + end + return self +end + + +--- Registers a score for a player. +-- @param #SCORING self +-- @param #string PlayerName The name of the player. +-- @param #string ScoreType The type of the score. +-- @param #string ScoreTimes The amount of scores achieved. +-- @param #string ScoreAmount The score given. +-- @param #string PlayerUnitName The unit name of the player. +-- @param #string PlayerUnitCoalition The coalition of the player unit. +-- @param #string PlayerUnitCategory The category of the player unit. +-- @param #string PlayerUnitType The type of the player unit. +-- @param #string TargetUnitName The name of the target unit. +-- @param #string TargetUnitCoalition The coalition of the target unit. +-- @param #string TargetUnitCategory The category of the target unit. +-- @param #string TargetUnitType The type of the target unit. +-- @return #SCORING self +function SCORING:ScoreCSV( PlayerName, ScoreType, ScoreTimes, ScoreAmount, PlayerUnitName, PlayerUnitCoalition, PlayerUnitCategory, PlayerUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + --write statistic information to file + local ScoreTime = self:SecondsToClock( timer.getTime() ) + PlayerName = PlayerName:gsub( '"', '_' ) + + if PlayerUnitName and PlayerUnitName ~= '' then + local PlayerUnit = Unit.getByName( PlayerUnitName ) + + if PlayerUnit then + if not PlayerUnitCategory then + --PlayerUnitCategory = SCORINGCategory[PlayerUnit:getCategory()] + PlayerUnitCategory = _SCORINGCategory[PlayerUnit:getDesc().category] + end + + if not PlayerUnitCoalition then + PlayerUnitCoalition = _SCORINGCoalition[PlayerUnit:getCoalition()] + end + + if not PlayerUnitType then + PlayerUnitType = PlayerUnit:getTypeName() + end + else + PlayerUnitName = '' + PlayerUnitCategory = '' + PlayerUnitCoalition = '' + PlayerUnitType = '' + end + else + PlayerUnitName = '' + PlayerUnitCategory = '' + PlayerUnitCoalition = '' + PlayerUnitType = '' + end + + if not TargetUnitCoalition then + TargetUnitCoalition = '' + end + + if not TargetUnitCategory then + TargetUnitCategory = '' + end + + if not TargetUnitType then + TargetUnitType = '' + end + + if not TargetUnitName then + TargetUnitName = '' + end + + if lfs and io and os then + self.CSVFile:write( + '"' .. self.GameName .. '"' .. ',' .. + '"' .. self.RunTime .. '"' .. ',' .. + '' .. ScoreTime .. '' .. ',' .. + '"' .. PlayerName .. '"' .. ',' .. + '"' .. ScoreType .. '"' .. ',' .. + '"' .. PlayerUnitCoalition .. '"' .. ',' .. + '"' .. PlayerUnitCategory .. '"' .. ',' .. + '"' .. PlayerUnitType .. '"' .. ',' .. + '"' .. PlayerUnitName .. '"' .. ',' .. + '"' .. TargetUnitCoalition .. '"' .. ',' .. + '"' .. TargetUnitCategory .. '"' .. ',' .. + '"' .. TargetUnitType .. '"' .. ',' .. + '"' .. TargetUnitName .. '"' .. ',' .. + '' .. ScoreTimes .. '' .. ',' .. + '' .. ScoreAmount + ) + + self.CSVFile:write( "\n" ) + end +end + + +function SCORING:CloseCSV() + if lfs and io and os then + self.CSVFile:close() + end +end + +--- CARGO Classes +-- @module CARGO + +Include.File( "Routines" ) +Include.File( "Base" ) +Include.File( "Message" ) +Include.File( "Scheduler" ) + + +--- Clients are those Groups defined within the Mission Editor that have the skillset defined as "Client" or "Player". +-- These clients are defined within the Mission Orchestration Framework (MOF) + +CARGOS = {} + + +CARGO_ZONE = { + ClassName="CARGO_ZONE", + CargoZoneName = '', + CargoHostUnitName = '', + SIGNAL = { + TYPE = { + SMOKE = { ID = 1, TEXT = "smoke" }, + FLARE = { ID = 2, TEXT = "flare" } + }, + COLOR = { + GREEN = { ID = 1, TRIGGERCOLOR = trigger.smokeColor.Green, TEXT = "A green" }, + RED = { ID = 2, TRIGGERCOLOR = trigger.smokeColor.Red, TEXT = "A red" }, + WHITE = { ID = 3, TRIGGERCOLOR = trigger.smokeColor.White, TEXT = "A white" }, + ORANGE = { ID = 4, TRIGGERCOLOR = trigger.smokeColor.Orange, TEXT = "An orange" }, + BLUE = { ID = 5, TRIGGERCOLOR = trigger.smokeColor.Blue, TEXT = "A blue" }, + YELLOW = { ID = 6, TRIGGERCOLOR = trigger.flareColor.Yellow, TEXT = "A yellow" } + } + } +} + +--- Creates a new zone where cargo can be collected or deployed. +-- The zone functionality is useful to smoke or indicate routes for cargo pickups or deployments. +-- Provide the zone name as declared in the mission file into the CargoZoneName in the :New method. +-- An optional parameter is the CargoHostName, which is a Group declared with Late Activation switched on in the mission file. +-- The CargoHostName is the "host" of the cargo zone: +-- +-- * It will smoke the zone position when a client is approaching the zone. +-- * Depending on the cargo type, it will assist in the delivery of the cargo by driving to and from the client. +-- +-- @param #CARGO_ZONE self +-- @param #string CargoZoneName The name of the zone as declared within the mission editor. +-- @param #string CargoHostName The name of the Group "hosting" the zone. The Group MUST NOT be a static, and must be a "mobile" unit. +function CARGO_ZONE:New( CargoZoneName, CargoHostName ) local self = BASE:Inherit( self, ZONE:New( CargoZoneName ) ) + self:F( { CargoZoneName, CargoHostName } ) + + self.CargoZoneName = CargoZoneName + self.SignalHeight = 2 + --self.CargoZone = trigger.misc.getZone( CargoZoneName ) + + + if CargoHostName then + self.CargoHostName = CargoHostName + end + + self:T( self.CargoZoneName ) + + return self +end + +function CARGO_ZONE:Spawn() + self:F( self.CargoHostName ) + + if self.CargoHostName then -- Only spawn a host in the zone when there is one given as a parameter in the New function. + if self.CargoHostSpawn then + local CargoHostGroup = self.CargoHostSpawn:GetGroupFromIndex() + if CargoHostGroup and CargoHostGroup:IsAlive() then + else + self.CargoHostSpawn:ReSpawn( 1 ) + end + else + self:T( "Initialize CargoHostSpawn" ) + self.CargoHostSpawn = SPAWN:New( self.CargoHostName ):Limit( 1, 1 ) + self.CargoHostSpawn:ReSpawn( 1 ) + end + end + + return self +end + +function CARGO_ZONE:GetHostUnit() + self:F( self ) + + if self.CargoHostName then + + -- A Host has been given, signal the host + local CargoHostGroup = self.CargoHostSpawn:GetGroupFromIndex() + local CargoHostUnit + if CargoHostGroup and CargoHostGroup:IsAlive() then + CargoHostUnit = CargoHostGroup:GetUnit(1) + else + CargoHostUnit = StaticObject.getByName( self.CargoHostName ) + end + + return CargoHostUnit + end + + return nil +end + +function CARGO_ZONE:ReportCargosToClient( Client, CargoType ) + self:F() + + local SignalUnit = self:GetHostUnit() + + if SignalUnit then + + local SignalUnitTypeName = SignalUnit:getTypeName() + + local HostMessage = "" + + local IsCargo = false + for CargoID, Cargo in pairs( CARGOS ) do + if Cargo.CargoType == Task.CargoType then + if Cargo:IsStatusNone() then + HostMessage = HostMessage .. " - " .. Cargo.CargoName .. " - " .. Cargo.CargoType .. " (" .. Cargo.Weight .. "kg)" .. "\n" + IsCargo = true + end + end + end + + if not IsCargo then + HostMessage = "No Cargo Available." + end + + Client:Message( HostMessage, 20, Mission.Name .. "/StageHosts." .. SignalUnitTypeName, SignalUnitTypeName .. ": Reporting Cargo", 10 ) + end +end + + +function CARGO_ZONE:Signal() + self:F() + + local Signalled = false + + if self.SignalType then + + if self.CargoHostName then + + -- A Host has been given, signal the host + + local SignalUnit = self:GetHostUnit() + + if SignalUnit then + + self:T( 'Signalling Unit' ) + local SignalVehiclePos = SignalUnit:GetPointVec3() + SignalVehiclePos.y = SignalVehiclePos.y + 2 + + if self.SignalType.ID == CARGO_ZONE.SIGNAL.TYPE.SMOKE.ID then + + trigger.action.smoke( SignalVehiclePos, self.SignalColor.TRIGGERCOLOR ) + Signalled = true + + elseif self.SignalType.ID == CARGO_ZONE.SIGNAL.TYPE.FLARE.ID then + + trigger.action.signalFlare( SignalVehiclePos, self.SignalColor.TRIGGERCOLOR , 0 ) + Signalled = false + + end + end + + else + + local ZonePointVec3 = self:GetPointVec3( self.SignalHeight ) -- Get the zone position + the landheight + 2 meters + + if self.SignalType.ID == CARGO_ZONE.SIGNAL.TYPE.SMOKE.ID then + + trigger.action.smoke( ZonePointVec3, self.SignalColor.TRIGGERCOLOR ) + Signalled = true + + elseif self.SignalType.ID == CARGO_ZONE.SIGNAL.TYPE.FLARE.ID then + trigger.action.signalFlare( ZonePointVec3, self.SignalColor.TRIGGERCOLOR, 0 ) + Signalled = false + + end + end + end + + return Signalled + +end + +function CARGO_ZONE:WhiteSmoke( SignalHeight ) + self:F() + + self.SignalType = CARGO_ZONE.SIGNAL.TYPE.SMOKE + self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.WHITE + + if SignalHeight then + self.SignalHeight = SignalHeight + end + + return self +end + +function CARGO_ZONE:BlueSmoke( SignalHeight ) + self:F() + + self.SignalType = CARGO_ZONE.SIGNAL.TYPE.SMOKE + self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.BLUE + + if SignalHeight then + self.SignalHeight = SignalHeight + end + + return self +end + +function CARGO_ZONE:RedSmoke( SignalHeight ) + self:F() + + self.SignalType = CARGO_ZONE.SIGNAL.TYPE.SMOKE + self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.RED + + if SignalHeight then + self.SignalHeight = SignalHeight + end + + return self +end + +function CARGO_ZONE:OrangeSmoke( SignalHeight ) + self:F() + + self.SignalType = CARGO_ZONE.SIGNAL.TYPE.SMOKE + self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.ORANGE + + if SignalHeight then + self.SignalHeight = SignalHeight + end + + return self +end + +function CARGO_ZONE:GreenSmoke( SignalHeight ) + self:F() + + self.SignalType = CARGO_ZONE.SIGNAL.TYPE.SMOKE + self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.GREEN + + if SignalHeight then + self.SignalHeight = SignalHeight + end + + return self +end + + +function CARGO_ZONE:WhiteFlare( SignalHeight ) + self:F() + + self.SignalType = CARGO_ZONE.SIGNAL.TYPE.FLARE + self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.WHITE + + if SignalHeight then + self.SignalHeight = SignalHeight + end + + return self +end + +function CARGO_ZONE:RedFlare( SignalHeight ) + self:F() + + self.SignalType = CARGO_ZONE.SIGNAL.TYPE.FLARE + self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.RED + + if SignalHeight then + self.SignalHeight = SignalHeight + end + + return self +end + +function CARGO_ZONE:GreenFlare( SignalHeight ) + self:F() + + self.SignalType = CARGO_ZONE.SIGNAL.TYPE.FLARE + self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.GREEN + + if SignalHeight then + self.SignalHeight = SignalHeight + end + + return self +end + +function CARGO_ZONE:YellowFlare( SignalHeight ) + self:F() + + self.SignalType = CARGO_ZONE.SIGNAL.TYPE.FLARE + self.SignalColor = CARGO_ZONE.SIGNAL.COLOR.YELLOW + + if SignalHeight then + self.SignalHeight = SignalHeight + end + + return self +end + + +function CARGO_ZONE:GetCargoHostUnit() + self:F( self ) + + if self.CargoHostSpawn then + local CargoHostGroup = self.CargoHostSpawn:GetGroupFromIndex(1) + if CargoHostGroup and CargoHostGroup:IsAlive() then + local CargoHostUnit = CargoHostGroup:GetUnit(1) + if CargoHostUnit and CargoHostUnit:IsAlive() then + return CargoHostUnit + end + end + end + + return nil +end + +function CARGO_ZONE:GetCargoZoneName() + self:F() + + return self.CargoZoneName +end + +CARGO = { + ClassName = "CARGO", + STATUS = { + NONE = 0, + LOADED = 1, + UNLOADED = 2, + LOADING = 3 + }, + CargoClient = nil +} + +--- Add Cargo to the mission... Cargo functionality needs to be reworked a bit, so this is still under construction. I need to make a CARGO Class... +function CARGO:New( CargoType, CargoName, CargoWeight ) local self = BASE:Inherit( self, BASE:New() ) + self:F( { CargoType, CargoName, CargoWeight } ) + + + self.CargoType = CargoType + self.CargoName = CargoName + self.CargoWeight = CargoWeight + + self:StatusNone() + + return self +end + +function CARGO:Spawn( Client ) + self:F() + + return self + +end + +function CARGO:IsNear( Client, LandingZone ) + self:F() + + local Near = true + + return Near + +end + + +function CARGO:IsLoadingToClient() + self:F() + + if self:IsStatusLoading() then + return self.CargoClient + end + + return nil + +end + + +function CARGO:IsLoadedInClient() + self:F() + + if self:IsStatusLoaded() then + return self.CargoClient + end + + return nil + +end + + +function CARGO:UnLoad( Client, TargetZoneName ) + self:F() + + self:StatusUnLoaded() + + return self +end + +function CARGO:OnBoard( Client, LandingZone ) + self:F() + + local Valid = true + + self.CargoClient = Client + local ClientUnit = Client:GetClientGroupDCSUnit() + + return Valid +end + +function CARGO:OnBoarded( Client, LandingZone ) + self:F() + + local OnBoarded = true + + return OnBoarded +end + +function CARGO:Load( Client ) + self:F() + + self:StatusLoaded( Client ) + + return self +end + +function CARGO:IsLandingRequired() + self:F() + return true +end + +function CARGO:IsSlingLoad() + self:F() + return false +end + + +function CARGO:StatusNone() + self:F() + + self.CargoClient = nil + self.CargoStatus = CARGO.STATUS.NONE + + return self +end + +function CARGO:StatusLoading( Client ) + self:F() + + self.CargoClient = Client + self.CargoStatus = CARGO.STATUS.LOADING + self:T( "Cargo " .. self.CargoName .. " loading to Client: " .. self.CargoClient:GetClientGroupName() ) + + return self +end + +function CARGO:StatusLoaded( Client ) + self:F() + + self.CargoClient = Client + self.CargoStatus = CARGO.STATUS.LOADED + self:T( "Cargo " .. self.CargoName .. " loaded in Client: " .. self.CargoClient:GetClientGroupName() ) + + return self +end + +function CARGO:StatusUnLoaded() + self:F() + + self.CargoClient = nil + self.CargoStatus = CARGO.STATUS.UNLOADED + + return self +end + + +function CARGO:IsStatusNone() + self:F() + + return self.CargoStatus == CARGO.STATUS.NONE +end + +function CARGO:IsStatusLoading() + self:F() + + return self.CargoStatus == CARGO.STATUS.LOADING +end + +function CARGO:IsStatusLoaded() + self:F() + + return self.CargoStatus == CARGO.STATUS.LOADED +end + +function CARGO:IsStatusUnLoaded() + self:F() + + return self.CargoStatus == CARGO.STATUS.UNLOADED +end + + +CARGO_GROUP = { + ClassName = "CARGO_GROUP" +} + + +function CARGO_GROUP:New( CargoType, CargoName, CargoWeight, CargoGroupTemplate, CargoZone ) local self = BASE:Inherit( self, CARGO:New( CargoType, CargoName, CargoWeight ) ) + self:F( { CargoType, CargoName, CargoWeight, CargoGroupTemplate, CargoZone } ) + + self.CargoSpawn = SPAWN:NewWithAlias( CargoGroupTemplate, CargoName ) + self.CargoZone = CargoZone + + CARGOS[self.CargoName] = self + + return self + +end + +function CARGO_GROUP:Spawn( Client ) + self:F( { Client } ) + + local SpawnCargo = true + + if self:IsStatusNone() then + local CargoGroup = Group.getByName( self.CargoName ) + if CargoGroup and CargoGroup:isExist() then + SpawnCargo = false + end + + elseif self:IsStatusLoading() then + + local Client = self:IsLoadingToClient() + if Client and Client:GetDCSGroup() then + SpawnCargo = false + else + local CargoGroup = Group.getByName( self.CargoName ) + if CargoGroup and CargoGroup:isExist() then + SpawnCargo = false + end + end + + elseif self:IsStatusLoaded() then + + local ClientLoaded = self:IsLoadedInClient() + -- Now test if another Client is alive (not this one), and it has the CARGO, then this cargo does not need to be initialized and spawned. + if ClientLoaded and ClientLoaded ~= Client then + local ClientGroup = Client:GetDCSGroup() + if ClientLoaded:GetClientGroupDCSUnit() and ClientLoaded:GetClientGroupDCSUnit():isExist() then + SpawnCargo = false + else + self:StatusNone() + end + else + -- Same Client, but now in initialize, so set back the status to None. + self:StatusNone() + end + + elseif self:IsStatusUnLoaded() then + + SpawnCargo = false + + end + + if SpawnCargo then + if self.CargoZone:GetCargoHostUnit() then + --- ReSpawn the Cargo from the CargoHost + self.CargoGroupName = self.CargoSpawn:SpawnFromUnit( self.CargoZone:GetCargoHostUnit(), 60, 30, 1 ):GetName() + else + --- ReSpawn the Cargo in the CargoZone without a host ... + self:T( self.CargoZone ) + self.CargoGroupName = self.CargoSpawn:SpawnInZone( self.CargoZone, true, 1 ):GetName() + end + self:StatusNone() + end + + self:T( { self.CargoGroupName, CARGOS[self.CargoName].CargoGroupName } ) + + return self +end + +function CARGO_GROUP:IsNear( Client, LandingZone ) + self:F() + + local Near = false + + if self.CargoGroupName then + local CargoGroup = Group.getByName( self.CargoGroupName ) + if routines.IsPartOfGroupInRadius( CargoGroup, Client:GetPositionVec3(), 250 ) then + Near = true + end + end + + return Near + +end + + +function CARGO_GROUP:OnBoard( Client, LandingZone, OnBoardSide ) + self:F() + + local Valid = true + + local ClientUnit = Client:GetClientGroupDCSUnit() + + local CarrierPos = ClientUnit:getPoint() + local CarrierPosMove = ClientUnit:getPoint() + local CarrierPosOnBoard = ClientUnit:getPoint() + + local CargoGroup = Group.getByName( self.CargoGroupName ) + + local CargoUnit = CargoGroup:getUnit(1) + local CargoPos = CargoUnit:getPoint() + + self.CargoInAir = CargoUnit:inAir() + + self:T( self.CargoInAir ) + + -- Only move the group to the carrier when the cargo is not in the air + -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea). + if not self.CargoInAir then + + local Points = {} + + self:T( 'CargoPos x = ' .. CargoPos.x .. " z = " .. CargoPos.z ) + self:T( 'CarrierPosMove x = ' .. CarrierPosMove.x .. " z = " .. CarrierPosMove.z ) + + Points[#Points+1] = routines.ground.buildWP( CargoPos, "Cone", 10 ) + + self:T( 'Points[1] x = ' .. Points[1].x .. " y = " .. Points[1].y ) + + if OnBoardSide == nil then + OnBoardSide = CLIENT.ONBOARDSIDE.NONE + end + + if OnBoardSide == CLIENT.ONBOARDSIDE.LEFT then + + self:T( "TransportCargoOnBoard: Onboarding LEFT" ) + CarrierPosMove.z = CarrierPosMove.z - 25 + CarrierPosOnBoard.z = CarrierPosOnBoard.z - 5 + Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) + Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) + + elseif OnBoardSide == CLIENT.ONBOARDSIDE.RIGHT then + + self:T( "TransportCargoOnBoard: Onboarding RIGHT" ) + CarrierPosMove.z = CarrierPosMove.z + 25 + CarrierPosOnBoard.z = CarrierPosOnBoard.z + 5 + Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) + Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) + + elseif OnBoardSide == CLIENT.ONBOARDSIDE.BACK then + + self:T( "TransportCargoOnBoard: Onboarding BACK" ) + CarrierPosMove.x = CarrierPosMove.x - 25 + CarrierPosOnBoard.x = CarrierPosOnBoard.x - 5 + Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) + Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) + + elseif OnBoardSide == CLIENT.ONBOARDSIDE.FRONT then + + self:T( "TransportCargoOnBoard: Onboarding FRONT" ) + CarrierPosMove.x = CarrierPosMove.x + 25 + CarrierPosOnBoard.x = CarrierPosOnBoard.x + 5 + Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) + Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) + + elseif OnBoardSide == CLIENT.ONBOARDSIDE.NONE then + + self:T( "TransportCargoOnBoard: Onboarding CENTRAL" ) + Points[#Points+1] = routines.ground.buildWP( CarrierPos, "Cone", 10 ) + + end + self:T( "TransportCargoOnBoard: Routing " .. self.CargoGroupName ) + + --routines.scheduleFunction( routines.goRoute, { self.CargoGroupName, Points}, timer.getTime() + 4 ) + SCHEDULER:New( self, routines.goRoute, { self.CargoGroupName, Points}, 4 ) + end + + self:StatusLoading( Client ) + + return Valid + +end + + +function CARGO_GROUP:OnBoarded( Client, LandingZone ) + self:F() + + local OnBoarded = false + + local CargoGroup = Group.getByName( self.CargoGroupName ) + + if not self.CargoInAir then + if routines.IsPartOfGroupInRadius( CargoGroup, Client:GetPositionVec3(), 25 ) then + CargoGroup:destroy() + self:StatusLoaded( Client ) + OnBoarded = true + end + else + CargoGroup:destroy() + self:StatusLoaded( Client ) + OnBoarded = true + end + + return OnBoarded +end + + +function CARGO_GROUP:UnLoad( Client, TargetZoneName ) + self:F() + + self:T( 'self.CargoName = ' .. self.CargoName ) + + local CargoGroup = self.CargoSpawn:SpawnFromUnit( Client:GetClientGroupUnit(), 60, 30 ) + + self.CargoGroupName = CargoGroup:GetName() + self:T( 'self.CargoGroupName = ' .. self.CargoGroupName ) + + CargoGroup:TaskRouteToZone( ZONE:New( TargetZoneName ), true ) + + self:StatusUnLoaded() + + return self +end + + +CARGO_PACKAGE = { + ClassName = "CARGO_PACKAGE" +} + + +function CARGO_PACKAGE:New( CargoType, CargoName, CargoWeight, CargoClient ) local self = BASE:Inherit( self, CARGO:New( CargoType, CargoName, CargoWeight ) ) + self:F( { CargoType, CargoName, CargoWeight, CargoClient } ) + + self.CargoClient = CargoClient + + CARGOS[self.CargoName] = self + + return self + +end + + +function CARGO_PACKAGE:Spawn( Client ) + self:F( { self, Client } ) + + -- this needs to be checked thoroughly + + local CargoClientGroup = self.CargoClient:GetDCSGroup() + if not CargoClientGroup then + if not self.CargoClientSpawn then + self.CargoClientSpawn = SPAWN:New( self.CargoClient:GetClientGroupName() ):Limit( 1, 1 ) + end + self.CargoClientSpawn:ReSpawn( 1 ) + end + + local SpawnCargo = true + + if self:IsStatusNone() then + + elseif self:IsStatusLoading() or self:IsStatusLoaded() then + + local CargoClientLoaded = self:IsLoadedInClient() + if CargoClientLoaded and CargoClientLoaded:GetDCSGroup() then + SpawnCargo = false + end + + elseif self:IsStatusUnLoaded() then + + SpawnCargo = false + + else + + end + + if SpawnCargo then + self:StatusLoaded( self.CargoClient ) + end + + return self +end + + +function CARGO_PACKAGE:IsNear( Client, LandingZone ) + self:F() + + local Near = false + + if self.CargoClient and self.CargoClient:GetDCSGroup() then + self:T( self.CargoClient.ClientName ) + self:T( 'Client Exists.' ) + + if routines.IsUnitInRadius( self.CargoClient:GetClientGroupDCSUnit(), Client:GetPositionVec3(), 150 ) then + Near = true + end + end + + return Near + +end + + +function CARGO_PACKAGE:OnBoard( Client, LandingZone, OnBoardSide ) + self:F() + + local Valid = true + + local ClientUnit = Client:GetClientGroupDCSUnit() + + local CarrierPos = ClientUnit:getPoint() + local CarrierPosMove = ClientUnit:getPoint() + local CarrierPosOnBoard = ClientUnit:getPoint() + local CarrierPosMoveAway = ClientUnit:getPoint() + + local CargoHostGroup = self.CargoClient:GetDCSGroup() + local CargoHostName = self.CargoClient:GetDCSGroup():getName() + + local CargoHostUnits = CargoHostGroup:getUnits() + local CargoPos = CargoHostUnits[1]:getPoint() + + local Points = {} + + self:T( 'CargoPos x = ' .. CargoPos.x .. " z = " .. CargoPos.z ) + self:T( 'CarrierPosMove x = ' .. CarrierPosMove.x .. " z = " .. CarrierPosMove.z ) + + Points[#Points+1] = routines.ground.buildWP( CargoPos, "Cone", 10 ) + + self:T( 'Points[1] x = ' .. Points[1].x .. " y = " .. Points[1].y ) + + if OnBoardSide == nil then + OnBoardSide = CLIENT.ONBOARDSIDE.NONE + end + + if OnBoardSide == CLIENT.ONBOARDSIDE.LEFT then + + self:T( "TransportCargoOnBoard: Onboarding LEFT" ) + CarrierPosMove.z = CarrierPosMove.z - 25 + CarrierPosOnBoard.z = CarrierPosOnBoard.z - 5 + CarrierPosMoveAway.z = CarrierPosMoveAway.z - 20 + Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) + Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) + Points[#Points+1] = routines.ground.buildWP( CarrierPosMoveAway, "Cone", 10 ) + + elseif OnBoardSide == CLIENT.ONBOARDSIDE.RIGHT then + + self:T( "TransportCargoOnBoard: Onboarding RIGHT" ) + CarrierPosMove.z = CarrierPosMove.z + 25 + CarrierPosOnBoard.z = CarrierPosOnBoard.z + 5 + CarrierPosMoveAway.z = CarrierPosMoveAway.z + 20 + Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) + Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) + Points[#Points+1] = routines.ground.buildWP( CarrierPosMoveAway, "Cone", 10 ) + + elseif OnBoardSide == CLIENT.ONBOARDSIDE.BACK then + + self:T( "TransportCargoOnBoard: Onboarding BACK" ) + CarrierPosMove.x = CarrierPosMove.x - 25 + CarrierPosOnBoard.x = CarrierPosOnBoard.x - 5 + CarrierPosMoveAway.x = CarrierPosMoveAway.x - 20 + Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) + Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) + Points[#Points+1] = routines.ground.buildWP( CarrierPosMoveAway, "Cone", 10 ) + + elseif OnBoardSide == CLIENT.ONBOARDSIDE.FRONT then + + self:T( "TransportCargoOnBoard: Onboarding FRONT" ) + CarrierPosMove.x = CarrierPosMove.x + 25 + CarrierPosOnBoard.x = CarrierPosOnBoard.x + 5 + CarrierPosMoveAway.x = CarrierPosMoveAway.x + 20 + Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) + Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) + Points[#Points+1] = routines.ground.buildWP( CarrierPosMoveAway, "Cone", 10 ) + + elseif OnBoardSide == CLIENT.ONBOARDSIDE.NONE then + + self:T( "TransportCargoOnBoard: Onboarding FRONT" ) + CarrierPosMove.x = CarrierPosMove.x + 25 + CarrierPosOnBoard.x = CarrierPosOnBoard.x + 5 + CarrierPosMoveAway.x = CarrierPosMoveAway.x + 20 + Points[#Points+1] = routines.ground.buildWP( CarrierPosMove, "Cone", 10 ) + Points[#Points+1] = routines.ground.buildWP( CarrierPosOnBoard, "Cone", 10 ) + Points[#Points+1] = routines.ground.buildWP( CarrierPosMoveAway, "Cone", 10 ) + + end + self:T( "Routing " .. CargoHostName ) + + --routines.scheduleFunction( routines.goRoute, { CargoHostName, Points}, timer.getTime() + 4 ) + SCHEDULER:New( self, routines.goRoute, { CargoHostName, Points }, 4 ) + + return Valid + +end + + +function CARGO_PACKAGE:OnBoarded( Client, LandingZone ) + self:F() + + local OnBoarded = false + + if self.CargoClient and self.CargoClient:GetDCSGroup() then + if routines.IsUnitInRadius( self.CargoClient:GetClientGroupDCSUnit(), self.CargoClient:GetPositionVec3(), 10 ) then + + -- Switch Cargo from self.CargoClient to Client ... Each cargo can have only one client. So assigning the new client for the cargo is enough. + self:StatusLoaded( Client ) + + -- All done, onboarded the Cargo to the new Client. + OnBoarded = true + end + end + + return OnBoarded +end + + +function CARGO_PACKAGE:UnLoad( Client, TargetZoneName ) + self:F() + + self:T( 'self.CargoName = ' .. self.CargoName ) + --self:T( 'self.CargoHostName = ' .. self.CargoHostName ) + + --self.CargoSpawn:FromCarrier( Client:GetDCSGroup(), TargetZoneName, self.CargoHostName ) + self:StatusUnLoaded() + + return Cargo +end + + +CARGO_SLINGLOAD = { + ClassName = "CARGO_SLINGLOAD" +} + + +function CARGO_SLINGLOAD:New( CargoType, CargoName, CargoWeight, CargoZone, CargoHostName, CargoCountryID ) + local self = BASE:Inherit( self, CARGO:New( CargoType, CargoName, CargoWeight ) ) + self:F( { CargoType, CargoName, CargoWeight, CargoZone, CargoHostName, CargoCountryID } ) + + self.CargoHostName = CargoHostName + + -- Cargo will be initialized around the CargoZone position. + self.CargoZone = CargoZone + + self.CargoCount = 0 + self.CargoStaticName = string.format( "%s#%03d", self.CargoName, self.CargoCount ) + + -- The country ID needs to be correctly set. + self.CargoCountryID = CargoCountryID + + CARGOS[self.CargoName] = self + + return self + +end + + +function CARGO_SLINGLOAD:IsLandingRequired() + self:F() + return false +end + + +function CARGO_SLINGLOAD:IsSlingLoad() + self:F() + return true +end + + +function CARGO_SLINGLOAD:Spawn( Client ) + self:F( { self, Client } ) + + local Zone = trigger.misc.getZone( self.CargoZone ) + + local ZonePos = {} + ZonePos.x = Zone.point.x + math.random( Zone.radius / 2 * -1, Zone.radius / 2 ) + ZonePos.y = Zone.point.z + math.random( Zone.radius / 2 * -1, Zone.radius / 2 ) + + self:T( "Cargo Location = " .. ZonePos.x .. ", " .. ZonePos.y ) + + --[[ + -- This does not work in 1.5.2. + CargoStatic = StaticObject.getByName( self.CargoName ) + if CargoStatic then + CargoStatic:destroy() + end + --]] + + CargoStatic = StaticObject.getByName( self.CargoStaticName ) + + if CargoStatic and CargoStatic:isExist() then + CargoStatic:destroy() + end + + -- I need to make every time a new cargo due to bugs in 1.5.2. + + self.CargoCount = self.CargoCount + 1 + self.CargoStaticName = string.format( "%s#%03d", self.CargoName, self.CargoCount ) + + local CargoTemplate = { + ["category"] = "Cargo", + ["shape_name"] = "ab-212_cargo", + ["type"] = "Cargo1", + ["x"] = ZonePos.x, + ["y"] = ZonePos.y, + ["mass"] = self.CargoWeight, + ["name"] = self.CargoStaticName, + ["canCargo"] = true, + ["heading"] = 0, + } + + coalition.addStaticObject( self.CargoCountryID, CargoTemplate ) + +-- end + + return self +end + + +function CARGO_SLINGLOAD:IsNear( Client, LandingZone ) + self:F() + + local Near = false + + return Near +end + + +function CARGO_SLINGLOAD:IsInLandingZone( Client, LandingZone ) + self:F() + + local Near = false + + local CargoStaticUnit = StaticObject.getByName( self.CargoName ) + if CargoStaticUnit then + if routines.IsStaticInZones( CargoStaticUnit, LandingZone ) then + Near = true + end + end + + return Near +end + + +function CARGO_SLINGLOAD:OnBoard( Client, LandingZone, OnBoardSide ) + self:F() + + local Valid = true + + + return Valid +end + + +function CARGO_SLINGLOAD:OnBoarded( Client, LandingZone ) + self:F() + + local OnBoarded = false + + local CargoStaticUnit = StaticObject.getByName( self.CargoName ) + if CargoStaticUnit then + if not routines.IsStaticInZones( CargoStaticUnit, LandingZone ) then + OnBoarded = true + end + end + + return OnBoarded +end + + +function CARGO_SLINGLOAD:UnLoad( Client, TargetZoneName ) + self:F() + + self:T( 'self.CargoName = ' .. self.CargoName ) + self:T( 'self.CargoGroupName = ' .. self.CargoGroupName ) + + self:StatusUnLoaded() + + return Cargo +end +--- Message System to display Messages for Clients and Coalitions or All. +-- Messages are grouped on the display panel per Category to improve readability for the players. +-- Messages are shown on the display panel for an amount of seconds, and will then disappear. +-- Messages are identified by an ID. The messages with the same ID belonging to the same category will be overwritten if they were still being displayed on the display panel. +-- Messages are created with MESSAGE:@{New}(). +-- Messages are sent to Clients with MESSAGE:@{ToClient}(). +-- Messages are sent to Coalitions with MESSAGE:@{ToCoalition}(). +-- Messages are sent to All Players with MESSAGE:@{ToAll}(). +-- @module Message + +Include.File( "Base" ) + +--- The MESSAGE class +-- @type MESSAGE +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 #string MessageCategory is a string expressing the Category of the Message. Messages are grouped on the display panel per Category to improve readability. +-- @param #number MessageDuration is a number in seconds of how long the MESSAGE should be shown on the display panel. +-- @param #string MessageID is a string expressing the ID of the Message. +-- @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!", "End of Mission", 25, "Win" ) +-- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) +-- 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" ) +function MESSAGE:New( MessageText, MessageCategory, MessageDuration, MessageID ) + local self = BASE:Inherit( self, BASE:New() ) + self:F( { MessageText, MessageCategory, MessageDuration, MessageID } ) + + -- When no messagecategory is given, we don't show it as a title... + if MessageCategory and MessageCategory ~= "" then + self.MessageCategory = MessageCategory .. ": " + else + self.MessageCategory = "" + end + + self.MessageDuration = MessageDuration + self.MessageID = MessageID + self.MessageTime = timer.getTime() + self.MessageText = MessageText + + self.MessageSent = false + self.MessageGroup = false + self.MessageCoalition = false + + return self +end + +--- Sends a MESSAGE to a Client Group. Note that the Group needs to be defined within the ME with the skillset "Client" or "Player". +-- @param #MESSAGE self +-- @param Client#CLIENT Client is the Group of the Client. +-- @return #MESSAGE +-- @usage +-- -- Send the 2 messages created with the @{New} method to the Client Group. +-- -- Note that the Message of MessageClient2 is overwriting the Message of MessageClient1. +-- ClientGroup = Group.getByName( "ClientGroup" ) +-- +-- MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ):ToClient( ClientGroup ) +-- MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ):ToClient( ClientGroup ) +-- or +-- MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ):ToClient( ClientGroup ) +-- MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ):ToClient( ClientGroup ) +-- or +-- MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ) +-- MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ) +-- MessageClient1:ToClient( ClientGroup ) +-- MessageClient2:ToClient( ClientGroup ) +function MESSAGE:ToClient( Client ) + self:F( Client ) + + if Client and Client:GetClientGroupID() then + + local ClientGroupID = Client:GetClientGroupID() + self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) + trigger.action.outTextForGroup( ClientGroupID, self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration ) + end + + return self +end + +--- Sends a MESSAGE to the Blue coalition. +-- @param #MESSAGE self +-- @return #MESSAGE +-- @usage +-- -- Send a message created with the @{New} method to the BLUE coalition. +-- MessageBLUE = MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToBlue() +-- or +-- MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToBlue() +-- or +-- MessageBLUE = MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) +-- MessageBLUE:ToBlue() +function MESSAGE:ToBlue() + self:F() + + self:ToCoalition( coalition.side.BLUE ) + + return self +end + +--- Sends a MESSAGE to the Red Coalition. +-- @param #MESSAGE self +-- @return #MESSAGE +-- @usage +-- -- Send a message created with the @{New} method to the RED coalition. +-- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToRed() +-- or +-- MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToRed() +-- or +-- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) +-- MessageRED:ToRed() +function MESSAGE:ToRed( ) + self:F() + + self:ToCoalition( coalition.side.RED ) + + return self +end + +--- Sends a MESSAGE to a Coalition. +-- @param #MESSAGE self +-- @param CoalitionSide needs to be filled out by the defined structure of the standard scripting engine @{coalition.side}. +-- @return #MESSAGE +-- @usage +-- -- Send a message created with the @{New} method to the RED coalition. +-- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToCoalition( coalition.side.RED ) +-- or +-- MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToCoalition( coalition.side.RED ) +-- or +-- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ) +-- MessageRED:ToCoalition( coalition.side.RED ) +function MESSAGE:ToCoalition( CoalitionSide ) + self:F( CoalitionSide ) + + if CoalitionSide then + self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) + trigger.action.outTextForCoalition( CoalitionSide, self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration ) + end + + return self +end + +--- Sends a MESSAGE to all players. +-- @param #MESSAGE self +-- @return #MESSAGE +-- @usage +-- -- Send a message created to all players. +-- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ):ToAll() +-- or +-- MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ):ToAll() +-- or +-- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ) +-- MessageAll:ToAll() +function MESSAGE:ToAll() + self:F() + + self:ToCoalition( coalition.side.RED ) + self:ToCoalition( coalition.side.BLUE ) + + return self +end + + + +--- The MESSAGEQUEUE class +-- @type MESSAGEQUEUE +MESSAGEQUEUE = { + ClientGroups = {}, + CoalitionSides = {} +} + +function MESSAGEQUEUE:New( RefreshInterval ) + local self = BASE:Inherit( self, BASE:New() ) + self:F( { RefreshInterval } ) + + self.RefreshInterval = RefreshInterval + + --self.DisplayFunction = routines.scheduleFunction( self._DisplayMessages, { self }, 0, RefreshInterval ) + self.DisplayFunction = SCHEDULER:New( self, self._DisplayMessages, {}, 0, RefreshInterval ) + + return self +end + +--- This function is called automatically by the MESSAGEQUEUE scheduler. +function MESSAGEQUEUE:_DisplayMessages() + + -- First we display all messages that a coalition needs to receive... Also those who are not in a client (CA module clients...). + for CoalitionSideID, CoalitionSideData in pairs( self.CoalitionSides ) do + for MessageID, MessageData in pairs( CoalitionSideData.Messages ) do + if MessageData.MessageSent == false then + --trigger.action.outTextForCoalition( CoalitionSideID, MessageData.MessageCategory .. '\n' .. MessageData.MessageText:gsub("\n$",""):gsub("\n$",""), MessageData.MessageDuration ) + MessageData.MessageSent = true + end + local MessageTimeLeft = ( MessageData.MessageTime + MessageData.MessageDuration ) - timer.getTime() + if MessageTimeLeft <= 0 then + MessageData = nil + end + end + end + + -- Then we send the messages for each individual client, but also to be included are those Coalition messages for the Clients who belong to a coalition. + -- Because the Client messages will overwrite the Coalition messages (for that Client). + for ClientGroupName, ClientGroupData in pairs( self.ClientGroups ) do + for MessageID, MessageData in pairs( ClientGroupData.Messages ) do + if MessageData.MessageGroup == false then + trigger.action.outTextForGroup( Group.getByName(ClientGroupName):getID(), MessageData.MessageCategory .. '\n' .. MessageData.MessageText:gsub("\n$",""):gsub("\n$",""), MessageData.MessageDuration ) + MessageData.MessageGroup = true + end + local MessageTimeLeft = ( MessageData.MessageTime + MessageData.MessageDuration ) - timer.getTime() + if MessageTimeLeft <= 0 then + MessageData = nil + end + end + + -- Now check if the Client also has messages that belong to the Coalition of the Client... + for CoalitionSideID, CoalitionSideData in pairs( self.CoalitionSides ) do + for MessageID, MessageData in pairs( CoalitionSideData.Messages ) do + local CoalitionGroup = Group.getByName( ClientGroupName ) + if CoalitionGroup and CoalitionGroup:getCoalition() == CoalitionSideID then + if MessageData.MessageCoalition == false then + trigger.action.outTextForGroup( Group.getByName(ClientGroupName):getID(), MessageData.MessageCategory .. '\n' .. MessageData.MessageText:gsub("\n$",""):gsub("\n$",""), MessageData.MessageDuration ) + MessageData.MessageCoalition = true + end + end + local MessageTimeLeft = ( MessageData.MessageTime + MessageData.MessageDuration ) - timer.getTime() + if MessageTimeLeft <= 0 then + MessageData = nil + end + end + end + end + + return true +end + +--- The _MessageQueue object is created when the MESSAGE class module is loaded. +--_MessageQueue = MESSAGEQUEUE:New( 0.5 ) + +--- Stages within a @{TASK} within a @{MISSION}. All of the STAGE functionality is considered internally administered and not to be used by any Mission designer. +-- @module STAGE +-- @author Flightcontrol + +Include.File( "Routines" ) +Include.File( "Base" ) +Include.File( "Mission" ) +Include.File( "Client" ) +Include.File( "Task" ) + +--- The STAGE class +-- @type +STAGE = { + ClassName = "STAGE", + MSG = { ID = "None", TIME = 10 }, + FREQUENCY = { NONE = 0, ONCE = 1, REPEAT = -1 }, + + Name = "NoStage", + StageType = '', + WaitTime = 1, + Frequency = 1, + MessageCount = 0, + MessageInterval = 15, + MessageShown = {}, + MessageShow = false, + MessageFlash = false +} + + +function STAGE:New() + local self = BASE:Inherit( self, BASE:New() ) + self:F() + return self +end + +function STAGE:Execute( Mission, Client, Task ) + + local Valid = true + + return Valid +end + +function STAGE:Executing( Mission, Client, Task ) + +end + +function STAGE:Validate( Mission, Client, Task ) + local Valid = true + + return Valid +end + + +STAGEBRIEF = { + ClassName = "BRIEF", + MSG = { ID = "Brief", TIME = 1 }, + Name = "Brief", + StageBriefingTime = 0, + StageBriefingDuration = 1 +} + +function STAGEBRIEF:New() + local self = BASE:Inherit( self, STAGE:New() ) + self:F() + self.StageType = 'CLIENT' + return self +end + +--- Execute +-- @param #STAGEBRIEF self +-- @param Mission#MISSION Mission +-- @param Client#CLIENT Client +-- @param Task#TASK Task +-- @return #boolean +function STAGEBRIEF:Execute( Mission, Client, Task ) + local Valid = BASE:Inherited(self):Execute( Mission, Client, Task ) + self:F() + Client:ShowMissionBriefing( Mission.MissionBriefing ) + self.StageBriefingTime = timer.getTime() + return Valid +end + +function STAGEBRIEF:Validate( Mission, Client, Task ) + local Valid = STAGE:Validate( Mission, Client, Task ) + self:T() + + if timer.getTime() - self.StageBriefingTime <= self.StageBriefingDuration then + return 0 + else + self.StageBriefingTime = timer.getTime() + return 1 + end + +end + + +STAGESTART = { + ClassName = "START", + MSG = { ID = "Start", TIME = 1 }, + Name = "Start", + StageStartTime = 0, + StageStartDuration = 1 +} + +function STAGESTART:New() + local self = BASE:Inherit( self, STAGE:New() ) + self:F() + self.StageType = 'CLIENT' + return self +end + +function STAGESTART:Execute( Mission, Client, Task ) + self:F() + local Valid = BASE:Inherited(self):Execute( Mission, Client, Task ) + if Task.TaskBriefing then + Client:Message( Task.TaskBriefing, 30, Mission.Name .. "/Stage", "Command" ) + else + Client:Message( 'Task ' .. Task.TaskNumber .. '.', 30, Mission.Name .. "/Stage", "Command" ) + end + self.StageStartTime = timer.getTime() + return Valid +end + +function STAGESTART:Validate( Mission, Client, Task ) + self:F() + local Valid = STAGE:Validate( Mission, Client, Task ) + + if timer.getTime() - self.StageStartTime <= self.StageStartDuration then + return 0 + else + self.StageStartTime = timer.getTime() + return 1 + end + + return 1 + +end + +STAGE_CARGO_LOAD = { + ClassName = "STAGE_CARGO_LOAD" +} + +function STAGE_CARGO_LOAD:New() + local self = BASE:Inherit( self, STAGE:New() ) + self:F() + self.StageType = 'CLIENT' + return self +end + +function STAGE_CARGO_LOAD:Execute( Mission, Client, Task ) + self:F() + local Valid = BASE:Inherited(self):Execute( Mission, Client, Task ) + + for LoadCargoID, LoadCargo in pairs( Task.Cargos.LoadCargos ) do + LoadCargo:Load( Client ) + end + + if Mission.MissionReportFlash and Client:IsTransport() then + Client:ShowCargo() + end + + return Valid +end + +function STAGE_CARGO_LOAD:Validate( Mission, Client, Task ) + self:F() + local Valid = STAGE:Validate( Mission, Client, Task ) + + return 1 +end + + +STAGE_CARGO_INIT = { + ClassName = "STAGE_CARGO_INIT" +} + +function STAGE_CARGO_INIT:New() + local self = BASE:Inherit( self, STAGE:New() ) + self:F() + self.StageType = 'CLIENT' + return self +end + +function STAGE_CARGO_INIT:Execute( Mission, Client, Task ) + self:F() + local Valid = BASE:Inherited(self):Execute( Mission, Client, Task ) + + for InitLandingZoneID, InitLandingZone in pairs( Task.LandingZones.LandingZones ) do + self:T( InitLandingZone ) + InitLandingZone:Spawn() + end + + + self:T( Task.Cargos.InitCargos ) + for InitCargoID, InitCargoData in pairs( Task.Cargos.InitCargos ) do + self:T( { InitCargoData } ) + InitCargoData:Spawn( Client ) + end + + return Valid +end + + +function STAGE_CARGO_INIT:Validate( Mission, Client, Task ) + self:F() + local Valid = STAGE:Validate( Mission, Client, Task ) + + return 1 +end + + + +STAGEROUTE = { + ClassName = "STAGEROUTE", + MSG = { ID = "Route", TIME = 5 }, + Frequency = STAGE.FREQUENCY.REPEAT, + Name = "Route" +} + +function STAGEROUTE:New() + local self = BASE:Inherit( self, STAGE:New() ) + self:F() + self.StageType = 'CLIENT' + self.MessageSwitch = true + return self +end + + +--- Execute the routing. +-- @param #STAGEROUTE self +-- @param Mission#MISSION Mission +-- @param Client#CLIENT Client +-- @param Task#TASK Task +function STAGEROUTE:Execute( Mission, Client, Task ) + self:F() + local Valid = BASE:Inherited(self):Execute( Mission, Client, Task ) + + local RouteMessage = "Fly to: " + self:T( Task.LandingZones ) + for LandingZoneID, LandingZoneName in pairs( Task.LandingZones.LandingZoneNames ) do + RouteMessage = RouteMessage .. "\n " .. LandingZoneName .. ' at ' .. routines.getBRStringZone( { zone = LandingZoneName, ref = Client:GetClientGroupDCSUnit():getPoint(), true, true } ) .. ' km.' + end + + if Client:IsMultiSeated() then + Client:Message( RouteMessage, self.MSG.TIME, Mission.Name .. "/StageRoute", "Co-Pilot", 20 ) + else + Client:Message( RouteMessage, self.MSG.TIME, Mission.Name .. "/StageRoute", "Command", 20 ) + end + + + if Mission.MissionReportFlash and Client:IsTransport() then + Client:ShowCargo() + end + + return Valid +end + +function STAGEROUTE:Validate( Mission, Client, Task ) + self:F() + local Valid = STAGE:Validate( Mission, Client, Task ) + + -- check if the Client is in the landing zone + self:T( Task.LandingZones.LandingZoneNames ) + Task.CurrentLandingZoneName = routines.IsUnitNearZonesRadius( Client:GetClientGroupDCSUnit(), Task.LandingZones.LandingZoneNames, 500 ) + + if Task.CurrentLandingZoneName then + + Task.CurrentLandingZone = Task.LandingZones.LandingZones[Task.CurrentLandingZoneName].CargoZone + Task.CurrentCargoZone = Task.LandingZones.LandingZones[Task.CurrentLandingZoneName] + + if Task.CurrentCargoZone then + if not Task.Signalled then + Task.Signalled = Task.CurrentCargoZone:Signal() + end + end + + self:T( 1 ) + return 1 + end + + self:T( 0 ) + return 0 +end + + + +STAGELANDING = { + ClassName = "STAGELANDING", + MSG = { ID = "Landing", TIME = 10 }, + Name = "Landing", + Signalled = false +} + +function STAGELANDING:New() + local self = BASE:Inherit( self, STAGE:New() ) + self:F() + self.StageType = 'CLIENT' + return self +end + +--- Execute the landing coordination. +-- @param #STAGELANDING self +-- @param Mission#MISSION Mission +-- @param Client#CLIENT Client +-- @param Task#TASK Task +function STAGELANDING:Execute( Mission, Client, Task ) + self:F() + + if Client:IsMultiSeated() then + Client:Message( "We have arrived at the landing zone.", self.MSG.TIME, Mission.Name .. "/StageArrived", "Co-Pilot", 10 ) + else + Client:Message( "You have arrived at the landing zone.", self.MSG.TIME, Mission.Name .. "/StageArrived", "Command", 10 ) + end + + Task.HostUnit = Task.CurrentCargoZone:GetHostUnit() + + self:T( { Task.HostUnit } ) + + if Task.HostUnit then + + Task.HostUnitName = Task.HostUnit:GetPrefix() + Task.HostUnitTypeName = Task.HostUnit:GetTypeName() + + local HostMessage = "" + Task.CargoNames = "" + + local IsFirst = true + + for CargoID, Cargo in pairs( CARGOS ) do + if Cargo.CargoType == Task.CargoType then + + if Cargo:IsLandingRequired() then + self:T( "Task for cargo " .. Cargo.CargoType .. " requires landing.") + Task.IsLandingRequired = true + end + + if Cargo:IsSlingLoad() then + self:T( "Task for cargo " .. Cargo.CargoType .. " is a slingload.") + Task.IsSlingLoad = true + end + + if IsFirst then + IsFirst = false + Task.CargoNames = Task.CargoNames .. Cargo.CargoName .. "( " .. Cargo.CargoWeight .. " )" + else + Task.CargoNames = Task.CargoNames .. "; " .. Cargo.CargoName .. "( " .. Cargo.CargoWeight .. " )" + end + end + end + + if Task.IsLandingRequired then + HostMessage = "Land the helicopter to " .. Task.TEXT[1] .. " " .. Task.CargoNames .. "." + else + HostMessage = "Use the Radio menu and F6 to find the cargo, then fly or land near the cargo and " .. Task.TEXT[1] .. " " .. Task.CargoNames .. "." + end + + local Host = "Command" + if Task.HostUnitName then + Host = Task.HostUnitName .. " (" .. Task.HostUnitTypeName .. ")" + else + if Client:IsMultiSeated() then + Host = "Co-Pilot" + end + end + + Client:Message( HostMessage, self.MSG.TIME, Mission.Name .. "/STAGELANDING.EXEC." .. Host, Host, 10 ) + + end +end + +function STAGELANDING:Validate( Mission, Client, Task ) + self:F() + + Task.CurrentLandingZoneName = routines.IsUnitNearZonesRadius( Client:GetClientGroupDCSUnit(), Task.LandingZones.LandingZoneNames, 500 ) + if Task.CurrentLandingZoneName then + + -- Client is in de landing zone. + self:T( Task.CurrentLandingZoneName ) + + Task.CurrentLandingZone = Task.LandingZones.LandingZones[Task.CurrentLandingZoneName].CargoZone + Task.CurrentCargoZone = Task.LandingZones.LandingZones[Task.CurrentLandingZoneName] + + if Task.CurrentCargoZone then + if not Task.Signalled then + Task.Signalled = Task.CurrentCargoZone:Signal() + end + end + else + if Task.CurrentLandingZone then + Task.CurrentLandingZone = nil + end + if Task.CurrentCargoZone then + Task.CurrentCargoZone = nil + end + Task.Signalled = false + Task:RemoveCargoMenus( Client ) + self:T( -1 ) + return -1 + end + + + local DCSUnitVelocityVec3 = Client:GetClientGroupDCSUnit():getVelocity() + local DCSUnitVelocity = ( DCSUnitVelocityVec3.x ^2 + DCSUnitVelocityVec3.y ^2 + DCSUnitVelocityVec3.z ^2 ) ^ 0.5 + + local DCSUnitPointVec3 = Client:GetClientGroupDCSUnit():getPoint() + local LandHeight = land.getHeight( { x = DCSUnitPointVec3.x, y = DCSUnitPointVec3.z } ) + local DCSUnitHeight = DCSUnitPointVec3.y - LandHeight + + self:T( { Task.IsLandingRequired, Client:GetClientGroupDCSUnit():inAir() } ) + if Task.IsLandingRequired and not Client:GetClientGroupDCSUnit():inAir() then + self:T( 1 ) + Task.IsInAirTestRequired = true + return 1 + end + + self:T( { DCSUnitVelocity, DCSUnitHeight, LandHeight, Task.CurrentCargoZone.SignalHeight } ) + if Task.IsLandingRequired and DCSUnitVelocity <= 0.05 and DCSUnitHeight <= Task.CurrentCargoZone.SignalHeight then + self:T( 1 ) + Task.IsInAirTestRequired = false + return 1 + end + + self:T( 0 ) + return 0 +end + +STAGELANDED = { + ClassName = "STAGELANDED", + MSG = { ID = "Land", TIME = 10 }, + Name = "Landed", + MenusAdded = false +} + +function STAGELANDED:New() + local self = BASE:Inherit( self, STAGE:New() ) + self:F() + self.StageType = 'CLIENT' + return self +end + +function STAGELANDED:Execute( Mission, Client, Task ) + self:F() + + if Task.IsLandingRequired then + + local Host = "Command" + if Task.HostUnitName then + Host = Task.HostUnitName .. " (" .. Task.HostUnitTypeName .. ")" + else + if Client:IsMultiSeated() then + Host = "Co-Pilot" + end + end + + Client:Message( 'You have landed within the landing zone. Use the radio menu (F10) to ' .. Task.TEXT[1] .. ' the ' .. Task.CargoType .. '.', + self.MSG.TIME, Mission.Name .. "/STAGELANDED.EXEC" .. Host, Host ) + + if not self.MenusAdded then + Task.Cargo = nil + Task:RemoveCargoMenus( Client ) + Task:AddCargoMenus( Client, CARGOS, 250 ) + end + end +end + + + +function STAGELANDED:Validate( Mission, Client, Task ) + self:F() + + if not routines.IsUnitNearZonesRadius( Client:GetClientGroupDCSUnit(), Task.CurrentLandingZoneName, 500 ) then + self:T( "Client is not anymore in the landing zone, go back to stage Route, and remove cargo menus." ) + Task.Signalled = false + Task:RemoveCargoMenus( Client ) + self:T( -2 ) + return -2 + end + + local DCSUnitVelocityVec3 = Client:GetClientGroupDCSUnit():getVelocity() + local DCSUnitVelocity = ( DCSUnitVelocityVec3.x ^2 + DCSUnitVelocityVec3.y ^2 + DCSUnitVelocityVec3.z ^2 ) ^ 0.5 + + local DCSUnitPointVec3 = Client:GetClientGroupDCSUnit():getPoint() + local LandHeight = land.getHeight( { x = DCSUnitPointVec3.x, y = DCSUnitPointVec3.z } ) + local DCSUnitHeight = DCSUnitPointVec3.y - LandHeight + + self:T( { Task.IsLandingRequired, Client:GetClientGroupDCSUnit():inAir() } ) + if Task.IsLandingRequired and Task.IsInAirTestRequired == true and Client:GetClientGroupDCSUnit():inAir() then + self:T( "Client went back in the air. Go back to stage Landing." ) + self:T( -1 ) + return -1 + end + + self:T( { DCSUnitVelocity, DCSUnitHeight, LandHeight, Task.CurrentCargoZone.SignalHeight } ) + if Task.IsLandingRequired and Task.IsInAirTestRequired == false and DCSUnitVelocity >= 2 and DCSUnitHeight >= Task.CurrentCargoZone.SignalHeight then + self:T( "It seems the Client went back in the air and over the boundary limits. Go back to stage Landing." ) + self:T( -1 ) + return -1 + end + + -- Wait until cargo is selected from the menu. + if Task.IsLandingRequired then + if not Task.Cargo then + self:T( 0 ) + return 0 + end + end + + self:T( 1 ) + return 1 +end + +STAGEUNLOAD = { + ClassName = "STAGEUNLOAD", + MSG = { ID = "Unload", TIME = 10 }, + Name = "Unload" +} + +function STAGEUNLOAD:New() + local self = BASE:Inherit( self, STAGE:New() ) + self:F() + self.StageType = 'CLIENT' + return self +end + +--- Coordinate UnLoading +-- @param #STAGEUNLOAD self +-- @param Mission#MISSION Mission +-- @param Client#CLIENT Client +-- @param Task#TASK Task +function STAGEUNLOAD:Execute( Mission, Client, Task ) + self:F() + + if Client:IsMultiSeated() then + Client:Message( 'The ' .. Task.CargoType .. ' are being ' .. Task.TEXT[2] .. ' within the landing zone. Wait until the helicopter is ' .. Task.TEXT[3] .. '.', + self.MSG.TIME, Mission.Name .. "/StageUnLoad", "Co-Pilot" ) + else + Client:Message( 'You are unloading the ' .. Task.CargoType .. ' ' .. Task.TEXT[2] .. ' within the landing zone. Wait until the helicopter is ' .. Task.TEXT[3] .. '.', + self.MSG.TIME, Mission.Name .. "/StageUnLoad", "Command" ) + end + Task:RemoveCargoMenus( Client ) +end + +function STAGEUNLOAD:Executing( Mission, Client, Task ) + self:F() + env.info( 'STAGEUNLOAD:Executing() Task.Cargo.CargoName = ' .. Task.Cargo.CargoName ) + + local TargetZoneName + + if Task.TargetZoneName then + TargetZoneName = Task.TargetZoneName + else + TargetZoneName = Task.CurrentLandingZoneName + end + + if Task.Cargo:UnLoad( Client, TargetZoneName ) then + Task.ExecuteStage = _TransportExecuteStage.SUCCESS + if Mission.MissionReportFlash then + Client:ShowCargo() + end + end +end + +--- Validate UnLoading +-- @param #STAGEUNLOAD self +-- @param Mission#MISSION Mission +-- @param Client#CLIENT Client +-- @param Task#TASK Task +function STAGEUNLOAD:Validate( Mission, Client, Task ) + self:F() + env.info( 'STAGEUNLOAD:Validate()' ) + + if routines.IsUnitNearZonesRadius( Client:GetClientGroupDCSUnit(), Task.CurrentLandingZoneName, 500 ) then + else + Task.ExecuteStage = _TransportExecuteStage.FAILED + Task:RemoveCargoMenus( Client ) + if Client:IsMultiSeated() then + Client:Message( 'The ' .. Task.CargoType .. " haven't been successfully " .. Task.TEXT[3] .. ' within the landing zone. Task and mission has failed.', + _TransportStageMsgTime.DONE, Mission.Name .. "/StageFailure", "Co-Pilot" ) + else + Client:Message( 'The ' .. Task.CargoType .. " haven't been successfully " .. Task.TEXT[3] .. ' within the landing zone. Task and mission has failed.', + _TransportStageMsgTime.DONE, Mission.Name .. "/StageFailure", "Command" ) + end + return 1 + end + + if not Client:GetClientGroupDCSUnit():inAir() then + else + Task.ExecuteStage = _TransportExecuteStage.FAILED + Task:RemoveCargoMenus( Client ) + if Client:IsMultiSeated() then + Client:Message( 'The ' .. Task.CargoType .. " haven't been successfully " .. Task.TEXT[3] .. ' within the landing zone. Task and mission has failed.', + _TransportStageMsgTime.DONE, Mission.Name .. "/StageFailure", "Co-Pilot" ) + else + Client:Message( 'The ' .. Task.CargoType .. " haven't been successfully " .. Task.TEXT[3] .. ' within the landing zone. Task and mission has failed.', + _TransportStageMsgTime.DONE, Mission.Name .. "/StageFailure", "Command" ) + end + return 1 + end + + if Task.ExecuteStage == _TransportExecuteStage.SUCCESS then + if Client:IsMultiSeated() then + Client:Message( 'The ' .. Task.CargoType .. ' have been sucessfully ' .. Task.TEXT[3] .. ' within the landing zone.', _TransportStageMsgTime.DONE, Mission.Name .. "/Stage", "Co-Pilot" ) + else + Client:Message( 'The ' .. Task.CargoType .. ' have been sucessfully ' .. Task.TEXT[3] .. ' within the landing zone.', _TransportStageMsgTime.DONE, Mission.Name .. "/Stage", "Command" ) + end + Task:RemoveCargoMenus( Client ) + Task.MissionTask:AddGoalCompletion( Task.MissionTask.GoalVerb, Task.CargoName, 1 ) -- We set the cargo as one more goal completed in the mission. + return 1 + end + + return 1 +end + +STAGELOAD = { + ClassName = "STAGELOAD", + MSG = { ID = "Load", TIME = 10 }, + Name = "Load" +} + +function STAGELOAD:New() + local self = BASE:Inherit( self, STAGE:New() ) + self:F() + self.StageType = 'CLIENT' + return self +end + +function STAGELOAD:Execute( Mission, Client, Task ) + self:F() + + if not Task.IsSlingLoad then + + local Host = "Command" + if Task.HostUnitName then + Host = Task.HostUnitName .. " (" .. Task.HostUnitTypeName .. ")" + else + if Client:IsMultiSeated() then + Host = "Co-Pilot" + end + end + + Client:Message( 'The ' .. Task.CargoType .. ' are being ' .. Task.TEXT[2] .. ' within the landing zone. Wait until the helicopter is ' .. Task.TEXT[3] .. '.', + _TransportStageMsgTime.EXECUTING, Mission.Name .. "/STAGELOAD.EXEC." .. Host, Host ) + + -- Route the cargo to the Carrier + + Task.Cargo:OnBoard( Client, Task.CurrentCargoZone, Task.OnBoardSide ) + Task.ExecuteStage = _TransportExecuteStage.EXECUTING + else + Task.ExecuteStage = _TransportExecuteStage.EXECUTING + end +end + +function STAGELOAD:Executing( Mission, Client, Task ) + self:F() + + -- If the Cargo is ready to be loaded, load it into the Client. + + local Host = "Command" + if Task.HostUnitName then + Host = Task.HostUnitName .. " (" .. Task.HostUnitTypeName .. ")" + else + if Client:IsMultiSeated() then + Host = "Co-Pilot" + end + end + + if not Task.IsSlingLoad then + self:T( Task.Cargo.CargoName) + + if Task.Cargo:OnBoarded( Client, Task.CurrentCargoZone ) then + + -- Load the Cargo onto the Client + Task.Cargo:Load( Client ) + + -- Message to the pilot that cargo has been loaded. + Client:Message( "The cargo " .. Task.Cargo.CargoName .. " has been loaded in our helicopter.", + 20, Mission.Name .. "/STAGELANDING.LOADING1." .. Host, Host ) + Task.ExecuteStage = _TransportExecuteStage.SUCCESS + + Client:ShowCargo() + end + else + Client:Message( "Hook the " .. Task.CargoNames .. " onto the helicopter " .. Task.TEXT[3] .. " within the landing zone.", + _TransportStageMsgTime.EXECUTING, Mission.Name .. "/STAGELOAD.LOADING.1." .. Host, Host , 10 ) + for CargoID, Cargo in pairs( CARGOS ) do + self:T( "Cargo.CargoName = " .. Cargo.CargoName ) + + if Cargo:IsSlingLoad() then + local CargoStatic = StaticObject.getByName( Cargo.CargoStaticName ) + if CargoStatic then + self:T( "Cargo is found in the DCS simulator.") + local CargoStaticPosition = CargoStatic:getPosition().p + self:T( "Cargo Position x = " .. CargoStaticPosition.x .. ", y = " .. CargoStaticPosition.y .. ", z = " .. CargoStaticPosition.z ) + local CargoStaticHeight = routines.GetUnitHeight( CargoStatic ) + if CargoStaticHeight > 5 then + self:T( "Cargo is airborne.") + Cargo:StatusLoaded() + Task.Cargo = Cargo + Client:Message( 'The Cargo has been successfully hooked onto the helicopter and is now being sling loaded. Fly outside the landing zone.', + self.MSG.TIME, Mission.Name .. "/STAGELANDING.LOADING.2." .. Host, Host ) + Task.ExecuteStage = _TransportExecuteStage.SUCCESS + break + end + else + self:T( "Cargo not found in the DCS simulator." ) + end + end + end + end + +end + +function STAGELOAD:Validate( Mission, Client, Task ) + self:F() + + self:T( "Task.CurrentLandingZoneName = " .. Task.CurrentLandingZoneName ) + + local Host = "Command" + if Task.HostUnitName then + Host = Task.HostUnitName .. " (" .. Task.HostUnitTypeName .. ")" + else + if Client:IsMultiSeated() then + Host = "Co-Pilot" + end + end + + if not Task.IsSlingLoad then + if not routines.IsUnitNearZonesRadius( Client:GetClientGroupDCSUnit(), Task.CurrentLandingZoneName, 500 ) then + Task:RemoveCargoMenus( Client ) + Task.ExecuteStage = _TransportExecuteStage.FAILED + Task.CargoName = nil + Client:Message( "The " .. Task.CargoType .. " loading has been aborted. You flew outside the pick-up zone while loading. ", + self.MSG.TIME, Mission.Name .. "/STAGELANDING.VALIDATE.1." .. Host, Host ) + self:T( -1 ) + return -1 + end + + local DCSUnitVelocityVec3 = Client:GetClientGroupDCSUnit():getVelocity() + local DCSUnitVelocity = ( DCSUnitVelocityVec3.x ^2 + DCSUnitVelocityVec3.y ^2 + DCSUnitVelocityVec3.z ^2 ) ^ 0.5 + + local DCSUnitPointVec3 = Client:GetClientGroupDCSUnit():getPoint() + local LandHeight = land.getHeight( { x = DCSUnitPointVec3.x, y = DCSUnitPointVec3.z } ) + local DCSUnitHeight = DCSUnitPointVec3.y - LandHeight + + self:T( { Task.IsLandingRequired, Client:GetClientGroupDCSUnit():inAir() } ) + if Task.IsLandingRequired and Task.IsInAirTestRequired == true and Client:GetClientGroupDCSUnit():inAir() then + Task:RemoveCargoMenus( Client ) + Task.ExecuteStage = _TransportExecuteStage.FAILED + Task.CargoName = nil + Client:Message( "The " .. Task.CargoType .. " loading has been aborted. Re-start the " .. Task.TEXT[3] .. " process. Don't fly outside the pick-up zone.", + self.MSG.TIME, Mission.Name .. "/STAGELANDING.VALIDATE.1." .. Host, Host ) + self:T( -1 ) + return -1 + end + + self:T( { DCSUnitVelocity, DCSUnitHeight, LandHeight, Task.CurrentCargoZone.SignalHeight } ) + if Task.IsLandingRequired and Task.IsInAirTestRequired == false and DCSUnitVelocity >= 2 and DCSUnitHeight >= Task.CurrentCargoZone.SignalHeight then + Task:RemoveCargoMenus( Client ) + Task.ExecuteStage = _TransportExecuteStage.FAILED + Task.CargoName = nil + Client:Message( "The " .. Task.CargoType .. " loading has been aborted. Re-start the " .. Task.TEXT[3] .. " process. Don't fly outside the pick-up zone.", + self.MSG.TIME, Mission.Name .. "/STAGELANDING.VALIDATE.1." .. Host, Host ) + self:T( -1 ) + return -1 + end + + if Task.ExecuteStage == _TransportExecuteStage.SUCCESS then + Task:RemoveCargoMenus( Client ) + Client:Message( "Good Job. The " .. Task.CargoType .. " has been sucessfully " .. Task.TEXT[3] .. " within the landing zone.", + self.MSG.TIME, Mission.Name .. "/STAGELANDING.VALIDATE.3." .. Host, Host ) + Task.MissionTask:AddGoalCompletion( Task.MissionTask.GoalVerb, Task.CargoName, 1 ) + self:T( 1 ) + return 1 + end + + else + if Task.ExecuteStage == _TransportExecuteStage.SUCCESS then + CargoStatic = StaticObject.getByName( Task.Cargo.CargoStaticName ) + if CargoStatic and not routines.IsStaticInZones( CargoStatic, Task.CurrentLandingZoneName ) then + Client:Message( "Good Job. The " .. Task.CargoType .. " has been sucessfully " .. Task.TEXT[3] .. " and flown outside of the landing zone.", + self.MSG.TIME, Mission.Name .. "/STAGELANDING.VALIDATE.4." .. Host, Host ) + Task.MissionTask:AddGoalCompletion( Task.MissionTask.GoalVerb, Task.Cargo.CargoName, 1 ) + self:T( 1 ) + return 1 + end + end + + end + + + self:T( 0 ) + return 0 +end + + +STAGEDONE = { + ClassName = "STAGEDONE", + MSG = { ID = "Done", TIME = 10 }, + Name = "Done" +} + +function STAGEDONE:New() + local self = BASE:Inherit( self, STAGE:New() ) + self:F() + self.StageType = 'AI' + return self +end + +function STAGEDONE:Execute( Mission, Client, Task ) + self:F() + +end + +function STAGEDONE:Validate( Mission, Client, Task ) + self:F() + + Task:Done() + + return 0 +end + +STAGEARRIVE = { + ClassName = "STAGEARRIVE", + MSG = { ID = "Arrive", TIME = 10 }, + Name = "Arrive" +} + +function STAGEARRIVE:New() + local self = BASE:Inherit( self, STAGE:New() ) + self:F() + self.StageType = 'CLIENT' + return self +end + + +--- Execute Arrival +-- @param #STAGEARRIVE self +-- @param Mission#MISSION Mission +-- @param Client#CLIENT Client +-- @param Task#TASK Task +function STAGEARRIVE:Execute( Mission, Client, Task ) + self:F() + + if Client:IsMultiSeated() then + Client:Message( 'We have arrived at ' .. Task.CurrentLandingZoneName .. ".", self.MSG.TIME, Mission.Name .. "/Stage", "Co-Pilot" ) + else + Client:Message( 'We have arrived at ' .. Task.CurrentLandingZoneName .. ".", self.MSG.TIME, Mission.Name .. "/Stage", "Command" ) + end + +end + +function STAGEARRIVE:Validate( Mission, Client, Task ) + self:F() + + Task.CurrentLandingZoneID = routines.IsUnitInZones( Client:GetClientGroupDCSUnit(), Task.LandingZones ) + if ( Task.CurrentLandingZoneID ) then + else + return -1 + end + + return 1 +end + +STAGEGROUPSDESTROYED = { + ClassName = "STAGEGROUPSDESTROYED", + DestroyGroupSize = -1, + Frequency = STAGE.FREQUENCY.REPEAT, + MSG = { ID = "DestroyGroup", TIME = 10 }, + Name = "GroupsDestroyed" +} + +function STAGEGROUPSDESTROYED:New() + local self = BASE:Inherit( self, STAGE:New() ) + self:F() + self.StageType = 'AI' + return self +end + +--function STAGEGROUPSDESTROYED:Execute( Mission, Client, Task ) +-- +-- Client:Message( 'Task: Still ' .. DestroyGroupSize .. " of " .. Task.DestroyGroupCount .. " " .. Task.DestroyGroupType .. " to be destroyed!", self.MSG.TIME, Mission.Name .. "/Stage" ) +-- +--end + +function STAGEGROUPSDESTROYED:Validate( Mission, Client, Task ) + self:F() + + if Task.MissionTask:IsGoalReached() then + return 1 + else + return 0 + end +end + +function STAGEGROUPSDESTROYED:Execute( Mission, Client, Task ) + self:F() + self:T( { Task.ClassName, Task.Destroyed } ) + --env.info( 'Event Table Task = ' .. tostring(Task) ) + +end + + + + + + + + + + + + + +--[[ + _TransportStage: Defines the different stages of which of transport missions can be in. This table is internal and is used to control the sequence of messages, actions and flow. + + - _TransportStage.START + - _TransportStage.ROUTE + - _TransportStage.LAND + - _TransportStage.EXECUTE + - _TransportStage.DONE + - _TransportStage.REMOVE +--]] +_TransportStage = { + HOLD = "HOLD", + START = "START", + ROUTE = "ROUTE", + LANDING = "LANDING", + LANDED = "LANDED", + EXECUTING = "EXECUTING", + LOAD = "LOAD", + UNLOAD = "UNLOAD", + DONE = "DONE", + NEXT = "NEXT" +} + +_TransportStageMsgTime = { + HOLD = 10, + START = 60, + ROUTE = 5, + LANDING = 10, + LANDED = 30, + EXECUTING = 30, + LOAD = 30, + UNLOAD = 30, + DONE = 30, + NEXT = 0 +} + +_TransportStageTime = { + HOLD = 10, + START = 5, + ROUTE = 5, + LANDING = 1, + LANDED = 1, + EXECUTING = 5, + LOAD = 5, + UNLOAD = 5, + DONE = 1, + NEXT = 0 +} + +_TransportStageAction = { + REPEAT = -1, + NONE = 0, + ONCE = 1 +} +--- The TASK Classes define major end-to-end activities within a MISSION. The TASK Class is the Master Class to orchestrate these activities. From this class, many concrete TASK classes are inherited. +-- @module TASK + +Include.File( "Routines" ) +Include.File( "Base" ) +Include.File( "Mission" ) +Include.File( "Client" ) +Include.File( "Stage" ) + +--- The TASK class +-- @type TASK +-- @extends Base#BASE +TASK = { + + -- Defines the different signal types with a Task. + SIGNAL = { + COLOR = { + RED = { ID = 1, COLOR = trigger.smokeColor.Red, TEXT = "A red" }, + GREEN = { ID = 2, COLOR = trigger.smokeColor.Green, TEXT = "A green" }, + BLUE = { ID = 3, COLOR = trigger.smokeColor.Blue, TEXT = "A blue" }, + WHITE = { ID = 4, COLOR = trigger.smokeColor.White, TEXT = "A white" }, + ORANGE = { ID = 5, COLOR = trigger.smokeColor.Orange, TEXT = "An orange" } + }, + TYPE = { + SMOKE = { ID = 1, TEXT = "smoke" }, + FLARE = { ID = 2, TEXT = "flare" } + } + }, + ClassName = "TASK", + Mission = {}, -- Owning mission of the Task + Name = '', + Stages = {}, + Stage = {}, + Cargos = { + InitCargos = {}, + LoadCargos = {} + }, + LandingZones = { + LandingZoneNames = {}, + LandingZones = {} + }, + ActiveStage = 0, + TaskDone = false, + TaskFailed = false, + GoalTasks = {} +} + +--- Instantiates a new TASK Base. Should never be used. Interface Class. +-- @return TASK +function TASK:New() + local self = BASE:Inherit( self, BASE:New() ) + self:F() + + -- assign Task default values during construction + self.TaskBriefing = "Task: No Task." + self.Time = timer.getTime() + self.ExecuteStage = _TransportExecuteStage.NONE + + return self +end + +function TASK:SetStage( StageSequenceIncrement ) + self:F( { StageSequenceIncrement } ) + + local Valid = false + if StageSequenceIncrement ~= 0 then + self.ActiveStage = self.ActiveStage + StageSequenceIncrement + if 1 <= self.ActiveStage and self.ActiveStage <= #self.Stages then + self.Stage = self.Stages[self.ActiveStage] + self:T( { self.Stage.Name } ) + self.Frequency = self.Stage.Frequency + Valid = true + else + Valid = false + env.info( "TASK:SetStage() self.ActiveStage is smaller or larger than self.Stages array. self.ActiveStage = " .. self.ActiveStage ) + end + end + self.Time = timer.getTime() + return Valid +end + +function TASK:Init() + self:F() + self.ActiveStage = 0 + self:SetStage(1) + self.TaskDone = false + self.TaskFailed = false +end + + +--- Get progress of a TASK. +-- @return string GoalsText +function TASK:GetGoalProgress() + self:F2() + + local GoalsText = "" + for GoalVerb, GoalVerbData in pairs( self.GoalTasks ) do + local Goals = self:GetGoalCompletion( GoalVerb ) + if Goals and Goals ~= "" then + Goals = '(' .. Goals .. ')' + else + Goals = '( - )' + end + GoalsText = GoalsText .. GoalVerb .. ': ' .. self:GetGoalCount(GoalVerb) .. ' goals ' .. Goals .. ' of ' .. self:GetGoalTotal(GoalVerb) .. ' goals completed (' .. self:GetGoalPercentage(GoalVerb) .. '%); ' + end + + if GoalsText == "" then + GoalsText = "( - )" + end + + return GoalsText +end + +--- Show progress of a TASK. +-- @param MISSION Mission Group structure describing the Mission. +-- @param CLIENT Client Group structure describing the Client. +function TASK:ShowGoalProgress( Mission, Client ) + self:F2() + + local GoalsText = "" + for GoalVerb, GoalVerbData in pairs( self.GoalTasks ) do + if Mission:IsCompleted() then + else + local Goals = self:GetGoalCompletion( GoalVerb ) + if Goals and Goals ~= "" then + else + Goals = "-" + end + GoalsText = GoalsText .. self:GetGoalProgress() + end + end + + if Mission.MissionReportFlash or Mission.MissionReportShow then + Client:Message( GoalsText, 10, "/TASKPROGRESS" .. self.ClassName, "Mission Command: Task Status", 30 ) + end +end + +--- Sets a TASK to status Done. +function TASK:Done() + self:F2() + self.TaskDone = true +end + +--- Returns if a TASK is done. +-- @return bool +function TASK:IsDone() + self:F2( self.TaskDone ) + return self.TaskDone +end + +--- Sets a TASK to status failed. +function TASK:Failed() + self:F() + self.TaskFailed = true +end + +--- Returns if a TASk has failed. +-- @return bool +function TASK:IsFailed() + self:F2( self.TaskFailed ) + return self.TaskFailed +end + +function TASK:Reset( Mission, Client ) + self:F2() + self.ExecuteStage = _TransportExecuteStage.NONE +end + +--- Returns the Goals of a TASK +-- @return @table Goals +function TASK:GetGoals() + return self.GoalTasks +end + +--- Returns if a TASK has Goal(s). +-- @param #TASK self +-- @param #string GoalVerb is the name of the Goal of the TASK. +-- @return bool +function TASK:Goal( GoalVerb ) + self:F2( { GoalVerb } ) + if not GoalVerb then + GoalVerb = self.GoalVerb + end + self:T2( {self.GoalTasks[GoalVerb] } ) + if self.GoalTasks[GoalVerb] and self.GoalTasks[GoalVerb].GoalTotal > 0 then + return true + else + return false + end +end + +--- Sets the total Goals to be achieved of the Goal Name +-- @param number GoalTotal is the number of times the GoalVerb needs to be achieved. +-- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. +function TASK:SetGoalTotal( GoalTotal, GoalVerb ) + self:F2( { GoalTotal, GoalVerb } ) + + if not GoalVerb then + GoalVerb = self.GoalVerb + end + self.GoalTasks[GoalVerb] = {} + self.GoalTasks[GoalVerb].Goals = {} + self.GoalTasks[GoalVerb].GoalTotal = GoalTotal + self.GoalTasks[GoalVerb].GoalCount = 0 + return self +end + +--- Gets the total of Goals to be achieved within the TASK of the GoalVerb. +-- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. +function TASK:GetGoalTotal( GoalVerb ) + self:F2( { GoalVerb } ) + if not GoalVerb then + GoalVerb = self.GoalVerb + end + if self:Goal( GoalVerb ) then + return self.GoalTasks[GoalVerb].GoalTotal + else + return 0 + end +end + +--- Sets the total of Goals currently achieved within the TASK of the GoalVerb. +-- @param number GoalCount is the total number of Goals achieved within the TASK. +-- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. +-- @return TASK +function TASK:SetGoalCount( GoalCount, GoalVerb ) + self:F2() + if not GoalVerb then + GoalVerb = self.GoalVerb + end + if self:Goal( GoalVerb) then + self.GoalTasks[GoalVerb].GoalCount = GoalCount + end + return self +end + +--- Increments the total of Goals currently achieved within the TASK of the GoalVerb, with the given GoalCountIncrease. +-- @param number GoalCountIncrease is the number of new Goals achieved within the TASK. +-- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. +-- @return TASK +function TASK:IncreaseGoalCount( GoalCountIncrease, GoalVerb ) + self:F2( { GoalCountIncrease, GoalVerb } ) + if not GoalVerb then + GoalVerb = self.GoalVerb + end + if self:Goal( GoalVerb) then + self.GoalTasks[GoalVerb].GoalCount = self.GoalTasks[GoalVerb].GoalCount + GoalCountIncrease + end + return self +end + +--- Gets the total of Goals currently achieved within the TASK of the GoalVerb. +-- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. +-- @return TASK +function TASK:GetGoalCount( GoalVerb ) + self:F2() + if not GoalVerb then + GoalVerb = self.GoalVerb + end + if self:Goal( GoalVerb ) then + return self.GoalTasks[GoalVerb].GoalCount + else + return 0 + end +end + +--- Gets the percentage of Goals currently achieved within the TASK of the GoalVerb. +-- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. +-- @return TASK +function TASK:GetGoalPercentage( GoalVerb ) + self:F2() + if not GoalVerb then + GoalVerb = self.GoalVerb + end + if self:Goal( GoalVerb ) then + return math.floor( self:GetGoalCount( GoalVerb ) / self:GetGoalTotal( GoalVerb ) * 100 + .5 ) + else + return 100 + end +end + +--- Returns if all the Goals of the TASK were achieved. +-- @return bool +function TASK:IsGoalReached() + self:F2() + + local GoalReached = true + + for GoalVerb, Goals in pairs( self.GoalTasks ) do + self:T2( { "GoalVerb", GoalVerb } ) + if self:Goal( GoalVerb ) then + local GoalToDo = self:GetGoalTotal( GoalVerb ) - self:GetGoalCount( GoalVerb ) + self:T2( "GoalToDo = " .. GoalToDo ) + if GoalToDo <= 0 then + else + GoalReached = false + break + end + else + break + end + end + + self:T( { GoalReached, self.GoalTasks } ) + return GoalReached +end + +--- Adds an Additional Goal for the TASK to be achieved. +-- @param string GoalVerb is the name of the Goal of the TASK. +-- @param string GoalTask is a text describing the Goal of the TASK to be achieved. +-- @param number GoalIncrease is a number by which the Goal achievement is increasing. +function TASK:AddGoalCompletion( GoalVerb, GoalTask, GoalIncrease ) + self:F2( { GoalVerb, GoalTask, GoalIncrease } ) + + if self:Goal( GoalVerb ) then + self.GoalTasks[GoalVerb].Goals[#self.GoalTasks[GoalVerb].Goals+1] = GoalTask + self.GoalTasks[GoalVerb].GoalCount = self.GoalTasks[GoalVerb].GoalCount + GoalIncrease + end + return self +end + +--- Returns if the additional Goal for the TASK was completed. +-- @param ?string GoalVerb is the name of the Goal of the TASK. If the GoalVerb is not given, then the default TASK Goals will be used. +-- @return string Goals +function TASK:GetGoalCompletion( GoalVerb ) + self:F2( { GoalVerb } ) + + if self:Goal( GoalVerb ) then + local Goals = "" + for GoalID, GoalName in pairs( self.GoalTasks[GoalVerb].Goals ) do Goals = Goals .. GoalName .. " + " end + return Goals:gsub(" + $", ""), self.GoalTasks[GoalVerb].GoalCount + end +end + +function TASK.MenuAction( Parameter ) + Parameter.ReferenceTask.ExecuteStage = _TransportExecuteStage.EXECUTING + Parameter.ReferenceTask.Cargo = Parameter.CargoTask +end + +function TASK:StageExecute() + self:F() + + local Execute = false + + if self.Frequency == STAGE.FREQUENCY.REPEAT then + Execute = true + elseif self.Frequency == STAGE.FREQUENCY.NONE then + Execute = false + elseif self.Frequency >= 0 then + Execute = true + self.Frequency = self.Frequency - 1 + end + + return Execute + +end + +--- Work function to set signal events within a TASK. +function TASK:AddSignal( SignalUnitNames, SignalType, SignalColor, SignalHeight ) + self:F() + + local Valid = true + + if Valid then + if type( SignalUnitNames ) == "table" then + self.LandingZoneSignalUnitNames = SignalUnitNames + else + self.LandingZoneSignalUnitNames = { SignalUnitNames } + end + self.LandingZoneSignalType = SignalType + self.LandingZoneSignalColor = SignalColor + self.Signalled = false + if SignalHeight ~= nil then + self.LandingZoneSignalHeight = SignalHeight + else + self.LandingZoneSignalHeight = 0 + end + + if self.TaskBriefing then + self.TaskBriefing = self.TaskBriefing .. " " .. SignalColor.TEXT .. " " .. SignalType.TEXT .. " will be fired when entering the landing zone." + end + end + + return Valid +end + +--- When the CLIENT is approaching the landing zone, a RED SMOKE will be fired by an optional SignalUnitNames. +-- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. +-- @param number SignalHeight Altitude that the Signal should be fired... +function TASK:AddSmokeRed( SignalUnitNames, SignalHeight ) + self:F() + self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.SMOKE, TASK.SIGNAL.COLOR.RED, SignalHeight ) +end + +--- When the CLIENT is approaching the landing zone, a GREEN SMOKE will be fired by an optional SignalUnitNames. +-- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. +-- @param number SignalHeight Altitude that the Signal should be fired... +function TASK:AddSmokeGreen( SignalUnitNames, SignalHeight ) + self:F() + self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.SMOKE, TASK.SIGNAL.COLOR.GREEN, SignalHeight ) +end + +--- When the CLIENT is approaching the landing zone, a BLUE SMOKE will be fired by an optional SignalUnitNames. +-- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. +-- @param number SignalHeight Altitude that the Signal should be fired... +function TASK:AddSmokeBlue( SignalUnitNames, SignalHeight ) + self:F() + self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.SMOKE, TASK.SIGNAL.COLOR.BLUE, SignalHeight ) +end + +--- When the CLIENT is approaching the landing zone, a WHITE SMOKE will be fired by an optional SignalUnitNames. +-- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. +-- @param number SignalHeight Altitude that the Signal should be fired... +function TASK:AddSmokeWhite( SignalUnitNames, SignalHeight ) + self:F() + self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.SMOKE, TASK.SIGNAL.COLOR.WHITE, SignalHeight ) +end + +--- When the CLIENT is approaching the landing zone, an ORANGE SMOKE will be fired by an optional SignalUnitNames. +-- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. +-- @param number SignalHeight Altitude that the Signal should be fired... +function TASK:AddSmokeOrange( SignalUnitNames, SignalHeight ) + self:F() + self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.SMOKE, TASK.SIGNAL.COLOR.ORANGE, SignalHeight ) +end + +--- When the CLIENT is approaching the landing zone, a RED FLARE will be fired by an optional SignalUnitNames. +-- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. +-- @param number SignalHeight Altitude that the Signal should be fired... +function TASK:AddFlareRed( SignalUnitNames, SignalHeight ) + self:F() + self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.FLARE, TASK.SIGNAL.COLOR.RED, SignalHeight ) +end + +--- When the CLIENT is approaching the landing zone, a GREEN FLARE will be fired by an optional SignalUnitNames. +-- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. +-- @param number SignalHeight Altitude that the Signal should be fired... +function TASK:AddFlareGreen( SignalUnitNames, SignalHeight ) + self:F() + self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.FLARE, TASK.SIGNAL.COLOR.GREEN, SignalHeight ) +end + +--- When the CLIENT is approaching the landing zone, a BLUE FLARE will be fired by an optional SignalUnitNames. +-- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. +-- @param number SignalHeight Altitude that the Signal should be fired... +function TASK:AddFlareBlue( SignalUnitNames, SignalHeight ) + self:F() + self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.FLARE, TASK.SIGNAL.COLOR.BLUE, SignalHeight ) +end + +--- When the CLIENT is approaching the landing zone, a WHITE FLARE will be fired by an optional SignalUnitNames. +-- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. +-- @param number SignalHeight Altitude that the Signal should be fired... +function TASK:AddFlareWhite( SignalUnitNames, SignalHeight ) + self:F() + self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.FLARE, TASK.SIGNAL.COLOR.WHITE, SignalHeight ) +end + +--- When the CLIENT is approaching the landing zone, an ORANGE FLARE will be fired by an optional SignalUnitNames. +-- @param table|string SignalUnitNames Name of the Group that will fire the signal. If this parameter is NIL, the signal will be fired from the center of the landing zone. +-- @param number SignalHeight Altitude that the Signal should be fired... +function TASK:AddFlareOrange( SignalUnitNames, SignalHeight ) + self:F() + self:AddSignal( SignalUnitNames, TASK.SIGNAL.TYPE.FLARE, TASK.SIGNAL.COLOR.ORANGE, SignalHeight ) +end +--- A GOHOMETASK orchestrates the travel back to the home base, which is a specific zone defined within the ME. +-- @module GOHOMETASK + +Include.File("Task") + +--- The GOHOMETASK class +-- @type +GOHOMETASK = { + ClassName = "GOHOMETASK", +} + +--- Creates a new GOHOMETASK. +-- @param table{string,...}|string LandingZones Table of Landing Zone names where Home(s) are located. +-- @return GOHOMETASK +function GOHOMETASK:New( LandingZones ) + local self = BASE:Inherit( self, TASK:New() ) + self:F( { LandingZones } ) + local Valid = true + + Valid = routines.ValidateZone( LandingZones, "LandingZones", Valid ) + + if Valid then + self.Name = 'Fly Home' + self.TaskBriefing = "Task: Fly back to your home base. Your co-pilot will provide you with the directions (required flight angle in degrees) and the distance (in km) to your home base." + if type( LandingZones ) == "table" then + self.LandingZones = LandingZones + else + self.LandingZones = { LandingZones } + end + self.Stages = { STAGEBRIEF:New(), STAGESTART:New(), STAGEROUTE:New(), STAGEARRIVE:New(), STAGEDONE:New() } + self.SetStage( self, 1 ) + end + + return self +end +--- A DESTROYBASETASK will monitor the destruction of Groups and Units. This is a BASE class, other classes are derived from this class. +-- @module DESTROYBASETASK +-- @see DESTROYGROUPSTASK +-- @see DESTROYUNITTYPESTASK +-- @see DESTROY_RADARS_TASK + +Include.File("Task") + +--- The DESTROYBASETASK class +-- @type DESTROYBASETASK +DESTROYBASETASK = { + ClassName = "DESTROYBASETASK", + Destroyed = 0, + GoalVerb = "Destroy", + DestroyPercentage = 100, +} + +--- Creates a new DESTROYBASETASK. +-- @param #DESTROYBASETASK self +-- @param #string DestroyGroupType Text describing the group to be destroyed. f.e. "Radar Installations", "Ships", "Vehicles", "Command Centers". +-- @param #string DestroyUnitType Text describing the unit types to be destroyed. f.e. "SA-6", "Row Boats", "Tanks", "Tents". +-- @param #list<#string> DestroyGroupPrefixes Table of Prefixes of the Groups to be destroyed before task is completed. +-- @param #number DestroyPercentage defines the %-tage that needs to be destroyed to achieve mission success. eg. If in the Group there are 10 units, then a value of 75 would require 8 units to be destroyed from the Group to complete the @{TASK}. +-- @return DESTROYBASETASK +function DESTROYBASETASK:New( DestroyGroupType, DestroyUnitType, DestroyGroupPrefixes, DestroyPercentage ) + local self = BASE:Inherit( self, TASK:New() ) + self:F() + + self.Name = 'Destroy' + self.Destroyed = 0 + self.DestroyGroupPrefixes = DestroyGroupPrefixes + self.DestroyGroupType = DestroyGroupType + self.DestroyUnitType = DestroyUnitType + if DestroyPercentage then + self.DestroyPercentage = DestroyPercentage + end + self.TaskBriefing = "Task: Destroy " .. DestroyGroupType .. "." + self.Stages = { STAGEBRIEF:New(), STAGESTART:New(), STAGEGROUPSDESTROYED:New(), STAGEDONE:New() } + self.SetStage( self, 1 ) + + return self +end + +--- Handle the S_EVENT_DEAD events to validate the destruction of units for the task monitoring. +-- @param #DESTROYBASETASK self +-- @param Event#EVENTDATA Event structure of MOOSE. +function DESTROYBASETASK:EventDead( Event ) + self:F( { Event } ) + + if Event.IniDCSUnit then + local DestroyUnit = Event.IniDCSUnit + local DestroyUnitName = Event.IniDCSUnitName + local DestroyGroup = Event.IniDCSGroup + local DestroyGroupName = Event.IniDCSGroupName + + --TODO: I need to fix here if 2 groups in the mission have a similar name with GroupPrefix equal, then i should differentiate for which group the goal was reached! + --I may need to test if for the goalverb that group goal was reached or something. Need to think about it a bit more ... + local UnitsDestroyed = 0 + for DestroyGroupPrefixID, DestroyGroupPrefix in pairs( self.DestroyGroupPrefixes ) do + self:T( DestroyGroupPrefix ) + if string.find( DestroyGroupName, DestroyGroupPrefix, 1, true ) then + self:T( BASE:Inherited(self).ClassName ) + UnitsDestroyed = self:ReportGoalProgress( DestroyGroup, DestroyUnit ) + self:T( UnitsDestroyed ) + end + end + + self:T( { UnitsDestroyed } ) + self:IncreaseGoalCount( UnitsDestroyed, self.GoalVerb ) + end + +end + +--- Validate task completeness of DESTROYBASETASK. +-- @param DestroyGroup Group structure describing the group to be evaluated. +-- @param DestroyUnit Unit structure describing the Unit to be evaluated. +function DESTROYBASETASK:ReportGoalProgress( DestroyGroup, DestroyUnit ) + self:F() + + return 0 +end +--- DESTROYGROUPSTASK +-- @module DESTROYGROUPSTASK + +Include.File("DestroyBaseTask") + +--- The DESTROYGROUPSTASK class +-- @type +DESTROYGROUPSTASK = { + ClassName = "DESTROYGROUPSTASK", + GoalVerb = "Destroy Groups", +} + +--- Creates a new DESTROYGROUPSTASK. +-- @param #DESTROYGROUPSTASK self +-- @param #string DestroyGroupType String describing the group to be destroyed. +-- @param #string DestroyUnitType String describing the unit to be destroyed. +-- @param #list<#string> DestroyGroupNames Table of string containing the name of the groups to be destroyed before task is completed. +-- @param #number DestroyPercentage defines the %-tage that needs to be destroyed to achieve mission success. eg. If in the Group there are 10 units, then a value of 75 would require 8 units to be destroyed from the Group to complete the @{TASK}. +---@return DESTROYGROUPSTASK +function DESTROYGROUPSTASK:New( DestroyGroupType, DestroyUnitType, DestroyGroupNames, DestroyPercentage ) + local self = BASE:Inherit( self, DESTROYBASETASK:New( DestroyGroupType, DestroyUnitType, DestroyGroupNames, DestroyPercentage ) ) + self:F() + + self.Name = 'Destroy Groups' + self.GoalVerb = "Destroy " .. DestroyGroupType + + _EVENTDISPATCHER:OnDead( self.EventDead , self ) + _EVENTDISPATCHER:OnCrash( self.EventDead , self ) + + return self +end + +--- Report Goal Progress. +-- @param #DESTROYGROUPSTASK self +-- @param DCSGroup#Group DestroyGroup Group structure describing the group to be evaluated. +-- @param DCSUnit#Unit DestroyUnit Unit structure describing the Unit to be evaluated. +-- @return #number The DestroyCount reflecting the amount of units destroyed within the group. +function DESTROYGROUPSTASK:ReportGoalProgress( DestroyGroup, DestroyUnit ) + self:F( { DestroyGroup, DestroyUnit, self.DestroyPercentage } ) + + local DestroyGroupSize = DestroyGroup:getSize() - 1 -- When a DEAD event occurs, the getSize is still one larger than the destroyed unit. + local DestroyGroupInitialSize = DestroyGroup:getInitialSize() + self:T( { DestroyGroupSize, DestroyGroupInitialSize - ( DestroyGroupInitialSize * self.DestroyPercentage / 100 ) } ) + + local DestroyCount = 0 + if DestroyGroup then + if DestroyGroupSize <= DestroyGroupInitialSize - ( DestroyGroupInitialSize * self.DestroyPercentage / 100 ) then + DestroyCount = 1 + end + else + DestroyCount = 1 + end + + self:T( DestroyCount ) + + return DestroyCount +end +--- Task class to destroy radar installations. +-- @module DESTROYRADARSTASK + +Include.File("DestroyBaseTask") + +--- The DESTROYRADARS class +-- @type +DESTROYRADARSTASK = { + ClassName = "DESTROYRADARSTASK", + GoalVerb = "Destroy Radars" +} + +--- Creates a new DESTROYRADARSTASK. +-- @param table{string,...} DestroyGroupNames Table of string containing the group names of which the radars are be destroyed. +-- @return DESTROYRADARSTASK +function DESTROYRADARSTASK:New( DestroyGroupNames ) + local self = BASE:Inherit( self, DESTROYGROUPSTASK:New( 'radar installations', 'radars', DestroyGroupNames ) ) + self:F() + + self.Name = 'Destroy Radars' + + _EVENTDISPATCHER:OnDead( self.EventDead , self ) + + return self +end + +--- Report Goal Progress. +-- @param Group DestroyGroup Group structure describing the group to be evaluated. +-- @param Unit DestroyUnit Unit structure describing the Unit to be evaluated. +function DESTROYRADARSTASK:ReportGoalProgress( DestroyGroup, DestroyUnit ) + self:F( { DestroyGroup, DestroyUnit } ) + + local DestroyCount = 0 + if DestroyUnit and DestroyUnit:hasSensors( Unit.SensorType.RADAR, Unit.RadarType.AS ) then + if DestroyUnit and DestroyUnit:getLife() <= 1.0 then + self:T( 'Destroyed a radar' ) + DestroyCount = 1 + end + end + return DestroyCount +end +--- Set TASK to destroy certain unit types. +-- @module DESTROYUNITTYPESTASK + +Include.File("DestroyBaseTask") + +--- The DESTROYUNITTYPESTASK class +-- @type +DESTROYUNITTYPESTASK = { + ClassName = "DESTROYUNITTYPESTASK", + GoalVerb = "Destroy", +} + +--- Creates a new DESTROYUNITTYPESTASK. +-- @param string DestroyGroupType String describing the group to be destroyed. f.e. "Radar Installations", "Fleet", "Batallion", "Command Centers". +-- @param string DestroyUnitType String describing the unit to be destroyed. f.e. "radars", "ships", "tanks", "centers". +-- @param table{string,...} DestroyGroupNames Table of string containing the group names of which the radars are be destroyed. +-- @param string DestroyUnitTypes Table of string containing the type names of the units to achieve mission success. +-- @return DESTROYUNITTYPESTASK +function DESTROYUNITTYPESTASK:New( DestroyGroupType, DestroyUnitType, DestroyGroupNames, DestroyUnitTypes ) + local self = BASE:Inherit( self, DESTROYBASETASK:New( DestroyGroupType, DestroyUnitType, DestroyGroupNames ) ) + self:F( { DestroyGroupType, DestroyUnitType, DestroyGroupNames, DestroyUnitTypes } ) + + if type(DestroyUnitTypes) == 'table' then + self.DestroyUnitTypes = DestroyUnitTypes + else + self.DestroyUnitTypes = { DestroyUnitTypes } + end + + self.Name = 'Destroy Unit Types' + self.GoalVerb = "Destroy " .. DestroyGroupType + + _EVENTDISPATCHER:OnDead( self.EventDead , self ) + + return self +end + +--- Report Goal Progress. +-- @param Group DestroyGroup Group structure describing the group to be evaluated. +-- @param Unit DestroyUnit Unit structure describing the Unit to be evaluated. +function DESTROYUNITTYPESTASK:ReportGoalProgress( DestroyGroup, DestroyUnit ) + self:F( { DestroyGroup, DestroyUnit } ) + + local DestroyCount = 0 + for UnitTypeID, UnitType in pairs( self.DestroyUnitTypes ) do + if DestroyUnit and DestroyUnit:getTypeName() == UnitType then + if DestroyUnit and DestroyUnit:getLife() <= 1.0 then + DestroyCount = DestroyCount + 1 + end + end + end + return DestroyCount +end +--- A PICKUPTASK orchestrates the loading of CARGO at a specific landing zone. +-- @module PICKUPTASK +-- @parent TASK + +Include.File("Task") +Include.File("Cargo") + +--- The PICKUPTASK class +-- @type +PICKUPTASK = { + ClassName = "PICKUPTASK", + TEXT = { "Pick-Up", "picked-up", "loaded" }, + GoalVerb = "Pick-Up" +} + +--- Creates a new PICKUPTASK. +-- @param table{string,...}|string LandingZones Table of Zone names where Cargo is to be loaded. +-- @param CARGO_TYPE CargoType Type of the Cargo. The type must be of the following Enumeration:.. +-- @param number OnBoardSide Reflects from which side the cargo Group will be on-boarded on the Carrier. +function PICKUPTASK:New( CargoType, OnBoardSide ) + local self = BASE:Inherit( self, TASK:New() ) + self:F() + + -- self holds the inherited instance of the PICKUPTASK Class to the BASE class. + + local Valid = true + + if Valid then + self.Name = 'Pickup Cargo' + self.TaskBriefing = "Task: Fly to the indicated landing zones and pickup " .. CargoType .. ". Your co-pilot will provide you with the directions (required flight angle in degrees) and the distance (in km) to the pickup zone." + self.CargoType = CargoType + self.GoalVerb = CargoType .. " " .. self.GoalVerb + self.OnBoardSide = OnBoardSide + self.IsLandingRequired = true -- required to decide whether the client needs to land or not + self.IsSlingLoad = false -- Indicates whether the cargo is a sling load cargo + self.Stages = { STAGE_CARGO_INIT:New(), STAGE_CARGO_LOAD:New(), STAGEBRIEF:New(), STAGESTART:New(), STAGEROUTE:New(), STAGELANDING:New(), STAGELANDED:New(), STAGELOAD:New(), STAGEDONE:New() } + self.SetStage( self, 1 ) + end + + return self +end + +function PICKUPTASK:FromZone( LandingZone ) + self:F() + + self.LandingZones.LandingZoneNames[LandingZone.CargoZoneName] = LandingZone.CargoZoneName + self.LandingZones.LandingZones[LandingZone.CargoZoneName] = LandingZone + + return self +end + +function PICKUPTASK:InitCargo( InitCargos ) + self:F( { InitCargos } ) + + if type( InitCargos ) == "table" then + self.Cargos.InitCargos = InitCargos + else + self.Cargos.InitCargos = { InitCargos } + end + + return self +end + +function PICKUPTASK:LoadCargo( LoadCargos ) + self:F( { LoadCargos } ) + + if type( LoadCargos ) == "table" then + self.Cargos.LoadCargos = LoadCargos + else + self.Cargos.LoadCargos = { LoadCargos } + end + + return self +end + +function PICKUPTASK:AddCargoMenus( Client, Cargos, TransportRadius ) + self:F() + + for CargoID, Cargo in pairs( Cargos ) do + + self:T( { Cargo.ClassName, Cargo.CargoName, Cargo.CargoType, Cargo:IsStatusNone(), Cargo:IsStatusLoaded(), Cargo:IsStatusLoading(), Cargo:IsStatusUnLoaded() } ) + + -- If the Cargo has no status, allow the menu option. + if Cargo:IsStatusNone() or ( Cargo:IsStatusLoading() and Client == Cargo:IsLoadingToClient() ) then + + local MenuAdd = false + if Cargo:IsNear( Client, self.CurrentCargoZone ) then + MenuAdd = true + end + + if MenuAdd then + if Client._Menus[Cargo.CargoType] == nil then + Client._Menus[Cargo.CargoType] = {} + end + + if not Client._Menus[Cargo.CargoType].PickupMenu then + Client._Menus[Cargo.CargoType].PickupMenu = missionCommands.addSubMenuForGroup( + Client:GetClientGroupID(), + self.TEXT[1] .. " " .. Cargo.CargoType, + nil + ) + self:T( 'Added PickupMenu: ' .. self.TEXT[1] .. " " .. Cargo.CargoType ) + end + + if Client._Menus[Cargo.CargoType].PickupSubMenus == nil then + Client._Menus[Cargo.CargoType].PickupSubMenus = {} + end + + Client._Menus[Cargo.CargoType].PickupSubMenus[ #Client._Menus[Cargo.CargoType].PickupSubMenus + 1 ] = missionCommands.addCommandForGroup( + Client:GetClientGroupID(), + Cargo.CargoName .. " ( " .. Cargo.CargoWeight .. "kg )", + Client._Menus[Cargo.CargoType].PickupMenu, + self.MenuAction, + { ReferenceTask = self, CargoTask = Cargo } + ) + self:T( 'Added PickupSubMenu' .. Cargo.CargoType .. ":" .. Cargo.CargoName .. " ( " .. Cargo.CargoWeight .. "kg )" ) + end + end + end + +end + +function PICKUPTASK:RemoveCargoMenus( Client ) + self:F() + + for MenuID, MenuData in pairs( Client._Menus ) do + for SubMenuID, SubMenuData in pairs( MenuData.PickupSubMenus ) do + missionCommands.removeItemForGroup( Client:GetClientGroupID(), SubMenuData ) + self:T( "Removed PickupSubMenu " ) + SubMenuData = nil + end + if MenuData.PickupMenu then + missionCommands.removeItemForGroup( Client:GetClientGroupID(), MenuData.PickupMenu ) + self:T( "Removed PickupMenu " ) + MenuData.PickupMenu = nil + end + end + + for CargoID, Cargo in pairs( CARGOS ) do + self:T( { Cargo.ClassName, Cargo.CargoName, Cargo.CargoType, Cargo:IsStatusNone(), Cargo:IsStatusLoaded(), Cargo:IsStatusLoading(), Cargo:IsStatusUnLoaded() } ) + if Cargo:IsStatusLoading() and Client == Cargo:IsLoadingToClient() then + Cargo:StatusNone() + end + end + +end + + + +function PICKUPTASK:HasFailed( ClientDead ) + self:F() + + local TaskHasFailed = self.TaskFailed + return TaskHasFailed +end + +--- A DEPLOYTASK orchestrates the deployment of CARGO within a specific landing zone. +-- @module DEPLOYTASK + +Include.File( "Task" ) + +--- A DeployTask +-- @type DEPLOYTASK +DEPLOYTASK = { + ClassName = "DEPLOYTASK", + TEXT = { "Deploy", "deployed", "unloaded" }, + GoalVerb = "Deployment" +} + + +--- Creates a new DEPLOYTASK object, which models the sequence of STAGEs to unload a cargo. +-- @function [parent=#DEPLOYTASK] New +-- @param #string CargoType Type of the Cargo. +-- @return #DEPLOYTASK The created DeployTask +function DEPLOYTASK:New( CargoType ) + local self = BASE:Inherit( self, TASK:New() ) + self:F() + + local Valid = true + + if Valid then + self.Name = 'Deploy Cargo' + self.TaskBriefing = "Fly to one of the indicated landing zones and deploy " .. CargoType .. ". Your co-pilot will provide you with the directions (required flight angle in degrees) and the distance (in km) to the deployment zone." + self.CargoType = CargoType + self.GoalVerb = CargoType .. " " .. self.GoalVerb + self.Stages = { STAGE_CARGO_INIT:New(), STAGE_CARGO_LOAD:New(), STAGEBRIEF:New(), STAGESTART:New(), STAGEROUTE:New(), STAGELANDING:New(), STAGELANDED:New(), STAGEUNLOAD:New(), STAGEDONE:New() } + self.SetStage( self, 1 ) + end + + return self +end + +function DEPLOYTASK:ToZone( LandingZone ) + self:F() + + self.LandingZones.LandingZoneNames[LandingZone.CargoZoneName] = LandingZone.CargoZoneName + self.LandingZones.LandingZones[LandingZone.CargoZoneName] = LandingZone + + return self +end + + +function DEPLOYTASK:InitCargo( InitCargos ) + self:F( { InitCargos } ) + + if type( InitCargos ) == "table" then + self.Cargos.InitCargos = InitCargos + else + self.Cargos.InitCargos = { InitCargos } + end + + return self +end + + +function DEPLOYTASK:LoadCargo( LoadCargos ) + self:F( { LoadCargos } ) + + if type( LoadCargos ) == "table" then + self.Cargos.LoadCargos = LoadCargos + else + self.Cargos.LoadCargos = { LoadCargos } + end + + return self +end + + +--- When the cargo is unloaded, it will move to the target zone name. +-- @param string TargetZoneName Name of the Zone to where the Cargo should move after unloading. +function DEPLOYTASK:SetCargoTargetZoneName( TargetZoneName ) + self:F() + + local Valid = true + + Valid = routines.ValidateString( TargetZoneName, "TargetZoneName", Valid ) + + if Valid then + self.TargetZoneName = TargetZoneName + end + + return Valid + +end + +function DEPLOYTASK:AddCargoMenus( Client, Cargos, TransportRadius ) + self:F() + + local ClientGroupID = Client:GetClientGroupID() + + self:T( ClientGroupID ) + + for CargoID, Cargo in pairs( Cargos ) do + + self:T( { Cargo.ClassName, Cargo.CargoName, Cargo.CargoType, Cargo.CargoWeight } ) + + if Cargo:IsStatusLoaded() and Client == Cargo:IsLoadedInClient() then + + if Client._Menus[Cargo.CargoType] == nil then + Client._Menus[Cargo.CargoType] = {} + end + + if not Client._Menus[Cargo.CargoType].DeployMenu then + Client._Menus[Cargo.CargoType].DeployMenu = missionCommands.addSubMenuForGroup( + ClientGroupID, + self.TEXT[1] .. " " .. Cargo.CargoType, + nil + ) + self:T( 'Added DeployMenu ' .. self.TEXT[1] ) + end + + if Client._Menus[Cargo.CargoType].DeploySubMenus == nil then + Client._Menus[Cargo.CargoType].DeploySubMenus = {} + end + + if Client._Menus[Cargo.CargoType].DeployMenu == nil then + self:T( 'deploymenu is nil' ) + end + + Client._Menus[Cargo.CargoType].DeploySubMenus[ #Client._Menus[Cargo.CargoType].DeploySubMenus + 1 ] = missionCommands.addCommandForGroup( + ClientGroupID, + Cargo.CargoName .. " ( " .. Cargo.CargoWeight .. "kg )", + Client._Menus[Cargo.CargoType].DeployMenu, + self.MenuAction, + { ReferenceTask = self, CargoTask = Cargo } + ) + self:T( 'Added DeploySubMenu ' .. Cargo.CargoType .. ":" .. Cargo.CargoName .. " ( " .. Cargo.CargoWeight .. "kg )" ) + end + end + +end + +function DEPLOYTASK:RemoveCargoMenus( Client ) + self:F() + + local ClientGroupID = Client:GetClientGroupID() + self:T( ClientGroupID ) + + for MenuID, MenuData in pairs( Client._Menus ) do + if MenuData.DeploySubMenus ~= nil then + for SubMenuID, SubMenuData in pairs( MenuData.DeploySubMenus ) do + missionCommands.removeItemForGroup( ClientGroupID, SubMenuData ) + self:T( "Removed DeploySubMenu " ) + SubMenuData = nil + end + end + if MenuData.DeployMenu then + missionCommands.removeItemForGroup( ClientGroupID, MenuData.DeployMenu ) + self:T( "Removed DeployMenu " ) + MenuData.DeployMenu = nil + end + end + +end +--- A NOTASK is a dummy activity... But it will show a Mission Briefing... +-- @module NOTASK + +Include.File("Task") + +--- The NOTASK class +-- @type +NOTASK = { + ClassName = "NOTASK", +} + +--- Creates a new NOTASK. +function NOTASK:New() + local self = BASE:Inherit( self, TASK:New() ) + self:F() + + local Valid = true + + if Valid then + self.Name = 'Nothing' + self.TaskBriefing = "Task: Execute your mission." + self.Stages = { STAGEBRIEF:New(), STAGESTART:New(), STAGEDONE:New() } + self.SetStage( self, 1 ) + end + + return self +end +--- A ROUTETASK orchestrates the travel to a specific zone defined within the ME. +-- @module ROUTETASK + +--- The ROUTETASK class +-- @type +ROUTETASK = { + ClassName = "ROUTETASK", + GoalVerb = "Route", +} + +--- Creates a new ROUTETASK. +-- @param table{sring,...}|string LandingZones Table of Zone Names where the target is located. +-- @param string TaskBriefing (optional) Defines a text describing the briefing of the task. +-- @return ROUTETASK +function ROUTETASK:New( LandingZones, TaskBriefing ) + local self = BASE:Inherit( self, TASK:New() ) + self:F( { LandingZones, TaskBriefing } ) + + local Valid = true + + Valid = routines.ValidateZone( LandingZones, "LandingZones", Valid ) + + if Valid then + self.Name = 'Route To Zone' + if TaskBriefing then + self.TaskBriefing = TaskBriefing .. " Your co-pilot will provide you with the directions (required flight angle in degrees) and the distance (in km) to the target objective." + else + self.TaskBriefing = "Task: Fly to specified zone(s). Your co-pilot will provide you with the directions (required flight angle in degrees) and the distance (in km) to the target objective." + end + if type( LandingZones ) == "table" then + self.LandingZones = LandingZones + else + self.LandingZones = { LandingZones } + end + self.Stages = { STAGEBRIEF:New(), STAGESTART:New(), STAGEROUTE:New(), STAGEARRIVE:New(), STAGEDONE:New() } + self.SetStage( self, 1 ) + end + + return self +end + +--- A MISSION is the main owner of a Mission orchestration within MOOSE . The Mission framework orchestrates @{CLIENT}s, @{TASK}s, @{STAGE}s etc. +-- A @{CLIENT} needs to be registered within the @{MISSION} through the function @{AddClient}. A @{TASK} needs to be registered within the @{MISSION} through the function @{AddTask}. +-- @module Mission + +Include.File( "Routines" ) +Include.File( "Base" ) +Include.File( "Client" ) +Include.File( "Task" ) + +--- The MISSION class +-- @type MISSION +-- @extends Base#BASE +-- @field #MISSION.Clients _Clients +-- @field #string MissionBriefing +MISSION = { + ClassName = "MISSION", + Name = "", + MissionStatus = "PENDING", + _Clients = {}, + _Tasks = {}, + _ActiveTasks = {}, + GoalFunction = nil, + MissionReportTrigger = 0, + MissionProgressTrigger = 0, + MissionReportShow = false, + MissionReportFlash = false, + MissionTimeInterval = 0, + MissionCoalition = "", + SUCCESS = 1, + FAILED = 2, + REPEAT = 3, + _GoalTasks = {} +} + +--- @type MISSION.Clients +-- @list + +function MISSION:Meta() + + local self = BASE:Inherit( self, BASE:New() ) + self:F() + + return self +end + +--- This is the main MISSION declaration method. Each Mission is like the master or a Mission orchestration between, Clients, Tasks, Stages etc. +-- @param string MissionName is the name of the mission. This name will be used to reference the status of each mission by the players. +-- @param string MissionPriority is a string indicating the "priority" of the Mission. f.e. "Primary", "Secondary" or "First", "Second". It is free format and up to the Mission designer to choose. There are no rules behind this field. +-- @param string MissionBriefing is a string indicating the mission briefing to be shown when a player joins a @{CLIENT}. +-- @param string MissionCoalition is a string indicating the coalition or party to which this mission belongs to. It is free format and can be chosen freely by the mission designer. Note that this field is not to be confused with the coalition concept of the ME. Examples of a Mission Coalition could be "NATO", "CCCP", "Intruders", "Terrorists"... +-- @return MISSION +-- @usage +-- -- Declare a few missions. +-- local Mission = MISSIONSCHEDULER.AddMission( 'Russia Transport Troops SA-6', 'Operational', 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', 'Russia' ) +-- local Mission = MISSIONSCHEDULER.AddMission( 'Patriots', 'Primary', 'Our intelligence reports that 3 Patriot SAM defense batteries are located near Ruisi, Kvarhiti and Gori.', 'Russia' ) +-- local Mission = MISSIONSCHEDULER.AddMission( 'Package Delivery', 'Operational', 'In order to be in full control of the situation, we need you to deliver a very important package at a secret location. Fly undetected through the NATO defenses and deliver the secret package. The secret agent is located at waypoint 4.', 'Russia' ) +-- local Mission = MISSIONSCHEDULER.AddMission( 'Rescue General', 'Tactical', 'Our intelligence has received a remote signal behind Gori. We believe it is a very important Russian General that was captured by Georgia. Go out there and rescue him! Ensure you stay out of the battle zone, keep south. Waypoint 4 is the location of our Russian General.', 'Russia' ) +-- local Mission = MISSIONSCHEDULER.AddMission( 'NATO Transport Troops', 'Operational', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.', 'NATO' ) +-- local Mission = MISSIONSCHEDULER.AddMission( 'SA-6 SAMs', 'Primary', 'Our intelligence reports that 3 SA-6 SAM defense batteries are located near Didmukha, Khetagurov and Berula. Eliminate the Russian SAMs.', 'NATO' ) +-- local Mission = MISSIONSCHEDULER.AddMission( 'NATO Sling Load', 'Operational', 'Fly to the cargo pickup zone at Dzegvi or Kaspi, and sling the cargo to Soganlug airbase.', 'NATO' ) +-- local Mission = MISSIONSCHEDULER.AddMission( 'Rescue secret agent', 'Tactical', 'In order to be in full control of the situation, we need you to rescue a secret agent from the woods behind enemy lines. Avoid the Russian defenses and rescue the agent. Keep south until Khasuri, and keep your eyes open for any SAM presence. The agent is located at waypoint 4 on your kneeboard.', 'NATO' ) +function MISSION:New( MissionName, MissionPriority, MissionBriefing, MissionCoalition ) + + self = MISSION:Meta() + self:T({ MissionName, MissionPriority, MissionBriefing, MissionCoalition }) + + local Valid = true + + Valid = routines.ValidateString( MissionName, "MissionName", Valid ) + Valid = routines.ValidateString( MissionPriority, "MissionPriority", Valid ) + Valid = routines.ValidateString( MissionBriefing, "MissionBriefing", Valid ) + Valid = routines.ValidateString( MissionCoalition, "MissionCoalition", Valid ) + + if Valid then + self.Name = MissionName + self.MissionPriority = MissionPriority + self.MissionBriefing = MissionBriefing + self.MissionCoalition = MissionCoalition + end + + return self +end + +--- Returns if a Mission has completed. +-- @return bool +function MISSION:IsCompleted() + self:F() + return self.MissionStatus == "ACCOMPLISHED" +end + +--- Set a Mission to completed. +function MISSION:Completed() + self:F() + self.MissionStatus = "ACCOMPLISHED" + self:StatusToClients() +end + +--- Returns if a Mission is ongoing. +-- treturn bool +function MISSION:IsOngoing() + self:F() + return self.MissionStatus == "ONGOING" +end + +--- Set a Mission to ongoing. +function MISSION:Ongoing() + self:F() + self.MissionStatus = "ONGOING" + --self:StatusToClients() +end + +--- Returns if a Mission is pending. +-- treturn bool +function MISSION:IsPending() + self:F() + return self.MissionStatus == "PENDING" +end + +--- Set a Mission to pending. +function MISSION:Pending() + self:F() + self.MissionStatus = "PENDING" + self:StatusToClients() +end + +--- Returns if a Mission has failed. +-- treturn bool +function MISSION:IsFailed() + self:F() + return self.MissionStatus == "FAILED" +end + +--- Set a Mission to failed. +function MISSION:Failed() + self:F() + self.MissionStatus = "FAILED" + self:StatusToClients() +end + +--- Send the status of the MISSION to all Clients. +function MISSION:StatusToClients() + self:F() + if self.MissionReportFlash then + for ClientID, Client in pairs( self._Clients ) do + Client:Message( self.MissionCoalition .. ' "' .. self.Name .. '": ' .. self.MissionStatus .. '! ( ' .. self.MissionPriority .. ' mission ) ', 10, self.Name .. '/Status', "Mission Command: Mission Status") + end + end +end + +--- Handles the reporting. After certain time intervals, a MISSION report MESSAGE will be shown to All Players. +function MISSION:ReportTrigger() + self:F() + + if self.MissionReportShow == true then + self.MissionReportShow = false + return true + else + if self.MissionReportFlash == true then + if timer.getTime() >= self.MissionReportTrigger then + self.MissionReportTrigger = timer.getTime() + self.MissionTimeInterval + return true + else + return false + end + else + return false + end + end +end + +--- Report the status of all MISSIONs to all active Clients. +function MISSION:ReportToAll() + self:F() + + local AlivePlayers = '' + for ClientID, Client in pairs( self._Clients ) do + if Client:GetDCSGroup() then + if Client:GetClientGroupDCSUnit() then + if Client:GetClientGroupDCSUnit():getLife() > 0.0 then + if AlivePlayers == '' then + AlivePlayers = ' Players: ' .. Client:GetClientGroupDCSUnit():getPlayerName() + else + AlivePlayers = AlivePlayers .. ' / ' .. Client:GetClientGroupDCSUnit():getPlayerName() + end + end + end + end + end + local Tasks = self:GetTasks() + local TaskText = "" + for TaskID, TaskData in pairs( Tasks ) do + TaskText = TaskText .. " - Task " .. TaskID .. ": " .. TaskData.Name .. ": " .. TaskData:GetGoalProgress() .. "\n" + end + MESSAGE:New( self.MissionCoalition .. ' "' .. self.Name .. '": ' .. self.MissionStatus .. ' ( ' .. self.MissionPriority .. ' mission )' .. AlivePlayers .. "\n" .. TaskText:gsub("\n$",""), "Mission Command: Mission Report", 10, self.Name .. '/Status'):ToAll() +end + + +--- Add a goal function to a MISSION. Goal functions are called when a @{TASK} within a mission has been completed. +-- @param function GoalFunction is the function defined by the mission designer to evaluate whether a certain goal has been reached after a @{TASK} finishes within the @{MISSION}. A GoalFunction must accept 2 parameters: Mission, Client, which contains the current MISSION object and the current CLIENT object respectively. +-- @usage +-- PatriotActivation = { +-- { "US SAM Patriot Zerti", false }, +-- { "US SAM Patriot Zegduleti", false }, +-- { "US SAM Patriot Gvleti", false } +-- } +-- +-- function DeployPatriotTroopsGoal( Mission, Client ) +-- +-- +-- -- Check if the cargo is all deployed for mission success. +-- for CargoID, CargoData in pairs( Mission._Cargos ) do +-- if Group.getByName( CargoData.CargoGroupName ) then +-- CargoGroup = Group.getByName( CargoData.CargoGroupName ) +-- if CargoGroup then +-- -- Check if the cargo is ready to activate +-- CurrentLandingZoneID = routines.IsUnitInZones( CargoGroup:getUnits()[1], Mission:GetTask( 2 ).LandingZones ) -- The second task is the Deploytask to measure mission success upon +-- if CurrentLandingZoneID then +-- if PatriotActivation[CurrentLandingZoneID][2] == false then +-- -- Now check if this is a new Mission Task to be completed... +-- trigger.action.setGroupAIOn( Group.getByName( PatriotActivation[CurrentLandingZoneID][1] ) ) +-- PatriotActivation[CurrentLandingZoneID][2] = true +-- MessageToBlue( "Mission Command: Message to all airborne units! The " .. PatriotActivation[CurrentLandingZoneID][1] .. " is armed. Our air defenses are now stronger.", 60, "BLUE/PatriotDefense" ) +-- MessageToRed( "Mission Command: Our satellite systems are detecting additional NATO air defenses. To all airborne units: Take care!!!", 60, "RED/PatriotDefense" ) +-- Mission:GetTask( 2 ):AddGoalCompletion( "Patriots activated", PatriotActivation[CurrentLandingZoneID][1], 1 ) -- Register Patriot activation as part of mission goal. +-- end +-- end +-- end +-- end +-- end +-- end +-- +-- local Mission = MISSIONSCHEDULER.AddMission( 'NATO Transport Troops', 'Operational', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.', 'NATO' ) +-- Mission:AddGoalFunction( DeployPatriotTroopsGoal ) +function MISSION:AddGoalFunction( GoalFunction ) + self:F() + self.GoalFunction = GoalFunction +end + +--- Register a new @{CLIENT} to participate within the mission. +-- @param CLIENT Client is the @{CLIENT} object. The object must have been instantiated with @{CLIENT:New}. +-- @return CLIENT +-- @usage +-- Add a number of Client objects to the Mission. +-- Mission:AddClient( CLIENT:FindByName( 'US UH-1H*HOT-Deploy Troops 1', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.' ):Transport() ) +-- Mission:AddClient( CLIENT:FindByName( 'US UH-1H*RAMP-Deploy Troops 3', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.' ):Transport() ) +-- Mission:AddClient( CLIENT:FindByName( 'US UH-1H*HOT-Deploy Troops 2', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.' ):Transport() ) +-- Mission:AddClient( CLIENT:FindByName( 'US UH-1H*RAMP-Deploy Troops 4', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.' ):Transport() ) +function MISSION:AddClient( Client ) + self:F( { Client } ) + + local Valid = true + + if Valid then + self._Clients[Client.ClientName] = Client + end + + return Client +end + +--- Find a @{CLIENT} object within the @{MISSION} by its ClientName. +-- @param CLIENT ClientName is a string defining the Client Group as defined within the ME. +-- @return CLIENT +-- @usage +-- -- Seach for Client "Bomber" within the Mission. +-- local BomberClient = Mission:FindClient( "Bomber" ) +function MISSION:FindClient( ClientName ) + self:F( { self._Clients[ClientName] } ) + return self._Clients[ClientName] +end + + +--- Register a @{TASK} to be completed within the @{MISSION}. Note that there can be multiple @{TASK}s registered to be completed. Each TASK can be set a certain Goal. The MISSION will not be completed until all Goals are reached. +-- @param TASK Task is the @{TASK} object. The object must have been instantiated with @{TASK:New} or any of its inherited @{TASK}s. +-- @param number TaskNumber is the sequence number of the TASK within the MISSION. This number does have to be chronological. +-- @return TASK +-- @usage +-- -- Define a few tasks for the Mission. +-- PickupZones = { "NATO Gold Pickup Zone", "NATO Titan Pickup Zone" } +-- PickupSignalUnits = { "NATO Gold Coordination Center", "NATO Titan Coordination Center" } +-- +-- -- Assign the Pickup Task +-- local PickupTask = PICKUPTASK:New( PickupZones, CARGO_TYPE.ENGINEERS, CLIENT.ONBOARDSIDE.LEFT ) +-- PickupTask:AddSmokeBlue( PickupSignalUnits ) +-- PickupTask:SetGoalTotal( 3 ) +-- Mission:AddTask( PickupTask, 1 ) +-- +-- -- Assign the Deploy Task +-- local PatriotActivationZones = { "US Patriot Battery 1 Activation", "US Patriot Battery 2 Activation", "US Patriot Battery 3 Activation" } +-- local PatriotActivationZonesSmokeUnits = { "US SAM Patriot - Battery 1 Control", "US SAM Patriot - Battery 2 Control", "US SAM Patriot - Battery 3 Control" } +-- local DeployTask = DEPLOYTASK:New( PatriotActivationZones, CARGO_TYPE.ENGINEERS ) +-- --DeployTask:SetCargoTargetZoneName( 'US Troops Attack ' .. math.random(2) ) +-- DeployTask:AddSmokeBlue( PatriotActivationZonesSmokeUnits ) +-- DeployTask:SetGoalTotal( 3 ) +-- DeployTask:SetGoalTotal( 3, "Patriots activated" ) +-- Mission:AddTask( DeployTask, 2 ) + +function MISSION:AddTask( Task, TaskNumber ) + self:F() + + self._Tasks[TaskNumber] = Task + self._Tasks[TaskNumber]:EnableEvents() + self._Tasks[TaskNumber].ID = TaskNumber + + return Task + end + +--- Get the TASK idenified by the TaskNumber from the Mission. This function is useful in GoalFunctions. +-- @param number TaskNumber is the number of the @{TASK} within the @{MISSION}. +-- @return TASK +-- @usage +-- -- Get Task 2 from the Mission. +-- Task2 = Mission:GetTask( 2 ) + +function MISSION:GetTask( TaskNumber ) + self:F() + + local Valid = true + + local Task = nil + + if type(TaskNumber) ~= "number" then + Valid = false + end + + if Valid then + Task = self._Tasks[TaskNumber] + end + + return Task +end + +--- Get all the TASKs from the Mission. This function is useful in GoalFunctions. +-- @return {TASK,...} Structure of TASKS with the @{TASK} number as the key. +-- @usage +-- -- Get Tasks from the Mission. +-- Tasks = Mission:GetTasks() +-- env.info( "Task 2 Completion = " .. Tasks[2]:GetGoalPercentage() .. "%" ) +function MISSION:GetTasks() + self:F() + + return self._Tasks +end + + +--[[ + _TransportExecuteStage: Defines the different stages of Transport unload/load execution. This table is internal and is used to control the validity of Transport load/unload timing. + + - _TransportExecuteStage.EXECUTING + - _TransportExecuteStage.SUCCESS + - _TransportExecuteStage.FAILED + +--]] +_TransportExecuteStage = { + NONE = 0, + EXECUTING = 1, + SUCCESS = 2, + FAILED = 3 +} + + +--- The MISSIONSCHEDULER is an OBJECT and is the main scheduler of ALL active MISSIONs registered within this scheduler. It's workings are considered internal and is automatically created when the Mission.lua file is included. +-- @type MISSIONSCHEDULER +-- @field #MISSIONSCHEDULER.MISSIONS Missions +MISSIONSCHEDULER = { + Missions = {}, + MissionCount = 0, + TimeIntervalCount = 0, + TimeIntervalShow = 150, + TimeSeconds = 14400, + TimeShow = 5 +} + +--- @type MISSIONSCHEDULER.MISSIONS +-- @list <#MISSION> Mission + +--- This is the main MISSIONSCHEDULER Scheduler function. It is considered internal and is automatically created when the Mission.lua file is included. +function MISSIONSCHEDULER.Scheduler() + + + -- loop through the missions in the TransportTasks + for MissionName, MissionData in pairs( MISSIONSCHEDULER.Missions ) do + + local Mission = MissionData -- #MISSION + + if not Mission:IsCompleted() then + + -- This flag will monitor if for this mission, there are clients alive. If this flag is still false at the end of the loop, the mission status will be set to Pending (if not Failed or Completed). + local ClientsAlive = false + + for ClientID, ClientData in pairs( Mission._Clients ) do + + local Client = ClientData -- Client#CLIENT + + if Client:IsAlive() then + + -- There is at least one Client that is alive... So the Mission status is set to Ongoing. + ClientsAlive = true + + -- If this Client was not registered as Alive before: + -- 1. We register the Client as Alive. + -- 2. We initialize the Client Tasks and make a link to the original Mission Task. + -- 3. We initialize the Cargos. + -- 4. We flag the Mission as Ongoing. + if not Client.ClientAlive then + Client.ClientAlive = true + Client.ClientBriefingShown = false + for TaskNumber, Task in pairs( Mission._Tasks ) do + -- Note that this a deepCopy. Each client must have their own Tasks with own Stages!!! + Client._Tasks[TaskNumber] = routines.utils.deepCopy( Mission._Tasks[TaskNumber] ) + -- Each MissionTask must point to the original Mission. + Client._Tasks[TaskNumber].MissionTask = Mission._Tasks[TaskNumber] + Client._Tasks[TaskNumber].Cargos = Mission._Tasks[TaskNumber].Cargos + Client._Tasks[TaskNumber].LandingZones = Mission._Tasks[TaskNumber].LandingZones + end + + Mission:Ongoing() + end + + + -- For each Client, check for each Task the state and evolve the mission. + -- This flag will indicate if the Task of the Client is Complete. + local TaskComplete = false + + for TaskNumber, Task in pairs( Client._Tasks ) do + + if not Task.Stage then + Task:SetStage( 1 ) + end + + + local TransportTime = timer.getTime() + + if not Task:IsDone() then + + if Task:Goal() then + Task:ShowGoalProgress( Mission, Client ) + end + + --env.info( 'Scheduler: Mission = ' .. Mission.Name .. ' / Client = ' .. Client.ClientName .. ' / Task = ' .. Task.Name .. ' / Stage = ' .. Task.ActiveStage .. ' - ' .. Task.Stage.Name .. ' - ' .. Task.Stage.StageType ) + + -- Action + if Task:StageExecute() then + Task.Stage:Execute( Mission, Client, Task ) + end + + -- Wait until execution is finished + if Task.ExecuteStage == _TransportExecuteStage.EXECUTING then + Task.Stage:Executing( Mission, Client, Task ) + end + + -- Validate completion or reverse to earlier stage + if Task.Time + Task.Stage.WaitTime <= TransportTime then + Task:SetStage( Task.Stage:Validate( Mission, Client, Task ) ) + end + + if Task:IsDone() then + --env.info( 'Scheduler: Mission '.. Mission.Name .. ' Task ' .. Task.Name .. ' Stage ' .. Task.Stage.Name .. ' done. TaskComplete = ' .. string.format ( "%s", TaskComplete and "true" or "false" ) ) + TaskComplete = true -- when a task is not yet completed, a mission cannot be completed + + else + -- break only if this task is not yet done, so that future task are not yet activated. + TaskComplete = false -- when a task is not yet completed, a mission cannot be completed + --env.info( 'Scheduler: Mission "'.. Mission.Name .. '" Task "' .. Task.Name .. '" Stage "' .. Task.Stage.Name .. '" break. TaskComplete = ' .. string.format ( "%s", TaskComplete and "true" or "false" ) ) + break + end + + if TaskComplete then + + if Mission.GoalFunction ~= nil then + Mission.GoalFunction( Mission, Client ) + end + if MISSIONSCHEDULER.Scoring then + MISSIONSCHEDULER.Scoring:_AddMissionTaskScore( Client:GetClientGroupDCSUnit(), Mission.Name, 25 ) + end + +-- if not Mission:IsCompleted() then +-- end + end + end + end + + local MissionComplete = true + for TaskNumber, Task in pairs( Mission._Tasks ) do + if Task:Goal() then +-- Task:ShowGoalProgress( Mission, Client ) + if Task:IsGoalReached() then + else + MissionComplete = false + end + else + MissionComplete = false -- If there is no goal, the mission should never be ended. The goal status will be set somewhere else. + end + end + + if MissionComplete then + Mission:Completed() + if MISSIONSCHEDULER.Scoring then + MISSIONSCHEDULER.Scoring:_AddMissionScore( Mission.Name, 100 ) + end + else + if TaskComplete then + -- Reset for new tasking of active client + Client.ClientAlive = false -- Reset the client tasks. + end + end + + + else + if Client.ClientAlive then + env.info( 'Scheduler: Client "' .. Client.ClientName .. '" is inactive.' ) + Client.ClientAlive = false + + -- This is tricky. If we sanitize Client._Tasks before sanitizing Client._Tasks[TaskNumber].MissionTask, then the original MissionTask will be sanitized, and will be lost within the garbage collector. + -- So first sanitize Client._Tasks[TaskNumber].MissionTask, after that, sanitize only the whole _Tasks structure... + --Client._Tasks[TaskNumber].MissionTask = nil + --Client._Tasks = nil + end + end + end + + -- If all Clients of this Mission are not activated, then the Mission status needs to be put back into Pending status. + -- But only if the Mission was Ongoing. In case the Mission is Completed or Failed, the Mission status may not be changed. In these cases, this will be the last run of this Mission in the Scheduler. + if ClientsAlive == false then + if Mission:IsOngoing() then + -- Mission status back to pending... + Mission:Pending() + end + end + end + + Mission:StatusToClients() + + if Mission:ReportTrigger() then + Mission:ReportToAll() + end + end + + return true +end + +--- Start the MISSIONSCHEDULER. +function MISSIONSCHEDULER.Start() + if MISSIONSCHEDULER ~= nil then + --MISSIONSCHEDULER.SchedulerId = routines.scheduleFunction( MISSIONSCHEDULER.Scheduler, { }, 0, 2 ) + MISSIONSCHEDULER.SchedulerId = SCHEDULER:New( nil, MISSIONSCHEDULER.Scheduler, { }, 0, 2 ) + end +end + +--- Stop the MISSIONSCHEDULER. +function MISSIONSCHEDULER.Stop() + if MISSIONSCHEDULER.SchedulerId then + routines.removeFunction(MISSIONSCHEDULER.SchedulerId) + MISSIONSCHEDULER.SchedulerId = nil + end +end + +--- This is the main MISSION declaration method. Each Mission is like the master or a Mission orchestration between, Clients, Tasks, Stages etc. +-- @param Mission is the MISSION object instantiated by @{MISSION:New}. +-- @return MISSION +-- @usage +-- -- Declare a mission. +-- Mission = MISSION:New( 'Russia Transport Troops SA-6', +-- 'Operational', +-- 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', +-- 'Russia' ) +-- MISSIONSCHEDULER:AddMission( Mission ) +function MISSIONSCHEDULER.AddMission( Mission ) + MISSIONSCHEDULER.Missions[Mission.Name] = Mission + MISSIONSCHEDULER.MissionCount = MISSIONSCHEDULER.MissionCount + 1 + -- Add an overall AI Client for the AI tasks... This AI Client will facilitate the Events in the background for each Task. + --MissionAdd:AddClient( CLIENT:Register( 'AI' ) ) + + return Mission +end + +--- Remove a MISSION from the MISSIONSCHEDULER. +-- @param MissionName is the name of the MISSION given at declaration using @{AddMission}. +-- @usage +-- -- Declare a mission. +-- Mission = MISSION:New( 'Russia Transport Troops SA-6', +-- 'Operational', +-- 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', +-- 'Russia' ) +-- MISSIONSCHEDULER:AddMission( Mission ) +-- +-- -- Now remove the Mission. +-- MISSIONSCHEDULER:RemoveMission( 'Russia Transport Troops SA-6' ) +function MISSIONSCHEDULER.RemoveMission( MissionName ) + MISSIONSCHEDULER.Missions[MissionName] = nil + MISSIONSCHEDULER.MissionCount = MISSIONSCHEDULER.MissionCount - 1 +end + +--- Find a MISSION within the MISSIONSCHEDULER. +-- @param MissionName is the name of the MISSION given at declaration using @{AddMission}. +-- @return MISSION +-- @usage +-- -- Declare a mission. +-- Mission = MISSION:New( 'Russia Transport Troops SA-6', +-- 'Operational', +-- 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', +-- 'Russia' ) +-- MISSIONSCHEDULER:AddMission( Mission ) +-- +-- -- Now find the Mission. +-- MissionFind = MISSIONSCHEDULER:FindMission( 'Russia Transport Troops SA-6' ) +function MISSIONSCHEDULER.FindMission( MissionName ) + return MISSIONSCHEDULER.Missions[MissionName] +end + +-- Internal function used by the MISSIONSCHEDULER menu. +function MISSIONSCHEDULER.ReportMissionsShow( ) + for MissionName, Mission in pairs( MISSIONSCHEDULER.Missions ) do + Mission.MissionReportShow = true + Mission.MissionReportFlash = false + end +end + +-- Internal function used by the MISSIONSCHEDULER menu. +function MISSIONSCHEDULER.ReportMissionsFlash( TimeInterval ) + local Count = 0 + for MissionName, Mission in pairs( MISSIONSCHEDULER.Missions ) do + Mission.MissionReportShow = false + Mission.MissionReportFlash = true + Mission.MissionReportTrigger = timer.getTime() + Count * TimeInterval + Mission.MissionTimeInterval = MISSIONSCHEDULER.MissionCount * TimeInterval + env.info( "TimeInterval = " .. Mission.MissionTimeInterval ) + Count = Count + 1 + end +end + +-- Internal function used by the MISSIONSCHEDULER menu. +function MISSIONSCHEDULER.ReportMissionsHide( Prm ) + for MissionName, Mission in pairs( MISSIONSCHEDULER.Missions ) do + Mission.MissionReportShow = false + Mission.MissionReportFlash = false + end +end + +--- Enables a MENU option in the communications menu under F10 to control the status of the active missions. +-- This function should be called only once when starting the MISSIONSCHEDULER. +function MISSIONSCHEDULER.ReportMenu() + local ReportMenu = SUBMENU:New( 'Status' ) + local ReportMenuShow = COMMANDMENU:New( 'Show Report Missions', ReportMenu, MISSIONSCHEDULER.ReportMissionsShow, 0 ) + local ReportMenuFlash = COMMANDMENU:New('Flash Report Missions', ReportMenu, MISSIONSCHEDULER.ReportMissionsFlash, 120 ) + local ReportMenuHide = COMMANDMENU:New( 'Hide Report Missions', ReportMenu, MISSIONSCHEDULER.ReportMissionsHide, 0 ) +end + +--- Show the remaining mission time. +function MISSIONSCHEDULER:TimeShow() + self.TimeIntervalCount = self.TimeIntervalCount + 1 + if self.TimeIntervalCount >= self.TimeTriggerShow then + local TimeMsg = string.format("%00d", ( self.TimeSeconds / 60 ) - ( timer.getTime() / 60 )) .. ' minutes left until mission reload.' + MESSAGE:New( TimeMsg, "Mission time", self.TimeShow, '/TimeMsg' ):ToAll() + self.TimeIntervalCount = 0 + end +end + +function MISSIONSCHEDULER:Time( TimeSeconds, TimeIntervalShow, TimeShow ) + + self.TimeIntervalCount = 0 + self.TimeSeconds = TimeSeconds + self.TimeIntervalShow = TimeIntervalShow + self.TimeShow = TimeShow +end + +--- Adds a mission scoring to the game. +function MISSIONSCHEDULER:Scoring( Scoring ) + + self.Scoring = Scoring +end + +--- The CLEANUP class keeps an area clean of crashing or colliding airplanes. It also prevents airplanes from firing within this area. +-- @module CleanUp +-- @author Flightcontrol + +Include.File( "Routines" ) +Include.File( "Base" ) +Include.File( "Mission" ) +Include.File( "Client" ) +Include.File( "Task" ) + +--- The CLEANUP class. +-- @type CLEANUP +-- @extends Base#BASE +CLEANUP = { + ClassName = "CLEANUP", + ZoneNames = {}, + TimeInterval = 300, + CleanUpList = {}, +} + +--- Creates the main object which is handling the cleaning of the debris within the given Zone Names. +-- @param #CLEANUP self +-- @param #table ZoneNames Is a table of zone names where the debris should be cleaned. Also a single string can be passed with one zone name. +-- @param #number TimeInterval The interval in seconds when the clean activity takes place. The default is 300 seconds, thus every 5 minutes. +-- @return #CLEANUP +-- @usage +-- -- Clean these Zones. +-- CleanUpAirports = CLEANUP:New( { 'CLEAN Tbilisi', 'CLEAN Kutaisi' }, 150 ) +-- or +-- CleanUpTbilisi = CLEANUP:New( 'CLEAN Tbilisi', 150 ) +-- CleanUpKutaisi = CLEANUP:New( 'CLEAN Kutaisi', 600 ) +function CLEANUP:New( ZoneNames, TimeInterval ) local self = BASE:Inherit( self, BASE:New() ) + self:F( { ZoneNames, TimeInterval } ) + + if type( ZoneNames ) == 'table' then + self.ZoneNames = ZoneNames + else + self.ZoneNames = { ZoneNames } + end + if TimeInterval then + self.TimeInterval = TimeInterval + end + + _EVENTDISPATCHER:OnBirth( self._OnEventBirth, self ) + + --self.CleanUpScheduler = routines.scheduleFunction( self._CleanUpScheduler, { self }, timer.getTime() + 1, TimeInterval ) + self.CleanUpScheduler = SCHEDULER:New( self, self._CleanUpScheduler, {}, 1, TimeInterval ) + + return self +end + + +--- Destroys a group from the simulator, but checks first if it is still existing! +-- @param #CLEANUP self +-- @param DCSGroup#Group GroupObject The object to be destroyed. +-- @param #string CleanUpGroupName The groupname... +function CLEANUP:_DestroyGroup( GroupObject, CleanUpGroupName ) + self:F( { GroupObject, CleanUpGroupName } ) + + if GroupObject then -- and GroupObject:isExist() then + --MESSAGE:New( "Destroy Group " .. CleanUpGroupName, CleanUpGroupName, 1, CleanUpGroupName ):ToAll() + trigger.action.deactivateGroup(GroupObject) + self:T( { "GroupObject Destroyed", GroupObject } ) + end +end + +--- Destroys a @{DCSUnit#Unit} from the simulator, but checks first if it is still existing! +-- @param #CLEANUP self +-- @param DCSUnit#Unit CleanUpUnit The object to be destroyed. +-- @param #string CleanUpUnitName The Unit name ... +function CLEANUP:_DestroyUnit( CleanUpUnit, CleanUpUnitName ) + self:F( { CleanUpUnit, CleanUpUnitName } ) + + if CleanUpUnit then + --MESSAGE:New( "Destroy " .. CleanUpUnitName, CleanUpUnitName, 1, CleanUpUnitName ):ToAll() + local CleanUpGroup = Unit.getGroup(CleanUpUnit) + -- TODO Client bug in 1.5.3 + if CleanUpGroup and CleanUpGroup:isExist() then + local CleanUpGroupUnits = CleanUpGroup:getUnits() + if #CleanUpGroupUnits == 1 then + local CleanUpGroupName = CleanUpGroup:getName() + --self:CreateEventCrash( timer.getTime(), CleanUpUnit ) + CleanUpGroup:destroy() + self:T( { "Destroyed Group:", CleanUpGroupName } ) + else + CleanUpUnit:destroy() + self:T( { "Destroyed Unit:", CleanUpUnitName } ) + end + self.CleanUpList[CleanUpUnitName] = nil -- Cleaning from the list + CleanUpUnit = nil + end + end +end + +-- TODO check DCSTypes#Weapon +--- Destroys a missile from the simulator, but checks first if it is still existing! +-- @param #CLEANUP self +-- @param DCSTypes#Weapon MissileObject +function CLEANUP:_DestroyMissile( MissileObject ) + self:F( { MissileObject } ) + + if MissileObject and MissileObject:isExist() then + MissileObject:destroy() + self:T( "MissileObject Destroyed") + end +end + +function CLEANUP:_OnEventBirth( Event ) + self:F( { Event } ) + + self.CleanUpList[Event.IniDCSUnitName] = {} + self.CleanUpList[Event.IniDCSUnitName].CleanUpUnit = Event.IniDCSUnit + self.CleanUpList[Event.IniDCSUnitName].CleanUpGroup = Event.IniDCSGroup + self.CleanUpList[Event.IniDCSUnitName].CleanUpGroupName = Event.IniDCSGroupName + self.CleanUpList[Event.IniDCSUnitName].CleanUpUnitName = Event.IniDCSUnitName + + _EVENTDISPATCHER:OnEngineShutDownForUnit( Event.IniDCSUnitName, self._EventAddForCleanUp, self ) + _EVENTDISPATCHER:OnEngineStartUpForUnit( Event.IniDCSUnitName, self._EventAddForCleanUp, self ) + _EVENTDISPATCHER:OnHitForUnit( Event.IniDCSUnitName, self._EventAddForCleanUp, self ) + _EVENTDISPATCHER:OnPilotDeadForUnit( Event.IniDCSUnitName, self._EventCrash, self ) + _EVENTDISPATCHER:OnDeadForUnit( Event.IniDCSUnitName, self._EventCrash, self ) + _EVENTDISPATCHER:OnCrashForUnit( Event.IniDCSUnitName, self._EventCrash, self ) + _EVENTDISPATCHER:OnShotForUnit( Event.IniDCSUnitName, self._EventShot, self ) + + --self:AddEvent( world.event.S_EVENT_ENGINE_SHUTDOWN, self._EventAddForCleanUp ) + --self:AddEvent( world.event.S_EVENT_ENGINE_STARTUP, self._EventAddForCleanUp ) +-- self:AddEvent( world.event.S_EVENT_HIT, self._EventAddForCleanUp ) -- , self._EventHitCleanUp ) +-- self:AddEvent( world.event.S_EVENT_CRASH, self._EventCrash ) -- , self._EventHitCleanUp ) +-- --self:AddEvent( world.event.S_EVENT_DEAD, self._EventCrash ) +-- self:AddEvent( world.event.S_EVENT_SHOT, self._EventShot ) +-- +-- self:EnableEvents() + + +end + +--- Detects if a crash event occurs. +-- Crashed units go into a CleanUpList for removal. +-- @param #CLEANUP self +-- @param DCSTypes#Event event +function CLEANUP:_EventCrash( Event ) + self:F( { Event } ) + + --TODO: This stuff is not working due to a DCS bug. Burning units cannot be destroyed. + --MESSAGE:New( "Crash ", "Crash", 10, "Crash" ):ToAll() + -- self:T("before getGroup") + -- local _grp = Unit.getGroup(event.initiator)-- Identify the group that fired + -- self:T("after getGroup") + -- _grp:destroy() + -- self:T("after deactivateGroup") + -- event.initiator:destroy() + + self.CleanUpList[Event.IniDCSUnitName] = {} + self.CleanUpList[Event.IniDCSUnitName].CleanUpUnit = Event.IniDCSUnit + self.CleanUpList[Event.IniDCSUnitName].CleanUpGroup = Event.IniDCSGroup + self.CleanUpList[Event.IniDCSUnitName].CleanUpGroupName = Event.IniDCSGroupName + self.CleanUpList[Event.IniDCSUnitName].CleanUpUnitName = Event.IniDCSUnitName + +end + +--- Detects if a unit shoots a missile. +-- If this occurs within one of the zones, then the weapon used must be destroyed. +-- @param #CLEANUP self +-- @param DCSTypes#Event event +function CLEANUP:_EventShot( Event ) + self:F( { Event } ) + + -- Test if the missile was fired within one of the CLEANUP.ZoneNames. + local CurrentLandingZoneID = 0 + CurrentLandingZoneID = routines.IsUnitInZones( Event.IniDCSUnit, self.ZoneNames ) + if ( CurrentLandingZoneID ) then + -- Okay, the missile was fired within the CLEANUP.ZoneNames, destroy the fired weapon. + --_SEADmissile:destroy() + --routines.scheduleFunction( CLEANUP._DestroyMissile, { self, Event.Weapon }, timer.getTime() + 0.1) + SCHEDULER:New( self, CLEANUP._DestroyMissile, { Event.Weapon }, 0.1 ) + end +end + + +--- Detects if the Unit has an S_EVENT_HIT within the given ZoneNames. If this is the case, destroy the unit. +-- @param #CLEANUP self +-- @param DCSTypes#Event event +function CLEANUP:_EventHitCleanUp( Event ) + self:F( { Event } ) + + if Event.IniDCSUnit then + if routines.IsUnitInZones( Event.IniDCSUnit, self.ZoneNames ) ~= nil then + self:T( { "Life: ", Event.IniDCSUnitName, ' = ', Event.IniDCSUnit:getLife(), "/", Event.IniDCSUnit:getLife0() } ) + if Event.IniDCSUnit:getLife() < Event.IniDCSUnit:getLife0() then + self:T( "CleanUp: Destroy: " .. Event.IniDCSUnitName ) + --routines.scheduleFunction( CLEANUP._DestroyUnit, { self, Event.IniDCSUnit }, timer.getTime() + 0.1) + SCHEDULER:New( self, CLEANUP._DestroyUnit, { Event.IniDCSUnit }, 0.1 ) + end + end + end + + if Event.TgtDCSUnit then + if routines.IsUnitInZones( Event.TgtDCSUnit, self.ZoneNames ) ~= nil then + self:T( { "Life: ", Event.TgtDCSUnitName, ' = ', Event.TgtDCSUnit:getLife(), "/", Event.TgtDCSUnit:getLife0() } ) + if Event.TgtDCSUnit:getLife() < Event.TgtDCSUnit:getLife0() then + self:T( "CleanUp: Destroy: " .. Event.TgtDCSUnitName ) + --routines.scheduleFunction( CLEANUP._DestroyUnit, { self, Event.TgtDCSUnit }, timer.getTime() + 0.1 ) + SCHEDULER:New( self, CLEANUP._DestroyUnit, { Event.TgtDCSUnit }, 0.1 ) + end + end + end +end + +--- Add the @{DCSUnit#Unit} to the CleanUpList for CleanUp. +function CLEANUP:_AddForCleanUp( CleanUpUnit, CleanUpUnitName ) + self:F( { CleanUpUnit, CleanUpUnitName } ) + + self.CleanUpList[CleanUpUnitName] = {} + self.CleanUpList[CleanUpUnitName].CleanUpUnit = CleanUpUnit + self.CleanUpList[CleanUpUnitName].CleanUpUnitName = CleanUpUnitName + self.CleanUpList[CleanUpUnitName].CleanUpGroup = Unit.getGroup(CleanUpUnit) + self.CleanUpList[CleanUpUnitName].CleanUpGroupName = Unit.getGroup(CleanUpUnit):getName() + self.CleanUpList[CleanUpUnitName].CleanUpTime = timer.getTime() + self.CleanUpList[CleanUpUnitName].CleanUpMoved = false + + self:T( { "CleanUp: Add to CleanUpList: ", Unit.getGroup(CleanUpUnit):getName(), CleanUpUnitName } ) + +end + +--- Detects if the Unit has an S_EVENT_ENGINE_SHUTDOWN or an S_EVENT_HIT within the given ZoneNames. If this is the case, add the Group to the CLEANUP List. +-- @param #CLEANUP self +-- @param DCSTypes#Event event +function CLEANUP:_EventAddForCleanUp( Event ) + + if Event.IniDCSUnit then + if self.CleanUpList[Event.IniDCSUnitName] == nil then + if routines.IsUnitInZones( Event.IniDCSUnit, self.ZoneNames ) ~= nil then + self:_AddForCleanUp( Event.IniDCSUnit, Event.IniDCSUnitName ) + end + end + end + + if Event.TgtDCSUnit then + if self.CleanUpList[Event.TgtDCSUnitName] == nil then + if routines.IsUnitInZones( Event.TgtDCSUnit, self.ZoneNames ) ~= nil then + self:_AddForCleanUp( Event.TgtDCSUnit, Event.TgtDCSUnitName ) + end + end + end + +end + +local CleanUpSurfaceTypeText = { + "LAND", + "SHALLOW_WATER", + "WATER", + "ROAD", + "RUNWAY" + } + +--- At the defined time interval, CleanUp the Groups within the CleanUpList. +-- @param #CLEANUP self +function CLEANUP:_CleanUpScheduler() + self:F( { "CleanUp Scheduler" } ) + + local CleanUpCount = 0 + for CleanUpUnitName, UnitData in pairs( self.CleanUpList ) do + CleanUpCount = CleanUpCount + 1 + + self:T( { CleanUpUnitName, UnitData } ) + local CleanUpUnit = Unit.getByName(UnitData.CleanUpUnitName) + local CleanUpGroupName = UnitData.CleanUpGroupName + local CleanUpUnitName = UnitData.CleanUpUnitName + if CleanUpUnit then + self:T( { "CleanUp Scheduler", "Checking:", CleanUpUnitName } ) + if _DATABASE:GetStatusGroup( CleanUpGroupName ) ~= "ReSpawn" then + local CleanUpUnitVec3 = CleanUpUnit:getPoint() + --self:T( CleanUpUnitVec3 ) + local CleanUpUnitVec2 = {} + CleanUpUnitVec2.x = CleanUpUnitVec3.x + CleanUpUnitVec2.y = CleanUpUnitVec3.z + --self:T( CleanUpUnitVec2 ) + local CleanUpSurfaceType = land.getSurfaceType(CleanUpUnitVec2) + --self:T( CleanUpSurfaceType ) + --MESSAGE:New( "Surface " .. CleanUpUnitName .. " = " .. CleanUpSurfaceTypeText[CleanUpSurfaceType], CleanUpUnitName, 10, CleanUpUnitName ):ToAll() + + 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 + --MESSAGE:New( "Moved " .. CleanUpUnitName, CleanUpUnitName, 10, CleanUpUnitName ):ToAll() + 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 + +--- Dynamic spawning of groups (and units). +-- +-- @{#SPAWN} class +-- =============== +-- The @{#SPAWN} class allows to spawn dynamically new groups, based on pre-defined initialization settings, modifying the behaviour when groups are spawned. +-- For each group to be spawned, within the mission editor, a group has to be created with the "late activation flag" set. We call this group the *"Spawn Template"* of the SPAWN object. +-- A reference to this Spawn Template needs to be provided when constructing the SPAWN object, by indicating the name of the group within the mission editor in the constructor methods. +-- +-- Within the SPAWN object, there is an internal index that keeps track of which group from the internal group list was spawned. +-- When new groups get spawned by using the SPAWN functions (see below), it will be validated whether the Limits (@{#SPAWN.Limit}) of the SPAWN object are not reached. +-- When all is valid, a new group will be created by the spawning methods, and the internal index will be increased with 1. +-- +-- Regarding the name of new spawned groups, a _SpawnPrefix_ will be assigned for each new group created. +-- If you want to have the Spawn Template name to be used as the _SpawnPrefix_ name, use the @{#SPAWN.New} constructor. +-- However, when the @{#SPAWN.NewWithAlias} constructor was used, the Alias name will define the _SpawnPrefix_ name. +-- Groups will follow the following naming structure when spawned at run-time: +-- +-- 1. Spawned groups will have the name _SpawnPrefix_#ggg, where ggg is a counter from 0 to 999. +-- 2. Spawned units will have the name _SpawnPrefix_#ggg-uu, where uu is a counter from 0 to 99 for each new spawned unit belonging to the group. +-- +-- Some additional notes that need to be remembered: +-- +-- * Templates are actually groups defined within the mission editor, with the flag "Late Activation" set. As such, these groups are never used within the mission, but are used by the @{#SPAWN} module. +-- * It is important to defined BEFORE you spawn new groups, a proper initialization of the SPAWN instance is done with the options you want to use. +-- * When designing a mission, NEVER name groups using a "#" within the name of the group Spawn Template(s), or the SPAWN module logic won't work anymore. +-- +-- SPAWN construction methods: +-- =========================== +-- Create a new SPAWN object with the @{#SPAWN.New} or the @{#SPAWN.NewWithAlias} methods: +-- +-- * @{#SPAWN.New}: Creates a new SPAWN object taking the name of the group that functions as the Template. +-- +-- It is important to understand how the SPAWN class works internally. The SPAWN object created will contain internally a list of groups that will be spawned and that are already spawned. +-- The initialization functions will modify this list of groups so that when a group gets spawned, ALL information is already prepared when spawning. This is done for performance reasons. +-- So in principle, the group list will contain all parameters and configurations after initialization, and when groups get actually spawned, this spawning can be done quickly and efficient. +-- +-- SPAWN initialization methods: +-- ============================= +-- A spawn object will behave differently based on the usage of initialization methods: +-- +-- * @{#SPAWN.Limit}: Limits the amount of groups that can be alive at the same time and that can be dynamically spawned. +-- * @{#SPAWN.RandomizeRoute}: Randomize the routes of spawned groups. +-- * @{#SPAWN.RandomizeTemplate}: Randomize the group templates so that when a new group is spawned, a random group template is selected from one of the templates defined. +-- * @{#SPAWN.Uncontrolled}: Spawn plane groups uncontrolled. +-- * @{#SPAWN.Array}: Make groups visible before they are actually activated, and order these groups like a batallion in an array. +-- * @{#SPAWN.InitRepeat}: Re-spawn groups when they land at the home base. Similar functions are @{#SPAWN.InitRepeatOnLanding} and @{#SPAWN.InitRepeatOnEngineShutDown}. +-- +-- SPAWN spawning methods: +-- ======================= +-- Groups can be spawned at different times and methods: +-- +-- * @{#SPAWN.Spawn}: Spawn one new group based on the last spawned index. +-- * @{#SPAWN.ReSpawn}: Re-spawn a group based on a given index. +-- * @{#SPAWN.SpawnScheduled}: Spawn groups at scheduled but randomized intervals. You can use @{#SPAWN.SpawnScheduleStart} and @{#SPAWN.SpawnScheduleStop} to start and stop the schedule respectively. +-- * @{#SPAWN.SpawnFromUnit}: Spawn a new group taking the position of a @{UNIT}. +-- * @{#SPAWN.SpawnInZone}: Spawn a new group in a @{ZONE}. +-- +-- Note that @{#SPAWN.Spawn} and @{#SPAWN.ReSpawn} return a @{GROUP#GROUP.New} object, that contains a reference to the DCSGroup object. +-- You can use the @{GROUP} object to do further actions with the DCSGroup. +-- +-- SPAWN object cleaning: +-- ========================= +-- Sometimes, it will occur during a mission run-time, that ground or especially air objects get damaged, and will while being damged stop their activities, while remaining alive. +-- In such cases, the SPAWN object will just sit there and wait until that group gets destroyed, but most of the time it won't, +-- and it may occur that no new groups are or can be spawned as limits are reached. +-- To prevent this, a @{#SPAWN.CleanUp} initialization method has been defined that will silently monitor the status of each spawned group. +-- Once a group has a velocity = 0, and has been waiting for a defined interval, that group will be cleaned or removed from run-time. +-- There is a catch however :-) If a damaged group has returned to an airbase within the coalition, that group will not be considered as "lost"... +-- In such a case, when the inactive group is cleaned, a new group will Re-spawned automatically. +-- This models AI that has succesfully returned to their airbase, to restart their combat activities. +-- Check the @{#SPAWN.CleanUp} for further info. +-- +-- ==== +-- @module Spawn +-- @author FlightControl + +Include.File( "Routines" ) +Include.File( "Base" ) +Include.File( "Database" ) +Include.File( "Group" ) +Include.File( "Zone" ) +Include.File( "Event" ) +Include.File( "Scheduler" ) + +--- SPAWN Class +-- @type SPAWN +-- @extends Base#BASE +-- @field ClassName +-- @field #string SpawnTemplatePrefix +-- @field #string SpawnAliasPrefix +SPAWN = { + ClassName = "SPAWN", + SpawnTemplatePrefix = nil, + SpawnAliasPrefix = nil, +} + + + +--- Creates the main object to spawn a GROUP defined in the DCS ME. +-- @param #SPAWN self +-- @param #string SpawnTemplatePrefix is the name of the Group in the ME that defines the Template. Each new group will have the name starting with SpawnTemplatePrefix. +-- @return #SPAWN +-- @usage +-- -- NATO helicopters engaging in the battle field. +-- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ) +-- @usage local Plane = SPAWN:New( "Plane" ) -- Creates a new local variable that can initiate new planes with the name "Plane#ddd" using the template "Plane" as defined within the ME. +function SPAWN:New( SpawnTemplatePrefix ) + local self = BASE:Inherit( self, BASE:New() ) + self:F( { SpawnTemplatePrefix } ) + + local TemplateGroup = Group.getByName( SpawnTemplatePrefix ) + if TemplateGroup then + self.SpawnTemplatePrefix = SpawnTemplatePrefix + self.SpawnIndex = 0 + self.SpawnCount = 0 -- The internal counter of the amount of spawning the has happened since SpawnStart. + self.AliveUnits = 0 -- Contains the counter how many units are currently alive + self.SpawnIsScheduled = false -- Reflects if the spawning for this SpawnTemplatePrefix is going to be scheduled or not. + self.SpawnTemplate = self._GetTemplate( self, SpawnTemplatePrefix ) -- Contains the template structure for a Group Spawn from the Mission Editor. Note that this group must have lateActivation always on!!! + self.Repeat = false -- Don't repeat the group from Take-Off till Landing and back Take-Off by ReSpawning. + self.UnControlled = false -- When working in UnControlled mode, all planes are Spawned in UnControlled mode before the scheduler starts. + self.SpawnMaxUnitsAlive = 0 -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. + self.SpawnMaxGroups = 0 -- The maximum amount of groups that can be spawned. + self.SpawnRandomize = false -- Sets the randomization flag of new Spawned units to false. + self.SpawnVisible = false -- Flag that indicates if all the Groups of the SpawnGroup need to be visible when Spawned. + + self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. + else + error( "SPAWN:New: There is no group declared in the mission editor with SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" ) + end + + return self +end + +--- Creates a new SPAWN instance to create new groups based on the defined template and using a new alias for each new group. +-- @param #SPAWN self +-- @param #string SpawnTemplatePrefix is the name of the Group in the ME that defines the Template. +-- @param #string SpawnAliasPrefix is the name that will be given to the Group at runtime. +-- @return #SPAWN +-- @usage +-- -- NATO helicopters engaging in the battle field. +-- Spawn_BE_KA50 = SPAWN:NewWithAlias( 'BE KA-50@RAMP-Ground Defense', 'Helicopter Attacking a City' ) +-- @usage local PlaneWithAlias = SPAWN:NewWithAlias( "Plane", "Bomber" ) -- Creates a new local variable that can instantiate new planes with the name "Bomber#ddd" using the template "Plane" as defined within the ME. +function SPAWN:NewWithAlias( SpawnTemplatePrefix, SpawnAliasPrefix ) + local self = BASE:Inherit( self, BASE:New() ) + self:F( { SpawnTemplatePrefix, SpawnAliasPrefix } ) + + local TemplateGroup = Group.getByName( SpawnTemplatePrefix ) + if TemplateGroup then + self.SpawnTemplatePrefix = SpawnTemplatePrefix + self.SpawnAliasPrefix = SpawnAliasPrefix + self.SpawnIndex = 0 + self.SpawnCount = 0 -- The internal counter of the amount of spawning the has happened since SpawnStart. + self.AliveUnits = 0 -- Contains the counter how many units are currently alive + self.SpawnIsScheduled = false -- Reflects if the spawning for this SpawnTemplatePrefix is going to be scheduled or not. + self.SpawnTemplate = self._GetTemplate( self, SpawnTemplatePrefix ) -- Contains the template structure for a Group Spawn from the Mission Editor. Note that this group must have lateActivation always on!!! + self.Repeat = false -- Don't repeat the group from Take-Off till Landing and back Take-Off by ReSpawning. + self.UnControlled = false -- When working in UnControlled mode, all planes are Spawned in UnControlled mode before the scheduler starts. + self.SpawnMaxUnitsAlive = 0 -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. + self.SpawnMaxGroups = 0 -- The maximum amount of groups that can be spawned. + self.SpawnRandomize = false -- Sets the randomization flag of new Spawned units to false. + self.SpawnVisible = false -- Flag that indicates if all the Groups of the SpawnGroup need to be visible when Spawned. + + self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. + else + error( "SPAWN:New: There is no group declared in the mission editor with SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" ) + end + + return self +end + + +--- Limits the Maximum amount of Units that can be alive at the same time, and the maximum amount of groups that can be spawned. +-- Note that this method is exceptionally important to balance the performance of the mission. Depending on the machine etc, a mission can only process a maximum amount of units. +-- If the time interval must be short, but there should not be more Units or Groups alive than a maximum amount of units, then this function should be used... +-- When a @{#SPAWN.New} is executed and the limit of the amount of units alive is reached, then no new spawn will happen of the group, until some of these units of the spawn object will be destroyed. +-- @param #SPAWN self +-- @param #number SpawnMaxUnitsAlive The maximum amount of units that can be alive at runtime. +-- @param #number SpawnMaxGroups The maximum amount of groups that can be spawned. When the limit is reached, then no more actual spawns will happen of the group. +-- This parameter is useful to define a maximum amount of airplanes, ground troops, helicopters, ships etc within a supply area. +-- This parameter accepts the value 0, which defines that there are no maximum group limits, but there are limits on the maximum of units that can be alive at the same time. +-- @return #SPAWN self +-- @usage +-- -- NATO helicopters engaging in the battle field. +-- -- This helicopter group consists of one Unit. So, this group will SPAWN maximum 2 groups simultaneously within the DCSRTE. +-- -- There will be maximum 24 groups spawned during the whole mission lifetime. +-- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):Limit( 2, 24 ) +function SPAWN:Limit( SpawnMaxUnitsAlive, SpawnMaxGroups ) + self:F( { self.SpawnTemplatePrefix, SpawnMaxUnitsAlive, SpawnMaxGroups } ) + + self.SpawnMaxUnitsAlive = SpawnMaxUnitsAlive -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. + self.SpawnMaxGroups = SpawnMaxGroups -- The maximum amount of groups that can be spawned. + + for SpawnGroupID = 1, self.SpawnMaxGroups do + self:_InitializeSpawnGroups( SpawnGroupID ) + end + + return self +end + + +--- Randomizes the defined route of the SpawnTemplatePrefix group in the ME. This is very useful to define extra variation of the behaviour of groups. +-- @param #SPAWN self +-- @param #number SpawnStartPoint is the waypoint where the randomization begins. +-- Note that the StartPoint = 0 equaling the point where the group is spawned. +-- @param #number SpawnEndPoint is the waypoint where the randomization ends counting backwards. +-- This parameter is useful to avoid randomization to end at a waypoint earlier than the last waypoint on the route. +-- @param #number SpawnRadius is the radius in meters in which the randomization of the new waypoints, with the original waypoint of the original template located in the middle ... +-- @return #SPAWN +-- @usage +-- -- NATO helicopters engaging in the battle field. +-- -- The KA-50 has waypoints Start point ( =0 or SP ), 1, 2, 3, 4, End point (= 5 or DP). +-- -- Waypoints 2 and 3 will only be randomized. The others will remain on their original position with each new spawn of the helicopter. +-- -- The randomization of waypoint 2 and 3 will take place within a radius of 2000 meters. +-- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):RandomizeRoute( 2, 2, 2000 ) +function SPAWN:RandomizeRoute( SpawnStartPoint, SpawnEndPoint, SpawnRadius ) + self:F( { self.SpawnTemplatePrefix, SpawnStartPoint, SpawnEndPoint, SpawnRadius } ) + + self.SpawnRandomizeRoute = true + self.SpawnRandomizeRouteStartPoint = SpawnStartPoint + self.SpawnRandomizeRouteEndPoint = SpawnEndPoint + self.SpawnRandomizeRouteRadius = SpawnRadius + + for GroupID = 1, self.SpawnMaxGroups do + self:_RandomizeRoute( GroupID ) + end + + return self +end + + +--- This function is rather complicated to understand. But I'll try to explain. +-- This function becomes useful when you need to spawn groups with random templates of groups defined within the mission editor, +-- but they will all follow the same Template route and have the same prefix name. +-- In other words, this method randomizes between a defined set of groups the template to be used for each new spawn of a group. +-- @param #SPAWN self +-- @param #string SpawnTemplatePrefixTable A table with the names of the groups defined within the mission editor, from which one will be choosen when a new group will be spawned. +-- @return #SPAWN +-- @usage +-- -- NATO Tank Platoons invading Gori. +-- -- Choose between 13 different 'US Tank Platoon' configurations for each new SPAWN the Group to be spawned for the +-- -- 'US Tank Platoon Left', 'US Tank Platoon Middle' and 'US Tank Platoon Right' SpawnTemplatePrefixes. +-- -- Each new SPAWN will randomize the route, with a defined time interval of 200 seconds with 40% time variation (randomization) and +-- -- with a limit set of maximum 12 Units alive simulteneously and 150 Groups to be spawned during the whole mission. +-- Spawn_US_Platoon = { 'US Tank Platoon 1', 'US Tank Platoon 2', 'US Tank Platoon 3', 'US Tank Platoon 4', 'US Tank Platoon 5', +-- 'US Tank Platoon 6', 'US Tank Platoon 7', 'US Tank Platoon 8', 'US Tank Platoon 9', 'US Tank Platoon 10', +-- 'US Tank Platoon 11', 'US Tank Platoon 12', 'US Tank Platoon 13' } +-- Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):Limit( 12, 150 ):Schedule( 200, 0.4 ):RandomizeTemplate( Spawn_US_Platoon ):RandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):Limit( 12, 150 ):Schedule( 200, 0.4 ):RandomizeTemplate( Spawn_US_Platoon ):RandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):Limit( 12, 150 ):Schedule( 200, 0.4 ):RandomizeTemplate( Spawn_US_Platoon ):RandomizeRoute( 3, 3, 2000 ) +function SPAWN:RandomizeTemplate( SpawnTemplatePrefixTable ) + self:F( { self.SpawnTemplatePrefix, SpawnTemplatePrefixTable } ) + + self.SpawnTemplatePrefixTable = SpawnTemplatePrefixTable + self.SpawnRandomizeTemplate = true + + for SpawnGroupID = 1, self.SpawnMaxGroups do + self:_RandomizeTemplate( SpawnGroupID ) + end + + return self +end + + + + + +--- For planes and helicopters, when these groups go home and land on their home airbases and farps, they normally would taxi to the parking spot, shut-down their engines and wait forever until the Group is removed by the runtime environment. +-- This function is used to re-spawn automatically (so no extra call is needed anymore) the same group after it has landed. +-- This will enable a spawned group to be re-spawned after it lands, until it is destroyed... +-- Note: When the group is respawned, it will re-spawn from the original airbase where it took off. +-- So ensure that the routes for groups that respawn, always return to the original airbase, or players may get confused ... +-- @param #SPAWN self +-- @return #SPAWN self +-- @usage +-- -- RU Su-34 - AI Ship Attack +-- -- Re-SPAWN the Group(s) after each landing and Engine Shut-Down automatically. +-- SpawnRU_SU34 = SPAWN:New( 'TF1 RU Su-34 Krymsk@AI - Attack Ships' ):Schedule( 2, 3, 1800, 0.4 ):SpawnUncontrolled():RandomizeRoute( 1, 1, 3000 ):RepeatOnEngineShutDown() +function SPAWN:InitRepeat() + self:F( { self.SpawnTemplatePrefix, self.SpawnIndex } ) + + self.Repeat = true + self.RepeatOnEngineShutDown = false + self.RepeatOnLanding = true + + return self +end + +--- Respawn group after landing. +-- @param #SPAWN self +-- @return #SPAWN self +function SPAWN:InitRepeatOnLanding() + self:F( { self.SpawnTemplatePrefix } ) + + self:InitRepeat() + self.RepeatOnEngineShutDown = false + self.RepeatOnLanding = true + + return self +end + + +--- Respawn after landing when its engines have shut down. +-- @param #SPAWN self +-- @return #SPAWN self +function SPAWN:InitRepeatOnEngineShutDown() + self:F( { self.SpawnTemplatePrefix } ) + + self:InitRepeat() + self.RepeatOnEngineShutDown = true + self.RepeatOnLanding = false + + return self +end + + +--- CleanUp groups when they are still alive, but inactive. +-- When groups are still alive and have become inactive due to damage and are unable to contribute anything, then this group will be removed at defined intervals in seconds. +-- @param #SPAWN self +-- @param #string SpawnCleanUpInterval The interval to check for inactive groups within seconds. +-- @return #SPAWN self +-- @usage Spawn_Helicopter:CleanUp( 20 ) -- CleanUp the spawning of the helicopters every 20 seconds when they become inactive. +function SPAWN:CleanUp( SpawnCleanUpInterval ) + self:F( { self.SpawnTemplatePrefix, SpawnCleanUpInterval } ) + + self.SpawnCleanUpInterval = SpawnCleanUpInterval + self.SpawnCleanUpTimeStamps = {} + --self.CleanUpFunction = routines.scheduleFunction( self._SpawnCleanUpScheduler, { self }, timer.getTime() + 1, SpawnCleanUpInterval ) + self.CleanUpScheduler = SCHEDULER:New( self, self._SpawnCleanUpScheduler, {}, 1, SpawnCleanUpInterval, 0.2 ) + return self +end + + + +--- Makes the groups visible before start (like a batallion). +-- The method will take the position of the group as the first position in the array. +-- @param #SPAWN self +-- @param #number SpawnAngle The angle in degrees how the groups and each unit of the group will be positioned. +-- @param #number SpawnWidth The amount of Groups that will be positioned on the X axis. +-- @param #number SpawnDeltaX The space between each Group on the X-axis. +-- @param #number SpawnDeltaY The space between each Group on the Y-axis. +-- @return #SPAWN self +-- @usage +-- -- Define an array of Groups. +-- Spawn_BE_Ground = SPAWN:New( 'BE Ground' ):Limit( 2, 24 ):Visible( 90, "Diamond", 10, 100, 50 ) +function SPAWN:Array( SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY ) + self:F( { self.SpawnTemplatePrefix, SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY } ) + + self.SpawnVisible = true -- When the first Spawn executes, all the Groups need to be made visible before start. + + local SpawnX = 0 + local SpawnY = 0 + local SpawnXIndex = 0 + local SpawnYIndex = 0 + + for SpawnGroupID = 1, self.SpawnMaxGroups do + self:T( { SpawnX, SpawnY, SpawnXIndex, SpawnYIndex } ) + + self.SpawnGroups[SpawnGroupID].Visible = true + self.SpawnGroups[SpawnGroupID].Spawned = false + + SpawnXIndex = SpawnXIndex + 1 + if SpawnWidth and SpawnWidth ~= 0 then + if SpawnXIndex >= SpawnWidth then + SpawnXIndex = 0 + SpawnYIndex = SpawnYIndex + 1 + end + end + + local SpawnRootX = self.SpawnGroups[SpawnGroupID].SpawnTemplate.x + local SpawnRootY = self.SpawnGroups[SpawnGroupID].SpawnTemplate.y + + self:_TranslateRotate( SpawnGroupID, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle ) + + self.SpawnGroups[SpawnGroupID].SpawnTemplate.lateActivation = true + self.SpawnGroups[SpawnGroupID].SpawnTemplate.visible = true + + self.SpawnGroups[SpawnGroupID].Visible = true + + _EVENTDISPATCHER:OnBirthForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnBirth, self ) + _EVENTDISPATCHER:OnCrashForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnDeadOrCrash, self ) + _EVENTDISPATCHER:OnDeadForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnDeadOrCrash, self ) + + if self.Repeat then + _EVENTDISPATCHER:OnTakeOffForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnTakeOff, self ) + _EVENTDISPATCHER:OnLandForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnLand, self ) + end + if self.RepeatOnEngineShutDown then + _EVENTDISPATCHER:OnEngineShutDownForTemplate( self.SpawnGroups[SpawnGroupID].SpawnTemplate, self._OnEngineShutDown, self ) + end + + self.SpawnGroups[SpawnGroupID].Group = _DATABASE:Spawn( self.SpawnGroups[SpawnGroupID].SpawnTemplate ) + + SpawnX = SpawnXIndex * SpawnDeltaX + SpawnY = SpawnYIndex * SpawnDeltaY + end + + return self +end + + + +--- Will spawn a group based on the internal index. +-- Note: Uses @{DATABASE} module defined in MOOSE. +-- @param #SPAWN self +-- @return Group#GROUP The group that was spawned. You can use this group for further actions. +function SPAWN:Spawn() + self:F( { self.SpawnTemplatePrefix, self.SpawnIndex } ) + + return self:SpawnWithIndex( self.SpawnIndex + 1 ) +end + +--- Will re-spawn a group based on a given index. +-- Note: Uses @{DATABASE} module defined in MOOSE. +-- @param #SPAWN self +-- @param #string SpawnIndex The index of the group to be spawned. +-- @return Group#GROUP The group that was spawned. You can use this group for further actions. +function SPAWN:ReSpawn( SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) + + if not SpawnIndex then + SpawnIndex = 1 + end + +-- TODO: This logic makes DCS crash and i don't know why (yet). + local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) + if SpawnGroup then + local SpawnDCSGroup = SpawnGroup:GetDCSGroup() + if SpawnDCSGroup then + SpawnGroup:Destroy() + end + end + + return self:SpawnWithIndex( SpawnIndex ) +end + +--- Will spawn a group with a specified index number. +-- Uses @{DATABASE} global object defined in MOOSE. +-- @param #SPAWN self +-- @return Group#GROUP The group that was spawned. You can use this group for further actions. +function SPAWN:SpawnWithIndex( SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnMaxGroups } ) + + if self:_GetSpawnIndex( SpawnIndex ) then + + if self.SpawnGroups[self.SpawnIndex].Visible then + self.SpawnGroups[self.SpawnIndex].Group:Activate() + else + self:T( self.SpawnGroups[self.SpawnIndex].SpawnTemplate ) + _EVENTDISPATCHER:OnBirthForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnBirth, self ) + _EVENTDISPATCHER:OnCrashForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnDeadOrCrash, self ) + _EVENTDISPATCHER:OnDeadForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnDeadOrCrash, self ) + + if self.Repeat then + _EVENTDISPATCHER:OnTakeOffForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnTakeOff, self ) + _EVENTDISPATCHER:OnLandForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnLand, self ) + end + if self.RepeatOnEngineShutDown then + _EVENTDISPATCHER:OnEngineShutDownForTemplate( self.SpawnGroups[self.SpawnIndex].SpawnTemplate, self._OnEngineShutDown, self ) + end + + self:T( self.SpawnGroups[self.SpawnIndex].SpawnTemplate ) + + self.SpawnGroups[self.SpawnIndex].Group = _DATABASE:Spawn( self.SpawnGroups[self.SpawnIndex].SpawnTemplate ) + + -- If there is a SpawnFunction hook defined, call it. + if self.SpawnFunctionHook then + self.SpawnFunctionHook( self.SpawnGroups[self.SpawnIndex].Group, unpack( self.SpawnFunctionArguments ) ) + end + -- TODO: Need to fix this by putting an "R" in the name of the group when the group repeats. + --if self.Repeat then + -- _DATABASE:SetStatusGroup( SpawnTemplate.name, "ReSpawn" ) + --end + end + + self.SpawnGroups[self.SpawnIndex].Spawned = true + return self.SpawnGroups[self.SpawnIndex].Group + else + --self:E( { self.SpawnTemplatePrefix, "No more Groups to Spawn:", SpawnIndex, self.SpawnMaxGroups } ) + end + + return nil +end + +--- Spawns new groups at varying time intervals. +-- This is useful if you want to have continuity within your missions of certain (AI) groups to be present (alive) within your missions. +-- @param #SPAWN self +-- @param #number SpawnTime The time interval defined in seconds between each new spawn of new groups. +-- @param #number SpawnTimeVariation The variation to be applied on the defined time interval between each new spawn. +-- The variation is a number between 0 and 1, representing the %-tage of variation to be applied on the time interval. +-- @return #SPAWN self +-- @usage +-- -- NATO helicopters engaging in the battle field. +-- -- The time interval is set to SPAWN new helicopters between each 600 seconds, with a time variation of 50%. +-- -- The time variation in this case will be between 450 seconds and 750 seconds. +-- -- This is calculated as follows: +-- -- Low limit: 600 * ( 1 - 0.5 / 2 ) = 450 +-- -- High limit: 600 * ( 1 + 0.5 / 2 ) = 750 +-- -- Between these two values, a random amount of seconds will be choosen for each new spawn of the helicopters. +-- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):Schedule( 600, 0.5 ) +function SPAWN:SpawnScheduled( SpawnTime, SpawnTimeVariation ) + self:F( { SpawnTime, SpawnTimeVariation } ) + + if SpawnTime ~= nil and SpawnTimeVariation ~= nil then + self.SpawnScheduler = SCHEDULER:New( self, self._Scheduler, {}, 1, SpawnTime, SpawnTimeVariation ) + end + + return self +end + +--- Will re-start the spawning scheduler. +-- Note: This function is only required to be called when the schedule was stopped. +function SPAWN:SpawnScheduleStart() + self:F( { self.SpawnTemplatePrefix } ) + + self.SpawnScheduler:Start() +end + +--- Will stop the scheduled spawning scheduler. +function SPAWN:SpawnScheduleStop() + self:F( { self.SpawnTemplatePrefix } ) + + self.SpawnScheduler:Stop() +end + + +--- Allows to place a CallFunction hook when a new group spawns. +-- The provided function will be called when a new group is spawned, including its given parameters. +-- The first parameter of the SpawnFunction is the @{Group#GROUP} that was spawned. +-- @param #SPAWN self +-- @param #function SpawnFunctionHook The function to be called when a group spawns. +-- @param SpawnFunctionArguments A random amount of arguments to be provided to the function when the group spawns. +-- @return #SPAWN +function SPAWN:SpawnFunction( SpawnFunctionHook, ... ) + self:F( SpawnFunction ) + + self.SpawnFunctionHook = SpawnFunctionHook + self.SpawnFunctionArguments = {} + if arg then + self.SpawnFunctionArguments = arg + end + + return self +end + + + + +--- Will spawn a group from a hosting unit. This function is mostly advisable to be used if you want to simulate spawning from air units, like helicopters, which are dropping infantry into a defined Landing Zone. +-- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. +-- You can use the returned group to further define the route to be followed. +-- @param #SPAWN self +-- @param Unit#UNIT HostUnit The air or ground unit dropping or unloading the group. +-- @param #number OuterRadius The outer radius in meters where the new group will be spawned. +-- @param #number InnerRadius The inner radius in meters where the new group will NOT be spawned. +-- @param #number SpawnIndex (Optional) The index which group to spawn within the given zone. +-- @return Group#GROUP that was spawned. +-- @return #nil Nothing was spawned. +function SPAWN:SpawnFromUnit( HostUnit, OuterRadius, InnerRadius, SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, HostUnit, OuterRadius, InnerRadius, SpawnIndex } ) + + if HostUnit and HostUnit:IsAlive() then -- and HostUnit:getUnit(1):inAir() == false then + + if SpawnIndex then + else + SpawnIndex = self.SpawnIndex + 1 + end + + if self:_GetSpawnIndex( SpawnIndex ) then + + local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate + + if SpawnTemplate then + + local UnitPoint = HostUnit:GetPointVec2() + + self:T( { "Current point of ", self.SpawnTemplatePrefix, UnitPoint } ) + + --for PointID, Point in pairs( SpawnTemplate.route.points ) do + --Point.x = UnitPoint.x + --Point.y = UnitPoint.y + --Point.alt = nil + --Point.alt_type = nil + --end + + SpawnTemplate.route.points[1].x = UnitPoint.x + SpawnTemplate.route.points[1].y = UnitPoint.y + + if not InnerRadius then + InnerRadius = 10 + end + + if not OuterRadius then + OuterRadius = 50 + end + + -- Apply SpawnFormation + for UnitID = 1, #SpawnTemplate.units do + if InnerRadius == 0 then + SpawnTemplate.units[UnitID].x = UnitPoint.x + SpawnTemplate.units[UnitID].y = UnitPoint.y + else + local CirclePos = routines.getRandPointInCircle( UnitPoint, OuterRadius, InnerRadius ) + SpawnTemplate.units[UnitID].x = CirclePos.x + SpawnTemplate.units[UnitID].y = CirclePos.y + end + self:T( 'SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) + end + + local SpawnPos = routines.getRandPointInCircle( UnitPoint, OuterRadius, InnerRadius ) + local Point = {} + Point.type = "Turning Point" + Point.x = SpawnPos.x + Point.y = SpawnPos.y + Point.action = "Cone" + Point.speed = 5 + + table.insert( SpawnTemplate.route.points, 2, Point ) + + return self:SpawnWithIndex( self.SpawnIndex ) + end + end + end + + return nil +end + +--- Will spawn a Group within a given @{Zone#ZONE}. +-- Once the group is spawned within the zone, it will continue on its route. +-- The first waypoint (where the group is spawned) is replaced with the zone coordinates. +-- @param #SPAWN self +-- @param Zone#ZONE Zone The zone where the group is to be spawned. +-- @param #number ZoneRandomize (Optional) Set to true if you want to randomize the starting point in the zone. +-- @param #number SpawnIndex (Optional) The index which group to spawn within the given zone. +-- @return Group#GROUP that was spawned. +-- @return #nil when nothing was spawned. +function SPAWN:SpawnInZone( Zone, ZoneRandomize, SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, Zone, ZoneRandomize, SpawnIndex } ) + + if Zone then + + if SpawnIndex then + else + SpawnIndex = self.SpawnIndex + 1 + end + + if self:_GetSpawnIndex( SpawnIndex ) then + + local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate + + if SpawnTemplate then + + local ZonePoint + + if ZoneRandomize == true then + ZonePoint = Zone:GetRandomPointVec2() + else + ZonePoint = Zone:GetPointVec2() + end + + SpawnTemplate.route.points[1].x = ZonePoint.x + SpawnTemplate.route.points[1].y = ZonePoint.y + + -- Apply SpawnFormation + for UnitID = 1, #SpawnTemplate.units do + local ZonePointUnit = Zone:GetRandomPointVec2() + SpawnTemplate.units[UnitID].x = ZonePointUnit.x + SpawnTemplate.units[UnitID].y = ZonePointUnit.y + self:T( 'SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) + end + + return self:SpawnWithIndex( self.SpawnIndex ) + end + end + end + + return nil +end + + + + +--- Will spawn a plane group in uncontrolled mode... +-- This will be similar to the uncontrolled flag setting in the ME. +-- @return #SPAWN self +function SPAWN:UnControlled() + self:F( { self.SpawnTemplatePrefix } ) + + self.SpawnUnControlled = true + + for SpawnGroupID = 1, self.SpawnMaxGroups do + self.SpawnGroups[SpawnGroupID].UnControlled = true + end + + return self +end + + + +--- Will return the SpawnGroupName either with with a specific count number or without any count. +-- @param #SPAWN self +-- @param #number SpawnIndex Is the number of the Group that is to be spawned. +-- @return #string SpawnGroupName +function SPAWN:SpawnGroupName( SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) + + local SpawnPrefix = self.SpawnTemplatePrefix + if self.SpawnAliasPrefix then + SpawnPrefix = self.SpawnAliasPrefix + end + + if SpawnIndex then + local SpawnName = string.format( '%s#%03d', SpawnPrefix, SpawnIndex ) + self:T( SpawnName ) + return SpawnName + else + self:T( SpawnPrefix ) + return SpawnPrefix + end + +end + +--- Find the first alive group. +-- @param #SPAWN self +-- @param #number SpawnCursor A number holding the index from where to find the first group from. +-- @return Group#GROUP, #number The group found, the new index where the group was found. +-- @return #nil, #nil When no group is found, #nil is returned. +function SPAWN:GetFirstAliveGroup( SpawnCursor ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnCursor } ) + + for SpawnIndex = 1, self.SpawnCount do + local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) + if SpawnGroup and SpawnGroup:IsAlive() then + SpawnCursor = SpawnIndex + return SpawnGroup, SpawnCursor + end + end + + return nil, nil +end + + +--- Find the next alive group. +-- @param #SPAWN self +-- @param #number SpawnCursor A number holding the last found previous index. +-- @return Group#GROUP, #number The group found, the new index where the group was found. +-- @return #nil, #nil When no group is found, #nil is returned. +function SPAWN:GetNextAliveGroup( SpawnCursor ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnCursor } ) + + SpawnCursor = SpawnCursor + 1 + for SpawnIndex = SpawnCursor, self.SpawnCount do + local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) + if SpawnGroup and SpawnGroup:IsAlive() then + SpawnCursor = SpawnIndex + return SpawnGroup, SpawnCursor + end + end + + return nil, nil +end + +--- Find the last alive group during runtime. +function SPAWN:GetLastAliveGroup() + self:F( { self.SpawnTemplatePrefixself.SpawnAliasPrefix } ) + + self.SpawnIndex = self:_GetLastIndex() + for SpawnIndex = self.SpawnIndex, 1, -1 do + local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) + if SpawnGroup and SpawnGroup:IsAlive() then + self.SpawnIndex = SpawnIndex + return SpawnGroup + end + end + + self.SpawnIndex = nil + return nil +end + + + +--- Get the group from an index. +-- Returns the group from the SpawnGroups list. +-- If no index is given, it will return the first group in the list. +-- @param #SPAWN self +-- @param #number SpawnIndex The index of the group to return. +-- @return Group#GROUP +function SPAWN:GetGroupFromIndex( SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndex } ) + + if not SpawnIndex then + SpawnIndex = 1 + end + + if self.SpawnGroups and self.SpawnGroups[SpawnIndex] then + local SpawnGroup = self.SpawnGroups[SpawnIndex].Group + return SpawnGroup + else + return nil + end +end + +--- Get the group index from a DCSUnit. +-- The method will search for a #-mark, and will return the index behind the #-mark of the DCSUnit. +-- It will return nil of no prefix was found. +-- @param #SPAWN self +-- @param DCSUnit The DCS unit to be searched. +-- @return #string The prefix +-- @return #nil Nothing found +function SPAWN:_GetGroupIndexFromDCSUnit( DCSUnit ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, DCSUnit } ) + + if DCSUnit and DCSUnit:getName() then + local IndexString = string.match( DCSUnit:getName(), "#.*-" ):sub( 2, -2 ) + self:T( IndexString ) + + if IndexString then + local Index = tonumber( IndexString ) + self:T( { "Index:", IndexString, Index } ) + return Index + end + end + + return nil +end + +--- Return the prefix of a DCSUnit. +-- The method will search for a #-mark, and will return the text before the #-mark. +-- It will return nil of no prefix was found. +-- @param #SPAWN self +-- @param DCSUnit The DCS unit to be searched. +-- @return #string The prefix +-- @return #nil Nothing found +function SPAWN:_GetPrefixFromDCSUnit( DCSUnit ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, DCSUnit } ) + + if DCSUnit and DCSUnit:getName() then + local SpawnPrefix = string.match( DCSUnit:getName(), ".*#" ) + if SpawnPrefix then + SpawnPrefix = SpawnPrefix:sub( 1, -2 ) + end + self:T( SpawnPrefix ) + return SpawnPrefix + end + + return nil +end + +--- Return the group within the SpawnGroups collection with input a DCSUnit. +function SPAWN:_GetGroupFromDCSUnit( DCSUnit ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, DCSUnit } ) + + if DCSUnit then + local SpawnPrefix = self:_GetPrefixFromDCSUnit( DCSUnit ) + + if self.SpawnTemplatePrefix == SpawnPrefix or ( self.SpawnAliasPrefix and self.SpawnAliasPrefix == SpawnPrefix ) then + local SpawnGroupIndex = self:_GetGroupIndexFromDCSUnit( DCSUnit ) + local SpawnGroup = self.SpawnGroups[SpawnGroupIndex].Group + self:T( SpawnGroup ) + return SpawnGroup + end + end + + return nil +end + + +--- Get the index from a given group. +-- The function will search the name of the group for a #, and will return the number behind the #-mark. +function SPAWN:GetSpawnIndexFromGroup( SpawnGroup ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnGroup } ) + + local IndexString = string.match( SpawnGroup:GetName(), "#.*$" ):sub( 2 ) + local Index = tonumber( IndexString ) + + self:T( IndexString, Index ) + return Index + +end + +--- Return the last maximum index that can be used. +function SPAWN:_GetLastIndex() + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) + + return self.SpawnMaxGroups +end + +--- Initalize the SpawnGroups collection. +function SPAWN:_InitializeSpawnGroups( SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndex } ) + + if not self.SpawnGroups[SpawnIndex] then + self.SpawnGroups[SpawnIndex] = {} + self.SpawnGroups[SpawnIndex].Visible = false + self.SpawnGroups[SpawnIndex].Spawned = false + self.SpawnGroups[SpawnIndex].UnControlled = false + self.SpawnGroups[SpawnIndex].SpawnTime = 0 + + self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix = self.SpawnTemplatePrefix + self.SpawnGroups[SpawnIndex].SpawnTemplate = self:_Prepare( self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix, SpawnIndex ) + end + + self:_RandomizeTemplate( SpawnIndex ) + self:_RandomizeRoute( SpawnIndex ) + --self:_TranslateRotate( SpawnIndex ) + + return self.SpawnGroups[SpawnIndex] +end + + + +--- Gets the CategoryID of the Group with the given SpawnPrefix +function SPAWN:_GetGroupCategoryID( SpawnPrefix ) + local TemplateGroup = Group.getByName( SpawnPrefix ) + + if TemplateGroup then + return TemplateGroup:getCategory() + else + return nil + end +end + +--- Gets the CoalitionID of the Group with the given SpawnPrefix +function SPAWN:_GetGroupCoalitionID( SpawnPrefix ) + local TemplateGroup = Group.getByName( SpawnPrefix ) + + if TemplateGroup then + return TemplateGroup:getCoalition() + else + return nil + end +end + +--- Gets the CountryID of the Group with the given SpawnPrefix +function SPAWN:_GetGroupCountryID( SpawnPrefix ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnPrefix } ) + + local TemplateGroup = Group.getByName( SpawnPrefix ) + + if TemplateGroup then + local TemplateUnits = TemplateGroup:getUnits() + return TemplateUnits[1]:getCountry() + else + return nil + end +end + +--- Gets the Group Template from the ME environment definition. +-- This method used the @{DATABASE} object, which contains ALL initial and new spawned object in MOOSE. +-- @param #SPAWN self +-- @param #string SpawnTemplatePrefix +-- @return @SPAWN self +function SPAWN:_GetTemplate( SpawnTemplatePrefix ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnTemplatePrefix } ) + + local SpawnTemplate = nil + + SpawnTemplate = routines.utils.deepCopy( _DATABASE.Templates.Groups[SpawnTemplatePrefix].Template ) + + if SpawnTemplate == nil then + error( 'No Template returned for SpawnTemplatePrefix = ' .. SpawnTemplatePrefix ) + end + + SpawnTemplate.SpawnCoalitionID = self:_GetGroupCoalitionID( SpawnTemplatePrefix ) + SpawnTemplate.SpawnCategoryID = self:_GetGroupCategoryID( SpawnTemplatePrefix ) + SpawnTemplate.SpawnCountryID = self:_GetGroupCountryID( SpawnTemplatePrefix ) + + self:T( { SpawnTemplate } ) + return SpawnTemplate +end + +--- Prepares the new Group Template. +-- @param #SPAWN self +-- @param #string SpawnTemplatePrefix +-- @param #number SpawnIndex +-- @return #SPAWN self +function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) + + local SpawnTemplate = self:_GetTemplate( SpawnTemplatePrefix ) + SpawnTemplate.name = self:SpawnGroupName( SpawnIndex ) + + SpawnTemplate.groupId = nil + SpawnTemplate.lateActivation = false + + if SpawnTemplate.SpawnCategoryID == Group.Category.GROUND then + self:T( "For ground units, visible needs to be false..." ) + SpawnTemplate.visible = false + end + + if SpawnTemplate.SpawnCategoryID == Group.Category.HELICOPTER or SpawnTemplate.SpawnCategoryID == Group.Category.AIRPLANE then + SpawnTemplate.uncontrolled = false + end + + for UnitID = 1, #SpawnTemplate.units do + SpawnTemplate.units[UnitID].name = string.format( SpawnTemplate.name .. '-%02d', UnitID ) + SpawnTemplate.units[UnitID].unitId = nil + SpawnTemplate.units[UnitID].x = SpawnTemplate.route.points[1].x + SpawnTemplate.units[UnitID].y = SpawnTemplate.route.points[1].y + end + + self:T( { "Template:", SpawnTemplate } ) + return SpawnTemplate + +end + +--- Private method randomizing the routes. +-- @param #SPAWN self +-- @param #number SpawnIndex The index of the group to be spawned. +-- @return #SPAWN +function SPAWN:_RandomizeRoute( SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeRoute, self.SpawnRandomizeRouteStartPoint, self.SpawnRandomizeRouteEndPoint, self.SpawnRandomizeRouteRadius } ) + + if self.SpawnRandomizeRoute then + local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate + local RouteCount = #SpawnTemplate.route.points + + for t = self.SpawnRandomizeRouteStartPoint + 1, ( RouteCount - self.SpawnRandomizeRouteEndPoint ) do + SpawnTemplate.route.points[t].x = SpawnTemplate.route.points[t].x + math.random( self.SpawnRandomizeRouteRadius * -1, self.SpawnRandomizeRouteRadius ) + SpawnTemplate.route.points[t].y = SpawnTemplate.route.points[t].y + math.random( self.SpawnRandomizeRouteRadius * -1, self.SpawnRandomizeRouteRadius ) + -- TODO: manage altitude for airborne units ... + SpawnTemplate.route.points[t].alt = nil + --SpawnGroup.route.points[t].alt_type = nil + self:T( 'SpawnTemplate.route.points[' .. t .. '].x = ' .. SpawnTemplate.route.points[t].x .. ', SpawnTemplate.route.points[' .. t .. '].y = ' .. SpawnTemplate.route.points[t].y ) + end + end + + return self +end + +--- Private method that randomizes the template of the group. +-- @param #SPAWN self +-- @param #number SpawnIndex +-- @return #SPAWN self +function SPAWN:_RandomizeTemplate( SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) + + if self.SpawnRandomizeTemplate then + self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix = self.SpawnTemplatePrefixTable[ math.random( 1, #self.SpawnTemplatePrefixTable ) ] + self.SpawnGroups[SpawnIndex].SpawnTemplate = self:_Prepare( self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix, SpawnIndex ) + self.SpawnGroups[SpawnIndex].SpawnTemplate.route = routines.utils.deepCopy( self.SpawnTemplate.route ) + self.SpawnGroups[SpawnIndex].SpawnTemplate.x = self.SpawnTemplate.x + self.SpawnGroups[SpawnIndex].SpawnTemplate.y = self.SpawnTemplate.y + self.SpawnGroups[SpawnIndex].SpawnTemplate.start_time = self.SpawnTemplate.start_time + for UnitID = 1, #self.SpawnGroups[SpawnIndex].SpawnTemplate.units do + self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].heading = self.SpawnTemplate.units[1].heading + end + end + + self:_RandomizeRoute( SpawnIndex ) + + return self +end + +function SPAWN:_TranslateRotate( SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle } ) + + -- Translate + local TranslatedX = SpawnX + local TranslatedY = SpawnY + + -- Rotate + -- From Wikipedia: https://en.wikipedia.org/wiki/Rotation_matrix#Common_rotations + -- x' = x \cos \theta - y \sin \theta\ + -- y' = x \sin \theta + y \cos \theta\ + local RotatedX = - TranslatedX * math.cos( math.rad( SpawnAngle ) ) + + TranslatedY * math.sin( math.rad( SpawnAngle ) ) + local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) + + TranslatedY * math.cos( math.rad( SpawnAngle ) ) + + -- Assign + self.SpawnGroups[SpawnIndex].SpawnTemplate.x = SpawnRootX - RotatedX + self.SpawnGroups[SpawnIndex].SpawnTemplate.y = SpawnRootY + RotatedY + + + local SpawnUnitCount = table.getn( self.SpawnGroups[SpawnIndex].SpawnTemplate.units ) + for u = 1, SpawnUnitCount do + + -- Translate + local TranslatedX = SpawnX + local TranslatedY = SpawnY - 10 * ( u - 1 ) + + -- Rotate + local RotatedX = - TranslatedX * math.cos( math.rad( SpawnAngle ) ) + + TranslatedY * math.sin( math.rad( SpawnAngle ) ) + local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) + + TranslatedY * math.cos( math.rad( SpawnAngle ) ) + + -- Assign + self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].x = SpawnRootX - RotatedX + self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].y = SpawnRootY + RotatedY + self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].heading = self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].heading + math.rad( SpawnAngle ) + end + + return self +end + +--- Get the next index of the groups to be spawned. This function is complicated, as it is used at several spaces. +function SPAWN:_GetSpawnIndex( SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive, self.AliveUnits, #self.SpawnTemplate.units } ) + + + if ( self.SpawnMaxGroups == 0 ) or ( SpawnIndex <= self.SpawnMaxGroups ) then + if ( self.SpawnMaxUnitsAlive == 0 ) or ( self.AliveUnits < self.SpawnMaxUnitsAlive * #self.SpawnTemplate.units ) or self.UnControlled then + if SpawnIndex and SpawnIndex >= self.SpawnCount + 1 then + self.SpawnCount = self.SpawnCount + 1 + SpawnIndex = self.SpawnCount + end + self.SpawnIndex = SpawnIndex + if not self.SpawnGroups[self.SpawnIndex] then + self:_InitializeSpawnGroups( self.SpawnIndex ) + end + else + return nil + end + else + return nil + end + + return self.SpawnIndex +end + + +-- TODO Need to delete this... _DATABASE does this now ... +function SPAWN:_OnBirth( event ) + + if timer.getTime0() < timer.getAbsTime() then -- dont need to add units spawned in at the start of the mission if mist is loaded in init line + if event.initiator and event.initiator:getName() then + local EventPrefix = self:_GetPrefixFromDCSUnit( event.initiator ) + if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then + self:T( { "Birth event: " .. event.initiator:getName(), event } ) + --MessageToAll( "Mission command: unit " .. SpawnTemplatePrefix .. " spawned." , 5, EventPrefix .. '/Event') + self.AliveUnits = self.AliveUnits + 1 + self:T( "Alive Units: " .. self.AliveUnits ) + end + end + end + +end + +--- Obscolete +-- @todo Need to delete this... _DATABASE does this now ... +function SPAWN:_OnDeadOrCrash( event ) + self:F( self.SpawnTemplatePrefix, event ) + + if event.initiator and event.initiator:getName() then + local EventPrefix = self:_GetPrefixFromDCSUnit( event.initiator ) + if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then + self:T( { "Dead event: " .. event.initiator:getName(), event } ) +-- local DestroyedUnit = Unit.getByName( EventPrefix ) +-- if DestroyedUnit and DestroyedUnit.getLife() <= 1.0 then + --MessageToAll( "Mission command: unit " .. SpawnTemplatePrefix .. " crashed." , 5, EventPrefix .. '/Event') + self.AliveUnits = self.AliveUnits - 1 + self:T( "Alive Units: " .. self.AliveUnits ) +-- end + end + end +end + +--- Will detect AIR Units taking off... When the event takes place, the spawned Group is registered as airborne... +-- This is needed to ensure that Re-SPAWNing only is done for landed AIR Groups. +-- @todo Need to test for AIR Groups only... +function SPAWN:_OnTakeOff( event ) + self:F( self.SpawnTemplatePrefix, event ) + + if event.initiator and event.initiator:getName() then + local SpawnGroup = self:_GetGroupFromDCSUnit( event.initiator ) + if SpawnGroup then + self:T( { "TakeOff event: " .. event.initiator:getName(), event } ) + self:T( "self.Landed = false" ) + self.Landed = false + end + end +end + +--- Will detect AIR Units landing... When the event takes place, the spawned Group is registered as landed. +-- This is needed to ensure that Re-SPAWNing is only done for landed AIR Groups. +-- @todo Need to test for AIR Groups only... +function SPAWN:_OnLand( event ) + self:F( self.SpawnTemplatePrefix, event ) + + local SpawnUnit = event.initiator + if SpawnUnit and SpawnUnit:isExist() and Object.getCategory(SpawnUnit) == Object.Category.UNIT then + local SpawnGroup = self:_GetGroupFromDCSUnit( SpawnUnit ) + if SpawnGroup then + self:T( { "Landed event:" .. SpawnUnit:getName(), event } ) + self.Landed = true + self:T( "self.Landed = true" ) + if self.Landed and self.RepeatOnLanding then + local SpawnGroupIndex = self:GetSpawnIndexFromGroup( SpawnGroup ) + self:T( { "Landed:", "ReSpawn:", SpawnGroup:GetName(), SpawnGroupIndex } ) + self:ReSpawn( SpawnGroupIndex ) + end + end + end +end + +--- Will detect AIR Units shutting down their engines ... +-- When the event takes place, and the method @{RepeatOnEngineShutDown} was called, the spawned Group will Re-SPAWN. +-- But only when the Unit was registered to have landed. +-- @param #SPAWN self +-- @see _OnTakeOff +-- @see _OnLand +-- @todo Need to test for AIR Groups only... +function SPAWN:_OnEngineShutDown( event ) + self:F( self.SpawnTemplatePrefix, event ) + + local SpawnUnit = event.initiator + if SpawnUnit and SpawnUnit:isExist() and Object.getCategory(SpawnUnit) == Object.Category.UNIT then + local SpawnGroup = self:_GetGroupFromDCSUnit( SpawnUnit ) + if SpawnGroup then + self:T( { "EngineShutDown event: " .. SpawnUnit:getName(), event } ) + if self.Landed and self.RepeatOnEngineShutDown then + local SpawnGroupIndex = self:GetSpawnIndexFromGroup( SpawnGroup ) + self:T( { "EngineShutDown: ", "ReSpawn:", SpawnGroup:GetName(), SpawnGroupIndex } ) + self:ReSpawn( SpawnGroupIndex ) + end + end + end +end + +--- This function is called automatically by the Spawning scheduler. +-- It is the internal worker method SPAWNing new Groups on the defined time intervals. +function SPAWN:_Scheduler() + self:F( { "_Scheduler", self.SpawnTemplatePrefix, self.SpawnAliasPrefix, self.SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive } ) + + -- Validate if there are still groups left in the batch... + self:Spawn() + + return true +end + +function SPAWN:_SpawnCleanUpScheduler() + self:F( { "CleanUp Scheduler:", self.SpawnTemplatePrefix } ) + + local SpawnCursor + local SpawnGroup, SpawnCursor = self:GetFirstAliveGroup( SpawnCursor ) + + self:T( { "CleanUp Scheduler:", SpawnGroup } ) + + while SpawnGroup do + + if SpawnGroup:AllOnGround() and SpawnGroup:GetMaxVelocity() < 1 then + if not self.SpawnCleanUpTimeStamps[SpawnGroup:GetName()] then + self.SpawnCleanUpTimeStamps[SpawnGroup:GetName()] = timer.getTime() + else + if self.SpawnCleanUpTimeStamps[SpawnGroup:GetName()] + self.SpawnCleanUpInterval < timer.getTime() then + self:T( { "CleanUp Scheduler:", "Cleaning:", SpawnGroup } ) + SpawnGroup:Destroy() + end + end + else + self.SpawnCleanUpTimeStamps[SpawnGroup:GetName()] = nil + end + + SpawnGroup, SpawnCursor = self:GetNextAliveGroup( SpawnCursor ) + + self:T( { "CleanUp Scheduler:", SpawnGroup } ) + + end + + return true -- Repeat + +end +--- Limit the simultaneous movement of Groups within a running Mission. +-- This module is defined to improve the performance in missions, and to bring additional realism for GROUND vehicles. +-- Performance: If in a DCSRTE there are a lot of moving GROUND units, then in a multi player mission, this WILL create lag if +-- the main DCS execution core of your CPU is fully utilized. So, this class will limit the amount of simultaneous moving GROUND units +-- on defined intervals (currently every minute). +-- @module MOVEMENT + +Include.File( "Routines" ) + +--- the MOVEMENT class +-- @type +MOVEMENT = { + ClassName = "MOVEMENT", +} + +--- Creates the main object which is handling the GROUND forces movement. +-- @param table{string,...}|string MovePrefixes is a table of the Prefixes (names) of the GROUND Groups that need to be controlled by the MOVEMENT Object. +-- @param number MoveMaximum is a number that defines the maximum amount of GROUND Units to be moving during one minute. +-- @return MOVEMENT +-- @usage +-- -- Limit the amount of simultaneous moving units on the ground to prevent lag. +-- Movement_US_Platoons = MOVEMENT:New( { 'US Tank Platoon Left', 'US Tank Platoon Middle', 'US Tank Platoon Right', 'US CH-47D Troops' }, 15 ) + +function MOVEMENT:New( MovePrefixes, MoveMaximum ) + local self = BASE:Inherit( self, BASE:New() ) + self:F( { MovePrefixes, MoveMaximum } ) + + if type( MovePrefixes ) == 'table' then + self.MovePrefixes = MovePrefixes + else + self.MovePrefixes = { MovePrefixes } + end + self.MoveCount = 0 -- The internal counter of the amount of Moveing the has happened since MoveStart. + self.MoveMaximum = MoveMaximum -- Contains the Maximum amount of units that are allowed to move... + self.AliveUnits = 0 -- Contains the counter how many units are currently alive + self.MoveUnits = {} -- Reflects if the Moving for this MovePrefixes is going to be scheduled or not. + + _EVENTDISPATCHER:OnBirth( self.OnBirth, self ) + +-- self:AddEvent( world.event.S_EVENT_BIRTH, self.OnBirth ) +-- +-- self:EnableEvents() + + self:ScheduleStart() + + return self +end + +--- Call this function to start the MOVEMENT scheduling. +function MOVEMENT:ScheduleStart() + self:F() + --self.MoveFunction = routines.scheduleFunction( self._Scheduler, { self }, timer.getTime() + 1, 120 ) + self.MoveFunction = SCHEDULER:New( self, self._Scheduler, {}, 1, 120 ) +end + +--- Call this function to stop the MOVEMENT scheduling. +-- @todo need to implement it ... Forgot. +function MOVEMENT:ScheduleStop() + self:F() + +end + +--- Captures the birth events when new Units were spawned. +-- @todo This method should become obsolete. The new @{DATABASE} class will handle the collection administration. +function MOVEMENT:OnBirth( Event ) + self:F( { Event } ) + + if timer.getTime0() < timer.getAbsTime() then -- dont need to add units spawned in at the start of the mission if mist is loaded in init line + if Event.IniDCSUnit then + self:T( "Birth object : " .. Event.IniDCSUnitName ) + if Event.IniDCSGroup and Event.IniDCSGroup:isExist() then + for MovePrefixID, MovePrefix in pairs( self.MovePrefixes ) do + if string.find( Event.IniDCSUnitName, MovePrefix, 1, true ) then + self.AliveUnits = self.AliveUnits + 1 + self.MoveUnits[Event.IniDCSUnitName] = Event.IniDCSGroupName + self:T( self.AliveUnits ) + end + end + end + end + _EVENTDISPATCHER:OnCrashForUnit( Event.IniDCSUnitName, self.OnDeadOrCrash, self ) + _EVENTDISPATCHER:OnDeadForUnit( Event.IniDCSUnitName, self.OnDeadOrCrash, self ) + end + +end + +--- Captures the Dead or Crash events when Units crash or are destroyed. +-- @todo This method should become obsolete. The new @{DATABASE} class will handle the collection administration. +function MOVEMENT:OnDeadOrCrash( Event ) + self:F( { Event } ) + + if Event.IniDCSUnit then + self:T( "Dead object : " .. Event.IniDCSUnitName ) + for MovePrefixID, MovePrefix in pairs( self.MovePrefixes ) do + if string.find( Event.IniDCSUnitName, MovePrefix, 1, true ) then + self.AliveUnits = self.AliveUnits - 1 + self.MoveUnits[Event.IniDCSUnitName] = nil + self:T( self.AliveUnits ) + end + end + end +end + +--- This function is called automatically by the MOVEMENT scheduler. A new function is scheduled when MoveScheduled is true. +function MOVEMENT:_Scheduler() + self:F( { self.MovePrefixes, self.MoveMaximum, self.AliveUnits, self.MovementGroups } ) + + if self.AliveUnits > 0 then + local MoveProbability = ( self.MoveMaximum * 100 ) / self.AliveUnits + self:T( 'Move Probability = ' .. MoveProbability ) + + for MovementUnitName, MovementGroupName in pairs( self.MoveUnits ) do + local MovementGroup = Group.getByName( MovementGroupName ) + if MovementGroup and MovementGroup:isExist() then + local MoveOrStop = math.random( 1, 100 ) + self:T( 'MoveOrStop = ' .. MoveOrStop ) + if MoveOrStop <= MoveProbability then + self:T( 'Group continues moving = ' .. MovementGroupName ) + trigger.action.groupContinueMoving( MovementGroup ) + else + self:T( 'Group stops moving = ' .. MovementGroupName ) + trigger.action.groupStopMoving( MovementGroup ) + end + else + self.MoveUnits[MovementUnitName] = nil + end + end + end + return true +end +--- Provides defensive behaviour to a set of SAM sites within a running Mission. +-- @module Sead +-- @author to be searched on the forum +-- @author (co) Flightcontrol (Modified and enriched with functionality) + +Include.File( "Routines" ) +Include.File( "Event" ) +Include.File( "Base" ) +Include.File( "Mission" ) +Include.File( "Client" ) +Include.File( "Task" ) + +--- The SEAD class +-- @type SEAD +-- @extends Base#BASE +SEAD = { + ClassName = "SEAD", + TargetSkill = { + Average = { Evade = 50, DelayOff = { 10, 25 }, DelayOn = { 10, 30 } } , + Good = { Evade = 30, DelayOff = { 8, 20 }, DelayOn = { 20, 40 } } , + High = { Evade = 15, DelayOff = { 5, 17 }, DelayOn = { 30, 50 } } , + Excellent = { Evade = 10, DelayOff = { 3, 10 }, DelayOn = { 30, 60 } } + }, + SEADGroupPrefixes = {} +} + +--- Creates the main object which is handling defensive actions for SA sites or moving SA vehicles. +-- When an anti radiation missile is fired (KH-58, KH-31P, KH-31A, KH-25MPU, HARM missiles), the SA will shut down their radars and will take evasive actions... +-- Chances are big that the missile will miss. +-- @param table{string,...}|string SEADGroupPrefixes which is a table of Prefixes of the SA Groups in the DCSRTE on which evasive actions need to be taken. +-- @return SEAD +-- @usage +-- -- CCCP SEAD Defenses +-- -- Defends the Russian SA installations from SEAD attacks. +-- SEAD_RU_SAM_Defenses = SEAD:New( { 'RU SA-6 Kub', 'RU SA-6 Defenses', 'RU MI-26 Troops', 'RU Attack Gori' } ) +function SEAD:New( SEADGroupPrefixes ) + local self = BASE:Inherit( self, BASE:New() ) + self:F( SEADGroupPrefixes ) + if type( SEADGroupPrefixes ) == 'table' then + for SEADGroupPrefixID, SEADGroupPrefix in pairs( SEADGroupPrefixes ) do + self.SEADGroupPrefixes[SEADGroupPrefix] = SEADGroupPrefix + end + else + self.SEADGroupNames[SEADGroupPrefixes] = SEADGroupPrefixes + end + _EVENTDISPATCHER:OnShot( self.EventShot, self ) + + return self +end + +--- Detects if an SA site was shot with an anti radiation missile. In this case, take evasive actions based on the skill level set within the ME. +-- @see SEAD +function SEAD:EventShot( Event ) + self:F( { Event } ) + + local SEADUnit = Event.IniDCSUnit + local SEADUnitName = Event.IniDCSUnitName + local SEADWeapon = Event.Weapon -- Identify the weapon fired + local SEADWeaponName = Event.WeaponName -- return weapon type + --trigger.action.outText( string.format("Alerte, depart missile " ..string.format(SEADWeaponName)), 20) --debug message + -- Start of the 2nd loop + self:T( "Missile Launched = " .. SEADWeaponName ) + if SEADWeaponName == "KH-58" or SEADWeaponName == "KH-25MPU" or SEADWeaponName == "AGM-88" or SEADWeaponName == "KH-31A" or SEADWeaponName == "KH-31P" then -- Check if the missile is a SEAD + local _evade = math.random (1,100) -- random number for chance of evading action + local _targetMim = Event.Weapon:getTarget() -- Identify target + local _targetMimname = Unit.getName(_targetMim) + local _targetMimgroup = Unit.getGroup(Weapon.getTarget(SEADWeapon)) + local _targetMimgroupName = _targetMimgroup:getName() + local _targetMimcont= _targetMimgroup:getController() + local _targetskill = _DATABASE.Templates.Units[_targetMimname].Template.skill + self:T( self.SEADGroupPrefixes ) + self:T( _targetMimgroupName ) + local SEADGroupFound = false + for SEADGroupPrefixID, SEADGroupPrefix in pairs( self.SEADGroupPrefixes ) do + if string.find( _targetMimgroupName, SEADGroupPrefix, 1, true ) then + SEADGroupFound = true + self:T( 'Group Found' ) + break + end + end + if SEADGroupFound == true then + if _targetskill == "Random" then -- when skill is random, choose a skill + local Skills = { "Average", "Good", "High", "Excellent" } + _targetskill = Skills[ math.random(1,4) ] + end + self:T( _targetskill ) -- debug message for skill check + if self.TargetSkill[_targetskill] then + if (_evade > self.TargetSkill[_targetskill].Evade) then + self:T( string.format("Evading, target skill " ..string.format(_targetskill)) ) --debug message + local _targetMim = Weapon.getTarget(SEADWeapon) + local _targetMimname = Unit.getName(_targetMim) + local _targetMimgroup = Unit.getGroup(Weapon.getTarget(SEADWeapon)) + local _targetMimcont= _targetMimgroup:getController() + routines.groupRandomDistSelf(_targetMimgroup,300,'Diamond',250,20) -- move randomly + local SuppressedGroups1 = {} -- unit suppressed radar off for a random time + local function SuppressionEnd1(id) + id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) + SuppressedGroups1[id.groupName] = nil + end + local id = { + groupName = _targetMimgroup, + ctrl = _targetMimcont + } + local delay1 = math.random(self.TargetSkill[_targetskill].DelayOff[1], self.TargetSkill[_targetskill].DelayOff[2]) + if SuppressedGroups1[id.groupName] == nil then + SuppressedGroups1[id.groupName] = { + SuppressionEndTime1 = timer.getTime() + delay1, + SuppressionEndN1 = SuppressionEndCounter1 --Store instance of SuppressionEnd() scheduled function + } + Controller.setOption(_targetMimcont, AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) + timer.scheduleFunction(SuppressionEnd1, id, SuppressedGroups1[id.groupName].SuppressionEndTime1) --Schedule the SuppressionEnd() function + --trigger.action.outText( string.format("Radar Off " ..string.format(delay1)), 20) + end + + local SuppressedGroups = {} + local function SuppressionEnd(id) + id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.RED) + SuppressedGroups[id.groupName] = nil + end + local id = { + groupName = _targetMimgroup, + ctrl = _targetMimcont + } + local delay = math.random(self.TargetSkill[_targetskill].DelayOn[1], self.TargetSkill[_targetskill].DelayOn[2]) + if SuppressedGroups[id.groupName] == nil then + SuppressedGroups[id.groupName] = { + SuppressionEndTime = timer.getTime() + delay, + SuppressionEndN = SuppressionEndCounter --Store instance of SuppressionEnd() scheduled function + } + timer.scheduleFunction(SuppressionEnd, id, SuppressedGroups[id.groupName].SuppressionEndTime) --Schedule the SuppressionEnd() function + --trigger.action.outText( string.format("Radar On " ..string.format(delay)), 20) + end + end + end + end + end +end +--- Taking the lead of AI escorting your flight. +-- +-- @{#ESCORT} class +-- ================ +-- The @{#ESCORT} class allows you to interact with escorting AI on your flight and take the lead. +-- Each escorting group can be commanded with a whole set of radio commands (radio menu in your flight, and then F10). +-- +-- The radio commands will vary according the category of the group. The richest set of commands are with Helicopters and AirPlanes. +-- Ships and Ground troops will have a more limited set, but they can provide support through the bombing of targets designated by the other escorts. +-- +-- RADIO MENUs that can be created: +-- ================================ +-- Find a summary below of the current available commands: +-- +-- Navigation ...: +-- --------------- +-- Escort group navigation functions: +-- +-- * **"Join-Up and Follow at x meters":** The escort group fill follow you at about x meters, and they will follow you. +-- * **"Flare":** Provides menu commands to let the escort group shoot a flare in the air in a color. +-- * **"Smoke":** Provides menu commands to let the escort group smoke the air in a color. Note that smoking is only available for ground and naval troops. +-- +-- Hold position ...: +-- ------------------ +-- Escort group navigation functions: +-- +-- * **"At current location":** Stops the escort group and they will hover 30 meters above the ground at the position they stopped. +-- * **"At client location":** Stops the escort group and they will hover 30 meters above the ground at the position they stopped. +-- +-- Report targets ...: +-- ------------------- +-- Report targets will make the escort group to report any target that it identifies within a 8km range. Any detected target can be attacked using the 4. Attack nearby targets function. (see below). +-- +-- * **"Report now":** Will report the current detected targets. +-- * **"Report targets on":** Will make the escort group to report detected targets and will fill the "Attack nearby targets" menu list. +-- * **"Report targets off":** Will stop detecting targets. +-- +-- Scan targets ...: +-- ----------------- +-- Menu items to pop-up the escort group for target scanning. After scanning, the escort group will resume with the mission or defined task. +-- +-- * **"Scan targets 30 seconds":** Scan 30 seconds for targets. +-- * **"Scan targets 60 seconds":** Scan 60 seconds for targets. +-- +-- Attack targets ...: +-- ------------------- +-- This menu item will list all detected targets within a 15km range. Depending on the level of detection (known/unknown) and visuality, the targets type will also be listed. +-- +-- Request assistance from ...: +-- ---------------------------- +-- This menu item will list all detected targets within a 15km range, as with the menu item **Attack Targets**. +-- This menu item allows to request attack support from other escorts supporting the current client group. +-- eg. the function allows a player to request support from the Ship escort to attack a target identified by the Plane escort with its Tomahawk missiles. +-- eg. the function allows a player to request support from other Planes escorting to bomb the unit with illumination missiles or bombs, so that the main plane escort can attack the area. +-- +-- ROE ...: +-- -------- +-- Sets the Rules of Engagement (ROE) of the escort group when in flight. +-- +-- * **"Hold Fire":** The escort group will hold fire. +-- * **"Return Fire":** The escort group will return fire. +-- * **"Open Fire":** The escort group will open fire on designated targets. +-- * **"Weapon Free":** The escort group will engage with any target. +-- +-- Evasion ...: +-- ------------ +-- Will define the evasion techniques that the escort group will perform during flight or combat. +-- +-- * **"Fight until death":** The escort group will have no reaction to threats. +-- * **"Use flares, chaff and jammers":** The escort group will use passive defense using flares and jammers. No evasive manoeuvres are executed. +-- * **"Evade enemy fire":** The rescort group will evade enemy fire before firing. +-- * **"Go below radar and evade fire":** The escort group will perform evasive vertical manoeuvres. +-- +-- Resume Mission ...: +-- ------------------- +-- Escort groups can have their own mission. This menu item will allow the escort group to resume their Mission from a given waypoint. +-- Note that this is really fantastic, as you now have the dynamic of taking control of the escort groups, and allowing them to resume their path or mission. +-- +-- ESCORT construction methods. +-- ============================ +-- Create a new SPAWN object with the @{#ESCORT.New} method: +-- +-- * @{#ESCORT.New}: Creates a new ESCORT object from a @{Group#GROUP} for a @{Client#CLIENT}, with an optional briefing text. +-- +-- ESCORT initialization methods. +-- ============================== +-- The following menus are created within the RADIO MENU of an active unit hosted by a player: +-- +-- * @{#ESCORT.MenuFollowAt}: Creates a menu to make the escort follow the client. +-- * @{#ESCORT.MenuHoldAtEscortPosition}: Creates a menu to hold the escort at its current position. +-- * @{#ESCORT.MenuHoldAtLeaderPosition}: Creates a menu to hold the escort at the client position. +-- * @{#ESCORT.MenuScanForTargets}: Creates a menu so that the escort scans targets. +-- * @{#ESCORT.MenuFlare}: Creates a menu to disperse flares. +-- * @{#ESCORT.MenuSmoke}: Creates a menu to disparse smoke. +-- * @{#ESCORT.MenuReportTargets}: Creates a menu so that the escort reports targets. +-- * @{#ESCORT.MenuReportPosition}: Creates a menu so that the escort reports its current position from bullseye. +-- * @{#ESCORT.MenuAssistedAttack: Creates a menu so that the escort supportes assisted attack from other escorts with the client. +-- * @{#ESCORT.MenuROE: Creates a menu structure to set the rules of engagement of the escort. +-- * @{#ESCORT.MenuEvasion: Creates a menu structure to set the evasion techniques when the escort is under threat. +-- * @{#ESCORT.MenuResumeMission}: Creates a menu structure so that the escort can resume from a waypoint. +-- +-- @module Escort +-- @author FlightControl + +Include.File( "Routines" ) +Include.File( "Base" ) +Include.File( "Database" ) +Include.File( "Group" ) +Include.File( "Zone" ) + +--- +-- @type ESCORT +-- @extends Base#BASE +-- @field Client#CLIENT EscortClient +-- @field Group#GROUP EscortGroup +-- @field #string EscortName +-- @field #ESCORT.MODE EscortMode The mode the escort is in. +-- @field #number FollowScheduler The id of the _FollowScheduler function. +-- @field #boolean ReportTargets If true, nearby targets are reported. +-- @Field DCSTypes#AI.Option.Air.val.ROE OptionROE Which ROE is set to the EscortGroup. +-- @field DCSTypes#AI.Option.Air.val.REACTION_ON_THREAT OptionReactionOnThreat Which REACTION_ON_THREAT is set to the EscortGroup. +-- @field Menu#MENU_CLIENT EscortMenuResumeMission +ESCORT = { + ClassName = "ESCORT", + EscortName = nil, -- The Escort Name + EscortClient = nil, + EscortGroup = nil, + EscortMode = nil, + 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, + TaskPoints = {} +} + +--- ESCORT.Mode class +-- @type ESCORT.MODE +-- @field #number FOLLOW +-- @field #number MISSION + +--- MENUPARAM type +-- @type MENUPARAM +-- @field #ESCORT ParamSelf +-- @field #Distance ParamDistance +-- @field #function ParamFunction +-- @field #string ParamMessage + +--- ESCORT class constructor for an AI group +-- @param #ESCORT self +-- @param Client#CLIENT EscortClient The client escorted by the EscortGroup. +-- @param Group#GROUP EscortGroup The group AI escorting the EscortClient. +-- @param #string EscortName Name of the escort. +-- @return #ESCORT self +function ESCORT:New( EscortClient, EscortGroup, EscortName, EscortBriefing ) + local self = BASE:Inherit( self, BASE:New() ) + self:F( { EscortClient, EscortGroup, EscortName } ) + + self.EscortClient = EscortClient -- Client#CLIENT + self.EscortGroup = EscortGroup -- Group#GROUP + self.EscortName = EscortName + self.EscortBriefing = EscortBriefing + + self:T( EscortGroup:GetClassNameAndID() ) + + -- Set EscortGroup known at EscortClient. + if not self.EscortClient._EscortGroups then + self.EscortClient._EscortGroups = {} + end + + if not self.EscortClient._EscortGroups[EscortGroup:GetName()] then + self.EscortClient._EscortGroups[EscortGroup:GetName()] = {} + self.EscortClient._EscortGroups[EscortGroup:GetName()].EscortGroup = self.EscortGroup + self.EscortClient._EscortGroups[EscortGroup:GetName()].EscortName = self.EscortName + self.EscortClient._EscortGroups[EscortGroup:GetName()].Targets = {} + self.EscortMode = ESCORT.MODE.FOLLOW + end + + + self.EscortMenu = MENU_CLIENT:New( self.EscortClient, self.EscortName ) + + self.EscortGroup:WayPointInitialize(1) + + self.EscortGroup:OptionROTVertical() + self.EscortGroup:OptionROEOpenFire() + + EscortGroup:MessageToClient( EscortGroup:GetCategoryName() .. " '" .. EscortName .. "' (" .. EscortGroup:GetCallsign() .. ") reporting! " .. + "We're escorting your flight. " .. + "Use the Radio Menu and F10 and use the options under + " .. EscortName .. "\n", + 60, EscortClient + ) + + return self +end + + +--- Defines the default menus +-- @param #ESCORT self +-- @return #ESCORT +function ESCORT:Menus() + self:F() + + self:MenuFollowAt( 100 ) + self:MenuFollowAt( 200 ) + self:MenuFollowAt( 300 ) + self:MenuFollowAt( 400 ) + + self:MenuScanForTargets( 100, 60 ) + + self:MenuHoldAtEscortPosition( 30 ) + self:MenuHoldAtLeaderPosition( 30 ) + + self:MenuFlare() + self:MenuSmoke() + + self:MenuReportTargets( 60 ) + self:MenuAssistedAttack() + self:MenuROE() + self:MenuEvasion() + self:MenuResumeMission() + + return self +end + + + +--- Defines a menu slot to let the escort Join and Follow you at a certain distance. +-- This menu will appear under **Navigation**. +-- @param #ESCORT self +-- @param DCSTypes#Distance Distance The distance in meters that the escort needs to follow the client. +-- @return #ESCORT +function ESCORT:MenuFollowAt( Distance ) + self:F(Distance) + + if self.EscortGroup:IsAir() then + if not self.EscortMenuReportNavigation then + self.EscortMenuReportNavigation = MENU_CLIENT:New( self.EscortClient, "Navigation", self.EscortMenu ) + end + + if not self.EscortMenuJoinUpAndFollow then + self.EscortMenuJoinUpAndFollow = {} + end + + self.EscortMenuJoinUpAndFollow[#self.EscortMenuJoinUpAndFollow+1] = MENU_CLIENT_COMMAND:New( self.EscortClient, "Join-Up and Follow at " .. Distance, self.EscortMenuReportNavigation, ESCORT._JoinUpAndFollow, { ParamSelf = self, ParamDistance = Distance } ) + + self.EscortMode = ESCORT.MODE.FOLLOW + end + + return self +end + +--- Defines a menu slot to let the escort hold at their current position and stay low with a specified height during a specified time in seconds. +-- This menu will appear under **Hold position**. +-- @param #ESCORT self +-- @param DCSTypes#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. +-- @param DCSTypes#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. +-- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. +-- @return #ESCORT +-- TODO: Implement Seconds parameter. Challenge is to first develop the "continue from last activity" function. +function ESCORT:MenuHoldAtEscortPosition( Height, Seconds, MenuTextFormat ) + self:F( { Height, Seconds, MenuTextFormat } ) + + if self.EscortGroup:IsAir() then + + if not self.EscortMenuHold then + self.EscortMenuHold = MENU_CLIENT:New( self.EscortClient, "Hold position", self.EscortMenu ) + end + + if not Height then + Height = 30 + end + + if not Seconds then + Seconds = 0 + end + + local MenuText = "" + if not MenuTextFormat then + if Seconds == 0 then + MenuText = string.format( "Hold at %d meter", Height ) + else + MenuText = string.format( "Hold at %d meter for %d seconds", Height, Seconds ) + end + else + if Seconds == 0 then + MenuText = string.format( MenuTextFormat, Height ) + else + MenuText = string.format( MenuTextFormat, Height, Seconds ) + end + end + + if not self.EscortMenuHoldPosition then + self.EscortMenuHoldPosition = {} + end + + self.EscortMenuHoldPosition[#self.EscortMenuHoldPosition+1] = MENU_CLIENT_COMMAND + :New( + self.EscortClient, + MenuText, + self.EscortMenuHold, + ESCORT._HoldPosition, + { ParamSelf = self, + ParamOrbitGroup = self.EscortGroup, + ParamHeight = Height, + ParamSeconds = Seconds + } + ) + end + + return self +end + + +--- Defines a menu slot to let the escort hold at the client position and stay low with a specified height during a specified time in seconds. +-- This menu will appear under **Navigation**. +-- @param #ESCORT self +-- @param DCSTypes#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. +-- @param DCSTypes#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. +-- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain one or two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. +-- @return #ESCORT +-- TODO: Implement Seconds parameter. Challenge is to first develop the "continue from last activity" function. +function ESCORT:MenuHoldAtLeaderPosition( Height, Seconds, MenuTextFormat ) + self:F( { Height, Seconds, MenuTextFormat } ) + + if self.EscortGroup:IsAir() then + + if not self.EscortMenuHold then + self.EscortMenuHold = MENU_CLIENT:New( self.EscortClient, "Hold position", self.EscortMenu ) + end + + if not Height then + Height = 30 + end + + if not Seconds then + Seconds = 0 + end + + local MenuText = "" + if not MenuTextFormat then + if Seconds == 0 then + MenuText = string.format( "Rejoin and hold at %d meter", Height ) + else + MenuText = string.format( "Rejoin and hold at %d meter for %d seconds", Height, Seconds ) + end + else + if Seconds == 0 then + MenuText = string.format( MenuTextFormat, Height ) + else + MenuText = string.format( MenuTextFormat, Height, Seconds ) + end + end + + if not self.EscortMenuHoldAtLeaderPosition then + self.EscortMenuHoldAtLeaderPosition = {} + end + + self.EscortMenuHoldAtLeaderPosition[#self.EscortMenuHoldAtLeaderPosition+1] = MENU_CLIENT_COMMAND + :New( + self.EscortClient, + MenuText, + self.EscortMenuHold, + ESCORT._HoldPosition, + { ParamSelf = self, + ParamOrbitGroup = self.EscortClient, + ParamHeight = Height, + ParamSeconds = Seconds + } + ) + end + + return self +end + +--- Defines a menu slot to let the escort scan for targets at a certain height for a certain time in seconds. +-- This menu will appear under **Scan targets**. +-- @param #ESCORT self +-- @param DCSTypes#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. +-- @param DCSTypes#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. +-- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain one or two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. +-- @return #ESCORT +function ESCORT:MenuScanForTargets( Height, Seconds, MenuTextFormat ) + self:F( { Height, Seconds, MenuTextFormat } ) + + if self.EscortGroup:IsAir() then + if not self.EscortMenuScan then + self.EscortMenuScan = MENU_CLIENT:New( self.EscortClient, "Scan for targets", self.EscortMenu ) + end + + if not Height then + Height = 100 + end + + if not Seconds then + Seconds = 30 + end + + local MenuText = "" + if not MenuTextFormat then + if Seconds == 0 then + MenuText = string.format( "At %d meter", Height ) + else + MenuText = string.format( "At %d meter for %d seconds", Height, Seconds ) + end + else + if Seconds == 0 then + MenuText = string.format( MenuTextFormat, Height ) + else + MenuText = string.format( MenuTextFormat, Height, Seconds ) + end + end + + if not self.EscortMenuScanForTargets then + self.EscortMenuScanForTargets = {} + end + + self.EscortMenuScanForTargets[#self.EscortMenuScanForTargets+1] = MENU_CLIENT_COMMAND + :New( + self.EscortClient, + MenuText, + self.EscortMenuScan, + ESCORT._ScanTargets, + { ParamSelf = self, + ParamScanDuration = 30 + } + ) + end + + return self +end + + + +--- Defines a menu slot to let the escort disperse a flare in a certain color. +-- This menu will appear under **Navigation**. +-- The flare will be fired from the first unit in the group. +-- @param #ESCORT self +-- @param #string MenuTextFormat Optional parameter that shows the menu option text. If no text is given, the default text will be displayed. +-- @return #ESCORT +function ESCORT:MenuFlare( MenuTextFormat ) + self:F() + + if not self.EscortMenuReportNavigation then + self.EscortMenuReportNavigation = MENU_CLIENT:New( self.EscortClient, "Navigation", self.EscortMenu ) + end + + local MenuText = "" + if not MenuTextFormat then + MenuText = "Flare" + else + MenuText = MenuTextFormat + end + + if not self.EscortMenuFlare then + self.EscortMenuFlare = MENU_CLIENT:New( self.EscortClient, MenuText, self.EscortMenuReportNavigation, ESCORT._Flare, { ParamSelf = self } ) + self.EscortMenuFlareGreen = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release green flare", self.EscortMenuFlare, ESCORT._Flare, { ParamSelf = self, ParamColor = UNIT.FlareColor.Green, ParamMessage = "Released a green flare!" } ) + self.EscortMenuFlareRed = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release red flare", self.EscortMenuFlare, ESCORT._Flare, { ParamSelf = self, ParamColor = UNIT.FlareColor.Red, ParamMessage = "Released a red flare!" } ) + self.EscortMenuFlareWhite = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release white flare", self.EscortMenuFlare, ESCORT._Flare, { ParamSelf = self, ParamColor = UNIT.FlareColor.White, ParamMessage = "Released a white flare!" } ) + self.EscortMenuFlareYellow = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release yellow flare", self.EscortMenuFlare, ESCORT._Flare, { ParamSelf = self, ParamColor = UNIT.FlareColor.Yellow, ParamMessage = "Released a yellow flare!" } ) + end + + return self +end + +--- Defines a menu slot to let the escort disperse a smoke in a certain color. +-- This menu will appear under **Navigation**. +-- Note that smoke menu options will only be displayed for ships and ground units. Not for air units. +-- The smoke will be fired from the first unit in the group. +-- @param #ESCORT self +-- @param #string MenuTextFormat Optional parameter that shows the menu option text. If no text is given, the default text will be displayed. +-- @return #ESCORT +function ESCORT:MenuSmoke( MenuTextFormat ) + self:F() + + if not self.EscortGroup:IsAir() then + if not self.EscortMenuReportNavigation then + self.EscortMenuReportNavigation = MENU_CLIENT:New( self.EscortClient, "Navigation", self.EscortMenu ) + end + + local MenuText = "" + if not MenuTextFormat then + MenuText = "Smoke" + else + MenuText = MenuTextFormat + end + + if not self.EscortMenuSmoke then + self.EscortMenuSmoke = MENU_CLIENT:New( self.EscortClient, "Smoke", self.EscortMenuReportNavigation, ESCORT._Smoke, { ParamSelf = self } ) + self.EscortMenuSmokeGreen = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release green smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.Green, ParamMessage = "Releasing green smoke!" } ) + self.EscortMenuSmokeRed = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release red smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.Red, ParamMessage = "Releasing red smoke!" } ) + self.EscortMenuSmokeWhite = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release white smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.White, ParamMessage = "Releasing white smoke!" } ) + self.EscortMenuSmokeOrange = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release orange smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.Orange, ParamMessage = "Releasing orange smoke!" } ) + self.EscortMenuSmokeBlue = MENU_CLIENT_COMMAND:New( self.EscortClient, "Release blue smoke", self.EscortMenuSmoke, ESCORT._Smoke, { ParamSelf = self, ParamColor = UNIT.SmokeColor.Blue, ParamMessage = "Releasing blue smoke!" } ) + end + end + + return self +end + +--- Defines a menu slot to let the escort report their current detected targets with a specified time interval in seconds. +-- This menu will appear under **Report targets**. +-- Note that if a report targets menu is not specified, no targets will be detected by the escort, and the attack and assisted attack menus will not be displayed. +-- @param #ESCORT self +-- @param DCSTypes#Time Seconds Optional parameter that lets the escort report their current detected targets after specified time interval in seconds. The default time is 30 seconds. +-- @return #ESCORT +function ESCORT:MenuReportTargets( Seconds ) + self:F( { Seconds } ) + + if not self.EscortMenuReportNearbyTargets then + self.EscortMenuReportNearbyTargets = MENU_CLIENT:New( self.EscortClient, "Report targets", self.EscortMenu ) + end + + if not Seconds then + Seconds = 30 + end + + -- Report Targets + self.EscortMenuReportNearbyTargetsNow = MENU_CLIENT_COMMAND:New( self.EscortClient, "Report targets now!", self.EscortMenuReportNearbyTargets, ESCORT._ReportNearbyTargetsNow, { ParamSelf = self } ) + self.EscortMenuReportNearbyTargetsOn = MENU_CLIENT_COMMAND:New( self.EscortClient, "Report targets on", self.EscortMenuReportNearbyTargets, ESCORT._SwitchReportNearbyTargets, { ParamSelf = self, ParamReportTargets = true } ) + self.EscortMenuReportNearbyTargetsOff = MENU_CLIENT_COMMAND:New( self.EscortClient, "Report targets off", self.EscortMenuReportNearbyTargets, ESCORT._SwitchReportNearbyTargets, { ParamSelf = self, ParamReportTargets = false, } ) + + -- Attack Targets + self.EscortMenuAttackNearbyTargets = MENU_CLIENT:New( self.EscortClient, "Attack targets", self.EscortMenu ) + + + --self.ReportTargetsScheduler = routines.scheduleFunction( self._ReportTargetsScheduler, { self }, timer.getTime() + 1, Seconds ) + self.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, {}, 1, Seconds ) + + return self +end + +--- Defines a menu slot to let the escort attack its detected targets using assisted attack from another escort joined also with the client. +-- This menu will appear under **Request assistance from**. +-- Note that this method needs to be preceded with the method MenuReportTargets. +-- @param #ESCORT self +-- @return #ESCORT +function ESCORT:MenuAssistedAttack() + self:F() + + -- Request assistance from other escorts. + -- This is very useful to let f.e. an escorting ship attack a target detected by an escorting plane... + self.EscortMenuTargetAssistance = MENU_CLIENT:New( self.EscortClient, "Request assistance from", self.EscortMenu ) + + return self +end + +--- Defines a menu to let the escort set its rules of engagement. +-- All rules of engagement will appear under the menu **ROE**. +-- @param #ESCORT self +-- @return #ESCORT +function ESCORT:MenuROE( MenuTextFormat ) + self:F( MenuTextFormat ) + + if not self.EscortMenuROE then + -- Rules of Engagement + self.EscortMenuROE = MENU_CLIENT:New( self.EscortClient, "ROE", self.EscortMenu ) + if self.EscortGroup:OptionROEHoldFirePossible() then + self.EscortMenuROEHoldFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Hold Fire", self.EscortMenuROE, ESCORT._ROE, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROEHoldFire(), ParamMessage = "Holding weapons!" } ) + end + if self.EscortGroup:OptionROEReturnFirePossible() then + self.EscortMenuROEReturnFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Return Fire", self.EscortMenuROE, ESCORT._ROE, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROEReturnFire(), ParamMessage = "Returning fire!" } ) + end + if self.EscortGroup:OptionROEOpenFirePossible() then + self.EscortMenuROEOpenFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Open Fire", self.EscortMenuROE, ESCORT._ROE, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROEOpenFire(), ParamMessage = "Opening fire on designated targets!!" } ) + end + if self.EscortGroup:OptionROEWeaponFreePossible() then + self.EscortMenuROEWeaponFree = MENU_CLIENT_COMMAND:New( self.EscortClient, "Weapon Free", self.EscortMenuROE, ESCORT._ROE, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROEWeaponFree(), ParamMessage = "Opening fire on targets of opportunity!" } ) + end + end + + return self +end + + +--- Defines a menu to let the escort set its evasion when under threat. +-- All rules of engagement will appear under the menu **Evasion**. +-- @param #ESCORT self +-- @return #ESCORT +function ESCORT:MenuEvasion( MenuTextFormat ) + self:F( MenuTextFormat ) + + if self.EscortGroup:IsAir() then + if not self.EscortMenuEvasion then + -- Reaction to Threats + self.EscortMenuEvasion = MENU_CLIENT:New( self.EscortClient, "Evasion", self.EscortMenu ) + if self.EscortGroup:OptionROTNoReactionPossible() then + self.EscortMenuEvasionNoReaction = MENU_CLIENT_COMMAND:New( self.EscortClient, "Fight until death", self.EscortMenuEvasion, ESCORT._ROT, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROTNoReaction(), ParamMessage = "Fighting until death!" } ) + end + if self.EscortGroup:OptionROTPassiveDefensePossible() then + self.EscortMenuEvasionPassiveDefense = MENU_CLIENT_COMMAND:New( self.EscortClient, "Use flares, chaff and jammers", self.EscortMenuEvasion, ESCORT._ROT, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROTPassiveDefense(), ParamMessage = "Defending using jammers, chaff and flares!" } ) + end + if self.EscortGroup:OptionROTEvadeFirePossible() then + self.EscortMenuEvasionEvadeFire = MENU_CLIENT_COMMAND:New( self.EscortClient, "Evade enemy fire", self.EscortMenuEvasion, ESCORT._ROT, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROTEvadeFire(), ParamMessage = "Evading on enemy fire!" } ) + end + if self.EscortGroup:OptionROTVerticalPossible() then + self.EscortMenuOptionEvasionVertical = MENU_CLIENT_COMMAND:New( self.EscortClient, "Go below radar and evade fire", self.EscortMenuEvasion, ESCORT._ROT, { ParamSelf = self, ParamFunction = self.EscortGroup:OptionROTVertical(), ParamMessage = "Evading on enemy fire with vertical manoeuvres!" } ) + end + end + end + + return self +end + +--- Defines a menu to let the escort resume its mission from a waypoint on its route. +-- All rules of engagement will appear under the menu **Resume mission from**. +-- @param #ESCORT self +-- @return #ESCORT +function ESCORT:MenuResumeMission() + self:F() + + if not self.EscortMenuResumeMission then + -- Mission Resume Menu Root + self.EscortMenuResumeMission = MENU_CLIENT:New( self.EscortClient, "Resume mission from", self.EscortMenu ) + end + + return self +end + + +--- @param #MENUPARAM MenuParam +function ESCORT._HoldPosition( MenuParam ) + + local self = MenuParam.ParamSelf + local EscortGroup = self.EscortGroup + local EscortClient = self.EscortClient + + local OrbitGroup = MenuParam.ParamOrbitGroup -- Group#GROUP + local OrbitUnit = OrbitGroup:GetUnit(1) -- Unit#UNIT + local OrbitHeight = MenuParam.ParamHeight + local OrbitSeconds = MenuParam.ParamSeconds -- Not implemented yet + + routines.removeFunction( self.FollowScheduler ) + + local PointFrom = {} + local GroupPoint = EscortGroup:GetUnit(1):GetPointVec3() + PointFrom = {} + PointFrom.x = GroupPoint.x + PointFrom.y = GroupPoint.z + PointFrom.speed = 250 + PointFrom.type = AI.Task.WaypointType.TURNING_POINT + PointFrom.alt = GroupPoint.y + PointFrom.alt_type = AI.Task.AltitudeType.BARO + + local OrbitPoint = OrbitUnit:GetPointVec2() + local PointTo = {} + PointTo.x = OrbitPoint.x + PointTo.y = OrbitPoint.y + PointTo.speed = 250 + PointTo.type = AI.Task.WaypointType.TURNING_POINT + PointTo.alt = OrbitHeight + PointTo.alt_type = AI.Task.AltitudeType.BARO + PointTo.task = EscortGroup:TaskOrbitCircleAtVec2( OrbitPoint, OrbitHeight, 0 ) + + local Points = { PointFrom, PointTo } + + EscortGroup:OptionROEHoldFire() + EscortGroup:OptionROTPassiveDefense() + + EscortGroup:SetTask( EscortGroup:TaskRoute( Points ) ) + EscortGroup:MessageToClient( "Orbiting at location.", 10, EscortClient ) +end + +--- @param #MENUPARAM MenuParam +function ESCORT._JoinUpAndFollow( MenuParam ) + + local self = MenuParam.ParamSelf + local EscortGroup = self.EscortGroup + local EscortClient = self.EscortClient + + self.Distance = MenuParam.ParamDistance + + self:JoinUpAndFollow( EscortGroup, EscortClient, self.Distance ) +end + +--- JoinsUp and Follows a CLIENT. +-- @param Escort#ESCORT self +-- @param Group#GROUP EscortGroup +-- @param Client#CLIENT EscortClient +-- @param DCSTypes#Distance Distance +function ESCORT:JoinUpAndFollow( EscortGroup, EscortClient, Distance ) + self:F( { EscortGroup, EscortClient, Distance } ) + + if self.FollowScheduler then + routines.removeFunction( self.FollowScheduler ) + end + + EscortGroup:OptionROEHoldFire() + EscortGroup:OptionROTPassiveDefense() + + self.EscortMode = ESCORT.MODE.FOLLOW + + self.CT1 = 0 + self.GT1 = 0 + --self.FollowScheduler = routines.scheduleFunction( self._FollowScheduler, { self, Distance }, timer.getTime() + 1, .5 ) + self.FollowScheduler = SCHEDULER:New( self, self._FollowScheduler, { Distance }, 1, .5, .1 ) + EscortGroup:MessageToClient( "Rejoining and Following at " .. Distance .. "!", 30, EscortClient ) +end + +--- @param #MENUPARAM MenuParam +function ESCORT._Flare( MenuParam ) + + local self = MenuParam.ParamSelf + local EscortGroup = self.EscortGroup + local EscortClient = self.EscortClient + + local Color = MenuParam.ParamColor + local Message = MenuParam.ParamMessage + + EscortGroup:GetUnit(1):Flare( Color ) + EscortGroup:MessageToClient( Message, 10, EscortClient ) +end + +--- @param #MENUPARAM MenuParam +function ESCORT._Smoke( MenuParam ) + + local self = MenuParam.ParamSelf + local EscortGroup = self.EscortGroup + local EscortClient = self.EscortClient + + local Color = MenuParam.ParamColor + local Message = MenuParam.ParamMessage + + EscortGroup:GetUnit(1):Smoke( Color ) + EscortGroup:MessageToClient( Message, 10, EscortClient ) +end + + +--- @param #MENUPARAM MenuParam +function ESCORT._ReportNearbyTargetsNow( MenuParam ) + + local self = MenuParam.ParamSelf + local EscortGroup = self.EscortGroup + local EscortClient = self.EscortClient + + self:_ReportTargetsScheduler() + +end + +function ESCORT._SwitchReportNearbyTargets( MenuParam ) + + local self = MenuParam.ParamSelf + local EscortGroup = self.EscortGroup + local EscortClient = self.EscortClient + + self.ReportTargets = MenuParam.ParamReportTargets + + if self.ReportTargets then + if not self.ReportTargetsScheduler then + --self.ReportTargetsScheduler = routines.scheduleFunction( self._ReportTargetsScheduler, { self }, timer.getTime() + 1, 30 ) + self.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, {}, 1, 30 ) + end + else + routines.removeFunction( self.ReportTargetsScheduler ) + self.ReportTargetsScheduler = nil + end +end + +--- @param #MENUPARAM MenuParam +function ESCORT._ScanTargets( MenuParam ) + + local self = MenuParam.ParamSelf + local EscortGroup = self.EscortGroup + local EscortClient = self.EscortClient + + local ScanDuration = MenuParam.ParamScanDuration + + if self.FollowScheduler then + routines.removeFunction( self.FollowScheduler ) + end + + self:T( { "FollowScheduler after removefunction: ", self.FollowScheduler } ) + + if EscortGroup:IsHelicopter() then + SCHEDULER:New( EscortGroup, EscortGroup.PushTask, + { EscortGroup:TaskControlled( + EscortGroup:TaskOrbitCircle( 200, 20 ), + EscortGroup:TaskCondition( nil, nil, nil, nil, ScanDuration, nil ) + ) + }, + 1 + ) + elseif EscortGroup:IsAirPlane() then + SCHEDULER:New( EscortGroup, EscortGroup.PushTask, + { EscortGroup:TaskControlled( + EscortGroup:TaskOrbitCircle( 1000, 500 ), + EscortGroup:TaskCondition( nil, nil, nil, nil, ScanDuration, nil ) + ) + }, + 1 + ) + end + + EscortGroup:MessageToClient( "Scanning targets for " .. ScanDuration .. " seconds.", ScanDuration, EscortClient ) + + if self.EscortMode == ESCORT.MODE.FOLLOW then + --self.FollowScheduler = routines.scheduleFunction( self._FollowScheduler, { self, Distance }, timer.getTime() + ScanDuration, 1 ) + self.FollowScheduler:Start() + end + +end + +function _Resume( EscortGroup ) + env.info( '_Resume' ) + + local Escort = EscortGroup.Escort -- #ESCORT + env.info( "EscortMode = " .. Escort.EscortMode ) + if Escort.EscortMode == ESCORT.MODE.FOLLOW then + Escort:JoinUpAndFollow( EscortGroup, Escort.EscortClient, Escort.Distance ) + end + +end + +--- @param #MENUPARAM MenuParam +function ESCORT._AttackTarget( MenuParam ) + + local self = MenuParam.ParamSelf + local EscortGroup = self.EscortGroup + local EscortClient = self.EscortClient + local AttackUnit = MenuParam.ParamUnit -- Unit#UNIT + + if self.FollowScheduler then + routines.removeFunction( self.FollowScheduler ) + end + + self:T( AttackUnit ) + + if EscortGroup:IsAir() then + EscortGroup:OptionROEOpenFire() + EscortGroup:OptionROTPassiveDefense() + EscortGroup.Escort = self -- Need to do this trick to get the reference for the escort in the _Resume function. +-- routines.scheduleFunction( +-- EscortGroup.PushTask, +-- { EscortGroup, +-- EscortGroup:TaskCombo( +-- { EscortGroup:TaskAttackUnit( AttackUnit ), +-- EscortGroup:TaskFunction( 1, 2, "_Resume", {"''"} ) +-- } +-- ) +-- }, timer.getTime() + 10 +-- ) + SCHEDULER:New( EscortGroup, + EscortGroup.PushTask, + { EscortGroup:TaskCombo( + { EscortGroup:TaskAttackUnit( AttackUnit ), + EscortGroup:TaskFunction( 1, 2, "_Resume", {"''"} ) + } + ) + }, 10 + ) + else +-- routines.scheduleFunction( +-- EscortGroup.PushTask, +-- { EscortGroup, +-- EscortGroup:TaskCombo( +-- { EscortGroup:TaskFireAtPoint( AttackUnit:GetPointVec2(), 50 ) +-- } +-- ) +-- }, timer.getTime() + 10 +-- ) + SCHEDULER:New( EscortGroup, + EscortGroup.PushTask, + { EscortGroup:TaskCombo( + { EscortGroup:TaskFireAtPoint( AttackUnit:GetPointVec2(), 50 ) + } + ) + }, 10 + ) + end + EscortGroup:MessageToClient( "Engaging Designated Unit!", 10, EscortClient ) + + +end + +--- @param #MENUPARAM MenuParam +function ESCORT._AssistTarget( MenuParam ) + + local self = MenuParam.ParamSelf + local EscortGroup = self.EscortGroup + local EscortClient = self.EscortClient + local EscortGroupAttack = MenuParam.ParamEscortGroup + local AttackUnit = MenuParam.ParamUnit -- Unit#UNIT + + if self.FollowScheduler then + routines.removeFunction( self.FollowScheduler ) + end + + + self:T( AttackUnit ) + + if EscortGroupAttack:IsAir() then + EscortGroupAttack:OptionROEOpenFire() + EscortGroupAttack:OptionROTVertical() +-- routines.scheduleFunction( +-- EscortGroupAttack.PushTask, +-- { EscortGroupAttack, +-- EscortGroupAttack:TaskCombo( +-- { EscortGroupAttack:TaskAttackUnit( AttackUnit ), +-- EscortGroupAttack:TaskOrbitCircle( 500, 350 ) +-- } +-- ) +-- }, timer.getTime() + 10 +-- ) + SCHDULER:New( EscortGroupAttack, + EscortGroupAttack.PushTask, + { EscortGroupAttack:TaskCombo( + { EscortGroupAttack:TaskAttackUnit( AttackUnit ), + EscortGroupAttack:TaskOrbitCircle( 500, 350 ) + } + ) + }, 10 + ) + else +-- routines.scheduleFunction( +-- EscortGroupAttack.PushTask, +-- { EscortGroupAttack, +-- EscortGroupAttack:TaskCombo( +-- { EscortGroupAttack:TaskFireAtPoint( AttackUnit:GetPointVec2(), 50 ) +-- } +-- ) +-- }, timer.getTime() + 10 +-- ) + SCHEDULER:New( EscortGroupAttack, + EscortGroupAttack.PushTask, + { EscortGroupAttack:TaskCombo( + { EscortGroupAttack:TaskFireAtPoint( AttackUnit:GetPointVec2(), 50 ) + } + ) + }, 10 + ) + end + EscortGroupAttack:MessageToClient( "Assisting with the destroying the enemy unit!", 10, EscortClient ) + +end + +--- @param #MENUPARAM MenuParam +function ESCORT._ROE( MenuParam ) + + local self = MenuParam.ParamSelf + local EscortGroup = self.EscortGroup + local EscortClient = self.EscortClient + + local EscortROEFunction = MenuParam.ParamFunction + local EscortROEMessage = MenuParam.ParamMessage + + pcall( function() EscortROEFunction() end ) + EscortGroup:MessageToClient( EscortROEMessage, 10, EscortClient ) +end + +--- @param #MENUPARAM MenuParam +function ESCORT._ROT( MenuParam ) + + local self = MenuParam.ParamSelf + local EscortGroup = self.EscortGroup + local EscortClient = self.EscortClient + + local EscortROTFunction = MenuParam.ParamFunction + local EscortROTMessage = MenuParam.ParamMessage + + pcall( function() EscortROTFunction() end ) + EscortGroup:MessageToClient( EscortROTMessage, 10, EscortClient ) +end + +--- @param #MENUPARAM MenuParam +function ESCORT._ResumeMission( MenuParam ) + + local self = MenuParam.ParamSelf + local EscortGroup = self.EscortGroup + local EscortClient = self.EscortClient + + local WayPoint = MenuParam.ParamWayPoint + + routines.removeFunction( self.FollowScheduler ) + self.FollowScheduler = nil + + local WayPoints = EscortGroup:GetTaskRoute() + self:T( WayPoint, WayPoints ) + + for WayPointIgnore = 1, WayPoint do + table.remove( WayPoints, 1 ) + end + + --routines.scheduleFunction( EscortGroup.SetTask, {EscortGroup, EscortGroup:TaskRoute( WayPoints ) }, timer.getTime() + 1 ) + SCHEDULER:New( EscortGroup, EscortGroup.SetTask, { EscortGroup:TaskRoute( WayPoints ) }, 1 ) + + EscortGroup:MessageToClient( "Resuming mission from waypoint " .. WayPoint .. ".", 10, EscortClient ) +end + +--- Registers the waypoints +-- @param #ESCORT self +-- @return #table +function ESCORT:RegisterRoute() + self:F() + + local EscortGroup = self.EscortGroup -- Group#GROUP + + local TaskPoints = EscortGroup:GetTaskRoute() + + self:T( TaskPoints ) + + return TaskPoints +end + +--- @param Escort#ESCORT self +function ESCORT:_FollowScheduler( FollowDistance ) + self:F( { FollowDistance }) + + if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then + + local ClientUnit = self.EscortClient:GetClientGroupUnit() + local GroupUnit = self.EscortGroup:GetUnit( 1 ) + + if self.CT1 == 0 and self.GT1 == 0 then + self.CV1 = ClientUnit:GetPointVec3() + self.CT1 = timer.getTime() + self.GV1 = GroupUnit:GetPointVec3() + self.GT1 = timer.getTime() + else + local CT1 = self.CT1 + local CT2 = timer.getTime() + local CV1 = self.CV1 + local CV2 = ClientUnit:GetPointVec3() + self.CT1 = CT2 + self.CV1 = CV2 + + local CD = ( ( CV2.x - CV1.x )^2 + ( CV2.y - CV1.y )^2 + ( CV2.z - CV1.z )^2 ) ^ 0.5 + local CT = CT2 - CT1 + + local CS = ( 3600 / CT ) * ( CD / 1000 ) + + self:T2( { "Client:", CS, CD, CT, CV2, CV1, CT2, CT1 } ) + + local GT1 = self.GT1 + local GT2 = timer.getTime() + local GV1 = self.GV1 + local GV2 = GroupUnit:GetPointVec3() + self.GT1 = GT2 + self.GV1 = GV2 + + local GD = ( ( GV2.x - GV1.x )^2 + ( GV2.y - GV1.y )^2 + ( GV2.z - GV1.z )^2 ) ^ 0.5 + local GT = GT2 - GT1 + + local GS = ( 3600 / GT ) * ( GD / 1000 ) + + self:T2( { "Group:", GS, GD, GT, GV2, GV1, GT2, GT1 } ) + + -- Calculate the group direction vector + local GV = { x = GV2.x - CV2.x, y = GV2.y - CV2.y, z = GV2.z - CV2.z } + + -- Calculate GH2, GH2 with the same height as CV2. + local GH2 = { x = GV2.x, y = CV2.y, z = GV2.z } + + -- Calculate the angle of GV to the orthonormal plane + local alpha = math.atan2( GV.z, GV.x ) + + -- Now we calculate the intersecting vector between the circle around CV2 with radius FollowDistance and GH2. + -- From the GeoGebra model: CVI = (x(CV2) + FollowDistance cos(alpha), y(GH2) + FollowDistance sin(alpha), z(CV2)) + local CVI = { x = CV2.x + FollowDistance * math.cos(alpha), + y = GH2.y, + z = CV2.z + FollowDistance * math.sin(alpha), + } + + -- Calculate the direction vector DV of the escort group. We use CVI as the base and CV2 as the direction. + local DV = { x = CV2.x - CVI.x, y = CV2.y - CVI.y, z = CV2.z - CVI.z } + + -- We now calculate the unary direction vector DVu, so that we can multiply DVu with the speed, which is expressed in meters / s. + -- We need to calculate this vector to predict the point the escort group needs to fly to according its speed. + -- The distance of the destination point should be far enough not to have the aircraft starting to swipe left to right... + local DVu = { x = DV.x / FollowDistance, y = DV.y / FollowDistance, z = DV.z / FollowDistance } + + -- Now we can calculate the group destination vector GDV. + local GDV = { x = DVu.x * CS * 8 + CVI.x, y = CVI.y, z = DVu.z * CS * 8 + CVI.z } + + --trigger.action.smoke( GDV, trigger.smokeColor.Red ) + 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, FlyDistance, Time:", CS, GS, Speed, Distance, Time } ) + + -- Now route the escort to the desired point with the desired speed. + self.EscortGroup:TaskRouteToVec3( GDV, Speed / 3.6 ) -- DCS models speed in Mps (Miles per second) + end + return true + end + + return false +end + + +--- Report Targets Scheduler. +-- @param #ESCORT self +function ESCORT:_ReportTargetsScheduler() + self:F( self.EscortGroup:GetName() ) + + if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then + local EscortGroupName = self.EscortGroup:GetName() + local EscortTargets = self.EscortGroup:GetDetectedTargets() + + local ClientEscortTargets = self.EscortClient._EscortGroups[EscortGroupName].Targets + + local EscortTargetMessages = "" + for EscortTargetID, EscortTarget in pairs( EscortTargets ) do + local EscortObject = EscortTarget.object + self:T( EscortObject ) + if EscortObject and EscortObject:isExist() and EscortObject.id_ < 50000000 then + + local EscortTargetUnit = UNIT:Find( EscortObject ) + local EscortTargetUnitName = EscortTargetUnit:GetName() + + + + -- local EscortTargetIsDetected, + -- EscortTargetIsVisible, + -- EscortTargetLastTime, + -- EscortTargetKnowType, + -- EscortTargetKnowDistance, + -- EscortTargetLastPos, + -- EscortTargetLastVelocity + -- = self.EscortGroup:IsTargetDetected( EscortObject ) + -- + -- self:T( { EscortTargetIsDetected, + -- EscortTargetIsVisible, + -- EscortTargetLastTime, + -- EscortTargetKnowType, + -- EscortTargetKnowDistance, + -- EscortTargetLastPos, + -- EscortTargetLastVelocity } ) + + + local EscortTargetUnitPositionVec3 = EscortTargetUnit:GetPointVec3() + local EscortPositionVec3 = self.EscortGroup:GetPointVec3() + local Distance = ( ( EscortTargetUnitPositionVec3.x - EscortPositionVec3.x )^2 + + ( EscortTargetUnitPositionVec3.y - EscortPositionVec3.y )^2 + + ( EscortTargetUnitPositionVec3.z - EscortPositionVec3.z )^2 + ) ^ 0.5 / 1000 + + self:T( { self.EscortGroup:GetName(), EscortTargetUnit:GetName(), Distance, EscortTarget } ) + + if Distance <= 15 then + + if not ClientEscortTargets[EscortTargetUnitName] then + ClientEscortTargets[EscortTargetUnitName] = {} + end + ClientEscortTargets[EscortTargetUnitName].AttackUnit = EscortTargetUnit + ClientEscortTargets[EscortTargetUnitName].visible = EscortTarget.visible + ClientEscortTargets[EscortTargetUnitName].type = EscortTarget.type + ClientEscortTargets[EscortTargetUnitName].distance = EscortTarget.distance + else + if ClientEscortTargets[EscortTargetUnitName] then + ClientEscortTargets[EscortTargetUnitName] = nil + end + end + end + end + + self:T( { "Sorting Targets Table:", ClientEscortTargets } ) + table.sort( ClientEscortTargets, function( a, b ) return a.Distance < b.Distance end ) + self:T( { "Sorted Targets Table:", ClientEscortTargets } ) + + -- Remove the sub menus of the Attack menu of the Escort for the EscortGroup. + self.EscortMenuAttackNearbyTargets:RemoveSubMenus() + + if self.EscortMenuTargetAssistance then + self.EscortMenuTargetAssistance:RemoveSubMenus() + end + + --for MenuIndex = 1, #self.EscortMenuAttackTargets do + -- self:T( { "Remove Menu:", self.EscortMenuAttackTargets[MenuIndex] } ) + -- self.EscortMenuAttackTargets[MenuIndex] = self.EscortMenuAttackTargets[MenuIndex]:Remove() + --end + + + if ClientEscortTargets then + for ClientEscortTargetUnitName, ClientEscortTargetData in pairs( ClientEscortTargets ) do + + for ClientEscortGroupName, EscortGroupData in pairs( self.EscortClient._EscortGroups ) do + + if ClientEscortTargetData and ClientEscortTargetData.AttackUnit:IsAlive() then + + local EscortTargetMessage = "" + local EscortTargetCategoryName = ClientEscortTargetData.AttackUnit:GetCategoryName() + local EscortTargetCategoryType = ClientEscortTargetData.AttackUnit:GetTypeName() + if ClientEscortTargetData.type then + EscortTargetMessage = EscortTargetMessage .. EscortTargetCategoryName .. " (" .. EscortTargetCategoryType .. ") at " + else + EscortTargetMessage = EscortTargetMessage .. "Unknown target at " + end + + local EscortTargetUnitPositionVec3 = ClientEscortTargetData.AttackUnit:GetPointVec3() + local EscortPositionVec3 = self.EscortGroup:GetPointVec3() + local Distance = ( ( EscortTargetUnitPositionVec3.x - EscortPositionVec3.x )^2 + + ( EscortTargetUnitPositionVec3.y - EscortPositionVec3.y )^2 + + ( EscortTargetUnitPositionVec3.z - EscortPositionVec3.z )^2 + ) ^ 0.5 / 1000 + + self:T( { self.EscortGroup:GetName(), ClientEscortTargetData.AttackUnit:GetName(), Distance, ClientEscortTargetData.AttackUnit } ) + if ClientEscortTargetData.visible == false then + EscortTargetMessage = EscortTargetMessage .. string.format( "%.2f", Distance ) .. " estimated km" + else + EscortTargetMessage = EscortTargetMessage .. string.format( "%.2f", Distance ) .. " km" + end + + if ClientEscortTargetData.visible then + EscortTargetMessage = EscortTargetMessage .. ", visual" + end + + if ClientEscortGroupName == EscortGroupName then + + MENU_CLIENT_COMMAND:New( self.EscortClient, + EscortTargetMessage, + self.EscortMenuAttackNearbyTargets, + ESCORT._AttackTarget, + { ParamSelf = self, + ParamUnit = ClientEscortTargetData.AttackUnit + } + ) + EscortTargetMessages = EscortTargetMessages .. "\n - " .. EscortTargetMessage + else + if self.EscortMenuTargetAssistance then + local MenuTargetAssistance = MENU_CLIENT:New( self.EscortClient, EscortGroupData.EscortName, self.EscortMenuTargetAssistance ) + MENU_CLIENT_COMMAND:New( self.EscortClient, + EscortTargetMessage, + MenuTargetAssistance, + ESCORT._AssistTarget, + { ParamSelf = self, + ParamEscortGroup = EscortGroupData.EscortGroup, + ParamUnit = ClientEscortTargetData.AttackUnit + } + ) + end + end + else + ClientEscortTargetData = nil + end + end + end + + if EscortTargetMessages ~= "" and self.ReportTargets == true then + self.EscortGroup:MessageToClient( "Detected targets within 15 km range:" .. EscortTargetMessages:gsub("\n$",""), 20, self.EscortClient ) + else + self.EscortGroup:MessageToClient( "No targets detected!", 20, self.EscortClient ) + end + end + + if self.EscortMenuResumeMission then + self.EscortMenuResumeMission:RemoveSubMenus() + + -- if self.EscortMenuResumeWayPoints then + -- for MenuIndex = 1, #self.EscortMenuResumeWayPoints do + -- self:T( { "Remove Menu:", self.EscortMenuResumeWayPoints[MenuIndex] } ) + -- self.EscortMenuResumeWayPoints[MenuIndex] = self.EscortMenuResumeWayPoints[MenuIndex]:Remove() + -- end + -- end + + local TaskPoints = self:RegisterRoute() + for WayPointID, WayPoint in pairs( TaskPoints ) do + local EscortPositionVec3 = self.EscortGroup:GetPointVec3() + local Distance = ( ( WayPoint.x - EscortPositionVec3.x )^2 + + ( WayPoint.y - EscortPositionVec3.z )^2 + ) ^ 0.5 / 1000 + MENU_CLIENT_COMMAND:New( self.EscortClient, "Waypoint " .. WayPointID .. " at " .. string.format( "%.2f", Distance ).. "km", self.EscortMenuResumeMission, ESCORT._ResumeMission, { ParamSelf = self, ParamWayPoint = WayPointID } ) + end + end + return true + end + + return false +end +--- Provides missile training functions. +-- +-- @{#MISSILETRAINER} class +-- ======================== +-- 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. +-- +-- +-- 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. +-- +-- 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. +-- +-- @module MissileTrainer +-- @author FlightControl + + +Include.File( "Client" ) +Include.File( "Scheduler" ) + +--- The MISSILETRAINER class +-- @type MISSILETRAINER +-- @extends Base#BASE +MISSILETRAINER = { + ClassName = "MISSILETRAINER", +} + +--- Creates the main object which is handling missile tracking. +-- When a missile is fired a SCHEDULER is set off that follows the missile. When near a certain a client player, the missile will be destroyed. +-- @param #MISSILETRAINER self +-- @param #number Distance The distance in meters when a tracked missile needs to be destroyed when close to a player. +-- @param #string Briefing (Optional) Will show a text to the players when starting their mission. Can be used for briefing purposes. +-- @return #MISSILETRAINER +function MISSILETRAINER:New( Distance, Briefing ) + local self = BASE:Inherit( self, BASE:New() ) + self:F( Distance ) + + if Briefing then + self.Briefing = Briefing + end + + self.Schedulers = {} + self.SchedulerID = 0 + + self.MessageInterval = 2 + self.MessageLastTime = timer.getTime() + + self.Distance = Distance / 1000 + + _EVENTDISPATCHER:OnShot( self._EventShot, self ) + + self.DB = DATABASE:New():FilterStart() + self.DBClients = self.DB.Clients + self.DBUnits = self.DB.Units + + for ClientID, Client in pairs( self.DBClients ) do + + local function _Alive( Client ) + + if self.Briefing then + Client:Message( self.Briefing, 15, "HELLO WORLD", "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, "MENU", "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 + + Client:Alive( _Alive ) + + end + +-- self.DB:ForEachClient( +-- --- @param Client#CLIENT Client +-- function( Client ) +-- +-- ... actions ... +-- +-- end +-- ) + + self.MessagesOnOff = true + + self.TrackingToAll = false + self.TrackingOnOff = true + self.TrackingFrequency = 3 + + self.AlertsToAll = true + self.AlertsHitsOnOff = true + self.AlertsLaunchesOnOff = true + + self.DetailsRangeOnOff = true + self.DetailsBearingOnOff = true + + self.MenusOnOff = true + + self.TrackingMissiles = {} + + self.TrackingScheduler = SCHEDULER:New( self, self._TrackMissiles, {}, 0.5, 0.05, 0 ) + + return self +end + +-- Initialization methods. + + +--- Sets by default the display of any message to be ON or OFF. +-- @param #MISSILETRAINER self +-- @param #boolean MessagesOnOff true or false +-- @return #MISSILETRAINER self +function MISSILETRAINER:InitMessagesOnOff( MessagesOnOff ) + self:F( MessagesOnOff ) + + self.MessagesOnOff = MessagesOnOff + if self.MessagesOnOff == true then + MESSAGE:New( "Messages ON", "Menu", 15, "ID" ):ToAll() + else + MESSAGE:New( "Messages OFF", "Menu", 15, "ID" ):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", "Menu", 15, "ID" ):ToAll() + else + MESSAGE:New( "Missile tracking to all players OFF", "Menu", 15, "ID" ):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", "Menu", 15, "ID" ):ToAll() + else + MESSAGE:New( "Missile tracking OFF", "Menu", 15, "ID" ):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.", "Menu", 15, "ID" ):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", "Menu", 15, "ID" ):ToAll() + else + MESSAGE:New( "Alerts to all players OFF", "Menu", 15, "ID" ):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", "Menu", 15, "ID" ):ToAll() + else + MESSAGE:New( "Alerts Hits OFF", "Menu", 15, "ID" ):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", "Menu", 15, "ID" ):ToAll() + else + MESSAGE:New( "Alerts Launches OFF", "Menu", 15, "ID" ):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", "Menu", 15, "ID" ):ToAll() + else + MESSAGE:New( "Range display OFF", "Menu", 15, "ID" ):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", "Menu", 15, "ID" ):ToAll() + else + MESSAGE:New( "Bearing display OFF", "Menu", 15, "ID" ):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)", "Menu", 15, "ID" ):ToAll() + else + MESSAGE:New( "Menus are DISABLED", "Menu", 15, "ID" ):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", "Menu", 15, "ID" ):ToAll() + end + +end + +--- Detects if an SA site was shot with an anti radiation missile. In this case, take evasive actions based on the skill level set within the ME. +-- @param #MISSILETRAINER self +-- @param Event#EVENTDATA Event +function MISSILETRAINER:_EventShot( Event ) + self:F( { Event } ) + + local TrainerSourceDCSUnit = Event.IniDCSUnit + local TrainerSourceDCSUnitName = Event.IniDCSUnitName + local TrainerWeapon = Event.Weapon -- Identify the weapon fired + local TrainerWeaponName = Event.WeaponName -- return weapon type + + self:T( "Missile Launched = " .. TrainerWeaponName ) + + local TrainerTargetDCSUnit = TrainerWeapon:getTarget() -- Identify target + local TrainerTargetDCSUnitName = Unit.getName( TrainerTargetDCSUnit ) + local TrainerTargetSkill = _DATABASE.Templates.Units[TrainerTargetDCSUnitName].Template.skill + + self:T(TrainerTargetDCSUnitName ) + + local Client = self.DBClients[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 ),"Launch Alert", 5, "ID" ) + + if self.AlertsToAll then + Message:ToAll() + else + Message:ToClient( Client ) + end + end + + local ClientID = Client:GetID() + local MissileData = {} + MissileData.TrainerSourceUnit = TrainerSourceUnit + MissileData.TrainerWeapon = TrainerWeapon + MissileData.TrainerTargetUnit = TrainerTargetUnit + MissileData.TrainerWeaponTypeName = TrainerWeapon:getTypeName() + MissileData.TrainerWeaponLaunched = true + table.insert( self.TrackingMissiles[ClientID].MissileData, MissileData ) + --self:T( self.TrackingMissiles ) + end +end + +function MISSILETRAINER:_AddRange( Client, TrainerWeapon ) + + local RangeText = "" + + if self.DetailsRangeOnOff then + + local PositionMissile = TrainerWeapon:getPoint() + local PositionTarget = Client:GetPointVec3() + + local Range = ( ( PositionMissile.x - PositionTarget.x )^2 + + ( PositionMissile.y - PositionTarget.y )^2 + + ( PositionMissile.z - PositionTarget.z )^2 + ) ^ 0.5 / 1000 + + RangeText = string.format( ", at %4.2fkm", Range ) + end + + return RangeText +end + +function MISSILETRAINER:_AddBearing( Client, TrainerWeapon ) + + local BearingText = "" + + if self.DetailsBearingOnOff then + + local PositionMissile = TrainerWeapon:getPoint() + local PositionTarget = Client:GetPointVec3() + + self:T2( { PositionTarget, PositionMissile }) + + local DirectionVector = { x = PositionMissile.x - PositionTarget.x, y = PositionMissile.y - PositionTarget.y, z = PositionMissile.z - PositionTarget.z } + local DirectionRadians = math.atan2( DirectionVector.z, DirectionVector.x ) + --DirectionRadians = DirectionRadians + routines.getNorthCorrection( PositionTarget ) + if DirectionRadians < 0 then + DirectionRadians = DirectionRadians + 2 * math.pi + end + local DirectionDegrees = DirectionRadians * 180 / math.pi + + BearingText = string.format( ", %d degrees", DirectionDegrees ) + end + + return BearingText +end + + +function MISSILETRAINER:_TrackMissiles() + self:F2() + + + local ShowMessages = false + if self.MessagesOnOff and self.MessageLastTime + self.TrackingFrequency <= timer.getTime() then + self.MessageLastTime = timer.getTime() + ShowMessages = true + end + + -- ALERTS PART + + -- Loop for all Player Clients to check the alerts and deletion of missiles. + for ClientDataID, ClientData in pairs( self.TrackingMissiles ) do + + local Client = ClientData.Client + self:T2( { Client:GetName() } ) + + for MissileDataID, MissileData in pairs( ClientData.MissileData ) do + self:T3( MissileDataID ) + + local TrainerSourceUnit = MissileData.TrainerSourceUnit + local TrainerWeapon = MissileData.TrainerWeapon + local TrainerTargetUnit = MissileData.TrainerTargetUnit + local TrainerWeaponTypeName = MissileData.TrainerWeaponTypeName + local TrainerWeaponLaunched = MissileData.TrainerWeaponLaunched + + if Client and Client:IsAlive() and TrainerSourceUnit and TrainerSourceUnit:IsAlive() and TrainerWeapon and TrainerWeapon:isExist() and TrainerTargetUnit and TrainerTargetUnit:IsAlive() then + local PositionMissile = TrainerWeapon:getPosition().p + local PositionTarget = Client:GetPointVec3() + + local Distance = ( ( PositionMissile.x - PositionTarget.x )^2 + + ( PositionMissile.y - PositionTarget.y )^2 + + ( PositionMissile.z - PositionTarget.z )^2 + ) ^ 0.5 / 1000 + + if Distance <= self.Distance then + -- Hit alert + TrainerWeapon:destroy() + if self.MessagesOnOff == true and self.AlertsHitsOnOff == true then + + self:T( "killed" ) + + local Message = MESSAGE:New( + string.format( "%s launched by %s killed %s", + TrainerWeapon:getTypeName(), + TrainerSourceUnit:GetTypeName(), + TrainerTargetUnit:GetPlayerName() + ),"Hit Alert", 15, "ID" ) + + 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() + ),"Tracking", 5, "ID" ) + + 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, "Tracking", 1, "ID" ):ToClient( Client ) + end + end + end + + return true +end +env.info( '*** MOOSE INCLUDE END *** ' ) diff --git a/Moose Test Missions/Moose_Test_CLEANUP/Moose_Test_CLEANUP.miz b/Moose Test Missions/Moose_Test_CLEANUP/Moose_Test_CLEANUP.miz index c8858385e1d3314424ec4d99caa97951791d6e01..5b3946c14af8835f10236a53218148171feba072 100644 GIT binary patch delta 102712 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¨k~iI|Eys0+&b<0Unolc>xfYnG^vK4FCWD009610002Ho&uMj zeE~`XD4_zE?0o?|14z9Bmn(h&JOjne0+(uj0YU?`)dH8VegQiJbKwG)?S26~0|Vv) zm+)Hw7?)Lg0T7o|e*qc;bLRq=aeo0t1G@MEm$`odK?8F90+;=N0UHJm{{jF20QUU< AbpQYW delta 1005 zcmZ9~ZA_C_6bJD8Jf|(K6etxtY@uTa#6ha4FccAi@+MT*0Wt+~%}l2QEbjxypg65! zAx&T_H#Jyo#KMrlbeO`VmuVj3ePo&r5P4`2Fm?T2Ibo5ycU!c5{l9BUMpj(+SX}L%HUgkPpxbY~L>%!cp zN|^~7>uFEb?y8r)c_m`?JDv%=a?94xL=D4ig-)jCZ=kmvv$Pv-J>xLUe=Dt1 z*kWodZ?ku*ySl%VOA5xMg5XaZCo0&JyUym;jQDY)Yx-htCpE7>^g-7T8sTa$P3DGQ zyCfciH@tDvXyZ&-rFskd&2m@%MLbz9qN0o98aT5mkJH{EPQ(7PT&kby~<9 z?oF@G9iHVzpZezK>GB6d73?kcDy2{;btrPpJL6-h`XlVH!LiBZRkzAivMP$te7ZR3 z`p9*lj^%jN^8`6?QOm4zE}e01e*08w-5UcU*MUqEKi5sGhI&_R?Co50%e>aM&1#Fkn)>`+ z&$?rt+pe|+roI}VX+7_sk#gU8|47H2VY@2V+YtLGWT|#&%$lDwY7KbZEZM2vQ!Hv5 z>??j*ICW)m_qfy7x^FvVm7E|a+fsyH)M(jSPDm4hrkw!(>1iWjnl_TU{r~?m+DZs! zd|afH&YHA65JuK|;3*^Fi~R`jhXPN8lL^=i4OAvy zl!R~y+>3B22|HlnAi~#4*a>%w5MCl765>h`js%DWeg(p6fOsf5f$#_w$mc2?!6ChI z695O7tR^{5h+4$ThCr$8+?5Z=L{h{YC!%NUfbx*uT+10tYe0AZQ~ zb*7g^;8+K0!T>ITOX^ACzqEe+e`r;11RivN1X$n^*n>$maoqTpmA zf$uS?o5b%D*po$-hSCY_Wh33OibhR)l12H&83Z=4AO_S50zYI?q^NuXi&lYvF;%$o G4F3RSUy9EF diff --git a/Moose Test Missions/Moose_Test_DATABASE/Moose_Test_DATABASE.miz b/Moose Test Missions/Moose_Test_DATABASE/Moose_Test_DATABASE.miz index 35621d8d47379b72dda92f0f740f4a5442d94ba8..bc67a6daa2d83f936fd12899a5f42ed6fb284dea 100644 GIT binary patch delta 102550 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 diff --git a/Moose Test Missions/Moose_Test_DESTROY/MOOSE_Test_DESTROY.miz b/Moose Test Missions/Moose_Test_DESTROY/MOOSE_Test_DESTROY.miz index 5d1f7a1b2485548e55ff2da197103fa218d945db..741f65acd7eb6d392bfc04fe70cac57bb86326d9 100644 GIT binary patch delta 102476 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�Zo1el-UDLaD0@xRWq{JOcr? z0h4ckP6J`M0h7RgK?6v+0h0uPLIW_m0h399G6R_A0h4}!G6?JJ0RU4$L`;+Kd>xZ2 zV-O7h000010000008H%xlk$K<1HA45lc{7Ilf{4#lVX7y0|Nj8lYfCb10Mkblf8jE z2dM!9098mvlW}Ve&dn>H4K1vH7{XaZ4K1*55 zZdtC75I;vmYsd?e8;7@qP4%3Upt(`|_RZ5<{>RaX9MesKHpy7k|8cJ0}BD%Gd>Mu4OUCg3vh zxa+Z?RT{}@doK3L2`ltv={?%xBXfTL|ASV)9M4|MTeh@gu4>Lfh5*j&#cu)+nWa9H z*!y7tb4IJjlGK$+&klCY4B~8;6T5OGx~JgEJ0X|0U3VVTJ-^(*WBk3lUQRB6y;QKU zQ)OZIdz}%a#!Q~{y@$uJr!B>K3rg3uT$~H5dSv-|v8ejDbzE%Eh>!vB5 z(G^_cB^EmK`_y2g;4L3mRr*$IMP<3JxaK1nQ>wCku9q%dtYXUp81*&PuW;+P?^}fSuK6_endS9o9-G3x+&`>bDQmfDy+PQYn=jVQ*NP2! z%olle$A^X1tp!R8-Cmjhopkxe^`iW1*R|#M`q+2*mFuoQf4uy6$-RwteQ)w^o_S(N zLI;lt@3X%S^K_RVw5Vg;&bW-lNcC+9bJ5;%d<1d zPLF!aD8S1A1dJepK_;1Dx)L{&6H`yh^c(LOWu|}SViK4>iJM7^`En`4^!408OWv0; zOn(bxuveBdFob%zO<%~&Bs~2B4^Uwc50eyAamDogOia=ck#1%t0UnUa5c7R2r_W@D V$Z0XFR548d%flqiwyy@H0swzhP;USL diff --git a/Moose Test Missions/Moose_Test_ESCORT/MOOSE_Test_ESCORT.miz b/Moose Test Missions/Moose_Test_ESCORT/MOOSE_Test_ESCORT.miz index d80475c06c2e55b3d8890031c5313528df38e87f..e8c0176c4ab95d1c452f564c193f1094b4b24830 100644 GIT binary patch delta 102557 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�ZlN;xz{SLaD0@xRXcXJOdE> z0h9f3Ba^A(Dg){L0h89^K?BhK0h7IS7?VZi5R;a55DfqT0003100000xc>o@U*j?Z z^+5uYl;c_h+(ZJC@#8WHIZFZnQeQYSIFmo-9g|7q8Uy4@0+Z3f6O&Bl5R=Qo5E1|Y z0003100000?MwmyQ$a*blRxGilgQ*615Ql>ljY<>1HMiIldHrVlV;%%lU3y!5H?`~ z08~LlFLPydbZKs9bCW^k6qA$X8Uv+b0+X=iI|Dak0+a6LK?5*k0+Suu8k0=sDgz2- z0+V^>L<8Yv0+YbzK_qKt0swbma%E_5b#rBNP)h{{000009snKy4+sDNFlhn+001FY B-{JrO delta 895 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 delta 695 zcmV;o0!aPyu?@qW2e5P=5qwa)NVnrR(+C0p0K^3V02Y_=ssR;$E^KvSrBq#S+AtJ- zrc(dIm0xNj7CxqJ5)uyxScO#RG}yzmR+UMdU@5UH+u6&s|6bb(U}#!_`Vz(Vx#ygF zuYH;L8*!0J3^(m|8y5HXs~JpxFUO1d1m?@h-LL5kR_pQ4HMsK|AaH#Z1TCc03~rfV ziW)8jta-t-p~drm5C*;e*WQ=Gx2Jxu_qmF^zRG1xbJ$QDHG-$xfDGM9nsAZTCJ;oC z$3@0cswiV-ywQ#x6CjRiF5J?=hJ|H3^VtO9v)B!brEriB%zD% zJZGT`s1N+Ug|~PLLK+D1`p*&S5zLke3vC^P0?eb82$l|e=nkq+hOU+&%q{+ORC^BSaMb|TJ>d4 zakcjMv=Y6n@|ET}*XBsFR`lP)BY7P;vac{!{MqQPpDJBc;|^KXxbUGng>WLFM(Q{H zlGE7Na&OkxD<8c~KZ;IwEw9hVcl&hSvE5DC4YQdP$hcsLG!5qcK%On9!8m>aP)h>@ zmt0o^8i(|%0k`z30tT!Pd{Dbcx8pa{2m$~A#03DC30DI(26}awQ0}*vL{|em1EwGV zmxEUWRRfbE0GHcW12O}%uK|}3SOYQx2DSm0MOXtu19rComx@>eMI^n*0RVSla%E_5 db#rBNP)h{{000003;+xOP6Ge{U&{di003FxJtP1C diff --git a/Moose Test Missions/Moose_Test_SEAD/MOOSE_Test_SEAD.miz b/Moose Test Missions/Moose_Test_SEAD/MOOSE_Test_SEAD.miz index 48d70c1058e4294120e4d02ccbaf524929e71d1b..b7f520bf6fa532db920d65145007449d6938f047 100644 GIT binary patch delta 102362 zcmV(#K;*xx$pPEl2e8H$5<+adNReK1%{Yt!0AIui02Tm~++rAi>>d46+sg6hbmsjJ zS27*ji4b6#*A6XhNgyvw`AC7hOiNQn*fOZGEmx8Olc9fm_v1cwl5IXhXd7N0NPBy` zdwaWk-=}zZ5hlZ4+7yi!FJ6e<{r$r&ad@Tdu^SW-%lC33ucny8>JCmHyt*A3eh=_hG(%T&f`2bnBgG$XPW&S zXIKM}ZPiXnhIL7&g94(G>pb@6o+0te5Iazv>jwVM~>17gOH;zY`YI}Zq`p>vi zfMGm;;~3OW)6e74Nr422r%KUT#7PHeSQ(!sJyA?Xu>y&IgBuco1vH8wqCAs9i%}2~ zgFQk$F}#RTlw`Rs zO*bV`bgpAAKV@nmJ$|R$EiILn9b9q{7ZGKDZE7NcIJ4l27i8z$vPcY;gw{R9bAp3K zp7GT|js0+a&g=TEjhu|jCeixO&dO3ks4RUIK zmROKyRW?~+mRjVd06{4%8slWy*VjdZ$TY~tpeQOkNZp@^%QX9(3$zKs%72LzsQSyM z3gon%&Nfu8ReX|CLv}2xDve~~b68iH21x>rpDP+K#R?)ezBGu1w&?&&(wH+~qb=LD z2gY7NX_L6$%^{-_4)Z#~86`prDRE(cI3AqFnf7n|fi#wjABd)N;l9$8`~iA-Nk^O3Ek)#7H=2bS@ip6Ql5Qc54U~1;XbGss zQqpOf_Ty;SXtiw3Loy3O558V0{|wwlKr+|c7kfkqO;#r=nzG2P+vEX`Dvu6-|8DVE z+{Qd*n_5dxvB1?O9wvPqio_*#r()<5i1TPPilJAKVF&S#Q;Wh>oE)80bUd` zPEM%FbYjNhVjXXTKN}UQQed;R4WyIdXZ)QD_(#eM#J2b+#)znk9ry%0AF2T4oKsbM zQnD!@{r-~7r+!ssx5{MInT$t&Q)*8&g{lH)Rgl7`F_#;}XJ)>OqPWI@DE{b?Sz}Bi z9N=%-HSWq$@k9fJn-MLv8d`u08KQ!_gz#7{Sj!hc4%iDEshvih&pZQQ)UDe8>5cbI z!|L$5l_*hLs@H5GYT*#cbUx+p+J2-iI%b>Rlf?2?bR!q?s24Nq1wrb6)Y|vQ9h6?# zq0Esv7-EhF2vJO0o-W?P-{C)q0J_|!xy&{G8jRz{yaQ2N32obVF&MWk46D0>0Qf^S6K!}lDK_@0cpghaU|9zR;O6Ex*t`%sa z3R#;$J=?ji?E>q6a-!UUF8*yx{L_vsbK2|Wu{FIEb^i%`846Jri&&|YX(ed(qhS{( z$lt}-TOdv?@e*mbZmW$lEf>M4$f9q6C8d;faSbymM=5H?3pSZM7)))0j?N#T`(Ph9v!!LMETwWS$Img*{h3*t9lm;#gldUh6PFq@lq zP=$X3Fz1pcjn67mt-!Ss*NSY*T-`!~IfFd8IFD)3W--P&ahQV2GU(nwkFuM<1KBUl zr4nhd{1o~)7mxtB@F%P{b0EH%WKBQ{IJ2%%Bn=W%(+K^AVH=Uc>wpMeAsQp#@0W@! z5!{1L*7+NM6p;-@(@lDW{d=*jbm`D&t9F>Ycr-2uo`B3WE1&i-UkYfWL}^Lt4-v~M z16U4XcVKErHKj1QQe4I{^m8s#DlK3&+kgo3c{p_q{pP8fYHw}=O)k%h)K?n|#t^Yy z7er(hjoO}t+BPkVK?KDE+8KK`Qv@?y{z+ zz!c3noXGcU`F>6BQ~8}p1FaFo%hest)DA1=1wyf6q)zyKqTa9h{kmGV&TKAU1Zlz0 zmKrv+CsHv6B&*AacP({gK(g`-)hnaEa_lR~XbIP(+WAMDOB+t~)Ac{vt$%=XSJK3P z`1@g)6yP!?(#CC>ZCMh;@pMBnEJ3PeAoObN17Af=0kag=1Zk*K5~dwscqk%|A?u5K z0i85&GM07*PJrb&nV`fnnM|5?-7+;6_G>SV&s>?;O;ptBETTH_VGI**6KrAPsB;d# za*WkWL+nn$LnyFq2SWz^!GPx10X7DIuk7V@HGQ-D)4`#wb9@hw0f%RN!rt(5IZ7j5 z_~B?5yT3et*nmF_#$THW>|Yt3%NTz9j3&vOGFnC`3Pw0xm_Vv(E1N`dODd*`jpD0C zEEm|qa*I0af_@%|6ZK>(S$W;`<6}r%`{5VRvTp9KsfM5XCRnAgQDj^I57?xCgBXVu zD0vLiq+LYeE=4-zUIbQ?!UjBg?W1+T()8Id=_Q?Lh<)X=qyPa9Q~34kcDULWWL=m8 z$N8T=RanZ-j%_ERq${nkY$sF#58{L^x;YXfLRiMU>2z^UjopY zF2%a|(b&y1UAAR@byb856ad#|BTcV@4nqwctDt2%x>#6CZ?i#hSY%j}oh*jNk6e`_ z4@N8*ix7e-s$$jX;F*fezX#67D2Ay!j0j06!w&u7SIg~g!&+5QLR}_Z+Do`9ULs|p zx4aUrt}F{|b)o_W01;zgS~I43D1~4aHc%dA zLUlEstUbp2dnKk3F_4zQ*b9iO;uiA}qG4KzDLm8Re6pp(+sKvF^mtd%N2^~K9p~kf;=qRXhEvCbN4^^(k<-e_KvC8h| zT9Ef~TniX|8`m<7%kEX@G52v6!>#NvB_HtnR4~o)uQ5z;@g666qTi>LJCeGCtDbG1T_n8)I!7?W4SndG6jE0CoQ$b153LRoV1`5^z zdO(NfG!PXAT_ga1w$;Qg!POvo8&LjH1~!IsXHd8OKA>Di6PRbixuz(CrDy5zlw)rf z#DeT$Wq0lMfwPu*9T4LHxdtp8xqWn9=kYbmtw4&pwX#zpNolV|CUq1sZE0nB8CvMJ zn%z-(=9I!LtrE2sQ&Ofb-K}b!d3hDhFS?ed2XHn&q#}!df~ryFFeNK3U#qt?fpFr@{X^k6G2#hOlkgt4qg+wN(32w5#Y+7EL>y`hP~ zv@0zI=Eb;&8|KIjjas_ti7G(%L?6{?N_R)?!XGS*r4uG{Pvrp#RpDH{YJ+fhZ#-a| zr7EW-0T@!pUEIA0ZNWK2<$j#mbqi(O!nAjs9&;aJGsLPxk9Ns>0gLS3&3)Sbh?;_X=^G zr3bTdjsBEZAH0qS1xhq$wM&RXAGXXG6aVOcEP$D?d;pTX$MCW}e3N9Ie#{XrpkB*3 z4?quadhdW^)6L6O@4ug-8a~_5EIGVnSN3TK!X-aN)yO0l;kg&g<82$6VP>NX5Ws_ z!=a?{%bfw0siIGi9>AlsYwipZN3b}_2L|;(DnE+y&&ZYI4~{$J(ZWVR`itO%Vy+O# z@niKSwTr1W$FZ8kE+FI^_#=N?n?hKBuIkMglmx_o!oTQHuP1xaKxHX)DhT)nI~Dx6 z2KaCI_#s3C=?fZ5^H6{_=)t2Zno=dL(qg4$Hil}bGASy7Fxj1m;`S~+PSf}o09iQPV= zA7Hu~H#sDSRTPKQ2@dP&K`Z##C5A`#PNGvBWBUoBE`)qLb3DVyfccIffFmyT;()%1>X#!uf~bncQ{wB#_efm^ zHJYlSUq=9Z#Q3TwLooDkB!rjR_=kw=h;9fQa2sZO9J;DJnu2^UM;24;XI3uhyxk-v zJ7`2_#@ZCu6IDY88=KpI`?}^!L{6hj(a5LJc)f8TNeoR)ujS-N@Z4#2Dw`ZXQ(^1p z<9o@)r1??Yu$i~Td9}RUs&1s{ug`+zsB9`cXE1azmtVOEHFe8xd`P7~!R87prUVu> z@DqY7vC4A2g^c@vU<4z{T&4vJPx>Veewvt;ctVL?sEk2p^<;p(}PE7wa?@ z6<1-=3(jwsu;l!VLgP2u1g$X4^pZidK+ecgrQT`9*)tS>4!r=28N)~uwQ*$AsBIq_0|mkDPBS6hZ|?V7HdpVV|; zPNzGObg_nypW|+<@T` zr9)U0b0*A3o3ND3J1P`k=}|@i6BMfBfuv$EdvL4^cSWn9qTWN*uo1s<8?%brDR5Io z%g#bhV^$1uS}zFj$sCHxdEKD8-9{?9oQiC9S?5rHrA@U;k=OOa&3b^uu3iw}G;c*f zWwMR)VUBvDe@#2RO3m%HslK_1AKSGHap*Zw|KygS)c>B@`6dK!rwnxml)0HM7(iHS z5~>Z*E{{r+ohAIEKwcL|0%W?|@0UPrv9;%kmF6MCl&&WAFQ;21EW^Ow2iOAeGu+JK zoc!v4a3Co-8Aon8F4D_2L#ovnkIt!^(&!F2*x)guH%m4xWM-|ryW=WJ1Z8m&N|FLQCsnh5oR!fm{{DWQ5dOeq%cC&H1~n-*V^rW z6=kSyVJNA(6;SzD>TeM3A1b0d!_0N9q4@(o>*22P{)b;wbh^B453Skz4KOsYriRd= zn1ADHliQC+yAh$iF<<2(iAN3bX-8> z!t@Mc7KAT=oCPv!rQH|`40?$dM_HPG_Hu}7M9MlLguwtDd#LveosZA#TVewe%J?q6 z$>3)0y8FE$W5pATG-5H(DQis(L9&cTtu87I+U>0?yf1*^Ev*p{dO}xW_LHKZJJC{* z6gp68g1xZIFbvI(4M`Pi;k`UL*KCEOiv8s5E~o?Bj_|DCAsGi8og+Ku+Fdk%Jpu%f zslX=kz5OG4NAoyB1Su_7Pkx?``(1gJbSsnup+`Zxqly?JgT7;7nG^wou>>!~&fyXP zb573}&YN-bjM2!hlYp^GfJZtqYSJ#w-g_gN4FoEKph}Z?<!N!8-AHi ztHmcy=B}Qd9WMW))J-6GZ2|~?%cs2RE(e(vPw!zLLtbzILXXDiRsKmneTq+reRS)~ zB%hjrwgMSzFuHh9TUga;P!g2}kM}i^DI33y9Za(&!17O{R+$V{1h+7@O++-TA*)c; zoW(uBz<&R#fM?nCvw6_E0A6H)#z#W~waf62GB=+i;j8#SB%bx^O3&?og6RQ5Qos{d zW^UW7Iy7Q3pMj&hcdvXN!0)_}7mV!fhJMYzKH!?i-b8)vPaB8|qbZ#MH8JHf8l^Zq zqCjDpYKs~Uc^&{XLGL-XYwFIeP?P87JEcJlW}T=MeuKo)O-mA4-tkesaY-zq@H7eH zrc5}6CPmzoYfj-g68oBeqbb)SVTP7L=W{LfmjfAYI%oM9$}@OTa!vd#$rY!5njmE5 zSsybcXniWA&T7{9c)%KQ#u}q5d#5-RIrswx+W!#`yi@FbQhxRTvE4YOOm`eoLmg+- z(8q8@<}}L*8S4Suuyk>eI3OydHL5>u0h3__ZM(X}Ap(<+;AS&_b=`x4<88SyY&wo4 zP})>~E#fGqGjbY2lFcL*c6!IdU3?FCkhnN_`V09n{ zOlq7?6xZKbMAnhAsS>-)TrY|*-)z*CR~yw_dep4D_tj8;#ah&}PAxU^ZbE=(sc4c_ zmFxF5+wjU7kSi^@2zHtrtb5~ft|Ayt(??wJlP9{0pgE;AIcm=d!b5JS?T{u|S`0C@ zgQm-(S%uaj;e3ci?<&CG6Z(A}K#(Zp(Y!$Y1N<2kgyPgU2}YtoxV7M)Zo(fF3OLc9 zmu`;m%HYU<)^6hAObm`}`>s8lYl)+a%Ma+Dm2TwU z2ok1Nj(^(#=O{IIb2L1+nqYa{_D79|HMxLq8GRMjwoUWQ$e0ykzhPrkwN+1BT>XU2 zy(V2ocbRcvS$B3GpwiTY%A-^s2*np+l_za@V!GLXf!p9u%_W~LXBtN{rP&;LF9hER z0SNW>FndnsA+2GR6a6`x+4C-=hnRGk%W_^H{5M#RH!jpOc{sH6hlFMzT9HQ&MD;k- z8TQhscZtTe@D9`Z(8Xt~tT(LL1J1h}7LP}jd>ZZ#B&UufE60N)e=s=mM}nir83vAK zj{!%24?O^^agPCi$`TIT>*cf}zCsBa?_i!I6(8|bd&634gQ?~TUSdFpZWMfwpS?}# z5`j({;hzR0-tKYYeT>$m|s$$pCA%Ht#g9P&F@WtBJ^iz~}-8aFpQ z(>zV|oEnlP4y%BllAy#4?#UIGY=zS9h<+R`JY!^a5a z!b`cpcN&AbF>2JXD;qM#!_WmOl$?Q(R z5nB@l1q=GsukwajoTHBel7ak+h5#~m`Gy{uIp-NI-hY5J?5Pl%;BsrwT!e-Xf&6}d z(rAP?gGgL>3|+>^if*2=M%`78M+ezuO~~8$p**t+=SznPAoQUj+LqUTWih_1Yd-);+L!C_1@s{Lqa2aC(P;uJ z(vxtSv-@4MZxsV}c4kC?fCa_h--E$_eS(1u2j3S^OmEo_1duR25X|ZIuMfVp*So!E zy`2;*hA;U$jpg-4UDQq0D$>1{ZXP*~kj#D@k!qlID}8)3(311R_g==s1(vV(gVgcs zhwLI!9q=iOMcVg_j_W#Lh+2U4E9~|3Khxln?qfpCvzV@AMVN_F;bR0wHVj~Yid*R5 zyP5xUfp6RgW1Up(|M0dIE4}dEM^K5`sH&1|FW-C(9QY+eJgJQS(nXB}o z>7211?#LRm*6@D(T?>i~(kxHm{4dy9xryDVHnQ?^q^WGbifeHz795?!tj;8jhOFEB zzvr@x#jp4?W9(;^3hp%r`tWmqpQ5R5rff}F{di#G@>O%|>6Ne1jl0`!fjgY}ei~Ck zQuFmQL*D7@K|@>-q6`Kt%AZ$Ah1 zt;5(8ne0;4@ffk~`^4FUg*mGS|3t%>6;1BAa%@q!?IN<3b?)`#uN?7zzWw~$qeZ;h zSvY^@?yF1=ew!Hj`rs?Z(9-mvJ#}a%6u$@6`vg^D<9yvdmq)bmqmg*fl0KnBUsJ-H ztvghBytF^(1X4Y|^Vwns(!KuHac?{>N;t>Q@%KyIPYejeT+>f_%u&Y+n%TgB3pZZl zM%)B{1Iav>bA~ePgM+Yt01*U)K+{XrA|xpvsgz{b9OVpxkUxA19`T!q8(81b2~Jz` zrhm=}Y9g*JEDNiOD!i5$Ma0aOGh55FMt2iR^LE?ZlJO9intg;`w{A*8G!y;<3zWN$t`g@*6* ztZNyJqO+!0SP1BU|1Tw?H^N*`?@ zL7c-U6ZA(sDllNya9XlVGfvop_I43$Do-JL>KhXhbl)69c0L|qUU^<16M)c>Um2(L zE`F>Dwl+{^_dE`LdV;q$@L4`GWLHbBExT!46a%V0h@mfkGH~@xbL$Hfh87eP@&X$T z{J?(2IauutWqNu8z0v2Cqof{LW6(~1+n_7z@r&*~IEkU^OI&#==0h3PNwmArPcmQB zkW#|+bR~mUN7%|@>O1Q^?CUCQbB*2a*S%C(Fg;gJKb0QfRwJXDm|%i}A04=%x=Tfp z7eAqjdP>!QR9#6cURWlnE07H-6(*CWiIEsDolI*B&||Emqd!4+3t|uwA$*-B9f-dE zK3AWMxq|sN9L}Sxiwq29fLhBj)7qFl_!1(SAYIBiy{!oZ{vpIWv^z6f9f{%@g-L&U z?T!$Y4G&9m*QN*a&fWM3`nhlWBjnGmLl?I}hCoe!J0$ealZbIfoJR|Fw(C@k8s=(? z>ZOMFdPZgPQ z25NTr2BNbjqNwe2r_~PB;+F~`0IC=oIK6*)M=-6pA^e+!NNaYJ438hHy5a`g6c+Hk zk|QSKN^2r|%7hHEG9HtD?WDWLa$wnp)55ZU63yo$^z65)WJaKBooiCrsat|QROL#D z;Y0Y@?wk|gLU>$e8YAcdeysb(rq}42LmEGi zL7&PmK@rk$Vb_(I*Bnab_lR4ItEmv&mbsK<1osk*^i@g9qO_};B2MvFjdNR{HC|hP z;kTt>9J%pqZ#MqydlfGoHz5amMxdUk-=v(%cVN;6_4`a-+2`Z6TQk>ys2YbboOuYk z=MEdC8rk$kcsO)%q=R<=4sd1hG)8yFHF_ZOOCBNk3UqZwfCmsj-SVautrd5l_mq~H zk+JQ)MNn39?$NH92dzZjh{F{%r?yysin+DL93Ea6Z+(iVZD1Ip?W=k4*Hm5?K-O_se?)GgIV(0sYN_pf_XwsWKYT)tyxtO#x(=kBDq{nV9=E1_DX*@>@)u!EQHQX55FKr;7lj-K_O7zM+SHwytJ} z_6x)&cMKag>O+V6x~fGJCL^>yI%x@8ig*g>e(6Gg0trOmrq)K(UMvZpY;GKFyxut63XkFeY+1v* zAxJ6tvB~u*dw~t3)S5bQ5~&I+hK6d39begw#HD^{wxXG*xDQbMn!mXbvT| z%!g?e6Ymo;5IR(JRyZNwvolfOk+43}pi+nc<>SI2@FeL5{8TDVnXW8}%AB75lfDqk z>d>zxB8ITePExzssp-Ox2X|mkG4{pE^trcvo{Vae@P3ZCCha0VPdchmd`4G;_DM;3 z%wJD}srC!GM>%Ktq?}xTYfY*NMYmKPsL9`qCJj; zgKe0{RGlVe2294tZGJQb`}5azIHhk~n;+`-)aIfbIw$=`3rmVd4lsyf++Ie!jdgcR zbm$QB0Iu^;dx{kayx0|sMXg5YaOCJ&Ede$(N&xw(;5ta#dPO%X_HY^nm29`ypIn;2p!D)cg=0@?VF zwY%W%YE2DL>$BP~s_V+sREnQVe`(21M?;+&r7-xHVY-ne7`)LZmJLy$`5*z5Nj;^O zgJd(JeVf*|nkcOg>G7(O@qRrFrg4uh4imUz=SpXuv%fcgPj?UKGTB+3(8>;|9zT>n zYztqf&C))N9jcB_;XiZYXE0KTUvBKre)~}QkvDa`huEY6&}CX z-&4o01HQFBy>Ef4>7_$2%djrTGp_Pd*`zseM^@La?z-NkUgZC#exB=cb z%dOIP2aM3}X9o~06{9hZ6IJsiM50{6R2@1qsCBDz#N)=PhAcL)&SWq8i|}l z-6`M)T>Pg#0hX2d96#s3N}r7>U7cp5I-6gA&oF<#1Fwg1mOyvvI&Nx_rf_Z<^6<(> zGV#+%aJ-kHEvHk3AXEogY!?Z9%<$}PcZ2EyF3QFf{K)8lKd(Xc%ax?^sho$MEEyGz zfMF&Nz2g_b2Ltqlby+z?+KkL@AfNmre?t{X^QkZtIdas6a6X=!B98g0zwIZ}Kj3eF z>Z;;2l?{Z9^ioz3zm^F2%QI-nkgZQ_1c(EGG8zjT-=rng!y1K6& z!@nt(?R}wm%F4k{yiey{oSdB(u+;-UI*tHm(Z}AVj0sxVh$+8bZL)%{7}#vthh_YPT znS*H?MFA}?D6aoA+;RX8u9t)?>l*+8K0H;GWx?1OWk#HeVWYL!WHLZFMxW`&8Rs$- zFy|Yc+SKq8Gb2U?%Y_ez0uhA7c)q;6Lejcy!#pnbN4yy8rUYVRJLIHrBgw*~8y@U$ zm7%cqh~RMl{np;e+wFrbabF&PTN`hVw)gi=;NQ_Zcsr_M>0^akKW}Vq84;46r=)*7 zE9zw-N8|h)Taac+#4p^cam|yKp*6HxLcJJK=7)i3d$ZXJ@b2~)TRbcXvIsxM(I_2m z5jfn~*?}t+i2PrQ$xbpz)OC(GHzwX2p9vwK(vESIMJxTR@7Y-whMllDEsNiw|P z+mLX^K35;PxP)@tGpg)=rLcQ^YD2wHLjLBVg`}7oTL_~zxKL_)uKexV{Ht^2U;Td# zPo#lIj8DYxo&gGHa*a<{73dK|6!E)fjGA4b)kiMS>H`;OM$}#XDp7ZcCAo9h-Oo;w zVs^kqw6D~QTXRG0eWOQxg!;I(@gAbZyZxQbrxK%FMr+!>g|ujYt(Pu>U8BaCF?wKB zsv!ph9t&7&A6iCDNbeaO9&EhXIyr*he-#lDZ??Tl#P1e=Z;OBZ*Y_KT zhbJ3*nr$tgok_hTJolfIVZ1T^bD z@VDyNhRw}6mSZPW*YbA037a~a>X=UBSr8t|6(dv7KttqJfBx$i4YWp14-~jT~T|<0$YYGW;ZHY_}lHrJa=ZZ zn-IhomY3asp6;HW?w+36c?MJm{$Z3IxwK~Wndg1H`V7@7a>93 zc@#n9e*fSgP@P22A4Q@$pGS29y}Lqz7U{;$m+HNL6-s3{feo^Le}!2G+N`?lKUiV* zx?!oy^urZqYPzDW-~pviQfNl3YWgdWSzfbdbY;jrQkjKLML4^~y^WtqBCq9H3pF%e zmm!!Z^mE>G`URn`z|kyEho?!N6m4-iz7k&kYI|->_+3Ce%aiH;IKFCs^j-uc0j4Lo z{Co$0o_~g-8Kv!$Ng~Y0%SLq*Op8+@3z+uV8=KEZt;pL!tOdl3I^0QcOOeeAJT>2l z7Hw_%g{7aL^`8B5SI0O*Km}^Ly<*ZYwDdufxNR-<_c~y_Wj>;~i#4wc%8f7W1QVhb z^PR?+&l_XD+ZgjbdzH4m#=*gJ21N;#wnHd?Tz^x1DOt!B`@6h0E5|Dr`m@)F0Is;J zmhrdM5-tVk=IJPbZ@0ZjrnLHJ8(^b&QluN$%7WwL`jEV(5I_uma>B`4MadSGtWvPn zKFJ|kj-N#VWdMkTqu)U0@_C`mxeF5Nnvq_r%db=Yl_xSu<36I?UiWQLsd4`DspO@9 z?%kpS)8PMKAu;y4aIHP1nhv4U3v4btj^6$yiat%~C@+dwi^%6R>IC<8jLZvkDuDUr zm?pR<^q@#?oN@$8P{msb;@zFQA&y_?**GZ@y%LmfVfrz-EwIx%P4IGO^ta1td_yl? zr(xW^&ayl?m|VuRN2@Ic$(R&)GEFXj(_iVX-ADtE;$KTAchhpWR>x~SUA{16qL%#y z`0wHdFa3Z%I-SXl zhQ0hq>DK6tPS^)}y^G3AJ#xLy*b*N*sxuvnMfhwllq;-k=PX>)u=Y!zFhT42 z-&u9?(mK+5G*?g_#Hx_fy$iv?FiLd2i-j+L9I}^vT(x|j zBHj`ku0R#_nyaHz@%g1=jdOZ`3evmRblHN{P#2gAT3MSGu1>YJM2Xb^b%6@CpzAGy z&8{%FN*On)u2kAn7`s6npnBB+ucBgAi_3CU)U7&i=ZD(rMM;WbRsAyj{5*ZdEmFtc zSxFtUdOCia=L~|c2Wukh+*CxL(i5j@EMp;;?O6T9u(+mFjy5ZMBHoC9zn{?Q)G_0^ zmi3Ok>&0iJJC#(VWTfcXu}z1OHde^}9HK4clYf~UtH>X}a=pl6MqR}D7*1|dB@WDN zMXF_Gi7Gky(i0PJ=G)}&X6?>?8$!dk`h;^Y>rROJB-7is94H@jt3l>ubMcnbL$)#G zVd-y*4d^!e2^~L=yI>xFzda}q$fcxKvy4bD4nj(2ZIC`!$CWVBOMY;)QHEXv#71{h zEdrYF90_D7tYO4nK7_iWy7QY>ZQzuu`oO7dQT28h=f$`29c|Q7u-@bf29IHuve--W zBAyJBqgWFR;wiWZZidV2suL5~6{;5JVO7dy=9E`5Q!u|mscblZzExyu&?TcI{{^@BKXPUxKQ&C@Tz(l9v<7=_8Tx;1+RO$gC9@zyAV! z+&|H;G_oev-JIbAEt|m1BzA}u6T$SNg=N!F>YlcL1oRj)5eo+KJnd|nbl16Ks*(ML zDu$6e&dqS&DBLB-R2q5I!=?(*EkH{EmHKj$MrnRKj_*)?jDb%M2CUA4$x>v>w=Cfk zB9?DZf+}hm^j^3kaHn1wh*hnRce!Cj<1};I6r)of>huhv+&E7rG{uTms27b!JU2ce zY^4)_Go?`Nf542-X(2|7yPu+bdh01kR%NC6dV}Lx+KVacsMqiBeBOKUGWo@#Y)Ou- z5R(^!%*^Kte?q?oSyCwvNBnb*&_%Gwb40= z$ZbfLXqPa&lM4gqNn2yOaiHU_r*}t|5Xbs|0`NnKO~Pw#Y2?t*c*jb46#nT_40jKh zVwgE&%9oXuRz|(!9GjdrRTf7I`_~;gr4Q9Mss3qEJUeD%e?))YVjwrUx-Ze}9I@4% z9oEpghC|7)iyk7zR8Mw}*aD}2UC{Ds)8aXh?tVH>@&NqnSu(4_-&HdLp;g>hXtr+=Cn~MY z{?nJ>DQ=;=kLaMeDb_p8yxl>?ZbtBb|D3NF&Nw@P-kR-){7(HzguxdE2m&r%yCmM? zSLu-M91o+)5qsWvW`Qsh8m>36gVC=_K=%$*4b%6Ecv#n&X7Q69; zP8Dx6w(0D(w>o8Kwdv;;^!O`*b*N8gok~rc+jdYes8nWw5(~?^!9^)5o7#LNQJjn9 z=C-)&3ThZ}BqA;ZPyajFuW`@pj# z_U>2VNA=Xe6k1qzI(`L>G$7T=BrwremxkWBql7vl7EFS9Iui*dcYo6rxeR=%bM^B` zqdEbYvp+s(>hC&wG?c1IdE^E;!r37F2DJobD7!nUlI-1tgQ*o>r|;ZBmGp3H@Q)rRoiHHp+op$-R(6|;_<9-)+Hunz824D9vk7t`nSYovYct|sXE{V;yB>M8^MHN>{K zh9LnM!Is;PUz&nE3DK(W*>yUN&eGo+!x$N*VQfgJ(~yDnJ!b%o$V;I}GV#F3N}9$N z@GUF1kG;5NnMiv*+sEG$;p2EcQO=ujnt464;p#`Z)Z(MNGXZCRYK9(y<*tC zE*2mEN?*$B>rp7WM7*-RGryH^hMa7T9^glx;43I{ahSz;?a|L$-7U5uZ|K9Dcg1iV z=XsKM5hUoJPk(=ZhAH=)M4)Zwk3fU##KqJ%j^?x5WGX>uuA3|`L`EH9m75qPm+2%K z(Ot$VIPLLqzh{YXP>5^rBM5sh3{Z$SDc50Uc7>9+rIYJqn$qG9%x4H&T}HZG4k=ay zbgYc7-Ajf=j%#)SqV`vdt|0{)%e&{CeeY$krmW@Ek@dB%HpEd7hw1j?EdUOY`eiXx%)#)!w-F}o^B zGQ7UUw;i(_Kq<{GO`NW{^Q`FG(-DZ4pvUP1EK1C7lHxiW!CNK)x6|xu3L+~t#l8kX z%K#oGQ)DE6_1#t4G549~iJJ#1K$#RY{Q6EL@$j0Jpd4*dU@GW`L}sQtIe6aiI%0Ir zP7I-OL@t-p>_&`$2#m%|7{n!xCu|erAzFJ@k<%hKg)=&r>W(!D@EeaQWh3;bv^`Cz zq_1NL2jnEr20Hu0g>%E`5D?}0?jiS8p@$?qnMCP(X%~7V4-7Qk%3*a z6kL3NHvZ8zJEWkTSIH!q#`7EAB=J(+u-3_DPc!`ErUgm!8xY*HAnX34}m zME6`kmA(c3DZuW?zkK(4=Ry8Ox-x3v%)5Rr&sUzy^MGr=vufXNSiHgWzar!3<%Z$F zG?eOGvStkfG;rhW7bhZ36Dukenc`io_}>Ls{**JU8*3cuiG>Y9{OJmuskkKLeI^#q1;@1ip{0l|d>5 z@0PZg7pwiw4as17TQ?bY7sY3Bo-iPufk;z9N42PpUf5+jm<-3W5qGHDqIK%t1;w9# zz8B|Pupus=bsf`R;cnI_{Ar{jkPkxbyh>7sW%+=D&Bgdbjs zUq;6a*olN;4uBRW(Bgl^N3ZYgLC`MLG`Ld)I`|8#lUI)YnvAJ8W_@5}(=I?${bf^RxA;Go85ICkpI?$u>5~TW?V7(c zxiejM3L-f-^SW2c0St08pHA4zhHoi5rb@$5;1?7gMV^c=8OnnL494P6j?jtMpGNLi zhvYTG0d;&djSkjnS}fJE`T#xDqS`BCIq*9_+vjaoYe>HD_&9#l4v!9Ss9Qq2(LNdawMOpD3d%qx;8 zL_?77#YM_{IT!=rq%~86ombu@NSGBU6x0|2mVi^m*O~V(df#9i{OsU#@I@pTi%>pG zp^>&u@uf89P$H*#;>WRS(%B5`f1R>>;q(`(>XCtRW|B9*Fe!bxD&8SRtW8D9Z#Jpn zm)aemu3w26&A5Y8>B`nyz(WYZcadreJCF2|=}WWOwVqY!T4+w=A9my6LUth37Lhk_;ke5J_S_M6gpCD+r>PJpp|~b0>eQq!)uGVf8Szg5k>)z zsqmF#G{@*yzTr50n`K)@t-A3e)dnpDWvkM=Q9GwOdjCA(x?%9&Qhf%ex1pOfF8`o{ zB+X@K0HuyWq&IRnYvZHQdRH`QMCI0p!GZ$0>W3m};+@He=OuM3;%)&p0!Ac`L)`9^ zfIQ(oUw@vE&ASk)^o`h6f0x-VF6CmJAtZXOb{7|XVNMA~t3+;eEmqwxXijGeDA24zvk^;Ym2lN_t+Xqv zaWBmuS&y89uSgCIfAGrU^J$x*5Yqgkg&^Z1;4h|3w+t`?5#Z+LYm^z5TNujFfbpi&K~-YUFUa8dfA}jHn|TT2T!)o%1J~md zmFj%Rcv?M!L7t2Kor6OV($mk&&ktVqSj4lFs?;ye27AZfzV!5RxbuDQ^a38?UmPH- zK!D+?Z+deOXdnt+Db@9n;{lYF#H&mVsqkp$I|SVW6f0dekxkR@u-j<6tgiESUTyz= z-pXgMVHSLtf3#k0zZbza;`9a|H{mgzq(vGR*|bH7>F5F;w%-5n!*}2Pa5K6Gj9Vr& zsqbyV-@BNI5JX3wFgdQ4P6G(9|J>i$dT;mB)19M}jh%EV`f@D$D4X#tDBelB0oRfO zqk9S;s>%Bu-_%JL35<-%$VPD~l8FV6N8m7o0_n2we`Wd_m5(7P0jXFem{OCQEqyQ( zT;csf_gDbdxAZ_zJ=C^4O=2@sOT=8V2H!He2ZP08DtzK;c6`W-_~sTkn>gJq(I3-X z)-(PQ2TCQ#LMe!*J}AAClkW0aW-YUr!6OZF+JvlE3xG-`Sh+nQJtbyHqHW3>8Mz}{ zf{3|Cf10P$_4<7o2KV;8O2z}Roj~`qJ+JGIsUQkFp~;tkmKz!|D8*Lhwk;Q>Dv!ip zhG;3bwZI4lSJTaxJ)XMwvPGS)EwBK&8D+HWARvI=>vkG<-mf7|7OcY966EwasV$epQjGeN#Uw=eYh z#r(>4R7#-44o@~2gSj26b2dUcdKEDCb%O&qy1T-|s_3V@bJagj!oP zT$Gevrb9Unj}7pW#q|?d=4wT7x>QFb(OE6LCAV4uTv>%^prdM(F289^QdBMae>wjc zJNagi%49n*IzF{PyK^m6wd9f3`WLi{rtns6AwZ+^Vy6b}^v?8Zc0&!}j(Ap3wWY{> zCuri|>wE3DH3^B#M$)TdD6qC2Z+6F|UjchxvmO$MVPdam#rKlev#SGW<>VO!p!gAj zZ~~eezjq?hnv7eb6C~T8W;Z^~e{-NQBsQTnirP|2XfZ;1=}E&U-xz-s7JV2Q@wBrE zp*TNSdCEUk{-&0gvK^+AM61ykZ^*x6!1C#&9yd9~RR*Rx*$IopMe{PTl_7-g(v;_L zT5r5RYQz7Xx8K;xU%c_oX8y+Jd!z0fdHapL(-M(gwn*m=R))bf;i}4hf5+aU(pXfU zrOhS@GU_MLvW?T<5~mX2f0ixjr@k($rHg-uI{i*nr8e#8C+7B6wZg-T1zG`(OaJ`u zQ2OVMO84bsCk@TBS&Yd#e?*`06nM;&<_a9Yqe3C zs+$VSPh3NnjL3rr#3U7xpSa{pM&o8Brw?6g;|i|@qU|eY--_JG^0a#Bd1_cHh%OpK znWQ>`)(6lA@lzUPso>^?wv8?WN@9eO5>TXS~-nQbCh?`0gcoUI7BJE3}nc6GGJz+d? z44v68`_n8fK#(}U!?k_0+}<<8J76?X7ghn^zrewf*{xi7q;_=kU}a8w;M0O40lJtP zHc3YQ?mM?2s1Lo$e^KeRabY+RZw3d4BEZ!VCFT$7?Pg4-%|s+?ih z6KTdJ-N3I2yy(vnpXR6S8Hv939*8osEjq#rzjzvqY{HI%f1aG9Uv|a8rHFY|zsN3e z84IS}PP3oWQ8J1|Y>cAXHKrs-0quhODE8Cj652Iy-JhzFM&5l5(JO)!60ex0DxQpZ z#5iV|C1N&&NSk*9q-M!&JcI_Bi)-qURk_lDEIJf+eNE577RPFzU#vj9W}GXTj8Sop zg5W%)Q709_e^9Bd2o6y!IRD?d99t3pVw;RJZui~L*fv$GelpC!J+)7ORG8aMHk+^* zl+Lnh!~PQU4(b|nE*35EwH zA~^vix}Hc`1*8b)O$Usnzws4dF;+G!=U7bbbaBZDf3K6`mjv?Or1EZoaWo!jCtRWm zn`F05O;S-_6SCc9*~Q0zyYR*&K~Nf5 z`u%TG44Pq6l6A|Wc)Kj6k7@O6J(!2>mjlSE6E(dP3$~r2t8byO21*}9-7wO2m>B8s zz?=s3f1C2#%1GlY-jgMOvK=B+a@5j#QdpKQ=WZu$+lbCrqEl?M2Gli7)xI~w;058i zYnT0{)27>%JAtD?hhl*k!lF2}G3@-EfC|jC78ln)*_N>{%+a<42%K%}dp5wjn_Hak z96gb-9#=!z?E@4v ze*PFk22P30GDbP~h8b$$pT?H z(`**ewSk?wx;EfhHFk|iN7kF!V_^e%VWbV=dTv2*VKgid#x<2;C{`#xg^qcwe*l*$ zV9BPHgAWbin;d+wp963A@F8J5le4G&yh8mZ^jPfF0EOBxVD~1OhCad{_RX$eIRD0* z3jI!SKs+MF>!I3enKqIdd3Czjwu9xlM1JM=w+*_xhmqFa^Q=iOih!#1iuT0Y8BCdm zRo^n8HqnF6yVH@Z18SQD4aZre(Icd>&bnc-{d++#NG>28MZ>2Z!=ee>0A_0hjcA zQITX*rH8hwmdI5>WJHh3`ML4MWkb`#DhBA?8xY(~6>x5kDU+iHc^?OPT{&wGPZlG} zuUdA|2|HwyqY1zTRb%^|vdCnV+yW&$t5b4NV1d}OW$ZR_%g1&*JuS9U2b8sU0eAZx zm6;CP)xM>edgIIrejjL1f8>sOFTg6Up>sqi14gMiSHVT&%|C{B*pU`?r6yitw>_q( z^VbHjzl$e9;KK5^J#9n_`&OC5N*zqLt37**lW9a=iGFZm( z?Pi-c{XVGOrat;JI)IV|Ai!Vh_^WaE8sBDBEz>ABZi5l!VXZ%Pf8dm;Tn!rx_SXhN z`jTgT@s##d-f@^~gNWDJ^l#gsE@0qxLo+%v6AQGVm0Q_YY^O%1sA#o#Z#eEn(;9Za z+SooaV2rx|AoDKKHZ^FHj3+#(4Saxa*fAaV=Kmkl8~FsP>K_u*SyU6#9~LQaczTgY z@z~~Gx>loFo+N0ae+I)9Q#`UQ{*B)njm*46P<`6qt$n0v=zj1h+J7c;qo$ULr;P%) z5GxJcDC^21H%|O55G)?s+Div+RLWDtZPXugf9UYgf|mF%hHW(R64A6l)PA&>UGE>w zyaT7{|M!swK5~+dv*I7yHQ$H`+qE1%yky6>`xg|N~T_=_wa)4|DC6a z^Wbipv)d)CfBJXu9X9kTTnD#YEZ{KzHfQKxXT<`;pDP;tM5V9Rc$|XzY-Y#*V#Nnr zFH;krvc9j%QNB3%FIVy}(kH0nUmQGv<^GG+`ipd#`u3EydPS~L+G)3tqx{>P;A9-% zCDY!dNWexc!NDgKLtJp3x z^)S`GQbKj9Q2mjIoD zyE)=j_L1Tu4&OUv80nphT2+2cM`Id}Y8y@%e|jEbz(G)OHs;V|3cIac=Y&`lv#~(x zmXgRWS&Q021cq{w3gN$|(v@yGT8zxvxTUf-hw)UYm6bQi3O_IHpAXiR(uJpu%JTXp zYF5e0hnx2%!}vCzjrEOxP-;Qj*TqpXnTh-^FOnN{%@~STAO@T@amHmx+#^|t;^F76 ze-MB7j8+C#9?y!2!E;tHfeyDI(N5yxnlJR#AbEU1%SM`lHVvvd2E+7HNE$j1K{JWU z#C#Mt2)r|$#*?cA&)SuTe&*^(Yre9-(U}WCLdadWAi}^+Y1E@m^UCD{%*Dls&i-K= z1kV?N*25KF$P_EC#EvKsEt!z^nD`~#IvAFQ~ z9))s?LYdT=R9Q9XMttE3hyb7OgrZTj_&54he}DEG z5$18Lwj^InN-~?v1OYP3`M~92JiO-fd~bkfu6Kb#>TEKi^TtwhF!T}OjIzmkAx0o+ ze4E5m`q)=^0;Ce7Brm4fUHN!*=F!iv!JpnyhCE0iiN%msK8(c8EHBg@5PVZ|xq5;< zKW7rzf>W8yv1|n>l=0~laC+CO?dqe+&F=KF#Zy~<^!RaP(yCs@g> zbgXFZ!jhcFiMWI*^js=dlz5{+yJVzi%$LMr2o*^_!VEg=h&33x`C7R#f72E4fC5yI zQ1!MdB+=Z`D`UIkJyyZX2bEe$rf#y-(JBKDSbBcE5n`!;gT;AZyfHU8*B9PB;n|>r z2>=s;&Dz<;bFOTYLt;VR zjw}mX*`YQjb3+c4sT@^uqjxXq++nI@)cRYinsqoOdl6^6NMH9Me{i%v{W%%m@tA#@ z+u_%a@P5c%Dxk9*(tx>r=yTiEmwhxA!VdP>aOHs|;$udW2uJ49Hh%Rt)j^jvGZtc(`=y5G@5q5Rd8=H9F-@)18KcYIw@ zlIQM;Ex{p$or#F~S&OYRa=X!NK;I-@)bUaNWbas;+o*w`^Djj50SB(fYv4nN0w!@G zK$DOd7+{#r)xIPLjxXGuTLP0`%%5N%l2$tD;b69%6;a>aOwg3TpjtjkJ~O7i;V9tJi(^c~zzQh2^FtE6u=S zft4m;;lJfwHzqJ_7>M8QkNZc1{}_Y*KjgcIknd>t)d9X@5JY=^_L4x)fJI|GAEZ0H zo0QqCKz)`Of68ExBzR(1i|-i>I~G@37gJt*wVBj<3-I@tx%68lmhIT>3S6tP)coT2Fln_+Rvw~WuE`Uf7z&)$Cn=bh|Wwc?NV=5@H=;- zD|VzyrcVr5&lwa0-eVHQfNpXgWhajMrS!ck^tnqr+rROg%k3<-#Koa{2L!8)lx`0l z{>kCP-zgU8*XgeyxmCgg{e8Lj+|bw?$y#+#t<cF>nH#bE z-hJvTf9yJgj*~e{H=1()m?~lk^W_i9xN&;UN8X>=rPG2`1q$g!mBAz7l}>6(eQgT* z9qgX?GFAO%fBjHuRd1-%#nn|=H46N`13ElDs8a(nnghB?P8)wX-#n~8AwZf!DX72j zQQ*d4i-q6%Vt=MV^oxwMys}VFvUzsSgm2dkfArhi5VHB0HLUs9^0dm&a*)h~eD*~- z)m)7KaY0Z5mBU`JfJq9p;5oOLb~smyId;}YpAwaErH1pS*uWgy+BFZjo9$hUi<$KF zlRxlTJoM%_x@0Qb)(YBdmaD8mU_fzRaOKPhLxUP{CoL)bSbx3CFB?p+=hIvbr#K~D)Q$nd?rt1N;O;Z+;&uc0>*`@X!vkmLN>xs(Ng^~ zFJAzQIpE=Da9!9CYR0y1$9B|w(iRg;e+x+7LT|LJdUmbmKp)Gapnz+h=hJ+uH|UYk z=SXr`AfYB;Gy+*~^P(>hPk97x6j*m{pL)fOEjt)l(t|a*8t9^9{hk{Pj z>F{tkKKNmTxNF1VqfhaDRRyn-j!s60d!y6W)Zh|s{}Ry#212sm^^RRIHJg74x0U`R zuj}kQnuUj9{6ic}eK0y;gRN+H_)Vv?F5p&Lv)g2-|F3xevN|xOf#fP%6HlIaaqVA9-a@w99+a@jty-S zg;_Sqe^&J=H3^7t8wC1`;QmmUN5af=(rZ@U@Pc)M2*v;6EIhS@S_fd!3CP93pHcIP%Pbr#4(- z!lOMtqU^)Th|LFh)?p-fe-*9v-4kPkT-%q|9I#t$JfY(CDw@VMimF94END6|(wPG8 zkYH3S3Mtj>G!c;{JJ&hm^=bW<3zfBT>wV#dI~;vmk^ zP!y*6JxZXJIF%k3{osa3A_dHaO)aUuEn3Pu#e+wpRSX5WDY@R2CcRnTc zO4R8^adn$ifceY)=7uEW)=2@pv6vrytxAUnC&zGi`UvL-w@t&glP>VjM_(Q69Ua3} zNK`<$(p?1kaJWz44!gSHPd`66CTQg6cA^vm-rR@cxytAB>_-p!r{%gBXrs(TucwLb z`W@f((q6^$f7k6WiE3IJFIluNOB*TG*4mPnO%Da;7Ykj5WG@Q{UhT=_kK(X(!7^w&u%zy!VDRp(`;vHT7#0;-qY1YyGKlC+K&Lle@{-0fg z)|zofd!9W(xL}CL)TFA{v_wDyNs>}oigEh+ra`VPpL))YaP3f;akOwu7uc&fWCXYA z`MQy~f1o!THbz9P=S+xvp0Vtrn=4{;$SZ3yg(qvN3hY3neOYz!d)S?dlGNp8-j-1v z!28#YU|t+AZRondZA{aCLt@*txL62xROF|Se=)dS#wgbfN|jQv+LlNmDd@BX*q5QC z_Yq)V@#Yeh`w(TayaKw$X1)Qby0ye(1qOIULN>LBvbH&)$b+JRUe%$}2uRKS~Qq?8|b=;O_Ie)8gh==5S3 ze*)BzzP+}h?mexrIk8$yT^;A&RH*F^u?{=KUc5!l0)adoVAsY`J7gKP7f*4j#+Kia zuK`=1LUoe;ayd(>QhAHPk`ZvZRWRneoF@0xXWD=x>t1HFm|0gTUtkOfvyl_$R`ZLp z{H1nwtl2^@vg<3{vw#Kk{-kf}+>Kd3fBC~%E`HeF<8XtOP4ly2PKBrl(Qe}b=w?CLBaZCKAB;KE;ahm=Hx(nGL&mHv>M&a$a zK$w#eJufucf31&Fb9$h{Vf6!dhqKA99X%aq^)I-CCm-~97Tj#Ps8uFn_0I$Fe-=i= zKXWWVn#y~G;UBhRL((dv6jKKMq>#axskgY7vp@x?WKw{V1|=h?-g)h|ny5db#b8oI z-qyL8HO19Tn&9gsR3%vn-XiE@BW=qyk@>HST;JQDF^V+)szBQ|JQl=W7+5L*Q#kEH z1zX7E);>i9<){MbDhg@n9SBQ&e|986(-M`YX?rx-zqoPG_&*<4F6MP!HG`HZ{Aj-Y zlr~@rDK)UB?{%vZLbyWlAHC8)8KXmsTZr9vzSS6zQ<`{{-{y=!9gMLN zaWLg~I;o1Q#)NT_^5c&E%8}VN6)|p5_nPjOBNn^bHfJS6iqg6INWCq6e`t4Yt=7@0 z^nq7*qP8*BJJ2512vEtk+NJWlKpEfkedo%R#s$5q(i}Amc9rEjitVREElMW>&}S6( zORjdnS;}vYP-voq-If+WsxcM`Ei5Iie!-;MW0cZ8=$^E;p55>E{Z~-VbKFT|!Njv9 z?7gqc1$eMIdCsOZccXfde+58gw(qx?VjVLT3#&5syC$}Wr%Hm*#p|t27YBXDx}BE! zK|3jtzzW-F`b@Wten0SCR?xNyLXutL`V3SEa^~IeFCZHTgWktX2;iEPi*6;St;Oj( z4<^9B3&R44O-j-u^-gOpUhk_SUbhJ}drK#IMdt#D{VX0-Izq`Cf8cc|H_c}t$*FEO zOR0eECayZrpi;`y+QK`DkoZ8oL!(JC9X|8Z?kf_2lT*Z=vG{;3YdYmHYrwy3#1)nb z8QdL}6DNsjoomW*(=z;~UWLtMpi34Cx_0vkj|JMIX@U5U(M&fdD}ymADU<2T=^rl_W2~E! zirxdoDSp6OY* z>$vB3<3%~Q3G*rTlHgaSW*gK&P_(GK$|js^WU%q@#L(f_+8Lez{L!2((F;;G8k9Jd zn9zjUYKm?xUH$Joa0L6$UZQwUx!9bEzNY? zSynfB-RwfD-Raj)|MYz4lXit?$eif-1wg;mQ1_>KP9=Eco^tM}aa3HR7u*&L7Lj(v zfX8J6zaD{bnu|1hRu61W`Ww{pW-Q~nE8}<8e@JTzekC8e3~;T&``4`ERho+2kg2t5ZwXe;C3;UiNwc=qDFB-r(b1omo5EOuSuOkjqwHmdlKy(zgPohA!EQAjngLJW+o*$%(D%CTX7+S8sY6!quJb zeLmX%=Bv>OUF+guidW`T;G^>&@$SI~Yg235+a3h#m5I6t?bm2+C?r-B&U#7qe+V^p znxnrH$3SfC5XS!ox}JfiHOtIb`?#&n%O+~CHj!5AMXOgaWgz3umfX*dp{5>YyoNnG zu@`C+DvLh8o!bZZIh{$Fl795q1Oj9tX*>82u_1Qn(4x+($FuBQIhl4iZ*#xRW?=h~ z@%UEOttr7y4ql^pJj$kK?ZvPftTKx@dlQf5igY+6fH3 zf@%I?2qafkhBIH3UiVOp9lU$#Lsy<383O86MGo`~Y~ilB zr;ZDgi;Em3Ef684_K_Z>f7n`k3k7fo@{p{!D7=lPzDUgV;i`u$pdt;uV@gTVYQpfh zzB*_Up0f+2BM=e!1Qkn}+CAqXizux~fBm$qk<{vNrfu$PQGbP-p`f1Y&RjE%3T_Z@IZ z%ELNN^4jw*c?HoWZhfs`_HJ@v_Z}OO^pJ%K5$b5uFX6FRW2eljOyIU;Y^^zg7!4iYn zPQyPjTj7pn43;#EN%sy;_P!bsn(jYg9237BTr-b_wH#bnDq8)f!;bFqCYFmPVlqoj zubm%QCdS7Ae*}FyKbxo+xM07(b%fI!d6&r6MdAr^T_nrJyE@jJ{ zcCNqnz_U7?9S}E>)ZIYQ!yuLVzM$W8mO+h2_fHbwlBK^S+Sx7k=;HoqebivpZm9*n zu{r#3bGb(0Z`BWD2u(=W%HT}f7K^8(vla88_OEISY$DmLwl@C zcTfXt+OaO#=RjBMP+6!-@4cOV10u*0|B0f#i5&DNj3iSvT3W zzuj#$W@01#fKRIJ7wK8>n_JFJ!}c5jgev8SoNM0N3Zo_@@Abs(g8g37vW2Imc}gPM z6WpMKf8N5C&dtLRT4tf?8|ljL@a1o% zUr5=2=o5WO)CuI3m&D$sci`Ppwyk)S$o|>`6PK<1G$BPl%}_0Gjj9p{_W{GF>-_`m zeatMbJ7KKQJ`me9+2j|#b8BAo@=5+3mo#!Z)IxmM-8?$0hm0E3Yux*O9SQm zG9Mz4?g&v*$q+G7W!z789oLP)jIGV?ZJ`k^&)R?uJ#OY0?ITBZUhN&!?ak`RI;M4JTT(Wif?DT znw8g-#27c#k7ApLjb3Z8kw&5^T58u}39)Vmb!gm9X18&7PRVL6xwfEMst$y&f7Cts z1a<4WB}kgyt=~eZW{-tkK23SEzA_tyeo}gu3^t`6QtFtHue0GTSA6~7R88GcQ-1j@ ztDs-%KR)j{2E~fk9wTt!kd_fri;L;f6`o61$c zYkYFiw?9o@Pite~uEn$m5B&A|$Sh7k4I5fNJJV`)n~5QSV_?MDlU9!cP@L^|9h_*E zxz^EAvulZN)WbO$d)gyT8@@kef{ht2hyO*(^C%X7hQJOh7?O#>KHQu5(p@InN zfm@HS4(<(Nfz;!{fyU!af7rPdBBbpN^hxZ6iHsU}z5&iZ$$n|Q3{QNYUBidrJJ(FE`2uft z?Za>XMk4XF(;Q#A;+~Sxf-5H&+@F*+y_ULww5H17?C)yxqa?0zf9bkRMjb@@UZ!}+ z?$sxri+e}yM&>MAv!%En$ZQLjlob6Vqh=@R=U)4{*M4rpBmC|0qCz*qIom!AZ@F6r zKc8pMw|}CG5#8h(xRO))+g^G$KaZwA z@5-o~Jg+GIU3bS+e~b0wQW(#_Q>R;Ulbx=}NnXNegb+4A1{*p8YYBmTF8diFhb8;( z#ovf(6Ir4Ui6K1VqoOxi`u;RrPW<0WzS)`f=PM2ZLqp$FT?td{ODK`G*wGRzrsGij%f(50tiUALDKHe~ERrV!_THFobi%Am`n;)m!SF z^aD#heF@_QNhba>AEe2}naIbv^a(+G?fRh|V%=o|XmJ5s62&llz)=ciIL~G0^0>q{ zO%zR<76dI8NZJzuiaewpQV}b5SV9KnxNH^Q_(d)c&U|$Bb-Lb|MyE{wfewSaCFKy|Lv7YV^$HPyDr=!%=S~HUR zM<3x&7Qk#E?q&v*xg#eSQ#k7^lf`k?po#Q*fB4}Z7?W}U&m?wo$#Tc6uwk2A(yh#ID6KE*iP0H`Jqy!H&`-O^Hy&a86 zM)PS(8uZ0D1LK1HtvlUHBNrnL7? zo$4EnNJ(uWv4KY>7K9FTav_hsU?%RH+D_lt*(PF&Gbv{GuRDk&_hG$wHo(Vs9`LIS z++q5_em%USJXpcWKkKQRv1QkV-+%jUN;BLDN&Ja|#vsjHd&;&oFqOQa9=s!#5l2XoW{|c5e?lOY z#Ktt+5AGf`W>--o5modV-@Kul!^Sl3t9x}xJ;zS*<5^5seCFcec}P%?GgiSJ;#gKN zQRH;=>}U9)b~pJ*!m>N$n2dwk5R%xGZ;QHs5W*yUmDM;)Vt;=*FJHj=g7;hY@kyEE zvUYzu_u!hpt~sK#2$&RwODykte;itl{|UO3Dx(8t-yWQPGyE#qAVzo_;$Ux+2v3Il z!;?)SJUBTWZxY|hcN-;d|Eq(;FW156ouz!GO3!ZMwh`sgmG|<`UN)DvyzM#2qSCwK z>yLX42Xt%~I3lj>ntDXrY6 z09scvx+3QtgUj8tIR*>yoZ)X89NAd=-q1F)d%Z1qXAZA6Cm)rH#TMS~!)+Si)_?w* zLW#7PrqIl!l?pd&V{-c4Xn1^d`1JFmulC*RWVuoi{fhN+Ayajb(Z6=ovK8w-%m8~> zHK*TN^L)DYMVz5`;3Fg&e-?GPoCA*h@|q4DY{*ma@cVyXAyW~^-Jb7`^zjai^grw$ zPDbNzP7a?wJ~$b@0{^hI_IvXWLCr=5H|-#%IX=RPPZa3Iv`s}3M_2jkp5e|K{P*(< z$cN231;F@Ub_qwvqgUn-pzc@V4s5*}xHg^P=y30zIv28<`K$>sY?kf0+MMrqadLzR$?_F(Sj* z|7|E@L!W++UCh(r;r`Rn>E7^oL#CvjdZJtF?Mbyy z<iQkdx=&q<>)W`5SIS?l(jn}vf9n5`i0rfND5@h^6#!2~PySbv z)z#!ApZ@*T1cbLkBj-#0V92_a&TEW-w>*)6tNNkLZ!5(@_ysE zBNgQy&P&Ke5Gq)zUtpIjA2Z4tZVOk0jHi}t>!xhS>?Za3V2CGy3#qY^c$jMrw6Q!O zxu`)Z`81#EN889;a(tFif|G-@C@RBse*iCPoteEB9Qnym1GJI17l5ae@FUemQ#pIXSwT{wfFkv>=Y&X{8c_L809n4KpJ#OHD zg2FE<^NPqj_;TFGu!a>R3*;8V|LHX<%SLJe$$Bh1cyfGnG9Dg|A1b^y;PmsOe{W!$ zQ__A4Z6})|@^1^o!BnSe?Km{s~UwpT4FBmwYVkP0^b!)M_LGy z{4u4(Iu74GvkzF*po9<+UUGqd{!}Vo}r=fTu2?e@>|Hx?EI%VoI$Bm^0l}DA~No@v>3dK#ZhVnMDXT zJ)s&Uikjv4I+<~yOcnK*v$})=S4ES}=t2o~S|(tpz>5ryMCFNZd|VJT2vRU%2mitQ zHnbK|t?0wC>}5U&$4{2!voEWy4t{2fx(;i~FM4IOK$_uS%seM0L|6K;e@BbU_=A)* z9T>{j%hU@v5M5n6)t_FLzd*wu&pZJPOm@*+(9?xJY&F!}o&ojjxy(O%!ebM)oenaWtghI?62krMV{r7c>+S771;#8+RG zd`TQB4ETZxG8y>@Jh{f~s<9&!M}y7FSc#J7Zr9Y^@)-sKzB~~pf2eb}U;`<*w`MSJ z!;JSJgZUZONda?E6)UQ_K7H`+{w~jrBQiC34}LxejzJPK9=?%hH&F$;TJ-|D;&v-b z^j#p9_x7cM#e6bbkl@Oy_;cRKZS3-b1lLt3sEqYu26)>HU}UDL%;bPJ;{#`qB7zGz zdQcMtGgHzCkzfade}tw~mvrM!(PmxEQGNoz8AjNNqErs4;5T)tlYBq+?i{E^QG4;7(Xq&1(i=JV>;-#Yqqtya0cq z_C)FMJ^y+OrCeU|w8fOi{(1MNzzABz5l%?w(AE-WGkmmDZ#MDZXGkiY!731+zv~@q z*J$u;5AraCe^M-+j{!IfDhmb_CIF6fIFUbTku|P3E)y;&Ir%crCS&CcxcfLlD*5Sw z$$Z9O{FA{jk0YLJ*jt#ldzjiAU>eLQS4BzdUgwSMGa9%|X0kUt{`BL$PY%B`9<9B- zyaX5VX$DAQ1wK6dc(@P#pohob=>i{SlYE3>{u)Z)e`ya~C?a9saS1k4CfPNVIr(t- zDdD$dp|Nrk@aggA``~~;jbLCC#=YkSeGw*0q!^kqDRN z-W^V18p>yVN*quc9};xI2i$h$D{ zk4j>E#56n+;aEmBUbNI-eS0Bgs;xL}eF-(QTeL(fb^BXDiv*GL3;CfN z{5&m0)yhd1WjKfB?A?kI)TN)(f0LXdc3Gq#ynpEoA5kyYEi%IYxB{apJ#m@DP*_r+ zE-J}cDkxQxRWJwO*ytR788*ooL^>K}e1$hHrcY=WiuXvg-^Apit*Ff~dMYEbw+~*g zcu{_jDO;ZR{5k@uVd7Ge`4Y3o1#|bPpf%6gke7{O0h52=uttvaG03BGTvzGzro1{t zQh#d?{0pq%{f0!z5_@1sU&48-`}gO*!L(ipjAoA@fh;7H+ZDS2b&L$HIev%2w zH5@*Ok%40}V!&~ac1Nf@BCt+ai$E{F!=Pd_OH9SvIynL7GhMI34&yZ=mHi!mCx2?K z8uJMueN3(48hy>-w3_w^@QPiPq=n?<%nrUaLxz!ve#xuWOBXljmpt6gv%mwa7YY4i z3WX~4WWq_lNCebMq}8HPWFm&1WQV{M(2EW{yS0^vyL>l}84o88tl=cTdfffAuG1Ha zX=@-CDez1y>)wcHuCsJ9yH33n=A*-^6sJHuA6)Zk0+SyrND(JY<*SS8w2v412>c*pRF zP%;eWvsiF_LJ#oZEeCHbrmJ?l)~#5tF1!zQJf;^ zP9L?kYwBF6GLO3LqzaFw1NmU~d3}vDf}Yk~y0PmGmi9l-Ensw=jedkCuq;^Yv|Ih< zIv-sd@n*A8QtMtUuW?qg=O|(w0FVT%=NEz&AL#o_~()L1{YWeZKuJGgGZ~ z&65_Tm%Jm%kRU}(QY&ch!z=EmBpg!Pdk-HQ4NAKqI?L6?9*B|}?fX#PlTZE=hJwp~ zQ&!U3j6A+7vtGGx#J~K9Hc2f#Yq;3M*GyCA6=g^K98DkkHBRl&Fy ztZ6oBqYlP#!G9gY#LgdFnr^8?L$R0TDN`>QpMmo6C%;CZ?gA)!7(LK5DH14~WVO+^ z#oG4DQf+(Xp|tH4(wkTQ3T=C3P1>f^`qV8d{8s|uOBQC>C%js0;T^%7%0c?Chv%Skb zy5Gs`h}J4C{ixHUTIQKLgKlke0%--sJc$L_%cQ1iVk_ra#8FH<8e0pmZiFN#*13fo zPWSRhJzSs)U7I5zK!hZazH}tLt5}e6lUY8Sk5}GU+TLUyI8&#~v93c#c!;fQ@g=op z(ZnZ6N`D3<;KJvPCxKjKA$7M{I*``bX|xCL6vBHH__@6u%x7ubkVVQ4^c}5;ZlTEa4hOPJ|ALWtVPbZ~JvzCt&xFRtuhRx5 za0TrPtC}7e_V;!IFKMRWAL|%tyyadch1~_Gz<&uj1T3GC66R5V)$jLp3D{^nkusF9 z_5I801!Vz#69An609)VhB(I^705^O~h?k{j57huR!JJ$RkV~&0*9)LEY)}Fd8B+H~ z`qOTesx?GTLG{^2WnN4dWfWj@q}pMTA}1aWntf9zsz_u!H2HkibziM<2SLFscHwT? z8Gj|o(Q9Ja^PN8;`nXyeH=tEYIiZ|gG^kH0YL&z<%zlIhx`O+?G&mDPuuD3z#MfyN3V&sdObcJrH z>unKvwmwjU2=}7)l-YQ_tpT^(2WksomVbtLZp2*)r9yow+q#P*Y@AjfPcw0fgwch> zwwlpQTcc=6RA{U{j?I=CB%Z0 zd?!)?e*J_3QlCsUv>}3)xGu5tbDEd6yXc`0ma1zGmNCmp4&g(gHh>zA4V9bv*ni62 zSx*kcz?)W!KDU+~P3$>@sSJ%uQetSCVE~^bbae_28zN5BvV=72ujp;FUHblBD`i(? z0~L=+GEhySN5XKgMDj5;2dL>cRfSGA@yZm&RO(Vts6krXmb%2aUgU1N*BQQnxPcrT zHmkhoHs2J}>_KDLP|bb}8&2fU!+$V_4Q7T>XE3#=1AA6#VYhLU1NM!sTtv zHte+m^#i7eu((OQRk6sqKp5Vl#=iQcpKyZ^cV|gZOc$a`P_*h-O=Uty6@SUE19j)8 z{H8$8hLTh0+LSq96x=zbZJfHXymioXKFlzB6L*@%W@6?l?Nq8=Lt#iBBN`@#5!Ivs zqs0rF*n#>j${eMdx#MpxIbAg~=oknpZn`RgZRQ`VYlbZcQ}_f6-lVhZq$GNP3)}pC zn{U7TCV*0nNu8G8##QCh4!F8$>H;|s#+K~w#_51W zrcFq5nxqMoQCtUPhe;!wR5}>D#H+i3=_c2Hz2<_kO58By&k=$k5_u4Pbe}MDAX!+Y zR2iK9lIhR(cI>d}Dc7YA$N8+#F2&cW4*6XD2rHZ_o7sYCrOwZ}hJW*ORh00>o~`HS zOsvF^965A+a&*f0^r08gLkp@qn?KD)IQy0PWX%(%4 zc*>a71f%UKr~t`gM2M6tNFFT4J9)?jLEbT5P_bQ1}Jp}8%T7ZQm*pv8G&9n;|)a2dMi=xHrSn+>pq1`8U0W2%$=@TzXqLeAEk)1yz`JK8%wI2j(8?drxS zymj3f;K#>@?<43-^l?1=;6wa+nZDlJ+y4kYUZIcokB?3{Hk_74aa%$6fAad~Ky4nuq@S{bEq)5zjNn9$CR)!Z zo6;htaJ;Yx8~pQBRr!q8TQ)|LUhNd7ngnZMqBa#jbYR-D3xt1Aq?5in$ZjXa-O1kT zOe`icY<~dHtm(7Xc@Y|+md}AVPu_TX`o+uJA0C}PVXTG$FG2OYwz;_HUK#oE-qTNO z*k4&6_K@_K*M~ep`l}&!*{d?XFp8*AORBd;6j6qY2g>vwzzRZGx<%#c-c8+oKPW2~ z@(x_>F5aBeaT95obBSQ^Z@J3{cRp4qd(YvI@qg8Wk?!Gs0eTGgN`>PSnGg+BD&x)! z2c{9gU1S{Bo!%h%e;jQ)j&A(hhS7~~qv*y8gGJjxbQRpK$Iw-fw;e)P!F`Vr)Zmkj zIH>oVdkvXg2F%|VP=SYqK_TRk0qEBYgy4x@J)XcYGP&f!aoKU zaV#jiOtUhu^0dTfDl#qbJ-C1+^0QJE?0TY;Vb_&%9Dgz^Ubf1oN&r zFR5j`U4s<3=B+KlEMh$Z2dfdo@J_1Izy++pvcYTi@Ac7P(AXL=jpkeCMnD zAI;cj@+A}~)DuJCpjS>Q`I#iKVs3QN=7K%-X^HWw&*%VU@8i?&UO0R2JL~{I=h-j* z%ThOWz3fFE*|pdQ>h+yLFc@Q0f^yOVDzAF;nLT_Y3kx>Kug@jxRo1ckuT353wGqb$4E29b5nw4=aG*j~R?URbG zc4(2)a=fd#rFnQFEgDu&5#WUK`=J4b-_-h|*C|V$NA81DpeeW#?|r8r)qi{pZ0_eD zK6&Xw$fR8Ukjzh?y!O(2CX+uX?+-a{Ao3(Wx@fygpv2S=j|0viJz&oRKSYQfVdE`4 z^YMP?)N6HLM(`|HF-L==izeX0*}hJ%FH+&ggguKnB^c9z!e)h>*Sj4wXVPBz+(aaPo5`XX{x)oc}2?oZDizBj|FmtE(EcXB%sD33I7=rqnP9(!;uD&vF6MMhhDg;*$!p0gt%h<# z!gKU*sMhQ~6$Dr&hd4y!kA(R47x0C^H*D33+aUQ&wD*dw3v9t24N;f<$TfR2}r zyBxrp&jz0DaUIr_QhzX0GAaP)%N;dmhydM&H9fT?gakHP_CcPCxdQ#46o*eWD}F(5 zWPlK>&gm=v9i-&=@IY-1ywYe+-nRIAAAgP($Y?za1HaHU91r9JX+-V0O>;!y6Az`w z1zuP>_SQYG=x(o$i_PD#M&cS&Mt_^?ajWs`;Zj@${}$|^LVuE$^0SX|^^x(0k4$zC z`!(UokR%hc?^g={IXYp2fE&;-4)_KU=t8@=T*plhAsuN2s>K*Xl+se40YXZR7m5*u zn9Q35F#N^P*lcM-tredX-39YePJO?b&AP& zAz+e8g9Cri2!A3uWJ36h`XwXl8n#2JiU}JQ)rV$W#CS#boyU&H`uRlQhP=&SUA&nr zJ;17dtubVu2omBpuw{l+B!YTxeuZ>|k_z?vG=u#%EKJ=)=fosd3DtO3lj?VgS5&aa zr5IM0Tv4ttJ0{FkNHT}6owX9BcfnjUlDz?F=Acp8rGE-U`hz2oD3JaVM*%KJV?dNH zIsdlR3T9s~Wvrutlo7e*2jN!VpwvqlgWCKtzT<3j2)K-mrm>ray!hzZap-IqH&+`p zePprmu*EpmL0d#J4cZ_AqrVb5D%wS`A4jr~ZJfzPV-YrZF?n=ww99)x31GrQ$nby{ zrLfF%UVm&E+Bi}y)pn->0qW>#(rcSJHIL{9;rExH3@bVv;nfyos{`{~J9S$Rg|I0s zEGJ3A5{bYl>{*33`hcmKBQmV5!jo`eEcouh6D-~o8-g~Lz>4-qn!!`ROPFD!2zsGz z+;IdO(Pd>GVU*z5lA%iyVM*nu1Pe=l&P0&-+kdYiMY5oNx$hMBJisKG2j50%X!2GO zi0!wCV3|+=I4&2ZRu&9zw^raiuMCBQ0vfgZ@n5;Eb@xN|T+O2Aq%(b$*S5F2=c6Hp z@OV_N6os!19n*paHxMSUAX&sdx4(D%!Oe9Qmm-<$G+}&LLUcLbqfW)1 z34b8;q5pGF4^K|^J~;eCjdL9CF6Pw8>aLn)H-NG2NgB3=D)pHmeV;O)R&rUzJ6let zgEju?>@$h{6T89Gr#`ULVMq|c$3;5M#=|Lb+3nsNQk~SCB>X#}e`Ohf9ch=AB5ppW za{ia()@Q611cK8E=G$qnw%$^ThbAEe8-H#IblCx`;MrZzE9l?L7+|<^e{B$5%DhM* zy#emhx4a1vA69U9jgIk>})fW50yKc{70%sr&!S2Qzw zSA_5rhTN;`fGLOry5I7y6}d@d0{r`aHpBxg^R$g*)Lhftz@=Q zZSjGZ%|k?i{GgLzSCL^s{}|K-JP^JN>RcEE?Qoq}e_s<$all#DB=cpp5B} zJ%$YBL1{GoJpAPIbLNL@u(WAFz?{!7i(V?3&OMs?7|!>Po<80CUcpBk!P(_$_Wxg_H!l z40!HMpBr^9TuX7!i#GJ2L>g!SBmKR3a6mKKg1a)SFe;u9Djhz)LE+a+i`haT6M9y7 z2WSJs$0w?4Bkp(JUJ7TLiMEtGw$caWRttGHs;UT+n0{aObmULfR(}ouL0k-uQsEz3 zV~R(*7?GKqfJAxm6wJ{=5K$qFsqoZb$}l_~-kFb5+`4f+29fx!lT#=B@Z4hQpy$MV z(3`&I%Xqk(Pc|j-6ZNEW;=y={T+z`KD!v`yqFUaBe0p>Q^rd4sxXP=UR*l{G5PN!**NU6>}0X zOw=OHxce|8#1ay?gI?6iz;U|F#=-(=P7d5nfj!l(XkA3@zJF#aGF}7S%vA z-jXV)Ti>G&-ham~tb~V!`&L0l5R2;J0p{Md(HyW$Ff@wYM>JeZ+ctHp&~wH=(B1FB z-SKfl5lbf}jAfwy$I=hMATgT3JQKHm8K8n_b2fvMvM}PD0bn(kYxUsR;PlQQ);MrR)3aH>cGx7#2x(l@HkA^`*`@p zp`_}0I7q00eUko+;j4rsa{Z@6JegUnEsuH|$`~HOK^yWC%$<4zo$@lJpa~Nv#gLiB znvW|UUXjp!ioq^z6;0t33G_o7C$j+_nfb}VIkK1ve35}g<)jJw$%n&F3GC!Lziom! zJ^pUx`qkwB1gGUjE-m9x?gf!dcB z0a`f@3MRSeIzRjZy4b-G-Y+}_SGnWeqv?BjIlDGqc{K9rw1L@##LFFI>DVjANk`>= znO4gR6HL3(;%=zkr*)CP)aRua4`>aQ@$7ww3wsowS8b5Hgm8xbI0M&;I4;TcaoHQ- zbARK@X|<@o6=s@AGnU7-aIt3hG6@a9N{iUWsvsp5$N!~0%{z>?F{k3U>U!}y=YHZ{ zH%2a{qc8kms&O@D-qK`qW`Mtw_6n$Ov^X>_Yqbj3EzMjA+pst0CYafBKtKHEc#(sv zGY0^MVFORnlUCNDeAi#Ul3JS&GW1L{&3~qm!fW9w;tw0nwNTqTHyKX}7ly9zN#r%k zQW_ils6r*>WOGF&ipmMDs9gQt9N7?TmZZiyt5hzMlaG=IXCtv-=xsjMc8eX??5LU()1S?}>j=A?Fv1i-lI z){u^0XSs>=!#zg!1O6u9)OiUZ`}LN>a2|fOw}1Na*J2G&nH?ILMaieX$)GbwvU91_ z%NVyI*HN1;^K3GG^Gd@=ze5*AyTVR{xrm&lyL)R_G=p4CLj6#yyfHZxjKvV&sM z{&+dbx4ij;8wX!YD3q?$On*=S6|Y{vnzvR_^${1f)XaqoJ4mP}j#eDABwMw-p45U3 zenK0LL_}1RFEu%wqvO3v)+|!ixBjF{jf%|k_ymYxWU!J0;DLf90veG&z^E)LH05Lm z@QRO|e@%gaEV>GVs@M#$z7!iMPOl9Ql4z10SDj#KlW6d_bvdtlg^_gP5)k~p?AjN>fkd1xoCsS{feFn`TJ@#Q&Jj`Ph2q3Mql5&s6yqIJ`Kwoqw=jio14m3T|3MxYqo z9op{t_66CUFzK&4qHajE8)6-YN$-RUSN$}J0Ji6RbaaN_85y!VipO7jnX9)!d+b&U z9m+`a%zDl4&d$N=mG@_+h#2O_TA;1;>yNUw>gxh1T|Ml5%zr;R&urOU8wyZ56BN+c z36J^m@pQwnwDWYsRPRBn)70F9Y8%QPJy+2icv7UaKO|&Mm`TG{Ka&3T+j3nD%GdzG zyUdcgOfg&XRn`l5`Q~5u}7=dOj8ar6S2S%p7*_cGh0rTvQr?O4yE zjK+>dxzwdvmRo<1GAHDK!2Vv+tE8{k5=**=0h5ULk$<=%QpnL@BMHR?dP1c967_Wv z$mKy(kuBR4;e{Sx%unHj5H0aTEGq?#|F-_Z5dVu>`#Ja2;Lf(Gr!K$c4f?5UlLIoS zowa9-b`_9gnZPsz7{hLF6g3Q+9jYk!w;1_C@wO(>7)2(@~mnV6OtI!nyT z0-aNn#CH5Pa;nLgIqkIK{eruZUsqN6EczH2=6$yy1k^ftM2lz(YpWziRqZD1sFEYa%}G1Ty1^uqKuq@4OZ z`ewcj6bE}ym`AvY$sOMC_sJ!?z77pmj6uxiAjs&b0FnebyO#BZqm4W_p@x~|F5CH@ zY=GC?9&Y%aLvMx@M(UmwyM<((DgB;u;~|4Nd>s{(Zf#0Za#+k~3{KiioSM>mQGfXj z!9shA+ZsxjtAth9Dok4XCW!&yCrEaZot>BDkJ?(kxnFTaqLF5Mn|kv0HctD&xWq&7 z-*DhTVuNAF!$$W``uQq+=vAerHPV0;2P%yxHeSJEt$C>8)TP6nwm%SUMksm4`m$501$bY1GjbsvpjlV(Q`!WfLU)W1d-sCoC-20GJz*twr5edyVr)PHPy`yZ|g zL}&Lk;97Oz;YSA71!0j^w6bBW`W{WL9V|H2--=Vc?}ccSo;-OEeZJ)_&+!FYNs)H= zc+92Ls7TLoE*{XYv3F~VW>e;3*ssDH%Z$^v!zPfD?0Jgh%{R+^&+?{Tir>Y!@5^0ll=0LwW8 zn|GC_rnG;pih%iaJfI&wmm|nr9-B&;cL1}PcZ(Zxrs;qdiI%{h3Fu0e-J;8ctpe1q zBhs@?{N6<521$a01O}8knU%zSgy2}6piZ+%rY`cCx+%(x&_Bt3g@2wf#ol(8bgrGU zHd$I!86~*RF()U7&~fLt^wMfRTe*s}&0qay{<=D*JHoWHI)dMPQtn(#H2lPVuGWp< zzgRaIvBQJu12P#XlS7*7cZm^Zoaf^K|BChs!R>bmIl9!@`dWgWND~M;02>5B>sog% z8e+}_o9^@-%bfM}*MD%_Lj(>9`8eLA1tjRxSmh7%3i?O-BZcstmzf|DPC~?wbHv31 zxqSK@m5O{`3PC<1vUj|e@FzH~hX57EF^OkkPtF?heCO-N@LIjv{(NrC}mM7rs8 zgi-X$JNDO*h6Ji1aY4N*!V)}_^+_HEG!O~TU|u$xrcyc=j(_BaDEiOEki4UIw)7(- zrVDLW=I;4fm*#F7X}D0IKWj?F^^p0sZ+2Sy*?nAB$y#BEu>u)O$J`UDpFLkkyC@b5{?Iq~7Ly9e;2N5VMs@SPk>k?%-{Fv0tED zU7N+V4Z}LQgLSs>o2w@;X^AQ-5w)at!%4ftxBjZ5qLlXwP@06YsvR?YBs`48wmlvpD)|^@>$Q|5c{isQ5BZ{RHm}LRg4HxqRC{J>bE}$rq&018M?-QMW=c zc-562B*BW=*;Ry9!q}NV#Ak7F)Ts5ZVa7Q42(t%TXJx)}KhBrAffl)z*1cbstfoS2W&Oa*+ z>wj*Jw`dzwh>e>YUi<^dRbSwF@0~X$+7OAlF1DEIIpEMAfm!`u9F_4;_Kvl=jT-nl%^#2AGP5!=wtIdh{6!6o_dd2 z^NI+EU@=@XWuYmjno&(t97wt$-@M}c?R*=+U1~BKXN9@~Yva25)kbcTJD@nK2-pqJhY08h>?$lV?CMCYE2z&v|y8sDa+`f}sn>0JE)UbRV~p z0?pW+i_b6N6@*;U(x3zlhb zC>LL4Y-CtSH-ZTHMhGcl-kA(iBuB^|WF4g8*oNHp;53YXSj_`sf z_Ow>>AM4n{vA0rt>ek6awK=5^=1pEW{-dTTy?RUcyw>d1o2plnY){IwZ<7!!Z`0!6 zVr=CoEa#MBYq4JlI8W^>M-Xom<@6@|8q3QayNSB^RDkeRz?&hag!# zmZ*@6Bsm^VgyK5<{OD1btGt9Ss#|Z5KOBMf9C7u07CA{iJr;VdDE_Rq>^Cn)6fwY_ zLv+!7EIZyc6?ZocM1L{O%^9|oVQCdes`uOo5dG~QeI?FEr4GXy0qbTBkN(-*`l_z( zO(7+%*0AaLH>MJaaK*SG}4IwJYJo=4tb-P{<$X((}1J%4;}N&eNNg|JpoGQurZ zI>Nc93YBsmCG0R!wN5X9^GEgc_PQRL@C~R*2CW6-9l`li27*^zJ6m2E-=O4HeYmMI zM3Bznm5I{nyJ`#RFGA8|tCJ|j9dB4nN0+;G!ko7iP_bT(PzM}EJ$SzS$-z%P}ORzHraiaNPE z=d#xk%`0oirCxj{t38E0u49^v!6JgWVGp5|Vef zP>=*x++FU>E!8s=o{L6iUc^BxslS+(&rX~e)PJM-Ae-vF?Rbh6ClRCtmZnpSOr|)d zYAjn+95X<`zLP3Ws6XmSQPD)kgSO`wov!AIuv^GLW@2PzCGIhS22dK@oK_l*A<`@9 z^qi92&xiJxtaA2_;h5TRaC}q~r~q42rwiqPc+hlYSMfSm7XmD~B{vNCY9l<2n`=~L8bLoJFdEHJ46e+J?9T{Hc56Fxl8E_8 zWrIF~={Vo56k{T5HvU7kL#PW`m|gWv|Ktc7^bIvQ=hyO!92~+o_{uDWM`ly?*-3wR zGT0mJ)A?Qw2plQh9dsPY3y6-SD4|iVc7LF2Ag$^U?cVWMCqwmR|71AmA0F%z6^PRL zasWrw_RWJ0S!pI!Z>S$~in5sE%1T{pDP`quoY9$OHs0A@vE7JiqWNQmhUAgL?8*KutlfRS0ut-qTE(;$VK}iUQV-_LGu0z2XKDyGYJaeW zcd&h1x309UNIZNm;z@N?HAf*?(ZnUz^!hTL@`}^UvxlSdr>2#shCvV_*HscQ(lZp; zHdB5rFkud~EQ|bArJx44spn4Uc(b~fO}s8r_|T>OH3gT)jF3nD4! z1sK(3xSk-Tag09fl=G6xD<5<`};Iq2!SfF!&O7fx$TGcYPnOg;_LlMHtIC4Wj<6PaUa z*vdp$masKhF0dgG+AzzEv~khXUmqo^)3*ZGMJ+N8+I^sn&d2M0K=*M3m2oem{_~eW znns&G&~TM)_;%sP=0@R{6YmGbwTbi7E^mqQlXNeTPqKpHf0EIFBw{z1PQD+57mj@K z^Zd}haO8^*Fo&=k)Ym3Ha({jhB;zd^ANgO8kQDXw;3|N)=txLwj*YB9Vk09xb%uml zQIVd03w$sll4IQ^YEiFO6mX(Gy}2-8cE=E+!JtVWv?*Xx;IXv?HXI$fM2Oh}p=E+* zb0-mzbE3{98nV`cp_nt-oe^Oyu#xCSPP$q@yqHGD1p{PETrhw|#eW3@TwGi*0N!I< za9l6A)9~R}@xlwm3opK1F5Sg+mb$h5tG3ZORhjjG{*=Q%P3)&CO)xMLYd+;6L(N9m z8|5MT}DX1-9I))|(r)ICnEmJW$^1qkr>wFsy|c|1+bEG14Wn zUuv5d1GM7a9#wKYA>eM+%&o`}5v>)Op()Y+rx+nqvrvrle47wm!avsfkbC?YMqIekkEa}m0;3abm8!kz6zv5&zn zd5qO1C8lHf!@lzl6T27;B1?uqrn5%U0G^iZ{*ZSh*V9BauO-3>P;8X+vl(I1q6#kd zM#C{%FLYX#J>eBpI18wRb466mnI5w1s^BQFlAn+!8Go_q2r0LFC9i4X&{ZU}WZ+#6 zTh(7rNh*3g0=cjv5c%m~aGHZ8ax%Yc6rB{rpn^P5q3_!78OkWPf1jfPnu72jl)Dec zN_9jN?LGUgiq(xk(|P0_AipE{Nl8*q^AD}L+xEd7djYN=brnf_0+xw3o61Ojn+53R z*y7|-e}BWgf+#H(bZhy={)INg?Y0h(%c`C;Dn#YsAy|i1%?jEENEKyhV~a&=nMY=R12(KZ*pD@v>ZafP;1 z3JK`zbV5)PeDxpBet4HNPhMNBdtE*B%RRN-rl8uJCrU1YyP;nnUvl%rt@I?VR8;tgpU`= z`DIZ|x~9~(-+6xiaOMm?Lx1rU#T=HA*11UobA==RbLYfXT1z*e#QFSH2 zU`f*1?Ia08ncq}s=+O8vc+=$JD)UsNn$ml5D0R+h`8b_^VhYH$dRPvb;mby6aALo6 z=R7Z`4%4Zcy0760o#*LAmeUJObroOaUAj8LF0xKxjxO=wBX$BTa}Q1 z>zv%=!;F6MAFT&Em&d8wYsoO~VN?uBv>d%oqKB&K1_E3q)g|VJxJrT$L&i(PEDW{! zg*Zn9x|-@y)!3Z}x7^mxsR7<^fo6x@rCW*v$_;NS6f0pz|#}ONs`E`jNUVI!TV>#q=J2~ zVt(5hQEu$e8=peX$1edVCPcNVgChd;S7Hn~Ouh_A1ESFj6^ z-hDw%J|%}!w?@H~Rx2-bqS|j12MUGgg8&2Jja2_yh9B$Rl(-$)h6@{h^rj8rs)Uc( zYKYhk=D_6@k>#Tb28;GKkpmY^ND1BmUvFmA6Up+WE7{?yOTNz^XZh2CFb{J%XfD6G zbdfSkSUP#Ag?~f2F_e6@_cJ5tAmbj57BwPa3cj1+;zul00yAmd(rA)YlGI+>=6@jF zWUu$|kB=jh)44e7N}NX;L2x)bT#jv1)%gQJzBO*zP5Ti!qpkvOb0 z?drog?drog?drogvGu9xP8p*+lxWXOIETl=aG#(kO@Fl!aY7KL042?hHrn>SGsHwR z-!HV+Kl%Jv?Ol$_GA((k#tS3PL9ju7c6l>S6Cj1`p6JKjfoU)A(@~j*(%I`6%&MMs zr%cUO^;Y+2f))w5S9EhwsmAF!6h5{e@iv~_gK;)3_<|m~W}7lgd{U`QrxoI;R2`cT~nl9m(tTFb8zC_`b-~%Jx>NkBibMvE-Z%K8Mr&n(W%}yC(INf*ndB<;2sLx`Q>PWa0tf0d&h^zCkVSn zsGpx492~htoD4qy{NTjE!K&fnBAp~u;$CrCOp?zhuu7LQ`sxA`DsJG#af_E2f+y+N zCO1jP6|ryN46;?VcVftW^8&i3P4HJts7M~4^p8HbXucam^LPS237crH3QQ8R_*S|Q zeSa^8D7?h(@cA6RD|+7@93CEjBNX3nq)3g-6u(Q+VPey~r4+3K@IUdFP)gDkxbv^WhH9U*$d1Yv1t=V$aj5+ zHSB{heqr-cLMmEHRpcQ~MUGA}rtHoONa*nDTsI3zVVBV8e{ZS5lrEKLNsFMai+?hk zRlkJTouou={xLZQPzw>>QCDmZe@El_BMEN zPm+Sp{%Qa8V0Vy1|IMadO6;iyN{ajsYjOo}V>$B77=tJE;m!FrK&ROS^rXirypA!& zK>*UXwp?_}enGpOp+5qXJw^eQ3eV1kD{Hy4CR|C@BNr5)^t@@!xx+qG(tn!oISx0^ z>GkN>ylYF*XG9z~fy^qB{y2xo`4Y+D>&}IAD`Su8YeFUgQrK~eIzAFpWl+$r8E$EE zyXKWmj8oy1zs5Rbvdwj%A*9{UQ_jXa0|)sy{f%d)YgmX&fE37gPh{^CjhnrLzNNvkC>VPRaK2?ytBzoBAoM7K=}}m48=F+>C7p^A{$OR=)hJ!N;`AUw^+1N)5p_C+OoS^Fs)Mg0JwJ_c zclEilYO2)1Cv{1G#XZr~8<=Kyrm|nAW6>$g(s8b>74YvIJAa5EEiECT#P&V|?svf| zPe!M8T|bl@!mX3gYK48B!qVb0eNvQ8C&hxF;KLVRr)TeUHC`Pfp^X(kaT8}>jN%J3 z!IkABtC14+MkMHlNXY$wAY3P{=VsP6q6X&WY}YHfXBWk5Q)l(Mp{Yr;n|^~CY}MQH zi*Atl{1UOxV1IG20M{Iq&2+;G>v#G!8KfNj!ii_qs-j|6ODlgay@3-(017PfHYx2A zM%Whwi!HBca#%vC7$mYP6$om8Tc3G?WGRN5e>JT?v*}y1E6v{6>SEyo^8|wgsWayb z21@scfEbEP7x-hRpt{?B`_I(TjGq2}aPz5jM6m+*bGJh{^Ez528XaL2WuVe!x+ zude<$4z#R4TD<#cedzc1UibUy_oVmtUiX)w_o3SJF3r7HpLdwE& zQZ4Pe+SypfNyUFfw{lhKvIM~tB`Kkj!tSW>tTWy@#Z4d%aCc!YgaW?uiY}3>?})>9 zhcY^}y*=y?zdAKr+DE*DzTLx*!{h$`0qr`|=T}En!ow;b!Y6Rs7xyjAHaa`f%=3D( znb{v}S*mGa@Ao9efm7S0%+qvig2*Qn0DwUT>N#HWP!@mu@G|^cPI1HQ1~g`ZA@LiZG_P+_T zlWFg$)Lx^v=puxC$zF@)u8ZDa(R(XOxAQE$Y+O)5KRQZB6Y;hlj%ZU8RJhUKiU`#@ zR9K~SOyGZ#f@K{RBcxtAdEFAkkQoQ2L6C0l^)@Ee=ysmRO*$UrZJ0^Ncfre7IntI^ z;6N_fb8|Rzbp}zHt;n%;_c;Hw7){18ZLXAQbv!eciTi;v>h*?P{$Y^#6Ya3%kwKNm zmoIgu; zho-nLf07$Hsnj2&#KZIo&b&;C&t;lK^(y! z|6N2(Yb2-kf(mfrFWW_6;W>Gez!*Tp=1uxJ4Ctu%Ro}pRUOfxDCxM{oYfxQChhl#? zzHEb|6*Jy22Y6QH9`V6Y9N#%EQaZHFt6*M7XH^W=7^!erTRKjp359dWcN6|H1HMSm zuCQ*bVB-U@S3uK*91D&DY}KUZM_}12O)g+~Jq2i%=7Vp`&7rBX+>#;rA@B&q^U>K) zd?)Oyjo&|D?9=g zy7m*~wLyRlZ1s}yd}ey;{cXHAheG-=pC@+GLXB)@@Y zr3N|T)#>zTP;j}IW)G-@(~9!cx6cR=4G(>Fe*cVK0`efZghR#+2l82FMQndR0#cSU z3{Lb3sbo+-8BM0g=L9myPhdp4ai$eL%M3lyAs8H<17TP1Z7WOPUAK}rYg_r4Sco%S z96hCTPqT45f5!qo4d7IyJ;~Au2*IGqYyU+?BasjKW${be0$f)th+)`Srtw{DBLW%Q zFT{=SKmuz9FvyY=H^}?h#DRa!Fogl7OV$vWH&sQ6Ulsar+y$~_{M=_OCk_>t^A{P9 z&KakTfSM-NO)nDnginE?=S6m@#wn0G`>BT|fm#VQeHg?B;*%ibNpbCimFZ|Bk)z69 zy{N}3Qa*&zG9}rS)l=k@B@uL$#D4<_I`?T(lHYhy|2r<7oQ&VqhD(3MRY4);raJ`v zY$PiTGS^SvOCdWsg9`{f7PY3%4wAPnU_9n7H)Wd_%hEh~5 zQhzuZad)#rcs1N9EktFV1f_>cVd*aE9tht`A%!Am6^MrqxGR?%)dE=5i= z=1`Q@H1oI!*+b;oV@vq$rH=&KiGOne@o)Y|#s6u4?~7%mkN8c(M`Pe6&Cq>D@KgV@ zy8qe9@zGMcPXtl3VqjAMUuIpUiVE@*TO&KYII z-rB18DfNX2pXr%<*J8Cml4-AL&BgWmv*%qCwW;3K6*%2{e6au3;lYUPFN{k-<}~(VoBTM*^@2>+1KV{Qije9mFy9LlT*A8kA6AeC ze~cuEkHzEO!&+}N8NArBwF1w(QMVlyfAVZFKCu9uEgxZZM~l`5x=*D9Oz;uJyhe!;(U(|Ri*;+1nQr%j@@K;4ql6Q(<{P1e{I~I z%kHSn-ODN_GXtq=F3I?P8?({m?U#6|E%(4sCM(z!KnnCrEgg%21@z$+%xF(G>{-QIa2Jo1Z!Q`XUd?Sh62xBVg|!0?=X4wV;et(WNvR+e z;(L3k+AHw%-iepISJ%H6z;-?X`#Glrl1zwG9%8XP^|3-@wDZqekMt($za zeUpFx|9W}LCdc9bRg3@2TfA~|Nb5QK7J;h>+wWFXMB?@-;&&;6gcWX3EVl2Um!hlP zd05svu$o1^K+ag+JFELD#FXxywIVD%T5|SB;W^oZ5r9i*5M9PyVB}GeSGTsR(}M1Q zy)aKh(dEIMiFf+CAxLSC&PfT1Y(5MmZ1;aLu83R*MgE-Ig^0@OUM^H)Gx4ekJ-0^t zeGRQlFJWf`s_P+1zYs(_N014>z72vqAliC#gTd1Edh~+yW40Lx#oK2%J%qD6c;cmT zr$IoP_8J5tazjAQia7Vz3fLar>W?pnW7Ym4r(I&W)q4O@_uP^`a3V$Lc_H~U$T)uy z%yNTRGW%4F1T+eRc$D8sj9q^R6_ z`V(hvl$BiLK#wyven)izVk@q?oHu`=p;5i99;&z0)=tk$-LNR$x)gt3oT7hh^bo2- z!ej>=VTt##=O*4(>iYDl(b}w(<1zHKM3$?a^?NQ3u z=F~r{>0R~gVKN$zZ58aQfc<~&xl-k6BHs4WZojZHhAY`Iv^K}$iX7xtG0T|oG-mQ*;x#&E?Y3OqP6ASw+H&d=O6dj}FlS9w~kPS2Z z%Obzv+eswDjHT7;2C9EL!ROefz(ID7fRLABjiw9%6ikJ}A`D6VjNU~WGgn_8oSvdH z;FCXKHTY+DBP2e@lTz04%u)8)$f629ew8v<-F6BACy>r)LtVA-#;Iz8-Lx>TdKG*6IZXnan9SwnyDp{k0Jf^8(6g`TGL zdK@-xnbz`2L}UwZ)9E~2P~R0d{8GMb^8&KXah@sTgI%sva^6E(a{mon=vks8yRSckgc)c#3>e_pB4Ec;1qw36x%N0=ID2=O>e z`^?rS!r^_$(qgb(zpV!zf zq~=2tb)wZJF@z)6`YIa-NH8~T)OJ>JfyH_%ZF0h`wrb7jmLXFbIMz(K>UF$JE`WFDGPgZJb_N zcQq1$ro$CqWSa$XR3!MAC781sufW)Os`o(l9JZEEd92~oa|g=IEuiKL zo~*2PD6B=)7v5mt{89VLt!3qFVRtiPbyI%=m{};?ptzlhKwjXVMVehgr<*sn)IMJK zOyAgyM!K^AmBow3mSrg!-h#OfZEM}kSuON-BJX>ge6>|yGk7x+S>OECWAmqSdeVka zb1${JtyahTWLxpq_)aRi#zCV&EF?G;M9_PA!A1dY54uajts6&3E~ge-u26hpoFIQC zw+OY1idtx8=$loc;i5q5xL{$ECuW7Jwp6LmjPA~WdH4vn z0mXBUTAbnm>?H`kzW}LIhLpz(L1%yCGSp);oxlYIjIV5ED4qx4go9>WY>rR+1>_8h z?3~p06fYu>8{B?9ORjg3x$}%RcgF7!3oe4|-5F#LpC7Kg+`K+p?Vm~t+mYLb6QXi|{S zulFDmxds0Gi`_3GSHY4Av}%8uOJUh&vW;jL5*?=HWPST9BO##l{2ZpkA}xRJjVEdG zTYq%L({m3R9g52_t)IK)m)F*eRCtGP8_l~KC_LlM4b}KjS26d2#)}s|hgt8&da6`S zQ?d6qwT;w*ua{?R+geQvIfH-a)1K)hKLfdlpBJQL`Ltp>UnF(&>Z91M?7;W3q6AA? zxyprB;oJ7{N$ zH-Qe7!WoI1u+yLP5!iVjP}TG5(RD|i<^T(U$(4d!%{Tx;;AZ&|IIw>_+m+qR1brhQ zW?V3g3ooD^da;#s^}8_|QrX28Sy&_0WPN44>$)h`XnGYkhjG^J%;R*31>0^rlCNH! zje%MVZg+U#gGSy3n-v=D0BMftQy;I9sGNIUL_l4IC70ZtsK*JqOVGI12!Vg2V*u)g6DExFG47@cKsrNhdQ7@-|MV;yXC-`VCw}AHn4C>ZV-bB} zwutI9#i!M2;TY<2i=5J*qRU6MH^OH-(B!1ZM-cD4!T+#521`@VEz(aN19^3olD_I^#1{yQ8!X^q zXF48Sm=wb&)jL=_(b_0(JhU^8$Sjrjy>6;8;jz>AV2FM;8Nowo8fB*HE9`wcvAtf{ z%I@#!U^;(E+lEZL@^AyD9|1<++cXG%Yr)6E_L(ij>k{DpOyrcRe+v{vPZR5hhrQjs zm_0jX&@()tS4uCkbSMPNw6}t_Bo|YN=I9gptSN0h$gs@RHvAJ2!LK<9V4aW##AMCk z1h&CiopBbRiNZO;6tsft89h5MYLRNb=b3+5`80o@FQSa5U+xjQqJ2%?$+hayjB1bFGI;xgai^S*)D4-;g4_tnlOnw0?nCAA1_L9 zaTiZiHsW0Z-9mB#qW7bbuUB4YR~8u}Q+>neBTD&P4*xqmj;>3T6I+*{?PB8cxmjN% zTULJ;`W7~KlIJoxA#p2A9>HTl9#{@jWkdR%;Q!2o8}cx0rq|4NSdt_CRV-HpfqqTv z0BFv3b_SXmXfZ-#0o+wW2lolm#6CSup)EwRO6@dnW_)U9L}=kkj85~@oMHFcSK#^i zjBffPTrSCru{c#(jK3w(W_Rk{kNgr<{L_B~EbkU-g*@pNxC5MNVbg@2md&5~JP)79 za(s=Qu6)SZ1+xt$mPwT_=N47cru{Tnn1yK0UpWs`^j1fjvTd{3D~%^R#~+2P$kb3< z!)~gkTI1Y=NtVNBLdt}bPsqLf-tk;Q#zJvImI8NSi4kmWWuei8+#6>T>oGi#v!8$D z6Sb99Iv-5jVlfc_A`#{#YM(u_N?5nOE*D`%!$lVIh70AxJw=Se2$B8M1MrN|brL-X zLxOYz2MgYu-{9iqGnKFvgtR%1A5utms1fpci*;1-P(2F{4hTR}on0h+Mj?u$ff916 zX?cO|{v~IAV-9wT~TCz0@XAYMOaoi+{4KFWTo{x3eU}BnLG968_BRYRRu2z5S zj;GSw-8RNzz|;w{aIGRwby%BU`z#yf6Ex~suXYaC(hzSOxE=kZ5xaaBR3c=qMnZQ9 za-02KMlbWS(gZplF|`r#RqWM_H=sB#jMJh3pg!Gi`OaL?i4nmvK3(T{a&3S_7wpH@ ztsoC-7DDn0DP9Uto}-?8I|G0G_u}EtZ-O$e-ar-kCJtNf580Oy7dwL65jHuJODQj7 zD-LjzO91f)H&x8FhE_II?Z#3H(JaZvHP9m-Z}0Dbv!fbKLJGum2|ZAeiw}YzCAhNm zT>}eUD z_Wp*Kgp)v$C9S5*#x?_SRw#*sTE9fKz>fX8(BSn;urNHwjB7##)$uLLQ!XPxo@9sO zf_Nr}Q*MUDt#KP=hH?tCgKbb!vfrsWw*nG;T_uyp)#Q07ma){omH{dr%SxRtb$TTm zJ&I|7u{f!AcmDYbJGOtdjheU!H^ZE&v6d8BM5IE@pRI^nD_j6^0pc{kYNRd(O_Fhc z!Ej1LO-YM}$jI)XJ?XY>2jO8UPI9k|AG`fQXlPB~VJGbbc*)3_3A6FgNg|dhHJo`r z#Y3}Vt+vwxao)*&=(IFii#FAIN42ilWX$?wMOOF749hrq+<|}PNbvlejslk#jWe8P9xkJAOlJ1pZ;~u-ll6xtPg;kZ zCT(7Bh=|X*lz;Hi;Mk*DSa)k~)g`~~z+=v7M|#Q?N$PB$o;En`*u4vP^MuHP*YeLl zH&<_lBK11s%1o7UWmy{I$~7vC@k5GLb4Wo_Qapdr1?LpXDENga`0c%ob-*T9tAVag zR|-^K@??i-%zFT*@E=!ijoTRs9gyDYsjbHUq7z|X$Xr%%L91G}8?4Hv3;+uYboHSD zK1^PhDEqGZTtn20+Cabu_i#Cia$L{T8nR95)oFmJ!GHuB-Jj(}8oM{>Dj4FE{@_>g z0Q4Zo3#q&D$0h1dT33EigI(c%` z=Ou5&3d=EMP<)np?O9w2`f~$133h*2M>9UoCbsxvWtwDXBu8I-^bx$^g<5mWoDA%^ zIg>0Jj)bJ$%iWfbi*2xKh*6+)R+&#&8@jn|m@S_cnEt)(-s9aJt~Qsco1Z}}vOu)x zv|+r8ej(DaDyM@1R2xq5VRtJk+*F;Yt%H@9A6* zL<2gH(dxW4nJDeLsBx7h!OVb6!0Lh%u-d)cXhsC*F5p2MO$0DlQc0 zqUnocuycw_216ZDLn@>RHXY)74`I^EFB%#Nrv(;rjah?;&GCa!o$P-Ea9(h8Sancu zWv^0wd^(?j8lMU1!1nrmiasr+(3cnN_ep!K>e648p%YRk*i?cr>N#vKe=UQ%a6}hb z-fQ>@22~Db=F1kbM2j7abS$INkmAyLG^eb%Jw)5ouuM|!&VpRI);vmIVK@@^Syw;D z{*yC7<{8xbc=_jfPpE%hV1IvH@t90d%p%tX$BQHSlNY&>v_v|rJXoJ=O9@1de>Xr3 zgmPeIA+EAcY*ywU$kce+XnmaLzv58Vo6pK*Nw?$rd8=0Nww5MC^#CT35oDXxg-!S6 z_ANL!jGAh9^oTbxj}HRR*9V<9?MPYQPo(5T4e%@RH^eI{qsM>2{#ljT8HNwb3o*72 z5c@1IwSPoAm2ZJLr`<`;<84-Tf0r=}04KlGRMKrQ7mAj;(53#ES?Ui}+YxhtEJto| zpdj~!ZM!T%35VC*EUJex(9w|wf;!2IaVq1wLU}rry#%u7JmD@g%A8Z9|9m78E!ZFb z3~Za8##qyY&K-Z}PgzOgw-!bqDtr(PBZJ-(qZ=PaiT`PEM-UNi$%BwL^+;b{!ofc$ zu&)0aIPPS@7S0c@eKT1>b>_L#RCVruE^%PYABy=4*YN6gOt1KWWLf4{*K2@v1rsdp z=JL(3u3|!Tc|)v*hjf>|aZoDC^CojyY=4-pmFUO~dse5ZpBFi56N%cv5aamdE-EBM zdtcak;A(%ufzw1jnB_ui zta-ut2yRQRu(!Xo&hUCT)C6<+*CGB4dYXtRBKpu|88g^y|1ZeK4gtb50GI<`8Rnec zn3Q2>r#n;U6?ON9NC{CNj?h25;p2Q*Ls*yGidKJ=^+uGkmPuN10t-n@Xe``r+B?o> zu5YNfcpDkNX*0R8o3@>}qkpc;*}+Yl0eD?r@wR)-ZrK2Z1^vs_18U|7%Z9|V7zc>e z^4jNl+lPm}FXK}q`O(Q?XD;>A!v(?#60p}bMfx(dk#j;qvOjW+`~<^Wa7;+;E=V~I zoeY0POcWwshF0gD>aeGZZWC<X-}HtBS$gcizN5%5s5eL1A5a?F zUBn%#D}HQnGlUT3m%4Bo8fJv}UfToV_{x950%-<5jot}^bix0{{nKxPCI?aZh>^l+ z;G46dr2*MRD3lGCi5C#Pim$Ox%}~Fv07r5c@IM%l`SY_NEAkCq z@hsHOgB&puiC1H@yPCFkpB#Ki;ktjm2fs63E(tcQ_wm7F5^wmIPoI@KFt>&D^jKtDME^Km={SiXxh6RQ{CJB z2@t=ufZ0Yl!e^c=>iB#Fk4GfLBB$-qLkv3=l|;-jAfFl5?TtbE8BenQ1VxS-r%#D# ziBA&+#%Qo`r!b&Ggg8p|0NFb_`tVp-W~#(yKL) z^uhgxdN)!DNIHK=#DXu%{<4%aaY;l3q{O#Ai0`Sm>^YofpY+qetnLi9oco;nL@ItA65>a1EGFYRv1?<5gq5Z)$jvmM0gyrgYP>98a} zQ9B+oFjIeF_2YcfAH}Q7!>4vo?X$EPq+_Bdx9-cGca$Fy#p+u%aDJa#G$O4qZj;n@ zp3S2vcbo?0+}j+PS84_{NrB{P$pPSWcr+-o6XTCU7j&TTvARC!TA2@n3no6jBf>Vp z2d6aZMmRt`7{GCqCVDxH5?Z|+uQXv6(|~Y8Yps7}-TT$^f*@g!V<1}RhNH=gcrH#+ zT|iGkq>I~c1t*C(t)Cqpz^>!yNTj}Xj@B}Crw{)(SrcLl=oSX*lt5)RX{VD?lQX$a zqT&h#bwE^@=t|B|wukZV4~el#l^L$VMeh^}yAm8j83(IQ88oTf_GI~TGa7V}-y{c} z0~bRuPm&=hW$oTX0jFe==Ayn zIu_+3HOq5pt0S&RnN3nO)L6D%cHEdMm`k9XR8bd5oR}kGh6N*@D3hLZI9^JVp&X~_so#o2 z6z^YV`t{>RyAsXdPoVyeIbH(g>zyZ6(j9&Zw^L@19In*1W+XlB))7)rL}==&WEFp- z26NCblUm9~rFaInqyuv*u(Cn7C+?~w3OCF*)--K|{G9wUW zo)l;xJLcamihKxXZ@@SFql?SUpcW58Lz&$Q=SpLBP244-m~)JFWbe+T-!($y8c3v~ zkY*fR{bsN;_}d&KEsIXNQ~;BB76gA4Y6;IaLrMw@1q%_LkN$V)$#gSDR3@)}g$j#a zvw&KV`Wh5^HSEM(Qhjti!t8ESigPY>W8!AP6^XEGwjx!NcwU`3{l8{X&f0EsI?wu( zpA#^A$%}S){^(SEEj(a!@?U`ily%_eSILPv2y#s9L6SE0D$W=R{D0@!iSQ)|??}R+QgCYtSia3L z-_;qPq`WJ4Qo+%l44T};$V+veVyVM9k+VFxGqQko{Skg3Kh-Rrd~!`MC&uw9PfW|f`^-OhHOJ=gp{Qa*}72QWSm^Eshj}TV9rc5Hm(J2 z2Ya3H;fK(C)s3#(wyS!~FHbYqL=es1@P7N^?EF_ZaOZ5Y=fYc9Gij`ph$v$>`m<>9 zX}`LLxmzbDH5Yvz{mi{EsNXO*AqeM&g0Ds@vE18XGQ;yLk;}8EljVQ30my%i{1Yz_ zufl&k!_E!W=1=AHq>b4xp}l{r(``UI?n>tl9tGdo9`LArhLa9NP>|`pe1bU?*Uk&rhm1d#2Jg^a|-6woNN-$FZLulUls-Zl7Rm4FaPVT^W}kwz@wGfJT}ON zO@QISxqu2#tB3WTta^WO9KzQ9gta=4kqiu=I6~vAy!cg_;ifB-v@*9#(HL8-AZapU zl3d4vRQ%VtHYvM00qyF|Z!#al4|!i5*!Tn%EZlkKT%9P+NysQLXj^sYN|^2;TCP=z z{our}wdKPxibTWOe+GCw6UC#ZzJ638AJL*RdRSJzOyNjeCLVtucdY-W7v(btL<(;? zbsG#tRc~EMx0OL8G{KU)+lYBqSY94)*dsvCTqVmINXe;InPFDoP7d2m(SQx1hL2za zWid3I5e71A8(-&>hI9`01E34VfOmn9&?uFyAzidiE>MTe&w17MNy%BIiLn3T6*8c? zeQT*En*#FIM~#0`OO(Mpu2*hA`n*~QGJw!~+_g%%cuGK=0gYkwA#tckx`en5&-x}{ zO-!g!b{l>0J z7i4C9PZ?LI3^KvKMcSZ=j8ccZRKGNpp#AtgkzAcf(6)d67HI^haoGX{Uj*FK#u8`) zUrXF3ao|KQn>xmd-gw;8#}ep+-oM-?fzVVgn?gu2#pgHo)Nxtr06Wm)W)xGLr<+ww z?;Ca%Cd98O0PQ>V_55d)l}%ZQPJziZgZdis?OlT$w&@N-@Q$ zT3j)ut8#z!=-}gnC%ahHc9g)kjPUcfbY!A?Q_{~l%-ji@mhp8n-$9q>=^z_s0~%TO z<=~p=JlS>JB}l8G(*$}4+;SWlaOuFE0iDiEUes@tWVgE38|#mX)Oubmg+dbklS#M> z)BMbkv-2SQ7)l{An^XnKd_h~n5TUKg$1rB_K0<#1MbP!w#J^jW_JxupksoWjjW&C_ zEib*@C(8~j zC9meKIF>ZTd67>Gq0U5ogWrREjRNGbcmTQgTXvdaQ87k61s>Kuf7*C$$MR!DoC4fC zqHBNU(-hrC;1rHebuE0OKS)&@a(_MR1Q^i0gBt@UXYL$HdX)6!J6Uq>!r_of7(2?D{@2sa3{Kp3vhbxdn~hgHpC5kF=jF zAZMa|p1HFeq+ecWtA)yy0CF8WVxx&c5m|q-D<~FA2*kK`$)Rpxd2&HO>~jY**-k_v8fCcM52N)L{t*$rK4BIaj2J`8uvY7 zlp+4{oT6g7NM4K9YbC9ZrTb*e&8&Yt7PyH!j@VZHgt@-&xI_&TkPR&elhH5O(Z+vh z)F)dlQ>Hga@h+ zW^6+3ANR+<@()13Z+--F=v|G(d(OaoR)Dbl-CmzeilcZ1xWLCSM-7ZD*Xrz&USy3a zLI9_h%b=InAc++ydK?xshTm+aX5xQsR#AQumHtG!Up98sL^A#?QXn%L&{Eqo^~vK1 z_4BQV>M3bxTAZu5sbttVGrqvitIksD2Y(_JK6969p;3RmmK<5M15)k!l+uk(S~oJV zjT}eDSA1=0<*c&0fmM_4aJ?jkxIum_IaWm*Q}{_zRE9!IS}DAOhC^W#2L^u=5kg@d z2@F`TPncS4xe#?mnY6CBqy{cQ>sQkHg>;75;m07VMohm)Kkj}Bd%hoccDHxPdWc!z zH?6$I7E|UEwoPOaV3uAOr^SRd`Hp;%FaS5?+x#u=6tJ}?2M0&H$qW{vIy-6qw9VCB zEtb=4LqbkTdgDyHwRLR>ZSsFoTS4JyIb@fhEGJ8On^e5Qlol@;0bQk=FhFZ-#|JwH zo2p0h6Q>WDO_30U?mpmEuJl3{Yfr}*>MMh7DD6d9;k7tcfpF}PwDipMrdFK1q4y~w z1m5NSq@^zhOD2QWf-SFKFo^clS^uI!a{?U&6bXc>_8Y%g=ua4207`#B*%I_;+%NGoQ(XhnxbzF7CWfWNfSwoT z1fhObkjI_CR(* z|NY2-V#DuOU>@cC@QHt$TUw0%#e!Hj;z&BP31u;5R_-zTiJ7TruGsya@9D+XBvaHf zWCJe%W$1VU>cL@d0IgmKUSi^8)eqeBx^KYnHt6p3z5 zf>N!z&4^ZITXXjv0xMX|Bzv*F$I209G~0eO**XZoVE5p|Yr20I;6vAyYA3*D$QyD$ z8^h1wO~Ft1dxL-2JJ#k#YS`y8!+$UVrU``QdOQX?l%cc*%A>t$dc2vNZL{qyq}fh3 z;V8`gcl5AUer>X8k2gH-Hh##mEX%Si%boWIuj$?(;3A#Xe%`J32QZL#*c?2I9ZEX} zksUj=)dDdoR+Tz6N>c^~frzFXA8G>Ts)-Nq4_8wOP(HT)rFy%F6`I zr~)a!!c^T+=)B@H0qsZDHb?HZ_L<+>%MDF>+o(GUo=Jc4!xCN+K2V9t{Q=(cFml+I z8{Kc2>im2$V=-j7I7gnOXA`Pv;Rc?>LP{tiT$aS;5OT_V(t?tc>&O3(`ZH{W`kLvt z=DpHJz1Qi!uYJEVMBI%iQ(kz$o!cWZ$=}=gV~}ig>j1y4_`&$NM0{!0D>HEKD0o?p z4t7dPs||lqF2inSWS1-*yJXrNO}1keEVt9n7(hxh)9jU+nU-qT%*fJO_m(MU3TpKU z+%41^5;LUS=z9{F!NlgH0A2b6i5KIi^2*E8H9bagt<^cy5H=G$u{pHK@D(=~Eyan2 zSjPqz;@d5&cUd9rp6i)!#AqG(19oFH#(&_Nw2pt&#Pa-ns{Y=FIDDgGT3550x6>K_ zqKj9A{E(Az%sF*;q;hZtbX-?HFE3z0L4mxLX)J(70&ePNKj)dXX$H$BEdIGtPL2w` zpX^G>^J0b*Tn|)ILQ>D)?y!mVK*Z8AD78{xYC(yNpAea=cd-g4l$WiW)U|f0%MZyu zniPM<96vtHF$0oUo|3igbl2JHTBL}z2#;D0>*jJmc5d7LyDTBgI$v+BY!!J-8x_LT zOZLU#(iL-yO13cjS#3&fnKf6vyYER&;Nx;DAYmExj@fX$Il?mJ&#_62w(0wGgeBu6 zF7@kZuku?0E7d~Ni#soDSol^u?f*3bA7p>~y!lI&0zBOirob}|`EEJMkZeB1=(g@- zd3v6ixLhCzn|Fbb)Er{P!29{Cz7f2hzOZW^9PWMg@Il(4QC?xM;7!xrlaG&0K6ktq zBmYoUDHLeKT)4kq=DZW8Qd|vR%TTDXZlQTejY6|&?lV6Ub%TJ;WYZ1wJJsefeb7s;_lN-@4Nf^G{uNRQ~@B7JO|{@@jnQ!{fa z%jMOgo&zk&zNjYfh99DI0G6v_Mq12vmh$~Qo`T2CY&=2-!4tM-)ocak<-U1r|D3R< z+1hv=SJT(dpf;cnpMuz|EM|`2P%3}&DyKafx{dR4b8F|$fu0<5<;T@zOw6w@m%$f% zFAA9yGkrA;)75`2W3bD-L}Sm+4S!`qHd|bCw>L3&xo4*!Q35ZK++2ik;K=LBy zzsCMoIDle0W2BKQxRZ+8j^f$m>Rb#u!5#C;G?kQP5UBoAu!psshbP=C88Clj4e2Ov ztInYs&PhB$VIIe5^?EKtlm}^wQKTI3qX__q7q&FW2|zxy!&UnL?Nmo;i?wL7b1DK% zy3;)3*?)L+s4g^4IzC)z)4RLh1RkyWUVfAo_~u}1w{&|IN8FWlkaGgp7WUIQ+xs9pQryy4b2`kY&!FKLEVkW@3*yk%b2Tp;t_ zi_;&fkhhi&kdPr>qn|iyY(M!k?x$wN{iM9E{q)|z(;wQ;_R@^R9s0RD=GqwKta*Zb zXc*$qG=a-bKrdU=(Geo6xLo{j-_}#*mVb*j>v^DXmO97Sy4*`YfhB*gBtX1Se!1m{ zUO)*`KPM$6nT{MEXFQxr74d?>|4~pn8Vo(7F_jlmh9*a~g=AL&cm99aq0I3@Y2iPsD)7Sz+PrOFLhLTDp0P56ncdrTEA5Ew#v0D* zEQcFki!;ppsG5;_*|$(Pk}vlk?jy5t5j%x(6Y$;E52`zaJg((|SoV{6(zWF7j!}zB4bbRUY@N=7)dupxSR|P&ShMCx)@{oq36hOZjFJz>3D712pQ}~-!2<51lp_F2qG3#&# zx2b!#?7A7W84Z>$QYXb#^>Y!gQ=z`aixC!qwTS-qA7k^_Tx~2^3A@ep=rgV=NdaT2 zyJ^T-GeY`sUR-}QoMl>h%5Qbxo!s=O`{4i?A*NfA=B1JrXSU>p0`XbkoPfO?cMtJI z?XUy<=Sn35w;LlGiGPZUQC{3x7ib2Ni?|4f1k~BpHp4#2<&`yRn+?bD^kRhrMM_h_ zjXcRIs(pF|Ee?8(@WJQD-=wX8Qa68Npu$eP_>M>+3o0hm8YZiG z&jX>~NlTw*hn7BujJ7@QQ8C8v#kAehx8L-fjeTv$j7qz+AHg^xcfFzi#e`{C+t-GH zg1jFlI{~+Ca^Bfnsx=Jp9a~LzUepGDFtCMAc*QO=_XRF#3XIHv=hT(4OLk>RE*qQR zIyz7;F}Z(jEH{iTvx_mNtL$RdB34b27%L59%n*Mk!Bj`JbEU^q{JRP z&FQDrzul4)_4{9IONx5>ueBy={2JeYEX&)_lva451}IWy4#f(RFnrXM^29i$GQwnD zEE-5eP<%e6D~`P#$1UX-^I~Sv0B~7)NMQ0(Rlk3+e$aSi@VaFn4)&u7p@TBuC-~tw zI2cDd7Ds^QP5?O>%Yh+rT3Q_4-6Ys7wBBaH?6hwqJV)K2+MtW}|LjeS?&eiA_ExRE z7`H0zoP9X;baq6ti_yikvUIVKs8vPHb)Btctm0s1=k{!!wT)%o2%~A^Hs&!P%raAk zqLzO!jSV~^bsG%ZS=sfkJ2pXe8mil4fte8oN`gj_TYxymLgMIY=lr_ZuC}#t`PeOZo?x7UhZwaZ?qMC*P8;t$&#$9n znu^Bm3+u^9(%zuxoTCE*X1qQF_|0 z!&&X#-WH~-Uvdg<6z>uMZmFi(DMZ^*MKKyyM`*Lp9C2(k%SCsNq0%`{N~eHhxQ~Bd z9{+G&-6q6g16TP(6BDeWSt)Lz-1JqelL{!@&PUp2%6V@c-@POC-E%ivWvx*Sqi9Tf3=Wn@<`(xrM5`UP} ziUx6$_x;hsPi=O`=*6eM$b87WX6=8`5VkMtS&KtAM?yP$i!n{#em(fB%%JIhOvnGt zV3D^L-6FidI2?@*kB{V8%qzSWRy~<=$X{LHRkGtR7TO$sYJ;7JrQijI7=8k;@cE0u z2fuOreJKO6!UYIPPS&RQu_zBYBk~x1)Nb&p&4?^Xj|dExtC3Hh+>vKP(Wo z){rax0UZHQrfr?K{0MsdIFWl=UKNLUu?{Z71W@+?*^)37f{Y&S?H!Cp@Ouk?ezbe|`2p_e z?&B|h)f0ZbLEXo$YUfpb@y9*$#t>uNnD;p8MV@G*_uXx%RNMZdnCE|}>2^8S!?f0@ ziEYA%!_2DOX&88101I1DgTc(Iab^zS#-6YoDNN;0g{_H`7o<(Av^p#1PYQgumwRpX z4fuKLp|>O(|$c6*=X zH^r<@A|m`*RzUI_-kx!u9AS!o*4MkXg?biEsVuiY0mxN8qa*K8ab8Ww_ydX8`>34N zb4~AM=8!bgi&;T&?5=XYo5a_}6f`c~7(%|7Vfy*ws;Ublii8rH0v$jw)0w7&l+MCuXxvcGTDTJhd11NLSk>pxy zSu1kIq?%shQqxAy^BhYd;p$>axsw}VimQOanSp(y4f&*Wl-H@b|1fwLi`p(`KC+m1 zUR=)`-Z;o$f0<)crrKZb_q2+H>hB%xo*vUh<-NTJ_&12O87xRM8(McZ1IDW{zHa92 zGyLUeQDYDh`iB4ZX`&Z@(C|g^M0#uI0LvUfXh*<|;3DPz!_N;+GFX3YZXZ3av&6p|+P-2>lYfS|!2Uzob29d5<;5gQA% z1SurVWY`KL1=ihLbm&12j91NTgkvWy^#fGy5-^*9`b?5#MoUkB%{=jjvDFC4F^HdO zjltCC_%v)$yYcfu1vCplde_8n_1=Fy$3eBA?F9HMec=CKl(w zh=@H83~gu@({V9>FV2Auo_2>UZpp@OAkqfrG9-bHGEURr_tf3TW7ALao9n8CL1W`w9{M0TEW>QRDo{kxRrnG+5EGN} zo=V%qX#C!Oem!6CnbnT!|zH&CD3{$K}<33+7;2Lj@c@TnulpB#$!l z=%lbusQgqE*QSQt$ADl%MOlPpB(W;&KtlxYPHg0YNRliSB5&|LY8Im9teJVm^s)qw zow0mDY@wsG88A={4{N}^!XDA6cUl6ed%)AI8eH=vwiEXzl1VCd5Tx5;4ZY_M+)nC< zfLmAWV7;z?GH1UWUoC!moP*eTT!03)0Gk1k;llzH1>m8BNqJRHtx%QWMd;eQyNoPd zx+6!5#F$!ZhdNB?bwrXU`@a^KKbN@bf0ozR<&Y4s5u(;X0Hf+MpH3E+I5_y2#U00m z=D#m(R0up9X=*2G?lq$8;*+WxH#!{5G&mcWkm=2T?EYMpV|S>03CeIWpFw8kvu1jO z9G;gG;Da0l0~3=bKU=$to1(@Qb&Bc|qIs{D-0TJq%UUVXQc+LzvkKwo`ik_S>DCdCJES?VghxE8Sf@6ngs0adcS&j3VC) zQf>u*PP^UvVAsoO9lN6w(+%kt#mugK`&|xX1DPc*+(i)^_$=FB`Ch0L=d*BdC zuTAMH_%P$5;IkIMSyfFEGvlz{lk$CckMFmfc?KP_yzcJp9e?rQ^TQEHww}dILsKbA z^UB32MxeF%M7wdbT!FL2Nx$w7R#OUO3+&*3swRB@d=;6Fx%7HOIzIaN7|dSF>7$6m z?b=qm(MDT?dz5z|%r%thPM8N!dTqj>J#aN)6p^T1Y_&6CY-@Mn7o1rabfKanS?s-z zfmw$vXct^Lodk`zRcy2CM8c*zPoWWf2iC5+nx;v01*OE+O<@*;R}@(;GnTR8RU|ck zDhpg2y(suA4Cf=3W%ViH1X!i(nod&E0yxXers=86ns@+h_VkWtKtjrX!SpphTq_u= z*m?#ARFtiILld;{I)mHP)yr6KlI>>9xkxW?HHu$1LA_mcczP*fmt1lsEa1FElc%*< z<7H|Y9b3hG0T$}rkM(U!w7C(-?ZWSW2V`?g4z6c@4~8~1xN34|wMJLtbTPeQZKn^_ z(zCoJ>=QPDWr+~83}ZNz6u;^Hov^xgM=Nf$m)VIxzN!#|qXL8#_G82LM02#qJl0ym zD`47Lf-&?bEzVLIwc5^F-gFi^d0WS~0ygNkc&hE9J;kyJo~^@i`t4>c43Fb~<|fg= z`vZ3X(|&L^xVIMVQr3yX^}e~MVYs+*&@JqjTVNyXwM-J08XcPE@*GBtf$BKN@ZFy* zU0%OSL&z)ok-Q$P7Y}RegNcpMZ-S%w_^=zDcJaVd8Vo!A?K=Wek`B05RygEOA^*Ha z>FkNgP}yx$QxHB$)Kmr_?8l#f8&Ik5r|Mw9a*s~Bp7jc`#sY!mf+1!+@oyo`_GP^| z>qF{Gy*E?+`W8U|#8p0Tw|wkIg~A3ZQ-1Ga5OQ z*9)Zj2oo9{ifQ_l3~kRl@ns{SW?Db9wi6XVR)Q320H<75Lh6{}A+Icd@dXye99k%A zIB-n+55QP?C~}Ck{G3i=?(3t>bu%JX2Fk5b32VwdJLlLSLSS};p`#=(n-97q7x?*_ z-DM^>nR60DpK@qGr|D!IBEJ>PLkH{L!x0)@q?GvD=_ZQ7f$$+pMi3=`) zsFGvWN6u{*tjenciCJ9qKfEEysvt7@($~y*O|msNG}G*?1BB{x)0*W%Zl?$``S`}Q zF*j9gR2s+g>2Tk*yByZIj2uoer?%)duPR^`h*Y@W(O9x%h3Lb7EEr^g&zALRsp&9U z-pe6OnFlZhvAinKb{GZng=6YUI}3eGBCizTQt?dqE>-V`@9xv>A7)ksB7{8xRrFkm zOI{A_N-A9^F>NiiD&4Zofiq{7G0Tbbv%28LdiU^n+K+glGvau?#tfWhx>m#*L`3!@3+iF&Nu) ziu41a^7cIpw*C3TgV$|C`&_zb2C3eh6yro^sHr-qGY~vXyQ#r>U*18U+oxOL22II_ z33Qg|p$2PniVWMp3qR5PD_(fv1;O^@V1FsHsVRgP1L;kFIA$owhHh9OwM=IWaj*ux zK`G$Lcw)We8j+prYLVBtiqTmfPeWcf9d5SX;@-D@>X&}lzv+9uvyy+)@tuq_53TAD z{kD}bb*K?nc7K_a5h!q?^M2~)(Pax3Z|b`_uZSng^YR)ys+y6oh^NbOEm^!-&?_*j z#qfy_f0Gh_MbC7SYQ_Os`O!`Gd}(TpT8C1gcm$3o7^E!Q%tBb^i|3);NY5)X1twI6Z zQ)q$AWm&Ubgvx60`m1;Q%cL3>;^SwTc zSs^}uqgRW`yo8zpu6FJ+1fa!(8p}y_*ixvegVy-Wy+=USeRM4K;IPXE>akRV6ksuo zcnrnyq;%hAk-Y69^(_dnCcAP110e3iBWQb7H3Q9`^?81R$cLINVbnx}3KSrI$zE2u2Jm`JA>C#~ZbC_hC4|z!g@LIArOcuGn zy2u-D5%Y8b4%q~&jxfuO+&M^Z!ND46> zUWk<%QDJuHbI?QBEPs`QGwKXfk?N{{e_zxj1{ZVOYTEksWByxNV3^$5x3R6l8JhdF zbj3@emgaIQGFxJu@nx*a+L_^Z9Z_Q4ddVIKUfNBil58Ce!J!BvE~3FGKNU<6U& zU5<-sc~Pp#U!kZs;}_oLoeMoYz`Hx(H)%@$SfS&hqHBq>;H4M!PRj&#jQIb5euO&J zr_B3iCuvblUFy;TihQ?7jNTa@->dUNpeeioM*Zk9DfGfEL(>C#8orc@Y~i;s2G@+) z!RH22PlO}tyAQiUsV^>%7}*99e3e|oO$D?;wD=%#PyO9uHzAg*+bdI z^yjRcUR0p&T1HXppV`p^&KeAVxN^X$uld>|&X-dK$2!@UxUFe3PV@DNO+Hs=@zvcFw z>iW_N&4oNU|S* zwbKP@A;w}yaP`~XA2p`IgKmmazFr-@&k4=t(KozUwW{N*36*wxQ*->`rw4ndmXdlg zvqtTHF=5fR`}ya*CbFC3_BPk~{6m1C?E4Pm~RS64Z?woh(2 zAgw(w_9n<=0Sa_!xqz>KM~^eT_#AaIan)HfGf5Eez|&zF8(2h2$faXVX`Ys&MAw_A zJ;!=3ZM~&gdex=Lr3%08+;%dJ~GLVhXwAUH`xKWgb z;=VP=NE{w=V>J^{+PvFbS29Yx`0B(biN)?GNhf^&Y&S;07HeF8Q;eZMf@2B~8sI17 zm%t+u`cKMv9@fD_A~Hx9ll)R*c7h@a_yY{+hIi?7$W{WLWY%jcM|Y8oo=g!q`i?%I zW3qJt^(w2t5JiVBDXoK;`u!Dj>(si~n!fFj4LlBLH60pc`XGvUkdX_EqY_YG)Bc<3 z)AocLr!n>~x248^swP9>U4N$vp%>U8W9Ns2aGli6b5lt05ywkt-D26{-mgD&XYQs z7QRn~OJKATI`nJd7N4v8V6AX8Yw?^ElL6QC#m; z*EejV<2kEFF}Yieco0BZV(BLbFo0uY2CsdH6=&B`1OA%SnY60-CTa~F#Jv~{Scp-i zyH;%@pRLA!EOR05EJ3CNDCQxB(q5(mFxMkRL9Qw(fHjz>9@pzF4V}?ckiT3p^HDY9 zqYiNeV~3IpNyg?IpIyFcTl`#2m|M)1PyKlLkkI$m{fEJ^*w<6oMtln#-2qCD%nU@m z%hd3wtucY!X^dGEKgplI8)62btUj9GlB9;e6<1w<^HF_@-LJWD0s&Y6_N!?i|G5n3 z<3>tXX%|J57_J{_(m8EIF_&*=rs=M3vr%@oRkI+Il>Ag*P4l`mz$JWv=oUjfZ6W&h zU=V%1Tn@8C>0IbFCzFyDK%B>a>eE(XXAK8*jVMgj>_#Y}w1VHZMItgY@1==tjElfm zc{%5Qa}C{P0{e?!=h!&^xM*@G{k_`G%8lcyo`8I~LtpIWM>(;P7$vg;uJzXiAE3?v z=8QMoA=NXQC$GB0ht#3e_jdg0q#a4kPp4mkQ){}jsJbcU zdNemw-)TJJ3s3G8K7Z*I0_C1F*rdTaFrD0p>tfg(b$)2-iZz&v1scJq5bigml-i^- zg`ZeAo^8KYu|Y4%2R(ajAi)Ve%L=_#Ug$B&{zz`12K@Qq&kF^oi!*IXI+8#SW^p$iDZPD&wGYMiw(5t9RjhG>iltH^EhH?ka2_X#r0K2oO!JQGtl-H{ zuA_m~fgcZ<;u?liAKEC!nsT-l83r~0ewB5@5b@W z?EyD$-MbfPx)E{HQb48pNr5CmuA5qS0}c!u51BedC2Mtjd7~!u0))hs% z>_m(GQ0tRdQop`o2-gkd(ENw4KCxEd*ltm4XtRVNhj|cimJN~ zd>~vvz3Tcl#H+4t6o}>c$|wtQ__+-w&9chG1WTdQ7%`9hBjVW#6h5v7z8muBd*W${D{CLjTXOKVpWlI#7sv_myxzlz6|Dtt# zi)5pcjCRGl5-rpI9ED+h_0OXMwchs}-JB=l8*jKjgGx+*m2?V-**$<4cjgtZuL(eI z_DX^5LnGuJSKk0s&**t@x;fg^2+@&AJOEuMDwihWM*rc5*k8L;+UQRvRoGG)qO5)aPxL#LD1*z%m}2|| ztvNIuL@1bl50cIDxW(;mQpVldB@ke8zn^Xz`8U~P%(z?22!VR+@gPx>Lp5IIbOBfR zhQ!daq23mMVZCOPaz{>NsBkDR+VbCbYFg3AxU%8JQVw&!s3F3!e-hg7y?pN;h9T(- z5cPO|9m1brz!B4r1TAL`BpBLsf$GtuxDeM3q0Fp*K!^Se0}FZq+XNjAg+@*EhKdFA z-RA4IB;FKP%V0YZ?|I^5)u8lU7w=)@3eG9`rE$dDn~ey#<3dl>w$nCD@?dEZ%2Jl3 z&3^5i$JcV$RoNqm6*tVG zt9O1K&Lf@Pl4vqopNF&gsb}467Cl@hS19!*wm?gH zthQD$$8>*T%65HF9;uRH1+9KZ?pQM z#h!T2%iya;+Mz?_cuuS?_$i2~zU1n(tt&SphsQI!$Z5I3-7VB-K`1xyuk*TjsR^Je zXdh9&Najsh8q!jJ;1wO7@uVOqb(;5^0sWrFpIe{MF%@-4N^Td&{_38Ksbd`dC z9)WpF#H_y&7s5qx1mS-iT|SRu0&-QEEW725pAFd&u4Bpko7tq_=Jc$bly$i|l!VU~ z^Bfc4I00|S*sW|ebD`Qw0niAf)Ypm(JY7+n#)w?^+-<5?TG?b6Y<6z@YM%M66()zB z&|8<2M^5h}Ej(EXKpy-YT>gL2rK|kwxM@p8HfcAb~i7j#`oG65@a4^9mrJE#~YWUfV-|{t|;@ z=mnqRm$JqU!wa3{pTtsrk^VIF=P?UnjoD#O#>b4FANxa|2$gxJktv}*cY}mnmL(?D zBim!D)OTASM)3S;v%D-%9^zxSAh9t@E?8OI^zE`fK&t7PF|eSgPWCLKT#K&SMvV>= zDO+)(t7k@$AIF6l0GT8Xl2}}S-jl&-4NjIY4#oR((vb}DZdXe4BZ=0t?nV6L>7T=J zO?yHc_`WT>VbzA)@CO^Am$MtW61otE?vI$+mKRlCbF)@*lJbzk8pa7*QJON4?!H_m zGdn~G6m-l)PJQbDfNJ9L{;?d)oh|UuOS6@|mA%0}6&^sGE?z=BUs8B~*vKN9=0}GW z|C1LJb>J?nLT1qMJc=PZk<8_-6V@_mq1nT&LbTQN$ZKq653uD!2JB72X4)Ap*-J~J zAeO(=UMJX8u$SgLb(*njv!ii;=T^}3Bi7tlo7VY0feHl0sl?^ej|o@~V!?ou8t;76 zFMd_z9E(@QOtXWW6n~C?cyTc?UvSGT50{7pTdwa{@Wnuor}N&=py{G1>Gj3U_t-$0 zpr>Vg86U{>xKo~L)i?Cbx{daQ6Q6Q2WrgfRC9=b52{ct6!fjml;+fA*VVU&-(ZmWM zQQ9p*VM+{+)7J+n35l*UKm?i4iw`I=;^^^Wz7JPDKdNT5LM35;h{{QMV4#7*wsF;= zm$X4XX%Daem?l0c=YI^~2jv7~Bj@Ap;7QuA0DCKl%Ry41XABeUa%<2POKZOoq+JSu z;8dWGp*i41?t&nVxWPvpKg*;GCxM46z(#e1^9HJgEK*cu$U+x*{Lfk)pzH}DuKO!xj2dp==U;Lno z{O~k%=(AnXd-m#pDCRwz&HC-5%UP_t^Qcqs1k{!Wcx*d{r1FAJX?cHbZ|9X54Z?4b z{Q1N7s3lc7QZ;lN>QxM|aba{{EC1IZe!hzaVNB}`CJsikbTxOBZs1;xBf=9~?B?o< z#OCPQxgqg?1odZj3Bb?~{*3qo<~Rzl4pW@2>i`<|;)Qe)FJABrh`S~Yso67A-;~r# zA$;1F%xZrEB^s`w32d#_ncei$t-7`+bNo|&GhEuibkBw|MwHFvc_cH2{AdJAkAJ36o~lL zNI8Qhiu(4`JsXdxG|Bz|{#Q++%Pc4_B7PJ0fxJ8LsDCTPH%Z`=c-y$iy(w4N_vWw>D;F29-1lF7nSXn1S2zJ$k z=sYQZq?xz@ofYwueLlyWUe4F%7^w3sIEKK)9HW_OhS36D7zT^Q45OLPfMEd2AHgsR zwv}Pb-8cqX=*Tg?7YI@)IxAo|kZ>CyzmxWNZ@2dBE%03|puausQZUduonPzLw#r

BKwIDQQ^ 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 delta 667 zcmV;M0%ZN$-3P150kFmw5`0j*NVnrR(+C0p0K^3V02Tn34DA6Jf2CAiZ`v>veWp_X z!2v~(w=rq{Fv{sc#oM0)jE8E%2wEte)31Dbif%+1~_POVrd#`<& z_#1JNN(?vcb{iJ=_p2F9e=o<2`2^<6$=$E%3|8y$&o#L78z69f6$CA$)C_K!V2T

`}rtW#$;Y@Sp=| zg-E1jsbc2AikVUghaMKx#9NGT)b0{QhPvF~e&^GG3^VtOe;yuC2Xzr~i6o(m?>uLr z3#bqLzJ<4V3PKtP@%qmZ>JiMA2@7ol?av&d*}|TPlm3RA^oAfD7F?ifcPc)m;UL#`KGYg^&H z>kDkxp=a4{fAZ~&DMZ6`Vyy(3B5yI<(_NTxl5|xG+1(0FZ=1eZ?j z0UEbPqyeT#4}4I&NVnrR(+C0p0K^3Vm;3AiH3oWhnNaSxmp1JIJOlU{0GD;`0ZRj! z8UUBW?Ex|aw?zP#`0W8Q0}4z4mo4rALIZkD0GD#^0YwABUI3T6?g1MHQDFc8008(Z BIrRVl diff --git a/Moose Test Missions/Moose_Test_SPAWN/MOOSE_Test_SPAWN.miz b/Moose Test Missions/Moose_Test_SPAWN/MOOSE_Test_SPAWN.miz index cf86dece2888efeee19a1c58c078aebe40e023a7..efb125ade2b655ad94a89933d879792ea4cf2dd8 100644 GIT binary patch delta 102389 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�ZlK#x(~0LaD0@xRXT2JOiDo z0h5X#B9o=YDgzy|0h0%|? delta 692 zcmV;l0!#g*qzUwo1F$Nf2CAiZ`v>veWp_X!2v~(w=rq{Fv{sc#oM0)jE8E%2wEte)31Dbif%+1~_POVrd#`<&_#1JN zN(?vcb{iJ=_p2F9e=o<2`2^<6$=$E%3|8y$&o#L78z69f6$CA$)C_K!V2T`}rtW#$;Y@Sp=|g-E1j zsbc2AikVUghaMKx#9NGT)b0{QhPvF~e&^GG3^VtOe;yuC2Xzr~i6o(m?>uLr3#bqL zzJ<4V3PKtP@%qmZ>JiMA2@7ol?av&d*}|TPlm3RA^oAfD7F?ifcPc)m;UL#`KGYg^&H>kDkx zp=a4{fAZ~&DMZ6`Vyy(3B5yI<(_NTxl5|xG+1(0F@I0ab diff --git a/Moose Test Missions/Moose_Test_SPAWN_Repeat/MOOSE_Test_SPAWN_Repeat.miz b/Moose Test Missions/Moose_Test_SPAWN_Repeat/MOOSE_Test_SPAWN_Repeat.miz index 8899cab3200d9bb6025a9ddcdac781202d45c2dc..91112db60c338e0a577d6fdfb4630d804c70386d 100644 GIT binary patch delta 102388 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 z%0A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 delta 688 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�Zmnc{K+8LaD0@ zxRbVdJOc!#0h93?Gm|KKDg)Z80h3jFG6PfI0h5Y)G6SaM0h7OaJp)nY0h9lFJp)?i z0h2#_MFXMt0h5kx7n7cQDgzb$0h7ynLIVu`0h17XLj#=t0h3mILjyGb0h6438wTzH H0ssI2=!mRS delta 735 zcmV<50wDeEjR?K@0kEPO4}4I&NVnrR(+C0p0K^3Vld%~af2CAiZ`v>veWp_X!2v~(w=rq{Fv{sc#oM0)jE8E%2wEte)31Dbif%+1~_POVrd#`<&_#1JN zN(?vcb{iJ=_p2F9e=o<2`2^<6$=$E%3|8y$&o#L78z69f6$CA$)C_K!V2T`}rtW#$;Y@Sp=|g-E1j zsbc2AikVUghaMKx#9NGT)b0{QhPvF~e&^GG3^VtOe;yuC2Xzr~i6o(m?>uLr3#bqL zzJ<4V3PKtP@%qmZ>JiMA2@7ol?av&d*}|TPlm3RA^oAfD7F?ifcPc)m;UL#`KGYg^&H>kDkx zp=a4{fAZ~&DMZ6`Vyy(3B5yI<(_NTxl5|xG+1(0FZ=1eg8;0vfj> zrU80g4}4I&NVnrR(+C0p0K^3Vm#YH;H3oWhnNaSxm*xWkJOb1kmmsDAG?!Qe0xAQl zAOM$z1OhSxBUAvFx19n;fm+u4uJp&A} z&IJN00_Ad-G5rAS-lO0d{&t)1cH!r%YC5G*V$w6GvuTZ*?yKenY1GJ)Ve`ui+B%c23x4)S!eWK`r! zq+R)Y!C>zuc@9J+wq0;6tzAD0l?+(B6-g~WuVLrp^^;oV;^$N~Whl_4@+_bh%Jm<2 zW%aAdf883MCBqo?ySTV0evYyv9p@rT#|40Mgo-~6((bq)mw?f@I8QV2wx67x7oBui zWN9DxL0UFn-c*oafOQCn{O!e+tn#kA`QlD9+oX5P+ z%-ze>h^I=?S;R>PXjmDaB|TA0MzI2ke}fwmfdw>*A)-8!L5ook5`#TL zMw{O;r+zBuszkzc3RBOde8c$C75iBw?RA5QI6W})WY~?b0&x-b$1%K!QIur4E=@Nj zQFN|jEL+Mbc5!*~Z?4;7wmEX8TRB?J&wRH#wKC=v-) zebfi3hwz#MUwH2WsXz=krV8Y= zoz6B?u2p=JQbTqusw$0S;&WJ6m-_0ST5)Sh^!Wkt(3Mp}6e>fhT#+mkS{DCx7mDc}Yi`)-6TkS~r@78u2yTY?5vvjSZA_+h_@>#!}L0 zn)c&p*l4wE%|kK^LJz)PDgO-IMnE#x+ZTI82u)TeDw?v$t=r@Qjw+81fB$arSlq@u zWt&<{PqDz&B_1Yy9g4&yb*EzJ5{UC?G>V~DkYNY$k5h}nRGvg>2;ZqbKmlG9GEPpY z$#i1I;$j_dgFhP;s#0LHv<;+_;b;7v3;0LM3&ghgC&q}Viyim`J0Ge5IA~3mKwjgpTf7IId#~qYj*`dsl zIT&J&1_)71TAnW6!r$RPhyc3Wrn$`P8q3^mD*Y76y()?HUIE7;Ru}r}kfd5(zYeH0 zu5aZv?HUf}7#KolZ2*+Xte~N~)iAh3BK(vk+jBsFVdX(<%+T<8&(PID*dexpq>1`Yr^Uzuj zSC@bN+d67{yGQ9RA=BML%oBJ@tmSwfj=e~E!49xVh~r0{jg7D%@# z<+4Se~7E*3dpFoI_T|p-%LZCd$%Kv?t+e+q1Ag&c?q6%4? zK|R~KuI&Qre{!PSfiC`SOZ?N0EOXlH<*_xr6m|azd>IN+7K>P^lxZbs_M>4JC&=H$ z*jpe@E%6d*w{EMAGA$RusK}ykfF-4rba4$cDMu-4!*&3Kf;!Bcl^P4mCW8>1-ZTg( z5EzcqWLRkJK-Lh2HA&%>f=^udM8U6J__d`ONS5j2JII2VurxbP>eH*+AqnPg2s3OKW_Q6vo#Q_~3jg<%_!!s~ztULhJI;P01;ED_v; zPS*Jwe-x1oMbk}sg#CN5taRznXsdRZym&M&2%dnFkcF2qeN**>JJgiDg#&! zVs~I_NHwJ}xl&xlG4yjTQz|WBHQRs)^LaRR4gKb+nrd%u0!=Q@iquyd3&s$!UKd1U z7LD4Th1xbPi$Mg%1KJsTHd6#MT;xaX=F8yNeuA;0(S1uJ6DB5M zGEezr8Q)G{UpPz#qkepljv*7GLo#^c8B7IVfJV9_nm)}i8OIC}`XE|iH|`_@a1fgG z#Ukbj<|twJK$|YA7gq_{4yiB%2#_%pM6IL_#(nH1t}Oqf2?^BDP%5jF-LnU?tr=z( ze=ivSrC1JMwZsy=ky7fX(YvzG?@6Z;+)<91t%lW|44txqo12SL4Z%U5J$&{VJ4 z@*6T*&Ldh*=wpFbH^t8s#-O0XWHjzaJe6)+kzP-7Oe^X-ng!4bg)X*5(}p4p4eupB zu-Pf#gn?{w3V5x6*Eb}o#~7BYZ^d}Ae~R&9730MjjG4Am>6zH;m<$!H1Jq}usMn@bx`^wae}+O2;qp#O#!nM)&yy&Qxc{fV0b7Zk0I-edjXv^ zZ!(s422OzGIGLctGMP-8cHJ^H7WQi|jn7<}*G*K^=`5l;@L>!SZxd``;;3^DzjBP# zOGE5V!9yspZ3jaJ{lS3d*8w&Lf3NK2bv1pn`_sXpt#f=2kO7Bhe8S%Fayd#PUijf? z7rVbaf7pOO48~uZ3G81Poy!=0`-~>Zn=)EPC<;b6U6??sYAc&WaZ4(uiH+i`MJyNC z!g7l`>wD_MEn^y6blT>Ie{(6VmsuBnEf`zBbWuu)`O01w!te}fo@6)1TO z)1+NQ;VwlwDFzF?oXo!8~v!nn44paE`>vp)>7GzzR1IPKF zK2=!C&W>#-(7FcDoDa!X0+bTUsUruZzKaJ5Vc-MV5<DeNWi;7-BKh<@Q)S=ehRnt;XF(BFEs|W^0Rg7f#q#PX1LnoZ}8d@`^c_@Wo7B)~GWkPi| zo~%8_`+Fs(5iyXK!PpCktKt^(5u#yQi77nO;e4{C!`sM~4qe$yD3xK(i2|zHb=zT_ zyv}<#m7PU8

a9^<4{qWtU*6b_pFm`Ju+OD(EPvaV@69e-Blz#pS=PYq84i=30>V zaa;=+eH+&@jLYs-=P~zj7Q?OVFeM-G`&2N^@vkvVady{4gu}Cp7W8QExG-rnjRmPZ zgN4$jqk`tDAY0$0h$ha}7b(vvMiZE4!?~s?gQaKb@RVb37sP_> zVP$vi^?|dNc^wer0J#P%9JzgTUFY#N%dJ3)y0x-XB1vhlMJ9C=F>PsOc^O*hwwm2h zdFGVDEUgl?7E@BDF5Rtaoq2f`%`du^rU!5~Kcpgye}bw}5|q1IWYX}m zUKPJp-YM(9NS8EUn*G=At-WE&1iP{cGuMv62kTZ-2@QP= z<_=))e`B+TO#@Levj+Csgq7LM3FGvPop7ZSu3FACBR3flI4(G3^usS`qcnhz-ffB9 zjqN@0_QT$rqwW1Y4Dv#3bUJV<0Tc;#FA6+ZumsIK`nq*wAaUx2Dwt2$v4^q5U zX-=j9CNl$}&*Jbw99$fsO3h-GTl8QnEXA5me}u8DN89dcc?el8KH3j+L%pGiz_cqZ z1?I)Lha2X|4UJm5>4_>p_e3AnXi9fS?ZO`{jHMGMa!=&}302`-y=sGScW*pko24qJ zB>@;x$6egL2yMYRMCE>**>wwL+`_bXoaF48Xh9j0F6{r(t;R2PKSt6rduA1L`m6dr ze-bGKDl)}}_K=$mNQWegGf5x!djX5jn{W^i_L>r6R^nlgc)wdNPE%6WA=oZ@77x;k zxUTd`x1|mue2yo9DzDkO%?x8JdoE8UP;j=24o~*$I;z6j%vV9^7+8K1wD$^eoTUe| zagF|zS0B8N2L(zrXSGX+LLauw7!&{Ke=LBRuzUcLyvOjeJ$#d7oqo&_E}&k^I1fM% zaC+~6UUb;wsZgWOZS~3IU)W*tY~d<#y0n!R8eiRI8RSm zZ2_?cocG_a%W~L+UtEP4zBSH%)$lhut^Wwh$A^+?jq-2tb1tN=g&RFO=jjnT1-pQ4`R0XlCDz&%>dl z@yne7m8qgnkRHIJvuo}Q5=XE&$p;4YKq^0q^3TYX;}4EICZ|gkr7`$nj(K zCbf&HHOH}<#4aG@8~7uCTbn{yf3E7y7?cFWf5N}$P_HL@(LiM>bt(w>20In}xCZ!d z`1m121L+GIOY=~GHR!>kDwc7G~pa=+>%QAO3PkD@!I0_ov94*e<(x^$NHpf2FnA@ zvPR)JnjnXuB|L1OaqLhUXmdQn$bk8dAb=w-_2PiOiRzalJ%Xr;#Z%(z$M;BG1~rxga$8*m$DdmOr|Jeq=hFGm(r>}OUk>Ac+}B|B(D zXU5tT*ArDk2OFE)fBU-TOGHkiOwq`v(0ILZAV~~OOt0nSNATQfbt;=2KT~1r=i__H z#iaRB+_0Iq#d)>7+^TM*=&#R$<)~~bJZCU;F_&Mt2sL%fZ+u9lKf&e-E2ab%HSiOH zE3wLQy@ibXfM5h8$y}xd3s3qb4t|=LmUu#mT85drsSHGAf4p189y{_7da0}Te-(0G zeUCU#GUBwqN1R8AIP(UauNHA=#Cwz;&`c9%a5tGonKh%O#_<4^8KEn7AQ$U278O@v z(F@LRm$2mgj6&l#+61jI%=D5$vp~+sQl;ZF5Rv^vg`&<_C@lF3i0&-g3q4a;&dG~u zS_~Jekl8a7e-6C>iy6a66SZ+=5{E#!_9-R=YhG~3hLf}nDO})2lN!dOOo}@NYN`i{ zD4Pf3Ajf0qeo16NyyZta?`nxE8kUrwhx zk#wJ%sPf&8r`8e9VcdrhSGP|_ zwX?UQe^#xE_NyvYP{NH{$;#z!WiMO=htTZ|PT8t&aL>wf-ld6d-HO#TVcdY>5v4;| z6murbN1L#e%sVO+U+Gas0236deN|D#~#Laqu#I9Zt;52VVKxMLx z^I?v9qJK?0y-LmPwW+?ji67gw3vuWJ1(2b8&)E*L;qYZ9sr z&@PWklbt2}qd;C4M*?KJ+wYe^ZLziIiIwId!<4Qj^)IJeBrL~}Wo6_hGIN0DZqBoU~Y(na(%QZB(t70) z%?jNSyk2!g4m3x99mP33q|)x8?OUi@N^(0@Q!%uXZ>43_ucvPQP365n-`b`tCi&SZ zvP+uFOQls86Ij|HujC?^6?LpBd$(t*L5vZswCq~jQc)PGYNRki)in2k?$_Gwe-&k@ zZeb{?x)o6QSn6*O?H?+lJHyO%t)ck?KI`GG@&1QjRCKz$Z4a&4`wcKOu%?F4p_qU7 zff6|eF@o<9_;~uYk(nEHcWHzE7zT5w>vx$3x&(ahu?{r*1|0_363X~4zRBQb z?z;QEA!Ef8i!@>}&?#$83_-GtN3AX@4BG9jE4(j&;VrEZ5PCvaVfK@vpgYl0kQ6#l zX@b45%P@KJS+m7(8-ysMjSF6;JPBA46Vn078$(=vDqnK7EQ$h<$YH%Oszg zfwlq}YcRTaP+M5lX;2cC29NhOktrL$jU7z0CBX7eqgI&=RRp&%woODdtRbsV)ttpW zz`%b0s(@$N^s{-;x&U5efyPHe1GUTWk1{u(BjKy~KqQ{^>PpY;e}d@&LQ=pJR%UM7 zt2#7dGM|B?yLYdA9>DLskQa>X?S_8Mzdqoa$KFJJ?N1ws3Zp5V0W~q@G8&~gJfc8h znQDs~4tX8`G(qnEwczHvz`qVO~c;-*YE zg(gMZlxt4mITHJtf1@eaB4LJ>LFaQV^_K$~ZaQcA7|JtvQF2ZEEy)$9ewrX;D|@Fn6*>3=2HO7-54=Lp zMCLTh2^s4F+^}?UkvJeKq&2EPZUK{F1Z}&z#32HckKkrAe|6o1g5zzuF>E@HBv9Jb zAf*F8h@!@7=Ki;EY7dIAv7~c!a3zLbr7}EZirVZi`$quvhm*Fm<(CO=z00-GMXI_x z#>sq9@8o9HYq(RFlgY>#FmsKw=7!m3Megb$Bb4jtv@ZwjJQ#x)oDrsS$N`wahr|!? z#~P3FU^4lne~cR6aTq}lYic}YO#m6T$Is2PT7p4{H*oUDZFbH-*hi?bDH)`h{KbXn z(P9oC4C8ZZ0sNssB%@3>W$WN~bj60SXpLdZ$uJT{Nfh!Tp9~*o@Q0Wf!eDhE225(4 zP88SQSwz;6vZ)fg%v>*uFW+p`l~)_pTzb^3yZ6;lf5lqVvra8F@@_(aXQ^nCRh8@a zHrw#Z8jve3xd?We9ISiea;_p6PSZzR@RKLHil8~AH92a}3Bp5er|pm?SXvA*wS%V1 zqFII3BH?_9Mei!W-xKZy9|R*0xRa%*dD(V!vTyRJB!4TU`Bw&AldF zMt7NUVOe)}9-z|Hgvz5-9tg!3VU;Iscw)NQe}UWJPt7HtEoT}>Go{%ac`pRt2muK7 z_Aq-+<{_cro+bw<-$w3 zz;_yhx-naWWnJqM=mfW-%LUMAy5246-3xMR7PY=8S?O=U^5O3r`}Y1L7Rl^Rz!6&$ z1qBQG)vxk~S)8Mf1CoLKiiQ9(clm}MnK|bfE#7~CH0-Gmn&5J4&|HLu4}tuCf6{1# zH-kuAcnn>}$ck>BvPRuijzLA=;MLeq}Mft7|_1NZOa{@CEcB`lB3?y3uI@E7Fs2 znzQ>|vu_mxc6Me&fPe+X-`|75e|>_13*GtiRr!}ngs!v&VF_k+~&>xb+j zQXTLqi$&V^jE?I%V2E0P^(*Z4^gq+!lI~+d%(IxTWJQ>XQsH9+Mm7v!e~Me^;Jcas zbAfN%2Vw$6)U~)-bYZ0*{G_LY%kw@4IKC;DBL$1o=R4LM}uOw`kAZrqUoHm z9`48*v)1r_{9Ox*4ALx5;ruVyS-FYbs5Y|la-^wjzlv*dD;6A`!>rCEjfSk-`@iS1 zi^Z?_Gh^&$mJ04Q2m0`Hf1jeMZl-KaS^aom*p<&3J_KI5UBLJ0rHAYIQ4pIW$n5OgE5v-gWfm*cJn~wd#rdlTvTr{J^{vC$ z6PfH%)$tgy?fb;pgM~S(2meIFm=#U#xN>Y!xa}gcm38j*$`ugB2#?aFApgnbHCKSI1)%yfhV&i<>K9@(d@uQJ=(2_o(Ltj(Eo2@%k zc)YYf=LAwczVq2)2GYI$)^Tqxf{;Ia3Lf#Bh#Oen(Fsml@}_^z z32Gv)Ei4PGiYmO87~{AlmlR331iH7ltf_h0T263}D*;p%^VJZ|k+2fl6>2~|)_(G- zR8&JsAwW0zSU!GYJT0$&@Qp@W>304eZV1}%(b?$j;onjKe>TONC_76D6o{ROK6_0m zqJ^z_k>Llmdd`XHH{%Q%Dmsm}y{W#{z@BZ0xf_)@;e(7r2Fo0|R^Oa3nA@9yc`*iS zld_LG8&oKSc|#66Ne{mu@(0*%94=d1gG&XmHHBGS6(OXoA-!4Ip=579K81$w^Q>zb zjH0uqSXcb!5_I1jLv}tMVP1J&AQOPlkzX07^e%p^ z3AQ#+X7@Y}eR_hoHt<0OO>^rD6owWQ6Y>HZ4gA1< z#W`5*4P|hX*2JvfP>>PuXCDds~N)k(Cw&`&a7)R0ob z^>ihJR!7*%V(L5VJnZW#Y;%p>@7KLlSuj0UPCu0%;8r7}nwVgMf*&2Yp}I>&k{3Ur zih4@be^gybD_&S8sVk5TDHSG@riqalFP%(l3(#Y%q@zDUcMD<=5+Qt@B^`*q{ytZq zi@AdNHXP2Qtcwf`Wq?}CG1JQ8i`y=GftwR^LL54t0e>)`f&y$F8Mw~|rb++qNj2h-@i|h=t zMKS<#bpORXa$vb*J(I&fxf*;5wO7+XmDO(^rnFNk(|obNzNs=5emjbGdsUKpTAQU< z-HwrLwm6&JaLqF;J}e+Mcl;3&!)FeQ|3`;Gx>#j}kl1CQ-}2mM%yZu+=Pn&()!Nh# ze|VnMct?h(vfk$pP>erch?=7>eTxy#%t;V(_&Zb~dE=9*qbF8FHEqd!b59kSaRzF3 z_y(f0CZeeAbEnk~)Z&*4Apoiv8aTawc}FmYMpCR*{NHCJyhjNh~Z2C zSmGfrF2@D)vJl9et#4r?$Z`G&a3S0Y@a~)w;6iv@W*Q^t0e-Ce#-`WknnM~tk3pZx zFF_H~aADV#nAaRi=J$wOi>s*++?KhNWCZsTjPzAW%A&NZnj%i|SB-O9pEX`vf8n>K zVH~;fY;QLH?0Xe29XBBddq$w1sNbZV%6DMW2KD<)UfJj4wOcdSfT$XWFr0Y^y5|lX zr5f4vMR+)LaioKH01j|v@iazv$2EE&@=G2e_zHA&Mt}zpK;81D7OfR`pZAoOn31vV zy+u$~a_-TtnFp;z-iX5$HK(>%e~P)a#T*`9TXX1(ocs3bdgj*BnZT zh}=a4rzn0YCuU0;c{Zm)p54-V{Oeo~Mfdq}{CbF}|UPezvY=hxQA^ zCU*=QHtIu%`nsw`6DA|HK03ACjq&t#f#3hwGvcauOXEX}n!`rDme>h1K1hkz9zK{WXh-jPv%VnK&+&9I75J8{-=kII?KkrGQ zlHq7<-BNOfNbACs#EN(d=zi%!e*y_a;HK6_)LtwJpKNX%ZM@z%+zOB40c=^ryCFy^ z`LW6MDdcB!*ChPoPpd>HLv#~*>N=JUeR*|WFoe`Tb@i?9jWktee{=HDqG%2!w#PPcinz%JjLneV&YJlJI_xxF+o)K2JKTQG7;MgZ4>DdCXr= zf~ockxkou?`J|j&e``&u2}QS59jdmRWHugx%glbNPonv}0u3)UZOLt;2_SEL096BfcMhi=dMh-BDV%%Ovyp45tOLXWE z@&K;$P#45o39E)Em8W9Ld|p0mF5NR+-m?j&j^4zw&L zwVSnA4p0eWxBTz3^djlTdem1ED6<8@lxhi|S^BOU?KwGH{&a__8^anC=KO@X(zhXW zJm;sTe=U63F1axC{b@&IdSoxlVO28SB6a{shm~KcZ**Z43ahmZjX%QML=_&t*xysf zuLHidKD}>&s_CUeFUznl$1|?-QrV%OEES_MjuTb$B}AfJ!c-kPGpKc|e`wuk*J>%|U<+oX2@Yq`b85J;KNR@d zDAQ@WUp4g^b5x@#nusI5m^X{cF?WU&%F(J;iYX5!QBd%4jGCQ-;x8So9iw2@Jk8?P zhN)X8gF{#Sx{Jx$dI+@9@sqJv!-)!jo=-04>Xg7-lx{kPX7)7dd~TG#6ww)H#L&OT zf4LjSqc`bj(p18?oNqupS2+XG4#eUL`^%k54U|lw)r+wN8>Zu`zxlru#Ld{j7f2=Y zr%OK5r-^eK@9`&7#K-plr!(mU>_-Iio8`_Cj3riOzrY-teNSBu1=axBE*goPMBORi z2VDH8J^_}M`5Zszze=BtDP5grqdJ>kf6p*~zXPv_ah5=L>N;*}k*08N8S?PTM>6r# zNpQTEp)IFVg&xe9Z9dZg+#~0WQkM6#U5OfIqK6^~;r{@~NDMoh%s@jeub$ z5540T!3P8Mg>_juMB0qZZXlohBY#5`N%N^N6ghI#g>XKen<9?+s=w_g(?8&Ef9k5@ zyU66v`s@AL-2G1ud!5blC-Xf^@;aa%hHc`0SJ3;kF~50W)iI&J3g|RKRS*8XVJ&rri=+%*@!8>UTw01t{B*C*@$|hj`}(b z1NHVq2XHM+xG?fLC=(+Gt+Xi=fAvLgRVIhGlzwqhZ@Gb0AK1GB7t?2qzvdF7^&A7? z4K*f%K`H@Tijl8ET1WB{Nj3pU0^zfTwi9O$>x4@c4E@Q`oPWV+0AHy(~5GM<+}e~7YN+L?oC z8$|&vE-0@5Gu(0j4z8DkEbAKp0X{rcm1V)$7-dGBieaO**km$5I7Xl8#~J4`6fox- zo!Zp!5;G%41!t)^V>{%ea3jgWq#GXWZXFRHW7IDEv{HA_%2(yYP z#7d1DVY)yBcA}cx?Rj#y{~ujY>-TXyP_uhj2P~JS{kWxRKWu4cgp>9?!$~r{;M3zP(uFZp@pQF8e0gXHn>n~d#?QL+Wf0?8T&8HHhTt;i!zJ;`Cf324;f?cD=nK61`RH`8d z10D-lYad!hO-Sz<93E`E**ZCb-+vY`a1X#3(-AQ2nrW2oIE&=VkITz|h5xl?8~U&B zWa!&)A`Io@4V{-!g5!UEo-7#xjWHQ+erIbxg30FnUT?O&OT_OMe{YL_{nz&!hleK{ zdz&X)hi^9Ce{aoWK)=0{0b!*rLap7|Jl>`K?rhsYtnY^LZ4BepoeTqNjM`0X)9({L z4vt>WHV;5rn4=^=-l-%9@?yO3X;&7Df7Sx|Xa zZ&@C4yp4WxQSv#zI0m!q!$%;&Odoc$Y~ewhTy+KIf5|CBP4{CRWRt#@w)Vu=tGg#$_OMO{&Q$pTx3EM_+?UEpDXT=Z`rA6 zP8%Hq607}5&iPZ4GA}~PC50hdlf1@y!GF8mQf+8qZD?7wAwbcp8OUwv)fXW_-gy*3 zIJffnh;&X?-Fe-%n)H-QbZet(5o2imN<>_1px_PSxI z%k;w)W@@^kt>6KrPf}<`tZMozk6B)`W^`r9JyMy4PDMDo#l4N6Ng}W1Sqn8ZUY8-5 zC-igPbNU6LuE5bOPlu;To)m3yIldBJ{%U(}O!!?uJj;{m{y4sBfAn4iBmt%;xcqzv zf1ZDaq8X*_lSv}X$IC`_6HJR!A`6)I*&CbBN3F=)L97MDj5^#&a7&TR3OqI6h!$;a z`h}&RpY@*oa#zPVLqG*;y1io3FSPVQleldy_4hhpyk$P3xQjKf3(Acz?F18|7W19P zn9mzyzS|h{J$se5y~e@8a|T5Tm9|4De_Vf4d?{JT75lrqHY>*~7y7f;hybp*tCsP% z)e*);`H0 zT8^JZ0c8M)grna;=JI)=%()8^>Y9;Ws>`oa{go#&N#j1E++O!>QK@nM@~Pydf9~C) z0@L9CUm-E}x^S&MrJ4?*(+g}aJdWP}C5k>x=qN9WSc}N#H0lKRc8tsmbSi-P<(MY8 zC-k65Zk%!iN>IgH3F6(IyCIHW=h-+Z61@_XZ(;f|xh=5MI!*9$XY{wrX?#O3U8iB( zz0R^cIhb6=v`4Eg2FaKdcrr~cf74&-uH8rjkK$iTCwJ3ww^qk%Jzc&qWTKY+1^Dmc z1~FU}9!LvP0N+vaD+XZDQ+Enl&TiVc1EZ2Tyfii^4vw&wW%KSe4Ua>m+P;@hS(AxJE}7si$(ZsFO(~+Z09Uo)3EkSpD;n|`QKS} z^3poedNfy19>l7U)4dD9!7xg6y^Dn}e;l%xeO&GG#w5xi`IptGnN?)xf9us$Y$Dzg z8?HbV^_r`rQ}OwwV~ulqe+tsO*L2x})le6h3R+p47OqaUwM2>40Cj;1wV>-Qg3Ycl zw@Mi|sjgJoQy9BJ9H4sD0I#BARg24VRMf3HZ|8^F>P1P4VO9My{QNw9#Vu0D-dRZ< zvwAvyo97IIuLo-)>)cdCpVAYjYAjC`dfxt8^g zz3atiq&t;Vq-3P%*|ANBkv3My{T!k#|`O*^;Z|2+N?`G}Jej7r=xB7&0FY8W-`Xtlaw;U)RbgMz;WOMPB(?hl~e@f~f{Qn23S3I>m1ma^DO^CF%M zlcQJ@4B{!c32uhV>#7qI*cGZ4=V4XKW#*JuGE*?WLaA&xf4)^@YS1O4BmTIidQj%% zq*W7gP?zsKmRypunz2t>A-EpxwX-+-e8Afr=H(qg*oL@XXT7O*+~Le>EVW}u<+i%d z7$V^E-N>;($K%+YFiS5RA5*PSr6Tyr>)O`v+#0iyHStL|h`^@%q&-x;|&j3C?2oJ{D!){+b_o+6x6)Xul` zMq(8bTQIitI>RW5<6C0EKNUz9r~G|!68uBym&)$}dEy^vZrNNPm@^-?iam1m39y8n z?7w(cWU@v)lofjr``a?^WB>6n7g*OG$Y^<=T%pDAe{YP<^KC(&BiN1Dy^beW$>=Da z%<#_p71;p)WX$xMK3>$nf(-1UaLidTQ`C>8khRiZV)trzU~_JN$LCO3U7u~gJG6r0 z+-47Ux)nJd{-Jxlv$=v|;oq;TWcC{{TLrIsx`Q82^}7`3Ezo}J)x(&&3@MB}h^NDE zYe`0~e^A)`EhW<`%I1rL9I&cUSC*Fwjz(@*Cvy9I*r}=p11+1t%p`V*6%)bqqJ?GCQ0ktxe+2XxGZ700@jUHpnsnE>W2%w;g(`-T zJI>8;-zeNA$5a}5)WfC<&@Dhq0G0Z3l16EMJC5&AeT;!m4hF2wg2_^3%C{`x6C##x zP=YFI8T4MbB50^}?SH_G&uJk>i@Tqqe0u9CNmga0`FexnS=x&!>!{c7?|j~S@iO_vqHIZytq_wJ zgUrn53x7ht2II5CvtDQspuL!IHH8>t0(c26%95p-BkE`4S+X)&k=Y{%3$@WXiO6k8 zmS~qSypsz9=Sf>*x^bZ6uBUfLmJrAKe**ABh)u$4ZfWGu(0Ip6c@+NXQVe$wm|~bY zW6GD6l~zW*;~bluHdPi!3j5a`Ii(NPHmUw;Q9L_lV}C?{-eMp(xw>RPxogLQD zx`so^u!|ld##B#sj@SaHe_hb>YSZF5knVmuPVxZ!>sd0Z!rxUh0-;shS7}~ge{Z-y z8J#AV5&j|#Jq*%tP_&iiuz4ZbC|&RyD4~`J^VC%Q+JuZGWHR~6WF%%-4<3}qs)H2y zc>tSd-y36hSs$#5=2WZ@rHkfttP!aUpfd+oJLu92bi@xO+9zq|!|P-;8z(BQ&HmGu z;3;mQyN~Fgxhd8=%)H$}#coFMfB&4X7|u95f!>%J$`O0scxHhx6B@2Ju!GUBN|J$TWqe+~$_ZxO4<7!8WM)`C&+T{Nu)L2nO-8KqElyAXPl`E#Wm+xa+{sm7VECLA{M*xgiaN2 zGq&mMwYNHDXSM0)7WDWlfpw@)XPrt-o7;9!FsM{!ff5VLy1_*$Dx2DTBvG7;k4WZaU>!x1W*4v*{^ZVfBrJvpgxJpU7AFpK{2VK<4~Xq=0|d-mraFgE9^2GulDX& z;Yan{06lIWhlElsgms7goCLSUZ?Nee`4nCWWv813hvAp z|7k$el{XudoBL+Ord$h^y@o4su^2X^I@kje*QJ_f$hKqOXhxwL2_{o-&1mI$(_A!v zHPJ=@3M65-=`JyFh^ahiEOvf=7SxzysLyE6f2e`n4*CLi%_Jf@Q>`sBIn^{}dV#Z_5_?zx?3zus4Nmc3%wy)G6X z|4LuV>+4Y{xx>%_&>H;(4B+hi(1Xs(+qFGNNiVU?R0C70=x22QoWSY|A4$Nl=TU|!FTn;H#1az#7 zuH8$9MUHEB0iyO-i{#g2I3vz18cDsk+wYFED{+ZEf6+GhB$O|Ye+K!lz*JbuF@QQ& z0bXWB@&f*y-q2E>yKE+Y$!24|8hOThcP#yj*#yd(6J9(`K#C%v^~Q+D+A+H-Niw{? z#kU=^96%|}E=`=Sxbv*&+tU$DNHO0OLLCXLh zCR1c2fA!r}+A;T;=82mJDnOYOH2nHbB=PW?m7pALQeZ0RheT$kJ2`mX@H%32&Q1)W zaYQbc)9gl!fC!AnOc=x^jwfsr;~`pmR*};pH-$4gm+Fo+3Gf?_DP<${r?fpysHCrB z2nXaO&*c@!*`<=gsO0B39b-I`>L$6}s-Y@le|P`>1&PGvc@5*NlL`Ag%c$!O73c~y zf`JpA3zV97J$?hlPoK>LRUsWJNeoZqMH$_)+lim~0(LCYbZ! ze>VQnHan!CoL9*tnZ~jXjI$!Q!WK*K_&u3>pA0)tj>Fe!!-RHfD{N92uV%@_J4E+f zK$X4){wcuj$iIB|dgnp@MY=L-;mo^!F3(q<%kzM1zq4xJZdkm*^S>hF=jDdsz%-QV zT(V{j12k~s>=!2@O%p3B6`A5^H^vnKe@aRL8jw#i7RMZzyHy0q2uS#lEbeIB#yA;h zg>%!zuu8r^1B%2dCqr57x;!`Wop?=Eg=!}2>z8Y&f!e}y*`u}@7gB>b7bBGAo@9pe4fW2OSaC-bbA`W)?UpZ+< z$KUjhdM^hqhm)OeU%HUN&gX6}KODbw0iT~9pPe|6vzG@JUjcD?_wb;c*Y3{g=f^(e zg4y}-6FAx#W${z`hX*e|KRkxQfBR`AX;G5i&dX8;pog;y7O@I`v3q=UwDZ!<0#158 zKD9~i12#J%-_?7NHtb#;A+SF|uY!U6dYLBU5vSva29ZqQ!RexRJ=}vm!h|1QiC;#? z4A_Z;VGe*6CeY%4#YeC2?Lp8k)HJwL1UmQ&tCLrb{o~W49Xuh|5fJw~f9{AO{#+_j zkVzv;!4Q#T!rzvVnVO8LH)efcWYaD{Q~hOAWViS~ml+iRRi9syQR$Ng@$H(wGr2Qe zb_yanH}kqz$^i^=GoMb_%Z6_$JEls*P~aC79z~vvFB!^%0}RIEP>#@v*Pll2SBK;^ z!vS@CG>s0{X<97RvHAc)e~<@Keqx)upfWVK2^bNK=>$;HTcqKfI{RD8$pE8^B|Imk3N<_~MTI6dmfC-9Yj1-MkJCOI}> zc0%_h)N!{QtYV{c{Q73^%to1R98y?Fn-}R-TqGi8wX17N(U$-=K*_(R34eC@2^%`1 z-LBZnChLVjzxh$*S=S8b7>!yx1?l^?`W{p^kikEpW>U=%iIoOQDgr zPVuEQ=TIW2dg8~iYSP&Z?0=oId*SpKs_Kz}a%Pe@zc49%xhmcvMyyRm$!|8P;g{MS zpsrtu8O^wZQ|ZdqTfjpI!FQ2r3OkSVlIcsc*|nZk=~`$`;~#e8;X-&CvP&bvj^=ilCK##R9`YEyHVzxqshcXc0yMkE!sL zWHiUBh>~i1ZAtzyHPu*IePy*;kse)-%@=Br?;V-G%o+3f+Wpl zX8@&+L8LcwIBVmh(Rx=jX+-7LhrxmZx$1`^Y2uy9i037BE8=beHUdT@jziq;lz=?p zK3{*Hkj=Xgs`QQ6RezV+E-vL_oFOE7t#%g|d|^|$HgZ}qN8~<;98(W#oaP0N)u6kX zoNQ^mNB|?sw6E|PwR3|0bxNmX<0Q3SufD$P)|Ck(mtJ&KsN9zb2$8TZ0rK-_*2`O4 zytH5r$}9c_H2ST_v=$~h@AZ^D-skMhRa<7V&EpIdBqShiCV!#J(x(mli1K?z9h0(( zDG7bV@)GXC>>a>o?qK(e-l-PF{=i`IjYQgUa^*PK6U;HK?7am@HeY&)-6`uFKA9@3MkO5LbDM|XO(c(bFH*1t8p*Q zA6bu_gRe*q41e&-;`3>np%Oe;ztFGJu(0p5eycS_Z9I)C5&3R5N^#cz?ePK7?V5GHmyBr~gG6*!!|a*V>ffr@j5N9*(h;6ZU#L9_N#T z!{cxY8tnqE#u=QQ_PoTSgMJ^fzJT$j(?L~Y&o9W}_<#5-7@K(s<6MW8as$`n6P4kgF&8){hfnD5Yp4n%g+y9_E^NTld9A&&IWtO-@f$pa=7z-@ALv5;a?met3ZI^ zsc(975NIF@UMbb}k>dfBmBgz|4XN;G=Q{-50~9M=Hjz!!@37lwyR5GBcV2D(e%{Jw zuVEH^n18fhZNC@6HsbUKA2;DKouow?7umE$i0SA89=6{9@WXfC{ctn72aH=LG^y`x z!r!}?h!8|ao-jGCmQDi*um9ZN*m`gG)6<=!lZ~BpD*AFP`zV|7EGXVdx&hac0;78h zAF9dw9pBVR7YU4v$;d`=DUyi=k4NAzgaYZZ@PB3c8kLVBC;_QhC74o^n=O4X6I|i_ zLibnz*0=OPP(9SPJ56FUQ%l5LvIgHWx(9>BVk&&%X?A?bi}>aiIGZ@#Ezuv-T-G!G z5eG^o$U-TIramaWl9TT8S!OM>n870ra@vHfR||kjC0MyVAU!2!NTO}Z8yUGHTY`wW zM}L~9)Ajm&83yK{SA@4X|L3@3rL$l$Dx9T(J1k{zY<`zActcBx!SUWP9u;tN zOJ{+*A2QZ403!TnhT3l+u(Aqyt&hFxqJP`vfp>dN#VxYUambyiax+1`K({aS`NjOo zc2r8B#12n38H2eUt8+F&I(ii`Gi|%{k}}tWWPZ1he6Fr@hgYyuNJ`hkvK3 zAmEc_n7`qM{}{~)nI5U$9p!KMFu8N{k6Al_`S}j63NK%#o13Zh=0Rg`R%D*S&2B>E zsA!mivuw81U9Y9e6XGXfhuM{gZZ6QFT^J1q4?exy{YIZ{n;f{D#P_?DVd?#q) z-|Ktrw>1fg%tq3yVkoe-9dCBWq+bDhU$Y((hhbu`XT|rD*R!hwXyxP?1)%s5f^Y(w z8^3oV(VC1~q7x+BpJq2c&3|*CF(fvjHHz9&N@y`cdg)2SC*K%<6c&9L8S%8U386SY zSb54nRsN=yn6e$FlSHf07jMYFW5Dw1q#idp#Z?BTIoSz|#6|Nmv6Uf&?$VU!a9VG? zKWfAOowwiE%3r+k&Sw6`=6j>=8+rSUywehqUA9Q)4pxT2HQ}ntet*Z_qS9DYo~6wu z2{P&@(6Wuw-x8-1;D44a>8HLftEG#7hdTXERi!rV=qKj(R<**ziv?N%jZ6Rh?@;>Z zjY|J~MWyFr2-yDuWks=|lQmT<8ULCR54-}eJ!%MFTU#Q4udOW@zSq`>s5*3Ga#i3q zz5e>BEj!kKLA+jDTYn~6GnF5!-O^Kv$*6gpUL!|ms>Or!yZ}9@4bTq=%WJh!nW~!# z%THWGmyF1R2gD>5lApNbOGe{nC8rNvYvT&91)}XMX5Wh3$nvy$=y_^bDu^x`Lz$#H zg4PGn2JurGWU1ihg|>|?FmY96YaG^;OiM(xx^8E$HmW_O+kYwLRt|oYjgoQBWAZ$P zOWiFfh&2#wj|02gQv0y7V(;v*cj|6Ut=_iclZcy25qJ}kKO*f*qM6z&#XVs>aSWZ= zFZEx8*RObWwz$8Ro>i(HeUF@oDFbgGR zCEdWU3B2gf5ufI#?HP%__8y2bvMoBo3%__8jBLV=gMXf!qhEH#!KH|KRlmqCaTyDy z-A=Qg(@`>tL~M+r+BK#mM*;1E`zZF)80HUzrf^wZEw@Iu7JHQ>`;C3YeQwfF#B_cTi zCAywSSp}pB=S>HUrN8kNU@=xUE9Y2D?R0U;2!F4W;+F*S-lXzwfpIh*X(wEw3Y%oN zO-)i!UK6t2W#oRK(Du*?v>~M&FCyQqqdA@)IgS;D3m=6v9ofalfV=R>SE5sFvj)^POx3L5E_27{a1BwK44coq!6=v=$fFKiQVCFU--l1PGjM>w7l9x|>^^?;Jgm zu^v}L+3f=qL_RXV4B}*pH{~VzBM-bTD1VC4qbpEtSkbb{MNHFaHZ{w@FSWIj>GUSQ z;vncwu(XIqq?S0E?gh#TA7rqkjI2Jvwsf7 z6^$E3t$a=tz(hW(`&Kz{xh zLk3QX%rZ(dV8AK%sRz#pqE-sh4}@<521g7iC3F(Qy>)&FA?+T+`5ymo7Wsd($kS{V z(Y1k{y1F*tSv7WzNJrM2*<)b?d10gt;d*XCaA7no5XLo?VJKE8KZTBYtbYKPDPYN_ zm4gor;hP+Mu%82O_wXTMJd?Ah{k%f`CiGbB)BuIrFkts4nT9^XANI|zUpW89n+p9- za6mjF#p|KkYMC~Y8hLfP*tUb^xkP^D_O}hXyN8k1-t(+UE{cGv^@{ey+ZjxmhE}wH zi^YFtZ3<8&2&{VKBx_A1%YW;(DV}mbYNMCP;$a);IdtTLs&NSJ?G=>vl6Q^nd?@?L z_Ik(~PW<4l$*jjMWx+bCB`cYl_k&=+?BKM5Pd_!*oGxGqlax2(_0jwMB z8>i}AJtmGyamK@y`%z!W{-$NNxO^U2l6c+$L);xRa|VWang@sSQGYXzxB-{+d{L2P zQ>BNttCq-BL1aXa%K5qR#bra&!YT&n-5U_xOcii$k13O*26-O`d0jbc4o?;%%CA~> z(Fr?blcNd11yy7FowCSel-vR(JgZZ3P+)=BvSsWxam&YcJ3TG7QU{c^cL8_%9F>_4 z+tt3Mn0n*P3Vt7GP=Dl(dN05#uAy^8C<8{RIak3&Vz)h}r}Nha zu)m8ZLEysjw>@n{3;R}?!%7`YwyQmRi<4iDa1_Zr`3RV~vfH*SLwHqhU20n6BrUouZt=M{(X|zX|D#1i7g@9_KV`sBw*5b5tlS}b zlPlcZe?2SqvR@|4)abuHbht9Fu)9%ORQrD{SJ_%B!TFVZKd<6j&+f#v>-)%uHcnfmsWwR%OaQQB#@kfZ$DoZw^}-zC%D zq)5Opqgo{M2UkmN~!OM?$suQ16x$A?9Y{!+rV4Tl9k?5AP&4 zS%3LyD5e8g!gNY{vqM*GJ3Ir3#&!ng+panXR*whvwBzOJ+mE&4qoHaHwAO-^8ToKe zN;X~tgYtH_K*jl%6Si%g1b55 zRrZnMA`ag>WfY6|=EG>Xwqo zE?JA(LIj3#k_zF!rqY#eIa-X&+PI~%Hiz+4sg;#C$qGL&?Vk_UmC}W$jmq-+C2Cg5 z%7>fxCd2qPpN;j6e^6>c+ts;0?ft5h|d0D8wAf6 zf#m=JyXZ8}Je0*V21}euD_QCCOn;p0<_2mVvQVzi#Vx0*xXD)hL129vx<_MBwaL}#iHlY_)=iq{parRe9MAEP&(+)( z)1%BYZ0G>gq413#fYv#6|Ln7%Lo>YEnIXN>k!4z|?1%uL?}VaJwD>prReyi>8WHAk ztF|OxOiD7F%LD;3%lW|NVLZI%^L%fBXRddFLh5WXqVvX5b1?J~;f%7$dLc$2X?&Z+ zQ~KCfcmkvnqa-h;*`8}u)&|+QHDH7A&JG1Rz8fx%`7j}9TGpnd!F%8&9~F+ zY8v0jJ2v9sFdYFI^djy+rhg z*@9D<%&}|*D3tN(6mWXis_p8d$vc>6TQl1rBzli3ny5~taPks z?!uCs$BDRvDfC<_R+M<7K)Yn5XUvzxVF(pTKEez->xeZNy7^kUF@Mt)@PGnTkWlru zDkRa|(ko-T<2_cv%LkQONv3YH)X^#f4p@4Ayb)rlfP=+(V7xImIM)~6J>l7)g9zru zPv_6|gFOcgk85yJ&7oNJ{Vs*)_uX`w5!=xvraSbxG>5Pr8Wn?H2rK zUaVsyl6r9T>9uy#D*zeWYqdwtD1ErBW@5;i{7ZYV3-nxTyR3{B`nuoIL!tcDljh#6D)JFOCU<;YQIhBG zi7mk)g`J6r`B{stGjhApY(U>6UexhX{$%f1o7<>?pYtz7@&N~~$7|q2hXN*XAwZLm z7#Luf&egso2aYe?om&EvU(BCiACgu&>Eg5Fw7fD<+ket(wc3|f?|z~6Y7-&rO(-pm z!`B=4Zz2r;r-#-5s2*a4$m*`~^9pMHZ;iB->KAM4@2l5+_jy&N`i14DB`eLqVu6(= zVBx>zT{k8$Y#4~&?vMLNgZ~(V{y*fqhmh}R_|*ZvVh}`oe)f_;&wxc^JRhVxyqlES ztU!I18Gp)PkR*6wSBvi%3_BKAS{GAZe6^X>dJFLPn7Q;@C6?{yQjp;{=C&zJrMki= zrEUqHl)5E)QmXGXrZ$OQcj)(xpv0B^(Tfe?WTt4rBOk-diJ;;WlYYF?W16lE$vcoRq#7^qbqi# zOQuf@SkD<01Kwj2#ei;d9%Uzv`la-}EA+WbJKMkUoXhPjw#3DudItonjg)Q=9sbGT z!`~?u=-275Ah}h-1O0uu_uSCf8_8OAP_5LeqiPiJDltZ%03+-4m*U7)^O+m5{N8=) zD}U@dgN~CqOE;Qw|ClOb3G?L-%D8cQ&PU##*`?EhR0Rs@MU}xL;gwEmN_}k#`W@_^ z_%c=fW`F%qYE^Hj)5XUSH@ajh+tv!&YnH35L0~{}UU22i2t$J!a3?J({8)dz%P$*DujkoiF`<282!Dyu zPtZ=@lIV+*7#m*))5NAg0;_b;=4hi@Wdc?D! zy=>2mDZoE`T+F8y1a%h8r7H60EPN(UWJ)z#^4xY*egejYsc86cUP3m)P|;HTGA~~M zi#g!oW^i5D5NgJ@ZpU`ie9{&ZOn(bV-a>D*ta^5>=0G3Iqo9Cmp6AnisyFD7(d56u zctv8RgR>|uVSA3paexnmt(YaMiPI+T2=xOu^T8*Sp5#|pn`6N#=A9L@{1&lWQF3?( zR$RWo78uvJh#7zIAWrwLPB}A|DJ}v_keswv<&sc_ zDB+RjmN(F(jO_%Hy+d@8V&CfBl75YY0ZcQbO3HWH^>JCqAs(I&!yH`1WsVJP6NOne z$$wV$DK!a*a2o{ri{SoHm`B3QbJA;8-tdBTf(XaEz6^Yp`bOY|e1D9t9pO>p!xOy$ zd*Gnk?X&#sABNd7XMqKcO->!LNp|NpFLlxmCsNHtau8yPe(+@-My#_+SLbe^;fyH4 zSh21&U@vvH7MH&Y>#`+0psAo9=PLeIvB%{CSn)`FBGp7$z6c~5mU7H&pQkolV#1?6 zKBDZy$%xGdc-CPgcYhVF_T3X>gk0N~*Br20Z9Jjk^(vaiHHxZ5G%RR3F4CC-?&HAX z*K|Qp9mAoP*-KQGOii_bT#6)L6g30`*{sfc@p$K1SKNs`JV3TngL08=G!^gJbrKB!|%Fc^s+>haY3>e_xb!xOOr8HrNL==hA;n928I=8 z^n6%0IR*19yeT6wLIgRLs>%vF&tfw9=&dVxb4CX%;#_;7hz_17S;LoZ_(;rpLLbX( z30ODxWeFH3ZGR5t)BLxD*yo=s_Z&ZQcd%<$UaZ79dZ0ta`mrM9U4<4uF0iw3P`A#) zO9h&JMwR@qsGp)3d!OhV8NwL?)yFh__h;{SKtCv`5B6h@P{S@4-U^~yYL}Kkzn?bA zfpL0;LhF>g)wtd`>@gAa8l8{no%cLckJRV+teBKn5Pu6MYFJcPvuvIxgm*qA^-9#~ zMR9eTRDk)*{pN-w>VA$RY+7o zxYAt&`Ea;T;10XG;ZHw5I3{T1=XRnL1K!++;TjlIt*LDAh>G9@<3B7;4hr{GMZ1VbnvBARC-v$%l7fQRgZ!mdGO9Ie$@bh2))vw z06A+NSATa8A5IHIjb+nbyBEJZ$GstY893lAr+@bhaie23M}_G`^m zxPPEG8#YEnt>;XLe4erLlqMuswUkJ(r2ey*asiWo_?jl^R@ZuJn$eSgmVTMqZ__FS zBE=%Wu~VvRa_H}*QIO#%)e=qe^~fu0GKD8=sS4~sq&^enVp0wYXRacU0u3kbg0_UB)Qa4N8?#vD%hMAt~sz1=yFNr1ueE zVDaV>mHQB7vb+Mi#%8_&sk*hqV+96yMM5^UhqAUgp~!=xf#fB-uL88girRFo^>XR< zi|6>z#5lm;*=yG?S038u)xI2r62BYgWz3$Fj#R*#X{3}Ncj)8IcYgBXf#~#N7=HrP zk-ojQqV7GdusN|>OkEx4-&CmW4zUh9!(O~a&H{lv9bnhSQ9EQAwHHrus>YVzk*@(; zpF(w#{c<@=sZx21!IBYhxm7UcyPPKX)o0p(BkNvfvzS>|DPLd=2(ytB=T`HJvizlX zcC6V#FS6?^+_QiM^!}u8>fDW4KY#hdSuTFq-s5nCl}+=rVorsqB3mt8Z*I31+Pdo7 z>iNX4XGD9a@?EHgcf(PUoKj_i5Vt>=CUHyrr6k^;rE!}62D%H`AkQ836Gq|fxImbb z5j`(7+JCK&QgeEs!eR9TcZajdt{pucXZ0_*gC`&Kcoy7jxu{hpV)f4h@P8IY!#{H@ zK$^;XgyA2yV?)v^qZCsH{iKk=nW?w9m$N_xsAN)rk_IIssNQ+)wwkCvqQziRMBdi9 zm^H=KOq$^9Bvd6?3Em>;VaLIqpM z6$`5}_q!&xho?${(8cSmO&14!#=4!B`9V7= zk-!SuX!=aIjebAyT~^Sx2||)x;`$6!2y*7#@Gl@62!r0oObFnbm5XjAr>({5I}awn zzYD_xh)qh;BlS*eE?)1eB3`!%G}i2W=cR60V*8-L(+CpXP!Ajzq2HcP31 z?Ix}|(4bPv)7rv2iIDg}y+fl(F&#eh)9x!0fRj_ip0W6VEo(aEFl)fSY{V6o3K`rT zi=jJs&WBW4ZoPLAaY>H&ww8==w!n9tpF>>5aS42ISppkGMmz2m%VBP-4Uosq0{1BU zTdVtznPtf^j(;Y;sx=1CSF(I)D(+sd^+^*Ylk(xJVvyNy{mTjfUzj*8&h5D5tf=b7 zPC~3Xn_(og7XDCVh@&pfm+y?&{BoorzguWRTqrat?+eY#TW6buULfE;YVd77c~?Ep zCrw%HM)@3`xC<6*1oqn*1ktP=xG;}VM~V04G@b;x5|2B5WkqFo4yKHw1(&G{yU|V2DP!LMM}~Z#yR(N5X$nrK%@!Fp3?o&T8i?)Ah7u&A z`|WMC^?y)xCq#?mwZf;$I`$j_EBhCcorgq#Q525`qFGy^o3WbP3wFcngO|0l3AA2K zj(V5VSSfkQmfPa~6XlwO~+t4~CgZtMvD1Q;a7MBEO{LAEkTh>>W0-otvxa+v* zcH>1kw+Ztp_LAUNre+(|K~S`)yUHeh!EnWTZJaV)JcUapp4~sbkTT=hlR-|s`E9Yzdu%CV;;}#C%88>^KsKy;}TxP!& zmw$^(7;ON{ism{pBQnY+7bIJ~+$#Art7?K#oY&0wh-`>;@W?(etzU;mO)brI+*wvP zdEM+ns@>_=Pyh6M=aY7YXULrB_ys_})lm1Rc}^vGxL|;q4-b;125USJiLVvfq|6istm~Ou}(_bp`&*FpM&3Jz>QeWit5NCeWcGJ%fW)+<$}5 zG6-brjz$sPq@*0Uhgap5AxbH4{h(i$biAnP+9O0_A_v%=u*mO+lTaJhWLF!V6_8iY zR{U3%t1@j%bjeknsG`xEBeG}ZqEVS%iBnC9=H}g}LO*KI`x!pUpQ_Wm;e{_$Uo5bg z>-)60h}@I}Ir(p}!r9~|a;sBI)_)knLtgfJ0q7?eIo{yoU7cAw+f2M&T;&yI%YHhn z)?)S)sx2bvXOPQQUY5&@qSChlrG_rqiy+8TgFI1xILV2v>LzKQ7guk38p73`?tMPm z|K_XF30>>rVTxDgRN$lYAMx(N2WwMn+S?uk>y?SR2<_KsZ73vG6V7@`_J0U9cABHV z6URVo>=4HP2D+YsrZvmVSNpiF&dVlhuQriZ>qVm&1PWxk@7-r zXU)?^#Y+^i4Jxy&Zp>B$27mDP`|RL9(OqVHreeO_BeAq5KGlhkm{?j8zhZw&!rk%R zL?+))oAS!iY__Zg8EM1zfFIF(E3ZeEtSFpEGXu--Xo-nKoc$h!v=V(p4CsXOc=E_& zQLyjpkD$2WA}d6g9T~c)Oe}UvofrCIy7Z8Kk&olA7Eez@GP-Dfcz?wL+S&;Wy@G1% zHb|EkplmB`H(=OT`ju!|gc+!%#*O2T*Ch)9Ur%%DIu zVV_IkgLo}kFPn_GQj^OZyTzZbqMk-u03pvcVFA*-pbh zF`o5swbCyAkNB2(>;F6`kB-+_6_UPjNX?@gS)o!T;zOgy{ zaC5mv;cwLsW8|)0R}ck6_c`KX6zOx|wq^603V+onHqw$8iyO-tyI5o~mqUB3OLtHM zY}&Cd+2=r4>rh#!O7Fd$sCV90t)p7x@7mUdGU|PL|JJzCo`K|eV<}HNcv&~uwZGkM zHD+QX{eVxZ?HB1;@S9uCO~dva0fZ{$hn#EP+6tp4B=7aa?SlPY(z1o8rFlvs+7sNM zgMZ$_mCnu7M6((OE2~ZXB=J_%-FK_lCx84<&hwog;0>JJK3Zm>=^N?F@9^bsrC&(d zfanu_Nz@7Cm6yccrFY=nQnsyll*sh#a~;JrEvnp1aSnfnZV}(bj1>N=xTNOl)lUb8odY*J1dw?2Ca#5w6Mr3# z7iXmE81x7mrGU94w_~K}^DxT3bo|{>5-l9G*}C;k@Eght6XFxc>ZaSYe8bB69Q~za zZ=VU5mgi#l8z0_hdFRuOS{=u!ZoNzWsRJ8Y9w&=28HJHdZF-^QiSS5e2pCO|^Wug_ znLW*pVdG{;dA`w&c(FpvhG4o3lYcV35pYmG1PP{`LbDEwv(3T>e6DP=#MIkuRwUpQ zxo@l8xEVeM5!E{I9__)A?DZmVZ?5(Xa01FS3T^}mal^D&)Ks#fuRJi~(28$oyPB2P zl*AY})sJGEhmBrqu#rZhDOzgRVF|Hr2X$!NPG+}pcTUM_F1fa#TB;6&uYc4%`2=_cmkc(g9#ZOOVg3IR?dw*B&Eq;gFUQQj3e}(iNUdSICW7rz?!wjjgOB%ud?mtTwuB7f*pKulL;Y z)fy5+rL9g|9Hh}(T^jwH(SPRjH=~~b8~$BexN8dsfLr{hV&h;7CxE)Yt{<0`r4Pwy zx7a*BaFb{hfn>DQ#_YvtED`9mzQCoon2LK#K)ahMx%Gkp-MK@Uw^TZjLUJA&1-ye z(YHTMUQcUd;I75A2M_%9`p7I!Kn)vOKReTEb(@JHfMa09*^^d}0#KaocO9H)mbuo^ zQnPD`Zq&m$8GG6%vm2?K{PJp+HT|CcreH=%+E>VaF2 zuMX}FVu94-!GXr(O@G+A6(s5g{#(1h?&Nk;>PNg)#+R<~#j4iqpIvindRad6Ul*{f zFV8g2D@tFL-6vSq=jp~7P4r3Zg^7$Bc)kJ7KgoV+y$nx$pIyU;;XBt%uK5CQcJ0G& z|3)J5w9_14y5gRa(Sj={7~G$fHNBR)fV8H{;Oy^e^P?oLaewK$Ohz3<`d+4Z$nMoA zo{M`&?MCJ-TeGFOAINMAmy{I!Bco;~>E~Yixz~Pf!z29d@uEUE!a3VM3~#wx2IVbj zD;3Fn3QhUNZb|e3;MB@hUNwdP?x0dXZa<%A&$oY~ixJ)A8n}{E`rBT5Hb0N1Kkv$@ zn>?>5{ats*RDX;0<5C#Uzf-4Ma+95|$VpzpXoL_pKL#5*0&59@d@lPLA%`XV@5SGU zY7<$a4~Zc>;-jKBS^EAoTu%Job4}iRDW2=Sd-Ghsx4hD<2|b&i|I`@Wr^Az@?f)I$ zQp4Pk5Gs7k>U5gtu;K0M+N)QJ=|FvR;rrHV=L8-+SaLwuqmU`T<3U>o zVf3u+J8K`Zbx8%(8qwBDM;x?zljiyQZ4{ek59FSB&OU?wd=}~0C)u3dAf&Ad#qVa? zbjY2!$$w_N#a#Vz`PPMo;6#cD2%!ht$E=p_FhMEemo$2g_r#E__Gy${W5EW>V$mk6 zKxDd&@U3=Q=v?YTHTQ{TJeW_jI^TJGbn;|4J~%p5j|~S1T=>DKiPPJn9A^WIv`2?D zi6D0U1A%YshEw{d|5ihW?~0SQZV!~UZXe@q^?!+Vwqn7~9x#M+!yxC~x7Azfo%91s zJ$(t|1xY6UG9RSL#hJ**x%3G^d+qw69b(;O0%&moTN1@Ee85o(WjN1e=kmD3Hcb>w znid2t7D(C?0*XAO9a0f1c34Rm^n~~WiG~&Qjy4!Pj5qwO$duRC>i+t{19LXXUaz!Y zDt~%LWTg{n>`kC77xiNeIX2h!Ho?m#7DM1dY=(a(Y@1WJJC9m&BLLK*zU`!?Ta!O! zC+V?!BuF>g^U>?Pa$sKuwB51B6}vl2=6;u7mM?ATj>dw=-h9vG8y0M8_La>;VXtgvCT7PVP|_~eiM z4ETUOx}o5Pp6%{8)vlw3PxJGFx_>jr9=u5dSrcd|-A&5xwWI_OHT#8%TD={ONJjH% zP(utkaDh-`0gIDhpjC}dd4LH50R{a0i>zkS-d+-X*>9QM`EO`UkUYMD^_CSzzJGD3 za9-E((QVcl8h<8|!A;Q#(h|NoiTP|3x9BI|B}%ts3M7g`;NXW3 zh2Q8wB6*F7alMI**@-)f2ZVH5LSd>V3iYEw`IA(?b<9~i^vUnzb~?1BXm<%`7+6xW zZ9nU&o3Ulrh2MYsZAvrT2ub{jg2o`tTzks4H87RDp&q;=mJvrtk!Fy#0)IjvmBhw0 z+Yjy@G-g*(BN0{f8Q;92o5RL5?yGxsNj=9-@#9%cSA6E;;dw|{X zB0M-b9d8of$#)wiZ~v=w;MRZsnnH=R zn5NLoq?HOcYh!Zy-Dr4xboli1qp$Ye>twl75&eqwav@W7kkP+()Up-pKFk1nST(2L zTJwCm_C=hbciYm}w8vOV33&@Af zIt9S^Uv>#c$D>!~5TNc?;tp)R8@fY3<_XyR>HJ4|g@+&T4z^5c`y=zgMQ=mN_s-*|vLJ}gi2jB@_HYR~*UEb+M{ zTwSyy4XCrqv!>@Oj6DoNvvZZ3o9@KJS7ivJ;U4ZddJ6xIKZjrAm|cWW>EY2+lX^d9 z0ur9)dR6r*1Am2nPOHfIyv{ciHja-O?LJ{$t+TkZykDNp&?VArEN0z)xOWj_c0>F*#B)P zVnd&Pk6p~u;o<(%(dpjsctfV7p5%3%o#$$Jd{DRFK+V)8=i2ADvROfr=={c-WeK$f9J{Q^mO>yC{Q(>KQMra)X_5H5MpC? zY-nW~@qZ|Y&|?wpWwRO1r-GKHsoGY9ZTPggxL%C5pmTS(g-izpZc0R`;e3kMZS}df z7o~p+-yP96r2<++_H6L32zeVJK5>#&{Gy*^WmTS~A@H;Oyx^x02zsJh>+MOkPvz2v z^ze${8t6o&lIr>$&bm)sjO*LDgjdR6teEE%N{M1^?eJFz~Bf>f@rDJH)7Uq8C_LyGPZuXgsnIv5>} z^)D(s;jbm{1FiB&UOWeSdMX+tiU>H-b^Og#&d|%B%Of6CJ-UUOf+b=obM+5Qy-^dJ z{C~*%Gdk%5ssdx?7xda&MM?b50BF`s?&J7oUKn}Q52Z@=hB{r)av8>37Lo$<)If~p zfNYYJDpR(3XJbNev;~|^!b1SOYb=h>i5!*%L&(_00zJv***Vj^T{l^iZ?pCZUJWY$ zG9xRPB&>3z@iK2N%BgTK{G(!cPEh69kZCb)XwpW}V{$shLk4wtl!rXV>^>8`p4fysSv-OK|+f*SCF6-nWfI`uAJ z);GPLXO{(fn`=do^ZZ4i0_d}pdE$ED9yFrYw6$vOT6VMIano7F-gskL<|KQ*FMo}= zMdOOyH{Q@4S?8iL4`^e-hmVW-)Bz)Xt#WW#Io}YfsT8pi0Yas;rt*H{xFZ$i z9?nb1MGz`js$XE2D<3n;8g2_$gp8+_Z0n|M$LuEc`Cy1AfeWdzl6aVF4z#g6Ai1bP zD)}^@>POqiTylJtQG%0$vnVRVb$Q9W+pfP%s= zD)WlSJNR z+tGo9bi@FYL$}f6lvp|~RU(7rwB}7;Hv42O4%lLzoG#wbq z*UQukI1pW3JJp|FmcKy5AJ0HBsM%ckgW&Hr&*1v>8WEz4IwO<|M5$#i&eXDms)z6$ znUeUapH-j&nCztVIn)T#3d8_5y0j@#>`t*riXI+FF_J2L7e4x0o;T*8*;$6oqeI zGV&r!{O+P?@-X@P9GyI*6VYDZ!gKWH*_p~%!iIZUQIQh%VWllv!&hM91H@Nflzd4X zDGd062{IY^2t2vQ?5eRN6i0*2%UFq$=Wf^3-SQa*0=_&ECx57Ow_pP)xVL67Z^MlD zAcOfC)=2?#P!%hxxjudH?*1;%jUzHOcn^L)2aZ7!G9JE>XE#v=x?1%Dy5e>#OY~hJ zmiP9hfyI0>Tae(&s`zu>$ZhQMf&|x9C#a0|Vg`8I3}9rYsm$bnHsb?lkRpN$IC@YM z1T$082$5h1gMWmkRF`z)PSIvv%~5^=z!^r^iK0{vso*zvE`-vdY@FA8KRrj4?9Ga~ zKHwedTHI|O{l(s~KQ&VWf941Ohgip6qO;3w5lhvEuyQE`Sn=Wk;0^m+qSIuh%U_3~)xW!2j7`y;~qV`1T z@IC)}3#D9M@wCO1$NqWuroaeV#1T$N=g`&?W;1-WQ*So$;AcoGoxv&)pTFxJYu9M- zY!C7josR)H3n~i+6ea+UbU2YeX^}OqI4%<|C^`8u&n9E#47mF^LMr*`fysQv zVEmK8FpndiY}i|vw|kh{8(t5%L>@ym;OlGn-JpT0Ky-yCmGajwIy}Se$ z@o5G~Vg)`t{CKzz{-B4)-{}G$W|Mq`Vg4FQ;D2ckTqq)8-*E{xQzqFplsWlu_$lGH zWTCNg6Y%Nr=lkG*K#gEv6UM#g1$_}FOQaZ@GAVLlesz_V$)$x!C`%)NDKriQ85)Ep z)Ho1jX)vB(<8Ykkk}LvmCK=ZWOG!qcr6dzjNHQ4`RvK6!%_RUdxk{S|Vqc$qmfzJfwufF7F>B`+9dnFQ*2p@>%Uo zWf@_n=wJh%gp8INd~Wk$nL+O}S~z6#cdsOliVO8V4zO;B6hezRF6)K8Qpme7@sCPk ze8e<75#d-yHD0vTUwwNagu(2}e1xygyyKIedNbF~9=~F~T|g*T5;_SgGRFsoIDcQ^ zy=aJBCNSkN8O~)x3kT22HK)0ZKAst{)PI3t-<0P-70=0)YL$;!RQSVSmu%lO3jk6; zt-r|gPo#3^RFn4`yhu-!iZGX{2;!ZEe2g%kp?wK8vs<)8Ds}r?K#K&C^9%W*8~i*i zMAgbk7iBnyqb4^4HsRo$d&#i1{Pc+RDc6Y{T%Wvv-Hi7!Mk-fmQmZ==h14J- z5BP0ju5?A^qw4j<7TXoO0CkbM5#WDjg@1mUXU;;Pt^B;pvEc!WXn3$z84eE~T6SJ% zJMd%%-?2V+V8*$woYpZ$^Ql;b$=bl?a*-;%F-aB$UC8EW)~$;TXX+-sLw=G8$~7E5 zh>?L~GGf4Sk9J3>JR-18Sc^a}zQdqmGfPax+d4S`=QCZe!Vcp#BbEIfe9o@9r>6wr$fJiE1(hr4_?ju{Up4y@rMzk1yLw64<^ifL;g z7b);eE9>5fXs)w#GP_RHL6(1-4b^pOlGR)A_)xObT0Z{ZCFm76Q_a`8)(6&poL@fp zn#B2ca5)|oBf zZTPJ|;Rii&1X4U4ImdsiS*f{?SG)vHBH4vu&D8F&`4oV3P-;cVU_+B~>fXs#Am3jK z9^7iK74M@7D(AqMInGDvjLnf!G)zq)ubA>di!4z}tf*gY@$(Xi_0q`TRAp>%Rj!ph zm*;|;dT`FF85!B}R$w~03u+~Bw{F5(rXx0N)R#%qfQ^S zwQK5Js4|bb?W78irUUt4_IZ7cGlHJhT)MIA4VLyl&n;kdosE8kCa^46?6h0`6rzZ@X8oQO0 zB6Vp=mX%oSF_O5zZp-X4|258KwdZEDZ8iwdjJ}+DkzE!v$!_KTd<$b>WE;CcDbZ^Xa+hc-z!s-(}p<&pT859;C(Rp2A?Zo43<8ik7Jxhf{(fmOk{7p!SE zX`>FtalwBb!^F-XT$*mFL_@KcG$|4&n`E`ox5e7_ z%2I86<)O6g71Enm{t9h-Wlh?q)cVvdD*RUh;Y$`~*eASNY~dZjo614@>?xkAWE&Zs|qgv*fI)iR)a{_4v#XN}x*~_G+YGNzrS;SFHJQ`aIuWp1SDAu`!98UN0 zM?GAi3SFBcAwYyAkiK*zy{lM|ag$j-n~zuCSlZrX9yn8{%dxIQMtF#=Yw;zuX3@ka zNJ@VOB;dm5jVFOzWFd98SUQl_*lDx}?-as&6!^Kl9n5EG+>y~PmpjY1p0oE$n$Pf) zBt~RJg0=W~**6jk80vu18}uEmh;E_C^$rKJOaFqAcwu68#63E>u+N0X#;?-`CU6Dq z3#*zQ8TR*f0xxN%;2-N4X}slLC57Dur@(&+IRq@9krL)nf7S2zbqUyLJdrY#u=V}R z>IG#1eiHzl003Lx?D^4w`*aC#pzfJT&=y)^%U4aR))cEOy~;+8KW( z$_z%OEn?)0nskM3sOxPJ zdA2@Kg9!Jc_LSLpy{!Sa+y`n4V3vP|cy7d938g}PD%-k?BW#>jA5SxJiiFXH#I~Bz zOk1O8`eI6f)m`XpC-ZJp0?x%W(&*EL%CxgsBXTN>XBInPC8*By@EO4jUp))Ut#$>#yi-vt9cBUMpo+WCInC zNitAPp+~}SuSD`OH3z8aH&umBHu1_7##HK3P^dv#+?Kk;xL)LLy4M-Lfw+Mj95$=G z=r-RJ)9gWG*ig-W3>!}5&%=K(h7D$hQD-os*l8= z!9+Lc2ppl0(A6H3Yt_R4`6X_nDVaJQByD76S(LycO2j7+q!j$_)k1J4J;LQ}%{J_{ z0`&u?h_JXxyj8Ktxj-1+qQ<`ZrJrzv5O-%uP)rx1N>H@wS50L?M-_j`uLE`Gr~IZs z&W4gx=-QMyU=-XrrEQ$LvAlKAb3V*4dJ}h=#%5yXD(zINT|;3=9wQnih7r}I0HehV zn%IH*Ey^6Fnz`d|E;(H_Gw2uyDsH+efo!c)lfD7CFeVcE; z{3d`>jY*xU>IH@x6;*#<`u}a)9_IB*B=_%Kx4#)Yd&@RQ-sSf#E{Rk_ZDx2AYX{FB3xrTr9b5)e^#h$I_=S-}` zkQ_O5d~$Tk`1GL{(L)QWJDWevMmYPG`Q(ldP2Iqa)b`bbtKXwpzVYyvP-zvdf_Tc9 z)dZvMDX0L+Vnm3PD@YzJ#yfe)1wr01qN!1=AE4_n_Xj$%b;i6VXB~_~osVkK;Vbht z0P&ddRGovv2bzC*k>N4rykd8BzQ}(%JUM}saxBX{2~+#mceb zsr277T0D`q{&?|(c6|}!8C`SCc*+l>##8cY5#6;6!#aN?p4ggdx<~AIs&z^9c!FsW zKVI)Id60Sd_^JD8c~vamlm-hLe`BhX{qU-8)I!eIo71CD-aFbmJ~$a3nC|ex zUyqAOG&e^^^gClbj31$B4&>*s;uGICS}z!3y_iH;}MqRfEag}b4?4_R)w-bM(c5*?pP~i1HHwC4q7au44A08fj z{_){)vNs;<+`ig8p2M$Hi8h>;MR8j}_kZ&G=0I&8!K9zEgDrjv+>GEvjwV{qD4WtE zrf|Hl2pjzKR8{$m)>}44l3wi;rkVt6VWKt_KXhQ)vI~TNP^6Q-I>>G(#ofu?>r5;r zGHibU&#dXQ)_D;cp_b2qH&5Ppdiur7+aDgCK4Gke0WU%IySBNw=Uy54@!r!5AhSh_{!>fTM=eLpBG7xE5V z?JnM&({U4NnRAI?@Nc=x2X{VJD0|Q0kMV!igOTpxegS$6_ezE16PXYVR4U`n3gc|q2TZMlb zhdn@3dG~@YoO?Ju@g_$3F!@pTNMTv%ziig)nl~`S5eQ}Oqf_(=xJHlX{2Y9#w^K=_ z6W`Yy=`-Tn zyG*k(u=2FTXDTu+@IAPICGxXU73_a^)ChKq>ezQVI0P?b{-F{9Ting%g1WZ8I%% zx)Zv*SKee_$jX9eM*mziHS^{Q^S`Z>!$8o&f|(16?rd+#sn5JpOTek+g9P)gIWMVY zyj_D7xaO@b!YpDv0tc%R!|+b3(!d3*z_P(>_V4x4VbItbF^%S1=0-pnEW$)wD?_M~2?>p=OKj+ym{>xG~ zb-nCG9@(|n2kP~mK`LXlAe{Dp*E3Y(vab6QukDl@aZwJW0u*P4}aEi_Z|_3e|2u6AgV z({jA4xutn{A}tzLPZ8jR^829yhTqisqSq-)o=5J3Q=loh67PMdAk}|-3~cV_A3k~M zL&&6D{*cU1pS z=hSO;Uq-sN6+b1QuDg2$F!)ZfR?r)Wd8j zO;Wf(FvGN-#TONv*)+_jxAYa4oPPGnTUf-mOkrQ4j?v5%DiVM2B)Sz_(g_Js=wD?o zK5b;=RF4I6g)TiXuh9M`zsxx%);LQiG?`$(gyIvLs4nJoOom9+OUY}=E3JldL&9_P zZ>ZMnJrx94CWkmg6XNUmZhBZC4B!mPuTJ}Mnin#*)pA?5rH7kBWZ)AWF ztIp{w{~e^{`0zk&4ZPB5PTscodmn#}7RYEl3j@ECj*HFTutwqT#>_>)}#d1^*W8phAC=mh!WYarKe$hL22k5BoLY z$&e%yv+q|5|2aBgf`A*)Fb?l(I0sfr037S)GlT*P=q_npU%$NKq1;D)@-U|qbKEIq)g zeyuTNp9m7-Hn3%eR3w6WZ+?YzgpvyN`!s|7HY`ltL+8XKRteR3R+H*?iC0vx$E6rn zmRwP;Fgqs9RY)?2uAQ|KrFX$xGm^alXy%|%*`8H3vVF}~w$a|pPMji#}ig}nIa*>UJ>7&liNG<{^T z@vy}>)>y2I2RYpA0KH9pTj$WUB-7Tsw7J4~4KPEG#EU z!V-zVDC}8GTZrpJM z8_{KD9$}Q=*pi`35@AW@rvwX2f6hdZ_}hQ4Aw{yFe!1@y_dLKPnFrrSX=w6R5s2-# zh+vse05~oerB)UUZ?{(9J+BOfg8~}0`|)47t#$W9_FT=P=cF@zmDjenyXT`JhVXb) zt`vo@4IR^h1~(8Uupn8)KDWPj{K3&5kpQ*Qv{~W4%9kRU>@;C~SVD9;-=j{&o(X>- z^r8QAPY+K{_C7fLLydDB?k?uk$m*_|WjBDa?MWK8g(~%#A$^}RpH^~N#XDP0r-L>A z>FhI!{1dyu)TchM(_u&u!N)~9&c?$jaoO$O8&aLroFx1^p?_r=fE{UM=XAiBSBbvu)?}+eZ|am)5%{>`uuELFpEQ>_07UPScACkGOy4y~_wsjv8{7?aB2%dvmv9nO+8r9#8#%bcj)1+ZQa`6wqbU1G?Yxt`)gSWCHyAem2U}9oo?7qve>_rue~}4bcpmvHgbrPOnRdX@)6- z1IK&&f&8G8VONo1LH`)k1w0VG4C-7M1nqF0SASmsNgYi$2Vn~a@qx=& zVhwl|@y9|kE-#nTA!GpbN_6l=5fdWwlaWqn$R_tFBBGkS^~1Cxp&(!N3?a_6Sg)@- zvnusnfU*gY?#>rNB=&zNz}nxfik#+nKvg%PYgN?r6H}+8=p*l-kN7QaQH7KQy9{{l zO`jWeE?i4-&xt1v2_5Gox$zCq#FON-e;AQO64cn4?$ z!^bD8Y9sD<-d+l4nu)fQJGRmXuLHma%!lbC*A_H^V=)mDEE|3O>~j#A+tT4Rbw zx)_m}n}9@l@f6I_LJ(0QjH&R{V9GE&9p0IbQrx<6JqD5Zt&>wH{P5gj>7eJte9)V| z=F51vn@=_+@e}o=a^k^wiCodq6e_+Q;G$aIgnW8*1oWk2IJnBInO2S4d3jq@Ffo}{ zKqCYQYNm91gz$esIWKP2%-j#OGvg8lJT0Iuc4toKv*HG5z}?E7_J~IZ4)NA4E+d?k zW&QT{)ugycC)?Y`jaDu&q%sx*3PiG9QZLa0;+LZQQIVf&1^k?VQp0vz-4$~ZF-+7V z&A9t8Bg7ICxPxBQ%D{2D%*Mh3X-*E@O@Tetu4r9E?Y@6zDl~ZJU2DmzJN-G$C%E0R zXpQr@*-3>tEQepx$&Wstt86x~#!KqbL_^3Xd`lzDLrAf0!labAW?ajqYH=?Qh)7Et zaC&kl?T~7OGPftO_qP=|+@Bl*k*KGC;11ijy0VRcGS~tC05a4ix|zkQNfy;WGv1Oa zs9WEo4&HyqF06!yh5J@PMi7hY;Q{8}wb2}~OfWQx-A6QBOWQVetI%`CKhWLp!QJt3 zLlH|SC5&aD{>Rb}!5}f3!8{YUei@*GXmd7$ld>@4oB?1rex*DIe3RK5V3xgMHn583 z80{g?3Din2g{iIh^gEg{JdkAmmsY2OnNw=0c~*axPwK$VH^d$M`tUeR*!y_+#i69? zc{oU@fqjzxjNz+6LL75(BVm zV9u!W41eRx*8FoumefT_BZyfLxexW#EPM?q!DWYJMci)>2yeYL{1#}!XQc=4@h45~ z63#~gj8Kyxi#mX{agy#BUCkXLpgA`^KIMNwQR;e|h><{$ax&&+$d$9w4uRU27Xey1 z4hkl@=sG|A0=n425Z*651y{M_-J|Jyc{#f_UU@X~>9m2_gv84oWa-!|#z{xzewkLw z3KLAb(&BEY-luhuztrcY7Y}F+mGSI-hzolZpI2>=yM%Ct{x}2Iia0LG^>Nu7;B$ZD z%W1W!zZGVhNi&wmwQ#X!_c93$z)FkQ#;PDC703UjJI&jGtGadk-}@?D&h|t&b3h6J2x3m2^WU0@JZw~%2FB| z{HQ`DMg25?L4!hXQG15L0Ce`y1%f=RY;(og}SG*K6+L1gZ>m*;Y6uv|~C zX}D%&GUp65`dYvWNp!7z#Eca%5v_ayPYYml4G6J{Ijd%p-U6Ko0J2HGJW$y!wSni} z7K5jJZy1viyKadjlZXgp*))H`m90LJXsN6wi+!?kVnTO&%USR7N9LqDG{r zUuU_A^us+y_5=PV;M92uA^Y`~!f+mbwYPuz@z-JvP?;SXnMKK`zsaC8N3wIN)XNyR zA=goxF7s?MwmTC(1U$h<#rJ6k27CSifAaeZM5zYGecuO zrQb!FmUke%qNIzU>3lK)BN7%{nPGYsLzl>$!qk}kG@jK&$rS)8>NYc4P_lz!(*Afk z$+x`ugc}E6ODL4C)J%U+02QxZz?!#KQS}iQwbaao3p+@tCyrJevm{%!yq?s84Sqr! zjzmOMlP@(noTKBtN!Bb<*0=toOO1-m^Y{daU}Uh81K@#zBmx?dKftIgDm3L}2k?rI zoPSM$fGoNSgR0mJu)Y)CzIKCE~U0lBIGG`g4NMKBsWBe2|a8=KQ1Vu5TcvPlb_m;t~-2zURd_he_{*3s?O#i2%0ed~|e%-x(RQI*P|%dzq`ZL3`|03LVNw z^UQk9?#|A^>Xr9rrid8k$6BDR^y`nZw(9EwC|y15eawG9I?rs`T^kBeIujJo*a?sM z^6_-Tv9$Aa!&L79+O$8jv#w9X}cXFg~v157bn@>SLgc=_gE_vGG#9ZOdT z-B$q4?FxTV{I?CLq162%YM@+Kh>*eB)hZ{>o&ZB1?M}UH*&0!=%OZStX&9Qz6dFQT$mT~j~nOTKDeD^Zjq^13f2JKkSqKw9l zMY+_aT9#XXk1{9ZfWZD<(yOGe*b+;+hXIp__mO|NB2viFU?T~|1$siH{1Wwb5y<61 zQ;{v(6yb#)V9Zb9gb*$9Lo6!=jsLd(!Vv$9TKhTo)Zosxsi!W#;8l8+wLKf4TAm|24us+F*Z}33zVsQccH;6or@GPbI3*cQbEDnd zR|6#q00%*ea4+a>0&2Qlm=qL`8Mp{3;d(3-1l#1`@>N^xh)5nw9AXr-9-%<4Z>3O& z4Hf_d?^!-H3yx!a;cW~@aG_JcqAar!T5Eroa0UWAOHC+`=?JxYqM4YM89GbM$^xBJ zlf-uXHgc-TnB_#z0m4p$yU%FmE~EL#tZyvhi3SA5krLkHgYG=udMKIJh+T_Z!)0Iq z9bXR0Qn2}^Q0fU+aX~fVif#o-S`#vn(O0u9Fb|wy@B$?Q>^2LKzkwd1G)wn6nCIbvYM7*a{YCD@hLCZVZf>i z=bJ;%9a)?+*YQe^f5}=2tMoRxa+H5*U}e!4kZoWjaxBs76fxBBU-ZKCHl&>TJo;w7 z4HO4^P?$%!iOC(_@b}3jy1ot#R*XT+<{-%Er~r}#IlGqig`uK+HR)5@ z-%s>NRcFy-p?k~y!G%ti-%WoKU$4B$;-e;TPW<~xoXF}daw>IinbUsyL50pXvk%+P zd7V}lH>Z-D)!#|zOjbvE`|VkE3Cg>I2Nk#8usq(p9+$lvrI8{2PAWjOucioV8tWZ5 z@5fLFc643p#dRNzV3TG?55gFXw$#5y7pQsn*9JP%e%QI6D1GSJpVWVBd;1@*3q)u4 zHQ-ux;o(OH*9BpbRrZ*dkBzzNu)7 za_Ka5({A+T)eUWNkeT3jxouE#$Nv1CHnYt;;jL<*HeuaOYFvZ?>mvpY3vNl;- zR2e0>&M_w^htP57xAf9#K3lnpv&~=qX8yW5raQv4vpRy`d{XXQOf>w&ey-My;J;Wm z7_q~H=>swuD3e2)>UW6|W}N5a0so5j3c>An2|2pd+4@?7ok$Z1Ish94LF-y~E*fIa z1e@;k9m|~c^w)oI+(QHo3HdnQqXi`B(^%yX^9uS$`Xhz#otK#)5l%wHk8{Mu1G#+q z9F>ZEUJ5}zBC>b9mhdMyu7>~>#xaR!VNcE)@_yzhDNRUc7&)zH^htsNV??^?bc9j# z$~*SgkcI@RA#p*yD#8*xll4g+1~d=}&tP6Qo2F7a7mk1AhA8^a#gM$CcDD2*Bc=;& zR_5;cS(oN+8fmytpFe9##PyK*wQqha3KXF_s!769A5rKqAd3^?UL-TxC!{+7(yKI| zJ|Pz#9&vt#b;_7q6VfGvYby^DDdLT&Ktk)r$=n=6b5F*{k^Y@@8EFhD{F@FnhFBqV z_0hGCsl|VAkA~4O@=ptyK_vKj$cLXdfeQqfvZN z69#`x5$ud{?BbwwHcn(wjwyq3J|lENmvc@)b8MIe6e4kTV0#Q zwGG2Mxr24K@SCeAFKLM?DiO7$cEd@#!?*sbqoS1e3s9Pb*>%vb7=-J>V~Nq6w$nt$ zwV7hjhl5`lFqSyDuCiYGsjBiDESDFkVAh;kD99b$WBsT}Y9orJ6PRTI)eRT=5`H4i z8(@D4EL74dJwRYW$DWMJ{B}l5)pcPd(tl$jKL^&jV@#f>F0ZFnHCK z9wfnv+1XWuRl?VMLr5(qm~}h9nB>*9TO(bhPiB?>#0x&2e}hxlUE&6ZG|X5O8*)v3 zt8;Wn>2NY2Sb~Fak0<*_$HPxP;1r6%pgw=qYOx26hodbzuHoKlVz`Rxpb+@M_8$tA zKLK6IrA8!vq^>a59fv)aBL^PEq6f&qY%VV<{R*@lb~yLn20d zT?ftw^Xz9jTOiE{PFyvn=5R+Kf6{?dc|A^4iH0{hGJ#P+h6IGdL4j!Dsm?zu4eNhy zj<;wVREUk68(#ba$W>q9dGDP!CfX2*x-Pbu={exgALZ=R1CzEtNaV!)QYxT<((y&K z8E;z{-)G)Du(?NaUqN4_5+i_UxWC^L$?VCK~n;*5|GU#LTJcz;&s-Aj}S@Vhr zhhQ;WGi9MErX{>`|W%iz+GxG8fS&N0&IMK3rAYorYOs5^gn+gh^>#* zXFJ<-`hB=p`aMH97TMq@3gxR$@zoX<$-3@=>y`}rmzRWIY6ShROARSi0H^vKldR=v zKk?G5TJa>s87biaimgp3E}#DTcX&bko*pR)4+vXSf~1)Rq29c$#!XL`QdHwGl$2W6 z2nKI*l6Osy_?a;vouYxrXc~WYhLdMNFea8?%g=dsov4A{@q(cX#sIUeXLKL8k^;@x zpr-D#_^Q(YM%JU@S^4c%9CG!K;BZHD+l~i3`4FpzmqEC*x zHp(~j0R1Ws*gmU{8GHugm)x4l(9H)zCE(YMEg4QAFj=YngZ{okxtxFWPan&*G|s@2 zL`^&99VgIuk*9fT2MMHohocGTi@Z5p&!H)NILkUQ*y#lE8gp6ljoDS(g$tHxZzvaE zWo%?vNH>BA`9=sSV&0hyQY1&n9%LP);n;@U_TV&(e^|`}VuC)PQ1lx`Cxy?85R%_i zE{^0N3cJ9)=Cd&*!ytdm0pCx{v+hQ6X7UxT<>CF55me6Xnu$ty=$Po$$k!5o;r6sv z^B?Qj!m+nfd+OH7L$x`j59Uo?IR2xiDZP42_q^8Z)tjnUlWb4Qvu~3SD{s@{-(qa# zC@klcVr#Kq2slseD@PD-6y@|L`{gYi6ng=uyUZes z&S`mFREK_k3SrN_8f8bd=@!LK0OwCt|APwR=`TXkW2=)W#T{>0Oh=cyb;6vt6;QEWjZg<1MLl@F{K>&l|8V$SNWO1l zqBHi;dyjwM2nk$WLM4MU);_Wf^uHJ!9zHW=^b{Nw+}e2v1qVj7m_bX1z3_C+B_fGJ zramd8nuKmbQNCOsc}b|e3mYDy(6ZyKsMH0zEa9!H!{%UI)2sXGu2d1Pvr_7=YTZ)_ z?kn{X=_bK90X@jQ;1mQ=xxGmg7|Xru+a+z=s*(%`$EvNyYM7 z-XIr!dtB?ARoV3@&RR`QsGs;fEuhcq)S=SJ@V9~KI^{MKvqAFs){DX(3c*R^_RX z=laLsVa;YBq9vGHXDC}I$I0k1Te(Q+YMnF}QjY*_U@MhxgY?a4l7rn76cUnmw@{D- zR@`0g%q`V36rPJlW?sZWEUCYkmd{R{7}S5G`5>F>z3q646(6#)G!!7@e->iLhJ9KxSfOWhL%0fd)_-+?-Y#jUm!2>GYhE z-Oq>im#lL3j^UWvaBzH76Q}@NQ>U1TQ$^5P2S(sh*P5J6BUTk`Eme9 z)%MMU4OwX>Rd1*ta*DE;;>t>0Ybj;rZk*AXWj5Z~Ua{SXX`=aKg@)vj!tW|AmFYcE zCV5%gd0_{20~b87qv@@Ae5~Dlz5){K-de@97GXHCC{hpR%rn&^D`#p5Hfn#cg?F%h zTeq&Xtw=n4FXBmcRW(N;S<%EL*7W)^o$`v)%(I81@~5Vir-nfgBG*+CFw!#=*fvvs zEihpYv@DDKRi&T?x2fk(6LoPQvoOfxVlPE0-x(vu8!`z3!$S`(RLY1qm{ zSeCFgSuU_45ZW-yjI?pl(_bGYs?)au*F`Nd4%&U7jn2pGeL(kd1eI|wr2g}lK$=FI zKG1NLZTNQK$L2=imlN*?#kGm^(=Km`@{@EgkWaFL;eV3RfFxo!m`=VQgBOl`@$>x9 zzHsD=4={(Y8`Re(K5~D45G3O*86WvykB}7g^x!IhxadeoYmSYqKw={!J#~hJSy7Rm zehYjsB9dd>C2CQxR}^rfKE1gxV0On4qQRg^AG9f8QsA+*1U4KUxkQNB0-CELX7yy48FNo%7Sgz`l+|*)&m*vhQhup4?oNNxSkfwI^u9zxu3V zqk&gejBpSo`~|k$tJa$vwm5e)O*~NE>Z5=2crdJm8UHh*j4{$BvR`VO z7X!57-X2wQJR#t2)y%EP5D~2vnV~7s{-+orQ?pQv^L(2SUBW-s`jC73CE6Z4c={h{ zcW_4K|6qgUFA*OF*H5|X4`lJAmrZzT&fgCA9qjg18nU&97i?$wdC{etkuD4LR=`5A zcKw6C5~P1DM;CR+E;DWCx!bcH)-W!;v+O3-OF4Z@4092>vkI#VoWh;$M6r*-E_saA zB_*a~`NO{R4imc=3?fU0K&G=s(g2>8?f#H=B-hhKG_NJX2~cd5^s^aZ(xM73_C~`o zTQ784mObGWR5%N$gmXny&6ysu>#E==u#%sUCK-RR=?E#ednK=F;?Pwjvt;014qMe< zPf03zJOa6}A`toMU~rm)ByuvpY!sao#GryaP@(VI?-|M{w|}3b0h)sFAC$Wf#!7WW z6YV|wt%}u+K+}2T9U#9W_(@4pPxBA0x!d-^9eV+;A9WQ;djgh;Hk-;wewzj8=Gfxo zQGb8Kyn-k#7IbU*#r}mh#O<~Ykjtu`Gb%*o;UQR&4jC^gzuAt_SvWRicVosnSimAS zz_oKoWd()~$t-T^1bb0ag%nn8tC%bK?creJip&+4<$U>=Ql6)Qv$QZ^!IWB=4Z{)~ zs9-Pf)7&1WW%c%u=y%o@w#Vqx|JP zXB=BZ6j-2f)E2Z&7yi>HXf`^%(COe=Tw{wxYnex8egijDXVEqnzAH+s0da-4Qwj;_ z>vTd;66EXWN(`|Kk4^}r`Dix!8o;&n^`hNSLYrt}X9>Q=oc#7NIbG0#atWo9j2nMq z5I<0^=p!wmRL^28tZxQ(LG?n?jzVDq=V`){DSTOOx<-j?G^m~Di*UaEPA|U22j#kU z&an~T4>SqVH#Q!WonF9cp+Vgq?qv3Ac}~UrSWGW^+o?v+aLrVZr*|-*6nr|Iu9GW2 zTBJ{ikzcumUb%(->29G{KB1_U(kp+T&=QNRyYvbDkR$X1RMQ7zZUz`~K7heygPscM zz6oD%;^(|2mmF!4Zx%z(uF3qB*(>kTovg?f_AdR9&-X8>*s1p`_fovkeC1wx$?l~) zH-=bl&#umVYxv4fb*ENwaX-}$d5otj+AzLyRK=*-eLJeK(JO*&9aPb}@XCKb_5bou z{gB_Ohf3X*gNnz=F8+^)JJ(4C;Zy%wH`Odh)oBWc>c{1<*n@8OQ@7OL8{%-vbrJ~3 zU+1}oze>;>e18e@cxS5pRG^X=l`(bhpRfLZh$@yhb zOuDAjx8Hex8=j)o1>|35c}su%_qUSuXK-S_bLTuS zrw-Gpn!2yy37zNZMV8YGO?4Gt&HI1px=UQ1mwb;; zL4OK)B$~1X%fDgJ%6nu1x})OxIGaE=6qEcq$)+iMb~n(zJ$-suT$Dfjh!@v+^mxF= zKSi00VX}#H(eJh%T%n13@ObONms<}`K@bF(oRzS04a5e zCon+2ZUP1Gkdq)gHI{#8_zv*zE`H^CDkS7hPXK7`h_@0 z1iG5)QPtR;2e;hT&#JcG@UN$ocM;h{&}}QDEQg~Y zkV`b_l90SWifL}<-}E*oKJkU^-?O?DKUZ5qKETrx#YvLLtBl?=bHV#(#H50KuVQ}N z8BuQR&>NpZ&c`yE#z<5A!AQ(cGY+*rA&su^U2jBVi(5Tb+Kk+1l%XkxTP_Ii>M)_) zz(;KT1YOoeL;HX3OP2=G4ifTv9MjPs?a4Sok42YG7;NY%Wyhd5NMiv)A!)QH<47Zf z-u`jR`XxG|>pe?_d$vaQYUdsnavkluYk zPCg}vQ@2LJlvXP*bfVgC6bA~0=z{*=mT` z4d%e*6_Mqm3I>bzHjx7tO-Kpe0AFur)Dy|_r7PLts!P7lA7}Z~fiMqqIcP4wxpa{- zOISL2sD*z+xiOS{wf8e4=pf@BjTSW`VG6#R;o?UuR01<;-O^~1RFc$Q+U9>C-DI!# z@Q;rplhe64>q?wQ8bNS4J6w)!Q`P)(I8ml#@i+mcY=fhfqfI%;U|Zfm4v{#lH0|oc zIPL1gIPL1gII;Dq=}sA=JCtb8OE`ze!f>CUC{2I05phBgrT`_)jW*i$zB9x`G~X|@ z*FX9ESnXYo$}%l^s>TZ=&Oxw2es*~?P7@%7?4Iby-GON@@6%D4hSJ&V7|g1kb*D_t zR`pi*Xo40AxL0&@P^rf0ITSv&AMrMx-Ggy9E%<^Sx@Ma)OMFtPOs5s%s8k)M-cbOp zm)d{q9Kv}l6m=$r&noYDat7ej3D4y|n}AOJ3U^7V?w7}(QB_ESr}|EmD&g7p$43X4 z`1)&WBzIKCM;*!Q^e_i>wfMfs)5`W%sgH}&D6!<64nBv|{hI9B@Vh4m=J(p^m&aew zmm2KDAJ`Zi9(+5*t(5Yr_z4>2&mO^jcG!PEvEUvG-1+5bf^Z1Nzt_n;NviMfI5Pg3y zhA6zm?(q2>zAJj)9ULAWe&xOc9ar6s(TvT5k%R86N4L!R%_T0BsKSVv z<>L$|<7xtih;RJW1zl%nKY@)9`d5G0jk$;?lj4_bj5dLb;??&E4ayl*T&@Dvm!uol z;k2Msr#l)@^cgF>V5-pdz~;PW{uQp5AupYnr;$wG$_`PDPGRF{bR!3rOhj>RdMqNnw}J=znji!IUnQXGx2ou8V&%n^vzi zjkI+n&aba0#pn$0*Q&r1O>95ChEWD{08mab90jba7RQ( zkevJFSEoaDbUeh4Gh*+-Sr30}PcBnV?OYYlq3Jv9W>MCRLF!N28g6BKHTE`maZi$h z&i-lt^k8?8L;uaDT}te!21<(j4{LG-aAP_0%ou|w_2JF=HbAG@1@xrHDZGv`#X$hl zx3*k#%YH$-oS{DglRZWOl?uP=P&*}B( z*Su>>(Pu;)H-XG5lKwb{$oUe<;p@(YbSq1#qJ0aDm;i#k3MQ)N)lt{HA=a=Yf0 zO^j3Fl)uJ0WU|e5pdqB)&r{CEI|B##IQ@-hrfXP;OMn#KP1+rleKZr@4XbeD8G!LF zRNzU=5`L4rvN=aA$>fRak9$s-&)oz^H4m%voLoJPLdp z?Sg6ns^Aa;PFxINf~B(qWwQzeu};bM^6sy=JDd6`s}_q-x0QcaA%Z{viEo(Mlza$> zsGuX52EDH0zuL&M!&#b{8<^$ER#evp^Bvu&R6)RO74T@t`;?OCxeskrEXh3`B6)+7 zxQF!fk(NKs<|q*ZpE*||vUowIqp;RQGZGO3-E&y}Bq>#G1RRtFTlJ@E@03 z{UxX7wrD5WE);*N+?XdfSaq$eZ)iND^q(#^I&x5x7!Z+6CpRP!qf-3W)iCYC(@0X0 zn$V95Ez15cqwEq7Jtdur68>Oi+SMpsyW;d9LiIp~+!1v;g-nDi6RLx+B0WEiad-8( zvTCZ-!6$V|f5kn~)Ek&)cc!vmreo15%hGYKtrhU^96NuAAT2E+p~Ut+1MYXhDo;kI zbzMJ{9Kx-W&}xN!ox;-MGJR5%PAA2JpWwq6U#Dm9bTwWbBcY8IKXDUhUyR}lGr^VR zBdd`T_C_S=hDgZ$fFN8at>G&tQLXumIN_mCba+3hQ_JH5sHF{lbZ7)vBUmR!b{?F1>*hMgR&d@-`{$5=Ph; z1dA=NXmVIWsTd@(DisK7fLot=f@CR%n}0Q}KeOpuvMbHr*y>{81M>uf1gSIU3*@*u zWpO&q?%B+`QJmd?6@t_+h(VQ2$mPcTXXg$dMznt_u*h=&07yW$zX7K0v=@9~_g{7N z;qj6!Tyv!&s@{Ie5B|@4uR3^b@1f@8R=xjrCztSjv^=@e@4fo832?`?pJDOPBCoFg zI1aR|KU%!|XnpAS_g?q=>G!1f_g?pxq4%NM^DfQ3SD$y7?zsMqPA*!swf1z0_fjqG zy4u-T#!1D0MYnQQ>9PdD6eTI4lEUt&@T@c5ImJyN4sds2E`$QU@`^5ztM7=zcZV`M zw7os-55GD!TiQpwgTCFvkHh2s{sHYe)8|)5Rl>t6AHpYa+ZXpO%{Dqa(#-REvYFW* zYgwvkVej`O#(`7Yq|DQFY=X!q6aauh2I@Ip@=z9k{O~gTTTXGq>jpGtf+6uGEK_(n zp$0?(DO`cFD?c+pH*PA&S%l5wsvXg1= zsMKDgx9B2-e92ym<*tj~V9|RkO1JYYy=+`iK|eZ5M-%b39*$^J6I8g----y;I#gJt zbWGrXl7eL&79*ryIeFa@#E=;Wra_Qy?)5e%)#!Gf$4xpOiI^b9Dw$nXSmNb@w>`v=~jsF>S7tX>~j^mWlg;GV1k)T>fE@_!I50;__c-wn_ zXEBpKQ*KD>!?mpsZi^S9RXL`dbi7DThZ;*tl7Q0BPmaHO5}ZFvc88|8 zE`O36IjPhiq{PGY3eLPu4FC@=YHjYMv*jeV<-~Lzhkg)}7r*fhgnGf^X;gNbY00#s z7L6oDQ8Bz>{ZydLD~XsAJqzVF^5?UEA{d=mRgYVN5Ng}WGH(m+g1d`hvk<1gq2LFR zgse(s^(&vdS$TjrkJ;-{n>!wP-OcS3Qs&Ngg<+O}7Hx1Y%{=la@h8UFyg?knAOBrM zOlu^k_JRs<;xF4pVBtA=lfW22#O6)b$Q&8uKuM`u+G))=X9SX(+yqzQ#{$afR|G6TLy(5|p< ztYG5iuoJH-|#{FrO!O(n5`FX7WMADZUuO*-hwXl1QWYfu?GsZKVb| z;??Q&Xi#vumu3&Bgwu-h)VI$F5DgD~b$$L9nx$WLHIx^bozJ;f2?)WU$!q^bMkA3A`epG;+5%ixEQn#)S*Gz_Y$F00+b_h8 z??3`;1~ABy6gSBG*~Ec=%`k-lrAyWjm^W2LiC-1^aNGs5W&GS{EGG^Xm-81HkIosV zjewda)lDxF_k>S@q31<*sm3XgI{T@IC4pKAHGLSw2I7+-<4JMtgO%xMBax%ZUcIQt zDpEd#(lRC4mDN+^lqC^#mBfDo2s-y^Qj*_zQU5zGot%u{)rL!d#8p8d<)%9X{cI#F z3^LbG-{ilWJFrR+LxK&JkCW#&+2#0~C*6v(9<1@8&sOo1O1XS$qCl)-u}U)B-5TyB z=hkKnWG@z>diqj=@10DwcQ`mW8Yag_pC0#5_D=`<2ej6gQ1&LWLDa*8&xTS|EmD6t z8Rcaa8m)=#0@de#_yhG0(W|h1Pxz1g_SgceTZh(!3BgMx97bu~f>zOPJ1#{|Gv-i~ z)-?0D2-!pA+G9)j?WKuPb!bfA^CC$)%M(|Vrv%3G; z$??%rx=#dJiGOne@&8wKA4iO3q`wCQPx^Kch>T-@b2;Lh5-w;2%qVhd)H#MK$2;%Y0bs;`?Kd=6Sb+{)fG71dwj6})#1U3998QrP=F@qEC!o* zrxgCykN-osjNG#R|uv9$uvyHU3t7UboTYPaY#kfd%y870hT)Htbo&TW}YTly5E?6kg43JQBoS>4mie4(D_m{NaL4Zb_*i72>^xlb|7g%e(4)uM!RP)vNcY&dqzTIel&u-P>Tl;hYr`5W?^5@366WOEH#+kUK z2*K}ZgyAQ1A@H!!!afGy1+xS(U6Ga1Gu3nnQoFVA=GczpwYE);$Zgtuvvr$qwr}&z z|6QA}wQY2CKQG$mmFqgrbIRSkjo-Ann=^)={J-qxtr{FX-V66~LT=IIo2{FCvwf3) z|NnY<%O=O+|5c0s%Uis1a!Bhr`xb$#2;1*gR7B$TDdKl2f`k=rP%O6ZpqHYn-FaBn zJFuEXy+F=b-aD)ND#VoTp0y$@Jz8@1N8vfygAssBXb@e-U0~!0T~WV>9up2|c$)`+W_q zOfO+)1FGvGNxu+8J4cWSzrGEEJ0RM6bc4at^?LMz^kcRe2*ulHI6Z{3J9y%yai>8* zn)Vt5B634O&Wbqq*9zDk-s+Dphhx?LA*WqpxYc_AQTN=EK5!yM=XoLdG{`uA5zKOf zSTg%mj07|agLstRN##2?nYqC8*Ka=9erMb<`X-X8k*x+Re&LEmp$7Zz{zoyHq-RsS zJ2#yQ@2>E;0+O)Q*`8({jXvc{V@n)CbL2JZz-%+6_M1)IHD=z??5c@}_oS%YdioP* zZj_Z=<3NuyHhxES0%9w!x|}zEp`lT|tsbhk)YeYVOWm+2-ntZjU!0h?E(7o~`jYZb6b z^^{Gu1AupBrfdheIc-=y;j#9#b7o%nL=)4|*QT1x^2O*neU<}x&K^DAdbsoO*_jK4 zm+xEXJCgGoCX0Sn=~SAiXqA2iLosG~7kE?<6)!|rt*k8Ks~s(}4}?zvLsX(HbC(r&-7GKMSJF|;aX##xE)%bFa1^M!w`HJ+@L8^jS@T?4U>j3#|3>D7KGpNM#TckGF=QmTXG!z}9ISNRI>G1IrocgVj)0JtVvVK@0TfJy!XgYw{EXg38Z%d49-N+{GvJdy zU^Vz>cOxV|$CFal@yt>7*~p>_K7N%qn?(Q8JoOr&0hx>~PsJH3pwx{I!T~%l#0)WY zhUFpy!X_)>tH4j8h@i)IuWU`ZHRFQdx!qj&2gS78pDs6JYt+r~-=$0WZTLsKHJHx)V`C!ZTn z^YIIRd=k$y00T;y&;m#yu;Zf>kMf09Hym%kw@YJ9vfG*N5CNHLg9M5{6iO)yr#`Up zxIda)z~~J6C=W04=_+JkWKQDz6xznbLLs+J(cBA>`i!eFHhMe=pI{CGoIV1U2Hu~J zi2uv3Y4R5o$v>mVR2`zeWdP4_<-uaDaPOLbG-Q?r2f8h!yS~`%`Vy{6XnHPP=lb*V z%|5eUnSX_v+&DLS*+5nc6E)>&idQqM6OynBC#1xo$|j>UNfNdt^P5M;3DW+?m_mNs z+dwl2s&N>amOO;1t&8yOXDw1l#sLa>DN<0Bk?OabspI|occE1wnGl2_Ur@j+a|Bd> zD!W4`i?$^-d6o7d`Oza0)}fBzH_VkGR$J65YQ%gFTC3HtfgL4PU%q^hi3!bd3 zb||bx)EC}h;QUeh%B^MPYhiaYVs%r00+?AS+@QFfi9lZ9pGBHoLZ_QIw$wge_e|f| zj7GY%0F}jy#+GF%8Qy}q4sC1Q%vmk;b|UY4oP4!aUo&_!5?SB;)noIga(dE+P;)P} zx~*2n`(#`3*Z58bPKGlP6|{s~7Jxi{4k-77XHh0GF5DPAX>)jb-51%Ieh_J2H@mNGyIm}^o*i9zA_wK2% zb^W`eYOHmJKUc6_TC}+B-2P$oCS8p=tKgkL;TDI)LqO0ASeSAyp=y$UNN7@!(69F( z6S)Qc{EOW$B3HqZ3AAc|nM+~WX0nZF7ZM$&S8XbzuF|D7w<(Jpij8u4sZyU|K8Yn#D%?;J~QCBhdfyRp$K8IQF#(Jt$O;fS= zH?@t_g0GinY};B*3ps;-=hL3)BtHYWh@TgvWcjpWI$tDp^XjA6uI#|~vZ4e_TDi)F zR^%X;4r2R4)1vz6bs3+wY>z0YTVAH%C^xTc0d|#ao#p@wfytGET+KKDLf~fk5je1aJlmDs%LIKRAZA=J zj0-QI9(u8rboIM28dBNC7Fk#$)nt8Tyz9Cs)@XVaHivQ6?abqJhy~kjJCd(nosEH7 z3vPFK;Dbiq1)CKb>;P$w=~ExCk>%~sqK8(1*|V2*PW zdw~sjPLZ^k&}A}zU4U|}Q4%k(trhRNWMN>`KGb@w)Dc)@gk|nGut>DUHaFRy==#D? zuFt2goe*Uw$zGJ1q~=1bL^VQQntz(=-`jXyC9f-Z9DFy~i}3u{^Vf<*Kw)QTDzmN* znRQ5?bJr1J2A7_5yE2D)SnjgWkdzy&3p`I!wDva9BTG$x63(_yV%@k&NWrpfbbv|x zkq}elTK=kPggo{i<1(SxX!nKJ3>C|d8|1qNj2CjA;2K}zR-$j#WJB?zhWPn3ou=Am zQ)O~J&tVOG`%|r`VL^`NM_XJM)>dTOu%0{=tJb4QWqgc!AR2IPAAS1FPI%HgI_e$l zKTFWK)(C-rqhkQ-h7%@?^D*wG3P3tTZF)?)aR2ly8)qeaYbSo=+?bqAOJfmzVYZ0s zG{vXYY2g^^a*LeOpQ6i0wKu|NJJ95$$VU+a-b20Vz>Q|yxHnAKIK;A;w_-Wy#niDB zMr=bnVVHPI=8d3Ij(ITUw7Qu7TR2>%tM;XS>_S@Oy1T*TMg=JqAlt&n?nV9RqoFm6E>dXT%o>iyJK9VP`rX zT$mKYC)GPxJJH%GZalOzj>s&P_q}eaG2yY(_h5*AHW|S~X&Pmw>MQJhJF&f9*vjtj z>0mm4N!x}@yYg@YrXK-D-`g|@erv(U!}ggi#Oo5^{!HYQs(%Xlr;eFKUr$zUP^LS@|@7pD&_}reE$6=}DfC(*8I)L=EWIpNMDEt*&Ae6s5-&r^RD=W)EZHt=DdCTB0Gcp~I|9v_OCK*vaB&w; zR5s#W0^LG#0;2b$k*`-?XIB;(B2#_C=p#z`Tn_&`JdUnQloMN*pzUJf^0`@GBwJR0 z7y1@9carBaIU#W?Odi2wK^|BRQ)NT?o#6k>gd6fOY^K-Dc36@l{Z%Yi1%ZA|>i}rZ zc6J7u8E7#=V*%V%LI?K=(!@SJO`$DBvP$hVZ)SXIW<+S=N{mkP)0|=V+E?KD`HXJ* zBU~=Yi?KLWS&Y9W(Pnq*-H-edRs7R`1T60sYK1)M7PtePX<^fZotDj?`aBPx$Z~v* zovwVy*#)x=C6-B*FXt9j(x&}1SeS)q&R;nXQ}k9xnzC)P*(;4FJI5b|tjN?*Tf=Uu zrds3Ngh`geXF|$^lTXOK{oe6hLdHUILY4w|VTloJZe^j-gxniv6YDWNkh7nEpBCbs$HCqg_#Ce*)Ds7DZTAINZa?`edc`I||Rr7Fiw;(sosULaMOXHsCS*;nAef zB~f_%K|aL%`dw9|!@02>BxZn)eZ>Dq5yS}mdX}uZF6?FPe=;3SvLiZwKCV`O?2f0> z+ub(CV!+f1vT&^;Pjy(EU;8W@KJG)~z59 zY8FEB3MpO+P@bcnd^-bw{P*JF&u@Y9*9&hjOfU~0-O+pI9bO}9Bk&6$4ASJl6^jz)= zJWiGNu3dweb6V^$=^)Pw_|XsFa@i=~yvm>(Zm`B%qeA2{e;EpY4whP9rR-@Lo%a5Q zmxPl*k|nLC%f>bXaaJgagId2twZM-3y3pYDORz9J$Bb)21=aB_%2O^QL7rrX;(~Z4 zhf{8b#I11~WrlJJvx99=QnKHvIky55d|f4z$JOL{D3-C*zm@?i9?MFdE_He(8$F6? zfU!8Kc6a{y3OlxcwT+s%2sgu=s223?wRz!yA1-~Hr zRbx>6qyW~uEPA4Rh@?i`LOZ=yCQ34PIkZE{V2hH5s)tZUqWq=$`;+S2#7{I4%?&Xo!TB`!vy>o5DkK4tDF5U>fLQ z$>X$toCLZg7P2pfg&w85-rRTH@#|V6)r~WpW*#o1Z%k(P-fxmDZj<$gCQn+2oF;8v zZitA_xs-qK(csvlT3B~$Zq+5f?!aTtX-9g>6-nxBpPn{2?by8wck_hEg4goTKQ~uz zh9dPkZz^9|DqFNU&vfma6zkDwi~ReXq0sKI~)8r`4eMH;&|=qecElm6gW^5l$v zoX;ksH!REamQ&yS4PI#%x(*sv|dSFZ9`Dos_zR%9BzzuRYZ- z>a6%+p{5No+r+r$Dg2Lk!Y~Y+IVS3n&nJ(~`ChmoFe!$rT^&rR>s7+8GK$2;QC;o5 zE%hLxsJ(YdMXl9C@t>ZRG{Q4R9^0gUr?-@Fl26fR(S;giXW67&?zFID;C?ny@X{q4 zNoYp^kq%&qr^u3s2MNPuuJU4sCy^zsTIPIRQ>;USx6`hMD^@+OTKdI)t+A)%N7d3V z#&8;3tC&iGKm%p4w`LtmArs8iTB;~|2~LB1!zM`H`~}+=`?a94fy~>Qe1XM>%1vB;Bf88*M zNxha@^HnV>7N^KY57BIzOvk?^*?5@4K*zy!tWgf)7RB>A_5qU{7zuMkzB+kw)aNB{ z#R|(YWKevTdhJD%@0^sN}MwxSjE2lJ|*WKZCuZ5s53lM3kX_p)q+<(o45w z=U{(^eB1+=(;|(m6U6lYhJjFO`6FR36(|j&C&I!XmpPWcr}Jjhd%biH(C_J74@3hx zkJ0M9HJK>wx~OrLCc(^rOu*`b6R_I7+-ODw=jAEtZgj?$eKh|Ih#NvuLpMjskMjNq zpq`IXasU*RNwC0|X>rnj^~_(oxL2nmg%l&V+h#N+`NyS0*`J`c8Zdlvr^4q8x`#6f zI(`T+`v{=!0O0ZL?f>K2&Ht@!{arLJy~)On?EHVQSAtQv`v(c^SSl_Q=%VS1 zW3Y3IO9n$7Q9~-E2{s+#dk9{- z3INUtu^B_gPmz$NrNu zLFO6M`gr;0c~7W+USNNJT=AGpP|PCN1;>jc`jZ#Ak+eiQtUOqsYfA}4j(;~m41{uE zWg)JzPHa}@AIQ{r+Gu^8=fC1m)|=1DWJ$N<`gyBX@V1sFL-hbAkr8B@)P+s=<@PN& zH;kHUcl3xiF^>-d&({Z?H|AbmK?#S~+$^exGSJbH27)@ti*YLBxJ(qyKy)5-r#t{|s!K zp2k?ygw7p*=TBKl;o!Qpm|4p3*e#})_ZrrG;fi=-$ zG;IzbgBL?=XQt{EmR8*Vp|-gA{Ohe$+}n?EZ6Yrc1*AMfMi+bSJ!KRbp;bF?&k8% zu&!c4ba_LphKF>QzHv}0%JU|3S!{opu9fJ>4SQCnsh<})Y7>dt!4Tv4T$c72NxyGzjoo0&^I0>6Hr9qoWm@xy~}YTxvR1zCFRz`mo%FQ_+1*&k3E+FisQ zsw;kMa5IDu<(Il}8X9JV_+Hxs;rPmb!2)RpK8@Z9gLJ|F#r@N7f+h!1`G}FiY2cf) zp``)YMJSXFmx&h;y^61~Pt8!jumDGL7w|tAk@@qpAS?0>bD&1`N>q{#F_NMsCLQI6 zImLNwL)UH9&902a!fj8ZCX9uiD)sDvhe3YbU~ zs!@;s%cS`2w=iVBRoN!c%@BnZ$H~uz!xSY9KO`%GeDHjB@H1)0rlPUT!f>!-4n~=9 zsMrZ%nw3>VEOQk!EjdcHj$lQNl`I@c^FFKM)N=T@vQp3!Bf8dn)DsGU5YHk5{Xx1v%8wMcAp%4N#VMGz6ZZEUM>kXtoQN3V-j!pm*pm*Z^a#| z-X&X(A4z_gZIX>V=5FZn{2?_CwyC_Bz*X<%TK3C0>@B9jUxUOxY@aX*{~BoA-p%_XdbRoQJo6i}pr<^+Q1# zKNx5E6qGlbV7yJ7VsN%&pz%V z@#AmVB(o#uX&pK7ps0+c$df+CzvDDy`n&)^T!Q#u+p;pXfWmDszbA#d1FX;m6vh`Q zCN=fwmn9rYgVZ$D-jaFlZJ_}~yf_#6X6#IvTwL}s#0?9K8F?({#( zM|qJP;*H6%=J{<^fQkhg7(sb-xVyVAa30Kr^QcJEu?^`bOh8zFj1mT;7g(>)g!SQQ zn%baF;Jd==tB-rf7Nj?3Li+0GY~p}=iV1?%!QuA)BMaKYna~b^FMqPZja80G2$0}0 zipSr4Mj)w$qbCQSS`BPSZ3Hac+IzfxXq9^)<%Xkvf#t9m{*Nj+Iy+KR3sae*bu2CErIJ3)Rz#_PxqA@_<)YD;Om@SI9bAt1ZoTe^ zO~&RZ-*yaK>Y+KkEbYy3Pz0d8KbL}%OeNhYnwGhf zjHpSGolDD|$4<~yaw&0!o^uLvw2JSCDRBJQ?jD{N^)Hg3HrptL>48rp?q{PwYc;fp zLaiM;0`O#yPL@7zR3&|pWYR;i0NfF*W!irgc(pIcbD7mtQH-ZX8i(Q1R#q{Vo+cN+$#>M*Bp1>y|;^*5qo zIqNh4SKGJ?=0fyB0-ibxX*vbfn(C}t9xv@~%I_o*WDwpaZL=N3>Aa+EO6jm9KT$g# zGB8trVfEvD(jUdE%fqL3Q0=p{7^GvOC%5j)op+QU5yk3THE@2PTQnlAFm997cAm|n zDR-O(<=opGnOABCG)aNvX~_ZLba*rNvLKk$P@Ugl+=vtW%g9|1;y(7Xl!3U=_ z>P9#~JQ%=nlqPyPj1pSC9IrHC7Sn)mLu;*nW!?ML^MW8@k7FQO=Z2%ni+CA{S=t!i#b&l3Dbf*vhH(3*63+NUG>XblbHfg7mQj;^ePNL!p z1$97FnCME*P_~Eh?hlEvN|hO|!A0*B3cC^4Z-o#dr| zD7x0kO4X#FzgjP!n{<^S*L0RMgYHUJ&ANS4oK#U4NSv4>n=vpFVsE-)ej||iQ%LO#OlKnh zlyufT*??M7WtHLtql=nvN0r+=Mv(4*0DWspelIQ5l|Xq}o0(J#wR%sjd)Ed;_QA~o zt8H=N!XrB&cuXFAkpkBeSqfL6&Um9FvnOiIv4#aBo+y)^b2wf~lc5}^>8am}Llo~{ zX8QHxM!OQt;7_3bjyYZe;^qXu)( zFq2x!Mx}TLx1Nv@coqbI6>163HbY7Z3k3@io{#=_=*e_5MpP!ReuWB)UbBE& zkop=FdNu6CTvB~>J;Lm6Q;KsgbYtRX!4-+HYqla)lXza8IsLz8QqJ0Lb2`uZlb;hX ze94P;c>d^Ad@Vd+bn;(;1C({(=j5U41SEiYj9cyg5{A&;*~&~mO$(!cU^d&}JLXt? zZxkNCNg+98h#ta~^H1MotEgJ0ax0L<^XEiRgYn(T)z+LLUzpodVw zTnZJ<*rr}aT14wJ)*SDDae{}Na)xX}X@r!e5ZSs=-(;Lzu&JB?)?m&|G&ZgUZ3lau z@ZpEheASJv+qSEE%r8$f*F+G_-td0=;_UoaH*n`{vgg8ESTkv?l!z!}H~O<^@oB%h zhPhiOCN&p*9{tR{FsR=!Hz5e;hJvp~DzV(#VKT$>E0N2yr<3J>v;oL}jr6xm)4(gohK zR;GWopu`!Aiw1xHc)fIsxtK&2KUv!w-319oYB;7A)L(=3Jd9&Pm89FlbwK=t`LGAzH3giT&Wj zueIgFF^WXP+J6RkJQKyEroMhuAs^AAGJ056zD(grTqYiWA9t+(rWfTi2Sf^QIdvNh zMOANINw<|jBs9U2yW5C)R#;vhZ`dP1&s-(T8c4~hR+(W|;7$(PP0@f2p@xrO17$HZ zoDl{xYa3tZlZJE-_5+{`#ejE#kI*QUtsz~sPA*V~%+Gn%_DRWErHQcr;uSKWxqWM? zCYu8C)<=zhQA?D;Jg!%6K>EB|2r_`sd)&23xp+!IoB@qt^dWJmNVHs^?;${?6oTr;rOz$b< z%9L?KiYd<2Eh(n=L~>;!xf#V2C+db3(|g*uGHu+DVu~|$^NQ&`eO#G7Zb~u5sajky zrK@s(_2}T^gD1OK)pnG?w~X-fw{&EpdsEWSIn3M%nwIf(Gv7g%=jk9DW&;{o_T}K3 z=sekV+$Bh>q0mgt6KJFtIL(S1&IMD*GASgntAsR@fm2yRwb|I ztvHr6#CefV3!%jW6ky@gze$)METhEX@2Vp~ovF`l-bJ7S}WK@nMhvMVSSO9;fcb;+S_VR>>vKZPMs#&M{Zo*MT(Vw55N z@tmS!x=3D&)@voLkEQ!$%+0KSJr=l$JC4{^{e-!`@3=$_6Oaup2$Rt-*wMy+Xw)a} zM{ACwpR;rN@ns5Y+bAt9myYTqQYFe(G6N-~z*>x^kjYMS>d-pcUV9UpJcI|T5N2#b z?H~8Yzw!@2z;Av8a_C)+#Cy)bd{%(4{M}xkOp2p;1-QV+Fh>oHEZ6Gnl3rwuDMA3J zmdl`**C2@%D0&7=EK(pd8qiYPGxf>i2=()= zhw3S5Xj+`Bx2a^IZ)!6+Uy9YN1hoy_OtVv;$J@`jpa*PFgoIu#Fr? z##el8Y2~c4x`9=b?r^;%hPXj~EIC$18&mj6QdEXQN?Iwrf`&t36bA->6A?mT9SICr zuTPj-Y`GA1Mwzs(xTFRyLF-r2`h|3c+2O|^szyw|M?da<346XDcXqdT$a;ub;5V(j z#THZM6Shrc5nz^H7^lUAHTjNwkuU%^dyU7d|qB=Wi|Fq53T`iW= zY(qj$NqXZdcXgOq;pe!d#d7D(c!ju*-83A3Tn=n9YYsUvW2b-!# z@)M^Im`#xogzi4zRj%|x7Hdz(80ss7Z7A(USmCueR)KKrkF@m6^rlvvyrK6gA_U&$ z{iLNY2TLY{)q*XrUoeRF)LH+cLURHg1r!N{srDPcSm;j}TmVXcLD>@YXWTV{?BCa* z@bg!UmoWv16X@4THsQd{Ns`FWlJ@6HF*X#9h$}Q34O3kM(zx^sqb7!>#DJa`<^-h7 zX*-zv7z)7PUD|tz{i`(PNPM`}z~qkcL?Mw9gMpyx6e=H{wV-vk7G}WmfJn`-z#UXs+1(p6}_!)+AHZGGqfU z0A=WS0_wqGZUC)b2wr01WYrJc^W(_XZCzx?Bf0V2iEa=b(nU8aQa^rX6BLPVPJ&Xc zy3L4IWm|Li9Re#@%p`lUy~oNCWi;D}iycZk29X^* zwd2sq+H5?3g8AI8fI|g}AkI4F;poBc>E0&?C-5XUtB3@xQ5nsUNik@tvp0hnQ7x5L z=-4cg`QDAl@csW0Lm-s*6xl}UHMGPPOGGhDtL1 zzrs}AQRuwlGXd>K);34(w)UCd+RF`1dfTWw37$!R@xu~c5W1 z7vkG3t9Mx;?VjtIZ^URF_ycxhG{%46nzW98)Wq`qe5(H5hB$nqVp>5eQbJPC-|n!9^+3eZGAOlDU}`~$jGqvht9P*qCX|=0o7A;-sml+^KAIGN z#T-9A%rOI!SDuo!?R3}K>RP0TwFr+|4(sM}Kz451{<|z8%Q|0gtZWr|OdA!#)Jyio z;nEdzi%PaI`&n&DZJ9M!y}R#8P2l5lD$875T?L04f$?4$&hS5#pt%~V|jX> znYdga2%C3-kklMv#lZXds=g7tp1!ba9vtp{_V7X4p;2C8ui#D7-II@xO+I(L7bE{r zRVfr`!(6z(U*^0MrczuDU&~Oav2LMxNsU6YY40Vk^85xl_!#`dYg`cVUHD>u{Mv$k zVBhH#rC_h7HCpu!Zfy1LY^i@_>=((gM@liiGJ$nn3a*=D)`N zS2%!TI%A}fE4Y)2+m7Pd8!Fs z8qP^PL17-pX!Uw7LzD+;iczE-@S_O;hZnXq$O%9`w8K^V0PR#qX^XXJvU4f|OuExN z;@N+Abf_*gPC7nZXw$p9-vl15`d)sN7Wn31YqxZJ6-V5ak54%Q0$(MJysM*m9(w|3 z;VWj}ISI^$l7(^7S~FLFz+M9`tEgT3-Mrz}H~O4gpf72LRghFO*Suw3)?6U--iy;8 zs*tyq4v>%`UZbBlYivLHGw!Em#QmhauKo1hz|$Yv&-T)c#2xy%JLcLLf!T(WEIvNZ;qcN2iQidi+wS`@}fN=K0RmKW?5=|7&{T7sX z(PGjAuShA$Kdm?m%Belji9Sm4>Beq+PY;WuBA@BNGAD7gUXcpEFO!19wa9xANI=^n zS_u=mh3pY=(s%xU*rCkvLTTYYt19rr3EI4EUqb9Iub#0ogPGmibSv$M?Zz6;>MVyF zUyC!${HU6ddfB&7H6SSE>b7ORrPZbuT!DE#fuRZfwhSK_8(*O*j#NaSP8q$_2@IMDoFujsk>>& zSu;ZVa9&)0HJoKydCG5f;GNv`sQcjn86l=yk>;h67H78Pg#z(e;GBTH9Cr`#MD4Hx z{O3v~1GgI^8i{|3icwzNSr=#qk&CzphXmBw)i%RE$>o(bYMTwm@$_Pa14T+x!Hqo0 zDXM*X1uYJGjqt(e$KRx_faBtNQr%!&Y&8V3Us5-JW1zxLy!ehtAqy%d)EXwMdCvo( z-$_fKW`~wOhK#m7?olzu@5Qv;(zoCAoQ-{L$Bat5vme1YB6q!^|HXu9Sliczfr7jr zCOZMQZF1h(TdFk-@f}-DcV5&6elW0wPI$#GGxr59X$p+Yfalbeu}gMkNiG|k;5s@` zE-|@(Z7es8EwhU;rmO5?)*@C-kr*otW6ThLC&QRdxlVbvWwBhwX$@vkf>Ef&2^ovWvt?0X6N>7owbc+-Uy><<2L3oAj~pThN6~# zFpUj7B6S-K+gaK5uRAtDbsDPMV}Y3w21kIR=B)@}s&M{&IoEE&#v92Xg zfw}FiWfrsDi_LeD4J_gV9oZ4~bk0B)(K*(pTZQAIHtR!3;F&m3`VG|NSIj-k>yPD-bMW4MohUmpK( zUfm|dVFOqBL=zLNqFE_!q1^OUtCI>S+|EbZX3BYQ9pAkp_1$wfTV<_L4ddOG#}|DG z9Xc@kM8UR~5znMXbP@$7!uE1e&j7Dr4RdWbyB_=!Gb*5_}zjr(KbDH4B}(~1Uh zllT46!%uB?$LPhUzsP*Zyk_lx(Ga#T>sgCKH%CG{dy6qm-+n##tIVM3eoV*z&0vwY z7TqGezc?I?4v&xIS!p@;8n8YFBaMyerkiAho#^Jh8TVVukiVc!3V!_ z{N$(g(1w2CFY43N+7$D1mQK4`)Qf5{D=vW{ikY`$Kv|3bQ}_we;F39iPUIQngNDy~ z)zb`MbJNcZGR}~v-l8RRNSte(SYZVkT9k)(=u+v7y5_W6`*f1&n`f zu*DZqWOFjiY%tHgXp3gtZfzdf53dlfOVU%37|%avEA#5R;Vr&4i#C6fMn5bNw$_j< z{Q(^TP^N92x8sz6y>@zEs&Qrx;KrV?94SoYPlc_ClNY2-tF$^R=1&TIx0icu^$qxo zgeJ?LSnm>G#IEt9!2n1=x4(@pPTim!@9kd{E;BJrPz82P#OfHVP3l8Lcy@cAleb18(Re*-9U(vjp^YgsFD z#iW{E;!@K_&+{BhA>ry`O1YC8VT!AO!kK}6qYe3_bd=Yrx&JVD7mM01W7Z&GaFiWHUq}1F}`l*?KAx4 zXHjDi5&DMz_GzLQf6(wn@kDxS=K#wbL1;(7jNl^W{=?4?PBK`3ZEhb_^aXt7diDWd zRdrn(3_7NKkv;-+6 z&1BdLBL&voTXg6_4vbgLYlLGbE%gIb?h-JYfci|5WkyR+f6YAchOyNM$uWqZX^p|u z=lC>iQM>W;K?TZZ=44UU<!{w?u8{|SeBznH)h4amoXaEP-9 z7|1ei3EaP!Ok@Ns&5_g#ssuhX1u1U0yP#_~3>vrx8!+V+QX-$u%{8}e?_9^V?@nywf=H4q6(Vo&J!%%B<*b=`#q_cSj-9c5 zL2RL;vl%c@4G(L;y}};RsCQZdse8cFtQuVNB(@XxCXz`ib`Yf7Vhz3L4%|-ah=5yH z>|njFe==vk9A7PddYpsUd0c=7wg8&}k>SGv6b0a+gGqT+POVUt;YH}$ySt1mUAiMj zio}>&Ylk{a=ygPrC;Pt^mp_-d>VKBk*X589uMwiwK>(xbGM`QsmpC~1n8h8(h33C6 zZBz(68)<4MYVI|n>*ABD8aFx|%rrO~n2_nsf9(ETm1B3PdQ`9!;Mvs{6*#Yw;J4^~qOWDD%zf2t;Y|9lmhj=A)DL^?kD_!!Jy%ju(t#O>Nv zyU|8lgL{;BAj~zC=}wpjPqNq)I!~bydzYnd(gHZk%%!}Y$or(w9ba?ma8ms?;X?6pi1mKq(J=JFgyjDhMn$MD^sEL~o| zOGC&j`jNaItQQY!>w}4n&~JjH`S`FKop$lSQyL6A{p~vfQj!k1R#rIVPa*%jM(ON{ z$xzvCQ&SK=Nz_yZAneDVe;ZJ#@2Bcuz;cgHx}Nn4vBm;{<$@t*Jn?TK&Gu!zIO{{| zOTA&QH(0i6@T~1EU3CWAc2)=1OEAhr&hHR4(qLZciUAfuH;>IhzzU#!*fSbAlh+HR z`Un#m9ExfBl?-jqJMm>Bp=Me?v$hizKvsejY5=EPRzm8S;vugrfAIwt#T;5FYdCOB z`wzfac_?y-wfvk;V(#ms%yly&RtCzgQ3-3xJv-;vAVOevgrTD(FPjg#Bp3Mkn%!k4 zH<@!1L!WYJK&R`E$KCoyd;wJP1R%z-m!l`+eS^Rv3(#d`Pfc-oJ6p)=xmy~YfjX1Z3y9oD}0kXQ-(dOuMPUd0*Z^p4+Ee;08^}hY56+ z=%EH{bBYYxzzaXo{3~8~;RV6=i3+$cAoMAhk?q3~{gqy+JA9 z$#`PDuQnLxQfwP9#2DFI2~@b-s0Z3e(INg*uUv}y|a>k)A5~*GY_rm5dF55 zFm;VDqDYFRs@OCAlA35#@anT+5O@{(B(J&k z`ToDW$B99H^Pkk^SYm3&;qU0-WJ!E#+C$g0f1M(#DeF7_JLA73UI+h*&-FOn6TUbY zCXeJ~dK-#6V0`>STT4k`&2`-$k$C#72A8n`yz@GJm8*-yh5tr0oN53BJ*`3k+*4?Q z%w<}0DXyc(qR5^ya0)3gz+OS^b1=%mXz4KF!0X7g7PEsDu9a#z2zLPKk@LMii&-H) zf1_86$-IP`04)Fa1)q`;L^BFX3--q zQcr{<>bnoSZHmT|pTHY6k6%?6)XIp4*&TP`hMalhr{X4CGQ{z~=t1I`)@f4FkMsjvCkBhHtTIF|$VP zelcOuw)^?#yyP@c>f{~jvetTd`E9c{crP5NY)^qwcO30;SO=+H%qeR!6r#;7d zE^WP~S$qYv#?&|8yr=25=v&cM-`38!mwu(o?+do8F5+sxM>3F&(6rYX|F}_a$_|UP};oPTvswmy!h(GCyB-GCrKxK|7C^Uv z8>cb$FSn(}f2t-!;az{H3ZWO+A!FxClB4tT4j-o#SGsvZsV z+NE0j;0*soeerzof^4O5vVdI1K#@E3nAEbUDy6(vO0lQ!z-Ig9S@Ss5T2Wl@Ro6Fc zqvJWNMlrctjCc@0T4L!Z2QYwRV+OB%h!tnoQ3L*()S0xZ_$F!%9K^jC3|NR!q`Ova zBcH9te=Kt$?kqv311RPph07*S=%}=Laf>Ud^T4sbx?BuN6=s06GQ`3bYCg%WQy@bWb zfI}9DK`ze6rI`)y(g)K@-5Yu(`8XTZe+~}2L(;S7kzm6suTRdhzqI&5lBl{V=6WWVd(iv=3Ns1WWqq?FpEGlid6 zH=b?3RNr;C;5+x~hkT~SVOl@B|H$why~wUwbarf|aXR)nK^B6iw!Q(>(gf9t1y zxjK?S4`y*U9Vxwih_w&J_O|MWidC#}f{LY5AuS{#X3(Pp+eZ z)qx)mnc^CTQyaBx>vfsW&TzFluO<`ls;Op~_6H477Z~BM0oD~oy6i-Y z{ZQ+ohQMKA9#@mXSTfXdD3)bT)1BJk6UCNFn%R0Ak-kWlxEIV#WtAA)lBg)Ep6Cz+ zHaagv?DYa&4bM?XtGUw_f6Q#t6dqqSCx!RKF<6;HRAK7EE1(-~Hkz{ebeKZwD!*ww zvK~Y1WE6vkgnnHR;2NuyDhtVsi$==XVj5Ay@=Y_nGQBpdX2|fOi!U(Uco$j#=@FpL z^wUrc$|AO$25M-(Y>G2fD@36Jq=2I5-#Awk6^M6bIeysGy?^bUe=`Tus zs2ZPJB-RdJFQYrHueBe49C}f2YMZQzTYJr+`3)}PnS107!VRrxrb#F78;YvC4tyY7 zK)vevHpHv0Z4`**_{t~?arn6nCC#$R!~{#B(-<+2{3GJo3KT{yxH#%NTb|vhiwb#L z-XfHzcfUhzRu0tFfAKo0Zu%eZ9?$Zke^Q*!B^U%TKqIc*c5f~Ttp>~HKud*<)teGz@(s^Xc<#nS}}!{#LW6`Xptv;{&-$a%DF`R zWA`w-I?=j;cL%0p-}s#wYTcp)I3#W>h{rX)^mGVgVEl;Gf3~hSiTOz#y}+&<6N@UH zW2=Hi3$V))`eyREClVGlEPF8TbZDQtopkvE#0>w{>}ah`g_2Xv`iR;|?#M z$v!BTgSt&0f~GviO485hV2@x0(4#3nm2WJvEC zn|t{jQ9$7ge^DvkZK62rZ4SjzscCpotG{rhl;cZ{OZ8ITLrAk|fa{yV691xge2ZkG zl8ko6yAmza{v3s2ef7_y0=3@v9NnBJ;u~+cKZ8n4fR%I#h}k`W7kB0rudfL}ZuUxn z>_a2u9arA~RL|&naJo6#)CkcV_3h8*iBHNgUXGYufBiYL1INAN^hoUH_^LvUtKh-W zL&NW`u~JHhPgjd+$$`jgq65X0pP7%g?zzs#aVSK#O9qb}s6V=6?>>`ANo-caw;SlJ z3;|NyzV&y|c02%GCn}dF;zs}BhuB}cRNClICRNx{8KSIy0Z;Thl_-PD$(UmN1+6(W z9YiRYe-Dz)^0>wAZc@hG+9eQRa=)K$8u>TbW6Zc)%Lsvb?C~H`l0!9K<#Yj8_=d#L zvZ3A#0ow!}4TVNc^oEKB^WEm_ zwj|yZSIb~K5$}28W7VMaT^H|RZ#4DkRd2KUqQ#zg z&&%MeMcSc5kslMduw5=;QBZtQ`yU1y|!QCy?XF(`8@UQc_d8rAYDrg^3 zzDVXxSsKz(e&7`yp7Eq0D0Q0mn*sfw0Ab*AemQ|2;LR5kbo0cKvGwWr2y~T#e;$E( zOT?_d5f{QmaRlLi99=$-VghninJl~IjGqnJ5w2s&{F~XN-{$nJoRoFBIh2IY7V{hv z;5Y$q$k?rHHFKfbN&(Oaq}11n3_M*?o5qM-_uOr&S6bO*7;JWK`)Z!~traGRozPpC z+PZz_bd$Jr(~mde%uFq>L6M_Ee~N%UZAxu=$oOppbKU*vo~AuI_!8{0`-h_kyQi=w zoPb00hvjVkSc3c>Ps#X8X}m6Tk0nlhW=Br%BP~4vW1wN3p$+YUJQeTew!(45A zbS%ch+TJR(7eQ}-B9TSnKA!tjHz0vHf{t2|>Js9AA70x-fBq7KW9S8+ z;+L|<4Z{naK|A4c%}X|ud6Pafi9w;-`GN-kJg-1P0TK0vDJnK7`Sr%v`PqFjrv+D45I6DeD9 zqN`^{ksrr}7yy|h4w6`0f8LY9Xbn!5Fb>80bJCFv@ora2^COAYv+hOw3BLk8?k!DiYSF4;>J3e^te-=YSlON&AN^Dg%h80GG&GALnX4qX$dq{9>Q&0_u`q)PGOn#0nx+?AW_;a zL19V^j?>o%DG7~d?+6-#Ts5u{xTf#6i2 zkD)o>Mec$ijkv)_96;;Q=#z)1`^R4$wGWjUtPS&Y_vG~9gJmGurqBW8Cx@rY;Nars zQY~!L@~JFUvDQs!OZQWX6J+h zwPULD^MyQ6Pq<+)NYc$ND|U>|jY>uG{$jtHeA3PC)~wTn8?~BRo|QW8I<^rlW6J8N zm2C4FTRmS~T-aM?5{|}<7aXDPHB06ZExq57!AU2ko@_> z_NXOQIZ`!r8|qaIuyJ8@Un~FDAb!4!24PI=3?>dnvvf6gly2Z&jU&PnTkPiQip1vV z+PNX|e+2bsb_u}H5B`k!1Limiuntq4uIm6A_Tq(f5-(ox42ZiX4XN2PQ{R--OCfyP zmdt8@0wo%*p$TlQ)|uV()2+I;Cv*H$eluL!!F11tGDeim<#{AChWuy*OOJ)A^Oy75 zjjK&V!FEZkt3%s+-gKpHt@pAmwmzf{sqHO)uHs5Hs`0RC4^qRT8OFCu;u_JO=R@Th+)#WzXdlX%;>=B0JeO}q*{G*Pa; zrUwh;>smc3aIZh$4zgjW9gcZ#84Sf;x6+oh+p>%1=irhZVFcEk4_H|zLkM=&gy=je zf25hX0i6}`lYKtNoL@DzJETF$V?NTt%I-OtZ*0#!Akg^Vx zE=K(|p<&8Wh>bOUE!c6~rF9{E0kQ0*(oj86CH6k#V zm}+)sB_p@WfIYSvOth&v*&5(7%%c8A3> z?zv0@n@i zrE*v3n5IBatxX<{f@==(NiKW^u}vdJ(jc=wQ2nV6yPi8hduxQPo)c>^k^lOzq*Mu) zub_Ush;XeiFWG8XDP9*_&e65Af5|}sO_82Bh5dBala{f()VdKy+vRk(^uk_5qlX5k zPYQHfb_S0V(&Lt7ORkXMs2lTI+!Dj-FntAVG~S$Hr?B=sD)1iowo2zawyC~2Z7g1>1zsfN#3#|!$v%a&@vhB-YWjw8C*Ntk5!C{`{H!1Y* zhJ73iN$AR?Tuwekc?IgB+1375t{P-(T8M7s?o1dWC3_p^$_=(b1%o zoc>@{U3!Pd?C`rl`B~jhl*xX99s2STH!Dm0I#99DipP6qE06-Zf0}x`C@qvOWut#? zy>rjJ7j4>ZIg$8ujSk%4Fp#cdOmD8LSz3xe`&p9=nI;`yxlPs55i#}3XlkjVGMzp8 zRtrME#f^he^}RjtrhiPHzQcP+Vfy}sHgoK@EDa6$NVtH z1>@UJ9Lt;9e+DFRNT}Ljps%QU+_uKa%%}R?3Sd+G!Uz6*BW{lG=K?~!s0Q~2v>vBV z%9`Jhkg>NYASIOH&|yGIAO#k+PaN4q!<48Cs~Qc4rkqP8I(~vlUVy=VQ0>plD_s5i zG7U0HcsfW8twir@o*Pv}kzLA~+W`Jk#Ho4ZK)K4Ue{ujc}wL5X3VhXZAPM0>+D zeCB2(n78?~p5GwzTc<)p%%{9&1TgHE%b(oSPpT()DQrknB&vW63(*4e+N+6dMeXyk zH5H&%f4SokjGoXvr?`i&;i|r^<7My_l#vXL zwxHAmr~ytdUS3`f?R`J^JIIMvCD`F#Ttlb21~#mjU3S|4VE}vwgC(^m!la?FK z7q2bLvMkH8EbYka;zXzTI<|E)r?sIQoXAok1=jD&Kaep%Tg`f38h z@r+*P`k1NaG#(?tDOb0)Fd1#&s*)16TwAp@+}!8`FWND{9L+ps4?7OHS%=Kve^`ek zT`KHTvH@@;e=bFhp;mPf?g0Qk2Y}#Ul!6Cpuwa-M?MrrM4izYiha?`XI0Qh83OC~> ze9B1)umKKi(wGmO#D=HdDDhZVpo@SUh*%?O@Y6+cc~fO}hA*bk+9m~oF;tC6<)4g` z$iu(%oi~r*`x}TPJ!UIQesNKpf8#qV!e5t^->EJtlU!BtiDHktX6E5>v$pSK+5{Bj z@IZ9pIG*K2Yu_fZs#I0m5giU;Wmm9UMcl|#;G}4hX08VMvq^_}Q>*ETM_DxP{Frhv zLo8~HsgMH}O#y+gu(Sq50v&7~Bwo=WFcky#bHwhds};)dP+k#`pf%OWe=$CHKH@!H zy%Mzx*@lJ*vY$;jzywAH0#sS7L$ir)30fmXy1lB(X3+8|`>tv7Ls1ux$C5A(k(v1$ zyhhBX2`eiQY~Jq75o|Nyttm9%SVpY-A-(fWBAmbyC38k5Y2leC zpt=z6f@%dkH_j@RQHX5{fB3_Rb*a2Xyi+xTr#2FHP#0yhoICAtj@~L9t?H<*LgIar zK8rRlnqZk|3b+nmIJ0VJY9JcWh~Rt)O> zTz?}<6#rANmd!+fr15DQCTEt3zrKIO|5`d z6Ox1g8^2!W%2q`s5mgFAhihtZE>8PtBk9ozMuYPEdL8%lEjrNyOR-&tG0i|qU2_zG>DIpmrSx{3!mA=qA6j4l2gF-2^&ky&Ssx7QV5eNs&?)2c{`gR zSEBfJPjKgDe>Ns*6Qf2(bT5N8KwQUsAnO=o+hf3tSWxXYO==X?S$LQU!W zG`<&?wnq2TjOa~dFQ-sKYcT0VL1u$hYc)EPOC9Q6oWs1}Vf2Muzf#coW=Fp&Q2j(+ zVhdu2=}k&zXbW>zzHC(x;nQ~zBh*4K%6YU%MZ2cCw!J_PygIn^z=B}Rfp7DTO@$is z6D33ne+4f_7Y`v7EBoWU(}&=$FfPu^YdZhtlgkY4g&J>vI)$}D^GYzevq$e7Jo<1C zB53UrLrE;{cMi;ldz-Ipy?wlQ^vUM?yy3nJI>w~VF>I(JU_IkL!GnqqWSz>^=_zes zq~SI{WL1H0R`r-@-y5S8v_&TonfARjP*D7Mq zE-wO$ft<3i7KoOFk5UsyZ}n0f0VSRdsOu}#gu$T>%ToI!FMI*8E~o?N7CK% zogx`$tz2~5kiIiOD)icWIH0=*6<`6&B|xLLB(Z6#LHRrV7^%DL)d zYlCkImWWJnF%oRQU<6i0!77dcaJ8(p(EcR1wnQ--sDFT5tNQn9(~r2p|EVZzQ;q4p42xiUWc#E%iESv zhMVEW9$b-8#B}=Z98NvsmL3`{e*(1Nd1GgY87hd03-rHKvrBcN>TKMDe|y7LxM``K z+8b?Lw10uV*Ic+QQv3!G620lt>`kX2n0co5;cVWn7d9QFdljv10Ee#N{k8IjrM ztBnHqe)UEjL5jl$4oCb^Dy7@UKSIg$k9%75ze4)ILi)c#`oBWD3JU)#f299Cg>-Ik zZP#<*DB>XG`z{U))^&7wE(8R8xg|Fx@TdHz^PEVI)^^{g=y5hiof&&{Jz}6cmx@DB z#%!u(QK7G3;y`1;{ICPPG()^7!4Ueio#*I+9uI^$+X)+$`%iJ3a8UIVKosw%-iFnh-&w8XV z@K;+ODutJqxHKn9^CVfX%EsM<+tsb~b#9;3lM8?%8^EQLx^brXe@TGX>tKhD;9y_$J^vr_+ z_TLYWgz`CFxr1P(1qZvf1J$4lC<%zuuG__h%W7ClD|u-h&o8)>qGtmysOCel{I&-s z(KE$akT-{i;=_<@+q&lgT4?qnB}07)YuAv)9*QE?QuMm?f2~aF4~tJ?V^IfpyH!SJ z5Aj9!-+C#nmfl_PjLU%w#8w68r)&JpZxv$g7`g?4Q4a&ZCaS2=P@OmV_aO0QMFv14 zx5{BFjt7&`av0)8IG@0wb2_R0m{p^?!`_npP!=N}8W!j5sF-Qjc~*^!oE&e|Lr*`K z6LnQ>AJ}m=e=2Tj8CGR~O$;H1f8q@$7@=|i=_}G_?YzGzs{-aI8#@SHcpv0U&L+C4 ztEi7gV|>#2hu9#EBhgvRqVYwCzzk$rrDt}x0AUcUrjLm+Q0BfgFfR`bFoe|EHLqZu zcR%SX@Xs1>4@e<=iM9?_gdZy;Fy6ZP!F2 zg^fN8|ZU5 z{V}JLs32SCs;~q`PkpKxKkZ|0<9oTxI;9(te?87+=-+(E$#e+{qw$kBA5`ttX%d1> z*ZCxR9U&F;RJg?MahgrO!#kpBSx#`%`ynIC#`_?98#%g99XEvEe#!B+-#9z*U~B|$ zetfbMDt_QS#T%>AH|aNoZ9d8`rW=E}{D}6=4P>(T<1u<7Zlq={ubUwr_!#nb6hC~i zf4h8^j+;hXg{FMux-V}%$F)?0?0K6~pU@UW7(Sm%g|S69-+o}u)P`bDaJih|5DcYa zWC6ysU3fZk_p`H;M?y5PV#bu*PSO5Bs_>;$;mfJQS2nCfN&OX9e>DZ;wN&Bjslqo> zh1*-p;jq1(gz_L+`VwT=Op^E!GP9>Tf28?ckG;@^&9%8B*Hk^^Hw-TOva49=Wl`8^ ztbU+U_mZd|X+(K-EkKnk{q@MwYF~gW*7=j^8mo0N$n>tVoEnz^5b12`TDz?Pyrv=M zs`0IC5ojaE5?Yq%S`=GFZtDQ7XL+QJ(|=lbS<>4#stS!pdiKC#aj0id7g+_ie@xbT zCU`e6Mza|i%BLWp#hv6jTY7Ay%?HX7FZAe?pNAf1UuWA}mN?z{L zD+HnxsTtBc@0a<67%B5#lt|nIe|_*Eun0~qHhs2UtEGOZ7dbm>=Dt+oSaAAwNxC5Y z$vbe%M6%5S8^EH)KrR1DHtFGWtkxNiM|nhN=5pmz1@K2HKGe`aInVx3=X zniFK(cgv7z_c<@W0;a0$Nq z8n+jV*KKG54N1K2q~DwNV!5l2Q+r*-YTypzOH+mw9D38($n|tsOWYb5##~RoW9B_~ zNy_9c&q!^QQItB8$fJlyf8qNfNLuO#H1Vmyk{&c=k+*d8Bw=h&7~dIN*(TAnlu(8AT92wx(;M6{=mRbF z0y@KwGm5k4TucYYT-g`1Y4q5z){On#tnu!V-cm(Uh?DtH027kZ11Rfn!YhQOP-%uZ0!Ud$9oHCCjIMthVu|RQA)2#3M{932913|nXIv! zZ0I%7D3WM6yt4i^QQCUR{m~_BJ;7Z%b$u&+R>)xAh5_{5(i85C93(nk)Vi!O?gXkS zZNVq_MQAUKC(+kHe>H4vQr+$tX0^e#jzT8Dtuw$c5MU?KBzoh!e>OtU(=z1PrWurV=#^M<*F6IpHbG-HSi@u19K4&d9uG*B};d1}9 z@sbx>eKBymf0aVjq?10BU%4HBGxW*Yp#lAJQ`>MvUw{10FO={$8G_C{$`<$MZ$KJ1 zy@&_ktzEaOhI0-`u7yf({Y7|0xkBI%?mQir^^9KVEo=t4k!)U&xT5qZi(;jim4AU!nxxrl6O&>D~7%=hZJ_JGCmOR z&%2v=e^cuV6~0leFS?kVH*sS}oiH8kD-rbAHEEvJc@4hQGH8jR>PFExdie0V6VMM~hHRl(jlgK|9zWz5ChCn$Nnf8 zTW#iuMh7AwAwab%f|d_G5@rHQ>nk+lwzTOqxE0fY1mQx)_fdxy?czlo1Ssy>MIiwR ze+?fFiOFJazZ{qFxW>Sd!`gM27Xra!0;R9#%YNCV2R`k{iW+HvG+pgLp#<1XiOF z>IqK#f5V)Ey!`(Tt8bab-%0mf+@<6#vM&iWbRyMk zEaXq%=3BOafxZ4GYm$mMufYdUU*(x;snG@F_vHy6m@1(`3}~;qyg|2y+VuUTscVfQ z@v$|Hfv~6T1)U?VZ>Ceezc%Z~>!vkM|Bt>C0lzt~hqi;%8s;x+&g=wPEPW#af9L(s z9F~0xz>cCh5_RDW`m>h{i#Ot~bk;Y+k`MA={?57nv3)A2?t@fsXT9sL8fZF`V(jWN z45apgq)yQ(60;}yew4NPwKxw%ia0|H-5VB@n_?;t*sw|^zK!I1d!APmoxZns*yoNl zIZQMbld3YJT zsVt&6KLK88B&b^-D2oz{fwbCUpHs{+W{D|2rq69oIr#kow0v}yxt>_=?TfUEW`@JY z6Gu^ZL<9vJOw+6C-7BuZ1kQgR_R7|CVTT?i`Bodl$EtI;bo25nO%Fuef7kT)(0ETm z3BvgN1{N{Lsbvp*$!Q?&tRIy>vN!RLF@*o~2E1p?Cfq2#VqP?Gt7vwySHIN<;36r_ zoK`Qmtq?b9^bVmf-gVO-OMa$XYq);BRFm<9j_r6zSopZ-9vk$lWKqHE;oBUAh zJzaEz#QO7(CkyJ{HaXw*f2$-M2TCQH$XC^xR0wyyat^PVI}9jsa!ng!B+#&WDb&)P zV}yzP6!}e91@9D#N-n&RrGehMR|_OQWll?Yvp$MXj&XQQXz+9^7S=Z-N2 zjbEkN{6<*U+R=<*f0SZ@)NYd3Xi^z4e2oeUIg1QqadcJI6c=m+mx1J%rjN%0<&&S@ zWK)%Rwu_>Ys4D1W<9nZLBIZ#AVvIGF3jkQn;12*)UloZv9|kWE;@R*9$uC$7edjEx z4uypjGtXr@pUP)>n5=b&sr(d7YtNZm%y^8fV!-6GT1dBLe~7P{p21un<^!NT{=(Hq zWVCMAHVdqhtV9ZrOM0cI3nQr~KLn&Ie)dUVOZ*)@^wxLm?Jw$*;14Gg7d$|mECv4K zho{iR;s^L_jO;zMQH%O|+u)`l)t8?7u+YlepP7r&#@w{{RN;bp$H$!cIH(oN8_oC9 zYbJEyPQ$<@f7)Ob`|grpd2?r(BTY%kk{>&V6p%xbaHA?KpCxxWjc2>xr2OBtZY9(} zZ>QaERkZOLoONpVmLzr86|)1hkIOh{I7M zpl7y2jY@>0kf`wxW{|t(U#TiHugWY{B^ysUA;w*s zMWfJ$d>!a6y}+z%yl=bU+o#_WV^Em7wquyyH;6y67$z#h6kduYlq5>j3cwsRMsad` z#wQ7Ff9S#oil;X=^$j!5UkNy0aYxa7bZV1g_)^Hd%x9>vl{-FtystUy)j0*y z(*Y5l;T(06ZYv#?$_*Bo?_x?Vr@E@QAFI0mQGMxy-GAXU?1Z=*`w|$W+ zpIn5vM*D3z^%BgOb-dI~r){$j%Rj-B9rz zeQ4EJO=~Ea#x-E|=Q?wIs}ub!Qc z^S3TqNMb)fbjv&Wk+vzW=KPX>SD}UX5|85B1cI zAjNf^o48l?S5sCaZ&BGC0c^@XJ9H;RaOz+bwJ8@OMiQX z+yyoie@`A>fw6Jtg;VL#4)`cwHe;0gVOXPcOz{W||u z*7g{CocYpO;umU;ws@95%3P_`QOgRaHSH9lGbKk*?vAP5Pfcmql$>rhxBs%q4AAK| zq0t!4DgLSp{>dfKE*nUq$Z6xO#y4P=Z;wBNGo zj!;%rCfnb7ahI*aF=W}rGu?rGBe}H71Fpy-v+XEPsfRDbaq$sC4JfKBMqEC(-H?hx zg-#09uAz)3n8JUOtmdj>89qD118h3rNu6ia`4gKR?U~Ih2;@g8h(d;9kbjr}K%^B{ z7KZRTIkfz!7?X!@DdCT0=j-GzVId>t_-A&F;G+v8-*R@L2Haum?LK1>G3g`WQko!S zRf|MVu(>Oz3K;c^lp-WP{#kC~>0s3lc#H5lp<(Jt1dbu+PkM3NfmwfM`26NNc!Ab8 zk0~U{<^%6A;=$|d<1e>dFn{n+z+-aB;vxp(#>ZHvkKk7C*1{jUR)zh!P~Q&lf7@N` zkCjI)dzBaW#`ROwH8sJVt^4oca^lCoBZoVO+D;rkeEu0=aAsS#v9hfP(XGO(Vp~=P z@!jg@?hb5v>D-V-d;K}upUyS$LotD;J`O*(3#&hT7T3IKoHt87FKHOcYSB*Zsq^G-V=|GKv8Yr_W2_-0k1aH&hORU?mjHKVTZPncZik)IeGl(%p3my$iQqbKf$)#a zZoKbdjd0IPi3~X!N`FXvo>MPJ)lHo+0{92Ejz_~ZN#jr!v60#0&OcOWV20e@Qt_5tW+S+RS zHtL4q?s4r(g2DM3poVXUri6e zV^r7cy@@^CI*T7mtij^3YkrJ3rn)w4a zjR8L;p*a^_On);!;eL*7ynr?w$wuDPoJ+p@y^lUJ1Sq4Yl=_YoKyZ_eL2ni&eDV&M zZ)@`;KeZ(JxJQm^2`-*=f5JJVuR*zbox=ad^>CEuun4YaJ&jH=l-l)Bgm&EfH%%?& z(hvuI&*~&K-}tyxRw2?#QmD8Wb$N+D$u<`clF8>vO@H2B(?T!}I}KBWcmkw+S{9u61Bag1N2DN^V+x|;U}jD$EK{7ZoPMS{K-dqA0N<2V^m#_ zun=GPe`l{mft&WMnX^xIzmoPIe>v$nvuaght?A`r8MAZEsy zao0*NEx=H|galh#3#1rDRSUC7iiZOr)6utmseg+=^(5=`QrkaU-PLu>m0>>MfmxiB zwY`q!gPosm)qQi>TGcMFC0w*ykGU=sf=$~afOw{z@myzZT(wZW18q1FM9=MIb0N9x z6T*d_(bI+N4Ql`jAPt@jnUQ;ors8>UI9NDe{chOJ7GgL1L$Q+(V9>>clE{pw#q?&B z<9`HFx~&q_Mc#!lvO%{&H7_u=7iGGrcKS)CI%>CfMXKr)|Kqr8FPv<4*zO8;)(xyU zZnt{o(17MiQO<)VljSGeWm&kYH`w4$K7xT&RM=2ea)#Z!$>wiUT ziu8R15~wPEdS<`8x`JPoEv_xlpEjW1pUl_iWo^C&K{(Ao3!0g)>tbSme{E~dY)$(yPD7laoM3Q-cUhr9hif2LAZjv|7qZg2Jb zUaBjOnu-l3NEr^}RVc|AdYBD%GF5*bFPZ69uW#PeqP@p8 zl)ACcM!-o;`oV0(R(%aef>BJS8eGH583~pRlZAgI>46dYxIqCB%zqe;dFWh*xBJ`q~r?BytxTrgbp@yQa-YAIq-ju>x+k;wk!o<4W3GZ6y&w;?)#KrW)l_^n!tlPJaf8S)Mtq_ysN^$y3V{ zg&*^pgGUdjST=(9HKyz(Nc_Cw2=rox9}c|?4z#22qRl7uO=Tn59_O319HD;>1zyy+ z0kK7YoK2t!Au3siV-YZgxu)7$jUc7gyk^Sz3)j2qA(2-{r`4|QlS|PKaD~vUXgPfz+HKkxyiCDa;$=A=T+rH2~U$|Vg0_Y z8ld|T{_7{I!gA+{--3rRUEgn{hP<<`xq{%&=_eOazkjzLJlnh%R>MlG?FaP}n@wqC z^K=!J$tiJGZA7fkS+_*csXj4$`Z($Z_?9#jX*uW!ZAC@7zpzi{+M;{ceP{w z;sqZX{(mH`MRqeSF~T1HoSDZnd8d?2<#LF}dq1`Dk7e~;Udf7=jaHpT>$xeYQYOU{ z=`(n#wXR>*2g`bK+TvA>jp_snRQMFEN+amd6q0E#tI#L{QcJ9^aD`ic}9>*D=G4lioP;(rtSi9+1Lcsqn4Z0yIU2P*cUXk6l0a9q%X zfMSPmVmNMy6ev(noaL2ZH(*2EXNT}|=e){Mq++Dag0wMKB(q|IeVY8I{QPFh2AT>T z%{Wuz{wHUK_BGtR;5(Z8N1!Q`7WDZzt8I+Wu}p_%VhScE9suD{PK4&VZJFLVXMd@H zZt;q221A;FLrdT4oOZg79)5ao1les-upLq|tbF($h_@c70@nSBjwaBE2COX@a|Q-p zhp>L+3qG#}M%%~lHUi_id|Mwp{nV|1+b4?FN=JSK#m}aYYEOVQ#^Mdl!$xj6&y`7C z6`Jq$f2sNDHf(6Ozw)2W5!B|5C4Z-Lc*a%bhM@V>L(lcPPvzPrF9JTRPjbx#r}tWq zXC7@>KlO2AM?%8nNM46)&PoE#vnT}NO3Dj@4ej}ig5?N_K87X(o@L*$ThL0Rsr;xT z&+$b|sZlX_&0bfe-QuF)E|2WAJUTmh^a*U|C8jyuZ7|@>f4`WGvlDZgO@F>KwvPD- z)RB!rTz*uHM&ms7xN|XH$HeY`2U2qC0%jUXc}22}r=(uUlE|=Z;Em1MI?Qx8GRi0AkLbod<6fu(6{Z;i)X^yL zUv4Al$N2?DP^eiCCvtF*pNfQkB$h5q{LD*RGdGqM>ViYw*4r}X+kc~al=OZLmnC!P z-e^ST{vIBQ#-kyh&`%iRqjwIEPd^xVP^{@*AskGQKM6?Pa~0?)w`F@?PD=7f!}rn{ z*4WY$qCx?*#wHeW*r%>Teeh57BT4&Qk6t0JeCs9hkWvxIl2AwGcNW_Zws=KH{Vbd5 zlpuAYPATOdt%0;4~6ObyB-BjZd^Fwn;_JG=GqF2vnnWMx|<>(_ZO8 zp-M1JU8cXB{AKaZ#BYw|j#OR}7H~>VV(Y@i6~kv5#iZNSW#a^}7KrYzQhma~E~Wy0 zAQ(#Xq~Cy^F!>L$?JFh5>9QZzD8gwW1o!572XL7<2}NMJ?j4{q!tFu;8qp4g&3s`% z^xzw4sKA*;`F|AtcM=IY=&pjHZ5`?|M3|jd;oVo!{j8zID~K-tmsme!7+Jhl?$0PJ z)9M(7%^vcN2Z29I!emxar3k(gWm3%2YWI!jW?({HH}(bP0y!`75z`Ic7g6givlU!% zaF^xd>@k=XrszQ2i#j&XgU>iRcfJ?`bXj%-TNs5UkAL;TE_lj>Z3TH{GH*Co6mu~5 zuIwvg(U!^ZGBIe~#N?B1U{cb)d1E?6rhBgoiX$ea zg#u>WC2yLXyS0;e@C*PJ{^ey}*&KEpc46N_;p0X?I%=tA-53c{H1CE04!pf{S@ViS zEK{UaEPqd8fswkT;D@1{wwx@I`J@KM$C!s1rV6M?PXnGLUF>A<$kfF&pKs_?tJw(% zcrM6pVBe~O@ULPgfi-e-<<;w*mwofz7)=+}dSrS><*2wQ>|J0!sR{%^h%{m=L$>M9 zx6wu4NGzv5ypz>=s@x7W^=W@xv;aHJ>hGXj<$pC$=V0&Mz&gl<{1Z&yFhjLLG@ik& z{OPCgW6ElXns|*W3-4Y#8AawEcHS- z-=r^sKtH+}pd-37Jp)^`e1t#CNLVqjH??xPQBJsdh`nQ-rqTJ){t1bM4S0Wl{}W!c z@6wFswS1x9Bj$zU8yI*tL7c!|HU+&aqdqBW8aL0T)9m~^rU;chIzD>@vh|U`HGjKQ z+*pB+&j8%!tLDR-#~TCvDb;P+(c$KUSJ}8LMSIkfdB3cR4OW^&RB8t?&F^H~O+Z^y zYi{s8ZPFFJNvn#^uSJ34pN2as^|mtl{#8(dTW3(D+o#=t6_jl73P&L$q^OYh;Iw7( zXK3y`5XoY>nn*kcCCg>D{aweHmQ z@9Z6Ga~n1AGyg&a7;HF9Qy#-)fSEwj5FQPa1Q?!~OMGsRI=rqu`3V1L-(K27F`3Q(Ao(3YvDsZ3EtP5HYAU%c^NV{KMwD|5fx z;!Q?UN(Bf#K1*s~cNqy}r+Gl$e5mhpcjoLLB8Zx1Xv)%L=Cs`%N$g1AoOpiP&!j!}JYP z)nzYL5v}!PmF4BeX`*6n?bT$6N?z#rRMFs+1FGw;(bLl2dmSX%Q$W)b-G##DqfuksQLS1RzTtTE5QkbM#?7}ZNF0LoBZ|0{0{X*zOb5S@;+ zeZ<*iZ6{R*Q>m)z!hhtP{7^jG z+1nUuM|-Qp@oLi)^`t1xf6lx>`Qmx0I?Jx_T{!AzMrC_e;(WNK)_@i;2`{fmQ(!@g zzt7bzf(cFG!teJ@q$H@Bq!3#rJgC-2g6oA_lU*;|KnlU1<$qZx6=L4qgI%x_Ab2kT zk-c^AUiRu<3FqrDfwKFsh-Q6Dr|Fip#|saullj%v6lh!wd5N-3D@mxs_tNTRHm|$z z3Kz#hv*747&4s9&v;rD_L!7$pUOALby-*GZ7qjVj2aGg+1;4j@IN)Cc_Ou*8k=@T> zS$KAGx_i2xMSuC=9px5)=%M@j50?!@-TJ;9G6}(4DGSjx~{U7^!GPjl%8Yj6u`H((ZR)-l6RI!KgX*+j5 zYpC}SEBQ$uvmeJcn1`?vIW2g>$_7qQ6dKU;0AWCB>3{kK`K9w~pq%HATV`&V6NwP9 zr$R6}GT^#w-AD}*r0yeiEN~pch%g~J+MD^uG~t1r67re{(^~Y=JX$GXNWip_77nAnVVzOFTbe9#KL`ActXcQDpa^l8fqSK5`PuR8d_3o$Z zq-nPDWPfev*nt1n0~olvwtTIb&csI`Bu!C+hx80G*h6|m$05AQ!aFU$zqEhy@zL?= zwR4K_LGVW&#fnO^FJO(Z$qNccIKSX69c>!%s3tA~xG&5*MS?D)w4YzN^=OdnV@`4v zUyf^XSQ1p@LQa)%U#gNawdwIUoNCQY={3E)mVY=g54P{Ihqm-DfWrIEzvj~gtk40c zq0sljUWM}`oDu{&QL1NxmVm;&ZN~eP0^%!GSF^(Ij_;OJ@Uop0vXlF`yy6TNh-OAx zvEq7xdurwq<_|lt7ej0)Gv{;KFkcX@uJ|O5tio!F835_gV|F8}F3%^=iPdn#;AP{G|02shTmX_lD zY4Tt5a!h;mLc^UUCJ*6Q@e%Jr!oEK@{D%LB*@?Pf*=VAbWcE5B4V>M-_S)8`RXKU% zD?ZnLP$6@pFWGOvNH}0yJFmS)qg=RvM}IgpBPxuHh86PjYOd9mEC=^h{jvkM?FVCE z5^6%uQkj3H|2V_O7~m^JpoUd2v}r~NKu$06a!Q_X=%iCky@pOMt0$W^gcVcJ@kvd- zUr%M+yY)JbBYhx%9%O->v1M;%vQMh<&|Fn@8IA7k4Djx3$zh0nSr~6p?`sp`g{wXF?U5#h)?Z}WI&c`!l2qvCxEM`r6f|Q z=2o(MTQpzguRdw|S_A8Q$)pWU| z1i;b51BPn^JiGkPqFVBPCg=uMn4a%$HPs7m;0qBAsM`sgA$0*(`;g@}<$q}!qo-6z z!+1A_p=mJ;g2O}Bf|iixTqSVSSg7}^my<+`uEHh)Mp$fhezN9hYy6T6A}+T{A)?$3bYDCgPzjMk z(poNe9<^DoJ*-4FpluS$30gVRLM5mgY-B)64WbA-{hfl-=HvY7lcmeX4N+rKu(7H} zIus|X5@F(Qq#!UJ&%q1@|J}KGkG4+FKu?k{A6r)j8{jEenzo@#=Hp2wgH!P=_*0u&r z>Bo~2IZMYrYG1A{+kbnOvS7q>Q{bFD9-Rl2KZhxSjlv#Gx^KFGnjnAYa@7a0fUkg3 zU)2R$(>0zk<)aY^%b)X0*mI~?z@J0CD-?;)40~)9Y^_*mE*LbLmyQx(cUQ&yS1K|s z?EU7zW@Lx7Vp3c#%*f@6X<`tAvhrB}s6?WAU=J!bkND_-Fn{FQ+Q!Opk}iNzN-pBs z7kBMQ3SAsL*BhIzVPB&-;GtvRGSFUP5e!mlge6+q@*+GKj$q?I1O7B$&;u!iyRK`< z#f9UJ0dyd0J^>2^It*Hf_CL^yTq0?P3pl*C(R-hxjEEniXjk3%8Gy6awT162u3igD z?1*clh00k1mI)43Xg}^HylegCH5yZt$o{V{ef;?$xHw&BB8foMv3o8CFDOPtDA-|( z2vJI;-@F?sbejt>k)c4wqA4l69Im&LZiwKhqRPTo_J6Dvd|cIbOGOl`lFOwQ+S0t! zusd_f#ymZ%qE!SVM=5M~43aQWY3KV;6WKD9;ueuNY&n~KLN#|ebVqypwg_A{DM+6Hp^ISr z%gMm4El;1ZCRpBmquS+Ac=RmDmNi@e>%6Kkgpu)t48-gqx2r7NDgzN`g~0Nv=mUg< z{(tYMK;#SS4FY0gNaQTdiQ;0${SRri_482-eg13n&CJ|Q*6yD~#r6lTXLSJ)>3GsDJ& zn6k_-#$!M($<-B4O?CN56OqFN9b+CGeSiM>(Kqxu4kvtZcya<1ft34{a!vndF5qK* zLCx7o5N?2?ArETH0YkAHA7&^oKw^?x21j4+KRkPIc)YLWqcMfbZ8&1Ei#$Pq$I8^}yw?jFPKHUz5;XhuR@n13(v zDhs-ad6|2%r$*WG8AsvoMT zO~BJVMlTDy5zINo_M3b``bZxi>TNfb6%~5=wcSVS^;eeL$K7Gv9PU|&ZlUFC7L0K) ztu)dz(~_UBc$F^dXs;;sRnx=dxc+#ITg|p^9}P$r$S(pUd2tywoL`+54K7GdIoxjL z4Ha^~!GT~0jO}~$_nfos27i#OonAL7gnyW@IcDIPE(@%EBLfv*gwv~V5)_a&aC3;w z$9q7{iZE6lvEKLsLD&J+U{CVpZUOZq5<)WX{))V}R^+|Sd72yTs$jc#g*hyj-bjMh z!p7pAAT6c^W#7f2#d0$xqKRj2_7FF%6>mKRAYq2MDY}YxQX^}>*MG<$A&clbd|b^E z4m0RT#7yiv2AK<(-Z|tAtx*@AP4WgdxN786W^~x2SAQos%WPwk$qGviN%=u`l(cQi zi8zsjI|!4aU`L>MyRkjXD+5aNo>e1>`p|YH?G9^L2lVu9y_Bj|R#7@R70lYQ?S%f8 z*4WBInBGI`s}M!L_*7 zTo~w+xU0gR@&&K8p$=$3d%&qV8Vz6kOWMeWrNi8f#KR|y(tnX0v0yop4A!Yn&ea)p zl^Y4tRMYcP_V)u{J3g;d%?j)LIbNz&XSP8SP4YoMVVX4Ih-lPn@z9;^Y8p{~V0tiP zHI^eJFN#^e0q3O`PzJc?{HH0Fe(MlM7g!MCeZb zfIP*UhY5nI)_>{^!}b!Ll2IS%mtm0D9{Zj1Q?HX{JUHkRDX7`g%De)E0nI#-m>9M) zLmOele;Z#_IEdYdm@vyM8opjR;tm2m4^cESHq!0PHMuB@iFZ!giVo0*Y0V=~XF^nR z893n;rOyC^jNnDjM>KY`%3?o6Hz^zHb4vn3@t!%XJb!0=q6h9#Te{U6sk&w>Nr)o& zWWhiNYy7pvuT5)J*`$L6*R9|EWjBI_a|%DiDKQIqrr`zK zn|8{l(|_h2^5L|?ellFo%`)R+u1=S0WlOmg#E?p6Hd;}F2~8g*Pdtxe(n(SpEzMn8 zQZ7Q8owBDi({SbyD5%EGpSZY8QyILdfKEvJOG{kK&;D9tY(sMlS1JFLEp`j*wEqpU z{|&Lg5UVByp#Hx-wt+py+fo19WB+M;?6~+@Hh<$D-=b;eg8ko0Yh$I!d~9p2N%_uJ zn^aWmtwo`Ks};w{{bOcadm|2gHr*Rl{(d-n&lc5lZ*+Wp@mTEoaX+19bNmTYAO z`F~Z^{z+#4y_OHQov?f`+2YgD@WGmUbAEV+=FmoVN;g|Yu825MH`FZQpcrk8k!Zt+ zR0Km5LC(?H`Es!6558hlPBi*VD?{cM_2b&G!mK-I+zy4#B_C8I4;ITUc*G8 z+9M)lmTsU0h8d@PjB^~4zIu3gI-tUoG9VN)<9S^}F+pNzau<&mw~Sfcx(ru4X2r%`eu}m4dXx#pm7~mfv>oMd3v_?kw95;sEq{H! zqb@Uw*{Uux9@^*Y7J7Fhor^asZ%^m?!K*xNR}%<|Waa5G4&*Gc+9dE2YGu0DZ}%z6 zDJQ9F#|Hja?da~gp=$58SbPrM0&lRMb1QKSH-DkD)+A`NDnYnuGHHRfhJnShgI$0X zn(cySjBdp;s2SUI11)e&2~aHD%6~Gz$*X9P_*~QHTQCilJ-(|xi)qlI1`Gi#+lJ*Kh8%K?rPIMS5#X%eMfy}6tm_&D;m`4wSE2wwv9LE?Q^ItJ-##E zMijGEysc>HFpCtvMY~8N7OE?9I)GZrzXePJ*rV@wh~fttjP0m$gf_odh<|~VH7Xs& zB7;vUAtr_5e!kFtYT#ap76G4O1PN+{0bx6yebUN|y4e{@yOGE-1Y(+Aw`}dN3ZQAUtP!s_Ra_#1dTez+9;4|6UJ(lzad%V4_H8iN{soZ{Vga5-# z7HV!+FjUlwTUm>yR7j=qXn(Sl+I8e;a>IU9la9SO0V#CxI8~%s{HB5qbm-$~bkk0b zM*l!B>9MFxr=rFcZRS-ju+d!Os;lRkAVOIlzHY1tc?u^E&Cf8;(lwDZ%)MFSt%2>} zq-8Ys$m#s>I^?#%TI4EE`{!u=0BYz+nbTYycx4T=F!Tkj{+hE5o6)wM>v(j2iA->{8mB2#2n5Ny3(Zs=M(A3Jhr z!r7*vFmG{0EMcqN@qaj#88eygA_2D@?XUHzX~PEqXVvyll-F#pFfg?Sh=JnZu$&iH zY&~#C`%MN4q1AkiA3eVC8vmCSjQRN(7p>5FJ*ZJ^vjkMtUii_hr$m~Y4_fr8 z*N<0(n1{26Up1{?-13%1EeOUjbvu!at879vuK72SP0@@ACx55_>1f-yJKZ2&L^rBI zg^g$i_5Lo335qej7!9FTIymx+-HFH{lp%t0donJobS9+6YyKNZxkR%SG1WLW6-9Hz zQ;WrmTiy0JI_fHktcc+h-2jR=(O4K1qRoAsHd@QV+E8=}qJZ$kMeSf!Z4xK)%1hwQGYDREbvGck$juDF6pVx-WJbn(&| zWam&Tihju<$L&-89jWS7?kuBLMoRyCsO)zjMQO?+tAp2FjGl)v_hB>gQ($&bLtQCP z^)}J4VI=s!ht7g-lEjcH&yOfxn~_rjV6~+tSF}Br%70x++7t!EX7kH-Dev)+H|rUl~-#_rlEw0*b@jQ6fa_!B|HHP>ainu+%BgFg=Kp&DlHkf zBNtt%;lAp|>a>7|!ozLVAZV{YFcgIF0q_g^j!Qa|12Q!_Xw-N7NhQ86F5aSv5<}CJ z!EcbJPzS&HxeGt{nV(xqutL5XF-`*n1KMO$bj%Vcz!jBW^k9LobA~v(sQl@bUbidaRh<;iLk>s05#Gr&k znz!)t7EYc=9!=hWI8cd5<1(<5*^({}22dj4AFM>sKQ5;i<6@VI<8&=^8_&!=pIeQG1RAG$qTm$+&6B(ZV}nUtENIl4U;hfPx68_Tto`LBl%MGawkGYeuvP zN8K*|b-R#^(SUZMfj|ik^IKJM4*JttM1L%t4YUs#A$KuT+eP|Ymi+j1n<5d&_kn)D z@rHwZr!!2h5*=PDX&a9*2k$U(oR(W2UB+#?tubu)Z@Z0D;@AZ!mUMc+R~X5WL+YZd z!dtf~uE?qUITBJ_mYllchACM~Sz5;BYz9GWvZc7=+)|5SvI)1WEYO4PKS_P;_&LieTizG{JzCD$_szz| z(7D`hRxM;H0)GboGyN=CMIjG)uZ-SK3jXJfB_-*kH2U*;*~B*g=*whMTIoR{z03SO zQEK}hlIop%|0Jy@!xnF2P#BPjK8jC~UdRsi8Yf$L>3h8uVCKzb<>`2}l7CvREC=l$ zSd;j@*cOteu-G{P0?m3#+Sn2}<4tOh+hi<$9h8$%)ibZ!La$;*APj?zNJ}}Oifrfm zptfAf!szIOATG`Oo_y&t8eFv1dVG+2ph-6;lxR25PwW=S8tOSwP*RdnLsL6$8&kbK z^uJR>DPqp7TB%VbTIsZQP2(Gb?CQ2{la0G z?e!Ljxvf`U-2yYh2XXCwYF>k3d`}AW*Q!n~V7hBirq71dxiM`V%6}x)C(s};GM-~L z!g;t$2MJ24_$UUAkADMObn9)$fu0Q4+ehRkjrP%=x=-%%5^%7cF~TUAM41tOe;+(MPrwj4W$hOTq|?9nB=B`w39!!?54_$%SDSNTqeQB z$sNcm6<0}mv><3B5r5hUuy*9S0kJxYyUq35Kgns`G{CL$&;IknK;5X&kug{g#tp~X zkQEfDL%k6-R>|(zz7xScoWIWn$tZVBnTiiBn-g0I~U{E%`^R>8M^Uk$E zrd{h64rj;3qkpotFToZ|SS_DDrh$vym;@$uHgvLXJA)#(U9E_OY{jMyqiUt_mcM2w zVODvG#r+Mp;CqG>8eFu(-0RhM!Nw)biat8*#KJ@Jz2|0xOk9MQpK(ycZMqqku zt(2s~f<%~iKMhEWa3`g|Vcz|Y1b_xpB>Wmu1JHYr-ZMSS1MlNE5D)mf|I>Z*x=mf! z)PB_$HR^o7YSa5qgu32bE`Je%_}e zWy_gc8Gk^?pm+v#Gejvvd@^h>!}j#_Sli?Qm9Y)-){hahj&MX>LI5Cx(iv3E5Tgvy z!7uz3j?f#xCHeSbK87tsjc;m(<+NZy`YZfhO@BkGlOs5|%?ovyku$TfNei2_$jmcH z61p%8UpF*=@CjfD|I<}monJ0rY{_e??qNcu6%|1tZu)vfk0A)&xG>cSyEUsQ354n6 zYJ%5+#~f3{Z#5rZJYzBE=H{{*7ULZN zb%@;cTMuA@{S<<_sOlc|t`CIl0nkL5x|}S_JR>-fP(74EVZMlAyvDTC=5)&m2avk6 z&@>!LW}Ze+f|Y_=HRut8o4v1*1@tVBCw~H1Z3V`tp+4noiJj;0o%68RGzI7LCqNA- z@diTj*p!omT~){*hHML{V3@Liljf6rx=3|ro|(F$KtjxZ6LSec>&mO~0yA}v(Iu7) z17Kb}#;`oaZ&y>WZ;PS6kpzOL9t4Q+8L5X%Sxf7X0YHZ31)rCa;Tq9aV;YUd3xE78 zzkp1-Q(4Yx#>;7yL+{C`SEJ=NPdQ;{5N?Iv0HRRN`P3AXYX0c4nQ%0y$N8^z%ER=U zf>)FQXcRdJbL*$&Rbj@(h{{e;=nMwYc+Qstx`%W9&43+YU^#1%$a=j2au6$kE4}Im?1F|DJ(HBA1>uB*h=}tBq%pq? z-Zp*s=S>0?+B5`q4+hIL&e$9o_eCUV=Bf zt?SA=xr%+5(;m+)F2R)_9Vpn0`CtVGaXleNy4IeJPG?IYfOzd60Dp>+ADlU0>+b`E ze;lYQd;@xRld9Hu_PFim6*=6?yl>>BE6|N~FX(7rHUxu~j?RCEu9^?L;)9A(g9+-@ zaKqAg890z$!cJN5|5nzp0TN{m%Hf%LAI_HekGIW|B#CUe;FX`qB?-quSHR}4AD#0yK7lyMg-s?$1^I^oi zu?B{309{xu}$sn5`T_M%=gXOFx{xhr0TnqhoPy0BGR(nT16*1NkdUkN}<} zZw3=$ON0P1Z9{0X-U=Z~x!QW&D?N+>3B6BVf{!y9vUn;PHf;|F_cc>*p1f| zF)GaUda7DOR(~&5Fw^sTlV|4K^ujx77(MESYmA?XJv%7QJ>>xWN@|j*bU4*oD7TK z_+0#@WCw(H!%C`p?j8+JhZ(U0G;>Xni3~M2+aW$2-mU0k2s?Nh52;4ML^0MmkGHdc3|=*ejQ0v?5zq z7B)$WN7Zz}YmOGk9C&pAskSYEwb;4^EUVC4P?A+kEUHbKFv--xppi0s1F*NHWum@u znLY*55Puw0``iett*<6ez_1s&Uiu`!VEzHC4J^5D#_eWc9#3P=NctCMUmr|Zw-u9$x ziNcf1^;2;@*7;XBIOgF!+v7 zAAe~2qII1N`ok}Yr9w-u_P*Uqy{uSHU?U0CfT33|cbevBz4Hi`xILJ}FK=aATYU^A zBHSPN<|^enVfRp;Q`ASNUff#WOtA65RBapm*sLs=Tx{z$ZgWHG9IZ|1)^Cf2a|mWw zg+Mwip*>*HB02)xq522y%j$w!+miv*q<>%_V|QuWWXeI?n80CwPaXAj`Kz#arTaJR zhYJ!NtbV7`qNPC|8si&i8+f;R7 zH9yZ!&(89bGr440ml$}(*(|F0bm`$m*FLdLLV@3*mCJ#cEcHcYow(3cwaA(!id^)Vf^^H7Q>wzG`oMCBB+q^=P zaSqr&F6s=RUySG2GODl~@`i~9>3(HxYe8A5<{+F|Z2wF*o*Yzm+j~-CtbePJ_RUaS z3ea5RQJ-JTwCDR1+!8l$;ta=MyzN&_M&+aVl()aXuntc;J4CYXA0f*}I8Q2vp zQ>JD81WXvV@BpTFMIbgNKpG+IdZhsMQ5AL*UvFj?^x?pGgMaYOJ-%5%BfvfO#hWy$ zz-E=-U|HMOv8t&7$(N6@guwNOeEZ%VHOGMC_U@{C+v)z@zpMV;5Bn$fdtkmHpObw? zOvaCKTU+Xb7JsPV$M_s1AOtM`Gz3o$wdzkEFR%u%@V&MT>mmcGlP1WMyXsy$F;MyK z*l~PL;=K2K*HEGfy3FSk z-WzfUxFEu|?qsfkmJRIX{+w;s)xNX$^Aacap4V=}GjGmZmdUI0H}1;WyjaqgU%Mr4 zdDg`Y^PcbiBWT3-_VEH%0Z@h$^i4j012R6F!5uLkDLCb>ci_~-_u}yr94E(*9##E> z*yuc6f`9#_3FR%iRej0PM1ffa1f8jb<sGBlV;Esb0mKfi;)Ek&_5a&H3j8J0Dh zmCJ0!)~;(q&H%zr=;tvb6#dt>j`RNd(T)7*CtIw`A< z9S`vL3h!dNGVyB!KF5nCUJNQ?*9BG0u3^Nl;Pw_?%JiAfDQfM=d6??z5i}`f=AWpV z^~EDsiD=1L8Glyb=KK;Om(psgfTyBsyl?jbY7Uk00LJV45B5(E-+MO(!}wuz#Xdx2 zK7eZ{2HAfB$K(z3!HY4iw9UdV2XZI(6|7z7&|_+1pbmQ0VW`g}FAf&A^%eB%8C@nM zCsQ=tRDg(WZt;W8__`!p;6V#suw+?}FNVe6%xP+*esO*Ayk3ceJ-Xs6*N@4bppUNS zmVLvEb66n4jYG^AIr@tStEN7~KT1k}IXj;e&)|QH+>C|O`3-BN`SNui@ zmN^uYFyDqAJ%Up#+1e){-e2Y&jAdM7%q(gbC#;B)?d()foMy_XOJ?MxMalPuQb-Y5J8uXy$mwdRVSB#O)aa-SC+eMWz~ zkizT4O$T=XvLSnCXCw3HPE?t}ER$^G_jU{enL8%hM45y@{%Rzkv6w$UCd=J3$Rl~X zt!xtX$ecp~sRgqO*S*Cvg+2 zcs=-N0`~Ye7#Ch2dItvhO`{_&7b#~SCcfoo_e>(;!%W1lk=O+y9|jnGiqM+@7I*L2 zc-*}=I+MHi>|7cpxjVinckQygZO`4L>~!3F;R>SCc-|%+`Og_mZ?fF?f>gVG!E8ABAC4vx@#&f5c`iR=x=o@&3ZSN6XbAoTF32(pMIeN!_D-pS0KiVlYWXA(aJ+Vb|G9_%jZ^cxHbGMU;wT%7P)R1i zlUv3ZSdWyRFPG=_Y!Q3!oag`@YmgklWD$0s!>H(qGA0zBujs07#gBeVxJ{u#vetm3S9-1nG{ZzuHt;whWuP#&SzKkX3Fk^GL!`+x2%dq{Swys*N!=e zYiQix82yQS2l=SoSIp}8E09m!0aY>52wL-7>Q7K3Nmut-kX;oYX;eJi@(sQ~(@F+h zCqV?`5zvRwK6ZaYRi#U>d5m;1mNowNL-gb>;t8!9&C@rT?&Gia0p5~{q$|Cu$Ztgi zO4fJ*vaRq0JL0-d2;atPE^(f>UAou1x;xkrB)H3zSfU5IvS;rpCFwFQz{cHy??z}a zhBg42e@6!>26t37pxygjYz{N{OGQ+xL>^$k=a7_Y^^kvR>ju*IEj7B`J~Bdem)ps0 zgQ+)FozSNy54>4dgSbY+HKaC_3Zj)<5Pky*NZajH0r|Ou1xC=8 zL_&3YTAqJ(CJPMEGe+sg-GHE!PU7&%{)5Md2StenItS~Ml?d|wZP_eZC}_`@SCiq6 z0txj~gCK54ZWvWxpSxbb=92yf3cDm9pUTPO)O0_g^Mz^wsrVF+1MQRg#LxS;%et0q z5Hrr_ek>w|EX~Akn5U_onY~!dG#!Tqn)&bK+RcBybG^AW&=oJjFt#E3WC`_1S#2Wo z7;F+=?+ONWDc!boVlwJeXmAc|a?Vn=U>x8=n=^SHQlo1ajgc@ePX+~#R&oofLGm0H zkkHgOWbzHO(=K$rOIqkAMG1{*Yv)jB|B+xPw-F(>PSC+BOo<#@{5n`cPxgzp!RzER zx}JaS?|Lub->b&mA$0beTwgm{&?}TLW^Fxn&*R~p^cI|xd^{c9q>e6)w>9B!Tos66 zqgCHsAbQ_jGQz7u^t*c@mkUQFfu`?}Q{hdtvhbaeljZg5MGYy`D2FJ);(bhC)ZKuy z^hb@PST%fB&n|&^hW0<|s(GhtZUR;>G zk(>h_)H_WeD=dl&7GXuL_MnOY%1QiLuCNE02onFSUiwttd8(5RL?@_zfTH-7ua{M) z^~L2m-UX#=VLx>d{$&kWIVFx{mTRO1#IY{PwX$xKH0^LZ$v~aeHucn6TpfufD;$4~ zAjcSg($InVY%#YwMdH9FN$?#`j0{ugl#!hO&RxI#0470!En*qyHx+WQqTf+t1q~=B!6Lj&@?1$y6_4X&Oipv81mR321Rb{$aIypwaHF!>|qcVR36;_el1SaY;fJ7!xr|#iXM6g4H&MY^HQ=5i_ z-V_t(eDEn`%XNRrap@y- zdvL*G(f~hQnRLLz_G^I$dYgJ6Z&xP`r;7YdqC5xu$omiO(Qkg<+oVYDv2T<5xVOM< zN^_It?kO7Pix!Z$hxuW8Sit8d%vXO-RnZ?Zjc8;V|3~f_X6T;Q*MTF_uz|zLBpq-@8geP+_}_*Y;#G7ZihX_c9>4?@KI-|gi8Cc?kjtq2mnnO z)vsVuHDw)khnS4buV;Dqx+@{=+~$h3O5 z3;ouaYA!Q#zNuJb{dMaP>4+Z$!u8fM!WXZaTqx}MUFopXuc-Q@N) z2iOGViRgW5CE3Vryxr~&QOUoownNF@lSC_Z{>$t3u1Hkv@V!5iT;sA{i z3Ld6MfbVid1y3!wmyQMp77vllhm;;3VRD`S(BO!E8b`^k#|Vv(Zj;XN9Cmabl^EEf z)Fz$pzLJ0nevE&C1@k5RZuoT%giix`Bq1Qj{J4yS@~$=PZytVg^yDis@ww)akDrJ9 z;FxpQ=RAy(9lGc;$1+C|ah~~Nwh9Oqr%P-WZxt#fm} zkjsg0iU}Q8%N7PM?rWMBdx$cZ;3g2)n%zcYGu7`u-O?zwl$a*A9l(#)`xorS(#i@thTqEz)A^a~CFHqT@05 z(NqT9Cu4tDzC*X`o?>wR$h5S~O{n(@T?I{(ezNrxv`f2b87>XnVWuwYA<LW{Riv(&lD&2bH;`80|MRK4*oKWSz&f)`0`yw$a@E<9 ze+SxU+0ONk;Vv?9($ZbtSDBCQ^=Sk--~B=G>k6$%c-8L14?irvJw84;ERLQ$eEj{v zAw(7*6j%xm*g@C)1G9lnZBGwEY`aL2UUSVji~#^�Zo9oizsiLaD0@xRW@ZJOi=V z0h69iB$Jz-Dg%ky0h7p{G6TN{0+aooS_7sE0+U*wG6Mq<0+aBD8veWp_X!2v~(w=rq{Fv{sc#oM0)jE8E%2wEte)31Dbif%+1~_POVrd#`<&_#1JN zN(?vcb{iJ=_p2F9e=o<2`2^<6$=$E%3|8y$&o#L78z69f6$CA$)C_K!V2T`}rtW#$;Y@Sp=|g-E1j zsbc2AikVUghaMKx#9NGT)b0{QhPvF~e&^GG3^VtOe;yuC2Xzr~i6o(m?>uLr3#bqL zzJ<4V3PKtP@%qmZ>JiMA2@7ol?av&d*}|TPlm3RA^oAfD7F?ifcPc)m;UL#`KGYg^&H>kDkx zp=a4{fAZ~&DMZ6`Vyy(3B5yI<(_NTxl5|xG+1(0FWoU18b7gZ-O9ci10000E Q01g200{{T=n*aa+0H_x^9{>OV From 3173dfb42ad96a72f8286f5515739c3146a6a917 Mon Sep 17 00:00:00 2001 From: FlightControl Date: Fri, 27 May 2016 13:41:51 +0200 Subject: [PATCH 5/5] Documentations --- Moose Training/Documentation/Base.html | 28 +- Moose Training/Documentation/CARGO.html | 4 +- Moose Training/Documentation/CleanUp.html | 4 +- Moose Training/Documentation/Client.html | 40 +- Moose Training/Documentation/DCSAirbase.html | 4 +- .../Documentation/DCSCoalitionObject.html | 4 +- Moose Training/Documentation/DCSCommand.html | 4 +- .../Documentation/DCSController.html | 4 +- Moose Training/Documentation/DCSGroup.html | 4 +- Moose Training/Documentation/DCSObject.html | 4 +- Moose Training/Documentation/DCSTask.html | 4 +- Moose Training/Documentation/DCSTypes.html | 4 +- Moose Training/Documentation/DCSUnit.html | 4 +- Moose Training/Documentation/DCSWorld.html | 4 +- Moose Training/Documentation/DCStimer.html | 4 +- Moose Training/Documentation/DEPLOYTASK.html | 4 +- .../Documentation/DESTROYBASETASK.html | 4 +- .../Documentation/DESTROYGROUPSTASK.html | 4 +- .../Documentation/DESTROYRADARSTASK.html | 4 +- .../Documentation/DESTROYUNITTYPESTASK.html | 4 +- Moose Training/Documentation/Database.html | 622 ++++++------ Moose Training/Documentation/Escort.html | 7 +- Moose Training/Documentation/Event.html | 4 +- Moose Training/Documentation/GOHOMETASK.html | 4 +- Moose Training/Documentation/Group.html | 38 +- Moose Training/Documentation/GroupSet.html | 644 ++++++++++++ Moose Training/Documentation/MISSION.html | 82 +- Moose Training/Documentation/MOVEMENT.html | 4 +- Moose Training/Documentation/Menu.html | 4 +- Moose Training/Documentation/Message.html | 4 +- .../Documentation/MissileTrainer.html | 4 +- Moose Training/Documentation/NOTASK.html | 4 +- Moose Training/Documentation/PICKUPTASK.html | 4 +- Moose Training/Documentation/ROUTETASK.html | 4 +- Moose Training/Documentation/STAGE.html | 31 +- Moose Training/Documentation/Scheduler.html | 44 +- Moose Training/Documentation/Scoring.html | 4 +- Moose Training/Documentation/Sead.html | 4 +- Moose Training/Documentation/Set.html | 948 ++---------------- Moose Training/Documentation/Spawn.html | 4 +- .../Documentation/StaticObject.html | 4 +- Moose Training/Documentation/TASK.html | 4 +- Moose Training/Documentation/Unit.html | 4 +- Moose Training/Documentation/UnitSet.html | 682 +++++++++++++ Moose Training/Documentation/Zone.html | 4 +- Moose Training/Documentation/env.html | 4 +- Moose Training/Documentation/index.html | 22 +- Moose Training/Documentation/land.html | 4 +- Moose Training/Documentation/routines.html | 6 +- 49 files changed, 2040 insertions(+), 1298 deletions(-) create mode 100644 Moose Training/Documentation/GroupSet.html create mode 100644 Moose Training/Documentation/UnitSet.html diff --git a/Moose Training/Documentation/Base.html b/Moose Training/Documentation/Base.html index f7177aa41..c35a7adac 100644 --- a/Moose Training/Documentation/Base.html +++ b/Moose Training/Documentation/Base.html @@ -42,11 +42,12 @@

  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • @@ -253,7 +255,7 @@ These tracing levels were defined to avoid bulks of tracing to be generated by l BASE:New() - +

    The base constructor.

    @@ -769,7 +771,29 @@ is the Child class from which the Parent class needs to be retrieved.

    +

    The base constructor.

    + +

    This is the top top class of all classed defined within the MOOSE. +Any new class needs to be derived from this class for proper inheritance.

    + +

    Return value

    + +

    #BASE: +The new instance of the BASE class.

    + +

    Usage:

    +
    function TASK:New()
    +
    +    local self = BASE:Inherit( self, BASE:New() )
    +
    +    -- assign Task default values during construction
    +    self.TaskBriefing = "Task: No Task."
    +    self.Time = timer.getTime()
    +    self.ExecuteStage = _TransportExecuteStage.NONE
    +
    +    return self
    +end
    diff --git a/Moose Training/Documentation/CARGO.html b/Moose Training/Documentation/CARGO.html index fdacc75e3..f7ca9c91e 100644 --- a/Moose Training/Documentation/CARGO.html +++ b/Moose Training/Documentation/CARGO.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • diff --git a/Moose Training/Documentation/CleanUp.html b/Moose Training/Documentation/CleanUp.html index 1b0702bb3..c22bc448d 100644 --- a/Moose Training/Documentation/CleanUp.html +++ b/Moose Training/Documentation/CleanUp.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • diff --git a/Moose Training/Documentation/Client.html b/Moose Training/Documentation/Client.html index 67e324e6f..7ad2cd35e 100644 --- a/Moose Training/Documentation/Client.html +++ b/Moose Training/Documentation/Client.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • @@ -309,13 +311,19 @@ If the DCS Unit object does not exist or is nil, the CLIENT methods will return CLIENT:ShowBriefing() -

    Show the briefing of the MISSION to the CLIENT.

    +

    Show the briefing of a CLIENT.

    CLIENT:ShowCargo()

    Shows the Cargo#CARGO contained within the CLIENT to the player as a message.

    + + + + CLIENT:ShowMissionBriefing(MissionBriefing) + +

    Show the mission briefing of a MISSION to the CLIENT.

    @@ -992,7 +1000,7 @@ Name of the Group as defined within the Mission Editor. The Group must have a Un
    -

    Show the briefing of the MISSION to the CLIENT.

    +

    Show the briefing of a CLIENT.

    Return value

    @@ -1020,6 +1028,32 @@ self

    + +CLIENT:ShowMissionBriefing(MissionBriefing) + +
    +
    + +

    Show the mission briefing of a MISSION to the CLIENT.

    + +

    Parameter

    +
      +
    • + +

      #string MissionBriefing :

      + +
    • +
    +

    Return value

    + +

    #CLIENT: +self

    + +
    +
    +
    +
    + CLIENT.SwitchMessages(PrmTable) diff --git a/Moose Training/Documentation/DCSAirbase.html b/Moose Training/Documentation/DCSAirbase.html index 907c576e5..c5ad209a8 100644 --- a/Moose Training/Documentation/DCSAirbase.html +++ b/Moose Training/Documentation/DCSAirbase.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • diff --git a/Moose Training/Documentation/DCSCoalitionObject.html b/Moose Training/Documentation/DCSCoalitionObject.html index ee0e7b8dc..d7a968f25 100644 --- a/Moose Training/Documentation/DCSCoalitionObject.html +++ b/Moose Training/Documentation/DCSCoalitionObject.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • diff --git a/Moose Training/Documentation/DCSCommand.html b/Moose Training/Documentation/DCSCommand.html index 4c9762082..5f433a2f3 100644 --- a/Moose Training/Documentation/DCSCommand.html +++ b/Moose Training/Documentation/DCSCommand.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • diff --git a/Moose Training/Documentation/DCSController.html b/Moose Training/Documentation/DCSController.html index c4e7c7428..228983cdf 100644 --- a/Moose Training/Documentation/DCSController.html +++ b/Moose Training/Documentation/DCSController.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • diff --git a/Moose Training/Documentation/DCSGroup.html b/Moose Training/Documentation/DCSGroup.html index a7a84c8d1..d5df1b04a 100644 --- a/Moose Training/Documentation/DCSGroup.html +++ b/Moose Training/Documentation/DCSGroup.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • diff --git a/Moose Training/Documentation/DCSObject.html b/Moose Training/Documentation/DCSObject.html index 44157433f..d536b3e22 100644 --- a/Moose Training/Documentation/DCSObject.html +++ b/Moose Training/Documentation/DCSObject.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • diff --git a/Moose Training/Documentation/DCSTask.html b/Moose Training/Documentation/DCSTask.html index 93585f188..adf8812c1 100644 --- a/Moose Training/Documentation/DCSTask.html +++ b/Moose Training/Documentation/DCSTask.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • diff --git a/Moose Training/Documentation/DCSTypes.html b/Moose Training/Documentation/DCSTypes.html index 2cf9f22b0..56ccd79ff 100644 --- a/Moose Training/Documentation/DCSTypes.html +++ b/Moose Training/Documentation/DCSTypes.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • diff --git a/Moose Training/Documentation/DCSUnit.html b/Moose Training/Documentation/DCSUnit.html index e09265c44..e40060e37 100644 --- a/Moose Training/Documentation/DCSUnit.html +++ b/Moose Training/Documentation/DCSUnit.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • diff --git a/Moose Training/Documentation/DCSWorld.html b/Moose Training/Documentation/DCSWorld.html index e91ba3f3f..13b3fe634 100644 --- a/Moose Training/Documentation/DCSWorld.html +++ b/Moose Training/Documentation/DCSWorld.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • diff --git a/Moose Training/Documentation/DCStimer.html b/Moose Training/Documentation/DCStimer.html index b4af45ac2..b05ff053c 100644 --- a/Moose Training/Documentation/DCStimer.html +++ b/Moose Training/Documentation/DCStimer.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • diff --git a/Moose Training/Documentation/DEPLOYTASK.html b/Moose Training/Documentation/DEPLOYTASK.html index b57bf41c4..53467e608 100644 --- a/Moose Training/Documentation/DEPLOYTASK.html +++ b/Moose Training/Documentation/DEPLOYTASK.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • diff --git a/Moose Training/Documentation/DESTROYBASETASK.html b/Moose Training/Documentation/DESTROYBASETASK.html index 263a03453..aa3bef726 100644 --- a/Moose Training/Documentation/DESTROYBASETASK.html +++ b/Moose Training/Documentation/DESTROYBASETASK.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • diff --git a/Moose Training/Documentation/DESTROYGROUPSTASK.html b/Moose Training/Documentation/DESTROYGROUPSTASK.html index 2235e0590..baa779ad1 100644 --- a/Moose Training/Documentation/DESTROYGROUPSTASK.html +++ b/Moose Training/Documentation/DESTROYGROUPSTASK.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • diff --git a/Moose Training/Documentation/DESTROYRADARSTASK.html b/Moose Training/Documentation/DESTROYRADARSTASK.html index f246dfc10..cab844e6f 100644 --- a/Moose Training/Documentation/DESTROYRADARSTASK.html +++ b/Moose Training/Documentation/DESTROYRADARSTASK.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • diff --git a/Moose Training/Documentation/DESTROYUNITTYPESTASK.html b/Moose Training/Documentation/DESTROYUNITTYPESTASK.html index 62898b2ee..f5ce3a192 100644 --- a/Moose Training/Documentation/DESTROYUNITTYPESTASK.html +++ b/Moose Training/Documentation/DESTROYUNITTYPESTASK.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • diff --git a/Moose Training/Documentation/Database.html b/Moose Training/Documentation/Database.html index baef7069e..211aacd2c 100644 --- a/Moose Training/Documentation/Database.html +++ b/Moose Training/Documentation/Database.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • @@ -68,80 +70,42 @@

    Module Database

    -

    Manage sets of units and groups.

    +

    Manage the mission database.

    -

    #Database class

    -

    Mission designers can use the DATABASE class to build sets of units belonging to certain:

    +

    #DATABASE class

    +

    Mission designers can use the DATABASE class to refer to:

      -
    • Coalitions
    • -
    • Categories
    • -
    • Countries
    • -
    • Unit types
    • -
    • Starting with certain prefix strings.
    • +
    • UNITS
    • +
    • GROUPS
    • +
    • players
    • +
    • alive players
    • +
    • CLIENTS
    • +
    • alive CLIENTS
    -

    This list will grow over time. Planned developments are to include filters and iterators. -Additional filters will be added around Zone#ZONEs, Radiuses, Active players, ... -More iterators will be implemented in the near future ...

    - -

    Administers the Initial Sets of the Mission Templates as defined within the Mission Editor.

    - -

    DATABASE construction methods:

    -

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

    - - - - -

    DATABASE filter criteria:

    -

    You can set filter criteria to define the set of units within the database. -Filter criteria are defined by:

    - - - -

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

    - - - -

    Planned filter criteria within development are (so these are not yet available):

    - - +

    On top, for internal MOOSE administration purposes, the DATBASE administers the Unit and Gruop 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.

    DATABASE iterators:

    -

    Once the filters have been defined and the DATABASE has been built, you can iterate the database with the available iterator methods. +

    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:

    -

    Planned iterators methods in development are (so these are not yet available):

    - - - -
    -

    Global(s)

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

    + + + + @@ -178,13 +148,13 @@ The following iterator methods are currently available within the DATABASE:

    - + - + @@ -199,24 +169,18 @@ The following iterator methods are currently available within the DATABASE:

    + + + + - - - - - - - - @@ -240,25 +204,49 @@ The following iterator methods are currently available within the DATABASE:

    + + + + + + + + + + + + + + + + @@ -286,19 +274,13 @@ The following iterator methods are currently available within the DATABASE:

    - + - - - - - + @@ -313,24 +295,12 @@ The following iterator methods are currently available within the DATABASE:

    - - - - - - - - @@ -361,24 +331,6 @@ The following iterator methods are currently available within the DATABASE:

    - - - - - - - - - - - - @@ -388,15 +340,21 @@ The following iterator methods are currently available within the DATABASE:

    - + + + + + - +
    DATABASE:AddGroup(DCSGroup, GroupName)

    Adds a GROUP based on the GroupName in the DATABASE.

    +
    DATABASE:AddPlayer(UnitName, PlayerName) +

    Adds a player based on the Player Name in the DATABASE.

    DATABASE.ClassNameDATABASE.CLIENTSALIVE
    DATABASE.ClientsAliveDATABASE.ClassName DATABASE.DCSUnits +
    DATABASE:DeletePlayer(PlayerName) +

    Deletes a player from the DATABASE based on the Player Name.

    DATABASE:DeleteUnit(DCSUnitName)

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

    -
    DATABASE.Filter - -
    DATABASE.FilterMeta -
    DATABASE:ForEach(IteratorFunction, arg, Set) -

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

    +

    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:ForEachClient(IteratorFunction, ...) -

    Interate the DATABASE and call an interator function for each client, providing the Client to the function and optional parameters.

    +

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

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

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

    DATABASE:ForEachDCSUnit(IteratorFunction, ...) -

    Interate the DATABASE and call an interator function for each alive unit, providing the Unit and optional parameters.

    +

    Iterate the DATABASE and call an iterator function for each alive unit, providing the DCSUnit and optional parameters.

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

    Iterate the DATABASE and call an iterator function for each alive GROUP, providing the GROUP and optional parameters.

    DATABASE:ForEachPlayer(IteratorFunction, ...) -

    Interate the DATABASE and call an interator function for each alive player, providing the Unit of the player and optional parameters.

    +

    Iterate the DATABASE and call an iterator function for each player, providing the player name and optional parameters.

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

    Iterate the DATABASE and call an iterator function for each alive player, providing the Unit of the player and optional parameters.

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

    Iterate the DATABASE and call an iterator function for each alive UNIT, providing the UNIT and optional parameters.

    DATABASE.PlayersDATABASE.PLAYERS
    DATABASE.PlayersAlive - -
    DATABASE:ScanEnvironment()DATABASE.PLAYERSALIVE DATABASE:Spawn(SpawnTemplate)

    Instantiate new Groups within the DCSRTE.

    -
    DATABASE.Statics -
    DATABASE.Templates -
    DATABASE:TraceDatabase() -

    Traces the current database contents in the log ...

    DATABASE:_EventOnPlayerLeaveUnit(Event)

    Handles the OnPlayerLeaveUnit event to clean the active players table.

    -
    DATABASE:_IsAliveDCSGroup(DCSGroup) - -
    DATABASE:_IsAliveDCSUnit(DCSUnit) - -
    DATABASE:_IsIncludeDCSUnit(DCSUnit) -
    DATABASE:_RegisterGroup(GroupTemplate)DATABASE:_RegisterPlayers() +

    Private method that registers all alive players in the mission.

    +
    DATABASE:_RegisterTemplate(GroupTemplate)

    Private method that registers new Group Templates within the DATABASE Object.

    DATABASE:_RegisterPlayers()DATABASE:_RegisterTemplates() -

    Private method that registers all alive players in the mission.

    +
    @@ -473,6 +431,32 @@ The following iterator methods are currently available within the DATABASE:

    + +DATABASE:AddPlayer(UnitName, PlayerName) + +
    +
    + +

    Adds a player based on the Player Name in the DATABASE.

    + +

    Parameters

    +
      +
    • + +

      UnitName :

      + +
    • +
    • + +

      PlayerName :

      + +
    • +
    +
    +
    +
    +
    + DATABASE:AddUnit(DCSUnit, DCSUnitName) @@ -513,9 +497,9 @@ The following iterator methods are currently available within the DATABASE:

    - #string - -DATABASE.ClassName + + +DATABASE.CLIENTSALIVE
    @@ -527,9 +511,9 @@ The following iterator methods are currently available within the DATABASE:

    - - -DATABASE.ClientsAlive + #string + +DATABASE.ClassName
    @@ -564,6 +548,27 @@ The following iterator methods are currently available within the DATABASE:

    +
    +
    +
    +
    + + +DATABASE:DeletePlayer(PlayerName) + +
    +
    + +

    Deletes a player from the DATABASE based on the Player Name.

    + +

    Parameter

    +
      +
    • + +

      PlayerName :

      + +
    • +
    @@ -585,34 +590,6 @@ The following iterator methods are currently available within the DATABASE:

    -
    -
    -
    -
    - - - -DATABASE.Filter - -
    -
    - - - -
    -
    -
    -
    - - - -DATABASE.FilterMeta - -
    -
    - - -
    @@ -702,7 +679,7 @@ The found Unit.

    -

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

    +

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

    Parameters

      @@ -739,7 +716,7 @@ self

    -

    Interate the DATABASE and call an interator function for each client, providing the Client to the function and optional parameters.

    +

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

    Parameters

      @@ -765,20 +742,84 @@ self

      - -DATABASE:ForEachDCSUnit(IteratorFunction, ...) + +DATABASE:ForEachClientAlive(IteratorFunction, ...)
      -

      Interate the DATABASE and call an interator function for each alive unit, providing the Unit and optional parameters.

      +

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

      Parameters

      • #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 when there is an alive CLIENT in the database. The function needs to accept a CLIENT parameter.

        + +
      • +
      • + +

        ... :

        + +
      • +
      +

      Return value

      + +

      #DATABASE: +self

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

      Iterate the DATABASE and call an iterator function for each alive unit, providing the DCSUnit and optional parameters.

      + +

      Parameters

      +
        +
      • + +

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

        + +
      • +
      • + +

        ... :

        + +
      • +
      +

      Return value

      + +

      #DATABASE: +self

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

      Iterate the DATABASE and call an iterator function for each alive GROUP, providing the GROUP and optional parameters.

      + +

      Parameters

      +
        +
      • + +

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

      • @@ -803,7 +844,39 @@ self

        -

        Interate the DATABASE and call an interator function for each alive player, providing the Unit of the player and optional parameters.

        +

        Iterate the DATABASE and call an iterator function for each player, providing the player name and optional parameters.

        + +

        Parameters

        +
          +
        • + +

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

        + +

        #DATABASE: +self

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

      Iterate the DATABASE and call an iterator function for each alive player, providing the Unit of the player and optional parameters.

      Parameters

        @@ -824,6 +897,38 @@ The function that will be called when there is an alive player in the database.

        #DATABASE: self

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

      Iterate the DATABASE and call an iterator function for each alive UNIT, providing the UNIT and optional parameters.

      + +

      Parameters

      +
        +
      • + +

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

      + +

      #DATABASE: +self

      +
      @@ -901,8 +1006,8 @@ DBObject = DATABASE:New()
      - -DATABASE.Players + +DATABASE.PLAYERS
      @@ -915,21 +1020,8 @@ DBObject = DATABASE:New()
      - -DATABASE.PlayersAlive - -
      -
      - - - -
      -
      -
      -
      - - -DATABASE:ScanEnvironment() + +DATABASE.PLAYERSALIVE
      @@ -993,20 +1085,6 @@ This method is used by the SPAWN class.

      #DATABASE: self

      -
      -
      -
      -
      - - - -DATABASE.Statics - -
      -
      - - -
      @@ -1021,27 +1099,6 @@ self

      -
    -
    -
    -
    - - -DATABASE:TraceDatabase() - -
    -
    - -

    Traces the current database contents in the log ...

    - - -

    (for debug reasons).

    - -

    Return value

    - -

    #DATABASE: -self

    -
    @@ -1145,84 +1202,6 @@ self

    - -DATABASE:_IsAliveDCSGroup(DCSGroup) - -
    -
    - - - -

    Parameter

    - -

    Return value

    - -

    #DATABASE: -self

    - -
    -
    -
    -
    - - -DATABASE:_IsAliveDCSUnit(DCSUnit) - -
    -
    - - - -

    Parameter

    - -

    Return value

    - -

    #DATABASE: -self

    - -
    -
    -
    -
    - - -DATABASE:_IsIncludeDCSUnit(DCSUnit) - -
    -
    - - - -

    Parameter

    - -

    Return value

    - -

    #DATABASE: -self

    - -
    -
    -
    -
    - DATABASE:_RegisterDatabase() @@ -1241,32 +1220,6 @@ self

    - -DATABASE:_RegisterGroup(GroupTemplate) - -
    -
    - -

    Private method that registers new Group Templates within the DATABASE Object.

    - -

    Parameter

    -
      -
    • - -

      #table GroupTemplate :

      - -
    • -
    -

    Return value

    - -

    #DATABASE: -self

    - -
    -
    -
    -
    - DATABASE:_RegisterPlayers() @@ -1280,6 +1233,45 @@ self

    #DATABASE: self

    +
    + +
    +
    + + +DATABASE:_RegisterTemplate(GroupTemplate) + +
    +
    + +

    Private method that registers new Group Templates within the DATABASE Object.

    + +

    Parameter

    +
      +
    • + +

      #table GroupTemplate :

      + +
    • +
    +

    Return value

    + +

    #DATABASE: +self

    + +
    +
    +
    +
    + + +DATABASE:_RegisterTemplates() + +
    +
    + + +
    diff --git a/Moose Training/Documentation/Escort.html b/Moose Training/Documentation/Escort.html index 6da80cd1e..040d46ca3 100644 --- a/Moose Training/Documentation/Escort.html +++ b/Moose Training/Documentation/Escort.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • @@ -1859,6 +1861,9 @@ self

    + +

    self.ReportTargetsScheduler = routines.scheduleFunction( self._ReportTargetsScheduler, { self }, timer.getTime() + 1, Seconds )

    +
    diff --git a/Moose Training/Documentation/Event.html b/Moose Training/Documentation/Event.html index 153c8cb07..1591464a5 100644 --- a/Moose Training/Documentation/Event.html +++ b/Moose Training/Documentation/Event.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • diff --git a/Moose Training/Documentation/GOHOMETASK.html b/Moose Training/Documentation/GOHOMETASK.html index 3a3adccd4..be69f3990 100644 --- a/Moose Training/Documentation/GOHOMETASK.html +++ b/Moose Training/Documentation/GOHOMETASK.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • diff --git a/Moose Training/Documentation/Group.html b/Moose Training/Documentation/Group.html index cfbc60f20..4e957bb03 100644 --- a/Moose Training/Documentation/Group.html +++ b/Moose Training/Documentation/Group.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • @@ -212,6 +214,12 @@ If the DCS Group object does not exist or is nil, the GROUP methods will return GROUP:GetCoalition()

    Returns the coalition of the DCS Group.

    + + + + GROUP:GetCountry() + +

    Returns the country of the DCS Group.

    @@ -1068,6 +1076,34 @@ The coalition side of the DCS Group.

    + +GROUP:GetCountry() + +
    +
    + +

    Returns the country of the DCS Group.

    + +

    Return values

    +
      +
    1. + +

      DCScountry#country.id: +The country identifier.

      + +
    2. +
    3. + +

      #nil: +The DCS Group is not existing or alive.

      + +
    4. +
    +
    +
    +
    +
    + GROUP:GetDCSGroup() diff --git a/Moose Training/Documentation/GroupSet.html b/Moose Training/Documentation/GroupSet.html new file mode 100644 index 000000000..140e7e19a --- /dev/null +++ b/Moose Training/Documentation/GroupSet.html @@ -0,0 +1,644 @@ + + + + + + +
    +
    + +
    +
    +
    +
    + +
    +

    Module GroupSet

    + +

    Create and manage a set of groups.

    + + + +

    #GROUPSET class

    +

    Mission designers can use the GROUPSET class to build sets of groups belonging to certain:

    + +
      +
    • Coalitions
    • +
    • Categories
    • +
    • Countries
    • +
    • Starting with certain prefix strings.
    • +
    + +

    GROUPSET construction methods:

    +

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

    + + + + +

    GROUPSET filter criteria:

    +

    You can set filter criteria to define the set of groups within the GROUPSET. +Filter criteria are defined by:

    + + + +

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

    + + + +

    Planned filter criteria within development are (so these are not yet available):

    + + + + +

    GROUPSET iterators:

    +

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

    + + + +

    Planned iterators methods in development are (so these are not yet available):

    + + + + +

    Global(s)

    + + + + + +
    GROUPSET + +
    +

    Type GROUPSET

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    GROUPSET:AddInDatabase(Event) +

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

    +
    GROUPSET.ClassName + +
    GROUPSET.Filter + +
    GROUPSET:FilterCategories(Categories) +

    Builds a set of groups out of categories.

    +
    GROUPSET:FilterCoalitions(Coalitions) +

    Builds a set of groups of coalitions.

    +
    GROUPSET:FilterCountries(Countries) +

    Builds a set of groups of defined countries.

    +
    GROUPSET.FilterMeta + +
    GROUPSET:FilterPrefixes(Prefixes) +

    Builds a set of groups of defined unit prefixes.

    +
    GROUPSET:FilterStart() +

    Starts the filtering.

    +
    GROUPSET:FindInDatabase(Event) +

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

    +
    GROUPSET:FindUnit(GroupName) +

    Finds a Unit based on the Unit Name.

    +
    GROUPSET:ForEachUnit(IteratorFunction, ...) +

    Interate the GROUPSET and call an interator function for each alive GROUP, providing the GROUP and optional parameters.

    +
    GROUPSET:IsIncludeObject(MooseGroup) + +
    GROUPSET:New() +

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

    +
    GROUPSET.Units + +
    + +

    Global(s)

    +
    +
    + + #GROUPSET + +GROUPSET + +
    +
    + + + +
    +
    +

    Type GroupSet

    + +

    Type GROUPSET

    + +

    GROUPSET class

    + +

    Field(s)

    +
    +
    + + +GROUPSET: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 SET birth event!

    + +

    Parameter

    + +

    Return values

    +
      +
    1. + +

      #string: +The name of the GROUP

      + +
    2. +
    3. + +

      #table: +The GROUP

      + +
    4. +
    +
    +
    +
    +
    + + #string + +GROUPSET.ClassName + +
    +
    + + + +
    +
    +
    +
    + + + +GROUPSET.Filter + +
    +
    + + + +
    +
    +
    +
    + + +GROUPSET:FilterCategories(Categories) + +
    +
    + +

    Builds a set of groups out of categories.

    + + +

    Possible current categories are plane, helicopter, ground, ship.

    + +

    Parameter

    +
      +
    • + +

      #string Categories : +Can take the following values: "plane", "helicopter", "ground", "ship".

      + +
    • +
    +

    Return value

    + +

    #GROUPSET: +self

    + +
    +
    +
    +
    + + +GROUPSET:FilterCoalitions(Coalitions) + +
    +
    + +

    Builds a set of groups of coalitions.

    + + +

    Possible current coalitions are red, blue and neutral.

    + +

    Parameter

    +
      +
    • + +

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

      + +
    • +
    +

    Return value

    + +

    #GROUPSET: +self

    + +
    +
    +
    +
    + + +GROUPSET:FilterCountries(Countries) + +
    +
    + +

    Builds a set of groups 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

    + +

    #GROUPSET: +self

    + +
    +
    +
    +
    + + + +GROUPSET.FilterMeta + +
    +
    + + + +
    +
    +
    +
    + + +GROUPSET:FilterPrefixes(Prefixes) + +
    +
    + +

    Builds a set of groups of defined unit prefixes.

    + + +

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

    + +

    Parameter

    +
      +
    • + +

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

      + +
    • +
    +

    Return value

    + +

    #GROUPSET: +self

    + +
    +
    +
    +
    + + +GROUPSET:FilterStart() + +
    +
    + +

    Starts the filtering.

    + +

    Return value

    + +

    #GROUPSET: +self

    + +
    +
    +
    +
    + + +GROUPSET: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 SET event or vise versa!

    + +

    Parameter

    + +

    Return values

    +
      +
    1. + +

      #string: +The name of the GROUP

      + +
    2. +
    3. + +

      #table: +The GROUP

      + +
    4. +
    +
    +
    +
    +
    + + +GROUPSET:FindUnit(GroupName) + +
    +
    + +

    Finds a Unit based on the Unit Name.

    + +

    Parameter

    +
      +
    • + +

      #string GroupName :

      + +
    • +
    +

    Return value

    + +

    Group#GROUP: +The found Unit.

    + +
    +
    +
    +
    + + +GROUPSET:ForEachUnit(IteratorFunction, ...) + +
    +
    + +

    Interate the GROUPSET and call an interator function for each alive GROUP, providing the GROUP and optional parameters.

    + +

    Parameters

    +
      +
    • + +

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

      + +
    • +
    • + +

      ... :

      + +
    • +
    +

    Return value

    + +

    #GROUPSET: +self

    + +
    +
    +
    +
    + + +GROUPSET:IsIncludeObject(MooseGroup) + +
    +
    + + + +

    Parameter

    + +

    Return value

    + +

    #GROUPSET: +self

    + +
    +
    +
    +
    + + +GROUPSET:New() + +
    +
    + +

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

    + +

    Return value

    + +

    #GROUPSET:

    + + +

    Usage:

    +
    -- Define a new GROUPSET Object. This DBObject will contain a reference to all alive GROUPS.
    +DBObject = GROUPSET:New()
    + +
    +
    +
    +
    + + + +GROUPSET.Units + +
    +
    + + + +
    +
    + +
    + +
    + + diff --git a/Moose Training/Documentation/MISSION.html b/Moose Training/Documentation/MISSION.html index 0e909bbc5..785f9854a 100644 --- a/Moose Training/Documentation/MISSION.html +++ b/Moose Training/Documentation/MISSION.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • @@ -66,7 +68,7 @@
    -

    Module MISSION

    +

    Module Mission

    A MISSION is the main owner of a Mission orchestration within MOOSE .

    @@ -86,12 +88,6 @@ A CLIENT needs to be registered within the MISSIONSCHEDULER - - - - TaskComplete - - @@ -101,8 +97,8 @@ A CLIENT needs to be registered within the Type MISSION - +

    Type MISSION

    +
    + + + + @@ -466,24 +468,6 @@ A CLIENT needs to be registered within the -
    - - #boolean - -TaskComplete - -
    -
    - - - - -

    For each Client, check for each Task the state and evolve the mission. - This flag will indicate if the Task of the Client is Complete.

    -
    @@ -511,8 +495,13 @@ A CLIENT needs to be registered within the Type MISSION -

    Field(s)

    +

    Type Mission

    + +

    Type MISSION

    + +

    The MISSION class

    + +

    Field(s)

    @@ -550,10 +539,10 @@ Client is the CLIENT object. The object must have been

    Usage:

    Add a number of Client objects to the Mission.
    -	Mission:AddClient( CLIENT:New( 'US UH-1H*HOT-Deploy Troops 1', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.' ):Transport() )
    -	Mission:AddClient( CLIENT:New( 'US UH-1H*RAMP-Deploy Troops 3', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.' ):Transport() )
    -	Mission:AddClient( CLIENT:New( 'US UH-1H*HOT-Deploy Troops 2', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.' ):Transport() )
    -	Mission:AddClient( CLIENT:New( 'US UH-1H*RAMP-Deploy Troops 4', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.' ):Transport() )
    + Mission:AddClient( CLIENT:FindByName( 'US UH-1H*HOT-Deploy Troops 1', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.' ):Transport() ) + Mission:AddClient( CLIENT:FindByName( 'US UH-1H*RAMP-Deploy Troops 3', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.' ):Transport() ) + Mission:AddClient( CLIENT:FindByName( 'US UH-1H*HOT-Deploy Troops 2', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.' ):Transport() ) + Mission:AddClient( CLIENT:FindByName( 'US UH-1H*RAMP-Deploy Troops 4', 'Transport 3 groups of air defense engineers from our barracks "Gold" and "Titan" to each patriot battery control center to activate our air defenses.' ):Transport() )
    @@ -885,6 +874,20 @@ env.info( "Task 2 Completion = " .. Tasks[2]:GetGoalPercentage() .. "%" ) + +
    +
    +
    + + #string + +MISSION.MissionBriefing + +
    +
    + + +
    @@ -1197,7 +1200,7 @@ local Mission = MISSIONSCHEDULER.AddMission( 'Rescue secret agent', 'Tactical',
    - + #MISSION.Clients MISSION._Clients @@ -1237,6 +1240,8 @@ local Mission = MISSIONSCHEDULER.AddMission( 'Rescue secret agent', 'Tactical',
    +

    Type MISSION.Clients

    +

    Type MISSIONSCHEDULER

    The MISSIONSCHEDULER is an OBJECT and is the main scheduler of ALL active MISSIONs registered within this scheduler.

    @@ -1339,7 +1344,7 @@ MissionFind = MISSIONSCHEDULER:FindMission( 'Russia Transport Troops SA-6' )
    - + #MISSIONSCHEDULER.MISSIONS MISSIONSCHEDULER.Missions @@ -1491,6 +1496,9 @@ MISSIONSCHEDULER:RemoveMission( 'Russia Transport Troops SA-6' ) + +

    MISSIONSCHEDULER.SchedulerId = routines.scheduleFunction( MISSIONSCHEDULER.Scheduler, { }, 0, 2 )

    +
    @@ -1628,6 +1636,8 @@ MISSIONSCHEDULER:RemoveMission( 'Russia Transport Troops SA-6' )
    +

    Type MISSIONSCHEDULER.MISSIONS

    + diff --git a/Moose Training/Documentation/MOVEMENT.html b/Moose Training/Documentation/MOVEMENT.html index 5d35d954c..ef93fb7c1 100644 --- a/Moose Training/Documentation/MOVEMENT.html +++ b/Moose Training/Documentation/MOVEMENT.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • diff --git a/Moose Training/Documentation/Menu.html b/Moose Training/Documentation/Menu.html index 286cd3f1f..7086e16b5 100644 --- a/Moose Training/Documentation/Menu.html +++ b/Moose Training/Documentation/Menu.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • diff --git a/Moose Training/Documentation/Message.html b/Moose Training/Documentation/Message.html index e7fb13b06..5b3fd432f 100644 --- a/Moose Training/Documentation/Message.html +++ b/Moose Training/Documentation/Message.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • diff --git a/Moose Training/Documentation/MissileTrainer.html b/Moose Training/Documentation/MissileTrainer.html index 8e21f7b5b..10678955f 100644 --- a/Moose Training/Documentation/MissileTrainer.html +++ b/Moose Training/Documentation/MissileTrainer.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • diff --git a/Moose Training/Documentation/NOTASK.html b/Moose Training/Documentation/NOTASK.html index 8aeb474b4..66d88c1ba 100644 --- a/Moose Training/Documentation/NOTASK.html +++ b/Moose Training/Documentation/NOTASK.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • diff --git a/Moose Training/Documentation/PICKUPTASK.html b/Moose Training/Documentation/PICKUPTASK.html index 079600693..50e1e220d 100644 --- a/Moose Training/Documentation/PICKUPTASK.html +++ b/Moose Training/Documentation/PICKUPTASK.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • diff --git a/Moose Training/Documentation/ROUTETASK.html b/Moose Training/Documentation/ROUTETASK.html index b53f143b3..efd64169e 100644 --- a/Moose Training/Documentation/ROUTETASK.html +++ b/Moose Training/Documentation/ROUTETASK.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • diff --git a/Moose Training/Documentation/STAGE.html b/Moose Training/Documentation/STAGE.html index c45a68326..a4476cf94 100644 --- a/Moose Training/Documentation/STAGE.html +++ b/Moose Training/Documentation/STAGE.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • @@ -274,6 +276,16 @@ + +
    MISSION.AddClient(CLIENT, self, Client) @@ -197,6 +193,12 @@ A CLIENT needs to be registered within the MISSION:Meta() +
    MISSION.MissionBriefing +
    STAGE.WaitTime +
    + +

    Type STAGEBRIEF

    + + + +
    STAGEBRIEF.StageBriefingTime +
    @@ -809,6 +821,23 @@

    Type STAGEARRIVE

    +

    Type STAGEBRIEF

    +

    Field(s)

    +
    +
    + + + +STAGEBRIEF.StageBriefingTime + +
    +
    + + + +
    +
    +

    Type STAGELANDING

    Type STAGEROUTE

    diff --git a/Moose Training/Documentation/Scheduler.html b/Moose Training/Documentation/Scheduler.html index 0b09abf54..59557d5cf 100644 --- a/Moose Training/Documentation/Scheduler.html +++ b/Moose Training/Documentation/Scheduler.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • @@ -91,8 +93,6 @@ - -

    Global(s)

    @@ -120,12 +120,6 @@ - - - - @@ -138,6 +132,12 @@ + + + +
    SCHEDULER.Repeat -
    SCHEDULER:Scheduler() -
    SCHEDULER:Stop()

    Stops the scheduler.

    +
    SCHEDULER:_Scheduler() +
    @@ -253,19 +253,6 @@ self

    - -
    -
    -
    - - -SCHEDULER:Scheduler() - -
    -
    - - -
    @@ -302,6 +289,19 @@ self

    #SCHEDULER: self

    + +
    +
    +
    + + +SCHEDULER:_Scheduler() + +
    +
    + + +
    diff --git a/Moose Training/Documentation/Scoring.html b/Moose Training/Documentation/Scoring.html index bcecc4626..199f2d13d 100644 --- a/Moose Training/Documentation/Scoring.html +++ b/Moose Training/Documentation/Scoring.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • diff --git a/Moose Training/Documentation/Sead.html b/Moose Training/Documentation/Sead.html index e8bf12d86..68a532b60 100644 --- a/Moose Training/Documentation/Sead.html +++ b/Moose Training/Documentation/Sead.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • diff --git a/Moose Training/Documentation/Set.html b/Moose Training/Documentation/Set.html index e43329a31..5d11030b7 100644 --- a/Moose Training/Documentation/Set.html +++ b/Moose Training/Documentation/Set.html @@ -42,11 +42,12 @@
  • Event
  • GOHOMETASK
  • Group
  • -
  • MISSION
  • +
  • GroupSet
  • MOVEMENT
  • Menu
  • Message
  • MissileTrainer
  • +
  • Mission
  • NOTASK
  • PICKUPTASK
  • ROUTETASK
  • @@ -59,6 +60,7 @@
  • StaticObject
  • TASK
  • Unit
  • +
  • UnitSet
  • Zone
  • env
  • land
  • @@ -154,111 +156,21 @@ The following iterator methods are currently available within the SET:

    Type SET

    - - - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -268,99 +180,33 @@ The following iterator methods are currently available within the SET:

    - + - - - - - - - - - - - - - - - - - - - - - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - @@ -370,45 +216,15 @@ The following iterator methods are currently available within the SET:

    - + - + - - - - - - - - - - - - - - - - - - - -
    SET:AddUnit(UnitToAdd) -

    Finds a Unit based on the Unit Name.

    -
    SET.ClassName
    SET.ClientsSET.Database
    SET.ClientsAliveSET:Flush() - -
    SET.DCSGroups - -
    SET.DCSGroupsAlive - -
    SET.DCSUnits - -
    SET.DCSUnitsAlive - -
    SET.Filter - -
    SET:FilterCategories(Categories) -

    Builds a set of units out of categories.

    -
    SET:FilterCoalitions(Coalitions) -

    Builds a set of units of coalitons.

    -
    SET:FilterCountries(Countries) -

    Builds a set of units of defined countries.

    -
    SET:FilterGroupPrefixes(Prefixes) -

    Builds a set of units of defined group prefixes.

    -
    SET.FilterMeta - -
    SET:FilterStart() -

    Starts the filtering.

    -
    SET:FilterTypes(Types) -

    Builds a set of units of defined unit types.

    -
    SET:FilterUnitPrefixes(Prefixes) -

    Builds a set of units of defined unit prefixes.

    -
    SET:FindUnit(UnitName) -

    Finds a Unit based on the Unit Name.

    +

    Flushes the current SET contents in the log ...

    SET:ForEachClient(IteratorFunction, ...)SET:IsIncludeObject(Object) -

    Interate the SET and call an interator function for each client, providing the Client to the function and optional parameters.

    +

    Decides whether to include the Object

    SET:ForEachDCSUnitAlive(IteratorFunction, ...) -

    Interate the SET and call an interator function for each alive unit, providing the Unit and optional parameters.

    -
    SET:ForEachPlayer(IteratorFunction, ...) -

    Interate the SET and call an interator function for each alive player, providing the Unit of the player and optional parameters.

    -
    SET.Groups - -
    SET.GroupsAlive - -
    SET.NavPoints - -
    SET:New()SET:New(Database)

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

    SET.PlayersSET.Set
    SET.PlayersAliveSET:_Add(ObjectName, Object) - -
    SET:ScanEnvironment() - -
    SET.Statics - -
    SET.Templates - -
    SET:TraceDatabase() -

    Traces the current SET contents in the log ...

    -
    SET.Units - -
    SET.UnitsAlive - +

    Adds a Object based on the Object Name.

    SET:_EventOnBirth(Event) -

    Handles the OnBirth event for the alive units set.

    +

    Handles the OnBirth event for the Set.

    SET:_EventOnPlayerEnterUnit(Event)SET:_FilterStart() -

    Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied).

    +

    Starts the filtering for the defined collection.

    SET:_EventOnPlayerLeaveUnit(Event)SET:_Find(ObjectName) -

    Handles the OnPlayerLeaveUnit event to clean the active players table.

    -
    SET:_IsAliveDCSGroup(DCSGroup) - -
    SET:_IsAliveDCSUnit(DCSUnit) - -
    SET:_IsIncludeDCSUnit(DCSUnit) - -
    SET:_RegisterDatabase() -

    Private method that registers all datapoints within in the mission.

    -
    SET:_RegisterPlayers() -

    Private method that registers all alive players in the mission.

    +

    Finds an Object based on the Object Name.

    @@ -438,32 +254,6 @@ The following iterator methods are currently available within the SET:

    - -SET:AddUnit(UnitToAdd) - -
    -
    - -

    Finds a Unit based on the Unit Name.

    - -

    Parameter

    - -

    Return value

    - -

    Unit#UNIT: -The added Unit.

    - -
    -
    -
    -
    - #string SET.ClassName @@ -479,8 +269,8 @@ The added Unit.

    - -SET.Clients + +SET.Database
    @@ -492,322 +282,21 @@ The added Unit.

    - - -SET.ClientsAlive + +SET:Flush()
    - - -
    -
    -
    -
    - - - -SET.DCSGroups - -
    -
    - - - -
    -
    -
    -
    - - - -SET.DCSGroupsAlive - -
    -
    - - - -
    -
    -
    -
    - - - -SET.DCSUnits - -
    -
    - - - -
    -
    -
    -
    - - - -SET.DCSUnitsAlive - -
    -
    - - - -
    -
    -
    -
    - - - -SET.Filter - -
    -
    - - - -
    -
    -
    -
    - - -SET:FilterCategories(Categories) - -
    -
    - -

    Builds a set of units out of categories.

    +

    Flushes the current SET contents in the log ...

    -

    Possible current categories are plane, helicopter, ground, ship.

    - -

    Parameter

    -
      -
    • - -

      #string Categories : -Can take the following values: "plane", "helicopter", "ground", "ship".

      - -
    • -
    -

    Return value

    - -

    #SET: -self

    - -
    -
    -
    -
    - - -SET:FilterCoalitions(Coalitions) - -
    -
    - -

    Builds a set of units of coalitons.

    - - -

    Possible current coalitions are red, blue and neutral.

    - -

    Parameter

    -
      -
    • - -

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

      - -
    • -
    -

    Return value

    - -

    #SET: -self

    - -
    -
    -
    -
    - - -SET:FilterCountries(Countries) - -
    -
    - -

    Builds a set of units 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: -self

    - -
    -
    -
    -
    - - -SET:FilterGroupPrefixes(Prefixes) - -
    -
    - -

    Builds a set of units of defined group prefixes.

    - - -

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

    - -

    Parameter

    -
      -
    • - -

      #string Prefixes : -The prefix of which the group name where the unit belongs to starts with.

      - -
    • -
    -

    Return value

    - -

    #SET: -self

    - -
    -
    -
    -
    - - - -SET.FilterMeta - -
    -
    - - - -
    -
    -
    -
    - - -SET:FilterStart() - -
    -
    - -

    Starts the filtering.

    +

    (for debug reasons).

    Return value

    -

    #SET: -self

    - -
    -
    -
    -
    - - -SET:FilterTypes(Types) - -
    -
    - -

    Builds a set of units of defined unit 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: -self

    - -
    -
    -
    -
    - - -SET:FilterUnitPrefixes(Prefixes) - -
    -
    - -

    Builds a set of units of defined unit prefixes.

    - - -

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

    - -

    Parameter

    -
      -
    • - -

      #string Prefixes : -The prefix of which the unit name starts with.

      - -
    • -
    -

    Return value

    - -

    #SET: -self

    - -
    -
    -
    -
    - - -SET:FindUnit(UnitName) - -
    -
    - -

    Finds a Unit based on the Unit Name.

    - -

    Parameter

    -
      -
    • - -

      #string UnitName :

      - -
    • -
    -

    Return value

    - -

    Unit#UNIT: -The found Unit.

    +

    #string: +A string with the names of the objects.

    @@ -851,25 +340,19 @@ self

    - -SET:ForEachClient(IteratorFunction, ...) + +SET:IsIncludeObject(Object)
    -

    Interate the SET and call an interator function for each client, providing the Client to the function and optional parameters.

    +

    Decides whether to include the Object

    -

    Parameters

    +

    Parameter

    • -

      #function IteratorFunction : -The function that will be called when there is an alive player in the SET. The function needs to accept a CLIENT parameter.

      - -
    • -
    • - -

      ... :

      +

      #table Object :

    @@ -878,125 +361,27 @@ The function that will be called when there is an alive player in the SET. The f

    #SET: self

    -
    -
    -
    -
    - - -SET:ForEachDCSUnitAlive(IteratorFunction, ...) - -
    -
    - -

    Interate the SET and call an interator function for each alive unit, providing the Unit and optional parameters.

    - -

    Parameters

    -
      -
    • - -

      #function IteratorFunction : -The function that will be called when there is an alive unit in the SET. The function needs to accept a UNIT parameter.

      - -
    • -
    • - -

      ... :

      - -
    • -
    -

    Return value

    - -

    #SET: -self

    - -
    -
    -
    -
    - - -SET:ForEachPlayer(IteratorFunction, ...) - -
    -
    - -

    Interate the SET and call an interator function for each alive player, providing the Unit of the player and optional parameters.

    - -

    Parameters

    -
      -
    • - -

      #function IteratorFunction : -The function that will be called when there is an alive player in the SET. The function needs to accept a UNIT parameter.

      - -
    • -
    • - -

      ... :

      - -
    • -
    -

    Return value

    - -

    #SET: -self

    - -
    -
    -
    -
    - - - -SET.Groups - -
    -
    - - - -
    -
    -
    -
    - - - -SET.GroupsAlive - -
    -
    - - - -
    -
    -
    -
    - - - -SET.NavPoints - -
    -
    - - -
    -SET:New() +SET:New(Database)

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

    +

    Parameter

    +
      +
    • + +

      Database :

      + +
    • +

    Return value

    #SET:

    @@ -1012,8 +397,8 @@ DBObject = SET:New()
    - -SET.Players + +SET.Set
    @@ -1025,104 +410,31 @@ DBObject = SET:New()
    - - -SET.PlayersAlive + +SET:_Add(ObjectName, Object)
    +

    Adds a Object based on the Object Name.

    +

    Parameters

    +
      +
    • + +

      #string ObjectName :

      -
    -
    -
    -
    - - -SET:ScanEnvironment() - -
    -
    - - - -
    -
    -
    -
    - - - -SET.Statics - -
    -
    - - - -
    -
    -
    -
    - - - -SET.Templates - -
    -
    - - - -
    -
    -
    -
    - - -SET:TraceDatabase() - -
    -
    - -

    Traces the current SET contents in the log ...

    - - -

    (for debug reasons).

    + +
  • + +

    #table Object :

    +
  • +

    Return value

    -

    #SET: -self

    - -
    -
    -
    -
    - - - -SET.Units - -
    -
    - - - -
    -
    -
    -
    - - - -SET.UnitsAlive - -
    -
    - - +

    #table: +The added Object.

    @@ -1135,7 +447,7 @@ self

    -

    Handles the OnBirth event for the alive units set.

    +

    Handles the OnBirth event for the Set.

    Parameter