OPS Commander

This commit is contained in:
Frank 2021-08-26 21:24:47 +02:00
parent 418d6c882c
commit 1b0ad13529
5 changed files with 296 additions and 87 deletions

View File

@ -1622,13 +1622,17 @@ WAREHOUSE = {
-- @field #boolean spawned If true, asset was spawned into the cruel world. If false, it is still in stock.
-- @field #string spawngroupname Name of the spawned group.
-- @field #boolean iscargo If true, asset is cargo. If false asset is transport. Nil if in stock.
-- @field #number rid The request ID of this asset.
-- @field #boolean arrived If true, asset arrived at its destination.
--
-- @field #number damage Damage of asset group in percent.
-- @field Ops.AirWing#AIRWING.Payload payload The payload of the asset.
-- @field Ops.FlightGroup#FLIGHTGROUP flightgroup The flightgroup object.
-- @field Ops.OpsGroup#OPSGROUP flightgroup The flightgroup object.
-- @field Ops.Cohort#COHORT cohort The cohort this asset belongs to.
-- @field Ops.Legion#LEGION legion The legion this asset belonts to.
-- @field #string squadname Name of the squadron this asset belongs to.
-- @field #number Treturned Time stamp when asset returned to the airwing.
-- @field #number Treturned Time stamp when asset returned to its legion (airwing, brigade).
-- @field #boolean requested If `true`, asset was requested and cannot be selected by another request.
-- @field #boolean isReserved If `true`, asset was reserved and cannot be selected by another request.
--- Item of the warehouse queue table.
-- @type WAREHOUSE.Queueitem
@ -3118,14 +3122,16 @@ end
-- @param #WAREHOUSE self
-- @return DCS#Vec3 The 3D vector of the warehouse.
function WAREHOUSE:GetVec3()
return self.warehouse:GetVec3()
local vec3=self.warehouse:GetVec3()
return vec3
end
--- Get 2D vector of warehouse static.
-- @param #WAREHOUSE self
-- @return DCS#Vec2 The 2D vector of the warehouse.
function WAREHOUSE:GetVec2()
return self.warehouse:GetVec2()
local vec2=self.warehouse:GetVec2()
return vec2
end
@ -3194,18 +3200,6 @@ function WAREHOUSE:GetAssignment(request)
return tostring(request.assignment)
end
--[[
--- Get warehouse unique ID from static warehouse object. This is the ID under which you find the @{#WAREHOUSE} object in the global data base.
-- @param #WAREHOUSE self
-- @param #string staticname Name of the warehouse static object.
-- @return #number Warehouse unique ID.
function WAREHOUSE:GetWarehouseID(staticname)
local warehouse=STATIC:FindByName(staticname, true)
local uid=tonumber(warehouse:GetID())
return uid
end
]]
--- Find a warehouse in the global warehouse data base.
-- @param #WAREHOUSE self
-- @param #number uid The unique ID of the warehouse.
@ -3951,6 +3945,8 @@ function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribu
-- Asset is not spawned.
asset.spawned=false
asset.requested=false
asset.isReserved=false
asset.iscargo=nil
asset.arrived=nil
@ -4141,6 +4137,8 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay,
asset.skill=skill
asset.assignment=assignment
asset.spawned=false
asset.requested=false
asset.isReserved=false
asset.life0=group:GetLife0()
asset.damage=0
asset.spawngroupname=string.format("%s_AID-%d", templategroupname, asset.uid)

View File

@ -372,6 +372,8 @@ end
function COHORT:AddAsset(Asset)
self:T(self.lid..string.format("Adding asset %s of type %s", Asset.spawngroupname, Asset.unittype))
Asset.squadname=self.name
Asset.legion=self.legion
Asset.cohort=self
table.insert(self.assets, Asset)
return self
end
@ -700,7 +702,7 @@ end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Check if there is a cohort that can execute a given mission.
-- We check the mission type, the refuelling system, engagement range
-- We check the mission type, the refuelling system, mission range.
-- @param #COHORT self
-- @param Ops.Auftrag#AUFTRAG Mission The mission.
-- @return #boolean If true, Cohort can do that type of mission.

View File

@ -28,14 +28,12 @@
--
-- # The COMMANDER Concept
--
-- A commander is the head of legions. He will find the best LEGIONs to perform an assigned AUFTRAG (mission).
-- A commander is the head of legions. He/she will find the best LEGIONs to perform an assigned AUFTRAG (mission).
--
--
-- @field #COMMANDER
COMMANDER = {
ClassName = "COMMANDER",
Debug = nil,
lid = nil,
legions = {},
missionqueue = {},
}
@ -50,6 +48,8 @@ COMMANDER.version="0.1.0"
-- TODO: Improve legion selection. Mostly done!
-- TODO: Allow multiple Legions for one mission.
-- TODO: Add ops transports.
-- TODO: Find solution for missions, which require a transport. This is not as easy as it sounds since the selected mission assets restrict the possible transport assets.
-- NOGO: Maybe it's possible to preselect the assets for the mission.
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
@ -247,15 +247,19 @@ function COMMANDER:onafterStatus(From, Event, To)
-- FSM state.
local fsmstate=self:GetState()
-- Check mission queue and assign one PLANNED mission.
self:CheckMissionQueue()
-- Status.
local text=string.format("Status %s: Legions=%d, Missions=%d", fsmstate, #self.legions, #self.missionqueue)
self:I(self.lid..text)
-- Legion info.
-- Check mission queue and assign one PLANNED mission.
self:CheckMissionQueue()
---
-- LEGIONS
---
if #self.legions>0 then
local text="Legions:"
for _,_legion in pairs(self.legions) do
local legion=_legion --Ops.Legion#LEGION
@ -272,8 +276,70 @@ function COMMANDER:onafterStatus(From, Event, To)
end
end
self:I(self.lid..text)
local assets={}
local Ntotal=0
local Nspawned=0
local Nrequested=0
local Nreserved=0
local Nstock=0
local text="===========================================\n"
text=text.."Assets:"
for _,_legion in pairs(self.legions) do
local legion=_legion --Ops.Legion#LEGION
for _,_cohort in pairs(legion.cohorts) do
local cohort=_cohort --Ops.Cohort#COHORT
for _,_asset in pairs(cohort.assets) do
local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem
table.insert(assets, asset)
text=text..string.format("\n- %s [UID=%d] Legion=%s, Cohort=%s: Spawned=%s, Requested=%s [RID=%s], Reserved=%s",
asset.spawngroupname, asset.uid, legion.alias, cohort.name, tostring(asset.spawned), tostring(asset.requested), tostring(asset.rid), tostring(asset.isReserved))
if asset.spawned then
Nspawned=Nspawned+1
end
if asset.requested then
Nrequested=Nrequested+1
end
if asset.isReserved then
Nreserved=Nreserved+1
end
if not (asset.spawned or asset.requested or asset.isReserved) then
Nstock=Nstock+1
end
Ntotal=Ntotal+1
end
end
end
text=text.."\n-------------------------------------------"
text=text..string.format("\nNstock = %d", Nstock)
text=text..string.format("\nNreserved = %d", Nreserved)
text=text..string.format("\nNrequested = %d", Nrequested)
text=text..string.format("\nNspawned = %d", Nspawned)
text=text..string.format("\nNtotal = %d (=%d)", Ntotal, Nstock+Nspawned+Nrequested+Nreserved)
text=text.."\n==========================================="
self:I(self.lid..text)
end
---
-- MISSIONS
---
-- Mission queue.
if #self.missionqueue>0 then
@ -314,6 +380,9 @@ function COMMANDER:onafterMissionAssign(From, Event, To, Legion, Mission)
-- Add mission to legion.
Legion:AddMission(Mission)
-- Directly request the mission as the assets have already been selected.
Legion:MissionRequest(Mission)
end
--- On after "MissionCancel" event.
@ -362,25 +431,127 @@ end
function COMMANDER:CheckMissionQueue()
-- TODO: Sort mission queue. wrt what? Threat level?
-- Currently, we sort wrt to priority. So that should reflect the threat level of the mission target.
-- Number of missions.
local Nmissions=#self.missionqueue
-- Treat special cases.
if Nmissions==0 then
return nil
end
-- Sort results table wrt prio and start time.
local function _sort(a, b)
local taskA=a --Ops.Auftrag#AUFTRAG
local taskB=b --Ops.Auftrag#AUFTRAG
return (taskA.prio<taskB.prio) or (taskA.prio==taskB.prio and taskA.Tstart<taskB.Tstart)
end
table.sort(self.missionqueue, _sort)
-- Get the lowest importance value (lower means more important).
-- If a mission with importance 1 exists, mission with importance 2 will not be assigned. Missions with no importance (nil) can still be selected.
local vip=math.huge
for _,_mission in pairs(self.missionqueue) do
local mission=_mission --Ops.Auftrag#AUFTRAG
if mission.importance and mission.importance<vip then
vip=mission.importance
end
end
-- Loop over missions in queue.
for _,_mission in pairs(self.missionqueue) do
local mission=_mission --Ops.Auftrag#AUFTRAG
-- We look for PLANNED missions.
if mission:IsPlanned() then
if mission:IsPlanned() and mission:IsReadyToGo() and (mission.importance==nil or mission.importance<=vip) then
---
-- PLANNNED Mission
---
---
-- 1. Select best assets from legions
---
-- Get legions for 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
for _,_asset in pairs(assets) do
local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem
asset.payload=asset.legion:FetchPayloadFromStock(asset.unittype, mission.type, mission.payloads)
asset.score=asset.legion:CalculateAssetMissionScore(asset, mission, true)
end
--- Sort assets wrt to their mission score. Higher is better.
local function optimize(assetA, assetB)
return (assetA.score>assetB.score)
end
table.sort(assets, optimize)
-- Remove distance parameter.
local text=string.format("Optimized assets for %s mission:", mission.type)
for i,Asset in pairs(assets) do
local asset=Asset --Functional.Warehouse#WAREHOUSE.Assetitem
-- Score text.
text=text..string.format("\n%s %s: score=%d", asset.squadname, asset.spawngroupname, asset.score)
-- Nillify score.
asset.score=nil
-- Add assets to mission.
if i<=mission.nassets then
-- Add asset to mission.
mission:AddAsset(Asset)
-- Put into table.
legions[asset.legion.alias]=asset.legion
-- Number of assets requested from this legion.
-- TODO: Check if this is really necessary as we do not go through the selection process.
mission.Nassets=mission.Nassets or {}
if mission.Nassets[asset.legion.alias] then
mission.Nassets[asset.legion.alias]=mission.Nassets[asset.legion.alias]+1
else
mission.Nassets[asset.legion.alias]=1
end
else
-- Return payload of asset (if any).
if asset.payload then
asset.legion:ReturnPayloadFromAsset(asset)
end
end
end
self:T2(self.lid..text)
else
self:T2(self.lid..string.format("Not enough assets available for mission"))
end
---
-- Assign Mission to Legions
---
if legions 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)
@ -424,7 +595,7 @@ function COMMANDER:GetLegionsForMission(Mission)
end
-- Has it assets that can?
if Nassets>0 then
if Nassets>0 and false then
-- Get coordinate of the target.
local coord=Mission:GetTargetCoordinate()
@ -447,61 +618,14 @@ function COMMANDER:GetLegionsForMission(Mission)
end
-- Add legion if it can provide at least 1 asset.
if Nassets>0 then
table.insert(legions, legion)
end
-- Can anyone?
if #legions>0 then
--- Something like:
-- * Closest legion that can should be first prio.
-- * However, there should be a certain "quantization". if wing is 50 or 60 NM way should not really matter. In that case, the legion with more resources should get the job.
local function score(a)
local d=math.round(a.dist/10)
end
env.info(self.lid.."FF #legions="..#legions)
-- Sort table wrt distance and number of assets.
-- Distances within 10 NM are equal and the legion with more assets is preferred.
local function sortdist(a,b)
local ad=a.dist
local bd=b.dist
return ad<bd or (ad==bd and a.nassets>b.nassets)
end
table.sort(legions, sortdist)
-- Loops over all legions and stop if enough assets are summed up.
local selection={} ; local N=0
for _,leg in ipairs(legions) do
local legion=leg.airwing --Ops.Legion#LEGION
Mission.Nassets=Mission.Nassets or {}
Mission.Nassets[legion.alias]=leg.nassets
table.insert(selection, legion)
N=N+leg.nassets
if N>=Mission.nassets then
self:I(self.lid..string.format("Found enough assets!"))
break
end
end
if N>=Mission.nassets then
self:I(self.lid..string.format("Found %d legions that can do mission %s (%s) requiring %d assets", #selection, Mission:GetName(), Mission:GetType(), Mission.nassets))
return selection
else
self:T(self.lid..string.format("Not enough LEGIONs found that could do the job :/ Number of assets avail %d < %d required for the mission", N, Mission.nassets))
return nil
end
else
self:T(self.lid..string.format("No LEGION found that could do the job :/"))
end
return nil
return legions
end
--- Count assets of all assigned legions.
@ -521,6 +645,42 @@ function COMMANDER:CountAssets(InStock, MissionTypes, Attributes)
return N
end
--- Count assets of all assigned legions.
-- @param #COMMANDER self
-- @param #boolean InStock If true, only assets that are in the warehouse stock/inventory are counted.
-- @param #table Legions (Optional) Table of legions. Default is all legions.
-- @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 Amount of asset groups.
function COMMANDER:GetAssets(InStock, Legions, MissionTypes, Attributes)
-- Selected assets.
local assets={}
for _,_legion in pairs(Legions or self.legions) do
local legion=_legion --Ops.Legion#LEGION
--TODO Check if legion is running and maybe if runway is operational if air assets are requested.
for _,_cohort in pairs(legion.cohorts) do
local cohort=_cohort --Ops.Cohort#COHORT
for _,_asset in pairs(cohort.assets) do
local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem
-- TODO: Check if repaired.
-- TODO: currently we take only unspawned assets.
if not (asset.spawned or asset.isReserved or asset.requested) then
table.insert(assets, asset)
end
end
end
end
return assets
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

View File

@ -118,6 +118,12 @@ function LEGION:New(WarehouseName, LegionName)
-- @param #LEGION self
-- @param Ops.Auftrag#AUFTRAG Mission The mission.
--- Triggers the FSM event "MissionCancel" after a delay.
-- @function [parent=#LEGION] __MissionCancel
-- @param #LEGION self
-- @param #number delay Delay in seconds.
-- @param Ops.Auftrag#AUFTRAG Mission The mission.
--- On after "MissionCancel" event.
-- @function [parent=#LEGION] OnAfterMissionCancel
-- @param #LEGION self
@ -446,12 +452,14 @@ function LEGION:_GetNextTransport()
local function getAssets(n)
local assets={}
-- Loop over assets.
-- Loop over cohorts.
for _,_cohort in pairs(self.cohorts) do
local cohort=_cohort --Ops.Cohort#COHORT
-- Check if chort can do a transport.
if cohort:CheckMissionCapability({AUFTRAG.Type.OPSTRANSPORT}, cohort.missiontypes) then
-- Loop over cohort assets.
for _,_asset in pairs(cohort.assets) do
local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem
@ -534,6 +542,23 @@ function LEGION:CalculateAssetMissionScore(asset, Mission, includePayload)
end
end
-- Get coordinate of the target.
local coord=Mission:GetTargetCoordinate()
local dist=0
if coord then
-- Distance from legion to target.
local distance=UTILS.MetersToNM(coord:Get2DDistance(self:GetCoordinate()))
-- Round: 55 NM ==> 5.5 ==> 6, 63 NM ==> 6.3 ==> 6
dist=UTILS.Round(distance/10, 0)
end
-- Reduce score for legions that are futher away.
score=score-dist
-- TODO: This could be vastly improved. Need to gather ideas during testing.
-- Calculate ETA? Assets on orbit missions should arrive faster even if they are further away.
-- Max speed of assets.
@ -669,6 +694,7 @@ function LEGION:onafterMissionRequest(From, Event, To, Mission)
-- Set asset to requested! Important so that new requests do not use this asset!
asset.requested=true
asset.isReserved=false
if Mission.missionTask then
asset.missionTask=Mission.missionTask
@ -728,6 +754,7 @@ function LEGION:onafterTransportRequest(From, Event, To, OpsTransport)
-- The queueid has been increased in the onafterAddRequest function. So we can simply use it here.
OpsTransport.requestID=OpsTransport.requestID or {}
OpsTransport.requestID[self.alias]=self.queueid
end
end
@ -1174,7 +1201,6 @@ function LEGION:_CreateFlightGroup(asset)
opsgroup=FLIGHTGROUP:New(asset.spawngroupname)
elseif self:IsBrigade() then
---
@ -1183,8 +1209,6 @@ function LEGION:_CreateFlightGroup(asset)
opsgroup=ARMYGROUP:New(asset.spawngroupname)
else
self:E(self.lid.."ERROR: not airwing or brigade!")
end
@ -1198,6 +1222,8 @@ function LEGION:_CreateFlightGroup(asset)
-- Set home base.
opsgroup.homebase=self.airbase
-- Set home zone.
opsgroup.homezone=self.spawnzone
-- Set weapon data.
if opsgroup.cohort.weaponData then
@ -1594,10 +1620,11 @@ function LEGION:CanMission(Mission)
end
end
-- Loop over cohorts and recruit assets.
for cohortname,_cohort in pairs(cohorts) do
local cohort=_cohort --Ops.Cohort#COHORT
-- Check if this squadron can.
-- Check if this cohort can.
local can=cohort:CanMission(Mission)
if can then
@ -1774,6 +1801,26 @@ function LEGION:GetMissionFromRequest(Request)
return self:GetMissionFromRequestID(Request.uid)
end
--- Fetch a payload from the airwing resources for a given unit and mission type.
-- The payload with the highest priority is preferred.
-- @param #LEGION self
-- @param #string UnitType The type of the unit.
-- @param #string MissionType The mission type.
-- @param #table Payloads Specific payloads only to be considered.
-- @return Ops.Airwing#AIRWING.Payload Payload table or *nil*.
function LEGION:FetchPayloadFromStock(UnitType, MissionType, Payloads)
-- Polymorphic. Will return something when called by airwing.
return nil
end
--- Return payload from asset back to stock.
-- @param #LEGION self
-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset The squadron asset.
function LEGION:ReturnPayloadFromAsset(asset)
-- Polymorphic.
return nil
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

View File

@ -52,6 +52,8 @@
-- @field #number Ndelivered Total number of cargo groups delivered.
-- @field #table pathsTransport Transport paths of `#OPSGROUP.Path`.
-- @field #table pathsPickup Pickup paths of `#OPSGROUP.Path`.
-- @field Ops.Auftrag#AUFTRAG mission The mission attached to this transport.
-- @field #table assets Warehouse assets assigned for this transport.
-- @extends Core.Fsm#FSM
--- *Victory is the beautiful, bright-colored flower. Transport is the stem without which it could never have blossomed.* -- Winston Churchill