diff --git a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua index a4ad52423..13b92ec50 100644 --- a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua @@ -2361,7 +2361,7 @@ do -- AI_A2A_DISPATCHER end --- Set flashing player messages on or off - -- @param #AI_A2G_DISPATCHER self + -- @param #AI_A2A_DISPATCHER self -- @param #boolean onoff Set messages on (true) or off (false) function AI_A2A_DISPATCHER:SetSendMessages( onoff ) self.SetSendPlayerMessages = onoff diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index ae40ec4ca..2e96efb67 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -194,7 +194,12 @@ do -- COORDINATE -- ## 9) Coordinate text generation -- -- * @{#COORDINATE.ToStringBR}(): Generates a Bearing & Range text in the format of DDD for DI where DDD is degrees and DI is distance. - -- * @{#COORDINATE.ToStringLL}(): Generates a Latutude & Longutude text. + -- * @{#COORDINATE.ToStringBRA}(): Generates a Bearing, Range & Altitude text. + -- * @{#COORDINATE.ToStringBRAANATO}(): Generates a Generates a Bearing, Range, Aspect & Altitude text in NATOPS. + -- * @{#COORDINATE.ToStringLL}(): Generates a Latutide & Longitude text. + -- * @{#COORDINATE.ToStringLLDMS}(): Generates a Lat, Lon, Degree, Minute, Second text. + -- * @{#COORDINATE.ToStringLLDDM}(): Generates a Lat, Lon, Degree, decimal Minute text. + -- * @{#COORDINATE.ToStringMGRS}(): Generates a MGRS grid coordinate text. -- -- ## 10) Drawings on F10 map -- @@ -1112,25 +1117,28 @@ do -- COORDINATE -- @param #COORDINATE self -- @param #number Distance The distance in meters. -- @param Core.Settings#SETTINGS Settings + -- @param #string Language (optional) "EN" or "RU" + -- @param #number Precision (optional) round to this many decimal places -- @return #string The distance text expressed in the units of measurement. - function COORDINATE:GetDistanceText( Distance, Settings, Language ) + function COORDINATE:GetDistanceText( Distance, Settings, Language, Precision ) local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS local Language = Language or "EN" - + local Precision = Precision or 0 + local DistanceText if Settings:IsMetric() then if Language == "EN" then - DistanceText = " for " .. UTILS.Round( Distance / 1000, 2 ) .. " km" + DistanceText = " for " .. UTILS.Round( Distance / 1000, Precision ) .. " km" elseif Language == "RU" then - DistanceText = " за " .. UTILS.Round( Distance / 1000, 2 ) .. " километров" + DistanceText = " за " .. UTILS.Round( Distance / 1000, Precision ) .. " километров" end else if Language == "EN" then - DistanceText = " for " .. UTILS.Round( UTILS.MetersToNM( Distance ), 2 ) .. " miles" + DistanceText = " for " .. UTILS.Round( UTILS.MetersToNM( Distance ), Precision ) .. " miles" elseif Language == "RU" then - DistanceText = " за " .. UTILS.Round( UTILS.MetersToNM( Distance ), 2 ) .. " миль" + DistanceText = " за " .. UTILS.Round( UTILS.MetersToNM( Distance ), Precision ) .. " миль" end end @@ -1208,7 +1216,7 @@ do -- COORDINATE local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS local BearingText = self:GetBearingText( AngleRadians, 0, Settings, Language ) - local DistanceText = self:GetDistanceText( Distance, Settings, Language ) + local DistanceText = self:GetDistanceText( Distance, Settings, Language, 0 ) local BRText = BearingText .. DistanceText @@ -1226,7 +1234,7 @@ do -- COORDINATE local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS local BearingText = self:GetBearingText( AngleRadians, 0, Settings, Language ) - local DistanceText = self:GetDistanceText( Distance, Settings, Language ) + local DistanceText = self:GetDistanceText( Distance, Settings, Language, 0 ) local AltitudeText = self:GetAltitudeText( Settings, Language ) local BRAText = BearingText .. DistanceText .. AltitudeText -- When the POINT is a VEC2, there will be no altitude shown. @@ -2756,7 +2764,44 @@ do -- COORDINATE local Altitude = self:GetAltitudeText() return "BRA, " .. self:GetBRAText( AngleRadians, Distance, Settings, Language ) end + + --- Create a BRAA NATO call string to this COORDINATE from the FromCOORDINATE. + -- @param #COORDINATE self + -- @param #COORDINATE FromCoordinate The coordinate to measure the distance and the bearing from. + -- @param #boolean Spades Add "Spades" at the end if true (no IFF/VID ID yet known) + -- @return #string The BRAA text. + function COORDINATE:ToStringBRAANATO(FromCoordinate,Spades) + + -- Thanks to @Pikey + local BRAANATO = "Merged." + local currentCoord = FromCoordinate + local DirectionVec3 = FromCoordinate:GetDirectionVec3( self ) + local AngleRadians = self:GetAngleRadians( DirectionVec3 ) + + local bearing = UTILS.Round( UTILS.ToDegree( AngleRadians ),0 ) + + local rangeMetres = self:Get2DDistance(currentCoord) + local rangeNM = UTILS.Round( UTILS.MetersToNM(rangeMetres), 0) + + local aspect = self:ToStringAspect(currentCoord) + + local alt = UTILS.Round(UTILS.MetersToFeet(self.y)/1000,0)--*1000 + + local track = Utils.BearingToCardinal(bearing) + + if rangeNM > 3 then + BRAANATO = string.format("BRAA, %s, %d miles, Angels %d, %s, Track %s",bearing, rangeNM, alt, aspect, track) + if Spades then + BRAANATO = BRAANATO..", Spades." + else + BRAANATO = BRAANATO.."." + end + end + + return BRAANATO + end + --- Return a BULLS string out of the BULLS of the coalition to the COORDINATE. -- @param #COORDINATE self -- @param DCS#coalition.side Coalition The coalition. diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index 0127bd185..e5c77f044 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -1029,7 +1029,7 @@ do -- SET_GROUP --- Gets the Set. -- @param #SET_GROUP self - -- @return #SET_GROUP self + -- @return #table Table of objects function SET_GROUP:GetAliveSet() self:F2() diff --git a/Moose Development/Moose/Core/TextAndSound.lua b/Moose Development/Moose/Core/TextAndSound.lua index 8a9ea69b5..f56015fe2 100644 --- a/Moose Development/Moose/Core/TextAndSound.lua +++ b/Moose Development/Moose/Core/TextAndSound.lua @@ -1,5 +1,7 @@ --- **Core** - TEXTANDSOUND (MOOSE gettext) system -- +-- === +-- -- ## Main Features: -- -- * A GetText for Moose @@ -18,7 +20,8 @@ -- ## Date: April 2022 -- -- === --- @module Core.TEXTANDSOUND +-- +-- @module Core.TextAndSound -- @image MOOSE.JPG --- Text and Sound class. diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index 6b6bd46e1..e7359808f 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -810,7 +810,7 @@ end --- Scan the zone for the presence of units of the given ObjectCategories. --- Note that after a zone has been scanned, the zone can be evaluated by: +-- Note that **only after** a zone has been scanned, the zone can be evaluated by: -- -- * @{ZONE_RADIUS.IsAllInZoneOfCoalition}(): Scan the presence of units in the zone of a coalition. -- * @{ZONE_RADIUS.IsAllInZoneOfOtherCoalition}(): Scan the presence of units in the zone of an other coalition. @@ -819,10 +819,10 @@ end -- * @{ZONE_RADIUS.IsNoneInZone}(): Scan if the zone is empty. -- @{#ZONE_RADIUS. -- @param #ZONE_RADIUS self --- @param ObjectCategories An array of categories of the objects to find in the zone. --- @param UnitCategories An array of unit categories of the objects to find in the zone. +-- @param ObjectCategories An array of categories of the objects to find in the zone. E.g. `{Object.Category.UNIT}` +-- @param UnitCategories An array of unit categories of the objects to find in the zone. E.g. `{Unit.Category.GROUND_UNIT,Unit.Category.SHIP}` -- @usage --- self.Zone:Scan() +-- self.Zone:Scan({Object.Category.UNIT},{Unit.Category.GROUND_UNIT}) -- local IsAttacked = self.Zone:IsSomeInZoneOfCoalition( self.Coalition ) function ZONE_RADIUS:Scan( ObjectCategories, UnitCategories ) diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index 3b5e08a74..e81fde9af 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -4,6 +4,7 @@ __Moose.Include( 'Scripts/Moose/Utilities/Utils.lua' ) __Moose.Include( 'Scripts/Moose/Utilities/Profiler.lua' ) __Moose.Include( 'Scripts/Moose/Utilities/Templates.lua' ) __Moose.Include( 'Scripts/Moose/Utilities/STTS.lua' ) +__Moose.Include( 'Scripts/Moose/Utilities/FiFo.lua' ) __Moose.Include( 'Scripts/Moose/Core/Base.lua' ) __Moose.Include( 'Scripts/Moose/Core/Beacon.lua' ) diff --git a/Moose Development/Moose/Ops/AirWing.lua b/Moose Development/Moose/Ops/AirWing.lua index 44750e7bc..102955153 100644 --- a/Moose Development/Moose/Ops/AirWing.lua +++ b/Moose Development/Moose/Ops/AirWing.lua @@ -1239,6 +1239,26 @@ function AIRWING:GetTankerForFlight(flightgroup) return nil end +--- Add the ability to call back an Ops.Awacs#AWACS object with an FSM call "FlightOnMission(FlightGroup, Mission)". +-- @param #AIRWING self +-- @param Ops.Awacs#AWACS ConnectecdAwacs +-- @return #AIRWING self +function AIRWING:SetUsingOpsAwacs(ConnectecdAwacs) + self:I(self.lid .. "Added AWACS Object: "..ConnectecdAwacs:GetName() or "unknown") + self.UseConnectedOpsAwacs = true + self.ConnectedOpsAwacs = ConnectecdAwacs + return self +end + +--- Remove the ability to call back an Ops.Awacs#AWACS object with an FSM call "FlightOnMission(FlightGroup, Mission)". +-- @param #AIRWING self +-- @return #AIRWING self +function AIRWING:RemoveUsingOpsAwacs() + self:I(self.lid .. "Reomve AWACS Object: "..self.ConnectedOpsAwacs:GetName() or "unknown") + self.UseConnectedOpsAwacs = false + return self +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- FSM Events ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1248,11 +1268,14 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param Ops.FlightGroup#FLIGHTGROUP ArmyGroup Ops army group on mission. +-- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup Ops flight group on mission. -- @param Ops.Auftrag#AUFTRAG Mission The requested mission. 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())) + self:T(self.lid..string.format("Group %s on %s mission %s", FlightGroup:GetName(), Mission:GetType(), Mission:GetName())) + if self.UseConnectedOpsAwacs and self.ConnectedOpsAwacs then + self.ConnectedOpsAwacs:__FlightOnMission(2,FlightGroup,Mission) + end end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/Auftrag.lua b/Moose Development/Moose/Ops/Auftrag.lua index 0ea0b42bb..87c481c84 100644 --- a/Moose Development/Moose/Ops/Auftrag.lua +++ b/Moose Development/Moose/Ops/Auftrag.lua @@ -2031,15 +2031,16 @@ function AUFTRAG:NewNOTHING() --mission:_TargetFromObject(Coordinate) - mission.optionROE=ENUMS.ROE.ReturnFire - mission.optionAlarm=ENUMS.AlarmState.Auto + mission.optionROE=ENUMS.ROE.WeaponHold + mission.optionAlarm=ENUMS.AlarmState.Green mission.missionFraction=1.0 mission.categories={AUFTRAG.Category.ALL} mission.DCStask=mission:GetDCSMissionTask() - + mission.DCStask.params.adinfinitum=true + return mission end @@ -5403,8 +5404,25 @@ function AUFTRAG:GetDCSMissionTask(TaskControllable) DCStask.params=param table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.NOTHING then - elseif self.type==AUFTRAG.Type.HOVER then + --------------------- + -- NOTHING Mission -- + --------------------- + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.NOTHING + + -- 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.HOVER then --------------------- -- HOVER Mission -- diff --git a/Moose Development/Moose/Ops/Awacs.lua b/Moose Development/Moose/Ops/Awacs.lua new file mode 100644 index 000000000..995402c63 --- /dev/null +++ b/Moose Development/Moose/Ops/Awacs.lua @@ -0,0 +1,2808 @@ +--- **Ops** - AWACS +-- +-- === +-- +-- ## Main Features: +-- +-- * TBD +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/). +-- +-- === +-- +-- ### Author: **applevangelist** +-- Last Update April 2022 +-- +-- === +-- @module Ops.AWACS +-- @image MOOSE.JPG + +do +--- Ops AWACS Class +-- @type AWACS +-- @field #string ClassName Name of this class. +-- @field #string version Versioning. +-- @field #string lid LID for log entries. +-- @field #number coalition Colition side. +-- @field #string coalitiontxt = "blue" +-- @field Core.Zone#ZONE OpsZone, +-- @field Core.Zone#ZONE AnchorZone, +-- @field #number Frequency +-- @field #number Modulation +-- @field Wrapper.Airbase#AIRBASE Airbase +-- @field Ops.AirWing#AIRWING AirWing +-- @field #number AwacsAngels +-- @field Core.Zone#ZONE OrbitZone +-- @field #number CallSign +-- @field #number CallSignNo +-- @field #boolean debug +-- @field #number verbose +-- @field #table ManagedGrps +-- @field #number ManagedGrpID +-- @field #number ManagedTaskID +-- @field Utilities.FiFo#FIFO AnchorStacks +-- @field Utilities.FiFo#FIFO CAPIdleAI +-- @field Utilities.FiFo#FIFO CAPIdleHuman +-- @field Utilities.FiFo#FIFO TaskedCAPAI +-- @field Utilities.FiFo#FIFO TaskedCAPHuman +-- @field Utilities.FiFo#FIFO OpenTasks +-- @field Utilities.FiFo#FIFO AssignedTasks +-- @field Utilities.FiFo#FIFO PictureAO +-- @field Utilities.FiFo#FIFO PictureEWR +-- @field Utilities.FiFo#FIFO Contacts +-- @field Utilities.FiFo#FIFO ContactsAO +-- @field Utilities.FiFo#FIFO RadioQueue +-- @field Utilities.FiFo#FIFO CAPAirwings +-- @field #number AwacsTimeOnStation +-- @field #number AwacsTimeStamp +-- @field #number EscortsTimeOnStation +-- @field #number EscortsTimeStamp +-- @field #string AwacsROE +-- @field #string AwacsROT +-- @field Ops.Auftrag#AUFTRAG AwacsMission +-- @field Ops.Auftrag#AUFTRAG EscortMission +-- @field Ops.Auftrag#AUFTRAG AwacsMissionReplacement +-- @field Ops.Auftrag#AUFTRAG EscortMissionReplacement +-- @field Utilities.FiFo#FIFO AICAPMissions FIFO for Ops.Auftrag#AUFTRAG for AI CAP +-- @field #boolean MenuStrict +-- @field #number MaxAIonCAP +-- @field #number AIonCAP +-- @field #boolean ShiftChangeAwacsFlag +-- @field #boolean ShiftChangeEscortsFlag +-- @field #boolean ShiftChangeAwacsRequested +-- @field #boolean ShiftChangeEscortsRequested +-- @extends Core.Fsm#FSM + +--- +-- +-- @field #AWACS +AWACS = { + ClassName = "AWACS", -- #string + version = "alpha 0.0.3", -- #string + lid = "", -- #string + coalition = coalition.side.BLUE, -- #number + coalitiontxt = "blue", -- #string + OpsZone = nil, + AnchorZone = nil, + AirWing = nil, + Frequency = 271, -- #number + Modulation = radio.modulation.AM, -- #number + Airbase = nil, + AwacsAngels = 25, -- orbit at 25'000 ft + OrbitZone = nil, + CallSign = CALLSIGN.AWACS.Magic, -- #number + CallSignNo = 1, -- #number + debug = true, + verbose = true, + ManagedGrps = {}, + ManagedGrpID = 0, -- #number + ManagedTaskID = 0, -- #number + AnchorStacks = {}, -- Utilities.FiFo#FIFO + CAPIdleAI = {}, + CAPIdleHuman = {}, + TaskedCAPAI = {}, + TaskedCAPHuman = {}, + OpenTasks = {}, -- Utilities.FiFo#FIFO + AssignedTasks = {}, -- Utilities.FiFo#FIFO + PictureAO = {}, -- Utilities.FiFo#FIFO + PictureEWR = {}, -- Utilities.FiFo#FIFO + Contacts = {}, -- Utilities.FiFo#FIFO + ContactsAO = {}, -- Utilities.FiFo#FIFO + RadioQueue = {}, -- Utilities.FiFo#FIFO + AwacsTimeOnStation = 1, + AwacsTimeStamp = 0, + EscortsTimeOnStation = 0.5, + EscortsTimeStamp = 0, + AwacsROE = "", + AwacsROT = "", + MenuStrict = true, + MaxAIonCAP = 4, + AIonCAP = 0, + AICAPMissions = {}, -- Utilities.FiFo#FIFO + ShiftChangeAwacsFlag = false, + ShiftChangeEscortsFlag = false, + ShiftChangeAwacsRequested = false, + ShiftChangeEscortsRequested = false, + CAPAirwings = {}, -- Utilities.FiFo#FIFO +} + +--- +--@field CallSignClear +AWACS.CallSignClear = { + [1]="Overlord", + [2]="Magic", + [3]="Wizard", + [4]="Focus", + [5]="Darkstar", +} + +--- +-- @field AnchorNames +AWACS.AnchorNames = { + [1] = "One", + [2] = "Two", + [3] = "Three", + [4] = "Four", + [5] = "Five", + [6] = "Six", + [7] = "Seven", + [8] = "Eight", + [9] = "Nine", + [10] = "Ten", +} + +--- +-- @field ROE +AWACS.ROE = { + POLICE = "Police", + VID = "Visual ID", + IFF = "IFF", + BVR = "Beyond Visual Range", +} + +--- +-- @field AWACS.ROT +AWACS.ROT = { + PASSIVE = "Passive Defense", + ACTIVE = "Active Defense", + LOCK = "Lock", + RETURNFIRE = "Return Fire", + OPENFIRE = "Open Fire", + } + +--- +--@field THREATLEVEL -- can be 1-10, thresholds +AWACS.THREATLEVEL = { + GREEN = 3, + AMBER = 7, + RED = 10, +} + +--- +-- @type AWACS.MenuStructure +-- @field #boolean menuset +-- @field #string groupname +-- @field Core.Menu#MENU_GROUP basemenu +-- @field Core.Menu#MENU_GROUP_COMMAND checkin +-- @field Core.Menu#MENU_GROUP_COMMAND checkout +-- @field Core.Menu#MENU_GROUP_COMMAND picture +-- @field Core.Menu#MENU_GROUP_COMMAND bogeydope +-- @field Core.Menu#MENU_GROUP_COMMAND declare +-- @field Core.Menu#MENU_GROUP_COMMAND showtask + +--- +-- @type AWACS.ManagedGroup +-- @field Wrapper.Group#GROUP Group +-- @field #string GroupName +-- @field Ops.FlightGroup#FLIGHTGROUP FlightGroup for AI +-- @field #boolean IsPlayer +-- @field #boolean IsAI +-- @field #string CallSign +-- @field #number CurrentTask +-- @field #boolean HasAssignedTask +-- @field #number GID +-- @field #number AnchorStackNo +-- @field #number AnchorStackAngels + +--- +-- @type AWACS.TaskDescription +AWACS.TaskDescription = { + ANCHOR = "Anchor", + REANCHOR = "Re-Anchor", + INTERCEPT = "Intercept", + SWEEP = "Sweep", + RTB = "RTB", +} + +--- +-- @type AWACS.TaskStatus +AWACS.TaskStatus = { + IDLE = "Idle", + ASSIGNED = "Assigned", + EXECUTING = "Executing", + SUCCESS = "Success", + FAILED = "Failed", +} + +--- +-- @type AWACS.ManagedTask +-- @field #number TID +-- @field #number AssignedGroupID +-- @field #boolean IsPlayerTask +-- @field #boolean IsUnassigned +-- @field Ops.Target#TARGET Target +-- @field Ops.Auftrag#AUFTRAG Auftrag +-- @field #AWACS.TaskStatus Status +-- @field #AWACS.TaskDescription ToDo +-- @field #string ScreenText Long descrition +-- @field Ops.Intelligence#INTEL.Contact Contact +-- @field Ops.Intelligence#INTEL.Cluster Cluster + +--- +-- @type AWACS.AnchorAssignedEntry +-- @field #number ID +-- @field #number Angels + +--- +-- @type AWACS.AnchorData +-- @field #number AnchorBaseAngels +-- @field #boolean AnchorZone +-- @field Core.Point#COORDINATE AnchorZoneCoordinate +-- @field #boolean AnchorZoneCoordinateText +-- @field Utilities.FiFo#FIFO AnchorAssignedID FiFo of #AWACS.AnchorAssignedEntry +-- @field Utilities.FiFo#FIFO Anchors FiFo of available stacks + +--- +--@type RadioEntry +--@field #string TextTTS +--@field #string TextScreen +--@field #boolean IsNew +--@field #boolean IsGroup +--@field #boolean GroupID +--@field #number Duration +--@field #boolean ToScreen +--@field #boolean FromAI + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO-List +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- DEBUG - Escorts via AirWing not staying on +-- TODO - Link (multiple) AWs to the AWACS Controller +-- DONE - Use AO as Anchor of Bulls, AO as default +-- DONE - SRS TTS output +-- DONE - Check-In/Out Humans +-- DONE - Check-In/Out AI +-- DONE - Picture +-- TODO - TripWire +-- DONE - Radio Menu +-- DONE - Intel Detection +-- TODO - CHIEF / COMMANDER / AIRWING connection? +-- TODO - LotATC / IFF +-- DONE - ROE +-- TODO - Player & AI tasking +-- DONE - Anchor Stack Management +-- TODO - Reporting +-- TODO - Missile launch callout +-- TODO - Localization +-- DONE - Shift Length AWACS/AI +-- DEBUG - Shift Change, Change on asset RTB or dead or mission done +-- TODO - Borders for INTEL +-- TODO - FIFO for checkin/checkout and tasking +-- TODO - Event detection +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO Constructor + +--- Set up a new AI AWACS. +-- @param #AWACS self +-- @param #string Name Name of this AWACS for the radio menu. +-- @param #string AirWing The core Ops.AirWing#AIRWING managing the AWACS, Escort and (optionally) AI CAP planes for us. +-- @param #number Coalition Coalition, e.g. coalition.side.BLUE. Can also be passed as "blue", "red" or "neutral". +-- @param #string AirbaseName Name of the home airbase. +-- @param #string AwacsOrbit Name of the round, mission editor created zone where this AWACS orbits. +-- @param #string OpsZone Name of the round, mission editor created operations zone this AWACS controls. Can be passed as #ZONE_POLYGON +-- @param #string AnchorZone Name of the round, mission editor created anchor zone where CAP groups will be stacked. +-- @param #number Frequency Radio frequency, e.g. 271. +-- @param #number Modulation Radio modulation, e.g. radio.modulation.AM or radio.modulation.FM. +-- @return #AWACS self +function AWACS:New(Name,AirWing,Coalition,AirbaseName,AwacsOrbit,OpsZone,AnchorZone,Frequency,Modulation) + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) + + --set Coalition + if Coalition and type(Coalition)=="string" then + if Coalition=="blue" then + self.coalition=coalition.side.BLUE + self.coalitiontxt = Coalition + elseif Coalition=="red" then + self.coalition=coalition.side.RED + self.coalitiontxt = Coalition + elseif Coalition=="neutral" then + self.coalition=coalition.side.NEUTRAL + self.coalitiontxt = Coalition + else + self:E("ERROR: Unknown coalition in AWACS!") + end + else + self.coalition = Coalition + self.coalitiontxt = string.lower(UTILS.GetCoalitionName(self.coalition)) + end + + -- base setup + self.Name = Name -- #string + self.AirWing = AirWing -- Ops.AirWing#AIRWING object + + AirWing:SetUsingOpsAwacs(self) + + self.CAPAirwings = FIFO:New() -- Utilities.FiFo#FIFO + self.CAPAirwings:Push(AirWing,1) + + self.AwacsFG = nil + --self.AwacsPayload = PayLoad -- Ops.AirWing#AIRWING.Payload + self.ModernEra = true -- use of EPLRS + self.RadarBlur = 10 -- 10% detection precision i.e. 90-110 reported group size + if type(OpsZone) == "string" then + self.OpsZone = ZONE:New(OpsZone) -- Core.Zone#ZONE + elseif type(OpsZone) == "table" and OpsZone.ClassName and OpsZone.ClassName == "ZONE_POLYGON" then + self.OpsZone = OpsZone + else + self:E("AWACS - Invalid OpsZone passed!") + return + end + + self.AOCoordinate = self.OpsZone:GetCoordinate() + self.UseBullsAO = false + self.ControlZoneRadius = 100 -- nm + self.AnchorZone = ZONE:New(AnchorZone) -- Core.Zone#ZONE + self.Frequency = Frequency or 271 -- #number + self.Modulation = Modulation or radio.modulation.AM + self.Airbase = AIRBASE:FindByName(AirbaseName) + self.AwacsAngels = 25 -- orbit at 25'000 ft + self.OrbitZone = ZONE:New(AwacsOrbit) -- Core.Zone#ZONE + self.CallSign = CALLSIGN.AWACS.Magic -- #number + self.CallSignNo = 1 -- #number + self.NoHelos = true + self.MaxAIonCAP = 4 + self.AIonCAP = 0 + self.AICAPMissions = FIFO:New() -- Utilities.FiFo#FIFO + + local speed = 250 + self.SpeedBase = speed + self.Speed = UTILS.KnotsToAltKIAS(speed,self.AwacsAngels*1000) + self.CapSpeedBase = 220 + self.Heading = 0 -- north + self.Leg = 50 -- nm + self.invisible = true + self.immortal = true + self.callsigntxt = "AWACS" + + self.AwacsTimeOnStation = 2 + self.AwacsTimeStamp = 0 + self.EscortsTimeOnStation = 2 + self.EscortsTimeStamp = 0 + self.ShiftChangeTime = 0.25 -- 15mins + self.ShiftChangeAwacsFlag = false + self.ShiftChangeEscortsFlag = false + + self.AwacsROE = AWACS.ROE.POLICE + self.AwacsROT = AWACS.ROT.PASSIVE + + self.MenuStrict = true + + -- Escorts + self.HasEscorts = false + self.EscortTemplate = "" + + -- SRS + self.PathToSRS = "C:\\Program Files\\DCS-SimpleRadio-Standalone" + self.Gender = "male" + self.Culture = "en-US" + self.Voice = nil + self.Port = 5002 + self.RadioQueue = FIFO:New() -- Utilities.FiFo#FIFO + self.maxspeakentries = 3 + + self.CAPGender = "male" + self.CAPCulture = "en-US" + self.CAPVoice = nil + + -- Client SET + self.clientset = SET_GROUP:New():FilterCategoryAirplane():FilterCoalitions(self.coalitiontxt):FilterStart() + + -- managed groups + self.ManagedGrps = {} -- #table of #AWACS.ManagedGroup entries + self.ManagedGrpID = 0 + + self.AICAPCAllName = CALLSIGN.F16.Viper + self.AICAPCAllNumber = 0 + + -- Anchor stacks init + self.AnchorStacks = FIFO:New() -- Utilities.FiFo#FIFO + self.AnchorBaseAngels = 22 + self.AnchorStackDistance = 2 + self.AnchorMaxStacks = 4 + self.AnchorMaxAnchors = 2 + self.AnchorMaxZones = 6 + self.AnchorCurrZones = 1 + self.AnchorTurn = -(360/self.AnchorMaxZones) + + self:_CreateAnchorStack() + + -- Task lists + self.AssignedTasks = FIFO:New() -- Utilities.FiFo#FIFO + self.OpenTasks = FIFO:New() -- Utilities.FiFo#FIFO + + -- Pilot lists + --[[ ToDo - Maybe only 2? Move to managedgroups + self.CAPIdleAI = FIFO:New() -- Utilities.FiFo#FIFO + self.CAPIdleHuman = FIFO:New() -- Utilities.FiFo#FIFO + self.TaskedCAPAI = FIFO:New() -- Utilities.FiFo#FIFO + self.TaskedCAPHuman = FIFO:New() -- Utilities.FiFo#FIFO + --]] + + -- Picture, Contacts, Bogeys + self.PictureAO = FIFO:New() -- Utilities.FiFo#FIFO + self.PictureEWR = FIFO:New() -- Utilities.FiFo#FIFO + self.Contacts = FIFO:New() -- Utilities.FiFo#FIFO + self.ContactsAO = FIFO:New() -- Utilities.FiFo#FIFO + + -- SET for Intel Detection + self.DetectionSet=SET_GROUP:New() + + -- Set some string id for output to DCS.log file. + self.lid=string.format("%s (%s) | ", self.Name, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown") + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start FSM. + self:AddTransition("*", "Status", "*") -- Status update. + self:AddTransition("*", "CheckedIn", "*") + self:AddTransition("*", "CheckedOut", "*") + self:AddTransition("*", "AssignAnchor", "*") + self:AddTransition("*", "AssignedAnchor", "*") + self:AddTransition("*", "NewCluster", "*") + self:AddTransition("*", "NewContact", "*") + self:AddTransition("*", "LostCluster", "*") + self:AddTransition("*", "LostContact", "*") + self:AddTransition("*", "CheckRadioQueue", "*") + self:AddTransition("*", "EscortShiftChange", "*") + self:AddTransition("*", "AwacsShiftChange", "*") + self:AddTransition("*", "FlightOnMission", "*") + -- + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + + -- self:__Start(math.random(2,5)) + + local text = string.format("%sAWACS Version %s Initiated",self.lid,self.version) + + self:I(text) + + -- debug zone markers + if self.debug then + self.OpsZone:DrawZone(-1,{1,0,0},1,{1,0,0},0.2,5,true) + MARKER:New(self.OpsZone:GetCoordinate(),"AO Zone"):ToAll() + self.AnchorZone:DrawZone(-1,{0,0,1},1,{0,0,1},0.2,5,true) + MARKER:New(self.AnchorZone:GetCoordinate(),"Anchor Zone"):ToAll() + self.OrbitZone:DrawZone(-1,{0,1,0},1,{0,1,0},0.2,5,true) + MARKER:New(self.OrbitZone:GetCoordinate(),"Orbit Zone"):ToAll() + end + + return self +end + +-- TODO Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- [User] Get AWACS Name +-- @param #AWACS self +-- @return #string Name of this instance +function AWACS:GetName() + return self.Name or "not set" +end + +--- [User] Set AWACS flight details +-- @param #AWACS self +-- @param #number CallSign Defaults to CALLSIGN.AWACS.Magic +-- @param #number CallSignNo Defaults to 1 +-- @param #number Angels Defaults to 25 (i.e. 25000 ft) +-- @param #number Speed Defaults to 250kn +-- @param #number Heading Defaults to 0 (North) +-- @param #number Leg Defaults to 25nm +-- @return #AWACS self +function AWACS:SetAwacsDetails(CallSign,CallSignNo,Angels,Speed,Heading,Leg) + self:T(self.lid.."SetAwacsDetails") + self.CallSign = CallSign or CALLSIGN.AWACS.Magic + self.CallSignNo = CallSignNo or 1 + self.Angels = Angels or 25 + local speed = Speed or 250 + self.SpeedBase = speed + self.Speed = UTILS.KnotsToAltKIAS(speed,self.Angels*1000) + self.Heading = Heading or 0 + self.Leg = Leg or 25 + return self +end + +--- [User] Add a radar GROUP object to the INTEL detection SET_GROUP +-- @param Wrapper.Group#GROUP Group The GROUP to be added. Can be passed as SET_GROUP. +-- @return #AWACS self +function AWACS:AddGroupToDetection(Group) + self:T(self.lid.."AddGroupToDetection") + if Group and Group.ClassName and Group.ClassName == "GROUP" then + self.DetectionSet:AddGroup(Group) + elseif Group and Group.ClassName and Group.ClassName == "SET_GROUP" then + self.DetectionSet:AddSet(Group) + end + return self +end + +--- [User] Set AWACS SRS TTS details - see @{#MSRS} for details +-- @param #AWACS self +-- @param #string PathToSRS Defaults to "C:\\Program Files\\DCS-SimpleRadio-Standalone" +-- @param #string Gender Defaults to "male" +-- @param #string Culture Defaults to "en-US" +-- @param #number Port Defaults to 5002 +-- @param #string Voice (Optional) Use a specifc voice with the @{#MSRS.SetVoice} function, e.g, `:SetVoice("Microsoft Hedda Desktop")`. +-- Note that this must be installed on your windows system. +-- @return #AWACS self +function AWACS:SetSRS(PathToSRS,Gender,Culture,Port,Voice) + self:T(self.lid.."SetSRS") + self.PathToSRS = PathToSRS or "C:\\Program Files\\DCS-SimpleRadio-Standalone" + self.Gender = Gender or "male" + self.Culture = Culture or "en-US" + self.Port = Port or 5002 + self.Voice = Voice + return self +end + +--- [User] Set AWACS Escorts Template +-- @param #AWACS self +-- @param #number EscortNumber Number of fighther planes to accompany this AWACS. 0 or nil means no escorts. +-- @return #AWACS self +function AWACS:SetEscort(EscortNumber) + self:T(self.lid.."SetEscort") + if EscortNumber and EscortNumber > 0 then + self.HasEscorts = true + self.EscortNumber = EscortNumber + end + return self +end + +--- [Internal] Start AWACS Escorts FlightGroup +-- @param #AWACS self +-- @param #boolean Shiftchange This is a shift change call +-- @return #AWACS self +function AWACS:_StartEscorts(Shiftchange) + self:T(self.lid.."_StartEscorts") + + local AwacsFG = self.AwacsFG -- Ops.FlightGroup#FLIGHTGROUP + local group = AwacsFG:GetGroup() + local mission = AUFTRAG:NewESCORT(group,{x=-100, y=0, z=200},30,{"Air"}) + mission:SetRequiredAssets(self.EscortNumber) + + local timeonstation = (self.EscortsTimeOnStation + self.ShiftChangeTime) * 3600 -- hours to seconds + mission:SetTime(nil,timeonstation) + + self.AirWing:AddMission(mission) + + if Shiftchange then + self.EscortMissionReplacement = mission + else + self.EscortMission = mission + end + + return self +end + +--- [Internal] AWACS further Start Settings +-- @param #AWACS self +-- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup +-- @param Ops.Auftrag#AUFTRAG Mission +-- @return #AWACS self +function AWACS:_StartSettings(FlightGroup,Mission) + self:T(self.lid.."_StartSettings") + + -- called by AW OnafterFlightOnMission(...) + + local Mission = Mission -- Ops.Auftrag#AUFTRAG + local AwacsFG = FlightGroup -- Ops.FlightGroup#FLIGHTGROUP + + -- Is this our Awacs mission? + if self.AwacsMission:GetName() == Mission:GetName() then + + AwacsFG:SetDefaultRadio(self.Frequency,self.Modulation,false) + AwacsFG:SwitchRadio(self.Frequency,self.Modulation) + AwacsFG:SetDefaultAltitude(self.AwacsAngels*1000) + AwacsFG:SetHomebase(self.Airbase) + AwacsFG:SetDefaultCallsign(self.CallSign,self.CallSignNo) + AwacsFG:SetDefaultROE(ENUMS.ROE.WeaponHold) + AwacsFG:SetDefaultAlarmstate(AI.Option.Ground.val.ALARM_STATE.GREEN) + AwacsFG:SetDefaultEPLRS(self.ModernEra) + AwacsFG:SetDespawnAfterLanding() + AwacsFG:SetFuelLowRTB(true) + AwacsFG:SetFuelLowThreshold(20) + + local group = AwacsFG:GetGroup() -- Wrapper.Group#GROUP + + group:SetCommandInvisible(self.invisible) + group:SetCommandImmortal(self.immortal) + group:CommandSetCallsign(self.CallSign,self.CallSignNo,2) + -- Non AWACS does not seem take AWACS CS in DCS Group + --group:CommandSetCallsign(CALLSIGN.Aircraft.Pig,self.CallSignNo,2) + + AwacsFG:SetSRS(self.PathToSRS,self.Gender,self.Culture,self.Voice,self.Port,nil) + self.callsigntxt = string.format("%s %d %d",AWACS.CallSignClear[self.CallSign],1,self.CallSignNo) + + local text = string.format("%s starting for AO %s control.",self.callsigntxt,self.OpsZone:GetName() or "AO") + self:T(self.lid..text) + + AwacsFG:RadioTransmission(text,1,false) + + self.AwacsFG = AwacsFG + + self:__CheckRadioQueue(10) + + if self.HasEscorts then + --mission:SetRequiredEscorts(self.EscortNumber) + self:_StartEscorts() + end + + self.AwacsTimeStamp = timer.getTime() + self.EscortsTimeStamp = timer.getTime() + + elseif self.ShiftChangeAwacsRequested and self.AwacsMissionReplacement and self.AwacsMissionReplacement:GetName() == Mission:GetName() then + + -- manage AWACS Replacement + AwacsFG:SetDefaultRadio(self.Frequency,self.Modulation,false) + AwacsFG:SwitchRadio(self.Frequency,self.Modulation) + AwacsFG:SetDefaultAltitude(self.AwacsAngels*1000) + AwacsFG:SetHomebase(self.Airbase) + self.CallSignNo = self.CallSignNo+1 + AwacsFG:SetDefaultCallsign(self.CallSign,self.CallSignNo) + AwacsFG:SetDefaultROE(ENUMS.ROE.WeaponHold) + AwacsFG:SetDefaultAlarmstate(AI.Option.Ground.val.ALARM_STATE.GREEN) + AwacsFG:SetDefaultEPLRS(self.ModernEra) + AwacsFG:SetDespawnAfterLanding() + AwacsFG:SetFuelLowRTB(true) + AwacsFG:SetFuelLowThreshold(20) + + local group = AwacsFG:GetGroup() -- Wrapper.Group#GROUP + + group:SetCommandInvisible(self.invisible) + group:SetCommandImmortal(self.immortal) + group:CommandSetCallsign(self.CallSign,self.CallSignNo,2) + -- Non AWACS does not seem take AWACS CS in DCS Group + -- group:CommandSetCallsign(CALLSIGN.Aircraft.Pig,self.CallSignNo,2) + + AwacsFG:SetSRS(self.PathToSRS,self.Gender,self.Culture,self.Voice,self.Port,nil) + self.callsigntxt = string.format("%s %d %d",AWACS.CallSignClear[self.CallSign],1,self.CallSignNo) + + local text = string.format("%s shift change for AO %s control.",self.callsigntxt,self.OpsZone:GetName() or "AO") + self:T(self.lid..text) + + AwacsFG:RadioTransmission(text,1,false) + + self.AwacsFG = AwacsFG + + --self:__CheckRadioQueue(10) + + if self.HasEscorts then + --mission:SetRequiredEscorts(self.EscortNumber) + self:_StartEscorts(true) + end + + self.AwacsTimeStamp = timer.getTime() + self.EscortsTimeStamp = timer.getTime() + + end + return self +end + +--- [Internal] Check if a group has checked in +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to check +-- @return #number ID +-- @return #boolean CheckedIn +function AWACS:_GetManagedGrpID(Group) + self:T(self.lid.."_GetManagedGrpID for "..Group:GetName()) + local GID = 0 + local Outcome = false + local nametocheck = Group:GetName() + local managedgrps = self.ManagedGrps or {} + for _,_managed in pairs (managedgrps) do + local managed = _managed -- #AWACS.ManagedGroup + if managed.GroupName == nametocheck then + GID = managed.GID + Outcome = true + end + end + return GID, Outcome +end + +--- [Internal] AWACS Get TTS compatible callsign +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to use +-- @return #string Callsign +function AWACS:_GetCallSign(Group) + self:T(self.lid.."_GetCallSign") + local callsign = "" + local shortcallsign = Group:GetCallsign() -- e.g.Uzi11, but we want Uzi 1 1 + local callnumber = string.match(shortcallsign, "(%d+)$" ) + local callnumbermajor = string.char(string.byte(callnumber,1)) + local callnumberminor = string.char(string.byte(callnumber,2)) + callsign = string.gsub(shortcallsign,callnumber,"").." "..callnumbermajor.." "..callnumberminor + self:T("Callsign for TTS = " .. callsign) + return callsign +end + +--- [Internal] AWACS Speak Picture AO/EWR entries +-- @param #AWACS self +-- @param #boolean AO If true this is for AO, else EWR +-- @param #string Callsign Callsign to address +-- @param #number GID GroupID for comms +-- @return #AWACS self +function AWACS:_CreatePicture(AO,Callsign,GID) + self:T(self.lid.."_CreatePicture AO="..tostring(AO).." for "..Callsign.." GID "..GID) + + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + local group = managedgroup.Group -- Wrapper.Group#GROUP + local groupcoord = group:GetCoordinate() + + local fifo = self.PictureAO -- Utilities.FiFo#FIFO + local maxentries = self.maxspeakentries + local counter = 0 + + if not AO then + fifo = self.PictureEWR + end + + local entries = fifo:GetSize() + + if entries < maxentries then maxentries = entries end + + local text = "First group." + local textScreen = text + + while counter < maxentries do + counter = counter + 1 + local cluster = fifo:Pull() -- Ops.Intelligence#INTEL.Cluster + self:T({cluster}) + if cluster and cluster.coordinate then + local clustercoord = cluster.coordinate -- Core.Point#COORDINATE + local refBRAA = clustercoord:ToStringBRA(groupcoord) + text = text .. " "..refBRAA.."." + textScreen = textScreen .." "..refBRAA..".\n" + if counter < maxentries then + text = text .. " Next group." + textScreen = textScreen .. text .. "\n" + end + end + end + + -- empty queue from leftovers + fifo:Clear() + + local RadioEntry = {} -- #AWACS.RadioEntry + RadioEntry.IsNew = true + RadioEntry.TextTTS = text + RadioEntry.TextScreen = textScreen + RadioEntry.GroupID = GID + RadioEntry.IsGroup = managedgroup.IsPlayer + RadioEntry.ToScreen = managedgroup.IsPlayer + RadioEntry.Duration = STTS.getSpeechTime(text,1.1,false) or 8 + + self.RadioQueue:Push(RadioEntry) + + return self +end + +--- [Internal] AWACS Speak Bogey Dope entries +-- @param #AWACS self +-- @param #string Callsign Callsign to address +-- @param #number GID GroupID for comms +-- @return #AWACS self +function AWACS:_CreateBogeyDope(Callsign,GID) + self:T(self.lid.."_CreateBogeyDope for "..Callsign.." GID "..GID) + + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + local group = managedgroup.Group -- Wrapper.Group#GROUP + local groupcoord = group:GetCoordinate() + + local fifo = self.ContactsAO -- Utilities.FiFo#FIFO + local maxentries = self.maxspeakentries + local counter = 0 + + local entries = fifo:GetSize() + + if entries < maxentries then maxentries = entries end + + local sortedIDs = fifo:GetIDStackSorted() -- sort by distance + + while counter < maxentries do + counter = counter + 1 + local cluster = fifo:PullByID(sortedIDs[counter]) -- Ops.Intelligence#INTEL.Contact + self:T({cluster}) + if cluster and cluster.position then + self:_AnnounceContact(cluster,false,group,true) + end + end + + -- empty queue from leftovers + fifo:Clear() + + return self +end + +--- [Internal] AWACS Menu for Picture +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to use +-- @return #AWACS self +function AWACS:_Picture(Group) + self:T(self.lid.."_Picture") + local text = "Picture WIP" + local textScreen = text + + if not self.intel then + -- no intel yet! + text = string.format("%s. %s. Clear.",self.callsigntxt,self:_GetCallSign(Group) or "Unknown 1 1") + textScreen = text + local RadioEntry = {} -- #AWACS.RadioEntry + RadioEntry.IsNew = true + RadioEntry.TextTTS = text + RadioEntry.TextScreen = text + RadioEntry.ToScreen = true + RadioEntry.Duration = STTS.getSpeechTime(text,1.1,false) or 8 + + self.RadioQueue:Push(RadioEntry) + return self + end + + local GID, Outcome = self:_GetManagedGrpID(Group) + + if Outcome then + -- Pilot is checked in + -- get clusters from Intel + local clustertable = self.intel:GetClusterTable() + -- sort into buckets + for _,_cluster in pairs(clustertable) do + local cluster = _cluster -- Ops.Intelligence#INTEL.Cluster + local coordVec2 = cluster.coordinate:GetVec2() + + if self.OpsZone:IsVec2InZone(coordVec2) then + self.PictureAO:Push(cluster) + elseif self.ControlZone:IsVec2InZone(coordVec2) then + self.PictureEWR:Push(cluster) + end + end + + local clustersAO = self.PictureAO:GetSize() + local clustersEWR = self.PictureEWR:GetSize() + + if clustersAO == 0 and clustersEWR == 0 then + -- clear + text = string.format("%s. %s. Clear.",self.callsigntxt,self:_GetCallSign(Group) or "Unknown 1 1") + textScreen = text + local RadioEntry = {} -- #AWACS.RadioEntry + RadioEntry.IsNew = true + RadioEntry.TextTTS = text + RadioEntry.TextScreen = textScreen + RadioEntry.GroupID = GID + RadioEntry.IsGroup = Outcome + RadioEntry.ToScreen = true + RadioEntry.Duration = STTS.getSpeechTime(text,1.1,false) or 8 + + self.RadioQueue:Push(RadioEntry) + else + + if clustersAO > 0 then + text = string.format("%s. %s. Picture A O. ",self.callsigntxt,self:_GetCallSign(Group) or "Unknown 1 1") + textScreen = string.format("%s. %s. Picture AO. ",self.callsigntxt,self:_GetCallSign(Group) or "Unknown 1 1") + if clustersAO == 1 then + text = text .. "One group. " + textScreen = textScreen .. "One group.\n" + else + text = text .. clustersAO .. " groups. " + textScreen = textScreen .. clustersAO .. " groups.\n" + end + local RadioEntry = {} -- #AWACS.RadioEntry + RadioEntry.IsNew = true + RadioEntry.TextTTS = text + RadioEntry.TextScreen = text + RadioEntry.GroupID = GID + RadioEntry.IsGroup = Outcome + RadioEntry.ToScreen = true + RadioEntry.Duration = STTS.getSpeechTime(text,1.1,false) or 8 + self.RadioQueue:Push(RadioEntry) + + self:_CreatePicture(true,self:_GetCallSign(Group) or "Unknown 1 1",GID) + end + + if clustersEWR > 0 then + text = string.format("%s. %s. Picture Early Warning. ",self.callsigntxt,self:_GetCallSign(Group) or "Unknown 1 1") + textScreen = string.format("%s. %s. Picture EWR. ",self.callsigntxt,self:_GetCallSign(Group) or "Unknown 1 1") + if clustersEWR == 1 then + text = text .. "One group. " + textScreen = textScreen .. "One group.\n" + else + text = text .. clustersEWR .. " groups. " + textScreen = textScreen .. clustersAO .. " groups.\n" + end + local RadioEntry = {} -- #AWACS.RadioEntry + RadioEntry.IsNew = true + RadioEntry.TextTTS = text + RadioEntry.TextScreen = textScreen + RadioEntry.GroupID = GID + RadioEntry.IsGroup = Outcome + RadioEntry.ToScreen = true + RadioEntry.Duration = STTS.getSpeechTime(text,1.1,false) or 8 + self.RadioQueue:Push(RadioEntry) + + self:_CreatePicture(false,self:_GetCallSign(Group) or "Unknown 1 1",GID) + end + end + + elseif self.AwacsFG then + -- no, unknown + text = string.format("%s. Negative %s. You are not checked in.",self.callsigntxt,self:_GetCallSign(Group) or "Unknown 1 1") + local RadioEntry = {} -- #AWACS.RadioEntry + RadioEntry.IsNew = true + RadioEntry.TextTTS = text + RadioEntry.TextScreen = text + RadioEntry.GroupID = GID + RadioEntry.IsGroup = Outcome + RadioEntry.ToScreen = true + RadioEntry.Duration = STTS.getSpeechTime(text,1.1,false) or 8 + + self.RadioQueue:Push(RadioEntry) + end + return self +end + +--- [Internal] AWACS Menu for Bogey Dope +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to use +-- @return #AWACS self +function AWACS:_BogeyDope(Group) + self:T(self.lid.."_BogeyDope") + local text = "BogeyDope WIP" + local textScreen = "BogeyDope WIP" + + if not self.intel then + -- no intel yet! + text = string.format("%s. %s. Clear.",self.callsigntxt,self:_GetCallSign(Group) or "Unknown 1 1") + textScreen = text + local RadioEntry = {} -- #AWACS.RadioEntry + RadioEntry.IsNew = true + RadioEntry.TextTTS = text + RadioEntry.TextScreen = textScreen + RadioEntry.GroupID = 0 + RadioEntry.IsGroup = false + RadioEntry.ToScreen = true + RadioEntry.Duration = STTS.getSpeechTime(text,1.1,false) or 8 + + self.RadioQueue:Push(RadioEntry) + return self + end + + local GID, Outcome = self:_GetManagedGrpID(Group) + + if Outcome then + -- Pilot is checked in + + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + local pilotgroup = managedgroup.Group + local pilotcoord = managedgroup.Group:GetCoordinate() + + -- get contacts from Intel + local contactstable = self.intel:GetContactTable() + + -- sort into buckets - AO only for bogey dope! + for _,_contact in pairs(contactstable) do + local cluster = _contact -- Ops.Intelligence#INTEL.Contact + local coordVec2 = cluster.position:GetVec2() + + -- Get distance for sorting + local dist = pilotcoord:Get2DDistance(cluster.position) + + if self.OpsZone:IsVec2InZone(coordVec2) then + self.ContactsAO:Push(cluster,dist) + end + end + + local contactsAO = self.ContactsAO:GetSize() + + if contactsAO == 0 then + -- clear + text = string.format("%s. %s. Clear.",self.callsigntxt,self:_GetCallSign(Group) or "Unknown 1 1") + textScreen = text + local RadioEntry = {} -- #AWACS.RadioEntry + RadioEntry.IsNew = true + RadioEntry.TextTTS = text + RadioEntry.TextScreen = textScreen + RadioEntry.GroupID = GID + RadioEntry.IsGroup = Outcome + RadioEntry.ToScreen = Outcome + RadioEntry.Duration = STTS.getSpeechTime(text,1.1,false) or 8 + + self.RadioQueue:Push(RadioEntry) + else + + if contactsAO > 0 then + text = string.format("%s. %s. Bogey Dope. ",self.callsigntxt,self:_GetCallSign(Group) or "Unknown 1 1") + if contactsAO == 1 then + text = text .. "One group. " + textScreen = text .. "\n" + else + text = text .. contactsAO .. " groups. " + textScreen = text .. contactsAO .. " groups.\n" + end + local RadioEntry = {} -- #AWACS.RadioEntry + RadioEntry.IsNew = true + RadioEntry.TextTTS = text + RadioEntry.TextScreen = textScreen + RadioEntry.GroupID = GID + RadioEntry.IsGroup = Outcome + RadioEntry.ToScreen = true + RadioEntry.Duration = STTS.getSpeechTime(text,1.1,false) or 8 + self.RadioQueue:Push(RadioEntry) + + self:_CreateBogeyDope(self:_GetCallSign(Group) or "Unknown 1 1",GID) + end + end + + elseif self.AwacsFG then + -- no, unknown + text = string.format("%s. Negative %s. You are not checked in.",self.callsigntxt,self:_GetCallSign(Group) or "Unknown 1 1") + local RadioEntry = {} -- #AWACS.RadioEntry + RadioEntry.IsNew = true + RadioEntry.TextTTS = text + RadioEntry.TextScreen = text + RadioEntry.GroupID = GID + RadioEntry.IsGroup = Outcome + RadioEntry.ToScreen = true + RadioEntry.Duration = STTS.getSpeechTime(text,1.1,false) or 8 + + self.RadioQueue:Push(RadioEntry) + end + return self +end + + +--- [Internal] AWACS Menu for Declare +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to use +-- @return #AWACS self +function AWACS:_Declare(Group) + self:T(self.lid.."_Declare") + + local GID, Outcome = self:_GetManagedGrpID(Group) + local text = "Declare Not yet implemented" + if Outcome then + --[[ yes, known + + --]] + elseif self.AwacsFG then + -- no, unknown + text = string.format("%s. Negative %s. You are not checked in.",self.callsigntxt,self:_GetCallSign(Group) or "Unknown 1 1") + end + + local RadioEntry = {} -- #AWACS.RadioEntry + RadioEntry.IsNew = true + RadioEntry.TextTTS = text + RadioEntry.TextScreen = text + RadioEntry.GroupID = GID + RadioEntry.IsGroup = Outcome + RadioEntry.ToScreen = true + RadioEntry.Duration = STTS.getSpeechTime(text,1.1,false) or 8 + + self.RadioQueue:Push(RadioEntry) + + return self +end + +--- [Internal] AWACS Menu for Showtask +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to use +-- @return #AWACS self +function AWACS:_Showtask(Group) + self:T(self.lid.."_Showtask") + + local GID, Outcome = self:_GetManagedGrpID(Group) + local text = "Showtask WIP" + + if Outcome then + -- known group + + -- Do we have a task? + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + + if managedgroup.IsPlayer and self.TaskedCAPHuman:HasUniqueID(GID) then + + if managedgroup.CurrentTask >0 and self.AssignedTasks:HasUniqueID(managedgroup.CurrentTask) then + -- get task structure + local currenttask = self.AssignedTasks:ReadByID(managedgroup.CurrentTask) -- #AWACS.ManagedTask + if currenttask then + local status = currenttask.Status + local targettype = currenttask.Target:GetCategory() + local targetstatus = currenttask.Target:GetState() + local ToDo = currenttask.ToDo + local description = currenttask.ScreenText + local callsign = self:_GetCallSign(Group) + + if self.debug then + local taskreport = REPORT:New("AWACS Tasking Display") + taskreport:Add("===============") + taskreport:Add(string.format("Task for Callsign: %s",callsign)) + taskreport:Add(string.format("Task: %s with Status: %s",ToDo,status)) + taskreport:Add(string.format("Target of Type: %s",targettype)) + taskreport:Add(string.format("Target in State: %s",targetstatus)) + taskreport:Add("===============") + self:I(taskreport:Text()) + end + + MESSAGE:New(description,30,"AWACS",true):ToGroup(Group) + + end + end + end + + elseif self.AwacsFG then + -- no, unknown + text = string.format("%s. Negative %s. You are not checked in.",self.callsigntxt,self:_GetCallSign(Group) or "Unknown 1 1") + + local RadioEntry = {} -- #AWACS.RadioEntry + RadioEntry.IsNew = true + RadioEntry.TextTTS = text + RadioEntry.TextScreen = text + RadioEntry.GroupID = GID + RadioEntry.IsGroup = Outcome + RadioEntry.ToScreen = true + RadioEntry.Duration = STTS.getSpeechTime(text,1.1,false) or 8 + + self.RadioQueue:Push(RadioEntry) + end + return self +end + +--- [Internal] AWACS Menu for Check in +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to use +-- @return #AWACS self +function AWACS:_CheckIn(Group) + self:I(self.lid.."_CheckIn "..Group:GetName()) + -- check if already known + local GID, Outcome = self:_GetManagedGrpID(Group) + local text = "" + if not Outcome then + self.ManagedGrpID = self.ManagedGrpID + 1 + local managedgroup = {} -- #AWACS.ManagedGroup + managedgroup.Group = Group + managedgroup.GroupName = Group:GetName() + managedgroup.IsPlayer = true + managedgroup.IsAI = false + managedgroup.CallSign = self:_GetCallSign(Group) or "Unknown 1 1" + managedgroup.CurrentTask = 0 + managedgroup.HasAssignedTask = false + managedgroup.GID = self.ManagedGrpID + GID = managedgroup.GID + self.ManagedGrps[self.ManagedGrpID]=managedgroup + text = string.format("%s. Copy %s. Await tasking.",self.callsigntxt,managedgroup.CallSign) + self:__CheckedIn(1,managedgroup.GID) + self:__AssignAnchor(5,managedgroup.GID) + elseif self.AwacsFG then + text = string.format("%s. Negative %s. You are already checked in.",self.callsigntxt,self:_GetCallSign(Group) or "Unknown 1 1") + end + + local RadioEntry = {} -- #AWACS.RadioEntry + RadioEntry.IsNew = true + RadioEntry.TextTTS = text + RadioEntry.TextScreen = text + RadioEntry.GroupID = GID + RadioEntry.IsGroup = true + RadioEntry.ToScreen = true + RadioEntry.Duration = STTS.getSpeechTime(text,1.1,false) or 8 + + self.RadioQueue:Push(RadioEntry) + + return self +end + +--- [Internal] AWACS Menu for CheckInAI +-- @param #AWACS self +-- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup to use +-- @param Wrapper.Group#GROUP Group Group to use +-- @param #number AuftragsNr Ops.Auftrag#AUFTRAG.auftragsnummer +-- @return #AWACS self +function AWACS:_CheckInAI(FlightGroup,Group,AuftragsNr) + self:I(self.lid.."_CheckInAI "..Group:GetName()) + -- check if already known + local GID, Outcome = self:_GetManagedGrpID(Group) + local text = "" + if not Outcome then + self.ManagedGrpID = self.ManagedGrpID + 1 + local managedgroup = {} -- #AWACS.ManagedGroup + managedgroup.Group = Group + managedgroup.GroupName = Group:GetName() + managedgroup.FlightGroup = FlightGroup + managedgroup.IsPlayer = false + managedgroup.IsAI = true + managedgroup.CallSign = self:_GetCallSign(Group) or "Unknown 1 1" + managedgroup.CurrentTask = AuftragsNr + managedgroup.HasAssignedTask = false + managedgroup.GID = self.ManagedGrpID + + -- SRS voice for CAP + --FlightGroup:SetSRS(PathToSRS,Gender,Culture,Voice,Port,PathToGoogleKey) + + FlightGroup:SetDefaultRadio(self.Frequency,self.Modulation,false) + FlightGroup:SwitchRadio(self.Frequency,self.Modulation) + + FlightGroup:SetSRS(self.PathToSRS,self.CAPGender,self.CAPCulture,self.CAPVoice,self.Port,nil) + + self.ManagedGrps[self.ManagedGrpID]=managedgroup + text = string.format("%s. Copy %s. Await tasking.",self.callsigntxt,managedgroup.CallSign) + self:__CheckedIn(1,managedgroup.GID) + self:__AssignAnchor(5,managedgroup.GID) + else + text = string.format("%s. Negative %s. You are already checked in.",self.callsigntxt,self:_GetCallSign(Group) or "Unknown 1 1") + end + + local RadioEntry = {} -- #AWACS.RadioEntry + RadioEntry.IsNew = true + RadioEntry.TextTTS = text + RadioEntry.ToScreen = false + RadioEntry.Duration = STTS.getSpeechTime(text,1.1,false) or 8 + + self.RadioQueue:Push(RadioEntry) + + return self +end + +--- [Internal] AWACS Menu for Check Out +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to use +-- @return #AWACS self +function AWACS:_CheckOut(Group) + self:I(self.lid.."_CheckOut") + + -- check if already known + local GID, Outcome = self:_GetManagedGrpID(Group) + local text = "" + if Outcome then + -- yes, known + text = string.format("%s. Copy %s. Have a safe flight home.",self.callsigntxt,self:_GetCallSign(Group) or "Unknown 1 1") + -- grab some data before we nil the entry + local AnchorAssigned = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + local Stack = AnchorAssigned.AnchorStackNo + local Angels = AnchorAssigned.AnchorStackAngels + self.ManagedGrps[GID] = nil + self:__CheckedOut(1,GID,Stack,Angels) + else + -- no, unknown + text = string.format("%s. Negative %s. You are not checked in.",self.callsigntxt,self:_GetCallSign(Group) or "Unknown 1 1") + end + + local RadioEntry = {} -- #AWACS.RadioEntry + RadioEntry.IsNew = true + RadioEntry.TextTTS = text + RadioEntry.ToScreen = true + RadioEntry.Duration = STTS.getSpeechTime(text,1.1,false) or 8 + + self.RadioQueue:Push(RadioEntry) + + return self +end + +--- [Internal] AWACS set client menus +-- @param #AWACS self +-- @return #AWACS self +function AWACS:_SetClientMenus() + self:T(self.lid.."_SetClientMenus") + local clientset = self.clientset -- Core.Set#SET_GROUP + local aliveset = clientset:GetAliveSet() -- #table of #GROUP objects + --local aliveobjects = aliveset:GetSetObjects() or {} + local clientmenus = {} + for _,_group in pairs(aliveset) do + -- go through set and build the menu + local grp = _group -- Wrapper.Group#GROUP + if self.MenuStrict then + -- check if pilot has checked in + if grp and grp:IsAlive() and grp:GetUnit(1):IsPlayer() then + local GID, checkedin = self:_GetManagedGrpID(grp) + if checkedin then + -- full menu minus checkin + local hasclientmenu = self.clientmenus[grp:GetName()] -- #AWACS.MenuStructure + --local checkinmenu = hasclientmenu.checkin -- Core.Menu#MENU_GROUP_COMMAND + --checkinmenu:Remove(nil,grp:GetName()) + local basemenu = hasclientmenu.basemenu -- Core.Menu#MENU_GROUP + --local basemenu = MENU_GROUP:New(grp.Name,nil) + basemenu:RemoveSubMenus() + local picture = MENU_GROUP_COMMAND:New(grp,"Picture",basemenu,self._Picture,self,grp) + local bogeydope = MENU_GROUP_COMMAND:New(grp,"Bogey Dope",basemenu,self._BogeyDope,self,grp) + local declare = MENU_GROUP_COMMAND:New(grp,"Declare",basemenu,self._Declare,self,grp) + local showtask = MENU_GROUP_COMMAND:New(grp,"Showtask",basemenu,self._Showtask,self,grp) + local checkout = MENU_GROUP_COMMAND:New(grp,"Check Out",basemenu,self._CheckOut,self,grp):Refresh() + clientmenus[grp:GetName()] = { -- #AWACS.MenuStructure + groupname = grp:GetName(), + menuset = true, + basemenu = basemenu, + --checkin = checkin, + checkout= checkout, + picture = picture, + bogeydope = bogeydope, + declare = declare, + showtask = showtask, + } + elseif not clientmenus[grp:GetName()] then + -- check in only + local basemenu = MENU_GROUP:New(grp,self.Name,nil) + local checkin = MENU_GROUP_COMMAND:New(grp,"Check In",basemenu,self._CheckIn,self,grp) + checkin:SetTag(grp:GetName()) + checkin:Refresh() + clientmenus[grp:GetName()] = { -- #AWACS.MenuStructure + groupname = grp:GetName(), + menuset = true, + basemenu = basemenu, + checkin = checkin, + --checkout= checkout, + --picture = picture, + --bogeydope = bogeydope, + --declare = declare, + --showtask = showtask, + } + end + end + else + if grp and grp:IsAlive() and grp:GetUnit(1):IsPlayer() and not clientmenus[grp:GetName()] then + local basemenu = MENU_COALITION:New(self.coalition,self.Name,nil) + local picture = MENU_COALITION_COMMAND:New(self.coalition,"Picture",basemenu,self._Picture,self,grp) + local bogeydope = MENU_COALITION_COMMAND:New(self.coalition,"Bogey Dope",basemenu,self._BogeyDope,self,grp) + local declare = MENU_COALITION_COMMAND:New(self.coalition,"Declare",basemenu,self._Declare,self,grp) + local showtask = MENU_COALITION_COMMAND:New(self.coalition,"Showtask",basemenu,self._Showtask,self,grp) + local checkin = MENU_COALITION_COMMAND:New(self.coalition,"Check In",basemenu,self._CheckIn,self,grp) + local checkout = MENU_COALITION_COMMAND:New(self.coalition,"Check Out",basemenu,self._CheckOut,self,grp):Refresh() + clientmenus[grp:GetName()] = { -- #AWACS.MenuStructure + groupname = grp:GetName(), + menuset = true, + basemenu = basemenu, + checkin = checkin, + checkout= checkout, + picture = picture, + bogeydope = bogeydope, + declare = declare, + showtask = showtask, + } + end + end + end + self.clientmenus = clientmenus + return self +end + +--- [Internal] AWACS Create a new Anchor Stack +-- @param #AWACS self +-- @return #boolean success +-- @return #nunber AnchorStackNo +function AWACS:_CreateAnchorStack() + self:T(self.lid.."_CreateAnchorStack") + local stackscreated = self.AnchorStacks:GetSize() + if stackscreated == self.AnchorMaxAnchors then + -- only create self.AnchorMaxAnchors Anchors + return false, 0 + end + local AnchorStackOne = {} -- #AWACS.AnchorData + AnchorStackOne.AnchorBaseAngels = self.AnchorBaseAngels + AnchorStackOne.Anchors = FIFO:New() -- Utilities.FiFo#FIFO + AnchorStackOne.AnchorAssignedID = FIFO:New() -- Utilities.FiFo#FIFO + local newname = "" + for i=1,self.AnchorMaxStacks do + AnchorStackOne.Anchors:Push((i-1)*self.AnchorStackDistance+self.AnchorBaseAngels) + end + if self.debug then + --AnchorStackOne.Anchors:Flush() + end + if stackscreated == 0 then + AnchorStackOne.AnchorZone = self.AnchorZone + AnchorStackOne.AnchorZoneCoordinate = self.AnchorZone:GetCoordinate() + AnchorStackOne.AnchorZoneCoordinateText = self.AnchorZone:GetCoordinate():ToStringLLDDM() + --push to AnchorStacks + self.AnchorStacks:Push(AnchorStackOne,"One") + else + local newsubname = AWACS.AnchorNames[stackscreated+1] or tostring(stackscreated+1) + newname = self.AnchorZone:GetName() .. "-"..newsubname + local anchorbasecoord = self.OpsZone:GetCoordinate() -- Core.Point#COORDINATE + -- OpsZone can be Polygon, so use distance to AnchorZone as radius + local anchorradius = anchorbasecoord:Get2DDistance(self.AnchorZone:GetCoordinate()) + --local anchorradius = self.OpsZone:GetRadius() -- #number + --anchorradius = anchorradius + self.AnchorZone:GetRadius() + local angel = self.AnchorZone:GetCoordinate():GetAngleDegrees(self.OpsZone:GetVec3()) + self:T("Angel Radians= " .. angel) + local turn = math.fmod(self.AnchorTurn*stackscreated,360) -- #number + if self.AnchorTurn < 0 then turn = -turn end + local newanchorbasecoord = anchorbasecoord:Translate(anchorradius,turn+angel) -- Core.Point#COORDINATE + AnchorStackOne.AnchorZone = ZONE_RADIUS:New(newname, newanchorbasecoord:GetVec2(), self.AnchorZone:GetRadius()) + AnchorStackOne.AnchorZoneCoordinate = newanchorbasecoord + AnchorStackOne.AnchorZoneCoordinateText = newanchorbasecoord:ToStringLLDDM() + --push to AnchorStacks + self.AnchorStacks:Push(AnchorStackOne,newname) + end + + if self.debug then + --self.AnchorStacks:Flush() + AnchorStackOne.AnchorZone:DrawZone(-1,{0,0,1},1,{0,0,1},0.2,5,true) + MARKER:New(AnchorStackOne.AnchorZone:GetCoordinate(),"Anchor Zone: "..newname):ToAll() + end + + return true,self.AnchorStacks:GetSize() + +end + +--- [Internal] AWACS get free anchor stack for managed groups +-- @param #AWACS self +-- @return #number AnchorStackNo +-- @return #boolean free +function AWACS:_GetFreeAnchorStack() + self:T(self.lid.."_GetFreeAnchorStack") + local AnchorStackNo, Free = 0, false + --return AnchorStackNo, Free + local availablestacks = self.AnchorStacks:GetPointerStack() or {} -- #table + for _id,_entry in pairs(availablestacks) do + local entry = _entry -- Utilities.FiFo#FIFO.IDEntry + local data = entry.data -- #AWACS.AnchorData + if data.Anchors:IsNotEmpty() then + AnchorStackNo = _id + Free = true + break + end + end + -- TODO - if extension of anchor stacks to max, send AI home + if not Free then + -- try to create another stack + local created, number = self:_CreateAnchorStack() + if created then + -- we could create a new one - phew! + self:_GetFreeAnchorStack() + end + end + return AnchorStackNo, Free +end + +--- [Internal] AWACS Assign Anchor Position to a Group +-- @param #AWACS self +-- @return #number GID Managed Group ID +-- @return #AWACS self +function AWACS:_AssignAnchorToID(GID) + self:T(self.lid.."_AssignAnchorToID") + local AnchorStackNo, Free = self:_GetFreeAnchorStack() + if Free then + -- get the Anchor from the stack + local Anchor = self.AnchorStacks:PullByPointer(AnchorStackNo) -- #AWACS.AnchorData + -- pull one free angels + local freeangels = Anchor.Anchors:Pull() + -- push GID on anchor + Anchor.AnchorAssignedID:Push(GID) + if self.debug then + --Anchor.AnchorAssignedID:Flush() + --Anchor.Anchors:Flush() + end + -- push back to AnchorStacks + self.AnchorStacks:Push(Anchor) + self:T({Anchor,freeangels}) + self:__AssignedAnchor(5,GID,Anchor,AnchorStackNo,freeangels) + else + self:E(self.lid .. "Cannot assing free anchor stack to GID ".. GID) + -- try again ... + self:__AssignAnchor(10,GID) + end + return self +end + +--- [Internal] Remove GID (group) from Anchor Stack +-- @param #AWACS self +-- @param #AWACS.ManagedGroup.GID ID +-- @param #number AnchorStackNo +-- @param #number Angels +-- @return #AWACS self +function AWACS:_RemoveIDFromAnchor(GID,AnchorStackNo,Angels) + self:I(self.lid.."_RemoveIDFromAnchor for GID="..GID.." Stack="..AnchorStackNo.." Angels="..Angels) + -- pull correct anchor + local AnchorStackNo = AnchorStackNo or 1 + local Anchor = self.AnchorStacks:PullByPointer(AnchorStackNo) -- #AWACS.AnchorData + -- pull GID from stack + local removedID = Anchor.AnchorAssignedID:PullByID(GID) + -- push free angels to stack + Anchor.Anchors:Push(Angels) + -- push back AnchorStack + self.AnchorStacks:Push(Anchor) + return self +end + +--- [Internal] Start INTEL detection when we reach the AWACS Orbit Zone +-- @param #AWACS self +-- @param Wrapper.Group#GROUP awacs +-- @return #AWACS self +function AWACS:_StartIntel(awacs) + self:T(self.lid.."_StartIntel") + + self.DetectionSet:AddGroup(awacs) + + local intel = INTEL:New(self.DetectionSet,self.coalition,self.callsigntxt) + --intel:SetVerbosity(2) + intel:SetClusterAnalysis(true,self.debug) + if self.NoHelos then + intel:SetFilterCategory({Unit.Category.AIRPLANE}) + else + intel:SetFilterCategory({Unit.Category.AIRPLANE,Unit.Category.HELICOPTER}) + end + + -- Callbacks + local function NewCluster(Cluster) + self:__NewCluster(5,Cluster) + end + function intel:OnAfterNewCluster(From,Event,To,Cluster) + NewCluster(Cluster) + end + + local function NewContact(Contact) + self:__NewContact(5,Contact) + end + function intel:OnAfterNewContact(From,Event,To,Contact) + NewContact(Contact) + end + + local function LostContact(Contact) + self:__LostContact(5,Contact) + end + function intel:OnAfterLostContact(From,Event,To,Contact) + LostContact(Contact) + end + + local function LostCluster(Cluster,Mission) + self:__LostCluster(5,Cluster,Mission) + end + function intel:OnAfterLostCluster(From,Event,To,Cluster,Mission) + LostCluster(Cluster,Mission) + end + + intel:__Start(2) + + self.intel = intel -- Ops.Intelligence#INTEL + return self +end + +--- [Internal] Get blurred size of group or cluster +-- @param #AWACS self +-- @param #number size +-- @return #number adjusted size +function AWACS:_GetBlurredSize(size) + self:T(self.lid.."_GetBlurredSize") + local threatsize = 0 + local blur = self.RadarBlur + local blurmin = 100 - blur + local blurmax = 100 + blur + local actblur = math.random(blurmin,blurmax) / 100 + threatsize = math.floor(size * actblur) + return threatsize +end + +--- [Internal] Get threat level as clear test +-- @param #AWACS self +-- @param #number threatlevel +-- @return #string threattext +function AWACS:_GetThreatLevelText(threatlevel) + self:T(self.lid.."_GetThreatLevelText") + local threattext = "GREEN" + if threatlevel <= AWACS.THREATLEVEL.GREEN then + threattext = "GREEN" + elseif threatlevel <= AWACS.THREATLEVEL.AMBER then + threattext = "AMBER" + else + threattext = "RED" + end + return threattext +end + +--- [Internal] Get BRA text for TTS +-- @param #AWACS self +-- @param Core.Point#COORDINATE clustercoordinate +-- @return #string BRAText +function AWACS:_GetBRAfromBullsOrAO(clustercoordinate) + self:T(self.lid.."__GetBRAfromBullsOrAO") + local refcoord = self.AOCoordinate -- Core.Point#COORDINATE + local BRAText = "" + if not self.UseBullsAO then + -- get BR from AO + BRAText = "AO "..refcoord:ToStringBR(clustercoordinate) + else + -- get BR from Bulls + BRAText = clustercoordinate:ToStringBULLS(self.coalition) + end + return BRAText +end + +--- [Internal] Register Task for Group by GID +-- @param #AWACS self +-- @param #number GroupID ManagedGroup ID +-- @param #AWACS.TaskDescription Description Short Description Task Type +-- @param #string ScreenText Long task description for screen output +-- @param #table Object Object for Ops.Target#TARGET assignment +-- @return #AWACS self +function AWACS:_CreateTaskForGroup(GroupID,Description,ScreenText,Object) + self:I(self.lid.."_CreateTaskForGroup "..GroupID .." Description: "..Description) + local managedgroup = self.ManagedGrps[GroupID] -- #AWACS.ManagedGroup + local task = {} -- #AWACS.ManagedTask + self.ManagedTaskID = self.ManagedTaskID + 1 + task.TID = self.ManagedTaskID + task.AssignedGroupID = GroupID + task.Status = AWACS.TaskStatus.ASSIGNED + task.ToDo = Description + task.Target = TARGET:New(Object) + task.ScreenText = ScreenText + if Description == AWACS.TaskDescription.ANCHOR or Description == AWACS.TaskDescription.REANCHOR then + task.Target.Type = TARGET.ObjectType.ZONE + end + self.AssignedTasks:Push(task,task.TID) + --self:I({task}) + managedgroup.HasAssignedTask = true + managedgroup.CurrentTask = task.TID + self.ManagedGrps[GroupID] = managedgroup + --if managedgroup.IsPlayer then + --self.TaskedCAPHuman:Push(GroupID) + --elseif managedgroup.IsAI then + --self.TaskedCAPAI:Push(GroupID) + --end + return self +end + +--- [Internal] Read registered Task for Group by its ID +-- @param #AWACS self +-- @param #number GroupID ManagedGroup ID +-- @return #AWACS.ManagedTask Task or nil if n/e +function AWACS:_ReadAssignedTaskFromGID(GroupID) + self:T(self.lid.."_GetAssignedTaskFromGID "..GroupID) + local managedgroup = self.ManagedGrps[GroupID] -- #AWACS.ManagedGroup + if managedgroup and managedgroup.HasAssignedTask then + local TaskID = managedgroup.CurrentTask + if self.AssignedTasks:HasUniqueID(TaskID) then + return self.AssignedTasks:ReadByID(TaskID) + end + end + return nil +end + +--- [Internal] Read assigned Group from a TaskID +-- @param #AWACS self +-- @param #number TaskID ManagedTask ID +-- @return #AWACS.ManagedGroup Group structure or nil if n/e +function AWACS:_ReadAssignedGroupFromTID(TaskID) + self:T(self.lid.."_ReadAssignedGroupFromTID "..TaskID) + if self.AssignedTasks:HasUniqueID(TaskID) then + local task = self.AssignedTasks:ReadByID(TaskID) -- #AWACS.ManagedTask + if task and task.AssignedGroupID and task.AssignedGroupID > 0 then + return self.ManagedGrps[task.AssignedGroupID] + end + end + return nil +end + +--- [Internal] Create new idle task from contact to pick up later +-- @param #AWACS self +-- @param #string Description Task Type +-- @param #table Object Object of TARGET +-- @param Ops.Intelligence#INTEL.Contact Contact +-- @return #AWACS self +function AWACS:_CreateIdleTaskForContact(Description,Object,Contact) + self:T(self.lid.."_CreateIdleTaskForContact "..Description) + local task = {} -- #AWACS.ManagedTask + self.ManagedTaskID = self.ManagedTaskID + 1 + task.TID = self.ManagedTaskID + task.AssignedGroupID = 0 + task.Status = AWACS.TaskStatus.IDLE + task.ToDo = Description + task.Target = TARGET:New(Object) + task.Contact = Contact + task.IsContact = true + task.ScreenText = Description + if Description == AWACS.TaskDescription.ANCHOR or Description == AWACS.TaskDescription.REANCHOR then + task.Target.Type = TARGET.ObjectType.ZONE + end + self.OpenTasks:Push(task,task.TID) + return self +end + +--- [Internal] Create new idle task from cluster to pick up later +-- @param #AWACS self +-- @param #string Description Task Type +-- @param #table Object Object of TARGET +-- @param Ops.Intelligence#INTEL.Cluster Cluster +-- @return #AWACS self +function AWACS:_CreateIdleTaskForCluster(Description,Object,Cluster) + self:T(self.lid.."_CreateIdleTaskForCluster "..Description) + local task = {} -- #AWACS.ManagedTask + self.ManagedTaskID = self.ManagedTaskID + 1 + task.TID = self.ManagedTaskID + task.AssignedGroupID = 0 + task.Status = AWACS.TaskStatus.IDLE + task.ToDo = Description + --self:T({Cluster.Contacts}) + --task.Target = TARGET:New(Cluster.Contacts[1]) + task.Target = TARGET:New(self.intel:GetClusterCoordinate(Cluster)) + task.Cluster = Cluster + task.IsCluster = true + task.ScreenText = Description + if Description == AWACS.TaskDescription.ANCHOR or Description == AWACS.TaskDescription.REANCHOR then + task.Target.Type = TARGET.ObjectType.ZONE + end + self.OpenTasks:Push(task,task.TID) + return self +end + +--- [Internal] Create radio entry to tell players that CAP is on station in Anchor +-- @param #AWACS self +-- @param #number GID Group ID +-- @return #AWACS self +function AWACS:_MessageAIReadyForTasking(GID) + self:I(self.lid.."_MessageAIReadyForTasking") + -- obtain group details + if GID >0 and self.ManagedGrps[GID] then + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + local GFCallsign = self:_GetCallSign(managedgroup.Group) + local TextTTS = string.format("%s. %s. On station over anchor %d at angels %d. Ready for tasking.",GFCallsign,self.callsigntxt,managedgroup.AnchorStackNo or 1,managedgroup.AnchorStackAngels or 25) + local RadioEntry = {} -- #AWACS.RadioEntry + RadioEntry.TextTTS = TextTTS + RadioEntry.TextScreen = "" + RadioEntry.IsNew = true + RadioEntry.IsGroup = false + RadioEntry.GroupID = GID + RadioEntry.Duration = STTS.getSpeechTime(TextTTS,1.2,false)+2 or 16 + RadioEntry.ToScreen = false + RadioEntry.FromAI = true + + self.RadioQueue:Push(RadioEntry) + end + return self +end + +--- [Internal] Check available tasks and status +-- @param #AWACS self +-- @return #AWACS self +function AWACS:_CheckTaskQueue() + self:I(self.lid.."_CheckTaskQueue") + local opentasks = 0 + local assignedtasks = 0 + if self.OpenTasks:IsNotEmpty() then + opentasks = self.OpenTasks:GetSize() + self:I("Open Tasks: " .. opentasks) + local taskstack = self.OpenTasks:GetPointerStack() + for _id,_entry in pairs(taskstack) do + local data = _entry -- Utilities.FiFo#FIFO.IDEntry + local entry = data.data -- #AWACS.ManagedTask + local target = entry.Target -- Ops.Target#TARGET + local description = entry.ToDo + self:I("ToDo = "..description) + if description == AWACS.TaskDescription.ANCHOR or description == AWACS.TaskDescription.REANCHOR then + self:I("Open Tasks ANCHOR/REANCHOR") + -- see if we have reached the anchor zone + local managedgroup = self.ManagedGrps[entry.AssignedGroupID] -- #AWACS.ManagedGroup + if managedgroup then + local group = managedgroup.Group + local groupcoord = group:GetCoordinate() + local zone = target:GetObject() -- Core.Zone#ZONE + self:T({zone}) + if group:IsInZone(zone) then + self:I("Open Tasks ANCHOR/REANCHOR success for GroupID "..entry.AssignedGroupID) + -- made it + target:Stop() + -- pull task from OpenTasks + self.OpenTasks:PullByPointer(_id) + --[[ add group to idle stack + if managedgroup.IsAI then + self.TaskedCAPAI:PullByPointer(entry.AssignedGroupID) + self.CAPIdleAI:Push(entry.AssignedGroupID) + elseif managedgroup.IsPlayer then + self.TaskedCAPHuman:PullByPointer(entry.AssignedGroupID) + self.CAPIdleHuman:Push(entry.AssignedGroupID) + end + --]] + else + -- not there yet + end + end + elseif description == AWACS.TaskDescription.INTERCEPT then + -- TODO + elseif description == AWACS.TaskDescription.RTB then + -- TODO + end + end + end + + if self.AssignedTasks:IsNotEmpty() then + opentasks = self.AssignedTasks:GetSize() + self:I("Assigned Tasks: " .. opentasks) + local taskstack = self.AssignedTasks:GetPointerStack() + for _id,_entry in pairs(taskstack) do + local data = _entry -- Utilities.FiFo#FIFO.IDEntry + local entry = data.data -- #AWACS.ManagedTask + local target = entry.Target -- Ops.Target#TARGET + local description = entry.ToDo + self:I("ToDo = "..description) + if description == AWACS.TaskDescription.ANCHOR or description == AWACS.TaskDescription.REANCHOR then + self:I("Open Tasks ANCHOR/REANCHOR") + -- see if we have reached the anchor zone + local managedgroup = self.ManagedGrps[entry.AssignedGroupID] -- #AWACS.ManagedGroup + if managedgroup then + local group = managedgroup.Group + local groupcoord = group:GetCoordinate() + local zone = target:GetObject() -- Core.Zone#ZONE + self:T({zone}) + if group:IsInZone(zone) then + self:I("Open Tasks ANCHOR/REANCHOR success for GroupID "..entry.AssignedGroupID) + -- made it + target:Stop() + -- pull task from OpenTasks + self.AssignedTasks:PullByPointer(_id) + -- add group to idle stack + if managedgroup.IsAI then + -- message AI on station + self:_MessageAIReadyForTasking(managedgroup.GID) + elseif managedgroup.IsPlayer then + --self.TaskedCAPHuman:PullByPointer(entry.AssignedGroupID) + ---self.CAPIdleHuman:Push(entry.AssignedGroupID) + end + else + -- not there yet + end + end + elseif description == AWACS.TaskDescription.INTERCEPT then + -- TODO + elseif description == AWACS.TaskDescription.RTB then + -- TODO + end + end + end + + return self +end + +--- [Internal] Write stats to log +-- @param #AWACS self +-- @return #AWACS self +function AWACS:_LogStatistics() + self:T(self.lid.."_LogStatistics") + return self +end + +--- [User] Add another AirWing for CAP Flights under management +-- @param #AWACS self +-- @param Ops.AirWing#AIRWING AirWing The AirWing to (also) obtain CAP flights from +-- @return #AWACS self +function AWACS:AddCAPAirWing(AirWing) + self:I(self.lid.."AddCAPAirWing") + if AirWing then + -- TODO - Test Install callback + -- TODO - add distance to AO as UniqueID + AirWing:SetUsingOpsAwacs(self) + self.CAPAirwings:Push(AirWing) + end + return self +end + +--- Recruit assets for a given TARGET. +-- @param #AWACS self +-- @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 AWACS:RecruitAssets(MissionType, NassetsMin, NassetsMax) + + -- Cohorts. + local Cohorts={} + local AWFiFo = self.CAPAirwings -- Utilities.FiFo#FIFO + local AWStack = AWFiFo:GetPointerStack() + local AirWingList = {} + + for _ID,_AWID in pairs(AWStack) do + local SubAW = self.CAPAirwings:ReadByPointer(_ID) + if SubAW then + table.insert(AirWingList,SubAW) + end + end + + for _,_legion in pairs(AirWingList) 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=self.OpsZone:GetVec2() + + -- Recruit assets. + local recruited, assets, legions=LEGION.RecruitCohortAssets(Cohorts, MissionType, nil, NassetsMin, NassetsMax, TargetVec2) + + return recruited, assets, legions +end + +--- [Internal] Announce a new contact +-- @param #AWACS self +-- @param Ops.Intelligence#INTEL.Contact Contact +-- @param #boolean IsNew +-- @param Wrapper.Group#GROUP Group Announce to Group if not nil +-- @param #boolean IsBogeyDope If true, this is a bogey dope announcement +-- @return #AWACS self +function AWACS:_AnnounceContact(Contact,IsNew,Group,IsBogeyDope) + self:T(self.lid.."_AnnounceContact") + -- do we have a group to talk to? + local isGroup = false + local GID = 0 + local grpcallsign = "Unknown 1 1" + if Group and Group:IsAlive() then + GID, isGroup = self:_GetManagedGrpID(Group) + self:T("GID="..GID.." CheckedIn = "..tostring(isGroup)) + grpcallsign = self:_GetCallSign(Group) or "Unknown 1 1" + end + local contact = Contact -- Ops.Intelligence#INTEL.Contact + local intel = self.intel -- Ops.Intelligence#INTEL + local size = contact.group:CountAliveUnits() + local threatsize = self:_GetBlurredSize(size) + local threatlevel = contact.threatlevel + local threattext = self:_GetThreatLevelText(threatlevel) + local clustercoordinate = contact.position + + local BRAfromBulls = self:_GetBRAfromBullsOrAO(clustercoordinate) + if isGroup then + BRAfromBulls = clustercoordinate:ToStringBRA(Group:GetCoordinate()) + end + + local Warnlevel = "Early Warning." + + if self.OpsZone:IsVec2InZone(clustercoordinate:GetVec2()) and not IsBogeyDope then + Warnlevel = "Warning." + elseif IsBogeyDope then + Warnlevel = "" + end + + if IsNew then + Warnlevel = Warnlevel .. " New" + end + + Warnlevel = string.format("%s %s", Warnlevel, threattext) + + if isGroup then + Warnlevel = string.format("%s. %s",grpcallsign,Warnlevel) + end + + -- TTS + local TextTTS = string.format("%s. %s %d ship contact. %s",self.callsigntxt,Warnlevel,threatsize,BRAfromBulls) + + -- TextOutput + local TextScreen = string.format("%s. %s %d ship contact.\n%s\nThreatlevel %s",self.callsigntxt,Warnlevel,threatsize,BRAfromBulls,threattext) + + local RadioEntry = {} -- #AWACS.RadioEntry + RadioEntry.TextTTS = TextTTS + RadioEntry.TextScreen = TextScreen + RadioEntry.IsNew = IsNew + RadioEntry.IsGroup = isGroup + RadioEntry.GroupID = GID + RadioEntry.Duration = STTS.getSpeechTime(TextTTS,1.2,false)+2 or 16 + RadioEntry.ToScreen = true + + self.RadioQueue:Push(RadioEntry) + + return self +end + +--- [Internal] Check for alive OpsGroup from Mission OpsGroups table +-- @param #AWACS self +-- @param #table OpsGroups +-- @return Ops.OpsGroup#OPSGROUP or nil +function AWACS:_GetAliveOpsGroupFromTable(OpsGroups) + self:T(self.lid.."_GetAliveOpsGroupFromTable") + local handback = nil + for _,_OG in pairs(OpsGroups or {}) do + local OG = _OG -- Ops.OpsGroup#OPSGROUP + if OG and OG:IsFlightgroup() and OG:IsAlive() then + handback = OG + self:T("Handing back OG: " .. OG:GetName()) + break + end + end + return handback +end + +--- [Internal] Check Enough AI CAP on Station +-- @param #AWACS self +-- @return #AWACS self +function AWACS:_CheckAICAPOnStation() + self:I(self.lid.."_CheckAICAPOnStation") + if self.MaxAIonCAP > 0 then + local onstation = self.AICAPMissions:Count() + -- control number of AI CAP Flights + if onstation < self.MaxAIonCAP then + -- not enough + local AnchorStackNo,free = self:_GetFreeAnchorStack() + if free then + -- recruit assets (thanks to FF!) + local recruited, assets, airwings = self:RecruitAssets(AUFTRAG.Type.CAP,1,1) + if recruited then + -- yes, there should be ONE asset and ONE AW + + self:I(self.lid..string.format("Recruited %d assets for mission type CAP", #assets)) + local mission = AUFTRAG:NewCAP(self.AnchorZone,20000,350,self.AnchorZone:GetCoordinate(),nil,nil,{}) + + -- Add asset to mission. + if mission then + for _,_asset in pairs(assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + mission:AddAsset(asset) + asset.legion:AddMission(mission) + -- Debug info. + self:I(self.lid..string.format("Assigning mission \"%s\" [%s] to AW \"%s\"", mission.name, mission.type, asset.legion.alias)) + end + end + + -- TODO - necessary? + LEGION.UnRecruitAssets(assets) + + --self.AirWing:AddMission(mission) + self.AICAPMissions:Push(mission,mission.auftragsnummer) + + else + -- no + self:I(self.lid.."Could NOT recruit assets for mission type CAP") + end + end + elseif onstation > self.MaxAIonCAP then + -- too many, send one home + local mission = self.AICAPMissions:Pull() -- Ops.Auftrag#AUFTRAG + local Groups = mission:GetOpsGroups() + local OpsGroup = self:_GetAliveOpsGroupFromTable(Groups) + mission:__Cancel(5) + self:_CheckOut(OpsGroup) + end + -- Check CAP Mission states + if onstation > 0 then + local missionIDs = self.AICAPMissions:GetIDStackSorted() + --self:_CheckInAI(FlightGroup,FlightGroup:GetGroup(),Mission.auftragsnummer) + -- get mission type and state + for _,_MissionID in pairs(missionIDs) do + local mission = self.AICAPMissions:ReadByID(_MissionID) -- Ops.Auftrag#AUFTRAG + self:T("Looking at AuftragsNr " .. mission.auftragsnummer) + local type = mission:GetType() + local state = mission:GetState() + if type == AUFTRAG.Type.CAP then + local OpsGroups = mission:GetOpsGroups() + local OpsGroup = self:_GetAliveOpsGroupFromTable(OpsGroups) + local FGstate = mission:GetGroupStatus(OpsGroup) + -- FG ready? + if FGstate == AUFTRAG.Status.STARTED or FGstate == AUFTRAG.Status.EXECUTING then + -- has this group checked in already? Avoid double tasking + local GID, CheckedInAlready = self:_GetManagedGrpID(OpsGroup:GetGroup()) + if not CheckedInAlready then + self:_SetAIROE(OpsGroup,OpsGroup:GetGroup()) + self:_CheckInAI(OpsGroup,OpsGroup:GetGroup(),mission.auftragsnummer) + end + end + end + end + end + -- cycle mission status + if onstation > 0 then + local report = REPORT:New("CAP Mission Status") + report:Add("===============") + local missionIDs = self.AICAPMissions:GetIDStackSorted() + local i = 1 + for _,_MissionID in pairs(missionIDs) do + --for i=1,self.MaxAIonCAP do + local mission = self.AICAPMissions:ReadByID(_MissionID) -- Ops.Auftrag#AUFTRAG + --local mission = self.AICAPMissions:ReadByPointer(i) -- Ops.Auftrag#AUFTRAG + if mission then + i = i + 1 + report:Add(string.format("Entry %d",i)) + report:Add(string.format("Mission No %d",mission.auftragsnummer)) + report:Add(string.format("Mission Type %s",mission:GetType())) + report:Add(string.format("Mission State %s",mission:GetState())) + local OpsGroups = mission:GetOpsGroups() + local OpsGroup = self:_GetAliveOpsGroupFromTable(OpsGroups) -- Ops.OpsGroup#OPSGROUP + if OpsGroup then + local OpsName = OpsGroup:GetName() or "Unknown" + local OpsCallSign = OpsGroup:GetCallsignName() or "Unknown" + report:Add(string.format("Mission FG %s",OpsName)) + report:Add(string.format("Callsign %s",OpsCallSign)) + report:Add(string.format("Mission FG State %s",OpsGroup:GetState())) + else + report:Add("***** Cannot obtain (yet) this missions OpsGroup!") + end + report:Add(string.format("Target Type %s",mission:GetTargetType())) + end + report:Add("===============") + end + if self.debug then + self:T(report:Text()) + end + end + end + return self +end + +--- [Internal] Set ROE for AI CAP +-- @param #AWACS self +-- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup +-- @param Wrapper.Group#GROUP Group +-- @return #AWACS self +function AWACS:_SetAIROE(FlightGroup,Group) + self:T(self.lid.."_SetAIROE") + local ROE = self.AwacsROE or AWACS.ROE.POLICE + local ROT = self.AwacsROT or AWACS.ROT.PASSIVE + + -- TODO adjust to AWACS set ROE + -- for the time being set to be defensive + Group:OptionAlarmStateGreen() + Group:OptionECM_OnlyLockByRadar() + Group:OptionROEHoldFire() + Group:OptionROTEvadeFire() + Group:OptionRTBBingoFuel(true) + Group:OptionKeepWeaponsOnThreat() + local callname = self.AICAPCAllName or CALLSIGN.F16.Viper + self.AICAPCAllNumber = self.AICAPCAllNumber + 1 + Group:CommandSetCallsign(callname,math.fmod(self.AICAPCAllNumber,9)) + -- FG level + FlightGroup:SetDefaultAlarmstate(AI.Option.Ground.val.ALARM_STATE.GREEN) + FlightGroup:SetDefaultROE(ENUMS.ROE.WeaponHold) + FlightGroup:SetDefaultROT(ENUMS.ROT.EvadeFire) + FlightGroup:SetFuelLowRTB(true) + FlightGroup:SetFuelLowThreshold(0.2) + FlightGroup:SetEngageDetectedOff() + FlightGroup:SetOutOfAAMRTB(true) + return self +end + +-- TODO FSMs +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- [Internal] onafterStart +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AWACS self +function AWACS:onafterStart(From, Event, To) + self:I({From, Event, To}) + + -- Set up control zone + self.ControlZone = ZONE_RADIUS:New(self.OpsZone:GetName(),self.OpsZone:GetVec2(),UTILS.NMToMeters(self.ControlZoneRadius)) + if self.debug then + self.ControlZone:DrawZone(-1,{0,1,0},1,{1,0,0},0.05,3,true) + MARKER:New(self.ControlZone:GetCoordinate(),"Control Zone"):ToAll() + end + + -- set up the AWACS and let it orbit + local AwacsAW = self.AirWing -- Ops.AirWing#AIRWING + local mission = AUFTRAG:NewORBIT_RACETRACK(self.OrbitZone:GetCoordinate(),self.AwacsAngels*1000,self.Speed,self.Heading,self.Leg) + local timeonstation = (self.AwacsTimeOnStation + self.ShiftChangeTime) * 3600 + mission:SetTime(nil,timeonstation) + + AwacsAW:AddMission(mission) + + --[[ callback functions + local function StartSettings(FlightGroup,Mission) + self:_StartSettings(FlightGroup,Mission) + end + + function AwacsAW:OnAfterFlightOnMission(From,Event,To,FlightGroup,Mission) + StartSettings(FlightGroup,Mission) + end + --]] + + self.AwacsMission = mission + self.AwacsInZone = false -- not yet arrived or gone again + + self:__Status(-30) + return self +end + +--- [Internal] onafterStatus +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AWACS self +function AWACS:onafterStatus(From, Event, To) + self:I({From, Event, To}) + + self:_SetClientMenus() + + local awacs = nil + if self.AwacsFG then + awacs = self.AwacsFG:GetGroup() -- Wrapper.Group#GROUP + end + if awacs and awacs:IsAlive() and not self.AwacsInZone then + -- check if we arrived + local orbitzone = self.OrbitZone -- Core.Zone#ZONE + if awacs:IsInZone(orbitzone) then + -- arrived + self.AwacsInZone = true + self:I(self.lid.."Arrived in Orbit Zone: " .. orbitzone:GetName()) + local text = string.format("%s on station for A O %s control.",self.callsigntxt,self.OpsZone:GetName() or "A O") + + local RadioEntry = {} -- #AWACS.RadioEntry + RadioEntry.IsNew = true + RadioEntry.TextTTS = text + RadioEntry.TextScreen = text + RadioEntry.ToScreen = true + RadioEntry.Duration = STTS.getSpeechTime(text,1.1,false) or 8 + + self.RadioQueue:Push(RadioEntry) + self:_StartIntel(awacs) + end + end + + -------------------------------- + -- AWACS + -------------------------------- + + if (awacs and awacs:IsAlive()) then + + -- Check on Awacs Mission Status + local AWmission = self.AwacsMission -- Ops.Auftrag#AUFTRAG + local awstatus = AWmission:GetState() + local AWmissiontime = (timer.getTime() - self.AwacsTimeStamp) + + local AWTOSLeft = UTILS.Round((((self.AwacsTimeOnStation+self.ShiftChangeTime)*3600) - AWmissiontime),0) -- seconds + + AWTOSLeft = UTILS.Round(AWTOSLeft/60,0) -- minutes + + local ChangeTime = UTILS.Round(((self.ShiftChangeTime * 3600)/60),0) + + local Changedue = "No" + + if not self.ShiftChangeAwacsFlag and (AWTOSLeft <= ChangeTime or AWmission:IsOver()) then + Changedue = "Yes" + self.ShiftChangeAwacsFlag = true + self:__AwacsShiftChange(2) + end + + local report = REPORT:New("AWACS:") + report:Add("====================") + report:Add("AWACS:") + report:Add(string.format("Auftrag Status: %s",awstatus)) + report:Add(string.format("TOS Left: %d min",AWTOSLeft)) + report:Add(string.format("Needs ShiftChange: %s",Changedue)) + + local OpsGroups = AWmission:GetOpsGroups() + local OpsGroup = self:_GetAliveOpsGroupFromTable(OpsGroups) -- Ops.OpsGroup#OPSGROUP + if OpsGroup then + local OpsName = OpsGroup:GetName() or "Unknown" + local OpsCallSign = OpsGroup:GetCallsignName() or "Unknown" + report:Add(string.format("Mission FG %s",OpsName)) + report:Add(string.format("Callsign %s",OpsCallSign)) + report:Add(string.format("Mission FG State %s",OpsGroup:GetState())) + else + report:Add("***** Cannot obtain (yet) this missions OpsGroup!") + end + + -- Check for replacement mission - if any + if self.ShiftChangeAwacsFlag and self.ShiftChangeAwacsRequested then -- Ops.Auftrag#AUFTRAG + AWmission = self.EscortMissionReplacement + local esstatus = AWmission:GetState() + local ESmissiontime = (timer.getTime() - self.AwacsTimeStamp) + local ESTOSLeft = UTILS.Round((((self.AwacsTimeOnStation+self.ShiftChangeTime)*3600) - ESmissiontime),0) -- seconds + ESTOSLeft = UTILS.Round(ESTOSLeft/60,0) -- minutes + local ChangeTime = UTILS.Round(((self.ShiftChangeTime * 3600)/60),0) + --local Changedue = "No" + + --report:Add("====================") + report:Add("AWACS REPLACEMENT:") + report:Add(string.format("Auftrag Status: %s",esstatus)) + report:Add(string.format("TOS Left: %d min",ESTOSLeft)) + --report:Add(string.format("Needs ShiftChange: %s",Changedue)) + + local OpsGroups = AWmission:GetOpsGroups() + local OpsGroup = self:_GetAliveOpsGroupFromTable(OpsGroups) -- Ops.OpsGroup#OPSGROUP + if OpsGroup then + local OpsName = OpsGroup:GetName() or "Unknown" + local OpsCallSign = OpsGroup:GetCallsignName() or "Unknown" + report:Add(string.format("Mission FG %s",OpsName)) + report:Add(string.format("Callsign %s",OpsCallSign)) + report:Add(string.format("Mission FG State %s",OpsGroup:GetState())) + else + report:Add("***** Cannot obtain (yet) this missions OpsGroup!") + end + + if AWmission:IsExecuting() then + -- make the actual change in the queue + self.ShiftChangeAwacsFlag = false + self.ShiftChangeAwacsRequested = false + -- cancel old mission + if self.AwacsMission and self.AwacsMission:IsNotOver() then + self.AwacsMission:Cancel() + end + self.AwacsMission = self.AwacsMissionReplacement + self.AwacsMissionReplacement = nil + self.AwacsTimeStamp = timer.getTime() + report:Add("*** Replacement DONE ***") + end + report:Add("====================") + end + + -------------------------------- + -- ESCORTS + -------------------------------- + + if self.HasEscorts then + local ESmission = self.EscortMission -- Ops.Auftrag#AUFTRAG + local esstatus = ESmission:GetState() + local ESmissiontime = (timer.getTime() - self.EscortsTimeStamp) + local ESTOSLeft = UTILS.Round((((self.EscortsTimeOnStation+self.ShiftChangeTime)*3600) - ESmissiontime),0) -- seconds + ESTOSLeft = UTILS.Round(ESTOSLeft/60,0) -- minutes + local ChangeTime = UTILS.Round(((self.ShiftChangeTime * 3600)/60),0) + local Changedue = "No" + + if (ESTOSLeft <= ChangeTime and not self.ShiftChangeEscortsFlag) or (ESmission:IsOver() and not self.ShiftChangeEscortsFlag) then + Changedue = "Yes" + self.ShiftChangeEscortsFlag = true -- set this back when new Escorts arrived + self:__EscortShiftChange(2) + end + + report:Add("====================") + report:Add("ESCORTS:") + report:Add(string.format("Auftrag Status: %s",esstatus)) + report:Add(string.format("TOS Left: %d min",ESTOSLeft)) + report:Add(string.format("Needs ShiftChange: %s",Changedue)) + + local OpsGroups = ESmission:GetOpsGroups() + local OpsGroup = self:_GetAliveOpsGroupFromTable(OpsGroups) -- Ops.OpsGroup#OPSGROUP + if OpsGroup then + local OpsName = OpsGroup:GetName() or "Unknown" + local OpsCallSign = OpsGroup:GetCallsignName() or "Unknown" + report:Add(string.format("Mission FG %s",OpsName)) + report:Add(string.format("Callsign %s",OpsCallSign)) + report:Add(string.format("Mission FG State %s",OpsGroup:GetState())) + else + report:Add("***** Cannot obtain (yet) this missions OpsGroup!") + end + + report:Add("====================") + + -- Check for replacement mission - if any + if self.ShiftChangeEscortsFlag and self.ShiftChangeEscortsRequested then -- Ops.Auftrag#AUFTRAG + ESmission = self.EscortMissionReplacement + local esstatus = ESmission:GetState() + local ESmissiontime = (timer.getTime() - self.EscortsTimeStamp) + local ESTOSLeft = UTILS.Round((((self.EscortsTimeOnStation+self.ShiftChangeTime)*3600) - ESmissiontime),0) -- seconds + ESTOSLeft = UTILS.Round(ESTOSLeft/60,0) -- minutes + local ChangeTime = UTILS.Round(((self.ShiftChangeTime * 3600)/60),0) + --local Changedue = "No" + + --report:Add("====================") + report:Add("ESCORTS REPLACEMENT:") + report:Add(string.format("Auftrag Status: %s",esstatus)) + report:Add(string.format("TOS Left: %d min",ESTOSLeft)) + --report:Add(string.format("Needs ShiftChange: %s",Changedue)) + + local OpsGroups = ESmission:GetOpsGroups() + local OpsGroup = self:_GetAliveOpsGroupFromTable(OpsGroups) -- Ops.OpsGroup#OPSGROUP + if OpsGroup then + local OpsName = OpsGroup:GetName() or "Unknown" + local OpsCallSign = OpsGroup:GetCallsignName() or "Unknown" + report:Add(string.format("Mission FG %s",OpsName)) + report:Add(string.format("Callsign %s",OpsCallSign)) + report:Add(string.format("Mission FG State %s",OpsGroup:GetState())) + else + report:Add("***** Cannot obtain (yet) this missions OpsGroup!") + end + + if ESmission:IsExecuting() then + -- make the actual change in the queue + self.ShiftChangeEscortsFlag = false + self.ShiftChangeEscortsRequested = false + -- cancel old mission + if self.EscortMission and self.EscortMission:IsNotOver() then + self.EscortMission:Cancel() + end + self.EscortMission = self.EscortMissionReplacement + self.EscortMissionReplacement = nil + self.EscortsTimeStamp = timer.getTime() + report:Add("*** Replacement DONE ***") + end + report:Add("====================") + end + end + + if self.debug then + self:I(report:Text()) + end + -- Check on AUFTRAG status for CAP AI + self:_CheckAICAPOnStation() + else + -- do other stuff + + end + + -- Check task queue (both) + self:_CheckTaskQueue() + + self:__Status(30) + return self +end + +--- [Internal] onafterStop +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AWACS self +function AWACS:onafterStop(From, Event, To) + self:T({From, Event, To}) + -- unhandle stuff, exit intel + + self.intel:Stop() + + local AWFiFo = self.CAPAirwings -- Utilities.FiFo#FIFO + local AWStack = AWFiFo:GetPointerStack() + for _ID,_AWID in pairs(AWStack) do + local SubAW = self.CAPAirwings:ReadByPointer(_ID) + if SubAW then + SubAW:RemoveUsingOpsAwacs() + end + end + + return self +end + +--- [Internal] onafterAssignAnchor +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param #number GID Group ID +-- @return #AWACS self +function AWACS:onafterAssignAnchor(From, Event, To, GID) + self:T({From, Event, To, "GID = " .. GID}) + self:_AssignAnchorToID(GID) + return self +end + +--- [Internal] onafterCheckedOut +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param #AWACS.ManagedGroup.GID Group ID +-- @param #number AnchorStackNo +-- @param #number Angels +-- @return #AWACS self +function AWACS:onafterCheckedOut(From, Event, To, GID, AnchorStackNo, Angels) + self:T({From, Event, To, "GID = " .. GID}) + self:_RemoveIDFromAnchor(GID,AnchorStackNo,Angels) + return self +end + +--- [Internal] onafterAssignedAnchor +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param #number GID Managed Group ID +-- @param #AWACS.AnchorData Anchor +-- @param #number AnchorStackNo +-- @return #AWACS self +function AWACS:onafterAssignedAnchor(From, Event, To, GID, Anchor, AnchorStackNo, AnchorAngels) + self:I({From, Event, To, "GID=" .. GID, "Stack=" .. AnchorStackNo}) + -- TODO + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + managedgroup.AnchorStackNo = AnchorStackNo + managedgroup.AnchorStackAngels = AnchorAngels + self.ManagedGrps[GID] = managedgroup + local isPlayer = managedgroup.IsPlayer + local isAI = managedgroup.IsAI + local Group = managedgroup.Group + local CallSign = managedgroup.CallSign or "unknown 1 1" + local AnchorName = Anchor.AnchorZone:GetName() or "unknown" + local AnchorCoordTxt = Anchor.AnchorZoneCoordinateText or "unknown" + local Angels = AnchorAngels or 25 + local AnchorSpeed = self.CapSpeedBase or 220 + local AuftragsNr = managedgroup.CurrentTask + + local textTTS = string.format("%s. %s. Anchor at %s at angels %d doing %d knots. Wait for task assignment.",self.callsigntxt,CallSign,AnchorName,Angels,AnchorSpeed) + local ROEROT = self.AwacsROE.." "..self.AwacsROT + local textScreen = string.format("%s. %s.\nAnchor at %s\nAngels %d\nSpeed %d knots\nCoord %s\nROE %s\nWait for task assignment.",self.callsigntxt,CallSign,AnchorName,Angels,AnchorSpeed,AnchorCoordTxt,ROEROT) + local TextTasking = string.format("%s. %s.\nAnchor at %s\nAngels %d\nSpeed %d knots\nCoord %s\nROE %s",self.callsigntxt,CallSign,AnchorName,Angels,AnchorSpeed,AnchorCoordTxt,ROEROT) + + local RadioEntry = {} -- #AWACS.RadioEntry + RadioEntry.IsNew = true + RadioEntry.TextTTS = textTTS + RadioEntry.TextScreen = textScreen + RadioEntry.GroupID = GID + RadioEntry.IsGroup = isPlayer + RadioEntry.Duration = STTS.getSpeechTime(textTTS,1.0,false) or 10 + RadioEntry.ToScreen = isPlayer + + self.RadioQueue:Push(RadioEntry) + + self:_CreateTaskForGroup(GID,AWACS.TaskDescription.ANCHOR,TextTasking,Anchor.AnchorZone) + + if isAI and AuftragsNr and AuftragsNr > 0 and self.AICAPMissions:HasUniqueID(AuftragsNr) then + -- Change current Auftrag to Orbit at Anchor + local capalt = Angels*1000 + local capspeed = UTILS.KnotsToAltKIAS(self.CapSpeedBase,capalt) + local AnchorMission = AUFTRAG:NewORBIT(Anchor.AnchorZoneCoordinate,capalt,capspeed,0,15) + managedgroup.FlightGroup:AddMission(AnchorMission) + managedgroup.FlightGroup:GetMissionCurrent():__Cancel(5) + self.AICAPMissions:PullByID(AuftragsNr) + self.AICAPMissions:Push(AnchorMission,AnchorMission.auftragsnummer) + managedgroup.CurrentTask = AnchorMission.auftragsnummer + self.ManagedGrps[GID] = managedgroup + end + + return self +end + +--- [Internal] onafterNewCluster +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.Intelligence#INTEL.Cluster Cluster +-- @return #AWACS self +function AWACS:onafterNewCluster(From,Event,To,Cluster) + self:T({From, Event, To, Cluster}) + return self +end + +--- [Internal] onafterNewContact +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.Intelligence#INTEL.Contact Contact +-- @return #AWACS self +function AWACS:onafterNewContact(From,Event,To,Contact) + self:T({From, Event, To, Contact}) + self.Contacts:Push(Contact) + self:_AnnounceContact(Contact,true,nil,false) + return self +end + +--- [Internal] onafterLostContact +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.Intelligence#INTEL.Contact Contact +-- @return #AWACS self +function AWACS:onafterLostContact(From,Event,To,Contact) + self:T({From, Event, To, Contact}) + -- TODO Check Idle, Assigned Tasks for status + return self +end + +--- [Internal] onafterLostCluster +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.Intelligence#INTEL.Cluster Cluster +-- @param Ops.Auftrag#AUFTRAG Mission +-- @return #AWACS self +function AWACS:onafterLostCluster(From,Event,To,Cluster,Mission) + self:T({From, Event, To}) + -- TODO Remove Cluster from Picture + return self +end + +--- [Internal] onafterCheckRadioQueue +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AWACS self +function AWACS:onafterCheckRadioQueue(From,Event,To) + self:T({From, Event, To}) + -- do we have messages queued? + + local nextcall = 10 + if self.RadioQueue:IsNotEmpty() then + local RadioEntry = self.RadioQueue:Pull() -- #AWACS.RadioEntry + + self:T({RadioEntry}) + + if not RadioEntry.FromAI then + -- AI AWACS Speaking + self.AwacsFG:RadioTransmission(RadioEntry.TextTTS,1,false) + else + -- CAP AI speaking + if RadioEntry.GroupID and RadioEntry.GroupID ~= 0 then + local managedgroup = self.ManagedGrps[RadioEntry.GroupID] -- #AWACS.ManagedGroup + if managedgroup and managedgroup.FlightGroup and managedgroup.FlightGroup:IsAlive() then + managedgroup.FlightGroup:RadioTransmission(RadioEntry.TextTTS,1,false) + end + end + end + + if RadioEntry.Duration then nextcall = RadioEntry.Duration end + if RadioEntry.ToScreen and RadioEntry.TextScreen then + if RadioEntry.GroupID and RadioEntry.GroupID ~= 0 then + local managedgroup = self.ManagedGrps[RadioEntry.GroupID] -- #AWACS.ManagedGroup + if managedgroup and managedgroup.Group and managedgroup.Group:IsAlive() then + MESSAGE:New(RadioEntry.TextScreen,20,"AWACS"):ToGroup(managedgroup.Group) + end + else + MESSAGE:New(RadioEntry.TextScreen,20,"AWACS"):ToCoalition(self.coalition) + end + end + end + + if self:Is("Running") then + -- exit if stopped + self:__CheckRadioQueue(nextcall+2) + end + return self +end + +--- [Internal] onafterEscortShiftChange +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AWACS self +function AWACS:onafterEscortShiftChange(From,Event,To) + self:I({From, Event, To}) + -- request new Escorts, check if AWACS-FG still alive first! + if self.AwacsFG and self.ShiftChangeEscortsFlag and not self.ShiftChangeEscortsRequested then + local awacs = self.AwacsFG:GetGroup() -- Wrapper.Group#GROUP + if awacs and awacs:IsAlive() then + -- ok we're good to re-request + self.ShiftChangeEscortsRequested = true + self.EscortsTimeStamp = timer.getTime() + self:_StartEscorts(true) + else + -- should not happen + self:E("**** AWACS group dead at onafterEscortShiftChange!") + end + end + return self +end + +--- [Internal] onafterAwacsShiftChange +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AWACS self +function AWACS:onafterAwacsShiftChange(From,Event,To) + self:I({From, Event, To}) + -- request new Escorts, check if AWACS-FG still alive first! + if self.AwacsFG and self.ShiftChangeAwacsFlag and not self.ShiftChangeAwacsRequested then + + -- ok we're good to re-request + self.ShiftChangeAwacsRequested = true + self.AwacsTimeStamp = timer.getTime() + + -- set up the AWACS and let it orbit + local AwacsAW = self.AirWing -- Ops.AirWing#AIRWING + local mission = AUFTRAG:NewORBIT_RACETRACK(self.OrbitZone:GetCoordinate(),self.AwacsAngels*1000,self.Speed,self.Heading,self.Leg) + local timeonstation = (self.AwacsTimeOnStation + self.ShiftChangeTime) * 3600 + mission:SetTime(nil,timeonstation) + + AwacsAW:AddMission(mission) + + self.AwacsMissionReplacement = mission + + end + return self +end + +--- On after "FlightOnMission". +-- @param #AWACS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup on mission. +-- @param Ops.Auftrag#AUFTRAG Mission The requested mission. +-- @return #AWACS self +function AWACS:onafterFlightOnMission(From, Event, To, FlightGroup, Mission) + self:I({From, Event, To}) + -- coming back from AW, set up the flight + if self:Is("Running") then + self:_StartSettings(FlightGroup,Mission) + end + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- END AWACS +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +end -- end do + +--- Testing +_SETTINGS:SetLocale("en") +_SETTINGS:SetImperial() +_SETTINGS:SetPlayerMenuOff() + +-- We need an AirWing +local AwacsAW = AIRWING:New("AirForce WH-1","AirForce One") +AwacsAW:SetReportOn() +AwacsAW:SetMarker(true) +AwacsAW:SetAirbase(AIRBASE:FindByName(AIRBASE.Caucasus.Kutaisi)) +AwacsAW:SetRespawnAfterDestroyed(900) +AwacsAW:Start() + +-- And a couple of Squads +-- AWACS itself +local Squad_One = SQUADRON:New("Awacs One",2,"Awacs North") +Squad_One:AddMissionCapability({AUFTRAG.Type.ORBIT},100) +Squad_One:SetFuelLowRefuel(false) +Squad_One:SetFuelLowThreshold(0.2) +Squad_One:SetTurnoverTime(10,20) +AwacsAW:AddSquadron(Squad_One) +AwacsAW:NewPayload("Awacs One One",-1,{AUFTRAG.Type.ORBIT},100) + +-- Escorts +local Squad_Two = SQUADRON:New("Escorts",4,"Escorts North") +Squad_Two:AddMissionCapability({AUFTRAG.Type.ESCORT}) +Squad_Two:SetFuelLowRefuel(true) +Squad_Two:SetFuelLowThreshold(0.3) +Squad_Two:SetTurnoverTime(10,20) +AwacsAW:AddSquadron(Squad_Two) +AwacsAW:NewPayload("Escorts",-1,{AUFTRAG.Type.ESCORT},100) + +-- CAP +local Squad_Three = SQUADRON:New("CAP",2,"CAP North") +Squad_Three:AddMissionCapability({AUFTRAG.Type.ALERT5, AUFTRAG.Type.CAP, AUFTRAG.Type.GCICAP, AUFTRAG.Type.INTERCEPT}) +Squad_Three:SetFuelLowRefuel(true) +Squad_Three:SetFuelLowThreshold(0.3) +Squad_Three:SetTurnoverTime(10,20) +AwacsAW:AddSquadron(Squad_Three) +AwacsAW:NewPayload("Aerial-1-2",-1,{AUFTRAG.Type.ALERT5,AUFTRAG.Type.CAP, AUFTRAG.Type.GCICAP, AUFTRAG.Type.INTERCEPT},100) + + +-- We need a secondary AirWing for testing +local AwacsAW2 = AIRWING:New("AirForce WH-2","AirForce Two") +AwacsAW2:SetReportOn() +AwacsAW2:SetMarker(true) +AwacsAW2:SetAirbase(AIRBASE:FindByName(AIRBASE.Caucasus.Beslan)) +AwacsAW2:SetRespawnAfterDestroyed(900) +AwacsAW2:Start() + +-- CAP2 +local Squad_ThreeOne = SQUADRON:New("CAP2",4,"CAP West") +Squad_ThreeOne:AddMissionCapability({AUFTRAG.Type.ALERT5, AUFTRAG.Type.CAP, AUFTRAG.Type.GCICAP, AUFTRAG.Type.INTERCEPT}) +Squad_ThreeOne:SetFuelLowRefuel(true) +Squad_ThreeOne:SetFuelLowThreshold(0.3) +Squad_ThreeOne:SetTurnoverTime(10,20) +AwacsAW2:AddSquadron(Squad_ThreeOne) +AwacsAW2:NewPayload("CAP 2-1",-1,{AUFTRAG.Type.ALERT5,AUFTRAG.Type.CAP, AUFTRAG.Type.GCICAP, AUFTRAG.Type.INTERCEPT},100) + +-- Get AWACS started +local testawacs = AWACS:New("AWACS North",AwacsAW,"blue",AIRBASE.Caucasus.Kutaisi,"Awacs Orbit",ZONE:FindByName("NW Zone"),"Anchor One",255,radio.modulation.AM ) +testawacs:SetEscort(2) +testawacs:SetAwacsDetails(CALLSIGN.AWACS.Darkstar,1,22,230,61,15) +testawacs:SetSRS("E:\\Program Files\\DCS-SimpleRadio-Standalone","female","en-GB",5010,nil) +testawacs:AddCAPAirWing(AwacsAW2) +testawacs:__Start(5) diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index 3c6a4f3e6..8907ad8b6 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -1956,7 +1956,7 @@ function OPSGROUP:RadioTransmission(Text, Delay, SayCallsign) end -- Debug info. - self:I(self.lid..string.format("Radio transmission on %.3f MHz %s: %s", freq, UTILS.GetModulationName(modu), Text)) + self:T(self.lid..string.format("Radio transmission on %.3f MHz %s: %s", freq, UTILS.GetModulationName(modu), Text)) self.msrs:PlayText(Text) end diff --git a/Moose Development/Moose/Utilities/FiFo.lua b/Moose Development/Moose/Utilities/FiFo.lua new file mode 100644 index 000000000..6d8654931 --- /dev/null +++ b/Moose Development/Moose/Utilities/FiFo.lua @@ -0,0 +1,745 @@ +--- **UTILS** - ClassicFiFo Stack. +-- +-- === +-- +-- ## Main Features: +-- +-- * Build a simple multi-purpose FiFo (First-In, First-Out) stack for generic data. +-- * [Wikipedia](https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics) +-- +-- === +-- +-- ### Author: **applevangelist** +-- @module Utils.FiFo +-- @image MOOSE.JPG + +-- Date: April 2022 + +do +--- FIFO class. +-- @type FIFO +-- @field #string ClassName Name of the class. +-- @field #string lid Class id string for output to DCS log file. +-- @field #string version Version of FiFo +-- @field #number counter +-- @field #number pointer +-- @field #table stackbypointer +-- @field #table stackbyid +-- @extends Core.Base#BASE + +--- +-- @type FIFO.IDEntry +-- @field #number pointer +-- @field #table data +-- @field #table uniqueID + +--- +-- @field #FIFO +FIFO = { + ClassName = "FIFO", + lid = "", + version = "0.0.3", + counter = 0, + pointer = 0, + stackbypointer = {}, + stackbyid = {} +} + +--- Instantiate a new FIFO Stack +-- @param #FIFO self +-- @return #FIFO self +function FIFO:New() + -- Inherit everything from BASE class. + local self=BASE:Inherit(self, BASE:New()) + self.pointer = 0 + self.counter = 0 + self.stackbypointer = {} + self.stackbyid = {} + self.uniquecounter = 0 + -- Set some string id for output to DCS.log file. + self.lid=string.format("%s (%s) | ", "FiFo", self.version) + self:T(self.lid .."Created.") + return self +end + +--- Empty FIFO Stack +-- @param #FIFO self +-- @return #FIFO self +function FIFO:Clear() + self:T(self.lid.."Clear") + self.pointer = 0 + self.counter = 0 + self.stackbypointer = nil + self.stackbyid = nil + self.stackbypointer = {} + self.stackbyid = {} + self.uniquecounter = 0 + return self +end + +--- FIFO Push Object to Stack +-- @param #FIFO self +-- @param #table Object +-- @param #string UniqueID (optional) - will default to current pointer + 1. Note - if you intend to use `FIFO:GetIDStackSorted()` keep the UniqueID numerical! +-- @return #FIFO self +function FIFO:Push(Object,UniqueID) + self:T(self.lid.."Push") + self:T({Object,UniqueID}) + self.pointer = self.pointer + 1 + self.counter = self.counter + 1 + local uniID = UniqueID + if not UniqueID then + self.uniquecounter = self.uniquecounter + 1 + uniID = self.uniquecounter + end + self.stackbyid[uniID] = { pointer = self.pointer, data = Object, uniqueID = uniID } + self.stackbypointer[self.pointer] = { pointer = self.pointer, data = Object, uniqueID = uniID } + return self +end + +--- FIFO Pull Object from Stack +-- @param #FIFO self +-- @return #table Object or nil if stack is empty +function FIFO:Pull() + self:T(self.lid.."Pull") + if self.counter == 0 then return nil end + --local object = self.stackbypointer[self.pointer].data + --self.stackbypointer[self.pointer] = nil + local object = self.stackbypointer[1].data + self.stackbypointer[1] = nil + self.counter = self.counter - 1 + --self.pointer = self.pointer - 1 + self:Flatten() + return object +end + +--- FIFO Pull Object from Stack by Pointer +-- @param #FIFO self +-- @param #number Pointer +-- @return #table Object or nil if stack is empty +function FIFO:PullByPointer(Pointer) + self:T(self.lid.."PullByPointer " .. tostring(Pointer)) + if self.counter == 0 then return nil end + local object = self.stackbypointer[Pointer] -- #FIFO.IDEntry + self.stackbypointer[Pointer] = nil + if object then self.stackbyid[object.uniqueID] = nil end + self.counter = self.counter - 1 + self:Flatten() + if object then + return object.data + else + return nil + end +end + + +--- FIFO Read, not Pull, Object from Stack by Pointer +-- @param #FIFO self +-- @param #number Pointer +-- @return #table Object or nil if stack is empty or pointer does not exist +function FIFO:ReadByPointer(Pointer) + self:T(self.lid.."ReadByPointer " .. tostring(Pointer)) + if self.counter == 0 or not Pointer or not self.stackbypointer[Pointer] then return nil end + local object = self.stackbypointer[Pointer] -- #FIFO.IDEntry + return object.data +end + +--- FIFO Read, not Pull, Object from Stack by UniqueID +-- @param #FIFO self +-- @param #number UniqueID +-- @return #table Object data or nil if stack is empty or ID does not exist +function FIFO:ReadByID(UniqueID) + self:T(self.lid.."ReadByID " .. tostring(UniqueID)) + if self.counter == 0 or not UniqueID or not self.stackbyid[UniqueID] then return nil end + local object = self.stackbyid[UniqueID] -- #FIFO.IDEntry + return object.data +end + +--- FIFO Pull Object from Stack by UniqueID +-- @param #FIFO self +-- @param #tableUniqueID +-- @return #table Object or nil if stack is empty +function FIFO:PullByID(UniqueID) + self:T(self.lid.."PullByID " .. tostring(UniqueID)) + if self.counter == 0 then return nil end + local object = self.stackbyid[UniqueID] -- #FIFO.IDEntry + --self.stackbyid[UniqueID] = nil + return self:PullByPointer(object.pointer) +end + +--- FIFO Housekeeping +-- @param #FIFO self +-- @return #FIFO self +function FIFO:Flatten() + self:T(self.lid.."Flatten") + -- rebuild stacks + local pointerstack = {} + local idstack = {} + local counter = 0 + for _ID,_entry in pairs(self.stackbypointer) do + counter = counter + 1 + pointerstack[counter] = { pointer = counter, data = _entry.data, uniqueID = _entry.uniqueID} + end + for _ID,_entry in pairs(pointerstack) do + idstack[_entry.uniqueID] = { pointer = _entry.pointer , data = _entry.data, uniqueID = _entry.uniqueID} + end + self.stackbypointer = nil + self.stackbypointer = pointerstack + self.stackbyid = nil + self.stackbyid = idstack + self.counter = counter + self.pointer = counter + return self +end + +--- FIFO Check Stack is empty +-- @param #FIFO self +-- @return #boolean empty +function FIFO:IsEmpty() + self:T(self.lid.."IsEmpty") + return self.counter == 0 and true or false +end + +--- FIFO Get stack size +-- @param #FIFO self +-- @return #number size +function FIFO:GetSize() + self:T(self.lid.."GetSize") + return self.counter +end + +--- FIFO Get stack size +-- @param #FIFO self +-- @return #number size +function FIFO:Count() + self:T(self.lid.."Count") + return self.counter +end + +--- FIFO Check Stack is NOT empty +-- @param #FIFO self +-- @return #boolean notempty +function FIFO:IsNotEmpty() + self:T(self.lid.."IsNotEmpty") + return not self:IsEmpty() +end + +--- FIFO Get the data stack by pointer +-- @param #FIFO self +-- @return #table Table of #FIFO.IDEntry entries +function FIFO:GetPointerStack() + self:T(self.lid.."GetPointerStack") + return self.stackbypointer +end + +--- FIFO Check if a certain UniqeID exists +-- @param #FIFO self +-- @return #boolean exists +function FIFO:HasUniqueID(UniqueID) + self:T(self.lid.."HasUniqueID") + return self.stackbyid[UniqueID] and true or false +end + +--- FIFO Get the data stack by UniqueID +-- @param #FIFO self +-- @return #table Table of #FIFO.IDEntry entries +function FIFO:GetIDStack() + self:T(self.lid.."GetIDStack") + return self.stackbyid +end + +--- FIFO Get table of UniqueIDs sorted smallest to largest +-- @param #FIFO self +-- @return #table Table with index [1] to [n] of UniqueID entries +function FIFO:GetIDStackSorted() + self:T(self.lid.."GetIDStackSorted") + + local stack = self:GetIDStack() + local idstack = {} + for _id,_entry in pairs(stack) do + idstack[#idstack+1] = _id + + self:T({"pre",_id}) + end + + local function sortID(a, b) + return a < b + end + + table.sort(idstack) + + return idstack +end + +--- FIFO Get table of data entries +-- @param #FIFO self +-- @return #table Raw table indexed [1] to [n] of object entries - might be empty! +function FIFO:GetDataTable() + self:T(self.lid.."GetDataTable") + local datatable = {} + for _,_entry in pairs(self.stackbypointer) do + datatable[#datatable+1] = _entry.data + end + return datatable +end + +--- FIFO Get sorted table of data entries by UniqueIDs (must be numerical UniqueIDs only!) +-- @param #FIFO self +-- @return #table Table indexed [1] to [n] of sorted object entries - might be empty! +function FIFO:GetSortedDataTable() + self:T(self.lid.."GetSortedDataTable") + local datatable = {} + local idtablesorted = self:GetIDStackSorted() + for _,_entry in pairs(idtablesorted) do + datatable[#datatable+1] = self:ReadByID(_entry) + end + return datatable +end + +--- Iterate the FIFO and call an iterator function for the given FIFO data, providing the object for each element of the stack and optional parameters. +-- @param #FIFO self +-- @param #function IteratorFunction The function that will be called. +-- @param #table Arg (Optional) Further Arguments of the IteratorFunction. +-- @param #function Function (Optional) A function returning a #boolean true/false. Only if true, the IteratorFunction is called. +-- @param #table FunctionArguments (Optional) Function arguments. +-- @return #FIFO self +function FIFO:ForEach( IteratorFunction, Arg, Function, FunctionArguments ) + self:T(self.lid.."ForEach") + + local Set = self:GetPointerStack() or {} + Arg = Arg or {} + + local function CoRoutine() + local Count = 0 + for ObjectID, ObjectData in pairs( Set ) do + local Object = ObjectData.data + self:T( {Object} ) + if Function then + if Function( unpack( FunctionArguments or {} ), Object ) == true then + IteratorFunction( Object, unpack( Arg ) ) + end + else + IteratorFunction( Object, unpack( Arg ) ) + end + Count = Count + 1 + end + return true + end + + local co = CoRoutine + + local function Schedule() + + local status, res = co() + self:T( { status, res } ) + + if status == false then + error( res ) + end + if res == false then + return true -- resume next time the loop + end + + return false + end + + Schedule() + + return self +end + +--- FIFO Print stacks to dcs.log +-- @param #FIFO self +-- @return #FIFO self +function FIFO:Flush() + self:T(self.lid.."FiFo Flush") + self:I("FIFO Flushing Stack by Pointer") + for _id,_data in pairs (self.stackbypointer) do + local data = _data -- #FIFO.IDEntry + self:I(string.format("Pointer: %s | Entry: Number = %s Data = %s UniqueID = %s",tostring(_id),tostring(data.pointer),tostring(data.data),tostring(data.uniqueID))) + end + self:I("FIFO Flushing Stack by ID") + for _id,_data in pairs (self.stackbyid) do + local data = _data -- #FIFO.IDEntry + self:I(string.format("ID: %s | Entry: Number = %s Data = %s UniqueID = %s",tostring(_id),tostring(data.pointer),tostring(data.data),tostring(data.uniqueID))) + end + self:I("Counter = " .. self.counter) + self:I("Pointer = ".. self.pointer) + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- End FIFO +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- LIFO +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +do +--- **UTILS** - LiFo Stack. +-- +-- **Main Features:** +-- +-- * Build a simple multi-purpose LiFo (Last-In, First-Out) stack for generic data. +-- +-- === +-- +-- ### Author: **applevangelist** + +--- LIFO class. +-- @type LIFO +-- @field #string ClassName Name of the class. +-- @field #string lid Class id string for output to DCS log file. +-- @field #string version Version of LiFo +-- @field #number counter +-- @field #number pointer +-- @field #table stackbypointer +-- @field #table stackbyid +-- @extends Core.Base#BASE + +--- +-- @type LIFO.IDEntry +-- @field #number pointer +-- @field #table data +-- @field #table uniqueID + +--- +-- @field #LIFO +LIFO = { + ClassName = "LIFO", + lid = "", + version = "0.0.2", + counter = 0, + pointer = 0, + stackbypointer = {}, + stackbyid = {} +} + +--- Instantiate a new LIFO Stack +-- @param #LIFO self +-- @return #LIFO self +function LIFO:New() + -- Inherit everything from BASE class. + local self=BASE:Inherit(self, BASE:New()) + self.pointer = 0 + self.counter = 0 + self.uniquecounter = 0 + self.stackbypointer = {} + self.stackbyid = {} + -- Set some string id for output to DCS.log file. + self.lid=string.format("%s (%s) | ", "LiFo", self.version) + self:T(self.lid .."Created.") + return self +end + +--- Empty LIFO Stack +-- @param #LIFO self +-- @return #LIFO self +function LIFO:Clear() + self:T(self.lid.."Clear") + self.pointer = 0 + self.counter = 0 + self.stackbypointer = nil + self.stackbyid = nil + self.stackbypointer = {} + self.stackbyid = {} + self.uniquecounter = 0 + return self +end + +--- LIFO Push Object to Stack +-- @param #LIFO self +-- @param #table Object +-- @param #string UniqueID (optional) - will default to current pointer + 1 +-- @return #LIFO self +function LIFO:Push(Object,UniqueID) + self:T(self.lid.."Push") + self:T({Object,UniqueID}) + self.pointer = self.pointer + 1 + self.counter = self.counter + 1 + local uniID = UniqueID + if not UniqueID then + self.uniquecounter = self.uniquecounter + 1 + uniID = self.uniquecounter + end + self.stackbyid[uniID] = { pointer = self.pointer, data = Object, uniqueID = uniID } + self.stackbypointer[self.pointer] = { pointer = self.pointer, data = Object, uniqueID = uniID } + return self +end + +--- LIFO Pull Object from Stack +-- @param #LIFO self +-- @return #table Object or nil if stack is empty +function LIFO:Pull() + self:T(self.lid.."Pull") + if self.counter == 0 then return nil end + local object = self.stackbypointer[self.pointer].data + self.stackbypointer[self.pointer] = nil + --local object = self.stackbypointer[1].data + --self.stackbypointer[1] = nil + self.counter = self.counter - 1 + self.pointer = self.pointer - 1 + self:Flatten() + return object +end + +--- LIFO Pull Object from Stack by Pointer +-- @param #LIFO self +-- @param #number Pointer +-- @return #table Object or nil if stack is empty +function LIFO:PullByPointer(Pointer) + self:T(self.lid.."PullByPointer " .. tostring(Pointer)) + if self.counter == 0 then return nil end + local object = self.stackbypointer[Pointer] -- #FIFO.IDEntry + self.stackbypointer[Pointer] = nil + if object then self.stackbyid[object.uniqueID] = nil end + self.counter = self.counter - 1 + self:Flatten() + if object then + return object.data + else + return nil + end +end + +--- LIFO Read, not Pull, Object from Stack by Pointer +-- @param #LIFO self +-- @param #number Pointer +-- @return #table Object or nil if stack is empty or pointer does not exist +function LIFO:ReadByPointer(Pointer) + self:T(self.lid.."ReadByPointer " .. tostring(Pointer)) + if self.counter == 0 or not Pointer or not self.stackbypointer[Pointer] then return nil end + local object = self.stackbypointer[Pointer] -- #LIFO.IDEntry + return object.data +end + +--- LIFO Read, not Pull, Object from Stack by UniqueID +-- @param #LIFO self +-- @param #number UniqueID +-- @return #table Object or nil if stack is empty or ID does not exist +function LIFO:ReadByID(UniqueID) + self:T(self.lid.."ReadByID " .. tostring(UniqueID)) + if self.counter == 0 or not UniqueID or not self.stackbyid[UniqueID] then return nil end + local object = self.stackbyid[UniqueID] -- #LIFO.IDEntry + return object.data +end + +--- LIFO Pull Object from Stack by UniqueID +-- @param #LIFO self +-- @param #tableUniqueID +-- @return #table Object or nil if stack is empty +function LIFO:PullByID(UniqueID) + self:T(self.lid.."PullByID " .. tostring(UniqueID)) + if self.counter == 0 then return nil end + local object = self.stackbyid[UniqueID] -- #LIFO.IDEntry + --self.stackbyid[UniqueID] = nil + return self:PullByPointer(object.pointer) +end + +--- LIFO Housekeeping +-- @param #LIFO self +-- @return #LIFO self +function LIFO:Flatten() + self:T(self.lid.."Flatten") + -- rebuild stacks + local pointerstack = {} + local idstack = {} + local counter = 0 + for _ID,_entry in pairs(self.stackbypointer) do + counter = counter + 1 + pointerstack[counter] = { pointer = counter, data = _entry.data, uniqueID = _entry.uniqueID} + end + for _ID,_entry in pairs(pointerstack) do + idstack[_entry.uniqueID] = { pointer = _entry.pointer , data = _entry.data, uniqueID = _entry.uniqueID} + end + self.stackbypointer = nil + self.stackbypointer = pointerstack + self.stackbyid = nil + self.stackbyid = idstack + self.counter = counter + self.pointer = counter + return self +end + +--- LIFO Check Stack is empty +-- @param #LIFO self +-- @return #boolean empty +function LIFO:IsEmpty() + self:T(self.lid.."IsEmpty") + return self.counter == 0 and true or false +end + +--- LIFO Get stack size +-- @param #LIFO self +-- @return #number size +function LIFO:GetSize() + self:T(self.lid.."GetSize") + return self.counter +end + +--- LIFO Get stack size +-- @param #LIFO self +-- @return #number size +function LIFO:Count() + self:T(self.lid.."Count") + return self.counter +end + +--- LIFO Check Stack is NOT empty +-- @param #LIFO self +-- @return #boolean notempty +function LIFO:IsNotEmpty() + self:T(self.lid.."IsNotEmpty") + return not self:IsEmpty() +end + +--- LIFO Get the data stack by pointer +-- @param #LIFO self +-- @return #table Table of #LIFO.IDEntry entries +function LIFO:GetPointerStack() + self:T(self.lid.."GetPointerStack") + return self.stackbypointer +end + +--- LIFO Get the data stack by UniqueID +-- @param #LIFO self +-- @return #table Table of #LIFO.IDEntry entries +function LIFO:GetIDStack() + self:T(self.lid.."GetIDStack") + return self.stackbyid +end + +--- LIFO Get table of UniqueIDs sorted smallest to largest +-- @param #LIFO self +-- @return #table Table of #LIFO.IDEntry entries +function LIFO:GetIDStackSorted() + self:T(self.lid.."GetIDStackSorted") + + local stack = self:GetIDStack() + local idstack = {} + for _id,_entry in pairs(stack) do + idstack[#idstack+1] = _id + + self:T({"pre",_id}) + end + + local function sortID(a, b) + return a < b + end + + table.sort(idstack) + + return idstack +end + +--- LIFO Check if a certain UniqeID exists +-- @param #LIFO self +-- @return #boolean exists +function LIFO:HasUniqueID(UniqueID) + self:T(self.lid.."HasUniqueID") + return self.stackbyid[UniqueID] and true or false +end + +--- LIFO Print stacks to dcs.log +-- @param #LIFO self +-- @return #LIFO self +function LIFO:Flush() + self:T(self.lid.."FiFo Flush") + self:I("LIFO Flushing Stack by Pointer") + for _id,_data in pairs (self.stackbypointer) do + local data = _data -- #LIFO.IDEntry + self:I(string.format("Pointer: %s | Entry: Number = %s Data = %s UniqueID = %s",tostring(_id),tostring(data.pointer),tostring(data.data),tostring(data.uniqueID))) + end + self:I("LIFO Flushing Stack by ID") + for _id,_data in pairs (self.stackbyid) do + local data = _data -- #LIFO.IDEntry + self:I(string.format("ID: %s | Entry: Number = %s Data = %s UniqueID = %s",tostring(_id),tostring(data.pointer),tostring(data.data),tostring(data.uniqueID))) + end + self:I("Counter = " .. self.counter) + self:I("Pointer = ".. self.pointer) + return self +end + +--- LIFO Get table of data entries +-- @param #LIFO self +-- @return #table Raw table indexed [1] to [n] of object entries - might be empty! +function LIFO:GetDataTable() + self:T(self.lid.."GetDataTable") + local datatable = {} + for _,_entry in pairs(self.stackbypointer) do + datatable[#datatable+1] = _entry.data + end + return datatable +end + +--- LIFO Get sorted table of data entries by UniqueIDs (must be numerical UniqueIDs only!) +-- @param #LIFO self +-- @return #table Table indexed [1] to [n] of sorted object entries - might be empty! +function LIFO:GetSortedDataTable() + self:T(self.lid.."GetSortedDataTable") + local datatable = {} + local idtablesorted = self:GetIDStackSorted() + for _,_entry in pairs(idtablesorted) do + datatable[#datatable+1] = self:ReadByID(_entry) + end + return datatable +end + +--- Iterate the LIFO and call an iterator function for the given LIFO data, providing the object for each element of the stack and optional parameters. +-- @param #LIFO self +-- @param #function IteratorFunction The function that will be called. +-- @param #table Arg (Optional) Further Arguments of the IteratorFunction. +-- @param #function Function (Optional) A function returning a #boolean true/false. Only if true, the IteratorFunction is called. +-- @param #table FunctionArguments (Optional) Function arguments. +-- @return #LIFO self +function LIFO:ForEach( IteratorFunction, Arg, Function, FunctionArguments ) + self:T(self.lid.."ForEach") + + local Set = self:GetPointerStack() or {} + Arg = Arg or {} + + local function CoRoutine() + local Count = 0 + for ObjectID, ObjectData in pairs( Set ) do + local Object = ObjectData.data + self:T( {Object} ) + if Function then + if Function( unpack( FunctionArguments or {} ), Object ) == true then + IteratorFunction( Object, unpack( Arg ) ) + end + else + IteratorFunction( Object, unpack( Arg ) ) + end + Count = Count + 1 + end + return true + end + + local co = CoRoutine + + local function Schedule() + + local status, res = co() + self:T( { status, res } ) + + if status == false then + error( res ) + end + if res == false then + return true -- resume next time the loop + end + + return false + end + + Schedule() + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- End LIFO +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +end \ No newline at end of file diff --git a/Moose Development/Moose/Utilities/STTS.lua b/Moose Development/Moose/Utilities/STTS.lua index 97836a1ee..dccbdd67b 100644 --- a/Moose Development/Moose/Utilities/STTS.lua +++ b/Moose Development/Moose/Utilities/STTS.lua @@ -125,6 +125,9 @@ end -- -- * (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min -- +-- @param #number length can also be passed as #string +-- @param #number speed Defaults to 1.0 +-- @param #boolean isGoogle We're using Google TTS function STTS.getSpeechTime(length,speed,isGoogle) local maxRateRatio = 3 diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 7c7bd66fe..f45cd7ab5 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -1475,7 +1475,49 @@ function UTILS.GetCallsignName(Callsign) return name end end - + + for name, value in pairs(CALLSIGN.B1B) do + if value==Callsign then + return name + end + end + + for name, value in pairs(CALLSIGN.B52) do + if value==Callsign then + return name + end + end + + for name, value in pairs(CALLSIGN.F15E) do + if value==Callsign then + return name + end + end + + for name, value in pairs(CALLSIGN.F16) do + if value==Callsign then + return name + end + end + + for name, value in pairs(CALLSIGN.F18) do + if value==Callsign then + return name + end + end + + for name, value in pairs(CALLSIGN.FARP) do + if value==Callsign then + return name + end + end + + for name, value in pairs(CALLSIGN.TransportAircraft) do + if value==Callsign then + return name + end + end + return "Ghostrider" end @@ -2356,214 +2398,45 @@ function UTILS.LoadStationaryListOfStatics(Path,Filename,Reduce) return datatable end --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- FIFO -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - -do ---- **UTILS** - FiFo Stack. --- --- **Main Features:** --- --- * Build a simple multi-purpose FiFo (First-In, First-Out) stack for generic data. --- --- === --- --- ### Author: **applevangelist** --- @module Utils.FiFo --- @image MOOSE.JPG - - ---- FIFO class. --- @type FIFO --- @field #string ClassName Name of the class. --- @field #boolean debug --- @field #string lid Class id string for output to DCS log file. --- @field #string version Version of FiFo --- @field #number counter --- @field #number pointer --- @field #number nextin --- @field #number nextout --- @field #table stackbypointer --- @field #table stackbyid --- @extends Core.Base#BASE --- - ---- --- @type FIFO.IDEntry --- @field #number pointer --- @field #table data --- @field #table uniqueID - ---- --- @field #FIFO -FIFO = { - ClassName = "FIFO", - debug = true, - lid = "", - version = "0.0.1", - counter = 0, - pointer = 0, - nextin = 0, - nextout = 0, - stackbypointer = {}, - stackbyid = {} -} - ---- Instantiate a new FIFO Stack --- @param #FIFO self --- @return #FIFO self -function FIFO:New() - -- Inherit everything from BASE class. - local self=BASE:Inherit(self, BASE:New()) -- #INTEL - self.pointer = 0 - self.counter = 0 - self.stackbypointer = {} - self.stackbyid = {} - -- Set some string id for output to DCS.log file. - self.lid=string.format("%s (%s) | ", "FiFo", self.version) - self:I(self.lid .."Created.") - return self -end - ---- FIFO Push Object to Stack --- @param #FIFO self --- @param #table Object --- @param #string UniqueID (optional) - will default to current pointer + 1 --- @return #FIFO self -function FIFO:Push(Object,UniqueID) - self:T(self.lid.."Push") - self:T({Object,UniqueID}) - self.pointer = self.pointer + 1 - self.counter = self.counter + 1 - self.stackbypointer[self.pointer] = { pointer = self.pointer, data = Object, uniqueID = UniqueID } - if UniqueID then - self.stackbyid[UniqueID] = { pointer = self.pointer, data = Object, uniqueID = UniqueID } - else - self.stackbyid[self.pointer] = { pointer = self.pointer, data = Object, uniqueID = UniqueID } +--- Heading Degrees (0-360) to Cardinal +-- @param #number Heading The heading +-- @return #string Cardinal, e.g. "NORTH" +function UTILS.BearingToCardinal(Heading) + if Heading >= 0 and Heading <= 22 then return "North" + elseif Heading >= 23 and Heading <= 66 then return "North-East" + elseif Heading >= 67 and Heading <= 101 then return "East" + elseif Heading >= 102 and Heading <= 146 then return "South-East" + elseif Heading >= 147 and Heading <= 201 then return "South" + elseif Heading >= 202 and Heading <= 246 then return "South-West" + elseif Heading >= 247 and Heading <= 291 then return "West" + elseif Heading >= 292 and Heading <= 338 then return "North-West" + elseif Heading >= 339 then return "North" end - return self end ---- FIFO Pull Object from Stack --- @param #FIFO self --- @return #table Object or nil if stack is empty -function FIFO:Pull() - self:T(self.lid.."Pull") - if self.counter == 0 then return nil end - local object = self.stackbypointer[self.pointer].data - self.stackbypointer[self.pointer] = nil - self.counter = self.counter - 1 - self.pointer = self.pointer - 1 - self:Flatten() - return object -end - ---- FIFO Pull Object from Stack by Pointer --- @param #FIFO self --- @param #number Pointer --- @return #table Object or nil if stack is empty -function FIFO:PullByPointer(Pointer) - self:T(self.lid.."PullByPointer " .. tostring(Pointer)) - if self.counter == 0 then return nil end - local object = self.stackbypointer[Pointer] -- #FIFO.IDEntry - self.stackbypointer[Pointer] = nil - self.stackbyid[object.uniqueID] = nil - self.counter = self.counter - 1 - self:Flatten() - return object.data -end - ---- FIFO Pull Object from Stack by UniqueID --- @param #FIFO self --- @param #tableUniqueID --- @return #table Object or nil if stack is empty -function FIFO:PullByID(UniqueID) - self:T(self.lid.."PullByID " .. tostring(UniqueID)) - if self.counter == 0 then return nil end - local object = self.stackbyid[UniqueID] -- #FIFO.IDEntry - --self.stackbyid[UniqueID] = nil - return self:PullByPointer(object.pointer) -end - ---- FIFO Housekeeping --- @param #FIFO self --- @return #FIFO self -function FIFO:Flatten() - self:T(self.lid.."Flatten") - -- rebuild stacks - local pointerstack = {} - local idstack = {} - local counter = 0 - for _ID,_entry in pairs(self.stackbypointer) do - counter = counter + 1 - pointerstack[counter] = { pointer = counter, data = _entry.data, uniqueID = _entry.uniqueID} +--- Create a BRAA NATO call string BRAA between two GROUP objects +-- @param Wrapper.Group#GROUP FromGrp GROUP object +-- @param Wrapper.Group#GROUP ToGrp GROUP object +-- @return #string Formatted BRAA NATO call +function UTILS.ToStringBRAANATO(FromGrp,ToGrp) + local BRAANATO = "Merged." + local GroupNumber = FromGrp:GetSize() + local GroupWords = "Singleton" + if GroupNumber == 2 then GroupWords = "Two-Ship" + elseif GroupNumber >= 3 then GroupWords = "Heavy" end - for _ID,_entry in pairs(pointerstack) do - idstack[_entry.uniqueID] = { pointer = _entry.pointer , data = _entry.data, uniqueID = _entry.uniqueID} + local grpLeadUnit = ToGrp:GetUnit(1) + local tgtCoord = grpLeadUnit:GetCoordinate() + local currentCoord = FromGrp:GetCoordinate() + local hdg = UTILS.Round(ToGrp:GetHeading()/100,1)*100 + local bearing = UTILS.Round(currentCoord:HeadingTo(tgtCoord),0) + local rangeMetres = tgtCoord:Get2DDistance(currentCoord) + local rangeNM = UTILS.Round( UTILS.MetersToNM(rangeMetres), 0) + local aspect = tgtCoord:ToStringAspect(currentCoord) + local alt = UTILS.Round(UTILS.MetersToFeet(grpLeadUnit:GetAltitude())/1000,0)--*1000 + local track = UTILS.BearingToCardinal(hdg) + if rangeNM > 3 then + BRAANATO = string.format("%s, BRAA, %s, %d miles, Angels %d, %s, Track %s, Spades.",GroupWords,bearing, rangeNM, alt, aspect, track) end - self.stackbypointer = nil - self.stackbypointer = pointerstack - self.stackbyid = nil - self.stackbyid = idstack - self.counter = counter - self.pointer = counter - return self -end - ---- FIFO Check Stack is empty --- @param #FIFO self --- @return #boolean empty -function FIFO:IsEmpty() - self:T(self.lid.."IsEmpty") - return self.counter == 0 and true or false -end - ---- FIFO Check Stack is NOT empty --- @param #FIFO self --- @return #boolean notempty -function FIFO:IsNotEmpty() - self:T(self.lid.."IsNotEmpty") - return not self:IsEmpty() -end - ---- FIFO Get the data stack by pointer --- @param #FIFO self --- @return #table Table of #FIFO.IDEntry entries -function FIFO:GetPointerStack() - self:T(self.lid.."GetPointerStack") - return self.stackbypointer -end - ---- FIFO Get the data stack by UniqueID --- @param #FIFO self --- @return #table Table of #FIFO.IDEntry entries -function FIFO:GetIDStack() - self:T(self.lid.."GetIDStack") - return self.stackbyid -end - ---- FIFO Print stacks to dcs.log --- @param #FIFO self --- @return #FIFO self -function FIFO:Flush() - self:T(self.lid.."FiFo Flush") - self:I("FIFO Flushing Stack by Pointer") - for _id,_data in pairs (self.stackbypointer) do - local data = _data -- #FIFO.IDEntry - self:I(string.format("Pointer: %s | Entry: Number = %s Data = %s UniID = %s",tostring(_id),tostring(data.pointer),tostring(data.data),tostring(data.uniqueID))) - end - self:I("FIFO Flushing Stack by ID") - for _id,_data in pairs (self.stackbyid) do - local data = _data -- #FIFO.IDEntry - self:I(string.format("ID: %s | Entry: Number = %s Data = %s UniID = %s",tostring(_id),tostring(data.pointer),tostring(data.data),tostring(data.uniqueID))) - end - self:I("Counter = " .. self.counter) - self:I("Pointer = ".. self.pointer) - return self -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- End FIFO -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + return BRAANATO end \ No newline at end of file diff --git a/Moose Development/Moose/Wrapper/Marker.lua b/Moose Development/Moose/Wrapper/Marker.lua index 6dfcd4f71..3dbffa91e 100644 --- a/Moose Development/Moose/Wrapper/Marker.lua +++ b/Moose Development/Moose/Wrapper/Marker.lua @@ -588,7 +588,7 @@ end --- Set text that is displayed in the marker panel. Note this does not show the marker. -- @param #MARKER self --- @param #string Text Marker text. Default is an empty sting "". +-- @param #string Text Marker text. Default is an empty string "". -- @return #MARKER self function MARKER:SetText(Text) self.text=Text and tostring(Text) or "" diff --git a/Moose Setup/Moose.files b/Moose Setup/Moose.files index 33a49c594..e6dddf6ee 100644 --- a/Moose Setup/Moose.files +++ b/Moose Setup/Moose.files @@ -5,6 +5,7 @@ Utilities/Enums.lua Utilities/Profiler.lua Utilities/Templates.lua Utilities/STTS.lua +Utilities/FiFo.lua Core/Base.lua Core/Beacon.lua