From 4a6377c20445cd399c0d39bf9ff959789bca5fac Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 27 Feb 2021 21:17:58 +0100 Subject: [PATCH] Ops Cargo --- Moose Development/Moose/Core/Database.lua | 16 + Moose Development/Moose/Core/Set.lua | 602 +++++++++++++++- Moose Development/Moose/Ops/ArmyGroup.lua | 5 - Moose Development/Moose/Ops/FlightGroup.lua | 5 +- Moose Development/Moose/Ops/NavyGroup.lua | 29 +- Moose Development/Moose/Ops/OpsGroup.lua | 684 ++++++++++--------- Moose Development/Moose/Ops/OpsTransport.lua | 400 ++++++++--- 7 files changed, 1276 insertions(+), 465 deletions(-) diff --git a/Moose Development/Moose/Core/Database.lua b/Moose Development/Moose/Core/Database.lua index 8ae42a151..35077c05b 100644 --- a/Moose Development/Moose/Core/Database.lua +++ b/Moose Development/Moose/Core/Database.lua @@ -1444,6 +1444,22 @@ function DATABASE:GetOpsGroup(groupname) return self.FLIGHTGROUPS[groupname] end +--- Find an OPSGROUP (FLIGHTGROUP, ARMYGROUP, NAVYGROUP) in the data base. +-- @param #DATABASE self +-- @param #string groupname Group name of the group. Can also be passed as GROUP object. +-- @return Ops.OpsGroup#OPSGROUP OPS group object. +function DATABASE:FindOpsGroup(groupname) + + -- Get group and group name. + if type(groupname)=="string" then + else + groupname=groupname:GetName() + end + + env.info("Getting OPSGROUP "..tostring(groupname)) + return self.FLIGHTGROUPS[groupname] +end + --- Add a flight control to the data base. -- @param #DATABASE self -- @param Ops.FlightControl#FLIGHTCONTROL flightcontrol diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index c8cb5dd05..5fb677d9e 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -233,7 +233,7 @@ do -- SET_BASE -- @param Core.Base#BASE Object The object itself. -- @return Core.Base#BASE The added BASE Object. function SET_BASE:Add( ObjectName, Object ) - self:F2( { ObjectName = ObjectName, Object = Object } ) + self:I( { ObjectName = ObjectName, Object = Object } ) -- Ensure that the existing element is removed from the Set before a new one is inserted to the Set if self.Set[ObjectName] then @@ -5948,3 +5948,603 @@ do -- SET_ZONE_GOAL end end + + + +do -- SET_OPSGROUP + + --- @type SET_OPSGROUP + -- @extends Core.Set#SET_BASE + + --- Mission designers can use the @{Core.Set#SET_OPSGROUP} class to build sets of OPS groups belonging to certain: + -- + -- * Coalitions + -- * Categories + -- * Countries + -- * Contain a certain string pattern + -- + -- ## SET_OPSGROUP constructor + -- + -- Create a new SET_OPSGROUP object with the @{#SET_OPSGROUP.New} method: + -- + -- * @{#SET_OPSGROUP.New}: Creates a new SET_OPSGROUP object. + -- + -- ## Add or Remove GROUP(s) from SET_OPSGROUP + -- + -- GROUPS can be added and removed using the @{Core.Set#SET_OPSGROUP.AddGroupsByName} and @{Core.Set#SET_OPSGROUP.RemoveGroupsByName} respectively. + -- These methods take a single GROUP name or an array of GROUP names to be added or removed from SET_OPSGROUP. + -- + -- ## SET_OPSGROUP filter criteria + -- + -- You can set filter criteria to define the set of groups within the SET_OPSGROUP. + -- Filter criteria are defined by: + -- + -- * @{#SET_OPSGROUP.FilterCoalitions}: Builds the SET_OPSGROUP with the groups belonging to the coalition(s). + -- * @{#SET_OPSGROUP.FilterCategories}: Builds the SET_OPSGROUP with the groups belonging to the category(ies). + -- * @{#SET_OPSGROUP.FilterCountries}: Builds the SET_OPSGROUP with the groups belonging to the country(ies). + -- * @{#SET_OPSGROUP.FilterPrefixes}: Builds the SET_OPSGROUP with the groups *containing* the given string in the group name. **Attention!** Bad naming convention, as this not really filtering *prefixes*. + -- * @{#SET_OPSGROUP.FilterActive}: Builds the SET_OPSGROUP with the groups that are only active. Groups that are inactive (late activation) won't be included in the set! + -- + -- For the Category Filter, extra methods have been added: + -- + -- * @{#SET_OPSGROUP.FilterCategoryAirplane}: Builds the SET_OPSGROUP from airplanes. + -- * @{#SET_OPSGROUP.FilterCategoryHelicopter}: Builds the SET_OPSGROUP from helicopters. + -- * @{#SET_OPSGROUP.FilterCategoryGround}: Builds the SET_OPSGROUP from ground vehicles or infantry. + -- * @{#SET_OPSGROUP.FilterCategoryShip}: Builds the SET_OPSGROUP from ships. + -- + -- + -- Once the filter criteria have been set for the SET_OPSGROUP, you can start filtering using: + -- + -- * @{#SET_OPSGROUP.FilterStart}: Starts the filtering of the groups within the SET_OPSGROUP and add or remove GROUP objects **dynamically**. + -- * @{#SET_OPSGROUP.FilterOnce}: Filters of the groups **once**. + -- + -- + -- ## SET_OPSGROUP iterators + -- + -- Once the filters have been defined and the SET_OPSGROUP has been built, you can iterate the SET_OPSGROUP with the available iterator methods. + -- The iterator methods will walk the SET_OPSGROUP set, and call for each element within the set a function that you provide. + -- The following iterator methods are currently available within the SET_OPSGROUP: + -- + -- * @{#SET_OPSGROUP.ForEachGroup}: Calls a function for each alive group it finds within the SET_OPSGROUP. + -- + -- ## SET_OPSGROUP trigger events on the GROUP objects. + -- + -- The SET is derived from the FSM class, which provides extra capabilities to track the contents of the GROUP objects in the SET_OPSGROUP. + -- + -- ### When a GROUP object crashes or is dead, the SET_OPSGROUP will trigger a **Dead** event. + -- + -- You can handle the event using the OnBefore and OnAfter event handlers. + -- The event handlers need to have the paramters From, Event, To, GroupObject. + -- The GroupObject is the GROUP object that is dead and within the SET_OPSGROUP, and is passed as a parameter to the event handler. + -- See the following example: + -- + -- -- Create the SetCarrier SET_OPSGROUP collection. + -- + -- local SetHelicopter = SET_OPSGROUP:New():FilterPrefixes( "Helicopter" ):FilterStart() + -- + -- -- Put a Dead event handler on SetCarrier, to ensure that when a carrier is destroyed, that all internal parameters are reset. + -- + -- function SetHelicopter:OnAfterDead( From, Event, To, GroupObject ) + -- self:F( { GroupObject = GroupObject:GetName() } ) + -- end + -- + -- + -- === + -- + -- @field #SET_OPSGROUP SET_OPSGROUP + -- + SET_OPSGROUP = { + ClassName = "SET_OPSGROUP", + Filter = { + Coalitions = nil, + Categories = nil, + Countries = nil, + GroupPrefixes = nil, + }, + FilterMeta = { + Coalitions = { + red = coalition.side.RED, + blue = coalition.side.BLUE, + neutral = coalition.side.NEUTRAL, + }, + Categories = { + plane = Group.Category.AIRPLANE, + helicopter = Group.Category.HELICOPTER, + ground = Group.Category.GROUND, + ship = Group.Category.SHIP, + }, + }, -- FilterMeta + } + + + --- Creates a new SET_OPSGROUP object, building a set of groups belonging to a coalitions, categories, countries, types or with defined prefix names. + -- @param #SET_OPSGROUP self + -- @return #SET_OPSGROUP + function SET_OPSGROUP:New() + + -- Inherit SET_BASE. + local self = BASE:Inherit(self, SET_BASE:New(_DATABASE.GROUPS)) -- #SET_OPSGROUP + + -- Include non activated + self:FilterActive( false ) + + return self + end + + --- Gets the Set. + -- @param #SET_OPSGROUP self + -- @return #SET_OPSGROUP self + function SET_OPSGROUP:GetAliveSet() + + local AliveSet = SET_OPSGROUP:New() + + -- Clean the Set before returning with only the alive Groups. + for GroupName, GroupObject in pairs(self.Set) do + local GroupObject=GroupObject --Wrapper.Group#GROUP + + if GroupObject and GroupObject:IsAlive() then + AliveSet:Add(GroupName, GroupObject) + end + end + + return AliveSet.Set or {} + end + + --- Adds a @{Core.Base#BASE} object in the @{Core.Set#SET_BASE}, using a given ObjectName as the index. + -- @param #SET_BASE self + -- @param #string ObjectName The name of the object. + -- @param Core.Base#BASE Object The object itself. + -- @return Core.Base#BASE The added BASE Object. + function SET_OPSGROUP:Add(ObjectName, Object) + self:I( { ObjectName = ObjectName, Object = Object } ) + + -- Ensure that the existing element is removed from the Set before a new one is inserted to the Set + if self.Set[ObjectName] then + self:Remove(ObjectName, true) + end + + local object=nil --Ops.OpsGroup#OPSGROUP + if Object:IsInstanceOf("GROUP") then + + --- + -- GROUP Object + --- + + -- Fist, look up in the DATABASE if an OPSGROUP already exists. + object=_DATABASE:FindOpsGroup(ObjectName) + + if not object then + + if Object:IsShip() then + object=NAVYGROUP:New(Object) + elseif Object:IsGround() then + object=ARMYGROUP:New(Object) + elseif Object:IsAir() then + object=FLIGHTGROUP:New(Object) + else + env.error("ERROR: Unknown category of group object!") + end + end + + elseif Object:IsInstanceOf("OPSGROUP") then + -- We already have an OPSGROUP. + object=Object + else + env.error("ERROR: Object must be a GROUP or OPSGROUP!") + end + + -- Add object to set. + self.Set[ObjectName]=object + + -- Add Object name to Index. + table.insert(self.Index, ObjectName) + + -- Trigger Added event. + self:Added(ObjectName, object) + end + + --- Add a GROUP or OPSGROUP object to the set. + -- **NOTE** that an OPSGROUP is automatically created from the GROUP if it does not exist already. + -- @param Core.Set#SET_OPSGROUP self + -- @param Wrapper.Group#GROUP group The GROUP which should be added to the set. Can also be given as an #OPSGROUP object. + -- @return Core.Set#SET_OPSGROUP self + function SET_OPSGROUP:AddGroup(group) + + local groupname=group:GetName() + + self:Add(groupname, group ) + + return self + end + + --- Add GROUP(s) or OPSGROUP(s) to the set. + -- @param Core.Set#SET_OPSGROUP self + -- @param #string AddGroupNames A single name or an array of GROUP names. + -- @return Core.Set#SET_OPSGROUP self + function SET_OPSGROUP:AddGroupsByName( AddGroupNames ) + + local AddGroupNamesArray = ( type( AddGroupNames ) == "table" ) and AddGroupNames or { AddGroupNames } + + for AddGroupID, AddGroupName in pairs( AddGroupNamesArray ) do + self:Add(AddGroupName, GROUP:FindByName(AddGroupName)) + end + + return self + end + + --- Remove GROUP(s) or OPSGROUP(s) from the set. + -- @param Core.Set#SET_OPSGROUP self + -- @param Wrapper.Group#GROUP RemoveGroupNames A single name or an array of GROUP names. + -- @return Core.Set#SET_OPSGROUP self + function SET_OPSGROUP:RemoveGroupsByName( RemoveGroupNames ) + + local RemoveGroupNamesArray = ( type( RemoveGroupNames ) == "table" ) and RemoveGroupNames or { RemoveGroupNames } + + for RemoveGroupID, RemoveGroupName in pairs( RemoveGroupNamesArray ) do + self:Remove( RemoveGroupName ) + end + + return self + end + + --- Finds an OPSGROUP based on the group name. + -- @param #SET_OPSGROUP self + -- @param #string GroupName Name of the group. + -- @return Ops.OpsGroup#OPSGROUP The found OPSGROUP (FLIGHTGROUP, ARMYGROUP or NAVYGROUP) or `#nil` if the group is not in the set. + function SET_OPSGROUP:FindGroup(GroupName) + local GroupFound = self.Set[GroupName] + return GroupFound + end + + --- Finds a FLIGHTGROUP based on the group name. + -- @param #SET_OPSGROUP self + -- @param #string GroupName Name of the group. + -- @return Ops.FlightGroup#FLIGHTGROUP The found FLIGHTGROUP or `#nil` if the group is not in the set. + function SET_OPSGROUP:FindFlightGroup(GroupName) + local GroupFound = self:FindGroup(GroupName) + return GroupFound + end + + --- Finds a ARMYGROUP based on the group name. + -- @param #SET_OPSGROUP self + -- @param #string GroupName Name of the group. + -- @return Ops.ArmyGroup#ARMYGROUP The found ARMYGROUP or `#nil` if the group is not in the set. + function SET_OPSGROUP:FindArmyGroup(GroupName) + local GroupFound = self:FindGroup(GroupName) + return GroupFound + end + + + --- Finds a NAVYGROUP based on the group name. + -- @param #SET_OPSGROUP self + -- @param #string GroupName Name of the group. + -- @return Ops.NavyGroup#NAVYGROUP The found NAVYGROUP or `#nil` if the group is not in the set. + function SET_OPSGROUP:FindNavyGroup(GroupName) + local GroupFound = self:FindGroup(GroupName) + return GroupFound + end + + --- Builds a set of groups of coalitions. + -- Possible current coalitions are red, blue and neutral. + -- @param #SET_OPSGROUP self + -- @param #string Coalitions Can take the following values: "red", "blue", "neutral" or combinations as a table, for example `{"red", "neutral"}`. + -- @return #SET_OPSGROUP self + function SET_OPSGROUP:FilterCoalitions(Coalitions) + + -- Create an empty set. + if not self.Filter.Coalitions then + self.Filter.Coalitions={} + end + + -- Ensure we got a table. + if type(Coalitions)~="table" then + Coalitions = {Coalitions} + end + + -- Set filter. + for CoalitionID, Coalition in pairs( Coalitions ) do + self.Filter.Coalitions[Coalition] = Coalition + end + + return self + end + + + --- Builds a set of groups out of categories. + -- + -- Possible current categories are: + -- + -- * "plane" for fixed wing groups + -- * "helicopter" for rotary wing groups + -- * "ground" for ground groups + -- * "ship" for naval groups + -- + -- @param #SET_OPSGROUP self + -- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship" or combinations as a table, for example `{"plane", "helicopter"}`. + -- @return #SET_OPSGROUP self + function SET_OPSGROUP:FilterCategories( Categories ) + + if 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 + + --- Builds a set of groups out of ground category. + -- @param #SET_OPSGROUP self + -- @return #SET_OPSGROUP self + function SET_OPSGROUP:FilterCategoryGround() + self:FilterCategories("ground") + return self + end + + --- Builds a set of groups out of airplane category. + -- @param #SET_OPSGROUP self + -- @return #SET_OPSGROUP self + function SET_OPSGROUP:FilterCategoryAirplane() + self:FilterCategories("plane") + return self + end + + --- Builds a set of groups out of aicraft category (planes and helicopters). + -- @param #SET_OPSGROUP self + -- @return #SET_OPSGROUP self + function SET_OPSGROUP:FilterCategoryAircraft() + self:FilterCategories({"plane", "helicopter"}) + return self + end + + --- Builds a set of groups out of helicopter category. + -- @param #SET_OPSGROUP self + -- @return #SET_OPSGROUP self + function SET_OPSGROUP:FilterCategoryHelicopter() + self:FilterCategories("helicopter") + return self + end + + --- Builds a set of groups out of ship category. + -- @param #SET_OPSGROUP self + -- @return #SET_OPSGROUP self + function SET_OPSGROUP:FilterCategoryShip() + self:FilterCategories("ship") + return self + end + + --- Builds a set of groups of defined countries. + -- @param #SET_OPSGROUP self + -- @param #string Countries Can take those country strings known within DCS world. + -- @return #SET_OPSGROUP self + function SET_OPSGROUP:FilterCountries(Countries) + + -- Create empty table if necessary. + if not self.Filter.Countries then + self.Filter.Countries = {} + end + + -- Ensure input is a table. + if type(Countries)~="table" then + Countries={Countries} + end + + -- Set filter. + for CountryID, Country in pairs( Countries ) do + self.Filter.Countries[Country] = Country + end + + return self + end + + + --- Builds a set of groups that contain the given string in their group name. + -- **Attention!** Bad naming convention as this **does not** filter only **prefixes** but all groups that **contain** the string. + -- @param #SET_OPSGROUP self + -- @param #string Prefixes The string pattern(s) that needs to be contained in the group name. Can also be passed as a `#table` of strings. + -- @return #SET_OPSGROUP self + function SET_OPSGROUP:FilterPrefixes(Prefixes) + + -- Create emtpy table if necessary. + if not self.Filter.GroupPrefixes then + self.Filter.GroupPrefixes={} + end + + -- Ensure we have a table. + if type(Prefixes)~="table" then + Prefixes={Prefixes} + end + + -- Set group prefixes. + for PrefixID, Prefix in pairs(Prefixes) do + self.Filter.GroupPrefixes[Prefix]=Prefix + end + + return self + end + + --- Builds a set of groups that are only active. + -- Only the groups that are active will be included within the set. + -- @param #SET_OPSGROUP self + -- @param #boolean Active (optional) Include only active groups to the set. + -- Include inactive groups if you provide false. + -- @return #SET_OPSGROUP self + -- @usage + -- + -- -- Include only active groups to the set. + -- GroupSet = SET_OPSGROUP:New():FilterActive():FilterStart() + -- + -- -- Include only active groups to the set of the blue coalition, and filter one time. + -- GroupSet = SET_OPSGROUP:New():FilterActive():FilterCoalition( "blue" ):FilterOnce() + -- + -- -- Include only active groups to the set of the blue coalition, and filter one time. + -- -- Later, reset to include back inactive groups to the set. + -- GroupSet = SET_OPSGROUP:New():FilterActive():FilterCoalition( "blue" ):FilterOnce() + -- ... logic ... + -- GroupSet = SET_OPSGROUP:New():FilterActive( false ):FilterCoalition( "blue" ):FilterOnce() + -- + function SET_OPSGROUP:FilterActive( Active ) + Active = Active or not ( Active == false ) + self.Filter.Active = Active + return self + end + + + --- Starts the filtering. + -- @param #SET_OPSGROUP self + -- @return #SET_OPSGROUP self + function SET_OPSGROUP:FilterStart() + + if _DATABASE then + self:_FilterStart() + self:HandleEvent( EVENTS.Birth, self._EventOnBirth ) + self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) + self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) + self:HandleEvent( EVENTS.RemoveUnit, self._EventOnDeadOrCrash ) + end + + return self + end + + --- Handles the OnDead or OnCrash event for alive groups set. + -- Note: The GROUP object in the SET_OPSGROUP collection will only be removed if the last unit is destroyed of the GROUP. + -- @param #SET_OPSGROUP self + -- @param Core.Event#EVENTDATA Event + function SET_OPSGROUP:_EventOnDeadOrCrash( Event ) + self:F( { Event } ) + + if Event.IniDCSUnit then + local ObjectName, Object = self:FindInDatabase( Event ) + if ObjectName then + if Event.IniDCSGroup:getSize() == 1 then -- Only remove if the last unit of the group was destroyed. + self:Remove( ObjectName ) + end + end + end + end + + --- Handles the Database to check on an event (birth) that the Object was added in the Database. + -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! + -- @param #SET_OPSGROUP self + -- @param Core.Event#EVENTDATA Event + -- @return #string The name of the GROUP + -- @return #table The GROUP + function SET_OPSGROUP:AddInDatabase( Event ) + + if Event.IniObjectCategory==1 then + + if not self.Database[Event.IniDCSGroupName] then + self.Database[Event.IniDCSGroupName] = GROUP:Register( Event.IniDCSGroupName ) + end + + end + + return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName] + end + + --- Handles the Database to check on any event that Object exists in the Database. + -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! + -- @param #SET_OPSGROUP self + -- @param Core.Event#EVENTDATA Event Event data table. + -- @return #string The name of the GROUP + -- @return #table The GROUP + function SET_OPSGROUP:FindInDatabase(Event) + return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName] + end + + --- Iterate the set and call an iterator function for each OPSGROUP object. + -- @param #SET_OPSGROUP self + -- @param #function IteratorFunction The function that will be called for all OPSGROUPs in the set. **NOTE** that the function must have the OPSGROUP as first parameter! + -- @param ... (Optional) arguments passed to the `IteratorFunction`. + -- @return #SET_OPSGROUP self + function SET_OPSGROUP:ForEachGroup( IteratorFunction, ... ) + + self:ForEach(IteratorFunction, arg, self:GetSet()) + + return self + end + + --- Check include object. + -- @param #SET_OPSGROUP self + -- @param Wrapper.Group#GROUP MGroup The group that is checked for inclusion. + -- @return #SET_OPSGROUP self + function SET_OPSGROUP:IsIncludeObject(MGroup) + + -- Assume it is and check later if not. + local MGroupInclude=true + + -- Filter active. + if self.Filter.Active~=nil then + + local MGroupActive = false + + if self.Filter.Active==false or (self.Filter.Active==true and MGroup:IsActive()==true) then + MGroupActive = true + end + + MGroupInclude = MGroupInclude and MGroupActive + end + + -- Filter coalitions. + if self.Filter.Coalitions then + + local MGroupCoalition = false + + for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do + if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName]==MGroup:GetCoalition() then + MGroupCoalition = true + end + end + + MGroupInclude = MGroupInclude and MGroupCoalition + end + + -- Filter categories. + if self.Filter.Categories then + + local MGroupCategory = false + + for CategoryID, CategoryName in pairs( self.Filter.Categories ) do + if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName]==MGroup:GetCategory() then + MGroupCategory = true + end + end + + MGroupInclude = MGroupInclude and MGroupCategory + end + + -- Filter countries. + if self.Filter.Countries then + local MGroupCountry = false + for CountryID, CountryName in pairs( self.Filter.Countries ) do + if country.id[CountryName] == MGroup:GetCountry() then + MGroupCountry = true + end + end + MGroupInclude = MGroupInclude and MGroupCountry + end + + -- Filter "prefixes". + if self.Filter.GroupPrefixes then + + local MGroupPrefix = false + + for GroupPrefixId, GroupPrefix in pairs( self.Filter.GroupPrefixes ) do + if string.find( MGroup:GetName(), GroupPrefix:gsub ("-", "%%-"), 1 ) then --Not sure why "-" is replaced by "%-" ?! + MGroupPrefix = true + end + end + + MGroupInclude = MGroupInclude and MGroupPrefix + end + + return MGroupInclude + end + +end diff --git a/Moose Development/Moose/Ops/ArmyGroup.lua b/Moose Development/Moose/Ops/ArmyGroup.lua index 7660edc0f..fc82983ac 100644 --- a/Moose Development/Moose/Ops/ArmyGroup.lua +++ b/Moose Development/Moose/Ops/ArmyGroup.lua @@ -1086,15 +1086,10 @@ end -- @return Ops.OpsGroup#OPSGROUP.Waypoint Waypoint table. function ARMYGROUP:AddWaypoint(Coordinate, Speed, AfterWaypointWithID, Formation, Updateroute) - env.info("FF Current waypoint index="..self.currentwp) - env.info("FF Add waypoint after index="..(AfterWaypointWithID or "nil")) - local coordinate=self:_CoordinateFromObject(Coordinate) -- Set waypoint index. local wpnumber=self:GetWaypointIndexAfterID(AfterWaypointWithID) - - env.info("FF Add waypoint index="..wpnumber) -- Check if final waypoint is still passed. if wpnumber>self.currentwp then diff --git a/Moose Development/Moose/Ops/FlightGroup.lua b/Moose Development/Moose/Ops/FlightGroup.lua index 8472b6ad0..ad8146281 100644 --- a/Moose Development/Moose/Ops/FlightGroup.lua +++ b/Moose Development/Moose/Ops/FlightGroup.lua @@ -1682,13 +1682,11 @@ function FLIGHTGROUP:onafterAirborne(From, Event, To) if self.isAI then if self:IsTransporting() then if self.cargoTransport and self.cargoTransport.deployzone and self.cargoTransport.deployzone:IsInstanceOf("ZONE_AIRBASE") then - env.info("FF transporting land at airbase ") local airbase=self.cargoTransport.deployzone:GetAirbase() self:LandAtAirbase(airbase) end elseif self:IsPickingup() then if self.cargoTransport and self.cargoTransport.pickupzone and self.cargoTransport.pickupzone:IsInstanceOf("ZONE_AIRBASE") then - env.info("FF pickingup land at airbase ") local airbase=self.cargoTransport.pickupzone:GetAirbase() self:LandAtAirbase(airbase) end @@ -2959,6 +2957,9 @@ function FLIGHTGROUP:_InitGroup() text=text..string.format("Start Rwy = %s\n", tostring(self:IsTakeoffRunway())) self:I(self.lid..text) end + + env.info("DCS Unit BOOM_AND_RECEPTACLE="..tostring(Unit.RefuelingSystem.BOOM_AND_RECEPTACLE)) + env.info("DCS Unit PROBE_AND_DROGUE="..tostring(Unit.RefuelingSystem.PROBE_AND_DROGUE)) -- Init done. self.groupinitialized=true diff --git a/Moose Development/Moose/Ops/NavyGroup.lua b/Moose Development/Moose/Ops/NavyGroup.lua index 8c78e0ede..4f233faac 100644 --- a/Moose Development/Moose/Ops/NavyGroup.lua +++ b/Moose Development/Moose/Ops/NavyGroup.lua @@ -498,7 +498,8 @@ function NAVYGROUP:onafterStatus(From, Event, To) -- Check if group started or stopped turning. self:_CheckTurning() - local freepath=UTILS.NMToMeters(10) + local disttoWP=math.min(self:GetDistanceToWaypoint(), UTILS.NMToMeters(10)) + local freepath=disttoWP -- Only check if not currently turning. if not self:IsTurning() then @@ -506,7 +507,7 @@ function NAVYGROUP:onafterStatus(From, Event, To) -- Check free path ahead. freepath=self:_CheckFreePath(freepath, 100) - if freepath<5000 then + if disttoWP>1 and freepath= 5 meters. + if dist<5 then + return + end + local boxwidth=dist*2 local spacex=dist*0.1 local delta=dist/10 -- Create a grid of nodes. We only want nodes of surface type water. - astar:CreateGrid({land.SurfaceType.WATER}, boxwidth, spacex, delta, delta*2, self.Debug) + astar:CreateGrid({land.SurfaceType.WATER}, boxwidth, spacex, delta, delta, self.verbose>10) -- Valid neighbour nodes need to have line of sight. astar:SetValidNeighbourLoS(self.pathCorridor) @@ -1539,7 +1550,9 @@ function NAVYGROUP:_FindPathToNextWaypoint() uid=wp.uid -- Debug: smoke and mark path. - --node.coordinate:MarkToAll(string.format("Path node #%d", i)) + if self.verbose>=10 then + node.coordinate:MarkToAll(string.format("Path node #%d", i)) + end end diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index 20f482f2b..047912831 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -108,6 +108,7 @@ -- -- @field #OPSGROUP.Element carrier Carrier the group is loaded into as cargo. -- @field #OPSGROUP carrierGroup Carrier group transporting this group as cargo. +-- @field #OPSGROUP.MyCarrier mycarrier Carrier group for this group. -- @field #table cargoqueue Table containing cargo groups to be transported. -- @field #table cargoBay Table containing OPSGROUP loaded into this group. -- @field Ops.OpsTransport#OPSTRANSPORT cargoTransport Current cargo transport assignment. @@ -172,6 +173,7 @@ OPSGROUP = { cargoqueue = {}, cargoBay = {}, cargocounter = 1, + mycarrier = {}, } @@ -418,25 +420,18 @@ OPSGROUP.TransportStatus={ } --- Cargo transport data. --- @type OPSGROUP.CargoTransport --- @field #table cargos Cargos. Each element is a @{#OPSGROUP.Cargo}. --- @field #string status Status of the transport. See @{#OPSGROUP.TransportStatus}. --- @field #number prio Priority of this transport. Should be a number between 0 (high prio) and 100 (low prio). --- @field #number importance Importance of this transport. Smaller=higher. --- @field #number Tstart Start time in *abs.* seconds. --- @field Core.Zone#ZONE pickupzone Zone where the cargo is picked up. --- @field Core.Zone#ZONE deployzone Zone where the cargo is dropped off. --- @field Core.Zone#ZONE embarkzone (Optional) Zone where the cargo is supposed to embark. Default is the pickup zone. --- @field Core.Zone#ZONE disembarkzone (Optional) Zone where the cargo is disembarked. Default is the deploy zone. --- @field #OPSGROUP carrierGroup The new carrier group. +-- @type OPSGROUP.MyCarrier +-- @field #OPSGROUP group The carrier group. +-- @field #OPSGROUP.Element element The carrier element. +-- @field #boolean reserved If `true`, the carrier has caro space reserved for me. --- Cargo group data. -- @type OPSGROUP.CargoGroup -- @field #OPSGROUP opsgroup The cargo opsgroup. --- @field #string status Status of the cargo group. See @{#OPSGROUP.CargoStatus}. --- @field Core.Zone#ZONE pickupzone Zone where the cargo is picked up. --- @field Core.Zone#ZONE deployzone Zone where the cargo is dropped off. --- @field #boolean delivered If true, group was delivered. +-- @field #boolean delivered If `true`, group was delivered. +-- @field #OPSGROUP disembarkCarrierGroup Carrier group where the cargo group is directly loaded to. +-- @field #OPSGROUP disembarkCarrierElement Carrier element to which the cargo group is directly loaded to. +-- @field #string status Status of the cargo group. Not used yet. --- OpsGroup version. -- @field #string version @@ -1022,8 +1017,9 @@ function OPSGROUP:GetVec3(UnitName) local vec3=nil --DCS#Vec3 -- First check if this group is loaded into a carrier - if self.carrier and self.carrier.status~=OPSGROUP.ElementStatus.DEAD and self:IsLoaded() then - local unit=self.carrier.unit + local carrier=self:_GetMyCarrierElement() + if carrier and carrier.status~=OPSGROUP.ElementStatus.DEAD and self:IsLoaded() then + local unit=carrier.unit if unit and unit:IsAlive()~=nil then vec3=unit:GetVec3() return vec3 @@ -1219,8 +1215,10 @@ end -- @return #OPSGROUP self function OPSGROUP:DespawnUnit(UnitName, Delay, NoEventRemoveUnit) - env.info("FF despawn element .."..tostring(UnitName)) + -- Debug info. + self:T(self.lid.."Despawn element "..tostring(UnitName)) + -- Get element. local element=self:GetElementByName(UnitName) if element then @@ -1652,7 +1650,8 @@ end -- @return #boolean If true, group is boarding. function OPSGROUP:IsBoarding(CarrierGroupName) if CarrierGroupName then - if self.carrierGroup and self.carrierGroup.groupname~=CarrierGroupName then + local carrierGroup=self:_GetMyCarrierGroup() + if carrierGroup and carrierGroup.groupname~=CarrierGroupName then return false end end @@ -1665,7 +1664,8 @@ end -- @return #boolean If true, group is loaded. function OPSGROUP:IsLoaded(CarrierGroupName) if CarrierGroupName then - if self.carrierGroup and self.carrierGroup.groupname~=CarrierGroupName then + local carrierGroup=self:_GetMyCarrierGroup() + if carrierGroup and carrierGroup.groupname~=CarrierGroupName then return false end end @@ -3007,7 +3007,6 @@ end -- @param #OPSGROUP self -- @return #number Number of unfinished transports in the queue. function OPSGROUP:CountRemainingTransports() - env.info("FF Count remaining transports="..#self.cargoqueue) local N=0 @@ -3015,17 +3014,15 @@ function OPSGROUP:CountRemainingTransports() for _,_transport in pairs(self.cargoqueue) do local transport=_transport --Ops.OpsTransport#OPSTRANSPORT - self:I(self.lid..string.format("Transport status=%s [%s]", transport:GetCarrierTransportStatus(self), transport:GetState())) + -- Debug info. + self:T(self.lid..string.format("Transport status=%s [%s]", transport:GetCarrierTransportStatus(self), transport:GetState())) -- Count not delivered (executing or scheduled) assignments. if transport and transport:GetCarrierTransportStatus(self)==OPSTRANSPORT.Status.SCHEDULED and transport:GetState()~=OPSTRANSPORT.Status.DELIVERED then - - N=N+1 - + N=N+1 end end - env.info("FF Count remaining transports="..N) return N end @@ -4114,8 +4111,6 @@ end -- @param #string To To state. function OPSGROUP:onafterLaserLostLOS(From, Event, To) - --env.info("FF lost LOS") - -- No of sight. self.spot.LOS=false @@ -4123,8 +4118,6 @@ function OPSGROUP:onafterLaserLostLOS(From, Event, To) self.spot.lostLOS=true if self.spot.On then - - --env.info("FF lost LOS ==> pause laser") -- Switch laser off. self:LaserPause() @@ -4142,19 +4135,14 @@ function OPSGROUP:onafterLaserGotLOS(From, Event, To) -- Has line of sight. self.spot.LOS=true - - --env.info("FF Laser Got LOS") if self.spot.lostLOS then -- Did not loose LOS anymore. self.spot.lostLOS=false - - --env.info("FF had lost LOS and regained it") -- Resume laser if currently paused. if self.spot.Paused then - --env.info("FF laser was paused ==> resume") self:LaserResume() end @@ -4205,8 +4193,6 @@ function OPSGROUP:SetLaserTarget(Target) else self.spot.offsetTarget={x=0, 2, z=0} end - - --env.info(string.format("Target offset %.3f", y)) else self:E("WARNING: LASER target is not alive!") @@ -4310,8 +4296,6 @@ function OPSGROUP:_UpdateLaser() -- Check current LOS. local los=self:HasLoS(self.spot.Coordinate, self.spot.element, self.spot.offset) - --env.info(string.format("FF check LOS current=%s previous=%s", tostring(los), tostring(self.spot.LOS))) - if los then -- Got LOS if self.spot.lostLOS then @@ -4425,8 +4409,13 @@ function OPSGROUP:onafterElementDead(From, Event, To, Element) local opsgroup=_DATABASE:GetOpsGroup(groupname) if opsgroup and not (opsgroup:IsDead() or opsgroup:IsStopped()) then for _,element in pairs(opsgroup.elements) do - env.info("FF cargo element dead "..element.name) + + -- Debug info. + self:T2(self.lid.."Cargo element dead "..element.name) + + -- Trigger dead event. opsgroup:ElementDead(element) + end end end @@ -4662,9 +4651,10 @@ function OPSGROUP:_CheckCargoTransport() local state=cargo.opsgroup:GetState() local status=cargo.opsgroup.cargoStatus local name=cargo.opsgroup.groupname - local carriergroup=cargo.opsgroup.carrierGroup and cargo.opsgroup.carrierGroup.groupname or "N/A" - local carrierelement=cargo.opsgroup.carrier and cargo.opsgroup.carrier.name or "N/A" - text=text..string.format("\n (%d) %s [%s]: %s, carrier=%s(%s), delivered=%s", j, name, state, status, carriergroup, carrierelement, tostring(cargo.delivered)) + local carriergroup, carrierelement, reserved=cargo.opsgroup:_GetMyCarrier() + local carrierGroupname=carriergroup and carriergroup.groupname or "none" + local carrierElementname=carrierelement and carrierelement.name or "none" + text=text..string.format("\n (%d) %s [%s]: %s, carrier=%s(%s), delivered=%s", j, name, state, status, carrierGroupname, carrierElementname, tostring(cargo.delivered)) end end if text~="" then @@ -4689,9 +4679,10 @@ function OPSGROUP:_CheckCargoTransport() local gstatus=cargo.opsgroup:GetState() local cstatus=cargo.opsgroup.cargoStatus local weight=cargo.opsgroup:GetWeightTotal() - local carriername=cargo.opsgroup.carrier and cargo.opsgroup.carrier.name or "none" - local carriergroup=cargo.opsgroup.carrierGroup and cargo.opsgroup.carrierGroup.groupname or "none" - text=text..string.format("\n- %s (%.1f kg) [%s]: %s, carrier=%s (%s), delivered=%s", name, weight, gstatus, cstatus, carriername, carriergroup, tostring(cargo.delivered)) + local carriergroup, carrierelement, reserved=cargo.opsgroup:_GetMyCarrier() + local carrierGroupname=carriergroup and carriergroup.groupname or "none" + local carrierElementname=carrierelement and carrierelement.name or "none" + text=text..string.format("\n- %s (%.1f kg) [%s]: %s, carrier=%s (%s), delivered=%s", name, weight, gstatus, cstatus, carrierElementname, carrierGroupname, tostring(cargo.delivered)) end self:I(self.lid..text) end @@ -4761,7 +4752,10 @@ function OPSGROUP:_CheckCargoTransport() for _,_cargo in pairs(self.cargoTransport.cargos) do local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup - if (cargo.opsgroup.carrierGroup and cargo.opsgroup.carrierGroup:GetName()==self:GetName()) and not cargo.delivered then + local carrierGroup=cargo.opsgroup:_GetMyCarrierGroup() + + -- Check that this group is + if (carrierGroup and carrierGroup:GetName()==self:GetName()) and not cargo.delivered then delivered=false break end @@ -4780,15 +4774,6 @@ function OPSGROUP:_CheckCargoTransport() end - -- Loop over cargo queue and check if everything was delivered. - for i=#self.cargoqueue,1,-1 do - local transport=self.cargoqueue[i] --Ops.OpsTransport#OPSTRANSPORT - local delivered=self:_CheckDelivered(transport) - if delivered then - --self:Delivered(transport) - end - end - return self end @@ -4825,8 +4810,14 @@ function OPSGROUP:_DelCargobay(CargoGroup, CarrierElement) self.cargoBay[CargoGroup.groupname]=nil -- Reduce carrier weight. - local weight=CargoGroup:GetWeightTotal() - self:RedWeightCargo(CargoGroup.carrier.name, weight) + local weight=CargoGroup:GetWeightTotal() + + -- Get carrier of group. + local carrier=CargoGroup:_GetMyCarrierElement() + + if carrier then + self:RedWeightCargo(carrier.name, weight) + end return true end @@ -4909,36 +4900,6 @@ function OPSGROUP:_CheckDelivered(CargoTransport) return done end ---- Create a cargo transport assignment. --- @param #OPSGROUP self --- @param Core.Set#SET_GROUP GroupSet Set of groups to be transported. Can also be a single @{Wrapper.Group#GROUP} object. --- @param Core.Zone#ZONE Pickupzone Pickup zone. This is the zone, where the carrier is going to pickup the cargo. **Important**: only cargo is considered, if it is in this zone when the carrier starts loading! --- @param Core.Zone#ZONE Deployzone Deploy zone. This is the zone, where the carrier is going to drop off the cargo. --- @param #number Prio Priority of this transport assignment. Should be a number between 1 (high prio) and 100 (low prio). --- @param #number Importance Importance of this transport assignment (lower=more important). A transport is only considered, if all more important (if any) transports are done. Default `nil`. --- @param #string ClockStart Start time in format "HH:MM(:SS)(+D)", e.g. "13:05:30" or "08:30+1". Can also be given as a `#number`, in which case it is interpreted as relative amount in seconds from now on. --- @param Core.Zone#ZONE Embarkzone (Optional) Zone where the cargo is going to be embarked into the transport. By default is goes to the assigned carrier unit. --- @param Core.Zone#ZONE Disembarkzone (Optional) Zone where the cargo disembarks to (is spawned after unloaded). Default is anywhere in the deploy zone. --- @param #OPSGROUP DisembarkCarrierGroup (Optional) The OPSGROUP where the cargo is directly loaded into. --- @return Ops.OpsTransport#OPSTRANSPORT Cargo transport. -function OPSGROUP:AddCargoTransport(GroupSet, Pickupzone, Deployzone, Prio, Importance, ClockStart, Embarkzone, Disembarkzone, DisembarkCarrierGroup) - - -- Create a new cargo transport assignment. - local cargotransport=OPSTRANSPORT:New(GroupSet, Pickupzone, Deployzone) - - if cargotransport then - - -- Set state to SCHEDULED. - cargotransport.status=OPSGROUP.TransportStatus.SCHEDULED - - --Add to cargo queue - table.insert(self.cargoqueue, cargotransport) - - end - - return cargotransport -end - --- Create a cargo transport assignment. -- @param #OPSGROUP self -- @param Ops.OpsTransport#OPSTRANSPORT OpsTransport The troop transport assignment. @@ -4971,134 +4932,6 @@ function OPSGROUP:DelCargoTransport(CargoTransport) return self end ---- Create a cargo transport assignment. --- @param #OPSGROUP self --- @param Core.Set#SET_GROUP GroupSet Set of groups to be transported. Can also be a single @{Wrapper.Group#GROUP} or @{Ops.OpsGroup#OPSGROUP} object. --- @param Core.Zone#ZONE Pickupzone Pickup zone. This is the zone, where the carrier is going to pickup the cargo. **Important**: only cargo is considered, if it is in this zone when the carrier starts loading! --- @param Core.Zone#ZONE Deployzone Deploy zone. This is the zone, where the carrier is going to drop off the cargo. --- @param #number Prio Priority of this transport assignment. Should be a number between 1 (high prio) and 100 (low prio). --- @param #number Importance Importance of this transport assignment (lower=more important). A transport is only considered, if all more important (if any) transports are done. Default `nil`. --- @param #string ClockStart Start time in format "HH:MM:SS+D", e.g. "13:05:30" or "08:30+1". Can also be passed as a `#number` in which case it is interpreted as relative amount in seconds from now on. --- @param Core.Zone#ZONE Embarkzone (Optional) Zone where the cargo is going to be embarked into the transport. By default is goes to the assigned carrier unit. --- @param Core.Zone#ZONE Disembarkzone (Optional) Zone where the cargo disembarks to (is spawned after unloaded). Default is anywhere in the deploy zone. --- @param #OPSGROUP DisembarkCarrierGroup (Optional) The OPSGROUP where the cargo is directly loaded into. --- @return Ops.OpsTransport#OPSTRANSPORT Cargo transport. -function OPSGROUP:CreateCargoTransport(GroupSet, Pickupzone, Deployzone, Prio, Importance, ClockStart, Embarkzone, Disembarkzone, DisembarkCarrierGroup) - - -- Current mission time. - local Tnow=timer.getAbsTime() - - -- Set start time. Default in 5 sec. - local Tstart=Tnow+5 - if ClockStart and type(ClockStart)=="number" then - Tstart=Tnow+ClockStart - elseif ClockStart and type(ClockStart)=="string" then - Tstart=UTILS.ClockToSeconds(ClockStart) - end - - -- Data structure. - local transport={} --Ops.OpsTransport#OPSTRANSPORT - transport.uid=self.cargocounter - transport.status=OPSGROUP.TransportStatus.PLANNING - transport.pickupzone=Pickupzone - transport.deployzone=Deployzone - transport.embarkzone=Embarkzone or Pickupzone - transport.disembarkzone=Disembarkzone or Deployzone - transport.prio=Prio or 50 - transport.importance=Importance - transport.Tstart=Tstart - transport.carrierGroup=DisembarkCarrierGroup - transport.cargos={} - - -- Check type of GroupSet provided. - if GroupSet:IsInstanceOf("GROUP") or GroupSet:IsInstanceOf("OPSGROUP") then - - -- We got a single GROUP or OPSGROUP objectg. - local cargo=self:CreateCargoGroupData(GroupSet, Pickupzone, Deployzone) - - if cargo and self:CanCargo(cargo.opsgroup) then - table.insert(transport.cargos, cargo) - end - - else - - -- We got a SET_GROUP object. - - for _,group in pairs(GroupSet.Set) do - - local cargo=self:CreateCargoGroupData(group, Pickupzone, Deployzone) - - if cargo and self:CanCargo(cargo.opsgroup) then - table.insert(transport.cargos, cargo) - end - - end - end - - -- Debug info. - if self.verbose>=0 then - local text=string.format("Created Cargo Transport (UID=%d) from %s(%s) --> %s(%s)", - transport.uid, transport.pickupzone:GetName(), transport.embarkzone:GetName(), transport.deployzone:GetName(), transport.disembarkzone:GetName()) - local Weight=0 - for _,_cargo in pairs(transport.cargos) do - local cargo=_cargo --#OPSGROUP.CargoGroup - local weight=cargo.opsgroup:GetWeightTotal() - Weight=Weight+weight - text=text..string.format("\n- %s [%s] weight=%.1f kg", cargo.opsgroup:GetName(), cargo.opsgroup:GetState(), weight) - end - text=text..string.format("\nTOTAL: Ncargo=%d, Weight=%.1f kg", #transport.cargos, Weight) - self:I(self.lid..text) - end - - if #transport.cargos>0 then - self.cargocounter=self.cargocounter+1 - return transport - else - self:E(self.lid.."ERROR: Cargo too heavy for this carrier group!") - return nil - end -end - ---- Create a cargo group data structure. --- @param #OPSGROUP self --- @param Wrapper.Group#GROUP group The GROUP object. --- @param Core.Zone#ZONE Pickupzone Pickup zone. --- @param Core.Zone#ZONE Deployzone Deploy zone. --- @return #OPSGROUP.CargoGroup Cargo group data. -function OPSGROUP:CreateCargoGroupData(group, Pickupzone, Deployzone) - - local opsgroup=nil - - if group:IsInstanceOf("OPSGROUP") then - opsgroup=group - else - - opsgroup=_DATABASE:GetOpsGroup(group) - - if not opsgroup then - if group:IsAir() then - opsgroup=FLIGHTGROUP:New(group) - elseif group:IsShip() then - opsgroup=NAVYGROUP:New(group) - else - opsgroup=ARMYGROUP:New(group) - end - else - --env.info("FF found opsgroup in createcargo") - end - - end - - local cargo={} --#OPSGROUP.CargoGroup - - cargo.opsgroup=opsgroup - cargo.delivered=false - cargo.status="Unknown" - cargo.pickupzone=Pickupzone - cargo.deployzone=Deployzone - - return cargo -end --- Get total weight of the group including cargo. -- @param #OPSGROUP self @@ -5124,7 +4957,7 @@ end --- Get free cargo bay weight. -- @param #OPSGROUP self -- @param #string UnitName Name of the unit. Default is of the whole group. --- @return #number Total weight in kg. +-- @return #number Free cargo bay in kg. function OPSGROUP:GetFreeCargobay(UnitName) local Free=0 @@ -5133,10 +4966,17 @@ function OPSGROUP:GetFreeCargobay(UnitName) if (UnitName==nil or UnitName==element.name) and element.status~=OPSGROUP.ElementStatus.DEAD then local free=element.weightMaxCargo-element.weightCargo + --[[ + for _,_opsgroup in pairs(element.reservedCargos or {}) do + local opsgroup=_opsgroup --#OPSGROUP + free=free-opsgroup:GetWeightTotal() + end + ]] Free=Free+free end end + self:I(self.lid..string.format("Free cargo bay=%d kg (unit=%s)", Free, (UnitName or "whole group"))) return Free end @@ -5188,6 +5028,21 @@ function OPSGROUP:GetWeightCargo(UnitName) end end + + local gewicht=0 + for groupname, carriername in pairs(self.cargoBay) do + local element=self:GetElementByName(carriername) + if (UnitName==nil or UnitName==carriername) and (element and element.status~=OPSGROUP.ElementStatus.DEAD) then + local opsgroup=_DATABASE:FindOpsGroup(groupname) + if opsgroup then + gewicht=gewicht+opsgroup:GetWeightTotal() + end + end + end + + if gewicht~=weight then + self:I(self.lid..string.format("ERROR: FF weight!=gewicht: weight=%.1f, gewicht=%.1f", weight, gewicht)) + end return weight end @@ -5247,7 +5102,7 @@ function OPSGROUP:RedWeightCargo(UnitName, Weight) return self end ---- Check if the group can be carrier of a cargo group. +--- Check if the group can *in principle* be carrier of a cargo group. This checks the max cargo capacity of the group but *not* how much cargo is already loaded (if any). -- **Note** that the cargo group *cannot* be split into units, i.e. the largest cargo bay of any element of the group must be able to load the whole cargo group in one piece. -- @param #OPSGROUP self -- @param #OPSGROUP CargoGroup Cargo group, which needs a carrier. @@ -5258,11 +5113,14 @@ function OPSGROUP:CanCargo(CargoGroup) local weight=CargoGroup:GetWeightTotal() - for _,element in pairs(self.elements) do - local can=element.weightMaxCargo>=weight - if can then + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + + -- Check that element is not dead and has + if element and element.status~=OPSGROUP.ElementStatus.DEAD and element.weightMaxCargo>=weight then return true end + end end @@ -5278,16 +5136,104 @@ function OPSGROUP:FindCarrierForCargo(CargoGroup) local weight=CargoGroup:GetWeightTotal() - for _,element in pairs(self.elements) do + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + local free=self:GetFreeCargobay(element.name) + if free>=weight then return element + else + self:T3(self.lid..string.format("%s: Weight %d>%d free cargo bay", element.name, weight, free)) end + end return nil end +--- Reserve cargo space for a cargo group. +-- @param #OPSGROUP self +-- @param #OPSGROUP CargoGroup Cargo group, which needs a carrier. +-- @return #OPSGROUP.Element Carrier able to transport the cargo. +function OPSGROUP:ReserveCargoSpace(CargoGroup) + + local element=self:FindCarrierForCargo(CargoGroup) + + if element then + element.reservedCargo=element.reservedCargo or {} + table.insert(element.reservedCargo, CargoGroup) + end + + return nil +end + +--- Set my carrier. +-- @param #OPSGROUP self +-- @param #OPSGROUP CarrierGroup Carrier group. +-- @param #OPSGROUP.Element CarrierElement Carrier element. +-- @param #boolean Reserved If `true`, reserve space for me. +function OPSGROUP:_SetMyCarrier(CarrierGroup, CarrierElement, Reserved) + + self.mycarrier.group=CarrierGroup + self.mycarrier.element=CarrierElement + self.mycarrier.reserved=Reserved + +end + +--- Get my carrier group. +-- @param #OPSGROUP self +-- @return #OPSGROUP Carrier group. +function OPSGROUP:_GetMyCarrierGroup() + if self.mycarrier and self.mycarrier.group then + return self.mycarrier.group + end + return nil +end + +--- Get my carrier element. +-- @param #OPSGROUP self +-- @return #OPSGROUP.Element Carrier element. +function OPSGROUP:_GetMyCarrierElement() + if self.mycarrier and self.mycarrier.element then + return self.mycarrier.element + end + return nil +end + +--- Is my carrier reserved. +-- @param #OPSGROUP self +-- @return #boolean If `true`, space for me was reserved. +function OPSGROUP:_IsMyCarrierReserved() + if self.mycarrier then + return self.mycarrier.reserved + end + return nil +end + + + +--- Get my carrier. +-- @param #OPSGROUP self +-- @return #OPSGROUP Carrier group. +-- @return #OPSGROUP.Element Carrier element. +-- @return #boolean If `true`, space is reserved for me +function OPSGROUP:_GetMyCarrier() + return self.mycarrier.group, self.mycarrier.element, self.mycarrier.reserved +end + + +--- Remove my carrier. +-- @param #OPSGROUP self +-- @return #OPSGROUP self +function OPSGROUP:_RemoveMyCarrier() + self.mycarrier.group=nil + self.mycarrier.element=nil + self.mycarrier.reserved=nil + self.mycarrier={} + return self +end + --- On after "Pickup" event. -- @param #OPSGROUP self -- @param #string From From state. @@ -5403,14 +5349,6 @@ function OPSGROUP:onafterPickup(From, Event, To) end ---- On before "Loading" event. --- @param #OPSGROUP self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. -function OPSGROUP:onbeforeLoading(From, Event, To) - -end --- On after "Loading" event. -- @param #OPSGROUP self @@ -5485,12 +5423,12 @@ function OPSGROUP:onafterLoading(From, Event, To) else -- Debug info. - env.info("FF cannot board carrier") + self:T(self.lid.."Cannot board carrier!") end else -- Debug info. - env.info("FF cargo NOT in embark zone "..self.cargoTransport.embarkzone:GetName()) + self:T(self.lid.."Cargo NOT in embark zone "..self.cargoTransport.embarkzone:GetName()) end end @@ -5526,7 +5464,7 @@ function OPSGROUP:onafterLoad(From, Event, To, CargoGroup, Carrier) self:I(self.lid..string.format("Loading group %s", tostring(CargoGroup.groupname))) -- Carrier element. - local carrier=Carrier or CargoGroup.carrier --#OPSGROUP.Element + local carrier=Carrier or CargoGroup:_GetMyCarrierElement() --#OPSGROUP.Element -- No carrier provided. if not carrier then @@ -5553,8 +5491,7 @@ function OPSGROUP:onafterLoad(From, Event, To, CargoGroup, Carrier) CargoGroup:ClearWaypoints() -- Set carrier (again). - CargoGroup.carrier=carrier - CargoGroup.carrierGroup=self + CargoGroup:_SetMyCarrier(self, carrier, false) -- Despawn this group. if CargoGroup:IsAlive() then @@ -5576,7 +5513,9 @@ end -- @param #string Event Event. -- @param #string To To state. function OPSGROUP:onafterLoaded(From, Event, To) - env.info("FF loaded") + + -- Debug info. + self:T(self.lid.."Carrier Loaded ==> Transport") -- Cancel landedAt task. if self:IsFlightgroup() and self:IsLandedAt() then @@ -5691,6 +5630,17 @@ function OPSGROUP:onafterTransport(From, Event, To) elseif self.isArmygroup then + local path=self.cargoTransport:_GetPathTransport() + if path then + + for _,_zone in pairs(path.zones:GetSet()) do + local zone=_zone --Core.Zone#ZONE + local coordinate=zone:GetRandomCoordinate(nil, nil, nil) --TODO: surface type land + local waypoint=ARMYGROUP.AddWaypoint(self, coordinate, nil, self:GetWaypointCurrent().uid) ; waypoint.temp=true + end + + end + -- ARMYGROUP local waypoint=ARMYGROUP.AddWaypoint(self, Coordinate, nil, self:GetWaypointCurrent().uid) ; waypoint.detour=true @@ -5699,8 +5649,24 @@ function OPSGROUP:onafterTransport(From, Event, To) elseif self.isNavygroup then + local cwp=self:GetWaypointCurrent() + local uid=cwp and cwp.uid or nil + + -- Get a (random) pre-defined transport path. + local path=self.cargoTransport:_GetPathTransport() + + if path then + -- Loop over zones + for i,coordinate in pairs(path) do + local waypoint=NAVYGROUP.AddWaypoint(self, coordinate, nil, uid) + waypoint.temp=true + uid=waypoint.uid + coordinate:MarkToAll(string.format("Path i=%d, UID=%d", i, uid)) + end + end + -- NAVYGROUP - local waypoint=NAVYGROUP.AddWaypoint(self, Coordinate, nil, self:GetWaypointCurrent().uid) ; waypoint.detour=true + local waypoint=NAVYGROUP.AddWaypoint(self, Coordinate, nil, uid) ; waypoint.detour=true -- Give cruise command. self:Cruise() @@ -5731,93 +5697,107 @@ function OPSGROUP:onafterUnloading(From, Event, To) -- Check that cargo is loaded into this group. -- NOTE: Could be that the element carriing this cargo group is DEAD, which would mean that the cargo group is also DEAD. - if cargo.opsgroup:IsLoaded(self.groupname) and not (cargo.opsgroup:IsDead() or cargo.opsgroup:IsStopped()) then + if cargo.opsgroup:IsLoaded(self.groupname) and not cargo.opsgroup:IsDead() then - env.info("FF deploy cargo "..cargo.opsgroup:GetName()) - - -- New carrier group. - local carrierGroup=self.cargoTransport.carrierGroup + -- Disembark to carrier. + local needscarrier=false + local carrier=nil + local carrierGroup=nil - -- Cargo was delivered (somehow). - cargo.delivered=true - - -- Increase number of delivered cargos. - self.cargoTransport.Ndelivered=self.cargoTransport.Ndelivered+1 - - if carrierGroup then - - --- - -- Delivered to another carrier group. - --- - - -- Get new carrier. - local carrier=carrierGroup:FindCarrierForCargo(cargo.opsgroup) - - if carrier then - -- Unload from this and directly load into the other carrier. - self:Unload(cargo.opsgroup) - carrierGroup:Load(cargo.opsgroup, carrier) - else - env.info("ERROR: No element of the group can take this cargo!") - end - - elseif zone and zone:IsInstanceOf("ZONE_AIRBASE") and zone:GetAirbase():IsShip() then - - --- - -- Delivered to a ship via helo or VTOL - --- - - -- - env.info("ERROR: Deploy/disembark zone is a ZONE_AIRBASE of a ship! Where to put the cargo? Dumping into the sea, sorry!") - - else - - --- - -- Delivered to deploy zone - --- - - if self.cargoTransport.disembarkInUtero then - - -- Unload but keep "in utero" (no coordinate provided). - self:Unload(cargo.opsgroup) - - else - - local Coordinate=nil - - if self.cargoTransport.disembarkzone then - -- Random coordinate in disembark zone. - Coordinate=self.cargoTransport.disembarkzone:GetRandomCoordinate() - - else + if self.cargoTransport.disembarkCarriers and #self.cargoTransport.disembarkCarriers>0 then + + needscarrier=true - -- TODO: Optimize with random Vec2 - - -- Create a zone around the carrier. - local zoneCarrier=ZONE_RADIUS:New("Carrier", self:GetVec2(), 100) - - -- Random coordinate/heading in the zone. - Coordinate=zoneCarrier:GetRandomCoordinate(50) - - end - - -- Random heading of the group. - local Heading=math.random(0,359) - - -- Unload to Coordinate. - self:Unload(cargo.opsgroup, Coordinate, self.cargoTransport.disembarkActivation, Heading) - - end + carrier, carrierGroup=self.cargoTransport:FindTransferCarrierForCargo(cargo.opsgroup, zone) - -- Trigger "Unloaded" event for current cargo transport - self.cargoTransport:Unloaded(cargo.opsgroup) - + --TODO: max unloading time if transfer carrier does not arrive in the zone. + end - end + if needscarrier==false or (needscarrier and carrier and carrierGroup) then + + -- Cargo was delivered (somehow). + cargo.delivered=true + + -- Increase number of delivered cargos. + self.cargoTransport.Ndelivered=self.cargoTransport.Ndelivered+1 + + if carrier and carrierGroup then + + --- + -- Delivered to another carrier group. + --- + + -- Unload from this and directly load into the other carrier. + self:Unload(cargo.opsgroup) + carrierGroup:Load(cargo.opsgroup, carrier) + + elseif zone and zone:IsInstanceOf("ZONE_AIRBASE") and zone:GetAirbase():IsShip() then - end + --- + -- Delivered to a ship via helo or VTOL + --- + + -- Issue warning. + env.info("ERROR: Deploy/disembark zone is a ZONE_AIRBASE of a ship! Where to put the cargo? Dumping into the sea, sorry!") + --TODO: Dumb into sea. + + else + + --- + -- Delivered to deploy zone + --- + + if self.cargoTransport.disembarkInUtero then + + -- Unload but keep "in utero" (no coordinate provided). + self:Unload(cargo.opsgroup) + + else + + local Coordinate=nil + + if self.cargoTransport.disembarkzone then + + -- Random coordinate in disembark zone. + Coordinate=self.cargoTransport.disembarkzone:GetRandomCoordinate() + + else + + -- TODO: Optimize with random Vec2 + + -- Create a zone around the carrier. + local zoneCarrier=ZONE_RADIUS:New("Carrier", self:GetVec2(), 100) + + -- Random coordinate/heading in the zone. + Coordinate=zoneCarrier:GetRandomCoordinate(50) + + end + + -- Random heading of the group. + local Heading=math.random(0,359) + + -- Unload to Coordinate. + self:Unload(cargo.opsgroup, Coordinate, self.cargoTransport.disembarkActivation, Heading) + + end + + -- Trigger "Unloaded" event for current cargo transport + self.cargoTransport:Unloaded(cargo.opsgroup) + + + end + + else + self:T(self.lid.."Cargo needs carrier but no carrier is avaiable (yet)!") + end + + else + -- Not loaded or dead + end + + end -- loop over cargos end @@ -5854,6 +5834,8 @@ function OPSGROUP:onafterUnload(From, Event, To, OpsGroup, Coordinate, Activated -- Set cargo status. OpsGroup.cargoStatus=OPSGROUP.CargoStatus.NOTCARGO + --TODO: Unload flightgroup. Find parking spot etc. + if Coordinate then --- @@ -5899,6 +5881,14 @@ function OPSGROUP:onafterUnload(From, Event, To, OpsGroup, Coordinate, Activated -- Respawn group. OpsGroup:_Respawn(0, Template) + + if self:IsNavygroup() then + self.currentwp=1 + NAVYGROUP.AddWaypoint(self, Coordinate, nil, nil, nil, false) + elseif self:IsArmygroup() then + self.currentwp=1 + ARMYGROUP.AddWaypoint(self, Coordinate, nil, nil, nil, false) + end else @@ -6033,6 +6023,14 @@ function OPSGROUP:onbeforeBoard(From, Event, To, CarrierGroup, Carrier) if self:IsDead() then self:I(self.lid.."Group DEAD ==> Deny Board transition!") return false + elseif CarrierGroup:IsDead() then + self:I(self.lid.."Carrier Group DEAD ==> Deny Board transition!") + self.cargoStatus=OPSGROUP.CargoStatus.NOTCARGO + return false + elseif Carrier.status==OPSGROUP.ElementStatus.DEAD then + self:I(self.lid.."Carrier Element DEAD ==> Deny Board transition!") + self.cargoStatus=OPSGROUP.CargoStatus.NOTCARGO + return false end return true @@ -6052,21 +6050,21 @@ function OPSGROUP:onafterBoard(From, Event, To, CarrierGroup, Carrier) -- Set cargo status. self.cargoStatus=OPSGROUP.CargoStatus.BOARDING - -- Set carrier. - self.carrier=Carrier - self.carrierGroup=CarrierGroup + -- Set carrier. As long as the group is not loaded, we only reserve the cargo space. + self:_SetMyCarrier(CarrierGroup, Carrier, true) - -- Army or navy group. - local isArmyOrNavy=CarrierGroup:IsArmygroup() or CarrierGroup:IsNavygroup() + -- Army or Navy group. + local CarrierIsArmyOrNavy=CarrierGroup:IsArmygroup() or CarrierGroup:IsNavygroup() + local CargoIsArmyOrNavy=self:IsArmygroup() or self:IsNavygroup() -- Check that carrier is standing still. - if isArmyOrNavy and CarrierGroup:IsHolding() or CarrierGroup:IsParking() or CarrierGroup:IsLandedAt() then + if (CarrierIsArmyOrNavy and CarrierGroup:IsHolding()) or (CarrierGroup:IsParking() or CarrierGroup:IsLandedAt()) then -- Board if group is mobile, not late activated and army or navy. Everything else is loaded directly. - local board=self.speedMax>0 and self:IsAlive() and isArmyOrNavy and self.carrierGroup:IsAlive() + local board=self.speedMax>0 and CargoIsArmyOrNavy and self:IsAlive() and CarrierGroup:IsAlive() -- Armygroup cannot board ship ==> Load directly. - if self:IsArmygroup() and self.carrierGroup:IsNavygroup() then + if self:IsArmygroup() and CarrierGroup:IsNavygroup() then board=false end @@ -6093,16 +6091,20 @@ function OPSGROUP:onafterBoard(From, Event, To, CarrierGroup, Carrier) else + --- + -- Direct load into carrier. + --- + -- Debug info. - self:I(self.lid..string.format("FF board with direct load to carrier %s", self.carrierGroup:GetName())) + self:I(self.lid..string.format("Board with direct load to carrier %s", CarrierGroup:GetName())) -- Trigger Load event. - self.carrierGroup:Load(self) + CarrierGroup:Load(self) end else - env.info("FF Carrier not ready for boarding yet ==> repeating boarding call in 10 sec") + self:T(self.lid.."Carrier not ready for boarding yet ==> repeating boarding call in 10 sec") self:__Board(-10, CarrierGroup, Carrier) end @@ -6827,7 +6829,17 @@ function OPSGROUP._PassingWaypoint(group, opsgroup, uid) opsgroup:I(opsgroup.lid..text) -- Trigger PassingWaypoint event. - if waypoint.astar then + if waypoint.temp then + + -- Remove temp waypoint. + opsgroup:RemoveWaypointByID(uid) + + if opsgroup:IsNavygroup() or opsgroup:IsArmygroup() then + --TODO: not sure if this works with FLIGHTGROUPS + opsgroup:Cruise() + end + + elseif waypoint.astar then -- Remove Astar waypoint. opsgroup:RemoveWaypointByID(uid) @@ -6866,8 +6878,6 @@ function OPSGROUP._PassingWaypoint(group, opsgroup, uid) elseif opsgroup:IsTransporting() then if opsgroup.isFlightgroup then - - env.info("FF passing waypointg in state istransporting ==> land at") -- Land at current pos and wait for 60 min max. opsgroup:LandAt(opsgroup:GetCoordinate(), 60*60) @@ -6880,12 +6890,15 @@ function OPSGROUP._PassingWaypoint(group, opsgroup, uid) elseif opsgroup:IsBoarding() then - if opsgroup.carrierGroup and opsgroup.carrierGroup:IsAlive() then + local carrierGroup=opsgroup:_GetMyCarrierGroup() + local carrier=opsgroup:_GetMyCarrierElement() + + if carrierGroup and carrierGroup:IsAlive() then - if opsgroup.carrier and opsgroup.carrier.unit and opsgroup.carrier.unit:IsAlive() then + if carrier and carrier.unit and carrier.unit:IsAlive() then -- Load group into the carrier. - opsgroup.carrierGroup:Load(opsgroup) + carrierGroup:Load(opsgroup) else opsgroup:E(opsgroup.lid.."ERROR: Group cannot board assigned carrier UNIT as it is NOT alive!") @@ -7651,10 +7664,7 @@ function OPSGROUP:_UpdatePosition() -- Add up travelled distance. self.traveldist=self.traveldist+self.travelds - - -- Debug info. - --env.info(string.format("FF Traveled %.1f m", self.traveldist)) - + end return self diff --git a/Moose Development/Moose/Ops/OpsTransport.lua b/Moose Development/Moose/Ops/OpsTransport.lua index 6bf39a147..ef21c495a 100644 --- a/Moose Development/Moose/Ops/OpsTransport.lua +++ b/Moose Development/Moose/Ops/OpsTransport.lua @@ -26,9 +26,8 @@ -- @field #string lid Log ID. -- @field #number uid Unique ID of the transport. -- @field #number verbose Verbosity level. --- @field #table cargos Cargos. Each element is a @{#OPSGROUP.Cargo}. +-- @field #table cargos Cargos. Each element is a @{Ops.OpsGroup#OPSGROUP.CargoGroup}. -- @field #table carriers Carriers assigned for this transport. --- @field #string status Status of the transport. See @{#OPSTRANSPORT.Status}. -- @field #number prio Priority of this transport. Should be a number between 0 (high prio) and 100 (low prio). -- @field #number importance Importance of this transport. Smaller=higher. -- @field #number Tstart Start time in *abs.* seconds. @@ -39,12 +38,13 @@ -- @field Core.Zone#ZONE embarkzone (Optional) Zone where the cargo is supposed to embark. Default is the pickup zone. -- @field Core.Zone#ZONE disembarkzone (Optional) Zone where the cargo is disembarked. Default is the deploy zone. -- @field Core.Zone#ZONE unboardzone (Optional) Zone where the cargo is going to after disembarkment. --- @field Ops.OpsGroup#OPSGROUP carrierGroup The new carrier group. -- @field #boolean disembarkActivation Activation setting when group is disembared from carrier. -- @field #boolean disembarkInUtero Do not spawn the group in any any state but leave it "*in utero*". For example, to directly load it into another carrier. +-- @field #table disembarkCarriers Table of carriers to which the cargo is disembared. This is a direct transfer from the old to the new carrier. -- @field #number Ncargo Total number of cargo groups. -- @field #number Ncarrier Total number of assigned carriers. -- @field #number Ndelivered Total number of cargo groups delivered. +-- @field #table pathsTransport Paths of `#OPSGROUP.Path`. -- @extends Core.Fsm#FSM --- *Victory is the beautiful, bright-colored flower. Transport is the stem without which it could never have blossomed.* -- Winston Churchill @@ -64,12 +64,13 @@ OPSTRANSPORT = { cargos = {}, carriers = {}, carrierTransportStatus = {}, - conditionStart = {}, + conditionStart = {}, + pathsTransport = {}, } --- Cargo transport status. -- @type OPSTRANSPORT.Status --- @field #string PLANNING Planning state. +-- @field #string PLANNED Planning state. -- @field #string SCHEDULED Transport is scheduled in the cargo queue. -- @field #string EXECUTING Transport is being executed. -- @field #string DELIVERED Transport was delivered. @@ -80,6 +81,12 @@ OPSTRANSPORT.Status={ DELIVERED="delivered", } +--- Path. +-- @type OPSTRANSPORT.Path +-- @field #table coords Table of coordinates. +-- @field #number radius Radomization radius in meters. Default 0 m. +-- @field #number altitude Altitude in feet AGL. Only for aircraft. + --- Generic mission condition. -- @type OPSTRANSPORT.Condition -- @field #function func Callback function to check for a condition. Should return a #boolean. @@ -115,32 +122,30 @@ function OPSTRANSPORT:New(GroupSet, Pickupzone, Deployzone) local self=BASE:Inherit(self, FSM:New()) -- #OPSTRANSPORT -- Increase ID counter. - _OPSTRANSPORTID=_OPSTRANSPORTID+1 + _OPSTRANSPORTID=_OPSTRANSPORTID+1 -- Set some string id for output to DCS.log file. self.lid=string.format("OPSTRANSPORT [UID=%d] | ", _OPSTRANSPORTID) -- Defaults. self.uid=_OPSTRANSPORTID - self.status=OPSTRANSPORT.Status.PLANNING + --self.status=OPSTRANSPORT.Status.PLANNING self.pickupzone=Pickupzone self.deployzone=Deployzone self.embarkzone=Pickupzone - self.carrierGroup=nil self.cargos={} self.carriers={} self.Ncargo=0 self.Ncarrier=0 self.Ndelivered=0 - self:SetPriority() self:SetTime() - -- Add cargo groups. + -- Add cargo groups (could also be added later). if GroupSet then - self:AddCargoGroups(GroupSet, Pickupzone, Deployzone) + self:AddCargoGroups(GroupSet) end @@ -152,8 +157,11 @@ function OPSTRANSPORT:New(GroupSet, Pickupzone, Deployzone) self:AddTransition(OPSTRANSPORT.Status.PLANNED, "Scheduled", OPSTRANSPORT.Status.SCHEDULED) -- Cargo is queued at at least one carrier. self:AddTransition(OPSTRANSPORT.Status.SCHEDULED, "Executing", OPSTRANSPORT.Status.EXECUTING) -- Cargo is being transported. self:AddTransition(OPSTRANSPORT.Status.EXECUTING, "Delivered", OPSTRANSPORT.Status.DELIVERED) -- Cargo was delivered. + self:AddTransition("*", "Status", "*") self:AddTransition("*", "Stop", "*") + + self:AddTransition("*", "Loaded", "*") self:AddTransition("*", "Unloaded", "*") @@ -171,13 +179,13 @@ end -- @param #OPSTRANSPORT self -- @param Core.Set#SET_GROUP GroupSet Set of groups to be transported. Can also be passed as a single GROUP or OPSGROUP object. -- @return #OPSTRANSPORT self -function OPSTRANSPORT:AddCargoGroups(GroupSet, Pickupzone, Deployzone) +function OPSTRANSPORT:AddCargoGroups(GroupSet) -- Check type of GroupSet provided. if GroupSet:IsInstanceOf("GROUP") or GroupSet:IsInstanceOf("OPSGROUP") then -- We got a single GROUP or OPSGROUP object. - local cargo=self:_CreateCargoGroupData(GroupSet, Pickupzone, Deployzone) + local cargo=self:_CreateCargoGroupData(GroupSet) if cargo then --and self:CanCargo(cargo.opsgroup) table.insert(self.cargos, cargo) @@ -190,7 +198,7 @@ function OPSTRANSPORT:AddCargoGroups(GroupSet, Pickupzone, Deployzone) for _,group in pairs(GroupSet.Set) do - local cargo=self:_CreateCargoGroupData(group, Pickupzone, Deployzone) + local cargo=self:_CreateCargoGroupData(group) if cargo then table.insert(self.cargos, cargo) @@ -202,11 +210,10 @@ function OPSTRANSPORT:AddCargoGroups(GroupSet, Pickupzone, Deployzone) -- Debug info. if self.verbose>=0 then - local text=string.format("Created Cargo Transport (UID=%d) from %s(%s) --> %s(%s)", - self.uid, self.pickupzone:GetName(), self.embarkzone and self.embarkzone:GetName() or "none", self.deployzone:GetName(), self.disembarkzone and self.disembarkzone:GetName() or "none") + local text=string.format("Added cargo groups:") local Weight=0 for _,_cargo in pairs(self.cargos) do - local cargo=_cargo --#OPSGROUP.CargoGroup + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup local weight=cargo.opsgroup:GetWeightTotal() Weight=Weight+weight text=text..string.format("\n- %s [%s] weight=%.1f kg", cargo.opsgroup:GetName(), cargo.opsgroup:GetState(), weight) @@ -250,6 +257,40 @@ function OPSTRANSPORT:SetDisembarkActivation(Active) return self end +--- Set transfer carrier(s). These are carrier groups, where the cargo is directly loaded into when disembarked. +-- @param #OPSTRANSPORT self +-- @param Core.Set#SET_GROUP Carriers Carrier set. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetDisembarkCarriers(Carriers) + + self:I(self.lid.."Setting transfer carriers!") + + -- Create table. + self.disembarkCarriers=self.disembarkCarriers or {} + + if Carriers:IsInstanceOf("GROUP") or Carriers:IsInstanceOf("OPSGROUP") then + + local carrier=self:_GetOpsGroupFromObject(Carriers) + if carrier then + table.insert(self.disembarkCarriers, carrier) + end + + elseif Carriers:IsInstanceOf("SET_GROUP") or Carriers:IsInstanceOf("SET_OPSGROUP") then + + for _,object in pairs(Carriers:GetSet()) do + local carrier=self:_GetOpsGroupFromObject(object) + if carrier then + table.insert(self.disembarkCarriers, carrier) + end + end + + else + self:E(self.lid.."ERROR: Carriers must be a GROUP, OPSGROUP, SET_GROUP or SET_OPSGROUP object!") + end + + return self +end + --- Set if group remains *in utero* after disembarkment from carrier. Can be used to directly load the group into another carrier. Similar to disembark in late activated state. -- @param #OPSTRANSPORT self @@ -264,21 +305,7 @@ function OPSTRANSPORT:SetDisembarkInUtero(InUtero) return self end ---- Check if an OPS group is assigned as carrier for this transport. --- @param #OPSTRANSPORT self --- @param Ops.OpsGroup#OPSGROUP CarrierGroup Potential carrier OPSGROUP. --- @return #boolean If true, group is an assigned carrier. -function OPSTRANSPORT:IsCarrier(CarrierGroup) - for _,_carrier in pairs(self.carriers) do - local carrier=_carrier --Ops.OpsGroup#OPSGROUP - if carrier.groupname==CarrierGroup.groupname then - return true - end - end - - return false -end --- Add a carrier assigned for this transport. -- @param #OPSTRANSPORT self @@ -358,20 +385,76 @@ end -- @param ... Condition function arguments if any. -- @return #OPSTRANSPORT self function OPSTRANSPORT:AddConditionStart(ConditionFunction, ...) + + if ConditionFunction then - local condition={} --#OPSTRANSPORT.Condition - - condition.func=ConditionFunction - condition.arg={} - if arg then - condition.arg=arg + local condition={} --#OPSTRANSPORT.Condition + + condition.func=ConditionFunction + condition.arg={} + if arg then + condition.arg=arg + end + + table.insert(self.conditionStart, condition) + end - table.insert(self.conditionStart, condition) - return self end +--- Add path used for transportation from the pickup to the deploy zone. If multiple paths are defined, a random one is chosen. +-- @param #OPSTRANSPORT self +-- @param Wrapper.Group#GROUP PathGroup A (late activated) GROUP defining a transport path by their waypoints. +-- @param #number Radius Randomization radius in meters. Default 0 m. +-- @param #number Altitude Altitude in feet AGL. Only for aircraft. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:AddPathTransport(PathGroup, Radius, Altitude) + + local path={} --#OPSTRANSPORT.Path + path.coords={} + path.radius=Radius or 0 + path.altitude=Altitude + + -- Get route points. + local waypoints=PathGroup:GetTaskRoute() + + for _,wp in pairs(waypoints) do + local coord=COORDINATE:New(wp.x, wp.alt, wp.y) + table.insert(path.coords, coord) + end + + -- Add path. + table.insert(self.pathsTransport, path) + + return self +end + +--- Get a path for transportation. +-- @param #OPSTRANSPORT self +-- @return #table The path of COORDINATEs. +function OPSTRANSPORT:_GetPathTransport() + + if self.pathsTransport and #self.pathsTransport>0 then + + -- Get a random path for transport. + local path=self.pathsTransport[math.random(#self.pathsTransport)] --#OPSTRANSPORT.Path + + + local coordinates={} + for _,coord in ipairs(path.coords) do + + -- TODO: Add randomization. + + table.insert(coordinates, coord) + end + + return coordinates + end + + return nil +end + --- Add a carrier assigned for this transport. -- @param #OPSTRANSPORT self @@ -394,46 +477,22 @@ function OPSTRANSPORT:GetCarrierTransportStatus(CarrierGroup) end ---- Create a cargo group data structure. --- @param #OPSTRANSPORT self --- @param Wrapper.Group#GROUP group The GROUP object. --- @param Core.Zone#ZONE Pickupzone Pickup zone. --- @param Core.Zone#ZONE Deployzone Deploy zone. --- @return #OPSGROUP.CargoGroup Cargo group data. -function OPSTRANSPORT:_CreateCargoGroupData(group, Pickupzone, Deployzone) - local opsgroup=nil - - if group:IsInstanceOf("OPSGROUP") then - opsgroup=group - elseif group:IsInstanceOf("GROUP") then - - opsgroup=_DATABASE:GetOpsGroup(group) - - if not opsgroup then - if group:IsAir() then - opsgroup=FLIGHTGROUP:New(group) - elseif group:IsShip() then - opsgroup=NAVYGROUP:New(group) - else - opsgroup=ARMYGROUP:New(group) - end + +--- Check if an OPS group is assigned as carrier for this transport. +-- @param #OPSTRANSPORT self +-- @param Ops.OpsGroup#OPSGROUP CarrierGroup Potential carrier OPSGROUP. +-- @return #boolean If true, group is an assigned carrier. +function OPSTRANSPORT:IsCarrier(CarrierGroup) + + for _,_carrier in pairs(self.carriers) do + local carrier=_carrier --Ops.OpsGroup#OPSGROUP + if carrier.groupname==CarrierGroup.groupname then + return true end - - else - self:E(self.lid.."ERROR: Cargo must be a GROUP or OPSGROUP object!") - return nil end - local cargo={} --#OPSGROUP.CargoGroup - - cargo.opsgroup=opsgroup - cargo.delivered=false - cargo.status="Unknown" - cargo.pickupzone=Pickupzone - cargo.deployzone=Deployzone - - return cargo + return false end --- Check if transport is ready to be started. @@ -443,31 +502,49 @@ end -- @param #OPSTRANSPORT self -- @return #boolean If true, mission can be started. function OPSTRANSPORT:IsReadyToGo() + + -- Debug text. + local text=self.lid.."Is ReadyToGo? " + -- Current abs time. local Tnow=timer.getAbsTime() -- Start time did not pass yet. if self.Tstart and Tnowself.Tstop or false then + text=text.."Nope, stop time already passed!" + self:T(text) return false end -- All start conditions true? local startme=self:EvalConditionsAll(self.conditionStart) + -- Nope, not yet. if not startme then + text=text..("No way, at least one start condition is not true!") + self:I(text) return false end - -- We're good to go! + text=text.."Yes!" + self:T(text) return true end +--- Check if all cargo was delivered (or is dead). +-- @param #OPSTRANSPORT self +-- @return #boolean If true, all possible cargo was delivered. +function OPSTRANSPORT:IsDelivered() + return self:is(OPSTRANSPORT.Status.DELIVERED) +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Status Update ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -482,14 +559,16 @@ function OPSTRANSPORT:onafterStatus(From, Event, To) -- Current FSM state. local fsmstate=self:GetState() - + -- Info text. local text=string.format("%s [%s --> %s]: Ncargo=%d/%d, Ncarrier=%d", fsmstate:upper(), self.pickupzone:GetName(), self.deployzone:GetName(), self.Ncargo, self.Ndelivered, self.Ncarrier) text=text..string.format("\nCargos:") for _,_cargo in pairs(self.cargos) do local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup - local name=cargo.opsgroup.carrier and cargo.opsgroup.carrier.name or "none" - text=text..string.format("\n- %s: %s [%s], weight=%d kg, carrier=%s", cargo.opsgroup:GetName(), cargo.opsgroup.cargoStatus:upper(), cargo.opsgroup:GetState(), cargo.opsgroup:GetWeightTotal(), name) + local carrier=cargo.opsgroup:_GetMyCarrierElement() + local name=carrier and carrier.name or "none" + local cstate=carrier and carrier.status or "N/A" + text=text..string.format("\n- %s: %s [%s], weight=%d kg, carrier=%s [%s]", cargo.opsgroup:GetName(), cargo.opsgroup.cargoStatus:upper(), cargo.opsgroup:GetState(), cargo.opsgroup:GetWeightTotal(), name, cstate) end text=text..string.format("\nCarriers:") @@ -504,7 +583,10 @@ function OPSTRANSPORT:onafterStatus(From, Event, To) -- Check if all cargo was delivered (or is dead). self:_CheckDelivered() - self:__Status(-30) + -- Update status again. + if not self:IsDelivered() then + self:__Status(-30) + end end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -574,39 +656,44 @@ end -- @param #OPSTRANSPORT self function OPSTRANSPORT:_CheckDelivered() - local done=true - local dead=true - for _,_cargo in pairs(self.cargos) do - local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup - - if cargo.delivered then - -- This one is delivered. - dead=false - elseif cargo.opsgroup==nil then - -- This one is nil?! - dead=false - elseif cargo.opsgroup:IsDestroyed() then - -- This one was destroyed. - elseif cargo.opsgroup:IsDead() then - -- This one is dead. - dead=false - elseif cargo.opsgroup:IsStopped() then - -- This one is stopped. - dead=false - else - done=false --Someone is not done! - dead=false + -- First check that at least one cargo was added (as we allow to do that later). + if self.Ncargo>0 then + + local done=true + local dead=true + for _,_cargo in pairs(self.cargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + + if cargo.delivered then + -- This one is delivered. + dead=false + elseif cargo.opsgroup==nil then + -- This one is nil?! + dead=false + elseif cargo.opsgroup:IsDestroyed() then + -- This one was destroyed. + elseif cargo.opsgroup:IsDead() then + -- This one is dead. + dead=false + elseif cargo.opsgroup:IsStopped() then + -- This one is stopped. + dead=false + else + done=false --Someone is not done! + dead=false + end + end - - end - - if dead then - --self:CargoDead() - self:I(self.lid.."All cargo DEAD!") - self:Delivered() - elseif done then - self:I(self.lid.."All cargo delivered") - self:Delivered() + + if dead then + --self:CargoDead() + self:I(self.lid.."All cargo DEAD!") + self:Delivered() + elseif done then + self:I(self.lid.."All cargo delivered") + self:Delivered() + end + end end @@ -634,3 +721,92 @@ function OPSTRANSPORT:EvalConditionsAll(Conditions) -- All conditions were true. return true end + + + +--- Find transfer carrier element for cargo group. +-- @param #OPSTRANSPORT self +-- @param Ops.OpsGroup#OPSGROUP CargoGroup The cargo group that needs to be loaded into a carrier unit/element of the carrier group. +-- @param Core.Zone#ZONE Zone (Optional) Zone where the carrier must be in. +-- @return Ops.OpsGroup#OPSGROUP.Element New carrier element for cargo or nil. +-- @return Ops.OpsGroup#OPSGROUP New carrier group for cargo or nil. +function OPSTRANSPORT:FindTransferCarrierForCargo(CargoGroup, Zone) + + local carrier=nil --Ops.OpsGroup#OPSGROUP.Element + local carrierGroup=nil --Ops.OpsGroup#OPSGROUP + + --TODO: maybe sort the carriers wrt to largest free cargo bay. Or better smallest free cargo bay that can take the cargo group weight. + + for _,_carrier in pairs(self.disembarkCarriers or {}) do + local carrierGroup=_carrier --Ops.OpsGroup#OPSGROUP + + -- Find an element of the group that has enough free space. + carrier=carrierGroup:FindCarrierForCargo(CargoGroup) + + if carrier then + if Zone==nil or Zone:IsCoordinateInZone(carrier.unit:GetCoordinate()) then + return carrier, carrierGroup + else + self:T3(self.lid.."Got transfer carrier but carrier not in zone (yet)!") + end + else + self:T3(self.lid.."No transfer carrier available!") + end + end + + return nil, nil +end + +--- Create a cargo group data structure. +-- @param #OPSTRANSPORT self +-- @param Wrapper.Group#GROUP group The GROUP or OPSGROUP object. +-- @return Ops.OpsGroup#OPSGROUP.CargoGroup Cargo group data. +function OPSTRANSPORT:_CreateCargoGroupData(group) + + local opsgroup=self:_GetOpsGroupFromObject(group) + + local cargo={} --Ops.OpsGroup#OPSGROUP.CargoGroup + + cargo.opsgroup=opsgroup + cargo.delivered=false + cargo.status="Unknown" + cargo.disembarkCarrierElement=nil + cargo.disembarkCarrierGroup=nil + + return cargo +end + +--- Get an OPSGROUIP +-- @param #OPSTRANSPORT self +-- @param Core.Base#BASE Object The object, which can be a GROUP or OPSGROUP. +-- @return Ops.OpsGroup#OPSGROUP Ops Group. +function OPSTRANSPORT:_GetOpsGroupFromObject(Object) + + local opsgroup=nil + + if Object:IsInstanceOf("OPSGROUP") then + -- We already have an OPSGROUP + opsgroup=Object + elseif Object:IsInstanceOf("GROUP") then + + -- Look into DB and try to find an existing OPSGROUP. + opsgroup=_DATABASE:GetOpsGroup(Object) + + if not opsgroup then + if Object:IsAir() then + opsgroup=FLIGHTGROUP:New(Object) + elseif Object:IsShip() then + opsgroup=NAVYGROUP:New(Object) + else + opsgroup=ARMYGROUP:New(Object) + end + end + + else + self:E(self.lid.."ERROR: Object must be a GROUP or OPSGROUP object!") + return nil + end + + return opsgroup +end +