--- **Core** - Define collections of objects to perform bulk actions and logically group objects. -- -- === -- -- ## Features: -- -- * Dynamically maintain collections of objects. -- * Manually modify the collection, by adding or removing objects. -- * Collections of different types. -- * Validate the presence of objects in the collection. -- * Perform bulk actions on collection. -- -- === -- -- Group objects or data of the same type into a collection, which is either: -- -- * Manually managed using the **:Add...()** or **:Remove...()** methods. The initial SET can be filtered with the **@{#SET_BASE.FilterOnce}()** method. -- * Dynamically updated when new objects are created or objects are destroyed using the **@{#SET_BASE.FilterStart}()** method. -- -- Various types of SET_ classes are available: -- -- * @{#SET_GROUP}: Defines a collection of @{Wrapper.Group}s filtered by filter criteria. -- * @{#SET_UNIT}: Defines a collection of @{Wrapper.Unit}s filtered by filter criteria. -- * @{#SET_STATIC}: Defines a collection of @{Wrapper.Static}s filtered by filter criteria. -- * @{#SET_CLIENT}: Defines a collection of @{Wrapper.Client}s filtered by filter criteria. -- * @{#SET_AIRBASE}: Defines a collection of @{Wrapper.Airbase}s filtered by filter criteria. -- * @{#SET_ZONE}: Defines a collection of @{Core.Zone}s filtered by filter criteria. -- * @{#SET_SCENERY}: Defines a collection of @{Wrapper.Scenery}s added via a filtered @{#SET_ZONE}. -- * @{#SET_DYNAMICCARGO}: Defines a collection of @{Wrapper.DynamicCargo}s filtered by filter criteria. -- -- These classes are derived from @{#SET_BASE}, which contains the main methods to manage the collections. -- -- A multitude of other methods are available in the individual set classes that allow to: -- -- * Validate the presence of objects in the SET. -- * Trigger events when objects in the SET change a zone presence. -- -- ## Notes on `FilterPrefixes()`: -- -- This filter always looks for a **partial match** somewhere in the given field. LUA regular expression apply here, so special characters in names like minus, dot, hash (#) etc might lead to unexpected results. -- Have a read through the following to understand the application of regular expressions: [LUA regular expressions](https://riptutorial.com/lua/example/20315/lua-pattern-matching). -- For example, setting a filter like so `FilterPrefixes("Huey")` is perfectly all right, whilst `FilterPrefixes("UH-1H Al-Assad")` might not be due to the minus signs. A quick fix here is to use a dot (.) -- in place of the special character, or escape it with a percentage sign (%), i.e. either `FilterPrefixes("UH.1H Al.Assad")` or `FilterPrefixes("UH%-1H Al%-Assad")` will give you the expected results. -- -- === -- -- ### Author: **FlightControl** -- ### Contributions: **funkyfranky**, **applevangelist** -- -- === -- -- @module Core.Set -- @image Core_Sets.JPG do -- SET_BASE --- SET_BASE class. -- @type SET_BASE -- @field #table Filter Table of filters. -- @field #table Set Table of objects. -- @field #table Index Table of indices. -- @field #table List Unused table. -- @field Core.Scheduler#SCHEDULER CallScheduler -- @field #SET_BASE.Filters Filter Filters -- @field #boolean filterNoRegex If true, FilterPrefix ignores special characters and evaluates plain string. Defaults to false. -- @field #boolean filterReplaceDash If true then if filterNoRegex is false, replace dashes with regex pattern friendly pattern. Defaults to true -- @extends Core.Base#BASE --- The @{Core.Set#SET_BASE} class defines the core functions that define a collection of objects. -- A SET provides iterators to iterate the SET, but will **temporarily** yield the ForEach iterator loop at defined **"intervals"** to the mail simulator loop. -- In this way, large loops can be done while not blocking the simulator main processing loop. -- The default **"yield interval"** is after 10 objects processed. -- The default **"time interval"** is after 0.001 seconds. -- -- ## Add or remove objects from the SET -- -- Some key core functions are @{Core.Set#SET_BASE.Add} and @{Core.Set#SET_BASE.Remove} to add or remove objects from the SET in your logic. -- -- ## Define the SET iterator **"yield interval"** and the **"time interval"** -- -- Modify the iterator intervals with the @{Core.Set#SET_BASE.SetIteratorIntervals} method. -- You can set the **"yield interval"**, and the **"time interval"**. (See above). -- -- @field #SET_BASE SET_BASE SET_BASE = { ClassName = "SET_BASE", Filter = {}, Set = {}, List = {}, Index = {}, Database = nil, CallScheduler = nil, FilterCoalitionNumbers = { [coalition.side.RED+1] = "red", [coalition.side.BLUE+1] = "blue", [coalition.side.NEUTRAL+1] = "neutral", }, FilterMeta = { Coalitions = { ["red"] = coalition.side.RED, ["blue"] = coalition.side.BLUE, ["neutral"] = coalition.side.NEUTRAL, }, }, filterNoRegex=false, filterReplaceDash=true, } --- Filters -- @type SET_BASE.Filter -- @field #table Coalition Coalitions -- @field #table Prefix Prefixes. --- Creates a new SET_BASE object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. -- @param #SET_BASE self -- @return #SET_BASE -- @usage -- -- Define a new SET_BASE Object. This DBObject will contain a reference to all Group and Unit Templates defined within the ME and the DCSRTE. -- DBObject = SET_BASE:New() function SET_BASE:New(Database) -- Inherits from BASE local self = BASE:Inherit(self, FSM:New()) -- Core.Set#SET_BASE self.Database = Database self:SetStartState("Started") --- Added Handler OnAfter for SET_BASE -- @function [parent=#SET_BASE] OnAfterAdded -- @param #SET_BASE self -- @param #string From -- @param #string Event -- @param #string To -- @param #string ObjectName The name of the object. -- @param Object The object. self:AddTransition("*", "Added", "*") --- Removed Handler OnAfter for SET_BASE -- @function [parent=#SET_BASE] OnAfterRemoved -- @param #SET_BASE self -- @param #string From -- @param #string Event -- @param #string To -- @param #string ObjectName The name of the object. -- @param Object The object. self:AddTransition("*", "Removed", "*") self.YieldInterval = 10 self.TimeInterval = 0.001 self.Set = {} self.Index = {} self.CallScheduler = SCHEDULER:New(self) self:SetEventPriority(2) return self end --- [Internal] Add a functional filter -- @param #SET_BASE self -- @param #function ConditionFunction If this function returns `true`, the object is added to the SET. The function needs to take a CONTROLLABLE object as first argument. -- @param ... Condition function arguments, if any. -- @return #boolean If true, at least one condition is true function SET_BASE:FilterFunction(ConditionFunction, ...) local condition={} condition.func=ConditionFunction condition.arg={} if arg then condition.arg=arg end if not self.Filter.Functions then self.Filter.Functions = {} end table.insert(self.Filter.Functions, condition) return self end --- [Internal] Check if the condition functions returns true. -- @param #SET_BASE self -- @param Wrapper.Controllable#CONTROLLABLE Object The object to filter for -- @return #boolean If true, if **all** conditions are true function SET_BASE:_EvalFilterFunctions(Object) -- All conditions must be true. for _,_condition in pairs(self.Filter.Functions or {}) do local condition=_condition -- Call function. if condition.func(Object,unpack(condition.arg)) == false then return false end end -- No condition was false. return true end --- Clear the Objects in the Set. -- @param #SET_BASE self -- @param #boolean TriggerEvent If `true`, an event remove is triggered for each group that is removed from the set. -- @return #SET_BASE self function SET_BASE:Clear(TriggerEvent) for Name, Object in pairs(self.Set) do self:Remove(Name, not TriggerEvent) end return self end --- Builds a set of objects of same coalitions. -- Possible current coalitions are red, blue and neutral. -- @param #SET_BASE self -- @param #string Coalitions Can take the following values: "red", "blue", "neutral" and coalition.side.RED, coalition.side.BLUE,coalition.side.NEUTRAL -- @param #boolean Clear If `true`, clear any previously defined filters. -- @return #SET_BASE self function SET_BASE:FilterCoalitions(Coalitions, Clear) if Clear or (not self.Filter.Coalitions) then self.Filter.Coalitions = {} end -- Ensure table. if type(Coalitions) ~= "table" then Coalitions = {Coalitions} end for CoalitionID, Coalition in pairs(Coalitions) do local coalition = Coalition if type(Coalition) == "number" then coalition = self.FilterCoalitionNumbers[Coalition+1] or "unknown" --self:I("Filter Coaltion for "..coalition) end self.Filter.Coalitions[coalition] = coalition end return self end --- Finds an @{Core.Base#BASE} object based on the object Name. -- @param #SET_BASE self -- @param #string ObjectName -- @return Core.Base#BASE The Object found. function SET_BASE:_Find(ObjectName) local ObjectFound = self.Set[ObjectName] return ObjectFound end --- Compares string for FilterPrefix function. -- @param #SET_BASE self -- @param #string Name The Name of the object. -- @param #string Pattern The pattern that is contained in the Name. -- @param #boolean NoRegex (Optional) If `true`, special characters in the Name are interpreted as plain text and not regular expressions. Default is `self.filterNoregex`. -- @param #boolean ReplaceDash If `true`, dashes are not used as regex. -- @return #boolean Returns `true`, if the pattern is contained in the name and false otherwise. function SET_BASE:_SearchPattern(Name, Pattern, NoRegex, ReplaceDash) NoRegex=NoRegex or self.filterNoRegex ReplaceDash=ReplaceDash or self.filterReplaceDash if ReplaceDash==true and NoRegex ~= true then -- Not sure why "-" is replaced by "%-" ?! - So we can still match group names with a dash in them -- reason is that the string is interpreted as a pattern and "-" is a special character then. For interpreting it as a string, fourth parameter needs to be set to true. Pattern=Pattern:gsub("-", "%%-") end local contain=string.find(Name, Pattern, 1, NoRegex) return contain end ---Set Regex Options for FilterPrefix function. -- @param #SET_BASE self -- @param #boolean NoRegex If true, switch off Regex pattern matching for FilterPrefixes. -- @param #boolean ReplaceDash If false, switch off dash "-" replacement in strings for FilterPrefixes. -- @return #SET_BASE self function SET_BASE:FilterSetRegex(NoRegex,ReplaceDash) if NoRegex ~= nil then self.filterNoRegex = NoRegex end self.filterReplaceDash = ReplaceDash or true return self end --- Gets the Set. -- @param #SET_BASE self -- @return #SET_BASE self function SET_BASE:GetSet() --self:F2() return self.Set or {} end --- Gets a list of the Names of the Objects in the Set. -- @param #SET_BASE self -- @return #table Table of names. function SET_BASE:GetSetNames() -- R2.3 --self:F2() local Names = {} for Name, Object in pairs(self.Set) do table.insert(Names, Name) end return Names end --- Returns a table of the Objects in the Set. -- @param #SET_BASE self -- @return #table Table of objects. function SET_BASE:GetSetObjects() -- R2.3 --self:F2() local Objects = {} for Name, Object in pairs(self.Set) do table.insert(Objects, Object) end return Objects end --- Removes a @{Core.Base#BASE} object from the @{Core.Set#SET_BASE} and derived classes, based on the Object Name. -- @param #SET_BASE self -- @param #string ObjectName -- @param #boolean NoTriggerEvent (Optional) When `true`, the :Remove() method will not trigger a **Removed** event. function SET_BASE:Remove(ObjectName, NoTriggerEvent) --self:F2({ ObjectName = ObjectName }) local TriggerEvent = true if NoTriggerEvent then TriggerEvent = false else TriggerEvent = true end local Object = self.Set[ObjectName] if Object then for Index, Key in ipairs(self.Index) do if Key == ObjectName then table.remove(self.Index, Index) self.Set[ObjectName] = nil break end end -- When NoTriggerEvent is true, then no Removed event will be triggered. if TriggerEvent then self:Removed(ObjectName, Object) end end end --- Adds a @{Core.Base#BASE} object in the @{Core.Set#SET_BASE}, using a given ObjectName as the index. -- @param #SET_BASE self -- @param #string ObjectName The name of the object. -- @param Core.Base#BASE Object The object itself. -- @return Core.Base#BASE The added BASE Object. function SET_BASE:Add(ObjectName, Object) -- Debug info. --self:T2({ ObjectName = ObjectName, Object = Object }) -- Error ahndling if not ObjectName or ObjectName == "" then self:E("SET_BASE:Add - Invalid ObjectName handed") self:E({ObjectName=ObjectName, Object=Object}) return self end -- Ensure that the existing element is removed from the Set before a new one is inserted to the Set if self.Set[ObjectName] then self:Remove(ObjectName, true) end -- Add object to set. self.Set[ObjectName] = Object -- Add Object name to Index. table.insert(self.Index, ObjectName) -- Trigger Added event. self:Added(ObjectName, Object) return self end --- Adds a @{Core.Base#BASE} object in the @{Core.Set#SET_BASE}, using the Object Name as the index. -- @param #SET_BASE self -- @param Wrapper.Object#OBJECT Object -- @return Core.Base#BASE The added BASE Object. function SET_BASE:AddObject(Object) --self:F2(Object.ObjectName) --self:T(Object.UnitName) --self:T(Object.ObjectName) self:Add(Object.ObjectName, Object) end --- Sort the set by name. -- @param #SET_BASE self -- @return Core.Base#BASE The added BASE Object. function SET_BASE:SortByName() local function sort(a, b) return a ThreatMax then ThreatMax = threat end end return ThreatMax end --- Filters for the defined collection. -- @param #SET_BASE self -- @return #SET_BASE self function SET_BASE:FilterOnce() --self:Clear() for ObjectName, Object in pairs(self.Database) do if self:IsIncludeObject(Object) then self:Add(ObjectName, Object) else self:Remove(ObjectName, true) end end return self end --- Clear all filters. You still need to apply :FilterOnce() -- @param #SET_BASE self -- @return #SET_BASE self function SET_BASE:FilterClear() for key,value in pairs(self.Filter) do self.Filter[key]={} end return self end --- Starts the filtering for the defined collection. -- @param #SET_BASE self -- @return #SET_BASE self function SET_BASE:_FilterStart() for ObjectName, Object in pairs(self.Database) do if self:IsIncludeObject(Object) then self:Add(ObjectName, Object) end end -- Follow alive players and clients -- self:HandleEvent(EVENTS.PlayerEnterUnit, self._EventOnPlayerEnterUnit) -- self:HandleEvent(EVENTS.PlayerLeaveUnit, self._EventOnPlayerLeaveUnit) return self end --- Starts the filtering of the Dead events for the collection. -- @param #SET_BASE self -- @return #SET_BASE self function SET_BASE:FilterDeads() -- R2.1 allow deads to be filtered to automatically handle deads in the collection. self:HandleEvent(EVENTS.Dead, self._EventOnDeadOrCrash) return self end --- Starts the filtering of the Crash events for the collection. -- @param #SET_BASE self -- @return #SET_BASE self function SET_BASE:FilterCrashes() -- R2.1 allow crashes to be filtered to automatically handle crashes in the collection. self:HandleEvent(EVENTS.Crash, self._EventOnDeadOrCrash) return self end --- Stops the filtering for the defined collection. -- @param #SET_BASE self -- @return #SET_BASE self function SET_BASE:FilterStop() self:UnHandleEvent(EVENTS.Birth) self:UnHandleEvent(EVENTS.Dead) self:UnHandleEvent(EVENTS.Crash) return self end --- Iterate the SET_BASE while identifying the nearest object in the set from a @{Core.Point#COORDINATE}. -- @param #SET_BASE self -- @param Core.Point#COORDINATE Coordinate A @{Core.Point#COORDINATE} object (but **not** a simple DCS#Vec2!) from where to evaluate the closest object in the set. -- @return Core.Base#BASE The closest object. -- @usage -- myset:FindNearestObjectFromPointVec2(ZONE:New("Test Zone"):GetCoordinate()) function SET_BASE:FindNearestObjectFromPointVec2(Coordinate) --self:F2(Coordinate) local NearestObject = nil local ClosestDistance = nil for ObjectID, ObjectData in pairs(self.Set) do if NearestObject == nil then NearestObject = ObjectData ClosestDistance = Coordinate:DistanceFromPointVec2(ObjectData:GetCoordinate()) else local Distance = Coordinate:DistanceFromPointVec2(ObjectData:GetCoordinate()) if Distance < ClosestDistance then NearestObject = ObjectData ClosestDistance = Distance end end end return NearestObject end --- Events --- Handles the OnBirth event for the Set. -- @param #SET_BASE self -- @param Core.Event#EVENTDATA Event function SET_BASE:_EventOnBirth(Event) --self:F3({ Event }) if Event.IniDCSUnit then local ObjectName, Object = self:AddInDatabase(Event) --self:T3(ObjectName, Object) if Object and self:IsIncludeObject(Object) then self:Add(ObjectName, Object) -- self:_EventOnPlayerEnterUnit(Event) end end end --- Handles the OnDead or OnCrash event for alive units set. -- @param #SET_BASE self -- @param Core.Event#EVENTDATA Event function SET_BASE:_EventOnDeadOrCrash(Event) --self:F({ Event }) if Event.IniDCSUnit then local ObjectName, Object = self:FindInDatabase(Event) if ObjectName then self:Remove(ObjectName) end end end --- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). -- @param #SET_BASE self -- @param Core.Event#EVENTDATA Event -- function SET_BASE:_EventOnPlayerEnterUnit(Event) -- --self:F3({ Event }) -- -- if Event.IniDCSUnit then -- local ObjectName, Object = self:AddInDatabase(Event) -- self:T3(ObjectName, Object) -- if self:IsIncludeObject(Object) then -- self:Add(ObjectName, Object) -- --self:_EventOnPlayerEnterUnit(Event) -- end -- end -- end --- Handles the OnPlayerLeaveUnit event to clean the active players table. -- @param #SET_BASE self -- @param Core.Event#EVENTDATA Event -- function SET_BASE:_EventOnPlayerLeaveUnit(Event) -- --self:F3({ Event }) -- -- local ObjectName = Event.IniDCSUnit -- if Event.IniDCSUnit then -- if Event.IniDCSGroup then -- local GroupUnits = Event.IniDCSGroup:getUnits() -- local PlayerCount = 0 -- for _, DCSUnit in pairs(GroupUnits) do -- if DCSUnit ~= Event.IniDCSUnit then -- if DCSUnit:getPlayerName() ~= nil then -- PlayerCount = PlayerCount + 1 -- end -- end -- end -- self:E(PlayerCount) -- if PlayerCount == 0 then -- self:Remove(Event.IniDCSGroupName) -- end -- end -- end -- end -- Iterators --- Iterate the SET_BASE and derived classes and call an iterator function for the given SET_BASE, providing the Object for each element within the set and optional parameters. -- @param #SET_BASE self -- @param #function IteratorFunction The function that will be called. -- @param #table arg Arguments of the IteratorFunction. -- @param #SET_BASE Set (Optional) The set to use. Default self:GetSet(). -- @param #function Function (Optional) A function returning a #boolean true/false. Only if true, the IteratorFunction is called. -- @param #table FunctionArguments (Optional) Function arguments. -- @return #SET_BASE self function SET_BASE:ForEach(IteratorFunction, arg, Set, Function, FunctionArguments) --self:F3(arg) Set = Set or self:GetSet() arg = arg or {} local function CoRoutine() local Count = 0 for ObjectID, ObjectData in pairs(Set) do local Object = ObjectData --self:T3(Object) if Function then if Function(unpack(FunctionArguments or {}), Object) == true then IteratorFunction(Object, unpack(arg)) end else IteratorFunction(Object, unpack(arg)) end Count = Count + 1 -- if Count % self.YieldInterval == 0 then -- coroutine.yield(false) -- end end return true end -- local co = coroutine.create(CoRoutine) local co = CoRoutine local function Schedule() -- local status, res = coroutine.resume(co) local status, res = co() --self:T3({ status, res }) if status == false then error(res) end if res == false then return true -- resume next time the loop end return false end -- self.CallScheduler:Schedule(self, Schedule, {}, self.TimeInterval, self.TimeInterval, 0) Schedule() return self end --- Iterate the SET_BASE and derived classes and call an iterator function for the given SET_BASE, providing the Object for each element within the set and optional parameters. -- @param #SET_BASE self -- @param #function IteratorFunction The function that will be called. -- @return #SET_BASE self function SET_BASE:ForSome(IteratorFunction, arg, Set, Function, FunctionArguments) --self:F3(arg) Set = Set or self:GetSet() arg = arg or {} local Limit = self:GetSomeIteratorLimit() local function CoRoutine() local Count = 0 for ObjectID, ObjectData in pairs(Set) do local Object = ObjectData --self:T3(Object) if Function then if Function(unpack(FunctionArguments), Object) == true then IteratorFunction(Object, unpack(arg)) end else IteratorFunction(Object, unpack(arg)) end Count = Count + 1 if Count >= Limit then break end -- if Count % self.YieldInterval == 0 then -- coroutine.yield(false) -- end end return true end -- local co = coroutine.create(CoRoutine) local co = CoRoutine local function Schedule() -- local status, res = coroutine.resume(co) local status, res = co() --self:T3({ status, res }) if status == false then error(res) end if res == false then return true -- resume next time the loop end return false end -- self.CallScheduler:Schedule(self, Schedule, {}, self.TimeInterval, self.TimeInterval, 0) Schedule() return self end ----- Iterate the SET_BASE and call an iterator function for each **alive** unit, providing the Unit and optional parameters. -- @param #SET_BASE self -- @param #function IteratorFunction The function that will be called when there is an alive unit in the SET_BASE. The function needs to accept a UNIT parameter. ---- @return #SET_BASE self -- function SET_BASE:ForEachDCSUnitAlive(IteratorFunction, ...) -- --self:F3(arg) -- -- self:ForEach(IteratorFunction, arg, self.DCSUnitsAlive) -- -- return self -- end -- ----- Iterate the SET_BASE and call an iterator function for each **alive** player, providing the Unit of the player and optional parameters. -- @param #SET_BASE self -- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_BASE. The function needs to accept a UNIT parameter. ---- @return #SET_BASE self -- function SET_BASE:ForEachPlayer(IteratorFunction, ...) -- --self:F3(arg) -- -- self:ForEach(IteratorFunction, arg, self.PlayersAlive) -- -- return self -- end -- -- ----- Iterate the SET_BASE and call an iterator function for each client, providing the Client to the function and optional parameters. -- @param #SET_BASE self -- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_BASE. The function needs to accept a CLIENT parameter. ---- @return #SET_BASE self -- function SET_BASE:ForEachClient(IteratorFunction, ...) -- --self:F3(arg) -- -- self:ForEach(IteratorFunction, arg, self.Clients) -- -- return self -- end --- Decides whether to include the Object. -- @param #SET_BASE self -- @param #table Object -- @return #SET_BASE self function SET_BASE:IsIncludeObject(Object) --self:F3(Object) return true end --- Decides whether an object is in the SET -- @param #SET_BASE self -- @param #table Object -- @return #boolean `true` if object is in set and `false` otherwise. function SET_BASE:IsInSet(Object) --self:F3(Object) local outcome = false if Object == nil then return false end local name = (Object ~= nil and Object.GetName) and Object:GetName() or "none" --self:I("SET_BASE: Objectname = "..name) self:ForEach( function(object) --self:I("SET_BASE: In set objectname = "..object:GetName()) if object:GetName() == name then outcome = true end end ) return outcome end --- Decides whether an object is **not** in the SET -- @param #SET_BASE self -- @param #table Object -- @return #SET_BASE self function SET_BASE:IsNotInSet(Object) --self:F3(Object) return not self:IsInSet(Object) end --- Gets a string with all the object names. -- @param #SET_BASE self -- @return #string A string with the names of the objects. function SET_BASE:GetObjectNames() --self:F3() local ObjectNames = "" for ObjectName, Object in pairs(self.Set) do ObjectNames = ObjectNames .. ObjectName .. ", " end return ObjectNames end --- Checks whether all or optionally any objects is inside a given zone. -- @param #SET_BASE self -- @param Core.Zone#ZONE Zone The zone. -- @param #boolean Any If `true`, at least one object has to be inside the zone. If `false` or `nil`, all objects need to be in the zone. -- @return #boolean Retruns `true` if objects are in the zone and `false` otherwise. function SET_BASE:IsInZone(Zone, Any) for ObjectName, Object in pairs(self.Set) do local object=Object --Wrapper.Positionable#POSITIONABLE local inzone=object:IsInZone(Zone) if inzone and Any then -- We want at least one and this one is return true elseif not inzone then -- We want all but at least one is not return false end end return true end --- Flushes the current SET_BASE contents in the log ... (for debugging reasons). -- @param #SET_BASE self -- @param Core.Base#BASE MasterObject (Optional) The master object as a reference. -- @return #string A string with the names of the objects. function SET_BASE:Flush(MasterObject) --self:F3() local ObjectNames = "" for ObjectName, Object in pairs(self.Set) do ObjectNames = ObjectNames .. ObjectName .. ", " end --self:F({ MasterObject = MasterObject and MasterObject:GetClassNameAndID(), "Objects in Set:", ObjectNames }) return ObjectNames end --- Get a *new* set table that only contains alive objects. -- @param #SET_BASE self -- @return #table Set table of alive objects. function SET_BASE:GetAliveSet() --self:F2() local AliveSet = {} -- Clean the Set before returning with only the alive Objects. for ObjectName, Object in pairs(self.Set) do if Object then if Object:IsAlive() then AliveSet[#AliveSet+1] = Object end end end return AliveSet or {} end end do -- SET_GROUP --- -- @type SET_GROUP #SET_GROUP -- @field Core.Timer#TIMER ZoneTimer -- @field #number ZoneTimerInterval -- @extends Core.Set#SET_BASE --- Mission designers can use the @{Core.Set#SET_GROUP} class to build sets of groups belonging to certain: -- -- * Coalitions -- * Categories -- * Countries -- * Starting with certain prefix strings. -- -- ## SET_GROUP constructor -- -- Create a new SET_GROUP object with the @{#SET_GROUP.New} method: -- -- * @{#SET_GROUP.New}: Creates a new SET_GROUP object. -- -- ## Add or Remove GROUP(s) from SET_GROUP -- -- GROUPS can be added and removed using the @{Core.Set#SET_GROUP.AddGroupsByName} and @{Core.Set#SET_GROUP.RemoveGroupsByName} respectively. -- These methods take a single GROUP name or an array of GROUP names to be added or removed from SET_GROUP. -- -- ## SET_GROUP filter criteria -- -- You can set filter criteria to define the set of groups within the SET_GROUP. -- Filter criteria are defined by: -- -- * @{#SET_GROUP.FilterCoalitions}: Builds the SET_GROUP with the groups belonging to the coalition(s). -- * @{#SET_GROUP.FilterCategories}: Builds the SET_GROUP with the groups belonging to the category(ies). -- * @{#SET_GROUP.FilterCountries}: Builds the SET_GROUP with the groups belonging to the country(ies). -- * @{#SET_GROUP.FilterPrefixes}: Builds the SET_GROUP with the groups *containing* the given string in the group name. **Attention!** LUA regular expression apply here, so special characters in names like minus, dot, hash (#) etc might lead to unexpected results. -- Have a read through here to understand the application of regular expressions: [LUA regular expressions](https://riptutorial.com/lua/example/20315/lua-pattern-matching) -- * @{#SET_GROUP.FilterActive}: Builds the SET_GROUP with the groups that are only active. Groups that are inactive (late activation) won't be included in the set! -- -- For the Category Filter, extra methods have been added: -- -- * @{#SET_GROUP.FilterCategoryAirplane}: Builds the SET_GROUP from airplanes. -- * @{#SET_GROUP.FilterCategoryHelicopter}: Builds the SET_GROUP from helicopters. -- * @{#SET_GROUP.FilterCategoryGround}: Builds the SET_GROUP from ground vehicles or infantry. -- * @{#SET_GROUP.FilterCategoryShip}: Builds the SET_GROUP from ships. -- * @{#SET_GROUP.FilterCategoryStructure}: Builds the SET_GROUP from structures. -- * @{#SET_GROUP.FilterZones}: Builds the SET_GROUP with the groups within a @{Core.Zone#ZONE}. -- * @{#SET_GROUP.FilterFunction}: Builds the SET_GROUP with a custom condition. -- -- Once the filter criteria have been set for the SET_GROUP, you can start filtering using: -- -- * @{#SET_GROUP.FilterStart}: Starts the filtering of the groups within the SET_GROUP and add or remove GROUP objects **dynamically**. -- * @{#SET_GROUP.FilterOnce}: Filters of the groups **once**. -- -- ## SET_GROUP iterators -- -- Once the filters have been defined and the SET_GROUP has been built, you can iterate the SET_GROUP with the available iterator methods. -- The iterator methods will walk the SET_GROUP set, and call for each element within the set a function that you provide. -- The following iterator methods are currently available within the SET_GROUP: -- -- * @{#SET_GROUP.ForEachGroup}: Calls a function for each alive group it finds within the SET_GROUP. -- * @{#SET_GROUP.ForEachGroupCompletelyInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence completely in a @{Core.Zone}, providing the GROUP and optional parameters to the called function. -- * @{#SET_GROUP.ForEachGroupPartlyInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence partly in a @{Core.Zone}, providing the GROUP and optional parameters to the called function. -- * @{#SET_GROUP.ForEachGroupNotInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence not in a @{Core.Zone}, providing the GROUP and optional parameters to the called function. -- -- -- ## SET_GROUP trigger events on the GROUP objects. -- -- The SET is derived from the FSM class, which provides extra capabilities to track the contents of the GROUP objects in the SET_GROUP. -- -- ### When a GROUP object crashes or is dead, the SET_GROUP will trigger a **Dead** event. -- -- You can handle the event using the OnBefore and OnAfter event handlers. -- The event handlers need to have the parameters From, Event, To, GroupObject. -- The GroupObject is the GROUP object that is dead and within the SET_GROUP, and is passed as a parameter to the event handler. -- See the following example: -- -- -- Create the SetCarrier SET_GROUP collection. -- -- local SetHelicopter = SET_GROUP:New():FilterPrefixes("Helicopter"):FilterStart() -- -- -- Put a Dead event handler on SetCarrier, to ensure that when a carrier is destroyed, that all internal parameters are reset. -- -- function SetHelicopter:OnAfterDead(From, Event, To, GroupObject) -- --self:F({ GroupObject = GroupObject:GetName() }) -- end -- -- === -- @field #SET_GROUP SET_GROUP SET_GROUP = { ClassName = "SET_GROUP", Filter = { Coalitions = nil, Categories = nil, Countries = nil, GroupPrefixes = nil, Zones = nil, Functions = nil, Alive = 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, -- R2.2 ship = Group.Category.SHIP, structure = Group.Category.STRUCTURE, }, }, } --- Creates a new SET_GROUP object, building a set of groups belonging to a coalitions, categories, countries, types or with defined prefix names. -- @param #SET_GROUP self -- @return #SET_GROUP -- @usage -- -- Define a new SET_GROUP Object. This DBObject will contain a reference to all alive GROUPS. -- DBObject = SET_GROUP:New() function SET_GROUP:New() -- Inherits from BASE local self = BASE:Inherit(self, SET_BASE:New(_DATABASE.GROUPS)) -- #SET_GROUP self:FilterActive(false) return self --- Filter the set once -- @function [parent=#SET_GROUP] FilterOnce -- @param #SET_GROUP self -- @return #SET_GROUP self ---Set Regex Options for FilterPrefix function. -- @function [parent=#SET_GROUP] FilterSetRegex -- @param #SET_GROUP self -- @param #boolean NoRegex If true, switch off Regex pattern matching for FilterPrefixes. -- @param #boolean ReplaceDash If false, switch off dash "-" replacement in strings for FilterPrefixes. -- @return #SET_GROUP self --- Builds a set of objects of same coalitions. -- Possible current coalitions are red, blue and neutral. -- @function [parent=#SET_GROUP] FilterCoalitions -- @param #SET_GROUP self -- @param #string Coalitions Can take the following values: "red", "blue", "neutral" and coalition.side.RED, coalition.side.BLUE,coalition.side.NEUTRAL -- @param #boolean Clear If `true`, clear any previously defined filters. -- @return #SET_GROUP self end --- Get a *new* set table that only contains alive groups. -- @param #SET_GROUP self -- @return #table Set of alive groups. function SET_GROUP:GetAliveSet() --self:F2() --local AliveSet = SET_GROUP:New() local AliveSet = {} -- Clean the Set before returning with only the alive Groups. for GroupName, GroupObject in pairs(self.Set) do local GroupObject = GroupObject -- Wrapper.Group#GROUP if GroupObject then if GroupObject:IsAlive() then AliveSet[GroupName] = GroupObject end end end return AliveSet or {} end --- Returns a report of of unit types. -- @param #SET_GROUP self -- @return Core.Report#REPORT A report of the unit types found. The key is the UnitTypeName and the value is the amount of unit types found. function SET_GROUP:GetUnitTypeNames() --self:F2() local MT = {} -- Message Text local UnitTypes = {} local ReportUnitTypes = REPORT:New() for GroupID, GroupData in pairs(self:GetSet()) do local Units = GroupData:GetUnits() for UnitID, UnitData in pairs(Units) do if UnitData:IsAlive() then local UnitType = UnitData:GetTypeName() if not UnitTypes[UnitType] then UnitTypes[UnitType] = 1 else UnitTypes[UnitType] = UnitTypes[UnitType] + 1 end end end end for UnitTypeID, UnitType in pairs(UnitTypes) do ReportUnitTypes:Add(UnitType .. " of " .. UnitTypeID) end return ReportUnitTypes end --- Add a GROUP to SET_GROUP. -- Note that for each unit in the group that is set, a default cargo bay limit is initialized. -- @param Core.Set#SET_GROUP self -- @param Wrapper.Group#GROUP group The group which should be added to the set. -- @param #boolean DontSetCargoBayLimit If true, do not attempt to auto-add the cargo bay limit per unit in this group. -- @return Core.Set#SET_GROUP self function SET_GROUP:AddGroup(group, DontSetCargoBayLimit) self:Add(group:GetName(), group) if not DontSetCargoBayLimit then -- I set the default cargo bay weight limit each time a new group is added to the set. -- TODO Why is this here in the first place? for UnitID, UnitData in pairs(group:GetUnits() or {}) do if UnitData and UnitData:IsAlive() then UnitData:SetCargoBayWeightLimit() end end end return self end --- Add GROUP(s) to SET_GROUP. -- @param Core.Set#SET_GROUP self -- @param #string AddGroupNames A single name or an array of GROUP names. -- @return Core.Set#SET_GROUP self function SET_GROUP:AddGroupsByName(AddGroupNames) local AddGroupNamesArray = (type(AddGroupNames) == "table") and AddGroupNames or { AddGroupNames } for AddGroupID, AddGroupName in pairs(AddGroupNamesArray) do self:Add(AddGroupName, GROUP:FindByName(AddGroupName)) end return self end --- Remove GROUP(s) from SET_GROUP. -- @param Core.Set#SET_GROUP self -- @param Wrapper.Group#GROUP RemoveGroupNames A single name or an array of GROUP names. -- @return Core.Set#SET_GROUP self function SET_GROUP:RemoveGroupsByName(RemoveGroupNames) local RemoveGroupNamesArray = (type(RemoveGroupNames) == "table") and RemoveGroupNames or { RemoveGroupNames } for RemoveGroupID, RemoveGroupName in pairs(RemoveGroupNamesArray) do self:Remove(RemoveGroupName) end return self end --- Finds a Group based on the Group Name. -- @param #SET_GROUP self -- @param #string GroupName -- @return Wrapper.Group#GROUP The found Group. function SET_GROUP:FindGroup(GroupName) local GroupFound = self.Set[GroupName] return GroupFound end --- Iterate the SET_GROUP while identifying the nearest object from a @{Core.Point#COORDINATE}. -- @param #SET_GROUP self -- @param Core.Point#COORDINATE Coordinate A @{Core.Point#COORDINATE} object from where to evaluate the closest object in the set. -- @return Wrapper.Group#GROUP The closest group. function SET_GROUP:FindNearestGroupFromPointVec2(Coordinate) --self:F2(Coordinate) local NearestGroup = nil -- Wrapper.Group#GROUP local ClosestDistance = nil local Set = self:GetAliveSet() for ObjectID, ObjectData in pairs(Set) do if NearestGroup == nil then NearestGroup = ObjectData ClosestDistance = Coordinate:DistanceFromPointVec2(ObjectData:GetCoordinate()) else local Distance = Coordinate:DistanceFromPointVec2(ObjectData:GetCoordinate()) if Distance < ClosestDistance then NearestGroup = ObjectData ClosestDistance = Distance end end end return NearestGroup end --- Builds a set of groups in zones. -- @param #SET_GROUP self -- @param #table Zones Table of Core.Zone#ZONE Zone objects, or a Core.Set#SET_ZONE -- @param #boolean Clear If `true`, clear any previously defined filters. -- @return #SET_GROUP self function SET_GROUP:FilterZones(Zones, Clear) if Clear or not self.Filter.Zones then self.Filter.Zones = {} end local zones = {} if Zones.ClassName and Zones.ClassName == "SET_ZONE" then zones = Zones.Set elseif type(Zones) ~= "table" or (type(Zones) == "table" and Zones.ClassName) then self:E("***** FilterZones needs either a table of ZONE Objects or a SET_ZONE as parameter!") return self else zones = Zones end for _, Zone in pairs(zones) do local zonename = Zone:GetName() self.Filter.Zones[zonename] = Zone end return self end --- [User] Add a custom condition function. -- @function [parent=#SET_GROUP] FilterFunction -- @param #SET_GROUP self -- @param #function ConditionFunction If this function returns `true`, the object is added to the SET. The function needs to take a GROUP object as first argument. -- @param ... Condition function arguments if any. -- @return #SET_GROUP self -- @usage -- -- Image you want to exclude a specific GROUP from a SET: -- local groundset = SET_GROUP:New():FilterCoalitions("blue"):FilterCategoryGround():FilterFunction( -- -- The function needs to take a GROUP object as first - and in this case, only - argument. -- function(grp) -- local isinclude = true -- if grp:GetName() == "Exclude Me" then isinclude = false end -- return isinclude -- end -- ):FilterOnce() -- BASE:I(groundset:Flush()) --- Builds a set of groups of coalitions. -- Possible current coalitions are red, blue and neutral. -- @param #SET_GROUP self -- @param #string Coalitions Can take the following values: "red", "blue", "neutral". -- @param #boolean Clear If `true`, clear any previously defined filters. -- @return #SET_GROUP self --- Builds a set of groups out of categories. -- Possible current categories are plane, helicopter, ground, ship. -- @param #SET_GROUP self -- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship". -- @param #boolean Clear If `true`, clear any previously defined filters. -- @return #SET_GROUP self function SET_GROUP:FilterCategories(Categories, Clear) if Clear or 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 out of ground category. -- @param #SET_GROUP self -- @return #SET_GROUP self function SET_GROUP:FilterCategoryGround() self:FilterCategories("ground") return self end --- Builds a set of groups out of airplane category. -- @param #SET_GROUP self -- @return #SET_GROUP self function SET_GROUP:FilterCategoryAirplane() self:FilterCategories("plane") return self end --- Builds a set of groups out of helicopter category. -- @param #SET_GROUP self -- @return #SET_GROUP self function SET_GROUP:FilterCategoryHelicopter() self:FilterCategories("helicopter") return self end --- Builds a set of groups out of ship category. -- @param #SET_GROUP self -- @return #SET_GROUP self function SET_GROUP:FilterCategoryShip() self:FilterCategories("ship") return self end --- Builds a set of groups out of structure category. -- @param #SET_GROUP self -- @return #SET_GROUP self function SET_GROUP:FilterCategoryStructure() self:FilterCategories("structure") return self end --- Builds a set of groups of defined countries. -- Possible current countries are those known within DCS world. -- @param #SET_GROUP self -- @param #string Countries Can take those country strings known within DCS world. -- @return #SET_GROUP self function SET_GROUP:FilterCountries(Countries) if not self.Filter.Countries then self.Filter.Countries = {} end if type(Countries) ~= "table" then Countries = { Countries } end for CountryID, Country in pairs(Countries) do self.Filter.Countries[Country] = Country end return self end --- Builds a set of groups that contain the given string in their group name. -- **Attention!** Bad naming convention as this **does not** filter only **prefixes** but all groups that **contain** the string. -- @param #SET_GROUP self -- @param #string Prefixes The string pattern(s) that needs to be contained in the group name. Can also be passed as a `#table` of strings. -- @return #SET_GROUP self function SET_GROUP:FilterPrefixes(Prefixes) if not self.Filter.GroupPrefixes then self.Filter.GroupPrefixes = {} end if type(Prefixes) ~= "table" then Prefixes = { Prefixes } end for PrefixID, Prefix in pairs(Prefixes) do self.Filter.GroupPrefixes[Prefix] = Prefix end return self end --- [Internal] Private function for use of continous zone filter -- @param #SET_GROUP self -- @return #SET_GROUP self function SET_GROUP:_ContinousZoneFilter() local Database = _DATABASE.GROUPS for ObjectName, Object in pairs(Database) do if self:IsIncludeObject(Object) and self:IsNotInSet(Object) then self:Add(ObjectName, Object) elseif (not self:IsIncludeObject(Object)) and self:IsInSet(Object) then self:Remove(ObjectName) end end return self end --- Builds a set of groups that are active, ie in the mission but not yet activated (false) or actived (true). -- Only the groups that are active will be included within the set. -- @param #SET_GROUP self -- @param #boolean Active (Optional) Include only active groups to the set. -- Include inactive groups if you provide false. -- @return #SET_GROUP self -- @usage -- -- -- Include only active groups to the set. -- GroupSet = SET_GROUP:New():FilterActive():FilterStart() -- -- -- Include only active groups to the set of the blue coalition, and filter one time. -- GroupSet = SET_GROUP:New():FilterActive():FilterCoalition("blue"):FilterOnce() -- -- -- Include only active groups to the set of the blue coalition, and filter one time. -- -- Later, reset to include back inactive groups to the set. -- GroupSet = SET_GROUP:New():FilterActive():FilterCoalition("blue"):FilterOnce() -- ... logic ... -- GroupSet = SET_GROUP:New():FilterActive(false):FilterCoalition("blue"):FilterOnce() -- function SET_GROUP:FilterActive(Active) Active = Active or not (Active == false) self.Filter.Active = Active return self end --- Build a set of groups that are alive. -- @param #SET_GROUP self -- @return #SET_GROUP self function SET_GROUP:FilterAlive() self.Filter.Alive = true return self end --- Starts the filtering. -- @param #SET_GROUP self -- @return #SET_GROUP self function SET_GROUP:FilterStart() if _DATABASE then self:_FilterStart() self:HandleEvent(EVENTS.Birth, self._EventOnBirth) self:HandleEvent(EVENTS.Dead, self._EventOnDeadOrCrash) self:HandleEvent(EVENTS.Crash, self._EventOnDeadOrCrash) self:HandleEvent(EVENTS.RemoveUnit, self._EventOnDeadOrCrash) self:HandleEvent(EVENTS.UnitLost, self._EventOnDeadOrCrash) self:HandleEvent(EVENTS.PlayerLeaveUnit, self._EventOnDeadOrCrash) if self.Filter.Zones then self.ZoneTimer = TIMER:New(self._ContinousZoneFilter,self) local timing = self.ZoneTimerInterval or 30 self.ZoneTimer:Start(timing,timing) end end return self end --- Set filter timer interval for FilterZones if using active filtering with FilterStart(). -- @param #SET_GROUP self -- @param #number Seconds (Optional) Seconds between check intervals, defaults to 30. **Caution** - do not be too agressive with timing! Groups are usually not moving fast enough -- to warrant a check of below 10 seconds. -- @return #SET_GROUP self function SET_GROUP:FilterZoneTimer(Seconds) self.ZoneTimerInterval = Seconds or 30 return self end --- Stops the filtering. -- @param #SET_GROUP self -- @return #SET_GROUP self function SET_GROUP:FilterStop() if _DATABASE then self:UnHandleEvent(EVENTS.Birth) self:UnHandleEvent(EVENTS.Dead) self:UnHandleEvent(EVENTS.Crash) self:UnHandleEvent(EVENTS.RemoveUnit) self:UnHandleEvent(EVENTS.UnitLost) if self.Filter.Zones and self.ZoneTimer and self.ZoneTimer:IsRunning() then self.ZoneTimer:Stop() end end return self end --- Handles the OnDead or OnCrash event for alive groups set. -- Note: The GROUP object in the SET_GROUP collection will only be removed if the last unit is destroyed of the GROUP. -- @param #SET_GROUP self -- @param Core.Event#EVENTDATA Event function SET_GROUP:_EventOnDeadOrCrash(Event) --self:F({ Event }) if Event.IniDCSUnit then local ObjectName, Object = self:FindInDatabase(Event) if ObjectName then local size = 1 if Event.IniDCSGroup then size = Event.IniDCSGroup:getSize() elseif Event.IniDCSGroupName then local grp = Group.getByName(Event.IniDCSGroupName) if grp then size = grp:getSize() end elseif Object:IsAlive() then size = Object:CountAliveUnits() end if size == 1 then -- Only remove if the last unit of the group was destroyed. self:Remove(ObjectName) end end end end --- Handles the Database to check on an event (birth) that the Object was added in the Database. -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! -- @param #SET_GROUP self -- @param Core.Event#EVENTDATA Event -- @return #string The name of the GROUP -- @return #table The GROUP function SET_GROUP:AddInDatabase(Event) --self:F3({ Event }) if Event.IniObjectCategory == Object.Category.UNIT then if not self.Database[Event.IniDCSGroupName] then self.Database[Event.IniDCSGroupName] = GROUP:Register(Event.IniDCSGroupName) --self:T(3(self.Database[Event.IniDCSGroupName]) end end return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName] end --- Handles the Database to check on any event that Object exists in the Database. -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! -- @param #SET_GROUP self -- @param Core.Event#EVENTDATA Event -- @return #string The name of the GROUP -- @return #table The GROUP function SET_GROUP:FindInDatabase(Event) --self:F3({ Event }) return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName] end --- Iterate the SET_GROUP and call an iterator function for each GROUP object, providing the GROUP and optional parameters. -- @param #SET_GROUP self -- @param #function IteratorFunction The function that will be called for all GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. -- @return #SET_GROUP self function SET_GROUP:ForEachGroup(IteratorFunction, ...) --self:F2(arg) self:ForEach(IteratorFunction, arg, self:GetSet()) return self end --- Iterate the SET_GROUP and call an iterator function for some GROUP objects, providing the GROUP and optional parameters. -- @param #SET_GROUP self -- @param #function IteratorFunction The function that will be called for some GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. -- @return #SET_GROUP self function SET_GROUP:ForSomeGroup(IteratorFunction, ...) --self:F2(arg) self:ForSome(IteratorFunction, arg, self:GetSet()) return self end --- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP object, providing the GROUP and optional parameters. -- @param #SET_GROUP self -- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. -- @return #SET_GROUP self function SET_GROUP:ForEachGroupAlive(IteratorFunction, ...) --self:F2(arg) self:ForEach(IteratorFunction, arg, self:GetAliveSet()) return self end --- Iterate the SET_GROUP and call an iterator function for some **alive** GROUP objects, providing the GROUP and optional parameters. -- @param #SET_GROUP self -- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. -- @return #SET_GROUP self function SET_GROUP:ForSomeGroupAlive(IteratorFunction, ...) --self:F2(arg) self:ForSome(IteratorFunction, arg, self:GetAliveSet()) return self end --- Activate late activated groups. -- @param #SET_GROUP self -- @param #number Delay Delay in seconds. -- @return #SET_GROUP self function SET_GROUP:Activate(Delay) local Set = self:GetSet() for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP local group=GroupData --Wrapper.Group#GROUP if group and group:IsAlive()==false then group:Activate(Delay) end end return self end --- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence completely in a @{Core.Zone}, providing the GROUP and optional parameters to the called function. -- @param #SET_GROUP self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. -- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. -- @return #SET_GROUP self function SET_GROUP:ForEachGroupCompletelyInZone(ZoneObject, IteratorFunction, ...) --self:F2(arg) self:ForEach(IteratorFunction, arg, self:GetSet(), -- @param Core.Zone#ZONE_BASE ZoneObject -- @param Wrapper.Group#GROUP GroupObject function(ZoneObject, GroupObject) if GroupObject:IsCompletelyInZone(ZoneObject) then return true else return false end end, { ZoneObject }) return self end --- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence partly in a @{Core.Zone}, providing the GROUP and optional parameters to the called function. -- @param #SET_GROUP self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. -- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. -- @return #SET_GROUP self function SET_GROUP:ForEachGroupPartlyInZone(ZoneObject, IteratorFunction, ...) --self:F2(arg) self:ForEach(IteratorFunction, arg, self:GetSet(), -- @param Core.Zone#ZONE_BASE ZoneObject -- @param Wrapper.Group#GROUP GroupObject function(ZoneObject, GroupObject) if GroupObject:IsPartlyInZone(ZoneObject) then return true else return false end end, { ZoneObject }) return self end --- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence not in a @{Core.Zone}, providing the GROUP and optional parameters to the called function. -- @param #SET_GROUP self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. -- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. -- @return #SET_GROUP self function SET_GROUP:ForEachGroupNotInZone(ZoneObject, IteratorFunction, ...) --self:F2(arg) self:ForEach(IteratorFunction, arg, self:GetSet(), -- @param Core.Zone#ZONE_BASE ZoneObject -- @param Wrapper.Group#GROUP GroupObject function(ZoneObject, GroupObject) if GroupObject:IsNotInZone(ZoneObject) then return true else return false end end, { ZoneObject }) return self end --- Iterate the SET_GROUP and return true if all the @{Wrapper.Group#GROUP} are completely in the @{Core.Zone#ZONE} -- @param #SET_GROUP self -- @param Core.Zone#ZONE Zone The Zone to be tested for. -- @return #boolean true if all the @{Wrapper.Group#GROUP} are completely in the @{Core.Zone#ZONE}, false otherwise -- @usage -- local MyZone = ZONE:New("Zone1") -- local MySetGroup = SET_GROUP:New() -- MySetGroup:AddGroupsByName({"Group1", "Group2"}) -- -- if MySetGroup:AllCompletelyInZone(MyZone) then -- MESSAGE:New("All the SET's GROUP are in zone !", 10):ToAll() -- else -- MESSAGE:New("Some or all SET's GROUP are outside zone !", 10):ToAll() -- end function SET_GROUP:AllCompletelyInZone(Zone) --self:F2(Zone) local Set = self:GetSet() for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP if not GroupData:IsCompletelyInZone(Zone) then return false end end return true end --- Iterate the SET_GROUP and call an iterator function for each alive GROUP that has any unit in the @{Core.Zone}, providing the GROUP and optional parameters to the called function. -- @param #SET_GROUP self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. -- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. -- @return #SET_GROUP self function SET_GROUP:ForEachGroupAnyInZone(ZoneObject, IteratorFunction, ...) --self:F2(arg) self:ForEach(IteratorFunction, arg, self:GetSet(), -- @param Core.Zone#ZONE_BASE ZoneObject -- @param Wrapper.Group#GROUP GroupObject function(ZoneObject, GroupObject) if GroupObject:IsAnyInZone(ZoneObject) then return true else return false end end, { ZoneObject }) return self end --- Iterate the SET_GROUP and return true if at least one of the @{Wrapper.Group#GROUP} is completely inside the @{Core.Zone#ZONE} -- @param #SET_GROUP self -- @param Core.Zone#ZONE Zone The Zone to be tested for. -- @return #boolean true if at least one of the @{Wrapper.Group#GROUP} is completely inside the @{Core.Zone#ZONE}, false otherwise. -- @usage -- local MyZone = ZONE:New("Zone1") -- local MySetGroup = SET_GROUP:New() -- MySetGroup:AddGroupsByName({"Group1", "Group2"}) -- -- if MySetGroup:AnyCompletelyInZone(MyZone) then -- MESSAGE:New("At least one GROUP is completely in zone !", 10):ToAll() -- else -- MESSAGE:New("No GROUP is completely in zone !", 10):ToAll() -- end function SET_GROUP:AnyCompletelyInZone(Zone) --self:F2(Zone) local Set = self:GetSet() for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP if GroupData:IsCompletelyInZone(Zone) then return true end end return false end --- Iterate the SET_GROUP and return true if at least one @{#UNIT} of one @{Wrapper.Group#GROUP} of the @{#SET_GROUP} is in @{Core.Zone} -- @param #SET_GROUP self -- @param Core.Zone#ZONE Zone The Zone to be tested for. -- @return #boolean true if at least one of the @{Wrapper.Group#GROUP} is partly or completely inside the @{Core.Zone#ZONE}, false otherwise. -- @usage -- local MyZone = ZONE:New("Zone1") -- local MySetGroup = SET_GROUP:New() -- MySetGroup:AddGroupsByName({"Group1", "Group2"}) -- -- if MySetGroup:AnyPartlyInZone(MyZone) then -- MESSAGE:New("At least one GROUP has at least one UNIT in zone !", 10):ToAll() -- else -- MESSAGE:New("No UNIT of any GROUP is in zone !", 10):ToAll() -- end function SET_GROUP:AnyInZone(Zone) --self:F2(Zone) local Set = self:GetSet() for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP if GroupData:IsPartlyInZone(Zone) or GroupData:IsCompletelyInZone(Zone) then return true end end return false end --- Iterate the SET_GROUP and return true if at least one @{Wrapper.Group#GROUP} of the @{#SET_GROUP} is partly in @{Core.Zone}. -- Will return false if a @{Wrapper.Group#GROUP} is fully in the @{Core.Zone} -- @param #SET_GROUP self -- @param Core.Zone#ZONE Zone The Zone to be tested for. -- @return #boolean true if at least one of the @{Wrapper.Group#GROUP} is partly or completely inside the @{Core.Zone#ZONE}, false otherwise. -- @usage -- local MyZone = ZONE:New("Zone1") -- local MySetGroup = SET_GROUP:New() -- MySetGroup:AddGroupsByName({"Group1", "Group2"}) -- -- if MySetGroup:AnyPartlyInZone(MyZone) then -- MESSAGE:New("At least one GROUP is partially in the zone, but none are fully in it !", 10):ToAll() -- else -- MESSAGE:New("No GROUP are in zone, or one (or more) GROUP is completely in it !", 10):ToAll() -- end function SET_GROUP:AnyPartlyInZone(Zone) --self:F2(Zone) local IsPartlyInZone = false local Set = self:GetSet() for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP if GroupData:IsCompletelyInZone(Zone) then return false elseif GroupData:IsPartlyInZone(Zone) then IsPartlyInZone = true -- at least one GROUP is partly in zone end end if IsPartlyInZone then return true else return false end end --- Iterate the SET_GROUP and return true if no @{Wrapper.Group#GROUP} of the @{#SET_GROUP} is in @{Core.Zone} -- This could also be achieved with `not SET_GROUP:AnyPartlyInZone(Zone)`, but it's easier for the -- mission designer to add a dedicated method -- @param #SET_GROUP self -- @param Core.Zone#ZONE Zone The Zone to be tested for. -- @return #boolean true if no @{Wrapper.Group#GROUP} is inside the @{Core.Zone#ZONE} in any way, false otherwise. -- @usage -- local MyZone = ZONE:New("Zone1") -- local MySetGroup = SET_GROUP:New() -- MySetGroup:AddGroupsByName({"Group1", "Group2"}) -- -- if MySetGroup:NoneInZone(MyZone) then -- MESSAGE:New("No GROUP is completely in zone !", 10):ToAll() -- else -- MESSAGE:New("No UNIT of any GROUP is in zone !", 10):ToAll() -- end function SET_GROUP:NoneInZone(Zone) --self:F2(Zone) local Set = self:GetSet() for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP if not GroupData:IsNotInZone(Zone) then -- If the GROUP is in Zone in any way return false end end return true end --- Iterate the SET_GROUP and count how many GROUPs are completely in the Zone -- That could easily be done with SET_GROUP:ForEachGroupCompletelyInZone(), but this function -- provides an easy to use shortcut... -- @param #SET_GROUP self -- @param Core.Zone#ZONE Zone The Zone to be tested for. -- @return #number the number of GROUPs completely in the Zone -- @usage -- local MyZone = ZONE:New("Zone1") -- local MySetGroup = SET_GROUP:New() -- MySetGroup:AddGroupsByName({"Group1", "Group2"}) -- -- MESSAGE:New("There are " .. MySetGroup:CountInZone(MyZone) .. " GROUPs in the Zone !", 10):ToAll() function SET_GROUP:CountInZone(Zone) --self:F2(Zone) local Count = 0 local Set = self:GetSet() for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP if GroupData:IsCompletelyInZone(Zone) then Count = Count + 1 end end return Count end --- Iterate the SET_GROUP and count how many UNITs are completely in the Zone -- @param #SET_GROUP self -- @param Core.Zone#ZONE Zone The Zone to be tested for. -- @return #number the number of GROUPs completely in the Zone -- @usage -- local MyZone = ZONE:New("Zone1") -- local MySetGroup = SET_GROUP:New() -- MySetGroup:AddGroupsByName({"Group1", "Group2"}) -- -- MESSAGE:New("There are " .. MySetGroup:CountUnitInZone(MyZone) .. " UNITs in the Zone !", 10):ToAll() function SET_GROUP:CountUnitInZone(Zone) --self:F2(Zone) local Count = 0 local Set = self:GetSet() for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP Count = Count + GroupData:CountInZone(Zone) end return Count end --- Iterate the SET_GROUP and count how many GROUPs and UNITs are alive. -- @param #SET_GROUP self -- @return #number The number of GROUPs alive. -- @return #number The number of UNITs alive. function SET_GROUP:CountAlive() local CountG = 0 local CountU = 0 local Set = self:GetSet() for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP if GroupData and GroupData:IsAlive() then CountG = CountG + 1 -- Count Units. for _, _unit in pairs(GroupData:GetUnits()) do local unit = _unit -- Wrapper.Unit#UNIT if unit and unit:IsAlive() then CountU = CountU + 1 end end end end return CountG, CountU end ----- Iterate the SET_GROUP and call an iterator function for each **alive** player, providing the Group of the player and optional parameters. -- @param #SET_GROUP self -- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_GROUP. The function needs to accept a GROUP parameter. ---- @return #SET_GROUP self -- function SET_GROUP:ForEachPlayer(IteratorFunction, ...) -- --self:F2(arg) -- -- self:ForEach(IteratorFunction, arg, self.PlayersAlive) -- -- return self -- end -- -- ----- Iterate the SET_GROUP and call an iterator function for each client, providing the Client to the function and optional parameters. -- @param #SET_GROUP self -- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_GROUP. The function needs to accept a CLIENT parameter. ---- @return #SET_GROUP self -- function SET_GROUP:ForEachClient(IteratorFunction, ...) -- --self:F2(arg) -- -- self:ForEach(IteratorFunction, arg, self.Clients) -- -- return self -- end --- -- @param #SET_GROUP self -- @param Wrapper.Group#GROUP MGroup The group that is checked for inclusion. -- @return #SET_GROUP self function SET_GROUP:IsIncludeObject(MGroup) --self:F2(MGroup) local MGroupInclude = true if self.Filter.Alive == true then local MGroupAlive = false --self:F({ Active = self.Filter.Active }) if MGroup and MGroup:IsAlive() then MGroupAlive = true end MGroupInclude = MGroupInclude and MGroupAlive end if self.Filter.Active ~= nil then local MGroupActive = false --self:F({ Active = self.Filter.Active }) if self.Filter.Active == false or (self.Filter.Active == true and MGroup:IsActive() == true) then MGroupActive = true end MGroupInclude = MGroupInclude and MGroupActive end if self.Filter.Coalitions and MGroupInclude then local MGroupCoalition = false for CoalitionID, CoalitionName in pairs(self.Filter.Coalitions) do --self:T3({ "Coalition:", MGroup:GetCoalition(), self.FilterMeta.Coalitions[CoalitionName], CoalitionName }) if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == MGroup:GetCoalition() then MGroupCoalition = true end end MGroupInclude = MGroupInclude and MGroupCoalition end if self.Filter.Categories and MGroupInclude then local MGroupCategory = false for CategoryID, CategoryName in pairs(self.Filter.Categories) do --self:I({ "Category:", MGroup:GetCategory(), self.FilterMeta.Categories[CategoryName], CategoryName }) if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == MGroup:GetCategory() then MGroupCategory = true end end MGroupInclude = MGroupInclude and MGroupCategory --self:I("Is Included: "..tostring(MGroupInclude)) end if self.Filter.Countries and MGroupInclude then local MGroupCountry = false for CountryID, CountryName in pairs(self.Filter.Countries) do --self:T3({ "Country:", MGroup:GetCountry(), CountryName }) if country.id[CountryName] == MGroup:GetCountry() then MGroupCountry = true end end MGroupInclude = MGroupInclude and MGroupCountry end if self.Filter.GroupPrefixes and MGroupInclude then local MGroupPrefix = false for GroupPrefixId, GroupPrefix in pairs(self.Filter.GroupPrefixes) do if self:_SearchPattern(MGroup:GetName(), GroupPrefix, self.filterNoRegex, self.filterReplaceDash) then MGroupPrefix = true end end MGroupInclude = MGroupInclude and MGroupPrefix end if self.Filter.Zones and MGroupInclude then local MGroupZone = false for ZoneName, Zone in pairs(self.Filter.Zones) do if MGroup:IsInZone(Zone) then MGroupZone = true end end MGroupInclude = MGroupInclude and MGroupZone end if self.Filter.Functions and MGroupInclude then local MGroupFunc = false MGroupFunc = self:_EvalFilterFunctions(MGroup) MGroupInclude = MGroupInclude and MGroupFunc end return MGroupInclude end --- Get the closest group of the set with respect to a given reference coordinate. Optionally, only groups of given coalitions are considered in the search. -- @param #SET_GROUP self -- @param Core.Point#COORDINATE Coordinate Reference Coordinate from which the closest group is determined. -- @param #table Coalitions (Optional) Table of coalition #number entries to filter for. -- @return Wrapper.Group#GROUP The closest group (if any). -- @return #number Distance in meters to the closest group. function SET_GROUP:GetClosestGroup(Coordinate, Coalitions) local Set = self:GetSet() local dmin=math.huge local gmin=nil for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP local group=GroupData --Wrapper.Group#GROUP if group and group:IsAlive() and (Coalitions==nil or UTILS.IsAnyInTable(Coalitions, group:GetCoalition())) then local coord=group:GetCoordinate() local d if coord ~= nil then -- Distance between ref. coordinate and group coordinate. d=UTILS.VecDist3D(Coordinate, coord) if d A map of the unit types found. The key is the UnitTypeName and the value is the amount of unit types found. function SET_UNIT:GetUnitTypes() --self:F2() local MT = {} -- Message Text local UnitTypes = {} for UnitID, UnitData in pairs(self:GetSet()) do local TextUnit = UnitData -- Wrapper.Unit#UNIT if TextUnit:IsAlive() then local UnitType = TextUnit:GetTypeName() if not UnitTypes[UnitType] then UnitTypes[UnitType] = 1 else UnitTypes[UnitType] = UnitTypes[UnitType] + 1 end end end for UnitTypeID, UnitType in pairs(UnitTypes) do MT[#MT + 1] = UnitType .. " of " .. UnitTypeID end return UnitTypes end --- Returns a comma separated string of the unit types with a count in the @{Core.Set}. -- @param #SET_UNIT self -- @return #string The unit types string function SET_UNIT:GetUnitTypesText() --self:F2() local MT = {} -- Message Text local UnitTypes = self:GetUnitTypes() for UnitTypeID, UnitType in pairs(UnitTypes) do MT[#MT + 1] = UnitType .. " of " .. UnitTypeID end return table.concat(MT, ", ") end --- Returns map of unit threat levels. -- @param #SET_UNIT self -- @return #table. function SET_UNIT:GetUnitThreatLevels() --self:F2() local UnitThreatLevels = {} for UnitID, UnitData in pairs(self:GetSet()) do local ThreatUnit = UnitData -- Wrapper.Unit#UNIT if ThreatUnit:IsAlive() then local UnitThreatLevel, UnitThreatLevelText = ThreatUnit:GetThreatLevel() local ThreatUnitName = ThreatUnit:GetName() UnitThreatLevels[UnitThreatLevel] = UnitThreatLevels[UnitThreatLevel] or {} UnitThreatLevels[UnitThreatLevel].UnitThreatLevelText = UnitThreatLevelText UnitThreatLevels[UnitThreatLevel].Units = UnitThreatLevels[UnitThreatLevel].Units or {} UnitThreatLevels[UnitThreatLevel].Units[ThreatUnitName] = ThreatUnit end end return UnitThreatLevels end --- Calculate the maximum A2G threat level of the SET_UNIT. -- @param #SET_UNIT self -- @return #number The maximum threat level function SET_UNIT:CalculateThreatLevelA2G() local MaxThreatLevelA2G = 0 local MaxThreatText = "" for UnitName, UnitData in pairs(self:GetSet()) do local ThreatUnit = UnitData -- Wrapper.Unit#UNIT local ThreatLevelA2G, ThreatText = ThreatUnit:GetThreatLevel() if ThreatLevelA2G > MaxThreatLevelA2G then MaxThreatLevelA2G = ThreatLevelA2G MaxThreatText = ThreatText end end --self:F({ MaxThreatLevelA2G = MaxThreatLevelA2G, MaxThreatText = MaxThreatText }) return MaxThreatLevelA2G, MaxThreatText end --- Get the center coordinate of the SET_UNIT. -- @param #SET_UNIT self -- @return Core.Point#COORDINATE The center coordinate of all the units in the set, including heading in degrees and speed in mps in case of moving units. function SET_UNIT:GetCoordinate() local function GetSetVec3(units) -- Init. local x=0 local y=0 local z=0 local n=0 -- Loop over all units. for _,unit in pairs(units) do local vec3=nil --DCS#Vec3 if unit and unit:IsAlive() then vec3 = unit:GetVec3() end if vec3 then -- Sum up posits. x=x+vec3.x y=y+vec3.y z=z+vec3.z -- Increase counter. n=n+1 end end if n>0 then -- Average. local Vec3={x=x/n, y=y/n, z=z/n} --DCS#Vec3 return Vec3 end return nil end local Coordinate = nil local Vec3 = GetSetVec3(self.Set) if Vec3 then Coordinate = COORDINATE:NewFromVec3(Vec3) end if Coordinate then local heading = self:GetHeading() or 0 local velocity = self:GetVelocity() or 0 Coordinate:SetHeading(heading) Coordinate:SetVelocity(velocity) --self:T(UTILS.PrintTableToLog(Coordinate)) end return Coordinate end --- Get the maximum velocity of the SET_UNIT. -- @param #SET_UNIT self -- @return #number The speed in mps in case of moving units. function SET_UNIT:GetVelocity() local Coordinate = self:GetFirst():GetCoordinate() local MaxVelocity = 0 for UnitName, UnitData in pairs(self:GetSet()) do local Unit = UnitData -- Wrapper.Unit#UNIT local Coordinate = Unit:GetCoordinate() local Velocity = Coordinate:GetVelocity() if Velocity ~= 0 then MaxVelocity = (MaxVelocity < Velocity) and Velocity or MaxVelocity end end --self:F({ MaxVelocity = MaxVelocity }) return MaxVelocity end --- Get the average heading of the SET_UNIT. -- @param #SET_UNIT self -- @return #number Heading Heading in degrees and speed in mps in case of moving units. function SET_UNIT:GetHeading() local HeadingSet = nil local MovingCount = 0 for UnitName, UnitData in pairs(self:GetSet()) do local Unit = UnitData -- Wrapper.Unit#UNIT local Coordinate = Unit:GetCoordinate() local Velocity = Coordinate:GetVelocity() if Velocity ~= 0 then local Heading = Coordinate:GetHeading() if HeadingSet == nil then HeadingSet = Heading else local HeadingDiff = (HeadingSet - Heading + 180 + 360) % 360 - 180 HeadingDiff = math.abs(HeadingDiff) if HeadingDiff > 5 then HeadingSet = nil break end end end end return HeadingSet end --- Returns if the @{Core.Set} has targets having a radar (of a given type). -- @param #SET_UNIT self -- @param DCS#Unit.RadarType RadarType -- @return #number The amount of radars in the Set with the given type function SET_UNIT:HasRadar(RadarType) --self:F2(RadarType) local RadarCount = 0 for UnitID, UnitData in pairs(self:GetSet()) do local UnitSensorTest = UnitData -- Wrapper.Unit#UNIT local HasSensors if RadarType then HasSensors = UnitSensorTest:HasSensors(Unit.SensorType.RADAR, RadarType) else HasSensors = UnitSensorTest:HasSensors(Unit.SensorType.RADAR) end --self:T3(HasSensors) if HasSensors then RadarCount = RadarCount + 1 end end return RadarCount end --- Returns if the @{Core.Set} has targets that can be SEADed. -- @param #SET_UNIT self -- @return #number The amount of SEADable units in the Set function SET_UNIT:HasSEAD() --self:F2() local SEADCount = 0 for UnitID, UnitData in pairs(self:GetSet()) do local UnitSEAD = UnitData -- Wrapper.Unit#UNIT if UnitSEAD:IsAlive() then local UnitSEADAttributes = UnitSEAD:GetDesc().attributes local HasSEAD = UnitSEAD:HasSEAD() --self:T3(HasSEAD) if HasSEAD then SEADCount = SEADCount + 1 end end end return SEADCount end --- Returns if the @{Core.Set} has ground targets. -- @param #SET_UNIT self -- @return #number The amount of ground targets in the Set. function SET_UNIT:HasGroundUnits() --self:F2() local GroundUnitCount = 0 for UnitID, UnitData in pairs(self:GetSet()) do local UnitTest = UnitData -- Wrapper.Unit#UNIT if UnitTest:IsGround() then GroundUnitCount = GroundUnitCount + 1 end end return GroundUnitCount end --- Returns if the @{Core.Set} has air targets. -- @param #SET_UNIT self -- @return #number The amount of air targets in the Set. function SET_UNIT:HasAirUnits() --self:F2() local AirUnitCount = 0 for UnitID, UnitData in pairs(self:GetSet()) do local UnitTest = UnitData -- Wrapper.Unit#UNIT if UnitTest:IsAir() then AirUnitCount = AirUnitCount + 1 end end return AirUnitCount end --- Returns if the @{Core.Set} has friendly ground units. -- @param #SET_UNIT self -- @return #number The amount of ground targets in the Set. function SET_UNIT:HasFriendlyUnits(FriendlyCoalition) --self:F2() local FriendlyUnitCount = 0 for UnitID, UnitData in pairs(self:GetSet()) do local UnitTest = UnitData -- Wrapper.Unit#UNIT if UnitTest:IsFriendly(FriendlyCoalition) then FriendlyUnitCount = FriendlyUnitCount + 1 end end return FriendlyUnitCount end ----- Iterate the SET_UNIT and call an iterator function for each **alive** player, providing the Unit of the player and optional parameters. -- @param #SET_UNIT self -- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_UNIT. The function needs to accept a UNIT parameter. ---- @return #SET_UNIT self -- function SET_UNIT:ForEachPlayer(IteratorFunction, ...) -- --self:F2(arg) -- -- self:ForEach(IteratorFunction, arg, self.PlayersAlive) -- -- return self -- end -- -- ----- Iterate the SET_UNIT and call an iterator function for each client, providing the Client to the function and optional parameters. -- @param #SET_UNIT self -- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_UNIT. The function needs to accept a CLIENT parameter. ---- @return #SET_UNIT self -- function SET_UNIT:ForEachClient(IteratorFunction, ...) -- --self:F2(arg) -- -- self:ForEach(IteratorFunction, arg, self.Clients) -- -- return self -- end --- -- @param #SET_UNIT self -- @param Wrapper.Unit#UNIT MUnit -- @return #SET_UNIT self function SET_UNIT:IsIncludeObject(MUnit) --self:F2({MUnit}) local MUnitInclude = false if MUnit:IsAlive() ~= nil then MUnitInclude = true if self.Filter.Active ~= nil then local MUnitActive = false if self.Filter.Active == false or (self.Filter.Active == true and MUnit:IsActive() == true) then MUnitActive = true end MUnitInclude = MUnitInclude and MUnitActive end if self.Filter.Coalitions and MUnitInclude then local MUnitCoalition = false for CoalitionID, CoalitionName in pairs(self.Filter.Coalitions) do --self:F({ "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 and MUnitInclude then local MUnitCategory = false for CategoryID, CategoryName in pairs(self.Filter.Categories) do --self:T3({ "Category:", MUnit:GetDesc().category, self.FilterMeta.Categories[CategoryName], CategoryName }) if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == MUnit:GetDesc().category then MUnitCategory = true end end MUnitInclude = MUnitInclude and MUnitCategory end if self.Filter.Types and MUnitInclude then local MUnitType = false for TypeID, TypeName in pairs(self.Filter.Types) do --self:T3({ "Type:", MUnit:GetTypeName(), TypeName }) if TypeName == MUnit:GetTypeName() then MUnitType = true end end MUnitInclude = MUnitInclude and MUnitType end if self.Filter.Countries and MUnitInclude then local MUnitCountry = false for CountryID, CountryName in pairs(self.Filter.Countries) do --self:T3({ "Country:", MUnit:GetCountry(), CountryName }) if country.id[CountryName] == MUnit:GetCountry() then MUnitCountry = true end end MUnitInclude = MUnitInclude and MUnitCountry end if self.Filter.UnitPrefixes and MUnitInclude then local MUnitPrefix = false for UnitPrefixId, UnitPrefix in pairs(self.Filter.UnitPrefixes) do if self:_SearchPattern(MUnit:GetName(), UnitPrefix, self.filterNoRegex, self.filterReplaceDash) then MUnitPrefix = true end end MUnitInclude = MUnitInclude and MUnitPrefix end if self.Filter.RadarTypes and MUnitInclude then local MUnitRadar = false for RadarTypeID, RadarType in pairs(self.Filter.RadarTypes) do --self:T3({ "Radar:", RadarType }) if MUnit:HasSensors(Unit.SensorType.RADAR, RadarType) == true then if MUnit:GetRadar() == true then -- This call is necessary to evaluate the SEAD capability. --self:T3("RADAR Found") end MUnitRadar = true end end MUnitInclude = MUnitInclude and MUnitRadar end if self.Filter.SEAD and MUnitInclude then local MUnitSEAD = false if MUnit:HasSEAD() == true then --self:T3("SEAD Found") MUnitSEAD = true end MUnitInclude = MUnitInclude and MUnitSEAD end end if self.Filter.Zones and MUnitInclude then local MGroupZone = false for ZoneName, Zone in pairs(self.Filter.Zones) do --self:T3("Zone:", ZoneName) if MUnit:IsInZone(Zone) then MGroupZone = true end end MUnitInclude = MUnitInclude and MGroupZone end if self.Filter.Functions and MUnitInclude then local MUnitFunc = self:_EvalFilterFunctions(MUnit) MUnitInclude = MUnitInclude and MUnitFunc end --self:T2(MUnitInclude) return MUnitInclude end --- Retrieve the type names of the @{Wrapper.Unit}s in the SET, delimited by an optional delimiter. -- @param #SET_UNIT self -- @param #string Delimiter (Optional) The delimiter, which is default a comma. -- @return #string The types of the @{Wrapper.Unit}s delimited. function SET_UNIT:GetTypeNames(Delimiter) Delimiter = Delimiter or ", " local TypeReport = REPORT:New() local Types = {} for UnitName, UnitData in pairs(self:GetSet()) do local Unit = UnitData -- Wrapper.Unit#UNIT local UnitTypeName = Unit:GetTypeName() if not Types[UnitTypeName] then Types[UnitTypeName] = UnitTypeName TypeReport:Add(UnitTypeName) end end return TypeReport:Text(Delimiter) end --- Iterate the SET_UNIT and set for each unit the default cargo bay weight limit. -- @param #SET_UNIT self -- @usage -- -- Set the default cargo bay weight limits of the carrier units. -- local MySetUnit = SET_UNIT:New() -- MySetUnit:SetCargoBayWeightLimit() function SET_UNIT:SetCargoBayWeightLimit() local Set = self:GetSet() for UnitID, UnitData in pairs(Set) do -- For each UNIT in SET_UNIT -- local UnitData = UnitData -- Wrapper.Unit#UNIT UnitData:SetCargoBayWeightLimit() end end end do -- SET_STATIC --- -- @type SET_STATIC -- @extends Core.Set#SET_BASE --- Mission designers can use the SET_STATIC class to build sets of Statics belonging to certain: -- -- * Coalitions -- * Categories -- * Countries -- * Static types -- * Starting with certain prefix strings. -- -- ## SET_STATIC constructor -- -- Create a new SET_STATIC object with the @{#SET_STATIC.New} method: -- -- * @{#SET_STATIC.New}: Creates a new SET_STATIC object. -- -- ## Add or Remove STATIC(s) from SET_STATIC -- -- STATICs can be added and removed using the @{Core.Set#SET_STATIC.AddStaticsByName} and @{Core.Set#SET_STATIC.RemoveStaticsByName} respectively. -- These methods take a single STATIC name or an array of STATIC names to be added or removed from SET_STATIC. -- -- ## SET_STATIC filter criteria -- -- You can set filter criteria to define the set of units within the SET_STATIC. -- Filter criteria are defined by: -- -- * @{#SET_STATIC.FilterCoalitions}: Builds the SET_STATIC with the units belonging to the coalition(s). -- * @{#SET_STATIC.FilterCategories}: Builds the SET_STATIC with the units belonging to the category(ies). -- * @{#SET_STATIC.FilterTypes}: Builds the SET_STATIC with the units belonging to the unit type(s). -- * @{#SET_STATIC.FilterCountries}: Builds the SET_STATIC with the units belonging to the country(ies). -- * @{#SET_STATIC.FilterPrefixes}: Builds the SET_STATIC with the units containing the same string(s) in their name. **Attention!** LUA regular expression apply here, so special characters in names like minus, dot, hash (#) etc might lead to unexpected results. -- Have a read through here to understand the application of regular expressions: [LUA regular expressions](https://riptutorial.com/lua/example/20315/lua-pattern-matching) -- * @{#SET_STATIC.FilterZones}: Builds the SET_STATIC with the units within a @{Core.Zone#ZONE}. -- * @{#SET_STATIC.FilterFunction}: Builds the SET_STATIC with a custom condition. -- -- Once the filter criteria have been set for the SET_STATIC, you can start filtering using: -- -- * @{#SET_STATIC.FilterStart}: Starts the filtering of the units within the SET_STATIC. -- -- ## SET_STATIC iterators -- -- Once the filters have been defined and the SET_STATIC has been built, you can iterate the SET_STATIC with the available iterator methods. -- The iterator methods will walk the SET_STATIC set, and call for each element within the set a function that you provide. -- The following iterator methods are currently available within the SET_STATIC: -- -- * @{#SET_STATIC.ForEachStatic}: Calls a function for each alive unit it finds within the SET_STATIC. -- * @{#SET_STATIC.ForEachStaticCompletelyInZone}: Iterate the SET_STATIC and call an iterator function for each **alive** STATIC presence completely in a @{Core.Zone}, providing the STATIC and optional parameters to the called function. -- * @{#SET_STATIC.ForEachStaticInZone}: Iterate the SET_STATIC and call an iterator function for each **alive** STATIC presence completely in a @{Core.Zone}, providing the STATIC and optional parameters to the called function. -- * @{#SET_STATIC.ForEachStaticNotInZone}: Iterate the SET_STATIC and call an iterator function for each **alive** STATIC presence not in a @{Core.Zone}, providing the STATIC and optional parameters to the called function. -- -- ## SET_STATIC atomic methods -- -- Various methods exist for a SET_STATIC to perform actions or calculations and retrieve results from the SET_STATIC: -- -- * @{#SET_STATIC.GetTypeNames}(): Retrieve the type names of the @{Wrapper.Static}s in the SET, delimited by a comma. -- -- === -- @field #SET_STATIC SET_STATIC SET_STATIC = { ClassName = "SET_STATIC", Statics = {}, Filter = { Coalitions = nil, Categories = nil, Types = nil, Countries = nil, StaticPrefixes = nil, Zones = 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_STATIC, ship = Unit.Category.SHIP, structure = Unit.Category.STRUCTURE, }, }, } --- Get the first unit from the set. -- @function [parent=#SET_STATIC] GetFirst -- @param #SET_STATIC self -- @return Wrapper.Static#STATIC The STATIC object. --- Creates a new SET_STATIC object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. -- @param #SET_STATIC self -- @return #SET_STATIC -- @usage -- -- Define a new SET_STATIC Object. This DBObject will contain a reference to all alive Statics. -- DBObject = SET_STATIC:New() function SET_STATIC:New() -- Inherits from BASE local self = BASE:Inherit(self, SET_BASE:New(_DATABASE.STATICS)) -- Core.Set#SET_STATIC --- Filter the set once -- @function [parent=#SET_STATIC] FilterOnce -- @param #SET_STATIC self -- @return #SET_STATIC self ---Set Regex Options for FilterPrefix function. -- @function [parent=#SET_STATIC] FilterSetRegex -- @param #SET_STATIC self -- @param #boolean NoRegex If true, switch off Regex pattern matching for FilterPrefixes. -- @param #boolean ReplaceDash If false, switch off dash "-" replacement in strings for FilterPrefixes. -- @return #SET_STATIC self --- Builds a set of objects of same coalitions. -- Possible current coalitions are red, blue and neutral. -- @function [parent=#SET_STATIC] FilterCoalitions -- @param #SET_STATIC self -- @param #string Coalitions Can take the following values: "red", "blue", "neutral" and coalition.side.RED, coalition.side.BLUE,coalition.side.NEUTRAL -- @param #boolean Clear If `true`, clear any previously defined filters. -- @return #SET_STATIC self return self end --- Add STATIC(s) to SET_STATIC. -- @param #SET_STATIC self -- @param Wrapper.Static#STATIC AddStatic A single STATIC. -- @return #SET_STATIC self function SET_STATIC:AddStatic(AddStatic) --self:F2(AddStatic:GetName()) self:Add(AddStatic:GetName(), AddStatic) return self end --- Add STATIC(s) to SET_STATIC. -- @param #SET_STATIC self -- @param #string AddStaticNames A single name or an array of STATIC names. -- @return #SET_STATIC self function SET_STATIC:AddStaticsByName(AddStaticNames) local AddStaticNamesArray = (type(AddStaticNames) == "table") and AddStaticNames or { AddStaticNames } --self:T((AddStaticNamesArray) for AddStaticID, AddStaticName in pairs(AddStaticNamesArray) do self:Add(AddStaticName, STATIC:FindByName(AddStaticName)) end return self end --- Remove STATIC(s) from SET_STATIC. -- @param Core.Set#SET_STATIC self -- @param Wrapper.Static#STATIC RemoveStaticNames A single name or an array of STATIC names. -- @return self function SET_STATIC:RemoveStaticsByName(RemoveStaticNames) local RemoveStaticNamesArray = (type(RemoveStaticNames) == "table") and RemoveStaticNames or { RemoveStaticNames } for RemoveStaticID, RemoveStaticName in pairs(RemoveStaticNamesArray) do self:Remove(RemoveStaticName) end return self end --- Finds a Static based on the Static Name. -- @param #SET_STATIC self -- @param #string StaticName -- @return Wrapper.Static#STATIC The found Static. function SET_STATIC:FindStatic(StaticName) local StaticFound = self.Set[StaticName] return StaticFound end --- Builds a set of units of coalitions. -- Possible current coalitions are red, blue and neutral. -- @param #SET_STATIC self -- @param #string Coalitions Can take the following values: "red", "blue", "neutral". -- @return #SET_STATIC self --- Builds a set of statics in zones. -- @param #SET_STATIC self -- @param #table Zones Table of Core.Zone#ZONE Zone objects, or a Core.Set#SET_ZONE -- @return #SET_STATIC self function SET_STATIC:FilterZones(Zones) if not self.Filter.Zones then self.Filter.Zones = {} end local zones = {} if Zones.ClassName and Zones.ClassName == "SET_ZONE" then zones = Zones.Set elseif type(Zones) ~= "table" or (type(Zones) == "table" and Zones.ClassName) then self:E("***** FilterZones needs either a table of ZONE Objects or a SET_ZONE as parameter!") return self else zones = Zones end for _,Zone in pairs(zones) do local zonename = Zone:GetName() self.Filter.Zones[zonename] = Zone end return self end --- Builds a set of units out of categories. -- Possible current categories are plane, helicopter, ground, ship. -- @param #SET_STATIC self -- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship". -- @return #SET_STATIC self function SET_STATIC: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_STATIC self -- @param #string Types Can take those type strings known within DCS world. -- @return #SET_STATIC self function SET_STATIC: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 --- [User] Add a custom condition function. -- @function [parent=#SET_STATIC] FilterFunction -- @param #SET_STATIC self -- @param #function ConditionFunction If this function returns `true`, the object is added to the SET. The function needs to take a STATIC object as first argument. -- @param ... Condition function arguments if any. -- @return #SET_STATIC self -- @usage -- -- Image you want to exclude a specific CLIENT from a SET: -- local groundset = SET_STATIC:New():FilterCoalitions("blue"):FilterActive(true):FilterFunction( -- -- The function needs to take a STATIC object as first - and in this case, only - argument. -- function(static) -- local isinclude = true -- if static:GetName() == "Exclude Me" then isinclude = false end -- return isinclude -- end -- ):FilterOnce() -- BASE:I(groundset:Flush()) --- Builds a set of units of defined countries. -- Possible current countries are those known within DCS world. -- @param #SET_STATIC self -- @param #string Countries Can take those country strings known within DCS world. -- @return #SET_STATIC self function SET_STATIC: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 STATICs that contain the given string in their name. -- **Attention!** Bad naming convention as this **does not** filter only **prefixes** but all statics that **contain** the string. -- @param #SET_STATIC self -- @param #string Prefixes The string pattern(s) that need to be contained in the static name. Can also be passed as a `#table` of strings. -- @return #SET_STATIC self function SET_STATIC:FilterPrefixes(Prefixes) if not self.Filter.StaticPrefixes then self.Filter.StaticPrefixes = {} end if type(Prefixes) ~= "table" then Prefixes = { Prefixes } end for PrefixID, Prefix in pairs(Prefixes) do self.Filter.StaticPrefixes[Prefix] = Prefix end return self end --- Starts the filtering. -- @param #SET_STATIC self -- @return #SET_STATIC self function SET_STATIC:FilterStart() if _DATABASE then self:_FilterStart() self:HandleEvent(EVENTS.Birth, self._EventOnBirth) self:HandleEvent(EVENTS.Dead, self._EventOnDeadOrCrash) self:HandleEvent(EVENTS.UnitLost, self._EventOnDeadOrCrash) end return self end --- Iterate the SET_STATIC and count how many STATICSs are alive. -- @param #SET_STATIC self -- @return #number The number of UNITs alive. function SET_STATIC:CountAlive() local Set = self:GetSet() local CountU = 0 for UnitID, UnitData in pairs(Set) do if UnitData and UnitData:IsAlive() then CountU = CountU + 1 end end return CountU end --- Handles the Database to check on an event (birth) that the Object was added in the Database. -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! -- @param #SET_STATIC self -- @param Core.Event#EVENTDATA Event -- @return #string The name of the STATIC -- @return #table The STATIC function SET_STATIC:AddInDatabase(Event) --self:F3({ Event }) if Event.IniObjectCategory == Object.Category.STATIC then if not self.Database[Event.IniDCSUnitName] then self.Database[Event.IniDCSUnitName] = STATIC:Register(Event.IniDCSUnitName) --self:T(3(self.Database[Event.IniDCSUnitName]) end end return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] end --- Handles the Database to check on any event that Object exists in the Database. -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! -- @param #SET_STATIC self -- @param Core.Event#EVENTDATA Event -- @return #string The name of the STATIC -- @return #table The STATIC function SET_STATIC:FindInDatabase(Event) --self:F2({ Event.IniDCSUnitName, self.Set[Event.IniDCSUnitName], Event }) return Event.IniDCSUnitName, self.Set[Event.IniDCSUnitName] end do -- Is Zone methods --- Check if minimal one element of the SET_STATIC is in the Zone. -- @param #SET_STATIC self -- @param Core.Zone#ZONE Zone The Zone to be tested for. -- @return #boolean function SET_STATIC:IsPartiallyInZone(Zone) local IsPartiallyInZone = false local function EvaluateZone(ZoneStatic) local ZoneStaticName = ZoneStatic:GetName() if self:FindStatic(ZoneStaticName) then IsPartiallyInZone = true return false end return true end return IsPartiallyInZone end --- Check if no element of the SET_STATIC is in the Zone. -- @param #SET_STATIC self -- @param Core.Zone#ZONE Zone The Zone to be tested for. -- @return #boolean function SET_STATIC:IsNotInZone(Zone) local IsNotInZone = true local function EvaluateZone(ZoneStatic) local ZoneStaticName = ZoneStatic:GetName() if self:FindStatic(ZoneStaticName) then IsNotInZone = false return false end return true end Zone:Search(EvaluateZone) return IsNotInZone end --- Check if minimal one element of the SET_STATIC is in the Zone. -- @param #SET_STATIC self -- @param #function IteratorFunction The function that will be called when there is an alive STATIC in the SET_STATIC. The function needs to accept a STATIC parameter. -- @return #SET_STATIC self function SET_STATIC:ForEachStaticInZone(IteratorFunction, ...) --self:F2(arg) self:ForEach(IteratorFunction, arg, self:GetSet()) return self end end --- Iterate the SET_STATIC and call an iterator function for each **alive** STATIC, providing the STATIC and optional parameters. -- @param #SET_STATIC self -- @param #function IteratorFunction The function that will be called when there is an alive STATIC in the SET_STATIC. The function needs to accept a STATIC parameter. -- @return #SET_STATIC self function SET_STATIC:ForEachStatic(IteratorFunction, ...) --self:F2(arg) self:ForEach(IteratorFunction, arg, self:GetSet()) return self end --- Iterate the SET_STATIC and call an iterator function for each **alive** STATIC presence completely in a @{Core.Zone}, providing the STATIC and optional parameters to the called function. -- @param #SET_STATIC self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. -- @param #function IteratorFunction The function that will be called when there is an alive STATIC in the SET_STATIC. The function needs to accept a STATIC parameter. -- @return #SET_STATIC self function SET_STATIC:ForEachStaticCompletelyInZone(ZoneObject, IteratorFunction, ...) --self:F2(arg) self:ForEach(IteratorFunction, arg, self:GetSet(), -- @param Core.Zone#ZONE_BASE ZoneObject -- @param Wrapper.Static#STATIC StaticObject function(ZoneObject, StaticObject) if StaticObject:IsInZone(ZoneObject) then return true else return false end end, { ZoneObject }) return self end --- Iterate the SET_STATIC and call an iterator function for each **alive** STATIC presence not in a @{Core.Zone}, providing the STATIC and optional parameters to the called function. -- @param #SET_STATIC self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. -- @param #function IteratorFunction The function that will be called when there is an alive STATIC in the SET_STATIC. The function needs to accept a STATIC parameter. -- @return #SET_STATIC self function SET_STATIC:ForEachStaticNotInZone(ZoneObject, IteratorFunction, ...) --self:F2(arg) self:ForEach(IteratorFunction, arg, self:GetSet(), -- @param Core.Zone#ZONE_BASE ZoneObject -- @param Wrapper.Static#STATIC StaticObject function(ZoneObject, StaticObject) if StaticObject:IsNotInZone(ZoneObject) then return true else return false end end, { ZoneObject }) return self end --- Returns map of unit types. -- @param #SET_STATIC self -- @return #map<#string,#number> A map of the unit types found. The key is the StaticTypeName and the value is the amount of unit types found. function SET_STATIC:GetStaticTypes() --self:F2() local MT = {} -- Message Text local StaticTypes = {} for StaticID, StaticData in pairs(self:GetSet()) do local TextStatic = StaticData -- Wrapper.Static#STATIC if TextStatic:IsAlive() then local StaticType = TextStatic:GetTypeName() if not StaticTypes[StaticType] then StaticTypes[StaticType] = 1 else StaticTypes[StaticType] = StaticTypes[StaticType] + 1 end end end for StaticTypeID, StaticType in pairs(StaticTypes) do MT[#MT + 1] = StaticType .. " of " .. StaticTypeID end return StaticTypes end --- Returns a comma separated string of the unit types with a count in the @{Core.Set}. -- @param #SET_STATIC self -- @return #string The unit types string function SET_STATIC:GetStaticTypesText() --self:F2() local MT = {} -- Message Text local StaticTypes = self:GetStaticTypes() for StaticTypeID, StaticType in pairs(StaticTypes) do MT[#MT + 1] = StaticType .. " of " .. StaticTypeID end return table.concat(MT, ", ") end --- Get the center coordinate of the SET_STATIC. -- @param #SET_STATIC self -- @return Core.Point#COORDINATE The center coordinate of all the units in the set, including heading in degrees and speed in mps in case of moving units. function SET_STATIC:GetCoordinate() local Coordinate = self:GetFirst():GetCoordinate() local x1 = Coordinate.x local x2 = Coordinate.x local y1 = Coordinate.y local y2 = Coordinate.y local z1 = Coordinate.z local z2 = Coordinate.z local MaxVelocity = 0 local AvgHeading = nil local MovingCount = 0 for StaticName, StaticData in pairs(self:GetSet()) do local Static = StaticData -- Wrapper.Static#STATIC local Coordinate = Static:GetCoordinate() x1 = (Coordinate.x < x1) and Coordinate.x or x1 x2 = (Coordinate.x > x2) and Coordinate.x or x2 y1 = (Coordinate.y < y1) and Coordinate.y or y1 y2 = (Coordinate.y > y2) and Coordinate.y or y2 z1 = (Coordinate.y < z1) and Coordinate.z or z1 z2 = (Coordinate.y > z2) and Coordinate.z or z2 local Velocity = Coordinate:GetVelocity() if Velocity ~= 0 then MaxVelocity = (MaxVelocity < Velocity) and Velocity or MaxVelocity local Heading = Coordinate:GetHeading() AvgHeading = AvgHeading and (AvgHeading + Heading) or Heading MovingCount = MovingCount + 1 end end AvgHeading = AvgHeading and (AvgHeading / MovingCount) Coordinate.x = (x2 - x1) / 2 + x1 Coordinate.y = (y2 - y1) / 2 + y1 Coordinate.z = (z2 - z1) / 2 + z1 Coordinate:SetHeading(AvgHeading) Coordinate:SetVelocity(MaxVelocity) --self:F({ Coordinate = Coordinate }) return Coordinate end --- Get the maximum velocity of the SET_STATIC. -- @param #SET_STATIC self -- @return #number The speed in mps in case of moving units. function SET_STATIC:GetVelocity() return 0 end --- Get the average heading of the SET_STATIC. -- @param #SET_STATIC self -- @return #number Heading Heading in degrees and speed in mps in case of moving units. function SET_STATIC:GetHeading() local HeadingSet = nil local MovingCount = 0 for StaticName, StaticData in pairs(self:GetSet()) do local Static = StaticData -- Wrapper.Static#STATIC local Coordinate = Static:GetCoordinate() local Velocity = Coordinate:GetVelocity() if Velocity ~= 0 then local Heading = Coordinate:GetHeading() if HeadingSet == nil then HeadingSet = Heading else local HeadingDiff = (HeadingSet - Heading + 180 + 360) % 360 - 180 HeadingDiff = math.abs(HeadingDiff) if HeadingDiff > 5 then HeadingSet = nil break end end end end return HeadingSet end --- Calculate the maximum A2G threat level of the SET_STATIC. -- @param #SET_STATIC self -- @return #number The maximum threatlevel function SET_STATIC:CalculateThreatLevelA2G() local MaxThreatLevelA2G = 0 local MaxThreatText = "" for StaticName, StaticData in pairs(self:GetSet()) do local ThreatStatic = StaticData -- Wrapper.Static#STATIC local ThreatLevelA2G, ThreatText = ThreatStatic:GetThreatLevel() if ThreatLevelA2G > MaxThreatLevelA2G then MaxThreatLevelA2G = ThreatLevelA2G MaxThreatText = ThreatText end end --self:F({ MaxThreatLevelA2G = MaxThreatLevelA2G, MaxThreatText = MaxThreatText }) return MaxThreatLevelA2G, MaxThreatText end --- -- @param #SET_STATIC self -- @param Wrapper.Static#STATIC MStatic -- @return #SET_STATIC self function SET_STATIC:IsIncludeObject(MStatic) --self:F2(MStatic) local MStaticInclude = true if self.Filter.Coalitions then local MStaticCoalition = false for CoalitionID, CoalitionName in pairs(self.Filter.Coalitions) do --self:T(3({ "Coalition:", MStatic:GetCoalition(), self.FilterMeta.Coalitions[CoalitionName], CoalitionName }) if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == MStatic:GetCoalition() then MStaticCoalition = true end end MStaticInclude = MStaticInclude and MStaticCoalition end if self.Filter.Categories then local MStaticCategory = false for CategoryID, CategoryName in pairs(self.Filter.Categories) do --self:T(3({ "Category:", MStatic:GetDesc().category, self.FilterMeta.Categories[CategoryName], CategoryName }) if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == MStatic:GetDesc().category then MStaticCategory = true end end MStaticInclude = MStaticInclude and MStaticCategory end if self.Filter.Types then local MStaticType = false for TypeID, TypeName in pairs(self.Filter.Types) do --self:T(3({ "Type:", MStatic:GetTypeName(), TypeName }) if TypeName == MStatic:GetTypeName() then MStaticType = true end end MStaticInclude = MStaticInclude and MStaticType end if self.Filter.Countries then local MStaticCountry = false for CountryID, CountryName in pairs(self.Filter.Countries) do --self:T(3({ "Country:", MStatic:GetCountry(), CountryName }) if country.id[CountryName] == MStatic:GetCountry() then MStaticCountry = true end end MStaticInclude = MStaticInclude and MStaticCountry end if self.Filter.StaticPrefixes then local MStaticPrefix = false for StaticPrefixId, StaticPrefix in pairs(self.Filter.StaticPrefixes) do if self:_SearchPattern(MStatic:GetName(), StaticPrefix, self.filterNoRegex, self.filterReplaceDash) then MStaticPrefix = true end end MStaticInclude = MStaticInclude and MStaticPrefix end if self.Filter.Zones then local MStaticZone = false for ZoneName, Zone in pairs(self.Filter.Zones) do --self:T(3("Zone:", ZoneName) if MStatic and MStatic:IsInZone(Zone) then MStaticZone = true end end MStaticInclude = MStaticInclude and MStaticZone end if self.Filter.Functions and MStaticInclude then local MClientFunc = self:_EvalFilterFunctions(MStatic) MStaticInclude = MStaticInclude and MClientFunc end --self:T(2(MStaticInclude) return MStaticInclude end --- Retrieve the type names of the @{Wrapper.Static}s in the SET, delimited by an optional delimiter. -- @param #SET_STATIC self -- @param #string Delimiter (Optional) The delimiter, which is default a comma. -- @return #string The types of the @{Wrapper.Static}s delimited. function SET_STATIC:GetTypeNames(Delimiter) Delimiter = Delimiter or ", " local TypeReport = REPORT:New() local Types = {} for StaticName, StaticData in pairs(self:GetSet()) do local Static = StaticData -- Wrapper.Static#STATIC local StaticTypeName = Static:GetTypeName() if not Types[StaticTypeName] then Types[StaticTypeName] = StaticTypeName TypeReport:Add(StaticTypeName) end end return TypeReport:Text(Delimiter) end --- Get the closest static of the set with respect to a given reference coordinate. Optionally, only statics of given coalitions are considered in the search. -- @param #SET_STATIC self -- @param Core.Point#COORDINATE Coordinate Reference Coordinate from which the closest static is determined. -- @return Wrapper.Static#STATIC The closest static (if any). -- @return #number Distance in meters to the closest static. function SET_STATIC:GetClosestStatic(Coordinate, Coalitions) local Set = self:GetSet() local dmin=math.huge local gmin=nil for GroupID, GroupData in pairs(Set) do -- For each STATIC in SET_STATIC local group=GroupData --Wrapper.Static#STATIC if group and group:IsAlive() and (Coalitions==nil or UTILS.IsAnyInTable(Coalitions, group:GetCoalition())) then local coord=group:GetCoord() -- Distance between ref. coordinate and group coordinate. local d=UTILS.VecDist3D(Coordinate, coord) if d 1 then x = x/count y = y/count z = z/count end local coord = COORDINATE:New(x,y,z) return coord end --- Private function. -- @param #SET_ZONE self -- @param Core.Zone#ZONE_BASE MZone -- @return #SET_ZONE self function SET_ZONE:IsIncludeObject(MZone) --self:F2(MZone) local MZoneInclude = true if MZone then local MZoneName = MZone:GetName() if self.Filter.Prefixes then local MZonePrefix = false for ZonePrefixId, ZonePrefix in pairs(self.Filter.Prefixes) do if self:_SearchPattern(MZoneName, ZonePrefix, self.filterNoRegex, self.filterReplaceDash) then MZonePrefix = true end end MZoneInclude = MZoneInclude and MZonePrefix end end if self.Filter.Functions and MZoneInclude then local MClientFunc = self:_EvalFilterFunctions(MZone) MZoneInclude = MZoneInclude and MClientFunc end --self:T(2(MZoneInclude) return MZoneInclude end --- Handles the OnEventNewZone event for the Set. -- @param #SET_ZONE self -- @param Core.Event#EVENTDATA EventData function SET_ZONE:OnEventNewZone(EventData) -- R2.1 --self:F({ "New Zone", EventData }) if EventData.Zone then if EventData.Zone and self:IsIncludeObject(EventData.Zone) then self:Add(EventData.Zone.ZoneName, EventData.Zone) end end end --- Handles the OnDead or OnCrash event for alive units set. -- @param #SET_ZONE self -- @param Core.Event#EVENTDATA EventData function SET_ZONE:OnEventDeleteZone(EventData) -- R2.1 --self:F3({ EventData }) if EventData.Zone then local Zone = _DATABASE:FindZone(EventData.Zone.ZoneName) if Zone and Zone.ZoneName then if Zone.NoDestroy then else self:Remove(Zone.ZoneName) end end end end --- Validate if a coordinate is in one of the zones in the set. -- Returns the ZONE object where the coordinate is located. -- If zones overlap, the first zone that validates the test is returned. -- @param #SET_ZONE self -- @param Core.Point#COORDINATE Coordinate The coordinate to be searched. -- @return Core.Zone#ZONE_BASE The zone (if any) that validates the coordinate location. function SET_ZONE:IsCoordinateInZone(Coordinate) for _, Zone in pairs(self:GetSet()) do local Zone = Zone -- Core.Zone#ZONE_BASE if Zone:IsCoordinateInZone(Coordinate) then return Zone end end return nil end --- Get the closest zone to a given coordinate. -- @param #SET_ZONE self -- @param Core.Point#COORDINATE Coordinate The reference coordinate from which the closest zone is determined. -- @return Core.Zone#ZONE_BASE The closest zone (if any). -- @return #number Distance to ref coordinate in meters. function SET_ZONE:GetClosestZone(Coordinate) local dmin=math.huge local zmin=nil for _, Zone in pairs(self:GetSet()) do local Zone = Zone -- Core.Zone#ZONE_BASE local d=Zone:Get2DDistance(Coordinate) if d x2) and Coordinate.x or x2 y1 = (Coordinate.y < y1) and Coordinate.y or y1 y2 = (Coordinate.y > y2) and Coordinate.y or y2 z1 = (Coordinate.y < z1) and Coordinate.z or z1 z2 = (Coordinate.y > z2) and Coordinate.z or z2 end Coordinate.x = (x2 - x1) / 2 + x1 Coordinate.y = (y2 - y1) / 2 + y1 Coordinate.z = (z2 - z1) / 2 + z1 --self:F({ Coordinate = Coordinate }) return Coordinate end --- [Internal] Determine if an object is to be included in the SET -- @param #SET_SCENERY self -- @param Wrapper.Scenery#SCENERY MScenery -- @return #SET_SCENERY self function SET_SCENERY:IsIncludeObject(MScenery) --self:T((MScenery.SceneryName) local MSceneryInclude = true if MScenery then local MSceneryName = MScenery:GetName() -- Filter Prefixes if self.Filter.Prefixes then local MSceneryPrefix = false for ZonePrefixId, ZonePrefix in pairs(self.Filter.Prefixes) do if self:_SearchPattern(MSceneryName, ZonePrefix, self.filterNoRegex, self.filterReplaceDash) then MSceneryPrefix = true end end MSceneryInclude = MSceneryInclude and MSceneryPrefix end if self.Filter.Zones then local MSceneryZone = false for ZoneName, Zone in pairs(self.Filter.Zones) do --self:T(("Zone:", ZoneName) local coord = MScenery:GetCoordinate() if coord and Zone:IsCoordinateInZone(coord) then MSceneryZone = true end --self:T(({ "Evaluated Zone", MSceneryZone }) end MSceneryInclude = MSceneryInclude and MSceneryZone end -- Filter Roles if self.Filter.SceneryRoles then local MSceneryRole = false local Role = MScenery:GetProperty("ROLE") or "none" for ZoneRoleId, ZoneRole in pairs(self.Filter.SceneryRoles) do --self:T(({ "Role:", ZoneRole, Role }) if ZoneRole == Role then MSceneryRole = true end end --self:T(({ "Evaluated Role ", MSceneryRole }) MSceneryInclude = MSceneryInclude and MSceneryRole end end if self.Filter.Functions and MSceneryInclude then local MClientFunc = self:_EvalFilterFunctions(MScenery) MSceneryInclude = MSceneryInclude and MClientFunc end --self:T(2(MSceneryInclude) return MSceneryInclude end --- Filters for the defined collection. -- @param #SET_SCENERY self -- @return #SET_SCENERY self function SET_SCENERY:FilterOnce() for ObjectName, Object in pairs(self:GetSet()) do --self:T((ObjectName) if self:IsIncludeObject(Object) then self:Add(ObjectName, Object) else self:Remove(ObjectName, true) end end return self --FilteredSet end --- Count overall initial (Life0) lifepoints of the SET objects. -- @param #SET_SCENERY self -- @return #number LIfe0Points function SET_SCENERY:GetLife0() local life0 = 0 self:ForEachScenery( function(obj) local Obj = obj -- Wrapper.Scenery#SCENERY life0 = life0 + Obj:GetLife0() end ) return life0 end --- Count overall current lifepoints of the SET objects. -- @param #SET_SCENERY self -- @return #number LifePoints function SET_SCENERY:GetLife() local life = 0 self:ForEachScenery( function(obj) local Obj = obj -- Wrapper.Scenery#SCENERY life = life + Obj:GetLife() end ) return life end --- Calculate current relative lifepoints of the SET objects, i.e. Life divided by Life0 as percentage value, eg 75 meaning 75% alive. -- **CAVEAT**: Some objects change their life value or "hitpoints" **after** the first hit. Hence we will adjust the Life0 value to 120% -- of the last life value if life exceeds life0 ata any point. -- Thus we will get a smooth percentage decrease, if you use this e.g. as success criteria for a bombing task. -- @param #SET_SCENERY self -- @return #number LifePoints function SET_SCENERY:GetRelativeLife() local life = self:GetLife() local life0 = self:GetLife0() --self:T(2(string.format("Set Lifepoints: %d life0 | %d life",life0,life)) local rlife = math.floor((life / life0) * 100) return rlife end end -- TODO SET_DYNAMICCARGO do -- SET_DYNAMICCARGO --- -- @type SET_DYNAMICCARGO -- @field #table Filter Table of filters. -- @field #table Set Table of objects. -- @field #table Index Table of indices. -- @field #table List Unused table. -- @field Core.Scheduler#SCHEDULER CallScheduler. -- @field #SET_DYNAMICCARGO.Filters Filter Filters. -- @field #number ZoneTimerInterval. -- @field Core.Timer#TIMER ZoneTimer Timer for active filtering of zones. -- @extends Core.Set#SET_BASE --- -- @type SET_DYNAMICCARGO.Filters -- @field #string Coalitions -- @field #string Types -- @field #string Countries -- @field #string StaticPrefixes -- @field #string Zones --- The @{Core.Set#SET_DYNAMICCARGO} class defines the functions that define a collection of objects form @{Wrapper.DynamicCargo#DYNAMICCARGO}. -- A SET provides iterators to iterate the SET. --- Mission designers can use the SET_DYNAMICCARGO class to build sets of cargos belonging to certain: -- -- * Coalitions -- * Categories -- * Countries -- * Static types -- * Starting with certain prefix strings. -- * Etc. -- -- ## SET_DYNAMICCARGO constructor -- -- Create a new SET_DYNAMICCARGO object with the @{#SET_DYNAMICCARGO.New} method: -- -- * @{#SET_DYNAMICCARGO.New}: Creates a new SET_DYNAMICCARGO object. -- -- ## SET_DYNAMICCARGO filter criteria -- -- You can set filter criteria to define the set of objects within the SET_DYNAMICCARGO. -- Filter criteria are defined by: -- -- * @{#SET_DYNAMICCARGO.FilterCoalitions}: Builds the SET_DYNAMICCARGO with the objects belonging to the coalition(s). -- * @{#SET_DYNAMICCARGO.FilterTypes}: Builds the SET_DYNAMICCARGO with the cargos belonging to the statiy type name(s). -- * @{#SET_DYNAMICCARGO.FilterCountries}: Builds the SET_DYNAMICCARGO with the objects belonging to the country(ies). -- * @{#SET_DYNAMICCARGO.FilterNamePatterns}, @{#SET_DYNAMICCARGO.FilterPrefixes}: Builds the SET_DYNAMICCARGO with the cargo containing the same string(s) in their name. **Attention!** LUA regular expression apply here, so special characters in names like minus, dot, hash (#) etc might lead to unexpected results. -- Have a read through here to understand the application of regular expressions: [LUA regular expressions](https://riptutorial.com/lua/example/20315/lua-pattern-matching) -- * @{#SET_DYNAMICCARGO.FilterZones}: Builds the SET_DYNAMICCARGO with the cargo within a @{Core.Zone#ZONE}. -- * @{#SET_DYNAMICCARGO.FilterFunction}: Builds the SET_DYNAMICCARGO with a custom condition. -- * @{#SET_DYNAMICCARGO.FilterCurrentOwner}: Builds the SET_DYNAMICCARGO with a specific owner name. -- * @{#SET_DYNAMICCARGO.FilterIsLoaded}: Builds the SET_DYNAMICCARGO which is in state LOADED. -- * @{#SET_DYNAMICCARGO.FilterIsNew}: Builds the SET_DYNAMICCARGO with is in state NEW. -- * @{#SET_DYNAMICCARGO.FilterIsUnloaded}: Builds the SET_DYNAMICCARGO with is in state UNLOADED. -- -- Once the filter criteria have been set for the SET\_DYNAMICCARGO, you can start and stop filtering using: -- -- * @{#SET_DYNAMICCARGO.FilterStart}: Starts the continous filtering of the objects within the SET_DYNAMICCARGO. -- * @{#SET_DYNAMICCARGO.FilterStop}: Stops the continous filtering of the objects within the SET_DYNAMICCARGO. -- * @{#SET_DYNAMICCARGO.FilterOnce}: Filters once for the objects within the SET_DYNAMICCARGO. -- -- ## SET_DYNAMICCARGO iterators -- -- Once the filters have been defined and the SET\_DYNAMICCARGO has been built, you can iterate the SET\_DYNAMICCARGO with the available iterator methods. -- The iterator methods will walk the SET\_DYNAMICCARGO set, and call for each element within the set a function that you provide. -- The following iterator methods are currently available within the SET\_DYNAMICCARGO: -- -- * @{#SET_DYNAMICCARGO.ForEach}: Calls a function for each alive dynamic cargo it finds within the SET\_DYNAMICCARGO. -- -- ## SET_DYNAMICCARGO atomic methods -- -- Various methods exist for a SET_DYNAMICCARGO to perform actions or calculations and retrieve results from the SET\_DYNAMICCARGO: -- -- * @{#SET_DYNAMICCARGO.GetOwnerClientObjects}(): Retrieve the type names of the @{Wrapper.Static}s in the SET, delimited by a comma. -- * @{#SET_DYNAMICCARGO.GetOwnerNames}(): Retrieve the type names of the @{Wrapper.Static}s in the SET, delimited by a comma. -- * @{#SET_DYNAMICCARGO.GetStorageObjects}(): Retrieve the type names of the @{Wrapper.Static}s in the SET, delimited by a comma. -- -- === -- @field #SET_DYNAMICCARGO SET_DYNAMICCARGO SET_DYNAMICCARGO = { ClassName = "SET_DYNAMICCARGO", Set = {}, List = {}, Index = {}, Database = nil, CallScheduler = nil, Filter = { Coalitions = nil, Types = nil, Countries = nil, StaticPrefixes = nil, Zones = nil, }, FilterMeta = { Coalitions = { red = coalition.side.RED, blue = coalition.side.BLUE, neutral = coalition.side.NEUTRAL, } }, ZoneTimerInterval = 20, ZoneTimer = nil, } --- Creates a new SET_DYNAMICCARGO object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. -- @param #SET_DYNAMICCARGO self -- @return #SET_DYNAMICCARGO -- @usage -- -- Define a new SET_DYNAMICCARGO Object. This DBObject will contain a reference to all alive Statics. -- DBObject = SET_DYNAMICCARGO:New() function SET_DYNAMICCARGO:New() --- Inherits from BASE local self = BASE:Inherit(self, SET_BASE:New(_DATABASE.DYNAMICCARGO)) -- Core.Set#SET_DYNAMICCARGO --- Filter the set once -- @function [parent=#SET_DYNAMICCARGO] FilterOnce -- @param #SET_DYNAMICCARGO self -- @return #SET_DYNAMICCARGO self ---Set Regex Options for FilterPrefix function. -- @function [parent=#SET_DYNAMICCARGO] FilterSetRegex -- @param #SET_DYNAMICCARGO self -- @param #boolean NoRegex If true, switch off Regex pattern matching for FilterPrefixes. -- @param #boolean ReplaceDash If false, switch off dash "-" replacement in strings for FilterPrefixes. -- @return #SET_DYNAMICCARGO self return self end --- -- @param #SET_DYNAMICCARGO self -- @param Wrapper.DynamicCargo#DYNAMICCARGO DCargo -- @return #SET_DYNAMICCARGO self function SET_DYNAMICCARGO:IsIncludeObject(DCargo) --self:F2(DCargo) local DCargoInclude = true if self.Filter.Coalitions then local DCargoCoalition = false for CoalitionID, CoalitionName in pairs(self.Filter.Coalitions) do --self:T2({ "Coalition:", DCargo:GetCoalition(), self.FilterMeta.Coalitions[CoalitionName], CoalitionName }) if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == DCargo:GetCoalition() then DCargoCoalition = true end end DCargoInclude = DCargoInclude and DCargoCoalition end if self.Filter.Types then local DCargoType = false for TypeID, TypeName in pairs(self.Filter.Types) do --self:T2({ "Type:", DCargo:GetTypeName(), TypeName }) if TypeName == DCargo:GetTypeName() then DCargoType = true end end DCargoInclude = DCargoInclude and DCargoType end if self.Filter.Countries then local DCargoCountry = false for CountryID, CountryName in pairs(self.Filter.Countries) do --self:T2({ "Country:", DCargo:GetCountry(), CountryName }) if country.id[CountryName] == DCargo:GetCountry() then DCargoCountry = true end end DCargoInclude = DCargoInclude and DCargoCountry end if self.Filter.StaticPrefixes then local DCargoPrefix = false for StaticPrefixId, StaticPrefix in pairs(self.Filter.StaticPrefixes) do if self:_SearchPattern(DCargo:GetName(), StaticPrefix, self.filterNoRegex, self.filterReplaceDash) then DCargoPrefix = true end end DCargoInclude = DCargoInclude and DCargoPrefix end if self.Filter.Zones then local DCargoZone = false for ZoneName, Zone in pairs(self.Filter.Zones) do --self:T2("In zone: "..ZoneName) if DCargo and DCargo:IsInZone(Zone) then DCargoZone = true end end DCargoInclude = DCargoInclude and DCargoZone end if self.Filter.Functions and DCargoInclude then local MClientFunc = self:_EvalFilterFunctions(DCargo) DCargoInclude = DCargoInclude and MClientFunc end --self:T2(DCargoInclude) return DCargoInclude end --- Builds a set of dynamic cargo of defined coalitions. -- Possible current coalitions are red, blue and neutral. -- @param #SET_DYNAMICCARGO self -- @param #string Coalitions Can take the following values: "red", "blue", "neutral". -- @return #SET_DYNAMICCARGO self --- Builds a set of dynamic cargo of defined dynamic cargo type names. -- @param #SET_DYNAMICCARGO self -- @param #string Types Can take those type name strings known within DCS world. -- @return #SET_DYNAMICCARGO self function SET_DYNAMICCARGO: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 --- [User] Add a custom condition function. -- @function [parent=#SET_DYNAMICCARGO] FilterFunction -- @param #SET_DYNAMICCARGO self -- @param #function ConditionFunction If this function returns `true`, the object is added to the SET. The function needs to take a DYNAMICCARGO object as first argument. -- @param ... Condition function arguments if any. -- @return #SET_DYNAMICCARGO self -- @usage -- -- Image you want to exclude a specific DYNAMICCARGO from a SET: -- local cargoset = SET_DYNAMICCARGO:New():FilterCoalitions("blue"):FilterFunction( -- -- The function needs to take a DYNAMICCARGO object as first - and in this case, only - argument. -- function(dynamiccargo) -- local isinclude = true -- if dynamiccargo:GetName() == "Exclude Me" then isinclude = false end -- return isinclude -- end -- ):FilterOnce() -- BASE:I(cargoset:Flush()) --- Builds a set of dynamic cargo of defined countries. -- Possible current countries are those known within DCS world. -- @param #SET_DYNAMICCARGO self -- @param #string Countries Can take those country strings known within DCS world. -- @return #SET_DYNAMICCARGO self function SET_DYNAMICCARGO: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 DYNAMICCARGOs that contain the given string in their name. -- **Attention!** Bad naming convention as this **does not** filter only **prefixes** but all names that **contain** the string. LUA Regex applies. -- @param #SET_DYNAMICCARGO self -- @param #string Prefixes The string pattern(s) that need to be contained in the dynamic cargo name. Can also be passed as a `#table` of strings. -- @return #SET_DYNAMICCARGO self function SET_DYNAMICCARGO:FilterPrefixes(Prefixes) if not self.Filter.StaticPrefixes then self.Filter.StaticPrefixes = {} end if type(Prefixes) ~= "table" then Prefixes = { Prefixes } end for PrefixID, Prefix in pairs(Prefixes) do self.Filter.StaticPrefixes[Prefix] = Prefix end return self end --- Builds a set of DYNAMICCARGOs that contain the given string in their name. -- **Attention!** LUA Regex applies! -- @param #SET_DYNAMICCARGO self -- @param #string Patterns The string pattern(s) that need to be contained in the dynamic cargo name. Can also be passed as a `#table` of strings. -- @return #SET_DYNAMICCARGO self function SET_DYNAMICCARGO:FilterNamePattern(Patterns) return self:FilterPrefixes(Patterns) end --- Builds a set of DYNAMICCARGOs that are in state DYNAMICCARGO.State.LOADED (i.e. is on board of a Chinook). -- @param #SET_DYNAMICCARGO self -- @return #SET_DYNAMICCARGO self function SET_DYNAMICCARGO:FilterIsLoaded() self:FilterFunction( function(cargo) if cargo and cargo.CargoState and cargo.CargoState == DYNAMICCARGO.State.LOADED then return true else return false end end ) return self end --- Builds a set of DYNAMICCARGOs that are in state DYNAMICCARGO.State.LOADED (i.e. was on board of a Chinook previously and is now unloaded). -- @param #SET_DYNAMICCARGO self -- @return #SET_DYNAMICCARGO self function SET_DYNAMICCARGO:FilterIsUnloaded() self:FilterFunction( function(cargo) if cargo and cargo.CargoState and cargo.CargoState == DYNAMICCARGO.State.UNLOADED then return true else return false end end ) return self end --- Builds a set of DYNAMICCARGOs that are in state DYNAMICCARGO.State.NEW (i.e. new and never loaded into a Chinook). -- @param #SET_DYNAMICCARGO self -- @return #SET_DYNAMICCARGO self function SET_DYNAMICCARGO:FilterIsNew() self:FilterFunction( function(cargo) if cargo and cargo.CargoState and cargo.CargoState == DYNAMICCARGO.State.NEW then return true else return false end end ) return self end --- Builds a set of DYNAMICCARGOs that are owned at the moment by this player name. -- @param #SET_DYNAMICCARGO self -- @param #string PlayerName -- @return #SET_DYNAMICCARGO self function SET_DYNAMICCARGO:FilterCurrentOwner(PlayerName) self:FilterFunction( function(cargo) if cargo and cargo.Owner and self:_SearchPattern(cargo.Owner, PlayerName, self.filterNoRegex, self.filterReplaceDash) then return true else return false end end ) return self end --- Builds a set of dynamic cargo in zones. -- @param #SET_DYNAMICCARGO self -- @param #table Zones Table of Core.Zone#ZONE Zone objects, or a Core.Set#SET_ZONE -- @return #SET_DYNAMICCARGO self function SET_DYNAMICCARGO:FilterZones(Zones) if not self.Filter.Zones then self.Filter.Zones = {} end local zones = {} if Zones.ClassName and Zones.ClassName == "SET_ZONE" then zones = Zones.Set elseif type(Zones) ~= "table" or (type(Zones) == "table" and Zones.ClassName) then self:E("***** FilterZones needs either a table of ZONE Objects or a SET_ZONE as parameter!") return self else zones = Zones end for _,Zone in pairs(zones) do local zonename = Zone:GetName() self.Filter.Zones[zonename] = Zone end return self end --- Starts the filtering. -- @param #SET_DYNAMICCARGO self -- @return #SET_DYNAMICCARGO self function SET_DYNAMICCARGO:FilterStart() if _DATABASE then self:HandleEvent(EVENTS.NewDynamicCargo, self._EventHandlerDCAdd) self:HandleEvent(EVENTS.DynamicCargoRemoved, self._EventHandlerDCRemove) if self.Filter.Zones then self.ZoneTimer = TIMER:New(self._ContinousZoneFilter,self) local timing = self.ZoneTimerInterval or 30 self.ZoneTimer:Start(timing,timing) end self:_FilterStart() end return self end --- Stops the filtering. -- @param #SET_DYNAMICCARGO self -- @return #SET_DYNAMICCARGO self function SET_DYNAMICCARGO:FilterStop() if _DATABASE then self:UnHandleEvent(EVENTS.NewDynamicCargo) self:UnHandleEvent(EVENTS.DynamicCargoRemoved) if self.ZoneTimer and self.ZoneTimer:IsRunning() then self.ZoneTimer:Stop() end end return self end --- [Internal] Private function for use of continous zone filter -- @param #SET_DYNAMICCARGO self -- @return #SET_DYNAMICCARGO self function SET_DYNAMICCARGO:_ContinousZoneFilter() local Database = _DATABASE.DYNAMICCARGO for ObjectName, Object in pairs(Database) do if self:IsIncludeObject(Object) and self:IsNotInSet(Object) then self:Add(ObjectName, Object) elseif (not self:IsIncludeObject(Object)) and self:IsInSet(Object) then self:Remove(ObjectName) end end return self end --- Handles the events for the Set. -- @param #SET_DYNAMICCARGO self -- @param Core.Event#EVENTDATA Event function SET_DYNAMICCARGO:_EventHandlerDCAdd(Event) if Event.IniDynamicCargo and Event.IniDynamicCargoName then if not _DATABASE.DYNAMICCARGO[Event.IniDynamicCargoName] then _DATABASE:AddDynamicCargo(Event.IniDynamicCargoName) end local ObjectName, Object = self:FindInDatabase(Event) if Object and self:IsIncludeObject(Object) then self:Add(ObjectName, Object) end end return self end --- Handles the remove event for dynamic cargo set. -- @param #SET_DYNAMICCARGO self -- @param Core.Event#EVENTDATA Event function SET_DYNAMICCARGO:_EventHandlerDCRemove(Event) if Event.IniDCSUnitName then local ObjectName, Object = self:FindInDatabase(Event) if ObjectName then self:Remove(ObjectName) end end return self 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_DYNAMICCARGO event or vise versa! -- @param #SET_DYNAMICCARGO self -- @param Core.Event#EVENTDATA Event -- @return #string The name of the DYNAMICCARGO -- @return Wrapper.DynamicCargo#DYNAMICCARGO The DYNAMICCARGO object function SET_DYNAMICCARGO:FindInDatabase(Event) return Event.IniDCSUnitName, self.Set[Event.IniDCSUnitName] end --- Set filter timer interval for FilterZones if using active filtering with FilterStart(). -- @param #SET_DYNAMICCARGO self -- @param #number Seconds (Optional) Seconds between check intervals, defaults to 30. **Caution** - do not be too agressive with timing! Objects are usually not moving fast enough -- to warrant a check of below 10 seconds. -- @return #SET_DYNAMICCARGO self function SET_DYNAMICCARGO:FilterZoneTimer(Seconds) self.ZoneTimerInterval = Seconds or 30 return self end --- This filter is N/A for SET_DYNAMICCARGO -- @param #SET_DYNAMICCARGO self -- @return #SET_DYNAMICCARGO self function SET_DYNAMICCARGO:FilterDeads() return self end --- This filter is N/A for SET_DYNAMICCARGO -- @param #SET_DYNAMICCARGO self -- @return #SET_DYNAMICCARGO self function SET_DYNAMICCARGO:FilterCrashes() return self end --- Returns a list of current owners (playernames) indexed by playername from the SET. -- @param #SET_DYNAMICCARGO self -- @return #list<#string> Ownerlist function SET_DYNAMICCARGO:GetOwnerNames() local owners = {} self:ForEach( function(cargo) if cargo and cargo.Owner then table.insert(owners, cargo.Owner, cargo.Owner) end end ) return owners end --- Returns a list of @{Wrapper.Storage#STORAGE} objects from the SET indexed by cargo name. -- @param #SET_DYNAMICCARGO self -- @return #list Storagelist function SET_DYNAMICCARGO:GetStorageObjects() local owners = {} self:ForEach( function(cargo) if cargo and cargo.warehouse then table.insert(owners, cargo.StaticName, cargo.warehouse) end end ) return owners end --- Returns a list of current owners (Wrapper.Client#CLIENT objects) indexed by playername from the SET. -- @param #SET_DYNAMICCARGO self -- @return #list<#string> Ownerlist function SET_DYNAMICCARGO:GetOwnerClientObjects() local owners = {} self:ForEach( function(cargo) if cargo and cargo.Owner then local client = CLIENT:FindByPlayerName(cargo.Owner) if client then table.insert(owners, cargo.Owner, client) end end end ) return owners end end