diff --git a/Moose Development/Moose/Core/Base.lua b/Moose Development/Moose/Core/Base.lua index e085f0081..3bd050937 100644 --- a/Moose Development/Moose/Core/Base.lua +++ b/Moose Development/Moose/Core/Base.lua @@ -234,7 +234,8 @@ FORMATION = { -- @param #BASE self -- @return #BASE function BASE:New() - local self = routines.utils.deepCopy( self ) -- Create a new self instance + --local self = routines.utils.deepCopy( self ) -- Create a new self instance + local self = UTILS.DeepCopy(self) _ClassID = _ClassID + 1 self.ClassID = _ClassID @@ -873,20 +874,22 @@ do -- Scheduling -- @param #number Start Specifies the amount of seconds that will be waited before the scheduling is started, and the event function is called. -- @param #function SchedulerFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in SchedulerArguments. -- @param #table ... Optional arguments that can be given as part of scheduler. The arguments need to be given as a table { param1, param 2, ... }. - -- @return #number The ScheduleID of the planned schedule. + -- @return #string The Schedule ID of the planned schedule. function BASE:ScheduleOnce( Start, SchedulerFunction, ... ) - self:F2( { Start } ) - self:T3( { ... } ) + -- Object name. local ObjectName = "-" ObjectName = self.ClassName .. self.ClassID + -- Debug info. self:F3( { "ScheduleOnce: ", ObjectName, Start } ) if not self.Scheduler then self.Scheduler = SCHEDULER:New( self ) end + -- FF this was wrong! + --[[ local ScheduleID = _SCHEDULEDISPATCHER:AddSchedule( self, SchedulerFunction, @@ -896,6 +899,10 @@ do -- Scheduling nil, nil ) + ]] + + -- NOTE: MasterObject (first parameter) needs to be nil or it will be the first argument passed to the SchedulerFunction! + local ScheduleID = self.Scheduler:Schedule(nil, SchedulerFunction, {...}, Start, nil, nil, nil) self._.Schedules[#self._.Schedules+1] = ScheduleID @@ -910,7 +917,7 @@ do -- Scheduling -- @param #number Stop Specifies the amount of seconds when the scheduler will be stopped. -- @param #function SchedulerFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in SchedulerArguments. -- @param #table ... Optional arguments that can be given as part of scheduler. The arguments need to be given as a table { param1, param 2, ... }. - -- @return #number The ScheduleID of the planned schedule. + -- @return #string The Schedule ID of the planned schedule. function BASE:ScheduleRepeat( Start, Repeat, RandomizeFactor, Stop, SchedulerFunction, ... ) self:F2( { Start } ) self:T3( { ... } ) @@ -924,6 +931,7 @@ do -- Scheduling self.Scheduler = SCHEDULER:New( self ) end + -- NOTE: MasterObject (first parameter) should(!) be nil as it will be the first argument passed to the SchedulerFunction!s local ScheduleID = self.Scheduler:Schedule( self, SchedulerFunction, @@ -942,13 +950,13 @@ do -- Scheduling --- Stops the Schedule. -- @param #BASE self - -- @param #function SchedulerFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in SchedulerArguments. - function BASE:ScheduleStop( SchedulerFunction ) - + -- @param #string SchedulerID (Optional) Scheduler ID to be stopped. If nil, all pending schedules are stopped. + function BASE:ScheduleStop( SchedulerID ) self:F3( { "ScheduleStop:" } ) if self.Scheduler then - _SCHEDULEDISPATCHER:Stop( self.Scheduler, self._.Schedules[SchedulerFunction] ) + --_SCHEDULEDISPATCHER:Stop( self.Scheduler, self._.Schedules[SchedulerFunction] ) + _SCHEDULEDISPATCHER:Stop(self.Scheduler, SchedulerID) end end @@ -1092,7 +1100,7 @@ end --- Set tracing for a class -- @param #BASE self --- @param #string Class +-- @param #string Class Class name. function BASE:TraceClass( Class ) _TraceClass[Class] = true _TraceClassMethod[Class] = {} @@ -1101,8 +1109,8 @@ end --- Set tracing for a specific method of class -- @param #BASE self --- @param #string Class --- @param #string Method +-- @param #string Class Class name. +-- @param #string Method Method. function BASE:TraceClassMethod( Class, Method ) if not _TraceClassMethod[Class] then _TraceClassMethod[Class] = {} diff --git a/Moose Development/Moose/Core/Database.lua b/Moose Development/Moose/Core/Database.lua index df182aac2..fe5f5c450 100644 --- a/Moose Development/Moose/Core/Database.lua +++ b/Moose Development/Moose/Core/Database.lua @@ -860,6 +860,20 @@ function DATABASE:GetGroupTemplateFromUnitName( UnitName ) end end +--- Get group template from unit name. +-- @param #DATABASE self +-- @param #string UnitName Name of the unit. +-- @return #table Group template. +function DATABASE:GetUnitTemplateFromUnitName( UnitName ) + if self.Templates.Units[UnitName] then + return self.Templates.Units[UnitName] + else + self:E("ERROR: Unit template does not exist for unit "..tostring(UnitName)) + return nil + end +end + + --- Get coalition ID from client name. -- @param #DATABASE self -- @param #string ClientName Name of the Client. @@ -1474,19 +1488,19 @@ function DATABASE:SetPlayerSettings( PlayerName, Settings ) self.PLAYERSETTINGS[PlayerName] = Settings end ---- Add a flight group to the data base. +--- Add an OPS group (FLIGHTGROUP, ARMYGROUP, NAVYGROUP) to the data base. -- @param #DATABASE self --- @param Ops.FlightGroup#FLIGHTGROUP flightgroup -function DATABASE:AddFlightGroup(flightgroup) - self:T({NewFlightGroup=flightgroup.groupname}) - self.FLIGHTGROUPS[flightgroup.groupname]=flightgroup +-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group added to the DB. +function DATABASE:AddOpsGroup(opsgroup) + --env.info("Adding OPSGROUP "..tostring(opsgroup.groupname)) + self.FLIGHTGROUPS[opsgroup.groupname]=opsgroup end ---- Get a flight group from the data base. +--- Get an OPS group (FLIGHTGROUP, ARMYGROUP, NAVYGROUP) from the data base. -- @param #DATABASE self --- @param #string groupname Group name of the flight group. Can also be passed as GROUP object. --- @return Ops.FlightGroup#FLIGHTGROUP Flight group object. -function DATABASE:GetFlightGroup(groupname) +-- @param #string groupname Group name of the group. Can also be passed as GROUP object. +-- @return Ops.OpsGroup#OPSGROUP OPS group object. +function DATABASE:GetOpsGroup(groupname) -- Get group and group name. if type(groupname)=="string" then @@ -1494,6 +1508,23 @@ function DATABASE:GetFlightGroup(groupname) groupname=groupname:GetName() end + --env.info("Getting OPSGROUP "..tostring(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 @@ -1581,7 +1612,7 @@ function DATABASE:_RegisterTemplates() if obj_type_name ~= "static" and Template and Template.units and type(Template.units) == 'table' then --making sure again- this is a valid group - self:_RegisterGroupTemplate(Template, CoalitionSide, _DATABASECategory[string.lower(CategoryName)], CountryID) + self:_RegisterGroupTemplate(Template, CoalitionSide, _DATABASECategory[string.lower(CategoryName)], CountryID) else diff --git a/Moose Development/Moose/Core/Fsm.lua b/Moose Development/Moose/Core/Fsm.lua index 0ada59e71..3638153ef 100644 --- a/Moose Development/Moose/Core/Fsm.lua +++ b/Moose Development/Moose/Core/Fsm.lua @@ -410,7 +410,7 @@ do -- FSM Transition.To = To -- Debug message. - self:T2( Transition ) + --self:T3( Transition ) self._Transitions[Transition] = Transition self:_eventmap( self.Events, Transition ) @@ -432,7 +432,7 @@ do -- FSM -- @param #table ReturnEvents A table indicating for which returned events of the SubFSM which Event must be triggered in the FSM. -- @return Core.Fsm#FSM_PROCESS The SubFSM. function FSM:AddProcess( From, Event, Process, ReturnEvents ) - self:T( { From, Event } ) + --self:T3( { From, Event } ) local Sub = {} Sub.From = From @@ -533,7 +533,7 @@ do -- FSM Process._Scores[State].ScoreText = ScoreText Process._Scores[State].Score = Score - self:T( Process._Scores ) + --self:T3( Process._Scores ) return Process end @@ -576,7 +576,7 @@ do -- FSM self[__Event] = self[__Event] or self:_delayed_transition(Event) -- Debug message. - self:T2( "Added methods: " .. Event .. ", " .. __Event ) + --self:T3( "Added methods: " .. Event .. ", " .. __Event ) Events[Event] = self.Events[Event] or { map = {} } self:_add_to_map( Events[Event].map, EventStructure ) @@ -791,7 +791,7 @@ do -- FSM return function( self, DelaySeconds, ... ) -- Debug. - self:T2( "Delayed Event: " .. EventName ) + self:T3( "Delayed Event: " .. EventName ) local CallID = 0 if DelaySeconds ~= nil then @@ -809,23 +809,23 @@ do -- FSM self._EventSchedules[EventName] = CallID -- Debug output. - self:T2(string.format("NEGATIVE Event %s delayed by %.1f sec SCHEDULED with CallID=%s", EventName, DelaySeconds, tostring(CallID))) + self:T2(string.format("NEGATIVE Event %s delayed by %.3f sec SCHEDULED with CallID=%s", EventName, DelaySeconds, tostring(CallID))) else - self:T2(string.format("NEGATIVE Event %s delayed by %.1f sec CANCELLED as we already have such an event in the queue.", EventName, DelaySeconds)) + self:T2(string.format("NEGATIVE Event %s delayed by %.3f sec CANCELLED as we already have such an event in the queue.", EventName, DelaySeconds)) -- reschedule end else CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1, nil, nil, nil, 4, true ) - self:T2(string.format("Event %s delayed by %.1f sec SCHEDULED with CallID=%s", EventName, DelaySeconds, tostring(CallID))) + self:T2(string.format("Event %s delayed by %.3f sec SCHEDULED with CallID=%s", EventName, DelaySeconds, tostring(CallID))) end else error( "FSM: An asynchronous event trigger requires a DelaySeconds parameter!!! This can be positive or negative! Sorry, but will not process this." ) end -- Debug. - self:T2( { CallID = CallID } ) + --self:T3( { CallID = CallID } ) end end @@ -846,7 +846,7 @@ do -- FSM function FSM:_gosub( ParentFrom, ParentEvent ) local fsmtable = {} if self.subs[ParentFrom] and self.subs[ParentFrom][ParentEvent] then - self:T( { ParentFrom, ParentEvent, self.subs[ParentFrom], self.subs[ParentFrom][ParentEvent] } ) + --self:T3( { ParentFrom, ParentEvent, self.subs[ParentFrom], self.subs[ParentFrom][ParentEvent] } ) return self.subs[ParentFrom][ParentEvent] else return {} @@ -893,7 +893,7 @@ do -- FSM end end - self:T3( { Map, Event } ) + --self:T3( { Map, Event } ) end --- Get current state. @@ -1150,7 +1150,7 @@ do -- FSM_PROCESS -- @param #FSM_PROCESS self -- @return #FSM_PROCESS function FSM_PROCESS:Copy( Controllable, Task ) - self:T( { self:GetClassNameAndID() } ) + --self:T3( { self:GetClassNameAndID() } ) local NewFsm = self:New( Controllable, Task ) -- Core.Fsm#FSM_PROCESS @@ -1176,13 +1176,13 @@ do -- FSM_PROCESS -- Copy End States for EndStateID, EndState in pairs( self:GetEndStates() ) do - self:T( EndState ) + --self:T3( EndState ) NewFsm:AddEndState( EndState ) end -- Copy the score tables for ScoreID, Score in pairs( self:GetScores() ) do - self:T( Score ) + --self:T3( Score ) NewFsm:AddScore( ScoreID, Score.ScoreText, Score.Score ) end @@ -1431,7 +1431,7 @@ do -- FSM_SET -- @param #FSM_SET self -- @return Core.Set#SET_BASE function FSM_SET:Get() - return self.Controllable + return self.Set end function FSM_SET:_call_handler( step, trigger, params, EventName ) diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index bf64fb2b6..43eb4af78 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -26,19 +26,28 @@ -- -- ### Authors: -- --- * FlightControl : Design & Programming +-- * FlightControl (Design & Programming) -- -- ### Contributions: +-- +-- * funkyfranky +-- * Applevangelist +-- +-- === -- -- @module Core.Point -- @image Core_Coordinate.JPG - - do -- COORDINATE --- @type COORDINATE + -- @field #string ClassName Name of the class + -- @field #number x Component of the 3D vector. + -- @field #number y Component of the 3D vector. + -- @field #number z Component of the 3D vector. + -- @field #number Heading Heading in degrees. Needs to be set first. + -- @field #number Velocity Velocity in meters per second. Needs to be set first. -- @extends Core.Base#BASE @@ -201,44 +210,69 @@ do -- COORDINATE ClassName = "COORDINATE", } - --- @field COORDINATE.WaypointAltType + --- Waypoint altitude types. + -- @type COORDINATE.WaypointAltType + -- @field #string BARO Barometric altitude. + -- @field #string RADIO Radio altitude. COORDINATE.WaypointAltType = { BARO = "BARO", RADIO = "RADIO", } - --- @field COORDINATE.WaypointAction + --- Waypoint actions. + -- @type COORDINATE.WaypointAction + -- @field #string TurningPoint Turning point. + -- @field #string FlyoverPoint Fly over point. + -- @field #string FromParkingArea From parking area. + -- @field #string FromParkingAreaHot From parking area hot. + -- @field #string FromGroundAreaHot From ground area hot. + -- @field #string FromGroundArea From ground area. + -- @field #string FromRunway From runway. + -- @field #string Landing Landing. + -- @field #string LandingReFuAr Landing and refuel and rearm. COORDINATE.WaypointAction = { TurningPoint = "Turning Point", FlyoverPoint = "Fly Over Point", FromParkingArea = "From Parking Area", FromParkingAreaHot = "From Parking Area Hot", + FromGroundAreaHot = "From Ground Area Hot", + FromGroundArea = "From Ground Area", FromRunway = "From Runway", Landing = "Landing", LandingReFuAr = "LandingReFuAr", } - --- @field COORDINATE.WaypointType + --- Waypoint types. + -- @type COORDINATE.WaypointType + -- @field #string TakeOffParking Take of parking. + -- @field #string TakeOffParkingHot Take of parking hot. + -- @field #string TakeOff Take off parking hot. + -- @field #string TakeOffGroundHot Take of from ground hot. + -- @field #string TurningPoint Turning point. + -- @field #string Land Landing point. + -- @field #string LandingReFuAr Landing and refuel and rearm. COORDINATE.WaypointType = { TakeOffParking = "TakeOffParking", TakeOffParkingHot = "TakeOffParkingHot", TakeOff = "TakeOffParkingHot", + TakeOffGroundHot = "TakeOffGroundHot", + TakeOffGround = "TakeOffGround", TurningPoint = "Turning Point", Land = "Land", - LandingReFuAr = "LandingReFuAr", + LandingReFuAr = "LandingReFuAr", } --- COORDINATE constructor. -- @param #COORDINATE self -- @param DCS#Distance x The x coordinate of the Vec3 point, pointing to the North. - -- @param DCS#Distance y The y coordinate of the Vec3 point, pointing to the Right. - -- @param DCS#Distance z The z coordinate of the Vec3 point, pointing to the Right. - -- @return #COORDINATE + -- @param DCS#Distance y The y coordinate of the Vec3 point, pointing to up. + -- @param DCS#Distance z The z coordinate of the Vec3 point, pointing to the right. + -- @return #COORDINATE self function COORDINATE:New( x, y, z ) - --env.info("FF COORDINATE New") - local self = BASE:Inherit( self, BASE:New() ) -- #COORDINATE + local self=BASE:Inherit(self, BASE:New()) -- #COORDINATE + self.x = x self.y = y self.z = z @@ -249,7 +283,7 @@ do -- COORDINATE --- COORDINATE constructor. -- @param #COORDINATE self -- @param #COORDINATE Coordinate. - -- @return #COORDINATE + -- @return #COORDINATE self function COORDINATE:NewFromCoordinate( Coordinate ) local self = BASE:Inherit( self, BASE:New() ) -- #COORDINATE @@ -263,8 +297,8 @@ do -- COORDINATE --- Create a new COORDINATE object from Vec2 coordinates. -- @param #COORDINATE self -- @param DCS#Vec2 Vec2 The Vec2 point. - -- @param DCS#Distance LandHeightAdd (optional) The default height if required to be evaluated will be the land height of the x, y coordinate. You can specify an extra height to be added to the land height. - -- @return #COORDINATE + -- @param DCS#Distance LandHeightAdd (Optional) The default height if required to be evaluated will be the land height of the x, y coordinate. You can specify an extra height to be added to the land height. + -- @return #COORDINATE self function COORDINATE:NewFromVec2( Vec2, LandHeightAdd ) local LandHeight = land.getHeight( Vec2 ) @@ -274,8 +308,6 @@ do -- COORDINATE local self = self:New( Vec2.x, LandHeight, Vec2.y ) -- #COORDINATE - self:F2( self ) - return self end @@ -283,7 +315,7 @@ do -- COORDINATE --- Create a new COORDINATE object from Vec3 coordinates. -- @param #COORDINATE self -- @param DCS#Vec3 Vec3 The Vec3 point. - -- @return #COORDINATE + -- @return #COORDINATE self function COORDINATE:NewFromVec3( Vec3 ) local self = self:New( Vec3.x, Vec3.y, Vec3.z ) -- #COORDINATE @@ -292,6 +324,22 @@ do -- COORDINATE return self end + + --- Create a new COORDINATE object from a waypoint. This uses the components + -- + -- * `waypoint.x` + -- * `waypoint.alt` + -- * `waypoint.y` + -- + -- @param #COORDINATE self + -- @param DCS#Waypoint Waypoint The waypoint. + -- @return #COORDINATE self + function COORDINATE:NewFromWaypoint(Waypoint) + + local self=self:New(Waypoint.x, Waypoint.alt, Waypoint.y) -- #COORDINATE + + return self + end --- Return the coordinates itself. Sounds stupid but can be useful for compatibility. -- @param #COORDINATE self @@ -647,7 +695,8 @@ do -- COORDINATE local y=X*math.sin(phi)+Y*math.cos(phi) -- Coordinate assignment looks bit strange but is correct. - return COORDINATE:NewFromVec3({x=y, y=self.y, z=x}) + local coord=COORDINATE:NewFromVec3({x=y, y=self.y, z=x}) + return coord end --- Return a random Vec2 within an Outer Radius and optionally NOT within an Inner Radius of the COORDINATE. @@ -690,7 +739,8 @@ do -- COORDINATE function COORDINATE:GetRandomCoordinateInRadius( OuterRadius, InnerRadius ) self:F2( { OuterRadius, InnerRadius } ) - return COORDINATE:NewFromVec2( self:GetRandomVec2InRadius( OuterRadius, InnerRadius ) ) + local coord=COORDINATE:NewFromVec2( self:GetRandomVec2InRadius( OuterRadius, InnerRadius ) ) + return coord end @@ -1611,8 +1661,8 @@ do -- COORDINATE if x and y then local vec2={ x = x, y = y } coord=COORDINATE:NewFromVec2(vec2) - end - return coord + end + return coord end @@ -1718,7 +1768,7 @@ do -- COORDINATE return self:GetSurfaceType()==land.SurfaceType.LAND end - --- Checks if the surface type is road. + --- Checks if the surface type is land. -- @param #COORDINATE self -- @return #boolean If true, the surface type at the coordinate is land. function COORDINATE:IsSurfaceTypeLand() @@ -1758,10 +1808,9 @@ do -- COORDINATE --- Creates an explosion at the point of a certain intensity. -- @param #COORDINATE self -- @param #number ExplosionIntensity Intensity of the explosion in kg TNT. Default 100 kg. - -- @param #number Delay Delay before explosion in seconds. + -- @param #number Delay (Optional) Delay before explosion is triggered in seconds. -- @return #COORDINATE self function COORDINATE:Explosion( ExplosionIntensity, Delay ) - self:F2( { ExplosionIntensity } ) ExplosionIntensity=ExplosionIntensity or 100 if Delay and Delay>0 then self:ScheduleOnce(Delay, self.Explosion, self, ExplosionIntensity) @@ -1773,11 +1822,17 @@ do -- COORDINATE --- Creates an illumination bomb at the point. -- @param #COORDINATE self - -- @param #number power Power of illumination bomb in Candela. + -- @param #number Power Power of illumination bomb in Candela. Default 1000 cd. + -- @param #number Delay (Optional) Delay before bomb is ignited in seconds. -- @return #COORDINATE self - function COORDINATE:IlluminationBomb(power) - self:F2() - trigger.action.illuminationBomb( self:GetVec3(), power ) + function COORDINATE:IlluminationBomb(Power, Delay) + Power=Power or 1000 + if Delay and Delay>0 then + self:ScheduleOnce(Delay, self.IlluminationBomb, self, Power) + else + trigger.action.illuminationBomb(self:GetVec3(), Power) + end + return self end @@ -2075,7 +2130,7 @@ do -- COORDINATE --- Circle to all. -- Creates a circle on the map with a given radius, color, fill color, and outline. -- @param #COORDINATE self - -- @param #numberr Radius Radius in meters. Default 1000 m. + -- @param #number Radius Radius in meters. Default 1000 m. -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red (default). -- @param #number Alpha Transparency [0,1]. Default 1. diff --git a/Moose Development/Moose/Core/ScheduleDispatcher.lua b/Moose Development/Moose/Core/ScheduleDispatcher.lua index db04a1500..c28891239 100644 --- a/Moose Development/Moose/Core/ScheduleDispatcher.lua +++ b/Moose Development/Moose/Core/ScheduleDispatcher.lua @@ -317,7 +317,7 @@ end --- Stop dispatcher. -- @param #SCHEDULEDISPATCHER self -- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object. --- @param #table CallID Call ID. +-- @param #string CallID (Optional) Scheduler Call ID. If nil, all pending schedules are stopped recursively. function SCHEDULEDISPATCHER:Stop( Scheduler, CallID ) self:F2( { Stop = CallID, Scheduler = Scheduler } ) diff --git a/Moose Development/Moose/Core/Scheduler.lua b/Moose Development/Moose/Core/Scheduler.lua index 781b90ebe..ee0950f22 100644 --- a/Moose Development/Moose/Core/Scheduler.lua +++ b/Moose Development/Moose/Core/Scheduler.lua @@ -238,7 +238,7 @@ end -- @param #number Stop Time interval in seconds after which the scheduler will be stoppe. -- @param #number TraceLevel Trace level [0,3]. Default 3. -- @param Core.Fsm#FSM Fsm Finite state model. --- @return #table The ScheduleID of the planned schedule. +-- @return #string The Schedule ID of the planned schedule. function SCHEDULER:Schedule( MasterObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop, TraceLevel, Fsm ) self:F2( { Start, Repeat, RandomizeFactor, Stop } ) self:T3( { SchedulerArguments } ) @@ -273,7 +273,7 @@ end --- (Re-)Starts the schedules or a specific schedule if a valid ScheduleID is provided. -- @param #SCHEDULER self --- @param #string ScheduleID (Optional) The ScheduleID of the planned (repeating) schedule. +-- @param #string ScheduleID (Optional) The Schedule ID of the planned (repeating) schedule. function SCHEDULER:Start( ScheduleID ) self:F3( { ScheduleID } ) self:T(string.format("Starting scheduler ID=%s", tostring(ScheduleID))) diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index 5230b20fe..0de8218b2 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -233,7 +233,9 @@ 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 } ) + + -- Debug info. + self:T( { 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 @@ -263,6 +265,32 @@ do -- SET_BASE end + --- Sort the set by name. + -- @param #SET_BASE self + -- @return Core.Base#BASE The added BASE Object. + function SET_BASE:SortByName() + + local function sort(a, b) + return a0 then + if self.Debug and duration>0 then MESSAGE:New(text, duration):ToAllIf(self.Debug) end self:T(self.lid..text) @@ -9047,11 +9105,11 @@ function WAREHOUSE:_GetFlightplan(asset, departure, destination) -- Hot start. if asset.takeoffType and asset.takeoffType==COORDINATE.WaypointType.TakeOffParkingHot then - env.info("FF hot") + --env.info("FF hot") _type=COORDINATE.WaypointType.TakeOffParkingHot _action=COORDINATE.WaypointAction.FromParkingAreaHot else - env.info("FF cold") + --env.info("FF cold") end diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index 1c4bd1c6a..30f02b45d 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -82,11 +82,19 @@ __Moose.Include( 'Scripts/Moose/Ops/OpsGroup.lua' ) __Moose.Include( 'Scripts/Moose/Ops/FlightGroup.lua' ) __Moose.Include( 'Scripts/Moose/Ops/NavyGroup.lua' ) __Moose.Include( 'Scripts/Moose/Ops/ArmyGroup.lua' ) +__Moose.Include( 'Scripts/Moose/Ops/Cohort.lua' ) __Moose.Include( 'Scripts/Moose/Ops/Squadron.lua' ) +__Moose.Include( 'Scripts/Moose/Ops/Platoon.lua' ) +__Moose.Include( 'Scripts/Moose/Ops/Legion.lua' ) __Moose.Include( 'Scripts/Moose/Ops/AirWing.lua' ) +__Moose.Include( 'Scripts/Moose/Ops/Brigade.lua' ) __Moose.Include( 'Scripts/Moose/Ops/Intelligence.lua' ) +__Moose.Include( 'Scripts/Moose/Ops/Commander.lua' ) +__Moose.Include( 'Scripts/Moose/Ops/OpsTransport.lua' ) __Moose.Include( 'Scripts/Moose/Ops/CSAR.lua' ) __Moose.Include( 'Scripts/Moose/Ops/CTLD.lua' ) +__Moose.Include( 'Scripts/Moose/Ops/OpsZone.lua' ) +__Moose.Include( 'Scripts/Moose/Ops/Chief.lua' ) __Moose.Include( 'Scripts/Moose/AI/AI_Balancer.lua' ) __Moose.Include( 'Scripts/Moose/AI/AI_Air.lua' ) diff --git a/Moose Development/Moose/Ops/AirWing.lua b/Moose Development/Moose/Ops/AirWing.lua index 4493264c9..d23079f07 100644 --- a/Moose Development/Moose/Ops/AirWing.lua +++ b/Moose Development/Moose/Ops/AirWing.lua @@ -19,7 +19,7 @@ -- @field #table menu Table of menu items. -- @field #table squadrons Table of squadrons. -- @field #table missionqueue Mission queue table. --- @field #table payloads Playloads for specific aircraft and mission types. +-- @field #table payloads Playloads for specific aircraft and mission types. -- @field #number payloadcounter Running index of payloads. -- @field Core.Set#SET_ZONE zonesetCAP Set of CAP zones. -- @field Core.Set#SET_ZONE zonesetTANKER Set of TANKER zones. @@ -27,82 +27,79 @@ -- @field #number nflightsCAP Number of CAP flights constantly in the air. -- @field #number nflightsAWACS Number of AWACS flights constantly in the air. -- @field #number nflightsTANKERboom Number of TANKER flights with BOOM constantly in the air. --- @field #number nflightsTANKERprobe Number of TANKER flights with PROBE constantly in the air. +-- @field #number nflightsTANKERprobe Number of TANKER flights with PROBE constantly in the air. -- @field #number nflightsRescueHelo Number of Rescue helo flights constantly in the air. -- @field #table pointsCAP Table of CAP points. -- @field #table pointsTANKER Table of Tanker points. -- @field #table pointsAWACS Table of AWACS points. -- @field #boolean markpoints Display markers on the F10 map. --- @field Ops.WingCommander#WINGCOMMANDER wingcommander The wing commander responsible for this airwing. --- +-- -- @field Ops.RescueHelo#RESCUEHELO rescuehelo The rescue helo. -- @field Ops.RecoveryTanker#RECOVERYTANKER recoverytanker The recoverytanker. --- --- @extends Functional.Warehouse#WAREHOUSE +-- +-- @extends Ops.Legion#LEGION --- Be surprised! -- -- === -- --- ![Banner Image](..\Presentations\OPS\AirWing\_Main.png) --- -- # The AIRWING Concept --- +-- -- An AIRWING consists of multiple SQUADRONS. These squadrons "live" in a WAREHOUSE, i.e. a physical structure that is connected to an airbase (airdrome, FRAP or ship). -- For an airwing to be operational, it needs airframes, weapons/fuel and an airbase. --- +-- -- # Create an Airwing --- +-- -- ## Constructing the Airwing --- +-- -- airwing=AIRWING:New("Warehouse Batumi", "8th Fighter Wing") -- airwing:Start() --- +-- -- The first parameter specified the warehouse, i.e. the static building housing the airwing (or the name of the aircraft carrier). The second parameter is optional -- and sets an alias. --- +-- -- ## Adding Squadrons --- +-- -- At this point the airwing does not have any assets (aircraft). In order to add these, one needs to first define SQUADRONS. --- +-- -- VFA151=SQUADRON:New("F-14 Group", 8, "VFA-151 (Vigilantes)") -- VFA151:AddMissionCapability({AUFTRAG.Type.GCICAP, AUFTRAG.Type.INTERCEPT}) --- +-- -- airwing:AddSquadron(VFA151) --- +-- -- This adds eight Tomcat groups beloning to VFA-151 to the airwing. This squadron has the ability to perform combat air patrols and intercepts. --- +-- -- ## Adding Payloads --- +-- -- Adding pure airframes is not enough. The aircraft also need weapons (and fuel) for certain missions. These must be given to the airwing from template groups -- defined in the Mission Editor. --- +-- -- -- F-14 payloads for CAP and INTERCEPT. Phoenix are first, sparrows are second choice. -- airwing:NewPayload(GROUP:FindByName("F-14 Payload AIM-54C"), 2, {AUFTRAG.Type.INTERCEPT, AUFTRAG.Type.GCICAP}, 80) -- airwing:NewPayload(GROUP:FindByName("F-14 Payload AIM-7M"), 20, {AUFTRAG.Type.INTERCEPT, AUFTRAG.Type.GCICAP}) --- +-- -- This will add two AIM-54C and 20 AIM-7M payloads. --- +-- -- If the airwing gets an intercept or patrol mission assigned, it will first use the AIM-54s. Once these are consumed, the AIM-7s are attached to the aircraft. --- +-- -- When an airwing does not have a payload for a certain mission type, the mission cannot be carried out. --- +-- -- You can set the number of payloads to "unlimited" by setting its quantity to -1. --- +-- -- # Adding Missions --- +-- -- Various mission types can be added easily via the AUFTRAG class. --- +-- -- Once you created an AUFTRAG you can add it to the AIRWING with the :AddMission(mission) function. --- --- This mission will be put into the AIRWING queue. Once the mission start time is reached and all resources (airframes and pylons) are available, the mission is started. +-- +-- This mission will be put into the AIRWING queue. Once the mission start time is reached and all resources (airframes and payloads) are available, the mission is started. -- If the mission stop time is over (and the mission is not finished), it will be cancelled and removed from the queue. This applies also to mission that were not even -- started. --- +-- -- # Command an Airwing --- +-- -- An airwing can receive missions from a WINGCOMMANDER. See docs of that class for details. --- +-- -- However, you are still free to add missions at anytime. -- -- @@ -112,25 +109,14 @@ AIRWING = { verbose = 0, lid = nil, menu = nil, - squadrons = {}, - missionqueue = {}, payloads = {}, payloadcounter = 0, pointsCAP = {}, pointsTANKER = {}, pointsAWACS = {}, - wingcommander = nil, markpoints = false, } ---- Squadron asset. --- @type AIRWING.SquadronAsset --- @field #AIRWING.Payload payload The payload of the asset. --- @field Ops.FlightGroup#FLIGHTGROUP flightgroup The flightgroup object. --- @field #string squadname Name of the squadron this asset belongs to. --- @field #number Treturned Time stamp when asset returned to the airwing. --- @extends Functional.Warehouse#WAREHOUSE.Assetitem - --- Payload data. -- @type AIRWING.Payload -- @field #number uid Unique payload ID. @@ -149,20 +135,47 @@ AIRWING = { -- @field #number heading Heading in degrees. -- @field #number leg Leg length in NM. -- @field #number speed Speed in knots. +-- @field #number refuelsystem Refueling system type: `0=Unit.RefuelingSystem.BOOM_AND_RECEPTACLE`, `1=Unit.RefuelingSystem.PROBE_AND_DROGUE`. -- @field #number noccupied Number of flights on this patrol point. -- @field Wrapper.Marker#MARKER marker F10 marker. +--- Patrol zone. +-- @type AIRWING.PatrolZone +-- @field Core.Zone#ZONE zone Zone. +-- @field #number altitude Altitude in feet. +-- @field #number heading Heading in degrees. +-- @field #number leg Leg length in NM. +-- @field #number speed Speed in knots. +-- @field Ops.Auftrag#AUFTRAG mission Mission assigned. +-- @field Wrapper.Marker#MARKER marker F10 marker. + +--- AWACS zone. +-- @type AIRWING.AwacsZone +-- @field Core.Zone#ZONE zone Zone. +-- @field #number altitude Altitude in feet. +-- @field #number heading Heading in degrees. +-- @field #number leg Leg length in NM. +-- @field #number speed Speed in knots. +-- @field Ops.Auftrag#AUFTRAG mission Mission assigned. +-- @field Wrapper.Marker#MARKER marker F10 marker. + +--- Tanker zone. +-- @type AIRWING.TankerZone +-- @field #number refuelsystem Refueling system type: `0=Unit.RefuelingSystem.BOOM_AND_RECEPTACLE`, `1=Unit.RefuelingSystem.PROBE_AND_DROGUE`. +-- @extends #AIRWING.PatrolZone + --- AIRWING class version. -- @field #string version -AIRWING.version="0.5.2" +AIRWING.version="0.9.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Spawn in air or hot ==> Needs WAREHOUSE update. +-- TODO: Spawn in air ==> Needs WAREHOUSE update. +-- DONE: Spawn in air. -- TODO: Make special request to transfer squadrons to anther airwing (or warehouse). --- TODO: Check that airbase has enough parking spots if a request is BIG. Alternatively, split requests. +-- TODO: Check that airbase has enough parking spots if a request is BIG. -- DONE: Add squadrons to warehouse. -- DONE: Build mission queue. -- DONE: Find way to start missions. @@ -182,8 +195,8 @@ AIRWING.version="0.5.2" -- @return #AIRWING self function AIRWING:New(warehousename, airwingname) - -- Inherit everything from WAREHOUSE class. - local self=BASE:Inherit(self, WAREHOUSE:New(warehousename, airwingname)) -- #AIRWING + -- Inherit everything from LEGION class. + local self=BASE:Inherit(self, LEGION:New(warehousename, airwingname)) -- #AIRWING -- Nil check. if not self then @@ -193,25 +206,19 @@ function AIRWING:New(warehousename, airwingname) -- Set some string id for output to DCS.log file. self.lid=string.format("AIRWING %s | ", self.alias) - - -- Add FSM transitions. - -- From State --> Event --> To State - self:AddTransition("*", "MissionRequest", "*") -- Add a (mission) request to the warehouse. - self:AddTransition("*", "MissionCancel", "*") -- Cancel mission. - self:AddTransition("*", "SquadAssetReturned", "*") -- Flight was spawned with a mission. - - self:AddTransition("*", "FlightOnMission", "*") -- Flight was spawned with a mission. - -- Defaults: - --self:SetVerbosity(0) self.nflightsCAP=0 self.nflightsAWACS=0 self.nflightsTANKERboom=0 self.nflightsTANKERprobe=0 self.nflightsRecoveryTanker=0 self.nflightsRescueHelo=0 - self.markpoints=false + self.markpoints=false + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("*", "FlightOnMission", "*") -- A FLIGHTGROUP was send on a Mission (AUFTRAG). ------------------------ --- Pseudo Functions --- @@ -226,6 +233,7 @@ function AIRWING:New(warehousename, airwingname) -- @param #AIRWING self -- @param #number delay Delay in seconds. + --- Triggers the FSM event "Stop". Stops the AIRWING and all its event handlers. -- @param #AIRWING self @@ -233,24 +241,29 @@ function AIRWING:New(warehousename, airwingname) -- @function [parent=#AIRWING] __Stop -- @param #AIRWING self -- @param #number delay Delay in seconds. - - --- On after "FlightOnMission" event. Triggered when an asset group starts a mission. - -- @function [parent=#AIRWING] OnAfterFlightOnMission - -- @param #AIRWING self - -- @param #string From The From state - -- @param #string Event The Event called - -- @param #string To The To state - -- @param Ops.FlightGroup#FLIGHTGROUP Flightgroup The Flightgroup on mission - -- @param Ops.Auftrag#AUFTRAG Mission The Auftrag of the Flightgroup - - --- On after "AssetReturned" event. Triggered when an asset group returned to its airwing. - -- @function [parent=#AIRWING] OnAfterAssetReturned - -- @param #AIRWING self - -- @param #string From From state. - -- @param #string Event Event. - -- @param #string To To state. - -- @param Ops.Squadron#SQUADRON Squadron The asset squadron. - -- @param #AIRWING.SquadronAsset Asset The asset that returned. + + + --- Triggers the FSM event "FlightOnMission". + -- @function [parent=#AIRWING] FlightOnMission + -- @param #AIRWING self + -- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup The FLIGHTGROUP on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "FlightOnMission" after a delay. + -- @function [parent=#AIRWING] __FlightOnMission + -- @param #AIRWING self + -- @param #number delay Delay in seconds. + -- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup The FLIGHTGROUP on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "FlightOnMission" event. + -- @function [parent=#AIRWING] OnAfterFlightOnMission + -- @param #AIRWING self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup The FLIGHTGROUP on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. return self end @@ -266,11 +279,11 @@ end function AIRWING:AddSquadron(Squadron) -- Add squadron to airwing. - table.insert(self.squadrons, Squadron) - + table.insert(self.cohorts, Squadron) + -- Add assets to squadron. self:AddAssetToSquadron(Squadron, Squadron.Ngroups) - + -- Tanker and AWACS get unlimited payloads. if Squadron.attribute==GROUP.Attribute.AIR_AWACS then self:NewPayload(Squadron.templategroup, -1, AUFTRAG.Type.AWACS) @@ -280,7 +293,7 @@ function AIRWING:AddSquadron(Squadron) -- Set airwing to squadron. Squadron:SetAirwing(self) - + -- Start squadron. if Squadron:IsStopped() then Squadron:Start() @@ -315,17 +328,17 @@ function AIRWING:NewPayload(Unit, Npayloads, MissionTypes, Performance) if Unit:IsInstanceOf("GROUP") then Unit=Unit:GetUnit(1) end - + -- Ensure Missiontypes is a table. if MissionTypes and type(MissionTypes)~="table" then MissionTypes={MissionTypes} end - + -- Create payload. local payload={} --#AIRWING.Payload payload.uid=self.payloadcounter payload.unitname=Unit:GetName() - payload.aircrafttype=Unit:GetTypeName() + payload.aircrafttype=Unit:GetTypeName() payload.pylons=Unit:GetTemplatePayload() payload.unlimited=Npayloads<0 if payload.unlimited then @@ -333,7 +346,7 @@ function AIRWING:NewPayload(Unit, Npayloads, MissionTypes, Performance) else payload.navail=Npayloads or 99 end - + payload.capabilities={} for _,missiontype in pairs(MissionTypes) do local capability={} --Ops.Auftrag#AUFTRAG.Capability @@ -341,27 +354,27 @@ function AIRWING:NewPayload(Unit, Npayloads, MissionTypes, Performance) capability.Performance=Performance table.insert(payload.capabilities, capability) end - - -- Add ORBIT for all. - if not self:CheckMissionType(AUFTRAG.Type.ORBIT, MissionTypes) then + + -- Add ORBIT for all. + if not AUFTRAG.CheckMissionType(AUFTRAG.Type.ORBIT, MissionTypes) then local capability={} --Ops.Auftrag#AUFTRAG.Capability capability.MissionType=AUFTRAG.Type.ORBIT capability.Performance=50 table.insert(payload.capabilities, capability) - end - + end + -- Info - self:T(self.lid..string.format("Adding new payload from unit %s for aircraft type %s: ID=%d, N=%d (unlimited=%s), performance=%d, missions: %s", + self:T(self.lid..string.format("Adding new payload from unit %s for aircraft type %s: ID=%d, N=%d (unlimited=%s), performance=%d, missions: %s", payload.unitname, payload.aircrafttype, payload.uid, payload.navail, tostring(payload.unlimited), Performance, table.concat(MissionTypes, ", "))) -- Add payload table.insert(self.payloads, payload) - + -- Increase counter self.payloadcounter=self.payloadcounter+1 - + return payload - + end self:E(self.lid.."ERROR: No UNIT found to create PAYLOAD!") @@ -382,15 +395,15 @@ function AIRWING:AddPayloadCapability(Payload, MissionTypes, Performance) end Payload.capabilities=Payload.capabilities or {} - + for _,missiontype in pairs(MissionTypes) do - + local capability={} --Ops.Auftrag#AUFTRAG.Capability capability.MissionType=missiontype capability.Performance=Performance - + --TODO: check that capability does not already exist! - + table.insert(Payload.capabilities, capability) end @@ -411,7 +424,7 @@ function AIRWING:FetchPayloadFromStock(UnitType, MissionType, Payloads) self:T(self.lid.."WARNING: No payloads in stock!") return nil end - + -- Debug. if self.verbose>=4 then self:I(self.lid..string.format("Looking for payload for unit type=%s and mission type=%s", UnitType, MissionType)) @@ -424,7 +437,7 @@ function AIRWING:FetchPayloadFromStock(UnitType, MissionType, Payloads) --- Sort payload wrt the following criteria: -- 1) Highest performance is the main selection criterion. - -- 2) If payloads have the same performance, unlimited payloads are preferred over limited ones. + -- 2) If payloads have the same performance, unlimited payloads are preferred over limited ones. -- 3) If payloads have the same performance _and_ are limited, the more abundant one is preferred. local function sortpayloads(a,b) local pA=a --#AIRWING.Payload @@ -457,7 +470,7 @@ function AIRWING:FetchPayloadFromStock(UnitType, MissionType, Payloads) return nil end return false - end + end -- Pre-selection: filter out only those payloads that are valid for the airframe and mission type and are available. local payloads={} @@ -465,31 +478,31 @@ function AIRWING:FetchPayloadFromStock(UnitType, MissionType, Payloads) local payload=_payload --#AIRWING.Payload local specialpayload=_checkPayloads(payload) - local compatible=self:CheckMissionCapability(MissionType, payload.capabilities) - + local compatible=AUFTRAG.CheckMissionCapability(MissionType, payload.capabilities) + local goforit = specialpayload or (specialpayload==nil and compatible) if payload.aircrafttype==UnitType and payload.navail>0 and goforit then table.insert(payloads, payload) end end - + -- Debug. if self.verbose>=4 then - self:I(self.lid..string.format("Sorted payloads for mission type X and aircraft type=Y:")) + self:I(self.lid..string.format("Sorted payloads for mission type %s and aircraft type=%s:", MissionType, UnitType)) for _,_payload in ipairs(self.payloads) do local payload=_payload --#AIRWING.Payload - if payload.aircrafttype==UnitType and self:CheckMissionCapability(MissionType, payload.capabilities) then + if payload.aircrafttype==UnitType and AUFTRAG.CheckMissionCapability(MissionType, payload.capabilities) then local performace=self:GetPayloadPeformance(payload, MissionType) - self:I(self.lid..string.format("FF %s payload for %s: avail=%d performace=%d", MissionType, payload.aircrafttype, payload.navail, performace)) + self:I(self.lid..string.format("- %s payload for %s: avail=%d performace=%d", MissionType, payload.aircrafttype, payload.navail, performace)) end end end - + -- Cases: if #payloads==0 then -- No payload available. - self:T(self.lid.."Warning could not find a payload for airframe X mission type Y!") + self:T(self.lid..string.format("WARNING: Could not find a payload for airframe %s mission type %s!", UnitType, MissionType)) return nil elseif #payloads==1 then -- Only one payload anyway. @@ -504,21 +517,21 @@ function AIRWING:FetchPayloadFromStock(UnitType, MissionType, Payloads) local payload=payloads[1] --#AIRWING.Payload if not payload.unlimited then payload.navail=payload.navail-1 - end + end return payload end - + end --- Return payload from asset back to stock. -- @param #AIRWING self --- @param #AIRWING.SquadronAsset asset The squadron asset. +-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset The squadron asset. function AIRWING:ReturnPayloadFromAsset(asset) local payload=asset.payload - + if payload then - + -- Increase count if not unlimited. if not payload.unlimited then payload.navail=payload.navail+1 @@ -526,11 +539,11 @@ function AIRWING:ReturnPayloadFromAsset(asset) -- Remove asset payload. asset.payload=nil - + else self:E(self.lid.."ERROR: asset had no payload attached!") end - + end @@ -542,23 +555,23 @@ end function AIRWING:AddAssetToSquadron(Squadron, Nassets) if Squadron then - + -- Get the template group of the squadron. local Group=GROUP:FindByName(Squadron.templatename) - + if Group then - + -- Debug text. local text=string.format("Adding asset %s to squadron %s", Group:GetName(), Squadron.name) self:T(self.lid..text) - + -- Add assets to airwing warehouse. self:AddAsset(Group, Nassets, nil, nil, nil, nil, Squadron.skill, Squadron.livery, Squadron.name) - + else self:E(self.lid.."ERROR: Group does not exist!") end - + else self:E(self.lid.."ERROR: Squadron does not exit!") end @@ -571,31 +584,13 @@ end -- @param #string SquadronName Name of the squadron, e.g. "VFA-37". -- @return Ops.Squadron#SQUADRON The squadron object. function AIRWING:GetSquadron(SquadronName) - - for _,_squadron in pairs(self.squadrons) do - local squadron=_squadron --Ops.Squadron#SQUADRON - - if squadron.name==SquadronName then - return squadron - end - - end - - return nil -end - ---- Set verbosity level. --- @param #AIRWING self --- @param #number VerbosityLevel Level of output (higher=more). Default 0. --- @return #AIRWING self -function AIRWING:SetVerbosity(VerbosityLevel) - self.verbose=VerbosityLevel or 0 - return self + local squad=self:_GetCohort(SquadronName) + return squad end --- Get squadron of an asset. -- @param #AIRWING self --- @param #AIRWING.SquadronAsset Asset The squadron asset. +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The squadron asset. -- @return Ops.Squadron#SQUADRON The squadron object. function AIRWING:GetSquadronOfAsset(Asset) return self:GetSquadron(Asset.squadname) @@ -603,7 +598,7 @@ end --- Remove asset from squadron. -- @param #AIRWING self --- @param #AIRWING.SquadronAsset Asset The squad asset. +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The squad asset. function AIRWING:RemoveAssetFromSquadron(Asset) local squad=self:GetSquadronOfAsset(Asset) if squad then @@ -611,45 +606,6 @@ function AIRWING:RemoveAssetFromSquadron(Asset) end end ---- Add mission to queue. --- @param #AIRWING self --- @param Ops.Auftrag#AUFTRAG Mission for this group. --- @return #AIRWING self -function AIRWING:AddMission(Mission) - - -- Set status to QUEUED. This also attaches the airwing to this mission. - Mission:Queued(self) - - -- Add mission to queue. - table.insert(self.missionqueue, Mission) - - -- Info text. - local text=string.format("Added mission %s (type=%s). Starting at %s. Stopping at %s", - tostring(Mission.name), tostring(Mission.type), UTILS.SecondsToClock(Mission.Tstart, true), Mission.Tstop and UTILS.SecondsToClock(Mission.Tstop, true) or "INF") - self:T(self.lid..text) - - return self -end - ---- Remove mission from queue. --- @param #AIRWING self --- @param Ops.Auftrag#AUFTRAG Mission Mission to be removed. --- @return #AIRWING self -function AIRWING:RemoveMission(Mission) - - for i,_mission in pairs(self.missionqueue) do - local mission=_mission --Ops.Auftrag#AUFTRAG - - if mission.auftragsnummer==Mission.auftragsnummer then - table.remove(self.missionqueue, i) - break - end - - end - - return self -end - --- Set number of CAP flights constantly carried out. -- @param #AIRWING self -- @param #number n Number of flights. Default 1. @@ -708,13 +664,13 @@ function AIRWING:SetNumberRescuehelo(n) return self end ---- +--- -- @param #AIRWING self -- @param #AIRWING.PatrolData point Patrol point table. -- @return #string Marker text. function AIRWING:_PatrolPointMarkerText(point) - local text=string.format("%s Occupied=%d, \nheading=%03d, leg=%d NM, alt=%d ft, speed=%d kts", + local text=string.format("%s Occupied=%d, \nheading=%03d, leg=%d NM, alt=%d ft, speed=%d kts", point.type, point.noccupied, point.heading, point.leg, point.altitude, point.speed) return text @@ -724,9 +680,9 @@ end -- @param #AIRWING.PatrolData point Patrol point table. function AIRWING:UpdatePatrolPointMarker(point) if self.markpoints then -- sometimes there's a direct call from #OPSGROUP - local text=string.format("%s Occupied=%d\nheading=%03d, leg=%d NM, alt=%d ft, speed=%d kts", + local text=string.format("%s Occupied=%d\nheading=%03d, leg=%d NM, alt=%d ft, speed=%d kts", point.type, point.noccupied, point.heading, point.leg, point.altitude, point.speed) - + point.marker:UpdateText(text, 1) end end @@ -740,8 +696,14 @@ end -- @param #number Heading Heading in degrees. Default random (0, 360] degrees. -- @param #number LegLength Length of race-track orbit in NM. Default 15 NM. -- @param #number Speed Orbit speed in knots. Default 350 knots. +-- @param #number RefuelSystem Refueling system: 0=Boom, 1=Probe. Default nil=any. -- @return #AIRWING.PatrolData Patrol point table. -function AIRWING:NewPatrolPoint(Type, Coordinate, Altitude, Speed, Heading, LegLength) +function AIRWING:NewPatrolPoint(Type, Coordinate, Altitude, Speed, Heading, LegLength, RefuelSystem) + + -- Check if a zone was passed instead of a coordinate. + if Coordinate:IsInstanceOf("ZONE_BASE") then + Coordinate=Coordinate:GetCoordinate() + end local patrolpoint={} --#AIRWING.PatrolData patrolpoint.type=Type or "Unknown" @@ -751,12 +713,13 @@ function AIRWING:NewPatrolPoint(Type, Coordinate, Altitude, Speed, Heading, LegL patrolpoint.altitude=Altitude or math.random(10,20)*1000 patrolpoint.speed=Speed or 350 patrolpoint.noccupied=0 - + patrolpoint.refuelsystem=RefuelSystem + if self.markpoints then patrolpoint.marker=MARKER:New(Coordinate, "New Patrol Point"):ToAll() AIRWING.UpdatePatrolPointMarker(patrolpoint) end - + return patrolpoint end @@ -769,7 +732,7 @@ end -- @param #number LegLength Length of race-track orbit in NM. -- @return #AIRWING self function AIRWING:AddPatrolPointCAP(Coordinate, Altitude, Speed, Heading, LegLength) - + local patrolpoint=self:NewPatrolPoint("CAP", Coordinate, Altitude, Speed, Heading, LegLength) table.insert(self.pointsCAP, patrolpoint) @@ -784,10 +747,11 @@ end -- @param #number Speed Orbit speed in knots. -- @param #number Heading Heading in degrees. -- @param #number LegLength Length of race-track orbit in NM. +-- @param #number RefuelSystem Set refueling system of tanker: 0=boom, 1=probe. Default any (=nil). -- @return #AIRWING self -function AIRWING:AddPatrolPointTANKER(Coordinate, Altitude, Speed, Heading, LegLength) - - local patrolpoint=self:NewPatrolPoint("Tanker", Coordinate, Altitude, Speed, Heading, LegLength) +function AIRWING:AddPatrolPointTANKER(Coordinate, Altitude, Speed, Heading, LegLength, RefuelSystem) + + local patrolpoint=self:NewPatrolPoint("Tanker", Coordinate, Altitude, Speed, Heading, LegLength, RefuelSystem) table.insert(self.pointsTANKER, patrolpoint) @@ -803,7 +767,7 @@ end -- @param #number LegLength Length of race-track orbit in NM. -- @return #AIRWING self function AIRWING:AddPatrolPointAWACS(Coordinate, Altitude, Speed, Heading, LegLength) - + local patrolpoint=self:NewPatrolPoint("AWACS", Coordinate, Altitude, Speed, Heading, LegLength) table.insert(self.pointsAWACS, patrolpoint) @@ -820,7 +784,7 @@ end function AIRWING:onafterStart(From, Event, To) -- Start parent Warehouse. - self:GetParent(self).onafterStart(self, From, Event, To) + self:GetParent(self, AIRWING).onafterStart(self, From, Event, To) -- Info. self:I(self.lid..string.format("Starting AIRWING v%s", AIRWING.version)) @@ -835,39 +799,53 @@ function AIRWING:onafterStatus(From, Event, To) self:GetParent(self).onafterStatus(self, From, Event, To) local fsmstate=self:GetState() - + -- Check CAP missions. self:CheckCAP() - + -- Check TANKER missions. self:CheckTANKER() - + -- Check AWACS missions. self:CheckAWACS() - + -- Check Rescue Helo missions. self:CheckRescuhelo() - - + + ---------------- + -- Transport --- + ---------------- + + -- Check transport queue. + self:CheckTransportQueue() + + -------------- + -- Mission --- + -------------- + + -- Check mission queue. + self:CheckMissionQueue() + + -- General info: if self.verbose>=1 then -- Count missions not over yet. local Nmissions=self:CountMissionsInQueue() - + -- Count ALL payloads in stock. If any payload is unlimited, this gives 999. local Npayloads=self:CountPayloadsInStock(AUFTRAG.Type) - + -- Assets tot local Npq, Np, Nq=self:CountAssetsOnMission() - + local assets=string.format("%d (OnMission: Total=%d, Active=%d, Queued=%d)", self:CountAssets(), Npq, Np, Nq) -- Output. - local text=string.format("%s: Missions=%d, Payloads=%d (%d), Squads=%d, Assets=%s", fsmstate, Nmissions, Npayloads, #self.payloads, #self.squadrons, assets) + local text=string.format("%s: Missions=%d, Payloads=%d (%d), Squads=%d, Assets=%s", fsmstate, Nmissions, Npayloads, #self.payloads, #self.cohorts, assets) self:I(self.lid..text) end - + ------------------ -- Mission Info -- ------------------ @@ -875,56 +853,43 @@ function AIRWING:onafterStatus(From, Event, To) local text=string.format("Missions Total=%d:", #self.missionqueue) for i,_mission in pairs(self.missionqueue) do local mission=_mission --Ops.Auftrag#AUFTRAG - + local prio=string.format("%d/%s", mission.prio, tostring(mission.importance)) ; if mission.urgent then prio=prio.." (!)" end - local assets=string.format("%d/%d", mission:CountOpsGroups(), mission.nassets) + local assets=string.format("%d/%d", mission:CountOpsGroups(), mission:GetNumberOfRequiredAssets()) local target=string.format("%d/%d Damage=%.1f", mission:CountMissionTargets(), mission:GetTargetInitialNumber(), mission:GetTargetDamage()) - - text=text..string.format("\n[%d] %s %s: Status=%s, Prio=%s, Assets=%s, Targets=%s", i, mission.name, mission.type, mission.status, prio, assets, target) + local mystatus=mission:GetLegionStatus(self) + + text=text..string.format("\n[%d] %s %s: Status=%s [%s], Prio=%s, Assets=%s, Targets=%s", i, mission.name, mission.type, mystatus, mission.status, prio, assets, target) end self:I(self.lid..text) end - + ------------------- -- Squadron Info -- ------------------- if self.verbose>=3 then local text="Squadrons:" - for i,_squadron in pairs(self.squadrons) do + for i,_squadron in pairs(self.cohorts) do local squadron=_squadron --Ops.Squadron#SQUADRON - + local callsign=squadron.callsignName and UTILS.GetCallsignName(squadron.callsignName) or "N/A" local modex=squadron.modex and squadron.modex or -1 local skill=squadron.skill and tostring(squadron.skill) or "N/A" - + -- Squadron text - text=text..string.format("\n* %s %s: %s*%d/%d, Callsign=%s, Modex=%d, Skill=%s", squadron.name, squadron:GetState(), squadron.aircrafttype, squadron:CountAssetsInStock(), #squadron.assets, callsign, modex, skill) + text=text..string.format("\n* %s %s: %s*%d/%d, Callsign=%s, Modex=%d, Skill=%s", squadron.name, squadron:GetState(), squadron.aircrafttype, squadron:CountAssets(true), #squadron.assets, callsign, modex, skill) end self:I(self.lid..text) end - - -------------- - -- Mission --- - -------------- - - -- Check if any missions should be cancelled. - self:_CheckMissions() - - -- Get next mission. - local mission=self:_GetNextMission() - - -- Request mission execution. - if mission then - self:MissionRequest(mission) - end end ---- Get patrol data +--- Get patrol data. -- @param #AIRWING self -- @param #table PatrolPoints Patrol data points. --- @return #AIRWING.PatrolData -function AIRWING:_GetPatrolData(PatrolPoints) +-- @param #number RefuelSystem If provided, only return points with the specific refueling system. +-- @return #AIRWING.PatrolData Patrol point data table. +function AIRWING:_GetPatrolData(PatrolPoints, RefuelSystem) -- Sort wrt lowest number of flights on this point. local function sort(a,b) @@ -932,17 +897,21 @@ function AIRWING:_GetPatrolData(PatrolPoints) end if PatrolPoints and #PatrolPoints>0 then - + -- Sort data wrt number of flights at that point. table.sort(PatrolPoints, sort) - return PatrolPoints[1] - else - - return self:NewPatrolPoint() - + for _,_patrolpoint in pairs(PatrolPoints) do + local patrolpoint=_patrolpoint --#AIRWING.PatrolData + if (RefuelSystem and patrolpoint.refuelsystem and RefuelSystem==patrolpoint.refuelsystem) or RefuelSystem==nil or patrolpoint.refuelsystem==nil then + return patrolpoint + end + end + end - + + -- Return a new point. + return self:NewPatrolPoint() end --- Check how many CAP missions are assigned and add number of missing missions. @@ -951,25 +920,25 @@ end function AIRWING:CheckCAP() local Ncap=self:CountMissionsInQueue({AUFTRAG.Type.GCICAP, AUFTRAG.Type.INTERCEPT}) - + for i=1,self.nflightsCAP-Ncap do - + local patrol=self:_GetPatrolData(self.pointsCAP) - + local altitude=patrol.altitude+1000*patrol.noccupied - + local missionCAP=AUFTRAG:NewGCICAP(patrol.coord, altitude, patrol.speed, patrol.heading, patrol.leg) - + missionCAP.patroldata=patrol - + patrol.noccupied=patrol.noccupied+1 - + if self.markpoints then AIRWING.UpdatePatrolPointMarker(patrol) end - + self:AddMission(missionCAP) - + end - + return self end @@ -980,58 +949,60 @@ function AIRWING:CheckTANKER() local Nboom=0 local Nprob=0 - - -- Count tanker mission. + + -- Count tanker missions. for _,_mission in pairs(self.missionqueue) do local mission=_mission --Ops.Auftrag#AUFTRAG - - if mission:IsNotOver() and mission.type==AUFTRAG.Type.TANKER then - if mission.refuelSystem==0 then + + if mission:IsNotOver() and mission.type==AUFTRAG.Type.TANKER and mission.patroldata then + if mission.refuelSystem==Unit.RefuelingSystem.BOOM_AND_RECEPTACLE then Nboom=Nboom+1 - elseif mission.refuelSystem==1 then + elseif mission.refuelSystem==Unit.RefuelingSystem.PROBE_AND_DROGUE then Nprob=Nprob+1 end - + end - + end - + + -- Check missing boom tankers. for i=1,self.nflightsTANKERboom-Nboom do - + local patrol=self:_GetPatrolData(self.pointsTANKER) - + local altitude=patrol.altitude+1000*patrol.noccupied - - local mission=AUFTRAG:NewTANKER(patrol.coord, altitude, patrol.speed, patrol.heading, patrol.leg, 1) - + + local mission=AUFTRAG:NewTANKER(patrol.coord, altitude, patrol.speed, patrol.heading, patrol.leg, Unit.RefuelingSystem.BOOM_AND_RECEPTACLE) + mission.patroldata=patrol - + patrol.noccupied=patrol.noccupied+1 - + if self.markpoints then AIRWING.UpdatePatrolPointMarker(patrol) end - + self:AddMission(mission) - + end - + + -- Check missing probe tankers. for i=1,self.nflightsTANKERprobe-Nprob do - + local patrol=self:_GetPatrolData(self.pointsTANKER) - + local altitude=patrol.altitude+1000*patrol.noccupied - - local mission=AUFTRAG:NewTANKER(patrol.coord, altitude, patrol.speed, patrol.heading, patrol.leg, 0) - + + local mission=AUFTRAG:NewTANKER(patrol.coord, altitude, patrol.speed, patrol.heading, patrol.leg, Unit.RefuelingSystem.PROBE_AND_DROGUE) + mission.patroldata=patrol - + patrol.noccupied=patrol.noccupied+1 - + if self.markpoints then AIRWING.UpdatePatrolPointMarker(patrol) end - + self:AddMission(mission) - - end - + + end + return self end @@ -1041,25 +1012,25 @@ end function AIRWING:CheckAWACS() local N=self:CountMissionsInQueue({AUFTRAG.Type.AWACS}) - + for i=1,self.nflightsAWACS-N do - + local patrol=self:_GetPatrolData(self.pointsAWACS) - + local altitude=patrol.altitude+1000*patrol.noccupied - + local mission=AUFTRAG:NewAWACS(patrol.coord, altitude, patrol.speed, patrol.heading, patrol.leg) - + mission.patroldata=patrol - + patrol.noccupied=patrol.noccupied+1 - + if self.markpoints then AIRWING.UpdatePatrolPointMarker(patrol) end - + self:AddMission(mission) - + end - + return self end @@ -1069,55 +1040,55 @@ end function AIRWING:CheckRescuhelo() local N=self:CountMissionsInQueue({AUFTRAG.Type.RESCUEHELO}) - + local name=self.airbase:GetName() - + local carrier=UNIT:FindByName(name) - + for i=1,self.nflightsRescueHelo-N do - + local mission=AUFTRAG:NewRESCUEHELO(carrier) - + self:AddMission(mission) - + end - + return self end --- Check how many AWACS missions are assigned and add number of missing missions. -- @param #AIRWING self -- @param Ops.FlightGroup#FLIGHTGROUP flightgroup The flightgroup. --- @return #AIRWING.SquadronAsset The tanker asset. +-- @return Functional.Warehouse#WAREHOUSE.Assetitem The tanker asset. function AIRWING:GetTankerForFlight(flightgroup) local tankers=self:GetAssetsOnMission(AUFTRAG.Type.TANKER) - + if #tankers>0 then - + local tankeropt={} for _,_tanker in pairs(tankers) do - local tanker=_tanker --#AIRWING.SquadronAsset - + local tanker=_tanker --Functional.Warehouse#WAREHOUSE.Assetitem + -- Check that donor and acceptor use the same refuelling system. if flightgroup.refueltype and flightgroup.refueltype==tanker.flightgroup.tankertype then - + local tankercoord=tanker.flightgroup.group:GetCoordinate() local assetcoord=flightgroup.group:GetCoordinate() - + local dist=assetcoord:Get2DDistance(tankercoord) - + -- Ensure that the flight does not find itself. Asset could be a tanker! if dist>5 then table.insert(tankeropt, {tanker=tanker, dist=dist}) end - + end end - + -- Sort tankers wrt to distance. table.sort(tankeropt, function(a,b) return a.dist0 then return tankeropt[1].tanker @@ -1129,782 +1100,26 @@ function AIRWING:GetTankerForFlight(flightgroup) return nil end - ---- Check if mission is not over and ready to cancel. --- @param #AIRWING self -function AIRWING:_CheckMissions() - - -- Loop over missions in queue. - for _,_mission in pairs(self.missionqueue) do - local mission=_mission --Ops.Auftrag#AUFTRAG - - if mission:IsNotOver() and mission:IsReadyToCancel() then - mission:Cancel() - end - end - -end ---- Get next mission. --- @param #AIRWING self --- @return Ops.Auftrag#AUFTRAG Next mission or *nil*. -function AIRWING:_GetNextMission() - - -- Number of missions. - local Nmissions=#self.missionqueue - - -- Treat special cases. - if Nmissions==0 then - return nil - end - - -- Sort results table wrt prio and start time. - local function _sort(a, b) - local taskA=a --Ops.Auftrag#AUFTRAG - local taskB=b --Ops.Auftrag#AUFTRAG - return (taskA.prio0 then - self:E(self.lid..string.format("ERROR: mission %s of type %s has already assets attached!", mission.name, mission.type)) - end - mission.assets={} - - -- Assign assets to mission. - for i=1,mission.nassets do - local asset=assets[i] --#AIRWING.SquadronAsset - - -- Should not happen as we just checked! - if not asset.payload then - self:E(self.lid.."ERROR: No payload for asset! This should not happen!") - end - - -- Add asset to mission. - mission:AddAsset(asset) - end - - -- Now return the remaining payloads. - for i=mission.nassets+1,#assets do - local asset=assets[i] --#AIRWING.SquadronAsset - for _,uid in pairs(gotpayload) do - if uid==asset.uid then - self:ReturnPayloadFromAsset(asset) - break - end - end - end - - return mission - end - - end -- mission due? - end -- mission loop - - return nil -end - ---- Calculate the mission score of an asset. --- @param #AIRWING self --- @param #AIRWING.SquadronAsset asset Asset --- @param Ops.Auftrag#AUFTRAG Mission Mission for which the best assets are desired. --- @param #boolean includePayload If true, include the payload in the calulation if the asset has one attached. --- @return #number Mission score. -function AIRWING:CalculateAssetMissionScore(asset, Mission, includePayload) - - local score=0 - - -- Prefer highly skilled assets. - if asset.skill==AI.Skill.AVERAGE then - score=score+0 - elseif asset.skill==AI.Skill.GOOD then - score=score+10 - elseif asset.skill==AI.Skill.HIGH then - score=score+20 - elseif asset.skill==AI.Skill.EXCELLENT then - score=score+30 - end - - -- Add mission performance to score. - local squad=self:GetSquadronOfAsset(asset) - local missionperformance=squad:GetMissionPeformance(Mission.type) - score=score+missionperformance - - -- Add payload performance to score. - if includePayload and asset.payload then - score=score+self:GetPayloadPeformance(asset.payload, Mission.type) - end - - -- Intercepts need to be carried out quickly. We prefer spawned assets. - if Mission.type==AUFTRAG.Type.INTERCEPT then - if asset.spawned then - self:T(self.lid.."Adding 25 to asset because it is spawned") - score=score+25 - end - end - - -- TODO: This could be vastly improved. Need to gather ideas during testing. - -- Calculate ETA? Assets on orbit missions should arrive faster even if they are further away. - -- Max speed of assets. - -- Fuel amount? - -- Range of assets? - - return score -end - ---- Optimize chosen assets for the mission at hand. --- @param #AIRWING self --- @param #table assets Table of (unoptimized) assets. --- @param Ops.Auftrag#AUFTRAG Mission Mission for which the best assets are desired. --- @param #boolean includePayload If true, include the payload in the calulation if the asset has one attached. -function AIRWING:_OptimizeAssetSelection(assets, Mission, includePayload) - - local TargetVec2=Mission:GetTargetVec2() - - --local dStock=self:GetCoordinate():Get2DDistance(TargetCoordinate) - - local dStock=UTILS.VecDist2D(TargetVec2, self:GetVec2()) - - -- Calculate distance to mission target. - local distmin=math.huge - local distmax=0 - for _,_asset in pairs(assets) do - local asset=_asset --#AIRWING.SquadronAsset - - if asset.spawned then - local group=GROUP:FindByName(asset.spawngroupname) - --asset.dist=group:GetCoordinate():Get2DDistance(TargetCoordinate) - asset.dist=UTILS.VecDist2D(group:GetVec2(), TargetVec2) - else - asset.dist=dStock - end - - if asset.distdistmax then - distmax=asset.dist - end - - end - - -- Calculate the mission score of all assets. - for _,_asset in pairs(assets) do - local asset=_asset --#AIRWING.SquadronAsset - --self:I(string.format("FF asset %s has payload %s", asset.spawngroupname, asset.payload and "yes" or "no!")) - asset.score=self:CalculateAssetMissionScore(asset, Mission, includePayload) - end - - --- Sort assets wrt to their mission score. Higher is better. - local function optimize(a, b) - local assetA=a --#AIRWING.SquadronAsset - local assetB=b --#AIRWING.SquadronAsset - - -- Higher score wins. If equal score ==> closer wins. - -- TODO: Need to include the distance in a smarter way! - return (assetA.score>assetB.score) or (assetA.score==assetB.score and assetA.dist0 then - - --local text=string.format("Requesting assets for mission %s:", Mission.name) - for i,_asset in pairs(Assetlist) do - local asset=_asset --#AIRWING.SquadronAsset - - -- Set asset to requested! Important so that new requests do not use this asset! - asset.requested=true - - if Mission.missionTask then - asset.missionTask=Mission.missionTask - end - - end - - -- Add request to airwing warehouse. - -- TODO: better Assignment string. - self:AddRequest(self, WAREHOUSE.Descriptor.ASSETLIST, Assetlist, #Assetlist, nil, nil, Mission.prio, tostring(Mission.auftragsnummer)) - - -- The queueid has been increased in the onafterAddRequest function. So we can simply use it here. - Mission.requestID=self.queueid - end - -end - ---- On after "MissionCancel" event. Cancels the missions of all flightgroups. Deletes request from warehouse queue. --- @param #AIRWING self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. --- @param Ops.Auftrag#AUFTRAG Mission The mission to be cancelled. -function AIRWING:onafterMissionCancel(From, Event, To, Mission) - - -- Info message. - self:I(self.lid..string.format("Cancel mission %s", Mission.name)) - - local Ngroups = Mission:CountOpsGroups() - - if Mission:IsPlanned() or Mission:IsQueued() or Mission:IsRequested() or Ngroups == 0 then - - Mission:Done() - - else - - for _,_asset in pairs(Mission.assets) do - local asset=_asset --#AIRWING.SquadronAsset - - local flightgroup=asset.flightgroup - - if flightgroup then - flightgroup:MissionCancel(Mission) - end - - -- Not requested any more (if it was). - asset.requested=nil - end - - end - - -- Remove queued request (if any). - if Mission.requestID then - self:_DeleteQueueItemByID(Mission.requestID, self.queue) - end - -end - ---- On after "NewAsset" event. Asset is added to the given squadron (asset assignment). --- @param #AIRWING self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. --- @param #AIRWING.SquadronAsset asset The asset that has just been added. --- @param #string assignment The (optional) assignment for the asset. -function AIRWING:onafterNewAsset(From, Event, To, asset, assignment) - - -- Call parent warehouse function first. - self:GetParent(self).onafterNewAsset(self, From, Event, To, asset, assignment) - - -- Debug text. - local text=string.format("New asset %s with assignment %s and request assignment %s", asset.spawngroupname, tostring(asset.assignment), tostring(assignment)) - self:T3(self.lid..text) - - -- Get squadron. - local squad=self:GetSquadron(asset.assignment) - - -- Check if asset is already part of the squadron. If an asset returns, it will be added again! We check that asset.assignment is also assignment. - if squad then - - if asset.assignment==assignment then - - local nunits=#asset.template.units - - -- Debug text. - local text=string.format("Adding asset to squadron %s: assignment=%s, type=%s, attribute=%s, nunits=%d %s", squad.name, assignment, asset.unittype, asset.attribute, nunits, tostring(squad.ngrouping)) - self:T(self.lid..text) - - -- Adjust number of elements in the group. - if squad.ngrouping then - local template=asset.template - - local N=math.max(#template.units, squad.ngrouping) - - -- Handle units. - for i=1,N do - - -- Unit template. - local unit = template.units[i] - - -- If grouping is larger than units present, copy first unit. - if i>nunits then - table.insert(template.units, UTILS.DeepCopy(template.units[1])) - end - - -- Remove units if original template contains more than in grouping. - if squad.ngroupingnunits then - unit=nil - end - end - - asset.nunits=squad.ngrouping - end - - -- Set takeoff type. - asset.takeoffType=squad.takeoffType - - -- Create callsign and modex (needs to be after grouping). - squad:GetCallsign(asset) - squad:GetModex(asset) - - -- Set spawn group name. This has to include "AID-" for warehouse. - asset.spawngroupname=string.format("%s_AID-%d", squad.name, asset.uid) - - -- Add asset to squadron. - squad:AddAsset(asset) - - -- TODO - --asset.terminalType=AIRBASE.TerminalType.OpenBig - else - - --env.info("FF squad asset returned") - self:SquadAssetReturned(squad, asset) - - end - - end -end - ---- On after "AssetReturned" event. Triggered when an asset group returned to its airwing. --- @param #AIRWING self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. --- @param Ops.Squadron#SQUADRON Squadron The asset squadron. --- @param #AIRWING.SquadronAsset Asset The asset that returned. -function AIRWING:onafterSquadAssetReturned(From, Event, To, Squadron, Asset) - -- Debug message. - self:T(self.lid..string.format("Asset %s from squadron %s returned! asset.assignment=\"%s\"", Asset.spawngroupname, Squadron.name, tostring(Asset.assignment))) - - -- Stop flightgroup. - if Asset.flightgroup and not Asset.flightgroup:IsStopped() then - Asset.flightgroup:Stop() - end - - -- Return payload. - self:ReturnPayloadFromAsset(Asset) - - -- Return tacan channel. - if Asset.tacan then - Squadron:ReturnTacan(Asset.tacan) - end - - -- Set timestamp. - Asset.Treturned=timer.getAbsTime() -end - - ---- On after "AssetSpawned" event triggered when an asset group is spawned into the cruel world. --- Creates a new flightgroup element and adds the mission to the flightgroup queue. --- @param #AIRWING self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. --- @param Wrapper.Group#GROUP group The group spawned. --- @param #AIRWING.SquadronAsset asset The asset that was spawned. --- @param Functional.Warehouse#WAREHOUSE.Pendingitem request The request of the dead asset. -function AIRWING:onafterAssetSpawned(From, Event, To, group, asset, request) - - -- Call parent warehouse function first. - self:GetParent(self).onafterAssetSpawned(self, From, Event, To, group, asset, request) - - -- Get the SQUADRON of the asset. - local squadron=self:GetSquadronOfAsset(asset) - - -- Check if we have a squadron or if this was some other request. - if squadron then - - -- Create a flight group. - local flightgroup=self:_CreateFlightGroup(asset) - - --- - -- Asset - --- - - -- Set asset flightgroup. - asset.flightgroup=flightgroup - - -- Not requested any more. - asset.requested=nil - - -- Did not return yet. - asset.Treturned=nil - - --- - -- Squadron - --- - - -- Get TACAN channel. - local Tacan=squadron:FetchTacan() - if Tacan then - asset.tacan=Tacan - end - - -- Set radio frequency and modulation - local radioFreq, radioModu=squadron:GetRadio() - if radioFreq then - flightgroup:SwitchRadio(radioFreq, radioModu) - end - - if squadron.fuellow then - flightgroup:SetFuelLowThreshold(squadron.fuellow) - end - - if squadron.fuellowRefuel then - flightgroup:SetFuelLowRefuel(squadron.fuellowRefuel) - end - - --- - -- Mission - --- - - -- Get Mission (if any). - local mission=self:GetMissionByID(request.assignment) - - -- Add mission to flightgroup queue. - if mission then - - if Tacan then - mission:SetTACAN(Tacan, Morse, UnitName, Band) - end - - -- Add mission to flightgroup queue. - asset.flightgroup:AddMission(mission) - - -- Trigger event. - self:FlightOnMission(flightgroup, mission) - - else - - if Tacan then - flightgroup:SwitchTACAN(Tacan, Morse, UnitName, Band) - end - - end - - -- Add group to the detection set of the WINGCOMMANDER. - if self.wingcommander and self.wingcommander.chief then - self.wingcommander.chief.detectionset:AddGroup(asset.flightgroup.group) - end - - end - -end - ---- On after "AssetDead" event triggered when an asset group died. --- @param #AIRWING self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. --- @param #AIRWING.SquadronAsset asset The asset that is dead. --- @param Functional.Warehouse#WAREHOUSE.Pendingitem request The request of the dead asset. -function AIRWING:onafterAssetDead(From, Event, To, asset, request) - - -- Call parent warehouse function first. - self:GetParent(self).onafterAssetDead(self, From, Event, To, asset, request) - - -- Add group to the detection set of the WINGCOMMANDER. - if self.wingcommander and self.wingcommander.chief then - self.wingcommander.chief.detectionset:RemoveGroupsByName({asset.spawngroupname}) - end - - -- Remove asset from mission is done via Mission:AssetDead() call from flightgroup onafterFlightDead function - -- Remove asset from squadron same -end - ---- On after "Destroyed" event. Remove assets from squadrons. Stop squadrons. Remove airwing from wingcommander. --- @param #AIRWING self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. -function AIRWING:onafterDestroyed(From, Event, To) - - self:I(self.lid.."Airwing warehouse destroyed!") - - -- Cancel all missions. - for _,_mission in pairs(self.missionqueue) do - local mission=_mission --Ops.Auftrag#AUFTRAG - mission:Cancel() - end - - -- Remove all squadron assets. - for _,_squadron in pairs(self.squadrons) do - local squadron=_squadron --Ops.Squadron#SQUADRON - -- Stop Squadron. This also removes all assets. - squadron:Stop() - end - - -- Call parent warehouse function first. - self:GetParent(self).onafterDestroyed(self, From, Event, To) - -end - - ---- On after "Request" event. --- @param #AIRWING self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. --- @param Functional.Warehouse#WAREHOUSE.Queueitem Request Information table of the request. -function AIRWING:onafterRequest(From, Event, To, Request) - - -- Assets - local assets=Request.cargoassets - - -- Get Mission - local Mission=self:GetMissionByID(Request.assignment) - - if Mission and assets then - - for _,_asset in pairs(assets) do - local asset=_asset --#AIRWING.SquadronAsset - -- This would be the place to modify the asset table before the asset is spawned. - end - - end - - -- Call parent warehouse function after assets have been adjusted. - self:GetParent(self).onafterRequest(self, From, Event, To, Request) - -end - ---- On after "SelfRequest" event. --- @param #AIRWING self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. --- @param Core.Set#SET_GROUP groupset The set of asset groups that was delivered to the warehouse itself. --- @param Functional.Warehouse#WAREHOUSE.Pendingitem request Pending self request. -function AIRWING:onafterSelfRequest(From, Event, To, groupset, request) - - -- Call parent warehouse function first. - self:GetParent(self).onafterSelfRequest(self, From, Event, To, groupset, request) - - -- Get Mission - local mission=self:GetMissionByID(request.assignment) - - for _,_asset in pairs(request.assets) do - local asset=_asset --#AIRWING.SquadronAsset - end - - for _,_group in pairs(groupset:GetSet()) do - local group=_group --Wrapper.Group#GROUP - end - +function AIRWING:onafterFlightOnMission(From, Event, To, FlightGroup, Mission) + -- Debug info. + self:T(self.lid..string.format("Group %s on %s mission %s", FlightGroup:GetName(), Mission:GetType(), Mission:GetName())) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Misc Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Create a new flight group after an asset was spawned. --- @param #AIRWING self --- @param #AIRWING.SquadronAsset asset The asset. --- @return Ops.FlightGroup#FLIGHTGROUP The created flightgroup object. -function AIRWING:_CreateFlightGroup(asset) - - -- Create flightgroup. - local flightgroup=FLIGHTGROUP:New(asset.spawngroupname) - - -- Set airwing. - flightgroup:SetAirwing(self) - - -- Set squadron. - flightgroup.squadron=self:GetSquadronOfAsset(asset) - - -- Set home base. - flightgroup.homebase=self.airbase - - return flightgroup -end - - ---- Check if an asset is currently on a mission (STARTED or EXECUTING). --- @param #AIRWING self --- @param #AIRWING.SquadronAsset asset The asset. --- @param #table MissionTypes Types on mission to be checked. Default all. --- @return #boolean If true, asset has at least one mission of that type in the queue. -function AIRWING:IsAssetOnMission(asset, MissionTypes) - - if MissionTypes then - if type(MissionTypes)~="table" then - MissionTypes={MissionTypes} - end - else - -- Check all possible types. - MissionTypes=AUFTRAG.Type - end - - if asset.flightgroup and asset.flightgroup:IsAlive() then - - -- Loop over mission queue. - for _,_mission in pairs(asset.flightgroup.missionqueue or {}) do - local mission=_mission --Ops.Auftrag#AUFTRAG - - if mission:IsNotOver() then - - -- Get flight status. - local status=mission:GetGroupStatus(asset.flightgroup) - - -- Only if mission is started or executing. - if (status==AUFTRAG.GroupStatus.STARTED or status==AUFTRAG.GroupStatus.EXECUTING) and self:CheckMissionType(mission.type, MissionTypes) then - return true - end - - end - - end - - end - - -- Alternative: run over all missions and compare to mission assets. - --[[ - for _,_mission in pairs(self.missionqueue) do - local mission=_mission --Ops.Auftrag#AUFTRAG - - if mission:IsNotOver() then - for _,_asset in pairs(mission.assets) do - local sqasset=_asset --#AIRWING.SquadronAsset - - if sqasset.uid==asset.uid then - return true - end - - end - end - - end - ]] - - return false -end - ---- Get the current mission of the asset. --- @param #AIRWING self --- @param #AIRWING.SquadronAsset asset The asset. --- @return Ops.Auftrag#AUFTRAG Current mission or *nil*. -function AIRWING:GetAssetCurrentMission(asset) - - if asset.flightgroup then - return asset.flightgroup:GetMissionCurrent() - end - - return nil -end - --- Count payloads in stock. -- @param #AIRWING self -- @param #table MissionTypes Types on mission to be checked. Default *all* possible types `AUFTRAG.Type`. @@ -1924,7 +1139,7 @@ function AIRWING:CountPayloadsInStock(MissionTypes, UnitTypes, Payloads) UnitTypes={UnitTypes} end end - + local function _checkUnitTypes(payload) if UnitTypes then for _,unittype in pairs(UnitTypes) do @@ -1938,7 +1153,7 @@ function AIRWING:CountPayloadsInStock(MissionTypes, UnitTypes, Payloads) end return false end - + local function _checkPayloads(payload) if Payloads then for _,Payload in pairs(Payloads) do @@ -1951,280 +1166,36 @@ function AIRWING:CountPayloadsInStock(MissionTypes, UnitTypes, Payloads) return nil end return false - end + end local n=0 for _,_payload in pairs(self.payloads) do local payload=_payload --#AIRWING.Payload - + for _,MissionType in pairs(MissionTypes) do - + local specialpayload=_checkPayloads(payload) - local compatible=self:CheckMissionCapability(MissionType, payload.capabilities) - + local compatible=AUFTRAG.CheckMissionCapability(MissionType, payload.capabilities) + local goforit = specialpayload or (specialpayload==nil and compatible) - + if goforit and _checkUnitTypes(payload) then - + if payload.unlimited then -- Payload is unlimited. Return a BIG number. return 999 else n=n+payload.navail end - + end - + end end return n end ---- Count missions in mission queue. --- @param #AIRWING self --- @param #table MissionTypes Types on mission to be checked. Default *all* possible types `AUFTRAG.Type`. --- @return #number Number of missions that are not over yet. -function AIRWING:CountMissionsInQueue(MissionTypes) - - MissionTypes=MissionTypes or AUFTRAG.Type - - local N=0 - for _,_mission in pairs(self.missionqueue) do - local mission=_mission --Ops.Auftrag#AUFTRAG - - -- Check if this mission type is requested. - if mission:IsNotOver() and self:CheckMissionType(mission.type, MissionTypes) then - N=N+1 - end - - end - - return N -end - ---- Count total number of assets. This is the sum of all squadron assets. --- @param #AIRWING self --- @return #number Amount of asset groups. -function AIRWING:CountAssets() - - local N=0 - - for _,_squad in pairs(self.squadrons) do - local squad=_squad --Ops.Squadron#SQUADRON - N=N+#squad.assets - end - - return N -end - ---- Count assets on mission. --- @param #AIRWING self --- @param #table MissionTypes Types on mission to be checked. Default all. --- @param Ops.Squadron#SQUADRON Squadron Only count assets of this squadron. Default count assets of all squadrons. --- @return #number Number of pending and queued assets. --- @return #number Number of pending assets. --- @return #number Number of queued assets. -function AIRWING:CountAssetsOnMission(MissionTypes, Squadron) - - local Nq=0 - local Np=0 - - for _,_mission in pairs(self.missionqueue) do - local mission=_mission --Ops.Auftrag#AUFTRAG - - -- Check if this mission type is requested. - if self:CheckMissionType(mission.type, MissionTypes or AUFTRAG.Type) then - - for _,_asset in pairs(mission.assets or {}) do - local asset=_asset --#AIRWING.SquadronAsset - - if Squadron==nil or Squadron.name==asset.squadname then - - local request, isqueued=self:GetRequestByID(mission.requestID) - - if isqueued then - Nq=Nq+1 - else - Np=Np+1 - end - - end - - end - end - end - - --env.info(string.format("FF N=%d Np=%d, Nq=%d", Np+Nq, Np, Nq)) - return Np+Nq, Np, Nq -end - ---- Count assets on mission. --- @param #AIRWING self --- @param #table MissionTypes Types on mission to be checked. Default all. --- @return #table Assets on pending requests. -function AIRWING:GetAssetsOnMission(MissionTypes) - - local assets={} - local Np=0 - - for _,_mission in pairs(self.missionqueue) do - local mission=_mission --Ops.Auftrag#AUFTRAG - - -- Check if this mission type is requested. - if self:CheckMissionType(mission.type, MissionTypes) then - - for _,_asset in pairs(mission.assets or {}) do - local asset=_asset --#AIRWING.SquadronAsset - - table.insert(assets, asset) - - end - end - end - - return assets -end - ---- Get the aircraft types of this airwing. --- @param #AIRWING self --- @param #boolean onlyactive Count only the active ones. --- @param #table squadrons Table of squadrons. Default all. --- @return #table Table of unit types. -function AIRWING:GetAircraftTypes(onlyactive, squadrons) - - -- Get all unit types that can do the job. - local unittypes={} - - -- Loop over all squadrons. - for _,_squadron in pairs(squadrons or self.squadrons) do - local squadron=_squadron --Ops.Squadron#SQUADRON - - if (not onlyactive) or squadron:IsOnDuty() then - - local gotit=false - for _,unittype in pairs(unittypes) do - if squadron.aircrafttype==unittype then - gotit=true - break - end - end - if not gotit then - table.insert(unittypes, squadron.aircrafttype) - end - - end - end - - return unittypes -end - ---- Check if assets for a given mission type are available. --- @param #AIRWING self --- @param Ops.Auftrag#AUFTRAG Mission The mission. --- @return #boolean If true, enough assets are available. --- @return #table Assets that can do the required mission. -function AIRWING:CanMission(Mission) - - -- Assume we CAN and NO assets are available. - local Can=true - local Assets={} - - -- Squadrons for the job. If user assigned to mission or simply all. - local squadrons=Mission.squadrons or self.squadrons - - -- Get aircraft unit types for the job. - local unittypes=self:GetAircraftTypes(true, squadrons) - - -- Count all payloads in stock. - local Npayloads=self:CountPayloadsInStock(Mission.type, unittypes, Mission.payloads) - - if Npayloads #Assets then - self:T(self.lid..string.format("INFO: Not enough assets available! Got %d but need at least %d", #Assets, Mission.nassets)) - Can=false - end - - return Can, Assets -end - ---- Check if assets for a given mission type are available. --- @param #AIRWING self --- @param Ops.Auftrag#AUFTRAG Mission The mission. --- @return #table Assets that can do the required mission. -function AIRWING:RecruitAssets(Mission) - -end - - ---- Check if a mission type is contained in a list of possible types. --- @param #AIRWING self --- @param #string MissionType The requested mission type. --- @param #table PossibleTypes A table with possible mission types. --- @return #boolean If true, the requested mission type is part of the possible mission types. -function AIRWING:CheckMissionType(MissionType, PossibleTypes) - - if type(PossibleTypes)=="string" then - PossibleTypes={PossibleTypes} - end - - for _,canmission in pairs(PossibleTypes) do - if canmission==MissionType then - return true - end - end - - return false -end - ---- Check if a mission type is contained in a list of possible capabilities. --- @param #AIRWING self --- @param #string MissionType The requested mission type. --- @param #table Capabilities A table with possible capabilities. --- @return #boolean If true, the requested mission type is part of the possible mission types. -function AIRWING:CheckMissionCapability(MissionType, Capabilities) - - for _,cap in pairs(Capabilities) do - local capability=cap --Ops.Auftrag#AUFTRAG.Capability - if capability.MissionType==MissionType then - return true - end - end - - return false -end - --- Get payload performance for a given type of misson type. -- @param #AIRWING self -- @param #AIRWING.Payload Payload The payload table. @@ -2233,7 +1204,7 @@ end function AIRWING:GetPayloadPeformance(Payload, MissionType) if Payload then - + for _,Capability in pairs(Payload.capabilities) do local capability=Capability --Ops.Auftrag#AUFTRAG.Capability if capability.MissionType==MissionType then @@ -2255,7 +1226,7 @@ end function AIRWING:GetPayloadMissionTypes(Payload) local missiontypes={} - + for _,Capability in pairs(Payload.capabilities) do local capability=Capability --Ops.Auftrag#AUFTRAG.Capability table.insert(missiontypes, capability.MissionType) @@ -2264,46 +1235,6 @@ function AIRWING:GetPayloadMissionTypes(Payload) return missiontypes end ---- Returns the mission for a given mission ID (Autragsnummer). --- @param #AIRWING self --- @param #number mid Mission ID (Auftragsnummer). --- @return Ops.Auftrag#AUFTRAG Mission table. -function AIRWING:GetMissionByID(mid) - - for _,_mission in pairs(self.missionqueue) do - local mission=_mission --Ops.Auftrag#AUFTRAG - - if mission.auftragsnummer==tonumber(mid) then - return mission - end - - end - - return nil -end - ---- Returns the mission for a given request ID. --- @param #AIRWING self --- @param #number RequestID Unique ID of the request. --- @return Ops.Auftrag#AUFTRAG Mission table or *nil*. -function AIRWING:GetMissionFromRequestID(RequestID) - for _,_mission in pairs(self.missionqueue) do - local mission=_mission --Ops.Auftrag#AUFTRAG - if mission.requestID and mission.requestID==RequestID then - return mission - end - end - return nil -end - ---- Returns the mission for a given request. --- @param #AIRWING self --- @param Functional.Warehouse#WAREHOUSE.Queueitem Request The warehouse request. --- @return Ops.Auftrag#AUFTRAG Mission table or *nil*. -function AIRWING:GetMissionFromRequest(Request) - return self:GetMissionFromRequestID(Request.uid) -end - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 3651c74ae..becd99218 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -3628,36 +3628,39 @@ function AIRBOSS:_CheckAIStatus() -- Unit local unit=element.unit - -- Get lineup and distance to carrier. - local lineup=self:_Lineup(unit, true) + if unit and unit:IsAlive() then - local unitcoord=unit:GetCoord() + -- Get lineup and distance to carrier. + local lineup=self:_Lineup(unit, true) - local dist=unitcoord:Get2DDistance(self:GetCoord()) + local unitcoord=unit:GetCoord() - -- Distance in NM. - local distance=UTILS.MetersToNM(dist) + local dist=unitcoord:Get2DDistance(self:GetCoord()) - -- Altitude in ft. - local alt=UTILS.MetersToFeet(unitcoord.y) + -- Distance in NM. + local distance=UTILS.MetersToNM(dist) - -- Check if parameters are right and flight is in the groove. - if lineup<2 and distance<=0.75 and alt<500 and not element.ballcall then + -- Altitude in ft. + local alt=UTILS.MetersToFeet(unitcoord.y) - -- Paddles: Call the ball! - self:RadioTransmission(self.LSORadio, self.LSOCall.CALLTHEBALL, nil, nil, nil, true) + -- Check if parameters are right and flight is in the groove. + if lineup<2 and distance<=0.75 and alt<500 and not element.ballcall then - -- Pilot: "405, Hornet Ball, 3.2" - self:_LSOCallAircraftBall(element.onboard,self:_GetACNickname(unit:GetTypeName()), self:_GetFuelState(unit)/1000) + -- Paddles: Call the ball! + self:RadioTransmission(self.LSORadio, self.LSOCall.CALLTHEBALL, nil, nil, nil, true) - -- Paddles: Roger ball after 0.5 seconds. - self:RadioTransmission(self.LSORadio, self.LSOCall.ROGERBALL, nil, nil, 0.5, true) + -- Pilot: "405, Hornet Ball, 3.2" + self:_LSOCallAircraftBall(element.onboard,self:_GetACNickname(unit:GetTypeName()), self:_GetFuelState(unit)/1000) - -- Flight element called the ball. - element.ballcall=true + -- Paddles: Roger ball after 0.5 seconds. + self:RadioTransmission(self.LSORadio, self.LSOCall.ROGERBALL, nil, nil, 0.5, true) - -- This is for the whole flight. Maybe we need it. - flight.ballcall=true + -- Flight element called the ball. + element.ballcall=true + + -- This is for the whole flight. Maybe we need it. + flight.ballcall=true + end end end @@ -6263,6 +6266,11 @@ function AIRBOSS:_ScanCarrierZone() -- Get flight group if possible. local knownflight=self:_GetFlightFromGroupInQueue(group, self.flights) + + -- Unknown new AI flight. Create a new flight group. + if not knownflight and not self:_IsHuman(group) then + knownflight=self:_CreateFlightGroup(group) + end -- Get aircraft type name. local actype=group:GetTypeName() @@ -6276,10 +6284,10 @@ function AIRBOSS:_ScanCarrierZone() local putintomarshal=false -- Get flight group. - local flight=_DATABASE:GetFlightGroup(groupname) + local flight=_DATABASE:GetOpsGroup(groupname) if flight and flight:IsInbound() and flight.destbase:GetName()==self.carrier:GetName() then - if flight.ishelo then + if flight.isHelo then else putintomarshal=true end @@ -6320,10 +6328,7 @@ function AIRBOSS:_ScanCarrierZone() else - -- Unknown new AI flight. Create a new flight group. - if not self:_IsHuman(group) then - self:_CreateFlightGroup(group) - end + end diff --git a/Moose Development/Moose/Ops/ArmyGroup.lua b/Moose Development/Moose/Ops/ArmyGroup.lua index a5754efd9..9bdfb59a0 100644 --- a/Moose Development/Moose/Ops/ArmyGroup.lua +++ b/Moose Development/Moose/Ops/ArmyGroup.lua @@ -38,7 +38,7 @@ -- @field Core.Set#SET_ZONE retreatZones Set of retreat zones. -- @extends Ops.OpsGroup#OPSGROUP ---- *Your soul may belong to Jesus, but your ass belongs to the marines.* -- Eugene B. Sledge +--- *Your soul may belong to Jesus, but your ass belongs to the marines.* -- Eugene B Sledge -- -- === -- @@ -55,33 +55,26 @@ ARMYGROUP = { engage = {}, } ---- Army group element. --- @type ARMYGROUP.Element --- @field #string name Name of the element, i.e. the unit. --- @field Wrapper.Unit#UNIT unit The UNIT object. --- @field #string status The element status. --- @field #string typename Type name. --- @field #number length Length of element in meters. --- @field #number width Width of element in meters. --- @field #number height Height of element in meters. - ---- Target +--- Engage Target. -- @type ARMYGROUP.Target -- @field Ops.Target#TARGET Target The target. -- @field Core.Point#COORDINATE Coordinate Last known coordinate of the target. +-- @field Ops.OpsGroup#OPSGROUP.Waypoint Waypoint the waypoint created to go to the target. +-- @field #number roe ROE backup. +-- @field #number alarmstate Alarm state backup. --- Army Group version. -- @field #string version -ARMYGROUP.version="0.4.0" +ARMYGROUP.version="0.7.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Retreat. -- TODO: Suppression of fire. -- TODO: Check if group is mobile. -- TODO: F10 menu. +-- DONE: Retreat. -- DONE: Rearm. Specify a point where to go and wait until ammo is full. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -90,20 +83,27 @@ ARMYGROUP.version="0.4.0" --- Create a new ARMYGROUP class object. -- @param #ARMYGROUP self --- @param Wrapper.Group#GROUP Group The group object. Can also be given by its group name as `#string`. +-- @param Wrapper.Group#GROUP group The GROUP object. Can also be given by its group name as `#string`. -- @return #ARMYGROUP self -function ARMYGROUP:New(Group) - +function ARMYGROUP:New(group) + + -- First check if we already have an OPS group for this group. + local og=_DATABASE:GetOpsGroup(group) + if og then + og:I(og.lid..string.format("WARNING: OPS group already exists in data base!")) + return og + end + -- Inherit everything from FSM class. - local self=BASE:Inherit(self, OPSGROUP:New(Group)) -- #ARMYGROUP + local self=BASE:Inherit(self, OPSGROUP:New(group)) -- #ARMYGROUP -- Set some string id for output to DCS.log file. self.lid=string.format("ARMYGROUP %s | ", self.groupname) -- Defaults - self.isArmygroup=true self:SetDefaultROE() self:SetDefaultAlarmstate() + self:SetDefaultEPLRS(self.isEPLRS) self:SetDetection() self:SetPatrolAdInfinitum(false) self:SetRetreatZones() @@ -112,17 +112,20 @@ function ARMYGROUP:New(Group) -- From State --> Event --> To State self:AddTransition("*", "FullStop", "Holding") -- Hold position. self:AddTransition("*", "Cruise", "Cruising") -- Cruise along the given route of waypoints. + + self:AddTransition("*", "RTZ", "Returning") -- Group is returning to (home) zone. + self:AddTransition("Returning", "Returned", "Returned") -- Group is returned to (home) zone. self:AddTransition("*", "Detour", "OnDetour") -- Make a detour to a coordinate and resume route afterwards. self:AddTransition("OnDetour", "DetourReached", "Cruising") -- Group reached the detour coordinate. - self:AddTransition("*", "Retreat", "Retreating") -- - self:AddTransition("Retreating", "Retreated", "Retreated") -- + self:AddTransition("*", "Retreat", "Retreating") -- Order a retreat. + self:AddTransition("Retreating", "Retreated", "Retreated") -- Group retreated. - self:AddTransition("Cruising", "EngageTarget", "Engaging") -- Engage a target - self:AddTransition("Holding", "EngageTarget", "Engaging") -- Engage a target - self:AddTransition("OnDetour", "EngageTarget", "Engaging") -- Engage a target - self:AddTransition("Engaging", "Disengage", "Cruising") -- Engage a target + self:AddTransition("Cruising", "EngageTarget", "Engaging") -- Engage a target from Cruising state + self:AddTransition("Holding", "EngageTarget", "Engaging") -- Engage a target from Holding state + self:AddTransition("OnDetour", "EngageTarget", "Engaging") -- Engage a target from OnDetour state + self:AddTransition("Engaging", "Disengage", "Cruising") -- Disengage and back to cruising. self:AddTransition("*", "Rearm", "Rearm") -- Group is send to a coordinate and waits until ammo is refilled. self:AddTransition("Rearm", "Rearming", "Rearming") -- Group has arrived at the rearming coodinate and is waiting to be fully rearmed. @@ -131,20 +134,243 @@ function ARMYGROUP:New(Group) ------------------------ --- Pseudo Functions --- ------------------------ - - --- Triggers the FSM event "Stop". Stops the ARMYGROUP and all its event handlers. + --- Triggers the FSM event "Cruise". + -- @function [parent=#ARMYGROUP] Cruise -- @param #ARMYGROUP self + -- @param #number Speed Speed in knots until next waypoint is reached. + -- @param #number Formation Formation. - --- Triggers the FSM event "Stop" after a delay. Stops the ARMYGROUP and all its event handlers. - -- @function [parent=#ARMYGROUP] __Stop + --- Triggers the FSM event "Cruise" after a delay. + -- @function [parent=#ARMYGROUP] __Cruise -- @param #ARMYGROUP self -- @param #number delay Delay in seconds. - + -- @param #number Speed Speed in knots until next waypoint is reached. + -- @param #number Formation Formation. + + --- On after "Cruise" event. + -- @function [parent=#ARMYGROUP] OnAfterCruise + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #number Speed Speed in knots until next waypoint is reached. + -- @param #number Formation Formation. + + + --- Triggers the FSM event "FullStop". + -- @function [parent=#ARMYGROUP] FullStop + -- @param #ARMYGROUP self + + --- Triggers the FSM event "FullStop" after a delay. + -- @function [parent=#ARMYGROUP] __FullStop + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "FullStop" event. + -- @function [parent=#ARMYGROUP] OnAfterFullStop + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "RTZ". + -- @function [parent=#ARMYGROUP] RTZ + -- @param #ARMYGROUP self + + --- Triggers the FSM event "RTZ" after a delay. + -- @function [parent=#ARMYGROUP] __RTZ + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "RTZ" event. + -- @function [parent=#ARMYGROUP] OnAfterRTZ + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Returned". + -- @function [parent=#ARMYGROUP] Returned + -- @param #ARMYGROUP self + + --- Triggers the FSM event "Returned" after a delay. + -- @function [parent=#ARMYGROUP] __Returned + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "Returned" event. + -- @function [parent=#ARMYGROUP] OnAfterReturned + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Detour". + -- @function [parent=#ARMYGROUP] Detour + -- @param #ARMYGROUP self + + --- Triggers the FSM event "Detour" after a delay. + -- @function [parent=#ARMYGROUP] __Detour + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "Detour" event. + -- @function [parent=#ARMYGROUP] OnAfterDetour + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "DetourReached". + -- @function [parent=#ARMYGROUP] DetourReached + -- @param #ARMYGROUP self + + --- Triggers the FSM event "DetourReached" after a delay. + -- @function [parent=#ARMYGROUP] __DetourReached + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "DetourReached" event. + -- @function [parent=#ARMYGROUP] OnAfterDetourReached + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Retreat". + -- @function [parent=#ARMYGROUP] Retreat + -- @param #ARMYGROUP self + + --- Triggers the FSM event "Retreat" after a delay. + -- @function [parent=#ARMYGROUP] __Retreat + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "Retreat" event. + -- @function [parent=#ARMYGROUP] OnAfterRetreat + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Retreated". + -- @function [parent=#ARMYGROUP] Retreated + -- @param #ARMYGROUP self + + --- Triggers the FSM event "Retreated" after a delay. + -- @function [parent=#ARMYGROUP] __Retreated + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "Retreated" event. + -- @function [parent=#ARMYGROUP] OnAfterRetreated + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "EngageTarget". + -- @function [parent=#ARMYGROUP] EngageTarget + -- @param #ARMYGROUP self + + --- Triggers the FSM event "EngageTarget" after a delay. + -- @function [parent=#ARMYGROUP] __EngageTarget + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "EngageTarget" event. + -- @function [parent=#ARMYGROUP] OnAfterEngageTarget + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Disengage". + -- @function [parent=#ARMYGROUP] Disengage + -- @param #ARMYGROUP self + + --- Triggers the FSM event "Disengage" after a delay. + -- @function [parent=#ARMYGROUP] __Disengage + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "Disengage" event. + -- @function [parent=#ARMYGROUP] OnAfterDisengage + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Rearm". + -- @function [parent=#ARMYGROUP] Rearm + -- @param #ARMYGROUP self + -- @param Core.Point#COORDINATE Coordinate Coordinate where to rearm. + -- @param #number Formation Formation of the group. + + --- Triggers the FSM event "Rearm" after a delay. + -- @function [parent=#ARMYGROUP] __Rearm + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + -- @param Core.Point#COORDINATE Coordinate Coordinate where to rearm. + -- @param #number Formation Formation of the group. + + --- On after "Rearm" event. + -- @function [parent=#ARMYGROUP] OnAfterRearm + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Point#COORDINATE Coordinate Coordinate where to rearm. + -- @param #number Formation Formation of the group. + + + --- Triggers the FSM event "Rearming". + -- @function [parent=#ARMYGROUP] Rearming + -- @param #ARMYGROUP self + + --- Triggers the FSM event "Rearming" after a delay. + -- @function [parent=#ARMYGROUP] __Rearming + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "Rearming" event. + -- @function [parent=#ARMYGROUP] OnAfterRearming + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Rearmed". + -- @function [parent=#ARMYGROUP] Rearmed + -- @param #ARMYGROUP self + + --- Triggers the FSM event "Rearmed" after a delay. + -- @function [parent=#ARMYGROUP] __Rearmed + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "Rearmed" event. + -- @function [parent=#ARMYGROUP] OnAfterRearmed + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + -- TODO: Add pseudo functions. - -- Init waypoints. - self:InitWaypoints() + self:_InitWaypoints() -- Initialize the group. self:_InitGroup() @@ -152,18 +378,20 @@ function ARMYGROUP:New(Group) -- Handle events: self:HandleEvent(EVENTS.Birth, self.OnEventBirth) self:HandleEvent(EVENTS.Dead, self.OnEventDead) - self:HandleEvent(EVENTS.RemoveUnit, self.OnEventRemoveUnit) - + self:HandleEvent(EVENTS.RemoveUnit, self.OnEventRemoveUnit) --self:HandleEvent(EVENTS.Hit, self.OnEventHit) -- Start the status monitoring. - self:__Status(-1) + self.timerStatus=TIMER:New(self.Status, self):Start(1, 30) -- Start queue update timer. self.timerQueueUpdate=TIMER:New(self._QueueUpdate, self):Start(2, 5) -- Start check zone timer. self.timerCheckZone=TIMER:New(self._CheckInZones, self):Start(2, 30) + + -- Add OPSGROUP to _DATABASE. + _DATABASE:AddOpsGroup(self) return self end @@ -189,7 +417,8 @@ end -- @param #ARMYGROUP self -- @return Core.Point#COORDINATE Coordinate of a road closest to the group. function ARMYGROUP:GetClosestRoad() - return self:GetCoordinate():GetClosestPointToRoad() + local coord=self:GetCoordinate():GetClosestPointToRoad() + return coord end --- Get 2D distance to the closest road. @@ -225,6 +454,40 @@ function ARMYGROUP:AddTaskFireAtPoint(Coordinate, Clock, Radius, Nshots, WeaponT return task end +--- Add a *scheduled* task to fire at a given coordinate. +-- @param #ARMYGROUP self +-- @param #string Clock Time when to start the attack. +-- @param #number Heading Heading min in Degrees. +-- @param #number Alpha Shooting angle in Degrees. +-- @param #number Altitude Altitude in meters. +-- @param #number Radius Radius in meters. Default 100 m. +-- @param #number Nshots Number of shots to fire. Default nil. +-- @param #number WeaponType Type of weapon. Default auto. +-- @param #number Prio Priority of the task. +-- @return Ops.OpsGroup#OPSGROUP.Task The task table. +function ARMYGROUP:AddTaskBarrage(Clock, Heading, Alpha, Altitude, Radius, Nshots, WeaponType, Prio) + + Heading=Heading or 0 + + Alpha=Alpha or 60 + + Altitude=Altitude or 100 + + local distance=Altitude/math.tan(math.rad(Alpha)) + + local a=self:GetVec2() + + local vec2=UTILS.Vec2Translate(a, distance, Heading) + + --local coord=COORDINATE:NewFromVec2(vec2):MarkToAll("Fire At Point",ReadOnly,Text) + + local DCStask=CONTROLLABLE.TaskFireAtPoint(nil, vec2, Radius, Nshots, WeaponType, Altitude) + + local task=self:AddTask(DCStask, Clock, nil, Prio) + + return task +end + --- Add a *waypoint* task to fire at a given coordinate. -- @param #ARMYGROUP self -- @param Core.Point#COORDINATE Coordinate Coordinate of the target. @@ -264,6 +527,26 @@ function ARMYGROUP:AddTaskAttackGroup(TargetGroup, WeaponExpend, WeaponType, Clo return task end +--- Add a *scheduled* task to transport group(s). +-- @param #ARMYGROUP self +-- @param Core.Set#SET_GROUP GroupSet Set of cargo groups. Can also be a singe @{Wrapper.Group#GROUP} object. +-- @param Core.Zone#ZONE PickupZone Zone where the cargo is picked up. +-- @param Core.Zone#ZONE DeployZone Zone where the cargo is delivered to. +-- @param #string Clock Time when to start the attack. +-- @param #number Prio Priority of the task. +-- @return Ops.OpsGroup#OPSGROUP.Task The task table. +function ARMYGROUP:AddTaskCargoGroup(GroupSet, PickupZone, DeployZone, Clock, Prio) + + local DCStask={} + DCStask.id="CargoTransport" + DCStask.params={} + DCStask.params.cargoqueu=1 + + local task=self:AddTask(DCStask, Clock, nil, Prio) + + return task +end + --- Define a set of possible retreat zones. -- @param #ARMYGROUP self -- @param Core.Set#SET_ZONE RetreatZoneSet The retreat zone set. Default is an empty set. @@ -309,7 +592,11 @@ end function ARMYGROUP:IsCombatReady() local combatready=true - if self:IsRearming() or self:IsRetreating() or self.outofAmmo or self:IsEngaging() or self:is("Retreated") or self:IsDead() or self:IsStopped() or self:IsInUtero() then + if self:IsRearming() or self:IsRetreating() or self:IsOutOfAmmo() or self:IsEngaging() or self:IsDead() or self:IsStopped() or self:IsInUtero() then + combatready=false + end + + if self:IsPickingup() or self:IsLoading() or self:IsTransporting() or self:IsLoaded() or self:IsCargo() or self:IsCarrier() then combatready=false end @@ -321,56 +608,55 @@ end -- Status ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ----- Update status. --- @param #ARMYGROUP self -function ARMYGROUP:onbeforeStatus(From, Event, To) - - if self:IsDead() then - self:T(self.lid..string.format("Onbefore Status DEAD ==> false")) - return false - elseif self:IsStopped() then - self:T(self.lid..string.format("Onbefore Status STOPPED ==> false")) - return false - end - - return true -end - --- Update status. -- @param #ARMYGROUP self -function ARMYGROUP:onafterStatus(From, Event, To) +function ARMYGROUP:Status() -- FSM state. local fsmstate=self:GetState() - if self:IsAlive() then + -- Is group alive? + local alive=self:IsAlive() - --- - -- Detection - --- - - -- Check if group has detected any units. - if self.detectionOn then - self:_CheckDetectedUnits() - end - - -- Check ammo status. - self:_CheckAmmoStatus() + -- Check that group EXISTS and is ACTIVE. + if alive then -- Update position etc. self:_UpdatePosition() - - -- Check if group got stuck. - self:_CheckStuck() + -- Check if group has detected any units. + self:_CheckDetectedUnits() + + -- Check ammo status. + self:_CheckAmmoStatus() + -- Check damage of elements and group. self:_CheckDamage() + + -- Check if group got stuck. + self:_CheckStuck() -- Update engagement. if self:IsEngaging() then self:_UpdateEngageTarget() end + -- Check if group is waiting. + if self:IsWaiting() then + if self.Twaiting and self.dTwait then + if timer.getAbsTime()>self.Twaiting+self.dTwait then + self.Twaiting=nil + self.dTwait=nil + self:Cruise() + end + end + end + + end + + -- Check that group EXISTS. + if alive~=nil then + if self.verbose>=1 then -- Get number of tasks and missions. @@ -379,14 +665,20 @@ function ARMYGROUP:onafterStatus(From, Event, To) local roe=self:GetROE() local alarm=self:GetAlarmstate() - local speed=UTILS.MpsToKnots(self.velocity) + local speed=UTILS.MpsToKnots(self.velocity or 0) local speedEx=UTILS.MpsToKnots(self:GetExpectedSpeed()) local formation=self.option.Formation or "unknown" local ammo=self:GetAmmoTot() + + local cargo=0 + for _,_element in pairs(self.elements) do + local element=_element --Ops.OpsGroup#OPSGROUP.Element + cargo=cargo+element.weightCargo + end -- Info text. - local text=string.format("%s [ROE-AS=%d-%d T/M=%d/%d]: Wp=%d/%d-->%d (final %s), Life=%.1f, Speed=%.1f (%d), Heading=%03d, Ammo=%d", - fsmstate, roe, alarm, nTaskTot, nMissions, self.currentwp, #self.waypoints, self:GetWaypointIndexNext(), tostring(self.passedfinalwp), self.life or 0, speed, speedEx, self.heading, ammo.Total) + local text=string.format("%s [ROE-AS=%d-%d T/M=%d/%d]: Wp=%d/%d-->%d (final %s), Life=%.1f, Speed=%.1f (%d), Heading=%03d, Ammo=%d, Cargo=%.1f", + fsmstate, roe, alarm, nTaskTot, nMissions, self.currentwp, #self.waypoints, self:GetWaypointIndexNext(), tostring(self.passedfinalwp), self.life or 0, speed, speedEx, self.heading or 0, ammo.Total, cargo) self:I(self.lid..text) end @@ -394,11 +686,65 @@ function ARMYGROUP:onafterStatus(From, Event, To) else -- Info text. - local text=string.format("State %s: Alive=%s", fsmstate, tostring(self:IsAlive())) - self:T2(self.lid..text) + if self.verbose>=1 then + local text=string.format("State %s: Alive=%s", fsmstate, tostring(self:IsAlive())) + self:I(self.lid..text) + end end + --- + -- Elements + --- + + if self.verbose>=2 then + local text="Elements:" + for i,_element in pairs(self.elements) do + local element=_element --Ops.OpsGroup#OPSGROUP.Element + + local name=element.name + local status=element.status + local unit=element.unit + --local life=unit:GetLifeRelative() or 0 + local life,life0=self:GetLifePoints(element) + + local life0=element.life0 + + -- Get ammo. + local ammo=self:GetAmmoElement(element) + + -- Output text for element. + text=text..string.format("\n[%d] %s: status=%s, life=%.1f/%.1f, guns=%d, rockets=%d, bombs=%d, missiles=%d, cargo=%d/%d kg", + i, name, status, life, life0, ammo.Guns, ammo.Rockets, ammo.Bombs, ammo.Missiles, element.weightCargo, element.weightMaxCargo) + end + if #self.elements==0 then + text=text.." none!" + end + self:I(self.lid..text) + end + + --- + -- Engage Detected Targets + --- + if self:IsCruising() and self.detectionOn and self.engagedetectedOn then + + local targetgroup, targetdist=self:_GetDetectedTarget() + + -- If we found a group, we engage it. + if targetgroup then + self:I(self.lid..string.format("Engaging target group %s at distance %d meters", targetgroup:GetName(), targetdist)) + self:EngageTarget(targetgroup) + end + + end + + + --- + -- Cargo + --- + + self:_CheckCargoTransport() + --- -- Tasks & Missions @@ -406,9 +752,26 @@ function ARMYGROUP:onafterStatus(From, Event, To) self:_PrintTaskAndMissionStatus() +end - -- Next status update. - self:__Status(-30) +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- DCS Events ==> See OPSGROUP +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Event function handling when a unit is hit. +-- @param #ARMYGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function ARMYGROUP:OnEventHit(EventData) + + -- Check that this is the right group. + if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then + local unit=EventData.IniUnit + local group=EventData.IniGroup + local unitname=EventData.IniUnitName + + -- TODO: suppression + + end end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -420,7 +783,7 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #ARMYGROUP.Element Element The group element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The group element. function ARMYGROUP:onafterElementSpawned(From, Event, To, Element) self:T(self.lid..string.format("Element spawned %s", Element.name)) @@ -437,8 +800,31 @@ end function ARMYGROUP:onafterSpawned(From, Event, To) self:T(self.lid..string.format("Group spawned!")) + -- Debug info. + if self.verbose>=1 then + local text=string.format("Initialized Army Group %s:\n", self.groupname) + text=text..string.format("Unit type = %s\n", self.actype) + text=text..string.format("Speed max = %.1f Knots\n", UTILS.KmphToKnots(self.speedMax)) + text=text..string.format("Speed cruise = %.1f Knots\n", UTILS.KmphToKnots(self.speedCruise)) + text=text..string.format("Weight = %.1f kg\n", self:GetWeightTotal()) + text=text..string.format("Cargo bay = %.1f kg\n", self:GetFreeCargobay()) + text=text..string.format("Has EPLRS = %s\n", tostring(self.isEPLRS)) + text=text..string.format("Elements = %d\n", #self.elements) + text=text..string.format("Waypoints = %d\n", #self.waypoints) + text=text..string.format("Radio = %.1f MHz %s %s\n", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu), tostring(self.radio.On)) + text=text..string.format("Ammo = %d (G=%d/R=%d/M=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Rockets, self.ammo.Missiles) + text=text..string.format("FSM state = %s\n", self:GetState()) + text=text..string.format("Is alive = %s\n", tostring(self:IsAlive())) + text=text..string.format("LateActivate = %s\n", tostring(self:IsLateActivated())) + self:I(self.lid..text) + end + -- Update position. self:_UpdatePosition() + + -- Not dead or destroyed yet. + self.isDead=false + self.isDestroyed=false if self.isAI then @@ -448,6 +834,9 @@ function ARMYGROUP:onafterSpawned(From, Event, To) -- Set default Alarm State. self:SwitchAlarmstate(self.option.Alarm) + -- Set default EPLRS. + self:SwitchEPLRS(self.option.EPLRS) + -- Set TACAN to default. self:_SwitchTACAN() @@ -460,18 +849,50 @@ function ARMYGROUP:onafterSpawned(From, Event, To) -- Formation if not self.option.Formation then - self.option.Formation=self.optionDefault.Formation + -- Will be set in update route. + --self.option.Formation=self.optionDefault.Formation end - + + -- Update route. + if #self.waypoints>1 then + self:T(self.lid.."Got waypoints on spawn ==> Cruise in -0.1 sec!") + self:__Cruise(-1, nil, self.option.Formation) + else + self:T(self.lid.."No waypoints on spawn ==> Full Stop!") + self:FullStop() + end + end - -- Update route. - if #self.waypoints>1 then - self:Cruise(nil, self.option.Formation or self.optionDefault.Formation) - else - self:FullStop() +end + +--- On before "UpdateRoute" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number n Next waypoint index. Default is the one coming after that one that has been passed last. +-- @param #number N Waypoint Max waypoint index to be included in the route. Default is the final waypoint. +-- @param #number Speed Speed in knots. Default cruise speed. +-- @param #number Formation Formation of the group. +function ARMYGROUP:onbeforeUpdateRoute(From, Event, To, n, N, Speed, Formation) + if self:IsWaiting() then + self:E(self.lid.."Update route denied. Group is WAIRING!") + return false + elseif self:IsInUtero() then + self:E(self.lid.."Update route denied. Group is INUTERO!") + return false + elseif self:IsDead() then + self:E(self.lid.."Update route denied. Group is DEAD!") + return false + elseif self:IsStopped() then + self:E(self.lid.."Update route denied. Group is STOPPED!") + return false + elseif self:IsHolding() then + self:T(self.lid.."Update route denied. Group is holding position!") + return false end - + return true end --- On after "UpdateRoute" event. @@ -479,91 +900,98 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #number n Waypoint number. Default is next waypoint. +-- @param #number n Next waypoint index. Default is the one coming after that one that has been passed last. +-- @param #number N Waypoint Max waypoint index to be included in the route. Default is the final waypoint. -- @param #number Speed Speed in knots. Default cruise speed. -- @param #number Formation Formation of the group. -function ARMYGROUP:onafterUpdateRoute(From, Event, To, n, Speed, Formation) +function ARMYGROUP:onafterUpdateRoute(From, Event, To, n, N, Speed, Formation) -- Debug info. - local text=string.format("Update route n=%s, Speed=%s, Formation=%s", tostring(n), tostring(Speed), tostring(Formation)) + local text=string.format("Update route state=%s: n=%s, N=%s, Speed=%s, Formation=%s", self:GetState(), tostring(n), tostring(N), tostring(Speed), tostring(Formation)) self:T(self.lid..text) -- Update route from this waypoint number onwards. n=n or self:GetWaypointIndexNext(self.adinfinitum) - -- Update waypoint tasks, i.e. inject WP tasks into waypoint table. - self:_UpdateWaypointTasks(n) - + -- Max index. + N=N or #self.waypoints + N=math.min(N, #self.waypoints) + -- Waypoints. local waypoints={} - -- Next waypoint. - local wp=UTILS.DeepCopy(self.waypoints[n]) --Ops.OpsGroup#OPSGROUP.Waypoint + local formationlast=nil + for i=n, #self.waypoints do - -- Do we want to drive on road to the next wp? - local onroad=wp.action==ENUMS.Formation.Vehicle.OnRoad - - -- Speed. - if Speed then - wp.speed=UTILS.KnotsToMps(Speed) - else - -- Take default waypoint speed. But make sure speed>0 if patrol ad infinitum. - if self.adinfinitum and wp.speed<0.1 then - wp.speed=UTILS.KmphToMps(self.speedCruise) + -- Next waypoint. + local wp=UTILS.DeepCopy(self.waypoints[i]) --Ops.OpsGroup#OPSGROUP.Waypoint + + -- Speed. + if Speed then + wp.speed=UTILS.KnotsToMps(Speed) + else + -- Take default waypoint speed. But make sure speed>0 if patrol ad infinitum. + if wp.speed<0.1 then --self.adinfinitum and + wp.speed=UTILS.KmphToMps(self.speedCruise) + end end + + -- Formation. + if self.formationPerma then + wp.action=self.formationPerma + elseif Formation then + wp.action=Formation + end + + -- Add waypoint in between because this waypoint is "On Road" but lies "Off Road". + if wp.action==ENUMS.Formation.Vehicle.OnRoad and wp.roaddist>10 then + + -- The real waypoint is actually off road. + wp.action=ENUMS.Formation.Vehicle.OffRoad + + -- Add "On Road" waypoint in between. + local wproad=wp.roadcoord:WaypointGround(UTILS.MpsToKmph(wp.speed), ENUMS.Formation.Vehicle.OnRoad) --Ops.OpsGroup#OPSGROUP.Waypoint + + -- Insert road waypoint. + table.insert(waypoints, wproad) + end + + -- Add waypoint. + table.insert(waypoints, wp) + + -- Last formation. + formationlast=wp.action end - -- Formation. - if self.formationPerma then - wp.action=self.formationPerma - elseif Formation then - wp.action=Formation - end + -- First (next wp). + local wp=waypoints[1] --Ops.OpsGroup#OPSGROUP.Waypoint -- Current set formation. self.option.Formation=wp.action - - -- Current set speed in m/s. - self.speedWp=wp.speed - - -- Add waypoint in between because this waypoint is "On Road" but lies "Off Road". - if onroad then - - -- The real waypoint is actually off road. - wp.action=ENUMS.Formation.Vehicle.OffRoad - - -- Add "On Road" waypoint in between. - local wproad=wp.roadcoord:WaypointGround(wp.speed, ENUMS.Formation.Vehicle.OnRoad) --Ops.OpsGroup#OPSGROUP.Waypoint - -- Insert road waypoint. - table.insert(waypoints, wproad) - end - - -- Add waypoint. - table.insert(waypoints, wp) + -- Current set speed in m/s. + self.speedWp=wp.speed - -- Apply formation at the current position or it will only be changed when reaching the next waypoint. - local formation=ENUMS.Formation.Vehicle.OffRoad - if wp.action~=ENUMS.Formation.Vehicle.OnRoad then - formation=wp.action - end + local formation0=wp.action==ENUMS.Formation.Vehicle.OnRoad and ENUMS.Formation.Vehicle.OffRoad or wp.action -- Current point. - local current=self:GetCoordinate():WaypointGround(UTILS.MpsToKmph(self.speedWp), formation) + local current=self:GetCoordinate():WaypointGround(UTILS.MpsToKmph(self.speedWp), formation0) table.insert(waypoints, 1, current) -- Insert a point on road. - if onroad then + if wp.action==ENUMS.Formation.Vehicle.OnRoad then local current=self:GetClosestRoad():WaypointGround(UTILS.MpsToKmph(self.speedWp), ENUMS.Formation.Vehicle.OnRoad) table.insert(waypoints, 2, current) end + -- Debug output. - if false then + if self.verbose>=10 then for i,_wp in pairs(waypoints) do - local wp=_wp - local text=string.format("WP #%d UID=%d type=%s: Speed=%d m/s, alt=%d m, Action=%s", i, wp.uid and wp.uid or 0, wp.type, wp.speed, wp.alt, wp.action) - self:T(text) + local wp=_wp --Ops.OpsGroup#OPSGROUP.Waypoint + local text=string.format("WP #%d UID=%d type=%s: Speed=%d m/s, alt=%d m, Action=%s", i, wp.uid and wp.uid or -1, wp.type, wp.speed, wp.alt, wp.action) + local coord=COORDINATE:NewFromWaypoint(wp):MarkToAll(text) + self:I(text) end end @@ -582,7 +1010,7 @@ function ARMYGROUP:onafterUpdateRoute(From, Event, To, n, Speed, Formation) -- Passed final WP ==> Full Stop --- - self:E(self.lid..string.format("WARNING: Passed final WP ==> Full Stop!")) + self:E(self.lid..string.format("WARNING: Passed final WP when UpdateRoute() ==> Full Stop!")) self:FullStop() end @@ -601,26 +1029,13 @@ function ARMYGROUP:onafterGotoWaypoint(From, Event, To, UID, Speed, Formation) local n=self:GetWaypointIndex(UID) - --env.info(string.format("FF AG Goto waypoint UID=%s Index=%s, Speed=%s, Formation=%s", tostring(UID), tostring(n), tostring(Speed), tostring(Formation))) - if n then - - -- TODO: switch to re-enable waypoint tasks. - if false then - local tasks=self:GetTasksWaypoint(n) - - for _,_task in pairs(tasks) do - local task=_task --Ops.OpsGroup#OPSGROUP.Task - task.status=OPSGROUP.TaskStatus.SCHEDULED - end - - end -- Speed to waypoint. Speed=Speed or self:GetSpeedToWaypoint(n) -- Update the route. - self:UpdateRoute(n, Speed, Formation) + self:__UpdateRoute(-0.01, n, nil, Speed, Formation) end @@ -662,6 +1077,88 @@ function ARMYGROUP:onafterDetour(From, Event, To, Coordinate, Speed, Formation, end +--- On after "OutOfAmmo" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARMYGROUP:onafterOutOfAmmo(From, Event, To) + self:I(self.lid..string.format("Group is out of ammo at t=%.3f", timer.getTime())) + + -- Get current task. + local task=self:GetTaskCurrent() + + if task then + if task.dcstask.id=="FireAtPoint" or task.dcstask.id==AUFTRAG.SpecialTask.BARRAGE then + self:I(self.lid..string.format("Cancelling current %s task because out of ammo!", task.dcstask.id)) + self:TaskCancel(task) + end + end + + -- Fist, check if we want to rearm once out-of-ammo. + --TODO: IsMobile() check + if self.rearmOnOutOfAmmo then + local truck, dist=self:FindNearestAmmoSupply(30) + if truck then + self:T(self.lid..string.format("Found Ammo Truck %s [%s]", truck:GetName(), truck:GetTypeName())) + local Coordinate=truck:GetCoordinate() + self:__Rearm(-1, Coordinate) + return + end + end + + -- Second, check if we want to retreat once out of ammo. + if self.retreatOnOutOfAmmo then + self:__Retreat(-1) + return + end + + -- Third, check if we want to RTZ once out of ammo. + if self.rtzOnOutOfAmmo then + self:__RTZ(-1) + end + +end + + +--- On before "Rearm" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Point#COORDINATE Coordinate Coordinate where to rearm. +-- @param #number Formation Formation of the group. +function ARMYGROUP:onbeforeRearm(From, Event, To, Coordinate, Formation) + + local dt=nil + local allowed=true + + -- Pause current mission. + if self.currentmission and self.currentmission>0 then + self:T(self.lid.."Rearm command but have current mission ==> Pausing mission!") + self:PauseMission() + dt=-0.1 + allowed=false + end + + -- Disengage. + if self:IsEngaging() then + self:T(self.lid.."Rearm command but currently engaging ==> Disengage!") + self:Disengage() + dt=-0.1 + allowed=false + end + + -- Try again... + if dt then + self:T(self.lid..string.format("Trying Rearm again in %.2f sec", dt)) + self:__Rearm(dt, Coordinate, Formation) + allowed=false + end + + return allowed +end + --- On after "Rearm" event. -- @param #ARMYGROUP self -- @param #string From From state. @@ -671,6 +1168,9 @@ end -- @param #number Formation Formation of the group. function ARMYGROUP:onafterRearm(From, Event, To, Coordinate, Formation) + -- Debug info. + self:I(self.lid..string.format("Group send to rearm")) + -- ID of current waypoint. local uid=self:GetWaypointCurrent().uid @@ -682,6 +1182,78 @@ function ARMYGROUP:onafterRearm(From, Event, To, Coordinate, Formation) end +--- On after "Rearmed" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARMYGROUP:onafterRearmed(From, Event, To) + self:I(self.lid.."Group rearmed") + + -- Check group done. + self:_CheckGroupDone(1) +end + +--- On after "RTZ" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Zone#ZONE Zone The zone to return to. +-- @param #number Formation Formation of the group. +function ARMYGROUP:onafterRTZ(From, Event, To, Zone, Formation) + + -- ID of current waypoint. + local uid=self:GetWaypointCurrent().uid + + -- Zone. + local zone=Zone or self.homezone + + if zone then + + if self:IsInZone(zone) then + self:Returned() + else + + -- Debug info. + self:I(self.lid..string.format("RTZ to Zone %s", zone:GetName())) + + local Coordinate=zone:GetRandomCoordinate() + + -- Add waypoint after current. + local wp=self:AddWaypoint(Coordinate, nil, uid, Formation, true) + + -- Set if we want to resume route after reaching the detour waypoint. + wp.detour=0 + + end + + else + self:E(self.lid.."ERROR: No RTZ zone given!") + end + +end + +--- On after "Returned" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARMYGROUP:onafterReturned(From, Event, To) + + -- Debug info. + self:T(self.lid..string.format("Group returned")) + + if self.legion then + -- Debug info. + self:T(self.lid..string.format("Adding group back to warehouse stock")) + + -- Add asset back in 10 seconds. + self.legion:__AddAsset(10, self.group, 1) + end + +end + --- On after "Rearming" event. -- @param #ARMYGROUP self -- @param #string From From state. @@ -787,14 +1359,32 @@ end -- @param Wrapper.Group#GROUP Group the group to be engaged. function ARMYGROUP:onbeforeEngageTarget(From, Event, To, Target) + local dt=nil + local allowed=true + local ammo=self:GetAmmoTot() if ammo.Total==0 then self:E(self.lid.."WARNING: Cannot engage TARGET because no ammo left!") return false end + + -- Pause current mission. + if self.currentmission and self.currentmission>0 then + self:T(self.lid.."Engage command but have current mission ==> Pausing mission!") + self:PauseMission() + dt=-0.1 + allowed=false + end - return true + -- Try again... + if dt then + self:T(self.lid..string.format("Trying Engage again in %.2f sec", dt)) + self:__EngageTarget(dt, Target) + allowed=false + end + + return allowed end --- On after "EngageTarget" event. @@ -804,27 +1394,35 @@ end -- @param #string To To state. -- @param Wrapper.Group#GROUP Group the group to be engaged. function ARMYGROUP:onafterEngageTarget(From, Event, To, Target) + self:T(self.lid.."Engaging Target") if Target:IsInstanceOf("TARGET") then self.engage.Target=Target else self.engage.Target=TARGET:New(Target) end - + -- Target coordinate. - self.engage.Coordinate=UTILS.DeepCopy(self.engage.Target:GetCoordinate()) + self.engage.Coordinate=UTILS.DeepCopy(self.engage.Target:GetCoordinate()) + - -- TODO: Backup current ROE and alarm state and reset after disengage. + local intercoord=self:GetCoordinate():GetIntermediateCoordinate(self.engage.Coordinate, 0.9) + + + + -- Backup ROE and alarm state. + self.engage.roe=self:GetROE() + self.engage.alarmstate=self:GetAlarmstate() -- Switch ROE and alarm state. self:SwitchAlarmstate(ENUMS.AlarmState.Auto) - self:SwitchROE(ENUMS.ROE.WeaponFree) + self:SwitchROE(ENUMS.ROE.OpenFire) -- ID of current waypoint. local uid=self:GetWaypointCurrent().uid -- Add waypoint after current. - self.engage.Waypoint=self:AddWaypoint(self.engage.Coordinate, nil, uid, Formation, true) + self.engage.Waypoint=self:AddWaypoint(intercoord, nil, uid, Formation, true) -- Set if we want to resume route after reaching the detour waypoint. self.engage.Waypoint.detour=1 @@ -836,17 +1434,19 @@ end function ARMYGROUP:_UpdateEngageTarget() if self.engage.Target and self.engage.Target:IsAlive() then - - --env.info("FF Update Engage Target "..self.engage.Target:GetName()) - local vec3=self.engage.Target:GetCoordinate():GetVec3() + -- Get current position vector. + local vec3=self.engage.Target:GetVec3() - local dist=UTILS.VecDist2D(vec3, self.engage.Coordinate:GetVec3()) + -- Distance to last known position of target. + local dist=UTILS.VecDist3D(vec3, self.engage.Coordinate:GetVec3()) + -- Check if target moved more than 100 meters. if dist>100 then --env.info("FF Update Engage Target Moved "..self.engage.Target:GetName()) + -- Update new position. self.engage.Coordinate:UpdateFromVec3(vec3) -- ID of current waypoint. @@ -854,9 +1454,11 @@ function ARMYGROUP:_UpdateEngageTarget() -- Remove current waypoint self:RemoveWaypointByID(self.engage.Waypoint.uid) + + local intercoord=self:GetCoordinate():GetIntermediateCoordinate(self.engage.Coordinate, 0.9) -- Add waypoint after current. - self.engage.Waypoint=self:AddWaypoint(self.engage.Coordinate, nil, uid, Formation, true) + self.engage.Waypoint=self:AddWaypoint(intercoord, nil, uid, Formation, true) -- Set if we want to resume route after reaching the detour waypoint. self.engage.Waypoint.detour=0 @@ -864,7 +1466,10 @@ function ARMYGROUP:_UpdateEngageTarget() end else + + -- Target not alive any more == Disengage. self:Disengage() + end end @@ -875,19 +1480,19 @@ end -- @param #string Event Event. -- @param #string To To state. function ARMYGROUP:onafterDisengage(From, Event, To) - -- TODO: Reset ROE and alarm state. - self:_CheckGroupDone(1) -end + self:T(self.lid.."Disengage Target") ---- On after "Rearmed" event. --- @param #ARMYGROUP self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. -function ARMYGROUP:onafterRearmed(From, Event, To) + -- Restore previous ROE and alarm state. + self:SwitchROE(self.engage.roe) + self:SwitchAlarmstate(self.engage.alarmstate) + + -- Remove current waypoint + if self.engage.Waypoint then + self:RemoveWaypointByID(self.engage.Waypoint.uid) + end + -- Check group is done self:_CheckGroupDone(1) - end --- On after "DetourReached" event. @@ -896,7 +1501,7 @@ end -- @param #string Event Event. -- @param #string To To state. function ARMYGROUP:onafterDetourReached(From, Event, To) - self:I(self.lid.."Group reached detour coordinate.") + self:T(self.lid.."Group reached detour coordinate") end @@ -907,6 +1512,9 @@ end -- @param #string To To state. function ARMYGROUP:onafterFullStop(From, Event, To) + -- Debug info. + self:T(self.lid..string.format("Full stop!")) + -- Get current position. local pos=self:GetCoordinate() @@ -927,129 +1535,12 @@ end -- @param #number Formation Formation. function ARMYGROUP:onafterCruise(From, Event, To, Speed, Formation) - self:__UpdateRoute(-1, nil, Speed, Formation) + -- Not waiting anymore. + self.Twaiting=nil + self.dTwait=nil -end + self:__UpdateRoute(-0.1, nil, nil, Speed, Formation) ---- On after "Stop" event. --- @param #ARMYGROUP self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. -function ARMYGROUP:onafterStop(From, Event, To) - - -- Handle events: - self:UnHandleEvent(EVENTS.Birth) - self:UnHandleEvent(EVENTS.Dead) - self:UnHandleEvent(EVENTS.RemoveUnit) - - -- Call OPSGROUP function. - self:GetParent(self).onafterStop(self, From, Event, To) - -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Events DCS -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Event function handling the birth of a unit. --- @param #ARMYGROUP self --- @param Core.Event#EVENTDATA EventData Event data. -function ARMYGROUP:OnEventBirth(EventData) - - -- Check that this is the right group. - if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then - local unit=EventData.IniUnit - local group=EventData.IniGroup - local unitname=EventData.IniUnitName - - if self.respawning then - - local function reset() - self.respawning=nil - end - - -- Reset switch in 1 sec. This should allow all birth events of n>1 groups to have passed. - -- TODO: Can I do this more rigorously? - self:ScheduleOnce(1, reset) - - else - - -- Get element. - local element=self:GetElementByName(unitname) - - -- Set element to spawned state. - self:T3(self.lid..string.format("EVENT: Element %s born ==> spawned", element.name)) - self:ElementSpawned(element) - - end - - end - -end - ---- Event function handling the crash of a unit. --- @param #ARMYGROUP self --- @param Core.Event#EVENTDATA EventData Event data. -function ARMYGROUP:OnEventDead(EventData) - - -- Check that this is the right group. - if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then - self:T(self.lid..string.format("EVENT: Unit %s dead!", EventData.IniUnitName)) - - local unit=EventData.IniUnit - local group=EventData.IniGroup - local unitname=EventData.IniUnitName - - -- Get element. - local element=self:GetElementByName(unitname) - - if element then - self:T(self.lid..string.format("EVENT: Element %s dead ==> destroyed", element.name)) - self:ElementDestroyed(element) - end - - end - -end - ---- Event function handling when a unit is removed from the game. --- @param #ARMYGROUP self --- @param Core.Event#EVENTDATA EventData Event data. -function ARMYGROUP:OnEventRemoveUnit(EventData) - - -- Check that this is the right group. - if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then - local unit=EventData.IniUnit - local group=EventData.IniGroup - local unitname=EventData.IniUnitName - - -- Get element. - local element=self:GetElementByName(unitname) - - if element then - self:T(self.lid..string.format("EVENT: Element %s removed ==> dead", element.name)) - self:ElementDead(element) - end - - end - -end - ---- Event function handling when a unit is hit. --- @param #ARMYGROUP self --- @param Core.Event#EVENTDATA EventData Event data. -function ARMYGROUP:OnEventHit(EventData) - - -- Check that this is the right group. - if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then - local unit=EventData.IniUnit - local group=EventData.IniGroup - local unitname=EventData.IniUnitName - - -- TODO: suppression - - end end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1066,16 +1557,12 @@ end -- @return Ops.OpsGroup#OPSGROUP.Waypoint Waypoint table. function ARMYGROUP:AddWaypoint(Coordinate, Speed, AfterWaypointWithID, Formation, Updateroute) + -- Create coordinate. local coordinate=self:_CoordinateFromObject(Coordinate) -- Set waypoint index. local wpnumber=self:GetWaypointIndexAfterID(AfterWaypointWithID) - -- Check if final waypoint is still passed. - if wpnumber>self.currentwp then - self.passedfinalwp=false - end - -- Speed in knots. Speed=Speed or self:GetSpeedCruise() @@ -1101,7 +1588,7 @@ function ARMYGROUP:AddWaypoint(Coordinate, Speed, AfterWaypointWithID, Formation -- Update route. if Updateroute==nil or Updateroute==true then - self:_CheckGroupDone(1) + self:__UpdateRoute(-0.01) end return waypoint @@ -1109,28 +1596,24 @@ end --- Initialize group parameters. Also initializes waypoints if self.waypoints is nil. -- @param #ARMYGROUP self +-- @param #table Template Template used to init the group. Default is `self.template`. -- @return #ARMYGROUP self -function ARMYGROUP:_InitGroup() +function ARMYGROUP:_InitGroup(Template) -- First check if group was already initialized. if self.groupinitialized then - self:E(self.lid.."WARNING: Group was already initialized!") + self:T(self.lid.."WARNING: Group was already initialized! Will NOT do it again!") return end -- Get template of group. - self.template=self.group:GetTemplate() - - -- Define category. - self.isAircraft=false - self.isNaval=false - self.isGround=true + local template=Template or self:_GetTemplate() -- Ground are always AI. self.isAI=true -- Is (template) group late activated. - self.isLateActivated=self.template.lateActivation + self.isLateActivated=template.lateActivation -- Ground groups cannot be uncontrolled. self.isUncontrolled=false @@ -1153,7 +1636,7 @@ function ARMYGROUP:_InitGroup() self:SetDefaultRadio(self.radio.Freq, self.radio.Modu, self.radio.On) -- Set default formation from first waypoint. - self.optionDefault.Formation=self:GetWaypoint(1).action + self.optionDefault.Formation=template.route.points[1].action --self:GetWaypoint(1).action -- Default TACAN off. self:SetDefaultTACAN(nil, nil, nil, nil, true) @@ -1162,65 +1645,22 @@ function ARMYGROUP:_InitGroup() -- Units of the group. local units=self.group:GetUnits() - for _,_unit in pairs(units) do - local unit=_unit --Wrapper.Unit#UNIT - - -- TODO: this is wrong when grouping is used! - local unittemplate=unit:GetTemplate() - - local element={} --#ARMYGROUP.Element - element.name=unit:GetName() - element.unit=unit - element.status=OPSGROUP.ElementStatus.INUTERO - element.typename=unit:GetTypeName() - element.skill=unittemplate.skill or "Unknown" - element.ai=true - element.category=element.unit:GetUnitCategory() - element.categoryname=element.unit:GetCategoryName() - element.size, element.length, element.height, element.width=unit:GetObjectSize() - element.ammo0=self:GetAmmoUnit(unit, false) - element.life0=unit:GetLife0() - element.life=element.life0 - - -- Debug text. - if self.verbose>=2 then - local text=string.format("Adding element %s: status=%s, skill=%s, life=%.3f category=%s (%d), size: %.1f (L=%.1f H=%.1f W=%.1f)", - element.name, element.status, element.skill, element.life, element.categoryname, element.category, element.size, element.length, element.height, element.width) - self:I(self.lid..text) - end + -- DCS group. + local dcsgroup=Group.getByName(self.groupname) + local size0=dcsgroup:getInitialSize() - -- Add element to table. - table.insert(self.elements, element) - - -- Get Descriptors. - self.descriptors=self.descriptors or unit:GetDesc() - - -- Set type name. - self.actype=self.actype or unit:GetTypeName() - - if unit:IsAlive() then - -- Trigger spawned event. - self:ElementSpawned(element) - end - + -- Quick check. + if #units~=size0 then + self:E(self.lid..string.format("ERROR: Got #units=%d but group consists of %d units!", #units, size0)) end + + -- Add elemets. + for _,unit in pairs(units) do + local unitname=unit:GetName() + self:_AddElementByName(unitname) + end + - -- Debug info. - if self.verbose>=1 then - local text=string.format("Initialized Army Group %s:\n", self.groupname) - text=text..string.format("Unit type = %s\n", self.actype) - text=text..string.format("Speed max = %.1f Knots\n", UTILS.KmphToKnots(self.speedMax)) - text=text..string.format("Speed cruise = %.1f Knots\n", UTILS.KmphToKnots(self.speedCruise)) - text=text..string.format("Elements = %d\n", #self.elements) - text=text..string.format("Waypoints = %d\n", #self.waypoints) - text=text..string.format("Radio = %.1f MHz %s %s\n", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu), tostring(self.radio.On)) - text=text..string.format("Ammo = %d (G=%d/R=%d/M=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Rockets, self.ammo.Missiles) - text=text..string.format("FSM state = %s\n", self:GetState()) - text=text..string.format("Is alive = %s\n", tostring(self:IsAlive())) - text=text..string.format("LateActivate = %s\n", tostring(self:IsLateActivated())) - self:I(self.lid..text) - end - -- Init done. self.groupinitialized=true @@ -1276,7 +1716,54 @@ end -- Misc Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Find the neares ammo supply group within a given radius. +-- @param #ARMYGROUP self +-- @param #number Radius Search radius in NM. Default 30 NM. +-- @return Wrapper.Group#GROUP Closest ammo supplying group or `nil` if no group is in the given radius. +-- @return #number Distance to closest group in meters. +function ARMYGROUP:FindNearestAmmoSupply(Radius) + -- Radius in meters. + Radius=UTILS.NMToMeters(Radius or 30) + + -- Current positon. + local coord=self:GetCoordinate() + + -- Get my coalition. + local myCoalition=self:GetCoalition() + + -- Scanned units. + local units=coord:ScanUnits(Radius) + + -- Find closest + local dmin=math.huge + local truck=nil --Wrapper.Unit#UNIT + for _,_unit in pairs(units.Set) do + local unit=_unit --Wrapper.Unit#UNIT + + -- Check coaliton and if unit can supply ammo. + if unit:IsAlive() and unit:GetCoalition()==myCoalition and unit:IsAmmoSupply() and unit:GetVelocityKMH()<1 then + + -- Distance. + local d=coord:Get2DDistance(unit:GetCoord()) + + -- Check if distance is smaller. + if d Create a new auftrag. -- TODO: F10 marker to create new missions. -- TODO: Add recovery tanker mission for boat ops. +-- DONE: Added auftrag category. +-- DONE: Missions can be assigned to multiple legions. +-- DONE: Option to assign a specific payload for the mission (requires an AIRWING). +-- NOPE: Clone mission. How? Deepcopy? ==> Create a new auftrag. +-- DONE: Recon mission. What input? Set of coordinates? -- DONE: Option to assign mission to specific squadrons (requires an AIRWING). -- DONE: Add mission start conditions. -- DONE: Add rescue helo mission for boat ops. @@ -495,20 +603,22 @@ function AUFTRAG:New(Type) -- State is planned. self.status=AUFTRAG.Status.PLANNED - -- Defaults - --self:SetVerbosity(0) + -- Defaults . self:SetName() self:SetPriority() self:SetTime() + self:SetRequiredAssets() + --self:SetRequiredCarriers() self.engageAsGroup=true + self.dTevaluate=5 + + -- Init counters and stuff. self.repeated=0 self.repeatedSuccess=0 self.repeatedFailure=0 self.Nrepeat=0 self.NrepeatFailure=0 self.NrepeatSuccess=0 - self.nassets=1 - self.dTevaluate=5 self.Ncasualties=0 self.Nkills=0 self.Nelements=0 @@ -517,20 +627,19 @@ function AUFTRAG:New(Type) self:SetStartState(self.status) -- PLANNED --> (QUEUED) --> (REQUESTED) --> SCHEDULED --> STARTED --> EXECUTING --> DONE - - self:AddTransition("*", "Planned", AUFTRAG.Status.PLANNED) -- Mission is in planning stage. - self:AddTransition(AUFTRAG.Status.PLANNED, "Queued", AUFTRAG.Status.QUEUED) -- Mission is in queue of an AIRWING. + self:AddTransition("*", "Planned", AUFTRAG.Status.PLANNED) -- Mission is in planning stage. Could be in the queue of a COMMANDER or CHIEF. + self:AddTransition(AUFTRAG.Status.PLANNED, "Queued", AUFTRAG.Status.QUEUED) -- Mission is in queue of a LEGION. self:AddTransition(AUFTRAG.Status.QUEUED, "Requested", AUFTRAG.Status.REQUESTED) -- Mission assets have been requested from the warehouse. self:AddTransition(AUFTRAG.Status.REQUESTED, "Scheduled", AUFTRAG.Status.SCHEDULED) -- Mission added to the first ops group queue. self:AddTransition(AUFTRAG.Status.PLANNED, "Scheduled", AUFTRAG.Status.SCHEDULED) -- From planned directly to scheduled. - self:AddTransition(AUFTRAG.Status.SCHEDULED, "Started", AUFTRAG.Status.STARTED) -- First asset has started the mission + self:AddTransition(AUFTRAG.Status.SCHEDULED, "Started", AUFTRAG.Status.STARTED) -- First asset has started the mission. self:AddTransition(AUFTRAG.Status.STARTED, "Executing", AUFTRAG.Status.EXECUTING) -- First asset is executing the mission. self:AddTransition("*", "Done", AUFTRAG.Status.DONE) -- All assets have reported that mission is done. - self:AddTransition("*", "Cancel", "*") -- Command to cancel the mission. + self:AddTransition("*", "Cancel", AUFTRAG.Status.CANCELLED) -- Command to cancel the mission. self:AddTransition("*", "Success", AUFTRAG.Status.SUCCESS) self:AddTransition("*", "Failed", AUFTRAG.Status.FAILED) @@ -543,6 +652,213 @@ function AUFTRAG:New(Type) self:AddTransition("*", "ElementDestroyed", "*") self:AddTransition("*", "GroupDead", "*") self:AddTransition("*", "AssetDead", "*") + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Status". + -- @function [parent=#AUFTRAG] Status + -- @param #AUFTRAG self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#AUFTRAG] __Status + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". + -- @function [parent=#AUFTRAG] Stop + -- @param #AUFTRAG self + + --- Triggers the FSM event "Stop" after a delay. + -- @function [parent=#AUFTRAG] __Stop + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Planned". + -- @function [parent=#AUFTRAG] Planned + -- @param #AUFTRAG self + + --- Triggers the FSM event "Planned" after a delay. + -- @function [parent=#AUFTRAG] __Planned + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- On after "Planned" event. + -- @function [parent=#AUFTRAG] OnAfterPlanned + -- @param #AUFTRAG self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Queued". + -- @function [parent=#AUFTRAG] Queued + -- @param #AUFTRAG self + + --- Triggers the FSM event "Queued" after a delay. + -- @function [parent=#AUFTRAG] __Queued + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- On after "Queued" event. + -- @function [parent=#AUFTRAG] OnAfterQueued + -- @param #AUFTRAG self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Requested". + -- @function [parent=#AUFTRAG] Requested + -- @param #AUFTRAG self + + --- Triggers the FSM event "Requested" after a delay. + -- @function [parent=#AUFTRAG] __Requested + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- On after "Requested" event. + -- @function [parent=#AUFTRAG] OnAfterRequested + -- @param #AUFTRAG self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Scheduled". + -- @function [parent=#AUFTRAG] Scheduled + -- @param #AUFTRAG self + + --- Triggers the FSM event "Scheduled" after a delay. + -- @function [parent=#AUFTRAG] __Scheduled + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- On after "Scheduled" event. + -- @function [parent=#AUFTRAG] OnAfterScheduled + -- @param #AUFTRAG self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Started". + -- @function [parent=#AUFTRAG] Started + -- @param #AUFTRAG self + + --- Triggers the FSM event "Started" after a delay. + -- @function [parent=#AUFTRAG] __Started + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- On after "Started" event. + -- @function [parent=#AUFTRAG] OnAfterStarted + -- @param #AUFTRAG self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Executing". + -- @function [parent=#AUFTRAG] Executing + -- @param #AUFTRAG self + + --- Triggers the FSM event "Executing" after a delay. + -- @function [parent=#AUFTRAG] __Executing + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- On after "Executing" event. + -- @function [parent=#AUFTRAG] OnAfterExecuting + -- @param #AUFTRAG self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Cancel". + -- @function [parent=#AUFTRAG] Cancel + -- @param #AUFTRAG self + + --- Triggers the FSM event "Cancel" after a delay. + -- @function [parent=#AUFTRAG] __Cancel + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- On after "Cancel" event. + -- @function [parent=#AUFTRAG] OnAfterCancel + -- @param #AUFTRAG self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Done". + -- @function [parent=#AUFTRAG] Done + -- @param #AUFTRAG self + + --- Triggers the FSM event "Done" after a delay. + -- @function [parent=#AUFTRAG] __Done + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- On after "Done" event. + -- @function [parent=#AUFTRAG] OnAfterDone + -- @param #AUFTRAG self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Success". + -- @function [parent=#AUFTRAG] Success + -- @param #AUFTRAG self + + --- Triggers the FSM event "Success" after a delay. + -- @function [parent=#AUFTRAG] __Success + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- On after "Success" event. + -- @function [parent=#AUFTRAG] OnAfterSuccess + -- @param #AUFTRAG self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- Triggers the FSM event "Failure". + -- @function [parent=#AUFTRAG] Failure + -- @param #AUFTRAG self + + --- Triggers the FSM event "Failure" after a delay. + -- @function [parent=#AUFTRAG] __Failure + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- On after "Failure" event. + -- @function [parent=#AUFTRAG] OnAfterFailure + -- @param #AUFTRAG self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- Triggers the FSM event "Repeat". + -- @function [parent=#AUFTRAG] Repeat + -- @param #AUFTRAG self + + --- Triggers the FSM event "Repeat" after a delay. + -- @function [parent=#AUFTRAG] __Repeat + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- On after "Repeat" event. + -- @function [parent=#AUFTRAG] OnAfterRepeat + -- @param #AUFTRAG self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. -- Init status update. self:__Status(-1) @@ -554,7 +870,7 @@ end -- Create Missions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Create an ANTI-SHIP mission. +--- **[AIR]** Create an ANTI-SHIP mission. -- @param #AUFTRAG self -- @param Wrapper.Positionable#POSITIONABLE Target The target to attack. Can be passed as a @{Wrapper.Group#GROUP} or @{Wrapper.Unit#UNIT} object. -- @param #number Altitude Engage altitude in feet. Default 2000 ft. @@ -577,12 +893,14 @@ function AUFTRAG:NewANTISHIP(Target, Altitude) mission.optionROE=ENUMS.ROE.OpenFire mission.optionROT=ENUMS.ROT.EvadeFire + mission.categories={AUFTRAG.Category.AIRCRAFT} + mission.DCStask=mission:GetDCSMissionTask() return mission end ---- Create an ORBIT mission, which can be either a circular orbit or a race-track pattern. +--- **[AIR]** Create an ORBIT mission, which can be either a circular orbit or a race-track pattern. -- @param #AUFTRAG self -- @param Core.Point#COORDINATE Coordinate Where to orbit. -- @param #number Altitude Orbit altitude in feet. Default is y component of `Coordinate`. @@ -618,13 +936,15 @@ function AUFTRAG:NewORBIT(Coordinate, Altitude, Speed, Heading, Leg) mission.missionFraction=0.9 mission.optionROE=ENUMS.ROE.ReturnFire mission.optionROT=ENUMS.ROT.PassiveDefense + + mission.categories={AUFTRAG.Category.AIRCRAFT} mission.DCStask=mission:GetDCSMissionTask() return mission end ---- Create an ORBIT mission, where the aircraft will go in a circle around the specified coordinate. +--- **[AIR]** Create an ORBIT mission, where the aircraft will go in a circle around the specified coordinate. -- @param #AUFTRAG self -- @param Core.Point#COORDINATE Coordinate Position where to orbit around. -- @param #number Altitude Orbit altitude in feet. Default is y component of `Coordinate`. @@ -637,7 +957,7 @@ function AUFTRAG:NewORBIT_CIRCLE(Coordinate, Altitude, Speed) return mission end ---- Create an ORBIT mission, where the aircraft will fly a race-track pattern. +--- **[AIR]** Create an ORBIT mission, where the aircraft will fly a race-track pattern. -- @param #AUFTRAG self -- @param Core.Point#COORDINATE Coordinate Where to orbit. -- @param #number Altitude Orbit altitude in feet. Default is y component of `Coordinate`. @@ -655,7 +975,7 @@ function AUFTRAG:NewORBIT_RACETRACK(Coordinate, Altitude, Speed, Heading, Leg) return mission end ---- Create a Ground Controlled CAP (GCICAP) mission. Flights with this task are considered for A2A INTERCEPT missions by the CHIEF class. They will perform a compat air patrol but not engage by +--- **[AIR]** Create a Ground Controlled CAP (GCICAP) mission. Flights with this task are considered for A2A INTERCEPT missions by the CHIEF class. They will perform a compat air patrol but not engage by -- themselfs. They wait for the CHIEF to tell them whom to engage. -- @param #AUFTRAG self -- @param Core.Point#COORDINATE Coordinate Where to orbit. @@ -678,17 +998,19 @@ function AUFTRAG:NewGCICAP(Coordinate, Altitude, Speed, Heading, Leg) mission.missionTask=ENUMS.MissionTask.INTERCEPT mission.optionROT=ENUMS.ROT.PassiveDefense + mission.categories={AUFTRAG.Category.AIRCRAFT} + return mission end ---- Create a TANKER mission. +--- **[AIR]** Create a TANKER mission. -- @param #AUFTRAG self -- @param Core.Point#COORDINATE Coordinate Where to orbit. -- @param #number Altitude Orbit altitude in feet. Default is y component of `Coordinate`. -- @param #number Speed Orbit speed in knots. Default 350 kts. -- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West). -- @param #number Leg Length of race-track in NM. Default 10 NM. --- @param #number RefuelSystem Refueling system (1=boom, 0=probe). This info is *only* for AIRWINGs so they launch the right tanker type. +-- @param #number RefuelSystem Refueling system (0=boom, 1=probe). This info is *only* for AIRWINGs so they launch the right tanker type. -- @return #AUFTRAG self function AUFTRAG:NewTANKER(Coordinate, Altitude, Speed, Heading, Leg, RefuelSystem) @@ -707,12 +1029,14 @@ function AUFTRAG:NewTANKER(Coordinate, Altitude, Speed, Heading, Leg, RefuelSyst mission.optionROE=ENUMS.ROE.WeaponHold mission.optionROT=ENUMS.ROT.PassiveDefense + mission.categories={AUFTRAG.Category.AIRCRAFT} + mission.DCStask=mission:GetDCSMissionTask() return mission end ---- Create a AWACS mission. +--- **[AIR]** Create a AWACS mission. -- @param #AUFTRAG self -- @param Core.Point#COORDINATE Coordinate Where to orbit. Altitude is also taken from the coordinate. -- @param #number Altitude Orbit altitude in feet. Default is y component of `Coordinate`. @@ -735,6 +1059,8 @@ function AUFTRAG:NewAWACS(Coordinate, Altitude, Speed, Heading, Leg) mission.optionROE=ENUMS.ROE.WeaponHold mission.optionROT=ENUMS.ROT.PassiveDefense + mission.categories={AUFTRAG.Category.AIRCRAFT} + mission.DCStask=mission:GetDCSMissionTask() return mission @@ -742,7 +1068,7 @@ end ---- Create an INTERCEPT mission. +--- **[AIR]** Create an INTERCEPT mission. -- @param #AUFTRAG self -- @param Wrapper.Positionable#POSITIONABLE Target The target to intercept. Can also be passed as simple @{Wrapper.Group#GROUP} or @{Wrapper.Unit#UNIT} object. -- @return #AUFTRAG self @@ -758,12 +1084,14 @@ function AUFTRAG:NewINTERCEPT(Target) mission.optionROE=ENUMS.ROE.OpenFire mission.optionROT=ENUMS.ROT.EvadeFire + mission.categories={AUFTRAG.Category.AIRCRAFT} + mission.DCStask=mission:GetDCSMissionTask() return mission end ---- Create a CAP mission. +--- **[AIR]** Create a CAP mission. -- @param #AUFTRAG self -- @param Core.Zone#ZONE_RADIUS ZoneCAP Circular CAP zone. Detected targets in this zone will be engaged. -- @param #number Altitude Altitude at which to orbit in feet. Default is 10,000 ft. @@ -798,12 +1126,14 @@ function AUFTRAG:NewCAP(ZoneCAP, Altitude, Speed, Coordinate, Heading, Leg, Targ mission.optionROE=ENUMS.ROE.OpenFire mission.optionROT=ENUMS.ROT.EvadeFire + mission.categories={AUFTRAG.Category.AIRCRAFT} + mission.DCStask=mission:GetDCSMissionTask() return mission end ---- Create a CAS mission. +--- **[AIR]** Create a CAS mission. -- @param #AUFTRAG self -- @param Core.Zone#ZONE_RADIUS ZoneCAS Circular CAS zone. Detected targets in this zone will be engaged. -- @param #number Altitude Altitude at which to orbit. Default is 10,000 ft. @@ -811,7 +1141,7 @@ end -- @param Core.Point#COORDINATE Coordinate Where to orbit. Default is the center of the CAS zone. -- @param #number Heading Heading of race-track pattern in degrees. If not specified, a simple circular orbit is performed. -- @param #number Leg Length of race-track in NM. If not specified, a simple circular orbit is performed. --- @param #table TargetTypes (Optional) Table of target types. Default {"Helicopters", "Ground Units", "Light armed ships"}. +-- @param #table TargetTypes (Optional) Table of target types. Default `{"Helicopters", "Ground Units", "Light armed ships"}`. -- @return #AUFTRAG self function AUFTRAG:NewCAS(ZoneCAS, Altitude, Speed, Coordinate, Heading, Leg, TargetTypes) @@ -837,13 +1167,15 @@ function AUFTRAG:NewCAS(ZoneCAS, Altitude, Speed, Coordinate, Heading, Leg, Targ mission.missionTask=ENUMS.MissionTask.CAS mission.optionROE=ENUMS.ROE.OpenFire mission.optionROT=ENUMS.ROT.EvadeFire + + mission.categories={AUFTRAG.Category.AIRCRAFT} mission.DCStask=mission:GetDCSMissionTask() return mission end ---- Create a FACA mission. +--- **[AIR]** Create a FACA mission. -- @param #AUFTRAG self -- @param Wrapper.Group#GROUP Target Target group. Must be a GROUP object. -- @param #string Designation Designation of target. See `AI.Task.Designation`. Default `AI.Task.Designation.AUTO`. @@ -871,6 +1203,8 @@ function AUFTRAG:NewFACA(Target, Designation, DataLink, Frequency, Modulation) mission.missionFraction=0.5 mission.optionROE=ENUMS.ROE.ReturnFire mission.optionROT=ENUMS.ROT.PassiveDefense + + mission.categories={AUFTRAG.Category.AIRCRAFT} mission.DCStask=mission:GetDCSMissionTask() @@ -878,10 +1212,10 @@ function AUFTRAG:NewFACA(Target, Designation, DataLink, Frequency, Modulation) end ---- Create a BAI mission. +--- **[AIR]** Create a BAI mission. -- @param #AUFTRAG self -- @param Wrapper.Positionable#POSITIONABLE Target The target to attack. Can be a GROUP, UNIT or STATIC object. --- @param #number Altitude Engage altitude in feet. Default 2000 ft. +-- @param #number Altitude Engage altitude in feet. Default 5000 ft. -- @return #AUFTRAG self function AUFTRAG:NewBAI(Target, Altitude) @@ -892,7 +1226,7 @@ function AUFTRAG:NewBAI(Target, Altitude) -- DCS Task options: mission.engageWeaponType=ENUMS.WeaponFlag.AnyAG mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL - mission.engageAltitude=UTILS.FeetToMeters(Altitude or 2000) + mission.engageAltitude=UTILS.FeetToMeters(Altitude or 5000) -- Mission options: mission.missionTask=ENUMS.MissionTask.GROUNDATTACK @@ -901,15 +1235,17 @@ function AUFTRAG:NewBAI(Target, Altitude) mission.optionROE=ENUMS.ROE.OpenFire mission.optionROT=ENUMS.ROT.PassiveDefense + mission.categories={AUFTRAG.Category.AIRCRAFT} + mission.DCStask=mission:GetDCSMissionTask() return mission end ---- Create a SEAD mission. +--- **[AIR]** Create a SEAD mission. -- @param #AUFTRAG self -- @param Wrapper.Positionable#POSITIONABLE Target The target to attack. Can be a GROUP or UNIT object. --- @param #number Altitude Engage altitude in feet. Default 2000 ft. +-- @param #number Altitude Engage altitude in feet. Default 8000 ft. -- @return #AUFTRAG self function AUFTRAG:NewSEAD(Target, Altitude) @@ -920,7 +1256,7 @@ function AUFTRAG:NewSEAD(Target, Altitude) -- DCS Task options: mission.engageWeaponType=ENUMS.WeaponFlag.AnyAG --ENUMS.WeaponFlag.Cannons mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL - mission.engageAltitude=UTILS.FeetToMeters(Altitude or 2000) + mission.engageAltitude=UTILS.FeetToMeters(Altitude or 8000) -- Mission options: mission.missionTask=ENUMS.MissionTask.SEAD @@ -930,14 +1266,16 @@ function AUFTRAG:NewSEAD(Target, Altitude) mission.optionROT=ENUMS.ROT.EvadeFire --mission.optionROT=ENUMS.ROT.AllowAbortMission + mission.categories={AUFTRAG.Category.AIRCRAFT} + mission.DCStask=mission:GetDCSMissionTask() return mission end ---- Create a STRIKE mission. Flight will attack the closest map object to the specified coordinate. +--- **[AIR]** Create a STRIKE mission. Flight will attack the closest map object to the specified coordinate. -- @param #AUFTRAG self --- @param Core.Point#COORDINATE Target The target coordinate. Can also be given as a GROUP, UNIT or STATIC object. +-- @param Core.Point#COORDINATE Target The target coordinate. Can also be given as a GROUP, UNIT, STATIC or TARGET object. -- @param #number Altitude Engage altitude in feet. Default 2000 ft. -- @return #AUFTRAG self function AUFTRAG:NewSTRIKE(Target, Altitude) @@ -958,14 +1296,16 @@ function AUFTRAG:NewSTRIKE(Target, Altitude) mission.optionROE=ENUMS.ROE.OpenFire mission.optionROT=ENUMS.ROT.PassiveDefense + mission.categories={AUFTRAG.Category.AIRCRAFT} + mission.DCStask=mission:GetDCSMissionTask() return mission end ---- Create a BOMBING mission. Flight will drop bombs a specified coordinate. +--- **[AIR]** Create a BOMBING mission. Flight will drop bombs a specified coordinate. -- @param #AUFTRAG self --- @param Core.Point#COORDINATE Target Target coordinate. Can also be specified as a GROUP, UNIT or STATIC object. +-- @param Core.Point#COORDINATE Target Target coordinate. Can also be specified as a GROUP, UNIT, STATIC or TARGET object. -- @param #number Altitude Engage altitude in feet. Default 25000 ft. -- @return #AUFTRAG self function AUFTRAG:NewBOMBING(Target, Altitude) @@ -989,13 +1329,15 @@ function AUFTRAG:NewBOMBING(Target, Altitude) -- Evaluate result after 5 min. We might need time until the bombs have dropped and targets have been detroyed. mission.dTevaluate=5*60 + mission.categories={AUFTRAG.Category.AIRCRAFT} + -- Get DCS task. mission.DCStask=mission:GetDCSMissionTask() return mission end ---- Create a BOMBRUNWAY mission. +--- **[AIR]** Create a BOMBRUNWAY mission. -- @param #AUFTRAG self -- @param Wrapper.Airbase#AIRBASE Airdrome The airbase to bomb. This must be an airdrome (not a FARP or ship) as these to not have a runway. -- @param #number Altitude Engage altitude in feet. Default 25000 ft. @@ -1005,10 +1347,6 @@ function AUFTRAG:NewBOMBRUNWAY(Airdrome, Altitude) if type(Airdrome)=="string" then Airdrome=AIRBASE:FindByName(Airdrome) end - - if Airdrome:IsInstanceOf("AIRBASE") then - - end local mission=AUFTRAG:New(AUFTRAG.Type.BOMBRUNWAY) @@ -1029,13 +1367,15 @@ function AUFTRAG:NewBOMBRUNWAY(Airdrome, Altitude) -- Evaluate result after 5 min. mission.dTevaluate=5*60 + mission.categories={AUFTRAG.Category.AIRCRAFT} + -- Get DCS task. mission.DCStask=mission:GetDCSMissionTask() return mission end ---- Create a CARPET BOMBING mission. +--- **[AIR]** Create a CARPET BOMBING mission. -- @param #AUFTRAG self -- @param Core.Point#COORDINATE Target Target coordinate. Can also be specified as a GROUP, UNIT or STATIC object. -- @param #number Altitude Engage altitude in feet. Default 25000 ft. @@ -1065,6 +1405,8 @@ function AUFTRAG:NewBOMBCARPET(Target, Altitude, CarpetLength) -- Evaluate result after 5 min. mission.dTevaluate=5*60 + mission.categories={AUFTRAG.Category.AIRCRAFT} + -- Get DCS task. mission.DCStask=mission:GetDCSMissionTask() @@ -1072,10 +1414,10 @@ function AUFTRAG:NewBOMBCARPET(Target, Altitude, CarpetLength) end ---- Create an ESCORT (or FOLLOW) mission. Flight will escort another group and automatically engage certain target types. +--- **[AIR]** Create an ESCORT (or FOLLOW) mission. Flight will escort another group and automatically engage certain target types. -- @param #AUFTRAG self -- @param Wrapper.Group#GROUP EscortGroup The group to escort. --- @param DCS#Vec3 OffsetVector A table with x, y and z components specifying the offset of the flight to the escorted group. Default {x=-100, y=0, z=200} for z=200 meters to the right, same alitude, x=100 meters behind. +-- @param DCS#Vec3 OffsetVector A table with x, y and z components specifying the offset of the flight to the escorted group. Default {x=-100, y=0, z=200} for z=200 meters to the right, same alitude (y=0), x=-100 meters behind. -- @param #number EngageMaxDistance Max engage distance of targets in nautical miles. Default auto (*nil*). -- @param #table TargetTypes Types of targets to engage automatically. Default is {"Air"}, i.e. all enemy airborne units. Use an empty set {} for a simple "FOLLOW" mission. -- @return #AUFTRAG self @@ -1083,7 +1425,13 @@ function AUFTRAG:NewESCORT(EscortGroup, OffsetVector, EngageMaxDistance, TargetT local mission=AUFTRAG:New(AUFTRAG.Type.ESCORT) - mission:_TargetFromObject(EscortGroup) + -- If only a string is passed we set a variable and check later if the group exists. + if type(EscortGroup)=="string" then + mission.escortGroupName=EscortGroup + mission:_TargetFromObject() + else + mission:_TargetFromObject(EscortGroup) + end -- DCS task parameters: mission.escortVec3=OffsetVector or {x=-100, y=0, z=200} @@ -1094,24 +1442,24 @@ function AUFTRAG:NewESCORT(EscortGroup, OffsetVector, EngageMaxDistance, TargetT mission.missionTask=ENUMS.MissionTask.ESCORT mission.missionFraction=0.1 mission.missionAltitude=1000 - mission.optionROE=ENUMS.ROE.OpenFire -- TODO: what's the best ROE here? Make dependent on ESCORT or FOLLOW! + mission.optionROE=ENUMS.ROE.OpenFireWeaponFree -- TODO: what's the best ROE here? Make dependent on ESCORT or FOLLOW! mission.optionROT=ENUMS.ROT.PassiveDefense + mission.categories={AUFTRAG.Category.AIRCRAFT} + mission.DCStask=mission:GetDCSMissionTask() return mission end ---- Create a RESCUE HELO mission. +--- **[AIR ROTARY]** Create a RESCUE HELO mission. -- @param #AUFTRAG self -- @param Wrapper.Unit#UNIT Carrier The carrier unit. -- @return #AUFTRAG self function AUFTRAG:NewRESCUEHELO(Carrier) local mission=AUFTRAG:New(AUFTRAG.Type.RESCUEHELO) - - --mission.carrier=Carrier - + mission:_TargetFromObject(Carrier) -- Mission options: @@ -1120,13 +1468,15 @@ function AUFTRAG:NewRESCUEHELO(Carrier) mission.optionROE=ENUMS.ROE.WeaponHold mission.optionROT=ENUMS.ROT.NoReaction + mission.categories={AUFTRAG.Category.HELICOPTER} + mission.DCStask=mission:GetDCSMissionTask() return mission end ---- Create a TROOP TRANSPORT mission. +--- **[AIR ROTARY, GROUND]** Create a TROOP TRANSPORT mission. -- @param #AUFTRAG self -- @param Core.Set#SET_GROUP TransportGroupSet The set group(s) to be transported. -- @param Core.Point#COORDINATE DropoffCoordinate Coordinate where the helo will land drop off the the troops. @@ -1159,25 +1509,68 @@ function AUFTRAG:NewTROOPTRANSPORT(TransportGroupSet, DropoffCoordinate, PickupC mission.optionROE=ENUMS.ROE.ReturnFire mission.optionROT=ENUMS.ROT.PassiveDefense + mission.categories={AUFTRAG.Category.HELICOPTER, AUFTRAG.Category.GROUND} + mission.DCStask=mission:GetDCSMissionTask() return mission end ---- Create an ARTY mission. +--[[ + +--- **[AIR, GROUND, NAVAL]** Create a OPS TRANSPORT mission. +-- @param #AUFTRAG self +-- @param Core.Set#SET_GROUP CargoGroupSet The set group(s) to be transported. +-- @param Core.Zone#ZONE PickupZone Pick up zone +-- @param Core.Zone#ZONE DeployZone Deploy zone +-- @return #AUFTRAG self +function AUFTRAG:NewOPSTRANSPORT(CargoGroupSet, PickupZone, DeployZone) + + local mission=AUFTRAG:New(AUFTRAG.Type.OPSTRANSPORT) + + mission.transportGroupSet=CargoGroupSet + + mission:_TargetFromObject(mission.transportGroupSet) + + mission.opstransport=OPSTRANSPORT:New(CargoGroupSet, PickupZone, DeployZone) + + function mission.opstransport:OnAfterExecuting(From, Event, To) + mission:Executing() + end + + function mission.opstransport:OnAfterDelivered(From, Event, To) + mission:Done() + end + + -- TODO: what's the best ROE here? + mission.optionROE=ENUMS.ROE.ReturnFire + mission.optionROT=ENUMS.ROT.PassiveDefense + + mission.categories={AUFTRAG.Category.ALL} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +]] + +--- **[GROUND, NAVAL]** Create an ARTY mission. -- @param #AUFTRAG self -- @param Core.Point#COORDINATE Target Center of the firing solution. -- @param #number Nshots Number of shots to be fired. Default 3. -- @param #number Radius Radius of the shells in meters. Default 100 meters. +-- @param #number Altitude Altitude in meters. Can be used to setup a Barrage. Default `#nil`. -- @return #AUFTRAG self -function AUFTRAG:NewARTY(Target, Nshots, Radius) +function AUFTRAG:NewARTY(Target, Nshots, Radius, Altitude) local mission=AUFTRAG:New(AUFTRAG.Type.ARTY) mission:_TargetFromObject(Target) - mission.artyShots=Nshots or 3 + mission.artyShots=Nshots or nil mission.artyRadius=Radius or 100 + mission.artyAltitude=Altitude mission.engageWeaponType=ENUMS.WeaponFlag.Auto @@ -1189,12 +1582,52 @@ function AUFTRAG:NewARTY(Target, Nshots, Radius) -- Evaluate after 8 min. mission.dTevaluate=8*60 + mission.categories={AUFTRAG.Category.GROUND, AUFTRAG.Category.NAVAL} + mission.DCStask=mission:GetDCSMissionTask() return mission end ---- Create a PATROLZONE mission. Group(s) will go to the zone and patrol it randomly. +--- **[GROUND, NAVAL]** Create an BARRAGE mission. Assigned groups will move to a random coordinate within a given zone and start firing into the air. +-- @param #AUFTRAG self +-- @param Core.Zone#ZONE Zone The zone where the unit will go. +-- @param #number Heading Heading in degrees. Default random heading [0, 360). +-- @param #number Angle Shooting angle in degrees. Default random [45, 85]. +-- @param #number Radius Radius of the shells in meters. Default 100 meters. +-- @param #number Altitude Altitude in meters. Default 500 m. +-- @param #number Nshots Number of shots to be fired. Default is until ammo is empty (`#nil`). +-- @return #AUFTRAG self +function AUFTRAG:NewBARRAGE(Zone, Heading, Angle, Radius, Altitude, Nshots) + + local mission=AUFTRAG:New(AUFTRAG.Type.BARRAGE) + + mission:_TargetFromObject(Zone) + + mission.artyShots=Nshots + mission.artyRadius=Radius or 100 + mission.artyAltitude=Altitude + mission.artyHeading=Heading + mission.artyAngle=Angle + + mission.engageWeaponType=ENUMS.WeaponFlag.Auto + + mission.optionROE=ENUMS.ROE.OpenFire -- Ground/naval need open fire! + mission.optionAlarm=0 + + mission.missionFraction=0.0 + + -- Evaluate after instantly. + mission.dTevaluate=10 + + mission.categories={AUFTRAG.Category.GROUND, AUFTRAG.Category.NAVAL} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[AIR, GROUND, NAVAL]** Create a PATROLZONE mission. Group(s) will go to the zone and patrol it randomly. -- @param #AUFTRAG self -- @param Core.Zone#ZONE Zone The patrol zone. -- @param #number Speed Speed in knots. @@ -1204,6 +1637,11 @@ function AUFTRAG:NewPATROLZONE(Zone, Speed, Altitude) local mission=AUFTRAG:New(AUFTRAG.Type.PATROLZONE) + -- Ensure we got a ZONE and not just the zone name. + if type(Zone)=="string" then + Zone=ZONE:New(Zone) + end + mission:_TargetFromObject(Zone) mission.optionROE=ENUMS.ROE.OpenFire @@ -1214,30 +1652,170 @@ function AUFTRAG:NewPATROLZONE(Zone, Speed, Altitude) mission.missionSpeed=Speed and UTILS.KnotsToKmph(Speed) or nil mission.missionAltitude=Altitude and UTILS.FeetToMeters(Altitude) or nil + mission.categories={AUFTRAG.Category.ALL} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[AIR, GROUND, NAVAL]** Create a RECON mission. +-- @param #AUFTRAG self +-- @param Core.Set#SET_ZONE ZoneSet The recon zones. +-- @param #number Speed Speed in knots. +-- @param #number Altitude Altitude in feet. Only for airborne units. Default 2000 feet ASL. +-- @param #boolean Adinfinitum If true, the group will start over again after reaching the final zone. +-- @param #boolean Randomly If true, the group will select a random zone. +-- @return #AUFTRAG self +function AUFTRAG:NewRECON(ZoneSet, Speed, Altitude, Adinfinitum, Randomly) + + local mission=AUFTRAG:New(AUFTRAG.Type.RECON) + + mission:_TargetFromObject(ZoneSet) + + mission.optionROE=ENUMS.ROE.WeaponHold + mission.optionROT=ENUMS.ROT.PassiveDefense + mission.optionAlarm=ENUMS.AlarmState.Auto + + mission.missionFraction=0.5 + mission.missionSpeed=Speed and UTILS.KnotsToKmph(Speed) or nil + mission.missionAltitude=Altitude and UTILS.FeetToMeters(Altitude) or UTILS.FeetToMeters(2000) + + mission.categories={AUFTRAG.Category.ALL} + + mission.DCStask=mission:GetDCSMissionTask() + mission.DCStask.params.adinfitum=Adinfinitum + mission.DCStask.params.randomly=Randomly + + return mission +end + +--- **[GROUND]** Create a AMMO SUPPLY mission. +-- @param #AUFTRAG self +-- @param Core.Zone#ZONE Zone The zone, where supply units go. +-- @return #AUFTRAG self +function AUFTRAG:NewAMMOSUPPLY(Zone) + + local mission=AUFTRAG:New(AUFTRAG.Type.AMMOSUPPLY) + + mission:_TargetFromObject(Zone) + + mission.optionROE=ENUMS.ROE.WeaponHold + mission.optionAlarm=ENUMS.AlarmState.Auto + + mission.missionFraction=1.0 + + mission.missionWaypointRadius=0 + + mission.categories={AUFTRAG.Category.GROUND} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[GROUND]** Create a FUEL SUPPLY mission. +-- @param #AUFTRAG self +-- @param Core.Zone#ZONE Zone The zone, where supply units go. +-- @return #AUFTRAG self +function AUFTRAG:NewFUELSUPPLY(Zone) + + local mission=AUFTRAG:New(AUFTRAG.Type.FUELSUPPLY) + + mission:_TargetFromObject(Zone) + + mission.optionROE=ENUMS.ROE.WeaponHold + mission.optionAlarm=ENUMS.AlarmState.Auto + + mission.missionFraction=1.0 + + mission.categories={AUFTRAG.Category.GROUND} + mission.DCStask=mission:GetDCSMissionTask() return mission end ---- Create a mission to attack a group. Mission type is automatically chosen from the group category. +--- **[AIR]** Create an ALERT 5 mission. Aircraft will be spawned uncontrolled and wait for an assignment. You must specify **one** mission type which is performed. +-- This determines the payload and the DCS mission task which are used when the aircraft is spawned. +-- @param #AUFTRAG self +-- @param #string MissionType Mission type `AUFTRAG.Type.XXX`. Determines payload and mission task (intercept, ground attack, etc.). +-- @return #AUFTRAG self +function AUFTRAG:NewALERT5(MissionType) + + local mission=AUFTRAG:New(AUFTRAG.Type.ALERT5) + + mission.missionTask=self:GetMissionTaskforMissionType(MissionType) + + mission.optionROE=ENUMS.ROE.WeaponHold + mission.optionROT=ENUMS.ROT.NoReaction + + mission.alert5MissionType=MissionType + + mission.missionFraction=1.0 + + mission.categories={AUFTRAG.Category.AIRCRAFT} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[GROUND, NAVAL]** Create an ON GUARD mission. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Coordinate Coordinate, where to stand guard. +-- @return #AUFTRAG self +function AUFTRAG:NewONGUARD(Coordinate) + + local mission=AUFTRAG:New(AUFTRAG.Type.ONGUARD) + + mission:_TargetFromObject(Coordinate) + + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionAlarm=ENUMS.AlarmState.Auto + + mission.missionFraction=1.0 + + mission.categories={AUFTRAG.Category.GROUND, AUFTRAG.Category.NAVAL} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + + +--- Create a mission to attack a TARGET object. -- @param #AUFTRAG self -- @param Ops.Target#TARGET Target The target. +-- @param #string MissionType The mission type. -- @return #AUFTRAG self -function AUFTRAG:NewTargetAir(Target) +function AUFTRAG:NewFromTarget(Target, MissionType) local mission=nil --#AUFTRAG - self.engageTarget=Target - - local target=self.engageTarget:GetObject() - - local mission=self:NewAUTO(target) - - if mission then - mission:SetPriority(10, true) + if MissionType==AUFTRAG.Type.ANTISHIP then + mission=self:NewANTISHIP(Target, Altitude) + elseif MissionType==AUFTRAG.Type.ARTY then + mission=self:NewARTY(Target, Nshots, Radius) + elseif MissionType==AUFTRAG.Type.BAI then + mission=self:NewBAI(Target, Altitude) + elseif MissionType==AUFTRAG.Type.BOMBCARPET then + mission=self:NewBOMBCARPET(Target, Altitude, CarpetLength) + elseif MissionType==AUFTRAG.Type.BOMBING then + mission=self:NewBOMBING(Target, Altitude) + elseif MissionType==AUFTRAG.Type.BOMBRUNWAY then + mission=self:NewBOMBRUNWAY(Target, Altitude) + elseif MissionType==AUFTRAG.Type.INTERCEPT then + mission=self:NewINTERCEPT(Target) + elseif MissionType==AUFTRAG.Type.SEAD then + mission=self:NewSEAD(Target, Altitude) + elseif MissionType==AUFTRAG.Type.STRIKE then + mission=self:NewSTRIKE(Target, Altitude) + else + return nil end - + return mission end @@ -1436,6 +2014,33 @@ function AUFTRAG:SetTime(ClockStart, ClockStop) return self end +--- Set time how low the mission is executed. Once this time limit has passed, the mission is cancelled. +-- @param #AUFTRAG self +-- @param #number Duration Duration in seconds. +-- @return #AUFTRAG self +function AUFTRAG:SetDuration(Duration) + self.durationExe=Duration + return self +end + + +--- Set mission push time. This is the time the mission is executed. If the push time is not passed, the group will wait at the mission execution waypoint. +-- @param #AUFTRAG self +-- @param #string ClockPush Time the mission is executed, e.g. "05:00" for 5 am. Can also be given as a `#number`, where it is interpreted as relative push time in seconds. +-- @return #AUFTRAG self +function AUFTRAG:SetPushTime(ClockPush) + + if ClockPush then + if type(ClockPush)=="string" then + self.Tpush=UTILS.ClockToSeconds(ClockPush) + elseif type(ClockPush)=="number" then + self.Tpush=timer.getAbsTime()+ClockPush + end + end + + return self +end + --- Set mission priority and (optional) urgency. Urgent missions can cancel other running missions. -- @param #AUFTRAG self -- @param #number Prio Priority 1=high, 100=low. Default 50. @@ -1449,7 +2054,7 @@ function AUFTRAG:SetPriority(Prio, Urgent, Importance) return self end ---- Set how many times the mission is repeated. Only valid if the mission is handled by an AIRWING or higher level. +--- Set how many times the mission is repeated. Only valid if the mission is handled by a LEGION (AIRWING, BRIGADE, ...) or higher level. -- @param #AUFTRAG self -- @param #number Nrepeat Number of repeats. Default 0. -- @return #AUFTRAG self @@ -1458,7 +2063,7 @@ function AUFTRAG:SetRepeat(Nrepeat) return self end ---- Set how many times the mission is repeated if it fails. Only valid if the mission is handled by an AIRWING or higher level. +--- Set how many times the mission is repeated if it fails. Only valid if the mission is handled by a LEGION (AIRWING, BRIGADE, ...) or higher level. -- @param #AUFTRAG self -- @param #number Nrepeat Number of repeats. Default 0. -- @return #AUFTRAG self @@ -1467,7 +2072,7 @@ function AUFTRAG:SetRepeatOnFailure(Nrepeat) return self end ---- Set how many times the mission is repeated if it was successful. Only valid if the mission is handled by an AIRWING or higher level. +--- Set how many times the mission is repeated if it was successful. Only valid if the mission is handled by a LEGION (AIRWING, BRIGADE, ...) or higher level. -- @param #AUFTRAG self -- @param #number Nrepeat Number of repeats. Default 0. -- @return #AUFTRAG self @@ -1476,12 +2081,60 @@ function AUFTRAG:SetRepeatOnSuccess(Nrepeat) return self end ---- Define how many assets are required to do the job. Only valid if the mission is handled by an AIRWING or higher level. +--- Define how many assets are required to do the job. Only used if the mission is handled by a **LEGION** (AIRWING, BRIGADE, ...) or higher level. -- @param #AUFTRAG self --- @param #number Nassets Number of asset groups. Default 1. +-- @param #number NassetsMin Minimum number of asset groups. Default 1. +-- @param #number NassetsMax Maximum Number of asset groups. Default is same as `NassetsMin`. -- @return #AUFTRAG self -function AUFTRAG:SetRequiredAssets(Nassets) - self.nassets=Nassets or 1 +function AUFTRAG:SetRequiredAssets(NassetsMin, NassetsMax) + + self.NassetsMin=NassetsMin or 1 + + self.NassetsMax=NassetsMax or self.NassetsMin + + -- Ensure that max is at least equal to min. + if self.NassetsMaxself.Tstop or false then + if self.Tstop and Tnow>self.Tstop then return false end @@ -1992,7 +2979,7 @@ function AUFTRAG:IsReadyToCancel() local Tnow=timer.getAbsTime() -- Stop time already passed. - if self.Tstop and Tnow>self.Tstop then + if self.Tstop and Tnow>=self.Tstop then return true end @@ -2016,6 +3003,26 @@ function AUFTRAG:IsReadyToCancel() return false end +--- Check if mission is ready to be pushed. +-- * Mission push time already passed. +-- * **All** push conditions are true. +-- @param #AUFTRAG self +-- @return #boolean If true, mission groups can push. +function AUFTRAG:IsReadyToPush() + + local Tnow=timer.getAbsTime() + + -- Push time passed? + if self.Tpush and Tnow<=self.Tpush then + return false + end + + -- Evaluate push condition(s) if any. All need to be true. + local push=self:EvalConditionsAll(self.conditionPush) + + return push +end + --- Check if all given condition are true. -- @param #AUFTRAG self -- @param #table Conditions Table of conditions. @@ -2078,6 +3085,26 @@ function AUFTRAG:onafterStatus(From, Event, To) -- Current abs. mission time. local Tnow=timer.getAbsTime() + + -- ESCORT: Check if only the group NAME of an escort had been specified. + if self.escortGroupName then + -- Try to find the group. + local group=GROUP:FindByName(self.escortGroupName) + if group and group:IsAlive() then + + -- Debug info. + self:T(self.lid..string.format("ESCORT group %s is now alive. Updating DCS task and adding group to TARGET", tostring(self.escortGroupName))) + + -- Add TARGET object. + self.engageTarget:AddObject(group) + + -- Update DCS task with the known group ID. + self.DCStask=self:GetDCSMissionTask() + + -- Set value to nil so we do not do this again in the next cycle. + self.escortGroupName=nil + end + end -- Number of alive mission targets. local Ntargets=self:CountMissionTargets() @@ -2094,10 +3121,30 @@ function AUFTRAG:onafterStatus(From, Event, To) -- All groups have reported MISSON DONE. self:Done() - elseif (self.Tstop and Tnow>self.Tstop+10) or (Ntargets0>0 and Ntargets==0) then - + elseif (self.Tstop and Tnow>self.Tstop+10) then + -- Cancel mission if stop time passed. self:Cancel() + + elseif self.durationExe and self.Texecuting and Tnow-self.Texecuting>self.durationExe then + + -- Backup repeat values + local Nrepeat=self.Nrepeat + local NrepeatS=self.NrepeatSuccess + local NrepeatF=self.NrepeatFailure + + -- Cancel mission if stop time passed. + self:Cancel() + + self.Nrepeat=Nrepeat + self.NrepeatSuccess=NrepeatS + self.NrepeatFailure=NrepeatF + + elseif (Ntargets0>0 and Ntargets==0) then + + -- Cancel mission if mission targets are gone (if there were any in the beginning). + -- TODO: I commented this out for some reason but I forgot why... + self:Cancel() end @@ -2120,11 +3167,13 @@ function AUFTRAG:onafterStatus(From, Event, To) local targetname=self:GetTargetName() or "unknown" - local airwing=self.airwing and self.airwing.alias or "N/A" - local commander=self.wingcommander and tostring(self.wingcommander.coalition) or "N/A" + local Nlegions=#self.legions + local commander=self.commander and self.statusCommander or "N/A" + local chief=self.chief and self.statusChief or "N/A" -- Info message. - self:I(self.lid..string.format("Status %s: Target=%s, T=%s-%s, assets=%d, groups=%d, targets=%d, wing=%s, commander=%s", self.status, targetname, Cstart, Cstop, #self.assets, Ngroups, Ntargets, airwing, commander)) + self:I(self.lid..string.format("Status %s: Target=%s, T=%s-%s, assets=%d, groups=%d, targets=%d, legions=%d, commander=%s, chief=%s", + self.status, targetname, Cstart, Cstop, #self.assets, Ngroups, Ntargets, Nlegions, commander, chief)) end -- Group info. @@ -2140,11 +3189,6 @@ function AUFTRAG:onafterStatus(From, Event, To) -- Ready to evaluate mission outcome? local ready2evaluate=self.Tover and Tnow-self.Tover>=self.dTevaluate or false - - --env.info("FF Tover="..tostring(self.Tover)) - --if self.Tover then - -- env.info("FF Tnow-Tover="..tostring(Tnow-self.Tover)) - --end -- Check if mission is OVER (done or cancelled) and enough time passed to evaluate the result. if self:IsOver() and ready2evaluate then @@ -2304,10 +3348,16 @@ end -- @param #AUFTRAG self -- @param Ops.OpsGroup#OPSGROUP opsgroup The flight group. -- @param #string status New status. +-- @return #AUFTRAG self function AUFTRAG:SetGroupStatus(opsgroup, status) - self:T(self.lid..string.format("Setting flight %s to status %s", opsgroup and opsgroup.groupname or "nil", tostring(status))) - if self:GetGroupStatus(opsgroup)==AUFTRAG.GroupStatus.CANCELLED and status==AUFTRAG.GroupStatus.DONE then + -- Current status. + local oldstatus=self:GetGroupStatus(opsgroup) + + -- Debug info. + self:T(self.lid..string.format("Setting OPSGROUP %s to status %s-->%s", opsgroup and opsgroup.groupname or "nil", tostring(oldstatus), tostring(status))) + + if oldstatus==AUFTRAG.GroupStatus.CANCELLED and status==AUFTRAG.GroupStatus.DONE then -- Do not overwrite a CANCELLED status with a DONE status. else local groupdata=self:GetGroupData(opsgroup) @@ -2318,22 +3368,30 @@ function AUFTRAG:SetGroupStatus(opsgroup, status) end end + -- Check if mission is NOT over. + local isNotOver=self:IsNotOver() + + -- Check if all assigned groups are done. + local groupsDone=self:CheckGroupsDone() + -- Debug info. - self:T2(self.lid..string.format("Setting flight %s status to %s. IsNotOver=%s CheckGroupsDone=%s", opsgroup.groupname, self:GetGroupStatus(opsgroup), tostring(self:IsNotOver()), tostring(self:CheckGroupsDone()))) + self:T2(self.lid..string.format("Setting OPSGROUP %s status to %s. IsNotOver=%s CheckGroupsDone=%s", opsgroup.groupname, self:GetGroupStatus(opsgroup), tostring(self:IsNotOver()), tostring(self:CheckGroupsDone()))) -- Check if ALL flights are done with their mission. - if self:IsNotOver() and self:CheckGroupsDone() then - self:T3(self.lid.."All flights done ==> mission DONE!") + if isNotOver and groupsDone then + self:T3(self.lid.."All assigned OPSGROUPs done ==> mission DONE!") self:Done() else self:T3(self.lid.."Mission NOT DONE yet!") end + return self end --- Get ops group mission status. -- @param #AUFTRAG self --- @param Ops.OpsGroup#OPSGROUP opsgroup The flight group. +-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group. +-- @return #string The group status. function AUFTRAG:GetGroupStatus(opsgroup) self:T3(self.lid..string.format("Trying to get Flight status for flight group %s", opsgroup and opsgroup.groupname or "nil")) @@ -2349,20 +3407,90 @@ function AUFTRAG:GetGroupStatus(opsgroup) end end - ---- Set Ops group waypoint coordinate. +--- Add LEGION to mission. -- @param #AUFTRAG self --- @param Ops.OpsGroup#OPSGROUP opsgroup The flight group. +-- @param Ops.Legion#LEGION Legion The legion. +-- @return #AUFTRAG self +function AUFTRAG:AddLegion(Legion) + + -- Debug info. + self:T(self.lid..string.format("Adding legion %s", Legion.alias)) + + -- Add legion to table. + table.insert(self.legions, Legion) + + return self +end + +--- Remove LEGION from mission. +-- @param #AUFTRAG self +-- @param Ops.Legion#LEGION Legion The legion. +-- @return #AUFTRAG self +function AUFTRAG:RemoveLegion(Legion) + + -- Loop over legions + for i=#self.legions,1,-1 do + local legion=self.legions[i] --Ops.Legion#LEGION + if legion.alias==Legion.alias then + -- Debug info. + self:T(self.lid..string.format("Removing legion %s", Legion.alias)) + table.remove(self.legions, i) + return self + end + end + + self:E(self.lid..string.format("ERROR: Legion %s not found and could not be removed!", Legion.alias)) + return self +end + +--- Set LEGION mission status. +-- @param #AUFTRAG self +-- @param Ops.Legion#LEGION Legion The legion. +-- @param #string Status New status. +-- @return #AUFTRAG self +function AUFTRAG:SetLegionStatus(Legion, Status) + + -- Old status + local status=self:GetLegionStatus(Legion) + + -- Debug info. + self:T(self.lid..string.format("Setting LEGION %s to status %s-->%s", Legion.alias, tostring(status), tostring(Status))) + + -- New status. + self.statusLegion[Legion.alias]=Status + + return self +end + +--- Get LEGION mission status. +-- @param #AUFTRAG self +-- @param Ops.Legion#LEGION Legion The legion. +-- @return #string status Current status. +function AUFTRAG:GetLegionStatus(Legion) + + -- New status. + local status=self.statusLegion[Legion.alias] or "unknown" + + return status +end + + +--- Set mission (ingress) waypoint coordinate for OPS group. +-- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group. -- @param Core.Point#COORDINATE coordinate Waypoint Coordinate. +-- @return #AUFTRAG self function AUFTRAG:SetGroupWaypointCoordinate(opsgroup, coordinate) local groupdata=self:GetGroupData(opsgroup) if groupdata then groupdata.waypointcoordinate=coordinate end + return self end ---- Get opsgroup waypoint coordinate. +--- Get mission (ingress) waypoint coordinate of OPS group -- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group. -- @return Core.Point#COORDINATE Waypoint Coordinate. function AUFTRAG:GetGroupWaypointCoordinate(opsgroup) local groupdata=self:GetGroupData(opsgroup) @@ -2372,9 +3500,9 @@ function AUFTRAG:GetGroupWaypointCoordinate(opsgroup) end ---- Set Ops group waypoint task. +--- Set mission waypoint task for OPS group. -- @param #AUFTRAG self --- @param Ops.OpsGroup#OPSGROUP opsgroup The flight group. +-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group. -- @param Ops.OpsGroup#OPSGROUP.Task task Waypoint task. function AUFTRAG:SetGroupWaypointTask(opsgroup, task) self:T2(self.lid..string.format("Setting waypoint task %s", task and task.description or "WTF")) @@ -2384,9 +3512,9 @@ function AUFTRAG:SetGroupWaypointTask(opsgroup, task) end end ---- Get opsgroup waypoint task. +--- Get mission waypoint task of OPS group. -- @param #AUFTRAG self --- @param Ops.OpsGroup#OPSGROUP opsgroup The flight group. +-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group. -- @return Ops.OpsGroup#OPSGROUP.Task task Waypoint task. Waypoint task. function AUFTRAG:GetGroupWaypointTask(opsgroup) local groupdata=self:GetGroupData(opsgroup) @@ -2395,22 +3523,24 @@ function AUFTRAG:GetGroupWaypointTask(opsgroup) end end ---- Set opsgroup waypoint index. +--- Set mission (ingress) waypoint UID for OPS group. -- @param #AUFTRAG self --- @param Ops.OpsGroup#OPSGROUP opsgroup The flight group. --- @param #number waypointindex Waypoint index. +-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group. +-- @param #number waypointindex Waypoint UID. +-- @return #AUFTRAG self function AUFTRAG:SetGroupWaypointIndex(opsgroup, waypointindex) - self:T2(self.lid..string.format("Setting waypoint index %d", waypointindex)) + self:T2(self.lid..string.format("Setting Mission waypoint UID=%d", waypointindex)) local groupdata=self:GetGroupData(opsgroup) if groupdata then groupdata.waypointindex=waypointindex end + return self end ---- Get opsgroup waypoint index. +--- Get mission (ingress) waypoint UID of OPS group. -- @param #AUFTRAG self --- @param Ops.OpsGroup#OPSGROUP opsgroup The flight group. --- @return #number Waypoint index +-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group. +-- @return #number Waypoint UID. function AUFTRAG:GetGroupWaypointIndex(opsgroup) local groupdata=self:GetGroupData(opsgroup) if groupdata then @@ -2418,36 +3548,90 @@ function AUFTRAG:GetGroupWaypointIndex(opsgroup) end end +--- Set Egress waypoint UID for OPS group. +-- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group. +-- @param #number waypointindex Waypoint UID. +-- @return #AUFTRAG self +function AUFTRAG:SetGroupEgressWaypointUID(opsgroup, waypointindex) + self:T2(self.lid..string.format("Setting Egress waypoint UID=%d", waypointindex)) + local groupdata=self:GetGroupData(opsgroup) + if groupdata then + groupdata.waypointEgressUID=waypointindex + end + return self +end + +--- Get Egress waypoint UID of OPS group. +-- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group. +-- @return #number Waypoint UID. +function AUFTRAG:GetGroupEgressWaypointUID(opsgroup) + local groupdata=self:GetGroupData(opsgroup) + if groupdata then + return groupdata.waypointEgressUID + end +end + + --- Check if all flights are done with their mission (or dead). -- @param #AUFTRAG self -- @return #boolean If true, all flights are done with the mission. function AUFTRAG:CheckGroupsDone() - -- These are early stages, where we might not even have a opsgroup defined to be checked. - if self:IsPlanned() or self:IsQueued() or self:IsRequested() then + -- Check status of all OPS groups. + for groupname,data in pairs(self.groupdata) do + local groupdata=data --#AUFTRAG.GroupData + if groupdata then + if not (groupdata.status==AUFTRAG.GroupStatus.DONE or groupdata.status==AUFTRAG.GroupStatus.CANCELLED) then + -- At least this flight is not DONE or CANCELLED. + self:T(self.lid..string.format("CheckGroupsDone: OPSGROUP %s is not DONE or CANCELLED but in state %s. Mission NOT DONE!", groupdata.opsgroup.groupname, groupdata.status)) + return false + end + end + end + + -- Check status of all LEGIONs. + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + local status=self:GetLegionStatus(legion) + if not status==AUFTRAG.Status.CANCELLED then + -- At least one LEGION has not CANCELLED. + self:T(self.lid..string.format("CheckGroupsDone: LEGION %s is not CANCELLED but in state %s. Mission NOT DONE!", legion.alias, status)) + return false + end + end + + -- Check commander status. + if self.commander then + if not self.statusCommander==AUFTRAG.Status.CANCELLED then + self:T(self.lid..string.format("CheckGroupsDone: COMMANDER is not CANCELLED but in state %s. Mission NOT DONE!", self.statusCommander)) + return false + end + end + + -- Check chief status. + if self.chief then + if not self.statusChief==AUFTRAG.Status.CANCELLED then + self:T(self.lid..string.format("CheckGroupsDone: CHIEF is not CANCELLED but in state %s. Mission NOT DONE!", self.statusChief)) + return false + end + end + + -- These are early stages, where we might not even have a opsgroup defined to be checked. If there were any groups, we checked above. + if self:IsPlanned() or self:IsQueued() or self:IsRequested() then + self:T(self.lid..string.format("CheckGroupsDone: Mission is still in state %s [FSM=%s] (PLANNED or QUEUED or REQUESTED). Mission NOT DONE!", self.status, self:GetState())) return false end -- It could be that all flights were destroyed on the way to the mission execution waypoint. -- TODO: would be better to check if everybody is dead by now. if self:IsStarted() and self:CountOpsGroups()==0 then + self:T(self.lid..string.format("CheckGroupsDone: Mission is STARTED state %s [FSM=%s] but count of alive OPSGROUP is zero. Mission DONE!", self.status, self:GetState())) return true end - -- Check status of all flight groups. - for groupname,data in pairs(self.groupdata) do - local groupdata=data --#AUFTRAG.GroupData - if groupdata then - if groupdata.status==AUFTRAG.GroupStatus.DONE or groupdata.status==AUFTRAG.GroupStatus.CANCELLED then - -- This one is done or cancelled. - else - -- At least this flight is not DONE or CANCELLED. - return false - end - end - end - return true end @@ -2492,16 +3676,14 @@ function AUFTRAG:onafterPlanned(From, Event, To) self:T(self.lid..string.format("New mission status=%s", self.status)) end ---- On after "Queue" event. Mission is added to the mission queue of an AIRWING. +--- On after "Queue" event. Mission is added to the mission queue of a LEGION. -- @param #AUFTRAG self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param Ops.AirWing#AIRWING Airwing The airwing. function AUFTRAG:onafterQueued(From, Event, To, Airwing) self.status=AUFTRAG.Status.QUEUED - self.airwing=Airwing - self:T(self.lid..string.format("New mission status=%s at airwing %s", self.status, tostring(Airwing.alias))) + self:T(self.lid..string.format("New mission status=%s", self.status)) end @@ -2525,7 +3707,7 @@ function AUFTRAG:onafterAssign(From, Event, To) self:T(self.lid..string.format("New mission status=%s", self.status)) end ---- On after "Schedule" event. Mission is added to the mission queue of a FLIGHTGROUP. +--- On after "Schedule" event. Mission is added to the mission queue of an OPSGROUP. -- @param #AUFTRAG self -- @param #string From From state. -- @param #string Event Event. @@ -2542,6 +3724,7 @@ end -- @param #string To To state. function AUFTRAG:onafterStarted(From, Event, To) self.status=AUFTRAG.Status.STARTED + self.Tstarted=timer.getAbsTime() self:T(self.lid..string.format("New mission status=%s", self.status)) end @@ -2552,23 +3735,10 @@ end -- @param #string To To state. function AUFTRAG:onafterExecuting(From, Event, To) self.status=AUFTRAG.Status.EXECUTING + self.Texecuting=timer.getAbsTime() self:T(self.lid..string.format("New mission status=%s", self.status)) end ---- On after "Done" event. --- @param #AUFTRAG self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. -function AUFTRAG:onafterDone(From, Event, To) - self.status=AUFTRAG.Status.DONE - self:T(self.lid..string.format("New mission status=%s", self.status)) - - -- Set time stamp. - self.Tover=timer.getAbsTime() - -end - --- On after "ElementDestroyed" event. -- @param #AUFTRAG self -- @param #string From From state. @@ -2600,7 +3770,7 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param Ops.AirWing#AIRWING.SquadronAsset Asset The asset. +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset. function AUFTRAG:onafterAssetDead(From, Event, To, Asset) -- Number of groups alive. @@ -2636,8 +3806,11 @@ end -- @param #string To To state. function AUFTRAG:onafterCancel(From, Event, To) + -- Number of OPSGROUPS assigned and alive. + local Ngroups = self:CountOpsGroups() + -- Debug info. - self:I(self.lid..string.format("CANCELLING mission in status %s. Will wait for groups to report mission DONE before evaluation", self.status)) + self:I(self.lid..string.format("CANCELLING mission in status %s. Will wait for %d groups to report mission DONE before evaluation", self.status, Ngroups)) -- Time stamp. self.Tover=timer.getAbsTime() @@ -2650,38 +3823,82 @@ function AUFTRAG:onafterCancel(From, Event, To) -- Not necessary to delay the evaluaton?! self.dTevaluate=0 - if self.wingcommander then - - self:T(self.lid..string.format("Wingcommander will cancel the mission. Will wait for mission DONE before evaluation!")) - - self.wingcommander:CancelMission(self) + if self.chief then - elseif self.airwing then + -- Debug info. + self:T(self.lid..string.format("CHIEF will cancel the mission. Will wait for mission DONE before evaluation!")) - self:T(self.lid..string.format("Airwing %s will cancel the mission. Will wait for mission DONE before evaluation!", self.airwing.alias)) + -- CHIEF will cancel the mission. + self.chief:MissionCancel(self) + + elseif self.commander then + + -- Debug info. + self:T(self.lid..string.format("COMMANDER will cancel the mission. Will wait for mission DONE before evaluation!")) - -- Airwing will cancel all flight missions and remove queued request from warehouse queue. - self.airwing:MissionCancel(self) + -- COMMANDER will cancel the mission. + self.commander:MissionCancel(self) + + elseif self.legions and #self.legions>0 then - else + -- Loop over all LEGIONs. + for _,_legion in pairs(self.legions or {}) do + local legion=_legion --Ops.Legion#LEGION + + -- Debug info. + self:T(self.lid..string.format("LEGION %s will cancel the mission. Will wait for mission DONE before evaluation!", legion.alias)) + + -- Legion will cancel all flight missions and remove queued request from warehouse queue. + legion:MissionCancel(self) + + end + + else - self:T(self.lid..string.format("No airwing or wingcommander. Attached flights will cancel the mission on their own. Will wait for mission DONE before evaluation!")) + -- Debug info. + self:T(self.lid..string.format("No legion, commander or chief. Attached flights will cancel the mission on their own. Will wait for mission DONE before evaluation!")) - for _,_groupdata in pairs(self.groupdata) do + -- Loop over all groups. + for _,_groupdata in pairs(self.groupdata or {}) do local groupdata=_groupdata --#AUFTRAG.GroupData groupdata.opsgroup:MissionCancel(self) end end - + -- Special mission states. - if self.status==AUFTRAG.Status.PLANNED then - self:T(self.lid..string.format("Cancelled mission was in planned stage. Call it done!")) + if self:IsPlanned() or self:IsQueued() or self:IsRequested() or Ngroups==0 then + self:T(self.lid..string.format("Cancelled mission was in %s stage with %d groups assigned and alive. Call it done!", self.status, Ngroups)) self:Done() end end +--- On after "Done" event. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AUFTRAG:onafterDone(From, Event, To) + self.status=AUFTRAG.Status.DONE + self:T(self.lid..string.format("New mission status=%s", self.status)) + + -- Set time stamp. + self.Tover=timer.getAbsTime() + + -- Not executing any more. + self.Texecuting=nil + + -- Set status for CHIEF, COMMANDER and LEGIONs + self.statusChief=AUFTRAG.Status.DONE + self.statusCommander=AUFTRAG.Status.DONE + for _,_legion in pairs(self.legions) do + local Legion=_legion --Ops.Legion#LEGION + self:SetLegionStatus(Legion, AUFTRAG.Status.DONE) + end + +end + --- On after "Success" event. -- @param #AUFTRAG self -- @param #string From From state. @@ -2692,6 +3909,14 @@ function AUFTRAG:onafterSuccess(From, Event, To) self.status=AUFTRAG.Status.SUCCESS self:T(self.lid..string.format("New mission status=%s", self.status)) + -- Set status for CHIEF, COMMANDER and LEGIONs + self.statusChief=self.status + self.statusCommander=self.status + for _,_legion in pairs(self.legions) do + local Legion=_legion --Ops.Legion#LEGION + self:SetLegionStatus(Legion, self.status) + end + local repeatme=self.repeatedSuccess0) then + self:E(self.lid.."ERROR: Mission can only be repeated by a CHIEF, COMMANDER or LEGION! Stopping AUFTRAG") + self:Stop() + return false + end + + return true +end --- On after "Repeat" event. -- @param #AUFTRAG self @@ -2760,38 +4008,71 @@ function AUFTRAG:onafterRepeat(From, Event, To) -- Set mission status to PLANNED. self.status=AUFTRAG.Status.PLANNED - + + -- Debug info. self:T(self.lid..string.format("New mission status=%s (on Repeat)", self.status)) + + -- Set status for CHIEF, COMMANDER and LEGIONs + self.statusChief=self.status + self.statusCommander=self.status + for _,_legion in pairs(self.legions) do + local Legion=_legion --Ops.Legion#LEGION + self:SetLegionStatus(Legion, self.status) + end -- Increase repeat counter. self.repeated=self.repeated+1 if self.chief then - --TODO + self.statusChief=AUFTRAG.Status.PLANNED + + -- Remove mission from wingcommander because Chief will assign it again. + if self.commander then + self.commander:RemoveMission(self) + self.statusCommander=AUFTRAG.Status.PLANNED + end + + -- Remove mission from airwing because WC will assign it again but maybe to a different wing. + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + legion:RemoveMission(self) + end - elseif self.wingcommander then + elseif self.commander then + + self.statusCommander=AUFTRAG.Status.PLANNED -- Remove mission from airwing because WC will assign it again but maybe to a different wing. - if self.airwing then - self.airwing:RemoveMission(self) - end + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + legion:RemoveMission(self) + self:SetLegionStatus(legion, AUFTRAG.Status.PLANNED) + end - elseif self.airwing then + elseif #self.legions>0 then - -- Already at the airwing ==> Queued() - self:Queued(self.airwing) + -- Remove mission from airwing because WC will assign it again but maybe to a different wing. + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + legion:RemoveMission(self) + self:SetLegionStatus(legion, AUFTRAG.Status.PLANNED) + legion:AddMission(self) + end else - self:E(self.lid.."ERROR: Mission can only be repeated by a CHIEF, WINGCOMMANDER or AIRWING! Stopping AUFTRAG") + self:E(self.lid.."ERROR: Mission can only be repeated by a CHIEF, COMMANDER or LEGION! Stopping AUFTRAG") self:Stop() + return end -- No mission assets. self.assets={} - for _,_groupdata in pairs(self.groupdata) do + + -- Remove OPS groups. This also removes the mission from the OPSGROUP mission queue. + for groupname,_groupdata in pairs(self.groupdata) do local groupdata=_groupdata --#AUFTRAG.GroupData local opsgroup=groupdata.opsgroup if opsgroup then @@ -2806,6 +4087,9 @@ function AUFTRAG:onafterRepeat(From, Event, To) self.Ncasualties=0 self.Nelements=0 + -- Update DCS mission task. Could be that the initial task (e.g. for bombing) was destroyed. Then we need to update the coordinate. + self.DCStask=self:GetDCSMissionTask() + -- Call status again. self:__Status(-30) @@ -2818,19 +4102,30 @@ end -- @param #string To To state. function AUFTRAG:onafterStop(From, Event, To) + -- Debug info. self:I(self.lid..string.format("STOPPED mission in status=%s. Removing missions from queues. Stopping CallScheduler!", self.status)) - - -- TODO: remove missions from queues in WINGCOMMANDER, AIRWING and FLIGHGROUPS! + -- TODO: Mission should be OVER! we dont want to remove running missions from any queues. - if self.wingcommander then - self.wingcommander:RemoveMission(self) + -- Remove mission from CHIEF queue. + if self.chief then + self.chief:RemoveMission(self) end - if self.airwing then - self.airwing:RemoveMission(self) + -- Remove mission from WINGCOMMANDER queue. + if self.commander then + self.commander:RemoveMission(self) + end + + -- Remove mission from LEGION queues. + if #self.legions>0 then + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + legion:RemoveMission(self) + end end + -- Remove mission from OPSGROUP queue for _,_groupdata in pairs(self.groupdata) do local groupdata=_groupdata --#AUFTRAG.GroupData groupdata.opsgroup:RemoveMission(self) @@ -2858,7 +4153,7 @@ function AUFTRAG:_TargetFromObject(Object) if not self.engageTarget then - if Object:IsInstanceOf("TARGET") then + if Object and Object:IsInstanceOf("TARGET") then self.engageTarget=Object @@ -2873,7 +4168,7 @@ function AUFTRAG:_TargetFromObject(Object) -- Target was already specified elsewhere. end - + -- Debug info. --self:T2(self.lid..string.format("Mission Target %s Type=%s, Ntargets=%d, Lifepoints=%d", self.engageTarget.lid, self.engageTarget.lid, self.engageTarget.N0, self.engageTarget:GetLife())) @@ -2886,12 +4181,13 @@ end -- @return #number Number of alive target units. function AUFTRAG:CountMissionTargets() + local N=0 + if self.engageTarget then - return self.engageTarget:CountTargets() - else - return 0 + N=self.engageTarget:CountTargets() end + return N end --- Get initial number of targets. @@ -2955,14 +4251,16 @@ end -- @param #AUFTRAG self -- @return Wrapper.Positionable#POSITIONABLE The target object. Could be many things. function AUFTRAG:GetObjective() - return self:GetTargetData():GetObject() + local objective=self:GetTargetData():GetObject() + return objective end --- Get type of target. -- @param #AUFTRAG self -- @return #string The target type. function AUFTRAG:GetTargetType() - return self:GetTargetData().Type + local ttype=self:GetTargetData().Type + return ttype end --- Get 2D vector of target. @@ -2971,7 +4269,8 @@ end function AUFTRAG:GetTargetVec2() local coord=self:GetTargetCoordinate() if coord then - return coord:GetVec2() + local vec2=coord:GetVec2() + return vec2 end return nil end @@ -2988,7 +4287,8 @@ function AUFTRAG:GetTargetCoordinate() elseif self.engageTarget then - return self.engageTarget:GetCoordinate() + local coord=self.engageTarget:GetCoordinate() + return coord else self:E(self.lid.."ERROR: Cannot get target coordinate!") @@ -3003,7 +4303,8 @@ end function AUFTRAG:GetTargetName() if self.engageTarget then - return self.engageTarget:GetName() + local name=self.engageTarget:GetName() + return name end return "N/A" @@ -3034,12 +4335,15 @@ end --- Add asset to mission. -- @param #AUFTRAG self --- @param Ops.AirWing#AIRWING.SquadronAsset Asset The asset to be added to the mission. +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset to be added to the mission. -- @return #AUFTRAG self function AUFTRAG:AddAsset(Asset) + -- Debug info + self:T(self.lid..string.format("Adding asset \"%s\" to mission", tostring(Asset.spawngroupname))) + + -- Add to table. self.assets=self.assets or {} - table.insert(self.assets, Asset) return self @@ -3047,15 +4351,15 @@ end --- Delete asset from mission. -- @param #AUFTRAG self --- @param Ops.AirWing#AIRWING.SquadronAsset Asset The asset to be removed. +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset to be removed. -- @return #AUFTRAG self function AUFTRAG:DelAsset(Asset) for i,_asset in pairs(self.assets or {}) do - local asset=_asset --Ops.AirWing#AIRWING.SquadronAsset + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem if asset.uid==Asset.uid then - self:T(self.lid..string.format("Removing asset \"%s\" from mission", tostring(asset.spawngroupname))) + self:T(self.lid..string.format("Removing asset \"%s\" from mission", tostring(Asset.spawngroupname))) table.remove(self.assets, i) return self end @@ -3068,11 +4372,11 @@ end --- Get asset by its spawn group name. -- @param #AUFTRAG self -- @param #string Name Asset spawn group name. --- @return Ops.AirWing#AIRWING.SquadronAsset +-- @return Functional.Warehouse#WAREHOUSE.Assetitem Asset. function AUFTRAG:GetAssetByName(Name) for i,_asset in pairs(self.assets or {}) do - local asset=_asset --Ops.AirWing#AIRWING.SquadronAsset + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem if asset.spawngroupname==Name then return asset @@ -3083,9 +4387,9 @@ function AUFTRAG:GetAssetByName(Name) return nil end ---- Count alive ops groups assigned for this mission. +--- Count alive OPS groups assigned for this mission. -- @param #AUFTRAG self --- @return #number Number of alive flight groups. +-- @return #number Number of alive OPS groups. function AUFTRAG:CountOpsGroups() local N=0 for _,_groupdata in pairs(self.groupdata) do @@ -3112,19 +4416,63 @@ function AUFTRAG:GetMissionTypesText(MissionTypes) return text end ---- Set the mission waypoint coordinate where the mission is executed. +--- Set the mission waypoint coordinate where the mission is executed. Note that altitude is set via `:SetMissionAltitude`. -- @param #AUFTRAG self --- @return Core.Point#COORDINATE Coordinate where the mission is executed. +-- @param Core.Point#COORDINATE Coordinate Coordinate where the mission is executed. -- @return #AUFTRAG self function AUFTRAG:SetMissionWaypointCoord(Coordinate) + + -- Obviously a zone was passed. We get the coordinate. + if Coordinate:IsInstanceOf("ZONE_BASE") then + Coordinate=Coordinate:GetCoordinate() + end + self.missionWaypointCoord=Coordinate + return self +end + +--- Set randomization of the mission waypoint coordinate. Each assigned group will get a random ingress coordinate, where the mission is executed. +-- @param #AUFTRAG self +-- @param #number Radius Distance in meters. Default `#nil`. +-- @return #AUFTRAG self +function AUFTRAG:SetMissionWaypointRandomization(Radius) + self.missionWaypointRadius=Radius + return self +end + +--- Set the mission egress coordinate. This is the coordinate where the assigned group will go once the mission is finished. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Coordinate Egrees coordinate. +-- @param #number Altitude (Optional) Altitude in feet. Default is y component of coordinate. +-- @return #AUFTRAG self +function AUFTRAG:SetMissionEgressCoord(Coordinate, Altitude) + + -- Obviously a zone was passed. We get the coordinate. + if Coordinate:IsInstanceOf("ZONE_BASE") then + Coordinate=Coordinate:GetCoordinate() + end + + self.missionEgressCoord=Coordinate + + if Altitude then + self.missionEgressCoord.y=UTILS.FeetToMeters(Altitude) + end +end + +--- Get the mission egress coordinate if this was defined. +-- @param #AUFTRAG self +-- @return Core.Point#COORDINATE Coordinate Coordinate or nil. +function AUFTRAG:GetMissionEgressCoord() + return self.missionEgressCoord end --- Get coordinate of target. First unit/group of the set is used. -- @param #AUFTRAG self -- @param Wrapper.Group#GROUP group Group. +-- @param #number randomradius Random radius in meters. +-- @param #table surfacetypes Surface types of random zone. -- @return Core.Point#COORDINATE Coordinate where the mission is executed. -function AUFTRAG:GetMissionWaypointCoord(group) +function AUFTRAG:GetMissionWaypointCoord(group, randomradius, surfacetypes) -- Check if a coord has been explicitly set. if self.missionWaypointCoord then @@ -3136,11 +4484,13 @@ function AUFTRAG:GetMissionWaypointCoord(group) end -- Create waypoint coordinate half way between us and the target. - local waypointcoord=group:GetCoordinate():GetIntermediateCoordinate(self:GetTargetCoordinate(), self.missionFraction) + local waypointcoord=group:GetCoordinate():GetIntermediateCoordinate(self:GetTargetCoordinate(), self.missionFraction) local alt=waypointcoord.y -- Add some randomization. - waypointcoord=ZONE_RADIUS:New("Temp", waypointcoord:GetVec2(), 1000):GetRandomCoordinate():SetAltitude(alt, false) + if randomradius then + waypointcoord=ZONE_RADIUS:New("Temp", waypointcoord:GetVec2(), randomradius):GetRandomCoordinate(nil, nil, surfacetypes):SetAltitude(alt, false) + end -- Set altitude of mission waypoint. if self.missionAltitude then @@ -3168,7 +4518,7 @@ function AUFTRAG:UpdateMarker() local text=string.format("%s %s: %s", self.name, self.type:upper(), self.status:upper()) text=text..string.format("\n%s", self:GetTargetName()) text=text..string.format("\nTargets %d/%d, Life Points=%d/%d", self:CountMissionTargets(), self:GetTargetInitialNumber(), self:GetTargetLife(), self:GetTargetInitialLife()) - text=text..string.format("\nFlights %d/%d", self:CountOpsGroups(), self.nassets) + text=text..string.format("\nOpsGroups %d/%d", self:CountOpsGroups(), self:GetNumberOfRequiredAssets()) if not self.marker then @@ -3334,8 +4684,21 @@ function AUFTRAG:GetDCSMissionTask(TaskControllable) ------------------- -- RECON Mission -- ------------------- - - -- TODO: What? Table of coordinates? + + local DCStask={} + + DCStask.id="ReconMission" + + -- We create a "fake" DCS task and pass the parameters to the OPSGROUP. + local param={} + param.target=self.engageTarget + param.altitude=self.missionAltitude + param.speed=self.missionSpeed + param.lastindex=nil + + DCStask.params=param + + table.insert(DCStasks, DCStask) elseif self.type==AUFTRAG.Type.SEAD then @@ -3386,6 +4749,23 @@ function AUFTRAG:GetDCSMissionTask(TaskControllable) table.insert(DCStasks, TaskEmbark) table.insert(DCStasks, TaskDisEmbark) + elseif self.type==AUFTRAG.Type.OPSTRANSPORT then + + -------------------------- + -- OPSTRANSPORT Mission -- + -------------------------- + + local DCStask={} + + DCStask.id="OpsTransport" + + -- We create a "fake" DCS task and pass the parameters to the FLIGHTGROUP. + local param={} + DCStask.params=param + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.RESCUEHELO then ------------------------- @@ -3398,7 +4778,7 @@ function AUFTRAG:GetDCSMissionTask(TaskControllable) -- We create a "fake" DCS task and pass the parameters to the FLIGHTGROUP. local param={} - param.unitname=self:GetTargetName() --self.carrier:GetName() + param.unitname=self:GetTargetName() param.offsetX=200 param.offsetZ=240 param.altitude=70 @@ -3414,9 +4794,33 @@ function AUFTRAG:GetDCSMissionTask(TaskControllable) -- ARTY Mission -- ------------------ - local DCStask=CONTROLLABLE.TaskFireAtPoint(nil, self:GetTargetVec2(), self.artyRadius, self.artyShots, self.engageWeaponType) + local DCStask=CONTROLLABLE.TaskFireAtPoint(nil, self:GetTargetVec2(), self.artyRadius, self.artyShots, self.engageWeaponType, self.artyAltitude) - table.insert(DCStasks, DCStask) + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.BARRAGE then + + --------------------- + -- BARRAGE Mission -- + --------------------- + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.BARRAGE + + -- We create a "fake" DCS task and pass the parameters to the FLIGHTGROUP. + local param={} + param.zone=self:GetObjective() + param.altitude=self.artyAltitude + param.radius=self.artyRadius + param.heading=self.artyHeading + param.angle=self.artyAngle + param.shots=self.artyShots + param.weaponTypoe=self.engageWeaponType + + DCStask.params=param + + table.insert(DCStasks, DCStask) elseif self.type==AUFTRAG.Type.PATROLZONE then @@ -3437,6 +4841,77 @@ function AUFTRAG:GetDCSMissionTask(TaskControllable) DCStask.params=param table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.AMMOSUPPLY then + + ------------------------- + -- AMMO SUPPLY Mission -- + ------------------------- + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.AMMOSUPPLY + + -- We create a "fake" DCS task and pass the parameters to the OPSGROUP. + local param={} + param.zone=self:GetObjective() + + DCStask.params=param + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.FUELSUPPLY then + + ------------------------- + -- FUEL SUPPLY Mission -- + ------------------------- + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.FUELSUPPLY + + -- We create a "fake" DCS task and pass the parameters to the OPSGROUP. + local param={} + param.zone=self:GetObjective() + + DCStask.params=param + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.ALERT5 then + + --------------------- + -- ALERT 5 Mission -- + --------------------- + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.ALERT5 + + -- We create a "fake" DCS task and pass the parameters to the OPSGROUP. + local param={} + + DCStask.params=param + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.ONGUARD then + + ---------------------- + -- ON GUARD Mission -- + ---------------------- + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.ONGUARD + + -- We create a "fake" DCS task and pass the parameters to the OPSGROUP. + local param={} + param.coordinate=self:GetObjective() + + DCStask.params=param + + table.insert(DCStasks, DCStask) else self:E(self.lid..string.format("ERROR: Unknown mission task!")) @@ -3507,6 +4982,133 @@ function AUFTRAG:_GetDCSAttackTask(Target, DCStasks) return DCStasks end +--- Get DCS task table for an attack group or unit task. +-- @param #AUFTRAG self +-- @param #string MissionType Mission (AUFTAG) type. +-- @return #string DCS mission task for the auftrag type. +function AUFTRAG:GetMissionTaskforMissionType(MissionType) + + local mtask=ENUMS.MissionTask.NOTHING + + if MissionType==AUFTRAG.Type.ANTISHIP then + mtask=ENUMS.MissionTask.ANTISHIPSTRIKE + elseif MissionType==AUFTRAG.Type.AWACS then + mtask=ENUMS.MissionTask.AWACS + elseif MissionType==AUFTRAG.Type.BAI then + mtask=ENUMS.MissionTask.GROUNDATTACK + elseif MissionType==AUFTRAG.Type.BOMBCARPET then + mtask=ENUMS.MissionTask.GROUNDATTACK + elseif MissionType==AUFTRAG.Type.BOMBING then + mtask=ENUMS.MissionTask.GROUNDATTACK + elseif MissionType==AUFTRAG.Type.BOMBRUNWAY then + mtask=ENUMS.MissionTask.RUNWAYATTACK + elseif MissionType==AUFTRAG.Type.CAP then + mtask=ENUMS.MissionTask.CAP + elseif MissionType==AUFTRAG.Type.CAS then + mtask=ENUMS.MissionTask.CAS + elseif MissionType==AUFTRAG.Type.ESCORT then + mtask=ENUMS.MissionTask.ESCORT + elseif MissionType==AUFTRAG.Type.FACA then + mtask=ENUMS.MissionTask.AFAC + elseif MissionType==AUFTRAG.Type.FERRY then + mtask=ENUMS.MissionTask.NOTHING + elseif MissionType==AUFTRAG.Type.INTERCEPT then + mtask=ENUMS.MissionTask.INTERCEPT + elseif MissionType==AUFTRAG.Type.RECON then + mtask=ENUMS.MissionTask.RECONNAISSANCE + elseif MissionType==AUFTRAG.Type.SEAD then + mtask=ENUMS.MissionTask.SEAD + elseif MissionType==AUFTRAG.Type.STRIKE then + mtask=ENUMS.MissionTask.GROUNDATTACK + elseif MissionType==AUFTRAG.Type.TANKER then + mtask=ENUMS.MissionTask.REFUELING + elseif MissionType==AUFTRAG.Type.TROOPTRANSPORT then + mtask=ENUMS.MissionTask.TRANSPORT + end + + return mtask +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Global Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Checks if a mission type is contained in a table of possible types. +-- @param #string MissionType The requested mission type. +-- @param #table PossibleTypes A table with possible mission types. +-- @return #boolean If true, the requested mission type is part of the possible mission types. +function AUFTRAG.CheckMissionType(MissionType, PossibleTypes) + + if type(PossibleTypes)=="string" then + PossibleTypes={PossibleTypes} + end + + for _,canmission in pairs(PossibleTypes) do + if canmission==MissionType then + return true + end + end + + return false +end + +--- Check if a mission type is contained in a list of possible capabilities. +-- @param #table MissionTypes The requested mission type. Can also be passed as a single mission type `#string`. +-- @param #table Capabilities A table with possible capabilities `Ops.Auftrag#AUFTRAG.Capability`. +-- @param #boolean All If `true`, given mission type must be includedin ALL capabilities. If `false` or `nil`, it must only match one. +-- @return #boolean If true, the requested mission type is part of the possible mission types. +function AUFTRAG.CheckMissionCapability(MissionTypes, Capabilities, All) + + -- Ensure table. + if type(MissionTypes)~="table" then + MissionTypes={MissionTypes} + end + + for _,cap in pairs(Capabilities) do + local capability=cap --Ops.Auftrag#AUFTRAG.Capability + for _,MissionType in pairs(MissionTypes) do + if All==true then + if capability.MissionType~=MissionType then + return false + end + else + if capability.MissionType==MissionType then + return true + end + end + end + end + + if All==true then + return true + else + return false + end +end + + +--- Check if a mission type is contained in a list of possible capabilities. +-- @param #table MissionTypes The requested mission type. Can also be passed as a single mission type `#string`. +-- @param #table Capabilities A table with possible capabilities `Ops.Auftrag#AUFTRAG.Capability`. +-- @return #boolean If true, the requested mission type is part of the possible mission types. +function AUFTRAG.CheckMissionCapabilityAny(MissionTypes, Capabilities) + + local res=AUFTRAG.CheckMissionCapability(MissionTypes, Capabilities, false) + + return res +end + + +--- Check if a mission type is contained in a list of possible capabilities. +-- @param #table MissionTypes The requested mission type. Can also be passed as a single mission type `#string`. +-- @param #table Capabilities A table with possible capabilities `Ops.Auftrag#AUFTRAG.Capability`. +-- @return #boolean If true, the requested mission type is part of the possible mission types. +function AUFTRAG.CheckMissionCapabilityAll(MissionTypes, Capabilities) + + local res=AUFTRAG.CheckMissionCapability(MissionTypes, Capabilities, true) + + return res +end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/Brigade.lua b/Moose Development/Moose/Ops/Brigade.lua new file mode 100644 index 000000000..be530f2e5 --- /dev/null +++ b/Moose Development/Moose/Ops/Brigade.lua @@ -0,0 +1,475 @@ +--- **Ops** - Brigade Warehouse. +-- +-- **Main Features:** +-- +-- * Manage platoons +-- * Carry out ARTY and PATROLZONE missions (AUFTRAG) +-- * Define rearming zones +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- @module Ops.Brigade +-- @image OPS_Brigade.png + + +--- BRIGADE class. +-- @type BRIGADE +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity of output. +-- @field #table rearmingZones Rearming zones. Each element is of type `#BRIGADE.SupplyZone`. +-- @field #table refuellingZones Refuelling zones. Each element is of type `#BRIGADE.SupplyZone`. +-- @field Core.Set#SET_ZONE retreatZones Retreat zone set. +-- @extends Ops.Legion#LEGION + +--- Be surprised! +-- +-- === +-- +-- # The BRIGADE Concept +-- +-- An BRIGADE consists of one or multiple PLATOONs. These platoons "live" in a WAREHOUSE that has a phyiscal struction (STATIC or UNIT) and can be captured or destroyed. +-- +-- +-- @field #BRIGADE +BRIGADE = { + ClassName = "BRIGADE", + verbose = 0, + rearmingZones = {}, + refuellingZones = {}, +} + +--- Supply Zone. +-- @type BRIGADE.SupplyZone +-- @field Core.Zone#ZONE zone The zone. +-- @field Ops.Auftrag#AUFTRAG mission Mission assigned to supply ammo or fuel. +-- @field #boolean markerOn If `true`, marker is on. +-- @field Wrapper.Marker#MARKER marker F10 marker. + +--- BRIGADE class version. +-- @field #string version +BRIGADE.version="0.1.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ToDo list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Spawn when hosting warehouse is a ship or oil rig or gas platform. +-- TODO: Rearming zones. +-- TODO: Retreat zones. +-- DONE: Add weapon range. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new BRIGADE class object. +-- @param #BRIGADE self +-- @param #string WarehouseName Name of the warehouse STATIC or UNIT object representing the warehouse. +-- @param #string BrigadeName Name of the brigade. +-- @return #BRIGADE self +function BRIGADE:New(WarehouseName, BrigadeName) + + -- Inherit everything from LEGION class. + local self=BASE:Inherit(self, LEGION:New(WarehouseName, BrigadeName)) -- #BRIGADE + + -- Nil check. + if not self then + BASE:E(string.format("ERROR: Could not find warehouse %s!", WarehouseName)) + return nil + end + + -- Set some string id for output to DCS.log file. + self.lid=string.format("BRIGADE %s | ", self.alias) + + -- Defaults + self:SetRetreatZones() + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("*", "ArmyOnMission", "*") -- An ARMYGROUP was send on a Mission (AUFTRAG). + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the BRIGADE. Initializes parameters and starts event handlers. + -- @function [parent=#BRIGADE] Start + -- @param #BRIGADE self + + --- Triggers the FSM event "Start" after a delay. Starts the BRIGADE. Initializes parameters and starts event handlers. + -- @function [parent=#BRIGADE] __Start + -- @param #BRIGADE self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Stop". Stops the BRIGADE and all its event handlers. + -- @param #BRIGADE self + + --- Triggers the FSM event "Stop" after a delay. Stops the BRIGADE and all its event handlers. + -- @function [parent=#BRIGADE] __Stop + -- @param #BRIGADE self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "ArmyOnMission". + -- @function [parent=#BRIGADE] ArmyOnMission + -- @param #BRIGADE self + -- @param Ops.ArmyGroup#ARMYGROUP ArmyGroup The ARMYGROUP on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "ArmyOnMission" after a delay. + -- @function [parent=#BRIGADE] __ArmyOnMission + -- @param #BRIGADE self + -- @param #number delay Delay in seconds. + -- @param Ops.ArmyGroup#ARMYGROUP ArmyGroup The ARMYGROUP on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "ArmyOnMission" event. + -- @function [parent=#BRIGADE] OnAfterArmyOnMission + -- @param #BRIGADE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.ArmyGroup#ARMYGROUP ArmyGroup The ARMYGROUP on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add a platoon to the brigade. +-- @param #BRIGADE self +-- @param Ops.Platoon#PLATOON Platoon The platoon object. +-- @return #BRIGADE self +function BRIGADE:AddPlatoon(Platoon) + + -- Add platoon to brigade. + table.insert(self.cohorts, Platoon) + + -- Add assets to platoon. + self:AddAssetToPlatoon(Platoon, Platoon.Ngroups) + + -- Set brigade of platoon. + Platoon:SetBrigade(self) + + -- Start platoon. + if Platoon:IsStopped() then + Platoon:Start() + end + + return self +end + + + +--- Add asset group(s) to platoon. +-- @param #BRIGADE self +-- @param Ops.Platoon#PLATOON Platoon The platoon object. +-- @param #number Nassets Number of asset groups to add. +-- @return #BRIGADE self +function BRIGADE:AddAssetToPlatoon(Platoon, Nassets) + + if Platoon then + + -- Get the template group of the platoon. + local Group=GROUP:FindByName(Platoon.templatename) + + if Group then + + -- Debug text. + local text=string.format("Adding asset %s to platoon %s", Group:GetName(), Platoon.name) + self:T(self.lid..text) + + -- Add assets to airwing warehouse. + self:AddAsset(Group, Nassets, nil, nil, nil, nil, Platoon.skill, Platoon.livery, Platoon.name) + + else + self:E(self.lid.."ERROR: Group does not exist!") + end + + else + self:E(self.lid.."ERROR: Platoon does not exit!") + end + + return self +end + +--- Define a set of retreat zones. +-- @param #BRIGADE self +-- @param Core.Set#SET_ZONE RetreatZoneSet Set of retreat zones. +-- @return #BRIGADE self +function BRIGADE:SetRetreatZones(RetreatZoneSet) + self.retreatZones=RetreatZoneSet or SET_ZONE:New() + return self +end + +--- Add a retreat zone. +-- @param #BRIGADE self +-- @param Core.Zone#ZONE RetreatZone Retreat zone. +-- @return #BRIGADE self +function BRIGADE:AddRetreatZone(RetreatZone) + self.retreatZones:AddZone(RetreatZone) + return self +end + +--- Get retreat zones. +-- @param #BRIGADE self +-- @return Core.Set#SET_ZONE Set of retreat zones. +function BRIGADE:GetRetreatZones() + return self.retreatZones +end + +--- Add a rearming zone. +-- @param #BRIGADE self +-- @param Core.Zone#ZONE RearmingZone Rearming zone. +-- @return #BRIGADE.SupplyZone The rearming zone data. +function BRIGADE:AddRearmingZone(RearmingZone) + + local rearmingzone={} --#BRIGADE.SupplyZone + + rearmingzone.zone=RearmingZone + rearmingzone.mission=nil + rearmingzone.marker=MARKER:New(rearmingzone.zone:GetCoordinate(), "Rearming Zone"):ToCoalition(self:GetCoalition()) + + table.insert(self.rearmingZones, rearmingzone) + + return rearmingzone +end + + +--- Add a refuelling zone. +-- @param #BRIGADE self +-- @param Core.Zone#ZONE RefuellingZone Refuelling zone. +-- @return #BRIGADE.SupplyZone The refuelling zone data. +function BRIGADE:AddRefuellingZone(RefuellingZone) + + local supplyzone={} --#BRIGADE.SupplyZone + + supplyzone.zone=RefuellingZone + supplyzone.mission=nil + supplyzone.marker=MARKER:New(supplyzone.zone:GetCoordinate(), "Refuelling Zone"):ToCoalition(self:GetCoalition()) + + table.insert(self.rearmingZones, supplyzone) + + return supplyzone +end + + +--- Get platoon by name. +-- @param #BRIGADE self +-- @param #string PlatoonName Name of the platoon. +-- @return Ops.Platoon#PLATOON The Platoon object. +function BRIGADE:GetPlatoon(PlatoonName) + local platoon=self:_GetCohort(PlatoonName) + return platoon +end + +--- Get platoon of an asset. +-- @param #BRIGADE self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The platoon asset. +-- @return Ops.Platoon#PLATOON The platoon object. +function BRIGADE:GetPlatoonOfAsset(Asset) + local platoon=self:GetPlatoon(Asset.squadname) + return platoon +end + +--- Remove asset from platoon. +-- @param #BRIGADE self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The platoon asset. +function BRIGADE:RemoveAssetFromPlatoon(Asset) + local platoon=self:GetPlatoonOfAsset(Asset) + if platoon then + platoon:DelAsset(Asset) + end +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Start BRIGADE FSM. +-- @param #BRIGADE self +function BRIGADE:onafterStart(From, Event, To) + + -- Start parent Warehouse. + self:GetParent(self, BRIGADE).onafterStart(self, From, Event, To) + + -- Info. + self:I(self.lid..string.format("Starting BRIGADE v%s", BRIGADE.version)) + +end + +--- Update status. +-- @param #BRIGADE self +function BRIGADE:onafterStatus(From, Event, To) + + -- Status of parent Warehouse. + self:GetParent(self).onafterStatus(self, From, Event, To) + + -- FSM state. + local fsmstate=self:GetState() + + ---------------- + -- Transport --- + ---------------- + + self:CheckTransportQueue() + + -------------- + -- Mission --- + -------------- + + -- Check if any missions should be cancelled. + self:CheckMissionQueue() + + --------------------- + -- Rearming Zones --- + --------------------- + + for _,_rearmingzone in pairs(self.rearmingZones) do + local rearmingzone=_rearmingzone --#BRIGADE.SupplyZone + if (not rearmingzone.mission) or rearmingzone.mission:IsOver() then + rearmingzone.mission=AUFTRAG:NewAMMOSUPPLY(rearmingzone.zone) + self:AddMission(rearmingzone.mission) + end + end + + ----------------------- + -- Refuelling Zones --- + ----------------------- + + -- Check refuelling zones. + for _,_supplyzone in pairs(self.refuellingZones) do + local supplyzone=_supplyzone --#BRIGADE.SupplyZone + -- Check if mission is nil or over. + if (not supplyzone.mission) or supplyzone.mission:IsOver() then + supplyzone.mission=AUFTRAG:NewFUELSUPPLY(supplyzone.zone) + self:AddMission(supplyzone.mission) + end + end + + + ----------- + -- Info --- + ----------- + + -- General info: + if self.verbose>=1 then + + -- Count missions not over yet. + local Nmissions=self:CountMissionsInQueue() + + -- Asset count. + local Npq, Np, Nq=self:CountAssetsOnMission() + + -- Asset string. + local assets=string.format("%d [OnMission: Total=%d, Active=%d, Queued=%d]", self:CountAssets(), Npq, Np, Nq) + + -- Output. + local text=string.format("%s: Missions=%d, Platoons=%d, Assets=%s", fsmstate, Nmissions, #self.cohorts, assets) + self:I(self.lid..text) + end + + ------------------ + -- Mission Info -- + ------------------ + if self.verbose>=2 then + local text=string.format("Missions Total=%d:", #self.missionqueue) + for i,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + local prio=string.format("%d/%s", mission.prio, tostring(mission.importance)) ; if mission.urgent then prio=prio.." (!)" end + local assets=string.format("%d/%d", mission:CountOpsGroups(), mission.Nassets or 0) + local target=string.format("%d/%d Damage=%.1f", mission:CountMissionTargets(), mission:GetTargetInitialNumber(), mission:GetTargetDamage()) + + text=text..string.format("\n[%d] %s %s: Status=%s, Prio=%s, Assets=%s, Targets=%s", i, mission.name, mission.type, mission.status, prio, assets, target) + end + self:I(self.lid..text) + end + + -------------------- + -- Transport Info -- + -------------------- + if self.verbose>=2 then + local text=string.format("Transports Total=%d:", #self.transportqueue) + for i,_transport in pairs(self.transportqueue) do + local transport=_transport --Ops.OpsTransport#OPSTRANSPORT + + local prio=string.format("%d/%s", transport.prio, tostring(transport.importance)) ; if transport.urgent then prio=prio.." (!)" end + local carriers=string.format("Ncargo=%d/%d, Ncarriers=%d", transport.Ncargo, transport.Ndelivered, transport.Ncarrier) + + text=text..string.format("\n[%d] UID=%d: Status=%s, Prio=%s, Cargo: %s", i, transport.uid, transport:GetState(), prio, carriers) + end + self:I(self.lid..text) + end + + ------------------- + -- Platoon Info -- + ------------------- + if self.verbose>=3 then + local text="Platoons:" + for i,_platoon in pairs(self.cohorts) do + local platoon=_platoon --Ops.Platoon#PLATOON + + local callsign=platoon.callsignName and UTILS.GetCallsignName(platoon.callsignName) or "N/A" + local modex=platoon.modex and platoon.modex or -1 + local skill=platoon.skill and tostring(platoon.skill) or "N/A" + + -- Platoon text. + text=text..string.format("\n* %s %s: %s*%d/%d, Callsign=%s, Modex=%d, Skill=%s", platoon.name, platoon:GetState(), platoon.aircrafttype, platoon:CountAssets(true), #platoon.assets, callsign, modex, skill) + end + self:I(self.lid..text) + end + + ------------------- + -- Rearming Info -- + ------------------- + if self.verbose>=4 then + local text="Rearming Zones:" + for i,_rearmingzone in pairs(self.rearmingZones) do + local rearmingzone=_rearmingzone --#BRIGADE.SupplyZone + -- Info text. + text=text..string.format("\n* %s: Mission status=%s, suppliers=%d", rearmingzone.zone:GetName(), rearmingzone.mission:GetState(), rearmingzone.mission:CountOpsGroups()) + end + self:I(self.lid..text) + end + + ------------------- + -- Refuelling Info -- + ------------------- + if self.verbose>=4 then + local text="Refuelling Zones:" + for i,_refuellingzone in pairs(self.refuellingZones) do + local refuellingzone=_refuellingzone --#BRIGADE.SupplyZone + -- Info text. + text=text..string.format("\n* %s: Mission status=%s, suppliers=%d", refuellingzone.zone:GetName(), refuellingzone.mission:GetState(), refuellingzone.mission:CountOpsGroups()) + end + self:I(self.lid..text) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "ArmyOnMission". +-- @param #BRIGADE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.ArmyGroup#ARMYGROUP ArmyGroup Ops army group on mission. +-- @param Ops.Auftrag#AUFTRAG Mission The requested mission. +function BRIGADE:onafterArmyOnMission(From, Event, To, ArmyGroup, Mission) + -- Debug info. + self:T(self.lid..string.format("Group %s on %s mission %s", ArmyGroup:GetName(), Mission:GetType(), Mission:GetName())) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/Chief.lua b/Moose Development/Moose/Ops/Chief.lua new file mode 100644 index 000000000..b449dc95c --- /dev/null +++ b/Moose Development/Moose/Ops/Chief.lua @@ -0,0 +1,2190 @@ +---- **Ops** - Chief of Staff. +-- +-- **Main Features:** +-- +-- * Automatic target engagement based on detection network +-- * Define multiple border, conflict and attack zones +-- * Define strategic "capture" zones +-- * Set stragegy of chief from passive to agressive +-- * Manual target engagement via AUFTRAG and TARGET classes +-- * Add AIRWINGS, BRIGADES and FLEETS as resources +-- * Seamless air-to-air, air-to-ground, ground-to-ground dispatching +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Ops.Chief +-- @image OPS_Chief.png + + +--- CHIEF class. +-- @type CHIEF +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity level. +-- @field #string lid Class id string for output to DCS log file. +-- @field #table targetqueue Target queue. +-- @field #table zonequeue Strategic zone queue. +-- @field Core.Set#SET_ZONE borderzoneset Set of zones defining the border of our territory. +-- @field Core.Set#SET_ZONE yellowzoneset Set of zones defining the extended border. Defcon is set to YELLOW if enemy activity is detected. +-- @field Core.Set#SET_ZONE engagezoneset Set of zones where enemies are actively engaged. +-- @field #number threatLevelMin Lowest threat level of targets to attack. +-- @field #number threatLevelMax Highest threat level of targets to attack. +-- @field #string Defcon Defence condition. +-- @field #string strategy Strategy of the CHIEF. +-- @field Ops.Commander#COMMANDER commander Commander of assigned legions. +-- @extends Ops.Intelligence#INTEL + +--- *In preparing for battle I have always found that plans are useless, but planning is indispensable* -- Dwight D Eisenhower +-- +-- === +-- +-- # The CHIEF Concept +-- +-- The Chief of staff gathers INTEL and assigns missions (AUFTRAG) the airforce, army and/or navy. +-- +-- # Territory +-- +-- The chief class allows you to define boarder zones, conflict zones and attack zones. +-- +-- ## Border Zones +-- +-- Border zones define your own territory. +-- They can be set via the @{#CHIEF.SetBorderZones}() function as a set or added zone by zone via the @{#CHIEF.AddBorderZone}() function. +-- +-- ## Conflict Zones +-- +-- Conflict zones define areas, which usually are under dispute of different coalitions. +-- They can be set via the @{#CHIEF.SetConflictZones}() function as a set or added zone by zone via the @{#CHIEF.AddConflictZone}() function. +-- +-- ## Attack Zones +-- +-- Attack zones are zones that usually lie within the enemy territory. They are only enganged with an agressive strategy. +-- They can be set via the @{#CHIEF.SetAttackZones}() function as a set or added zone by zone via the @{#CHIEF.AddAttackZone}() function. +-- +-- # Defense Condition +-- +-- The defence condition (DEFCON) depends on enemy activity detected in the different zone types and is set automatically. +-- +-- * `CHIEF.Defcon.GREEN`: No enemy activities detected. +-- * `CHIEF.Defcon.YELLOW`: Enemy activity detected in conflict zones. +-- * `CHIEF.Defcon.RED`: Enemy activity detected in border zones. +-- +-- The current DEFCON can be retrieved with the @(#CHIEF.GetDefcon)() function. +-- +-- When the DEFCON changed, an FSM event @{#CHIEF.DefconChange} is triggered. Mission designers can hook into this event via the @{#CHIEF.OnAfterDefconChange}() function: +-- +-- --- Function called when the DEFCON changes. +-- function myChief:OnAfterDefconChange(From, Event, To, Defcon) +-- local text=string.format("Changed DEFCON to %s", Defcon) +-- MESSAGE:New(text, 120):ToAll() +-- end +-- +-- # Strategy +-- +-- The strategy of the chief determines, in which areas targets are engaged automatically. +-- +-- * `CHIEF.Strategy.PASSIVE`: Chief is completely passive. No targets at all are engaged automatically. +-- * `CHIEF.Strategy.DEFENSIVE`: Chief acts defensively. Only targets in his own territory are engaged. +-- * `CHIEF.Strategy.OFFENSIVE`: Chief behaves offensively. Targets in his own territory and in conflict zones are enganged. +-- * `CHIEF.Strategy.AGGRESSIVE`: Chief is aggressive. Targets in his own territory, in conflict zones and in attack zones are enganged. +-- * `CHIEF.Strategy.TOTALWAR`: Anything anywhere is enganged. +-- +-- The strategy can be set by the @(#CHIEF.SetStrategy)() and retrieved with the @(#CHIEF.GetStrategy)() function. +-- +-- When the strategy is changed, the FSM event @{#CHIEF.StrategyChange} is triggered and customized code can be added to the @{#CHIEF.OnAfterStrategyChange}() function: +-- +-- --- Function called when the STRATEGY changes. +-- function myChief:OnAfterStrategyChange(From, Event, To, Strategy) +-- local text=string.format("Strategy changd to %s", Strategy) +-- MESSAGE:New(text, 120):ToAll() +-- end +-- +-- # Strategic (Capture) Zones +-- +-- Strategically important zones, which should be captured can be added via the @{#CHIEF.AddStrateticZone}() function. +-- +-- If the zone is currently owned by another coalition and enemy ground troops are present in the zone, a CAS mission is lauchned. +-- +-- Once the zone is cleaned of enemy forces, ground (infantry) troops are send there. These require a transportation via helicopters. +-- So in order to deploy our own troops, infantry assets with `AUFTRAG.Type.ONGUARD` and helicopters with `AUFTRAG.Type.OPSTRANSPORT` need to be available. +-- +-- Whenever a strategic zone is captured by us the FSM event @{#CHIEF.ZoneCaptured} is triggered and customized further actions can be executed +-- with the @{#CHIEF.OnAfterZoneCaptured}() function. +-- +-- Whenever a strategic zone is lost (captured by the enemy), the FSM event @{#CHIEF.ZoneLost} is triggered and customized further actions can be executed +-- with the @{#CHIEF.OnAfterZoneLost}() function. +-- +-- Further events are +-- +-- * @{#CHIEF.ZoneEmpty}, once the zone is completely empty of ground troops. Code can be added to the @{#CHIEF.OnAfterZoneEmpty}() function. +-- * @{#CHIEF.ZoneAttacked}, once the zone is under attack. Code can be added to the @{#CHIEF.OnAfterZoneAttacked}() function. +-- +-- Note that the ownership of a zone is determined via zone scans, i.e. not via the detection network. In other words, there is an all knowing eye. +-- Think of it as the local population providing the intel. It's not totally realistic but the best compromise within the limits of DCS. +-- +-- +-- +-- @field #CHIEF +CHIEF = { + ClassName = "CHIEF", + verbose = 0, + lid = nil, + targetqueue = {}, + zonequeue = {}, + borderzoneset = nil, + yellowzoneset = nil, + engagezoneset = nil, +} + +--- Defence condition. +-- @type CHIEF.DEFCON +-- @field #string GREEN No enemy activities detected in our terretory or conflict zones. +-- @field #string YELLOW Enemy in conflict zones. +-- @field #string RED Enemy within our border. +CHIEF.DEFCON = { + GREEN="Green", + YELLOW="Yellow", + RED="Red", +} + +--- Strategy. +-- @type CHIEF.Strategy +-- @field #string PASSIVE No targets at all are engaged. +-- @field #string DEFENSIVE Only target in our own terretory are engaged. +-- @field #string OFFENSIVE Targets in own terretory and yellow zones are engaged. +-- @field #string AGGRESSIVE Targets in own terretory, conflict zones and attack zones are engaged. +-- @field #string TOTALWAR Anything is engaged anywhere. +CHIEF.Strategy = { + PASSIVE="Passive", + DEFENSIVE="Defensive", + OFFENSIVE="Offensive", + AGGRESSIVE="Aggressive", + TOTALWAR="Total War" +} + +--- Mission performance. +-- @type CHIEF.MissionPerformance +-- @field #string MissionType Mission Type. +-- @field #number Performance Performance: a number between 0 and 100, where 100 is best performance. + +--- Strategic zone. +-- @type CHIEF.StrategicZone +-- @field Ops.OpsZone#OPSZONE opszone OPS zone. +-- @field #number prio Priority. +-- @field #number importance Importance +-- @field Ops.Auftrag#AUFTRAG missionPatrol Patrol mission. +-- @field Ops.Auftrag#AUFTRAG missionCAS CAS mission. + + +--- CHIEF class version. +-- @field #string version +CHIEF.version="0.0.1" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Tactical overview. +-- DONE: Add event for opsgroups on mission. +-- DONE: Add event for zone captured. +-- TODO: Limits of missions? +-- DONE: Create a good mission, which can be passed on to the COMMANDER. +-- DONE: Capture OPSZONEs. +-- DONE: Get list of own assets and capabilities. +-- DONE: Get list/overview of enemy assets etc. +-- DONE: Put all contacts into target list. Then make missions from them. +-- DONE: Set of interesting zones. +-- DONE: Add/remove spawned flightgroups to detection set. +-- DONE: Borderzones. +-- NOGO: Maybe it's possible to preselect the assets for the mission. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new CHIEF object and start the FSM. +-- @param #CHIEF self +-- @param #number Coalition Coalition side, e.g. `coaliton.side.BLUE`. Can also be passed as a string "red", "blue" or "neutral". +-- @param Core.Set#SET_GROUP AgentSet Set of agents (groups) providing intel. Default is an empty set. +-- @param #string Alias An *optional* alias how this object is called in the logs etc. +-- @return #CHIEF self +function CHIEF:New(Coalition, AgentSet, Alias) + + -- Set alias. + Alias=Alias or "CHIEF" + + -- Inherit everything from INTEL class. + local self=BASE:Inherit(self, INTEL:New(AgentSet, Coalition, Alias)) --#CHIEF + + -- Defaults. + self:SetBorderZones() + self:SetConflictZones() + self:SetAttackZones() + self:SetThreatLevelRange() + + -- Init stuff. + self.Defcon=CHIEF.DEFCON.GREEN + self.strategy=CHIEF.Strategy.DEFENSIVE + + -- Create a new COMMANDER. + self.commander=COMMANDER:New(Coalition) + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("*", "MissionAssign", "*") -- Assign mission to a COMMANDER. + self:AddTransition("*", "MissionCancel", "*") -- Cancel mission. + + self:AddTransition("*", "TransportCancel", "*") -- Cancel transport. + + self:AddTransition("*", "OpsOnMission", "*") -- An OPSGROUP was send on a Mission (AUFTRAG). + + self:AddTransition("*", "ZoneCaptured", "*") -- + self:AddTransition("*", "ZoneLost", "*") -- + self:AddTransition("*", "ZoneEmpty", "*") -- + self:AddTransition("*", "ZoneAttacked", "*") -- + + self:AddTransition("*", "DefconChange", "*") -- Change defence condition. + self:AddTransition("*", "StrategyChange", "*") -- Change strategy condition. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". + -- @function [parent=#CHIEF] Start + -- @param #CHIEF self + + --- Triggers the FSM event "Start" after a delay. + -- @function [parent=#CHIEF] __Start + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Stop". + -- @param #CHIEF self + + --- Triggers the FSM event "Stop" after a delay. + -- @function [parent=#CHIEF] __Stop + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Status". + -- @function [parent=#CHIEF] Status + -- @param #CHIEF self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#CHIEF] __Status + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "DefconChange". + -- @function [parent=#CHIEF] DefconChange + -- @param #CHIEF self + -- @param #string Defcon New Defence Condition. + + --- Triggers the FSM event "DefconChange" after a delay. + -- @function [parent=#CHIEF] __DefconChange + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + -- @param #string Defcon New Defence Condition. + + --- On after "DefconChange" event. + -- @function [parent=#CHIEF] OnAfterDefconChange + -- @param #CHIEF self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string Defcon New Defence Condition. + + + --- Triggers the FSM event "StrategyChange". + -- @function [parent=#CHIEF] StrategyChange + -- @param #CHIEF self + -- @param #string Strategy New strategy. + + --- Triggers the FSM event "StrategyChange" after a delay. + -- @function [parent=#CHIEF] __StrategyChange + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + -- @param #string Strategy New strategy. + + --- On after "StrategyChange" event. + -- @function [parent=#CHIEF] OnAfterStrategyChange + -- @param #CHIEF self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string Strategy New stragegy. + + + --- Triggers the FSM event "MissionAssign". + -- @function [parent=#CHIEF] MissionAssign + -- @param #CHIEF self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + -- @param #table Legions The Legion(s) to which the mission is assigned. + + --- Triggers the FSM event "MissionAssign" after a delay. + -- @function [parent=#CHIEF] __MissionAssign + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + -- @param #table Legions The Legion(s) to which the mission is assigned. + + --- On after "MissionAssign" event. + -- @function [parent=#CHIEF] OnAfterMissionAssign + -- @param #CHIEF self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + -- @param #table Legions The Legion(s) to which the mission is assigned. + + --- Triggers the FSM event "MissionCancel". + -- @function [parent=#CHIEF] MissionCancel + -- @param #CHIEF self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "MissionCancel" after a delay. + -- @function [parent=#CHIEF] __MissionCancel + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "MissionCancel" event. + -- @function [parent=#CHIEF] OnAfterMissionCancel + -- @param #CHIEF self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + + --- Triggers the FSM event "TransportCancel". + -- @function [parent=#CHIEF] TransportCancel + -- @param #CHIEF self + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + --- Triggers the FSM event "TransportCancel" after a delay. + -- @function [parent=#CHIEF] __TransportCancel + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + --- On after "TransportCancel" event. + -- @function [parent=#CHIEF] OnAfterTransportCancel + -- @param #CHIEF self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + + --- Triggers the FSM event "OpsOnMission". + -- @function [parent=#CHIEF] OpsOnMission + -- @param #CHIEF self + -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "OpsOnMission" after a delay. + -- @function [parent=#CHIEF] __OpsOnMission + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "OpsOnMission" event. + -- @function [parent=#CHIEF] OnAfterOpsOnMission + -- @param #CHIEF self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + + --- Triggers the FSM event "ZoneCaptured". + -- @function [parent=#CHIEF] ZoneCaptured + -- @param #CHIEF self + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that was captured. + + --- Triggers the FSM event "ZoneCaptured" after a delay. + -- @function [parent=#CHIEF] __ZoneCaptured + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that was captured. + + --- On after "ZoneCaptured" event. + -- @function [parent=#CHIEF] OnAfterZoneCaptured + -- @param #CHIEF self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that was captured. + + --- Triggers the FSM event "ZoneLost". + -- @function [parent=#CHIEF] ZoneLost + -- @param #CHIEF self + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that was lost. + + --- Triggers the FSM event "ZoneLost" after a delay. + -- @function [parent=#CHIEF] __ZoneLost + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that was lost. + + --- On after "ZoneLost" event. + -- @function [parent=#CHIEF] OnAfterZoneLost + -- @param #CHIEF self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that was lost. + + --- Triggers the FSM event "ZoneEmpty". + -- @function [parent=#CHIEF] ZoneEmpty + -- @param #CHIEF self + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that is empty now. + + --- Triggers the FSM event "ZoneEmpty" after a delay. + -- @function [parent=#CHIEF] __ZoneEmpty + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that is empty now. + + --- On after "ZoneEmpty" event. + -- @function [parent=#CHIEF] OnAfterZoneEmpty + -- @param #CHIEF self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that is empty now. + + --- Triggers the FSM event "ZoneAttacked". + -- @function [parent=#CHIEF] ZoneAttacked + -- @param #CHIEF self + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that is being attacked. + + --- Triggers the FSM event "ZoneAttacked" after a delay. + -- @function [parent=#CHIEF] __ZoneAttacked + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that is being attacked. + + --- On after "ZoneAttacked" event. + -- @function [parent=#CHIEF] OnAfterZoneAttacked + -- @param #CHIEF self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that is being attacked. + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set this to be an air-to-any dispatcher, i.e. engaging air, ground and naval targets. This is the default anyway. +-- @param #CHIEF self +-- @return #CHIEF self +function CHIEF:SetAirToAny() + + self:SetFilterCategory({}) + + return self +end + +--- Set this to be an air-to-air dispatcher. +-- @param #CHIEF self +-- @return #CHIEF self +function CHIEF:SetAirToAir() + + self:SetFilterCategory({Unit.Category.AIRPLANE, Unit.Category.HELICOPTER}) + + return self +end + +--- Set this to be an air-to-ground dispatcher, i.e. engage only ground units +-- @param #CHIEF self +-- @return #CHIEF self +function CHIEF:SetAirToGround() + + self:SetFilterCategory({Unit.Category.GROUND_UNIT}) + + return self +end + +--- Set this to be an air-to-sea dispatcher, i.e. engage only naval units. +-- @param #CHIEF self +-- @return #CHIEF self +function CHIEF:SetAirToSea() + + self:SetFilterCategory({Unit.Category.SHIP}) + + return self +end + +--- Set this to be an air-to-surface dispatcher, i.e. engaging ground and naval groups. +-- @param #CHIEF self +-- @return #CHIEF self +function CHIEF:SetAirToSurface() + + self:SetFilterCategory({Unit.Category.GROUND_UNIT, Unit.Category.SHIP}) + + return self +end + +--- Set a threat level range that will be engaged. Threat level is a number between 0 and 10, where 10 is a very dangerous threat. +-- Targets with threat level 0 are usually harmless. +-- @param #CHIEF self +-- @param #number ThreatLevelMin Min threat level. Default 1. +-- @param #number ThreatLevelMax Max threat level. Default 10. +-- @return #CHIEF self +function CHIEF:SetThreatLevelRange(ThreatLevelMin, ThreatLevelMax) + + self.threatLevelMin=ThreatLevelMin or 1 + self.threatLevelMax=ThreatLevelMax or 10 + + return self +end + +--- Set defence condition. +-- @param #CHIEF self +-- @param #string Defcon Defence condition. See @{#CHIEF.DEFCON}, e.g. `CHIEF.DEFCON.RED`. +-- @return #CHIEF self +function CHIEF:SetDefcon(Defcon) + + -- Check if valid string was passed. + local gotit=false + for _,defcon in pairs(CHIEF.DEFCON) do + if defcon==Defcon then + gotit=true + end + end + if not gotit then + self:E(self.lid..string.format("ERROR: Unknown DEFCON specified! Dont know defcon=%s", tostring(Defcon))) + return self + end + + -- Trigger event if defcon changed. + if Defcon~=self.Defcon then + self:DefconChange(Defcon) + end + + -- Set new DEFCON. + self.Defcon=Defcon + + return self +end + +--- Get defence condition. +-- @param #CHIEF self +-- @param #string Current Defence condition. See @{#CHIEF.DEFCON}, e.g. `CHIEF.DEFCON.RED`. +function CHIEF:GetDefcon(Defcon) + return self.Defcon +end + +--- Set stragegy. +-- @param #CHIEF self +-- @param #string Strategy Strategy. See @{#CHIEF.Stragegy}, e.g. `CHIEF.Strategy.DEFENSIVE` (default). +-- @return #CHIEF self +function CHIEF:SetStragety(Strategy) + + -- Trigger event if Strategy changed. + if Strategy~=self.strategy then + self:StrategyChange(Strategy) + end + + -- Set new Strategy. + self.strategy=Strategy + + return self +end + +--- Get defence condition. +-- @param #CHIEF self +-- @param #string Current Defence condition. See @{#CHIEF.DEFCON}, e.g. `CHIEF.DEFCON.RED`. +function CHIEF:GetDefcon(Defcon) + return self.Defcon +end + + +--- Get the commander. +-- @param #CHIEF self +-- @return Ops.Commander#COMMANDER The commander. +function CHIEF:GetCommander() + return self.commander +end + + +--- Add an AIRWING to the chief's commander. +-- @param #CHIEF self +-- @param Ops.AirWing#AIRWING Airwing The airwing to add. +-- @return #CHIEF self +function CHIEF:AddAirwing(Airwing) + + -- Add airwing to the commander. + self:AddLegion(Airwing) + + return self +end + +--- Add a BRIGADE to the chief's commander. +-- @param #CHIEF self +-- @param Ops.Brigade#BRIGADE Brigade The brigade to add. +-- @return #CHIEF self +function CHIEF:AddBrigade(Brigade) + + -- Add brigade to the commander + self:AddLegion(Brigade) + + return self +end + +--- Add a LEGION to the chief's commander. +-- @param #CHIEF self +-- @param Ops.Legion#LEGION Legion The legion to add. +-- @return #CHIEF self +function CHIEF:AddLegion(Legion) + + -- Set chief of the legion. + Legion.chief=self + + -- Add legion to the commander. + self.commander:AddLegion(Legion) + + return self +end + + +--- Add mission to mission queue of the COMMANDER. +-- @param #CHIEF self +-- @param Ops.Auftrag#AUFTRAG Mission Mission to be added. +-- @return #CHIEF self +function CHIEF:AddMission(Mission) + + Mission.chief=self + + self.commander:AddMission(Mission) + + return self +end + +--- Remove mission from queue. +-- @param #CHIEF self +-- @param Ops.Auftrag#AUFTRAG Mission Mission to be removed. +-- @return #CHIEF self +function CHIEF:RemoveMission(Mission) + + Mission.chief=nil + + self.commander:RemoveMission(Mission) + + return self +end + +--- Add transport to transport queue of the COMMANDER. +-- @param #CHIEF self +-- @param Ops.OpsTransport#OPSTRANSPORT Transport Transport to be added. +-- @return #CHIEF self +function CHIEF:AddOpsTransport(Transport) + + Transport.chief=self + + self.commander:AddOpsTransport(Transport) + + return self +end + +--- Remove transport from queue. +-- @param #CHIEF self +-- @param Ops.OpsTransport#OPSTRANSPORT Transport Transport to be removed. +-- @return #CHIEF self +function CHIEF:RemoveTransport(Transport) + + Transport.chief=nil + + self.commander:RemoveTransport(Transport) + + return self +end + +--- Add target. +-- @param #CHIEF self +-- @param Ops.Target#TARGET Target Target object to be added. +-- @return #CHIEF self +function CHIEF:AddTarget(Target) + + if not self:IsTarget(Target) then + table.insert(self.targetqueue, Target) + end + + return self +end + +--- Check if a TARGET is already in the queue. +-- @param #CHIEF self +-- @param Ops.Target#TARGET Target Target object to be added. +-- @return #boolean If `true`, target exists in the target queue. +function CHIEF:IsTarget(Target) + + for _,_target in pairs(self.targetqueue) do + local target=_target --Ops.Target#TARGET + if target.uid==Target.uid or target:GetName()==Target:GetName() then + return true + end + end + + return false +end + +--- Remove target from queue. +-- @param #CHIEF self +-- @param Ops.Target#TARGET Target The target. +-- @return #CHIEF self +function CHIEF:RemoveTarget(Target) + + for i,_target in pairs(self.targetqueue) do + local target=_target --Ops.Target#TARGET + + if target.uid==Target.uid then + self:I(self.lid..string.format("Removing target %s from queue", Target.name)) + table.remove(self.targetqueue, i) + break + end + + end + + return self +end + +--- Add strategically important zone. +-- @param #CHIEF self +-- @param Ops.OpsZone#OPSZONE OpsZone OPS zone object. +-- @param #number Priority Priority. +-- @param #number Importance Importance. +-- @return #CHIEF self +function CHIEF:AddStrateticZone(OpsZone, Priority, Importance) + + local stratzone={} --#CHIEF.StrategicZone + + stratzone.opszone=OpsZone + stratzone.prio=Priority or 50 + stratzone.importance=Importance + + -- Start ops zone. + if OpsZone:IsStopped() then + OpsZone:Start() + end + + -- Add to table. + table.insert(self.zonequeue, stratzone) + + -- Add chief so we get informed when something happens. + OpsZone:_AddChief(self) + + return self +end + +--- Add a rearming zone. +-- @param #CHIEF self +-- @param Core.Zone#ZONE RearmingZone Rearming zone. +-- @return Ops.Brigade#BRIGADE.SupplyZone The rearming zone data. +function CHIEF:AddRearmingZone(RearmingZone) + + -- Hand over to commander. + local supplyzone=self.commander:AddRearmingZone(RearmingZone) + + return supplyzone +end + +--- Add a refuelling zone. +-- @param #CHIEF self +-- @param Core.Zone#ZONE RefuellingZone Refuelling zone. +-- @return Ops.Brigade#BRIGADE.SupplyZone The refuelling zone data. +function CHIEF:AddRefuellingZone(RefuellingZone) + + -- Hand over to commander. + local supplyzone=self.commander:AddRefuellingZone(RefuellingZone) + + return supplyzone +end + +--- Add a CAP zone. +-- @param #CHIEF self +-- @param Core.Zone#ZONE Zone Zone. +-- @param #number Altitude Orbit altitude in feet. Default is 12,0000 feet. +-- @param #number Speed Orbit speed in KIAS. Default 350 kts. +-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West). +-- @param #number Leg Length of race-track in NM. Default 30 NM. +-- @return Ops.AirWing#AIRWING.PatrolZone The CAP zone data. +function CHIEF:AddCapZone(Zone, Altitude, Speed, Heading, Leg) + + -- Hand over to commander. + local zone=self.commander:AddCapZone(Zone, Altitude, Speed, Heading, Leg) + + return zone +end + + +--- Add an AWACS zone. +-- @param #CHIEF self +-- @param Core.Zone#ZONE Zone Zone. +-- @param #number Altitude Orbit altitude in feet. Default is 12,0000 feet. +-- @param #number Speed Orbit speed in KIAS. Default 350 kts. +-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West). +-- @param #number Leg Length of race-track in NM. Default 30 NM. +-- @return Ops.AirWing#AIRWING.PatrolZone The AWACS zone data. +function CHIEF:AddAwacsZone(Zone, Altitude, Speed, Heading, Leg) + + -- Hand over to commander. + local zone=self.commander:AddAwacsZone(Zone, Altitude, Speed, Heading, Leg) + + return zone +end + +--- Add a refuelling tanker zone. +-- @param #CHIEF self +-- @param Core.Zone#ZONE Zone Zone. +-- @param #number Altitude Orbit altitude in feet. Default is 12,0000 feet. +-- @param #number Speed Orbit speed in KIAS. Default 350 kts. +-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West). +-- @param #number Leg Length of race-track in NM. Default 30 NM. +-- @param #number RefuelSystem Refuelling system. +-- @return Ops.AirWing#AIRWING.TankerZone The tanker zone data. +function CHIEF:AddTankerZone(Zone, Altitude, Speed, Heading, Leg, RefuelSystem) + + -- Hand over to commander. + local zone=self.commander:AddTankerZone(Zone, Altitude, Speed, Heading, Leg, RefuelSystem) + + return zone +end + + +--- Set border zone set, defining your territory. +-- +-- * Detected enemy troops in these zones will trigger defence condition `RED`. +-- * Enemies in these zones will only be engaged if strategy is at least `CHIEF.STRATEGY.DEFENSIVE`. +-- +-- @param #CHIEF self +-- @param Core.Set#SET_ZONE BorderZoneSet Set of zones, defining our borders. +-- @return #CHIEF self +function CHIEF:SetBorderZones(BorderZoneSet) + + -- Border zones. + self.borderzoneset=BorderZoneSet or SET_ZONE:New() + + return self +end + +--- Add a zone defining your territory. +-- +-- * Detected enemy troops in these zones will trigger defence condition `RED`. +-- * Enemies in these zones will only be engaged if strategy is at least `CHIEF.STRATEGY.DEFENSIVE`. +-- +-- @param #CHIEF self +-- @param Core.Zone#ZONE Zone The zone. +-- @return #CHIEF self +function CHIEF:AddBorderZone(Zone) + + -- Add a border zone. + self.borderzoneset:AddZone(Zone) + + return self +end + +--- Set conflict zone set. +-- +-- * Detected enemy troops in these zones will trigger defence condition `YELLOW`. +-- * Enemies in these zones will only be engaged if strategy is at least `CHIEF.STRATEGY.OFFENSIVE`. +-- +-- @param #CHIEF self +-- @param Core.Set#SET_ZONE ZoneSet Set of zones. +-- @return #CHIEF self +function CHIEF:SetConflictZones(ZoneSet) + + -- Conflict zones. + self.yellowzoneset=ZoneSet or SET_ZONE:New() + + return self +end + +--- Add a conflict zone. +-- +-- * Detected enemy troops in these zones will trigger defence condition `YELLOW`. +-- * Enemies in these zones will only be engaged if strategy is at least `CHIEF.STRATEGY.OFFENSIVE`. +-- +-- @param #CHIEF self +-- @param Core.Zone#ZONE Zone The zone to add. +-- @return #CHIEF self +function CHIEF:AddConflictZone(Zone) + + -- Add a conflict zone. + self.yellowzoneset:AddZone(Zone) + + return self +end + +--- Set attack zone set. +-- +-- * Enemies in these zones will only be engaged if strategy is at least `CHIEF.STRATEGY.AGGRESSIVE`. +-- +-- @param #CHIEF self +-- @param Core.Set#SET_ZONE ZoneSet Set of zones. +-- @return #CHIEF self +function CHIEF:SetAttackZones(ZoneSet) + + -- Attacak zones. + self.engagezoneset=ZoneSet or SET_ZONE:New() + + return self +end + +--- Add an attack zone. +-- +-- * Enemies in these zones will only be engaged if strategy is at least `CHIEF.STRATEGY.AGGRESSIVE`. +-- +-- @param #CHIEF self +-- @param Core.Zone#ZONE Zone The zone to add. +-- @return #CHIEF self +function CHIEF:AddAttackZone(Zone) + + -- Add an attack zone. + self.engagezoneset:AddZone(Zone) + + return self +end + +--- Check if current strategy is passive. +-- @param #CHIEF self +-- @return #boolean If `true`, strategy is passive. +function CHIEF:IsPassive() + return self.strategy==CHIEF.Strategy.PASSIVE +end + +--- Check if current strategy is defensive. +-- @param #CHIEF self +-- @return #boolean If `true`, strategy is defensive. +function CHIEF:IsDefensive() + return self.strategy==CHIEF.Strategy.DEFENSIVE +end + +--- Check if current strategy is offensive. +-- @param #CHIEF self +-- @return #boolean If `true`, strategy is offensive. +function CHIEF:IsOffensive() + return self.strategy==CHIEF.Strategy.OFFENSIVE +end + +--- Check if current strategy is aggressive. +-- @param #CHIEF self +-- @return #boolean If `true`, strategy is agressive. +function CHIEF:IsAgressive() + return self.strategy==CHIEF.Strategy.AGGRESSIVE +end + +--- Check if current strategy is total war. +-- @param #CHIEF self +-- @return #boolean If `true`, strategy is total war. +function CHIEF:IsTotalWar() + return self.strategy==CHIEF.Strategy.TOTALWAR +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Start & Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. +-- @param #CHIEF self +-- @param Wrapper.Group#GROUP Group Flight group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function CHIEF:onafterStart(From, Event, To) + + -- Short info. + local text=string.format("Starting Chief of Staff") + self:I(self.lid..text) + + -- Start parent INTEL. + self:GetParent(self).onafterStart(self, From, Event, To) + + -- Start commander. + if self.commander then + if self.commander:GetState()=="NotReadyYet" then + self.commander:Start() + end + end + +end + +--- On after "Status" event. +-- @param #CHIEF self +-- @param Wrapper.Group#GROUP Group Flight group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function CHIEF:onafterStatus(From, Event, To) + + -- Start parent INTEL. + self:GetParent(self).onafterStatus(self, From, Event, To) + + -- FSM state. + local fsmstate=self:GetState() + + --- + -- CONTACTS: Mission Cleanup + --- + + -- Clean up missions where the contact was lost. + for _,_contact in pairs(self.ContactsLost) do + local contact=_contact --Ops.Intelligence#INTEL.Contact + + if contact.mission and contact.mission:IsNotOver() then + + -- Debug info. + local text=string.format("Lost contact to target %s! %s mission %s will be cancelled.", contact.groupname, contact.mission.type:upper(), contact.mission.name) + MESSAGE:New(text, 120, "CHIEF"):ToAll() + self:I(self.lid..text) + + -- Cancel this mission. + contact.mission:Cancel() + + end + + -- Remove a target from the queue. + if contact.target then + self:RemoveTarget(contact.target) + end + + end + + --- + -- CONTACTS: Create new TARGETS + --- + + -- Create TARGETs for all new contacts. + local Nborder=0 ; local Nconflict=0 ; local Nattack=0 + for _,_contact in pairs(self.Contacts) do + local contact=_contact --Ops.Intelligence#INTEL.Contact + local group=contact.group --Wrapper.Group#GROUP + + -- Check if contact inside of our borders. + local inred=self:CheckGroupInBorder(group) + if inred then + Nborder=Nborder+1 + end + + -- Check if contact is in the conflict zones. + local inyellow=self:CheckGroupInConflict(group) + if inyellow then + Nconflict=Nconflict+1 + end + + -- Check if contact is in the attack zones. + local inattack=self:CheckGroupInAttack(group) + if inattack then + Nattack=Nattack+1 + end + + + -- Check if this is not already a target. + if not contact.target then + + -- Create a new TARGET of the contact group. + local Target=TARGET:New(contact.group) + + -- Set to contact. + contact.target=Target + + -- Set contact to target. Might be handy. + Target.contact=contact + + -- Add target to queue. + self:AddTarget(Target) + + end + + end + + --- + -- Defcon + --- + + -- TODO: Need to introduce time check to avoid fast oscillation between different defcon states in case groups move in and out of the zones. + if Nborder>0 then + self:SetDefcon(CHIEF.DEFCON.RED) + elseif Nconflict>0 then + self:SetDefcon(CHIEF.DEFCON.YELLOW) + else + self:SetDefcon(CHIEF.DEFCON.GREEN) + end + + --- + -- Check Target Queue + --- + + -- Check target queue and assign missions to new targets. + self:CheckTargetQueue() + + --- + -- Check Strategic Zone Queue + --- + + -- Check target queue and assign missions to new targets. + self:CheckOpsZoneQueue() + + --- + -- Info General + --- + + if self.verbose>=1 then + local Nassets=self.commander:CountAssets() + local Ncontacts=#self.Contacts + local Nmissions=#self.commander.missionqueue + local Ntargets=#self.targetqueue + + -- Info message + local text=string.format("Defcon=%s Strategy=%s: Assets=%d, Contacts=%d [Border=%d, Conflict=%d, Attack=%d], Targets=%d, Missions=%d", + self.Defcon, self.strategy, Nassets, Ncontacts, Nborder, Nconflict, Nattack, Ntargets, Nmissions) + self:I(self.lid..text) + + end + + --- + -- Info Contacts + --- + + -- Info about contacts. + if self.verbose>=2 and #self.Contacts>0 then + local text="Contacts:" + for i,_contact in pairs(self.Contacts) do + local contact=_contact --Ops.Intelligence#INTEL.Contact + + local mtext="N/A" + if contact.mission then + mtext=string.format("\"%s\" [%s] %s", contact.mission:GetName(), contact.mission:GetType(), contact.mission.status:upper()) + end + text=text..string.format("\n[%d] %s Type=%s (%s): Threat=%d Mission=%s", i, contact.groupname, contact.categoryname, contact.typename, contact.threatlevel, mtext) + end + self:I(self.lid..text) + end + + --- + -- Info Targets + --- + + if self.verbose>=3 and #self.targetqueue>0 then + local text="Targets:" + for i,_target in pairs(self.targetqueue) do + local target=_target --Ops.Target#TARGET + + local mtext="N/A" + if target.mission then + mtext=string.format("\"%s\" [%s] %s", target.mission:GetName(), target.mission:GetType(), target.mission.status:upper()) + end + text=text..string.format("\n[%d] %s: Category=%s, prio=%d, importance=%d, alive=%s [%.1f/%.1f], Mission=%s", + i, target:GetName(), target.category, target.prio, target.importance or -1, tostring(target:IsAlive()), target:GetLife(), target:GetLife0(), mtext) + end + self:I(self.lid..text) + end + + --- + -- Info Missions + --- + + -- Mission queue. + if self.verbose>=4 and #self.commander.missionqueue>0 then + local text="Mission queue:" + for i,_mission in pairs(self.commander.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + local target=mission:GetTargetName() or "unknown" + + text=text..string.format("\n[%d] %s (%s): status=%s, target=%s", i, mission.name, mission.type, mission.status, target) + end + self:I(self.lid..text) + end + + --- + -- Info Strategic Zones + --- + + -- Loop over targets. + if self.verbose>=4 and #self.zonequeue>0 then + local text="Zone queue:" + for i,_stratzone in pairs(self.zonequeue) do + local stratzone=_stratzone --#CHIEF.StrategicZone + + -- OPS zone object. + local opszone=stratzone.opszone + + local owner=UTILS.GetCoalitionName(opszone.ownerCurrent) + local prevowner=UTILS.GetCoalitionName(opszone.ownerPrevious) + + text=text..string.format("\n[%d] %s [%s]: owner=%s [%s] (prio=%d, importance=%s): Blue=%d, Red=%d, Neutral=%d", + i, opszone.zone:GetName(), opszone:GetState(), owner, prevowner, stratzone.prio, tostring(stratzone.importance), opszone.Nblu, opszone.Nred, opszone.Nnut) + + end + self:I(self.lid..text) + end + + + --- + -- Info Assets + --- + + if self.verbose>=5 then + local text="Assets:" + for _,missiontype in pairs(AUFTRAG.Type) do + local N=self.commander:CountAssets(nil, missiontype) + if N>0 then + text=text..string.format("\n- %s: %d", missiontype, N) + end + end + self:I(self.lid..text) + + local text="Assets:" + for _,attribute in pairs(WAREHOUSE.Attribute) do + local N=self.commander:CountAssets(nil, nil, attribute) + if N>0 or self.verbose>=10 then + text=text..string.format("\n- %s: %d", attribute, N) + end + end + self:I(self.lid..text) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Events +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "MissionAssignToAny" event. +-- @param #CHIEF self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Auftrag#AUFTRAG Mission The mission. + -- @param #table Legions The Legion(s) to which the mission is assigned. +function CHIEF:onafterMissionAssign(From, Event, To, Mission, Legions) + + if self.commander then + self:I(self.lid..string.format("Assigning mission %s (%s) to COMMANDER", Mission.name, Mission.type)) + Mission.chief=self + Mission.statusChief=AUFTRAG.Status.QUEUED + self.commander:MissionAssign(Mission, Legions) + else + self:E(self.lid..string.format("Mission cannot be assigned as no COMMANDER is defined!")) + end + +end + +--- On after "MissionCancel" event. +-- @param #CHIEF self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +function CHIEF:onafterMissionCancel(From, Event, To, Mission) + + -- Debug info. + self:I(self.lid..string.format("Cancelling mission %s (%s) in status %s", Mission.name, Mission.type, Mission.status)) + + -- Set status to CANCELLED. + Mission.statusChief=AUFTRAG.Status.CANCELLED + + if Mission:IsPlanned() then + + -- Mission is still in planning stage. Should not have any LEGIONS assigned ==> Just remove it form the COMMANDER queue. + self:RemoveMission(Mission) + + else + + -- COMMANDER will cancel mission. + if Mission.commander then + Mission.commander:MissionCancel(Mission) + end + + end + +end + +--- On after "TransportCancel" event. +-- @param #CHIEF self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. +function CHIEF:onafterTransportCancel(From, Event, To, Transport) + + -- Debug info. + self:I(self.lid..string.format("Cancelling transport UID=%d in status %s", Transport.uid, Transport:GetState())) + + if Transport:IsPlanned() then + + -- Mission is still in planning stage. Should not have any LEGIONS assigned ==> Just remove it form the COMMANDER queue. + self:RemoveTransport(Transport) + + else + + -- COMMANDER will cancel mission. + if Transport.commander then + Transport.commander:TransportCancel(Transport) + end + + end + +end + +--- On after "DefconChange" event. +-- @param #CHIEF self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string Defcon New defence condition. +function CHIEF:onafterDefconChange(From, Event, To, Defcon) + self:I(self.lid..string.format("Changing Defcon from %s --> %s", self.Defcon, Defcon)) +end + +--- On after "StrategyChange" event. +-- @param #CHIEF self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string Strategy +function CHIEF:onafterStrategyChange(From, Event, To, Strategy) + self:I(self.lid..string.format("Changing Strategy from %s --> %s", self.strategy, Strategy)) +end + +--- On after "OpsOnMission". +-- @param #CHIEF self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsGroup#OPSGROUP OpsGroup Ops group on mission +-- @param Ops.Auftrag#AUFTRAG Mission The requested mission. +function CHIEF:onafterOpsOnMission(From, Event, To, OpsGroup, Mission) + -- Debug info. + self:T(self.lid..string.format("Group %s on mission %s [%s]", OpsGroup:GetName(), Mission:GetName(), Mission:GetType())) +end + + +--- On after "ZoneCaptured". +-- @param #CHIEF self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsZone#OPSZONE OpsZone The zone that was captured by us. +function CHIEF:onafterZoneCaptured(From, Event, To, OpsZone) + -- Debug info. + self:T(self.lid..string.format("Zone %s captured!", OpsZone:GetName())) +end + + +--- On after "ZoneLost". +-- @param #CHIEF self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsZone#OPSZONE OpsZone The zone that was lost. +function CHIEF:onafterZoneLost(From, Event, To, OpsZone) + -- Debug info. + self:T(self.lid..string.format("Zone %s lost!", OpsZone:GetName())) +end + +--- On after "ZoneEmpty". +-- @param #CHIEF self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsZone#OPSZONE OpsZone The zone that is empty now. +function CHIEF:onafterZoneEmpty(From, Event, To, OpsZone) + -- Debug info. + self:T(self.lid..string.format("Zone %s empty!", OpsZone:GetName())) +end + +--- On after "ZoneAttacked". +-- @param #CHIEF self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsZone#OPSZONE OpsZone The zone that being attacked. +function CHIEF:onafterZoneAttacked(From, Event, To, OpsZone) + -- Debug info. + self:T(self.lid..string.format("Zone %s attacked!", OpsZone:GetName())) +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Target Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check target queue and assign ONE valid target by adding it to the mission queue of the COMMANDER. +-- @param #CHIEF self +function CHIEF:CheckTargetQueue() + + -- Number of missions. + local Ntargets=#self.targetqueue + + -- Treat special cases. + if Ntargets==0 then + return nil + end + + -- Sort results table wrt prio and threatlevel. + local function _sort(a, b) + local taskA=a --Ops.Target#TARGET + local taskB=b --Ops.Target#TARGET + return (taskA.priotaskB.threatlevel0) + end + table.sort(self.targetqueue, _sort) + + -- Get the lowest importance value (lower means more important). + -- If a target with importance 1 exists, targets with importance 2 will not be assigned. Targets with no importance (nil) can still be selected. + local vip=math.huge + for _,_target in pairs(self.targetqueue) do + local target=_target --Ops.Target#TARGET + if target:IsAlive() and target.importance and target.importance=self.threatLevelMin and target.threatlevel0<=self.threatLevelMax + + -- Airbases, Zones and Coordinates have threat level 0. We consider them threads independent of min/max threat level set. + if target.category==TARGET.Category.AIRBASE or target.category==TARGET.Category.ZONE or target.Category==TARGET.Category.COORDINATE then + isThreat=true + end + + -- Debug message. + local text=string.format("Target %s: Alive=%s, Threat=%s, Important=%s", target:GetName(), tostring(isAlive), tostring(isThreat), tostring(isImportant)) + + -- Check if mission is done. + if target.mission then + text=text..string.format(", Mission \"%s\" (%s) [%s]", target.mission:GetName(), target.mission:GetState(), target.mission:GetType()) + if target.mission:IsOver() then + text=text..string.format(" - DONE ==> removing mission") + target.mission=nil + end + else + text=text..string.format(", NO mission yet") + end + self:T2(self.lid..text) + + -- Check that target is alive and not already a mission has been assigned. + if isAlive and isThreat and isImportant and not target.mission then + + -- Check if this target is "valid", i.e. fits with the current strategy. + local valid=false + if self.strategy==CHIEF.Strategy.PASSIVE then + + --- + -- PASSIVE: No targets at all are attacked. + --- + + valid=false + + elseif self.strategy==CHIEF.Strategy.DEFENSIVE then + + --- + -- DEFENSIVE: Attack inside borders only. + --- + + if self:CheckTargetInZones(target, self.borderzoneset) then + valid=true + end + + elseif self.strategy==CHIEF.Strategy.OFFENSIVE then + + --- + -- OFFENSIVE: Attack inside borders and in yellow zones. + --- + + if self:CheckTargetInZones(target, self.borderzoneset) or self:CheckTargetInZones(target, self.yellowzoneset) then + valid=true + end + + elseif self.strategy==CHIEF.Strategy.AGGRESSIVE then + + --- + -- AGGRESSIVE: Attack in all zone sets. + --- + + if self:CheckTargetInZones(target, self.borderzoneset) or self:CheckTargetInZones(target, self.yellowzoneset) or self:CheckTargetInZones(target, self.engagezoneset) then + valid=true + end + + elseif self.strategy==CHIEF.Strategy.TOTALWAR then + + --- + -- TOTAL WAR: We attack anything we find. + --- + + valid=true + end + + -- Valid target? + if valid then + + -- Debug info. + self:I(self.lid..string.format("Got valid target %s: category=%s, threatlevel=%d", target:GetName(), target.category, target.threatlevel0)) + + -- Get mission performances for the given target. + local MissionPerformances=self:_GetMissionPerformanceFromTarget(target) + + -- Mission. + local mission=nil --Ops.Auftrag#AUFTRAG + local Legions=nil + + if #MissionPerformances>0 then + + --TODO: Number of required assets. How many do we want? Should depend on: + -- * number of enemy units + -- * target threatlevel + -- * how many assets are still in stock + -- * is it inside of our border + local NassetsMin=1 + local NassetsMax=1 + + if target.threatlevel0>=8 then + NassetsMax=3 + elseif target.threatlevel0>=5 then + NassetsMax=2 + else + NassetsMax=1 + end + + for _,_mp in pairs(MissionPerformances) do + local mp=_mp --#CHIEF.MissionPerformance + + -- Debug info. + self:T2(self.lid..string.format("Recruiting assets for mission type %s [performance=%d] of target %s", mp.MissionType, mp.Performance, target:GetName())) + + -- Recruit assets. + local recruited, assets, legions=self:RecruitAssetsForTarget(target, mp.MissionType, NassetsMin, NassetsMax) + + if recruited then + + self:T(self.lid..string.format("Recruited %d assets for mission type %s [performance=%d] of target %s", #assets, mp.MissionType, mp.Performance, target:GetName())) + + -- Create a mission. + mission=AUFTRAG:NewFromTarget(target, mp.MissionType) + + -- Add asset to mission. + if mission then + for _,_asset in pairs(assets) do + local asset=_asset + mission:AddAsset(asset) + end + Legions=legions + + -- We got what we wanted ==> leave loop. + break + end + else + self:T(self.lid..string.format("Could NOT recruit assets for mission type %s [performance=%d] of target %s", mp.MissionType, mp.Performance, target:GetName())) + end + end + end + + -- Check if mission could be defined. + if mission and Legions then + + -- Set target mission entry. + target.mission=mission + + -- Mission parameters. + mission.prio=target.prio + mission.importance=target.importance + + -- Assign mission to legions. + self:MissionAssign(mission, Legions) + + -- Only ONE target is assigned per check. + return + end + + end + + end + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Strategic Zone Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check strategic zone queue. +-- @param #CHIEF self +function CHIEF:CheckOpsZoneQueue() + + -- Passive strategy ==> Do not act. + if self:IsPassive() then + return + end + + -- Number of zones. + local Nzones=#self.zonequeue + + -- Treat special cases. + if Nzones==0 then + return nil + end + + -- Sort results table wrt prio. + local function _sort(a, b) + local taskA=a --#CHIEF.StrategicZone + local taskB=b --#CHIEF.StrategicZone + return (taskA.prio Recruit Patrol zone infantry assets")) + + -- Recruit ground assets that + local recruited=self:RecruitAssetsForZone(stratzone, AUFTRAG.Type.ONGUARD, 1, 3, {Group.Category.GROUND}, {GROUP.Attribute.GROUND_INFANTRY}) + + -- Debug info. + self:T(self.lid..string.format("Zone is empty ==> Recruit Patrol zone infantry assets=%s", tostring(recruited))) + + end + + else + + --- + -- Zone is NOT EMPTY + -- + -- We first send a CAS flight to eliminate enemy activity. + --- + + if not hasMissionCAS then + + -- Debug message. + self:T3(self.lid..string.format("Zone is NOT empty ==> Recruit CAS assets")) + + + -- Recruite CAS assets. + local recruited=self:RecruitAssetsForZone(stratzone, AUFTRAG.Type.CAS, 1, 1) + + -- Debug message. + self:T(self.lid..string.format("Zone is NOT empty ==> Recruit CAS assets=%s", tostring(recruited))) + + end + + end + + end + end + + -- Loop over strategic zone. + for _,_startzone in pairs(self.zonequeue) do + local stratzone=_startzone --#CHIEF.StrategicZone + + -- Current owner of the zone. + local ownercoalition=stratzone.opszone:GetOwner() + + -- Has a patrol mission? + local hasMissionPatrol=stratzone.missionPatrol and stratzone.missionPatrol:IsNotOver() or false + + -- Has a CAS mission? + local hasMissionCAS=stratzone.missionCAS and stratzone.missionCAS:IsNotOver() or false + + if ownercoalition==self.coalition and stratzone.opszone:IsEmpty() and hasMissionCAS then + -- Cancel CAS mission if zone is ours and no enemies are present. + -- TODO: Might want to check if we still have CAS capable assets in stock?! + stratzone.missionCAS:Cancel() + end + + + + end + + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Zone Check Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check if group is inside our border. +-- @param #CHIEF self +-- @param Wrapper.Group#GROUP group The group. +-- @return #boolean If true, group is in any border zone. +function CHIEF:CheckGroupInBorder(group) + + local inside=self:CheckGroupInZones(group, self.borderzoneset) + + return inside +end + +--- Check if group is in a conflict zone. +-- @param #CHIEF self +-- @param Wrapper.Group#GROUP group The group. +-- @return #boolean If true, group is in any conflict zone. +function CHIEF:CheckGroupInConflict(group) + + -- Check inside yellow but not inside our border. + local inside=self:CheckGroupInZones(group, self.yellowzoneset) --and not self:CheckGroupInZones(group, self.borderzoneset) + + return inside +end + +--- Check if group is in a attack zone. +-- @param #CHIEF self +-- @param Wrapper.Group#GROUP group The group. +-- @return #boolean If true, group is in any attack zone. +function CHIEF:CheckGroupInAttack(group) + + -- Check inside yellow but not inside our border. + local inside=self:CheckGroupInZones(group, self.engagezoneset) --and not self:CheckGroupInZones(group, self.borderzoneset) + + return inside +end + +--- Check if group is inside a zone. +-- @param #CHIEF self +-- @param Wrapper.Group#GROUP group The group. +-- @param Core.Set#SET_ZONE zoneset Set of zones. +-- @return #boolean If true, group is in any zone. +function CHIEF:CheckGroupInZones(group, zoneset) + + for _,_zone in pairs(zoneset.Set or {}) do + local zone=_zone --Core.Zone#ZONE + + if group:IsInZone(zone) then + return true + end + end + + return false +end + +--- Check if group is inside a zone. +-- @param #CHIEF self +-- @param Ops.Target#TARGET target The target. +-- @param Core.Set#SET_ZONE zoneset Set of zones. +-- @return #boolean If true, group is in any zone. +function CHIEF:CheckTargetInZones(target, zoneset) + + for _,_zone in pairs(zoneset.Set or {}) do + local zone=_zone --Core.Zone#ZONE + + if zone:IsCoordinateInZone(target:GetCoordinate()) then + return true + end + end + + return false +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Resources +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a mission performance table. +-- @param #CHIEF self +-- @param #string MissionType Mission type. +-- @param #number Performance Performance. +-- @return #CHIEF.MissionPerformance Mission performance. +function CHIEF:_CreateMissionPerformance(MissionType, Performance) + local mp={} --#CHIEF.MissionPerformance + mp.MissionType=MissionType + mp.Performance=Performance + return mp +end + +--- Get mission performance for a given TARGET. +-- @param #CHIEF self +-- @param Ops.Target#TARGET Target +-- @return #table Mission performances of type `#CHIEF.MissionPerformance`. +function CHIEF:_GetMissionPerformanceFromTarget(Target) + + -- Possible target objects. + local group=nil --Wrapper.Group#GROUP + local airbase=nil --Wrapper.Airbase#AIRBASE + local scenery=nil --Wrapper.Scenery#SCENERY + local coordinate=nil --Core.Point#COORDINATE + + -- Get target objective. + local target=Target:GetObject() + + if target:IsInstanceOf("GROUP") then + group=target --Target is already a group. + elseif target:IsInstanceOf("UNIT") then + group=target:GetGroup() + elseif target:IsInstanceOf("AIRBASE") then + airbase=target + elseif target:IsInstanceOf("SCENERY") then + scenery=target + end + + -- Target category. + local TargetCategory=Target:GetCategory() + + -- Mission performances. + local missionperf={} --#CHIEF.MissionPerformance + + if group then + + local category=group:GetCategory() + local attribute=group:GetAttribute() + + if category==Group.Category.AIRPLANE or category==Group.Category.HELICOPTER then + + --- + -- A2A: Intercept + --- + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.INTERCEPT, 100)) + + elseif category==Group.Category.GROUND or category==Group.Category.TRAIN then + + --- + -- GROUND + --- + + if attribute==GROUP.Attribute.GROUND_SAM then + + -- SEAD/DEAD + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.SEAD, 100)) + + elseif attribute==GROUP.Attribute.GROUND_EWR then + + -- EWR + + --table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.SEAD, 100)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI, 100)) + + elseif attribute==GROUP.Attribute.GROUND_AAA then + + -- AAA + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI, 100)) + + elseif attribute==GROUP.Attribute.GROUND_ARTILLERY then + + -- ARTY + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI, 100)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBING, 70)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY, 30)) + + elseif attribute==GROUP.Attribute.GROUND_INFANTRY then + + -- Infantry + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI, 100)) + + else + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI, 100)) + + end + + + elseif category==Group.Category.SHIP then + + --- + -- NAVAL + --- + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ANTISHIP, 100)) + + else + self:E(self.lid.."ERROR: Unknown Group category!") + end + + elseif airbase then + + --- + -- AIRBASE + --- + + -- Bomb runway. + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBRUNWAY, 100)) + + elseif scenery then + + --- + -- SCENERY + --- + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.STRIKE, 100)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBING, 70)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBCARPET, 50)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY, 30)) + + elseif coordinate then + + --- + -- COORDINATE + --- + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBING, 100)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBCARPET, 50)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY, 30)) + + end + + return missionperf +end + +--- Get mission performances for a given Group Attribute. +-- @param #CHIEF self +-- @param #string Attribute Group attibute. +-- @return #table Mission performances of type `#CHIEF.MissionPerformance`. +function CHIEF:_GetMissionTypeForGroupAttribute(Attribute) + + local missionperf={} --#CHIEF.MissionPerformance + + if Attribute==GROUP.Attribute.AIR_ATTACKHELO then + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.INTERCEPT), 100) + + elseif Attribute==GROUP.Attribute.GROUND_AAA then + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI), 100) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBING), 80) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBCARPET), 70) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY), 30) + + elseif Attribute==GROUP.Attribute.GROUND_SAM then + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.SEAD), 100) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI), 90) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY), 50) + + elseif Attribute==GROUP.Attribute.GROUND_EWR then + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.SEAD), 100) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI), 100) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY), 50) + + end + + return missionperf +end + +--- Recruit assets for a given TARGET. +-- @param #CHIEF self +-- @param Ops.Target#TARGET Target The target. +-- @param #string MissionType Mission Type. +-- @param #number NassetsMin Min number of required assets. +-- @param #number NassetsMax Max number of required assets. +-- @return #boolean If `true` enough assets could be recruited. +-- @return #table Assets that have been recruited from all legions. +-- @return #table Legions that have recruited assets. +function CHIEF:RecruitAssetsForTarget(Target, MissionType, NassetsMin, NassetsMax) + + -- Cohorts. + local Cohorts={} + for _,_legion in pairs(self.commander.legions) do + local legion=_legion --Ops.Legion#LEGION + + -- Check that runway is operational. + local Runway=legion:IsAirwing() and legion:IsRunwayOperational() or true + + if legion:IsRunning() and Runway then + + -- Loops over cohorts. + for _,_cohort in pairs(legion.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + table.insert(Cohorts, cohort) + end + + end + end + + -- Target position. + local TargetVec2=Target:GetVec2() + + -- Recruite assets. + local recruited, assets, legions=LEGION.RecruitCohortAssets(Cohorts, MissionType, nil, NassetsMin, NassetsMax, TargetVec2) + + + return recruited, assets, legions +end + +--- Recruit assets for a given OPS zone. +-- @param #CHIEF self +-- @param #CHIEF.StrategicZone StratZone The stratetic zone. +-- @param #string MissionType Mission Type. +-- @param #number NassetsMin Min number of required assets. +-- @param #number NassetsMax Max number of required assets. +-- @param #table Categories Group categories of the assets. +-- @param #table Attributes Generalized group attributes. +-- @return #boolean If `true` enough assets could be recruited. +function CHIEF:RecruitAssetsForZone(StratZone, MissionType, NassetsMin, NassetsMax, Categories, Attributes) + + -- Cohorts. + local Cohorts={} + for _,_legion in pairs(self.commander.legions) do + local legion=_legion --Ops.Legion#LEGION + + -- Check that runway is operational. + local Runway=legion:IsAirwing() and legion:IsRunwayOperational() or true + + if legion:IsRunning() and Runway then + + -- Loops over cohorts. + for _,_cohort in pairs(legion.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + table.insert(Cohorts, cohort) + end + + end + end + + -- Target position. + local TargetVec2=StratZone.opszone.zone:GetVec2() + + -- Max range in meters. + local RangeMax=nil + + -- Set max range to 250 NM because we use helos as transport for the infantry. + if MissionType==AUFTRAG.Type.PATROLZONE or MissionType==AUFTRAG.Type.ONGUARD then + RangeMax=UTILS.NMToMeters(250) + end + + -- Recruite infantry assets. + local recruited, assets, legions=LEGION.RecruitCohortAssets(Cohorts, MissionType, nil, NassetsMin, NassetsMax, TargetVec2, nil, RangeMax, nil, nil, Categories, Attributes) + + if recruited then + + if MissionType==AUFTRAG.Type.PATROLZONE or MissionType==AUFTRAG.Type.ONGUARD then + + -- Debug messgage. + self:T2(self.lid..string.format("Recruited %d assets from for PATROL mission", #assets)) + + local recruitedTrans=true + local transport=nil + if Attributes and Attributes[1]==GROUP.Attribute.GROUND_INFANTRY then + + -- Categories. Currently only helicopters are allowed due to problems with ground transports (might get stuck, might not be a land connection. + -- TODO: Check if ground transport is possible. For example, by trying land.getPathOnRoad or something. + local Categories={Group.Category.HELICOPTER} + --local Categories={Group.Category.HELICOPTER, Group.Category.GROUND} + + -- Recruit transport assets for infantry. + recruitedTrans, transport=LEGION.AssignAssetsForTransport(self.commander, self.commander.legions, assets, 1, 1, StratZone.opszone.zone, nil, Categories) + + end + + if recruitedTrans then + + -- Create Patrol zone mission. + local mission=nil --Ops.Auftrag#AUFTRAG + + if MissionType==AUFTRAG.Type.PATROLZONE then + mission=AUFTRAG:NewPATROLZONE(StratZone.opszone.zone) + mission:SetEngageDetected(25, {"Ground Units", "Light armed ships", "Helicopters"}) + elseif MissionType==AUFTRAG.Type.ONGUARD then + mission=AUFTRAG:NewONGUARD(StratZone.opszone.zone:GetRandomCoordinate(), nil, nil, {land.SurfaceType.LAND}) + end + mission:SetEngageDetected() + + -- Add assets to mission. + for _,asset in pairs(assets) do + mission:AddAsset(asset) + end + + -- Attach OPS transport to mission. + mission.opstransport=transport + + -- Assign mission to legions. + self:MissionAssign(mission, legions) + + -- Attach mission to ops zone. + -- TODO: Need a better way! + StratZone.missionPatrol=mission + + return true + else + LEGION.UnRecruitAssets(assets) + return false + end + + elseif MissionType==AUFTRAG.Type.CAS then + + -- Create Patrol zone mission. + local mission=AUFTRAG:NewPATROLZONE(StratZone.opszone.zone) + mission:SetEngageDetected(25, {"Ground Units", "Light armed ships", "Helicopters"}) + + -- Add assets to mission. + for _,asset in pairs(assets) do + mission:AddAsset(asset) + end + + -- Assign mission to legions. + self:MissionAssign(mission, legions) + + -- Attach mission to ops zone. + -- TODO: Need a better way! + StratZone.missionCAS=mission + + return true + end + + end + + return false +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- \ No newline at end of file diff --git a/Moose Development/Moose/Ops/Cohort.lua b/Moose Development/Moose/Ops/Cohort.lua new file mode 100644 index 000000000..3dde5b505 --- /dev/null +++ b/Moose Development/Moose/Ops/Cohort.lua @@ -0,0 +1,1037 @@ +--- **Ops** - Cohort encompassed all characteristics of SQUADRONs, PLATOONs and FLOTILLAs. +-- +-- **Main Features:** +-- +-- * Set parameters like livery, skill valid for all cohort members. +-- * Define modex and callsigns. +-- * Define mission types, this cohort can perform (see Ops.Auftrag#AUFTRAG). +-- * Pause/unpause cohort operations. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Ops.Cohort +-- @image OPS_Cohort.png + + +--- COHORT class. +-- @type COHORT +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity level. +-- @field #string lid Class id string for output to DCS log file. +-- @field #string name Name of the cohort. +-- @field #string templatename Name of the template group. +-- @field #string aircrafttype Type of the units the cohort is using. +-- @field #number category Group category of the assets: `Group.Category.AIRPLANE`, `Group.Category.HELICOPTER`, `Group.Category.GROUND`, `Group.Category.SHIP`, `Group.Category.TRAIN`. +-- @field Wrapper.Group#GROUP templategroup Template group. +-- @field #table assets Cohort assets. +-- @field #table missiontypes Capabilities (mission types and performances) of the cohort. +-- @field #number maintenancetime Time in seconds needed for maintenance of a returned flight. +-- @field #number repairtime Time in seconds for each +-- @field #string livery Livery of the cohort. +-- @field #number skill Skill of cohort members. +-- @field Ops.Legion#LEGION legion The LEGION object the cohort belongs to. +-- @field #number Ngroups Number of asset OPS groups this cohort has. +-- @field #number engageRange Mission range in meters. +-- @field #string attribute Generalized attribute of the cohort template group. +-- @field #table tacanChannel List of TACAN channels available to the cohort. +-- @field #number radioFreq Radio frequency in MHz the cohort uses. +-- @field #number radioModu Radio modulation the cohort uses. +-- @field #table tacanChannel List of TACAN channels available to the cohort. +-- @field #number weightAsset Weight of one assets group in kg. +-- @field #number cargobayLimit Cargo bay capacity in kg. +-- @extends Core.Fsm#FSM + +--- *It is unbelievable what a platoon of twelve aircraft did to tip the balance.* -- Adolf Galland +-- +-- === +-- +-- # The COHORT Concept +-- +-- A COHORT is essential part of a LEGION and consists of **one** unit type. +-- +-- +-- +-- @field #COHORT +COHORT = { + ClassName = "COHORT", + verbose = 0, + lid = nil, + name = nil, + templatename = nil, + assets = {}, + missiontypes = {}, + repairtime = 0, + maintenancetime= 0, + livery = nil, + skill = nil, + legion = nil, + Ngroups = nil, + engageRange = nil, + tacanChannel = {}, + weightAsset = 99999, + cargobayLimit = 0, +} + +--- COHORT class version. +-- @field #string version +COHORT.version="0.1.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Create FLOTILLA class. +-- DONE: Make general so that PLATOON and SQUADRON can inherit this class. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new COHORT object and start the FSM. +-- @param #COHORT self +-- @param #string TemplateGroupName Name of the template group. +-- @param #number Ngroups Number of asset groups of this Cohort. Default 3. +-- @param #string CohortName Name of the cohort. +-- @return #COHORT self +function COHORT:New(TemplateGroupName, Ngroups, CohortName) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #COHORT + + -- Name of the template group. + self.templatename=TemplateGroupName + + -- Cohort name. + self.name=tostring(CohortName or TemplateGroupName) + + -- Set some string id for output to DCS.log file. + self.lid=string.format("COHORT %s | ", self.name) + + -- Template group. + self.templategroup=GROUP:FindByName(self.templatename) + + -- Check if template group exists. + if not self.templategroup then + self:E(self.lid..string.format("ERROR: Template group %s does not exist!", tostring(self.templatename))) + return nil + end + + -- Generalized attribute. + self.attribute=self.templategroup:GetAttribute() + + -- Group category. + self.category=self.templategroup:GetCategory() + + -- Aircraft type. + self.aircrafttype=self.templategroup:GetTypeName() + + -- Defaults. + self.Ngroups=Ngroups or 3 + self:SetSkill(AI.Skill.GOOD) + + -- Mission range depends on + if self.category==Group.Category.AIRPLANE then + self:SetMissionRange(200) + elseif self.category==Group.Category.HELICOPTER then + self:SetMissionRange(150) + elseif self.category==Group.Category.GROUND then + self:SetMissionRange(75) + elseif self.category==Group.Category.SHIP then + self:SetMissionRange(100) + elseif self.category==Group.Category.TRAIN then + self:SetMissionRange(100) + end + + -- Units. + local units=self.templategroup:GetUnits() + + -- Weight of the whole group. + self.weightAsset=0 + for i,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + local desc=unit:GetDesc() + self.weightAsset=self.weightAsset + (desc.massMax or 666) + if i==1 then + self.cargobayLimit=unit:GetCargoBayFreeWeight() + end + end + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "OnDuty") -- Start FSM. + self:AddTransition("*", "Status", "*") -- Status update. + + self:AddTransition("OnDuty", "Pause", "Paused") -- Pause cohort. + self:AddTransition("Paused", "Unpause", "OnDuty") -- Unpause cohort. + + self:AddTransition("*", "Stop", "Stopped") -- Stop cohort. + + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the COHORT. Initializes parameters and starts event handlers. + -- @function [parent=#COHORT] Start + -- @param #COHORT self + + --- Triggers the FSM event "Start" after a delay. Starts the COHORT. Initializes parameters and starts event handlers. + -- @function [parent=#COHORT] __Start + -- @param #COHORT self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Stop". Stops the COHORT and all its event handlers. + -- @param #COHORT self + + --- Triggers the FSM event "Stop" after a delay. Stops the COHORT and all its event handlers. + -- @function [parent=#COHORT] __Stop + -- @param #COHORT self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Status". + -- @function [parent=#COHORT] Status + -- @param #COHORT self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#COHORT] __Status + -- @param #COHORT self + -- @param #number delay Delay in seconds. + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set livery painted on all cohort units. +-- Note that the livery name in general is different from the name shown in the mission editor. +-- +-- Valid names are the names of the **livery directories**. Check out the folder in your DCS installation for: +-- +-- * Full modules: `DCS World OpenBeta\CoreMods\aircraft\\Liveries\\` +-- * AI units: `DCS World OpenBeta\Bazar\Liveries\\` +-- +-- The folder name `` is the string you want. +-- +-- Or personal liveries you have installed somewhere in your saved games folder. +-- +-- @param #COHORT self +-- @param #string LiveryName Name of the livery. +-- @return #COHORT self +function COHORT:SetLivery(LiveryName) + self.livery=LiveryName + return self +end + +--- Set skill level of all cohort team members. +-- @param #COHORT self +-- @param #string Skill Skill of all flights. +-- @usage mycohort:SetSkill(AI.Skill.EXCELLENT) +-- @return #COHORT self +function COHORT:SetSkill(Skill) + self.skill=Skill + return self +end + +--- Set verbosity level. +-- @param #COHORT self +-- @param #number VerbosityLevel Level of output (higher=more). Default 0. +-- @return #COHORT self +function COHORT:SetVerbosity(VerbosityLevel) + self.verbose=VerbosityLevel or 0 + return self +end + +--- Set turnover and repair time. If an asset returns from a mission, it will need some time until the asset is available for further missions. +-- @param #COHORT self +-- @param #number MaintenanceTime Time in minutes it takes until a flight is combat ready again. Default is 0 min. +-- @param #number RepairTime Time in minutes it takes to repair a flight for each life point taken. Default is 0 min. +-- @return #COHORT self +function COHORT:SetTurnoverTime(MaintenanceTime, RepairTime) + self.maintenancetime=MaintenanceTime and MaintenanceTime*60 or 0 + self.repairtime=RepairTime and RepairTime*60 or 0 + return self +end + +--- Set radio frequency and modulation the cohort uses. +-- @param #COHORT self +-- @param #number Frequency Radio frequency in MHz. Default 251 MHz. +-- @param #number Modulation Radio modulation. Default 0=AM. +-- @return #COHORT self +function COHORT:SetRadio(Frequency, Modulation) + self.radioFreq=Frequency or 251 + self.radioModu=Modulation or radio.modulation.AM + return self +end + +--- Set number of units in groups. +-- @param #COHORT self +-- @param #number nunits Number of units. Must be >=1 and <=4. Default 2. +-- @return #COHORT self +function COHORT:SetGrouping(nunits) + self.ngrouping=nunits or 2 + if self.ngrouping<1 then self.ngrouping=1 end + if self.ngrouping>4 then self.ngrouping=4 end + return self +end + +--- Set mission types this cohort is able to perform. +-- @param #COHORT self +-- @param #table MissionTypes Table of mission types. Can also be passed as a #string if only one type. +-- @param #number Performance Performance describing how good this mission can be performed. Higher is better. Default 50. Max 100. +-- @return #COHORT self +function COHORT:AddMissionCapability(MissionTypes, Performance) + + -- Ensure Missiontypes is a table. + if MissionTypes and type(MissionTypes)~="table" then + MissionTypes={MissionTypes} + end + + -- Set table. + self.missiontypes=self.missiontypes or {} + + for _,missiontype in pairs(MissionTypes) do + + -- Check not to add the same twice. + if AUFTRAG.CheckMissionCapability(missiontype, self.missiontypes) then + self:E(self.lid.."WARNING: Mission capability already present! No need to add it twice.") + -- TODO: update performance. + else + + local capability={} --Ops.Auftrag#AUFTRAG.Capability + capability.MissionType=missiontype + capability.Performance=Performance or 50 + table.insert(self.missiontypes, capability) + self:T(self.lid..string.format("Adding mission capability %s, performance=%d", tostring(capability.MissionType), capability.Performance)) + end + end + + -- Debug info. + self:T2(self.missiontypes) + + return self +end + +--- Get mission types this cohort is able to perform. +-- @param #COHORT self +-- @return #table Table of mission types. Could be empty {}. +function COHORT:GetMissionTypes() + + local missiontypes={} + + for _,Capability in pairs(self.missiontypes) do + local capability=Capability --Ops.Auftrag#AUFTRAG.Capability + table.insert(missiontypes, capability.MissionType) + end + + return missiontypes +end + +--- Get mission capabilities of this cohort. +-- @param #COHORT self +-- @return #table Table of mission capabilities. +function COHORT:GetMissionCapabilities() + return self.missiontypes +end + +--- Get mission performance for a given type of misson. +-- @param #COHORT self +-- @param #string MissionType Type of mission. +-- @return #number Performance or -1. +function COHORT:GetMissionPeformance(MissionType) + + for _,Capability in pairs(self.missiontypes) do + local capability=Capability --Ops.Auftrag#AUFTRAG.Capability + if capability.MissionType==MissionType then + return capability.Performance + end + end + + return -1 +end + +--- Set max mission range. Only missions in a circle of this radius around the cohort base are executed. +-- @param #COHORT self +-- @param #number Range Range in NM. Default 150 NM. +-- @return #COHORT self +function COHORT:SetMissionRange(Range) + self.engageRange=UTILS.NMToMeters(Range or 150) + return self +end + +--- Set call sign. +-- @param #COHORT self +-- @param #number Callsign Callsign from CALLSIGN.Aircraft, e.g. "Chevy" for CALLSIGN.Aircraft.CHEVY. +-- @param #number Index Callsign index, Chevy-**1**. +-- @return #COHORT self +function COHORT:SetCallsign(Callsign, Index) + self.callsignName=Callsign + self.callsignIndex=Index + return self +end + +--- Set modex. +-- @param #COHORT self +-- @param #number Modex A number like 100. +-- @param #string Prefix A prefix string, which is put before the `Modex` number. +-- @param #string Suffix A suffix string, which is put after the `Modex` number. +-- @return #COHORT self +function COHORT:SetModex(Modex, Prefix, Suffix) + self.modex=Modex + self.modexPrefix=Prefix + self.modexSuffix=Suffix + return self +end + +--- Set Legion. +-- @param #COHORT self +-- @param Ops.Legion#LEGION Legion The Legion. +-- @return #COHORT self +function COHORT:SetLegion(Legion) + self.legion=Legion + return self +end + +--- Add asset to cohort. +-- @param #COHORT self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The warehouse asset. +-- @return #COHORT self +function COHORT:AddAsset(Asset) + self:T(self.lid..string.format("Adding asset %s of type %s", Asset.spawngroupname, Asset.unittype)) + Asset.squadname=self.name + Asset.legion=self.legion + Asset.cohort=self + table.insert(self.assets, Asset) + return self +end + +--- Remove asset from chort. +-- @param #COHORT self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset. +-- @return #COHORT self +function COHORT:DelAsset(Asset) + for i,_asset in pairs(self.assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + if Asset.uid==asset.uid then + self:T2(self.lid..string.format("Removing asset %s", asset.spawngroupname)) + table.remove(self.assets, i) + break + end + end + return self +end + +--- Remove asset group from cohort. +-- @param #COHORT self +-- @param #string GroupName Name of the asset group. +-- @return #COHORT self +function COHORT:DelGroup(GroupName) + for i,_asset in pairs(self.assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + if GroupName==asset.spawngroupname then + self:T2(self.lid..string.format("Removing asset %s", asset.spawngroupname)) + table.remove(self.assets, i) + break + end + end + return self +end + +--- Get name of the cohort. +-- @param #COHORT self +-- @return #string Name of the cohort. +function COHORT:GetName() + return self.name +end + +--- Get radio frequency and modulation. +-- @param #COHORT self +-- @return #number Radio frequency in MHz. +-- @return #number Radio Modulation (0=AM, 1=FM). +function COHORT:GetRadio() + return self.radioFreq, self.radioModu +end + +--- Create a callsign for the asset. +-- @param #COHORT self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The warehouse asset. +-- @return #COHORT self +function COHORT:GetCallsign(Asset) + + if self.callsignName then + + Asset.callsign={} + + for i=1,Asset.nunits do + + local callsign={} + + callsign[1]=self.callsignName + callsign[2]=math.floor(self.callsigncounter / 10) + callsign[3]=self.callsigncounter % 10 + if callsign[3]==0 then + callsign[3]=1 + self.callsigncounter=self.callsigncounter+2 + else + self.callsigncounter=self.callsigncounter+1 + end + + Asset.callsign[i]=callsign + + self:T3({callsign=callsign}) + + --TODO: there is also a table entry .name, which is a string. + end + + + end + +end + +--- Create a modex for the asset. +-- @param #COHORT self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The warehouse asset. +-- @return #COHORT self +function COHORT:GetModex(Asset) + + if self.modex then + + Asset.modex={} + + for i=1,Asset.nunits do + + Asset.modex[i]=string.format("%03d", self.modex+self.modexcounter) + + self.modexcounter=self.modexcounter+1 + + self:T3({modex=Asset.modex[i]}) + + end + + end + +end + + +--- Add TACAN channels to the cohort. Note that channels can only range from 1 to 126. +-- @param #COHORT self +-- @param #number ChannelMin Channel. +-- @param #number ChannelMax Channel. +-- @return #COHORT self +-- @usage mysquad:AddTacanChannel(64,69) -- adds channels 64, 65, 66, 67, 68, 69 +function COHORT:AddTacanChannel(ChannelMin, ChannelMax) + + ChannelMax=ChannelMax or ChannelMin + + if ChannelMin>126 then + self:E(self.lid.."ERROR: TACAN Channel must be <= 126! Will not add to available channels") + return self + end + if ChannelMax>126 then + self:E(self.lid.."WARNING: TACAN Channel must be <= 126! Adjusting ChannelMax to 126") + ChannelMax=126 + end + + for i=ChannelMin,ChannelMax do + self.tacanChannel[i]=true + end + + return self +end + +--- Get an unused TACAN channel. +-- @param #COHORT self +-- @return #number TACAN channel or *nil* if no channel is free. +function COHORT:FetchTacan() + + -- Get the smallest free channel if there is one. + local freechannel=nil + for channel,free in pairs(self.tacanChannel) do + if free then + if freechannel==nil or channel=2 and #self.assets>0 then + + local text="" + for j,_asset in pairs(self.assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + -- Text. + text=text..string.format("\n[%d] %s (%s*%d): ", j, asset.spawngroupname, asset.unittype, asset.nunits) + + if asset.spawned then + + --- + -- Spawned + --- + + -- Mission info. + local mission=self.legion and self.legion:GetAssetCurrentMission(asset) or false + if mission then + local distance=asset.flightgroup and UTILS.MetersToNM(mission:GetTargetDistance(asset.flightgroup.group:GetCoordinate())) or 0 + text=text..string.format("Mission %s - %s: Status=%s, Dist=%.1f NM", mission.name, mission.type, mission.status, distance) + else + text=text.."Mission None" + end + + -- Flight status. + text=text..", Flight: " + if asset.flightgroup and asset.flightgroup:IsAlive() then + local status=asset.flightgroup:GetState() + text=text..string.format("%s", status) + + if asset.flightgroup:IsFlightgroup() then + local fuelmin=asset.flightgroup:GetFuelMin() + local fuellow=asset.flightgroup:IsFuelLow() + local fuelcri=asset.flightgroup:IsFuelCritical() + text=text..string.format("Fuel=%d", fuelmin) + if fuelcri then + text=text.." (Critical!)" + elseif fuellow then + text=text.." (Low)" + end + end + + local lifept, lifept0=asset.flightgroup:GetLifePoints() + text=text..string.format(", Life=%d/%d", lifept, lifept0) + + local ammo=asset.flightgroup:GetAmmoTot() + text=text..string.format(", Ammo=%d [G=%d, R=%d, B=%d, M=%d]", ammo.Total,ammo.Guns, ammo.Rockets, ammo.Bombs, ammo.Missiles) + else + text=text.."N/A" + end + + -- Payload info. + if asset.flightgroup:IsFlightgroup() then + local payload=asset.payload and table.concat(self.legion:GetPayloadMissionTypes(asset.payload), ", ") or "None" + text=text..", Payload={"..payload.."}" + end + + else + + --- + -- In Stock + --- + + text=text..string.format("In Stock") + + if self:IsRepaired(asset) then + text=text..", Combat Ready" + else + + text=text..string.format(", Repaired in %d sec", self:GetRepairTime(asset)) + + if asset.damage then + text=text..string.format(" (Damage=%.1f)", asset.damage) + end + end + + if asset.Treturned then + local T=timer.getAbsTime()-asset.Treturned + text=text..string.format(", Returned for %d sec", T) + end + + end + end + self:I(self.lid..text) + end + +end + +--- On after "Stop" event. +-- @param #COHORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function COHORT:onafterStop(From, Event, To) + + -- Debug info. + self:I(self.lid.."STOPPING Cohort and removing all assets!") + + -- Remove all assets. + for i=#self.assets,1,-1 do + local asset=self.assets[i] + self:DelAsset(asset) + end + + -- Clear call scheduler. + self.CallScheduler:Clear() + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check if there is a cohort that can execute a given mission. +-- We check the mission type, the refuelling system, mission range. +-- @param #COHORT self +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @return #boolean If true, Cohort can do that type of mission. +function COHORT:CanMission(Mission) + + local cando=true + + -- On duty?= + if not self:IsOnDuty() then + self:T(self.lid..string.format("Cohort in not OnDuty but in state %s. Cannot do mission %s with target %s", self:GetState(), Mission.name, Mission:GetTargetName())) + return false + end + + -- Check mission type. WARNING: This assumes that all assets of the cohort can do the same mission types! + if not AUFTRAG.CheckMissionType(Mission.type, self:GetMissionTypes()) then + self:T(self.lid..string.format("INFO: Cohort cannot do mission type %s (%s, %s)", Mission.type, Mission.name, Mission:GetTargetName())) + return false + end + + -- Check that tanker mission has the correct refuelling system. + if Mission.type==AUFTRAG.Type.TANKER then + + if Mission.refuelSystem and Mission.refuelSystem==self.tankerSystem then + -- Correct refueling system. + else + self:T(self.lid..string.format("INFO: Wrong refueling system requested=%s != %s=available", tostring(Mission.refuelSystem), tostring(self.tankerSystem))) + return false + end + + end + + -- Distance to target. + local TargetDistance=Mission:GetTargetDistance(self.legion:GetCoordinate()) + + -- Max engage range. + local engagerange=Mission.engageRange and math.max(self.engageRange, Mission.engageRange) or self.engageRange + + -- Set range is valid. Mission engage distance can overrule the cohort engage range. + if TargetDistance>engagerange then + self:I(self.lid..string.format("INFO: Cohort is not in range. Target dist=%d > %d NM max mission Range", UTILS.MetersToNM(TargetDistance), UTILS.MetersToNM(engagerange))) + return false + end + + return true +end + +--- Count assets in legion warehouse stock. +-- @param #COHORT self +-- @param #boolean InStock If `true`, only assets that are in the warehouse stock/inventory are counted. If `false`, only assets that are NOT in stock (i.e. spawned) are counted. If `nil`, all assets are counted. +-- @param #table MissionTypes (Optional) Count only assest that can perform certain mission type(s). Default is all types. +-- @param #table Attributes (Optional) Count only assest that have a certain attribute(s), e.g. `WAREHOUSE.Attribute.AIR_BOMBER`. +-- @return #number Number of assets. +function COHORT:CountAssets(InStock, MissionTypes, Attributes) + + local N=0 + for _,_asset in pairs(self.assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + if MissionTypes==nil or AUFTRAG.CheckMissionCapability(MissionTypes, self.missiontypes) then + if Attributes==nil or self:CheckAttribute(Attributes) then + if asset.spawned then + if InStock==false or InStock==nil then + N=N+1 --Spawned but we also count the spawned ones. + end + else + if InStock==true or InStock==nil then + N=N+1 --This is in stock. + end + end + end + end + end + + return N +end + +--- Get assets for a mission. +-- @param #COHORT self +-- @param #string MissionType Mission type. +-- @param #number Npayloads Number of payloads available. +-- @return #table Assets that can do the required mission. +-- @return #number Number of payloads still available after recruiting the assets. +function COHORT:RecruitAssets(MissionType, Npayloads) + + -- Debug info. + self:T3(self.lid..string.format("Recruiting asset for Mission type=%s", MissionType)) + + -- Recruited assets. + local assets={} + + -- Loop over assets. + for _,_asset in pairs(self.assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + -- First check that asset is not requested or reserved. This could happen if multiple requests are processed simultaniously. + if not (asset.requested or asset.isReserved) then + + + -- Check if asset is currently on a mission (STARTED or QUEUED). + if self.legion:IsAssetOnMission(asset) then + + --- + -- Asset is already on a mission. + --- + + -- Check if this asset is currently on a GCICAP mission (STARTED or EXECUTING). + if self.legion:IsAssetOnMission(asset, AUFTRAG.Type.GCICAP) and MissionType==AUFTRAG.Type.INTERCEPT then + + -- Check if the payload of this asset is compatible with the mission. + -- Note: we do not check the payload as an asset that is on a GCICAP mission should be able to do an INTERCEPT as well! + self:I(self.lid..string.format("Adding asset on GCICAP mission for an INTERCEPT mission")) + table.insert(assets, asset) + + elseif self.legion:IsAssetOnMission(asset, AUFTRAG.Type.ALERT5) and AUFTRAG.CheckMissionCapability(MissionType, asset.payload.capabilities) then + + -- Check if the payload of this asset is compatible with the mission. + self:I(self.lid..string.format("Adding asset on ALERT 5 mission for %s mission", MissionType)) + table.insert(assets, asset) + + end + + else + + --- + -- Asset as NO current mission + --- + + if asset.spawned then + + --- + -- Asset is already SPAWNED (could be uncontrolled on the airfield or inbound after another mission) + --- + + -- Opsgroup. + local flightgroup=asset.flightgroup + + + if flightgroup and flightgroup:IsAlive() and not (flightgroup:IsDead() or flightgroup:IsStopped()) then + + -- Assume we are ready and check if any condition tells us we are not. + local combatready=true + + -- Check if in a state where we really do not want to fight any more. + if flightgroup:IsFlightgroup() then + + --- + -- FLIGHTGROUP combat ready? + --- + + -- No more attacks if fuel is already low. Safety first! + if flightgroup:IsFuelLow() then + combatready=false + end + + if MissionType==AUFTRAG.Type.INTERCEPT and not flightgroup:CanAirToAir() then + combatready=false + else + local excludeguns=MissionType==AUFTRAG.Type.BOMBING or MissionType==AUFTRAG.Type.BOMBRUNWAY or MissionType==AUFTRAG.Type.BOMBCARPET or MissionType==AUFTRAG.Type.SEAD or MissionType==AUFTRAG.Type.ANTISHIP + if excludeguns and not flightgroup:CanAirToGround(excludeguns) then + combatready=false + end + end + + if flightgroup:IsHolding() or flightgroup:IsLanding() or flightgroup:IsLanded() or flightgroup:IsArrived() then + combatready=false + end + if asset.payload and not AUFTRAG.CheckMissionCapability(MissionType, asset.payload.capabilities) then + combatready=false + end + + else + + --- + -- ARMY/NAVYGROUP combat ready? + --- + + if flightgroup:IsRearming() or flightgroup:IsRetreating() or flightgroup:IsReturning() then + combatready=false + end + + end + + -- Check transport/cargo for combat readyness! + if flightgroup:IsLoading() or flightgroup:IsTransporting() or flightgroup:IsUnloading() or flightgroup:IsPickingup() or flightgroup:IsCarrier() then + combatready=false + end + if flightgroup:IsCargo() or flightgroup:IsBoarding() or flightgroup:IsAwaitingLift() then + combatready=false + end + + -- Disable this for now as it can cause problems - at least with transport and cargo assets. + combatready=false + + -- This asset is "combatready". + if combatready then + self:I(self.lid.."Adding SPAWNED asset to ANOTHER mission as it is COMBATREADY") + table.insert(assets, asset) + end + + end + + else + + --- + -- Asset is still in STOCK + --- + + -- Check that we have payloads and asset is repaired. + if Npayloads>0 and self:IsRepaired(asset) then + + -- Add this asset to the selection. + table.insert(assets, asset) + + -- Reduce number of payloads so we only return the number of assets that could do the job. + Npayloads=Npayloads-1 + + end + + end + end + + end -- not requested check + end -- loop over assets + + self:T2(self.lid..string.format("Recruited %d assets for Mission type=%s", #assets, MissionType)) + + return assets, Npayloads +end + + +--- Get the time an asset needs to be repaired. +-- @param #COHORT self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset. +-- @return #number Time in seconds until asset is repaired. +function COHORT:GetRepairTime(Asset) + + if Asset.Treturned then + + local t=self.maintenancetime + t=t+Asset.damage*self.repairtime + + -- Seconds after returned. + local dt=timer.getAbsTime()-Asset.Treturned + + local T=t-dt + + return T + else + return 0 + end + +end + +--- Checks if a mission type is contained in a table of possible types. +-- @param #COHORT self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset. +-- @return #boolean If true, the requested mission type is part of the possible mission types. +function COHORT:IsRepaired(Asset) + + if Asset.Treturned then + local Tnow=timer.getAbsTime() + local Trepaired=Asset.Treturned+self.maintenancetime + if Tnow>=Trepaired then + return true + else + return false + end + + else + return true + end + +end + +--- Check if the cohort attribute matches the given attribute(s). +-- @param #COHORT self +-- @param #table Attributes The requested attributes. See `WAREHOUSE.Attribute` enum. Can also be passed as a single attribute `#string`. +-- @return #boolean If true, the cohort has the requested attribute. +function COHORT:CheckAttribute(Attributes) + + if type(Attributes)~="table" then + Attributes={Attributes} + end + + for _,attribute in pairs(Attributes) do + if attribute==self.attribute then + return true + end + end + + return false +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + diff --git a/Moose Development/Moose/Ops/Commander.lua b/Moose Development/Moose/Ops/Commander.lua new file mode 100644 index 000000000..894718a78 --- /dev/null +++ b/Moose Development/Moose/Ops/Commander.lua @@ -0,0 +1,1489 @@ +--- **Ops** - Commander of Airwings, Brigades and Fleets. +-- +-- **Main Features:** +-- +-- * Manages AIRWINGS, BRIGADEs and FLOTILLAs +-- * Handles missions (AUFTRAG) and finds the best assets for the job +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Ops.Commander +-- @image OPS_Commander.png + + +--- COMMANDER class. +-- @type COMMANDER +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity level. +-- @field #string lid Class id string for output to DCS log file. +-- @field #number coalition Coalition side of the commander. +-- @field #string alias Alias name. +-- @field #table legions Table of legions which are commanded. +-- @field #table missionqueue Mission queue. +-- @field #table transportqueue Transport queue. +-- @field #table rearmingZones Rearming zones. Each element is of type `#BRIGADE.SupplyZone`. +-- @field #table refuellingZones Refuelling zones. Each element is of type `#BRIGADE.SupplyZone`. +-- @field #table capZones CAP zones. Each element is of type `#AIRWING.PatrolZone`. +-- @field #table awacsZones AWACS zones. Each element is of type `#AIRWING.PatrolZone`. +-- @field #table tankerZones Tanker zones. Each element is of type `#AIRWING.TankerZone`. +-- @field Ops.Chief#CHIEF chief Chief of staff. +-- @extends Core.Fsm#FSM + +--- *He who has never leared to obey cannot be a good commander* -- Aristotle +-- +-- === +-- +-- # The COMMANDER Concept +-- +-- A commander is the head of legions. He/she will find the best LEGIONs to perform an assigned AUFTRAG (mission) or OPSTRANSPORT. +-- A legion can be an AIRWING, BRIGADE or FLEET. +-- +-- # Constructor +-- +-- A new COMMANDER object is created with the @{#COMMANDER.New}(*Coalition, Alias*) function, where the parameter *Coalition* is the coalition side. +-- It can be `coalition.side.RED`, `coalition.side.BLUE` or `coalition.side.NEUTRAL`. This parameter is mandatory! +-- +-- The second parameter *Alias* is optional and can be used to give the COMMANDER a "name", which is used for output in the dcs.log file. +-- +-- local myCommander=COMANDER:New(coalition.side.BLUE, "General Patton") +-- +-- # Adding Legions +-- +-- Legions, i.e. AIRWINGS, BRIGADES and FLEETS can be added via the @{#COMMANDER.AddLegion}(*Legion*) command: +-- +-- myCommander:AddLegion(myLegion) +-- +-- ## Adding Airwings +-- +-- It is also possible to use @{#COMMANDER.AddAirwing}(*myAirwing*) function. This does the same as the `AddLegion` function but might be a bit more intuitive. +-- +-- ## Adding Brigades +-- +-- It is also possible to use @{#COMMANDER.AddBrigade}(*myBrigade*) function. This does the same as the `AddLegion` function but might be a bit more intuitive. +-- +-- ## Adding Fleets +-- +-- It is also possible to use @{#COMMANDER.AddFleet}(*myFleet*) function. This does the same as the `AddLegion` function but might be a bit more intuitive. +-- +-- # Adding Missions +-- +-- Mission can be added via the @{#COMMANDER.AddMission}(*myMission*) function. +-- +-- # Adding OPS Transports +-- +-- Transportation assignments can be added via the @{#COMMANDER.AddOpsTransport}(*myTransport*) function. +-- +-- # Adding CAP Zones +-- +-- A CAP zone can be added via the @{#COMMANDER.AddCapZone}() function. +-- +-- # Adding Rearming Zones +-- +-- A rearming zone can be added via the @{#COMMANDER.AddRearmingZone}() function. +-- +-- # Adding Refuelling Zones +-- +-- A refuelling zone can be added via the @{#COMMANDER.AddRefuellingZone}() function. +-- +-- +-- # FSM Events +-- +-- The COMMANDER will +-- +-- # OPSGROUP on Mission +-- +-- Whenever an OPSGROUP (FLIGHTGROUP, ARMYGROUP or NAVYGROUP) is send on a mission, the `OnAfterOpsOnMission()` event is triggered. +-- Mission designers can hook into the event with the @{#COMMANDER.OnAfterOpsOnMission}() function +-- +-- function myCommander:OnAfterOpsOnMission(From, Event, To, OpsGroup, Mission) +-- -- Your code +-- end +-- +-- # Canceling a Mission +-- +-- A mission can be cancelled with the @{#COMMMANDER.MissionCancel}() function +-- +-- myCommander:MissionCancel(myMission) +-- +-- or +-- myCommander:__MissionCancel(5*60, myMission) +-- +-- The last commander cancels the mission after 5 minutes (300 seconds). +-- +-- The cancel command will be forwarded to all assigned legions and OPS groups, which will abort their mission or remove it from their queue. +-- +-- @field #COMMANDER +COMMANDER = { + ClassName = "COMMANDER", + verbose = 0, + coalition = nil, + legions = {}, + missionqueue = {}, + transportqueue = {}, + rearmingZones = {}, + refuellingZones = {}, + capZones = {}, + awacsZones = {}, + tankerZones = {}, +} + +--- COMMANDER class version. +-- @field #string version +COMMANDER.version="0.1.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- DONE: Add CAP zones. +-- DONE: Add tanker zones. +-- DONE: Improve legion selection. Mostly done! +-- DONE: Find solution for missions, which require a transport. This is not as easy as it sounds since the selected mission assets restrict the possible transport assets. +-- DONE: Add ops transports. +-- DONE: Allow multiple Legions for one mission. +-- NOGO: Maybe it's possible to preselect the assets for the mission. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new COMMANDER object and start the FSM. +-- @param #COMMANDER self +-- @param #number Coalition Coaliton of the commander. +-- @param #string Alias Some name you want the commander to be called. +-- @return #COMMANDER self +function COMMANDER:New(Coalition, Alias) + + -- Inherit everything from INTEL class. + local self=BASE:Inherit(self, FSM:New()) --#COMMANDER + + if Coalition==nil then + env.error("ERROR: Coalition parameter is nil in COMMANDER:New() call!") + return nil + end + + -- Set coaliton. + self.coalition=Coalition + + -- Alias name. + self.alias=Alias + + -- Choose a name for red or blue. + if self.alias==nil then + if Coalition==coalition.side.BLUE then + self.alias="George S. Patton" + elseif Coalition==coalition.side.RED then + self.alias="Georgy Zhukov" + elseif Coalition==coalition.side.NEUTRAL then + self.alias="Mahatma Gandhi" + end + end + + -- Log ID. + self.lid=string.format("COMMANDER %s [%s] | ", self.alias, UTILS.GetCoalitionName(self.coalition)) + + -- Start state. + self:SetStartState("NotReadyYet") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("NotReadyYet", "Start", "OnDuty") -- Start COMMANDER. + self:AddTransition("*", "Status", "*") -- Status report. + self:AddTransition("*", "Stop", "Stopped") -- Stop COMMANDER. + + self:AddTransition("*", "MissionAssign", "*") -- Mission is assigned to a or multiple LEGIONs. + self:AddTransition("*", "MissionCancel", "*") -- COMMANDER cancels a mission. + + self:AddTransition("*", "TransportAssign", "*") -- Transport is assigned to a or multiple LEGIONs. + self:AddTransition("*", "TransportCancel", "*") -- COMMANDER cancels a Transport. + + self:AddTransition("*", "OpsOnMission", "*") -- An OPSGROUP was send on a Mission (AUFTRAG). + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the COMMANDER. + -- @function [parent=#COMMANDER] Start + -- @param #COMMANDER self + + --- Triggers the FSM event "Start" after a delay. Starts the COMMANDER. + -- @function [parent=#COMMANDER] __Start + -- @param #COMMANDER self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Stop". Stops the COMMANDER. + -- @param #COMMANDER self + + --- Triggers the FSM event "Stop" after a delay. Stops the COMMANDER. + -- @function [parent=#COMMANDER] __Stop + -- @param #COMMANDER self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Status". + -- @function [parent=#COMMANDER] Status + -- @param #COMMANDER self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#COMMANDER] __Status + -- @param #COMMANDER self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "MissionAssign". Mission is added to a LEGION mission queue and already requested. Needs assets to be added to the mission! + -- @function [parent=#COMMANDER] MissionAssign + -- @param #COMMANDER self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + -- @param #table Legions The Legion(s) to which the mission is assigned. + + --- Triggers the FSM event "MissionAssign" after a delay. Mission is added to a LEGION mission queue and already requested. Needs assets to be added to the mission! + -- @function [parent=#COMMANDER] __MissionAssign + -- @param #COMMANDER self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + -- @param #table Legions The Legion(s) to which the mission is assigned. + + --- On after "MissionAssign" event. + -- @function [parent=#COMMANDER] OnAfterMissionAssign + -- @param #COMMANDER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + -- @param #table Legions The Legion(s) to which the mission is assigned. + + + --- Triggers the FSM event "MissionCancel". + -- @function [parent=#COMMANDER] MissionCancel + -- @param #COMMANDER self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "MissionCancel" after a delay. + -- @function [parent=#COMMANDER] __MissionCancel + -- @param #COMMANDER self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "MissionCancel" event. + -- @function [parent=#COMMANDER] OnAfterMissionCancel + -- @param #COMMANDER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + + --- Triggers the FSM event "TransportAssign". + -- @function [parent=#COMMANDER] TransportAssign + -- @param #COMMANDER self + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + -- @param #table Legions The legion(s) to which this transport is assigned. + + --- Triggers the FSM event "TransportAssign" after a delay. + -- @function [parent=#COMMANDER] __TransportAssign + -- @param #COMMANDER self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + -- @param #table Legions The legion(s) to which this transport is assigned. + + --- On after "TransportAssign" event. + -- @function [parent=#COMMANDER] OnAfterTransportAssign + -- @param #COMMANDER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + -- @param #table Legions The legion(s) to which this transport is assigned. + + + --- Triggers the FSM event "TransportCancel". + -- @function [parent=#COMMANDER] TransportCancel + -- @param #COMMANDER self + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + --- Triggers the FSM event "TransportCancel" after a delay. + -- @function [parent=#COMMANDER] __TransportCancel + -- @param #COMMANDER self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + --- On after "TransportCancel" event. + -- @function [parent=#COMMANDER] OnAfterTransportCancel + -- @param #COMMANDER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + + --- Triggers the FSM event "OpsOnMission". + -- @function [parent=#COMMANDER] OpsOnMission + -- @param #COMMANDER self + -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "OpsOnMission" after a delay. + -- @function [parent=#COMMANDER] __OpsOnMission + -- @param #COMMANDER self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "OpsOnMission" event. + -- @function [parent=#COMMANDER] OnAfterOpsOnMission + -- @param #COMMANDER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set verbosity level. +-- @param #COMMANDER self +-- @param #number VerbosityLevel Level of output (higher=more). Default 0. +-- @return #COMMANDER self +function COMMANDER:SetVerbosity(VerbosityLevel) + self.verbose=VerbosityLevel or 0 + return self +end + +--- Get coalition. +-- @param #COMMANDER self +-- @return #number Coalition. +function COMMANDER:GetCoalition() + return self.coalition +end + +--- Add an AIRWING to the commander. +-- @param #COMMANDER self +-- @param Ops.AirWing#AIRWING Airwing The airwing to add. +-- @return #COMMANDER self +function COMMANDER:AddAirwing(Airwing) + + -- Add legion. + self:AddLegion(Airwing) + + return self +end + +--- Add an BRIGADE to the commander. +-- @param #COMMANDER self +-- @param Ops.Brigade#BRIGADE Brigade The brigade to add. +-- @return #COMMANDER self +function COMMANDER:AddBrigade(Brigade) + + -- Add legion. + self:AddLegion(Brigade) + + return self +end + +--- Add a LEGION to the commander. +-- @param #COMMANDER self +-- @param Ops.Legion#LEGION Legion The legion to add. +-- @return #COMMANDER self +function COMMANDER:AddLegion(Legion) + + -- This legion is managed by the commander. + Legion.commander=self + + -- Add to legions. + table.insert(self.legions, Legion) + + return self +end + +--- Add mission to mission queue. +-- @param #COMMANDER self +-- @param Ops.Auftrag#AUFTRAG Mission Mission to be added. +-- @return #COMMANDER self +function COMMANDER:AddMission(Mission) + + if not self:IsMission(Mission) then + + Mission.commander=self + + Mission.statusCommander=AUFTRAG.Status.PLANNED + + table.insert(self.missionqueue, Mission) + + end + + return self +end + +--- Add transport to queue. +-- @param #COMMANDER self +-- @param Ops.OpsTransport#OPSTRANSPORT Transport The OPS transport to be added. +-- @return #COMMANDER self +function COMMANDER:AddOpsTransport(Transport) + + Transport.commander=self + + Transport.statusCommander=TRANSPORT.Status.PLANNED + + table.insert(self.transportqueue, Transport) + + return self +end + +--- Remove mission from queue. +-- @param #COMMANDER self +-- @param Ops.Auftrag#AUFTRAG Mission Mission to be removed. +-- @return #COMMANDER self +function COMMANDER:RemoveMission(Mission) + + for i,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission.auftragsnummer==Mission.auftragsnummer then + self:I(self.lid..string.format("Removing mission %s (%s) status=%s from queue", Mission.name, Mission.type, Mission.status)) + mission.commander=nil + table.remove(self.missionqueue, i) + break + end + + end + + return self +end + +--- Remove transport from queue. +-- @param #COMMANDER self +-- @param Ops.OpsTransport#OPSTRANSPORT Transport The OPS transport to be removed. +-- @return #COMMANDER self +function COMMANDER:RemoveTransport(Transport) + + for i,_transport in pairs(self.transportqueue) do + local transport=_transport --Ops.OpsTransport#OPSTRANSPORT + + if transport.uid==Transport.uid then + self:I(self.lid..string.format("Removing transport UID=%d status=%s from queue", transport.uid, transport:GetState())) + transport.commander=nil + table.remove(self.transportqueue, i) + break + end + + end + + return self +end + +--- Add a rearming zone. +-- @param #COMMANDER self +-- @param Core.Zone#ZONE RearmingZone Rearming zone. +-- @return Ops.Brigade#BRIGADE.SupplyZone The rearming zone data. +function COMMANDER:AddRearmingZone(RearmingZone) + + local rearmingzone={} --Ops.Brigade#BRIGADE.SupplyZone + + rearmingzone.zone=RearmingZone + rearmingzone.mission=nil + rearmingzone.marker=MARKER:New(rearmingzone.zone:GetCoordinate(), "Rearming Zone"):ToCoalition(self:GetCoalition()) + + table.insert(self.rearmingZones, rearmingzone) + + return rearmingzone +end + +--- Add a refuelling zone. +-- @param #COMMANDER self +-- @param Core.Zone#ZONE RefuellingZone Refuelling zone. +-- @return Ops.Brigade#BRIGADE.SupplyZone The refuelling zone data. +function COMMANDER:AddRefuellingZone(RefuellingZone) + + local rearmingzone={} --Ops.Brigade#BRIGADE.SupplyZone + + rearmingzone.zone=RefuellingZone + rearmingzone.mission=nil + rearmingzone.marker=MARKER:New(rearmingzone.zone:GetCoordinate(), "Refuelling Zone"):ToCoalition(self:GetCoalition()) + + table.insert(self.refuellingZones, rearmingzone) + + return rearmingzone +end + +--- Add a CAP zone. +-- @param #COMMANDER self +-- @param Core.Zone#ZONE CapZone Zone. +-- @param #number Altitude Orbit altitude in feet. Default is 12,0000 feet. +-- @param #number Speed Orbit speed in KIAS. Default 350 kts. +-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West). +-- @param #number Leg Length of race-track in NM. Default 30 NM. +-- @return Ops.AirWing#AIRWING.PatrolZone The CAP zone data. +function COMMANDER:AddCapZone(Zone, Altitude, Speed, Heading, Leg) + + local patrolzone={} --Ops.AirWing#AIRWING.PatrolZone + + patrolzone.zone=Zone + patrolzone.altitude=Altitude or 12000 + patrolzone.heading=Heading or 270 + patrolzone.speed=UTILS.KnotsToAltKIAS(Speed or 350, patrolzone.altitude) + patrolzone.leg=Leg or 30 + patrolzone.mission=nil + patrolzone.marker=MARKER:New(patrolzone.zone:GetCoordinate(), "AWACS Zone"):ToCoalition(self:GetCoalition()) + + table.insert(self.capZones, patrolzone) + + return patrolzone +end + +--- Add an AWACS zone. +-- @param #COMMANDER self +-- @param Core.Zone#ZONE Zone Zone. +-- @param #number Altitude Orbit altitude in feet. Default is 12,0000 feet. +-- @param #number Speed Orbit speed in KIAS. Default 350 kts. +-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West). +-- @param #number Leg Length of race-track in NM. Default 30 NM. +-- @return Ops.AirWing#AIRWING.PatrolZone The AWACS zone data. +function COMMANDER:AddAwacsZone(Zone, Altitude, Speed, Heading, Leg) + + local awacszone={} --Ops.AirWing#AIRWING.PatrolZone + + awacszone.zone=Zone + awacszone.altitude=Altitude or 12000 + awacszone.heading=Heading or 270 + awacszone.speed=UTILS.KnotsToAltKIAS(Speed or 350, awacszone.altitude) + awacszone.leg=Leg or 30 + awacszone.mission=nil + awacszone.marker=MARKER:New(awacszone.zone:GetCoordinate(), "AWACS Zone"):ToCoalition(self:GetCoalition()) + + table.insert(self.awacsZones, awacszone) + + return awacszone +end + +--- Add a refuelling tanker zone. +-- @param #COMMANDER self +-- @param Core.Zone#ZONE Zone Zone. +-- @param #number Altitude Orbit altitude in feet. Default is 12,0000 feet. +-- @param #number Speed Orbit speed in KIAS. Default 350 kts. +-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West). +-- @param #number Leg Length of race-track in NM. Default 30 NM. +-- @param #number RefuelSystem Refuelling system. +-- @return Ops.AirWing#AIRWING.TankerZone The tanker zone data. +function COMMANDER:AddTankerZone(Zone, Altitude, Speed, Heading, Leg, RefuelSystem) + + local tankerzone={} --Ops.AirWing#AIRWING.TankerZone + + tankerzone.zone=Zone + tankerzone.altitude=Altitude or 12000 + tankerzone.heading=Heading or 270 + tankerzone.speed=UTILS.KnotsToAltKIAS(Speed or 350, tankerzone.altitude) + tankerzone.leg=Leg or 30 + tankerzone.refuelsystem=RefuelSystem + tankerzone.mission=nil + tankerzone.marker=MARKER:New(tankerzone.zone:GetCoordinate(), "Tanker Zone"):ToCoalition(self:GetCoalition()) + + table.insert(self.tankerZones, tankerzone) + + return tankerzone +end + +--- Check if this mission is already in the queue. +-- @param #COMMANDER self +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @return #boolean If `true`, this mission is in the queue. +function COMMANDER:IsMission(Mission) + + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + if mission.auftragsnummer==Mission.auftragsnummer then + return true + end + end + + return false +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Start & Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. Starts the FLIGHTGROUP FSM and event handlers. +-- @param #COMMANDER self +-- @param Wrapper.Group#GROUP Group Flight group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function COMMANDER:onafterStart(From, Event, To) + + -- Short info. + local text=string.format("Starting Commander") + self:I(self.lid..text) + + -- Start attached legions. + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + if legion:GetState()=="NotReadyYet" then + legion:Start() + end + end + + self:__Status(-1) +end + +--- On after "Status" event. +-- @param #COMMANDER self +-- @param Wrapper.Group#GROUP Group Flight group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function COMMANDER:onafterStatus(From, Event, To) + + -- FSM state. + local fsmstate=self:GetState() + + -- Status. + if self.verbose>=1 then + local text=string.format("Status %s: Legions=%d, Missions=%d, Transports", fsmstate, #self.legions, #self.missionqueue, #self.transportqueue) + self:I(self.lid..text) + end + + -- Check mission queue and assign one PLANNED mission. + self:CheckMissionQueue() + + -- Check transport queue and assign one PLANNED transport. + self:CheckTransportQueue() + + -- Check rearming zones. + for _,_rearmingzone in pairs(self.rearmingZones) do + local rearmingzone=_rearmingzone --Ops.Brigade#BRIGADE.SupplyZone + -- Check if mission is nil or over. + if (not rearmingzone.mission) or rearmingzone.mission:IsOver() then + rearmingzone.mission=AUFTRAG:NewAMMOSUPPLY(rearmingzone.zone) + self:AddMission(rearmingzone.mission) + end + end + + -- Check refuelling zones. + for _,_supplyzone in pairs(self.refuellingZones) do + local supplyzone=_supplyzone --Ops.Brigade#BRIGADE.SupplyZone + -- Check if mission is nil or over. + if (not supplyzone.mission) or supplyzone.mission:IsOver() then + supplyzone.mission=AUFTRAG:NewFUELSUPPLY(supplyzone.zone) + self:AddMission(supplyzone.mission) + end + end + + + -- Check CAP zones. + for _,_patrolzone in pairs(self.capZones) do + local patrolzone=_patrolzone --Ops.AirWing#AIRWING.PatrolZone + -- Check if mission is nil or over. + if (not patrolzone.mission) or patrolzone.mission:IsOver() then + local Coordinate=patrolzone.zone:GetCoordinate() + patrolzone.mission=AUFTRAG:NewCAP(patrolzone.zone, patrolzone.altitude, patrolzone.speed, Coordinate, patrolzone.heading, patrolzone.leg) + self:AddMission(patrolzone.mission) + end + end + + -- Check AWACS zones. + for _,_awacszone in pairs(self.awacsZones) do + local awacszone=_awacszone --Ops.AirWing#AIRWING.Patrol + -- Check if mission is nil or over. + if (not awacszone.mission) or awacszone.mission:IsOver() then + local Coordinate=awacszone.zone:GetCoordinate() + awacszone.mission=AUFTRAG:NewAWACS(Coordinate, awacszone.altitude, awacszone.speed, awacszone.heading, awacszone.leg) + self:AddMission(awacszone.mission) + end + end + + -- Check Tanker zones. + for _,_tankerzone in pairs(self.tankerZones) do + local tankerzone=_tankerzone --Ops.AirWing#AIRWING.TankerZone + -- Check if mission is nil or over. + if (not tankerzone.mission) or tankerzone.mission:IsOver() then + local Coordinate=tankerzone.zone:GetCoordinate() + tankerzone.mission=AUFTRAG:NewTANKER(Coordinate, tankerzone.altitude, tankerzone.speed, tankerzone.heading, tankerzone.leg, tankerzone.refuelsystem) + self:AddMission(tankerzone.mission) + end + end + + --- + -- LEGIONS + --- + + if self.verbose>=2 and #self.legions>0 then + + local text="Legions:" + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + local Nassets=legion:CountAssets() + local Nastock=legion:CountAssets(true) + text=text..string.format("\n* %s [%s]: Assets=%s stock=%s", legion.alias, legion:GetState(), Nassets, Nastock) + for _,aname in pairs(AUFTRAG.Type) do + local na=legion:CountAssets(true, {aname}) + local np=legion:CountPayloadsInStock({aname}) + local nm=legion:CountAssetsOnMission({aname}) + if na>0 or np>0 then + text=text..string.format("\n - %s: assets=%d, payloads=%d, on mission=%d", aname, na, np, nm) + end + end + end + self:I(self.lid..text) + + + if self.verbose>=3 then + + -- Count numbers + local Ntotal=0 + local Nspawned=0 + local Nrequested=0 + local Nreserved=0 + local Nstock=0 + + local text="\n===========================================\n" + text=text.."Assets:" + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + + for _,_cohort in pairs(legion.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + + for _,_asset in pairs(cohort.assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + local state="In Stock" + if asset.flightgroup then + state=asset.flightgroup:GetState() + local mission=legion:GetAssetCurrentMission(asset) + if mission then + state=state..string.format(", Mission \"%s\" [%s]", mission:GetName(), mission:GetType()) + end + else + if asset.spawned then + env.info("FF ERROR: asset has opsgroup but is NOT spawned!") + end + if asset.requested and asset.isReserved then + env.info("FF ERROR: asset is requested and reserved. Should not be both!") + state="Reserved+Requested!" + elseif asset.isReserved then + state="Reserved" + elseif asset.requested then + state="Requested" + end + end + + -- Text. + text=text..string.format("\n[UID=%03d] %s Legion=%s [%s]: State=%s [RID=%s]", + asset.uid, asset.spawngroupname, legion.alias, cohort.name, state, tostring(asset.rid)) + + + if asset.spawned then + Nspawned=Nspawned+1 + end + if asset.requested then + Nrequested=Nrequested+1 + end + if asset.isReserved then + Nreserved=Nreserved+1 + end + if not (asset.spawned or asset.requested or asset.isReserved) then + Nstock=Nstock+1 + end + + Ntotal=Ntotal+1 + + end + + end + + end + text=text.."\n-------------------------------------------" + text=text..string.format("\nNstock = %d", Nstock) + text=text..string.format("\nNreserved = %d", Nreserved) + text=text..string.format("\nNrequested = %d", Nrequested) + text=text..string.format("\nNspawned = %d", Nspawned) + text=text..string.format("\nNtotal = %d (=%d)", Ntotal, Nstock+Nspawned+Nrequested+Nreserved) + text=text.."\n===========================================" + self:I(self.lid..text) + end + + end + + --- + -- MISSIONS + --- + + -- Mission queue. + if self.verbose>=2 and #self.missionqueue>0 then + local text="Mission queue:" + for i,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + local target=mission:GetTargetName() or "unknown" + text=text..string.format("\n[%d] %s (%s): status=%s, target=%s", i, mission.name, mission.type, mission.status, target) + end + self:I(self.lid..text) + end + + --- + -- TRANSPORTS + --- + + -- Transport queue. + if self.verbose>=2 and #self.transportqueue>0 then + local text="Transport queue:" + for i,_transport in pairs(self.transportqueue) do + local transport=_transport --Ops.OpsTransport#OPSTRANSPORT + text=text..string.format("\n[%d] UID=%d: status=%s", i, transport.uid, transport:GetState()) + end + self:I(self.lid..text) + end + + self:__Status(-30) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Events +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "MissionAssign" event. Mission is added to a LEGION mission queue and already requested. Needs assets to be added to the mission already. +-- @param #COMMANDER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @param #table Legions The Legion(s) to which the mission is assigned. +function COMMANDER:onafterMissionAssign(From, Event, To, Mission, Legions) + + -- Add mission to queue. + self:AddMission(Mission) + + -- Set mission commander status to QUEUED as it is now queued at a legion. + Mission.statusCommander=AUFTRAG.Status.QUEUED + + for _,_Legion in pairs(Legions) do + local Legion=_Legion --Ops.Legion#LEGION + + -- Debug info. + self:I(self.lid..string.format("Assigning mission \"%s\" [%s] to legion \"%s\"", Mission.name, Mission.type, Legion.alias)) + + -- Add mission to legion. + Legion:AddMission(Mission) + + -- Directly request the mission as the assets have already been selected. + Legion:MissionRequest(Mission) + + end + +end + +--- On after "MissionCancel" event. +-- @param #COMMANDER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +function COMMANDER:onafterMissionCancel(From, Event, To, Mission) + + -- Debug info. + self:I(self.lid..string.format("Cancelling mission \"%s\" [%s] in status %s", Mission.name, Mission.type, Mission.status)) + + -- Set commander status. + Mission.statusCommander=AUFTRAG.Status.CANCELLED + + if Mission:IsPlanned() then + + -- Mission is still in planning stage. Should not have a legion assigned ==> Just remove it form the queue. + self:RemoveMission(Mission) + + else + + -- Legion will cancel mission. + if #Mission.legions>0 then + for _,_legion in pairs(Mission.legions) do + local legion=_legion --Ops.Legion#LEGION + + -- TODO: Should check that this legions actually belongs to this commander. + + -- Legion will cancel the mission. + legion:MissionCancel(Mission) + end + end + + end + +end + +--- On after "TransportAssign" event. Transport is added to a LEGION mission queue. +-- @param #COMMANDER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + -- @param #table Legions The legion(s) to which this transport is assigned. +function COMMANDER:onafterTransportAssign(From, Event, To, Transport, Legions) + + -- Set mission commander status to QUEUED as it is now queued at a legion. + Transport.statusCommander=OPSTRANSPORT.Status.QUEUED + + for _,_Legion in pairs(Legions) do + local Legion=_Legion --Ops.Legion#LEGION + + -- Debug info. + self:I(self.lid..string.format("Assigning transport UID=%d to legion \"%s\"", Transport.uid, Legion.alias)) + + -- Add mission to legion. + Legion:AddOpsTransport(Transport) + + -- Directly request the mission as the assets have already been selected. + Legion:TransportRequest(Transport) + + end + +end + +--- On after "TransportCancel" event. +-- @param #COMMANDER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. +function COMMANDER:onafterTransportCancel(From, Event, To, Transport) + + -- Debug info. + self:I(self.lid..string.format("Cancelling Transport UID=%d in status %s", Transport.uid, Transport:GetState())) + + -- Set commander status. + Transport.statusCommander=OPSTRANSPORT.Status.CANCELLED + + if Transport:IsPlanned() then + + -- Transport is still in planning stage. Should not have a legion assigned ==> Just remove it form the queue. + self:RemoveTransport(Transport) + + else + + -- Legion will cancel mission. + if #Transport.legions>0 then + for _,_legion in pairs(Transport.legions) do + local legion=_legion --Ops.Legion#LEGION + + -- TODO: Should check that this legions actually belongs to this commander. + + -- Legion will cancel the mission. + legion:TransportCancel(Transport) + end + end + + end + +end + +--- On after "OpsOnMission". +-- @param #COMMANDER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsGroup#OPSGROUP OpsGroup Ops group on mission +-- @param Ops.Auftrag#AUFTRAG Mission The requested mission. +function COMMANDER:onafterOpsOnMission(From, Event, To, OpsGroup, Mission) + -- Debug info. + self:T2(self.lid..string.format("Group \"%s\" on mission \"%s\" [%s]", OpsGroup:GetName(), Mission:GetName(), Mission:GetType())) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Mission Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check mission queue and assign ONE planned mission. +-- @param #COMMANDER self +function COMMANDER:CheckMissionQueue() + + -- Number of missions. + local Nmissions=#self.missionqueue + + -- Treat special cases. + if Nmissions==0 then + return nil + end + + -- Sort results table wrt prio and start time. + local function _sort(a, b) + local taskA=a --Ops.Auftrag#AUFTRAG + local taskB=b --Ops.Auftrag#AUFTRAG + return (taskA.prio take own legions. + if #Cohorts==0 then + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + for _,_cohort in pairs(legion.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + table.insert(Cohorts, cohort) + end + end + end + + -- Number of required assets. + local NreqMin, NreqMax=Mission:GetRequiredAssets() + + -- Target position. + local TargetVec2=Mission:GetTargetVec2() + + -- Special payloads. + local Payloads=Mission.payloads + + -- Recruite assets. + local recruited, assets, legions=LEGION.RecruitCohortAssets(Cohorts, Mission.type, Mission.alert5MissionType, NreqMin, NreqMax, TargetVec2, Payloads, Mission.engageRange, Mission.refuelSystem, nil) + + return recruited, assets, legions +end + +--- Recruit assets performing an escort mission for a given asset. +-- @param #COMMANDER self +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @param #table Assets Table of assets to be escorted. +-- @return #boolean If `true`, enough assets could be recruited or no escort was required in the first place. +function COMMANDER:RecruitAssetsForEscort(Mission, Assets) + + -- Is an escort requested in the first place? + if Mission.NescortMin and Mission.NescortMax and (Mission.NescortMin>0 or Mission.NescortMax>0) then + + -- Cohorts. + local Cohorts={} + for _,_legion in pairs(Mission.escortLegions or {}) do + local legion=_legion --Ops.Legion#LEGION + for _,_cohort in pairs(legion.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + table.insert(Cohorts, cohort) + end + end + for _,_cohort in pairs(Mission.escortCohorts or {}) do + local cohort=_cohort --Ops.Cohort#COHORT + table.insert(Cohorts, cohort) + end + + -- No special escort legions/cohorts found ==> take own legions. + if #Cohorts==0 then + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + for _,_cohort in pairs(legion.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + table.insert(Cohorts, cohort) + end + end + end + + + -- Call LEGION function but provide COMMANDER as self. + local assigned=LEGION.AssignAssetsForEscort(self, Cohorts, Assets, Mission.NescortMin, Mission.NescortMax) + + return assigned + end + + return true +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Transport Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check transport queue and assign ONE planned transport. +-- @param #COMMANDER self +function COMMANDER:CheckTransportQueue() + + -- Number of missions. + local Ntransports=#self.transportqueue + + -- Treat special cases. + if Ntransports==0 then + return nil + end + + -- Sort results table wrt prio and start time. + local function _sort(a, b) + local taskA=a --Ops.Auftrag#AUFTRAG + local taskB=b --Ops.Auftrag#AUFTRAG + return (taskA.prio0 then + for _,_opsgroup in pairs(cargoOpsGroups) do + local opsgroup=_opsgroup --Ops.OpsGroup#OPSGROUP + local weight=opsgroup:GetWeightTotal() + if weight>weightGroup then + weightGroup=weight + end + end + end + + if weightGroup>0 then + + -- Recruite assets from legions. + local recruited, assets, legions=self:RecruitAssetsForTransport(transport, weightGroup) + + if recruited then + + -- Add asset to transport. + for _,_asset in pairs(assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + transport:AddAsset(asset) + end + + -- Assign transport to legion(s). + self:TransportAssign(transport, legions) + + -- Only ONE transport is assigned. + return + else + -- Not recruited. + LEGION.UnRecruitAssets(assets) + end + + end + + else + + --- + -- Missions NOT in PLANNED state + --- + + end + + end + +end + +--- Recruit assets for a given OPS transport. +-- @param #COMMANDER self +-- @param Ops.OpsTransport#OPSTRANSPORT Transport The OPS transport. +-- @return #boolean If `true`, enough assets could be recruited. +-- @return #table Recruited assets. +-- @return #table Legions that have recruited assets. +function COMMANDER:RecruitAssetsForTransport(Transport, CargoWeight) + + if CargoWeight==0 then + -- No cargo groups! + return false, {}, {} + end + + -- Cohorts. + local Cohorts={} + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + + -- Check that runway is operational. + local Runway=legion:IsAirwing() and legion:IsRunwayOperational() or true + + if legion:IsRunning() and Runway then + + -- Loops over cohorts. + for _,_cohort in pairs(legion.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + table.insert(Cohorts, cohort) + end + + end + end + + + -- Target is the deploy zone. + local TargetVec2=Transport:GetDeployZone():GetVec2() + + -- Number of required carriers. + local NreqMin,NreqMax=Transport:GetRequiredCarriers() + + -- Recruit assets and legions. + local recruited, assets, legions=LEGION.RecruitCohortAssets(Cohorts, AUFTRAG.Type.OPSTRANSPORT, nil, NreqMin, NreqMax, TargetVec2, nil, nil, nil, CargoWeight) + + return recruited, assets, legions +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Resources +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Count assets of all assigned legions. +-- @param #COMMANDER self +-- @param #boolean InStock If true, only assets that are in the warehouse stock/inventory are counted. +-- @param #table MissionTypes (Optional) Count only assest that can perform certain mission type(s). Default is all types. +-- @param #table Attributes (Optional) Count only assest that have a certain attribute(s), e.g. `WAREHOUSE.Attribute.AIR_BOMBER`. +-- @return #number Amount of asset groups. +function COMMANDER:CountAssets(InStock, MissionTypes, Attributes) + + local N=0 + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + N=N+legion:CountAssets(InStock, MissionTypes, Attributes) + end + + return N +end + +--- Count assets of all assigned legions. +-- @param #COMMANDER self +-- @param #boolean InStock If true, only assets that are in the warehouse stock/inventory are counted. +-- @param #table Legions (Optional) Table of legions. Default is all legions. +-- @param #table MissionTypes (Optional) Count only assest that can perform certain mission type(s). Default is all types. +-- @param #table Attributes (Optional) Count only assest that have a certain attribute(s), e.g. `WAREHOUSE.Attribute.AIR_BOMBER`. +-- @return #number Amount of asset groups. +function COMMANDER:GetAssets(InStock, Legions, MissionTypes, Attributes) + + -- Selected assets. + local assets={} + + for _,_legion in pairs(Legions or self.legions) do + local legion=_legion --Ops.Legion#LEGION + + --TODO Check if legion is running and maybe if runway is operational if air assets are requested. + + for _,_cohort in pairs(legion.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + + for _,_asset in pairs(cohort.assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + -- TODO: Check if repaired. + -- TODO: currently we take only unspawned assets. + if not (asset.spawned or asset.isReserved or asset.requested) then + table.insert(assets, asset) + end + + end + end + end + + return assets +end + +--- Check all legions if they are able to do a specific mission type at a certain location with a given number of assets. +-- @param #COMMANDER self +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @return #table Table of LEGIONs that can do the mission and have at least one asset available right now. +function COMMANDER:GetLegionsForMission(Mission) + + -- Table of legions that can do the mission. + local legions={} + + -- Loop over all legions. + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + + -- Count number of assets in stock. + local Nassets=0 + if legion:IsAirwing() then + Nassets=legion:CountAssetsWithPayloadsInStock(Mission.payloads, {Mission.type}, Attributes) + else + Nassets=legion:CountAssets(true, {Mission.type}, Attributes) --Could also specify the attribute if Air or Ground mission. + end + + -- Has it assets that can? + if Nassets>0 and false then + + -- Get coordinate of the target. + local coord=Mission:GetTargetCoordinate() + + if coord then + + -- Distance from legion to target. + local distance=UTILS.MetersToNM(coord:Get2DDistance(legion:GetCoordinate())) + + -- Round: 55 NM ==> 5.5 ==> 6, 63 NM ==> 6.3 ==> 6 + local dist=UTILS.Round(distance/10, 0) + + -- Debug info. + self:I(self.lid..string.format("Got legion %s with Nassets=%d and dist=%.1f NM, rounded=%.1f", legion.alias, Nassets, distance, dist)) + + -- Add legion to table of legions that can. + table.insert(legions, {airwing=legion, distance=distance, dist=dist, targetcoord=coord, nassets=Nassets}) + + end + + end + + -- Add legion if it can provide at least 1 asset. + if Nassets>0 then + table.insert(legions, legion) + end + + end + + return legions +end + +--- Get assets on given mission or missions. +-- @param #COMMANDER self +-- @param #table MissionTypes Types on mission to be checked. Default all. +-- @return #table Assets on pending requests. +function COMMANDER:GetAssetsOnMission(MissionTypes) + + local assets={} + + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + -- Check if this mission type is requested. + if AUFTRAG.CheckMissionType(mission.type, MissionTypes) then + + for _,_asset in pairs(mission.assets or {}) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + table.insert(assets, asset) + end + end + end + + return assets +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- \ No newline at end of file diff --git a/Moose Development/Moose/Ops/FlightGroup.lua b/Moose Development/Moose/Ops/FlightGroup.lua index a9e42d16e..24e435237 100644 --- a/Moose Development/Moose/Ops/FlightGroup.lua +++ b/Moose Development/Moose/Ops/FlightGroup.lua @@ -17,13 +17,13 @@ -- === -- -- ## Example Missions: --- +-- -- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20Flightgroup). --- +-- -- === -- -- ### Author: **funkyfranky** --- +-- -- === -- @module Ops.FlightGroup -- @image OPS_FlightGroup.png @@ -31,12 +31,8 @@ --- FLIGHTGROUP class. -- @type FLIGHTGROUP --- @field Wrapper.Airbase#AIRBASE homebase The home base of the flight group. --- @field Wrapper.Airbase#AIRBASE destbase The destination base of the flight group. --- @field Core.Zone#ZONE homezone The home zone of the flight group. Set when spawn happens in air. --- @field Core.Zone#ZONE destzone The destination zone of the flight group. Set when final waypoint is in air. -- @field #string actype Type name of the aircraft. --- @field #number rangemax Max range in km. +-- @field #number rangemax Max range in meters. -- @field #number ceiling Max altitude the aircraft can fly at in meters. -- @field #number tankertype The refueling system type (0=boom, 1=probe), if the group is a tanker. -- @field #number refueltype The refueling system type (0=boom, 1=probe), if the group can refuel from a tanker. @@ -48,8 +44,6 @@ -- @field #boolean fuelcritical Fuel critical switch. -- @field #number fuelcriticalthresh Critical fuel threshold in percent. -- @field #boolean fuelcriticalrtb RTB on critical fuel switch. --- @field Ops.Squadron#SQUADRON squadron The squadron of this flight group. --- @field Ops.AirWing#AIRWING airwing The airwing the flight group belongs to. -- @field Ops.FlightControl#FLIGHTCONTROL flightcontrol The flightcontrol handling this group. -- @field Ops.Airboss#AIRBOSS airboss The airboss handling this group. -- @field Core.UserFlag#USERFLAG flaghold Flag for holding. @@ -57,10 +51,8 @@ -- @field #number Tparking Abs. mission time stamp when the group was spawned uncontrolled and is parking. -- @field #table menu F10 radio menu. -- @field #string controlstatus Flight control status. --- @field #boolean ishelo If true, the is a helicopter group. --- @field #number callsignName Callsign name. --- @field #number callsignNumber Callsign number. -- @field #boolean despawnAfterLanding If true, group is despawned after landed at an airbase. +-- @field #number RTBRecallCount Number that counts RTB calls. -- -- @extends Ops.OpsGroup#OPSGROUP @@ -68,8 +60,6 @@ -- -- === -- --- ![Banner Image](..\Presentations\OPS\FlightGroup\_Main.png) --- -- # The FLIGHTGROUP Concept -- -- # Events @@ -140,16 +130,16 @@ FLIGHTGROUP = { fuelcritical = nil, fuelcriticalthresh = nil, fuelcriticalrtb = false, - outofAAMrtb = true, - outofAGMrtb = true, - squadron = nil, + outofAAMrtb = false, + outofAGMrtb = false, flightcontrol = nil, flaghold = nil, Tholding = nil, Tparking = nil, + Twaiting = nil, menu = nil, - ishelo = nil, - RTBRecallCount = 0, + isHelo = nil, + RTBRecallCount = 0, } @@ -176,44 +166,21 @@ FLIGHTGROUP.Attribute = { OTHER="Other", } ---- Flight group element. --- @type FLIGHTGROUP.Element --- @field #string name Name of the element, i.e. the unit/client. --- @field Wrapper.Unit#UNIT unit Element unit object. --- @field Wrapper.Group#GROUP group Group object of the element. --- @field #string modex Tail number. --- @field #string skill Skill level. --- @field #boolean ai If true, element is AI. --- @field Wrapper.Client#CLIENT client The client if element is occupied by a human player. --- @field #table pylons Table of pylons. --- @field #number fuelmass Mass of fuel in kg. --- @field #number category Aircraft category. --- @field #string categoryname Aircraft category name. --- @field #string callsign Call sign, e.g. "Uzi 1-1". --- @field #string status Status, i.e. born, parking, taxiing. See @{#OPSGROUP.ElementStatus}. --- @field #number damage Damage of element in percent. --- @field Wrapper.Airbase#AIRBASE.ParkingSpot parking The parking spot table the element is parking on. - - --- FLIGHTGROUP class version. -- @field #string version -FLIGHTGROUP.version="0.6.1" +FLIGHTGROUP.version="0.7.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: VTOL aircraft. --- TODO: Use new UnitLost event instead of crash/dead. --- TODO: Options EPLRS, Afterburner restrict etc. --- DONE: Add TACAN beacon. --- TODO: Damage? --- TODO: shot events? --- TODO: Marks to add waypoints/tasks on-the-fly. -- TODO: Mark assigned parking spot on F10 map. -- TODO: Let user request a parking spot via F10 marker :) --- TODO: Monitor traveled distance in air ==> calculate fuel consumption ==> calculate range remaining. Will this give half way accurate results? +-- DONE: Use new UnitLost event instead of crash/dead. +-- DONE: Monitor traveled distance in air ==> calculate fuel consumption ==> calculate range remaining. Will this give half way accurate results? -- DONE: Out of AG/AA missiles. Safe state of out-of-ammo. +-- DONE: Add TACAN beacon. -- DONE: Add tasks. -- DONE: Waypoints, read, add, insert, detour. -- DONE: Get ammo. @@ -233,10 +200,10 @@ FLIGHTGROUP.version="0.6.1" function FLIGHTGROUP:New(group) -- First check if we already have a flight group for this group. - local fg=_DATABASE:GetFlightGroup(group) - if fg then - fg:I(fg.lid..string.format("WARNING: Flight group already exists in data base!")) - return fg + local og=_DATABASE:GetOpsGroup(group) + if og then + og:I(og.lid..string.format("WARNING: OPS group already exists in data base!")) + return og end -- Inherit everything from FSM class. @@ -246,15 +213,14 @@ function FLIGHTGROUP:New(group) self.lid=string.format("FLIGHTGROUP %s | ", self.groupname) -- Defaults - --self:SetVerbosity(0) + self:SetDefaultROE() + self:SetDefaultROT() + self:SetDefaultEPLRS(self.isEPLRS) + self:SetDetection() self:SetFuelLowThreshold() self:SetFuelLowRTB() self:SetFuelCriticalThreshold() - self:SetFuelCriticalRTB() - self:SetDefaultROE() - self:SetDefaultROT() - self:SetDetection() - self.isFlightgroup=true + self:SetFuelCriticalRTB() -- Holding flag. self.flaghold=USERFLAG:New(string.format("%s_FlagHold", self.groupname)) @@ -262,27 +228,22 @@ function FLIGHTGROUP:New(group) -- Add FSM transitions. -- From State --> Event --> To State - self:AddTransition("*", "RTB", "Inbound") -- Group is returning to destination base. + self:AddTransition("*", "LandAtAirbase", "Inbound") -- Group is ordered to land at an airbase. + self:AddTransition("*", "RTB", "Inbound") -- Group is returning to (home/destination) airbase. self:AddTransition("*", "RTZ", "Inbound") -- Group is returning to destination zone. Not implemented yet! self:AddTransition("Inbound", "Holding", "Holding") -- Group is in holding pattern. self:AddTransition("*", "Refuel", "Going4Fuel") -- Group is send to refuel at a tanker. - self:AddTransition("Going4Fuel", "Refueled", "Airborne") -- Group finished refueling. + self:AddTransition("Going4Fuel", "Refueled", "Cruising") -- Group finished refueling. self:AddTransition("*", "LandAt", "LandingAt") -- Helo group is ordered to land at a specific point. self:AddTransition("LandingAt", "LandedAt", "LandedAt") -- Helo group landed landed at a specific point. - self:AddTransition("*", "Wait", "*") -- Group is orbiting. - self:AddTransition("*", "FuelLow", "*") -- Fuel state of group is low. Default ~25%. self:AddTransition("*", "FuelCritical", "*") -- Fuel state of group is critical. Default ~10%. - self:AddTransition("*", "OutOfMissilesAA", "*") -- Group is out of A2A missiles. - self:AddTransition("*", "OutOfMissilesAG", "*") -- Group is out of A2G missiles. - self:AddTransition("*", "OutOfMissilesAS", "*") -- Group is out of A2S(ship) missiles. Not implemented yet! - - self:AddTransition("Airborne", "EngageTarget", "Engaging") -- Engage targets. - self:AddTransition("Engaging", "Disengage", "Airborne") -- Engagement over. + self:AddTransition("Cruising", "EngageTarget", "Engaging") -- Engage targets. + self:AddTransition("Engaging", "Disengage", "Cruising") -- Engagement over. self:AddTransition("*", "ElementParking", "*") -- An element is parking. self:AddTransition("*", "ElementEngineOn", "*") -- An element spooled up the engines. @@ -298,6 +259,7 @@ function FLIGHTGROUP:New(group) self:AddTransition("*", "Taxiing", "Taxiing") -- The whole flight group is taxiing. self:AddTransition("*", "Takeoff", "Airborne") -- The whole flight group is airborne. self:AddTransition("*", "Airborne", "Airborne") -- The whole flight group is airborne. + self:AddTransition("*", "Cruise", "Cruising") -- The whole flight group is cruising. self:AddTransition("*", "Landing", "Landing") -- The whole flight group is landing. self:AddTransition("*", "Landed", "Landed") -- The whole flight group has landed. self:AddTransition("*", "Arrived", "Arrived") -- The whole flight group has arrived. @@ -317,16 +279,6 @@ function FLIGHTGROUP:New(group) -- TODO: Add pseudo functions. - -- Debug trace. - if false then - BASE:TraceOnOff(true) - BASE:TraceClass(self.ClassName) - BASE:TraceLevel(1) - end - - -- Add to data base. - _DATABASE:AddFlightGroup(self) - -- Handle events: self:HandleEvent(EVENTS.Birth, self.OnEventBirth) self:HandleEvent(EVENTS.EngineStartup, self.OnEventEngineStartup) @@ -341,20 +293,23 @@ function FLIGHTGROUP:New(group) self:HandleEvent(EVENTS.Kill, self.OnEventKill) -- Init waypoints. - self:InitWaypoints() + self:_InitWaypoints() -- Initialize group. self:_InitGroup() -- Start the status monitoring. - self:__Status(-1) - + self.timerStatus=TIMER:New(self.Status, self):Start(1, 30) + -- Start queue update timer. self.timerQueueUpdate=TIMER:New(self._QueueUpdate, self):Start(2, 5) - + -- Start check zone timer. self.timerCheckZone=TIMER:New(self._CheckInZones, self):Start(3, 10) + -- Add OPSGROUP to _DATABASE. + _DATABASE:AddOpsGroup(self) + return self end @@ -372,21 +327,19 @@ function FLIGHTGROUP:AddTaskEnrouteEngageTargetsInZone(ZoneRadius, TargetTypes, self:AddTaskEnroute(Task) end ---- Set AIRWING the flight group belongs to. --- @param #FLIGHTGROUP self --- @param Ops.AirWing#AIRWING airwing The AIRWING object. --- @return #FLIGHTGROUP self -function FLIGHTGROUP:SetAirwing(airwing) - self:T(self.lid..string.format("Add flight to AIRWING %s", airwing.alias)) - self.airwing=airwing - return self -end - --- Get airwing the flight group belongs to. -- @param #FLIGHTGROUP self -- @return Ops.AirWing#AIRWING The AIRWING object. function FLIGHTGROUP:GetAirWing() - return self.airwing + return self.legion +end + +--- Set if aircraft is VTOL capable. Unfortunately, there is no DCS way to determine this via scripting. +-- @param #FLIGHTGROUP self +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetVTOL() + self.isVTOL=true + return self end --- Set the FLIGHTCONTROL controlling this flight group. @@ -434,6 +387,9 @@ end -- @param Wrapper.Airbase#AIRBASE HomeAirbase The home airbase. -- @return #FLIGHTGROUP self function FLIGHTGROUP:SetHomebase(HomeAirbase) + if type(HomeAirbase)=="string" then + HomeAirbase=AIRBASE:FindByName(HomeAirbase) + end self.homebase=HomeAirbase return self end @@ -443,6 +399,9 @@ end -- @param Wrapper.Airbase#AIRBASE DestinationAirbase The destination airbase. -- @return #FLIGHTGROUP self function FLIGHTGROUP:SetDestinationbase(DestinationAirbase) + if type(DestinationAirbase)=="string" then + DestinationAirbase=AIRBASE:FindByName(DestinationAirbase) + end self.destbase=DestinationAirbase return self end @@ -540,55 +499,6 @@ function FLIGHTGROUP:SetFuelCriticalRTB(switch) return self end ---- Enable to automatically engage detected targets. --- @param #FLIGHTGROUP self --- @param #number RangeMax Max range in NM. Only detected targets within this radius from the group will be engaged. Default is 25 NM. --- @param #table TargetTypes Types of target attributes that will be engaged. See [DCS enum attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes). Default "All". --- @param Core.Set#SET_ZONE EngageZoneSet Set of zones in which targets are engaged. Default is anywhere. --- @param Core.Set#SET_ZONE NoEngageZoneSet Set of zones in which targets are *not* engaged. Default is nowhere. --- @return #FLIGHTGROUP self -function FLIGHTGROUP:SetEngageDetectedOn(RangeMax, TargetTypes, EngageZoneSet, NoEngageZoneSet) - - -- Ensure table. - if TargetTypes then - if type(TargetTypes)~="table" then - TargetTypes={TargetTypes} - end - else - TargetTypes={"All"} - end - - -- Ensure SET_ZONE if ZONE is provided. - if EngageZoneSet and EngageZoneSet:IsInstanceOf("ZONE_BASE") then - local zoneset=SET_ZONE:New():AddZone(EngageZoneSet) - EngageZoneSet=zoneset - end - if NoEngageZoneSet and NoEngageZoneSet:IsInstanceOf("ZONE_BASE") then - local zoneset=SET_ZONE:New():AddZone(NoEngageZoneSet) - NoEngageZoneSet=zoneset - end - - -- Set parameters. - self.engagedetectedOn=true - self.engagedetectedRmax=UTILS.NMToMeters(RangeMax or 25) - self.engagedetectedTypes=TargetTypes - self.engagedetectedEngageZones=EngageZoneSet - self.engagedetectedNoEngageZones=NoEngageZoneSet - - -- Ensure detection is ON or it does not make any sense. - self:SetDetection(true) - - return self -end - ---- Disable to automatically engage detected targets. --- @param #FLIGHTGROUP self --- @return #OPSGROUP self -function FLIGHTGROUP:SetEngageDetectedOff() - self.engagedetectedOn=false - return self -end - --- Enable that the group is despawned after landing. This can be useful to avoid DCS taxi issues with other AI or players or jamming taxiways. -- @param #FLIGHTGROUP self @@ -613,18 +523,18 @@ function FLIGHTGROUP:IsTaxiing() return self:Is("Taxiing") end ---- Check if flight is airborne. +--- Check if flight is airborne or cruising. -- @param #FLIGHTGROUP self -- @return #boolean If true, flight is airborne. function FLIGHTGROUP:IsAirborne() - return self:Is("Airborne") + return self:Is("Airborne") or self:Is("Cruising") end ---- Check if flight is waiting after passing final waypoint. +--- Check if flight is airborne or cruising. -- @param #FLIGHTGROUP self --- @return #boolean If true, flight is waiting. -function FLIGHTGROUP:IsWaiting() - return self:Is("Waiting") +-- @return #boolean If true, flight is airborne. +function FLIGHTGROUP:IsCruising() + return self:Is("Cruising") end --- Check if flight is landing. @@ -697,6 +607,15 @@ function FLIGHTGROUP:IsFuelCritical() return self.fuelcritical end +--- Check if flight is good on fuel (not below low or even critical state). +-- @param #FLIGHTGROUP self +-- @return #boolean If true, flight is good on fuel. +function FLIGHTGROUP:IsFuelGood() + local isgood=not (self.fuellow or self.fuelcritical) + return isgood +end + + --- Check if flight can do air-to-ground tasks. -- @param #FLIGHTGROUP self -- @param #boolean ExcludeGuns If true, exclude gun @@ -736,11 +655,18 @@ function FLIGHTGROUP:StartUncontrolled(delay) self:ScheduleOnce(delay, FLIGHTGROUP.StartUncontrolled, self) else - if self:IsAlive() then - --TODO: check Alive==true and Alive==false ==> Activate first + local alive=self:IsAlive() + + if alive~=nil then + -- Check if group is already active. + local _delay=0 + if alive==false then + self:Activate() + _delay=1 + end self:T(self.lid.."Starting uncontrolled group") - self.group:StartUncontrolled(delay) - self.isUncontrolled=true + self.group:StartUncontrolled(_delay) + self.isUncontrolled=false else self:E(self.lid.."ERROR: Could not start uncontrolled group as it is NOT alive!") end @@ -776,7 +702,7 @@ function FLIGHTGROUP:GetFuelMin() local fuelmin=math.huge for i,_element in pairs(self.elements) do - local element=_element --#FLIGHTGROUP.Element + local element=_element --Ops.OpsGroup#OPSGROUP.Element local unit=element.unit @@ -810,46 +736,46 @@ function FLIGHTGROUP:onbeforeStatus(From, Event, To) -- First we check if elements are still alive. Could be that they were despawned without notice, e.g. when landing on a too small airbase. for i,_element in pairs(self.elements) do - local element=_element --#FLIGHTGROUP.Element - + local element=_element --Ops.OpsGroup#OPSGROUP.Element + -- Check that element is not already dead or not yet alive. if element.status~=OPSGROUP.ElementStatus.DEAD and element.status~=OPSGROUP.ElementStatus.INUTERO then - + -- Unit shortcut. local unit=element.unit - - local isdead=false + + local isdead=false if unit and unit:IsAlive() then - + -- Get life points. local life=unit:GetLife() or 0 - + -- Units with life <=1 are dead. if life<=1 then --env.info(string.format("FF unit %s: live<=1 in status at T=%.3f", unit:GetName(), timer.getTime())) isdead=true end - + else -- Not alive any more. --env.info(string.format("FF unit %s: NOT alive in status at T=%.3f", unit:GetName(), timer.getTime())) isdead=true end - + -- This one is dead. if isdead then - local text=string.format("Element %s is dead at t=%.3f! Maybe despawned without notice or landed at a too small airbase. Calling ElementDead in 60 sec to give other events a chance", - tostring(element.name), timer.getTime()) + local text=string.format("Element %s is dead at t=%.3f but has status %s! Maybe despawned without notice or landed at a too small airbase. Calling ElementDead in 60 sec to give other events a chance", + tostring(element.name), timer.getTime(), tostring(element.status)) self:E(self.lid..text) self:__ElementDead(60, element) end - - end + + end end - if self:IsDead() then + if self:IsDead() then self:T(self.lid..string.format("Onbefore Status DEAD ==> false")) - return false + return false elseif self:IsStopped() then self:T(self.lid..string.format("Onbefore Status STOPPED ==> false")) return false @@ -858,101 +784,124 @@ function FLIGHTGROUP:onbeforeStatus(From, Event, To) return true end ---- On after "Status" event. +--- Status update. -- @param #FLIGHTGROUP self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. -function FLIGHTGROUP:onafterStatus(From, Event, To) +function FLIGHTGROUP:Status() -- FSM state. local fsmstate=self:GetState() - -- Update position. - self:_UpdatePosition() + -- Is group alive? + local alive=self:IsAlive() + + if alive then - --- - -- Detection - --- - - -- Check if group has detected any units. - if self.detectionOn then + -- Update position. + self:_UpdatePosition() + + -- Check if group has detected any units. self:_CheckDetectedUnits() - end - - --- - -- Parking - --- - - -- Check if flight began to taxi (if it was parking). - if self:IsParking() then - for _,_element in pairs(self.elements) do - local element=_element --#FLIGHTGROUP.Element - if element.parking then - - -- Get distance to assigned parking spot. - local dist=element.unit:GetCoordinate():Get2DDistance(element.parking.Coordinate) - - -- If distance >10 meters, we consider the unit as taxiing. - -- TODO: Check distance threshold! If element is taxiing, the parking spot is free again. - -- When the next plane is spawned on this spot, collisions should be avoided! - if dist>10 then - if element.status==OPSGROUP.ElementStatus.ENGINEON then - self:ElementTaxiing(element) - end + + -- Check ammo status. + self:_CheckAmmoStatus() + + -- Check damage. + self:_CheckDamage() + + -- TODO: Check if group is waiting? + if self:IsWaiting() then + if self.Twaiting and self.dTwait then + if timer.getAbsTime()>self.Twaiting+self.dTwait then + --self.Twaiting=nil + --self.dTwait=nil + --self:Cruise() end - - else - --self:E(self.lid..string.format("Element %s is in PARKING queue but has no parking spot assigned!", element.name)) end end + + + -- TODO: _CheckParking() function + + -- Check if flight began to taxi (if it was parking). + if self:IsParking() then + for _,_element in pairs(self.elements) do + local element=_element --Ops.OpsGroup#OPSGROUP.Element + if element.parking then + + -- Get distance to assigned parking spot. + local dist=element.unit:GetCoordinate():Get2DDistance(element.parking.Coordinate) + + -- If distance >10 meters, we consider the unit as taxiing. + -- TODO: Check distance threshold! If element is taxiing, the parking spot is free again. + -- When the next plane is spawned on this spot, collisions should be avoided! + if dist>10 then + if element.status==OPSGROUP.ElementStatus.ENGINEON then + self:ElementTaxiing(element) + end + end + + else + --self:E(self.lid..string.format("Element %s is in PARKING queue but has no parking spot assigned!", element.name)) + end + end + end + end - + --- -- Group --- -- Short info. if self.verbose>=1 then - + + local nelem=self:CountElements() + local Nelem=#self.elements local nTaskTot, nTaskSched, nTaskWP=self:CountRemainingTasks() local nMissions=self:CountRemainingMissison() - - - local text=string.format("Status %s [%d/%d]: Tasks=%d (%d,%d) Curr=%d, Missions=%s, Waypoint=%d/%d, Detected=%d, Home=%s, Destination=%s", - fsmstate, #self.elements, #self.elements, nTaskTot, nTaskSched, nTaskWP, self.taskcurrent, nMissions, self.currentwp or 0, self.waypoints and #self.waypoints or 0, - self.detectedunits:Count(), self.homebase and self.homebase:GetName() or "unknown", self.destbase and self.destbase:GetName() or "unknown") - self:I(self.lid..text) + local currT=self.taskcurrent or "None" + local currM=self.currentmission or "None" + local currW=self.currentwp or 0 + local nWp=self.waypoints and #self.waypoints or 0 + local home=self.homebase and self.homebase:GetName() or "unknown" + local dest=self.destbase and self.destbase:GetName() or "unknown" + local fc=self.flightcontrol and self.flightcontrol.airbasename or "N/A" + local curr=self.currbase and self.currbase:GetName() or "N/A" + local ndetected=self.detectionOn and tostring(self.detectedunits:Count()) or "OFF" + + local text=string.format("Status %s [%d/%d]: T/M=%d/%d [Current %s/%s] [%s], Waypoint=%d/%d [%s], Base=%s [%s-->%s]", + fsmstate, nelem, Nelem, + nTaskTot, nMissions, currT, currM, tostring(self:HasTaskController()), + currW, nWp, tostring(self.passedfinalwp), curr, home, dest) + self:I(self.lid..text) + end --- -- Elements --- - + if self.verbose>=2 then local text="Elements:" for i,_element in pairs(self.elements) do - local element=_element --#FLIGHTGROUP.Element + local element=_element --Ops.OpsGroup#OPSGROUP.Element local name=element.name local status=element.status local unit=element.unit local fuel=unit:GetFuel() or 0 local life=unit:GetLifeRelative() or 0 + local lp=unit:GetLife() + local lp0=unit:GetLife0() local parking=element.parking and tostring(element.parking.TerminalID) or "X" - -- Check if element is not dead and we missed an event. - --if life<=0 and element.status~=OPSGROUP.ElementStatus.DEAD and element.status~=OPSGROUP.ElementStatus.INUTERO then - -- self:ElementDead(element) - --end - -- Get ammo. local ammo=self:GetAmmoElement(element) -- Output text for element. - text=text..string.format("\n[%d] %s: status=%s, fuel=%.1f, life=%.1f, guns=%d, rockets=%d, bombs=%d, missiles=%d (AA=%d, AG=%d, AS=%s), parking=%s", - i, name, status, fuel*100, life*100, ammo.Guns, ammo.Rockets, ammo.Bombs, ammo.Missiles, ammo.MissilesAA, ammo.MissilesAG, ammo.MissilesAS, parking) + text=text..string.format("\n[%d] %s: status=%s, fuel=%.1f, life=%.1f [%.1f/%.1f], guns=%d, rockets=%d, bombs=%d, missiles=%d (AA=%d, AG=%d, AS=%s), parking=%s", + i, name, status, fuel*100, life*100, lp, lp0, ammo.Guns, ammo.Rockets, ammo.Bombs, ammo.Missiles, ammo.MissilesAA, ammo.MissilesAG, ammo.MissilesAS, parking) end if #self.elements==0 then text=text.." none!" @@ -964,7 +913,9 @@ function FLIGHTGROUP:onafterStatus(From, Event, To) -- Distance travelled --- - if self.verbose>=4 and self:IsAlive() then + if self.verbose>=4 and alive then + + -- TODO: _Check distance travelled. -- Travelled distance since last check. local ds=self.travelds @@ -980,7 +931,7 @@ function FLIGHTGROUP:onafterStatus(From, Event, To) local TmaxFuel=math.huge for _,_element in pairs(self.elements) do - local element=_element --#FLIGHTGROUP.Element + local element=_element --Ops.OpsGroup#OPSGROUP.Element -- Get relative fuel of element. local fuel=element.unit:GetFuel() or 0 @@ -1007,24 +958,23 @@ function FLIGHTGROUP:onafterStatus(From, Event, To) -- Log outut. self:I(self.lid..string.format("Travelled ds=%.1f km dt=%.1f s ==> v=%.1f knots. Fuel left for %.1f min", self.traveldist/1000, dt, UTILS.MpsToKnots(v), TmaxFuel/60)) - + end - --- - -- Tasks & Missions - --- - - self:_PrintTaskAndMissionStatus() - --- -- Fuel State --- + -- TODO: _CheckFuelState() function. + -- Only if group is in air. - if self:IsAlive() and self.group:IsAirborne(true) then + if alive and self.group:IsAirborne(true) then local fuelmin=self:GetFuelMin() + -- Debug info. + self:T2(self.lid..string.format("Fuel state=%d", fuelmin)) + if fuelmin>=self.fuellowthresh then self.fuellow=false end @@ -1043,35 +993,13 @@ function FLIGHTGROUP:onafterStatus(From, Event, To) if fuelmin See also OPSGROUP ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Flightgroup event function, handling the birth of a unit. --- @param #FLIGHTGROUP self --- @param Core.Event#EVENTDATA EventData Event data. -function FLIGHTGROUP:OnEventBirth(EventData) - - --env.info(string.format("EVENT: Birth for unit %s", tostring(EventData.IniUnitName))) - - -- Check that this is the right group. - if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then - local unit=EventData.IniUnit - local group=EventData.IniGroup - local unitname=EventData.IniUnitName - - -- Set group. - self.group=self.group or EventData.IniGroup - - if self.respawning then - - local function reset() - self.respawning=nil - end - - -- Reset switch in 1 sec. This should allow all birth events of n>1 groups to have passed. - -- TODO: Can I do this more rigorously? - self:ScheduleOnce(1, reset) - - else - - -- Set homebase if not already set. - if EventData.Place then - self.homebase=self.homebase or EventData.Place - end - - if self.homebase and not self.destbase then - self.destbase=self.homebase - end - - -- Get element. - local element=self:GetElementByName(unitname) - - -- Create element spawned event if not already present. - if not self:_IsElement(unitname) then - element=self:AddElementByName(unitname) - end - - -- Set element to spawned state. - self:T(self.lid..string.format("EVENT: Element %s born at airbase %s==> spawned", element.name, self.homebase and self.homebase:GetName() or "unknown")) - -- This is delayed by a millisec because inAir check for units spawned in air failed (returned false even though the unit was spawned in air). - self:__ElementSpawned(0.0, element) - - end - - end - -end - --- Flightgroup event function handling the crash of a unit. -- @param #FLIGHTGROUP self -- @param Core.Event#EVENTDATA EventData Event data. @@ -1272,6 +1081,7 @@ end -- @param #FLIGHTGROUP self -- @param Core.Event#EVENTDATA EventData Event data. function FLIGHTGROUP:OnEventTakeOff(EventData) + self:T3(self.lid.."EVENT: TakeOff") -- Check that this is the right group. if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then @@ -1283,7 +1093,7 @@ function FLIGHTGROUP:OnEventTakeOff(EventData) local element=self:GetElementByName(unitname) if element then - self:T3(self.lid..string.format("EVENT: Element %s took off ==> airborne", element.name)) + self:T2(self.lid..string.format("EVENT: Element %s took off ==> airborne", element.name)) self:ElementTakeoff(element, EventData.Place) end @@ -1340,7 +1150,7 @@ function FLIGHTGROUP:OnEventEngineShutdown(EventData) if element.unit and element.unit:IsAlive() then local airbase=self:GetClosestAirbase() - local parking=self:GetParkingSpot(element, 10, airbase) + local parking=self:GetParkingSpot(element, 100, airbase) if airbase and parking then self:ElementArrived(element, airbase, parking) @@ -1375,7 +1185,7 @@ function FLIGHTGROUP:OnEventCrash(EventData) local element=self:GetElementByName(unitname) if element and element.status~=OPSGROUP.ElementStatus.DEAD then - self:T(self.lid..string.format("EVENT: Element %s crashed ==> destroyed", element.name)) + self:T(self.lid..string.format("EVENT: Element %s crashed ==> destroyed", element.name)) self:ElementDestroyed(element) end @@ -1391,7 +1201,7 @@ function FLIGHTGROUP:OnEventUnitLost(EventData) -- Check that this is the right group. if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then self:T2(self.lid..string.format("EVENT: Unit %s lost at t=%.3f", EventData.IniUnitName, timer.getTime())) - + local unit=EventData.IniUnit local group=EventData.IniGroup local unitname=EventData.IniUnitName @@ -1403,75 +1213,13 @@ function FLIGHTGROUP:OnEventUnitLost(EventData) self:T(self.lid..string.format("EVENT: Element %s unit lost ==> destroyed t=%.3f", element.name, timer.getTime())) self:ElementDestroyed(element) end - - end - -end - ---- Flightgroup event function handling the crash of a unit. --- @param #FLIGHTGROUP self --- @param Core.Event#EVENTDATA EventData Event data. -function FLIGHTGROUP:OnEventKill(EventData) - - -- Check that this is the right group. - if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then - - -- Target name - local targetname=tostring(EventData.TgtUnitName) - - -- Debug info. - self:T2(self.lid..string.format("EVENT: Unit %s killed object %s!", tostring(EventData.IniUnitName), targetname)) - - -- Check if this was a UNIT or STATIC object. - local target=UNIT:FindByName(targetname) - if not target then - target=STATIC:FindByName(targetname, false) - end - - -- Only count UNITS and STATICs (not SCENERY) - if target then - - -- Debug info. - self:T(self.lid..string.format("EVENT: Unit %s killed unit/static %s!", tostring(EventData.IniUnitName), targetname)) - - -- Kill counter. - self.Nkills=self.Nkills+1 - - -- Check if on a mission. - local mission=self:GetMissionCurrent() - if mission then - mission.Nkills=mission.Nkills+1 -- Increase mission kill counter. - end - - end - - end - -end - ---- Flightgroup event function handling the crash of a unit. --- @param #FLIGHTGROUP self --- @param Core.Event#EVENTDATA EventData Event data. -function FLIGHTGROUP:OnEventRemoveUnit(EventData) - - -- Check that this is the right group. - if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then - local unit=EventData.IniUnit - local group=EventData.IniGroup - local unitname=EventData.IniUnitName - - -- Get element. - local element=self:GetElementByName(unitname) - - if element then - self:T3(self.lid..string.format("EVENT: Element %s removed ==> dead", element.name)) - self:ElementDead(element) - end end end + + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- FSM functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1481,8 +1229,10 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. function FLIGHTGROUP:onafterElementSpawned(From, Event, To, Element) + + -- Debug info. self:T(self.lid..string.format("Element spawned %s", Element.name)) -- Set element status. @@ -1507,6 +1257,7 @@ function FLIGHTGROUP:onafterElementSpawned(From, Event, To, Element) self:__ElementParking(0.11, Element) end end + end --- On after "ElementParking" event. @@ -1514,18 +1265,21 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. -- @param Wrapper.Airbase#AIRBASE.ParkingSpot Spot Parking Spot. function FLIGHTGROUP:onafterElementParking(From, Event, To, Element, Spot) + + -- Set parking spot. + if Spot then + self:_SetElementParkingAt(Element, Spot) + end + + -- Debug info. self:T(self.lid..string.format("Element parking %s at spot %s", Element.name, Element.parking and tostring(Element.parking.TerminalID) or "N/A")) -- Set element status. self:_UpdateStatus(Element, OPSGROUP.ElementStatus.PARKING) - if Spot then - self:_SetElementParkingAt(Element, Spot) - end - if self:IsTakeoffCold() then -- Wait for engine startup event. elseif self:IsTakeoffHot() then @@ -1533,6 +1287,7 @@ function FLIGHTGROUP:onafterElementParking(From, Event, To, Element, Spot) elseif self:IsTakeoffRunway() then self:__ElementEngineOn(0.5, Element) end + end --- On after "ElementEngineOn" event. @@ -1540,7 +1295,7 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. function FLIGHTGROUP:onafterElementEngineOn(From, Event, To, Element) -- Debug info. @@ -1548,6 +1303,7 @@ function FLIGHTGROUP:onafterElementEngineOn(From, Event, To, Element) -- Set element status. self:_UpdateStatus(Element, OPSGROUP.ElementStatus.ENGINEON) + end --- On after "ElementTaxiing" event. @@ -1555,7 +1311,7 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. function FLIGHTGROUP:onafterElementTaxiing(From, Event, To, Element) -- Get terminal ID. @@ -1569,6 +1325,7 @@ function FLIGHTGROUP:onafterElementTaxiing(From, Event, To, Element) -- Set element status. self:_UpdateStatus(Element, OPSGROUP.ElementStatus.TAXIING) + end --- On after "ElementTakeoff" event. @@ -1576,7 +1333,7 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. -- @param Wrapper.Airbase#AIRBASE airbase The airbase if applicable or nil. function FLIGHTGROUP:onafterElementTakeoff(From, Event, To, Element, airbase) self:T(self.lid..string.format("Element takeoff %s at %s airbase.", Element.name, airbase and airbase:GetName() or "unknown")) @@ -1590,7 +1347,8 @@ function FLIGHTGROUP:onafterElementTakeoff(From, Event, To, Element, airbase) self:_UpdateStatus(Element, OPSGROUP.ElementStatus.TAKEOFF, airbase) -- Trigger element airborne event. - self:__ElementAirborne(2, Element) + self:__ElementAirborne(0.01, Element) + end --- On after "ElementAirborne" event. @@ -1598,12 +1356,15 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. function FLIGHTGROUP:onafterElementAirborne(From, Event, To, Element) + + -- Debug info. self:T2(self.lid..string.format("Element airborne %s", Element.name)) -- Set element status. self:_UpdateStatus(Element, OPSGROUP.ElementStatus.AIRBORNE) + end --- On after "ElementLanded" event. @@ -1611,31 +1372,37 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. -- @param Wrapper.Airbase#AIRBASE airbase The airbase if applicable or nil. function FLIGHTGROUP:onafterElementLanded(From, Event, To, Element, airbase) + + -- Debug info. self:T2(self.lid..string.format("Element landed %s at %s airbase", Element.name, airbase and airbase:GetName() or "unknown")) - + if self.despawnAfterLanding then - + -- Despawn the element. self:DespawnElement(Element) - + else - -- Helos with skids land directly on parking spots. - if self.ishelo then - - local Spot=self:GetParkingSpot(Element, 10, airbase) - - self:_SetElementParkingAt(Element, Spot) - - end - -- Set element status. self:_UpdateStatus(Element, OPSGROUP.ElementStatus.LANDED, airbase) - + + -- Helos with skids land directly on parking spots. + if self.isHelo then + + local Spot=self:GetParkingSpot(Element, 10, airbase) + + if Spot then + self:_SetElementParkingAt(Element, Spot) + self:_UpdateStatus(Element, OPSGROUP.ElementStatus.ARRIVED) + end + + end + end + end --- On after "ElementArrived" event. @@ -1643,12 +1410,13 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. -- @param Wrapper.Airbase#AIRBASE airbase The airbase, where the element arrived. -- @param Wrapper.Airbase#AIRBASE.ParkingSpot Parking The Parking spot the element has. function FLIGHTGROUP:onafterElementArrived(From, Event, To, Element, airbase, Parking) self:T(self.lid..string.format("Element arrived %s at %s airbase using parking spot %d", Element.name, airbase and airbase:GetName() or "unknown", Parking and Parking.TerminalID or -99)) + -- Set element parking. self:_SetElementParkingAt(Element, Parking) -- Set element status. @@ -1660,12 +1428,12 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. function FLIGHTGROUP:onafterElementDestroyed(From, Event, To, Element) -- Call OPSGROUP function. self:GetParent(self).onafterElementDestroyed(self, From, Event, To, Element) - + end --- On after "ElementDead" event. @@ -1673,7 +1441,7 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. function FLIGHTGROUP:onafterElementDead(From, Event, To, Element) -- Call OPSGROUP function. @@ -1685,21 +1453,54 @@ function FLIGHTGROUP:onafterElementDead(From, Event, To, Element) -- Not parking any more. Element.parking=nil - + end ---- On after "Spawned" event. Sets the template, initializes the waypoints. +--- On after "Spawned" event. -- @param #FLIGHTGROUP self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. function FLIGHTGROUP:onafterSpawned(From, Event, To) self:T(self.lid..string.format("Flight spawned")) + + -- Debug info. + if self.verbose>=1 then + local text=string.format("Initialized Flight Group %s:\n", self.groupname) + text=text..string.format("Unit type = %s\n", self.actype) + text=text..string.format("Speed max = %.1f Knots\n", UTILS.KmphToKnots(self.speedMax)) + text=text..string.format("Range max = %.1f km\n", self.rangemax/1000) + text=text..string.format("Ceiling = %.1f feet\n", UTILS.MetersToFeet(self.ceiling)) + text=text..string.format("Weight = %.1f kg\n", self:GetWeightTotal()) + text=text..string.format("Cargo bay = %.1f kg\n", self:GetFreeCargobay()) + text=text..string.format("Tanker type = %s\n", tostring(self.tankertype)) + text=text..string.format("Refuel type = %s\n", tostring(self.refueltype)) + text=text..string.format("AI = %s\n", tostring(self.isAI)) + text=text..string.format("Has EPLRS = %s\n", tostring(self.isEPLRS)) + text=text..string.format("Helicopter = %s\n", tostring(self.isHelo)) + text=text..string.format("Elements = %d\n", #self.elements) + text=text..string.format("Waypoints = %d\n", #self.waypoints) + text=text..string.format("Radio = %.1f MHz %s %s\n", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu), tostring(self.radio.On)) + text=text..string.format("Ammo = %d (G=%d/R=%d/B=%d/M=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Rockets, self.ammo.Bombs, self.ammo.Missiles) + text=text..string.format("FSM state = %s\n", self:GetState()) + text=text..string.format("Is alive = %s\n", tostring(self.group:IsAlive())) + text=text..string.format("LateActivate = %s\n", tostring(self:IsLateActivated())) + text=text..string.format("Uncontrolled = %s\n", tostring(self:IsUncontrolled())) + text=text..string.format("Start Air = %s\n", tostring(self:IsTakeoffAir())) + text=text..string.format("Start Cold = %s\n", tostring(self:IsTakeoffCold())) + text=text..string.format("Start Hot = %s\n", tostring(self:IsTakeoffHot())) + text=text..string.format("Start Rwy = %s\n", tostring(self:IsTakeoffRunway())) + self:I(self.lid..text) + end - -- Update position. + -- Update position. self:_UpdatePosition() + -- Not dead or destroyed yet. + self.isDead=false + self.isDestroyed=false + if self.isAI then -- Set ROE. @@ -1707,37 +1508,42 @@ function FLIGHTGROUP:onafterSpawned(From, Event, To) -- Set ROT. self:SwitchROT(self.option.ROT) - + + -- Set default EPLRS. + self:SwitchEPLRS(self.option.EPLRS) + -- Set Formation self:SwitchFormation(self.option.Formation) - + -- Set TACAN beacon. self:_SwitchTACAN() - + -- Set radio freq and modu. if self.radioDefault then self:SwitchRadio() else self:SetDefaultRadio(self.radio.Freq, self.radio.Modu, self.radio.On) end - + -- Set callsign. if self.callsignDefault then self:SwitchCallsign(self.callsignDefault.NumberSquad, self.callsignDefault.NumberGroup) else self:SetDefaultCallsign(self.callsign.NumberSquad, self.callsign.NumberGroup) end - + -- TODO: make this input. self:GetGroup():SetOption(AI.Option.Air.id.PROHIBIT_JETT, true) self:GetGroup():SetOption(AI.Option.Air.id.PROHIBIT_AB, true) -- Does not seem to work. AI still used the after burner. self:GetGroup():SetOption(AI.Option.Air.id.RTB_ON_BINGO, false) - --self.group:SetOption(AI.Option.Air.id.RADAR_USING, AI.Option.Air.val.RADAR_USING.FOR_CONTINUOUS_SEARCH) + --self.group:SetOption(AI.Option.Air.id.RADAR_USING, AI.Option.Air.val.RADAR_USING.FOR_CONTINUOUS_SEARCH) -- Update route. self:__UpdateRoute(-0.5) else + + env.info("FF Spawned update menu") -- F10 other menu. self:_UpdateMenu() @@ -1752,11 +1558,16 @@ end -- @param #string Event Event. -- @param #string To To state. function FLIGHTGROUP:onafterParking(From, Event, To) - self:T(self.lid..string.format("Flight is parking")) - - local airbase=self:GetClosestAirbase() --self.group:GetCoordinate():GetClosestAirbase() + -- Get closest airbase + local airbase=self:GetClosestAirbase() local airbasename=airbase:GetName() or "unknown" + + -- Debug info + self:T(self.lid..string.format("Flight is parking at airbase %s", airbasename)) + + -- Set current airbase. + self.currbase=airbase -- Parking time stamp. self.Tparking=timer.getAbsTime() @@ -1795,7 +1606,7 @@ function FLIGHTGROUP:onafterTaxiing(From, Event, To) self.Tparking=nil -- TODO: need a better check for the airbase. - local airbase=self:GetClosestAirbase() --self.group:GetCoordinate():GetClosestAirbase(nil, self.group:GetCoalition()) + local airbase=self:GetClosestAirbase() if self.flightcontrol and airbase and self.flightcontrol.airbasename==airbase:GetName() then @@ -1806,6 +1617,7 @@ function FLIGHTGROUP:onafterTaxiing(From, Event, To) else -- Human flights go to TAXI OUT queue. They will go to the ready for takeoff queue when they request it. self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.TAXIOUT) + -- Update menu. self:_UpdateMenu() end @@ -1839,11 +1651,58 @@ end function FLIGHTGROUP:onafterAirborne(From, Event, To) self:T(self.lid..string.format("Flight airborne")) + -- No current airbase any more. + self.currbase=nil + + -- Cruising. + self:__Cruise(-0.01) + +end + +--- On after "Cruising" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterCruise(From, Event, To) + self:T(self.lid..string.format("Flight cruising")) + + -- Not waiting anymore. + self.Twaiting=nil + self.dTwait=nil + if self.isAI then - self:_CheckGroupDone(1) + + --- + -- AI + --- + + --[[ + if self:IsTransporting() then + if self.cargoTransport and self.cargoTZC and self.cargoTZC.DeployAirbase then + self:LandAtAirbase(self.cargoTZC.DeployAirbase) + end + elseif self:IsPickingup() then + if self.cargoTransport and self.cargoTZC and self.cargoTZC.PickupAirbase then + self:LandAtAirbase(self.cargoTZC.PickupAirbase) + end + else + self:_CheckGroupDone(nil, 120) + end + ]] + + self:_CheckGroupDone(nil, 120) + else - self:_UpdateMenu() + + --- + -- CLIENT + --- + + self:_UpdateMenu(0.1) + end + end --- On after "Landing" event. @@ -1872,7 +1731,7 @@ function FLIGHTGROUP:onafterLanded(From, Event, To, airbase) -- Add flight to taxiinb queue. self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.TAXIINB) end - + end --- On after "LandedAt" event. @@ -1881,9 +1740,16 @@ end -- @param #string Event Event. -- @param #string To To state. function FLIGHTGROUP:onafterLandedAt(From, Event, To) - self:T(self.lid..string.format("Flight landed at")) -end + self:T(self.lid..string.format("Flight landed at")) + -- Trigger (un-)loading process. + if self:IsPickingup() then + self:__Loading(-1) + elseif self:IsTransporting() then + self:__Unloading(-1) + end + +end --- On after "Arrived" event. -- @param #FLIGHTGROUP self @@ -1898,9 +1764,97 @@ function FLIGHTGROUP:onafterArrived(From, Event, To) -- Add flight to arrived queue. self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.ARRIVED) end + + --TODO: Check that current base is airwing base. + local airwing=self:GetAirWing() --airwing:GetAirbaseName()==self.currbase:GetName() - -- Despawn in 5 min. - if not self.airwing then + -- Check what to do. + if airwing and not (self:IsPickingup() or self:IsTransporting()) then + + -- Debug info. + self:T(self.lid..string.format("Airwing asset group %s arrived ==> Adding asset back to stock of airwing %s", self.groupname, airwing.alias)) + + -- Add the asset back to the airwing. + airwing:AddAsset(self.group, 1) + + elseif self.isLandingAtAirbase then + + local Template=UTILS.DeepCopy(self.template) --DCS#Template + + -- No late activation. + self.isLateActivated=false + Template.lateActivation=self.isLateActivated + + -- Spawn in uncontrolled state. + self.isUncontrolled=true + Template.uncontrolled=self.isUncontrolled + + -- First waypoint of the group. + local SpawnPoint=Template.route.points[1] + + -- These are only for ships and FARPS. + SpawnPoint.linkUnit = nil + SpawnPoint.helipadId = nil + SpawnPoint.airdromeId = nil + + -- Airbase. + local airbase=self.isLandingAtAirbase --Wrapper.Airbase#AIRBASE + + -- Get airbase ID and category. + local AirbaseID = airbase:GetID() + + -- Set airdromeId. + if airbase:IsShip() then + SpawnPoint.linkUnit = AirbaseID + SpawnPoint.helipadId = AirbaseID + elseif airbase:IsHelipad() then + SpawnPoint.linkUnit = AirbaseID + SpawnPoint.helipadId = AirbaseID + elseif airbase:IsAirdrome() then + SpawnPoint.airdromeId = AirbaseID + end + + -- Set waypoint type/action. + SpawnPoint.alt = 0 + SpawnPoint.type = COORDINATE.WaypointType.TakeOffParking + SpawnPoint.action = COORDINATE.WaypointAction.FromParkingArea + + local units=Template.units + + for i=#units,1,-1 do + local unit=units[i] + local element=self:GetElementByName(unit.name) + if element and element.status~=OPSGROUP.ElementStatus.DEAD then + unit.parking=element.parking and element.parking.TerminalID or nil + unit.parking_id=nil + local vec3=element.unit:GetVec3() + local heading=element.unit:GetHeading() + unit.x=vec3.x + unit.y=vec3.z + unit.alt=vec3.y + unit.heading=math.rad(heading) + unit.psi=-unit.heading + else + table.remove(units, i) + end + end + + -- Respawn with this template. + self:_Respawn(0, Template) + + -- Reset. + self.isLandingAtAirbase=nil + + -- Init (un-)loading process. + if self:IsPickingup() then + self:__Loading(-1) + elseif self:IsTransporting() then + self:__Unloading(-1) + end + + else + -- Depawn after 5 min. Important to trigger dead events before DCS despawns on its own without any notification. + self:T(self.lid..string.format("Despawning group in 5 minutes after arrival!")) self:Despawn(5*60) end end @@ -1917,22 +1871,10 @@ function FLIGHTGROUP:onafterDead(From, Event, To) self.flightcontrol:_RemoveFlight(self) self.flightcontrol=nil end - - if self.Ndestroyed==#self.elements then - if self.squadron then - -- All elements were destroyed ==> Asset group is gone. - self.squadron:DelGroup(self.groupname) - end - else - if self.airwing then - -- Not all assets were destroyed (despawn) ==> Add asset back to airwing. - self.airwing:AddAsset(self.group, 1) - end - end -- Call OPSGROUP function. self:GetParent(self).onafterDead(self, From, Event, To) - + end @@ -1941,61 +1883,87 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #number n Waypoint number. +-- @param #number n Next waypoint index. Default is the one coming after that one that has been passed last. +-- @param #number N Waypoint Max waypoint index to be included in the route. Default is the final waypoint. -- @return #boolean Transision allowed? -function FLIGHTGROUP:onbeforeUpdateRoute(From, Event, To, n) +function FLIGHTGROUP:onbeforeUpdateRoute(From, Event, To, n, N) -- Is transition allowed? We assume yes until proven otherwise. local allowed=true local trepeat=nil - if self:IsAlive() then -- and (self:IsAirborne() or self:IsWaiting() or self:IsInbound() or self:IsHolding()) then + if self:IsAlive() then -- Alive & Airborne ==> Update route possible. self:T3(self.lid.."Update route possible. Group is ALIVE") elseif self:IsDead() then -- Group is dead! No more updates. self:E(self.lid.."Update route denied. Group is DEAD!") allowed=false + elseif self:IsInUtero() then + self:E(self.lid.."Update route denied. Group is INUTERO!") + allowed=false else -- Not airborne yet. Try again in 5 sec. self:T(self.lid.."Update route denied ==> checking back in 5 sec") trepeat=-5 allowed=false end + + -- Check if group is uncontrolled. If so, the mission task cannot be set yet! + if allowed and self:IsUncontrolled() then + self:T(self.lid.."Update route denied. Group is UNCONTROLLED!") + local mission=self:GetMissionCurrent() + if mission and mission.type==AUFTRAG.Type.ALERT5 then + trepeat=nil --Alert 5 is just waiting for the real mission. No need to try to update the route. + else + trepeat=-5 + end + allowed=false + end + -- Requested waypoint index <1. Something is seriously wrong here! if n and n<1 then self:E(self.lid.."Update route denied because waypoint n<1!") allowed=false end + -- No current waypoint. Something is serously wrong! if not self.currentwp then self:E(self.lid.."Update route denied because self.currentwp=nil!") allowed=false end - local N=n or self.currentwp+1 - if not N or N<1 then + local Nn=n or self.currentwp+1 + if not Nn or Nn<1 then self:E(self.lid.."Update route denied because N=nil or N<1") trepeat=-5 allowed=false end + -- Check for a current task. if self.taskcurrent>0 then - - --local task=self:GetTaskCurrent() + + -- Get the current task. Must not be executing already. local task=self:GetTaskByID(self.taskcurrent) - + if task then if task.dcstask.id=="PatrolZone" then - -- For patrol zone, we need to allow the update. + -- For patrol zone, we need to allow the update as we insert new waypoints. + self:T2(self.lid.."Allowing update route for Task: PatrolZone") + elseif task.dcstask.id=="ReconMission" then + -- For recon missions, we need to allow the update as we insert new waypoints. + self:T2(self.lid.."Allowing update route for Task: ReconMission") + elseif task.description and task.description=="Task_Land_At" then + -- We allow this + self:T2(self.lid.."Allowing update route for Task: Task_Land_At") else local taskname=task and task.description or "No description" self:E(self.lid..string.format("WARNING: Update route denied because taskcurrent=%d>0! Task description = %s", self.taskcurrent, tostring(taskname))) allowed=false end else - -- Now this can happen, if we directly use TaskExecute as the task is not in the task queue and cannot be removed. - self:T(self.lid..string.format("WARNING: before update route taskcurrent=%d>0 but no task?!", self.taskcurrent)) + -- Now this can happen, if we directly use TaskExecute as the task is not in the task queue and cannot be removed. Therefore, also directly executed tasks should be added to the queue! + self:T(self.lid..string.format("WARNING: before update route taskcurrent=%d (>0!) but no task?!", self.taskcurrent)) -- Anyhow, a task is running so we do not allow to update the route! allowed=false end @@ -2011,8 +1979,9 @@ function FLIGHTGROUP:onbeforeUpdateRoute(From, Event, To, n) end -- Debug info. - self:T2(self.lid..string.format("Onbefore Updateroute allowed=%s state=%s repeat in %s", tostring(allowed), self:GetState(), tostring(trepeat))) + self:T2(self.lid..string.format("Onbefore Updateroute in state %s: allowed=%s (repeat in %s)", self:GetState(), tostring(allowed), tostring(trepeat))) + -- Try again? if trepeat then self:__UpdateRoute(trepeat, n) end @@ -2025,14 +1994,16 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #number n Waypoint number. Default is next waypoint. -function FLIGHTGROUP:onafterUpdateRoute(From, Event, To, n) +-- @param #number n Next waypoint index. Default is the one coming after that one that has been passed last. +-- @param #number N Waypoint Max waypoint index to be included in the route. Default is the final waypoint. +function FLIGHTGROUP:onafterUpdateRoute(From, Event, To, n, N) -- Update route from this waypoint number onwards. n=n or self.currentwp+1 - -- Update waypoint tasks, i.e. inject WP tasks into waypoint table. - self:_UpdateWaypointTasks(n) + -- Max index. + N=N or #self.waypoints + N=math.min(N, #self.waypoints) -- Waypoints. local wp={} @@ -2040,22 +2011,29 @@ function FLIGHTGROUP:onafterUpdateRoute(From, Event, To, n) -- Current velocity. local speed=self.group and self.group:GetVelocityKMH() or 100 + -- Waypoint type. + local waypointType=COORDINATE.WaypointType.TurningPoint + local waypointAction=COORDINATE.WaypointAction.TurningPoint + if self:IsLanded() or self:IsLandedAt() or self:IsAirborne()==false then + -- Had some issues with passing waypoint function of the next WP called too ealy when the type is TurningPoint. Setting it to TakeOff solved it! + waypointType=COORDINATE.WaypointType.TakeOff + --waypointType=COORDINATE.WaypointType.TakeOffGroundHot + --waypointAction=COORDINATE.WaypointAction.FromGroundAreaHot + end + -- Set current waypoint or we get problem that the _PassingWaypoint function is triggered too early, i.e. right now and not when passing the next WP. - local current=self.group:GetCoordinate():WaypointAir(COORDINATE.WaypointAltType.BARO, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, speed, true, nil, {}, "Current") + local current=self:GetCoordinate():WaypointAir(COORDINATE.WaypointAltType.BARO, waypointType, waypointAction, speed, true, nil, {}, "Current") table.insert(wp, current) - - local Nwp=self.waypoints and #self.waypoints or 0 -- Add remaining waypoints to route. - for i=n, Nwp do + for i=n, N do table.insert(wp, self.waypoints[i]) end -- Debug info. local hb=self.homebase and self.homebase:GetName() or "unknown" local db=self.destbase and self.destbase:GetName() or "unknown" - self:T(self.lid..string.format("Updating route for WP #%d-%d homebase=%s destination=%s", n, #wp, hb, db)) - + self:T(self.lid..string.format("Updating route for WP #%d-%d [%s], homebase=%s destination=%s", n, #wp, self:GetState(), hb, db)) if #wp>1 then @@ -2067,7 +2045,7 @@ function FLIGHTGROUP:onafterUpdateRoute(From, Event, To, n) --- -- No waypoints left --- - + if self:IsAirborne() then self:T(self.lid.."No waypoints left ==> CheckGroupDone") self:_CheckGroupDone() @@ -2077,26 +2055,6 @@ function FLIGHTGROUP:onafterUpdateRoute(From, Event, To, n) end ---- On after "Respawn" event. --- @param #FLIGHTGROUP self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. --- @param #table Template The template used to respawn the group. -function FLIGHTGROUP:onafterRespawn(From, Event, To, Template) - - self:T(self.lid.."Respawning group!") - - local template=UTILS.DeepCopy(Template or self.template) - - if self.group and self.group:InAir() then - template.lateActivation=false - self.respawning=true - self.group=self.group:Respawn(template) - end - -end - --- On after "OutOfMissilesAA" event. -- @param #FLIGHTGROUP self -- @param #string From From state. @@ -2107,7 +2065,7 @@ function FLIGHTGROUP:onafterOutOfMissilesAA(From, Event, To) if self.outofAAMrtb then -- Back to destination or home. local airbase=self.destbase or self.homebase - self:__RTB(-5,airbase) + self:__RTB(-5, airbase) end end @@ -2121,7 +2079,7 @@ function FLIGHTGROUP:onafterOutOfMissilesAG(From, Event, To) if self.outofAGMrtb then -- Back to destination or home. local airbase=self.destbase or self.homebase - self:__RTB(-5,airbase) + self:__RTB(-5, airbase) end end @@ -2135,51 +2093,95 @@ end -- -- @param #FLIGHTGROUP self -- @param #number delay Delay in seconds. -function FLIGHTGROUP:_CheckGroupDone(delay) +-- @param #number waittime Time to wait if group is done. +function FLIGHTGROUP:_CheckGroupDone(delay, waittime) + + -- FSM state. + local fsmstate=self:GetState() if self:IsAlive() and self.isAI then if delay and delay>0 then + -- Debug info. + self:T(self.lid..string.format("Check FLIGHTGROUP [state=%s] done in %.3f seconds... (t=%.4f)", fsmstate, delay, timer.getTime())) + -- Delayed call. self:ScheduleOnce(delay, FLIGHTGROUP._CheckGroupDone, self) else + -- Debug info. + self:T(self.lid..string.format("Check FLIGHTGROUP [state=%s] done? (t=%.4f)", fsmstate, timer.getTime())) + -- First check if there is a paused mission that if self.missionpaused then + self:T(self.lid..string.format("Found paused mission %s [%s]. Unpausing mission...", self.missionpaused.name, self.missionpaused.type)) self:UnpauseMission() return end - + -- Group is currently engaging. if self:IsEngaging() then + self:T(self.lid.."Engaging! Group NOT done...") return end + -- Group is ordered to land at an airbase. + if self.isLandingAtAirbase then + self:T(self.lid..string.format("Landing at airbase %s! Group NOT done...", self.isLandingAtAirbase:GetName())) + return + end + + -- Group is waiting. + if self:IsWaiting() then + self:T(self.lid.."Waiting! Group NOT done...") + return + end + -- Number of tasks remaining. local nTasks=self:CountRemainingTasks() -- Number of mission remaining. local nMissions=self:CountRemainingMissison() + -- Number of cargo transports remaining. + local nTransports=self:CountRemainingTransports() + + -- Debug info. + self:T(self.lid..string.format("Remaining (final=%s): missions=%d, tasks=%d, transports=%d", tostring(self.passedfinalwp), nMissions, nTasks, nTransports)) + -- Final waypoint passed? - if self.passedfinalwp then + -- Or next waypoint index is the first waypoint. Could be that the group was on a mission and the mission waypoints were deleted. then the final waypoint is FALSE but no real waypoint left. + -- Since we do not do ad infinitum, this leads to a rapid oscillation between UpdateRoute and CheckGroupDone! + if self:HasPassedFinalWaypoint() or self:GetWaypointIndexNext()==1 then + + --- + -- Final Waypoint PASSED + --- -- Got current mission or task? - if self.currentmission==nil and self.taskcurrent==0 then + if self.currentmission==nil and self.taskcurrent==0 and (self.cargoTransport==nil or self.cargoTransport:GetCarrierTransportStatus(self)==OPSTRANSPORT.Status.DELIVERED) then -- Number of remaining tasks/missions? - if nTasks==0 and nMissions==0 then - + if nTasks==0 and nMissions==0 and nTransports==0 then + local destbase=self.destbase or self.homebase local destzone=self.destzone or self.homezone -- Send flight to destination. - if destbase then - self:T(self.lid.."Passed Final WP and No current and/or future missions/task ==> RTB!") - self:__RTB(-3, destbase) + if waittime then + self:T(self.lid..string.format("Passed Final WP and No current and/or future missions/tasks/transports. Waittime given ==> Waiting for %d sec!", waittime)) + self:Wait(waittime) + elseif destbase then + if self.currbase and self.currbase.AirbaseName==destbase.AirbaseName and self:IsParking() then + self:T(self.lid.."Passed Final WP and No current and/or future missions/tasks/transports AND parking at destination airbase ==> Arrived!") + self:Arrived() + else + self:T(self.lid.."Passed Final WP and No current and/or future missions/tasks/transports ==> RTB!") + self:__RTB(-0.1, destbase) + end elseif destzone then - self:T(self.lid.."Passed Final WP and No current and/or future missions/task ==> RTZ!") - self:__RTZ(-3, destzone) + self:T(self.lid.."Passed Final WP and No current and/or future missions/tasks/transports ==> RTZ!") + self:__RTZ(-0.1, destzone) else self:T(self.lid.."Passed Final WP and NO Tasks/Missions left. No DestBase or DestZone ==> Wait!") self:__Wait(-1) @@ -2193,8 +2195,17 @@ function FLIGHTGROUP:_CheckGroupDone(delay) self:T(self.lid..string.format("Passed Final WP but still have current Task (#%s) or Mission (#%s) left to do", tostring(self.taskcurrent), tostring(self.currentmission))) end else - self:T(self.lid..string.format("Flight (status=%s) did NOT pass the final waypoint yet ==> update route", self:GetState())) - self:__UpdateRoute(-1) + + --- + -- Final Waypoint NOT PASSED + --- + + -- Debug info. + self:T(self.lid..string.format("Flight (status=%s) did NOT pass the final waypoint yet ==> update route in -0.01 sec", self:GetState())) + + -- Update route. + self:__UpdateRoute(-0.01) + end end @@ -2224,45 +2235,66 @@ function FLIGHTGROUP:onbeforeRTB(From, Event, To, airbase, SpeedTo, SpeedHold) -- Check that coaliton is okay. We allow same (blue=blue, red=red) or landing on neutral bases. if airbase and airbase:GetCoalition()~=self.group:GetCoalition() and airbase:GetCoalition()>0 then - self:E(self.lid..string.format("ERROR: Wrong airbase coalition %d in RTB() call! We allow only same as group %d or neutral airbases 0.", airbase:GetCoalition(), self.group:GetCoalition())) - allowed=false + self:E(self.lid..string.format("ERROR: Wrong airbase coalition %d in RTB() call! We allow only same as group %d or neutral airbases 0", airbase:GetCoalition(), self.group:GetCoalition())) + return false + end + + if self.currbase and self.currbase:GetName()==airbase:GetName() then + self:E(self.lid.."WARNING: Currbase is already same as RTB airbase. RTB canceled!") + return false + end + + -- Check if the group has landed at an airbase. If so, we lost control and RTBing is not possible (only after a respawn). + if self:IsLanded() then + self:E(self.lid.."WARNING: Flight has already landed. RTB canceled!") + return false end if not self.group:IsAirborne(true) then -- this should really not happen, either the AUFTRAG is cancelled before the group was airborne or it is stuck at the ground for some reason - self:I(self.lid..string.format("WARNING: Group is not AIRBORNE ==> RTB event is suspended for 20 sec.")) + self:T(self.lid..string.format("WARNING: Group [%s] is not AIRBORNE ==> RTB event is suspended for 20 sec", self:GetState())) allowed=false Tsuspend=-20 local groupspeed = self.group:GetVelocityMPS() - if groupspeed <= 1 then self.RTBRecallCount = self.RTBRecallCount+1 end - if self.RTBRecallCount > 6 then + if groupspeed<=1 and not self:IsParking() then + self.RTBRecallCount = self.RTBRecallCount+1 + end + if self.RTBRecallCount>6 then + self:I(self.lid..string.format("WARNING: Group [%s] is not moving and was called RTB %d times. Assuming a problem and despawning!", self:GetState(), self.RTBRecallCount)) + self.RTBRecallCount=0 self:Despawn(5) + return end end - + -- Only if fuel is not low or critical. - if not (self:IsFuelLow() or self:IsFuelCritical()) then + if self:IsFuelGood() then -- Check if there are remaining tasks. local Ntot,Nsched, Nwp=self:CountRemainingTasks() if self.taskcurrent>0 then - self:I(self.lid..string.format("WARNING: Got current task ==> RTB event is suspended for 10 sec.")) + self:I(self.lid..string.format("WARNING: Got current task ==> RTB event is suspended for 10 sec")) Tsuspend=-10 allowed=false end if Nsched>0 then - self:I(self.lid..string.format("WARNING: Still got %d SCHEDULED tasks in the queue ==> RTB event is suspended for 10 sec.", Nsched)) + self:I(self.lid..string.format("WARNING: Still got %d SCHEDULED tasks in the queue ==> RTB event is suspended for 10 sec", Nsched)) Tsuspend=-10 allowed=false end if Nwp>0 then - self:I(self.lid..string.format("WARNING: Still got %d WAYPOINT tasks in the queue ==> RTB event is suspended for 10 sec.", Nwp)) + self:I(self.lid..string.format("WARNING: Still got %d WAYPOINT tasks in the queue ==> RTB event is suspended for 10 sec", Nwp)) Tsuspend=-10 allowed=false end + + if self.Twaiting and self.dTwait then + self:I(self.lid..string.format("WARNING: Group is Waiting for a specific duration ==> RTB event is canceled", Nwp)) + allowed=false + end end @@ -2296,36 +2328,132 @@ function FLIGHTGROUP:onafterRTB(From, Event, To, airbase, SpeedTo, SpeedHold, Sp -- Set the destination base. self.destbase=airbase - -- Clear holding time in any case. - self.Tholding=nil - -- Cancel all missions. for _,_mission in pairs(self.missionqueue) do local mission=_mission --Ops.Auftrag#AUFTRAG local mystatus=mission:GetGroupStatus(self) - + -- Check if mission is already over! if not (mystatus==AUFTRAG.GroupStatus.DONE or mystatus==AUFTRAG.GroupStatus.CANCELLED) then local text=string.format("Canceling mission %s in state=%s", mission.name, mission.status) self:T(self.lid..text) self:MissionCancel(mission) end - + end + self:_LandAtAirbase(airbase, SpeedTo, SpeedHold, SpeedLand) + +end + + +--- On before "LandAtAirbase" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Airbase#AIRBASE airbase The airbase to hold at. +function FLIGHTGROUP:onbeforeLandAtAirbase(From, Event, To, airbase) + + if self:IsAlive() then + + local allowed=true + local Tsuspend=nil + + if airbase==nil then + self:E(self.lid.."ERROR: Airbase is nil in LandAtAirase() call!") + allowed=false + end + + -- Check that coaliton is okay. We allow same (blue=blue, red=red) or landing on neutral bases. + if airbase and airbase:GetCoalition()~=self.group:GetCoalition() and airbase:GetCoalition()>0 then + self:E(self.lid..string.format("ERROR: Wrong airbase coalition %d in LandAtAirbase() call! We allow only same as group %d or neutral airbases 0", airbase:GetCoalition(), self.group:GetCoalition())) + return false + end + + if self.currbase and self.currbase:GetName()==airbase:GetName() then + self:E(self.lid.."WARNING: Currbase is already same as LandAtAirbase airbase. LandAtAirbase canceled!") + return false + end + + -- Check if the group has landed at an airbase. If so, we lost control and RTBing is not possible (only after a respawn). + if self:IsLanded() then + self:E(self.lid.."WARNING: Flight has already landed. LandAtAirbase canceled!") + return false + end + + if self:IsParking() then + allowed=false + Tsuspend=-30 + self:E(self.lid.."WARNING: Flight is parking. LandAtAirbase call delayed by 30 sec") + elseif self:IsTaxiing() then + allowed=false + Tsuspend=-1 + self:E(self.lid.."WARNING: Flight is parking. LandAtAirbase call delayed by 1 sec") + end + + if Tsuspend and not allowed then + self:__LandAtAirbase(Tsuspend, airbase) + end + + return allowed + else + self:E(self.lid.."WARNING: Group is not alive! LandAtAirbase call not allowed") + return false + end + +end + + +--- On after "LandAtAirbase" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Airbase#AIRBASE airbase The airbase to hold at. +function FLIGHTGROUP:onafterLandAtAirbase(From, Event, To, airbase) + + self.isLandingAtAirbase=airbase + + self:_LandAtAirbase(airbase) + +end + +--- Land at an airbase. +-- @param #FLIGHTGROUP self +-- @param Wrapper.Airbase#AIRBASE airbase Airbase where the group shall land. +-- @param #number SpeedTo Speed used for travelling from current position to holding point in knots. +-- @param #number SpeedHold Holding speed in knots. +-- @param #number SpeedLand Landing speed in knots. Default 170 kts. +function FLIGHTGROUP:_LandAtAirbase(airbase, SpeedTo, SpeedHold, SpeedLand) + + -- Set current airbase. + self.currbase=airbase + + -- Passed final waypoint! + self:_PassedFinalWaypoint(true, "_LandAtAirbase") + + -- Not waiting any more. + self.Twaiting=nil + self.dTwait=nil + -- Defaults: SpeedTo=SpeedTo or UTILS.KmphToKnots(self.speedCruise) - SpeedHold=SpeedHold or (self.ishelo and 80 or 250) - SpeedLand=SpeedLand or (self.ishelo and 40 or 170) + SpeedHold=SpeedHold or (self.isHelo and 80 or 250) + SpeedLand=SpeedLand or (self.isHelo and 40 or 170) + + -- Clear holding time in any case. + self.Tholding=nil -- Debug message. local text=string.format("Flight group set to hold at airbase %s. SpeedTo=%d, SpeedHold=%d, SpeedLand=%d", airbase:GetName(), SpeedTo, SpeedHold, SpeedLand) self:T(self.lid..text) - local althold=self.ishelo and 1000+math.random(10)*100 or math.random(4,10)*1000 + -- Holding altitude. + local althold=self.isHelo and 1000+math.random(10)*100 or math.random(4,10)*1000 -- Holding points. - local c0=self.group:GetCoordinate() + local c0=self:GetCoordinate() local p0=airbase:GetZone():GetRandomCoordinate():SetAltitude(UTILS.FeetToMeters(althold)) local p1=nil local wpap=nil @@ -2339,7 +2467,7 @@ function FLIGHTGROUP:onafterRTB(From, Event, To, airbase, SpeedTo, SpeedHold, Sp p1=HoldingPoint.pos1 -- Debug marks. - if self.Debug then + if false then p0:MarkToAll("Holding point P0") p1:MarkToAll("Holding point P1") end @@ -2350,20 +2478,26 @@ function FLIGHTGROUP:onafterRTB(From, Event, To, airbase, SpeedTo, SpeedHold, Sp -- Add flight to inbound queue. self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.INBOUND) end + + -- Some intermediate coordinate to climb to the default cruise alitude. + local c1=c0:GetIntermediateCoordinate(p0, 0.25):SetAltitude(self.altitudeCruise, true) + local c2=c0:GetIntermediateCoordinate(p0, 0.75):SetAltitude(self.altitudeCruise, true) -- Altitude above ground for a glide slope of 3 degrees. - local x1=self.ishelo and UTILS.NMToMeters(5.0) or UTILS.NMToMeters(10) - local x2=self.ishelo and UTILS.NMToMeters(2.5) or UTILS.NMToMeters(5) + local x1=self.isHelo and UTILS.NMToMeters(2.0) or UTILS.NMToMeters(10) + local x2=self.isHelo and UTILS.NMToMeters(1.0) or UTILS.NMToMeters(5) local alpha=math.rad(3) local h1=x1*math.tan(alpha) local h2=x2*math.tan(alpha) + -- Get active runway. local runway=airbase:GetActiveRunway() -- Set holding flag to 0=false. self.flaghold:Set(0) - local holdtime=5*60 + -- Set holding time. + local holdtime=2*60 if fc or self.airboss then holdtime=nil end @@ -2379,27 +2513,30 @@ function FLIGHTGROUP:onafterRTB(From, Event, To, airbase, SpeedTo, SpeedHold, Sp -- Waypoints from current position to holding point. local wp={} - wp[#wp+1]=c0:WaypointAir(nil, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, UTILS.KnotsToKmph(SpeedTo), true , nil, {}, "Current Pos") - wp[#wp+1]=p0:WaypointAir(nil, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, UTILS.KnotsToKmph(SpeedTo), true , nil, {TaskArrived, TaskHold, TaskKlar}, "Holding Point") + -- NOTE: Currently, this first waypoint confuses the AI. It makes them go in circles. Looks like they cannot find the waypoint and are flying around it. + --wp[#wp+1]=c0:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, UTILS.KnotsToKmph(SpeedTo), true , nil, {}, "Current Pos") + wp[#wp+1]=c1:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, UTILS.KnotsToKmph(SpeedTo), true , nil, {}, "Climb") + wp[#wp+1]=c2:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, UTILS.KnotsToKmph(SpeedTo), true , nil, {}, "Descent") + wp[#wp+1]=p0:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, UTILS.KnotsToKmph(SpeedTo), true , nil, {TaskArrived, TaskHold, TaskKlar}, "Holding Point") -- Approach point: 10 NN in direction of runway. - if airbase:GetAirbaseCategory()==Airbase.Category.AIRDROME then + if airbase:IsAirdrome() then --- -- Airdrome --- local papp=airbase:GetCoordinate():Translate(x1, runway.heading-180):SetAltitude(h1) - wp[#wp+1]=papp:WaypointAirTurningPoint(nil, UTILS.KnotsToKmph(SpeedLand), {}, "Final Approach") + wp[#wp+1]=papp:WaypointAirTurningPoint("BARO", UTILS.KnotsToKmph(SpeedLand), {}, "Final Approach") -- Okay, it looks like it's best to specify the coordinates not at the airbase but a bit away. This causes a more direct landing approach. local pland=airbase:GetCoordinate():Translate(x2, runway.heading-180):SetAltitude(h2) wp[#wp+1]=pland:WaypointAirLanding(UTILS.KnotsToKmph(SpeedLand), airbase, {}, "Landing") - elseif airbase:GetAirbaseCategory()==Airbase.Category.SHIP then + elseif airbase:IsShip() or airbase:IsHelipad() then --- - -- Ship + -- Ship or Helipad --- local pland=airbase:GetCoordinate() @@ -2409,33 +2546,13 @@ function FLIGHTGROUP:onafterRTB(From, Event, To, airbase, SpeedTo, SpeedHold, Sp if self.isAI then - local routeto=false - if fc or world.event.S_EVENT_KILL then - routeto=true - end - -- Clear all tasks. -- Warning, looks like this can make DCS CRASH! Had this after calling RTB once passed the final waypoint. --self:ClearTasks() - -- Respawn? - if routeto then - - -- Just route the group. Respawn might happen when going from holding to final. - self:Route(wp, 1) - - else - - -- Get group template. - local Template=self.group:GetTemplate() - - -- Set route points. - Template.route.points=wp - - --Respawn the group with new waypoints. - self:Respawn(Template) - - end + -- Just route the group. Respawn might happen when going from holding to final. + -- NOTE: I have delayed that here because of RTB calling _LandAtAirbase which resets current task immediately. So the stop flag change to 1 will not trigger TaskDone() and a current mission is not done either + self:Route(wp, 0.1) end @@ -2446,37 +2563,31 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param Core.Point#COORDINATE Coord Coordinate where to orbit. Default current position. --- @param #number Altitude Altitude in feet. Default 10000 ft. --- @param #number Speed Speed in knots. Default 250 kts. -function FLIGHTGROUP:onbeforeWait(From, Event, To, Coord, Altitude, Speed) +-- @param #number Duration Duration how long the group will be waiting in seconds. Default `nil` (=forever). +-- @param #number Altitude Altitude in feet. Default 10,000 ft for airplanes and 1,000 feet for helos. +-- @param #number Speed Speed in knots. Default 250 kts for airplanes and 20 kts for helos. +function FLIGHTGROUP:onbeforeWait(From, Event, To, Duration, Altitude, Speed) local allowed=true local Tsuspend=nil - -- Check if there are remaining tasks. - local Ntot,Nsched, Nwp=self:CountRemainingTasks() - - if self.taskcurrent>0 then - self:I(self.lid..string.format("WARNING: Got current task ==> WAIT event is suspended for 10 sec.")) - Tsuspend=-10 + -- Check for a current task. + if self.taskcurrent>0 and not self:IsLandedAt() then + self:I(self.lid..string.format("WARNING: Got current task ==> WAIT event is suspended for 30 sec!")) + Tsuspend=-30 allowed=false end - - if Nsched>0 then - self:I(self.lid..string.format("WARNING: Still got %d SCHEDULED tasks in the queue ==> WAIT event is suspended for 10 sec.", Nsched)) - Tsuspend=-10 - allowed=false - end - - if Nwp>0 then - self:I(self.lid..string.format("WARNING: Still got %d WAYPOINT tasks in the queue ==> WAIT event is suspended for 10 sec.", Nwp)) - Tsuspend=-10 - allowed=false + + -- Check for a current transport assignment. + if self.cargoTransport and not self:IsLandedAt() then + --self:I(self.lid..string.format("WARNING: Got current TRANSPORT assignment ==> WAIT event is suspended for 30 sec!")) + --Tsuspend=-30 + --allowed=false end + -- Call wait again. if Tsuspend and not allowed then - self:__Wait(Tsuspend, Coord, Altitude, Speed) + self:__Wait(Tsuspend, Duration, Altitude, Speed) end return allowed @@ -2488,26 +2599,53 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param Core.Point#COORDINATE Coord Coordinate where to orbit. Default current position. --- @param #number Altitude Altitude in feet. Default 10000 ft. --- @param #number Speed Speed in knots. Default 250 kts. -function FLIGHTGROUP:onafterWait(From, Event, To, Coord, Altitude, Speed) +-- @param #number Duration Duration how long the group will be waiting in seconds. Default `nil` (=forever). +-- @param #number Altitude Altitude in feet. Default 10,000 ft for airplanes and 1,000 feet for helos. +-- @param #number Speed Speed in knots. Default 250 kts for airplanes and 20 kts for helos. +function FLIGHTGROUP:onafterWait(From, Event, To, Duration, Altitude, Speed) - Coord=Coord or self.group:GetCoordinate() - Altitude=Altitude or (self.ishelo and 1000 or 10000) - Speed=Speed or (self.ishelo and 80 or 250) + -- Group will orbit at its current position. + local Coord=self:GetCoordinate() + + -- Set altitude: 1000 ft for helos and 10,000 ft for panes. + if Altitude then + Altitude=UTILS.FeetToMeters(Altitude) + else + Altitude=self.altitudeCruise + end + + -- Set speed. + Speed=Speed or (self.isHelo and 20 or 250) -- Debug message. - local text=string.format("Flight group set to wait/orbit at altitude %d m and speed %.1f km/h", Altitude, Speed) + local text=string.format("Group set to wait/orbit at altitude %d m and speed %.1f km/h for %s seconds", Altitude, Speed, tostring(Duration)) self:T(self.lid..text) --TODO: set ROE passive. introduce roe event/state/variable. - -- Orbit task. - local TaskOrbit=self.group:TaskOrbit(Coord, UTILS.FeetToMeters(Altitude), UTILS.KnotsToMps(Speed)) + -- Orbit until flaghold=1 (true) but max 5 min if no FC is giving the landing clearance. + self.flaghold:Set(0) + local TaskOrbit = self.group:TaskOrbit(Coord, Altitude, UTILS.KnotsToMps(Speed)) + local TaskStop = self.group:TaskCondition(nil, self.flaghold.UserFlagName, 1, nil, Duration) + local TaskCntr = self.group:TaskControlled(TaskOrbit, TaskStop) + local TaskOver = self.group:TaskFunction("FLIGHTGROUP._FinishedWaiting", self) + + local DCSTasks + if Duration or true then + DCSTasks=self.group:TaskCombo({TaskCntr, TaskOver}) + else + DCSTasks=self.group:TaskCombo({TaskOrbit, TaskOver}) + end + -- Set task. - self:SetTask(TaskOrbit) + self:PushTask(DCSTasks) + + -- Set time stamp. + self.Twaiting=timer.getAbsTime() + + -- Max waiting + self.dTwait=Duration end @@ -2537,7 +2675,7 @@ function FLIGHTGROUP:onafterRefuel(From, Event, To, Coordinate) local Speed=self.speedCruise - local coordinate=self.group:GetCoordinate() + local coordinate=self:GetCoordinate() Coordinate=Coordinate or coordinate:Translate(UTILS.NMToMeters(5), self.group:GetHeading(), true) @@ -2593,7 +2731,7 @@ function FLIGHTGROUP:onafterHolding(From, Event, To) elseif self.airboss then - if self.ishelo then + if self.isHelo then local carrierpos=self.airboss:GetCoordinate() local carrierheading=self.airboss:GetHeading() @@ -2631,39 +2769,39 @@ function FLIGHTGROUP:onafterEngageTarget(From, Event, To, Target) -- Check target object. if Target:IsInstanceOf("UNIT") or Target:IsInstanceOf("STATIC") then - + DCStask=self:GetGroup():TaskAttackUnit(Target, true) - + elseif Target:IsInstanceOf("GROUP") then DCStask=self:GetGroup():TaskAttackGroup(Target, nil, nil, nil, nil, nil, nil, true) - + elseif Target:IsInstanceOf("SET_UNIT") then local DCSTasks={} - + for _,_unit in pairs(Target:GetSet()) do --detected by =HRP= Zero local unit=_unit --Wrapper.Unit#UNIT local task=self:GetGroup():TaskAttackUnit(unit, true) table.insert(DCSTasks) end - + -- Task combo. DCStask=self:GetGroup():TaskCombo(DCSTasks) elseif Target:IsInstanceOf("SET_GROUP") then local DCSTasks={} - + for _,_unit in pairs(Target:GetSet()) do --detected by =HRP= Zero local unit=_unit --Wrapper.Unit#UNIT local task=self:GetGroup():TaskAttackGroup(Target, nil, nil, nil, nil, nil, nil, true) table.insert(DCSTasks) end - + -- Task combo. DCStask=self:GetGroup():TaskCombo(DCSTasks) - + else self:E("ERROR: unknown Target in EngageTarget! Needs to be a UNIT, STATIC, GROUP, SET_UNIT or SET_GROUP") return @@ -2671,10 +2809,10 @@ function FLIGHTGROUP:onafterEngageTarget(From, Event, To, Target) -- Create new task.The description "Engage_Target" is checked so do not change that lightly. local Task=self:NewTaskScheduled(DCStask, 1, "Engage_Target", 0) - + -- Backup ROE setting. Task.backupROE=self:GetROE() - + -- Switch ROE to open fire self:SwitchROE(ENUMS.ROE.OpenFire) @@ -2685,7 +2823,7 @@ function FLIGHTGROUP:onafterEngageTarget(From, Event, To, Target) end -- Execute task. - self:TaskExecute(Task) + self:TaskExecute(Task) end @@ -2707,7 +2845,7 @@ end -- @param Core.Point#COORDINATE Coordinate The coordinate where to land. Default is current position. -- @param #number Duration The duration in seconds to remain on ground. Default 600 sec (10 min). function FLIGHTGROUP:onbeforeLandAt(From, Event, To, Coordinate, Duration) - return self.ishelo + return self.isHelo end --- On after "LandAt" event. Order helicopter to land at a specific point. @@ -2716,11 +2854,13 @@ end -- @param #string Event Event. -- @param #string To To state. -- @param Core.Point#COORDINATE Coordinate The coordinate where to land. Default is current position. --- @param #number Duration The duration in seconds to remain on ground. Default 600 sec (10 min). +-- @param #number Duration The duration in seconds to remain on ground. Default `nil` = forever. function FLIGHTGROUP:onafterLandAt(From, Event, To, Coordinate, Duration) -- Duration. - Duration=Duration or 600 + --Duration=Duration or 600 + + self:T(self.lid..string.format("Landing at Coordinate for %s seconds", tostring(Duration))) Coordinate=Coordinate or self:GetCoordinate() @@ -2728,11 +2868,8 @@ function FLIGHTGROUP:onafterLandAt(From, Event, To, Coordinate, Duration) local Task=self:NewTaskScheduled(DCStask, 1, "Task_Land_At", 0) - -- Add task with high priority. - --self:AddTask(task, 1, "Task_Land_At", 0) - self:TaskExecute(Task) - + end --- On after "FuelLow" event. @@ -2742,8 +2879,11 @@ end -- @param #string To To state. function FLIGHTGROUP:onafterFuelLow(From, Event, To) + -- Current min fuel. + local fuel=self:GetFuelMin() or 0 + -- Debug message. - local text=string.format("Low fuel for flight group %s", self.groupname) + local text=string.format("Low fuel %d for flight group %s", fuel, self.groupname) self:I(self.lid..text) -- Set switch to true. @@ -2752,55 +2892,30 @@ function FLIGHTGROUP:onafterFuelLow(From, Event, To) -- Back to destination or home. local airbase=self.destbase or self.homebase - if self.airwing then + if self.fuellowrefuel and self.refueltype then - -- Get closest tanker from airwing that can refuel this flight. - local tanker=self.airwing:GetTankerForFlight(self) + -- Find nearest tanker within 50 NM. + local tanker=self:FindNearestTanker(50) + + if tanker then - if tanker and self.fuellowrefuel then - -- Debug message. - self:I(self.lid..string.format("Send to refuel at tanker %s", tanker.flightgroup:GetName())) - - -- Get a coordinate towards the tanker. - local coordinate=self:GetCoordinate():GetIntermediateCoordinate(tanker.flightgroup:GetCoordinate(), 0.75) + self:I(self.lid..string.format("Send to refuel at tanker %s", tanker:GetName())) - -- Send flight to tanker with refueling task. + -- Get a coordinate towards the tanker. + local coordinate=self:GetCoordinate():GetIntermediateCoordinate(tanker:GetCoordinate(), 0.75) + + -- Trigger refuel even. self:Refuel(coordinate) - else - - if airbase and self.fuellowrtb then - self:RTB(airbase) - --TODO: RTZ - end - - end - - else - - if self.fuellowrefuel and self.refueltype then - - local tanker=self:FindNearestTanker(50) - - if tanker then - - self:I(self.lid..string.format("Send to refuel at tanker %s", tanker:GetName())) - - -- Get a coordinate towards the tanker. - local coordinate=self:GetCoordinate():GetIntermediateCoordinate(tanker:GetCoordinate(), 0.75) - - self:Refuel(coordinate) - - return - end - end - - if airbase and self.fuellowrtb then - self:RTB(airbase) - --TODO: RTZ + return end + end + -- Send back to airbase. + if airbase and self.fuellowrtb then + self:RTB(airbase) + --TODO: RTZ end end @@ -2828,44 +2943,6 @@ function FLIGHTGROUP:onafterFuelCritical(From, Event, To) end end ---- On after "Stop" event. --- @param #FLIGHTGROUP self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. -function FLIGHTGROUP:onafterStop(From, Event, To) - - -- Check if group is still alive. - if self:IsAlive() then - - -- Set element parking spot to FREE (after arrived for example). - if self.flightcontrol then - for _,_element in pairs(self.elements) do - local element=_element --#FLIGHTGROUP.Element - self:_SetElementParkingFree(element) - end - end - - end - - -- Handle events: - self:UnHandleEvent(EVENTS.Birth) - self:UnHandleEvent(EVENTS.EngineStartup) - self:UnHandleEvent(EVENTS.Takeoff) - self:UnHandleEvent(EVENTS.Land) - self:UnHandleEvent(EVENTS.EngineShutdown) - self:UnHandleEvent(EVENTS.PilotDead) - self:UnHandleEvent(EVENTS.Ejection) - self:UnHandleEvent(EVENTS.Crash) - self:UnHandleEvent(EVENTS.RemoveUnit) - - -- Call OPSGROUP function. - self:GetParent(self).onafterStop(self, From, Event, To) - - -- Remove flight from data base. - _DATABASE.FLIGHTGROUPS[self.groupname]=nil -end - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Task functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2912,46 +2989,56 @@ function FLIGHTGROUP._FinishedRefuelling(group, flightgroup) flightgroup:__Refueled(-1) end +--- Function called when flight finished waiting. +-- @param Wrapper.Group#GROUP group Group object. +-- @param #FLIGHTGROUP flightgroup Flight group object. +function FLIGHTGROUP._FinishedWaiting(group, flightgroup) + flightgroup:T(flightgroup.lid..string.format("Group finished waiting")) + + -- Not waiting any more. + flightgroup.Twaiting=nil + flightgroup.dTwait=nil + + -- Check group done. + flightgroup:_CheckGroupDone(0.1) +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Misc functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Initialize group parameters. Also initializes waypoints if self.waypoints is nil. -- @param #FLIGHTGROUP self +-- @param #table Template Template used to init the group. Default is `self.template`. -- @return #FLIGHTGROUP self -function FLIGHTGROUP:_InitGroup() +function FLIGHTGROUP:_InitGroup(Template) -- First check if group was already initialized. if self.groupinitialized then - self:E(self.lid.."WARNING: Group was already initialized!") + self:T(self.lid.."WARNING: Group was already initialized! Will NOT do it again!") return end - + -- Group object. local group=self.group --Wrapper.Group#GROUP -- Get template of group. - self.template=group:GetTemplate() - - -- Define category. - self.isAircraft=true - self.isNaval=false - self.isGround=false + local template=Template or self:_GetTemplate() -- Helo group. - self.ishelo=group:IsHelicopter() + self.isHelo=group:IsHelicopter() -- Is (template) group uncontrolled. - self.isUncontrolled=self.template.uncontrolled + self.isUncontrolled=template.uncontrolled -- Is (template) group late activated. - self.isLateActivated=self.template.lateActivation + self.isLateActivated=template.lateActivation -- Max speed in km/h. self.speedMax=group:GetSpeedMax() -- Cruise speed limit 350 kts for fixed and 80 knots for rotary wings. - local speedCruiseLimit=self.ishelo and UTILS.KnotsToKmph(80) or UTILS.KnotsToKmph(350) + local speedCruiseLimit=self.isHelo and UTILS.KnotsToKmph(80) or UTILS.KnotsToKmph(350) -- Cruise speed: 70% of max speed but within limit. self.speedCruise=math.min(self.speedMax*0.7, speedCruiseLimit) @@ -2960,12 +3047,12 @@ function FLIGHTGROUP:_InitGroup() self.ammo=self:GetAmmoTot() -- Radio parameters from template. Default is set on spawn if not modified by user. - self.radio.Freq=tonumber(self.template.frequency) - self.radio.Modu=tonumber(self.template.modulation) - self.radio.On=self.template.communication - + self.radio.Freq=tonumber(template.frequency) + self.radio.Modu=tonumber(template.modulation) + self.radio.On=template.communication + -- Set callsign. Default is set on spawn if not modified by user. - local callsign=self.template.units[1].callsign + local callsign=template.units[1].callsign if type(callsign)=="number" then -- Sometimes callsign is just "101". local cs=tostring(callsign) callsign={} @@ -2975,16 +3062,15 @@ function FLIGHTGROUP:_InitGroup() end self.callsign.NumberSquad=callsign[1] self.callsign.NumberGroup=callsign[2] - self.callsign.NumberElement=callsign[3] -- First element only self.callsign.NameSquad=UTILS.GetCallsignName(self.callsign.NumberSquad) -- Set default formation. - if self.ishelo then + if self.isHelo then self.optionDefault.Formation=ENUMS.Formation.RotaryWing.EchelonLeft.D300 else self.optionDefault.Formation=ENUMS.Formation.FixedWing.EchelonLeft.Group end - + -- Default TACAN off. self:SetDefaultTACAN(nil, nil, nil, nil, true) self.tacan=UTILS.DeepCopy(self.tacanDefault) @@ -2999,125 +3085,36 @@ function FLIGHTGROUP:_InitGroup() self.menu.atc.root=self.menu.atc.root or MENU_GROUP:New(self.group, "ATC") end + -- Units of the group. + local units=self.group:GetUnits() + + -- DCS group. + local dcsgroup=Group.getByName(self.groupname) + local size0=dcsgroup:getInitialSize() + + -- Quick check. + if #units~=size0 then + self:E(self.lid..string.format("ERROR: Got #units=%d but group consists of %d units!", #units, size0)) + end + -- Add elemets. - for _,unit in pairs(self.group:GetUnits()) do - local element=self:AddElementByName(unit:GetName()) - end - - -- Get first unit. This is used to extract other parameters. - local unit=self.group:GetUnit(1) - - if unit then - - self.rangemax=unit:GetRange() - - self.descriptors=unit:GetDesc() - - self.actype=unit:GetTypeName() - - self.ceiling=self.descriptors.Hmax - - self.tankertype=select(2, unit:IsTanker()) - self.refueltype=select(2, unit:IsRefuelable()) - - -- Debug info. - if self.verbose>=1 then - local text=string.format("Initialized Flight Group %s:\n", self.groupname) - text=text..string.format("Unit type = %s\n", self.actype) - text=text..string.format("Speed max = %.1f Knots\n", UTILS.KmphToKnots(self.speedMax)) - text=text..string.format("Range max = %.1f km\n", self.rangemax/1000) - text=text..string.format("Ceiling = %.1f feet\n", UTILS.MetersToFeet(self.ceiling)) - text=text..string.format("Tanker type = %s\n", tostring(self.tankertype)) - text=text..string.format("Refuel type = %s\n", tostring(self.refueltype)) - text=text..string.format("AI = %s\n", tostring(self.isAI)) - text=text..string.format("Helicopter = %s\n", tostring(self.group:IsHelicopter())) - text=text..string.format("Elements = %d\n", #self.elements) - text=text..string.format("Waypoints = %d\n", #self.waypoints) - text=text..string.format("Radio = %.1f MHz %s %s\n", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu), tostring(self.radio.On)) - text=text..string.format("Ammo = %d (G=%d/R=%d/B=%d/M=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Rockets, self.ammo.Bombs, self.ammo.Missiles) - text=text..string.format("FSM state = %s\n", self:GetState()) - text=text..string.format("Is alive = %s\n", tostring(self.group:IsAlive())) - text=text..string.format("LateActivate = %s\n", tostring(self:IsLateActivated())) - text=text..string.format("Uncontrolled = %s\n", tostring(self:IsUncontrolled())) - text=text..string.format("Start Air = %s\n", tostring(self:IsTakeoffAir())) - text=text..string.format("Start Cold = %s\n", tostring(self:IsTakeoffCold())) - text=text..string.format("Start Hot = %s\n", tostring(self:IsTakeoffHot())) - text=text..string.format("Start Rwy = %s\n", tostring(self:IsTakeoffRunway())) - self:I(self.lid..text) - end - - -- Init done. - self.groupinitialized=true - + for _,unit in pairs(units) do + self:_AddElementByName(unit:GetName()) end + -- Init done. + self.groupinitialized=true + return self end ---- Add an element to the flight group. --- @param #FLIGHTGROUP self --- @param #string unitname Name of unit. --- @return #FLIGHTGROUP.Element The element or nil. -function FLIGHTGROUP:AddElementByName(unitname) - - local unit=UNIT:FindByName(unitname) - - if unit then - - local element={} --#FLIGHTGROUP.Element - - element.name=unitname - element.unit=unit - element.status=OPSGROUP.ElementStatus.INUTERO - element.group=unit:GetGroup() - - -- TODO: this is wrong when grouping is used! - local unittemplate=element.unit:GetTemplate() - - element.modex=unittemplate.onboard_num - element.skill=unittemplate.skill - element.payload=unittemplate.payload - element.pylons=unittemplate.payload and unittemplate.payload.pylons or nil --element.unit:GetTemplatePylons() - element.fuelmass0=unittemplate.payload and unittemplate.payload.fuel or 0 --element.unit:GetTemplatePayload().fuel - element.fuelmass=element.fuelmass0 - element.fuelrel=element.unit:GetFuel() - element.category=element.unit:GetUnitCategory() - element.categoryname=element.unit:GetCategoryName() - element.callsign=element.unit:GetCallsign() - element.size=element.unit:GetObjectSize() - - if element.skill=="Client" or element.skill=="Player" then - element.ai=false - element.client=CLIENT:FindByName(unitname) - else - element.ai=true - end - - -- Debug text. - local text=string.format("Adding element %s: status=%s, skill=%s, modex=%s, fuelmass=%.1f (%d), category=%d, categoryname=%s, callsign=%s, ai=%s", - element.name, element.status, element.skill, element.modex, element.fuelmass, element.fuelrel*100, element.category, element.categoryname, element.callsign, tostring(element.ai)) - self:T(self.lid..text) - - -- Add element to table. - table.insert(self.elements, element) - - if unit:IsAlive() then - self:ElementSpawned(element) - end - - return element - end - - return nil -end - --- Check if a unit is and element of the flightgroup. -- @param #FLIGHTGROUP self -- @return Wrapper.Airbase#AIRBASE Final destination airbase or #nil. function FLIGHTGROUP:GetHomebaseFromWaypoints() - local wp=self:GetWaypoint(1) + local wp=self.waypoints0 and self.waypoints0[1] or nil --self:GetWaypoint(1) if wp then @@ -3127,7 +3124,7 @@ function FLIGHTGROUP:GetHomebaseFromWaypoints() -- Get airbase ID depending on airbase category. local airbaseID=nil - + if wp.airdromeId then airbaseID=wp.airdromeId else @@ -3135,7 +3132,7 @@ function FLIGHTGROUP:GetHomebaseFromWaypoints() end local airbase=AIRBASE:FindByID(airbaseID) - + return airbase end @@ -3203,13 +3200,13 @@ function FLIGHTGROUP:FindNearestTanker(Radius) local istanker, refuelsystem=unit:IsTanker() - if istanker and self.refueltype==refuelsystem then + if istanker and self.refueltype==refuelsystem and unit:IsAlive() and unit:GetCoalition()==self:GetCoalition() then -- Distance. local d=unit:GetCoordinate():Get2DDistance(coord) if d1 then table.remove(self.waypoints, #self.waypoints) else self.destbase=self.homebase @@ -3412,7 +3410,7 @@ function FLIGHTGROUP:InitWaypoints() -- Check if only 1 wp? if #self.waypoints==1 then - self.passedfinalwp=true + self:_PassedFinalWaypoint(true, "FLIGHTGROUP:InitWaypoints #self.waypoints==1") end end @@ -3430,22 +3428,69 @@ end -- @return Ops.OpsGroup#OPSGROUP.Waypoint Waypoint table. function FLIGHTGROUP:AddWaypoint(Coordinate, Speed, AfterWaypointWithID, Altitude, Updateroute) + -- Create coordinate. + local coordinate=self:_CoordinateFromObject(Coordinate) + + -- Set waypoint index. + local wpnumber=self:GetWaypointIndexAfterID(AfterWaypointWithID) + + -- Speed in knots. + Speed=Speed or self.speedCruise + + -- Create air waypoint. + local wp=coordinate:WaypointAir(COORDINATE.WaypointAltType.BARO, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, UTILS.KnotsToKmph(Speed), true, nil, {}) + + -- Create waypoint data table. + local waypoint=self:_CreateWaypoint(wp) + + -- Set altitude. + if Altitude then + waypoint.alt=UTILS.FeetToMeters(Altitude) + end + + -- Add waypoint to table. + self:_AddWaypoint(waypoint, wpnumber) + + -- Debug info. + self:T(self.lid..string.format("Adding AIR waypoint #%d, speed=%.1f knots. Last waypoint passed was #%s. Total waypoints #%d", wpnumber, Speed, self.currentwp, #self.waypoints)) + + -- Update route. + if Updateroute==nil or Updateroute==true then + self:__UpdateRoute(-0.01) + end + + return waypoint +end + +--- Add an LANDING waypoint to the flight plan. +-- @param #FLIGHTGROUP self +-- @param Wrapper.Airbase#AIRBASE Airbase The airbase where the group should land. +-- @param #number Speed Speed in knots. Default 350 kts. +-- @param #number AfterWaypointWithID Insert waypoint after waypoint given ID. Default is to insert as last waypoint. +-- @param #number Altitude Altitude in feet. Default is y-component of Coordinate. Note that these altitudes are wrt to sea level (barometric altitude). +-- @param #boolean Updateroute If true or nil, call UpdateRoute. If false, no call. +-- @return Ops.OpsGroup#OPSGROUP.Waypoint Waypoint table. +function FLIGHTGROUP:AddWaypointLanding(Airbase, Speed, AfterWaypointWithID, Altitude, Updateroute) + -- Set waypoint index. local wpnumber=self:GetWaypointIndexAfterID(AfterWaypointWithID) if wpnumber>self.currentwp then - self.passedfinalwp=false + self:_PassedFinalWaypoint(false, "AddWaypointLanding") end -- Speed in knots. - Speed=Speed or 350 + Speed=Speed or self.speedCruise + + -- Get coordinate of airbase. + local Coordinate=Airbase:GetCoordinate() -- Create air waypoint. - local wp=Coordinate:WaypointAir(COORDINATE.WaypointAltType.BARO, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, UTILS.KnotsToKmph(Speed), true, nil, {}) + local wp=Coordinate:WaypointAir(COORDINATE.WaypointAltType.BARO,COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, Speed, nil, Airbase, {}, "Landing Temp", nil) -- Create waypoint data table. local waypoint=self:_CreateWaypoint(wp) - + -- Set altitude. if Altitude then waypoint.alt=UTILS.FeetToMeters(Altitude) @@ -3466,30 +3511,9 @@ function FLIGHTGROUP:AddWaypoint(Coordinate, Speed, AfterWaypointWithID, Altitud end - ---- Check if a unit is an element of the flightgroup. --- @param #FLIGHTGROUP self --- @param #string unitname Name of unit. --- @return #boolean If true, unit is element of the flight group or false if otherwise. -function FLIGHTGROUP:_IsElement(unitname) - - for _,_element in pairs(self.elements) do - local element=_element --#FLIGHTGROUP.Element - - if element.name==unitname then - return true - end - - end - - return false -end - - - --- Set parking spot of element. -- @param #FLIGHTGROUP self --- @param #FLIGHTGROUP.Element Element The element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The element. -- @param Wrapper.Airbase#AIRBASE.ParkingSpot Spot Parking Spot. function FLIGHTGROUP:_SetElementParkingAt(Element, Spot) @@ -3513,7 +3537,7 @@ end --- Set parking spot of element to free -- @param #FLIGHTGROUP self --- @param #FLIGHTGROUP.Element Element The element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The element. function FLIGHTGROUP:_SetElementParkingFree(Element) if Element.parking then @@ -3623,25 +3647,35 @@ end --- Returns the parking spot of the element. -- @param #FLIGHTGROUP self --- @param #FLIGHTGROUP.Element element Element of the flight group. +-- @param Ops.OpsGroup#OPSGROUP.Element element Element of the flight group. -- @param #number maxdist Distance threshold in meters. Default 5 m. -- @param Wrapper.Airbase#AIRBASE airbase (Optional) The airbase to check for parking. Default is closest airbase to the element. -- @return Wrapper.Airbase#AIRBASE.ParkingSpot Parking spot or nil if no spot is within distance threshold. function FLIGHTGROUP:GetParkingSpot(element, maxdist, airbase) + -- Coordinate of unit landed local coord=element.unit:GetCoordinate() + -- Airbase. airbase=airbase or self:GetClosestAirbase() --coord:GetClosestAirbase(nil, self:GetCoalition()) -- TODO: replace by airbase.parking if AIRBASE is updated. local parking=airbase:GetParkingSpotsTable() + -- If airbase is ship, translate parking coords. Alternatively, we just move the coordinate of the unit to the origin of the map, which is way more efficient. + if airbase and airbase:IsShip() then + coord.x=0 + coord.z=0 + maxdist=500 -- 100 meters was not enough, e.g. on the Seawise Giant, where the spot is 139 meters from the "center" + end + local spot=nil --Wrapper.Airbase#AIRBASE.ParkingSpot local dist=nil local distmin=math.huge for _,_parking in pairs(parking) do local parking=_parking --Wrapper.Airbase#AIRBASE.ParkingSpot dist=coord:Get2DDistance(parking.Coordinate) + --env.info(string.format("FF parking %d dist=%.1f", parking.TerminalID, dist)) if dist0 and DetectionSet:GetFirst():GetCoalition() or nil - + -- Filter coalition. if self.coalition then local coalitionname=UTILS.GetCoalitionName(self.coalition):lower() self.detectionset:FilterCoalitions(coalitionname) end - + -- Filter once. self.detectionset:FilterOnce() - + -- Set alias. if Alias then self.alias=tostring(Alias) else - self.alias="SPECTRE" + self.alias="INTEL SPECTRE" if self.coalition then if self.coalition==coalition.side.RED then - self.alias="KGB" + self.alias="INTEL KGB" elseif self.coalition==coalition.side.BLUE then - self.alias="CIA" + self.alias="INTEL CIA" end end - end - + end + self.DetectVisual = true self.DetectOptical = true self.DetectRadar = true self.DetectIRST = true self.DetectRWR = true self.DetectDLINK = true - + self.statusupdate = -60 - + -- Set some string id for output to DCS.log file. - self.lid=string.format("INTEL %s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown") + self.lid=string.format("%s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown") -- Start State. self:SetStartState("Stopped") @@ -223,17 +224,18 @@ function INTEL:New(DetectionSet, Coalition, Alias) -- Add FSM transitions. -- From State --> Event --> To State self:AddTransition("Stopped", "Start", "Running") -- Start FSM. - self:AddTransition("*", "Status", "*") -- INTEL status update - + self:AddTransition("*", "Status", "*") -- INTEL status update. + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + self:AddTransition("*", "Detect", "*") -- Start detection run. Not implemented yet! - + self:AddTransition("*", "NewContact", "*") -- New contact has been detected. self:AddTransition("*", "LostContact", "*") -- Contact could not be detected any more. - + self:AddTransition("*", "NewCluster", "*") -- New cluster has been detected. self:AddTransition("*", "LostCluster", "*") -- Cluster could not be detected any more. - self:AddTransition("*", "Stop", "Stopped") - + + -- Defaults self:SetForgetTime() self:SetAcceptZones() @@ -268,7 +270,7 @@ function INTEL:New(DetectionSet, Coalition, Alias) -- @function [parent=#INTEL] __Status -- @param #INTEL self -- @param #number delay Delay in seconds. - + --- On After "NewContact" event. -- @function [parent=#INTEL] OnAfterNewContact -- @param #INTEL self @@ -276,7 +278,7 @@ function INTEL:New(DetectionSet, Coalition, Alias) -- @param #string Event Event. -- @param #string To To state. -- @param #INTEL.Contact Contact Detected contact. - + --- On After "LostContact" event. -- @function [parent=#INTEL] OnAfterLostContact -- @param #INTEL self @@ -284,7 +286,7 @@ function INTEL:New(DetectionSet, Coalition, Alias) -- @param #string Event Event. -- @param #string To To state. -- @param #INTEL.Contact Contact Lost contact. - + --- On After "NewCluster" event. -- @function [parent=#INTEL] OnAfterNewCluster -- @param #INTEL self @@ -293,7 +295,7 @@ function INTEL:New(DetectionSet, Coalition, Alias) -- @param #string To To state. -- @param #INTEL.Contact Contact Detected contact. -- @param #INTEL.Cluster Cluster Detected cluster - + --- On After "LostCluster" event. -- @function [parent=#INTEL] OnAfterLostCluster -- @param #INTEL self @@ -309,7 +311,7 @@ end -- User functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Set accept zones. Only contacts detected in this/these zone(s) are considered. +--- Set accept zones. Only contacts detected in this/these zone(s) are considered. -- @param #INTEL self -- @param Core.Set#SET_ZONE AcceptZoneSet Set of accept zones. -- @return #INTEL self @@ -365,7 +367,7 @@ function INTEL:RemoveRejectZone(RejectZone) return self end ---- Set forget contacts time interval. +--- Set forget contacts time interval. -- Previously known contacts that are not detected any more, are "lost" after this time. -- This avoids fast oscillations between a contact being detected and undetected. -- @param #INTEL self @@ -377,13 +379,13 @@ function INTEL:SetForgetTime(TimeInterval) end --- Filter unit categories. Valid categories are: --- +-- -- * Unit.Category.AIRPLANE -- * Unit.Category.HELICOPTER -- * Unit.Category.GROUND_UNIT -- * Unit.Category.SHIP -- * Unit.Category.STRUCTURE --- +-- -- @param #INTEL self -- @param #table Categories Filter categories, e.g. {Unit.Category.AIRPLANE, Unit.Category.HELICOPTER}. -- @return #INTEL self @@ -391,26 +393,26 @@ function INTEL:SetFilterCategory(Categories) if type(Categories)~="table" then Categories={Categories} end - + self.filterCategory=Categories - + local text="Filter categories: " for _,category in pairs(self.filterCategory) do text=text..string.format("%d,", category) end self:T(self.lid..text) - + return self end --- Filter group categories. Valid categories are: --- +-- -- * Group.Category.AIRPLANE -- * Group.Category.HELICOPTER -- * Group.Category.GROUND -- * Group.Category.SHIP -- * Group.Category.TRAIN --- +-- -- @param #INTEL self -- @param #table GroupCategories Filter categories, e.g. `{Group.Category.AIRPLANE, Group.Category.HELICOPTER}`. -- @return #INTEL self @@ -418,15 +420,15 @@ function INTEL:FilterCategoryGroup(GroupCategories) if type(GroupCategories)~="table" then GroupCategories={GroupCategories} end - + self.filterCategoryGroup=GroupCategories - + local text="Filter group categories: " for _,category in pairs(self.filterCategoryGroup) do text=text..string.format("%d,", category) end self:T(self.lid..text) - + return self end @@ -442,7 +444,7 @@ function INTEL:SetClusterAnalysis(Switch, Markers) return self end ---- Set verbosity level for debugging. +--- Set verbosity level for debugging. -- @param #INTEL self -- @param #number Verbosity The higher, the noisier, e.g. 0=off, 2=debug -- @return #INTEL self @@ -495,7 +497,7 @@ end -- @param #boolean DetectDLINK Data link detection -- @return self function INTEL:SetDetectionTypes(DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) - self.DetectVisual = DetectVisual and true + self.DetectVisual = DetectVisual and true self.DetectOptical = DetectOptical and true self.DetectRadar = DetectRadar and true self.DetectIRST = DetectIRST and true @@ -554,14 +556,14 @@ function INTEL:onafterStatus(From, Event, To) -- FSM state. local fsmstate=self:GetState() - + -- Fresh arrays. self.ContactsLost={} self.ContactsUnknown={} - + -- Check if group has detected any units. self:UpdateIntel() - + -- Number of total contacts. local Ncontacts=#self.Contacts local Nclusters=#self.Clusters @@ -571,7 +573,7 @@ function INTEL:onafterStatus(From, Event, To) local text=string.format("Status %s [Agents=%s]: Contacts=%d, Clusters=%d, New=%d, Lost=%d", fsmstate, self.detectionset:CountAlive(), Ncontacts, Nclusters, #self.ContactsUnknown, #self.ContactsLost) self:I(self.lid..text) end - + -- Detailed info. if self.verbose>=2 and Ncontacts>0 then local text="Detected Contacts:" @@ -585,9 +587,9 @@ function INTEL:onafterStatus(From, Event, To) end end self:I(self.lid..text) - end + end - self:__Status(self.statusupdate) + self:__Status(self.statusupdate) end @@ -602,24 +604,24 @@ function INTEL:UpdateIntel() -- Loop over all units providing intel. for _,_group in pairs(self.detectionset.Set or {}) do local group=_group --Wrapper.Group#GROUP - + if group and group:IsAlive() then - + for _,_recce in pairs(group:GetUnits()) do local recce=_recce --Wrapper.Unit#UNIT - + -- Get detected units. self:GetDetectedUnits(recce, DetectedUnits, RecceDetecting, self.DetectVisual, self.DetectOptical, self.DetectRadar, self.DetectIRST, self.DetectRWR, self.DetectDLINK) - + end - - end + + end end - + local remove={} for unitname,_unit in pairs(DetectedUnits) do local unit=_unit --Wrapper.Unit#UNIT - + -- Check if unit is in any of the accept zones. if self.acceptzoneset:Count()>0 then local inzone=false @@ -630,7 +632,7 @@ function INTEL:UpdateIntel() break end end - + -- Unit is not in accept zone ==> remove! if not inzone then table.insert(remove, unitname) @@ -647,13 +649,13 @@ function INTEL:UpdateIntel() break end end - + -- Unit is inside a reject zone ==> remove! if inzone then table.insert(remove, unitname) end end - + -- Filter unit categories. if #self.filterCategory>0 then local unitcategory=unit:GetUnitCategory() @@ -668,18 +670,18 @@ function INTEL:UpdateIntel() self:T(self.lid..string.format("Removing unit %s category=%d", unitname, unit:GetCategory())) table.insert(remove, unitname) end - end - + end + end - + -- Remove filtered units. for _,unitname in pairs(remove) do DetectedUnits[unitname]=nil end - + -- Create detected groups. local DetectedGroups={} - local RecceGroups={} + local RecceGroups={} for unitname,_unit in pairs(DetectedUnits) do local unit=_unit --Wrapper.Unit#UNIT local group=unit:GetGroup() @@ -689,15 +691,15 @@ function INTEL:UpdateIntel() RecceGroups[groupname]=RecceDetecting[unitname] end end - - -- Create detected contacts. + + -- Create detected contacts. self:CreateDetectedItems(DetectedGroups, RecceGroups) - + -- Paint a picture of the battlefield. if self.clusteranalysis then self:PaintPicture() end - + end @@ -710,34 +712,35 @@ end -- @param #table RecceDetecting Table of detecting recce names function INTEL:CreateDetectedItems(DetectedGroups, RecceDetecting) self:F({RecceDetecting=RecceDetecting}) + -- Current time. local Tnow=timer.getAbsTime() - + for groupname,_group in pairs(DetectedGroups) do local group=_group --Wrapper.Group#GROUP - - + + -- Get contact if already known. local detecteditem=self:GetContactByName(groupname) - + if detecteditem then --- -- Detected item already exists ==> Update data. --- - + detecteditem.Tdetected=Tnow detecteditem.position=group:GetCoordinate() detecteditem.velocity=group:GetVelocityVec3() detecteditem.speed=group:GetVelocityMPS() - - else + + else --- -- Detected item does not exist in our list yet. --- - + -- Create new contact. local item={} --#INTEL.Contact - + item.groupname=groupname item.group=group item.Tdetected=Tnow @@ -753,28 +756,29 @@ function INTEL:CreateDetectedItems(DetectedGroups, RecceDetecting) item.isground = group:IsGround() or false item.isship = group:IsShip() or false self:T(string.format("%s group detect by %s/%s", groupname, RecceDetecting[groupname] or "unknown", item.recce or "unknown")) - -- Add contact to table. + + -- Add contact to table. self:AddContact(item) - + -- Trigger new contact event. self:NewContact(item) end - + end - + -- Now check if there some groups could not be detected any more. for i=#self.Contacts,1,-1 do local item=self.Contacts[i] --#INTEL.Contact - + -- Check if deltaT>Tforget. We dont want quick oscillations between detected and undetected states. if self:_CheckContactLost(item) then - + -- Trigger LostContact event. This also adds the contact to the self.ContactsLost table. self:LostContact(item) - + -- Remove contact from table. self:RemoveContact(item) - + end end @@ -798,21 +802,33 @@ function INTEL:GetDetectedUnits(Unit, DetectedUnits, RecceDetecting, DetectVisua -- Get detected DCS units. local reccename = Unit:GetName() local detectedtargets=Unit:GetDetectedTargets(DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) - + for DetectionObjectID, Detection in pairs(detectedtargets or {}) do local DetectedObject=Detection.object -- DCS#Object + -- NOTE: Got an object that exists but when trying UNIT:Find() the DCS getName() function failed. ID of the object was 5,000,031 if DetectedObject and DetectedObject:isExist() and DetectedObject.id_<50000000 then - - local unit=UNIT:Find(DetectedObject) - if unit and unit:IsAlive() then - - local unitname=unit:GetName() - - DetectedUnits[unitname]=unit - RecceDetecting[unitname]=reccename - self:T(string.format("Unit %s detect by %s", unitname, reccename)) + -- Protected call to get the name of the object. + local status,name = pcall( + function() + local name=DetectedObject:getName() + return name + end) + + if status then + + local unit=UNIT:FindByName(name) + + if unit and unit:IsAlive() then + DetectedUnits[name]=unit + RecceDetecting[name]=reccename + self:T(string.format("Unit %s detect by %s", name, reccename)) + end + + else + -- Warning! + self:T(self.lid..string.format("WARNING: Could not get name of detected object ID=%s! Detected by %s", DetectedObject.id_, reccename)) end end end @@ -831,7 +847,7 @@ end -- @param #INTEL.Contact Contact Detected contact. function INTEL:onafterNewContact(From, Event, To, Contact) self:F(self.lid..string.format("NEW contact %s", Contact.groupname)) - table.insert(self.ContactsUnknown, Contact) + table.insert(self.ContactsUnknown, Contact) end --- On after "LostContact" event. @@ -906,11 +922,11 @@ function INTEL:RemoveContact(Contact) for i,_contact in pairs(self.Contacts) do local contact=_contact --#INTEL.Contact - + if contact.groupname==Contact.groupname then table.remove(self.Contacts, i) end - + end end @@ -928,7 +944,7 @@ function INTEL:_CheckContactLost(Contact) -- Time since last detected. local dT=timer.getAbsTime()-Contact.Tdetected - + local dTforget=self.dTforget if Contact.category==Group.Category.GROUND then dTforget=60*60*2 -- 2 hours @@ -941,13 +957,13 @@ function INTEL:_CheckContactLost(Contact) elseif Contact.category==Group.Category.TRAIN then dTforget=60*60 -- 1 hour end - + if dT>dTforget then return true else return false end - + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -955,7 +971,7 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- [Internal] Paint picture of the battle field. Does Cluster analysis and updates clusters. Sets markers if markers are enabled. --- @param #INTEL self +-- @param #INTEL self function INTEL:PaintPicture() -- First remove all lost contacts from clusters. @@ -987,64 +1003,64 @@ function INTEL:PaintPicture() self.Clusters = ClusterSet -- update positions self:_UpdateClusterPositions() - + for _,_contact in pairs(self.Contacts) do local contact=_contact --#INTEL.Contact self:T(string.format("Paint Picture: checking for %s",contact.groupname)) -- Check if this contact is in any cluster. local isincluster=self:CheckContactInClusters(contact) - + -- Get the current cluster (if any) this contact belongs to. local currentcluster=self:GetClusterOfContact(contact) - + if currentcluster then --self:I(string.format("Paint Picture: %s has current cluster",contact.groupname)) --- -- Contact is currently part of a cluster. --- - + -- Check if the contact is still connected to the cluster. local isconnected=self:IsContactConnectedToCluster(contact, currentcluster) - + if (not isconnected) and (currentcluster.size > 1) then --self:I(string.format("Paint Picture: %s has LOST current cluster",contact.groupname)) local cluster=self:IsContactPartOfAnyClusters(contact) - + if cluster then self:AddContactToCluster(contact, cluster) else - + local newcluster=self:CreateCluster(contact.position) self:AddContactToCluster(contact, newcluster) self:NewCluster(contact, newcluster) end - + end - - + + else - + --- -- Contact is not in any cluster yet. --- --self:I(string.format("Paint Picture: %s has NO current cluster",contact.groupname)) local cluster=self:IsContactPartOfAnyClusters(contact) - + if cluster then self:AddContactToCluster(contact, cluster) else - + local newcluster=self:CreateCluster(contact.position) self:AddContactToCluster(contact, newcluster) self:NewCluster(contact, newcluster) end - - end - - end - - + end + + end + + + -- Update F10 marker text if cluster has changed. if self.clustermarkers then for _,_cluster in pairs(self.Clusters) do @@ -1060,24 +1076,24 @@ end --- Create a new cluster. -- @param #INTEL self -- @param Core.Point#COORDINATE coordinate The coordinate of the cluster. --- @return #INTEL.Cluster cluster The cluster. +-- @return #INTEL.Cluster cluster The cluster. function INTEL:CreateCluster(coordinate) -- Create new cluster local cluster={} --#INTEL.Cluster - + cluster.index=self.clustercounter - cluster.coordinate=coordinate + cluster.coordinate=coordinate cluster.threatlevelSum=0 cluster.threatlevelMax=0 cluster.size=0 cluster.Contacts={} - + -- Add cluster. table.insert(self.Clusters, cluster) - + -- Increase counter. - self.clustercounter=self.clustercounter+1 + self.clustercounter=self.clustercounter+1 return cluster end @@ -1085,16 +1101,16 @@ end --- Add a contact to the cluster. -- @param #INTEL self -- @param #INTEL.Contact contact The contact. --- @param #INTEL.Cluster cluster The cluster. +-- @param #INTEL.Cluster cluster The cluster. function INTEL:AddContactToCluster(contact, cluster) if contact and cluster then - + -- Add neighbour to cluster contacts. table.insert(cluster.Contacts, contact) - + cluster.threatlevelSum=cluster.threatlevelSum+contact.threatlevel - + cluster.size=cluster.size+1 end @@ -1103,26 +1119,26 @@ end --- Remove a contact from a cluster. -- @param #INTEL self -- @param #INTEL.Contact contact The contact. --- @param #INTEL.Cluster cluster The cluster. +-- @param #INTEL.Cluster cluster The cluster. function INTEL:RemoveContactFromCluster(contact, cluster) if contact and cluster then - + for i,_contact in pairs(cluster.Contacts) do local Contact=_contact --#INTEL.Contact - + if Contact.groupname==contact.groupname then - + cluster.threatlevelSum=cluster.threatlevelSum-contact.threatlevel cluster.size=cluster.size-1 - + table.remove(cluster.Contacts, i) - + return end - + end - + end end @@ -1130,16 +1146,16 @@ end --- Calculate cluster threat level sum. -- @param #INTEL self -- @param #INTEL.Cluster cluster The cluster of contacts. --- @return #number Sum of all threat levels of all groups in the cluster. +-- @return #number Sum of all threat levels of all groups in the cluster. function INTEL:CalcClusterThreatlevelSum(cluster) local threatlevel=0 - + for _,_contact in pairs(cluster.Contacts) do local contact=_contact --#INTEL.Contact - + threatlevel=threatlevel+contact.threatlevel - + end cluster.threatlevelSum = threatlevel return threatlevel @@ -1148,10 +1164,10 @@ end --- Calculate cluster threat level average. -- @param #INTEL self -- @param #INTEL.Cluster cluster The cluster of contacts. --- @return #number Average of all threat levels of all groups in the cluster. +-- @return #number Average of all threat levels of all groups in the cluster. function INTEL:CalcClusterThreatlevelAverage(cluster) - local threatlevel=self:CalcClusterThreatlevelSum(cluster) + local threatlevel=self:CalcClusterThreatlevelSum(cluster) threatlevel=threatlevel/cluster.size cluster.threatlevelAve = threatlevel return threatlevel @@ -1160,19 +1176,19 @@ end --- Calculate max cluster threat level. -- @param #INTEL self -- @param #INTEL.Cluster cluster The cluster of contacts. --- @return #number Max threat levels of all groups in the cluster. +-- @return #number Max threat levels of all groups in the cluster. function INTEL:CalcClusterThreatlevelMax(cluster) local threatlevel=0 - + for _,_contact in pairs(cluster.Contacts) do - + local contact=_contact --#INTEL.Contact - + if contact.threatlevel>threatlevel then threatlevel=contact.threatlevel end - + end cluster.threatlevelMax = threatlevel return threatlevel @@ -1181,7 +1197,7 @@ end --- Calculate cluster heading. -- @param #INTEL self -- @param #INTEL.Cluster cluster The cluster of contacts. --- @return #number Heading average of all groups in the cluster. +-- @return #number Heading average of all groups in the cluster. function INTEL:CalcClusterDirection(cluster) local direction = 0 @@ -1192,15 +1208,15 @@ function INTEL:CalcClusterDirection(cluster) direction = direction + group:GetHeading() n=n+1 end - end + end return math.floor(direction / n) - + end --- Calculate cluster speed. -- @param #INTEL self -- @param #INTEL.Cluster cluster The cluster of contacts. --- @return #number Speed average of all groups in the cluster in MPS. +-- @return #number Speed average of all groups in the cluster in MPS. function INTEL:CalcClusterSpeed(cluster) local velocity = 0 @@ -1211,9 +1227,9 @@ function INTEL:CalcClusterSpeed(cluster) velocity = velocity + group:GetVelocityMPS() n=n+1 end - end + end return math.floor(velocity / n) - + end --- Calculate cluster future position after given seconds. @@ -1241,15 +1257,15 @@ end --- Check if contact is in any known cluster. -- @param #INTEL self -- @param #INTEL.Contact contact The contact. --- @return #boolean If true, contact is in clusters +-- @return #boolean If true, contact is in clusters function INTEL:CheckContactInClusters(contact) for _,_cluster in pairs(self.Clusters) do local cluster=_cluster --#INTEL.Cluster - + for _,_contact in pairs(cluster.Contacts) do local Contact=_contact --#INTEL.Contact - + if Contact.groupname==contact.groupname then return true end @@ -1268,21 +1284,21 @@ function INTEL:IsContactConnectedToCluster(contact, cluster) for _,_contact in pairs(cluster.Contacts) do local Contact=_contact --#INTEL.Contact - + if Contact.groupname~=contact.groupname then - + --local dist=Contact.position:Get2DDistance(contact.position) local dist=Contact.position:DistanceFromPointVec2(contact.position) - + local radius = self.clusterradius or 15 if dist1000 then return true else @@ -1416,25 +1432,25 @@ function INTEL:UpdateClusterMarker(cluster) cluster.marker=MARKER:New(cluster.coordinate, text):ToBlue() else cluster.marker=MARKER:New(cluster.coordinate, text):ToNeutral() - end + end else - + local refresh=false - + if cluster.marker.text~=text then cluster.marker.text=text refresh=true end - + if cluster.marker.coordinate~=cluster.coordinate then cluster.marker.coordinate=cluster.coordinate refresh=true end - + if refresh then cluster.marker:Refresh() end - + end return self @@ -1455,7 +1471,7 @@ end -- * Overcome limitations of (non-available) datalinks between ground radars -- * Detect and track contacts consistently across INTEL instances -- * Use FSM events to link functionality into your scripts --- * Easy setup +-- * Easy setup -- --- === -- @@ -1498,12 +1514,12 @@ INTEL_DLINK.version = "0.0.1" -- @param #string Alias (optional) Name of this instance. Default "SPECTRE" -- @param #number Interval (optional) When to query #INTEL objects for detected items (default 20 seconds). -- @param #number Cachetime (optional) How long to cache detected items (default 300 seconds). --- @usage Use #INTEL_DLINK if you want to merge data from a number of #INTEL objects into one. This might be useful to simulate a +-- @usage Use #INTEL_DLINK if you want to merge data from a number of #INTEL objects into one. This might be useful to simulate a -- Data Link, e.g. for Russian-tech based EWR, realising a Star Topology @{https://en.wikipedia.org/wiki/Network_topology#Star} --- in a basic setup. It will collect the contacts and clusters from the #INTEL objects. +-- in a basic setup. It will collect the contacts and clusters from the #INTEL objects. -- Contact duplicates are removed. Clusters might contain duplicates (Might fix that later, WIP). -- --- Basic setup: +-- Basic setup: -- local datalink = INTEL_DLINK:New({myintel1,myintel2}), "FSB", 20, 300) -- datalink:__Start(2) -- @@ -1515,36 +1531,36 @@ INTEL_DLINK.version = "0.0.1" -- datalink:GetClusterTable() -- #table of #INTEL.Cluster clusters. -- datalink:GetDetectedItemCoordinates() -- #table of contact coordinates, to be compatible with @{Functional.Detection#DETECTION}. -- --- Gather data with the event function: --- function datalink:OnAfterCollected(From, Event, To, Contacts, Clusters) +-- Gather data with the event function: +-- function datalink:OnAfterCollected(From, Event, To, Contacts, Clusters) -- ... ... -- end --- +-- function INTEL_DLINK:New(Intels, Alias, Interval, Cachetime) -- Inherit everything from FSM class. local self=BASE:Inherit(self, FSM:New()) -- #INTEL - + self.intels = Intels or {} self.contacts = {} self.clusters = {} self.contactcoords = {} - + -- Set alias. if Alias then self.alias=tostring(Alias) else self.alias="SPECTRE" end - + -- Cache time self.cachetime = Cachetime or 300 - + -- Interval self.interval = Interval or 20 - + -- Set some string id for output to DCS.log file. self.lid=string.format("INTEL_DLINK %s | ", self.alias) - + -- Start State. self:SetStartState("Stopped") @@ -1554,7 +1570,7 @@ function INTEL_DLINK:New(Intels, Alias, Interval, Cachetime) self:AddTransition("*", "Collect", "*") -- Collect data. self:AddTransition("*", "Collected", "*") -- Collection of data done. self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. - + ---------------------------------------------------------------------------------------------- -- Pseudo Functions ---------------------------------------------------------------------------------------------- @@ -1583,7 +1599,7 @@ function INTEL_DLINK:New(Intels, Alias, Interval, Cachetime) -- @function [parent=#INTEL_DLINK] __Status -- @param #INTEL_DLINK self -- @param #number delay Delay in seconds. - + --- On After "Collected" event. Data tables have been refreshed. -- @function [parent=#INTEL_DLINK] OnAfterCollected -- @param #INTEL_DLINK self @@ -1592,7 +1608,7 @@ function INTEL_DLINK:New(Intels, Alias, Interval, Cachetime) -- @param #string To To state. -- @param #table Contacts Table of #INTEL.Contact Contacts. -- @param #table Clusters Table of #INTEL.Cluster Clusters. - + return self end ---------------------------------------------------------------------------------------------- @@ -1689,7 +1705,7 @@ function INTEL_DLINK:onbeforeCollect(From, Event, To) self:__Collected(1, contacttable, newclusters) -- make table available via FSM Event -- schedule next round local interv = self.interval * -1 - self:__Collect(interv) + self:__Collect(interv) return self end diff --git a/Moose Development/Moose/Ops/Legion.lua b/Moose Development/Moose/Ops/Legion.lua new file mode 100644 index 000000000..d85b696da --- /dev/null +++ b/Moose Development/Moose/Ops/Legion.lua @@ -0,0 +1,2516 @@ +--- **Ops** - Legion Warehouse. +-- +-- Parent class of Airwings, Brigades and Fleets. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Ops.Legion +-- @image OPS_Legion.png + + +--- LEGION class. +-- @type LEGION +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity of output. +-- @field #string lid Class id string for output to DCS log file. +-- @field #table missionqueue Mission queue table. +-- @field #table transportqueue Transport queue. +-- @field #table cohorts Cohorts of this legion. +-- @field Ops.Commander#COMMANDER commander Commander of this legion. +-- @field Ops.Chief#CHIEF chief Chief of this legion. +-- @extends Functional.Warehouse#WAREHOUSE + +--- *Per aspera ad astra* +-- +-- === +-- +-- # The LEGION Concept +-- +-- The LEGION class contains all functions that are common for the AIRWING, BRIGADE and FLEET classes, which inherit the LEGION class. +-- +-- An LEGION consists of multiple COHORTs. These cohorts "live" in a WAREHOUSE, i.e. a physical structure that can be destroyed or captured. +-- +-- ** The LEGION class is not meant to be used directly. Use AIRWING, BRIGADE or FLEET instead! ** +-- +-- @field #LEGION +LEGION = { + ClassName = "LEGION", + verbose = 0, + lid = nil, + missionqueue = {}, + transportqueue = {}, + cohorts = {}, +} + +--- LEGION class version. +-- @field #string version +LEGION.version="0.1.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ToDo list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Create FLEED class. +-- DONE: OPS transport. +-- DONE: Make general so it can be inherited by AIRWING and BRIGADE classes. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new LEGION class object. +-- @param #LEGION self +-- @param #string WarehouseName Name of the warehouse STATIC or UNIT object representing the warehouse. +-- @param #string LegionName Name of the legion. +-- @return #LEGION self +function LEGION:New(WarehouseName, LegionName) + + -- Inherit everything from WAREHOUSE class. + local self=BASE:Inherit(self, WAREHOUSE:New(WarehouseName, LegionName)) -- #LEGION + + -- Nil check. + if not self then + BASE:E(string.format("ERROR: Could not find warehouse %s!", WarehouseName)) + return nil + end + + -- Set some string id for output to DCS.log file. + self.lid=string.format("LEGION %s | ", self.alias) + + -- Defaults: + -- TODO: What? + self:SetMarker(false) + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("*", "MissionRequest", "*") -- Add a (mission) request to the warehouse. + self:AddTransition("*", "MissionCancel", "*") -- Cancel mission. + self:AddTransition("*", "MissionAssign", "*") -- Recruit assets, add to queue and request immediately. + + self:AddTransition("*", "TransportRequest", "*") -- Add a (mission) request to the warehouse. + self:AddTransition("*", "TransportCancel", "*") -- Cancel transport. + self:AddTransition("*", "TransportAssign", "*") -- Recruit assets, add to queue and request immediately. + + self:AddTransition("*", "OpsOnMission", "*") -- An OPSGROUP was send on a Mission (AUFTRAG). + + self:AddTransition("*", "LegionAssetReturned", "*") -- An asset returned (from a mission) to the Legion warehouse. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the LEGION. Initializes parameters and starts event handlers. + -- @function [parent=#LEGION] Start + -- @param #LEGION self + + --- Triggers the FSM event "Start" after a delay. Starts the LEGION. Initializes parameters and starts event handlers. + -- @function [parent=#LEGION] __Start + -- @param #LEGION self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the LEGION and all its event handlers. + -- @param #LEGION self + + --- Triggers the FSM event "Stop" after a delay. Stops the LEGION and all its event handlers. + -- @function [parent=#LEGION] __Stop + -- @param #LEGION self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "MissionCancel". + -- @function [parent=#LEGION] MissionCancel + -- @param #LEGION self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + + --- Triggers the FSM event "MissionAssign". + -- @function [parent=#LEGION] MissionAssign + -- @param #LEGION self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + -- @param #table Legions The legion(s) from which the mission assets are requested. + + --- Triggers the FSM event "MissionAssign" after a delay. + -- @function [parent=#LEGION] __MissionAssign + -- @param #LEGION self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + -- @param #table Legions The legion(s) from which the mission assets are requested. + + --- On after "MissionAssign" event. + -- @function [parent=#LEGION] OnAfterMissionAssign + -- @param #LEGION self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + -- @param #table Legions The legion(s) from which the mission assets are requested. + + + --- Triggers the FSM event "MissionRequest". + -- @function [parent=#LEGION] MissionRequest + -- @param #LEGION self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "MissionRequest" after a delay. + -- @function [parent=#LEGION] __MissionRequest + -- @param #LEGION self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "MissionRequest" event. + -- @function [parent=#LEGION] OnAfterMissionRequest + -- @param #LEGION self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + + --- Triggers the FSM event "MissionCancel" after a delay. + -- @function [parent=#LEGION] __MissionCancel + -- @param #LEGION self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "MissionCancel" event. + -- @function [parent=#LEGION] OnAfterMissionCancel + -- @param #LEGION self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + + --- Triggers the FSM event "TransportAssign". + -- @function [parent=#LEGION] TransportAssign + -- @param #LEGION self + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + -- @param #table Legions The legion(s) to which this transport is assigned. + + --- Triggers the FSM event "TransportAssign" after a delay. + -- @function [parent=#LEGION] __TransportAssign + -- @param #LEGION self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + -- @param #table Legions The legion(s) to which this transport is assigned. + + --- On after "TransportAssign" event. + -- @function [parent=#LEGION] OnAfterTransportAssign + -- @param #LEGION self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + -- @param #table Legions The legion(s) to which this transport is assigned. + + + --- Triggers the FSM event "TransportRequest". + -- @function [parent=#LEGION] TransportRequest + -- @param #LEGION self + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + --- Triggers the FSM event "TransportRequest" after a delay. + -- @function [parent=#LEGION] __TransportRequest + -- @param #LEGION self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + --- On after "TransportRequest" event. + -- @function [parent=#LEGION] OnAfterTransportRequest + -- @param #LEGION self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + + --- Triggers the FSM event "TransportCancel". + -- @function [parent=#LEGION] TransportCancel + -- @param #LEGION self + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + --- Triggers the FSM event "TransportCancel" after a delay. + -- @function [parent=#LEGION] __TransportCancel + -- @param #LEGION self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + --- On after "TransportCancel" event. + -- @function [parent=#LEGION] OnAfterTransportCancel + -- @param #LEGION self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + + --- Triggers the FSM event "OpsOnMission". + -- @function [parent=#LEGION] OpsOnMission + -- @param #LEGION self + -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "OpsOnMission" after a delay. + -- @function [parent=#LEGION] __OpsOnMission + -- @param #LEGION self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "OpsOnMission" event. + -- @function [parent=#LEGION] OnAfterOpsOnMission + -- @param #LEGION self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + + --- Triggers the FSM event "LegionAssetReturned". + -- @function [parent=#LEGION] LegionAssetReturned + -- @param #LEGION self + -- @param Ops.Cohort#COHORT Cohort The cohort the asset belongs to. + -- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset that returned. + + --- Triggers the FSM event "LegionAssetReturned" after a delay. + -- @function [parent=#LEGION] __LegionAssetReturned + -- @param #LEGION self + -- @param #number delay Delay in seconds. + -- @param Ops.Cohort#COHORT Cohort The cohort the asset belongs to. + -- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset that returned. + + --- On after "LegionAssetReturned" event. Triggered when an asset group returned to its Legion. + -- @function [parent=#LEGION] OnAfterLegionAssetReturned + -- @param #LEGION self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Cohort#COHORT Cohort The cohort the asset belongs to. + -- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset that returned. + + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set verbosity level. +-- @param #LEGION self +-- @param #number VerbosityLevel Level of output (higher=more). Default 0. +-- @return #LEGION self +function LEGION:SetVerbosity(VerbosityLevel) + self.verbose=VerbosityLevel or 0 + return self +end + +--- Add a mission for the legion. It will pick the best available assets for the mission and lauch it when ready. +-- @param #LEGION self +-- @param Ops.Auftrag#AUFTRAG Mission Mission for this legion. +-- @return #LEGION self +function LEGION:AddMission(Mission) + + -- Set status to QUEUED. This event is only allowed for the first legion that calls it. + Mission:Queued() + + -- Set legion status. + Mission:SetLegionStatus(self, AUFTRAG.Status.QUEUED) + + -- Add legion to mission. + Mission:AddLegion(self) + + -- Set target for ALERT 5. + if Mission.type==AUFTRAG.Type.ALERT5 then + Mission:_TargetFromObject(self:GetCoordinate()) + end + + -- Add mission to queue. + table.insert(self.missionqueue, Mission) + + -- Info text. + local text=string.format("Added mission %s (type=%s). Starting at %s. Stopping at %s", + tostring(Mission.name), tostring(Mission.type), UTILS.SecondsToClock(Mission.Tstart, true), Mission.Tstop and UTILS.SecondsToClock(Mission.Tstop, true) or "INF") + self:T(self.lid..text) + + return self +end + +--- Remove mission from queue. +-- @param #LEGION self +-- @param Ops.Auftrag#AUFTRAG Mission Mission to be removed. +-- @return #LEGION self +function LEGION:RemoveMission(Mission) + + for i,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission.auftragsnummer==Mission.auftragsnummer then + mission:RemoveLegion(self) + table.remove(self.missionqueue, i) + break + end + + end + + return self +end + +--- Add transport assignment to queue. +-- @param #LEGION self +-- @param Ops.OpsTransport#OPSTRANSPORT OpsTransport Transport assignment. +-- @return #LEGION self +function LEGION:AddOpsTransport(OpsTransport) + + -- Is not queued at a legion. + OpsTransport:Queued() + + -- Set legion status. + OpsTransport:SetLegionStatus(self, AUFTRAG.Status.QUEUED) + + -- Add mission to queue. + table.insert(self.transportqueue, OpsTransport) + + -- Add this legion to the transport. + OpsTransport:AddLegion(self) + + -- Info text. + local text=string.format("Added Transport %s. Starting at %s-%s", + tostring(OpsTransport.uid), UTILS.SecondsToClock(OpsTransport.Tstart, true), OpsTransport.Tstop and UTILS.SecondsToClock(OpsTransport.Tstop, true) or "INF") + self:T(self.lid..text) + + return self +end + + +--- Get cohort by name. +-- @param #LEGION self +-- @param #string CohortName Name of the platoon. +-- @return Ops.Cohort#COHORT The Cohort object. +function LEGION:_GetCohort(CohortName) + + for _,_cohort in pairs(self.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + + if cohort.name==CohortName then + return cohort + end + + end + + return nil +end + +--- Get cohort of an asset. +-- @param #LEGION self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset. +-- @return Ops.Cohort#COHORT The Cohort object. +function LEGION:_GetCohortOfAsset(Asset) + local cohort=self:_GetCohort(Asset.squadname) + return cohort +end + + +--- Check if a BRIGADE class is calling. +-- @param #LEGION self +-- @return #boolean If true, this is a BRIGADE. +function LEGION:IsBrigade() + local is=self.ClassName==BRIGADE.ClassName + return is +end + +--- Check if the AIRWING class is calling. +-- @param #LEGION self +-- @return #boolean If true, this is an AIRWING. +function LEGION:IsAirwing() + local is=self.ClassName==AIRWING.ClassName + return is +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Start & Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Start LEGION FSM. +-- @param #LEGION self +function LEGION:onafterStart(From, Event, To) + + -- Start parent Warehouse. + self:GetParent(self, LEGION).onafterStart(self, From, Event, To) + + -- Info. + self:T3(self.lid..string.format("Starting LEGION v%s", LEGION.version)) + +end + +--- Check mission queue and assign ONE mission. +-- @param #LEGION self +-- @return #boolean If `true`, a mission was found and requested. +function LEGION:CheckMissionQueue() + + -- Number of missions. + local Nmissions=#self.missionqueue + + -- Treat special cases. + if Nmissions==0 then + return nil + end + + -- Loop over missions in queue. + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission:IsNotOver() and mission:IsReadyToCancel() then + mission:Cancel() + end + end + + -- Sort results table wrt prio and start time. + local function _sort(a, b) + local taskA=a --Ops.Auftrag#AUFTRAG + local taskB=b --Ops.Auftrag#AUFTRAG + return (taskA.prio Request and return. + self:TransportRequest(transport) + return true + end + + end + end + + -- No transport found. + return nil +end + + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Events +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "MissionAssign" event. Mission is added to a LEGION mission queue and already requested. Needs assets to be added to the mission already. +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @param #table Legions The LEGIONs. +function LEGION:onafterMissionAssign(From, Event, To, Mission, Legions) + + for _,_Legion in pairs(Legions) do + local Legion=_Legion --Ops.Legion#LEGION + + -- Debug info. + self:I(self.lid..string.format("Assigning mission %s (%s) to legion %s", Mission.name, Mission.type, Legion.alias)) + + -- Add mission to legion. + Legion:AddMission(Mission) + + -- Directly request the mission as the assets have already been selected. + Legion:MissionRequest(Mission) + + end + +end + + +--- On after "MissionRequest" event. Performs a self request to the warehouse for the mission assets. Sets mission status to REQUESTED. +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Auftrag#AUFTRAG Mission The requested mission. +function LEGION:onafterMissionRequest(From, Event, To, Mission) + + -- Set mission status from QUEUED to REQUESTED. + Mission:Requested() + + -- Set legion status. Ensures that it is not considered in the next selection. + Mission:SetLegionStatus(self, AUFTRAG.Status.REQUESTED) + + --- + -- Some assets might already be spawned and even on a different mission (orbit). + -- Need to dived to set into spawned and instock assets and handle the other + --- + + -- Assets to be requested. + local Assetlist={} + + for _,_asset in pairs(Mission.assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + -- Check that this asset belongs to this Legion warehouse. + if asset.wid==self.uid then + + if asset.spawned then + + if asset.flightgroup then + + -- Add new mission. + asset.flightgroup:AddMission(Mission) + + --- + -- Special Missions + --- + + local currM=asset.flightgroup:GetMissionCurrent() + + -- Check if mission is INTERCEPT and asset is currently on GCI mission. If so, GCI is paused. + if Mission.type==AUFTRAG.Type.INTERCEPT then + if currM and currM.type==AUFTRAG.Type.GCICAP then + self:I(self.lid..string.format("Pausing %s mission %s to send flight on intercept mission %s", currM.type, currM.name, Mission.name)) + asset.flightgroup:PauseMission() + end + end + + -- Cancel the current ALERT 5 mission. + if currM and currM.type==AUFTRAG.Type.ALERT5 then + asset.flightgroup:MissionCancel(currM) + end + + + -- Trigger event. + self:__OpsOnMission(5, asset.flightgroup, Mission) + + else + self:E(self.lid.."ERROR: flight group for asset does NOT exist!") + end + + else + -- These assets need to be requested and spawned. + table.insert(Assetlist, asset) + end + + end + end + + -- Add request to legion warehouse. + if #Assetlist>0 then + + --local text=string.format("Requesting assets for mission %s:", Mission.name) + for i,_asset in pairs(Assetlist) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + -- Set asset to requested! Important so that new requests do not use this asset! + asset.requested=true + asset.isReserved=false + + -- Set missin task so that the group is spawned with the right one. + if Mission.missionTask then + asset.missionTask=Mission.missionTask + end + + end + + -- TODO: Get/set functions for assignment string. + local assignment=string.format("Mission-%d", Mission.auftragsnummer) + + -- Add request to legion warehouse. + self:AddRequest(self, WAREHOUSE.Descriptor.ASSETLIST, Assetlist, #Assetlist, nil, nil, Mission.prio, assignment) + + -- The queueid has been increased in the onafterAddRequest function. So we can simply use it here. + Mission.requestID[self.alias]=self.queueid + + -- Get request. + local request=self:GetRequestByID(self.queueid) + + if request then + if self:IsShip() then + self:T(self.lid.."Warehouse phyiscal structure is SHIP. Requestes assets will be late activated!") + request.lateActivation=true + end + end + + end + +end + +--- On after "TransportAssign" event. Transport is added to a LEGION transport queue and assets are requested from the LEGION warehouse. +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. +-- @param #table Legions The legion(s) to which the transport is assigned. +function LEGION:onafterTransportAssign(From, Event, To, Transport, Legions) + + for _,_Legion in pairs(Legions) do + local Legion=_Legion --Ops.Legion#LEGION + + -- Debug info. + self:I(self.lid..string.format("Assigning transport %d to legion %s", Transport.uid, Legion.alias)) + + -- Add mission to legion. + Legion:AddOpsTransport(Transport) + + -- Directly request the mission as the assets have already been selected. + Legion:TransportRequest(Transport) + + end + +end + +--- On after "TransportRequest" event. Performs a self request to the warehouse for the transport assets. Sets transport status to REQUESTED. +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsTransport#OPSTRANSPORT Opstransport The requested mission. +function LEGION:onafterTransportRequest(From, Event, To, OpsTransport) + + -- List of assets that will be requested. + local AssetList={} + + --TODO: Find spawned assets on ALERT 5 mission OPSTRANSPORT. + + --local text=string.format("Requesting assets for mission %s:", Mission.name) + for i,_asset in pairs(OpsTransport.assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + -- Check that this asset belongs to this Legion warehouse. + if asset.wid==self.uid then + + -- Set asset to requested! Important so that new requests do not use this asset! + asset.requested=true + asset.isReserved=false + + -- Set transport mission task. + asset.missionTask=ENUMS.MissionTask.TRANSPORT + + -- Add asset to list. + table.insert(AssetList, asset) + end + end + + if #AssetList>0 then + + -- Set mission status from QUEUED to REQUESTED. + OpsTransport:Requested() + + -- Set legion status. Ensures that it is not considered in the next selection. + OpsTransport:SetLegionStatus(self, OPSTRANSPORT.Status.REQUESTED) + + -- TODO: Get/set functions for assignment string. + local assignment=string.format("Transport-%d", OpsTransport.uid) + + -- Add request to legion warehouse. + self:AddRequest(self, WAREHOUSE.Descriptor.ASSETLIST, AssetList, #AssetList, nil, nil, OpsTransport.prio, assignment) + + -- The queueid has been increased in the onafterAddRequest function. So we can simply use it here. + OpsTransport.requestID[self.alias]=self.queueid + end + +end + +--- On after "TransportCancel" event. +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport to be cancelled. +function LEGION:onafterTransportCancel(From, Event, To, Transport) + + -- Info message. + self:I(self.lid..string.format("Cancel transport UID=%d", Transport.uid)) + + -- Set status to cancelled. + Transport:SetLegionStatus(self, OPSTRANSPORT.Status.CANCELLED) + + for i=#Transport.assets, 1, -1 do + local asset=Transport.assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem + + -- Asset should belong to this legion. + if asset.wid==self.uid then + + local opsgroup=asset.flightgroup + + if opsgroup then + opsgroup:TransportCancel(Transport) + end + + -- Delete awaited transport. + local cargos=Transport:GetCargoOpsGroups(false) + for _,_cargo in pairs(cargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP + cargo:_DelMyLift(Transport) + end + + -- Remove asset from mission. + Transport:DelAsset(asset) + + -- Not requested any more (if it was). + asset.requested=nil + asset.isReserved=nil + + end + end + + -- Remove queued request (if any). + if Transport.requestID[self.alias] then + self:_DeleteQueueItemByID(Transport.requestID[self.alias], self.queue) + end + +end + + +--- On after "MissionCancel" event. Cancels the missions of all flightgroups. Deletes request from warehouse queue. +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Auftrag#AUFTRAG Mission The mission to be cancelled. +function LEGION:onafterMissionCancel(From, Event, To, Mission) + + -- Info message. + self:I(self.lid..string.format("Cancel mission %s", Mission.name)) + + -- Set status to cancelled. + Mission:SetLegionStatus(self, AUFTRAG.Status.CANCELLED) + + for i=#Mission.assets, 1, -1 do + local asset=Mission.assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem + + -- Asset should belong to this legion. + if asset.wid==self.uid then + + local opsgroup=asset.flightgroup + + if opsgroup then + opsgroup:MissionCancel(Mission) + end + + -- Remove asset from mission. + Mission:DelAsset(asset) + + -- Not requested any more (if it was). + asset.requested=nil + asset.isReserved=nil + + end + end + + -- Remove queued request (if any). + if Mission.requestID[self.alias] then + self:_DeleteQueueItemByID(Mission.requestID[self.alias], self.queue) + end + +end + +--- On after "OpsOnMission". +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsGroup#OPSGROUP OpsGroup Ops group on mission +-- @param Ops.Auftrag#AUFTRAG Mission The requested mission. +function LEGION:onafterOpsOnMission(From, Event, To, OpsGroup, Mission) + -- Debug info. + self:T2(self.lid..string.format("Group %s on %s mission %s", OpsGroup:GetName(), Mission:GetType(), Mission:GetName())) + + if self:IsAirwing() then + -- Trigger event for Airwings. + self:FlightOnMission(OpsGroup, Mission) + elseif self:IsBrigade() then + -- Trigger event for Brigades. + self:ArmyOnMission(OpsGroup, Mission) + else + --TODO: Flotilla + end + + -- Trigger event for chief. + if self.chief then + self.chief:OpsOnMission(OpsGroup, Mission) + end + + -- Trigger event for commander. + if self.commander then + self.commander:OpsOnMission(OpsGroup, Mission) + end + +end + +--- On after "NewAsset" event. Asset is added to the given cohort (asset assignment). +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset The asset that has just been added. +-- @param #string assignment The (optional) assignment for the asset. +function LEGION:onafterNewAsset(From, Event, To, asset, assignment) + + -- Call parent WAREHOUSE function first. + self:GetParent(self, LEGION).onafterNewAsset(self, From, Event, To, asset, assignment) + + -- Debug text. + local text=string.format("New asset %s with assignment %s and request assignment %s", asset.spawngroupname, tostring(asset.assignment), tostring(assignment)) + self:T3(self.lid..text) + + -- Get cohort. + local cohort=self:_GetCohort(asset.assignment) + + -- Check if asset is already part of the squadron. If an asset returns, it will be added again! We check that asset.assignment is also assignment. + if cohort then + + if asset.assignment==assignment then + + --- + -- Asset is added to the COHORT for the first time + --- + + local nunits=#asset.template.units + + -- Debug text. + local text=string.format("Adding asset to squadron %s: assignment=%s, type=%s, attribute=%s, nunits=%d %s", cohort.name, assignment, asset.unittype, asset.attribute, nunits, tostring(cohort.ngrouping)) + self:T(self.lid..text) + + -- Adjust number of elements in the group. + if cohort.ngrouping then + local template=asset.template + + local N=math.max(#template.units, cohort.ngrouping) + + -- Handle units. + for i=1,N do + + -- Unit template. + local unit = template.units[i] + + -- If grouping is larger than units present, copy first unit. + if i>nunits then + table.insert(template.units, UTILS.DeepCopy(template.units[1])) + end + + -- Remove units if original template contains more than in grouping. + if cohort.ngroupingnunits then + unit=nil + end + end + + asset.nunits=cohort.ngrouping + end + + -- Set takeoff type. + asset.takeoffType=cohort.takeoffType + + -- Set parking IDs. + asset.parkingIDs=cohort.parkingIDs + + -- Create callsign and modex (needs to be after grouping). + cohort:GetCallsign(asset) + cohort:GetModex(asset) + + -- Set spawn group name. This has to include "AID-" for warehouse. + asset.spawngroupname=string.format("%s_AID-%d", cohort.name, asset.uid) + + -- Add asset to cohort. + cohort:AddAsset(asset) + + else + + --- + -- Asset is returned to the COHORT + --- + + -- Trigger event. + self:LegionAssetReturned(cohort, asset) + + end + + end +end + +--- On after "LegionAssetReturned" event. Triggered when an asset group returned to its legion. +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Cohort#COHORT Cohort The cohort the asset belongs to. +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset that returned. +function LEGION:onafterLegionAssetReturned(From, Event, To, Cohort, Asset) + -- Debug message. + self:T(self.lid..string.format("Asset %s from Cohort %s returned! asset.assignment=\"%s\"", Asset.spawngroupname, Cohort.name, tostring(Asset.assignment))) + + -- Stop flightgroup. + if Asset.flightgroup and not Asset.flightgroup:IsStopped() then + Asset.flightgroup:Stop() + end + + -- Return payload. + if Asset.flightgroup:IsFlightgroup() then + self:ReturnPayloadFromAsset(Asset) + end + + -- Return tacan channel. + if Asset.tacan then + Cohort:ReturnTacan(Asset.tacan) + end + + -- Set timestamp. + Asset.Treturned=timer.getAbsTime() + +end + + +--- On after "AssetSpawned" event triggered when an asset group is spawned into the cruel world. +-- Creates a new flightgroup element and adds the mission to the flightgroup queue. +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Group#GROUP group The group spawned. +-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset The asset that was spawned. +-- @param Functional.Warehouse#WAREHOUSE.Pendingitem request The request of the dead asset. +function LEGION:onafterAssetSpawned(From, Event, To, group, asset, request) + + -- Call parent warehouse function first. + self:GetParent(self, LEGION).onafterAssetSpawned(self, From, Event, To, group, asset, request) + + -- Get the COHORT of the asset. + local cohort=self:_GetCohortOfAsset(asset) + + -- Check if we have a cohort or if this was some other request. + if cohort then + + -- Debug info. + self:T(self.lid..string.format("Cohort asset spawned %s", asset.spawngroupname)) + + -- Create a flight group. + local flightgroup=self:_CreateFlightGroup(asset) + + --- + -- Asset + --- + + -- Set asset flightgroup. + asset.flightgroup=flightgroup + + -- Not requested any more. + asset.requested=nil + + -- Did not return yet. + asset.Treturned=nil + + --- + -- Cohort + --- + + -- Get TACAN channel. + local Tacan=cohort:FetchTacan() + if Tacan then + asset.tacan=Tacan + --flightgroup:SetDefaultTACAN(Tacan,Morse,UnitName,Band,OffSwitch) + flightgroup:SwitchTACAN(Tacan, Morse, UnitName, Band) + end + + -- Set radio frequency and modulation + local radioFreq, radioModu=cohort:GetRadio() + if radioFreq then + flightgroup:SwitchRadio(radioFreq, radioModu) + end + + if cohort.fuellow then + flightgroup:SetFuelLowThreshold(cohort.fuellow) + end + + if cohort.fuellowRefuel then + flightgroup:SetFuelLowRefuel(cohort.fuellowRefuel) + end + + -- Assignment. + local assignment=request.assignment + + if string.find(assignment, "Mission-") then + + --- + -- Mission + --- + + local uid=UTILS.Split(assignment, "-")[2] + + -- Get Mission (if any). + local mission=self:GetMissionByID(uid) + + -- Add mission to flightgroup queue. + if mission then + + if Tacan then + --mission:SetTACAN(Tacan, Morse, UnitName, Band) + end + + -- Add mission to flightgroup queue. If mission has an OPSTRANSPORT attached, all added OPSGROUPS are added as CARGO for a transport. + flightgroup:AddMission(mission) + + -- Trigger event. + self:__OpsOnMission(5, flightgroup, mission) + + else + + if Tacan then + --flightgroup:SwitchTACAN(Tacan, Morse, UnitName, Band) + end + + end + + -- Add group to the detection set of the CHIEF (INTEL). + local chief=self.chief or (self.commander and self.commander.chief or nil) --Ops.Chief#CHIEF + if chief then + self:T(self.lid..string.format("Adding group %s to agents of CHIEF", group:GetName())) + chief.detectionset:AddGroup(asset.flightgroup.group) + end + + elseif string.find(assignment, "Transport-") then + + --- + -- Transport + --- + + local uid=UTILS.Split(assignment, "-")[2] + + -- Get Mission (if any). + local transport=self:GetTransportByID(uid) + + -- Add mission to flightgroup queue. + if transport then + flightgroup:AddOpsTransport(transport) + end + + end + + end + +end + +--- On after "AssetDead" event triggered when an asset group died. +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset The asset that is dead. +-- @param Functional.Warehouse#WAREHOUSE.Pendingitem request The request of the dead asset. +function LEGION:onafterAssetDead(From, Event, To, asset, request) + + -- Call parent warehouse function first. + self:GetParent(self, LEGION).onafterAssetDead(self, From, Event, To, asset, request) + + -- Remove group from the detection set of the CHIEF (INTEL). + if self.commander and self.commander.chief then + self.commander.chief.detectionset:RemoveGroupsByName({asset.spawngroupname}) + end + + -- Remove asset from mission is done via Mission:AssetDead() call from flightgroup onafterFlightDead function + -- Remove asset from squadron same +end + +--- On after "Destroyed" event. Remove assets from cohorts. Stop cohorts. +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function LEGION:onafterDestroyed(From, Event, To) + + -- Debug message. + self:I(self.lid.."Legion warehouse destroyed!") + + -- Cancel all missions. + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + mission:Cancel() + end + + -- Remove all cohort assets. + for _,_cohort in pairs(self.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + -- Stop Cohort. This also removes all assets. + cohort:Stop() + end + + -- Call parent warehouse function first. + self:GetParent(self, LEGION).onafterDestroyed(self, From, Event, To) + +end + + +--- On after "Request" event. +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Functional.Warehouse#WAREHOUSE.Queueitem Request Information table of the request. +function LEGION:onafterRequest(From, Event, To, Request) + + -- Assets + local assets=Request.cargoassets + + -- Get Mission + local Mission=self:GetMissionByID(Request.assignment) + + if Mission and assets then + + for _,_asset in pairs(assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + -- This would be the place to modify the asset table before the asset is spawned. + end + + end + + -- Call parent warehouse function after assets have been adjusted. + self:GetParent(self, LEGION).onafterRequest(self, From, Event, To, Request) + +end + +--- On after "SelfRequest" event. +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Set#SET_GROUP groupset The set of asset groups that was delivered to the warehouse itself. +-- @param Functional.Warehouse#WAREHOUSE.Pendingitem request Pending self request. +function LEGION:onafterSelfRequest(From, Event, To, groupset, request) + + -- Call parent warehouse function first. + self:GetParent(self, LEGION).onafterSelfRequest(self, From, Event, To, groupset, request) + + -- Get Mission + local mission=self:GetMissionByID(request.assignment) + + for _,_asset in pairs(request.assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + end + + for _,_group in pairs(groupset:GetSet()) do + local group=_group --Wrapper.Group#GROUP + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Mission Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new flight group after an asset was spawned. +-- @param #LEGION self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset The asset. +-- @return Ops.FlightGroup#FLIGHTGROUP The created flightgroup object. +function LEGION:_CreateFlightGroup(asset) + + -- Create flightgroup. + local opsgroup=nil --Ops.OpsGroup#OPSGROUP + + if self:IsAirwing() then + + --- + -- FLIGHTGROUP + --- + + opsgroup=FLIGHTGROUP:New(asset.spawngroupname) + + elseif self:IsBrigade() then + + --- + -- ARMYGROUP + --- + + opsgroup=ARMYGROUP:New(asset.spawngroupname) + + else + self:E(self.lid.."ERROR: not airwing or brigade!") + end + + -- Set legion. + opsgroup:_SetLegion(self) + + -- Set cohort. + opsgroup.cohort=self:_GetCohortOfAsset(asset) + + -- Set home base. + opsgroup.homebase=self.airbase + + -- Set home zone. + opsgroup.homezone=self.spawnzone + + -- Set weapon data. + if opsgroup.cohort.weaponData then + local text="Weapon data for group:" + opsgroup.weaponData=opsgroup.weaponData or {} + for bittype,_weapondata in pairs(opsgroup.cohort.weaponData) do + local weapondata=_weapondata --Ops.OpsGroup#OPSGROUP.WeaponData + opsgroup.weaponData[bittype]=UTILS.DeepCopy(weapondata) -- Careful with the units. + text=text..string.format("\n- Bit=%s: Rmin=%.1f km, Rmax=%.1f km", bittype, weapondata.RangeMin/1000, weapondata.RangeMax/1000) + end + self:T3(self.lid..text) + end + + return opsgroup +end + + +--- Check if an asset is currently on a mission (STARTED or EXECUTING). +-- @param #LEGION self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset The asset. +-- @param #table MissionTypes Types on mission to be checked. Default all. +-- @return #boolean If true, asset has at least one mission of that type in the queue. +function LEGION:IsAssetOnMission(asset, MissionTypes) + + if MissionTypes then + if type(MissionTypes)~="table" then + MissionTypes={MissionTypes} + end + else + -- Check all possible types. + MissionTypes=AUFTRAG.Type + end + + if asset.flightgroup and asset.flightgroup:IsAlive() then + + -- Loop over mission queue. + for _,_mission in pairs(asset.flightgroup.missionqueue or {}) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission:IsNotOver() then + + -- Get flight status. + local status=mission:GetGroupStatus(asset.flightgroup) + + -- Only if mission is started or executing. + if (status==AUFTRAG.GroupStatus.STARTED or status==AUFTRAG.GroupStatus.EXECUTING) and AUFTRAG.CheckMissionType(mission.type, MissionTypes) then + return true + end + + end + + end + + end + + -- Alternative: run over all missions and compare to mission assets. + --[[ + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission:IsNotOver() then + for _,_asset in pairs(mission.assets) do + local sqasset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + if sqasset.uid==asset.uid then + return true + end + + end + end + + end + ]] + + return false +end + +--- Get the current mission of the asset. +-- @param #LEGION self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset The asset. +-- @return Ops.Auftrag#AUFTRAG Current mission or *nil*. +function LEGION:GetAssetCurrentMission(asset) + + if asset.flightgroup then + return asset.flightgroup:GetMissionCurrent() + end + + return nil +end + +--- Count payloads in stock. +-- @param #LEGION self +-- @param #table MissionTypes Types on mission to be checked. Default *all* possible types `AUFTRAG.Type`. +-- @param #table UnitTypes Types of units. +-- @param #table Payloads Specific payloads to be counted only. +-- @return #number Count of available payloads in stock. +function LEGION:CountPayloadsInStock(MissionTypes, UnitTypes, Payloads) + + if MissionTypes then + if type(MissionTypes)=="string" then + MissionTypes={MissionTypes} + end + end + + if UnitTypes then + if type(UnitTypes)=="string" then + UnitTypes={UnitTypes} + end + end + + local function _checkUnitTypes(payload) + if UnitTypes then + for _,unittype in pairs(UnitTypes) do + if unittype==payload.aircrafttype then + return true + end + end + else + -- Unit type was not specified. + return true + end + return false + end + + local function _checkPayloads(payload) + if Payloads then + for _,Payload in pairs(Payloads) do + if Payload.uid==payload.uid then + return true + end + end + else + -- Payload was not specified. + return nil + end + return false + end + + local n=0 + for _,_payload in pairs(self.payloads or {}) do + local payload=_payload --Ops.Airwing#AIRWING.Payload + + for _,MissionType in pairs(MissionTypes) do + + local specialpayload=_checkPayloads(payload) + local compatible=AUFTRAG.CheckMissionCapability(MissionType, payload.capabilities) + + local goforit = specialpayload or (specialpayload==nil and compatible) + + if goforit and _checkUnitTypes(payload) then + + if payload.unlimited then + -- Payload is unlimited. Return a BIG number. + return 999 + else + n=n+payload.navail + end + + end + + end + end + + return n +end + +--- Count missions in mission queue. +-- @param #LEGION self +-- @param #table MissionTypes Types on mission to be checked. Default *all* possible types `AUFTRAG.Type`. +-- @return #number Number of missions that are not over yet. +function LEGION:CountMissionsInQueue(MissionTypes) + + MissionTypes=MissionTypes or AUFTRAG.Type + + local N=0 + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + -- Check if this mission type is requested. + if mission:IsNotOver() and AUFTRAG.CheckMissionType(mission.type, MissionTypes) then + N=N+1 + end + + end + + return N +end + +--- Count total number of assets of the legion. +-- @param #LEGION self +-- @param #boolean InStock If true, only assets that are in the warehouse stock/inventory are counted. +-- @param #table MissionTypes (Optional) Count only assest that can perform certain mission type(s). Default is all types. +-- @param #table Attributes (Optional) Count only assest that have a certain attribute(s), e.g. `WAREHOUSE.Attribute.AIR_BOMBER`. +-- @return #number Amount of asset groups in stock. +function LEGION:CountAssets(InStock, MissionTypes, Attributes) + + local N=0 + + for _,_cohort in pairs(self.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + N=N+cohort:CountAssets(InStock, MissionTypes, Attributes) + end + + return N +end + +--- Count total number of assets in LEGION warehouse stock that also have a payload. +-- @param #LEGION self +-- @param #boolean Payloads (Optional) Specifc payloads to consider. Default all. +-- @param #table MissionTypes (Optional) Count only assest that can perform certain mission type(s). Default is all types. +-- @param #table Attributes (Optional) Count only assest that have a certain attribute(s), e.g. `WAREHOUSE.Attribute.AIR_BOMBER`. +-- @return #number Amount of asset groups in stock. +function LEGION:CountAssetsWithPayloadsInStock(Payloads, MissionTypes, Attributes) + + -- Total number counted. + local N=0 + + -- Number of payloads in stock per aircraft type. + local Npayloads={} + + -- First get payloads for aircraft types of squadrons. + for _,_cohort in pairs(self.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + if Npayloads[cohort.aircrafttype]==nil then + Npayloads[cohort.aircrafttype]=self:CountPayloadsInStock(MissionTypes, cohort.aircrafttype, Payloads) + self:T3(self.lid..string.format("Got Npayloads=%d for type=%s",Npayloads[cohort.aircrafttype], cohort.aircrafttype)) + end + end + + for _,_cohort in pairs(self.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + + -- Number of assets in stock. + local n=cohort:CountAssets(true, MissionTypes, Attributes) + + -- Number of payloads. + local p=Npayloads[cohort.aircrafttype] or 0 + + -- Only the smaller number of assets or paylods is really available. + local m=math.min(n, p) + + -- Add up what we have. Could also be zero. + N=N+m + + -- Reduce number of available payloads. + Npayloads[cohort.aircrafttype]=Npayloads[cohort.aircrafttype]-m + end + + return N +end + +--- Count assets on mission. +-- @param #LEGION self +-- @param #table MissionTypes Types on mission to be checked. Default all. +-- @param Ops.Cohort#COHORT Cohort Only count assets of this cohort. Default count assets of all cohorts. +-- @return #number Number of pending and queued assets. +-- @return #number Number of pending assets. +-- @return #number Number of queued assets. +function LEGION:CountAssetsOnMission(MissionTypes, Cohort) + + local Nq=0 + local Np=0 + + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + -- Check if this mission type is requested. + if AUFTRAG.CheckMissionType(mission.type, MissionTypes or AUFTRAG.Type) then + + for _,_asset in pairs(mission.assets or {}) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + -- Ensure asset belongs to this letion. + if asset.wid==self.uid then + + if Cohort==nil or Cohort.name==asset.squadname then + + local request, isqueued=self:GetRequestByID(mission.requestID[self.alias]) + + if isqueued then + Nq=Nq+1 + else + Np=Np+1 + end + + end + + end + end + end + end + + --env.info(string.format("FF N=%d Np=%d, Nq=%d", Np+Nq, Np, Nq)) + return Np+Nq, Np, Nq +end + +--- Count assets on mission. +-- @param #LEGION self +-- @param #table MissionTypes Types on mission to be checked. Default all. +-- @return #table Assets on pending requests. +function LEGION:GetAssetsOnMission(MissionTypes) + + local assets={} + local Np=0 + + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + -- Check if this mission type is requested. + if AUFTRAG.CheckMissionType(mission.type, MissionTypes) then + + for _,_asset in pairs(mission.assets or {}) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + -- Ensure asset belongs to this legion. + if asset.wid==self.uid then + + table.insert(assets, asset) + + end + + end + end + end + + return assets +end + +--- Get the unit types of this legion. These are the unit types of all assigned cohorts. +-- @param #LEGION self +-- @param #boolean onlyactive Count only the active ones. +-- @param #table cohorts Table of cohorts. Default all. +-- @return #table Table of unit types. +function LEGION:GetAircraftTypes(onlyactive, cohorts) + + -- Get all unit types that can do the job. + local unittypes={} + + -- Loop over all cohorts. + for _,_cohort in pairs(cohorts or self.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + + if (not onlyactive) or cohort:IsOnDuty() then + + local gotit=false + for _,unittype in pairs(unittypes) do + if cohort.aircrafttype==unittype then + gotit=true + break + end + end + if not gotit then + table.insert(unittypes, cohort.aircrafttype) + end + + end + end + + return unittypes +end + +--- Count payloads of all cohorts for all unit types. +-- @param #LEGION self +-- @param #string MissionType Mission type. +-- @param #table Cohorts Cohorts included. +-- @param #table Payloads (Optional) Special payloads. +-- @return #table Table of payloads for each unit type. +function LEGION:_CountPayloads(MissionType, Cohorts, Payloads) + + -- Number of payloads in stock per aircraft type. + local Npayloads={} + + -- First get payloads for aircraft types of squadrons. + for _,_cohort in pairs(Cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + + -- We only need that element once. + if Npayloads[cohort.aircrafttype]==nil then + + -- Count number of payloads in stock for the cohort aircraft type. + Npayloads[cohort.aircrafttype]=cohort.legion:IsAirwing() and self:CountPayloadsInStock(MissionType, cohort.aircrafttype, Payloads) or 999 + + -- Debug info. + self:T2(self.lid..string.format("Got N=%d payloads for mission type=%s and unit type=%s", Npayloads[cohort.aircrafttype], MissionType, cohort.aircrafttype)) + end + end + + return Npayloads +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Recruiting Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Recruit assets for a given mission. +-- @param #LEGION self +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @return #boolean If `true` enough assets could be recruited. +-- @return #table Recruited assets. +-- @return #table Legions of recruited assets. +function LEGION:RecruitAssetsForMission(Mission) + + -- Get required assets. + local NreqMin, NreqMax=Mission:GetRequiredAssets() + + -- Target position vector. + local TargetVec2=Mission:GetTargetVec2() + + -- Payloads. + local Payloads=Mission.payloads + + -- Get special escort legions and/or cohorts. + local Cohorts={} + for _,_legion in pairs(Mission.specialLegions or {}) do + local legion=_legion --Ops.Legion#LEGION + for _,_cohort in pairs(legion.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + table.insert(Cohorts, cohort) + end + end + for _,_cohort in pairs(Mission.specialCohorts or {}) do + local cohort=_cohort --Ops.Cohort#COHORT + table.insert(Cohorts, cohort) + end + + -- No escort cohorts/legions given ==> take own cohorts. + if #Cohorts==0 then + Cohorts=self.cohorts + end + + -- Recuit assets. + local recruited, assets, legions=LEGION.RecruitCohortAssets(Cohorts, Mission.type, Mission.alert5MissionType, NreqMin, NreqMax, TargetVec2, Payloads, Mission.engageRange, Mission.refuelSystem, nil) + + return recruited, assets, legions +end + +--- Recruit assets for a given OPS transport. +-- @param #LEGION self +-- @param Ops.OpsTransport#OPSTRANSPORT Transport The OPS transport. +-- @return #boolean If `true`, enough assets could be recruited. +-- @return #table assets Recruited assets. +-- @return #table legions Legions of recruited assets. +function LEGION:RecruitAssetsForTransport(Transport) + + -- Get all undelivered cargo ops groups. + local cargoOpsGroups=Transport:GetCargoOpsGroups(false) + + local weightGroup=0 + + -- At least one group should be spawned. + if #cargoOpsGroups>0 then + + -- Calculate the max weight so we know which cohorts can provide carriers. + for _,_opsgroup in pairs(cargoOpsGroups) do + local opsgroup=_opsgroup --Ops.OpsGroup#OPSGROUP + local weight=opsgroup:GetWeightTotal() + if weight>weightGroup then + weightGroup=weight + end + end + else + -- No cargo groups! + return false + end + + + -- TODO: Special transport cohorts/legions. + + -- Target is the deploy zone. + local TargetVec2=Transport:GetDeployZone():GetVec2() + + -- Number of required carriers. + local NreqMin,NreqMax=Transport:GetRequiredCarriers() + + + -- Recruit assets and legions. + local recruited, assets, legions=LEGION.RecruitCohortAssets(self.cohorts, AUFTRAG.Type.OPSTRANSPORT, nil, NreqMin, NreqMax, TargetVec2, nil, nil, nil, weightGroup) + + return recruited, assets, legions +end + +--- Recruit assets performing an escort mission for a given asset. +-- @param #LEGION self +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @param #table Assets Table of assets. +-- @return #boolean If `true`, enough assets could be recruited or no escort was required in the first place. +function LEGION:RecruitAssetsForEscort(Mission, Assets) + + -- Is an escort requested in the first place? + if Mission.NescortMin and Mission.NescortMax and (Mission.NescortMin>0 or Mission.NescortMax>0) then + + -- Debug info. + self:I(self.lid..string.format("Requested escort for mission %s [%s]. Required assets=%d-%d", Mission:GetName(), Mission:GetType(), Mission.NescortMin,Mission.NescortMax)) + + -- Get special escort legions and/or cohorts. + local Cohorts={} + for _,_legion in pairs(Mission.escortLegions or {}) do + local legion=_legion --Ops.Legion#LEGION + for _,_cohort in pairs(legion.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + table.insert(Cohorts, cohort) + end + end + for _,_cohort in pairs(Mission.escortCohorts or {}) do + local cohort=_cohort --Ops.Cohort#COHORT + table.insert(Cohorts, cohort) + end + + -- No escort cohorts/legions given ==> take own cohorts. + if #Cohorts==0 then + Cohorts=self.cohorts + end + + -- Call LEGION function but provide COMMANDER as self. + local assigned=LEGION.AssignAssetsForEscort(self, Cohorts, Assets, Mission.NescortMin, Mission.NescortMax) + + return assigned + end + + return true +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Recruiting and Optimization Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Recruit assets from Cohorts for the given parameters. **NOTE** that we set the `asset.isReserved=true` flag so it cant be recruited by anyone else. +-- @param #table Cohorts Cohorts included. +-- @param #string MissionTypeRecruit Mission type for recruiting the cohort assets. +-- @param #string MissionTypeOpt Mission type for which the assets are optimized. Default is the same as `MissionTypeRecruit`. +-- @param #number NreqMin Minimum number of required assets. +-- @param #number NreqMax Maximum number of required assets. +-- @param DCS#Vec2 TargetVec2 Target position as 2D vector. +-- @param #table Payloads Special payloads. +-- @param #number RangeMax Max range in meters. +-- @param #number RefuelSystem Refuelsystem. +-- @param #number CargoWeight Cargo weight for recruiting transport carriers. +-- @param #table Categories Group categories. +-- @param #table Attributes Group attributes. See `GROUP.Attribute.` +-- @return #boolean If `true` enough assets could be recruited. +-- @return #table Recruited assets. **NOTE** that we set the `asset.isReserved=true` flag so it cant be recruited by anyone else. +-- @return #table Legions of recruited assets. +function LEGION.RecruitCohortAssets(Cohorts, MissionTypeRecruit, MissionTypeOpt, NreqMin, NreqMax, TargetVec2, Payloads, RangeMax, RefuelSystem, CargoWeight, Categories, Attributes) + + -- The recruited assets. + local Assets={} + + -- Legions of recruited assets. + local Legions={} + + -- Set MissionTypeOpt to Recruit if nil. + if MissionTypeOpt==nil then + MissionTypeOpt=MissionTypeRecruit + end + + --- Function to check category. + local function CheckCategory(_cohort) + local cohort=_cohort --Ops.Cohort#COHORT + if Categories and #Categories>0 then + for _,category in pairs(Categories) do + if category==cohort.category then + return true + end + end + else + return true + end + end + + --- Function to check attribute. + local function CheckAttribute(_cohort) + local cohort=_cohort --Ops.Cohort#COHORT + if Attributes and #Attributes>0 then + for _,attribute in pairs(Attributes) do + if attribute==cohort.attribute then + return true + end + end + else + return true + end + end + + -- Loops over cohorts. + for _,_cohort in pairs(Cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + + -- Distance to target. + local TargetDistance=TargetVec2 and UTILS.VecDist2D(TargetVec2, cohort.legion:GetVec2()) or 0 + + -- Is in range? + local InRange=(RangeMax and math.max(RangeMax, cohort.engageRange) or cohort.engageRange) >= TargetDistance + + -- Has the requested refuelsystem? + local Refuel=RefuelSystem~=nil and (RefuelSystem==cohort.tankerSystem) or true + + -- STRANGE: Why did the above line did not give the same result?! Above Refuel is always true! + local Refuel=true + if RefuelSystem then + if cohort.tankerSystem then + Refuel=RefuelSystem==cohort.tankerSystem + else + Refuel=false + end + end + + --env.info(string.format("Cohort=%s: RefuelSystem=%s, TankerSystem=%s ==> Refuel=%s", cohort.name, tostring(RefuelSystem), tostring(cohort.tankerSystem), tostring(Refuel))) + + -- Is capable of the mission type? + local Capable=AUFTRAG.CheckMissionCapability({MissionTypeRecruit}, cohort.missiontypes) + + -- Can carry the cargo? + local CanCarry=CargoWeight and cohort.cargobayLimit>=CargoWeight or true + + -- Right category. + local RightCategory=CheckCategory(cohort) + + -- Right attribute. + local RightAttribute=CheckAttribute(cohort) + + -- Debug info. + cohort:T(cohort.lid..string.format("State=%s: Capable=%s, InRange=%s, Refuel=%s, CanCarry=%s, RightCategory=%s, RightAttribute=%s", + cohort:GetState(), tostring(Capable), tostring(InRange), tostring(Refuel), tostring(CanCarry), tostring(RightCategory), tostring(RightAttribute))) + + -- Check OnDuty, capable, in range and refueling type (if TANKER). + if cohort:IsOnDuty() and Capable and InRange and Refuel and CanCarry and RightCategory and RightAttribute then + + -- Recruit assets from cohort. + local assets, npayloads=cohort:RecruitAssets(MissionTypeRecruit, 999) + + -- Add assets to the list. + for _,asset in pairs(assets) do + table.insert(Assets, asset) + end + + end + + end + + -- Now we have a long list with assets. + LEGION._OptimizeAssetSelection(Assets, MissionTypeOpt, TargetVec2, false) + + + -- Get payloads for air assets. + for _,_asset in pairs(Assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + -- Only assets that have no payload. Should be only spawned assets! + if asset.legion:IsAirwing() and not asset.payload then + + -- Fetch payload for asset. This can be nil! + asset.payload=asset.legion:FetchPayloadFromStock(asset.unittype, MissionTypeOpt, Payloads) + + end + end + + -- Remove assets that dont have a payload. + for i=#Assets,1,-1 do + local asset=Assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem + if asset.legion:IsAirwing() and not asset.payload then + table.remove(Assets, i) + end + end + + -- Now find the best asset for the given payloads. + LEGION._OptimizeAssetSelection(Assets, MissionTypeOpt, TargetVec2, true) + + -- Number of assets. At most NreqMax. + local Nassets=math.min(#Assets, NreqMax) + + if #Assets>=NreqMin then + + --- + -- Found enough assets + --- + + -- Add assets to mission. + for i=1,Nassets do + local asset=Assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem + asset.isReserved=true + Legions[asset.legion.alias]=asset.legion + end + + -- Return payloads of not needed assets. + for i=#Assets,Nassets+1,-1 do + local asset=Assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem + if asset.legion:IsAirwing() and not asset.spawned then + asset.legion:T2(asset.legion.lid..string.format("Returning payload from asset %s", asset.spawngroupname)) + asset.legion:ReturnPayloadFromAsset(asset) + end + table.remove(Assets, i) + end + + -- Found enough assets. + return true, Assets, Legions + else + + --- + -- NOT enough assets + --- + + -- Return payloads of assets. + for i=1,#Assets do + local asset=Assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem + if asset.legion:IsAirwing() and not asset.spawned then + asset.legion:T2(asset.legion.lid..string.format("Returning payload from asset %s", asset.spawngroupname)) + asset.legion:ReturnPayloadFromAsset(asset) + end + end + + -- Not enough assets found. + return false, {}, {} + end + + return false, {}, {} +end + +--- Unrecruit assets. Set `isReserved` to false, return payload to airwing and (optionally) remove from assigned mission. +-- @param #table Assets List of assets. +-- @param Ops.Auftrag#AUFTRAG Mission (Optional) The mission from which the assets will be deleted. +function LEGION.UnRecruitAssets(Assets, Mission) + + -- Return payloads of assets. + for i=1,#Assets do + local asset=Assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem + -- Not reserved any more. + asset.isReserved=false + -- Return payload. + if asset.legion:IsAirwing() and not asset.spawned then + asset.legion:T2(asset.legion.lid..string.format("Returning payload from asset %s", asset.spawngroupname)) + asset.legion:ReturnPayloadFromAsset(asset) + end + -- Remove from mission. + if Mission then + Mission:DelAsset(asset) + end + end + +end + + +--- Recruit and assign assets performing an escort mission for a given asset list. Note that each asset gets an escort. +-- @param #LEGION self +-- @param #table Cohorts Cohorts for escorting assets. +-- @param #table Assets Table of assets to be escorted. +-- @param #number NescortMin Min number of escort groups required per escorted asset. +-- @param #number NescortMax Max number of escort groups required per escorted asset. +-- @return #boolean If `true`, enough assets could be recruited or no escort was required in the first place. +function LEGION:AssignAssetsForEscort(Cohorts, Assets, NescortMin, NescortMax) + + -- Is an escort requested in the first place? + if NescortMin and NescortMax and (NescortMin>0 or NescortMax>0) then + + -- Debug info. + self:T(self.lid..string.format("Requested escort for %d assets from %d cohorts. Required escort assets=%d-%d", #Assets, #Cohorts, NescortMin, NescortMax)) + + -- Escorts for each asset. + local Escorts={} + + local EscortAvail=true + for _,_asset in pairs(Assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + -- Target vector is the legion of the asset. + local TargetVec2=asset.legion:GetVec2() + + -- We want airplanes for airplanes and helos for everything else. + local Categories={Group.Category.HELICOPTER} + local TargetTypes={"Ground Units"} + if asset.category==Group.Category.AIRPLANE then + Categories={Group.Category.AIRPLANE} + TargetTypes={"Air"} + end + + -- Recruit escort asset for the mission asset. + local Erecruited, eassets, elegions=LEGION.RecruitCohortAssets(Cohorts, AUFTRAG.Type.ESCORT, nil, NescortMin, NescortMax, TargetVec2, nil, nil, nil, nil, Categories) + + if Erecruited then + Escorts[asset.spawngroupname]={EscortLegions=elegions, EscortAssets=eassets, ecategory=asset.category, TargetTypes=TargetTypes} + else + -- Could not find escort for this asset ==> Escort not possible ==> Break the loop. + EscortAvail=false + break + end + end + + -- ALL escorts could be recruited. + if EscortAvail then + + local N=0 + for groupname,value in pairs(Escorts) do + + local Elegions=value.EscortLegions + local Eassets=value.EscortAssets + local ecategory=value.ecategory + + for _,_legion in pairs(Elegions) do + local legion=_legion --Ops.Legion#LEGION + + local OffsetVector=nil --DCS#Vec3 + if ecategory==Group.Category.GROUND then + -- Overhead + OffsetVector={} + OffsetVector.x=0 + OffsetVector.y=UTILS.FeetToMeters(1000) + OffsetVector.z=0 + end + + -- Create and ESCORT mission for this asset. + local escort=AUFTRAG:NewESCORT(groupname, OffsetVector, nil, value.TargetTypes) + + -- Reserve assts and add to mission. + for _,_asset in pairs(Eassets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + escort:AddAsset(asset) + N=N+1 + end + + -- Assign mission to legion. + self:MissionAssign(escort, {legion}) + end + end + + -- Debug info. + self:T(self.lid..string.format("Recruited %d escort assets", N)) + + -- Yup! + return true + else + + -- Debug info. + self:T(self.lid..string.format("Could not get at least one escort!")) + + -- Could not get at least one escort. Unrecruit all recruited ones. + for groupname,value in pairs(Escorts) do + local Eassets=value.EscortAssets + LEGION.UnRecruitAssets(Eassets) + end + + -- No,no! + return false + end + + else + -- No escort required. + self:T(self.lid..string.format("No escort required! NescortMin=%s, NescortMax=%s", tostring(NescortMin), tostring(NescortMax))) + return true + end + +end + +--- Recruit and assign assets performing an OPSTRANSPORT for a given asset list. +-- @param #LEGION self +-- @param #table Legions Transport legions. +-- @param #table CargoAssets Weight of the heaviest cargo group to be transported. +-- @param #number NcarriersMin Min number of carrier assets. +-- @param #number NcarriersMax Max number of carrier assets. +-- @param Core.Zone#ZONE DeployZone Deploy zone. +-- @param Core.Zone#ZONE DisembarkZone (Optional) Disembark zone. +-- @return #boolean If `true`, enough assets could be recruited and an OPSTRANSPORT object was created. +-- @return Ops.OpsTransport#OPSTRANSPORT Transport The transport. +function LEGION:AssignAssetsForTransport(Legions, CargoAssets, NcarriersMin, NcarriersMax, DeployZone, DisembarkZone, Categories, Attributes) + + -- Is an escort requested in the first place? + if NcarriersMin and NcarriersMax and (NcarriersMin>0 or NcarriersMax>0) then + + -- Cohorts. + local Cohorts={} + for _,_legion in pairs(Legions) do + local legion=_legion --Ops.Legion#LEGION + + -- Check that runway is operational. + local Runway=legion:IsAirwing() and legion:IsRunwayOperational() or true + + if legion:IsRunning() and Runway then + + -- Loops over cohorts. + for _,_cohort in pairs(legion.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + table.insert(Cohorts, cohort) + end + + end + end + + -- Get all legions and heaviest cargo group weight + local CargoLegions={} ; local CargoWeight=nil + for _,_asset in pairs(CargoAssets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + CargoLegions[asset.legion.alias]=asset.legion + if CargoWeight==nil or asset.weight>CargoWeight then + CargoWeight=asset.weight + end + end + + -- Target is the deploy zone. + local TargetVec2=DeployZone:GetVec2() + + -- Recruit assets and legions. + local TransportAvail, CarrierAssets, CarrierLegions= + LEGION.RecruitCohortAssets(Cohorts, AUFTRAG.Type.OPSTRANSPORT, nil, NcarriersMin, NcarriersMax, TargetVec2, nil, nil, nil, CargoWeight, Categories, Attributes) + + if TransportAvail then + + -- Create and OPSTRANSPORT assignment. + local Transport=OPSTRANSPORT:New(nil, nil, DeployZone) + if DisembarkZone then + Transport:SetDisembarkZone(DisembarkZone) + end + + -- Debug info. + self:T(self.lid..string.format("Transport available with %d carrier assets", #CarrierAssets)) + + -- Add cargo assets to transport. + for _,_legion in pairs(CargoLegions) do + local legion=_legion --Ops.Legion#LEGION + + -- Set pickup zone to spawn zone or airbase if the legion has one that is operational. + local pickupzone=legion.spawnzone + if legion.airbase and legion:IsRunwayOperational() then + --pickupzone=ZONE_AIRBASE:New(legion.airbasename, 4000) + end + + -- Add TZC from legion spawn zone to deploy zone. + local tpz=Transport:AddTransportZoneCombo(nil, pickupzone, Transport:GetDeployZone()) + tpz.PickupAirbase=legion:IsRunwayOperational() and legion.airbase or nil + Transport:SetEmbarkZone(legion.spawnzone, tpz) + + + -- Add cargo assets to transport. + for _,_asset in pairs(CargoAssets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + if asset.legion.alias==legion.alias then + Transport:AddAssetCargo(asset, tpz) + end + end + end + + -- Add carrier assets. + for _,_asset in pairs(CarrierAssets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + Transport:AddAsset(asset) + end + + -- Assign TRANSPORT to legions. This also sends the request for the assets. + self:TransportAssign(Transport, CarrierLegions) + + -- Got transport. + return true, Transport + else + -- Uncrecruit transport assets. + LEGION.UnRecruitAssets(CarrierAssets) + return false, nil + end + + return nil, nil + end + + -- No transport requested in the first place. + return true, nil +end + + +--- Calculate the mission score of an asset. +-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset Asset +-- @param #string MissionType Mission type for which the best assets are desired. +-- @param DCS#Vec2 TargetVec2 Target 2D vector. +-- @param #boolean IncludePayload If `true`, include the payload in the calulation if the asset has one attached. +-- @return #number Mission score. +function LEGION.CalculateAssetMissionScore(asset, MissionType, TargetVec2, IncludePayload) + + -- Mission score. + local score=0 + + -- Prefer highly skilled assets. + if asset.skill==AI.Skill.AVERAGE then + score=score+0 + elseif asset.skill==AI.Skill.GOOD then + score=score+10 + elseif asset.skill==AI.Skill.HIGH then + score=score+20 + elseif asset.skill==AI.Skill.EXCELLENT then + score=score+30 + end + + -- Add mission performance to score. + score=score+asset.cohort:GetMissionPeformance(MissionType) + + -- Add payload performance to score. + local function scorePayload(Payload, MissionType) + for _,Capability in pairs(Payload.capabilities) do + local capability=Capability --Ops.Auftrag#AUFTRAG.Capability + if capability.MissionType==MissionType then + return capability.Performance + end + end + return 0 + end + + if IncludePayload and asset.payload then + score=score+scorePayload(asset.payload, MissionType) + end + + -- Origin: We take the OPSGROUP position or the one of the legion. + local OrigVec2=asset.flightgroup and asset.flightgroup:GetVec2() or asset.legion:GetVec2() + + -- Distance factor. + local distance=0 + if TargetVec2 and OrigVec2 then + -- Distance in NM. + distance=UTILS.MetersToNM(UTILS.VecDist2D(OrigVec2, TargetVec2)) + -- Round: 55 NM ==> 5.5 ==> 6, 63 NM ==> 6.3 ==> 6 + distance=UTILS.Round(distance/10, 0) + end + + -- Reduce score for legions that are futher away. + score=score-distance + + -- Intercepts need to be carried out quickly. We prefer spawned assets. + if MissionType==AUFTRAG.Type.INTERCEPT then + if asset.spawned then + score=score+25 + end + end + + -- TRANSPORT specific. + if MissionType==AUFTRAG.Type.OPSTRANSPORT then + -- Add 1 score point for each 10 kg of cargo bay. + score=score+UTILS.Round(asset.cargobaymax/10, 0) + end + + -- TODO: This could be vastly improved. Need to gather ideas during testing. + -- Calculate ETA? Assets on orbit missions should arrive faster even if they are further away. + -- Max speed of assets. + -- Fuel amount? + -- Range of assets? + + return score +end + +--- Optimize chosen assets for the mission at hand. +-- @param #table assets Table of (unoptimized) assets. +-- @param #string MissionType Mission type. +-- @param DCS#Vec2 TargetVec2 Target position as 2D vector. +-- @param #boolean IncludePayload If `true`, include the payload in the calulation if the asset has one attached. +function LEGION._OptimizeAssetSelection(assets, MissionType, TargetVec2, IncludePayload) + + -- Calculate the mission score of all assets. + for _,_asset in pairs(assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + asset.score=LEGION.CalculateAssetMissionScore(asset, MissionType, TargetVec2, IncludePayload) + end + + --- Sort assets wrt to their mission score. Higher is better. + local function optimize(a, b) + local assetA=a --Functional.Warehouse#WAREHOUSE.Assetitem + local assetB=b --Functional.Warehouse#WAREHOUSE.Assetitem + -- Higher score wins. If equal score ==> closer wins. + return (assetA.score>assetB.score) + end + table.sort(assets, optimize) + + -- Remove distance parameter. + if LEGION.verbose>0 then + local text=string.format("Optimized %d assets for %s mission/transport (payload=%s):", #assets, MissionType, tostring(IncludePayload)) + for i,Asset in pairs(assets) do + local asset=Asset --Functional.Warehouse#WAREHOUSE.Assetitem + text=text..string.format("\n%s %s: score=%d", asset.squadname, asset.spawngroupname, asset.score) + asset.score=nil + end + env.info(text) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Returns the mission for a given mission ID (Autragsnummer). +-- @param #LEGION self +-- @param #number mid Mission ID (Auftragsnummer). +-- @return Ops.Auftrag#AUFTRAG Mission table. +function LEGION:GetMissionByID(mid) + + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission.auftragsnummer==tonumber(mid) then + return mission + end + + end + + return nil +end + +--- Returns the mission for a given ID. +-- @param #LEGION self +-- @param #number uid Transport UID. +-- @return Ops.OpsTransport#OPSTRANSPORT Transport assignment. +function LEGION:GetTransportByID(uid) + + for _,_transport in pairs(self.transportqueue) do + local transport=_transport --Ops.OpsTransport#OPSTRANSPORT + + if transport.uid==tonumber(uid) then + return transport + end + + end + + return nil +end + +--- Returns the mission for a given request ID. +-- @param #LEGION self +-- @param #number RequestID Unique ID of the request. +-- @return Ops.Auftrag#AUFTRAG Mission table or *nil*. +function LEGION:GetMissionFromRequestID(RequestID) + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + local mid=mission.requestID[self.alias] + if mid and mid==RequestID then + return mission + end + end + return nil +end + +--- Returns the mission for a given request. +-- @param #LEGION self +-- @param Functional.Warehouse#WAREHOUSE.Queueitem Request The warehouse request. +-- @return Ops.Auftrag#AUFTRAG Mission table or *nil*. +function LEGION:GetMissionFromRequest(Request) + return self:GetMissionFromRequestID(Request.uid) +end + +--- Fetch a payload from the airwing resources for a given unit and mission type. +-- The payload with the highest priority is preferred. +-- @param #LEGION self +-- @param #string UnitType The type of the unit. +-- @param #string MissionType The mission type. +-- @param #table Payloads Specific payloads only to be considered. +-- @return Ops.Airwing#AIRWING.Payload Payload table or *nil*. +function LEGION:FetchPayloadFromStock(UnitType, MissionType, Payloads) + -- Polymorphic. Will return something when called by airwing. + return nil +end + +--- Return payload from asset back to stock. +-- @param #LEGION self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset The squadron asset. +function LEGION:ReturnPayloadFromAsset(asset) + -- Polymorphic. + return nil +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/NavyGroup.lua b/Moose Development/Moose/Ops/NavyGroup.lua index 7fa71c335..e34549feb 100644 --- a/Moose Development/Moose/Ops/NavyGroup.lua +++ b/Moose Development/Moose/Ops/NavyGroup.lua @@ -5,7 +5,7 @@ -- * Let the group steam into the wind -- * Command a full stop -- * Patrol waypoints *ad infinitum* --- * Collision warning, if group is heading towards a land mass +-- * Collision warning, if group is heading towards a land mass or another obstacle -- * Automatic pathfinding, e.g. around islands -- * Let a submarine dive and surface -- * Manage TACAN and ICLS beacons @@ -43,14 +43,13 @@ -- @field #boolean pathfindingOn If true, enable pathfining. -- @field #number pathCorridor Path corrdidor width in meters. -- @field #boolean ispathfinding If true, group is currently path finding. +-- @field #NAVYGROUP.Target engage Engage target. -- @extends Ops.OpsGroup#OPSGROUP --- *Something must be left to chance; nothing is sure in a sea fight above all.* -- Horatio Nelson -- -- === --- --- ![Banner Image](..\Presentations\OPS\NavyGroup\_Main.png) --- +-- -- # The NAVYGROUP Concept -- -- This class enhances naval groups. @@ -63,14 +62,10 @@ NAVYGROUP = { intowindcounter = 0, Qintowind = {}, pathCorridor = 400, + engage = {}, } ---- Navy group element. --- @type NAVYGROUP.Element --- @field #string name Name of the element, i.e. the unit. --- @field #string typename Type name. - ---- Navy group element. +--- Turn into wind parameters. -- @type NAVYGROUP.IntoWind -- @field #number Tstart Time to start. -- @field #number Tstop Time to stop. @@ -84,16 +79,27 @@ NAVYGROUP = { -- @field #boolean Open Currently active. -- @field #boolean Over This turn is over. +--- Engage Target. +-- @type NAVYGROUP.Target +-- @field Ops.Target#TARGET Target The target. +-- @field Core.Point#COORDINATE Coordinate Last known coordinate of the target. +-- @field Ops.OpsGroup#OPSGROUP.Waypoint Waypoint the waypoint created to go to the target. +-- @field #number roe ROE backup. +-- @field #number alarmstate Alarm state backup. --- NavyGroup version. -- @field #string version -NAVYGROUP.version="0.5.0" +NAVYGROUP.version="0.7.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Extend, shorten turn into wind windows +-- TODO: Add RTZ. +-- TODO: Add Retreat. +-- TODO: Add EngageTarget. +-- TODO: Submaries. +-- TODO: Extend, shorten turn into wind windows. -- TODO: Skipper menu. -- DONE: Collision warning. -- DONE: Detour, add temporary waypoint and resume route. @@ -107,63 +113,282 @@ NAVYGROUP.version="0.5.0" --- Create a new NAVYGROUP class object. -- @param #NAVYGROUP self --- @param #string GroupName Name of the group. +-- @param Wrapper.Group#GROUP group The group object. Can also be given by its group name as `#string`. -- @return #NAVYGROUP self -function NAVYGROUP:New(GroupName) +function NAVYGROUP:New(group) + + -- First check if we already have an OPS group for this group. + local og=_DATABASE:GetOpsGroup(group) + if og then + og:I(og.lid..string.format("WARNING: OPS group already exists in data base!")) + return og + end -- Inherit everything from FSM class. - local self=BASE:Inherit(self, OPSGROUP:New(GroupName)) -- #NAVYGROUP + local self=BASE:Inherit(self, OPSGROUP:New(group)) -- #NAVYGROUP -- Set some string id for output to DCS.log file. self.lid=string.format("NAVYGROUP %s | ", self.groupname) -- Defaults - self:SetDetection() self:SetDefaultROE() self:SetDefaultAlarmstate() + self:SetDefaultEPLRS(self.isEPLRS) + self:SetDetection() self:SetPatrolAdInfinitum(true) self:SetPathfinding(false) - self.isNavygroup=true -- Add FSM transitions. -- From State --> Event --> To State self:AddTransition("*", "FullStop", "Holding") -- Hold position. self:AddTransition("*", "Cruise", "Cruising") -- Hold position. + + self:AddTransition("*", "RTZ", "Returning") -- Group is returning to (home) zone. + self:AddTransition("Returning", "Returned", "Returned") -- Group is returned to (home) zone. + + self:AddTransition("*", "Detour", "Cruising") -- Make a detour to a coordinate and resume route afterwards. + self:AddTransition("*", "DetourReached", "*") -- Group reached the detour coordinate. + + self:AddTransition("*", "Retreat", "Retreating") -- Order a retreat. + self:AddTransition("Retreating", "Retreated", "Retreated") -- Group retreated. + + self:AddTransition("Cruising", "EngageTarget", "Engaging") -- Engage a target from Cruising state + self:AddTransition("Holding", "EngageTarget", "Engaging") -- Engage a target from Holding state + self:AddTransition("OnDetour", "EngageTarget", "Engaging") -- Engage a target from OnDetour state + self:AddTransition("Engaging", "Disengage", "Cruising") -- Disengage and back to cruising. - self:AddTransition("*", "TurnIntoWind", "IntoWind") -- Command the group to turn into the wind. - self:AddTransition("IntoWind", "TurnedIntoWind", "IntoWind") -- Group turned into wind. - self:AddTransition("IntoWind", "TurnIntoWindStop", "IntoWind") -- Stop a turn into wind. - self:AddTransition("IntoWind", "TurnIntoWindOver", "Cruising") -- Turn into wind is over. + self:AddTransition("*", "TurnIntoWind", "Cruising") -- Command the group to turn into the wind. + self:AddTransition("*", "TurnedIntoWind", "*") -- Group turned into wind. + self:AddTransition("*", "TurnIntoWindStop", "*") -- Stop a turn into wind. + self:AddTransition("*", "TurnIntoWindOver", "*") -- Turn into wind is over. self:AddTransition("*", "TurningStarted", "*") -- Group started turning. self:AddTransition("*", "TurningStopped", "*") -- Group stopped turning. - self:AddTransition("*", "Detour", "OnDetour") -- Make a detour to a coordinate and resume route afterwards. - self:AddTransition("OnDetour", "DetourReached", "Cruising") -- Group reached the detour coordinate. - self:AddTransition("*", "CollisionWarning", "*") -- Collision warning. self:AddTransition("*", "ClearAhead", "*") -- Clear ahead. - self:AddTransition("*", "Dive", "Diving") -- Command a submarine to dive. - self:AddTransition("Diving", "Surface", "Cruising") -- Command a submarine to go to the surface. + self:AddTransition("Cruising", "Dive", "Cruising") -- Command a submarine to dive. + self:AddTransition("Engaging", "Dive", "Engaging") -- Command a submarine to dive. + self:AddTransition("Cruising", "Surface", "Cruising") -- Command a submarine to go to the surface. + self:AddTransition("Engaging", "Surface", "Engaging") -- Command a submarine to go to the surface. ------------------------ --- Pseudo Functions --- ------------------------ - --- Triggers the FSM event "Stop". Stops the NAVYGROUP and all its event handlers. + --- Triggers the FSM event "Cruise". + -- @function [parent=#NAVYGROUP] Cruise -- @param #NAVYGROUP self + -- @param #number Speed Speed in knots until next waypoint is reached. - --- Triggers the FSM event "Stop" after a delay. Stops the NAVYGROUP and all its event handlers. - -- @function [parent=#NAVYGROUP] __Stop + --- Triggers the FSM event "Cruise" after a delay. + -- @function [parent=#NAVYGROUP] __Cruise -- @param #NAVYGROUP self -- @param #number delay Delay in seconds. - - -- TODO: Add pseudo functions. + -- @param #number Speed Speed in knots until next waypoint is reached. + + --- On after "Cruise" event. + -- @function [parent=#NAVYGROUP] OnAfterCruise + -- @param #NAVYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #number Speed Speed in knots until next waypoint is reached. + + + + + + + + --- Triggers the FSM event "TurnIntoWind". + -- @function [parent=#NAVYGROUP] TurnIntoWind + -- @param #NAVYGROUP self + -- @param #NAVYGROUP.IntoWind Into wind parameters. + + --- Triggers the FSM event "TurnIntoWind" after a delay. + -- @function [parent=#NAVYGROUP] __TurnIntoWind + -- @param #NAVYGROUP self + -- @param #number delay Delay in seconds. + -- @param #NAVYGROUP.IntoWind Into wind parameters. + + --- On after "TurnIntoWind" event. + -- @function [parent=#NAVYGROUP] OnAfterTurnIntoWind + -- @param #NAVYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #NAVYGROUP.IntoWind Into wind parameters. + + + --- Triggers the FSM event "TurnedIntoWind". + -- @function [parent=#NAVYGROUP] TurnedIntoWind + -- @param #NAVYGROUP self + + --- Triggers the FSM event "TurnedIntoWind" after a delay. + -- @function [parent=#NAVYGROUP] __TurnedIntoWind + -- @param #NAVYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "TurnedIntoWind" event. + -- @function [parent=#NAVYGROUP] OnAfterTurnedIntoWind + -- @param #NAVYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "TurnIntoWindStop". + -- @function [parent=#NAVYGROUP] TurnIntoWindStop + -- @param #NAVYGROUP self + + --- Triggers the FSM event "TurnIntoWindStop" after a delay. + -- @function [parent=#NAVYGROUP] __TurnIntoWindStop + -- @param #NAVYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "TurnIntoWindStop" event. + -- @function [parent=#NAVYGROUP] OnAfterTurnIntoWindStop + -- @param #NAVYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "TurnIntoWindOver". + -- @function [parent=#NAVYGROUP] TurnIntoWindOver + -- @param #NAVYGROUP self + -- @param #NAVYGROUP.IntoWind IntoWindData Data table. + + --- Triggers the FSM event "TurnIntoWindOver" after a delay. + -- @function [parent=#NAVYGROUP] __TurnIntoWindOver + -- @param #NAVYGROUP self + -- @param #number delay Delay in seconds. + -- @param #NAVYGROUP.IntoWind IntoWindData Data table. + + --- On after "TurnIntoWindOver" event. + -- @function [parent=#NAVYGROUP] OnAfterTurnIntoWindOver + -- @param #NAVYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #NAVYGROUP.IntoWind IntoWindData Data table. + + + --- Triggers the FSM event "TurningStarted". + -- @function [parent=#NAVYGROUP] TurningStarted + -- @param #NAVYGROUP self + + --- Triggers the FSM event "TurningStarted" after a delay. + -- @function [parent=#NAVYGROUP] __TurningStarted + -- @param #NAVYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "TurningStarted" event. + -- @function [parent=#NAVYGROUP] OnAfterTurningStarted + -- @param #NAVYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "TurningStopped". + -- @function [parent=#NAVYGROUP] TurningStopped + -- @param #NAVYGROUP self + + --- Triggers the FSM event "TurningStopped" after a delay. + -- @function [parent=#NAVYGROUP] __TurningStopped + -- @param #NAVYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "TurningStopped" event. + -- @function [parent=#NAVYGROUP] OnAfterTurningStopped + -- @param #NAVYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "CollisionWarning". + -- @function [parent=#NAVYGROUP] CollisionWarning + -- @param #NAVYGROUP self + + --- Triggers the FSM event "CollisionWarning" after a delay. + -- @function [parent=#NAVYGROUP] __CollisionWarning + -- @param #NAVYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "CollisionWarning" event. + -- @function [parent=#NAVYGROUP] OnAfterCollisionWarning + -- @param #NAVYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "ClearAhead". + -- @function [parent=#NAVYGROUP] ClearAhead + -- @param #NAVYGROUP self + + --- Triggers the FSM event "ClearAhead" after a delay. + -- @function [parent=#NAVYGROUP] __ClearAhead + -- @param #NAVYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "ClearAhead" event. + -- @function [parent=#NAVYGROUP] OnAfterClearAhead + -- @param #NAVYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Dive". + -- @function [parent=#NAVYGROUP] Dive + -- @param #NAVYGROUP self + -- @param #number Depth Dive depth in meters. Default 50 meters. + -- @param #number Speed Speed in knots until next waypoint is reached. + + --- Triggers the FSM event "Dive" after a delay. + -- @function [parent=#NAVYGROUP] __Dive + -- @param #NAVYGROUP self + -- @param #number delay Delay in seconds. + -- @param #number Depth Dive depth in meters. Default 50 meters. + -- @param #number Speed Speed in knots until next waypoint is reached. + + --- On after "Dive" event. + -- @function [parent=#NAVYGROUP] OnAfterDive + -- @param #NAVYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #number Depth Dive depth in meters. Default 50 meters. + -- @param #number Speed Speed in knots until next waypoint is reached. + + + --- Triggers the FSM event "Surface". + -- @function [parent=#NAVYGROUP] Surface + -- @param #NAVYGROUP self + -- @param #number Speed Speed in knots until next waypoint is reached. + + --- Triggers the FSM event "Surface" after a delay. + -- @function [parent=#NAVYGROUP] __Surface + -- @param #NAVYGROUP self + -- @param #number delay Delay in seconds. + -- @param #number Speed Speed in knots until next waypoint is reached. + + --- On after "Surface" event. + -- @function [parent=#NAVYGROUP] OnAfterSurface + -- @param #NAVYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #number Speed Speed in knots until next waypoint is reached. -- Init waypoints. - self:InitWaypoints() + self:_InitWaypoints() -- Initialize the group. self:_InitGroup() @@ -174,13 +399,16 @@ function NAVYGROUP:New(GroupName) self:HandleEvent(EVENTS.RemoveUnit, self.OnEventRemoveUnit) -- Start the status monitoring. - self:__Status(-1) + self.timerStatus=TIMER:New(self.Status, self):Start(1, 30) -- Start queue update timer. self.timerQueueUpdate=TIMER:New(self._QueueUpdate, self):Start(2, 5) -- Start check zone timer. self.timerCheckZone=TIMER:New(self._CheckInZones, self):Start(2, 60) + + -- Add OPSGROUP to _DATABASE. + _DATABASE:AddOpsGroup(self) return self end @@ -226,7 +454,7 @@ end -- @param #NAVYGROUP self -- @return #NAVYGROUP self function NAVYGROUP:SetPathfindingOff() - self:SetPathfinding(true, self.pathCorridor) + self:SetPathfinding(false, self.pathCorridor) return self end @@ -355,7 +583,7 @@ end -- @param #NAVYGROUP self -- @param #string starttime Start time, e.g. "8:00" for eight o'clock. Default now. -- @param #string stoptime Stop time, e.g. "9:00" for nine o'clock. Default 90 minutes after start time. --- @param #number speed Speed in knots during turn into wind leg. +-- @param #number speed Wind speed on deck in knots during turn into wind leg. Default 20 knots. -- @param #boolean uturn If `true` (or `nil`), carrier wil perform a U-turn and go back to where it came from before resuming its route to the next waypoint. If false, it will go directly to the next waypoint. -- @param #number offset Offset angle in degrees, e.g. to account for an angled runway. Default 0 deg. -- @return #NAVYGROUP.IntoWind Turn into window data table. @@ -379,7 +607,6 @@ function NAVYGROUP:RemoveTurnIntoWind(IntoWindData) -- Check if this is a window currently open. if self.intowind and self.intowind.Id==IntoWindData.Id then - --env.info("FF stop in remove") self:TurnIntoWindStop() return end @@ -449,46 +676,34 @@ end -- Status ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ----- Update status. --- @param #NAVYGROUP self -function NAVYGROUP:onbeforeStatus(From, Event, To) - - if self:IsDead() then - self:T(self.lid..string.format("Onbefore Status DEAD ==> false")) - return false - elseif self:IsStopped() then - self:T(self.lid..string.format("Onbefore Status STOPPED ==> false")) - return false - end - - return true -end - --- Update status. -- @param #NAVYGROUP self -function NAVYGROUP:onafterStatus(From, Event, To) +function NAVYGROUP:Status(From, Event, To) -- FSM state. local fsmstate=self:GetState() + + -- Is group alive? + local alive=self:IsAlive() - if self:IsAlive() then + -- Free path. + local freepath=0 - --- - -- Detection - --- - - -- Check if group has detected any units. - if self.detectionOn then - self:_CheckDetectedUnits() - end - + -- Check if group is exists and is active. + if alive then + -- Update last known position, orientation, velocity. self:_UpdatePosition() + + -- Check if group has detected any units. + self:_CheckDetectedUnits() -- Check if group started or stopped turning. self:_CheckTurning() - - local freepath=UTILS.NMToMeters(10) + + -- Distance to next Waypoint. + local disttoWP=math.min(self:GetDistanceToWaypoint(), UTILS.NMToMeters(10)) + freepath=disttoWP -- Only check if not currently turning. if not self:IsTurning() then @@ -496,7 +711,7 @@ function NAVYGROUP:onafterStatus(From, Event, To) -- Check free path ahead. freepath=self:_CheckFreePath(freepath, 100) - if freepath<5000 then + if disttoWP>1 and freepathself.Twaiting+self.dTwait then + self.Twaiting=nil + self.dTwait=nil + self:Cruise() + end + end + end + + end + + -- Group exists but can also be inactive. + if alive~=nil then if self.verbose>=1 then @@ -525,15 +756,17 @@ function NAVYGROUP:onafterStatus(From, Event, To) local intowind=self:IsSteamingIntoWind() and UTILS.SecondsToClock(self.intowind.Tstop-timer.getAbsTime(), true) or "N/A" local turning=tostring(self:IsTurning()) - local alt=self.position.y - local speed=UTILS.MpsToKnots(self.velocity) - local speedExpected=UTILS.MpsToKnots(self:GetExpectedSpeed()) --UTILS.MpsToKnots(self.speedWp or 0) + local alt=self.position and self.position.y or 0 + local speed=UTILS.MpsToKnots(self.velocity or 0) + local speedExpected=UTILS.MpsToKnots(self:GetExpectedSpeed()) -- Waypoint stuff. local wpidxCurr=self.currentwp local wpuidCurr=self:GetWaypointUIDFromIndex(wpidxCurr) or 0 local wpidxNext=self:GetWaypointIndexNext() or 0 local wpuidNext=self:GetWaypointUIDFromIndex(wpidxNext) or 0 + local wpN=#self.waypoints or 0 + local wpF=tostring(self.passedfinalwp) local wpDist=UTILS.MetersToNM(self:GetDistanceToWaypoint() or 0) local wpETA=UTILS.SecondsToClock(self:GetTimeToWaypoint() or 0, true) @@ -542,22 +775,10 @@ function NAVYGROUP:onafterStatus(From, Event, To) local als=self:GetAlarmstate() or 0 -- Info text. - local text=string.format("%s [ROE=%d,AS=%d, T/M=%d/%d]: Wp=%d[%d]-->%d[%d] (of %d) Dist=%.1f NM ETA=%s - Speed=%.1f (%.1f) kts, Depth=%.1f m, Hdg=%03d, Turn=%s Collision=%d IntoWind=%s", - fsmstate, roe, als, nTaskTot, nMissions, wpidxCurr, wpuidCurr, wpidxNext, wpuidNext, #self.waypoints or 0, wpDist, wpETA, speed, speedExpected, alt, self.heading, turning, freepath, intowind) + local text=string.format("%s [ROE=%d,AS=%d, T/M=%d/%d]: Wp=%d[%d]-->%d[%d] /%d [%s] Dist=%.1f NM ETA=%s - Speed=%.1f (%.1f) kts, Depth=%.1f m, Hdg=%03d, Turn=%s Collision=%d IntoWind=%s", + fsmstate, roe, als, nTaskTot, nMissions, wpidxCurr, wpuidCurr, wpidxNext, wpuidNext, wpN, wpF, wpDist, wpETA, speed, speedExpected, alt, self.heading or 0, turning, freepath, intowind) self:I(self.lid..text) - - if false then - local text="Waypoints:" - for i,wp in pairs(self.waypoints) do - local waypoint=wp --Ops.OpsGroup#OPSGROUP.Waypoint - text=text..string.format("\n%d. UID=%d", i, waypoint.uid) - if i==self.currentwp then - text=text.." current!" - end - end - env.info(text) - end - + end else @@ -572,7 +793,7 @@ function NAVYGROUP:onafterStatus(From, Event, To) -- Recovery Windows --- - if self.verbose>=2 then + if alive and self.verbose>=2 and #self.Qintowind>0 then -- Debug output: local text=string.format(self.lid.."Turn into wind time windows:") @@ -599,6 +820,26 @@ function NAVYGROUP:onafterStatus(From, Event, To) end + --- + -- Engage Detected Targets + --- + if self:IsCruising() and self.detectionOn and self.engagedetectedOn then + + local targetgroup, targetdist=self:_GetDetectedTarget() + + -- If we found a group, we engage it. + if targetgroup then + self:I(self.lid..string.format("Engaging target group %s at distance %d meters", targetgroup:GetName(), targetdist)) + self:EngageTarget(targetgroup) + end + + end + + --- + -- Cargo + --- + + self:_CheckCargoTransport() --- -- Tasks & Missions @@ -606,11 +847,14 @@ function NAVYGROUP:onafterStatus(From, Event, To) self:_PrintTaskAndMissionStatus() - - -- Next status update in 30 seconds. - self:__Status(-30) end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- DCS Events ==> See OPSGROUP +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- See OPSGROUP! + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- FSM Events ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -620,7 +864,7 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #NAVYGROUP.Element Element The group element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The group element. function NAVYGROUP:onafterElementSpawned(From, Event, To, Element) self:T(self.lid..string.format("Element spawned %s", Element.name)) @@ -637,8 +881,32 @@ end function NAVYGROUP:onafterSpawned(From, Event, To) self:T(self.lid..string.format("Group spawned!")) + -- Debug info. + if self.verbose>=1 then + local text=string.format("Initialized Navy Group %s:\n", self.groupname) + text=text..string.format("Unit type = %s\n", self.actype) + text=text..string.format("Speed max = %.1f Knots\n", UTILS.KmphToKnots(self.speedMax)) + text=text..string.format("Speed cruise = %.1f Knots\n", UTILS.KmphToKnots(self.speedCruise)) + text=text..string.format("Weight = %.1f kg\n", self:GetWeightTotal()) + text=text..string.format("Cargo bay = %.1f kg\n", self:GetFreeCargobay()) + text=text..string.format("Has EPLRS = %s\n", tostring(self.isEPLRS)) + text=text..string.format("Is Submarine = %s\n", tostring(self.isSubmarine)) + text=text..string.format("Elements = %d\n", #self.elements) + text=text..string.format("Waypoints = %d\n", #self.waypoints) + text=text..string.format("Radio = %.1f MHz %s %s\n", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu), tostring(self.radio.On)) + text=text..string.format("Ammo = %d (G=%d/R=%d/M=%d/T=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Rockets, self.ammo.Missiles, self.ammo.Torpedos) + text=text..string.format("FSM state = %s\n", self:GetState()) + text=text..string.format("Is alive = %s\n", tostring(self:IsAlive())) + text=text..string.format("LateActivate = %s\n", tostring(self:IsLateActivated())) + self:I(self.lid..text) + end + -- Update position. self:_UpdatePosition() + + -- Not dead or destroyed yet. + self.isDead=false + self.isDestroyed=false if self.isAI then @@ -648,6 +916,9 @@ function NAVYGROUP:onafterSpawned(From, Event, To) -- Set default Alarm State. self:SwitchAlarmstate(self.option.Alarm) + -- Set default EPLRS. + self:SwitchEPLRS(self.option.EPLRS) + -- Set TACAN beacon. self:_SwitchTACAN() @@ -656,20 +927,50 @@ function NAVYGROUP:onafterSpawned(From, Event, To) -- Set radio. if self.radioDefault then - self:SwitchRadio() + -- CAREFUL: This makes DCS crash for some ships like speed boats or Higgins boats! (On a respawn for example). Looks like the command SetFrequency is causing this. + --self:SwitchRadio() else self:SetDefaultRadio(self.radio.Freq, self.radio.Modu, false) end + + -- Update route. + if #self.waypoints>1 then + self:__Cruise(-0.1) + else + self:FullStop() + end end - -- Update route. - if #self.waypoints>1 then - self:Cruise() - else - self:FullStop() +end + +--- On before "UpdateRoute" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number n Next waypoint index. Default is the one coming after that one that has been passed last. +-- @param #number N Waypoint Max waypoint index to be included in the route. Default is the final waypoint. +-- @param #number Speed Speed in knots to the next waypoint. +-- @param #number Depth Depth in meters to the next waypoint. +function NAVYGROUP:onbeforeUpdateRoute(From, Event, To, n, Speed, Depth) + if self:IsWaiting() then + self:E(self.lid.."Update route denied. Group is WAITING!") + return false + elseif self:IsInUtero() then + self:E(self.lid.."Update route denied. Group is INUTERO!") + return false + elseif self:IsDead() then + self:E(self.lid.."Update route denied. Group is DEAD!") + return false + elseif self:IsStopped() then + self:E(self.lid.."Update route denied. Group is STOPPED!") + return false + elseif self:IsHolding() then + self:T(self.lid.."Update route denied. Group is holding position!") + return false end - + return true end --- On after "UpdateRoute" event. @@ -677,57 +978,80 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #number n Waypoint number. Default is next waypoint. +-- @param #number n Next waypoint index. Default is the one coming after that one that has been passed last. +-- @param #number N Waypoint Max waypoint index to be included in the route. Default is the final waypoint. -- @param #number Speed Speed in knots to the next waypoint. -- @param #number Depth Depth in meters to the next waypoint. -function NAVYGROUP:onafterUpdateRoute(From, Event, To, n, Speed, Depth) +function NAVYGROUP:onafterUpdateRoute(From, Event, To, n, N, Speed, Depth) -- Update route from this waypoint number onwards. n=n or self:GetWaypointIndexNext() - -- Update waypoint tasks, i.e. inject WP tasks into waypoint table. - self:_UpdateWaypointTasks(n) + -- Max index. + N=N or #self.waypoints + N=math.min(N, #self.waypoints) + -- Waypoints. local waypoints={} - -- Waypoint. - local wp=UTILS.DeepCopy(self.waypoints[n]) --Ops.OpsGroup#OPSGROUP.Waypoint + for i=n, N do + + -- Waypoint. + local wp=UTILS.DeepCopy(self.waypoints[i]) --Ops.OpsGroup#OPSGROUP.Waypoint - -- Speed. - if Speed then - -- Take speed specified. - wp.speed=UTILS.KnotsToMps(Speed) - else - -- Take default waypoint speed. But make sure speed>0 if patrol ad infinitum. - if self.adinfinitum and wp.speed<0.1 then - wp.speed=UTILS.KmphToMps(self.speedCruise) + --env.info(string.format("FF i=%d UID=%d n=%d, N=%d", i, wp.uid, n, N)) + + -- Speed. + if Speed then + -- Take speed specified. + wp.speed=UTILS.KnotsToMps(Speed) + else + -- Take default waypoint speed. But make sure speed>0 if patrol ad infinitum. + if wp.speed<0.1 then --self.adinfinitum and + wp.speed=UTILS.KmphToMps(self.speedCruise) + end + end + + -- Depth. + if Depth then + wp.alt=-Depth + elseif self.depth then + wp.alt=-self.depth + else + -- Take default waypoint alt. + wp.alt=wp.alt or 0 + end + + -- Current set speed in m/s. + if i==n then + self.speedWp=wp.speed + self.altWp=wp.alt end - end - if Depth then - wp.alt=-Depth - elseif self.depth then - wp.alt=-self.depth - else - -- Take default waypoint alt. - end + -- Add waypoint. + table.insert(waypoints, wp) - -- Current set speed in m/s. - self.speedWp=wp.speed - - -- Add waypoint. - table.insert(waypoints, wp) + end -- Current waypoint. - local current=self:GetCoordinate():WaypointNaval(UTILS.MpsToKmph(self.speedWp), wp.alt) + local current=self:GetCoordinate():WaypointNaval(UTILS.MpsToKmph(self.speedWp), self.altWp) table.insert(waypoints, 1, current) - if not self.passedfinalwp then + if self:IsEngaging() or not self.passedfinalwp then + + if self.verbose>=10 then + for i=1,#waypoints do + local wp=waypoints[i] --Ops.OpsGroup#OPSGROUP.Waypoint + local text=string.format("%s Waypoint [%d] UID=%d speed=%d", self.groupname, i-1, wp.uid or -1, wp.speed) + self:I(self.lid..text) + COORDINATE:NewFromWaypoint(wp):MarkToAll(text) + end + end -- Debug info. - self:T(self.lid..string.format("Updateing route: WP %d-->%d (%d/%d), Speed=%.1f knots, Depth=%d m", self.currentwp, n, #waypoints, #self.waypoints, UTILS.MpsToKnots(self.speedWp), wp.alt)) + self:T(self.lid..string.format("Updateing route: WP %d-->%d (%d/%d), Speed=%.1f knots, Depth=%d m", self.currentwp, n, #waypoints, #self.waypoints, UTILS.MpsToKnots(self.speedWp), self.altWp)) -- Route group to all defined waypoints remaining. self:Route(waypoints) @@ -827,7 +1151,7 @@ function NAVYGROUP:onafterTurnIntoWind(From, Event, To, IntoWind) IntoWind.waypoint=wptiw - if IntoWind.Uturn and self.Debug then + if IntoWind.Uturn and false then IntoWind.Coordinate:MarkToAll("Return coord") end @@ -885,8 +1209,13 @@ function NAVYGROUP:onafterTurnIntoWindOver(From, Event, To, IntoWindData) -- Detour to where we left the route. self:T(self.lid.."FF Turn Into Wind Over ==> Uturn!") - self:Detour(self.intowind.Coordinate, self:GetSpeedCruise(), 0, true) - + + -- ID of current waypoint. + local uid=self:GetWaypointCurrent().uid + + -- Add temp waypoint. + local wp=self:AddWaypoint(self.intowind.Coordinate, self:GetSpeedCruise(), uid) ; wp.temp=true + else --- @@ -895,11 +1224,11 @@ function NAVYGROUP:onafterTurnIntoWindOver(From, Event, To, IntoWindData) -- Next waypoint index and speed. local indx=self:GetWaypointIndexNext() - local speed=self:GetWaypointSpeed(indx) + local speed=self:GetSpeedToWaypoint(indx) -- Update route. self:T(self.lid..string.format("FF Turn Into Wind Over ==> Next WP Index=%d at %.1f knots via update route!", indx, speed)) - self:__UpdateRoute(-1, indx, speed) + self:__UpdateRoute(-1, indx, nil, speed) end @@ -940,10 +1269,14 @@ end -- @param #number Speed Speed in knots until next waypoint is reached. Default is speed set for waypoint. function NAVYGROUP:onafterCruise(From, Event, To, Speed) + -- Not waiting anymore. + self.Twaiting=nil + self.dTwait=nil + -- No set depth. self.depth=nil - self:__UpdateRoute(-1, nil, Speed) + self:__UpdateRoute(-0.1, nil, nil, Speed) end @@ -958,11 +1291,11 @@ function NAVYGROUP:onafterDive(From, Event, To, Depth, Speed) Depth=Depth or 50 - self:T(self.lid..string.format("Diving to %d meters", Depth)) + self:I(self.lid..string.format("Diving to %d meters", Depth)) self.depth=Depth - self:__UpdateRoute(-1, nil, Speed) + self:__UpdateRoute(-1, nil, nil, Speed) end @@ -976,7 +1309,7 @@ function NAVYGROUP:onafterSurface(From, Event, To, Speed) self.depth=0 - self:__UpdateRoute(-1, nil, Speed) + self:__UpdateRoute(-1, nil, nil, Speed) end @@ -1015,153 +1348,149 @@ function NAVYGROUP:onafterCollisionWarning(From, Event, To, Distance) self.collisionwarning=true end ---- On after Start event. Starts the NAVYGROUP FSM and event handlers. +--- On after "EngageTarget" event. -- @param #NAVYGROUP self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -function NAVYGROUP:onafterStop(From, Event, To) +-- @param Wrapper.Group#GROUP Group the group to be engaged. +function NAVYGROUP:onafterEngageTarget(From, Event, To, Target) + self:T(self.lid.."Engaging Target") - -- Handle events: - self:UnHandleEvent(EVENTS.Birth) - self:UnHandleEvent(EVENTS.Dead) - self:UnHandleEvent(EVENTS.RemoveUnit) - - -- Call OPSGROUP function. - self:GetParent(self).onafterStop(self, From, Event, To) - -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Events DCS -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Event function handling the birth of a unit. --- @param #NAVYGROUP self --- @param Core.Event#EVENTDATA EventData Event data. -function NAVYGROUP:OnEventBirth(EventData) - - -- Check that this is the right group. - if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then - local unit=EventData.IniUnit - local group=EventData.IniGroup - local unitname=EventData.IniUnitName - - if self.respawning then - - local function reset() - self.respawning=nil - end - - -- Reset switch in 1 sec. This should allow all birth events of n>1 groups to have passed. - -- TODO: Can I do this more rigorously? - self:ScheduleOnce(1, reset) - - else - - -- Get element. - local element=self:GetElementByName(unitname) - - -- Set element to spawned state. - self:T3(self.lid..string.format("EVENT: Element %s born ==> spawned", element.name)) - self:ElementSpawned(element) - - end - + if Target:IsInstanceOf("TARGET") then + self.engage.Target=Target + else + self.engage.Target=TARGET:New(Target) end + + -- Target coordinate. + self.engage.Coordinate=UTILS.DeepCopy(self.engage.Target:GetCoordinate()) + + + local intercoord=self:GetCoordinate():GetIntermediateCoordinate(self.engage.Coordinate, 0.9) + + + + -- Backup ROE and alarm state. + self.engage.roe=self:GetROE() + self.engage.alarmstate=self:GetAlarmstate() + + -- Switch ROE and alarm state. + self:SwitchAlarmstate(ENUMS.AlarmState.Auto) + self:SwitchROE(ENUMS.ROE.OpenFire) + + -- ID of current waypoint. + local uid=self:GetWaypointCurrent().uid + + -- Add waypoint after current. + self.engage.Waypoint=self:AddWaypoint(intercoord, nil, uid, Formation, true) + + -- Set if we want to resume route after reaching the detour waypoint. + self.engage.Waypoint.detour=1 end ---- Flightgroup event function handling the crash of a unit. +--- Update engage target. -- @param #NAVYGROUP self --- @param Core.Event#EVENTDATA EventData Event data. -function NAVYGROUP:OnEventDead(EventData) +function NAVYGROUP:_UpdateEngageTarget() - -- Check that this is the right group. - if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then - self:T(self.lid..string.format("EVENT: Unit %s dead!", EventData.IniUnitName)) + if self.engage.Target and self.engage.Target:IsAlive() then + + -- Get current position vector. + local vec3=self.engage.Target:GetVec3() + + -- Distance to last known position of target. + local dist=UTILS.VecDist3D(vec3, self.engage.Coordinate:GetVec3()) - local unit=EventData.IniUnit - local group=EventData.IniGroup - local unitname=EventData.IniUnitName + -- Check if target moved more than 100 meters. + if dist>100 then + + --env.info("FF Update Engage Target Moved "..self.engage.Target:GetName()) + + -- Update new position. + self.engage.Coordinate:UpdateFromVec3(vec3) - -- Get element. - local element=self:GetElementByName(unitname) - - if element then - self:T(self.lid..string.format("EVENT: Element %s dead ==> destroyed", element.name)) - self:ElementDestroyed(element) + -- ID of current waypoint. + local uid=self:GetWaypointCurrent().uid + + -- Remove current waypoint + self:RemoveWaypointByID(self.engage.Waypoint.uid) + + local intercoord=self:GetCoordinate():GetIntermediateCoordinate(self.engage.Coordinate, 0.9) + + -- Add waypoint after current. + self.engage.Waypoint=self:AddWaypoint(intercoord, nil, uid, Formation, true) + + -- Set if we want to resume route after reaching the detour waypoint. + self.engage.Waypoint.detour=0 + end + else + + -- Target not alive any more == Disengage. + self:Disengage() + end end ---- Flightgroup event function handling the crash of a unit. +--- On after "Disengage" event. -- @param #NAVYGROUP self --- @param Core.Event#EVENTDATA EventData Event data. -function NAVYGROUP:OnEventRemoveUnit(EventData) - - -- Check that this is the right group. - if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then - local unit=EventData.IniUnit - local group=EventData.IniGroup - local unitname=EventData.IniUnitName - - -- Get element. - local element=self:GetElementByName(unitname) - - if element then - self:T(self.lid..string.format("EVENT: Element %s removed ==> dead", element.name)) - self:ElementDead(element) - end +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function NAVYGROUP:onafterDisengage(From, Event, To) + self:T(self.lid.."Disengage Target") + -- Restore previous ROE and alarm state. + self:SwitchROE(self.engage.roe) + self:SwitchAlarmstate(self.engage.alarmstate) + + -- Remove current waypoint + if self.engage.Waypoint then + self:RemoveWaypointByID(self.engage.Waypoint.uid) end + -- Check group is done + self:_CheckGroupDone(1) end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Routing ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Add an a waypoint to the route. -- @param #NAVYGROUP self --- @param Core.Point#COORDINATE Coordinate The coordinate of the waypoint. Use COORDINATE:SetAltitude(altitude) to define the altitude. +-- @param Core.Point#COORDINATE Coordinate The coordinate of the waypoint. Use `COORDINATE:SetAltitude()` to define the altitude. -- @param #number Speed Speed in knots. Default is default cruise speed or 70% of max speed. -- @param #number AfterWaypointWithID Insert waypoint after waypoint given ID. Default is to insert as last waypoint. --- @param #number Depth Depth at waypoint in meters. Only for submarines. +-- @param #number Depth Depth at waypoint in feet. Only for submarines. -- @param #boolean Updateroute If true or nil, call UpdateRoute. If false, no call. -- @return Ops.OpsGroup#OPSGROUP.Waypoint Waypoint table. function NAVYGROUP:AddWaypoint(Coordinate, Speed, AfterWaypointWithID, Depth, Updateroute) - -- Check if a coordinate was given or at least a positionable. - if not Coordinate:IsInstanceOf("COORDINATE") then - if Coordinate:IsInstanceOf("POSITIONABLE") or Coordinate:IsInstanceOf("ZONE_BASE") then - self:T(self.lid.."WARNING: Coordinate is not a COORDINATE but a POSITIONABLE or ZONE. Trying to get coordinate") - Coordinate=Coordinate:GetCoordinate() - else - self:E(self.lid.."ERROR: Coordinate is neither a COORDINATE nor any POSITIONABLE or ZONE!") - return nil - end - end + -- Create coordinate. + local coordinate=self:_CoordinateFromObject(Coordinate) -- Set waypoint index. local wpnumber=self:GetWaypointIndexAfterID(AfterWaypointWithID) - -- Check if final waypoint is still passed. - if wpnumber>self.currentwp then - self.passedfinalwp=false - end - -- Speed in knots. Speed=Speed or self:GetSpeedCruise() -- Create a Naval waypoint. - local wp=Coordinate:WaypointNaval(UTILS.KnotsToKmph(Speed), Depth) + local wp=coordinate:WaypointNaval(UTILS.KnotsToKmph(Speed), Depth) -- Create waypoint data table. local waypoint=self:_CreateWaypoint(wp) + -- Set altitude. + if Depth then + waypoint.alt=UTILS.FeetToMeters(Depth) + end + -- Add waypoint to table. self:_AddWaypoint(waypoint, wpnumber) @@ -1170,7 +1499,7 @@ function NAVYGROUP:AddWaypoint(Coordinate, Speed, AfterWaypointWithID, Depth, Up -- Update route. if Updateroute==nil or Updateroute==true then - self:_CheckGroupDone(1) + self:__UpdateRoute(-0.01) end return waypoint @@ -1178,31 +1507,24 @@ end --- Initialize group parameters. Also initializes waypoints if self.waypoints is nil. -- @param #NAVYGROUP self +-- @param #table Template Template used to init the group. Default is `self.template`. -- @return #NAVYGROUP self -function NAVYGROUP:_InitGroup() +function NAVYGROUP:_InitGroup(Template) -- First check if group was already initialized. if self.groupinitialized then - self:E(self.lid.."WARNING: Group was already initialized!") + self:T(self.lid.."WARNING: Group was already initialized! Will NOT do it again!") return end -- Get template of group. - self.template=self.group:GetTemplate() + local template=Template or self:_GetTemplate() - -- Define category. - self.isAircraft=false - self.isNaval=true - self.isGround=false - - --TODO: Submarine check - --self.isSubmarine=self.group:IsSubmarine() - -- Ships are always AI. self.isAI=true -- Is (template) group late activated. - self.isLateActivated=self.template.lateActivation + self.isLateActivated=template.lateActivation -- Naval groups cannot be uncontrolled. self.isUncontrolled=false @@ -1218,8 +1540,8 @@ function NAVYGROUP:_InitGroup() -- Radio parameters from template. Default is set on spawn if not modified by the user. self.radio.On=true -- Radio is always on for ships. - self.radio.Freq=tonumber(self.template.units[1].frequency)/1000000 - self.radio.Modu=tonumber(self.template.units[1].modulation) + self.radio.Freq=tonumber(template.units[1].frequency)/1000000 + self.radio.Modu=tonumber(template.units[1].modulation) -- Set default formation. No really applicable for ships. self.optionDefault.Formation="Off Road" @@ -1235,63 +1557,19 @@ function NAVYGROUP:_InitGroup() -- Get all units of the group. local units=self.group:GetUnits() + + -- DCS group. + local dcsgroup=Group.getByName(self.groupname) + local size0=dcsgroup:getInitialSize() - for _,_unit in pairs(units) do - local unit=_unit --Wrapper.Unit#UNIT - - -- Get unit template. - local unittemplate=unit:GetTemplate() - - local element={} --#NAVYGROUP.Element - element.name=unit:GetName() - element.unit=unit - element.status=OPSGROUP.ElementStatus.INUTERO - element.typename=unit:GetTypeName() - element.skill=unittemplate.skill or "Unknown" - element.ai=true - element.category=element.unit:GetUnitCategory() - element.categoryname=element.unit:GetCategoryName() - element.size, element.length, element.height, element.width=unit:GetObjectSize() - element.ammo0=self:GetAmmoUnit(unit, false) - - -- Debug text. - if self.verbose>=2 then - local text=string.format("Adding element %s: status=%s, skill=%s, category=%s (%d), size: %.1f (L=%.1f H=%.1f W=%.1f)", - element.name, element.status, element.skill, element.categoryname, element.category, element.size, element.length, element.height, element.width) - self:I(self.lid..text) - end - - -- Add element to table. - table.insert(self.elements, element) - - -- Get Descriptors. - self.descriptors=self.descriptors or unit:GetDesc() - - -- Set type name. - self.actype=self.actype or unit:GetTypeName() - - if unit:IsAlive() then - -- Trigger spawned event. - self:ElementSpawned(element) - end - + -- Quick check. + if #units~=size0 then + self:E(self.lid..string.format("ERROR: Got #units=%d but group consists of %d units!", #units, size0)) end - - - -- Debug info. - if self.verbose>=1 then - local text=string.format("Initialized Navy Group %s:\n", self.groupname) - text=text..string.format("Unit type = %s\n", self.actype) - text=text..string.format("Speed max = %.1f Knots\n", UTILS.KmphToKnots(self.speedMax)) - text=text..string.format("Speed cruise = %.1f Knots\n", UTILS.KmphToKnots(self.speedCruise)) - text=text..string.format("Elements = %d\n", #self.elements) - text=text..string.format("Waypoints = %d\n", #self.waypoints) - text=text..string.format("Radio = %.1f MHz %s %s\n", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu), tostring(self.radio.On)) - text=text..string.format("Ammo = %d (G=%d/R=%d/M=%d/T=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Rockets, self.ammo.Missiles, self.ammo.Torpedos) - text=text..string.format("FSM state = %s\n", self:GetState()) - text=text..string.format("Is alive = %s\n", tostring(self:IsAlive())) - text=text..string.format("LateActivate = %s\n", tostring(self:IsLateActivated())) - self:I(self.lid..text) + + -- Add elemets. + for _,unit in pairs(units) do + self:_AddElementByName(unit:GetName()) end -- Init done. @@ -1346,8 +1624,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 @@ -1375,7 +1651,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:T(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 @@ -1549,6 +1825,9 @@ end -- @param #NAVYGROUP self -- @return #boolean If true, a path was found. function NAVYGROUP:_FindPathToNextWaypoint() + self:T3(self.lid.."Path finding") + + --TODO: Do not create a new ASTAR object each time this function is called but make it self.astar and reuse. Should be better for performance. -- Pathfinding A* local astar=ASTAR:New() @@ -1559,6 +1838,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 @@ -1575,16 +1859,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) @@ -1611,7 +1900,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 e581c5f11..58766d671 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -1,11 +1,11 @@ --- **Ops** - Generic group enhancement. --- +-- -- This class is **not** meant to be used itself by the end user. It contains common functionalities of derived classes for air, ground and sea. --- +-- -- === -- -- ### Author: **funkyfranky** --- +-- -- === -- @module Ops.OpsGroup -- @image OPS_OpsGroup.png @@ -14,26 +14,35 @@ --- OPSGROUP class. -- @type OPSGROUP -- @field #string ClassName Name of the class. --- @field #boolean Debug Debug mode. Messages to all about status. -- @field #number verbose Verbosity level. 0=silent. -- @field #string lid Class id string for output to DCS log file. -- @field #string groupname Name of the group. -- @field Wrapper.Group#GROUP group Group object. --- @field #table template Template of the group. +-- @field DCS#Controller controller The DCS controller of the group. +-- @field DCS#Template template Template table of the group. +-- @field #table elements Table of elements, i.e. units of the group. -- @field #boolean isLateActivated Is the group late activated. -- @field #boolean isUncontrolled Is the group uncontrolled. -- @field #boolean isFlightgroup Is a FLIGHTGROUP. -- @field #boolean isArmygroup Is an ARMYGROUP. -- @field #boolean isNavygroup Is a NAVYGROUP. --- @field #table elements Table of elements, i.e. units of the group. +-- @field #boolean isHelo If true, this is a helicopter group. +-- @field #boolean isVTOL If true, this is capable of Vertical TakeOff and Landing (VTOL). +-- @field #boolean isSubmarine If true, this is a submarine group. -- @field #boolean isAI If true, group is purely AI. --- @field #boolean isAircraft If true, group is airplane or helicopter. --- @field #boolean isNaval If true, group is ships or submarine. --- @field #boolean isGround If true, group is some ground unit. +-- @field #boolean isDestroyed If true, the whole group was destroyed. +-- @field #boolean isDead If true, the whole group is dead. -- @field #table waypoints Table of waypoints. -- @field #table waypoints0 Table of initial waypoints. +-- @field Wrapper.Airbase#AIRBASE homebase The home base of the flight group. +-- @field Wrapper.Airbase#AIRBASE destbase The destination base of the flight group. +-- @field Wrapper.Airbase#AIRBASE currbase The current airbase of the flight group, i.e. where it is currently located or landing at. +-- @field Core.Zone#ZONE homezone The home zone of the flight group. Set when spawn happens in air. +-- @field Core.Zone#ZONE destzone The destination zone of the flight group. Set when final waypoint is in air. -- @field #number currentwp Current waypoint index. This is the index of the last passed waypoint. -- @field #boolean adinfinitum Resume route at first waypoint when final waypoint is reached. +-- @field #number Twaiting Abs. mission time stamp when the group was ordered to wait. +-- @field #number dTwait Time to wait in seconds. Default `nil` (for ever). -- @field #table taskqueue Queue of tasks. -- @field #number taskcounter Running number of task ids. -- @field #number taskcurrent ID of current task. If 0, there is no current task assigned. @@ -49,9 +58,9 @@ -- @field #number speedWp Speed to the next waypoint in m/s. -- @field #boolean passedfinalwp Group has passed the final waypoint. -- @field #number wpcounter Running number counting waypoints. --- @field #boolean respawning Group is being respawned. -- @field Core.Set#SET_ZONE checkzones Set of zones. -- @field Core.Set#SET_ZONE inzones Set of zones in which the group is currently in. +-- @field Core.Timer#TIMER timerStatus Timer for status update. -- @field Core.Timer#TIMER timerCheckZone Timer for check zones. -- @field Core.Timer#TIMER timerQueueUpdate Timer for queue updates. -- @field #boolean groupinitialized If true, group parameters were initialized. @@ -59,9 +68,14 @@ -- @field Ops.Auftrag#AUFTRAG missionpaused Paused mission. -- @field #number Ndestroyed Number of destroyed units. -- @field #number Nkills Number kills of this groups. --- +-- +-- @field #boolean rearmOnOutOfAmmo If `true`, group will go to rearm once it runs out of ammo. +-- +-- @field Ops.Legion#LEGION legion Legion the group belongs to. +-- @field Ops.Cohort#COHORT cohort Cohort the group belongs to. +-- -- @field Core.Point#COORDINATE coordinate Current coordinate. --- +-- -- @field DCS#Vec3 position Position of the group at last status check. -- @field DCS#Vec3 positionLast Backup of last position vec to monitor changes. -- @field #number heading Heading of the group at last status check. @@ -70,31 +84,55 @@ -- @field DCS#Vec3 orientXLast Backup of last orientation to monitor changes. -- @field #number traveldist Distance traveled in meters. This is a lower bound. -- @field #number traveltime Time. --- +-- -- @field Core.Astar#ASTAR Astar path finding. -- @field #boolean ispathfinding If true, group is on pathfinding route. --- +-- +-- @field #boolean engagedetectedOn If `true`, auto engage detected targets. +-- @field #number engagedetectedRmax Max range in NM. Only detected targets within this radius from the group will be engaged. Default is 25 NM. +-- @field #table engagedetectedTypes Types of target attributes that will be engaged. See [DCS enum attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes). Default "All". +-- @field Core.Set#SET_ZONE engagedetectedEngageZones Set of zones in which targets are engaged. Default is anywhere. +-- @field Core.Set#SET_ZONE engagedetectedNoEngageZones Set of zones in which targets are *not* engaged. Default is nowhere. +-- -- @field #OPSGROUP.Radio radio Current radio settings. -- @field #OPSGROUP.Radio radioDefault Default radio settings. -- @field Core.RadioQueue#RADIOQUEUE radioQueue Radio queue. --- +-- -- @field #OPSGROUP.Beacon tacan Current TACAN settings. -- @field #OPSGROUP.Beacon tacanDefault Default TACAN settings. --- +-- -- @field #OPSGROUP.Beacon icls Current ICLS settings. -- @field #OPSGROUP.Beacon iclsDefault Default ICLS settings. --- +-- -- @field #OPSGROUP.Option option Current optional settings. -- @field #OPSGROUP.Option optionDefault Default option settings. --- +-- -- @field #OPSGROUP.Callsign callsign Current callsign settings. -- @field #OPSGROUP.Callsign callsignDefault Default callsign settings. --- +-- @field #string callsignName Callsign name. +-- @field #string callsignAlias Callsign alias. +-- -- @field #OPSGROUP.Spot spot Laser and IR spot. --- +-- -- @field #OPSGROUP.Ammo ammo Initial ammount of ammo. -- @field #OPSGROUP.WeaponData weaponData Weapon data table with key=BitType. --- +-- +-- @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. +-- @field Ops.OpsTransport#OPSTRANSPORT.TransportZoneCombo cargoTZC Transport zone combo (pickup, deploy etc.) currently used. +-- @field #string cargoStatus Cargo status of this group acting as cargo. +-- @field #number cargoTransportUID Unique ID of the transport assignment this cargo group is associated with. +-- @field #string carrierStatus Carrier status of this group acting as cargo carrier. +-- @field #OPSGROUP.CarrierLoader carrierLoader Carrier loader parameters. +-- @field #OPSGROUP.CarrierLoader carrierUnloader Carrier unloader parameters. +-- +-- @field #boolean useSRS Use SRS for transmissions. +-- @field Sound.SRS#MSRS msrs MOOSE SRS wrapper. +-- -- @extends Core.Fsm#FSM --- *A small group of determined and like-minded people can change the course of history.* --- Mahatma Gandhi @@ -104,17 +142,16 @@ -- ![Banner Image](..\Presentations\OPS\OpsGroup\_Main.png) -- -- # The OPSGROUP Concept --- --- The OPSGROUP class contains common functions used by other classes such as FLIGHGROUP, NAVYGROUP and ARMYGROUP. --- Those classes inherit everything of this class and extend it with features specific to their unit category. --- +-- +-- The OPSGROUP class contains common functions used by other classes such as FLIGHTGROUP, NAVYGROUP and ARMYGROUP. +-- Those classes inherit everything of this class and extend it with features specific to their unit category. +-- -- This class is **NOT** meant to be used by the end user itself. --- --- +-- +-- -- @field #OPSGROUP OPSGROUP = { ClassName = "OPSGROUP", - Debug = false, verbose = 0, lid = nil, groupname = nil, @@ -131,14 +168,13 @@ OPSGROUP = { taskenroute = nil, taskpaused = {}, missionqueue = {}, - currentmission = nil, + currentmission = nil, detectedunits = {}, detectedgroups = {}, attribute = nil, checkzones = nil, inzones = nil, groupinitialized = nil, - respawning = nil, wpcounter = 1, radio = {}, option = {}, @@ -149,20 +185,60 @@ OPSGROUP = { Ndestroyed = 0, Nkills = 0, weaponData = {}, + cargoqueue = {}, + cargoBay = {}, + mycarrier = {}, + carrierLoader = {}, + carrierUnloader = {}, } --- OPS group element. -- @type OPSGROUP.Element -- @field #string name Name of the element, i.e. the unit. +-- @field #string status The element status. See @{#OPSGROUP.ElementStatus}. -- @field Wrapper.Unit#UNIT unit The UNIT object. --- @field #string status The element status. +-- @field Wrapper.Group#GROUP group The GROUP object. +-- @field DCS#Unit DCSunit The DCS unit object. +-- @field #boolean ai If true, element is AI. +-- @field #string skill Skill level. +-- +-- @field Core.Zone#ZONE_POLYGON_BASE zoneBoundingbox Bounding box zone of the element unit. +-- @field Core.Zone#ZONE_POLYGON_BASE zoneLoad Loading zone. +-- @field Core.Zone#ZONE_POLYGON_BASE zoneUnload Unloading zone. +-- -- @field #string typename Type name. +-- @field #number category Aircraft category. +-- @field #string categoryname Aircraft category name. +-- +-- @field #number size Size (max of length, width, height) in meters. -- @field #number length Length of element in meters. -- @field #number width Width of element in meters. -- @field #number height Height of element in meters. +-- +-- @field DCS#Vec3 vec3 Last known 3D position vector. +-- @field DCS#Vec3 orientX Last known ordientation vector in the direction of the nose X. +-- @field #number heading Last known heading in degrees. +-- -- @field #number life0 Initial life points. -- @field #number life Life points when last updated. +-- @field #number damage Damage of element in percent. +-- +-- @field DCS#Object.Desc descriptors Descriptors table. +-- @field #number weightEmpty Empty weight in kg. +-- @field #number weightMaxTotal Max. total weight in kg. +-- @field #number weightMaxCargo Max. cargo weight in kg. +-- @field #number weightCargo Current cargo weight in kg. +-- @field #number weight Current weight including cargo in kg. +-- @field #table cargoBay Cargo bay. +-- +-- @field #string modex Tail number. +-- @field Wrapper.Client#CLIENT client The client if element is occupied by a human player. +-- @field #table pylons Table of pylons. +-- @field #number fuelmass Mass of fuel in kg. +-- @field #string callsign Call sign, e.g. "Uzi 1-1". +-- @field Wrapper.Airbase#AIRBASE.ParkingSpot parking The parking spot table the element is parking on. + --- Status of group element. -- @type OPSGROUP.ElementStatus @@ -216,6 +292,7 @@ OPSGROUP.TaskType={ --- Task structure. -- @type OPSGROUP.Task -- @field #string type Type of task: either SCHEDULED or WAYPOINT. +-- @field #boolean ismission This is an AUFTRAG task. -- @field #number id Task ID. Running number to get the task. -- @field #number prio Priority. -- @field #number time Abs. mission time when to execute the task. @@ -228,11 +305,6 @@ OPSGROUP.TaskType={ -- @field Core.UserFlag#USERFLAG stopflag If flag is set to 1 (=true), the task is stopped. -- @field #number backupROE Rules of engagement that are restored once the task is over. ---- Enroute task. --- @type OPSGROUP.EnrouteTask --- @field DCS#Task DCStask DCS task structure table. --- @field #number WaypointIndex Waypoint number at which the enroute task is added. - --- Beacon data. -- @type OPSGROUP.Beacon -- @field #number Channel Channel. @@ -252,9 +324,7 @@ OPSGROUP.TaskType={ -- @type OPSGROUP.Callsign -- @field #number NumberSquad Squadron number corresponding to a name like "Uzi". -- @field #number NumberGroup Group number. First number after name, e.g. "Uzi-**1**-1". --- @field #number NumberElement Element number.Second number after name, e.g. "Uzi-1-**1**" -- @field #string NameSquad Name of the squad, e.g. "Uzi". --- @field #string NameElement Name of group element, e.g. Uzi 11. --- Option data. -- @type OPSGROUP.Option @@ -306,6 +376,13 @@ OPSGROUP.TaskType={ -- @field #number MissilesAS Amount of anti-ship missiles. -- @field #number MissilesCR Amount of cruise missiles. -- @field #number MissilesBM Amount of ballistic missiles. +-- @field #number MissilesSA Amount of surfe-to-air missiles. + +--- Spawn point data. +-- @type OPSGROUP.Spawnpoint +-- @field Core.Point#COORDINATE Coordinate Coordinate where to spawn +-- @field Wrapper.Airbase#AIRBASE Airport Airport where to spawn. +-- @field #table TerminalIDs Terminal IDs, where to spawn the group. It is a table of `#number`s because a group can consist of multiple units. --- Waypoint data. -- @type OPSGROUP.Waypoint @@ -318,30 +395,93 @@ OPSGROUP.TaskType={ -- @field #string name Waypoint description. Shown in the F10 map. -- @field #number x Waypoint x-coordinate. -- @field #number y Waypoint y-coordinate. --- @field #boolean detour If true, this waypoint is not part of the normal route. +-- @field #number detour Signifies that this waypoint is not part of the normal route: 0=Hold, 1=Resume Route. -- @field #boolean intowind If true, this waypoint is a turn into wind route point. -- @field #boolean astar If true, this waypint was found by A* pathfinding algorithm. +-- @field #boolean temp If true, this is a temporary waypoint and will be deleted when passed. Also the passing waypoint FSM event is not triggered. -- @field #number npassed Number of times a groups passed this waypoint. -- @field Core.Point#COORDINATE coordinate Waypoint coordinate. -- @field Core.Point#COORDINATE roadcoord Closest point to road. -- @field #number roaddist Distance to closest point on road. -- @field Wrapper.Marker#MARKER marker Marker on the F10 map. -- @field #string formation Ground formation. Similar to action but on/off road. +-- @field #number missionUID Mission UID (Auftragsnr) this waypoint belongs to. ---- NavyGroup version. +--- Cargo Carrier status. +-- @type OPSGROUP.CarrierStatus +-- @field #string NOTCARRIER This group is not a carrier yet. +-- @field #string PICKUP Carrier is on its way to pickup cargo. +-- @field #string LOADING Carrier is loading cargo. +-- @field #string LOADED Carrier has loaded cargo. +-- @field #string TRANSPORTING Carrier is transporting cargo. +-- @field #string UNLOADING Carrier is unloading cargo. +OPSGROUP.CarrierStatus={ + NOTCARRIER="not carrier", + PICKUP="pickup", + LOADING="loading", + LOADED="loaded", + TRANSPORTING="transporting", + UNLOADING="unloading", +} + +--- Cargo status. +-- @type OPSGROUP.CargoStatus +-- @field #string AWAITING Group is awaiting carrier. +-- @field #string NOTCARGO This group is no cargo yet. +-- @field #string ASSIGNED Cargo is assigned to a carrier. +-- @field #string BOARDING Cargo is boarding a carrier. +-- @field #string LOADED Cargo is loaded into a carrier. +OPSGROUP.CargoStatus={ + AWAITING="Awaiting carrier", + NOTCARGO="not cargo", + ASSIGNED="assigned to carrier", + BOARDING="boarding", + LOADED="loaded", +} + +--- Cargo carrier loader parameters. +-- @type OPSGROUP.CarrierLoader +-- @field #string type Loader type "Front", "Back", "Left", "Right", "All". +-- @field #number length Length of (un-)loading zone in meters. +-- @field #number width Width of (un-)loading zone in meters. + +--- Data of the carrier that has loaded this 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. + +--- Element cargo bay data. +-- @type OPSGROUP.MyCargo +-- @field #OPSGROUP group The cargo group. +-- @field #boolean reserved If `true`, the cargo bay space is reserved but cargo has not actually been loaded yet. + +--- Cargo group data. +-- @type OPSGROUP.CargoGroup +-- @field #OPSGROUP opsgroup The cargo opsgroup. +-- @field #boolean delivered If `true`, group was delivered. +-- @field #boolean disembarkActivation If `true`, group is activated. If `false`, group is late activated. +-- @field #string status Status of the cargo group. Not used yet. + +--- OpsGroup version. -- @field #string version -OPSGROUP.version="0.7.1" +OPSGROUP.version="0.7.5" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - + -- TODO: AI on/off. +-- TODO: Emission on/off. -- TODO: Invisible/immortal. -- TODO: F10 menu. -- TODO: Add pseudo function. --- TODO: EPLRS datalink. --- TODO: Emission on/off. +-- TODO: Afterburner restrict. +-- TODO: What more options? +-- TODO: Damage? +-- TODO: Shot events? +-- TODO: Marks to add waypoints/tasks on-the-fly. +-- DONE: Options EPLRS ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor @@ -349,70 +489,142 @@ OPSGROUP.version="0.7.1" --- Create a new OPSGROUP class object. -- @param #OPSGROUP self --- @param Wrapper.Group#GROUP Group The group object. Can also be given by its group name as `#string`. +-- @param Wrapper.Group#GROUP group The GROUP object. Can also be given by its group name as `#string`. -- @return #OPSGROUP self -function OPSGROUP:New(Group) +function OPSGROUP:New(group) -- Inherit everything from FSM class. local self=BASE:Inherit(self, FSM:New()) -- #OPSGROUP - + -- Get group and group name. - if type(Group)=="string" then - self.groupname=Group + if type(group)=="string" then + self.groupname=group self.group=GROUP:FindByName(self.groupname) else - self.group=Group - self.groupname=Group:GetName() + self.group=group + self.groupname=group:GetName() end - + -- Set some string id for output to DCS.log file. self.lid=string.format("OPSGROUP %s | ", tostring(self.groupname)) - + + -- Check if group exists. if self.group then if not self:IsExist() then self:E(self.lid.."ERROR: GROUP does not exist! Returning nil") return nil end end - + + -- Set the template. + self:_SetTemplate() + + -- Set DCS group and controller. + self.dcsgroup=self:GetDCSGroup() + self.controller=self.dcsgroup:getController() + + -- Category. + self.category=self.dcsgroup:getCategory() + if self.category==Group.Category.GROUND then + self.isArmygroup=true + elseif self.category==Group.Category.TRAIN then + self.isArmygroup=true + self.isTrain=true + elseif self.category==Group.Category.SHIP then + self.isNavygroup=true + elseif self.category==Group.Category.AIRPLANE then + self.isFlightgroup=true + elseif self.category==Group.Category.HELICOPTER then + self.isFlightgroup=true + self.isHelo=true + else + + end + + local units=self.group:GetUnits() + + if units then + local masterunit=units[1] --Wrapper.Unit#UNIT + + -- Get Descriptors. + self.descriptors=masterunit:GetDesc() + + -- Set type name. + self.actype=masterunit:GetTypeName() + + -- Is this a submarine. + self.isSubmarine=masterunit:HasAttribute("Submarines") + + -- Has this a datalink? + self.isEPLRS=masterunit:HasAttribute("Datalink") + + if self:IsFlightgroup() then + + self.rangemax=self.descriptors.range and self.descriptors.range*1000 or 500*1000 + + self.ceiling=self.descriptors.Hmax + + self.tankertype=select(2, masterunit:IsTanker()) + self.refueltype=select(2, masterunit:IsRefuelable()) + + --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)) + + end + + end + -- Init set of detected units. self.detectedunits=SET_UNIT:New() - + -- Init set of detected groups. self.detectedgroups=SET_GROUP:New() - + -- Init inzone set. self.inzones=SET_ZONE:New() - + + -- Set Default altitude. + self:SetDefaultAltitude() + -- Laser. self.spot={} self.spot.On=false self.spot.timer=TIMER:New(self._UpdateLaser, self) self.spot.Coordinate=COORDINATE:New(0, 0, 0) self:SetLaser(1688, true, false, 0.5) - + + -- Cargo. + self.cargoStatus=OPSGROUP.CargoStatus.NOTCARGO + self.carrierStatus=OPSGROUP.CarrierStatus.NOTCARRIER + self:SetCarrierLoaderAllAspect() + self:SetCarrierUnloaderAllAspect() + -- Init task counter. self.taskcurrent=0 self.taskcounter=0 - + -- Start state. self:SetStartState("InUtero") -- Add FSM transitions. -- From State --> Event --> To State self:AddTransition("InUtero", "Spawned", "Spawned") -- The whole group was spawned. - self:AddTransition("*", "Dead", "Dead") -- The whole group is dead. + self:AddTransition("*", "Respawn", "InUtero") -- Respawn group. + self:AddTransition("*", "Dead", "InUtero") -- The whole group is dead and goes back to mummy. + self:AddTransition("*", "InUtero", "InUtero") -- Deactivated group goes back to mummy. self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. - self:AddTransition("*", "Status", "*") -- Status update. - - self:AddTransition("*", "Destroyed", "*") -- The whole group is dead. + self:AddTransition("*", "Destroyed", "*") -- The whole group is dead. self:AddTransition("*", "Damaged", "*") -- Someone in the group took damage. - self:AddTransition("*", "UpdateRoute", "*") -- Update route of group. Only if airborne. - self:AddTransition("*", "Respawn", "*") -- Respawn group. - self:AddTransition("*", "PassingWaypoint", "*") -- Passing waypoint. - + self:AddTransition("*", "UpdateRoute", "*") -- Update route of group. + + self:AddTransition("*", "PassingWaypoint", "*") -- Group passed a waypoint. + self:AddTransition("*", "PassedFinalWaypoint", "*") -- Group passed the waypoint. + self:AddTransition("*", "GotoWaypoint", "*") -- Group switches to a specific waypoint. + + self:AddTransition("*", "Wait", "*") -- Group will wait for further orders. + self:AddTransition("*", "DetectedUnit", "*") -- Unit was detected (again) in this detection cycle. self:AddTransition("*", "DetectedUnitNew", "*") -- Add a newly detected unit to the detected units set. self:AddTransition("*", "DetectedUnitKnown", "*") -- A known unit is still detected. @@ -421,16 +633,18 @@ function OPSGROUP:New(Group) self:AddTransition("*", "DetectedGroup", "*") -- Unit was detected (again) in this detection cycle. self:AddTransition("*", "DetectedGroupNew", "*") -- Add a newly detected unit to the detected units set. self:AddTransition("*", "DetectedGroupKnown", "*") -- A known unit is still detected. - self:AddTransition("*", "DetectedGroupLost", "*") -- Group lost a detected target group. - - self:AddTransition("*", "PassingWaypoint", "*") -- Group passed a waypoint. - self:AddTransition("*", "GotoWaypoint", "*") -- Group switches to a specific waypoint. + self:AddTransition("*", "DetectedGroupLost", "*") -- Group lost a detected target group. self:AddTransition("*", "OutOfAmmo", "*") -- Group is completely out of ammo. self:AddTransition("*", "OutOfGuns", "*") -- Group is out of gun shells. self:AddTransition("*", "OutOfRockets", "*") -- Group is out of rockets. self:AddTransition("*", "OutOfBombs", "*") -- Group is out of bombs. self:AddTransition("*", "OutOfMissiles", "*") -- Group is out of missiles. + self:AddTransition("*", "OutOfTorpedos", "*") -- Group is out of torpedos. + + self:AddTransition("*", "OutOfMissilesAA", "*") -- Group is out of A2A (air) missiles. + self:AddTransition("*", "OutOfMissilesAG", "*") -- Group is out of A2G (ground) missiles. + self:AddTransition("*", "OutOfMissilesAS", "*") -- Group is out of A2S (ship) missiles. self:AddTransition("*", "EnterZone", "*") -- Group entered a certain zone. self:AddTransition("*", "LeaveZone", "*") -- Group leaves a certain zone. @@ -447,19 +661,38 @@ function OPSGROUP:New(Group) self:AddTransition("*", "TaskPause", "*") -- Pause current task. Not implemented yet! self:AddTransition("*", "TaskCancel", "*") -- Cancel current task. self:AddTransition("*", "TaskDone", "*") -- Task is over. - + self:AddTransition("*", "MissionStart", "*") -- Mission is started. self:AddTransition("*", "MissionExecute", "*") -- Mission execution began. - self:AddTransition("*", "MissionCancel", "*") -- Cancel current mission. + self:AddTransition("*", "MissionCancel", "*") -- Cancel current mission. self:AddTransition("*", "PauseMission", "*") -- Pause the current mission. self:AddTransition("*", "UnpauseMission", "*") -- Unpause the the paused mission. self:AddTransition("*", "MissionDone", "*") -- Mission is over. + self:AddTransition("*", "ElementInUtero", "*") -- An element is in utero again. self:AddTransition("*", "ElementSpawned", "*") -- An element was spawned. self:AddTransition("*", "ElementDestroyed", "*") -- An element was destroyed. self:AddTransition("*", "ElementDead", "*") -- An element is dead. self:AddTransition("*", "ElementDamaged", "*") -- An element was damaged. + self:AddTransition("*", "Board", "*") -- Group is ordered to board the carrier. + self:AddTransition("*", "Embarked", "*") -- Group was loaded into a cargo carrier. + self:AddTransition("*", "Disembarked", "*") -- Group was unloaded from a cargo carrier. + + self:AddTransition("*", "Pickup", "*") -- Carrier and is on route to pick up cargo. + self:AddTransition("*", "Loading", "*") -- Carrier is loading cargo. + self:AddTransition("*", "Load", "*") -- Carrier loads cargo into carrier. + self:AddTransition("*", "Loaded", "*") -- Carrier loaded cargo into carrier. + self:AddTransition("*", "LoadingDone", "*") -- Carrier loaded all assigned/possible cargo into carrier. + self:AddTransition("*", "Transport", "*") -- Carrier is transporting cargo. + self:AddTransition("*", "Unloading", "*") -- Carrier is unloading the cargo. + self:AddTransition("*", "Unload", "*") -- Carrier unloads a cargo group. + self:AddTransition("*", "Unloaded", "*") -- Carrier unloaded a cargo group. + self:AddTransition("*", "UnloadingDone", "*") -- Carrier unloaded all its current cargo. + self:AddTransition("*", "Delivered", "*") -- Carrier delivered ALL cargo of the transport assignment. + + self:AddTransition("*", "TransportCancel", "*") -- Cancel (current) transport. + ------------------------ --- Pseudo Functions --- ------------------------ @@ -481,9 +714,109 @@ function OPSGROUP:New(Group) -- @param #OPSGROUP self -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "MissionStart". + -- @function [parent=#OPSGROUP] MissionStart + -- @param #OPSGROUP self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "MissionStart" after a delay. + -- @function [parent=#OPSGROUP] __MissionStart + -- @param #OPSGROUP self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "MissionStart" event. + -- @function [parent=#OPSGROUP] OnAfterMissionStart + -- @param #OPSGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + + --- Triggers the FSM event "MissionExecute". + -- @function [parent=#OPSGROUP] MissionExecute + -- @param #OPSGROUP self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "MissionExecute" after a delay. + -- @function [parent=#OPSGROUP] __MissionExecute + -- @param #OPSGROUP self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "MissionExecute" event. + -- @function [parent=#OPSGROUP] OnAfterMissionExecute + -- @param #OPSGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + + --- Triggers the FSM event "MissionCancel". + -- @function [parent=#OPSGROUP] MissionCancel + -- @param #OPSGROUP self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "MissionCancel" after a delay. + -- @function [parent=#OPSGROUP] __MissionCancel + -- @param #OPSGROUP self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "MissionCancel" event. + -- @function [parent=#OPSGROUP] OnAfterMissionCancel + -- @param #OPSGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + + --- Triggers the FSM event "MissionDone". + -- @function [parent=#OPSGROUP] MissionDone + -- @param #OPSGROUP self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "MissionDone" after a delay. + -- @function [parent=#OPSGROUP] __MissionDone + -- @param #OPSGROUP self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "MissionDone" event. + -- @function [parent=#OPSGROUP] OnAfterMissionDone + -- @param #OPSGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + + --- Triggers the FSM event "TransportCancel". + -- @function [parent=#OPSGROUP] TransportCancel + -- @param #OPSGROUP self + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + --- Triggers the FSM event "TransportCancel" after a delay. + -- @function [parent=#OPSGROUP] __TransportCancel + -- @param #OPSGROUP self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + --- On after "TransportCancel" event. + -- @function [parent=#OPSGROUP] OnAfterTransportCancel + -- @param #OPSGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + -- TODO: Add pseudo functions. - return self + return self end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -499,12 +832,35 @@ end --- Returns the absolute (average) life points of the group. -- @param #OPSGROUP self +-- @param #OPSGROUP.Element Element (Optional) Only get life points of this element. -- @return #number Life points. If group contains more than one element, the average is given. -- @return #number Initial life points. -function OPSGROUP:GetLifePoints() - if self.group then - return self.group:GetLife(), self.group:GetLife0() +function OPSGROUP:GetLifePoints(Element) + + local life=0 + local life0=0 + + if Element then + + local unit=Element.unit + + if unit then + life=unit:GetLife() + life0=unit:GetLife0() + life=math.min(life, life0) -- Some units have more life than life0 returns! + end + + else + + for _,element in pairs(self.elements) do + local l,l0=self:GetLifePoints(element) + life=life+l + life0=life+l0 + end + end + + return life, life0 end @@ -517,6 +873,15 @@ function OPSGROUP:SetVerbosity(VerbosityLevel) return self end +--- Set legion this ops group belongs to. +-- @param #OPSGROUP self +-- @param Ops.Legion#LEGION Legion The Legion. +-- @return #OPSGROUP self +function OPSGROUP:_SetLegion(Legion) + self.legion=Legion + return self +end + --- Set default cruise speed. -- @param #OPSGROUP self -- @param #number Speed Speed in knots. @@ -532,20 +897,51 @@ end -- @param #OPSGROUP self -- @return #number Cruise speed (>0) in knots. function OPSGROUP:GetSpeedCruise() - return UTILS.KmphToKnots(self.speedCruise or self.speedMax*0.7) + local speed=UTILS.KmphToKnots(self.speedCruise or self.speedMax*0.7) + return speed +end + +--- Set default cruise altitude. +-- @param #OPSGROUP self +-- @param #number Altitude Altitude in feet. Default is 10,000 ft for airplanes and 1,500 feet for helicopters. +-- @return #OPSGROUP self +function OPSGROUP:SetDefaultAltitude(Altitude) + if Altitude then + self.altitudeCruise=UTILS.FeetToMeters(Altitude) + else + if self:IsFlightgroup() then + if self.isHelo then + self.altitudeCruise=UTILS.FeetToMeters(1500) + else + self.altitudeCruise=UTILS.FeetToMeters(10000) + end + else + self.altitudeCruise=0 + end + end + return self +end + +--- Get default cruise speed. +-- @param #OPSGROUP self +-- @return #number Cruise altitude in feet. +function OPSGROUP:GetCruiseAltitude() + local alt=UTILS.MetersToFeet(self.altitudeCruise) + return alt end --- Set detection on or off. --- If detection is on, detected targets of the group will be evaluated and FSM events triggered. +-- If detection is on, detected targets of the group will be evaluated and FSM events triggered. -- @param #OPSGROUP self -- @param #boolean Switch If `true`, detection is on. If `false` or `nil`, detection is off. Default is off. -- @return #OPSGROUP self function OPSGROUP:SetDetection(Switch) + self:T(self.lid..string.format("Detection is %s", tostring(Switch))) self.detectionOn=Switch return self end ---- Set LASER parameters. +--- Set LASER parameters. -- @param #OPSGROUP self -- @param #number Code Laser code. Default 1688. -- @param #boolean CheckLOS Check if lasing unit has line of sight to target coordinate. Default is `true`. @@ -564,7 +960,7 @@ function OPSGROUP:SetLaser(Code, CheckLOS, IROff, UpdateTime) return self end ---- Get LASER code. +--- Get LASER code. -- @param #OPSGROUP self -- @return #number Current Laser code. function OPSGROUP:GetLaserCode() @@ -607,7 +1003,7 @@ function OPSGROUP:AddCheckZone(CheckZone) end ---- Add a weapon range for ARTY auftrag. +--- Add a weapon range for ARTY auftrag. -- @param #OPSGROUP self -- @param #number RangeMin Minimum range in nautical miles. Default 0 NM. -- @param #number RangeMax Maximum range in nautical miles. Default 10 NM. @@ -624,9 +1020,9 @@ function OPSGROUP:AddWeaponRange(RangeMin, RangeMax, BitType) weapon.RangeMax=RangeMax weapon.RangeMin=RangeMin - self.weaponData=self.weaponData or {} - self.weaponData[weapon.BitType]=weapon - + self.weaponData=self.weaponData or {} + self.weaponData[tostring(weapon.BitType)]=weapon + return self end @@ -636,12 +1032,12 @@ end -- @return #OPSGROUP.WeaponData Weapon range data. function OPSGROUP:GetWeaponData(BitType) - BitType=BitType or ENUMS.WeaponFlag.Auto + BitType=tostring(BitType or ENUMS.WeaponFlag.Auto) - if self.weaponData[BitType] then + if self.weaponData[BitType] then return self.weaponData[BitType] else - return self.weaponData[ENUMS.WeaponFlag.Auto] + return self.weaponData[tostring(ENUMS.WeaponFlag.Auto)] end end @@ -682,22 +1078,22 @@ function OPSGROUP:GetThreat(ThreatLevelMin, ThreatLevelMax) local level=0 for _,_unit in pairs(self.detectedunits:GetSet()) do local unit=_unit --Wrapper.Unit#UNIT - + -- Get threatlevel of unit. local threatlevel=unit:GetThreatLevel() - + -- Check if withing threasholds. if threatlevel>=ThreatLevelMin and threatlevel<=ThreatLevelMax then - + if threatlevellevelmax then threat=unit levelmax=threatlevel end - + end return threat, levelmax end +--- Enable to automatically engage detected targets. +-- @param #OPSGROUP self +-- @param #number RangeMax Max range in NM. Only detected targets within this radius from the group will be engaged. Default is 25 NM. +-- @param #table TargetTypes Types of target attributes that will be engaged. See [DCS enum attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes). Default "All". +-- @param Core.Set#SET_ZONE EngageZoneSet Set of zones in which targets are engaged. Default is anywhere. +-- @param Core.Set#SET_ZONE NoEngageZoneSet Set of zones in which targets are *not* engaged. Default is nowhere. +-- @return #OPSGROUP self +function OPSGROUP:SetEngageDetectedOn(RangeMax, TargetTypes, EngageZoneSet, NoEngageZoneSet) + + -- Ensure table. + if TargetTypes then + if type(TargetTypes)~="table" then + TargetTypes={TargetTypes} + end + else + TargetTypes={"All"} + end + + -- Ensure SET_ZONE if ZONE is provided. + if EngageZoneSet and EngageZoneSet:IsInstanceOf("ZONE_BASE") then + local zoneset=SET_ZONE:New():AddZone(EngageZoneSet) + EngageZoneSet=zoneset + end + if NoEngageZoneSet and NoEngageZoneSet:IsInstanceOf("ZONE_BASE") then + local zoneset=SET_ZONE:New():AddZone(NoEngageZoneSet) + NoEngageZoneSet=zoneset + end + + -- Set parameters. + self.engagedetectedOn=true + self.engagedetectedRmax=UTILS.NMToMeters(RangeMax or 25) + self.engagedetectedTypes=TargetTypes + self.engagedetectedEngageZones=EngageZoneSet + self.engagedetectedNoEngageZones=NoEngageZoneSet + + -- Debug info. + self:T(self.lid..string.format("Engage detected ON: Rmax=%d NM", UTILS.MetersToNM(self.engagedetectedRmax))) + + -- Ensure detection is ON or it does not make any sense. + self:SetDetection(true) + + return self +end + +--- Disable to automatically engage detected targets. +-- @param #OPSGROUP self +-- @return #OPSGROUP self +function OPSGROUP:SetEngageDetectedOff() + self:T(self.lid..string.format("Engage detected OFF")) + self.engagedetectedOn=false + return self +end + +--- Set that group is going to rearm once it runs out of ammo. +-- @param #OPSGROUP self +-- @return #OPSGROUP self +function OPSGROUP:SetRearmOnOutOfAmmo() + self.rearmOnOutOfAmmo=true + return self +end + --- Check if an element of the group has line of sight to a coordinate. -- @param #OPSGROUP self -- @param Core.Point#COORDINATE Coordinate The position to which we check the LoS. @@ -742,8 +1199,8 @@ function OPSGROUP:HasLoS(Coordinate, Element, OffsetElement, OffsetCoordinate) end --- Function to check LoS for an element of the group. - local function checklos(element) - local vec3=element.unit:GetVec3() + local function checklos(element) + local vec3=element.unit:GetVec3() if OffsetElement then vec3=UTILS.VecAdd(vec3, OffsetElement) end @@ -752,19 +1209,19 @@ function OPSGROUP:HasLoS(Coordinate, Element, OffsetElement, OffsetCoordinate) return _los end - if Element then + if Element then local los=checklos(Element) return los else - + for _,element in pairs(self.elements) do -- Get LoS of this element. - local los=checklos(element) + local los=checklos(element) if los then return true end end - + return false end @@ -800,12 +1257,12 @@ end function OPSGROUP:GetUnit(UnitNumber) local DCSUnit=self:GetDCSUnit(UnitNumber) - + if DCSUnit then local unit=UNIT:Find(DCSUnit) return unit end - + return nil end @@ -816,12 +1273,12 @@ end function OPSGROUP:GetDCSUnit(UnitNumber) local DCSGroup=self:GetDCSGroup() - + if DCSGroup then local unit=DCSGroup:getUnit(UnitNumber or 1) return unit end - + return nil end @@ -831,132 +1288,24 @@ end function OPSGROUP:GetDCSUnits() local DCSGroup=self:GetDCSGroup() - + if DCSGroup then local units=DCSGroup:getUnits() return units end - + return nil end ---- Despawn the group. The whole group is despawned and (optionally) a "Remove Unit" event is generated for all current units of the group. --- @param #OPSGROUP self --- @param #number Delay Delay in seconds before the group will be despawned. Default immediately. --- @param #boolean NoEventRemoveUnit If true, no event "Remove Unit" is generated. --- @return #OPSGROUP self -function OPSGROUP:Despawn(Delay, NoEventRemoveUnit) - - if Delay and Delay>0 then - self:ScheduleOnce(Delay, OPSGROUP.Despawn, self, 0, NoEventRemoveUnit) - else - - local DCSGroup=self:GetDCSGroup() - - if DCSGroup then - - -- Destroy DCS group. - DCSGroup:destroy() - - if not NoEventRemoveUnit then - - -- Get all units. - local units=self:GetDCSUnits() - - -- Create a "Remove Unit" event. - local EventTime=timer.getTime() - for i=1,#units do - self:CreateEventRemoveUnit(EventTime, units[i]) - end - - end - end - end - - return self -end - ---- Destroy group. The whole group is despawned and a *Unit Lost* for aircraft or *Dead* event for ground/naval units is generated for all current units. --- @param #OPSGROUP self --- @param #number Delay Delay in seconds before the group will be destroyed. Default immediately. --- @return #OPSGROUP self -function OPSGROUP:Destroy(Delay) - - if Delay and Delay>0 then - self:ScheduleOnce(Delay, OPSGROUP.Destroy, self) - else - - local DCSGroup=self:GetDCSGroup() - - if DCSGroup then - - self:T(self.lid.."Destroying group") - - -- Destroy DCS group. - DCSGroup:destroy() - - -- Get all units. - local units=self:GetDCSUnits() - - -- Create a "Unit Lost" event. - local EventTime=timer.getTime() - for i=1,#units do - if self.isAircraft then - self:CreateEventUnitLost(EventTime, units[i]) - else - self:CreateEventDead(EventTime, units[i]) - end - end - end - - end - - return self -end - ---- Despawn an element/unit of the group. --- @param #OPSGROUP self --- @param #OPSGROUP.Element Element The element that will be despawned. --- @param #number Delay Delay in seconds before the element will be despawned. Default immediately. --- @param #boolean NoEventRemoveUnit If true, no event "Remove Unit" is generated. --- @return #OPSGROUP self -function OPSGROUP:DespawnElement(Element, Delay, NoEventRemoveUnit) - - if Delay and Delay>0 then - self:ScheduleOnce(Delay, OPSGROUP.DespawnElement, self, Element, 0, NoEventRemoveUnit) - else - - if Element then - - -- Get DCS unit object. - local DCSunit=Unit.getByName(Element.name) - - if DCSunit then - - -- Destroy object. - DCSunit:destroy() - - -- Create a remove unit event. - if not NoEventRemoveUnit then - self:CreateEventRemoveUnit(timer.getTime(), DCSunit) - end - - end - - end - - end - - return self -end --- Get current 2D position vector of the group. -- @param #OPSGROUP self +-- @param #string UnitName (Optional) Get position of a specifc unit of the group. Default is the first existing unit in the group. -- @return DCS#Vec2 Vector with x,y components. -function OPSGROUP:GetVec2() +function OPSGROUP:GetVec2(UnitName) + + local vec3=self:GetVec3(UnitName) - local vec3=self:GetVec3() - if vec3 then local vec2={x=vec3.x, y=vec3.z} return vec2 @@ -968,34 +1317,59 @@ end --- Get current 3D position vector of the group. -- @param #OPSGROUP self +-- @param #string UnitName (Optional) Get position of a specifc unit of the group. Default is the first existing unit in the group. -- @return DCS#Vec3 Vector with x,y,z components. -function OPSGROUP:GetVec3() - if self:IsExist() then - - local unit=self:GetDCSUnit() - - if unit then - local vec3=unit:getPoint() - +function OPSGROUP:GetVec3(UnitName) + + local vec3=nil --DCS#Vec3 + + -- First check if this group is loaded into a carrier + 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 end - end + + if self:IsExist() then + + local unit=nil --DCS#Unit + if UnitName then + unit=Unit.getByName(UnitName) + else + unit=self:GetDCSUnit() + end + + + if unit then + local vec3=unit:getPoint() + return vec3 + end + + end + + -- Return last known position. + if self.position then + return self.position + end + return nil end ---- Get current coordinate of the group. +--- Get current coordinate of the group. If the current position cannot be determined, the last known position is returned. -- @param #OPSGROUP self -- @param #boolean NewObject Create a new coordiante object. -- @return Core.Point#COORDINATE The coordinate (of the first unit) of the group. function OPSGROUP:GetCoordinate(NewObject) - local vec3=self:GetVec3() + local vec3=self:GetVec3() or self.position --DCS#Vec3 if vec3 then - + self.coordinate=self.coordinate or COORDINATE:New(0,0,0) - + self.coordinate.x=vec3.x self.coordinate.y=vec3.y self.coordinate.z=vec3.z @@ -1005,100 +1379,126 @@ function OPSGROUP:GetCoordinate(NewObject) return coord else return self.coordinate - end + end else - self:E(self.lid.."WARNING: Group is not alive. Cannot get coordinate!") + self:E(self.lid.."WARNING: Cannot get coordinate!") end - + return nil end --- Get current velocity of the group. -- @param #OPSGROUP self +-- @param #string UnitName (Optional) Get heading of a specific unit of the group. Default is from the first existing unit in the group. -- @return #number Velocity in m/s. -function OPSGROUP:GetVelocity() +function OPSGROUP:GetVelocity(UnitName) + if self:IsExist() then - - local unit=self:GetDCSUnit(1) - - if unit then - - local velvec3=unit:getVelocity() - - local vel=UTILS.VecNorm(velvec3) - - return vel - + + local unit=nil --DCS#Unit + + if UnitName then + unit=Unit.getByName(UnitName) + else + unit=self:GetDCSUnit() end + + if unit then + + local velvec3=unit:getVelocity() + + local vel=UTILS.VecNorm(velvec3) + + return vel + + else + self:E(self.lid.."WARNING: Unit does not exist. Cannot get velocity!") + end + else self:E(self.lid.."WARNING: Group does not exist. Cannot get velocity!") end + return nil end ---- Get current heading of the group. +--- Get current heading of the group or (optionally) of a specific unit of the group. -- @param #OPSGROUP self +-- @param #string UnitName (Optional) Get heading of a specific unit of the group. Default is from the first existing unit in the group. -- @return #number Current heading of the group in degrees. -function OPSGROUP:GetHeading() +function OPSGROUP:GetHeading(UnitName) if self:IsExist() then - - local unit=self:GetDCSUnit() - + + local unit=nil --DCS#Unit + if UnitName then + unit=Unit.getByName(UnitName) + else + unit=self:GetDCSUnit() + end + if unit then - + local pos=unit:getPosition() - + local heading=math.atan2(pos.x.z, pos.x.x) - + if heading<0 then heading=heading+ 2*math.pi end - + heading=math.deg(heading) - + return heading end - + else self:E(self.lid.."WARNING: Group does not exist. Cannot get heading!") end - + return nil end ---- Get current orientation of the first unit in the group. +--- Get current orientation of the group. -- @param #OPSGROUP self +-- @param #string UnitName (Optional) Get orientation of a specific unit of the group. Default is the first existing unit of the group. -- @return DCS#Vec3 Orientation X parallel to where the "nose" is pointing. -- @return DCS#Vec3 Orientation Y pointing "upwards". -- @return DCS#Vec3 Orientation Z perpendicular to the "nose". -function OPSGROUP:GetOrientation() +function OPSGROUP:GetOrientation(UnitName) if self:IsExist() then - - local unit=self:GetDCSUnit() - + + local unit=nil --DCS#Unit + + if UnitName then + unit=Unit.getByName(UnitName) + else + unit=self:GetDCSUnit() + end + if unit then - + local pos=unit:getPosition() - + return pos.x, pos.y, pos.z end - + else self:E(self.lid.."WARNING: Group does not exist. Cannot get orientation!") end - + return nil end ---- Get current orientation of the first unit in the group. +--- Get current "X" orientation of the first unit in the group. -- @param #OPSGROUP self +-- @param #string UnitName (Optional) Get orientation of a specific unit of the group. Default is the first existing unit of the group. -- @return DCS#Vec3 Orientation X parallel to where the "nose" is pointing. -function OPSGROUP:GetOrientationX() +function OPSGROUP:GetOrientationX(UnitName) + + local X,Y,Z=self:GetOrientation(UnitName) - local X,Y,Z=self:GetOrientation() - return X end @@ -1122,6 +1522,189 @@ function OPSGROUP:CheckTaskDescriptionUnique(description) end +--- Despawn a unit of the group. A "Remove Unit" event is generated by default. +-- @param #OPSGROUP self +-- @param #string UnitName Name of the unit +-- @param #number Delay Delay in seconds before the group will be despawned. Default immediately. +-- @param #boolean NoEventRemoveUnit If true, no event "Remove Unit" is generated. +-- @return #OPSGROUP self +function OPSGROUP:DespawnUnit(UnitName, Delay, NoEventRemoveUnit) + + -- Debug info. + self:T(self.lid.."Despawn element "..tostring(UnitName)) + + -- Get element. + local element=self:GetElementByName(UnitName) + + if element then + + -- Get DCS unit object. + local DCSunit=Unit.getByName(UnitName) + + if DCSunit then + + -- Despawn unit. + DCSunit:destroy() + + -- Element goes back in utero. + self:ElementInUtero(element) + + if not NoEventRemoveUnit then + self:CreateEventRemoveUnit(timer.getTime(), DCSunit) + end + + end + + end + +end + +--- Despawn an element/unit of the group. +-- @param #OPSGROUP self +-- @param #OPSGROUP.Element Element The element that will be despawned. +-- @param #number Delay Delay in seconds before the element will be despawned. Default immediately. +-- @param #boolean NoEventRemoveUnit If true, no event "Remove Unit" is generated. +-- @return #OPSGROUP self +function OPSGROUP:DespawnElement(Element, Delay, NoEventRemoveUnit) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP.DespawnElement, self, Element, 0, NoEventRemoveUnit) + else + + if Element then + + -- Get DCS unit object. + local DCSunit=Unit.getByName(Element.name) + + if DCSunit then + + -- Destroy object. + DCSunit:destroy() + + -- Create a remove unit event. + if not NoEventRemoveUnit then + self:CreateEventRemoveUnit(timer.getTime(), DCSunit) + end + + end + + end + + end + + return self +end + +--- Despawn the group. The whole group is despawned and a "`Remove Unit`" event is generated for all current units of the group. +-- If no `Remove Unit` event should be generated, the second optional parameter needs to be set to `true`. +-- If this group belongs to an AIRWING, BRIGADE or FLEET, it will be added to the warehouse stock if the `NoEventRemoveUnit` parameter is `false` or `nil`. +-- @param #OPSGROUP self +-- @param #number Delay Delay in seconds before the group will be despawned. Default immediately. +-- @param #boolean NoEventRemoveUnit If `true`, **no** event "Remove Unit" is generated. +-- @return #OPSGROUP self +function OPSGROUP:Despawn(Delay, NoEventRemoveUnit) + + if Delay and Delay>0 then + self.scheduleIDDespawn=self:ScheduleOnce(Delay, OPSGROUP.Despawn, self, 0, NoEventRemoveUnit) + else + + if self.legion and not NoEventRemoveUnit then + -- Add asset back in 10 seconds. + self:T(self.lid..string.format("Despawning Group by adding asset to LEGION!")) + self.legion:AddAsset(self.group, 1) + return + end + + -- Debug info. + self:T(self.lid..string.format("Despawning Group!")) + + -- DCS group obejct. + local DCSGroup=self:GetDCSGroup() + + if DCSGroup then + + -- Clear any task ==> makes DCS crash! + --self.group:ClearTasks() + + -- Get all units. + local units=self:GetDCSUnits() + + for i=1,#units do + local unit=units[i] + if unit then + local name=unit:getName() + if name then + -- Despawn the unit. + self:DespawnUnit(name, 0, NoEventRemoveUnit) + end + end + end + + end + end + + return self +end + +--- Destroy a unit of the group. A *Unit Lost* for aircraft or *Dead* event for ground/naval units is generated. +-- @param #OPSGROUP self +-- @param #string UnitName Name of the unit which should be destroyed. +-- @param #number Delay Delay in seconds before the group will be destroyed. Default immediately. +-- @return #OPSGROUP self +function OPSGROUP:DestroyUnit(UnitName, Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP.DestroyUnit, self, UnitName, 0) + else + + local unit=Unit.getByName(UnitName) + + if unit then + + -- Create a "Unit Lost" event. + local EventTime=timer.getTime() + + if self:IsFlightgroup() then + self:CreateEventUnitLost(EventTime, unit) + else + self:CreateEventDead(EventTime, unit) + end + + end + + end + +end + +--- Destroy group. The whole group is despawned and a *Unit Lost* for aircraft or *Dead* event for ground/naval units is generated for all current units. +-- @param #OPSGROUP self +-- @param #number Delay Delay in seconds before the group will be destroyed. Default immediately. +-- @return #OPSGROUP self +function OPSGROUP:Destroy(Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP.Destroy, self, 0) + else + + -- Get all units. + local units=self:GetDCSUnits() + + if units then + + -- Create a "Unit Lost" event. + for _,unit in pairs(units) do + if unit then + self:DestroyUnit(unit:getName()) + end + end + + end + + end + + return self +end + --- Activate a *late activated* group. -- @param #OPSGROUP self -- @param #number delay (Optional) Delay in seconds before the group is activated. Default is immediately. @@ -1129,22 +1712,47 @@ end function OPSGROUP:Activate(delay) if delay and delay>0 then - self:T2(self.lid..string.format("Activating late activated group in %d seconds", delay)) - self:ScheduleOnce(delay, OPSGROUP.Activate, self) + self:T2(self.lid..string.format("Activating late activated group in %d seconds", delay)) + self:ScheduleOnce(delay, OPSGROUP.Activate, self) else - + if self:IsAlive()==false then - + self:T(self.lid.."Activating late activated group") self.group:Activate() self.isLateActivated=false - + elseif self:IsAlive()==true then self:E(self.lid.."WARNING: Activating group that is already activated") else self:E(self.lid.."ERROR: Activating group that is does not exist!") end - + + end + + return self +end + +--- Deactivate the group. Group will be respawned in late activated state. +-- @param #OPSGROUP self +-- @param #number delay (Optional) Delay in seconds before the group is deactivated. Default is immediately. +-- @return #OPSGROUP self +function OPSGROUP:Deactivate(delay) + + if delay and delay>0 then + self:ScheduleOnce(delay, OPSGROUP.Deactivate, self) + else + + if self:IsAlive()==true then + + self.template.lateActivation=true + + local template=UTILS.DeepCopy(self.template) + + self:_Respawn(0, template) + + end + end return self @@ -1153,26 +1761,253 @@ end --- Self destruction of group. An explosion is created at the position of each element. -- @param #OPSGROUP self -- @param #number Delay Delay in seconds. Default now. --- @param #number ExplosionPower (Optional) Explosion power in kg TNT. Default 500 kg. --- @return #number Relative fuel in percent. +-- @param #number ExplosionPower (Optional) Explosion power in kg TNT. Default 100 kg. +-- @return #OPSGROUP self function OPSGROUP:SelfDestruction(Delay, ExplosionPower) if Delay and Delay>0 then self:ScheduleOnce(Delay, OPSGROUP.SelfDestruction, self, 0, ExplosionPower) else - + -- Loop over all elements. for i,_element in pairs(self.elements) do local element=_element --#OPSGROUP.Element - + local unit=element.unit - + if unit and unit:IsAlive() then - unit:Explode(ExplosionPower) + unit:Explode(ExplosionPower or 100) end end end + return self +end + +--- Use SRS Simple-Text-To-Speech for transmissions. +-- @param #OPSGROUP self +-- @param #string PathToSRS Path to SRS directory. +-- @param #string Gender Gender: "male" or "female" (default). +-- @param #string Culture Culture, e.g. "en-GB" (default). +-- @param #string Voice Specific voice. Overrides `Gender` and `Culture`. +-- @param #number Port SRS port. Default 5002. +-- @return #OPSGROUP self +function OPSGROUP:SetSRS(PathToSRS, Gender, Culture, Voice, Port) + self.useSRS=true + self.msrs=MSRS:New(PathToSRS, self.frequency, self.modulation) + self.msrs:SetGender(Gender) + self.msrs:SetCulture(Culture) + self.msrs:SetVoice(Voice) + self.msrs:SetPort(Port) + self.msrs:SetCoalition(self:GetCoalition()) + return self +end + +--- Send a radio transmission via SRS Text-To-Speech. +-- @param #OPSGROUP self +-- @param #string Text Text of transmission. +-- @param #number Delay Delay in seconds before the transmission is started. +-- @return #OPSGROUP self +function OPSGROUP:RadioTransmission(Text, Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP.RadioTransmission, self, Text, 0) + else + + if self.useSRS and self.msrs then + + local freq, modu, radioon=self:GetRadio() + + self.msrs:SetFrequencies(freq) + self.msrs:SetModulations(modu) + + -- Debug info. + self:T(self.lid..string.format("Radio transmission on %.3f MHz %s: %s", freq, UTILS.GetModulationName(modu), Text)) + + self.msrs:PlayText(Text) + end + + end + + return self +end + +--- Set that this carrier is an all aspect loader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierLoaderAllAspect(Length, Width) + self.carrierLoader.type="front" + self.carrierLoader.length=Length or 50 + self.carrierLoader.width=Width or 20 + return self +end + +--- Set that this carrier is a front loader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierLoaderFront(Length, Width) + self.carrierLoader.type="front" + self.carrierLoader.length=Length or 50 + self.carrierLoader.width=Width or 20 + return self +end + +--- Set that this carrier is a back loader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierLoaderBack(Length, Width) + self.carrierLoader.type="back" + self.carrierLoader.length=Length or 50 + self.carrierLoader.width=Width or 20 + return self +end + +--- Set that this carrier is a starboard (right side) loader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierLoaderStarboard(Length, Width) + self.carrierLoader.type="right" + self.carrierLoader.length=Length or 50 + self.carrierLoader.width=Width or 20 + return self +end + +--- Set that this carrier is a port (left side) loader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierLoaderPort(Length, Width) + self.carrierLoader.type="left" + self.carrierLoader.length=Length or 50 + self.carrierLoader.width=Width or 20 + return self +end + + +--- Set that this carrier is an all aspect unloader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierUnloaderAllAspect(Length, Width) + self.carrierUnloader.type="front" + self.carrierUnloader.length=Length or 50 + self.carrierUnloader.width=Width or 20 + return self +end + +--- Set that this carrier is a front unloader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierUnloaderFront(Length, Width) + self.carrierUnloader.type="front" + self.carrierUnloader.length=Length or 50 + self.carrierUnloader.width=Width or 20 + return self +end + +--- Set that this carrier is a back unloader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierUnloaderBack(Length, Width) + self.carrierUnloader.type="back" + self.carrierUnloader.length=Length or 50 + self.carrierUnloader.width=Width or 20 + return self +end + +--- Set that this carrier is a starboard (right side) unloader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierUnloaderStarboard(Length, Width) + self.carrierUnloader.type="right" + self.carrierUnloader.length=Length or 50 + self.carrierUnloader.width=Width or 20 + return self +end + +--- Set that this carrier is a port (left side) unloader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierUnloaderPort(Length, Width) + self.carrierUnloader.type="left" + self.carrierUnloader.length=Length or 50 + self.carrierUnloader.width=Width or 20 + return self +end + +--- Check if group is currently inside a zone. +-- @param #OPSGROUP self +-- @param Core.Zone#ZONE Zone The zone. +-- @return #boolean If true, group is in this zone +function OPSGROUP:IsInZone(Zone) + local vec2=self:GetVec2() + local is=false + if vec2 then + is=Zone:IsVec2InZone(vec2) + else + self:T3(self.lid.."WARNING: Cannot get vec2 at IsInZone()!") + end + return is +end + +--- Get 2D distance to a coordinate. +-- @param #OPSGROUP self +-- @param Core.Point#COORDINATE Coordinate. Can also be a DCS#Vec2 or DCS#Vec3. +-- @return #number Distance in meters. +function OPSGROUP:Get2DDistance(Coordinate) + + local a=self:GetVec2() + local b={} + if Coordinate.z then + b.x=Coordinate.x + b.y=Coordinate.z + else + b.x=Coordinate.x + b.y=Coordinate.y + end + + local dist=UTILS.VecDist2D(a, b) + + return dist +end + +--- Check if this is a FLIGHTGROUP. +-- @param #OPSGROUP self +-- @return #boolean If true, this is an airplane or helo group. +function OPSGROUP:IsFlightgroup() + return self.isFlightgroup +end + +--- Check if this is a ARMYGROUP. +-- @param #OPSGROUP self +-- @return #boolean If true, this is a ground group. +function OPSGROUP:IsArmygroup() + return self.isArmygroup +end + +--- Check if this is a NAVYGROUP. +-- @param #OPSGROUP self +-- @return #boolean If true, this is a ship group. +function OPSGROUP:IsNavygroup() + return self.isNavygroup end @@ -1182,7 +2017,7 @@ end function OPSGROUP:IsExist() local DCSGroup=self:GetDCSGroup() - + if DCSGroup then local exists=DCSGroup:isExist() return exists @@ -1196,6 +2031,12 @@ end -- @return #boolean If true, the group exists or false if the group does not exist. If nil, the DCS group could not be found. function OPSGROUP:IsActive() + if self.group then + local active=self.group:IsActive() + return active + end + + return nil end --- Check if group is alive. @@ -1218,32 +2059,42 @@ function OPSGROUP:IsLateActivated() return self.isLateActivated end ---- Check if group is in state in utero. +--- Check if group is in state in utero. Note that dead groups are also in utero but will return `false` here. -- @param #OPSGROUP self -- @return #boolean If true, group is not spawned yet. function OPSGROUP:IsInUtero() - return self:Is("InUtero") + local is=self:Is("InUtero") and not self:IsDead() + return is end --- Check if group is in state spawned. -- @param #OPSGROUP self -- @return #boolean If true, group is spawned. function OPSGROUP:IsSpawned() - return self:Is("Spawned") + local is=self:Is("Spawned") + return is end ---- Check if group is dead. +--- Check if group is dead. Could be destroyed or despawned. FSM state of dead group is `InUtero` though. -- @param #OPSGROUP self -- @return #boolean If true, all units/elements of the group are dead. function OPSGROUP:IsDead() - return self:Is("Dead") + return self.isDead +end + +--- Check if group was destroyed. +-- @param #OPSGROUP self +-- @return #boolean If true, all units/elements of the group were destroyed. +function OPSGROUP:IsDestroyed() + return self.isDestroyed end --- Check if FSM is stopped. -- @param #OPSGROUP self -- @return #boolean If true, FSM state is stopped. function OPSGROUP:IsStopped() - return self:Is("Stopped") + local is=self:Is("Stopped") + return is end --- Check if this group is currently "uncontrolled" and needs to be "started" to begin its route. @@ -1260,7 +2111,7 @@ function OPSGROUP:HasPassedFinalWaypoint() return self.passedfinalwp end ---- Check if the group is currently rearming. +--- Check if the group is currently rearming or on its way to the rearming place. -- @param #OPSGROUP self -- @return #boolean If true, group is rearming. function OPSGROUP:IsRearming() @@ -1268,6 +2119,42 @@ function OPSGROUP:IsRearming() return rearming end +--- Check if the group is completely out of ammo. +-- @param #OPSGROUP self +-- @return #boolean If `true`, group is out-of-ammo. +function OPSGROUP:IsOutOfAmmo() + return self.outofAmmo +end + +--- Check if the group is out of bombs. +-- @param #OPSGROUP self +-- @return #boolean If `true`, group is out of bombs. +function OPSGROUP:IsOutOfBombs() + return self.outofBombs +end + +--- Check if the group is out of guns. +-- @param #OPSGROUP self +-- @return #boolean If `true`, group is out of guns. +function OPSGROUP:IsOutOfGuns() + return self.outofGuns +end + +--- Check if the group is out of missiles. +-- @param #OPSGROUP self +-- @return #boolean If `true`, group is out of missiles. +function OPSGROUP:IsOutOfMissiles() + return self.outofMissiles +end + +--- Check if the group is out of torpedos. +-- @param #OPSGROUP self +-- @return #boolean If `true`, group is out of torpedos. +function OPSGROUP:IsOutOfTorpedos() + return self.outofTorpedos +end + + --- Check if the group has currently switched a LASER on. -- @param #OPSGROUP self -- @return #boolean If true, LASER of the group is on. @@ -1275,18 +2162,244 @@ function OPSGROUP:IsLasing() return self.spot.On end ---- Check if the group is currently retreating. +--- Check if the group is currently retreating or retreated. -- @param #OPSGROUP self --- @return #boolean If true, group is retreating. +-- @return #boolean If true, group is retreating or retreated. function OPSGROUP:IsRetreating() - return self:is("Retreating") + local is=self:is("Retreating") or self:is("Retreated") + return is +end + +--- Check if the group is retreated (has reached its retreat zone). +-- @param #OPSGROUP self +-- @return #boolean If true, group is retreated. +function OPSGROUP:IsRetreated() + local is=self:is("Retreated") + return is +end + + +--- Check if the group is currently returning to a zone. +-- @param #OPSGROUP self +-- @return #boolean If true, group is returning. +function OPSGROUP:IsReturning() + local is=self:is("Returning") + return is end --- Check if the group is engaging another unit or group. -- @param #OPSGROUP self -- @return #boolean If true, group is engaging. function OPSGROUP:IsEngaging() - return self:is("Engaging") + local is=self:is("Engaging") + return is +end + +--- Check if group is currently waiting. +-- @param #OPSGROUP self +-- @return #boolean If true, group is currently waiting. +function OPSGROUP:IsWaiting() + if self.Twaiting then + return true + end + return false +end + +--- Check if the group is not a carrier yet. +-- @param #OPSGROUP self +-- @return #boolean If true, group is not a carrier. +function OPSGROUP:IsNotCarrier() + return self.carrierStatus==OPSGROUP.CarrierStatus.NOTCARRIER +end + +--- Check if the group is a carrier. +-- @param #OPSGROUP self +-- @return #boolean If true, group is a carrier. +function OPSGROUP:IsCarrier() + return not self:IsNotCarrier() +end + +--- Check if the group is picking up cargo. +-- @param #OPSGROUP self +-- @return #boolean If true, group is picking up. +function OPSGROUP:IsPickingup() + return self.carrierStatus==OPSGROUP.CarrierStatus.PICKUP +end + +--- Check if the group is loading cargo. +-- @param #OPSGROUP self +-- @return #boolean If true, group is loading. +function OPSGROUP:IsLoading() + return self.carrierStatus==OPSGROUP.CarrierStatus.LOADING +end + +--- Check if the group is transporting cargo. +-- @param #OPSGROUP self +-- @return #boolean If true, group is transporting. +function OPSGROUP:IsTransporting() + return self.carrierStatus==OPSGROUP.CarrierStatus.TRANSPORTING +end + +--- Check if the group is unloading cargo. +-- @param #OPSGROUP self +-- @return #boolean If true, group is unloading. +function OPSGROUP:IsUnloading() + return self.carrierStatus==OPSGROUP.CarrierStatus.UNLOADING +end + + +--- Check if the group is assigned as cargo. +-- @param #OPSGROUP self +-- @param #boolean CheckTransport If `true` or `nil`, also check if cargo is associated with a transport assignment. If not, we consider it not cargo. +-- @return #boolean If true, group is cargo. +function OPSGROUP:IsCargo(CheckTransport) + return not self:IsNotCargo(CheckTransport) +end + +--- Check if the group is **not** cargo. +-- @param #OPSGROUP self +-- @param #boolean CheckTransport If `true` or `nil`, also check if cargo is associated with a transport assignment. If not, we consider it not cargo. +-- @return #boolean If true, group is *not* cargo. +function OPSGROUP:IsNotCargo(CheckTransport) + local notcargo=self.cargoStatus==OPSGROUP.CargoStatus.NOTCARGO + + if notcargo then + -- Not cargo. + return true + else + -- Is cargo (e.g. loaded or boarding) + + if CheckTransport then + -- Check if transport UID was set. + if self.cargoTransportUID==nil then + return true + else + -- Some transport UID was assigned. + return false + end + else + -- Is cargo. + return false + end + + end + + + return notcargo +end + +--- Check if awaiting a transport. +-- @param #OPSGROUP self +-- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. +-- @return #OPSGROUP self +function OPSGROUP:_AddMyLift(Transport) + self.mylifts=self.mylifts or {} + self.mylifts[Transport.uid]=true + return self +end + +--- Remove my lift. +-- @param #OPSGROUP self +-- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. +-- @return #OPSGROUP self +function OPSGROUP:_DelMyLift(Transport) + if self.mylifts then + self.mylifts[Transport.uid]=nil + end + return self +end + + +--- Check if awaiting a transport lift. +-- @param #OPSGROUP self +-- @param Ops.OpsTransport#OPSTRANSPORT Transport (Optional) The transport. +-- @return #boolean If true, group is awaiting transport lift.. +function OPSGROUP:IsAwaitingLift(Transport) + + if self.mylifts then + + for uid,iswaiting in pairs(self.mylifts) do + if Transport==nil or Transport.uid==uid then + if iswaiting==true then + return true + end + end + end + + end + + return false +end + +--- Check if the group is currently boarding a carrier. +-- @param #OPSGROUP self +-- @param #string CarrierGroupName (Optional) Additionally check if group is boarding this particular carrier group. +-- @return #boolean If true, group is boarding. +function OPSGROUP:IsBoarding(CarrierGroupName) + if CarrierGroupName then + local carrierGroup=self:_GetMyCarrierGroup() + if carrierGroup and carrierGroup.groupname~=CarrierGroupName then + return false + end + end + return self.cargoStatus==OPSGROUP.CargoStatus.BOARDING +end + +--- Check if the group is currently loaded into a carrier. +-- @param #OPSGROUP self +-- @param #string CarrierGroupName (Optional) Additionally check if group is loaded into a particular carrier group(s). +-- @return #boolean If true, group is loaded. +function OPSGROUP:IsLoaded(CarrierGroupName) + if CarrierGroupName then + if type(CarrierGroupName)~="table" then + CarrierGroupName={CarrierGroupName} + end + for _,CarrierName in pairs(CarrierGroupName) do + local carrierGroup=self:_GetMyCarrierGroup() + if carrierGroup and carrierGroup.groupname==CarrierName then + return true + end + end + return false + end + return self.cargoStatus==OPSGROUP.CargoStatus.LOADED +end + +--- Check if the group is currently busy doing something. +-- +-- * Boarding +-- * Rearming +-- * Returning +-- * Pickingup, Loading, Transporting, Unloading +-- * Engageing +-- +-- @param #OPSGROUP self +-- @return #boolean If `true`, group is busy. +function OPSGROUP:IsBusy() + + if self:IsBoarding() then + return true + end + + if self:IsRearming() then + return true + end + + if self:IsReturning() then + return true + end + + -- Busy as carrier? + if self:IsPickingup() or self:IsLoading() or self:IsTransporting() or self:IsUnloading() then + return true + end + + if self:IsEngaging() then + return true + end + + + return false end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1308,21 +2421,21 @@ function OPSGROUP:MarkWaypoints(Duration) for i,_waypoint in pairs(self.waypoints or {}) do local waypoint=_waypoint --#OPSGROUP.Waypoint - + local text=string.format("Waypoint ID=%d of %s", waypoint.uid, self.groupname) text=text..string.format("\nSpeed=%.1f kts, Alt=%d ft (%s)", UTILS.MpsToKnots(waypoint.speed), UTILS.MetersToFeet(waypoint.alt), "BARO") - + if waypoint.marker then if waypoint.marker.text~=text then waypoint.marker.text=text end - + else waypoint.marker=MARKER:New(waypoint.coordinate, text):ToCoalition(self:GetCoalition()) end end - - + + if Duration then self:RemoveWaypointMarkers(Duration) end @@ -1342,14 +2455,14 @@ function OPSGROUP:RemoveWaypointMarkers(Delay) for i,_waypoint in pairs(self.waypoints or {}) do local waypoint=_waypoint --#OPSGROUP.Waypoint - + if waypoint.marker then waypoint.marker:Remove() end end - + end - + return self end @@ -1427,27 +2540,32 @@ end -- @return #number Next waypoint index. function OPSGROUP:GetWaypointIndexNext(cyclic, i) + -- If not specified, we take the adinititum value. if cyclic==nil then cyclic=self.adinfinitum end - + + -- Total number of waypoints. local N=#self.waypoints - + + -- Default is currentwp. i=i or self.currentwp + -- If no next waypoint exists, because the final waypoint was reached, we return the last waypoint. local n=math.min(i+1, N) - + + -- If last waypoint was reached, the first waypoint is the next in line. if cyclic and i==N then n=1 end - + return n end --- Get current waypoint index. This is the index of the last passed waypoint. -- @param #OPSGROUP self -- @return #number Current waypoint index. -function OPSGROUP:GetWaypointIndexCurrent() +function OPSGROUP:GetWaypointIndexCurrent() return self.currentwp or 1 end @@ -1462,8 +2580,8 @@ function OPSGROUP:GetWaypointIndexAfterID(uid) return index+1 else return #self.waypoints+1 - end - + end + end --- Get waypoint. @@ -1488,7 +2606,7 @@ end function OPSGROUP:GetWaypointNext(cyclic) local n=self:GetWaypointIndexNext(cyclic) - + return self.waypoints[n] end @@ -1505,7 +2623,7 @@ end -- @return Core.Point#COORDINATE Coordinate of the next waypoint. function OPSGROUP:GetNextWaypointCoordinate(cyclic) - -- Get next waypoint + -- Get next waypoint local waypoint=self:GetWaypointNext(cyclic) return waypoint.coordinate @@ -1530,7 +2648,7 @@ end function OPSGROUP:GetWaypointSpeed(indx) local waypoint=self:GetWaypoint(indx) - + if waypoint then return UTILS.MpsToKnots(waypoint.speed) end @@ -1553,7 +2671,7 @@ end function OPSGROUP:GetWaypointID(indx) local waypoint=self:GetWaypoint(indx) - + if waypoint then return waypoint.uid end @@ -1569,8 +2687,8 @@ end function OPSGROUP:GetSpeedToWaypoint(indx) local speed=self:GetWaypointSpeed(indx) - - if speed<=0.1 then + + if speed<=0.01 then speed=self:GetSpeedCruise() end @@ -1583,22 +2701,22 @@ end -- @return #number Distance in meters. function OPSGROUP:GetDistanceToWaypoint(indx) local dist=0 - + if #self.waypoints>0 then indx=indx or self:GetWaypointIndexNext() - + local wp=self:GetWaypoint(indx) - + if wp then - + local coord=self:GetCoordinate() - + dist=coord:Get2DDistance(wp.coordinate) end - + end - + return dist end @@ -1607,21 +2725,21 @@ end -- @param #number indx Waypoint index. Default is the next waypoint. -- @return #number Time in seconds. If velocity is 0 function OPSGROUP:GetTimeToWaypoint(indx) - + local s=self:GetDistanceToWaypoint(indx) - + local v=self:GetVelocity() - + local t=s/v - + if t==math.inf then return 365*24*60*60 elseif t==math.nan then return 0 - else + else return t end - + end --- Returns the currently expected speed. @@ -1629,12 +2747,12 @@ end -- @return #number Expected speed in m/s. function OPSGROUP:GetExpectedSpeed() - if self:IsHolding() then + if self:IsHolding() or self:Is("Rearming") or self:IsWaiting() or self:IsRetreated() then return 0 else return self.speedWp or 0 end - + end --- Remove a waypoint with a ceratin UID. @@ -1644,9 +2762,9 @@ end function OPSGROUP:RemoveWaypointByID(uid) local index=self:GetWaypointIndex(uid) - + if index then - self:RemoveWaypoint(index) + self:RemoveWaypoint(index) end return self @@ -1659,69 +2777,230 @@ end function OPSGROUP:RemoveWaypoint(wpindex) if self.waypoints then - + + -- The waypoitn to be removed. + local wp=self:GetWaypoint(wpindex) + + -- Is this a temporary waypoint. + local istemp=wp.temp or wp.detour or wp.astar or wp.missionUID + -- Number of waypoints before delete. local N=#self.waypoints - + + -- Always keep at least one waypoint. + if N==1 then + self:E(self.lid..string.format("ERROR: Cannot remove waypoint with index=%d! It is the only waypoint and a group needs at least ONE waypoint", wpindex)) + return self + end + + -- Check that wpindex is not larger than the number of waypoints in the table. + if wpindex>N then + self:E(self.lid..string.format("ERROR: Cannot remove waypoint with index=%d as there are only N=%d waypoints!", wpindex, N)) + return self + end + -- Remove waypoint marker. - local wp=self:GetWaypoint(wpindex) if wp and wp.marker then wp.marker:Remove() end -- Remove waypoint. table.remove(self.waypoints, wpindex) - + -- Number of waypoints after delete. local n=#self.waypoints - + -- Debug info. - self:T(self.lid..string.format("Removing waypoint index %d, current wp index %d. N %d-->%d", wpindex, self.currentwp, N, n)) - + self:T(self.lid..string.format("Removing waypoint UID=%d [temp=%s]: index=%d [currentwp=%d]. N %d-->%d", wp.uid, tostring(istemp), wpindex, self.currentwp, N, n)) + -- Waypoint was not reached yet. if wpindex > self.currentwp then - + --- -- Removed a FUTURE waypoint --- - - -- TODO: patrol adinfinitum. - - if self.currentwp>=n then - self.passedfinalwp=true + + -- TODO: patrol adinfinitum. Not sure this is handled correctly. If patrol adinfinitum and we have now only one WP left, we should at least go back. + + -- Could be that the waypoint we are currently moving to was the LAST waypoint. Then we now passed the final waypoint. + if self.currentwp>=n and not (self.adinfinitum or istemp) then + self:_PassedFinalWaypoint(true, "Removed FUTURE waypoint we are currently moving to and that was the LAST waypoint") end + -- Check if group is done. self:_CheckGroupDone(1) else - + --- -- Removed a waypoint ALREADY PASSED --- - + -- If an already passed waypoint was deleted, we do not need to update the route. - + -- If current wp = 1 it stays 1. Otherwise decrease current wp. - + if self.currentwp==1 then - + if self.adinfinitum then self.currentwp=#self.waypoints else self.currentwp=1 end - + else self.currentwp=self.currentwp-1 end - + end - + end return self end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- DCS Events +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Event function handling the birth of a unit. +-- @param #OPSGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function OPSGROUP:OnEventBirth(EventData) + + -- Check that this is the right group. + if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then + local unit=EventData.IniUnit + local group=EventData.IniGroup + local unitname=EventData.IniUnitName + + + -- Set homebase if not already set. + if self.isFlightgroup then + + if EventData.Place then + self.homebase=self.homebase or EventData.Place + self.currbase=EventData.Place + else + self.currbase=nil + end + + if self.homebase and not self.destbase then + self.destbase=self.homebase + end + + self:T(self.lid..string.format("EVENT: Element %s born at airbase %s ==> spawned", unitname, self.currbase and self.currbase:GetName() or "unknown")) + else + self:T3(self.lid..string.format("EVENT: Element %s born ==> spawned", unitname)) + end + + -- Get element. + local element=self:GetElementByName(unitname) + + if element and element.status~=OPSGROUP.ElementStatus.SPAWNED then + + -- Debug info. + self:T(self.lid..string.format("EVENT: Element %s born ==> spawned", unitname)) + + -- Set element to spawned state. + self:ElementSpawned(element) + + end + + end + +end + +--- Event function handling the crash of a unit. +-- @param #OPSGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function OPSGROUP:OnEventDead(EventData) + + -- Check that this is the right group. + if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then + self:T2(self.lid..string.format("EVENT: Unit %s dead!", EventData.IniUnitName)) + + local unit=EventData.IniUnit + local group=EventData.IniGroup + local unitname=EventData.IniUnitName + + -- Get element. + local element=self:GetElementByName(unitname) + + if element and element.status~=OPSGROUP.ElementStatus.DEAD then + self:T(self.lid..string.format("EVENT: Element %s dead ==> destroyed", element.name)) + self:ElementDestroyed(element) + end + + end + +end + +--- Event function handling when a unit is removed from the game. +-- @param #OPSGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function OPSGROUP:OnEventRemoveUnit(EventData) + + -- Check that this is the right group. + if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then + self:T2(self.lid..string.format("EVENT: Unit %s removed!", EventData.IniUnitName)) + + local unit=EventData.IniUnit + local group=EventData.IniGroup + local unitname=EventData.IniUnitName + + -- Get element. + local element=self:GetElementByName(unitname) + + if element and element.status~=OPSGROUP.ElementStatus.DEAD then + self:T(self.lid..string.format("EVENT: Element %s removed ==> dead", element.name)) + self:ElementDead(element) + end + + end + +end + +--- Event function handling the event that a unit achieved a kill. +-- @param #OPSGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function OPSGROUP:OnEventKill(EventData) + + -- Check that this is the right group. + if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then + + -- Target name + local targetname=tostring(EventData.TgtUnitName) + + -- Debug info. + self:T2(self.lid..string.format("EVENT: Unit %s killed object %s!", tostring(EventData.IniUnitName), targetname)) + + -- Check if this was a UNIT or STATIC object. + local target=UNIT:FindByName(targetname) + if not target then + target=STATIC:FindByName(targetname, false) + end + + -- Only count UNITS and STATICs (not SCENERY) + if target then + + -- Debug info. + self:T(self.lid..string.format("EVENT: Unit %s killed unit/static %s!", tostring(EventData.IniUnitName), targetname)) + + -- Kill counter. + self.Nkills=self.Nkills+1 + + -- Check if on a mission. + local mission=self:GetMissionCurrent() + if mission then + mission.Nkills=mission.Nkills+1 -- Increase mission kill counter. + end + + end + + end + +end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Task Functions @@ -1734,16 +3013,7 @@ end function OPSGROUP:SetTask(DCSTask) if self:IsAlive() then - - if self.taskcurrent>0 then - - -- TODO: Why the hell did I do this? It breaks scheduled tasks. I comment it out for now to see where it fails. - --local task=self:GetTaskCurrent() - --self:RemoveTask(task) - --self.taskcurrent=0 - - end - + -- Inject enroute tasks. if self.taskenroute and #self.taskenroute>0 then if tostring(DCSTask.id)=="ComboTask" then @@ -1753,14 +3023,14 @@ function OPSGROUP:SetTask(DCSTask) else local tasks=UTILS.DeepCopy(self.taskenroute) table.insert(tasks, DCSTask) - + DCSTask=self.group.TaskCombo(self, tasks) end end - + -- Set task. - self.group:SetTask(DCSTask) - + self.controller:setTask(DCSTask) + -- Debug info. local text=string.format("SETTING Task %s", tostring(DCSTask.id)) if tostring(DCSTask.id)=="ComboTask" then @@ -1768,9 +3038,9 @@ function OPSGROUP:SetTask(DCSTask) text=text..string.format("\n[%d] %s", i, tostring(task.id)) end end - self:T(self.lid..text) + self:T(self.lid..text) end - + return self end @@ -1781,10 +3051,24 @@ end function OPSGROUP:PushTask(DCSTask) if self:IsAlive() then - + + -- Inject enroute tasks. + if self.taskenroute and #self.taskenroute>0 then + if tostring(DCSTask.id)=="ComboTask" then + for _,task in pairs(self.taskenroute) do + table.insert(DCSTask.params.tasks, 1, task) + end + else + local tasks=UTILS.DeepCopy(self.taskenroute) + table.insert(tasks, DCSTask) + + DCSTask=self.group.TaskCombo(self, tasks) + end + end + -- Push task. - self.group:PushTask(DCSTask) - + self.controller:pushTask(DCSTask) + -- Debug info. local text=string.format("PUSHING Task %s", tostring(DCSTask.id)) if tostring(DCSTask.id)=="ComboTask" then @@ -1792,24 +3076,38 @@ function OPSGROUP:PushTask(DCSTask) text=text..string.format("\n[%d] %s", i, tostring(task.id)) end end - self:T(self.lid..text) + self:T(self.lid..text) end - + return self end +--- Returns true if the DCS controller currently has a task. +-- @param #OPSGROUP self +-- @return #boolean True or false if the controller has a task. Nil if no controller. +function OPSGROUP:HasTaskController() + local hastask=nil + if self.controller then + hastask=self.controller:hasTask() + end + self:T3(self.lid..string.format("Controller hasTask=%s", tostring(hastask))) + return hastask +end + --- Clear DCS tasks. -- @param #OPSGROUP self --- @param #table DCSTask DCS task structure. -- @return #OPSGROUP self function OPSGROUP:ClearTasks() - if self:IsAlive() then - self.group:ClearTasks() + local hastask=self:HasTaskController() + if self:IsAlive() and self.controller and self:HasTaskController() then self:I(self.lid..string.format("CLEARING Tasks")) + self.controller:resetTask() end return self end + + --- Add a *scheduled* task. -- @param #OPSGROUP self -- @param #table task DCS task table structure. @@ -1824,7 +3122,7 @@ function OPSGROUP:AddTask(task, clock, description, prio, duration) -- Add to table. table.insert(self.taskqueue, newtask) - + -- Info. self:T(self.lid..string.format("Adding SCHEDULED task %s starting at %s", newtask.description, UTILS.SecondsToClock(newtask.time, true))) self:T3({newtask=newtask}) @@ -1859,14 +3157,14 @@ function OPSGROUP:NewTaskScheduled(task, clock, description, prio, duration) local newtask={} --#OPSGROUP.Task newtask.status=OPSGROUP.TaskStatus.SCHEDULED newtask.dcstask=task - newtask.description=description or task.id + newtask.description=description or task.id newtask.prio=prio or 50 newtask.time=time newtask.id=self.taskcounter newtask.duration=duration newtask.waypoint=-1 newtask.type=OPSGROUP.TaskType.SCHEDULED - newtask.stopflag=USERFLAG:New(string.format("%s StopTaskFlag %d", self.groupname, newtask.id)) + newtask.stopflag=USERFLAG:New(string.format("%s StopTaskFlag %d", self.groupname, newtask.id)) newtask.stopflag:Set(0) return newtask @@ -1876,15 +3174,15 @@ end -- @param #OPSGROUP self -- @param #table task DCS task table structure. -- @param #OPSGROUP.Waypoint Waypoint where the task is executed. Default is the at *next* waypoint. --- @param #string description Brief text describing the task, e.g. "Attack SAM". +-- @param #string description Brief text describing the task, e.g. "Attack SAM". -- @param #number prio Priority of the task. Number between 1 and 100. Default is 50. -- @param #number duration Duration before task is cancelled in seconds counted after task started. Default never. -- @return #OPSGROUP.Task The task structure. function OPSGROUP:AddTaskWaypoint(task, Waypoint, description, prio, duration) - + -- Waypoint of task. Waypoint=Waypoint or self:GetWaypointNext() - + if Waypoint then -- Increase counter. @@ -1901,22 +3199,22 @@ function OPSGROUP:AddTaskWaypoint(task, Waypoint, description, prio, duration) newtask.time=0 newtask.waypoint=Waypoint.uid newtask.type=OPSGROUP.TaskType.WAYPOINT - newtask.stopflag=USERFLAG:New(string.format("%s StopTaskFlag %d", self.groupname, newtask.id)) + newtask.stopflag=USERFLAG:New(string.format("%s StopTaskFlag %d", self.groupname, newtask.id)) newtask.stopflag:Set(0) - + -- Add to table. table.insert(self.taskqueue, newtask) - + -- Info. self:T(self.lid..string.format("Adding WAYPOINT task %s at WP ID=%d", newtask.description, newtask.waypoint)) self:T3({newtask=newtask}) - + -- Update route. - self:__UpdateRoute(-1) - - return newtask + --self:__UpdateRoute(-1) + + return newtask end - + return nil end @@ -1928,7 +3226,7 @@ function OPSGROUP:AddTaskEnroute(task) if not self.taskenroute then self.taskenroute={} end - + -- Check not to add the same task twice! local gotit=false for _,Task in pairs(self.taskenroute) do @@ -1937,11 +3235,11 @@ function OPSGROUP:AddTaskEnroute(task) break end end - + if not gotit then table.insert(self.taskenroute, task) end - + end --- Get the unfinished waypoint tasks @@ -1950,7 +3248,7 @@ end -- @return #table Table of tasks. Table could also be empty {}. function OPSGROUP:GetTasksWaypoint(id) - -- Tasks table. + -- Tasks table. local tasks={} -- Sort queue. @@ -1963,7 +3261,7 @@ function OPSGROUP:GetTasksWaypoint(id) table.insert(tasks, task) end end - + return tasks end @@ -1973,7 +3271,7 @@ end -- @return #number Number of waypoint tasks. function OPSGROUP:CountTasksWaypoint(id) - -- Tasks table. + -- Tasks table. local n=0 -- Look for first task that SCHEDULED. @@ -1983,7 +3281,7 @@ function OPSGROUP:CountTasksWaypoint(id) n=n+1 end end - + return n end @@ -1997,7 +3295,7 @@ function OPSGROUP:_SortTaskQueue() local taskB=b --#OPSGROUP.Task return (taskA.prio0) then + + if Mission:IsReadyToPush() then + + --- + -- READY to push yet + --- + + -- Group is currently waiting. + if self:IsWaiting() then + + -- Not waiting any more. + self.Twaiting=nil + self.dTwait=nil + + -- For a flight group, we must cancel the wait/orbit task. + if self:IsFlightgroup() then + + -- Set hold flag to 1. This is a condition in the wait/orbit task. + self.flaghold:Set(1) + + -- Reexecute task in 1 sec to allow to flag to take effect. + --self:__TaskExecute(-1, Task) + + -- Deny transition for now. + --return false + end + end + + else + + --- + -- NOT READY to push yet + --- + + if self:IsWaiting() then + -- Group is already waiting + else + -- Wait indefinately. + local alt=Mission.missionAltitude and UTILS.MetersToFeet(Mission.missionAltitude) or nil + self:Wait(nil, alt) + end + + -- Time to for the next try. Best guess is when push time is reached or 20 sec when push conditions are not true yet. + local dt=Mission.Tpush and Mission.Tpush-timer.getAbsTime() or 20 + + -- Debug info. + self:T(self.lid..string.format("Mission %s task execute suspended for %d seconds", Mission.name, dt)) + + -- Reexecute task. + self:__TaskExecute(-dt, Task) + + -- Deny transition. + return false + end + + end + + return true +end + --- On after "TaskExecute" event. -- @param #OPSGROUP self -- @param #string From From state. @@ -2134,110 +3504,233 @@ function OPSGROUP:onafterTaskExecute(From, Event, To, Task) -- Debug message. local text=string.format("Task %s ID=%d execute", tostring(Task.description), Task.id) self:T(self.lid..text) - + -- Cancel current task if there is any. if self.taskcurrent>0 then self:TaskCancel() end - + -- Set current task. self.taskcurrent=Task.id - + -- Set time stamp. Task.timestamp=timer.getAbsTime() -- Task status executing. Task.status=OPSGROUP.TaskStatus.EXECUTING - + + -- Insert into task queue. Not sure any more, why I added this. But probably if a task is just executed without having been put into the queue. + if self:GetTaskCurrent()==nil then + table.insert(self.taskqueue, Task) + end + + -- Get mission of this task (if any). + local Mission=self:GetMissionByTaskID(self.taskcurrent) + if Task.dcstask.id=="Formation" then -- Set of group(s) to follow Mother. local followSet=SET_GROUP:New():AddGroup(self.group) - + local param=Task.dcstask.params - + local followUnit=UNIT:FindByName(param.unitname) - + -- Define AI Formation object. Task.formation=AI_FORMATION:New(followUnit, followSet, "Formation", "Follow X at given parameters.") - + -- Formation parameters. Task.formation:FormationCenterWing(-param.offsetX, 50, math.abs(param.altitude), 50, param.offsetZ, 50) - + -- Set follow time interval. Task.formation:SetFollowTimeInterval(param.dtFollow) - + -- Formation mode. Task.formation:SetFlightModeFormation(self.group) - + -- Start formation FSM. - Task.formation:Start() + Task.formation:Start() elseif Task.dcstask.id=="PatrolZone" then - + --- -- Task patrol zone. --- - + -- Parameters. - local zone=Task.dcstask.params.zone --Core.Zone#ZONE - local Coordinate=zone:GetRandomCoordinate() - local Speed=UTILS.KmphToKnots(Task.dcstask.params.speed or self.speedCruise) + local zone=Task.dcstask.params.zone --Core.Zone#ZONE + + local surfacetypes=nil + if self:IsArmygroup() then + surfacetypes={land.SurfaceType.LAND, land.SurfaceType.ROAD} + elseif self:IsNavygroup() then + surfacetypes={land.SurfaceType.WATER, land.SurfaceType.SHALLOW_WATER} + end + + -- Random coordinate in zone. + local Coordinate=zone:GetRandomCoordinate(nil, nil, surfacetypes) + + --Coordinate:MarkToAll("Random Patrol Zone Coordinate") + + -- Speed and altitude. + local Speed=UTILS.KmphToKnots(Task.dcstask.params.speed or self.speedCruise) local Altitude=Task.dcstask.params.altitude and UTILS.MetersToFeet(Task.dcstask.params.altitude) or nil - -- New waypoint. + local currUID=self:GetWaypointCurrent().uid + + -- New waypoint. + local wp=nil --#OPSGROUP.Waypoint if self.isFlightgroup then - FLIGHTGROUP.AddWaypoint(self, Coordinate, Speed, AfterWaypointWithID, Altitude) - elseif self.isNavygroup then - ARMYGROUP.AddWaypoint(self, Coordinate, Speed, AfterWaypointWithID, Formation) + wp=FLIGHTGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Altitude) elseif self.isArmygroup then - NAVYGROUP.AddWaypoint(self, Coordinate, Speed, AfterWaypointWithID, Altitude) + wp=ARMYGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Formation) + elseif self.isNavygroup then + wp=NAVYGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Altitude) + end + + -- Set mission UID. + wp.missionUID=Mission and Mission.auftragsnummer or nil + + elseif Task.dcstask.id=="ReconMission" then + + --- + -- Task recon. + --- + + -- Target + local target=Task.dcstask.params.target --Ops.Target#TARGET + self.lastindex=1 + + -- Target object and zone. + local object=target.targets[1] --Ops.Target#TARGET.Object + local zone=object.Object --Core.Zone#ZONE + + -- Random coordinate in zone. + local Coordinate=zone:GetRandomCoordinate() + + -- Speed and altitude. + local Speed=UTILS.KmphToKnots(Task.dcstask.params.speed or self.speedCruise) + local Altitude=Task.dcstask.params.altitude and UTILS.MetersToFeet(Task.dcstask.params.altitude) or nil + + --Coordinate:MarkToAll("Recon Waypoint Execute") + + local currUID=self:GetWaypointCurrent().uid + + -- New waypoint. + local wp=nil --#OPSGROUP.Waypoint + if self.isFlightgroup then + wp=FLIGHTGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Altitude) + elseif self.isArmygroup then + wp=ARMYGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Formation) + elseif self.isNavygroup then + wp=NAVYGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Altitude) + end + + -- Set mission UID. + wp.missionUID=Mission and Mission.auftragsnummer or nil + + elseif Task.dcstask.id==AUFTRAG.SpecialTask.AMMOSUPPLY or Task.dcstask.id==AUFTRAG.SpecialTask.FUELSUPPLY then + + --- + -- Task "Ammo Supply" or "Fuel Supply" mission. + --- + + -- Just stay put and wait until something happens. + + elseif Task.dcstask.id==AUFTRAG.SpecialTask.ALERT5 then + + --- + -- Task "Alert 5" mission. + --- + + -- Just stay put on the airfield and wait until something happens. + + elseif Task.dcstask.id==AUFTRAG.SpecialTask.ONGUARD then + + --- + -- Task "On Guard" Mission. + --- + + -- Just stay put. + --TODO: Change ALARM STATE + + if self:IsArmygroup() or self:IsNavygroup() then + -- Especially NAVYGROUP needs a full stop as patrol ad infinitum + self:FullStop() + else + -- FLIGHTGROUP not implemented (intended!) for this AUFTRAG type. end else -- If task is scheduled (not waypoint) set task. - if Task.type==OPSGROUP.TaskType.SCHEDULED then - - local DCStasks={} - if Task.dcstask.id=='ComboTask' then - -- Loop over all combo tasks. - for TaskID, Task in ipairs(Task.dcstask.params.tasks) do - table.insert(DCStasks, Task) - end + if Task.type==OPSGROUP.TaskType.SCHEDULED or Task.ismission then + + local DCSTask=nil --UTILS.DeepCopy(Task.dcstask) + + -- BARRAGE is special! + if Task.dcstask.id==AUFTRAG.SpecialTask.BARRAGE then + env.info("FF Barrage") + local vec2=self:GetVec2() + local param=Task.dcstask.params + local heading=param.heading or math.random(1, 360) + local Altitude=param.altitude or 500 + local Alpha=param.angle or math.random(45, 85) + local distance=Altitude/math.tan(math.rad(Alpha)) + local tvec2=UTILS.Vec2Translate(vec2, distance, heading) + self:T(self.lid..string.format("Barrage: Shots=%s, Altitude=%d m, Angle=%d°, heading=%03d°, distance=%d m", tostring(param.shots), Altitude, Alpha, heading, distance)) + DCSTask=CONTROLLABLE.TaskFireAtPoint(nil, tvec2, param.radius, param.shots, param.weaponType, Altitude) else - table.insert(DCStasks, Task.dcstask) + DCSTask=Task.dcstask end - + + local DCStasks={} + if DCSTask.id=='ComboTask' then + -- Loop over all combo tasks. + for TaskID, Task in ipairs(DCSTask.params.tasks) do + table.insert(DCStasks, Task) + end + else + table.insert(DCStasks, DCSTask) + end + -- Combo task. local TaskCombo=self.group:TaskCombo(DCStasks) - - -- Stop condition! + + -- Stop condition! local TaskCondition=self.group:TaskCondition(nil, Task.stopflag:GetName(), 1, nil, Task.duration) - - -- Controlled task. + + -- Controlled task. local TaskControlled=self.group:TaskControlled(TaskCombo, TaskCondition) - + -- Task done. local TaskDone=self.group:TaskFunction("OPSGROUP._TaskDone", self, Task) - + -- Final task. local TaskFinal=self.group:TaskCombo({TaskControlled, TaskDone}) - + -- Set task for group. - self:SetTask(TaskFinal) - + -- NOTE: I am pushing the task instead of setting it as it seems to keep the mission task alive. + -- There were issues that flights did not proceed to a later waypoint because the task did not finish until the fired missiles + -- impacted (took rather long). Then the flight flew to the nearest airbase and one lost completely the control over the group. + self:PushTask(TaskFinal) + --self:SetTask(TaskFinal) + + + elseif Task.type==OPSGROUP.TaskType.WAYPOINT then + -- Waypoint tasks are executed elsewhere! + else + self:E(self.lid.."ERROR: Unknown task type: ") end - + end - -- Get mission of this task (if any). - local Mission=self:GetMissionByTaskID(self.taskcurrent) + + -- Set AUFTRAG status. if Mission then - -- Set AUFTRAG status. self:MissionExecute(Mission) end - + end --- On after "TaskCancel" event. Cancels the current task or simply sets the status to DONE if the task is not the current one. @@ -2247,60 +3740,70 @@ end -- @param #string To To state. -- @param #OPSGROUP.Task Task The task to cancel. Default is the current task (if any). function OPSGROUP:onafterTaskCancel(From, Event, To, Task) - + -- Get current task. local currenttask=self:GetTaskCurrent() - + -- If no task, we take the current task. But this could also be *nil*! Task=Task or currenttask - + if Task then - + -- Check if the task is the current task? if currenttask and Task.id==currenttask.id then - + -- Current stop flag value. I noticed cases, where setting the flag to 1 would not cancel the task, e.g. when firing HARMS on a dead ship. local stopflag=Task.stopflag:Get() - + -- Debug info. local text=string.format("Current task %s ID=%d cancelled (flag %s=%d)", Task.description, Task.id, Task.stopflag:GetName(), stopflag) self:T(self.lid..text) - + -- Set stop flag. When the flag is true, the _TaskDone function is executed and calls :TaskDone() Task.stopflag:Set(1) - + local done=false if Task.dcstask.id=="Formation" then Task.formation:Stop() done=true elseif Task.dcstask.id=="PatrolZone" then done=true + elseif Task.dcstask.id=="ReconMission" then + done=true + elseif Task.dcstask.id==AUFTRAG.SpecialTask.AMMOSUPPLY then + done=true + elseif Task.dcstask.id==AUFTRAG.SpecialTask.FUELSUPPLY then + done=true + elseif Task.dcstask.id==AUFTRAG.SpecialTask.ALERT5 then + done=true + elseif Task.dcstask.id==AUFTRAG.SpecialTask.ONGUARD then + done=true elseif stopflag==1 or (not self:IsAlive()) or self:IsDead() or self:IsStopped() then -- Manual call TaskDone if setting flag to one was not successful. done=true end - + if done then self:TaskDone(Task) end - + else - + -- Debug info. self:T(self.lid..string.format("TaskCancel: Setting task %s ID=%d to DONE", Task.description, Task.id)) - - -- Call task done function. + + -- Call task done function. self:TaskDone(Task) end - + else - + local text=string.format("WARNING: No (current) task to cancel!") self:E(self.lid..text) - + end - + end --- On before "TaskDone" event. Deny transition if task status is PAUSED. @@ -2336,38 +3839,61 @@ function OPSGROUP:onafterTaskDone(From, Event, To, Task) if Task.id==self.taskcurrent then self.taskcurrent=0 end - + -- Task status done. Task.status=OPSGROUP.TaskStatus.DONE - + -- Restore old ROE. if Task.backupROE then self:SwitchROE(Task.backupROE) end - + -- Check if this task was the task of the current mission ==> Mission Done! local Mission=self:GetMissionByTaskID(Task.id) - + if Mission and Mission:IsNotOver() then - - local status=Mission:GetGroupStatus(self) - + + local status=Mission:GetGroupStatus(self) + if status~=AUFTRAG.GroupStatus.PAUSED then - self:T(self.lid.."Task Done ==> Mission Done!") - self:MissionDone(Mission) + local EgressUID=Mission:GetGroupEgressWaypointUID(self) + if EgressUID then + self:T(self.lid..string.format("Task Done but Egress waypoint defined ==> Will call Mission Done once group passed waypoint UID=%d!", EgressUID)) + else + self:T(self.lid.."Task Done ==> Mission Done!") + self:MissionDone(Mission) + end else - --Mission paused. Do nothing! + --Mission paused. Do nothing! Just set the current mission to nil so we can launch a new one. + if self.currentmission and self.currentmission==Mission.auftragsnummer then + self.currentmission=nil + end + self:T(self.lid.."Remove mission waypoints") + self:_RemoveMissionWaypoints(Mission, false) end + else - + if Task.description=="Engage_Target" then + self:T(self.lid.."Taske DONE Engage_Target ==> Cruise") self:Disengage() - end - - self:T(self.lid.."Task Done but NO mission found ==> _CheckGroupDone in 1 sec") - self:_CheckGroupDone(1) + end + + if Task.description==AUFTRAG.SpecialTask.ONGUARD then + self:T(self.lid.."Taske DONE OnGuard ==> Cruise") + self:Cruise() + end + + if Task.description=="Task_Land_At" then + self:T(self.lid.."Taske DONE Task_Land_At ==> Wait") + self:Wait(20, 100) + else + self:T(self.lid.."Task Done but NO mission found ==> _CheckGroupDone in 1 sec") + self:_CheckGroupDone(1) + end + end - + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2379,27 +3905,27 @@ end -- @param Ops.Auftrag#AUFTRAG Mission Mission for this group. -- @return #OPSGROUP self function OPSGROUP:AddMission(Mission) - + -- Add group to mission. Mission:AddOpsGroup(self) - + -- Set group status to SCHEDULED.. Mission:SetGroupStatus(self, AUFTRAG.GroupStatus.SCHEDULED) - + -- Set mission status to SCHEDULED. Mission:Scheduled() - + -- Add elements. Mission.Nelements=Mission.Nelements+#self.elements -- Add mission to queue. table.insert(self.missionqueue, Mission) - + -- Info text. - local text=string.format("Added %s mission %s starting at %s, stopping at %s", + local text=string.format("Added %s mission %s starting at %s, stopping at %s", tostring(Mission.type), tostring(Mission.name), UTILS.SecondsToClock(Mission.Tstart, true), Mission.Tstop and UTILS.SecondsToClock(Mission.Tstop, true) or "INF") self:T(self.lid..text) - + return self end @@ -2411,22 +3937,27 @@ function OPSGROUP:RemoveMission(Mission) for i,_mission in pairs(self.missionqueue) do local mission=_mission --Ops.Auftrag#AUFTRAG - + if mission.auftragsnummer==Mission.auftragsnummer then - + -- Remove mission waypoint task. local Task=Mission:GetGroupWaypointTask(self) - + if Task then self:RemoveTask(Task) end + -- Take care of a paused mission. + if self.missionpaused and self.missionpaused.auftragsnummer==Mission.auftragsnummer then + self.missionpaused=nil + end + -- Remove mission from queue. table.remove(self.missionqueue, i) - + return self end - + end return self @@ -2442,19 +3973,52 @@ function OPSGROUP:CountRemainingMissison() -- Loop over mission queue. for _,_mission in pairs(self.missionqueue) do local mission=_mission --Ops.Auftrag#AUFTRAG - + if mission and mission:IsNotOver() then - + -- Get group status. local status=mission:GetGroupStatus(self) - + if status~=AUFTRAG.GroupStatus.DONE and status~=AUFTRAG.GroupStatus.CANCELLED then N=N+1 end - + end end - + + return N +end + +--- Count remaining cargo transport assignments. +-- @param #OPSGROUP self +-- @return #number Number of unfinished transports in the queue. +function OPSGROUP:CountRemainingTransports() + + local N=0 + + -- Loop over mission queue. + for _,_transport in pairs(self.cargoqueue) do + local transport=_transport --Ops.OpsTransport#OPSTRANSPORT + + local mystatus=transport:GetCarrierTransportStatus(self) + local status=transport:GetState() + + -- Debug info. + self:T(self.lid..string.format("Transport my status=%s [%s]", mystatus, status)) + + -- Count not delivered (executing or scheduled) assignments. + if transport and mystatus==OPSTRANSPORT.Status.SCHEDULED and status~=OPSTRANSPORT.Status.DELIVERED and status~=OPSTRANSPORT.Status.CANCELLED then + N=N+1 + end + end + + -- In case we directly set the cargo transport (not in queue). + if N==0 and self.cargoTransport and + self.cargoTransport:GetState()~=OPSTRANSPORT.Status.DELIVERED and self.cargoTransport:GetCarrierTransportStatus(self)~=OPSTRANSPORT.Status.DELIVERED and + self.cargoTransport:GetState()~=OPSTRANSPORT.Status.CANCELLED and self.cargoTransport:GetCarrierTransportStatus(self)~=OPSTRANSPORT.Status.CANCELLED then + N=1 + end + return N end @@ -2463,6 +4027,11 @@ end -- @return Ops.Auftrag#AUFTRAG Next mission or *nil*. function OPSGROUP:_GetNextMission() + -- Check if group is acting as carrier or cargo at the moment. + if self:IsPickingup() or self:IsLoading() or self:IsTransporting() or self:IsUnloading() or self:IsLoaded() then + return nil + end + -- Number of missions. local Nmissions=#self.missionqueue @@ -2478,14 +4047,11 @@ function OPSGROUP:_GetNextMission() return (taskA.prio3.6 then + self:RouteToMission(Mission, 3) + + else + --- + -- IMMOBILE Group + --- + + env.info("FF Immobile GROUP") + + -- Add waypoint task. UpdateRoute is called inside. + local Clock=Mission.Tpush and UTILS.SecondsToClock(Mission.Tpush) or 5 + local Task=self:AddTask(Mission.DCStask, Clock, Mission.name, Mission.prio, Mission.duration) + Task.ismission=true + + -- Set waypoint task. + Mission:SetGroupWaypointTask(self, Task) + + -- Execute task. This calls mission execute. + self:__TaskExecute(3, Task) + end + end --- On after "MissionExecute" event. Mission execution began. @@ -2623,13 +4249,18 @@ function OPSGROUP:onafterMissionExecute(From, Event, To, Mission) local text=string.format("Executing %s Mission %s, target %s", Mission.type, tostring(Mission.name), Mission:GetTargetName()) self:T(self.lid..text) - + -- Set group mission status to EXECUTING. Mission:SetGroupStatus(self, AUFTRAG.GroupStatus.EXECUTING) - + -- Set mission status to EXECUTING. Mission:Executing() - + + -- Set auto engage detected targets. + if Mission.engagedetectedOn then + self:SetEngageDetectedOn(UTILS.MetersToNM(Mission.engagedetectedRmax), Mission.engagedetectedTypes, Mission.engagedetectedEngageZones, Mission.engagedetectedNoEngageZones) + end + end --- On after "PauseMission" event. @@ -2640,24 +4271,24 @@ end function OPSGROUP:onafterPauseMission(From, Event, To) local Mission=self:GetMissionCurrent() - + if Mission then -- Set group mission status to PAUSED. Mission:SetGroupStatus(self, AUFTRAG.GroupStatus.PAUSED) - + -- Get mission waypoint task. local Task=Mission:GetGroupWaypointTask(self) - + -- Debug message. self:T(self.lid..string.format("Pausing current mission %s. Task=%s", tostring(Mission.name), tostring(Task and Task.description or "WTF"))) - + -- Cancelling the mission is actually cancelling the current task. self:TaskCancel(Task) - + -- Set mission to pause so we can unpause it later. self.missionpaused=Mission - + end end @@ -2671,18 +4302,20 @@ function OPSGROUP:onafterUnpauseMission(From, Event, To) -- Debug info. self:T(self.lid..string.format("Unpausing mission")) - + if self.missionpaused then - + local mission=self:GetMissionByID(self.missionpaused.auftragsnummer) - - self:MissionStart(mission) - + + if mission then + self:MissionStart(mission) + end + self.missionpaused=nil else self:E(self.lid.."ERROR: No mission to unpause!") end - + end @@ -2700,9 +4333,15 @@ function OPSGROUP:onafterMissionCancel(From, Event, To, Mission) -- Current Mission --- + -- Alert 5 missoins dont have a task set, which could be cancelled. + if Mission.type==AUFTRAG.Type.ALERT5 or Mission.type==AUFTRAG.Type.ONGUARD then + self:MissionDone(Mission) + return + end + -- Get mission waypoint task. local Task=Mission:GetGroupWaypointTask(self) - + -- Debug info. self:T(self.lid..string.format("Cancel current mission %s. Task=%s", tostring(Mission.name), tostring(Task and Task.description or "WTF"))) @@ -2710,26 +4349,45 @@ function OPSGROUP:onafterMissionCancel(From, Event, To, Mission) -- Note that two things can happen. -- 1.) Group is still on the way to the waypoint (status should be STARTED). In this case there would not be a current task! -- 2.) Group already passed the mission waypoint (status should be EXECUTING). - + self:TaskCancel(Task) - + else - + --- -- NOT the current mission --- - + -- Set mission group status. Mission:SetGroupStatus(self, AUFTRAG.GroupStatus.CANCELLED) - + -- Remove mission from queue self:RemoveMission(Mission) - + -- Send group RTB or WAIT if nothing left to do. self:_CheckGroupDone(1) - + end - + +end + +--- On after "MissionDone" event. +-- @param #OPSGROUP self +-- @param Ops.Auftrag#AUFTRAG Mission +-- @param #boolean Silently Remove waypoints by `table.remove()` and do not update the route. +function OPSGROUP:_RemoveMissionWaypoints(Mission, Silently) + + for i=#self.waypoints,1,-1 do + local wp=self.waypoints[i] --#OPSGROUP.Waypoint + if wp.missionUID==Mission.auftragsnummer then + if Silently then + table.remove(self.waypoints, i) + else + self:RemoveWaypoint(i) + end + end + end + end --- On after "MissionDone" event. @@ -2737,45 +4395,51 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param Ops.Auftrag#AUFTRAG Mission +-- @param Ops.Auftrag#AUFTRAG Mission The mission that is done. function OPSGROUP:onafterMissionDone(From, Event, To, Mission) -- Debug info. local text=string.format("Mission %s DONE!", Mission.name) self:T(self.lid..text) - + -- Set group status. Mission:SetGroupStatus(self, AUFTRAG.GroupStatus.DONE) - + -- Set current mission to nil. if self.currentmission and Mission.auftragsnummer==self.currentmission then self.currentmission=nil end - - -- Remove mission waypoint. - local wpidx=Mission:GetGroupWaypointIndex(self) - if wpidx then - self:RemoveWaypointByID(wpidx) - end - + + -- Remove mission waypoints. + self:_RemoveMissionWaypoints(Mission) + -- Decrease patrol data. if Mission.patroldata then Mission.patroldata.noccupied=Mission.patroldata.noccupied-1 AIRWING.UpdatePatrolPointMarker(Mission.patroldata) end - + + -- Switch auto engage detected off. This IGNORES that engage detected had been activated for the group! + if Mission.engagedetectedOn then + self:SetEngageDetectedOff() + end + -- ROE to default. if Mission.optionROE then self:SwitchROE() end -- ROT to default - if Mission.optionROT then + if self:IsFlightgroup() and Mission.optionROT then self:SwitchROT() end -- Alarm state to default. if Mission.optionAlarm then self:SwitchAlarmstate() - end + end + -- Alarm state to default. + if Mission.optionEPLRS then + self:SwitchEPLRS() + end -- Formation to default. if Mission.optionFormation then self:SwitchFormation() @@ -2788,29 +4452,35 @@ function OPSGROUP:onafterMissionDone(From, Event, To, Mission) -- TACAN beacon. if Mission.tacan then - -- Switch to default. + -- Switch to default. self:_SwitchTACAN() - - -- Return Squadron TACAN channel. - local squadron=self.squadron --Ops.Squadron#SQUADRON - if squadron then - squadron:ReturnTacan(Mission.tacan.Channel) + + -- Return Cohort's TACAN channel. + local cohort=self.cohort --Ops.Cohort#COHORT + if cohort then + cohort:ReturnTacan(Mission.tacan.Channel) end - + -- Set asset TACAN to nil. local asset=Mission:GetAssetByName(self.groupname) if asset then asset.tacan=nil - end + end end - + -- ICLS beacon to default. if Mission.icls then - self:_SwitchICLS() + self:_SwitchICLS() end - + + -- We add a 10 sec delay for ARTY. Found that they need some time to readjust the barrel of their gun. Not sure if necessary for all. Needs some more testing! + local delay=1 + if Mission.type==AUFTRAG.Type.ARTY then + delay=60 + end + -- Check if group is done. - self:_CheckGroupDone(1) + self:_CheckGroupDone(delay) end @@ -2824,142 +4494,234 @@ function OPSGROUP:RouteToMission(mission, delay) -- Delayed call. self:ScheduleOnce(delay, OPSGROUP.RouteToMission, self, mission) else - - if self:IsDead() then + + -- Debug info. + self:T(self.lid..string.format("Route To Mission")) + + -- Catch dead or stopped groups. + if self:IsDead() or self:IsStopped() then + self:T(self.lid..string.format("Route To Mission: I am DEAD or STOPPED! Ooops...")) return end - + + -- OPSTRANSPORT: Just add the ops transport to the queue. + if mission.type==AUFTRAG.Type.OPSTRANSPORT then + self:T(self.lid..string.format("Route To Mission: I am OPSTRANSPORT! Add transport and return...")) + self:AddOpsTransport(mission.opstransport) + return + end + + -- ALERT5: Just set the mission to executing. + if mission.type==AUFTRAG.Type.ALERT5 then + self:T(self.lid..string.format("Route To Mission: I am ALERT5! Go right to MissionExecute()...")) + self:MissionExecute(mission) + return + end + -- ID of current waypoint. - local uid=self:GetWaypointCurrent().uid - - -- Get coordinate where the mission is executed. - local waypointcoord=mission:GetMissionWaypointCoord(self.group) - + local uid=self:GetWaypointCurrent().uid + + -- Ingress waypoint coordinate where the mission is executed. + local waypointcoord=nil --Core.Point#COORDINATE + + -- Random radius of 1000 meters. + local randomradius=mission.missionWaypointRadius or 1000 + + -- Surface types. + local surfacetypes=nil + if self:IsArmygroup() then + surfacetypes={land.SurfaceType.LAND, land.SurfaceType.ROAD} + elseif self:IsNavygroup() then + surfacetypes={land.SurfaceType.WATER, land.SurfaceType.SHALLOW_WATER} + end + + -- Get ingress waypoint. + if mission.type==AUFTRAG.Type.PATROLZONE or mission.type==AUFTRAG.Type.BARRAGE or mission.type==AUFTRAG.Type.AMMOSUPPLY or mission.type.FUELSUPPLY then + local zone=mission.engageTarget:GetObject() --Core.Zone#ZONE + waypointcoord=zone:GetRandomCoordinate(nil , nil, surfacetypes) + elseif mission.type==AUFTRAG.Type.ONGUARD then + waypointcoord=mission:GetMissionWaypointCoord(self.group, nil, surfacetypes) + else + waypointcoord=mission:GetMissionWaypointCoord(self.group, randomradius, surfacetypes) + end + -- Add enroute tasks. for _,task in pairs(mission.enrouteTasks) do self:AddTaskEnroute(task) end - + -- Speed to mission waypoint. local SpeedToMission=UTILS.KmphToKnots(self.speedCruise) - + -- Special for Troop transport. if mission.type==AUFTRAG.Type.TROOPTRANSPORT then - - -- Refresh DCS task with the known controllable. + + --- + -- TROOP TRANSPORT + --- + + -- Refresh DCS task with the known controllable. mission.DCStask=mission:GetDCSMissionTask(self.group) - + -- Add task to embark for the troops. for _,_group in pairs(mission.transportGroupSet.Set) do local group=_group --Wrapper.Group#GROUP - + if group and group:IsAlive() then local DCSTask=group:TaskEmbarkToTransport(mission.transportPickup, 500) group:SetTask(DCSTask, 5) end - + end - + elseif mission.type==AUFTRAG.Type.ARTY then - + + --- + -- ARTY + --- + -- Get weapon range. local weapondata=self:GetWeaponData(mission.engageWeaponType) - + if weapondata then - + -- Get target coordinate. local targetcoord=mission:GetTargetCoordinate() - + -- Heading to target. local heading=self:GetCoordinate():HeadingTo(targetcoord) - + -- Distance to target. local dist=self:GetCoordinate():Get2DDistance(targetcoord) - + -- Check if we are within range. if dist>weapondata.RangeMax then - + local d=(dist-weapondata.RangeMax)*1.1 - + -- New waypoint coord. waypointcoord=self:GetCoordinate():Translate(d, heading) - - self:T(self.lid..string.format("Out of max range = %.1f km for weapon %d", weapondata.RangeMax/1000, mission.engageWeaponType)) + + -- Debug info. + self:T(self.lid..string.format("Out of max range = %.1f km for weapon %s", weapondata.RangeMax/1000, tostring(mission.engageWeaponType))) elseif dist0 then + self:T(self.lid..string.format("WARNING: Got current task ==> WAIT event is suspended for 30 sec!")) + Tsuspend=-30 + allowed=false + end + + -- Check for a current transport assignment. + if self.cargoTransport then + self:T(self.lid..string.format("WARNING: Got current TRANSPORT assignment ==> WAIT event is suspended for 30 sec!")) + Tsuspend=-30 + allowed=false + end + + -- Call wait again. + if Tsuspend and not allowed then + self:__Wait(Tsuspend, Duration) + end + + return allowed +end + +--- On after "Wait" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number Duration Duration in seconds how long the group will be waiting. Default `nil` (for ever). +function OPSGROUP:onafterWait(From, Event, To, Duration) + + -- Order Group to hold. + self:FullStop() + + -- Set time stamp. + self.Twaiting=timer.getAbsTime() + + -- Max waiting + self.dTwait=Duration + +end + + --- On after "PassingWaypoint" event. -- @param #OPSGROUP self -- @param #string From From state. @@ -3031,57 +4846,206 @@ function OPSGROUP:onafterPassingWaypoint(From, Event, To, Waypoint) -- Get the current task. local task=self:GetTaskCurrent() - + + -- Get the corresponding mission. + local mission=nil --Ops.Auftrag#AUFTRAG + if task then + mission=self:GetMissionByTaskID(task.id) + end + if task and task.dcstask.id=="PatrolZone" then - - -- Remove old waypoint. + + --- + -- SPECIAL TASK: Patrol Zone + --- + + -- Remove old waypoint. self:RemoveWaypointByID(Waypoint.uid) - local zone=task.dcstask.params.zone --Core.Zone#ZONE - local Coordinate=zone:GetRandomCoordinate() - local Speed=UTILS.KmphToKnots(task.dcstask.params.speed or self.speedCruise) - local Altitude=task.dcstask.params.altitude and UTILS.MetersToFeet(task.dcstask.params.altitude) or nil - - if self.isFlightgroup then - FLIGHTGROUP.AddWaypoint(self, Coordinate, Speed, AfterWaypointWithID, Altitude) - elseif self.isNavygroup then - ARMYGROUP.AddWaypoint(self, Coordinate, Speed, AfterWaypointWithID, Formation) - elseif self.isArmygroup then - NAVYGROUP.AddWaypoint(self, Coordinate, Speed, AfterWaypointWithID, Altitude) + -- Zone object. + local zone=task.dcstask.params.zone --Core.Zone#ZONE + + -- Surface types. + local surfacetypes=nil + if self:IsArmygroup() then + surfacetypes={land.SurfaceType.LAND, land.SurfaceType.ROAD} + elseif self:IsNavygroup() then + surfacetypes={land.SurfaceType.WATER, land.SurfaceType.SHALLOW_WATER} + end + + -- Random coordinate in zone. + local Coordinate=zone:GetRandomCoordinate(nil, nil, surfacetypes) + + -- Speed and altitude. + local Speed=UTILS.KmphToKnots(task.dcstask.params.speed or self.speedCruise) + local Altitude=UTILS.MetersToFeet(task.dcstask.params.altitude or self.altitudeCruise) + + local currUID=self:GetWaypointCurrent().uid + + local wp=nil --#OPSGROUP.Waypoint + if self.isFlightgroup then + wp=FLIGHTGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Altitude) + elseif self.isArmygroup then + wp=ARMYGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Formation) + elseif self.isNavygroup then + wp=NAVYGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Altitude) + end + wp.missionUID=mission and mission.auftragsnummer or nil + + elseif task and task.dcstask.id=="ReconMission" then + + --- + -- SPECIAL TASK: Recon Mission + --- + + -- TARGET. + local target=task.dcstask.params.target --Ops.Target#TARGET + + local n=self.lastindex+1 + + if n<=#target.targets then + + -- Zone object. + local object=target.targets[n] --Ops.Target#TARGET.Object + local zone=object.Object --Core.Zone#ZONE + + -- Random coordinate in zone. + local Coordinate=zone:GetRandomCoordinate() + + -- Speed and altitude. + local Speed=UTILS.KmphToKnots(task.dcstask.params.speed or self.speedCruise) + local Altitude=task.dcstask.params.altitude and UTILS.MetersToFeet(task.dcstask.params.altitude) or nil + + -- Debug. + --Coordinate:MarkToAll("Recon Waypoint n="..tostring(n)) + + local currUID=self:GetWaypointCurrent().uid + + local wp=nil --#OPSGROUP.Waypoint + if self.isFlightgroup then + wp=FLIGHTGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Altitude) + elseif self.isArmygroup then + wp=ARMYGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Formation) + elseif self.isNavygroup then + wp=NAVYGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Altitude) + end + wp.missionUID=mission and mission.auftragsnummer or nil + + -- Increase counter. + self.lastindex=self.lastindex+1 + + else + + -- Get waypoint index. + local wpindex=self:GetWaypointIndex(Waypoint.uid) + + -- Final waypoint reached? + if wpindex==nil or wpindex==#self.waypoints then + + -- Set switch to true. + if not self.adinfinitum or #self.waypoints<=1 then + self:_PassedFinalWaypoint(true, "Passing waypoint and NOT adinfinitum and #self.waypoints<=1") + end + + end + + -- Final zone reached ==> task done. + self:TaskDone(task) + end - else - + + --- + -- No special task active + --- + -- Apply tasks of this waypoint. local ntasks=self:_SetWaypointTasks(Waypoint) - + -- Get waypoint index. local wpindex=self:GetWaypointIndex(Waypoint.uid) - + -- Final waypoint reached? if wpindex==nil or wpindex==#self.waypoints then - - -- Set switch to true. - if not self.adinfinitum or #self.waypoints<=1 then - self.passedfinalwp=true + + -- Ad infinitum and not mission waypoint? + if self.adinfinitum then + --- + -- Ad Infinitum + --- + + if Waypoint.missionUID then + --- + -- Last waypoint was a mission waypoint ==> Do nothing (when mission is over, it should take care of this) + --- + else + + --- + -- Last waypoint reached. + --- + + if #self.waypoints<=1 then + -- Only one waypoint. Ad infinitum does not really make sense. However, another waypoint could be added later... + self:_PassedFinalWaypoint(true, "PassingWaypoint: adinfinitum but only ONE WAYPOINT left") + else + + -- Looks like the passing waypoint function is triggered over and over again if the group is near the final waypoint. + -- So the only good solution is to guide the group away from that waypoint and then update the route. + + -- Get first waypoint. + local wp1=self:GetWaypointByIndex(1) + + -- Get a waypoint + local Coordinate=Waypoint.coordinate:GetIntermediateCoordinate(wp1.coordinate, 0.1) + + -- Detour to the temp waypoint. When reached, the normal route is resumed. + self:Detour(Coordinate, self.speedCruise, nil, true) + + end + end + else + --- + -- NOT Ad Infinitum + --- + + -- Final waypoint reached. + self:_PassedFinalWaypoint(true, "PassingWaypoint: wpindex=#self.waypoints (or wpindex=nil)") end - + end - + + -- Passing mission waypoint? + local isEgress=false + if Waypoint.missionUID then + + -- Debug info. + self:T2(self.lid..string.format("Passing mission waypoint UID=%s", tostring(Waypoint.uid))) + + -- Get the mission. + local mission=self:GetMissionByID(Waypoint.missionUID) + + -- Check if this was an Egress waypoint of the mission. If so, call Mission Done! This will call CheckGroupDone. + local EgressUID=mission and mission:GetGroupEgressWaypointUID(self) or nil + isEgress=EgressUID and Waypoint.uid==EgressUID + if isEgress and mission:GetGroupStatus(self)~=AUFTRAG.GroupStatus.DONE then + self:MissionDone(mission) + end + end + -- Check if all tasks/mission are done? -- Note, we delay it for a second to let the OnAfterPassingwaypoint function to be executed in case someone wants to add another waypoint there. - if ntasks==0 then - self:_CheckGroupDone(0.1) + if ntasks==0 and self:HasPassedFinalWaypoint() and not isEgress then + self:_CheckGroupDone(0.01) end - + -- Debug info. - local text=string.format("Group passed waypoint %s/%d ID=%d: final=%s detour=%s astar=%s", + local text=string.format("Group passed waypoint %s/%d ID=%d: final=%s detour=%s astar=%s", tostring(wpindex), #self.waypoints, Waypoint.uid, tostring(self.passedfinalwp), tostring(Waypoint.detour), tostring(Waypoint.astar)) self:T(self.lid..text) - + end - + end --- Set tasks at this waypoint @@ -3095,37 +5059,47 @@ function OPSGROUP:_SetWaypointTasks(Waypoint) -- Debug info. local text=string.format("WP uid=%d tasks:", Waypoint.uid) + local missiontask=nil --Ops.OpsGroup#OPSGROUP.Task if #tasks>0 then for i,_task in pairs(tasks) do local task=_task --#OPSGROUP.Task text=text..string.format("\n[%d] %s", i, task.description) + if task.ismission then + missiontask=task + end end else text=text.." None" end self:T(self.lid..text) - + + -- Check if there is mission task + if missiontask then + self:T(self.lid.."Executing mission task") + self:TaskExecute(missiontask) + return 1 + end + + -- TODO: maybe set waypoint enroute tasks? -- Tasks at this waypoints. local taskswp={} - - -- TODO: maybe set waypoint enroute tasks? - + for _,task in pairs(tasks) do - local Task=task --Ops.OpsGroup#OPSGROUP.Task - + local Task=task --Ops.OpsGroup#OPSGROUP.Task + -- Task execute. table.insert(taskswp, self.group:TaskFunction("OPSGROUP._TaskExecute", self, Task)) -- Stop condition if userflag is set to 1 or task duration over. local TaskCondition=self.group:TaskCondition(nil, Task.stopflag:GetName(), 1, nil, Task.duration) - - -- Controlled task. + + -- Controlled task. table.insert(taskswp, self.group:TaskControlled(Task.dcstask, TaskCondition)) - + -- Task done. table.insert(taskswp, self.group:TaskFunction("OPSGROUP._TaskDone", self, Task)) - + end -- Execute waypoint tasks. @@ -3136,36 +5110,45 @@ function OPSGROUP:_SetWaypointTasks(Waypoint) return #taskswp end +--- On after "PassedFinalWaypoint" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterPassedFinalWaypoint(From, Event, To) + self:T(self.lid..string.format("Group passed final waypoint")) + + -- Check if group is done? No tasks mission running. + --self:_CheckGroupDone() + +end + --- On after "GotoWaypoint" event. Group will got to the given waypoint and execute its route from there. -- @param #OPSGROUP self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -- @param #number UID The goto waypoint unique ID. -function OPSGROUP:onafterGotoWaypoint(From, Event, To, UID) +-- @param #number Speed (Optional) Speed to waypoint in knots. +function OPSGROUP:onafterGotoWaypoint(From, Event, To, UID, Speed) + + --env.info("FF goto waypoint uid="..tostring(UID)) local n=self:GetWaypointIndex(UID) - + if n then - - -- TODO: switch to re-enable waypoint tasks. - if false then - local tasks=self:GetTasksWaypoint(n) - - for _,_task in pairs(tasks) do - local task=_task --#OPSGROUP.Task - task.status=OPSGROUP.TaskStatus.SCHEDULED - end - - end - - local Speed=self:GetSpeedToWaypoint(n) - + + -- Speed to waypoint. + Speed=Speed or self:GetSpeedToWaypoint(n) + + -- Debug message + self:T(self.lid..string.format("Goto Waypoint UID=%d index=%d from %d at speed %.1f knots", UID, n, self.currentwp, Speed)) + -- Update the route. - self:__UpdateRoute(-1, n, Speed) - + self:__UpdateRoute(0.1, n, nil, Speed) + end - + end --- On after "DetectedUnit" event. @@ -3181,7 +5164,7 @@ function OPSGROUP:onafterDetectedUnit(From, Event, To, Unit) -- Debug. self:T2(self.lid..string.format("Detected unit %s", unitname)) - + if self.detectedunits:FindUnit(unitname) then -- Unit is already in the detected unit set ==> Trigger "DetectedUnitKnown" event. self:DetectedUnitKnown(Unit) @@ -3199,10 +5182,10 @@ end -- @param #string To To state. -- @param Wrapper.Unit#UNIT Unit The detected unit. function OPSGROUP:onafterDetectedUnitNew(From, Event, To, Unit) - + -- Debug info. self:T(self.lid..string.format("Detected New unit %s", Unit:GetName())) - + -- Add unit to detected unit set. self.detectedunits:AddUnit(Unit) end @@ -3220,7 +5203,7 @@ function OPSGROUP:onafterDetectedGroup(From, Event, To, Group) -- Debug info. self:T(self.lid..string.format("Detected group %s", groupname)) - + if self.detectedgroups:FindGroup(groupname) then -- Group is already in the detected set ==> Trigger "DetectedGroupKnown" event. self:DetectedGroupKnown(Group) @@ -3228,7 +5211,7 @@ function OPSGROUP:onafterDetectedGroup(From, Event, To, Group) -- Group is was not detected ==> Trigger "DetectedGroupNew" event. self:DetectedGroupNew(Group) end - + end --- On after "DetectedGroupNew" event. Add newly detected group to detected group set. @@ -3241,7 +5224,7 @@ function OPSGROUP:onafterDetectedGroupNew(From, Event, To, Group) -- Debug info. self:T(self.lid..string.format("Detected New group %s", Group:GetName())) - + -- Add unit to detected unit set. self.detectedgroups:AddGroup(Group) end @@ -3285,51 +5268,51 @@ function OPSGROUP:onbeforeLaserOn(From, Event, To, Target) if Target then - -- Target specified ==> set target. + -- Target specified ==> set target. self:SetLaserTarget(Target) - + else -- No target specified. self:E(self.lid.."ERROR: No target provided for LASER!") return false end - + -- Get the first element alive. local element=self:GetElementAlive() - + if element then - + -- Set element. - self.spot.element=element - + self.spot.element=element + -- Height offset. No offset for aircraft. We take the height for ground or naval. local offsetY=0 - if self.isGround or self.isNaval then + if self.isFlightgroup or self.isNavygroup then offsetY=element.height end - + -- Local offset of the LASER source. self.spot.offset={x=0, y=offsetY, z=0} - + -- Check LOS. if self.spot.CheckLOS then - + -- Check LOS. local los=self:HasLoS(self.spot.Coordinate, self.spot.element, self.spot.offset) - + --self:I({los=los, coord=self.spot.Coordinate, offset=self.spot.offset}) if los then self:LaserGotLOS() else -- Try to switch laser on again in 10 sec. - self:I(self.lid.."LASER got no LOS currently. Trying to switch the laser on again in 10 sec") + self:T(self.lid.."LASER got no LOS currently. Trying to switch the laser on again in 10 sec") self:__LaserOn(-10, Target) return false end - + end - + else self:E(self.lid.."ERROR: No element alive for lasing") return false @@ -3359,16 +5342,16 @@ function OPSGROUP:onafterLaserOn(From, Event, To, Target) if self.spot.IRon then self.spot.IR=Spot.createInfraRed(DCSunit, self.spot.offset, self.spot.vec3) end - + -- Laser is on. self.spot.On=true - + -- No paused in case it was. self.spot.Paused=false -- Debug message. self:T(self.lid.."Switching LASER on") - + end --- On before "LaserOff" event. Check if LASER is on. @@ -3394,7 +5377,7 @@ function OPSGROUP:onafterLaserOff(From, Event, To) if self.spot.On then self.spot.Laser:destroy() self.spot.IR:destroy() - + -- Set to nil. self.spot.Laser=nil self.spot.IR=nil @@ -3402,13 +5385,13 @@ function OPSGROUP:onafterLaserOff(From, Event, To) -- Stop update timer. self.spot.timer:Stop() - + -- No target unit. self.spot.TargetUnit=nil -- Laser is off. self.spot.On=false - + -- Not paused if it was. self.spot.Paused=false end @@ -3426,17 +5409,17 @@ function OPSGROUP:onafterLaserPause(From, Event, To) -- "Destroy" the laser beam. self.spot.Laser:destroy() self.spot.IR:destroy() - + -- Set to nil. self.spot.Laser=nil self.spot.IR=nil -- Laser is off. self.spot.On=false - + -- Laser is paused. self.spot.Paused=true - + end --- On before "LaserResume" event. @@ -3457,12 +5440,12 @@ function OPSGROUP:onafterLaserResume(From, Event, To) -- Debug info. self:T(self.lid.."Resuming LASER") - + -- Unset paused. self.spot.Paused=false -- Set target. - local target=nil + local target=nil if self.spot.TargetType==0 then target=self.spot.Coordinate elseif self.spot.TargetType==1 or self.spot.TargetType==2 then @@ -3476,7 +5459,7 @@ function OPSGROUP:onafterLaserResume(From, Event, To) -- Debug message. self:T(self.lid.."Switching LASER on again") - + self:LaserOn(target) end @@ -3495,16 +5478,16 @@ function OPSGROUP:onafterLaserCode(From, Event, To, Code) -- Debug message. self:T2(self.lid..string.format("Setting LASER Code to %d", self.spot.Code)) - + if self.spot.On then - + -- Debug info. self:T(self.lid..string.format("New LASER Code is %d", self.spot.Code)) - + -- Set LASER code. self.spot.Laser:setCode(self.spot.Code) end - + end --- On after "LaserLostLOS" event. @@ -3514,23 +5497,19 @@ end -- @param #string To To state. function OPSGROUP:onafterLaserLostLOS(From, Event, To) - --env.info("FF lost LOS") - -- No of sight. self.spot.LOS=false - + -- Lost line of sight. self.spot.lostLOS=true if self.spot.On then - - --env.info("FF lost LOS ==> pause laser") -- Switch laser off. self:LaserPause() - + end - + end --- On after "LaserGotLOS" event. @@ -3542,24 +5521,19 @@ 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 end - + end --- Set LASER target. @@ -3571,17 +5545,17 @@ function OPSGROUP:SetLaserTarget(Target) -- Check object type. if Target:IsInstanceOf("SCENERY") then - + -- Scenery as target. Treat it like a coordinate. Set offset to 1 meter above ground. self.spot.TargetType=0 self.spot.offsetTarget={x=0, y=1, z=0} - + elseif Target:IsInstanceOf("POSITIONABLE") then - + local target=Target --Wrapper.Positionable#POSITIONABLE - + if target:IsAlive() then - + if target:IsInstanceOf("GROUP") then -- We got a GROUP as target. self.spot.TargetGroup=target @@ -3596,39 +5570,37 @@ function OPSGROUP:SetLaserTarget(Target) self.spot.TargetType=2 end end - + -- Get object size. local size,x,y,z=self.spot.TargetUnit:GetObjectSize() - + if y then self.spot.offsetTarget={x=0, y=y*0.75, z=0} 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!") return end - + elseif Target:IsInstanceOf("COORDINATE") then - + -- Coordinate as target. self.spot.TargetType=0 self.spot.offsetTarget={x=0, y=0, z=0} - + else self:E(self.lid.."ERROR: LASER target should be a POSITIONABLE (GROUP, UNIT or STATIC) or a COORDINATE object!") return end - + -- Set vec3 and account for target offset. self.spot.vec3=UTILS.VecAdd(Target:GetVec3(), self.spot.offsetTarget) - + -- Set coordinate. - self.spot.Coordinate:UpdateFromVec3(self.spot.vec3) + self.spot.Coordinate:UpdateFromVec3(self.spot.vec3) end end @@ -3639,31 +5611,31 @@ function OPSGROUP:_UpdateLaser() -- Check if we have a POSITIONABLE to lase. if self.spot.TargetUnit then - + --- -- Lasing a possibly moving target --- - + if self.spot.TargetUnit:IsAlive() then - -- Get current target position. + -- Get current target position. local vec3=self.spot.TargetUnit:GetVec3() - + -- Add target offset. vec3=UTILS.VecAdd(vec3, self.spot.offsetTarget) - - -- Calculate distance + + -- Calculate distance local dist=UTILS.VecDist3D(vec3, self.spot.vec3) -- Store current position. self.spot.vec3=vec3 - + -- Update beam coordinate. self.spot.Coordinate:UpdateFromVec3(vec3) - + -- Update laser if target moved more than one meter. if dist>1 then - + -- If the laser is ON, set the new laser target point. if self.spot.On then self.spot.Laser:setPoint(vec3) @@ -3671,16 +5643,16 @@ function OPSGROUP:_UpdateLaser() self.spot.IR:setPoint(vec3) end end - + end - + else - + if self.spot.TargetGroup and self.spot.TargetGroup:IsAlive() then - + -- Get first alive unit in the group. local unit=self.spot.TargetGroup:GetHighestThreat() - + if unit then self:T(self.lid..string.format("Switching to target unit %s in the group", unit:GetName())) self.spot.TargetUnit=unit @@ -3690,47 +5662,74 @@ function OPSGROUP:_UpdateLaser() -- Switch laser off. self:T(self.lid.."Target is not alive any more ==> switching LASER off") self:LaserOff() - return + return end - + else - + -- Switch laser off. self:T(self.lid.."Target is not alive any more ==> switching LASER off") self:LaserOff() return end - - end + + end end - + -- Check LOS. if self.spot.CheckLOS then - + -- 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 los then + -- Got LOS if self.spot.lostLOS then --self:I({los=self.spot.LOS, coord=self.spot.Coordinate, offset=self.spot.offset}) self:LaserGotLOS() end - - else - -- No LOS currently + + else + -- No LOS currently if not self.spot.lostLOS then self:LaserLostLOS() - end - + end + end - + end - + end +--- On before "ElementSpawned" event. Check that element is not in status spawned already. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP.Element Element The flight group element. +function OPSGROUP:onbeforeElementSpawned(From, Event, To, Element) + + if Element and Element.status==OPSGROUP.ElementStatus.SPAWNED then + self:T2(self.lid..string.format("Element %s is already spawned ==> Transition denied!", Element.name)) + return false + end + + return true +end + +--- On after "ElementInUtero" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP.Element Element The flight group element. +function OPSGROUP:onafterElementInUtero(From, Event, To, Element) + self:T(self.lid..string.format("Element in utero %s", Element.name)) + + -- Set element status. + self:_UpdateStatus(Element, OPSGROUP.ElementStatus.INUTERO) + +end --- On after "ElementDestroyed" event. -- @param #OPSGROUP self @@ -3740,7 +5739,7 @@ end -- @param #OPSGROUP.Element Element The flight group element. function OPSGROUP:onafterElementDestroyed(From, Event, To, Element) self:T(self.lid..string.format("Element destroyed %s", Element.name)) - + -- Cancel all missions. for _,_mission in pairs(self.missionqueue) do local mission=_mission --Ops.Auftrag#AUFTRAG @@ -3748,13 +5747,13 @@ function OPSGROUP:onafterElementDestroyed(From, Event, To, Element) mission:ElementDestroyed(self, Element) end - + -- Increase counter. self.Ndestroyed=self.Ndestroyed+1 - -- Set element status. - self:_UpdateStatus(Element, OPSGROUP.ElementStatus.DEAD) - + -- Element is dead. + self:ElementDead(Element) + end --- On after "ElementDead" event. @@ -3764,23 +5763,25 @@ end -- @param #string To To state. -- @param #OPSGROUP.Element Element The flight group element. function OPSGROUP:onafterElementDead(From, Event, To, Element) + + -- Debug info. self:T(self.lid..string.format("Element dead %s at t=%.3f", Element.name, timer.getTime())) - + -- Set element status. self:_UpdateStatus(Element, OPSGROUP.ElementStatus.DEAD) - + -- Check if element was lasing and if so, switch to another unit alive to lase. if self.spot.On and self.spot.element.name==Element.name then - + -- Switch laser off. self:LaserOff() - - -- If there is another element alive, switch laser on again. + + -- If there is another element alive, switch laser on again. if self:GetNelements()>0 then - + -- New target if any. local target=nil - + if self.spot.TargetType==0 then -- Coordinate target=self.spot.Coordinate @@ -3788,21 +5789,250 @@ function OPSGROUP:onafterElementDead(From, Event, To, Element) -- Static or unit if self.spot.TargetUnit and self.spot.TargetUnit:IsAlive() then target=self.spot.TargetUnit - end + end elseif self.spot.TargetType==3 then -- Group if self.spot.TargetGroup and self.spot.TargetGroup:IsAlive() then target=self.spot.TargetGroup - end + end end - + -- Switch laser on again. if target then self:__LaserOn(-1, target) end end end - + + + -- Clear cargo bay of element. + for i=#Element.cargoBay,1,-1 do + local cargo=Element.cargoBay[i] --#OPSGROUP.MyCargo + + -- Remove from cargo bay. + self:_DelCargobay(cargo.group) + + if cargo.group and not (cargo.group:IsDead() or cargo.group:IsStopped()) then + + -- Remove my carrier + cargo.group:_RemoveMyCarrier() + + if cargo.reserved then + + -- This group was not loaded yet ==> Not cargo any more. + cargo.group:_NewCargoStatus(OPSGROUP.CargoStatus.NOTCARGO) + + else + + -- Carrier dead ==> cargo dead. + for _,cargoelement in pairs(cargo.group.elements) do + + -- Debug info. + self:T2(self.lid.."Cargo element dead "..cargoelement.name) + + -- Trigger dead event. + cargo.group:ElementDead(cargoelement) + + end + end + + end + end + +end + +--- On after "Respawn" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #table Template The template used to respawn the group. Default is the inital template of the group. +function OPSGROUP:onafterRespawn(From, Event, To, Template) + + -- Debug info. + self:I(self.lid.."Respawning group!") + + -- Copy template. + local template=UTILS.DeepCopy(Template or self.template) + + -- Late activation off. + template.lateActivation=false + + self:_Respawn(0, template) + +end + +--- Respawn the group. +-- @param #OPSGROUP self +-- @param #number Delay Delay in seconds before respawn happens. Default 0. +-- @param DCS#Template Template (optional) The template of the Group retrieved with GROUP:GetTemplate(). If the template is not provided, the template will be retrieved of the group itself. +-- @param #boolean Reset Reset waypoints and reinit group if `true`. +-- @return #OPSGROUP self +function OPSGROUP:_Respawn(Delay, Template, Reset) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP._Respawn, self, 0, Template, Reset) + else + + -- Debug message. + self:T2(self.lid.."FF _Respawn") + + -- Given template or get old. + Template=Template or self:_GetTemplate(true) + + if self:IsAlive() then + + --- + -- Group is ALIVE + --- + + --[[ + + -- Get units. + local units=self.group:GetUnits() + + -- Loop over template units. + for UnitID, Unit in pairs(Template.units) do + + for _,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + + if unit:GetName()==Unit.name then + local vec3=unit:GetVec3() + local heading=unit:GetHeading() + Unit.x=vec3.x + Unit.y=vec3.z + Unit.alt=vec3.y + Unit.heading=math.rad(heading) + Unit.psi=-Unit.heading + end + end + + end + + ]] + + local units=Template.units + + for i=#units,1,-1 do + local unit=units[i] + local element=self:GetElementByName(unit.name) + if element and element.status~=OPSGROUP.ElementStatus.DEAD then + unit.parking=element.parking and element.parking.TerminalID or unit.parking + unit.parking_id=nil + local vec3=element.unit:GetVec3() + local heading=element.unit:GetHeading() + unit.x=vec3.x + unit.y=vec3.z + unit.alt=vec3.y + unit.heading=math.rad(heading) + unit.psi=-unit.heading + else + table.remove(units, i) + end + end + + + -- Despawn old group. Dont trigger any remove unit event since this is a respawn. + self:Despawn(0, true) + + else + + --- + -- Group is DESPAWNED + --- + + -- Ensure elements in utero. + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + self:ElementInUtero(element) + end + + end + + -- Debug output. + self:T({Template=Template}) + + -- Spawn new group. + self.group=_DATABASE:Spawn(Template) + + -- Set DCS group and controller. + self.dcsgroup=self:GetDCSGroup() + self.controller=self.dcsgroup:getController() + + -- Set activation and controlled state. + self.isLateActivated=Template.lateActivation + self.isUncontrolled=Template.uncontrolled + + -- Not dead or destroyed any more. + self.isDead=false + self.isDestroyed=false + + + self.groupinitialized=false + self.Ndestroyed=0 + self.wpcounter=1 + self.currentwp=1 + + -- Init waypoints. + self:_InitWaypoints() + + -- Init Group. + self:_InitGroup(Template) + + -- Reset events. + --self:ResetEvents() + + end + + return self +end + +--- On after "InUtero" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterInUtero(From, Event, To) + self:T(self.lid..string.format("Group inutero at t=%.3f", timer.getTime())) + --TODO: set element status to inutero +end + +--- On after "Damaged" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterDamaged(From, Event, To) + self:T(self.lid..string.format("Group damaged at t=%.3f", timer.getTime())) + + --[[ + local lifemin=nil + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + if element.status~=OPSGROUP.ElementStatus.DEAD and element.status~=OPSGROUP.ElementStatus.INUTERO then + local life, life0=self:GetLifePoints(element) + if lifemin==nil or life Asset group is gone. + self.cohort:DelGroup(self.groupname) + end + else + -- Not all assets were destroyed (despawn) ==> Add asset back to legion? + end + -- Stop in a sec. - self:__Stop(-5) + --self:__Stop(-5) +end + +--- On before "Stop" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onbeforeStop(From, Event, To) + + -- We check if + if self:IsAlive() then + self:E(self.lid..string.format("WARNING: Group is still alive! Will not stop the FSM. Use :Despawn() instead")) + return false + end + + return true end --- On after "Stop" event. @@ -3849,14 +6133,46 @@ end -- @param #string Event Event. -- @param #string To To state. function OPSGROUP:onafterStop(From, Event, To) - + + -- Handle events: + self:UnHandleEvent(EVENTS.Birth) + self:UnHandleEvent(EVENTS.Dead) + self:UnHandleEvent(EVENTS.RemoveUnit) + + -- Handle events: + if self.isFlightgroup then + self:UnHandleEvent(EVENTS.EngineStartup) + self:UnHandleEvent(EVENTS.Takeoff) + self:UnHandleEvent(EVENTS.Land) + self:UnHandleEvent(EVENTS.EngineShutdown) + self:UnHandleEvent(EVENTS.PilotDead) + self:UnHandleEvent(EVENTS.Ejection) + self:UnHandleEvent(EVENTS.Crash) + self.currbase=nil + end + -- Stop check timers. self.timerCheckZone:Stop() self.timerQueueUpdate:Stop() + self.timerStatus:Stop() -- Stop FSM scheduler. self.CallScheduler:Clear() - + if self.Scheduler then + self.Scheduler:Clear() + end + + -- Flightcontrol. + if self.flightcontrol then + self.flightcontrol:_RemoveFlight(self) + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + if element.parking then + self.flightcontrol:SetParkingFree(element.parking) + end + end + end + if self:IsAlive() and not (self:IsDead() or self:IsStopped()) then local life, life0=self:GetLifePoints() local state=self:GetState() @@ -3864,8 +6180,2141 @@ function OPSGROUP:onafterStop(From, Event, To) self:E(self.lid..text) end + -- Remove flight from data base. + _DATABASE.FLIGHTGROUPS[self.groupname]=nil + -- Debug output. - self:I(self.lid.."STOPPED! Unhandled events, cleared scheduler and removed from database.") + self:I(self.lid.."STOPPED! Unhandled events, cleared scheduler and removed from _DATABASE") +end + +--- On after "OutOfAmmo" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterOutOfAmmo(From, Event, To) + self:T(self.lid..string.format("Group is out of ammo at t=%.3f", timer.getTime())) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Cargo Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check cargo transport assignments. +-- @param #OPSGROUP self +-- @return #OPSGROUP self +function OPSGROUP:_CheckCargoTransport() + + -- Abs. missin time in seconds. + local Time=timer.getAbsTime() + + -- Cargo bay debug info. + if self.verbose>=1 then + local text="" + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + for _,_cargo in pairs(element.cargoBay) do + local cargo=_cargo --#OPSGROUP.MyCargo + text=text..string.format("\n- %s in carrier %s, reserved=%s", tostring(cargo.group:GetName()), tostring(element.name), tostring(cargo.reserved)) + end + end + if text=="" then + text=" empty" + end + self:I(self.lid.."Cargo bay:"..text) + end + + -- Cargo queue debug info. + if self.verbose>=3 then + local text="" + for i,_transport in pairs(self.cargoqueue) do + local transport=_transport --Ops.OpsTransport#OPSTRANSPORT + local pickupzone=transport:GetPickupZone() + local deployzone=transport:GetDeployZone() + local pickupname=pickupzone and pickupzone:GetName() or "unknown" + local deployname=deployzone and deployzone:GetName() or "unknown" + text=text..string.format("\n[%d] UID=%d Status=%s: %s --> %s", i, transport.uid, transport:GetState(), pickupname, deployname) + for j,_cargo in pairs(transport:GetCargos()) do + local cargo=_cargo --#OPSGROUP.CargoGroup + local state=cargo.opsgroup:GetState() + local status=cargo.opsgroup.cargoStatus + local name=cargo.opsgroup.groupname + 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 + self:I(self.lid.."Cargo queue:"..text) + end + end + + if self.cargoTransport and self.cargoTransport:GetCarrierTransportStatus(self)==OPSTRANSPORT.Status.DELIVERED then + -- Remove transport from queue. + self:DelOpsTransport(self.cargoTransport) + -- No current transport any more. + self.cargoTransport=nil + self.cargoTZC=nil + end + + -- Check if there is anything in the queue. + if not self.cargoTransport and not self:IsOnMission() then + self.cargoTransport=self:_GetNextCargoTransport() + if self.cargoTransport and not self:IsActive() then + self:Activate() + end + end + + -- Now handle the transport. + if self.cargoTransport then + + if self:IsNotCarrier() then + + -- Unset time stamps. + self.Tpickingup=nil + self.Tloading=nil + self.Ttransporting=nil + self.Tunloading=nil + + -- Get transport zone combo (TZC). + self.cargoTZC=self.cargoTransport:_GetTransportZoneCombo(self) + + if self.cargoTZC then + + -- Found TZC + self:T(self.lid..string.format("Not carrier ==> pickup at %s [TZC UID=%d]", self.cargoTZC.PickupZone and self.cargoTZC.PickupZone:GetName() or "unknown", self.cargoTZC.uid)) + + -- Initiate the cargo transport process. + self:__Pickup(-1) + + else + self:T2(self.lid.."Not carrier ==> No TZC found") + end + + elseif self:IsPickingup() then + + -- Set time stamp. + self.Tpickingup=self.Tpickingup or Time + + -- Current pickup time. + local tpickingup=Time-self.Tpickingup + + -- Debug Info. + self:T(self.lid..string.format("Picking up at %s [TZC UID=%d] for %s sec...", self.cargoTZC.PickupZone and self.cargoTZC.PickupZone:GetName() or "unknown", self.cargoTZC.uid, tpickingup)) + + elseif self:IsLoading() then + + -- Set loading time stamp. + self.Tloading=self.Tloading or Time + + -- Current pickup time. + local tloading=Time-self.Tloading + + --TODO: Check max loading time. If exceeded ==> abort transport. + + -- Debug info. + self:T(self.lid..string.format("Loading at %s [TZC UID=%d] for %s sec...", self.cargoTZC.PickupZone and self.cargoTZC.PickupZone:GetName() or "unknown", self.cargoTZC.uid, tloading)) + + local boarding=false + local gotcargo=false + for _,_cargo in pairs(self.cargoTZC.Cargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + + -- Check if anyone is still boarding. + if cargo.opsgroup:IsBoarding(self.groupname) then + boarding=true + end + + -- Check if we have any cargo to transport. + if cargo.opsgroup:IsLoaded(self.groupname) then + gotcargo=true + end + + end + + -- Boarding finished ==> Transport cargo. + if gotcargo and self.cargoTransport:_CheckRequiredCargos(self.cargoTZC) and not boarding then + self:T(self.lid.."Boarding finished ==> Loaded") + self:LoadingDone() + else + -- No cargo and no one is boarding ==> check again if we can make anyone board. + self:Loading() + end + + -- No cargo and no one is boarding ==> check again if we can make anyone board. + if not gotcargo and not boarding then + --self:Loading() + end + + elseif self:IsTransporting() then + + -- Set time stamp. + self.Ttransporting=self.Ttransporting or Time + + -- Current pickup time. + local ttransporting=Time-self.Ttransporting + + -- Debug info. + self:T(self.lid.."Transporting (nothing to do)") + + elseif self:IsUnloading() then + + -- Set time stamp. + self.Tunloading=self.Tunloading or Time + + -- Current pickup time. + local tunloading=Time-self.Tunloading + + -- Debug info. + self:T(self.lid.."Unloading ==> Checking if all cargo was delivered") + + local delivered=true + for _,_cargo in pairs(self.cargoTZC.Cargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + + 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 + + end + + -- Unloading finished ==> pickup next batch or call it a day. + if delivered then + self:T(self.lid.."Unloading finished ==> UnloadingDone") + self:UnloadingDone() + else + self:Unloading() + end + + end + + -- Debug info. (At this point, we might not have a current cargo transport ==> hence the check) + if self.verbose>=2 and self.cargoTransport then + local pickupzone=self.cargoTransport:GetPickupZone(self.cargoTZC) + local deployzone=self.cargoTransport:GetDeployZone(self.cargoTZC) + local pickupname=pickupzone and pickupzone:GetName() or "unknown" + local deployname=deployzone and deployzone:GetName() or "unknown" + local text=string.format("Carrier [%s]: %s --> %s", self.carrierStatus, pickupname, deployname) + for _,_cargo in pairs(self.cargoTransport:GetCargos(self.cargoTZC)) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + local name=cargo.opsgroup:GetName() + local gstatus=cargo.opsgroup:GetState() + local cstatus=cargo.opsgroup.cargoStatus + local weight=cargo.opsgroup:GetWeightTotal() + 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 + + end + + return self +end + + +--- Check if a group is in the cargo bay. +-- @param #OPSGROUP self +-- @param #OPSGROUP OpsGroup Group to check. +-- @return #boolean If `true`, group is in the cargo bay. +function OPSGROUP:_IsInCargobay(OpsGroup) + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + for _,_cargo in pairs(element.cargoBay) do + local cargo=_cargo --#OPSGROUP.MyCargo + if cargo.group.groupname==OpsGroup.groupname then + return true + end + end + end + + return false +end + +--- Add OPSGROUP to cargo bay of a carrier. +-- @param #OPSGROUP self +-- @param #OPSGROUP CargoGroup Cargo group. +-- @param #OPSGROUP.Element CarrierElement The element of the carrier. +-- @param #boolean Reserved Only reserve the cargo bay space. +function OPSGROUP:_AddCargobay(CargoGroup, CarrierElement, Reserved) + + --TODO: Check group is not already in cargobay of this carrier or any other carrier. + + local cargo=self:_GetCargobay(CargoGroup) + + if cargo then + cargo.reserved=Reserved + else + + cargo={} --#OPSGROUP.MyCargo + cargo.group=CargoGroup + cargo.reserved=Reserved + + table.insert(CarrierElement.cargoBay, cargo) + end + + + -- Set my carrier. + CargoGroup:_SetMyCarrier(self, CarrierElement, Reserved) + + -- Fill cargo bay (obsolete). + self.cargoBay[CargoGroup.groupname]=CarrierElement.name + + if not Reserved then + + -- Cargo weight. + local weight=CargoGroup:GetWeightTotal() + + -- Add weight to carrier. + self:AddWeightCargo(CarrierElement.name, weight) + + end + + return self +end + +--- Get all groups currently loaded as cargo. +-- @param #OPSGROUP self +-- @param #string CarrierName (Optional) Only return cargo groups loaded into a particular carrier unit. +-- @return #table Cargo ops groups. +function OPSGROUP:GetCargoGroups(CarrierName) + local cargos={} + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + if CarrierName==nil or element.name==CarrierName then + for _,_cargo in pairs(element.cargoBay) do + local cargo=_cargo --#OPSGROUP.MyCargo + if not cargo.reserved then + table.insert(cargos, cargo.group) + end + end + end + end + + return cargos +end + +--- Get cargo bay item. +-- @param #OPSGROUP self +-- @param #OPSGROUP CargoGroup Cargo group. +-- @return #OPSGROUP.MyCargo Cargo bay item or `nil` if the group is not in the carrier. +-- @return #number CargoBayIndex Index of item in the cargo bay table. +-- @return #OPSGROUP.Element Carrier element. +function OPSGROUP:_GetCargobay(CargoGroup) + + -- Loop over elements and their cargo bay items. + local CarrierElement=nil --#OPSGROUP.Element + local cargobayIndex=nil + local reserved=nil + for i,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + for j,_cargo in pairs(element.cargoBay) do + local cargo=_cargo --#OPSGROUP.MyCargo + if cargo.group and cargo.group.groupname==CargoGroup.groupname then + return cargo, j, element + end + end + end + + return nil, nil, nil +end + +--- Remove OPSGROUP from cargo bay of a carrier. +-- @param #OPSGROUP self +-- @param #OPSGROUP CargoGroup Cargo group. +-- @return #boolean If `true`, cargo could be removed. +function OPSGROUP:_DelCargobay(CargoGroup) + + if self.cargoBay[CargoGroup.groupname] then + + -- Not in cargo bay any more. + self.cargoBay[CargoGroup.groupname]=nil + + end + + -- Get cargo bay info. + local cargoBayItem, cargoBayIndex, CarrierElement=self:_GetCargobay(CargoGroup) + + if cargoBayItem and cargoBayIndex then + + -- Debug info. + self:T(self.lid..string.format("Removing cargo group %s from cargo bay (index=%d) of carrier %s", CargoGroup:GetName(), cargoBayIndex, CarrierElement.name)) + + -- Remove + table.remove(CarrierElement.cargoBay, cargoBayIndex) + + -- Reduce weight (if cargo space was not just reserved). + if not cargoBayItem.reserved then + local weight=CargoGroup:GetWeightTotal() + self:RedWeightCargo(CarrierElement.name, weight) + end + + return true + end + + self:E(self.lid.."ERROR: Group is not in cargo bay. Cannot remove it!") + return false +end + +--- Get cargo transport from cargo queue. +-- @param #OPSGROUP self +-- @return Ops.OpsTransport#OPSTRANSPORT The next due cargo transport or `nil`. +function OPSGROUP:_GetNextCargoTransport() + + -- Current position. + local coord=self:GetCoordinate() + + -- Sort results table wrt prio and distance to pickup zone. + local function _sort(a, b) + local transportA=a --Ops.OpsTransport#OPSTRANSPORT + local transportB=b --Ops.OpsTransport#OPSTRANSPORT + --TODO: Include distance + --local distA=transportA.pickupzone:GetCoordinate():Get2DDistance(coord) + --local distB=transportB.pickupzone:GetCoordinate():Get2DDistance(coord) + return (transportA.priomaxweight then + maxweight=weight + end + + end + end + + return maxweight +end + + +--- Get weight of the internal cargo the group is carriing right now. +-- @param #OPSGROUP self +-- @param #string UnitName Name of the unit. Default is of the whole group. +-- @param #boolean IncludeReserved If `false`, cargo weight that is only *reserved* is **not** counted. By default (`true` or `nil`), the reserved cargo is included. +-- @return #number Cargo weight in kg. +function OPSGROUP:GetWeightCargo(UnitName, IncludeReserved) + + -- Calculate weight based on actual cargo weight. + local weight=0 + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + + if (UnitName==nil or UnitName==element.name) and element.status~=OPSGROUP.ElementStatus.DEAD then + + weight=weight+element.weightCargo or 0 + + end + + end + + -- Calculate weight from stuff in cargo bay. By default this includes the reserved weight if a cargo group was assigned and is currently boarding. + local gewicht=0 + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + if (UnitName==nil or UnitName==element.name) and (element and element.status~=OPSGROUP.ElementStatus.DEAD) then + for _,_cargo in pairs(element.cargoBay) do + local cargo=_cargo --#OPSGROUP.MyCargo + if (not cargo.reserved) or (cargo.reserved==true and (IncludeReserved==true or IncludeReserved==nil)) then + local cargoweight=cargo.group:GetWeightTotal() + gewicht=gewicht+cargoweight + --self:I(self.lid..string.format("unit=%s (reserved=%s): cargo=%s weight=%d, total weight=%d", tostring(UnitName), tostring(IncludeReserved), cargo.group:GetName(), cargoweight, weight)) + end + end + end + end + + -- Debug info. + self:T3(self.lid..string.format("Unit=%s (reserved=%s): weight=%d, gewicht=%d", tostring(UnitName), tostring(IncludeReserved), weight, gewicht)) + + -- Quick check. + if IncludeReserved==false and gewicht~=weight then + self:E(self.lid..string.format("ERROR: FF weight!=gewicht: weight=%.1f, gewicht=%.1f", weight, gewicht)) + end + + return gewicht +end + +--- Get max weight of the internal cargo the group can carry. Optionally, the max cargo weight of a specific unit can be requested. +-- @param #OPSGROUP self +-- @param #string UnitName Name of the unit. Default is of the whole group. +-- @return #number Max cargo weight in kg. This does **not** include any cargo loaded or reserved currently. +function OPSGROUP:GetWeightCargoMax(UnitName) + + local weight=0 + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + + if (UnitName==nil or UnitName==element.name) and element.status~=OPSGROUP.ElementStatus.DEAD then + + weight=weight+element.weightMaxCargo + + end + + end + + return weight +end + +--- Get OPSGROUPs in the cargo bay. +-- @param #OPSGROUP self +-- @return #table Cargo OPSGROUPs. +function OPSGROUP:GetCargoOpsGroups() + + local opsgroups={} + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + for _,_cargo in pairs(element.cargoBay) do + local cargo=_cargo --#OPSGROUP.MyCargo + table.insert(opsgroups, cargo.group) + end + end + + return opsgroups +end + +--- Add weight to the internal cargo of an element of the group. +-- @param #OPSGROUP self +-- @param #string UnitName Name of the unit. Default is of the whole group. +-- @param #number Weight Cargo weight to be added in kg. +function OPSGROUP:AddWeightCargo(UnitName, Weight) + + local element=self:GetElementByName(UnitName) + + if element then --we do not check if the element is actually alive because we need to remove cargo from dead units + + -- Add weight. + element.weightCargo=element.weightCargo+Weight + + -- Debug info. + self:T(self.lid..string.format("%s: Adding %.1f kg cargo weight. New cargo weight=%.1f kg", UnitName, Weight, element.weightCargo)) + + -- For airborne units, we set the weight in game. + if self.isFlightgroup then + trigger.action.setUnitInternalCargo(element.name, element.weightCargo) --https://wiki.hoggitworld.com/view/DCS_func_setUnitInternalCargo + end + + end + + return self +end + +--- Reduce weight to the internal cargo of an element of the group. +-- @param #OPSGROUP self +-- @param #string UnitName Name of the unit. +-- @param #number Weight Cargo weight to be reduced in kg. +function OPSGROUP:RedWeightCargo(UnitName, Weight) + + -- Reduce weight by adding negative weight. + self:AddWeightCargo(UnitName, -Weight) + + return self +end + +--- 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. +-- @return #boolean If `true`, there is an element of the group that can load the whole cargo group. +function OPSGROUP:CanCargo(CargoGroup) + + if CargoGroup then + + local weight=CargoGroup:GetWeightTotal() + + 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 + + return false +end + +--- Add weight to the internal cargo of an element of the group. +-- @param #OPSGROUP self +-- @param #OPSGROUP CargoGroup Cargo group, which needs a carrier. +-- @return #OPSGROUP.Element Carrier able to transport the cargo. +function OPSGROUP:FindCarrierForCargo(CargoGroup) + + local weight=CargoGroup:GetWeightTotal() + + 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 + +--- 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) + + -- Debug info. + self:T(self.lid..string.format("Setting My Carrier: %s (%s), reserved=%s", CarrierGroup:GetName(), tostring(CarrierElement.name), tostring(Reserved))) + + self.mycarrier.group=CarrierGroup + self.mycarrier.element=CarrierElement + self.mycarrier.reserved=Reserved + + self.cargoTransportUID=CarrierGroup.cargoTransport and CarrierGroup.cargoTransport.uid or nil + +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:T(self.lid..string.format("Removing my carrier!")) + self.mycarrier.group=nil + self.mycarrier.element=nil + self.mycarrier.reserved=nil + self.mycarrier={} + self.cargoTransportUID=nil + return self +end + +--- On after "Pickup" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterPickup(From, Event, To) + + -- Old status. + local oldstatus=self.carrierStatus + + -- Set carrier status. + self:_NewCarrierStatus(OPSGROUP.CarrierStatus.PICKUP) + + local TZC=self.cargoTZC + + -- Pickup zone. + local Zone=TZC.PickupZone + + -- Check if already in the pickup zone. + local inzone=self:IsInZone(Zone) + + -- Pickup at an airbase. + local airbasePickup=TZC.PickupAirbase --Wrapper.Airbase#AIRBASE + + -- Check if group is already ready for loading. + local ready4loading=false + if self:IsArmygroup() or self:IsNavygroup() then + + -- Army and Navy groups just need to be inside the zone. + ready4loading=inzone + + else + + -- Aircraft is already parking at the pickup airbase. + ready4loading=self.currbase and airbasePickup and self.currbase:GetName()==airbasePickup:GetName() and self:IsParking() + + -- If a helo is landed in the zone, we also are ready for loading. + if ready4loading==false and self.isHelo and self:IsLandedAt() and inzone then + ready4loading=true + end + end + + -- Ready for loading? + if ready4loading then + + -- We are already in the pickup zone ==> wait and initiate loading. + if (self:IsArmygroup() or self:IsNavygroup()) and not self:IsHolding() then + self:FullStop() + end + + -- Start loading. + self:__Loading(-5) + + else + + -- Set surface type of random coordinate. + local surfacetypes=nil + if self:IsArmygroup() or self:IsFlightgroup() then + surfacetypes={land.SurfaceType.LAND} + elseif self:IsNavygroup() then + surfacetypes={land.SurfaceType.WATER} + end + + -- Get a random coordinate in the pickup zone and let the carrier go there. + local Coordinate=Zone:GetRandomCoordinate(nil, nil, surfacetypes) + --Coordinate:MarkToAll(string.format("Pickup coordinate for group %s [Surface type=%d]", self:GetName(), Coordinate:GetSurfaceType())) + + -- Current Waypoint. + local cwp=self:GetWaypointCurrent() + + -- Current waypoint ID. + local uid=cwp and cwp.uid or nil + + -- Add waypoint. + if self:IsFlightgroup() then + + --- + -- Flight Group + --- + + -- Activate uncontrolled group. + if self:IsParking() and self:IsUncontrolled() then + self:StartUncontrolled() + end + + if airbasePickup then + + --- + -- Pickup at airbase + --- + + -- Get a (random) pre-defined transport path. + local path=self.cargoTransport:_GetPathTransport(self.category, self.cargoTZC) + + -- Get transport path. + if path and oldstatus~=OPSGROUP.CarrierStatus.NOTCARRIER then + + for i=#path.waypoints,1,-1 do + local wp=path.waypoints[i] + local coordinate=COORDINATE:NewFromWaypoint(wp) + local waypoint=FLIGHTGROUP.AddWaypoint(self, coordinate, nil, uid, nil, false) ; waypoint.temp=true + uid=waypoint.uid + if i==1 then + waypoint.temp=false + waypoint.detour=1 --Needs to trigger the landatairbase function. + end + end + + else + + local coordinate=self:GetCoordinate():GetIntermediateCoordinate(Coordinate, 0.5) + + --coordinate:MarkToAll("Pickup Inter Coord") + + -- If this is a helo and no ZONE_AIRBASE was given, we make the helo land in the pickup zone. + local waypoint=FLIGHTGROUP.AddWaypoint(self, coordinate, nil, uid, UTILS.MetersToFeet(self.altitudeCruise), true) ; waypoint.detour=1 + + end + + elseif self.isHelo then + + --- + -- Helo can also land in a zone (NOTE: currently VTOL cannot!) + --- + + -- If this is a helo and no ZONE_AIRBASE was given, we make the helo land in the pickup zone. + local waypoint=FLIGHTGROUP.AddWaypoint(self, Coordinate, nil, uid, UTILS.MetersToFeet(self.altitudeCruise), false) ; waypoint.detour=1 + + else + self:E(self.lid.."ERROR: Transportcarrier aircraft cannot land in Pickup zone! Specify a ZONE_AIRBASE as pickup zone") + end + + -- Cancel landedAt task. This should trigger Cruise once airborne. + if self.isHelo and self:IsLandedAt() then + local Task=self:GetTaskCurrent() + if Task then + self:TaskCancel(Task) + else + self:E(self.lid.."ERROR: No current task but landed at?!") + end + end + + if self:IsWaiting() then + self:__Cruise(-2) + end + + elseif self:IsNavygroup() then + + --- + -- Navy Group + --- + + -- Get a (random) pre-defined transport path. + local path=self.cargoTransport:_GetPathTransport(self.category, self.cargoTZC) + + -- Get transport path. + if path then --and oldstatus~=OPSGROUP.CarrierStatus.NOTCARRIER then + for i=#path.waypoints,1,-1 do + local wp=path.waypoints[i] + local coordinate=COORDINATE:NewFromWaypoint(wp) + local waypoint=NAVYGROUP.AddWaypoint(self, coordinate, nil, uid, nil, false) ; waypoint.temp=true + uid=waypoint.uid + end + end + + -- NAVYGROUP + local waypoint=NAVYGROUP.AddWaypoint(self, Coordinate, nil, uid, self.altitudeCruise, false) ; waypoint.detour=1 + + -- Give cruise command. + self:__Cruise(-2) + + + elseif self:IsArmygroup() then + + --- + -- Army Group + --- + + -- Get a (random) pre-defined transport path. + local path=self.cargoTransport:_GetPathTransport(self.category, self.cargoTZC) + + -- Formation used to go to the pickup zone.. + local Formation=self.cargoTransport:_GetFormationTransport(self.cargoTZC) + + -- Get transport path. + if path and oldstatus~=OPSGROUP.CarrierStatus.NOTCARRIER then + for i=#path.waypoints,1,-1 do + local wp=path.waypoints[i] + local coordinate=COORDINATE:NewFromWaypoint(wp) + local waypoint=ARMYGROUP.AddWaypoint(self, coordinate, nil, uid, wp.action, false) ; waypoint.temp=true + uid=waypoint.uid + end + end + + -- ARMYGROUP + local waypoint=ARMYGROUP.AddWaypoint(self, Coordinate, nil, uid, Formation, false) ; waypoint.detour=1 + + -- Give cruise command. + self:__Cruise(-2) + + end + + end + +end + + +--- On after "Loading" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterLoading(From, Event, To) + + -- Set carrier status. + self:_NewCarrierStatus(OPSGROUP.CarrierStatus.LOADING) + + -- Loading time stamp. + self.Tloading=timer.getAbsTime() + + -- Get valid cargos of the TZC. + local cargos={} + for _,_cargo in pairs(self.cargoTZC.Cargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + + local isCarrier=cargo.opsgroup:IsPickingup() or cargo.opsgroup:IsLoading() or cargo.opsgroup:IsTransporting() or cargo.opsgroup:IsUnloading() + + local isOnMission=cargo.opsgroup:IsOnMission() + + -- Check if cargo is in embark/pickup zone. + -- Added InUtero here, if embark zone is moving (ship) and cargo has been spawned late activated and its position is not updated. Not sure if that breaks something else! + local inzone=cargo.opsgroup:IsInZone(self.cargoTZC.EmbarkZone) --or cargo.opsgroup:IsInUtero() + + -- TODO: Need a better :IsBusy() function or :IsReadyForMission() :IsReadyForBoarding() :IsReadyForTransport() + if self:CanCargo(cargo.opsgroup) and inzone and cargo.opsgroup:IsNotCargo(true) and (not (cargo.delivered or cargo.opsgroup:IsDead() or isCarrier or isOnMission)) then + table.insert(cargos, cargo) + end + end + + -- Sort results table wrt descending weight. + local function _sort(a, b) + local cargoA=a --Ops.OpsGroup#OPSGROUP.CargoGroup + local cargoB=b --Ops.OpsGroup#OPSGROUP.CargoGroup + return cargoA.opsgroup:GetWeightTotal()>cargoB.opsgroup:GetWeightTotal() + end + table.sort(cargos, _sort) + + -- Loop over all cargos. + for _,_cargo in pairs(cargos) do + local cargo=_cargo --#OPSGROUP.CargoGroup + + -- Find a carrier for this cargo. + local carrier=self:FindCarrierForCargo(cargo.opsgroup) + + if carrier then + + -- Set cargo status. + cargo.opsgroup:_NewCargoStatus(OPSGROUP.CargoStatus.ASSIGNED) + + -- Order cargo group to board the carrier. + cargo.opsgroup:Board(self, carrier) + + end + + end +end + +--- Set (new) cargo status. +-- @param #OPSGROUP self +-- @param #string Status New status. +function OPSGROUP:_NewCargoStatus(Status) + + -- Debug info. + if self.verbose>=2 then + self:I(self.lid..string.format("New cargo status: %s --> %s", tostring(self.cargoStatus), tostring(Status))) + end + + -- Set cargo status. + self.cargoStatus=Status + +end + +--- Set (new) carrier status. +-- @param #OPSGROUP self +-- @param #string Status New status. +function OPSGROUP:_NewCarrierStatus(Status) + + -- Debug info. + if self.verbose>=2 then + self:I(self.lid..string.format("New carrier status: %s --> %s", tostring(self.carrierStatus), tostring(Status))) + end + + -- Set cargo status. + self.carrierStatus=Status + +end + +--- Transfer cargo from to another carrier. +-- @param #OPSGROUP self +-- @param #OPSGROUP CargoGroup The cargo group to be transferred. +-- @param #OPSGROUP CarrierGroup The new carrier group. +-- @param #OPSGROUP.Element CarrierElement The new carrier element. +function OPSGROUP:_TransferCargo(CargoGroup, CarrierGroup, CarrierElement) + + -- Debug info. + self:T(self.lid..string.format("Transferring cargo %s to new carrier group %s", CargoGroup:GetName(), CarrierGroup:GetName())) + + -- Unload from this and directly load into the other carrier. + self:Unload(CargoGroup) + CarrierGroup:Load(CargoGroup, CarrierElement) + +end + +--- On after "Load" event. Carrier loads a cargo group into ints cargo bay. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP CargoGroup The OPSGROUP loaded as cargo. +-- @param #OPSGROUP.Element Carrier The carrier element/unit. +function OPSGROUP:onafterLoad(From, Event, To, CargoGroup, Carrier) + + -- Debug info. + self:T(self.lid..string.format("Loading group %s", tostring(CargoGroup.groupname))) + + -- Carrier element. + local carrier=Carrier or CargoGroup:_GetMyCarrierElement() --#OPSGROUP.Element + + -- No carrier provided. + if not carrier then + -- Try to find a carrier manually. + carrier=self:FindCarrierForCargo(CargoGroup) + end + + if carrier then + + --- + -- Embark Cargo + --- + + -- New cargo status. + CargoGroup:_NewCargoStatus(OPSGROUP.CargoStatus.LOADED) + + -- Clear all waypoints. + CargoGroup:ClearWaypoints() + + -- Add into carrier bay. + self:_AddCargobay(CargoGroup, carrier, false) + + -- Despawn this group. + if CargoGroup:IsAlive() then + CargoGroup:Despawn(0, true) + end + + -- Trigger embarked event for cargo group. + CargoGroup:Embarked(self, carrier) + + -- Trigger Loaded event. + self:Loaded(CargoGroup) + + -- Trigger "Loaded" event for current cargo transport. + if self.cargoTransport then + CargoGroup:_DelMyLift(self.cargoTransport) + self.cargoTransport:Loaded(CargoGroup, self, carrier) + else + self:T(self.lid..string.format("WARNING: Loaded cargo but no current OPSTRANSPORT assignment!")) + end + + else + self:E(self.lid.."ERROR: Cargo has no carrier on Load event!") + end + +end + +--- On after "LoadingDone" event. Carrier has loaded all (possible) cargo at the pickup zone. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterLoadingDone(From, Event, To) + + -- Debug info. + self:T(self.lid.."Carrier Loading Done ==> Transport") + + -- Order group to transport. + self:__Transport(1) + +end + +--- On before "Transport" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onbeforeTransport(From, Event, To) + + if self.cargoTransport==nil then + return false + elseif self.cargoTransport:IsDelivered() then --could be if all cargo was dead on boarding + return false + end + + return true +end + + +--- On after "Transport" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterTransport(From, Event, To) + + -- Set carrier status. + self:_NewCarrierStatus(OPSGROUP.CarrierStatus.TRANSPORTING) + + --TODO: This is all very similar to the onafterPickup() function. Could make it general. + + -- Deploy zone. + local Zone=self.cargoTZC.DeployZone + + -- Check if already in deploy zone. + local inzone=self:IsInZone(Zone) + + -- Deploy airbase (if any). + local airbaseDeploy=self.cargoTZC.DeployAirbase --Wrapper.Airbase#AIRBASE + + -- Check if group is already ready for loading. + local ready2deploy=false + if self:IsArmygroup() or self:IsNavygroup() then + ready2deploy=inzone + else + -- Aircraft is already parking at the pickup airbase. + ready2deploy=self.currbase and airbaseDeploy and self.currbase:GetName()==airbaseDeploy:GetName() and self:IsParking() + + -- If a helo is landed in the zone, we also are ready for loading. + if ready2deploy==false and (self.isHelo or self.isVTOL) and self:IsLandedAt() and inzone then + ready2deploy=true + end + end + + if inzone then + + -- We are already in the deploy zone ==> wait and initiate unloading. + if (self:IsArmygroup() or self:IsNavygroup()) and not self:IsHolding() then + self:FullStop() + end + + -- Start unloading. + self:__Unloading(-5) + + else + + local surfacetypes=nil + if self:IsArmygroup() or self:IsFlightgroup() then + surfacetypes={land.SurfaceType.LAND} + elseif self:IsNavygroup() then + surfacetypes={land.SurfaceType.WATER, land.SurfaceType.SHALLOW_WATER} + end + + -- Coord where the carrier goes to unload. + local Coordinate=Zone:GetRandomCoordinate(nil, nil, surfacetypes) --Core.Point#COORDINATE + + --Coordinate:MarkToAll(string.format("Deploy coordinate for group %s [Surface type=%d]", self:GetName(), Coordinate:GetSurfaceType())) + + -- Add waypoint. + if self:IsFlightgroup() then + + -- Activate uncontrolled group. + if self:IsParking() and self:IsUncontrolled() then + self:StartUncontrolled() + end + + if airbaseDeploy then + + --- + -- Deploy at airbase + --- + + local cwp=self:GetWaypointCurrent() + local uid=cwp and cwp.uid or nil + + -- Get a (random) pre-defined transport path. + local path=self.cargoTransport:_GetPathTransport(self.category, self.cargoTZC) + + -- Get transport path. + if path then + + for i=1, #path.waypoints do + local wp=path.waypoints[i] + local coordinate=COORDINATE:NewFromWaypoint(wp) + local waypoint=FLIGHTGROUP.AddWaypoint(self, coordinate, nil, uid, nil, false) ; waypoint.temp=true + uid=waypoint.uid + if i==#path.waypoints then + waypoint.temp=false + waypoint.detour=1 --Needs to trigger the landatairbase function. + end + end + + else + + local coordinate=self:GetCoordinate():GetIntermediateCoordinate(Coordinate, 0.5) + + --coordinate:MarkToAll("Transport Inter Waypoint") + + -- If this is a helo and no ZONE_AIRBASE was given, we make the helo land in the pickup zone. + local waypoint=FLIGHTGROUP.AddWaypoint(self, coordinate, nil, uid, UTILS.MetersToFeet(self.altitudeCruise), true) ; waypoint.detour=1 + + end + + -- Order group to land at an airbase. + --self:__LandAtAirbase(-0.1, airbaseDeploy) + + elseif self.isHelo then + + --- + -- Helo can also land in a zone + --- + + -- If this is a helo and no ZONE_AIRBASE was given, we make the helo land in the pickup zone. + local waypoint=FLIGHTGROUP.AddWaypoint(self, Coordinate, nil, self:GetWaypointCurrent().uid, UTILS.MetersToFeet(self.altitudeCruise), false) ; waypoint.detour=1 + + else + self:E(self.lid.."ERROR: Aircraft (cargo carrier) cannot land in Deploy zone! Specify a ZONE_AIRBASE as deploy zone") + end + + -- Cancel landedAt task. This should trigger Cruise once airborne. + if self.isHelo and self:IsLandedAt() then + local Task=self:GetTaskCurrent() + if Task then + self:TaskCancel(Task) + else + self:E(self.lid.."ERROR: No current task but landed at?!") + end + end + + elseif self:IsArmygroup() then + + local cwp=self:GetWaypointCurrent() + local uid=cwp and cwp.uid or nil + + -- Get transport path. + local path=self.cargoTransport:_GetPathTransport(self.category, self.cargoTZC) + + -- Formation used for transporting. + local Formation=self.cargoTransport:_GetFormationTransport(self.cargoTZC) + + -- Get transport path. + if path then + for i=1,#path.waypoints do + local wp=path.waypoints[i] + local coordinate=COORDINATE:NewFromWaypoint(wp) + local waypoint=ARMYGROUP.AddWaypoint(self, coordinate, nil, uid, wp.action, false) ; waypoint.temp=true + uid=waypoint.uid + end + end + + -- ARMYGROUP + local waypoint=ARMYGROUP.AddWaypoint(self, Coordinate, nil, uid, Formation, false) ; waypoint.detour=1 + + -- Give cruise command. + self:Cruise() + + 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(self.category, self.cargoTZC) + + -- Get transport path. + if path then + for i=1,#path.waypoints do + local wp=path.waypoints[i] + local coordinate=COORDINATE:NewFromWaypoint(wp) + local waypoint=NAVYGROUP.AddWaypoint(self, coordinate, nil, uid, nil, false) ; waypoint.temp=true + uid=waypoint.uid + end + end + + -- NAVYGROUP + local waypoint=NAVYGROUP.AddWaypoint(self, Coordinate, nil, uid, self.altitudeCruise, false) ; waypoint.detour=1 + + -- Give cruise command. + self:Cruise() + + end + + end + +end + +--- On after "Unloading" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterUnloading(From, Event, To) + + -- Set carrier status to UNLOADING. + self:_NewCarrierStatus(OPSGROUP.CarrierStatus.UNLOADING) + + -- Deploy zone. + local zone=self.cargoTZC.DisembarkZone or self.cargoTZC.DeployZone --Core.Zone#ZONE + + for _,_cargo in pairs(self.cargoTZC.Cargos) do + local cargo=_cargo --#OPSGROUP.CargoGroup + + -- 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() then + + -- Disembark to carrier. + local needscarrier=false --#boolean + local carrier=nil --Ops.OpsGroup#OPSGROUP.Element + local carrierGroup=nil --Ops.OpsGroup#OPSGROUP + + -- Try to get the OPSGROUP if deploy zone is a ship. + if zone and zone:IsInstanceOf("ZONE_AIRBASE") and zone:GetAirbase():IsShip() then + local shipname=zone:GetAirbase():GetName() + local ship=UNIT:FindByName(shipname) + local group=ship:GetGroup() + carrierGroup=_DATABASE:GetOpsGroup(group:GetName()) + carrier=carrierGroup:GetElementByName(shipname) + end + + if self.cargoTZC.DisembarkCarriers and #self.cargoTZC.DisembarkCarriers>0 then + + needscarrier=true + + carrier, carrierGroup=self.cargoTransport:FindTransferCarrierForCargo(cargo.opsgroup, zone, self.cargoTZC) + + --TODO: max unloading time if transfer carrier does not arrive in the zone. + + 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. + --- + + self:_TransferCargo(cargo.opsgroup, carrierGroup, carrier) + + elseif zone and zone:IsInstanceOf("ZONE_AIRBASE") and zone:GetAirbase():IsShip() then + + --- + -- Delivered to a ship via helo or VTOL + --- + + -- Issue warning. + self:E(self.lid.."ERROR: Deploy/disembark zone is a ZONE_AIRBASE of a ship! Where to put the cargo? Dumping into the sea, sorry!") + + -- Unload but keep "in utero" (no coordinate provided). + self:Unload(cargo.opsgroup) + + else + + --- + -- Delivered to deploy zone + --- + + if self.cargoTransport:GetDisembarkInUtero(self.cargoTZC) then + + -- Unload but keep "in utero" (no coordinate provided). + self:Unload(cargo.opsgroup) + + else + + -- Get disembark zone of this TZC. + local DisembarkZone=self.cargoTransport:GetDisembarkZone(self.cargoTZC) + + local Coordinate=nil + + + if DisembarkZone then + + -- Random coordinate in disembark zone. + Coordinate=DisembarkZone:GetRandomCoordinate() + + else + + local element=cargo.opsgroup:_GetMyCarrierElement() + + if element then + + -- Get random point in disembark zone. + local zoneCarrier=self:GetElementZoneUnload(element.name) + + -- Random coordinate/heading in the zone. + Coordinate=zoneCarrier:GetRandomCoordinate() + + else + env.info(string.format("FF ERROR carrier element nil!")) + end + + end + + -- Random heading of the group. + local Heading=math.random(0,359) + + -- Activation on/off. + local activation=self.cargoTransport:GetDisembarkActivation(self.cargoTZC) + if cargo.disembarkActivation~=nil then + activation=cargo.disembarkActivation + end + + -- Unload to Coordinate. + self:Unload(cargo.opsgroup, Coordinate, activation, Heading) + + end + + -- Trigger "Unloaded" event for current cargo transport + self.cargoTransport:Unloaded(cargo.opsgroup, self) + + 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 + +--- On before "Unload" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP OpsGroup The OPSGROUP loaded as cargo. +-- @param Core.Point#COORDINATE Coordinate Coordinate were the group is unloaded to. +-- @param #number Heading Heading of group. +function OPSGROUP:onbeforeUnload(From, Event, To, OpsGroup, Coordinate, Heading) + + -- Remove group from carrier bay. If group is not in cargo bay, function will return false and transition is denied. + local removed=self:_DelCargobay(OpsGroup) + + return removed +end + +--- On after "Unload" event. Carrier unloads a cargo group from its cargo bay. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP OpsGroup The OPSGROUP loaded as cargo. +-- @param Core.Point#COORDINATE Coordinate Coordinate were the group is unloaded to. +-- @param #boolean Activated If `true`, group is active. If `false`, group is spawned in late activated state. +-- @param #number Heading (Optional) Heading of group in degrees. Default is random heading for each unit. +function OPSGROUP:onafterUnload(From, Event, To, OpsGroup, Coordinate, Activated, Heading) + + -- New cargo status. + OpsGroup:_NewCargoStatus(OPSGROUP.CargoStatus.NOTCARGO) + + --TODO: Unload flightgroup. Find parking spot etc. + + if Coordinate then + + --- + -- Respawn at a coordinate. + --- + + -- Template for the respawned group. + local Template=UTILS.DeepCopy(OpsGroup.template) --DCS#Template + + -- No late activation. + if Activated==false then + Template.lateActivation=true + else + Template.lateActivation=false + end + + -- Loop over template units. + for _,Unit in pairs(Template.units) do + + local element=OpsGroup:GetElementByName(Unit.name) + + if element then + + local vec3=element.vec3 + + -- Relative pos vector. + local rvec2={x=Unit.x-Template.x, y=Unit.y-Template.y} --DCS#Vec2 + + local cvec2={x=Coordinate.x, y=Coordinate.z} --DCS#Vec2 + + -- Position. + Unit.x=cvec2.x+rvec2.x + Unit.y=cvec2.y+rvec2.y + Unit.alt=land.getHeight({x=Unit.x, y=Unit.y}) + + -- Heading. + Unit.heading=Heading and math.rad(Heading) or Unit.heading + Unit.psi=-Unit.heading + + end + + end + + -- Respawn group. + OpsGroup:_Respawn(0, Template) + + -- Add current waypoint. These have been cleard on loading. + if OpsGroup:IsNavygroup() then + OpsGroup:ClearWaypoints() + OpsGroup.currentwp=1 + OpsGroup.passedfinalwp=true + NAVYGROUP.AddWaypoint(OpsGroup, Coordinate, nil, nil, nil, false) + elseif OpsGroup:IsArmygroup() then + OpsGroup:ClearWaypoints() + OpsGroup.currentwp=1 + OpsGroup.passedfinalwp=true + ARMYGROUP.AddWaypoint(OpsGroup, Coordinate, nil, nil, nil, false) + end + + else + + --- + -- Just remove from this carrier. + --- + + -- Nothing to do. + + OpsGroup.position=self:GetVec3() + + end + + -- Trigger "Disembarked" event. + OpsGroup:Disembarked(OpsGroup:_GetMyCarrierGroup(), OpsGroup:_GetMyCarrierElement()) + + -- Trigger "Unloaded" event. + self:Unloaded(OpsGroup) + + -- Remove my carrier. + OpsGroup:_RemoveMyCarrier() + +end + +--- On after "Unloaded" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP OpsGroupCargo Cargo OPSGROUP that was unloaded from a carrier. +function OPSGROUP:onafterUnloaded(From, Event, To, OpsGroupCargo) + self:I(self.lid..string.format("Unloaded OPSGROUP %s", OpsGroupCargo:GetName())) +end + + +--- On after "UnloadingDone" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterUnloadingDone(From, Event, To) + + -- Debug info + self:T(self.lid.."Cargo unloading done..") + + -- Cancel landedAt task. + if self:IsFlightgroup() and self:IsLandedAt() then + local Task=self:GetTaskCurrent() + self:__TaskCancel(5, Task) + end + + -- Check everything was delivered (or is dead). + local delivered=self:_CheckGoPickup(self.cargoTransport) + + if not delivered then + + -- Get new TZC. + self.cargoTZC=self.cargoTransport:_GetTransportZoneCombo(self) + + if self.cargoTZC then + + -- Pickup the next batch. + self:I(self.lid.."Unloaded: Still cargo left ==> Pickup") + self:Pickup() + + else + + -- Debug info. + self:I(self.lid..string.format("WARNING: Not all cargo was delivered but could not get a transport zone combo ==> setting carrier state to NOT CARRIER")) + + -- This is not a carrier anymore. + self:_NewCarrierStatus(OPSGROUP.CarrierStatus.NOTCARRIER) + + end + + else + + -- Everything delivered. + self:I(self.lid.."Unloaded: ALL cargo unloaded ==> Delivered (current)") + self:Delivered(self.cargoTransport) + + end + +end + +--- On after "Delivered" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsTransport#OPSTRANSPORT CargoTransport The cargo transport assignment. +function OPSGROUP:onafterDelivered(From, Event, To, CargoTransport) + + -- Check if this was the current transport. + if self.cargoTransport and self.cargoTransport.uid==CargoTransport.uid then + + -- Checks + if self:IsPickingup() then + -- Delete pickup waypoint? + local wpindex=self:GetWaypointIndexNext(false) + if wpindex then + self:RemoveWaypoint(wpindex) + end + -- Remove landing airbase. + self.isLandingAtAirbase=nil + elseif self:IsLoading() then + -- Nothing to do? + elseif self:IsTransporting() then + -- This should not happen. Carrier is transporting, how can the cargo be delivered? + elseif self:IsUnloading() then + -- Nothing to do? + end + + -- This is not a carrier anymore. + self:_NewCarrierStatus(OPSGROUP.CarrierStatus.NOTCARRIER) + + -- Startup uncontrolled aircraft to allow it to go back. + if self:IsFlightgroup() then + + local function atbase(_airbase) + local airbase=_airbase --Wrapper.Airbase#AIRBASE + if airbase and self.currbase then + if airbase.AirbaseName==self.currbase.AirbaseName then + return true + end + end + return false + end + + -- Check if uncontrolled and NOT at destination. If so, start up uncontrolled and let flight return to whereever it wants to go. + if self:IsUncontrolled() and not atbase(self.destbase) then + self:StartUncontrolled() + end + if self:IsLandedAt() then + local Task=self:GetTaskCurrent() + self:TaskCancel(Task) + end + else + -- Army & Navy: give Cruise command to "wake up" from waiting status. + self:__Cruise(0.1) + end + + -- Set carrier transport status. + self.cargoTransport:SetCarrierTransportStatus(self, OPSTRANSPORT.Status.DELIVERED) + + -- Check group done. + self:T(self.lid..string.format("All cargo of transport UID=%d delivered ==> check group done in 0.2 sec", self.cargoTransport.uid)) + self:_CheckGroupDone(0.2) + + + end + + -- Remove cargo transport from cargo queue. + --self:DelOpsTransport(CargoTransport) + +end + +--- On after "TransportCancel" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsTransport#OPSTRANSPORT The transport to be cancelled. +function OPSGROUP:onafterTransportCancel(From, Event, To, Transport) + + if self.cargoTransport and self.cargoTransport.uid==Transport.uid then + + --- + -- Current Transport + --- + + -- Debug info. + self:T(self.lid..string.format("Cancel current transport %d", Transport.uid)) + + -- Call delivered= + local calldelivered=false + + if self:IsPickingup() then + + -- On its way to the pickup zone. Remove waypoint. Will be done in delivered. + calldelivered=true + + elseif self:IsLoading() then + + -- Handle cargo groups. + local cargos=Transport:GetCargoOpsGroups(false) + + for _,_opsgroup in pairs(cargos) do + local opsgroup=_opsgroup --#OPSGROUP + + if opsgroup:IsBoarding(self.groupname) then + + -- Remove boarding waypoint. + opsgroup:RemoveWaypoint(self.currentwp+1) + + -- Remove from cargo bay (reserved), remove mycarrier, set cargo status. + self:_DelCargobay(opsgroup) + opsgroup:_RemoveMyCarrier() + opsgroup:_NewCargoStatus(OPSGROUP.CargoStatus.NOTCARGO) + + elseif opsgroup:IsLoaded(self.groupname) then + + -- Get random point in disembark zone. + local zoneCarrier=self:GetElementZoneUnload(opsgroup:_GetMyCarrierElement().name) + + -- Random coordinate/heading in the zone. + local Coordinate=zoneCarrier and zoneCarrier:GetRandomCoordinate() or self.cargoTransport:GetEmbarkZone(self.cargoTZC):GetRandomCoordinate() + + -- Random heading of the group. + local Heading=math.random(0,359) + + -- Unload to Coordinate. + self:Unload(opsgroup, Coordinate, self.cargoTransport:GetDisembarkActivation(self.cargoTZC), Heading) + + -- Trigger "Unloaded" event for current cargo transport + self.cargoTransport:Unloaded(opsgroup, self) + + end + + end + + -- Call delivered. + calldelivered=true + + elseif self:IsTransporting() then + + -- Well, we cannot just unload the cargo anywhere. + + -- TODO: Best would be to bring the cargo back to the pickup zone! + + elseif self:IsUnloading() then + -- Unloading anyway... delivered will be called when done. + else + + end + + -- Transport delivered. + if calldelivered then + self:__Delivered(-2, Transport) + end + + else + + --- + -- NOT the current transport + --- + + -- Set mission group status. + Transport:SetCarrierTransportStatus(self, AUFTRAG.GroupStatus.CANCELLED) + + -- Remove transport from queue. This also removes the carrier from the transport. + self:DelOpsTransport(Transport) + + -- Remove carrier. + --Transport:_DelCarrier(self) + + -- Send group RTB or WAIT if nothing left to do. + self:_CheckGroupDone(1) + + end + +end + + +--- +-- Cargo Group Functions +--- + +--- On before "Board" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP CarrierGroup The carrier group. +-- @param #OPSGROUP.Element Carrier The OPSGROUP element +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:_NewCargoStatus(OPSGROUP.CargoStatus.NOTCARGO) + return false + elseif Carrier.status==OPSGROUP.ElementStatus.DEAD then + self:I(self.lid.."Carrier Element DEAD ==> Deny Board transition!") + self:_NewCargoStatus(OPSGROUP.CargoStatus.NOTCARGO) + return false + end + + return true +end + +--- On after "Board" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP CarrierGroup The carrier group. +-- @param #OPSGROUP.Element Carrier The OPSGROUP element +function OPSGROUP:onafterBoard(From, Event, To, CarrierGroup, Carrier) + + -- Set cargo status. + self:_NewCargoStatus(OPSGROUP.CargoStatus.BOARDING) + + -- 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 (CarrierIsArmyOrNavy and (CarrierGroup:IsHolding() and CarrierGroup:GetVelocity(Carrier.name)<=1)) or (CarrierGroup:IsFlightgroup() and (CarrierGroup:IsParking() or CarrierGroup:IsLandedAt())) then + if (CarrierIsArmyOrNavy and (CarrierGroup:GetVelocity(Carrier.name)<=1)) or (CarrierGroup:IsFlightgroup() and (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 CargoIsArmyOrNavy and self:IsAlive() and CarrierGroup:IsAlive() + + -- Armygroup cannot board ship ==> Load directly. + if self:IsArmygroup() and CarrierGroup:IsNavygroup() then + board=false + end + + if board then + + -- Debug info. + self:T(self.lid..string.format("Boarding group=%s [%s], carrier=%s", CarrierGroup:GetName(), CarrierGroup:GetState(), tostring(Carrier.name))) + + -- TODO: Implement embarkzone. + local Coordinate=Carrier.unit:GetCoordinate() + + -- Clear all waypoints. + self:ClearWaypoints(self.currentwp+1) + + if self.isArmygroup then + local waypoint=ARMYGROUP.AddWaypoint(self, Coordinate, nil, nil, ENUMS.Formation.Vehicle.Diamond) ; waypoint.detour=1 + self:Cruise() + else + local waypoint=NAVYGROUP.AddWaypoint(self, Coordinate) ; waypoint.detour=1 + self:Cruise() + end + + -- Set carrier. As long as the group is not loaded, we only reserve the cargo space. + CarrierGroup:_AddCargobay(self, Carrier, true) + + else + + --- + -- Direct load into carrier. + --- + + -- Debug info. + self:T(self.lid..string.format("Board [loaded=%s] with direct load to carrier group=%s, element=%s", tostring(self:IsLoaded()), CarrierGroup:GetName(), tostring(Carrier.name))) + + -- Get current carrier group. + local mycarriergroup=self:_GetMyCarrierGroup() + if mycarriergroup then + self:T(self.lid..string.format("Current carrier group %s", mycarriergroup:GetName())) + end + + -- Unload cargo first. + if mycarriergroup and mycarriergroup:GetName()~=CarrierGroup:GetName() then + -- TODO: Unload triggers other stuff like Disembarked. This can be a problem! + self:T(self.lid.."Unloading from mycarrier") + mycarriergroup:Unload(self) + end + + -- Trigger Load event. + CarrierGroup:Load(self) + + end + + else + + -- Redo boarding call. + self:T(self.lid.."Carrier not ready for boarding yet ==> repeating boarding call in 10 sec") + self:__Board(-10, CarrierGroup, Carrier) + + -- Set carrier. As long as the group is not loaded, we only reserve the cargo space.� + CarrierGroup:_AddCargobay(self, Carrier, true) + + end + + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -3877,37 +8326,37 @@ end function OPSGROUP:_CheckInZones() if self.checkzones and self:IsAlive() then - + local Ncheck=self.checkzones:Count() local Ninside=self.inzones:Count() - + -- Debug info. self:T(self.lid..string.format("Check if group is in %d zones. Currently it is in %d zones.", self.checkzones:Count(), self.inzones:Count())) -- Firstly, check if group is still inside zone it was already in. If not, remove zones and trigger LeaveZone() event. local leftzones={} for inzonename, inzone in pairs(self.inzones:GetSet()) do - + -- Check if group is still inside the zone. local isstillinzone=self.group:IsInZone(inzone) --:IsPartlyOrCompletelyInZone(inzone) - + -- If not, trigger, LeaveZone event. if not isstillinzone then table.insert(leftzones, inzone) - end + end end - + -- Trigger leave zone event. for _,leftzone in pairs(leftzones) do self:LeaveZone(leftzone) end - - + + -- Now, run of all check zones and see if the group entered a zone. local enterzones={} for checkzonename,_checkzone in pairs(self.checkzones:GetSet()) do local checkzone=_checkzone --Core.Zone#ZONE - + -- Is group currtently in this check zone? local isincheckzone=self.group:IsInZone(checkzone) --:IsPartlyOrCompletelyInZone(checkzone) @@ -3915,12 +8364,12 @@ function OPSGROUP:_CheckInZones() table.insert(enterzones, checkzone) end end - + -- Trigger enter zone event. for _,enterzone in pairs(enterzones) do self:EnterZone(enterzone) end - + end end @@ -3929,7 +8378,7 @@ end -- @param #OPSGROUP self function OPSGROUP:_CheckDetectedUnits() - if self.group and not self:IsDead() then + if self.detectionOn and self.group and not self:IsDead() then -- Get detected DCS units. local detectedtargets=self.group:GetDetectedTargets() @@ -3940,33 +8389,33 @@ function OPSGROUP:_CheckDetectedUnits() local DetectedObject=Detection.object -- DCS#Object if DetectedObject and DetectedObject:isExist() and DetectedObject.id_<50000000 then - + -- Unit. local unit=UNIT:Find(DetectedObject) - + if unit and unit:IsAlive() then - + -- Name of detected unit local unitname=unit:GetName() - -- Add unit to detected table of this run. + -- Add unit to detected table of this run. table.insert(detected, unit) - + -- Trigger detected unit event ==> This also triggers the DetectedUnitNew and DetectedUnitKnown events. self:DetectedUnit(unit) - + -- Get group of unit. local group=unit:GetGroup() - + -- Add group to table. - if group then - groups[group:GetName()]=group + if group then + groups[group:GetName()]=group end - + end end end - + -- Call detected group event. for groupname, group in pairs(groups) do self:DetectedGroup(group) @@ -3992,7 +8441,7 @@ function OPSGROUP:_CheckDetectedUnits() end end - + -- Remove lost units from detected set. self.detectedunits:RemoveUnitsByName(lost) @@ -4017,7 +8466,7 @@ function OPSGROUP:_CheckDetectedUnits() end end - + -- Remove lost units from detected set. self.detectedgroups:RemoveGroupsByName(lost) @@ -4030,126 +8479,171 @@ end -- @param #number delay Delay in seconds. function OPSGROUP:_CheckGroupDone(delay) + -- FSM state. + local fsmstate=self:GetState() + if self:IsAlive() and self.isAI then if delay and delay>0 then + -- Debug info. + self:T(self.lid..string.format("Check OPSGROUP [state=%s] done in %.3f seconds...", fsmstate, delay)) + -- Delayed call. self:ScheduleOnce(delay, self._CheckGroupDone, self) else - + + -- Debug info. + self:T(self.lid..string.format("Check OSGROUP [state=%s] done?", fsmstate)) + + -- Group is engaging something. if self:IsEngaging() then + self:T(self.lid.."Engaging! Group NOT done ==> UpdateRoute()") self:UpdateRoute() return end - + + -- Group is returning + if self:IsReturning() then + self:T(self.lid.."Returning! Group NOT done...") + return + end + + -- Group is returning + if self:IsRearming() then + self:T(self.lid.."Rearming! Group NOT done...") + return + end + + -- Group is waiting. We deny all updates. + if self:IsWaiting() then + -- If group is waiting, we assume that is the way it is meant to be. + self:T(self.lid.."Waiting! Group NOT done...") + return + end + + -- First check if there is a paused mission that + if self.missionpaused then + self:T(self.lid..string.format("Found paused mission %s [%s]. Unpausing mission...", self.missionpaused.name, self.missionpaused.type)) + self:UnpauseMission() + return + end + -- Get current waypoint. local waypoint=self:GetWaypoint(self.currentwp) - - --env.info("FF CheckGroupDone") if waypoint then - + -- Number of tasks remaining for this waypoint. local ntasks=self:CountTasksWaypoint(waypoint.uid) - + -- We only want to update the route if there are no more tasks to be done. if ntasks>0 then self:T(self.lid..string.format("Still got %d tasks for the current waypoint UID=%d ==> RETURN (no action)", ntasks, waypoint.uid)) return end - end - + end + if self.adinfinitum then - + --- -- Parol Ad Infinitum --- if #self.waypoints>0 then - + -- Next waypoint index. local i=self:GetWaypointIndexNext(true) - + -- Get positive speed to first waypoint. local speed=self:GetSpeedToWaypoint(i) - - -- Start route at first waypoint. - self:UpdateRoute(i, speed) - + + -- Cruise. + self:Cruise(speed) + + -- Debug info. self:T(self.lid..string.format("Adinfinitum=TRUE ==> Goto WP index=%d at speed=%d knots", i, speed)) - + else self:E(self.lid..string.format("WARNING: No waypoints left! Commanding a Full Stop")) - self:__FullStop(-1) + self:__FullStop(-1) end else - + --- -- Finite Patrol --- - - if self.passedfinalwp then - + + if self:HasPassedFinalWaypoint() then + --- -- Passed FINAL waypoint --- - - -- No further waypoints. Command a full stop. - self:__FullStop(-1) - - self:T(self.lid..string.format("Passed final WP, adinfinitum=FALSE ==> Full Stop")) - + + if self.legion then + + self:T(self.lid..string.format("Passed final WP, adinfinitum=FALSE, LEGION set ==> RTZ")) + self:RTZ(self.legion.spawnzone) + + else + + -- No further waypoints. Command a full stop. + self:__FullStop(-1) + + self:T(self.lid..string.format("Passed final WP, adinfinitum=FALSE ==> Full Stop")) + + end + else - + --- -- Final waypoint NOT passed yet --- - + if #self.waypoints>0 then self:T(self.lid..string.format("NOT Passed final WP, #WP>0 ==> Update Route")) - self:UpdateRoute() + self:Cruise() else self:E(self.lid..string.format("WARNING: No waypoints left! Commanding a Full Stop")) self:__FullStop(-1) end - + end - + end - - end + + end end - + end --- Check if group got stuck. -- @param #OPSGROUP self function OPSGROUP:_CheckStuck() - -- Holding means we are not stuck. - if self:IsHolding() or self:Is("Rearming") then + -- Cases we are not stuck. + if self:IsHolding() or self:Is("Rearming") or self:IsWaiting() or self:HasPassedFinalWaypoint() then return end - + -- Current time. local Tnow=timer.getTime() - + -- Expected speed in m/s. local ExpectedSpeed=self:GetExpectedSpeed() - + -- Current speed in m/s. local speed=self:GetVelocity() - + -- Check speed. - if speed<0.5 then - + if speed<0.1 then + if ExpectedSpeed>0 and not self.stuckTimestamp then self:T2(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected", speed, ExpectedSpeed)) self.stuckTimestamp=Tnow self.stuckVec3=self:GetVec3() end - + else -- Moving (again). self.stuckTimestamp=nil @@ -4157,20 +8651,52 @@ function OPSGROUP:_CheckStuck() -- Somehow we are not moving... if self.stuckTimestamp then - + -- Time we are holding. local holdtime=Tnow-self.stuckTimestamp - - if holdtime>=10*60 then - + + if holdtime>=5*60 and holdtime<10*60 then + + -- Debug warning. self:E(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected for %d sec", speed, ExpectedSpeed, holdtime)) - + + -- Give cruise command again. + if self:IsReturning() then + self:__RTZ(1) + else + self:__Cruise(1) + end + + elseif holdtime>=10*60 and holdtime<30*60 then + + -- Debug warning. + self:E(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected for %d sec", speed, ExpectedSpeed, holdtime)) + --TODO: Stuck event! - + + -- Look for a current mission and cancel it as we do not seem to be able to perform it. + local mission=self:GetMissionCurrent() + if mission then + self:E(self.lid..string.format("WARNING: Cancelling mission %s [%s] due to being stuck", mission:GetName(), mission:GetType())) + self:MissionCancel(mission) + else + -- Give cruise command again. + if self:IsReturning() then + self:__RTZ(1) + else + self:__Cruise(1) + end + end + + elseif holdtime>=30*60 then + + -- Debug warning. + self:E(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected for %d sec", speed, ExpectedSpeed, holdtime)) + end - + end - + end @@ -4182,25 +8708,25 @@ function OPSGROUP:_CheckDamage() self.life=0 local damaged=false for _,_element in pairs(self.elements) do - local element=_element --Ops.OpsGroup#OPSGROUP - + local element=_element --Ops.OpsGroup#OPSGROUP.Element + -- Current life points. local life=element.unit:GetLife() - + self.life=self.life+life - + if life0 then - + -- Get current ammo. local ammo=self:GetAmmoTot() - + -- Check if rearming is completed. if self:IsRearming() then if ammo.Total==self.ammo.Total then self:Rearmed() end - end - + end + -- Total. if self.outofAmmo and ammo.Total>0 then self.outofAmmo=false @@ -4252,7 +8778,7 @@ function OPSGROUP:_CheckAmmoStatus() -- Guns. if self.outofGuns and ammo.Guns>0 then - self.outoffGuns=false + self.outofGuns=false end if ammo.Guns==0 and self.ammo.Guns>0 and not self.outofGuns then self.outofGuns=true @@ -4261,7 +8787,7 @@ function OPSGROUP:_CheckAmmoStatus() -- Rockets. if self.outofRockets and ammo.Rockets>0 then - self.outoffRockets=false + self.outofRockets=false end if ammo.Rockets==0 and self.ammo.Rockets>0 and not self.outofRockets then self.outofRockets=true @@ -4270,29 +8796,66 @@ function OPSGROUP:_CheckAmmoStatus() -- Bombs. if self.outofBombs and ammo.Bombs>0 then - self.outoffBombs=false + self.outofBombs=false end if ammo.Bombs==0 and self.ammo.Bombs>0 and not self.outofBombs then self.outofBombs=true self:OutOfBombs() end - -- Missiles. + -- Missiles (All). if self.outofMissiles and ammo.Missiles>0 then - self.outoffMissiles=false + self.outofMissiles=false end if ammo.Missiles==0 and self.ammo.Missiles>0 and not self.outofMissiles then self.outofMissiles=true self:OutOfMissiles() end - + + -- Missiles AA. + if self.outofMissilesAA and ammo.MissilesAA>0 then + self.outofMissilesAA=false + end + if ammo.MissilesAA==0 and self.ammo.MissilesAA>0 and not self.outofMissilesAA then + self.outofMissilesAA=true + self:OutOfMissilesAA() + end + + -- Missiles AG. + if self.outofMissilesAG and ammo.MissilesAG>0 then + self.outofMissilesAG=false + end + if ammo.MissilesAG==0 and self.ammo.MissilesAG>0 and not self.outofMissilesAG then + self.outofMissilesAG=true + self:OutOfMissilesAG() + end + + -- Missiles AS. + if self.outofMissilesAS and ammo.MissilesAS>0 then + self.outofMissilesAS=false + end + if ammo.MissilesAS==0 and self.ammo.MissilesAS>0 and not self.outofMissilesAS then + self.outofMissilesAS=true + self:OutOfMissilesAS() + end + + -- Torpedos. + if self.outofTorpedos and ammo.Torpedos>0 then + self.outofTorpedos=false + end + if ammo.Torpedos==0 and self.ammo.Torpedos>0 and not self.outofTorpedos then + self.outofTorpedos=true + self:OutOfTorpedos() + end + + -- Check if group is engaging. if self:IsEngaging() and ammo.Total==0 then self:Disengage() end end - + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -4306,9 +8869,9 @@ function OPSGROUP:_PrintTaskAndMissionStatus() --- -- Tasks: verbose >= 3 --- - + -- Task queue. - if self.verbose>=3 and #self.taskqueue>0 then + if self.verbose>=3 and #self.taskqueue>0 then local text=string.format("Tasks #%d", #self.taskqueue) for i,_task in pairs(self.taskqueue) do local task=_task --Ops.OpsGroup#OPSGROUP.Task @@ -4338,22 +8901,22 @@ function OPSGROUP:_PrintTaskAndMissionStatus() end self:I(self.lid..text) end - + --- -- Missions: verbose>=2 --- - + -- Current mission name. - if self.verbose>=2 then + if self.verbose>=2 then local Mission=self:GetMissionByID(self.currentmission) - + -- Current status. local text=string.format("Missions %d, Current: %s", self:CountRemainingMissison(), Mission and Mission.name or "none") for i,_mission in pairs(self.missionqueue) do local mission=_mission --Ops.Auftrag#AUFTRAG local Cstart= UTILS.SecondsToClock(mission.Tstart, true) local Cstop = mission.Tstop and UTILS.SecondsToClock(mission.Tstop, true) or "INF" - text=text..string.format("\n[%d] %s (%s) status=%s (%s), Time=%s-%s, prio=%d wp=%s targets=%d", + text=text..string.format("\n[%d] %s (%s) status=%s (%s), Time=%s-%s, prio=%d wp=%s targets=%d", i, tostring(mission.name), mission.type, mission:GetGroupStatus(self), tostring(mission.status), Cstart, Cstop, mission.prio, tostring(mission:GetGroupWaypointIndex(self)), mission:CountMissionTargets()) end self:I(self.lid..text) @@ -4365,32 +8928,63 @@ end -- Waypoints & Routing ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Simple task function. Can be used to call a function which has the warehouse and the executing group as parameters. +-- @param #OPSGROUP self +-- @param #string Function The name of the function to call passed as string. +-- @param #number uid Waypoint UID. +function OPSGROUP:_SimpleTaskFunction(Function, uid) + + -- Task script. + local DCSScript = {} + + --_DATABASE:FindOpsGroup(groupname) + + DCSScript[#DCSScript+1] = string.format('local mygroup = _DATABASE:FindOpsGroup(\"%s\") ', self.groupname) -- The group that executes the task function. Very handy with the "...". + DCSScript[#DCSScript+1] = string.format('%s(mygroup, %d)', Function, uid) -- Call the function, e.g. myfunction.(warehouse,mygroup) + + -- Create task. + local DCSTask=CONTROLLABLE.TaskWrappedAction(self, CONTROLLABLE.CommandDoScript(self, table.concat(DCSScript))) + + return DCSTask +end + --- Enhance waypoint table. -- @param #OPSGROUP self -- @param #OPSGROUP.Waypoint Waypoint data. -- @return #OPSGROUP.Waypoint Modified waypoint data. function OPSGROUP:_CreateWaypoint(waypoint) - + -- Set uid. waypoint.uid=self.wpcounter - + -- Waypoint has not been passed yet. waypoint.npassed=0 - + -- Coordinate. waypoint.coordinate=COORDINATE:New(waypoint.x, waypoint.alt, waypoint.y) -- Set waypoint name. - waypoint.name=string.format("Waypoint UID=%d", waypoint.uid) - + waypoint.name=string.format("Waypoint UID=%d", waypoint.uid) + -- Set types. waypoint.patrol=false waypoint.detour=false waypoint.astar=false + waypoint.temp=false + + -- Tasks of this waypoint + local taskswp={} + + -- At each waypoint report passing. + local TaskPassingWaypoint=self:_SimpleTaskFunction("OPSGROUP._PassingWaypoint", waypoint.uid) + table.insert(taskswp, TaskPassingWaypoint) + + -- Waypoint task combo. + waypoint.task=self.group:TaskCombo(taskswp) -- Increase UID counter. self.wpcounter=self.wpcounter+1 - + return waypoint end @@ -4407,59 +9001,115 @@ function OPSGROUP:_AddWaypoint(waypoint, wpnumber) table.insert(self.waypoints, wpnumber, waypoint) -- Debug info. - self:T(self.lid..string.format("Adding waypoint at index=%d id=%d", wpnumber, waypoint.uid)) - + self:T(self.lid..string.format("Adding waypoint at index=%d with UID=%d", wpnumber, waypoint.uid)) + -- Now we obviously did not pass the final waypoint. - self.passedfinalwp=false - - -- Switch to cruise mode. - if self:IsHolding() then - self:Cruise() + if self.currentwp and wpnumber>self.currentwp then + self:_PassedFinalWaypoint(false, string.format("_AddWaypoint: wpnumber/index %d>%d self.currentwp", wpnumber, self.currentwp)) end + end --- Initialize Mission Editor waypoints. -- @param #OPSGROUP self +-- @param #number WpIndexMin +-- @param #number WpIndexMax -- @return #OPSGROUP self -function OPSGROUP:InitWaypoints() +function OPSGROUP:_InitWaypoints(WpIndexMin, WpIndexMax) -- Template waypoints. - self.waypoints0=self.group:GetTemplateRoutePoints() + self.waypoints0=UTILS.DeepCopy(_DATABASE:GetGroupTemplate(self.groupname).route.points) --self.group:GetTemplateRoutePoints() - -- Waypoints + -- Waypoints empty! self.waypoints={} - - for index,wp in pairs(self.waypoints0) do - -- Coordinate of the waypoint. - local coordinate=COORDINATE:New(wp.x, wp.alt, wp.y) - + WpIndexMin=WpIndexMin or 1 + WpIndexMax=WpIndexMax or #self.waypoints0 + WpIndexMax=math.min(WpIndexMax, #self.waypoints0) --Ensure max is not out of bounce. + + --for index,wp in pairs(self.waypoints0) do + + for i=WpIndexMin,WpIndexMax do + + local wp=self.waypoints0[i] --DCS#Waypoint + + -- Coordinate of the waypoint. + local Coordinate=COORDINATE:NewFromWaypoint(wp) + -- Strange! wp.speed=wp.speed or 0 - + -- Speed at the waypoint. local speedknots=UTILS.MpsToKnots(wp.speed) - - if index==1 then + + -- Expected speed to the first waypoint. + if i<=2 then self.speedWp=wp.speed end - + + -- Speed in knots. + local Speed=UTILS.MpsToKnots(wp.speed) + -- Add waypoint. - self:AddWaypoint(coordinate, speedknots, index-1, nil, false) - + local Waypoint=nil + if self:IsFlightgroup() then + Waypoint=FLIGHTGROUP.AddWaypoint(self, Coordinate, Speed, nil, Altitude, false) + elseif self:IsArmygroup() then + Waypoint=ARMYGROUP.AddWaypoint(self, Coordinate, Speed, nil, wp.action, false) + elseif self:IsNavygroup() then + Waypoint=NAVYGROUP.AddWaypoint(self, Coordinate, Speed, nil, Depth, false) + end + + -- Get DCS waypoint tasks set in the ME. EXPERIMENTAL! + local DCStasks=wp.task and wp.task.params.tasks or nil + if DCStasks then + for _,DCStask in pairs(DCStasks) do + -- Wrapped Actions are commands. We do not take those. + if DCStask.id and DCStask.id~="WrappedAction" then + self:AddTaskWaypoint(DCStask,Waypoint, "ME Task") + end + end + end + end - + -- Debug info. self:T(self.lid..string.format("Initializing %d waypoints", #self.waypoints)) - + + -- Flight group specific. + if self:IsFlightgroup() then + + -- Get home and destination airbases from waypoints. + self.homebase=self.homebase or self:GetHomebaseFromWaypoints() + local destbase=self:GetDestinationFromWaypoints() + self.destbase=self.destbase or destbase + self.currbase=self:GetHomebaseFromWaypoints() + + --env.info("FF home base "..(self.homebase and self.homebase:GetName() or "unknown")) + --env.info("FF dest base "..(self.destbase and self.destbase:GetName() or "unknown")) + + -- Remove the landing waypoint. We use RTB for that. It makes adding new waypoints easier as we do not have to check if the last waypoint is the landing waypoint. + if destbase and #self.waypoints>1 then + table.remove(self.waypoints, #self.waypoints) + end + + -- Set destination to homebase. + if self.destbase==nil then + self.destbase=self.homebase + end + + end + -- Update route. if #self.waypoints>0 then - + -- Check if only 1 wp? if #self.waypoints==1 then - self.passedfinalwp=true + self:_PassedFinalWaypoint(true, "_InitWaypoints: #self.waypoints==1") end - + + else + self:E(self.lid.."WARNING: No waypoints initialized. Number of waypoints is 0!") end return self @@ -4477,29 +9127,27 @@ function OPSGROUP:Route(waypoints, delay) else if self:IsAlive() then - - -- DCS task combo. - local Tasks={} - - -- Route (Mission) task. - local TaskRoute=self.group:TaskRoute(waypoints) - table.insert(Tasks, TaskRoute) - - -- TaskCombo of enroute and mission tasks. - local TaskCombo=self.group:TaskCombo(Tasks) - - -- Set tasks. - if #Tasks>1 then - self:SetTask(TaskCombo) - else - self:SetTask(TaskRoute) - end - + + -- Clear all DCS tasks. NOTE: This can make DCS crash! + --self:ClearTasks() + + -- DCS mission task. + local DCSTask = { + id = 'Mission', + params = { + airborne = self:IsFlightgroup(), + route={points=waypoints}, + }, + } + + -- Set mission task. + self:SetTask(DCSTask) + else self:E(self.lid.."ERROR: Group is not alive! Cannot route group.") end end - + return self end @@ -4514,25 +9162,25 @@ function OPSGROUP:_UpdateWaypointTasks(n) local nwaypoints=#waypoints for i,_wp in pairs(waypoints) do - local wp=_wp --Ops.OpsGroup#OPSGROUP.Waypoint - + local wp=_wp --Ops.OpsGroup#OPSGROUP.Waypoint + if i>=n or nwaypoints==1 then - + -- Debug info. self:T2(self.lid..string.format("Updating waypoint task for waypoint %d/%d ID=%d. Last waypoint passed %d", i, nwaypoints, wp.uid, self.currentwp)) - + -- Tasks of this waypoint local taskswp={} - + -- At each waypoint report passing. local TaskPassingWaypoint=self.group:TaskFunction("OPSGROUP._PassingWaypoint", self, wp.uid) - table.insert(taskswp, TaskPassingWaypoint) - + table.insert(taskswp, TaskPassingWaypoint) + -- Waypoint task combo. wp.task=self.group:TaskCombo(taskswp) - + end - + end end @@ -4542,93 +9190,224 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Function called when a group is passing a waypoint. ---@param Wrapper.Group#GROUP group Group that passed the waypoint. --@param #OPSGROUP opsgroup Ops group object. --@param #number uid Waypoint UID. -function OPSGROUP._PassingWaypoint(group, opsgroup, uid) - +function OPSGROUP._PassingWaypoint(opsgroup, uid) + + -- Debug message. + local text=string.format("Group passing waypoint uid=%d", uid) + opsgroup:T(opsgroup.lid..text) + -- Get waypoint data. local waypoint=opsgroup:GetWaypointByID(uid) - + if waypoint then - + + -- Increase passing counter. + waypoint.npassed=waypoint.npassed+1 + -- Current wp. local currentwp=opsgroup.currentwp - + -- Get the current waypoint index. opsgroup.currentwp=opsgroup:GetWaypointIndex(uid) - - -- Set expected speed and formation from the next WP. - local wpnext=opsgroup:GetWaypointNext() - if wpnext then - + + local wpistemp=waypoint.temp or waypoint.detour or waypoint.astar + + -- Remove temp waypoints. + if wpistemp then + opsgroup:RemoveWaypointByID(uid) + end + + -- Get next waypoint. Tricky part is that if + local wpnext=opsgroup:GetWaypointNext() + + if wpnext then --and (opsgroup.currentwp<#opsgroup.waypoints or opsgroup.adinfinitum or wpistemp) + + -- Debug info. + opsgroup:T(opsgroup.lid..string.format("Next waypoint UID=%d index=%d", wpnext.uid, opsgroup:GetWaypointIndex(wpnext.uid))) + -- Set formation. - if opsgroup.isGround then + if opsgroup.isArmygroup then opsgroup.formation=wpnext.action end - - -- Set speed. + + -- Set speed to next wp. opsgroup.speed=wpnext.speed - + + if opsgroup.speed<0.01 then + opsgroup.speed=UTILS.KmphToMps(opsgroup.speedCruise) + end + + else + + -- Set passed final waypoint. + opsgroup:_PassedFinalWaypoint(true, "_PassingWaypoint No next Waypoint found") + end - - -- Debug message. - local text=string.format("Group passing waypoint uid=%d", uid) - opsgroup:T(opsgroup.lid..text) - + + -- Check if final waypoint was reached. + if opsgroup.currentwp==#opsgroup.waypoints and not (opsgroup.adinfinitum or wpistemp) then + -- Set passed final waypoint. + opsgroup:_PassedFinalWaypoint(true, "_PassingWaypoint currentwp==#waypoints and NOT adinfinitum and NOT a temporary waypoint") + end + -- Trigger PassingWaypoint event. - if waypoint.astar then - - -- Remove Astar waypoint. - opsgroup:RemoveWaypointByID(uid) - + if waypoint.temp then + + --- + -- Temporary Waypoint + --- + + if (opsgroup:IsNavygroup() or opsgroup:IsArmygroup()) and opsgroup.currentwp==#opsgroup.waypoints then + --TODO: not sure if this works with FLIGHTGROUPS + + -- Removing this for now. + opsgroup:Cruise() + end + + elseif waypoint.astar then + + --- + -- Pathfinding Waypoint + --- + -- Cruise. opsgroup:Cruise() - + elseif waypoint.detour then - - -- Remove detour waypoint. - opsgroup:RemoveWaypointByID(uid) - + + --- + -- Detour Waypoint + --- + if opsgroup:IsRearming() then - + -- Trigger Rearming event. opsgroup:Rearming() - + elseif opsgroup:IsRetreating() then - + -- Trigger Retreated event. opsgroup:Retreated() - + + elseif opsgroup:IsReturning() then + + -- Trigger Returned event. + opsgroup:Returned() + + elseif opsgroup:IsPickingup() then + + if opsgroup:IsFlightgroup() then + + -- Land at current pos and wait for 60 min max. + if opsgroup.cargoTZC then + + if opsgroup.cargoTZC.PickupAirbase then + -- Pickup airbase specified. Land there. + --env.info(opsgroup.lid.."FF Land at Pickup Airbase") + opsgroup:LandAtAirbase(opsgroup.cargoTZC.PickupAirbase) + else + -- Land somewhere in the pickup zone. Only helos can do that. + local coordinate=opsgroup.cargoTZC.PickupZone:GetRandomCoordinate(nil, nil, {land.SurfaceType.LAND}) + opsgroup:LandAt(coordinate, 60*60) + end + + else + local coordinate=opsgroup:GetCoordinate() + opsgroup:LandAt(coordinate, 60*60) + end + + + else + + -- Wait and load cargo. + opsgroup:FullStop() + opsgroup:__Loading(-5) + + end + + elseif opsgroup:IsTransporting() then + + if opsgroup:IsFlightgroup() then + + -- Land at current pos and wait for 60 min max. + if opsgroup.cargoTZC then + + if opsgroup.cargoTZC.DeployAirbase then + -- Pickup airbase specified. Land there. + --env.info(opsgroup.lid.."FF Land at Deploy Airbase") + opsgroup:LandAtAirbase(opsgroup.cargoTZC.DeployAirbase) + else + -- Land somewhere in the pickup zone. Only helos can do that. + local coordinate=opsgroup.cargoTZC.DeployZone:GetRandomCoordinate(nil, nil, {land.SurfaceType.LAND}) + opsgroup:LandAt(coordinate, 60*60) + end + + else + local coordinate=opsgroup:GetCoordinate() + opsgroup:LandAt(coordinate, 60*60) + end + + + else + -- Stop and unload. + opsgroup:FullStop() + opsgroup:Unloading() + end + + elseif opsgroup:IsBoarding() then + + local carrierGroup=opsgroup:_GetMyCarrierGroup() + local carrier=opsgroup:_GetMyCarrierElement() + + if carrierGroup and carrierGroup:IsAlive() then + + if carrier and carrier.unit and carrier.unit:IsAlive() then + + -- Load group into the carrier. + carrierGroup:Load(opsgroup) + + else + opsgroup:E(opsgroup.lid.."ERROR: Group cannot board assigned carrier UNIT as it is NOT alive!") + end + + else + opsgroup:E(opsgroup.lid.."ERROR: Group cannot board assigned carrier GROUP as it is NOT alive!") + end + elseif opsgroup:IsEngaging() then - + -- Nothing to do really. - + opsgroup:T(opsgroup.lid.."Passing engaging waypoint") + else - + -- Trigger DetourReached event. opsgroup:DetourReached() - + if waypoint.detour==0 then opsgroup:FullStop() elseif waypoint.detour==1 then opsgroup:Cruise() else opsgroup:E("ERROR: waypoint.detour should be 0 or 1") + opsgroup:FullStop() end - + end - + else + --- + -- Normal Route Waypoint + --- + -- Check if the group is still pathfinding. if opsgroup.ispathfinding then opsgroup.ispathfinding=false - end + end - -- Increase passing counter. - waypoint.npassed=waypoint.npassed+1 - -- Call event function. opsgroup:PassingWaypoint(waypoint) end @@ -4661,7 +9440,7 @@ function OPSGROUP._TaskDone(group, opsgroup, task) -- Debug message. local text=string.format("_TaskDone %s", task.description) - opsgroup:T3(opsgroup.lid..text) + opsgroup:T(opsgroup.lid..text) -- Set current task to nil so that the next in line can be executed. if opsgroup then @@ -4687,25 +9466,25 @@ end -- @param #string roe ROE of group. Default is value set in `SetDefaultROE` (usually `ENUMS.ROE.ReturnFire`). -- @return #OPSGROUP self function OPSGROUP:SwitchROE(roe) - + if self:IsAlive() or self:IsInUtero() then self.option.ROE=roe or self.optionDefault.ROE - + if self:IsInUtero() then self:T2(self.lid..string.format("Setting current ROE=%d when GROUP is SPAWNED", self.option.ROE)) else self.group:OptionROE(self.option.ROE) - + self:T(self.lid..string.format("Setting current ROE=%d (%s)", self.option.ROE, self:_GetROEName(self.option.ROE))) end - - + + else self:E(self.lid.."WARNING: Cannot switch ROE! Group is not alive") end - + return self end @@ -4749,25 +9528,30 @@ end -- @param #string rot ROT of group. Default is value set in `:SetDefaultROT` (usually `ENUMS.ROT.PassiveDefense`). -- @return #OPSGROUP self function OPSGROUP:SwitchROT(rot) - - if self:IsAlive() or self:IsInUtero() then - - self.option.ROT=rot or self.optionDefault.ROT - - if self:IsInUtero() then - self:T2(self.lid..string.format("Setting current ROT=%d when GROUP is SPAWNED", self.option.ROT)) - else - - self.group:OptionROT(self.option.ROT) - - self:T(self.lid..string.format("Setting current ROT=%d (0=NoReaction, 1=Passive, 2=Evade, 3=ByPass, 4=AllowAbort)", self.option.ROT)) - end - - else - self:E(self.lid.."WARNING: Cannot switch ROT! Group is not alive") + if self:IsFlightgroup() then + + if self:IsAlive() or self:IsInUtero() then + + self.option.ROT=rot or self.optionDefault.ROT + + if self:IsInUtero() then + self:T2(self.lid..string.format("Setting current ROT=%d when GROUP is SPAWNED", self.option.ROT)) + else + + self.group:OptionROT(self.option.ROT) + + -- Debug info. + self:T(self.lid..string.format("Setting current ROT=%d (0=NoReaction, 1=Passive, 2=Evade, 3=ByPass, 4=AllowAbort)", self.option.ROT)) + end + + + else + self:E(self.lid.."WARNING: Cannot switch ROT! Group is not alive") + end + end - + return self end @@ -4789,26 +9573,26 @@ function OPSGROUP:SetDefaultAlarmstate(alarmstate) end --- Set current Alarm State of the group. --- +-- -- * 0 = "Auto" -- * 1 = "Green" -- * 2 = "Red" --- +-- -- @param #OPSGROUP self -- @param #number alarmstate Alarm state of group. Default is 0="Auto". -- @return #OPSGROUP self function OPSGROUP:SwitchAlarmstate(alarmstate) - + if self:IsAlive() or self:IsInUtero() then - + if self.isArmygroup or self.isNavygroup then - + self.option.Alarm=alarmstate or self.optionDefault.Alarm - + if self:IsInUtero() then self:T2(self.lid..string.format("Setting current Alarm State=%d when GROUP is SPAWNED", self.option.Alarm)) else - + if self.option.Alarm==0 then self.group:OptionAlarmStateAuto() elseif self.option.Alarm==1 then @@ -4820,16 +9604,16 @@ function OPSGROUP:SwitchAlarmstate(alarmstate) self.group:OptionAlarmStateAuto() self.option.Alarm=0 end - + self:T(self.lid..string.format("Setting current Alarm State=%d (0=Auto, 1=Green, 2=Red)", self.option.Alarm)) - + end - + end else self:E(self.lid.."WARNING: Cannot switch Alarm State! Group is not alive.") end - + return self end @@ -4840,6 +9624,65 @@ function OPSGROUP:GetAlarmstate() return self.option.Alarm or self.optionDefault.Alarm end +--- Set the default Alarm State for the group. This is the state gets when the group is spawned or to which it defaults back after a mission. +-- @param #OPSGROUP self +-- @param #boolean OnOffSwitch If `true`, EPLRS is on by default. If `false` default EPLRS setting is off. If `nil`, default is on if group has EPLRS and off if it does not have a datalink. +-- @return #OPSGROUP self +function OPSGROUP:SetDefaultEPLRS(OnOffSwitch) + + if OnOffSwitch==nil then + self.optionDefault.EPLRS=self.isEPLRS + else + self.optionDefault.EPLRS=OnOffSwitch + end + + return self +end + +--- Switch EPLRS datalink on or off. +-- @param #OPSGROUP self +-- @param #boolean OnOffSwitch If `true` or `nil`, switch EPLRS on. If `false` EPLRS switched off. +-- @return #OPSGROUP self +function OPSGROUP:SwitchEPLRS(OnOffSwitch) + + if self:IsAlive() or self:IsInUtero() then + + if OnOffSwitch==nil then + + self.option.EPLRS=self.optionDefault.EPLRS + + else + + self.option.EPLRS=OnOffSwitch + + end + + if self:IsInUtero() then + self:T2(self.lid..string.format("Setting current EPLRS=%s when GROUP is SPAWNED", tostring(self.option.EPLRS))) + else + + self.group:CommandEPLRS(self.option.EPLRS) + self:T(self.lid..string.format("Setting current EPLRS=%s", tostring(self.option.EPLRS))) + + end + else + self:E(self.lid.."WARNING: Cannot switch Alarm State! Group is not alive") + end + + return self +end + +--- Get current EPLRS state. +-- @param #OPSGROUP self +-- @return #boolean If `true`, EPLRS is on. +function OPSGROUP:GetEPLRS() + return self.option.EPLRS or self.optionDefault.EPLRS +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- SETTINGS FUNCTIONS +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + --- Set default TACAN parameters. -- @param #OPSGROUP self -- @param #number Channel TACAN channel. Default is 74. @@ -4849,20 +9692,20 @@ end -- @param #boolean OffSwitch If true, TACAN is off by default. -- @return #OPSGROUP self function OPSGROUP:SetDefaultTACAN(Channel, Morse, UnitName, Band, OffSwitch) - + self.tacanDefault={} self.tacanDefault.Channel=Channel or 74 self.tacanDefault.Morse=Morse or "XXX" self.tacanDefault.BeaconName=UnitName - if self.isAircraft then + if self:IsFlightgroup() then Band=Band or "Y" else Band=Band or "X" end - self.tacanDefault.Band=Band - - + self.tacanDefault.Band=Band + + if OffSwitch then self.tacanDefault.On=false else @@ -4880,19 +9723,19 @@ end function OPSGROUP:_SwitchTACAN(Tacan) if Tacan then - + self:SwitchTACAN(Tacan.Channel, Tacan.Morse, Tacan.BeaconName, Tacan.Band) - + else - + if self.tacanDefault.On then self:SwitchTACAN() else self:TurnOffTACAN() end - + end - + end --- Activate/switch TACAN beacon settings. @@ -4905,12 +9748,12 @@ end function OPSGROUP:SwitchTACAN(Channel, Morse, UnitName, Band) if self:IsInUtero() then - + self:T(self.lid..string.format("Switching TACAN to DEFAULT when group is spawned")) self:SetDefaultTACAN(Channel, Morse, UnitName, Band) elseif self:IsAlive() then - + Channel=Channel or self.tacanDefault.Channel Morse=Morse or self.tacanDefault.Morse Band=Band or self.tacanDefault.Band @@ -4929,7 +9772,7 @@ function OPSGROUP:SwitchTACAN(Channel, Morse, UnitName, Band) self:T(self.lid.."WARNING: Could not get TACAN unit. Trying first unit in the group") unit=self:GetUnit(1) end - + if unit and unit:IsAlive() then -- Unit ID. @@ -4937,16 +9780,16 @@ function OPSGROUP:SwitchTACAN(Channel, Morse, UnitName, Band) -- Type local Type=BEACON.Type.TACAN - + -- System - local System=BEACON.System.TACAN - if self.isAircraft then + local System=BEACON.System.TACAN + if self:IsFlightgroup() then System=BEACON.System.TACAN_TANKER_Y end - + -- Tacan frequency. local Frequency=UTILS.TACANToFrequency(Channel, Band) - + -- Activate beacon. unit:CommandActivateBeacon(Type, System, Frequency, UnitID, Channel, Band, true, Morse, true) @@ -4957,10 +9800,10 @@ function OPSGROUP:SwitchTACAN(Channel, Morse, UnitName, Band) self.tacan.BeaconName=unit:GetName() self.tacan.BeaconUnit=unit self.tacan.On=true - - -- Debug info. + + -- Debug info. self:T(self.lid..string.format("Switching TACAN to Channel %d%s Morse %s on unit %s", self.tacan.Channel, self.tacan.Band, tostring(self.tacan.Morse), self.tacan.BeaconName)) - + else self:E(self.lid.."ERROR: Cound not set TACAN! Unit is not alive") end @@ -4997,6 +9840,12 @@ function OPSGROUP:GetTACAN() return self.tacan.Channel, self.tacan.Morse, self.tacan.Band, self.tacan.On, self.tacan.BeaconName end +--- Get current TACAN parameters. +-- @param #OPSGROUP self +-- @return #OPSGROUP.Beacon TACAN beacon. +function OPSGROUP:GetBeaconTACAN() + return self.tacan +end --- Set default ICLS parameters. @@ -5007,12 +9856,12 @@ end -- @param #boolean OffSwitch If true, TACAN is off by default. -- @return #OPSGROUP self function OPSGROUP:SetDefaultICLS(Channel, Morse, UnitName, OffSwitch) - + self.iclsDefault={} self.iclsDefault.Channel=Channel or 1 self.iclsDefault.Morse=Morse or "XXX" self.iclsDefault.BeaconName=UnitName - + if OffSwitch then self.iclsDefault.On=false else @@ -5030,17 +9879,17 @@ end function OPSGROUP:_SwitchICLS(Icls) if Icls then - + self:SwitchICLS(Icls.Channel, Icls.Morse, Icls.BeaconName) - + else - + if self.iclsDefault.On then self:SwitchICLS() else self:TurnOffICLS() end - + end end @@ -5054,17 +9903,17 @@ end function OPSGROUP:SwitchICLS(Channel, Morse, UnitName) if self:IsInUtero() then - + self:SetDefaultICLS(Channel,Morse,UnitName) - + self:T2(self.lid..string.format("Switching ICLS to Channel %d Morse %s on unit %s when GROUP is SPAWNED", self.iclsDefault.Channel, tostring(self.iclsDefault.Morse), tostring(self.iclsDefault.BeaconName))) elseif self:IsAlive() then - + Channel=Channel or self.iclsDefault.Channel Morse=Morse or self.iclsDefault.Morse local unit=self:GetUnit(1) --Wrapper.Unit#UNIT - + if UnitName then if type(UnitName)=="number" then unit=self:GetUnit(UnitName) @@ -5072,7 +9921,7 @@ function OPSGROUP:SwitchICLS(Channel, Morse, UnitName) unit=UNIT:FindByName(UnitName) end end - + if not unit then self:T(self.lid.."WARNING: Could not get ICLS unit. Trying first unit in the group") unit=self:GetUnit(1) @@ -5081,11 +9930,11 @@ function OPSGROUP:SwitchICLS(Channel, Morse, UnitName) if unit and unit:IsAlive() then -- Unit ID. - local UnitID=unit:GetID() + local UnitID=unit:GetID() -- Activate beacon. unit:CommandActivateICLS(Channel, UnitID, Morse) - + -- Update info. self.icls.Channel=Channel self.icls.Morse=Morse @@ -5093,10 +9942,10 @@ function OPSGROUP:SwitchICLS(Channel, Morse, UnitName) self.icls.BeaconName=unit:GetName() self.icls.BeaconUnit=unit self.icls.On=true - + -- Debug info. self:T(self.lid..string.format("Switching ICLS to Channel %d Morse %s on unit %s", self.icls.Channel, tostring(self.icls.Morse), self.icls.BeaconName)) - + else self:E(self.lid.."ERROR: Cound not set ICLS! Unit is not alive.") end @@ -5128,7 +9977,7 @@ end -- @param #boolean OffSwitch If true, radio is OFF by default. -- @return #OPSGROUP self function OPSGROUP:SetDefaultRadio(Frequency, Modulation, OffSwitch) - + self.radioDefault={} self.radioDefault.Freq=Frequency or 251 self.radioDefault.Modu=Modulation or radio.modulation.AM @@ -5137,7 +9986,7 @@ function OPSGROUP:SetDefaultRadio(Frequency, Modulation, OffSwitch) else self.radioDefault.On=true end - + return self end @@ -5158,33 +10007,34 @@ end function OPSGROUP:SwitchRadio(Frequency, Modulation) if self:IsInUtero() then - + -- Set default radio. self:SetDefaultRadio(Frequency, Modulation) - + -- Debug info. self:T2(self.lid..string.format("Switching radio to frequency %.3f MHz %s when GROUP is SPAWNED", self.radioDefault.Freq, UTILS.GetModulationName(self.radioDefault.Modu))) - + elseif self:IsAlive() then - + Frequency=Frequency or self.radioDefault.Freq Modulation=Modulation or self.radioDefault.Modu - if self.isAircraft and not self.radio.On then + if self:IsFlightgroup() and not self.radio.On then + --env.info("FF radio OFF") self.group:SetOption(AI.Option.Air.id.SILENCE, false) - end - + end + -- Give command self.group:CommandSetFrequency(Frequency, Modulation) - + -- Update current settings. self.radio.Freq=Frequency - self.radio.Modu=Modulation + self.radio.Modu=Modulation self.radio.On=true - + -- Debug info. self:T(self.lid..string.format("Switching radio to frequency %.3f MHz %s", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu))) - + else self:E(self.lid.."ERROR: Cound not set Radio! Group is not alive or not in utero any more") end @@ -5199,14 +10049,14 @@ function OPSGROUP:TurnOffRadio() if self:IsAlive() then - if self.isAircraft then - + if self:IsFlightgroup() then + -- Set group to be silient. self.group:SetOption(AI.Option.Air.id.SILENCE, true) - + -- Radio is off. self.radio.On=false - + self:T(self.lid..string.format("Switching radio OFF")) else self:E(self.lid.."ERROR: Radio can only be turned off for aircraft!") @@ -5224,7 +10074,7 @@ end -- @param #number Formation The formation the groups flies in. -- @return #OPSGROUP self function OPSGROUP:SetDefaultFormation(Formation) - + self.optionDefault.Formation=Formation return self @@ -5237,25 +10087,25 @@ end function OPSGROUP:SwitchFormation(Formation) if self:IsAlive() then - + Formation=Formation or self.optionDefault.Formation - - if self.isAircraft then + + if self:IsFlightgroup() then self.group:SetOption(AI.Option.Air.id.FORMATION, Formation) - - elseif self.isGround then - + + elseif self.isArmygroup then + -- Polymorphic and overwritten in ARMYGROUP. - + else self:E(self.lid.."ERROR: Formation can only be set for aircraft or ground units!") return self end - + -- Set current formation. self.option.Formation=Formation - + -- Debug info. self:T(self.lid..string.format("Switching formation to %d", self.option.Formation)) @@ -5269,13 +10119,14 @@ end --- Set default callsign. -- @param #OPSGROUP self -- @param #number CallsignName Callsign name. --- @param #number CallsignNumber Callsign number. +-- @param #number CallsignNumber Callsign number. Default 1. -- @return #OPSGROUP self function OPSGROUP:SetDefaultCallsign(CallsignName, CallsignNumber) - self.callsignDefault={} + self.callsignDefault={} --#OPSGROUP.Callsign self.callsignDefault.NumberSquad=CallsignName self.callsignDefault.NumberGroup=CallsignNumber or 1 + self.callsignDefault.NameSquad=UTILS.GetCallsignName(self.callsign.NumberSquad) return self end @@ -5288,9 +10139,10 @@ end function OPSGROUP:SwitchCallsign(CallsignName, CallsignNumber) if self:IsInUtero() then - + -- Set default callsign. We switch to this when group is spawned. self:SetDefaultCallsign(CallsignName, CallsignNumber) + --self.callsign=UTILS.DeepCopy(self.callsignDefault) elseif self:IsAlive() then @@ -5303,12 +10155,24 @@ function OPSGROUP:SwitchCallsign(CallsignName, CallsignNumber) -- Debug. self:T(self.lid..string.format("Switching callsign to %d-%d", self.callsign.NumberSquad, self.callsign.NumberGroup)) - + -- Give command to change the callsign. self.group:CommandSetCallsign(self.callsign.NumberSquad, self.callsign.NumberGroup) + -- Callsign of the group, e.g. Colt-1 + self.callsignName=UTILS.GetCallsignName(self.callsign.NumberSquad).."-"..self.callsign.NumberGroup + self.callsign.NameSquad=UTILS.GetCallsignName(self.callsign.NumberSquad) + + -- Set callsign of elements. + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + if element.status~=OPSGROUP.ElementStatus.DEAD then + element.callsign=element.unit:GetCallsign() + end + end + else - --TODO: Error + self:E(self.lid.."ERROR: Group is not alive and not in utero! Cannot switch callsign") end return self @@ -5323,38 +10187,40 @@ end -- @return #OPSGROUP self function OPSGROUP:_UpdatePosition() - if self:IsAlive() then - + if self:IsExist() then + -- Backup last state to monitor differences. self.positionLast=self.position or self:GetVec3() self.headingLast=self.heading or self:GetHeading() self.orientXLast=self.orientX or self:GetOrientationX() self.velocityLast=self.velocity or self.group:GetVelocityMPS() - + -- Current state. self.position=self:GetVec3() self.heading=self:GetHeading() self.orientX=self:GetOrientationX() self.velocity=self:GetVelocity() - + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + element.vec3=self:GetVec3(element.name) + end + -- Update time. local Tnow=timer.getTime() self.dTpositionUpdate=self.TpositionUpdate and Tnow-self.TpositionUpdate or 0 self.TpositionUpdate=Tnow - + if not self.traveldist then self.traveldist=0 end - + + -- Travel distance since last check. self.travelds=UTILS.VecNorm(UTILS.VecSubstract(self.position, self.positionLast)) - + -- 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 @@ -5400,17 +10266,25 @@ function OPSGROUP:_AllSimilarStatus(status) for _,_element in pairs(self.elements) do local element=_element --#OPSGROUP.Element - + self:T2(self.lid..string.format("Status=%s, element %s status=%s", status, element.name, element.status)) -- Dead units dont count ==> We wont return false for those. if element.status~=OPSGROUP.ElementStatus.DEAD then - + ---------- -- ALIVE ---------- - if status==OPSGROUP.ElementStatus.SPAWNED then + if status==OPSGROUP.ElementStatus.INUTERO then + + -- Element INUTERO: Check that ALL others are also INUTERO + if element.status~=status then + return false + end + + + elseif status==OPSGROUP.ElementStatus.SPAWNED then -- Element SPAWNED: Check that others are not still IN UTERO if element.status~=status and @@ -5446,7 +10320,7 @@ function OPSGROUP:_AllSimilarStatus(status) element.status==OPSGROUP.ElementStatus.PARKING or element.status==OPSGROUP.ElementStatus.ENGINEON) then return false - end + end elseif status==OPSGROUP.ElementStatus.TAKEOFF then @@ -5468,7 +10342,7 @@ function OPSGROUP:_AllSimilarStatus(status) element.status==OPSGROUP.ElementStatus.SPAWNED or element.status==OPSGROUP.ElementStatus.PARKING or element.status==OPSGROUP.ElementStatus.ENGINEON or - element.status==OPSGROUP.ElementStatus.TAXIING or + element.status==OPSGROUP.ElementStatus.TAXIING or element.status==OPSGROUP.ElementStatus.TAKEOFF) then return false end @@ -5493,7 +10367,7 @@ function OPSGROUP:_AllSimilarStatus(status) end end - + else -- Element is dead. We don't care unless all are dead. end --DEAD @@ -5502,7 +10376,7 @@ function OPSGROUP:_AllSimilarStatus(status) -- Debug info. self:T2(self.lid..string.format("All %d elements have similar status %s ==> returning TRUE", #self.elements, status)) - + return true end @@ -5518,30 +10392,39 @@ function OPSGROUP:_UpdateStatus(element, newstatus, airbase) -- Update status of element. element.status=newstatus - + -- Debug - self:T3(self.lid..string.format("UpdateStatus element=%s: %s --> %s", element.name, oldstatus, newstatus)) + self:T3(self.lid..string.format("UpdateStatus element=%s: %s --> %s", element.name, oldstatus, newstatus)) for _,_element in pairs(self.elements) do local Element=_element -- #OPSGROUP.Element self:T3(self.lid..string.format("Element %s: %s", Element.name, Element.status)) end - if newstatus==OPSGROUP.ElementStatus.SPAWNED then + if newstatus==OPSGROUP.ElementStatus.INUTERO then + --- + -- INUTERO + --- + + if self:_AllSimilarStatus(newstatus) then + self:InUtero() + end + + elseif newstatus==OPSGROUP.ElementStatus.SPAWNED then --- -- SPAWNED --- if self:_AllSimilarStatus(newstatus) then - self:__Spawned(-0.5) + self:Spawned() end - + elseif newstatus==OPSGROUP.ElementStatus.PARKING then --- -- PARKING --- if self:_AllSimilarStatus(newstatus) then - self:__Parking(-0.5) + self:Parking() end elseif newstatus==OPSGROUP.ElementStatus.ENGINEON then @@ -5557,9 +10440,9 @@ function OPSGROUP:_UpdateStatus(element, newstatus, airbase) --- if self:_AllSimilarStatus(newstatus) then - self:__Taxiing(-0.5) + self:Taxiing() end - + elseif newstatus==OPSGROUP.ElementStatus.TAKEOFF then --- -- TAKEOFF @@ -5567,7 +10450,7 @@ function OPSGROUP:_UpdateStatus(element, newstatus, airbase) if self:_AllSimilarStatus(newstatus) then -- Trigger takeoff event. Also triggers airborne event. - self:__Takeoff(-0.5, airbase) + self:Takeoff(airbase) end elseif newstatus==OPSGROUP.ElementStatus.AIRBORNE then @@ -5576,7 +10459,7 @@ function OPSGROUP:_UpdateStatus(element, newstatus, airbase) --- if self:_AllSimilarStatus(newstatus) then - self:__Airborne(-0.5) + self:Airborne() end elseif newstatus==OPSGROUP.ElementStatus.LANDED then @@ -5614,7 +10497,7 @@ function OPSGROUP:_UpdateStatus(element, newstatus, airbase) --- if self:_AllSimilarStatus(newstatus) then - self:__Dead(-1) + self:Dead() end end @@ -5640,11 +10523,15 @@ end -- @return #OPSGROUP.Element The element. function OPSGROUP:GetElementByName(unitname) - for _,_element in pairs(self.elements) do - local element=_element --#OPSGROUP.Element + if unitname and type(unitname)=="string" then + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + + if element.name==unitname then + return element + end - if element.name==unitname then - return element end end @@ -5652,6 +10539,181 @@ function OPSGROUP:GetElementByName(unitname) return nil end +--- Get the bounding box of the element. +-- @param #OPSGROUP self +-- @param #string UnitName Name of unit. +-- @return Core.Zone#ZONE_POLYGON Bounding box polygon zone. +function OPSGROUP:GetElementZoneBoundingBox(UnitName) + + local element=self:GetElementByName(UnitName) + + if element and element.status~=OPSGROUP.ElementStatus.DEAD then + + -- Create a new zone if necessary. + element.zoneBoundingbox=element.zoneBoundingbox or ZONE_POLYGON_BASE:New(element.name.." Zone Bounding Box", {}) + + -- Length in meters. + local l=element.length + -- Width in meters. + local w=element.width + + -- Orientation vector. + local X=self:GetOrientationX(element.name) + + -- Heading in degrees. + local heading=math.deg(math.atan2(X.z, X.x)) + + -- Debug info. + self:T(self.lid..string.format("Element %s bouding box: l=%d w=%d heading=%d", element.name, l, w, heading)) + + -- Set of edges facing "North" at the origin of the map. + local b={} + b[1]={x=l/2, y=-w/2} --DCS#Vec2 + b[2]={x=l/2, y=w/2} --DCS#Vec2 + b[3]={x=-l/2, y=w/2} --DCS#Vec2 + b[4]={x=-l/2, y=-w/2} --DCS#Vec2 + + -- Rotate box to match current heading of the unit. + for i,p in pairs(b) do + b[i]=UTILS.Vec2Rotate2D(p, heading) + end + + -- Translate the zone to the positon of the unit. + local vec2=self:GetVec2(element.name) + local d=UTILS.Vec2Norm(vec2) + local h=UTILS.Vec2Hdg(vec2) + for i,p in pairs(b) do + b[i]=UTILS.Vec2Translate(p, d, h) + end + + -- Update existing zone. + element.zoneBoundingbox:UpdateFromVec2(b) + + return element.zoneBoundingbox + end + + return nil +end + +--- Get the loading zone of the element. +-- @param #OPSGROUP self +-- @param #string UnitName Name of unit. +-- @return Core.Zone#ZONE_POLYGON Bounding box polygon zone. +function OPSGROUP:GetElementZoneLoad(UnitName) + + local element=self:GetElementByName(UnitName) + + if element and element.status~=OPSGROUP.ElementStatus.DEAD then + + element.zoneLoad=element.zoneLoad or ZONE_POLYGON_BASE:New(element.name.." Zone Load", {}) + + self:_GetElementZoneLoader(element, element.zoneLoad, self.carrierLoader) + + return element.zoneLoad + end + + return nil +end + +--- Get the unloading zone of the element. +-- @param #OPSGROUP self +-- @param #string UnitName Name of unit. +-- @return Core.Zone#ZONE_POLYGON Bounding box polygon zone. +function OPSGROUP:GetElementZoneUnload(UnitName) + + local element=self:GetElementByName(UnitName) + + if element and element.status~=OPSGROUP.ElementStatus.DEAD then + + element.zoneUnload=element.zoneUnload or ZONE_POLYGON_BASE:New(element.name.." Zone Unload", {}) + + self:_GetElementZoneLoader(element, element.zoneUnload, self.carrierUnloader) + + return element.zoneUnload + end + + return nil +end + +--- Get/update the (un-)loading zone of the element. +-- @param #OPSGROUP self +-- @param #OPSGROUP.Element Element Element. +-- @param Core.Zone#ZONE_POLYGON Zone The zone. +-- @param #OPSGROUP.CarrierLoader Loader Loader parameters. +-- @return Core.Zone#ZONE_POLYGON Bounding box polygon zone. +function OPSGROUP:_GetElementZoneLoader(Element, Zone, Loader) + + if Element.status~=OPSGROUP.ElementStatus.DEAD then + + local l=Element.length + local w=Element.width + + -- Orientation 3D vector where the "nose" is pointing. + local X=self:GetOrientationX(Element.name) + + -- Heading in deg. + local heading=math.deg(math.atan2(X.z, X.x)) + + -- Bounding box at the origin of the map facing "North". + local b={} + + -- Create polygon rectangles. + if Loader.type:lower()=="front" then + table.insert(b, {x= l/2, y=-Loader.width/2}) -- left, low + table.insert(b, {x= l/2+Loader.length, y=-Loader.width/2}) -- left, up + table.insert(b, {x= l/2+Loader.length, y= Loader.width/2}) -- right, up + table.insert(b, {x= l/2, y= Loader.width/2}) -- right, low + elseif Loader.type:lower()=="back" then + table.insert(b, {x=-l/2, y=-Loader.width/2}) -- left, low + table.insert(b, {x=-l/2-Loader.length, y=-Loader.width/2}) -- left, up + table.insert(b, {x=-l/2-Loader.length, y= Loader.width/2}) -- right, up + table.insert(b, {x=-l/2, y= Loader.width/2}) -- right, low + elseif Loader.type:lower()=="left" then + table.insert(b, {x= Loader.length/2, y= -w/2}) -- right, up + table.insert(b, {x= Loader.length/2, y= -w/2-Loader.width}) -- left, up + table.insert(b, {x=-Loader.length/2, y= -w/2-Loader.width}) -- left, down + table.insert(b, {x=-Loader.length/2, y= -w/2}) -- right, down + elseif Loader.type:lower()=="right" then + table.insert(b, {x= Loader.length/2, y= w/2}) -- right, up + table.insert(b, {x= Loader.length/2, y= w/2+Loader.width}) -- left, up + table.insert(b, {x=-Loader.length/2, y= w/2+Loader.width}) -- left, down + table.insert(b, {x=-Loader.length/2, y= w/2}) -- right, down + else + -- All aspect. Rectangle around the unit but need to cut out the area of the unit itself. + b[1]={x= l/2, y=-w/2} --DCS#Vec2 + b[2]={x= l/2, y= w/2} --DCS#Vec2 + b[3]={x=-l/2, y= w/2} --DCS#Vec2 + b[4]={x=-l/2, y=-w/2} --DCS#Vec2 + table.insert(b, {x=b[1].x+Loader.length, y=b[1].y-Loader.width}) + table.insert(b, {x=b[2].x+Loader.length, y=b[2].y+Loader.width}) + table.insert(b, {x=b[3].x-Loader.length, y=b[3].y+Loader.width}) + table.insert(b, {x=b[4].x-Loader.length, y=b[4].y-Loader.width}) + end + + -- Rotate edges to match the current heading of the unit. + for i,p in pairs(b) do + b[i]=UTILS.Vec2Rotate2D(p, heading) + end + + -- Translate box to the current position of the unit. + local vec2=self:GetVec2(Element.name) + local d=UTILS.Vec2Norm(vec2) + local h=UTILS.Vec2Hdg(vec2) + + for i,p in pairs(b) do + b[i]=UTILS.Vec2Translate(p, d, h) + end + + -- Update existing zone. + Zone:UpdateFromVec2(b) + + return Zone + end + + return nil +end + + --- Get the first element of a group, which is alive. -- @param #OPSGROUP self -- @return #OPSGROUP.Element The element or `#nil` if no element is alive any more. @@ -5661,7 +10723,7 @@ function OPSGROUP:GetElementAlive() local element=_element --#OPSGROUP.Element if element.status~=OPSGROUP.ElementStatus.DEAD then if element.unit and element.unit:IsAlive() then - return element + return element end end end @@ -5669,6 +10731,7 @@ function OPSGROUP:GetElementAlive() return nil end + --- Get number of elements alive. -- @param #OPSGROUP self -- @param #string status (Optional) Only count number, which are in a special status. @@ -5687,7 +10750,7 @@ function OPSGROUP:GetNelements(status) end end - + return n end @@ -5705,7 +10768,7 @@ end function OPSGROUP:GetAmmoTot() local units=self.group:GetUnits() - + local Ammo={} --#OPSGROUP.Ammo Ammo.Total=0 Ammo.Guns=0 @@ -5718,15 +10781,15 @@ function OPSGROUP:GetAmmoTot() Ammo.MissilesAS=0 Ammo.MissilesCR=0 Ammo.MissilesSA=0 - + for _,_unit in pairs(units) do local unit=_unit --Wrapper.Unit#UNIT - + if unit and unit:IsAlive()~=nil then - + -- Get ammo of the unit. local ammo=self:GetAmmoUnit(unit) - + -- Add up total. Ammo.Total=Ammo.Total+ammo.Total Ammo.Guns=Ammo.Guns+ammo.Guns @@ -5739,9 +10802,9 @@ function OPSGROUP:GetAmmoTot() Ammo.MissilesAS=Ammo.MissilesAS+ammo.MissilesAS Ammo.MissilesCR=Ammo.MissilesCR+ammo.MissilesCR Ammo.MissilesSA=Ammo.MissilesSA+ammo.MissilesSA - + end - + end return Ammo @@ -5837,16 +10900,16 @@ function OPSGROUP:GetAmmoUnit(unit, display) nmissilesAA=nmissilesAA+Nammo elseif MissileCategory==Weapon.MissileCategory.SAM then nmissiles=nmissiles+Nammo - nmissilesSA=nmissilesSA+Nammo + nmissilesSA=nmissilesSA+Nammo elseif MissileCategory==Weapon.MissileCategory.ANTI_SHIP then nmissiles=nmissiles+Nammo nmissilesAS=nmissilesAS+Nammo elseif MissileCategory==Weapon.MissileCategory.BM then nmissiles=nmissiles+Nammo - nmissilesAG=nmissilesAG+Nammo + nmissilesBM=nmissilesBM+Nammo elseif MissileCategory==Weapon.MissileCategory.CRUISE then nmissiles=nmissiles+Nammo - nmissilesCR=nmissilesCR+Nammo + nmissilesCR=nmissilesCR+Nammo elseif MissileCategory==Weapon.MissileCategory.OTHER then nmissiles=nmissiles+Nammo nmissilesAG=nmissilesAG+Nammo @@ -5854,11 +10917,11 @@ function OPSGROUP:GetAmmoUnit(unit, display) -- Debug info. text=text..string.format("- %d %s missiles of type %s\n", Nammo, self:_MissileCategoryName(MissileCategory), _weaponName) - + elseif Category==Weapon.Category.TORPEDO then - + -- Add up all rockets. - ntorps=ntorps+Nammo + ntorps=ntorps+Nammo -- Debug info. text=text..string.format("- %d torpedos of type %s\n", Nammo, _weaponName) @@ -5922,26 +10985,367 @@ function OPSGROUP:_MissileCategoryName(categorynumber) return cat end +--- Set passed final waypoint value. +-- @param #OPSGROUP self +-- @param #boolean final If `true`, final waypoint was passed. +-- @param #string comment Some comment as to why the final waypoint was passed. +function OPSGROUP:_PassedFinalWaypoint(final, comment) + + -- Debug info. + self:T(self.lid..string.format("Passed final waypoint=%s [from %s]: comment \"%s\"", tostring(final), tostring(self.passedfinalwp), tostring(comment))) + + if final==true and not self.passedfinalwp then + self:PassedFinalWaypoint() + end + + -- Set value. + self.passedfinalwp=final +end + + --- Get coordinate from an object. -- @param #OPSGROUP self -- @param Wrapper.Object#OBJECT Object The object. -- @return Core.Point#COORDINATE The coordinate of the object. function OPSGROUP:_CoordinateFromObject(Object) - - if Object:IsInstanceOf("COORDINATE") then - return Object - else - if Object:IsInstanceOf("POSITIONABLE") or Object:IsInstanceOf("ZONE_BASE") then - self:T(self.lid.."WARNING: Coordinate is not a COORDINATE but a POSITIONABLE or ZONE. Trying to get coordinate") - return Object:GetCoordinate() + + if Object then + if Object:IsInstanceOf("COORDINATE") then + return Object else - self:E(self.lid.."ERROR: Coordinate is neither a COORDINATE nor any POSITIONABLE or ZONE!") + if Object:IsInstanceOf("POSITIONABLE") or Object:IsInstanceOf("ZONE_BASE") then + self:T(self.lid.."WARNING: Coordinate is not a COORDINATE but a POSITIONABLE or ZONE. Trying to get coordinate") + local coord=Object:GetCoordinate() + return coord + else + self:E(self.lid.."ERROR: Coordinate is neither a COORDINATE nor any POSITIONABLE or ZONE!") + end end - end + else + self:E(self.lid.."ERROR: Object passed is nil!") + end return nil end +--- Check if a unit is an element of the flightgroup. +-- @param #OPSGROUP self +-- @param #string unitname Name of unit. +-- @return #boolean If true, unit is element of the flight group or false if otherwise. +function OPSGROUP:_IsElement(unitname) + + for _,_element in pairs(self.elements) do + local element=_element --Ops.OpsGroup#OPSGROUP.Element + + if element.name==unitname then + return true + end + + end + + return false +end + +--- Count elements of the group. +-- @param #OPSGROUP self +-- @param #table States (Optional) Only count elements in specific states. Can also be a single state passed as #string. +-- @return #number Number of elements. +function OPSGROUP:CountElements(States) + + if States then + if type(States)=="string" then + States={States} + end + else + States=OPSGROUP.ElementStatus + end + + local IncludeDeads=true + + local N=0 + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + if element and (IncludeDeads or element.status~=OPSGROUP.ElementStatus.DEAD) then + for _,state in pairs(States) do + if element.status==state then + N=N+1 + break + end + end + end + end + + return N +end + +--- Add a unit/element to the OPS group. +-- @param #OPSGROUP self +-- @param #string unitname Name of unit. +-- @return #OPSGROUP.Element The element or nil. +function OPSGROUP:_AddElementByName(unitname) + + local unit=UNIT:FindByName(unitname) + + if unit then + + -- Get unit template. + local unittemplate=unit:GetTemplate() + --local unittemplate=_DATABASE:GetUnitTemplateFromUnitName(unitname) + + -- Element table. + local element=self:GetElementByName(unitname) + + -- Add element to table. + if element then + -- We already know this element. + else + -- Add a new element. + element={} + element.status=OPSGROUP.ElementStatus.INUTERO + table.insert(self.elements, element) + end + + -- Name and status. + element.name=unitname + + -- Unit and group. + element.unit=unit + element.DCSunit=Unit.getByName(unitname) + element.gid=element.DCSunit:getNumber() + element.uid=element.DCSunit:getID() + --element.group=unit:GetGroup() + element.opsgroup=self + + -- Skill etc. + element.skill=unittemplate.skill or "Unknown" + if element.skill=="Client" or element.skill=="Player" then + element.ai=false + element.client=CLIENT:FindByName(unitname) + else + element.ai=true + end + + -- Descriptors and type/category. + element.descriptors=unit:GetDesc() + element.category=unit:GetUnitCategory() + element.categoryname=unit:GetCategoryName() + element.typename=unit:GetTypeName() + --self:I({desc=element.descriptors}) + + -- Ammo. + element.ammo0=self:GetAmmoUnit(unit, false) + + -- Life points. + element.life=unit:GetLife() + element.life0=math.max(unit:GetLife0(), element.life) -- Some units report a life0 that is smaller than its initial life points. + + -- Size and dimensions. + element.size, element.length, element.height, element.width=unit:GetObjectSize() + + -- Weight and cargo. + element.weightEmpty=element.descriptors.massEmpty or 666 + + if self.isArmygroup then + + element.weightMaxTotal=element.weightEmpty+10*95 --If max mass is not given, we assume 10 soldiers. + + elseif self.isNavygroup then + + element.weightMaxTotal=element.weightEmpty+10*1000 + + else + + -- Looks like only aircraft have a massMax value in the descriptors. + element.weightMaxTotal=element.descriptors.massMax or element.weightEmpty+8*95 --If max mass is not given, we assume 8 soldiers. + + end + + -- Max cargo weight: + unit:SetCargoBayWeightLimit() + element.weightMaxCargo=unit.__.CargoBayWeightLimit + + -- Cargo bay (empty). + if element.cargoBay then + -- After a respawn, the cargo bay might not be empty! + element.weightCargo=self:GetWeightCargo(element.name, false) + else + element.cargoBay={} + element.weightCargo=0 + end + element.weight=element.weightEmpty+element.weightCargo + + -- FLIGHTGROUP specific. + if self.isFlightgroup then + element.callsign=element.unit:GetCallsign() + element.modex=unittemplate.onboard_num + element.payload=unittemplate.payload + element.pylons=unittemplate.payload and unittemplate.payload.pylons or nil + element.fuelmass0=unittemplate.payload and unittemplate.payload.fuel or 0 + element.fuelmass=element.fuelmass0 + element.fuelrel=element.unit:GetFuel() + else + element.callsign="Peter-1-1" + element.modex="000" + element.payload={} + element.pylons={} + element.fuelmass0=99999 + element.fuelmass =99999 + element.fuelrel=1 + end + + -- Debug text. + local text=string.format("Adding element %s: status=%s, skill=%s, life=%.1f/%.1f category=%s (%d), type=%s, size=%.1f (L=%.1f H=%.1f W=%.1f), weight=%.1f/%.1f (cargo=%.1f/%.1f)", + element.name, element.status, element.skill, element.life, element.life0, element.categoryname, element.category, element.typename, + element.size, element.length, element.height, element.width, element.weight, element.weightMaxTotal, element.weightCargo, element.weightMaxCargo) + self:T(self.lid..text) + + -- Trigger spawned event if alive. + if unit:IsAlive() and element.status~=OPSGROUP.ElementStatus.SPAWNED then + -- This needs to be slightly delayed (or moved elsewhere) or the first element will always trigger the group spawned event as it is not known that more elements are in the group. + self:__ElementSpawned(0.05, element) + end + + return element + end + + return nil +end + +--- Set the template of the group. +-- @param #OPSGROUP self +-- @param #table Template Template to set. Default is from the GROUP. +-- @return #OPSGROUP self +function OPSGROUP:_SetTemplate(Template) + + -- Set the template. + self.template=Template or UTILS.DeepCopy(_DATABASE:GetGroupTemplate(self.groupname)) --self.group:GetTemplate() + + -- Debug info. + self:T3(self.lid.."Setting group template") + + return self +end + +--- Get the template of the group. +-- @param #OPSGROUP self +-- @param #boolean Copy Get a deep copy of the template. +-- @return #table Template table. +function OPSGROUP:_GetTemplate(Copy) + + if self.template then + + if Copy then + local template=UTILS.DeepCopy(self.template) + return template + else + return self.template + end + + else + self:E(self.lid..string.format("ERROR: No template was set yet!")) + end + + return nil +end + +--- Clear waypoints. +-- @param #OPSGROUP self +-- @param #number IndexMin Clear waypoints up to this min WP index. Default 1. +-- @param #number IndexMax Clear waypoints up to this max WP index. Default `#self.waypoints`. +function OPSGROUP:ClearWaypoints(IndexMin, IndexMax) + + IndexMin=IndexMin or 1 + IndexMax=IndexMax or #self.waypoints + + -- Clear all waypoints. + for i=IndexMax,IndexMin,-1 do + table.remove(self.waypoints, i) + end + --self.waypoints={} +end + +--- Get target group. +-- @param #OPSGROUP self +-- @return Wrapper.Group#GROUP Detected target group. +-- @return #number Distance to target. +function OPSGROUP:_GetDetectedTarget() + + -- Target. + local targetgroup=nil --Wrapper.Group#GROUP + local targetdist=math.huge + + -- Loop over detected groups. + for _,_group in pairs(self.detectedgroups:GetSet()) do + local group=_group --Wrapper.Group#GROUP + + if group and group:IsAlive() then + + -- Get 3D vector of target. + local targetVec3=group:GetVec3() + + -- Distance to target. + local distance=UTILS.VecDist3D(self.position, targetVec3) + + if distance<=self.engagedetectedRmax and distance SCHEDULED --> EXECUTING --> DELIVERED + self:AddTransition("*", "Planned", OPSTRANSPORT.Status.PLANNED) -- Cargo transport was planned. + self:AddTransition(OPSTRANSPORT.Status.PLANNED, "Queued", OPSTRANSPORT.Status.QUEUED) -- Cargo is queued at at least one carrier. + self:AddTransition(OPSTRANSPORT.Status.QUEUED, "Requested", OPSTRANSPORT.Status.REQUESTED) -- Transport assets have been requested from a warehouse. + self:AddTransition(OPSTRANSPORT.Status.REQUESTED, "Scheduled", OPSTRANSPORT.Status.SCHEDULED) -- Cargo is queued at at least one carrier. + 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("*", "Delivered", OPSTRANSPORT.Status.DELIVERED) -- Cargo was delivered. + + self:AddTransition("*", "StatusUpdate", "*") + self:AddTransition("*", "Stop", "*") + + self:AddTransition("*", "Cancel", OPSTRANSPORT.Status.CANCELLED) -- Command to cancel the transport. + + self:AddTransition("*", "Loaded", "*") + self:AddTransition("*", "Unloaded", "*") + + self:AddTransition("*", "DeadCarrierUnit", "*") + self:AddTransition("*", "DeadCarrierGroup", "*") + self:AddTransition("*", "DeadCarrierAll", "*") + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "StatusUpdate". + -- @function [parent=#OPSTRANSPORT] StatusUpdate + -- @param #OPSTRANSPORT self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#OPSTRANSPORT] __StatusUpdate + -- @param #OPSTRANSPORT self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Planned". + -- @function [parent=#OPSTRANSPORT] Planned + -- @param #OPSTRANSPORT self + + --- Triggers the FSM event "Planned" after a delay. + -- @function [parent=#OPSTRANSPORT] __Planned + -- @param #OPSTRANSPORT self + -- @param #number delay Delay in seconds. + + --- On after "Planned" event. + -- @function [parent=#OPSTRANSPORT] OnAfterPlanned + -- @param #OPSTRANSPORT self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Queued". + -- @function [parent=#OPSTRANSPORT] Queued + -- @param #OPSTRANSPORT self + + --- Triggers the FSM event "Queued" after a delay. + -- @function [parent=#OPSTRANSPORT] __Queued + -- @param #OPSTRANSPORT self + -- @param #number delay Delay in seconds. + + --- On after "Queued" event. + -- @function [parent=#OPSTRANSPORT] OnAfterQueued + -- @param #OPSTRANSPORT self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Requested". + -- @function [parent=#OPSTRANSPORT] Requested + -- @param #OPSTRANSPORT self + + --- Triggers the FSM event "Requested" after a delay. + -- @function [parent=#OPSTRANSPORT] __Requested + -- @param #OPSTRANSPORT self + -- @param #number delay Delay in seconds. + + --- On after "Requested" event. + -- @function [parent=#OPSTRANSPORT] OnAfterRequested + -- @param #OPSTRANSPORT self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Scheduled". + -- @function [parent=#OPSTRANSPORT] Scheduled + -- @param #OPSTRANSPORT self + + --- Triggers the FSM event "Scheduled" after a delay. + -- @function [parent=#OPSTRANSPORT] __Scheduled + -- @param #OPSTRANSPORT self + -- @param #number delay Delay in seconds. + + --- On after "Scheduled" event. + -- @function [parent=#OPSTRANSPORT] OnAfterScheduled + -- @param #OPSTRANSPORT self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Executing". + -- @function [parent=#OPSTRANSPORT] Executing + -- @param #OPSTRANSPORT self + + --- Triggers the FSM event "Executing" after a delay. + -- @function [parent=#OPSTRANSPORT] __Executing + -- @param #OPSTRANSPORT self + -- @param #number delay Delay in seconds. + + --- On after "Executing" event. + -- @function [parent=#OPSTRANSPORT] OnAfterExecuting + -- @param #OPSTRANSPORT self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Delivered". + -- @function [parent=#OPSTRANSPORT] Delivered + -- @param #OPSTRANSPORT self + + --- Triggers the FSM event "Delivered" after a delay. + -- @function [parent=#OPSTRANSPORT] __Delivered + -- @param #OPSTRANSPORT self + -- @param #number delay Delay in seconds. + + --- On after "Delivered" event. + -- @function [parent=#OPSTRANSPORT] OnAfterDelivered + -- @param #OPSTRANSPORT self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Cancel". + -- @function [parent=#OPSTRANSPORT] Cancel + -- @param #OPSTRANSPORT self + + --- Triggers the FSM event "Cancel" after a delay. + -- @function [parent=#OPSTRANSPORT] __Cancel + -- @param #OPSTRANSPORT self + -- @param #number delay Delay in seconds. + + --- On after "Cancel" event. + -- @function [parent=#OPSTRANSPORT] OnAfterCancel + -- @param #OPSTRANSPORT self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Loaded". + -- @function [parent=#OPSTRANSPORT] Loaded + -- @param #OPSTRANSPORT self + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCargo OPSGROUP that was loaded into a carrier. + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCarrier OPSGROUP that was loaded into a carrier. + -- @param Ops.OpsGroup#OPSGROUP.Element CarrierElement Carrier element. + + --- Triggers the FSM event "Loaded" after a delay. + -- @function [parent=#OPSTRANSPORT] __Loaded + -- @param #OPSTRANSPORT self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCargo OPSGROUP that was loaded into a carrier. + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCarrier OPSGROUP that was loaded into a carrier. + -- @param Ops.OpsGroup#OPSGROUP.Element CarrierElement Carrier element. + + --- On after "Loaded" event. + -- @function [parent=#OPSTRANSPORT] OnAfterLoaded + -- @param #OPSTRANSPORT self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCargo OPSGROUP that was loaded into a carrier. + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCarrier OPSGROUP that was loaded into a carrier. + -- @param Ops.OpsGroup#OPSGROUP.Element CarrierElement Carrier element. + + + --- Triggers the FSM event "Unloaded". + -- @function [parent=#OPSTRANSPORT] Unloaded + -- @param #OPSTRANSPORT self + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCargo Cargo OPSGROUP that was unloaded from a carrier. + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCarrier Carrier OPSGROUP that unloaded the cargo. + + --- Triggers the FSM event "Unloaded" after a delay. + -- @function [parent=#OPSTRANSPORT] __Unloaded + -- @param #OPSTRANSPORT self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCargo Cargo OPSGROUP that was unloaded from a carrier. + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCarrier Carrier OPSGROUP that unloaded the cargo. + + --- On after "Unloaded" event. + -- @function [parent=#OPSTRANSPORT] OnAfterUnloaded + -- @param #OPSTRANSPORT self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCargo Cargo OPSGROUP that was unloaded from a carrier. + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCarrier Carrier OPSGROUP that unloaded the cargo. + + + --TODO: Psydofunctions + + -- Call status update. + self:__StatusUpdate(-1) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add pickup and deploy zone combination. +-- @param #OPSTRANSPORT self +-- @param Core.Set#SET_GROUP CargoGroups Groups to be transported as cargo. Can also be a single @{Wrapper.Group#GROUP} or @{Ops.OpsGroup#OPSGROUP} object. +-- @param Core.Zone#ZONE PickupZone Zone where the troops are picked up. +-- @param Core.Zone#ZONE DeployZone Zone where the troops are picked up. +-- @return #OPSTRANSPORT.TransportZoneCombo Transport zone table. +function OPSTRANSPORT:AddTransportZoneCombo(CargoGroups, PickupZone, DeployZone) + + -- Increase counter. + self.tzcCounter=self.tzcCounter+1 + + local tzcombo={} --#OPSTRANSPORT.TransportZoneCombo + + -- Init. + tzcombo.uid=self.tzcCounter + tzcombo.Ncarriers=0 + tzcombo.Ncargo=0 + tzcombo.Cargos={} + tzcombo.RequiredCargos={} + tzcombo.DisembarkCarriers={} + tzcombo.PickupPaths={} + tzcombo.TransportPaths={} + + -- Set zones. + self:SetPickupZone(PickupZone, tzcombo) + self:SetDeployZone(DeployZone, tzcombo) + self:SetEmbarkZone(nil, tzcombo) + + -- Add cargo groups (could also be added later). + if CargoGroups then + self:AddCargoGroups(CargoGroups, tzcombo) + end + + -- Add to table. + table.insert(self.tzCombos, tzcombo) + + return tzcombo +end + +--- Add cargo groups to be transported. +-- @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. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @param #boolean DisembarkActivation If `true`, cargo group is activated when disembarked. If `false`, cargo groups are late activated when disembarked. Default `nil` (usually activated). +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:AddCargoGroups(GroupSet, TransportZoneCombo, DisembarkActivation) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + -- 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, TransportZoneCombo, DisembarkActivation) + + if cargo then + + -- Add to main table. + --table.insert(self.cargos, cargo) + self.Ncargo=self.Ncargo+1 + + -- Add to TZC table. + table.insert(TransportZoneCombo.Cargos, cargo) + TransportZoneCombo.Ncargo=TransportZoneCombo.Ncargo+1 + + cargo.opsgroup:_AddMyLift(self) + + end + + else + + -- We got a SET_GROUP object. + + for _,group in pairs(GroupSet.Set) do + + -- Call iteravely for each group. + self:AddCargoGroups(group, TransportZoneCombo, DisembarkActivation) + + end + end + + -- Debug info. + if self.verbose>=1 then + local text=string.format("Added cargo groups:") + local Weight=0 + for _,_cargo in pairs(self:GetCargos()) do + 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) + end + text=text..string.format("\nTOTAL: Ncargo=%d, Weight=%.1f kg", self.Ncargo, Weight) + self:I(self.lid..text) + end + + + return self +end + + +--- Set pickup zone. +-- @param #OPSTRANSPORT self +-- @param Core.Zone#ZONE PickupZone Zone where the troops are picked up. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetPickupZone(PickupZone, TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + TransportZoneCombo.PickupZone=PickupZone + + if PickupZone and PickupZone:IsInstanceOf("ZONE_AIRBASE") then + TransportZoneCombo.PickupAirbase=PickupZone._.ZoneAirbase + end + + return self +end + +--- Get pickup zone. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return Core.Zone#ZONE Zone where the troops are picked up. +function OPSTRANSPORT:GetPickupZone(TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + return TransportZoneCombo.PickupZone +end + +--- Set deploy zone. +-- @param #OPSTRANSPORT self +-- @param Core.Zone#ZONE DeployZone Zone where the troops are deployed. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetDeployZone(DeployZone, TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + -- Set deploy zone. + TransportZoneCombo.DeployZone=DeployZone + + -- Check if this is an airbase. + if DeployZone and DeployZone:IsInstanceOf("ZONE_AIRBASE") then + TransportZoneCombo.DeployAirbase=DeployZone._.ZoneAirbase + end + + return self +end + +--- Get deploy zone. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return Core.Zone#ZONE Zone where the troops are deployed. +function OPSTRANSPORT:GetDeployZone(TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + return TransportZoneCombo.DeployZone +end + +--- Set embark zone. +-- @param #OPSTRANSPORT self +-- @param Core.Zone#ZONE EmbarkZone Zone where the troops are embarked. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetEmbarkZone(EmbarkZone, TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + TransportZoneCombo.EmbarkZone=EmbarkZone or TransportZoneCombo.PickupZone + + return self +end + +--- Get embark zone. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return Core.Zone#ZONE Zone where the troops are embarked from. +function OPSTRANSPORT:GetEmbarkZone(TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + return TransportZoneCombo.EmbarkZone +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. Can also be passed as a #GROUP, #OPSGROUP or #SET_OPSGROUP object. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetEmbarkCarriers(Carriers, TransportZoneCombo) + + -- Debug info. + self:T(self.lid.."Setting embark carriers!") + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + if Carriers:IsInstanceOf("GROUP") or Carriers:IsInstanceOf("OPSGROUP") then + + local carrier=self:_GetOpsGroupFromObject(Carriers) + if carrier then + table.insert(TransportZoneCombo.EmbarkCarriers, 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(TransportZoneCombo.EmbarkCarriers, 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 + +--- Get embark transfer carrier(s). These are carrier groups, where the cargo is directly loaded into when disembarked. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #table Table of carrier OPS groups. +function OPSTRANSPORT:GetEmbarkCarriers(TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + return TransportZoneCombo.EmbarkCarriers +end + +]] + +--- Set disembark zone. +-- @param #OPSTRANSPORT self +-- @param Core.Zone#ZONE DisembarkZone Zone where the troops are disembarked. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetDisembarkZone(DisembarkZone, TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + TransportZoneCombo.DisembarkZone=DisembarkZone + + return self +end + +--- Get disembark zone. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return Core.Zone#ZONE Zone where the troops are disembarked to. +function OPSTRANSPORT:GetDisembarkZone(TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + return TransportZoneCombo.DisembarkZone +end + +--- Set activation status of group when disembarked from transport carrier. +-- @param #OPSTRANSPORT self +-- @param #boolean Active If `true` or `nil`, group is activated when disembarked. If `false`, group is late activated and needs to be activated manually. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetDisembarkActivation(Active, TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + if Active==true or Active==nil then + TransportZoneCombo.disembarkActivation=true + else + TransportZoneCombo.disembarkActivation=false + end + + return self +end + +--- Get disembark activation. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #boolean If `true`, groups are spawned in late activated state. +function OPSTRANSPORT:GetDisembarkActivation(TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + return TransportZoneCombo.disembarkActivation +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. Can also be passed as a #GROUP, #OPSGROUP or #SET_OPSGROUP object. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetDisembarkCarriers(Carriers, TransportZoneCombo) + + -- Debug info. + self:T(self.lid.."Setting transfer carriers!") + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + if Carriers:IsInstanceOf("GROUP") or Carriers:IsInstanceOf("OPSGROUP") then + + local carrier=self:_GetOpsGroupFromObject(Carriers) + if carrier then + table.insert(TransportZoneCombo.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(TransportZoneCombo.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 + +--- Get transfer carrier(s). These are carrier groups, where the cargo is directly loaded into when disembarked. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #table Table of carrier OPS groups. +function OPSTRANSPORT:GetDisembarkCarriers(TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + return TransportZoneCombo.DisembarkCarriers +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 +-- @param #boolean InUtero If `true` or `nil`, group remains *in utero* after disembarkment. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetDisembarkInUtero(InUtero, TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + if InUtero==true or InUtero==nil then + TransportZoneCombo.disembarkInUtero=true + else + TransportZoneCombo.disembarkInUtero=false + end + + return self +end + +--- Get disembark in utero. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #boolean If `true`, groups stay in utero after disembarkment. +function OPSTRANSPORT:GetDisembarkInUtero(TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + return TransportZoneCombo.disembarkInUtero +end + +--- Set pickup formation. +-- @param #OPSTRANSPORT self +-- @param #number Formation Pickup formation. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetFormationPickup(Formation, TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + TransportZoneCombo.PickupFormation=Formation + + return self +end + +--- Get pickup formation. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #number Formation. +function OPSTRANSPORT:_GetFormationPickup(TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + return TransportZoneCombo.PickupFormation +end + +--- Set transport formation. +-- @param #OPSTRANSPORT self +-- @param #number Formation Pickup formation. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetFormationTransport(Formation, TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + TransportZoneCombo.TransportFormation=Formation + + return self +end + +--- Get transport formation. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #number Formation. +function OPSTRANSPORT:_GetFormationTransport(TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + return TransportZoneCombo.TransportFormation +end + + + +--- Set required cargo. This is a list of cargo groups that need to be loaded before the **first** transport will start. +-- @param #OPSTRANSPORT self +-- @param Core.Set#SET_GROUP Cargos Required cargo set. Can also be passed as a #GROUP, #OPSGROUP or #SET_OPSGROUP object. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetRequiredCargos(Cargos, TransportZoneCombo) + + -- Debug info. + self:T(self.lid.."Setting required cargos!") + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + -- Create table. + TransportZoneCombo.RequiredCargos=TransportZoneCombo.RequiredCargos or {} + + if Cargos:IsInstanceOf("GROUP") or Cargos:IsInstanceOf("OPSGROUP") then + + local cargo=self:_GetOpsGroupFromObject(Cargos) + if cargo then + table.insert(TransportZoneCombo.RequiredCargos, cargo) + end + + elseif Cargos:IsInstanceOf("SET_GROUP") or Cargos:IsInstanceOf("SET_OPSGROUP") then + + for _,object in pairs(Cargos:GetSet()) do + local cargo=self:_GetOpsGroupFromObject(object) + if cargo then + table.insert(TransportZoneCombo.RequiredCargos, cargo) + end + end + + else + self:E(self.lid.."ERROR: Required Cargos must be a GROUP, OPSGROUP, SET_GROUP or SET_OPSGROUP object!") + end + + return self +end + +--- Get required cargos. This is a list of cargo groups that need to be loaded before the **first** transport will start. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #table Table of required cargo ops groups. +function OPSTRANSPORT:GetRequiredCargos(TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + return TransportZoneCombo.RequiredCargos +end + +--- Set number of required carrier groups for an OPSTRANSPORT assignment. Only used if transport is assigned at **LEGION** or higher level. +-- @param #OPSTRANSPORT self +-- @param #number NcarriersMin Number of carriers *at least* required. Default 1. +-- @param #number NcarriersMax Number of carriers *at most* used for transportation. Default is same as `NcarriersMin`. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetRequiredCarriers(NcarriersMin, NcarriersMax) + + self.nCarriersMin=NcarriersMin or 1 + + self.nCarriersMax=NcarriersMax or self.nCarriersMin + + -- Ensure that max is at least equal to min. + if self.nCarriersMax0 then + self:ScheduleOnce(Delay, OPSTRANSPORT._DelCarrier, self, CarrierGroup) + else + if self:IsCarrier(CarrierGroup) then + + for i=#self.carriers,1,-1 do + local carrier=self.carriers[i] --Ops.OpsGroup#OPSGROUP + if carrier.groupname==CarrierGroup.groupname then + self:T(self.lid..string.format("Removing carrier %s", CarrierGroup.groupname)) + table.remove(self.carriers, i) + end + end + + end + end + + return self +end + +--- Get a list of alive carriers. +-- @param #OPSTRANSPORT self +-- @return #table Names of all carriers +function OPSTRANSPORT:_GetCarrierNames() + + local names={} + for _,_carrier in pairs(self.carriers) do + local carrier=_carrier --Ops.OpsGroup#OPSGROUP + if carrier:IsAlive()~=nil then + table.insert(names, carrier.groupname) + end + end + + return names +end + +--- Get (all) cargo @{Ops.OpsGroup#OPSGROUP}s. Optionally, only delivered or undelivered groups can be returned. +-- @param #OPSTRANSPORT self +-- @param #boolean Delivered If `true`, only delivered groups are returned. If `false` only undelivered groups are returned. If `nil`, all groups are returned. +-- @param Ops.OpsGroup#OPSGROUP Carrier (Optional) Only count cargo groups that fit into the given carrier group. Current cargo is not a factor. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #table Cargo Ops groups. Can be and empty table `{}`. +function OPSTRANSPORT:GetCargoOpsGroups(Delivered, Carrier, TransportZoneCombo) + + local cargos=self:GetCargos(TransportZoneCombo) + + local opsgroups={} + for _,_cargo in pairs(cargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + if Delivered==nil or cargo.delivered==Delivered then + if cargo.opsgroup and not (cargo.opsgroup:IsDead() or cargo.opsgroup:IsStopped()) then + if Carrier==nil or Carrier:CanCargo(cargo.opsgroup) then + table.insert(opsgroups, cargo.opsgroup) + end + end + end + end + + return opsgroups +end + +--- Get carriers. +-- @param #OPSTRANSPORT self +-- @return #table Carrier Ops groups. +function OPSTRANSPORT:GetCarriers() + return self.carriers +end + +--- Get cargos. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #table Cargos. +function OPSTRANSPORT:GetCargos(TransportZoneCombo) + + if TransportZoneCombo then + return TransportZoneCombo.Cargos + else + local cargos={} + for _,_tzc in pairs(self.tzCombos) do + local tzc=_tzc --#OPSTRANSPORT.TransportZoneCombo + for _,cargo in pairs(tzc.Cargos) do + table.insert(cargos, cargo) + end + end + return cargos + end + +end + +--- Set transport start and stop time. +-- @param #OPSTRANSPORT self +-- @param #string ClockStart Time the transport is started, e.g. "05:00" for 5 am. If specified as a #number, it will be relative (in seconds) to the current mission time. Default is 5 seconds after mission was added. +-- @param #string ClockStop (Optional) Time the transport is stopped, e.g. "13:00" for 1 pm. If mission could not be started at that time, it will be removed from the queue. If specified as a #number it will be relative (in seconds) to the current mission time. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetTime(ClockStart, ClockStop) + + -- 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 + + -- Set stop time. Default nil. + local Tstop=nil + if ClockStop and type(ClockStop)=="number" then + Tstop=Tnow+ClockStop + elseif ClockStop and type(ClockStop)=="string" then + Tstop=UTILS.ClockToSeconds(ClockStop) + end + + self.Tstart=Tstart + self.Tstop=Tstop + + if Tstop then + self.duration=self.Tstop-self.Tstart + end + + return self +end + +--- Set mission priority and (optional) urgency. Urgent missions can cancel other running missions. +-- @param #OPSTRANSPORT self +-- @param #number Prio Priority 1=high, 100=low. Default 50. +-- @param #number Importance Number 1-10. If missions with lower value are in the queue, these have to be finished first. Default is `nil`. +-- @param #boolean Urgent If *true*, another running mission might be cancelled if it has a lower priority. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetPriority(Prio, Importance, Urgent) + self.prio=Prio or 50 + self.urgent=Urgent + self.importance=Importance + return self +end + +--- Set verbosity. +-- @param #OPSTRANSPORT self +-- @param #number Verbosity Be more verbose. Default 0 +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetVerbosity(Verbosity) + self.verbose=Verbosity or 0 + return self +end + +--- Add start condition. +-- @param #OPSTRANSPORT self +-- @param #function ConditionFunction Function that needs to be true before the transport can be started. Must return a #boolean. +-- @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 + end + + table.insert(self.conditionStart, condition) + + end + + 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. The path is retrieved from the waypoints of a given group. +-- **NOTE** that the category group defines for which carriers this path is valid. +-- For example, if you specify a GROUND group to provide the waypoints, only assigned GROUND carriers will use the +-- path. +-- @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 #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport Zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:AddPathTransport(PathGroup, Reversed, Radius, TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + if type(PathGroup)=="string" then + PathGroup=GROUP:FindByName(PathGroup) + end + + local path={} --#OPSTRANSPORT.Path + path.category=PathGroup:GetCategory() + path.radius=Radius or 0 + path.waypoints=PathGroup:GetTaskRoute() + + -- TODO: Check that only flyover waypoints are given for aircraft. + + -- Add path. + table.insert(TransportZoneCombo.TransportPaths, path) + + return self +end + +--- Get a path for transportation. +-- @param #OPSTRANSPORT self +-- @param #number Category Group category. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport Zone combo. +-- @return #OPSTRANSPORT.Path The path object. +function OPSTRANSPORT:_GetPathTransport(Category, TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + local pathsTransport=TransportZoneCombo.TransportPaths + + if pathsTransport and #pathsTransport>0 then + + local paths={} + + for _,_path in pairs(pathsTransport) do + local path=_path --#OPSTRANSPORT.Path + if path.category==Category then + table.insert(paths, path) + end + end + + if #paths>0 then + + local path=paths[math.random(#paths)] --#OPSTRANSPORT.Path + + return path + end + end + + return nil +end + +--- Add a carrier assigned for this transport. +-- @param #OPSTRANSPORT self +-- @param Ops.OpsGroup#OPSGROUP CarrierGroup Carrier OPSGROUP. +-- @param #string Status Carrier Status. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetCarrierTransportStatus(CarrierGroup, Status) + + -- Old status + local oldstatus=self:GetCarrierTransportStatus(CarrierGroup) + + -- Debug info. + self:T(self.lid..string.format("New carrier transport status for %s: %s --> %s", CarrierGroup:GetName(), oldstatus, Status)) + + -- Set new status. + self.carrierTransportStatus[CarrierGroup.groupname]=Status + + return self +end + +--- Get carrier transport status. +-- @param #OPSTRANSPORT self +-- @param Ops.OpsGroup#OPSGROUP CarrierGroup Carrier OPSGROUP. +-- @return #string Carrier status. +function OPSTRANSPORT:GetCarrierTransportStatus(CarrierGroup) + local status=self.carrierTransportStatus[CarrierGroup.groupname] or "unknown" + return status +end + +--- Get unique ID of the transport assignment. +-- @param #OPSTRANSPORT self +-- @return #number UID. +function OPSTRANSPORT:GetUID() + return self.uid +end + +--- Get number of delivered cargo groups. +-- @param #OPSTRANSPORT self +-- @return #number Total number of delivered cargo groups. +function OPSTRANSPORT:GetNcargoDelivered() + return self.Ndelivered +end + +--- Get number of cargo groups. +-- @param #OPSTRANSPORT self +-- @return #number Total number of cargo groups. +function OPSTRANSPORT:GetNcargoTotal() + return self.Ncargo +end + +--- Get number of carrier groups assigned for this transport. +-- @param #OPSTRANSPORT self +-- @return #number Total number of carrier groups. +function OPSTRANSPORT:GetNcarrier() + return self.Ncarrier +end + +--- Add asset to transport. +-- @param #OPSTRANSPORT self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset to be added. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:AddAsset(Asset, TransportZoneCombo) + + -- Debug info + self:T(self.lid..string.format("Adding asset carrier \"%s\" to transport", tostring(Asset.spawngroupname))) + + -- Add asset to table. + self.assets=self.assets or {} + + table.insert(self.assets, Asset) + + return self +end + +--- Delete asset from mission. +-- @param #OPSTRANSPORT self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset to be removed. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:DelAsset(Asset) + + for i,_asset in pairs(self.assets or {}) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + if asset.uid==Asset.uid then + self:T(self.lid..string.format("Removing asset \"%s\" from transport", tostring(Asset.spawngroupname))) + table.remove(self.assets, i) + return self + end + + end + + return self +end + +--- Add cargo asset. +-- @param #OPSTRANSPORT self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset to be added. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:AddAssetCargo(Asset, TransportZoneCombo) + + -- Debug info + self:T(self.lid..string.format("Adding asset cargo \"%s\" to transport and TZC=%s", tostring(Asset.spawngroupname), TransportZoneCombo and TransportZoneCombo.uid or "N/A")) + + -- Add asset to table. + self.assetsCargo=self.assetsCargo or {} + + table.insert(self.assetsCargo, Asset) + + TransportZoneCombo.assetsCargo=TransportZoneCombo.assetsCargo or {} + + TransportZoneCombo.assetsCargo[Asset.spawngroupname]=Asset + + return self +end + +--- Add LEGION to the transport. +-- @param #OPSTRANSPORT self +-- @param Ops.Legion#LEGION Legion The legion. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:AddLegion(Legion) + + -- Debug info. + self:T(self.lid..string.format("Adding legion %s", Legion.alias)) + + -- Add legion to table. + table.insert(self.legions, Legion) + + return self +end + +--- Remove LEGION from transport. +-- @param #OPSTRANSPORT self +-- @param Ops.Legion#LEGION Legion The legion. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:RemoveLegion(Legion) + + -- Loop over legions + for i=#self.legions,1,-1 do + local legion=self.legions[i] --Ops.Legion#LEGION + if legion.alias==Legion.alias then + -- Debug info. + self:T(self.lid..string.format("Removing legion %s", Legion.alias)) + table.remove(self.legions, i) + return self + end + end + + self:E(self.lid..string.format("ERROR: Legion %s not found and could not be removed!", Legion.alias)) + 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) + + if CarrierGroup then + for _,_carrier in pairs(self.carriers) do + local carrier=_carrier --Ops.OpsGroup#OPSGROUP + if carrier.groupname==CarrierGroup.groupname then + return true + end + end + end + + return false +end + +--- Check if transport is ready to be started. +-- * Start time passed. +-- * Stop time did not pass already. +-- * All start conditions are true. +-- @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() + + -- Pickup AND deploy zones must be set. + local gotzones=false + for _,_tz in pairs(self.tzCombos) do + local tz=_tz --#OPSTRANSPORT.TransportZoneCombo + if tz.PickupZone and tz.DeployZone then + gotzones=true + break + end + end + if not gotzones then + text=text.."No, pickup/deploy zone combo not yet defined!" + return false + end + + -- Start time did not pass yet. + if self.Tstart and Tnowself.Tstop 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:T(text) + return false + end + + -- We're good to go! + text=text.."Yes!" + self:T(text) + return true +end + +--- Set LEGION transport status. +-- @param #OPSTRANSPORT self +-- @param Ops.Legion#LEGION Legion The legion. +-- @param #string Status New status. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetLegionStatus(Legion, Status) + + -- Old status + local status=self:GetLegionStatus(Legion) + + -- Debug info. + self:T(self.lid..string.format("Setting LEGION %s to status %s-->%s", Legion.alias, tostring(status), tostring(Status))) + + -- New status. + self.statusLegion[Legion.alias]=Status + + return self +end + +--- Get LEGION transport status. +-- @param #OPSTRANSPORT self +-- @param Ops.Legion#LEGION Legion The legion. +-- @return #string status Current status. +function OPSTRANSPORT:GetLegionStatus(Legion) + + -- Current status. + local status=self.statusLegion[Legion.alias] or "unknown" + + return status +end + +--- Check if state is PLANNED. +-- @param #OPSTRANSPORT self +-- @return #boolean If true, status is PLANNED. +function OPSTRANSPORT:IsPlanned() + local is=self:is(OPSTRANSPORT.Status.PLANNED) + return is +end + +--- Check if state is QUEUED. +-- @param #OPSTRANSPORT self +-- @param Ops.Legion#LEGION Legion (Optional) Check if transport is queued at this legion. +-- @return #boolean If true, status is QUEUED. +function OPSTRANSPORT:IsQueued(Legion) + local is=self:is(OPSTRANSPORT.Status.QUEUED) + if Legion then + is=self:GetLegionStatus(Legion)==OPSTRANSPORT.Status.QUEUED + end + return is +end + +--- Check if state is REQUESTED. +-- @param #OPSTRANSPORT self +-- @param Ops.Legion#LEGION Legion (Optional) Check if transport is queued at this legion. +-- @return #boolean If true, status is REQUESTED. +function OPSTRANSPORT:IsRequested(Legion) + local is=self:is(OPSTRANSPORT.Status.REQUESTED) + if Legion then + is=self:GetLegionStatus(Legion)==OPSTRANSPORT.Status.REQUESTED + end + return is +end + +--- Check if state is SCHEDULED. +-- @param #OPSTRANSPORT self +-- @return #boolean If true, status is SCHEDULED. +function OPSTRANSPORT:IsScheduled() + local is=self:is(OPSTRANSPORT.Status.SCHEDULED) + return is +end + +--- Check if state is EXECUTING. +-- @param #OPSTRANSPORT self +-- @return #boolean If true, status is EXECUTING. +function OPSTRANSPORT:IsExecuting() + local is=self:is(OPSTRANSPORT.Status.EXECUTING) + return is +end + +--- Check if all cargo was delivered (or is dead). +-- @param #OPSTRANSPORT self +-- @param #number Nmin Number of groups that must be actually delivered (and are not dead). Default 0. +-- @return #boolean If true, all possible cargo was delivered. +function OPSTRANSPORT:IsDelivered(Nmin) + local is=self:is(OPSTRANSPORT.Status.DELIVERED) + Nmin=Nmin or 0 + if Nmin>self.Ncargo then + Nmin=self.Ncargo + end + if self.Ndelivered=1 then + + -- Info text. + local text=string.format("%s: Ncargo=%d/%d, Ncarrier=%d/%d, Nlegions=%d", fsmstate:upper(), self.Ncargo, self.Ndelivered, #self.carriers, self.Ncarrier, #self.legions) + + -- Info about cargo and carrier. + if self.verbose>=2 then + + for i,_tz in pairs(self.tzCombos) do + local tz=_tz --#OPSTRANSPORT.TransportZoneCombo + local pickupzone=tz.PickupZone and tz.PickupZone:GetName() or "Unknown" + local deployzone=tz.DeployZone and tz.DeployZone:GetName() or "Unknown" + text=text..string.format("\n[%d] %s --> %s: Ncarriers=%d, Ncargo=%d (%d)", i, pickupzone, deployzone, tz.Ncarriers, #tz.Cargos, tz.Ncargo) + end + + end + + -- Info about cargo and carrier. + if self.verbose>=3 then + + text=text..string.format("\nCargos:") + for _,_cargo in pairs(self:GetCargos()) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + 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], delivered=%s [UID=%s]", + cargo.opsgroup:GetName(), cargo.opsgroup.cargoStatus:upper(), cargo.opsgroup:GetState(), cargo.opsgroup:GetWeightTotal(), name, cstate, tostring(cargo.delivered), tostring(cargo.opsgroup.cargoTransportUID)) + end + + text=text..string.format("\nCarriers:") + for _,_carrier in pairs(self.carriers) do + local carrier=_carrier --Ops.OpsGroup#OPSGROUP + text=text..string.format("\n- %s: %s [%s], Cargo Bay [current/reserved/total]=%d/%d/%d kg [free %d/%d/%d kg]", + carrier:GetName(), carrier.carrierStatus:upper(), carrier:GetState(), + carrier:GetWeightCargo(nil, false), carrier:GetWeightCargo(), carrier:GetWeightCargoMax(), + carrier:GetFreeCargobay(nil, false), carrier:GetFreeCargobay(), carrier:GetFreeCargobayMax()) + end + end + + self:I(self.lid..text) + end + + -- Check if all cargo was delivered (or is dead). + self:_CheckDelivered() + + -- Update status again. + if not self:IsDelivered() then + self:__StatusUpdate(-30) + end +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Event Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "Planned" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSTRANSPORT:onafterPlanned(From, Event, To) + self:T(self.lid..string.format("New status: %s-->%s", From, To)) +end + +--- On after "Scheduled" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSTRANSPORT:onafterScheduled(From, Event, To) + self:T(self.lid..string.format("New status: %s-->%s", From, To)) +end + +--- On after "Executing" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSTRANSPORT:onafterExecuting(From, Event, To) + self:T(self.lid..string.format("New status: %s-->%s", From, To)) +end + +--- On before "Delivered" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSTRANSPORT:onbeforeDelivered(From, Event, To) + + -- Check that we do not call delivered again. + if From==OPSTRANSPORT.Status.DELIVERED then + return false + end + + return true +end + +--- On after "Delivered" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSTRANSPORT:onafterDelivered(From, Event, To) + self:T(self.lid..string.format("New status: %s-->%s", From, To)) + + -- Inform all assigned carriers that cargo was delivered. They can have this in the queue or are currently processing this transport. + for i=#self.carriers, 1, -1 do + local carrier=self.carriers[i] --Ops.OpsGroup#OPSGROUP + if self:GetCarrierTransportStatus(carrier)~=OPSTRANSPORT.Status.DELIVERED then + carrier:Delivered(self) + end + end + +end + +--- On after "Loaded" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsGroup#OPSGROUP OpsGroupCargo OPSGROUP that was loaded into a carrier. +-- @param Ops.OpsGroup#OPSGROUP OpsGroupCarrier OPSGROUP that was loaded into a carrier. +-- @param Ops.OpsGroup#OPSGROUP.Element CarrierElement Carrier element. +function OPSTRANSPORT:onafterLoaded(From, Event, To, OpsGroupCargo, OpsGroupCarrier, CarrierElement) + self:I(self.lid..string.format("Loaded OPSGROUP %s into carrier %s", OpsGroupCargo:GetName(), tostring(CarrierElement.name))) +end + +--- On after "Unloaded" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsGroup#OPSGROUP OpsGroupCargo Cargo OPSGROUP that was unloaded from a carrier. +-- @param Ops.OpsGroup#OPSGROUP OpsGroupCarrier Carrier OPSGROUP that unloaded the cargo. +function OPSTRANSPORT:onafterUnloaded(From, Event, To, OpsGroupCargo, OpsGroupCarrier) + self:I(self.lid..string.format("Unloaded OPSGROUP %s", OpsGroupCargo:GetName())) +end + +--- On after "DeadCarrierGroup" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsGroup#OPSGROUP OpsGroup Carrier OPSGROUP that is dead. +function OPSTRANSPORT:onafterDeadCarrierGroup(From, Event, To, OpsGroup) + self:I(self.lid..string.format("Carrier OPSGROUP %s dead!", OpsGroup:GetName())) + + -- Increase dead counter. + self.NcarrierDead=self.NcarrierDead+1 + + if #self.carriers==0 then + self:DeadCarrierAll() + end + + -- Remove group from carrier list/table. + self:_DelCarrier(OpsGroup) +end + +--- On after "DeadCarrierAll" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSTRANSPORT:onafterDeadCarrierAll(From, Event, To) + self:I(self.lid..string.format("ALL Carrier OPSGROUPs are dead! Setting stage to PLANNED if not all cargo was delivered.")) + + -- Check if cargo was delivered. + self:_CheckDelivered() + + -- Set state back to PLANNED if not delivered. + if not self:IsDelivered() then + self:Planned() + end +end + +--- On after "Cancel" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSTRANSPORT:onafterCancel(From, Event, To) + + -- Number of OPSGROUPS assigned and alive. + local Ngroups = #self.carriers + + -- Debug info. + self:I(self.lid..string.format("CANCELLING transport in status %s. Will wait for %d carrier groups to report DONE before evaluation", self:GetState(), Ngroups)) + + -- Time stamp. + self.Tover=timer.getAbsTime() + + + if self.chief then + + -- Debug info. + self:T(self.lid..string.format("CHIEF will cancel the transport. Will wait for mission DONE before evaluation!")) + + -- CHIEF will cancel the transport. + self.chief:TransportCancel(self) + + elseif self.commander then + + -- Debug info. + self:T(self.lid..string.format("COMMANDER will cancel the transport. Will wait for transport DELIVERED before evaluation!")) + + -- COMMANDER will cancel the transport. + self.commander:TransportCancel(self) + + elseif self.legions and #self.legions>0 then + + -- Loop over all LEGIONs. + for _,_legion in pairs(self.legions or {}) do + local legion=_legion --Ops.Legion#LEGION + + -- Debug info. + self:T(self.lid..string.format("LEGION %s will cancel the transport. Will wait for transport DELIVERED before evaluation!", legion.alias)) + + -- Legion will cancel all flight missions and remove queued request from warehouse queue. + legion:TransportCancel(self) + + end + + else + + -- Debug info. + self:T(self.lid..string.format("No legion, commander or chief. Attached OPS groups will cancel the transport on their own. Will wait for transport DELIVERED before evaluation!")) + + -- Loop over all carrier groups. + for _,_carrier in pairs(self:GetCarriers()) do + local carrier=_carrier --Ops.OpsGroup#OPSGROUP + carrier:TransportCancel(self) + end + + -- Delete awaited transport. + local cargos=self:GetCargoOpsGroups(false) + for _,_cargo in pairs(cargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP + cargo:_DelMyLift(self) + end + + + end + + -- Special mission states. + if self:IsPlanned() or self:IsQueued() or self:IsRequested() or Ngroups==0 then + self:T(self.lid..string.format("Cancelled transport was in %s stage with %d carrier groups assigned and alive. Call it DELIVERED!", self:GetState(), Ngroups)) + self:Delivered() + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check if all cargo of this transport assignment was delivered. +-- @param #OPSTRANSPORT self +function OPSTRANSPORT:_CheckDelivered() + + -- 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:GetCargos()) 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. + elseif cargo.opsgroup:IsStopped() then + -- This one is stopped. + dead=false + else + done=false --Someone is not done! + dead=false + end + + end + + if dead then + self:I(self.lid.."All cargo DEAD ==> Delivered!") + self:Delivered() + elseif done then + self:I(self.lid.."All cargo DONE ==> Delivered!") + self:Delivered() + end + + end + +end + +--- Check if all required cargos are loaded. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #boolean If true, all required cargos are loaded or there is no required cargo. +function OPSTRANSPORT:_CheckRequiredCargos(TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + local requiredCargos=TransportZoneCombo.RequiredCargos + + if requiredCargos==nil or #requiredCargos==0 then + return true + end + + local carrierNames=self:_GetCarrierNames() + + local gotit=true + for _,_cargo in pairs(requiredCargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP + + + if not cargo:IsLoaded(carrierNames) then + return false + end + + end + + return true +end + +--- Check if all given condition are true. +-- @param #OPSTRANSPORT self +-- @param #table Conditions Table of conditions. +-- @return #boolean If true, all conditions were true. Returns false if at least one condition returned false. +function OPSTRANSPORT:EvalConditionsAll(Conditions) + + -- Any stop condition must be true. + for _,_condition in pairs(Conditions or {}) do + local condition=_condition --#OPSTRANSPORT.Condition + + -- Call function. + local istrue=condition.func(unpack(condition.arg)) + + -- Any false will return false. + if not istrue then + return false + end + + end + + -- 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. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @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, TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + 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(TransportZoneCombo.DisembarkCarriers) do + local carrierGroup=_carrier --Ops.OpsGroup#OPSGROUP + + -- First check if carrier is alive and loading cargo. + if carrierGroup and carrierGroup:IsAlive() and (carrierGroup:IsLoading() or TransportZoneCombo.DeployAirbase) then + + -- Find an element of the group that has enough free space. + carrier=carrierGroup:FindCarrierForCargo(CargoGroup) + + if carrier then + if Zone==nil or Zone:IsVec2InZone(carrier.unit:GetVec2()) then + return carrier, carrierGroup + else + self:T2(self.lid.."Got transfer carrier but carrier not in zone (yet)!") + end + else + self:T2(self.lid.."No transfer carrier available!") + end + + 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. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @param #boolean DisembarkActivation If `true`, cargo group is activated when disembarked. +-- @return Ops.OpsGroup#OPSGROUP.CargoGroup Cargo group data. +function OPSTRANSPORT:_CreateCargoGroupData(group, TransportZoneCombo, DisembarkActivation) + + -- Get ops group. + local opsgroup=self:_GetOpsGroupFromObject(group) + + -- First check that this group is not already contained in this TZC. + for _,_cargo in pairs(TransportZoneCombo.Cargos or {}) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + if cargo.opsgroup.groupname==opsgroup.groupname then + -- Group is already contained. + return nil + end + end + + + -- Create a new data item. + local cargo={} --Ops.OpsGroup#OPSGROUP.CargoGroup + + cargo.opsgroup=opsgroup + cargo.delivered=false + cargo.status="Unknown" + cargo.disembarkActivation=DisembarkActivation + cargo.tzcUID=TransportZoneCombo + + return cargo +end + +--- Count how many cargo groups are inside a zone. +-- @param #OPSTRANSPORT self +-- @param Core.Zone#ZONE Zone The zone object. +-- @param #boolean Delivered If `true`, only delivered groups are returned. If `false` only undelivered groups are returned. If `nil`, all groups are returned. +-- @param Ops.OpsGroup#OPSGROUP Carrier (Optional) Only count cargo groups that fit into the given carrier group. Current cargo is not a factor. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #number Number of cargo groups. +function OPSTRANSPORT:_CountCargosInZone(Zone, Delivered, Carrier, TransportZoneCombo) + + -- Get cargo ops groups. + local cargos=self:GetCargoOpsGroups(Delivered, Carrier, TransportZoneCombo) + + --- Function to check if carrier is supposed to be disembarked to. + local function iscarrier(_cargo) + local cargo=_cargo --Ops.OpsGroup#OPSGROUP + + local mycarrier=cargo:_GetMyCarrierGroup() + + if mycarrier and mycarrier:IsUnloading() then + + local carriers=mycarrier.cargoTransport:GetDisembarkCarriers(mycarrier.cargoTZC) + + for _,_carrier in pairs(carriers) do + local carrier=_carrier --Ops.OpsGroup#OPSGROUP + if Carrier:GetName()==carrier:GetName() then + return true + end + end + + end + + return false + end + + local N=0 + for _,_cargo in pairs(cargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP + + -- Is not cargo? + local isNotCargo=cargo:IsNotCargo(true) + if not isNotCargo then + isNotCargo=iscarrier(cargo) + end + + -- Is in zone? + local isInZone=cargo:IsInZone(Zone) + + -- Is in utero? + local isInUtero=cargo:IsInUtero() + + -- Debug info. + self:T(self.lid..string.format("Cargo=%s: notcargo=%s, iscarrier=%s inzone=%s, inutero=%s", cargo:GetName(), tostring(cargo:IsNotCargo(true)), tostring(iscarrier(cargo)), tostring(isInZone), tostring(isInUtero))) + + -- We look for groups that are not cargo, in the zone or in utero. + if isNotCargo and (isInZone or isInUtero) then + N=N+1 + end + end + + -- Debug info. + self:T(self.lid..string.format("Found %d units in zone %s", N, Zone:GetName())) + + return N +end + +--- Get a transport zone combination (TZC) for a carrier group. The pickup zone will be a zone, where the most cargo groups are located that fit into the carrier. +-- @param #OPSTRANSPORT self +-- @param Ops.OpsGroup#OPSGROUP Carrier The carrier OPS group. +-- @return Core.Zone#ZONE Pickup zone or `#nil`. +function OPSTRANSPORT:_GetTransportZoneCombo(Carrier) + + --- Selection criteria + -- * Distance: pickup zone should be as close as possible. + -- * Ncargo: Number of cargo groups. Pickup, where there is most cargo. + -- * Ncarrier: Number of carriers already "working" on this TZC. Would be better if not all carriers work on the same combo while others are ignored. + + -- Get carrier position. + local vec2=Carrier:GetVec2() + + --- Penalty function. + local function penalty(candidate) + local p=candidate.ncarriers*10-candidate.ncargo+candidate.distance/10 + return p + end + + -- TZC candidates. + local candidates={} + + for i,_transportzone in pairs(self.tzCombos) do + local tz=_transportzone --#OPSTRANSPORT.TransportZoneCombo + + -- Check that pickup and deploy zones were defined. + if tz.PickupZone and tz.DeployZone and tz.EmbarkZone then + + --TODO: Check if Carrier is an aircraft and if so, check that pickup AND deploy zones are airbases (not ships, not farps). + + -- Count undelivered cargos in embark(!) zone that fit into the carrier. + local ncargo=self:_CountCargosInZone(tz.EmbarkZone, false, Carrier, tz) + + -- At least one group in the zone. + if ncargo>=1 then + + -- Distance to the carrier in meters. + local dist=tz.PickupZone:Get2DDistance(vec2) + + local ncarriers=0 + for _,_carrier in pairs(self.carriers) do + local carrier=_carrier --Ops.OpsGroup#OPSGROUP + if carrier and carrier:IsAlive() and carrier.cargoTZC and carrier.cargoTZC.uid==tz.uid then + ncarriers=ncarriers+1 + end + end + + -- New candidate. + local candidate={tzc=tz, distance=dist/1000, ncargo=ncargo, ncarriers=ncarriers} + + -- Calculdate penalty of candidate. + candidate.penalty=penalty(candidate) + + -- Add candidate. + table.insert(candidates, candidate) + + end + end + end + + if #candidates>0 then + + -- Minimize penalty. + local function optTZC(candA, candB) + return candA.penalty=3 then + local text="TZC optimized" + for i,candidate in pairs(candidates) do + text=text..string.format("\n[%d] TPZ=%d, Ncarriers=%d, Ncargo=%d, Distance=%.1f km, PENALTY=%d", i, candidate.tzc.uid, candidate.ncarriers, candidate.ncargo, candidate.distance, candidate.penalty) + end + self:I(self.lid..text) + end + + -- Return best candidate. + return candidates[1].tzc + else + -- No candidates. + self:T(self.lid..string.format("Could NOT find a pickup zone (with cargo) for carrier group %s", Carrier:GetName())) + end + + return nil +end + +--- Get an OPSGROUP from a given OPSGROUP or GROUP object. If the object is a GROUUP, an OPSGROUP is created automatically. +-- @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 diff --git a/Moose Development/Moose/Ops/OpsZone.lua b/Moose Development/Moose/Ops/OpsZone.lua new file mode 100644 index 000000000..fee913378 --- /dev/null +++ b/Moose Development/Moose/Ops/OpsZone.lua @@ -0,0 +1,1203 @@ +--- **Ops** - Strategic Zone. +-- +-- **Main Features:** +-- +-- * Monitor if a zone is captured. +-- * Monitor if an airbase is captured. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- @module Ops.OpsZone +-- @image OPS_OpsZone.png + + +--- OPSZONE class. +-- @type OPSZONE +-- @field #string ClassName Name of the class. +-- @field #string lid DCS log ID string. +-- @field #number verbose Verbosity of output. +-- @field Core.Zone#ZONE zone The zone. +-- @field Wrapper.Airbase#AIRBASE airbase The airbase that is monitored. +-- @field #string airbaseName Name of the airbase that is monitored. +-- @field #string zoneName Name of the zone. +-- @field #number zoneRadius Radius of the zone in meters. +-- @field #number ownerCurrent Coalition of the current owner of the zone. +-- @field #number ownerPrevious Coalition of the previous owner of the zone. +-- @field Core.Timer#TIMER timerStatus Timer for calling the status update. +-- @field #number Nred Number of red units in the zone. +-- @field #number Nblu Number of blue units in the zone. +-- @field #number Nnut Number of neutral units in the zone. +-- @field #table ObjectCategories Object categories for the scan. +-- @field #table UnitCategories Unit categories for the scan. +-- @field #number Tattacked Abs. mission time stamp when an attack was started. +-- @field #number dTCapture Time interval in seconds until a zone is captured. +-- @field #boolean neutralCanCapture Neutral units can capture. Default `false`. +-- @field #boolean drawZone If `true`, draw the zone on the F10 map. +-- @field #boolean markZone If `true`, mark the zone on the F10 map. +-- @field Wrapper.Marker#MARKER marker Marker on the F10 map. +-- @field #string markerText Text shown in the maker. +-- @field #table chiefs Chiefs that monitor this zone. +-- @extends Core.Fsm#FSM + +--- Be surprised! +-- +-- === +-- +-- # The OPSZONE Concept +-- +-- An OPSZONE is a strategically important area. +-- +-- **Restrictions** +-- +-- * Since we are using a DCS routine that scans a zone for units or other objects present in the zone and this DCS routine is limited to cicular zones, only those can be used. +-- +-- @field #OPSZONE +OPSZONE = { + ClassName = "OPSZONE", + verbose = 0, + Nred = 0, + Nblu = 0, + Nnut = 0, + chiefs = {}, +} + + +--- OPSZONE class version. +-- @field #string version +OPSZONE.version="0.2.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ToDo list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Pause/unpause evaluations. +-- TODO: Capture time, i.e. time how long a single coalition has to be inside the zone to capture it. +-- TODO: Can neutrals capture? No, since they are _neutral_! +-- TODO: Differentiate between ground attack and boming by air or arty. +-- DONE: Capture airbases. +-- DONE: Can statics capture or hold a zone? No, unless explicitly requested by mission designer. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new OPSZONE class object. +-- @param #OPSZONE self +-- @param Core.Zone#ZONE Zone The zone. +-- @param #number CoalitionOwner Initial owner of the coaliton. Default `coalition.side.NEUTRAL`. +-- @return #OPSZONE self +function OPSZONE:New(Zone, CoalitionOwner) + + -- Inherit everything from LEGION class. + local self=BASE:Inherit(self, FSM:New()) -- #OPSZONE + + -- Check if zone name instead of ZONE object was passed. + if Zone then + if type(Zone)=="string" then + -- Convert string into a ZONE or ZONE_AIRBASE + local Name=Zone + Zone=ZONE:New(Name) + if not Zone then + local airbase=AIRBASE:FindByName(Name) + if airbase then + Zone=ZONE_AIRBASE:New(Name, 2000) + end + end + if not Zone then + self:E(string.format("ERROR: No ZONE or ZONE_AIRBASE found for name: %s", Name)) + return nil + end + end + else + self:E("ERROR: First parameter Zone is nil in OPSZONE:New(Zone) call!") + return nil + end + + -- Basic checks. + if Zone:IsInstanceOf("ZONE_AIRBASE") then + self.airbase=Zone._.ZoneAirbase + self.airbaseName=self.airbase:GetName() + elseif Zone:IsInstanceOf("ZONE_RADIUS") then + -- Nothing to do. + else + self:E("ERROR: OPSZONE must be a SPHERICAL zone due to DCS restrictions!") + return nil + end + + -- Set some string id for output to DCS.log file. + self.lid=string.format("OPSZONE %s | ", Zone:GetName()) + + -- Set some values. + self.zone=Zone + self.zoneName=Zone:GetName() + self.zoneRadius=Zone:GetRadius() + + -- Current and previous owners. + self.ownerCurrent=CoalitionOwner or coalition.side.NEUTRAL + self.ownerPrevious=CoalitionOwner or coalition.side.NEUTRAL + + -- Contested. + self.isContested=false + + -- We take the airbase coalition. + if self.airbase then + self.ownerCurrent=self.airbase:GetCoalition() + self.ownerPrevious=self.airbase:GetCoalition() + end + + -- Set object categories. + self:SetObjectCategories() + self:SetUnitCategories() + + -- Draw zone. Default is on. + self:SetDrawZone() + self:SetMarkZone(true) + + -- Status timer. + self.timerStatus=TIMER:New(OPSZONE.Status, self) + + + -- FMS start state is EMPTY. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Empty") -- Start FSM. + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + + self:AddTransition("*", "Captured", "Guarded") -- Zone was captured. + + self:AddTransition("Empty", "Guarded", "Guarded") -- Owning coalition left the zone and returned. + + self:AddTransition("*", "Empty", "Empty") -- No red or blue units inside the zone. + + self:AddTransition("*", "Attacked", "Attacked") -- A guarded zone is under attack. + self:AddTransition("*", "Defeated", "Guarded") -- The owning coalition defeated an attack. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". + -- @function [parent=#OPSZONE] Start + -- @param #OPSZONE self + + --- Triggers the FSM event "Start" after a delay. + -- @function [parent=#OPSZONE] __Start + -- @param #OPSZONE self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Stop". + -- @param #OPSZONE self + + --- Triggers the FSM event "Stop" after a delay. + -- @function [parent=#OPSZONE] __Stop + -- @param #OPSZONE self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Captured". + -- @function [parent=#OPSZONE] Captured + -- @param #OPSZONE self + -- @param #number Coalition Coalition side that captured the zone. + + --- Triggers the FSM event "Captured" after a delay. + -- @function [parent=#OPSZONE] __Captured + -- @param #OPSZONE self + -- @param #number delay Delay in seconds. + -- @param #number Coalition Coalition side that captured the zone. + + --- On after "Captured" event. + -- @function [parent=#OPSZONE] OnAfterCaptured + -- @param #OPSZONE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #number Coalition Coalition side that captured the zone. + + + --- Triggers the FSM event "Guarded". + -- @function [parent=#OPSZONE] Guarded + -- @param #OPSZONE self + + --- Triggers the FSM event "Guarded" after a delay. + -- @function [parent=#OPSZONE] __Guarded + -- @param #OPSZONE self + -- @param #number delay Delay in seconds. + + --- On after "Guarded" event. + -- @function [parent=#OPSZONE] OnAfterGuarded + -- @param #OPSZONE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Empty". + -- @function [parent=#OPSZONE] Empty + -- @param #OPSZONE self + + --- Triggers the FSM event "Empty" after a delay. + -- @function [parent=#OPSZONE] __Empty + -- @param #OPSZONE self + -- @param #number delay Delay in seconds. + + --- On after "Empty" event. + -- @function [parent=#OPSZONE] OnAfterEmpty + -- @param #OPSZONE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Attacked". + -- @function [parent=#OPSZONE] Attacked + -- @param #OPSZONE self + -- @param #number AttackerCoalition Coalition side that is attacking the zone. + + --- Triggers the FSM event "Attacked" after a delay. + -- @function [parent=#OPSZONE] __Attacked + -- @param #OPSZONE self + -- @param #number delay Delay in seconds. + -- @param #number AttackerCoalition Coalition side that is attacking the zone. + + --- On after "Attacked" event. + -- @function [parent=#OPSZONE] OnAfterAttacked + -- @param #OPSZONE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #number AttackerCoalition Coalition side that is attacking the zone. + + + --- Triggers the FSM event "Defeated". + -- @function [parent=#OPSZONE] Defeated + -- @param #OPSZONE self + -- @param #number DefeatedCoalition Coalition side that was defeated. + + --- Triggers the FSM event "Defeated" after a delay. + -- @function [parent=#OPSZONE] __Defeated + -- @param #OPSZONE self + -- @param #number delay Delay in seconds. + -- @param #number DefeatedCoalition Coalition side that was defeated. + + --- On after "Defeated" event. + -- @function [parent=#OPSZONE] OnAfterDefeated + -- @param #OPSZONE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #number DefeatedCoalition Coalition side that was defeated. + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set verbosity level. +-- @param #OPSZONE self +-- @param #number VerbosityLevel Level of output (higher=more). Default 0. +-- @return #OPSZONE self +function OPSZONE:SetVerbosity(VerbosityLevel) + self.verbose=VerbosityLevel or 0 + return self +end + +--- Set categories of objects that can capture or hold the zone. +-- +-- * Default is {Object.Category.UNIT} so only units can capture and hold zones. +-- * Set to `{Object.Category.UNIT, Object.Category.STATIC}` if static objects can capture and hold zones +-- +-- Which units can capture zones can be further refined by `:SetUnitCategories()`. +-- +-- @param #OPSZONE self +-- @param #table Categories Object categories. Default is `{Object.Category.UNIT}`. +-- @return #OPSZONE self +function OPSZONE:SetObjectCategories(Categories) + + -- Ensure table if something was passed. + if Categories and type(Categories)~="table" then + Categories={Categories} + end + + -- Set categories. + self.ObjectCategories=Categories or {Object.Category.UNIT, Object.Category.STATIC} + + return self +end + +--- Set categories of units that can capture or hold the zone. See [DCS Class Unit](https://wiki.hoggitworld.com/view/DCS_Class_Unit). +-- @param #OPSZONE self +-- @param #table Categories Table of unit categories. Default `{Unit.Category.GROUND_UNIT}`. +-- @return #OPSZONE +function OPSZONE:SetUnitCategories(Categories) + + -- Ensure table. + if Categories and type(Categories)~="table" then + Categories={Categories} + end + + -- Set categories. + self.UnitCategories=Categories or {Unit.Category.GROUND_UNIT} + + return self +end + +--- Set whether *neutral* units can capture the zone. +-- @param #OPSZONE self +-- @param #boolean CanCapture If `true`, neutral units can. +-- @return #OPSZONE self +function OPSZONE:SetNeutralCanCapture(CanCapture) + self.neutralCanCapture=CanCapture + return self +end + +--- Set if zone is drawn on the F10 map. Color will change depending on current owning coalition. +-- @param #OPSZONE self +-- @param #boolean Switch If `true` or `nil`, draw zone. If `false`, zone is not drawn. +-- @return #OPSZONE self +function OPSZONE:SetDrawZone(Switch) + if Switch==false then + self.drawZone=false + else + self.drawZone=true + end + return self +end + +--- Set if a marker on the F10 map shows the current zone status. +-- @param #OPSZONE self +-- @param #boolean Switch If `true`, zone is marked. If `false` or `nil`, zone is not marked. +-- @param #boolean ReadOnly If `true` or `nil` then mark is read only. +-- @return #OPSZONE self +function OPSZONE:SetMarkZone(Switch, ReadOnly) + if Switch then + self.markZone=true + local Coordinate=self:GetCoordinate() + self.markerText=self:_GetMarkerText() + self.marker=self.marker or MARKER:New(Coordinate, self.markerText) + if ReadOnly==false then + self.marker.readonly=false + else + self.marker.readonly=true + end + self.marker:ToAll() + else + if self.marker then + self.marker:Remove() + end + self.marker=nil + self.marker=false + end + return self +end + + +--- Get current owner of the zone. +-- @param #OPSZONE self +-- @return #number Owner coalition. +function OPSZONE:GetOwner() + return self.ownerCurrent +end + +--- Get coordinate of zone. +-- @param #OPSZONE self +-- @return Core.Point#COORDINATE Coordinate of the zone. +function OPSZONE:GetCoordinate() + local coordinate=self.zone:GetCoordinate() + return coordinate +end + +--- Get name. +-- @param #OPSZONE self +-- @return #string Name of the zone. +function OPSZONE:GetName() + return self.zoneName +end + +--- Get previous owner of the zone. +-- @param #OPSZONE self +-- @return #number Previous owner coalition. +function OPSZONE:GetPreviousOwner() + return self.ownerPrevious +end + +--- Get duration of the current attack. +-- @param #OPSZONE self +-- @return #number Duration in seconds since when the last attack began. Is `nil` if the zone is not under attack currently. +function OPSZONE:GetAttackDuration() + if self:IsAttacked() and self.Tattacked then + + local dT=timer.getAbsTime()-self.Tattacked + return dT + end + + return nil +end + + +--- Check if the red coalition is currently owning the zone. +-- @param #OPSZONE self +-- @return #boolean If `true`, zone is red. +function OPSZONE:IsRed() + local is=self.ownerCurrent==coalition.side.RED + return is +end + +--- Check if the blue coalition is currently owning the zone. +-- @param #OPSZONE self +-- @return #boolean If `true`, zone is blue. +function OPSZONE:IsBlue() + local is=self.ownerCurrent==coalition.side.BLUE + return is +end + +--- Check if the neutral coalition is currently owning the zone. +-- @param #OPSZONE self +-- @return #boolean If `true`, zone is neutral. +function OPSZONE:IsNeutral() + local is=self.ownerCurrent==coalition.side.NEUTRAL + return is +end + +--- Check if zone is guarded. +-- @param #OPSZONE self +-- @return #boolean If `true`, zone is guarded. +function OPSZONE:IsGuarded() + local is=self:is("Guarded") + return is +end + +--- Check if zone is empty. +-- @param #OPSZONE self +-- @return #boolean If `true`, zone is empty. +function OPSZONE:IsEmpty() + local is=self:is("Empty") + return is +end + +--- Check if zone is being attacked by the opposite coalition. +-- @param #OPSZONE self +-- @return #boolean If `true`, zone is being attacked. +function OPSZONE:IsAttacked() + local is=self:is("Attacked") + return is +end + +--- Check if zone is contested. Contested here means red *and* blue units are present in the zone. +-- @param #OPSZONE self +-- @return #boolean If `true`, zone is contested. +function OPSZONE:IsContested() + return self.isContested +end + +--- Check if FMS is stopped. +-- @param #OPSZONE self +-- @return #boolean If `true`, FSM is stopped +function OPSZONE:IsStopped() + local is=self:is("Stopped") + return is +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Start/Stop Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Start OPSZONE FSM. +-- @param #OPSZONE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSZONE:onafterStart(From, Event, To) + + -- Info. + self:I(self.lid..string.format("Starting OPSZONE v%s", OPSZONE.version)) + + -- Reinit the timer. + self.timerStatus=self.timerStatus or TIMER:New(OPSZONE.Status, self) + + -- Status update. + self.timerStatus:Start(1, 120) + + -- Handle base captured event. + if self.airbase then + self:HandleEvent(EVENTS.BaseCaptured) + end + +end + +--- Stop OPSZONE FSM. +-- @param #OPSZONE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSZONE:onafterStop(From, Event, To) + + -- Info. + self:I(self.lid..string.format("Stopping OPSZONE")) + + -- Reinit the timer. + self.timerStatus:Stop() + + -- Unhandle events. + self:UnHandleEvent(EVENTS.BaseCaptured) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Status Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Update status. +-- @param #OPSZONE self +function OPSZONE:Status() + + -- Current FSM state. + local fsmstate=self:GetState() + + -- Get contested. + local contested=tostring(self:IsContested()) + + -- Info message. + if self.verbose>=1 then + local text=string.format("State %s: Owner %d (previous %d), contested=%s, Nunits: red=%d, blue=%d, neutral=%d", fsmstate, self.ownerCurrent, self.ownerPrevious, contested, self.Nred, self.Nblu, self.Nnut) + self:I(self.lid..text) + end + + -- Scanning zone. + self:Scan() + + -- Evaluate the scan result. + self:EvaluateZone() + + -- Update F10 marker (only if enabled). + self:_UpdateMarker() + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "Captured" event. +-- @param #OPSZONE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number NewOwnerCoalition Coalition of the new owner. +function OPSZONE:onafterCaptured(From, Event, To, NewOwnerCoalition) + + -- Debug info. + self:T(self.lid..string.format("Zone captured by coalition=%d", NewOwnerCoalition)) + + -- Set owners. + self.ownerPrevious=self.ownerCurrent + self.ownerCurrent=NewOwnerCoalition + + for _,_chief in pairs(self.chiefs) do + local chief=_chief --Ops.Chief#CHIEF + if chief.coalition==self.ownerCurrent then + chief:ZoneCaptured(self) + else + chief:ZoneLost(self) + end + end + +end + +--- On after "Empty" event. +-- @param #OPSZONE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSZONE:onafterEmpty(From, Event, To) + + -- Debug info. + self:T(self.lid..string.format("Zone is empty EVENT")) + + -- Inform chief. + for _,_chief in pairs(self.chiefs) do + local chief=_chief --Ops.Chief#CHIEF + chief:ZoneEmpty(self) + end + +end + +--- On after "Attacked" event. +-- @param #OPSZONE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number AttackerCoalition Coalition of the attacking ground troops. +function OPSZONE:onafterAttacked(From, Event, To, AttackerCoalition) + + -- Debug info. + self:T(self.lid..string.format("Zone is being attacked by coalition=%s!", tostring(AttackerCoalition))) + + -- Inform chief. + if AttackerCoalition then + for _,_chief in pairs(self.chiefs) do + local chief=_chief --Ops.Chief#CHIEF + if chief.coalition~=AttackerCoalition then + chief:ZoneAttacked(self) + end + end + end + +end + +--- On after "Defeated" event. +-- @param #OPSZONE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number DefeatedCoalition Coalition side that was defeated. +function OPSZONE:onafterDefeated(From, Event, To, DefeatedCoalition) + + -- Debug info. + self:T(self.lid..string.format("Defeated attack on zone by coalition=%d", DefeatedCoalition)) + + -- Not attacked any more. + self.Tattacked=nil + +end + +--- On enter "Guarded" state. +-- @param #OPSZONE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSZONE:onenterGuarded(From, Event, To) + + -- Debug info. + self:T(self.lid..string.format("Zone is guarded")) + + -- Not attacked any more. + self.Tattacked=nil + + if self.drawZone then + self.zone:UndrawZone() + + local color=self:_GetZoneColor() + + self.zone:DrawZone(nil, color, 1.0, color, 0.5) + end + +end + +--- On enter "Attacked" state. +-- @param #OPSZONE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSZONE:onenterAttacked(From, Event, To) + + -- Debug info. + self:T(self.lid..string.format("Zone is Attacked")) + + -- Time stamp when the attack started. + self.Tattacked=timer.getAbsTime() + + -- Draw zone? + if self.drawZone then + self.zone:UndrawZone() + + -- Color. + local color={1, 204/255, 204/255} + + -- Draw zone. + self.zone:DrawZone(nil, color, 1.0, color, 0.5) + end + +end + +--- On enter "Empty" event. +-- @param #OPSZONE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSZONE:onenterEmpty(From, Event, To) + + -- Debug info. + self:T(self.lid..string.format("Zone is empty now")) + + if self.drawZone then + self.zone:UndrawZone() + + local color=self:_GetZoneColor() + + self.zone:DrawZone(nil, color, 1.0, color, 0.2) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Scan Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Scan zone. +-- @param #OPSZONE self +-- @return #OPSZONE self +function OPSZONE:Scan() + + -- Debug info. + if self.verbose>=3 then + local text=string.format("Scanning zone %s R=%.1f m", self.zoneName, self.zoneRadius) + self:I(self.lid..text) + end + + -- Search. + local SphereSearch={id=world.VolumeType.SPHERE, params={point=self.zone:GetVec3(), radius=self.zoneRadius}} + + -- Init number of red, blue and neutral units. + local Nred=0 + local Nblu=0 + local Nnut=0 + + --- Function to evaluate the world search + local function EvaluateZone(_ZoneObject) + + local ZoneObject=_ZoneObject --DCS#Object + + if ZoneObject then + + -- Object category. + local ObjectCategory=ZoneObject:getCategory() + + if ObjectCategory==Object.Category.UNIT and ZoneObject:isExist() and ZoneObject:isActive() then + + --- + -- UNIT + --- + + -- This is a DCS unit object. + local DCSUnit=ZoneObject --DCS#Unit + + --- Function to check if unit category is included. + local function Included() + + if not self.UnitCategories then + -- Any unit is included. + return true + else + -- Check if found object is in specified categories. + local CategoryDCSUnit = ZoneObject:getDesc().category + + for _,UnitCategory in pairs(self.UnitCategories) do + if UnitCategory==CategoryDCSUnit then + return true + end + end + + end + + return false + end + + + if Included() then + + -- Get Coalition. + local Coalition=DCSUnit:getCoalition() + + -- Increase counter. + if Coalition==coalition.side.RED then + Nred=Nred+1 + elseif Coalition==coalition.side.BLUE then + Nblu=Nblu+1 + elseif Coalition==coalition.side.NEUTRAL then + Nnut=Nnut+1 + end + + -- Debug info. + if self.verbose>=4 then + self:I(self.lid..string.format("Found unit %s (coalition=%d)", DCSUnit:getName(), Coalition)) + end + end + + elseif ObjectCategory==Object.Category.STATIC and ZoneObject:isExist() then + + --- + -- STATIC + --- + + -- This is a DCS static object. + local DCSStatic=ZoneObject --DCS#Static + + -- Get coalition. + local Coalition=DCSStatic:getCoalition() + + -- CAREFUL! Downed pilots break routine here without any error thrown. + --local unit=STATIC:Find(DCSStatic) + + -- Increase counter. + if Coalition==coalition.side.RED then + Nred=Nred+1 + elseif Coalition==coalition.side.BLUE then + Nblu=Nblu+1 + elseif Coalition==coalition.side.NEUTRAL then + Nnut=Nnut+1 + end + + -- Debug info + if self.verbose>=4 then + self:I(self.lid..string.format("Found static %s (coalition=%d)", DCSStatic:getName(), Coalition)) + end + + elseif ObjectCategory==Object.Category.SCENERY then + + --- + -- SCENERY + --- + + local SceneryType = ZoneObject:getTypeName() + local SceneryName = ZoneObject:getName() + + -- Debug info. + self:T2(self.lid..string.format("Found scenery type=%s, name=%s", SceneryType, SceneryName)) + end + + end + + return true + end + + -- Search objects. + world.searchObjects(self.ObjectCategories, SphereSearch, EvaluateZone) + + -- Debug info. + if self.verbose>=3 then + local text=string.format("Scan result Nred=%d, Nblue=%d, Nneutral=%d", Nred, Nblu, Nnut) + self:I(self.lid..text) + end + + -- Set values. + self.Nred=Nred + self.Nblu=Nblu + self.Nnut=Nnut + + return self +end + +--- Evaluate zone. +-- @param #OPSZONE self +-- @return #OPSZONE self +function OPSZONE:EvaluateZone() + + -- Set values. + local Nred=self.Nred + local Nblu=self.Nblu + local Nnut=self.Nnut + + if self:IsRed() then + + --- + -- RED zone + --- + + if Nred==0 then + + -- No red units in red zone any more. + + if Nblu>0 then + -- Blue captured red zone. + if not self.airbase then + self:Captured(coalition.side.BLUE) + end + elseif Nnut>0 and self.neutralCanCapture then + -- Neutral captured red zone. + if not self.airbase then + self:Captured(coalition.side.NEUTRAL) + end + else + -- Red zone is now empty (but will remain red). + if not self:IsEmpty() then + self:Empty() + end + end + + else + + -- Red units in red zone. + + if Nblu>0 then + + if not self:IsAttacked() then + self:Attacked(coalition.side.BLUE) + end + + elseif Nblu==0 then + + if self:IsAttacked() and self:IsContested() then + self:Defeated(coalition.side.BLUE) + elseif self:IsEmpty() then + -- Red units left zone and returned (or from initial Empty state). + self:Guarded() + end + + end + + end + + -- Contested by blue? + if Nblu==0 then + self.isContested=false + else + self.isContested=true + end + + elseif self:IsBlue() then + + --- + -- BLUE zone + --- + + if Nblu==0 then + + -- No blue units in blue zone any more. + + if Nred>0 then + -- Red captured blue zone. + if not self.airbase then + self:Captured(coalition.side.RED) + end + elseif Nnut>0 and self.neutralCanCapture then + -- Neutral captured blue zone. + if not self.airbase then + self:Captured(coalition.side.NEUTRAL) + end + else + -- Blue zone is empty now. + if not self:IsEmpty() then + self:Empty() + end + end + + else + + -- Still blue units in blue zone. + + if Nred>0 then + + if not self:IsAttacked() then + -- Red is attacking blue zone. + self:Attacked(coalition.side.RED) + end + + elseif Nred==0 then + + if self:IsAttacked() and self:IsContested() then + -- Blue defeated read attack. + self:Defeated(coalition.side.RED) + elseif self:IsEmpty() then + -- Blue units left zone and returned (or from initial Empty state). + self:Guarded() + end + + end + + end + + -- Contested by red? + if Nred==0 then + self.isContested=false + else + self.isContested=true + end + + elseif self:IsNeutral() then + + --- + -- NEUTRAL zone + --- + + -- Not checked as neutrals cant capture (for now). + --if Nnut==0 then + + -- No neutral units in neutral zone any more. + + if Nred>0 and Nblu>0 then + self:T(self.lid.."FF neutrals left neutral zone and red and blue are present! What to do?") + if not self:IsAttacked() then + self:Attacked() + end + self.isContested=true + elseif Nred>0 then + -- Red captured neutral zone. + if not self.airbase then + self:Captured(coalition.side.RED) + end + elseif Nblu>0 then + -- Blue captured neutral zone. + if not self.airbase then + self:Captured(coalition.side.BLUE) + end + else + -- Neutral zone is empty now. + if not self:IsEmpty() then + self:Empty() + end + end + + --end + + else + self:E(self.lid.."ERROR: Unknown coaliton!") + end + + + -- Finally, check airbase coalition + if self.airbase then + + -- Current coalition. + local airbasecoalition=self.airbase:GetCoalition() + + if airbasecoalition~=self.ownerCurrent then + self:T(self.lid..string.format("Captured airbase %s: Coaltion %d-->%d", self.airbaseName, self.ownerCurrent, airbasecoalition)) + self:Captured(airbasecoalition) + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- DCS Event Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Monitor hit events. +-- @param #OPSZONE self +-- @param Core.Event#EVENTDATA EventData The event data. +function OPSZONE:OnEventHit(EventData) + + if self.HitsOn then + + local UnitHit = EventData.TgtUnit + + -- Check if unit is inside the capture zone and that it is of the defending coalition. + if UnitHit and UnitHit:IsInZone(self) and UnitHit:GetCoalition()==self.ownerCurrent then + + -- Update last hit time. + self.HitTimeLast=timer.getTime() + + -- Only trigger attacked event if not already in state "Attacked". + if not self:IsAttacked() then + self:T3(self.lid.."Hit ==> Attack") + self:Attacked() + end + + end + + end + +end + +--- Monitor base captured events. +-- @param #OPSZONE self +-- @param Core.Event#EVENTDATA EventData The event data. +function OPSZONE:OnEventBaseCaptured(EventData) + + if EventData and EventData.Place and self.airbase and self.airbaseName then + + -- Place is the airbase that was captured. + local airbase=EventData.Place --Wrapper.Airbase#AIRBASE + + -- Check that this airbase belongs or did belong to this warehouse. + if EventData.PlaceName==self.airbaseName then + + -- New coalition of the airbase + local CoalitionNew=airbase:GetCoalition() + + -- Debug info. + self:I(self.lid..string.format("EVENT BASE CAPTURED: New coalition of airbase %s: %d [previous=%d]", self.airbaseName, CoalitionNew, self.ownerCurrent)) + + -- Check that coalition actually changed. + if CoalitionNew~=self.ownerCurrent then + self:Captured(CoalitionNew) + end + + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get RGB color of zone depending on current owner. +-- @param #OPSZONE self +-- @return #table RGB color. +function OPSZONE:_GetZoneColor() + + local color={0,0,0} + + if self.ownerCurrent==coalition.side.NEUTRAL then + color={1, 1, 1} + elseif self.ownerCurrent==coalition.side.BLUE then + color={0, 0, 1} + elseif self.ownerCurrent==coalition.side.RED then + color={1, 0, 0} + else + + end + + return color +end + +--- Update marker on the F10 map. +-- @param #OPSZONE self +function OPSZONE:_UpdateMarker() + + if self.markZone then + + -- Get marker text. + local text=self:_GetMarkerText() + + -- Chck if marker text changed and if so, update the marker. + if text~=self.markerText then + self.markerText=text + self.marker:UpdateText(self.markerText) + end + + --TODO: Update position if changed. + + end + +end + +--- Get marker text +-- @param #OPSZONE self +-- @return #string Marker text. +function OPSZONE:_GetMarkerText() + + local owner=UTILS.GetCoalitionName(self.ownerCurrent) + local prevowner=UTILS.GetCoalitionName(self.ownerPrevious) + + -- Get marker text. + local text=string.format("%s: Owner=%s [%s]\nState=%s [Contested=%s]\nBlue=%d, Red=%d, Neutral=%d", + self.zoneName, owner, prevowner, self:GetState(), tostring(self:IsContested()), self.Nblu, self.Nred, self.Nnut) + + return text +end + +--- Add a chief that monitors this zone. Chief will be informed about capturing etc. +-- @param #OPSZONE self +-- @param Ops.Chief#CHIEF Chief The chief. +-- @return #table RGB color. +function OPSZONE:_AddChief(Chief) + + -- Add chief. + table.insert(self.chiefs, Chief) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/Platoon.lua b/Moose Development/Moose/Ops/Platoon.lua new file mode 100644 index 000000000..cbaa8748f --- /dev/null +++ b/Moose Development/Moose/Ops/Platoon.lua @@ -0,0 +1,202 @@ +--- **Ops** - Brigade Platoon. +-- +-- **Main Features:** +-- +-- * Set parameters like livery, skill valid for all platoon members. +-- * Define modex and callsigns. +-- * Define mission types, this platoon can perform (see Ops.Auftrag#AUFTRAG). +-- * Pause/unpause platoon operations. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Ops.Platoon +-- @image OPS_Platoon.png + + +--- PLATOON class. +-- @type PLATOON +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity level. +-- @field Ops.OpsGroup#OPSGROUP.WeaponData weaponData Weapon data table with key=BitType. +-- @extends Ops.Cohort#COHORT + +--- *Some cool cohort quote* -- Known Author +-- +-- === +-- +-- # The PLATOON Concept +-- +-- A PLATOON is essential part of an BRIGADE. +-- +-- +-- +-- @field #PLATOON +PLATOON = { + ClassName = "PLATOON", + verbose = 0, + weaponData = {}, +} + +--- PLATOON class version. +-- @field #string version +PLATOON.version="0.1.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Add weapon data. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new PLATOON object and start the FSM. +-- @param #PLATOON self +-- @param #string TemplateGroupName Name of the template group. +-- @param #number Ngroups Number of asset groups of this platoon. Default 3. +-- @param #string PlatoonName Name of the platoon, e.g. "VFA-37". +-- @return #PLATOON self +function PLATOON:New(TemplateGroupName, Ngroups, PlatoonName) + + -- Inherit everything from COHORT class. + local self=BASE:Inherit(self, COHORT:New(TemplateGroupName, Ngroups, PlatoonName)) -- #PLATOON + + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + -- TODO: Platoon specific user functions. + +--- Set brigade of this platoon. +-- @param #PLATOON self +-- @param Ops.Brigade#BRIGADE Brigade The brigade. +-- @return #PLATOON self +function PLATOON:SetBrigade(Brigade) + self.legion=Brigade + return self +end + +--- Get brigade of this platoon. +-- @param #PLATOON self +-- @return Ops.Brigade#BRIGADE The brigade. +function PLATOON:GetBrigade() + return self.legion +end + +--- Add a weapon range for ARTY auftrag. +-- @param #PLATOON self +-- @param #number RangeMin Minimum range in nautical miles. Default 0 NM. +-- @param #number RangeMax Maximum range in nautical miles. Default 10 NM. +-- @param #number BitType Bit mask of weapon type for which the given min/max ranges apply. Default is `ENUMS.WeaponFlag.Auto`, i.e. for all weapon types. +-- @return #PLATOON self +function PLATOON:AddWeaponRange(RangeMin, RangeMax, BitType) + + RangeMin=UTILS.NMToMeters(RangeMin or 0) + RangeMax=UTILS.NMToMeters(RangeMax or 10) + + local weapon={} --Ops.OpsGroup#OPSGROUP.WeaponData + + weapon.BitType=BitType or ENUMS.WeaponFlag.Auto + weapon.RangeMax=RangeMax + weapon.RangeMin=RangeMin + + self.weaponData=self.weaponData or {} + self.weaponData[tostring(weapon.BitType)]=weapon + + -- Debug info. + self:T(self.lid..string.format("Adding weapon data: Bit=%s, Rmin=%d m, Rmax=%d m", tostring(weapon.BitType), weapon.RangeMin, weapon.RangeMax)) + + if self.verbose>=2 then + local text="Weapon data:" + for _,_weapondata in pairs(self.weaponData) do + local weapondata=_weapondata + text=text..string.format("\n- Bit=%s, Rmin=%d m, Rmax=%d m", tostring(weapondata.BitType), weapondata.RangeMin, weapondata.RangeMax) + end + self:I(self.lid..text) + end + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Start & Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. Starts the FLIGHTGROUP FSM and event handlers. +-- @param #PLATOON self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function PLATOON:onafterStart(From, Event, To) + + -- Short info. + local text=string.format("Starting %s v%s %s", self.ClassName, self.version, self.name) + self:I(self.lid..text) + + -- Start the status monitoring. + self:__Status(-1) +end + +--- On after "Status" event. +-- @param #PLATOON self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function PLATOON:onafterStatus(From, Event, To) + + if self.verbose>=1 then + + -- FSM state. + local fsmstate=self:GetState() + + local callsign=self.callsignName and UTILS.GetCallsignName(self.callsignName) or "N/A" + local modex=self.modex and self.modex or -1 + local skill=self.skill and tostring(self.skill) or "N/A" + + local NassetsTot=#self.assets + local NassetsInS=self:CountAssets(true) + local NassetsQP=0 ; local NassetsP=0 ; local NassetsQ=0 + if self.legion then + NassetsQP, NassetsP, NassetsQ=self.legion:CountAssetsOnMission(nil, self) + end + + -- Short info. + local text=string.format("%s [Type=%s, Call=%s, Modex=%d, Skill=%s]: Assets Total=%d, Stock=%d, Mission=%d [Active=%d, Queue=%d]", + fsmstate, self.aircrafttype, callsign, modex, skill, NassetsTot, NassetsInS, NassetsQP, NassetsP, NassetsQ) + self:I(self.lid..text) + + -- Weapon data info. + if self.verbose>=3 and self.weaponData then + local text="Weapon Data:" + for bit,_weapondata in pairs(self.weaponData) do + local weapondata=_weapondata --Ops.OpsGroup#OPSGROUP.WeaponData + text=text..string.format("\n- Bit=%s: Rmin=%.1f km, Rmax=%.1f km", bit, weapondata.RangeMin/1000, weapondata.RangeMax/1000) + end + self:I(self.lid..text) + end + + -- Check if group has detected any units. + self:_CheckAssetStatus() + + end + + if not self:IsStopped() then + self:__Status(-60) + end +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Misc functions. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/Squadron.lua b/Moose Development/Moose/Ops/Squadron.lua index c0f53d960..22cd1f964 100644 --- a/Moose Development/Moose/Ops/Squadron.lua +++ b/Moose Development/Moose/Ops/Squadron.lua @@ -36,7 +36,6 @@ -- @field #number modexcounter Counter to incease modex number for assets. -- @field #string callsignName Callsign name. -- @field #number callsigncounter Counter to increase callsign names for new assets. --- @field Ops.AirWing#AIRWING airwing The AIRWING object the squadron belongs to. -- @field #number Ngroups Number of asset flight groups this squadron has. -- @field #number engageRange Mission range in meters. -- @field #string attribute Generalized attribute of the squadron template group. @@ -46,7 +45,8 @@ -- @field #number radioFreq Radio frequency in MHz the squad uses. -- @field #number radioModu Radio modulation the squad uses. -- @field #number takeoffType Take of type. --- @extends Core.Fsm#FSM +-- @field #table parkingIDs Parking IDs for this squadron. +-- @extends Ops.Cohort#COHORT --- *It is unbelievable what a squadron of twelve aircraft did to tip the balance.* -- Adolf Galland -- @@ -64,37 +64,23 @@ SQUADRON = { ClassName = "SQUADRON", verbose = 0, - lid = nil, - name = nil, - templatename = nil, - aircrafttype = nil, - assets = {}, - missiontypes = {}, - repairtime = 0, - maintenancetime= 0, - livery = nil, - skill = nil, modex = nil, modexcounter = 0, callsignName = nil, callsigncounter= 11, - airwing = nil, - Ngroups = nil, - engageRange = nil, tankerSystem = nil, refuelSystem = nil, - tacanChannel = {}, } --- SQUADRON class version. -- @field #string version -SQUADRON.version="0.5.2" +SQUADRON.version="0.8.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Parking spots for squadrons? +-- DONE: Parking spots for squadrons? -- DONE: Engage radius. -- DONE: Modex. -- DONE: Call signs. @@ -112,97 +98,20 @@ SQUADRON.version="0.5.2" function SQUADRON:New(TemplateGroupName, Ngroups, SquadronName) -- Inherit everything from FSM class. - local self=BASE:Inherit(self, FSM:New()) -- #SQUADRON + local self=BASE:Inherit(self, COHORT:New(TemplateGroupName, Ngroups, SquadronName)) -- #SQUADRON - -- Name of the template group. - self.templatename=TemplateGroupName - - -- Squadron name. - self.name=tostring(SquadronName or TemplateGroupName) - - -- Set some string id for output to DCS.log file. - self.lid=string.format("SQUADRON %s | ", self.name) - - -- Template group. - self.templategroup=GROUP:FindByName(self.templatename) - - -- Check if template group exists. - if not self.templategroup then - self:E(self.lid..string.format("ERROR: Template group %s does not exist!", tostring(self.templatename))) - return nil - end - - -- Defaults. - self.Ngroups=Ngroups or 3 - self:SetMissionRange() - self:SetSkill(AI.Skill.GOOD) - --self:SetVerbosity(0) - -- Everyone can ORBIT. self:AddMissionCapability(AUFTRAG.Type.ORBIT) - - -- Generalized attribute. - self.attribute=self.templategroup:GetAttribute() - - -- Aircraft type. - self.aircrafttype=self.templategroup:GetTypeName() - + -- Refueling system. self.refuelSystem=select(2, self.templategroup:GetUnit(1):IsRefuelable()) self.tankerSystem=select(2, self.templategroup:GetUnit(1):IsTanker()) - - -- Start State. - self:SetStartState("Stopped") - - -- Add FSM transitions. - -- From State --> Event --> To State - self:AddTransition("Stopped", "Start", "OnDuty") -- Start FSM. - self:AddTransition("*", "Status", "*") -- Status update. - - self:AddTransition("OnDuty", "Pause", "Paused") -- Pause squadron. - self:AddTransition("Paused", "Unpause", "OnDuty") -- Unpause squadron. - - self:AddTransition("*", "Stop", "Stopped") -- Stop squadron. - - ------------------------ --- Pseudo Functions --- ------------------------ - --- Triggers the FSM event "Start". Starts the SQUADRON. Initializes parameters and starts event handlers. - -- @function [parent=#SQUADRON] Start - -- @param #SQUADRON self - - --- Triggers the FSM event "Start" after a delay. Starts the SQUADRON. Initializes parameters and starts event handlers. - -- @function [parent=#SQUADRON] __Start - -- @param #SQUADRON self - -- @param #number delay Delay in seconds. - - --- Triggers the FSM event "Stop". Stops the SQUADRON and all its event handlers. - -- @param #SQUADRON self - - --- Triggers the FSM event "Stop" after a delay. Stops the SQUADRON and all its event handlers. - -- @function [parent=#SQUADRON] __Stop - -- @param #SQUADRON self - -- @param #number delay Delay in seconds. - - --- Triggers the FSM event "Status". - -- @function [parent=#SQUADRON] Status - -- @param #SQUADRON self - - --- Triggers the FSM event "Status" after a delay. - -- @function [parent=#SQUADRON] __Status - -- @param #SQUADRON self - -- @param #number delay Delay in seconds. - - - -- Debug trace. - if false then - BASE:TraceOnOff(true) - BASE:TraceClass(self.ClassName) - BASE:TraceLevel(1) - end + -- See COHORT class return self end @@ -211,68 +120,6 @@ end -- User functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Set livery painted on all squadron aircraft. --- Note that the livery name in general is different from the name shown in the mission editor. --- --- Valid names are the names of the **livery directories**. Check out the folder in your DCS installation for: --- --- * Full modules: `DCS World OpenBeta\CoreMods\aircraft\\Liveries\\` --- * AI units: `DCS World OpenBeta\Bazar\Liveries\\` --- --- The folder name `` is the string you want. --- --- Or personal liveries you have installed somewhere in your saved games folder. --- --- @param #SQUADRON self --- @param #string LiveryName Name of the livery. --- @return #SQUADRON self -function SQUADRON:SetLivery(LiveryName) - self.livery=LiveryName - return self -end - ---- Set skill level of all squadron team members. --- @param #SQUADRON self --- @param #string Skill Skill of all flights. --- @usage mysquadron:SetSkill(AI.Skill.EXCELLENT) --- @return #SQUADRON self -function SQUADRON:SetSkill(Skill) - self.skill=Skill - return self -end - ---- Set verbosity level. --- @param #SQUADRON self --- @param #number VerbosityLevel Level of output (higher=more). Default 0. --- @return #SQUADRON self -function SQUADRON:SetVerbosity(VerbosityLevel) - self.verbose=VerbosityLevel or 0 - return self -end - ---- Set turnover and repair time. If an asset returns from a mission to the airwing, it will need some time until the asset is available for further missions. --- @param #SQUADRON self --- @param #number MaintenanceTime Time in minutes it takes until a flight is combat ready again. Default is 0 min. --- @param #number RepairTime Time in minutes it takes to repair a flight for each life point taken. Default is 0 min. --- @return #SQUADRON self -function SQUADRON:SetTurnoverTime(MaintenanceTime, RepairTime) - self.maintenancetime=MaintenanceTime and MaintenanceTime*60 or 0 - self.repairtime=RepairTime and RepairTime*60 or 0 - return self -end - ---- Set radio frequency and modulation the squad uses. --- @param #SQUADRON self --- @param #number Frequency Radio frequency in MHz. Default 251 MHz. --- @param #number Modulation Radio modulation. Default 0=AM. --- @usage mysquadron:SetSkill(AI.Skill.EXCELLENT) --- @return #SQUADRON self -function SQUADRON:SetRadio(Frequency, Modulation) - self.radioFreq=Frequency or 251 - self.radioModu=Modulation or radio.modulation.AM - return self -end - --- Set number of units in groups. -- @param #SQUADRON self -- @param #number nunits Number of units. Must be >=1 and <=4. Default 2. @@ -284,6 +131,18 @@ function SQUADRON:SetGrouping(nunits) return self end +--- Set valid parking spot IDs. Assets of this squad are only allowed to be spawned at these parking spots. **Note** that the IDs are different from the ones displayed in the mission editor! +-- @param #SQUADRON self +-- @param #table ParkingIDs Table of parking ID numbers or a single `#number`. +-- @return #SQUADRON self +function SQUADRON:SetParkingIDs(ParkingIDs) + if type(ParkingIDs)~="table" then + ParkingIDs={ParkingIDs} + end + self.parkingIDs=ParkingIDs + return self +end + --- Set takeoff type. All assets of this squadron will be spawned with cold (default) or hot engines. -- Spawning on runways is not supported. @@ -302,7 +161,7 @@ function SQUADRON:SetTakeoffType(TakeoffType) return self end ---- Set takeoff type cold (default). +--- Set takeoff type cold (default). All assets of this squadron will be spawned with engines off (cold). -- @param #SQUADRON self -- @return #SQUADRON self function SQUADRON:SetTakeoffCold() @@ -310,7 +169,7 @@ function SQUADRON:SetTakeoffCold() return self end ---- Set takeoff type hot. +--- Set takeoff type hot. All assets of this squadron will be spawned with engines on (hot). -- @param #SQUADRON self -- @return #SQUADRON self function SQUADRON:SetTakeoffHot() @@ -318,115 +177,6 @@ function SQUADRON:SetTakeoffHot() return self end - ---- Set mission types this squadron is able to perform. --- @param #SQUADRON self --- @param #table MissionTypes Table of mission types. Can also be passed as a #string if only one type. --- @param #number Performance Performance describing how good this mission can be performed. Higher is better. Default 50. Max 100. --- @return #SQUADRON self -function SQUADRON:AddMissionCapability(MissionTypes, Performance) - - -- Ensure Missiontypes is a table. - if MissionTypes and type(MissionTypes)~="table" then - MissionTypes={MissionTypes} - end - - -- Set table. - self.missiontypes=self.missiontypes or {} - - for _,missiontype in pairs(MissionTypes) do - - -- Check not to add the same twice. - if self:CheckMissionCapability(missiontype, self.missiontypes) then - self:E(self.lid.."WARNING: Mission capability already present! No need to add it twice.") - -- TODO: update performance. - else - - local capability={} --Ops.Auftrag#AUFTRAG.Capability - capability.MissionType=missiontype - capability.Performance=Performance or 50 - table.insert(self.missiontypes, capability) - - end - end - - -- Debug info. - self:T2(self.missiontypes) - - return self -end - ---- Get mission types this squadron is able to perform. --- @param #SQUADRON self --- @return #table Table of mission types. Could be empty {}. -function SQUADRON:GetMissionTypes() - - local missiontypes={} - - for _,Capability in pairs(self.missiontypes) do - local capability=Capability --Ops.Auftrag#AUFTRAG.Capability - table.insert(missiontypes, capability.MissionType) - end - - return missiontypes -end - ---- Get mission capabilities of this squadron. --- @param #SQUADRON self --- @return #table Table of mission capabilities. -function SQUADRON:GetMissionCapabilities() - return self.missiontypes -end - ---- Get mission performance for a given type of misson. --- @param #SQUADRON self --- @param #string MissionType Type of mission. --- @return #number Performance or -1. -function SQUADRON:GetMissionPeformance(MissionType) - - for _,Capability in pairs(self.missiontypes) do - local capability=Capability --Ops.Auftrag#AUFTRAG.Capability - if capability.MissionType==MissionType then - return capability.Performance - end - end - - return -1 -end - ---- Set max mission range. Only missions in a circle of this radius around the squadron airbase are executed. --- @param #SQUADRON self --- @param #number Range Range in NM. Default 100 NM. --- @return #SQUADRON self -function SQUADRON:SetMissionRange(Range) - self.engageRange=UTILS.NMToMeters(Range or 100) - return self -end - ---- Set call sign. --- @param #SQUADRON self --- @param #number Callsign Callsign from CALLSIGN.Aircraft, e.g. "Chevy" for CALLSIGN.Aircraft.CHEVY. --- @param #number Index Callsign index, Chevy-**1**. --- @return #SQUADRON self -function SQUADRON:SetCallsign(Callsign, Index) - self.callsignName=Callsign - self.callsignIndex=Index - return self -end - ---- Set modex. --- @param #SQUADRON self --- @param #number Modex A number like 100. --- @param #string Prefix A prefix string, which is put before the `Modex` number. --- @param #string Suffix A suffix string, which is put after the `Modex` number. --- @return #SQUADRON self -function SQUADRON:SetModex(Modex, Prefix, Suffix) - self.modex=Modex - self.modexPrefix=Prefix - self.modexSuffix=Suffix - return self -end - --- Set low fuel threshold. -- @param #SQUADRON self -- @param #number LowFuel Low fuel threshold in percent. Default 25. @@ -454,201 +204,17 @@ end -- @param Ops.AirWing#AIRWING Airwing The airwing. -- @return #SQUADRON self function SQUADRON:SetAirwing(Airwing) - self.airwing=Airwing + self.legion=Airwing return self end ---- Add airwing asset to squadron. +--- Get airwing. -- @param #SQUADRON self --- @param Ops.AirWing#AIRWING.SquadronAsset Asset The airwing asset. --- @return #SQUADRON self -function SQUADRON:AddAsset(Asset) - self:T(self.lid..string.format("Adding asset %s of type %s", Asset.spawngroupname, Asset.unittype)) - Asset.squadname=self.name - table.insert(self.assets, Asset) - return self +-- @return Ops.AirWing#AIRWING The airwing. +function SQUADRON:GetAirwing(Airwing) + return self.legion end ---- Remove airwing asset from squadron. --- @param #SQUADRON self --- @param Ops.AirWing#AIRWING.SquadronAsset Asset The airwing asset. --- @return #SQUADRON self -function SQUADRON:DelAsset(Asset) - for i,_asset in pairs(self.assets) do - local asset=_asset --Ops.AirWing#AIRWING.SquadronAsset - if Asset.uid==asset.uid then - self:T2(self.lid..string.format("Removing asset %s", asset.spawngroupname)) - table.remove(self.assets, i) - break - end - end - return self -end - ---- Remove airwing asset group from squadron. --- @param #SQUADRON self --- @param #string GroupName Name of the asset group. --- @return #SQUADRON self -function SQUADRON:DelGroup(GroupName) - for i,_asset in pairs(self.assets) do - local asset=_asset --Ops.AirWing#AIRWING.SquadronAsset - if GroupName==asset.spawngroupname then - self:T2(self.lid..string.format("Removing asset %s", asset.spawngroupname)) - table.remove(self.assets, i) - break - end - end - return self -end - ---- Get name of the squadron --- @param #SQUADRON self --- @return #string Name of the squadron. -function SQUADRON:GetName() - return self.name -end - ---- Get radio frequency and modulation. --- @param #SQUADRON self --- @return #number Radio frequency in MHz. --- @return #number Radio Modulation (0=AM, 1=FM). -function SQUADRON:GetRadio() - return self.radioFreq, self.radioModu -end - ---- Create a callsign for the asset. --- @param #SQUADRON self --- @param Ops.AirWing#AIRWING.SquadronAsset Asset The airwing asset. --- @return #SQUADRON self -function SQUADRON:GetCallsign(Asset) - - if self.callsignName then - - Asset.callsign={} - - for i=1,Asset.nunits do - - local callsign={} - - callsign[1]=self.callsignName - callsign[2]=math.floor(self.callsigncounter / 10) - callsign[3]=self.callsigncounter % 10 - if callsign[3]==0 then - callsign[3]=1 - self.callsigncounter=self.callsigncounter+2 - else - self.callsigncounter=self.callsigncounter+1 - end - - Asset.callsign[i]=callsign - - self:T3({callsign=callsign}) - - --TODO: there is also a table entry .name, which is a string. - end - - - end - -end - ---- Create a modex for the asset. --- @param #SQUADRON self --- @param Ops.AirWing#AIRWING.SquadronAsset Asset The airwing asset. --- @return #SQUADRON self -function SQUADRON:GetModex(Asset) - - if self.modex then - - Asset.modex={} - - for i=1,Asset.nunits do - - Asset.modex[i]=string.format("%03d", self.modex+self.modexcounter) - - self.modexcounter=self.modexcounter+1 - - self:T3({modex=Asset.modex[i]}) - - end - - end - -end - - ---- Add TACAN channels to the squadron. Note that channels can only range from 1 to 126. --- @param #SQUADRON self --- @param #number ChannelMin Channel. --- @param #number ChannelMax Channel. --- @return #SQUADRON self --- @usage mysquad:AddTacanChannel(64,69) -- adds channels 64, 65, 66, 67, 68, 69 -function SQUADRON:AddTacanChannel(ChannelMin, ChannelMax) - - ChannelMax=ChannelMax or ChannelMin - - if ChannelMin>126 then - self:E(self.lid.."ERROR: TACAN Channel must be <= 126! Will not add to available channels") - return self - end - if ChannelMax>126 then - self:E(self.lid.."WARNING: TACAN Channel must be <= 126! Adjusting ChannelMax to 126") - ChannelMax=126 - end - - for i=ChannelMin,ChannelMax do - self.tacanChannel[i]=true - end - - return self -end - ---- Get an unused TACAN channel. --- @param #SQUADRON self --- @return #number TACAN channel or *nil* if no channel is free. -function SQUADRON:FetchTacan() - - for channel,free in pairs(self.tacanChannel) do - if free then - self:T(self.lid..string.format("Checking out Tacan channel %d", channel)) - self.tacanChannel[channel]=false - return channel - end - end - - return nil -end - ---- "Return" a used TACAN channel. --- @param #SQUADRON self --- @param #number channel The channel that is available again. -function SQUADRON:ReturnTacan(channel) - self:T(self.lid..string.format("Returning Tacan channel %d", channel)) - self.tacanChannel[channel]=true -end - ---- Check if squadron is "OnDuty". --- @param #SQUADRON self --- @return #boolean If true, squdron is in state "OnDuty". -function SQUADRON:IsOnDuty() - return self:Is("OnDuty") -end - ---- Check if squadron is "Stopped". --- @param #SQUADRON self --- @return #boolean If true, squdron is in state "Stopped". -function SQUADRON:IsStopped() - return self:Is("Stopped") -end - ---- Check if squadron is "Paused". --- @param #SQUADRON self --- @return #boolean If true, squdron is in state "Paused". -function SQUADRON:IsPaused() - return self:Is("Paused") -end - - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Start & Status ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -685,10 +251,10 @@ function SQUADRON:onafterStatus(From, Event, To) local skill=self.skill and tostring(self.skill) or "N/A" local NassetsTot=#self.assets - local NassetsInS=self:CountAssetsInStock() + local NassetsInS=self:CountAssets(true) local NassetsQP=0 ; local NassetsP=0 ; local NassetsQ=0 - if self.airwing then - NassetsQP, NassetsP, NassetsQ=self.airwing:CountAssetsOnMission(nil, self) + if self.legion then + NassetsQP, NassetsP, NassetsQ=self.legion:CountAssetsOnMission(nil, self) end -- Short info. @@ -706,369 +272,10 @@ function SQUADRON:onafterStatus(From, Event, To) end end - ---- Check asset status. --- @param #SQUADRON self -function SQUADRON:_CheckAssetStatus() - - if self.verbose>=2 and #self.assets>0 then - - local text="" - for j,_asset in pairs(self.assets) do - local asset=_asset --Ops.AirWing#AIRWING.SquadronAsset - - -- Text. - text=text..string.format("\n[%d] %s (%s*%d): ", j, asset.spawngroupname, asset.unittype, asset.nunits) - - if asset.spawned then - - --- - -- Spawned - --- - - -- Mission info. - local mission=self.airwing and self.airwing:GetAssetCurrentMission(asset) or false - if mission then - local distance=asset.flightgroup and UTILS.MetersToNM(mission:GetTargetDistance(asset.flightgroup.group:GetCoordinate())) or 0 - text=text..string.format("Mission %s - %s: Status=%s, Dist=%.1f NM", mission.name, mission.type, mission.status, distance) - else - text=text.."Mission None" - end - - -- Flight status. - text=text..", Flight: " - if asset.flightgroup and asset.flightgroup:IsAlive() then - local status=asset.flightgroup:GetState() - local fuelmin=asset.flightgroup:GetFuelMin() - local fuellow=asset.flightgroup:IsFuelLow() - local fuelcri=asset.flightgroup:IsFuelCritical() - - text=text..string.format("%s Fuel=%d", status, fuelmin) - if fuelcri then - text=text.." (Critical!)" - elseif fuellow then - text=text.." (Low)" - end - - local lifept, lifept0=asset.flightgroup:GetLifePoints() - text=text..string.format(", Life=%d/%d", lifept, lifept0) - - local ammo=asset.flightgroup:GetAmmoTot() - text=text..string.format(", Ammo=%d [G=%d, R=%d, B=%d, M=%d]", ammo.Total,ammo.Guns, ammo.Rockets, ammo.Bombs, ammo.Missiles) - else - text=text.."N/A" - end - - -- Payload info. - local payload=asset.payload and table.concat(self.airwing:GetPayloadMissionTypes(asset.payload), ", ") or "None" - text=text..", Payload={"..payload.."}" - - else - - --- - -- In Stock - --- - - text=text..string.format("In Stock") - - if self:IsRepaired(asset) then - text=text..", Combat Ready" - else - - text=text..string.format(", Repaired in %d sec", self:GetRepairTime(asset)) - - if asset.damage then - text=text..string.format(" (Damage=%.1f)", asset.damage) - end - end - - if asset.Treturned then - local T=timer.getAbsTime()-asset.Treturned - text=text..string.format(", Returned for %d sec", T) - end - - end - end - self:I(self.lid..text) - end - -end - ---- On after "Stop" event. --- @param #SQUADRON self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. -function SQUADRON:onafterStop(From, Event, To) - - self:I(self.lid.."STOPPING Squadron!") - - -- Remove all assets. - for i=#self.assets,1,-1 do - local asset=self.assets[i] - self:DelAsset(asset) - end - - self.CallScheduler:Clear() - -end - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Misc Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Check if there is a squadron that can execute a given mission. --- We check the mission type, the refuelling system, engagement range --- @param #SQUADRON self --- @param Ops.Auftrag#AUFTRAG Mission The mission. --- @return #boolean If true, Squadron can do that type of mission. -function SQUADRON:CanMission(Mission) - - local cando=true - - -- On duty?= - if not self:IsOnDuty() then - self:T(self.lid..string.format("Squad in not OnDuty but in state %s. Cannot do mission %s with target %s", self:GetState(), Mission.name, Mission:GetTargetName())) - return false - end - - -- Check mission type. WARNING: This assumes that all assets of the squad can do the same mission types! - if not self:CheckMissionType(Mission.type, self:GetMissionTypes()) then - self:T(self.lid..string.format("INFO: Squad cannot do mission type %s (%s, %s)", Mission.type, Mission.name, Mission:GetTargetName())) - return false - end - - -- Check that tanker mission - if Mission.type==AUFTRAG.Type.TANKER then - - if Mission.refuelSystem and Mission.refuelSystem==self.tankerSystem then - -- Correct refueling system. - else - self:T(self.lid..string.format("INFO: Wrong refueling system requested=%s != %s=available", tostring(Mission.refuelSystem), tostring(self.tankerSystem))) - return false - end - - end - - -- Distance to target. - local TargetDistance=Mission:GetTargetDistance(self.airwing:GetCoordinate()) - - -- Max engage range. - local engagerange=Mission.engageRange and math.max(self.engageRange, Mission.engageRange) or self.engageRange - - -- Set range is valid. Mission engage distance can overrule the squad engage range. - if TargetDistance>engagerange then - self:I(self.lid..string.format("INFO: Squad is not in range. Target dist=%d > %d NM max mission Range", UTILS.MetersToNM(TargetDistance), UTILS.MetersToNM(engagerange))) - return false - end - - return true -end - ---- Count assets in airwing (warehous) stock. --- @param #SQUADRON self --- @return #number Assets not spawned. -function SQUADRON:CountAssetsInStock() - - local N=0 - for _,_asset in pairs(self.assets) do - local asset=_asset --Ops.AirWing#AIRWING.SquadronAsset - if asset.spawned then - - else - N=N+1 - end - end - - return N -end - ---- Get assets for a mission. --- @param #SQUADRON self --- @param Ops.Auftrag#AUFTRAG Mission The mission. --- @param #number Nplayloads Number of payloads available. --- @return #table Assets that can do the required mission. -function SQUADRON:RecruitAssets(Mission, Npayloads) - - -- Number of payloads available. - Npayloads=Npayloads or self.airwing:CountPayloadsInStock(Mission.type, self.aircrafttype, Mission.payloads) - - local assets={} - - -- Loop over assets. - for _,_asset in pairs(self.assets) do - local asset=_asset --Ops.AirWing#AIRWING.SquadronAsset - - - -- Check if asset is currently on a mission (STARTED or QUEUED). - if self.airwing:IsAssetOnMission(asset) then - - --- - -- Asset is already on a mission. - --- - - -- Check if this asset is currently on a GCICAP mission (STARTED or EXECUTING). - if self.airwing:IsAssetOnMission(asset, AUFTRAG.Type.GCICAP) and Mission.type==AUFTRAG.Type.INTERCEPT then - - -- Check if the payload of this asset is compatible with the mission. - -- Note: we do not check the payload as an asset that is on a GCICAP mission should be able to do an INTERCEPT as well! - self:I(self.lid.."Adding asset on GCICAP mission for an INTERCEPT mission") - table.insert(assets, asset) - - end - - else - - --- - -- Asset as NO current mission - --- - - if asset.spawned then - - --- - -- Asset is already SPAWNED (could be uncontrolled on the airfield or inbound after another mission) - --- - - local flightgroup=asset.flightgroup - - -- Firstly, check if it has the right payload. - if self:CheckMissionCapability(Mission.type, asset.payload.capabilities) and flightgroup and flightgroup:IsAlive() then - - -- Assume we are ready and check if any condition tells us we are not. - local combatready=true - - if Mission.type==AUFTRAG.Type.INTERCEPT then - combatready=flightgroup:CanAirToAir() - else - local excludeguns=Mission.type==AUFTRAG.Type.BOMBING or Mission.type==AUFTRAG.Type.BOMBRUNWAY or Mission.type==AUFTRAG.Type.BOMBCARPET or Mission.type==AUFTRAG.Type.SEAD or Mission.type==AUFTRAG.Type.ANTISHIP - combatready=flightgroup:CanAirToGround(excludeguns) - end - - -- No more attacks if fuel is already low. Safety first! - if flightgroup:IsFuelLow() then - combatready=false - end - - -- Check if in a state where we really do not want to fight any more. - if flightgroup:IsHolding() or flightgroup:IsLanding() or flightgroup:IsLanded() or flightgroup:IsArrived() or flightgroup:IsDead() or flightgroup:IsStopped() then - combatready=false - end - - -- This asset is "combatready". - if combatready then - self:I(self.lid.."Adding SPAWNED asset to ANOTHER mission as it is COMBATREADY") - table.insert(assets, asset) - end - - end - - else - - --- - -- Asset is still in STOCK - --- - - -- Check that asset is not already requested for another mission. - if Npayloads>0 and self:IsRepaired(asset) and (not asset.requested) then - - -- Add this asset to the selection. - table.insert(assets, asset) - - -- Reduce number of payloads so we only return the number of assets that could do the job. - Npayloads=Npayloads-1 - - end - - end - end - end -- loop over assets - - return assets -end - - ---- Get the time an asset needs to be repaired. --- @param #SQUADRON self --- @param Ops.AirWing#AIRWING.SquadronAsset Asset The asset. --- @return #number Time in seconds until asset is repaired. -function SQUADRON:GetRepairTime(Asset) - - if Asset.Treturned then - - local t=self.maintenancetime - t=t+Asset.damage*self.repairtime - - -- Seconds after returned. - local dt=timer.getAbsTime()-Asset.Treturned - - local T=t-dt - - return T - else - return 0 - end - -end - ---- Checks if a mission type is contained in a table of possible types. --- @param #SQUADRON self --- @param Ops.AirWing#AIRWING.SquadronAsset Asset The asset. --- @return #boolean If true, the requested mission type is part of the possible mission types. -function SQUADRON:IsRepaired(Asset) - - if Asset.Treturned then - local Tnow=timer.getAbsTime() - local Trepaired=Asset.Treturned+self.maintenancetime - if Tnow>=Trepaired then - return true - else - return false - end - - else - return true - end - -end - - ---- Checks if a mission type is contained in a table of possible types. --- @param #SQUADRON self --- @param #string MissionType The requested mission type. --- @param #table PossibleTypes A table with possible mission types. --- @return #boolean If true, the requested mission type is part of the possible mission types. -function SQUADRON:CheckMissionType(MissionType, PossibleTypes) - - if type(PossibleTypes)=="string" then - PossibleTypes={PossibleTypes} - end - - for _,canmission in pairs(PossibleTypes) do - if canmission==MissionType then - return true - end - end - - return false -end - ---- Check if a mission type is contained in a list of possible capabilities. --- @param #SQUADRON self --- @param #string MissionType The requested mission type. --- @param #table Capabilities A table with possible capabilities. --- @return #boolean If true, the requested mission type is part of the possible mission types. -function SQUADRON:CheckMissionCapability(MissionType, Capabilities) - - for _,cap in pairs(Capabilities) do - local capability=cap --Ops.Auftrag#AUFTRAG.Capability - if capability.MissionType==MissionType then - return true - end - end - - return false -end - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/Target.lua b/Moose Development/Moose/Ops/Target.lua index 7aea445b2..a92606276 100644 --- a/Moose Development/Moose/Ops/Target.lua +++ b/Moose Development/Moose/Ops/Target.lua @@ -17,6 +17,7 @@ -- @type TARGET -- @field #string ClassName Name of the class. -- @field #number verbose Verbosity level. +-- @field #number uid Unique ID of the target. -- @field #string lid Class id string for output to DCS log file. -- @field #table targets Table of target objects. -- @field #number targetcounter Running number to generate target object IDs. @@ -30,14 +31,17 @@ -- @field #number Ndead Number of target elements/units that are dead (destroyed or despawned). -- @field #table elements Table of target elements/units. -- @field #table casualties Table of dead element names. +-- @field #number prio Priority. +-- @field #number importance Importance. +-- @field Ops.Auftrag#AUFTRAG mission Mission attached to this target. +-- @field Ops.Intelligence#INTEL.Contact contact Contact attached to this target. +-- @field #boolean isDestroyed If true, target objects were destroyed. -- @extends Core.Fsm#FSM ---- **It is far more important to be able to hit the target than it is to haggle over who makes a weapon or who pulls a trigger** -- Dwight D. Eisenhower +--- **It is far more important to be able to hit the target than it is to haggle over who makes a weapon or who pulls a trigger** -- Dwight D Eisenhower -- -- === -- --- ![Banner Image](..\Presentations\OPS\Target\_Main.pngs) --- -- # The TARGET Concept -- -- Define a target of your mission and monitor its status. Events are triggered when the target is damaged or destroyed. @@ -128,13 +132,15 @@ _TARGETID=0 --- TARGET class version. -- @field #string version -TARGET.version="0.3.1" +TARGET.version="0.5.2" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: A lot. +-- TODO: Had cases where target life was 0 but target was not dead. Need to figure out why! +-- TODO: Add pseudo functions. +-- DONE: Initial object can be nil. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor @@ -151,26 +157,23 @@ function TARGET:New(TargetObject) -- Increase counter. _TARGETID=_TARGETID+1 + + -- Set UID. + self.uid=_TARGETID + + if TargetObject then - -- Add object. - self:AddObject(TargetObject) - - -- Get first target. - local Target=self.targets[1] --#TARGET.Object - - if not Target then - self:E("ERROR: No valid TARGET!") - return nil - end + -- Add object. + self:AddObject(TargetObject) - -- Target Name. - self.name=self:GetTargetName(Target) + end - -- Target category. - self.category=self:GetTargetCategory(Target) + -- Defaults. + self:SetPriority() + self:SetImportance() -- Log ID. - self.lid=string.format("TARGET #%03d [%s] | ", _TARGETID, tostring(self.category)) + self.lid=string.format("TARGET #%03d | ", _TARGETID) -- Start state. self:SetStartState("Stopped") @@ -187,7 +190,7 @@ function TARGET:New(TargetObject) self:AddTransition("*", "Damaged", "*") -- Target was damaged. self:AddTransition("*", "Destroyed", "Dead") -- Target was completely destroyed. - self:AddTransition("*", "Dead", "Dead") -- Target was completely destroyed. + self:AddTransition("*", "Dead", "Dead") -- Target is dead. Could be destroyed or despawned. ------------------------ --- Pseudo Functions --- @@ -220,7 +223,6 @@ function TARGET:New(TargetObject) -- @param #number delay Delay in seconds. - -- Start. self:__Start(-1) @@ -231,12 +233,24 @@ end -- User functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Create target data from a given object. +--- Create target data from a given object. Valid objects are: +-- +-- * GROUP +-- * UNIT +-- * STATIC +-- * AIRBASE +-- * COORDINATE +-- * ZONE +-- * SET_GROUP +-- * SET_UNIT +-- * SET_STATIC +-- * SET_OPSGROUP +-- -- @param #TARGET self -- @param Wrapper.Positionable#POSITIONABLE Object The target GROUP, UNIT, STATIC, AIRBASE or COORDINATE. function TARGET:AddObject(Object) - if Object:IsInstanceOf("SET_GROUP") or Object:IsInstanceOf("SET_UNIT") then + if Object:IsInstanceOf("SET_GROUP") or Object:IsInstanceOf("SET_UNIT") or Object:IsInstanceOf("SET_STATIC") or Object:IsInstanceOf("SET_OPSGROUP") then --- -- Sets @@ -247,31 +261,81 @@ function TARGET:AddObject(Object) for _,object in pairs(set.Set) do self:AddObject(object) end - + + elseif Object:IsInstanceOf("SET_ZONE") then + + local set=Object --Core.Set#SET_ZONE + + set:SortByName() + + for index,ZoneName in pairs(set.Index) do + local zone=set.Set[ZoneName] --Core.Zone#ZONE + self:_AddObject(zone) + end + else --- -- Groups, Units, Statics, Airbases, Coordinates --- - self:_AddObject(Object) + if Object:IsInstanceOf("OPSGROUP") then + self:_AddObject(Object:GetGroup()) -- We add the MOOSE GROUP object not the OPSGROUP object. + else + self:_AddObject(Object) + end end end +--- Set priority of the target. +-- @param #TARGET self +-- @param #number Priority Priority of the target. Default 50. +-- @return #TARGET self +function TARGET:SetPriority(Priority) + self.prio=Priority or 50 + return self +end + +--- Set importance of the target. +-- @param #TARGET self +-- @param #number Importance Importance of the target. Default `nil`. +-- @return #TARGET self +function TARGET:SetImportance(Importance) + self.importance=Importance + return self +end + --- Check if TARGET is alive. -- @param #TARGET self -- @return #boolean If true, target is alive. function TARGET:IsAlive() - return self:Is("Alive") + + for _,_target in pairs(self.targets) do + local target=_target --Ops.Target#TARGET.Object + if target.Status==TARGET.ObjectStatus.ALIVE then + return true + end + end + + return false end +--- Check if TARGET is destroyed. +-- @param #TARGET self +-- @return #boolean If true, target is destroyed. +function TARGET:IsDestroyed() + return self.isDestroyed +end + + --- Check if TARGET is dead. -- @param #TARGET self -- @return #boolean If true, target is dead. function TARGET:IsDead() - return self:Is("Dead") + local is=self:Is("Dead") + return is end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -322,6 +386,11 @@ function TARGET:onafterStatus(From, Event, To) damaged=true end + if life==0 then + self:I(self.lid..string.format("FF life is zero but no object dead event fired ==> object dead now for traget object %s!", tostring(target.Name))) + self:ObjectDead(target) + end + end -- Target was damaged. @@ -422,6 +491,8 @@ function TARGET:onafterObjectDead(From, Event, To, Target) if self.Ndestroyed==self.Ntargets0 then + self.isDestroyed=true + self:Destroyed() else @@ -691,6 +762,13 @@ function TARGET:_AddObject(Object) target.Object=Object table.insert(self.targets, target) + + if self.name==nil then + self.name=self:GetTargetName(target) + end + if self.category==nil then + self.category=self:GetTargetCategory(target) + end end @@ -805,10 +883,25 @@ function TARGET:GetLife() return N end +--- Get target 2D position vector. +-- @param #TARGET self +-- @param #TARGET.Object Target Target object. +-- @return DCS#Vec2 Vector with x,y components. +function TARGET:GetTargetVec2(Target) + + local vec3=self:GetTargetVec3(Target) + + if vec3 then + return {x=vec3.x, y=vec3.z} + end + + return nil +end + --- Get target 3D position vector. -- @param #TARGET self -- @param #TARGET.Object Target Target object. --- @return DCS#Vec3 Vector with x,y,z components +-- @return DCS#Vec3 Vector with x,y,z components. function TARGET:GetTargetVec3(Target) if Target.Type==TARGET.ObjectType.GROUP then @@ -955,6 +1048,12 @@ function TARGET:GetTargetName(Target) local coord=Target.Object --Core.Point#COORDINATE return coord:ToStringMGRS() + + elseif Target.Type==TARGET.ObjectType.ZONE then + + local Zone=Target.Object --Core.Zone#ZONE + + return Zone:GetName() end @@ -965,7 +1064,48 @@ end -- @param #TARGET self -- @return #string Name of the target usually the first object. function TARGET:GetName() - return self.name + local name=self.name or "Unknown" + return name +end + +--- Get 2D vector. +-- @param #TARGET self +-- @return DCS#Vec2 2D vector of the target. +function TARGET:GetVec2() + + for _,_target in pairs(self.targets) do + local Target=_target --#TARGET.Object + + local coordinate=self:GetTargetVec2(Target) + + if coordinate then + return coordinate + end + + end + + self:E(self.lid..string.format("ERROR: Cannot get Vec2 of target %s", self.name)) + return nil +end + +--- Get 3D vector. +-- @param #TARGET self +-- @return DCS#Vec3 3D vector of the target. +function TARGET:GetVec3() + + for _,_target in pairs(self.targets) do + local Target=_target --#TARGET.Object + + local coordinate=self:GetTargetVec3(Target) + + if coordinate then + return coordinate + end + + end + + self:E(self.lid..string.format("ERROR: Cannot get Vec3 of target %s", self.name)) + return nil end --- Get coordinate. @@ -988,6 +1128,12 @@ function TARGET:GetCoordinate() return nil end +--- Get category. +-- @param #TARGET self +-- @return #string Target category. See `TARGET.Category.X`, where `X=AIRCRAFT, GROUND`. +function TARGET:GetCategory() + return self.category +end --- Get target category. -- @param #TARGET self diff --git a/Moose Development/Moose/Sound/SRS.lua b/Moose Development/Moose/Sound/SRS.lua index 8f27c6dbf..e1b8e5637 100644 --- a/Moose Development/Moose/Sound/SRS.lua +++ b/Moose Development/Moose/Sound/SRS.lua @@ -671,7 +671,7 @@ function MSRS:_GetCommand(freqs, modus, coal, gender, voice, culture, volume, sp else -- Add gender. if gender and gender~="female" then - command=command..string.format(" --gender=%s", tostring(gender)) + command=command..string.format(" -g %s", tostring(gender)) end -- Add culture. if culture and culture~="en-GB" then diff --git a/Moose Development/Moose/Utilities/Routines.lua b/Moose Development/Moose/Utilities/Routines.lua index 15e4cc1c8..eddd2c8dd 100644 --- a/Moose Development/Moose/Utilities/Routines.lua +++ b/Moose Development/Moose/Utilities/Routines.lua @@ -750,7 +750,7 @@ routines.groupRandomDistSelf = function(gpData, dist, form, heading, speed) local pos = routines.getLeadPos(gpData) local fakeZone = {} fakeZone.radius = dist or math.random(300, 1000) - fakeZone.point = {x = pos.x, y, pos.y, z = pos.z} + fakeZone.point = {x = pos.x, y = pos.y, z = pos.z} routines.groupToRandomZone(gpData, fakeZone, form, heading, speed) return diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 39ab3190f..ebd859320 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -970,6 +970,15 @@ function UTILS.VecDot(a, b) return a.x*b.x + a.y*b.y + a.z*b.z end +--- Calculate the [dot product](https://en.wikipedia.org/wiki/Dot_product) of two 2D vectors. The result is a number. +-- @param DCS#Vec2 a Vector in 2D with x, y components. +-- @param DCS#Vec2 b Vector in 2D with x, y components. +-- @return #number Scalar product of the two vectors a*b. +function UTILS.Vec2Dot(a, b) + return a.x*b.x + a.y*b.y +end + + --- Calculate the [euclidean norm](https://en.wikipedia.org/wiki/Euclidean_distance) (length) of a 3D vector. -- @param DCS#Vec3 a Vector in 3D with x, y, z components. -- @return #number Norm of the vector. @@ -977,6 +986,13 @@ function UTILS.VecNorm(a) return math.sqrt(UTILS.VecDot(a, a)) end +--- Calculate the [euclidean norm](https://en.wikipedia.org/wiki/Euclidean_distance) (length) of a 2D vector. +-- @param DCS#Vec2 a Vector in 2D with x, y components. +-- @return #number Norm of the vector. +function UTILS.Vec2Norm(a) + return math.sqrt(UTILS.Vec2Dot(a, a)) +end + --- Calculate the distance between two 2D vectors. -- @param DCS#Vec2 a Vector in 3D with x, y components. -- @param DCS#Vec2 b Vector in 3D with x, y components. @@ -1059,6 +1075,17 @@ function UTILS.VecHdg(a) return h end +--- Calculate "heading" of a 2D vector in the X-Y plane. +-- @param DCS#Vec2 a Vector in "D with x, y components. +-- @return #number Heading in degrees in [0,360). +function UTILS.Vec2Hdg(a) + local h=math.deg(math.atan2(a.y, a.x)) + if h<0 then + h=h+360 + end + return h +end + --- Calculate the difference between two "heading", i.e. angles in [0,360) deg. -- @param #number h1 Heading one. -- @param #number h2 Heading two. @@ -1095,6 +1122,22 @@ function UTILS.VecTranslate(a, distance, angle) return {x=TX, y=a.y, z=TY} end +--- Translate 2D vector in the 2D (x,z) plane. +-- @param DCS#Vec2 a Vector in 2D with x, y components. +-- @param #number distance The distance to translate. +-- @param #number angle Rotation angle in degrees. +-- @return DCS#Vec2 Translated vector. +function UTILS.Vec2Translate(a, distance, angle) + + local SX = a.x + local SY = a.y + local Radians=math.rad(angle or 0) + local TX=distance*math.cos(Radians)+SX + local TY=distance*math.sin(Radians)+SY + + return {x=TX, y=TY} +end + --- Rotate 3D vector in the 2D (x,z) plane. y-component (usually altitude) unchanged. -- @param DCS#Vec3 a Vector in 3D with x, y, z components. -- @param #number angle Rotation angle in degrees. @@ -1115,6 +1158,25 @@ function UTILS.Rotate2D(a, angle) return A end +--- Rotate 2D vector in the 2D (x,z) plane. +-- @param DCS#Vec2 a Vector in 2D with x, y components. +-- @param #number angle Rotation angle in degrees. +-- @return DCS#Vec2 Vector rotated in the (x,y) plane. +function UTILS.Vec2Rotate2D(a, angle) + + local phi=math.rad(angle) + + local x=a.x + local y=a.y + + local X=x*math.cos(phi)-y*math.sin(phi) + local Y=x*math.sin(phi)+y*math.cos(phi) + + local A={x=X, y=Y} + + return A +end + --- Converts a TACAN Channel/Mode couple into a frequency in Hz. -- @param #number TACANChannel The TACAN channel, i.e. the 10 in "10X". diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index 95c3e75af..db98fb789 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -142,7 +142,7 @@ AIRBASE.Caucasus = { -- * AIRBASE.Nevada.Pahute_Mesa_Airstrip -- * AIRBASE.Nevada.Tonopah_Airport -- * AIRBASE.Nevada.Tonopah_Test_Range_Airfield --- +-- -- @field Nevada AIRBASE.Nevada = { ["Creech_AFB"] = "Creech AFB", @@ -197,7 +197,7 @@ AIRBASE.Nevada = { -- * AIRBASE.Normandy.Funtington -- * AIRBASE.Normandy.Tangmere -- * AIRBASE.Normandy.Ford_AF --- +-- -- @field Normandy AIRBASE.Normandy = { ["Saint_Pierre_du_Mont"] = "Saint Pierre du Mont", @@ -271,7 +271,7 @@ AIRBASE.Normandy = { -- * AIRBASE.PersianGulf.Sirri_Island -- * AIRBASE.PersianGulf.Tunb_Island_AFB -- * AIRBASE.PersianGulf.Tunb_Kochak --- +-- -- @field PersianGulf AIRBASE.PersianGulf = { ["Abu_Dhabi_International_Airport"] = "Abu Dhabi Intl", @@ -473,6 +473,13 @@ AIRBASE.MarianaIslands={ -- @field #boolean Free This spot is currently free, i.e. there is no alive aircraft on it at the present moment. -- @field #number TerminalID0 Unknown what this means. If you know, please tell us! -- @field #number DistToRwy Distance to runway in meters. Currently bugged and giving the same number as the TerminalID. +-- @field #string AirbaseName Name of the airbase. +-- @field #number MarkerID Numerical ID of marker placed at parking spot. +-- @field Wrapper.Marker#MARKER Marker The marker on the F10 map. +-- @field #string ClientSpot Client unit sitting at this spot or *nil*. +-- @field #string Status Status of spot e.g. AIRBASE.SpotStatus.FREE. +-- @field #string OccupiedBy Name of the aircraft occupying the spot or "unknown". Can be *nil* if spot is not occupied. +-- @field #string ReservedBy Name of the aircraft for which this spot is reserved. Can be *nil* if spot is not reserved. --- Terminal Types of parking spots. See also https://wiki.hoggitworld.com/view/DCS_func_getParking -- @@ -507,6 +514,17 @@ AIRBASE.TerminalType = { FighterAircraft=244, } +--- Status of a parking spot. +-- @type AIRBASE.SpotStatus +-- @field #string FREE Spot is free. +-- @field #string OCCUPIED Spot is occupied. +-- @field #string RESERVED Spot is reserved. +AIRBASE.SpotStatus = { + FREE="Free", + OCCUPIED="Occupied", + RESERVED="Reserved", +} + --- Runway data. -- @type AIRBASE.Runway -- @field #number heading Heading of the runway in degrees. @@ -537,6 +555,9 @@ function AIRBASE:Register(AirbaseName) -- Get descriptors. self.descriptors=self:GetDesc() + -- Debug info. + --self:I({airbase=AirbaseName, descriptors=self.descriptors}) + -- Category. self.category=self.descriptors and self.descriptors.category or Airbase.Category.AIRDROME @@ -553,13 +574,15 @@ function AIRBASE:Register(AirbaseName) self.isShip=false self.category=Airbase.Category.HELIPAD _DATABASE:AddStatic(AirbaseName) - end + end else self:E("ERROR: Unknown airbase category!") end + -- Init parking spots. self:_InitParkingSpots() + -- Get 2D position vector. local vec2=self:GetVec2() -- Init coordinate. @@ -1024,6 +1047,8 @@ function AIRBASE:GetParkingSpotsTable(termtype) spot.Free=_isfree(_spot) -- updated spot.TOAC=_spot.TO_AC -- updated + spot.AirbaseName=self.AirbaseName + spot.ClientSpot=nil --TODO table.insert(spots, spot) @@ -1060,6 +1085,8 @@ function AIRBASE:GetFreeParkingSpotsTable(termtype, allowTOAC) spot.Free=true -- updated spot.TOAC=_spot.TO_AC -- updated + spot.AirbaseName=self.AirbaseName + spot.ClientSpot=nil --TODO table.insert(freespots, spot) @@ -1141,7 +1168,7 @@ end -- @param #table parkingdata (Optional) Parking spots data table. If not given it is automatically derived from the GetParkingSpotsTable() function. -- @return #table Table of coordinates and terminal IDs of free parking spots. Each table entry has the elements .Coordinate and .TerminalID. function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, scanunits, scanstatics, scanscenery, verysafe, nspots, parkingdata) - + -- Init default scanradius=scanradius or 50 if scanunits==nil then @@ -1197,7 +1224,7 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, az = 17 -- width end - + -- Number of spots we are looking for. Note that, e.g. grouping can require a number different from the group size! local _nspots=nspots or group:GetSize() @@ -1325,7 +1352,7 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, -- Retrun spots we found, even if there were not enough. return validspots - + end --- Check black and white lists. @@ -1344,7 +1371,7 @@ function AIRBASE:_CheckParkingLists(TerminalID) end end - + -- Check if a whitelist was defined. if self.parkingWhitelist and #self.parkingWhitelist>0 then for _,terminalID in pairs(self.parkingWhitelist or {}) do @@ -1454,7 +1481,7 @@ function AIRBASE:GetRunwayData(magvar, mark) name==AIRBASE.PersianGulf.Abu_Dhabi_International_Airport or name==AIRBASE.PersianGulf.Dubai_Intl or name==AIRBASE.PersianGulf.Shiraz_International_Airport or - name==AIRBASE.PersianGulf.Kish_International_Airport or + name==AIRBASE.PersianGulf.Kish_International_Airport or name==AIRBASE.MarianaIslands.Andersen_AFB then -- 1-->4, 2-->3, 3-->2, 4-->1 diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index e50d143e3..35ede51b9 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -1407,7 +1407,7 @@ function CONTROLLABLE:TaskEscort( FollowControllable, Vec3, LastWaypointIndex, E DCSTask = { id = 'Escort', params = { - groupId = FollowControllable:GetID(), + groupId = FollowControllable and FollowControllable:GetID() or nil, pos = Vec3, lastWptIndexFlag = LastWaypointIndex and true or false, lastWptIndex = LastWaypointIndex, @@ -1437,11 +1437,11 @@ function CONTROLLABLE:TaskFireAtPoint( Vec2, Radius, AmmoCount, WeaponType, Alti id = 'FireAtPoint', params = { point = Vec2, - x=Vec2.x, - y=Vec2.y, + x = Vec2.x, + y = Vec2.y, zoneRadius = Radius, radius = Radius, - expendQty = 100, -- dummy value + expendQty = 1, -- dummy value expendQtyEnabled = false, alt_type = ASL and 0 or 1 } @@ -1460,7 +1460,8 @@ function CONTROLLABLE:TaskFireAtPoint( Vec2, Radius, AmmoCount, WeaponType, Alti DCSTask.params.weaponType=WeaponType end - --self:I(DCSTask) + --env.info("FF fireatpoint") + --BASE:I(DCSTask) return DCSTask end @@ -2022,22 +2023,20 @@ do -- Patrol methods end ---- Return a Misson task to follow a given route defined by Points. +--- Return a "Misson" task to follow a given route defined by Points. -- @param #CONTROLLABLE self -- @param #table Points A table of route points. --- @return DCS#Task +-- @return DCS#Task DCS mission task. Has entries `.id="Mission"`, `params`, were params has entries `airborne` and `route`, which is a table of `points`. function CONTROLLABLE:TaskRoute( Points ) - self:F2( Points ) local DCSTask = { id = 'Mission', params = { - airborne = self:IsAir(), + airborne = self:IsAir(), -- This is important to make aircraft land without respawning them (which was a long standing DCS issue). route = {points = Points}, }, } - - self:T3( { DCSTask } ) + return DCSTask end diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index 72c9215ab..5659df13a 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -310,8 +310,7 @@ end --- Returns the @{DCS#Position3} position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission. -- @param Wrapper.Positionable#POSITIONABLE self --- @return DCS#Position The 3D position vectors of the POSITIONABLE. --- @return #nil The POSITIONABLE is not existing or alive. +-- @return DCS#Position The 3D position vectors of the POSITIONABLE or #nil if the groups not existing or alive. function GROUP:GetPositionVec3() -- Overridden from POSITIONABLE:GetPositionVec3() self:F2( self.PositionableName ) @@ -339,9 +338,7 @@ end -- If the first @{Wrapper.Unit} of the group is inactive, it will return false. -- -- @param #GROUP self --- @return #boolean true if the group is alive and active. --- @return #boolean false if the group is alive but inactive. --- @return #nil if the group does not exist anymore. +-- @return #boolean `true` if the group is alive *and* active, `false` if the group is alive but inactive or `#nil` if the group does not exist anymore. function GROUP:IsAlive() self:F2( self.GroupName ) @@ -363,8 +360,7 @@ end --- Returns if the group is activated. -- @param #GROUP self --- @return #boolean true if group is activated. --- @return #nil The group is not existing or alive. +-- @return #boolean `true` if group is activated or `#nil` The group is not existing or alive. function GROUP:IsActive() self:F2( self.GroupName ) @@ -412,7 +408,6 @@ function GROUP:Destroy( GenerateEvent, delay ) self:F2( self.GroupName ) if delay and delay>0 then - --SCHEDULER:New(nil, GROUP.Destroy, {self, GenerateEvent}, delay) self:ScheduleOnce(delay, GROUP.Destroy, self, GenerateEvent) else diff --git a/Moose Development/Moose/Wrapper/Identifiable.lua b/Moose Development/Moose/Wrapper/Identifiable.lua index 5b340aec8..4f3fabeae 100644 --- a/Moose Development/Moose/Wrapper/Identifiable.lua +++ b/Moose Development/Moose/Wrapper/Identifiable.lua @@ -56,8 +56,7 @@ end -- If the Identifiable is not alive, nil is returned. -- If the Identifiable is alive, true is returned. -- @param #IDENTIFIABLE self --- @return #boolean true if Identifiable is alive. --- @return #nil if the Identifiable is not existing or is not alive. +-- @return #boolean true if Identifiable is alive or `#nil` if the Identifiable is not existing or is not alive. function IDENTIFIABLE:IsAlive() self:F3( self.IdentifiableName ) @@ -77,11 +76,8 @@ end --- Returns DCS Identifiable object name. -- The function provides access to non-activated objects too. -- @param #IDENTIFIABLE self --- @return #string The name of the DCS Identifiable. --- @return #nil The DCS Identifiable is not existing or alive. +-- @return #string The name of the DCS Identifiable or `#nil`. function IDENTIFIABLE:GetName() - self:F2( self.IdentifiableName ) - local IdentifiableName = self.IdentifiableName return IdentifiableName end @@ -148,8 +144,7 @@ end --- Returns coalition of the Identifiable. -- @param #IDENTIFIABLE self --- @return DCS#coalition.side The side of the coalition. --- @return #nil The DCS Identifiable is not existing or alive. +-- @return DCS#coalition.side The side of the coalition or `#nil` The DCS Identifiable is not existing or alive. function IDENTIFIABLE:GetCoalition() self:F2( self.IdentifiableName ) @@ -190,8 +185,7 @@ end --- Returns country of the Identifiable. -- @param #IDENTIFIABLE self --- @return DCS#country.id The country identifier. --- @return #nil The DCS Identifiable is not existing or alive. +-- @return DCS#country.id The country identifier or `#nil` The DCS Identifiable is not existing or alive. function IDENTIFIABLE:GetCountry() self:F2( self.IdentifiableName ) @@ -222,8 +216,7 @@ end --- Returns Identifiable descriptor. Descriptor type depends on Identifiable category. -- @param #IDENTIFIABLE self --- @return DCS#Object.Desc The Identifiable descriptor. --- @return #nil The DCS Identifiable is not existing or alive. +-- @return DCS#Object.Desc The Identifiable descriptor or `#nil` The DCS Identifiable is not existing or alive. function IDENTIFIABLE:GetDesc() self:F2( self.IdentifiableName ) @@ -242,8 +235,7 @@ end --- Check if the Object has the attribute. -- @param #IDENTIFIABLE self -- @param #string AttributeName The attribute name. --- @return #boolean true if the attribute exists. --- @return #nil The DCS Identifiable is not existing or alive. +-- @return #boolean true if the attribute exists or `#nil` The DCS Identifiable is not existing or alive. function IDENTIFIABLE:HasAttribute( AttributeName ) self:F2( self.IdentifiableName ) @@ -266,8 +258,10 @@ function IDENTIFIABLE:GetCallsign() return '' end - +--- Gets the threat level. +-- @param #IDENTIFIABLE self +-- @return #number Threat level. +-- @return #string Type. function IDENTIFIABLE:GetThreatLevel() - return 0, "Scenery" end diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index d21d58cac..f1a704cc3 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -1368,8 +1368,6 @@ do -- Cargo return self.__.Cargo end - - --- Remove cargo. -- @param #POSITIONABLE self -- @param Core.Cargo#CARGO Cargo @@ -1414,17 +1412,15 @@ do -- Cargo return ItemCount end --- --- Get Cargo Bay Free Volume in m3. --- -- @param #POSITIONABLE self --- -- @return #number CargoBayFreeVolume --- function POSITIONABLE:GetCargoBayFreeVolume() --- local CargoVolume = 0 --- for CargoName, Cargo in pairs( self.__.Cargo ) do --- CargoVolume = CargoVolume + Cargo:GetVolume() --- end --- return self.__.CargoBayVolumeLimit - CargoVolume --- end --- + --- Get the number of infantry soldiers that can be embarked into an aircraft (airplane or helicopter). + -- Returns `nil` for ground or ship units. + -- @param #POSITIONABLE self + -- @return #number Descent number of soldiers that fit into the unit. Returns `#nil` for ground and ship units. + function POSITIONABLE:GetTroopCapacity() + local DCSunit=self:GetDCSObject() --DCS#Unit + local capacity=DCSunit:getDescentCapacity() + return capacity + end --- Get Cargo Bay Free Weight in kg. -- @param #POSITIONABLE self @@ -1443,55 +1439,97 @@ do -- Cargo return self.__.CargoBayWeightLimit - CargoWeight end --- --- Get Cargo Bay Volume Limit in m3. --- -- @param #POSITIONABLE self --- -- @param #number VolumeLimit --- function POSITIONABLE:SetCargoBayVolumeLimit( VolumeLimit ) --- self.__.CargoBayVolumeLimit = VolumeLimit --- end - --- Set Cargo Bay Weight Limit in kg. -- @param #POSITIONABLE self - -- @param #number WeightLimit + -- @param #number WeightLimit (Optional) Weight limit in kg. If not given, the value is taken from the descriptors or hard coded. function POSITIONABLE:SetCargoBayWeightLimit( WeightLimit ) - if WeightLimit then + if WeightLimit then + --- + -- User defined value + --- self.__.CargoBayWeightLimit = WeightLimit elseif self.__.CargoBayWeightLimit~=nil then -- Value already set ==> Do nothing! else - -- If weightlimit is not provided, we will calculate it depending on the type of unit. + --- + -- Weightlimit is not provided, we will calculate it depending on the type of unit. + --- + + -- Descriptors that contain the type name and for aircraft also weights. + local Desc = self:GetDesc() + self:F({Desc=Desc}) + + -- Unit type name. + local TypeName=Desc.typeName or "Unknown Type" -- When an airplane or helicopter, we calculate the weightlimit based on the descriptor. if self:IsAir() then - local Desc = self:GetDesc() - self:F({Desc=Desc}) + -- Max takeoff weight if DCS descriptors have unrealstic values. local Weights = { - ["C-17A"] = 35000, --77519 cannot be used, because it loads way too much apcs and infantry., - ["C-130"] = 22000 --The real value cannot be used, because it loads way too much apcs and infantry., + -- C-17A + -- Wiki says: max=265,352, empty=128,140, payload=77,516 (134 troops, 1 M1 Abrams tank, 2 M2 Bradley or 3 Stryker) + -- DCS says: max=265,350, empty=125,645, fuel=132,405 ==> Cargo Bay=7300 kg with a full fuel load (lot of fuel!) and 73300 with half a fuel load. + --["C-17A"] = 35000, --77519 cannot be used, because it loads way too much apcs and infantry. + -- C-130: + -- DCS says: max=79,380, empty=36,400, fuel=10,415 kg ==> Cargo Bay=32,565 kg with fuel load. + -- Wiki says: max=70,307, empty=34,382, payload=19,000 kg (92 passengers, 2-3 Humvees or 2 M113s), max takeoff weight 70,037 kg. + -- Here we say two M113s should be transported. Each one weights 11,253 kg according to DCS. So the cargo weight should be 23,000 kg with a full load of fuel. + -- This results in a max takeoff weight of 69,815 kg (23,000+10,415+36,400), which is very close to the Wiki value of 70,037 kg. + ["C-130"] = 70000, } - - self.__.CargoBayWeightLimit = Weights[Desc.typeName] or ( Desc.massMax - ( Desc.massEmpty + Desc.fuelMassMax ) ) + + -- Max (takeoff) weight (empty+fuel+cargo weight). + local massMax= Desc.massMax or 0 + + -- Adjust value if set above. + local maxTakeoff=Weights[TypeName] + if maxTakeoff then + massMax=maxTakeoff + end + + -- Empty weight. + local massEmpty=Desc.massEmpty or 0 + + -- Fuel. The descriptor provides the max fuel mass in kg. This needs to be multiplied by the relative fuel amount to calculate the actual fuel mass on board. + local massFuelMax=Desc.fuelMassMax or 0 + local relFuel=math.max(self:GetFuel() or 1.0, 1.0) -- We take 1.0 as max in case of external fuel tanks. + local massFuel=massFuelMax*relFuel + + -- Number of soldiers according to DCS function + --local troopcapacity=self:GetTroopCapacity() or 0 + + -- Calculate max cargo weight, which is the max (takeoff) weight minus the empty weight minus the actual fuel weight. + local CargoWeight=massMax-(massEmpty+massFuel) + + -- Debug info. + self:T(string.format("Setting Cargo bay weight limit [%s]=%d kg (Mass max=%d, empty=%d, fuelMax=%d kg (rel=%.3f), fuel=%d kg", TypeName, CargoWeight, massMax, massEmpty, massFuelMax, relFuel, massFuel)) + --self:T(string.format("Descent Troop Capacity=%d ==> %d kg (for 95 kg soldier)", troopcapacity, troopcapacity*95)) + + -- Set value. + self.__.CargoBayWeightLimit = CargoWeight + elseif self:IsShip() then - local Desc = self:GetDesc() - self:F({Desc=Desc}) + -- Hard coded cargo weights in kg. local Weights = { - ["Type_071"] = 245000, - ["LHA_Tarawa"] = 500000, - ["Ropucha-class"] = 150000, - ["Dry-cargo ship-1"] = 70000, - ["Dry-cargo ship-2"] = 70000, - ["Higgins_boat"] = 3700, -- Higgins Boat can load 3700 kg of general cargo or 36 men (source wikipedia). - ["USS_Samuel_Chase"] = 25000, -- Let's say 25 tons for now. Wiki says 33 Higgins boats, which would be 264 tons (can't be right!) and/or 578 troops. - ["LST_Mk2"] =2100000, -- Can carry 2100 tons according to wiki source! + ["Type_071"] = 245000, + ["LHA_Tarawa"] = 500000, + ["Ropucha-class"] = 150000, + ["Dry-cargo ship-1"] = 70000, + ["Dry-cargo ship-2"] = 70000, + ["Higgins_boat"] = 3700, -- Higgins Boat can load 3700 kg of general cargo or 36 men (source wikipedia). + ["USS_Samuel_Chase"] = 25000, -- Let's say 25 tons for now. Wiki says 33 Higgins boats, which would be 264 tons (can't be right!) and/or 578 troops. + ["LST_Mk2"] = 2100000, -- Can carry 2100 tons according to wiki source! + ["speedboat"] = 500, -- 500 kg ~ 5 persons + ["Seawise_Giant"] =261000000, -- Gross tonnage is 261,000 tonns. } - self.__.CargoBayWeightLimit = ( Weights[Desc.typeName] or 50000 ) + self.__.CargoBayWeightLimit = ( Weights[TypeName] or 50000 ) else - local Desc = self:GetDesc() + -- Hard coded number of soldiers. local Weights = { ["AAV7"] = 25, ["Bedford_MWD"] = 8, -- new by kappa @@ -1537,12 +1575,28 @@ do -- Cargo ["VAB_Mephisto"] = 8, -- new by Apple } - local CargoBayWeightLimit = ( Weights[Desc.typeName] or 0 ) * 95 + -- Assuming that each passenger weighs 95 kg on average. + local CargoBayWeightLimit = ( Weights[TypeName] or 0 ) * 95 + self.__.CargoBayWeightLimit = CargoBayWeightLimit end end + self:F({CargoBayWeightLimit = self.__.CargoBayWeightLimit}) end + + --- Get Cargo Bay Weight Limit in kg. + -- @param #POSITIONABLE self + -- @return #number Max cargo weight in kg. + function POSITIONABLE:GetCargoBayWeightLimit() + + if self.__.CargoBayWeightLimit==nil then + self:SetCargoBayWeightLimit() + end + + return self.__.CargoBayWeightLimit + end + end --- Cargo --- Signal a flare at the position of the POSITIONABLE. diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index 01f7ac4e7..680dbb318 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -524,6 +524,63 @@ function UNIT:IsTanker() return tanker, system end +--- Check if the unit can supply ammo. Currently, we have +-- +-- * M 818 +-- * Ural-375 +-- * ZIL-135 +-- +-- This list needs to be extended, if DCS adds other units capable of supplying ammo. +-- +-- @param #UNIT self +-- @return #boolean If `true`, unit can supply ammo. +function UNIT:IsAmmoSupply() + + -- Type name is the only thing we can check. There is no attribute (Sep. 2021) which would tell us. + local typename=self:GetTypeName() + + if typename=="M 818" then + -- Blue ammo truck. + return true + elseif typename=="Ural-375" then + -- Red ammo truck. + return true + elseif typename=="ZIL-135" then + -- Red ammo truck. Checked that it can also provide ammo. + return true + end + + return false +end + +--- Check if the unit can supply fuel. Currently, we have +-- +-- * M978 HEMTT Tanker +-- * ATMZ-5 +-- * ATMZ-10 +-- * ATZ-5 +-- +-- This list needs to be extended, if DCS adds other units capable of supplying fuel. +-- +-- @param #UNIT self +-- @return #boolean If `true`, unit can supply fuel. +function UNIT:IsFuelSupply() + + -- Type name is the only thing we can check. There is no attribute (Sep. 2021) which would tell us. + local typename=self:GetTypeName() + + if typename=="M978 HEMTT Tanker" then + return true + elseif typename=="ATMZ-5" then + return true + elseif typename=="ATMZ-10" then + return true + elseif typename=="ATZ-5" then + return true + end + + return false +end --- Returns the unit's group if it exist and nil otherwise. -- @param Wrapper.Unit#UNIT self @@ -999,7 +1056,7 @@ function UNIT:GetThreatLevel() elseif ( Attributes["Tanks"] or Attributes["IFV"] ) and not Attributes["ATGM"] then ThreatLevel = 3 elseif Attributes["Old Tanks"] or Attributes["APC"] or Attributes["Artillery"] then ThreatLevel = 2 - elseif Attributes["Infantry"] then ThreatLevel = 1 + elseif Attributes["Infantry"] or Attributes["EWR"] then ThreatLevel = 1 end ThreatText = ThreatLevels[ThreatLevel+1] diff --git a/Moose Setup/Moose.files b/Moose Setup/Moose.files index 4f0e34d30..7085a12e9 100644 --- a/Moose Setup/Moose.files +++ b/Moose Setup/Moose.files @@ -79,9 +79,15 @@ Ops/Auftrag.lua Ops/OpsGroup.lua Ops/FlightGroup.lua Ops/NavyGroup.lua +Ops/Cohort.lua Ops/Squadron.lua +Ops/Platoon.lua +Ops/Legion.lua Ops/AirWing.lua +Ops/Brigade.lua Ops/Intelligence.lua +Ops/Commander.lua +Ops/Chief.lua Ops/CSAR.lua Ops/CTLD.lua