From 1d0eb9806dcb6049f9c0dfda74c8ed351f446443 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 13 Sep 2021 08:31:00 +0200 Subject: [PATCH] COMMANDER - Added OPS transport (untested) --- Moose Development/Moose/Core/Zone.lua | 14 + .../Moose/Functional/Warehouse.lua | 137 ++-- Moose Development/Moose/Ops/Chief.lua | 26 + Moose Development/Moose/Ops/Commander.lua | 615 ++++++++++++++---- Moose Development/Moose/Ops/FlightGroup.lua | 18 +- Moose Development/Moose/Ops/Legion.lua | 29 +- Moose Development/Moose/Ops/OpsGroup.lua | 50 +- Moose Development/Moose/Ops/OpsTransport.lua | 26 +- Moose Development/Moose/Ops/Platoon.lua | 14 +- 9 files changed, 672 insertions(+), 257 deletions(-) diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index 45419d5f9..813d74e1f 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -2223,6 +2223,20 @@ do -- ZONE_AIRBASE self._.ZoneAirbase = Airbase self._.ZoneVec2Cache = self._.ZoneAirbase:GetVec2() + + if Airbase:IsShip() then + self.isShip=true + self.isHelipad=false + self.isAirdrome=false + elseif Airbase:IsHelipad() then + self.isShip=false + self.isHelipad=true + self.isAirdrome=false + elseif Airbase:IsAirdrome() then + self.isShip=false + self.isHelipad=false + self.isAirdrome=true + end -- Zone objects are added to the _DATABASE and SET_ZONE objects. _EVENTDISPATCHER:CreateEventNewZone( self ) diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 3fa15d19a..e67257aa5 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -1866,6 +1866,9 @@ function WAREHOUSE:New(warehouse, alias) self.isunit=false else self.isunit=true + if warehouse:IsShip() then + self.isShip=true + end end end @@ -1909,8 +1912,14 @@ function WAREHOUSE:New(warehouse, alias) end -- Define warehouse and default spawn zone. - self.zone=ZONE_RADIUS:New(string.format("Warehouse zone %s", self.warehouse:GetName()), warehouse:GetVec2(), 500) - self.spawnzone=ZONE_RADIUS:New(string.format("Warehouse %s spawn zone", self.warehouse:GetName()), warehouse:GetVec2(), 250) + if self.isShip then + self.zone=ZONE_AIRBASE:New(self.warehouse:GetName(), 1000) + self.spawnzone=ZONE_AIRBASE:New(self.warehouse:GetName(), 1000) + else + self.zone=ZONE_RADIUS:New(string.format("Warehouse zone %s", self.warehouse:GetName()), warehouse:GetVec2(), 500) + self.spawnzone=ZONE_RADIUS:New(string.format("Warehouse %s spawn zone", self.warehouse:GetName()), warehouse:GetVec2(), 250) + end + -- Defaults self:SetMarker(true) @@ -4507,6 +4516,11 @@ function WAREHOUSE:onafterRequest(From, Event, To, Request) return end + -- Trigger event. + if spawngroup then + self:__AssetSpawned(0.01, spawngroup, _assetitem, Request) + end + end -- Init problem table. @@ -5336,24 +5350,6 @@ function WAREHOUSE:onafterRunwayRepaired(From, Event, To) end ---- On before "AssetSpawned" event. Checks whether the asset was already set to "spawned" for groups with multiple units. --- @param #WAREHOUSE self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. --- @param Wrapper.Group#GROUP group The group spawned. --- @param #WAREHOUSE.Assetitem asset The asset that is dead. --- @param #WAREHOUSE.Pendingitem request The request of the dead asset. -function WAREHOUSE:onbeforeAssetSpawned(From, Event, To, group, asset, request) - if asset.spawned then - --return false - else - --return true - end - - return true -end - --- On after "AssetSpawned" event triggered when an asset group is spawned into the cruel world. -- @param #WAREHOUSE self -- @param #string From From state. @@ -5368,6 +5364,24 @@ function WAREHOUSE:onafterAssetSpawned(From, Event, To, group, asset, request) -- Sete asset state to spawned. asset.spawned=true + + -- Set spawn group name. + asset.spawngroupname=group:GetName() + + -- Remove asset from stock. + self:_DeleteStockItem(asset) + + -- Add group. + if asset.iscargo==true then + request.cargogroupset=request.cargogroupset or SET_GROUP:New() + request.cargogroupset:AddGroup(group) + else + request.transportgroupset=request.transportgroupset or SET_GROUP:New() + request.transportgroupset:AddGroup(group) + end + + -- Set warehouse state. + group:SetState(group, "WAREHOUSE", self) -- Check if all assets groups are spawned and trigger events. local n=0 @@ -5718,15 +5732,15 @@ function WAREHOUSE:_SpawnAssetRequest(Request) if asset.category==Group.Category.GROUND then -- Spawn ground troops. - _group=self:_SpawnAssetGroundNaval(_alias, asset, Request, self.spawnzone) + _group=self:_SpawnAssetGroundNaval(_alias, asset, Request, self.spawnzone, Request.lateActivation) elseif asset.category==Group.Category.AIRPLANE or asset.category==Group.Category.HELICOPTER then -- Spawn air units. if Parking[asset.uid] then - _group=self:_SpawnAssetAircraft(_alias, asset, Request, Parking[asset.uid], UnControlled) + _group=self:_SpawnAssetAircraft(_alias, asset, Request, Parking[asset.uid], UnControlled, Request.lateActivation) else - _group=self:_SpawnAssetAircraft(_alias, asset, Request, nil, UnControlled) + _group=self:_SpawnAssetAircraft(_alias, asset, Request, nil, UnControlled, Request.lateActivation) end elseif asset.category==Group.Category.TRAIN then @@ -5736,7 +5750,7 @@ function WAREHOUSE:_SpawnAssetRequest(Request) --TODO: Rail should only get one asset because they would spawn on top! -- Spawn naval assets. - _group=self:_SpawnAssetGroundNaval(_alias, asset, Request, self.spawnzone) + _group=self:_SpawnAssetGroundNaval(_alias, asset, Request, self.spawnzone, Request.lateActivation) end --self:E(self.lid.."ERROR: Spawning of TRAIN assets not possible yet!") @@ -5744,11 +5758,16 @@ function WAREHOUSE:_SpawnAssetRequest(Request) elseif asset.category==Group.Category.SHIP then -- Spawn naval assets. - _group=self:_SpawnAssetGroundNaval(_alias, asset, Request, self.portzone) + _group=self:_SpawnAssetGroundNaval(_alias, asset, Request, self.portzone, Request.lateActivation) else self:E(self.lid.."ERROR: Unknown asset category!") end + + -- Trigger event. + if _group then + self:__AssetSpawned(0.01, _group, asset, Request) + end end @@ -5761,9 +5780,9 @@ end -- @param #WAREHOUSE.Assetitem asset Ground asset that will be spawned. -- @param #WAREHOUSE.Queueitem request Request belonging to this asset. Needed for the name/alias. -- @param Core.Zone#ZONE spawnzone Zone where the assets should be spawned. --- @param #boolean aioff If true, AI of ground units are set to off. +-- @param #boolean lateactivated If true, groups are spawned late activated. -- @return Wrapper.Group#GROUP The spawned group or nil if the group could not be spawned. -function WAREHOUSE:_SpawnAssetGroundNaval(alias, asset, request, spawnzone, aioff) +function WAREHOUSE:_SpawnAssetGroundNaval(alias, asset, request, spawnzone, lateactivated) if asset and (asset.category==Group.Category.GROUND or asset.category==Group.Category.SHIP or asset.category==Group.Category.TRAIN) then @@ -5806,6 +5825,11 @@ function WAREHOUSE:_SpawnAssetGroundNaval(alias, asset, request, spawnzone, aiof end end + + -- Late activation. + template.lateActivation=lateactivated + + env.info("FF lateActivation="..tostring(template.lateActivation)) template.route.points[1].x = coord.x template.route.points[1].y = coord.z @@ -5817,14 +5841,6 @@ function WAREHOUSE:_SpawnAssetGroundNaval(alias, asset, request, spawnzone, aiof -- Spawn group. local group=_DATABASE:Spawn(template) --Wrapper.Group#GROUP - -- Activate group. Should only be necessary for late activated groups. - --group:Activate() - - -- Switch AI off if desired. This works only for ground and naval groups. - if aioff then - group:SetAIOff() - end - return group end @@ -5838,8 +5854,9 @@ end -- @param #WAREHOUSE.Queueitem request Request belonging to this asset. Needed for the name/alias. -- @param #table parking Parking data for this asset. -- @param #boolean uncontrolled Spawn aircraft in uncontrolled state. +-- @param #boolean lateactivated If true, groups are spawned late activated. -- @return Wrapper.Group#GROUP The spawned group or nil if the group could not be spawned. -function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrolled) +function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrolled, lateactivated) if asset and asset.category==Group.Category.AIRPLANE or asset.category==Group.Category.HELICOPTER then @@ -6329,54 +6346,13 @@ function WAREHOUSE:_OnEventBirth(EventData) local request=self:GetRequestByID(rid) if asset and request then - - if asset.spawned and type(asset.spawned)=="boolean" and asset.spawned==true then - return - end - + -- Debug message. self:T(self.lid..string.format("Warehouse %s captured event birth of request ID=%d, asset ID=%d, unit %s spawned=%s", self.alias, request.uid, asset.uid, EventData.IniUnitName, tostring(asset.spawned))) -- Set born to true. request.born=true - - if not asset.spawned then - asset.spawned=1 - else - asset.spawned=asset.spawned+1 - end - - - -- Birth is triggered for each unit. We need to make sure not to call this too often! - if asset.spawned==asset.nunits then - - -- Remove asset from stock. - self:_DeleteStockItem(asset) - - -- Set spawned switch. - asset.spawned=true - asset.spawngroupname=group:GetName() - - -- Add group. - if asset.iscargo==true then - request.cargogroupset=request.cargogroupset or SET_GROUP:New() - request.cargogroupset:AddGroup(group) - else - request.transportgroupset=request.transportgroupset or SET_GROUP:New() - request.transportgroupset:AddGroup(group) - end - - -- Set warehouse state. - group:SetState(group, "WAREHOUSE", self) - - -- Asset spawned FSM function. - -- This needs to be delayed a bit for all units to be present. Especially, since MOOSE needs a birth event for UNITs to be added to the _DATABASE. - self:__AssetSpawned(0.1, group, asset, request) - --self:AssetSpawned(group, asset, request) - - end - else self:E(self.lid..string.format("ERROR: Either asset AID=%s or request RID=%s are nil in event birth of unit %s", tostring(aid), tostring(rid), tostring(EventData.IniUnitName))) end @@ -7076,10 +7052,9 @@ function WAREHOUSE:_CheckRequestValid(request) -- Check that both spawn zones are not in water. local inwater=self.spawnzone:GetCoordinate():IsSurfaceTypeWater() or request.warehouse.spawnzone:GetCoordinate():IsSurfaceTypeWater() - if inwater then + if inwater and not request.lateActivation then self:E("ERROR: Incorrect request. Ground asset requested but at least one spawn zone is in water!") - --valid=false - valid=false + return false end -- No ground assets directly to or from ships. diff --git a/Moose Development/Moose/Ops/Chief.lua b/Moose Development/Moose/Ops/Chief.lua index 0e3c868b2..255cb9f4f 100644 --- a/Moose Development/Moose/Ops/Chief.lua +++ b/Moose Development/Moose/Ops/Chief.lua @@ -299,6 +299,32 @@ function CHIEF:RemoveMission(Mission) return self end +--- Add transport to transport queue of the COMMANDER. +-- @param #CHIEF self +-- @param Ops.OpsTransport#OPSTRANSPORT Transport Transport to be added. +-- @return #CHIEF self +function CHIEF:AddOpsTransport(Transport) + + Transport.chief=self + + self.commander:AddOpsTransport(Transport) + + return self +end + +--- Remove transport from queue. +-- @param #CHIEF self +-- @param Ops.OpsTransport#OPSTRANSPORT Transport Transport to be removed. +-- @return #CHIEF self +function CHIEF:RemoveTransport(Transport) + + Transport.chief=nil + + self.commander:RemoveTransport(Transport) + + return self +end + --- Add target. -- @param #CHIEF self -- @param Ops.Target#TARGET Target Target object to be added. diff --git a/Moose Development/Moose/Ops/Commander.lua b/Moose Development/Moose/Ops/Commander.lua index dc4f4508c..f8776fa98 100644 --- a/Moose Development/Moose/Ops/Commander.lua +++ b/Moose Development/Moose/Ops/Commander.lua @@ -19,6 +19,7 @@ -- @field #string lid Class id string for output to DCS log file. -- @field #table legions Table of legions which are commanded. -- @field #table missionqueue Mission queue. +-- @field #table transportqueue Transport queue. -- @field Ops.ChiefOfStaff#CHIEF chief Chief of staff. -- @extends Core.Fsm#FSM @@ -37,6 +38,7 @@ COMMANDER = { verbose = 0, legions = {}, missionqueue = {}, + transportqueue = {}, } --- COMMANDER class version. @@ -80,6 +82,9 @@ function COMMANDER:New() self:AddTransition("*", "MissionAssign", "*") -- Mission is assigned to a or multiple LEGIONs. self:AddTransition("*", "MissionCancel", "*") -- COMMANDER cancels a mission. + self:AddTransition("*", "TransportAssign", "*") -- Transport is assigned to a or multiple LEGIONs. + self:AddTransition("*", "TransportCancel", "*") -- COMMANDER cancels a Transport. + ------------------------ --- Pseudo Functions --- ------------------------ @@ -155,6 +160,43 @@ function COMMANDER:New() -- @param #string To To state. -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "TransportAssign" after a delay. + -- @function [parent=#COMMANDER] __TransportAssign + -- @param #COMMANDER self + -- @param #number delay Delay in seconds. + -- @param Ops.Legion#LEGION Legion The Legion. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + --- On after "TransportAssign" event. + -- @function [parent=#COMMANDER] OnAfterTransportAssign + -- @param #COMMANDER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Legion#LEGION Legion The Legion. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + + --- Triggers the FSM event "TransportCancel". + -- @function [parent=#COMMANDER] TransportCancel + -- @param #COMMANDER self + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + --- Triggers the FSM event "TransportCancel" after a delay. + -- @function [parent=#COMMANDER] __TransportCancel + -- @param #COMMANDER self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + --- On after "TransportCancel" event. + -- @function [parent=#COMMANDER] OnAfterTransportCancel + -- @param #COMMANDER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + return self end @@ -225,6 +267,21 @@ function COMMANDER:AddMission(Mission) return self end +--- Add transport to queue. +-- @param #COMMANDER self +-- @param Ops.OpsTransport#OPSTRANSPORT Transport The OPS transport to be added. +-- @return #COMMANDER self +function COMMANDER:AddOpsTransport(Transport) + + Transport.commander=self + + Transport.statusCommander=TRANSPORT.Status.PLANNED + + table.insert(self.transportqueue, Transport) + + return self +end + --- Remove mission from queue. -- @param #COMMANDER self -- @param Ops.Auftrag#AUFTRAG Mission Mission to be removed. @@ -246,6 +303,27 @@ function COMMANDER:RemoveMission(Mission) return self end +--- Remove transport from queue. +-- @param #COMMANDER self +-- @param Ops.OpsTransport#OPSTRANSPORT Transport The OPS transport to be removed. +-- @return #COMMANDER self +function COMMANDER:RemoveTransport(Transport) + + for i,_transport in pairs(self.transportqueue) do + local transport=_transport --Ops.OpsTransport#OPSTRANSPORT + + if transport.uid==Transport.uid then + self:I(self.lid..string.format("Removing mission %s (%s) status=%s from queue", transport.uid, transport:GetState())) + transport.commander=nil + table.remove(self.transportqueue, i) + break + end + + end + + return self +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Start & Status ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -292,6 +370,8 @@ function COMMANDER:onafterStatus(From, Event, To) -- Check mission queue and assign one PLANNED mission. self:CheckMissionQueue() + + -- Check mission queue and assign one PLANNED mission --- -- LEGIONS @@ -387,6 +467,20 @@ function COMMANDER:onafterStatus(From, Event, To) end self:I(self.lid..text) end + + --- + -- TRANSPORTS + --- + + -- Transport queue. + if self.verbose>=2 and #self.transportqueue>0 then + local text="Transport queue:" + for i,_transport in pairs(self.transportqueue) do + local transport=_transport --Ops.OpsTransport#OPSTRANSPORT + text=text..string.format("\n[%d] UID=%d: status=%s", i, transport.uid, transport:GetState()) + end + self:I(self.lid..text) + end self:__Status(-30) end @@ -455,8 +549,31 @@ function COMMANDER:onafterMissionCancel(From, Event, To, Mission) end +--- On after "MissionAssign" event. Mission is added to a LEGION mission queue. +-- @param #COMMANDER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Legion#LEGION Legion The LEGION. +-- @param Ops.OpsTransport#OPSTRANSPORT +function COMMANDER:onafterTransportAssign(From, Event, To, Legion, Transport) + + -- Debug info. + self:I(self.lid..string.format("Assigning transport %d to legion %s", Transport.uid, Legion.alias)) + + -- Set mission commander status to QUEUED as it is now queued at a legion. + Transport.statusCommander=OPSTRANSPORT.Status.QUEUED + + -- Add mission to legion. + Legion:AddOpsTransport(Transport) + + -- Directly request the mission as the assets have already been selected. + Legion:TransportRequest(Transport) + +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Resources +-- Mission Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Check mission queue and assign ONE planned mission. @@ -538,113 +655,6 @@ function COMMANDER:CheckMissionQueue() end ---- Check all legions if they are able to do a specific mission type at a certain location with a given number of assets. --- @param #COMMANDER self --- @param Ops.Auftrag#AUFTRAG Mission The mission. --- @return #table Table of LEGIONs that can do the mission and have at least one asset available right now. -function COMMANDER:GetLegionsForMission(Mission) - - -- Table of legions that can do the mission. - local legions={} - - -- Loop over all legions. - for _,_legion in pairs(self.legions) do - local legion=_legion --Ops.Legion#LEGION - - -- Count number of assets in stock. - local Nassets=0 - if legion:IsAirwing() then - Nassets=legion:CountAssetsWithPayloadsInStock(Mission.payloads, {Mission.type}, Attributes) - else - Nassets=legion:CountAssets(true, {Mission.type}, Attributes) --Could also specify the attribute if Air or Ground mission. - end - - -- Has it assets that can? - if Nassets>0 and false then - - -- Get coordinate of the target. - local coord=Mission:GetTargetCoordinate() - - if coord then - - -- Distance from legion to target. - local distance=UTILS.MetersToNM(coord:Get2DDistance(legion:GetCoordinate())) - - -- Round: 55 NM ==> 5.5 ==> 6, 63 NM ==> 6.3 ==> 6 - local dist=UTILS.Round(distance/10, 0) - - -- Debug info. - self:I(self.lid..string.format("Got legion %s with Nassets=%d and dist=%.1f NM, rounded=%.1f", legion.alias, Nassets, distance, dist)) - - -- Add legion to table of legions that can. - table.insert(legions, {airwing=legion, distance=distance, dist=dist, targetcoord=coord, nassets=Nassets}) - - end - - end - - -- Add legion if it can provide at least 1 asset. - if Nassets>0 then - table.insert(legions, legion) - end - - end - - return legions -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 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:CountAssets(InStock, MissionTypes, Attributes) - - local N=0 - for _,_legion in pairs(self.legions) do - local legion=_legion --Ops.Legion#LEGION - N=N+legion:CountAssets(InStock, MissionTypes, Attributes) - end - - 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 --- Recruit assets for a given mission. -- @param #COMMANDER self @@ -757,7 +767,7 @@ function COMMANDER:RecruitAssets(Mission) -- Add assets to mission. for i=1,Nassets do local asset=Assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem - self:T(self.lid..string.format("Adding asset %s to mission %s [%s]", asset.spawngroupname, Mission.name, Mission.type)) + asset.isReserved=true Mission:AddAsset(asset) Legions[asset.legion.alias]=asset.legion end @@ -832,6 +842,393 @@ function COMMANDER:_OptimizeAssetSelection(assets, Mission, includePayload) end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Transport Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check transport queue and assign ONE planned transport. +-- @param #COMMANDER self +function COMMANDER:CheckTransportQueue() + + -- Number of missions. + local Ntransports=#self.transportqueue + + -- Treat special cases. + if Ntransports==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.prio0 then + + -- Calculate the max weight so we know which cohorts can provide carriers. + for _,_opsgroup in pairs(cargoOpsGroups) do + local opsgroup=_opsgroup --Ops.OpsGroup#OPSGROUP + local weight=opsgroup:GetWeightTotal() + if weight>weightGroup then + weightGroup=weight + end + end + + else + -- No cargo groups! + return false, {} + end + + -- The recruited assets. + local Assets={} + + -- Legions we consider for selecting assets. + local legions=self.legions + + --TODO: Setting of Mission.squadrons (cohorts) will not work here! + + -- Legions which have the best assets for the Mission. + 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(AUFTRAG.Type.OPSTRANSPORT, cohort.aircrafttype) or 999 + self:I(self.lid..string.format("Got N=%d payloads for mission type %s [%s]", Npayloads[cohort.aircrafttype], AUFTRAG.Type.OPSTRANSPORT, 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:IsOnDuty() and npayloads>0 and cohort:CheckMissionCapability({AUFTRAG.Type.OPSTRANSPORT}) and cohort.cargobayLimit>=weightGroup then + + -- Recruit assets from squadron. + local assets, npayloads=cohort:RecruitAssets(AUFTRAG.Type.OPSTRANSPORT, npayloads) + + Npayloads[cohort.aircrafttype]=npayloads + + for _,asset in pairs(assets) do + table.insert(Assets, asset) + end + + end + + end + + end + + -- Now we have a long list with assets. + self:_OptimizeAssetSelectionForTransport(Assets, Transport) + + 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, AUFTRAG.Type.OPSTRANSPORT) + + 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 + + + -- Number of required carriers. + local NreqMin,NreqMax=Transport:GetRequiredCarriers() + + -- Number of assets. At most NreqMax. + local Nassets=math.min(#Assets, NreqMax) + + if Nassets>=NreqMin then + + --- + -- Found enough assets + --- + + -- Add assets to transport. + for i=1,Nassets do + local asset=Assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem + asset.isReserved=true + Transport: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:T(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:T2(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 given transport. +-- @param #COMMANDER self +-- @param #table assets Table of (unoptimized) assets. +-- @param Ops.OpsTransport#OPSTRANSPORT Transport Transport assignment. +function COMMANDER:_OptimizeAssetSelectionForTransport(assets, Transport) + + -- Calculate the mission score of all assets. + for _,_asset in pairs(assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + asset.score=asset.legion:CalculateAssetTransportScore(asset, Transport) + 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. + return (assetA.score>assetB.score) + end + table.sort(assets, optimize) + + -- Remove distance parameter. + local text=string.format("Optimized %d assets for %s mission (payload=%s):", #assets, Mission.type, 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) + asset.dist=nil + asset.score=nil + end + self:T2(self.lid..text) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Resources +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- 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 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:CountAssets(InStock, MissionTypes, Attributes) + + local N=0 + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + N=N+legion:CountAssets(InStock, MissionTypes, Attributes) + end + + 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 + +--- Check all legions if they are able to do a specific mission type at a certain location with a given number of assets. +-- @param #COMMANDER self +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @return #table Table of LEGIONs that can do the mission and have at least one asset available right now. +function COMMANDER:GetLegionsForMission(Mission) + + -- Table of legions that can do the mission. + local legions={} + + -- Loop over all legions. + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + + -- Count number of assets in stock. + local Nassets=0 + if legion:IsAirwing() then + Nassets=legion:CountAssetsWithPayloadsInStock(Mission.payloads, {Mission.type}, Attributes) + else + Nassets=legion:CountAssets(true, {Mission.type}, Attributes) --Could also specify the attribute if Air or Ground mission. + end + + -- Has it assets that can? + if Nassets>0 and false then + + -- Get coordinate of the target. + local coord=Mission:GetTargetCoordinate() + + if coord then + + -- Distance from legion to target. + local distance=UTILS.MetersToNM(coord:Get2DDistance(legion:GetCoordinate())) + + -- Round: 55 NM ==> 5.5 ==> 6, 63 NM ==> 6.3 ==> 6 + local dist=UTILS.Round(distance/10, 0) + + -- Debug info. + self:I(self.lid..string.format("Got legion %s with Nassets=%d and dist=%.1f NM, rounded=%.1f", legion.alias, Nassets, distance, dist)) + + -- Add legion to table of legions that can. + table.insert(legions, {airwing=legion, distance=distance, dist=dist, targetcoord=coord, nassets=Nassets}) + + end + + end + + -- Add legion if it can provide at least 1 asset. + if Nassets>0 then + table.insert(legions, legion) + end + + end + + return legions +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- \ No newline at end of file diff --git a/Moose Development/Moose/Ops/FlightGroup.lua b/Moose Development/Moose/Ops/FlightGroup.lua index a5e3b0036..abd32593c 100644 --- a/Moose Development/Moose/Ops/FlightGroup.lua +++ b/Moose Development/Moose/Ops/FlightGroup.lua @@ -1374,16 +1374,17 @@ end -- @param Wrapper.Airbase#AIRBASE.ParkingSpot Spot Parking Spot. function FLIGHTGROUP:onafterElementParking(From, Event, To, Element, Spot) + -- Set parking spot. + if Spot then + self:_SetElementParkingAt(Element, Spot) + end + -- Debug info. self:T(self.lid..string.format("Element parking %s at spot %s", Element.name, Element.parking and tostring(Element.parking.TerminalID) or "N/A")) -- Set element status. self:_UpdateStatus(Element, OPSGROUP.ElementStatus.PARKING) - if Spot then - self:_SetElementParkingAt(Element, Spot) - end - if self:IsTakeoffCold() then -- Wait for engine startup event. elseif self:IsTakeoffHot() then @@ -1660,11 +1661,16 @@ end -- @param #string Event Event. -- @param #string To To state. function FLIGHTGROUP:onafterParking(From, Event, To) - self:T(self.lid..string.format("Flight is parking")) + -- Get closest airbase local airbase=self:GetClosestAirbase() --self.group:GetCoordinate():GetClosestAirbase() - local airbasename=airbase:GetName() or "unknown" + + -- Debug info + self:T(self.lid..string.format("Flight is parking at airbase %s", airbasename)) + + -- Set current airbase. + self.currbase=airbase -- Parking time stamp. self.Tparking=timer.getAbsTime() diff --git a/Moose Development/Moose/Ops/Legion.lua b/Moose Development/Moose/Ops/Legion.lua index c7930bcad..96de22712 100644 --- a/Moose Development/Moose/Ops/Legion.lua +++ b/Moose Development/Moose/Ops/Legion.lua @@ -256,8 +256,11 @@ function LEGION:AddMission(Mission) -- Add ops transport to transport Legions. if Mission.opstransport then + local PickupZone=self.spawnzone + local DeployZone=Mission.opstransport.tzcDefault.DeployZone + -- Add a new TZC: from pickup here to the deploy zone. - local tzc=Mission.opstransport:AddTransportZoneCombo(self.spawnzone, Mission.opstransport.tzcDefault.DeployZone) + local tzc=Mission.opstransport:AddTransportZoneCombo(PickupZone, DeployZone) --TODO: Depending on "from where to where" the assets need to transported, we need to set ZONE_AIRBASE etc. @@ -601,6 +604,17 @@ function LEGION:onafterMissionRequest(From, Event, To, Mission) -- The queueid has been increased in the onafterAddRequest function. So we can simply use it here. Mission.requestID[self.alias]=self.queueid + + -- Get request. + local request=self:GetRequestByID(self.queueid) + + if request then + if self.isShip then + self:T(self.lid.."FF request late activated") + request.lateActivation=true + end + end + end end @@ -653,7 +667,6 @@ 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[self.alias]=self.queueid - end end @@ -1571,7 +1584,7 @@ function LEGION:RecruitAssets(Mission) MissionType=Mission.alert5MissionType end Npayloads[cohort.aircrafttype]=self:IsAirwing() and self:CountPayloadsInStock(MissionType, cohort.aircrafttype, Mission.payloads) or 999 - self:I(self.lid..string.format("Got N=%d payloads for mission type=%s and unit type=%s", Npayloads[cohort.aircrafttype], MissionType, cohort.aircrafttype)) + self:T2(self.lid..string.format("Got N=%d payloads for mission type=%s and unit type=%s", Npayloads[cohort.aircrafttype], MissionType, cohort.aircrafttype)) end end @@ -1818,6 +1831,9 @@ function LEGION:RecruitAssetsForTransport(Transport) weightGroup=weight end end + else + -- No cargo groups! + return false end @@ -1829,7 +1845,7 @@ function LEGION:RecruitAssetsForTransport(Transport) local cohort=_cohort --Ops.Cohort#COHORT if Npayloads[cohort.aircrafttype]==nil then Npayloads[cohort.aircrafttype]=self:IsAirwing() and self:CountPayloadsInStock(AUFTRAG.Type.OPSTRANSPORT, cohort.aircrafttype) or 999 - self:I(self.lid..string.format("Got N=%d payloads for mission type=%s and unit type=%s", Npayloads[cohort.aircrafttype], AUFTRAG.Type.OPSTRANSPORT, cohort.aircrafttype)) + self:T2(self.lid..string.format("Got N=%d payloads for mission type=%s and unit type=%s", Npayloads[cohort.aircrafttype], AUFTRAG.Type.OPSTRANSPORT, cohort.aircrafttype)) end end @@ -1858,7 +1874,7 @@ function LEGION:RecruitAssetsForTransport(Transport) end -- Sort asset list. Best ones come first. - self:_OptimizeAssetSelectionForTransport(Assets, Transport, false) + self:_OptimizeAssetSelectionForTransport(Assets, Transport) -- If airwing, get the best payload available. if self:IsAirwing() then @@ -2019,7 +2035,8 @@ function LEGION:CalculateAssetTransportScore(asset, Transport) -- Reduce score for legions that are futher away. score=score-distance - --TODO: Check cargo bay capacity. + -- Add 1 score point for each 10 kg of cargo bay. + score=score+UTILS.Round(asset.cargobaymax/10, 0) --TODO: Check ALERT 5 for Transports. if asset.spawned then diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index 7e72e1a32..3fcc7f0eb 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -467,12 +467,12 @@ OPSGROUP.version="0.7.5" -- TODO: Invisible/immortal. -- TODO: F10 menu. -- TODO: Add pseudo function. --- TODO: Options EPLRS -- TODO: Afterburner restrict -- TODO: What more options? -- TODO: Damage? -- TODO: Shot events? -- TODO: Marks to add waypoints/tasks on-the-fly. +-- DONE: Options EPLRS -- DONE: A lot. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -3344,8 +3344,9 @@ function OPSGROUP:onafterTaskExecute(From, Event, To, Task) -- Target local target=Task.dcstask.params.target --Ops.Target#TARGET - Task.dcstask.params.lastindex=1 + self.lastindex=1 + -- Target object and zone. local object=target.targets[1] --Ops.Target#TARGET.Object local zone=object.Object --Core.Zone#ZONE @@ -4495,9 +4496,10 @@ function OPSGROUP:onafterPassingWaypoint(From, Event, To, Waypoint) -- SPECIAL TASK: Recon Mission --- + -- TARGET. local target=task.dcstask.params.target --Ops.Target#TARGET - local n=task.dcstask.params.lastindex+1 + local n=self.lastindex+1 if n<=#target.targets then @@ -4528,7 +4530,7 @@ function OPSGROUP:onafterPassingWaypoint(From, Event, To, Waypoint) wp.missionUID=mission and mission.auftragsnummer or nil -- Increase counter. - task.dcstask.params.lastindex=task.dcstask.params.lastindex+1 + self.lastindex=self.lastindex+1 else @@ -4599,7 +4601,7 @@ function OPSGROUP:onafterPassingWaypoint(From, Event, To, Waypoint) -- Passing mission waypoint? if Waypoint.missionUID then - self:T(self.lid.."FF passing mission waypoint") + self:T2(self.lid..string.format("Passing mission waypoint")) end -- Check if all tasks/mission are done? @@ -6636,24 +6638,17 @@ function OPSGROUP:onafterPickup(From, Event, To) -- Flight Group --- + -- Activate uncontrolled group. + if self:IsParking() and self:IsUncontrolled() then + self:StartUncontrolled() + end + if airbasePickup then --- -- Pickup at airbase --- - -- Current airbase. - local airbaseCurrent=self.currbase - - if airbaseCurrent then - - -- Activate uncontrolled group. - if self:IsParking() and self:IsUncontrolled() then - self:StartUncontrolled() - end - - end - -- Order group to land at an airbase. self:__LandAtAirbase(-0.1, airbasePickup) @@ -6662,11 +6657,6 @@ function OPSGROUP:onafterPickup(From, Event, To) --- -- Helo can also land in a zone (NOTE: currently VTOL cannot!) --- - - -- Activate uncontrolled group. - if self:IsParking() and self:IsUncontrolled() then - self:StartUncontrolled(0.5) - end -- If this is a helo and no ZONE_AIRBASE was given, we make the helo land in the pickup zone. local waypoint=FLIGHTGROUP.AddWaypoint(self, Coordinate, nil, self:GetWaypointCurrent().uid, UTILS.MetersToFeet(self.altitudeCruise), false) ; waypoint.detour=1 @@ -7004,22 +6994,16 @@ function OPSGROUP:onafterTransport(From, Event, To) -- Add waypoint. if self:IsFlightgroup() then + -- Activate uncontrolled group. + if self:IsParking() and self:IsUncontrolled() then + self:StartUncontrolled() + end + if airbaseDeploy then --- -- Deploy at airbase --- - - local airbaseCurrent=self.currbase - - if airbaseCurrent then - - -- Activate uncontrolled group. - if self:IsParking() and self:IsUncontrolled() then - self:StartUncontrolled() - end - - end -- Order group to land at an airbase. self:__LandAtAirbase(-0.1, airbaseDeploy) diff --git a/Moose Development/Moose/Ops/OpsTransport.lua b/Moose Development/Moose/Ops/OpsTransport.lua index 50b613719..d33453ad9 100644 --- a/Moose Development/Moose/Ops/OpsTransport.lua +++ b/Moose Development/Moose/Ops/OpsTransport.lua @@ -53,6 +53,8 @@ -- @field #table assets Warehouse assets assigned for this transport. -- @field #table legions Assigned legions. -- @field #table statusLegion Transport status of all assigned LEGIONs. +-- @field #string statusCommander Staus of the COMMANDER. +-- @field Ops.Commander#COMMANDER commander Commander of the transport. -- @field #table requestID The ID of the queued warehouse request. Necessary to cancel the request if the transport was cancelled before the request is processed. -- -- @extends Core.Fsm#FSM @@ -133,6 +135,9 @@ OPSTRANSPORT = { -- @field #string SCHEDULED Transport is scheduled in the cargo queue. -- @field #string EXECUTING Transport is being executed. -- @field #string DELIVERED Transport was delivered. +-- @field #string CANCELLED Transport was cancelled. +-- @field #string SUCCESS Transport was a success. +-- @field #string FAILED Transport failed. OPSTRANSPORT.Status={ PLANNED="planned", QUEUED="queued", @@ -140,6 +145,9 @@ OPSTRANSPORT.Status={ SCHEDULED="scheduled", EXECUTING="executing", DELIVERED="delivered", + CANCELLED="cancelled", + SUCCESS="success", + FAILED="failed", } --- Pickup and deploy set. @@ -177,14 +185,14 @@ _OPSTRANSPORTID=0 --- Army Group version. -- @field #string version -OPSTRANSPORT.version="0.4.2" +OPSTRANSPORT.version="0.5.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Allow multiple pickup/depoly zones. -- TODO: Stop/abort transport. +-- DONE: Allow multiple pickup/depoly zones. -- DONE: Add start conditions. -- DONE: Check carrier(s) dead. @@ -459,20 +467,6 @@ function OPSTRANSPORT:AddCargoGroups(GroupSet, TransportZoneCombo) -- Call iteravely for each group. self:AddCargoGroups(group, TransportZoneCombo) - - --[[ - local cargo=self:_CreateCargoGroupData(group) - - if cargo then - -- Add to main table. - table.insert(self.cargos, cargo) - self.Ncargo=self.Ncargo+1 - - -- Add to TZC table. - table.insert(TransportZoneCombo.Cargos, cargo) - TransportZoneCombo.Ncargo=TransportZoneCombo.Ncargo+1 - end - ]] end end diff --git a/Moose Development/Moose/Ops/Platoon.lua b/Moose Development/Moose/Ops/Platoon.lua index da63f6400..cbaa8748f 100644 --- a/Moose Development/Moose/Ops/Platoon.lua +++ b/Moose Development/Moose/Ops/Platoon.lua @@ -110,14 +110,16 @@ function PLATOON:AddWeaponRange(RangeMin, RangeMax, BitType) self.weaponData[tostring(weapon.BitType)]=weapon -- Debug info. - env.info(string.format("FF Adding weapon data: Bit=%s, Rmin=%d m, Rmax=%d m", tostring(weapon.BitType), weapon.RangeMin, weapon.RangeMax)) + self:T(self.lid..string.format("Adding weapon data: Bit=%s, Rmin=%d m, Rmax=%d m", tostring(weapon.BitType), weapon.RangeMin, weapon.RangeMax)) - local text="Weapon data:" - for _,_weapondata in pairs(self.weaponData) do - local weapondata=_weapondata - text=text..string.format("\n- Bit=%s, Rmin=%d m, Rmax=%d m", tostring(weapondata.BitType), weapondata.RangeMin, weapondata.RangeMax) + if self.verbose>=2 then + local text="Weapon data:" + for _,_weapondata in pairs(self.weaponData) do + local weapondata=_weapondata + text=text..string.format("\n- Bit=%s, Rmin=%d m, Rmax=%d m", tostring(weapondata.BitType), weapondata.RangeMin, weapondata.RangeMax) + end + self:I(self.lid..text) end - self:I(self.lid..text) return self end