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 82a7d65a5..b973172c1 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 --- 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() @@ -461,16 +473,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 @@ -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,11 +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 @@ -1183,34 +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 - - --- 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 @@ -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 d0! 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 - self:T(self.lid.."Rearm command but have current mission ==> Pausing mission!") - self:PauseMission() - dt=-0.1 - allowed=false + 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) @@ -1805,7 +1844,7 @@ function ARMYGROUP:_UpdateEngageTarget() self.engage.Waypoint=self:AddWaypoint(intercoord, self.engage.Speed, uid, self.engage.Formation, true) -- Set if we want to resume route after reaching the detour waypoint. - self.engage.Waypoint.detour=0 + self.engage.Waypoint.detour=0 end @@ -1952,8 +1991,10 @@ 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 + Formation = self.option.Formation else -- Default formation is on road. Formation = ENUMS.Formation.Vehicle.OnRoad @@ -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) diff --git a/Moose Development/Moose/Ops/Auftrag.lua b/Moose Development/Moose/Ops/Auftrag.lua index 9f4fe0d93..d83278c19 100644 --- a/Moose Development/Moose/Ops/Auftrag.lua +++ b/Moose Development/Moose/Ops/Auftrag.lua @@ -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=%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 @@ -4383,6 +4447,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. @@ -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 @@ -5789,6 +5866,22 @@ function AUFTRAG:GetDCSMissionTask() DCStask.params=param 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 -- 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..c2bf84a92 100644 --- a/Moose Development/Moose/Ops/Legion.lua +++ b/Moose Development/Moose/Ops/Legion.lua @@ -47,7 +47,7 @@ LEGION = { --- LEGION class version. -- @field #string version -LEGION.version="0.3.4" +LEGION.version="0.4.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list @@ -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/Operation.lua b/Moose Development/Moose/Ops/Operation.lua index c612ae053..c6598acc2 100644 --- a/Moose Development/Moose/Ops/Operation.lua +++ b/Moose Development/Moose/Ops/Operation.lua @@ -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,40 +533,66 @@ 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 end ---- Add condition function when the given phase is over. Must return a `#boolean`. --- @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:AddPhaseConditonOverAll(Phase, Function, ...) - if Phase then - Phase.conditionOver:AddFunctionAll(Function, ...) - end - return self -end - --- Add condition function when the given phase is over. Must return a `#boolean`. -- @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 +-- @return Core.Condition#CONDITION.Function Condition function table. +function OPERATION:AddPhaseConditonOverAll(Phase, Function, ...) + if Phase then + local cf=Phase.conditionOver:AddFunctionAll(Function, ...) + return cf + end + return nil +end + +--- Add condition function when the given phase is over. Must return a `#boolean`. +-- @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 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 - self:Over() + + 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 + 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 - self:SetPhaseStatus(self.phase, OPERATION.PhaseStatus.OVER) + 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,7 +1248,9 @@ function OPERATION:onafterOver(From, Event, To) local branch=_branch --#OPERATION.Branch for _,_phase in pairs(branch.phases) do local phase=_phase --#OPERATION.Phase - self:SetPhaseStatus(phase, OPERATION.PhaseStatus.OVER) + if not self:IsPhaseOver(phase) then + self:SetPhaseStatus(phase, OPERATION.PhaseStatus.OVER) + end end 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,26 +1305,28 @@ function OPERATION:_CheckPhases() if switch then - -- Switch to new branch. - self:BranchSwitch(edge.branchTo) + -- Get next phase of the branch + local phaseTo=edge.phaseTo or self:GetPhaseNext(edge.branchTo, nil) - -- If we want to switch to a specific phase of the branch. - if edge.phaseTo then + if phaseTo then - -- Change phase. - self:PhaseChange(edge.phaseTo) + -- Switch to new branch. + self:BranchSwitch(edge.branchTo, phaseTo) + + else + + -- No next phase ==> Ops is over! + self:Over() - -- Done here! - return end - -- Break the loop. - break + -- Done here! + return end end end - + -- Next phase. self:PhaseNext() @@ -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 diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index 7656d769b..7fddf424d 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -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) @@ -3315,7 +3316,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 @@ -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 @@ -4218,6 +4231,14 @@ function OPSGROUP:_UpdateTask(Task, Mission) -- Check if ammo is full. 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 @@ -4359,6 +4380,79 @@ function OPSGROUP:_UpdateTask(Task, Mission) end 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 @@ -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 @@ -5561,6 +5675,15 @@ function OPSGROUP:RouteToMission(mission, delay) -- Navy group: Route into direction of the target. 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 --- @@ -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) @@ -5686,6 +5810,8 @@ function OPSGROUP:RouteToMission(mission, delay) -- Add waypoint task. UpdateRoute is called inside. 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()") @@ -5744,7 +5872,7 @@ function OPSGROUP:RouteToMission(mission, delay) --- -- Mission Specific Settings --- - self:_SetMissionOptions(mission) + self:_SetMissionOptions(mission) end end @@ -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,8 +7501,10 @@ function OPSGROUP:CancelAllMissions() -- Cancel all missions. for _,_mission in pairs(self.missionqueue) do local mission=_mission --Ops.Auftrag#AUFTRAG - self:T(self.lid.."Cancelling mission "..tostring(mission:GetName())) - self:MissionCancel(mission) + if mission:IsNotOver() then + self:T(self.lid.."Cancelling mission "..tostring(mission:GetName())) + self:MissionCancel(mission) + end end end @@ -10118,28 +10260,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=self.ammo.Total then self:Rearmed() end end diff --git a/Moose Development/Moose/Ops/OpsZone.lua b/Moose Development/Moose/Ops/OpsZone.lua index 3968b7ee9..6a4749248 100644 --- a/Moose Development/Moose/Ops/OpsZone.lua +++ b/Moose Development/Moose/Ops/OpsZone.lua @@ -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. @@ -100,10 +110,10 @@ OPSZONE.version="0.3.1" -- @param #number CoalitionOwner Initial owner of the coaliton. Default `coalition.side.NEUTRAL`. -- @return #OPSZONE self -- @usage --- myopszone = OPSZONE:New(ZONE:FindByName("OpsZoneOne"),coalition.side.RED) -- base zone from the mission editor --- myopszone = OPSZONE:New(ZONE_RADIUS:New("OpsZoneTwo",mycoordinate:GetVec2(),5000),coalition.side.BLUE) -- radius zone of 5km at a coordinate --- myopszone = OPSZONE:New(ZONE_RADIUS:New("Batumi")) -- airbase zone from Batumi Airbase, ca 2500m radius --- myopszone = OPSZONE:New(ZONE_AIRBASE:New("Batumi",6000),coalition.side.BLUE) -- airbase zone from Batumi Airbase, but with a specific radius of 6km +-- myopszone = OPSZONE:New(ZONE:FindByName("OpsZoneOne"), coalition.side.RED) -- base zone from the mission editor +-- myopszone = OPSZONE:New(ZONE_RADIUS:New("OpsZoneTwo", mycoordinate:GetVec2(),5000),coalition.side.BLUE) -- radius zone of 5km at a coordinate +-- myopszone = OPSZONE:New(ZONE_RADIUS:New("Batumi")) -- airbase zone from Batumi Airbase, ca 2500m radius +-- myopszone = OPSZONE:New(ZONE_AIRBASE:New("Batumi",6000),coalition.side.BLUE) -- airbase zone from Batumi Airbase, but with a specific radius of 6km -- function OPSZONE:New(Zone, CoalitionOwner) @@ -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) @@ -186,6 +203,8 @@ function OPSZONE:New(Zone, CoalitionOwner) -- From State --> 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. @@ -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 @@ -771,17 +834,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 +859,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 +886,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 +932,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 +977,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 +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. @@ -1014,6 +1128,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 +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() - - -- 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 +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 @@ -1152,7 +1262,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 +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 @@ -1206,6 +1307,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 +1325,9 @@ function OPSZONE:EvaluateZone() end 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 diff --git a/Moose Development/Moose/Ops/Target.lua b/Moose Development/Moose/Ops/Target.lua index 96b96715b..4cdbbdbfa 100644 --- a/Moose Development/Moose/Ops/Target.lua +++ b/Moose Development/Moose/Ops/Target.lua @@ -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 @@ -984,6 +992,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!") @@ -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 @@ -1310,6 +1334,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 @@ -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 @@ -1665,6 +1696,10 @@ function TARGET:GetTargetCategory(Target) elseif Target.Type==TARGET.ObjectType.ZONE then return TARGET.Category.ZONE + + elseif Target.Type==TARGET.ObjectType.OPSZONE then + + return TARGET.Category.OPSZONE else self:E("ERROR: unknown target category!") @@ -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,14 +1797,45 @@ 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) - for _,_target in pairs(self.targets) do - local target=_target --#TARGET.Object - if target.Status~=TARGET.ObjectStatus.DEAD then - return target + 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 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 d1 then - N=N+1 + if Coalitions==nil or UTILS.IsInTable(UTILS.EnsureTable(Coalitions), unit:GetCoalition()) then + N=N+1 + end end end @@ -1749,7 +1886,9 @@ function TARGET:CountObjectives(Target) local target=Target.Object --Wrapper.Unit#UNIT if target and target:IsAlive()~=nil and target:GetLife()>1 then - N=N+1 + if Coalitions==nil or UTILS.IsInTable(Coalitions, target:GetCoalition()) then + N=N+1 + end end elseif Target.Type==TARGET.ObjectType.STATIC then @@ -1757,7 +1896,9 @@ function TARGET:CountObjectives(Target) local target=Target.Object --Wrapper.Static#STATIC if target and target:IsAlive() then - N=N+1 + if Coalitions==nil or UTILS.IsInTable(Coalitions, target:GetCoalition()) then + N=N+1 + end end elseif Target.Type==TARGET.ObjectType.SCENERY then @@ -1768,8 +1909,12 @@ 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 - N=N+1 + if Coalitions==nil or UTILS.IsInTable(Coalitions, target:GetCoalition()) then + N=N+1 + end end elseif Target.Type==TARGET.ObjectType.COORDINATE then @@ -1779,7 +1924,15 @@ function TARGET:CountObjectives(Target) elseif Target.Type==TARGET.ObjectType.ZONE then -- 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 diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 5889b8dd9..280fdeaad 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -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