Ops Cargo

This commit is contained in:
Frank 2021-02-27 21:17:58 +01:00
parent a63ee3fa8b
commit 4a6377c204
7 changed files with 1276 additions and 465 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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<disttoWP then
if not self.collisionwarning then
-- Issue a collision warning event.
@ -752,6 +753,7 @@ function NAVYGROUP:onafterUpdateRoute(From, Event, To, n, Speed, Depth)
wp.alt=-self.depth
else
-- Take default waypoint alt.
wp.alt=wp.alt or 0
end
-- Current set speed in m/s.
@ -1274,8 +1276,6 @@ function NAVYGROUP:_CheckFreePath(DistanceMax, dx)
--coordinate=coordinate:Translate(500, heading, true)
local function LoS(dist)
--local checkcoord=coordinate:Translate(dist, heading, true)
--return coordinate:IsLOS(checkcoord, offsetY)
local checkvec3=UTILS.VecTranslate(vec3, dist, heading)
local los=land.isVisible(vec3, checkvec3)
return los
@ -1303,7 +1303,7 @@ function NAVYGROUP:_CheckFreePath(DistanceMax, dx)
local los=LoS(x)
-- Debug message.
self:T2(self.lid..string.format("N=%d: xmin=%.1f xmax=%.1f x=%.1f d=%.3f los=%s", N, xmin, xmax, x, d, tostring(los)))
self:I(self.lid..string.format("N=%d: xmin=%.1f xmax=%.1f x=%.1f d=%.3f los=%s", N, xmin, xmax, x, d, tostring(los)))
if los and d<=eps then
return x
@ -1477,6 +1477,7 @@ end
-- @param #NAVYGROUP self
-- @return #boolean If true, a path was found.
function NAVYGROUP:_FindPathToNextWaypoint()
env.info("FF Path finding")
-- Pathfinding A*
local astar=ASTAR:New()
@ -1487,6 +1488,11 @@ function NAVYGROUP:_FindPathToNextWaypoint()
-- Next waypoint.
local wpnext=self:GetWaypointNext()
-- No next waypoint.
if wpnext==nil then
return
end
-- Next waypoint coordinate.
local nextwp=wpnext.coordinate
@ -1503,16 +1509,21 @@ function NAVYGROUP:_FindPathToNextWaypoint()
-- Set end coordinate.
astar:SetEndCoordinate(nextwp)
-- Distance to next waypoint.
local dist=position:Get2DDistance(nextwp)
-- Check distance >= 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

File diff suppressed because it is too large Load Diff

View File

@ -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 Tnow<self.Tstart or false then
text=text.."No, start time not passed!"
return false
end
-- Stop time already passed.
if self.Tstop and Tnow>self.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