From 30e654288765a14ced83569c948aabfea0994e1b Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 20 Dec 2022 23:51:18 +0100 Subject: [PATCH] OPS --- Moose Development/Moose/Core/Database.lua | 41 ++ Moose Development/Moose/Core/Set.lua | 606 ++++++++++++++++++-- Moose Development/Moose/Ops/Auftrag.lua | 58 +- Moose Development/Moose/Ops/Cohort.lua | 6 +- Moose Development/Moose/Ops/Commander.lua | 44 +- Moose Development/Moose/Ops/Legion.lua | 436 ++++++++++---- Moose Development/Moose/Ops/OpsGroup.lua | 47 +- Moose Development/Moose/Ops/OpsZone.lua | 367 +++++++----- Moose Development/Moose/Utilities/Utils.lua | 48 ++ 9 files changed, 1311 insertions(+), 342 deletions(-) diff --git a/Moose Development/Moose/Core/Database.lua b/Moose Development/Moose/Core/Database.lua index afa1d6a64..cf59f2958 100644 --- a/Moose Development/Moose/Core/Database.lua +++ b/Moose Development/Moose/Core/Database.lua @@ -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. diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index f3b227e24..ad178d931 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -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 --- Return 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() @@ -463,16 +475,32 @@ do -- SET_BASE -- @param #SET_BASE self -- @return #SET_BASE self function SET_BASE:FilterOnce() + + --self:Clear() for ObjectName, Object in pairs( self.Database ) do if self:IsIncludeObject( Object ) then self:Add( ObjectName, Object ) + else + self:Remove(ObjectName, true) end end return self end + + --- Clear all filters. You still need to apply :FilterOnce() + -- @param #SET_BASE self + -- @return #SET_BASE self + function SET_BASE:FilterClear() + + for key,value in pairs(self.Filter) do + self.Filter[key]={} + end + + return self + end --- Starts the filtering for the defined collection. -- @param #SET_BASE self @@ -819,7 +847,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 @@ -1023,9 +1051,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() @@ -1171,11 +1199,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 @@ -1185,34 +1216,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 - - --- 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 - 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 @@ -1220,17 +1229,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 @@ -1238,17 +1251,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 @@ -2127,9 +2145,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 @@ -4001,7 +4021,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 = {} @@ -5784,8 +5804,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 @@ -5797,6 +5816,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 d0 then + local N=self:CountOpsGroups() + if N Nmin=%d", self.NassetsMin, N, self.reinforce, Nmin)) + end + end + end return Nmin, Nmax @@ -2905,6 +2922,7 @@ function AUFTRAG:AddTransportCarriers(Carriers) end + return self end --- **[LEGION, COMMANDER, CHIEF]** Set required attribute(s) the assets must have. @@ -2912,10 +2930,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. @@ -2924,10 +2940,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. @@ -3798,7 +3812,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 @@ -4327,6 +4341,12 @@ function AUFTRAG:CheckGroupsDone() self:T2(self.lid..string.format("CheckGroupsDone: Mission is still in state %s [FSM=%s] (PLANNED or QUEUED or REQUESTED). Mission NOT DONE!", self.status, self:GetState())) 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. @@ -4486,7 +4506,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 diff --git a/Moose Development/Moose/Ops/Cohort.lua b/Moose Development/Moose/Ops/Cohort.lua index 9b748e992..5ab2e97da 100644 --- a/Moose Development/Moose/Ops/Cohort.lua +++ b/Moose Development/Moose/Ops/Cohort.lua @@ -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 diff --git a/Moose Development/Moose/Ops/Commander.lua b/Moose Development/Moose/Ops/Commander.lua index 2126a90de..575f28116 100644 --- a/Moose Development/Moose/Ops/Commander.lua +++ b/Moose Development/Moose/Ops/Commander.lua @@ -1671,25 +1671,51 @@ 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() - + -- Target position. local TargetVec2=Mission:GetTargetVec2() -- Special payloads. - local Payloads=Mission.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 diff --git a/Moose Development/Moose/Ops/Legion.lua b/Moose Development/Moose/Ops/Legion.lua index 76e0f641f..d33333a02 100644 --- a/Moose Development/Moose/Ops/Legion.lua +++ b/Moose Development/Moose/Ops/Legion.lua @@ -665,9 +665,24 @@ function LEGION:CheckMissionQueue() -- Look for first task that is not accomplished. 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 take own cohorts. - if #Cohorts==0 then - Cohorts=self.cohorts - end + + -- 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 + + -- 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}, 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,42 +2279,118 @@ 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 #table Categories Group categories. +-- @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) +-- @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) - -- The recruited assets. - local Assets={} - - -- Legions of recruited assets. - local Legions={} - - -- Set MissionTypeOpt to Recruit if nil. - if MissionTypeOpt==nil then - MissionTypeOpt=MissionTypeRecruit - end - --- Function to check category. local function CheckCategory(_cohort) local cohort=_cohort --Ops.Cohort#COHORT @@ -2350,9 +2456,9 @@ function LEGION.RecruitCohortAssets(Cohorts, MissionTypeRecruit, MissionTypeOpt, return true 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 can=AUFTRAG.CheckMissionCapability(MissionType, Cohort.missiontypes) + + if can then + can=CheckCategory(Cohort) + else + env.info(string.format("Cohort %s cannot because of mission types", Cohort.name)) + return false + end + + if can then + if MissionType==AUFTRAG.Type.RELOCATECOHORT then + can=Cohort:IsRelocating() + else + can=Cohort:IsOnDuty() end - - -- Is capable of the mission type? - local Capable=AUFTRAG.CheckMissionCapability({MissionTypeRecruit}, cohort.missiontypes) + 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 - -- Can carry the cargo? - local CanCarry=CargoWeight and cohort.cargobayLimit>=CargoWeight or true + -- Loops over cohorts. + for _,_cohort in pairs(Cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT - -- 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 - 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))) + -- 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 @@ -2714,13 +2936,17 @@ function LEGION:AssignAssetsForTransport(Legions, CargoAssets, NcarriersMin, Nca end 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) diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index 3dd305a27..556fc5662 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -885,10 +885,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) @@ -3315,7 +3315,13 @@ function OPSGROUP:RemoveWaypoint(wpindex) else 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 @@ -5689,6 +5695,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) @@ -5725,6 +5732,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()") @@ -6923,7 +6932,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() @@ -10145,28 +10154,32 @@ 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 - if element.status~=OPSGROUP.ElementStatus.DEAD and element.status~=OPSGROUP.ElementStatus.INUTERO then - - -- Current life points. - local life=element.unit:GetLife() - - self.life=self.life+life - - if life Event --> To State 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. @@ -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. @@ -753,11 +804,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 @@ -771,17 +818,7 @@ 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. @@ -806,19 +843,24 @@ end -- @param #string Event Event. -- @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")) + -- Debug info. + self:T(self.lid..string.format("Zone is guarded")) - -- Not attacked any more. - self.Tattacked=nil - - if self.drawZone then - self.zone:UndrawZone() + -- Not attacked any more. + self.Tattacked=nil + + if self.drawZone then - local color=self:_GetZoneColor() + self.zone:UndrawZone() + + local color=self:_GetZoneColor() + + self.zone:DrawZone(nil, color, 1.0, color, 0.5) + end - self.zone:DrawZone(nil, color, 1.0, color, 0.5) end end @@ -828,26 +870,43 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -function OPSZONE:onenterAttacked(From, Event, To) - - -- Debug info. - self:T(self.lid..string.format("Zone is Attacked")) +-- @param #number AttackerCoalition Coalition of the attacking ground troops. +function OPSZONE:onenterAttacked(From, Event, To, AttackerCoalition) -- Time stamp when the attack started. - self.Tattacked=timer.getAbsTime() + if From~="Attacked" then - -- Draw zone? - if self.drawZone then - self.zone:UndrawZone() - - -- Color. - local color={1, 204/255, 204/255} - - -- Draw zone. - self.zone:DrawZone(nil, color, 1.0, color, 0.5) - end + -- Debug info. + self:T(self.lid..string.format("Zone is Attacked")) - self:_CleanMissionTable() + -- 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() + + -- Color. + local color={1, 204/255, 204/255} + + -- Draw zone. + self.zone:DrawZone(nil, color, 1.0, color, 0.5) + end + + self:_CleanMissionTable() + + end + end --- On enter "Empty" event. @@ -857,17 +916,27 @@ end -- @param #string To To state. function OPSZONE:onenterEmpty(From, Event, To) - -- Debug info. - self:T(self.lid..string.format("Zone is empty now")) + if From~=To then - if self.drawZone then - self.zone:UndrawZone() - - local color=self:_GetZoneColor() - - self.zone:DrawZone(nil, color, 1.0, color, 0.2) - end + -- 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() + + local color=self:_GetZoneColor() + + self.zone:DrawZone(nil, color, 1.0, color, 0.2) + end + + end + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -892,6 +961,13 @@ function OPSZONE:Scan() local Nred=0 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 +1015,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. @@ -1014,6 +1112,10 @@ function OPSZONE:Scan() self.Nred=Nred self.Nblu=Nblu self.Nnut=Nnut + + self.Tblu=Tblu + self.Tred=Tred + self.Tnut=Tnut return self end @@ -1028,6 +1130,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 +1164,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() - - -- 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.BLUE) - self.Tcaptured=nil - end - end - elseif Nnut>0 and self.neutralCanCapture then + if Nblu>=self.nunitsCapture and self.Tblu>=self.threatlevelCapture then + + -- Blue captued red zone. + captured(coalition.side.BLUE) + + elseif Nnut>=self.nunitsCapture and self.Tnut>=self.threatlevelCapture and self.neutralCanCapture then + -- Neutral captured red zone. - if not self.airbase then - local Tnow=timer.getAbsTime() - - -- 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 + captured(coalition.side.NEUTRAL) + end else @@ -1117,21 +1216,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 @@ -1152,7 +1246,7 @@ function OPSZONE:EvaluateZone() self:Defeated(coalition.side.RED) elseif self:IsEmpty() then -- Blue units left zone and returned (or from initial Empty state). - self:Guarded() + self:Guarded() end end @@ -1183,21 +1277,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 @@ -1206,6 +1291,11 @@ function OPSZONE:EvaluateZone() self:E(self.lid.."ERROR: Unknown coaliton!") 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 @@ -1219,6 +1309,9 @@ function OPSZONE:EvaluateZone() end end + + -- Trigger event. + self:Evaluated() end @@ -1328,7 +1421,7 @@ function OPSZONE:_UpdateMarker() end ---- Get marker text +--- Get marker text. -- @param #OPSZONE self -- @return #string Marker text. function OPSZONE:_GetMarkerText() @@ -1337,8 +1430,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 diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index dd073f753..4bc76afac 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -2511,3 +2511,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