diff --git a/Moose Development/Moose/Core/Beacon.lua b/Moose Development/Moose/Core/Beacon.lua index cdd311372..3f255e0e0 100644 --- a/Moose Development/Moose/Core/Beacon.lua +++ b/Moose Development/Moose/Core/Beacon.lua @@ -16,20 +16,20 @@ --- *In order for the light to shine so brightly, the darkness must be present.* -- Francis Bacon -- -- After attaching a @{#BEACON} to your @{Wrapper.Positionable#POSITIONABLE}, you need to select the right function to activate the kind of beacon you want. --- There are two types of BEACONs available : the AA TACAN Beacon and the general purpose Radio Beacon. +-- There are two types of BEACONs available : the (aircraft) TACAN Beacon and the general purpose Radio Beacon. -- Note that in both case, you can set an optional parameter : the `BeaconDuration`. This can be very usefull to simulate the battery time if your BEACON is -- attach to a cargo crate, for exemple. -- --- ## AA TACAN Beacon usage +-- ## Aircraft TACAN Beacon usage -- --- This beacon only works with airborne @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}. Use @{#BEACON:AATACAN}() to set the beacon parameters and start the beacon. --- Use @#BEACON:StopAATACAN}() to stop it. +-- This beacon only works with airborne @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}. Use @{#BEACON.ActivateTACAN}() to set the beacon parameters and start the beacon. +-- Use @{#BEACON.StopRadioBeacon}() to stop it. -- -- ## General Purpose Radio Beacon usage -- -- This beacon will work with any @{Wrapper.Positionable#POSITIONABLE}, but **it won't follow the @{Wrapper.Positionable#POSITIONABLE}** ! This means that you should only use it with --- @{Wrapper.Positionable#POSITIONABLE} that don't move, or move very slowly. Use @{#BEACON:RadioBeacon}() to set the beacon parameters and start the beacon. --- Use @{#BEACON:StopRadioBeacon}() to stop it. +-- @{Wrapper.Positionable#POSITIONABLE} that don't move, or move very slowly. Use @{#BEACON.RadioBeacon}() to set the beacon parameters and start the beacon. +-- Use @{#BEACON.StopRadioBeacon}() to stop it. -- -- @type BEACON -- @field #string ClassName Name of the class "BEACON". @@ -245,7 +245,8 @@ function BEACON:ActivateICLS(Channel, Callsign, Duration) return self end ---- Activates a TACAN BEACON on an Aircraft. +--- DEPRECATED: Please use @{BEACON:ActivateTACAN}() instead. +-- Activates a TACAN BEACON on an Aircraft. -- @param #BEACON self -- @param #number TACANChannel (the "10" part in "10Y"). Note that AA TACAN are only available on Y Channels -- @param #string Message The Message that is going to be coded in Morse and broadcasted by the beacon @@ -325,7 +326,7 @@ function BEACON:StopAATACAN() end ---- Activates a general pupose Radio Beacon +--- Activates a general purpose Radio Beacon -- This uses the very generic singleton function "trigger.action.radioTransmission()" provided by DCS to broadcast a sound file on a specific frequency. -- Although any frequency could be used, only 2 DCS Modules can home on radio beacons at the time of writing : the Huey and the Mi-8. -- They can home in on these specific frequencies : @@ -402,7 +403,7 @@ function BEACON:RadioBeacon(FileName, Frequency, Modulation, Power, BeaconDurati end end ---- Stops the AA TACAN BEACON +--- Stops the Radio Beacon -- @param #BEACON self -- @return #BEACON self function BEACON:StopRadioBeacon() @@ -448,4 +449,4 @@ function BEACON:_TACANToFrequency(TACANChannel, TACANMode) end return (A + TACANChannel - B) * 1000000 -end \ No newline at end of file +end diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index dfbbb6a90..e459e702e 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -2777,8 +2777,10 @@ do -- COORDINATE -- @param #boolean Bogey Add "Bogey" at the end if true (not yet declared hostile or friendly) -- @param #boolean Spades Add "Spades" at the end if true (no IFF/VID ID yet known) -- @param #boolean SSML Add SSML tags speaking aspect as 0 1 2 and "brah" instead of BRAA + -- @param #boolean Angels If true, altitude is e.g. "Angels 25" (i.e., a friendly plane), else "25 thousand" + -- @param #boolean Zeros If using SSML, be aware that Google TTS will say "oh" and not "zero" for "0"; if Zeros is set to true, "0" will be replaced with "zero" -- @return #string The BRAA text. - function COORDINATE:ToStringBRAANATO(FromCoordinate,Bogey,Spades,SSML) + function COORDINATE:ToStringBRAANATO(FromCoordinate,Bogey,Spades,SSML,Angels,Zeros) -- Thanks to @Pikey local BRAANATO = "Merged." @@ -2796,14 +2798,36 @@ do -- COORDINATE local alt = UTILS.Round(UTILS.MetersToFeet(self.y)/1000,0)--*1000 + local alttext = string.format("%d thousand",alt) + + if Angels then + alttext = string.format("Angels %d",alt) + end + + if alt < 1 then + alttext = "very low" + end + local track = UTILS.BearingToCardinal(bearing) or "North" if rangeNM > 3 then - if SSML then - if aspect == "" then - BRAANATO = string.format("brah %03d, %d miles, Angels %d, Track %s",bearing, rangeNM, alt, track) + if SSML then -- google says "oh" instead of zero, be aware + if Zeros then + bearing = string.format("%03d",bearing) + local AngleDegText = string.gsub(bearing,"%d","%1 ") -- "0 5 1 " + AngleDegText = string.gsub(AngleDegText," $","") -- "0 5 1" + AngleDegText = string.gsub(AngleDegText,"0","zero") + if aspect == "" then + BRAANATO = string.format("brah %s, %d miles, %s, Track %s", AngleDegText, rangeNM, alttext, track) + else + BRAANATO = string.format("brah %s, %d miles, %s, %s, Track %s", AngleDegText, rangeNM, alttext, aspect, track) + end else - BRAANATO = string.format("brah %03d, %d miles, Angels %d, %s, Track %s",bearing, rangeNM, alt, aspect, track) + if aspect == "" then + BRAANATO = string.format("brah %03d, %d miles, %s, Track %s", bearing, rangeNM, alttext, track) + else + BRAANATO = string.format("brah %03d, %d miles, %s, %s, Track %s", bearing, rangeNM, alttext, aspect, track) + end end if Bogey and Spades then BRAANATO = BRAANATO..", Bogey, Spades." @@ -2816,9 +2840,9 @@ do -- COORDINATE end else if aspect == "" then - BRAANATO = string.format("BRA %03d, %d miles, Angels %d, Track %s",bearing, rangeNM, alt, track) + BRAANATO = string.format("BRA %03d, %d miles, %s, Track %s",bearing, rangeNM, alttext, track) else - BRAANATO = string.format("BRAA %03d, %d miles, Angels %d, %s, Track %s",bearing, rangeNM, alt, aspect, track) + BRAANATO = string.format("BRAA %03d, %d miles, %s, %s, Track %s",bearing, rangeNM, alttext, aspect, track) end if Bogey and Spades then BRAANATO = BRAANATO..", Bogey, Spades." diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index 518d8ff62..655e4eca2 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -4180,6 +4180,26 @@ do -- SET_CLIENT return CountU end + + --- Gets the alive set. + -- @param #SET_CLIENT self + -- @return #table Table of SET objects + function SET_CLIENT:GetAliveSet() + + local AliveSet = SET_CLIENT:New() + + -- Clean the Set before returning with only the alive Groups. + for GroupName, GroupObject in pairs(self.Set) do + local GroupObject=GroupObject --Wrapper.Client#CLIENT + + if GroupObject and GroupObject:IsAlive() then + AliveSet:Add(GroupName, GroupObject) + end + end + + return AliveSet.Set or {} + end + --- -- @param #SET_CLIENT self -- @param Wrapper.Client#CLIENT MClient diff --git a/Moose Development/Moose/Ops/Awacs.lua b/Moose Development/Moose/Ops/Awacs.lua index 1aa3fd14f..7dee1b2a2 100644 --- a/Moose Development/Moose/Ops/Awacs.lua +++ b/Moose Development/Moose/Ops/Awacs.lua @@ -1,47 +1,26 @@ --- **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 --- +-- === +-- +-- **AWACS** - MOOSE AI AWACS Operations using text-to-speech. +-- -- === -- -- ## Example Missions: -- --- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/). +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20Awacs/). +-- +-- ## Videos: +-- +-- Demo videos can be found on [Youtube](https://www.youtube.com/watch?v=ocdy8QzTNN4&list=PLFxp425SeXnq-oS0DSjam1HtddywH8i_k) -- -- === -- -- ### Author: **applevangelist** --- @date Last Update May 2022 --- --- == +-- @date Last Update June 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 @@ -109,22 +88,29 @@ do -- @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 #boolean PlayerGuidance if true additional callouts to guide/warn players +-- @field #boolean ModernEra if true we get more intel on targets, and EPLR on the AIC +-- @field #boolean callsignshort if true use short (group) callsigns, e.g. "Ghost 1", else "Ghost 1 1" +-- @field #number MeldDistance 25nm - distance for "Meld" Call , usually shortly before the actual engagement +-- @field #number TacDistance 30nm - distance for "TAC" Call +-- @field #number ThreatDistance 15nm - distance to declare untargeted (new) threats +-- @field #string AOName name of the FEZ, e.g. Rock +-- @field Core.Point#COORDINATE AOCoordinate Coordinate of bulls eye -- @field Utilities.FiFo#FIFO clientmenus --- @field #number RadarBlur -- Radar blur in % --- @field #number ReassignmentPause -- Wait this many seconds before re-assignment of a player +-- @field #number RadarBlur Radar blur in % +-- @field #number ReassignmentPause Wait this many seconds before re-assignment of a player +-- @field #boolean NoGroupTags Set to true if you don't want group tags. +-- @field #boolean SuppressScreenOutput Set to true to suppress all screen output. +-- @field #boolean NoMissileCalls Suppress missile callouts +-- @field #boolean PlayerCapAssigment Assign players to CAP tasks when they are logged on +-- @field #number GoogleTTSPadding +-- @field #number WindowsTTSPadding +-- @field #boolean AllowMarkers +-- @field #string PlayerStationName -- @extends Core.Fsm#FSM --- --- === -- -- *Of all men\'s miseries the bitterest is this: to know so much and to have control over nothing.* (Herodotus) -- @@ -255,12 +241,12 @@ do -- -- ### 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. +-- Get a picture from the AWACS. It will call out the three most important groups. References are **always** to the (named) BullsEye position. +-- **Note** that AWACS will anyway do a regular picture call to all stations every five minutes. -- -- ### 6.4 Bogey Dope -- --- Get bogey dope from the AWACS. It will call out the three most important groups. References are BRAA to the Player position. +-- Get bogey dope from the AWACS. It will call out the closest bogey group, if any. Reference is BRAA to the Player position. -- -- ### 6.5 Declare -- @@ -282,16 +268,103 @@ do -- Tac Distance = 45 -- Meld Distance = 35 -- Threat Distance = 25 --- Merge Distance = 3 +-- Merge Distance = 5 +-- +-- ## 8 Bespoke Player CallSigns +-- +-- Append the GROUP name of your client slots with "#CallSign" to use bespoke callsigns in AWACS callouts. E.g. "Player F14#Ghostrider" will be refered to +-- as "Ghostrider" plus group number, e.g. "Ghostrider 9". +-- +-- ## 9 Options +-- +-- There's a number of functions available, to set various options for the setup. +-- +-- * @{#AWACS.SetBullsEyeAlias}() : Set the alias name of the Bulls Eye. +-- * @{#AWACS.SetTOS}() : Set time on station for AWACS and CAP. +-- * @{#AWACS.SetReassignmentPause}() : Pause this number of seconds before re-assigning a Player to a task. +-- * @{#AWACS.SuppressScreenMessages}() : Suppress message output on screen. +-- * @{#AWACS.SetRadarBlur}() : Set the radar blur faktor in percent. +-- * @{#AWACS.SetColdWar}() : Set to cold war - no fill-ins, no EPLRS, VID as standard. +-- * @{#AWACS.SetModernEraDefensive}() : Set to modern, EPLRS, BVR/IFF engagement, fill-ins. +-- * @{#AWACS.SetModernEraAgressive}() : Set to modern, EPLRS, BVR/IFF engagement, fill-ins. +-- * @{#AWACS.SetPolicingModern}() : Set to modern, EPLRS, VID engagement, fill-ins. +-- * @{#AWACS.SetPolicingColdWar}() : Set to cold war, no EPLRS, VID engagement, no fill-ins. +-- * @{#AWACS.SetInterceptTimeline}() : Set distances for TAC, Meld and Threat range calls. +-- * @{#AWACS.SetAdditionalZone}() : Add one additional defense zone, e.g. own border. +-- * @{#AWACS.SetRejectionZone}() : Add one foreign border. Targets beyond will be ignored for tasking. +-- * @{#AWACS.DrawFEZ}() : Show the FEZ on the F10 map. +-- * @{#AWACS.SetAWACSDetails}() : Set AWACS details. +-- * @{#AWACS.AddGroupToDetection}() : Add a group object to INTEL detection, e.g. EWR. +-- * @{#AWACS.SetSRS}() : Set SRS details. +-- * @{#AWACS.SetSRSVoiceCAP}() : Set voice details for AI CAP planes, using Windows dektop TTS. +-- * @{#AWACS.SetAICAPDetails}() : Set AI CAP details. +-- * @{#AWACS.SetEscort}() : Set number of escorting planes for AWACS. +-- * @{#AWACS.AddCAPAirWing}() : Add an additional @{Ops.Airwing#AIRWING} for CAP flights. +-- * @{#AWACS.ZipLip}() : Do not show messages on screen, no extra calls for player guidance, use short callsigns, no group tags. +-- +-- ## 9.1 Single Options +-- +-- Further single options (set before starting your AWACS instance, but after `:New()`) +-- +-- testawacs.PlayerGuidance = true -- allow missile warning call-outs. +-- testawacs.NoGroupTags = false -- use group tags like Alpha, Bravo .. etc in call outs. +-- testawacs.callsignshort = true -- use short callsigns, e.g. "Moose 1", not "Moose 1-1". +-- testawacs.DeclareRadius = 5 -- you need to be this close to the lead unit for declare/VID to work, in NM. +-- testawacs.MenuStrict = true -- Players need to check-in to see the menu; check-in still require to use the menu. +-- testawacs.maxassigndistance = 100 -- Don't assign targets further out than this, in NM. +-- testawacs.debug = false -- set to true to produce more log output. +-- testawacs.NoMissileCalls = true -- suppress missile callouts +-- testawacs.PlayerCapAssigment = true -- no intercept task assignments for players +-- testawacs.invisible = false -- set AWACS to be invisible to hostiles +-- testawacs.immortal = false -- set AWACS to be immortal +-- -- By default, the radio queue is checked every 10 secs. This is altered by the calculated length of the sentence to speak +-- -- over the radio. Google and Windows speech speed is different. Use the below to fine-tune the setup in case of overlapping +-- -- messages or too long pauses +-- testawacs.GoogleTTSPadding = 1 -- seconds +-- testawacs.WindowsTTSPadding = 2.5 -- seconds +-- +-- ## 9.2 Bespoke random voices for AI CAP (Google TTS only) +-- +-- Currently there are 10 voices defined which are randomly assigned to the AI CAP flights: +-- +-- Defaults are: +-- +-- testawacs.CapVoices = { +-- [1] = "de-DE-Wavenet-A", +-- [2] = "de-DE-Wavenet-B", +-- [3] = "fr-FR-Wavenet-A", +-- [4] = "fr-FR-Wavenet-B", +-- [5] = "en-GB-Wavenet-A", +-- [6] = "en-GB-Wavenet-B", +-- [7] = "en-GB-Wavenet-D", +-- [8] = "en-AU-Wavenet-B", +-- [9] = "en-US-Wavenet-J", +-- [10] = "en-US-Wavenet-H", +-- } +-- +-- ## 10 Using F10 map markers to create new player station points +-- +-- You can use F10 map markers to create new station points for human CAP flights. The latest created station will take priority for (new) station assignments for humans. +-- Enable this option with +-- +-- testawacs.AllowMarkers = true -- --- ## 8 Discussion +-- Set a marker on the map and add the following text to create a station: "AWACS Station London" - "AWACS Station" are the necessary keywords, "London" +-- in this example will be the name of the new station point. The user marker can then be deleted, an info marker point at the same place will remain. +-- You can delete a player station point the same way: "AWACS Delete London"; note this will only work if currently there are no assigned flights on this station. +-- Lastly, you can move the station around with keyword "Move": "AWACS Move London". +-- +-- ## 11 Discussion -- -- If you have questions or suggestions, please visit the [MOOSE Discord](https://discord.gg/AeYAkHP) #ops-awacs channel. -- +-- +-- +-- -- @field #AWACS AWACS = { ClassName = "AWACS", -- #string - version = "beta 0.1.22", -- #string + version = "beta 0.1.30", -- #string lid = "", -- #string coalition = coalition.side.BLUE, -- #number coalitiontxt = "blue", -- #string @@ -365,6 +438,14 @@ AWACS = { clientmenus = nil, RadarBlur = 15, ReassignmentPause = 180, + NoGroupTags = false, + SuppressScreenOutput = false, + NoMissileCalls = true, + GoogleTTSPadding = 1, + WindowsTTSPadding = 2.5, + PlayerCapAssigment = true, + AllowMarkers = false, + PlayerStationName = nil, } --- @@ -554,6 +635,7 @@ AWACS.CapVoices = { -- @field #string EngagementTag -- @field #boolean TACCallDone -- @field #boolean MeldCallDone +-- @field #boolean MergeCallDone --- -- @type AWACS.TaskDescription @@ -624,12 +706,12 @@ AWACS.TaskStatus = { --@field #boolean FromAI ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO-List 0.1.22 +-- TODO-List 0.1.30 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- --- DEBUG - WIP - Player tasking, VID --- TODO - Localization --- TODO - (LOW) LotATC / IFF +-- DONE - WIP - Player tasking, VID +-- TODO - Localization (sensible?) +-- TODO - (LOW) LotATC -- TODO - SW Optimization -- WONTDO - Maybe check in AI only when airborne -- DONE - remove SSML tag when not on google (currently sometimes spoken) @@ -768,7 +850,6 @@ function AWACS:New(Name,AirWing,Coalition,AirbaseName,AwacsOrbit,OpsZone,Station self.invisible = false self.immortal = false self.callsigntxt = "AWACS" - self.maxassigndistance = 100 --nm self.AwacsTimeOnStation = 4 self.AwacsTimeStamp = 0 @@ -787,10 +868,6 @@ function AWACS:New(Name,AirWing,Coalition,AirbaseName,AwacsOrbit,OpsZone,Station 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 @@ -798,8 +875,6 @@ function AWACS:New(Name,AirWing,Coalition,AirbaseName,AwacsOrbit,OpsZone,Station self.AwacsROE = AWACS.ROE.IFF self.AwacsROT = AWACS.ROT.BYPASSESCAPE - self.MenuStrict = true - -- Escorts self.HasEscorts = false self.EscortTemplate = "" @@ -814,15 +889,25 @@ function AWACS:New(Name,AirWing,Coalition,AirbaseName,AwacsOrbit,OpsZone,Station self.RadioQueue = FIFO:New() -- Utilities.FiFo#FIFO self.PrioRadioQueue = FIFO:New() -- Utilities.FiFo#FIFO self.maxspeakentries = 3 - - self.SuppressScreenOutput = false + self.GoogleTTSPadding = 1 + self.WindowsTTSPadding = 2.5 -- Client SET self.clientset = SET_CLIENT:New():FilterActive(true):FilterCoalitions(self.coalitiontxt):FilterCategories("plane"):FilterStart() - + + -- Player options self.PlayerGuidance = true self.ModernEra = true - + self.NoGroupTags = false + self.SuppressScreenOutput = false + self.ReassignmentPause = 180 + self.callsignshort = true + self.DeclareRadius = 5 -- NM + self.MenuStrict = true + self.maxassigndistance = 100 --nm + self.NoMissileCalls = true + self.PlayerCapAssigment = true + -- managed groups self.ManagedGrps = {} -- #table of #AWACS.ManagedGroup entries self.ManagedGrpID = 0 @@ -902,10 +987,8 @@ function AWACS:New(Name,AirWing,Coalition,AirbaseName,AwacsOrbit,OpsZone,Station 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) @@ -926,6 +1009,119 @@ function AWACS:New(Name,AirWing,Coalition,AirbaseName,AwacsOrbit,OpsZone,Station -- Missile warning self:HandleEvent(EVENTS.Shot, self._EventHandler) + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the AWACS. Initializes parameters and starts event handlers. + -- @function [parent=#AWACS] Start + -- @param #AWACS self + + --- Triggers the FSM event "Start" after a delay. Starts the AWACS. Initializes parameters and starts event handlers. + -- @function [parent=#AWACS] __Start + -- @param #AWACS self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the AWACS and all its event handlers. + -- @param #AWACS self + + --- Triggers the FSM event "Stop" after a delay. Stops the AWACS and all its event handlers. + -- @function [parent=#AWACS] __Stop + -- @param #AWACS self + -- @param #number delay Delay in seconds. + + --- On After "CheckedIn" event. AI or Player checked in. + -- @function [parent=#AWACS] OnAfterCheckedIn + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "CheckedOut" event. AI or Player checked out. + -- @function [parent=#AWACS] OnAfterCheckedOut + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "AssignedAnchor" event. AI or Player has been assigned a CAP station. + -- @function [parent=#AWACS] OnAfterAssignedAnchor + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "ReAnchor" event. AI or Player has been send back to station. + -- @function [parent=#AWACS] OnAfterReAnchor + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "NewCluster" event. AWACS detected a cluster. + -- @function [parent=#AWACS] OnAfterNewCluster + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "NewContact" event. AWACS detected a contact. + -- @function [parent=#AWACS] OnAfterNewContact + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "LostCluster" event. AWACS lost a radar cluster. + -- @function [parent=#AWACS] OnAfterLostCluster + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "LostContact" event. AWACS lost a radar contact. + -- @function [parent=#AWACS] OnAfterLostContact + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "EscortShiftChange" event. AWACS escorts shift change. + -- @function [parent=#AWACS] OnAfterEscortShiftChange + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "AwacsShiftChange" event. AWACS shift change. + -- @function [parent=#AWACS] OnAfterAwacsShiftChange + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "Intercept" event. CAP send on intercept. + -- @function [parent=#AWACS] OnAfterIntercept + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "InterceptSuccess" event. Intercept successful. + -- @function [parent=#AWACS] OnAfterIntercept + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "InterceptFailure" event. Intercept failure. + -- @function [parent=#AWACS] OnAfterIntercept + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + return self end @@ -998,13 +1194,26 @@ 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 +-- @return #AWACS self function AWACS:SuppressScreenMessages(Switch) self:T(self.lid.."_SetBullsEyeAlias") self.SuppressScreenOutput = Switch or false return self end +--- [User] Do not show messages on screen, no extra calls for player guidance, use short callsigns etc. +-- @param #AWACS self +-- @return #AWACS self +function AWACS:ZipLip() + self:T(self.lid.."ZipLip") + self:SuppressScreenMessages(true) + self.PlayerGuidance = false + self.callsignshort = true + --self.NoGroupTags = true + self.NoMissileCalls = true + return self +end + --- [Internal] Event handler -- @param #AWACS self -- @param Wrapper.Group#GROUP Group Group, can also be passed as #string group name @@ -1080,7 +1289,7 @@ function AWACS:_EventHandler(EventData) end end - if Event.id == EVENTS.Shot and self.PlayerGuidance then + if Event.id == EVENTS.Shot and self.PlayerGuidance and not self.NoMissileCalls then if Event.IniCoalition ~= self.coalition then self:T("Shot from: " .. Event.IniGroupName) --self:T(UTILS.OneLineSerialize(Event)) @@ -1129,7 +1338,7 @@ end -- @return #AWACS self function AWACS:_MissileWarning(Coordinate,Type,Warndist) self:T(self.lid.."_MissileWarning Type="..Type.." WarnDist="..Warndist) - self:T(UTILS.OneLineSerialize(Coordinate)) + --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() @@ -1138,7 +1347,7 @@ function AWACS:_MissileWarning(Coordinate,Type,Warndist) for _,_grp in pairs (targets) do -- TODO -- player callouts only if _grp and _grp:IsAlive() then - local isPlayer = _grp:GetUnit(1):IsPlayer() + local isPlayer = _grp:IsPlayer() --if self.debug or isPlayer then if isPlayer then local callsign = self:_GetCallSign(_grp) @@ -1176,6 +1385,17 @@ function AWACS:SetColdWar() return self end +--- [User] Set AWACS to Modern Era standards - ROE to BVR, ROT to defensive (evade fire). Radar blur 15%. +-- @param #AWACS self +-- @return #AWACS self +function AWACS:SetModernEra() + self.ModernEra = true + self.AwacsROT = AWACS.ROT.EVADE + self.AwacsROE = AWACS.ROE.BVR + self.RadarBlur = 15 + return self +end + --- [User] Set AWACS to Modern Era standards - ROE to IFF, ROT to defensive (evade fire). Radar blur 15%. -- @param #AWACS self -- @return #AWACS self @@ -1222,6 +1442,19 @@ function AWACS:SetPolicingColdWar() return self end +--- [User] Set AWACS Player Guidance - influences missile callout and the "New" label in group callouts. +-- @param #AWACS self +-- @param #boolean Switch If true (default) it is on, if false, it is off. +-- @return #AWACS self +function AWACS:SetPlayerGuidance(Switch) + if (Switch == nil) or (Switch == true) then + self.PlayerGuidance = true + else + self.PlayerGuidance = false + end + return self +end + --- [User] Get AWACS Name -- @param #AWACS self -- @return #string Name of this instance @@ -1245,13 +1478,16 @@ end --- [User] Set additional defensive zone, e.g. the zone behind the FEZ to also be defended -- @param #AWACS self -- @param Core.Zone#ZONE Zone +-- @param #boolean Draw Draw lines around this zone if true -- @return #AWACS self -function AWACS:SetAdditionalZone(Zone) +function AWACS:SetAdditionalZone(Zone, Draw) self:T(self.lid.."SetAdditionalZone") self.BorderZone = Zone if self.debug then Zone:DrawZone(-1,{1,0.64,0},1,{1,0.64,0},0.2,1,true) MARKER:New(Zone:GetCoordinate(),"Defensive Zone"):ToAll() + elseif Draw then + Zone:DrawZone(-1,{1,0.64,0},1,{1,0.64,0},0.2,1,true) end return self end @@ -1259,17 +1495,29 @@ end --- [User] Set rejection zone, e.g. a border of a foreign country. Detected bogeys in here won't be engaged. -- @param #AWACS self -- @param Core.Zone#ZONE Zone +-- @param #boolean Draw Draw lines around this zone if true -- @return #AWACS self -function AWACS:SetRejectionZone(Zone) +function AWACS:SetRejectionZone(Zone,Draw) self:T(self.lid.."SetRejectionZone") self.RejectZone = Zone - if self.debug then + if Draw then + Zone:DrawZone(-1,{1,0.64,0},1,{1,0.64,0},0.2,1,true) + --MARKER:New(Zone:GetCoordinate(),"Rejection Zone"):ToAll() + elseif self.debug then Zone:DrawZone(-1,{1,0.64,0},1,{1,0.64,0},0.2,1,true) MARKER:New(Zone:GetCoordinate(),"Rejection Zone"):ToAll() end return self end +--- [User] Draw a line around the FEZ on the F10 map. +-- @param #AWACS self +-- @return #AWACS self +function AWACS:DrawFEZ() + self.OpsZone:DrawZone(-1,{1,0,0},1,{1,0,0},0.2,5,true) + return self +end + --- [User] Set AWACS flight details -- @param #AWACS self -- @param #number CallSign Defaults to CALLSIGN.AWACS.Magic @@ -1293,6 +1541,7 @@ function AWACS:SetAwacsDetails(CallSign,CallSignNo,Angels,Speed,Heading,Leg) end --- [User] Add a radar GROUP object to the INTEL detection SET_GROUP +-- @param #AWACS self -- @param Wrapper.Group#GROUP Group The GROUP to be added. Can be passed as SET_GROUP. -- @return #AWACS self function AWACS:AddGroupToDetection(Group) @@ -1471,6 +1720,7 @@ function AWACS:_StartSettings(FlightGroup,Mission) group:SetCommandInvisible(self.invisible) group:SetCommandImmortal(self.immortal) group:CommandSetCallsign(self.CallSign,self.CallSignNo,2) + group:CommandEPLRS(self.ModernEra,5) -- Non AWACS does not seem take AWACS CS in DCS Group self.AwacsFG = AwacsFG @@ -1566,7 +1816,8 @@ function AWACS:_ToStringBULLS( Coordinate, ssml, TTS ) return string.format("%s %03d, %d",bullseyename,Bearing,Distance) elseif TTS then Bearing = self:_ToStringBullsTTS(Bearing) - return string.format("%s %s, %d",bullseyename,Bearing,Distance) + local BearingTTS = string.gsub(Bearing,"0","zero") + return string.format("%s %s, %d",bullseyename,BearingTTS,Distance) else return string.format("%s %s, %d",bullseyename,Bearing,Distance) end @@ -1594,7 +1845,7 @@ end -- @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!") + self:T(self.lid.."_GetManagedGrpID - Requested Group is not alive!") return 0,false,"" end self:T(self.lid.."_GetManagedGrpID for "..Group:GetName()) @@ -1679,6 +1930,40 @@ function AWACS:_UpdateContactFromCluster(CID) return self end +--- [Internal] Check merges for Players +-- @param #AWACS self +-- @return #AWACS self +function AWACS:_CheckMerges() + self:T(self.lid.."_CheckMerges") + for _id,_pilot in pairs (self.ManagedGrps) do + local pilot = _pilot -- #AWACS.ManagedGroup + if pilot.Group and pilot.Group:IsAlive() then + local ppos = pilot.Group:GetCoordinate() + local pcallsign = pilot.CallSign + self:T(self.lid.."Checking for "..pcallsign) + if ppos then + self.Contacts:ForEach( + function (Contact) + local contact = Contact -- #AWACS.ManagedContact + local cpos = contact.Cluster.coordinate or contact.Contact.position or contact.Contact.group:GetCoordinate() + local dist = ppos:Get2DDistance(cpos) + local distnm = UTILS.Round(UTILS.MetersToNM(dist),0) + if (pilot.IsPlayer or self.debug) and distnm <= 5 and not contact.MergeCallDone then + local label = contact.EngagementTag or "" + if not contact.MergeCallDone or not string.find(label,pcallsign) then + self:T(self.lid.."Merged") + self:_MergedCall(_id) + contact.MergeCallDone = true + end + end + end + ) + end + end + end + return self +end + --- [Internal] Clean up contacts list -- @param #AWACS self -- @return #AWACS self @@ -1699,25 +1984,15 @@ function AWACS:_CleanUpContacts() --local aliveclusters = FIFO:New() -- announce VANISHED - if deadcontacts:Count() > 0 then + if deadcontacts:Count() > 0 and (not self.NoGroupTags) 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 @@ -1984,23 +2259,23 @@ function AWACS:_CreatePicture(AO,Callsign,GID,MaxEntries,IsGeneral) end local refBRAA = "" local refBRAATTS = "" - text = contact.TargetGroupNaming.." group." -- Alpha Group. - textScreen = contact.TargetGroupNaming.." group," - - if IsGeneral then - -- AO/BE Reference + + if self.NoGroupTags then + text = "Group." -- Alpha Group. + textScreen = "Group," + else + text = contact.TargetGroupNaming.." group." -- Alpha Group. + textScreen = contact.TargetGroupNaming.." group," + end + + if IsGeneral or not self.PlayerGuidance then refBRAA=self:_ToStringBULLS(coordinate) - if self.PathToGoogleKey then - refBRAATTS = self:_ToStringBULLS(coordinate, true) - else - --refBRAATTS = self:__ToStringBullsTTS(refBRAA) - refBRAATTS = self:_ToStringBULLS(coordinate,false,true) - end + refBRAATTS = self:_ToStringBULLS(coordinate, false, true) local alt = contact.Contact.group:GetAltitude() or 8000 alt = UTILS.Round(UTILS.MetersToFeet(alt)/1000,0) -- Alpha Group. Bulls eye 0 2 1, 16 miles, 25 thousand. text = text .. " "..refBRAATTS.." miles, "..alt.." thousand." -- Alpha Group. Bulls eye 0 2 1, 16 miles, 25 thousand. - textScreen = textScreen .. " "..refBRAA.." miles, "..alt.." thousand." -- Alpha Group, Bullseye 021, 16 miles, 25 thousand, + textScreen = textScreen .. " "..refBRAA.." miles, "..alt.." thousand." -- Alpha Group, Bullseye 021, 16 miles, 25 thousand, else -- pilot reference refBRAA = coordinate:ToStringBRAANATO(groupcoord,true,true) @@ -2008,7 +2283,7 @@ function AWACS:_CreatePicture(AO,Callsign,GID,MaxEntries,IsGeneral) 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) + refBRAATTS = coordinate:ToStringBRAANATO(groupcoord,true,true,true,false,true) end if contact.IFF ~= AWACS.IFF.BOGEY then refBRAA = string.gsub(refBRAA,"Bogey", contact.IFF) @@ -2021,12 +2296,6 @@ function AWACS:_CreatePicture(AO,Callsign,GID,MaxEntries,IsGeneral) -- 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) @@ -2038,7 +2307,7 @@ function AWACS:_CreatePicture(AO,Callsign,GID,MaxEntries,IsGeneral) -- 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. + text = text .. " "..contact.EngagementTag -- Alpha Group. Bulls eye 0 2 1, 16. Heavy. Targeted by Jazz 1 1. textScreen = textScreen .. " "..contact.EngagementTag -- Alpha Group, Bullseye 021, 16, Flanking. Targeted by Jazz 1 1. end @@ -2074,7 +2343,8 @@ function AWACS:_CreateBogeyDope(Callsign,GID) local groupcoord = group:GetCoordinate() local fifo = self.ContactsAO -- Utilities.FiFo#FIFO - local maxentries = self.maxspeakentries + --local maxentries = self.maxspeakentries + local maxentries = 1 local counter = 0 local entries = fifo:GetSize() @@ -2115,15 +2385,15 @@ function AWACS:_Picture(Group,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 Group and Outcome then + general = false + end if not self.intel then -- no intel yet! @@ -2190,13 +2460,13 @@ function AWACS:_Picture(Group,IsGeneral) 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 + --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 + --end if clustersAO == 1 then text = text .. "One group. " textScreen = textScreen .. "One group.\n" @@ -2235,7 +2505,7 @@ function AWACS:_BogeyDope(Group) 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) + self:_NewRadioEntry(text,text,0,false,true,true,false,true) return self end @@ -2277,7 +2547,7 @@ function AWACS:_BogeyDope(Group) text = string.format("%s. %s. Clean.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) - self:_NewRadioEntry(text,textScreen,GID,Outcome,Outcome,true,false) + self:_NewRadioEntry(text,textScreen,GID,Outcome,Outcome,true,false,true) else @@ -2291,7 +2561,7 @@ function AWACS:_BogeyDope(Group) textScreen = textScreen .. contactsAO .. " groups.\n" end - self:_NewRadioEntry(text,textScreen,GID,Outcome,true,true,false) + self:_NewRadioEntry(text,textScreen,GID,Outcome,true,true,false,true) self:_CreateBogeyDope(self:_GetCallSign(Group,GID) or "Ghost 1",GID) end @@ -2336,7 +2606,7 @@ end -- @param #string Declaration Text declaration the player used -- @return #AWACS self function AWACS:_VID(Group,Declaration) - self:I(self.lid.."_VID") + self:T(self.lid.."_VID") local GID, Outcome, Callsign = self:_GetManagedGrpID(Group) local text = "" @@ -2370,7 +2640,7 @@ function AWACS:_VID(Group,Declaration) distance = UTILS.Round(distance,0) + 1 if distance <= radius or self.debug then -- we can VID - self:I("Contact VID as "..Declaration) + self:T("Contact VID as "..Declaration) -- update cluster.IFF = Declaration task.Status = AWACS.TaskStatus.SUCCESS @@ -2379,12 +2649,12 @@ function AWACS:_VID(Group,Declaration) 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) + self:T(text) else -- too far away - self:I("Contact VID not close enough") + self:T("Contact VID not close enough") text = string.format("%s. %s. Negative, get closer to target.",Callsign,self.callsigntxt) - self:I(text) + self:T(text) end self:_NewRadioEntry(text,text,GID,Outcome,true,true,false,true) end @@ -2758,14 +3028,20 @@ function AWACS:_CheckIn(Group) self.ManagedGrps[self.ManagedGrpID]=managedgroup local alphacheckbulls = self:_ToStringBULLS(Group:GetCoordinate()) - local alphacheckbullstts = self:_ToStringBullsTTS(alphacheckbulls)-- make tts friendly + --local alphacheckbullstts = self:_ToStringBullsTTS(alphacheckbulls)-- make tts friendly + local alphacheckbullstts = self:_ToStringBULLS(Group:GetCoordinate(),false,true) self.ManagedGrps[self.ManagedGrpID]=managedgroup text = string.format("%s. %s. Alpha Check. %s",managedgroup.CallSign,self.callsigntxt,alphacheckbulls) textTTS = string.format("%s. %s. Alpha Check. %s",managedgroup.CallSign,self.callsigntxt,alphacheckbullstts) self:__CheckedIn(1,managedgroup.GID) - self:__AssignAnchor(5,managedgroup.GID) + + if self.PlayerStationName then + self:__AssignAnchor(5,managedgroup.GID,true,self.PlayerStationName) + else + self:__AssignAnchor(5,managedgroup.GID) + end elseif self.AwacsFG then text = string.format("%s. %s. Negative. You are already checked in.",self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) @@ -2826,8 +3102,8 @@ function AWACS:_CheckInAI(FlightGroup,Group,AuftragsNr) self:_NewRadioEntry(text,text,managedgroup.GID,Outcome,false,true,true) - local alphacheckbulls = self:_ToStringBULLS(Group:GetCoordinate()) - alphacheckbulls = self:_ToStringBullsTTS(alphacheckbulls)-- make tts friendly + local alphacheckbulls = self:_ToStringBULLS(Group:GetCoordinate(),false,true) + --alphacheckbulls = self:_ToStringBullsTTS(alphacheckbulls)-- make tts friendly text = string.format("%s. %s. Alpha Check. %s",managedgroup.CallSign,self.callsigntxt,alphacheckbulls) self:__CheckedIn(1,managedgroup.GID) @@ -2879,6 +3155,7 @@ function AWACS:_CheckOut(Group,GID,dead) -- delete open tasks if managedgroup.CurrentTask and managedgroup.CurrentTask > 0 then self.ManagedTasks:PullByID(managedgroup.CurrentTask ) + self:_UpdateContactEngagementTag(managedgroup.ContactCID,"",false,false) end self.ManagedGrps[GID] = nil self:__CheckedOut(1,GID,Stack,Angels) @@ -2910,8 +3187,11 @@ function AWACS:_SetClientMenus() -- go through set and build the menu local grp = _group -- Wrapper.Client#CLIENT local cgrp = grp:GetGroup() - local cgrpname = cgrp:GetName() - self:T(cgrpname) + local cgrpname = nil + if cgrp and cgrp:IsAlive() then + cgrpname = cgrp:GetName() + self:T(cgrpname) + end --cgrpname = string.match(cgrpname,"([%a%s]+)#") if self.MenuStrict then -- check if pilot has checked in @@ -3049,10 +3329,101 @@ function AWACS:_SetClientMenus() return self end +--- [Internal] AWACS Delete a new Anchor Stack from a Marker - only works if no assignments are on the station +-- @param #AWACS self +-- @return #AWACS self +function AWACS:_DeleteAnchorStackFromMarker(Name,Coord) + self:I(self.lid.."_DeleteAnchorStackFromMarker") + if self.AnchorStacks:HasUniqueID(Name) and self.PlayerStationName == Name then + local stack = self.AnchorStacks:ReadByID(Name) -- #AWACS.AnchorData + local marker = stack.AnchorMarker + if stack.AnchorAssignedID:Count() == 0 then + marker:Remove() + if self.debug then + stack.StationZone:UndrawZone() + end + self.AnchorStacks:PullByID(Name) + self.PlayerStationName = nil + else + if self.debug then + self:I(self.lid.."**** Cannot delete station, there are CAPs assigned!") + local text = marker:GetText() + marker:TextUpdate(text.."\nMarked for deletion") + end + end + end + return self +end + +--- [Internal] AWACS Move a new Anchor Stack from a Marker +-- @param #AWACS self +-- @return #AWACS self +function AWACS:_MoveAnchorStackFromMarker(Name,Coord) + self:I(self.lid.."_MoveAnchorStackFromMarker") + if self.AnchorStacks:HasUniqueID(Name) and self.PlayerStationName == Name then + local station = self.AnchorStacks:PullByID(Name) -- #AWACS.AnchorData + local stationtag = string.format("Station: %s\nCoordinate: %s",Name,Coord:ToStringLLDDM()) + local marker = station.AnchorMarker + local zone = station.StationZone + if self.debug then + zone:UndrawZone() + end + local radius = self.StationZone:GetRadius() + if radius < 10000 then radius = 10000 end + station.StationZone = ZONE_RADIUS:New(Name, Coord:GetVec2(), radius) + marker:UpdateCoordinate(Coord) + marker:UpdateText(stationtag) + station.AnchorMarker = marker + if self.debug then + station.StationZone:DrawZone(-1,{0,0,1},1,{0,0,1},0.2,5,true) + end + self.AnchorStacks:Push(station,Name) + end + return self +end + +--- [Internal] AWACS Create a new Anchor Stack from a Marker - this then is the preferred station for players +-- @param #AWACS self +-- @return #AWACS self +function AWACS:_CreateAnchorStackFromMarker(Name,Coord) + self:I(self.lid.."_CreateAnchorStackFromMarker") + local AnchorStackOne = {} -- #AWACS.AnchorData + AnchorStackOne.AnchorBaseAngels = self.AnchorBaseAngels + AnchorStackOne.Anchors = FIFO:New() -- Utilities.FiFo#FIFO + AnchorStackOne.AnchorAssignedID = FIFO:New() -- Utilities.FiFo#FIFO + + local newname = Name + + for i=1,self.AnchorMaxStacks do + AnchorStackOne.Anchors:Push((i-1)*self.AnchorStackDistance+self.AnchorBaseAngels) + end + local radius = self.StationZone:GetRadius() + if radius < 10000 then radius = 10000 end + AnchorStackOne.StationZone = ZONE_RADIUS:New(newname, Coord:GetVec2(), radius) + AnchorStackOne.StationZoneCoordinate = Coord + AnchorStackOne.StationZoneCoordinateText = Coord:ToStringLLDDM() + AnchorStackOne.StationName = newname + + --push to AnchorStacks + if self.debug then + AnchorStackOne.StationZone:DrawZone(-1,{0,0,1},1,{0,0,1},0.2,5,true) + local stationtag = string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM()) + AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToAll() + else + local stationtag = string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM()) + AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToAll() + end + + self.AnchorStacks:Push(AnchorStackOne,newname) + self.PlayerStationName = newname + + return self +end + --- [Internal] AWACS Create a new Anchor Stack -- @param #AWACS self -- @return #boolean success --- @return #nunber AnchorStackNo +-- @return #number AnchorStackNo function AWACS:_CreateAnchorStack() self:T(self.lid.."_CreateAnchorStack") local stackscreated = self.AnchorStacks:GetSize() @@ -3095,14 +3466,14 @@ function AWACS:_CreateAnchorStack() 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()) + local radius = self.StationZone:GetRadius() + if radius < 10000 then radius = 10000 end + AnchorStackOne.StationZone = ZONE_RADIUS:New(newname, newanchorbasecoord:GetVec2(), radius) AnchorStackOne.StationZoneCoordinate = newanchorbasecoord AnchorStackOne.StationZoneCoordinateText = newanchorbasecoord:ToStringLLDDM() AnchorStackOne.StationName = newname @@ -3356,14 +3727,16 @@ function AWACS:_ToStringBR(FromCoordinate,ToCoordinate) local AngleDegText = string.format("%03d",AngleDegrees) -- 051 local AngleDegTextTTS = "" - if self.PathToGoogleKey then - AngleDegTextTTS = string.format("%s",AngleDegText) - else - AngleDegTextTTS = string.format("%s",AngleDegText) - end + --if self.PathToGoogleKey then + --AngleDegTextTTS = string.format("%s",AngleDegText) + --else + --AngleDegTextTTS = string.format("%s",AngleDegText) + --end AngleDegText = string.gsub(AngleDegText,"%d","%1 ") -- "0 5 1 " AngleDegText = string.gsub(AngleDegText," $","") -- "0 5 1" + AngleDegTextTTS = string.gsub(AngleDegText,"0","zero") + local Distance = ToCoordinate:Get2DDistance( FromCoordinate ) --meters local distancenm = UTILS.Round(UTILS.MetersToNM(Distance),0) @@ -3395,15 +3768,25 @@ function AWACS:_ToStringBRA(FromCoordinate,ToCoordinate,Altitude) local AngleDegrees = UTILS.Round( UTILS.ToDegree( AngleRadians ), 0 ) -- degrees local AngleDegText = string.format("%03d",AngleDegrees) -- 051 - local AngleDegTextTTS = string.format("%s",AngleDegText) + --local AngleDegTextTTS = string.format("%s",AngleDegText) + AngleDegText = string.gsub(AngleDegText,"%d","%1 ") -- "0 5 1 " AngleDegText = string.gsub(AngleDegText," $","") -- "0 5 1" + local AngleDegTextTTS = string.gsub(AngleDegText,"0","zero") local Distance = ToCoordinate:Get2DDistance( FromCoordinate ) --meters local distancenm = UTILS.Round(UTILS.MetersToNM(Distance),0) - 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) + if altitude >= 1 then + BRText = string.format("%03d, %d miles, %d thousand",AngleDegrees,distancenm,altitude) + BRTextTTS = string.format("%s, %d miles, %d thousand",AngleDegText,distancenm,altitude) + if self.PathToGoogleKey then + BRTextTTS = string.format("%s, %d miles, %d thousand",AngleDegTextTTS,distancenm,altitude) + end + else + BRText = string.format("%03d, %d miles, very low",AngleDegrees,distancenm) + BRTextTTS = string.format("%s, %d miles, very low",AngleDegText,distancenm) + if self.PathToGoogleKey then + BRTextTTS = string.format("%s, %d miles, very low",AngleDegTextTTS,distancenm) + end end self:T(BRText,BRTextTTS) return BRText,BRTextTTS @@ -3603,7 +3986,7 @@ end -- @param #AWACS self -- @return #AWACS self function AWACS:_CheckTaskQueue() - self:I(self.lid.."_CheckTaskQueue") + self:T(self.lid.."_CheckTaskQueue") local opentasks = 0 local assignedtasks = 0 @@ -3626,7 +4009,7 @@ function AWACS:_CheckTaskQueue() if self.ManagedTasks:IsNotEmpty() then opentasks = self.ManagedTasks:GetSize() - self:I("Assigned Tasks: " .. opentasks) + self:T("Assigned Tasks: " .. opentasks) local taskstack = self.ManagedTasks:GetPointerStack() for _id,_entry in pairs(taskstack) do local data = _entry -- Utilities.FiFo#FIFO.IDEntry @@ -3634,7 +4017,7 @@ function AWACS:_CheckTaskQueue() 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") + self:T("Open Task ANCHOR/REANCHOR") -- see if we have reached the anchor zone local managedgroup = self.ManagedGrps[entry.AssignedGroupID] -- #AWACS.ManagedGroup if managedgroup then @@ -3644,7 +4027,7 @@ function AWACS:_CheckTaskQueue() 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) + self:T("Open Task ANCHOR/REANCHOR success for GroupID "..entry.AssignedGroupID) -- made it target:Stop() -- add group to idle stack @@ -3661,7 +4044,7 @@ function AWACS:_CheckTaskQueue() self.ManagedTasks:PullByID(entry.TID) else --inzone -- not there yet - self:I("Open Task ANCHOR/REANCHOR executing for GroupID "..entry.AssignedGroupID) + self:T("Open Task ANCHOR/REANCHOR executing for GroupID "..entry.AssignedGroupID) end else -- group dead, pull task @@ -3675,7 +4058,7 @@ function AWACS:_CheckTaskQueue() elseif description == AWACS.TaskDescription.INTERCEPT then -- DONE - self:I("Open Tasks INTERCEPT") + self:T("Open Tasks INTERCEPT") local taskstatus = entry.Status local targetstatus = entry.Target:GetState() @@ -3701,15 +4084,15 @@ function AWACS:_CheckTaskQueue() distance = grouposition:Get2DDistance(position) distance = UTILS.Round(UTILS.MetersToNM(distance),0) end - self:I("TAC/MELD distance check: "..distance.."NM!") + self:T("TAC/MELD distance check: "..distance.."NM!") if distance <= self.TacDistance and distance >= self.MeldDistance then -- TAC distance - self:I("TAC distance: "..distance.."NM!") + self:T("TAC distance: "..distance.."NM!") local Contact = self.Contacts:ReadByID(entry.Contact.CID) self:_TACRangeCall(entry.AssignedGroupID,Contact) elseif distance <= self.MeldDistance and distance >= self.ThreatDistance then -- MELD distance - self:I("MELD distance: "..distance.."NM!") + self:T("MELD distance: "..distance.."NM!") local Contact = self.Contacts:ReadByID(entry.Contact.CID) self:_MeldRangeCall(entry.AssignedGroupID,Contact) end @@ -3722,7 +4105,7 @@ function AWACS:_CheckTaskQueue() auftragstatus = auftrag:GetState() end local text = string.format("ID=%d | Status=%s | TargetState=%s | AuftragState=%s",entry.TID,taskstatus,targetstatus,auftragstatus) - self:I(text) + self:T(text) if auftrag then if auftrag:IsExecuting() then entry.Status = AWACS.TaskStatus.EXECUTING @@ -3780,7 +4163,7 @@ function AWACS:_CheckTaskQueue() end if entry.Status == AWACS.TaskStatus.SUCCESS then - self:I("Open Tasks INTERCEPT success for GroupID "..entry.AssignedGroupID) + self:T("Open Tasks INTERCEPT success for GroupID "..entry.AssignedGroupID) if managedgroup then self:_UpdateContactEngagementTag(managedgroup.ContactCID,"",true,true,AWACS.TaskStatus.SUCCESS) @@ -3803,7 +4186,7 @@ function AWACS:_CheckTaskQueue() end elseif entry.Status == AWACS.TaskStatus.FAILED then - self:I("Open Tasks INTERCEPT failed for GroupID "..entry.AssignedGroupID) + self:T("Open Tasks INTERCEPT failed for GroupID "..entry.AssignedGroupID) if managedgroup then managedgroup.HasAssignedTask = false self:_UpdateContactEngagementTag(managedgroup.ContactCID,"",false,false,AWACS.TaskStatus.UNASSIGNED) @@ -3828,7 +4211,7 @@ function AWACS:_CheckTaskQueue() elseif entry.Status == AWACS.TaskStatus.REQUESTED then -- requested - player tasks only! - self:I("Open Tasks INTERCEPT REQUESTED for GroupID "..entry.AssignedGroupID) + self:T("Open Tasks INTERCEPT REQUESTED for GroupID "..entry.AssignedGroupID) local created = entry.RequestedTimestamp or timer.getTime() - 120 local Tnow = timer.getTime() local Trunning = (Tnow-created) / 60 -- mins @@ -3838,7 +4221,7 @@ function AWACS:_CheckTaskQueue() entry.Status = AWACS.TaskStatus.UNASSIGNED self.ManagedTasks:PullByID(entry.TID) end - self:I(text) + self:T(text) end ---------------------------------------- @@ -3863,7 +4246,7 @@ function AWACS:_CheckTaskQueue() elseif entry.Target:IsAlive() then -- still alive -- out of zones? - self:I("Checking VID target out of bounds") + self:T("Checking VID target out of bounds") local targetpos = entry.Target:GetCoordinate() -- success == out of our controlled zones local outofzones = false @@ -3894,13 +4277,13 @@ function AWACS:_CheckTaskQueue() end if outofzones then entry.Status = AWACS.TaskStatus.SUCCESS - self:I("Out of bounds - SUCCESS") + self:T("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) + self:T("Open Tasks VID REQUESTED for GroupID "..entry.AssignedGroupID) local created = entry.RequestedTimestamp or timer.getTime() - 120 local Tnow = timer.getTime() local Trunning = (Tnow-created) / 60 -- mins @@ -3910,9 +4293,9 @@ function AWACS:_CheckTaskQueue() entry.Status = AWACS.TaskStatus.UNASSIGNED self.ManagedTasks:PullByID(entry.TID) end - self:I(text) + self:T(text) elseif entry.Status == AWACS.TaskStatus.ASSIGNED then - self:I("Open Tasks VID ASSIGNED for GroupID "..entry.AssignedGroupID) + self:T("Open Tasks VID ASSIGNED for GroupID "..entry.AssignedGroupID) -- check TAC/MELD ranges local targetgrp = entry.Contact.group local position = entry.Contact.position or entry.Cluster.coordinate @@ -3925,29 +4308,29 @@ function AWACS:_CheckTaskQueue() distance = grouposition:Get2DDistance(position) distance = UTILS.Round(UTILS.MetersToNM(distance),0) end - self:I("TAC/MELD distance check: "..distance.."NM!") + self:T("TAC/MELD distance check: "..distance.."NM!") if distance <= self.TacDistance and distance >= self.MeldDistance then -- TAC distance - self:I("TAC distance: "..distance.."NM!") + self:T("TAC distance: "..distance.."NM!") local Contact = self.Contacts:ReadByID(entry.Contact.CID) self:_TACRangeCall(entry.AssignedGroupID,Contact) elseif distance <= self.MeldDistance and distance >= self.ThreatDistance then -- MELD distance - self:I("MELD distance: "..distance.."NM!") + self:T("MELD distance: "..distance.."NM!") local Contact = self.Contacts:ReadByID(entry.Contact.CID) self:_MeldRangeCall(entry.AssignedGroupID,Contact) end end end elseif entry.Status == AWACS.TaskStatus.SUCCESS then - self:I("Open Tasks VID success for GroupID "..entry.AssignedGroupID) + self:T("Open Tasks VID success for GroupID "..entry.AssignedGroupID) -- outcomes - player ID'd -- target dead or left zones handled above -- target ID'd --> if hostile, assign INTERCEPT TASK self.ManagedTasks:PullByID(entry.TID) local Contact = self.Contacts:ReadByID(entry.Contact.CID) -- #AWACS.ManagedContact if Contact and (Contact.IFF == AWACS.IFF.FRIENDLY or Contact.IFF == AWACS.IFF.NEUTRAL) then - self:I("IFF outcome friendly/neutral for GroupID "..entry.AssignedGroupID) + self:T("IFF outcome friendly/neutral for GroupID "..entry.AssignedGroupID) -- nothing todo, re-anchor if managedgroup then managedgroup.HasAssignedTask = false @@ -3967,7 +4350,7 @@ function AWACS:_CheckTaskQueue() self:__ReAnchor(5,managedgroup.GID) end elseif Contact and Contact.IFF == AWACS.IFF.ENEMY then - self:I("IFF outcome hostile for GroupID "..entry.AssignedGroupID) + self:T("IFF outcome hostile for GroupID "..entry.AssignedGroupID) -- change to intercept --self.ManagedTasks:PullByID(entry.TID) entry.ToDo = AWACS.TaskDescription.INTERCEPT @@ -3978,7 +4361,7 @@ function AWACS:_CheckTaskQueue() 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) + self:T("IFF outcome target DEAD for GroupID "..entry.AssignedGroupID) -- nothing todo, re-anchor if managedgroup then managedgroup.HasAssignedTask = false @@ -4004,7 +4387,7 @@ function AWACS:_CheckTaskQueue() -- outcomes - player unable/abort -- Player dead managed above -- Remove task - self:I("Open Tasks VID failed for GroupID "..entry.AssignedGroupID) + self:T("Open Tasks VID failed for GroupID "..entry.AssignedGroupID) if managedgroup then managedgroup.HasAssignedTask = false self:_UpdateContactEngagementTag(managedgroup.ContactCID,"",false,false,AWACS.TaskStatus.UNASSIGNED) @@ -4128,6 +4511,9 @@ function AWACS:_AnnounceContact(Contact,IsNew,Group,IsBogeyDope,Tag,IsPopup,Repo Tag = Contact.TargetGroupNaming or "" --self:T({CID,Tag}) end + if self.NoGroupTags then + Tag = nil + end local isGroup = false local GID = 0 local grpcallsign = "Ghost 1" @@ -4162,7 +4548,7 @@ function AWACS:_AnnounceContact(Contact,IsNew,Group,IsBogeyDope,Tag,IsPopup,Repo BRAfromBullsTTS = string.gsub(BRAfromBulls,"BRAA","brah") BRAfromBullsTTS = string.gsub(BRAfromBullsTTS,"BRA","brah") if self.PathToGoogleKey then - BRAfromBullsTTS = clustercoordinate:ToStringBRAANATO(Group:GetCoordinate(),true,true,true) + BRAfromBullsTTS = clustercoordinate:ToStringBRAANATO(Group:GetCoordinate(),true,true,true,false,true) end end @@ -4180,7 +4566,7 @@ function AWACS:_AnnounceContact(Contact,IsNew,Group,IsBogeyDope,Tag,IsPopup,Repo TextScreen = string.format("%s.",self.callsigntxt) end - if IsNew then + if IsNew and self.PlayerGuidance then BRAText = BRAText .. " New group." TextScreen = TextScreen .. " New group." elseif IsPopup then @@ -4242,8 +4628,8 @@ function AWACS:_AnnounceContact(Contact,IsNew,Group,IsBogeyDope,Tag,IsPopup,Repo string.gsub(BRAText,"BRA","brah") --self:T(BRAText) - - self:_NewRadioEntry(BRAText,TextScreen,GID,isGroup,true,IsNew,false,IsNew) + local prio = IsNew or IsBogeyDope + self:_NewRadioEntry(BRAText,TextScreen,GID,isGroup,true,IsNew,false,prio) return self end @@ -4541,8 +4927,9 @@ end -- @param #AWACS.ManagedContact Contact -- @return #AWACS self function AWACS:_TACRangeCall(GID,Contact) - self:I(self.lid.."_TACRangeCall") + self:T(self.lid.."_TACRangeCall") -- AIC: “Enforcer 11, single group, 30 miles.” + if not Contact then return self end local pilotcallsign = self:_GetCallSign(nil,GID) local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup local contact = Contact.Contact -- Ops.Intelligence#INTEL.Contact @@ -4566,7 +4953,8 @@ end -- @param #AWACS.ManagedContact Contact -- @return #AWACS self function AWACS:_MeldRangeCall(GID,Contact) - self:I(self.lid.."_MeldRangeCall") + self:T(self.lid.."_MeldRangeCall") + if not Contact then return self end -- AIC: “Heat 11, single group, BRAA 089/28, 32 thousand, hot, hostile, crow.” local pilotcallsign = self:_GetCallSign(nil,GID) local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup @@ -4578,7 +4966,7 @@ function AWACS:_MeldRangeCall(GID,Contact) if position then local BRATExt = "" if self.PathToGoogleKey then - BRATExt = position:ToStringBRAANATO(flightpos,false,false,true) + BRATExt = position:ToStringBRAANATO(flightpos,false,false,true,false,true) else BRATExt = position:ToStringBRAANATO(flightpos,false,false) end @@ -4594,7 +4982,8 @@ end -- @param #AWACS self -- @return #AWACS self function AWACS:_ThreatRangeCall(GID,Contact) - self:I(self.lid.."_ThreatRangeCall") + self:T(self.lid.."_ThreatRangeCall") + if not Contact then return self end -- AIC: “Enforcer 11 12, east group, THREAT, BRAA 260/15, 29 thousand, hot, hostile, robin.” local pilotcallsign = self:_GetCallSign(nil,GID) local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup @@ -4606,7 +4995,7 @@ function AWACS:_ThreatRangeCall(GID,Contact) if position then local BRATExt = "" if self.PathToGoogleKey then - BRATExt = position:ToStringBRAANATO(flightpos,false,false,true) + BRATExt = position:ToStringBRAANATO(flightpos,false,false,true,false,true) else BRATExt = position:ToStringBRAANATO(flightpos,false,false) end @@ -4617,13 +5006,27 @@ function AWACS:_ThreatRangeCall(GID,Contact) return self end +--- [Internal] Merged Call to Pilot +-- @param #AWACS self +-- @param #number GID +-- @return #AWACS self +function AWACS:_MergedCall(GID) + self:T(self.lid.."_MergedCall") + -- AIC: “Enforcer, mergedb” + local pilotcallsign = self:_GetCallSign(nil,GID) + --local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + local text = string.format("%s. %s. Merged.",self.callsigntxt,pilotcallsign) + self:_NewRadioEntry(text,text,GID,true,self.debug,true,false,true) + return self +end + --- [Internal] Assign a Pilot to a target -- @param #AWACS self -- @param #table Pilots Table of #AWACS.ManagedGroup Pilot -- @param Utilities.FiFo#FIFO Targets FiFo of #AWACS.ManagedContact Targets -- @return #AWACS self function AWACS:_AssignPilotToTarget(Pilots,Targets) - self:I(self.lid.."_AssignPilotToTarget") + self:T(self.lid.."_AssignPilotToTarget") local inreach = false local Pilot = nil -- #AWACS.ManagedGroup @@ -4641,7 +5044,7 @@ function AWACS:_AssignPilotToTarget(Pilots,Targets) 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)) + self:T(string.format("%sTarget distance %d! Assignment %s!",self.lid,UTILS.Round(UTILS.MetersToNM(targetdist),0),_Pilot.CallSign)) inreach = true closest = targetdist Pilot = _Pilot @@ -4649,7 +5052,7 @@ function AWACS:_AssignPilotToTarget(Pilots,Targets) Targets:PullByID(_target.CID) break else - self:I(self.lid .. "Target distance > "..self.maxassigndistance.."NM! No Assignment!") + self:T(self.lid .. "Target distance > "..self.maxassigndistance.."NM! No Assignment!") end end end @@ -4689,8 +5092,8 @@ function AWACS:_AssignPilotToTarget(Pilots,Targets) 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) + local text = string.format("%s. %s group. %s. %s, request commit.", self.callsigntxt,Target.TargetGroupNaming,TargetDirectionsTTS,Pilot.CallSign) + local textScreen = string.format("%s. %s group. %s. %s, request commit.", self.callsigntxt,Target.TargetGroupNaming,TargetDirections,Pilot.CallSign) self:_NewRadioEntry(text,textScreen,Pilot.GID,true,self.debug,true,false,true) @@ -4698,15 +5101,15 @@ function AWACS:_AssignPilotToTarget(Pilots,Targets) -- Target information local callsign = Pilot.CallSign local FGStatus = Pilot.FlightGroup:GetState() - self:I("Pilot AI Callsign: " .. callsign) - self:I("Pilot FG State: " .. FGStatus) + self:T("Pilot AI Callsign: " .. callsign) + self:T("Pilot FG State: " .. FGStatus) local targetstatus = Target.Target:GetState() - self:I("Target State: " .. targetstatus) + self:T("Target State: " .. targetstatus) -- local currmission = Pilot.FlightGroup:GetMissionCurrent() if currmission then - self:I("Current Mission: " .. currmission:GetType()) + self:T("Current Mission: " .. currmission:GetType()) end -- create one intercept Auftrag and one to return to CAP post this one local ZoneSet = self.ZoneSet @@ -4807,8 +5210,8 @@ function AWACS:_AssignPilotToTarget(Pilots,Targets) 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) + local text = string.format("%s. %s group. %s. %s, commit.", self.callsigntxt,Target.TargetGroupNaming,bratexttts,Pilot.CallSign) + local textScreen = string.format("%s. %s group. %s. %s, request commit.", self.callsigntxt,Target.TargetGroupNaming,bratext,Pilot.CallSign) self:_NewRadioEntry(text,textScreen,Pilot.GID,true,self.debug,true,false,true) @@ -4890,6 +5293,55 @@ function AWACS:onafterStart(From, Event, To) self.ZoneSet = ZoneSet self.RejectZoneSet = RejectZoneSet + if self.AllowMarkers then + -- Add MarkerOps + + local MarkerOps = MARKEROPS_BASE:New("AWACS",{"Station","Delete","Move"}) + + local function Handler(Keywords,Coord,Text) + self:I(Text) + for _,_word in pairs (Keywords) do + if string.lower(_word) == "station" then + -- get the station name from the text field + local Name = string.match(Text," ([%a]+)$") + self:_CreateAnchorStackFromMarker(Name,Coord) + break + elseif string.lower(_word) == "delete" then + -- get the station name from the text field + local Name = string.match(Text," ([%a]+)$") + self:_DeleteAnchorStackFromMarker(Name,Coord) + break + elseif string.lower(_word) == "move" then + -- get the station name from the text field + local Name = string.match(Text," ([%a]+)$") + self:_MoveAnchorStackFromMarker(Name,Coord) + break + end + end + end + + -- Event functions + function MarkerOps:OnAfterMarkAdded(From,Event,To,Text,Keywords,Coord) + --local m = MESSAGE:New(string.format("AWACS %s Mark Added.", self.Tag),10,"Info",true):ToAllIf(self.debug) + BASE:I(string.format("%s Mark Added.", self.Tag)) + Handler(Keywords,Coord,Text) + end + + function MarkerOps:OnAfterMarkChanged(From,Event,To,Text,Keywords,Coord) + BASE:I(string.format("%s Mark Changed.", self.Tag)) + --local m = MESSAGE:New(string.format("AWACS %s Mark Changed.", self.Tag),10,"Info",true):ToAllIf(self.debug) + Handler(Keywords,Coord,Text) + end + + function MarkerOps:OnAfterMarkDeleted(From,Event,To) + BASE:I(string.format("%s Mark Deleted.", self.Tag)) + --local m = MESSAGE:New(string.format("AWACS %s Mark Deleted.", self.Tag),10,"Info",true):ToAllIf(self.debug) + end + + self.MarkerOps = MarkerOps + + end + self:__Status(-30) return self end @@ -5157,6 +5609,8 @@ function AWACS:onafterStatus(From, Event, To) self:_CleanUpContacts() + self:_CheckMerges() + if self.debug then --local outcome, targets = self:_TargetSelectionProcess() -- TODO for debug ATM end @@ -5167,7 +5621,7 @@ function AWACS:onafterStatus(From, Event, To) 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 + if outcome and #Humans > 0 and self.PlayerCapAssigment then -- add a task for AI self:_AssignPilotToTarget(Humans,targets) end @@ -5228,6 +5682,20 @@ function AWACS:onafterStop(From, Event, To) SubAW:RemoveUsingOpsAwacs() end end + -- Events + -- Player joins + self:UnHandleEvent(EVENTS.PlayerEnterAircraft) + self:UnHandleEvent(EVENTS.PlayerEnterUnit) + -- Player leaves + self:UnHandleEvent(EVENTS.PlayerLeaveUnit) + self:UnHandleEvent(EVENTS.Ejection) + self:UnHandleEvent(EVENTS.Crash) + self:UnHandleEvent(EVENTS.Dead) + self:UnHandleEvent(EVENTS.UnitLost) + self:UnHandleEvent(EVENTS.BDA) + self:UnHandleEvent(EVENTS.PilotDead) + -- Missile warning + self:UnHandleEvent(EVENTS.Shot) return self end @@ -5294,9 +5762,9 @@ function AWACS:onafterAssignedAnchor(From, Event, To, GID, Anchor, AnchorStackNo 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 textTTS = string.format("%s. %s. Station at %s at angels %d doing %d knots.",CallSign,self.callsigntxt,AnchorName,Angels,AnchorSpeed) + local ROEROT = self.AwacsROE..", "..self.AwacsROT + local textScreen = string.format("%s. %s.\nStation at %s\nAngels %d\nSpeed %d knots\nCoord %s\nROE %s.",CallSign,self.callsigntxt,AnchorName,Angels,AnchorSpeed,AnchorCoordTxt,ROEROT) local TextTasking = string.format("Station at %s\nAngels %d\nSpeed %d knots\nCoord %s\nROE %s",AnchorName,Angels,AnchorSpeed,AnchorCoordTxt,ROEROT) self:_NewRadioEntry(textTTS,textScreen,GID,isPlayer,isPlayer,true,false) @@ -5351,6 +5819,9 @@ function AWACS:onafterNewCluster(From,Event,To,Cluster) end local Contact = GetFirstAliveContact(ContactTable) -- Ops.Intelligence#INTEL.Contact + + if not Contact then return self end + local targetset = SET_GROUP:New() -- SET for TARGET for _,_grp in pairs(ContactTable) do @@ -5473,7 +5944,7 @@ function AWACS:onafterCheckRadioQueue(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 + if (self.RadioQueue:IsNotEmpty() or self.PrioRadioQueue:IsNotEmpty()) then local RadioEntry = nil @@ -5484,6 +5955,12 @@ function AWACS:onafterCheckRadioQueue(From,Event,To) end self:T({RadioEntry}) + if self.clientset:CountAlive() == 0 then + self:I(self.lid.."No player connected.") + self:__CheckRadioQueue(-5) + return self + end + if not RadioEntry.FromAI then -- AI AWACS Speaking if self.PathToGoogleKey then @@ -5528,7 +6005,12 @@ function AWACS:onafterCheckRadioQueue(From,Event,To) if self:Is("Running") then -- exit if stopped - self:__CheckRadioQueue(nextcall+2) + if self.PathToGoogleKey then + nextcall = nextcall + self.GoogleTTSPadding + else + nextcall = nextcall + self.WindowsTTSPadding + end + self:__CheckRadioQueue(-nextcall) end return self end @@ -5684,10 +6166,10 @@ function AWACS:onafterReAnchor(From, Event, To, GID) 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 + local brtexttts = self:_ToStringBULLS(lastknown,false,true) + --if self.PathToGoogleKey then + --brtexttts = self:_ToStringBULLS(lastknown,true) + --end text = text .. " "..brtexttts.." miles." textScreen = textScreen .. " "..brtext.." miles." @@ -5735,20 +6217,19 @@ function AWACS:onafterReAnchor(From, Event, To, GID) 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 + local brtexttts = self:_ToStringBULLS(lastknown,false,true) + --if self.PathToGoogleKey then + --brtexttts = self:_ToStringBULLS(lastknown,true) + --end text = text .. " "..brtexttts.." miles." textScreen = textScreen .. " "..brtext.." miles." - + self:_NewRadioEntry(text,textScreen,0,false,self.debug,true,false,true) end self.ManagedGrps[GID] = nil end end end - return self end end -- end do diff --git a/Moose Development/Moose/Ops/CSAR.lua b/Moose Development/Moose/Ops/CSAR.lua index a213f5632..76281a3f1 100644 --- a/Moose Development/Moose/Ops/CSAR.lua +++ b/Moose Development/Moose/Ops/CSAR.lua @@ -30,7 +30,7 @@ -- @module Ops.CSAR -- @image OPS_CSAR.jpg --- Date: Feb 2022 +-- Date: June 2022 ------------------------------------------------------------------------- --- **CSAR** class, extends Core.Base#BASE, Core.Fsm#FSM @@ -76,65 +76,69 @@ -- -- The following options are available (with their defaults). Only set the ones you want changed: -- --- self.allowDownedPilotCAcontrol = false -- Set to false if you don\'t want to allow control by Combined Arms. --- self.allowFARPRescue = true -- allows pilots to be rescued by landing at a FARP or Airbase. Else MASH only! --- self.FARPRescueDistance = 1000 -- you need to be this close to a FARP or Airport for the pilot to be rescued. --- self.autosmoke = false -- automatically smoke a downed pilot\'s location when a heli is near. --- self.autosmokedistance = 1000 -- distance for autosmoke --- self.coordtype = 1 -- Use Lat/Long DDM (0), Lat/Long DMS (1), MGRS (2), Bullseye imperial (3) or Bullseye metric (4) for coordinates. --- self.csarOncrash = false -- (WIP) If set to true, will generate a downed pilot when a plane crashes as well. --- self.enableForAI = false -- set to false to disable AI pilots from being rescued. --- self.pilotRuntoExtractPoint = true -- Downed pilot will run to the rescue helicopter up to self.extractDistance in meters. --- self.extractDistance = 500 -- Distance the downed pilot will start to run to the rescue helicopter. --- self.immortalcrew = true -- Set to true to make wounded crew immortal. --- self.invisiblecrew = false -- Set to true to make wounded crew insvisible. --- self.loadDistance = 75 -- configure distance for pilots to get into helicopter in meters. --- self.mashprefix = {"MASH"} -- prefixes of #GROUP objects used as MASHes. --- self.max_units = 6 -- max number of pilots that can be carried if #CSAR.AircraftType is undefined. --- self.messageTime = 15 -- Time to show messages for in seconds. Doubled for long messages. --- self.radioSound = "beacon.ogg" -- the name of the sound file to use for the pilots\' radio beacons. --- self.smokecolor = 4 -- Color of smokemarker, 0 is green, 1 is red, 2 is white, 3 is orange and 4 is blue. --- self.useprefix = true -- Requires CSAR helicopter #GROUP names to have the prefix(es) defined below. --- self.csarPrefix = { "helicargo", "MEDEVAC"} -- #GROUP name prefixes used for useprefix=true - DO NOT use # in helicopter names in the Mission Editor! --- self.verbose = 0 -- set to > 1 for stats output for debugging. +-- mycsar.allowDownedPilotCAcontrol = false -- Set to false if you don\'t want to allow control by Combined Arms. +-- mycsar.allowFARPRescue = true -- allows pilots to be rescued by landing at a FARP or Airbase. Else MASH only! +-- mycsar.FARPRescueDistance = 1000 -- you need to be this close to a FARP or Airport for the pilot to be rescued. +-- mycsar.autosmoke = false -- automatically smoke a downed pilot\'s location when a heli is near. +-- mycsar.autosmokedistance = 1000 -- distance for autosmoke +-- mycsar.coordtype = 1 -- Use Lat/Long DDM (0), Lat/Long DMS (1), MGRS (2), Bullseye imperial (3) or Bullseye metric (4) for coordinates. +-- mycsar.csarOncrash = false -- (WIP) If set to true, will generate a downed pilot when a plane crashes as well. +-- mycsar.enableForAI = false -- set to false to disable AI pilots from being rescued. +-- mycsar.pilotRuntoExtractPoint = true -- Downed pilot will run to the rescue helicopter up to mycsar.extractDistance in meters. +-- mycsar.extractDistance = 500 -- Distance the downed pilot will start to run to the rescue helicopter. +-- mycsar.immortalcrew = true -- Set to true to make wounded crew immortal. +-- mycsar.invisiblecrew = false -- Set to true to make wounded crew insvisible. +-- mycsar.loadDistance = 75 -- configure distance for pilots to get into helicopter in meters. +-- mycsar.mashprefix = {"MASH"} -- prefixes of #GROUP objects used as MASHes. +-- mycsar.max_units = 6 -- max number of pilots that can be carried if #CSAR.AircraftType is undefined. +-- mycsar.messageTime = 15 -- Time to show messages for in seconds. Doubled for long messages. +-- mycsar.radioSound = "beacon.ogg" -- the name of the sound file to use for the pilots\' radio beacons. +-- mycsar.smokecolor = 4 -- Color of smokemarker, 0 is green, 1 is red, 2 is white, 3 is orange and 4 is blue. +-- mycsar.useprefix = true -- Requires CSAR helicopter #GROUP names to have the prefix(es) defined below. +-- mycsar.csarPrefix = { "helicargo", "MEDEVAC"} -- #GROUP name prefixes used for useprefix=true - DO NOT use # in helicopter names in the Mission Editor! +-- mycsar.verbose = 0 -- set to > 1 for stats output for debugging. -- -- (added 0.1.4) limit amount of downed pilots spawned by **ejection** events --- self.limitmaxdownedpilots = true --- self.maxdownedpilots = 10 +-- mycsar.limitmaxdownedpilots = true +-- mycsar.maxdownedpilots = 10 -- -- (added 0.1.8) - allow to set far/near distance for approach and optionally pilot must open doors --- self.approachdist_far = 5000 -- switch do 10 sec interval approach mode, meters --- self.approachdist_near = 3000 -- switch to 5 sec interval approach mode, meters --- self.pilotmustopendoors = false -- switch to true to enable check of open doors +-- mycsar.approachdist_far = 5000 -- switch do 10 sec interval approach mode, meters +-- mycsar.approachdist_near = 3000 -- switch to 5 sec interval approach mode, meters +-- mycsar.pilotmustopendoors = false -- switch to true to enable check of open doors -- -- (added 0.1.9) --- self.suppressmessages = false -- switch off all messaging if you want to do your own +-- mycsar.suppressmessages = false -- switch off all messaging if you want to do your own -- -- (added 0.1.11) --- self.rescuehoverheight = 20 -- max height for a hovering rescue in meters --- self.rescuehoverdistance = 10 -- max distance for a hovering rescue in meters +-- mycsar.rescuehoverheight = 20 -- max height for a hovering rescue in meters +-- mycsar.rescuehoverdistance = 10 -- max distance for a hovering rescue in meters -- -- (added 0.1.12) -- -- Country codes for spawned pilots --- self.countryblue= country.id.USA --- self.countryred = country.id.RUSSIA --- self.countryneutral = country.id.UN_PEACEKEEPERS +-- mycsar.countryblue= country.id.USA +-- mycsar.countryred = country.id.RUSSIA +-- mycsar.countryneutral = country.id.UN_PEACEKEEPERS -- -- ## 2.1 Experimental Features -- -- WARNING - Here\'ll be dragons! -- DANGER - For this to work you need to de-sanitize your mission environment (all three entries) in \Scripts\MissionScripting.lua -- Needs SRS => 1.9.6 to work (works on the **server** side of SRS) --- self.useSRS = false -- Set true to use FF\'s SRS integration --- self.SRSPath = "E:\\Progra~1\\DCS-SimpleRadio-Standalone\\" -- adjust your own path in your SRS installation -- server(!) --- self.SRSchannel = 300 -- radio channel --- self.SRSModulation = radio.modulation.AM -- modulation --- self.SRSport = 5002 -- and SRS port +-- mycsar.useSRS = false -- Set true to use FF\'s SRS integration +-- mycsar.SRSPath = "C:\\Progra~1\\DCS-SimpleRadio-Standalone\\" -- adjust your own path in your SRS installation -- server(!) +-- mycsar.SRSchannel = 300 -- radio channel +-- mycsar.SRSModulation = radio.modulation.AM -- modulation +-- mycsar.SRSport = 5002 -- and SRS Server port +-- mycsar.SRSCulture = "en-GB" -- SRS voice culture +-- mycsar.SRSVoice = nil -- SRS voice, relevant for Google TTS +-- mycsar.SRSGPathToCredentials = nil -- Path to your Google credentials json file, set this if you want to use Google TTS +-- mycsar.SRSVolume = 1 -- Volume, between 0 and 1 -- -- --- self.csarUsePara = false -- If set to true, will use the LandingAfterEjection Event instead of Ejection --shagrat --- self.wetfeettemplate = "man in floating thingy" -- if you use a mod to have a pilot in a rescue float, put the template name in here for wet feet spawns. Note: in conjunction with csarUsePara this might create dual ejected pilots in edge cases. +-- mycsar.csarUsePara = false -- If set to true, will use the LandingAfterEjection Event instead of Ejection. Requires mycsar.enableForAI to be set to true. --shagrat +-- mycsar.wetfeettemplate = "man in floating thingy" -- if you use a mod to have a pilot in a rescue float, put the template name in here for wet feet spawns. Note: in conjunction with csarUsePara this might create dual ejected pilots in edge cases. -- -- ## 3. Results -- -- Number of successful landings with save pilots and aggregated number of saved pilots is stored in these variables in the object: -- --- self.rescues -- number of successful landings *with* saved pilots --- self.rescuedpilots -- aggregated number of pilots rescued from the field (of *all* players) +-- mycsar.rescues -- number of successful landings *with* saved pilots +-- mycsar.rescuedpilots -- aggregated number of pilots rescued from the field (of *all* players) -- -- ## 4. Events -- @@ -260,7 +264,7 @@ CSAR.AircraftType["AH-64D_BLK_II"] = 2 --- CSAR class version. -- @field #string version -CSAR.version="1.0.4e" +CSAR.version="1.0.6" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list @@ -329,6 +333,7 @@ function CSAR:New(Coalition, Template, Alias) self:AddTransition("*", "Status", "*") -- CSAR status update. self:AddTransition("*", "PilotDown", "*") -- Downed Pilot added self:AddTransition("*", "Approach", "*") -- CSAR heli closing in. + self:AddTransition("*", "Landed", "*") -- CSAR heli landed self:AddTransition("*", "Boarded", "*") -- Pilot boarded. self:AddTransition("*", "Returning", "*") -- CSAR able to return to base. self:AddTransition("*", "Rescued", "*") -- Pilot at MASH. @@ -415,6 +420,10 @@ function CSAR:New(Coalition, Template, Alias) self.SRSchannel = 300 -- radio channel self.SRSModulation = radio.modulation.AM -- modulation self.SRSport = 5002 -- port + self.SRSCulture = "en-GB" + self.SRSVoice = nil + self.SRSGPathToCredentials = nil + self.SRSVolume = 1 ------------------------ --- Pseudo Functions --- @@ -466,7 +475,16 @@ function CSAR:New(Coalition, Template, Alias) -- @param #string Heliname Name of the helicopter group. -- @param #string Woundedgroupname Name of the downed pilot\'s group. - --- On After "Boarded" event. Downed pilot boarded heli. + --- On After "Landed" event. Heli landed at an airbase. + -- @function [parent=#CSAR] OnAfterLanded + -- @param #CSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string HeliName Name of the #UNIT which has landed. + -- @param Wrapper.Airbase#AIRBASE Airbase Airbase where the heli landed. + + --- On After "Boarded" event. Downed pilot boarded heli. -- @function [parent=#CSAR] OnAfterBoarded -- @param #CSAR self -- @param #string From From state. @@ -861,12 +879,12 @@ function CSAR:_EventHandler(EventData) -- no Player if self.enableForAI == false and _event.IniPlayerName == nil then - return + return self end -- no event if _event == nil or _event.initiator == nil then - return false + return self -- take off elseif _event.id == EVENTS.Takeoff then -- taken off @@ -874,35 +892,43 @@ function CSAR:_EventHandler(EventData) local _coalition = _event.IniCoalition if _coalition ~= self.coalition then - return --ignore! + return self --ignore! end if _event.IniGroupName then self.takenOff[_event.IniUnitName] = true end - return true + return self -- player enter unit elseif _event.id == EVENTS.PlayerEnterAircraft or _event.id == EVENTS.PlayerEnterUnit then --player entered unit self:T(self.lid .. " Event unit - Player Enter") local _coalition = _event.IniCoalition + self:T("Coalition = "..UTILS.GetCoalitionName(_coalition)) if _coalition ~= self.coalition then - return --ignore! + return self --ignore! end if _event.IniPlayerName then self.takenOff[_event.IniPlayerName] = nil end + -- jumped into flying plane? + self:T("Taken Off: "..tostring(_event.IniUnit:InAir(true))) + + if _event.IniUnit:InAir(true) then + self.takenOff[_event.IniPlayerName] = true + end + local _unit = _event.IniUnit local _group = _event.IniGroup if _unit:IsHelicopter() or _group:IsHelicopter() then self:_AddMedevacMenuItem() end - return true + return self elseif (_event.id == EVENTS.PilotDead and self.csarOncrash == false) then -- Pilot dead @@ -914,57 +940,68 @@ function CSAR:_EventHandler(EventData) local _group = _event.IniGroup if _unit == nil then - return -- error! + return self -- error! end local _coalition = _event.IniCoalition if _coalition ~= self.coalition then - return --ignore! + return self --ignore! end -- Catch multiple events here? if self.takenOff[_event.IniUnitName] == true or _group:IsAirborne() then if self:_DoubleEjection(_unitname) then - return + return self end else self:T(self.lid .. " Pilot has not taken off, ignore") end - return + return self elseif _event.id == EVENTS.PilotDead or _event.id == EVENTS.Ejection then if _event.id == EVENTS.PilotDead and self.csarOncrash == false then - return + return self end self:T(self.lid .. " Event unit - Pilot Ejected") local _unit = _event.IniUnit local _unitname = _event.IniUnitName local _group = _event.IniGroup + + self:T({_unit.UnitName, _unitname, _group.GroupName}) if _unit == nil then - return -- error! + self:T("Unit NIL!") + return self -- error! end - local _coalition = _unit:GetCoalition() + --local _coalition = _unit:GetCoalition() -- nil now for some reason + local _coalition = _group:GetCoalition() if _coalition ~= self.coalition then - return --ignore! + self:T("Wrong coalition! Coalition = "..UTILS.GetCoalitionName(_coalition)) + return self --ignore! end - + + + self:T("Airborne: "..tostring(_group:IsAirborne())) + self:T("Taken Off: "..tostring(self.takenOff[_event.IniUnitName])) + if not self.takenOff[_event.IniUnitName] and not _group:IsAirborne() then self:T(self.lid .. " Pilot has not taken off, ignore") - return -- give up, pilot hasnt taken off + -- return self -- give up, pilot hasnt taken off end if self:_DoubleEjection(_unitname) then - return + self:T("Double Ejection!") + return self end -- limit no of pilots in the field. if self.limitmaxdownedpilots and self:_ReachedPilotLimit() then - return + self:T("Maxed Downed Pilot!") + return self end @@ -973,33 +1010,31 @@ function CSAR:_EventHandler(EventData) local wetfeet = false - local surface = _unit:GetCoordinate():GetSurfaceType() + local initdcscoord = nil + local initcoord = nil + if _event.id == EVENTS.Ejection then + initdcscoord = _event.TgtDCSUnit:getPoint() + initcoord = COORDINATE:NewFromVec3(initdcscoord) + self:T({initdcscoord}) + else + initdcscoord = _event.IniDCSUnit:getPoint() + initcoord = COORDINATE:NewFromVec3(initdcscoord) + self:T({initdcscoord}) + end + + --local surface = _unit:GetCoordinate():GetSurfaceType() + local surface = initcoord:GetSurfaceType() + if surface == land.SurfaceType.WATER then + self:T("Wet feet!") wetfeet = true end -- all checks passed, get going. if self.csarUsePara == false or (self.csarUsePara and wetfeet ) then --shagrat check parameter LandingAfterEjection, if true don't spawn a Pilot from EJECTION event, wait for the Chute to land - local _freq = self:_GenerateADFFrequency() - self:_AddCsar(_coalition, _unit:GetCountry(), _unit:GetCoordinate() , _unit:GetTypeName(), _unit:GetName(), _event.IniPlayerName, _freq, false, "none") - return true - end - - ---- shagrat on event LANDING_AFTER_EJECTION spawn pilot at parachute location - elseif (_event.id == EVENTS.LandingAfterEjection and self.csarUsePara == true) then - self:I({EVENT=_event}) - local _LandingPos = COORDINATE:NewFromVec3(_event.initiator:getPosition().p) - local _unitname = "Aircraft" --_event.initiator:getName() or "Aircraft" --shagrat Optional use of Object name which is unfortunately 'f15_Pilot_Parachute' - local _typename = "Ejected Pilot" --_event.Initiator.getTypeName() or "Ejected Pilot" - local _country = _event.initiator:getCountry() - local _coalition = coalition.getCountryCoalition( _country ) - if _coalition == self.coalition then local _freq = self:_GenerateADFFrequency() - self:I({coalition=_coalition,country= _country, coord=_LandingPos, name=_unitname, player=_event.IniPlayerName, freq=_freq}) - self:_AddCsar(_coalition, _country, _LandingPos, nil, _unitname, _event.IniPlayerName, _freq, false, "none")--shagrat add CSAR at Parachute location. - - Unit.destroy(_event.initiator) -- shagrat remove static Pilot model - end - return true + self:_AddCsar(_coalition, _unit:GetCountry(), initcoord , _unit:GetTypeName(), _unit:GetName(), _event.IniPlayerName, _freq, false, "none") + return self + end elseif _event.id == EVENTS.Land then self:T(self.lid .. " Landing") @@ -1014,38 +1049,60 @@ function CSAR:_EventHandler(EventData) if _unit == nil then self:T(self.lid .. " Unit nil on landing") - return -- error! + return self -- error! end - local _coalition = _event.IniCoalition + --local _coalition = _event.IniCoalition + local _coalition = _event.IniGroup:GetCoalition() if _coalition ~= self.coalition then - return --ignore! + self:T(self.lid .. " Wrong coalition") + return self --ignore! end self.takenOff[_event.IniUnitName] = nil local _place = _event.Place -- Wrapper.Airbase#AIRBASE - + if _place == nil then self:T(self.lid .. " Landing Place Nil") - return -- error! + return self -- error! end -- anyone on board? if self.inTransitGroups[_event.IniUnitName] == nil then -- ignore - return + return self end if _place:GetCoalition() == self.coalition or _place:GetCoalition() == coalition.side.NEUTRAL then + self:__Landed(2,_event.IniUnitName, _place) self:_ScheduledSARFlight(_event.IniUnitName,_event.IniGroupName,true) else self:T(string.format("Airfield %d, Unit %d", _place:GetCoalition(), _unit:GetCoalition())) end end - return true + return self end + + ---- shagrat on event LANDING_AFTER_EJECTION spawn pilot at parachute location + if (_event.id == EVENTS.LandingAfterEjection and self.csarUsePara == true) then + self:T("LANDING_AFTER_EJECTION") + local _LandingPos = COORDINATE:NewFromVec3(_event.initiator:getPosition().p) + local _unitname = "Aircraft" --_event.initiator:getName() or "Aircraft" --shagrat Optional use of Object name which is unfortunately 'f15_Pilot_Parachute' + local _typename = "Ejected Pilot" --_event.Initiator.getTypeName() or "Ejected Pilot" + local _country = _event.initiator:getCountry() + local _coalition = coalition.getCountryCoalition( _country ) + self:T("Country = ".._country.." Coalition = ".._coalition) + if _coalition == self.coalition then + local _freq = self:_GenerateADFFrequency() + self:I({coalition=_coalition,country= _country, coord=_LandingPos, name=_unitname, player=_event.IniPlayerName, freq=_freq}) + self:_AddCsar(_coalition, _country, _LandingPos, nil, _unitname, _event.IniPlayerName, _freq, false, "none")--shagrat add CSAR at Parachute location. + + Unit.destroy(_event.initiator) -- shagrat remove static Pilot model + end + end + return self end @@ -1145,7 +1202,7 @@ function CSAR:_CheckWoundedGroupStatus(heliname,woundedgroupname) self.heliVisibleMessage[_lookupKeyHeli] = nil self.heliCloseMessage[_lookupKeyHeli] = nil self.landedStatus[_lookupKeyHeli] = nil - self:T("...helinunit nil!") + self:T("...heliunit nil!") return end @@ -1240,8 +1297,8 @@ function CSAR:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupNam _maxUnits = self.max_units end if _unitsInHelicopter + 1 > _maxUnits then - self:_DisplayMessageToSAR(_heliUnit, string.format("%s, %s. We\'re already crammed with %d guys! Sorry!", _pilotName, _heliName, _unitsInHelicopter, _unitsInHelicopter), self.messageTime) - return true + self:_DisplayMessageToSAR(_heliUnit, string.format("%s, %s. We\'re already crammed with %d guys! Sorry!", _pilotName, _heliName, _unitsInHelicopter, _unitsInHelicopter), self.messageTime,false,false,true) + return self end local found,downedgrouptable = self:_CheckNameInDownedPilots(_woundedGroupName) @@ -1262,7 +1319,7 @@ function CSAR:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupNam self:__Boarded(5,_heliName,_woundedGroupName) - return true + return self end --- (Internal) Move group to destination. @@ -1328,7 +1385,8 @@ function CSAR:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedG local _time = self.landedStatus[_lookupKeyHeli] if _time == nil then self.landedStatus[_lookupKeyHeli] = math.floor( (_distance - self.loadDistance) / 3.6 ) - _time = self.landedStatus[_lookupKeyHeli] + _time = self.landedStatus[_lookupKeyHeli] + _woundedGroup:OptionAlarmStateGreen() self:_OrderGroupToMoveToPoint(_woundedGroup, _heliUnit:GetCoordinate()) self:_DisplayMessageToSAR(_heliUnit, "Wait till " .. _pilotName .. " gets in. \nETA " .. _time .. " more seconds.", self.messageTime, false) else @@ -1337,24 +1395,24 @@ function CSAR:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedG end --if _time <= 0 or _distance < self.loadDistance then if _distance < self.loadDistance + 5 or _distance <= 13 then - if self.pilotmustopendoors and not self:_IsLoadingDoorOpen(_heliName) then - self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true) - return true + if self.pilotmustopendoors and (self:_IsLoadingDoorOpen(_heliName) == false) then + self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true, true) + return false else self.landedStatus[_lookupKeyHeli] = nil self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) - return false + return true end end end else if (_distance < self.loadDistance) then - if self.pilotmustopendoors and not self:_IsLoadingDoorOpen(_heliName) then - self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true) - return true + if self.pilotmustopendoors and (self:_IsLoadingDoorOpen(_heliName) == false) then + self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true, true) + return false else self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) - return false + return true end end end @@ -1391,18 +1449,19 @@ function CSAR:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedG if _time > 0 then self:_DisplayMessageToSAR(_heliUnit, "Hovering above " .. _pilotName .. ". \n\nHold hover for " .. _time .. " seconds to winch them up. \n\nIf the countdown stops you\'re too far away!", self.messageTime, true) else - if self.pilotmustopendoors and not self:_IsLoadingDoorOpen(_heliName) then - self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true) - return true + if self.pilotmustopendoors and (self:_IsLoadingDoorOpen(_heliName) == false) then + self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true, true) + return false else self.hoverStatus[_lookupKeyHeli] = nil self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) - return false + return true end end _reset = false else self:_DisplayMessageToSAR(_heliUnit, "Too high to winch " .. _pilotName .. " \nReduce height and hover for 10 seconds!", self.messageTime, true,true) + return false end end @@ -1451,7 +1510,7 @@ function CSAR:_ScheduledSARFlight(heliname,groupname, isairport) if ( _dist < self.FARPRescueDistance or isairport ) and _heliUnit:InAir() == false then if self.pilotmustopendoors and self:_IsLoadingDoorOpen(heliname) == false then - self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me out!", self.messageTime, true) + self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me out!", self.messageTime, true, true) else self:_RescuePilots(_heliUnit) return @@ -1526,6 +1585,14 @@ function CSAR:_DisplayMessageToSAR(_unit, _text, _time, _clear, _speak, _overrid local channel = self.SRSchannel local msrs = MSRS:New(path,channel,modulation) msrs:SetPort(self.SRSport) + msrs:SetLabel("CSAR") + msrs:SetCulture(self.SRSCulture) + msrs:SetCoalition(self.coalition) + msrs:SetVoice(self.SRSVoice) + if self.SRSGPathToCredentials then + msrs:SetGoogle(self.SRSGPathToCredentials) + end + msrs:SetVolume(self.SRSVolume) msrs:PlaySoundText(srstext, 2) end return self @@ -2026,7 +2093,7 @@ end -- @param #string To To state. function CSAR:onafterStart(From, Event, To) self:T({From, Event, To}) - self:I(self.lid .. "Started.") + self:I(self.lid .. "Started ("..self.version..")") -- event handler self:HandleEvent(EVENTS.Takeoff, self._EventHandler) self:HandleEvent(EVENTS.Land, self._EventHandler) @@ -2226,6 +2293,18 @@ function CSAR:onbeforePilotDown(From, Event, To, Group, Frequency, Leadername, C self:T({From, Event, To, Group, Frequency, Leadername, CoordinatesText}) return self end + +--- (Internal) Function called before Landed() event. +-- @param #CSAR self. +-- @param #string From From state. +-- @param #string Event Event triggered. +-- @param #string To To state. +-- @param #string HeliName Name of the #UNIT which has landed. +-- @param Wrapper.Airbase#AIRBASE Airbase Airbase where the heli landed. +function CSAR:onbeforeLanded(From, Event, To, HeliName, Airbase) + self:T({From, Event, To, HeliName, Airbase}) + return self +end -------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- End Ops.CSAR -------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index 4a7a5e66c..522dd1efe 100644 --- a/Moose Development/Moose/Ops/CTLD.lua +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -722,7 +722,8 @@ do -- ["Mi-24P"] = {type="Mi-24P", crates=true, troops=true, cratelimit = 2, trooplimit = 8, length = 18, cargoweightlimit = 700}, -- ["Mi-24V"] = {type="Mi-24V", crates=true, troops=true, cratelimit = 2, trooplimit = 8, length = 18, cargoweightlimit = 700}, -- ["Hercules"] = {type="Hercules", crates=true, troops=true, cratelimit = 7, trooplimit = 64, length = 25, cargoweightlimit = 19000}, --- ["UH-60L"] = {type="UH-60L", crates=true, troops=true, cratelimit = 2, trooplimit = 20, length = 16, cargoweightlimit = 3500}, +-- ["UH-60L"] = {type="UH-60L", crates=true, troops=true, cratelimit = 2, trooplimit = 20, length = 16, cargoweightlimit = 3500}, +-- ["AH-64D_BLK_II"] = {type="AH-64D_BLK_II", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 17, cargoweightlimit = 200}, -- -- ### 2.1.2 Activate and deactivate zones -- @@ -870,12 +871,13 @@ do -- -- ## 5. Support for Hercules mod by Anubis -- --- Basic support for the Hercules mod By Anubis has been build into CTLD - that is you can load/drop/build the same objects as the helicopters. --- To also cover objects and troops which can be loaded from the groud crew Rearm/Refuel menu, you need to use @{#CTLD_HERCULES.New}() and link --- this object to your CTLD setup. In this case, do **not** use the `Hercules_Cargo.lua` or `Hercules_Cargo_CTLD.lua` which are part of the mod +-- Basic support for the Hercules mod By Anubis has been build into CTLD - that is you can load/drop/build the same way and for the same objects as +-- the helicopters (main method). +-- To cover objects and troops which can be loaded from the groud crew Rearm/Refuel menu (F8), you need to use @{#CTLD_HERCULES.New}() and link +-- this object to your CTLD setup (alternative method). In this case, do **not** use the `Hercules_Cargo.lua` or `Hercules_Cargo_CTLD.lua` which are part of the mod -- in your mission! -- --- ### 5.1 Create an own CTLD instance and allow the usage of the Hercules mod: +-- ### 5.1 Create an own CTLD instance and allow the usage of the Hercules mod (main method) -- -- local my_ctld = CTLD:New(coalition.side.BLUE,{"Helicargo", "Hercules"},"Lufttransportbrigade I") -- @@ -893,26 +895,38 @@ do -- -- my_ctld.useprefix = true -- this is true by default and MUST BE ON. -- --- ### 5.2 Integrate Hercules ground crew loadable objects --- --- Add ground crew loadable objects to your CTLD instance like so, where `my_ctld` is the previously created CTLD instance: +-- ### 5.2 Integrate Hercules ground crew (F8 Menu) loadable objects (alternative method) -- +-- Integrate to your CTLD instance like so, where `my_ctld` is a previously created CTLD instance: +-- +-- my_ctld.enableHercules = false -- avoid dual loading via CTLD F10 and F8 ground crew -- local herccargo = CTLD_HERCULES:New("blue", "Hercules Test", my_ctld) -- --- You also need: --- --- * A template called "Infantry" for 10 Paratroopers (as set via herccargo.infantrytemplate). --- * Depending on what you are loading with the help of the ground crew, there are 42 more templates for the various vehicles that are loadable. +-- You also need: +-- +-- * A template called "Infantry" for 10 Paratroopers (as set via herccargo.infantrytemplate). +-- * Depending on what you are loading with the help of the ground crew, there are 42 more templates for the various vehicles that are loadable. -- -- There's a **quick check output in the `dcs.log`** which tells you what's there and what not. --- E.g.: --- ...Checking template for APC BTR-82A Air [24998lb] (BTR-82A) ... MISSING) --- ...Checking template for ART 2S9 NONA Skid [19030lb] (SAU 2-C9) ... MISSING) --- ...Checking template for EWR SBORKA Air [21624lb] (Dog Ear radar) ... MISSING) --- ...Checking template for Transport Tigr Air [15900lb] (Tigr_233036) ... OK) +-- E.g.: +-- +-- ...Checking template for APC BTR-82A Air [24998lb] (BTR-82A) ... MISSING) +-- ...Checking template for ART 2S9 NONA Skid [19030lb] (SAU 2-C9) ... MISSING) +-- ...Checking template for EWR SBORKA Air [21624lb] (Dog Ear radar) ... MISSING) +-- ...Checking template for Transport Tigr Air [15900lb] (Tigr_233036) ... OK) -- -- Expected template names are the ones in the rounded brackets. -- +-- ### 5.2.1 Hints +-- +-- The script works on the EVENTS.Shot trigger, which is used by the mod when you **drop cargo from the Hercules while flying**. Unloading on the ground does +-- not achieve anything here. If you just want to unload on the ground, use the normal Moose CTLD (see 5.1). +-- +-- There are two ways of airdropping: +-- +-- 1) Very low and very slow (>5m and <10m AGL) - here you can drop stuff which has "Skid" at the end of the cargo name (loaded via F8 Ground Crew menu) +-- 2) Higher up and slow (>100m AGL) - here you can drop paratroopers and cargo which has "Air" at the end of the cargo name (loaded via F8 Ground Crew menu) +-- -- Standard transport capabilities as per the real Hercules are: -- -- ["Hercules"] = {type="Hercules", crates=true, troops=true, cratelimit = 7, trooplimit = 64}, -- 19t cargo, 64 paratroopers @@ -4792,10 +4806,11 @@ end end -- end do do ---- Hercules Cargo Drop Events by Anubis Yinepu +--- **Hercules Cargo AIR Drop Events** by Anubis Yinepu -- Moose CTLD OO refactoring by Applevangelist -- --- This script will only work for the Herculus mod by Anubis +-- This script will only work for the Herculus mod by Anubis, and only for **Air Dropping** cargo from the Hercules. +-- Use the standard Moose CTLD if you want to unload on the ground. -- Payloads carried by pylons 11, 12 and 13 need to be declared in the Herculus_Loadout.lua file -- Except for Ammo pallets, this script will spawn whatever payload gets launched from pylons 11, 12 and 13 -- Pylons 11, 12 and 13 are moveable within the Herculus cargobay area @@ -4883,7 +4898,7 @@ CTLD_HERCULES.Types = { ["ART GVOZDIKA [34720lb]"] = {['name'] = "SAU Gvozdika", ['container'] = false}, ["APC MTLB Air [26400lb]"] = {['name'] = "MTLB", ['container'] = true}, ["APC MTLB Skid [26290lb]"] = {['name'] = "MTLB", ['container'] = false}, - ["Generic Crate [20000lb]"] = {['name'] = "Hercules_Container_Parachute", ['container'] = true} --nothing generic in Moose CTLD + --["Generic Crate [20000lb]"] = {['name'] = "Hercules_Container_Parachute", ['container'] = true} --nothing generic in Moose CTLD } --- Cargo Object @@ -4908,7 +4923,8 @@ CTLD_HERCULES.Types = { -- @return #CTLD_HERCULES self -- @usage -- Integrate to your CTLD instance like so, where `my_ctld` is a previously created CTLD instance: --- +-- +-- my_ctld.enableHercules = false -- avoid dual loading via CTLD F10 and F8 ground crew -- local herccargo = CTLD_HERCULES:New("blue", "Hercules Test", my_ctld) -- -- You also need: @@ -4922,6 +4938,14 @@ CTLD_HERCULES.Types = { -- ...Checking template for Transport Tigr Air [15900lb] (Tigr_233036) ... OK) -- -- Expected template names are the ones in the rounded brackets. +-- +-- HINTS +-- +-- The script works on the EVENTS.Shot trigger, which is used by the mod when you **drop cargo from the Hercules while flying**. Unloading on the ground does +-- not achieve anything here. If you just want to unload on the ground, use the normal Moose CTLD. +-- There are two ways of airdropping: +-- 1) Very low and very slow (>5m and <10m AGL) - here you can drop stuff which has "Skid" at the end of the cargo name (loaded via F8 Ground Crew menu) +-- 2) Higher up and slow (>100m AGL) - here you can drop paratroopers and cargo which has "Air" at the end of the cargo name (loaded via F8 Ground Crew menu) function CTLD_HERCULES:New(Coalition, Alias, CtldObject) -- Inherit everything from FSM class. local self=BASE:Inherit(self, FSM:New()) -- #CTLD_HERCULES diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 8f7c4f7ec..77f4d536e 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -52,6 +52,7 @@ BIGSMOKEPRESET = { -- @field #string TheChannel The Channel map. -- @field #string Syria Syria map. -- @field #string MarianaIslands Mariana Islands map. +-- @field #string Falklands South Atlantic map. DCSMAP = { Caucasus="Caucasus", NTTR="Nevada", @@ -59,7 +60,8 @@ DCSMAP = { PersianGulf="PersianGulf", TheChannel="TheChannel", Syria="Syria", - MarianaIslands="MarianaIslands" + MarianaIslands="MarianaIslands", + Falklands="Falklands", } @@ -1347,6 +1349,7 @@ end -- * The Cannel Map -10 (West) -- * Syria +5 (East) -- * Mariana Islands +2 (East) +-- * Falklands +12 (East) - note there's a LOT of deviation across the map, as we're closer to the South Pole -- @param #string map (Optional) Map for which the declination is returned. Default is from env.mission.theatre -- @return #number Declination in degrees. function UTILS.GetMagneticDeclination(map) @@ -1369,6 +1372,8 @@ function UTILS.GetMagneticDeclination(map) declination=5 elseif map==DCSMAP.MarianaIslands then declination=2 + elseif map==DCSMAP.Falklands then + declination=12 else declination=0 end diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index 04b474db9..edde1fdc2 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -487,6 +487,28 @@ AIRBASE.MarianaIslands={ ["Olf_Orote"]="Olf Orote", } +--- Airbases of the South Atlantic map: +-- +-- * AIRBASE.SouthAtlantic.Port_Stanley +-- * AIRBASE.SouthAtlantic.Mount_Pleasant +-- * AIRBASE.SouthAtlantic.San_Carlos_FOB +-- * AIRBASE.SouthAtlantic.Rio_Grande +-- * AIRBASE.SouthAtlantic.Rio_Gallegos +-- * AIRBASE.SouthAtlantic.Ushuaia +-- * AIRBASE.SouthAtlantic.Ushuaia_Helo_Port +-- * AIRBASE.SouthAtlantic.Punta_Arenas +-- +--@field MarianaIslands +AIRBASE.SouthAtlantic={ + ["Port_Stanley"]="Port Stanley", + ["Mount_Pleasant"]="Mount Pleasant", + ["San_Carlos_FOB"]="San Carlos FOB", + ["Rio_Grande"]="Rio Grande", + ["Rio_Gallegos"]="Rio Gallegos", + ["Ushuaia"]="Ushuaia", + ["Ushuaia_Helo_Port"]="Ushuaia Helo Port", + ["Punta_Arenas"]="Punta Arenas", +} --- AIRBASE.ParkingSpot ".Coordinate, ".TerminalID", ".TerminalType", ".TOAC", ".Free", ".TerminalID0", ".DistToRwy". -- @type AIRBASE.ParkingSpot diff --git a/README.md b/README.md index 5a1979522..69ab8403a 100644 --- a/README.md +++ b/README.md @@ -61,9 +61,10 @@ Documentation on the MOOSE class hierarchy, usage guides and background informat -## [MOOSE Youtube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) +## [MOOSE Youtube Tutorials](https://youtube.com/playlist?list=PLLkY2GByvtC2ME0Q9wrKRDE6qnXJYV3iT) -MOOSE has a [broadcast and training channel on YouTube](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) with various channels that you can watch. +Pene has kindly created a [tutorial series for MOOSE](https://youtube.com/playlist?list=PLLkY2GByvtC2ME0Q9wrKRDE6qnXJYV3iT) + with various videos that you can watch.