5758 lines
218 KiB
Lua

--- **Ops** - AWACS
--
-- ## Main Features:
--
-- * WIP (beta)
-- * AWACS replacement for the in-game AWACS
-- * Will control a fighter engagement zone and assign tasks to AI and human CAP flights
-- * Concentrates on threat-based call outs
-- * Callouts referenced from:
-- ** References from ARN33396 ATP 3-52.4 (Sep 2021) (Combined Forces)
-- ** References from CNATRA P-877 (Rev 12-20) (NAVY)
-- * Many additional events that the mission designer can hook into
--
-- ===
--
-- ## Example Missions:
--
-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/).
--
-- ===
--
-- ### Author: **applevangelist**
-- @date Last Update May 2022
--
-- ==
-- @module Ops.AWACS
-- @image OPS_AWACS.jpg
---
-- ===
--
-- **AWACS** - MOOSE based AI AWACS Fighter Engagement Zone Operations for Players and AI
--
-- ===
--
-- ## Example Missions:
--
-- ### Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/).
--
-- ===
--
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
-- @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 BRAA to the Player position.
-- **Note** that AWACS will do a regular picture call to all stations every five minutes. Here, references are to the (named) BullsEye position.
--
-- ### 6.4 Bogey Dope
--
-- Get bogey dope from the AWACS. It will call out the three most important groups. References are 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 = 3
--
-- ## 8 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.22", -- #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,
}
---
--@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
---
-- @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.22
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--
-- DEBUG - WIP - Player tasking, VID
-- TODO - Localization
-- TODO - (LOW) LotATC / IFF
-- 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.maxassigndistance = 100 --nm
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.ReassignmentPause = 180
self.DeclareRadius = 5 -- NM
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
self.MenuStrict = true
-- 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.SuppressScreenOutput = false
-- Client SET
self.clientset = SET_CLIENT:New():FilterActive(true):FilterCoalitions(self.coalitiontxt):FilterCategories("plane"):FilterStart()
self.PlayerGuidance = true
self.ModernEra = 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.
-- self:__Start(math.random(2,5))
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)
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 sel
function AWACS:SuppressScreenMessages(Switch)
self:T(self.lid.."_SetBullsEyeAlias")
self.SuppressScreenOutput = Switch or false
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 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:GetUnit(1):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 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] 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
-- @return #AWACS self
function AWACS:SetAdditionalZone(Zone)
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()
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
-- @return #AWACS self
function AWACS:SetRejectionZone(Zone)
self:T(self.lid.."SetRejectionZone")
self.RejectZone = 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(),"Rejection Zone"):ToAll()
end
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 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)
-- 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 <say-as interpret-as='characters'>%03d</say-as>, %d",bullseyename,Bearing,Distance)
elseif TTS then
Bearing = self:_ToStringBullsTTS(Bearing)
return string.format("%s %s, %d",bullseyename,Bearing,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:E(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] 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 then
self:T("DEAD count="..deadcontacts:Count())
-- check cluster alive or announce lost
deadcontacts:ForEach(
function (Contact)
local contact = Contact -- #AWACS.ManagedContact
-- see if the complete cluster is dead
-- if contact.Cluster and self.intel:ClusterCountUnits(contact.Cluster) > 0 then
-- not complete cluster dead, update contact data later
-- aliveclusters:Push(contact)
--else
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)
-- pull from Contacts
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 = ""
-- "<tag> group, BRA <bearing> for <range> at angels <alt/1000>, <aspect>, <shipsize>"
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 = ""
text = contact.TargetGroupNaming.." group." -- Alpha Group.
textScreen = contact.TargetGroupNaming.." group,"
if IsGeneral then
-- AO/BE Reference
refBRAA=self:_ToStringBULLS(coordinate)
if self.PathToGoogleKey then
refBRAATTS = self:_ToStringBULLS(coordinate, true)
else
--refBRAATTS = self:__ToStringBullsTTS(refBRAA)
refBRAATTS = self:_ToStringBULLS(coordinate,false,true)
end
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)
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 = ""
if IsGeneral then
aspect = coordinate:ToStringAspect(self.OpsZone:GetCoordinate())
text = text .. " "..aspect.."." -- Alpha Group. Bulls eye 0 2 1, 1 6. Flanking.
textScreen = textScreen .. " "..aspect.."." -- Alpha Group, Bullseye 021, 16, Flanking.
end
-- 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. Flanking. 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 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 Group and Outcome then
general = false
end
if general then
gcallsign = "All Stations"
--else
--gcallsign = self:_GetCallSign(Group,GID) or "Ghost 1"
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)
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)
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)
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:I(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:I("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:I(text)
else
-- too far away
self:I("Contact VID not close enough")
text = string.format("%s. %s. Negative, get closer to target.",Callsign,self.callsigntxt)
self:I(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
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)
self:__AssignAnchor(5,managedgroup.GID)
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())
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 )
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 = cgrp:GetName()
self:T(cgrpname)
--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 Create a new Anchor Stack
-- @param #AWACS self
-- @return #boolean success
-- @return #nunber AnchorStackNo
function AWACS:_CreateAnchorStack()
self:T(self.lid.."_CreateAnchorStack")
local stackscreated = self.AnchorStacks:GetSize()
if stackscreated == self.AnchorMaxAnchors then
-- only create self.AnchorMaxAnchors Anchors
return false, 0
end
local AnchorStackOne = {} -- #AWACS.AnchorData
AnchorStackOne.AnchorBaseAngels = self.AnchorBaseAngels
AnchorStackOne.Anchors = FIFO:New() -- Utilities.FiFo#FIFO
AnchorStackOne.AnchorAssignedID = FIFO:New() -- Utilities.FiFo#FIFO
local newname = 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 anchorradius = self.OpsZone:GetRadius() -- #number
--anchorradius = anchorradius + self.StationZone:GetRadius()
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
AnchorStackOne.StationZone = ZONE_RADIUS:New(newname, newanchorbasecoord:GetVec2(), self.StationZone:GetRadius())
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("<say-as interpret-as='characters'>%s</say-as>",AngleDegText)
else
AngleDegTextTTS = string.format("%s",AngleDegText)
end
AngleDegText = string.gsub(AngleDegText,"%d","%1 ") -- "0 5 1 "
AngleDegText = string.gsub(AngleDegText," $","") -- "0 5 1"
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("<say-as interpret-as='characters'>%s</say-as>",AngleDegText)
AngleDegText = string.gsub(AngleDegText,"%d","%1 ") -- "0 5 1 "
AngleDegText = string.gsub(AngleDegText," $","") -- "0 5 1"
local Distance = ToCoordinate:Get2DDistance( FromCoordinate ) --meters
local distancenm = UTILS.Round(UTILS.MetersToNM(Distance),0)
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
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:I(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:I("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:I("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:I("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:I("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:I("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:I("TAC/MELD distance check: "..distance.."NM!")
if distance <= self.TacDistance and distance >= self.MeldDistance then
-- TAC distance
self:I("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:I("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:I(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:I("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:I("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:I("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:I(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:I("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:I("Out of bounds - SUCCESS")
end
end
if entry.Status == AWACS.TaskStatus.REQUESTED then
-- requested - player tasks only!
self:I("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:I(text)
elseif entry.Status == AWACS.TaskStatus.ASSIGNED then
self:I("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:I("TAC/MELD distance check: "..distance.."NM!")
if distance <= self.TacDistance and distance >= self.MeldDistance then
-- TAC distance
self:I("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:I("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:I("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:I("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:I("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:I("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:I("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
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)
end
end
-- "Uzi 1-1, Magic, BRA, 183 for 10 at 2000, hot"
-- "<togroup>, <fromgroup>, <New>/<Contact>, <tag>, BRA <bearing> for <range> at angels <alt/1000>, <aspect>, <shipsize>"
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 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)
self:_NewRadioEntry(BRAText,TextScreen,GID,isGroup,true,IsNew,false,IsNew)
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:I(self.lid.."_TACRangeCall")
-- AIC: “Enforcer 11, single group, 30 miles.”
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:I(self.lid.."_MeldRangeCall")
-- 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)
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:I(self.lid.."_ThreatRangeCall")
-- 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)
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] 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:I(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:I(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:I(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. Request commit %s group. %s.", self.callsigntxt,Pilot.CallSign,Target.TargetGroupNaming,TargetDirectionsTTS)
local textScreen = string.format("%s, %s. Request commit %s group. %s.", self.callsigntxt,Pilot.CallSign,Target.TargetGroupNaming,TargetDirections)
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:I("Pilot AI Callsign: " .. callsign)
self:I("Pilot FG State: " .. FGStatus)
local targetstatus = Target.Target:GetState()
self:I("Target State: " .. targetstatus)
--
local currmission = Pilot.FlightGroup:GetMissionCurrent()
if currmission then
self:I("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. Commit %s group. %s.", self.callsigntxt,Pilot.CallSign,Target.TargetGroupNaming,bratexttts)
local textScreen = string.format("%s, %s. Commit %s group. %s.", self.callsigntxt,Pilot.CallSign,Target.TargetGroupNaming,bratext)
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
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()
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 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
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. Wait for task assignment.",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\nWait for task assignment.",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
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()) and self.clientset:CountAlive() > 0 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 not RadioEntry.FromAI then
-- AI AWACS Speaking
if self.PathToGoogleKey then
local gtext = RadioEntry.TextTTS
gtext = string.format("<speak><prosody rate='medium'>%s</prosody></speak>",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("<speak><prosody rate='medium'>%s</prosody></speak>",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
self:__CheckRadioQueue(nextcall+2)
end
return self
end
--- [Internal] onafterEscortShiftChange
-- @param #AWACS self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @return #AWACS self
function AWACS:onafterEscortShiftChange(From,Event,To)
self: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(brtext,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(brtext,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
return self
end
end -- end do
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- END AWACS
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------