From 81120abfcb33f5981c58b9d535753167a58c16d9 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 6 Sep 2021 23:24:26 +0200 Subject: [PATCH] OPS - LEGION and COMMANDER: Recruit assets for mission --- Moose Development/Moose/Ops/Cohort.lua | 18 +- Moose Development/Moose/Ops/Commander.lua | 230 +++++++++++++++++++- Moose Development/Moose/Ops/FlightGroup.lua | 1 + Moose Development/Moose/Ops/Legion.lua | 161 +++++++++++++- Moose Development/Moose/Ops/OpsGroup.lua | 117 +++++++--- 5 files changed, 486 insertions(+), 41 deletions(-) diff --git a/Moose Development/Moose/Ops/Cohort.lua b/Moose Development/Moose/Ops/Cohort.lua index f1c4fab58..a62edb854 100644 --- a/Moose Development/Moose/Ops/Cohort.lua +++ b/Moose Development/Moose/Ops/Cohort.lua @@ -753,7 +753,7 @@ end --- Count assets in legion warehouse stock. -- @param #COHORT self --- @param #boolean InStock If true, only assets that are in the warehouse stock/inventory are counted. +-- @param #boolean InStock If `true`, only assets that are in the warehouse stock/inventory are counted. If `false`, only assets that are NOT in stock (i.e. spawned) are counted. If `nil`, all assets are counted. -- @param #table MissionTypes (Optional) Count only assest that can perform certain mission type(s). Default is all types. -- @param #table Attributes (Optional) Count only assest that have a certain attribute(s), e.g. `WAREHOUSE.Attribute.AIR_BOMBER`. -- @return #number Number of assets. @@ -766,11 +766,13 @@ function COHORT:CountAssets(InStock, MissionTypes, Attributes) if MissionTypes==nil or self:CheckMissionCapability(MissionTypes, self.missiontypes) then if Attributes==nil or self:CheckAttribute(Attributes) then if asset.spawned then - if not InStock then + if InStock==true or InStock==nil then N=N+1 --Spawned but we also count the spawned ones. end else - N=N+1 --This is in stock. + if InStock==false or InStock==nil then + N=N+1 --This is in stock. + end end end end @@ -784,6 +786,7 @@ end -- @param Ops.Auftrag#AUFTRAG Mission The mission. -- @param #number Npayloads Number of payloads available. -- @return #table Assets that can do the required mission. +-- @return #number Number of payloads still available after recruiting the assets. function COHORT:RecruitAssets(Mission, Npayloads) -- Number of payloads available. @@ -796,8 +799,8 @@ function COHORT:RecruitAssets(Mission, Npayloads) for _,_asset in pairs(self.assets) do local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem - -- First check that asset is not requested. This could happen if multiple requests are processed simultaniously. - if not asset.requested then + -- First check that asset is not requested or reserved. This could happen if multiple requests are processed simultaniously. + if not (asset.requested or asset.isReserved) then -- Check if asset is currently on a mission (STARTED or QUEUED). @@ -815,7 +818,7 @@ function COHORT:RecruitAssets(Mission, Npayloads) self:I(self.lid.."Adding asset on GCICAP mission for an INTERCEPT mission") table.insert(assets, asset) - end + end else @@ -829,6 +832,7 @@ function COHORT:RecruitAssets(Mission, Npayloads) -- Asset is already SPAWNED (could be uncontrolled on the airfield or inbound after another mission) --- + -- Opsgroup. local flightgroup=asset.flightgroup @@ -916,7 +920,7 @@ function COHORT:RecruitAssets(Mission, Npayloads) end -- not requested check end -- loop over assets - return assets + return assets, Npayloads end diff --git a/Moose Development/Moose/Ops/Commander.lua b/Moose Development/Moose/Ops/Commander.lua index 67086fa42..8025fdcf0 100644 --- a/Moose Development/Moose/Ops/Commander.lua +++ b/Moose Development/Moose/Ops/Commander.lua @@ -495,12 +495,35 @@ function COMMANDER:CheckMissionQueue() -- 1. Select best assets from legions --- + -- Recruite assets from legions. + local recruited, legions=self:RecruitAssets(mission) + + if recruited then + + for _,_legion in pairs(legions) do + local legion=_legion --Ops.Legion#LEGION + + -- Debug message. + self:I(self.lid..string.format("Assigning mission %s [%s] to legion %s", mission:GetName(), mission:GetType(), legion.alias)) + + -- Add mission to legion. + self:MissionAssign(legion, mission) + + end + + -- Only ONE mission is assigned. + return + end + + if false then + -- Get legions for mission. - local legions=self:GetLegionsForMission(mission) + local Legions=self:GetLegionsForMission(mission) -- Get ALL assets from pre-selected legions. local assets=self:GetAssets(InStock, legions, MissionTypes, Attributes) + -- Now we select the best assets from all legions. legions={} if #assets>=mission.nassets then @@ -582,6 +605,8 @@ function COMMANDER:CheckMissionQueue() return end + end -- if false then + else --- @@ -702,6 +727,209 @@ function COMMANDER:GetAssets(InStock, Legions, MissionTypes, Attributes) return assets end +--- Recruit assets for a given mission. +-- @param #COMMANDER self +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @return #boolean If `true` enough assets could be recruited. +-- @return #table Legions that have recruited assets. +function COMMANDER:RecruitAssets(Mission) + + env.info("FF recruit assets") + + -- The recruited assets. + local Assets={} + + local legions=Mission.mylegions or self.legions + + local Legions={} + + for _,_legion in pairs(legions) do + local legion=_legion --Ops.Legion#LEGION + + -- Number of payloads in stock per aircraft type. + local Npayloads={} + + -- First get payloads for aircraft types of squadrons. + for _,_cohort in pairs(legion.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + if Npayloads[cohort.aircrafttype]==nil then + Npayloads[cohort.aircrafttype]=legion:IsAirwing() and legion:CountPayloadsInStock(Mission.type, cohort.aircrafttype, Mission.payloads) or 999 + self:I(self.lid..string.format("Got Npayloads=%d for type=%s", Npayloads[cohort.aircrafttype], cohort.aircrafttype)) + end + end + + -- Loops over cohorts. + for _,_cohort in pairs(legion.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + + local npayloads=Npayloads[cohort.aircrafttype] + + if cohort:CanMission(Mission) and npayloads>0 then + + env.info("FF npayloads="..Npayloads[cohort.aircrafttype]) + + -- Recruit assets from squadron. + local assets, npayloads=cohort:RecruitAssets(Mission, npayloads) + + Npayloads[cohort.aircrafttype]=npayloads + + env.info("FF npayloads="..Npayloads[cohort.aircrafttype]) + + for _,asset in pairs(assets) do + table.insert(Assets, asset) + end + + end + + end + + end + + -- Now we have a long list with assets. + self:_OptimizeAssetSelection(Assets, Mission, false) + + for _,_asset in pairs(Assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + if asset.legion:IsAirwing() then + + -- Only assets that have no payload. Should be only spawned assets! + if not asset.payload then + + -- Fetch payload for asset. This can be nil! + asset.payload=asset.legion:FetchPayloadFromStock(asset.unittype, Mission.type, Mission.payloads) + + end + + end + + end + + -- Remove assets that dont have a payload. + for i=#Assets,1,-1 do + local asset=Assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem + if asset.legion:IsAirwing() and not asset.payload then + table.remove(Assets, i) + end + end + + -- Now find the best asset for the given payloads. + self:_OptimizeAssetSelection(Assets, Mission, true) + + local Nassets=Mission:GetRequiredAssets(self) + + if #Assets>=Nassets then + + --- + -- Found enough assets + --- + + -- Add assets to mission. + for i=1,Nassets do + local asset=Assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem + self:I(self.lid..string.format("Adding asset %s to mission %s [%s]", asset.spawngroupname, Mission.name, Mission.type)) + Mission:AddAsset(asset) + Legions[asset.legion.alias]=asset.legion + end + + + -- Return payloads of not needed assets. + for i=Nassets+1,#Assets do + local asset=Assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem + if asset.legion:IsAirwing() and not asset.spawned then + self:I(self.lid..string.format("Returning payload from asset %s", asset.spawngroupname)) + asset.legion:ReturnPayloadFromAsset(asset) + end + end + + -- Found enough assets. + return true, Legions + else + + --- + -- NOT enough assets + --- + + -- Return payloads of assets. + if self:IsAirwing() then + for i=1,#Assets do + local asset=Assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem + if asset.legion:IsAirwing() and not asset.spawned then + self:I(self.lid..string.format("Returning payload from asset %s", asset.spawngroupname)) + asset.legion:ReturnPayloadFromAsset(asset) + end + end + end + + -- Not enough assets found. + return false, {} + end + + return nil, {} +end + +--- Optimize chosen assets for the mission at hand. +-- @param #COMMANDER self +-- @param #table assets Table of (unoptimized) assets. +-- @param Ops.Auftrag#AUFTRAG Mission Mission for which the best assets are desired. +-- @param #boolean includePayload If true, include the payload in the calulation if the asset has one attached. +function COMMANDER:_OptimizeAssetSelection(assets, Mission, includePayload) + + -- Get target position. + local TargetVec2=Mission:GetTargetVec2() + + -- Calculate distance to mission target. + local distmin=math.huge + local distmax=0 + for _,_asset in pairs(assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + if asset.spawned then + local group=GROUP:FindByName(asset.spawngroupname) + asset.dist=UTILS.VecDist2D(group:GetVec2(), TargetVec2) + else + asset.dist=UTILS.VecDist2D(asset.legion:GetVec2(), TargetVec2) + end + + if asset.distdistmax then + distmax=asset.dist + end + + end + + -- Calculate the mission score of all assets. + for _,_asset in pairs(assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + asset.score=asset.legion:CalculateAssetMissionScore(asset, Mission, includePayload) + end + + --- Sort assets wrt to their mission score. Higher is better. + local function optimize(a, b) + local assetA=a --Functional.Warehouse#WAREHOUSE.Assetitem + local assetB=b --Functional.Warehouse#WAREHOUSE.Assetitem + + -- Higher score wins. If equal score ==> closer wins. + -- TODO: Need to include the distance in a smarter way! + return (assetA.score>assetB.score) or (assetA.score==assetB.score and assetA.dist0 then + + env.info("FF npayloads="..Npayloads[cohort.aircrafttype]) + + -- Recruit assets from squadron. + local assets, npayloads=cohort:RecruitAssets(Mission, npayloads) + + Npayloads[cohort.aircrafttype]=npayloads + + env.info("FF npayloads="..Npayloads[cohort.aircrafttype]) + + for _,asset in pairs(assets) do + table.insert(Assets, asset) + end + + end + + end + + -- Now we have a long list with assets. + self:_OptimizeAssetSelection(Assets, Mission, false) + + -- If airwing, get the best payload available. + if self:IsAirwing() then + + for _,_asset in pairs(Assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + -- Only assets that have no payload. Should be only spawned assets! + if not asset.payload then + + -- Fetch payload for asset. This can be nil! + asset.payload=self:FetchPayloadFromStock(asset.unittype, Mission.type, Mission.payloads) + + end + + end + + -- Remove assets that dont have a payload. + for i=#Assets,1,-1 do + local asset=Assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem + if not asset.payload then + table.remove(Assets, i) + end + end + + -- Now find the best asset for the given payloads. + self:_OptimizeAssetSelection(Assets, Mission, true) + + end + + local Nassets=Mission:GetRequiredAssets(self) + + if #Assets>=Nassets then + + --- + -- Found enough assets + --- + + -- Add assets to mission. + for i=1,Nassets do + local asset=Assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem + self:I(self.lid..string.format("Adding asset %s to mission %s [%s]", asset.spawngroupname, Mission.name, Mission.type)) + Mission:AddAsset(asset) + end + + if self:IsAirwing() then + + -- Return payloads of not needed assets. + for i=Nassets+1,#Assets do + local asset=Assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem + if not asset.spawned then + self:I(self.lid..string.format("Returning payload from asset %s", asset.spawngroupname)) + self:ReturnPayloadFromAsset(asset) + end + end + + end + + -- Found enough assets. + return true + else + + --- + -- NOT enough assets + --- + + -- Return payloads of assets. + if self:IsAirwing() then + for i=1,#Assets do + local asset=Assets[i] + if not asset.spawned then + self:I(self.lid..string.format("Returning payload from asset %s", asset.spawngroupname)) + self:ReturnPayloadFromAsset(asset) + end + end + end + + -- Not enough assets found. + return false + end + end diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index 8120f4ad8..f6999e6d3 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -706,6 +706,46 @@ function OPSGROUP:New(group) -- @param #number delay Delay in seconds. + --- Triggers the FSM event "MissionStart". + -- @function [parent=#OPSGROUP] MissionStart + -- @param #OPSGROUP self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "MissionStart" after a delay. + -- @function [parent=#OPSGROUP] __MissionStart + -- @param #OPSGROUP self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "MissionStart" event. + -- @function [parent=#OPSGROUP] OnAfterMissionStart + -- @param #OPSGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + + --- Triggers the FSM event "MissionExecute". + -- @function [parent=#OPSGROUP] MissionExecute + -- @param #OPSGROUP self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "MissionExecute" after a delay. + -- @function [parent=#OPSGROUP] __MissionExecute + -- @param #OPSGROUP self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "MissionExecute" event. + -- @function [parent=#OPSGROUP] OnAfterMissionExecute + -- @param #OPSGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "MissionCancel". -- @function [parent=#OPSGROUP] MissionCancel -- @param #OPSGROUP self @@ -725,6 +765,26 @@ function OPSGROUP:New(group) -- @param #string To To state. -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "MissionDone". + -- @function [parent=#OPSGROUP] MissionDone + -- @param #OPSGROUP self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "MissionDone" after a delay. + -- @function [parent=#OPSGROUP] __MissionDone + -- @param #OPSGROUP self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "MissionDone" event. + -- @function [parent=#OPSGROUP] OnAfterMissionDone + -- @param #OPSGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + -- TODO: Add pseudo functions. return self @@ -3295,7 +3355,7 @@ function OPSGROUP:onafterTaskExecute(From, Event, To, Task) local Speed=UTILS.KmphToKnots(Task.dcstask.params.speed or self.speedCruise) local Altitude=Task.dcstask.params.altitude and UTILS.MetersToFeet(Task.dcstask.params.altitude) or nil - --Coordinate:MarkToAll("Next waypoint", ReadOnly,Text) + Coordinate:MarkToAll("Recon Waypoint Execute") local currUID=self:GetWaypointCurrent().uid @@ -3484,7 +3544,11 @@ function OPSGROUP:onafterTaskDone(From, Event, To, Task) self:T(self.lid.."Task Done ==> Mission Done!") self:MissionDone(Mission) else - --Mission paused. Do nothing! + --Mission paused. Do nothing! Just set the current mission to nil so we can launch a new one. + if self.currentmission and self.currentmission==Mission.auftragsnummer then + self.currentmission=nil + end + self:_RemoveMissionWaypoints(Mission, false) end else @@ -3798,17 +3862,6 @@ function OPSGROUP:onafterMissionStart(From, Event, To, Mission) end ---- On before "MissionExecute" event. --- @param #OPSGROUP self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. --- @param Ops.Auftrag#AUFTRAG Mission The mission table. -function OPSGROUP:onbeforeMissionExecute(From, Event, To, Mission) - - return true -end - --- On after "MissionExecute" event. Mission execution began. -- @param #OPSGROUP self -- @param #string From From state. @@ -3928,12 +3981,31 @@ function OPSGROUP:onafterMissionCancel(From, Event, To, Mission) end +--- On after "MissionDone" event. +-- @param #OPSGROUP self +-- @param Ops.Auftrag#AUFTRAG Mission +-- @param #boolean Silently Remove waypoints by `table.remove()` and do not update the route. +function OPSGROUP:_RemoveMissionWaypoints(Mission, Silently) + + for i=#self.waypoints,1,-1 do + local wp=self.waypoints[i] --#OPSGROUP.Waypoint + if wp.missionUID==Mission.auftragsnummer then + if Silently then + table.remove(self.waypoints, i) + else + self:RemoveWaypoint(i) + end + end + end + +end + --- On after "MissionDone" event. -- @param #OPSGROUP self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param Ops.Auftrag#AUFTRAG Mission +-- @param Ops.Auftrag#AUFTRAG Mission The mission that is done. function OPSGROUP:onafterMissionDone(From, Event, To, Mission) -- Debug info. @@ -3947,20 +4019,9 @@ function OPSGROUP:onafterMissionDone(From, Event, To, Mission) if self.currentmission and Mission.auftragsnummer==self.currentmission then self.currentmission=nil end - - -- Remove mission waypoint. - local wpidx=Mission:GetGroupWaypointIndex(self) - if wpidx then - --self:RemoveWaypointByID(wpidx) - end - for i=#self.waypoints,1,-1 do - local wp=self.waypoints[i] --#OPSGROUP.Waypoint - if wp.missionUID==Mission.auftragsnummer then - --table.remove(self.waypoints, i) - self:RemoveWaypoint(i) - end - end + -- Remove mission waypoints. + self:_RemoveMissionWaypoints(Mission) -- Decrease patrol data. if Mission.patroldata then @@ -4387,7 +4448,7 @@ function OPSGROUP:onafterPassingWaypoint(From, Event, To, Waypoint) local Altitude=task.dcstask.params.altitude and UTILS.MetersToFeet(task.dcstask.params.altitude) or nil -- Debug. - --Coordinate:MarkToAll("Recon Waypoint n="..tostring(n)) + Coordinate:MarkToAll("Recon Waypoint n="..tostring(n)) local currUID=self:GetWaypointCurrent().uid