--- **Ops** - AWACS -- -- === -- -- **AWACS** - MOOSE AI AWACS Operations using text-to-speech. -- -- === -- -- ## Example Missions: -- -- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20Awacs/). -- -- ## Videos: -- -- Demo videos can be found on [Youtube](https://www.youtube.com/watch?v=ocdy8QzTNN4&list=PLFxp425SeXnq-oS0DSjam1HtddywH8i_k) -- -- === -- -- ### Author: **applevangelist** -- @date Last Update June 2022 -- @module Ops.AWACS -- @image OPS_AWACS.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 e.g."blue" -- @field Core.Zone#ZONE OpsZone, -- @field Core.Zone#ZONE StationZone, -- @field Core.Zone#ZONE BorderZone, -- @field Core.Zone#ZONE RejectZone, -- @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 ManagedTasks -- @field Utilities.FiFo#FIFO PictureAO -- @field Utilities.FiFo#FIFO PictureEWR -- @field Utilities.FiFo#FIFO Contacts -- @field #table CatchAllMissions -- @field #table CatchAllFGs -- @field #number Countactcounter -- @field Utilities.FiFo#FIFO ContactsAO -- @field Utilities.FiFo#FIFO RadioQueue -- @field Utilities.FiFo#FIFO PrioRadioQueue -- @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 -- @field #AWACS.MonitoringData MonitoringData -- @field #boolean MonitoringOn -- @field Core.Set#SET_CLIENT clientset -- @field Utilities.FiFo#FIFO FlightGroups -- @field #number PictureInterval Interval in seconds for general picture -- @field #number PictureTimeStamp Interval timestamp -- @field #number maxassigndistance Only assing AI/Pilots to targets max this far away -- @field #boolean PlayerGuidance if true additional callouts to guide/warn players -- @field #boolean ModernEra if true we get more intel on targets, and EPLR on the AIC -- @field #boolean callsignshort if true use short (group) callsigns, e.g. "Ghost 1", else "Ghost 1 1" -- @field #number MeldDistance 25nm - distance for "Meld" Call , usually shortly before the actual engagement -- @field #number TacDistance 30nm - distance for "TAC" Call -- @field #number ThreatDistance 15nm - distance to declare untargeted (new) threats -- @field #string AOName name of the FEZ, e.g. Rock -- @field Core.Point#COORDINATE AOCoordinate Coordinate of bulls eye -- @field Utilities.FiFo#FIFO clientmenus -- @field #number RadarBlur Radar blur in % -- @field #number ReassignmentPause Wait this many seconds before re-assignment of a player -- @field #boolean NoGroupTags Set to true if you don't want group tags. -- @field #boolean SuppressScreenOutput Set to true to suppress all screen output. -- @field #boolean NoMissileCalls Suppress missile callouts -- @field #boolean PlayerCapAssigment Assign players to CAP tasks when they are logged on -- @field #number GoogleTTSPadding -- @field #number WindowsTTSPadding -- @field #boolean AllowMarkers -- @field #string PlayerStationName -- @extends Core.Fsm#FSM --- -- -- *Of all men\'s miseries the bitterest is this: to know so much and to have control over nothing.* (Herodotus) -- -- === -- -- # AWACS AI Air Controller -- -- * WIP (beta) -- * AWACS replacement for the in-game AWACS -- * Will control a fighter engagement zone and assign tasks to AI and human CAP flights -- * Callouts referenced from: -- ** References from ARN33396 ATP 3-52.4 (Sep 2021) (Combined Forces) -- ** References from CNATRA P-877 (Rev 12-20) (NAVY) -- * FSM events that the mission designer can hook into -- -- ## 1 Prerequisites -- -- The radio callouts in this class are ***exclusively*** created with Text-To-Speech (TTS), based on the Moose @{Sound.SRS} Class, and output is via [Ciribob's SRS system](https://github.com/ciribob/DCS-SimpleRadioStandalone/releases) -- Ensure you have this covered and working before tackling this class. TTS generation can thus be done via the Windows built-in system or via Google TTS; -- the latter offers a wider range of voices and options, but you need to set up your own Google product account for this to work correctly. -- -- ## 2 Mission Design - Operational Priorities -- -- Basic operational target of the AWACS is to control a Fighter Engagement Zone, or FEZ, and defend itself. -- -- ## 3 Airwing(s) -- -- The AWACS plane, the optional escort planes, and the AI CAP planes work based on the @{Ops.AirWing} class. Read and understand the manual for this class in -- order to set everything up correctly. You will at least need one Squadron containing the AWACS plane itself. -- -- Set up the AirWing -- -- local AwacsAW = AIRWING:New("AirForce WH-1","AirForce One") -- AwacsAW:SetMarker(false) -- AwacsAW:SetAirbase(AIRBASE:FindByName(AIRBASE.Caucasus.Kutaisi)) -- AwacsAW:SetRespawnAfterDestroyed(900) -- AwacsAW:SetTakeoffAir() -- AwacsAW:__Start(2) -- -- Add the AWACS template Squadron - **Note**: remove the task AWACS in the mission editor under "Advanced Waypoint Actions" from the template to remove the DCS F10 AWACS menu -- -- local Squad_One = SQUADRON:New("Awacs One",2,"Awacs North") -- Squad_One:AddMissionCapability({AUFTRAG.Type.ORBIT},100) -- Squad_One:SetFuelLowRefuel(true) -- Squad_One:SetFuelLowThreshold(0.2) -- Squad_One:SetTurnoverTime(10,20) -- AwacsAW:AddSquadron(Squad_One) -- AwacsAW:NewPayload("Awacs One One",-1,{AUFTRAG.Type.ORBIT},100) -- -- Add Escorts Squad (recommended, optional) -- -- 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) -- Squad_Two:SetTakeoffAir() -- Squad_Two:SetRadio(255,radio.modulation.AM) -- AwacsAW:AddSquadron(Squad_Two) -- AwacsAW:NewPayload("Escorts",-1,{AUFTRAG.Type.ESCORT},100) -- -- Add CAP Squad (recommended, optional) -- -- local Squad_Three = SQUADRON:New("CAP",10,"CAP North") -- Squad_Three:AddMissionCapability({AUFTRAG.Type.ALERT5, AUFTRAG.Type.CAP, AUFTRAG.Type.GCICAP, AUFTRAG.Type.INTERCEPT},80) -- Squad_Three:SetFuelLowRefuel(true) -- Squad_Three:SetFuelLowThreshold(0.3) -- Squad_Three:SetTurnoverTime(10,20) -- Squad_Three:SetTakeoffAir() -- Squad_Two:SetRadio(255,radio.modulation.AM) -- AwacsAW:AddSquadron(Squad_Three) -- AwacsAW:NewPayload("Aerial-1-2",-1,{AUFTRAG.Type.ALERT5,AUFTRAG.Type.CAP, AUFTRAG.Type.GCICAP, AUFTRAG.Type.INTERCEPT},100) -- -- ## 4 Zones -- -- For the setup, you need to set up a couple of zones: -- -- * An Orbit Zone, where your AWACS will orbit -- * A Fighter Engagement Zone or FEZ -- * A zone where your CAP flights will be stationed, waiting for assignments -- * Optionally, an additional zone you wish to defend -- * Optionally, a border of the opposing party -- * Also, and move your BullsEye in the mission accordingly - this will be the key reference point for most AWACS callouts -- -- ### 4.1 Strategic considerations -- -- Your AWACS is an HVT or high-value-target. Thus it makes sense to position the Orbit Zone in a way that your FEZ and thus your CAP flights defend it. -- It should hence be positioned behind the FEZ, away from the direction of enemy engagement. -- The zone for CAP stations should be close to the FEZ, but not inside it. -- The optional additional defense zone can be anywhere, but keep an eye on the location so your CAP flights don't take ages to get there. -- The optional border is useful for e.g. "cold war" scenarios - planes across the border will not be considered as targets by AWACS. -- -- ## 5 Set up AWACS -- -- -- Set up AWACS called "AWACS North". It will use the AwacsAW AirWing set up above and be of the "blue" coalition. Homebase is Kutaisi. -- -- The AWACS Orbit Zone is a round zone set in the mission editor named "Awacs Orbit", the FEZ is a Polygon-Zone called "Rock" we have also -- -- set up in the mission editor with a late activated helo named "Rock#ZONE_POLYGON". Note this also sets the BullsEye to be referenced as "Rock". -- -- The CAP station zone is called "Fremont". We will be on 255 AM. -- local testawacs = AWACS:New("AWACS North",AwacsAW,"blue",AIRBASE.Caucasus.Kutaisi,"Awacs Orbit",ZONE:FindByName("Rock"),"Fremont",255,radio.modulation.AM ) -- -- set two escorts -- testawacs:SetEscort(2) -- -- Callsign will be "Focus". We'll be a Angels 30, doing 300 knots, orbit leg to 88deg with a length of 25nm. -- testawacs:SetAwacsDetails(CALLSIGN.AWACS.Focus,1,30,300,88,25) -- -- Set up SRS on port 5010 - change the below to your path and port -- testawacs:SetSRS("C:\\Program Files\\DCS-SimpleRadio-Standalone","female","en-GB",5010) -- -- Add a "red" border we don't want to cross, set up in the mission editor with a late activated helo named "Red Border#ZONE_POLYGON" -- testawacs:SetRejectionZone(ZONE:FindByName("Red Border")) -- -- Our CAP flight will have the callsign "Ford", we want 4 AI planes, Time-On-Station is four hours, doing 300 kn IAS. -- testawacs:SetAICAPDetails(CALLSIGN.Aircraft.Ford,4,4,300) -- -- We're modern (default), e.g. we have EPLRS and get more fill-in information on detections -- testawacs:SetModernEra() -- -- And start -- testawacs:__Start(5) -- -- ## 6 Menu entries -- -- **Note on Radio Menu entries**: Due to a DCS limitation, these are on GROUP level and not individual (UNIT level). Hence, either put each player in his/her own group, -- or ensure that only the flight lead will use the menu. Recommend the 1st option, unless you have a disciplined team. -- -- ### 6.1 Check-in -- -- In the base setup, you need to check in to the AWACS to get the full menu. This can be done once the AWACS is airborne. You will get an Alpha Check callout -- and be assigned a CAP station. -- -- ### 6.2 Check-out -- -- You can check-out anytime, of course. -- -- ### 6.3 Picture -- -- Get a picture from the AWACS. It will call out the three most important groups. References are **always** to the (named) BullsEye position. -- **Note** that AWACS will anyway do a regular picture call to all stations every five minutes. -- -- ### 6.4 Bogey Dope -- -- Get bogey dope from the AWACS. It will call out the closest bogey group, if any. Reference is BRAA to the Player position. -- -- ### 6.5 Declare -- -- AWACS will declare, if the bogey closest to the calling player in a 3nm circle is hostile, friendly or neutral. -- -- ### 6.6 Tasking -- -- Tasking will show you the current task with "Showtask". Updated directions are shown, also. -- You can decline a **requested** task with "unable", and abort **any task but CAP station** with "abort". -- You can "commit" to a requested task within 3 minutes. -- "VID" - if AWACS is set to Visial ID or VID oncoming planes first, there will also be an "VID" entry. Similar to "Declare" you can declare the requested contact -- to be hostile, friendly or neutral if you are close enough to it (3nm). If hostile, at the time of writing, an engagement task will be assigned to you (not: requested). -- If neutral/friendly, contact will be excluded from further tasking. -- -- ## 7 Air-to-Air Timeline Support -- -- To support your engagement timeline, AWACS will make Tac-Range, Meld, Merge and Threat call-outs to the player/group (Figure 7-3, CNATRA P-877). Default settings in NM are -- -- Tac Distance = 45 -- Meld Distance = 35 -- Threat Distance = 25 -- Merge Distance = 5 -- -- ## 8 Bespoke Player CallSigns -- -- Append the GROUP name of your client slots with "#CallSign" to use bespoke callsigns in AWACS callouts. E.g. "Player F14#Ghostrider" will be refered to -- as "Ghostrider" plus group number, e.g. "Ghostrider 9". -- -- ## 9 Options -- -- There's a number of functions available, to set various options for the setup. -- -- * @{#AWACS.SetBullsEyeAlias}() : Set the alias name of the Bulls Eye. -- * @{#AWACS.SetTOS}() : Set time on station for AWACS and CAP. -- * @{#AWACS.SetReassignmentPause}() : Pause this number of seconds before re-assigning a Player to a task. -- * @{#AWACS.SuppressScreenMessages}() : Suppress message output on screen. -- * @{#AWACS.SetRadarBlur}() : Set the radar blur faktor in percent. -- * @{#AWACS.SetColdWar}() : Set to cold war - no fill-ins, no EPLRS, VID as standard. -- * @{#AWACS.SetModernEraDefensive}() : Set to modern, EPLRS, BVR/IFF engagement, fill-ins. -- * @{#AWACS.SetModernEraAgressive}() : Set to modern, EPLRS, BVR/IFF engagement, fill-ins. -- * @{#AWACS.SetPolicingModern}() : Set to modern, EPLRS, VID engagement, fill-ins. -- * @{#AWACS.SetPolicingColdWar}() : Set to cold war, no EPLRS, VID engagement, no fill-ins. -- * @{#AWACS.SetInterceptTimeline}() : Set distances for TAC, Meld and Threat range calls. -- * @{#AWACS.SetAdditionalZone}() : Add one additional defense zone, e.g. own border. -- * @{#AWACS.SetRejectionZone}() : Add one foreign border. Targets beyond will be ignored for tasking. -- * @{#AWACS.DrawFEZ}() : Show the FEZ on the F10 map. -- * @{#AWACS.SetAWACSDetails}() : Set AWACS details. -- * @{#AWACS.AddGroupToDetection}() : Add a group object to INTEL detection, e.g. EWR. -- * @{#AWACS.SetSRS}() : Set SRS details. -- * @{#AWACS.SetSRSVoiceCAP}() : Set voice details for AI CAP planes, using Windows dektop TTS. -- * @{#AWACS.SetAICAPDetails}() : Set AI CAP details. -- * @{#AWACS.SetEscort}() : Set number of escorting planes for AWACS. -- * @{#AWACS.AddCAPAirWing}() : Add an additional @{Ops.Airwing#AIRWING} for CAP flights. -- * @{#AWACS.ZipLip}() : Do not show messages on screen, no extra calls for player guidance, use short callsigns, no group tags. -- -- ## 9.1 Single Options -- -- Further single options (set before starting your AWACS instance, but after `:New()`) -- -- testawacs.PlayerGuidance = true -- allow missile warning call-outs. -- testawacs.NoGroupTags = false -- use group tags like Alpha, Bravo .. etc in call outs. -- testawacs.callsignshort = true -- use short callsigns, e.g. "Moose 1", not "Moose 1-1". -- testawacs.DeclareRadius = 5 -- you need to be this close to the lead unit for declare/VID to work, in NM. -- testawacs.MenuStrict = true -- Players need to check-in to see the menu; check-in still require to use the menu. -- testawacs.maxassigndistance = 100 -- Don't assign targets further out than this, in NM. -- testawacs.debug = false -- set to true to produce more log output. -- testawacs.NoMissileCalls = true -- suppress missile callouts -- testawacs.PlayerCapAssigment = true -- no intercept task assignments for players -- testawacs.invisible = false -- set AWACS to be invisible to hostiles -- testawacs.immortal = false -- set AWACS to be immortal -- -- By default, the radio queue is checked every 10 secs. This is altered by the calculated length of the sentence to speak -- -- over the radio. Google and Windows speech speed is different. Use the below to fine-tune the setup in case of overlapping -- -- messages or too long pauses -- testawacs.GoogleTTSPadding = 1 -- seconds -- testawacs.WindowsTTSPadding = 2.5 -- seconds -- -- ## 9.2 Bespoke random voices for AI CAP (Google TTS only) -- -- Currently there are 10 voices defined which are randomly assigned to the AI CAP flights: -- -- Defaults are: -- -- testawacs.CapVoices = { -- [1] = "de-DE-Wavenet-A", -- [2] = "de-DE-Wavenet-B", -- [3] = "fr-FR-Wavenet-A", -- [4] = "fr-FR-Wavenet-B", -- [5] = "en-GB-Wavenet-A", -- [6] = "en-GB-Wavenet-B", -- [7] = "en-GB-Wavenet-D", -- [8] = "en-AU-Wavenet-B", -- [9] = "en-US-Wavenet-J", -- [10] = "en-US-Wavenet-H", -- } -- -- ## 10 Using F10 map markers to create new player station points -- -- You can use F10 map markers to create new station points for human CAP flights. The latest created station will take priority for (new) station assignments for humans. -- Enable this option with -- -- testawacs.AllowMarkers = true -- -- Set a marker on the map and add the following text to create a station: "AWACS Station London" - "AWACS Station" are the necessary keywords, "London" -- in this example will be the name of the new station point. The user marker can then be deleted, an info marker point at the same place will remain. -- You can delete a player station point the same way: "AWACS Delete London"; note this will only work if currently there are no assigned flights on this station. -- Lastly, you can move the station around with keyword "Move": "AWACS Move London". -- -- ## 11 Discussion -- -- If you have questions or suggestions, please visit the [MOOSE Discord](https://discord.gg/AeYAkHP) #ops-awacs channel. -- -- -- -- -- @field #AWACS AWACS = { ClassName = "AWACS", -- #string version = "beta 0.1.30", -- #string lid = "", -- #string coalition = coalition.side.BLUE, -- #number coalitiontxt = "blue", -- #string OpsZone = nil, StationZone = 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 = false, verbose = false, ManagedGrps = {}, ManagedGrpID = 0, -- #number ManagedTaskID = 0, -- #number AnchorStacks = {}, -- Utilities.FiFo#FIFO CAPIdleAI = {}, CAPIdleHuman = {}, TaskedCAPAI = {}, TaskedCAPHuman = {}, OpenTasks = {}, -- Utilities.FiFo#FIFO ManagedTasks = {}, -- Utilities.FiFo#FIFO PictureAO = {}, -- Utilities.FiFo#FIFO PictureEWR = {}, -- Utilities.FiFo#FIFO Contacts = {}, -- Utilities.FiFo#FIFO Countactcounter = 0, ContactsAO = {}, -- Utilities.FiFo#FIFO RadioQueue = {}, -- Utilities.FiFo#FIFO PrioRadioQueue = {}, -- Utilities.FiFo#FIFO AwacsTimeOnStation = 4, AwacsTimeStamp = 0, EscortsTimeOnStation = 4, EscortsTimeStamp = 0, CAPTimeOnStation = 4, AwacsROE = "", AwacsROT = "", MenuStrict = true, MaxAIonCAP = 3, AIonCAP = 0, AICAPMissions = {}, -- Utilities.FiFo#FIFO ShiftChangeAwacsFlag = false, ShiftChangeEscortsFlag = false, ShiftChangeAwacsRequested = false, ShiftChangeEscortsRequested = false, CAPAirwings = {}, -- Utilities.FiFo#FIFO MonitoringData = {}, MonitoringOn = false, FlightGroups = {}, AwacsMission = nil, AwacsInZone = false, -- not yet arrived or gone again AwacsReady = false, CatchAllMissions = {}, CatchAllFGs = {}, PictureInterval = 300, ReassignTime = 120, PictureTimeStamp = 0, BorderZone = nil, RejectZone = nil, maxassigndistance = 100, PlayerGuidance = true, ModernEra = true, callsignshort = true, TacDistance = 45, MeldDistance = 35, ThreatDistance = 25, AOName = "Rock", AOCoordinate = nil, clientmenus = nil, RadarBlur = 15, ReassignmentPause = 180, NoGroupTags = false, SuppressScreenOutput = false, NoMissileCalls = true, GoogleTTSPadding = 1, WindowsTTSPadding = 2.5, PlayerCapAssigment = true, AllowMarkers = false, PlayerStationName = nil, } --- --@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 IFF AWACS.IFF = { SPADES = "Spades", NEUTRAL = "Neutral", FRIENDLY = "Friendly", ENEMY = "Hostile", BOGEY = "Bogey", } --- -- @field Phonetic AWACS.Phonetic = { [1] = 'Alpha', [2] = 'Bravo', [3] = 'Charlie', [4] = 'Delta', [5] = 'Echo', [6] = 'Foxtrot', [7] = 'Golf', [8] = 'Hotel', [9] = 'India', [10] = 'Juliett', [11] = 'Kilo', [12] = 'Lima', [13] = 'Mike', [14] = 'November', [15] = 'Oscar', [16] = 'Papa', [17] = 'Quebec', [18] = 'Romeo', [19] = 'Sierra', [20] = 'Tango', [21] = 'Uniform', [22] = 'Victor', [23] = 'Whiskey', [24] = 'Xray', [25] = 'Yankee', [26] = 'Zulu', } --- -- @field Shipsize AWACS.Shipsize = { [1] = "Singleton", [2] = "Two-Ship", [3] = "Heavy", [4] = "Gorilla", } --- -- @field ROE AWACS.ROE = { POLICE = "Police", VID = "Visual ID", IFF = "IFF", BVR = "Beyond Visual Range", } --- -- @field AWACS.ROT AWACS.ROT = { BYPASSESCAPE = "Bypass and Escape", EVADE = "Evade Fire", PASSIVE = "Passive Defense", RETURNFIRE = "Return Fire", OPENFIRE = "Open Fire", } --- --@field THREATLEVEL -- can be 1-10, thresholds AWACS.THREATLEVEL = { GREEN = 3, AMBER = 7, RED = 10, } --- --@field CapVoices -- Random CAP voices AWACS.CapVoices = { [1] = "de-DE-Wavenet-A", [2] = "de-DE-Wavenet-B", [3] = "fr-FR-Wavenet-A", [4] = "fr-FR-Wavenet-B", [5] = "en-GB-Wavenet-A", [6] = "en-GB-Wavenet-B", [7] = "en-GB-Wavenet-D", [8] = "en-AU-Wavenet-B", [9] = "en-US-Wavenet-J", [10] = "en-US-Wavenet-H", } --- -- @type AWACS.MonitoringData -- @field #string AwacsStateMission -- @field #string AwacsStateFG -- @field #boolean AwacsShiftChange -- @field #string EscortsStateMission -- @field #string EscortsStateFG -- @field #boolean EscortsShiftChange -- @field #number AICAPMax -- @field #number AICAPCurrent -- @field #number Airwings -- @field #number Players -- @field #number PlayersCheckedin --- -- @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 tasking -- @field Core.Menu#MENU_GROUP_COMMAND showtask -- @field Core.Menu#MENU_GROUP_COMMAND judy -- @field Core.Menu#MENU_GROUP_COMMAND unable -- @field Core.Menu#MENU_GROUP_COMMAND abort -- @field Core.Menu#MENU_GROUP_COMMAND commit -- @field Core.Menu#MENU_GROUP vid -- @field Core.Menu#MENU_GROUP_COMMAND neutral -- @field Core.Menu#MENU_GROUP_COMMAND hostile -- @field Core.Menu#MENU_GROUP_COMMAND friendly --- Group Data -- @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 CurrentAuftrag -- Auftragsnummer for AI -- @field #number CurrentTask -- ManagedTask ID -- @field #boolean HasAssignedTask -- @field #number GID -- @field #number AnchorStackNo -- @field #number AnchorStackAngels -- @field #number ContactCID -- @field Core.Point#COORDINATE LastKnownPosition -- @field #number LastTasking TimeStamp --- Contact Data -- @type AWACS.ManagedContact -- @field #number CID -- @field Ops.Intelligence#INTEL.Contact Contact -- @field Ops.Intelligence#INTEL.Cluster Cluster -- @field #string IFF -- ID'ed or not (yet) -- @field Ops.Target#TARGET Target -- @field #number LinkedTask --> TID -- @field #number LinkedGroup --> GID -- @field #string Status - #AWACS.TaskStatus -- @field #string TargetGroupNaming -- Alpha, Charlie -- @field #string ReportingName -- NATO platform name -- @field #string EngagementTag -- @field #boolean TACCallDone -- @field #boolean MeldCallDone -- @field #boolean MergeCallDone --- -- @type AWACS.TaskDescription AWACS.TaskDescription = { ANCHOR = "Anchor", REANCHOR = "Re-Anchor", VID = "VID", IFF = "IFF", INTERCEPT = "Intercept", SWEEP = "Sweep", RTB = "RTB", } --- -- @type AWACS.TaskStatus AWACS.TaskStatus = { IDLE = "Idle", UNASSIGNED = "Unassigned", REQUESTED = "Requested", ASSIGNED = "Assigned", EXECUTING = "Executing", SUCCESS = "Success", FAILED = "Failed", DEAD = "Dead", } --- -- @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 -- @field #number CurrentAuftrag -- @field #number RequestedTimestamp --- -- @type AWACS.AnchorAssignedEntry -- @field #number ID -- @field #number Angels --- -- @type AWACS.AnchorData -- @field #number AnchorBaseAngels -- @field Core.Zone#ZONE_RADIUS StationZone -- @field Core.Point#COORDINATE StationZoneCoordinate -- @field #string StationZoneCoordinateText -- @field #string StationName -- @field Utilities.FiFo#FIFO AnchorAssignedID FiFo of #AWACS.AnchorAssignedEntry -- @field Utilities.FiFo#FIFO Anchors FiFo of available stacks -- @field Wrapper.Marker#MARKER AnchorMarker Tag for this station --- --@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 0.1.30 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- -- DONE - WIP - Player tasking, VID -- TODO - Localization (sensible?) -- TODO - (LOW) LotATC -- TODO - SW Optimization -- WONTDO - Maybe check in AI only when airborne -- DONE - remove SSML tag when not on google (currently sometimes spoken) -- DONE - Maybe - Assign specific number of AI CAP to a station -- DONE - Multiple AIRWING connection? Can't really get recruit to work, switched to random round robin -- DONE - System for Players to VID contacts? -- DONE - Task reassignment - if a player reject a task, don't choose him again for 3 minutes -- DONE - added SSML tags to make google readouts nicer -- DONE - 2nd audio queue for priority messages -- DONE - (WIP) Missile launch callout -- DONE - Event detection, Player joining, eject, crash, dead, leaving; AI shot -> DEFEND -- DONE - AI Tasking -- DONE - Shift Change, Change on asset RTB or dead or mission done (done for AWACS and Escorts) -- DONE - TripWire - WIP - Threat (35nm), Meld (45nm, on mission), Merged (<3nm) -- -- DONE - Escorts via AirWing not staying on -- DONE - Borders for INTEL. Optional, i.e. land based defense within borders -- 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 -- DONE - Declare -- DONE - Bogey Dope -- DONE - Radio Menu -- DONE - Intel Detection -- DONE - ROE -- DONE - Anchor Stack Management -- DONE - Shift Length AWACS/AI -- DONE - (WIP) Reporting ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- 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 Fighter Engagement operations zone (FEZ) this AWACS controls. Can be passed as #ZONE_POLYGON. -- The name of the zone will be used in reference calls as bulls eye name, so ensure a radio friendly name that does not collide with NATOPS keywords. -- @param #string StationZone Name of the round, mission editor created anchor zone where CAP groups will be stationed. Usually a short city name. -- @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 -- @usage -- You can set up the OpsZone/FEZ in a number of ways: -- * As a string denominating a normal, round zone you have created and named in the mission editor, e.g. "Rock". -- * As a polygon zone, defined e.g. like `ZONE_POLYGON:New("Rock",GROUP:FindByName("RockZone"))` where "RockZone" is the name of a late activated helo, and it\'s waypoints (not more than 10) describe a closed polygon zone in the mission editor. -- * As a string denominating a polygon zone from the mission editor (same late activated helo, but named "Rock#ZONE_POLYGON" in the mission editor. Here, Moose will auto-create a polygon zone when loading, and name it "Rock". Pass as `ZONE:FindByName("Rock")`. function AWACS:New(Name,AirWing,Coalition,AirbaseName,AwacsOrbit,OpsZone,StationZone,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 = 15 -- +/-15% detection precision i.e. 85-115 reported group size if type(OpsZone) == "string" then self.OpsZone = ZONE:New(OpsZone) -- Core.Zone#ZONE elseif type(OpsZone) == "table" and OpsZone.ClassName and string.find(OpsZone.ClassName,"ZONE") then self.OpsZone = OpsZone else self:E("AWACS - Invalid OpsZone passed!") return end --self.AOCoordinate = self.OpsZone:GetCoordinate() self.AOCoordinate = COORDINATE:NewFromVec3( coalition.getMainRefPoint( self.coalition ) ) -- bulls eye from ME self.AOName = self.OpsZone:GetName() self.UseBullsAO = true -- as per NATOPS self.ControlZoneRadius = 100 -- nm self.StationZone = ZONE:New(StationZone) -- Core.Zone#ZONE self.StationZoneName = StationZone 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.BorderZone = nil self.CallSign = CALLSIGN.AWACS.Darkstar -- #number self.CallSignNo = 1 -- #number self.NoHelos = true self.AIRequested = 0 self.AIonCAP = 0 self.AICAPMissions = FIFO:New() -- Utilities.FiFo#FIFO self.FlightGroups = FIFO:New() -- Utilities.FiFo#FIFO self.Countactcounter = 0 self.PictureInterval = 300 -- picture every 5s mins self.PictureTimeStamp = 0 -- timestamp self.ReassignTime = 120 -- time for player re-assignment self.intelstarted = false self.sunrisedone = false local speed = 250 self.SpeedBase = speed self.Speed = UTILS.KnotsToAltKIAS(speed,self.AwacsAngels*1000) self.Heading = 0 -- north self.Leg = 50 -- nm self.invisible = false self.immortal = false self.callsigntxt = "AWACS" self.AwacsTimeOnStation = 4 self.AwacsTimeStamp = 0 self.EscortsTimeOnStation = 4 self.EscortsTimeStamp = 0 self.ShiftChangeTime = 0.25 -- 15mins self.ShiftChangeAwacsFlag = false self.ShiftChangeEscortsFlag = false self.CapSpeedBase = 270 self.CAPTimeOnStation = 4 self.MaxAIonCAP = 4 self.AICAPCAllName = CALLSIGN.Aircraft.Colt self.AICAPCAllNumber = 0 self.CAPGender = "male" self.CAPCulture = "en-US" self.CAPVoice = nil self.AwacsMission = nil self.AwacsInZone = false -- not yet arrived or gone again self.AwacsReady = false self.AwacsROE = AWACS.ROE.IFF self.AwacsROT = AWACS.ROT.BYPASSESCAPE -- Escorts self.HasEscorts = false self.EscortTemplate = "" -- SRS self.PathToSRS = "C:\\Program Files\\DCS-SimpleRadio-Standalone" self.Gender = "female" self.Culture = "en-GB" self.Voice = nil self.Port = 5002 self.Volume = 1.0 self.RadioQueue = FIFO:New() -- Utilities.FiFo#FIFO self.PrioRadioQueue = FIFO:New() -- Utilities.FiFo#FIFO self.maxspeakentries = 3 self.GoogleTTSPadding = 1 self.WindowsTTSPadding = 2.5 -- Client SET self.clientset = SET_CLIENT:New():FilterActive(true):FilterCoalitions(self.coalitiontxt):FilterCategories("plane"):FilterStart() -- Player options self.PlayerGuidance = true self.ModernEra = true self.NoGroupTags = false self.SuppressScreenOutput = false self.ReassignmentPause = 180 self.callsignshort = true self.DeclareRadius = 5 -- NM self.MenuStrict = true self.maxassigndistance = 100 --nm self.NoMissileCalls = true self.PlayerCapAssigment = true -- managed groups self.ManagedGrps = {} -- #table of #AWACS.ManagedGroup entries self.ManagedGrpID = 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.ManagedTasks = FIFO:New() -- Utilities.FiFo#FIFO --self.OpenTasks = FIFO:New() -- Utilities.FiFo#FIFO -- Monitoring, init local MonitoringData = {} -- #AWACS.MonitoringData MonitoringData.AICAPCurrent = 0 MonitoringData.AICAPMax = self.MaxAIonCAP MonitoringData.Airwings = 1 MonitoringData.PlayersCheckedin = 0 MonitoringData.Players = 0 MonitoringData.AwacsShiftChange = false MonitoringData.AwacsStateFG = "unknown" MonitoringData.AwacsStateMission = "unknown" MonitoringData.EscortsShiftChange = false MonitoringData.EscortsStateFG= "unknown" MonitoringData.EscortsStateMission = "unknown" self.MonitoringOn = false -- #boolean self.MonitoringData = MonitoringData self.CatchAllMissions = {} self.CatchAllFGs = {} -- 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.ManagedContacts = FIFO:New() self.CID = 0 self.ContactsAO = FIFO:New() -- Utilities.FiFo#FIFO self.clientmenus = 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", "StartUp") -- Start FSM. self:AddTransition("StartUp", "Started", "Running") self:AddTransition("*", "Status", "*") -- Status update. self:AddTransition("*", "CheckedIn", "*") self:AddTransition("*", "CheckedOut", "*") self:AddTransition("*", "AssignAnchor", "*") self:AddTransition("*", "AssignedAnchor", "*") self:AddTransition("*", "ReAnchor", "*") 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("*", "Intercept", "*") self:AddTransition("*", "InterceptSuccess", "*") self:AddTransition("*", "InterceptFailure", "*") self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. local text = string.format("%sAWACS Version %s Initiated",self.lid,self.version) self:I(text) -- Events -- Player joins self:HandleEvent(EVENTS.PlayerEnterAircraft, self._EventHandler) self:HandleEvent(EVENTS.PlayerEnterUnit, self._EventHandler) -- Player leaves self:HandleEvent(EVENTS.PlayerLeaveUnit, self._EventHandler) self:HandleEvent(EVENTS.Ejection, self._EventHandler) self:HandleEvent(EVENTS.Crash, self._EventHandler) self:HandleEvent(EVENTS.Dead, self._EventHandler) self:HandleEvent(EVENTS.UnitLost, self._EventHandler) self:HandleEvent(EVENTS.BDA, self._EventHandler) self:HandleEvent(EVENTS.PilotDead, self._EventHandler) -- Missile warning self:HandleEvent(EVENTS.Shot, self._EventHandler) ------------------------ --- Pseudo Functions --- ------------------------ --- Triggers the FSM event "Start". Starts the AWACS. Initializes parameters and starts event handlers. -- @function [parent=#AWACS] Start -- @param #AWACS self --- Triggers the FSM event "Start" after a delay. Starts the AWACS. Initializes parameters and starts event handlers. -- @function [parent=#AWACS] __Start -- @param #AWACS self -- @param #number delay Delay in seconds. --- Triggers the FSM event "Stop". Stops the AWACS and all its event handlers. -- @param #AWACS self --- Triggers the FSM event "Stop" after a delay. Stops the AWACS and all its event handlers. -- @function [parent=#AWACS] __Stop -- @param #AWACS self -- @param #number delay Delay in seconds. --- On After "CheckedIn" event. AI or Player checked in. -- @function [parent=#AWACS] OnAfterCheckedIn -- @param #AWACS self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- On After "CheckedOut" event. AI or Player checked out. -- @function [parent=#AWACS] OnAfterCheckedOut -- @param #AWACS self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- On After "AssignedAnchor" event. AI or Player has been assigned a CAP station. -- @function [parent=#AWACS] OnAfterAssignedAnchor -- @param #AWACS self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- On After "ReAnchor" event. AI or Player has been send back to station. -- @function [parent=#AWACS] OnAfterReAnchor -- @param #AWACS self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- On After "NewCluster" event. AWACS detected a cluster. -- @function [parent=#AWACS] OnAfterNewCluster -- @param #AWACS self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- On After "NewContact" event. AWACS detected a contact. -- @function [parent=#AWACS] OnAfterNewContact -- @param #AWACS self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- On After "LostCluster" event. AWACS lost a radar cluster. -- @function [parent=#AWACS] OnAfterLostCluster -- @param #AWACS self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- On After "LostContact" event. AWACS lost a radar contact. -- @function [parent=#AWACS] OnAfterLostContact -- @param #AWACS self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- On After "EscortShiftChange" event. AWACS escorts shift change. -- @function [parent=#AWACS] OnAfterEscortShiftChange -- @param #AWACS self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- On After "AwacsShiftChange" event. AWACS shift change. -- @function [parent=#AWACS] OnAfterAwacsShiftChange -- @param #AWACS self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- On After "Intercept" event. CAP send on intercept. -- @function [parent=#AWACS] OnAfterIntercept -- @param #AWACS self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- On After "InterceptSuccess" event. Intercept successful. -- @function [parent=#AWACS] OnAfterIntercept -- @param #AWACS self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- On After "InterceptFailure" event. Intercept failure. -- @function [parent=#AWACS] OnAfterIntercept -- @param #AWACS self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. return self end -- TODO Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- [Internal] Create a AIC-TTS message entry -- @param #AWACS self -- @param #string TextTTS Text to speak -- @param #string TextScreen Text for screen -- @param #number GID Group ID #AWACS.ManagedGroup GID -- @param #boolean IsGroup Has a group -- @param #boolean ToScreen Show on screen -- @param #boolean IsNew New -- @param #boolean FromAI From AI -- @param #boolean IsPrio Priority entry -- @return #AWACS self function AWACS:_NewRadioEntry(TextTTS, TextScreen,GID,IsGroup,ToScreen,IsNew,FromAI,IsPrio) self:T(self.lid.."_NewRadioEntry") local RadioEntry = {} -- #AWACS.RadioEntry RadioEntry.IsNew = IsNew RadioEntry.TextTTS = TextTTS RadioEntry.TextScreen = TextScreen RadioEntry.GroupID = GID RadioEntry.ToScreen = ToScreen RadioEntry.Duration = STTS.getSpeechTime(TextTTS,0.95,false) or 8 RadioEntry.FromAI = FromAI RadioEntry.IsGroup = IsGroup if IsPrio then self.PrioRadioQueue:Push(RadioEntry) else self.RadioQueue:Push(RadioEntry) end return self end --- [User] Change the bulls eye alias for AWACS callout. Defaults to "Rock" -- @param #AWACS self -- @param #string Name -- @return #AWACS self function AWACS:SetBullsEyeAlias(Name) self:T(self.lid.."_SetBullsEyeAlias") self.AOName = Name or "Rock" return self end --- [User] Set TOS Time-on-Station in Hours -- @param #AWACS self -- @param #number AICHours AWACS stays this number of hours on station before shift change, default is 4. -- @param #number CapHours (optional) CAP stays this number of hours on station before shift change, default is 4. -- @return #AWACS self function AWACS:SetTOS(AICHours,CapHours) self:T(self.lid.."SetTOS") self.AwacsTimeOnStation = AICHours or 4 self.CAPTimeOnStation = CapHours or 4 return self end --- [User] Change number of seconds AWACS waits until a Player is re-assigned a different task. Defaults to 180. -- @param #AWACS self -- @param #number Seconds -- @return #AWACS self function AWACS:SetReassignmentPause(Seconds) self.ReassignmentPause = Seconds or 180 return self end --- [User] Do not show messages on screen -- @param #AWACS self -- @param #boolean Switch If true, no messages will be shown on screen. -- @return #AWACS self function AWACS:SuppressScreenMessages(Switch) self:T(self.lid.."_SetBullsEyeAlias") self.SuppressScreenOutput = Switch or false return self end --- [User] Do not show messages on screen, no extra calls for player guidance, use short callsigns etc. -- @param #AWACS self -- @return #AWACS self function AWACS:ZipLip() self:T(self.lid.."ZipLip") self:SuppressScreenMessages(true) self.PlayerGuidance = false self.callsignshort = true --self.NoGroupTags = true self.NoMissileCalls = true return self end --- [Internal] Event handler -- @param #AWACS self -- @param Wrapper.Group#GROUP Group Group, can also be passed as #string group name -- @return #boolean found -- @return #number GID -- @return #string CallSign function AWACS:_GetGIDFromGroupOrName(Group) self:T(self.lid.."_GetGIDFromGroupOrName") local GID = 0 local Outcome = false local CallSign = "Ghost 1" local nametocheck = CallSign if Group and type(Group) == "string" then nametocheck = Group elseif Group and Group:IsInstanceOf("GROUP") then nametocheck = Group:GetName() else return false, 0, CallSign end 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 CallSign = managed.CallSign end end return Outcome, GID, CallSign end --- [Internal] Event handler -- @param #AWACS self -- @param Core.Event#EVENTDATA EventData -- @return #AWACS self function AWACS:_EventHandler(EventData) self:T(self.lid.."_EventHandler") self:T({Event = EventData.id}) local Event = EventData -- Core.Event#EVENTDATA if Event.id == EVENTS.PlayerEnterAircraft or Event.id == EVENTS.PlayerEnterUnit then --player entered unit --self:T("Player enter unit: " .. Event.IniPlayerName) --self:T("Coalition = " .. UTILS.GetCoalitionName(Event.IniCoalition)) if Event.IniCoalition == self.coalition then self:_SetClientMenus() end end if Event.id == EVENTS.PlayerLeaveUnit then --player left unit -- check known player? --self:T("Player group left unit: " .. Event.IniGroupName) --self:T("Player name left: " .. Event.IniPlayerName) --self:T("Coalition = " .. UTILS.GetCoalitionName(Event.IniCoalition)) if Event.IniCoalition == self.coalition then local Outcome, GID, CallSign = self:_GetGIDFromGroupOrName(Event.IniGroupName) if Outcome and GID > 0 then self:_CheckOut(nil,GID,true) end end end if Event.id == EVENTS.Ejection or Event.id == EVENTS.Crash or Event.id == EVENTS.Dead or Event.id == EVENTS.PilotDead then --unit or player dead -- check known group? if Event.IniCoalition == self.coalition then --self:T("Ejection/Crash/Dead/PilotDead Group: " .. Event.IniGroupName) --self:T("Coalition = " .. UTILS.GetCoalitionName(Event.IniCoalition)) local Outcome, GID, CallSign = self:_GetGIDFromGroupOrName(Event.IniGroupName) if Outcome and GID > 0 then self:_CheckOut(nil,GID,true) end end end if Event.id == EVENTS.Shot and self.PlayerGuidance and not self.NoMissileCalls then if Event.IniCoalition ~= self.coalition then self:T("Shot from: " .. Event.IniGroupName) --self:T(UTILS.OneLineSerialize(Event)) local position = Event.IniGroup:GetCoordinate() if not position then return self end --self:T("Coalition = " .. UTILS.GetCoalitionName(Event.IniCoalition)) -- Check missile type local Category = Event.WeaponCategory local WeaponDesc = EventData.Weapon:getDesc() -- https://wiki.hoggitworld.com/view/DCS_enum_weapon self:T({WeaponDesc}) --self:T("Weapon = " .. tostring(WeaponDesc.displayName)) if WeaponDesc.category == 1 and (WeaponDesc.missileCategory == 1 or WeaponDesc.missileCategory == 2) then self:T("AAM or SAM Missile fired") -- Missile fired -- WIP Missile Callouts local warndist = 25 local Type = "SAM" if WeaponDesc.category == 1 then Type = "Missile" -- AAM local guidance = WeaponDesc.guidance -- IR=2, Radar Active=3, Radar Semi Active=4, Radar Passive = 5 if guidance == 2 then warndist = 10 elseif guidance == 3 then warndist = 25 elseif guidance == 4 then warndist = 15 elseif guidance == 5 then warndist = 10 end -- guidance end -- cat 1 self:_MissileWarning(position,Type,warndist) end -- cat 1 or 2 end -- end coalition end -- end shot return self end --- [Internal] Missile Warning Callout -- @param #AWACS self -- @param Core.Point#COORDINATE Coordinate Where the shot happened -- @param #string Type Type to call out, e.i. "SAM" or "Missile" -- @param #number Warndist Distance in NM to find friendly planes -- @return #AWACS self function AWACS:_MissileWarning(Coordinate,Type,Warndist) self:T(self.lid.."_MissileWarning Type="..Type.." WarnDist="..Warndist) --self:T(UTILS.OneLineSerialize(Coordinate)) if not Coordinate then return self end local shotzone = ZONE_RADIUS:New("WarningZone",Coordinate:GetVec2(),UTILS.NMToMeters(Warndist)) local targetgrpset = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterCategoryAirplane():FilterActive():FilterZones({shotzone}):FilterOnce() if targetgrpset:Count() > 0 then local targets = targetgrpset:GetSetObjects() for _,_grp in pairs (targets) do -- TODO -- player callouts only if _grp and _grp:IsAlive() then local isPlayer = _grp:IsPlayer() --if self.debug or isPlayer then if isPlayer then local callsign = self:_GetCallSign(_grp) local text = string.format("%s, %s! %s! %s! Defend!",callsign,Type,Type,Type) self:_NewRadioEntry(text, text,0,false,self.debug,true,false,true) end end end end return self end --- [User] Set AWACS Radar Blur - the radar contact count per group/cluster will be distored up or down by this number percent. Defaults to 15 in Modern Era and 25 in Cold War. -- @param #AWACS self -- @param #number Percent -- @return #AWACS self function AWACS:SetRadarBlur(Percent) local percent = Percent or 15 if percent < 0 then percent = 0 end if percent > 100 then percent = 100 end self.RadarBlur = Percent return self end --- [User] Set AWACS to Cold War standards - ROE to VID, ROT to Passive (bypass and escape). Radar blur 25%. -- Sets TAC/Meld/Threat call distances to 35, 25 and 15 nm. -- @param #AWACS self -- @return #AWACS self function AWACS:SetColdWar() self.ModernEra = false self.AwacsROT = AWACS.ROT.PASSIVE self.AwacsROE = AWACS.ROE.VID self.RadarBlur = 25 self:SetInterceptTimeline(35, 25, 15) return self end --- [User] Set AWACS to Modern Era standards - ROE to BVR, ROT to defensive (evade fire). Radar blur 15%. -- @param #AWACS self -- @return #AWACS self function AWACS:SetModernEra() self.ModernEra = true self.AwacsROT = AWACS.ROT.EVADE self.AwacsROE = AWACS.ROE.BVR self.RadarBlur = 15 return self end --- [User] Set AWACS to Modern Era standards - ROE to IFF, ROT to defensive (evade fire). Radar blur 15%. -- @param #AWACS self -- @return #AWACS self function AWACS:SetModernEraDefensive() self.ModernEra = true self.AwacsROT = AWACS.ROT.EVADE self.AwacsROE = AWACS.ROE.IFF self.RadarBlur = 15 return self end --- [User] Set AWACS to Modern Era standards - ROE to BVR, ROT to return fire. Radar blur 15%. -- @param #AWACS self -- @return #AWACS self function AWACS:SetModernEraAgressive() self.ModernEra = true self.AwacsROT = AWACS.ROT.RETURNFIRE self.AwacsROE = AWACS.ROE.BVR self.RadarBlur = 15 return self end --- [User] Set AWACS to Policing standards - ROE to VID, ROT to Lock (bypass and escape). Radar blur 15%. -- @param #AWACS self -- @return #AWACS self function AWACS:SetPolicingModern() self.ModernEra = true self.AwacsROT = AWACS.ROT.BYPASSESCAPE self.AwacsROE = AWACS.ROE.VID self.RadarBlur = 15 return self end --- [User] Set AWACS to Policing standards - ROE to VID, ROT to Lock (bypass and escape). Radar blur 25%. -- Sets TAC/Meld/Threat call distances to 35, 25 and 15 nm. -- @param #AWACS self -- @return #AWACS self function AWACS:SetPolicingColdWar() self.ModernEra = false self.AwacsROT = AWACS.ROT.BYPASSESCAPE self.AwacsROE = AWACS.ROE.VID self.RadarBlur = 25 self:SetInterceptTimeline(35, 25, 15) return self end --- [User] Set AWACS Player Guidance - influences missile callout and the "New" label in group callouts. -- @param #AWACS self -- @param #boolean Switch If true (default) it is on, if false, it is off. -- @return #AWACS self function AWACS:SetPlayerGuidance(Switch) if (Switch == nil) or (Switch == true) then self.PlayerGuidance = true else self.PlayerGuidance = false end return self end --- [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 intercept timeline support distance. -- @param #AWACS self -- @param #number TacDistance Distance for TAC call, default 45nm -- @param #number MeldDistance Distance for Meld call, default 35nm -- @param #number ThreatDistance Distance for Threat call, default 25nm -- @return #AWACS self function AWACS:SetInterceptTimeline(TacDistance, MeldDistance, ThreatDistance) self.TacDistance = TacDistance or 45 self.MeldDistance = MeldDistance or 35 self.ThreatDistance = ThreatDistance or 25 return self end --- [User] Set additional defensive zone, e.g. the zone behind the FEZ to also be defended -- @param #AWACS self -- @param Core.Zone#ZONE Zone -- @param #boolean Draw Draw lines around this zone if true -- @return #AWACS self function AWACS:SetAdditionalZone(Zone, Draw) self:T(self.lid.."SetAdditionalZone") self.BorderZone = Zone if self.debug then Zone:DrawZone(-1,{1,0.64,0},1,{1,0.64,0},0.2,1,true) MARKER:New(Zone:GetCoordinate(),"Defensive Zone"):ToAll() elseif Draw then Zone:DrawZone(-1,{1,0.64,0},1,{1,0.64,0},0.2,1,true) end return self end --- [User] Set rejection zone, e.g. a border of a foreign country. Detected bogeys in here won't be engaged. -- @param #AWACS self -- @param Core.Zone#ZONE Zone -- @param #boolean Draw Draw lines around this zone if true -- @return #AWACS self function AWACS:SetRejectionZone(Zone,Draw) self:T(self.lid.."SetRejectionZone") self.RejectZone = Zone if Draw then Zone:DrawZone(-1,{1,0.64,0},1,{1,0.64,0},0.2,1,true) --MARKER:New(Zone:GetCoordinate(),"Rejection Zone"):ToAll() elseif self.debug then Zone:DrawZone(-1,{1,0.64,0},1,{1,0.64,0},0.2,1,true) MARKER:New(Zone:GetCoordinate(),"Rejection Zone"):ToAll() end return self end --- [User] Draw a line around the FEZ on the F10 map. -- @param #AWACS self -- @return #AWACS self function AWACS:DrawFEZ() self.OpsZone:DrawZone(-1,{1,0,0},1,{1,0,0},0.2,5,true) return self 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 #AWACS self -- @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 @{Sound.SRS} 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 @{Sound.SRS.SetVoice} function, e.g, `:SetVoice("Microsoft Hedda Desktop")`. -- Note that this must be installed on your windows system. Can also be Google voice types, if you are using Google TTS. -- @param #number Volume Volume - between 0.0 (silent) and 1.0 (loudest) -- @param #string PathToGoogleKey Path to your google key if you want to use google TTS -- @return #AWACS self function AWACS:SetSRS(PathToSRS,Gender,Culture,Port,Voice,Volume,PathToGoogleKey) 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 self.PathToGoogleKey = PathToGoogleKey self.Volume = Volume or 1.0 return self end --- [User] Set AWACS Voice Details for AI CAP Planes - SRS TTS - see @{Sound.SRS} for details -- @param #AWACS self -- @param #string Gender Defaults to "male" -- @param #string Culture Defaults to "en-US" -- @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. Can also be Google voice types, if you are using Google TTS. -- @return #AWACS self function AWACS:SetSRSVoiceCAP(Gender, Culture, Voice) self:T(self.lid.."SetSRSVoiceCAP") self.CAPGender = Gender or "male" self.CAPCulture = Culture or "en-US" self.CAPVoice = Voice or "en-GB-Standard-B" return self end --- [User] Set AI CAP Plane Details -- @param #AWACS self -- @param #number Callsign Callsign name of AI CAP, e.g. CALLSIGN.Aircraft.Dodge. Defaults to CALLSIGN.Aircraft.Colt. Note that not all available callsigns work for all plane types. -- @param #number MaxAICap Maximum number of AI CAP planes on station that AWACS will set up automatically. Default to 4. -- @param #number TOS Time on station, in hours. AI planes might go back to base earlier if they run out of fuel or missiles. -- @param #number Speed Airspeed to be used in knots. Will be adjusted to flight height automatically. Defaults to 270. -- @return #AWACS self function AWACS:SetAICAPDetails(Callsign,MaxAICap,TOS,Speed) self:T(self.lid.."SetAICAPDetails") self.CapSpeedBase = Speed or 270 self.CAPTimeOnStation = TOS or 4 self.MaxAIonCAP = MaxAICap or 4 self.AICAPCAllName = Callsign or CALLSIGN.Aircraft.Colt 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 else self.HasEscorts = false self.EscortNumber = 0 end return self end --- [Internal] Message a vector BR to a position -- @param #AWACS self -- @param #number GID Group GID -- @param #string Tag (optional) Text to add after Vector, e.g. " to Anchor" - NOTE the leading space -- @param Core.Point#COORDINATE Coordinate The Coordinate to use -- @param #number Angels (Optional) Add Angels -- @return #AWACS self function AWACS:_MessageVector(GID,Tag,Coordinate,Angels) self:T(self.lid.."_MessageVector") local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup local Tag = Tag or "" if managedgroup and Coordinate then local tocallsign = managedgroup.CallSign or "Ghost 1" local group = managedgroup.Group local groupposition = group:GetCoordinate() --local BRtext = Coordinate:ToStringBR(groupposition) local BRtext,BRtextTTS = self:_ToStringBR(groupposition,Coordinate) local text = string.format("%s, %s. Vector%s %s",tocallsign, self.callsigntxt,Tag,BRtextTTS) local textScreen = string.format("%s, %s, Vector%s %s",tocallsign, self.callsigntxt,Tag,BRtext) if Angels then text = text .. ". Angels "..tostring(Angels).."." textScreen = textScreen .. ". Angels "..tostring(Angels).."." end self:_NewRadioEntry(text,textScreen,0,false,self.debug,true,false) 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},45,{"Air"}) self.CatchAllMissions[#self.CatchAllMissions+1] = mission 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") local Mission = Mission -- Ops.Auftrag#AUFTRAG local AwacsFG = FlightGroup -- Ops.FlightGroup#FLIGHTGROUP -- Is this our Awacs mission? if self.AwacsMission:GetName() == Mission:GetName() then self:T("Setting up Awacs") 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) group:CommandEPLRS(self.ModernEra,5) -- Non AWACS does not seem take AWACS CS in DCS Group self.AwacsFG = AwacsFG self.AwacsFG:SetSRS(self.PathToSRS,self.Gender,self.Culture,self.Voice,self.Port,self.PathToGoogleKey,"AWACS",self.Volume) self.callsigntxt = string.format("%s",AWACS.CallSignClear[self.CallSign]) self:__CheckRadioQueue(10) if self.HasEscorts then --mission:SetRequiredEscorts(self.EscortNumber) self:_StartEscorts() end self.AwacsTimeStamp = timer.getTime() self.EscortsTimeStamp = timer.getTime() self.PictureTimeStamp = timer.getTime() + 10*60 self.AwacsReady = true -- set FSM to started self:Started() elseif self.ShiftChangeAwacsRequested and self.AwacsMissionReplacement and self.AwacsMissionReplacement:GetName() == Mission:GetName() then self:I("Setting up Awacs Replacement") -- 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,"AWACS") --self.callsigntxt = string.format("%s %d %d",AWACS.CallSignClear[self.CallSign],1,self.CallSignNo) self.callsigntxt = string.format("%s",AWACS.CallSignClear[self.CallSign]) local text = string.format("%s shift change for %s control.",self.callsigntxt,self.AOName or "Rock") 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() self.AwacsReady = true end return self end --- [Internal] Return Bullseye BR for Alpha Check etc, returns e.g. "Rock 021, 16" ("Rock" being the set BE name) -- @param #AWACS self -- @param Core.Point#COORDINATE Coordinate -- @param #boolean ssml Add SSML tag -- @param #boolean TTS For non-Alpha checks, hand back in format "Rock 0 2 1, 16" -- @return #string BullseyeBR function AWACS:_ToStringBULLS( Coordinate, ssml, TTS ) -- local BullsCoordinate = COORDINATE:NewFromVec3( coalition.getMainRefPoint( self.coalition ) ) local bullseyename = self.AOName or "Rock" --local BullsCoordinate = self.OpsZone:GetCoordinate() local BullsCoordinate = self.AOCoordinate local DirectionVec3 = BullsCoordinate:GetDirectionVec3( Coordinate ) local AngleRadians = Coordinate:GetAngleRadians( DirectionVec3 ) local Distance = Coordinate:Get2DDistance( BullsCoordinate ) local AngleDegrees = UTILS.Round( UTILS.ToDegree( AngleRadians ), 0 ) local Bearing = string.format( '%03d', AngleDegrees ) local Distance = UTILS.Round( UTILS.MetersToNM( Distance ), 0 ) if ssml then return string.format("%s %03d, %d",bullseyename,Bearing,Distance) elseif TTS then Bearing = self:_ToStringBullsTTS(Bearing) local BearingTTS = string.gsub(Bearing,"0","zero") return string.format("%s %s, %d",bullseyename,BearingTTS,Distance) else return string.format("%s %s, %d",bullseyename,Bearing,Distance) end end --- [Internal] Change Bullseye string to be TTS friendly, "Bullseye 021, 16" returns e.g. "Bulls eye 0 2 1. 1 6" -- @param #AWACS self -- @param #string Text Input text -- @return #string BullseyeBRTTS function AWACS:_ToStringBullsTTS(Text) local text = Text text=string.gsub(text,"Bullseye","Bulls eye") text=string.gsub(text,"%d","%1 ") text=string.gsub(text," ," ,".") text=string.gsub(text," $","") return text 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 -- @return #string CallSign function AWACS:_GetManagedGrpID(Group) if not Group or not Group:IsAlive() then self:T(self.lid.."_GetManagedGrpID - Requested Group is not alive!") return 0,false,"" end self:T(self.lid.."_GetManagedGrpID for "..Group:GetName()) local GID = 0 local Outcome = false local CallSign = "Ghost 1" 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 CallSign = managed.CallSign end end return GID, Outcome, CallSign end --- [Internal] AWACS Get TTS compatible callsign -- @param #AWACS self -- @param Wrapper.Group#GROUP Group Group to use -- @param #number GID GID to use -- @return #string Callsign function AWACS:_GetCallSign(Group,GID) self:T(self.lid.."_GetCallSign - GID "..tostring(GID)) if GID and type(GID) == "number" and GID > 0 then local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup self:T("Saved Callsign for TTS = " .. tostring(managedgroup.CallSign)) return managedgroup.CallSign end local callsign = "Ghost 1" if Group and Group:IsAlive() then local shortcallsign = Group:GetCallsign() or "unknown11"-- e.g.Uzi11, but we want Uzi 1 1 local groupname = Group:GetName() local callnumber = string.match(shortcallsign, "(%d+)$" ) or "unknown11" local callnumbermajor = string.char(string.byte(callnumber,1)) local callnumberminor = string.char(string.byte(callnumber,2)) if string.find(groupname,"#") then -- personalized flight name in group naming shortcallsign = string.match(groupname,"#([%a]+)") end if self.callsignshort then callsign = string.gsub(shortcallsign,callnumber,"").." "..callnumbermajor else callsign = string.gsub(shortcallsign,callnumber,"").." "..callnumbermajor.." "..callnumberminor end self:T("Generated Callsign for TTS = " .. callsign) end return callsign end --- [Internal] Update contact from cluster data -- @param #AWACS self -- @param #number CID Contact ID -- @return #AWACS self function AWACS:_UpdateContactFromCluster(CID) self:T(self.lid.."_UpdateContactFromCluster CID="..CID) local existingcontact = self.Contacts:PullByID(CID) -- #AWACS.ManagedContact local ContactTable = existingcontact.Cluster.Contacts or {} local function GetFirstAliveContact(table) for _,_contact in pairs (table) do local contact = _contact -- Ops.Intelligence#INTEL.Contact if contact and contact.group and contact.group:IsAlive() then return contact end end return nil end local NewContact = GetFirstAliveContact(ContactTable) if NewContact then existingcontact.Contact = NewContact self.Contacts:Push(existingcontact,existingcontact.CID) end return self end --- [Internal] Check merges for Players -- @param #AWACS self -- @return #AWACS self function AWACS:_CheckMerges() self:T(self.lid.."_CheckMerges") for _id,_pilot in pairs (self.ManagedGrps) do local pilot = _pilot -- #AWACS.ManagedGroup if pilot.Group and pilot.Group:IsAlive() then local ppos = pilot.Group:GetCoordinate() local pcallsign = pilot.CallSign self:T(self.lid.."Checking for "..pcallsign) if ppos then self.Contacts:ForEach( function (Contact) local contact = Contact -- #AWACS.ManagedContact local cpos = contact.Cluster.coordinate or contact.Contact.position or contact.Contact.group:GetCoordinate() local dist = ppos:Get2DDistance(cpos) local distnm = UTILS.Round(UTILS.MetersToNM(dist),0) if (pilot.IsPlayer or self.debug) and distnm <= 5 and not contact.MergeCallDone then local label = contact.EngagementTag or "" if not contact.MergeCallDone or not string.find(label,pcallsign) then self:T(self.lid.."Merged") self:_MergedCall(_id) contact.MergeCallDone = true end end end ) end end end return self end --- [Internal] Clean up contacts list -- @param #AWACS self -- @return #AWACS self function AWACS:_CleanUpContacts() self:T(self.lid.."_CleanUpContacts") if self.Contacts:Count() > 0 then local deadcontacts = FIFO:New() self.Contacts:ForEach( function (Contact) local contact = Contact -- #AWACS.ManagedContact if not contact.Contact.group:IsAlive() or contact.Target:IsDead() then deadcontacts:Push(contact,contact.CID) self:T("DEAD contact CID="..contact.CID) end end ) --local aliveclusters = FIFO:New() -- announce VANISHED if deadcontacts:Count() > 0 and (not self.NoGroupTags) then self:T("DEAD count="..deadcontacts:Count()) deadcontacts:ForEach( function (Contact) local contact = Contact -- #AWACS.ManagedContact local text = string.format("%s, %s Group. Vanished.",self.callsigntxt, contact.TargetGroupNaming) local textScreen = string.format("%s, %s group vanished.", self.callsigntxt, contact.TargetGroupNaming) self:_NewRadioEntry(text,textScreen,0,false,self.debug,true,false,true) self.Contacts:PullByID(contact.CID) -- end end ) end if self.Contacts:Count() > 0 then self.Contacts:ForEach( function (Contact) local contact = Contact -- #AWACS.ManagedContact self:_UpdateContactFromCluster(contact.CID) end ) end -- cleanup deadcontacts:Clear() -- aliveclusters:Clear() end return self end --- [Internal] Select pilots available for tasking, return AI and Human -- @param #AWACS self -- @return #table AIPilots Table of #AWACS.ManagedGroup -- @return #table HumanPilots Table of #AWACS.ManagedGroup function AWACS:_GetIdlePilots() self:T(self.lid.."_GetIdlePilots") local AIPilots = {} local HumanPilots = {} for _name,_entry in pairs (self.ManagedGrps) do local entry = _entry -- #AWACS.ManagedGroup self:T("Looking at entry "..entry.GID.." Name "..entry.GroupName) local managedtask = self:_ReadAssignedTaskFromGID(entry.GID) -- #AWACS.ManagedTask local overridetask = false if managedtask then self:T("Current task = "..(managedtask.ToDo or "Unknown")) if managedtask.ToDo == AWACS.TaskDescription.ANCHOR then overridetask = true end end if entry.IsAI then if entry.FlightGroup:IsAirborne() and ((not entry.HasAssignedTask) or overridetask) then -- must be idle, or? self:T("Adding AI with Callsign: "..entry.CallSign) AIPilots[#AIPilots+1] = _entry end elseif entry.IsPlayer and not entry.Blocked then if (not entry.HasAssignedTask) or overridetask then -- must be idle, or? -- check last assignment local TNow = timer.getTime() if entry.LastTasking and (TNow-entry.LastTasking > self.ReassignTime) then self:T("Adding Human with Callsign: "..entry.CallSign) HumanPilots[#HumanPilots+1] = _entry end end end end return AIPilots, HumanPilots end --- [Internal] Select max 3 targets for picture, bogey dope etc -- @param #AWACS self -- @param #boolean Untargeted Return not yet targeted contacts only -- @return #boolean HaveTargets True if targets could be found, else false -- @return Utilities.FiFo#FIFO Targetselection function AWACS:_TargetSelectionProcess(Untargeted) self:T(self.lid.."_TargetSelectionProcess") local maxtargets = 3 -- handleable number of callouts local contactstable = self.Contacts:GetDataTable() local targettable = FIFO:New() local sortedtargets = FIFO:New() local prefiltered = FIFO:New() local HaveTargets = false self:T(self.lid.."Initial count: "..self.Contacts:Count()) -- Bucket sort if Untargeted then -- pre-filter self.Contacts:ForEach( function (Contact) local contact = Contact -- #AWACS.ManagedContact if contact.Contact.group:IsAlive() and (contact.Status == AWACS.TaskStatus.IDLE or contact.Status == AWACS.TaskStatus.UNASSIGNED) then if self.AwacsROE == AWACS.ROE.POLICE or self.AwacsROE == AWACS.ROE.VID then -- filter out VID'd non-hostiles if not (contact.IFF == AWACS.IFF.FRIENDLY or contact.IFF == AWACS.IFF.NEUTRAL) then prefiltered:Push(contact,contact.CID) end else prefiltered:Push(contact,contact.CID) end end end ) contactstable = prefiltered:GetDataTable() self:T(self.lid.."Untargeted: "..prefiltered:Count()) end -- Loop through for _,_contact in pairs(contactstable) do local contact = _contact -- #AWACS.ManagedContact local checked = false local contactname = contact.TargetGroupNaming or "ZETA" local typename = contact.ReportingName or "Unknown" self:T(self.lid..string.format("Looking at group %s type %s",contactname,typename)) local contactcoord = contact.Cluster.coordinate or contact.Contact.position or contact.Contact.group:GetCoordinate() local contactvec2 = contactcoord:GetVec2() -- self:T({contactcoord:ToStringMGRS()}) -- self:T({contactvec2}) -- Bucket 0 - NOT in Rejection Zone :) if self.RejectZone then local isinrejzone = self.RejectZone:IsVec2InZone(contactvec2) --local distance = self.OpsZone:Get2DDistance(contactcoord) if isinrejzone then self:T(self.lid.."Across Border = YES - ignore") --targettable:Push(contact,distance) checked = true end end -- Bucket 1 - close to AIC (HVT) ca ~45nm local HVTCoordinate = self.OrbitZone:GetCoordinate() local distance = UTILS.NMToMeters(200) if contactcoord then distance = HVTCoordinate:Get2DDistance(contactcoord) end self:T(self.lid.."HVT Distance = "..UTILS.Round(UTILS.MetersToNM(distance),0)) if UTILS.MetersToNM(distance) <= 45 and not checked then self:T(self.lid.."In HVT Distance = YES") targettable:Push(contact,distance) checked = true end -- Bucket 2 - in AO/FEZ local isinopszone = self.OpsZone:IsVec2InZone(contactvec2) local distance = self.OpsZone:Get2DDistance(contactcoord) if isinopszone and not checked then self:T(self.lid.."In FEZ = YES") targettable:Push(contact,distance) checked = true end -- Bucket 3 - in Radar(Control)Zone, < 100nm to AO, Aspect HOT on AO local isinopszone = self.ControlZone:IsVec2InZone(contactvec2) if isinopszone and not checked then self:T(self.lid.."In Radar Zone = YES") -- Close to Bulls Eye? local distance = self.AOCoordinate:Get2DDistance(contactcoord) -- m local AOdist = UTILS.Round(UTILS.MetersToNM(distance),0) -- NM if not contactcoord.Heading then contactcoord.Heading = self.intel:CalcClusterDirection(contact.Cluster) end -- end heading local aspect = contactcoord:ToStringAspect(self.ControlZone:GetCoordinate()) local sizing = contact.Cluster.size or self.intel:ClusterCountUnits(contact.Cluster) or 1 -- prefer heavy groups sizing = math.fmod((sizing * 0.1),1) local AOdist2 = (AOdist / 2) * sizing AOdist2 = UTILS.Round((AOdist/2)+((AOdist/2)-AOdist2), 0) self:T(self.lid.."Aspect = "..aspect.." | Size = "..sizing ) if (AOdist2 < 75) or (aspect == "Hot") then local text = string.format("In AO(Adj) dist = %d(%d) NM",AOdist,AOdist2) self:T(self.lid..text) --if sizing > 2 then distance = math.floor(distance / sizing)+1 end targettable:Push(contact,distance) checked = true end end -- Bucket 4 (if set) within the border polyzone to be defended if self.BorderZone then local isinborderzone = self.BorderZone:IsVec2InZone(contactvec2) if isinborderzone and not checked then self:T(self.lid.."In BorderZone = YES") targettable:Push(contact,distance) checked = true end end end self:T(self.lid.."Post filter count: "..targettable:Count()) if targettable:Count() > maxtargets then local targets = targettable:GetSortedDataTable() targettable:Clear() for i=1,maxtargets do targettable:Push(targets[i]) end end sortedtargets:Clear() prefiltered:Clear() if targettable:Count() > 0 then HaveTargets = true end return HaveTargets, targettable 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 -- @param #number MaxEntries Max entries to show -- @param #boolean IsGeneral Is a general picture, address all stations -- @return #AWACS self function AWACS:_CreatePicture(AO,Callsign,GID,MaxEntries,IsGeneral) self:T(self.lid.."_CreatePicture AO="..tostring(AO).." for "..Callsign.." GID "..GID) local managedgroup = nil local group = nil local groupcoord = nil if not IsGeneral then managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup group = managedgroup.Group -- Wrapper.Group#GROUP groupcoord = group:GetCoordinate() end local fifo = self.PictureAO -- Utilities.FiFo#FIFO local maxentries = self.maxspeakentries or 3 if MaxEntries and MaxEntries>0 and MaxEntries <= 3 then maxentries = MaxEntries end local counter = 0 if not AO then -- fifo = self.PictureEWR end local entries = fifo:GetSize() if entries < maxentries then maxentries = entries end local text = "" local textScreen = "" -- " group, BRA for at angels , , " while counter < maxentries do counter = counter + 1 local contact = fifo:Pull() -- #AWACS.ManagedContact self:T({contact}) if contact and contact.Contact.group and contact.Contact.group:IsAlive() then --local coordinate = contact.Contact.group:GetCoordinate() local coordinate = contact.Cluster.coordinate or contact.Contact.position or contact.Contact.group:GetCoordinate() -- Core.Point#COORDINATE if not coordinate then self:E(self.lid.."NO Coordinate for this cluster! CID="..contact.CID) self:E({contact}) break end if not coordinate.Heading then coordinate.Heading = contact.Contact.heading or contact.Contact.group:GetHeading() end local refBRAA = "" local refBRAATTS = "" if self.NoGroupTags then text = "Group." -- Alpha Group. textScreen = "Group," else text = contact.TargetGroupNaming.." group." -- Alpha Group. textScreen = contact.TargetGroupNaming.." group," end if IsGeneral or not self.PlayerGuidance then refBRAA=self:_ToStringBULLS(coordinate) refBRAATTS = self:_ToStringBULLS(coordinate, false, true) local alt = contact.Contact.group:GetAltitude() or 8000 alt = UTILS.Round(UTILS.MetersToFeet(alt)/1000,0) -- Alpha Group. Bulls eye 0 2 1, 16 miles, 25 thousand. text = text .. " "..refBRAATTS.." miles, "..alt.." thousand." -- Alpha Group. Bulls eye 0 2 1, 16 miles, 25 thousand. textScreen = textScreen .. " "..refBRAA.." miles, "..alt.." thousand." -- Alpha Group, Bullseye 021, 16 miles, 25 thousand, else -- pilot reference refBRAA = coordinate:ToStringBRAANATO(groupcoord,true,true) refBRAATTS = string.gsub(refBRAA,"BRAA","brah") refBRAATTS = string.gsub(refBRAATTS,"BRA","brah") -- Charlie group, BRAA 045, 105 miles, Angels 41, Flanking, Track North-East, Bogey, Spades. if self.PathToGoogleKey then refBRAATTS = coordinate:ToStringBRAANATO(groupcoord,true,true,true,false,true) end if contact.IFF ~= AWACS.IFF.BOGEY then refBRAA = string.gsub(refBRAA,"Bogey", contact.IFF) refBRAATTS = string.gsub(refBRAATTS,"Bogey", contact.IFF) end text = text .. " "..refBRAATTS textScreen = textScreen .." "..refBRAA end -- Aspect local aspect = "" -- sizing local size = contact.Contact.group:CountAliveUnits() local threatsize, threatsizetext = self:_GetBlurredSize(size) if threatsize > 1 then text = text.." "..threatsizetext.."." -- Alpha Group. Heavy. textScreen = textScreen.." "..threatsizetext.."." end -- engagement tag? if contact.EngagementTag then text = text .. " "..contact.EngagementTag -- Alpha Group. Bulls eye 0 2 1, 16. Heavy. Targeted by Jazz 1 1. textScreen = textScreen .. " "..contact.EngagementTag -- Alpha Group, Bullseye 021, 16, Flanking. Targeted by Jazz 1 1. end -- Transmit Radio local RadioEntry_IsGroup = false local RadioEntry_ToScreen = self.debug if managedgroup and not IsGeneral then RadioEntry_IsGroup = managedgroup.IsPlayer RadioEntry_ToScreen = managedgroup.IsPlayer end self:_NewRadioEntry(text,textScreen,GID,RadioEntry_IsGroup,RadioEntry_ToScreen,true,false) end end -- empty queue from leftovers fifo:Clear() 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 maxentries = 1 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 contact = fifo:PullByID(sortedIDs[counter]) -- #AWACS.ManagedContact self:T({contact}) local position = contact.Cluster.coordinate or contact.Contact.position if contact and position then local tag = contact.TargetGroupNaming local reportingname = contact.ReportingName -- DONE - add tag self:_AnnounceContact(contact,false,group,true,tag,false,reportingname) 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 -- @param #boolean IsGeneral General picture if true, address no-one specific -- @return #AWACS self function AWACS:_Picture(Group,IsGeneral) self:T(self.lid.."_Picture") local text = "" local textScreen = text local general = IsGeneral local GID, Outcome, gcallsign = self:_GetManagedGrpID(Group) --local gcallsign = "" if general then gcallsign = "All Stations" --else --gcallsign = self:_GetCallSign(Group,GID) or "Ghost 1" end if Group and Outcome then general = false end if not self.intel then -- no intel yet! text = string.format("%s. %s. Picture Clean.",self.callsigntxt, gcallsign) textScreen = text self:_NewRadioEntry(text,text,GID,false,true,true,false) return self end if Outcome or general then -- Pilot is checked in -- get clusters from Intel -- DONE Use contacts table! local contactstable = self.Contacts:GetDataTable() --local clustertable = self.intel:GetClusterTable() or {} -- sort into buckets for _,_contact in pairs(contactstable) do local contact = _contact -- #AWACS.ManagedContact --self:T(UTILS.OneLineSerialize(contact)) local coordVec2 = contact.Contact.position:GetVec2() --local coordVec2 = cluster.coordinate:GetVec2() if self.OpsZone:IsVec2InZone(coordVec2) then self.PictureAO:Push(contact) elseif self.OrbitZone:IsVec2InZone(coordVec2) then self.PictureAO:Push(contact) elseif self.ControlZone:IsVec2InZone(coordVec2) then local distance = math.floor((contact.Contact.position:Get2DDistance(self.ControlZone:GetCoordinate()) / 1000) + 1) -- km self.PictureEWR:Push(contact,distance) end end local clustersAO = self.PictureAO:GetSize() local clustersEWR = self.PictureEWR:GetSize() if clustersAO < 3 and clustersEWR > 0 then -- make sure we have 3, can only add 1, 2 or 3 local IDstack = self.PictureEWR:GetSortedDataTable() -- how many do we need? local weneed = 3-clustersAO -- do we have enough? self:T(string.format("Picture - adding %d/%d contacts from EWR",weneed,clustersEWR)) if weneed > clustersEWR then weneed = clustersEWR end for i=1,weneed do self.PictureAO:Push(IDstack[i]) end end clustersAO = self.PictureAO:GetSize() if clustersAO == 0 and clustersEWR == 0 then -- clean self:_NewRadioEntry(text,textScreen,GID,Outcome,true,true,false) else if clustersAO > 0 then --if general then --text = string.format("%s, %s. ",gcallsign, self.callsigntxt) --textScreen = string.format("%s, %s. ",gcallsign, self.callsigntxt) --else text = string.format("%s, %s. Picture. ",gcallsign, self.callsigntxt) textScreen = string.format("%s, %s. Picture. ",gcallsign, self.callsigntxt) --end if clustersAO == 1 then text = text .. "One group. " textScreen = textScreen .. "One group.\n" else text = text .. clustersAO .. " groups. " textScreen = textScreen .. clustersAO .. " groups.\n" end self:_NewRadioEntry(text,textScreen,GID,Outcome,true,true,false) self:_CreatePicture(true,gcallsign,GID,3,general) self.PictureAO:Clear() self.PictureEWR:Clear() end end elseif self.AwacsFG then -- no, unknown text = string.format("%s. %s. Negative. You are not checked in.",gcallsign, self.callsigntxt) self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) 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 = "" local textScreen = "" local GID, Outcome = self:_GetManagedGrpID(Group) local gcallsign = self:_GetCallSign(Group,GID) or "Ghost 1" if not self.intel then -- no intel yet! text = string.format("%s. %s. Clean.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) self:_NewRadioEntry(text,text,0,false,true,true,false,true) return self end if Outcome then -- Pilot is checked in local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup local pilotgroup = managedgroup.Group local pilotcoord = managedgroup.Group:GetCoordinate() -- TODO - Use known contacts local contactstable = self.Contacts:GetDataTable() -- sort into buckets - AO only for bogey dope! for _,_contact in pairs(contactstable) do local managedcontact = _contact -- #AWACS.ManagedContact local contactposition = managedcontact.Cluster.coordinate or managedcontact.Contact.position -- Core.Point#COORDINATE local coordVec2 = contactposition:GetVec2() -- Get distance for sorting local dist = pilotcoord:Get2DDistance(contactposition) if self.ControlZone:IsVec2InZone(coordVec2) then self.ContactsAO:Push(managedcontact,dist) elseif self.BorderZone and self.BorderZone:IsVec2InZone(coordVec2) then self.ContactsAO:Push(managedcontact,dist) else local distance = contactposition:Get2DDistance(self.OrbitZone:GetCoordinate()) if (distance <= UTILS.NMToMeters(45)) then self.ContactsAO:Push(managedcontact,distance) end end end local contactsAO = self.ContactsAO:GetSize() if contactsAO == 0 then -- clean text = string.format("%s. %s. Clean.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) self:_NewRadioEntry(text,textScreen,GID,Outcome,Outcome,true,false,true) else if contactsAO > 0 then text = string.format("%s. %s. Bogey Dope. ",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) if contactsAO == 1 then text = text .. "One group. " textScreen = text .. "\n" else text = text .. contactsAO .. " groups. " textScreen = textScreen .. contactsAO .. " groups.\n" end self:_NewRadioEntry(text,textScreen,GID,Outcome,true,true,false,true) self:_CreateBogeyDope(self:_GetCallSign(Group,GID) or "Ghost 1",GID) end end elseif self.AwacsFG then -- no, unknown text = string.format("%s. %s. Negative. You are not checked in.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) end return self end --- [Internal] AWACS Menu for Show Info -- @param #AWACS self -- @param Wrapper.Group#GROUP Group Group to use -- @return #AWACS self function AWACS:_ShowAwacsInfo(Group) self:I(self.lid.."_ShowAwacsInfo") local report = REPORT:New("Info") report:Add("====================") report:Add(string.format("AWACS %s",self.callsigntxt)) report:Add(string.format("Radio: %d %s",self.Frequency,UTILS.GetModulationName(self.Modulation))) report:Add(string.format("Bulls Alias: %s",self.AOName)) report:Add(string.format("Coordinate: %s",self.AOCoordinate:ToStringLLDDM())) report:Add("====================") report:Add(string.format("Assignment Distance: %d NM",self.maxassigndistance)) report:Add(string.format("TAC Distance: %d NM",self.TacDistance)) report:Add(string.format("MELD Distance: %d NM",self.MeldDistance)) report:Add(string.format("THREAT Distance: %d NM",self.ThreatDistance)) report:Add("====================") report:Add(string.format("ROE/ROT: %s, %s",self.AwacsROE,self.AwacsROT)) MESSAGE:New(report:Text(),45,"AWACS"):ToGroup(Group) return self end --- [Internal] AWACS Menu for VID -- @param #AWACS self -- @param Wrapper.Group#GROUP Group Group to use -- @param #string Declaration Text declaration the player used -- @return #AWACS self function AWACS:_VID(Group,Declaration) self:T(self.lid.."_VID") local GID, Outcome, Callsign = self:_GetManagedGrpID(Group) local text = "" local TextTTS = "" if Outcome then --yes, known local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup local group = managedgroup.Group local position = group:GetCoordinate() local radius = UTILS.NMToMeters(self.DeclareRadius) or UTILS.NMToMeters(5) -- find tasked contact local TID = managedgroup.CurrentTask or 0 if TID > 0 then local task = self.ManagedTasks:ReadByID(TID) -- #AWACS.ManagedTask -- correct task? if task.ToDo ~= AWACS.TaskDescription.VID then return self end -- already done? if task.Status ~= AWACS.TaskStatus.ASSIGNED then return self end local CID = task.Cluster.CID local cluster = self.Contacts:ReadByID(CID) -- #AWACS.ManagedContact if cluster then local gposition = cluster.Contact.group:GetCoordinate() local cposition = gposition or cluster.Cluster.coordinate or cluster.Contact.position local distance = cposition:Get2DDistance(position) distance = UTILS.Round(distance,0) + 1 if distance <= radius or self.debug then -- we can VID self:T("Contact VID as "..Declaration) -- update cluster.IFF = Declaration task.Status = AWACS.TaskStatus.SUCCESS self.ManagedTasks:PullByID(TID) self.ManagedTasks:Push(task,TID) self.Contacts:PullByID(CID) self.Contacts:Push(cluster,CID) text = string.format("%s. %s. Copy, target identified as %s.",Callsign,self.callsigntxt, Declaration) self:T(text) else -- too far away self:T("Contact VID not close enough") text = string.format("%s. %s. Negative, get closer to target.",Callsign,self.callsigntxt) self:T(text) end self:_NewRadioEntry(text,text,GID,Outcome,true,true,false,true) end end -- elseif self.AwacsFG then -- no, unknown text = string.format("%s. %s. Negative. You are not checked in.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) 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, Callsign = self:_GetManagedGrpID(Group) local text = "" local TextTTS = "" if Outcome then --yes, known local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup local group = managedgroup.Group local position = group:GetCoordinate() local radius = UTILS.NMToMeters(self.DeclareRadius) or UTILS.NMToMeters(5) -- find contacts nearby local groupzone = ZONE_GROUP:New(group:GetName(),group, radius) local Coalitions = {"red","neutral"} if self.coalition == coalition.side.NEUTRAL then Coalitions = {"red","blue"} elseif self.coalition == coalition.side.RED then Coalitions = {"blue","neutral"} end local contactset = SET_GROUP:New():FilterCategoryAirplane():FilterCoalitions(Coalitions):FilterZones({groupzone}):FilterOnce() local numbercontacts = contactset:CountAlive() or 0 local foundcontacts = {} if numbercontacts > 0 then -- we have some around -- sort by distance contactset:ForEach( function (airpl) local distance = position:Get2DDistance(airpl:GetCoordinate()) distance = UTILS.Round(distance,0) + 1 foundcontacts[distance] = airpl end ,{} ) for _dist,_contact in UTILS.spairs(foundcontacts) do local distanz = _dist local contact = _contact -- Wrapper.Group#GROUP local ccoalition = contact:GetCoalition() local ctypename = contact:GetTypeName() local friendorfoe = "Neutral" if self.self.ModernEra then if ccoalition == self.coalition then friendorfoe = "Friendly" elseif ccoalition == coalition.side.NEUTRAL then friendorfoe = "Neutral" elseif ccoalition ~= self.coalition then friendorfoe = "Hostile" end else friendorfoe = "Spades" end -- see if that works self:T(string.format("Distance %d ContactName %s Coalition %d (%s) TypeName %s",distanz,contact:GetName(),ccoalition,friendorfoe,ctypename)) text = string.format("%s. %s. %s.",Callsign,self.callsigntxt,friendorfoe) TextTTS = text if self.ModernEra then text = string.format("%s %s.",text,ctypename) end break end else -- clean text = string.format("%s. %s. %s.",Callsign,self.callsigntxt,"Clean") TextTTS = text end self:_NewRadioEntry(TextTTS,text,GID,Outcome,true,true,false,true) -- elseif self.AwacsFG then -- no, unknown text = string.format("%s. %s. Negative. You are not checked in.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) end return self end --- [Internal] AWACS Menu for Commit -- @param #AWACS self -- @param Wrapper.Group#GROUP Group Group to use -- @return #AWACS self function AWACS:_Commit(Group) self:T(self.lid.."_Commit") local GID, Outcome = self:_GetManagedGrpID(Group) local text = "" if Outcome then local Pilot = self.ManagedGrps[GID] -- #AWACS.ManagedGroup -- Get current task from the group local currtaskid = Pilot.CurrentTask local managedtask = self.ManagedTasks:ReadByID(currtaskid) -- #AWACS.ManagedTask self:T(string.format("TID %d(%d) | ToDo %s | Status %s",currtaskid,managedtask.TID,managedtask.ToDo,managedtask.Status)) if managedtask then -- got a task, status? if managedtask.Status == AWACS.TaskStatus.REQUESTED then -- ok let's commit this one managedtask = self.ManagedTasks:PullByID(currtaskid) managedtask.Status = AWACS.TaskStatus.ASSIGNED self.ManagedTasks:Push(managedtask,currtaskid) self:T(string.format("COMMITTED - TID %d(%d) for GID %d | ToDo %s | Status %s",currtaskid,GID,managedtask.TID,managedtask.ToDo,managedtask.Status)) -- link to Pilot Pilot.HasAssignedTask = true Pilot.CurrentTask = currtaskid self.ManagedGrps[GID] = Pilot text = string.format("%s. %s. Copy.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) local EngagementTag = string.format("Targeted by %s.",Pilot.CallSign) self:_UpdateContactEngagementTag(Pilot.ContactCID,EngagementTag,false,false,AWACS.TaskStatus.ASSIGNED) self:_NewRadioEntry(text,text,GID,Outcome,true,true,false,true) else self:E(self.lid.."Cannot find REQUESTED managed task with TID="..currtaskid.." for GID="..GID) end else self:E(self.lid.."Cannot find managed task with TID="..currtaskid.." for GID="..GID) end elseif self.AwacsFG then -- no, unknown text = string.format("%s. %s. Negative. You are not checked in.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) end return self end --- [Internal] AWACS Menu for Judy -- @param #AWACS self -- @param Wrapper.Group#GROUP Group Group to use -- @return #AWACS self function AWACS:_Judy(Group) self:T(self.lid.."_Judy") local GID, Outcome = self:_GetManagedGrpID(Group) local text = "" if Outcome then local Pilot = self.ManagedGrps[GID] -- #AWACS.ManagedGroup -- Get current task from the group local currtaskid = Pilot.CurrentTask local managedtask = self.ManagedTasks:ReadByID(currtaskid) -- #AWACS.ManagedTask if managedtask then -- got a task, status? if managedtask.Status == AWACS.TaskStatus.REQUESTED or managedtask.Status == AWACS.TaskStatus.UNASSIGNED then -- ok let's commit this one managedtask = self.ManagedTasks:PullByID(currtaskid) managedtask.Status = AWACS.TaskStatus.ASSIGNED self.ManagedTasks:Push(managedtask,currtaskid) text = string.format("%s. %s. Copy.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) local EngagementTag = string.format("Targeted by %s.",Pilot.CallSign) self:_UpdateContactEngagementTag(Pilot.ContactCID,EngagementTag,false,false,AWACS.TaskStatus.ASSIGNED) self:_NewRadioEntry(text,text,GID,Outcome,true,true,false,true) else self:E(self.lid.."Cannot find REQUESTED or UNASSIGNED managed task with TID="..currtaskid.." for GID="..GID) end else self:E(self.lid.."Cannot find managed task with TID="..currtaskid.." for GID="..GID) end elseif self.AwacsFG then -- no, unknown text = string.format("%s. %s. Negative. You are not checked in.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) end return self end --- [Internal] AWACS Menu for Unable -- @param #AWACS self -- @param Wrapper.Group#GROUP Group Group to use -- @return #AWACS self function AWACS:_Unable(Group) self:T(self.lid.."_Unable") local GID, Outcome = self:_GetManagedGrpID(Group) local text = "" if Outcome then local Pilot = self.ManagedGrps[GID] -- #AWACS.ManagedGroup -- Get current task from the group local currtaskid = Pilot.CurrentTask local managedtask = self.ManagedTasks:ReadByID(currtaskid) -- #AWACS.ManagedTask self:T(string.format("UNABLE for TID %d(%d) | ToDo %s | Status %s",currtaskid,managedtask.TID,managedtask.ToDo,managedtask.Status)) if managedtask then -- got a task, status? if managedtask.Status == AWACS.TaskStatus.REQUESTED then -- ok let's commit this one managedtask = self.ManagedTasks:PullByID(currtaskid) --managedtask.AssignedGroupID = 0 managedtask.IsUnassigned = true managedtask.Status = AWACS.TaskStatus.FAILED self.ManagedTasks:Push(managedtask,currtaskid) self:T(string.format("REJECTED - TID %d(%d) for GID %d | ToDo %s | Status %s",currtaskid,GID,managedtask.TID,managedtask.ToDo,managedtask.Status)) -- unlink group from task Pilot.HasAssignedTask = false Pilot.CurrentTask = 0 Pilot.LastTasking = timer.getTime() self.ManagedGrps[GID] = Pilot text = string.format("%s. %s. Copy.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) local EngagementTag = "" self:_UpdateContactEngagementTag(Pilot.ContactCID,EngagementTag,false,false,AWACS.TaskStatus.UNASSIGNED) self:_NewRadioEntry(text,text,GID,Outcome,true,true,false,true) else self:E(self.lid.."Cannot find REQUESTED managed task with TID="..currtaskid.." for GID="..GID) end else self:E(self.lid.."Cannot find managed task with TID="..currtaskid.." for GID="..GID) end elseif self.AwacsFG then -- no, unknown text = string.format("%s. %s. Negative. You are not checked in.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) end return self end --- [Internal] AWACS Menu for Abort -- @param #AWACS self -- @param Wrapper.Group#GROUP Group Group to use -- @return #AWACS self function AWACS:_TaskAbort(Group) self:T(self.lid.."_TaskAbort") local GID, Outcome = self:_GetManagedGrpID(Group) local text = "" if Outcome then local Pilot = self.ManagedGrps[GID] -- #AWACS.ManagedGroup -- Get current task from the group local currtaskid = Pilot.CurrentTask local managedtask = self.ManagedTasks:ReadByID(currtaskid) -- #AWACS.ManagedTask if managedtask then -- got a task, status? self:T(string.format("ABORT for TID %d(%d) | ToDo %s | Status %s",currtaskid,managedtask.TID,managedtask.ToDo,managedtask.Status)) if managedtask.Status == AWACS.TaskStatus.ASSIGNED then -- ok let's un-commit this one managedtask = self.ManagedTasks:PullByID(currtaskid) managedtask.Status = AWACS.TaskStatus.FAILED --managedtask.AssignedGroupID = 0 managedtask.IsUnassigned = true self.ManagedTasks:Push(managedtask,currtaskid) -- unlink group self:T(string.format("ABORTED - TID %d(%d) for GID %d | ToDo %s | Status %s",currtaskid,GID,managedtask.TID,managedtask.ToDo,managedtask.Status)) -- unlink group from task Pilot.HasAssignedTask = false Pilot.CurrentTask = 0 Pilot.LastTasking = timer.getTime() self.ManagedGrps[GID] = Pilot text = string.format("%s. %s. Copy.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) local EngagementTag = "" self:_UpdateContactEngagementTag(Pilot.ContactCID,EngagementTag,false,false,AWACS.TaskStatus.UNASSIGNED) self:_NewRadioEntry(text,text,GID,Outcome,true,true,false,true) else self:E(self.lid.."Cannot find ASSIGNED managed task with TID="..currtaskid.." for GID="..GID) end else self:E(self.lid.."Cannot find managed task with TID="..currtaskid.." for GID="..GID) end elseif self.AwacsFG then -- no, unknown text = string.format("%s. %s. Negative. You are not checked in.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) end 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, Callsign = self:_GetManagedGrpID(Group) local text = "" if Outcome then -- known group -- Do we have a task? local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup if managedgroup.IsPlayer then if managedgroup.CurrentTask >0 and self.ManagedTasks:HasUniqueID(managedgroup.CurrentTask) then -- get task structure local currenttask = self.ManagedTasks: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 = Callsign 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 local pposition = managedgroup.Group:GetCoordinate() or managedgroup.LastKnownPosition if currenttask.ToDo == AWACS.TaskDescription.INTERCEPT or currenttask.ToDo == AWACS.TaskDescription.VID then local targetpos = currenttask.Target:GetCoordinate() if pposition and targetpos then local alti = currenttask.Cluster.altitude or currenttask.Contact.altitude or currenttask.Contact.group:GetAltitude() local direction = self:_ToStringBRA(pposition,targetpos,alti) description = description .. "\nBRA "..direction end elseif currenttask.ToDo == AWACS.TaskDescription.ANCHOR or currenttask.ToDo == AWACS.TaskDescription.REANCHOR then local targetpos = currenttask.Target:GetCoordinate() local direction = self:_ToStringBR(pposition,targetpos) description = description .. "\nBR "..direction end MESSAGE:New(string.format("%s\nStatus %s",description,status),30,"AWACS",true):ToGroup(Group) end end end elseif self.AwacsFG then -- no, unknown text = string.format("%s. %s. Negative. You are not checked in.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) 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:T(self.lid.."_CheckIn "..Group:GetName()) -- check if already known local GID, Outcome = self:_GetManagedGrpID(Group) local text = "" local textTTS = "" if not Outcome then self.ManagedGrpID = self.ManagedGrpID + 1 local managedgroup = {} -- #AWACS.ManagedGroup managedgroup.Group = Group --managedgroup.GroupName = string.match(Group:GetName(),"([%a%s]+)#") managedgroup.GroupName = Group:GetName() managedgroup.IsPlayer = true managedgroup.IsAI = false managedgroup.CallSign = self:_GetCallSign(Group,GID) or "Ghost 1" managedgroup.CurrentAuftrag = 0 managedgroup.CurrentTask = 0 managedgroup.HasAssignedTask = true managedgroup.Blocked = true managedgroup.GID = self.ManagedGrpID --managedgroup.TaskQueue = FIFO:New() managedgroup.LastKnownPosition = Group:GetCoordinate() managedgroup.LastTasking = timer.getTime() GID = managedgroup.GID self.ManagedGrps[self.ManagedGrpID]=managedgroup local alphacheckbulls = self:_ToStringBULLS(Group:GetCoordinate()) --local alphacheckbullstts = self:_ToStringBullsTTS(alphacheckbulls)-- make tts friendly local alphacheckbullstts = self:_ToStringBULLS(Group:GetCoordinate(),false,true) self.ManagedGrps[self.ManagedGrpID]=managedgroup text = string.format("%s. %s. Alpha Check. %s",managedgroup.CallSign,self.callsigntxt,alphacheckbulls) textTTS = string.format("%s. %s. Alpha Check. %s",managedgroup.CallSign,self.callsigntxt,alphacheckbullstts) self:__CheckedIn(1,managedgroup.GID) if self.PlayerStationName then self:__AssignAnchor(5,managedgroup.GID,true,self.PlayerStationName) else self:__AssignAnchor(5,managedgroup.GID) end elseif self.AwacsFG then text = string.format("%s. %s. Negative. You are already checked in.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) end self:_NewRadioEntry(textTTS,text,GID,Outcome,true,true,false) 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:T(self.lid.."_CheckInAI "..Group:GetName() .. " to Auftrag Nr "..AuftragsNr) -- 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 local callsignstring = UTILS.GetCallsignName(self.AICAPCAllName) local callsignmajor = math.fmod(self.AICAPCAllNumber,9) local callsign = string.format("%s %d 1",callsignstring,callsignmajor) if self.callsignshort then callsign = string.format("%s %d",callsignstring,callsignmajor) end self:T("Assigned Callsign: ".. callsign) managedgroup.CallSign = callsign managedgroup.CurrentAuftrag = AuftragsNr managedgroup.HasAssignedTask = false managedgroup.GID = self.ManagedGrpID managedgroup.LastKnownPosition = Group:GetCoordinate() self.ManagedGrps[self.ManagedGrpID]=managedgroup -- SRS voice for CAP FlightGroup:SetDefaultRadio(self.Frequency,self.Modulation,false) FlightGroup:SwitchRadio(self.Frequency,self.Modulation) local CAPVoice = self.CAPVoice if self.PathToGoogleKey then CAPVoice = AWACS.CapVoices[math.floor(math.random(1,10))] end FlightGroup:SetSRS(self.PathToSRS,self.CAPGender,self.CAPCulture,CAPVoice,self.Port,self.PathToGoogleKey,"FLIGHT") text = string.format("%s. %s. Checking in as fragged. Expected playtime %d hours. Request Alpha Check %s.",self.callsigntxt, managedgroup.CallSign, self.CAPTimeOnStation, self.AOName) self:_NewRadioEntry(text,text,managedgroup.GID,Outcome,false,true,true) local alphacheckbulls = self:_ToStringBULLS(Group:GetCoordinate(),false,true) --alphacheckbulls = self:_ToStringBullsTTS(alphacheckbulls)-- make tts friendly text = string.format("%s. %s. Alpha Check. %s",managedgroup.CallSign,self.callsigntxt,alphacheckbulls) self:__CheckedIn(1,managedgroup.GID) local AW = FlightGroup:GetAirWing() if AW.HasOwnStation then self:__AssignAnchor(5,managedgroup.GID,AW.HasOwnStation,AW.StationName) else self:__AssignAnchor(5,managedgroup.GID) end else text = string.format("%s. %s. Negative. You are already checked in.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) end self:_NewRadioEntry(text,text,GID,Outcome,false,true,false) return self end --- [Internal] AWACS Menu for Check Out -- @param #AWACS self -- @param Wrapper.Group#GROUP Group Group to use -- @param #number GID GroupID -- @param #boolean dead If true, group is dead crashed or otherwise n/a -- @return #AWACS self function AWACS:_CheckOut(Group,GID,dead) self:T(self.lid.."_CheckOut") -- check if already known local GID, Outcome = self:_GetManagedGrpID(Group) local text = "" if Outcome then -- yes, known text = string.format("%s. %s. Copy. Have a safe flight home.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) self:T(text) -- grab some data before we nil the entry local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup local Stack = managedgroup.AnchorStackNo local Angels = managedgroup.AnchorStackAngels -- remove menus if managedgroup.IsPlayer then -- DONE Move to FIFO if self.clientmenus:HasUniqueID(managedgroup.GroupName) then local menus = self.clientmenus:PullByID(managedgroup.GroupName) --#AWACS.MenuStructure menus.basemenu:Remove() --self.clientmenus[AnchorAssigned.GroupName] = nil end end -- delete open tasks if managedgroup.CurrentTask and managedgroup.CurrentTask > 0 then self.ManagedTasks:PullByID(managedgroup.CurrentTask ) self:_UpdateContactEngagementTag(managedgroup.ContactCID,"",false,false) end self.ManagedGrps[GID] = nil self:__CheckedOut(1,GID,Stack,Angels) else -- no, unknown if not dead then text = string.format("%s. %s. Negative. You are not checked in.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) end end if not dead then self:_NewRadioEntry(text,text,GID,Outcome,false,true,false) end 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_CLIENT local aliveset = clientset:GetSetObjects() or {}-- #table of #CLIENT objects --local clientmenus = {} local clientcount = 0 local clientcheckedin = 0 for _,_group in pairs(aliveset) do -- go through set and build the menu local grp = _group -- Wrapper.Client#CLIENT local cgrp = grp:GetGroup() local cgrpname = nil if cgrp and cgrp:IsAlive() then cgrpname = cgrp:GetName() self:T(cgrpname) end --cgrpname = string.match(cgrpname,"([%a%s]+)#") if self.MenuStrict then -- check if pilot has checked in if cgrp and cgrp:IsAlive() then clientcount = clientcount + 1 local GID, checkedin = self:_GetManagedGrpID(cgrp) if checkedin then -- full menu minus checkin clientcheckedin = clientcheckedin + 1 --self.clientmenus:Flush() local hasclientmenu = self.clientmenus:ReadByID(cgrpname) -- #AWACS.MenuStructure --self:T({hasclientmenu}) local basemenu = hasclientmenu.basemenu -- Core.Menu#MENU_GROUP if hasclientmenu and (not hasclientmenu.menuset) then self:T(self.lid.."Setting Menus for "..cgrpname) basemenu:RemoveSubMenus() --basemenu:Refresh() local tasking = MENU_GROUP:New(cgrp,"Tasking",basemenu) local showtask = MENU_GROUP_COMMAND:New(cgrp,"Showtask",tasking,self._Showtask,self,cgrp) local commit = MENU_GROUP_COMMAND:New(cgrp,"Commit",tasking,self._Commit,self,cgrp) local unable = MENU_GROUP_COMMAND:New(cgrp,"Unable",tasking,self._Unable,self,cgrp) local abort = MENU_GROUP_COMMAND:New(cgrp,"Abort",tasking,self._TaskAbort,self,cgrp) --local judy = MENU_GROUP_COMMAND:New(cgrp,"Judy",tasking,self._Judy,self,cgrp) if self.AwacsROE == AWACS.ROE.POLICE or self.AwacsROE == AWACS.ROE.VID then local vid = MENU_GROUP:New(cgrp,"VID as",tasking) local hostile = MENU_GROUP_COMMAND:New(cgrp,"Hostile",vid,self._VID,self,cgrp,AWACS.IFF.ENEMY) local neutral = MENU_GROUP_COMMAND:New(cgrp,"Neutral",vid,self._VID,self,cgrp,AWACS.IFF.NEUTRAL) local friendly = MENU_GROUP_COMMAND:New(cgrp,"Friendly",vid,self._VID,self,cgrp,AWACS.IFF.FRIENDLY) end local picture = MENU_GROUP_COMMAND:New(cgrp,"Picture",basemenu,self._Picture,self,cgrp) local bogeydope = MENU_GROUP_COMMAND:New(cgrp,"Bogey Dope",basemenu,self._BogeyDope,self,cgrp) local declare = MENU_GROUP_COMMAND:New(cgrp,"Declare",basemenu,self._Declare,self,cgrp) local ainfo = MENU_GROUP_COMMAND:New(cgrp,"Awacs Info",basemenu,self._ShowAwacsInfo,self,cgrp) local checkout = MENU_GROUP_COMMAND:New(cgrp,"Check Out",basemenu,self._CheckOut,self,cgrp) --basemenu:Set() basemenu:Refresh() local menus = { -- #AWACS.MenuStructure groupname = cgrpname, menuset = true, basemenu = basemenu, checkout= checkout, picture = picture, bogeydope = bogeydope, declare = declare, tasking = tasking, showtask = showtask, --judy = judy, unable = unable, abort = abort, commit=commit, } self.clientmenus:PullByID(cgrpname) self.clientmenus:Push(menus,cgrpname) end elseif not self.clientmenus:HasUniqueID(cgrpname) then -- check in only local basemenu = MENU_GROUP:New(cgrp,self.Name,nil) --basemenu:RemoveSubMenus() local checkin = MENU_GROUP_COMMAND:New(cgrp,"Check In",basemenu,self._CheckIn,self,cgrp) checkin:SetTag(cgrp:GetName()) --basemenu:Set() basemenu:Refresh() local menus = { -- #AWACS.MenuStructure groupname = cgrpname, menuset = false, basemenu = basemenu, checkin = checkin, } self.clientmenus:Push(menus,cgrpname) end end else if cgrp and cgrp:IsAlive() and not self.clientmenus:HasUniqueID(cgrpname) then local basemenu = MENU_GROUP:New(cgrp,self.Name,nil) --basemenu:RemoveSubMenus() --basemenu:Refresh() local picture = MENU_GROUP_COMMAND:New(cgrp,"Picture",basemenu,self._Picture,self,cgrp) local bogeydope = MENU_GROUP_COMMAND:New(cgrp,"Bogey Dope",basemenu,self._BogeyDope,self,cgrp) local declare = MENU_GROUP_COMMAND:New(cgrp,"Declare",basemenu,self._Declare,self,cgrp) local tasking = MENU_GROUP:New(cgrp,"Tasking",basemenu) local showtask = MENU_GROUP_COMMAND:New(cgrp,"Showtask",tasking,self._Showtask,self,cgrp) local commit = MENU_GROUP_COMMAND:New(cgrp,"Commit",tasking,self._Commit,self,cgrp) local unable = MENU_GROUP_COMMAND:New(cgrp,"Unable",tasking,self._Unable,self,cgrp) local abort = MENU_GROUP_COMMAND:New(cgrp,"Abort",tasking,self._TaskAbort,self,cgrp) --local judy = MENU_GROUP_COMMAND:New(cgrp,"Judy",tasking,self._Judy,self,cgrp) if self.AwacsROE == AWACS.ROE.POLICE or self.AwacsROE == AWACS.ROE.VID then local vid = MENU_GROUP:New(cgrp,"VID as",tasking) local hostile = MENU_GROUP_COMMAND:New(cgrp,"Hostile",vid,self._VID,self,cgrp,AWACS.IFF.ENEMY) local neutral = MENU_GROUP_COMMAND:New(cgrp,"Neutral",vid,self._VID,self,cgrp,AWACS.IFF.NEUTRAL) local friendly = MENU_GROUP_COMMAND:New(cgrp,"Friendly",vid,self._VID,self,cgrp,AWACS.IFF.FRIENDLY) end local ainfo = MENU_GROUP_COMMAND:New(cgrp,"Awacs Info",basemenu,self._ShowAwacsInfo,self,cgrp) local checkin = MENU_GROUP_COMMAND:New(cgrp,"Check In",basemenu,self._CheckIn,self,cgrp) local checkout = MENU_GROUP_COMMAND:New(cgrp,"Check Out",basemenu,self._CheckOut,self,cgrp) --basemenu:Set() basemenu:Refresh() local menus = { -- #AWACS.MenuStructure groupname = cgrpname, menuset = true, basemenu = basemenu, checkin = checkin, checkout= checkout, picture = picture, bogeydope = bogeydope, declare = declare, showtask = showtask, tasking = tasking, --judy = judy, unable = unable, abort = abort, commit = commit, } self.clientmenus:Push(menus,cgrpname) end end end --self.clientmenus = clientmenus self.MonitoringData.Players = clientcount or 0 self.MonitoringData.PlayersCheckedin = clientcheckedin or 0 return self end --- [Internal] AWACS Delete a new Anchor Stack from a Marker - only works if no assignments are on the station -- @param #AWACS self -- @return #AWACS self function AWACS:_DeleteAnchorStackFromMarker(Name,Coord) self:I(self.lid.."_DeleteAnchorStackFromMarker") if self.AnchorStacks:HasUniqueID(Name) and self.PlayerStationName == Name then local stack = self.AnchorStacks:ReadByID(Name) -- #AWACS.AnchorData local marker = stack.AnchorMarker if stack.AnchorAssignedID:Count() == 0 then marker:Remove() if self.debug then stack.StationZone:UndrawZone() end self.AnchorStacks:PullByID(Name) self.PlayerStationName = nil else if self.debug then self:I(self.lid.."**** Cannot delete station, there are CAPs assigned!") local text = marker:GetText() marker:TextUpdate(text.."\nMarked for deletion") end end end return self end --- [Internal] AWACS Move a new Anchor Stack from a Marker -- @param #AWACS self -- @return #AWACS self function AWACS:_MoveAnchorStackFromMarker(Name,Coord) self:I(self.lid.."_MoveAnchorStackFromMarker") if self.AnchorStacks:HasUniqueID(Name) and self.PlayerStationName == Name then local station = self.AnchorStacks:PullByID(Name) -- #AWACS.AnchorData local stationtag = string.format("Station: %s\nCoordinate: %s",Name,Coord:ToStringLLDDM()) local marker = station.AnchorMarker local zone = station.StationZone if self.debug then zone:UndrawZone() end local radius = self.StationZone:GetRadius() if radius < 10000 then radius = 10000 end station.StationZone = ZONE_RADIUS:New(Name, Coord:GetVec2(), radius) marker:UpdateCoordinate(Coord) marker:UpdateText(stationtag) station.AnchorMarker = marker if self.debug then station.StationZone:DrawZone(-1,{0,0,1},1,{0,0,1},0.2,5,true) end self.AnchorStacks:Push(station,Name) end return self end --- [Internal] AWACS Create a new Anchor Stack from a Marker - this then is the preferred station for players -- @param #AWACS self -- @return #AWACS self function AWACS:_CreateAnchorStackFromMarker(Name,Coord) self:I(self.lid.."_CreateAnchorStackFromMarker") local AnchorStackOne = {} -- #AWACS.AnchorData AnchorStackOne.AnchorBaseAngels = self.AnchorBaseAngels AnchorStackOne.Anchors = FIFO:New() -- Utilities.FiFo#FIFO AnchorStackOne.AnchorAssignedID = FIFO:New() -- Utilities.FiFo#FIFO local newname = Name for i=1,self.AnchorMaxStacks do AnchorStackOne.Anchors:Push((i-1)*self.AnchorStackDistance+self.AnchorBaseAngels) end local radius = self.StationZone:GetRadius() if radius < 10000 then radius = 10000 end AnchorStackOne.StationZone = ZONE_RADIUS:New(newname, Coord:GetVec2(), radius) AnchorStackOne.StationZoneCoordinate = Coord AnchorStackOne.StationZoneCoordinateText = Coord:ToStringLLDDM() AnchorStackOne.StationName = newname --push to AnchorStacks if self.debug then AnchorStackOne.StationZone:DrawZone(-1,{0,0,1},1,{0,0,1},0.2,5,true) local stationtag = string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM()) AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToAll() else local stationtag = string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM()) AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToAll() end self.AnchorStacks:Push(AnchorStackOne,newname) self.PlayerStationName = newname return self end --- [Internal] AWACS Create a new Anchor Stack -- @param #AWACS self -- @return #boolean success -- @return #number 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 = self.StationZone:GetName() for i=1,self.AnchorMaxStacks do AnchorStackOne.Anchors:Push((i-1)*self.AnchorStackDistance+self.AnchorBaseAngels) end if stackscreated == 0 then local newsubname = AWACS.AnchorNames[stackscreated+1] or tostring(stackscreated+1) newname = self.StationZone:GetName() .. "-"..newsubname AnchorStackOne.StationZone = self.StationZone AnchorStackOne.StationZoneCoordinate = self.StationZone:GetCoordinate() AnchorStackOne.StationZoneCoordinateText = self.StationZone:GetCoordinate():ToStringLLDDM() AnchorStackOne.StationName = newname --push to AnchorStacks if self.debug then --self.AnchorStacks:Flush() AnchorStackOne.StationZone:DrawZone(-1,{0,0,1},1,{0,0,1},0.2,5,true) local stationtag = string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM()) AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToAll() else local stationtag = string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM()) AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToAll() end self.AnchorStacks:Push(AnchorStackOne,newname) else local newsubname = AWACS.AnchorNames[stackscreated+1] or tostring(stackscreated+1) newname = self.StationZone:GetName() .. "-"..newsubname local anchorbasecoord = self.OpsZone:GetCoordinate() -- Core.Point#COORDINATE -- OpsZone can be Polygon, so use distance to StationZone as radius local anchorradius = anchorbasecoord:Get2DDistance(self.StationZone:GetCoordinate()) local angel = self.StationZone: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 local radius = self.StationZone:GetRadius() if radius < 10000 then radius = 10000 end AnchorStackOne.StationZone = ZONE_RADIUS:New(newname, newanchorbasecoord:GetVec2(), radius) AnchorStackOne.StationZoneCoordinate = newanchorbasecoord AnchorStackOne.StationZoneCoordinateText = newanchorbasecoord:ToStringLLDDM() AnchorStackOne.StationName = newname --push to AnchorStacks if self.debug then --self.AnchorStacks:Flush() AnchorStackOne.StationZone:DrawZone(-1,{0,0,1},1,{0,0,1},0.2,5,true) local stationtag = string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM()) AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToAll() else local stationtag = string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM()) AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToAll() end self.AnchorStacks:Push(AnchorStackOne,newname) 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 -- @param #number GID Managed Group ID -- @param #boolean HasOwnStation -- @param #string StationName -- @return #AWACS self function AWACS:_AssignAnchorToID(GID, HasOwnStation, StationName) self:T(self.lid.."_AssignAnchorToID") if not HasOwnStation then 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) -- push back to AnchorStacks self.AnchorStacks:Push(Anchor,Anchor.StationName) self:T({Anchor,freeangels}) self:__AssignedAnchor(5,GID,Anchor,AnchorStackNo,freeangels) else self:E(self.lid .. "Cannot assign free anchor stack to GID ".. GID .. " Trying again in 10secs.") -- try again ... self:__AssignAnchor(10,GID) end else local Anchor = self.AnchorStacks:PullByID(StationName) -- #AWACS.AnchorData -- pull one free angels local freeangels = Anchor.Anchors:Pull() or 25 -- push GID on anchor Anchor.AnchorAssignedID:Push(GID) -- push back to AnchorStacks self.AnchorStacks:Push(Anchor,StationName) self:T({Anchor,freeangels}) local StackNo = self.AnchorStacks.stackbyid[StationName].pointer self:__AssignedAnchor(5,GID,Anchor,StackNo,freeangels) 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) local gid = GID or 0 local stack = AnchorStackNo or 0 local angels = Angels or 0 local debugstring = string.format("%s_RemoveIDFromAnchor for GID=%d Stack=%d Angels=%d",self.lid,gid,stack,angels) self:T(debugstring) -- pull correct anchor if stack > 0 and angels > 0 then local AnchorStackNo = AnchorStackNo or 1 local Anchor = self.AnchorStacks:ReadByPointer(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) end 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") if self.intelstarted then return self end self.DetectionSet:AddGroup(awacs) local intel = INTEL:New(self.DetectionSet,self.coalition,self.callsigntxt) --intel:SetVerbosity(2) --intel:SetClusterRadius(UTILS.NMToMeters(5)) intel:SetClusterAnalysis(true,false,false) local acceptzoneset = SET_ZONE:New() acceptzoneset:AddZone(self.ControlZone) acceptzoneset:AddZone(self.OpsZone) self.OrbitZone:SetRadius(UTILS.NMToMeters(55)) acceptzoneset:AddZone(self.OrbitZone) if self.BorderZone then acceptzoneset:AddZone(self.BorderZone) end --self.AwacsInZone intel:SetAcceptZones(acceptzoneset) 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 self.intelstarted = true intel.statusupdate = -30 intel:__Start(5) 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 -- @return #string AWACS.Shipsize entry for size 1..4 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) if threatsize == 0 then threatsize = 1 end if threatsize then end local threatsizetext = AWACS.Shipsize[1] if threatsize == 2 then threatsizetext = AWACS.Shipsize[2] elseif threatsize == 3 then threatsizetext = AWACS.Shipsize[3] elseif threatsize > 3 then threatsizetext = AWACS.Shipsize[4] end return threatsize, threatsizetext 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 BR text for TTS -- @param #AWACS self -- @param Core.Point#COORDINATE FromCoordinate -- @param Core.Point#COORDINATE ToCoordinate -- @return #string BRText Desired Output (BR) "214, 35 miles" -- @return #string BRTextTTS Desired Output (BR) "2 1 4, 35 miles" function AWACS:_ToStringBR(FromCoordinate,ToCoordinate) self:T(self.lid.."_ToStringBR") local BRText = "" local BRTextTTS = "" local DirectionVec3 = FromCoordinate:GetDirectionVec3( ToCoordinate ) local AngleRadians = FromCoordinate:GetAngleRadians( DirectionVec3 ) local AngleDegrees = UTILS.Round( UTILS.ToDegree( AngleRadians ), 0 ) -- degrees local AngleDegText = string.format("%03d",AngleDegrees) -- 051 local AngleDegTextTTS = "" --if self.PathToGoogleKey then --AngleDegTextTTS = string.format("%s",AngleDegText) --else --AngleDegTextTTS = string.format("%s",AngleDegText) --end AngleDegText = string.gsub(AngleDegText,"%d","%1 ") -- "0 5 1 " AngleDegText = string.gsub(AngleDegText," $","") -- "0 5 1" AngleDegTextTTS = string.gsub(AngleDegText,"0","zero") local Distance = ToCoordinate:Get2DDistance( FromCoordinate ) --meters local distancenm = UTILS.Round(UTILS.MetersToNM(Distance),0) BRText = string.format("%03d, %d miles",AngleDegrees,distancenm) BRTextTTS = string.format("%s, %d miles",AngleDegText,distancenm) if self.PathToGoogleKey then BRTextTTS = string.format("%s, %d miles",AngleDegTextTTS,distancenm) end self:T(BRText,BRTextTTS) return BRText,BRTextTTS end --- [Internal] Get BRA text for TTS -- @param #AWACS self -- @param Core.Point#COORDINATE FromCoordinate -- @param Core.Point#COORDINATE ToCoordinate -- @param #number Altitude Altitude in meters -- @return #string BRText Desired Output (BRA) "214, 35 miles, 20 thousand" -- @return #string BRTextTTS Desired Output (BRA) "2 1 4, 35 miles, 20 thousand" function AWACS:_ToStringBRA(FromCoordinate,ToCoordinate,Altitude) self:T(self.lid.."_ToStringBRA") local BRText = "" local BRTextTTS = "" local altitude = UTILS.Round(UTILS.MetersToFeet(Altitude)/1000,0) local DirectionVec3 = FromCoordinate:GetDirectionVec3( ToCoordinate ) local AngleRadians = FromCoordinate:GetAngleRadians( DirectionVec3 ) local AngleDegrees = UTILS.Round( UTILS.ToDegree( AngleRadians ), 0 ) -- degrees local AngleDegText = string.format("%03d",AngleDegrees) -- 051 --local AngleDegTextTTS = string.format("%s",AngleDegText) AngleDegText = string.gsub(AngleDegText,"%d","%1 ") -- "0 5 1 " AngleDegText = string.gsub(AngleDegText," $","") -- "0 5 1" local AngleDegTextTTS = string.gsub(AngleDegText,"0","zero") local Distance = ToCoordinate:Get2DDistance( FromCoordinate ) --meters local distancenm = UTILS.Round(UTILS.MetersToNM(Distance),0) if altitude >= 1 then BRText = string.format("%03d, %d miles, %d thousand",AngleDegrees,distancenm,altitude) BRTextTTS = string.format("%s, %d miles, %d thousand",AngleDegText,distancenm,altitude) if self.PathToGoogleKey then BRTextTTS = string.format("%s, %d miles, %d thousand",AngleDegTextTTS,distancenm,altitude) end else BRText = string.format("%03d, %d miles, very low",AngleDegrees,distancenm) BRTextTTS = string.format("%s, %d miles, very low",AngleDegText,distancenm) if self.PathToGoogleKey then BRTextTTS = string.format("%s, %d miles, very low",AngleDegTextTTS,distancenm) end end self:T(BRText,BRTextTTS) return BRText,BRTextTTS end --- [Internal] Get BR text for TTS - ie "Rock 214, 24 miles" and TTS "Rock 2 1 4, 24 miles" -- @param #AWACS self -- @param Core.Point#COORDINATE clustercoordinate -- @return #string BRAText -- @return #string BRATextTTS function AWACS:_GetBRAfromBullsOrAO(clustercoordinate) self:T(self.lid.."_GetBRAfromBullsOrAO") local refcoord = self.AOCoordinate -- Core.Point#COORDINATE local BRAText = "" local BRATextTTS = "" -- get BR from AO local bullsname = self.AOName or "Rock" local stringbr, stringbrtts = self:_ToStringBR(refcoord,clustercoordinate) BRAText = string.format("%s %s",bullsname,stringbr) BRATextTTS = string.format("%s %s",bullsname,stringbrtts) self:T(BRAText,BRATextTTS) return BRAText,BRATextTTS 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 -- @param #AWACS.TaskStatus TaskStatus Status of this task -- @param Ops.Auftrag#AUFTRAG Auftrag The Auftrag for this task if any -- @param Ops.Intelligence#INTEL.Cluster Cluster Intel Cluster for this task -- @param Ops.Intelligence#INTEL.Contact Contact Intel Contact for this task -- @return #number TID Task ID created function AWACS:_CreateTaskForGroup(GroupID,Description,ScreenText,Object,TaskStatus,Auftrag,Cluster,Contact) self:T(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 = TaskStatus or AWACS.TaskStatus.ASSIGNED task.ToDo = Description task.Auftrag = Auftrag task.Cluster = Cluster task.Contact = Contact task.IsPlayerTask = managedgroup.IsPlayer task.IsUnassigned = TaskStatus == AWACS.TaskStatus.UNASSIGNED and false or true -- task. if Object and Object:IsInstanceOf("TARGET") then task.Target = Object else task.Target = TARGET:New(Object) end task.ScreenText = ScreenText if Description == AWACS.TaskDescription.ANCHOR or Description == AWACS.TaskDescription.REANCHOR then task.Target.Type = TARGET.ObjectType.ZONE end task.RequestedTimestamp = timer.getTime() self.ManagedTasks:Push(task,task.TID) managedgroup.HasAssignedTask = true managedgroup.CurrentTask = task.TID --managedgroup.TaskQueue:Push(task.TID) self.ManagedGrps[GroupID] = managedgroup return task.TID 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 and managedgroup.CurrentTask ~= 0 then local TaskID = managedgroup.CurrentTask if self.ManagedTasks:HasUniqueID(TaskID) then return self.ManagedTasks: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.ManagedTasks:HasUniqueID(TaskID) then local task = self.ManagedTasks: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.ManagedTasks: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.ManagedTasks: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:T(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) self:_NewRadioEntry(TextTTS,TextTTS,GID,false,false,true,true) end return self end --- [Internal] Update Contact Tag -- @param #AWACS self -- @param #number CID Contact ID -- @param #string Text Text to be used -- @param #boolean TAC TAC Call done -- @param #boolean MELD MELD Call done -- @param #string TaskStatus Overwrite status with #AWACS.TaskStatus Status -- @return #AWACS self function AWACS:_UpdateContactEngagementTag(CID,Text,TAC,MELD,TaskStatus) self:T(self.lid.."_UpdateContactEngagementTag") local text = Text or "" -- get contact local contact = self.Contacts:PullByID(CID) -- #AWACS.ManagedContact if contact then contact.EngagementTag = text contact.TACCallDone = TAC or false contact.MeldCallDone = MELD or false contact.Status = TaskStatus or AWACS.TaskStatus.UNASSIGNED self.Contacts:Push(contact,CID) end return self end --- [Internal] Check available tasks and status -- @param #AWACS self -- @return #AWACS self function AWACS:_CheckTaskQueue() self:T(self.lid.."_CheckTaskQueue") local opentasks = 0 local assignedtasks = 0 -- update last known positions for _id,_managedgroup in pairs(self.ManagedGrps) do local group = _managedgroup -- #AWACS.ManagedGroup if group.Group and group.Group:IsAlive() then local coordinate = group.Group:GetCoordinate() if coordinate then local NewCoordinate = COORDINATE:New(0,0,0) group.LastKnownPosition = group.LastKnownPosition:UpdateFromCoordinate(coordinate) self.ManagedGrps[_id] = group end end end ---------------------------------------- -- ANCHOR ---------------------------------------- if self.ManagedTasks:IsNotEmpty() then opentasks = self.ManagedTasks:GetSize() self:T("Assigned Tasks: " .. opentasks) local taskstack = self.ManagedTasks: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 if description == AWACS.TaskDescription.ANCHOR or description == AWACS.TaskDescription.REANCHOR then self:T("Open Task 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 if group and group:IsAlive() then local groupcoord = group:GetCoordinate() local zone = target:GetObject() -- Core.Zone#ZONE self:T({zone}) if group:IsInZone(zone) then self:T("Open Task ANCHOR/REANCHOR success for GroupID "..entry.AssignedGroupID) -- made it target:Stop() -- 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 -- end isAI managedgroup.HasAssignedTask = false self.ManagedGrps[entry.AssignedGroupID] = managedgroup -- pull task from OpenTasks self.ManagedTasks:PullByID(entry.TID) else --inzone -- not there yet self:T("Open Task ANCHOR/REANCHOR executing for GroupID "..entry.AssignedGroupID) end else -- group dead, pull task self.ManagedTasks:PullByID(entry.TID) end end ---------------------------------------- -- INTERCEPT ---------------------------------------- elseif description == AWACS.TaskDescription.INTERCEPT then -- DONE self:T("Open Tasks INTERCEPT") local taskstatus = entry.Status local targetstatus = entry.Target:GetState() if taskstatus == AWACS.TaskStatus.UNASSIGNED then -- thou shallst not be in this list! self.ManagedTasks:PullByID(entry.TID) break end local managedgroup = self.ManagedGrps[entry.AssignedGroupID] -- #AWACS.ManagedGroup -- Check ranges for TAC and MELD -- postions relative to CAP position local targetgrp = entry.Contact.group local position = entry.Contact.position or entry.Cluster.coordinate if targetgrp and targetgrp:IsAlive() and managedgroup then --position = targetgrp:GetCoordinate() if position and managedgroup.Group and managedgroup.Group:IsAlive() then local grouposition = managedgroup.Group:GetCoordinate() or managedgroup.Group:GetCoordinate() local distance = 1000 if grouposition then distance = grouposition:Get2DDistance(position) distance = UTILS.Round(UTILS.MetersToNM(distance),0) end self:T("TAC/MELD distance check: "..distance.."NM!") if distance <= self.TacDistance and distance >= self.MeldDistance then -- TAC distance self:T("TAC distance: "..distance.."NM!") local Contact = self.Contacts:ReadByID(entry.Contact.CID) self:_TACRangeCall(entry.AssignedGroupID,Contact) elseif distance <= self.MeldDistance and distance >= self.ThreatDistance then -- MELD distance self:T("MELD distance: "..distance.."NM!") local Contact = self.Contacts:ReadByID(entry.Contact.CID) self:_MeldRangeCall(entry.AssignedGroupID,Contact) end end end local auftrag = entry.Auftrag -- Ops.Auftrag#AUFTRAG local auftragstatus = "Not Known" if auftrag then auftragstatus = auftrag:GetState() end local text = string.format("ID=%d | Status=%s | TargetState=%s | AuftragState=%s",entry.TID,taskstatus,targetstatus,auftragstatus) self:T(text) if auftrag then if auftrag:IsExecuting() then entry.Status = AWACS.TaskStatus.EXECUTING elseif auftrag:IsSuccess() then entry.Status = AWACS.TaskStatus.SUCCESS elseif auftrag:GetState() == AUFTRAG.Status.FAILED then entry.Status = AWACS.TaskStatus.FAILED end if targetstatus == "Dead" then entry.Status = AWACS.TaskStatus.SUCCESS elseif targetstatus == "Alive" and auftrag:IsOver() then entry.Status = AWACS.TaskStatus.FAILED end elseif entry.IsPlayerTask then -- Player task -- TODO if entry.Target:IsDead() or entry.Target:IsDestroyed() then -- success! entry.Status = AWACS.TaskStatus.SUCCESS elseif entry.Target:IsAlive() then -- still alive -- out of zones? local targetpos = entry.Target:GetCoordinate() -- success == out of our controlled zones local outofzones = false self.RejectZoneSet:ForEachZone( function(Zone,Position) local zone = Zone -- Core.Zone#ZONE local pos = Position -- Core.Point#VEC2 if pos and zone:IsVec2InZone(pos) then -- crossed the border outofzones = true end end, targetpos:GetVec2() ) if not outofzones then outofzones = true self.ZoneSet:ForEachZone( function(Zone,Position) local zone = Zone -- Core.Zone#ZONE local pos = Position -- Core.Point#VEC2 if pos and zone:IsVec2InZone(pos) then -- in any zone outofzones = false end end, targetpos:GetVec2() ) end if outofzones then entry.Status = AWACS.TaskStatus.SUCCESS end end end if entry.Status == AWACS.TaskStatus.SUCCESS then self:T("Open Tasks INTERCEPT success for GroupID "..entry.AssignedGroupID) if managedgroup then self:_UpdateContactEngagementTag(managedgroup.ContactCID,"",true,true,AWACS.TaskStatus.SUCCESS) managedgroup.HasAssignedTask = false managedgroup.ContactCID = 0 managedgroup.LastTasking = timer.getTime() if managedgroup.IsAI then managedgroup.CurrentAuftrag = 0 else managedgroup.CurrentTask = 0 end self.ManagedGrps[entry.AssignedGroupID] = managedgroup self.ManagedTasks:PullByID(entry.TID) self:__InterceptSuccess(1) self:__ReAnchor(5,managedgroup.GID) end elseif entry.Status == AWACS.TaskStatus.FAILED then self:T("Open Tasks INTERCEPT failed for GroupID "..entry.AssignedGroupID) if managedgroup then managedgroup.HasAssignedTask = false self:_UpdateContactEngagementTag(managedgroup.ContactCID,"",false,false,AWACS.TaskStatus.UNASSIGNED) managedgroup.ContactCID = 0 managedgroup.LastTasking = timer.getTime() if managedgroup.IsAI then managedgroup.CurrentAuftrag = 0 else managedgroup.CurrentTask = 0 end if managedgroup.IsPlayer then entry.IsPlayerTask = false end self.ManagedGrps[entry.AssignedGroupID] = managedgroup if managedgroup.Group:IsAlive() or managedgroup.FlightGroup:IsAlive() then self:__ReAnchor(5,managedgroup.GID) end end -- remove self.ManagedTasks:PullByID(entry.TID) self:__InterceptFailure(1) elseif entry.Status == AWACS.TaskStatus.REQUESTED then -- requested - player tasks only! self:T("Open Tasks INTERCEPT REQUESTED for GroupID "..entry.AssignedGroupID) local created = entry.RequestedTimestamp or timer.getTime() - 120 local Tnow = timer.getTime() local Trunning = (Tnow-created) / 60 -- mins local text = string.format("Task TID %s Requested %d minutes ago.",entry.TID,Trunning) if Trunning > self.ReassignmentPause then -- reassign if player didn't react within 3 mins entry.Status = AWACS.TaskStatus.UNASSIGNED self.ManagedTasks:PullByID(entry.TID) end self:T(text) end ---------------------------------------- -- VID/POLICE ---------------------------------------- elseif description == AWACS.TaskDescription.VID then -- TODO - how to do this with AI? -- humans only ATM local managedgroup = self.ManagedGrps[entry.AssignedGroupID] -- #AWACS.ManagedGroup -- check we're alive if (not managedgroup) or (not managedgroup.Group:IsAlive()) then self.ManagedTasks:PullByID(entry.TID) --entry.Status = AWACS.TaskStatus.FAILED return self end -- target dead or out of bounds? if entry.Target:IsDead() or entry.Target:IsDestroyed() then -- success! entry.Status = AWACS.TaskStatus.SUCCESS elseif entry.Target:IsAlive() then -- still alive -- out of zones? self:T("Checking VID target out of bounds") local targetpos = entry.Target:GetCoordinate() -- success == out of our controlled zones local outofzones = false self.RejectZoneSet:ForEachZone( function(Zone,Position) local zone = Zone -- Core.Zone#ZONE local pos = Position -- Core.Point#VEC2 if pos and zone:IsVec2InZone(pos) then -- crossed the border outofzones = true end end, targetpos:GetVec2() ) if not outofzones then outofzones = true self.ZoneSet:ForEachZone( function(Zone,Position) local zone = Zone -- Core.Zone#ZONE local pos = Position -- Core.Point#VEC2 if pos and zone:IsVec2InZone(pos) then -- in any zone outofzones = false end end, targetpos:GetVec2() ) end if outofzones then entry.Status = AWACS.TaskStatus.SUCCESS self:T("Out of bounds - SUCCESS") end end if entry.Status == AWACS.TaskStatus.REQUESTED then -- requested - player tasks only! self:T("Open Tasks VID REQUESTED for GroupID "..entry.AssignedGroupID) local created = entry.RequestedTimestamp or timer.getTime() - 120 local Tnow = timer.getTime() local Trunning = (Tnow-created) / 60 -- mins local text = string.format("Task TID %s Requested %d minutes ago.",entry.TID,Trunning) if Trunning > self.ReassignmentPause then -- reassign if player didn't react within 3 mins entry.Status = AWACS.TaskStatus.UNASSIGNED self.ManagedTasks:PullByID(entry.TID) end self:T(text) elseif entry.Status == AWACS.TaskStatus.ASSIGNED then self:T("Open Tasks VID ASSIGNED for GroupID "..entry.AssignedGroupID) -- check TAC/MELD ranges local targetgrp = entry.Contact.group local position = entry.Contact.position or entry.Cluster.coordinate if targetgrp and targetgrp:IsAlive() and managedgroup then --position = targetgrp:GetCoordinate() if position and managedgroup.Group and managedgroup.Group:IsAlive() then local grouposition = managedgroup.Group:GetCoordinate() or managedgroup.Group:GetCoordinate() local distance = 1000 if grouposition then distance = grouposition:Get2DDistance(position) distance = UTILS.Round(UTILS.MetersToNM(distance),0) end self:T("TAC/MELD distance check: "..distance.."NM!") if distance <= self.TacDistance and distance >= self.MeldDistance then -- TAC distance self:T("TAC distance: "..distance.."NM!") local Contact = self.Contacts:ReadByID(entry.Contact.CID) self:_TACRangeCall(entry.AssignedGroupID,Contact) elseif distance <= self.MeldDistance and distance >= self.ThreatDistance then -- MELD distance self:T("MELD distance: "..distance.."NM!") local Contact = self.Contacts:ReadByID(entry.Contact.CID) self:_MeldRangeCall(entry.AssignedGroupID,Contact) end end end elseif entry.Status == AWACS.TaskStatus.SUCCESS then self:T("Open Tasks VID success for GroupID "..entry.AssignedGroupID) -- outcomes - player ID'd -- target dead or left zones handled above -- target ID'd --> if hostile, assign INTERCEPT TASK self.ManagedTasks:PullByID(entry.TID) local Contact = self.Contacts:ReadByID(entry.Contact.CID) -- #AWACS.ManagedContact if Contact and (Contact.IFF == AWACS.IFF.FRIENDLY or Contact.IFF == AWACS.IFF.NEUTRAL) then self:T("IFF outcome friendly/neutral for GroupID "..entry.AssignedGroupID) -- nothing todo, re-anchor if managedgroup then managedgroup.HasAssignedTask = false self:_UpdateContactEngagementTag(managedgroup.ContactCID,"",false,false,AWACS.TaskStatus.UNASSIGNED) managedgroup.ContactCID = 0 managedgroup.LastTasking = timer.getTime() if managedgroup.IsAI then managedgroup.CurrentAuftrag = 0 else managedgroup.CurrentTask = 0 end if managedgroup.IsPlayer then entry.IsPlayerTask = false end --self.ManagedTasks:PullByID(entry.TID) self.ManagedGrps[entry.AssignedGroupID] = managedgroup self:__ReAnchor(5,managedgroup.GID) end elseif Contact and Contact.IFF == AWACS.IFF.ENEMY then self:T("IFF outcome hostile for GroupID "..entry.AssignedGroupID) -- change to intercept --self.ManagedTasks:PullByID(entry.TID) entry.ToDo = AWACS.TaskDescription.INTERCEPT entry.Status = AWACS.TaskStatus.ASSIGNED local cname = Contact.TargetGroupNaming entry.ScreenText = string.format("Engage hostile %s group.",cname) self.ManagedTasks:Push(entry,entry.TID) local TextTTS = string.format("%s, %s. Engage hostile target!",managedgroup.CallSign,self.callsigntxt) self:_NewRadioEntry(TextTTS,TextTTS,managedgroup.GID,true,self.debug,true,false,true) elseif not Contact then self:T("IFF outcome target DEAD for GroupID "..entry.AssignedGroupID) -- nothing todo, re-anchor if managedgroup then managedgroup.HasAssignedTask = false self:_UpdateContactEngagementTag(managedgroup.ContactCID,"",false,false,AWACS.TaskStatus.UNASSIGNED) managedgroup.ContactCID = 0 managedgroup.LastTasking = timer.getTime() if managedgroup.IsAI then managedgroup.CurrentAuftrag = 0 else managedgroup.CurrentTask = 0 end if managedgroup.IsPlayer then entry.IsPlayerTask = false end --self.ManagedTasks:PullByID(entry.TID) self.ManagedGrps[entry.AssignedGroupID] = managedgroup if managedgroup.Group:IsAlive() or managedgroup.FlightGroup:IsAlive() then self:__ReAnchor(5,managedgroup.GID) end end end elseif entry.Status == AWACS.TaskStatus.FAILED then -- outcomes - player unable/abort -- Player dead managed above -- Remove task self:T("Open Tasks VID failed for GroupID "..entry.AssignedGroupID) if managedgroup then managedgroup.HasAssignedTask = false self:_UpdateContactEngagementTag(managedgroup.ContactCID,"",false,false,AWACS.TaskStatus.UNASSIGNED) managedgroup.ContactCID = 0 managedgroup.LastTasking = timer.getTime() if managedgroup.IsAI then managedgroup.CurrentAuftrag = 0 else managedgroup.CurrentTask = 0 end if managedgroup.IsPlayer then entry.IsPlayerTask = false end self.ManagedGrps[entry.AssignedGroupID] = managedgroup if managedgroup.Group:IsAlive() or managedgroup.FlightGroup:IsAlive() then self:__ReAnchor(5,managedgroup.GID) end end -- remove self.ManagedTasks:PullByID(entry.TID) self:__InterceptFailure(1) end 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") local text = string.gsub(UTILS.OneLineSerialize(self.MonitoringData),",","\n") local text = string.gsub(text,"{","\n") local text = string.gsub(text,"}","") local text = string.gsub(text,"="," = ") self:T(text) if self.MonitoringOn then MESSAGE:New(text,20,"AWACS",false):ToAll() end return self end --- [User] Add another AirWing for AI CAP Flights under management -- @param #AWACS self -- @param Ops.AirWing#AIRWING AirWing The AirWing to (also) obtain CAP flights from -- @param Core.Zone#ZONE_RADIUS Zone (optional) This AirWing has it's own station zone, AI CAP will be send there -- @return #AWACS self function AWACS:AddCAPAirWing(AirWing,Zone) self:T(self.lid.."AddCAPAirWing") if AirWing then AirWing:SetUsingOpsAwacs(self) local distance = self.AOCoordinate:Get2DDistance(AirWing:GetCoordinate()) if Zone then -- create AnchorStack local stackscreated = self.AnchorStacks:GetSize() if stackscreated == self.AnchorMaxAnchors then -- only create self.AnchorMaxAnchors Anchors self:E(self.lid.."Max number of stacks already created!") else local AnchorStackOne = {} -- #AWACS.AnchorData AnchorStackOne.AnchorBaseAngels = self.AnchorBaseAngels AnchorStackOne.Anchors = FIFO:New() -- Utilities.FiFo#FIFO AnchorStackOne.AnchorAssignedID = FIFO:New() -- Utilities.FiFo#FIFO local newname = Zone:GetName() for i=1,self.AnchorMaxStacks do AnchorStackOne.Anchors:Push((i-1)*self.AnchorStackDistance+self.AnchorBaseAngels) end local newsubname = AWACS.AnchorNames[stackscreated+1] or tostring(stackscreated+1) newname = Zone:GetName() .. "-"..newsubname AnchorStackOne.StationZone = Zone AnchorStackOne.StationZoneCoordinate = Zone:GetCoordinate() AnchorStackOne.StationZoneCoordinateText = Zone:GetCoordinate():ToStringLLDDM() AnchorStackOne.StationName = newname --push to AnchorStacks if self.debug then --self.AnchorStacks:Flush() AnchorStackOne.StationZone:DrawZone(-1,{0,0,1},1,{0,0,1},0.2,5,true) local stationtag = string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM()) AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToAll() else local stationtag = string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM()) AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToAll() end self.AnchorStacks:Push(AnchorStackOne,newname) AirWing.HasOwnStation = true AirWing.StationName = newname end end self.CAPAirwings:Push(AirWing,distance) end return self end --- [Internal] Announce a new contact -- @param #AWACS self -- @param #AWACS.ManagedContact Contact -- @param #boolean IsNew Is a new contact -- @param Wrapper.Group#GROUP Group Announce to Group if not nil -- @param #boolean IsBogeyDope If true, this is a bogey dope announcement -- @param #string Tag Tag name for this contact. Alpha, Brave, Charlie ... -- @param #boolean IsPopup This is a pop-up group -- @param #string ReportingName The NATO code reporting name for the contact, e.g. "Foxbat". "Bogey" if unknown. -- @return #AWACS self function AWACS:_AnnounceContact(Contact,IsNew,Group,IsBogeyDope,Tag,IsPopup,ReportingName) self:T(self.lid.."_AnnounceContact") --self:T({Contact}) -- do we have a group to talk to? local tag = "" local Tag = Tag local CID = 0 if not Tag then -- injected data available? CID = Contact.CID or 0 Tag = Contact.TargetGroupNaming or "" --self:T({CID,Tag}) end if self.NoGroupTags then Tag = nil end local isGroup = false local GID = 0 local grpcallsign = "Ghost 1" if Group and Group:IsAlive() then GID, isGroup,grpcallsign = self:_GetManagedGrpID(Group) self:T("GID="..GID.." CheckedIn = "..tostring(isGroup)) --grpcallsign = self:_GetCallSign(Group,GID) or "Ghost 1" end local cluster = Contact.Cluster local intel = self.intel -- Ops.Intelligence#INTEL local size = self.intel:ClusterCountUnits(cluster) local threatsize, threatsizetext = self:_GetBlurredSize(size) local clustercoordinate = Contact.Cluster.coordinate or Contact.Contact.position local heading = Contact.Contact.group:GetHeading() or self.intel:CalcClusterDirection(cluster) clustercoordinate:SetHeading(Contact.Contact.group:GetHeading()) local BRAfromBulls, BRAfromBullsTTS = self:_GetBRAfromBullsOrAO(clustercoordinate) self:T(BRAfromBulls) self:T(BRAfromBullsTTS) BRAfromBulls=BRAfromBulls.."." BRAfromBullsTTS=BRAfromBullsTTS.."." if isGroup then BRAfromBulls = clustercoordinate:ToStringBRAANATO(Group:GetCoordinate(),true,true) BRAfromBullsTTS = string.gsub(BRAfromBulls,"BRAA","brah") BRAfromBullsTTS = string.gsub(BRAfromBullsTTS,"BRA","brah") if self.PathToGoogleKey then BRAfromBullsTTS = clustercoordinate:ToStringBRAANATO(Group:GetCoordinate(),true,true,true,false,true) end end -- "Uzi 1-1, Magic, BRA, 183 for 10 at 2000, hot" -- ", , /, , BRA for at angels , , " local BRAText = "" local TextScreen = "" if isGroup then BRAText = string.format("%s, %s.",grpcallsign,self.callsigntxt) TextScreen = string.format("%s, %s.",grpcallsign,self.callsigntxt) else BRAText = string.format("%s.",self.callsigntxt) TextScreen = string.format("%s.",self.callsigntxt) end if IsNew and self.PlayerGuidance then BRAText = BRAText .. " New group." TextScreen = TextScreen .. " New group." elseif IsPopup then BRAText = BRAText .. " Pop-up group." TextScreen = TextScreen .. " Pop-up group." elseif IsBogeyDope and Tag and Tag ~= "" then BRAText = BRAText .. " "..Tag.." group." TextScreen = TextScreen .. " "..Tag.." group." else BRAText = BRAText .. " Group." TextScreen = TextScreen .. " Group." end if not IsBogeyDope then if Tag and Tag ~= "" then BRAText = BRAText .. " "..Tag.."." TextScreen = TextScreen .. " "..Tag.."." end end if threatsize > 1 then --BRAText = BRAText .. " "..threatsizetext..". "..BRAfromBullsTTS BRAText = BRAText .. " "..BRAfromBullsTTS.." "..threatsizetext.."." --TextScreen = TextScreen .. " "..threatsizetext..". "..BRAfromBulls TextScreen = TextScreen .. " "..BRAfromBulls.." "..threatsizetext.."." else --BRAText = BRAText .. " "..threatsizetext..". "..BRAfromBullsTTS BRAText = BRAText .. " "..BRAfromBullsTTS --TextScreen = TextScreen .. " "..threatsizetext..". "..BRAfromBulls TextScreen = TextScreen .. " "..BRAfromBulls end if self.ModernEra then -- Platform if ReportingName and ReportingName ~= "Bogey" then ReportingName = string.gsub(ReportingName,"_"," ") BRAText = BRAText .. " "..ReportingName.."." TextScreen = TextScreen .. " "..ReportingName.."." end -- High - > 40k feet local height = Contact.Contact.group:GetHeight() local height = UTILS.Round(UTILS.MetersToFeet(height)/1000,0) -- e.g, 25 if height >= 40 then BRAText = BRAText .. " High." TextScreen = TextScreen .. " High." end -- Fast (>600kn) or Very fast (>900kn) local speed = Contact.Contact.group:GetVelocityKNOTS() if speed > 900 then BRAText = BRAText .. " Very Fast." TextScreen = TextScreen .. " Very Fast." elseif speed >= 600 and speed <= 900 then BRAText = BRAText .. " Fast." TextScreen = TextScreen .. " Fast." end end string.gsub(BRAText,"BRAA","brah") string.gsub(BRAText,"BRA","brah") --self:T(BRAText) local prio = IsNew or IsBogeyDope self:_NewRadioEntry(BRAText,TextScreen,GID,isGroup,true,IsNew,false,prio) 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:IsAlive() then handback = OG --self:T("Handing back OG: " .. OG:GetName()) break end end return handback end --- [Internal] Clean up mission stack -- @param #AWACS self -- @return #number CAPMissions -- @return #number Alert5Missions -- @return #number InterceptMissions function AWACS:_CleanUpAIMissionStack() self:T(self.lid.."_CleanUpAIMissionStack") local CAPMissions = 0 local Alert5Missions = 0 local InterceptMissions = 0 local MissionStack = FIFO:New() self:T("Checking MissionStack") for _,_mission in pairs(self.CatchAllMissions) do -- looking for missions of type CAP and ALERT5 local mission = _mission -- Ops.Auftrag#AUFTRAG local type = mission:GetType() if type == AUFTRAG.Type.ALERT5 then MissionStack:Push(mission,mission.auftragsnummer) Alert5Missions = Alert5Missions + 1 elseif type == AUFTRAG.Type.CAP then MissionStack:Push(mission,mission.auftragsnummer) CAPMissions = CAPMissions + 1 elseif type == AUFTRAG.Type.INTERCEPT then MissionStack:Push(mission,mission.auftragsnummer) InterceptMissions = InterceptMissions + 1 end end self.AICAPMissions = nil self.AICAPMissions = MissionStack return CAPMissions, Alert5Missions, InterceptMissions end function AWACS:_ConsistencyCheck() self:T(self.lid.."_ConsistencyCheck") if self.debug then self:T("CatchAllMissions") local catchallm = {} local report1 = REPORT:New("CatchAll") report1:Add("====================") report1:Add("CatchAllMissions") report1:Add("====================") for _,_mission in pairs(self.CatchAllMissions) do local mission = _mission -- Ops.Auftrag#AUFTRAG local nummer = mission.auftragsnummer or 0 local type = mission:GetType() local state = mission:GetState() local FG = mission:GetOpsGroups() local OG = self:_GetAliveOpsGroupFromTable(FG) local OGName = "UnknownFromMission" if OG then OGName=OG:GetName() end report1:Add(string.format("Auftrag Nr %d Type %s State %s FlightGroup %s",nummer,type,state,OGName)) if mission:IsNotOver() then catchallm[#catchallm+1] = mission end end self.CatchAllMissions = nil self.CatchAllMissions = catchallm local catchallfg = {} self:T("CatchAllFGs") report1:Add("====================") report1:Add("CatchAllFGs") report1:Add("====================") for _,_fg in pairs(self.CatchAllFGs) do local FG = _fg -- Ops.FlightGroup#FLIGHTGROUP local mission = FG:GetMissionCurrent() local OGName = FG:GetName() or "UnknownFromFG" local nummer = 0 local type = "No Type" local state = "None" if mission then type = mission:GetType() nummer = mission.auftragsnummer or 0 state = mission:GetState() end report1:Add(string.format("Auftrag Nr %d Type %s State %s FlightGroup %s",nummer,type,state,OGName)) if FG:IsAlive() then catchallfg[#catchallfg+1] = FG end end report1:Add("====================") self:T(report1:Text()) self.CatchAllFGs = nil self.CatchAllFGs = catchallfg end return self end --- [Internal] Check Enough AI CAP on Station -- @param #AWACS self -- @return #AWACS self function AWACS:_CheckAICAPOnStation() self:T(self.lid.."_CheckAICAPOnStation") self:_ConsistencyCheck() local capmissions, alert5missions, interceptmissions = self:_CleanUpAIMissionStack() self:T({capmissions, alert5missions, interceptmissions}) if self.MaxAIonCAP > 0 then --local onstation = self.AICAPMissions:Count() local onstation = capmissions + alert5missions -- control number of AI CAP Flights if self.AIRequested < self.MaxAIonCAP then -- not enough local AnchorStackNo,free = self:_GetFreeAnchorStack() if free then -- create Alert5 and assign to ONE of our AWs -- TODO better selection due to resource shortage? local mission = AUFTRAG:NewALERT5(AUFTRAG.Type.CAP) self.CatchAllMissions[#self.CatchAllMissions+1] = mission local availableAWS = self.CAPAirwings:Count() local AWS = self.CAPAirwings:GetDataTable() -- round robin self.AIRequested = self.AIRequested + 1 --print(((i-1) % divideby)+1) local selectedAW = AWS[(((self.AIRequested-1) % availableAWS)+1)] selectedAW:AddMission(mission) self:T("CAP="..capmissions.." ALERT5="..alert5missions.." Requested="..self.AIRequested) end end if self.AIRequested > self.MaxAIonCAP then -- too many, send one home self:T(string.format("*** Onstation %d > MaxAIOnCAP %d",onstation,self.MaxAIonCAP)) local mission = self.AICAPMissions:Pull() -- Ops.Auftrag#AUFTRAG local Groups = mission:GetOpsGroups() local OpsGroup = self:_GetAliveOpsGroupFromTable(Groups) local GID,checkedin = self:_GetManagedGrpID(OpsGroup) mission:__Cancel(30) self.AIRequested = self.AIRequested - 1 if checkedin then self:_CheckOut(OpsGroup,GID) end end -- Check CAP Mission states if onstation > 0 then local missions = self.AICAPMissions:GetDataTable() -- get mission type and state for _,_Mission in pairs(missions) do --local mission = self.AICAPMissions:ReadByID(_MissionID) -- Ops.Auftrag#AUFTRAG local mission = _Mission -- Ops.Auftrag#AUFTRAG self:T("Looking at AuftragsNr " .. mission.auftragsnummer) local type = mission:GetType() local state = mission:GetState() --if type == AUFTRAG.Type.CAP or type == AUFTRAG.Type.ALERT5 or type == AUFTRAG.Type.ORBIT then if type == AUFTRAG.Type.ALERT5 then -- parked up for CAP local OpsGroups = mission:GetOpsGroups() local OpsGroup = self:_GetAliveOpsGroupFromTable(OpsGroups) local FGstate = mission:GetGroupStatus(OpsGroup) if OpsGroup then FGstate = OpsGroup:GetState() self:T("FG Object in state: " .. FGstate) end -- FG ready? -- if OpsGroup and (state == AUFTRAG.Status.STARTED or FGstate == AUFTRAG.Status.EXECUTING or FGstate == AUFTRAG.Status.SCHEDULED) then if OpsGroup and (FGstate == "Parking" or FGstate == "Cruising") 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 missions = self.AICAPMissions:GetDataTable() local i = 1 for _,_Mission in pairs(missions) 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 local mission = _Mission -- 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" local found,GID,OpsCallSign = self:_GetGIDFromGroupOrName(OpsGroup) 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.Aircraft.Colt 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:SetDefaultCallsign(callname,math.fmod(self.AICAPCAllNumber,9)) if ROE == AWACS.ROE.POLICE or ROE == AWACS.ROE.VID then FlightGroup:SetDefaultROE(ENUMS.ROE.WeaponHold) elseif ROE == AWACS.ROE.IFF then FlightGroup:SetDefaultROE(ENUMS.ROE.ReturnFire) elseif ROE == AWACS.ROE.BVR then FlightGroup:SetDefaultROE(ENUMS.ROE.OpenFire) end if ROT == AWACS.ROT.BYPASSESCAPE or ROT == AWACS.ROT.PASSIVE then FlightGroup:SetDefaultROT(ENUMS.ROT.PassiveDefense) elseif ROT == AWACS.ROT.OPENFIRE or ROT == AWACS.ROT.RETURNFIRE then FlightGroup:SetDefaultROT(ENUMS.ROT.BypassAndEscape) elseif ROT == AWACS.ROT.EVADE then FlightGroup:SetDefaultROT(ENUMS.ROT.EvadeFire) end FlightGroup:SetFuelLowRTB(true) FlightGroup:SetFuelLowThreshold(0.2) FlightGroup:SetEngageDetectedOff() FlightGroup:SetOutOfAAMRTB(true) return self end --- [Internal] TAC Range Call to Pilot -- @param #AWACS self -- @param #number GID GID -- @param #AWACS.ManagedContact Contact -- @return #AWACS self function AWACS:_TACRangeCall(GID,Contact) self:T(self.lid.."_TACRangeCall") -- AIC: “Enforcer 11, single group, 30 miles.” if not Contact then return self end local pilotcallsign = self:_GetCallSign(nil,GID) local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup local contact = Contact.Contact -- Ops.Intelligence#INTEL.Contact local contacttag = Contact.TargetGroupNaming if contact and not Contact.TACCallDone then local position = contact.position -- Core.Point#COORDINATE if position then local distance = position:Get2DDistance(managedgroup.Group:GetCoordinate()) distance = UTILS.Round(UTILS.MetersToNM(distance)) -- 30nm - hopefully local text = string.format("%s. %s. %s group, %d miles.",self.callsigntxt,pilotcallsign,contacttag,distance) self:_NewRadioEntry(text,text,GID,true,self.debug,true,false,true) self:_UpdateContactEngagementTag(Contact.CID,Contact.EngagementTag,true,false,AWACS.TaskStatus.EXECUTING) end end return self end --- [Internal] Meld Range Call to Pilot -- @param #AWACS self -- @param #number GID GID -- @param #AWACS.ManagedContact Contact -- @return #AWACS self function AWACS:_MeldRangeCall(GID,Contact) self:T(self.lid.."_MeldRangeCall") if not Contact then return self end -- AIC: “Heat 11, single group, BRAA 089/28, 32 thousand, hot, hostile, crow.” local pilotcallsign = self:_GetCallSign(nil,GID) local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup local flightpos = managedgroup.Group:GetCoordinate() local contact = Contact.Contact -- Ops.Intelligence#INTEL.Contact local contacttag = Contact.TargetGroupNaming if contact and not Contact.MeldCallDone then local position = contact.position -- Core.Point#COORDINATE if position then local BRATExt = "" if self.PathToGoogleKey then BRATExt = position:ToStringBRAANATO(flightpos,false,false,true,false,true) else BRATExt = position:ToStringBRAANATO(flightpos,false,false) end local text = string.format("%s. %s. %s group, %s",self.callsigntxt,pilotcallsign,contacttag,BRATExt) self:_NewRadioEntry(text,text,GID,true,self.debug,true,false,true) self:_UpdateContactEngagementTag(Contact.CID,Contact.EngagementTag,true,true,AWACS.TaskStatus.EXECUTING) end end return self end --- [Internal] Threat Range Call to Pilot -- @param #AWACS self -- @return #AWACS self function AWACS:_ThreatRangeCall(GID,Contact) self:T(self.lid.."_ThreatRangeCall") if not Contact then return self end -- AIC: “Enforcer 11 12, east group, THREAT, BRAA 260/15, 29 thousand, hot, hostile, robin.” local pilotcallsign = self:_GetCallSign(nil,GID) local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup local flightpos = managedgroup.Group:GetCoordinate() or managedgroup.LastKnownPosition local contact = Contact.Contact -- Ops.Intelligence#INTEL.Contact local contacttag = Contact.TargetGroupNaming if contact then local position = contact.position or contact.group:GetCoordinate() -- Core.Point#COORDINATE if position then local BRATExt = "" if self.PathToGoogleKey then BRATExt = position:ToStringBRAANATO(flightpos,false,false,true,false,true) else BRATExt = position:ToStringBRAANATO(flightpos,false,false) end local text = string.format("%s. %s. %s group, Threat. %s",self.callsigntxt,pilotcallsign,contacttag,BRATExt) self:_NewRadioEntry(text,text,GID,true,self.debug,true,false,true) end end return self end --- [Internal] Merged Call to Pilot -- @param #AWACS self -- @param #number GID -- @return #AWACS self function AWACS:_MergedCall(GID) self:T(self.lid.."_MergedCall") -- AIC: “Enforcer, mergedb” local pilotcallsign = self:_GetCallSign(nil,GID) --local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup local text = string.format("%s. %s. Merged.",self.callsigntxt,pilotcallsign) self:_NewRadioEntry(text,text,GID,true,self.debug,true,false,true) return self end --- [Internal] Assign a Pilot to a target -- @param #AWACS self -- @param #table Pilots Table of #AWACS.ManagedGroup Pilot -- @param Utilities.FiFo#FIFO Targets FiFo of #AWACS.ManagedContact Targets -- @return #AWACS self function AWACS:_AssignPilotToTarget(Pilots,Targets) self:T(self.lid.."_AssignPilotToTarget") local inreach = false local Pilot = nil -- #AWACS.ManagedGroup local closest = UTILS.NMToMeters(self.maxassigndistance+1) local targets = Targets:GetDataTable() local Target = nil for _,_target in pairs(targets) do -- Check Distance local targetgroupcoord = _target.Contact.position -- get closest pilot from target for _,_Pilot in pairs(Pilots) do local pilotcoord = _Pilot.Group:GetCoordinate() local targetdist = targetgroupcoord:Get2DDistance(pilotcoord) if UTILS.MetersToNM(targetdist) < self.maxassigndistance and targetdist < closest then self:T(string.format("%sTarget distance %d! Assignment %s!",self.lid,UTILS.Round(UTILS.MetersToNM(targetdist),0),_Pilot.CallSign)) inreach = true closest = targetdist Pilot = _Pilot Target = _target Targets:PullByID(_target.CID) break else self:T(self.lid .. "Target distance > "..self.maxassigndistance.."NM! No Assignment!") end end end -- DONE Check Human assignment working if inreach and Pilot and Pilot.IsPlayer then local callsign = Pilot.CallSign -- update pilot TaskSheet self.ManagedTasks:PullByID(Pilot.CurrentTask) Pilot.HasAssignedTask = true local TargetPosition = Target.Target:GetCoordinate() local PlayerPositon = Pilot.LastKnownPosition local TargetAlt = Target.Contact.altitude or Target.Cluster.altitude or Target.Contact.group:GetAltitude() local TargetDirections, TargetDirectionsTTS = self:_ToStringBRA(PlayerPositon,TargetPosition,TargetAlt) local ScreenText = "" local TaskType = AWACS.TaskDescription.INTERCEPT if self.AwacsROE == AWACS.ROE.POLICE or self.AwacsROE == AWACS.ROE.VID then ScreenText = string.format("Intercept and VID %s group.",Target.TargetGroupNaming) TaskType = AWACS.TaskDescription.VID else ScreenText = string.format("Intercept %s group.",Target.TargetGroupNaming) end Pilot.CurrentTask = self:_CreateTaskForGroup(Pilot.GID,TaskType,ScreenText,Target.Target,AWACS.TaskStatus.REQUESTED,nil,Target.Cluster,Target.Contact) Pilot.ContactCID = Target.CID -- update managed group self.ManagedGrps[Pilot.GID] = Pilot -- Update Contact Status Target.LinkedTask = Pilot.CurrentTask Target.LinkedGroup = Pilot.GID Target.Status = AWACS.TaskStatus.REQUESTED Target.EngagementTag = string.format("Targeted by %s.",Pilot.CallSign) self.Contacts:PullByID(Target.CID) self.Contacts:Push(Target,Target.CID) local text = string.format("%s. %s group. %s. %s, request commit.", self.callsigntxt,Target.TargetGroupNaming,TargetDirectionsTTS,Pilot.CallSign) local textScreen = string.format("%s. %s group. %s. %s, request commit.", self.callsigntxt,Target.TargetGroupNaming,TargetDirections,Pilot.CallSign) self:_NewRadioEntry(text,textScreen,Pilot.GID,true,self.debug,true,false,true) elseif inreach and Pilot and Pilot.IsAI then -- Target information local callsign = Pilot.CallSign local FGStatus = Pilot.FlightGroup:GetState() self:T("Pilot AI Callsign: " .. callsign) self:T("Pilot FG State: " .. FGStatus) local targetstatus = Target.Target:GetState() self:T("Target State: " .. targetstatus) -- local currmission = Pilot.FlightGroup:GetMissionCurrent() if currmission then self:T("Current Mission: " .. currmission:GetType()) end -- create one intercept Auftrag and one to return to CAP post this one local ZoneSet = self.ZoneSet local RejectZoneSet = self.RejectZoneSet local intercept = AUFTRAG:NewINTERCEPT(Target.Target) intercept:SetWeaponExpend(AI.Task.WeaponExpend.ALL) intercept:SetWeaponType(ENUMS.WeaponFlag.Auto) -- TODO -- now this is going to be interesting... -- Check if the target left the "hot" area or is dead already intercept:AddConditionSuccess( function(target,zoneset,rzoneset) -- BASE:I("AUFTRAG Condition Succes Eval Running") local success = true local target = target -- Ops.Target#TARGET if target:IsDestroyed() then return true end local tgtcoord = target:GetCoordinate() local tgtvec2 = nil if tgtcoord then tgtvec2 = tgtcoord:GetVec2() end local zones = zoneset -- Core.Set#SET_ZONE local rzones = rzoneset -- Core.Set#SET_ZONE if tgtvec2 then zones:ForEachZone( function(zone) -- BASE:I("AUFTRAG Condition Succes ZONE Eval Running") if zone:IsVec2InZone(tgtvec2) then success = false end end ) rzones:ForEachZone( function(zone) -- BASE:I("AUFTRAG Condition Succes REJECT ZONE Eval Running") if zone:IsVec2InZone(tgtvec2) then success = true end end ) end return success end, Target.Target, ZoneSet, RejectZoneSet ) Pilot.FlightGroup:AddMission(intercept) local Angels = Pilot.AnchorStackAngels Angels = Angels * 1000 local AnchorSpeed = self.CapSpeedBase or 270 AnchorSpeed = UTILS.KnotsToAltKIAS(AnchorSpeed,Angels) local Anchor = self.AnchorStacks:ReadByPointer(Pilot.AnchorStackNo) -- #AWACS.AnchorData local capauftrag = AUFTRAG:NewCAP(Anchor.StationZone,Angels,AnchorSpeed,Anchor.StationZoneCoordinate,0,15,{}) capauftrag:SetTime(nil,((self.CAPTimeOnStation*3600)+(15*60))) Pilot.FlightGroup:AddMission(capauftrag) -- cancel current mission if currmission then currmission:__Cancel(3) end -- update known mission list self.CatchAllMissions[#self.CatchAllMissions+1] = intercept self.CatchAllMissions[#self.CatchAllMissions+1] = capauftrag -- update pilot TaskSheet self.ManagedTasks:PullByID(Pilot.CurrentTask) Pilot.HasAssignedTask = true Pilot.CurrentTask = self:_CreateTaskForGroup(Pilot.GID,AWACS.TaskDescription.INTERCEPT,"Intercept Task",Target.Target,AWACS.TaskStatus.ASSIGNED,intercept,Target.Cluster,Target.Contact) Pilot.CurrentAuftrag = intercept.auftragsnummer Pilot.ContactCID = Target.CID -- update managed group self.ManagedGrps[Pilot.GID] = Pilot -- Update Contact Status Target.LinkedTask = Pilot.CurrentTask Target.LinkedGroup = Pilot.GID Target.Status = AWACS.TaskStatus.ASSIGNED Target.EngagementTag = string.format("Targeted by %s.",Pilot.CallSign) self.Contacts:PullByID(Target.CID) self.Contacts:Push(Target,Target.CID) -- message commit and return commit from AI --local bratext = Target.Contact.position:ToStringBRA(Pilot.Group:GetCoordinate()) local altitude = Target.Contact.altitude or Target.Contact.group:GetAltitude() local position = Target.Cluster.coordinate or Target.Contact.position if not position then self.intel:GetClusterCoordinate(Target.Cluster,true) end local bratext, bratexttts = self:_ToStringBRA(Pilot.Group:GetCoordinate(),position,altitude or 8000) local text = string.format("%s. %s group. %s. %s, commit.", self.callsigntxt,Target.TargetGroupNaming,bratexttts,Pilot.CallSign) local textScreen = string.format("%s. %s group. %s. %s, request commit.", self.callsigntxt,Target.TargetGroupNaming,bratext,Pilot.CallSign) self:_NewRadioEntry(text,textScreen,Pilot.GID,true,self.debug,true,false,true) local text = string.format("%s. Commit.",Pilot.CallSign) self:_NewRadioEntry(text,text,Pilot.GID,true,self.debug,true,true,true) self:__Intercept(2) end 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:T({From, Event, To}) -- Set up control zone local controlzonename = "FEZ-"..self.AOName self.ControlZone = ZONE_RADIUS:New(controlzonename,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(),"Radar Zone"):ToAll() self.OpsZone:DrawZone(-1,{1,0,0},1,{1,0,0},0.2,5,true) local AOCoordString = self.AOCoordinate:ToStringLLDDM() local Rocktag = string.format("FEZ: %s\nBulls Coordinate: %s",self.AOName,AOCoordString) MARKER:New(self.AOCoordinate,Rocktag):ToAll() self.StationZone:DrawZone(-1,{0,0,1},1,{0,0,1},0.2,5,true) local stationtag = string.format("Station: %s\nCoordinate: %s",self.StationZoneName,self.StationZone:GetCoordinate():ToStringLLDDM()) MARKER:New(self.StationZone:GetCoordinate(),stationtag):ToAll() self.OrbitZone:DrawZone(-1,{0,1,0},1,{0,1,0},0.2,5,true) MARKER:New(self.OrbitZone:GetCoordinate(),"AIC Orbit Zone"):ToAll() else local AOCoordString = self.AOCoordinate:ToStringLLDDM() local Rocktag = string.format("FEZ: %s\nBulls Coordinate: %s",self.AOName,AOCoordString) MARKER:New(self.AOCoordinate,Rocktag):ToAll() MARKER:New(self.OrbitZone:GetCoordinate(),"AIC Orbit Zone"):ToAll() local stationtag = string.format("Station: %s\nCoordinate: %s",self.StationZoneName,self.StationZone:GetCoordinate():ToStringLLDDM()) MARKER:New(self.StationZone:GetCoordinate(),stationtag):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) self.CatchAllMissions[#self.CatchAllMissions+1] = mission AwacsAW:AddMission(mission) self.AwacsMission = mission self.AwacsInZone = false -- not yet arrived or gone again self.AwacsReady = false local ZoneSet = SET_ZONE:New() ZoneSet:AddZone(self.ControlZone) ZoneSet:AddZone(self.OrbitZone) if self.BorderZone then ZoneSet:AddZone(self.BorderZone) end local RejectZoneSet = SET_ZONE:New() if self.RejectZone then RejectZoneSet:AddZone(self.RejectZone) end self.ZoneSet = ZoneSet self.RejectZoneSet = RejectZoneSet if self.AllowMarkers then -- Add MarkerOps local MarkerOps = MARKEROPS_BASE:New("AWACS",{"Station","Delete","Move"}) local function Handler(Keywords,Coord,Text) self:I(Text) for _,_word in pairs (Keywords) do if string.lower(_word) == "station" then -- get the station name from the text field local Name = string.match(Text," ([%a]+)$") self:_CreateAnchorStackFromMarker(Name,Coord) break elseif string.lower(_word) == "delete" then -- get the station name from the text field local Name = string.match(Text," ([%a]+)$") self:_DeleteAnchorStackFromMarker(Name,Coord) break elseif string.lower(_word) == "move" then -- get the station name from the text field local Name = string.match(Text," ([%a]+)$") self:_MoveAnchorStackFromMarker(Name,Coord) break end end end -- Event functions function MarkerOps:OnAfterMarkAdded(From,Event,To,Text,Keywords,Coord) --local m = MESSAGE:New(string.format("AWACS %s Mark Added.", self.Tag),10,"Info",true):ToAllIf(self.debug) BASE:I(string.format("%s Mark Added.", self.Tag)) Handler(Keywords,Coord,Text) end function MarkerOps:OnAfterMarkChanged(From,Event,To,Text,Keywords,Coord) BASE:I(string.format("%s Mark Changed.", self.Tag)) --local m = MESSAGE:New(string.format("AWACS %s Mark Changed.", self.Tag),10,"Info",true):ToAllIf(self.debug) Handler(Keywords,Coord,Text) end function MarkerOps:OnAfterMarkDeleted(From,Event,To) BASE:I(string.format("%s Mark Deleted.", self.Tag)) --local m = MESSAGE:New(string.format("AWACS %s Mark Deleted.", self.Tag),10,"Info",true):ToAllIf(self.debug) end self.MarkerOps = MarkerOps end self:__Status(-30) return self end function AWACS:_CheckAwacsStatus() self:T(self.lid.."_CheckAwacsStatus") local awacs = nil -- Wrapper.Group#GROUP if self.AwacsFG then awacs = self.AwacsFG:GetGroup() -- Wrapper.Group#GROUP end local monitoringdata = self.MonitoringData -- #AWACS.MonitoringData 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:T(self.lid.."Arrived in Orbit Zone: " .. orbitzone:GetName()) local text = string.format("%s on station for %s control.",self.callsigntxt,self.AOName or "Rock") local textScreen = string.format("%s on station for %s control.",self.callsigntxt,self.AOName or "Rock") self:_NewRadioEntry(text,textScreen,0,false,true,true,false,true) end end -------------------------------- -- AWACS -------------------------------- if (awacs and awacs:IsAlive()) then if not self.intelstarted then local alt = UTILS.Round(UTILS.MetersToFeet(awacs:GetAltitude())/1000,0) if alt >= 10 then self:_StartIntel(awacs) end end if self.intelstarted and not self.sunrisedone then -- TODO Sunrise call on after airborne at ca 10k feet local alt = UTILS.Round(UTILS.MetersToFeet(awacs:GetAltitude())/1000,0) if alt >= 10 then local text = string.format("%s. All stations, SUNRISE SUNRISE SUNRISE, %s.",self.callsigntxt,self.callsigntxt) self:_NewRadioEntry(text,text,0,false,false,false,false,true) --self.AwacsFG:RadioTransmission(text,1,false) self:T(self.lid..text) self.sunrisedone = true end end -- 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.AwacsMissionReplacement 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 self.sunrisedone = 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())) monitoringdata.EscortsStateMission = esstatus monitoringdata.EscortsStateFG = 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:T(report:Text()) end else -- Check on Awacs Mission Status local AWmission = self.AwacsMission -- Ops.Auftrag#AUFTRAG local awstatus = AWmission:GetState() if AWmission:IsOver() then -- yup we're dead self:I(self.lid.."*****AWACS is dead!*****") self.ShiftChangeAwacsFlag = true self:__AwacsShiftChange(2) end end return monitoringdata 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 monitoringdata = self:_CheckAwacsStatus() local awacsalive = false if self.AwacsFG then local awacs = self.AwacsFG:GetGroup() -- Wrapper.Group#GROUP if awacs and awacs:IsAlive() then awacsalive= true end end -- Check on AUFTRAG status for CAP AI if self:Is("Running") and (awacsalive or self.AwacsInZone) then self:_CheckAICAPOnStation() self:_CleanUpContacts() self:_CheckMerges() if self.debug then --local outcome, targets = self:_TargetSelectionProcess() -- TODO for debug ATM end local outcome, targets = self:_TargetSelectionProcess(true) self:_CheckTaskQueue() local AI, Humans = self:_GetIdlePilots() -- assign Pilot if there are targets and available Pilots, prefer Humans to AI -- TODO - Implemented AI First, Humans laters - need to work out how to loop the targets to assign a pilot if outcome and #Humans > 0 and self.PlayerCapAssigment then -- add a task for AI self:_AssignPilotToTarget(Humans,targets) end if outcome and #AI > 0 then -- add a task for AI self:_AssignPilotToTarget(AI,targets) end end monitoringdata.AwacsShiftChange = self.ShiftChangeAwacsFlag if self.AwacsFG then monitoringdata.AwacsStateFG = self.AwacsFG:GetState() end monitoringdata.AwacsStateMission = self.AwacsMission:GetState() monitoringdata.EscortsShiftChange = self.ShiftChangeEscortsFlag monitoringdata.AICAPCurrent = self.AICAPMissions:Count() monitoringdata.AICAPMax = self.MaxAIonCAP monitoringdata.Airwings = self.CAPAirwings:Count() self.MonitoringData = monitoringdata if self.debug then self:_LogStatistics() end local picturetime = timer.getTime() - self.PictureTimeStamp if self.AwacsInZone and picturetime > self.PictureInterval then -- reset timer self.PictureTimeStamp = timer.getTime() self:_Picture(nil,true) end 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 -- Events -- Player joins self:UnHandleEvent(EVENTS.PlayerEnterAircraft) self:UnHandleEvent(EVENTS.PlayerEnterUnit) -- Player leaves self:UnHandleEvent(EVENTS.PlayerLeaveUnit) self:UnHandleEvent(EVENTS.Ejection) self:UnHandleEvent(EVENTS.Crash) self:UnHandleEvent(EVENTS.Dead) self:UnHandleEvent(EVENTS.UnitLost) self:UnHandleEvent(EVENTS.BDA) self:UnHandleEvent(EVENTS.PilotDead) -- Missile warning self:UnHandleEvent(EVENTS.Shot) return self end --- [Internal] onafterAssignAnchor -- @param #AWACS self -- @param #string From -- @param #string Event -- @param #string To -- @param #number GID Group ID -- @param #boolean HasOwnStation -- @param #string HasOwnStation -- @return #AWACS self function AWACS:onafterAssignAnchor(From, Event, To, GID, HasOwnStation, StationName) self:T({From, Event, To, "GID = " .. GID}) self:_AssignAnchorToID(GID, HasOwnStation, StationName) 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:T({From, Event, To, "GID=" .. GID, "Stack=" .. AnchorStackNo}) -- TODO local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup if not managedgroup then self:E(self.lid .. "**** GID "..GID.." Not Registered!") return self end managedgroup.AnchorStackNo = AnchorStackNo managedgroup.AnchorStackAngels = AnchorAngels managedgroup.Blocked = false self.ManagedGrps[GID] = managedgroup local isPlayer = managedgroup.IsPlayer local isAI = managedgroup.IsAI local Group = managedgroup.Group local CallSign = managedgroup.CallSign or "Ghost 1" --local AnchorName = Anchor.StationZone:GetName() or "unknown" local AnchorName = Anchor.StationName or "unknown" local AnchorCoordTxt = Anchor.StationZoneCoordinateText or "unknown" local Angels = AnchorAngels or 25 local AnchorSpeed = self.CapSpeedBase or 270 local AuftragsNr = managedgroup.CurrentAuftrag local textTTS = string.format("%s. %s. Station at %s at angels %d doing %d knots.",CallSign,self.callsigntxt,AnchorName,Angels,AnchorSpeed) local ROEROT = self.AwacsROE..", "..self.AwacsROT local textScreen = string.format("%s. %s.\nStation at %s\nAngels %d\nSpeed %d knots\nCoord %s\nROE %s.",CallSign,self.callsigntxt,AnchorName,Angels,AnchorSpeed,AnchorCoordTxt,ROEROT) local TextTasking = string.format("Station at %s\nAngels %d\nSpeed %d knots\nCoord %s\nROE %s",AnchorName,Angels,AnchorSpeed,AnchorCoordTxt,ROEROT) self:_NewRadioEntry(textTTS,textScreen,GID,isPlayer,isPlayer,true,false) managedgroup.CurrentTask = self:_CreateTaskForGroup(GID,AWACS.TaskDescription.ANCHOR,TextTasking,Anchor.StationZone) -- if it's a Alert5, we want to push CAP instead if isAI then local auftrag = managedgroup.FlightGroup:GetMissionCurrent() -- Ops.Auftrag#AUFTRAG if auftrag then local auftragtype = auftrag:GetType() if auftragtype == AUFTRAG.Type.ALERT5 then -- all correct local capauftrag = AUFTRAG:NewCAP(Anchor.StationZone,Angels*1000,AnchorSpeed,Anchor.StationZone:GetCoordinate(),0,15,{}) capauftrag:SetTime(nil,((self.CAPTimeOnStation*3600)+(15*60))) self.CatchAllMissions[#self.CatchAllMissions+1] = capauftrag managedgroup.FlightGroup:AddMission(capauftrag) auftrag:Cancel() else self:E("**** AssignedAnchor but Auftrag NOT ALERT5!") end else self:E("**** AssignedAnchor but NO Auftrag!") end 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.index}) self.CID = self.CID + 1 self.Countactcounter = self.Countactcounter + 1 local ContactTable = Cluster.Contacts or {} local function GetFirstAliveContact(table) for _,_contact in pairs (table) do local contact = _contact -- Ops.Intelligence#INTEL.Contact if contact and contact.group and contact.group:IsAlive() then return contact end end return nil end local Contact = GetFirstAliveContact(ContactTable) -- Ops.Intelligence#INTEL.Contact if not Contact then return self end local targetset = SET_GROUP:New() -- SET for TARGET for _,_grp in pairs(ContactTable) do local grp = _grp -- Ops.Intelligence#INTEL.Contact targetset:AddGroup(grp.group, true) end local managedcontact = {} -- #AWACS.ManagedContact managedcontact.CID = self.CID managedcontact.Contact = Contact managedcontact.Cluster = Cluster -- TODO set as per tech / engagement / alarm level age... managedcontact.IFF = AWACS.IFF.BOGEY -- no IFF yet managedcontact.Target = TARGET:New(targetset) managedcontact.LinkedGroup = 0 managedcontact.LinkedTask = 0 managedcontact.Status = AWACS.TaskStatus.IDLE local phoneid = math.fmod(self.Countactcounter,27) if phoneid == 0 then phoneid = 1 end managedcontact.TargetGroupNaming = AWACS.Phonetic[phoneid] managedcontact.ReportingName = Contact.group:GetNatoReportingName() -- e.g. Foxbat. Bogey if unknown managedcontact.TACCallDone = false managedcontact.MeldCallDone = false managedcontact.EngagementTag = "" local IsPopup = false -- is this a pop-up group? i.e. appeared inside AO if self.OpsZone:IsVec2InZone(Contact.position:GetVec2()) then IsPopup = true end -- let's see if we can inject some info into Contact Contact.CID = managedcontact.CID Contact.TargetGroupNaming = managedcontact.TargetGroupNaming Cluster.CID = managedcontact.CID Cluster.TargetGroupNaming = managedcontact.TargetGroupNaming self.Contacts:Push(managedcontact,self.CID) -- only announce if in right distance to HVT/AIC or in ControlZone or in BorderZone local ContactCoordinate = Contact.position:GetVec2() local incontrolzone = self.ControlZone:IsVec2InZone(ContactCoordinate) -- distance check to HVT local distance = Contact.position:Get2DDistance(self.OrbitZone:GetCoordinate()) local inborderzone = false if self.BorderZone then inborderzone = self.BorderZone:IsVec2InZone(ContactCoordinate) end if incontrolzone or inborderzone or (distance <= UTILS.NMToMeters(55)) or IsPopup then self:_AnnounceContact(managedcontact,true,nil,false,managedcontact.TargetGroupNaming,IsPopup,managedcontact.ReportingName) end 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}) local tdist = self.ThreatDistance -- NM -- is any plane near-by? for _gid,_mgroup in pairs(self.ManagedGrps) do local managedgroup = _mgroup -- #AWACS.ManagedGroup local group = managedgroup.Group if group and group:IsAlive() then -- contact distance local cpos = Contact.position or Contact.group:GetCoordinate() -- Core.Point#COORDINATE local mpos = group:GetCoordinate() local dist = cpos:Get2DDistance(mpos) -- meter dist = UTILS.Round(UTILS.MetersToNM(dist),0) if dist <= tdist then -- threat call self:_ThreatRangeCall(_gid,Contact) end end end 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}) --self:_CleanUpContacts() 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}) --self:_CleanUpContacts() 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() or self.PrioRadioQueue:IsNotEmpty()) then local RadioEntry = nil if self.PrioRadioQueue:IsNotEmpty() then RadioEntry = self.PrioRadioQueue:Pull() -- #AWACS.RadioEntry else RadioEntry = self.RadioQueue:Pull() -- #AWACS.RadioEntry end self:T({RadioEntry}) if self.clientset:CountAlive() == 0 then self:I(self.lid.."No player connected.") self:__CheckRadioQueue(-5) return self end if not RadioEntry.FromAI then -- AI AWACS Speaking if self.PathToGoogleKey then local gtext = RadioEntry.TextTTS gtext = string.format("%s",gtext) self.AwacsFG:RadioTransmission(gtext,1,false) else self.AwacsFG:RadioTransmission(RadioEntry.TextTTS,1,false) end self:T(RadioEntry.TextTTS) 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 if self.PathToGoogleKey then local gtext = RadioEntry.TextTTS gtext = string.format("%s",gtext) managedgroup.FlightGroup:RadioTransmission(gtext,1,false) else managedgroup.FlightGroup:RadioTransmission(RadioEntry.TextTTS,1,false) end self:T(RadioEntry.TextTTS) end end end if RadioEntry.Duration then nextcall = RadioEntry.Duration end if RadioEntry.ToScreen and RadioEntry.TextScreen and (not self.SuppressScreenOutput) 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) self:T(RadioEntry.TextScreen) end else MESSAGE:New(RadioEntry.TextScreen,20,"AWACS"):ToCoalition(self.coalition) end end end if self:Is("Running") then -- exit if stopped if self.PathToGoogleKey then nextcall = nextcall + self.GoogleTTSPadding else nextcall = nextcall + self.WindowsTTSPadding end self:__CheckRadioQueue(-nextcall) 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:T({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:T({From, Event, To}) -- request new AWACS 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) self.CatchAllMissions[#self.CatchAllMissions+1] = mission 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:T({From, Event, To}) -- coming back from AW, set up the flight self:T("FlightGroup " .. FlightGroup:GetName() .. " Mission " .. Mission:GetName() .. " Type "..Mission:GetType()) self.CatchAllFGs[#self.CatchAllFGs+1] = FlightGroup if not self:Is("Stopped") then if not self.AwacsReady or self.ShiftChangeAwacsFlag or self.ShiftChangeEscortsFlag then self:_StartSettings(FlightGroup,Mission) elseif Mission and (Mission:GetType() == AUFTRAG.Type.CAP or Mission:GetType() == AUFTRAG.Type.ALERT5 or Mission:GetType() == AUFTRAG.Type.ORBIT) then if not self.FlightGroups:HasUniqueID(FlightGroup:GetName()) then self:T("Pushing FG " .. FlightGroup:GetName() .. " to stack!") self.FlightGroups:Push(FlightGroup,FlightGroup:GetName()) end end end return self end --- On after "ReAnchor". -- @param #AWACS self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -- @param #number GID Group ID to check and re-anchor if possible -- @return #AWACS self function AWACS:onafterReAnchor(From, Event, To, GID) self:T({From, Event, To, GID}) -- get managedgroup -- check AI FG state -- check weapon state -- check fuel state -- vector back to anchor or RTB local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup if managedgroup then if managedgroup.IsAI then -- AI will now have a new CAP AUFTRAG and head back to the stack anyway local AIFG = managedgroup.FlightGroup -- Ops.FlightGroup#FLIGHTGROUP if AIFG and AIFG:IsAlive() then -- check state if AIFG:IsFuelLow() or AIFG:IsOutOfMissiles() or AIFG:IsOutOfAmmo() then local destbase = AIFG.homebase if not destbase then destbase = self.Airbase end -- RTB call needs an AIRBASE AIFG:RTB(destbase) -- Check out self:_CheckOut(AIFG:GetGroup(),GID) self.AIRequested = self.AIRequested - 1 else -- re-establish anchor task -- get anchor zone data local Anchor = self.AnchorStacks:ReadByPointer(managedgroup.AnchorStackNo) -- #AWACS.AnchorData local StationZone = Anchor.StationZone -- Core.Zone#ZONE managedgroup.CurrentTask = self:_CreateTaskForGroup(GID,AWACS.TaskDescription.ANCHOR,"Re-Station AI",StationZone) managedgroup.HasAssignedTask = true local mission = AIFG:GetMissionCurrent() -- Ops.Auftrag#AUFTRAG if mission then managedgroup.CurrentAuftrag = mission.auftragsnummer or 0 else managedgroup.CurrentAuftrag = 0 end managedgroup.ContactCID = 0 self.ManagedGrps[GID] = managedgroup self:_MessageVector(GID," to Station",Anchor.StationZoneCoordinate,managedgroup.AnchorStackAngels) end else -- lost group, remove from known groups, declare vanished -- AI - remove from known FGs! -- done in status loop -- ALL remove from managedgrps -- message loss local savedcallsign = managedgroup.CallSign --vanished/friendly flight faded/lost contact with C/S/CSAR Scramble -- Magic, RIGHTGUARD, RIGHTGUARD, Dodge 41, Bullseye X/Y local textoptions = { [1] = "Lost friendly flight", [2] = "Vanished friendly flight", [3] = "Faded friendly contact", [4] = "Lost contact with", } -- DONE - need to save last known coordinate if managedgroup.LastKnownPosition then local lastknown = UTILS.DeepCopy(managedgroup.LastKnownPosition) local faded = textoptions[math.random(1,4)] local text = string.format("All stations. %s. %s %s.",self.callsigntxt, faded, savedcallsign) local textScreen = string.format("All stations, %s. %s %s.", self.callsigntxt, faded, savedcallsign) local brtext = self:_ToStringBULLS(lastknown) local brtexttts = self:_ToStringBULLS(lastknown,false,true) --if self.PathToGoogleKey then --brtexttts = self:_ToStringBULLS(lastknown,true) --end text = text .. " "..brtexttts.." miles." textScreen = textScreen .. " "..brtext.." miles." self:_NewRadioEntry(text,textScreen,0,false,self.debug,true,false,true) end self.ManagedGrps[GID] = nil end elseif managedgroup.IsPlayer then -- TODO local PLFG = managedgroup.Group -- Wrapper.Group#GROUP if PLFG and PLFG:IsAlive() then -- re-establish anchor task -- get anchor zone data local Anchor = self.AnchorStacks:ReadByPointer(managedgroup.AnchorStackNo) -- #AWACS.AnchorData local AnchorName = Anchor.StationName or "unknown" local AnchorCoordTxt = Anchor.StationZoneCoordinateText or "unknown" local Angels = managedgroup.AnchorStackAngels or 25 local AnchorSpeed = self.CapSpeedBase or 270 local StationZone = Anchor.StationZone -- Core.Zone#ZONE local ROEROT = self.AwacsROE.." "..self.AwacsROT local TextTasking = string.format("Station at %s\nAngels %d\nSpeed %d knots\nCoord %s\nROE %s",AnchorName,Angels,AnchorSpeed,AnchorCoordTxt,ROEROT) managedgroup.CurrentTask = self:_CreateTaskForGroup(GID,AWACS.TaskDescription.ANCHOR,TextTasking,StationZone) managedgroup.HasAssignedTask = true managedgroup.ContactCID = 0 self.ManagedGrps[GID] = managedgroup self:_MessageVector(GID," to Station",Anchor.StationZoneCoordinate,managedgroup.AnchorStackAngels) else -- lost group, remove from known groups, declare vanished -- ALL remove from managedgrps -- message loss local savedcallsign = managedgroup.CallSign --vanished/friendly flight faded/lost contact with C/S/CSAR Scramble -- Magic, RIGHTGUARD, RIGHTGUARD, Dodge 41, Bullseye X/Y local textoptions = { [1] = "Lost friendly flight", [2] = "Vanished friendly flight", [3] = "Faded friendly contact", [4] = "Lost contact with", } -- DONE - need to save last known coordinate local faded = textoptions[math.random(1,4)] local text = string.format("All stations. %s. %s %s.",self.callsigntxt, faded, savedcallsign) local textScreen = string.format("All stations, %s. %s %s.", self.callsigntxt, faded, savedcallsign) if managedgroup.LastKnownPosition then local lastknown = UTILS.DeepCopy(managedgroup.LastKnownPosition) local brtext = self:_ToStringBULLS(lastknown) local brtexttts = self:_ToStringBULLS(lastknown,false,true) --if self.PathToGoogleKey then --brtexttts = self:_ToStringBULLS(lastknown,true) --end text = text .. " "..brtexttts.." miles." textScreen = textScreen .. " "..brtext.." miles." self:_NewRadioEntry(text,textScreen,0,false,self.debug,true,false,true) end self.ManagedGrps[GID] = nil end end end end end -- end do ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- END AWACS -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------