Merge pull request #1858 from FlightControl-Master/FF/Ops

OPS Capture Zone
This commit is contained in:
Frank 2022-12-30 22:30:57 +01:00 committed by GitHub
commit ff7ebb4f92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 2140 additions and 497 deletions

View File

@ -88,6 +88,7 @@ DATABASE = {
WAREHOUSES = {},
FLIGHTGROUPS = {},
FLIGHTCONTROLS = {},
OPSZONES = {},
}
local _DATABASECoalition =
@ -410,6 +411,46 @@ do -- Zone_Goal
end
end -- Zone_Goal
do -- OpsZone
--- Finds a @{Ops.OpsZone#OPSZONE} based on the zone name.
-- @param #DATABASE self
-- @param #string ZoneName The name of the zone.
-- @return Ops.OpsZone#OPSZONE The found OPSZONE.
function DATABASE:FindOpsZone( ZoneName )
local ZoneFound = self.OPSZONES[ZoneName]
return ZoneFound
end
--- Adds a @{Ops.OpsZone#OPSZONE} based on the zone name in the DATABASE.
-- @param #DATABASE self
-- @param Ops.OpsZone#OPSZONE OpsZone The zone.
function DATABASE:AddOpsZone( OpsZone )
if OpsZone then
local ZoneName=OpsZone:GetName()
if not self.OPSZONES[ZoneName] then
self.OPSZONES[ZoneName] = OpsZone
end
end
end
--- Deletes a @{Ops.OpsZone#OPSZONE} from the DATABASE based on the zone name.
-- @param #DATABASE self
-- @param #string ZoneName The name of the zone.
function DATABASE:DeleteOpsZone( ZoneName )
self.OPSZONES[ZoneName] = nil
end
end -- OpsZone
do -- cargo
--- Adds a Cargo based on the Cargo Name in the DATABASE.

View File

@ -53,6 +53,7 @@ do -- SET_BASE
-- @field #table Index Table of indices.
-- @field #table List Unused table.
-- @field Core.Scheduler#SCHEDULER CallScheduler
-- @field #SET_BASE.Filters Filter Filters
-- @extends Core.Base#BASE
--- The @{Core.Set#SET_BASE} class defines the core functions that define a collection of objects.
@ -83,6 +84,11 @@ do -- SET_BASE
YieldInterval = nil,
}
--- Filters
-- @type SET_BASE.Filters
-- @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
@ -135,11 +141,12 @@ do -- SET_BASE
--- 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()
function SET_BASE:Clear(TriggerEvent)
for Name, Object in pairs( self.Set ) do
self:Remove( Name )
self:Remove( Name, not TriggerEvent )
end
return self
@ -166,7 +173,7 @@ do -- SET_BASE
--- Gets a list of the Names of the Objects in the Set.
-- @param #SET_BASE self
-- @return #SET_BASE self
-- @return #table Table of names.
function SET_BASE:GetSetNames() -- R2.3
self:F2()
@ -181,7 +188,7 @@ do -- SET_BASE
--- Returns a table of the Objects in the Set.
-- @param #SET_BASE self
-- @return #SET_BASE self
-- @return #table Table of objects.
function SET_BASE:GetSetObjects() -- R2.3
self:F2()
@ -197,16 +204,21 @@ do -- SET_BASE
--- 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 NoTriggerEvent (Optional) When `true`, the :Remove() method will not trigger a **Removed** event.
-- @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 end
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 )
@ -214,6 +226,7 @@ do -- SET_BASE
break
end
end
-- When NoTriggerEvent is true, then no Removed event will be triggered.
if TriggerEvent then
self:Removed( ObjectName, Object )
@ -311,7 +324,6 @@ do -- SET_BASE
-- @param #SET_BASE self
-- @param Core.Set#SET_BASE SetB Set other set, called *B*.
-- @return Core.Set#SET_BASE A set of objects that is included in set *A* **and** in set *B*.
function SET_BASE:GetSetIntersection(SetB)
local intersection=SET_BASE:New()
@ -462,16 +474,32 @@ do -- SET_BASE
-- @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
@ -817,7 +845,7 @@ do -- SET_BASE
--- Decides whether an object is in the SET
-- @param #SET_BASE self
-- @param #table Object
-- @return #SET_BASE self
-- @return #boolean `true` if object is in set and `false` otherwise.
function SET_BASE:IsInSet( Object )
self:F3( Object )
local outcome = false
@ -1021,9 +1049,9 @@ do -- SET_GROUP
return self
end
--- Gets the Set.
--- Get a *new* set that only contains alive groups.
-- @param #SET_GROUP self
-- @return #table Table of objects
-- @return #SET_GROUP Set of alive groups.
function SET_GROUP:GetAliveSet()
self:F2()
@ -1169,35 +1197,14 @@ do -- SET_GROUP
--- 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 )
if not self.Filter.Zones then
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
--- 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
-- @return #SET_GROUP self
function SET_GROUP: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
@ -1207,10 +1214,12 @@ do -- SET_GROUP
else
zones = Zones
end
for _, Zone in pairs( zones ) do
local zonename = Zone:GetName()
self.Filter.Zones[zonename] = Zone
end
return self
end
@ -1218,17 +1227,21 @@ do -- SET_GROUP
-- 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
function SET_GROUP:FilterCoalitions( Coalitions )
if not self.Filter.Coalitions then
function SET_GROUP:FilterCoalitions( Coalitions, Clear )
if Clear or (not self.Filter.Coalitions) then
self.Filter.Coalitions = {}
end
if type( Coalitions ) ~= "table" then
Coalitions = { Coalitions }
end
-- Ensure table.
Coalitions = UTILS.EnsureTable(Coalitions, false)
for CoalitionID, Coalition in pairs( Coalitions ) do
self.Filter.Coalitions[Coalition] = Coalition
end
return self
end
@ -1236,17 +1249,22 @@ do -- SET_GROUP
-- 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 )
if not self.Filter.Categories then
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
@ -1899,6 +1917,41 @@ do -- SET_GROUP
end
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.
-- @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:GetCoord()
-- Distance between ref. coordinate and group coordinate.
local d=UTILS.VecDist3D(Coordinate, coord)
if d<dmin then
dmin=d
gmin=group
end
end
end
return gmin, dmin
end
end
do -- SET_UNIT
@ -2120,9 +2173,11 @@ do -- SET_UNIT
if type( Coalitions ) ~= "table" then
Coalitions = { Coalitions }
end
for CoalitionID, Coalition in pairs( Coalitions ) do
self.Filter.Coalitions[Coalition] = Coalition
end
return self
end
@ -4030,7 +4085,7 @@ do -- SET_CLIENT
--- Builds a set of clients in zones.
-- @param #SET_CLIENT self
-- @param #table Zones Table of Core.Zone#ZONE Zone objects, or a Core.Set#SET_ZONE
-- @return #SET_TABLE self
-- @return #SET_CLIENT self
function SET_CLIENT:FilterZones( Zones )
if not self.Filter.Zones then
self.Filter.Zones = {}
@ -5840,8 +5895,7 @@ do -- SET_ZONE
-- 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 that validates the coordinate location.
-- @return #nil No zone has been found.
-- @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
@ -5854,6 +5908,27 @@ do -- SET_ZONE
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<dmin then
dmin=d
zmin=Zone
end
end
return zmin, dmin
end
end
do -- SET_ZONE_GOAL
@ -6167,6 +6242,461 @@ do -- SET_ZONE_GOAL
end
do -- SET_OPSZONE
--- @type SET_OPSZONE
-- @extends Core.Set#SET_BASE
--- Mission designers can use the @{Core.Set#SET_OPSZONE} class to build sets of zones of various types.
--
-- ## SET_OPSZONE constructor
--
-- Create a new SET_OPSZONE object with the @{#SET_OPSZONE.New} method:
--
-- * @{#SET_OPSZONE.New}: Creates a new SET_OPSZONE object.
--
-- ## Add or Remove ZONEs from SET_OPSZONE
--
-- ZONEs can be added and removed using the @{Core.Set#SET_OPSZONE.AddZonesByName} and @{Core.Set#SET_OPSZONE.RemoveZonesByName} respectively.
-- These methods take a single ZONE name or an array of ZONE names to be added or removed from SET_OPSZONE.
--
-- ## SET_OPSZONE filter criteria
--
-- You can set filter criteria to build the collection of zones in SET_OPSZONE.
-- Filter criteria are defined by:
--
-- * @{#SET_OPSZONE.FilterPrefixes}: Builds the SET_OPSZONE with the zones having a certain text pattern in their name. **ATTENTION!** Bad naming convention as this *does not* only filter *prefixes*.
--
-- Once the filter criteria have been set for the SET_OPSZONE, you can start filtering using:
--
-- * @{#SET_OPSZONE.FilterStart}: Starts the filtering of the zones within the SET_OPSZONE.
--
-- ## SET_OPSZONE iterators
--
-- Once the filters have been defined and the SET_OPSZONE has been built, you can iterate the SET_OPSZONE with the available iterator methods.
-- The iterator methods will walk the SET_OPSZONE set, and call for each airbase within the set a function that you provide.
-- The following iterator methods are currently available within the SET_OPSZONE:
--
-- * @{#SET_OPSZONE.ForEachZone}: Calls a function for each zone it finds within the SET_OPSZONE.
--
-- ===
-- @field #SET_OPSZONE SET_OPSZONE
SET_OPSZONE = {
ClassName = "SET_OPSZONE",
Zones = {},
Filter = {
Prefixes = nil,
Coalitions = nil,
},
FilterMeta = {
Coalitions = {
red = coalition.side.RED,
blue = coalition.side.BLUE,
neutral = coalition.side.NEUTRAL,
},
}, --FilterMeta
}
--- Creates a new SET_OPSZONE object, building a set of zones.
-- @param #SET_OPSZONE self
-- @return #SET_OPSZONE self
function SET_OPSZONE:New()
-- Inherits from BASE
local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.OPSZONES ) )
return self
end
--- Add an OPSZONE to set.
-- @param Core.Set#SET_OPSZONE self
-- @param Ops.OpsZone#OPSZONE Zone The OPSZONE object.
-- @return #SET_OPSZONE self
function SET_OPSZONE:AddZone( Zone )
self:Add( Zone:GetName(), Zone )
return self
end
--- Remove ZONEs from SET_OPSZONE.
-- @param Core.Set#SET_OPSZONE self
-- @param #table RemoveZoneNames A single name or an array of OPSZONE names.
-- @return #SET_OPSZONE self
function SET_OPSZONE:RemoveZonesByName( RemoveZoneNames )
local RemoveZoneNamesArray = (type( RemoveZoneNames ) == "table") and RemoveZoneNames or { RemoveZoneNames }
--UTILS.EnsureTable(Object,ReturnNil)
for RemoveZoneID, RemoveZoneName in pairs( RemoveZoneNamesArray ) do
self:Remove( RemoveZoneName )
end
return self
end
--- Finds a Zone based on its name.
-- @param #SET_OPSZONE self
-- @param #string ZoneName
-- @return Ops.OpsZone#OPSZONE The found Zone.
function SET_OPSZONE:FindZone( ZoneName )
local ZoneFound = self.Set[ZoneName]
return ZoneFound
end
--- Get a random zone from the set.
-- @param #SET_OPSZONE self
-- @return Ops.OpsZone#OPSZONE The random Zone.
function SET_OPSZONE:GetRandomZone()
if self:Count() ~= 0 then
local Index = self.Index
local ZoneFound = nil -- Core.Zone#ZONE_BASE
-- Loop until a zone has been found.
-- The :GetZoneMaybe() call will evaluate the probability for the zone to be selected.
-- If the zone is not selected, then nil is returned by :GetZoneMaybe() and the loop continues!
while not ZoneFound do
local ZoneRandom = math.random( 1, #Index )
ZoneFound = self.Set[Index[ZoneRandom]]:GetZoneMaybe()
end
return ZoneFound
end
return nil
end
--- Set a zone probability.
-- @param #SET_OPSZONE self
-- @param #string ZoneName The name of the zone.
-- @param #number Probability The probability in percent.
function SET_OPSZONE:SetZoneProbability( ZoneName, Probability )
local Zone = self:FindZone( ZoneName )
Zone:SetZoneProbability( Probability )
return self
end
--- Builds a set of OPSZONEs that contain the given string in their name.
-- **ATTENTION!** Bad naming convention as this **does not** filter only **prefixes** but all zones that **contain** the string.
-- @param #SET_OPSZONE self
-- @param #string Prefixes The string pattern(s) that needs to be contained in the zone name. Can also be passed as a `#table` of strings.
-- @return #SET_OPSZONE self
function SET_OPSZONE:FilterPrefixes( Prefixes )
if not self.Filter.Prefixes then
self.Filter.Prefixes = {}
end
Prefixes=UTILS.EnsureTable(Prefixes, false)
for PrefixID, Prefix in pairs( Prefixes ) do
self.Filter.Prefixes[Prefix] = Prefix
end
return self
end
--- Builds a set of groups of coalitions. Possible current coalitions are red, blue and neutral.
-- @param #SET_OPSZONE self
-- @param #string Coalitions Can take the following values: "red", "blue", "neutral" or combinations as a table, for example `{"red", "neutral"}`.
-- @return #SET_OPSZONE self
function SET_OPSZONE:FilterCoalitions(Coalitions)
-- Create an empty set.
if not self.Filter.Coalitions then
self.Filter.Coalitions={}
end
-- Ensure we got a table.
Coalitions=UTILS.EnsureTable(Coalitions, false)
-- Set filter.
for CoalitionID, Coalition in pairs( Coalitions ) do
self.Filter.Coalitions[Coalition] = Coalition
end
return self
end
--- Filters for the defined collection.
-- @param #SET_OPSZONE self
-- @return #SET_OPSZONE self
function SET_OPSZONE:FilterOnce()
for ObjectName, Object in pairs( self.Database ) do
-- First remove the object (without creating an event).
self:Remove(ObjectName, true)
if self:IsIncludeObject( Object ) then
self:Add( ObjectName, Object )
end
end
return self
end
--- Clear all filters. You still need to apply `FilterOnce()` to have an effect on the set.
-- @param #SET_OPSZONE self
-- @return #SET_OPSZONE self
function SET_OPSZONE:FilterClear()
local parent=self:GetParent(self, SET_OPSZONE) --#SET_BASE
parent:FilterClear()
return self
end
--- Starts the filtering.
-- @param #SET_OPSZONE self
-- @return #SET_OPSZONE self
function SET_OPSZONE:FilterStart()
if _DATABASE then
-- We initialize the first set.
for ObjectName, Object in pairs( self.Database ) do
if self:IsIncludeObject( Object ) then
self:Add( ObjectName, Object )
else
self:RemoveZonesByName( ObjectName )
end
end
end
self:HandleEvent( EVENTS.NewZoneGoal )
self:HandleEvent( EVENTS.DeleteZoneGoal )
return self
end
--- Stops the filtering for the defined collection.
-- @param #SET_OPSZONE self
-- @return #SET_OPSZONE self
function SET_OPSZONE:FilterStop()
self:UnHandleEvent( EVENTS.NewZoneGoal )
self:UnHandleEvent( EVENTS.DeleteZoneGoal )
return self
end
--- Handles the Database to check on an event (birth) that the Object was added in the Database.
-- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event!
-- @param #SET_OPSZONE self
-- @param Core.Event#EVENTDATA Event
-- @return #string The name of the AIRBASE
-- @return #table The AIRBASE
function SET_OPSZONE:AddInDatabase( Event )
self:F3( { Event } )
return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName]
end
--- Handles the Database to check on any event that Object exists in the Database.
-- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa!
-- @param #SET_OPSZONE self
-- @param Core.Event#EVENTDATA Event
-- @return #string The name of the AIRBASE
-- @return #table The AIRBASE
function SET_OPSZONE:FindInDatabase( Event )
self:F3( { Event } )
return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName]
end
--- Iterate the SET_OPSZONE and call an iterator function for each ZONE, providing the ZONE and optional parameters.
-- @param #SET_OPSZONE self
-- @param #function IteratorFunction The function that will be called when there is an alive ZONE in the SET_OPSZONE. The function needs to accept a AIRBASE parameter.
-- @return #SET_OPSZONE self
function SET_OPSZONE:ForEachZone( IteratorFunction, ... )
self:F2( arg )
self:ForEach( IteratorFunction, arg, self:GetSet() )
return self
end
--- Private function that checks if an object is contained in the set or filtered.
-- @param #SET_OPSZONE self
-- @param Ops.OpsZone#OPSZONE MZone The OPSZONE object.
-- @return #SET_OPSZONE self
function SET_OPSZONE:IsIncludeObject( MZone )
self:F2( MZone )
local MZoneInclude = true
if MZone then
local MZoneName = MZone:GetName()
if self.Filter.Prefixes then
local MZonePrefix = false
-- Loop over prefixes.
for ZonePrefixId, ZonePrefix in pairs( self.Filter.Prefixes ) do
-- Prifix
self:T3( { "Prefix:", string.find( MZoneName, ZonePrefix, 1 ), ZonePrefix } )
if string.find(MZoneName, ZonePrefix, 1) then
MZonePrefix = true
break --Break the loop as we found the prefix.
end
end
self:T( { "Evaluated Prefix", MZonePrefix } )
MZoneInclude = MZoneInclude and MZonePrefix
end
-- Filter coalitions.
if self.Filter.Coalitions then
local MGroupCoalition = false
local coalition=MZone:GetOwner()
for _, CoalitionName in pairs( self.Filter.Coalitions ) do
if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName]==coalition then
MGroupCoalition = true
break -- Break the loop the coalition is contains.
end
end
MZoneInclude = MZoneInclude and MGroupCoalition
end
end
self:T2( MZoneInclude )
return MZoneInclude
end
--- Handles the OnEventNewZone event for the Set.
-- @param #SET_OPSZONE self
-- @param Core.Event#EVENTDATA EventData
function SET_OPSZONE:OnEventNewZoneGoal( EventData )
-- Debug info.
self:T( { "New Zone Capture Coalition", EventData } )
self:T( { "Zone Capture Coalition", EventData.ZoneGoal } )
if EventData.ZoneGoal then
if EventData.ZoneGoal and self:IsIncludeObject( EventData.ZoneGoal ) then
self:T( { "Adding Zone Capture Coalition", EventData.ZoneGoal.ZoneName, EventData.ZoneGoal } )
self:Add( EventData.ZoneGoal.ZoneName, EventData.ZoneGoal )
end
end
end
--- Handles the OnDead or OnCrash event for alive units set.
-- @param #SET_OPSZONE self
-- @param Core.Event#EVENTDATA EventData
function SET_OPSZONE:OnEventDeleteZoneGoal( EventData ) -- R2.1
self:F3( { EventData } )
if EventData.ZoneGoal then
local Zone = _DATABASE:FindZone( EventData.ZoneGoal.ZoneName )
if Zone and Zone.ZoneName then
-- When cargo was deleted, it may probably be because of an S_EVENT_DEAD.
-- However, in the loading logic, an S_EVENT_DEAD is also generated after a Destroy() call.
-- And this is a problem because it will remove all entries from the SET_OPSZONEs.
-- To prevent this from happening, the Zone object has a flag NoDestroy.
-- When true, the SET_OPSZONE won't Remove the Zone object from the set.
-- This flag is switched off after the event handlers have been called in the EVENT class.
self:F( { ZoneNoDestroy = Zone.NoDestroy } )
if Zone.NoDestroy then
else
self:Remove( Zone.ZoneName )
end
end
end
end
--- Start all opszones of the set.
-- @param #SET_OPSZONE self
-- @return #SET_OPSZONE self
function SET_OPSZONE:Start()
for _,_Zone in pairs( self:GetSet() ) do
local Zone = _Zone --Ops.OpsZone#OPSZONE
if Zone:IsStopped() then
Zone:Start()
end
end
return self
end
--- Validate if a coordinate is in one of the zones in the set.
-- Returns the ZONE object where the coordiante is located.
-- If zones overlap, the first zone that validates the test is returned.
-- @param #SET_OPSZONE self
-- @param Core.Point#COORDINATE Coordinate The coordinate to be searched.
-- @return Core.Zone#ZONE_BASE The zone that validates the coordinate location.
-- @return #nil No zone has been found.
function SET_OPSZONE:IsCoordinateInZone( Coordinate )
for _,_Zone in pairs( self:GetSet() ) do
local Zone = _Zone --Ops.OpsZone#OPSZONE
if Zone:GetZone():IsCoordinateInZone( Coordinate ) then
return Zone
end
end
return nil
end
--- Get the closest OPSZONE from a given reference coordinate. Only started zones are considered.
-- @param #SET_OPSZONE self
-- @param Core.Point#COORDINATE Coordinate The reference coordinate from which the closest zone is determined.
-- @param #table Coalitions Only consider the given coalition(s), *e.g.* `{coaliton.side.RED}` to find the closest red zone.
-- @return Ops.OpsZone#OPSZONE The closest OPSZONE (if any).
-- @return #number Distance to ref coordinate in meters.
function SET_OPSZONE:GetClosestZone( Coordinate, Coalitions )
Coalitions=UTILS.EnsureTable(Coalitions, true)
local dmin=math.huge --#number
local zmin=nil --Ops.OpsZone#OPSZONE
for _,_opszone in pairs(self:GetSet()) do
local opszone=_opszone --Ops.OpsZone#OPSZONE
local coal=opszone:GetOwner()
if opszone:IsStarted() and (Coalitions==nil or (Coalitions and UTILS.IsInTable(Coalitions, coal))) then
-- Get 2D distance.
local d=opszone:GetZone():Get2DDistance(Coordinate)
if d<dmin then
dmin=d
zmin=opszone
end
end
end
return zmin, dmin
end
end
do -- SET_OPSGROUP
@ -6289,7 +6819,7 @@ do -- SET_OPSGROUP
return self
end
--- Gets the Set.
--- Gets a **new** set that only contains alive groups.
-- @param #SET_OPSGROUP self
-- @return #SET_OPSGROUP self
function SET_OPSGROUP:GetAliveSet()
@ -6454,11 +6984,12 @@ do -- SET_OPSGROUP
-- Possible current coalitions are red, blue and neutral.
-- @param #SET_OPSGROUP self
-- @param #string Coalitions Can take the following values: "red", "blue", "neutral" or combinations as a table, for example `{"red", "neutral"}`.
-- @param #boolean Clear If `true`, clear any previously defined filters.
-- @return #SET_OPSGROUP self
function SET_OPSGROUP:FilterCoalitions(Coalitions)
function SET_OPSGROUP:FilterCoalitions(Coalitions, Clear)
-- Create an empty set.
if not self.Filter.Coalitions then
if Clear or not self.Filter.Coalitions then
self.Filter.Coalitions={}
end
@ -6487,10 +7018,11 @@ do -- SET_OPSGROUP
--
-- @param #SET_OPSGROUP self
-- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship" or combinations as a table, for example `{"plane", "helicopter"}`.
-- @param #boolean Clear If `true`, clear any previously defined filters.
-- @return #SET_OPSGROUP self
function SET_OPSGROUP:FilterCategories( Categories )
function SET_OPSGROUP:FilterCategories( Categories, Clear )
if not self.Filter.Categories then
if Clear or not self.Filter.Categories then
self.Filter.Categories={}
end
@ -6548,11 +7080,12 @@ do -- SET_OPSGROUP
--- Builds a set of groups of defined countries.
-- @param #SET_OPSGROUP self
-- @param #string Countries Can take those country strings known within DCS world.
-- @param #boolean Clear If `true`, clear any previously defined filters.
-- @return #SET_OPSGROUP self
function SET_OPSGROUP:FilterCountries(Countries)
function SET_OPSGROUP:FilterCountries(Countries, Clear)
-- Create empty table if necessary.
if not self.Filter.Countries then
if Clear or not self.Filter.Countries then
self.Filter.Countries = {}
end
@ -6574,11 +7107,12 @@ do -- SET_OPSGROUP
-- **Attention!** Bad naming convention as this **does not** filter only **prefixes** but all groups that **contain** the string.
-- @param #SET_OPSGROUP 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.
-- @param #boolean Clear If `true`, clear any previously defined filters.
-- @return #SET_OPSGROUP self
function SET_OPSGROUP:FilterPrefixes(Prefixes)
function SET_OPSGROUP:FilterPrefixes(Prefixes, Clear)
-- Create emtpy table if necessary.
if not self.Filter.GroupPrefixes then
if Clear or not self.Filter.GroupPrefixes then
self.Filter.GroupPrefixes={}
end

View File

@ -68,7 +68,7 @@ ARMYGROUP = {
--- Army Group version.
-- @field #string version
ARMYGROUP.version="0.8.0"
ARMYGROUP.version="0.9.0"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
@ -142,6 +142,7 @@ function ARMYGROUP:New(group)
------------------------
--- Pseudo Functions ---
------------------------
--- Triggers the FSM event "Cruise".
-- @function [parent=#ARMYGROUP] Cruise
-- @param #ARMYGROUP self
@ -253,10 +254,14 @@ function ARMYGROUP:New(group)
--- Triggers the FSM event "Retreat".
-- @function [parent=#ARMYGROUP] Retreat
-- @param #ARMYGROUP self
-- @param Core.Zone#ZONE_BASE Zone (Optional) Zone where to retreat. Default is the closest retreat zone.
-- @param #number Formation (Optional) Formation of the group.
--- Triggers the FSM event "Retreat" after a delay.
-- @function [parent=#ARMYGROUP] __Retreat
-- @param #ARMYGROUP self
-- @param Core.Zone#ZONE_BASE Zone (Optional) Zone where to retreat. Default is the closest retreat zone.
-- @param #number Formation (Optional) Formation of the group.
-- @param #number delay Delay in seconds.
--- On after "Retreat" event.
@ -265,7 +270,8 @@ function ARMYGROUP:New(group)
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Zone#ZONE_BASE Zone Zone where to retreat.
-- @param #number Formation Formation of the group. Can be #nil.
--- Triggers the FSM event "Retreated".
-- @function [parent=#ARMYGROUP] Retreated
@ -287,7 +293,7 @@ function ARMYGROUP:New(group)
--- Triggers the FSM event "EngageTarget".
-- @function [parent=#ARMYGROUP] EngageTarget
-- @param #ARMYGROUP self
-- @param Wrapper.Group#GROUP Group the group to be engaged.
-- @param Ops.Target#TARGET Target The target to be engaged. Can also be a GROUP or UNIT object.
-- @param #number Speed Speed in knots.
-- @param #string Formation Formation used in the engagement.
@ -715,6 +721,25 @@ function ARMYGROUP:Status()
end
end
-- Get current mission (if any).
local mission=self:GetMissionCurrent()
-- If mission, check if DCS task needs to be updated.
if mission and mission.updateDCSTask and mission:GetGroupStatus(self)==AUFTRAG.GroupStatus.EXECUTING then
if mission.type==AUFTRAG.Type.CAPTUREZONE then
-- Get task.
local Task=mission:GetGroupWaypointTask(self)
-- Update task: Engage or get new zone.
self:_UpdateTask(Task, mission)
end
end
else
-- Check damage of elements and group.
self:_CheckDamage()
@ -1010,6 +1035,9 @@ function ARMYGROUP:onbeforeUpdateRoute(From, Event, To, n, N, Speed, Formation)
elseif task.dcstask.id==AUFTRAG.SpecialTask.RELOCATECOHORT then
-- For relocate
self:T2(self.lid.."Allowing update route for Task: Relocate Cohort")
elseif task.dcstask.id==AUFTRAG.SpecialTask.REARMING then
-- For relocate
self:T2(self.lid.."Allowing update route for Task: Rearming")
else
local taskname=task and task.description or "No description"
self:T(self.lid..string.format("WARNING: Update route denied because taskcurrent=%d>0! Task description = %s", self.taskcurrent, tostring(taskname)))
@ -1339,7 +1367,7 @@ function ARMYGROUP:onafterDetour(From, Event, To, Coordinate, Speed, Formation,
Speed=Speed or self:GetSpeedCruise()
-- ID of current waypoint.
local uid=self:GetWaypointCurrent().uid
local uid=self:GetWaypointCurrentUID()
-- Add waypoint after current.
local wp=self:AddWaypoint(Coordinate, Speed, uid, Formation, true)
@ -1411,10 +1439,15 @@ function ARMYGROUP:onbeforeRearm(From, Event, To, Coordinate, Formation)
-- Pause current mission.
if self:IsOnMission() then
local mission=self:GetMissionCurrent()
if mission and mission.type~=AUFTRAG.Type.REARMING then
self:T(self.lid.."Rearm command but have current mission ==> Pausing mission!")
self:PauseMission()
dt=-0.1
allowed=false
else
self:T(self.lid.."Rearm command and current mission is REARMING ==> Transition ALLOWED!")
end
end
-- Disengage.
@ -1457,7 +1490,7 @@ function ARMYGROUP:onafterRearm(From, Event, To, Coordinate, Formation)
self:T(self.lid..string.format("Group send to rearm"))
-- ID of current waypoint.
local uid=self:GetWaypointCurrent().uid
local uid=self:GetWaypointCurrentUID()
-- Add waypoint after current.
local wp=self:AddWaypoint(Coordinate, nil, uid, Formation, true)
@ -1480,6 +1513,7 @@ function ARMYGROUP:onafterRearmed(From, Event, To)
-- Check if this is a rearming mission.
if mission and mission.type==AUFTRAG.Type.REARMING then
-- Rearmed ==> Mission Done! This also checks if the group is done.
self:MissionDone(mission)
@ -1649,10 +1683,14 @@ end
function ARMYGROUP:onafterRetreat(From, Event, To, Zone, Formation)
-- ID of current waypoint.
local uid=self:GetWaypointCurrent().uid
local uid=self:GetWaypointCurrentUID()
-- Get random coordinate of the zone.
local Coordinate=Zone:GetRandomCoordinate()
-- Debug info.
self:T(self.lid..string.format("Retreating to zone %s", Zone:GetName()))
-- Add waypoint after current.
local wp=self:AddWaypoint(Coordinate, nil, uid, Formation, true)
@ -1702,10 +1740,11 @@ function ARMYGROUP:onbeforeEngageTarget(From, Event, To, Target, Speed, Formatio
return false
end
-- Pause current mission.
-- Get current mission.
local mission=self:GetMissionCurrent()
if mission and mission.type~=AUFTRAG.Type.GROUNDATTACK then
-- Pause current mission unless it uses the EngageTarget command.
if mission and mission.type~=AUFTRAG.Type.GROUNDATTACK and mission.type~=AUFTRAG.Type.CAPTUREZONE then
self:T(self.lid.."Engage command but have current mission ==> Pausing mission!")
self:PauseMission()
dt=-0.1
@ -1727,7 +1766,7 @@ end
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Wrapper.Group#GROUP Group the group to be engaged.
-- @param Ops.Target#TARGET Target The target to be engaged. Can also be a group or unit.
-- @param #number Speed Attack speed in knots.
-- @param #string Formation Formation used in the engagement. Default `ENUMS.Formation.Vehicle.Vee`.
function ARMYGROUP:onafterEngageTarget(From, Event, To, Target, Speed, Formation)
@ -1785,8 +1824,8 @@ function ARMYGROUP:_UpdateEngageTarget()
-- Distance to last known position of target.
local dist=UTILS.VecDist3D(vec3, self.engage.Coordinate:GetVec3())
-- Check if target moved more than 100 meters.
if dist>100 then
-- Check if target moved more than 100 meters or we do not have line of sight.
if dist>100 or not self:HasLoS(self.engage.Target:GetCoordinate()) then
--env.info("FF Update Engage Target Moved "..self.engage.Target:GetName())
@ -1794,7 +1833,7 @@ function ARMYGROUP:_UpdateEngageTarget()
self.engage.Coordinate:UpdateFromVec3(vec3)
-- ID of current waypoint.
local uid=self:GetWaypointCurrent().uid
local uid=self:GetWaypointCurrentUID()
-- Remove current waypoint
self:RemoveWaypointByID(self.engage.Waypoint.uid)
@ -1952,6 +1991,8 @@ function ARMYGROUP:AddWaypoint(Coordinate, Speed, AfterWaypointWithID, Formation
if not Formation then
if self.formationPerma then
Formation = self.formationPerma
elseif self.optionDefault.Formation then
Formation = self.optionDefault.Formation
elseif self.option.Formation then
Formation = self.option.Formation
else
@ -2037,8 +2078,11 @@ function ARMYGROUP:_InitGroup(Template)
-- Set default radio.
self:SetDefaultRadio(self.radio.Freq, self.radio.Modu, self.radio.On)
-- Set default formation from first waypoint.
self.optionDefault.Formation=template.route.points[1].action --self:GetWaypoint(1).action
-- Get current formation from first waypoint.
self.option.Formation=template.route.points[1].action
-- Set default formation to "on road".
self.optionDefault.Formation=ENUMS.Formation.Vehicle.OnRoad
-- Default TACAN off.
self:SetDefaultTACAN(nil, nil, nil, nil, true)

View File

@ -422,7 +422,8 @@ _AUFTRAGSNR=0
-- @field #string AIRDEFENSE Air defense.
-- @field #string EWR Early Warning Radar.
-- @field #string RECOVERYTANKER Recovery tanker.
-- @filed #string REARMING Rearming mission.
-- @field #string REARMING Rearming mission.
-- @field #string CAPTUREZONE Capture zone mission.
-- @field #string NOTHING Nothing.
AUFTRAG.Type={
ANTISHIP="Anti Ship",
@ -465,6 +466,7 @@ AUFTRAG.Type={
EWR="Early Warning Radar",
RECOVERYTANKER="Recovery Tanker",
REARMING="Rearming",
CAPTUREZONE="Capture Zone",
NOTHING="Nothing",
}
@ -487,6 +489,7 @@ AUFTRAG.Type={
-- @field #string EWR Early Warning Radar.
-- @field #string RECOVERYTANKER Recovery tanker.
-- @field #string REARMING Rearming.
-- @field #string CAPTUREZONE Capture OPS zone.
-- @field #string NOTHING Nothing.
AUFTRAG.SpecialTask={
FORMATION="Formation",
@ -507,6 +510,7 @@ AUFTRAG.SpecialTask={
EWR="Early Warning Radar",
RECOVERYTANKER="Recovery Tanker",
REARMING="Rearming",
CAPTUREZONE="Capture Zone",
NOTHING="Nothing",
}
@ -628,7 +632,7 @@ AUFTRAG.Category={
--- AUFTRAG class version.
-- @field #string version
AUFTRAG.version="0.9.7"
AUFTRAG.version="0.9.9"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
@ -637,6 +641,7 @@ AUFTRAG.version="0.9.7"
-- TODO: Replace engageRange by missionRange. Here and in other classes. CTRL+H is your friend!
-- TODO: Mission success options damaged, destroyed.
-- TODO: F10 marker to create new missions.
-- DONE: Add Capture zone task.
-- DONE: Add orbit mission for moving anker points.
-- DONE: Add recovery tanker mission for boat ops.
-- DONE: Added auftrag category.
@ -2020,6 +2025,51 @@ function AUFTRAG:NewPATROLZONE(Zone, Speed, Altitude, Formation)
return mission
end
--- **[AIR, GROUND, NAVAL]** Create a CAPTUREZONE mission. Group(s) will go to the zone and patrol it randomly.
-- @param #AUFTRAG self
-- @param Ops.OpsZone#OPSZONE OpsZone The OPS zone to capture.
-- @param #number Coalition The coalition which should capture the zone for the mission to be successful.
-- @param #number Speed Speed in knots.
-- @param #number Altitude Altitude in feet. Only for airborne units. Default 2000 feet ASL.
-- @param #string Formation Formation used by ground units during patrol. Default "Off Road".
-- @return #AUFTRAG self
function AUFTRAG:NewCAPTUREZONE(OpsZone, Coalition, Speed, Altitude, Formation)
local mission=AUFTRAG:New(AUFTRAG.Type.CAPTUREZONE)
mission:_TargetFromObject(OpsZone)
mission.coalition=Coalition
mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.CAPTUREZONE)
mission.optionROE=ENUMS.ROE.ReturnFire
mission.optionROT=ENUMS.ROT.PassiveDefense
mission.optionAlarm=ENUMS.AlarmState.Auto
mission.missionFraction=0.1
mission.missionSpeed=Speed and UTILS.KnotsToKmph(Speed) or nil
mission.missionAltitude=Altitude and UTILS.FeetToMeters(Altitude) or nil
mission.categories={AUFTRAG.Category.ALL}
mission.DCStask=mission:GetDCSMissionTask()
mission.updateDCSTask=true
local params={}
params.formation=Formation or "Off Road"
params.zone=mission:GetObjective()
params.altitude=mission.missionAltitude
params.speed=mission.missionSpeed
mission.DCStask.params=params
return mission
end
--- **[OBSOLETE]** Create a ARMORATTACK mission.
-- ** Note that this is actually creating a GROUNDATTACK mission!**
@ -2663,6 +2713,15 @@ function AUFTRAG:SetRepeatOnSuccess(Nrepeat)
return self
end
--- **[LEGION, COMMANDER, CHIEF]** Set that mission assets get reinforced if their number drops below Nmin.
-- @param #AUFTRAG self
-- @param #number Nreinforce Number of max asset groups used to reinforce.
-- @return #AUFTRAG self
function AUFTRAG:SetReinforce(Nreinforce)
self.reinforce=Nreinforce
return self
end
--- **[LEGION, COMMANDER, CHIEF]** Define how many assets are required to do the job. Only used if the mission is handled by a **LEGION** (AIRWING, BRIGADE, ...) or higher level.
-- @param #AUFTRAG self
-- @param #number NassetsMin Minimum number of asset groups. Default 1.
@ -2684,24 +2743,32 @@ end
--- **[LEGION, COMMANDER, CHIEF]** Get number of required assets.
-- @param #AUFTRAG self
-- @param Ops.Legion#Legion Legion (Optional) Only get the required assets for a specific legion. If required assets for this legion are not defined, the total number is returned.
-- @return #number Min. number of required assets.
-- @return #number Max. number of required assets.
function AUFTRAG:GetRequiredAssets(Legion)
--local N=self.nassets
--if Legion and self.Nassets[Legion.alias] then
-- N=self.Nassets[Legion.alias]
--end
function AUFTRAG:GetRequiredAssets()
local Nmin=self.NassetsMin
local Nmax=self.NassetsMax
if self.type==AUFTRAG.Type.RELOCATECOHORT then
-- Relocation gets all the assets.
local cohort=self.DCStask.params.cohort --Ops.Cohort#COHORT
Nmin=#cohort.assets
Nmax=Nmin
else
-- Check if this is an reinforcement.
if self:IsExecuting() and self.reinforce and self.reinforce>0 then
local N=self:CountOpsGroups()
if N<Nmin then
Nmin=math.min(Nmin-N, self.reinforce)
Nmax=Nmin
self:T(self.lid..string.format("FF Executing Nmin=%d, N=%d, Nreinfoce=%d ==> Nmin=%d", self.NassetsMin, N, self.reinforce, Nmin))
end
end
end
return Nmin, Nmax
@ -2961,6 +3028,7 @@ function AUFTRAG:AddTransportCarriers(Carriers)
end
return self
end
--- **[LEGION, COMMANDER, CHIEF]** Set required attribute(s) the assets must have.
@ -2968,10 +3036,8 @@ end
-- @param #table Attributes Generalized attribute(s).
-- @return #AUFTRAG self
function AUFTRAG:SetRequiredAttribute(Attributes)
if Attributes and type(Attributes)~="table" then
Attributes={Attributes}
end
self.attributes=Attributes
self.attributes=UTILS.EnsureTable(Attributes, true)
return self
end
--- **[LEGION, COMMANDER, CHIEF]** Set required property or properties the assets must have.
@ -2980,10 +3046,8 @@ end
-- @param #table Properties Property or table of properties.
-- @return #AUFTRAG self
function AUFTRAG:SetRequiredProperty(Properties)
if Properties and type(Properties)~="table" then
Properties={Properties}
end
self.properties=Properties
self.properties=UTILS.EnsureTable(Properties, true)
return self
end
--- **[LEGION, COMMANDER, CHIEF]** Set number of required carrier groups if an OPSTRANSPORT assignment is required.
@ -3854,7 +3918,7 @@ function AUFTRAG:onafterStatus(From, Event, To)
self:T(self.lid.."No targets left cancelling mission!")
self:Cancel()
elseif self:IsExecuting() then
elseif self:IsExecuting() and ((not self.reinforce) or self.reinforce==0) then
-- Had the case that mission was in state Executing but all assigned groups were dead.
-- TODO: might need to loop over all assigned groups
@ -4384,6 +4448,12 @@ function AUFTRAG:CheckGroupsDone()
return false
end
-- Check if there is still reinforcement to be expected.
if self:IsExecuting() and self.reinforce and self.reinforce>0 then
self:T2(self.lid..string.format("CheckGroupsDone: Mission is still in state %s [FSM=%s] and reinfoce=%d. Mission NOT DONE!", self.status, self:GetState(), self.reinforce))
return false
end
-- It could be that all flights were destroyed on the way to the mission execution waypoint.
-- TODO: would be better to check if everybody is dead by now.
if self:IsStarted() and self:CountOpsGroups()==0 then
@ -4542,7 +4612,7 @@ function AUFTRAG:onafterAssetDead(From, Event, To, Asset)
self:T(self.lid..string.format("Asset %s dead! Number of ops groups remaining %d", tostring(Asset.spawngroupname), N))
-- All assets dead?
if N==0 then
if N==0 and (self.reinforce==nil or self.reinforce==0) then
if self:IsNotOver() then
@ -4983,8 +5053,11 @@ function AUFTRAG:CountMissionTargets()
local N=0
-- Count specific coalitions.
local Coalitions=self.coalition and UTILS.GetCoalitionEnemy(self.coalition, true) or nil
if self.engageTarget then
N=self.engageTarget:CountTargets()
N=self.engageTarget:CountTargets(Coalitions)
end
return N
@ -5049,9 +5122,13 @@ end
--- Get mission objective object. Could be many things depending on the mission type.
-- @param #AUFTRAG self
-- @param Core.Point#COORDINATE RefCoordinate (Optional) Reference coordinate from which the closest target is determined.
-- @param #table Coalitions (Optional) Only consider targets of the given coalition(s).
-- @return Wrapper.Positionable#POSITIONABLE The target object. Could be many things.
function AUFTRAG:GetObjective()
local objective=self:GetTargetData():GetObject()
function AUFTRAG:GetObjective(RefCoordinate, Coalitions)
local objective=self:GetTargetData():GetObject(RefCoordinate, Coalitions)
return objective
end
@ -5790,6 +5867,22 @@ function AUFTRAG:GetDCSMissionTask()
table.insert(DCStasks, DCStask)
elseif self.type==AUFTRAG.Type.CAPTUREZONE then
--------------------------
-- CAPTURE ZONE Mission --
--------------------------
local DCStask={}
DCStask.id=AUFTRAG.SpecialTask.CAPTUREZONE
-- We create a "fake" DCS task and pass the parameters to the FLIGHTGROUP.
local param={}
DCStask.params=param
table.insert(DCStasks, DCStask)
elseif self.type==AUFTRAG.Type.CASENHANCED then
-------------------------
@ -5866,7 +5959,7 @@ function AUFTRAG:GetDCSMissionTask()
table.insert(DCStasks, DCStask)
elseif self.type==AUFTRAG.Type.AMMOSUPPLY then
elseif self.type==AUFTRAG.Type.REARMING then
----------------------
-- REARMING Mission --

View File

@ -176,7 +176,11 @@ function COHORT:New(TemplateGroupName, Ngroups, CohortName)
for i,_unit in pairs(units) do
local unit=_unit --Wrapper.Unit#UNIT
local desc=unit:GetDesc()
self.weightAsset=self.weightAsset + (desc.massMax or 666)
local mass=666
if desc then
mass=desc.massMax or desc.massEmpty
end
self.weightAsset=self.weightAsset + (mass or 666)
if i==1 then
self.cargobayLimit=unit:GetCargoBayFreeWeight()
end

View File

@ -1672,12 +1672,6 @@ function COMMANDER:RecruitAssetsForMission(Mission)
-- Debug info.
self:T2(self.lid..string.format("Recruiting assets for mission \"%s\" [%s]", Mission:GetName(), Mission:GetType()))
-- Cohorts.
local Cohorts=self:_GetCohorts(Mission.specialLegions, Mission.specialCohorts, Mission.operation)
-- Debug info.
self:T(self.lid..string.format("Found %d cohort candidates for mission", #Cohorts))
-- Number of required assets.
local NreqMin, NreqMax=Mission:GetRequiredAssets()
@ -1687,9 +1681,41 @@ function COMMANDER:RecruitAssetsForMission(Mission)
-- Special payloads.
local Payloads=Mission.payloads
-- Largest cargo bay available of available carrier assets if mission assets need to be transported.
local MaxWeight=nil
if Mission.NcarriersMin then
-- Get transport cohorts.
local Cohorts=LEGION._GetCohorts(Mission.transportLegions or self.legions, Mission.transportCohorts)
-- Filter cohorts that can actually perform transport missions.
local transportcohorts={}
for _,_cohort in pairs(Cohorts) do
local cohort=_cohort --Ops.Cohort#COHORT
-- Check if cohort can perform transport to target.
--TODO: Option to filter transport carrier asset categories, attributes and/or properties.
local can=LEGION._CohortCan(cohort, AUFTRAG.Type.OPSTRANSPORT, Categories, Attributes, Properties, nil, TargetVec2)
-- MaxWeight of cargo assets is limited by the largets available cargo bay. We don't want to select, e.g., tanks that cannot be transported by APCs or helos.
if can and (MaxWeight==nil or cohort.cargobayLimit>MaxWeight) then
MaxWeight=cohort.cargobayLimit
end
end
self:T(self.lid..string.format("Largest cargo bay available=%.1f", MaxWeight))
end
-- Get cohorts.
local Cohorts=LEGION._GetCohorts(Mission.specialLegions or self.legions, Mission.specialCohorts, Mission.operation, self.opsqueue)
-- Debug info.
self:T(self.lid..string.format("Found %d cohort candidates for mission", #Cohorts))
-- Recruite assets.
local recruited, assets, legions=LEGION.RecruitCohortAssets(Cohorts, Mission.type, Mission.alert5MissionType, NreqMin, NreqMax, TargetVec2, Payloads,
Mission.engageRange, Mission.refuelSystem, nil, nil, nil, Mission.attributes, Mission.properties, {Mission.engageWeaponType})
Mission.engageRange, Mission.refuelSystem, nil, nil, MaxWeight, nil, Mission.attributes, Mission.properties, {Mission.engageWeaponType})
return recruited, assets, legions
end

View File

@ -47,7 +47,7 @@ LEGION = {
--- LEGION class version.
-- @field #string version
LEGION.version="0.3.4"
LEGION.version="0.4.0"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- ToDo list
@ -666,8 +666,23 @@ function LEGION:CheckMissionQueue()
for _,_mission in pairs(self.missionqueue) do
local mission=_mission --Ops.Auftrag#AUFTRAG
-- Check if reinforcement is necessary.
local reinforce=false
if mission:IsExecuting() and mission.reinforce and mission.reinforce>0 then
local N=mission:CountOpsGroups()
local Nmin, Nmax=mission:GetRequiredAssets()
if N<Nmin then
reinforce=true
end
end
mission:CountOpsGroups()
-- Firstly, check if mission is due?
if mission:IsQueued(self) and mission:IsReadyToGo() and (mission.importance==nil or mission.importance<=vip) then
if (mission:IsQueued(self) or reinforce) and mission:IsReadyToGo() and (mission.importance==nil or mission.importance<=vip) then
-- Recruit best assets for the job.
local recruited, assets, legions=self:RecruitAssetsForMission(mission)
@ -693,8 +708,11 @@ function LEGION:CheckMissionQueue()
-- Recruit carrier assets for transport.
local Transport=nil
if mission.NcarriersMin then
-- Transport legions.
local Legions=mission.transportLegions or {self}
-- Assign carrier assets for transport.
TransportAvail, Transport=self:AssignAssetsForTransport(Legions, assets, mission.NcarriersMin, mission.NcarriersMax, mission.transportDeployZone, mission.transportDisembarkZone)
end
@ -706,8 +724,10 @@ function LEGION:CheckMissionQueue()
end
if EscortAvail and TransportAvail then
-- Got a mission.
self:MissionRequest(mission)
return true
else
-- Recruited assets but no requested escort available. Unrecruit assets!
@ -2134,28 +2154,38 @@ function LEGION:RecruitAssetsForMission(Mission)
-- Payloads.
local Payloads=Mission.payloads
-- Get special escort legions and/or cohorts.
local Cohorts={}
for _,_legion in pairs(Mission.specialLegions or {}) do
local legion=_legion --Ops.Legion#LEGION
for _,_cohort in pairs(legion.cohorts) do
-- Largest cargo bay available of available carrier assets if mission assets need to be transported.
local MaxWeight=nil
if Mission.NcarriersMin then
-- Get transport cohorts.
local Cohorts=LEGION._GetCohorts(Mission.transportLegions or {self}, Mission.transportCohorts or self.cohorts)
-- Filter cohorts that can actually perform transport missions.
local transportcohorts={}
for _,_cohort in pairs(Cohorts) do
local cohort=_cohort --Ops.Cohort#COHORT
table.insert(Cohorts, cohort)
-- Check if cohort can perform transport to target.
--TODO: Option to filter transport carrier asset categories, attributes and/or properties.
local can=LEGION._CohortCan(cohort, AUFTRAG.Type.OPSTRANSPORT, Categories, Attributes, Properties, nil, TargetVec2)
-- MaxWeight of cargo assets is limited by the largets available cargo bay. We don't want to select, e.g., tanks that cannot be transported by APCs or helos.
if can and (MaxWeight==nil or cohort.cargobayLimit>MaxWeight) then
MaxWeight=cohort.cargobayLimit
end
end
for _,_cohort in pairs(Mission.specialCohorts or {}) do
local cohort=_cohort --Ops.Cohort#COHORT
table.insert(Cohorts, cohort)
end
-- No escort cohorts/legions given ==> take own cohorts.
if #Cohorts==0 then
Cohorts=self.cohorts
self:T(self.lid..string.format("Largest cargo bay available=%.1f", MaxWeight))
end
-- Get cohorts.
local Cohorts=LEGION._GetCohorts(Mission.specialLegions or {self}, Mission.specialCohorts or self.cohorts, Operation, OpsQueue)
-- Recuit assets.
local recruited, assets, legions=LEGION.RecruitCohortAssets(Cohorts, Mission.type, Mission.alert5MissionType, NreqMin, NreqMax, TargetVec2, Payloads,
Mission.engageRange, Mission.refuelSystem, nil, nil, nil, Mission.attributes, Mission.properties, {Mission.engageWeaponType})
Mission.engageRange, Mission.refuelSystem, nil, nil, MaxWeight, nil, Mission.attributes, Mission.properties, {Mission.engageWeaponType})
return recruited, assets, legions
end
@ -2249,41 +2279,117 @@ function LEGION:RecruitAssetsForEscort(Mission, Assets)
return true
end
--- Get cohorts.
-- @param #table Legions Special legions.
-- @param #table Cohorts Special cohorts.
-- @param Ops.Operation#OPERATION Operation Operation.
-- @param #table OpsQueue Queue of operations.
-- @return #table Cohorts.
function LEGION._GetCohorts(Legions, Cohorts, Operation, OpsQueue)
OpsQueue=OpsQueue or {}
--- Function that check if a legion or cohort is part of an operation.
local function CheckOperation(LegionOrCohort)
-- No operations ==> no problem!
if #OpsQueue==0 then
return true
end
-- Cohort is not dedicated to a running(!) operation. We assume so.
local isAvail=true
-- Only available...
if Operation then
isAvail=false
end
for _,_operation in pairs(OpsQueue) do
local operation=_operation --Ops.Operation#OPERATION
-- Legion is assigned to this operation.
local isOps=operation:IsAssignedCohortOrLegion(LegionOrCohort)
if isOps and operation:IsRunning() then
-- Is dedicated.
isAvail=false
if Operation==nil then
-- No Operation given and this is dedicated to at least one operation.
return false
else
if Operation.uid==operation.uid then
-- Operation given and is part of it.
return true
end
end
end
end
return isAvail
end
-- Chosen cohorts.
local cohorts={}
-- Check if there are any special legions and/or cohorts.
if (Legions and #Legions>0) or (Cohorts and #Cohorts>0) then
-- Add cohorts of special legions.
for _,_legion in pairs(Legions or {}) do
local legion=_legion --Ops.Legion#LEGION
-- Check that runway is operational.
local Runway=legion:IsAirwing() and legion:IsRunwayOperational() or true
-- Legion has to be running.
if legion:IsRunning() and Runway then
-- Add cohorts of legion.
for _,_cohort in pairs(legion.cohorts) do
local cohort=_cohort --Ops.Cohort#COHORT
if (CheckOperation(cohort.legion) or CheckOperation(cohort)) and not UTILS.IsInTable(cohorts, cohort, "name") then
table.insert(cohorts, cohort)
end
end
end
end
-- Add special cohorts.
for _,_cohort in pairs(Cohorts or {}) do
local cohort=_cohort --Ops.Cohort#COHORT
if CheckOperation(cohort) and not UTILS.IsInTable(cohorts, cohort, "name") then
table.insert(cohorts, cohort)
end
end
end
return cohorts
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Recruiting and Optimization Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Recruit assets from Cohorts for the given parameters. **NOTE** that we set the `asset.isReserved=true` flag so it cant be recruited by anyone else.
-- @param #table Cohorts Cohorts included.
-- @param #string MissionTypeRecruit Mission type for recruiting the cohort assets.
-- @param #string MissionTypeOpt Mission type for which the assets are optimized. Default is the same as `MissionTypeRecruit`.
-- @param #number NreqMin Minimum number of required assets.
-- @param #number NreqMax Maximum number of required assets.
-- @param DCS#Vec2 TargetVec2 Target position as 2D vector.
-- @param #table Payloads Special payloads.
-- @param #number RangeMax Max range in meters.
-- @param #number RefuelSystem Refuelsystem.
-- @param #number CargoWeight Cargo weight for recruiting transport carriers.
-- @param #number TotalWeight Total cargo weight in kg.
-- @param Ops.Cohort#COHORT Cohort The Cohort.
-- @param #string MissionType Misson type(s).
-- @param #table Categories Group categories.
-- @param #table Attributes Group attributes. See `GROUP.Attribute.`
-- @param #table Properties DCS attributes.
-- @param #table WeaponTypes Bit of weapon types.
-- @return #boolean If `true` enough assets could be recruited.
-- @return #table Recruited assets. **NOTE** that we set the `asset.isReserved=true` flag so it cant be recruited by anyone else.
-- @return #table Legions of recruited assets.
function LEGION.RecruitCohortAssets(Cohorts, MissionTypeRecruit, MissionTypeOpt, NreqMin, NreqMax, TargetVec2, Payloads, RangeMax, RefuelSystem, CargoWeight, TotalWeight, Categories, Attributes, Properties, WeaponTypes)
-- The recruited assets.
local Assets={}
-- Legions of recruited assets.
local Legions={}
-- Set MissionTypeOpt to Recruit if nil.
if MissionTypeOpt==nil then
MissionTypeOpt=MissionTypeRecruit
end
-- @param DCS#Vec2 TargetVec2 Target position.
-- @param RangeMax Max range in meters.
-- @param #number RefuelSystem Refueling system (boom or probe).
-- @param #number CargoWeight Cargo weight [kg]. This checks the cargo bay of the cohort assets and ensures that it is large enough to carry the given cargo weight.
-- @param #number MaxWeight Max weight [kg]. This checks whether the cohort asset group is not too heavy.
-- @return #boolean Returns `true` if given cohort can meet all requirements.
function LEGION._CohortCan(Cohort, MissionType, Categories, Attributes, Properties, WeaponTypes, TargetVec2, RangeMax, RefuelSystem, CargoWeight, MaxWeight)
--- Function to check category.
local function CheckCategory(_cohort)
@ -2351,8 +2457,8 @@ function LEGION.RecruitCohortAssets(Cohorts, MissionTypeRecruit, MissionTypeOpt,
end
end
-- Loops over cohorts.
for _,_cohort in pairs(Cohorts) do
--- Function to check range.
local function CheckRange(_cohort)
local cohort=_cohort --Ops.Cohort#COHORT
-- Distance to target.
@ -2362,50 +2468,175 @@ function LEGION.RecruitCohortAssets(Cohorts, MissionTypeRecruit, MissionTypeOpt,
local Rmax=cohort:GetMissionRange(WeaponTypes)
local InRange=(RangeMax and math.max(RangeMax, Rmax) or Rmax) >= TargetDistance
return InRange
end
--- Function to check weapon type.
local function CheckRefueling(_cohort)
local cohort=_cohort --Ops.Cohort#COHORT
-- Has the requested refuelsystem?
local Refuel=RefuelSystem~=nil and (RefuelSystem==cohort.tankerSystem) or true
--local Refuel=RefuelSystem~=nil and (RefuelSystem==cohort.tankerSystem) or true
-- STRANGE: Why did the above line did not give the same result?! Above Refuel is always true!
local Refuel=true
if RefuelSystem then
if cohort.tankerSystem then
Refuel=RefuelSystem==cohort.tankerSystem
return RefuelSystem==cohort.tankerSystem
else
Refuel=false
return false
end
else
return true
end
end
--- Function to check cargo weight.
local function CheckCargoWeight(_cohort)
local cohort=_cohort --Ops.Cohort#COHORT
if CargoWeight~=nil then
return cohort.cargobayLimit>=CargoWeight
else
return true
end
end
--- Function to check cargo weight.
local function CheckMaxWeight(_cohort)
local cohort=_cohort --Ops.Cohort#COHORT
if MaxWeight~=nil then
cohort:I(string.format("Cohort weight=%.1f | max weight=%.1f", cohort.weightAsset, MaxWeight))
return cohort.weightAsset<=MaxWeight
else
return true
end
end
-- Is capable of the mission type?
local Capable=AUFTRAG.CheckMissionCapability({MissionTypeRecruit}, cohort.missiontypes)
local can=AUFTRAG.CheckMissionCapability(MissionType, Cohort.missiontypes)
-- Can carry the cargo?
local CanCarry=CargoWeight and cohort.cargobayLimit>=CargoWeight or true
-- Right category.
local RightCategory=CheckCategory(cohort)
-- Right attribute.
local RightAttribute=CheckAttribute(cohort)
-- Right property (DCS attribute).
local RightProperty=CheckProperty(cohort)
-- Right weapon type.
local RightWeapon=CheckWeapon(cohort)
-- Cohort ready to execute mission.
local Ready=cohort:IsOnDuty()
if MissionTypeRecruit==AUFTRAG.Type.RELOCATECOHORT then
Ready=cohort:IsRelocating()
Capable=true
if can then
can=CheckCategory(Cohort)
else
env.info(string.format("Cohort %s cannot because of mission types", Cohort.name))
return false
end
-- Debug info.
cohort:T(cohort.lid..string.format("State=%s: Capable=%s, InRange=%s, Refuel=%s, CanCarry=%s, Category=%s, Attribute=%s, Property=%s, Weapon=%s",
cohort:GetState(), tostring(Capable), tostring(InRange), tostring(Refuel), tostring(CanCarry), tostring(RightCategory), tostring(RightAttribute), tostring(RightProperty), tostring(RightWeapon)))
if can then
if MissionType==AUFTRAG.Type.RELOCATECOHORT then
can=Cohort:IsRelocating()
else
can=Cohort:IsOnDuty()
end
else
env.info(string.format("Cohort %s cannot because of category", Cohort.name))
BASE:I(Categories)
BASE:I(Cohort.category)
return false
end
if can then
can=CheckAttribute(Cohort)
else
env.info(string.format("Cohort %s cannot because of readyiness", Cohort.name))
return false
end
if can then
can=CheckProperty(Cohort)
else
env.info(string.format("Cohort %s cannot because of attribute", Cohort.name))
return false
end
if can then
can=CheckWeapon(Cohort)
else
env.info(string.format("Cohort %s cannot because of property", Cohort.name))
return false
end
if can then
can=CheckRange(Cohort)
else
env.info(string.format("Cohort %s cannot because of weapon type", Cohort.name))
return false
end
if can then
can=CheckRefueling(Cohort)
else
env.info(string.format("Cohort %s cannot because of range", Cohort.name))
return false
end
if can then
can=CheckCargoWeight(Cohort)
else
env.info(string.format("Cohort %s cannot because of refueling system", Cohort.name))
return false
end
if can then
can=CheckMaxWeight(Cohort)
else
env.info(string.format("Cohort %s cannot because of cargo weight", Cohort.name))
return false
end
if can then
return true
else
env.info(string.format("Cohort %s cannot because of max weight", Cohort.name))
return false
end
return nil
end
--- Recruit assets from Cohorts for the given parameters. **NOTE** that we set the `asset.isReserved=true` flag so it cant be recruited by anyone else.
-- @param #table Cohorts Cohorts included.
-- @param #string MissionTypeRecruit Mission type for recruiting the cohort assets.
-- @param #string MissionTypeOpt Mission type for which the assets are optimized. Default is the same as `MissionTypeRecruit`.
-- @param #number NreqMin Minimum number of required assets.
-- @param #number NreqMax Maximum number of required assets.
-- @param DCS#Vec2 TargetVec2 Target position as 2D vector.
-- @param #table Payloads Special payloads.
-- @param #number RangeMax Max range in meters.
-- @param #number RefuelSystem Refuelsystem.
-- @param #number CargoWeight Cargo weight for recruiting transport carriers.
-- @param #number TotalWeight Total cargo weight in kg.
-- @param #number MaxWeight Max weight [kg] of the asset group.
-- @param #table Categories Group categories.
-- @param #table Attributes Group attributes. See `GROUP.Attribute.`
-- @param #table Properties DCS attributes.
-- @param #table WeaponTypes Bit of weapon types.
-- @return #boolean If `true` enough assets could be recruited.
-- @return #table Recruited assets. **NOTE** that we set the `asset.isReserved=true` flag so it cant be recruited by anyone else.
-- @return #table Legions of recruited assets.
function LEGION.RecruitCohortAssets(Cohorts, MissionTypeRecruit, MissionTypeOpt, NreqMin, NreqMax, TargetVec2, Payloads, RangeMax, RefuelSystem, CargoWeight, TotalWeight, MaxWeight, Categories, Attributes, Properties, WeaponTypes)
-- The recruited assets.
local Assets={}
-- Legions of recruited assets.
local Legions={}
-- Set MissionTypeOpt to Recruit if nil.
if MissionTypeOpt==nil then
MissionTypeOpt=MissionTypeRecruit
end
-- Loops over cohorts.
for _,_cohort in pairs(Cohorts) do
local cohort=_cohort --Ops.Cohort#COHORT
-- Check if cohort can do the mission.
local can=LEGION._CohortCan(cohort, MissionTypeRecruit, Categories, Attributes, Properties, WeaponTypes, TargetVec2, RangeMax, RefuelSystem, CargoWeight, MaxWeight)
-- Check OnDuty, capable, in range and refueling type (if TANKER).
if Ready and Capable and InRange and Refuel and CanCarry and RightCategory and RightAttribute and RightProperty and RightWeapon then
if can then
-- Recruit assets from cohort.
local assets, npayloads=cohort:RecruitAssets(MissionTypeRecruit, 999)
@ -2456,23 +2687,30 @@ function LEGION.RecruitCohortAssets(Cohorts, MissionTypeRecruit, MissionTypeOpt,
-- Found enough assets
---
-- Add assets to mission.
-- Total cargo bay of all carrier assets.
local cargobay=0
-- Add assets to mission.
for i=1,Nassets do
local asset=Assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem
-- Asset is reserved and will not be picked for other missions.
asset.isReserved=true
-- Add legion.
Legions[asset.legion.alias]=asset.legion
-- Check if total cargo weight was given.
if TotalWeight then
-- Number of
local N=math.floor(asset.cargobaytot/asset.nunits / CargoWeight)*asset.nunits
--env.info(string.format("cargobaytot=%d, cargoweight=%d ==> N=%d", asset.cargobaytot, CargoWeight, N))
-- Sum up total cargo bay of all carrier assets.
cargobay=cargobay + N*CargoWeight
-- Check if enough carrier assets were found to transport all cargo.
if cargobay>=TotalWeight then
--env.info(string.format("FF found enough assets to transport all cargo! N=%d [%d], cargobay=%.1f >= %.1f kg total weight", i, Nassets, cargobay, TotalWeight))
Nassets=i
@ -2580,7 +2818,7 @@ function LEGION:AssignAssetsForEscort(Cohorts, Assets, NescortMin, NescortMax, M
TargetTypes=TargetTypes or targetTypes
-- Recruit escort asset for the mission asset.
local Erecruited, eassets, elegions=LEGION.RecruitCohortAssets(Cohorts, AUFTRAG.Type.ESCORT, MissionType, NescortMin, NescortMax, TargetVec2, nil, nil, nil, nil, nil, Categories)
local Erecruited, eassets, elegions=LEGION.RecruitCohortAssets(Cohorts, AUFTRAG.Type.ESCORT, MissionType, NescortMin, NescortMax, TargetVec2, nil, nil, nil, nil, nil, nil, Categories)
if Erecruited then
Escorts[asset.spawngroupname]={EscortLegions=elegions, EscortAssets=eassets, ecategory=asset.category}
@ -2685,24 +2923,8 @@ function LEGION:AssignAssetsForTransport(Legions, CargoAssets, NcarriersMin, Nca
-- Is an escort requested in the first place?
if NcarriersMin and NcarriersMax and (NcarriersMin>0 or NcarriersMax>0) then
-- Cohorts.
local Cohorts={}
for _,_legion in pairs(Legions) do
local legion=_legion --Ops.Legion#LEGION
-- Check that runway is operational.
local Runway=legion:IsAirwing() and legion:IsRunwayOperational() or true
if legion:IsRunning() and Runway then
-- Loops over cohorts.
for _,_cohort in pairs(legion.cohorts) do
local cohort=_cohort --Ops.Cohort#COHORT
table.insert(Cohorts, cohort)
end
end
end
-- Get cohorts.
local Cohorts=LEGION._GetCohorts(Legions)
-- Get all legions and heaviest cargo group weight
local CargoLegions={} ; local CargoWeight=nil ; local TotalWeight=0
@ -2715,12 +2937,16 @@ function LEGION:AssignAssetsForTransport(Legions, CargoAssets, NcarriersMin, Nca
TotalWeight=TotalWeight+asset.weight
end
-- Debug info.
self:T(self.lid..string.format("Cargo weight=%.1f", CargoWeight))
self:T(self.lid..string.format("Total weight=%.1f", TotalWeight))
-- Target is the deploy zone.
local TargetVec2=DeployZone:GetVec2()
-- Recruit assets and legions.
local TransportAvail, CarrierAssets, CarrierLegions=
LEGION.RecruitCohortAssets(Cohorts, AUFTRAG.Type.OPSTRANSPORT, nil, NcarriersMin, NcarriersMax, TargetVec2, nil, nil, nil, CargoWeight, TotalWeight, Categories, Attributes, Properties)
LEGION.RecruitCohortAssets(Cohorts, AUFTRAG.Type.OPSTRANSPORT, nil, NcarriersMin, NcarriersMax, TargetVec2, nil, nil, nil, CargoWeight, TotalWeight, nil, Categories, Attributes, Properties)
if TransportAvail then
@ -2922,7 +3148,7 @@ function LEGION._OptimizeAssetSelection(assets, MissionType, TargetVec2, Include
local text=string.format("Optimized %d assets for %s mission/transport (payload=%s):", #assets, MissionType, tostring(IncludePayload))
for i,Asset in pairs(assets) do
local asset=Asset --Functional.Warehouse#WAREHOUSE.Assetitem
text=text..string.format("\n%s %s: score=%d", asset.squadname, asset.spawngroupname, asset.score)
text=text..string.format("\n%s %s: score=%d", asset.squadname, asset.spawngroupname, asset.score or -1)
asset.score=nil
end
env.info(text)

View File

@ -4,6 +4,7 @@
--
-- * Define operation phases
-- * Define conditions when phases are over
-- * Option to have branches in the phase tree
-- * Dedicate resources to operations
--
-- ===
@ -25,10 +26,14 @@
-- @type OPERATION
-- @field #string ClassName Name of the class.
-- @field #number verbose Verbosity level.
-- @field #number uid Unique ID of the operation.
-- @field #string lid Class id string for output to DCS log file.
-- @field #string name Name of the operation.
-- @field #number Tstart Start time in seconds of abs mission time.
-- @field #number Tstop Stop time in seconds of abs mission time.
-- @field #number duration Duration of the operation in seconds.
-- @field Core.Condition#CONDITION conditionStart Start condition.
-- @field Core.Condition#CONDITION conditionStop Stop condition.
-- @field Core.Condition#CONDITION conditionOver Over condition.
-- @field #table branches Branches.
-- @field #OPERATION.Branch branchMaster Master branch.
-- @field #OPERATION.Branch branchActive Active branch.
@ -89,6 +94,9 @@ _OPERATIONID=0
-- @field #string name Name of the phase.
-- @field Core.Condition#CONDITION conditionOver Conditions when the phase is over.
-- @field #string status Phase status.
-- @field #number Tstart Abs. mission time when the phase was started.
-- @field #number nActive Number of times the phase was active.
-- @field #number duration Duration in seconds how long the phase should be active after it started.
-- @field #OPERATION.Branch branch The branch this phase belongs to.
--- Operation branch.
@ -120,14 +128,15 @@ OPERATION.PhaseStatus={
--- OPERATION class version.
-- @field #string version
OPERATION.version="0.1.0"
OPERATION.version="0.2.0"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO: Branches?
-- TODO: "Over" conditions.
-- TODO: Repeat phases: after over ==> planned (not over)
-- DONE: Branches.
-- DONE: Phases.
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
@ -161,6 +170,15 @@ function OPERATION:New(Name)
-- Master branch.
self.branchMaster=self:AddBranch("Master")
self.conditionStart=CONDITION:New("Operation %s start", self.name)
self.conditionStart:SetNoneResult(false) --If no condition function is specified, the ops will NOT be over.
self.conditionStart:SetDefaultPersistence(false)
self.conditionOver=CONDITION:New("Operation %s over", self.name)
self.conditionOver:SetNoneResult(false)
self.conditionOver:SetDefaultPersistence(false)
-- Set master as active branch.
self.branchActive=self.branchMaster
@ -197,6 +215,12 @@ function OPERATION:New(Name)
-- @param #OPERATION self
-- @param #number delay Delay in seconds.
--- On after "Start" event.
-- @function [parent=#OPERATION] OnAfterStart
-- @param #OPERATION self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
--- Triggers the FSM event "Stop".
-- @function [parent=#OPERATION] Stop
@ -279,12 +303,14 @@ function OPERATION:New(Name)
-- @function [parent=#OPERATION] BranchSwitch
-- @param #OPERATION self
-- @param #OPERATION.Branch Branch The branch that is now active.
-- @param #OPERATION.Phase Phase The new phase.
--- Triggers the FSM event "BranchSwitch" after a delay.
-- @function [parent=#OPERATION] __BranchSwitch
-- @param #OPERATION self
-- @param #number delay Delay in seconds.
-- @param #OPERATION.Branch Branch The branch that is now active.
-- @param #OPERATION.Phase Phase The new phase.
--- On after "BranchSwitch" event.
-- @function [parent=#OPERATION] OnAfterBranchSwitch
@ -293,7 +319,7 @@ function OPERATION:New(Name)
-- @param #string Event Event.
-- @param #string To To state.
-- @param #OPERATION.Branch Branch The branch that is now active.
-- @param #OPERATION.Phase Phase The new phase.
--- Triggers the FSM event "Over".
-- @function [parent=#OPERATION] Over
@ -366,12 +392,34 @@ function OPERATION:SetTime(ClockStart, ClockStop)
return self
end
--- Add (all) condition function when the whole operation is over. Must return a `#boolean`.
-- @param #OPERATION self
-- @param #function Function Function that needs to be `true` before the operation is over.
-- @param ... Condition function arguments if any.
-- @return Core.Condition#CONDITION.Function Condition function table.
function OPERATION:AddConditonOverAll(Function, ...)
local cf=self.conditionOver:AddFunctionAll(Function, ...)
return cf
end
--- Add (any) condition function when the whole operation is over. Must return a `#boolean`.
-- @param #OPERATION self
-- @param #function Function Function that needs to be `true` before the operation is over.
-- @param ... Condition function arguments if any.
-- @return Core.Condition#CONDITION.Function Condition function table.
function OPERATION:AddConditonOverAny(Phase, Function, ...)
local cf=self.conditionOver:AddFunctionAny(Function, ...)
return cf
end
--- Add a new phase to the operation. This is added add the end of all previously added phases (if any).
-- @param #OPERATION self
-- @param #string Name Name of the phase. Default "Phase-01" where the last number is a running number.
-- @param #OPERATION.Branch Branch The branch to which this phase is added. Default is the master branch.
-- @param #number Duration Duration in seconds how long the phase will last. Default `nil`=forever.
-- @return #OPERATION.Phase Phase table object.
function OPERATION:AddPhase(Name, Branch)
function OPERATION:AddPhase(Name, Branch, Duration)
-- Branch.
Branch=Branch or self.branchMaster
@ -382,6 +430,8 @@ function OPERATION:AddPhase(Name, Branch)
-- Branch of phase
phase.branch=Branch
-- Set duraction of pahse (if any).
phase.duration=Duration
-- Debug output.
self:T(self.lid..string.format("Adding phase %s to branch %s", phase.name, Branch.name))
@ -413,6 +463,12 @@ function OPERATION:InsertPhaseAfter(PhaseAfter, Name)
return nil
end
--- Get a name of this operation.
-- @param #OPERATION self
-- @return #string Name of this operation or "Unknown".
function OPERATION:GetName()
return self.name or "Unknown"
end
--- Get a phase by its name.
-- @param #OPERATION self
@ -439,10 +495,26 @@ end
-- @param #string Status New status, *e.g.* `OPERATION.PhaseStatus.OVER`.
-- @return #OPERATION self
function OPERATION:SetPhaseStatus(Phase, Status)
if Phase then
self:T(self.lid..string.format("Phase %s status: %s-->%s"), Phase.status, Status)
-- Debug message.
self:T(self.lid..string.format("Phase %s status: %s-->%s", tostring(Phase.name), tostring(Phase.status), tostring(Status)))
-- Set status.
Phase.status=Status
-- Set time stamp when phase becase active.
if Phase.status==OPERATION.PhaseStatus.ACTIVE then
Phase.Tstart=timer.getAbsTime()
Phase.nActive=Phase.nActive+1
elseif Phase.status==OPERATION.PhaseStatus.OVER then
-- Trigger PhaseOver event.
self:PhaseOver(Phase)
end
end
return self
end
@ -461,7 +533,7 @@ end
-- @return #OPERATION self
function OPERATION:SetPhaseConditonOver(Phase, Condition)
if Phase then
self:T(self.lid..string.format("Setting phase %s conditon over %s"), Phase.name, Condition and Condition.name or "None")
self:T(self.lid..string.format("Setting phase %s conditon over %s", self:GetPhaseName(Phase), Condition and Condition.name or "None"))
Phase.conditionOver=Condition
end
return self
@ -472,12 +544,13 @@ end
-- @param #OPERATION.Phase Phase The phase.
-- @param #function Function Function that needs to be `true` before the phase is over.
-- @param ... Condition function arguments if any.
-- @return #OPERATION self
-- @return Core.Condition#CONDITION.Function Condition function table.
function OPERATION:AddPhaseConditonOverAll(Phase, Function, ...)
if Phase then
Phase.conditionOver:AddFunctionAll(Function, ...)
local cf=Phase.conditionOver:AddFunctionAll(Function, ...)
return cf
end
return self
return nil
end
--- Add condition function when the given phase is over. Must return a `#boolean`.
@ -485,16 +558,41 @@ end
-- @param #OPERATION.Phase Phase The phase.
-- @param #function Function Function that needs to be `true` before the phase is over.
-- @param ... Condition function arguments if any.
-- @return #OPERATION self
-- @return Core.Condition#CONDITION.Function Condition function table.
function OPERATION:AddPhaseConditonOverAny(Phase, Function, ...)
if Phase then
Phase.conditionOver:AddFunctionAny(Function, ...)
local cf=Phase.conditionOver:AddFunctionAny(Function, ...)
return cf
end
return nil
end
--- Set persistence of condition function. By default, condition functions are removed after a phase is over.
-- @param #OPERATION self
-- @param Core.Condition#CONDITION.Function ConditionFunction Condition function table.
-- @param #boolean IsPersistent If `true` or `nil`, condition function is persistent.
-- @return #OPERATION self
function OPERATION:SetConditionFunctionPersistence(ConditionFunction, IsPersistent)
ConditionFunction.persistence=IsPersistent
return self
end
--- Add condition function when the given phase is to be repeated. The provided function must return a `#boolean`.
-- If the condition evaluation returns `true`, the phase is set to state `Planned` instead of `Over` and can be repeated.
-- @param #OPERATION self
-- @param #OPERATION.Phase Phase The phase.
-- @param #function Function Function that needs to be `true` before the phase is over.
-- @param ... Condition function arguments if any.
-- @return #OPERATION self
function OPERATION:AddPhaseConditonRepeatAll(Phase, Function, ...)
if Phase then
Phase.conditionRepeat:AddFunctionAll(Function, ...)
end
return self
end
--- Get codition when the given phase is over.
--- Get condition when the given phase is over.
-- @param #OPERATION self
-- @param #OPERATION.Phase Phase The phase.
-- @return Core.Condition#CONDITION Condition when the phase is over (if any).
@ -502,24 +600,12 @@ function OPERATION:GetPhaseConditonOver(Phase, Condition)
return Phase.conditionOver
end
--- Get currrently active phase.
--- Get how many times a phase has been active.
-- @param #OPERATION self
-- @param #OPERATION.Phase Phase The phase.
-- @param #string Status New status, e.g. `OPERATION.PhaseStatus.OVER`.
-- @return #OPERATION self
function OPERATION:SetPhaseStatus(Phase, Status)
if Phase then
self:T(self.lid..string.format("Phase \"%s\" status: %s-->%s", Phase.name, Phase.status, Status))
Phase.status=Status
end
return self
end
--- Get currrently active phase.
-- @param #OPERATION self
-- @return #OPERATION.Phase Current phase or `nil` if no current phase is active.
function OPERATION:GetPhaseActive()
return self.phase
-- @return #number Number of times the phase has been active.
function OPERATION:GetPhaseNactive(Phase)
return Phase.nActive
end
--- Get name of a phase.
@ -537,18 +623,11 @@ function OPERATION:GetPhaseName(Phase)
return "None"
end
--- Check if a phase is the currently active one.
--- Get currrently active phase.
-- @param #OPERATION self
-- @param #OPERATION.Phase Phase The phase to check.
-- @return #boolean If `true`, this phase is currently active.
function OPERATION:IsPhaseActive(Phase)
local phase=self:GetPhaseActive()
if phase and phase.uid==Phase.uid then
return true
else
return false
end
return nil
-- @return #OPERATION.Phase Current phase or `nil` if no current phase is active.
function OPERATION:GetPhaseActive()
return self.phase
end
--- Get index of phase.
@ -657,6 +736,13 @@ function OPERATION:AddBranch(Name)
return branch
end
--- Get the master branch. This is the default branch and should always exist (if it was not explicitly deleted).
-- @param #OPERATION self
-- @return #OPERATION.Branch The master branch.
function OPERATION:GetBranchMaster()
return self.branchMaster
end
--- Get the currently active branch.
-- @param #OPERATION self
-- @return #OPERATION.Branch The active branch. If no branch is active, the master branch is returned.
@ -678,20 +764,26 @@ end
--- Add an edge between two branches.
-- @param #OPERATION self
-- @param #OPERATION.Branch BranchTo The branch *to* which to switch.
-- @param #OPERATION.Phase PhaseAfter The phase of the *from* branch *after* which to switch.
-- @param #OPERATION.Phase PhaseNext The phase of the *to* branch *to* which to switch.
-- @param #OPERATION.Phase PhaseFrom The phase of the *from* branch *after* which to switch.
-- @param #OPERATION.Phase PhaseTo The phase of the *to* branch *to* which to switch.
-- @param Core.Condition#CONDITION ConditionSwitch (Optional) Condition(s) when to switch the branches.
-- @return #OPERATION.Branch Branch table object.
function OPERATION:AddEdge(BranchTo, PhaseAfter, PhaseNext, ConditionSwitch)
-- @return #OPERATION.Edge Edge table object.
function OPERATION:AddEdge(PhaseFrom, PhaseTo, ConditionSwitch)
local edge={} --#OPERATION.Edge
edge.branchFrom=PhaseAfter and PhaseAfter.branch or self.branchMaster
edge.phaseFrom=PhaseAfter
edge.branchTo=BranchTo
edge.phaseTo=PhaseNext
edge.conditionSwitch=ConditionSwitch or CONDITION:New("Edge")
edge.phaseFrom=PhaseFrom
edge.phaseTo=PhaseTo
edge.branchFrom=PhaseFrom.branch
edge.branchTo=PhaseTo.branch
if ConditionSwitch then
edge.conditionSwitch=ConditionSwitch
else
edge.conditionSwitch=CONDITION:New("Edge")
edge.conditionSwitch:SetNoneResult(true)
end
table.insert(edge.branchFrom.edges, edge)
@ -699,16 +791,18 @@ function OPERATION:AddEdge(BranchTo, PhaseAfter, PhaseNext, ConditionSwitch)
end
--- Add condition function to an edge when branches are switched. The function must return a `#boolean`.
-- If multiple condition functions are added, all of these must return true for the branch switch to occur.
-- @param #OPERATION self
-- @param #OPERATION.Edge Edge The edge connecting the two branches.
-- @param #function Function Function that needs to be `true` for switching between the branches.
-- @param ... Condition function arguments if any.
-- @return #OPERATION self
-- @return Core.Condition#CONDITION.Function Condition function table.
function OPERATION:AddEdgeConditonSwitchAll(Edge, Function, ...)
if Edge then
Edge.conditionSwitch:AddFunctionAll(Function, ...)
local cf=Edge.conditionSwitch:AddFunctionAll(Function, ...)
return cf
end
return self
return nil
end
--- Add mission to operation.
@ -739,9 +833,9 @@ function OPERATION:AddTarget(Target, Phase)
return self
end
--- Add Targets from operation.
--- Get targets of operation.
-- @param #OPERATION self
-- @param #OPERATION.Phase Phase
-- @param #OPERATION.Phase Phase (Optional) Only return targets set for this phase. Default is targets of all phases.
-- @return #table Targets Table of #TARGET objects
function OPERATION:GetTargets(Phase)
local N = {}
@ -896,6 +990,61 @@ function OPERATION:IsStopped()
return is
end
--- Check if operation is **not** "Over" or "Stopped".
-- @param #OPERATION self
-- @return #boolean If `true`, operation is not "Over" or "Stopped".
function OPERATION:IsNotOver()
local is=not (self:IsOver() or self:IsStopped())
return is
end
--- Check if phase is in status "Active".
-- @param #OPERATION self
-- @param #OPERATION.Phase Phase The phase.
-- @return #boolean If `true`, phase is active.
function OPERATION:IsPhaseActive(Phase)
if Phase and Phase.status and Phase.status==OPERATION.PhaseStatus.ACTIVE then
return true
end
return false
end
--- Check if a phase is the currently active one.
-- @param #OPERATION self
-- @param #OPERATION.Phase Phase The phase to check.
-- @return #boolean If `true`, this phase is currently active.
function OPERATION:IsPhaseActive(Phase)
local phase=self:GetPhaseActive()
if phase and phase.uid==Phase.uid then
return true
else
return false
end
return nil
end
--- Check if phase is in status "Planned".
-- @param #OPERATION self
-- @param #OPERATION.Phase Phase The phase.
-- @return #boolean If `true`, phase is Planned.
function OPERATION:IsPhasePlanned(Phase)
if Phase and Phase.status and Phase.status==OPERATION.PhaseStatus.PLANNED then
return true
end
return false
end
--- Check if phase is in status "Over".
-- @param #OPERATION self
-- @param #OPERATION.Phase Phase The phase.
-- @return #boolean If `true`, phase is over.
function OPERATION:IsPhaseOver(Phase)
if Phase and Phase.status and Phase.status==OPERATION.PhaseStatus.OVER then
return true
end
return false
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Status Update
@ -927,27 +1076,30 @@ function OPERATION:onafterStatusUpdate(From, Event, To)
-- Current FSM state.
local fsmstate=self:GetState()
-- Start operation.
if self:IsPlanned() then
if self.Tstart and Tnow>self.Tstart then
-- Start operation if start time has passed (if any) and start condition(s) are met (if any).
if (self.Tstart and Tnow>self.Tstart or self.Tstart==nil) and (self.conditionStart==nil or self.conditionStart:Evaluate()) then
self:Start()
end
end
if (self.Tstop and Tnow>self.Tstop) and not (self:IsOver() or self:IsStopped()) then
elseif self:IsNotOver() then
-- Operation is over if stop time has passed (if any) and over condition(s) are met (if any).
if (self.Tstop and Tnow>self.Tstop or self.Tstop==nil) and (self.conditionOver==nil or self.conditionOver:Evaluate()) then
self:Over()
end
if (not self:IsRunning()) and (self.conditionStart and self.conditionStart:Evaluate()) then
self:Start()
end
if self:IsRunning() and (self.conditionStop and self.conditionStop:Evaluate()) then
self:Over()
end
-- Check phases.
if self:IsRunning() then
self:_CheckPhases()
end
-- Debug output.
if self.verbose>=1 then
@ -972,7 +1124,7 @@ function OPERATION:onafterStatusUpdate(From, Event, To)
local text="Phases:"
for i,_phase in pairs(self.branchActive.phases) do
local phase=_phase --#OPERATION.Phase
text=text..string.format("\n[%d] %s: status=%s", i, phase.name, tostring(phase.status))
text=text..string.format("\n[%d] %s [uid=%d]: status=%s Nact=%d", i, phase.name, phase.uid, tostring(phase.status), phase.nActive)
end
if text=="Phases:" then text=text.." None" end
self:I(self.lid..text)
@ -1026,7 +1178,9 @@ function OPERATION:onafterPhaseChange(From, Event, To, Phase)
-- Previous phase (if any).
local oldphase="None"
if self.phase then
if self.phase.status~=OPERATION.PhaseStatus.OVER then
self:SetPhaseStatus(self.phase, OPERATION.PhaseStatus.OVER)
end
oldphase=self.phase.name
end
@ -1042,13 +1196,27 @@ function OPERATION:onafterPhaseChange(From, Event, To, Phase)
return self
end
--- On after "PhaseOver" event.
-- @param #OPERATION self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #OPERATION.Phase Phase The phase that is over.
function OPERATION:onafterPhaseOver(From, Event, To, Phase)
-- Remove all non-persistant condition functions.
Phase.conditionOver:RemoveNonPersistant()
end
--- On after "BranchSwitch" event.
-- @param #OPERATION self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #OPERATION.Branch Branch The new branch.
function OPERATION:onafterBranchSwitch(From, Event, To, Branch)
-- @param #OPERATION.Phase Phase The phase.
function OPERATION:onafterBranchSwitch(From, Event, To, Branch, Phase)
-- Debug info.
self:T(self.lid..string.format("Switching to branch %s", Branch.name))
@ -1056,6 +1224,9 @@ function OPERATION:onafterBranchSwitch(From, Event, To, Branch)
-- Set active branch.
self.branchActive=Branch
-- Change phase.
self:PhaseChange(Phase)
return self
end
@ -1064,7 +1235,6 @@ end
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #OPERATION.Phase Phase The new phase.
function OPERATION:onafterOver(From, Event, To)
-- Debug message.
@ -1078,9 +1248,11 @@ function OPERATION:onafterOver(From, Event, To)
local branch=_branch --#OPERATION.Branch
for _,_phase in pairs(branch.phases) do
local phase=_phase --#OPERATION.Phase
if not self:IsPhaseOver(phase) then
self:SetPhaseStatus(phase, OPERATION.PhaseStatus.OVER)
end
end
end
return self
end
@ -1098,7 +1270,18 @@ function OPERATION:_CheckPhases()
-- Check if active phase is over if conditon over is defined.
if phase and phase.conditionOver then
-- Evaluate if phase is over.
local isOver=phase.conditionOver:Evaluate()
local Tnow=timer.getAbsTime()
-- Check if duration of phase if over.
if phase.duration and phase.Tstart and Tnow-phase.Tstart>phase.duration then
isOver=true
end
-- Set phase status to over. This also triggers the PhaseOver() event.
if isOver then
self:SetPhaseStatus(phase, OPERATION.PhaseStatus.OVER)
end
@ -1110,6 +1293,11 @@ function OPERATION:_CheckPhases()
for _,_edge in pairs(self.branchActive.edges) do
local edge=_edge --#OPERATION.Edge
if phase then
--env.info(string.format("phase active uid=%d", phase.uid))
--env.info(string.format("Phase from uid=%d", edge.phaseFrom.uid))
end
if (edge.phaseFrom==nil) or (phase and edge.phaseFrom.uid==phase.uid) then
-- Evaluate switch condition.
@ -1117,22 +1305,24 @@ function OPERATION:_CheckPhases()
if switch then
-- Get next phase of the branch
local phaseTo=edge.phaseTo or self:GetPhaseNext(edge.branchTo, nil)
if phaseTo then
-- Switch to new branch.
self:BranchSwitch(edge.branchTo)
self:BranchSwitch(edge.branchTo, phaseTo)
-- If we want to switch to a specific phase of the branch.
if edge.phaseTo then
else
-- Change phase.
self:PhaseChange(edge.phaseTo)
-- No next phase ==> Ops is over!
self:Over()
end
-- Done here!
return
end
-- Break the loop.
break
end
end
end
@ -1157,7 +1347,9 @@ function OPERATION:_CreatePhase(Name)
phase.uid=self.counterPhase
phase.name=Name or string.format("Phase-%02d", self.counterPhase)
phase.conditionOver=CONDITION:New(Name.." Over")
phase.conditionOver:SetDefaultPersistence(false)
phase.status=OPERATION.PhaseStatus.PLANNED
phase.nActive=0
return phase
end

View File

@ -335,6 +335,7 @@ OPSGROUP.TaskType={
-- @field #number waypoint Waypoint index if task is a waypoint task.
-- @field Core.UserFlag#USERFLAG stopflag If flag is set to 1 (=true), the task is stopped.
-- @field #number backupROE Rules of engagement that are restored once the task is over.
-- @field Ops.Target#TARGET target Target object.
--- Option data.
-- @type OPSGROUP.Option
@ -499,7 +500,7 @@ OPSGROUP.CargoStatus={
--- OpsGroup version.
-- @field #string version
OPSGROUP.version="0.8.0"
OPSGROUP.version="0.9.0"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
@ -885,10 +886,10 @@ function OPSGROUP:GetCoalition()
return self.group:GetCoalition()
end
--- Returns the absolute (average) life points of the group.
--- Returns the absolute total life points of the group.
-- @param #OPSGROUP self
-- @param #OPSGROUP.Element Element (Optional) Only get life points of this element.
-- @return #number Life points. If group contains more than one element, the average is given.
-- @return #number Life points, *i.e.* the sum of life points over all units in the group (unless a specific element was passed).
-- @return #number Initial life points.
function OPSGROUP:GetLifePoints(Element)
@ -3316,6 +3317,12 @@ function OPSGROUP:RemoveWaypoint(wpindex)
self.currentwp=self.currentwp-1
end
-- Could be that the waypoint we are currently moving to was the LAST waypoint. Then we now passed the final waypoint.
if (self.adinfinitum or istemp) then
self:_PassedFinalWaypoint(false, "Removed PASSED temporary waypoint ")
end
end
end
@ -4043,11 +4050,16 @@ end
-- @param #string To To state.
-- @param Ops.OpsGroup#OPSGROUP.Task Task The task.
function OPSGROUP:onafterTaskExecute(From, Event, To, Task)
self:T({Task})
-- Debug message.
local text=string.format("Task %s ID=%d execute", tostring(Task.description), Task.id)
-- Debug info.
self:T(self.lid..text)
self:T({Task})
-- Debug info.
self:T2({Task})
-- Cancel current task if there is any.
if self.taskcurrent>0 then
self:TaskCancel()
@ -4070,7 +4082,7 @@ function OPSGROUP:onafterTaskExecute(From, Event, To, Task)
-- Get mission of this task (if any).
local Mission=self:GetMissionByTaskID(self.taskcurrent)
-- Update push DCS task.
self:_UpdateTask(Task, Mission)
-- Set AUFTRAG status.
@ -4080,12 +4092,13 @@ function OPSGROUP:onafterTaskExecute(From, Event, To, Task)
end
--- Push task
--- Update (DCS) task.
-- @param #OPSGROUP self
-- @param Ops.OpsGroup#OPSGROUP.Task Task The task.
-- @param Ops.Auftrag#AUFTRAG Mission The mission.
function OPSGROUP:_UpdateTask(Task, Mission)
local Mission=Mission or self:GetMissionByTaskID(self.taskcurrent)
Mission=Mission or self:GetMissionByTaskID(self.taskcurrent)
if Task.dcstask.id==AUFTRAG.SpecialTask.FORMATION then
@ -4219,6 +4232,14 @@ function OPSGROUP:_UpdateTask(Task, Mission)
local rearmed=self:_CheckAmmoFull()
if rearmed then
self:T2(self.lid.."Ammo already full ==> reaming task done!")
self:TaskDone(Task)
else
self:T2(self.lid.."Ammo not full ==> Rearm()")
self:Rearm()
end
elseif Task.dcstask.id==AUFTRAG.SpecialTask.ALERT5 then
@ -4360,6 +4381,79 @@ function OPSGROUP:_UpdateTask(Task, Mission)
wp.missionUID=Mission and Mission.auftragsnummer or nil
elseif Task.dcstask.id==AUFTRAG.SpecialTask.CAPTUREZONE then
---
-- Task "CaptureZone" Mission.
-- Check if zone was captured or find new target to engage.
---
-- Not enganging already.
if self:IsEngaging() then
-- Group is currently engaging an enemy unit to capture the zone.
self:T2(self.lid..string.format("CaptureZone: Engaging currently!"))
else
-- Get enemy coalitions. We do not include neutrals.
local Coalitions=UTILS.GetCoalitionEnemy(self:GetCoalition(), false)
-- Current target object.
local zoneCurr=Task.target --Ops.OpsZone#OPSZONE
if zoneCurr then
self:T(self.lid..string.format("Current target zone=%s owner=%s", zoneCurr:GetName(), zoneCurr:GetOwnerName()))
if zoneCurr:GetOwner()==self:GetCoalition() then
-- Current zone captured ==> Find next zone or call it a day!
-- Debug info.
self:T(self.lid..string.format("Zone %s captured ==> Task DONE!", zoneCurr:GetName()))
-- Task done.
self:TaskDone(Task)
else
-- Current zone NOT captured yet ==> Find Target
-- Debug info.
self:T(self.lid..string.format("Zone %s NOT captured!", zoneCurr:GetName()))
if Mission:GetGroupStatus(self)==AUFTRAG.GroupStatus.EXECUTING then
-- Debug info.
self:T(self.lid..string.format("Zone %s NOT captured and EXECUTING ==> Find target", zoneCurr:GetName()))
-- Get closest target.
local targetgroup=zoneCurr:GetScannedGroupSet():GetClosestGroup(self.coordinate, Coalitions)
if targetgroup then
-- Debug info.
self:T(self.lid..string.format("Zone %s NOT captured: engaging target %s", zoneCurr:GetName(), targetgroup:GetName()))
-- Engage target group.
self:EngageTarget(targetgroup)
else
-- Error Message.
self:E(self.lid..string.format("ERROR: Current zone not captured but no target group could be found. This should NOT happen!"))
end
else
self:T(self.lid..string.format("Zone %s NOT captured and NOT EXECUTING", zoneCurr:GetName()))
end
end
else
self:T(self.lid..string.format("NO Current target zone=%s"))
end
end
else
-- If task is scheduled (not waypoint) set task.
@ -4643,9 +4737,21 @@ function OPSGROUP:onafterTaskDone(From, Event, To, Task)
-- Check if mission is paused.
if status~=AUFTRAG.GroupStatus.PAUSED then
---
-- Mission is NOT over ==> trigger done
-- Mission is NOT over ==> trigger DONE
---
if Mission.type==AUFTRAG.Type.CAPTUREZONE and Mission:CountMissionTargets()>0 then
-- Remove mission waypoints.
self:T(self.lid.."Remove mission waypoints")
self:_RemoveMissionWaypoints(Mission, false)
self:T(self.lid.."Task done ==> Route to mission for next opszone")
self:MissionStart(Mission)
return
end
-- Get egress waypoint uid.
local EgressUID=Mission:GetGroupEgressWaypointUID(self)
@ -5477,6 +5583,13 @@ function OPSGROUP:RouteToMission(mission, delay)
surfacetypes={land.SurfaceType.WATER, land.SurfaceType.SHALLOW_WATER}
end
-- Get target object.
local targetobject=mission:GetObjective(currentcoord, UTILS.GetCoalitionEnemy(self:GetCoalition(), true))
if targetobject then
self:T(self.lid..string.format("Route to mission target object %s", targetobject:GetName()))
end
-- Get ingress waypoint.
if mission.opstransport and not mission.opstransport:IsCargoDelivered(self.groupname) then
@ -5507,7 +5620,7 @@ function OPSGROUP:RouteToMission(mission, delay)
---
-- Get the zone.
targetzone=mission.engageTarget:GetObject() --Core.Zone#ZONE
targetzone=targetobject --Core.Zone#ZONE
-- Random coordinate.
waypointcoord=targetzone:GetRandomCoordinate(nil , nil, surfacetypes)
@ -5526,7 +5639,7 @@ function OPSGROUP:RouteToMission(mission, delay)
---
-- Get the zone.
targetzone=mission.engageTarget:GetObject() --Core.Zone#ZONE
targetzone=targetobject --Core.Zone#ZONE
-- Random coordinate.
waypointcoord=targetzone:GetRandomCoordinate(nil , nil, surfacetypes)
@ -5536,7 +5649,8 @@ function OPSGROUP:RouteToMission(mission, delay)
-- Hover
---
local zone=mission.engageTarget:GetObject() --Core.Zone#ZONE
local zone=targetobject --Core.Zone#ZONE
waypointcoord=zone:GetCoordinate()
elseif mission.type==AUFTRAG.Type.RELOCATECOHORT then
@ -5562,6 +5676,15 @@ function OPSGROUP:RouteToMission(mission, delay)
waypointcoord=currentcoord:GetIntermediateCoordinate(ToCoordinate, 0.05)
end
elseif mission.type==AUFTRAG.Type.CAPTUREZONE then
-- Get the zone.
targetzone=targetobject:GetZone()
-- Random coordinate.
waypointcoord=targetzone:GetRandomCoordinate(nil , nil, surfacetypes)
else
---
-- Default case
@ -5676,6 +5799,7 @@ function OPSGROUP:RouteToMission(mission, delay)
end
waypoint=ARMYGROUP.AddWaypoint(self, waypointcoord, SpeedToMission, uid, formation, false)
elseif self:IsNavygroup() then
waypoint=NAVYGROUP.AddWaypoint(self, waypointcoord, SpeedToMission, uid, UTILS.MetersToFeet(mission.missionAltitude or self.altitudeCruise), false)
@ -5687,6 +5811,8 @@ function OPSGROUP:RouteToMission(mission, delay)
local waypointtask=self:AddTaskWaypoint(mission.DCStask, waypoint, mission.name, mission.prio, mission.duration)
waypointtask.ismission=true
waypointtask.target=targetobject
-- Set waypoint task.
mission:SetGroupWaypointTask(self, waypointtask)
@ -5712,6 +5838,8 @@ function OPSGROUP:RouteToMission(mission, delay)
if targetzone and self:IsInZone(targetzone) then
self:T(self.lid.."Already in mission zone ==> TaskExecute()")
self:TaskExecute(waypointtask)
-- TODO: Calling PassingWaypoint here is probably better as it marks the mission waypoint as passed!
--self:PassingWaypoint(waypoint)
return
elseif d<25 then
self:T(self.lid.."Already within 25 meters of mission waypoint ==> TaskExecute()")
@ -6070,6 +6198,18 @@ function OPSGROUP:onafterPassingWaypoint(From, Event, To, Waypoint)
-- Final zone reached ==> task done.
self:TaskDone(task)
elseif task and task.dcstask.id==AUFTRAG.SpecialTask.REARMING then
---
-- SPECIAL TASK: Rearming Mission
---
-- Debug info.
self:T(self.lid..string.format("FF Rearming Mission ==> Rearm()"))
-- Call rearm event.
self:Rearm()
else
---
@ -6898,7 +7038,7 @@ function OPSGROUP:onafterElementDamaged(From, Event, To, Element)
local lifepoints=0
if Element.DCSunit and Element.DCSunit:isExist() then
if Element.DCSunit then --and Element.DCSunit:isExist() then
-- Get life of unit
lifepoints=Element.DCSunit:getLife()
@ -7361,9 +7501,11 @@ function OPSGROUP:CancelAllMissions()
-- Cancel all missions.
for _,_mission in pairs(self.missionqueue) do
local mission=_mission --Ops.Auftrag#AUFTRAG
if mission:IsNotOver() then
self:T(self.lid.."Cancelling mission "..tostring(mission:GetName()))
self:MissionCancel(mission)
end
end
end
@ -10118,8 +10260,11 @@ end
-- @return #OPSGROUP self
function OPSGROUP:_CheckDamage()
self:T(self.lid..string.format("Checking damage..."))
self.life=0
local damaged=false
for _,_element in pairs(self.elements) do
local element=_element --Ops.OpsGroup#OPSGROUP.Element
@ -10140,6 +10285,7 @@ function OPSGROUP:_CheckDamage()
end
-- If anyone in the group was damaged, trigger event.
if damaged then
self:Damaged()
end
@ -10179,7 +10325,7 @@ function OPSGROUP:_CheckAmmoStatus()
-- Check if rearming is completed.
if self:IsRearming() then
if ammo.Total==self.ammo.Total then
if ammo.Total>=self.ammo.Total then
self:Rearmed()
end
end

View File

@ -30,6 +30,9 @@
-- @field #number Nred Number of red units in the zone.
-- @field #number Nblu Number of blue units in the zone.
-- @field #number Nnut Number of neutral units in the zone.
-- @field #number Tred Threat level of red units in the zone.
-- @field #number Tblu Threat level of blue units in the zone.
-- @field #number Tnut Threat level of neutral units in the zone.
-- @field #number TminCaptured Time interval in seconds how long an attacker must have troops inside the zone to capture.
-- @field #number Tcaptured Time stamp (abs.) when the attacker destroyed all owning troops.
-- @field #table ObjectCategories Object categories for the scan.
@ -43,6 +46,10 @@
-- @field #string markerText Text shown in the maker.
-- @field #table chiefs Chiefs that monitor this zone.
-- @field #table Missions Missions that are attached to this OpsZone.
-- @field #number nunitsCapture Number of units necessary to capture a zone.
-- @field #number threatlevelCapture Threat level necessary to capture a zone.
-- @field Core.Set#SET_UNIT ScanUnitSet Set of scanned units.
-- @field Core.Set#SET_GROUP ScanGroupSet Set of scanned groups.
-- @extends Core.Fsm#FSM
--- *Gentlemen, when the enemy is committed to a mistake we must not interrupt him too soon.* --- Horation Nelson
@ -64,6 +71,9 @@ OPSZONE = {
Nred = 0,
Nblu = 0,
Nnut = 0,
Tred = 0,
Tblu = 0,
Tnut = 0,
chiefs = {},
Missions = {},
}
@ -76,16 +86,16 @@ OPSZONE = {
--- OPSZONE class version.
-- @field #string version
OPSZONE.version="0.3.1"
OPSZONE.version="0.4.0"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- ToDo list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO: Capturing based on (total) threat level threshold. Unarmed units do not pose a threat and should not be able to hold a zone.
-- TODO: Pause/unpause evaluations.
-- TODO: Capture time, i.e. time how long a single coalition has to be inside the zone to capture it.
-- TODO: Differentiate between ground attack and boming by air or arty.
-- DONE: Capture time, i.e. time how long a single coalition has to be inside the zone to capture it.
-- DONE: Capturing based on (total) threat level threshold. Unarmed units do not pose a threat and should not be able to hold a zone.
-- DONE: Can neutrals capture? No, since they are _neutral_!
-- DONE: Capture airbases.
-- DONE: Can statics capture or hold a zone? No, unless explicitly requested by mission designer.
@ -151,6 +161,11 @@ function OPSZONE:New(Zone, CoalitionOwner)
self.zoneName=Zone:GetName()
self.zoneRadius=Zone:GetRadius()
self.Missions = {}
self.ScanUnitSet=SET_UNIT:New():FilterZones({Zone})
self.ScanGroupSet=SET_GROUP:New():FilterZones({Zone})
-- Add to database.
_DATABASE:AddOpsZone(self)
-- Current and previous owners.
self.ownerCurrent=CoalitionOwner or coalition.side.NEUTRAL
@ -165,9 +180,6 @@ function OPSZONE:New(Zone, CoalitionOwner)
self.ownerPrevious=self.airbase:GetCoalition()
end
-- Set time to capture.
self:SetTimeCapture()
-- Set object categories.
self:SetObjectCategories()
self:SetUnitCategories()
@ -176,6 +188,11 @@ function OPSZONE:New(Zone, CoalitionOwner)
self:SetDrawZone()
self:SetMarkZone(true)
-- Default capture parameters.
self:SetCaptureTime()
self:SetCaptureNunits()
self:SetCaptureThreatlevel()
-- Status timer.
self.timerStatus=TIMER:New(OPSZONE.Status, self)
@ -187,6 +204,8 @@ function OPSZONE:New(Zone, CoalitionOwner)
self:AddTransition("Stopped", "Start", "Empty") -- Start FSM.
self:AddTransition("*", "Stop", "Stopped") -- Stop FSM.
self:AddTransition("*", "Evaluated", "*") -- Evaluation done.
self:AddTransition("*", "Captured", "Guarded") -- Zone was captured.
self:AddTransition("Empty", "Guarded", "Guarded") -- Owning coalition left the zone and returned.
@ -220,6 +239,23 @@ function OPSZONE:New(Zone, CoalitionOwner)
-- @param #number delay Delay in seconds.
--- Triggers the FSM event "Evaluated".
-- @function [parent=#OPSZONE] Evaluated
-- @param #OPSZONE self
--- Triggers the FSM event "Evaluated" after a delay.
-- @function [parent=#OPSZONE] __Evaluated
-- @param #OPSZONE self
-- @param #number delay Delay in seconds.
--- On after "Evaluated" event.
-- @function [parent=#OPSZONE] OnAfterEvaluated
-- @param #OPSZONE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
--- Triggers the FSM event "Captured".
-- @function [parent=#OPSZONE] Captured
-- @param #OPSZONE self
@ -369,27 +405,27 @@ function OPSZONE:SetUnitCategories(Categories)
return self
end
--- Set threat level threshold that the defending units must have to hold a zone.
-- The reason why you might want to set this is that unarmed units (*e.g.* fuel trucks) should not be able to hold a zone as they do not pose a threat.
--- Set threat level threshold that the offending units must have to capture a zone.
-- The reason why you might want to set this is that unarmed units (*e.g.* fuel trucks) should not be able to capture a zone as they do not pose a threat.
-- @param #OPSZONE self
-- @param #number Threatlevel Threat level threshod. Default 0.
-- @param #number Threatlevel Threat level threshold. Default 0.
-- @return #OPSZONE self
function OPSZONE:SetThreatlevelDefinding(Threatlevel)
function OPSZONE:SetCaptureThreatlevel(Threatlevel)
self.threatlevelDefending=Threatlevel or 0
self.threatlevelCapture=Threatlevel or 0
return self
end
--- Set threat level threshold that the offending units must have to capture a zone.
-- The reason why you might want to set this is that unarmed units (*e.g.* fuel trucks) should not be able to capture a zone as they do not pose a threat.
--- Set how many units must be present in a zone to capture it. By default, one unit is enough.
-- @param #OPSZONE self
-- @param #number Threatlevel Threat level threshod. Default 0.
-- @param #number Nunits Number of units. Default 1.
-- @return #OPSZONE self
function OPSZONE:SetThreatlevelOffending(Threatlevel)
function OPSZONE:SetCaptureNunits(Nunits)
self.threatlevelOffending=Threatlevel or 0
Nunits=Nunits or 1
self.nunitsCapture=Nunits
return self
end
@ -399,7 +435,7 @@ end
-- @param #OPSZONE self
-- @param #number Tcapture Time in seconds. Default 0.
-- @return #OPSZONE self
function OPSZONE:SetTimeCapture(Tcapture)
function OPSZONE:SetCaptureTime(Tcapture)
self.TminCaptured=Tcapture or 0
@ -479,6 +515,21 @@ function OPSZONE:GetCoordinate()
return coordinate
end
--- Get scanned units inside the zone.
-- @param #OPSZONE self
-- @return Core.Set#SET_UNIT Set of units inside the zone.
function OPSZONE:GetScannedUnitSet()
return self.ScanUnitSet
end
--- Get scanned groups inside the zone.
-- @param #OPSZONE self
-- @return Core.Set#SET_GROUP Set of groups inside the zone.
function OPSZONE:GetScannedGroupSet()
return self.ScanGroupSet
end
--- Returns a random coordinate in the zone.
-- @param #OPSZONE self
-- @param #number inner (Optional) Minimal distance from the center of the zone in meters. Default is 0 m.
@ -562,6 +613,22 @@ function OPSZONE:IsCoalition(Coalition)
return is
end
--- Check if zone is started (not stopped).
-- @param #OPSZONE self
-- @return #boolean If `true`, zone is started.
function OPSZONE:IsStarted()
local is=not self:IsStopped()
return is
end
--- Check if zone is stopped.
-- @param #OPSZONE self
-- @return #boolean If `true`, zone is stopped.
function OPSZONE:IsStopped()
local is=self:is("Stopped")
return is
end
--- Check if zone is guarded.
-- @param #OPSZONE self
-- @return #boolean If `true`, zone is guarded.
@ -753,11 +820,7 @@ function OPSZONE:onafterEmpty(From, Event, To)
-- Debug info.
self:T(self.lid..string.format("Zone is empty EVENT"))
-- Inform chief.
for _,_chief in pairs(self.chiefs) do
local chief=_chief --Ops.Chief#CHIEF
chief:ZoneEmpty(self)
end
end
@ -772,16 +835,6 @@ function OPSZONE:onafterAttacked(From, Event, To, AttackerCoalition)
-- Debug info.
self:T(self.lid..string.format("Zone is being attacked by coalition=%s!", tostring(AttackerCoalition)))
-- Inform chief.
if AttackerCoalition then
for _,_chief in pairs(self.chiefs) do
local chief=_chief --Ops.Chief#CHIEF
if chief.coalition~=AttackerCoalition then
chief:ZoneAttacked(self)
end
end
end
end
--- On after "Defeated" event.
@ -807,6 +860,8 @@ end
-- @param #string To To state.
function OPSZONE:onenterGuarded(From, Event, To)
if From~=To then
-- Debug info.
self:T(self.lid..string.format("Zone is guarded"))
@ -814,6 +869,7 @@ function OPSZONE:onenterGuarded(From, Event, To)
self.Tattacked=nil
if self.drawZone then
self.zone:UndrawZone()
local color=self:_GetZoneColor()
@ -823,19 +879,35 @@ function OPSZONE:onenterGuarded(From, Event, To)
end
end
--- On enter "Attacked" state.
-- @param #OPSZONE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSZONE:onenterAttacked(From, Event, To)
-- @param #number AttackerCoalition Coalition of the attacking ground troops.
function OPSZONE:onenterAttacked(From, Event, To, AttackerCoalition)
-- Time stamp when the attack started.
if From~="Attacked" then
-- Debug info.
self:T(self.lid..string.format("Zone is Attacked"))
-- Time stamp when the attack started.
-- Set time stamp.
self.Tattacked=timer.getAbsTime()
-- Inform chief.
if AttackerCoalition then
for _,_chief in pairs(self.chiefs) do
local chief=_chief --Ops.Chief#CHIEF
if chief.coalition~=AttackerCoalition then
chief:ZoneAttacked(self)
end
end
end
-- Draw zone?
if self.drawZone then
self.zone:UndrawZone()
@ -848,6 +920,9 @@ function OPSZONE:onenterAttacked(From, Event, To)
end
self:_CleanMissionTable()
end
end
--- On enter "Empty" event.
@ -857,9 +932,17 @@ end
-- @param #string To To state.
function OPSZONE:onenterEmpty(From, Event, To)
if From~=To then
-- Debug info.
self:T(self.lid..string.format("Zone is empty now"))
-- Inform chief.
for _,_chief in pairs(self.chiefs) do
local chief=_chief --Ops.Chief#CHIEF
chief:ZoneEmpty(self)
end
if self.drawZone then
self.zone:UndrawZone()
@ -870,6 +953,8 @@ function OPSZONE:onenterEmpty(From, Event, To)
end
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Scan Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
@ -893,6 +978,13 @@ function OPSZONE:Scan()
local Nblu=0
local Nnut=0
local Tred=0
local Tblu=0
local Tnut=0
self.ScanGroupSet:Clear(false)
self.ScanUnitSet:Clear(false)
--- Function to evaluate the world search
local function EvaluateZone(_ZoneObject)
@ -939,13 +1031,35 @@ function OPSZONE:Scan()
-- Get Coalition.
local Coalition=DCSUnit:getCoalition()
local tl=0
local unit=UNIT:Find(DCSUnit)
if unit then
-- Threat level of unit.
tl=unit:GetThreatLevel()
-- Add unit to set.
self.ScanUnitSet:AddUnit(unit)
-- Get group of unit.
local group=unit:GetGroup()
if group then
self.ScanGroupSet:AddGroup(group, true)
end
end
-- Increase counter.
if Coalition==coalition.side.RED then
Nred=Nred+1
Tred=Tred+tl
elseif Coalition==coalition.side.BLUE then
Nblu=Nblu+1
Tblu=Tblu+tl
elseif Coalition==coalition.side.NEUTRAL then
Nnut=Nnut+1
Tnut=Tnut+tl
end
-- Debug info.
@ -1015,6 +1129,10 @@ function OPSZONE:Scan()
self.Nblu=Nblu
self.Nnut=Nnut
self.Tblu=Tblu
self.Tred=Tred
self.Tnut=Tnut
return self
end
@ -1028,6 +1146,30 @@ function OPSZONE:EvaluateZone()
local Nblu=self.Nblu
local Nnut=self.Nnut
local Tnow=timer.getAbsTime()
--- Capture
-- @param #number coal Coaltion capturing.
local function captured(coal)
-- Blue captured red zone.
if not self.airbase then
-- Set time stamp if it does not exist.
if not self.Tcaptured then
self.Tcaptured=Tnow
end
-- Check if enough time elapsed.
if Tnow-self.Tcaptured>=self.TminCaptured then
self:Captured(coal)
self.Tcaptured=nil
end
end
end
if self:IsRed() then
---
@ -1038,43 +1180,16 @@ function OPSZONE:EvaluateZone()
-- No red units in red zone any more.
if Nblu>0 then
-- Blue captured red zone.
if not self.airbase then
local Tnow=timer.getAbsTime()
if Nblu>=self.nunitsCapture and self.Tblu>=self.threatlevelCapture then
-- Set time stamp if it does not exist.
if not self.Tcaptured then
self.Tcaptured=Tnow
end
-- Blue captued red zone.
captured(coalition.side.BLUE)
elseif Nnut>=self.nunitsCapture and self.Tnut>=self.threatlevelCapture and self.neutralCanCapture then
-- Check if enough time elapsed.
if Tnow-self.Tcaptured>=self.TminCaptured then
self:Captured(coalition.side.BLUE)
self.Tcaptured=nil
end
end
elseif Nnut>0 and self.neutralCanCapture then
-- Neutral captured red zone.
if not self.airbase then
local Tnow=timer.getAbsTime()
captured(coalition.side.NEUTRAL)
-- Set time stamp if it does not exist.
if not self.Tcaptured then
self.Tcaptured=Tnow
end
-- Check if enough time elapsed.
if Tnow-self.Tcaptured>=self.TminCaptured then
self:Captured(coalition.side.NEUTRAL)
self.Tcaptured=nil
end
end
else
-- Red zone is now empty (but will remain red).
if not self:IsEmpty() then
self:Empty()
end
end
else
@ -1117,21 +1232,16 @@ function OPSZONE:EvaluateZone()
-- No blue units in blue zone any more.
if Nred>0 then
if Nred>=self.nunitsCapture and self.Tred>=self.threatlevelCapture then
-- Red captured blue zone.
if not self.airbase then
self:Captured(coalition.side.RED)
end
elseif Nnut>0 and self.neutralCanCapture then
captured(coalition.side.RED)
elseif Nnut>=self.nunitsCapture and self.Tnut>=self.threatlevelCapture and self.neutralCanCapture then
-- Neutral captured blue zone.
if not self.airbase then
self:Captured(coalition.side.NEUTRAL)
end
else
-- Blue zone is empty now.
if not self:IsEmpty() then
self:Empty()
end
captured(coalition.side.NEUTRAL)
end
else
@ -1183,21 +1293,12 @@ function OPSZONE:EvaluateZone()
self:Attacked()
end
self.isContested=true
elseif Nred>0 then
elseif Nred>=self.nunitsCapture and self.Tred>=self.threatlevelCapture then
-- Red captured neutral zone.
if not self.airbase then
self:Captured(coalition.side.RED)
end
elseif Nblu>0 then
captured(coalition.side.RED)
elseif Nblu>=self.nunitsCapture and self.Tblu>=self.threatlevelCapture then
-- Blue captured neutral zone.
if not self.airbase then
self:Captured(coalition.side.BLUE)
end
else
-- Neutral zone is empty now.
if not self:IsEmpty() then
self:Empty()
end
captured(coalition.side.BLUE)
end
--end
@ -1207,6 +1308,11 @@ function OPSZONE:EvaluateZone()
end
-- No units of any coalition in zone any more ==> Empty!
if Nblu==0 and Nred==0 and Nnut==0 and (not self:IsEmpty()) then
self:Empty()
end
-- Finally, check airbase coalition
if self.airbase then
@ -1220,6 +1326,9 @@ function OPSZONE:EvaluateZone()
end
-- Trigger event.
self:Evaluated()
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
@ -1328,7 +1437,7 @@ function OPSZONE:_UpdateMarker()
end
--- Get marker text
--- Get marker text.
-- @param #OPSZONE self
-- @return #string Marker text.
function OPSZONE:_GetMarkerText()
@ -1337,8 +1446,10 @@ function OPSZONE:_GetMarkerText()
local prevowner=UTILS.GetCoalitionName(self.ownerPrevious)
-- Get marker text.
local text=string.format("%s: Owner=%s [%s]\nState=%s [Contested=%s]\nBlue=%d, Red=%d, Neutral=%d",
self.zoneName, owner, prevowner, self:GetState(), tostring(self:IsContested()), self.Nblu, self.Nred, self.Nnut)
local text=string.format("%s [N=%d, TL=%d T=%d]:\nOwner=%s [%s]\nState=%s [Contested=%s]\nBlue=%d [TL=%d]\nRed=%d [TL=%d]\nNeutral=%d [TL=%d]",
self.zoneName, self.nunitsCapture or 0, self.threatlevelCapture or 0, self.TminCaptured or 0,
owner, prevowner, self:GetState(), tostring(self:IsContested()),
self.Nblu, self.Tblu, self.Nred, self.Tred, self.Nnut, self.Tnut)
return text
end

View File

@ -84,6 +84,7 @@ TARGET = {
-- @field #string COORDINATE Target is a COORDINATE.
-- @field #string AIRBASE Target is an AIRBASE.
-- @field #string ZONE Target is a ZONE object.
-- @field #string OPSZONE Target is an OPSZONE object.
TARGET.ObjectType={
GROUP="Group",
UNIT="Unit",
@ -92,6 +93,7 @@ TARGET.ObjectType={
COORDINATE="Coordinate",
AIRBASE="Airbase",
ZONE="Zone",
OPSZONE="OpsZone"
}
@ -151,7 +153,7 @@ _TARGETID=0
--- TARGET class version.
-- @field #string version
TARGET.version="0.5.6"
TARGET.version="0.6.0"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
@ -312,12 +314,18 @@ end
-- * SET_SCENERY
-- * SET_OPSGROUP
-- * SET_ZONE
-- * SET_OPSZONE
--
-- @param #TARGET self
-- @param Wrapper.Positionable#POSITIONABLE Object The target UNIT, GROUP, STATIC, SCENERY, AIRBASE, COORDINATE, ZONE, SET_GROUP, SET_UNIT, SET_STATIC, SET_SCENERY, SET_ZONE
function TARGET:AddObject(Object)
if Object:IsInstanceOf("SET_GROUP") or Object:IsInstanceOf("SET_UNIT") or Object:IsInstanceOf("SET_STATIC") or Object:IsInstanceOf("SET_SCENERY") or Object:IsInstanceOf("SET_OPSGROUP") then
if Object:IsInstanceOf("SET_GROUP") or
Object:IsInstanceOf("SET_UNIT") or
Object:IsInstanceOf("SET_STATIC") or
Object:IsInstanceOf("SET_SCENERY") or
Object:IsInstanceOf("SET_OPSGROUP") or
Object:IsInstanceOf("SET_OPSZONE") then
---
-- Sets
@ -985,6 +993,22 @@ function TARGET:_AddObject(Object)
target.Life0=1
target.Life=1
elseif Object:IsInstanceOf("OPSZONE") then
local zone=Object --Ops.OpsZone#OPSZONE
Object=zone
target.Type=TARGET.ObjectType.OPSZONE
target.Name=zone:GetName()
target.Coordinate=zone:GetCoordinate()
target.N0=target.N0+1
target.Life0=1
target.Life=1
else
self:E(self.lid.."ERROR: Unknown object type!")
return nil
@ -1102,7 +1126,7 @@ function TARGET:GetTargetLife(Target)
return 1
elseif Target.Type==TARGET.ObjectType.ZONE then
elseif Target.Type==TARGET.ObjectType.ZONE or Target.Type==TARGET.ObjectType.OPSZONE then
return 1
@ -1311,6 +1335,13 @@ function TARGET:GetTargetVec3(Target, Average)
local vec3=object:GetVec3()
return vec3
elseif Target.Type==TARGET.ObjectType.OPSZONE then
local object=Target.Object --Ops.OpsZone#OPSZONE
local vec3=object:GetZone():GetVec3()
return vec3
end
self:E(self.lid.."ERROR: Unknown TARGET type! Cannot get Vec3")
@ -1387,7 +1418,7 @@ function TARGET:GetTargetHeading(Target)
-- A coordinate has no heading. Return 0.
return 0
elseif Target.Type==TARGET.ObjectType.ZONE then
elseif Target.Type==TARGET.ObjectType.ZONE or Target.Type==TARGET.ObjectType.OPSZONE then
local object=Target.Object --Core.Zone#ZONE
@ -1666,6 +1697,10 @@ function TARGET:GetTargetCategory(Target)
return TARGET.Category.ZONE
elseif Target.Type==TARGET.ObjectType.OPSZONE then
return TARGET.Category.OPSZONE
else
self:E("ERROR: unknown target category!")
end
@ -1674,6 +1709,71 @@ function TARGET:GetTargetCategory(Target)
end
--- Get coalition of target object. If an object has no coalition (*e.g.* a coordinate) it is returned as neutral.
-- @param #TARGET self
-- @param #TARGET.Object Target Target object.
-- @return #number Coalition number.
function TARGET:GetTargetCoalition(Target)
-- We take neutral for objects that do not have a coalition.
local coal=coalition.side.NEUTRAL
if Target.Type==TARGET.ObjectType.GROUP then
if Target.Object and Target.Object:IsAlive()~=nil then
local object=Target.Object --Wrapper.Group#GROUP
coal=object:GetCoalition()
end
elseif Target.Type==TARGET.ObjectType.UNIT then
if Target.Object and Target.Object:IsAlive()~=nil then
local object=Target.Object --Wrapper.Unit#UNIT
coal=object:GetCoalition()
end
elseif Target.Type==TARGET.ObjectType.STATIC then
local object=Target.Object --Wrapper.Static#STATIC
coal=object:GetCoalition()
elseif Target.Type==TARGET.ObjectType.SCENERY then
-- Scenery has no coalition.
elseif Target.Type==TARGET.ObjectType.AIRBASE then
local object=Target.Object --Wrapper.Airbase#AIRBASE
coal=object:GetCoalition()
elseif Target.Type==TARGET.ObjectType.COORDINATE then
-- Coordinate has no coalition.
elseif Target.Type==TARGET.ObjectType.ZONE then
-- Zone has no coalition.
elseif Target.Type==TARGET.ObjectType.OPSZONE then
local object=Target.Object --Ops.OpsZone#OPSZONE
coal=object:GetOwner()
else
self:E("ERROR: unknown target category!")
end
return coal
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Misc Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
@ -1697,25 +1797,59 @@ end
--- Get the first target objective alive.
-- @param #TARGET self
-- @param Core.Point#COORDINATE RefCoordinate (Optional) Reference coordinate to determine the closest target objective.
-- @param #table Coalitions (Optional) Only consider targets of the given coalition(s).
-- @return #TARGET.Object The target objective.
function TARGET:GetObjective()
function TARGET:GetObjective(RefCoordinate, Coalitions)
if RefCoordinate then
local dmin=math.huge
local tmin=nil --#TARGET.Object
for _,_target in pairs(self.targets) do
local target=_target --#TARGET.Object
if target.Status~=TARGET.ObjectStatus.DEAD then
if target.Status~=TARGET.ObjectStatus.DEAD and (Coalitions==nil or UTILS.IsInTable(UTILS.EnsureTable(Coalitions), self:GetTargetCoalition(target))) then
local vec3=self:GetTargetVec3(target)
local d=UTILS.VecDist3D(vec3, RefCoordinate)
if d<dmin then
dmin=d
tmin=target
end
end
end
return tmin
else
for _,_target in pairs(self.targets) do
local target=_target --#TARGET.Object
if target.Status~=TARGET.ObjectStatus.DEAD and (Coalitions==nil or UTILS.IsInTable(UTILS.EnsureTable(Coalitions), self:GetTargetCoalition(target))) then
return target
end
end
end
return nil
end
--- Get the first target object alive.
-- @param #TARGET self
-- @param Core.Point#COORDINATE RefCoordinate Reference coordinate to determine the closest target objective.
-- @param #table Coalitions (Optional) Only consider targets of the given coalition(s).
-- @return Wrapper.Positionable#POSITIONABLE The target object or nil.
function TARGET:GetObject()
function TARGET:GetObject(RefCoordinate, Coalitions)
local target=self:GetObjective(RefCoordinate, Coalitions)
local target=self:GetObjective()
if target then
return target.Object
end
@ -1726,8 +1860,9 @@ end
--- Count alive objects.
-- @param #TARGET self
-- @param #TARGET.Object Target Target objective.
-- @param #table Coalitions (Optional) Only count targets of the given coalition(s).
-- @return #number Number of alive target objects.
function TARGET:CountObjectives(Target)
function TARGET:CountObjectives(Target, Coalitions)
local N=0
@ -1740,25 +1875,31 @@ function TARGET:CountObjectives(Target)
for _,_unit in pairs(units or {}) do
local unit=_unit --Wrapper.Unit#UNIT
if unit and unit:IsAlive()~=nil and unit:GetLife()>1 then
if Coalitions==nil or UTILS.IsInTable(UTILS.EnsureTable(Coalitions), unit:GetCoalition()) then
N=N+1
end
end
end
elseif Target.Type==TARGET.ObjectType.UNIT then
local target=Target.Object --Wrapper.Unit#UNIT
if target and target:IsAlive()~=nil and target:GetLife()>1 then
if Coalitions==nil or UTILS.IsInTable(Coalitions, target:GetCoalition()) then
N=N+1
end
end
elseif Target.Type==TARGET.ObjectType.STATIC then
local target=Target.Object --Wrapper.Static#STATIC
if target and target:IsAlive() then
if Coalitions==nil or UTILS.IsInTable(Coalitions, target:GetCoalition()) then
N=N+1
end
end
elseif Target.Type==TARGET.ObjectType.SCENERY then
@ -1768,9 +1909,13 @@ function TARGET:CountObjectives(Target)
elseif Target.Type==TARGET.ObjectType.AIRBASE then
local target=Target.Object --Wrapper.Airbase#AIRBASE
if Target.Status==TARGET.ObjectStatus.ALIVE then
if Coalitions==nil or UTILS.IsInTable(Coalitions, target:GetCoalition()) then
N=N+1
end
end
elseif Target.Type==TARGET.ObjectType.COORDINATE then
@ -1780,6 +1925,14 @@ function TARGET:CountObjectives(Target)
-- No target we can check!
elseif Target.Type==TARGET.ObjectType.OPSZONE then
local target=Target.Object --Ops.OpsZone#OPSZONE
if Coalitions==nil or UTILS.IsInTable(Coalitions, target:GetOwner()) then
N=N+1
end
else
self:E(self.lid.."ERROR: Unknown target type! Cannot count targets")
end
@ -1789,15 +1942,16 @@ end
--- Count alive targets.
-- @param #TARGET self
-- @param #table Coalitions (Optional) Only count targets of the given coalition(s).
-- @return #number Number of alive target objects.
function TARGET:CountTargets()
function TARGET:CountTargets(Coalitions)
local N=0
for _,_target in pairs(self.targets) do
local Target=_target --#TARGET.Object
N=N+self:CountObjectives(Target)
N=N+self:CountObjectives(Target, Coalitions)
end

View File

@ -1061,8 +1061,8 @@ function UTILS.Vec2Norm(a)
end
--- Calculate the distance between two 2D vectors.
-- @param DCS#Vec2 a Vector in 3D with x, y components.
-- @param DCS#Vec2 b Vector in 3D with x, y components.
-- @param DCS#Vec2 a Vector in 2D with x, y components.
-- @param DCS#Vec2 b Vector in 2D with x, y components.
-- @return #number Distance between the vectors.
function UTILS.VecDist2D(a, b)
@ -1446,6 +1446,30 @@ function UTILS.GetCoalitionName(Coalition)
end
--- Get the enemy coalition for a given coalition.
-- @param #number Coalition The coalition ID.
-- @param #boolean Neutral Include neutral as enemy.
-- @return #table Enemy coalition table.
function UTILS.GetCoalitionEnemy(Coalition, Neutral)
local Coalitions={}
if Coalition then
if Coalition==coalition.side.RED then
Coalitions={coalition.side.BLUE}
elseif Coalition==coalition.side.BLUE then
Coalitions={coalition.side.RED}
elseif Coalition==coalition.side.NEUTRAL then
Coalitions={coalition.side.RED, coalition.side.BLUE}
end
end
if Neutral then
table.insert(Coalitions, coalition.side.NEUTRAL)
end
return Coalitions
end
--- Get the modulation name from its numerical value.
-- @param #number Modulation The modulation enumerator number. Can be either 0 or 1.
-- @return #string The modulation name, i.e. "AM"=0 or "FM"=1. Anything else will return "Unknown".
@ -2695,3 +2719,51 @@ function UTILS.ToStringBRAANATO(FromGrp,ToGrp)
end
return BRAANATO
end
--- Check if an object is contained in a table.
-- @param #table Table The table.
-- @param #table Object The object to check.
-- @param #string Key (Optional) Key to check. By default, the object itself is checked.
-- @return #booolen Returns `true` if object is in table.
function UTILS.IsInTable(Table, Object, Key)
for key, object in pairs(Table) do
if Key then
if Object[Key]==object[Key] then
return true
end
else
if object==Object then
return true
end
end
end
return false
end
--- Check if any object of multiple given objects is contained in a table.
-- @param #table Table The table.
-- @param #table Objects The objects to check.
-- @param #string Key (Optional) Key to check.
-- @return #booolen Returns `true` if object is in table.
function UTILS.IsAnyInTable(Table, Objects, Key)
for _,Object in pairs(UTILS.EnsureTable(Objects)) do
for key, object in pairs(Table) do
if Key then
if Object[Key]==object[Key] then
return true
end
else
if object==Object then
return true
end
end
end
end
return false
end