diff --git a/Moose Development/Moose/Core/Beacon.lua b/Moose Development/Moose/Core/Beacon.lua new file mode 100644 index 000000000..37dba8c76 --- /dev/null +++ b/Moose Development/Moose/Core/Beacon.lua @@ -0,0 +1,442 @@ +--- **Core** - TACAN and other beacons. +-- +-- === +-- +-- ## Features: +-- +-- * Provide beacon functionality to assist pilots. +-- +-- === +-- +-- ### Authors: Hugues "Grey_Echo" Bousquet, funkyfranky +-- +-- @module Core.Beacon +-- @image Core_Radio.JPG + +--- *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. +-- 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 +-- +-- 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. +-- +-- ## 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. +-- +-- @type BEACON +-- @field #string ClassName Name of the class "BEACON". +-- @field Wrapper.Controllable#CONTROLLABLE Positionable The @{#CONTROLLABLE} that will receive radio capabilities. +-- @extends Core.Base#BASE +BEACON = { + ClassName = "BEACON", + Positionable = nil, + name = nil, +} + +--- Beacon types supported by DCS. +-- @type BEACON.Type +-- @field #number NULL +-- @field #number VOR +-- @field #number DME +-- @field #number VOR_DME +-- @field #number TACAN TACtical Air Navigation system. +-- @field #number VORTAC +-- @field #number RSBN +-- @field #number BROADCAST_STATION +-- @field #number HOMER +-- @field #number AIRPORT_HOMER +-- @field #number AIRPORT_HOMER_WITH_MARKER +-- @field #number ILS_FAR_HOMER +-- @field #number ILS_NEAR_HOMER +-- @field #number ILS_LOCALIZER +-- @field #number ILS_GLIDESLOPE +-- @field #number PRMG_LOCALIZER +-- @field #number PRMG_GLIDESLOPE +-- @field #number ICLS Same as ICLS glideslope. +-- @field #number ICLS_LOCALIZER +-- @field #number ICLS_GLIDESLOPE +-- @field #number NAUTICAL_HOMER +BEACON.Type={ + NULL = 0, + VOR = 1, + DME = 2, + VOR_DME = 3, + TACAN = 4, + VORTAC = 5, + RSBN = 128, + BROADCAST_STATION = 1024, + HOMER = 8, + AIRPORT_HOMER = 4104, + AIRPORT_HOMER_WITH_MARKER = 4136, + ILS_FAR_HOMER = 16408, + ILS_NEAR_HOMER = 16424, + ILS_LOCALIZER = 16640, + ILS_GLIDESLOPE = 16896, + PRMG_LOCALIZER = 33024, + PRMG_GLIDESLOPE = 33280, + ICLS = 131584, --leaving this in here but it is the same as ICLS_GLIDESLOPE + ICLS_LOCALIZER = 131328, + ICLS_GLIDESLOPE = 131584, + NAUTICAL_HOMER = 65536, +} + +--- Beacon systems supported by DCS. https://wiki.hoggitworld.com/view/DCS_command_activateBeacon +-- @type BEACON.System +-- @field #number PAR_10 ? +-- @field #number RSBN_5 Russian VOR/DME system. +-- @field #number TACAN TACtical Air Navigation system on ground. +-- @field #number TACAN_TANKER_X TACtical Air Navigation system for tankers on X band. +-- @field #number TACAN_TANKER_Y TACtical Air Navigation system for tankers on Y band. +-- @field #number VOR Very High Frequency Omni-Directional Range +-- @field #number ILS_LOCALIZER ILS localizer +-- @field #number ILS_GLIDESLOPE ILS glideslope. +-- @field #number PRGM_LOCALIZER PRGM localizer. +-- @field #number PRGM_GLIDESLOPE PRGM glideslope. +-- @field #number BROADCAST_STATION Broadcast station. +-- @field #number VORTAC Radio-based navigational aid for aircraft pilots consisting of a co-located VHF omnidirectional range (VOR) beacon and a tactical air navigation system (TACAN) beacon. +-- @field #number TACAN_AA_MODE_X TACtical Air Navigation for aircraft on X band. +-- @field #number TACAN_AA_MODE_Y TACtical Air Navigation for aircraft on Y band. +-- @field #number VORDME Radio beacon that combines a VHF omnidirectional range (VOR) with a distance measuring equipment (DME). +-- @field #number ICLS_LOCALIZER Carrier landing system. +-- @field #number ICLS_GLIDESLOPE Carrier landing system. +BEACON.System={ + PAR_10 = 1, + RSBN_5 = 2, + TACAN = 3, + TACAN_TANKER_X = 4, + TACAN_TANKER_Y = 5, + VOR = 6, + ILS_LOCALIZER = 7, + ILS_GLIDESLOPE = 8, + PRMG_LOCALIZER = 9, + PRMG_GLIDESLOPE = 10, + BROADCAST_STATION = 11, + VORTAC = 12, + TACAN_AA_MODE_X = 13, + TACAN_AA_MODE_Y = 14, + VORDME = 15, + ICLS_LOCALIZER = 16, + ICLS_GLIDESLOPE = 17, +} + +--- Create a new BEACON Object. This doesn't activate the beacon, though, use @{#BEACON.ActivateTACAN} etc. +-- If you want to create a BEACON, you probably should use @{Wrapper.Positionable#POSITIONABLE.GetBeacon}() instead. +-- @param #BEACON self +-- @param Wrapper.Positionable#POSITIONABLE Positionable The @{Positionable} that will receive radio capabilities. +-- @return #BEACON Beacon object or #nil if the positionable is invalid. +function BEACON:New(Positionable) + + -- Inherit BASE. + local self=BASE:Inherit(self, BASE:New()) --#BEACON + + -- Debug. + self:F(Positionable) + + -- Set positionable. + if Positionable:GetPointVec2() then -- It's stupid, but the only way I found to make sure positionable is valid + self.Positionable = Positionable + self.name=Positionable:GetName() + self:I(string.format("New BEACON %s", tostring(self.name))) + return self + end + + self:E({"The passed positionable is invalid, no BEACON created", Positionable}) + return nil +end + + +--- Activates a TACAN BEACON. +-- @param #BEACON self +-- @param #number Channel TACAN channel, i.e. the "10" part in "10Y". +-- @param #string Mode TACAN mode, i.e. the "Y" part in "10Y". +-- @param #string Message The Message that is going to be coded in Morse and broadcasted by the beacon. +-- @param #boolean Bearing If true, beacon provides bearing information. If false (or nil), only distance information is available. +-- @param #number Duration How long will the beacon last in seconds. Omit for forever. +-- @return #BEACON self +-- @usage +-- -- Let's create a TACAN Beacon for a tanker +-- local myUnit = UNIT:FindByName("MyUnit") +-- local myBeacon = myUnit:GetBeacon() -- Creates the beacon +-- +-- myBeacon:ActivateTACAN(20, "Y", "TEXACO", true) -- Activate the beacon +function BEACON:ActivateTACAN(Channel, Mode, Message, Bearing, Duration) + self:T({channel=Channel, mode=Mode, callsign=Message, bearing=Bearing, duration=Duration}) + + -- Get frequency. + local Frequency=UTILS.TACANToFrequency(Channel, Mode) + + -- Check. + if not Frequency then + self:E({"The passed TACAN channel is invalid, the BEACON is not emitting"}) + return self + end + + -- Beacon type. + local Type=BEACON.Type.TACAN + + -- Beacon system. + local System=BEACON.System.TACAN + + -- Check if unit is an aircraft and set system accordingly. + local AA=self.Positionable:IsAir() + if AA then + System=5 --NOTE: 5 is how you cat the correct tanker behaviour! --BEACON.System.TACAN_TANKER + -- Check if "Y" mode is selected for aircraft. + if Mode~="Y" then + self:E({"WARNING: The POSITIONABLE you want to attach the AA Tacan Beacon is an aircraft: Mode should Y !The BEACON is not emitting.", self.Positionable}) + end + end + + -- Attached unit. + local UnitID=self.Positionable:GetID() + + -- Debug. + self:I({string.format("BEACON Activating TACAN %s: Channel=%d%s, Morse=%s, Bearing=%s, Duration=%s!", tostring(self.name), Channel, Mode, Message, tostring(Bearing), tostring(Duration))}) + + -- Start beacon. + self.Positionable:CommandActivateBeacon(Type, System, Frequency, UnitID, Channel, Mode, AA, Message, Bearing) + + -- Stop sheduler. + if Duration then + self.Positionable:DeactivateBeacon(Duration) + end + + return self +end + +--- Activates an ICLS BEACON. The unit the BEACON is attached to should be an aircraft carrier supporting this system. +-- @param #BEACON self +-- @param #number Channel ICLS channel. +-- @param #string Callsign The Message that is going to be coded in Morse and broadcasted by the beacon. +-- @param #number Duration How long will the beacon last in seconds. Omit for forever. +-- @return #BEACON self +function BEACON:ActivateICLS(Channel, Callsign, Duration) + self:F({Channel=Channel, Callsign=Callsign, Duration=Duration}) + + -- Attached unit. + local UnitID=self.Positionable:GetID() + + -- Debug + self:T2({"ICLS BEACON started!"}) + + -- Start beacon. + self.Positionable:CommandActivateICLS(Channel, UnitID, Callsign) + + -- Stop sheduler + if Duration then -- Schedule the stop of the BEACON if asked by the MD + self.Positionable:DeactivateBeacon(Duration) + end + + return self +end + +--- 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 +-- @param #boolean Bearing Can the BEACON be homed on ? +-- @param #number BeaconDuration How long will the beacon last in seconds. Omit for forever. +-- @return #BEACON self +-- @usage +-- -- Let's create a TACAN Beacon for a tanker +-- local myUnit = UNIT:FindByName("MyUnit") +-- local myBeacon = myUnit:GetBeacon() -- Creates the beacon +-- +-- myBeacon:AATACAN(20, "TEXACO", true) -- Activate the beacon +function BEACON:AATACAN(TACANChannel, Message, Bearing, BeaconDuration) + self:F({TACANChannel, Message, Bearing, BeaconDuration}) + + local IsValid = true + + if not self.Positionable:IsAir() then + self:E({"The POSITIONABLE you want to attach the AA Tacan Beacon is not an aircraft ! The BEACON is not emitting", self.Positionable}) + IsValid = false + end + + local Frequency = self:_TACANToFrequency(TACANChannel, "Y") + if not Frequency then + self:E({"The passed TACAN channel is invalid, the BEACON is not emitting"}) + IsValid = false + end + + -- I'm using the beacon type 4 (BEACON_TYPE_TACAN). For System, I'm using 5 (TACAN_TANKER_MODE_Y) if the bearing shows its bearing + -- or 14 (TACAN_AA_MODE_Y) if it does not + local System + if Bearing then + System = 5 + else + System = 14 + end + + if IsValid then -- Starts the BEACON + self:T2({"AA TACAN BEACON started !"}) + self.Positionable:SetCommand({ + id = "ActivateBeacon", + params = { + type = 4, + system = System, + callsign = Message, + frequency = Frequency, + } + }) + + if BeaconDuration then -- Schedule the stop of the BEACON if asked by the MD + SCHEDULER:New(nil, + function() + self:StopAATACAN() + end, {}, BeaconDuration) + end + end + + return self +end + +--- Stops the AA TACAN BEACON +-- @param #BEACON self +-- @return #BEACON self +function BEACON:StopAATACAN() + self:F() + if not self.Positionable then + self:E({"Start the beacon first before stoping it !"}) + else + self.Positionable:SetCommand({ + id = 'DeactivateBeacon', + params = { + } + }) + end +end + + +--- Activates a general pupose 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 : +-- * **Mi8** +-- * R-828 -> 20-60MHz +-- * ARKUD -> 100-150MHz (canal 1 : 114166, canal 2 : 114333, canal 3 : 114583, canal 4 : 121500, canal 5 : 123100, canal 6 : 124100) AM +-- * ARK9 -> 150-1300KHz +-- * **Huey** +-- * AN/ARC-131 -> 30-76 Mhz FM +-- @param #BEACON self +-- @param #string FileName The name of the audio file +-- @param #number Frequency in MHz +-- @param #number Modulation either radio.modulation.AM or radio.modulation.FM +-- @param #number Power in W +-- @param #number BeaconDuration How long will the beacon last in seconds. Omit for forever. +-- @return #BEACON self +-- @usage +-- -- Let's create a beacon for a unit in distress. +-- -- Frequency will be 40MHz FM (home-able by a Huey's AN/ARC-131) +-- -- The beacon they use is battery-powered, and only lasts for 5 min +-- local UnitInDistress = UNIT:FindByName("Unit1") +-- local UnitBeacon = UnitInDistress:GetBeacon() +-- +-- -- Set the beacon and start it +-- UnitBeacon:RadioBeacon("MySoundFileSOS.ogg", 40, radio.modulation.FM, 20, 5*60) +function BEACON:RadioBeacon(FileName, Frequency, Modulation, Power, BeaconDuration) + self:F({FileName, Frequency, Modulation, Power, BeaconDuration}) + local IsValid = false + + -- Check the filename + if type(FileName) == "string" then + if FileName:find(".ogg") or FileName:find(".wav") then + if not FileName:find("l10n/DEFAULT/") then + FileName = "l10n/DEFAULT/" .. FileName + end + IsValid = true + end + end + if not IsValid then + self:E({"File name invalid. Maybe something wrong with the extension ? ", FileName}) + end + + -- Check the Frequency + if type(Frequency) ~= "number" and IsValid then + self:E({"Frequency invalid. ", Frequency}) + IsValid = false + end + Frequency = Frequency * 1000000 -- Conversion to Hz + + -- Check the modulation + if Modulation ~= radio.modulation.AM and Modulation ~= radio.modulation.FM and IsValid then --TODO Maybe make this future proof if ED decides to add an other modulation ? + self:E({"Modulation is invalid. Use DCS's enum radio.modulation.", Modulation}) + IsValid = false + end + + -- Check the Power + if type(Power) ~= "number" and IsValid then + self:E({"Power is invalid. ", Power}) + IsValid = false + end + Power = math.floor(math.abs(Power)) --TODO Find what is the maximum power allowed by DCS and limit power to that + + if IsValid then + self:T2({"Activating Beacon on ", Frequency, Modulation}) + -- Note that this is looped. I have to give this transmission a unique name, I use the class ID + trigger.action.radioTransmission(FileName, self.Positionable:GetPositionVec3(), Modulation, true, Frequency, Power, tostring(self.ID)) + + if BeaconDuration then -- Schedule the stop of the BEACON if asked by the MD + SCHEDULER:New( nil, + function() + self:StopRadioBeacon() + end, {}, BeaconDuration) + end + end +end + +--- Stops the AA TACAN BEACON +-- @param #BEACON self +-- @return #BEACON self +function BEACON:StopRadioBeacon() + self:F() + -- The unique name of the transmission is the class ID + trigger.action.stopRadioTransmission(tostring(self.ID)) + return self +end + +--- Converts a TACAN Channel/Mode couple into a frequency in Hz +-- @param #BEACON self +-- @param #number TACANChannel +-- @param #string TACANMode +-- @return #number Frequecy +-- @return #nil if parameters are invalid +function BEACON:_TACANToFrequency(TACANChannel, TACANMode) + self:F3({TACANChannel, TACANMode}) + + if type(TACANChannel) ~= "number" then + if TACANMode ~= "X" and TACANMode ~= "Y" then + return nil -- error in arguments + end + end + +-- This code is largely based on ED's code, in DCS World\Scripts\World\Radio\BeaconTypes.lua, line 137. +-- I have no idea what it does but it seems to work + local A = 1151 -- 'X', channel >= 64 + local B = 64 -- channel >= 64 + + if TACANChannel < 64 then + B = 1 + end + + if TACANMode == 'Y' then + A = 1025 + if TACANChannel < 64 then + A = 1088 + end + else -- 'X' + if TACANChannel < 64 then + A = 962 + end + end + + return (A + TACANChannel - B) * 1000000 +end \ No newline at end of file diff --git a/Moose Development/Moose/Core/Database.lua b/Moose Development/Moose/Core/Database.lua index 5997f05ca..bb4e25fe0 100644 --- a/Moose Development/Moose/Core/Database.lua +++ b/Moose Development/Moose/Core/Database.lua @@ -298,24 +298,78 @@ do -- Zones -- @return #DATABASE self function DATABASE:_RegisterZones() - for ZoneID, ZoneData in pairs( env.mission.triggers.zones ) do + for ZoneID, ZoneData in pairs(env.mission.triggers.zones) do local ZoneName = ZoneData.name + + -- Color + local color=ZoneData.color or {1, 0, 0, 0.15} + + -- Create new Zone + local Zone=nil --Core.Zone#ZONE_BASE + + if ZoneData.type==0 then + + --- + -- Circular zone + --- + + self:I(string.format("Register ZONE: %s (Circular)", ZoneName)) + + Zone=ZONE:New(ZoneName) + + else - self:I( { "Register ZONE:", Name = ZoneName } ) - local Zone = ZONE:New( ZoneName ) - self.ZONENAMES[ZoneName] = ZoneName - self:AddZone( ZoneName, Zone ) + --- + -- Quad-point zone + --- + + self:I(string.format("Register ZONE: %s (Polygon, Quad)", ZoneName)) + + Zone=ZONE_POLYGON_BASE:New(ZoneName, ZoneData.verticies) + + --for i,vec2 in pairs(ZoneData.verticies) do + -- local coord=COORDINATE:NewFromVec2(vec2) + -- coord:MarkToAll(string.format("%s Point %d", ZoneName, i)) + --end + + end + + if Zone then + + -- Store color of zone. + Zone.Color=color + + -- Store in DB. + self.ZONENAMES[ZoneName] = ZoneName + + -- Add zone. + self:AddZone(ZoneName, Zone) + + end + end + -- Polygon zones defined by late activated groups. for ZoneGroupName, ZoneGroup in pairs( self.GROUPS ) do if ZoneGroupName:match("#ZONE_POLYGON") then + local ZoneName1 = ZoneGroupName:match("(.*)#ZONE_POLYGON") local ZoneName2 = ZoneGroupName:match(".*#ZONE_POLYGON(.*)") local ZoneName = ZoneName1 .. ( ZoneName2 or "" ) - self:I( { "Register ZONE_POLYGON:", Name = ZoneName } ) + -- Debug output + self:I(string.format("Register ZONE: %s (Polygon)", ZoneName)) + + -- Create a new polygon zone. local Zone_Polygon = ZONE_POLYGON:New( ZoneName, ZoneGroup ) + + -- Set color. + Zone_Polygon:SetColor({1, 0, 0}, 0.15) + + -- Store name in DB. self.ZONENAMES[ZoneName] = ZoneName + + -- Add zone to DB. self:AddZone( ZoneName, Zone_Polygon ) end end diff --git a/Moose Development/Moose/Core/Event.lua b/Moose Development/Moose/Core/Event.lua index 9d10bdbbc..02dab0afc 100644 --- a/Moose Development/Moose/Core/Event.lua +++ b/Moose Development/Moose/Core/Event.lua @@ -1,98 +1,98 @@ --- **Core** - Models DCS event dispatching using a publish-subscribe model. --- +-- -- === --- +-- -- ## Features: --- +-- -- * Capture DCS events and dispatch them to the subscribed objects. -- * Generate DCS events to the subscribed objects from within the code. --- +-- -- === --- +-- -- # Event Handling Overview --- +-- -- ![Objects](..\Presentations\EVENT\Dia2.JPG) --- +-- -- Within a running mission, various DCS events occur. Units are dynamically created, crash, die, shoot stuff, get hit etc. -- This module provides a mechanism to dispatch those events occuring within your running mission, to the different objects orchestrating your mission. --- +-- -- ![Objects](..\Presentations\EVENT\Dia3.JPG) --- +-- -- Objects can subscribe to different events. The Event dispatcher will publish the received DCS events to the subscribed MOOSE objects, in a specified order. -- In this way, the subscribed MOOSE objects are kept in sync with your evolving running mission. --- +-- -- ## 1. Event Dispatching --- +-- -- ![Objects](..\Presentations\EVENT\Dia4.JPG) --- --- The _EVENTDISPATCHER object is automatically created within MOOSE, --- and handles the dispatching of DCS Events occurring --- in the simulator to the subscribed objects +-- +-- The _EVENTDISPATCHER object is automatically created within MOOSE, +-- and handles the dispatching of DCS Events occurring +-- in the simulator to the subscribed objects -- in the correct processing order. -- -- ![Objects](..\Presentations\EVENT\Dia5.JPG) --- +-- -- There are 5 levels of kind of objects that the _EVENTDISPATCHER services: --- +-- -- * _DATABASE object: The core of the MOOSE objects. Any object that is created, deleted or updated, is done in this database. -- * SET_ derived classes: Subsets of the _DATABASE object. These subsets are updated by the _EVENTDISPATCHER as the second priority. -- * UNIT objects: UNIT objects can subscribe to DCS events. Each DCS event will be directly published to teh subscribed UNIT object. -- * GROUP objects: GROUP objects can subscribe to DCS events. Each DCS event will be directly published to the subscribed GROUP object. -- * Any other object: Various other objects can subscribe to DCS events. Each DCS event triggered will be published to each subscribed object. --- +-- -- ![Objects](..\Presentations\EVENT\Dia6.JPG) --- +-- -- For most DCS events, the above order of updating will be followed. --- +-- -- ![Objects](..\Presentations\EVENT\Dia7.JPG) --- +-- -- But for some DCS events, the publishing order is reversed. This is due to the fact that objects need to be **erased** instead of added. --- +-- -- # 2. Event Handling --- +-- -- ![Objects](..\Presentations\EVENT\Dia8.JPG) --- +-- -- The actual event subscribing and handling is not facilitated through the _EVENTDISPATCHER, but it is done through the @{BASE} class, @{UNIT} class and @{GROUP} class. -- The _EVENTDISPATCHER is a component that is quietly working in the background of MOOSE. --- +-- -- ![Objects](..\Presentations\EVENT\Dia9.JPG) --- --- The BASE class provides methods to catch DCS Events. These are events that are triggered from within the DCS simulator, +-- +-- The BASE class provides methods to catch DCS Events. These are events that are triggered from within the DCS simulator, -- and handled through lua scripting. MOOSE provides an encapsulation to handle these events more efficiently. --- +-- -- ## 2.1. Subscribe to / Unsubscribe from DCS Events. --- +-- -- At first, the mission designer will need to **Subscribe** to a specific DCS event for the class. -- So, when the DCS event occurs, the class will be notified of that event. -- There are two functions which you use to subscribe to or unsubscribe from an event. --- +-- -- * @{Core.Base#BASE.HandleEvent}(): Subscribe to a DCS Event. -- * @{Core.Base#BASE.UnHandleEvent}(): Unsubscribe from a DCS Event. --- +-- -- Note that for a UNIT, the event will be handled **for that UNIT only**! -- Note that for a GROUP, the event will be handled **for all the UNITs in that GROUP only**! --- +-- -- For all objects of other classes, the subscribed events will be handled for **all UNITs within the Mission**! --- So if a UNIT within the mission has the subscribed event for that object, +-- So if a UNIT within the mission has the subscribed event for that object, -- then the object event handler will receive the event for that UNIT! --- +-- -- ## 2.2 Event Handling of DCS Events --- +-- -- Once the class is subscribed to the event, an **Event Handling** method on the object or class needs to be written that will be called -- when the DCS event occurs. The Event Handling method receives an @{Core.Event#EVENTDATA} structure, which contains a lot of information -- about the event that occurred. --- --- Find below an example of the prototype how to write an event handling function for two units: +-- +-- Find below an example of the prototype how to write an event handling function for two units: -- -- local Tank1 = UNIT:FindByName( "Tank A" ) -- local Tank2 = UNIT:FindByName( "Tank B" ) --- +-- -- -- Here we subscribe to the Dead events. So, if one of these tanks dies, the Tank1 or Tank2 objects will be notified. -- Tank1:HandleEvent( EVENTS.Dead ) -- Tank2:HandleEvent( EVENTS.Dead ) --- +-- -- --- This function is an Event Handling function that will be called when Tank1 is Dead. --- -- @param Wrapper.Unit#UNIT self +-- -- @param Wrapper.Unit#UNIT self -- -- @param Core.Event#EVENTDATA EventData -- function Tank1:OnEventDead( EventData ) -- @@ -100,73 +100,73 @@ -- end -- -- --- This function is an Event Handling function that will be called when Tank2 is Dead. --- -- @param Wrapper.Unit#UNIT self +-- -- @param Wrapper.Unit#UNIT self -- -- @param Core.Event#EVENTDATA EventData -- function Tank2:OnEventDead( EventData ) -- -- self:SmokeBlue() -- end --- +-- -- ## 2.3 Event Handling methods that are automatically called upon subscribed DCS events. --- +-- -- ![Objects](..\Presentations\EVENT\Dia10.JPG) --- +-- -- The following list outlines which EVENTS item in the structure corresponds to which Event Handling method. -- Always ensure that your event handling methods align with the events being subscribed to, or nothing will be executed. --- +-- -- # 3. EVENTS type --- --- The EVENTS structure contains names for all the different DCS events that objects can subscribe to using the +-- +-- The EVENTS structure contains names for all the different DCS events that objects can subscribe to using the -- @{Core.Base#BASE.HandleEvent}() method. --- +-- -- # 4. EVENTDATA type --- --- The @{Core.Event#EVENTDATA} structure contains all the fields that are populated with event information before +-- +-- The @{Core.Event#EVENTDATA} structure contains all the fields that are populated with event information before -- an Event Handler method is being called by the event dispatcher. -- The Event Handler received the EVENTDATA object as a parameter, and can be used to investigate further the different events. -- There are basically 4 main categories of information stored in the EVENTDATA structure: --- +-- -- * Initiator Unit data: Several fields documenting the initiator unit related to the event. -- * Target Unit data: Several fields documenting the target unit related to the event. -- * Weapon data: Certain events populate weapon information. -- * Place data: Certain events populate place information. --- +-- -- --- This function is an Event Handling function that will be called when Tank1 is Dead. -- -- EventData is an EVENTDATA structure. -- -- We use the EventData.IniUnit to smoke the tank Green. --- -- @param Wrapper.Unit#UNIT self +-- -- @param Wrapper.Unit#UNIT self -- -- @param Core.Event#EVENTDATA EventData -- function Tank1:OnEventDead( EventData ) -- -- EventData.IniUnit:SmokeGreen() -- end --- --- +-- +-- -- Find below an overview which events populate which information categories: --- +-- -- ![Objects](..\Presentations\EVENT\Dia14.JPG) --- --- **IMPORTANT NOTE:** Some events can involve not just UNIT objects, but also STATIC objects!!! +-- +-- **IMPORTANT NOTE:** Some events can involve not just UNIT objects, but also STATIC objects!!! -- In that case the initiator or target unit fields will refer to a STATIC object! -- In case a STATIC object is involved, the documentation indicates which fields will and won't not be populated. -- The fields **IniObjectCategory** and **TgtObjectCategory** contain the indicator which **kind of object is involved** in the event. -- You can use the enumerator **Object.Category.UNIT** and **Object.Category.STATIC** to check on IniObjectCategory and TgtObjectCategory. -- Example code snippet: --- +-- -- if Event.IniObjectCategory == Object.Category.UNIT then -- ... -- end -- if Event.IniObjectCategory == Object.Category.STATIC then -- ... --- end --- +-- end +-- -- When a static object is involved in the event, the Group and Player fields won't be populated. --- +-- -- === --- +-- -- ### Author: **FlightControl** --- ### Contributions: --- +-- ### Contributions: +-- -- === -- -- @module Core.Event @@ -236,11 +236,11 @@ EVENTS = { RemoveUnit = world.event.S_EVENT_REMOVE_UNIT, PlayerEnterAircraft = world.event.S_EVENT_PLAYER_ENTER_AIRCRAFT, -- Added with DCS 2.5.6 - DetailedFailure = world.event.S_EVENT_DETAILED_FAILURE or -1, --We set this to -1 for backward compatibility to DCS 2.5.5 and earlier - Kill = world.event.S_EVENT_KILL or -1, - Score = world.event.S_EVENT_SCORE or -1, - UnitLost = world.event.S_EVENT_UNIT_LOST or -1, - LandingAfterEjection = world.event.S_EVENT_LANDING_AFTER_EJECTION or -1, + DetailedFailure = world.event.S_EVENT_DETAILED_FAILURE or -1, --We set this to -1 for backward compatibility to DCS 2.5.5 and earlier + Kill = world.event.S_EVENT_KILL or -1, + Score = world.event.S_EVENT_SCORE or -1, + UnitLost = world.event.S_EVENT_UNIT_LOST or -1, + LandingAfterEjection = world.event.S_EVENT_LANDING_AFTER_EJECTION or -1, -- Added with DCS 2.7.0 ParatrooperLanding = world.event.S_EVENT_PARATROOPER_LENDING or -1, DiscardChairAfterEjection = world.event.S_EVENT_DISCARD_CHAIR_AFTER_EJECTION or -1, @@ -252,13 +252,13 @@ EVENTS = { --- The Event structure -- Note that at the beginning of each field description, there is an indication which field will be populated depending on the object type involved in the Event: --- +-- -- * A (Object.Category.)UNIT : A UNIT object type is involved in the Event. -- * A (Object.Category.)STATIC : A STATIC object type is involved in the Event. --- +-- -- @type EVENTDATA -- @field #number id The identifier of the event. --- +-- -- @field DCS#Unit initiator (UNIT/STATIC/SCENERY) The initiating @{DCS#Unit} or @{DCS#StaticObject}. -- @field DCS#Object.Category IniObjectCategory (UNIT/STATIC/SCENERY) The initiator object category ( Object.Category.UNIT or Object.Category.STATIC ). -- @field DCS#Unit IniDCSUnit (UNIT/STATIC) The initiating @{DCS#Unit} or @{DCS#StaticObject}. @@ -273,7 +273,7 @@ EVENTS = { -- @field DCS#coalition.side IniCoalition (UNIT) The coalition of the initiator. -- @field DCS#Unit.Category IniCategory (UNIT) The category of the initiator. -- @field #string IniTypeName (UNIT) The type name of the initiator. --- +-- -- @field DCS#Unit target (UNIT/STATIC) The target @{DCS#Unit} or @{DCS#StaticObject}. -- @field DCS#Object.Category TgtObjectCategory (UNIT/STATIC) The target object category ( Object.Category.UNIT or Object.Category.STATIC ). -- @field DCS#Unit TgtDCSUnit (UNIT/STATIC) The target @{DCS#Unit} or @{DCS#StaticObject}. @@ -288,19 +288,19 @@ EVENTS = { -- @field DCS#coalition.side TgtCoalition (UNIT) The coalition of the target. -- @field DCS#Unit.Category TgtCategory (UNIT) The category of the target. -- @field #string TgtTypeName (UNIT) The type name of the target. --- +-- -- @field DCS#Airbase place The @{DCS#Airbase} -- @field Wrapper.Airbase#AIRBASE Place The MOOSE airbase object. -- @field #string PlaceName The name of the airbase. --- +-- -- @field #table weapon The weapon used during the event. -- @field #table Weapon -- @field #string WeaponName Name of the weapon. -- @field DCS#Unit WeaponTgtDCSUnit Target DCS unit of the weapon. --- +-- -- @field Cargo.Cargo#CARGO Cargo The cargo object. -- @field #string CargoName The name of the cargo object. --- +-- -- @field Core.ZONE#ZONE Zone The zone object. -- @field #string ZoneName The name of the zone. @@ -311,255 +311,255 @@ local _EVENTMETA = { Order = 1, Side = "I", Event = "OnEventShot", - Text = "S_EVENT_SHOT" + Text = "S_EVENT_SHOT" }, [world.event.S_EVENT_HIT] = { Order = 1, Side = "T", Event = "OnEventHit", - Text = "S_EVENT_HIT" + Text = "S_EVENT_HIT" }, [world.event.S_EVENT_TAKEOFF] = { Order = 1, Side = "I", Event = "OnEventTakeoff", - Text = "S_EVENT_TAKEOFF" + Text = "S_EVENT_TAKEOFF" }, [world.event.S_EVENT_LAND] = { Order = 1, Side = "I", Event = "OnEventLand", - Text = "S_EVENT_LAND" + Text = "S_EVENT_LAND" }, [world.event.S_EVENT_CRASH] = { Order = -1, Side = "I", Event = "OnEventCrash", - Text = "S_EVENT_CRASH" + Text = "S_EVENT_CRASH" }, [world.event.S_EVENT_EJECTION] = { Order = 1, Side = "I", Event = "OnEventEjection", - Text = "S_EVENT_EJECTION" + Text = "S_EVENT_EJECTION" }, [world.event.S_EVENT_REFUELING] = { Order = 1, Side = "I", Event = "OnEventRefueling", - Text = "S_EVENT_REFUELING" + Text = "S_EVENT_REFUELING" }, [world.event.S_EVENT_DEAD] = { Order = -1, Side = "I", Event = "OnEventDead", - Text = "S_EVENT_DEAD" + Text = "S_EVENT_DEAD" }, [world.event.S_EVENT_PILOT_DEAD] = { Order = 1, Side = "I", Event = "OnEventPilotDead", - Text = "S_EVENT_PILOT_DEAD" + Text = "S_EVENT_PILOT_DEAD" }, [world.event.S_EVENT_BASE_CAPTURED] = { Order = 1, Side = "I", Event = "OnEventBaseCaptured", - Text = "S_EVENT_BASE_CAPTURED" + Text = "S_EVENT_BASE_CAPTURED" }, [world.event.S_EVENT_MISSION_START] = { Order = 1, Side = "N", Event = "OnEventMissionStart", - Text = "S_EVENT_MISSION_START" + Text = "S_EVENT_MISSION_START" }, [world.event.S_EVENT_MISSION_END] = { Order = 1, Side = "N", Event = "OnEventMissionEnd", - Text = "S_EVENT_MISSION_END" + Text = "S_EVENT_MISSION_END" }, [world.event.S_EVENT_TOOK_CONTROL] = { Order = 1, Side = "N", Event = "OnEventTookControl", - Text = "S_EVENT_TOOK_CONTROL" + Text = "S_EVENT_TOOK_CONTROL" }, [world.event.S_EVENT_REFUELING_STOP] = { Order = 1, Side = "I", Event = "OnEventRefuelingStop", - Text = "S_EVENT_REFUELING_STOP" + Text = "S_EVENT_REFUELING_STOP" }, [world.event.S_EVENT_BIRTH] = { Order = 1, Side = "I", Event = "OnEventBirth", - Text = "S_EVENT_BIRTH" + Text = "S_EVENT_BIRTH" }, [world.event.S_EVENT_HUMAN_FAILURE] = { Order = 1, Side = "I", Event = "OnEventHumanFailure", - Text = "S_EVENT_HUMAN_FAILURE" + Text = "S_EVENT_HUMAN_FAILURE" }, [world.event.S_EVENT_ENGINE_STARTUP] = { Order = 1, Side = "I", Event = "OnEventEngineStartup", - Text = "S_EVENT_ENGINE_STARTUP" + Text = "S_EVENT_ENGINE_STARTUP" }, [world.event.S_EVENT_ENGINE_SHUTDOWN] = { Order = 1, Side = "I", Event = "OnEventEngineShutdown", - Text = "S_EVENT_ENGINE_SHUTDOWN" + Text = "S_EVENT_ENGINE_SHUTDOWN" }, [world.event.S_EVENT_PLAYER_ENTER_UNIT] = { Order = 1, Side = "I", Event = "OnEventPlayerEnterUnit", - Text = "S_EVENT_PLAYER_ENTER_UNIT" + Text = "S_EVENT_PLAYER_ENTER_UNIT" }, [world.event.S_EVENT_PLAYER_LEAVE_UNIT] = { Order = -1, Side = "I", Event = "OnEventPlayerLeaveUnit", - Text = "S_EVENT_PLAYER_LEAVE_UNIT" + Text = "S_EVENT_PLAYER_LEAVE_UNIT" }, [world.event.S_EVENT_PLAYER_COMMENT] = { Order = 1, Side = "I", Event = "OnEventPlayerComment", - Text = "S_EVENT_PLAYER_COMMENT" + Text = "S_EVENT_PLAYER_COMMENT" }, [world.event.S_EVENT_SHOOTING_START] = { Order = 1, Side = "I", Event = "OnEventShootingStart", - Text = "S_EVENT_SHOOTING_START" + Text = "S_EVENT_SHOOTING_START" }, [world.event.S_EVENT_SHOOTING_END] = { Order = 1, Side = "I", Event = "OnEventShootingEnd", - Text = "S_EVENT_SHOOTING_END" + Text = "S_EVENT_SHOOTING_END" }, [world.event.S_EVENT_MARK_ADDED] = { Order = 1, Side = "I", Event = "OnEventMarkAdded", - Text = "S_EVENT_MARK_ADDED" + Text = "S_EVENT_MARK_ADDED" }, [world.event.S_EVENT_MARK_CHANGE] = { Order = 1, Side = "I", Event = "OnEventMarkChange", - Text = "S_EVENT_MARK_CHANGE" + Text = "S_EVENT_MARK_CHANGE" }, [world.event.S_EVENT_MARK_REMOVED] = { Order = 1, Side = "I", Event = "OnEventMarkRemoved", - Text = "S_EVENT_MARK_REMOVED" + Text = "S_EVENT_MARK_REMOVED" }, [EVENTS.NewCargo] = { Order = 1, Event = "OnEventNewCargo", - Text = "S_EVENT_NEW_CARGO" + Text = "S_EVENT_NEW_CARGO" }, [EVENTS.DeleteCargo] = { Order = 1, Event = "OnEventDeleteCargo", - Text = "S_EVENT_DELETE_CARGO" + Text = "S_EVENT_DELETE_CARGO" }, [EVENTS.NewZone] = { Order = 1, Event = "OnEventNewZone", - Text = "S_EVENT_NEW_ZONE" + Text = "S_EVENT_NEW_ZONE" }, [EVENTS.DeleteZone] = { Order = 1, Event = "OnEventDeleteZone", - Text = "S_EVENT_DELETE_ZONE" + Text = "S_EVENT_DELETE_ZONE" }, [EVENTS.NewZoneGoal] = { Order = 1, Event = "OnEventNewZoneGoal", - Text = "S_EVENT_NEW_ZONE_GOAL" + Text = "S_EVENT_NEW_ZONE_GOAL" }, [EVENTS.DeleteZoneGoal] = { Order = 1, Event = "OnEventDeleteZoneGoal", - Text = "S_EVENT_DELETE_ZONE_GOAL" + Text = "S_EVENT_DELETE_ZONE_GOAL" }, [EVENTS.RemoveUnit] = { Order = -1, Event = "OnEventRemoveUnit", - Text = "S_EVENT_REMOVE_UNIT" + Text = "S_EVENT_REMOVE_UNIT" }, [EVENTS.PlayerEnterAircraft] = { Order = 1, Event = "OnEventPlayerEnterAircraft", - Text = "S_EVENT_PLAYER_ENTER_AIRCRAFT" - }, + Text = "S_EVENT_PLAYER_ENTER_AIRCRAFT" + }, -- Added with DCS 2.5.6 [EVENTS.DetailedFailure] = { Order = 1, Event = "OnEventDetailedFailure", - Text = "S_EVENT_DETAILED_FAILURE" + Text = "S_EVENT_DETAILED_FAILURE" }, [EVENTS.Kill] = { Order = 1, Event = "OnEventKill", - Text = "S_EVENT_KILL" + Text = "S_EVENT_KILL" }, [EVENTS.Score] = { Order = 1, Event = "OnEventScore", - Text = "S_EVENT_SCORE" + Text = "S_EVENT_SCORE" }, [EVENTS.UnitLost] = { Order = 1, Event = "OnEventUnitLost", - Text = "S_EVENT_UNIT_LOST" + Text = "S_EVENT_UNIT_LOST" }, [EVENTS.LandingAfterEjection] = { Order = 1, Event = "OnEventLandingAfterEjection", - Text = "S_EVENT_LANDING_AFTER_EJECTION" + Text = "S_EVENT_LANDING_AFTER_EJECTION" }, -- Added with DCS 2.7.0 [EVENTS.ParatrooperLanding] = { Order = 1, Event = "OnEventParatrooperLanding", - Text = "S_EVENT_PARATROOPER_LENDING" + Text = "S_EVENT_PARATROOPER_LENDING" }, [EVENTS.DiscardChairAfterEjection] = { Order = 1, Event = "OnEventDiscardChairAfterEjection", - Text = "S_EVENT_DISCARD_CHAIR_AFTER_EJECTION" + Text = "S_EVENT_DISCARD_CHAIR_AFTER_EJECTION" }, [EVENTS.WeaponAdd] = { Order = 1, Event = "OnEventWeaponAdd", - Text = "S_EVENT_WEAPON_ADD" + Text = "S_EVENT_WEAPON_ADD" }, [EVENTS.TriggerZone] = { Order = 1, Event = "OnEventTriggerZone", - Text = "S_EVENT_TRIGGER_ZONE" + Text = "S_EVENT_TRIGGER_ZONE" }, [EVENTS.LandingQualityMark] = { Order = 1, Event = "OnEventLandingQualityMark", - Text = "S_EVENT_LANDING_QUALITYMARK" + Text = "S_EVENT_LANDING_QUALITYMARK" }, [EVENTS.BDA] = { Order = 1, Event = "OnEventBDA", - Text = "S_EVENT_BDA" - }, + Text = "S_EVENT_BDA" + }, } @@ -574,10 +574,10 @@ function EVENT:New() -- Inherit base. local self = BASE:Inherit( self, BASE:New() ) - + -- Add world event handler. self.EventHandler = world.addEventHandler(self) - + return self end @@ -590,22 +590,22 @@ end function EVENT:Init( EventID, EventClass ) self:F3( { _EVENTMETA[EventID].Text, EventClass } ) - if not self.Events[EventID] then + if not self.Events[EventID] then -- Create a WEAK table to ensure that the garbage collector is cleaning the event links when the object usage is cleaned. self.Events[EventID] = {} end - + -- Each event has a subtable of EventClasses, ordered by EventPriority. local EventPriority = EventClass:GetEventPriority() - + if not self.Events[EventID][EventPriority] then self.Events[EventID][EventPriority] = setmetatable( {}, { __mode = "k" } ) - end + end if not self.Events[EventID][EventPriority][EventClass] then self.Events[EventID][EventPriority][EventClass] = {} end - + return self.Events[EventID][EventPriority][EventClass] end @@ -625,11 +625,11 @@ function EVENT:RemoveEvent( EventClass, EventID ) -- Events. self.Events = self.Events or {} self.Events[EventID] = self.Events[EventID] or {} - self.Events[EventID][EventPriority] = self.Events[EventID][EventPriority] or {} - + self.Events[EventID][EventPriority] = self.Events[EventID][EventPriority] or {} + -- Remove self.Events[EventID][EventPriority][EventClass] = nil - + return self end @@ -643,7 +643,7 @@ function EVENT:Reset( EventObject ) --R2.1 self:F( { "Resetting subscriptions for class: ", EventObject:GetClassNameAndID() } ) local EventPriority = EventObject:GetEventPriority() - + for EventID, EventData in pairs( self.Events ) do if self.EventsDead then if self.EventsDead[EventID] then @@ -665,14 +665,14 @@ end function EVENT:RemoveAll(EventClass) local EventClassName = EventClass:GetClassNameAndID() - + -- Get Event prio. local EventPriority = EventClass:GetEventPriority() - + for EventID, EventData in pairs( self.Events ) do self.Events[EventID][EventPriority][EventClass] = nil end - + return self end @@ -705,7 +705,7 @@ function EVENT:OnEventGeneric( EventFunction, EventClass, EventID ) local EventData = self:Init( EventID, EventClass ) EventData.EventFunction = EventFunction - + return self end @@ -753,12 +753,12 @@ do -- OnBirth -- @return #EVENT self function EVENT:OnBirthForTemplate( EventTemplate, EventFunction, EventClass ) self:F2( EventTemplate.name ) - + self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EVENTS.Birth ) - + return self end - + end do -- OnCrash @@ -771,16 +771,16 @@ do -- OnCrash -- @return #EVENT function EVENT:OnCrashForTemplate( EventTemplate, EventFunction, EventClass ) self:F2( EventTemplate.name ) - + self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EVENTS.Crash ) - + return self end end do -- OnDead - + --- Create an OnDead event handler for a group -- @param #EVENT self -- @param Wrapper.Group#GROUP EventGroup The GROUP object. @@ -789,12 +789,12 @@ do -- OnDead -- @return #EVENT self function EVENT:OnDeadForTemplate( EventTemplate, EventFunction, EventClass ) self:F2( EventTemplate.name ) - + self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EVENTS.Dead ) - + return self end - + end @@ -808,12 +808,12 @@ do -- OnLand -- @return #EVENT self function EVENT:OnLandForTemplate( EventTemplate, EventFunction, EventClass ) self:F2( EventTemplate.name ) - + self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EVENTS.Land ) - + return self end - + end do -- OnTakeOff @@ -826,12 +826,12 @@ do -- OnTakeOff -- @return #EVENT self function EVENT:OnTakeOffForTemplate( EventTemplate, EventFunction, EventClass ) self:F2( EventTemplate.name ) - + self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EVENTS.Takeoff ) - + return self end - + end do -- OnEngineShutDown @@ -844,12 +844,12 @@ do -- OnEngineShutDown -- @return #EVENT function EVENT:OnEngineShutDownForTemplate( EventTemplate, EventFunction, EventClass ) self:F2( EventTemplate.name ) - + self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EVENTS.EngineShutdown ) - + return self end - + end do -- Event Creation @@ -859,13 +859,13 @@ do -- Event Creation -- @param AI.AI_Cargo#AI_CARGO Cargo The Cargo created. function EVENT:CreateEventNewCargo( Cargo ) self:F( { Cargo } ) - + local Event = { id = EVENTS.NewCargo, time = timer.getTime(), cargo = Cargo, } - + world.onEvent( Event ) end @@ -874,13 +874,13 @@ do -- Event Creation -- @param AI.AI_Cargo#AI_CARGO Cargo The Cargo created. function EVENT:CreateEventDeleteCargo( Cargo ) self:F( { Cargo } ) - + local Event = { id = EVENTS.DeleteCargo, time = timer.getTime(), cargo = Cargo, } - + world.onEvent( Event ) end @@ -889,13 +889,13 @@ do -- Event Creation -- @param Core.Zone#ZONE_BASE Zone The Zone created. function EVENT:CreateEventNewZone( Zone ) self:F( { Zone } ) - + local Event = { id = EVENTS.NewZone, time = timer.getTime(), zone = Zone, } - + world.onEvent( Event ) end @@ -904,13 +904,13 @@ do -- Event Creation -- @param Core.Zone#ZONE_BASE Zone The Zone created. function EVENT:CreateEventDeleteZone( Zone ) self:F( { Zone } ) - + local Event = { id = EVENTS.DeleteZone, time = timer.getTime(), zone = Zone, } - + world.onEvent( Event ) end @@ -919,13 +919,13 @@ do -- Event Creation -- @param Core.Functional#ZONE_GOAL ZoneGoal The ZoneGoal created. function EVENT:CreateEventNewZoneGoal( ZoneGoal ) self:F( { ZoneGoal } ) - + local Event = { id = EVENTS.NewZoneGoal, time = timer.getTime(), ZoneGoal = ZoneGoal, } - + world.onEvent( Event ) end @@ -935,13 +935,13 @@ do -- Event Creation -- @param Core.ZoneGoal#ZONE_GOAL ZoneGoal The ZoneGoal created. function EVENT:CreateEventDeleteZoneGoal( ZoneGoal ) self:F( { ZoneGoal } ) - + local Event = { id = EVENTS.DeleteZoneGoal, time = timer.getTime(), ZoneGoal = ZoneGoal, } - + world.onEvent( Event ) end @@ -951,30 +951,30 @@ do -- Event Creation -- @param Wrapper.Unit#UNIT PlayerUnit. function EVENT:CreateEventPlayerEnterUnit( PlayerUnit ) self:F( { PlayerUnit } ) - + local Event = { id = EVENTS.PlayerEnterUnit, time = timer.getTime(), initiator = PlayerUnit:GetDCSObject() } - + world.onEvent( Event ) end - + --- Creation of a S_EVENT_PLAYER_ENTER_AIRCRAFT event. -- @param #EVENT self -- @param Wrapper.Unit#UNIT PlayerUnit The aircraft unit the player entered. function EVENT:CreateEventPlayerEnterAircraft( PlayerUnit ) self:F( { PlayerUnit } ) - + local Event = { id = EVENTS.PlayerEnterAircraft, time = timer.getTime(), initiator = PlayerUnit:GetDCSObject() } - + world.onEvent( Event ) - end + end end @@ -989,31 +989,27 @@ function EVENT:onEvent( Event ) if BASE.Debug ~= nil then env.info( debug.traceback() ) end - + return errmsg end -- Get event meta data. local EventMeta = _EVENTMETA[Event.id] - + -- Check if this is a known event? if EventMeta then - - if self and - self.Events and - self.Events[Event.id] and - self.MissionEnd == false and - ( Event.initiator ~= nil or ( Event.initiator == nil and Event.id ~= EVENTS.PlayerLeaveUnit ) ) then - + + if self and self.Events and self.Events[Event.id] and self.MissionEnd==false and (Event.initiator~=nil or (Event.initiator==nil and Event.id~=EVENTS.PlayerLeaveUnit)) then + if Event.id and Event.id == EVENTS.MissionEnd then self.MissionEnd = true end - - if Event.initiator then - + + if Event.initiator then + Event.IniObjectCategory = Event.initiator:getCategory() - + if Event.IniObjectCategory == Object.Category.UNIT then Event.IniDCSUnit = Event.initiator Event.IniDCSUnitName = Event.IniDCSUnit:getName() @@ -1037,11 +1033,12 @@ function EVENT:onEvent( Event ) Event.IniTypeName = Event.IniDCSUnit:getTypeName() Event.IniCategory = Event.IniDCSUnit:getDesc().category end - + if Event.IniObjectCategory == Object.Category.STATIC then + if Event.id==31 then - --env.info("FF event 31") - -- Event.initiator is a Static object representing the pilot. But getName() error due to DCS bug. + + -- Event.initiator is a Static object representing the pilot. But getName() errors due to DCS bug. Event.IniDCSUnit = Event.initiator local ID=Event.initiator.id_ Event.IniDCSUnitName = string.format("Ejected Pilot ID %s", tostring(ID)) @@ -1067,7 +1064,7 @@ function EVENT:onEvent( Event ) Event.IniTypeName = Event.IniDCSUnit:getTypeName() end end - + if Event.IniObjectCategory == Object.Category.CARGO then Event.IniDCSUnit = Event.initiator Event.IniDCSUnitName = Event.IniDCSUnit:getName() @@ -1077,7 +1074,7 @@ function EVENT:onEvent( Event ) Event.IniCategory = Event.IniDCSUnit:getDesc().category Event.IniTypeName = Event.IniDCSUnit:getTypeName() end - + if Event.IniObjectCategory == Object.Category.SCENERY then Event.IniDCSUnit = Event.initiator Event.IniDCSUnitName = Event.IniDCSUnit:getName() @@ -1086,7 +1083,7 @@ function EVENT:onEvent( Event ) Event.IniCategory = Event.IniDCSUnit:getDesc().category Event.IniTypeName = Event.initiator:isExist() and Event.IniDCSUnit:getTypeName() or "SCENERY" -- TODO: Bug fix for 2.1! end - + if Event.IniObjectCategory == Object.Category.BASE then Event.IniDCSUnit = Event.initiator Event.IniDCSUnitName = Event.IniDCSUnit:getName() @@ -1094,15 +1091,15 @@ function EVENT:onEvent( Event ) Event.IniUnit = AIRBASE:FindByName(Event.IniDCSUnitName) Event.IniCoalition = Event.IniDCSUnit:getCoalition() Event.IniCategory = Event.IniDCSUnit:getDesc().category - Event.IniTypeName = Event.IniDCSUnit:getTypeName() + Event.IniTypeName = Event.IniDCSUnit:getTypeName() end end - + if Event.target then - + Event.TgtObjectCategory = Event.target:getCategory() - - if Event.TgtObjectCategory == Object.Category.UNIT then + + if Event.TgtObjectCategory == Object.Category.UNIT then Event.TgtDCSUnit = Event.target Event.TgtDCSGroup = Event.TgtDCSUnit:getGroup() Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() @@ -1121,38 +1118,37 @@ function EVENT:onEvent( Event ) Event.TgtCategory = Event.TgtDCSUnit:getDesc().category Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() end - + if Event.TgtObjectCategory == Object.Category.STATIC then - BASE:T({StaticTgtEvent = Event.id}) -- get base data - Event.TgtDCSUnit = Event.target - if Event.target:isExist() and Event.id ~= 33 then -- leave out ejected seat object - Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() + Event.TgtDCSUnit = Event.target + if Event.target:isExist() and Event.id ~= 33 then -- leave out ejected seat object + Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() + Event.TgtUnitName = Event.TgtDCSUnitName + Event.TgtUnit = STATIC:FindByName( Event.TgtDCSUnitName, false ) + Event.TgtCoalition = Event.TgtDCSUnit:getCoalition() + Event.TgtCategory = Event.TgtDCSUnit:getDesc().category + Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() + else + Event.TgtDCSUnitName = string.format("No target object for Event ID %s", tostring(Event.id)) + Event.TgtUnitName = Event.TgtDCSUnitName + Event.TgtUnit = nil + Event.TgtCoalition = 0 + Event.TgtCategory = 0 + if Event.id == 6 then + Event.TgtTypeName = "Ejected Pilot" + Event.TgtDCSUnitName = string.format("Ejected Pilot ID %s", tostring(Event.IniDCSUnitName)) + Event.TgtUnitName = Event.TgtDCSUnitName + elseif Event.id == 33 then + Event.TgtTypeName = "Ejection Seat" + Event.TgtDCSUnitName = string.format("Ejection Seat ID %s", tostring(Event.IniDCSUnitName)) Event.TgtUnitName = Event.TgtDCSUnitName - Event.TgtUnit = STATIC:FindByName( Event.TgtDCSUnitName, false ) - Event.TgtCoalition = Event.TgtDCSUnit:getCoalition() - Event.TgtCategory = Event.TgtDCSUnit:getDesc().category - Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() else - Event.TgtDCSUnitName = string.format("No target object for Event ID %s", tostring(Event.id)) - Event.TgtUnitName = Event.TgtDCSUnitName - Event.TgtUnit = nil - Event.TgtCoalition = 0 - Event.TgtCategory = 0 - if Event.id == 6 then - Event.TgtTypeName = "Ejected Pilot" - Event.TgtDCSUnitName = string.format("Ejected Pilot ID %s", tostring(Event.IniDCSUnitName)) - Event.TgtUnitName = Event.TgtDCSUnitName - elseif Event.id == 33 then - Event.TgtTypeName = "Ejection Seat" - Event.TgtDCSUnitName = string.format("Ejection Seat ID %s", tostring(Event.IniDCSUnitName)) - Event.TgtUnitName = Event.TgtDCSUnitName - else - Event.TgtTypeName = "Static" - end - end + Event.TgtTypeName = "Static" + end + end end - + if Event.TgtObjectCategory == Object.Category.SCENERY then Event.TgtDCSUnit = Event.target Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() @@ -1162,7 +1158,7 @@ function EVENT:onEvent( Event ) Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() end end - + if Event.weapon then Event.Weapon = Event.weapon Event.WeaponName = Event.Weapon:getTypeName() @@ -1173,20 +1169,20 @@ function EVENT:onEvent( Event ) Event.WeaponTypeName = Event.WeaponUNIT and Event.Weapon:getTypeName() --Event.WeaponTgtDCSUnit = Event.Weapon:getTarget() end - - -- Place should be given for takeoff and landing events as well as base captured. It should be a DCS airbase. - if Event.place then + + -- Place should be given for takeoff and landing events as well as base captured. It should be a DCS airbase. + if Event.place then if Event.id==EVENTS.LandingAfterEjection then -- Place is here the UNIT of which the pilot ejected. --local name=Event.place:getName() -- This returns a DCS error "Airbase doesn't exit" :( -- However, this is not a big thing, as the aircraft the pilot ejected from is usually long crashed before the ejected pilot touches the ground. --Event.Place=UNIT:Find(Event.place) - else + else Event.Place=AIRBASE:Find(Event.place) Event.PlaceName=Event.Place:GetName() end end - + -- Mark points. if Event.idx then Event.MarkID=Event.idx @@ -1196,80 +1192,80 @@ function EVENT:onEvent( Event ) Event.MarkCoalition=Event.coalition Event.MarkGroupID = Event.groupID end - + if Event.cargo then Event.Cargo = Event.cargo Event.CargoName = Event.cargo.Name end - + if Event.zone then Event.Zone = Event.zone Event.ZoneName = Event.zone.ZoneName end - + local PriorityOrder = EventMeta.Order local PriorityBegin = PriorityOrder == -1 and 5 or 1 local PriorityEnd = PriorityOrder == -1 and 1 or 5 - + if Event.IniObjectCategory ~= Object.Category.STATIC then self:F( { EventMeta.Text, Event, Event.IniDCSUnitName, Event.TgtDCSUnitName, PriorityOrder } ) end - + for EventPriority = PriorityBegin, PriorityEnd, PriorityOrder do - + if self.Events[Event.id][EventPriority] then - + -- Okay, we got the event from DCS. Now loop the SORTED self.EventSorted[] table for the received Event.id, and for each EventData registered, check if a function needs to be called. for EventClass, EventData in pairs( self.Events[Event.id][EventPriority] ) do - + --if Event.IniObjectCategory ~= Object.Category.STATIC then -- self:E( { "Evaluating: ", EventClass:GetClassNameAndID() } ) --end - + Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) Event.TgtGroup = GROUP:FindByName( Event.TgtDCSGroupName ) - + -- If the EventData is for a UNIT, the call directly the EventClass EventFunction for that UNIT. if EventData.EventUnit then - + -- So now the EventClass must be a UNIT class!!! We check if it is still "Alive". if EventClass:IsAlive() or - Event.id == EVENTS.PlayerEnterUnit or - Event.id == EVENTS.Crash or - Event.id == EVENTS.Dead or + Event.id == EVENTS.PlayerEnterUnit or + Event.id == EVENTS.Crash or + Event.id == EVENTS.Dead or Event.id == EVENTS.RemoveUnit then - + local UnitName = EventClass:GetName() - - if ( EventMeta.Side == "I" and UnitName == Event.IniDCSUnitName ) or + + if ( EventMeta.Side == "I" and UnitName == Event.IniDCSUnitName ) or ( EventMeta.Side == "T" and UnitName == Event.TgtDCSUnitName ) then - + -- First test if a EventFunction is Set, otherwise search for the default function if EventData.EventFunction then - + if Event.IniObjectCategory ~= 3 then self:F( { "Calling EventFunction for UNIT ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventPriority } ) end - - local Result, Value = xpcall( - function() - return EventData.EventFunction( EventClass, Event ) + + local Result, Value = xpcall( + function() + return EventData.EventFunction( EventClass, Event ) end, ErrorHandler ) - + else - + -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. local EventFunction = EventClass[ EventMeta.Event ] if EventFunction and type( EventFunction ) == "function" then - + -- Now call the default event function. if Event.IniObjectCategory ~= 3 then self:F( { "Calling " .. EventMeta.Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } ) end - - local Result, Value = xpcall( - function() - return EventFunction( EventClass, Event ) + + local Result, Value = xpcall( + function() + return EventFunction( EventClass, Event ) end, ErrorHandler ) end end @@ -1278,102 +1274,102 @@ function EVENT:onEvent( Event ) -- The EventClass is not alive anymore, we remove it from the EventHandlers... self:RemoveEvent( EventClass, Event.id ) end - + else - + --- If the EventData is for a GROUP, the call directly the EventClass EventFunction for the UNIT in that GROUP. if EventData.EventGroup then - + -- So now the EventClass must be a GROUP class!!! We check if it is still "Alive". if EventClass:IsAlive() or Event.id == EVENTS.PlayerEnterUnit or Event.id == EVENTS.Crash or Event.id == EVENTS.Dead or Event.id == EVENTS.RemoveUnit then - + -- We can get the name of the EventClass, which is now always a GROUP object. local GroupName = EventClass:GetName() - - if ( EventMeta.Side == "I" and GroupName == Event.IniDCSGroupName ) or + + if ( EventMeta.Side == "I" and GroupName == Event.IniDCSGroupName ) or ( EventMeta.Side == "T" and GroupName == Event.TgtDCSGroupName ) then - + -- First test if a EventFunction is Set, otherwise search for the default function if EventData.EventFunction then - + if Event.IniObjectCategory ~= 3 then self:F( { "Calling EventFunction for GROUP ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventPriority } ) end - - local Result, Value = xpcall( - function() - return EventData.EventFunction( EventClass, Event, unpack( EventData.Params ) ) + + local Result, Value = xpcall( + function() + return EventData.EventFunction( EventClass, Event, unpack( EventData.Params ) ) end, ErrorHandler ) - + else - + -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. local EventFunction = EventClass[ EventMeta.Event ] if EventFunction and type( EventFunction ) == "function" then - + -- Now call the default event function. if Event.IniObjectCategory ~= 3 then self:F( { "Calling " .. EventMeta.Event .. " for GROUP ", EventClass:GetClassNameAndID(), EventPriority } ) end - - local Result, Value = xpcall( - function() - return EventFunction( EventClass, Event, unpack( EventData.Params ) ) + + local Result, Value = xpcall( + function() + return EventFunction( EventClass, Event, unpack( EventData.Params ) ) end, ErrorHandler ) end end end else -- The EventClass is not alive anymore, we remove it from the EventHandlers... - --self:RemoveEvent( EventClass, Event.id ) + --self:RemoveEvent( EventClass, Event.id ) end else - + -- If the EventData is not bound to a specific unit, then call the EventClass EventFunction. -- Note that here the EventFunction will need to implement and determine the logic for the relevant source- or target unit, or weapon. if not EventData.EventUnit then - + -- First test if a EventFunction is Set, otherwise search for the default function if EventData.EventFunction then - + -- There is an EventFunction defined, so call the EventFunction. if Event.IniObjectCategory ~= 3 then self:F2( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), EventPriority } ) - end - local Result, Value = xpcall( - function() - return EventData.EventFunction( EventClass, Event ) + end + local Result, Value = xpcall( + function() + return EventData.EventFunction( EventClass, Event ) end, ErrorHandler ) else - + -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. local EventFunction = EventClass[ EventMeta.Event ] if EventFunction and type( EventFunction ) == "function" then - + -- Now call the default event function. if Event.IniObjectCategory ~= 3 then self:F2( { "Calling " .. EventMeta.Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } ) end - - local Result, Value = xpcall( - function() + + local Result, Value = xpcall( + function() local Result, Value = EventFunction( EventClass, Event ) - return Result, Value + return Result, Value end, ErrorHandler ) end end - + end end end end end end - + -- When cargo was deleted, it may probably be because of an S_EVENT_DEAD. -- However, in the loading logic, an S_EVENT_DEAD is also generated after a Destroy() call. -- And this is a problem because it will remove all entries from the SET_CARGOs. @@ -1384,12 +1380,12 @@ function EVENT:onEvent( Event ) Event.Cargo.NoDestroy = nil end else - self:T( { EventMeta.Text, Event } ) + self:T( { EventMeta.Text, Event } ) end else self:E(string.format("WARNING: Could not get EVENTMETA data for event ID=%d! Is this an unknown/new DCS event?", tostring(Event.id))) end - + Event = nil end diff --git a/Moose Development/Moose/Core/Fsm.lua b/Moose Development/Moose/Core/Fsm.lua index 0b49c3449..0ada59e71 100644 --- a/Moose Development/Moose/Core/Fsm.lua +++ b/Moose Development/Moose/Core/Fsm.lua @@ -540,7 +540,7 @@ do -- FSM --- Returns a table with the scores defined. -- @param #FSM self - -- @param #table Scores. + -- @return #table Scores. function FSM:GetScores() return self._Scores or {} end diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index dff81d026..354b584c1 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -164,6 +164,7 @@ do -- COORDINATE -- -- * @{#COORDINATE.WaypointAir}(): Build an air route point. -- * @{#COORDINATE.WaypointGround}(): Build a ground route point. + -- * @{#COORDINATE.WaypointNaval}(): Build a naval route point. -- -- Route points can be used in the Route methods of the @{Wrapper.Group#GROUP} class. -- @@ -183,10 +184,18 @@ do -- COORDINATE -- -- ## 9) Coordinate text generation -- - -- -- * @{#COORDINATE.ToStringBR}(): Generates a Bearing & Range text in the format of DDD for DI where DDD is degrees and DI is distance. -- * @{#COORDINATE.ToStringLL}(): Generates a Latutude & Longutude text. -- + -- ## 10) Drawings on F10 map + -- + -- * @{#COORDINATE.CircleToAll}(): Draw a circle on the F10 map. + -- * @{#COORDINATE.LineToAll}(): Draw a line on the F10 map. + -- * @{#COORDINATE.RectToAll}(): Draw a rectangle on the F10 map. + -- * @{#COORDINATE.QuadToAll}(): Draw a shape with four points on the F10 map. + -- * @{#COORDINATE.TextToAll}(): Write some text on the F10 map. + -- * @{#COORDINATE.ArrowToAll}(): Draw an arrow on the F10 map. + -- -- @field #COORDINATE COORDINATE = { ClassName = "COORDINATE", @@ -675,9 +684,9 @@ do -- COORDINATE --- Return a random Coordinate within an Outer Radius and optionally NOT within an Inner Radius of the COORDINATE. -- @param #COORDINATE self - -- @param DCS#Distance OuterRadius - -- @param DCS#Distance InnerRadius - -- @return #COORDINATE + -- @param DCS#Distance OuterRadius Outer radius in meters. + -- @param DCS#Distance InnerRadius Inner radius in meters. + -- @return #COORDINATE self function COORDINATE:GetRandomCoordinateInRadius( OuterRadius, InnerRadius ) self:F2( { OuterRadius, InnerRadius } ) @@ -1598,8 +1607,12 @@ do -- COORDINATE roadtype="railroads" end local x,y = land.getClosestPointOnRoads(roadtype, self.x, self.z) - local vec2={ x = x, y = y } - return COORDINATE:NewFromVec2(vec2) + local coord=nil + if x and y then + local vec2={ x = x, y = y } + coord=COORDINATE:NewFromVec2(vec2) + end + return coord end @@ -2039,13 +2052,13 @@ do -- COORDINATE -- @param #COORDINATE self -- @param #COORDINATE Endpoint COORDIANTE to where the line is drawn. -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. - -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red (default). -- @param #number Alpha Transparency [0,1]. Default 1. + -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. -- @param #string Text (Optional) Text displayed when mark is added. Default none. - -- @return #number The resulting Mark ID which is a number. - function COORDINATE:LineToAll(Endpoint, Coalition, LineType, Color, Alpha, ReadOnly, Text) + -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again. + function COORDINATE:LineToAll(Endpoint, Coalition, Color, Alpha, LineType, ReadOnly, Text) local MarkID = UTILS.GetMarkID() if ReadOnly==nil then ReadOnly=false @@ -2062,18 +2075,17 @@ do -- COORDINATE --- Circle to all. -- Creates a circle on the map with a given radius, color, fill color, and outline. -- @param #COORDINATE self - -- @param #COORDINATE Center COORDIANTE of the center of the circle. -- @param #numberr Radius Radius in meters. Default 1000 m. -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. - -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red (default). -- @param #number Alpha Transparency [0,1]. Default 1. - -- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red (default). - -- @param #number FillAlpha Transparency [0,1]. Default 0.5. + -- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. + -- @param #number FillAlpha Transparency [0,1]. Default 0.15. + -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. -- @param #string Text (Optional) Text displayed when mark is added. Default none. - -- @return #number The resulting Mark ID which is a number. - function COORDINATE:CircleToAll(Radius, Coalition, LineType, Color, Alpha, FillColor, FillAlpha, ReadOnly, Text) + -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again. + function COORDINATE:CircleToAll(Radius, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly, Text) local MarkID = UTILS.GetMarkID() if ReadOnly==nil then ReadOnly=false @@ -2084,14 +2096,193 @@ do -- COORDINATE Color=Color or {1,0,0} Color[4]=Alpha or 1.0 LineType=LineType or 1 - FillColor=FillColor or {1,0,0} - FillColor[4]=FillAlpha or 0.5 + FillColor=FillColor or Color + FillColor[4]=FillAlpha or 0.15 trigger.action.circleToAll(Coalition, MarkID, vec3, Radius, Color, FillColor, LineType, ReadOnly, Text or "") return MarkID end end -- Markings + --- Rectangle to all. Creates a rectangle on the map from the COORDINATE in one corner to the end COORDINATE in the opposite corner. + -- Creates a line on the F10 map from one point to another. + -- @param #COORDINATE self + -- @param #COORDINATE Endpoint COORDIANTE in the opposite corner. + -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. + -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red (default). + -- @param #number Alpha Transparency [0,1]. Default 1. + -- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. + -- @param #number FillAlpha Transparency [0,1]. Default 0.15. + -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. + -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. + -- @param #string Text (Optional) Text displayed when mark is added. Default none. + -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again. + function COORDINATE:RectToAll(Endpoint, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly, Text) + local MarkID = UTILS.GetMarkID() + if ReadOnly==nil then + ReadOnly=false + end + local vec3=Endpoint:GetVec3() + Coalition=Coalition or -1 + Color=Color or {1,0,0} + Color[4]=Alpha or 1.0 + LineType=LineType or 1 + FillColor=FillColor or Color + FillColor[4]=FillAlpha or 0.15 + trigger.action.rectToAll(Coalition, MarkID, self:GetVec3(), vec3, Color, FillColor, LineType, ReadOnly, Text or "") + return MarkID + end + + --- Creates a shape defined by 4 points on the F10 map. The first point is the current COORDINATE. The remaining three points need to be specified. + -- @param #COORDINATE self + -- @param #COORDINATE Coord2 Second COORDIANTE of the quad shape. + -- @param #COORDINATE Coord3 Third COORDIANTE of the quad shape. + -- @param #COORDINATE Coord4 Fourth COORDIANTE of the quad shape. + -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. + -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red (default). + -- @param #number Alpha Transparency [0,1]. Default 1. + -- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. + -- @param #number FillAlpha Transparency [0,1]. Default 0.15. + -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. + -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. + -- @param #string Text (Optional) Text displayed when mark is added. Default none. + -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again. + function COORDINATE:QuadToAll(Coord2, Coord3, Coord4, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly, Text) + local MarkID = UTILS.GetMarkID() + if ReadOnly==nil then + ReadOnly=false + end + local point1=self:GetVec3() + local point2=Coord2:GetVec3() + local point3=Coord3:GetVec3() + local point4=Coord4:GetVec3() + Coalition=Coalition or -1 + Color=Color or {1,0,0} + Color[4]=Alpha or 1.0 + LineType=LineType or 1 + FillColor=FillColor or Color + FillColor[4]=FillAlpha or 0.15 + trigger.action.quadToAll(Coalition, MarkID, self:GetVec3(), point2, point3, point4, Color, FillColor, LineType, ReadOnly, Text or "") + return MarkID + end + + --- Creates a free form shape on the F10 map. The first point is the current COORDINATE. The remaining points need to be specified. + -- **NOTE**: A free form polygon must have **at least three points** in total and currently only **up to 10 points** in total are supported. + -- @param #COORDINATE self + -- @param #table Coordinates Table of coordinates of the remaining points of the shape. + -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. + -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red (default). + -- @param #number Alpha Transparency [0,1]. Default 1. + -- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. + -- @param #number FillAlpha Transparency [0,1]. Default 0.15. + -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. + -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. + -- @param #string Text (Optional) Text displayed when mark is added. Default none. + -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again. + function COORDINATE:MarkupToAllFreeForm(Coordinates, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly, Text) + + local MarkID = UTILS.GetMarkID() + if ReadOnly==nil then + ReadOnly=false + end + + Coalition=Coalition or -1 + + Color=Color or {1,0,0} + Color[4]=Alpha or 1.0 + + LineType=LineType or 1 + + FillColor=FillColor or UTILS.DeepCopy(Color) + FillColor[4]=FillAlpha or 0.15 + + local vecs={} + vecs[1]=self:GetVec3() + for i,coord in ipairs(Coordinates) do + vecs[i+1]=coord:GetVec3() + end + + if #vecs<3 then + self:E("ERROR: A free form polygon needs at least three points!") + elseif #vecs==3 then + trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], Color, FillColor, LineType, ReadOnly, Text or "") + elseif #vecs==4 then + trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], Color, FillColor, LineType, ReadOnly, Text or "") + elseif #vecs==5 then + trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], Color, FillColor, LineType, ReadOnly, Text or "") + elseif #vecs==6 then + trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], vecs[6], Color, FillColor, LineType, ReadOnly, Text or "") + elseif #vecs==7 then + trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], vecs[6], vecs[7], Color, FillColor, LineType, ReadOnly, Text or "") + elseif #vecs==8 then + trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], vecs[6], vecs[7], vecs[8], Color, FillColor, LineType, ReadOnly, Text or "") + elseif #vecs==9 then + trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], vecs[6], vecs[7], vecs[8], vecs[9], Color, FillColor, LineType, ReadOnly, Text or "") + elseif #vecs==10 then + trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], vecs[6], vecs[7], vecs[8], vecs[9], vecs[10], Color, FillColor, LineType, ReadOnly, Text or "") + else + self:E("ERROR: Currently a free form polygon can only have 10 points in total!") + -- Unfortunately, unpack(vecs) does not work! So no idea how to generalize this :( + trigger.action.markupToAll(7, Coalition, MarkID, unpack(vecs), Color, FillColor, LineType, ReadOnly, Text or "") + end + + return MarkID + end + + --- Text to all. Creates a text imposed on the map at the COORDINATE. Text scales with the map. + -- @param #COORDINATE self + -- @param #string Text Text displayed on the F10 map. + -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. + -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red (default). + -- @param #number Alpha Transparency [0,1]. Default 1. + -- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. + -- @param #number FillAlpha Transparency [0,1]. Default 0.3. + -- @param #number FontSize Font size. Default 14. + -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. + -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again. + function COORDINATE:TextToAll(Text, Coalition, Color, Alpha, FillColor, FillAlpha, FontSize, ReadOnly) + local MarkID = UTILS.GetMarkID() + if ReadOnly==nil then + ReadOnly=false + end + Coalition=Coalition or -1 + Color=Color or {1,0,0} + Color[4]=Alpha or 1.0 + FillColor=FillColor or Color + FillColor[4]=FillAlpha or 0.3 + FontSize=FontSize or 14 + trigger.action.textToAll(Coalition, MarkID, self:GetVec3(), Color, FillColor, FontSize, ReadOnly, Text or "Hello World") + return MarkID + end + + --- Arrow to all. Creates an arrow from the COORDINATE to the endpoint COORDINATE on the F10 map. There is no control over other dimensions of the arrow. + -- @param #COORDINATE self + -- @param #COORDINATE Endpoint COORDINATE where the tip of the arrow is pointing at. + -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. + -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red (default). + -- @param #number Alpha Transparency [0,1]. Default 1. + -- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. + -- @param #number FillAlpha Transparency [0,1]. Default 0.15. + -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. + -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. + -- @param #string Text (Optional) Text displayed when mark is added. Default none. + -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again. + function COORDINATE:ArrowToAll(Endpoint, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly, Text) + local MarkID = UTILS.GetMarkID() + if ReadOnly==nil then + ReadOnly=false + end + local vec3=Endpoint:GetVec3() + Coalition=Coalition or -1 + Color=Color or {1,0,0} + Color[4]=Alpha or 1.0 + LineType=LineType or 1 + FillColor=FillColor or Color + FillColor[4]=FillAlpha or 0.15 + --trigger.action.textToAll(Coalition, MarkID, self:GetVec3(), Color, FillColor, FontSize, ReadOnly, Text or "Hello World") + trigger.action.arrowToAll(Coalition, MarkID, vec3, self:GetVec3(), Color, FillColor, LineType, ReadOnly, Text or "") + return MarkID + end --- Returns if a Coordinate has Line of Sight (LOS) with the ToCoordinate. -- @param #COORDINATE self diff --git a/Moose Development/Moose/Core/Settings.lua b/Moose Development/Moose/Core/Settings.lua index 3a557de65..74b5fa54b 100644 --- a/Moose Development/Moose/Core/Settings.lua +++ b/Moose Development/Moose/Core/Settings.lua @@ -236,6 +236,7 @@ do -- SETTINGS --- SETTINGS constructor. -- @param #SETTINGS self + -- @param #string PlayerName (Optional) Set settings for this player. -- @return #SETTINGS function SETTINGS:Set( PlayerName ) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 89d6d681c..d25186b66 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -3408,8 +3408,8 @@ function SPAWN:_SpawnCleanUpScheduler() if Stamp.Vec2 then if SpawnUnit:InAir() == false and SpawnUnit:GetVelocityKMH() < 1 then local NewVec2 = SpawnUnit:GetVec2() - if Stamp.Vec2.x == NewVec2.x and Stamp.Vec2.y == NewVec2.y then - -- If the plane is not moving, and is on the ground, assign it with a timestamp... + if (Stamp.Vec2.x == NewVec2.x and Stamp.Vec2.y == NewVec2.y) or (SpawnUnit:GetLife() <= 1) then + -- If the plane is not moving or dead , and is on the ground, assign it with a timestamp... if Stamp.Time + self.SpawnCleanUpInterval < timer.getTime() then self:T( { "CleanUp Scheduler:", "ReSpawning:", SpawnGroup:GetName() } ) self:ReSpawn( SpawnCursor ) @@ -3427,7 +3427,7 @@ function SPAWN:_SpawnCleanUpScheduler() else if SpawnUnit:InAir() == false then Stamp.Vec2 = SpawnUnit:GetVec2() - if SpawnUnit:GetVelocityKMH() < 1 then + if (SpawnUnit:GetVelocityKMH() < 1) then Stamp.Time = timer.getTime() end else diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index 88d487622..65ac3cb9d 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -1,9 +1,9 @@ --- **Core** - Define zones within your mission of various forms, with various capabilities. --- +-- -- === --- +-- -- ## Features: --- +-- -- * Create radius zones. -- * Create trigger zones. -- * Create polygon zones. @@ -17,24 +17,25 @@ -- * Get zone properties. -- * Get zone bounding box. -- * Set/get zone name. --- --- +-- * Draw zones (circular and polygon) on the F10 map. +-- +-- -- There are essentially two core functions that zones accomodate: --- +-- -- * Test if an object is within the zone boundaries. -- * Provide the zone behaviour. Some zones are static, while others are moveable. --- +-- -- The object classes are using the zone classes to test the zone boundaries, which can take various forms: --- +-- -- * Test if completely within the zone. -- * Test if partly within the zone (for @{Wrapper.Group#GROUP} objects). -- * Test if not in the zone. -- * Distance to the nearest intersecting point of the zone. -- * Distance to the center of the zone. -- * ... --- +-- -- Each of these ZONE classes have a zone name, and specific parameters defining the zone type: --- +-- -- * @{#ZONE_BASE}: The ZONE_BASE class defining the base for all other zone classes. -- * @{#ZONE_RADIUS}: The ZONE_RADIUS class defined by a zone name, a location and a radius. -- * @{#ZONE}: The ZONE class, defined by the zone name as defined within the Mission Editor. @@ -42,47 +43,48 @@ -- * @{#ZONE_GROUP}: The ZONE_GROUP class defines by a zone around a @{Wrapper.Group#GROUP} with a radius. -- * @{#ZONE_POLYGON}: The ZONE_POLYGON class defines by a sequence of @{Wrapper.Group#GROUP} waypoints within the Mission Editor, forming a polygon. -- --- === --- --- ### Author: **FlightControl** --- ### Contributions: --- -- === --- +-- +-- ### Author: **FlightControl** +-- ### Contributions: +-- +-- === +-- -- @module Core.Zone --- @image Core_Zones.JPG +-- @image Core_Zones.JPG --- @type ZONE_BASE -- @field #string ZoneName Name of the zone. -- @field #number ZoneProbability A value between 0 and 1. 0 = 0% and 1 = 100% probability. --- @field Core.Point#COORDINATE Coordinate object of the zone. +-- @field #number DrawID Unique ID of the drawn zone on the F10 map. +-- @field #table Color Table with four entries, e.g. {1, 0, 0, 0.15}. First three are RGB color code. Fourth is the transparency Alpha value. -- @extends Core.Fsm#FSM --- This class is an abstract BASE class for derived classes, and is not meant to be instantiated. --- +-- -- ## Each zone has a name: --- +-- -- * @{#ZONE_BASE.GetName}(): Returns the name of the zone. -- * @{#ZONE_BASE.SetName}(): Sets the name of the zone. --- --- +-- +-- -- ## Each zone implements two polymorphic functions defined in @{Core.Zone#ZONE_BASE}: --- +-- -- * @{#ZONE_BASE.IsVec2InZone}(): Returns if a 2D vector is within the zone. -- * @{#ZONE_BASE.IsVec3InZone}(): Returns if a 3D vector is within the zone. -- * @{#ZONE_BASE.IsPointVec2InZone}(): Returns if a 2D point vector is within the zone. -- * @{#ZONE_BASE.IsPointVec3InZone}(): Returns if a 3D point vector is within the zone. --- +-- -- ## A zone has a probability factor that can be set to randomize a selection between zones: --- +-- -- * @{#ZONE_BASE.SetZoneProbability}(): Set the randomization probability of a zone to be selected, taking a value between 0 and 1 ( 0 = 0%, 1 = 100% ) -- * @{#ZONE_BASE.GetZoneProbability}(): Get the randomization probability of a zone to be selected, passing a value between 0 and 1 ( 0 = 0%, 1 = 100% ) -- * @{#ZONE_BASE.GetZoneMaybe}(): Get the zone taking into account the randomization probability. nil is returned if this zone is not a candidate. --- +-- -- ## A zone manages vectors: --- +-- -- * @{#ZONE_BASE.GetVec2}(): Returns the 2D vector coordinate of the zone. -- * @{#ZONE_BASE.GetVec3}(): Returns the 3D vector coordinate of the zone. -- * @{#ZONE_BASE.GetPointVec2}(): Returns the 2D point vector coordinate of the zone. @@ -90,22 +92,24 @@ -- * @{#ZONE_BASE.GetRandomVec2}(): Define a random 2D vector within the zone. -- * @{#ZONE_BASE.GetRandomPointVec2}(): Define a random 2D point vector within the zone. -- * @{#ZONE_BASE.GetRandomPointVec3}(): Define a random 3D point vector within the zone. --- +-- -- ## A zone has a bounding square: --- +-- -- * @{#ZONE_BASE.GetBoundingSquare}(): Get the outer most bounding square of the zone. --- --- ## A zone can be marked: --- +-- +-- ## A zone can be marked: +-- -- * @{#ZONE_BASE.SmokeZone}(): Smokes the zone boundaries in a color. -- * @{#ZONE_BASE.FlareZone}(): Flares the zone boundaries in a color. --- +-- -- @field #ZONE_BASE ZONE_BASE = { ClassName = "ZONE_BASE", ZoneName = "", ZoneProbability = 1, - } + DrawID=nil, + Color={} +} --- The ZONE_BASE.BoundingSquare @@ -125,7 +129,7 @@ function ZONE_BASE:New( ZoneName ) self:F( ZoneName ) self.ZoneName = ZoneName - + return self end @@ -202,7 +206,7 @@ end -- @param #ZONE_BASE self -- @return #nil. function ZONE_BASE:GetVec2() - return nil + return nil end --- Returns a @{Core.Point#POINT_VEC2} of the zone. @@ -211,14 +215,14 @@ end -- @return Core.Point#POINT_VEC2 The PointVec2 of the zone. function ZONE_BASE:GetPointVec2() self:F2( self.ZoneName ) - + local Vec2 = self:GetVec2() local PointVec2 = POINT_VEC2:NewFromVec2( Vec2 ) self:T2( { PointVec2 } ) - - return PointVec2 + + return PointVec2 end @@ -228,16 +232,16 @@ end -- @return DCS#Vec3 The Vec3 of the zone. function ZONE_BASE:GetVec3( Height ) self:F2( self.ZoneName ) - + Height = Height or 0 - + local Vec2 = self:GetVec2() local Vec3 = { x = Vec2.x, y = Height and Height or land.getHeight( self:GetVec2() ), z = Vec2.y } self:T2( { Vec3 } ) - - return Vec3 + + return Vec3 end --- Returns a @{Core.Point#POINT_VEC3} of the zone. @@ -246,14 +250,14 @@ end -- @return Core.Point#POINT_VEC3 The PointVec3 of the zone. function ZONE_BASE:GetPointVec3( Height ) self:F2( self.ZoneName ) - + local Vec3 = self:GetVec3( Height ) local PointVec3 = POINT_VEC3:NewFromVec3( Vec3 ) self:T2( { PointVec3 } ) - - return PointVec3 + + return PointVec3 end --- Returns a @{Core.Point#COORDINATE} of the zone. @@ -262,25 +266,25 @@ end -- @return Core.Point#COORDINATE The Coordinate of the zone. function ZONE_BASE:GetCoordinate( Height ) --R2.1 self:F2(self.ZoneName) - + local Vec3 = self:GetVec3( Height ) - + if self.Coordinate then - + -- Update coordinates. self.Coordinate.x=Vec3.x self.Coordinate.y=Vec3.y self.Coordinate.z=Vec3.z - + --env.info("FF GetCoordinate NEW for ZONE_BASE "..tostring(self.ZoneName)) else - -- Create a new coordinate object. + -- Create a new coordinate object. self.Coordinate=COORDINATE:NewFromVec3(Vec3) - + --env.info("FF GetCoordinate NEW for ZONE_BASE "..tostring(self.ZoneName)) end - + return self.Coordinate end @@ -321,12 +325,82 @@ function ZONE_BASE:BoundZone() end + +--- Set color of zone. +-- @param #ZONE_BASE self +-- @param #table RGBcolor RGB color table. Default `{1, 0, 0}`. +-- @param #number Alpha Transparacy between 0 and 1. Default 0.15. +-- @return #ZONE_BASE self +function ZONE_BASE:SetColor(RGBcolor, Alpha) + + RGBcolor=RGBcolor or {1, 0, 0} + Alpha=Alpha or 0.15 + + self.Color={} + self.Color[1]=RGBcolor[1] + self.Color[2]=RGBcolor[2] + self.Color[3]=RGBcolor[3] + self.Color[4]=Alpha + + return self +end + +--- Get color table of the zone. +-- @param #ZONE_BASE self +-- @return #table Table with four entries, e.g. {1, 0, 0, 0.15}. First three are RGB color code. Fourth is the transparency Alpha value. +function ZONE_BASE:GetColor() + return self.Color +end + +--- Get RGB color of zone. +-- @param #ZONE_BASE self +-- @return #table Table with three entries, e.g. {1, 0, 0}, which is the RGB color code. +function ZONE_BASE:GetColorRGB() + local rgb={} + rgb[1]=self.Color[1] + rgb[2]=self.Color[2] + rgb[3]=self.Color[3] + return rgb +end + +--- Get transperency Alpha value of zone. +-- @param #ZONE_BASE self +-- @return #number Alpha value. +function ZONE_BASE:GetColorAlpha() + local alpha=self.Color[4] + return alpha +end + +--- Remove the drawing of the zone from the F10 map. +-- @param #ZONE_BASE self +-- @param #number Delay (Optional) Delay before the drawing is removed. +-- @return #ZONE_BASE self +function ZONE_BASE:UndrawZone(Delay) + if Delay and Delay>0 then + self:ScheduleOnce(Delay, ZONE_BASE.UndrawZone, self) + else + if self.DrawID then + UTILS.RemoveMark(self.DrawID) + end + end + return self +end + +--- Get ID of the zone object drawn on the F10 map. +-- The ID can be used to remove the drawn object from the F10 map view via `UTILS.RemoveMark(MarkID)`. +-- @param #ZONE_BASE self +-- @return #number Unique ID of the +function ZONE_BASE:GetDrawID() + return self.DrawID +end + + --- Smokes the zone boundaries in a color. -- @param #ZONE_BASE self -- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. function ZONE_BASE:SmokeZone( SmokeColor ) self:F2( SmokeColor ) - + end --- Set the randomization probability of a zone to be selected. @@ -335,7 +409,7 @@ end -- @return #ZONE_BASE self function ZONE_BASE:SetZoneProbability( ZoneProbability ) self:F( { self:GetName(), ZoneProbability = ZoneProbability } ) - + self.ZoneProbability = ZoneProbability or 1 return self end @@ -344,8 +418,8 @@ end -- @param #ZONE_BASE self -- @return #number A value between 0 and 1. 0 = 0% and 1 = 100% probability. function ZONE_BASE:GetZoneProbability() - self:F2() - + self:F2() + return self.ZoneProbability end @@ -354,15 +428,15 @@ end -- @return #ZONE_BASE The zone is selected taking into account the randomization probability factor. -- @return #nil The zone is not selected taking into account the randomization probability factor. -- @usage --- +-- -- local ZoneArray = { ZONE:New( "Zone1" ), ZONE:New( "Zone2" ) } --- +-- -- -- We set a zone probability of 70% to the first zone and 30% to the second zone. -- ZoneArray[1]:SetZoneProbability( 0.5 ) -- ZoneArray[2]:SetZoneProbability( 0.5 ) --- +-- -- local ZoneSelected = nil --- +-- -- while ZoneSelected == nil do -- for _, Zone in pairs( ZoneArray ) do -- ZoneSelected = Zone:GetZoneMaybe() @@ -371,12 +445,12 @@ end -- end -- end -- end --- +-- -- -- The result should be that Zone1 would be more probable selected than Zone2. --- +-- function ZONE_BASE:GetZoneMaybe() self:F2() - + local Randomization = math.random() if Randomization <= self.ZoneProbability then return self @@ -394,30 +468,34 @@ end --- The ZONE_RADIUS class defined by a zone name, a location and a radius. -- This class implements the inherited functions from Core.Zone#ZONE_BASE taking into account the own zone format and properties. --- +-- -- ## ZONE_RADIUS constructor --- +-- -- * @{#ZONE_RADIUS.New}(): Constructor. --- +-- -- ## Manage the radius of the zone --- +-- -- * @{#ZONE_RADIUS.SetRadius}(): Sets the radius of the zone. -- * @{#ZONE_RADIUS.GetRadius}(): Returns the radius of the zone. --- +-- -- ## Manage the location of the zone --- +-- -- * @{#ZONE_RADIUS.SetVec2}(): Sets the @{DCS#Vec2} of the zone. -- * @{#ZONE_RADIUS.GetVec2}(): Returns the @{DCS#Vec2} of the zone. -- * @{#ZONE_RADIUS.GetVec3}(): Returns the @{DCS#Vec3} of the zone, taking an additional height parameter. --- +-- -- ## Zone point randomization --- +-- -- Various functions exist to find random points within the zone. --- +-- -- * @{#ZONE_RADIUS.GetRandomVec2}(): Gets a random 2D point in the zone. -- * @{#ZONE_RADIUS.GetRandomPointVec2}(): Gets a @{Core.Point#POINT_VEC2} object representing a random 2D point in the zone. -- * @{#ZONE_RADIUS.GetRandomPointVec3}(): Gets a @{Core.Point#POINT_VEC3} object representing a random 3D point in the zone. Note that the height of the point is at landheight. --- +-- +-- ## Draw zone +-- +-- * @{#ZONE_RADIUS.DrawZone}(): Draws the zone on the F10 map. +-- -- @field #ZONE_RADIUS ZONE_RADIUS = { ClassName="ZONE_RADIUS", @@ -437,9 +515,9 @@ function ZONE_RADIUS:New( ZoneName, Vec2, Radius ) self.Radius = Radius self.Vec2 = Vec2 - + --self.Coordinate=COORDINATE:NewFromVec2(Vec2) - + return self end @@ -491,18 +569,44 @@ function ZONE_RADIUS:MarkZone(Points) local Angle local RadialBase = math.pi*2 - + for Angle = 0, 360, (360 / Points ) do - + local Radial = Angle * RadialBase / 360 - + Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() - + COORDINATE:NewFromVec2(Point):MarkToAll(self:GetName()) end - + +end + +--- Draw the zone circle on the F10 map. +-- @param #ZONE_RADIUS self +-- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. +-- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red. +-- @param #number Alpha Transparency [0,1]. Default 1. +-- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. +-- @param #number FillAlpha Transparency [0,1]. Default 0.15. +-- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. +-- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. +-- @return #ZONE_RADIUS self +function ZONE_RADIUS:DrawZone(Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) + + local coordinate=self:GetCoordinate() + + local Radius=self:GetRadius() + + Color=Color or self:GetColorRGB() + Alpha=Alpha or 1 + FillColor=FillColor or Color + FillAlpha=FillAlpha or self:GetColorAlpha() + + self.DrawID=coordinate:CircleToAll(Radius, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) + + return self end --- Bounds the zone with tires. @@ -520,17 +624,17 @@ function ZONE_RADIUS:BoundZone( Points, CountryID, UnBound ) local Angle local RadialBase = math.pi*2 - + -- for Angle = 0, 360, (360 / Points ) do local Radial = Angle * RadialBase / 360 Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() - + local CountryName = _DATABASE.COUNTRY_NAME[CountryID] - + local Tire = { - ["country"] = CountryName, + ["country"] = CountryName, ["category"] = "Fortifications", ["canCargo"] = false, ["shape_name"] = "H-tyre_B_WF", @@ -564,7 +668,7 @@ function ZONE_RADIUS:SmokeZone( SmokeColor, Points, AddHeight, AngleOffset ) local Point = {} local Vec2 = self:GetVec2() - + AddHeight = AddHeight or 0 AngleOffset = AngleOffset or 0 @@ -572,7 +676,7 @@ function ZONE_RADIUS:SmokeZone( SmokeColor, Points, AddHeight, AngleOffset ) local Angle local RadialBase = math.pi*2 - + for Angle = 0, 360, 360 / Points do local Radial = ( Angle + AngleOffset ) * RadialBase / 360 Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() @@ -596,14 +700,14 @@ function ZONE_RADIUS:FlareZone( FlareColor, Points, Azimuth, AddHeight ) local Point = {} local Vec2 = self:GetVec2() - + AddHeight = AddHeight or 0 - + Points = Points and Points or 360 local Angle local RadialBase = math.pi*2 - + for Angle = 0, 360, 360 / Points do local Radial = Angle * RadialBase / 360 Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() @@ -645,8 +749,8 @@ function ZONE_RADIUS:GetVec2() self:F2( self.ZoneName ) self:T2( { self.Vec2 } ) - - return self.Vec2 + + return self.Vec2 end --- Sets the @{DCS#Vec2} of the zone. @@ -655,12 +759,12 @@ end -- @return DCS#Vec2 The new location of the zone. function ZONE_RADIUS:SetVec2( Vec2 ) self:F2( self.ZoneName ) - + self.Vec2 = Vec2 self:T2( { self.Vec2 } ) - - return self.Vec2 + + return self.Vec2 end --- Returns the @{DCS#Vec3} of the ZONE_RADIUS. @@ -676,8 +780,8 @@ function ZONE_RADIUS:GetVec3( Height ) local Vec3 = { x = Vec2.x, y = land.getHeight( self:GetVec2() ) + Height, z = Vec2.y } self:T2( { Vec3 } ) - - return Vec3 + + return Vec3 end @@ -686,7 +790,7 @@ end --- Scan the zone for the presence of units of the given ObjectCategories. -- Note that after a zone has been scanned, the zone can be evaluated by: --- +-- -- * @{ZONE_RADIUS.IsAllInZoneOfCoalition}(): Scan the presence of units in the zone of a coalition. -- * @{ZONE_RADIUS.IsAllInZoneOfOtherCoalition}(): Scan the presence of units in the zone of an other coalition. -- * @{ZONE_RADIUS.IsSomeInZoneOfCoalition}(): Scan if there is some presence of units in the zone of the given coalition. @@ -708,7 +812,7 @@ function ZONE_RADIUS:Scan( ObjectCategories, UnitCategories ) local ZoneCoord = self:GetCoordinate() local ZoneRadius = self:GetRadius() - + self:F({ZoneCoord = ZoneCoord, ZoneRadius = ZoneRadius, ZoneCoordLL = ZoneCoord:ToStringLLDMS()}) local SphereSearch = { @@ -721,18 +825,18 @@ function ZONE_RADIUS:Scan( ObjectCategories, UnitCategories ) local function EvaluateZone( ZoneObject ) --if ZoneObject:isExist() then --FF: isExist always returns false for SCENERY objects since DCS 2.2 and still in DCS 2.5 - if ZoneObject then - + if ZoneObject then + local ObjectCategory = ZoneObject:getCategory() - + --local name=ZoneObject:getName() --env.info(string.format("Zone object %s", tostring(name))) --self:E(ZoneObject) - + if ( ObjectCategory == Object.Category.UNIT and ZoneObject:isExist() and ZoneObject:isActive() ) or (ObjectCategory == Object.Category.STATIC and ZoneObject:isExist()) then - + local CoalitionDCSUnit = ZoneObject:getCoalition() - + local Include = false if not UnitCategories then -- Anythink found is included. @@ -740,29 +844,29 @@ function ZONE_RADIUS:Scan( ObjectCategories, UnitCategories ) else -- Check if found object is in specified categories. local CategoryDCSUnit = ZoneObject:getDesc().category - + for UnitCategoryID, UnitCategory in pairs( UnitCategories ) do if UnitCategory == CategoryDCSUnit then Include = true break end end - + end - + if Include then - + local CoalitionDCSUnit = ZoneObject:getCoalition() - + -- This coalition is inside the zone. self.ScanData.Coalitions[CoalitionDCSUnit] = true - + self.ScanData.Units[ZoneObject] = ZoneObject - + self:F2( { Name = ZoneObject:getName(), Coalition = CoalitionDCSUnit } ) end end - + if ObjectCategory == Object.Category.SCENERY then local SceneryType = ZoneObject:getTypeName() local SceneryName = ZoneObject:getName() @@ -770,15 +874,15 @@ function ZONE_RADIUS:Scan( ObjectCategories, UnitCategories ) self.ScanData.Scenery[SceneryType][SceneryName] = SCENERY:Register( SceneryName, ZoneObject ) self:F2( { SCENERY = self.ScanData.Scenery[SceneryType][SceneryName] } ) end - + end - + return true end -- Search objects. world.searchObjects( ObjectCategories, SphereSearch, EvaluateZone ) - + end --- Count the number of different coalitions inside the zone. @@ -823,14 +927,14 @@ end function ZONE_RADIUS:GetScannedSetGroup() self.ScanSetGroup=self.ScanSetGroup or SET_GROUP:New() --Core.Set#SET_GROUP - + self.ScanSetGroup.Set={} if self.ScanData then for ObjectID, UnitObject in pairs( self.ScanData.Units ) do local UnitObject = UnitObject -- DCS#Unit if UnitObject:isExist() then - + local FoundUnit=UNIT:FindByName(UnitObject:getName()) if FoundUnit then local group=FoundUnit:GetGroup() @@ -850,11 +954,11 @@ end function ZONE_RADIUS:CountScannedCoalitions() local Count = 0 - + for CoalitionID, Coalition in pairs( self.ScanData.Coalitions ) do Count = Count + 1 end - + return Count end @@ -882,16 +986,16 @@ function ZONE_RADIUS:GetScannedCoalition( Coalition ) else local Count = 0 local ReturnCoalition = nil - + for CoalitionID, Coalition in pairs( self.ScanData.Coalitions ) do Count = Count + 1 ReturnCoalition = CoalitionID end - + if Count ~= 1 then ReturnCoalition = nil end - + return ReturnCoalition end end @@ -1002,7 +1106,7 @@ function ZONE_RADIUS:SearchZone( EvaluateFunction, ObjectCategories ) local ZoneCoord = self:GetCoordinate() local ZoneRadius = self:GetRadius() - + self:F({ZoneCoord = ZoneCoord, ZoneRadius = ZoneRadius, ZoneCoordLL = ZoneCoord:ToStringLLDMS()}) local SphereSearch = { @@ -1014,8 +1118,8 @@ function ZONE_RADIUS:SearchZone( EvaluateFunction, ObjectCategories ) } local function EvaluateZone( ZoneDCSUnit ) - - + + local ZoneUnit = UNIT:Find( ZoneDCSUnit ) return EvaluateFunction( ZoneUnit ) @@ -1031,15 +1135,15 @@ end -- @return #boolean true if the location is within the zone. function ZONE_RADIUS:IsVec2InZone( Vec2 ) self:F2( Vec2 ) - + local ZoneVec2 = self:GetVec2() - + if ZoneVec2 then if (( Vec2.x - ZoneVec2.x )^2 + ( Vec2.y - ZoneVec2.y ) ^2 ) ^ 0.5 <= self:GetRadius() then return true end end - + return false end @@ -1071,9 +1175,9 @@ function ZONE_RADIUS:GetRandomVec2( inner, outer ) local angle = math.random() * math.pi * 2; Point.x = Vec2.x + math.cos( angle ) * math.random(_inner, _outer); Point.y = Vec2.y + math.sin( angle ) * math.random(_inner, _outer); - + self:T( { Point } ) - + return Point end @@ -1088,7 +1192,7 @@ function ZONE_RADIUS:GetRandomPointVec2( inner, outer ) local PointVec2 = POINT_VEC2:NewFromVec2( self:GetRandomVec2( inner, outer ) ) self:T3( { PointVec2 } ) - + return PointVec2 end @@ -1103,7 +1207,7 @@ function ZONE_RADIUS:GetRandomVec3( inner, outer ) local Vec2 = self:GetRandomVec2( inner, outer ) self:T3( { x = Vec2.x, y = self.y, z = Vec2.y } ) - + return { x = Vec2.x, y = self.y, z = Vec2.y } end @@ -1119,7 +1223,7 @@ function ZONE_RADIUS:GetRandomPointVec3( inner, outer ) local PointVec3 = POINT_VEC3:NewFromVec2( self:GetRandomVec2( inner, outer ) ) self:T3( { PointVec3 } ) - + return PointVec3 end @@ -1135,7 +1239,7 @@ function ZONE_RADIUS:GetRandomCoordinate( inner, outer ) local Coordinate = COORDINATE:NewFromVec2( self:GetRandomVec2(inner, outer) ) self:T3( { Coordinate = Coordinate } ) - + return Coordinate end @@ -1147,55 +1251,70 @@ end --- The ZONE class, defined by the zone name as defined within the Mission Editor. -- This class implements the inherited functions from @{#ZONE_RADIUS} taking into account the own zone format and properties. --- +-- -- ## ZONE constructor --- +-- -- * @{#ZONE.New}(): Constructor. This will search for a trigger zone with the name given, and will return for you a ZONE object. --- +-- -- ## Declare a ZONE directly in the DCS mission editor! --- +-- -- You can declare a ZONE using the DCS mission editor by adding a trigger zone in the mission editor. --- +-- -- Then during mission startup, when loading Moose.lua, this trigger zone will be detected as a ZONE declaration. -- Within the background, a ZONE object will be created within the @{Core.Database}. -- The ZONE name will be the trigger zone name. --- +-- -- So, you can search yourself for the ZONE object by using the @{#ZONE.FindByName}() method. -- In this example, `local TriggerZone = ZONE:FindByName( "DefenseZone" )` would return the ZONE object --- that was created at mission startup, and reference it into the `TriggerZone` local object. --- +-- that was created at mission startup, and reference it into the `TriggerZone` local object. +-- -- Refer to mission `ZON-110` for a demonstration. --- +-- -- This is especially handy if you want to quickly setup a SET_ZONE... -- So when you would declare `local SetZone = SET_ZONE:New():FilterPrefixes( "Defense" ):FilterStart()`, -- then SetZone would contain the ZONE object `DefenseZone` as part of the zone collection, --- without much scripting overhead!!! --- --- --- @field #ZONE +-- without much scripting overhead!!! +-- +-- +-- @field #ZONE ZONE = { ClassName="ZONE", } ---- Constructor of ZONE, taking the zone name. +--- Constructor of ZONE taking the zone name. -- @param #ZONE self -- @param #string ZoneName The name of the zone as defined within the mission editor. --- @return #ZONE +-- @return #ZONE self function ZONE:New( ZoneName ) + -- First try to find the zone in the DB. + local zone=_DATABASE:FindZone(ZoneName) + + if zone then + --env.info("FF found zone in DB") + return zone + end + + -- Get zone from DCS trigger function. local Zone = trigger.misc.getZone( ZoneName ) - + + -- Error! if not Zone then error( "Zone " .. ZoneName .. " does not exist." ) return nil end - local self = BASE:Inherit( self, ZONE_RADIUS:New( ZoneName, { x = Zone.point.x, y = Zone.point.z }, Zone.radius ) ) - self:F( ZoneName ) + -- Create a new ZONE_RADIUS. + local self=BASE:Inherit( self, ZONE_RADIUS:New(ZoneName, {x=Zone.point.x, y=Zone.point.z}, Zone.radius)) + self:F(ZoneName) + -- Color of zone. + self.Color={1, 0, 0, 0.15} + + -- DCS zone. self.Zone = Zone - + return self end @@ -1204,7 +1323,7 @@ end -- @param #string ZoneName The name of the zone. -- @return #ZONE_BASE self function ZONE:FindByName( ZoneName ) - + local ZoneFound = _DATABASE:FindZone( ZoneName ) return ZoneFound end @@ -1217,15 +1336,15 @@ end --- # ZONE_UNIT class, extends @{Zone#ZONE_RADIUS} --- +-- -- The ZONE_UNIT class defined by a zone attached to a @{Wrapper.Unit#UNIT} with a radius and optional offsets. -- This class implements the inherited functions from @{#ZONE_RADIUS} taking into account the own zone format and properties. --- +-- -- @field #ZONE_UNIT ZONE_UNIT = { ClassName="ZONE_UNIT", } - + --- Constructor to create a ZONE_UNIT instance, taking the zone name, a zone unit and a radius and optional offsets in X and Y directions. -- @param #ZONE_UNIT self -- @param #string ZoneName Name of the zone. @@ -1240,30 +1359,30 @@ ZONE_UNIT = { -- dx, dy OR rho, theta may be used, not both. -- @return #ZONE_UNIT self function ZONE_UNIT:New( ZoneName, ZoneUNIT, Radius, Offset) - + if Offset then - -- check if the inputs was reasonable, either (dx, dy) or (rho, theta) can be given, else raise an exception. + -- check if the inputs was reasonable, either (dx, dy) or (rho, theta) can be given, else raise an exception. if (Offset.dx or Offset.dy) and (Offset.rho or Offset.theta) then - error("Cannot use (dx, dy) with (rho, theta)") + error("Cannot use (dx, dy) with (rho, theta)") end - + self.dy = Offset.dy or 0.0 self.dx = Offset.dx or 0.0 self.rho = Offset.rho or 0.0 self.theta = (Offset.theta or 0.0) * math.pi / 180.0 self.relative_to_unit = Offset.relative_to_unit or false end - + local self = BASE:Inherit( self, ZONE_RADIUS:New( ZoneName, ZoneUNIT:GetVec2(), Radius ) ) self:F( { ZoneName, ZoneUNIT:GetVec2(), Radius } ) self.ZoneUNIT = ZoneUNIT self.LastVec2 = ZoneUNIT:GetVec2() - + -- Zone objects are added to the _DATABASE and SET_ZONE objects. _EVENTDISPATCHER:CreateEventNewZone( self ) - + return self end @@ -1273,32 +1392,32 @@ end -- @return DCS#Vec2 The location of the zone based on the @{Wrapper.Unit#UNIT}location and the offset, if any. function ZONE_UNIT:GetVec2() self:F2( self.ZoneName ) - + local ZoneVec2 = self.ZoneUNIT:GetVec2() if ZoneVec2 then - + local heading if self.relative_to_unit then heading = ( self.ZoneUNIT:GetHeading() or 0.0 ) * math.pi / 180.0 else heading = 0.0 end - + -- update the zone position with the offsets. if (self.dx or self.dy) then - + -- use heading to rotate offset relative to unit using rotation matrix in 2D. -- see: https://en.wikipedia.org/wiki/Rotation_matrix - ZoneVec2.x = ZoneVec2.x + self.dx * math.cos( -heading ) + self.dy * math.sin( -heading ) - ZoneVec2.y = ZoneVec2.y - self.dx * math.sin( -heading ) + self.dy * math.cos( -heading ) + ZoneVec2.x = ZoneVec2.x + self.dx * math.cos( -heading ) + self.dy * math.sin( -heading ) + ZoneVec2.y = ZoneVec2.y - self.dx * math.sin( -heading ) + self.dy * math.cos( -heading ) end - + -- if using the polar coordinates - if (self.rho or self.theta) then + if (self.rho or self.theta) then ZoneVec2.x = ZoneVec2.x + self.rho * math.cos( self.theta + heading ) ZoneVec2.y = ZoneVec2.y + self.rho * math.sin( self.theta + heading ) end - + self.LastVec2 = ZoneVec2 return ZoneVec2 else @@ -1307,7 +1426,7 @@ function ZONE_UNIT:GetVec2() self:T2( { ZoneVec2 } ) - return nil + return nil end --- Returns a random location within the zone. @@ -1319,7 +1438,7 @@ function ZONE_UNIT:GetRandomVec2() local RandomVec2 = {} --local Vec2 = self.ZoneUNIT:GetVec2() -- FF: This does not take care of the new offset feature! local Vec2 = self:GetVec2() - + if not Vec2 then Vec2 = self.LastVec2 end @@ -1327,9 +1446,9 @@ function ZONE_UNIT:GetRandomVec2() local angle = math.random() * math.pi*2; RandomVec2.x = Vec2.x + math.cos( angle ) * math.random() * self:GetRadius(); RandomVec2.y = Vec2.y + math.sin( angle ) * math.random() * self:GetRadius(); - + self:T( { RandomVec2 } ) - + return RandomVec2 end @@ -1339,16 +1458,16 @@ end -- @return DCS#Vec3 The point of the zone. function ZONE_UNIT:GetVec3( Height ) self:F2( self.ZoneName ) - + Height = Height or 0 - + local Vec2 = self:GetVec2() local Vec3 = { x = Vec2.x, y = land.getHeight( self:GetVec2() ) + Height, z = Vec2.y } self:T2( { Vec3 } ) - - return Vec3 + + return Vec3 end --- @type ZONE_GROUP @@ -1357,12 +1476,12 @@ end --- The ZONE_GROUP class defines by a zone around a @{Wrapper.Group#GROUP} with a radius. The current leader of the group defines the center of the zone. -- This class implements the inherited functions from @{Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. --- +-- -- @field #ZONE_GROUP ZONE_GROUP = { ClassName="ZONE_GROUP", } - + --- Constructor to create a ZONE_GROUP instance, taking the zone name, a zone @{Wrapper.Group#GROUP} and a radius. -- @param #ZONE_GROUP self -- @param #string ZoneName Name of the zone. @@ -1378,7 +1497,7 @@ function ZONE_GROUP:New( ZoneName, ZoneGROUP, Radius ) -- Zone objects are added to the _DATABASE and SET_ZONE objects. _EVENTDISPATCHER:CreateEventNewZone( self ) - + return self end @@ -1388,9 +1507,9 @@ end -- @return DCS#Vec2 The location of the zone based on the @{Wrapper.Group} location. function ZONE_GROUP:GetVec2() self:F( self.ZoneName ) - + local ZoneVec2 = nil - + if self._.ZoneGROUP:IsAlive() then ZoneVec2 = self._.ZoneGROUP:GetVec2() self._.ZoneVec2Cache = ZoneVec2 @@ -1399,7 +1518,7 @@ function ZONE_GROUP:GetVec2() end self:T( { ZoneVec2 } ) - + return ZoneVec2 end @@ -1415,9 +1534,9 @@ function ZONE_GROUP:GetRandomVec2() local angle = math.random() * math.pi*2; Point.x = Vec2.x + math.cos( angle ) * math.random() * self:GetRadius(); Point.y = Vec2.y + math.sin( angle ) * math.random() * self:GetRadius(); - + self:T( { Point } ) - + return Point end @@ -1432,7 +1551,7 @@ function ZONE_GROUP:GetRandomPointVec2( inner, outer ) local PointVec2 = POINT_VEC2:NewFromVec2( self:GetRandomVec2() ) self:T3( { PointVec2 } ) - + return PointVec2 end @@ -1445,15 +1564,21 @@ end --- The ZONE_POLYGON_BASE class defined by a sequence of @{Wrapper.Group#GROUP} waypoints within the Mission Editor, forming a polygon. -- This class implements the inherited functions from @{Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. -- This class is an abstract BASE class for derived classes, and is not meant to be instantiated. --- +-- -- ## Zone point randomization --- +-- -- Various functions exist to find random points within the zone. --- +-- -- * @{#ZONE_POLYGON_BASE.GetRandomVec2}(): Gets a random 2D point in the zone. -- * @{#ZONE_POLYGON_BASE.GetRandomPointVec2}(): Return a @{Core.Point#POINT_VEC2} object representing a random 2D point within the zone. -- * @{#ZONE_POLYGON_BASE.GetRandomPointVec3}(): Return a @{Core.Point#POINT_VEC3} object representing a random 3D point at landheight within the zone. --- +-- +-- ## Draw zone +-- +-- * @{#ZONE_POLYGON_BASE.DrawZone}(): Draws the zone on the F10 map. +-- * @{#ZONE_POLYGON_BASE.Boundary}(): Draw a frontier on the F10 map with small filled circles. +-- +-- -- @field #ZONE_POLYGON_BASE ZONE_POLYGON_BASE = { ClassName="ZONE_POLYGON_BASE", @@ -1482,13 +1607,13 @@ function ZONE_POLYGON_BASE:New( ZoneName, PointsArray ) if PointsArray then self._.Polygon = {} - + for i = 1, #PointsArray do self._.Polygon[i] = {} self._.Polygon[i].x = PointsArray[i].x self._.Polygon[i].y = PointsArray[i].y end - + end return self @@ -1501,7 +1626,7 @@ end function ZONE_POLYGON_BASE:UpdateFromVec2(Vec2Array) self._.Polygon = {} - + for i=1,#Vec2Array do self._.Polygon[i] = {} self._.Polygon[i].x=Vec2Array[i].x @@ -1518,7 +1643,7 @@ end function ZONE_POLYGON_BASE:UpdateFromVec3(Vec3Array) self._.Polygon = {} - + for i=1,#Vec3Array do self._.Polygon[i] = {} self._.Polygon[i].x=Vec3Array[i].x @@ -1529,14 +1654,86 @@ function ZONE_POLYGON_BASE:UpdateFromVec3(Vec3Array) end --- Returns the center location of the polygon. --- @param #ZONE_GROUP self +-- @param #ZONE_POLYGON_BASE self -- @return DCS#Vec2 The location of the zone based on the @{Wrapper.Group} location. function ZONE_POLYGON_BASE:GetVec2() self:F( self.ZoneName ) local Bounds = self:GetBoundingSquare() - - return { x = ( Bounds.x2 + Bounds.x1 ) / 2, y = ( Bounds.y2 + Bounds.y1 ) / 2 } + + return { x = ( Bounds.x2 + Bounds.x1 ) / 2, y = ( Bounds.y2 + Bounds.y1 ) / 2 } +end + +--- Get a vertex of the polygon. +-- @param #ZONE_POLYGON_BASE self +-- @param #number Index Index of the vertex. Default 1. +-- @return DCS#Vec2 Vertex of the polygon. +function ZONE_POLYGON_BASE:GetVertexVec2(Index) + return self._.Polygon[Index or 1] +end + +--- Get a vertex of the polygon. +-- @param #ZONE_POLYGON_BASE self +-- @param #number Index Index of the vertex. Default 1. +-- @return DCS#Vec3 Vertex of the polygon. +function ZONE_POLYGON_BASE:GetVertexVec3(Index) + local vec2=self:GetVertexVec2(Index) + if vec2 then + local vec3={x=vec2.x, y=land.getHeight(vec2), z=vec2.y} + return vec3 + end + return nil +end + +--- Get a vertex of the polygon. +-- @param #ZONE_POLYGON_BASE self +-- @param #number Index Index of the vertex. Default 1. +-- @return Core.Point#COORDINATE Vertex of the polygon. +function ZONE_POLYGON_BASE:GetVertexCoordinate(Index) + local vec2=self:GetVertexVec2(Index) + if vec2 then + local coord=COORDINATE:NewFromVec2(vec2) + return coord + end + return nil +end + + +--- Get a list of verticies of the polygon. +-- @param #ZONE_POLYGON_BASE self +-- @return List of DCS#Vec2 verticies defining the edges of the polygon. +function ZONE_POLYGON_BASE:GetVerticiesVec2() + return self._.Polygon +end + +--- Get a list of verticies of the polygon. +-- @param #ZONE_POLYGON_BASE self +-- @return #table List of DCS#Vec3 verticies defining the edges of the polygon. +function ZONE_POLYGON_BASE:GetVerticiesVec3() + + local coords={} + + for i,vec2 in ipairs(self._.Polygon) do + local vec3={x=vec2.x, y=land.getHeight(vec2), z=vec2.y} + table.insert(coords, vec3) + end + + return coords +end + +--- Get a list of verticies of the polygon. +-- @param #ZONE_POLYGON_BASE self +-- @return #table List of COORDINATES verticies defining the edges of the polygon. +function ZONE_POLYGON_BASE:GetVerticiesCoordinates() + + local coords={} + + for i,vec2 in ipairs(self._.Polygon) do + local coord=COORDINATE:NewFromVec2(vec2) + table.insert(coords, coord) + end + + return coords end --- Flush polygon coordinates as a table in DCS.log. @@ -1556,24 +1753,24 @@ end -- @return #ZONE_POLYGON_BASE self function ZONE_POLYGON_BASE:BoundZone( UnBound ) - local i - local j + local i + local j local Segments = 10 - + i = 1 j = #self._.Polygon - + while i <= #self._.Polygon do self:T( { i, j, self._.Polygon[i], self._.Polygon[j] } ) - + local DeltaX = self._.Polygon[j].x - self._.Polygon[i].x local DeltaY = self._.Polygon[j].y - self._.Polygon[i].y - + for Segment = 0, Segments do -- We divide each line in 5 segments and smoke a point on the line. local PointX = self._.Polygon[i].x + ( Segment * DeltaX / Segments ) local PointY = self._.Polygon[i].y + ( Segment * DeltaY / Segments ) local Tire = { - ["country"] = "USA", + ["country"] = "USA", ["category"] = "Fortifications", ["canCargo"] = false, ["shape_name"] = "H-tyre_B_WF", @@ -1583,12 +1780,12 @@ function ZONE_POLYGON_BASE:BoundZone( UnBound ) ["name"] = string.format( "%s-Tire #%0d", self:GetName(), ((i - 1) * Segments) + Segment ), ["heading"] = 0, } -- end of ["group"] - + local Group = coalition.addStaticObject( country.id.USA, Tire ) if UnBound and UnBound == true then Group:destroy() end - + end j = i i = i + 1 @@ -1598,6 +1795,46 @@ function ZONE_POLYGON_BASE:BoundZone( UnBound ) end +--- Draw the zone on the F10 map. **NOTE** Currently, only polygons with **exactly four points** are supported! +-- @param #ZONE_POLYGON_BASE self +-- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. +-- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red. +-- @param #number Alpha Transparency [0,1]. Default 1. +-- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. +-- @param #number FillAlpha Transparency [0,1]. Default 0.15. +-- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. +-- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. +-- @return #ZONE_POLYGON_BASE self +function ZONE_POLYGON_BASE:DrawZone(Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) + + local coordinate=COORDINATE:NewFromVec2(self._.Polygon[1]) + + Color=Color or self:GetColorRGB() + Alpha=Alpha or 1 + FillColor=FillColor or Color + FillAlpha=FillAlpha or self:GetColorAlpha() + + + if #self._.Polygon==4 then + + local Coord2=COORDINATE:NewFromVec2(self._.Polygon[2]) + local Coord3=COORDINATE:NewFromVec2(self._.Polygon[3]) + local Coord4=COORDINATE:NewFromVec2(self._.Polygon[4]) + + self.DrawID=coordinate:QuadToAll(Coord2, Coord3, Coord4, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) + + else + + local Coordinates=self:GetVerticiesCoordinates() + table.remove(Coordinates, 1) + + self.DrawID=coordinate:MarkupToAllFreeForm(Coordinates, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) + + end + + + return self +end --- Smokes the zone boundaries in a color. -- @param #ZONE_POLYGON_BASE self @@ -1608,16 +1845,16 @@ function ZONE_POLYGON_BASE:SmokeZone( SmokeColor, Segments ) self:F2( SmokeColor ) Segments=Segments or 10 - + local i=1 local j=#self._.Polygon - + while i <= #self._.Polygon do self:T( { i, j, self._.Polygon[i], self._.Polygon[j] } ) - + local DeltaX = self._.Polygon[j].x - self._.Polygon[i].x local DeltaY = self._.Polygon[j].y - self._.Polygon[i].y - + for Segment = 0, Segments do -- We divide each line in 5 segments and smoke a point on the line. local PointX = self._.Polygon[i].x + ( Segment * DeltaX / Segments ) local PointY = self._.Polygon[i].y + ( Segment * DeltaY / Segments ) @@ -1642,18 +1879,18 @@ function ZONE_POLYGON_BASE:FlareZone( FlareColor, Segments, Azimuth, AddHeight ) self:F2(FlareColor) Segments=Segments or 10 - + AddHeight = AddHeight or 0 - + local i=1 local j=#self._.Polygon - + while i <= #self._.Polygon do self:T( { i, j, self._.Polygon[i], self._.Polygon[j] } ) - + local DeltaX = self._.Polygon[j].x - self._.Polygon[i].x local DeltaY = self._.Polygon[j].y - self._.Polygon[i].y - + for Segment = 0, Segments do -- We divide each line in 5 segments and smoke a point on the line. local PointX = self._.Polygon[i].x + ( Segment * DeltaX / Segments ) local PointY = self._.Polygon[i].y + ( Segment * DeltaY / Segments ) @@ -1677,17 +1914,17 @@ end function ZONE_POLYGON_BASE:IsVec2InZone( Vec2 ) self:F2( Vec2 ) - local Next - local Prev + local Next + local Prev local InPolygon = false - + Next = 1 Prev = #self._.Polygon - + while Next <= #self._.Polygon do self:T( { Next, Prev, self._.Polygon[Next], self._.Polygon[Prev] } ) if ( ( ( self._.Polygon[Next].y > Vec2.y ) ~= ( self._.Polygon[Prev].y > Vec2.y ) ) and - ( Vec2.x < ( self._.Polygon[Prev].x - self._.Polygon[Next].x ) * ( Vec2.y - self._.Polygon[Next].y ) / ( self._.Polygon[Prev].y - self._.Polygon[Next].y ) + self._.Polygon[Next].x ) + ( Vec2.x < ( self._.Polygon[Prev].x - self._.Polygon[Next].x ) * ( Vec2.y - self._.Polygon[Next].y ) / ( self._.Polygon[Prev].y - self._.Polygon[Next].y ) + self._.Polygon[Next].x ) ) then InPolygon = not InPolygon end @@ -1710,9 +1947,9 @@ function ZONE_POLYGON_BASE:GetRandomVec2() local Vec2Found = false local Vec2 local BS = self:GetBoundingSquare() - + self:T2( BS ) - + while Vec2Found == false do Vec2 = { x = math.random( BS.x1, BS.x2 ), y = math.random( BS.y1, BS.y2 ) } self:T2( Vec2 ) @@ -1720,7 +1957,7 @@ function ZONE_POLYGON_BASE:GetRandomVec2() Vec2Found = true end end - + self:T2( Vec2 ) return Vec2 @@ -1733,7 +1970,7 @@ function ZONE_POLYGON_BASE:GetRandomPointVec2() self:F2() local PointVec2 = POINT_VEC2:NewFromVec2( self:GetRandomVec2() ) - + self:T2( PointVec2 ) return PointVec2 @@ -1746,7 +1983,7 @@ function ZONE_POLYGON_BASE:GetRandomPointVec3() self:F2() local PointVec3 = POINT_VEC3:NewFromVec2( self:GetRandomVec2() ) - + self:T2( PointVec3 ) return PointVec3 @@ -1760,7 +1997,7 @@ function ZONE_POLYGON_BASE:GetRandomCoordinate() self:F2() local Coordinate = COORDINATE:NewFromVec2( self:GetRandomVec2() ) - + self:T2( Coordinate ) return Coordinate @@ -1776,19 +2013,58 @@ function ZONE_POLYGON_BASE:GetBoundingSquare() local y1 = self._.Polygon[1].y local x2 = self._.Polygon[1].x local y2 = self._.Polygon[1].y - + for i = 2, #self._.Polygon do self:T2( { self._.Polygon[i], x1, y1, x2, y2 } ) x1 = ( x1 > self._.Polygon[i].x ) and self._.Polygon[i].x or x1 x2 = ( x2 < self._.Polygon[i].x ) and self._.Polygon[i].x or x2 y1 = ( y1 > self._.Polygon[i].y ) and self._.Polygon[i].y or y1 y2 = ( y2 < self._.Polygon[i].y ) and self._.Polygon[i].y or y2 - + end return { x1 = x1, y1 = y1, x2 = x2, y2 = y2 } end +--- Draw a frontier on the F10 map with small filled circles. +-- @param #ZONE_POLYGON_BASE self +-- @param #number Coalition (Optional) Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1= All. +-- @param #table Color (Optional) RGB color table {r, g, b}, e.g. {1, 0, 0} for red. Default {1, 1, 1}= White. +-- @param #number Radius (Optional) Radius of the circles in meters. Default 1000. +-- @param #number Alpha (Optional) Alpha transparency [0,1]. Default 1. +-- @param #number Segments (Optional) Number of segments within boundary line. Default 10. +-- @param #boolean Closed (Optional) Link the last point with the first one to obtain a closed boundary. Default false +-- @return #ZONE_POLYGON_BASE self +function ZONE_POLYGON_BASE:Boundary(Coalition, Color, Radius, Alpha, Segments, Closed) + Coalition = Coalition or -1 + Color = Color or {1, 1, 1} + Radius = Radius or 1000 + Alpha = Alpha or 1 + Segments = Segments or 10 + Closed = Closed or false + local i = 1 + local j = #self._.Polygon + if (Closed) then + Limit = #self._.Polygon + 1 + else + Limit = #self._.Polygon + end + while i <= #self._.Polygon do + self:T( { i, j, self._.Polygon[i], self._.Polygon[j] } ) + if j ~= Limit then + local DeltaX = self._.Polygon[j].x - self._.Polygon[i].x + local DeltaY = self._.Polygon[j].y - self._.Polygon[i].y + for Segment = 0, Segments do + local PointX = self._.Polygon[i].x + ( Segment * DeltaX / Segments ) + local PointY = self._.Polygon[i].y + ( Segment * DeltaY / Segments ) + ZONE_RADIUS:New( "Zone", {x = PointX, y = PointY}, Radius ):DrawZone(Coalition, Color, 1, Color, Alpha, nil, true) + end + end + j = i + i = i + 1 + end + return self +end --- @type ZONE_POLYGON -- @extends #ZONE_POLYGON_BASE @@ -1796,27 +2072,27 @@ end --- The ZONE_POLYGON class defined by a sequence of @{Wrapper.Group#GROUP} waypoints within the Mission Editor, forming a polygon. -- This class implements the inherited functions from @{Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. --- +-- -- ## Declare a ZONE_POLYGON directly in the DCS mission editor! --- +-- -- You can declare a ZONE_POLYGON using the DCS mission editor by adding the ~ZONE_POLYGON tag in the group name. --- +-- -- So, imagine you have a group declared in the mission editor, with group name `DefenseZone~ZONE_POLYGON`. -- Then during mission startup, when loading Moose.lua, this group will be detected as a ZONE_POLYGON declaration. -- Within the background, a ZONE_POLYGON object will be created within the @{Core.Database} using the properties of the group. -- The ZONE_POLYGON name will be the group name without the ~ZONE_POLYGON tag. --- +-- -- So, you can search yourself for the ZONE_POLYGON by using the @{#ZONE_POLYGON.FindByName}() method. -- In this example, `local PolygonZone = ZONE_POLYGON:FindByName( "DefenseZone" )` would return the ZONE_POLYGON object -- that was created at mission startup, and reference it into the `PolygonZone` local object. --- +-- -- Mission `ZON-510` shows a demonstration of this feature or method. --- +-- -- This is especially handy if you want to quickly setup a SET_ZONE... -- So when you would declare `local SetZone = SET_ZONE:New():FilterPrefixes( "Defense" ):FilterStart()`, -- then SetZone would contain the ZONE_POLYGON object `DefenseZone` as part of the zone collection, --- without much scripting overhead!!! --- +-- without much scripting overhead! +-- -- @field #ZONE_POLYGON ZONE_POLYGON = { ClassName="ZONE_POLYGON", @@ -1868,7 +2144,7 @@ end -- @param #string ZoneName The name of the polygon zone. -- @return #ZONE_POLYGON self function ZONE_POLYGON:FindByName( ZoneName ) - + local ZoneFound = _DATABASE:FindZone( ZoneName ) return ZoneFound end @@ -1877,85 +2153,85 @@ do -- ZONE_AIRBASE --- @type ZONE_AIRBASE -- @extends #ZONE_RADIUS - - + + --- The ZONE_AIRBASE class defines by a zone around a @{Wrapper.Airbase#AIRBASE} with a radius. -- This class implements the inherited functions from @{Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. - -- + -- -- @field #ZONE_AIRBASE ZONE_AIRBASE = { ClassName="ZONE_AIRBASE", } - - - + + + --- Constructor to create a ZONE_AIRBASE instance, taking the zone name, a zone @{Wrapper.Airbase#AIRBASE} and a radius. -- @param #ZONE_AIRBASE self -- @param #string AirbaseName Name of the airbase. -- @param DCS#Distance Radius (Optional)The radius of the zone in meters. Default 4000 meters. -- @return #ZONE_AIRBASE self function ZONE_AIRBASE:New( AirbaseName, Radius ) - + Radius=Radius or 4000 - + local Airbase = AIRBASE:FindByName( AirbaseName ) - + local self = BASE:Inherit( self, ZONE_RADIUS:New( AirbaseName, Airbase:GetVec2(), Radius ) ) - + self._.ZoneAirbase = Airbase self._.ZoneVec2Cache = self._.ZoneAirbase:GetVec2() - + -- Zone objects are added to the _DATABASE and SET_ZONE objects. _EVENTDISPATCHER:CreateEventNewZone( self ) - + return self end - + --- Get the airbase as part of the ZONE_AIRBASE object. -- @param #ZONE_AIRBASE self -- @return Wrapper.Airbase#AIRBASE The airbase. function ZONE_AIRBASE:GetAirbase() return self._.ZoneAirbase - end - + end + --- Returns the current location of the @{Wrapper.Group}. -- @param #ZONE_AIRBASE self -- @return DCS#Vec2 The location of the zone based on the @{Wrapper.Group} location. function ZONE_AIRBASE:GetVec2() self:F( self.ZoneName ) - + local ZoneVec2 = nil - + if self._.ZoneAirbase:IsAlive() then ZoneVec2 = self._.ZoneAirbase:GetVec2() self._.ZoneVec2Cache = ZoneVec2 else ZoneVec2 = self._.ZoneVec2Cache end - + self:T( { ZoneVec2 } ) - + return ZoneVec2 end - + --- Returns a random location within the zone of the @{Wrapper.Group}. -- @param #ZONE_AIRBASE self -- @return DCS#Vec2 The random location of the zone based on the @{Wrapper.Group} location. function ZONE_AIRBASE:GetRandomVec2() self:F( self.ZoneName ) - + local Point = {} local Vec2 = self._.ZoneAirbase:GetVec2() - + local angle = math.random() * math.pi*2; Point.x = Vec2.x + math.cos( angle ) * math.random() * self:GetRadius(); Point.y = Vec2.y + math.sin( angle ) * math.random() * self:GetRadius(); - + self:T( { Point } ) - + return Point end - + --- Returns a @{Core.Point#POINT_VEC2} object reflecting a random 2D location within the zone. -- @param #ZONE_AIRBASE self -- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. @@ -1963,11 +2239,11 @@ do -- ZONE_AIRBASE -- @return Core.Point#POINT_VEC2 The @{Core.Point#POINT_VEC2} object reflecting the random 3D location within the zone. function ZONE_AIRBASE:GetRandomPointVec2( inner, outer ) self:F( self.ZoneName, inner, outer ) - + local PointVec2 = POINT_VEC2:NewFromVec2( self:GetRandomVec2() ) - + self:T3( { PointVec2 } ) - + return PointVec2 end diff --git a/Moose Development/Moose/DCS.lua b/Moose Development/Moose/DCS.lua index 92052d5cf..01661a678 100644 --- a/Moose Development/Moose/DCS.lua +++ b/Moose Development/Moose/DCS.lua @@ -295,6 +295,17 @@ do -- country -- @field QATAR -- @field OMAN -- @field UNITED_ARAB_EMIRATES + -- @field SOUTH_AFRICA + -- @field CUBA + -- @field PORTUGAL + -- @field GDR + -- @field LEBANON + -- @field CJTF_BLUE + -- @field CJTF_RED + -- @field UN_PEACEKEEPERS + -- @field Argentinia + -- @field Cyprus + -- @field Slovenia country = {} --#country diff --git a/Moose Development/Moose/Functional/ATC_Ground.lua b/Moose Development/Moose/Functional/ATC_Ground.lua index 455f3eaf5..e5d17ea14 100644 --- a/Moose Development/Moose/Functional/ATC_Ground.lua +++ b/Moose Development/Moose/Functional/ATC_Ground.lua @@ -59,7 +59,13 @@ function ATC_GROUND:New( Airbases, AirbaseList ) for AirbaseID, Airbase in pairs( self.Airbases ) do - Airbase.ZoneBoundary = _DATABASE:FindAirbase( AirbaseID ):GetZone() + -- Specified ZoneBoundary is used if setted or Airbase radius by default + if Airbase.ZoneBoundary then + Airbase.ZoneBoundary = ZONE_POLYGON_BASE:New( "Boundary " .. AirbaseID, Airbase.ZoneBoundary ) + else + Airbase.ZoneBoundary = _DATABASE:FindAirbase( AirbaseID ):GetZone() + end + Airbase.ZoneRunways = {} for PointsRunwayID, PointsRunway in pairs( Airbase.PointsRunways ) do Airbase.ZoneRunways[PointsRunwayID] = ZONE_POLYGON_BASE:New( "Runway " .. PointsRunwayID, PointsRunway ) @@ -3263,5 +3269,310 @@ function ATC_GROUND_PERSIANGULF:Start( RepeatScanSeconds ) end + --- @type ATC_GROUND_MARIANAISLANDS +-- @extends #ATC_GROUND + +--- # ATC\_GROUND\_MARIANA, extends @{#ATC_GROUND} +-- +-- The ATC\_GROUND\_MARIANA class monitors the speed of the airplanes at the airbase during taxi. +-- The pilots may not drive faster than the maximum speed for the airbase, or they will be despawned. +-- +-- --- +-- +-- ![Banner Image](..\Presentations\ATC_GROUND\Dia1.JPG) +-- +-- --- +-- +-- The default maximum speed for the airbases at Persian Gulf is **50 km/h**. Warnings are given if this speed limit is trespassed. +-- Players will be immediately kicked when driving faster than **150 km/h** on the taxi way. +-- +-- The ATC\_GROUND\_MARIANA class monitors the speed of the airplanes at the airbase during taxi. +-- The pilots may not drive faster than the maximum speed for the airbase, or they will be despawned. +-- +-- The pilot will receive 3 times a warning during speeding. After the 3rd warning, if the pilot is still driving +-- faster than the maximum allowed speed, the pilot will be kicked. +-- +-- Different airbases have different maximum speeds, according safety regulations. +-- +-- # Airbases monitored +-- +-- The following airbases are monitored at the Mariana Island region. +-- Use the @{Wrapper.Airbase#AIRBASE.MarianaIslands} enumeration to select the airbases to be monitored. +-- +-- * AIRBASE.MarianaIslands.Rota_Intl +-- * AIRBASE.MarianaIslands.Andersen_AFB +-- * AIRBASE.MarianaIslands.Antonio_B_Won_Pat_Intl +-- * AIRBASE.MarianaIslands.Saipan_Intl +-- * AIRBASE.MarianaIslands.Tinian_Intl +-- * AIRBASE.MarianaIslands.Olf_Orote +-- +-- # Installation +-- +-- ## In Single Player Missions +-- +-- ATC\_GROUND is fully functional in single player. +-- +-- ## In Multi Player Missions +-- +-- ATC\_GROUND is functional in multi player, however ... +-- +-- Due to a bug in DCS since release 1.5, the despawning of clients are not anymore working in multi player. +-- To **work around this problem**, a much better solution has been made, using the **slot blocker** script designed +-- by Ciribob. +-- +-- With the help of __Ciribob__, this script has been extended to also kick client players while in flight. +-- ATC\_GROUND is communicating with this modified script to kick players! +-- +-- Install the file **SimpleSlotBlockGameGUI.lua** on the server, following the installation instructions described by Ciribob. +-- +-- [Simple Slot Blocker from Ciribob & FlightControl](https://github.com/ciribob/DCS-SimpleSlotBlock) +-- +-- # Script it! +-- +-- ## 1. ATC_GROUND_MARIANAISLANDS Constructor +-- +-- Creates a new ATC_GROUND_MARIANAISLANDS object that will monitor pilots taxiing behaviour. +-- +-- -- This creates a new ATC_GROUND_MARIANAISLANDS object. +-- +-- -- Monitor for these clients the airbases. +-- AirbasePoliceCaucasus = ATC_GROUND_MARIANAISLANDS:New() +-- +-- ATC_Ground = ATC_GROUND_MARIANAISLANDS:New( +-- { AIRBASE.MarianaIslands.Andersen_AFB, +-- AIRBASE.MarianaIslands.Saipan_Intl +-- } +-- ) +-- +-- +-- ## 2. Set various options +-- +-- There are various methods that you can use to tweak the behaviour of the ATC\_GROUND classes. +-- +-- ### 2.1 Speed limit at an airbase. +-- +-- * @{#ATC_GROUND.SetKickSpeed}(): Set the speed limit allowed at an airbase in meters per second. +-- * @{#ATC_GROUND.SetKickSpeedKmph}(): Set the speed limit allowed at an airbase in kilometers per hour. +-- * @{#ATC_GROUND.SetKickSpeedMiph}(): Set the speed limit allowed at an airbase in miles per hour. +-- +-- ### 2.2 Prevent Takeoff at an airbase. Players will be kicked immediately. +-- +-- * @{#ATC_GROUND.SetMaximumKickSpeed}(): Set the maximum speed allowed at an airbase in meters per second. +-- * @{#ATC_GROUND.SetMaximumKickSpeedKmph}(): Set the maximum speed allowed at an airbase in kilometers per hour. +-- * @{#ATC_GROUND.SetMaximumKickSpeedMiph}(): Set the maximum speed allowed at an airbase in miles per hour. +-- +---- @field #ATC_GROUND_MARIANAISLANDS +ATC_GROUND_MARIANAISLANDS = { + ClassName = "ATC_GROUND_MARIANAISLANDS", + Airbases = { + + [AIRBASE.MarianaIslands.Andersen_AFB] = { + ZoneBoundary = { + [1]={["y"]=16534.138036037,["x"]=11357.42159178,}, + [2]={["y"]=16193.406442738,["x"]=12080.012957533,}, + [3]={["y"]=13846.966851869,["x"]=12017.348398727,}, + [4]={["y"]=13085.815989171,["x"]=11686.317876875,}, + [5]={["y"]=13157.991797443,["x"]=11307.826209991,}, + [6]={["y"]=12055.725179065,["x"]=10795.955695916,}, + [7]={["y"]=12762.455491112,["x"]=8890.9830441032,}, + [8]={["y"]=15955.829493693,["x"]=10333.527220132,}, + [9]={["y"]=16537.500532414,["x"]=11302.009499603,}, + }, + PointsRunways = { + [1]={ + [1]={["y"]=12586.683049611,["x"]=10224.374497932,}, + [2]={["y"]=16191.720475696,["x"]=11791.299100017,}, + [3]={["y"]=16126.93956642,["x"]=11938.855615591,}, + [4]={["y"]=12520.758127164,["x"]=10385.177131701,}, + [5]={["y"]=12584.654720512,["x"]=10227.416991581,}, + }, + [2]={ + [1]={["y"]=12663.030391743,["x"]=9661.9623015306,}, + [2]={["y"]=16478.347303358,["x"]=11328.665745976,}, + [3]={["y"]=16405.4731048,["x"]=11479.11570429,}, + [4]={["y"]=12597.277684174,["x"]=9817.9733769647,}, + [5]={["y"]=12661.894752524,["x"]=9674.4462086962,}, + }, + }, + }, + [AIRBASE.MarianaIslands.Antonio_B_Won_Pat_Intl] = { + ZoneBoundary = { + [1]={["y"]=2288.5182403943,["x"]=1469.0170841716,}, + [2]={["y"]=1126.2025877996,["x"]=1174.37135631,}, + [3]={["y"]=-2015.6461924287,["x"]=-484.62000718931,}, + [4]={["y"]=-2102.1292389114,["x"]=-988.03393750566,}, + [5]={["y"]=476.03853524366,["x"]=-1220.1783269883,}, + [6]={["y"]=2059.2220058047,["x"]=78.889693514402,}, + [7]={["y"]=1898.1396965104,["x"]=705.67531284795,}, + [8]={["y"]=2760.1768681934,["x"]=1026.0681119777,}, + [9]={["y"]=2317.2278959994,["x"]=1460.8143254273,}, + }, + PointsRunways = { + [1]={ + [1]={["y"]=-1872.6620108821,["x"]=-924.3572605835,}, + [2]={["y"]=1763.4754603305,["x"]=735.35988877983,}, + [3]={["y"]=1700.6941677961,["x"]=866.32615476157,}, + [4]={["y"]=-1934.0078007732,["x"]=-779.8149298453,}, + [5]={["y"]=-1875.0113982627,["x"]=-914.95971106094,}, + }, + [2]={ + [1]={["y"]=-1512.9403660377,["x"]=-1005.5903386188,}, + [2]={["y"]=1577.9055714735,["x"]=413.22750176368,}, + [3]={["y"]=1523.1182807849,["x"]=543.89726442232,}, + [4]={["y"]=-1572.5102998047,["x"]=-867.04004322806,}, + [5]={["y"]=-1514.2790162347,["x"]=-1003.5823633233,}, + }, + }, + }, + [AIRBASE.MarianaIslands.Rota_Intl] = { + ZoneBoundary = { + [1]={["y"]=47237.615412849,["x"]=76048.890408862,}, + [2]={["y"]=49938.030053628,["x"]=75921.721582932,}, + [3]={["y"]=49931.24873272,["x"]=75735.184004851,}, + [4]={["y"]=49295.999227075,["x"]=75754.716414519,}, + [5]={["y"]=49286.963307515,["x"]=75510.037806569,}, + [6]={["y"]=48774.280745707,["x"]=75513.331990155,}, + [7]={["y"]=48785.021396773,["x"]=75795.691662161,}, + [8]={["y"]=47232.749278491,["x"]=75839.239059146,}, + [9]={["y"]=47236.687866223,["x"]=76042.706764692,}, + }, + PointsRunways = { + [1]={ + [1]={["y"]=49741.295228062,["x"]=75901.50955922,}, + [2]={["y"]=49739.033213305,["x"]=75768.333440425,}, + [3]={["y"]=47448.460520408,["x"]=75857.400271466,}, + [4]={["y"]=47452.270177742,["x"]=75999.965448133,}, + [5]={["y"]=49738.502011054,["x"]=75905.338915708,}, + }, + }, + }, + [AIRBASE.MarianaIslands.Saipan_Intl] = { + ZoneBoundary = { + [1]={["y"]=100489.08491445,["x"]=179799.05158855,}, + [2]={["y"]=100869.73415313,["x"]=179948.98719903,}, + [3]={["y"]=101364.78967515,["x"]=180831.98517043,}, + [4]={["y"]=101563.85713359,["x"]=180885.21496237,}, + [5]={["y"]=101733.92591034,["x"]=180457.73296886,}, + [6]={["y"]=103340.30228775,["x"]=180990.08362622,}, + [7]={["y"]=103459.55080438,["x"]=180453.77747027,}, + [8]={["y"]=100406.63048095,["x"]=179266.60983762,}, + [9]={["y"]=100225.55027532,["x"]=179423.9380961,}, + [10]={["y"]=100477.48558937,["x"]=179791.9827288,}, + }, + PointsRunways = { + [1]={ + [1]={["y"]=103170.38882002,["x"]=180654.56630524,}, + [2]={["y"]=103235.37868835,["x"]=180497.25368418,}, + [3]={["y"]=100564.72969504,["x"]=179435.41443498,}, + [4]={["y"]=100509.30718722,["x"]=179584.65394733,}, + [5]={["y"]=103163.53918905,["x"]=180651.82645285,}, + }, + [2]={ + [1]={["y"]=103048.83223261,["x"]=180819.94107128,}, + [2]={["y"]=103087.60579257,["x"]=180720.06315265,}, + [3]={["y"]=101037.52694966,["x"]=179899.50061624,}, + [4]={["y"]=100994.61708907,["x"]=180009.33151758,}, + [5]={["y"]=103043.26643227,["x"]=180820.40488798,}, + }, + }, + }, + [AIRBASE.MarianaIslands.Tinian_Intl] = { + ZoneBoundary = { + [1]={["y"]=88393.477575413,["x"]=166704.16076438,}, + [2]={["y"]=91581.732441809,["x"]=167402.54409276,}, + [3]={["y"]=91533.451647402,["x"]=166826.23670062,}, + [4]={["y"]=90827.604136952,["x"]=166699.75590414,}, + [5]={["y"]=90894.853975623,["x"]=166375.37836304,}, + [6]={["y"]=89995.027922869,["x"]=166224.92495935,}, + [7]={["y"]=88937.62899352,["x"]=166244.48573911,}, + [8]={["y"]=88408.916178231,["x"]=166480.39896864,}, + [9]={["y"]=88387.745481732,["x"]=166685.82715656,}, + }, + PointsRunways = { + [1]={ + [1]={["y"]=91329.480937912,["x"]=167204.44064529,}, + [2]={["y"]=91363.95475433,["x"]=167038.15603429,}, + [3]={["y"]=88585.849307337,["x"]=166520.3807647,}, + [4]={["y"]=88554.422227212,["x"]=166686.49505251,}, + [5]={["y"]=91318.8152578,["x"]=167203.31794212,}, + }, + }, + }, + + }, +} + +--- Creates a new ATC_GROUND_MARIANAISLANDS object. +-- @param #ATC_GROUND_MARIANAISLANDS self +-- @param AirbaseNames A list {} of airbase names (Use AIRBASE.MarianaIslands enumerator). +-- @return #ATC_GROUND_MARIANAISLANDS self +function ATC_GROUND_MARIANAISLANDS:New( AirbaseNames ) + + -- Inherits from BASE + local self = BASE:Inherit( self, ATC_GROUND:New( self.Airbases, AirbaseNames ) ) + + self:SetKickSpeedKmph( 50 ) + self:SetMaximumKickSpeedKmph( 150 ) + +-- -- Andersen +-- local AndersenBoundary = GROUP:FindByName( "Andersen Boundary" ) +-- self.Airbases[AIRBASE.MarianaIslands.Andersen_AFB].ZoneBoundary = ZONE_POLYGON:New( "Andersen Boundary", AndersenBoundary ):SmokeZone(SMOKECOLOR.White):Flush() +-- +-- local AndersenRunway1 = GROUP:FindByName( "Andersen Runway 1" ) +-- self.Airbases[AIRBASE.MarianaIslands.Andersen_AFB].ZoneRunways[1] = ZONE_POLYGON:New( "Andersen Runway 1", AndersenRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() +-- +-- local AndersenRunway2 = GROUP:FindByName( "Andersen Runway 2" ) +-- self.Airbases[AIRBASE.MarianaIslands.Andersen_AFB].ZoneRunways[2] = ZONE_POLYGON:New( "Andersen Runway 2", AndersenRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() +-- +-- +-- -- Antonio_B_Won_Pat_International_Airport +-- local AntonioBoundary = GROUP:FindByName( "Antonio Boundary" ) +-- self.Airbases[AIRBASE.MarianaIslands.Antonio_B_Won_Pat_Intl].ZoneBoundary = ZONE_POLYGON:New( "Antonio Boundary", AntonioBoundary ):SmokeZone(SMOKECOLOR.White):Flush() +-- +-- local AntonioRunway1 = GROUP:FindByName( "Antonio Runway 1" ) +-- self.Airbases[AIRBASE.MarianaIslands.Antonio_B_Won_Pat_Intl].ZoneRunways[1] = ZONE_POLYGON:New( "Antonio Runway 1", AntonioRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() +-- +-- local AntonioRunway2 = GROUP:FindByName( "Antonio Runway 2" ) +-- self.Airbases[AIRBASE.MarianaIslands.Antonio_B_Won_Pat_Intl].ZoneRunways[2] = ZONE_POLYGON:New( "Antonio Runway 2", AntonioRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() +-- +-- +-- -- Rota_International_Airport +-- local RotaBoundary = GROUP:FindByName( "Rota Boundary" ) +-- self.Airbases[AIRBASE.MarianaIslands.Rota_Intl].ZoneBoundary = ZONE_POLYGON:New( "Rota Boundary", RotaBoundary ):SmokeZone(SMOKECOLOR.White):Flush() +-- +-- local RotaRunway1 = GROUP:FindByName( "Rota Runway 1" ) +-- self.Airbases[AIRBASE.MarianaIslands.Rota_Intl].ZoneRunways[1] = ZONE_POLYGON:New( "Rota Runway 1", RotaRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() +-- +-- +-- -- Saipan_International_Airport +-- local SaipanBoundary = GROUP:FindByName( "Saipan Boundary" ) +-- self.Airbases[AIRBASE.MarianaIslands.Saipan_Intl].ZoneBoundary = ZONE_POLYGON:New( "Saipan Boundary", SaipanBoundary ):SmokeZone(SMOKECOLOR.White):Flush() +-- +-- local SaipanRunway1 = GROUP:FindByName( "Saipan Runway 1" ) +-- self.Airbases[AIRBASE.MarianaIslands.Saipan_Intl].ZoneRunways[1] = ZONE_POLYGON:New( "Saipan Runway 1", SaipanRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() +-- +-- local SaipanRunway2 = GROUP:FindByName( "Saipan Runway 2" ) +-- self.Airbases[AIRBASE.MarianaIslands.Saipan_Intl].ZoneRunways[2] = ZONE_POLYGON:New( "Saipan Runway 2", SaipanRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush() +-- +-- +-- -- Tinian_International_Airport +-- local TinianBoundary = GROUP:FindByName( "Tinian Boundary" ) +-- self.Airbases[AIRBASE.MarianaIslands.Tinian_Intl].ZoneBoundary = ZONE_POLYGON:New( "Tinian Boundary", TinianBoundary ):SmokeZone(SMOKECOLOR.White):Flush() +-- +-- local TinianRunway1 = GROUP:FindByName( "Tinian Runway 1" ) +-- self.Airbases[AIRBASE.MarianaIslands.Tinian_Intl].ZoneRunways[1] = ZONE_POLYGON:New( "Tinian Runway 1", TinianRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + + return self +end + + +--- Start SCHEDULER for ATC_GROUND_MARIANAISLANDS object. +-- @param #ATC_GROUND_MARIANAISLANDS self +-- @param RepeatScanSeconds Time in second for defining occurency of alerts. +-- @return nothing +function ATC_GROUND_MARIANAISLANDS:Start( RepeatScanSeconds ) + RepeatScanSeconds = RepeatScanSeconds or 0.05 + self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, RepeatScanSeconds ) +end diff --git a/Moose Development/Moose/Functional/Mantis.lua b/Moose Development/Moose/Functional/Mantis.lua index 23faabb3f..6891fd3fd 100644 --- a/Moose Development/Moose/Functional/Mantis.lua +++ b/Moose Development/Moose/Functional/Mantis.lua @@ -20,7 +20,7 @@ -- @module Functional.Mantis -- @image Functional.Mantis.jpg --- Date: Apr 2021 +-- Date: July 2021 ------------------------------------------------------------------------- --- **MANTIS** class, extends #Core.Base#BASE @@ -59,7 +59,7 @@ -- @extends Core.Base#BASE ---- *The worst thing that can happen to a good cause is, not to be skillfully attacked, but to be ineptly defended.* - Frédéric Bastiat +--- *The worst thing that can happen to a good cause is, not to be skillfully attacked, but to be ineptly defended.* - Frédéric Bastiat -- -- Simple Class for a more intelligent Air Defense System -- @@ -191,7 +191,20 @@ MANTIS = { ShoradLink = false, ShoradTime = 600, ShoradActDistance = 15000, - UseEmOnOff = true, + UseEmOnOff = false, + TimeStamp = 0, + state2flag = false, + SamStateTracker = {}, + DLink = false, + DLTimeStamp = 0, +} + +--- Advanced state enumerator +-- @type MANTIS.AdvancedState +MANTIS.AdvancedState = { + GREEN = 0, + AMBER = 1, + RED = 2, } ----------------------------------------------------------------------- @@ -208,7 +221,7 @@ do --@param #string coaltion Coalition side of your setup, e.g. "blue", "red" or "neutral" --@param #boolean dynamic Use constant (true) filtering or just filter once (false, default) (optional) --@param #string awacs Group name of your Awacs (optional) - --@param #boolean EmOnOff Make MANTIS switch Emissions on and off instead of changing the alarm state between RED and GREEN (optional, deault true) + --@param #boolean EmOnOff Make MANTIS switch Emissions on and off instead of changing the alarm state between RED and GREEN --@return #MANTIS self --@usage Start up your MANTIS with a basic setting -- @@ -263,10 +276,17 @@ do self.ShoradLink = false self.ShoradTime = 600 self.ShoradActDistance = 15000 - -- TODO: add emissions on/off when available .... in 2 weeks + self.TimeStamp = timer.getAbsTime() + self.relointerval = math.random(1800,3600) -- random between 30 and 60 mins + self.state2flag = false + self.SamStateTracker = {} -- table to hold alert states, so we don't trigger state changes twice in adv mode + self.DLink = false + if EmOnOff then if EmOnOff == false then self.UseEmOnOff = false + else + self.UseEmOnOff = true end end @@ -277,7 +297,7 @@ do end -- Inherit everything from BASE class. - local self = BASE:Inherit(self, BASE:New()) -- #MANTIS + local self = BASE:Inherit(self, FSM:New()) -- #MANTIS -- Set the string id for output to DCS.log file. self.lid=string.format("MANTIS %s | ", self.name) @@ -308,27 +328,122 @@ do end -- @field #string version - self.version="0.4.1" + self.version="0.6.2" self:I(string.format("***** Starting MANTIS Version %s *****", self.version)) - return self - end + --- FSM Functions --- + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start FSM. + self:AddTransition("*", "Status", "*") -- MANTIS status update. + self:AddTransition("*", "Relocating", "*") -- MANTIS HQ and EWR are relocating. + self:AddTransition("*", "GreenState", "*") -- MANTIS A SAM switching to GREEN state. + self:AddTransition("*", "RedState", "*") -- MANTIS A SAM switching to RED state. + self:AddTransition("*", "AdvStateChange", "*") -- MANTIS advanced mode state change. + self:AddTransition("*", "ShoradActivated", "*") -- MANTIS woke up a connected SHORAD. + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the MANTIS. Initializes parameters and starts event handlers. + -- @function [parent=#MANTIS] Start + -- @param #MANTIS self + + --- Triggers the FSM event "Start" after a delay. Starts the MANTIS. Initializes parameters and starts event handlers. + -- @function [parent=#MANTIS] __Start + -- @param #MANTIS self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the MANTIS and all its event handlers. + -- @param #MANTIS self + + --- Triggers the FSM event "Stop" after a delay. Stops the MANTIS and all its event handlers. + -- @function [parent=#MANTIS] __Stop + -- @param #MANTIS self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Status". + -- @function [parent=#MANTIS] Status + -- @param #MANTIS self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#MANTIS] __Status + -- @param #MANTIS self + -- @param #number delay Delay in seconds. + + --- On After "Relocating" event. HQ and/or EWR moved. + -- @function [parent=#MANTIS] OnAfterRelocating + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @return #MANTIS self + + --- On After "GreenState" event. A SAM group was switched to GREEN alert. + -- @function [parent=#MANTIS] OnAfterGreenState + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @param Wrapper.Group#GROUP Group The GROUP object whose state was changed + -- @return #MANTIS self + + --- On After "RedState" event. A SAM group was switched to RED alert. + -- @function [parent=#MANTIS] OnAfterRedState + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @param Wrapper.Group#GROUP Group The GROUP object whose state was changed + -- @return #MANTIS self + + --- On After "AdvStateChange" event. Advanced state changed, influencing detection speed. + -- @function [parent=#MANTIS] OnAfterAdvStateChange + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @param #number Oldstate Old state - 0 = green, 1 = amber, 2 = red + -- @param #number Newstate New state - 0 = green, 1 = amber, 2 = red + -- @param #number Interval Calculated detection interval based on state and advanced feature setting + -- @return #MANTIS self + + --- On After "ShoradActivated" event. Mantis has activated a SHORAD. + -- @function [parent=#MANTIS] OnAfterShoradActivated + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @param #string Name Name of the GROUP which SHORAD shall protect + -- @param #number Radius Radius around the named group to find SHORAD groups + -- @param #number Ontime Seconds the SHORAD will stay active + + return self + end ----------------------------------------------------------------------- -- MANTIS helper functions ----------------------------------------------------------------------- - --- [internal] Function to get the self.SAM_Table + --- [Internal] Function to get the self.SAM_Table -- @param #MANTIS self -- @return #table table function MANTIS:_GetSAMTable() + self:T(self.lid .. "GetSAMTable") return self.SAM_Table end - --- [internal] Function to set the self.SAM_Table + --- [Internal] Function to set the self.SAM_Table -- @param #MANTIS self -- @return #MANTIS self function MANTIS:_SetSAMTable(table) + self:T(self.lid .. "SetSAMTable") self.SAM_Table = table return self end @@ -337,41 +452,50 @@ do -- @param #MANTIS self -- @param #number radius Radius upon which detected objects will be grouped function MANTIS:SetEWRGrouping(radius) + self:T(self.lid .. "SetEWRGrouping") local radius = radius or 5000 self.grouping = radius + return self end --- Function to set the detection radius of the EWR in meters -- @param #MANTIS self -- @param #number radius Radius of the EWR detection zone function MANTIS:SetEWRRange(radius) + self:T(self.lid .. "SetEWRRange") local radius = radius or 80000 self.acceptrange = radius + return self end --- Function to set switch-on/off zone for the SAM sites in meters -- @param #MANTIS self -- @param #number radius Radius of the firing zone function MANTIS:SetSAMRadius(radius) + self:T(self.lid .. "SetSAMRadius") local radius = radius or 25000 self.checkradius = radius + return self end --- Function to set SAM firing engage range, 0-100 percent, e.g. 75 -- @param #MANTIS self -- @param #number range Percent of the max fire range function MANTIS:SetSAMRange(range) + self:T(self.lid .. "SetSAMRange") local range = range or 75 if range < 0 or range > 100 then range = 75 end self.engagerange = range + return self end --- Function to set a new SAM firing engage range, use this method to adjust range while running MANTIS, e.g. for different setups day and night -- @param #MANTIS self -- @param #number range Percent of the max fire range function MANTIS:SetNewSAMRangeWhileRunning(range) + self:T(self.lid .. "SetNewSAMRangeWhileRunning") local range = range or 75 if range < 0 or range > 100 then range = 75 @@ -379,20 +503,32 @@ do self.engagerange = range self:_RefreshSAMTable() self.mysead.EngagementRange = range + return self end --- Function to set switch-on/off the debug state -- @param #MANTIS self -- @param #boolean onoff Set true to switch on function MANTIS:Debug(onoff) + self:T(self.lid .. "SetDebug") local onoff = onoff or false self.debug = onoff + if onoff then + -- Debug trace. + BASE:TraceOn() + BASE:TraceClass("MANTIS") + BASE:TraceLevel(1) + else + BASE:TraceOff() + end + return self end --- Function to get the HQ object for further use -- @param #MANTIS self -- @return Wrapper.GROUP#GROUP The HQ #GROUP object or *nil* if it doesn't exist function MANTIS:GetCommandCenter() + self:T(self.lid .. "GetCommandCenter") if self.HQ_CC then return self.HQ_CC else @@ -404,26 +540,31 @@ do -- @param #MANTIS self -- @param #string prefix Name of the AWACS group in the mission editor function MANTIS:SetAwacs(prefix) + self:T(self.lid .. "SetAwacs") if prefix ~= nil then if type(prefix) == "string" then self.AWACS_Prefix = prefix self.advAwacs = true end end + return self end --- Function to set AWACS detection range. Defaults to 250.000m (250km) - use **before** starting your Mantis! -- @param #MANTIS self -- @param #number range Detection range of the AWACS group function MANTIS:SetAwacsRange(range) - local range = range or 250000 - self.awacsrange = range + self:T(self.lid .. "SetAwacsRange") + local range = range or 250000 + self.awacsrange = range + return self end --- Function to set the HQ object for further use -- @param #MANTIS self -- @param Wrapper.GROUP#GROUP group The #GROUP object to be set as HQ function MANTIS:SetCommandCenter(group) + self:T(self.lid .. "SetCommandCenter") local group = group or nil if group ~= nil then if type(group) == "string" then @@ -434,14 +575,17 @@ do self.HQ_Template_CC = group:GetName() end end + return self end --- Function to set the detection interval -- @param #MANTIS self -- @param #number interval The interval in seconds function MANTIS:SetDetectInterval(interval) + self:T(self.lid .. "SetDetectInterval") local interval = interval or 30 self.detectinterval = interval + return self end --- Function to set Advanded Mode @@ -451,7 +595,8 @@ do -- @usage Advanced mode will *decrease* reactivity of MANTIS, if HQ and/or EWR network dies. Set SAMs to RED state if both are dead. Requires usage of an **HQ** object and the **dynamic** option. -- E.g. `mymantis:SetAdvancedMode(true, 90)` function MANTIS:SetAdvancedMode(onoff, ratio) - self:F({onoff, ratio}) + self:T(self.lid .. "SetAdvancedMode") + --self.T({onoff, ratio}) local onoff = onoff or false local ratio = ratio or 100 if (type(self.HQ_Template_CC) == "string") and onoff and self.dynamic then @@ -459,53 +604,69 @@ do self.advanced = true self.adv_state = 0 self.Adv_EWR_Group = SET_GROUP:New():FilterPrefixes(self.EWR_Templates_Prefix):FilterCoalitions(self.Coalition):FilterStart() - env.info(string.format("***** Starting Advanced Mode MANTIS Version %s *****", self.version)) + self:I(string.format("***** Starting Advanced Mode MANTIS Version %s *****", self.version)) else local text = self.lid.." Advanced Mode requires a HQ and dynamic to be set. Revisit your MANTIS:New() statement to add both." local m= MESSAGE:New(text,10,"MANTIS",true):ToAll() - BASE:E(text) + self:E(text) end + return self end --- Set using Emissions on/off instead of changing alarm state -- @param #MANTIS self -- @param #boolean switch Decide if we are changing alarm state or Emission state function MANTIS:SetUsingEmOnOff(switch) + self:T(self.lid .. "SetUsingEmOnOff") self.UseEmOnOff = switch or false + return self + end + + --- Set using an #INTEL_DLINK object instead of #DETECTION + -- @param #MANTIS self + -- @param Ops.Intelligence#INTEL_DLINK DLink The data link object to be used. + function MANTIS:SetUsingDLink(DLink) + self:T(self.lid .. "SetUsingDLink") + self.DLink = true + self.Detection = DLink + self.DLTimeStamp = timer.getAbsTime() + return self end --- [Internal] Function to check if HQ is alive -- @param #MANTIS self -- @return #boolean True if HQ is alive, else false function MANTIS:_CheckHQState() + self:T(self.lid .. "CheckHQState") local text = self.lid.." Checking HQ State" - self:T(text) local m= MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) - if self.verbose then env.info(text) end + if self.verbose then self:I(text) end -- start check if self.advanced then local hq = self.HQ_Template_CC local hqgrp = GROUP:FindByName(hq) if hqgrp then if hqgrp:IsAlive() then -- ok we're on, hq exists and as alive - env.info(self.lid.." HQ is alive!") + --self.T(self.lid.." HQ is alive!") return true else - env.info(self.lid.." HQ is dead!") + --self.T(self.lid.." HQ is dead!") return false end end - end + end + return self end --- [Internal] Function to check if EWR is (at least partially) alive -- @param #MANTIS self -- @return #boolean True if EWR is alive, else false function MANTIS:_CheckEWRState() + self:T(self.lid .. "CheckEWRState") local text = self.lid.." Checking EWR State" - self:F(text) + --self.T(text) local m= MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) - if self.verbose then env.info(text) end + if self.verbose then self:I(text) end -- start check if self.advanced then local EWR_Group = self.Adv_EWR_Group @@ -519,24 +680,24 @@ do end end end - env.info(self.lid..string.format(" No of EWR alive is %d", nalive)) + --self.T(self.lid..string.format(" No of EWR alive is %d", nalive)) if nalive > 0 then return true else return false end - end + end + return self end --- [Internal] Function to determine state of the advanced mode -- @param #MANTIS self -- @return #number Newly calculated interval -- @return #number Previous state for tracking 0, 1, or 2 - function MANTIS:_CheckAdvState() - local text = self.lid.." Checking Advanced State" - self:F(text) - local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) - if self.verbose then env.info(text) end + function MANTIS:_CalcAdvState() + self:T(self.lid .. "CalcAdvState") + local m=MESSAGE:New(self.lid.." Calculating Advanced State",10,"MANTIS"):ToAllIf(self.debug) + if self.verbose then self:I(self.lid.." Calculating Advanced State") end -- start check local currstate = self.adv_state -- save curr state for comparison later local EWR_State = self:_CheckEWRState() @@ -554,10 +715,12 @@ do local ratio = self.adv_ratio / 100 -- e.g. 80/100 = 0.8 ratio = ratio * self.adv_state -- e.g 0.8*2 = 1.6 local newinterval = interval + (interval * ratio) -- e.g. 30+(30*1.6) = 78 - local text = self.lid..string.format(" Calculated OldState/NewState/Interval: %d / %d / %d", currstate, self.adv_state, newinterval) - self:F(text) - local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) - if self.verbose then env.info(text) end + if self.debug or self.verbose then + local text = self.lid..string.format(" Calculated OldState/NewState/Interval: %d / %d / %d", currstate, self.adv_state, newinterval) + --self.T(text) + local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) + if self.verbose then self:I(text) end + end return newinterval, currstate end @@ -566,31 +729,34 @@ do -- @param #boolean hq If true, will relocate HQ object -- @param #boolean ewr If true, will relocate EWR objects function MANTIS:SetAutoRelocate(hq, ewr) - self:F({hq, ewr}) + self:T(self.lid .. "SetAutoRelocate") + --self.T({hq, ewr}) local hqrel = hq or false local ewrel = ewr or false if hqrel or ewrel then self.autorelocate = true self.autorelocateunits = { HQ = hqrel, EWR = ewrel } - self:T({self.autorelocate, self.autorelocateunits}) + --self.T({self.autorelocate, self.autorelocateunits}) end + return self end --- [Internal] Function to execute the relocation -- @param #MANTIS self function MANTIS:_RelocateGroups() - self:T(self.lid.." Relocating Groups") + self:T(self.lid .. "RelocateGroups") local text = self.lid.." Relocating Groups" local m= MESSAGE:New(text,10,"MANTIS",true):ToAllIf(self.debug) - if self.verbose then env.info(text) end + if self.verbose then self:I(text) end if self.autorelocate then -- relocate HQ - if self.autorelocateunits.HQ and self.HQ_CC then --only relocate if HQ exists + local HQGroup = self.HQ_CC + if self.autorelocateunits.HQ and self.HQ_CC and HQGroup:IsAlive() then --only relocate if HQ exists local _hqgrp = self.HQ_CC - self:T(self.lid.." Relocating HQ") + --self.T(self.lid.." Relocating HQ") local text = self.lid.." Relocating HQ" - local m= MESSAGE:New(text,10,"MANTIS"):ToAll() - _hqgrp:RelocateGroundRandomInRadius(20,500,true,true) + --local m= MESSAGE:New(text,10,"MANTIS"):ToAll() + _hqgrp:RelocateGroundRandomInRadius(20,500,true,true) end --relocate EWR -- TODO: maybe dependent on AlarmState? Observed: SA11 SR only relocates if no objects in reach @@ -599,38 +765,41 @@ do local EWR_GRP = SET_GROUP:New():FilterPrefixes(self.EWR_Templates_Prefix):FilterCoalitions(self.Coalition):FilterOnce() local EWR_Grps = EWR_GRP.Set --table of objects in SET_GROUP for _,_grp in pairs (EWR_Grps) do - if _grp:IsGround() then - self:T(self.lid.." Relocating EWR ".._grp:GetName()) + if _grp:IsAlive() and _grp:IsGround() then + --self.T(self.lid.." Relocating EWR ".._grp:GetName()) local text = self.lid.." Relocating EWR ".._grp:GetName() local m= MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) - if self.verbose then env.info(text) end + if self.verbose then self:I(text) end _grp:RelocateGroundRandomInRadius(20,500,true,true) end end end end + return self end - --- (Internal) Function to check if any object is in the given SAM zone + --- [Internal] Function to check if any object is in the given SAM zone -- @param #MANTIS self -- @param #table dectset Table of coordinates of detected items - -- @param samcoordinate Core.Point#COORDINATE Coordinate object. + -- @param Core.Point#COORDINATE samcoordinate Coordinate object. -- @return #boolean True if in any zone, else false -- @return #number Distance Target distance in meters or zero when no object is in zone function MANTIS:CheckObjectInZone(dectset, samcoordinate) - self:F(self.lid.."CheckObjectInZone Called") + self:T(self.lid.."CheckObjectInZone") -- check if non of the coordinate is in the given defense zone local radius = self.checkradius local set = dectset for _,_coord in pairs (set) do local coord = _coord -- get current coord to check -- output for cross-check - local dectstring = coord:ToStringLLDMS() - local samstring = samcoordinate:ToStringLLDMS() local targetdistance = samcoordinate:DistanceFromPointVec2(coord) - local text = string.format("Checking SAM at % s - Distance %d m - Target %s", samstring, targetdistance, dectstring) - local m = MESSAGE:New(text,10,"Check"):ToAllIf(self.debug) - if self.verbose then env.info(self.lid..text) end + if self.verbose or self.debug then + local dectstring = coord:ToStringLLDMS() + local samstring = samcoordinate:ToStringLLDMS() + local text = string.format("Checking SAM at % s - Distance %d m - Target %s", samstring, targetdistance, dectstring) + local m = MESSAGE:New(text,10,"Check"):ToAllIf(self.debug) + self:I(self.lid..text) + end -- end output to cross-check if targetdistance <= radius then return true, targetdistance @@ -639,11 +808,11 @@ do return false, 0 end - --- (Internal) Function to start the detection via EWR groups + --- [Internal] Function to start the detection via EWR groups -- @param #MANTIS self -- @return Functional.Detection #DETECTION_AREAS The running detection set function MANTIS:StartDetection() - self:F(self.lid.."Starting Detection") + self:T(self.lid.."Starting Detection") -- start detection local groupset = self.EWR_Group @@ -651,14 +820,14 @@ do local acceptrange = self.acceptrange or 80000 local interval = self.detectinterval or 60 - --@param Functional.Detection #DETECTION_AREAS _MANTISdetection [internal] The MANTIS detection object - _MANTISdetection = DETECTION_AREAS:New( groupset, grouping ) --[internal] Grouping detected objects to 5000m zones - _MANTISdetection:FilterCategories({ Unit.Category.AIRPLANE, Unit.Category.HELICOPTER }) - _MANTISdetection:SetAcceptRange(acceptrange) - _MANTISdetection:SetRefreshTimeInterval(interval) - _MANTISdetection:Start() + --@param Functional.Detection #DETECTION_AREAS _MANTISdetection [Internal] The MANTIS detection object + local MANTISdetection = DETECTION_AREAS:New( groupset, grouping ) --[Internal] Grouping detected objects to 5000m zones + MANTISdetection:FilterCategories({ Unit.Category.AIRPLANE, Unit.Category.HELICOPTER }) + MANTISdetection:SetAcceptRange(acceptrange) + MANTISdetection:SetRefreshTimeInterval(interval) + MANTISdetection:Start() - function _MANTISdetection:OnAfterDetectedItem(From,Event,To,DetectedItem) + function MANTISdetection:OnAfterDetectedItem(From,Event,To,DetectedItem) --BASE:I( { From, Event, To, DetectedItem }) local debug = false if DetectedItem.IsDetected and debug then @@ -667,14 +836,14 @@ do local m = MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) end end - return _MANTISdetection + return MANTISdetection end - --- (Internal) Function to start the detection via AWACS if defined as separate + --- [Internal] Function to start the detection via AWACS if defined as separate -- @param #MANTIS self -- @return Functional.Detection #DETECTION_AREAS The running detection set function MANTIS:StartAwacsDetection() - self:F(self.lid.."Starting Awacs Detection") + self:T(self.lid.."Starting Awacs Detection") -- start detection local group = self.AWACS_Prefix @@ -683,14 +852,14 @@ do --local acceptrange = self.acceptrange or 80000 local interval = self.detectinterval or 60 - --@param Functional.Detection #DETECTION_AREAS _MANTISdetection [internal] The MANTIS detection object - _MANTISAwacs = DETECTION_AREAS:New( groupset, grouping ) --[internal] Grouping detected objects to 5000m zones - _MANTISAwacs:FilterCategories({ Unit.Category.AIRPLANE, Unit.Category.HELICOPTER }) - _MANTISAwacs:SetAcceptRange(self.awacsrange) --250km - _MANTISAwacs:SetRefreshTimeInterval(interval) - _MANTISAwacs:Start() + --@param Functional.Detection #DETECTION_AREAS _MANTISdetection [Internal] The MANTIS detection object + local MANTISAwacs = DETECTION_AREAS:New( groupset, grouping ) --[Internal] Grouping detected objects to 5000m zones + MANTISAwacs:FilterCategories({ Unit.Category.AIRPLANE, Unit.Category.HELICOPTER }) + MANTISAwacs:SetAcceptRange(self.awacsrange) --250km + MANTISAwacs:SetRefreshTimeInterval(interval) + MANTISAwacs:Start() - function _MANTISAwacs:OnAfterDetectedItem(From,Event,To,DetectedItem) + function MANTISAwacs:OnAfterDetectedItem(From,Event,To,DetectedItem) --BASE:I( { From, Event, To, DetectedItem }) local debug = false if DetectedItem.IsDetected and debug then @@ -699,15 +868,15 @@ do local m = MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) end end - return _MANTISAwacs + return MANTISAwacs end - --- (Internal) Function to set the SAM start state + --- [Internal] Function to set the SAM start state -- @param #MANTIS self -- @return #MANTIS self function MANTIS:SetSAMStartState() -- DONE: if using dynamic filtering, update SAM_Table and the (active) SEAD groups, pull req #1405/#1406 - self:F(self.lid.."Setting SAM Start States") + self:T(self.lid.."Setting SAM Start States") -- get SAM Group local SAM_SET = self.SAM_Group local SAM_Grps = SAM_SET.Set --table of objects @@ -716,7 +885,7 @@ do local engagerange = self.engagerange -- firing range in % of max --cycle through groups and set alarm state etc for _i,_group in pairs (SAM_Grps) do - local group = _group + local group = _group -- Wrapper.Group#GROUP -- TODO: add emissions on/off if self.UseEmOnOff then group:EnableEmission(false) @@ -725,11 +894,12 @@ do group:OptionAlarmStateGreen() -- AI off end group:SetOption(AI.Option.Ground.id.AC_ENGAGEMENT_RANGE_RESTRICTION,engagerange) --default engagement will be 75% of firing range - if group:IsGround() then + if group:IsGround() and group:IsAlive() then local grpname = group:GetName() local grpcoord = group:GetCoordinate() table.insert( SAM_Tbl, {grpname, grpcoord}) table.insert( SEAD_Grps, grpname ) + self.SamStateTracker[grpname] = "GREEN" end end self.SAM_Table = SAM_Tbl @@ -740,11 +910,11 @@ do return self end - --- (Internal) Function to update SAM table and SEAD state + --- [Internal] Function to update SAM table and SEAD state -- @param #MANTIS self -- @return #MANTIS self function MANTIS:_RefreshSAMTable() - self:F(self.lid.."Setting SAM Start States") + self:T(self.lid.."RefreshSAMTable") -- Requires SEAD 0.2.2 or better -- get SAM Group local SAM_SET = self.SAM_Group @@ -756,7 +926,7 @@ do for _i,_group in pairs (SAM_Grps) do local group = _group group:SetOption(AI.Option.Ground.id.AC_ENGAGEMENT_RANGE_RESTRICTION,engagerange) --engagement will be 75% of firing range - if group:IsGround() then + if group:IsGround() and group:IsAlive() then local grpname = group:GetName() local grpcoord = group:GetCoordinate() table.insert( SAM_Tbl, {grpname, grpcoord}) -- make the table lighter, as I don't really use the zone here @@ -777,6 +947,7 @@ do -- @param Functional.Shorad#SHORAD Shorad The #SHORAD object -- @param #number Shoradtime Number of seconds #SHORAD stays active post wake-up function MANTIS:AddShorad(Shorad,Shoradtime) + self:T(self.lid.."AddShorad") local Shorad = Shorad or nil local ShoradTime = Shoradtime or 600 local ShoradLink = true @@ -785,184 +956,311 @@ do self.Shorad = Shorad --#SHORAD self.ShoradTime = Shoradtime -- #number end + return self end --- Function to unlink #MANTIS from a #SHORAD installation -- @param #MANTIS self function MANTIS:RemoveShorad() + self:T(self.lid.."RemoveShorad") self.ShoradLink = false + return self end ----------------------------------------------------------------------- -- MANTIS main functions ----------------------------------------------------------------------- - --- Function to set the SAM start state + --- [Internal] Check detection function + -- @param #MANTIS self + -- @param Functional.Detection#DETECTION_AREAS detection Detection object + -- @return #MANTIS self + function MANTIS:_Check(detection) + self:T(self.lid .. "Check") + --get detected set + local detset = detection:GetDetectedItemCoordinates() + self:T("Check:", {detset}) + -- randomly update SAM Table + local rand = math.random(1,100) + if rand > 65 then -- 1/3 of cases + self:_RefreshSAMTable() + end + -- switch SAMs on/off if (n)one of the detected groups is inside their reach + local samset = self:_GetSAMTable() -- table of i.1=names, i.2=coordinates + for _,_data in pairs (samset) do + local samcoordinate = _data[2] + local name = _data[1] + local samgroup = GROUP:FindByName(name) + local IsInZone, Distance = self:CheckObjectInZone(detset, samcoordinate) + if IsInZone then --check any target in zone + if samgroup:IsAlive() then + -- switch on SAM + if self.UseEmOnOff then + -- TODO: add emissions on/off + --samgroup:SetAIOn() + samgroup:EnableEmission(true) + end + samgroup:OptionAlarmStateRed() + if self.SamStateTracker[name] ~= "RED" then + self:__RedState(1,samgroup) + self.SamStateTracker[name] = "RED" + end + -- link in to SHORAD if available + -- DONE: Test integration fully + if self.ShoradLink and Distance < self.ShoradActDistance then -- don't give SHORAD position away too early + local Shorad = self.Shorad + local radius = self.checkradius + local ontime = self.ShoradTime + Shorad:WakeUpShorad(name, radius, ontime) + self:__ShoradActivated(1,name, radius, ontime) + end + -- debug output + if self.debug or self.verbose then + local text = string.format("SAM %s switched to alarm state RED!", name) + local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) + if self.verbose then self:I(self.lid..text) end + end + end --end alive + else + if samgroup:IsAlive() then + -- switch off SAM + if self.UseEmOnOff then + samgroup:EnableEmission(false) + end + samgroup:OptionAlarmStateGreen() + if self.SamStateTracker[name] ~= "GREEN" then + self:__GreenState(1,samgroup) + self.SamStateTracker[name] = "GREEN" + end + if self.debug or self.verbose then + local text = string.format("SAM %s switched to alarm state GREEN!", name) + local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) + if self.verbose then self:I(self.lid..text) end + end + end --end alive + end --end check + end --for for loop + return self + end + + --- [Internal] Relocation relay function -- @param #MANTIS self -- @return #MANTIS self - function MANTIS:Start() - self:F(self.lid.."Starting MANTIS") - self:SetSAMStartState() - self.Detection = self:StartDetection() - if self.advAwacs then - self.AWACS_Detection = self:StartAwacsDetection() - end - -- detection function - local function check(detection) - --get detected set - local detset = detection:GetDetectedItemCoordinates() - self:F("Check:", {detset}) - -- randomly update SAM Table - local rand = math.random(1,100) - if rand > 65 then -- 1/3 of cases - self:_RefreshSAMTable() - end - -- switch SAMs on/off if (n)one of the detected groups is inside their reach - local samset = self:_GetSAMTable() -- table of i.1=names, i.2=coordinates - for _,_data in pairs (samset) do - local samcoordinate = _data[2] - local name = _data[1] - local samgroup = GROUP:FindByName(name) - local IsInZone, Distance = self:CheckObjectInZone(detset, samcoordinate) - if IsInZone then --check any target in zone + function MANTIS:_Relocate() + self:T(self.lid .. "Relocate") + self:_RelocateGroups() + return self + end + + --- [Internal] Check advanced state + -- @param #MANTIS self + -- @return #MANTIS self + function MANTIS:_CheckAdvState() + self:T(self.lid .. "CheckAdvSate") + local interval, oldstate = self:_CalcAdvState() + local newstate = self.adv_state + if newstate ~= oldstate then + -- deal with new state + self:__AdvStateChange(1,oldstate,newstate,interval) + if newstate == 2 then + -- switch alarm state RED + self.state2flag = true + local samset = self:_GetSAMTable() -- table of i.1=names, i.2=coordinates + for _,_data in pairs (samset) do + local name = _data[1] + local samgroup = GROUP:FindByName(name) if samgroup:IsAlive() then - -- switch on SAM if self.UseEmOnOff then -- TODO: add emissions on/off --samgroup:SetAIOn() samgroup:EnableEmission(true) end samgroup:OptionAlarmStateRed() - -- link in to SHORAD if available - -- DONE: Test integration fully - if self.ShoradLink and Distance < self.ShoradActDistance then -- don't give SHORAD position away too early - local Shorad = self.Shorad - local radius = self.checkradius - local ontime = self.ShoradTime - Shorad:WakeUpShorad(name, radius, ontime) - end - -- debug output - local text = string.format("SAM %s switched to alarm state RED!", name) - local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) - if self.verbose then env.info(self.lid..text) end - end --end alive - else - if samgroup:IsAlive() then - -- switch off SAM - if self.UseEmOnOff then - -- TODO: add emissions on/off - samgroup:EnableEmission(false) - --samgroup:SetAIOff() - else - samgroup:OptionAlarmStateGreen() - end - --samgroup:OptionROEWeaponFree() - --samgroup:SetAIOn() - local text = string.format("SAM %s switched to alarm state GREEN!", name) - local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) - if self.verbose then env.info(self.lid..text) end - end --end alive - end --end check - end --for for loop - end --end function - -- relocation relay function - local function relocate() - self:_RelocateGroups() - end - -- check advanced state - local function checkadvstate() - local interval, oldstate = self:_CheckAdvState() - local newstate = self.adv_state - if newstate ~= oldstate then - -- deal with new state - if newstate == 2 then - -- switch alarm state RED - if self.MantisTimer.isrunning then - self.MantisTimer:Stop() - self.MantisTimer.isrunning = false - end -- stop Awacs timer - if self.MantisATimer.isrunning then - self.MantisATimer:Stop() - self.MantisATimer.isrunning = false - end -- stop timer - local samset = self:_GetSAMTable() -- table of i.1=names, i.2=coordinates - for _,_data in pairs (samset) do - local name = _data[1] - local samgroup = GROUP:FindByName(name) - if samgroup:IsAlive() then - if self.UseEmOnOff then - -- TODO: add emissions on/off - --samgroup:SetAIOn() - samgroup:EnableEmission(true) - end - samgroup:OptionAlarmStateRed() - end -- end alive - end -- end for loop - elseif newstate <= 1 then - -- change MantisTimer to slow down or speed up - if self.MantisTimer.isrunning then - self.MantisTimer:Stop() - self.MantisTimer.isrunning = false - end - if self.MantisATimer.isrunning then - self.MantisATimer:Stop() - self.MantisATimer.isrunning = false - end - self.MantisTimer = TIMER:New(check,self.Detection) - self.MantisTimer:Start(5,interval,nil) - self.MantisTimer.isrunning = true - if self.advAwacs then - self.MantisATimer = TIMER:New(check,self.AWACS_Detection) - self.MantisATimer:Start(15,interval,nil) - self.MantisATimer.isrunning = true - end - end - end -- end newstate vs oldstate - end - -- timers to run the system - local interval = self.detectinterval - self.MantisTimer = TIMER:New(check,self.Detection) - self.MantisTimer:Start(5,interval,nil) - self.MantisTimer.isrunning = true - -- Awacs timer - if self.advAwacs then - self.MantisATimer = TIMER:New(check,self.AWACS_Detection) - self.MantisATimer:Start(15,interval,nil) - self.MantisATimer.isrunning = true - end - -- timer to relocate HQ and EWR - if self.autorelocate then - local relointerval = math.random(1800,3600) -- random between 30 and 60 mins - self.MantisReloTimer = TIMER:New(relocate) - self.MantisReloTimer:Start(relointerval,relointerval,nil) - end - -- timer for advanced state check - if self.advanced then - self.MantisAdvTimer = TIMER:New(checkadvstate) - self.MantisAdvTimer:Start(30,interval*5,nil) - end + end -- end alive + end -- end for loop + elseif newstate <= 1 then + -- change MantisTimer to slow down or speed up + self.detectinterval = interval + self.state2flag = false + end + end -- end newstate vs oldstate return self end - --- Function to stop MANTIS + --- [Internal] Check DLink state -- @param #MANTIS self -- @return #MANTIS self - function MANTIS:Stop() - if self.MantisTimer.isrunning then - self.MantisTimer:Stop() + function MANTIS:_CheckDLinkState() + self:T(self.lid .. "_CheckDLinkState") + local dlink = self.Detection -- Ops.Intelligence#INTEL_DLINK + local TS = timer.getAbsTime() + if not dlink:Is("Running") and (TS - self.DLTimeStamp > 29) then + self.DLink = false + self.Detection = self:StartDetection() -- fall back + self:I(self.lid .. "Intel DLink not running - switching back to single detection!") end - if self.MantisATimer.isrunning then - self.MantisATimer:Stop() + end + + --- [Internal] Function to set start state + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @return #MANTIS self + function MANTIS:onafterStart(From, Event, To) + self:T({From, Event, To}) + self:T(self.lid.."Starting MANTIS") + self:SetSAMStartState() + if not self.DLink then + self.Detection = self:StartDetection() end - if self.autorelocate then - self.MantisReloTimer:Stop() + if self.advAwacs then + self.AWACS_Detection = self:StartAwacsDetection() end - if self.advanced then - self.MantisAdvTimer:Stop() - end - return self + self:__Status(-math.random(1,10)) + return self end + --- [Internal] Before status function for MANTIS + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @return #MANTIS self + function MANTIS:onbeforeStatus(From, Event, To) + self:T({From, Event, To}) + -- check detection + if not self.state2flag then + self:_Check(self.Detection) + end + + -- check Awacs + if self.advAwacs and not self.state2flag then + self:_Check(self.AWACS_Detection) + end + + -- relocate HQ and EWR + if self.autorelocate then + local relointerval = self.relointerval + local thistime = timer.getAbsTime() + local timepassed = thistime - self.TimeStamp + + local halfintv = math.floor(timepassed / relointerval) + + --self:T({timepassed=timepassed, halfintv=halfintv}) + + if halfintv >= 1 then + self.TimeStamp = timer.getAbsTime() + self:_Relocate() + self:__Relocating(1) + end + end + + -- advanced state check + if self.advanced then + self:_CheckAdvState() + end + + -- check DLink state + if self.DLink then + self:_CheckDLinkState() + end + + return self + end + + --- [Internal] Status function for MANTIS + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @return #MANTIS self + function MANTIS:onafterStatus(From,Event,To) + self:T({From, Event, To}) + -- Display some states + if self.debug then + self:I(self.lid .. "Status Report") + for _name,_state in pairs(self.SamStateTracker) do + self:I(string.format("Site %s\tStatus %s",_name,_state)) + end + end + local interval = self.detectinterval * -1 + self:__Status(interval) + return self + end + + --- [Internal] Function to stop MANTIS + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @return #MANTIS self + function MANTIS:onafterStop(From, Event, To) + self:T({From, Event, To}) + return self + end + + --- [Internal] Function triggered by Event Relocating + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @return #MANTIS self + function MANTIS:onafterRelocating(From, Event, To) + self:T({From, Event, To}) + return self + end + + --- [Internal] Function triggered by Event GreenState + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @param Wrapper.Group#GROUP Group The GROUP object whose state was changed + -- @return #MANTIS self + function MANTIS:onafterGreenState(From, Event, To, Group) + self:T({From, Event, To, Group}) + return self + end + + --- [Internal] Function triggered by Event RedState + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @param Wrapper.Group#GROUP Group The GROUP object whose state was changed + -- @return #MANTIS self + function MANTIS:onafterRedState(From, Event, To, Group) + self:T({From, Event, To, Group}) + return self + end + + --- [Internal] Function triggered by Event AdvStateChange + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @param #number Oldstate Old state - 0 = green, 1 = amber, 2 = red + -- @param #number Newstate New state - 0 = green, 1 = amber, 2 = red + -- @param #number Interval Calculated detection interval based on state and advanced feature setting + -- @return #MANTIS self + function MANTIS:onafterAdvStateChange(From, Event, To, Oldstate, Newstate, Interval) + self:T({From, Event, To, Oldstate, Newstate, Interval}) + return self + end + + --- [Internal] Function triggered by Event ShoradActivated + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @param #string Name Name of the GROUP which SHORAD shall protect + -- @param #number Radius Radius around the named group to find SHORAD groups + -- @param #number Ontime Seconds the SHORAD will stay active + function MANTIS:onafterShoradActivated(From, Event, To, Name, Radius, Ontime) + self:T({From, Event, To, Name, Radius, Ontime}) + return self + end end ----------------------------------------------------------------------- -- MANTIS end diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 327e2e518..691f53338 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -5786,6 +5786,8 @@ end -- If desired, the @{#RATMANAGER} can be stopped by the @{#RATMANAGER.Stop}(stoptime) function. The parameter "stoptime" specifies the time delay in seconds after which the manager stops. -- When this happens, no new aircraft will be spawned and the population will eventually decrease to zero. -- +-- When you are using a time intervall like @{#RATMANAGER.dTspawn}(delay), @{#RATMANAGER} will ignore the amount set with @{#RATMANAGER.New}(). @{#RATMANAGER.dTspawn}(delay) will spawn infinite groups. +-- -- ## Example -- In this example, three different @{#RAT} objects are created (but not spawned manually). The @{#RATMANAGER} takes care that at least five aircraft of each type are alive and that the total number of aircraft -- spawned is 25. The @{#RATMANAGER} is started after 30 seconds and stopped after two hours. diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index fc850743c..7ba9dcfc7 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -36,7 +36,7 @@ -- -- === -- --- ## Sound files: Check out the pinned messages in the Moose discord *#func-range* channel. +-- ## Sound files: [MOOSE Sound Files](https://github.com/FlightControl-Master/MOOSE_SOUND/releases) -- -- === -- @@ -91,9 +91,9 @@ -- @field #boolean defaultsmokebomb If true, initialize player settings to smoke bomb. -- @field #boolean autosave If true, automatically save results every X seconds. -- @field #number instructorfreq Frequency on which the range control transmitts. --- @field Core.RadioQueue#RADIOQUEUE instructor Instructor radio queue. +-- @field Sound.RadioQueue#RADIOQUEUE instructor Instructor radio queue. -- @field #number rangecontrolfreq Frequency on which the range control transmitts. --- @field Core.RadioQueue#RADIOQUEUE rangecontrol Range control radio queue. +-- @field Sound.RadioQueue#RADIOQUEUE rangecontrol Range control radio queue. -- @field #string rangecontrolrelayname Name of relay unit. -- @field #string instructorrelayname Name of relay unit. -- @field #string soundpath Path inside miz file where the sound files are located. Default is "Range Soundfiles/". diff --git a/Moose Development/Moose/Functional/Scoring.lua b/Moose Development/Moose/Functional/Scoring.lua index b5c8da81b..b92c7e789 100644 --- a/Moose Development/Moose/Functional/Scoring.lua +++ b/Moose Development/Moose/Functional/Scoring.lua @@ -667,8 +667,6 @@ function SCORING:_AddPlayerFromUnit( UnitData ) self.Players[PlayerName].ThreatLevel = UnitThreatLevel self.Players[PlayerName].ThreatType = UnitThreatType - -- TODO: DCS bug concerning Units with skill level client don't get destroyed in multi player. This logic is deactivated until this bug gets fixed. - --[[ if self.Players[PlayerName].Penalty > self.Fratricide * 0.50 then if self.Players[PlayerName].PenaltyWarning < 1 then MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "': WARNING! If you continue to commit FRATRICIDE and have a PENALTY score higher than " .. self.Fratricide .. ", you will be COURT MARTIALED and DISMISSED from this mission! \nYour total penalty is: " .. self.Players[PlayerName].Penalty, @@ -684,8 +682,6 @@ function SCORING:_AddPlayerFromUnit( UnitData ) ):ToAll() UnitData:GetGroup():Destroy() end - --]] - end end diff --git a/Moose Development/Moose/Functional/Sead.lua b/Moose Development/Moose/Functional/Sead.lua index 8eb98d4f4..c99e1f241 100644 --- a/Moose Development/Moose/Functional/Sead.lua +++ b/Moose Development/Moose/Functional/Sead.lua @@ -1,26 +1,26 @@ --- **Functional** -- Make SAM sites execute evasive and defensive behaviour when being fired upon. --- +-- -- === --- +-- -- ## Features: --- +-- -- * When SAM sites are being fired upon, the SAMs will take evasive action will reposition themselves when possible. -- * When SAM sites are being fired upon, the SAMs will take defensive action by shutting down their radars. --- +-- -- === --- +-- -- ## Missions: --- +-- -- [SEV - SEAD Evasion](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/SEV%20-%20SEAD%20Evasion) --- +-- -- === --- +-- -- ### Authors: **FlightControl**, **applevangelist** --- --- Last Update: April 2021 --- +-- +-- Last Update: July 2021 +-- -- === --- +-- -- @module Functional.Sead -- @image SEAD.JPG @@ -28,49 +28,32 @@ -- @extends Core.Base#BASE --- Make SAM sites execute evasive and defensive behaviour when being fired upon. --- +-- -- This class is very easy to use. Just setup a SEAD object by using @{#SEAD.New}() and SAMs will evade and take defensive action when being fired upon. --- +-- -- # Constructor: --- +-- -- Use the @{#SEAD.New}() constructor to create a new SEAD object. --- +-- -- SEAD_RU_SAM_Defenses = SEAD:New( { 'RU SA-6 Kub', 'RU SA-6 Defenses', 'RU MI-26 Troops', 'RU Attack Gori' } ) --- +-- -- @field #SEAD SEAD = { - ClassName = "SEAD", - TargetSkill = { - Average = { Evade = 30, DelayOn = { 40, 60 } } , - Good = { Evade = 20, DelayOn = { 30, 50 } } , - High = { Evade = 15, DelayOn = { 20, 40 } } , - Excellent = { Evade = 10, DelayOn = { 10, 30 } } - }, - SEADGroupPrefixes = {}, - SuppressedGroups = {}, - EngagementRange = 75 -- default 75% engagement range Feature Request #1355 + ClassName = "SEAD", + TargetSkill = { + Average = { Evade = 30, DelayOn = { 40, 60 } } , + Good = { Evade = 20, DelayOn = { 30, 50 } } , + High = { Evade = 15, DelayOn = { 20, 40 } } , + Excellent = { Evade = 10, DelayOn = { 10, 30 } } + }, + SEADGroupPrefixes = {}, + SuppressedGroups = {}, + EngagementRange = 75 -- default 75% engagement range Feature Request #1355 } - -- TODO Complete list? --- Missile enumerators -- @field Harms SEAD.Harms = { - --[[ - ["X58"] = "weapons.missiles.X_58", --Kh-58X anti-radiation missiles fired - ["Kh25"] = "weapons.missiles.Kh25MP_PRGS1VP", --Kh-25MP anti-radiation missiles fired - ["X25"] = "weapons.missiles.X_25MP", --Kh-25MPU anti-radiation missiles fired - ["X28"] = "weapons.missiles.X_28", --Kh-28 anti-radiation missiles fired - ["X31"] = "weapons.missiles.X_31P", --Kh-31P anti-radiation missiles fired - ["AGM45A"] = "weapons.missiles.AGM_45A", --AGM-45A anti-radiation missiles fired - ["AGM45"] = "weapons.missiles.AGM_45", --AGM-45B anti-radiation missiles fired - ["AGM88"] = "weapons.missiles.AGM_88", --AGM-88C anti-radiation missiles fired - ["AGM122"] = "weapons.missiles.AGM_122", --AGM-122 Sidearm anti-radiation missiles fired - ["LD10"] = "weapons.missiles.LD-10", --LD-10 anti-radiation missiles fired - ["ALARM"] = "weapons.missiles.ALARM", --ALARM anti-radiation missiles fired - ["AGM84E"] = "weapons.missiles.AGM_84E", --AGM84 anti-radiation missiles fired - ["AGM84A"] = "weapons.missiles.AGM_84A", --AGM84 anti-radiation missiles fired - ["AGM84H"] = "weapons.missiles.AGM_84H", --AGM84 anti-radiation missiles fired - --]] ["AGM_88"] = "AGM_88", ["AGM_45"] = "AGM_45", ["AGM_122"] = "AGM_122", @@ -84,7 +67,7 @@ SEAD = { ["X_31"] = "X_31", ["Kh25"] = "Kh25", } - + --- Creates the main object which is handling defensive actions for SA sites or moving SA vehicles. -- When an anti radiation missile is fired (KH-58, KH-31P, KH-31A, KH-25MPU, HARM missiles), the SA will shut down their radars and will take evasive actions... -- Chances are big that the missile will miss. @@ -97,20 +80,20 @@ SEAD = { -- SEAD_RU_SAM_Defenses = SEAD:New( { 'RU SA-6 Kub', 'RU SA-6 Defenses', 'RU MI-26 Troops', 'RU Attack Gori' } ) function SEAD:New( SEADGroupPrefixes ) - local self = BASE:Inherit( self, BASE:New() ) - self:F( SEADGroupPrefixes ) - - if type( SEADGroupPrefixes ) == 'table' then - for SEADGroupPrefixID, SEADGroupPrefix in pairs( SEADGroupPrefixes ) do - self.SEADGroupPrefixes[SEADGroupPrefix] = SEADGroupPrefix - end - else - self.SEADGroupPrefixes[SEADGroupPrefixes] = SEADGroupPrefixes - end - - self:HandleEvent( EVENTS.Shot ) - self:I("*** SEAD - Started Version 0.2.7") - return self + local self = BASE:Inherit( self, BASE:New() ) + self:F( SEADGroupPrefixes ) + + if type( SEADGroupPrefixes ) == 'table' then + for SEADGroupPrefixID, SEADGroupPrefix in pairs( SEADGroupPrefixes ) do + self.SEADGroupPrefixes[SEADGroupPrefix] = SEADGroupPrefix + end + else + self.SEADGroupPrefixes[SEADGroupPrefixes] = SEADGroupPrefixes + end + + self:HandleEvent( EVENTS.Shot, self.HandleEventShot ) + self:I("*** SEAD - Started Version 0.2.8") + return self end --- Update the active SEAD Set @@ -120,7 +103,7 @@ end function SEAD:UpdateSet( SEADGroupPrefixes ) self:F( SEADGroupPrefixes ) - + if type( SEADGroupPrefixes ) == 'table' then for SEADGroupPrefixID, SEADGroupPrefix in pairs( SEADGroupPrefixes ) do self.SEADGroupPrefixes[SEADGroupPrefix] = SEADGroupPrefix @@ -164,112 +147,83 @@ end -- @see SEAD -- @param #SEAD -- @param Core.Event#EVENTDATA EventData -function SEAD:OnEventShot( EventData ) - self:T( { EventData } ) +function SEAD:HandleEventShot( EventData ) + self:T( { EventData } ) - local SEADUnit = EventData.IniDCSUnit - local SEADUnitName = EventData.IniDCSUnitName - local SEADWeapon = EventData.Weapon -- Identify the weapon fired - local SEADWeaponName = EventData.WeaponName -- return weapon type + local SEADUnit = EventData.IniDCSUnit + local SEADUnitName = EventData.IniDCSUnitName + local SEADWeapon = EventData.Weapon -- Identify the weapon fired + local SEADWeaponName = EventData.WeaponName -- return weapon type - self:T( "*** SEAD - Missile Launched = " .. SEADWeaponName) - self:T({ SEADWeapon }) - - --[[check for SEAD missiles - if SEADWeaponName == "weapons.missiles.X_58" --Kh-58U anti-radiation missiles fired - or - SEADWeaponName == "weapons.missiles.Kh25MP_PRGS1VP" --Kh-25MP anti-radiation missiles fired - or - SEADWeaponName == "weapons.missiles.X_25MP" --Kh-25MPU anti-radiation missiles fired - or - SEADWeaponName == "weapons.missiles.X_28" --Kh-28 anti-radiation missiles fired - or - SEADWeaponName == "weapons.missiles.X_31P" --Kh-31P anti-radiation missiles fired - or - SEADWeaponName == "weapons.missiles.AGM_45A" --AGM-45A anti-radiation missiles fired - or - SEADWeaponName == "weapons.missiles.AGM_45" --AGM-45B anti-radiation missiles fired - or - SEADWeaponName == "weapons.missiles.AGM_88" --AGM-88C anti-radiation missiles fired - or - SEADWeaponName == "weapons.missiles.AGM_122" --AGM-122 Sidearm anti-radiation missiles fired - or - SEADWeaponName == "weapons.missiles.LD-10" --LD-10 anti-radiation missiles fired - or - SEADWeaponName == "weapons.missiles.ALARM" --ALARM anti-radiation missiles fired - or - SEADWeaponName == "weapons.missiles.AGM_84E" --AGM84 anti-radiation missiles fired - or - SEADWeaponName == "weapons.missiles.AGM_84A" --AGM84 anti-radiation missiles fired - or - SEADWeaponName == "weapons.missiles.AGM_84H" --AGM84 anti-radiation missiles fired - --]] + self:T( "*** SEAD - Missile Launched = " .. SEADWeaponName) + self:T({ SEADWeapon }) + if self:_CheckHarms(SEADWeaponName) then local _targetskill = "Random" local _targetMimgroupName = "none" - local _evade = math.random (1,100) -- random number for chance of evading action - local _targetMim = EventData.Weapon:getTarget() -- Identify target - local _targetUnit = UNIT:Find(_targetMim) -- Unit name by DCS Object - if _targetUnit and _targetUnit:IsAlive() then - local _targetMimgroup = _targetUnit:GetGroup() - local _targetMimgroupName = _targetMimgroup:GetName() -- group name - --local _targetskill = _DATABASE.Templates.Units[_targetUnit].Template.skill - self:T( self.SEADGroupPrefixes ) - self:T( _targetMimgroupName ) - end - -- see if we are shot at - local SEADGroupFound = false - for SEADGroupPrefixID, SEADGroupPrefix in pairs( self.SEADGroupPrefixes ) do - if string.find( _targetMimgroupName, SEADGroupPrefix, 1, true ) then - SEADGroupFound = true - self:T( '*** SEAD - Group Found' ) - break - end - end - if SEADGroupFound == true then -- yes we are being attacked - if _targetskill == "Random" then -- when skill is random, choose a skill - local Skills = { "Average", "Good", "High", "Excellent" } - _targetskill = Skills[ math.random(1,4) ] - end - self:T( _targetskill ) - if self.TargetSkill[_targetskill] then - if (_evade > self.TargetSkill[_targetskill].Evade) then + local _evade = math.random (1,100) -- random number for chance of evading action + local _targetMim = EventData.Weapon:getTarget() -- Identify target + local _targetUnit = UNIT:Find(_targetMim) -- Unit name by DCS Object + if _targetUnit and _targetUnit:IsAlive() then + local _targetMimgroup = _targetUnit:GetGroup() + local _targetMimgroupName = _targetMimgroup:GetName() -- group name + --local _targetskill = _DATABASE.Templates.Units[_targetUnit].Template.skill + self:T( self.SEADGroupPrefixes ) + self:T( _targetMimgroupName ) + end + -- see if we are shot at + local SEADGroupFound = false + for SEADGroupPrefixID, SEADGroupPrefix in pairs( self.SEADGroupPrefixes ) do + if string.find( _targetMimgroupName, SEADGroupPrefix, 1, true ) then + SEADGroupFound = true + self:T( '*** SEAD - Group Found' ) + break + end + end + if SEADGroupFound == true then -- yes we are being attacked + if _targetskill == "Random" then -- when skill is random, choose a skill + local Skills = { "Average", "Good", "High", "Excellent" } + _targetskill = Skills[ math.random(1,4) ] + end + self:T( _targetskill ) + if self.TargetSkill[_targetskill] then + if (_evade > self.TargetSkill[_targetskill].Evade) then + + self:T( string.format("*** SEAD - Evading, target skill " ..string.format(_targetskill)) ) + + local _targetMimgroup = Unit.getGroup(Weapon.getTarget(SEADWeapon)) + local _targetMimcont= _targetMimgroup:getController() + + routines.groupRandomDistSelf(_targetMimgroup,300,'Diamond',250,20) -- move randomly + + --tracker ID table to switch groups off and on again + local id = { + groupName = _targetMimgroup, + ctrl = _targetMimcont + } - self:T( string.format("*** SEAD - Evading, target skill " ..string.format(_targetskill)) ) - - local _targetMimgroup = Unit.getGroup(Weapon.getTarget(SEADWeapon)) - local _targetMimcont= _targetMimgroup:getController() - - routines.groupRandomDistSelf(_targetMimgroup,300,'Diamond',250,20) -- move randomly - - --tracker ID table to switch groups off and on again - local id = { - groupName = _targetMimgroup, - ctrl = _targetMimcont - } - - local function SuppressionEnd(id) --switch group back on - local range = self.EngagementRange -- Feature Request #1355 - self:T(string.format("*** SEAD - Engagement Range is %d", range)) - id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.RED) - --id.groupName:enableEmission(true) - id.ctrl:setOption(AI.Option.Ground.id.AC_ENGAGEMENT_RANGE_RESTRICTION,range) --Feature Request #1355 - self.SuppressedGroups[id.groupName] = nil --delete group id from table when done - end - -- randomize switch-on time - local delay = math.random(self.TargetSkill[_targetskill].DelayOn[1], self.TargetSkill[_targetskill].DelayOn[2]) - local SuppressionEndTime = timer.getTime() + delay - --create entry - if self.SuppressedGroups[id.groupName] == nil then --no timer entry for this group yet - self.SuppressedGroups[id.groupName] = { - SuppressionEndTime = delay - } - Controller.setOption(_targetMimcont, AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) - --_targetMimgroup:enableEmission(false) - timer.scheduleFunction(SuppressionEnd, id, SuppressionEndTime) --Schedule the SuppressionEnd() function - end - end - end - end - end + local function SuppressionEnd(id) --switch group back on + local range = self.EngagementRange -- Feature Request #1355 + self:T(string.format("*** SEAD - Engagement Range is %d", range)) + id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.RED) + --id.groupName:enableEmission(true) + id.ctrl:setOption(AI.Option.Ground.id.AC_ENGAGEMENT_RANGE_RESTRICTION,range) --Feature Request #1355 + self.SuppressedGroups[id.groupName] = nil --delete group id from table when done + end + -- randomize switch-on time + local delay = math.random(self.TargetSkill[_targetskill].DelayOn[1], self.TargetSkill[_targetskill].DelayOn[2]) + local SuppressionEndTime = timer.getTime() + delay + --create entry + if self.SuppressedGroups[id.groupName] == nil then --no timer entry for this group yet + self.SuppressedGroups[id.groupName] = { + SuppressionEndTime = delay + } + Controller.setOption(_targetMimcont, AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) + --_targetMimgroup:enableEmission(false) + timer.scheduleFunction(SuppressionEnd, id, SuppressionEndTime) --Schedule the SuppressionEnd() function + end + end + end + end + end end diff --git a/Moose Development/Moose/Functional/Shorad.lua b/Moose Development/Moose/Functional/Shorad.lua index 82d8a1482..958877ec8 100644 --- a/Moose Development/Moose/Functional/Shorad.lua +++ b/Moose Development/Moose/Functional/Shorad.lua @@ -1,24 +1,24 @@ --- **Functional** -- Short Range Air Defense System --- +-- -- === --- +-- -- **SHORAD** - Short Range Air Defense System -- Controls a network of short range air/missile defense groups. --- +-- -- === --- +-- -- ## Missions: -- -- ### [SHORAD - Short Range Air Defense](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/SRD%20-%20SHORAD%20Defense) --- +-- -- === --- +-- -- ### Author : **applevangelist ** --- +-- -- @module Functional.Shorad -- @image Functional.Shorad.jpg -- --- Date: May 2021 +-- Date: July 2021 ------------------------------------------------------------------------- --- **SHORAD** class, extends Core.Base#BASE @@ -26,7 +26,7 @@ -- @field #string ClassName -- @field #string name Name of this Shorad -- @field #boolean debug Set the debug state --- @field #string Prefixes String to be used to build the @{#Core.Set#SET_GROUP} +-- @field #string Prefixes String to be used to build the @{#Core.Set#SET_GROUP} -- @field #number Radius Shorad defense radius in meters -- @field Core.Set#SET_GROUP Groupset The set of Shorad groups -- @field Core.Set#SET_GROUP Samset The set of SAM groups to defend @@ -41,10 +41,10 @@ -- @field #boolean UseEmOnOff Decide if we are using Emission on/off (default) or AlarmState red/green. -- @extends Core.Base#BASE ---- *Good friends are worth defending.* Mr Tushman, Wonder (the Movie) --- +--- *Good friends are worth defending.* Mr Tushman, Wonder (the Movie) +-- -- Simple Class for a more intelligent Short Range Air Defense System --- +-- -- #SHORAD -- Moose derived missile intercepting short range defense system. -- Protects a network of SAM sites. Uses events to switch on the defense groups closest to the enemy. @@ -52,26 +52,26 @@ -- -- ## Usage -- --- Set up a #SET_GROUP for the SAM sites to be protected: --- --- `local SamSet = SET_GROUP:New():FilterPrefixes("Red SAM"):FilterCoalitions("red"):FilterStart()` --- +-- Set up a #SET_GROUP for the SAM sites to be protected: +-- +-- `local SamSet = SET_GROUP:New():FilterPrefixes("Red SAM"):FilterCoalitions("red"):FilterStart()` +-- -- By default, SHORAD will defense against both HARMs and AG-Missiles with short to medium range. The default defense probability is 70-90%. --- When a missile is detected, SHORAD will activate defense groups in the given radius around the target for 10 minutes. It will *not* react to friendly fire. --- +-- When a missile is detected, SHORAD will activate defense groups in the given radius around the target for 10 minutes. It will *not* react to friendly fire. +-- -- ### Start a new SHORAD system, parameters are: +-- +-- * Name: Name of this SHORAD. +-- * ShoradPrefix: Filter for the Shorad #SET_GROUP. +-- * Samset: The #SET_GROUP of SAM sites to defend. +-- * Radius: Defense radius in meters. +-- * ActiveTimer: Determines how many seconds the systems stay on red alert after wake-up call. +-- * Coalition: Coalition, i.e. "blue", "red", or "neutral".* +-- +-- `myshorad = SHORAD:New("RedShorad", "Red SHORAD", SamSet, 25000, 600, "red")` -- --- * Name: Name of this SHORAD. --- * ShoradPrefix: Filter for the Shorad #SET_GROUP. --- * Samset: The #SET_GROUP of SAM sites to defend. --- * Radius: Defense radius in meters. --- * ActiveTimer: Determines how many seconds the systems stay on red alert after wake-up call. --- * Coalition: Coalition, i.e. "blue", "red", or "neutral".* --- --- `myshorad = SHORAD:New("RedShorad", "Red SHORAD", SamSet, 25000, 600, "red")` --- --- ## Customize options --- +-- ## Customize options +-- -- * SHORAD:SwitchDebug(debug) -- * SHORAD:SwitchHARMDefense(onoff) -- * SHORAD:SwitchAGMDefense(onoff) @@ -96,7 +96,7 @@ SHORAD = { DefendMavs = true, DefenseLowProb = 70, DefenseHighProb = 90, - UseEmOnOff = false, + UseEmOnOff = false, } ----------------------------------------------------------------------- @@ -108,22 +108,6 @@ do --- Missile enumerators -- @field Harms SHORAD.Harms = { - --[[ - ["X58"] = "weapons.missiles.X_58", --Kh-58X anti-radiation missiles fired - ["Kh25"] = "weapons.missiles.Kh25MP_PRGS1VP", --Kh-25MP anti-radiation missiles fired - ["X25"] = "weapons.missiles.X_25MP", --Kh-25MPU anti-radiation missiles fired - ["X28"] = "weapons.missiles.X_28", --Kh-28 anti-radiation missiles fired - ["X31"] = "weapons.missiles.X_31P", --Kh-31P anti-radiation missiles fired - ["AGM45A"] = "weapons.missiles.AGM_45A", --AGM-45A anti-radiation missiles fired - ["AGM45"] = "weapons.missiles.AGM_45", --AGM-45B anti-radiation missiles fired - ["AGM88"] = "weapons.missiles.AGM_88", --AGM-88C anti-radiation missiles fired - ["AGM122"] = "weapons.missiles.AGM_122", --AGM-122 Sidearm anti-radiation missiles fired - ["LD10"] = "weapons.missiles.LD-10", --LD-10 anti-radiation missiles fired - ["ALARM"] = "weapons.missiles.ALARM", --ALARM anti-radiation missiles fired - ["AGM84E"] = "weapons.missiles.AGM_84E", --AGM84 anti-radiation missiles fired - ["AGM84A"] = "weapons.missiles.AGM_84A", --AGM84 anti-radiation missiles fired - ["AGM84H"] = "weapons.missiles.AGM_84H", --AGM84 anti-radiation missiles fired - --]] ["AGM_88"] = "AGM_88", ["AGM_45"] = "AGM_45", ["AGM_122"] = "AGM_122", @@ -137,7 +121,7 @@ do ["X_31"] = "X_31", ["Kh25"] = "Kh25", } - + --- TODO complete list? -- @field Mavs SHORAD.Mavs = { @@ -148,7 +132,7 @@ do ["Kh31"] = "Kh31", ["Kh66"] = "Kh66", } - + --- Instantiates a new SHORAD object -- @param #SHORAD self -- @param #string Name Name of this SHORAD @@ -157,10 +141,12 @@ do -- @param #number Radius Defense radius in meters, used to switch on groups -- @param #number ActiveTimer Determines how many seconds the systems stay on red alert after wake-up call -- @param #string Coalition Coalition, i.e. "blue", "red", or "neutral" - function SHORAD:New(Name, ShoradPrefix, Samset, Radius, ActiveTimer, Coalition) + -- @param #boolean UseEmOnOff Use Emissions On/Off rather than Alarm State Red/Green (default: use Emissions switch) + -- @retunr #SHORAD self + function SHORAD:New(Name, ShoradPrefix, Samset, Radius, ActiveTimer, Coalition, UseEmOnOff) local self = BASE:Inherit( self, BASE:New() ) self:T({Name, ShoradPrefix, Samset, Radius, ActiveTimer, Coalition}) - + local GroupSet = SET_GROUP:New():FilterPrefixes(ShoradPrefix):FilterCoalitions(Coalition):FilterCategoryGround():FilterStart() self.name = Name or "MyShorad" @@ -171,22 +157,23 @@ do self.ActiveTimer = ActiveTimer or 600 self.ActiveGroups = {} self.Groupset = GroupSet - self:HandleEvent( EVENTS.Shot ) self.DefendHarms = true self.DefendMavs = true self.DefenseLowProb = 70 -- probability to detect a missile shot, low margin self.DefenseHighProb = 90 -- probability to detect a missile shot, high margin - self.UseEmOnOff = true -- Decide if we are using Emission on/off (default) or AlarmState red/green - self:I("*** SHORAD - Started Version 0.2.5") + self.UseEmOnOff = UseEmOnOff or false -- Decide if we are using Emission on/off (default) or AlarmState red/green + self:I("*** SHORAD - Started Version 0.2.8") -- Set the string id for output to DCS.log file. self.lid=string.format("SHORAD %s | ", self.name) self:_InitState() + self:HandleEvent(EVENTS.Shot, self.HandleEventShot) return self end - + --- Initially set all groups to alarm state GREEN -- @param #SHORAD self function SHORAD:_InitState() + self:T(self.lid .. " _InitState") local table = {} local set = self.Groupset self:T({set = set}) @@ -195,33 +182,50 @@ do if self.UseEmOnOff then --_group:SetAIOff() _group:EnableEmission(false) + _group:OptionAlarmStateRed() --Wrapper.Group#GROUP else _group:OptionAlarmStateGreen() --Wrapper.Group#GROUP end + _group:OptionDisperseOnAttack(30) end -- gather entropy - for i=1,10 do + for i=1,100 do math.random() end + return self end - - --- Switch debug state + + --- Switch debug state on -- @param #SHORAD self -- @param #boolean debug Switch debug on (true) or off (false) - function SHORAD:SwitchDebug(debug) - self:T( { debug } ) - local onoff = debug or false - if debug then - self.debug = true - --tracing - BASE:TraceOn() - BASE:TraceClass("SHORAD") + function SHORAD:SwitchDebug(onoff) + self:T( { onoff } ) + if onoff then + self:SwitchDebugOn() else - self.debug = false - BASE:TraceOff() + self.SwitchDebugOff() end + return self end - + + --- Switch debug state on + -- @param #SHORAD self + function SHORAD:SwitchDebugOn() + self.debug = true + --tracing + BASE:TraceOn() + BASE:TraceClass("SHORAD") + return self + end + + --- Switch debug state off + -- @param #SHORAD self + function SHORAD:SwitchDebugOff() + self.debug = false + BASE:TraceOff() + return self + end + --- Switch defense for HARMs -- @param #SHORAD self -- @param #boolean onoff @@ -229,8 +233,9 @@ do self:T( { onoff } ) local onoff = onoff or true self.DefendHarms = onoff + return self end - + --- Switch defense for AGMs -- @param #SHORAD self -- @param #boolean onoff @@ -238,8 +243,9 @@ do self:T( { onoff } ) local onoff = onoff or true self.DefendMavs = onoff + return self end - + --- Set defense probability limits -- @param #SHORAD self -- @param #number low Minimum detection limit, integer 1-100 @@ -256,42 +262,50 @@ do end self.DefenseLowProb = low self.DefenseHighProb = high + return self end - + --- Set the number of seconds a SHORAD site will stay active -- @param #SHORAD self -- @param #number seconds Number of seconds systems stay active function SHORAD:SetActiveTimer(seconds) + self:T(self.lid .. " SetActiveTimer") local timer = seconds or 600 if timer < 0 then timer = 600 end self.ActiveTimer = timer + return self end --- Set the number of meters for the SHORAD defense zone -- @param #SHORAD self - -- @param #number meters Radius of the defense search zone in meters. #SHORADs in this range around a targeted group will go active + -- @param #number meters Radius of the defense search zone in meters. #SHORADs in this range around a targeted group will go active function SHORAD:SetDefenseRadius(meters) + self:T(self.lid .. " SetDefenseRadius") local radius = meters or 20000 if radius < 0 then radius = 20000 end self.Radius = radius + return self end - + --- Set using Emission on/off instead of changing alarm state -- @param #SHORAD self -- @param #boolean switch Decide if we are changing alarm state or AI state function SHORAD:SetUsingEmOnOff(switch) + self:T(self.lid .. " SetUsingEmOnOff") self.UseEmOnOff = switch or false + return self end - + --- Check if a HARM was fired -- @param #SHORAD self -- @param #string WeaponName -- @return #boolean Returns true for a match function SHORAD:_CheckHarms(WeaponName) + self:T(self.lid .. " _CheckHarms") self:T( { WeaponName } ) local hit = false if self.DefendHarms then @@ -301,12 +315,13 @@ do end return hit end - + --- Check if an AGM was fired -- @param #SHORAD self -- @param #string WeaponName -- @return #boolean Returns true for a match function SHORAD:_CheckMavs(WeaponName) + self:T(self.lid .. " _CheckMavs") self:T( { WeaponName } ) local hit = false if self.DefendMavs then @@ -316,15 +331,16 @@ do end return hit end - + --- Check the coalition of the attacker -- @param #SHORAD self -- @param #string Coalition name -- @return #boolean Returns false for a match function SHORAD:_CheckCoalition(Coalition) + self:T(self.lid .. " _CheckCoalition") local owncoalition = self.Coalition local othercoalition = "" - if Coalition == 0 then + if Coalition == 0 then othercoalition = "neutral" elseif Coalition == 1 then othercoalition = "red" @@ -338,12 +354,13 @@ do return false end end - + --- Check if the missile is aimed at a SHORAD -- @param #SHORAD self -- @param #string TargetGroupName Name of the target group -- @return #boolean Returns true for a match, else false function SHORAD:_CheckShotAtShorad(TargetGroupName) + self:T(self.lid .. " _CheckShotAtShorad") local tgtgrp = TargetGroupName local shorad = self.Groupset local shoradset = shorad:GetAliveSet() --#table @@ -352,17 +369,18 @@ do local groupname = _groups:GetName() if string.find(groupname, tgtgrp, 1) then returnname = true - _groups:RelocateGroundRandomInRadius(7,100,false,false) -- be a bit evasive + --_groups:RelocateGroundRandomInRadius(7,100,false,false) -- be a bit evasive end end - return returnname + return returnname end - + --- Check if the missile is aimed at a SAM site -- @param #SHORAD self -- @param #string TargetGroupName Name of the target group -- @return #boolean Returns true for a match, else false function SHORAD:_CheckShotAtSams(TargetGroupName) + self:T(self.lid .. " _CheckShotAtSams") local tgtgrp = TargetGroupName local shorad = self.Samset --local shoradset = shorad:GetAliveSet() --#table @@ -376,11 +394,12 @@ do end return returnname end - + --- Calculate if the missile shot is detected -- @param #SHORAD self -- @return #boolean Returns true for a detection, else false function SHORAD:_ShotIsDetected() + self:T(self.lid .. " _ShotIsDetected") local IsDetected = false local DetectionProb = math.random(self.DefenseLowProb, self.DefenseHighProb) -- reference value local ActualDetection = math.random(1,100) -- value for this shot @@ -389,15 +408,15 @@ do end return IsDetected end - + --- Wake up #SHORADs in a zone with diameter Radius for ActiveTimer seconds -- @param #SHORAD self -- @param #string TargetGroup Name of the target group used to build the #ZONE -- @param #number Radius Radius of the #ZONE -- @param #number ActiveTimer Number of seconds to stay active -- @param #number TargetCat (optional) Category, i.e. Object.Category.UNIT or Object.Category.STATIC - -- @usage Use this function to integrate with other systems, example - -- + -- @usage Use this function to integrate with other systems, example + -- -- local SamSet = SET_GROUP:New():FilterPrefixes("Blue SAM"):FilterCoalitions("blue"):FilterStart() -- myshorad = SHORAD:New("BlueShorad", "Blue SHORAD", SamSet, 22000, 600, "blue") -- myshorad:SwitchDebug(true) @@ -405,6 +424,7 @@ do -- mymantis:AddShorad(myshorad,720) -- mymantis:Start() function SHORAD:WakeUpShorad(TargetGroup, Radius, ActiveTimer, TargetCat) + self:T(self.lid .. " WakeUpShorad") self:T({TargetGroup, Radius, ActiveTimer, TargetCat}) local targetcat = TargetCat or Object.Category.UNIT local targetgroup = TargetGroup @@ -442,7 +462,7 @@ do self:T(text) local m = MESSAGE:New(text,10,"SHORAD"):ToAllIf(self.debug) if self.UseEmOnOff then - _group:SetAIOn() + --_group:SetAIOn() _group:EnableEmission(true) end _group:OptionAlarmStateRed() @@ -454,14 +474,15 @@ do end end end + return self end - + --- Main function - work on the EventData -- @param #SHORAD self -- @param Core.Event#EVENTDATA EventData The event details table data set - function SHORAD:OnEventShot( EventData ) + function SHORAD:HandleEventShot( EventData ) self:T( { EventData } ) - + self:T(self.lid .. " HandleEventShot") --local ShootingUnit = EventData.IniDCSUnit --local ShootingUnitName = EventData.IniDCSUnitName local ShootingWeapon = EventData.Weapon -- Identify the weapon fired @@ -473,7 +494,7 @@ do local IsDetected = self:_ShotIsDetected() -- convert to text local DetectedText = "false" - if IsDetected then + if IsDetected then DetectedText = "true" end local text = string.format("%s Missile Launched = %s | Detected probability state is %s", self.lid, ShootingWeaponName, DetectedText) @@ -490,7 +511,7 @@ do targetunit = UNIT:Find(targetdata) elseif targetcat == Object.Category.STATIC then -- STATIC targetunit = STATIC:Find(targetdata) - end + end --local targetunitname = Unit.getName(targetdata) -- Unit name if targetunit and targetunit:IsAlive() then local targetunitname = targetunit:GetName() @@ -507,7 +528,7 @@ do local text = string.format("%s Missile Target = %s", self.lid, tostring(targetgroupname)) self:T( text ) local m = MESSAGE:New(text,10,"Info"):ToAllIf(self.debug) - -- check if we or a SAM site are the target + -- check if we or a SAM site are the target --local TargetGroup = EventData.TgtGroup -- Wrapper.Group#GROUP local shotatus = self:_CheckShotAtShorad(targetgroupname) --#boolean local shotatsams = self:_CheckShotAtSams(targetgroupname) --#boolean @@ -516,12 +537,12 @@ do self:T({shotatsams=shotatsams,shotatus=shotatus}) self:WakeUpShorad(targetgroupname, self.Radius, self.ActiveTimer, targetcat) end - end + end end end - end + end -- end ----------------------------------------------------------------------- -- SHORAD end ------------------------------------------------------------------------ +----------------------------------------------------------------------- \ No newline at end of file diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 0d4f074c4..1d4e76d2e 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -5790,21 +5790,32 @@ end -- @param #WAREHOUSE.Queueitem request Request belonging to this asset. Needed for the name/alias. -- @param #table parking Parking data for this asset. -- @param #boolean uncontrolled Spawn aircraft in uncontrolled state. --- @param #boolean hotstart Spawn aircraft with engines already on. Default is a cold start with engines off. -- @return Wrapper.Group#GROUP The spawned group or nil if the group could not be spawned. -function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrolled, hotstart) +function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrolled) if asset and asset.category==Group.Category.AIRPLANE or asset.category==Group.Category.HELICOPTER then -- Prepare the spawn template. local template=self:_SpawnAssetPrepareTemplate(asset, alias) + -- Cold start (default). + local _type=COORDINATE.WaypointType.TakeOffParking + local _action=COORDINATE.WaypointAction.FromParkingArea + + -- Hot start. + if asset.takeoffType and asset.takeoffType==COORDINATE.WaypointType.TakeOffParkingHot then + _type=COORDINATE.WaypointType.TakeOffParkingHot + _action=COORDINATE.WaypointAction.FromParkingAreaHot + uncontrolled=false + end + + -- Set route points. if request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then -- Get flight path if the group goes to another warehouse by itself. if request.toself then - local wp=self.airbase:GetCoordinate():WaypointAir("RADIO", COORDINATE.WaypointType.TakeOffParking, COORDINATE.WaypointAction.FromParkingArea, 0, false, self.airbase, {}, "Parking") + local wp=self.airbase:GetCoordinate():WaypointAir("RADIO", _type, _action, 0, false, self.airbase, {}, "Parking") template.route.points={wp} else template.route.points=self:_GetFlightplan(asset, self.airbase, request.warehouse.airbase) @@ -5812,18 +5823,8 @@ function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrol else - -- Cold start (default). - local _type=COORDINATE.WaypointType.TakeOffParking - local _action=COORDINATE.WaypointAction.FromParkingArea - - -- Hot start. - if hotstart then - _type=COORDINATE.WaypointType.TakeOffParkingHot - _action=COORDINATE.WaypointAction.FromParkingAreaHot - end - -- First route point is the warehouse airbase. - template.route.points[1]=self.airbase:GetCoordinate():WaypointAir("BARO",_type,_action, 0, true, self.airbase, nil, "Spawnpoint") + template.route.points[1]=self.airbase:GetCoordinate():WaypointAir("BARO", _type, _action, 0, true, self.airbase, nil, "Spawnpoint") end @@ -9040,9 +9041,23 @@ function WAREHOUSE:_GetFlightplan(asset, departure, destination) local wp={} local c={} + -- Cold start (default). + local _type=COORDINATE.WaypointType.TakeOffParking + local _action=COORDINATE.WaypointAction.FromParkingArea + + -- Hot start. + if asset.takeoffType and asset.takeoffType==COORDINATE.WaypointType.TakeOffParkingHot then + env.info("FF hot") + _type=COORDINATE.WaypointType.TakeOffParkingHot + _action=COORDINATE.WaypointAction.FromParkingAreaHot + else + env.info("FF cold") + end + + --- Departure/Take-off c[#c+1]=Pdeparture - wp[#wp+1]=Pdeparture:WaypointAir("RADIO", COORDINATE.WaypointType.TakeOffParking, COORDINATE.WaypointAction.FromParkingArea, VxClimb*3.6, true, departure, nil, "Departure") + wp[#wp+1]=Pdeparture:WaypointAir("RADIO", _type, _action, VxClimb*3.6, true, departure, nil, "Departure") --- Begin of Cruise local Pcruise=Pdeparture:Translate(d_climb, heading) diff --git a/Moose Development/Moose/Globals.lua b/Moose Development/Moose/Globals.lua index d972cf38b..f63f57e84 100644 --- a/Moose Development/Moose/Globals.lua +++ b/Moose Development/Moose/Globals.lua @@ -1,5 +1,4 @@ --- The order of the declarations is important here. Don't touch it. - +--- GLOBALS: The order of the declarations is important here. Don't touch it. --- Declare the event dispatcher based on the EVENT class _EVENTDISPATCHER = EVENT:New() -- Core.Event#EVENT @@ -10,10 +9,38 @@ _SCHEDULEDISPATCHER = SCHEDULEDISPATCHER:New() -- Core.ScheduleDispatcher#SCHEDU --- Declare the main database object, which is used internally by the MOOSE classes. _DATABASE = DATABASE:New() -- Core.Database#DATABASE +--- Settings _SETTINGS = SETTINGS:Set() _SETTINGS:SetPlayerMenuOn() +--- Register cargos. _DATABASE:_RegisterCargos() + +--- Register zones. _DATABASE:_RegisterZones() _DATABASE:_RegisterAirbases() +--- Check if os etc is available. +BASE:I("Checking de-sanitization of os, io and lfs:") +local __na=false +if os then + BASE:I("- os available") +else + BASE:I("- os NOT available! Some functions may not work.") + __na=true +end +if io then + BASE:I("- io available") +else + BASE:I("- io NOT available! Some functions may not work.") + __na=true +end +if lfs then + BASE:I("- lfs available") +else + BASE:I("- lfs NOT available! Some functions may not work.") + __na=true +end +if __na then + BASE:I("Check /Scripts/MissionScripting.lua and comment out the lines with sanitizeModule(''). Use at your own risk!)") +end diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index 9c84d7f15..7ad1b1829 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -2,10 +2,12 @@ __Moose.Include( 'Scripts/Moose/Utilities/Enums.lua' ) __Moose.Include( 'Scripts/Moose/Utilities/Routines.lua' ) __Moose.Include( 'Scripts/Moose/Utilities/Utils.lua' ) __Moose.Include( 'Scripts/Moose/Utilities/Profiler.lua' ) +__Moose.Include( 'Scripts/Moose/Utilities/Templates.lua' ) +__Moose.Include( 'Scripts/Moose/Utilities/STTS.lua' ) __Moose.Include( 'Scripts/Moose/Core/Base.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Beacon.lua' ) __Moose.Include( 'Scripts/Moose/Core/UserFlag.lua' ) -__Moose.Include( 'Scripts/Moose/Core/UserSound.lua' ) __Moose.Include( 'Scripts/Moose/Core/Report.lua' ) __Moose.Include( 'Scripts/Moose/Core/Scheduler.lua' ) __Moose.Include( 'Scripts/Moose/Core/ScheduleDispatcher.lua' ) @@ -20,9 +22,6 @@ __Moose.Include( 'Scripts/Moose/Core/Point.lua' ) __Moose.Include( 'Scripts/Moose/Core/Velocity.lua' ) __Moose.Include( 'Scripts/Moose/Core/Message.lua' ) __Moose.Include( 'Scripts/Moose/Core/Fsm.lua' ) -__Moose.Include( 'Scripts/Moose/Core/Radio.lua' ) -__Moose.Include( 'Scripts/Moose/Core/RadioQueue.lua' ) -__Moose.Include( 'Scripts/Moose/Core/RadioSpeech.lua' ) __Moose.Include( 'Scripts/Moose/Core/Spawn.lua' ) __Moose.Include( 'Scripts/Moose/Core/SpawnStatic.lua' ) __Moose.Include( 'Scripts/Moose/Core/Timer.lua' ) @@ -85,6 +84,8 @@ __Moose.Include( 'Scripts/Moose/Ops/ArmyGroup.lua' ) __Moose.Include( 'Scripts/Moose/Ops/Squadron.lua' ) __Moose.Include( 'Scripts/Moose/Ops/AirWing.lua' ) __Moose.Include( 'Scripts/Moose/Ops/Intelligence.lua' ) +__Moose.Include( 'Scripts/Moose/Ops/CSAR.lua' ) +__Moose.Include( 'Scripts/Moose/Ops/CTLD.lua' ) __Moose.Include( 'Scripts/Moose/AI/AI_Balancer.lua' ) __Moose.Include( 'Scripts/Moose/AI/AI_Air.lua' ) @@ -123,6 +124,13 @@ __Moose.Include( 'Scripts/Moose/Actions/Act_Route.lua' ) __Moose.Include( 'Scripts/Moose/Actions/Act_Account.lua' ) __Moose.Include( 'Scripts/Moose/Actions/Act_Assist.lua' ) +__Moose.Include( 'Scripts/Moose/Sound/UserSound.lua' ) +__Moose.Include( 'Scripts/Moose/Sound/SoundOutput.lua' ) +__Moose.Include( 'Scripts/Moose/Sound/Radio.lua' ) +__Moose.Include( 'Scripts/Moose/Sound/RadioQueue.lua' ) +__Moose.Include( 'Scripts/Moose/Sound/RadioSpeech.lua' ) +__Moose.Include( 'Scripts/Moose/Sound/SRS.lua' ) + __Moose.Include( 'Scripts/Moose/Tasking/CommandCenter.lua' ) __Moose.Include( 'Scripts/Moose/Tasking/Mission.lua' ) __Moose.Include( 'Scripts/Moose/Tasking/Task.lua' ) diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index 91333138d..f865f98be 100644 --- a/Moose Development/Moose/Ops/ATIS.lua +++ b/Moose Development/Moose/Ops/ATIS.lua @@ -18,6 +18,7 @@ -- * Option to present information in imperial or metric units -- * Runway length and airfield elevation (optional) -- * Frequencies/channels of nav aids (ILS, VOR, NDB, TACAN, PRMG, RSBN) (optional) +-- * SRS Simple-Text-To-Speech (STTS) integration (no sound files necessary) -- -- === -- @@ -34,7 +35,7 @@ -- -- === -- --- ## Sound files: Check out the pinned messages in the Moose discord #ops-atis channel. +-- ## Sound files: [MOOSE Sound Files](https://github.com/FlightControl-Master/MOOSE_SOUND/releases) -- -- === -- @@ -59,7 +60,7 @@ -- @field #number frequency Radio frequency in MHz. -- @field #number modulation Radio modulation 0=AM or 1=FM. -- @field #number power Radio power in Watts. Default 100 W. --- @field Core.RadioQueue#RADIOQUEUE radioqueue Radio queue for broadcasing messages. +-- @field Sound.RadioQueue#RADIOQUEUE radioqueue Radio queue for broadcasing messages. -- @field #string soundpath Path to sound files. -- @field #string relayunitname Name of the radio relay unit. -- @field #table towerfrequency Table with tower frequencies. @@ -88,6 +89,9 @@ -- @field #boolean usemarker Use mark on the F10 map. -- @field #number markerid Numerical ID of the F10 map mark point. -- @field #number relHumidity Relative humidity (used to approximately calculate the dew point). +-- @field #boolean useSRS If true, use SRS for transmission. +-- @field Sound.SRS#MSRS msrs Moose SRS object. +-- @field #number dTQueueCheck Time interval to check the radio queue. Default 5 sec or 90 sec if SRS is used. -- @extends Core.Fsm#FSM --- *It is a very sad thing that nowadays there is so little useless information.* - Oscar Wilde @@ -252,6 +256,16 @@ -- # Marks on the F10 Map -- -- You can place marks on the F10 map via the @{#ATIS.SetMapMarks}() function. These will contain info about the ATIS frequency, the currently active runway and some basic info about the weather (wind, pressure and temperature). +-- +-- # Text-To-Speech +-- +-- You can enable text-to-speech ATIS information with the @{#ATIS.SetSRS}() function. This uses [SRS](http://dcssimpleradio.com/) (Version >= 1.9.6.0) for broadcasing. +-- Advantages are that **no sound files** or radio relay units are necessary. Also the issue that FC3 aircraft hear all transmissions will be circumvented. +-- +-- The @{#ATIS.SetSRS}() requires you to specify the path to the SRS install directory or more specifically the path to the DCS-SR-ExternalAudio.exe file. +-- +-- Unfortunately, it is not possible to determine the duration of the complete transmission. So once the transmission is finished, there might be some radio silence before +-- the next iteration begins. You can fine tune the time interval between transmissions with the @{#ATIS.SetQueueUpdateTime}() function. The default interval is 90 seconds. -- -- # Examples -- @@ -283,7 +297,14 @@ -- atisAbuDhabi:SetTowerFrequencies({250.5, 119.2}) -- atisAbuDhabi:SetVOR(114.25) -- atisAbuDhabi:Start() +-- +-- ## SRS +-- +-- atis=ATIS:New("Batumi", 305, radio.modulation.AM) +-- atis:SetSRS("D:\\DCS\\_SRS\\", "male", "en-US") +-- atis:Start() -- +-- This uses a male voice with US accent. It requires SRS to be installed in the `D:\DCS\_SRS\` directory. Not that backslashes need to be escaped or simply use slashes (as in linux). -- -- @field #ATIS ATIS = { @@ -368,6 +389,7 @@ ATIS.Alphabet = { -- @field #number PersianGulf +2° (East). -- @field #number TheChannel -10° (West). -- @field #number Syria +5° (East). +-- @field #number MarianaIslands +2° (East). ATIS.RunwayM2T={ Caucasus=0, Nevada=12, @@ -375,6 +397,7 @@ ATIS.RunwayM2T={ PersianGulf=2, TheChannel=-10, Syria=5, + MarianaIslands=2, } --- Whether ICAO phraseology is used for ATIS broadcasts. @@ -385,6 +408,7 @@ ATIS.RunwayM2T={ -- @field #boolean PersianGulf true. -- @field #boolean TheChannel true. -- @field #boolean Syria true. +-- @field #boolean MarianaIslands true. ATIS.ICAOPhraseology={ Caucasus=true, Nevada=false, @@ -392,6 +416,7 @@ ATIS.ICAOPhraseology={ PersianGulf=true, TheChannel=true, Syria=true, + MarianaIslands=true, } --- Nav point data. @@ -564,7 +589,7 @@ _ATIS={} --- ATIS class version. -- @field #string version -ATIS.version="0.9.1" +ATIS.version="0.9.6" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -628,6 +653,7 @@ function ATIS:New(airbasename, frequency, modulation) self:SetAltimeterQNH(true) self:SetMapMarks(false) self:SetRelativeHumidity() + self:SetQueueUpdateTime() -- Start State. self:SetStartState("Stopped") @@ -955,7 +981,9 @@ end -- * 170° on the Normany map -- * 182° on the Persian Gulf map -- --- Likewise, to convert *magnetic* into *true* heading, one has to substract easterly and add westerly variation. +-- Likewise, to convert *true* into *magnetic* heading, one has to substract easterly and add westerly variation. +-- +-- Or you make your life simple and just include the sign so you don't have to bother about East/West. -- -- @param #ATIS self -- @param #number magvar Magnetic variation in degrees. Positive for easterly and negative for westerly variation. Default is magnatic declinaton of the used map, c.f. @{Utilities.UTils#UTILS.GetMagneticDeclination}. @@ -1100,6 +1128,44 @@ function ATIS:MarkRunways(markall) end end +--- Use SRS Simple-Text-To-Speech for transmissions. No sound files necessary. +-- @param #ATIS self +-- @param #string PathToSRS Path to SRS directory. +-- @param #string Gender Gender: "male" or "female" (default). +-- @param #string Culture Culture, e.g. "en-GB" (default). +-- @param #string Voice Specific voice. Overrides `Gender` and `Culture`. +-- @param #number Port SRS port. Default 5002. +-- @return #ATIS self +function ATIS:SetSRS(PathToSRS, Gender, Culture, Voice, Port) + self.useSRS=true + self.msrs=MSRS:New(PathToSRS, self.frequency, self.modulation) + self.msrs:SetGender(Gender) + self.msrs:SetCulture(Culture) + self.msrs:SetVoice(Voice) + self.msrs:SetPort(Port) + self.msrs:SetCoalition(self:GetCoalition()) + if self.dTQueueCheck<=10 then + self:SetQueueUpdateTime(90) + end + return self +end + +--- Set the time interval between radio queue updates. +-- @param #ATIS self +-- @param #number TimeInterval Interval in seconds. Default 5 sec. +-- @return #ATIS self +function ATIS:SetQueueUpdateTime(TimeInterval) + self.dTQueueCheck=TimeInterval or 5 +end + +--- Get the coalition of the associated airbase. +-- @param #ATIS self +-- @return #number Coalition of the associcated airbase. +function ATIS:GetCoalition() + local coal=self.airbase and self.airbase:GetCoalition() or nil + return coal +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Start & Status ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1146,6 +1212,10 @@ function ATIS:onafterStart(From, Event, To) -- Start radio queue. self.radioqueue:Start(1, 0.1) + + -- Handle airbase capture + -- Handle events. + self:HandleEvent(EVENTS.BaseCaptured) -- Init status updates. self:__Status(-2) @@ -1171,7 +1241,13 @@ function ATIS:onafterStatus(From, Event, To) end -- Info text. - local text=string.format("State %s: Freq=%.3f MHz %s, Relay unit=%s (alive=%s)", fsmstate, self.frequency, UTILS.GetModulationName(self.modulation), tostring(self.relayunitname), relayunitstatus) + local text=string.format("State %s: Freq=%.3f MHz %s", fsmstate, self.frequency, UTILS.GetModulationName(self.modulation)) + if self.useSRS then + text=text..string.format(", SRS path=%s (%s), gender=%s, culture=%s, voice=%s", + tostring(self.msrs.path), tostring(self.msrs.port), tostring(self.msrs.gender), tostring(self.msrs.culture), tostring(self.msrs.voice)) + else + text=text..string.format(", Relay unit=%s (alive=%s)", tostring(self.relayunitname), relayunitstatus) + end self:I(self.lid..text) self:__Status(-60) @@ -1188,15 +1264,25 @@ end -- @param #string To To state. function ATIS:onafterCheckQueue(From, Event, To) - if #self.radioqueue.queue==0 then - self:T(self.lid..string.format("Radio queue empty. Repeating message.")) + if self.useSRS then + self:Broadcast() + else - self:T2(self.lid..string.format("Radio queue %d transmissions queued.", #self.radioqueue.queue)) + + if #self.radioqueue.queue==0 then + self:T(self.lid..string.format("Radio queue empty. Repeating message.")) + self:Broadcast() + else + self:T2(self.lid..string.format("Radio queue %d transmissions queued.", #self.radioqueue.queue)) + end + + + end - -- Check back in 5 seconds. - self:__CheckQueue(-5) + -- Check back in 5 seconds. + self:__CheckQueue(-math.abs(self.dTQueueCheck)) end --- Broadcast ATIS radio message. @@ -1322,11 +1408,14 @@ function ATIS:onafterBroadcast(From, Event, To) if time < 0 then time = 24*60*60 + time --avoid negative time around midnight - end + end local clock=UTILS.SecondsToClock(time) local zulu=UTILS.Split(clock, ":") local ZULU=string.format("%s%s", zulu[1], zulu[2]) + if self.useSRS then + ZULU=string.format("%s hours", zulu[1]) + end -- NATO time stamp. 0=Alfa, 1=Bravo, 2=Charlie, etc. @@ -1346,10 +1435,17 @@ function ATIS:onafterBroadcast(From, Event, To) local sunrise=coord:GetSunrise() sunrise=UTILS.Split(sunrise, ":") local SUNRISE=string.format("%s%s", sunrise[1], sunrise[2]) + if self.useSRS then + SUNRISE=string.format("%s %s hours", sunrise[1], sunrise[2]) + end local sunset=coord:GetSunset() sunset=UTILS.Split(sunset, ":") local SUNSET=string.format("%s%s", sunset[1], sunset[2]) + if self.useSRS then + SUNSET=string.format("%s %s hours", sunset[1], sunset[2]) + end + --------------------------------- --- Temperature and Dew Point --- @@ -1521,6 +1617,30 @@ function ATIS:onafterBroadcast(From, Event, To) -- Scattered 4 clouddens=4 elseif cloudspreset:find("RainyPreset") then + -- Overcast + Rain + clouddens=9 + if temperature>5 then + precepitation=1 -- rain + else + precepitation=3 -- snow + end + elseif cloudspreset:find("RainyPreset1") then + -- Overcast + Rain + clouddens=9 + if temperature>5 then + precepitation=1 -- rain + else + precepitation=3 -- snow + end + elseif cloudspreset:find("RainyPreset2") then + -- Overcast + Rain + clouddens=9 + if temperature>5 then + precepitation=1 -- rain + else + precepitation=3 -- snow + end + elseif cloudspreset:find("RainyPreset3") then -- Overcast + Rain clouddens=9 if temperature>5 then @@ -1591,36 +1711,46 @@ function ATIS:onafterBroadcast(From, Event, To) if self.airbasename:find("AFB")==nil and self.airbasename:find("Airport")==nil and self.airbasename:find("Airstrip")==nil and self.airbasename:find("airfield")==nil and self.airbasename:find("AB")==nil then subtitle=subtitle.." Airport" end - self.radioqueue:NewTransmission(string.format("%s/%s.ogg", self.theatre, self.airbasename), 3.0, self.soundpath, nil, nil, subtitle, self.subduration) + if not self.useSRS then + self.radioqueue:NewTransmission(string.format("%s/%s.ogg", self.theatre, self.airbasename), 3.0, self.soundpath, nil, nil, subtitle, self.subduration) + end local alltext=subtitle -- Information tag subtitle=string.format("Information %s", NATO) local _INFORMATION=subtitle - self:Transmission(ATIS.Sound.Information, 0.5, subtitle) - self.radioqueue:NewTransmission(string.format("NATO Alphabet/%s.ogg", NATO), 0.75, self.soundpath) + if not self.useSRS then + self:Transmission(ATIS.Sound.Information, 0.5, subtitle) + self.radioqueue:NewTransmission(string.format("NATO Alphabet/%s.ogg", NATO), 0.75, self.soundpath) + end alltext=alltext..";\n"..subtitle -- Zulu Time subtitle=string.format("%s Zulu", ZULU) - self.radioqueue:Number2Transmission(ZULU, nil, 0.5) - self:Transmission(ATIS.Sound.Zulu, 0.2, subtitle) + if not self.useSRS then + self.radioqueue:Number2Transmission(ZULU, nil, 0.5) + self:Transmission(ATIS.Sound.Zulu, 0.2, subtitle) + end alltext=alltext..";\n"..subtitle if not self.zulutimeonly then -- Sunrise Time subtitle=string.format("Sunrise at %s local time", SUNRISE) - self:Transmission(ATIS.Sound.SunriseAt, 0.5, subtitle) - self.radioqueue:Number2Transmission(SUNRISE, nil, 0.2) - self:Transmission(ATIS.Sound.TimeLocal, 0.2) + if not self.useSRS then + self:Transmission(ATIS.Sound.SunriseAt, 0.5, subtitle) + self.radioqueue:Number2Transmission(SUNRISE, nil, 0.2) + self:Transmission(ATIS.Sound.TimeLocal, 0.2) + end alltext=alltext..";\n"..subtitle -- Sunset Time subtitle=string.format("Sunset at %s local time", SUNSET) - self:Transmission(ATIS.Sound.SunsetAt, 0.5, subtitle) - self.radioqueue:Number2Transmission(SUNSET, nil, 0.5) - self:Transmission(ATIS.Sound.TimeLocal, 0.2) + if not self.useSRS then + self:Transmission(ATIS.Sound.SunsetAt, 0.5, subtitle) + self.radioqueue:Number2Transmission(SUNSET, nil, 0.5) + self:Transmission(ATIS.Sound.TimeLocal, 0.2) + end alltext=alltext..";\n"..subtitle end @@ -1634,17 +1764,19 @@ function ATIS:onafterBroadcast(From, Event, To) subtitle=subtitle..", gusting" end local _WIND=subtitle - self:Transmission(ATIS.Sound.WindFrom, 1.0, subtitle) - self.radioqueue:Number2Transmission(WINDFROM) - self:Transmission(ATIS.Sound.At, 0.2) - self.radioqueue:Number2Transmission(WINDSPEED) - if self.metric then - self:Transmission(ATIS.Sound.MetersPerSecond, 0.2) - else - self:Transmission(ATIS.Sound.Knots, 0.2) - end - if turbulence>0 then - self:Transmission(ATIS.Sound.Gusting, 0.2) + if not self.useSRS then + self:Transmission(ATIS.Sound.WindFrom, 1.0, subtitle) + self.radioqueue:Number2Transmission(WINDFROM) + self:Transmission(ATIS.Sound.At, 0.2) + self.radioqueue:Number2Transmission(WINDSPEED) + if self.metric then + self:Transmission(ATIS.Sound.MetersPerSecond, 0.2) + else + self:Transmission(ATIS.Sound.Knots, 0.2) + end + if turbulence>0 then + self:Transmission(ATIS.Sound.Gusting, 0.2) + end end alltext=alltext..";\n"..subtitle @@ -1654,12 +1786,14 @@ function ATIS:onafterBroadcast(From, Event, To) else subtitle=string.format("Visibility %s SM", VISIBILITY) end - self:Transmission(ATIS.Sound.Visibilty, 1.0, subtitle) - self.radioqueue:Number2Transmission(VISIBILITY) - if self.metric then - self:Transmission(ATIS.Sound.Kilometers, 0.2) - else - self:Transmission(ATIS.Sound.StatuteMiles, 0.2) + if not self.useSRS then + self:Transmission(ATIS.Sound.Visibilty, 1.0, subtitle) + self.radioqueue:Number2Transmission(VISIBILITY) + if self.metric then + self:Transmission(ATIS.Sound.Kilometers, 0.2) + else + self:Transmission(ATIS.Sound.StatuteMiles, 0.2) + end end alltext=alltext..";\n"..subtitle @@ -1699,57 +1833,67 @@ function ATIS:onafterBroadcast(From, Event, To) -- Actual output if wp then subtitle=string.format("Weather phenomena:%s", wpsub) - self:Transmission(ATIS.Sound.WeatherPhenomena, 1.0, subtitle) - if precepitation==1 then - self:Transmission(ATIS.Sound.Rain, 0.5) - elseif precepitation==2 then - self:Transmission(ATIS.Sound.ThunderStorm, 0.5) - elseif precepitation==3 then - self:Transmission(ATIS.Sound.Snow, 0.5) - elseif precepitation==4 then - self:Transmission(ATIS.Sound.SnowStorm, 0.5) - end - if fog then - self:Transmission(ATIS.Sound.Fog, 0.5) - end - if dust then - self:Transmission(ATIS.Sound.Dust, 0.5) + if not self.useSRS then + self:Transmission(ATIS.Sound.WeatherPhenomena, 1.0, subtitle) + if precepitation==1 then + self:Transmission(ATIS.Sound.Rain, 0.5) + elseif precepitation==2 then + self:Transmission(ATIS.Sound.ThunderStorm, 0.5) + elseif precepitation==3 then + self:Transmission(ATIS.Sound.Snow, 0.5) + elseif precepitation==4 then + self:Transmission(ATIS.Sound.SnowStorm, 0.5) + end + if fog then + self:Transmission(ATIS.Sound.Fog, 0.5) + end + if dust then + self:Transmission(ATIS.Sound.Dust, 0.5) + end end alltext=alltext..";\n"..subtitle end -- Cloud base - self:Transmission(CloudCover, 1.0, CLOUDSsub) + if not self.useSRS then + self:Transmission(CloudCover, 1.0, CLOUDSsub) + end if CLOUDBASE and static then -- Base + local cbase=tostring(tonumber(CLOUDBASE1000)*1000+tonumber(CLOUDBASE0100)*100) + local cceil=tostring(tonumber(CLOUDCEIL1000)*1000+tonumber(CLOUDCEIL0100)*100) if self.metric then - subtitle=string.format("Cloudbase %s, ceiling %s meters", CLOUDBASE, CLOUDCEIL) + --subtitle=string.format("Cloud base %s, ceiling %s meters", CLOUDBASE, CLOUDCEIL) + subtitle=string.format("Cloud base %s, ceiling %s meters", cbase, cceil) else - subtitle=string.format("Cloudbase %s, ceiling %s ft", CLOUDBASE, CLOUDCEIL) + --subtitle=string.format("Cloud base %s, ceiling %s feet", CLOUDBASE, CLOUDCEIL) + subtitle=string.format("Cloud base %s, ceiling %s feet", cbase, cceil) end - self:Transmission(ATIS.Sound.CloudBase, 1.0, subtitle) - if tonumber(CLOUDBASE1000)>0 then - self.radioqueue:Number2Transmission(CLOUDBASE1000) - self:Transmission(ATIS.Sound.Thousand, 0.1) - end - if tonumber(CLOUDBASE0100)>0 then - self.radioqueue:Number2Transmission(CLOUDBASE0100) - self:Transmission(ATIS.Sound.Hundred, 0.1) - end - -- Ceiling - self:Transmission(ATIS.Sound.CloudCeiling, 0.5) - if tonumber(CLOUDCEIL1000)>0 then - self.radioqueue:Number2Transmission(CLOUDCEIL1000) - self:Transmission(ATIS.Sound.Thousand, 0.1) - end - if tonumber(CLOUDCEIL0100)>0 then - self.radioqueue:Number2Transmission(CLOUDCEIL0100) - self:Transmission(ATIS.Sound.Hundred, 0.1) - end - if self.metric then - self:Transmission(ATIS.Sound.Meters, 0.1) - else - self:Transmission(ATIS.Sound.Feet, 0.1) + if not self.useSRS then + self:Transmission(ATIS.Sound.CloudBase, 1.0, subtitle) + if tonumber(CLOUDBASE1000)>0 then + self.radioqueue:Number2Transmission(CLOUDBASE1000) + self:Transmission(ATIS.Sound.Thousand, 0.1) + end + if tonumber(CLOUDBASE0100)>0 then + self.radioqueue:Number2Transmission(CLOUDBASE0100) + self:Transmission(ATIS.Sound.Hundred, 0.1) + end + -- Ceiling + self:Transmission(ATIS.Sound.CloudCeiling, 0.5) + if tonumber(CLOUDCEIL1000)>0 then + self.radioqueue:Number2Transmission(CLOUDCEIL1000) + self:Transmission(ATIS.Sound.Thousand, 0.1) + end + if tonumber(CLOUDCEIL0100)>0 then + self.radioqueue:Number2Transmission(CLOUDCEIL0100) + self:Transmission(ATIS.Sound.Hundred, 0.1) + end + if self.metric then + self:Transmission(ATIS.Sound.Meters, 0.1) + else + self:Transmission(ATIS.Sound.Feet, 0.1) + end end end alltext=alltext..";\n"..subtitle @@ -1769,15 +1913,17 @@ function ATIS:onafterBroadcast(From, Event, To) end end local _TEMPERATURE=subtitle - self:Transmission(ATIS.Sound.Temperature, 1.0, subtitle) - if temperature<0 then - self:Transmission(ATIS.Sound.Minus, 0.2) - end - self.radioqueue:Number2Transmission(TEMPERATURE) - if self.TDegF then - self:Transmission(ATIS.Sound.DegreesFahrenheit, 0.2) - else - self:Transmission(ATIS.Sound.DegreesCelsius, 0.2) + if not self.useSRS then + self:Transmission(ATIS.Sound.Temperature, 1.0, subtitle) + if temperature<0 then + self:Transmission(ATIS.Sound.Minus, 0.2) + end + self.radioqueue:Number2Transmission(TEMPERATURE) + if self.TDegF then + self:Transmission(ATIS.Sound.DegreesFahrenheit, 0.2) + else + self:Transmission(ATIS.Sound.DegreesCelsius, 0.2) + end end alltext=alltext..";\n"..subtitle @@ -1796,15 +1942,17 @@ function ATIS:onafterBroadcast(From, Event, To) end end local _DEWPOINT=subtitle - self:Transmission(ATIS.Sound.DewPoint, 1.0, subtitle) - if dewpoint<0 then - self:Transmission(ATIS.Sound.Minus, 0.2) - end - self.radioqueue:Number2Transmission(DEWPOINT) - if self.TDegF then - self:Transmission(ATIS.Sound.DegreesFahrenheit, 0.2) - else - self:Transmission(ATIS.Sound.DegreesCelsius, 0.2) + if not self.useSRS then + self:Transmission(ATIS.Sound.DewPoint, 1.0, subtitle) + if dewpoint<0 then + self:Transmission(ATIS.Sound.Minus, 0.2) + end + self.radioqueue:Number2Transmission(DEWPOINT) + if self.TDegF then + self:Transmission(ATIS.Sound.DegreesFahrenheit, 0.2) + else + self:Transmission(ATIS.Sound.DegreesCelsius, 0.2) + end end alltext=alltext..";\n"..subtitle @@ -1813,51 +1961,53 @@ function ATIS:onafterBroadcast(From, Event, To) if self.qnhonly then subtitle=string.format("Altimeter %s.%s mmHg", QNH[1], QNH[2]) else - subtitle=string.format("Altimeter QNH %s.%s, QFE %s.%s mmHg", QNH[1], QNH[2], QFE[1], QFE[2]) + subtitle=string.format("Altimeter: QNH %s.%s, QFE %s.%s mmHg", QNH[1], QNH[2], QFE[1], QFE[2]) end else if self.metric then if self.qnhonly then subtitle=string.format("Altimeter %s.%s hPa", QNH[1], QNH[2]) else - subtitle=string.format("Altimeter QNH %s.%s, QFE %s.%s hPa", QNH[1], QNH[2], QFE[1], QFE[2]) + subtitle=string.format("Altimeter: QNH %s.%s, QFE %s.%s hPa", QNH[1], QNH[2], QFE[1], QFE[2]) end else if self.qnhonly then subtitle=string.format("Altimeter %s.%s inHg", QNH[1], QNH[2]) else - subtitle=string.format("Altimeter QNH %s.%s, QFE %s.%s inHg", QNH[1], QNH[2], QFE[1], QFE[2]) + subtitle=string.format("Altimeter: QNH %s.%s, QFE %s.%s inHg", QNH[1], QNH[2], QFE[1], QFE[2]) end end end local _ALTIMETER=subtitle - self:Transmission(ATIS.Sound.Altimeter, 1.0, subtitle) - if not self.qnhonly then - self:Transmission(ATIS.Sound.QNH, 0.5) - end - self.radioqueue:Number2Transmission(QNH[1]) - - if ATIS.ICAOPhraseology[UTILS.GetDCSMap()] then - self:Transmission(ATIS.Sound.Decimal, 0.2) - end - self.radioqueue:Number2Transmission(QNH[2]) - - if not self.qnhonly then - self:Transmission(ATIS.Sound.QFE, 0.75) - self.radioqueue:Number2Transmission(QFE[1]) - if ATIS.ICAOPhraseology[UTILS.GetDCSMap()] then - self:Transmission(ATIS.Sound.Decimal, 0.2) + if not self.useSRS then + self:Transmission(ATIS.Sound.Altimeter, 1.0, subtitle) + if not self.qnhonly then + self:Transmission(ATIS.Sound.QNH, 0.5) end - self.radioqueue:Number2Transmission(QFE[2]) - end + self.radioqueue:Number2Transmission(QNH[1]) + + if ATIS.ICAOPhraseology[UTILS.GetDCSMap()] then + self:Transmission(ATIS.Sound.Decimal, 0.2) + end + self.radioqueue:Number2Transmission(QNH[2]) - if self.PmmHg then - self:Transmission(ATIS.Sound.MillimetersOfMercury, 0.1) - else - if self.metric then - self:Transmission(ATIS.Sound.HectoPascal, 0.1) + if not self.qnhonly then + self:Transmission(ATIS.Sound.QFE, 0.75) + self.radioqueue:Number2Transmission(QFE[1]) + if ATIS.ICAOPhraseology[UTILS.GetDCSMap()] then + self:Transmission(ATIS.Sound.Decimal, 0.2) + end + self.radioqueue:Number2Transmission(QFE[2]) + end + + if self.PmmHg then + self:Transmission(ATIS.Sound.MillimetersOfMercury, 0.1) else - self:Transmission(ATIS.Sound.InchesOfMercury, 0.1) + if self.metric then + self:Transmission(ATIS.Sound.HectoPascal, 0.1) + else + self:Transmission(ATIS.Sound.InchesOfMercury, 0.1) + end end end alltext=alltext..";\n"..subtitle @@ -1870,12 +2020,14 @@ function ATIS:onafterBroadcast(From, Event, To) subtitle=subtitle.." Right" end local _RUNACT=subtitle - self:Transmission(ATIS.Sound.ActiveRunway, 1.0, subtitle) - self.radioqueue:Number2Transmission(runway) - if rwyLeft==true then - self:Transmission(ATIS.Sound.Left, 0.2) - elseif rwyLeft==false then - self:Transmission(ATIS.Sound.Right, 0.2) + if not self.useSRS then + self:Transmission(ATIS.Sound.ActiveRunway, 1.0, subtitle) + self.radioqueue:Number2Transmission(runway) + if rwyLeft==true then + self:Transmission(ATIS.Sound.Left, 0.2) + elseif rwyLeft==false then + self:Transmission(ATIS.Sound.Right, 0.2) + end end alltext=alltext..";\n"..subtitle @@ -1900,21 +2052,22 @@ function ATIS:onafterBroadcast(From, Event, To) end -- Transmit. - self:Transmission(ATIS.Sound.RunwayLength, 1.0, subtitle) - if tonumber(L1000)>0 then - self.radioqueue:Number2Transmission(L1000) - self:Transmission(ATIS.Sound.Thousand, 0.1) + if not self.useSRS then + self:Transmission(ATIS.Sound.RunwayLength, 1.0, subtitle) + if tonumber(L1000)>0 then + self.radioqueue:Number2Transmission(L1000) + self:Transmission(ATIS.Sound.Thousand, 0.1) + end + if tonumber(L0100)>0 then + self.radioqueue:Number2Transmission(L0100) + self:Transmission(ATIS.Sound.Hundred, 0.1) + end + if self.metric then + self:Transmission(ATIS.Sound.Meters, 0.1) + else + self:Transmission(ATIS.Sound.Feet, 0.1) + end end - if tonumber(L0100)>0 then - self.radioqueue:Number2Transmission(L0100) - self:Transmission(ATIS.Sound.Hundred, 0.1) - end - if self.metric then - self:Transmission(ATIS.Sound.Meters, 0.1) - else - self:Transmission(ATIS.Sound.Feet, 0.1) - end - alltext=alltext..";\n"..subtitle end @@ -1937,22 +2090,23 @@ function ATIS:onafterBroadcast(From, Event, To) subtitle=subtitle.." feet" end - -- Transmitt. - self:Transmission(ATIS.Sound.Elevation, 1.0, subtitle) - if tonumber(L1000)>0 then - self.radioqueue:Number2Transmission(L1000) - self:Transmission(ATIS.Sound.Thousand, 0.1) + -- Transmit. + if not self.useSRS then + self:Transmission(ATIS.Sound.Elevation, 1.0, subtitle) + if tonumber(L1000)>0 then + self.radioqueue:Number2Transmission(L1000) + self:Transmission(ATIS.Sound.Thousand, 0.1) + end + if tonumber(L0100)>0 then + self.radioqueue:Number2Transmission(L0100) + self:Transmission(ATIS.Sound.Hundred, 0.1) + end + if self.metric then + self:Transmission(ATIS.Sound.Meters, 0.1) + else + self:Transmission(ATIS.Sound.Feet, 0.1) + end end - if tonumber(L0100)>0 then - self.radioqueue:Number2Transmission(L0100) - self:Transmission(ATIS.Sound.Hundred, 0.1) - end - if self.metric then - self:Transmission(ATIS.Sound.Meters, 0.1) - else - self:Transmission(ATIS.Sound.Feet, 0.1) - end - alltext=alltext..";\n"..subtitle end @@ -1966,9 +2120,47 @@ function ATIS:onafterBroadcast(From, Event, To) end end subtitle=string.format("Tower frequency %s", freqs) - self:Transmission(ATIS.Sound.TowerFrequency, 1.0, subtitle) - for _,freq in pairs(self.towerfrequency) do - local f=string.format("%.3f", freq) + if not self.useSRS then + self:Transmission(ATIS.Sound.TowerFrequency, 1.0, subtitle) + for _,freq in pairs(self.towerfrequency) do + local f=string.format("%.3f", freq) + f=UTILS.Split(f, ".") + self.radioqueue:Number2Transmission(f[1], nil, 0.5) + if tonumber(f[2])>0 then + self:Transmission(ATIS.Sound.Decimal, 0.2) + self.radioqueue:Number2Transmission(f[2]) + end + self:Transmission(ATIS.Sound.MegaHertz, 0.2) + end + end + alltext=alltext..";\n"..subtitle + end + + -- ILS + local ils=self:GetNavPoint(self.ils, runway, rwyLeft) + if ils then + subtitle=string.format("ILS frequency %.2f MHz", ils.frequency) + if not self.useSRS then + self:Transmission(ATIS.Sound.ILSFrequency, 1.0, subtitle) + local f=string.format("%.2f", ils.frequency) + f=UTILS.Split(f, ".") + self.radioqueue:Number2Transmission(f[1], nil, 0.5) + if tonumber(f[2])>0 then + self:Transmission(ATIS.Sound.Decimal, 0.2) + self.radioqueue:Number2Transmission(f[2]) + end + self:Transmission(ATIS.Sound.MegaHertz, 0.2) + end + alltext=alltext..";\n"..subtitle + end + + -- Outer NDB + local ndb=self:GetNavPoint(self.ndbouter, runway, rwyLeft) + if ndb then + subtitle=string.format("Outer NDB frequency %.2f MHz", ndb.frequency) + if not self.useSRS then + self:Transmission(ATIS.Sound.OuterNDBFrequency, 1.0, subtitle) + local f=string.format("%.2f", ndb.frequency) f=UTILS.Split(f, ".") self.radioqueue:Number2Transmission(f[1], nil, 0.5) if tonumber(f[2])>0 then @@ -1977,41 +2169,6 @@ function ATIS:onafterBroadcast(From, Event, To) end self:Transmission(ATIS.Sound.MegaHertz, 0.2) end - - alltext=alltext..";\n"..subtitle - end - - -- ILS - local ils=self:GetNavPoint(self.ils, runway, rwyLeft) - if ils then - subtitle=string.format("ILS frequency %.2f MHz", ils.frequency) - self:Transmission(ATIS.Sound.ILSFrequency, 1.0, subtitle) - local f=string.format("%.2f", ils.frequency) - f=UTILS.Split(f, ".") - self.radioqueue:Number2Transmission(f[1], nil, 0.5) - if tonumber(f[2])>0 then - self:Transmission(ATIS.Sound.Decimal, 0.2) - self.radioqueue:Number2Transmission(f[2]) - end - self:Transmission(ATIS.Sound.MegaHertz, 0.2) - - alltext=alltext..";\n"..subtitle - end - - -- Outer NDB - local ndb=self:GetNavPoint(self.ndbouter, runway, rwyLeft) - if ndb then - subtitle=string.format("Outer NDB frequency %.2f MHz", ndb.frequency) - self:Transmission(ATIS.Sound.OuterNDBFrequency, 1.0, subtitle) - local f=string.format("%.2f", ndb.frequency) - f=UTILS.Split(f, ".") - self.radioqueue:Number2Transmission(f[1], nil, 0.5) - if tonumber(f[2])>0 then - self:Transmission(ATIS.Sound.Decimal, 0.2) - self.radioqueue:Number2Transmission(f[2]) - end - self:Transmission(ATIS.Sound.MegaHertz, 0.2) - alltext=alltext..";\n"..subtitle end @@ -2019,51 +2176,58 @@ function ATIS:onafterBroadcast(From, Event, To) local ndb=self:GetNavPoint(self.ndbinner, runway, rwyLeft) if ndb then subtitle=string.format("Inner NDB frequency %.2f MHz", ndb.frequency) - self:Transmission(ATIS.Sound.InnerNDBFrequency, 1.0, subtitle) - local f=string.format("%.2f", ndb.frequency) - f=UTILS.Split(f, ".") - self.radioqueue:Number2Transmission(f[1], nil, 0.5) - if tonumber(f[2])>0 then - self:Transmission(ATIS.Sound.Decimal, 0.2) - self.radioqueue:Number2Transmission(f[2]) - end - self:Transmission(ATIS.Sound.MegaHertz, 0.2) - + if not self.useSRS then + self:Transmission(ATIS.Sound.InnerNDBFrequency, 1.0, subtitle) + local f=string.format("%.2f", ndb.frequency) + f=UTILS.Split(f, ".") + self.radioqueue:Number2Transmission(f[1], nil, 0.5) + if tonumber(f[2])>0 then + self:Transmission(ATIS.Sound.Decimal, 0.2) + self.radioqueue:Number2Transmission(f[2]) + end + self:Transmission(ATIS.Sound.MegaHertz, 0.2) + end alltext=alltext..";\n"..subtitle end -- VOR if self.vor then subtitle=string.format("VOR frequency %.2f MHz", self.vor) - self:Transmission(ATIS.Sound.VORFrequency, 1.0, subtitle) - local f=string.format("%.2f", self.vor) - f=UTILS.Split(f, ".") - self.radioqueue:Number2Transmission(f[1], nil, 0.5) - if tonumber(f[2])>0 then - self:Transmission(ATIS.Sound.Decimal, 0.2) - self.radioqueue:Number2Transmission(f[2]) + if self.useSRS then + subtitle=string.format("V O R frequency %.2f MHz", self.vor) + end + if not self.useSRS then + self:Transmission(ATIS.Sound.VORFrequency, 1.0, subtitle) + local f=string.format("%.2f", self.vor) + f=UTILS.Split(f, ".") + self.radioqueue:Number2Transmission(f[1], nil, 0.5) + if tonumber(f[2])>0 then + self:Transmission(ATIS.Sound.Decimal, 0.2) + self.radioqueue:Number2Transmission(f[2]) + end + self:Transmission(ATIS.Sound.MegaHertz, 0.2) end - self:Transmission(ATIS.Sound.MegaHertz, 0.2) - alltext=alltext..";\n"..subtitle end -- TACAN if self.tacan then subtitle=string.format("TACAN channel %dX", self.tacan) - self:Transmission(ATIS.Sound.TACANChannel, 1.0, subtitle) - self.radioqueue:Number2Transmission(tostring(self.tacan), nil, 0.2) - self.radioqueue:NewTransmission("NATO Alphabet/Xray.ogg", 0.75, self.soundpath, nil, 0.2) - + if not self.useSRS then + self:Transmission(ATIS.Sound.TACANChannel, 1.0, subtitle) + self.radioqueue:Number2Transmission(tostring(self.tacan), nil, 0.2) + self.radioqueue:NewTransmission("NATO Alphabet/Xray.ogg", 0.75, self.soundpath, nil, 0.2) + end alltext=alltext..";\n"..subtitle end -- RSBN if self.rsbn then subtitle=string.format("RSBN channel %d", self.rsbn) - self:Transmission(ATIS.Sound.RSBNChannel, 1.0, subtitle) - self.radioqueue:Number2Transmission(tostring(self.rsbn), nil, 0.2) - + if not self.useSRS then + self:Transmission(ATIS.Sound.RSBNChannel, 1.0, subtitle) + self.radioqueue:Number2Transmission(tostring(self.rsbn), nil, 0.2) + end alltext=alltext..";\n"..subtitle end @@ -2071,17 +2235,19 @@ function ATIS:onafterBroadcast(From, Event, To) local ndb=self:GetNavPoint(self.prmg, runway, rwyLeft) if ndb then subtitle=string.format("PRMG channel %d", ndb.frequency) - self:Transmission(ATIS.Sound.PRMGChannel, 1.0, subtitle) - self.radioqueue:Number2Transmission(tostring(ndb.frequency), nil, 0.5) - + if not self.useSRS then + self:Transmission(ATIS.Sound.PRMGChannel, 1.0, subtitle) + self.radioqueue:Number2Transmission(tostring(ndb.frequency), nil, 0.5) + end alltext=alltext..";\n"..subtitle end -- Advice on initial... subtitle=string.format("Advise on initial contact, you have information %s", NATO) - self:Transmission(ATIS.Sound.AdviceOnInitial, 0.5, subtitle) - self.radioqueue:NewTransmission(string.format("NATO Alphabet/%s.ogg", NATO), 0.75, self.soundpath) - + if not self.useSRS then + self:Transmission(ATIS.Sound.AdviceOnInitial, 0.5, subtitle) + self.radioqueue:NewTransmission(string.format("NATO Alphabet/%s.ogg", NATO), 0.75, self.soundpath) + end alltext=alltext..";\n"..subtitle -- Report ATIS text. @@ -2102,6 +2268,62 @@ end -- @param #string Text Report text. function ATIS:onafterReport(From, Event, To, Text) self:T(self.lid..string.format("Report:\n%s", Text)) + + if self.useSRS and self.msrs then + + -- Remove line breaks + local text=string.gsub(Text, "[\r\n]", "") + + -- Replace other stuff. + local text=string.gsub(text, "SM", "statute miles") + local text=string.gsub(text, "°C", "degrees Celsius") + local text=string.gsub(text, "°F", "degrees Fahrenheit") + local text=string.gsub(text, "inHg", "inches of Mercury") + local text=string.gsub(text, "mmHg", "millimeters of Mercury") + local text=string.gsub(text, "hPa", "hecto Pascals") + local text=string.gsub(text, "m/s", "meters per second") + + -- Replace ";" by "." + local text=string.gsub(text, ";", " . ") + + --Debug output. + self:T("SRS TTS: "..text) + + -- Play text-to-speech report. + self.msrs:PlayText(text) + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Event Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Base captured +-- @param #ATIS self +-- @param Core.Event#EVENTDATA EventData Event data. +function ATIS:OnEventBaseCaptured(EventData) + + if EventData and EventData.Place then + + -- Place is the airbase that was captured. + local airbase=EventData.Place --Wrapper.Airbase#AIRBASE + + -- Check that this airbase belongs or did belong to this warehouse. + if EventData.PlaceName==self.airbasename then + + -- New coalition of airbase after it was captured. + local NewCoalitionAirbase=airbase:GetCoalition() + + if self.useSRS and self.msrs and self.msrs.coalition~=NewCoalitionAirbase then + self.msrs:SetCoalition(NewCoalitionAirbase) + end + + end + + end + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/AirWing.lua b/Moose Development/Moose/Ops/AirWing.lua index 202eb631d..4493264c9 100644 --- a/Moose Development/Moose/Ops/AirWing.lua +++ b/Moose Development/Moose/Ops/AirWing.lua @@ -32,6 +32,7 @@ -- @field #table pointsCAP Table of CAP points. -- @field #table pointsTANKER Table of Tanker points. -- @field #table pointsAWACS Table of AWACS points. +-- @field #boolean markpoints Display markers on the F10 map. -- @field Ops.WingCommander#WINGCOMMANDER wingcommander The wing commander responsible for this airwing. -- -- @field Ops.RescueHelo#RESCUEHELO rescuehelo The rescue helo. @@ -153,7 +154,7 @@ AIRWING = { --- AIRWING class version. -- @field #string version -AIRWING.version="0.5.1" +AIRWING.version="0.5.2" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list @@ -210,7 +211,7 @@ function AIRWING:New(warehousename, airwingname) self.nflightsTANKERprobe=0 self.nflightsRecoveryTanker=0 self.nflightsRescueHelo=0 - self.markpoints = false + self.markpoints=false ------------------------ --- Pseudo Functions --- @@ -1551,6 +1552,9 @@ function AIRWING:onafterNewAsset(From, Event, To, asset, assignment) asset.nunits=squad.ngrouping end + + -- Set takeoff type. + asset.takeoffType=squad.takeoffType -- Create callsign and modex (needs to be after grouping). squad:GetCallsign(asset) @@ -1616,84 +1620,86 @@ function AIRWING:onafterAssetSpawned(From, Event, To, group, asset, request) -- Call parent warehouse function first. self:GetParent(self).onafterAssetSpawned(self, From, Event, To, group, asset, request) - - -- Create a flight group. - local flightgroup=self:_CreateFlightGroup(asset) - - - --- - -- Asset - --- - - -- Set asset flightgroup. - asset.flightgroup=flightgroup - - -- Not requested any more. - asset.requested=nil - - -- Did not return yet. - asset.Treturned=nil - - --- - -- Squadron - --- -- Get the SQUADRON of the asset. local squadron=self:GetSquadronOfAsset(asset) - -- Get TACAN channel. - local Tacan=squadron:FetchTacan() - if Tacan then - asset.tacan=Tacan - end - - -- Set radio frequency and modulation - local radioFreq, radioModu=squadron:GetRadio() - if radioFreq then - flightgroup:SwitchRadio(radioFreq, radioModu) - end + -- Check if we have a squadron or if this was some other request. + if squadron then + + -- Create a flight group. + local flightgroup=self:_CreateFlightGroup(asset) + + --- + -- Asset + --- - if squadron.fuellow then - flightgroup:SetFuelLowThreshold(squadron.fuellow) - end - - if squadron.fuellowRefuel then - flightgroup:SetFuelLowRefuel(squadron.fuellowRefuel) - end - - --- - -- Mission - --- - - -- Get Mission (if any). - local mission=self:GetMissionByID(request.assignment) - - -- Add mission to flightgroup queue. - if mission then + -- Set asset flightgroup. + asset.flightgroup=flightgroup + + -- Not requested any more. + asset.requested=nil + + -- Did not return yet. + asset.Treturned=nil + --- + -- Squadron + --- + + -- Get TACAN channel. + local Tacan=squadron:FetchTacan() if Tacan then - mission:SetTACAN(Tacan, Morse, UnitName, Band) + asset.tacan=Tacan + end + + -- Set radio frequency and modulation + local radioFreq, radioModu=squadron:GetRadio() + if radioFreq then + flightgroup:SwitchRadio(radioFreq, radioModu) end - -- Add mission to flightgroup queue. - asset.flightgroup:AddMission(mission) - - -- Trigger event. - self:FlightOnMission(flightgroup, mission) - - else - - if Tacan then - flightgroup:SwitchTACAN(Tacan, Morse, UnitName, Band) + if squadron.fuellow then + flightgroup:SetFuelLowThreshold(squadron.fuellow) end - - end - + if squadron.fuellowRefuel then + flightgroup:SetFuelLowRefuel(squadron.fuellowRefuel) + end - -- Add group to the detection set of the WINGCOMMANDER. - if self.wingcommander and self.wingcommander.chief then - self.wingcommander.chief.detectionset:AddGroup(asset.flightgroup.group) + --- + -- Mission + --- + + -- Get Mission (if any). + local mission=self:GetMissionByID(request.assignment) + + -- Add mission to flightgroup queue. + if mission then + + if Tacan then + mission:SetTACAN(Tacan, Morse, UnitName, Band) + end + + -- Add mission to flightgroup queue. + asset.flightgroup:AddMission(mission) + + -- Trigger event. + self:FlightOnMission(flightgroup, mission) + + else + + if Tacan then + flightgroup:SwitchTACAN(Tacan, Morse, UnitName, Band) + end + + end + + -- Add group to the detection set of the WINGCOMMANDER. + if self.wingcommander and self.wingcommander.chief then + self.wingcommander.chief.detectionset:AddGroup(asset.flightgroup.group) + end + end end @@ -1822,34 +1828,6 @@ function AIRWING:_CreateFlightGroup(asset) -- Set home base. flightgroup.homebase=self.airbase - --[[ - - --- Check if out of missiles. For A2A missions ==> RTB. - function flightgroup:OnAfterOutOfMissiles() - local airwing=flightgroup:GetAirWing() - - end - - --- Check if out of missiles. For A2G missions ==> RTB. But need to check A2G missiles, rockets as well. - function flightgroup:OnAfterOutOfBombs() - local airwing=flightgroup:GetAirWing() - - end - - --- Mission started. - function flightgroup:OnAfterMissionStart(From, Event, To, Mission) - local airwing=flightgroup:GetAirWing() - - end - - --- Flight is DEAD. - function flightgroup:OnAfterFlightDead(From, Event, To) - local airwing=flightgroup:GetAirWing() - - end - - ]] - return flightgroup end diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 38d04d4ff..2ce869f2c 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -15717,8 +15717,12 @@ function AIRBOSS:_MarshalCallRecoveryStart(case) -- Debug output. local text=string.format("Starting aircraft recovery Case %d ops.", case) - if case>1 then - text=text..string.format(" Marshal radial %03d°.", radial) + if case==1 then + text=text..string.format(" BRC %03d°.", self:GetBRC()) + elseif case==2 then + text=text..string.format(" Marshal radial %03d°. BRC %03d°.", radial, self:GetBRC()) + elseif case==3 then + text=text..string.format(" Marshal radial %03d°. Final heading %03d°.", radial, self:GetFinalBearing(false)) end self:T(self.lid..text) diff --git a/Moose Development/Moose/Ops/CSAR.lua b/Moose Development/Moose/Ops/CSAR.lua new file mode 100644 index 000000000..df419a5b9 --- /dev/null +++ b/Moose Development/Moose/Ops/CSAR.lua @@ -0,0 +1,2063 @@ + +--- **Ops** -- Combat Search and Rescue. +-- +-- === +-- +-- **CSAR** - MOOSE based Helicopter CSAR Operations. +-- +-- === +-- +-- ## Missions: +-- +-- ### [CSAR - Combat Search & Rescue](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20CSAR) +-- +-- === +-- +-- **Main Features:** +-- +-- * MOOSE-based Helicopter CSAR Operations for Players. +-- +-- === +-- +-- ### Author: **Applevangelist** (Moose Version), ***Ciribob*** (original), Thanks to: Shadowze, Cammel (testing) +-- @module Ops.CSAR +-- @image OPS_CSAR.jpg + +-- Date: July 2021 + +------------------------------------------------------------------------- +--- **CSAR** class, extends Core.Base#BASE, Core.Fsm#FSM +-- @type CSAR +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity level. +-- @field #string lid Class id string for output to DCS log file. +-- @field #number coalition Coalition side number, e.g. `coalition.side.RED`. +-- @extends Core.Fsm#FSM + +--- *Combat search and rescue (CSAR) are search and rescue operations that are carried out during war that are within or near combat zones.* (Wikipedia) +-- +-- === +-- +-- ![Banner Image](OPS_CSAR.jpg) +-- +-- # CSAR Concept +-- +-- * MOOSE-based Helicopter CSAR Operations for Players. +-- * Object oriented refactoring of Ciribob\'s fantastic CSAR script. +-- * No need for extra MIST loading. +-- * Additional events to tailor your mission. +-- +-- ## 0. Prerequisites +-- +-- You need to load an .ogg soundfile for the pilot\'s beacons into the mission, e.g. "beacon.ogg", use a once trigger, "sound to country" for that. +-- Create a late-activated single infantry unit as template in the mission editor and name it e.g. "Downed Pilot". +-- +-- ## 1. Basic Setup +-- +-- A basic setup example is the following: +-- +-- -- Instantiate and start a CSAR for the blue side, with template "Downed Pilot" and alias "Luftrettung" +-- local my_csar = CSAR:New(coalition.side.BLUE,"Downed Pilot","Luftrettung") +-- -- options +-- my_csar.immortalcrew = true -- downed pilot spawn is immortal +-- my_csar.invisiblecrew = false -- downed pilot spawn is visible +-- -- start the FSM +-- my_csar:__Start(5) +-- +-- ## 2. Options +-- +-- 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.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. +-- -- (added 0.1.4) limit amount of downed pilots spawned by **ejection** events +-- self.limitmaxdownedpilots = true +-- self.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 +-- +-- ## 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 +-- +-- ## 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) +-- +-- ## 4. Events +-- +-- The class comes with a number of FSM-based events that missions designers can use to shape their mission. +-- These are: +-- +-- ### 4.1. PilotDown. +-- +-- The event is triggered when a new downed pilot is detected. Use e.g. `function my_csar:OnAfterPilotDown(...)` to link into this event: +-- +-- function my_csar:OnAfterPilotDown(from, event, to, spawnedgroup, frequency, groupname, coordinates_text) +-- ... your code here ... +-- end +-- +-- ### 4.2. Approach. +-- +-- A CSAR helicpoter is closing in on a downed pilot. Use e.g. `function my_csar:OnAfterApproach(...)` to link into this event: +-- +-- function my_csar:OnAfterApproach(from, event, to, heliname, groupname) +-- ... your code here ... +-- end +-- +-- ### 4.3. Boarded. +-- +-- The pilot has been boarded to the helicopter. Use e.g. `function my_csar:OnAfterBoarded(...)` to link into this event: +-- +-- function my_csar:OnAfterBoarded(from, event, to, heliname, groupname) +-- ... your code here ... +-- end +-- +-- ### 4.4. Returning. +-- +-- The CSAR helicopter is ready to return to an Airbase, FARP or MASH. Use e.g. `function my_csar:OnAfterReturning(...)` to link into this event: +-- +-- function my_csar:OnAfterReturning(from, event, to, heliname, groupname) +-- ... your code here ... +-- end +-- +-- ### 4.5. Rescued. +-- +-- The CSAR helicopter has landed close to an Airbase/MASH/FARP and the pilots are safe. Use e.g. `function my_csar:OnAfterRescued(...)` to link into this event: +-- +-- function my_csar:OnAfterRescued(from, event, to, heliunit, heliname, pilotssaved) +-- ... your code here ... +-- end +-- +-- ## 5. Spawn downed pilots at location to be picked up. +-- +-- If missions designers want to spawn downed pilots into the field, e.g. at mission begin to give the helicopter guys works, they can do this like so: +-- +-- -- Create downed "Pilot Wagner" in #ZONE "CSAR_Start_1" at a random point for the blue coalition +-- my_csar:SpawnCSARAtZone( "CSAR_Start_1", coalition.side.BLUE, "Pilot Wagner", true ) +-- +-- +-- @field #CSAR +CSAR = { + ClassName = "CSAR", + verbose = 0, + lid = "", + coalition = 1, + coalitiontxt = "blue", + FreeVHFFrequencies = {}, + UsedVHFFrequencies = {}, + takenOff = {}, + csarUnits = {}, -- table of unit names + downedPilots = {}, + woundedGroups = {}, + landedStatus = {}, + addedTo = {}, + woundedGroups = {}, -- contains the new group of units + inTransitGroups = {}, -- contain a table for each SAR with all units he has with the original names + smokeMarkers = {}, -- tracks smoke markers for groups + heliVisibleMessage = {}, -- tracks if the first message has been sent of the heli being visible + heliCloseMessage = {}, -- tracks heli close message ie heli < 500m distance + max_units = 6, --number of pilots that can be carried + hoverStatus = {}, -- tracks status of a helis hover above a downed pilot + pilotDisabled = {}, -- tracks what aircraft a pilot is disabled for + pilotLives = {}, -- tracks how many lives a pilot has + useprefix = true, -- Use the Prefixed defined below, Requires Unit have the Prefix defined below + csarPrefix = {}, + template = nil, + mash = {}, + smokecolor = 4, + rescues = 0, + rescuedpilots = 0, + limitmaxdownedpilots = true, + maxdownedpilots = 10, +} + +--- Downed pilots info. +-- @type CSAR.DownedPilot +-- @field #number index Pilot index. +-- @field #string name Name of the spawned group. +-- @field #number side Coalition. +-- @field #string originalUnit Name of the original unit. +-- @field #string desc Description. +-- @field #string typename Typename of Unit. +-- @field #number frequency Frequency of the NDB. +-- @field #string player Player name if applicable. +-- @field Wrapper.Group#GROUP group Spawned group object. +-- @field #number timestamp Timestamp for approach process +-- @field #boolean alive Group is alive or dead/rescued +-- +--- Updated and sorted list of known NDB beacons (in kHz!) from the available maps. + +--[[ Moved to Utils +-- @field #CSAR.SkipFrequencies +CSAR.SkipFrequencies = { + 214,274,291.5,295,297.5, + 300.5,304,307,309.5,311,312,312.5,316, + 320,324,328,329,330,336,337, + 342,343,348,351,352,353,358, + 363,365,368,372.5,374, + 380,381,384,389,395,396, + 414,420,430,432,435,440,450,455,462,470,485, + 507,515,520,525,528,540,550,560,570,577,580,602,625,641,662,670,680,682,690, + 705,720,722,730,735,740,745,750,770,795, + 822,830,862,866, + 905,907,920,935,942,950,995, + 1000,1025,1030,1050,1065,1116,1175,1182,1210 + } +--]] + +--- All slot / Limit settings +-- @type CSAR.AircraftType +-- @field #string typename Unit type name. +CSAR.AircraftType = {} -- Type and limit +CSAR.AircraftType["SA342Mistral"] = 2 +CSAR.AircraftType["SA342Minigun"] = 2 +CSAR.AircraftType["SA342L"] = 4 +CSAR.AircraftType["SA342M"] = 4 +CSAR.AircraftType["UH-1H"] = 8 +CSAR.AircraftType["Mi-8MTV2"] = 12 +CSAR.AircraftType["Mi-8MT"] = 12 +CSAR.AircraftType["Mi-24P"] = 8 +CSAR.AircraftType["Mi-24V"] = 8 + +--- CSAR class version. +-- @field #string version +CSAR.version="0.1.8r3" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ToDo list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- DONE: SRS Integration (to be tested) +-- TODO: Maybe - add option to smoke/flare closest MASH + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new CSAR object and start the FSM. +-- @param #CSAR self +-- @param #number Coalition Coalition side. Can also be passed as a string "red", "blue" or "neutral". +-- @param #string Template Name of the late activated infantry unit standing in for the downed pilot. +-- @param #string Alias An *optional* alias how this object is called in the logs etc. +-- @return #CSAR self +function CSAR:New(Coalition, Template, Alias) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #CSAR + + --set Coalition + if Coalition and type(Coalition)=="string" then + if Coalition=="blue" then + self.coalition=coalition.side.BLUE + self.coalitiontxt = Coalition + elseif Coalition=="red" then + self.coalition=coalition.side.RED + self.coalitiontxt = Coalition + elseif Coalition=="neutral" then + self.coalition=coalition.side.NEUTRAL + self.coalitiontxt = Coalition + else + self:E("ERROR: Unknown coalition in CSAR!") + end + else + self.coalition = Coalition + self.coalitiontxt = string.lower(UTILS.GetCoalitionName(self.coalition)) + end + + -- Set alias. + if Alias then + self.alias=tostring(Alias) + else + self.alias="Red Cross" + if self.coalition then + if self.coalition==coalition.side.RED then + self.alias="IFRC" + elseif self.coalition==coalition.side.BLUE then + self.alias="CSAR" + end + end + end + + -- Set some string id for output to DCS.log file. + self.lid=string.format("%s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown") + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start FSM. + self:AddTransition("*", "Status", "*") -- CSAR status update. + self:AddTransition("*", "PilotDown", "*") -- Downed Pilot added + self:AddTransition("*", "Approach", "*") -- CSAR heli closing in. + self:AddTransition("*", "Boarded", "*") -- Pilot boarded. + self:AddTransition("*", "Returning", "*") -- CSAR able to return to base. + self:AddTransition("*", "Rescued", "*") -- Pilot at MASH. + self:AddTransition("*", "KIA", "*") -- Pilot killed in action. + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + + -- tables, mainly for tracking actions + self.addedTo = {} + self.allheligroupset = {} -- GROUP_SET of all helis + self.csarUnits = {} -- table of CSAR unit names + self.FreeVHFFrequencies = {} + self.heliVisibleMessage = {} -- tracks if the first message has been sent of the heli being visible + self.heliCloseMessage = {} -- tracks heli close message ie heli < 500m distance + self.hoverStatus = {} -- tracks status of a helis hover above a downed pilot + self.inTransitGroups = {} -- contain a table for each SAR with all units he has with the original names + self.landedStatus = {} + self.lastCrash = {} + self.takenOff = {} + self.smokeMarkers = {} -- tracks smoke markers for groups + self.UsedVHFFrequencies = {} + self.woundedGroups = {} -- contains the new group of units + self.downedPilots = {} -- Replacement woundedGroups + self.downedpilotcounter = 1 + + -- settings, counters etc + self.rescues = 0 -- counter for successful rescue landings at FARP/AFB/MASH + self.rescuedpilots = 0 -- counter for saved pilots + self.csarOncrash = false -- If set to true, will generate a csar when a plane crashes as well. + self.allowDownedPilotCAcontrol = false -- Set to false if you don\'t want to allow control by Combined arms. + self.enableForAI = false -- set to false to disable AI units from being rescued. + self.smokecolor = 4 -- Color of smokemarker for blue side, 0 is green, 1 is red, 2 is white, 3 is orange and 4 is blue + self.coordtype = 2 -- Use Lat/Long DDM (0), Lat/Long DMS (1), MGRS (2), Bullseye imperial (3) or Bullseye metric (4) for coordinates. + self.immortalcrew = true -- Set to true to make wounded crew immortal + self.invisiblecrew = false -- Set to true to make wounded crew insvisible + self.messageTime = 15 -- Time to show longer messages for in seconds + self.pilotRuntoExtractPoint = true -- Downed Pilot will run to the rescue helicopter up to self.extractDistance METERS + self.loadDistance = 75 -- configure distance for pilot to get in helicopter in meters. + self.extractDistance = 500 -- Distance the Downed pilot will run to the rescue helicopter + self.loadtimemax = 135 -- seconds + self.radioSound = "beacon.ogg" -- the name of the sound file to use for the Pilot radio beacons. If this isnt added to the mission BEACONS WONT WORK! + self.allowFARPRescue = true --allows pilot to be rescued by landing at a FARP or Airbase + self.max_units = 6 --max number of pilots that can be carried + self.useprefix = true -- Use the Prefixed defined below, Requires Unit have the Prefix defined below + self.csarPrefix = { "helicargo", "MEDEVAC"} -- prefixes used for useprefix=true - DON\'T use # in names! + self.template = Template or "generic" -- template for downed pilot + self.mashprefix = {"MASH"} -- prefixes used to find MASHes + self.mash = SET_GROUP:New():FilterCoalitions(self.coalition):FilterPrefixes(self.mashprefix):FilterOnce() -- currently only GROUP objects, maybe support STATICs also? + self.autosmoke = false -- automatically smoke location when heli is near + self.autosmokedistance = 1000 -- distance for autosmoke + -- added 0.1.4 + self.limitmaxdownedpilots = true + self.maxdownedpilots = 25 + -- generate Frequencies + self:_GenerateVHFrequencies() + -- added 0.1.8 + 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 on open doors + + -- WARNING - here\'ll be dragons + -- for this to work you need to de-sanitize your mission environment in \Scripts\MissionScripting.lua + -- needs SRS => 1.9.6 to work (works on the *server* side) + self.useSRS = false -- Use FF\'s SRS integration + self.SRSPath = "E:\\Progra~1\\DCS-SimpleRadio-Standalone\\" -- adjust your own path in your server(!) + self.SRSchannel = 300 -- radio channel + self.SRSModulation = radio.modulation.AM -- modulation + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the CSAR. Initializes parameters and starts event handlers. + -- @function [parent=#CSAR] Start + -- @param #CSAR self + + --- Triggers the FSM event "Start" after a delay. Starts the CSAR. Initializes parameters and starts event handlers. + -- @function [parent=#CSAR] __Start + -- @param #CSAR self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the CSAR and all its event handlers. + -- @param #CSAR self + + --- Triggers the FSM event "Stop" after a delay. Stops the CSAR and all its event handlers. + -- @function [parent=#CSAR] __Stop + -- @param #CSAR self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Status". + -- @function [parent=#CSAR] Status + -- @param #CSAR self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#CSAR] __Status + -- @param #CSAR self + -- @param #number delay Delay in seconds. + + --- On After "PilotDown" event. Downed Pilot detected. + -- @function [parent=#CSAR] OnAfterPilotDown + -- @param #CSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Group#GROUP Group Group object of the downed pilot. + -- @param #number Frequency Beacon frequency in kHz. + -- @param #string Leadername Name of the #UNIT of the downed pilot. + -- @param #string CoordinatesText String of the position of the pilot. Format determined by self.coordtype. + + --- On After "Aproach" event. Heli close to downed Pilot. + -- @function [parent=#CSAR] OnAfterApproach + -- @param #CSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @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. + -- @function [parent=#CSAR] OnAfterBoarded + -- @param #CSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string Heliname Name of the helicopter group. + -- @param #string Woundedgroupname Name of the downed pilot\'s group. + + --- On After "Returning" event. Heli can return home with downed pilot(s). + -- @function [parent=#CSAR] OnAfterReturning + -- @param #CSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string Heliname Name of the helicopter group. + -- @param #string Woundedgroupname Name of the downed pilot\'s group. + + --- On After "Rescued" event. Pilot(s) have been brought to the MASH/FARP/AFB. + -- @function [parent=#CSAR] OnAfterRescued + -- @param #CSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Unit#UNIT HeliUnit Unit of the helicopter. + -- @param #string HeliName Name of the helicopter group. + -- @param #number PilotsSaved Number of the saved pilots on board when landing. + + --- On After "KIA" event. Pilot is dead. + -- @function [parent=#CSAR] OnAfterKIA + -- @param #CSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string Pilotname Name of the pilot KIA. + + return self +end + +------------------------ +--- Helper Functions --- +------------------------ + +--- (Internal) Function to insert downed pilot tracker object. +-- @param #CSAR self +-- @param Wrapper.Group#GROUP Group The #GROUP object +-- @param #string Groupname Name of the spawned group. +-- @param #number Side Coalition. +-- @param #string OriginalUnit Name of original Unit. +-- @param #string Description Descriptive text. +-- @param #string Typename Typename of unit. +-- @param #number Frequency Frequency of the NDB in Hz +-- @param #string Playername Name of Player (if applicable) +-- @return #CSAR self. +function CSAR:_CreateDownedPilotTrack(Group,Groupname,Side,OriginalUnit,Description,Typename,Frequency,Playername) + self:T({"_CreateDownedPilotTrack",Groupname,Side,OriginalUnit,Description,Typename,Frequency,Playername}) + + -- create new entry + local DownedPilot = {} -- #CSAR.DownedPilot + DownedPilot.desc = Description or "" + DownedPilot.frequency = Frequency or 0 + DownedPilot.index = self.downedpilotcounter + DownedPilot.name = Groupname or "" + DownedPilot.originalUnit = OriginalUnit or "" + DownedPilot.player = Playername or "" + DownedPilot.side = Side or 0 + DownedPilot.typename = Typename or "" + DownedPilot.group = Group + DownedPilot.timestamp = 0 + DownedPilot.alive = true + + -- Add Pilot + local PilotTable = self.downedPilots + local counter = self.downedpilotcounter + PilotTable[counter] = {} + PilotTable[counter] = DownedPilot + self:T({Table=PilotTable}) + self.downedPilots = PilotTable + -- Increase counter + self.downedpilotcounter = self.downedpilotcounter+1 + return self +end + +--- (Internal) Count pilots on board. +-- @param #CSAR self +-- @param #string _heliName +-- @return #number count +function CSAR:_PilotsOnboard(_heliName) + self:T(self.lid .. " _PilotsOnboard") + local count = 0 + if self.inTransitGroups[_heliName] then + for _, _group in pairs(self.inTransitGroups[_heliName]) do + count = count + 1 + end + end + return count +end + +--- (Internal) Function to check for dupe eject events. +-- @param #CSAR self +-- @param #string _unitname Name of unit. +-- @return #boolean Outcome +function CSAR:_DoubleEjection(_unitname) + if self.lastCrash[_unitname] then + local _time = self.lastCrash[_unitname] + if timer.getTime() - _time < 10 then + self:E(self.lid.."Caught double ejection!") + return true + end + end + self.lastCrash[_unitname] = timer.getTime() + return false +end + +--- (Internal) Spawn a downed pilot +-- @param #CSAR self +-- @param #number country Country for template. +-- @param Core.Point#COORDINATE point Coordinate to spawn at. +-- @param #number frequency Frequency of the pilot's beacon +-- @return Wrapper.Group#GROUP group The #GROUP object. +-- @return #string alias The alias name. +function CSAR:_SpawnPilotInField(country,point,frequency) + self:T({country,point,frequency}) + local freq = frequency or 1000 + local freq = freq / 1000 -- kHz + for i=1,10 do + math.random(i,10000) + end + local template = self.template + local alias = string.format("Pilot %.2fkHz-%d", freq, math.random(1,99)) + local coalition = self.coalition + local pilotcacontrol = self.allowDownedPilotCAcontrol -- Switch AI on/oof - is this really correct for CA? + local _spawnedGroup = SPAWN + :NewWithAlias(template,alias) + :InitCoalition(coalition) + :InitCountry(country) + :InitAIOnOff(pilotcacontrol) + :InitDelayOff() + :SpawnFromCoordinate(point) + + return _spawnedGroup, alias -- Wrapper.Group#GROUP object +end + +--- (Internal) Add options to a downed pilot +-- @param #CSAR self +-- @param Wrapper.Group#GROUP group Group to use. +function CSAR:_AddSpecialOptions(group) + self:T(self.lid.." _AddSpecialOptions") + self:T({group}) + + local immortalcrew = self.immortalcrew + local invisiblecrew = self.invisiblecrew + if immortalcrew then + local _setImmortal = { + id = 'SetImmortal', + params = { + value = true + } + } + group:SetCommand(_setImmortal) + end + + if invisiblecrew then + local _setInvisible = { + id = 'SetInvisible', + params = { + value = true + } + } + group:SetCommand(_setInvisible) + end + + group:OptionAlarmStateGreen() + group:OptionROEHoldFire() + return self +end + +--- (Internal) Function to spawn a CSAR object into the scene. +-- @param #CSAR self +-- @param #number _coalition Coalition +-- @param DCS#country.id _country Country ID +-- @param Core.Point#COORDINATE _point Coordinate +-- @param #string _typeName Typename +-- @param #string _unitName Unitname +-- @param #string _playerName Playername +-- @param #number _freq Frequency +-- @param #boolean noMessage +-- @param #string _description Description +-- @param #boolean forcedesc Use the description only for the pilot track entry +function CSAR:_AddCsar(_coalition , _country, _point, _typeName, _unitName, _playerName, _freq, noMessage, _description, forcedesc ) + self:T(self.lid .. " _AddCsar") + self:T({_coalition , _country, _point, _typeName, _unitName, _playerName, _freq, noMessage, _description}) + + local template = self.template + + if not _freq then + _freq = self:_GenerateADFFrequency() + if not _freq then _freq = 333000 end --noob catch + end + + local _spawnedGroup, _alias = self:_SpawnPilotInField(_country,_point,_freq) + + local _typeName = _typeName or "Pilot" + + if not noMessage then + self:_DisplayToAllSAR("MAYDAY MAYDAY! " .. _typeName .. " is down. ", self.coalition, 10) + end + + if _freq then + self:_AddBeaconToGroup(_spawnedGroup, _freq) + end + + self:_AddSpecialOptions(_spawnedGroup) + + local _text = _description + if not forcedesc then + if _playerName ~= nil then + _text = "Pilot " .. _playerName + elseif _unitName ~= nil then + _text = "AI Pilot of " .. _unitName + end + end + self:T({_spawnedGroup, _alias}) + + local _GroupName = _spawnedGroup:GetName() or _alias + + self:_CreateDownedPilotTrack(_spawnedGroup,_GroupName,_coalition,_unitName,_text,_typeName,_freq,_playerName) + + self:_InitSARForPilot(_spawnedGroup, _GroupName, _freq, noMessage) + + return self +end + +--- (Internal) Function to add a CSAR object into the scene at a zone coordinate. For mission designers wanting to add e.g. PoWs to the scene. +-- @param #CSAR self +-- @param #string _zone Name of the zone. +-- @param #number _coalition Coalition. +-- @param #string _description (optional) Description. +-- @param #boolean _randomPoint (optional) Random yes or no. +-- @param #boolean _nomessage (optional) If true, don\'t send a message to SAR. +-- @param #string unitname (optional) Name of the lost unit. +-- @param #string typename (optional) Type of plane. +-- @param #boolean forcedesc (optional) Force to use the description passed only for the pilot track entry. Use to have fully custom names. +function CSAR:_SpawnCsarAtZone( _zone, _coalition, _description, _randomPoint, _nomessage, unitname, typename, forcedesc) + self:T(self.lid .. " _SpawnCsarAtZone") + local freq = self:_GenerateADFFrequency() + local _triggerZone = ZONE:New(_zone) -- trigger to use as reference position + if _triggerZone == nil then + self:E(self.lid.."ERROR: Can\'t find zone called " .. _zone, 10) + return + end + + local _description = _description or "PoW" + local unitname = unitname or "Old Rusty" + local typename = typename or "Phantom II" + + local pos = {} + if _randomPoint then + local _pos = _triggerZone:GetRandomPointVec3() + pos = COORDINATE:NewFromVec3(_pos) + else + pos = _triggerZone:GetCoordinate() + end + + local _country = 0 + if _coalition == coalition.side.BLUE then + _country = country.id.USA + elseif _coalition == coalition.side.RED then + _country = country.id.RUSSIA + else + _country = country.id.UN_PEACEKEEPERS + end + + self:_AddCsar(_coalition, _country, pos, typename, unitname, _description, freq, _nomessage, _description, forcedesc) + + return self +end + +--- Function to add a CSAR object into the scene at a zone coordinate. For mission designers wanting to add e.g. PoWs to the scene. +-- @param #CSAR self +-- @param #string Zone Name of the zone. +-- @param #number Coalition Coalition. +-- @param #string Description (optional) Description. +-- @param #boolean RandomPoint (optional) Random yes or no. +-- @param #boolean Nomessage (optional) If true, don\'t send a message to SAR. +-- @param #string Unitname (optional) Name of the lost unit. +-- @param #string Typename (optional) Type of plane. +-- @param #boolean Forcedesc (optional) Force to use the **description passed only** for the pilot track entry. Use to have fully custom names. +-- @usage If missions designers want to spawn downed pilots into the field, e.g. at mission begin, to give the helicopter guys work, they can do this like so: +-- +-- -- Create downed "Pilot Wagner" in #ZONE "CSAR_Start_1" at a random point for the blue coalition +-- my_csar:SpawnCSARAtZone( "CSAR_Start_1", coalition.side.BLUE, "Wagner", true, false, "Charly-1-1", "F5E" ) +function CSAR:SpawnCSARAtZone(Zone, Coalition, Description, RandomPoint, Nomessage, Unitname, Typename, Forcedesc) + self:_SpawnCsarAtZone(Zone, Coalition, Description, RandomPoint, Nomessage, Unitname, Typename, Forcedesc) + return self +end + +-- TODO: Split in functions per Event type +--- (Internal) Event handler. +-- @param #CSAR self +function CSAR:_EventHandler(EventData) + self:T(self.lid .. " _EventHandler") + self:T({Event = EventData.id}) + + local _event = EventData -- Core.Event#EVENTDATA + + -- no event + if _event == nil or _event.initiator == nil then + return false + + -- take off + elseif _event.id == EVENTS.Takeoff then -- taken off + self:T(self.lid .. " Event unit - Takeoff") + + local _coalition = _event.IniCoalition + if _coalition ~= self.coalition then + return --ignore! + end + + if _event.IniGroupName then + self.takenOff[_event.IniUnitName] = true + end + + return true + + -- 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 + if _coalition ~= self.coalition then + return --ignore! + end + + if _event.IniPlayerName then + self.takenOff[_event.IniPlayerName] = nil + end + + local _unit = _event.IniUnit + local _group = _event.IniGroup + if _unit:IsHelicopter() or _group:IsHelicopter() then + self:_AddMedevacMenuItem() + end + + return true + + elseif (_event.id == EVENTS.PilotDead and self.csarOncrash == false) then + -- Pilot dead + + self:T(self.lid .. " Event unit - Pilot Dead") + + local _unit = _event.IniUnit + local _unitname = _event.IniUnitName + local _group = _event.IniGroup + + if _unit == nil then + return -- error! + end + + local _coalition = _event.IniCoalition + if _coalition ~= self.coalition then + return --ignore! + end + + -- Catch multiple events here? + if self.takenOff[_event.IniUnitName] == true or _group:IsAirborne() then + if self:_DoubleEjection(_unitname) then + return + end + self:_DisplayToAllSAR("MAYDAY MAYDAY! " .. _unit:GetTypeName() .. " shot down. No Chute!", self.coalition, 10) + --local m = MESSAGE:New("MAYDAY MAYDAY! " .. _unit:GetTypeName() .. " shot down. No Chute!",10,"Info"):ToCoalition(self.coalition) + else + self:T(self.lid .. " Pilot has not taken off, ignore") + end + + return + + elseif _event.id == EVENTS.PilotDead or _event.id == EVENTS.Ejection then + if _event.id == EVENTS.PilotDead and self.csarOncrash == false then + return + end + self:T(self.lid .. " Event unit - Pilot Ejected") + + local _unit = _event.IniUnit + local _unitname = _event.IniUnitName + local _group = _event.IniGroup + + if _unit == nil then + return -- error! + end + + local _coalition = _unit:GetCoalition() + if _coalition ~= self.coalition then + return --ignore! + end + + if self.enableForAI == false and _event.IniPlayerName == nil then + return + end + + 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 + end + + if self:_DoubleEjection(_unitname) then + return + end + + -- limit no of pilots in the field. + if self.limitmaxdownedpilots and self:_ReachedPilotLimit() then + return + end + + -- all checks passed, get going. + local _freq = self:_GenerateADFFrequency() + self:_AddCsar(_coalition, _unit:GetCountry(), _unit:GetCoordinate() , _unit:GetTypeName(), _unit:GetName(), _event.IniPlayerName, _freq, false, "none") + + return true + + elseif _event.id == EVENTS.Land then + self:T(self.lid .. " Landing") + + if _event.IniUnitName then + self.takenOff[_event.IniUnitName] = nil + end + + if self.allowFARPRescue then + + local _unit = _event.IniUnit -- Wrapper.Unit#UNIT + + if _unit == nil then + self:T(self.lid .. " Unit nil on landing") + return -- error! + end + + local _coalition = _event.IniCoalition + if _coalition ~= self.coalition then + return --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! + end + + if _place:GetCoalition() == self.coalition or _place:GetCoalition() == coalition.side.NEUTRAL then + if self.pilotmustopendoors and not self:_IsLoadingDoorOpen(_event.IniUnitName) then + self:_DisplayMessageToSAR(_unit, "Open the door to let me out!", self.messageTime, true) + else + self:_RescuePilots(_unit) + end + else + self:T(string.format("Airfield %d, Unit %d", _place:GetCoalition(), _unit:GetCoalition())) + end + end + + return true + end + return self +end + +--- (Internal) Initialize the action for a pilot. +-- @param #CSAR self +-- @param Wrapper.Group#GROUP _downedGroup The group to rescue. +-- @param #string _GroupName Name of the Group +-- @param #number _freq Beacon frequency. +-- @param #boolean _nomessage Send message true or false. +function CSAR:_InitSARForPilot(_downedGroup, _GroupName, _freq, _nomessage) + self:T(self.lid .. " _InitSARForPilot") + local _leader = _downedGroup:GetUnit(1) + --local _groupName = _downedGroup:GetName() + local _groupName = _GroupName + local _freqk = _freq / 1000 + local _coordinatesText = self:_GetPositionOfWounded(_downedGroup) + local _leadername = _leader:GetName() + + if not _nomessage then + local _text = string.format("%s requests SAR at %s, beacon at %.2f KHz", _leadername, _coordinatesText, _freqk) + self:_DisplayToAllSAR(_text,self.coalition,self.messageTime) + end + + for _,_heliName in pairs(self.csarUnits) do + self:_CheckWoundedGroupStatus(_heliName, _groupName) + end + + -- trigger FSM event + self:__PilotDown(2,_downedGroup, _freqk, _leadername, _coordinatesText) + + return self +end + +--- (Internal) Check if a name is in downed pilot table +-- @param #CSAR self +-- @param #string name Name to search for. +-- @return #boolean Outcome. +-- @return #CSAR.DownedPilot Table if found else nil. +function CSAR:_CheckNameInDownedPilots(name) + local PilotTable = self.downedPilots --#CSAR.DownedPilot + local found = false + local table = nil + for _,_pilot in pairs(PilotTable) do + if _pilot.name == name and _pilot.alive == true then + found = true + table = _pilot + break + end + end + return found, table +end + +--- (Internal) Check if a name is in downed pilot table and remove it. +-- @param #CSAR self +-- @param #string name Name to search for. +-- @param #boolean force Force removal. +-- @return #boolean Outcome. +function CSAR:_RemoveNameFromDownedPilots(name,force) + local PilotTable = self.downedPilots --#CSAR.DownedPilot + local found = false + for _index,_pilot in pairs(PilotTable) do + if _pilot.name == name then + self.downedPilots[_index].alive = false + end + end + return found +end + +--- (Internal) Check state of wounded group. +-- @param #CSAR self +-- @param #string heliname heliname +-- @param #string woundedgroupname woundedgroupname +function CSAR:_CheckWoundedGroupStatus(heliname,woundedgroupname) + self:T(self.lid .. " _CheckWoundedGroupStatus") + local _heliName = heliname + local _woundedGroupName = woundedgroupname + self:T({Heli = _heliName, Downed = _woundedGroupName}) + -- if wounded group is not here then message already been sent to SARs + -- stop processing any further + local _found, _downedpilot = self:_CheckNameInDownedPilots(_woundedGroupName) + if not _found then + self:T("...not found in list!") + return + end + + local _woundedGroup = _downedpilot.group + if _woundedGroup ~= nil and _woundedGroup:IsAlive() then + local _heliUnit = self:_GetSARHeli(_heliName) -- Wrapper.Unit#UNIT + + local _lookupKeyHeli = _heliName .. "_" .. _woundedGroupName --lookup key for message state tracking + + if _heliUnit == nil then + self.heliVisibleMessage[_lookupKeyHeli] = nil + self.heliCloseMessage[_lookupKeyHeli] = nil + self.landedStatus[_lookupKeyHeli] = nil + self:T("...helinunit nil!") + return + end + + local _heliCoord = _heliUnit:GetCoordinate() + local _leaderCoord = _woundedGroup:GetCoordinate() + local _distance = self:_GetDistance(_heliCoord,_leaderCoord) + if _distance < self.approachdist_near and _distance > 0 then + if self:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedGroup, _woundedGroupName) == true then + -- we\'re close, reschedule + _downedpilot.timestamp = timer.getAbsTime() + self:__Approach(-5,heliname,woundedgroupname) + end + elseif _distance >= self.approachdist_near and _distance < self.approachdist_far then + self.heliVisibleMessage[_lookupKeyHeli] = nil + --reschedule as units aren\'t dead yet , schedule for a bit slower though as we\'re far away + _downedpilot.timestamp = timer.getAbsTime() + self:__Approach(-10,heliname,woundedgroupname) + end + else + self:T("...Downed Pilot KIA?!") + if not _downedpilot.alive then + --self:__KIA(1,_downedpilot.name) + self:_RemoveNameFromDownedPilots(_downedpilot.name, true) + end + end + return self +end + +--- (Internal) Function to pop a smoke at a wounded pilot\'s positions. +-- @param #CSAR self +-- @param #string _woundedGroupName Name of the group. +-- @param Wrapper.Group#GROUP _woundedLeader Object of the group. +function CSAR:_PopSmokeForGroup(_woundedGroupName, _woundedLeader) + self:T(self.lid .. " _PopSmokeForGroup") + -- have we popped smoke already in the last 5 mins + local _lastSmoke = self.smokeMarkers[_woundedGroupName] + if _lastSmoke == nil or timer.getTime() > _lastSmoke then + + local _smokecolor = self.smokecolor + local _smokecoord = _woundedLeader:GetCoordinate() + _smokecoord:Smoke(_smokecolor) + self.smokeMarkers[_woundedGroupName] = timer.getTime() + 300 -- next smoke time + end + return self +end + +--- (Internal) Function to pickup the wounded pilot from the ground. +-- @param #CSAR self +-- @param Wrapper.Unit#UNIT _heliUnit Object of the group. +-- @param #string _pilotName Name of the pilot. +-- @param Wrapper.Group#GROUP _woundedGroup Object of the group. +-- @param #string _woundedGroupName Name of the group. +function CSAR:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) + self:T(self.lid .. " _PickupUnit") + -- board + local _heliName = _heliUnit:GetName() + local _groups = self.inTransitGroups[_heliName] + local _unitsInHelicopter = self:_PilotsOnboard(_heliName) + + -- init table if there is none for this helicopter + if not _groups then + self.inTransitGroups[_heliName] = {} + _groups = self.inTransitGroups[_heliName] + end + + -- if the heli can\'t pick them up, show a message and return + local _maxUnits = self.AircraftType[_heliUnit:GetTypeName()] + if _maxUnits == nil then + _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 + end + + local found,downedgrouptable = self:_CheckNameInDownedPilots(_woundedGroupName) + local grouptable = downedgrouptable --#CSAR.DownedPilot + self.inTransitGroups[_heliName][_woundedGroupName] = + { + -- DONE: Fix with #CSAR.DownedPilot + originalUnit = grouptable.originalUnit, + woundedGroup = _woundedGroupName, + side = self.coalition, + desc = grouptable.desc, + player = grouptable.player, + } + + _woundedGroup:Destroy() + self:_RemoveNameFromDownedPilots(_woundedGroupName,true) + + self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s I\'m in! Get to the MASH ASAP! ", _heliName, _pilotName), self.messageTime,true,true) + + self:__Boarded(5,_heliName,_woundedGroupName) + + return true +end + +--- (Internal) Move group to destination. +-- @param #CSAR self +-- @param Wrapper.Group#GROUP _leader +-- @param Core.Point#COORDINATE _destination +function CSAR:_OrderGroupToMoveToPoint(_leader, _destination) + self:T(self.lid .. " _OrderGroupToMoveToPoint") + local group = _leader + local coordinate = _destination:GetVec2() + + group:SetAIOn() + group:RouteToVec2(coordinate,5) + return self +end + + +--- (internal) Function to check if the heli door(s) are open. Thanks to Shadowze. +-- @param #CSAR self +-- @param #string unit_name Name of unit. +-- @return #boolean outcome The outcome. +function CSAR:_IsLoadingDoorOpen( unit_name ) + self:T(self.lid .. " _IsLoadingDoorOpen") + local ret_val = false + local unit = Unit.getByName(unit_name) + if unit ~= nil then + local type_name = unit:getTypeName() + + if type_name == "Mi-8MT" and unit:getDrawArgumentValue(86) == 1 or unit:getDrawArgumentValue(250) == 1 then + self:T(unit_name .. " Cargo doors are open or cargo door not present") + ret_val = true + end + + if type_name == "Mi-24P" and unit:getDrawArgumentValue(38) == 1 or unit:getDrawArgumentValue(86) == 1 then + self:T(unit_name .. " a side door is open") + ret_val = true + end + + if type_name == "UH-1H" and unit:getDrawArgumentValue(43) == 1 or unit:getDrawArgumentValue(44) == 1 then + self:T(unit_name .. " a side door is open ") + ret_val = true + end + + if string.find(type_name, "SA342" ) and unit:getDrawArgumentValue(34) == 1 or unit:getDrawArgumentValue(38) == 1 then + self:T(unit_name .. " front door(s) are open") + ret_val = true + end + + if ret_val == false then + self:T(unit_name .. " all doors are closed") + end + return ret_val + + end -- nil + + return false +end + +--- (Internal) Function to check if heli is close to group. +-- @param #CSAR self +-- @param #number _distance +-- @param Wrapper.Unit#UNIT _heliUnit +-- @param #string _heliName +-- @param Wrapper.Group#GROUP _woundedGroup +-- @param #string _woundedGroupName +-- @return #boolean Outcome +function CSAR:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedGroup, _woundedGroupName) + self:T(self.lid .. " _CheckCloseWoundedGroup") + + local _woundedLeader = _woundedGroup + local _lookupKeyHeli = _heliUnit:GetName() .. "_" .. _woundedGroupName --lookup key for message state tracking + + local _found, _pilotable = self:_CheckNameInDownedPilots(_woundedGroupName) -- #boolean, #CSAR.DownedPilot + local _pilotName = _pilotable.desc + + + local _reset = true + + if (self.autosmoke == true) and (_distance < self.autosmokedistance) then + self:_PopSmokeForGroup(_woundedGroupName, _woundedLeader) + end + + if self.heliVisibleMessage[_lookupKeyHeli] == nil then + if self.autosmoke == true then + self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. I hear you! Damn, that thing is loud! Land or hover by the smoke.", _heliName, _pilotName), self.messageTime,true,true) + else + self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. I hear you! Damn, that thing is loud! Request a Flare or Smoke if you need", _heliName, _pilotName), self.messageTime,true,true) + end + --mark as shown for THIS heli and THIS group + self.heliVisibleMessage[_lookupKeyHeli] = true + end + + if (_distance < 500) then + + if self.heliCloseMessage[_lookupKeyHeli] == nil then + if self.autosmoke == true then + self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. You\'re close now! Land or hover at the smoke.", _heliName, _pilotName), self.messageTime,true,true) + else + self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. You\'re close now! Land in a safe place, I will go there ", _heliName, _pilotName), self.messageTime,true,true) + end + --mark as shown for THIS heli and THIS group + self.heliCloseMessage[_lookupKeyHeli] = true + end + + -- have we landed close enough? + if not _heliUnit:InAir() then + + -- if you land on them, doesnt matter if they were heading to someone else as you\'re closer, you win! :) + if self.pilotRuntoExtractPoint == true then + if (_distance < self.extractDistance) then + local _time = self.landedStatus[_lookupKeyHeli] + if _time == nil then + self.landedStatus[_lookupKeyHeli] = math.floor( (_distance - self.loadDistance) / 3.6 ) + _time = self.landedStatus[_lookupKeyHeli] + self:_OrderGroupToMoveToPoint(_woundedGroup, _heliUnit:GetCoordinate()) + self:_DisplayMessageToSAR(_heliUnit, "Wait till " .. _pilotName .. " gets in. \nETA " .. _time .. " more seconds.", self.messageTime, true) + else + _time = self.landedStatus[_lookupKeyHeli] - 10 + self.landedStatus[_lookupKeyHeli] = _time + end + if _time <= 0 or _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 + else + self.landedStatus[_lookupKeyHeli] = nil + self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) + return false + 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 + else + self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) + return false + end + end + end + else + + local _unitsInHelicopter = self:_PilotsOnboard(_heliName) + local _maxUnits = self.AircraftType[_heliUnit:GetTypeName()] + if _maxUnits == nil then + _maxUnits = self.max_units + end + + if _heliUnit:InAir() and _unitsInHelicopter + 1 <= _maxUnits then + + if _distance < 8.0 then + + --check height! + local leaderheight = _woundedLeader:GetHeight() + if leaderheight < 0 then leaderheight = 0 end + local _height = _heliUnit:GetHeight() - leaderheight + + if _height <= 20.0 then + + local _time = self.hoverStatus[_lookupKeyHeli] + + if _time == nil then + self.hoverStatus[_lookupKeyHeli] = 10 + _time = 10 + else + _time = self.hoverStatus[_lookupKeyHeli] - 10 + self.hoverStatus[_lookupKeyHeli] = _time + end + + 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 + else + self.hoverStatus[_lookupKeyHeli] = nil + self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) + return false + end + end + _reset = false + else + self:_DisplayMessageToSAR(_heliUnit, "Too high to winch " .. _pilotName .. " \nReduce height and hover for 10 seconds!", self.messageTime, true,true) + end + end + + end + end + end + + if _reset then + self.hoverStatus[_lookupKeyHeli] = nil + end + + if _distance < 500 then + return true + else + return false + end +end + +--- (Internal) Monitor in-flight returning groups. +-- @param #CSAR self +-- @param #string heliname Heli name +-- @param #string groupname Group name +function CSAR:_ScheduledSARFlight(heliname,groupname) + self:T(self.lid .. " _ScheduledSARFlight") + self:T({heliname,groupname}) + local _heliUnit = self:_GetSARHeli(heliname) + local _woundedGroupName = groupname + + if (_heliUnit == nil) then + --helicopter crashed? + self.inTransitGroups[heliname] = nil + return + end + + if self.inTransitGroups[heliname] == nil or self.inTransitGroups[heliname][_woundedGroupName] == nil then + -- Groups already rescued + return + end + + local _dist = self:_GetClosestMASH(_heliUnit) + + if _dist == -1 then + return + end + + if _dist < 200 and _heliUnit:InAir() == false then + if self.pilotmustopendoors and not self:_IsLoadingDoorOpen(heliname) then + self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me out!", self.messageTime, true) + else + self:_RescuePilots(_heliUnit) + return + end + end + + --queue up + self:__Returning(-5,heliname,_woundedGroupName) + return self +end + +--- (Internal) Mark pilot as rescued and remove from tables. +-- @param #CSAR self +-- @param Wrapper.Unit#UNIT _heliUnit +function CSAR:_RescuePilots(_heliUnit) + self:T(self.lid .. " _RescuePilots") + local _heliName = _heliUnit:GetName() + local _rescuedGroups = self.inTransitGroups[_heliName] + + if _rescuedGroups == nil then + -- Groups already rescued + return + end + + -- DONE: count saved units? + local PilotsSaved = self:_PilotsOnboard(_heliName) + + self.inTransitGroups[_heliName] = nil + + local _txt = string.format("%s: The %d pilot(s) have been taken to the\nmedical clinic. Good job!", _heliName, PilotsSaved) + + self:_DisplayMessageToSAR(_heliUnit, _txt, self.messageTime) + -- trigger event + self:__Rescued(-1,_heliUnit,_heliName, PilotsSaved) + return self +end + +--- (Internal) Check and return Wrappe.Unit#UNIT based on the name if alive. +-- @param #CSAR self +-- @param #string _unitname Name of Unit +-- @return Wrapper.Unit#UNIT The unit or nil +function CSAR:_GetSARHeli(_unitName) + self:T(self.lid .. " _GetSARHeli") + local unit = UNIT:FindByName(_unitName) + if unit and unit:IsAlive() then + return unit + else + return nil + end +end + +--- (Internal) Display message to single Unit. +-- @param #CSAR self +-- @param Wrapper.Unit#UNIT _unit Unit #UNIT to display to. +-- @param #string _text Text of message. +-- @param #number _time Message show duration. +-- @param #boolean _clear (optional) Clear screen. +-- @param #boolean _speak (optional) Speak message via SRS. +function CSAR:_DisplayMessageToSAR(_unit, _text, _time, _clear, _speak) + self:T(self.lid .. " _DisplayMessageToSAR") + local group = _unit:GetGroup() + local _clear = _clear or nil + local _time = _time or self.messageTime + local m = MESSAGE:New(_text,_time,"Info",_clear):ToGroup(group) + -- integrate SRS + if _speak and self.useSRS then + local srstext = SOUNDTEXT:New(_text) + local path = self.SRSPath + local modulation = self.SRSModulation + local channel = self.SRSchannel + local msrs = MSRS:New(path,channel,modulation) + msrs:PlaySoundText(srstext, 2) + end + return self +end + +--- (Internal) Function to get string of a group\'s position. +-- @param #CSAR self +-- @param Wrapper.Controllable#CONTROLLABLE _woundedGroup Group or Unit object. +-- @return #string Coordinates as Text +function CSAR:_GetPositionOfWounded(_woundedGroup) + self:T(self.lid .. " _GetPositionOfWounded") + local _coordinate = _woundedGroup:GetCoordinate() + local _coordinatesText = "None" + if _coordinate then + if self.coordtype == 0 then -- Lat/Long DMTM + _coordinatesText = _coordinate:ToStringLLDDM() + elseif self.coordtype == 1 then -- Lat/Long DMS + _coordinatesText = _coordinate:ToStringLLDMS() + elseif self.coordtype == 2 then -- MGRS + _coordinatesText = _coordinate:ToStringMGRS() + else -- Bullseye Metric --(medevac.coordtype == 4 or 3) + _coordinatesText = _coordinate:ToStringBULLS(self.coalition) + end + end + return _coordinatesText +end + +--- (Internal) Display active SAR tasks to player. +-- @param #CSAR self +-- @param #string _unitName Unit to display to +function CSAR:_DisplayActiveSAR(_unitName) + self:T(self.lid .. " _DisplayActiveSAR") + local _msg = "Active MEDEVAC/SAR:" + local _heli = self:_GetSARHeli(_unitName) -- Wrapper.Unit#UNIT + if _heli == nil then + return + end + + local _heliSide = self.coalition + local _csarList = {} + + local _DownedPilotTable = self.downedPilots + self:T({Table=_DownedPilotTable}) + for _, _value in pairs(_DownedPilotTable) do + local _groupName = _value.name + self:T(string.format("Display Active Pilot: %s", tostring(_groupName))) + self:T({Table=_value}) + --local _woundedGroup = GROUP:FindByName(_groupName) + local _woundedGroup = _value.group + if _woundedGroup and _value.alive then + local _coordinatesText = self:_GetPositionOfWounded(_woundedGroup) + local _helicoord = _heli:GetCoordinate() + local _woundcoord = _woundedGroup:GetCoordinate() + local _distance = self:_GetDistance(_helicoord, _woundcoord) + self:T({_distance = _distance}) + local distancetext = "" + if _SETTINGS:IsImperial() then + distancetext = string.format("%.1fnm",UTILS.MetersToNM(_distance)) + else + distancetext = string.format("%.1fkm", _distance/1000.0) + end + table.insert(_csarList, { dist = _distance, msg = string.format("%s at %s - %.2f KHz ADF - %s ", _value.desc, _coordinatesText, _value.frequency / 1000, distancetext) }) + end + end + + local function sortDistance(a, b) + return a.dist < b.dist + end + + table.sort(_csarList, sortDistance) + + for _, _line in pairs(_csarList) do + _msg = _msg .. "\n" .. _line.msg + end + + self:_DisplayMessageToSAR(_heli, _msg, self.messageTime*2) + return self +end + +--- (Internal) Find the closest downed pilot to a heli. +-- @param #CSAR self +-- @param Wrapper.Unit#UNIT _heli Helicopter #UNIT +-- @return #table Table of results +function CSAR:_GetClosestDownedPilot(_heli) + self:T(self.lid .. " _GetClosestDownedPilot") + local _side = self.coalition + local _closestGroup = nil + local _shortestDistance = -1 + local _distance = 0 + local _closestGroupInfo = nil + local _heliCoord = _heli:GetCoordinate() + + local DownedPilotsTable = self.downedPilots + for _, _groupInfo in pairs(DownedPilotsTable) do + local _woundedName = _groupInfo.name + local _tempWounded = _groupInfo.group + + -- check group exists and not moving to someone else + if _tempWounded then + local _tempCoord = _tempWounded:GetCoordinate() + _distance = self:_GetDistance(_heliCoord, _tempCoord) + + if _distance ~= nil and (_shortestDistance == -1 or _distance < _shortestDistance) then + _shortestDistance = _distance + _closestGroup = _tempWounded + _closestGroupInfo = _groupInfo + end + end + end + + return { pilot = _closestGroup, distance = _shortestDistance, groupInfo = _closestGroupInfo } +end + +--- (Internal) Fire a flare at the point of a downed pilot. +-- @param #CSAR self +-- @param #string _unitName Name of the unit. +function CSAR:_SignalFlare(_unitName) + self:T(self.lid .. " _SignalFlare") + local _heli = self:_GetSARHeli(_unitName) + if _heli == nil then + return + end + + local _closest = self:_GetClosestDownedPilot(_heli) + + if _closest ~= nil and _closest.pilot ~= nil and _closest.distance < 8000.0 then + + local _clockDir = self:_GetClockDirection(_heli, _closest.pilot) + local _distance = 0 + if _SETTINGS:IsImperial() then + _distance = string.format("%.1fnm",UTILS.MetersToNM(_closest.distance)) + else + _distance = string.format("%.1fkm",_closest.distance) + end + local _msg = string.format("%s - Popping signal flare at your %s o\'clock. Distance %s", _unitName, _clockDir, _distance) + self:_DisplayMessageToSAR(_heli, _msg, self.messageTime, false, true) + + local _coord = _closest.pilot:GetCoordinate() + _coord:FlareRed(_clockDir) + else + local disttext = "4.3nm" + if _SETTINGS:IsMetric() then + disttext = "8km" + end + self:_DisplayMessageToSAR(_heli, string.format("No Pilots within %s",disttext), self.messageTime) + end + return self +end + +--- (Internal) Display info to all SAR groups. +-- @param #CSAR self +-- @param #string _message Message to display. +-- @param #number _side Coalition of message. +-- @param #number _messagetime How long to show. +function CSAR:_DisplayToAllSAR(_message, _side, _messagetime) + self:T(self.lid .. " _DisplayToAllSAR") + for _, _unitName in pairs(self.csarUnits) do + local _unit = self:_GetSARHeli(_unitName) + if _unit then + if not _messagetime then + self:_DisplayMessageToSAR(_unit, _message, _messagetime) + end + end + end + return self +end + +---(Internal) Request smoke at closest downed pilot. +--@param #CSAR self +--@param #string _unitName Name of the helicopter +function CSAR:_Reqsmoke( _unitName ) + self:T(self.lid .. " _Reqsmoke") + local _heli = self:_GetSARHeli(_unitName) + if _heli == nil then + return + end + local _closest = self:_GetClosestDownedPilot(_heli) + if _closest ~= nil and _closest.pilot ~= nil and _closest.distance < 8000.0 then + local _clockDir = self:_GetClockDirection(_heli, _closest.pilot) + local _distance = 0 + if _SETTINGS:IsImperial() then + _distance = string.format("%.1fnm",UTILS.MetersToNM(_closest.distance)) + else + _distance = string.format("%.1fkm",_closest.distance) + end + local _msg = string.format("%s - Popping signal smoke at your %s o\'clock. Distance %s", _unitName, _clockDir, _distance) + self:_DisplayMessageToSAR(_heli, _msg, self.messageTime, false, true) + local _coord = _closest.pilot:GetCoordinate() + local color = self.smokecolor + _coord:Smoke(color) + else + local disttext = "4.3nm" + if _SETTINGS:IsMetric() then + disttext = "8km" + end + self:_DisplayMessageToSAR(_heli, string.format("No Pilots within %s",disttext), self.messageTime) + end + return self +end + +--- (Internal) Determine distance to closest MASH. +-- @param #CSAR self +-- @param Wrapper.Unit#UNIT _heli Helicopter #UNIT +-- @retunr +function CSAR:_GetClosestMASH(_heli) + self:T(self.lid .. " _GetClosestMASH") + local _mashset = self.mash -- Core.Set#SET_GROUP + local _mashes = _mashset:GetSetObjects() -- #table + local _shortestDistance = -1 + local _distance = 0 + local _helicoord = _heli:GetCoordinate() + + local function GetCloseAirbase(coordinate,Coalition,Category) + + local a=coordinate:GetVec3() + local distmin=math.huge + local airbase=nil + for DCSairbaseID, DCSairbase in pairs(world.getAirbases(Coalition)) do + local b=DCSairbase:getPoint() + + local c=UTILS.VecSubstract(a,b) + local dist=UTILS.VecNorm(c) + + if dist 0 then + local PilotTable = self.downedPilots + for _,_pilot in pairs (PilotTable) do + self:T({_pilot}) + local pilot = _pilot -- #CSAR.DownedPilot + local group = pilot.group + local frequency = pilot.frequency or 0 -- thanks to @Thrud + if group and group:IsAlive() and frequency > 0 then + self:_AddBeaconToGroup(group,frequency) + end + end + end + return self +end + +--- (Internal) Helper function to count active downed pilots. +-- @param #CSAR self +-- @return #number Number of pilots in the field. +function CSAR:_CountActiveDownedPilots() + self:T(self.lid .. " _CountActiveDownedPilots") + local PilotsInFieldN = 0 + for _, _unitName in pairs(self.downedPilots) do + self:T({_unitName.desc}) + if _unitName.alive == true then + PilotsInFieldN = PilotsInFieldN + 1 + end + end + return PilotsInFieldN +end + +--- (Internal) Helper to decide if we're over max limit. +-- @param #CSAR self +-- @return #boolean True or false. +function CSAR:_ReachedPilotLimit() + self:T(self.lid .. " _ReachedPilotLimit") + local limit = self.maxdownedpilots + local islimited = self.limitmaxdownedpilots + local count = self:_CountActiveDownedPilots() + if islimited and (count >= limit) then + return true + else + return false + end +end + + ------------------------------ + --- FSM internal Functions --- + ------------------------------ + +--- (Internal) Function called after Start() event. +-- @param #CSAR self. +-- @param #string From From state. +-- @param #string Event Event triggered. +-- @param #string To To state. +function CSAR:onafterStart(From, Event, To) + self:T({From, Event, To}) + self:I(self.lid .. "Started.") + -- event handler + self:HandleEvent(EVENTS.Takeoff, self._EventHandler) + self:HandleEvent(EVENTS.Land, self._EventHandler) + self:HandleEvent(EVENTS.Ejection, self._EventHandler) + self:HandleEvent(EVENTS.PlayerEnterAircraft, self._EventHandler) + self:HandleEvent(EVENTS.PlayerEnterUnit, self._EventHandler) + self:HandleEvent(EVENTS.PilotDead, self._EventHandler) + if self.useprefix then + local prefixes = self.csarPrefix or {} + self.allheligroupset = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(prefixes):FilterCategoryHelicopter():FilterStart() + else + self.allheligroupset = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterCategoryHelicopter():FilterStart() + end + self:__Status(-10) + return self +end + +--- (Internal) Function called before Status() event. +-- @param #CSAR self +function CSAR:_CheckDownedPilotTable() + local pilots = self.downedPilots + for _,_entry in pairs (pilots) do + self:T("Checking for " .. _entry.name) + self:T({entry=_entry}) + local group = _entry.group + if not group:IsAlive() then + self:T("Group is dead") + if _entry.alive == true then + self:T("Switching .alive to false") + self:__KIA(1,_entry.desc) + self:_RemoveNameFromDownedPilots(_entry.name,true) + end + end + end + return self +end + +--- (Internal) Function called before Status() event. +-- @param #CSAR self. +-- @param #string From From state. +-- @param #string Event Event triggered. +-- @param #string To To state. +function CSAR:onbeforeStatus(From, Event, To) + self:T({From, Event, To}) + -- housekeeping + self:_AddMedevacMenuItem() + self:_RefreshRadioBeacons() + self:_CheckDownedPilotTable() + for _,_sar in pairs (self.csarUnits) do + local PilotTable = self.downedPilots + for _,_entry in pairs (PilotTable) do + if _entry.alive then + local entry = _entry -- #CSAR.DownedPilot + local name = entry.name + local timestamp = entry.timestamp or 0 + local now = timer.getAbsTime() + if now - timestamp > 17 then -- only check if we\'re not in approach mode, which is iterations of 5 and 10. + self:_CheckWoundedGroupStatus(_sar,name) + end + end + end + end + return self +end + +--- (Internal) Function called after Status() event. +-- @param #CSAR self. +-- @param #string From From state. +-- @param #string Event Event triggered. +-- @param #string To To state. +function CSAR:onafterStatus(From, Event, To) + self:T({From, Event, To}) + -- collect some stats + local NumberOfSARPilots = 0 + for _, _unitName in pairs(self.csarUnits) do + NumberOfSARPilots = NumberOfSARPilots + 1 + end + + local PilotsInFieldN = self:_CountActiveDownedPilots() + + local PilotsBoarded = 0 + for _, _unitName in pairs(self.inTransitGroups) do + for _,_units in pairs(_unitName) do + PilotsBoarded = PilotsBoarded + 1 + end + end + + if self.verbose > 0 then + local text = string.format("%s Active SAR: %d | Downed Pilots in field: %d (max %d) | Pilots boarded: %d | Landings: %d | Pilots rescued: %d", + self.lid,NumberOfSARPilots,PilotsInFieldN,self.maxdownedpilots,PilotsBoarded,self.rescues,self.rescuedpilots) + self:T(text) + if self.verbose < 2 then + self:I(text) + elseif self.verbose > 1 then + self:I(text) + local m = MESSAGE:New(text,"10","Status",true):ToCoalition(self.coalition) + end + end + self:__Status(-20) + return self +end + +--- (Internal) Function called after Stop() event. +-- @param #CSAR self. +-- @param #string From From state. +-- @param #string Event Event triggered. +-- @param #string To To state. +function CSAR:onafterStop(From, Event, To) + self:T({From, Event, To}) + -- event handler + self:UnHandleEvent(EVENTS.Takeoff) + self:UnHandleEvent(EVENTS.Land) + self:UnHandleEvent(EVENTS.Ejection) + self:UnHandleEvent(EVENTS.PlayerEnterUnit) + self:UnHandleEvent(EVENTS.PlayerEnterAircraft) + self:UnHandleEvent(EVENTS.PilotDead) + self:T(self.lid .. "Stopped.") + return self +end + +--- (Internal) Function called before Approach() 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 helicopter group. +-- @param #string Woundedgroupname Name of the downed pilot\'s group. +function CSAR:onbeforeApproach(From, Event, To, Heliname, Woundedgroupname) + self:T({From, Event, To, Heliname, Woundedgroupname}) + self:_CheckWoundedGroupStatus(Heliname,Woundedgroupname) + return self +end + +--- (Internal) Function called before Boarded() 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 helicopter group. +-- @param #string Woundedgroupname Name of the downed pilot\'s group. +function CSAR:onbeforeBoarded(From, Event, To, Heliname, Woundedgroupname) + self:T({From, Event, To, Heliname, Woundedgroupname}) + self:_ScheduledSARFlight(Heliname,Woundedgroupname) + return self +end + +--- (Internal) Function called before Returning() 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 helicopter group. +-- @param #string Woundedgroupname Name of the downed pilot\'s group. +function CSAR:onbeforeReturning(From, Event, To, Heliname, Woundedgroupname) + self:T({From, Event, To, Heliname, Woundedgroupname}) + self:_ScheduledSARFlight(Heliname,Woundedgroupname) + return self +end + +--- (Internal) Function called before Rescued() event. +-- @param #CSAR self. +-- @param #string From From state. +-- @param #string Event Event triggered. +-- @param #string To To state. +-- @param Wrapper.Unit#UNIT HeliUnit Unit of the helicopter. +-- @param #string HeliName Name of the helicopter group. +-- @param #number PilotsSaved Number of the saved pilots on board when landing. +function CSAR:onbeforeRescued(From, Event, To, HeliUnit, HeliName, PilotsSaved) + self:T({From, Event, To, HeliName, HeliUnit}) + self.rescues = self.rescues + 1 + self.rescuedpilots = self.rescuedpilots + PilotsSaved + return self +end + +--- (Internal) Function called before PilotDown() event. +-- @param #CSAR self. +-- @param #string From From state. +-- @param #string Event Event triggered. +-- @param #string To To state. +-- @param Wrapper.Group#GROUP Group Group object of the downed pilot. +-- @param #number Frequency Beacon frequency in kHz. +-- @param #string Leadername Name of the #UNIT of the downed pilot. +-- @param #string CoordinatesText String of the position of the pilot. Format determined by self.coordtype. +function CSAR:onbeforePilotDown(From, Event, To, Group, Frequency, Leadername, CoordinatesText) + self:T({From, Event, To, Group, Frequency, Leadername, CoordinatesText}) + return self +end +-------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- End Ops.CSAR +-------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua new file mode 100644 index 000000000..9143eacf1 --- /dev/null +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -0,0 +1,2559 @@ +--- **Ops** -- Combat Troops & Logistics Deployment. +-- +-- === +-- +-- **CTLD** - MOOSE based Helicopter CTLD Operations. +-- +-- === +-- +-- ## Missions: +-- +-- ### [CTLD - Combat Troop & Logistics Deployment](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20CTLD) +-- +-- === +-- +-- **Main Features:** +-- +-- * MOOSE-based Helicopter CTLD Operations for Players. +-- +-- === +-- +-- ### Author: **Applevangelist** (Moose Version), ***Ciribob*** (original), Thanks to: Shadowze, Cammel (testing) +-- @module Ops.CTLD +-- @image OPS_CTLD.jpg + +-- Date: July 2021 + +do +------------------------------------------------------ +--- **CTLD_CARGO** class, extends #Core.Base#BASE +-- @type CTLD_CARGO +-- @field #number ID ID of this cargo. +-- @field #string Name Name for menu. +-- @field #table Templates Table of #POSITIONABLE objects. +-- @field #CTLD_CARGO.Enum Type Enumerator of Type. +-- @field #boolean HasBeenMoved Flag for moving. +-- @field #boolean LoadDirectly Flag for direct loading. +-- @field #number CratesNeeded Crates needed to build. +-- @field Wrapper.Positionable#POSITIONABLE Positionable Representation of cargo in the mission. +-- @field #boolean HasBeenDropped True if dropped from heli. +-- @extends Core.Fsm#FSM +CTLD_CARGO = { + ClassName = "CTLD_CARGO", + ID = 0, + Name = "none", + Templates = {}, + CargoType = "none", + HasBeenMoved = false, + LoadDirectly = false, + CratesNeeded = 0, + Positionable = nil, + HasBeenDropped = false, + } + + --- Define cargo types. + -- @type CTLD_CARGO.Enum + -- @field #string Type Type of Cargo. + CTLD_CARGO.Enum = { + ["VEHICLE"] = "Vehicle", -- #string vehicles + ["TROOPS"] = "Troops", -- #string troops + ["FOB"] = "FOB", -- #string FOB + ["CRATE"] = "Crate", -- #string crate + } + + --- Function to create new CTLD_CARGO object. + -- @param #CTLD_CARGO self + -- @param #number ID ID of this #CTLD_CARGO + -- @param #string Name Name for menu. + -- @param #table Templates Table of #POSITIONABLE objects. + -- @param #CTLD_CARGO.Enum Sorte Enumerator of Type. + -- @param #boolean HasBeenMoved Flag for moving. + -- @param #boolean LoadDirectly Flag for direct loading. + -- @param #number CratesNeeded Crates needed to build. + -- @param Wrapper.Positionable#POSITIONABLE Positionable Representation of cargo in the mission. + -- @param #boolean Dropped Cargo/Troops have been unloaded from a chopper. + -- @return #CTLD_CARGO self + function CTLD_CARGO:New(ID, Name, Templates, Sorte, HasBeenMoved, LoadDirectly, CratesNeeded, Positionable, Dropped) + -- Inherit everything from BASE class. + local self=BASE:Inherit(self, BASE:New()) -- #CTLD + self:T({ID, Name, Templates, Sorte, HasBeenMoved, LoadDirectly, CratesNeeded, Positionable, Dropped}) + self.ID = ID or math.random(100000,1000000) + self.Name = Name or "none" -- #string + self.Templates = Templates or {} -- #table + self.CargoType = Sorte or "type" -- #CTLD_CARGO.Enum + self.HasBeenMoved = HasBeenMoved or false -- #boolean + self.LoadDirectly = LoadDirectly or false -- #boolean + self.CratesNeeded = CratesNeeded or 0 -- #number + self.Positionable = Positionable or nil -- Wrapper.Positionable#POSITIONABLE + self.HasBeenDropped = Dropped or false --#boolean + return self + end + + --- Query ID. + -- @param #CTLD_CARGO self + -- @return #number ID + function CTLD_CARGO:GetID() + return self.ID + end + + --- Query Name. + -- @param #CTLD_CARGO self + -- @return #string Name + function CTLD_CARGO:GetName() + return self.Name + end + + --- Query Templates. + -- @param #CTLD_CARGO self + -- @return #table Templates + function CTLD_CARGO:GetTemplates() + return self.Templates + end + + --- Query has moved. + -- @param #CTLD_CARGO self + -- @return #boolean Has moved + function CTLD_CARGO:HasMoved() + return self.HasBeenMoved + end + + --- Query was dropped. + -- @param #CTLD_CARGO self + -- @return #boolean Has been dropped. + function CTLD_CARGO:WasDropped() + return self.HasBeenDropped + end + + --- Query directly loadable. + -- @param #CTLD_CARGO self + -- @return #boolean loadable + function CTLD_CARGO:CanLoadDirectly() + return self.LoadDirectly + end + + --- Query number of crates or troopsize. + -- @param #CTLD_CARGO self + -- @return #number Crates or size of troops. + function CTLD_CARGO:GetCratesNeeded() + return self.CratesNeeded + end + + --- Query type. + -- @param #CTLD_CARGO self + -- @return #CTLD_CARGO.Enum Type + function CTLD_CARGO:GetType() + return self.CargoType + end + + --- Query type. + -- @param #CTLD_CARGO self + -- @return Wrapper.Positionable#POSITIONABLE Positionable + function CTLD_CARGO:GetPositionable() + return self.Positionable + end + + --- Set HasMoved. + -- @param #CTLD_CARGO self + -- @param #boolean moved + function CTLD_CARGO:SetHasMoved(moved) + self.HasBeenMoved = moved or false + end + + --- Query if cargo has been loaded. + -- @param #CTLD_CARGO self + -- @param #boolean loaded + function CTLD_CARGO:Isloaded() + if self.HasBeenMoved and not self.WasDropped() then + return true + else + return false + end + end + --- Set WasDropped. + -- @param #CTLD_CARGO self + -- @param #boolean dropped + function CTLD_CARGO:SetWasDropped(dropped) + self.HasBeenDropped = dropped or false + end + +end + +do +------------------------------------------------------------------------- +--- **CTLD** class, extends Core.Base#BASE, Core.Fsm#FSM +-- @type CTLD +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity level. +-- @field #string lid Class id string for output to DCS log file. +-- @field #number coalition Coalition side number, e.g. `coalition.side.RED`. +-- @extends Core.Fsm#FSM + +--- *Combat Troop & Logistics Deployment (CTLD): Everyone wants to be a POG, until there\'s POG stuff to be done.* (Mil Saying) +-- +-- === +-- +-- ![Banner Image](OPS_CTLD.jpg) +-- +-- # CTLD Concept +-- +-- * MOOSE-based CTLD for Players. +-- * Object oriented refactoring of Ciribob\'s fantastic CTLD script. +-- * No need for extra MIST loading. +-- * Additional events to tailor your mission. +-- * ANY late activated group can serve as cargo, either as troops or crates, which have to be build on-location. +-- +-- ## 0. Prerequisites +-- +-- You need to load an .ogg soundfile for the pilot\'s beacons into the mission, e.g. "beacon.ogg", use a once trigger, "sound to country" for that. +-- Create the late-activated troops, vehicles (no statics at this point!) that will make up your deployable forces. +-- +-- ## 1. Basic Setup +-- +-- ## 1.1 Create and start a CTLD instance +-- +-- A basic setup example is the following: +-- +-- -- Instantiate and start a CTLD for the blue side, using helicopter groups named "Helicargo" and alias "Lufttransportbrigade I" +-- local my_ctld = CTLD:New(coalition.side.BLUE,{"Helicargo"},"Lufttransportbrigade I") +-- my_ctld:__Start(5) +-- +-- ## 1.2 Add cargo types available +-- +-- Add *generic* cargo types that you need for your missions, here infantry units, vehicles and a FOB. These need to be late-activated Wrapper.Group#GROUP objects: +-- +-- -- add infantry unit called "Anti-Tank Small" using template "ATS", of type TROOP with size 3 +-- -- infantry units will be loaded directly from LOAD zones into the heli (matching number of free seats needed) +-- my_ctld:AddTroopsCargo("Anti-Tank Small",{"ATS"},CTLD_CARGO.Enum.TROOPS,3) +-- +-- -- add infantry unit called "Anti-Tank" using templates "AA" and "AA"", of type TROOP with size 4 +-- my_ctld:AddTroopsCargo("Anti-Air",{"AA","AA2"},CTLD_CARGO.Enum.TROOPS,4) +-- +-- -- add vehicle called "Humvee" using template "Humvee", of type VEHICLE, size 2, i.e. needs two crates to be build +-- -- vehicles and FOB will be spawned as crates in a LOAD zone first. Once transported to DROP zones, they can be build into the objects +-- my_ctld:AddCratesCargo("Humvee",{"Humvee"},CTLD_CARGO.Enum.VEHICLE,2) +-- +-- -- add infantry unit called "Forward Ops Base" using template "FOB", of type FOB, size 4, i.e. needs four crates to be build: +-- my_ctld:AddCratesCargo("Forward Ops Base",{"FOB"},CTLD_CARGO.Enum.FOB,4) +-- +-- ## 1.3 Add logistics zones +-- +-- Add zones for loading troops and crates and dropping, building crates +-- +-- -- Add a zone of type LOAD to our setup. Players can load troops and crates. +-- -- "Loadzone" is the name of the zone from the ME. Players can load, if they are inside of the zone. +-- -- Smoke and Flare color for this zone is blue, it is active (can be used) and has a radio beacon. +-- my_ctld:AddCTLDZone("Loadzone",CTLD.CargoZoneType.LOAD,SMOKECOLOR.Blue,true,true) +-- +-- -- Add a zone of type DROP. Players can drop crates here. +-- -- Smoke and Flare color for this zone is blue, it is active (can be used) and has a radio beacon. +-- -- NOTE: Troops can be unloaded anywhere, also when hovering in parameters. +-- my_ctld:AddCTLDZone("Dropzone",CTLD.CargoZoneType.DROP,SMOKECOLOR.Red,true,true) +-- +-- -- Add two zones of type MOVE. Dropped troops and vehicles will move to the nearest one. See options. +-- -- Smoke and Flare color for this zone is blue, it is active (can be used) and has a radio beacon. +-- my_ctld:AddCTLDZone("Movezone",CTLD.CargoZoneType.MOVE,SMOKECOLOR.Orange,false,false) +-- +-- my_ctld:AddCTLDZone("Movezone2",CTLD.CargoZoneType.MOVE,SMOKECOLOR.White,true,true) +-- +-- +-- ## 2. Options +-- +-- The following options are available (with their defaults). Only set the ones you want changed: +-- +-- my_ctld.useprefix = true -- (DO NOT SWITCH THIS OFF UNLESS YOU KNOW WHAT YOU ARE DOING!) Adjust **before** starting CTLD. If set to false, *all* choppers of the coalition side will be enabled for CTLD. +-- my_ctld.CrateDistance = 30 -- List and Load crates in this radius only. +-- my_ctld.dropcratesanywhere = false -- Option to allow crates to be dropped anywhere. +-- my_ctld.maximumHoverHeight = 15 -- Hover max this high to load. +-- my_ctld.minimumHoverHeight = 4 -- Hover min this low to load. +-- my_ctld.forcehoverload = true -- Crates (not: troops) can only be loaded while hovering. +-- my_ctld.hoverautoloading = true -- Crates in CrateDistance in a LOAD zone will be loaded automatically if space allows. +-- my_ctld.smokedistance = 2000 -- Smoke or flares can be request for zones this far away (in meters). +-- my_ctld.movetroopstowpzone = true -- Troops and vehicles will move to the nearest MOVE zone... +-- my_ctld.movetroopsdistance = 5000 -- .. but only if this far away (in meters) +-- my_ctld.smokedistance = 2000 -- Only smoke or flare zones if requesting player unit is this far away (in meters) +-- my_ctld.suppressmessages = false -- Set to true if you want to script your own messages. +-- +-- ## 2.1 User functions +-- +-- ### 2.1.1 Adjust or add chopper unit-type capabilities +-- +-- Use this function to adjust what a heli type can or cannot do: +-- +-- -- E.g. update unit capabilities for testing. Please stay realistic in your mission design. +-- -- Make a Gazelle into a heavy truck, this type can load both crates and troops and eight of each type: +-- my_ctld:UnitCapabilities("SA342L", true, true, 8, 8) +-- +-- Default unit type capabilities are: +-- +-- ["SA342Mistral"] = {type="SA342Mistral", crates=false, troops=true, cratelimit = 0, trooplimit = 4}, +-- ["SA342L"] = {type="SA342L", crates=false, troops=true, cratelimit = 0, trooplimit = 2}, +-- ["SA342M"] = {type="SA342M", crates=false, troops=true, cratelimit = 0, trooplimit = 4}, +-- ["SA342Minigun"] = {type="SA342Minigun", crates=false, troops=true, cratelimit = 0, trooplimit = 2}, +-- ["UH-1H"] = {type="UH-1H", crates=true, troops=true, cratelimit = 1, trooplimit = 8}, +-- ["Mi-8MT"] = {type="Mi-8MT", crates=true, troops=true, cratelimit = 2, trooplimit = 12}, +-- ["Ka-50"] = {type="Ka-50", crates=false, troops=false, cratelimit = 0, trooplimit = 0}, +-- ["Mi-24P"] = {type="Mi-24P", crates=true, troops=true, cratelimit = 1, trooplimit = 8}, +-- ["Mi-24V"] = {type="Mi-24V", crates=true, troops=true, cratelimit = 1, trooplimit = 8}, +-- +-- +-- ### 2.1.2 Activate and deactivate zones +-- +-- Activate a zone: +-- +-- -- Activate zone called Name of type #CTLD.CargoZoneType ZoneType: +-- my_ctld:ActivateZone(Name,CTLD.CargoZoneType.MOVE) +-- +-- Deactivate a zone: +-- +-- -- Deactivate zone called Name of type #CTLD.CargoZoneType ZoneType: +-- my_ctld:DeactivateZone(Name,CTLD.CargoZoneType.DROP) +-- +-- ## 3. Events +-- +-- The class comes with a number of FSM-based events that missions designers can use to shape their mission. +-- These are: +-- +-- ## 3.1 OnAfterTroopsPickedUp +-- +-- This function is called when a player has loaded Troops: +-- +-- function my_ctld:OnAfterTroopsPickedUp(From, Event, To, Group, Unit, Cargo) +-- ... your code here ... +-- end +-- +-- ## 3.2 OnAfterCratesPickedUp +-- +-- This function is called when a player has picked up crates: +-- +-- function my_ctld:OnAfterCratesPickedUp(From, Event, To, Group, Unit, Cargo) +-- ... your code here ... +-- end +-- +-- ## 3.3 OnAfterTroopsDeployed +-- +-- This function is called when a player has deployed troops into the field: +-- +-- function my_ctld:OnAfterTroopsDeployed(From, Event, To, Group, Unit, Troops) +-- ... your code here ... +-- end +-- +-- ## 3.4 OnAfterTroopsExtracted +-- +-- This function is called when a player has re-boarded already deployed troops from the field: +-- +-- function my_ctld:OnAfterTroopsExtracted(From, Event, To, Group, Unit, Troops) +-- ... your code here ... +-- end +-- +-- ## 3.5 OnAfterCratesDropped +-- +-- This function is called when a player has deployed crates to a DROP zone: +-- +-- function my_ctld:OnAfterCratesDropped(From, Event, To, Group, Unit, Cargotable) +-- ... your code here ... +-- end +-- +-- ## 3.6 OnAfterCratesBuild +-- +-- This function is called when a player has build a vehicle or FOB: +-- +-- function my_ctld:OnAfterCratesBuild(From, Event, To, Group, Unit, Vehicle) +-- ... your code here ... +-- end + -- +-- ## 3.7 A simple SCORING example: +-- +-- To award player with points, using the SCORING Class (SCORING: my_Scoring, CTLD: CTLD_Cargotransport) +-- +-- function CTLD_Cargotransport:OnAfterCratesDropped(From, Event, To, Group, Unit, Cargotable) +-- local points = 10 +-- local PlayerName = Unit:GetPlayerName() +-- my_scoring:_AddPlayerFromUnit( Unit ) +-- my_scoring:AddGoalScore(Unit, "CTLD", string.format("Pilot %s has been awarded %d points for transporting cargo crates!", PlayerName, points), points) +-- end +-- +-- function CTLD_Cargotransport:OnAfterCratesBuild(From, Event, To, Group, Unit, Vehicle) +-- local points = 5 +-- local PlayerName = Unit:GetPlayerName() +-- my_scoring:_AddPlayerFromUnit( Unit ) +-- my_scoring:AddGoalScore(Unit, "CTLD", string.format("Pilot %s has been awarded %d points for the construction of Units!", PlayerName, points), points) +-- end +-- +-- ## 4. F10 Menu structure +-- +-- CTLD management menu is under the F10 top menu and called "CTLD" +-- +-- ## 4.1 Manage Crates +-- +-- Use this entry to get, load, list nearby, drop, and build crates. Also @see options. +-- +-- ## 4.2 Manage Troops +-- +-- Use this entry to load, drop and extract troops. NOTE - with extract you can only load troops from the field that were deployed prior. +-- Currently limited CTLD_CARGO troops, which are build from **one** template. Also, this will heal/complete your units as they are respawned. +-- +-- ## 4.3 List boarded cargo +-- +-- Lists what you have loaded. Shows load capabilities for number of crates and number of seats for troops. +-- +-- ## 4.4 Smoke & Flare zones nearby +-- +-- Does what it says. +-- +-- ## 4.5 List active zone beacons +-- +-- Lists active radio beacons for all zones, where zones are both active and have a beacon. @see `CTLD:AddCTLDZone()` +-- +-- ## 4.6 Show hover parameters +-- +-- Lists hover parameters and indicates if these are curently fulfilled. Also @see options on hover heights. +-- +-- ## 5. Support for Hercules mod by Anubis +-- +-- Basic support for the Hercules mod By Anubis has been build into CTLD. Currently this does **not** cover objects and troops which can +-- be loaded from the Rearm/Refuel menu, i.e. you can drop them into the field, but you cannot use them in functions scripted with this class. +-- +-- local my_ctld = CTLD:New(coalition.side.BLUE,{"Helicargo", "Hercules"},"Lufttransportbrigade I") +-- +-- Enable these options for Hercules support: +-- +-- my_ctld.enableHercules = true +-- my_ctld.HercMinAngels = 155 -- for troop/cargo drop via chute in meters, ca 470 ft +-- my_ctld.HercMaxAngels = 2000 -- for troop/cargo drop via chute in meters, ca 6000 ft +-- my_ctld.HercMaxSpeed = 77 -- 77mps or 270 kph or 150 kn +-- +-- Also, the following options need to be set to `true`: +-- +-- my_ctld.useprefix = true -- this is true by default and MUST BE ON. +-- +-- Standard transport capabilities as per the real Hercules are: +-- +-- ["Hercules"] = {type="Hercules", crates=true, troops=true, cratelimit = 7, trooplimit = 64}, -- 19t cargo, 64 paratroopers +-- +-- @field #CTLD +CTLD = { + ClassName = "CTLD", + verbose = 0, + lid = "", + coalition = 1, + coalitiontxt = "blue", + PilotGroups = {}, -- #GROUP_SET of heli pilots + CtldUnits = {}, -- Table of helicopter #GROUPs + FreeVHFFrequencies = {}, -- Table of VHF + FreeUHFFrequencies = {}, -- Table of UHF + FreeFMFrequencies = {}, -- Table of FM + CargoCounter = 0, + dropOffZones = {}, + wpZones = {}, + Cargo_Troops = {}, -- generic troops objects + Cargo_Crates = {}, -- generic crate objects + Loaded_Cargo = {}, -- cargo aboard units + Spawned_Crates = {}, -- Holds objects for crates spawned generally + Spawned_Cargo = {}, -- Binds together spawned_crates and their CTLD_CARGO objects + CrateDistance = 30, -- list crates in this radius + debug = false, + wpZones = {}, + pickupZones = {}, + dropOffZones = {}, +} + +------------------------------ +-- DONE: Zone Checks +-- DONE: TEST Hover load and unload +-- DONE: Crate unload +-- DONE: Hover (auto-)load +-- TODO: (More) Housekeeping +-- DONE: Troops running to WP Zone +-- DONE: Zone Radio Beacons +-- DONE: Stats Running +-- DONE: Added support for Hercules +-- TODO: Possibly - either/or loading crates and troops +-- TODO: Limit of troops, crates buildable? +------------------------------ + +--- Radio Beacons +-- @type CTLD.ZoneBeacon +-- @field #string name -- Name of zone for the coordinate +-- @field #number frequency -- in mHz +-- @field #number modulation -- i.e.radio.modulation.FM or radio.modulation.AM + +--- Zone Info. +-- @type CTLD.CargoZone +-- @field #string name Name of Zone. +-- @field #string color Smoke color for zone, e.g. SMOKECOLOR.Red. +-- @field #boolean active Active or not. +-- @field #string type Type of zone, i.e. load,drop,move +-- @field #boolean hasbeacon Create and run radio beacons if active. +-- @field #table fmbeacon Beacon info as #CTLD.ZoneBeacon +-- @field #table uhfbeacon Beacon info as #CTLD.ZoneBeacon +-- @field #table vhfbeacon Beacon info as #CTLD.ZoneBeacon + +--- Zone Type Info. +-- @type CTLD.CargoZoneType +CTLD.CargoZoneType = { + LOAD = "load", + DROP = "drop", + MOVE = "move", +} + +--- Buildable table info. +-- @type CTLD.Buildable +-- @field #string Name Name of the object. +-- @field #number Required Required crates. +-- @field #number Found Found crates. +-- @field #table Template Template names for this build. +-- @field #boolean CanBuild Is buildable or not. +-- @field #CTLD_CARGO.Enum Type Type enumerator (for moves). + +--- Unit capabilities. +-- @type CTLD.UnitCapabilities +-- @field #string type Unit type. +-- @field #boolean crates Can transport crate. +-- @field #boolean troops Can transport troops. +-- @field #number cratelimit Number of crates transportable. +-- @field #number trooplimit Number of troop units transportable. +CTLD.UnitTypes = { + ["SA342Mistral"] = {type="SA342Mistral", crates=false, troops=true, cratelimit = 0, trooplimit = 4}, + ["SA342L"] = {type="SA342L", crates=false, troops=true, cratelimit = 0, trooplimit = 2}, + ["SA342M"] = {type="SA342M", crates=false, troops=true, cratelimit = 0, trooplimit = 4}, + ["SA342Minigun"] = {type="SA342Minigun", crates=false, troops=true, cratelimit = 0, trooplimit = 2}, + ["UH-1H"] = {type="UH-1H", crates=true, troops=true, cratelimit = 1, trooplimit = 8}, + ["Mi-8MTV2"] = {type="Mi-8MTV2", crates=true, troops=true, cratelimit = 2, trooplimit = 12}, + ["Mi-8MT"] = {type="Mi-8MTV2", crates=true, troops=true, cratelimit = 2, trooplimit = 12}, + ["Ka-50"] = {type="Ka-50", crates=false, troops=false, cratelimit = 0, trooplimit = 0}, + ["Mi-24P"] = {type="Mi-24P", crates=true, troops=true, cratelimit = 2, trooplimit = 8}, + ["Mi-24V"] = {type="Mi-24V", crates=true, troops=true, cratelimit = 2, trooplimit = 8}, + ["Hercules"] = {type="Hercules", crates=true, troops=true, cratelimit = 7, trooplimit = 64}, -- 19t cargo, 64 paratroopers +} + +--- CTLD class version. +-- @field #string version +CTLD.version="0.1.4r3" + +--- Instantiate a new CTLD. +-- @param #CTLD self +-- @param #string Coalition Coalition of this CTLD. I.e. coalition.side.BLUE or coalition.side.RED or coalition.side.NEUTRAL +-- @param #table Prefixes Table of pilot prefixes. +-- @param #string Alias Alias of this CTLD for logging. +-- @return #CTLD self +function CTLD:New(Coalition, Prefixes, Alias) + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #CTLD + + BASE:T({Coalition, Prefixes, Alias}) + + --set Coalition + if Coalition and type(Coalition)=="string" then + if Coalition=="blue" then + self.coalition=coalition.side.BLUE + self.coalitiontxt = Coalition + elseif Coalition=="red" then + self.coalition=coalition.side.RED + self.coalitiontxt = Coalition + elseif Coalition=="neutral" then + self.coalition=coalition.side.NEUTRAL + self.coalitiontxt = Coalition + else + self:E("ERROR: Unknown coalition in CTLD!") + end + else + self.coalition = Coalition + self.coalitiontxt = string.lower(UTILS.GetCoalitionName(self.coalition)) + end + + -- Set alias. + if Alias then + self.alias=tostring(Alias) + else + self.alias="UNHCR" + if self.coalition then + if self.coalition==coalition.side.RED then + self.alias="Red CTLD" + elseif self.coalition==coalition.side.BLUE then + self.alias="Blue CTLD" + end + end + end + + -- Set some string id for output to DCS.log file. + self.lid=string.format("%s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown") + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start FSM. + self:AddTransition("*", "Status", "*") -- CTLD status update. + self:AddTransition("*", "TroopsPickedUp", "*") -- CTLD pickup event. + self:AddTransition("*", "TroopsExtracted", "*") -- CTLD extract event. + self:AddTransition("*", "CratesPickedUp", "*") -- CTLD pickup event. + self:AddTransition("*", "TroopsDeployed", "*") -- CTLD deploy event. + self:AddTransition("*", "TroopsRTB", "*") -- CTLD deploy event. + self:AddTransition("*", "CratesDropped", "*") -- CTLD deploy event. + self:AddTransition("*", "CratesBuild", "*") -- CTLD build event. + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + + -- tables + self.PilotGroups ={} + self.CtldUnits = {} + + -- Beacons + self.FreeVHFFrequencies = {} + self.FreeUHFFrequencies = {} + self.FreeFMFrequencies = {} + self.UsedVHFFrequencies = {} + self.UsedUHFFrequencies = {} + self.UsedFMFrequencies = {} + + -- radio beacons + self.RadioSound = "beacon.ogg" + + -- zones stuff + self.pickupZones = {} + self.dropOffZones = {} + self.wpZones = {} + + -- Cargo + self.Cargo_Crates = {} + self.Cargo_Troops = {} + self.Loaded_Cargo = {} + self.Spawned_Crates = {} + self.Spawned_Cargo = {} + self.MenusDone = {} + self.DroppedTroops = {} + self.DroppedCrates = {} + self.CargoCounter = 0 + self.CrateCounter = 0 + self.TroopCounter = 0 + + -- setup + self.CrateDistance = 30 -- list/load crates in this radius + self.prefixes = Prefixes or {"Cargoheli"} + --self.I({prefixes = self.prefixes}) + self.useprefix = true + + self.maximumHoverHeight = 15 + self.minimumHoverHeight = 4 + self.forcehoverload = true + self.hoverautoloading = true + self.dropcratesanywhere = false -- #1570 + + self.smokedistance = 2000 + self.movetroopstowpzone = true + self.movetroopsdistance = 5000 + + -- added support Hercules Mod + self.enableHercules = false + self.HercMinAngels = 165 -- for troop/cargo drop via chute + self.HercMaxAngels = 2000 -- for troop/cargo drop via chute + self.HercMaxSpeed = 77 -- 280 kph or 150kn eq 77 mps + + -- message suppression + self.suppressmessages = false + + for i=1,100 do + math.random() + end + + self:_GenerateVHFrequencies() + self:_GenerateUHFrequencies() + self:_GenerateFMFrequencies() + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the CTLD. Initializes parameters and starts event handlers. + -- @function [parent=#CTLD] Start + -- @param #CTLD self + + --- Triggers the FSM event "Start" after a delay. Starts the CTLD. Initializes parameters and starts event handlers. + -- @function [parent=#CTLD] __Start + -- @param #CTLD self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the CTLD and all its event handlers. + -- @param #CTLD self + + --- Triggers the FSM event "Stop" after a delay. Stops the CTLD and all its event handlers. + -- @function [parent=#CTLD] __Stop + -- @param #CTLD self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Status". + -- @function [parent=#CTLD] Status + -- @param #CTLD self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#CTLD] __Status + -- @param #CTLD self + -- @param #number delay Delay in seconds. + + --- FSM Function OnAfterTroopsPickedUp. + -- @function [parent=#CTLD] OnAfterTroopsPickedUp + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param #CTLD_CARGO Cargo Cargo troops. + -- @return #CTLD self + + --- FSM Function OnAfterTroopsExtracted. + -- @function [parent=#CTLD] OnAfterTroopsExtracted + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param #CTLD_CARGO Cargo Cargo troops. + -- @return #CTLD self + + --- FSM Function OnAfterCratesPickedUp. + -- @function [parent=#CTLD] OnAfterCratesPickedUp + -- @param #CTLD self + -- @param #string From State . + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param #CTLD_CARGO Cargo Cargo crate. + -- @return #CTLD self + + --- FSM Function OnAfterTroopsDeployed. + -- @function [parent=#CTLD] OnAfterTroopsDeployed + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param Wrapper.Group#GROUP Troops Troops #GROUP Object. + -- @return #CTLD self + + --- FSM Function OnAfterCratesDropped. + -- @function [parent=#CTLD] OnAfterCratesDropped + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param #table Cargotable Table of #CTLD_CARGO objects dropped. + -- @return #CTLD self + + --- FSM Function OnAfterCratesBuild. + -- @function [parent=#CTLD] OnAfterCratesBuild + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param Wrapper.Group#GROUP Vehicle The #GROUP object of the vehicle or FOB build. + -- @return #CTLD self + + --- FSM Function OnAfterTroopsRTB. + -- @function [parent=#CTLD] OnAfterTroopsRTB + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + + return self +end + +------------------------------------------------------------------- +-- Helper and User Functions +------------------------------------------------------------------- + +--- (Internal) Function to get capabilities of a chopper +-- @param #CTLD self +-- @param Wrapper.Unit#UNIT Unit The unit +-- @return #table Capabilities Table of caps +function CTLD:_GetUnitCapabilities(Unit) + self:T(self.lid .. " _GetUnitCapabilities") + local _unit = Unit -- Wrapper.Unit#UNIT + local unittype = _unit:GetTypeName() + local capabilities = self.UnitTypes[unittype] -- #CTLD.UnitCapabilities + if not capabilities or capabilities == {} then + -- e.g. ["Ka-50"] = {type="Ka-50", crates=false, troops=false, cratelimit = 0, trooplimit = 0}, + capabilities = {} + capabilities.troops = false + capabilities.crates = false + capabilities.cratelimit = 0 + capabilities.trooplimit = 0 + capabilities.type = "generic" + end + return capabilities +end + + +--- (Internal) Function to generate valid UHF Frequencies +-- @param #CTLD self +function CTLD:_GenerateUHFrequencies() + self:T(self.lid .. " _GenerateUHFrequencies") + self.FreeUHFFrequencies = {} + self.FreeUHFFrequencies = UTILS.GenerateUHFrequencies() + return self +end + +--- (Internal) Function to generate valid FM Frequencies +-- @param #CTLD self +function CTLD:_GenerateFMFrequencies() + self:T(self.lid .. " _GenerateFMrequencies") + self.FreeFMFrequencies = {} + self.FreeFMFrequencies = UTILS.GenerateFMFrequencies() + return self +end + +--- (Internal) Populate table with available VHF beacon frequencies. +-- @param #CTLD self +function CTLD:_GenerateVHFrequencies() + self:T(self.lid .. " _GenerateVHFrequencies") + self.FreeVHFFrequencies = {} + self.UsedVHFFrequencies = {} + self.FreeVHFFrequencies = UTILS.GenerateVHFrequencies() + return self +end + +--- (Internal) Event handler function +-- @param #CTLD self +-- @param Core.Event#EVENTDATA EventData +function CTLD:_EventHandler(EventData) + self:T(string.format("%s Event = %d",self.lid, EventData.id)) + local event = EventData -- Core.Event#EVENTDATA + if event.id == EVENTS.PlayerEnterAircraft or event.id == EVENTS.PlayerEnterUnit then + local _coalition = event.IniCoalition + if _coalition ~= self.coalition then + return --ignore! + end + -- check is Helicopter + local _unit = event.IniUnit + local _group = event.IniGroup + if _unit:IsHelicopter() or _group:IsHelicopter() then + local unitname = event.IniUnitName or "none" + self.Loaded_Cargo[unitname] = nil + self:_RefreshF10Menus() + end + -- Herc support + --self:T_unit:GetTypeName()) + if _unit:GetTypeName() == "Hercules" and self.enableHercules then + self.Loaded_Cargo[unitname] = nil + self:_RefreshF10Menus() + end + return + elseif event.id == EVENTS.PlayerLeaveUnit then + -- remove from pilot table + local unitname = event.IniUnitName or "none" + self.CtldUnits[unitname] = nil + self.Loaded_Cargo[unitname] = nil + end + return self +end + +--- (Internal) Function to message a group. +-- @param #CTLD self +-- @param #string Text The text to display. +-- @param #number Time Number of seconds to display the message. +-- @param #boolean Clearscreen Clear screen or not. +-- @param Wrapper.Group#GROUP Group The group receiving the message. +function CTLD:_SendMessage(Text, Time, Clearscreen, Group) + self:T(self.lid .. " _SendMessage") + if not self.suppressmessages then + local m = MESSAGE:New(Text,Time,"CTLD",Clearscreen):ToGroup(Group) + end + return self +end + +--- (Internal) Function to load troops into a heli. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Unit#UNIT Unit +-- @param #CTLD_CARGO Cargotype +function CTLD:_LoadTroops(Group, Unit, Cargotype) + self:T(self.lid .. " _LoadTroops") + -- landed or hovering over load zone? + local grounded = not self:IsUnitInAir(Unit) + local hoverload = self:CanHoverLoad(Unit) + -- check if we are in LOAD zone + local inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) + if not inzone then + self:_SendMessage("You are not close enough to a logistics zone!", 10, false, Group) + if not self.debug then return self end + elseif not grounded and not hoverload then + self:_SendMessage("You need to land or hover in position to load!", 10, false, Group) + if not self.debug then return self end + end + -- load troops into heli + local group = Group -- Wrapper.Group#GROUP + local unit = Unit -- Wrapper.Unit#UNIT + local unitname = unit:GetName() + local cargotype = Cargotype -- #CTLD_CARGO + local cratename = cargotype:GetName() -- #string + -- see if this heli can load troops + local unittype = unit:GetTypeName() + local capabilities = self:_GetUnitCapabilities(Unit) + local cantroops = capabilities.troops -- #boolean + local trooplimit = capabilities.trooplimit -- #number + local troopsize = cargotype:GetCratesNeeded() -- #number + -- have we loaded stuff already? + local numberonboard = 0 + local loaded = {} + if self.Loaded_Cargo[unitname] then + loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo + numberonboard = loaded.Troopsloaded or 0 + else + loaded = {} -- #CTLD.LoadedCargo + loaded.Troopsloaded = 0 + loaded.Cratesloaded = 0 + loaded.Cargo = {} + end + if troopsize + numberonboard > trooplimit then + self:_SendMessage("Sorry, we\'re crammed already!", 10, false, Group) + return + else + self.CargoCounter = self.CargoCounter + 1 + local loadcargotype = CTLD_CARGO:New(self.CargoCounter, Cargotype.Name, Cargotype.Templates, CTLD_CARGO.Enum.TROOPS, true, true, Cargotype.CratesNeeded) + self:T({cargotype=loadcargotype}) + loaded.Troopsloaded = loaded.Troopsloaded + troopsize + table.insert(loaded.Cargo,loadcargotype) + self.Loaded_Cargo[unitname] = loaded + self:_SendMessage("Troops boarded!", 10, false, Group) + self:__TroopsPickedUp(1,Group, Unit, Cargotype) + end + return self +end + + --- (Internal) Function to extract (load from the field) troops into a heli. + -- @param #CTLD self + -- @param Wrapper.Group#GROUP Group + -- @param Wrapper.Unit#UNIT Unit + -- @param #CTLD_CARGO Cargotype + function CTLD:_ExtractTroops(Group, Unit) -- #1574 thanks to @bbirchnz! + self:T(self.lid .. " _ExtractTroops") + -- landed or hovering over load zone? + local grounded = not self:IsUnitInAir(Unit) + local hoverload = self:CanHoverLoad(Unit) + + if not grounded and not hoverload then + self:_SendMessage("You need to land or hover in position to load!", 10, false, Group) + if not self.debug then return self end + end + -- load troops into heli + local unit = Unit -- Wrapper.Unit#UNIT + local unitname = unit:GetName() + -- see if this heli can load troops + local unittype = unit:GetTypeName() + local capabilities = self:_GetUnitCapabilities(Unit) + local cantroops = capabilities.troops -- #boolean + local trooplimit = capabilities.trooplimit -- #number + local unitcoord = unit:GetCoordinate() + + -- find nearest group of deployed troops + local nearestGroup = nil + local nearestGroupIndex = -1 + local nearestDistance = 10000000 + for k,v in pairs(self.DroppedTroops) do + local distance = self:_GetDistance(v:GetCoordinate(),unitcoord) + if distance < nearestDistance and distance ~= -1 then + nearestGroup = v + nearestGroupIndex = k + nearestDistance = distance + end + end + + if nearestGroup == nil or nearestDistance > self.CrateDistance then + self:_SendMessage("No units close enough to extract!", 10, false, Group) + return self + end + -- find matching cargo type + local groupType = string.match(nearestGroup:GetName(), "(.+)-(.+)$") + local Cargotype = nil + for k,v in pairs(self.Cargo_Troops) do + if v.Name == groupType then + Cargotype = v + break + end + end + + if Cargotype == nil then + self:_SendMessage("Can't find a matching cargo type for " .. groupType, 10, false, Group) + return self + end + + local troopsize = Cargotype:GetCratesNeeded() -- #number + -- have we loaded stuff already? + local numberonboard = 0 + local loaded = {} + if self.Loaded_Cargo[unitname] then + loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo + numberonboard = loaded.Troopsloaded or 0 + else + loaded = {} -- #CTLD.LoadedCargo + loaded.Troopsloaded = 0 + loaded.Cratesloaded = 0 + loaded.Cargo = {} + end + if troopsize + numberonboard > trooplimit then + self:_SendMessage("Sorry, we\'re crammed already!", 10, false, Group) + return + else + self.CargoCounter = self.CargoCounter + 1 + local loadcargotype = CTLD_CARGO:New(self.CargoCounter, Cargotype.Name, Cargotype.Templates, CTLD_CARGO.Enum.TROOPS, true, true, Cargotype.CratesNeeded) + self:T({cargotype=loadcargotype}) + loaded.Troopsloaded = loaded.Troopsloaded + troopsize + table.insert(loaded.Cargo,loadcargotype) + self.Loaded_Cargo[unitname] = loaded + self:_SendMessage("Troops boarded!", 10, false, Group) + self:__TroopsExtracted(1,Group, Unit, nearestGroup) + + -- clean up: + table.remove(self.DroppedTroops, nearestGroupIndex) + nearestGroup:Destroy() + end + return self + end + +--- (Internal) Function to spawn crates in front of the heli. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Unit#UNIT Unit +-- @param #CTLD_CARGO Cargo +-- @param #number number Number of crates to generate (for dropping) +-- @param #boolean drop If true we\'re dropping from heli rather than loading. +function CTLD:_GetCrates(Group, Unit, Cargo, number, drop) + self:T(self.lid .. " _GetCrates") + local cgoname = Cargo:GetName() + -- check if we are in LOAD zone + local inzone = false + local drop = drop or false + if not drop then + inzone = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) + else + if self.dropcratesanywhere then -- #1570 + inzone = true + else + inzone = self:IsUnitInZone(Unit,CTLD.CargoZoneType.DROP) + end + end + + if not inzone then + self:_SendMessage("You are not close enough to a logistics zone!", 10, false, Group) + if not self.debug then return self end + end + + -- avoid crate spam + local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitCapabilities + local canloadcratesno = capabilities.cratelimit + local loaddist = self.CrateDistance or 30 + local nearcrates, numbernearby = self:_FindCratesNearby(Group,Unit,loaddist) + if numbernearby >= canloadcratesno and not drop then + self:_SendMessage("There are enough crates nearby already! Take care of those first!", 10, false, Group) + return self + end + -- spawn crates in front of helicopter + local IsHerc = self:IsHercules(Unit) -- Herc + local cargotype = Cargo -- #CTLD_CARGO + local number = number or cargotype:GetCratesNeeded() --#number + local cratesneeded = cargotype:GetCratesNeeded() --#number + local cratename = cargotype:GetName() + local cratetemplate = "Container"-- #string + -- get position and heading of heli + local position = Unit:GetCoordinate() + local heading = Unit:GetHeading() + 1 + local height = Unit:GetHeight() + local droppedcargo = {} + -- loop crates needed + for i=1,number do + local cratealias = string.format("%s-%d", cratetemplate, math.random(1,100000)) + local cratedistance = i*4 + 8 + if IsHerc then + -- wider radius + cratedistance = i*4 + 12 + end + for i=1,50 do + math.random(90,270) + end + local rheading = math.floor(math.random(90,270) * heading + 1 / 360) + if not IsHerc then + rheading = rheading + 180 -- mirror for Helis + end + if rheading > 360 then rheading = rheading - 360 end -- catch > 360 + local cratecoord = position:Translate(cratedistance,rheading) + local cratevec2 = cratecoord:GetVec2() + self.CrateCounter = self.CrateCounter + 1 + self.Spawned_Crates[self.CrateCounter] = SPAWNSTATIC:NewFromType("container_cargo","Cargos",country.id.GERMANY) + :InitCoordinate(cratecoord) + :Spawn(270,cratealias) + + local templ = cargotype:GetTemplates() + local sorte = cargotype:GetType() + self.CargoCounter = self.CargoCounter +1 + local realcargo = nil + if drop then + realcargo = CTLD_CARGO:New(self.CargoCounter,cratename,templ,sorte,true,false,cratesneeded,self.Spawned_Crates[self.CrateCounter],true) + table.insert(droppedcargo,realcargo) + else + realcargo = CTLD_CARGO:New(self.CargoCounter,cratename,templ,sorte,false,false,cratesneeded,self.Spawned_Crates[self.CrateCounter]) + end + table.insert(self.Spawned_Cargo, realcargo) + end + local text = string.format("Crates for %s have been positioned near you!",cratename) + if drop then + text = string.format("Crates for %s have been dropped!",cratename) + self:__CratesDropped(1, Group, Unit, droppedcargo) + end + self:_SendMessage(text, 10, false, Group) + return self +end + +--- (Internal) Function to find and list nearby crates. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Unit#UNIT Unit +-- @return #CTLD self +function CTLD:_ListCratesNearby( _group, _unit) + self:T(self.lid .. " _ListCratesNearby") + local finddist = self.CrateDistance or 30 + local crates,number = self:_FindCratesNearby(_group,_unit, finddist) -- #table + if number > 0 then + local text = REPORT:New("Crates Found Nearby:") + text:Add("------------------------------------------------------------") + for _,_entry in pairs (crates) do + local entry = _entry -- #CTLD_CARGO + local name = entry:GetName() --#string + local dropped = entry:WasDropped() + if dropped then + text:Add(string.format("Dropped crate for %s",name)) + else + text:Add(string.format("Crate for %s",name)) + end + end + if text:GetCount() == 1 then + text:Add(" N O N E") + end + text:Add("------------------------------------------------------------") + self:_SendMessage(text:Text(), 30, true, _group) + else + self:_SendMessage(string.format("No (loadable) crates within %d meters!",finddist), 10, false, _group) + end + return self +end + +--- (Internal) Return distance in meters between two coordinates. +-- @param #CTLD self +-- @param Core.Point#COORDINATE _point1 Coordinate one +-- @param Core.Point#COORDINATE _point2 Coordinate two +-- @return #number Distance in meters +function CTLD:_GetDistance(_point1, _point2) + self:T(self.lid .. " _GetDistance") + if _point1 and _point2 then + local distance = _point1:DistanceFromPointVec2(_point2) + return distance + else + return -1 + end +end + +--- (Internal) Function to find and return nearby crates. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP _group Group +-- @param Wrapper.Unit#UNIT _unit Unit +-- @param #number _dist Distance +-- @return #table Table of crates +-- @return #number Number Number of crates found +function CTLD:_FindCratesNearby( _group, _unit, _dist) + self:T(self.lid .. " _FindCratesNearby") + local finddist = _dist + local location = _group:GetCoordinate() + local existingcrates = self.Spawned_Cargo -- #table + -- cycle + local index = 0 + local found = {} + for _,_cargoobject in pairs (existingcrates) do + local cargo = _cargoobject -- #CTLD_CARGO + local static = cargo:GetPositionable() -- Wrapper.Static#STATIC -- crates + local staticid = cargo:GetID() + if static and static:IsAlive() then + local staticpos = static:GetCoordinate() + local distance = self:_GetDistance(location,staticpos) + if distance <= finddist and static then + index = index + 1 + table.insert(found, staticid, cargo) + end + end + end + return found, index +end + +--- (Internal) Function to get and load nearby crates. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Unit#UNIT Unit +-- @return #CTLD self +function CTLD:_LoadCratesNearby(Group, Unit) + self:T(self.lid .. " _LoadCratesNearby") + -- load crates into heli + local group = Group -- Wrapper.Group#GROUP + local unit = Unit -- Wrapper.Unit#UNIT + local unitname = unit:GetName() + -- see if this heli can load crates + local unittype = unit:GetTypeName() + local capabilities = self:_GetUnitCapabilities(Unit) + --local capabilities = self.UnitTypes[unittype] -- #CTLD.UnitCapabilities + local cancrates = capabilities.crates -- #boolean + local cratelimit = capabilities.cratelimit -- #number + local grounded = not self:IsUnitInAir(Unit) + local canhoverload = self:CanHoverLoad(Unit) + --- cases ------------------------------- + -- Chopper can\'t do crates - bark & return + -- Chopper can do crates - + -- --> hover if forcedhover or bark and return + -- --> hover or land if not forcedhover + ----------------------------------------- + if not cancrates then + self:_SendMessage("Sorry this chopper cannot carry crates!", 10, false, Group) + elseif self.forcehoverload and not canhoverload then + self:_SendMessage("Hover over the crates to pick them up!", 10, false, Group) + elseif not grounded and not canhoverload then + self:_SendMessage("Land or hover over the crates to pick them up!", 10, false, Group) + else + -- have we loaded stuff already? + local numberonboard = 0 + local loaded = {} + if self.Loaded_Cargo[unitname] then + loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo + numberonboard = loaded.Cratesloaded or 0 + else + loaded = {} -- #CTLD.LoadedCargo + loaded.Troopsloaded = 0 + loaded.Cratesloaded = 0 + loaded.Cargo = {} + end + -- get nearby crates + local finddist = self.CrateDistance or 30 + local nearcrates,number = self:_FindCratesNearby(Group,Unit,finddist) -- #table + if number == 0 or numberonboard == cratelimit then + self:_SendMessage("Sorry no loadable crates nearby or fully loaded!", 10, false, Group) + return -- exit + else + -- go through crates and load + local capacity = cratelimit - numberonboard + local crateidsloaded = {} + local loops = 0 + while loaded.Cratesloaded < cratelimit and loops < number do + loops = loops + 1 + local crateind = 0 + -- get crate with largest index + for _ind,_crate in pairs (nearcrates) do + if not _crate:HasMoved() and not _crate:WasDropped() and _crate:GetID() > crateind then + crateind = _crate:GetID() + end + end + -- load one if we found one + if crateind > 0 then + local crate = nearcrates[crateind] -- #CTLD_CARGO + loaded.Cratesloaded = loaded.Cratesloaded + 1 + crate:SetHasMoved(true) + table.insert(loaded.Cargo, crate) + table.insert(crateidsloaded,crate:GetID()) + -- destroy crate + crate:GetPositionable():Destroy() + crate.Positionable = nil + self:_SendMessage(string.format("Crate ID %d for %s loaded!",crate:GetID(),crate:GetName()), 10, false, Group) + self:__CratesPickedUp(1, Group, Unit, crate) + end + end + self.Loaded_Cargo[unitname] = loaded + -- clean up real world crates + local existingcrates = self.Spawned_Cargo -- #table + local newexcrates = {} + for _,_crate in pairs(existingcrates) do + local excrate = _crate -- #CTLD_CARGO + local ID = excrate:GetID() + for _,_ID in pairs(crateidsloaded) do + if ID ~= _ID then + table.insert(newexcrates,_crate) + end + end + end + self.Spawned_Cargo = nil + self.Spawned_Cargo = newexcrates + end + end + return self +end + +--- (Internal) Function to list loaded cargo. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Unit#UNIT Unit +-- @return #CTLD self +function CTLD:_ListCargo(Group, Unit) + self:T(self.lid .. " _ListCargo") + local unitname = Unit:GetName() + local unittype = Unit:GetTypeName() + local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitCapabilities + local trooplimit = capabilities.trooplimit -- #boolean + local cratelimit = capabilities.cratelimit -- #number + local loadedcargo = self.Loaded_Cargo[unitname] or {} -- #CTLD.LoadedCargo + if self.Loaded_Cargo[unitname] then + local no_troops = loadedcargo.Troopsloaded or 0 + local no_crates = loadedcargo.Cratesloaded or 0 + local cargotable = loadedcargo.Cargo or {} -- #table + local report = REPORT:New("Transport Checkout Sheet") + report:Add("------------------------------------------------------------") + report:Add(string.format("Troops: %d(%d), Crates: %d(%d)",no_troops,trooplimit,no_crates,cratelimit)) + report:Add("------------------------------------------------------------") + report:Add(" -- TROOPS --") + for _,_cargo in pairs(cargotable) do + local cargo = _cargo -- #CTLD_CARGO + local type = cargo:GetType() -- #CTLD_CARGO.Enum + if type == CTLD_CARGO.Enum.TROOPS and not cargo:WasDropped() then + report:Add(string.format("Troop: %s size %d",cargo:GetName(),cargo:GetCratesNeeded())) + end + end + if report:GetCount() == 4 then + report:Add(" N O N E") + end + report:Add("------------------------------------------------------------") + report:Add(" -- CRATES --") + local cratecount = 0 + for _,_cargo in pairs(cargotable) do + local cargo = _cargo -- #CTLD_CARGO + local type = cargo:GetType() -- #CTLD_CARGO.Enum + if type ~= CTLD_CARGO.Enum.TROOPS then + report:Add(string.format("Crate: %s size 1",cargo:GetName())) + cratecount = cratecount + 1 + end + end + if cratecount == 0 then + report:Add(" N O N E") + end + report:Add("------------------------------------------------------------") + local text = report:Text() + self:_SendMessage(text, 30, true, Group) + else + self:_SendMessage(string.format("Nothing loaded!\nTroop limit: %d | Crate limit %d",trooplimit,cratelimit), 10, false, Group) + end + return self +end + +--- (Internal) Function to check if a unit is a Hercules C-130. +-- @param #CTLD self +-- @param Wrapper.Unit#UNIT Unit +-- @return #boolean Outcome +function CTLD:IsHercules(Unit) + if Unit:GetTypeName() == "Hercules" then + return true + else + return false + end +end + +--- (Internal) Function to unload troops from heli. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Unit#UNIT Unit +function CTLD:_UnloadTroops(Group, Unit) + self:T(self.lid .. " _UnloadTroops") + -- check if we are in LOAD zone + local droppingatbase = false + local inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) + if inzone then + droppingatbase = true + end + -- check for hover unload + local hoverunload = self:IsCorrectHover(Unit) --if true we\'re hovering in parameters + local IsHerc = self:IsHercules(Unit) + if IsHerc then + -- no hover but airdrop here + hoverunload = self:IsCorrectFlightParameters(Unit) + end + -- check if we\'re landed + local grounded = not self:IsUnitInAir(Unit) + -- Get what we have loaded + local unitname = Unit:GetName() + if self.Loaded_Cargo[unitname] and (grounded or hoverunload) then + if not droppingatbase or self.debug then + local loadedcargo = self.Loaded_Cargo[unitname] or {} -- #CTLD.LoadedCargo + -- looking for troops + local cargotable = loadedcargo.Cargo + for _,_cargo in pairs (cargotable) do + local cargo = _cargo -- #CTLD_CARGO + local type = cargo:GetType() -- #CTLD_CARGO.Enum + if type == CTLD_CARGO.Enum.TROOPS and not cargo:WasDropped() then + -- unload troops + local name = cargo:GetName() or "none" + local temptable = cargo:GetTemplates() or {} + local position = Group:GetCoordinate() + local zoneradius = 100 -- drop zone radius + local factor = 1 + if IsHerc then + factor = cargo:GetCratesNeeded() or 1 -- spread a bit more if airdropping + zoneradius = Unit:GetVelocityMPS() or 100 + end + local zone = ZONE_GROUP:New(string.format("Unload zone-%s",unitname),Group,zoneradius*factor) + local randomcoord = zone:GetRandomCoordinate(10,30*factor):GetVec2() + for _,_template in pairs(temptable) do + self.TroopCounter = self.TroopCounter + 1 + local alias = string.format("%s-%d", _template, math.random(1,100000)) + self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) + :InitRandomizeUnits(true,20,2) + :InitDelayOff() + :SpawnFromVec2(randomcoord) + if self.movetroopstowpzone then + self:_MoveGroupToZone(self.DroppedTroops[self.TroopCounter]) + end + end -- template loop + cargo:SetWasDropped(true) + self:_SendMessage(string.format("Dropped Troops %s into action!",name), 10, false, Group) + self:__TroopsDeployed(1, Group, Unit, self.DroppedTroops[self.TroopCounter]) + end -- if type end + end -- cargotable loop + else -- droppingatbase + self:_SendMessage("Troops have returned to base!", 10, false, Group) + self:__TroopsRTB(1, Group, Unit) + end + -- cleanup load list + local loaded = {} -- #CTLD.LoadedCargo + loaded.Troopsloaded = 0 + loaded.Cratesloaded = 0 + loaded.Cargo = {} + local loadedcargo = self.Loaded_Cargo[unitname] or {} -- #CTLD.LoadedCargo + local cargotable = loadedcargo.Cargo or {} + for _,_cargo in pairs (cargotable) do + local cargo = _cargo -- #CTLD_CARGO + local type = cargo:GetType() -- #CTLD_CARGO.Enum + local dropped = cargo:WasDropped() + if type ~= CTLD_CARGO.Enum.TROOP and not dropped then + table.insert(loaded.Cargo,_cargo) + loaded.Cratesloaded = loaded.Cratesloaded + 1 + end + end + self.Loaded_Cargo[unitname] = nil + self.Loaded_Cargo[unitname] = loaded + else + if IsHerc then + self:_SendMessage("Nothing loaded or not within airdrop parameters!", 10, false, Group) + else + self:_SendMessage("Nothing loaded or not hovering within parameters!", 10, false, Group) + end + end + return self +end + +--- (Internal) Function to unload crates from heli. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrappe.Unit#UNIT Unit +function CTLD:_UnloadCrates(Group, Unit) + self:T(self.lid .. " _UnloadCrates") + + if not self.dropcratesanywhere then -- #1570 + -- check if we are in DROP zone + local inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.DROP) + if not inzone then + self:_SendMessage("You are not close enough to a drop zone!", 10, false, Group) + if not self.debug then + return self + end + end + end + -- check for hover unload + local hoverunload = self:IsCorrectHover(Unit) --if true we\'re hovering in parameters + local IsHerc = self:IsHercules(Unit) + if IsHerc then + -- no hover but airdrop here + hoverunload = self:IsCorrectFlightParameters(Unit) + end + -- check if we\'re landed + local grounded = not self:IsUnitInAir(Unit) + -- Get what we have loaded + local unitname = Unit:GetName() + if self.Loaded_Cargo[unitname] and (grounded or hoverunload) then + local loadedcargo = self.Loaded_Cargo[unitname] or {} -- #CTLD.LoadedCargo + -- looking for troops + local cargotable = loadedcargo.Cargo + for _,_cargo in pairs (cargotable) do + local cargo = _cargo -- #CTLD_CARGO + local type = cargo:GetType() -- #CTLD_CARGO.Enum + if type ~= CTLD_CARGO.Enum.TROOPS and not cargo:WasDropped() then + -- unload crates + self:_GetCrates(Group, Unit, cargo, 1, true) + cargo:SetWasDropped(true) + cargo:SetHasMoved(true) + end + end + -- cleanup load list + local loaded = {} -- #CTLD.LoadedCargo + loaded.Troopsloaded = 0 + loaded.Cratesloaded = 0 + loaded.Cargo = {} + for _,_cargo in pairs (cargotable) do + local cargo = _cargo -- #CTLD_CARGO + local type = cargo:GetType() -- #CTLD_CARGO.Enum + local size = cargo:GetCratesNeeded() + if type == CTLD_CARGO.Enum.TROOP then + table.insert(loaded.Cargo,_cargo) + loaded.Cratesloaded = loaded.Troopsloaded + size + end + end + self.Loaded_Cargo[unitname] = nil + self.Loaded_Cargo[unitname] = loaded + else + if IsHerc then + self:_SendMessage("Nothing loaded or not within airdrop parameters!", 10, false, Group) + else + self:_SendMessage("Nothing loaded or not hovering within parameters!", 10, false, Group) + end + end + return self +end + +--- (Internal) Function to build nearby crates. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrappe.Unit#UNIT Unit +function CTLD:_BuildCrates(Group, Unit) + self:T(self.lid .. " _BuildCrates") + -- get nearby crates + local finddist = self.CrateDistance or 30 + local crates,number = self:_FindCratesNearby(Group,Unit, finddist) -- #table + local buildables = {} + local foundbuilds = false + local canbuild = false + if number > 0 then + -- get dropped crates + for _,_crate in pairs(crates) do + local Crate = _crate -- #CTLD_CARGO + if Crate:WasDropped() then + -- we can build these - maybe + local name = Crate:GetName() + local required = Crate:GetCratesNeeded() + local template = Crate:GetTemplates() + local ctype = Crate:GetType() + if not buildables[name] then + local object = {} -- #CTLD.Buildable + object.Name = name + object.Required = required + object.Found = 1 + object.Template = template + object.CanBuild = false + object.Type = ctype -- #CTLD_CARGO.Enum + buildables[name] = object + foundbuilds = true + else + buildables[name].Found = buildables[name].Found + 1 + foundbuilds = true + end + if buildables[name].Found >= buildables[name].Required then + buildables[name].CanBuild = true + canbuild = true + end + self:T({buildables = buildables}) + end -- end dropped + end -- end crate loop + -- ok let\'s list what we have + local report = REPORT:New("Checklist Buildable Crates") + report:Add("------------------------------------------------------------") + for _,_build in pairs(buildables) do + local build = _build -- Object table from above + local name = build.Name + local needed = build.Required + local found = build.Found + local txtok = "NO" + if build.CanBuild then + txtok = "YES" + end + local text = string.format("Type: %s | Required %d | Found %d | Can Build %s", name, needed, found, txtok) + report:Add(text) + end -- end list buildables + if not foundbuilds then report:Add(" --- None Found ---") end + report:Add("------------------------------------------------------------") + local text = report:Text() + self:_SendMessage(text, 30, true, Group) + -- let\'s get going + if canbuild then + -- loop again + for _,_build in pairs(buildables) do + local build = _build -- #CTLD.Buildable + if build.CanBuild then + self:_CleanUpCrates(crates,build,number) + self:_BuildObjectFromCrates(Group,Unit,build) + end + end + end + else + self:_SendMessage(string.format("No crates within %d meters!",finddist), 10, false, Group) + end -- number > 0 + return self +end + +--- (Internal) Function to actually SPAWN buildables in the mission. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Group#UNIT Unit +-- @param #CTLD.Buildable Build +function CTLD:_BuildObjectFromCrates(Group,Unit,Build) + self:T(self.lid .. " _BuildObjectFromCrates") + -- Spawn-a-crate-content + local position = Unit:GetCoordinate() or Group:GetCoordinate() + local unitname = Unit:GetName() or Group:GetName() + local name = Build.Name + local type = Build.Type -- #CTLD_CARGO.Enum + local canmove = false + if type == CTLD_CARGO.Enum.VEHICLE then canmove = true end + local temptable = Build.Template or {} + local zone = ZONE_GROUP:New(string.format("Unload zone-%s",unitname),Group,100) + local randomcoord = zone:GetRandomCoordinate(35):GetVec2() + for _,_template in pairs(temptable) do + self.TroopCounter = self.TroopCounter + 1 + local alias = string.format("%s-%d", _template, math.random(1,100000)) + self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) + :InitRandomizeUnits(true,20,2) + :InitDelayOff() + :SpawnFromVec2(randomcoord) + if self.movetroopstowpzone and canmove then + self:_MoveGroupToZone(self.DroppedTroops[self.TroopCounter]) + end + self:__CratesBuild(1,Group,Unit,self.DroppedTroops[self.TroopCounter]) + end -- template loop + return self +end + +--- (Internal) Function to move group to WP zone. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group The Group to move. +function CTLD:_MoveGroupToZone(Group) + self:T(self.lid .. " _MoveGroupToZone") + local groupname = Group:GetName() or "none" + local groupcoord = Group:GetCoordinate() + -- Get closest zone of type + local outcome, name, zone, distance = self:IsUnitInZone(Group,CTLD.CargoZoneType.MOVE) + --self:Tstring.format("Closest WP zone %s is %d meters",name,distance)) + if (distance <= self.movetroopsdistance) and zone then + -- yes, we can ;) + local groupname = Group:GetName() + local zonecoord = zone:GetRandomCoordinate(20,125) -- Core.Point#COORDINATE + local coordinate = zonecoord:GetVec2() + Group:SetAIOn() + Group:OptionAlarmStateAuto() + Group:OptionDisperseOnAttack(30) + Group:OptionROEOpenFirePossible() + Group:RouteToVec2(coordinate,5) + end + return self +end + +--- (Internal) Housekeeping - Cleanup crates when build +-- @param #CTLD self +-- @param #table Crates Table of #CTLD_CARGO objects near the unit. +-- @param #CTLD.Buildable Build Table build object. +-- @param #number Number Number of objects in Crates (found) to limit search. +function CTLD:_CleanUpCrates(Crates,Build,Number) + self:T(self.lid .. " _CleanUpCrates") + -- clean up real world crates + local build = Build -- #CTLD.Buildable + local existingcrates = self.Spawned_Cargo -- #table of exising crates + local newexcrates = {} + -- get right number of crates to destroy + local numberdest = Build.Required + local nametype = Build.Name + local found = 0 + local rounds = Number + local destIDs = {} + + -- loop and find matching IDs in the set + for _,_crate in pairs(Crates) do + local nowcrate = _crate -- #CTLD_CARGO + local name = nowcrate:GetName() + local thisID = nowcrate:GetID() + if name == nametype then -- matching crate type + table.insert(destIDs,thisID) + found = found + 1 + nowcrate:GetPositionable():Destroy() + nowcrate.Positionable = nil + end + if found == numberdest then break end -- got enough + end + -- loop and remove from real world representation + for _,_crate in pairs(existingcrates) do + local excrate = _crate -- #CTLD_CARGO + local ID = excrate:GetID() + for _,_ID in pairs(destIDs) do + if ID ~= _ID then + table.insert(newexcrates,_crate) + end + end + end + + -- reset Spawned_Cargo + self.Spawned_Cargo = nil + self.Spawned_Cargo = newexcrates + return self +end + +--- (Internal) Housekeeping - Function to refresh F10 menus. +-- @param #CTLD self +-- @return #CTLD self +function CTLD:_RefreshF10Menus() + self:T(self.lid .. " _RefreshF10Menus") + local PlayerSet = self.PilotGroups -- Core.Set#SET_GROUP + local PlayerTable = PlayerSet:GetSetObjects() -- #table of #GROUP objects + -- rebuild units table + local _UnitList = {} + for _key, _group in pairs (PlayerTable) do + local _unit = _group:GetUnit(1) -- Wrapper.Unit#UNIT Asume that there is only one unit in the flight for players + if _unit then + if _unit:IsAlive() and _unit:IsPlayer() then + if _unit:IsHelicopter() or (_unit:GetTypeName() == "Hercules" and self.enableHercules) then --ensure no stupid unit entries here + local unitName = _unit:GetName() + _UnitList[unitName] = unitName + end + end -- end isAlive + end -- end if _unit + end -- end for + self.CtldUnits = _UnitList + + -- build unit menus + local menucount = 0 + local menus = {} + for _, _unitName in pairs(self.CtldUnits) do + if not self.MenusDone[_unitName] then + local _unit = UNIT:FindByName(_unitName) -- Wrapper.Unit#UNIT + if _unit then + local _group = _unit:GetGroup() -- Wrapper.Group#GROUP + if _group then + -- get chopper capabilities + local unittype = _unit:GetTypeName() + local capabilities = self:_GetUnitCapabilities(_unit) -- #CTLD.UnitCapabilities + local cantroops = capabilities.troops + local cancrates = capabilities.crates + -- top menu + local topmenu = MENU_GROUP:New(_group,"CTLD",nil) + local topcrates = MENU_GROUP:New(_group,"Manage Crates",topmenu) + local toptroops = MENU_GROUP:New(_group,"Manage Troops",topmenu) + local listmenu = MENU_GROUP_COMMAND:New(_group,"List boarded cargo",topmenu, self._ListCargo, self, _group, _unit) + local smokemenu = MENU_GROUP_COMMAND:New(_group,"Smoke zones nearby",topmenu, self.SmokeZoneNearBy, self, _unit, false) + local smokemenu = MENU_GROUP_COMMAND:New(_group,"Flare zones nearby",topmenu, self.SmokeZoneNearBy, self, _unit, true):Refresh() + -- sub menus + -- sub menu crates management + if cancrates then + local loadmenu = MENU_GROUP_COMMAND:New(_group,"Load crates",topcrates, self._LoadCratesNearby, self, _group, _unit) + local cratesmenu = MENU_GROUP:New(_group,"Get Crates",topcrates) + for _,_entry in pairs(self.Cargo_Crates) do + local entry = _entry -- #CTLD_CARGO + menucount = menucount + 1 + local menutext = string.format("Get crate for %s",entry.Name) + menus[menucount] = MENU_GROUP_COMMAND:New(_group,menutext,cratesmenu,self._GetCrates, self, _group, _unit, entry) + end + listmenu = MENU_GROUP_COMMAND:New(_group,"List crates nearby",topcrates, self._ListCratesNearby, self, _group, _unit) + local unloadmenu = MENU_GROUP_COMMAND:New(_group,"Drop crates",topcrates, self._UnloadCrates, self, _group, _unit) + local buildmenu = MENU_GROUP_COMMAND:New(_group,"Build crates",topcrates, self._BuildCrates, self, _group, _unit):Refresh() + end + -- sub menu troops management + if cantroops then + local troopsmenu = MENU_GROUP:New(_group,"Load troops",toptroops) + for _,_entry in pairs(self.Cargo_Troops) do + local entry = _entry -- #CTLD_CARGO + menucount = menucount + 1 + menus[menucount] = MENU_GROUP_COMMAND:New(_group,entry.Name,troopsmenu,self._LoadTroops, self, _group, _unit, entry) + end + local unloadmenu1 = MENU_GROUP_COMMAND:New(_group,"Drop troops",toptroops, self._UnloadTroops, self, _group, _unit):Refresh() + local extractMenu1 = MENU_GROUP_COMMAND:New(_group, "Extract troops", toptroops, self._ExtractTroops, self, _group, _unit):Refresh() + end + local rbcns = MENU_GROUP_COMMAND:New(_group,"List active zone beacons",topmenu, self._ListRadioBeacons, self, _group, _unit) + if unittype == "Hercules" then + local hoverpars = MENU_GROUP_COMMAND:New(_group,"Show flight parameters",topmenu, self._ShowFlightParams, self, _group, _unit):Refresh() + else + local hoverpars = MENU_GROUP_COMMAND:New(_group,"Show hover parameters",topmenu, self._ShowHoverParams, self, _group, _unit):Refresh() + end + self.MenusDone[_unitName] = true + end -- end group + end -- end unit + else -- menu build check + self:T(self.lid .. " Menus already done for this group!") + end -- end menu build check + end -- end for + return self + end + +--- User function - Add *generic* troop type loadable as cargo. This type will load directly into the heli without crates. +-- @param #CTLD self +-- @param #string Name Unique name of this type of troop. E.g. "Anti-Air Small". +-- @param #table Templates Table of #string names of late activated Wrapper.Group#GROUP making up this troop. +-- @param #CTLD_CARGO.Enum Type Type of cargo, here TROOPS - these will move to a nearby destination zone when dropped/build. +-- @param #number NoTroops Size of the group in number of Units across combined templates (for loading). +function CTLD:AddTroopsCargo(Name,Templates,Type,NoTroops) + self:T(self.lid .. " AddTroopsCargo") + self.CargoCounter = self.CargoCounter + 1 + -- Troops are directly loadable + local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Templates,Type,false,true,NoTroops) + table.insert(self.Cargo_Troops,cargo) + return self +end + +--- User function - Add *generic* crate-type loadable as cargo. This type will create crates that need to be loaded, moved, dropped and built. +-- @param #CTLD self +-- @param #string Name Unique name of this type of cargo. E.g. "Humvee". +-- @param #table Templates Table of #string names of late activated Wrapper.Group#GROUP building this cargo. +-- @param #CTLD_CARGO.Enum Type Type of cargo. I.e. VEHICLE or FOB. VEHICLE will move to destination zones when dropped/build, FOB stays put. +-- @param #number NoCrates Number of crates needed to build this cargo. +function CTLD:AddCratesCargo(Name,Templates,Type,NoCrates) + self:T(self.lid .. " AddCratesCargo") + self.CargoCounter = self.CargoCounter + 1 + -- Crates are not directly loadable + local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Templates,Type,false,false,NoCrates) + table.insert(self.Cargo_Crates,cargo) + return self +end + +--- User function - Add a #CTLD.CargoZoneType zone for this CTLD instance. +-- @param #CTLD self +-- @param #CTLD.CargoZone Zone Zone #CTLD.CargoZone describing the zone. +function CTLD:AddZone(Zone) + self:T(self.lid .. " AddZone") + local zone = Zone -- #CTLD.CargoZone + if zone.type == CTLD.CargoZoneType.LOAD then + table.insert(self.pickupZones,zone) + elseif zone.type == CTLD.CargoZoneType.DROP then + table.insert(self.dropOffZones,zone) + else + table.insert(self.wpZones,zone) + end + return self +end + +--- User function - Activate Name #CTLD.CargoZone.Type ZoneType for this CTLD instance. +-- @param #CTLD self +-- @param #string Name Name of the zone to change in the ME. +-- @param #CTLD.CargoZoneTyp ZoneType Type of zone this belongs to. +-- @param #boolean NewState (Optional) Set to true to activate, false to switch off. +function CTLD:ActivateZone(Name,ZoneType,NewState) + self:T(self.lid .. " AddZone") + local newstate = true + -- set optional in case we\'re deactivating + if NewState ~= nil then + newstate = NewState + end + + -- get correct table + local table = {} + if ZoneType == CTLD.CargoZoneType.LOAD then + table = self.pickupZones + elseif ZoneType == CTLD.CargoZoneType.DROP then + table = self.dropOffZones + else + table = self.wpZones + end + -- loop table + for _,_zone in pairs(table) do + local thiszone = _zone --#CTLD.CargoZone + if thiszone.name == Name then + thiszone.active = newstate + break + end + end + return self +end + + +--- User function - Deactivate Name #CTLD.CargoZoneType ZoneType for this CTLD instance. +-- @param #CTLD self +-- @param #string Name Name of the zone to change in the ME. +-- @param #CTLD.CargoZoneTyp ZoneType Type of zone this belongs to. +function CTLD:DeactivateZone(Name,ZoneType) + self:T(self.lid .. " AddZone") + self:ActivateZone(Name,ZoneType,false) + return self +end + +--- (Internal) Function to obtain a valid FM frequency. +-- @param #CTLD self +-- @param #string Name Name of zone. +-- @return #CTLD.ZoneBeacon Beacon Beacon table. +function CTLD:_GetFMBeacon(Name) + self:T(self.lid .. " _GetFMBeacon") + local beacon = {} -- #CTLD.ZoneBeacon + if #self.FreeFMFrequencies <= 1 then + self.FreeFMFrequencies = self.UsedFMFrequencies + self.UsedFMFrequencies = {} + end + --random + local FM = table.remove(self.FreeFMFrequencies, math.random(#self.FreeFMFrequencies)) + table.insert(self.UsedFMFrequencies, FM) + beacon.name = Name + beacon.frequency = FM / 1000000 + beacon.modulation = radio.modulation.FM + return beacon +end + +--- (Internal) Function to obtain a valid UHF frequency. +-- @param #CTLD self +-- @param #string Name Name of zone. +-- @return #CTLD.ZoneBeacon Beacon Beacon table. +function CTLD:_GetUHFBeacon(Name) + self:T(self.lid .. " _GetUHFBeacon") + local beacon = {} -- #CTLD.ZoneBeacon + if #self.FreeUHFFrequencies <= 1 then + self.FreeUHFFrequencies = self.UsedUHFFrequencies + self.UsedUHFFrequencies = {} + end + --random + local UHF = table.remove(self.FreeUHFFrequencies, math.random(#self.FreeUHFFrequencies)) + table.insert(self.UsedUHFFrequencies, UHF) + beacon.name = Name + beacon.frequency = UHF / 1000000 + beacon.modulation = radio.modulation.AM + + return beacon +end + +--- (Internal) Function to obtain a valid VHF frequency. +-- @param #CTLD self +-- @param #string Name Name of zone. +-- @return #CTLD.ZoneBeacon Beacon Beacon table. +function CTLD:_GetVHFBeacon(Name) + self:T(self.lid .. " _GetVHFBeacon") + local beacon = {} -- #CTLD.ZoneBeacon + if #self.FreeVHFFrequencies <= 3 then + self.FreeVHFFrequencies = self.UsedVHFFrequencies + self.UsedVHFFrequencies = {} + end + --get random + local VHF = table.remove(self.FreeVHFFrequencies, math.random(#self.FreeVHFFrequencies)) + table.insert(self.UsedVHFFrequencies, VHF) + beacon.name = Name + beacon.frequency = VHF / 1000000 + beacon.modulation = radio.modulation.FM + return beacon +end + + +--- User function - Crates and adds a #CTLD.CargoZone zone for this CTLD instance. +-- Zones of type LOAD: Players load crates and troops here. +-- Zones of type DROP: Players can drop crates here. Note that troops can be unloaded anywhere. +-- Zone of type MOVE: Dropped troops and vehicles will start moving to the nearest zone of this type (also see options). +-- @param #CTLD self +-- @param #string Name Name of this zone, as in Mission Editor. +-- @param #string Type Type of this zone, #CTLD.CargoZoneType +-- @param #number Color Smoke/Flare color e.g. #SMOKECOLOR.Red +-- @param #string Active Is this zone currently active? +-- @param #string HasBeacon Does this zone have a beacon if it is active? +-- @return #CTLD self +function CTLD:AddCTLDZone(Name, Type, Color, Active, HasBeacon) + self:T(self.lid .. " AddCTLDZone") + + local ctldzone = {} -- #CTLD.CargoZone + ctldzone.active = Active or false + ctldzone.color = Color or SMOKECOLOR.Red + ctldzone.name = Name or "NONE" + ctldzone.type = Type or CTLD.CargoZoneType.MOVE -- #CTLD.CargoZoneType + ctldzone.hasbeacon = HasBeacon or false + + if HasBeacon then + ctldzone.fmbeacon = self:_GetFMBeacon(Name) + ctldzone.uhfbeacon = self:_GetUHFBeacon(Name) + ctldzone.vhfbeacon = self:_GetVHFBeacon(Name) + else + ctldzone.fmbeacon = nil + ctldzone.uhfbeacon = nil + ctldzone.vhfbeacon = nil + end + + self:AddZone(ctldzone) + return self +end + +--- (Internal) Function to show list of radio beacons +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Unit#UNIT Unit +function CTLD:_ListRadioBeacons(Group, Unit) + self:T(self.lid .. " _ListRadioBeacons") + local report = REPORT:New("Active Zone Beacons") + report:Add("------------------------------------------------------------") + local zones = {[1] = self.pickupZones, [2] = self.wpZones, [3] = self.dropOffZones} + for i=1,3 do + for index,cargozone in pairs(zones[i]) do + -- Get Beacon object from zone + local czone = cargozone -- #CTLD.CargoZone + if czone.active and czone.hasbeacon then + local FMbeacon = czone.fmbeacon -- #CTLD.ZoneBeacon + local VHFbeacon = czone.vhfbeacon -- #CTLD.ZoneBeacon + local UHFbeacon = czone.uhfbeacon -- #CTLD.ZoneBeacon + local Name = czone.name + local FM = FMbeacon.frequency -- MHz + local VHF = VHFbeacon.frequency * 1000 -- KHz + local UHF = UHFbeacon.frequency -- MHz + report:AddIndent(string.format(" %s | FM %s Mhz | VHF %s KHz | UHF %s Mhz ", Name, FM, VHF, UHF),"|") + end + end + end + if report:GetCount() == 1 then + report:Add(" N O N E") + end + report:Add("------------------------------------------------------------") + self:_SendMessage(report:Text(), 30, true, Group) + return self +end + +--- (Internal) Add radio beacon to zone. Runs 30 secs. +-- @param #CTLD self +-- @param #string Name Name of zone. +-- @param #string Sound Name of soundfile. +-- @param #number Mhz Frequency in Mhz. +-- @param #number Modulation Modulation AM or FM. +function CTLD:_AddRadioBeacon(Name, Sound, Mhz, Modulation) + self:T(self.lid .. " _AddRadioBeacon") + local Zone = ZONE:FindByName(Name) + local Sound = Sound or "beacon.ogg" + if Zone then + local ZoneCoord = Zone:GetCoordinate() + local ZoneVec3 = ZoneCoord:GetVec3() + local Frequency = Mhz * 1000000 -- Freq in Hertz + local Sound = "l10n/DEFAULT/"..Sound + trigger.action.radioTransmission(Sound, ZoneVec3, Modulation, false, Frequency, 1000) -- Beacon in MP only runs for 30secs straight + end + return self +end + +--- (Internal) Function to refresh radio beacons +-- @param #CTLD self +function CTLD:_RefreshRadioBeacons() + self:T(self.lid .. " _RefreshRadioBeacons") + + local zones = {[1] = self.pickupZones, [2] = self.wpZones, [3] = self.dropOffZones} + for i=1,3 do + for index,cargozone in pairs(zones[i]) do + -- Get Beacon object from zone + local czone = cargozone -- #CTLD.CargoZone + local Sound = self.RadioSound + if czone.active and czone.hasbeacon then + local FMbeacon = czone.fmbeacon -- #CTLD.ZoneBeacon + local VHFbeacon = czone.vhfbeacon -- #CTLD.ZoneBeacon + local UHFbeacon = czone.uhfbeacon -- #CTLD.ZoneBeacon + local Name = czone.name + local FM = FMbeacon.frequency -- MHz + local VHF = VHFbeacon.frequency -- KHz + local UHF = UHFbeacon.frequency -- MHz + self:_AddRadioBeacon(Name,Sound,FM,radio.modulation.FM) + self:_AddRadioBeacon(Name,Sound,VHF,radio.modulation.FM) + self:_AddRadioBeacon(Name,Sound,UHF,radio.modulation.AM) + end + end + end + return self +end + +--- (Internal) Function to see if a unit is in a specific zone type. +-- @param #CTLD self +-- @param Wrapper.Unit#UNIT Unit Unit +-- @param #CTLD.CargoZoneType Zonetype Zonetype +-- @return #boolean Outcome Is in zone or not +-- @return #string name Closest zone name +-- @return #string zone Closest Core.Zone#ZONE object +-- @return #number distance Distance to closest zone +function CTLD:IsUnitInZone(Unit,Zonetype) + self:T(self.lid .. " IsUnitInZone") + local unitname = Unit:GetName() + local zonetable = {} + local outcome = false + if Zonetype == CTLD.CargoZoneType.LOAD then + zonetable = self.pickupZones -- #table + elseif Zonetype == CTLD.CargoZoneType.DROP then + zonetable = self.dropOffZones -- #table + else + zonetable = self.wpZones -- #table + end + --- now see if we\'re in + local zonecoord = nil + local colorret = nil + local maxdist = 1000000 -- 100km + local zoneret = nil + local zonenameret = nil + for _,_cargozone in pairs(zonetable) do + local czone = _cargozone -- #CTLD.CargoZone + local unitcoord = Unit:GetCoordinate() + local zonename = czone.name + local zone = ZONE:FindByName(zonename) + zonecoord = zone:GetCoordinate() + local active = czone.active + local color = czone.color + local zoneradius = zone:GetRadius() + local distance = self:_GetDistance(zonecoord,unitcoord) + if distance <= zoneradius and active then + outcome = true + end + if maxdist > distance then + maxdist = distance + zoneret = zone + zonenameret = zonename + colorret = color + end + end + return outcome, zonenameret, zoneret, maxdist +end + +--- User function - Start smoke in a zone close to the Unit. +-- @param #CTLD self +-- @param Wrapper.Unit#UNIT Unit The Unit. +-- @param #boolean Flare If true, flare instead. +function CTLD:SmokeZoneNearBy(Unit, Flare) + self:T(self.lid .. " SmokeZoneNearBy") + -- table of #CTLD.CargoZone table + local unitcoord = Unit:GetCoordinate() + local Group = Unit:GetGroup() + local smokedistance = self.smokedistance + local smoked = false + local zones = {[1] = self.pickupZones, [2] = self.wpZones, [3] = self.dropOffZones} + for i=1,3 do + for index,cargozone in pairs(zones[i]) do + local CZone = cargozone --#CTLD.CargoZone + local zonename = CZone.name + local zone = ZONE:FindByName(zonename) + local zonecoord = zone:GetCoordinate() + local active = CZone.active + local color = CZone.color + local distance = self:_GetDistance(zonecoord,unitcoord) + if distance < smokedistance and active then + -- smoke zone since we\'re nearby + if not Flare then + zonecoord:Smoke(color or SMOKECOLOR.White) + else + if color == SMOKECOLOR.Blue then color = FLARECOLOR.White end + zonecoord:Flare(color or FLARECOLOR.White) + end + local txt = "smoking" + if Flare then txt = "flaring" end + self:_SendMessage(string.format("Roger, %s zone %s!",txt, zonename), 10, false, Group) + smoked = true + end + end + end + if not smoked then + local distance = UTILS.MetersToNM(self.smokedistance) + self:_SendMessage(string.format("Negative, need to be closer than %dnm to a zone!",distance), 10, false, Group) + end + return self +end + + --- User - Function to add/adjust unittype capabilities. + -- @param #CTLD self + -- @param #string Unittype The unittype to adjust. If passed as Wrapper.Unit#UNIT, it will search for the unit in the mission. + -- @param #boolean Cancrates Unit can load crates. + -- @param #boolean Cantroops Unit can load troops. + -- @param #number Cratelimit Unit can carry number of crates. + -- @param #number Trooplimit Unit can carry number of troops. + function CTLD:UnitCapabilities(Unittype, Cancrates, Cantroops, Cratelimit, Trooplimit) + self:T(self.lid .. " UnitCapabilities") + local unittype = nil + local unit = nil + if type(Unittype) == "string" then + unittype = Unittype + elseif type(Unittype) == "table" then + unit = UNIT:FindByName(Unittype) -- Wrapper.Unit#UNIT + unittype = unit:GetTypeName() + else + return self + end + -- set capabilities + local capabilities = {} -- #CTLD.UnitCapabilities + capabilities.type = unittype + capabilities.crates = Cancrates or false + capabilities.troops = Cantroops or false + capabilities.cratelimit = Cratelimit or 0 + capabilities.trooplimit = Trooplimit or 0 + self.UnitTypes[unittype] = capabilities + return self + end + + --- (Internal) Check if a unit is hovering *in parameters*. + -- @param #CTLD self + -- @param Wrapper.Unit#UNIT Unit + -- @return #boolean Outcome + function CTLD:IsCorrectHover(Unit) + self:T(self.lid .. " IsCorrectHover") + local outcome = false + -- see if we are in air and within parameters. + if self:IsUnitInAir(Unit) then + -- get speed and height + local uspeed = Unit:GetVelocityMPS() + local uheight = Unit:GetHeight() + local ucoord = Unit:GetCoordinate() + local gheight = ucoord:GetLandHeight() + local aheight = uheight - gheight -- height above ground + local maxh = self.maximumHoverHeight -- 15 + local minh = self.minimumHoverHeight -- 5 + local mspeed = 2 -- 2 m/s + if (uspeed <= mspeed) and (aheight <= maxh) and (aheight >= minh) then + -- yep within parameters + outcome = true + end + end + return outcome + end + + --- (Internal) Check if a Hercules is flying *in parameters* for air drops. + -- @param #CTLD self + -- @param Wrapper.Unit#UNIT Unit + -- @return #boolean Outcome + function CTLD:IsCorrectFlightParameters(Unit) + self:T(self.lid .. " IsCorrectFlightParameters") + local outcome = false + -- see if we are in air and within parameters. + if self:IsUnitInAir(Unit) then + -- get speed and height + local uspeed = Unit:GetVelocityMPS() + local uheight = Unit:GetHeight() + local ucoord = Unit:GetCoordinate() + local gheight = ucoord:GetLandHeight() + local aheight = uheight - gheight -- height above ground + local maxh = self.HercMinAngels-- 1500m + local minh = self.HercMaxAngels -- 5000m + local maxspeed = self.HercMaxSpeed -- 77 mps + -- DONE: TEST - Speed test for Herc, should not be above 280kph/150kn + local kmspeed = uspeed * 3.6 + local knspeed = kmspeed / 1.86 + self:T(string.format("%s Unit parameters: at %dm AGL with %dmps | %dkph | %dkn",self.lid,aheight,uspeed,kmspeed,knspeed)) + if (aheight <= maxh) and (aheight >= minh) and (uspeed <= maxspeed) then + -- yep within parameters + outcome = true + end + end + return outcome + end + + --- (Internal) List if a unit is hovering *in parameters*. + -- @param #CTLD self + -- @param Wrapper.Group#GROUP Group + -- @param Wrapper.Unit#UNIT Unit + function CTLD:_ShowHoverParams(Group,Unit) + local inhover = self:IsCorrectHover(Unit) + local htxt = "true" + if not inhover then htxt = "false" end + local text = "" + if _SETTINGS:IsMetric() then + text = string.format("Hover parameters (autoload/drop):\n - Min height %dm \n - Max height %dm \n - Max speed 2mps \n - In parameter: %s", self.minimumHoverHeight, self.maximumHoverHeight, htxt) + else + local minheight = UTILS.MetersToFeet(self.minimumHoverHeight) + local maxheight = UTILS.MetersToFeet(self.maximumHoverHeight) + text = string.format("Hover parameters (autoload/drop):\n - Min height %dm \n - Max height %dm \n - Max speed 6fts \n - In parameter: %s", minheight, maxheight, htxt) + end + self:_SendMessage(text, 10, false, Group) + return self + end + + --- (Internal) List if a Herc unit is flying *in parameters*. + -- @param #CTLD self + -- @param Wrapper.Group#GROUP Group + -- @param Wrapper.Unit#UNIT Unit + function CTLD:_ShowFlightParams(Group,Unit) + local inhover = self:IsCorrectFlightParameters(Unit) + local htxt = "true" + if not inhover then htxt = "false" end + local text = "" + if _SETTINGS:IsImperial() then + local minheight = UTILS.MetersToFeet(self.HercMinAngels) + local maxheight = UTILS.MetersToFeet(self.HercMaxAngels) + text = string.format("Flight parameters (airdrop):\n - Min height %dft \n - Max height %dft \n - In parameter: %s", minheight, maxheight, htxt) + else + local minheight = self.HercMinAngels + local maxheight = self.HercMaxAngels + text = string.format("Flight parameters (airdrop):\n - Min height %dm \n - Max height %dm \n - In parameter: %s", minheight, maxheight, htxt) + end + self:_SendMessage(text, 10, false, Group) + return self + end + + + --- (Internal) Check if a unit is in a load zone and is hovering in parameters. + -- @param #CTLD self + -- @param Wrapper.Unit#UNIT Unit + -- @return #boolean Outcome + function CTLD:CanHoverLoad(Unit) + self:T(self.lid .. " CanHoverLoad") + if self:IsHercules(Unit) then return false end + local outcome = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) and self:IsCorrectHover(Unit) + return outcome + end + + --- (Internal) Check if a unit is above ground. + -- @param #CTLD self + -- @param Wrapper.Unit#UNIT Unit + -- @return #boolean Outcome + function CTLD:IsUnitInAir(Unit) + -- get speed and height + local minheight = self.minimumHoverHeight + if self.enableHercules and Unit:GetTypeName() == "Hercules" then + minheight = 5.1 -- herc is 5m AGL on the ground + end + local uheight = Unit:GetHeight() + local ucoord = Unit:GetCoordinate() + local gheight = ucoord:GetLandHeight() + local aheight = uheight - gheight -- height above ground + if aheight >= minheight then + return true + else + return false + end + end + + --- (Internal) Autoload if we can do crates, have capacity free and are in a load zone. + -- @param #CTLD self + -- @param Wrapper.Unit#UNIT Unit + -- @return #CTLD self + function CTLD:AutoHoverLoad(Unit) + self:T(self.lid .. " AutoHoverLoad") + -- get capabilities and current load + local unittype = Unit:GetTypeName() + local unitname = Unit:GetName() + local Group = Unit:GetGroup() + local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitCapabilities + local cancrates = capabilities.crates -- #boolean + local cratelimit = capabilities.cratelimit -- #number + if cancrates then + -- get load + local numberonboard = 0 + local loaded = {} + if self.Loaded_Cargo[unitname] then + loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo + numberonboard = loaded.Cratesloaded or 0 + end + local load = cratelimit - numberonboard + local canload = self:CanHoverLoad(Unit) + if canload and load > 0 then + self:_LoadCratesNearby(Group,Unit) + end + end + return self + end + + --- (Internal) Run through all pilots and see if we autoload. + -- @param #CTLD self + -- @return #CTLD self + function CTLD:CheckAutoHoverload() + if self.hoverautoloading then + for _,_pilot in pairs (self.CtldUnits) do + local Unit = UNIT:FindByName(_pilot) + self:AutoHoverLoad(Unit) + end + end + return self + end + + --- (Internal) Run through DroppedTroops and capture alive units + -- @param #CTLD self + -- @return #CTLD self + function CTLD:CleanDroppedTroops() + local troops = self.DroppedTroops + local newtable = {} + for _index, _group in pairs (troops) do + if _group and _group:IsAlive() then + newtable[_index] = _group + end + end + self.DroppedTroops = newtable + return self + end +------------------------------------------------------------------- +-- FSM functions +------------------------------------------------------------------- + + --- (Internal) FSM Function onafterStart. + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @return #CTLD self + function CTLD:onafterStart(From, Event, To) + self:T({From, Event, To}) + self:I(self.lid .. "Started.") + if self.useprefix or self.enableHercules then + local prefix = self.prefixes + if self.enableHercules then + self.PilotGroups = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(prefix):FilterStart() + else + self.PilotGroups = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(prefix):FilterCategories("helicopter"):FilterStart() + end + else + self.PilotGroups = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterCategories("helicopter"):FilterStart() + end + -- Events + self:HandleEvent(EVENTS.PlayerEnterAircraft, self._EventHandler) + self:HandleEvent(EVENTS.PlayerEnterUnit, self._EventHandler) + self:HandleEvent(EVENTS.PlayerLeaveUnit, self._EventHandler) + self:__Status(-5) + return self + end + + --- (Internal) FSM Function onbeforeStatus. + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @return #CTLD self + function CTLD:onbeforeStatus(From, Event, To) + self:T({From, Event, To}) + self:CleanDroppedTroops() + self:_RefreshF10Menus() + self:_RefreshRadioBeacons() + self:CheckAutoHoverload() + return self + end + + --- (Internal) FSM Function onafterStatus. + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @return #CTLD self + function CTLD:onafterStatus(From, Event, To) + self:T({From, Event, To}) + -- gather some stats + -- pilots + local pilots = 0 + for _,_pilot in pairs (self.CtldUnits) do + pilots = pilots + 1 + end + + -- spawned cargo boxes curr in field + local boxes = 0 + for _,_pilot in pairs (self.Spawned_Cargo) do + boxes = boxes + 1 + end + + local cc = self.CargoCounter + local tc = self.TroopCounter + + if self.debug or self.verbose > 0 then + local text = string.format("%s Pilots %d | Live Crates %d |\nCargo Counter %d | Troop Counter %d", self.lid, pilots, boxes, cc, tc) + local m = MESSAGE:New(text,10,"CTLD"):ToAll() + end + self:__Status(-30) + return self + end + + --- (Internal) FSM Function onafterStop. + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @return #CTLD self + function CTLD:onafterStop(From, Event, To) + self:T({From, Event, To}) + self:UnhandleEvent(EVENTS.PlayerEnterAircraft) + self:UnhandleEvent(EVENTS.PlayerEnterUnit) + self:UnhandleEvent(EVENTS.PlayerLeaveUnit) + return self + end + + --- (Internal) FSM Function onbeforeTroopsPickedUp. + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param #CTLD_CARGO Cargo Cargo crate. + -- @return #CTLD self + function CTLD:onbeforeTroopsPickedUp(From, Event, To, Group, Unit, Cargo) + self:T({From, Event, To}) + return self + end + + --- (Internal) FSM Function onbeforeCratesPickedUp. + -- @param #CTLD self + -- @param #string From State . + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param #CTLD_CARGO Cargo Cargo crate. + -- @return #CTLD self + function CTLD:onbeforeCratesPickedUp(From, Event, To, Group, Unit, Cargo) + self:T({From, Event, To}) + return self + end + + --- (Internal) FSM Function onbeforeTroopsExtracted. + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param Wrapper.Group#GROUP Troops Troops #GROUP Object. + -- @return #CTLD self + function CTLD:onbeforeTroopsExtracted(From, Event, To, Group, Unit, Troops) + self:T({From, Event, To}) + return self + end + + + --- (Internal) FSM Function onbeforeTroopsDeployed. + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param Wrapper.Group#GROUP Troops Troops #GROUP Object. + -- @return #CTLD self + function CTLD:onbeforeTroopsDeployed(From, Event, To, Group, Unit, Troops) + self:T({From, Event, To}) + return self + end + + --- (Internal) FSM Function onbeforeCratesDropped. + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param #table Cargotable Table of #CTLD_CARGO objects dropped. + -- @return #CTLD self + function CTLD:onbeforeCratesDropped(From, Event, To, Group, Unit, Cargotable) + self:T({From, Event, To}) + return self + end + + --- (Internal) FSM Function onbeforeCratesBuild. + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param Wrapper.Group#GROUP Vehicle The #GROUP object of the vehicle or FOB build. + -- @return #CTLD self + function CTLD:onbeforeCratesBuild(From, Event, To, Group, Unit, Vehicle) + self:T({From, Event, To}) + return self + end + + --- (Internal) FSM Function onbeforeTroopsRTB. + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @return #CTLD self + function CTLD:onbeforeTroopsRTB(From, Event, To, Group, Unit) + self:T({From, Event, To}) + return self + end + +end -- end do +------------------------------------------------------------------- +-- End Ops.CTLD.lua +------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/FlightGroup.lua b/Moose Development/Moose/Ops/FlightGroup.lua index a71619474..a9e42d16e 100644 --- a/Moose Development/Moose/Ops/FlightGroup.lua +++ b/Moose Development/Moose/Ops/FlightGroup.lua @@ -2757,8 +2757,9 @@ function FLIGHTGROUP:onafterFuelLow(From, Event, To) -- Get closest tanker from airwing that can refuel this flight. local tanker=self.airwing:GetTankerForFlight(self) - if tanker then + if tanker and self.fuellowrefuel then + -- Debug message. self:I(self.lid..string.format("Send to refuel at tanker %s", tanker.flightgroup:GetName())) -- Get a coordinate towards the tanker. diff --git a/Moose Development/Moose/Ops/Intelligence.lua b/Moose Development/Moose/Ops/Intelligence.lua index 9c4a3a876..0b98a85ed 100644 --- a/Moose Development/Moose/Ops/Intelligence.lua +++ b/Moose Development/Moose/Ops/Intelligence.lua @@ -35,6 +35,7 @@ -- @field #number clustercounter Running number of clusters. -- @field #number dTforget Time interval in seconds before a known contact which is not detected any more is forgotten. -- @field #number clusterradius Radius im kilometers in which groups/units are considered to belong to a cluster +-- @field #number prediction Seconds default to be used with CalcClusterFuturePosition. -- @extends Core.Fsm#FSM --- Top Secret! @@ -55,30 +56,30 @@ -- -- ## set up a detection SET_GROUP -- --- `Red_DetectionSetGroup = SET_GROUP:New()` --- `Red_DetectionSetGroup:FilterPrefixes( { "Red EWR" } )` --- `Red_DetectionSetGroup:FilterOnce()` +-- `Red_DetectionSetGroup = SET_GROUP:New()` +-- `Red_DetectionSetGroup:FilterPrefixes( { "Red EWR" } )` +-- `Red_DetectionSetGroup:FilterOnce()` -- -- ## New Intel type detection for the red side, logname "KGB" -- --- `RedIntel = INTEL:New(Red_DetectionSetGroup,"red","KGB")` --- `RedIntel:SetClusterAnalysis(true,true)` --- `RedIntel:SetVerbosity(2)` --- `RedIntel:Start()` +-- `RedIntel = INTEL:New(Red_DetectionSetGroup,"red","KGB")` +-- `RedIntel:SetClusterAnalysis(true,true)` +-- `RedIntel:SetVerbosity(2)` +-- `RedIntel:__Start(2)` -- -- ## Hook into new contacts found -- --- `function RedIntel:OnAfterNewContact(From, Event, To, Contact)` --- `local text = string.format("NEW contact %s detected by %s", Contact.groupname, Contact.recce or "unknown")` --- `local m = MESSAGE:New(text,15,"KGB"):ToAll()` --- `end` +-- `function RedIntel:OnAfterNewContact(From, Event, To, Contact)` +-- `local text = string.format("NEW contact %s detected by %s", Contact.groupname, Contact.recce or "unknown")` +-- `local m = MESSAGE:New(text,15,"KGB"):ToAll()` +-- `end` -- -- ## And/or new clusters found -- --- `function RedIntel:OnAfterNewCluster(From, Event, To, Contact, Cluster)` --- `local text = string.format("NEW cluster %d size %d with contact %s", Cluster.index, Cluster.size, Contact.groupname)` --- `local m = MESSAGE:New(text,15,"KGB"):ToAll()` --- `end` +-- `function RedIntel:OnAfterNewCluster(From, Event, To, Contact, Cluster)` +-- `local text = string.format("NEW cluster %d size %d with contact %s", Cluster.index, Cluster.size, Contact.groupname)` +-- `local m = MESSAGE:New(text,15,"KGB"):ToAll()` +-- `end` -- -- -- @field #INTEL @@ -95,6 +96,9 @@ INTEL = { Clusters = {}, clustercounter = 1, clusterradius = 15, + clusteranalysis = true, + clustermarkers = false, + prediction = 300, } --- Detected item info. @@ -112,7 +116,7 @@ INTEL = { -- @field #number speed Last known speed in m/s. -- @field #boolean isship -- @field #boolean ishelo --- @field #boolean isgrund +-- @field #boolean isground -- @field Ops.Auftrag#AUFTRAG mission The current Auftrag attached to this contact -- @field #string recce The name of the recce unit that detected this contact @@ -131,13 +135,13 @@ INTEL = { --- INTEL class version. -- @field #string version -INTEL.version="0.2.1" +INTEL.version="0.2.6" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Filter detection methods. +-- DONE: Filter detection methods. -- TODO: process detected set asynchroniously for better performance. -- DONE: Accept zones. -- DONE: Reject zones. @@ -199,7 +203,14 @@ function INTEL:New(DetectionSet, Coalition, Alias) self.alias="CIA" end end - end + end + + self.DetectVisual = true + self.DetectOptical = true + self.DetectRadar = true + self.DetectIRST = true + self.DetectRWR = true + self.DetectDLINK = true -- Set some string id for output to DCS.log file. self.lid=string.format("INTEL %s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown") @@ -218,7 +229,8 @@ function INTEL:New(DetectionSet, Coalition, Alias) self:AddTransition("*", "LostContact", "*") -- Contact could not be detected any more. self:AddTransition("*", "NewCluster", "*") -- New cluster has been detected. - self:AddTransition("*", "LostCluster", "*") -- Cluster could not be detected any more. + self:AddTransition("*", "LostCluster", "*") -- Cluster could not be detected any more. + self:AddTransition("*", "Stop", "Stopped") -- Defaults self:SetForgetTime() @@ -471,6 +483,47 @@ function INTEL:SetClusterRadius(radius) return self end +--- Set detection types for this #INTEL - all default to true. +-- @param #INTEL self +-- @param #boolean DetectVisual Visual detection +-- @param #boolean DetectOptical Optical detection +-- @param #boolean DetectRadar Radar detection +-- @param #boolean DetectIRST IRST detection +-- @param #boolean DetectRWR RWR detection +-- @param #boolean DetectDLINK Data link detection +-- @return self +function INTEL:SetDetectionTypes(DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) + self.DetectVisual = DetectVisual and true + self.DetectOptical = DetectOptical and true + self.DetectRadar = DetectRadar and true + self.DetectIRST = DetectIRST and true + self.DetectRWR = DetectRWR and true + self.DetectDLINK = DetectDLINK and true + return self +end + +--- Get table of #INTEL.Contact objects +-- @param #INTEL self +-- @return #table Contacts or nil if not running +function INTEL:GetContactTable() + if self:Is("Running") then + return self.Contacts + else + return nil + end +end + +--- Get table of #INTEL.Cluster objects +-- @param #INTEL self +-- @return #table Clusters or nil if not running +function INTEL:GetClusterTable() + if self:Is("Running") and self.clusteranalysis then + return self.Clusters + else + return nil + end +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Start & Status ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -526,7 +579,7 @@ function INTEL:onafterStatus(From, Event, To) text=text..string.format("\n- %s (%s): %s, units=%d, T=%d sec", contact.categoryname, contact.attribute, contact.groupname, contact.group:CountAliveUnits(), dT) if contact.mission then local mission=contact.mission --Ops.Auftrag#AUFTRAG - text=text..string.format(" mission name=%s type=%s target=%s", mission.name, mission.type, mission:GetTargetName() or "unkown") + text=text..string.format(" mission name=%s type=%s target=%s", mission.name, mission.type, mission:GetTargetName() or "unknown") end end self:I(self.lid..text) @@ -554,14 +607,13 @@ function INTEL:UpdateIntel() local recce=_recce --Wrapper.Unit#UNIT -- Get detected units. - self:GetDetectedUnits(recce, DetectedUnits, RecceDetecting) + self:GetDetectedUnits(recce, DetectedUnits, RecceDetecting, self.DetectVisual, self.DetectOptical, self.DetectRadar, self.DetectIRST, self.DetectRWR, self.DetectDLINK) end end end - -- TODO: Filter detection methods? local remove={} for unitname,_unit in pairs(DetectedUnits) do local unit=_unit --Wrapper.Unit#UNIT @@ -696,7 +748,7 @@ function INTEL:CreateDetectedItems(DetectedGroups, RecceDetecting) item.velocity=group:GetVelocityVec3() item.speed=group:GetVelocityMPS() item.recce=RecceDetecting[groupname] - self:T(string.format("%s group detect by %s/%s", groupname, RecceDetecting[groupname] or "unknonw", item.recce or "unknown")) + self:T(string.format("%s group detect by %s/%s", groupname, RecceDetecting[groupname] or "unknown", item.recce or "unknown")) -- Add contact to table. self:AddContact(item) @@ -724,7 +776,7 @@ function INTEL:CreateDetectedItems(DetectedGroups, RecceDetecting) end ---- Return the detected target groups of the controllable as a @{SET_GROUP}. +--- (Internal) Return the detected target groups of the controllable as a @{SET_GROUP}. -- The optional parametes specify the detection methods that can be applied. -- If no detection method is given, the detection will use all the available methods by default. -- @param #INTEL self @@ -811,7 +863,7 @@ function INTEL:onafterLostCluster(From, Event, To, Cluster, Mission) local text = self.lid..string.format("LOST cluster %d", Cluster.index) if Mission then local mission=Mission --Ops.Auftrag#AUFTRAG - text=text..string.format(" mission name=%s type=%s target=%s", mission.name, mission.type, mission:GetTargetName() or "unkown") + text=text..string.format(" mission name=%s type=%s target=%s", mission.name, mission.type, mission:GetTargetName() or "unknown") end self:T(text) end @@ -898,7 +950,7 @@ end -- Cluster Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Paint picture of the battle field. +--- [Internal] Paint picture of the battle field. Does Cluster analysis and updates clusters. Sets markers if markers are enabled. -- @param #INTEL self function INTEL:PaintPicture() @@ -918,9 +970,13 @@ function INTEL:PaintPicture() else local mission = _cluster.mission or nil local marker = _cluster.marker + local markerID = _cluster.markerID if marker then marker:Remove() end + if markerID then + COORDINATE:RemoveMark(markerID) + end self:LostCluster(_cluster, mission) end end @@ -989,12 +1045,10 @@ function INTEL:PaintPicture() if self.clustermarkers then for _,_cluster in pairs(self.Clusters) do local cluster=_cluster --#INTEL.Cluster - - local coordinate=self:GetClusterCoordinate(cluster) - - - -- Update F10 marker. - self:UpdateClusterMarker(cluster) + --local coordinate=self:GetClusterCoordinate(cluster) + -- Update F10 marker. + self:UpdateClusterMarker(cluster) + self:CalcClusterFuturePosition(cluster,self.prediction) end end end @@ -1108,6 +1162,7 @@ function INTEL:CalcClusterThreatlevelMax(cluster) local threatlevel=0 for _,_contact in pairs(cluster.Contacts) do + local contact=_contact --#INTEL.Contact if contact.threatlevel>threatlevel then @@ -1119,6 +1174,65 @@ function INTEL:CalcClusterThreatlevelMax(cluster) return threatlevel end +--- Calculate cluster heading. +-- @param #INTEL self +-- @param #INTEL.Cluster cluster The cluster of contacts. +-- @return #number Heading average of all groups in the cluster. +function INTEL:CalcClusterDirection(cluster) + + local direction = 0 + local n=0 + for _,_contact in pairs(cluster.Contacts) do + local group = _contact.group -- Wrapper.Group#GROUP + if group:IsAlive() then + direction = direction + group:GetHeading() + n=n+1 + end + end + return math.floor(direction / n) + +end + +--- Calculate cluster speed. +-- @param #INTEL self +-- @param #INTEL.Cluster cluster The cluster of contacts. +-- @return #number Speed average of all groups in the cluster in MPS. +function INTEL:CalcClusterSpeed(cluster) + + local velocity = 0 + local n=0 + for _,_contact in pairs(cluster.Contacts) do + local group = _contact.group -- Wrapper.Group#GROUP + if group:IsAlive() then + velocity = velocity + group:GetVelocityMPS() + n=n+1 + end + end + return math.floor(velocity / n) + +end + +--- Calculate cluster future position after given seconds. +-- @param #INTEL self +-- @param #INTEL.Cluster cluster The cluster of contacts. +-- @param #number seconds Timeframe in seconds. +-- @return Core.Point#COORDINATE Calculated future position of the cluster. +function INTEL:CalcClusterFuturePosition(cluster,seconds) + local speed = self:CalcClusterSpeed(cluster) -- #number MPS + local direction = self:CalcClusterDirection(cluster) -- #number heading + -- local currposition = cluster.coordinate -- Core.Point#COORDINATE + local currposition = self:GetClusterCoordinate(cluster) -- Core.Point#COORDINATE + local distance = speed * seconds -- #number in meters the cluster will travel + local futureposition = currposition:Translate(distance,direction,true,false) + if self.clustermarkers and (self.verbose > 1) then + if cluster.markerID then + COORDINATE:RemoveMark(cluster.markerID) + end + cluster.markerID = currposition:ArrowToAll(futureposition,self.coalition,{1,0,0},1,{1,1,0},0.5,2,true,"Postion Calc") + end + return futureposition +end + --- Check if contact is in any known cluster. -- @param #INTEL self @@ -1216,10 +1330,16 @@ function INTEL:GetClusterCoordinate(cluster) for _,_contact in pairs(cluster.Contacts) do local contact=_contact --#INTEL.Contact - - x=x+contact.position.x - y=y+contact.position.y - z=z+contact.position.z + local group = contact.group --Wrapper.Group#GROUP + local coord = {} + if group:IsAlive() then + coord = group:GetCoordinate() + else + coord = contact.position + end + x=x+coord.x + y=y+coord.y + z=z+coord.z n=n+1 end @@ -1319,3 +1439,246 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +---------------------------------------------------------------------------------------------- +-- Start INTEL_DLINK +---------------------------------------------------------------------------------------------- + +--- **Ops_DLink** - Support for Office of Military Intelligence. +-- +-- **Main Features:** +-- +-- * Overcome limitations of (non-available) datalinks between ground radars +-- * Detect and track contacts consistently across INTEL instances +-- * Use FSM events to link functionality into your scripts +-- * Easy setup +-- +--- === +-- +-- ### Author: **applevangelist** + +--- INTEL_DLINK class. +-- @type INTEL_DLINK +-- @field #string ClassName Name of the class. +-- @field #string lid Class id string for output to DCS log file. +-- @field #number verbose Make the logging verbose. +-- @field #string alias Alias name for logging. +-- @field #number cachetime Number of seconds to keep an object. +-- @field #number interval Number of seconds between collection runs. +-- @field #table contacts Table of Ops.Intelligence#INTEL.Contact contacts. +-- @field #table clusters Table of Ops.Intelligence#INTEL.Cluster clusters. +-- @field #table contactcoords Table of contacts' Core.Point#COORDINATE objects. +-- @extends Core.Fsm#FSM + +--- INTEL_DLINK data aggregator +-- @field #INTEL_DLINK +INTEL_DLINK = { + ClassName = "INTEL_DLINK", + verbose = 0, + lid = nil, + alias = nil, + cachetime = 300, + interval = 20, + contacts = {}, + clusters = {}, + contactcoords = {}, +} + +--- Version string +-- @field #string version +INTEL_DLINK.version = "0.0.1" + +--- Function to instantiate a new object +-- @param #INTEL_DLINK self +-- @param #table Intels Table of Ops.Intelligence#INTEL objects. +-- @param #string Alias (optional) Name of this instance. Default "SPECTRE" +-- @param #number Interval (optional) When to query #INTEL objects for detected items (default 20 seconds). +-- @param #number Cachetime (optional) How long to cache detected items (default 300 seconds). +function INTEL_DLINK:New(Intels, Alias, Interval, Cachetime) + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #INTEL + + self.intels = Intels or {} + self.contacts = {} + self.clusters = {} + self.contactcoords = {} + + -- Set alias. + if Alias then + self.alias=tostring(Alias) + else + self.alias="SPECTRE" + end + + -- Cache time + self.cachetime = Cachetime or 300 + + -- Interval + self.interval = Interval or 20 + + -- Set some string id for output to DCS.log file. + self.lid=string.format("INTEL_DLINK %s | ", self.alias) + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start FSM. + self:AddTransition("*", "Collect", "*") -- Collect data. + self:AddTransition("*", "Collected", "*") -- Collection of data done. + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + + return self +end +---------------------------------------------------------------------------------------------- +-- Helper & User Functions +---------------------------------------------------------------------------------------------- + +--- Function to add an #INTEL object to the aggregator +-- @param #INTEL_DLINK self +-- @param Ops.Intelligence#INTEL Intel the #INTEL object to add +-- @return #INTEL_DLINK self +function INTEL_DLINK:AddIntel(Intel) + self:T(self.lid .. "AddIntel") + if Intel then + table.insert(self.intels,Intel) + end + return self +end + +---------------------------------------------------------------------------------------------- +-- FSM Functions +---------------------------------------------------------------------------------------------- + +--- Function to start the work. +-- @param #INTEL_DLINK self +-- @param #string From The From state +-- @param #string Event The Event triggering this call +-- @param #string To The To state +-- @return #INTEL_DLINK self +function INTEL_DLINK:onafterStart(From, Event, To) + self:T({From, Event, To}) + local text = string.format("Version %s started.", self.version) + self:I(self.lid .. text) + self:__Collect(-math.random(1,10)) + return self +end + +--- Function to collect data from the various #INTEL +-- @param #INTEL_DLINK self +-- @param #string From The From state +-- @param #string Event The Event triggering this call +-- @param #string To The To state +-- @return #INTEL_DLINK self +function INTEL_DLINK:onbeforeCollect(From, Event, To) + self:T({From, Event, To}) + -- run through our #INTEL objects and gather the contacts tables + self:T("Contacts Data Gathering") + local newcontacts = {} + local intels = self.intels -- #table + for _,_intel in pairs (intels) do + _intel = _intel -- #INTEL + if _intel:Is("Running") then + local ctable = _intel:GetContactTable() or {} -- #INTEL.Contact + for _,_contact in pairs (ctable) do + local _ID = string.format("%s-%d",_contact.groupname, _contact.Tdetected) + self:T(string.format("Adding %s",_ID)) + newcontacts[_ID] = _contact + end + end + end + -- clean up for stale contacts and dupes + self:T("Cleanup") + local contacttable = {} + local coordtable = {} + local TNow = timer.getAbsTime() + local Tcache = self.cachetime + for _ind, _contact in pairs(newcontacts) do -- #string, #INTEL.Contact + if TNow - _contact.Tdetected < Tcache then + if (not contacttable[_contact.groupname]) or (contacttable[_contact.groupname] and contacttable[_contact.groupname].Tdetected < _contact.Tdetected) then + self:T(string.format("Adding %s",_contact.groupname)) + contacttable[_contact.groupname] = _contact + table.insert(coordtable,_contact.position) + end + end + end + -- run through our #INTEL objects and gather the clusters tables + self:T("Clusters Data Gathering") + local newclusters = {} + local intels = self.intels -- #table + for _,_intel in pairs (intels) do + _intel = _intel -- #INTEL + if _intel:Is("Running") then + local ctable = _intel:GetClusterTable() or {} -- #INTEL.Cluster + for _,_cluster in pairs (ctable) do + local _ID = string.format("%s-%d", _intel.alias, _cluster.index) + self:T(string.format("Adding %s",_ID)) + table.insert(newclusters,_cluster) + end + end + end + -- update self tables + self.contacts = contacttable + self.contactcoords = coordtable + self.clusters = newclusters + self:__Collected(1, contacttable, newclusters) -- make table available via FSM Event + -- schedule next round + local interv = self.interval * -1 + self:__Collect(interv) + return self +end + +--- Function called after collection is done +-- @param #INTEL_DLINK self +-- @param #string From The From state +-- @param #string Event The Event triggering this call +-- @param #string To The To state +-- @param #table Contacts The table of collected #INTEL.Contact contacts +-- @param #table Clusters The table of collected #INTEL.Cluster clusters +-- @return #INTEL_DLINK self +function INTEL_DLINK:onbeforeCollected(From, Event, To, Contacts, Clusters) + self:T({From, Event, To}) + return self +end + +--- Function to stop +-- @param #INTEL_DLINK self +-- @param #string From The From state +-- @param #string Event The Event triggering this call +-- @param #string To The To state +-- @return #INTEL_DLINK self +function INTEL_DLINK:onafterStop(From, Event, To) + self:T({From, Event, To}) + local text = string.format("Version %s stopped.", self.version) + self:I(self.lid .. text) + return self +end + +--- Function to query the detected contacts +-- @param #INTEL_DLINK self +-- @return #table Table of #INTEL.Contact contacts +function INTEL_DLINK:GetContactTable() + self:T(self.lid .. "GetContactTable") + return self.contacts +end + +--- Function to query the detected clusters -- not yet implemented! +-- @param #INTEL_DLINK self +-- @return #table Table of #INTEL.Cluster clusters +function INTEL_DLINK:GetClusterTable() + self:T(self.lid .. "GetClusterTable") + return self.clusters +end + +--- Function to query the detected contact coordinates +-- @param #INTEL_DLINK self +-- @return #table Table of the contacts' Core.Point#COORDINATE objects. +function INTEL_DLINK:GetDetectedItemCoordinates() + self:T(self.lid .. "GetDetectedItemCoordinates") + return self.contactcoords +end + +---------------------------------------------------------------------------------------------- +-- End INTEL_DLINK +---------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/Squadron.lua b/Moose Development/Moose/Ops/Squadron.lua index 5ed71fba7..c0f53d960 100644 --- a/Moose Development/Moose/Ops/Squadron.lua +++ b/Moose Development/Moose/Ops/Squadron.lua @@ -45,6 +45,7 @@ -- @field #table tacanChannel List of TACAN channels available to the squadron. -- @field #number radioFreq Radio frequency in MHz the squad uses. -- @field #number radioModu Radio modulation the squad uses. +-- @field #number takeoffType Take of type. -- @extends Core.Fsm#FSM --- *It is unbelievable what a squadron of twelve aircraft did to tip the balance.* -- Adolf Galland @@ -87,12 +88,13 @@ SQUADRON = { --- SQUADRON class version. -- @field #string version -SQUADRON.version="0.5.0" +SQUADRON.version="0.5.2" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO: Parking spots for squadrons? -- DONE: Engage radius. -- DONE: Modex. -- DONE: Call signs. @@ -282,6 +284,41 @@ function SQUADRON:SetGrouping(nunits) return self end + +--- Set takeoff type. All assets of this squadron will be spawned with cold (default) or hot engines. +-- Spawning on runways is not supported. +-- @param #SQUADRON self +-- @param #string TakeoffType Take off type: "Cold" (default) or "Hot" with engines on. +-- @return #SQUADRON self +function SQUADRON:SetTakeoffType(TakeoffType) + TakeoffType=TakeoffType or "Cold" + if TakeoffType:lower()=="hot" then + self.takeoffType=COORDINATE.WaypointType.TakeOffParkingHot + elseif TakeoffType:lower()=="cold" then + self.takeoffType=COORDINATE.WaypointType.TakeOffParking + else + self.takeoffType=COORDINATE.WaypointType.TakeOffParking + end + return self +end + +--- Set takeoff type cold (default). +-- @param #SQUADRON self +-- @return #SQUADRON self +function SQUADRON:SetTakeoffCold() + self:SetTakeoffType("Cold") + return self +end + +--- Set takeoff type hot. +-- @param #SQUADRON self +-- @return #SQUADRON self +function SQUADRON:SetTakeoffHot() + self:SetTakeoffType("Hot") + return self +end + + --- Set mission types this squadron is able to perform. -- @param #SQUADRON self -- @param #table MissionTypes Table of mission types. Can also be passed as a #string if only one type. diff --git a/Moose Development/Moose/Core/Radio.lua b/Moose Development/Moose/Sound/Radio.lua similarity index 50% rename from Moose Development/Moose/Core/Radio.lua rename to Moose Development/Moose/Sound/Radio.lua index 175bc2106..872ad6e26 100644 --- a/Moose Development/Moose/Core/Radio.lua +++ b/Moose Development/Moose/Sound/Radio.lua @@ -1,13 +1,10 @@ ---- **Core** - Is responsible for everything that is related to radio transmission and you can hear in DCS, be it TACAN beacons, Radio transmissions. +--- **Sound** - Radio transmissions. -- -- === -- -- ## Features: -- -- * Provide radio functionality to broadcast radio transmissions. --- * Provide beacon functionality to assist pilots. --- --- The Radio contains 2 classes : RADIO and BEACON -- -- What are radio communications in DCS? -- @@ -35,13 +32,13 @@ -- -- ### Authors: Hugues "Grey_Echo" Bousquet, funkyfranky -- --- @module Core.Radio +-- @module Sound.Radio -- @image Core_Radio.JPG ---- Models the radio capability. +--- *It's not true I had nothing on, I had the radio on.* -- Marilyn Monroe -- --- ## RADIO usage +-- # RADIO usage -- -- There are 3 steps to a successful radio transmission. -- @@ -87,15 +84,15 @@ -- @field #string alias Name of the radio transmitter. -- @extends Core.Base#BASE RADIO = { - ClassName = "RADIO", - FileName = "", - Frequency = 0, - Modulation = radio.modulation.AM, - Subtitle = "", + ClassName = "RADIO", + FileName = "", + Frequency = 0, + Modulation = radio.modulation.AM, + Subtitle = "", SubtitleDuration = 0, - Power = 100, - Loop = false, - alias=nil, + Power = 100, + Loop = false, + alias = nil, } --- Create a new RADIO Object. This doesn't broadcast a transmission, though, use @{#RADIO.Broadcast} to actually broadcast. @@ -395,438 +392,3 @@ function RADIO:StopBroadcast() end return self end - - ---- 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. --- 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 --- --- 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. --- --- ## 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. --- --- @type BEACON --- @field #string ClassName Name of the class "BEACON". --- @field Wrapper.Controllable#CONTROLLABLE Positionable The @{#CONTROLLABLE} that will receive radio capabilities. --- @extends Core.Base#BASE -BEACON = { - ClassName = "BEACON", - Positionable = nil, - name=nil, -} - ---- Beacon types supported by DCS. --- @type BEACON.Type --- @field #number NULL --- @field #number VOR --- @field #number DME --- @field #number VOR_DME --- @field #number TACAN TACtical Air Navigation system. --- @field #number VORTAC --- @field #number RSBN --- @field #number BROADCAST_STATION --- @field #number HOMER --- @field #number AIRPORT_HOMER --- @field #number AIRPORT_HOMER_WITH_MARKER --- @field #number ILS_FAR_HOMER --- @field #number ILS_NEAR_HOMER --- @field #number ILS_LOCALIZER --- @field #number ILS_GLIDESLOPE --- @field #number PRMG_LOCALIZER --- @field #number PRMG_GLIDESLOPE --- @field #number ICLS Same as ICLS glideslope. --- @field #number ICLS_LOCALIZER --- @field #number ICLS_GLIDESLOPE --- @field #number NAUTICAL_HOMER -BEACON.Type={ - NULL = 0, - VOR = 1, - DME = 2, - VOR_DME = 3, - TACAN = 4, - VORTAC = 5, - RSBN = 128, - BROADCAST_STATION = 1024, - HOMER = 8, - AIRPORT_HOMER = 4104, - AIRPORT_HOMER_WITH_MARKER = 4136, - ILS_FAR_HOMER = 16408, - ILS_NEAR_HOMER = 16424, - ILS_LOCALIZER = 16640, - ILS_GLIDESLOPE = 16896, - PRMG_LOCALIZER = 33024, - PRMG_GLIDESLOPE = 33280, - ICLS = 131584, --leaving this in here but it is the same as ICLS_GLIDESLOPE - ICLS_LOCALIZER = 131328, - ICLS_GLIDESLOPE = 131584, - NAUTICAL_HOMER = 65536, - -} - ---- Beacon systems supported by DCS. https://wiki.hoggitworld.com/view/DCS_command_activateBeacon --- @type BEACON.System --- @field #number PAR_10 ? --- @field #number RSBN_5 Russian VOR/DME system. --- @field #number TACAN TACtical Air Navigation system on ground. --- @field #number TACAN_TANKER_X TACtical Air Navigation system for tankers on X band. --- @field #number TACAN_TANKER_Y TACtical Air Navigation system for tankers on Y band. --- @field #number VOR Very High Frequency Omni-Directional Range --- @field #number ILS_LOCALIZER ILS localizer --- @field #number ILS_GLIDESLOPE ILS glideslope. --- @field #number PRGM_LOCALIZER PRGM localizer. --- @field #number PRGM_GLIDESLOPE PRGM glideslope. --- @field #number BROADCAST_STATION Broadcast station. --- @field #number VORTAC Radio-based navigational aid for aircraft pilots consisting of a co-located VHF omnidirectional range (VOR) beacon and a tactical air navigation system (TACAN) beacon. --- @field #number TACAN_AA_MODE_X TACtical Air Navigation for aircraft on X band. --- @field #number TACAN_AA_MODE_Y TACtical Air Navigation for aircraft on Y band. --- @field #number VORDME Radio beacon that combines a VHF omnidirectional range (VOR) with a distance measuring equipment (DME). --- @field #number ICLS_LOCALIZER Carrier landing system. --- @field #number ICLS_GLIDESLOPE Carrier landing system. -BEACON.System={ - PAR_10 = 1, - RSBN_5 = 2, - TACAN = 3, - TACAN_TANKER_X = 4, - TACAN_TANKER_Y = 5, - VOR = 6, - ILS_LOCALIZER = 7, - ILS_GLIDESLOPE = 8, - PRMG_LOCALIZER = 9, - PRMG_GLIDESLOPE = 10, - BROADCAST_STATION = 11, - VORTAC = 12, - TACAN_AA_MODE_X = 13, - TACAN_AA_MODE_Y = 14, - VORDME = 15, - ICLS_LOCALIZER = 16, - ICLS_GLIDESLOPE = 17, -} - ---- Create a new BEACON Object. This doesn't activate the beacon, though, use @{#BEACON.ActivateTACAN} etc. --- If you want to create a BEACON, you probably should use @{Wrapper.Positionable#POSITIONABLE.GetBeacon}() instead. --- @param #BEACON self --- @param Wrapper.Positionable#POSITIONABLE Positionable The @{Positionable} that will receive radio capabilities. --- @return #BEACON Beacon object or #nil if the positionable is invalid. -function BEACON:New(Positionable) - - -- Inherit BASE. - local self=BASE:Inherit(self, BASE:New()) --#BEACON - - -- Debug. - self:F(Positionable) - - -- Set positionable. - if Positionable:GetPointVec2() then -- It's stupid, but the only way I found to make sure positionable is valid - self.Positionable = Positionable - self.name=Positionable:GetName() - self:I(string.format("New BEACON %s", tostring(self.name))) - return self - end - - self:E({"The passed positionable is invalid, no BEACON created", Positionable}) - return nil -end - - ---- Activates a TACAN BEACON. --- @param #BEACON self --- @param #number Channel TACAN channel, i.e. the "10" part in "10Y". --- @param #string Mode TACAN mode, i.e. the "Y" part in "10Y". --- @param #string Message The Message that is going to be coded in Morse and broadcasted by the beacon. --- @param #boolean Bearing If true, beacon provides bearing information. If false (or nil), only distance information is available. --- @param #number Duration How long will the beacon last in seconds. Omit for forever. --- @return #BEACON self --- @usage --- -- Let's create a TACAN Beacon for a tanker --- local myUnit = UNIT:FindByName("MyUnit") --- local myBeacon = myUnit:GetBeacon() -- Creates the beacon --- --- myBeacon:ActivateTACAN(20, "Y", "TEXACO", true) -- Activate the beacon -function BEACON:ActivateTACAN(Channel, Mode, Message, Bearing, Duration) - self:T({channel=Channel, mode=Mode, callsign=Message, bearing=Bearing, duration=Duration}) - - -- Get frequency. - local Frequency=UTILS.TACANToFrequency(Channel, Mode) - - -- Check. - if not Frequency then - self:E({"The passed TACAN channel is invalid, the BEACON is not emitting"}) - return self - end - - -- Beacon type. - local Type=BEACON.Type.TACAN - - -- Beacon system. - local System=BEACON.System.TACAN - - -- Check if unit is an aircraft and set system accordingly. - local AA=self.Positionable:IsAir() - if AA then - System=5 --NOTE: 5 is how you cat the correct tanker behaviour! --BEACON.System.TACAN_TANKER - -- Check if "Y" mode is selected for aircraft. - if Mode~="Y" then - self:E({"WARNING: The POSITIONABLE you want to attach the AA Tacan Beacon is an aircraft: Mode should Y !The BEACON is not emitting.", self.Positionable}) - end - end - - -- Attached unit. - local UnitID=self.Positionable:GetID() - - -- Debug. - self:I({string.format("BEACON Activating TACAN %s: Channel=%d%s, Morse=%s, Bearing=%s, Duration=%s!", tostring(self.name), Channel, Mode, Message, tostring(Bearing), tostring(Duration))}) - - -- Start beacon. - self.Positionable:CommandActivateBeacon(Type, System, Frequency, UnitID, Channel, Mode, AA, Message, Bearing) - - -- Stop sheduler. - if Duration then - self.Positionable:DeactivateBeacon(Duration) - end - - return self -end - ---- Activates an ICLS BEACON. The unit the BEACON is attached to should be an aircraft carrier supporting this system. --- @param #BEACON self --- @param #number Channel ICLS channel. --- @param #string Callsign The Message that is going to be coded in Morse and broadcasted by the beacon. --- @param #number Duration How long will the beacon last in seconds. Omit for forever. --- @return #BEACON self -function BEACON:ActivateICLS(Channel, Callsign, Duration) - self:F({Channel=Channel, Callsign=Callsign, Duration=Duration}) - - -- Attached unit. - local UnitID=self.Positionable:GetID() - - -- Debug - self:T2({"ICLS BEACON started!"}) - - -- Start beacon. - self.Positionable:CommandActivateICLS(Channel, UnitID, Callsign) - - -- Stop sheduler - if Duration then -- Schedule the stop of the BEACON if asked by the MD - self.Positionable:DeactivateBeacon(Duration) - end - - return self -end - - - - - - ---- 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 --- @param #boolean Bearing Can the BEACON be homed on ? --- @param #number BeaconDuration How long will the beacon last in seconds. Omit for forever. --- @return #BEACON self --- @usage --- -- Let's create a TACAN Beacon for a tanker --- local myUnit = UNIT:FindByName("MyUnit") --- local myBeacon = myUnit:GetBeacon() -- Creates the beacon --- --- myBeacon:AATACAN(20, "TEXACO", true) -- Activate the beacon -function BEACON:AATACAN(TACANChannel, Message, Bearing, BeaconDuration) - self:F({TACANChannel, Message, Bearing, BeaconDuration}) - - local IsValid = true - - if not self.Positionable:IsAir() then - self:E({"The POSITIONABLE you want to attach the AA Tacan Beacon is not an aircraft ! The BEACON is not emitting", self.Positionable}) - IsValid = false - end - - local Frequency = self:_TACANToFrequency(TACANChannel, "Y") - if not Frequency then - self:E({"The passed TACAN channel is invalid, the BEACON is not emitting"}) - IsValid = false - end - - -- I'm using the beacon type 4 (BEACON_TYPE_TACAN). For System, I'm using 5 (TACAN_TANKER_MODE_Y) if the bearing shows its bearing - -- or 14 (TACAN_AA_MODE_Y) if it does not - local System - if Bearing then - System = 5 - else - System = 14 - end - - if IsValid then -- Starts the BEACON - self:T2({"AA TACAN BEACON started !"}) - self.Positionable:SetCommand({ - id = "ActivateBeacon", - params = { - type = 4, - system = System, - callsign = Message, - frequency = Frequency, - } - }) - - if BeaconDuration then -- Schedule the stop of the BEACON if asked by the MD - SCHEDULER:New(nil, - function() - self:StopAATACAN() - end, {}, BeaconDuration) - end - end - - return self -end - ---- Stops the AA TACAN BEACON --- @param #BEACON self --- @return #BEACON self -function BEACON:StopAATACAN() - self:F() - if not self.Positionable then - self:E({"Start the beacon first before stoping it !"}) - else - self.Positionable:SetCommand({ - id = 'DeactivateBeacon', - params = { - } - }) - end -end - - ---- Activates a general pupose 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 : --- * **Mi8** --- * R-828 -> 20-60MHz --- * ARKUD -> 100-150MHz (canal 1 : 114166, canal 2 : 114333, canal 3 : 114583, canal 4 : 121500, canal 5 : 123100, canal 6 : 124100) AM --- * ARK9 -> 150-1300KHz --- * **Huey** --- * AN/ARC-131 -> 30-76 Mhz FM --- @param #BEACON self --- @param #string FileName The name of the audio file --- @param #number Frequency in MHz --- @param #number Modulation either radio.modulation.AM or radio.modulation.FM --- @param #number Power in W --- @param #number BeaconDuration How long will the beacon last in seconds. Omit for forever. --- @return #BEACON self --- @usage --- -- Let's create a beacon for a unit in distress. --- -- Frequency will be 40MHz FM (home-able by a Huey's AN/ARC-131) --- -- The beacon they use is battery-powered, and only lasts for 5 min --- local UnitInDistress = UNIT:FindByName("Unit1") --- local UnitBeacon = UnitInDistress:GetBeacon() --- --- -- Set the beacon and start it --- UnitBeacon:RadioBeacon("MySoundFileSOS.ogg", 40, radio.modulation.FM, 20, 5*60) -function BEACON:RadioBeacon(FileName, Frequency, Modulation, Power, BeaconDuration) - self:F({FileName, Frequency, Modulation, Power, BeaconDuration}) - local IsValid = false - - -- Check the filename - if type(FileName) == "string" then - if FileName:find(".ogg") or FileName:find(".wav") then - if not FileName:find("l10n/DEFAULT/") then - FileName = "l10n/DEFAULT/" .. FileName - end - IsValid = true - end - end - if not IsValid then - self:E({"File name invalid. Maybe something wrong with the extension ? ", FileName}) - end - - -- Check the Frequency - if type(Frequency) ~= "number" and IsValid then - self:E({"Frequency invalid. ", Frequency}) - IsValid = false - end - Frequency = Frequency * 1000000 -- Conversion to Hz - - -- Check the modulation - if Modulation ~= radio.modulation.AM and Modulation ~= radio.modulation.FM and IsValid then --TODO Maybe make this future proof if ED decides to add an other modulation ? - self:E({"Modulation is invalid. Use DCS's enum radio.modulation.", Modulation}) - IsValid = false - end - - -- Check the Power - if type(Power) ~= "number" and IsValid then - self:E({"Power is invalid. ", Power}) - IsValid = false - end - Power = math.floor(math.abs(Power)) --TODO Find what is the maximum power allowed by DCS and limit power to that - - if IsValid then - self:T2({"Activating Beacon on ", Frequency, Modulation}) - -- Note that this is looped. I have to give this transmission a unique name, I use the class ID - trigger.action.radioTransmission(FileName, self.Positionable:GetPositionVec3(), Modulation, true, Frequency, Power, tostring(self.ID)) - - if BeaconDuration then -- Schedule the stop of the BEACON if asked by the MD - SCHEDULER:New( nil, - function() - self:StopRadioBeacon() - end, {}, BeaconDuration) - end - end -end - ---- Stops the AA TACAN BEACON --- @param #BEACON self --- @return #BEACON self -function BEACON:StopRadioBeacon() - self:F() - -- The unique name of the transmission is the class ID - trigger.action.stopRadioTransmission(tostring(self.ID)) - return self -end - ---- Converts a TACAN Channel/Mode couple into a frequency in Hz --- @param #BEACON self --- @param #number TACANChannel --- @param #string TACANMode --- @return #number Frequecy --- @return #nil if parameters are invalid -function BEACON:_TACANToFrequency(TACANChannel, TACANMode) - self:F3({TACANChannel, TACANMode}) - - if type(TACANChannel) ~= "number" then - if TACANMode ~= "X" and TACANMode ~= "Y" then - return nil -- error in arguments - end - end - --- This code is largely based on ED's code, in DCS World\Scripts\World\Radio\BeaconTypes.lua, line 137. --- I have no idea what it does but it seems to work - local A = 1151 -- 'X', channel >= 64 - local B = 64 -- channel >= 64 - - if TACANChannel < 64 then - B = 1 - end - - if TACANMode == 'Y' then - A = 1025 - if TACANChannel < 64 then - A = 1088 - end - else -- 'X' - if TACANChannel < 64 then - A = 962 - end - end - - return (A + TACANChannel - B) * 1000000 -end - - diff --git a/Moose Development/Moose/Core/RadioQueue.lua b/Moose Development/Moose/Sound/RadioQueue.lua similarity index 84% rename from Moose Development/Moose/Core/RadioQueue.lua rename to Moose Development/Moose/Sound/RadioQueue.lua index a63677a98..80ac49752 100644 --- a/Moose Development/Moose/Core/RadioQueue.lua +++ b/Moose Development/Moose/Sound/RadioQueue.lua @@ -1,20 +1,24 @@ ---- **Core** - Queues Radio Transmissions. +--- **Sound** - Queues Radio Transmissions. -- -- === -- -- ## Features: -- --- * Managed Radio Transmissions. +-- * Manage Radio Transmissions -- -- === -- -- ### Authors: funkyfranky -- --- @module Core.RadioQueue +-- @module Sound.RadioQueue -- @image Core_Radio.JPG --- Manages radio transmissions. -- +-- The main goal of the RADIOQUEUE class is to string together multiple sound files to play a complete sentence. +-- The underlying problem is that radio transmissions in DCS are not queued but played "on top" of each other. +-- Therefore, to achive the goal, it is vital to know the precise duration how long it takes to play the sound file. +-- -- @type RADIOQUEUE -- @field #string ClassName Name of the class "RADIOQUEUE". -- @field #boolean Debugmode Debug mode. More info. @@ -35,6 +39,7 @@ -- @field #table numbers Table of number transmission parameters. -- @field #boolean checking Scheduler is checking the radio queue. -- @field #boolean schedonce Call ScheduleOnce instead of normal scheduler. +-- @field Sound.SRS#MSRS msrs Moose SRS class. -- @extends Core.Base#BASE RADIOQUEUE = { ClassName = "RADIOQUEUE", @@ -69,12 +74,14 @@ RADIOQUEUE = { -- @field #boolean isplaying If true, transmission is currently playing. -- @field #number Tplay Mission time (abs) in seconds when the transmission should be played. -- @field #number interval Interval in seconds before next transmission. +-- @field Sound.SoundOutput#SOUNDFILE soundfile Sound file object to play via SRS. +-- @field Sound.SoundOutput#SOUNDTEXT soundtext Sound TTS object to play via SRS. --- Create a new RADIOQUEUE object for a given radio frequency/modulation. -- @param #RADIOQUEUE self -- @param #number frequency The radio frequency in MHz. --- @param #number modulation (Optional) The radio modulation. Default radio.modulation.AM. +-- @param #number modulation (Optional) The radio modulation. Default `radio.modulation.AM` (=0). -- @param #string alias (Optional) Name of the radio queue. -- @return #RADIOQUEUE self The RADIOQUEUE object. function RADIOQUEUE:New(frequency, modulation, alias) @@ -125,9 +132,9 @@ function RADIOQUEUE:Start(delay, dt) -- Start Scheduler. if self.schedonce then - self:_CheckRadioQueueDelayed(delay) + self:_CheckRadioQueueDelayed(self.delay) else - self.RQid=self.scheduler:Schedule(nil, RADIOQUEUE._CheckRadioQueue, {self}, delay, dt) + self.RQid=self.scheduler:Schedule(nil, RADIOQUEUE._CheckRadioQueue, {self}, self.delay, self.dt) end return self @@ -170,6 +177,17 @@ function RADIOQUEUE:SetRadioPower(power) return self end +--- Set SRS. +-- @param #RADIOQUEUE self +-- @param #string PathToSRS Path to SRS. +-- @param #number Port SRS port. Default 5002. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:SetSRS(PathToSRS, Port) + self.msrs=MSRS:New(PathToSRS, self.frequency/1000000, self.modulation) + self.msrs:SetPort(Port) + return self +end + --- Set parameters of a digit. -- @param #RADIOQUEUE self -- @param #number digit The digit 0-9. @@ -202,7 +220,7 @@ end --- Add a transmission to the radio queue. -- @param #RADIOQUEUE self -- @param #RADIOQUEUE.Transmission transmission The transmission data table. --- @return #RADIOQUEUE self The RADIOQUEUE object. +-- @return #RADIOQUEUE self function RADIOQUEUE:AddTransmission(transmission) self:F({transmission=transmission}) @@ -221,7 +239,7 @@ function RADIOQUEUE:AddTransmission(transmission) return self end ---- Add a transmission to the radio queue. +--- Create a new transmission and add it to the radio queue. -- @param #RADIOQUEUE self -- @param #string filename Name of the sound file. Usually an ogg or wav file type. -- @param #number duration Duration in seconds the file lasts. @@ -230,7 +248,7 @@ end -- @param #number interval Interval in seconds after the last transmission finished. -- @param #string subtitle Subtitle of the transmission. -- @param #number subduration Duration [sec] of the subtitle being displayed. Default 5 sec. --- @return #RADIOQUEUE self The RADIOQUEUE object. +-- @return #RADIOQUEUE.Transmission Radio transmission table. function RADIOQUEUE:NewTransmission(filename, duration, path, tstart, interval, subtitle, subduration) -- Sanity checks. @@ -269,9 +287,36 @@ function RADIOQUEUE:NewTransmission(filename, duration, path, tstart, interval, -- Add transmission to queue. self:AddTransmission(transmission) + return transmission +end + +--- Add a SOUNDFILE to the radio queue. +-- @param #RADIOQUEUE self +-- @param Sound.SoundOutput#SOUNDFILE soundfile Sound file object to be added. +-- @param #number tstart Start time (abs) seconds. Default now. +-- @param #number interval Interval in seconds after the last transmission finished. +-- @return #RADIOQUEUE self +function RADIOQUEUE:AddSoundFile(soundfile, tstart, interval) + --env.info(string.format("FF add soundfile: name=%s%s", soundfile:GetPath(), soundfile:GetFileName())) + local transmission=self:NewTransmission(soundfile:GetFileName(), soundfile.duration, soundfile:GetPath(), tstart, interval, soundfile.subtitle, soundfile.subduration) + transmission.soundfile=soundfile return self end +--- Add a SOUNDTEXT to the radio queue. +-- @param #RADIOQUEUE self +-- @param Sound.SoundOutput#SOUNDTEXT soundtext Text-to-speech text. +-- @param #number tstart Start time (abs) seconds. Default now. +-- @param #number interval Interval in seconds after the last transmission finished. +-- @return #RADIOQUEUE self +function RADIOQUEUE:AddSoundText(soundtext, tstart, interval) + + local transmission=self:NewTransmission("SoundText.ogg", soundtext.duration, nil, tstart, interval, soundtext.subtitle, soundtext.subduration) + transmission.soundtext=soundtext + return self +end + + --- Convert a number (as string) into a radio transmission. -- E.g. for board number or headings. -- @param #RADIOQUEUE self @@ -280,19 +325,9 @@ end -- @param #number interval Interval between the next call. -- @return #number Duration of the call in seconds. function RADIOQUEUE:Number2Transmission(number, delay, interval) - - --- Split string into characters. - local function _split(str) - local chars={} - for i=1,#str do - local c=str:sub(i,i) - table.insert(chars, c) - end - return chars - end -- Split string into characters. - local numbers=_split(number) + local numbers=UTILS.GetCharacters(number) local wait=0 for i=1,#numbers do @@ -325,6 +360,11 @@ end -- @param #RADIOQUEUE.Transmission transmission The transmission. function RADIOQUEUE:Broadcast(transmission) + if ((transmission.soundfile and transmission.soundfile.useSRS) or transmission.soundtext) and self.msrs then + self:_BroadcastSRS(transmission) + return + end + -- Get unit sending the transmission. local sender=self:_GetRadioSender() @@ -416,6 +456,19 @@ function RADIOQUEUE:Broadcast(transmission) end end +--- Broadcast radio message. +-- @param #RADIOQUEUE self +-- @param #RADIOQUEUE.Transmission transmission The transmission. +function RADIOQUEUE:_BroadcastSRS(transmission) + + if transmission.soundfile and transmission.soundfile.useSRS then + self.msrs:PlaySoundFile(transmission.soundfile) + elseif transmission.soundtext then + self.msrs:PlaySoundText(transmission.soundtext) + end + +end + --- Start checking the radio queue. -- @param #RADIOQUEUE self -- @param #number delay Delay in seconds before checking. @@ -547,7 +600,7 @@ function RADIOQUEUE:_GetRadioSender() return nil end ---- Get unit from which we want to transmit a radio message. This has to be an aircraft for subtitles to work. +--- Get unit from which we want to transmit a radio message. This has to be an aircraft or ground unit for subtitles to work. -- @param #RADIOQUEUE self -- @return DCS#Vec3 Vector 3D. function RADIOQUEUE:_GetRadioSenderCoord() diff --git a/Moose Development/Moose/Core/RadioSpeech.lua b/Moose Development/Moose/Sound/RadioSpeech.lua similarity index 90% rename from Moose Development/Moose/Core/RadioSpeech.lua rename to Moose Development/Moose/Sound/RadioSpeech.lua index d4cf22af1..f77c446a2 100644 --- a/Moose Development/Moose/Core/RadioSpeech.lua +++ b/Moose Development/Moose/Sound/RadioSpeech.lua @@ -11,7 +11,7 @@ -- -- ### Authors: FlightControl -- --- @module Core.RadioSpeech +-- @module Sound.RadioSpeech -- @image Core_Radio.JPG --- Makes the radio speak. @@ -162,31 +162,31 @@ RADIOSPEECH.Vocabulary.RU = { ["8000"] = { "8000", 0.92 }, ["9000"] = { "9000", 0.87 }, - ["Ñтепени"] = { "degrees", 0.5 }, - ["километров"] = { "kilometers", 0.65 }, + ["�тõÿõýø"] = { "degrees", 0.5 }, + ["úøûþüõтрþò"] = { "kilometers", 0.65 }, ["km"] = { "kilometers", 0.65 }, - ["миль"] = { "miles", 0.45 }, + ["üøûь"] = { "miles", 0.45 }, ["mi"] = { "miles", 0.45 }, - ["метры"] = { "meters", 0.41 }, + ["üõтры"] = { "meters", 0.41 }, ["m"] = { "meters", 0.41 }, - ["ноги"] = { "feet", 0.37 }, + ["ýþóø"] = { "feet", 0.37 }, ["br"] = { "br", 1.1 }, ["bra"] = { "bra", 0.3 }, - ["возвращаÑÑÑŒ на базу"] = { "returning_to_base", 1.40 }, - ["на пути к наземной цели"] = { "on_route_to_ground_target", 1.45 }, - ["перехват Ñамолетов"] = { "intercepting_bogeys", 1.22 }, - ["поражение наземной цели"] = { "engaging_ground_target", 1.53 }, - ["захватывающие Ñамолеты"] = { "engaging_bogeys", 1.68 }, - ["колеÑа вверх"] = { "wheels_up", 0.92 }, - ["поÑадка на базу"] = { "landing at base", 1.04 }, - ["патрулирующий"] = { "patrolling", 0.96 }, + ["òþ÷òрðщðÑ�Ñ�ÑŒ ýð ñð÷у"] = { "returning_to_base", 1.40 }, + ["ýð ÿутø ú ýð÷õüýþù цõûø"] = { "on_route_to_ground_target", 1.45 }, + ["ÿõрõхòðт �ðüþûõтþò"] = { "intercepting_bogeys", 1.22 }, + ["ÿþрðöõýøõ ýð÷õüýþù цõûø"] = { "engaging_ground_target", 1.53 }, + ["÷ðхòðтыòðющøõ �ðüþûõты"] = { "engaging_bogeys", 1.68 }, + ["úþûõÑ�ð òòõрх"] = { "wheels_up", 0.92 }, + ["ÿþÑ�ðôúð ýð ñð÷у"] = { "landing at base", 1.04 }, + ["ÿðтруûøрующøù"] = { "patrolling", 0.96 }, - ["за"] = { "for", 0.27 }, - ["и"] = { "and", 0.17 }, - ["в"] = { "at", 0.19 }, + ["÷ð"] = { "for", 0.27 }, + ["ø"] = { "and", 0.17 }, + ["ò"] = { "at", 0.19 }, ["dot"] = { "dot", 0.51 }, ["defender"] = { "defender", 0.45 }, } diff --git a/Moose Development/Moose/Sound/SRS.lua b/Moose Development/Moose/Sound/SRS.lua new file mode 100644 index 000000000..9be975a78 --- /dev/null +++ b/Moose Development/Moose/Sound/SRS.lua @@ -0,0 +1,692 @@ +--- **Sound** - Simple Radio Standalone (SRS) Integration. +-- +-- === +-- +-- **Main Features:** +-- +-- * Play sound files via SRS +-- * Play text-to-speach via SRS +-- +-- === +-- +-- ## Youtube Videos: None yet +-- +-- === +-- +-- ## Missions: None yet +-- +-- === +-- +-- ## Sound files: [MOOSE Sound Files](https://github.com/FlightControl-Master/MOOSE_SOUND/releases) +-- +-- === +-- +-- The goal of the [SRS](https://github.com/ciribob/DCS-SimpleRadioStandalone) project is to bring VoIP communication into DCS and to make communication as frictionless as possible. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Sound.MSRS +-- @image Sound_MSRS.png + +--- MSRS class. +-- @type MSRS +-- @field #string ClassName Name of the class. +-- @field #string lid Class id string for output to DCS log file. +-- @field #table frequencies Frequencies used in the transmissions. +-- @field #table modulations Modulations used in the transmissions. +-- @field #number coalition Coalition of the transmission. +-- @field #number port Port. Default 5002. +-- @field #string name Name. Default "DCS-STTS". +-- @field #number volume Volume between 0 (min) and 1 (max). Default 1. +-- @field #string culture Culture. Default "en-GB". +-- @field #string gender Gender. Default "female". +-- @field #string voice Specifc voce. +-- @field Core.Point#COORDINATE coordinate Coordinate from where the transmission is send. +-- @field #string path Path to the SRS exe. This includes the final slash "/". +-- @field #string google Full path google credentials JSON file, e.g. "C:\Users\username\Downloads\service-account-file.json". +-- @extends Core.Base#BASE + +--- *It is a very sad thing that nowadays there is so little useless information.* - Oscar Wilde +-- +-- === +-- +-- ![Banner Image](..\Presentations\ATIS\ATIS_Main.png) +-- +-- # The MSRS Concept +-- +-- This class allows to broadcast sound files or text via Simple Radio Standalone (SRS). +-- +-- ## Prerequisites +-- +-- This script needs SRS version >= 1.9.6. +-- +-- # Play Sound Files +-- +-- local soundfile=SOUNDFILE:New("My Soundfile.ogg", "D:\\Sounds For DCS") +-- local msrs=MSRS:New("C:\\Path To SRS", 251, radio.modulation.AM) +-- msrs:PlaySoundFile(soundfile) +-- +-- # Play Text-To-Speech +-- +-- Basic example: +-- +-- -- Create a SOUNDTEXT object. +-- local text=SOUNDTEXT:New("All Enemies destroyed") +-- +-- -- MOOSE SRS +-- local msrs=MSRS:New("D:\\DCS\\_SRS\\", 305, radio.modulation.AM) +-- +-- -- Text-to speech with default voice after 2 seconds. +-- msrs:PlaySoundText(text, 2) +-- +-- ## Set Gender +-- +-- Use a specific gender with the @{#MSRS.SetGender} function, e.g. `SetGender("male")` or `:SetGender("female")`. +-- +-- ## Set Culture +-- +-- Use a specific "culture" with the @{#MSRS.SetCulture} function, e.g. `:SetCulture("en-US")` or `:SetCulture("de-DE")`. +-- +-- ## Set Voice +-- +-- Use a specifc voice with the @{#MSRS.SetVoice} function, e.g, `:SetVoice("Microsoft Hedda Desktop")`. +-- Note that this must be installed on your windows system. +-- +-- ## Set Coordinate +-- +-- Use @{#MSRS.SetCoordinate} to define the origin from where the transmission is broadcasted. +-- +-- @field #MSRS +MSRS = { + ClassName = "MSRS", + lid = nil, + port = 5002, + name = "MSRS", + frequencies = {}, + modulations = {}, + coalition = 0, + gender = "female", + culture = nil, + voice = nil, + volume = 1, + speed = 1, + coordinate = nil, +} + +--- MSRS class version. +-- @field #string version +MSRS.version="0.0.3" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Add functions to add/remove freqs and modulations. +-- DONE: Add coordinate. +-- DONE: Add google. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new MSRS object. +-- @param #MSRS self +-- @param #string PathToSRS Path to the directory, where SRS is located. +-- @param #number Frequency Radio frequency in MHz. Default 143.00 MHz. Can also be given as a #table of multiple frequencies. +-- @param #number Modulation Radio modulation: 0=AM (default), 1=FM. See `radio.modulation.AM` and `radio.modulation.FM` enumerators. Can also be given as a #table of multiple modulations. +-- @return #MSRS self +function MSRS:New(PathToSRS, Frequency, Modulation) + + -- Defaults. + Frequency =Frequency or 143 + Modulation= Modulation or radio.modulation.AM + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, BASE:New()) -- #MSRS + + self:SetPath(PathToSRS) + self:SetPort() + self:SetFrequencies(Frequency) + self:SetModulations(Modulation) + self:SetGender() + self:SetCoalition() + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set path to SRS install directory. More precisely, path to where the DCS- +-- @param #MSRS self +-- @param #string Path Path to the directory, where the sound file is located. This does **not** contain a final backslash or slash. +-- @return #MSRS self +function MSRS:SetPath(Path) + + if Path==nil then + self:E("ERROR: No path to SRS directory specified!") + return nil + end + + -- Set path. + self.path=Path + + -- Remove (back)slashes. + local n=1 ; local nmax=1000 + while (self.path:sub(-1)=="/" or self.path:sub(-1)==[[\]]) and n<=nmax do + self.path=self.path:sub(1,#self.path-1) + n=n+1 + end + + -- Debug output. + self:T(string.format("SRS path=%s", self:GetPath())) + + return self +end + +--- Get path to SRS directory. +-- @param #MSRS self +-- @return #string Path to the directory. This includes the final slash "/". +function MSRS:GetPath() + return self.path +end + +--- Set port. +-- @param #MSRS self +-- @param #number Port Port. Default 5002. +-- @return #MSRS self +function MSRS:SetPort(Port) + self.port=Port or 5002 +end + +--- Get port. +-- @param #MSRS self +-- @return #number Port. +function MSRS:GetPort() + return self.port +end + +--- Set coalition. +-- @param #MSRS self +-- @param #number Coalition Coalition. Default 0. +-- @return #MSRS self +function MSRS:SetCoalition(Coalition) + self.coalition=Coalition or 0 +end + +--- Get coalition. +-- @param #MSRS self +-- @return #number Coalition. +function MSRS:GetCoalition() + return self.coalition +end + + +--- Set frequencies. +-- @param #MSRS self +-- @param #table Frequencies Frequencies in MHz. Can also be given as a #number if only one frequency should be used. +-- @return #MSRS self +function MSRS:SetFrequencies(Frequencies) + + -- Ensure table. + if type(Frequencies)~="table" then + Frequencies={Frequencies} + end + + self.frequencies=Frequencies + + return self +end + +--- Get frequencies. +-- @param #MSRS self +-- @param #table Frequencies in MHz. +function MSRS:GetFrequencies() + return self.frequencies +end + + +--- Set modulations. +-- @param #MSRS self +-- @param #table Modulations Modulations. Can also be given as a #number if only one modulation should be used. +-- @return #MSRS self +function MSRS:SetModulations(Modulations) + + -- Ensure table. + if type(Modulations)~="table" then + Modulations={Modulations} + end + + self.modulations=Modulations + + return self +end + +--- Get modulations. +-- @param #MSRS self +-- @param #table Modulations. +function MSRS:GetModulations() + return self.modulations +end + +--- Set gender. +-- @param #MSRS self +-- @param #string Gender Gender: "male" or "female" (default). +-- @return #MSRS self +function MSRS:SetGender(Gender) + + Gender=Gender or "female" + + self.gender=Gender:lower() + + -- Debug output. + self:T("Setting gender to "..tostring(self.gender)) + + return self +end + +--- Set culture. +-- @param #MSRS self +-- @param #string Culture Culture, e.g. "en-GB" (default). +-- @return #MSRS self +function MSRS:SetCulture(Culture) + + self.culture=Culture + + return self +end + +--- Set to use a specific voice. Will override gender and culture settings. +-- @param #MSRS self +-- @param #string Voice Voice. +-- @return #MSRS self +function MSRS:SetVoice(Voice) + + self.voice=Voice + + return self +end + +--- Set the coordinate from which the transmissions will be broadcasted. +-- @param #MSRS self +-- @param Core.Point#COORDINATE Coordinate Origin of the transmission. +-- @return #MSRS self +function MSRS:SetCoordinate(Coordinate) + + self.coordinate=Coordinate + + return self +end + +--- Use google text-to-speech. +-- @param #MSRS self +-- @param PathToCredentials Full path to the google credentials JSON file, e.g. "C:\Users\username\Downloads\service-account-file.json". +-- @return #MSRS self +function MSRS:SetGoogle(PathToCredentials) + + self.google=PathToCredentials + + return self +end + +--- Print SRS STTS help to DCS log file. +-- @param #MSRS self +-- @return #MSRS self +function MSRS:Help() + + -- Path and exe. + local path=self:GetPath() or STTS.DIRECTORY + local exe=STTS.EXECUTABLE or "DCS-SR-ExternalAudio.exe" + + -- Text file for output. + local filename = os.getenv('TMP') .. "\\MSRS-help-"..STTS.uuid()..".txt" + + -- Print help. + local command=string.format("%s/%s --help > %s", path, exe, filename) + os.execute(command) + + local f=assert(io.open(filename, "rb")) + local data=f:read("*all") + f:close() + + -- Print to log file. + env.info("SRS STTS help output:") + env.info("======================================================================") + env.info(data) + env.info("======================================================================") + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Transmission Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Play sound file (ogg or mp3) via SRS. +-- @param #MSRS self +-- @param Sound.SoundFile#SOUNDFILE Soundfile Sound file to play. +-- @param #number Delay Delay in seconds, before the sound file is played. +-- @return #MSRS self +function MSRS:PlaySoundFile(Soundfile, Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, MSRS.PlaySoundFile, self, Soundfile, 0) + else + + -- Sound file name. + local soundfile=Soundfile:GetName() + + -- Get command. + local command=self:_GetCommand() + + -- Append file. + command=command.." --file="..tostring(soundfile) + + self:_ExecCommand(command) + + --[[ + + command=command.." > bla.txt" + + -- Debug output. + self:I(string.format("MSRS PlaySoundfile command=%s", command)) + + -- Execute SRS command. + local x=os.execute(command) + + ]] + + end + + return self +end + +--- Play a SOUNDTEXT text-to-speech object. +-- @param #MSRS self +-- @param Sound.SoundFile#SOUNDTEXT SoundText Sound text. +-- @param #number Delay Delay in seconds, before the sound file is played. +-- @return #MSRS self +function MSRS:PlaySoundText(SoundText, Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, MSRS.PlaySoundText, self, SoundText, 0) + else + + -- Get command. + local command=self:_GetCommand(nil, nil, nil, SoundText.gender, SoundText.voice, SoundText.culture, SoundText.volume, SoundText.speed) + + -- Append text. + command=command..string.format(" --text=\"%s\"", tostring(SoundText.text)) + + -- Execute command. + self:_ExecCommand(command) + + --[[ + command=command.." > bla.txt" + + -- Debug putput. + self:I(string.format("MSRS PlaySoundfile command=%s", command)) + + -- Execute SRS command. + local x=os.execute(command) + ]] + + end + + return self +end + +--- Play text message via STTS. +-- @param #MSRS self +-- @param #string Text Text message. +-- @param #number Delay Delay in seconds, before the message is played. +-- @return #MSRS self +function MSRS:PlayText(Text, Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, MSRS.PlayText, self, Text, 0) + else + + -- Get command line. + local command=self:_GetCommand() + + -- Append text. + command=command..string.format(" --text=\"%s\"", tostring(Text)) + + -- Execute command. + self:_ExecCommand(command) + + --[[ + + -- Check that length of command is max 255 chars or os.execute() will not work! + if string.len(command)>255 then + + -- Create a tmp file. + local filename = os.getenv('TMP') .. "\\MSRS-"..STTS.uuid()..".bat" + + local script = io.open(filename, "w+") + script:write(command.." && exit") + script:close() + + -- Play command. + command=string.format("\"%s\"", filename) + + -- Play file in 0.05 seconds + timer.scheduleFunction(os.execute, command, timer.getTime()+0.05) + + -- Remove file in 1 second. + timer.scheduleFunction(os.remove, filename, timer.getTime()+1) + else + + -- Debug output. + self:I(string.format("MSRS Text command=%s", command)) + + -- Execute SRS command. + local x=os.execute(command) + + end + + ]] + end + + return self +end + + +--- Play text file via STTS. +-- @param #MSRS self +-- @param #string TextFile Full path to the file. +-- @param #number Delay Delay in seconds, before the message is played. +-- @return #MSRS self +function MSRS:PlayTextFile(TextFile, Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, MSRS.PlayTextFile, self, TextFile, 0) + else + + -- First check if text file exists! + local exists=UTILS.FileExists(TextFile) + if not exists then + self:E("ERROR: MSRS Text file does not exist! File="..tostring(TextFile)) + return self + end + + -- Get command line. + local command=self:_GetCommand() + + -- Append text file. + command=command..string.format(" --textFile=\"%s\"", tostring(TextFile)) + + -- Debug output. + self:T(string.format("MSRS TextFile command=%s", command)) + + -- Count length of command. + local l=string.len(command) + + -- Execute command. + self:_ExecCommand(command) + + end + + return self +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Execute SRS command to play sound using the `DCS-SR-ExternalAudio.exe`. +-- @param #MSRS self +-- @param #string command Command to executer +-- @return #number Return value of os.execute() command. +function MSRS:_ExecCommand(command) + + -- Create a tmp file. + local filename=os.getenv('TMP').."\\MSRS-"..STTS.uuid()..".bat" + + local script=io.open(filename, "w+") + script:write(command.." && exit") + script:close() + + -- Play command. + command=string.format('start /b "" "%s"', filename) + + local res=nil + if true then + + -- Create a tmp file. + local filenvbs = os.getenv('TMP') .. "\\MSRS-"..STTS.uuid()..".vbs" + + -- VBS script + local script = io.open(filenvbs, "w+") + script:write(string.format('Dim WinScriptHost\n')) + script:write(string.format('Set WinScriptHost = CreateObject("WScript.Shell")\n')) + script:write(string.format('WinScriptHost.Run Chr(34) & "%s" & Chr(34), 0\n', filename)) + script:write(string.format('Set WinScriptHost = Nothing')) + script:close() + + -- Run visual basic script. This still pops up a window but very briefly and does not put the DCS window out of focus. + local runvbs=string.format('cscript.exe //Nologo //B "%s"', filenvbs) + + -- Debug output. + self:T("MSRS execute command="..command) + self:T("MSRS execute VBS command="..runvbs) + + -- Play file in 0.01 seconds + res=os.execute(runvbs) + + -- Remove file in 1 second. + timer.scheduleFunction(os.remove, filename, timer.getTime()+1) + timer.scheduleFunction(os.remove, filenvbs, timer.getTime()+1) + + + else + + -- Debug output. + self:T("MSRS execute command="..command) + + -- Execute command + res=os.execute(command) + + -- Remove file in 1 second. + timer.scheduleFunction(os.remove, filename, timer.getTime()+1) + + end + + + return res +end + +--- Get lat, long and alt from coordinate. +-- @param #MSRS self +-- @param Core.Point#Coordinate Coordinate Coordinate. Can also be a DCS#Vec3. +-- @return #number Latitude. +-- @return #number Longitude. +-- @return #number Altitude. +function MSRS:_GetLatLongAlt(Coordinate) + + local lat, lon, alt=coord.LOtoLL(Coordinate) + + return lat, lon, math.floor(alt) +end + + +--- Get SRS command to play sound using the `DCS-SR-ExternalAudio.exe`. +-- @param #MSRS self +-- @param #table freqs Frequencies in MHz. +-- @param #table modus Modulations. +-- @param #number coal Coalition. +-- @param #string gender Gender. +-- @param #string voice Voice. +-- @param #string culture Culture. +-- @param #number volume Volume. +-- @param #number speed Speed. +-- @param #number port Port. +-- @return #string Command. +function MSRS:_GetCommand(freqs, modus, coal, gender, voice, culture, volume, speed, port) + + local path=self:GetPath() or STTS.DIRECTORY + local exe=STTS.EXECUTABLE or "DCS-SR-ExternalAudio.exe" + freqs=table.concat(freqs or self.frequencies, ",") + modus=table.concat(modus or self.modulations, ",") + coal=coal or self.coalition + gender=gender or self.gender + voice=voice or self.voice + culture=culture or self.culture + volume=volume or self.volume + speed=speed or self.speed + port=port or self.port + + -- Replace modulation + modus=modus:gsub("0", "AM") + modus=modus:gsub("1", "FM") + + -- This did not work well. Stopped if the transmission was a bit longer with no apparent error. + --local command=string.format("%s --freqs=%s --modulations=%s --coalition=%d --port=%d --volume=%.2f --speed=%d", exe, freqs, modus, coal, port, volume, speed) + + -- Command from orig STTS script. Works better for some unknown reason! + local command=string.format("start /min \"\" /d \"%s\" /b \"%s\" -f %s -m %s -c %s -p %s -n \"%s\" -h", path, exe, freqs, modus, coal, port, "ROBOT") + + --local command=string.format('start /b "" /d "%s" "%s" -f %s -m %s -c %s -p %s -n "%s" > bla.txt', path, exe, freqs, modus, coal, port, "ROBOT") + + -- Command. + local command=string.format('%s/%s -f %s -m %s -c %s -p %s -n "%s"', path, exe, freqs, modus, coal, port, "ROBOT") + + -- Set voice or gender/culture. + if voice then + -- Use a specific voice (no need for gender and/or culture. + command=command..string.format(" --voice=\"%s\"", tostring(voice)) + else + -- Add gender. + if gender and gender~="female" then + command=command..string.format(" --gender=%s", tostring(gender)) + end + -- Add culture. + if culture and culture~="en-GB" then + command=command..string.format(" -l %s", tostring(culture)) + end + end + + -- Set coordinate. + if self.coordinate then + local lat,lon,alt=self:_GetLatLongAlt(self.coordinate) + command=command..string.format(" -L %.4f -O %.4f -A %d", lat, lon, alt) + end + + -- Set google. + if self.google then + command=command..string.format(' -G "%s"', self.google) + end + + -- Debug output. + self:T("MSRS command="..command) + + return command +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Sound/SoundOutput.lua b/Moose Development/Moose/Sound/SoundOutput.lua new file mode 100644 index 000000000..01fd00483 --- /dev/null +++ b/Moose Development/Moose/Sound/SoundOutput.lua @@ -0,0 +1,408 @@ +--- **Sound** - Sound output classes. +-- +-- === +-- +-- ## Features: +-- +-- * Create a SOUNDFILE object (mp3 or ogg) to be played via DCS or SRS transmissions +-- * Create a SOUNDTEXT object for text-to-speech output vis SRS Simple-Text-To-Speech (STTS) +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- === +-- +-- There are two classes, SOUNDFILE and SOUNDTEXT, defined in this section that deal with playing +-- sound files or arbitrary text (via SRS Simple-Text-To-Speech), respectively. +-- +-- The SOUNDFILE and SOUNDTEXT objects can be defined and used in other MOOSE classes. +-- +-- +-- @module Sound.SoundOutput +-- @image Sound_SoundOutput.png + +do -- Sound Base + + --- @type SOUNDBASE + -- @field #string ClassName Name of the class. + -- @extends Core.Base#BASE + + + --- Basic sound output inherited by other classes suche as SOUNDFILE and SOUNDTEXT. + -- + -- This class is **not** meant to be used by "ordinary" users. + -- + -- @field #SOUNDBASE + SOUNDBASE={ + ClassName = "SOUNDBASE", + } + + --- Constructor to create a new SOUNDBASE object. + -- @param #SOUNDBASE self + -- @return #SOUNDBASE self + function SOUNDBASE:New() + + -- Inherit BASE. + local self=BASE:Inherit(self, BASE:New()) -- #SOUNDBASE + + + + return self + end + + --- Function returns estimated speech time in seconds. + -- Assumptions for time calc: 100 Words per min, avarage of 5 letters for english word so + -- + -- * 5 chars * 100wpm = 500 characters per min = 8.3 chars per second + -- + -- So lengh of msg / 8.3 = number of seconds needed to read it. rounded down to 8 chars per sec map function: + -- + -- * (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min + -- + -- @param #string Text The text string to analyze. + -- @param #number Speed Speed factor. Default 1. + -- @param #boolean isGoogle If true, google text-to-speech is used. + function SOUNDBASE:GetSpeechTime(length,speed,isGoogle) + + local maxRateRatio = 3 + + speed = speed or 1.0 + isGoogle = isGoogle or false + + local speedFactor = 1.0 + if isGoogle then + speedFactor = speed + else + if speed ~= 0 then + speedFactor = math.abs(speed) * (maxRateRatio - 1) / 10 + 1 + end + if speed < 0 then + speedFactor = 1/speedFactor + end + end + + -- Words per minute. + local wpm = math.ceil(100 * speedFactor) + + -- Characters per second. + local cps = math.floor((wpm * 5)/60) + + if type(length) == "string" then + length = string.len(length) + end + + return math.ceil(length/cps) + end + +end + + +do -- Sound File + + --- @type SOUNDFILE + -- @field #string ClassName Name of the class + -- @field #string filename Name of the flag. + -- @field #string path Directory path, where the sound file is located. This includes the final slash "/". + -- @field #string duration Duration of the sound file in seconds. + -- @field #string subtitle Subtitle of the transmission. + -- @field #number subduration Duration in seconds how long the subtitle is displayed. + -- @field #boolean useSRS If true, sound file is played via SRS. Sound file needs to be on local disk not inside the miz file! + -- @extends Core.Base#BASE + + + --- Sound files used by other classes. + -- + -- # The SOUNDFILE Concept + -- + -- A SOUNDFILE object hold the important properties that are necessary to play the sound file, e.g. its file name, path, duration. + -- + -- It can be created with the @{#SOUNDFILE.New}(*FileName*, *Path*, *Duration*) function: + -- + -- local soundfile=SOUNDFILE:New("My Soundfile.ogg", "Sound File/", 3.5) + -- + -- ## SRS + -- + -- If sound files are supposed to be played via SRS, you need to use the @{#SOUNDFILE.SetPlayWithSRS}() function. + -- + -- # Location/Path + -- + -- ## DCS + -- + -- DCS can only play sound files that are located inside the mission (.miz) file. In particular, DCS cannot make use of files that are stored on + -- your hard drive. + -- + -- The default location where sound files are stored in DCS is the directory "l10n/DEFAULT/". This is where sound files are placed, if they are + -- added via the mission editor (TRIGGERS-->ACTIONS-->SOUND TO ALL). Note however, that sound files which are not added with a trigger command, + -- will be deleted each time the mission is saved! Therefore, this directory is not ideal to be used especially if many sound files are to + -- be included since for each file a trigger action needs to be created. Which is cumbersome, to say the least. + -- + -- The recommended way is to create a new folder inside the mission (.miz) file (a miz file is essentially zip file and can be opened, e.g., with 7-Zip) + -- and to place the sound files in there. Sound files in these folders are not wiped out by DCS on the next save. + -- + -- ## SRS + -- + -- SRS sound files need to be located on your local drive (not inside the miz). Therefore, you need to specify the full path. + -- + -- @field #SOUNDFILE + SOUNDFILE={ + ClassName = "SOUNDFILE", + filename = nil, + path = "l10n/DEFAULT/", + duration = 3, + subtitle = nil, + subduration = 0, + useSRS = false, + } + + --- Constructor to create a new SOUNDFILE object. + -- @param #SOUNDFILE self + -- @param #string FileName The name of the sound file, e.g. "Hello World.ogg". + -- @param #string Path The path of the directory, where the sound file is located. Default is "l10n/DEFAULT/" within the miz file. + -- @param #number Duration Duration in seconds, how long it takes to play the sound file. Default is 3 seconds. + -- @return #SOUNDFILE self + function SOUNDFILE:New(FileName, Path, Duration) + + -- Inherit BASE. + local self=BASE:Inherit(self, BASE:New()) -- #SOUNDFILE + + -- Set file name. + self:SetFileName(FileName) + + -- Set path. + self:SetPath(Path) + + -- Set duration. + self:SetDuration(Duration) + + -- Debug info: + self:T(string.format("New SOUNDFILE: file name=%s, path=%s", self.filename, self.path)) + + return self + end + + --- Set path, where the sound file is located. + -- @param #SOUNDFILE self + -- @param #string Path Path to the directory, where the sound file is located. + -- @return #SOUNDFILE self + function SOUNDFILE:SetPath(Path) + + -- Init path. + self.path=Path or "l10n/DEFAULT/" + + -- Remove (back)slashes. + local nmax=1000 ; local n=1 + while (self.path:sub(-1)=="/" or self.path:sub(-1)==[[\]]) and n<=nmax do + self.path=self.path:sub(1,#self.path-1) + n=n+1 + end + + -- Append slash. + self.path=self.path.."/" + + return self + end + + --- Get path of the directory, where the sound file is located. + -- @param #SOUNDFILE self + -- @return #string Path. + function SOUNDFILE:GetPath() + local path=self.path or "l10n/DEFAULT/" + return path + end + + --- Set sound file name. This must be a .ogg or .mp3 file! + -- @param #SOUNDFILE self + -- @param #string FileName Name of the file. Default is "Hello World.mp3". + -- @return #SOUNDFILE self + function SOUNDFILE:SetFileName(FileName) + --TODO: check that sound file is really .ogg or .mp3 + self.filename=FileName or "Hello World.mp3" + return self + end + + --- Get the sound file name. + -- @param #SOUNDFILE self + -- @return #string Name of the soud file. This does *not* include its path. + function SOUNDFILE:GetFileName() + return self.filename + end + + + --- Set duration how long it takes to play the sound file. + -- @param #SOUNDFILE self + -- @param #string Duration Duration in seconds. Default 3 seconds. + -- @return #SOUNDFILE self + function SOUNDFILE:SetDuration(Duration) + self.duration=Duration or 3 + return self + end + + --- Get duration how long the sound file takes to play. + -- @param #SOUNDFILE self + -- @return #number Duration in seconds. + function SOUNDFILE:GetDuration() + return self.duration or 3 + end + + --- Get the complete sound file name inlcuding its path. + -- @param #SOUNDFILE self + -- @return #string Name of the sound file. + function SOUNDFILE:GetName() + local path=self:GetPath() + local filename=self:GetFileName() + local name=string.format("%s%s", path, filename) + return name + end + + --- Set whether sound files should be played via SRS. + -- @param #SOUNDFILE self + -- @param #boolean Switch If true or nil, use SRS. If false, use DCS transmission. + -- @return #SOUNDFILE self + function SOUNDFILE:SetPlayWithSRS(Switch) + if Switch==true or Switch==nil then + self.useSRS=true + else + self.useSRS=false + end + return self + end + +end + +do -- Text-To-Speech + + --- @type SOUNDTEXT + -- @field #string ClassName Name of the class + -- @field #string text Text to speak. + -- @field #number duration Duration in seconds. + -- @field #string gender Gender: "male", "female". + -- @field #string culture Culture, e.g. "en-GB". + -- @field #string voice Specific voice to use. Overrules `gender` and `culture` settings. + -- @extends Core.Base#BASE + + + --- Text-to-speech objects for other classes. + -- + -- # The SOUNDTEXT Concept + -- + -- A SOUNDTEXT object holds all necessary information to play a general text via SRS Simple-Text-To-Speech. + -- + -- It can be created with the @{#SOUNDTEXT.New}(*Text*, *Duration*) function. + -- + -- * @{#SOUNDTEXT.New}(*Text, Duration*): Creates a new SOUNDTEXT object. + -- + -- # Options + -- + -- ## Gender + -- + -- You can choose a gender ("male" or "femal") with the @{#SOUNDTEXT.SetGender}(*Gender*) function. + -- Note that the gender voice needs to be installed on your windows machine for the used culture (see below). + -- + -- ## Culture + -- + -- You can choose a "culture" (accent) with the @{#SOUNDTEXT.SetCulture}(*Culture*) function, where the default (SRS) culture is "en-GB". + -- + -- Other examples for culture are: "en-US" (US accent), "de-DE" (German), "it-IT" (Italian), "ru-RU" (Russian), "zh-CN" (Chinese). + -- + -- Note that the chosen culture needs to be installed on your windows machine. + -- + -- ## Specific Voice + -- + -- You can use a specific voice for the transmission with the @{SOUNDTEXT.SetVoice}(*VoiceName*) function. Here are some examples + -- + -- * Name: Microsoft Hazel Desktop, Culture: en-GB, Gender: Female, Age: Adult, Desc: Microsoft Hazel Desktop - English (Great Britain) + -- * Name: Microsoft David Desktop, Culture: en-US, Gender: Male, Age: Adult, Desc: Microsoft David Desktop - English (United States) + -- * Name: Microsoft Zira Desktop, Culture: en-US, Gender: Female, Age: Adult, Desc: Microsoft Zira Desktop - English (United States) + -- * Name: Microsoft Hedda Desktop, Culture: de-DE, Gender: Female, Age: Adult, Desc: Microsoft Hedda Desktop - German + -- * Name: Microsoft Helena Desktop, Culture: es-ES, Gender: Female, Age: Adult, Desc: Microsoft Helena Desktop - Spanish (Spain) + -- * Name: Microsoft Hortense Desktop, Culture: fr-FR, Gender: Female, Age: Adult, Desc: Microsoft Hortense Desktop - French + -- * Name: Microsoft Elsa Desktop, Culture: it-IT, Gender: Female, Age: Adult, Desc: Microsoft Elsa Desktop - Italian (Italy) + -- * Name: Microsoft Irina Desktop, Culture: ru-RU, Gender: Female, Age: Adult, Desc: Microsoft Irina Desktop - Russian + -- * Name: Microsoft Huihui Desktop, Culture: zh-CN, Gender: Female, Age: Adult, Desc: Microsoft Huihui Desktop - Chinese (Simplified) + -- + -- Note that this must be installed on your windos machine. Also note that this overrides any culture and gender settings. + -- + -- @field #SOUNDTEXT + SOUNDTEXT={ + ClassName = "SOUNDTEXT", + } + + --- Constructor to create a new SOUNDTEXT object. + -- @param #SOUNDTEXT self + -- @param #string Text The text to speak. + -- @param #number Duration Duration in seconds, how long it takes to play the text. Default is 3 seconds. + -- @return #SOUNDTEXT self + function SOUNDTEXT:New(Text, Duration) + + -- Inherit BASE. + local self=BASE:Inherit(self, BASE:New()) -- #SOUNDTEXT + + self:SetText(Text) + self:SetDuration(Duration or STTS.getSpeechTime(Text)) + --self:SetGender() + --self:SetCulture() + + -- Debug info: + self:T(string.format("New SOUNDTEXT: text=%s, duration=%.1f sec", self.text, self.duration)) + + return self + end + + --- Set text. + -- @param #SOUNDTEXT self + -- @param #string Text Text to speak. Default "Hello World!". + -- @return #SOUNDTEXT self + function SOUNDTEXT:SetText(Text) + + self.text=Text or "Hello World!" + + return self + end + + --- Set duration, how long it takes to speak the text. + -- @param #SOUNDTEXT self + -- @param #number Duration Duration in seconds. Default 3 seconds. + -- @return #SOUNDTEXT self + function SOUNDTEXT:SetDuration(Duration) + + self.duration=Duration or 3 + + return self + end + + --- Set gender. + -- @param #SOUNDTEXT self + -- @param #string Gender Gender: "male" or "female" (default). + -- @return #SOUNDTEXT self + function SOUNDTEXT:SetGender(Gender) + + self.gender=Gender or "female" + + return self + end + + --- Set TTS culture - local for the voice. + -- @param #SOUNDTEXT self + -- @param #string Culture TTS culture. Default "en-GB". + -- @return #SOUNDTEXT self + function SOUNDTEXT:SetCulture(Culture) + + self.culture=Culture or "en-GB" + + return self + end + + --- Set to use a specific voice name. + -- See the list from `DCS-SR-ExternalAudio.exe --help` or if using google see [google voices](https://cloud.google.com/text-to-speech/docs/voices). + -- @param #SOUNDTEXT self + -- @param #string VoiceName Voice name. Note that this will overrule `Gender` and `Culture`. + -- @return #SOUNDTEXT self + function SOUNDTEXT:SetVoice(VoiceName) + + self.voice=VoiceName + + return self + end + +end \ No newline at end of file diff --git a/Moose Development/Moose/Core/UserSound.lua b/Moose Development/Moose/Sound/UserSound.lua similarity index 98% rename from Moose Development/Moose/Core/UserSound.lua rename to Moose Development/Moose/Sound/UserSound.lua index b0f6fb393..8b94ad114 100644 --- a/Moose Development/Moose/Core/UserSound.lua +++ b/Moose Development/Moose/Sound/UserSound.lua @@ -1,4 +1,4 @@ ---- **Core** - Manage user sound. +--- **Sound** - Manage user sound. -- -- === -- @@ -16,7 +16,7 @@ -- -- === -- --- @module Core.UserSound +-- @module Sound.UserSound -- @image Core_Usersound.JPG do -- UserSound diff --git a/Moose Development/Moose/Tasking/CommandCenter.lua b/Moose Development/Moose/Tasking/CommandCenter.lua index 0673facb1..2bcfe972c 100644 --- a/Moose Development/Moose/Tasking/CommandCenter.lua +++ b/Moose Development/Moose/Tasking/CommandCenter.lua @@ -202,6 +202,7 @@ function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) self:SetAutoAcceptTasks( true ) self:SetAutoAssignMethod( COMMANDCENTER.AutoAssignMethods.Distance ) self:SetFlashStatus( false ) + self:SetMessageDuration(10) self:HandleEvent( EVENTS.Birth, --- @param #COMMANDCENTER self @@ -682,7 +683,7 @@ end -- @param #string Message The message text. function COMMANDCENTER:MessageToAll( Message ) - self:GetPositionable():MessageToAll( Message, 20, self:GetName() ) + self:GetPositionable():MessageToAll( Message, self.MessageDuration, self:GetName() ) end @@ -692,7 +693,7 @@ end -- @param Wrapper.Group#GROUP MessageGroup The group to receive the message. function COMMANDCENTER:MessageToGroup( Message, MessageGroup ) - self:GetPositionable():MessageToGroup( Message, 15, MessageGroup, self:GetShortText() ) + self:GetPositionable():MessageToGroup( Message, self.MessageDuration, MessageGroup, self:GetShortText() ) end @@ -715,7 +716,7 @@ function COMMANDCENTER:MessageToCoalition( Message ) local CCCoalition = self:GetPositionable():GetCoalition() --TODO: Fix coalition bug! - self:GetPositionable():MessageToCoalition( Message, 15, CCCoalition, self:GetShortText() ) + self:GetPositionable():MessageToCoalition( Message, self.MessageDuration, CCCoalition, self:GetShortText() ) end @@ -795,9 +796,18 @@ end --- Let the command center flash a report of the status of the subscribed task to a group. -- @param #COMMANDCENTER self +-- @param Flash #boolean function COMMANDCENTER:SetFlashStatus( Flash ) self:F() - self.FlashStatus = Flash or true - + self.FlashStatus = Flash and true +end + +--- Duration a command center message is shown. +-- @param #COMMANDCENTER self +-- @param seconds #number +function COMMANDCENTER:SetMessageDuration(seconds) + self:F() + + self.MessageDuration = 10 or seconds end diff --git a/Moose Development/Moose/Utilities/STTS.lua b/Moose Development/Moose/Utilities/STTS.lua new file mode 100644 index 000000000..97836a1ee --- /dev/null +++ b/Moose Development/Moose/Utilities/STTS.lua @@ -0,0 +1,256 @@ +--- **Utilities** DCS Simple Text-To-Speech (STTS). +-- +-- +-- +-- @module Utils.STTS +-- @image MOOSE.JPG + +--- [DCS Enum world](https://wiki.hoggitworld.com/view/DCS_enum_world) +-- @type STTS +-- @field #string DIRECTORY Path of the SRS directory. + +--- Simple Text-To-Speech +-- +-- Version 0.4 - Compatible with SRS version 1.9.6.0+ +-- +-- # DCS Modification Required +-- +-- You will need to edit MissionScripting.lua in DCS World/Scripts/MissionScripting.lua and remove the sanitisation. +-- To do this remove all the code below the comment - the line starts "local function sanitizeModule(name)" +-- Do this without DCS running to allow mission scripts to use os functions. +-- +-- *You WILL HAVE TO REAPPLY AFTER EVERY DCS UPDATE* +-- +-- # USAGE: +-- +-- Add this script into the mission as a DO SCRIPT or DO SCRIPT FROM FILE to initialise it +-- Make sure to edit the STTS.SRS_PORT and STTS.DIRECTORY to the correct values before adding to the mission. +-- Then its as simple as calling the correct function in LUA as a DO SCRIPT or in your own scripts. +-- +-- Example calls: +-- +-- STTS.TextToSpeech("Hello DCS WORLD","251","AM","1.0","SRS",2) +-- +-- Arguments in order are: +-- +-- * Message to say, make sure not to use a newline (\n) ! +-- * Frequency in MHz +-- * Modulation - AM/FM +-- * Volume - 1.0 max, 0.5 half +-- * Name of the transmitter - ATC, RockFM etc +-- * Coalition - 0 spectator, 1 red 2 blue +-- * OPTIONAL - Vec3 Point i.e Unit.getByName("A UNIT"):getPoint() - needs Vec3 for Height! OR null if not needed +-- * OPTIONAL - Speed -10 to +10 +-- * OPTIONAL - Gender male, female or neuter +-- * OPTIONAL - Culture - en-US, en-GB etc +-- * OPTIONAL - Voice - a specfic voice by name. Run DCS-SR-ExternalAudio.exe with --help to get the ones you can use on the command line +-- * OPTIONAL - Google TTS - Switch to Google Text To Speech - Requires STTS.GOOGLE_CREDENTIALS path and Google project setup correctly +-- +-- +-- ## Example +-- +-- This example will say the words "Hello DCS WORLD" on 251 MHz AM at maximum volume with a client called SRS and to the Blue coalition only +-- +-- STTS.TextToSpeech("Hello DCS WORLD","251","AM","1.0","SRS",2,null,-5,"male","en-GB") +-- +-- ## Example +-- +--This example will say the words "Hello DCS WORLD" on 251 MHz AM at maximum volume with a client called SRS and to the Blue coalition only centered on the position of the Unit called "A UNIT" +-- +-- STTS.TextToSpeech("Hello DCS WORLD","251","AM","1.0","SRS",2,Unit.getByName("A UNIT"):getPoint(),-5,"male","en-GB") +-- +-- Arguments in order are: +-- +-- * FULL path to the MP3 OR OGG to play +-- * Frequency in MHz - to use multiple separate with a comma - Number of frequencies MUST match number of Modulations +-- * Modulation - AM/FM - to use multiple +-- * Volume - 1.0 max, 0.5 half +-- * Name of the transmitter - ATC, RockFM etc +-- * Coalition - 0 spectator, 1 red 2 blue +-- +-- ## Example +-- +-- This will play that MP3 on 255MHz AM & 31 FM at half volume with a client called "Multiple" and to Spectators only +-- +-- STTS.PlayMP3("C:\\Users\\Ciaran\\Downloads\\PR-Music.mp3","255,31","AM,FM","0.5","Multiple",0) +-- +-- @field #STTS +STTS={ + ClassName="STTS", + DIRECTORY="", + SRS_PORT=5002, + GOOGLE_CREDENTIALS="C:\\Users\\Ciaran\\Downloads\\googletts.json", + EXECUTABLE="DCS-SR-ExternalAudio.exe", +} + +--- FULL Path to the FOLDER containing DCS-SR-ExternalAudio.exe - EDIT TO CORRECT FOLDER +STTS.DIRECTORY = "D:/DCS/_SRS" + +--- LOCAL SRS PORT - DEFAULT IS 5002 +STTS.SRS_PORT = 5002 + +--- Google credentials file +STTS.GOOGLE_CREDENTIALS = "C:\\Users\\Ciaran\\Downloads\\googletts.json" + +--- DONT CHANGE THIS UNLESS YOU KNOW WHAT YOU'RE DOING +STTS.EXECUTABLE = "DCS-SR-ExternalAudio.exe" + + +--- Function for UUID. +function STTS.uuid() + local random = math.random + local template ='yxxx-xxxxxxxxxxxx' + return string.gsub(template, '[xy]', function (c) + local v = (c == 'x') and random(0, 0xf) or random(8, 0xb) + return string.format('%x', v) + end) +end + +--- Round a number. +-- @param #number x Number. +-- @param #number n Precision. +function STTS.round(x, n) + n = math.pow(10, n or 0) + x = x * n + if x >= 0 then x = math.floor(x + 0.5) else x = math.ceil(x - 0.5) end + return x / n +end + +--- Function returns estimated speech time in seconds. +-- Assumptions for time calc: 100 Words per min, avarage of 5 letters for english word so +-- +-- * 5 chars * 100wpm = 500 characters per min = 8.3 chars per second +-- +-- So lengh of msg / 8.3 = number of seconds needed to read it. rounded down to 8 chars per sec map function: +-- +-- * (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min +-- +function STTS.getSpeechTime(length,speed,isGoogle) + + local maxRateRatio = 3 + + speed = speed or 1.0 + isGoogle = isGoogle or false + + local speedFactor = 1.0 + if isGoogle then + speedFactor = speed + else + if speed ~= 0 then + speedFactor = math.abs(speed) * (maxRateRatio - 1) / 10 + 1 + end + if speed < 0 then + speedFactor = 1/speedFactor + end + end + + local wpm = math.ceil(100 * speedFactor) + local cps = math.floor((wpm * 5)/60) + + if type(length) == "string" then + length = string.len(length) + end + + return math.ceil(length/cps) +end + +--- Text to speech function. +function STTS.TextToSpeech(message, freqs, modulations, volume, name, coalition, point, speed, gender, culture, voice, googleTTS) + if os == nil or io == nil then + env.info("[DCS-STTS] LUA modules os or io are sanitized. skipping. ") + return + end + + speed = speed or 1 + gender = gender or "female" + culture = culture or "" + voice = voice or "" + coalition=coalition or "0" + name=name or "ROBOT" + volume=1 + speed=1 + + + message = message:gsub("\"","\\\"") + + local cmd = string.format("start /min \"\" /d \"%s\" /b \"%s\" -f %s -m %s -c %s -p %s -n \"%s\" -h", STTS.DIRECTORY, STTS.EXECUTABLE, freqs or "305", modulations or "AM", coalition, STTS.SRS_PORT, name) + + if voice ~= "" then + cmd = cmd .. string.format(" -V \"%s\"",voice) + else + + if culture ~= "" then + cmd = cmd .. string.format(" -l %s",culture) + end + + if gender ~= "" then + cmd = cmd .. string.format(" -g %s",gender) + end + end + + if googleTTS == true then + cmd = cmd .. string.format(" -G \"%s\"",STTS.GOOGLE_CREDENTIALS) + end + + if speed ~= 1 then + cmd = cmd .. string.format(" -s %s",speed) + end + + if volume ~= 1.0 then + cmd = cmd .. string.format(" -v %s",volume) + end + + if point and type(point) == "table" and point.x then + local lat, lon, alt = coord.LOtoLL(point) + + lat = STTS.round(lat,4) + lon = STTS.round(lon,4) + alt = math.floor(alt) + + cmd = cmd .. string.format(" -L %s -O %s -A %s",lat,lon,alt) + end + + cmd = cmd ..string.format(" -t \"%s\"",message) + + if string.len(cmd) > 255 then + local filename = os.getenv('TMP') .. "\\DCS_STTS-" .. STTS.uuid() .. ".bat" + local script = io.open(filename,"w+") + script:write(cmd .. " && exit" ) + script:close() + cmd = string.format("\"%s\"",filename) + timer.scheduleFunction(os.remove, filename, timer.getTime() + 1) + end + + if string.len(cmd) > 255 then + env.info("[DCS-STTS] - cmd string too long") + env.info("[DCS-STTS] TextToSpeech Command :\n" .. cmd.."\n") + end + os.execute(cmd) + + return STTS.getSpeechTime(message,speed,googleTTS) +end + +--- Play mp3 function. +-- @param #string pathToMP3 Path to the sound file. +-- @param #string freqs Frequencies, e.g. "305, 256". +-- @param #string modulations Modulations, e.g. "AM, FM". +-- @param #string volume Volume, e.g. "0.5". +function STTS.PlayMP3(pathToMP3, freqs, modulations, volume, name, coalition, point) + + local cmd = string.format("start \"\" /d \"%s\" /b /min \"%s\" -i \"%s\" -f %s -m %s -c %s -p %s -n \"%s\" -v %s -h", + STTS.DIRECTORY, STTS.EXECUTABLE, pathToMP3, freqs or "305", modulations or "AM", coalition or "0", STTS.SRS_PORT, name or "ROBOT", volume or "1") + + if point and type(point) == "table" and point.x then + local lat, lon, alt = coord.LOtoLL(point) + + lat = STTS.round(lat,4) + lon = STTS.round(lon,4) + alt = math.floor(alt) + + cmd = cmd .. string.format(" -L %s -O %s -A %s",lat,lon,alt) + end + + env.info("[DCS-STTS] MP3/OGG Command :\n" .. cmd.."\n") + os.execute(cmd) + +end \ No newline at end of file diff --git a/Moose Development/Moose/Utilities/Templates.lua b/Moose Development/Moose/Utilities/Templates.lua new file mode 100644 index 000000000..182925689 --- /dev/null +++ b/Moose Development/Moose/Utilities/Templates.lua @@ -0,0 +1,612 @@ +--- **Utils** Templates +-- +-- DCS unit templates +-- +-- @module Utilities.Templates +-- @image MOOSE.JPG + +--- TEMPLATE class. +-- @type TEMPLATE +-- @field #string ClassName Name of the class. + +--- *Templates* +-- +-- === +-- +-- ![Banner Image](..\Presentations\Utilities\PROFILER_Main.jpg) +-- +-- Get DCS templates from thin air. +-- +-- # Ground Units +-- +-- Ground units. +-- +-- # Naval Units +-- +-- Ships are not implemented yet. +-- +-- # Aircraft +-- +-- ## Airplanes +-- +-- Airplanes are not implemented yet. +-- +-- ## Helicopters +-- +-- Helicopters are not implemented yet. +-- +-- @field #TEMPLATE +TEMPLATE = { + ClassName = "TEMPLATE", + Ground = {}, + Naval = {}, + Airplane = {}, + Helicopter = {}, +} + +--- Ground unit type names. +-- @type TEMPLATE.TypeGround +-- @param #string InfantryAK +TEMPLATE.TypeGround={ + InfantryAK="Infantry AK", + ParatrooperAKS74="Paratrooper AKS-74", + ParatrooperRPG16="Paratrooper RPG-16", + SoldierWWIIUS="soldier_wwii_us", + InfantryM248="Infantry M249", + SoldierM4="Soldier M4", +} + +--- Naval unit type names. +-- @type TEMPLATE.TypeNaval +-- @param #string Ticonderoga +TEMPLATE.TypeNaval={ + Ticonderoga="TICONDEROG", +} + +--- Rotary wing unit type names. +-- @type TEMPLATE.TypeAirplane +-- @param #string A10C +TEMPLATE.TypeAirplane={ + A10C="A-10C", +} + +--- Rotary wing unit type names. +-- @type TEMPLATE.TypeHelicopter +-- @param #string AH1W +TEMPLATE.TypeHelicopter={ + AH1W="AH-1W", +} + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Ground Template +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get template for ground units. +-- @param #string TypeName Type name of the unit(s) in the groups. See `TEMPLATE.Ground`. +-- @param #string GroupName Name of the spawned group. **Must be unique!** +-- @param #number CountryID Country ID. Default `country.id.USA`. Coalition is automatically determined by the one the country belongs to. +-- @param DCS#Vec3 Vec3 Position of the group and the first unit. +-- @param #number Nunits Number of units. Default 1. +-- @param #number Radius Spawn radius for additonal units in meters. Default 50 m. +-- @return #table Template Template table. +function TEMPLATE.GetGround(TypeName, GroupName, CountryID, Vec3, Nunits, Radius) + + -- Defaults. + TypeName=TypeName or TEMPLATE.TypeGround.SoldierM4 + GroupName=GroupName or "Ground-1" + CountryID=CountryID or country.id.USA + Vec3=Vec3 or {x=0, y=0, z=0} + Nunits=Nunits or 1 + Radius=Radius or 50 + + + -- Get generic template. + local template=UTILS.DeepCopy(TEMPLATE.GenericGround) + + -- Set group name. + template.name=GroupName + + -- These are additional entries required by the MOOSE _DATABASE:Spawn() function. + template.CountryID=CountryID + template.CoalitionID=coalition.getCountryCoalition(template.CountryID) + template.CategoryID=Unit.Category.GROUND_UNIT + + -- Set first unit. + template.units[1].type=TypeName + template.units[1].name=GroupName.."-1" + + if Vec3 then + TEMPLATE.SetPositionFromVec3(template, Vec3) + end + + TEMPLATE.SetUnits(template, Nunits, COORDINATE:NewFromVec3(Vec3), Radius) + + return template +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Naval Template +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get template for ground units. +-- @param #string TypeName Type name of the unit(s) in the groups. See `TEMPLATE.Ground`. +-- @param #string GroupName Name of the spawned group. **Must be unique!** +-- @param #number CountryID Country ID. Default `country.id.USA`. Coalition is automatically determined by the one the country belongs to. +-- @param DCS#Vec3 Vec3 Position of the group and the first unit. +-- @param #number Nunits Number of units. Default 1. +-- @param #number Radius Spawn radius for additonal units in meters. Default 500 m. +-- @return #table Template Template table. +function TEMPLATE.GetNaval(TypeName, GroupName, CountryID, Vec3, Nunits, Radius) + + -- Defaults. + TypeName=TypeName or TEMPLATE.TypeNaval.Ticonderoga + GroupName=GroupName or "Naval-1" + CountryID=CountryID or country.id.USA + Vec3=Vec3 or {x=0, y=0, z=0} + Nunits=Nunits or 1 + Radius=Radius or 500 + + + -- Get generic template. + local template=UTILS.DeepCopy(TEMPLATE.GenericNaval) + + -- Set group name. + template.name=GroupName + + -- These are additional entries required by the MOOSE _DATABASE:Spawn() function. + template.CountryID=CountryID + template.CoalitionID=coalition.getCountryCoalition(template.CountryID) + template.CategoryID=Unit.Category.SHIP + + -- Set first unit. + template.units[1].type=TypeName + template.units[1].name=GroupName.."-1" + + if Vec3 then + TEMPLATE.SetPositionFromVec3(template, Vec3) + end + + TEMPLATE.SetUnits(template, Nunits, COORDINATE:NewFromVec3(Vec3), Radius) + + return template +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Aircraft Template +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get template for fixed wing units. +-- @param #string TypeName Type name of the unit(s) in the groups. See `TEMPLATE.Ground`. +-- @param #string GroupName Name of the spawned group. **Must be unique!** +-- @param #number CountryID Country ID. Default `country.id.USA`. Coalition is automatically determined by the one the country belongs to. +-- @param DCS#Vec3 Vec3 Position of the group and the first unit. +-- @param #number Nunits Number of units. Default 1. +-- @param #number Radius Spawn radius for additonal units in meters. Default 500 m. +-- @return #table Template Template table. +function TEMPLATE.GetAirplane(TypeName, GroupName, CountryID, Vec3, Nunits, Radius) + + -- Defaults. + TypeName=TypeName or TEMPLATE.TypeAirplane.A10C + GroupName=GroupName or "Airplane-1" + CountryID=CountryID or country.id.USA + Vec3=Vec3 or {x=0, y=1000, z=0} + Nunits=Nunits or 1 + Radius=Radius or 100 + + local template=TEMPLATE._GetAircraft(true, TypeName, GroupName, CountryID, Vec3, Nunits, Radius) + + return template +end + +--- Get template for fixed wing units. +-- @param #string TypeName Type name of the unit(s) in the groups. See `TEMPLATE.Ground`. +-- @param #string GroupName Name of the spawned group. **Must be unique!** +-- @param #number CountryID Country ID. Default `country.id.USA`. Coalition is automatically determined by the one the country belongs to. +-- @param DCS#Vec3 Vec3 Position of the group and the first unit. +-- @param #number Nunits Number of units. Default 1. +-- @param #number Radius Spawn radius for additonal units in meters. Default 500 m. +-- @return #table Template Template table. +function TEMPLATE.GetHelicopter(TypeName, GroupName, CountryID, Vec3, Nunits, Radius) + + -- Defaults. + TypeName=TypeName or TEMPLATE.TypeHelicopter.AH1W + GroupName=GroupName or "Helicopter-1" + CountryID=CountryID or country.id.USA + Vec3=Vec3 or {x=0, y=500, z=0} + Nunits=Nunits or 1 + Radius=Radius or 100 + + -- Limit unis to 4. + Nunits=math.min(Nunits, 4) + + local template=TEMPLATE._GetAircraft(false, TypeName, GroupName, CountryID, Vec3, Nunits, Radius) + + return template +end + + +--- Get template for aircraft units. +-- @param #boolean Airplane If true, this is a fixed wing. Else, rotary wing. +-- @param #string TypeName Type name of the unit(s) in the groups. See `TEMPLATE.Ground`. +-- @param #string GroupName Name of the spawned group. **Must be unique!** +-- @param #number CountryID Country ID. Default `country.id.USA`. Coalition is automatically determined by the one the country belongs to. +-- @param DCS#Vec3 Vec3 Position of the group and the first unit. +-- @param #number Nunits Number of units. Default 1. +-- @param #number Radius Spawn radius for additonal units in meters. Default 500 m. +-- @return #table Template Template table. +function TEMPLATE._GetAircraft(Airplane, TypeName, GroupName, CountryID, Vec3, Nunits, Radius) + + -- Defaults. + TypeName=TypeName + GroupName=GroupName or "Aircraft-1" + CountryID=CountryID or country.id.USA + Vec3=Vec3 or {x=0, y=0, z=0} + Nunits=Nunits or 1 + Radius=Radius or 100 + + -- Get generic template. + local template=UTILS.DeepCopy(TEMPLATE.GenericAircraft) + + -- Set group name. + template.name=GroupName + + -- These are additional entries required by the MOOSE _DATABASE:Spawn() function. + template.CountryID=CountryID + template.CoalitionID=coalition.getCountryCoalition(template.CountryID) + if Airplane then + template.CategoryID=Unit.Category.AIRPLANE + else + template.CategoryID=Unit.Category.HELICOPTER + end + + -- Set first unit. + template.units[1].type=TypeName + template.units[1].name=GroupName.."-1" + + -- Set position. + if Vec3 then + TEMPLATE.SetPositionFromVec3(template, Vec3) + end + + -- Set number of units. + TEMPLATE.SetUnits(template, Nunits, COORDINATE:NewFromVec3(Vec3), Radius) + + return template +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set the position of the template. +-- @param #table Template The template to be modified. +-- @param DCS#Vec2 Vec2 2D Position vector with x and y components of the group. +function TEMPLATE.SetPositionFromVec2(Template, Vec2) + + Template.x=Vec2.x + Template.y=Vec2.y + + for _,unit in pairs(Template.units) do + unit.x=Vec2.x + unit.y=Vec2.y + end + + Template.route.points[1].x=Vec2.x + Template.route.points[1].y=Vec2.y + Template.route.points[1].alt=0 --TODO: Use land height. + +end + +--- Set the position of the template. +-- @param #table Template The template to be modified. +-- @param DCS#Vec3 Vec3 Position vector of the group. +function TEMPLATE.SetPositionFromVec3(Template, Vec3) + + local Vec2={x=Vec3.x, y=Vec3.z} + + TEMPLATE.SetPositionFromVec2(Template, Vec2) + +end + +--- Set the position of the template. +-- @param #table Template The template to be modified. +-- @param #number N Total number of units in the group. +-- @param Core.Point#COORDINATE Coordinate Position of the first unit. +-- @param #number Radius Radius in meters to randomly place the additional units. +function TEMPLATE.SetUnits(Template, N, Coordinate, Radius) + + local units=Template.units + + local unit1=units[1] + + local Vec3=Coordinate:GetVec3() + + unit1.x=Vec3.x + unit1.y=Vec3.z + unit1.alt=Vec3.y + + for i=2,N do + units[i]=UTILS.DeepCopy(unit1) + end + + for i=1,N do + local unit=units[i] + unit.name=string.format("%s-%d", Template.name, i) + if i>1 then + local vec2=Coordinate:GetRandomCoordinateInRadius(Radius, 5):GetVec2() + unit.x=vec2.x + unit.y=vec2.y + unit.alt=unit1.alt + end + end + +end + +--- Set the position of the template. +-- @param #table Template The template to be modified. +-- @param Wrapper.Airbase#AIRBASE AirBase The airbase where the aircraft are spawned. +-- @param #table ParkingSpots List of parking spot IDs. Every unit needs one! +-- @param #boolean EngineOn If true, aircraft are spawned hot. +function TEMPLATE.SetAirbase(Template, AirBase, ParkingSpots, EngineOn) + + -- Airbase ID. + local AirbaseID=AirBase:GetID() + + -- Spawn point. + local point=Template.route.points[1] + + -- Set ID. + if AirBase:IsAirdrome() then + point.airdromeId=AirbaseID + else + point.helipadId=AirbaseID + point.linkUnit=AirbaseID + end + + if EngineOn then + point.action=COORDINATE.WaypointAction.FromParkingAreaHot + point.type=COORDINATE.WaypointType.TakeOffParkingHot + else + point.action=COORDINATE.WaypointAction.FromParkingArea + point.type=COORDINATE.WaypointType.TakeOffParking + end + + for i,unit in ipairs(Template.units) do + unit.parking_id=ParkingSpots[i] + end + +end + +--- Add a waypoint. +-- @param #table Template The template to be modified. +-- @param #table Waypoint Waypoint table. +function TEMPLATE.AddWaypoint(Template, Waypoint) + + table.insert(Template.route.points, Waypoint) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Generic Ground Template +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +TEMPLATE.GenericGround= +{ + ["visible"] = false, + ["tasks"] = {}, -- end of ["tasks"] + ["uncontrollable"] = false, + ["task"] = "Ground Nothing", + ["route"] = + { + ["spans"] = {}, -- end of ["spans"] + ["points"] = + { + [1] = + { + ["alt"] = 0, + ["type"] = "Turning Point", + ["ETA"] = 0, + ["alt_type"] = "BARO", + ["formation_template"] = "", + ["y"] = 0, + ["x"] = 0, + ["ETA_locked"] = true, + ["speed"] = 0, + ["action"] = "Off Road", + ["task"] = + { + ["id"] = "ComboTask", + ["params"] = + { + ["tasks"] = + { + }, -- end of ["tasks"] + }, -- end of ["params"] + }, -- end of ["task"] + ["speed_locked"] = true, + }, -- end of [1] + }, -- end of ["points"] + }, -- end of ["route"] + ["groupId"] = nil, + ["hidden"] = false, + ["units"] = + { + [1] = + { + ["transportable"] = + { + ["randomTransportable"] = false, + }, -- end of ["transportable"] + ["skill"] = "Average", + ["type"] = "Infantry AK", + ["unitId"] = nil, + ["y"] = 0, + ["x"] = 0, + ["name"] = "Infantry AK-47 Rus", + ["heading"] = 0, + ["playerCanDrive"] = false, + }, -- end of [1] + }, -- end of ["units"] + ["y"] = 0, + ["x"] = 0, + ["name"] = "Infantry AK-47 Rus", + ["start_time"] = 0, +} + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Generic Ship Template +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +TEMPLATE.GenericNaval= +{ + ["visible"] = false, + ["tasks"] = {}, -- end of ["tasks"] + ["uncontrollable"] = false, + ["route"] = + { + ["points"] = + { + [1] = + { + ["alt"] = 0, + ["type"] = "Turning Point", + ["ETA"] = 0, + ["alt_type"] = "BARO", + ["formation_template"] = "", + ["y"] = 0, + ["x"] = 0, + ["ETA_locked"] = true, + ["speed"] = 0, + ["action"] = "Turning Point", + ["task"] = + { + ["id"] = "ComboTask", + ["params"] = + { + ["tasks"] = + { + }, -- end of ["tasks"] + }, -- end of ["params"] + }, -- end of ["task"] + ["speed_locked"] = true, + }, -- end of [1] + }, -- end of ["points"] + }, -- end of ["route"] + ["groupId"] = nil, + ["hidden"] = false, + ["units"] = + { + [1] = + { + ["transportable"] = + { + ["randomTransportable"] = false, + }, -- end of ["transportable"] + ["skill"] = "Average", + ["type"] = "TICONDEROG", + ["unitId"] = nil, + ["y"] = 0, + ["x"] = 0, + ["name"] = "Naval-1-1", + ["heading"] = 0, + ["modulation"] = 0, + ["frequency"] = 127500000, + }, -- end of [1] + }, -- end of ["units"] + ["y"] = 0, + ["x"] = 0, + ["name"] = "Naval-1", + ["start_time"] = 0, +} + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Generic Aircraft Template +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +TEMPLATE.GenericAircraft= +{ + ["groupId"] = nil, + ["name"] = "Rotary-1", + ["uncontrolled"] = false, + ["hidden"] = false, + ["task"] = "Nothing", + ["y"] = 0, + ["x"] = 0, + ["start_time"] = 0, + ["communication"] = true, + ["radioSet"] = false, + ["frequency"] = 127.5, + ["modulation"] = 0, + ["taskSelected"] = true, + ["tasks"] = {}, -- end of ["tasks"] + ["route"] = + { + ["points"] = + { + [1] = + { + ["y"] = 0, + ["x"] = 0, + ["alt"] = 1000, + ["alt_type"] = "BARO", + ["action"] = "Turning Point", + ["type"] = "Turning Point", + ["airdromeId"] = nil, + ["task"] = + { + ["id"] = "ComboTask", + ["params"] = + { + ["tasks"] = {}, -- end of ["tasks"] + }, -- end of ["params"] + }, -- end of ["task"] + ["ETA"] = 0, + ["ETA_locked"] = true, + ["speed"] = 100, + ["speed_locked"] = true, + ["formation_template"] = "", + }, -- end of [1] + }, -- end of ["points"] + }, -- end of ["route"] + ["units"] = + { + [1] = + { + ["name"] = "Rotary-1-1", + ["unitId"] = nil, + ["type"] = "AH-1W", + ["onboard_num"] = "050", + ["livery_id"] = "USA X Black", + ["skill"] = "High", + ["ropeLength"] = 15, + ["speed"] = 0, + ["x"] = 0, + ["y"] = 0, + ["alt"] = 10, + ["alt_type"] = "BARO", + ["heading"] = 0, + ["psi"] = 0, + ["parking"] = nil, + ["parking_id"] = nil, + ["payload"] = + { + ["pylons"] = {}, -- end of ["pylons"] + ["fuel"] = "1250.0", + ["flare"] = 30, + ["chaff"] = 30, + ["gun"] = 100, + }, -- end of ["payload"] + ["callsign"] = + { + [1] = 2, + [2] = 1, + [3] = 1, + ["name"] = "Springfield11", + }, -- end of ["callsign"] + }, -- end of [1] + }, -- end of ["units"] +} +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 1e9c2597b..87198a2d7 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -50,6 +50,7 @@ BIGSMOKEPRESET = { -- @field #string PersianGulf Persian Gulf map. -- @field #string TheChannel The Channel map. -- @field #string Syria Syria map. +-- @field #string MarianaIslands Mariana Islands map. DCSMAP = { Caucasus="Caucasus", NTTR="Nevada", @@ -57,6 +58,7 @@ DCSMAP = { PersianGulf="PersianGulf", TheChannel="TheChannel", Syria="Syria", + MarianaIslands="MarianaIslands" } @@ -668,6 +670,17 @@ function UTILS.GetMarkID() end +--- Remove an object (marker, circle, arrow, text, quad, ...) on the F10 map. +-- @param #number MarkID Unique ID of the object. +-- @param #number Delay (Optional) Delay in seconds before the mark is removed. +function UTILS.RemoveMark(MarkID, Delay) + if Delay and Delay>0 then + TIMER:New(UTILS.RemoveMark, MarkID):Start(Delay) + else + trigger.action.removeMark(MarkID) + end +end + -- Test if a Vec2 is in a radius of another Vec2 function UTILS.IsInRadius( InVec2, Vec2, Radius ) @@ -685,7 +698,10 @@ function UTILS.IsInSphere( InVec3, Vec3, Radius ) return InSphere end --- Beaufort scale: returns Beaufort number and wind description as a function of wind speed in m/s. +--- Beaufort scale: returns Beaufort number and wind description as a function of wind speed in m/s. +-- @param #number speed Wind speed in m/s. +-- @return #number Beaufort number. +-- @return #string Beauford wind description. function UTILS.BeaufortScale(speed) local bn=nil local bd=nil @@ -745,6 +761,21 @@ function UTILS.Split(str, sep) return result end +--- Get a table of all characters in a string. +-- @param #string str Sting. +-- @return #table Individual characters. +function UTILS.GetCharacters(str) + + local chars={} + + for i=1,#str do + local c=str:sub(i,i) + table.insert(chars, c) + end + + return chars +end + --- Convert time in seconds to hours, minutes and seconds. -- @param #number seconds Time in seconds, e.g. from timer.getAbsTime() function. -- @param #boolean short (Optional) If true, use short output, i.e. (HH:)MM:SS without day. @@ -1194,6 +1225,9 @@ end -- * NTTR +12 (East), year ~ 2011 -- * Normandy -10 (West), year ~ 1944 -- * Persian Gulf +2 (East), year ~ 2011 +-- * The Cannel Map -10 (West) +-- * Syria +5 (East) +-- * Mariana Islands +2 (East) -- @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) @@ -1214,6 +1248,8 @@ function UTILS.GetMagneticDeclination(map) declination=-10 elseif map==DCSMAP.Syria then declination=5 + elseif map==DCSMAP.MarianaIslands then + declination=2 else declination=0 end @@ -1342,6 +1378,8 @@ function UTILS.GMTToLocalTimeDifference() return 2 -- This map currently needs +2 elseif theatre==DCSMAP.Syria then return 3 -- Damascus is UTC+3 hours + elseif theatre==DCSMAP.MarianaIslands then + return 10 -- Guam is UTC+10 hours. else BASE:E(string.format("ERROR: Unknown Map %s in UTILS.GMTToLocal function. Returning 0", tostring(theatre))) return 0 @@ -1529,3 +1567,188 @@ function UTILS.ShuffleTable(t) end return TempTable end + +--- (Helicopter) Check if one loading door is open. +--@param #string unit_name Unit name to be checked +--@return #boolean Outcome - true if a (loading door) is open, false if not, nil if none exists. +function UTILS.IsLoadingDoorOpen( unit_name ) + + local ret_val = false + local unit = Unit.getByName(unit_name) + if unit ~= nil then + local type_name = unit:getTypeName() + + if type_name == "Mi-8MT" and unit:getDrawArgumentValue(86) == 1 or unit:getDrawArgumentValue(250) == 1 then + BASE:T(unit_name .. " Cargo doors are open or cargo door not present") + ret_val = true + end + + if type_name == "Mi-24P" and unit:getDrawArgumentValue(38) == 1 or unit:getDrawArgumentValue(86) == 1 then + BASE:T(unit_name .. " a side door is open") + ret_val = true + end + + if type_name == "UH-1H" and unit:getDrawArgumentValue(43) == 1 or unit:getDrawArgumentValue(44) == 1 then + BASE:T(unit_name .. " a side door is open ") + ret_val = true + end + + if string.find(type_name, "SA342" ) and unit:getDrawArgumentValue(34) == 1 or unit:getDrawArgumentValue(38) == 1 then + BASE:T(unit_name .. " front door(s) are open") + ret_val = true + end + + if ret_val == false then + BASE:T(unit_name .. " all doors are closed") + end + return ret_val + + end -- nil + + return nil +end + +--- Function to generate valid FM frequencies in mHz for radio beacons (FM). +-- @return #table Table of frequencies. +function UTILS.GenerateFMFrequencies() + local FreeFMFrequencies = {} + for _first = 3, 7 do + for _second = 0, 5 do + for _third = 0, 9 do + local _frequency = ((100 * _first) + (10 * _second) + _third) * 100000 --extra 0 because we didnt bother with 4th digit + table.insert(FreeFMFrequencies, _frequency) + end + end + end + return FreeFMFrequencies +end + +--- Function to generate valid VHF frequencies in kHz for radio beacons (FM). +-- @return #table VHFrequencies +function UTILS.GenerateVHFrequencies() + + -- known and sorted map-wise NDBs in kHz + local _skipFrequencies = { + 214,274,291.5,295,297.5, + 300.5,304,307,309.5,311,312,312.5,316, + 320,324,328,329,330,332,336,337, + 342,343,348,351,352,353,358, + 363,365,368,372.5,374, + 380,381,384,385,389,395,396, + 414,420,430,432,435,440,450,455,462,470,485, + 507,515,520,525,528,540,550,560,570,577,580, + 602,625,641,662,670,680,682,690, + 705,720,722,730,735,740,745,750,770,795, + 822,830,862,866, + 905,907,920,935,942,950,995, + 1000,1025,1030,1050,1065,1116,1175,1182,1210 + } + + local FreeVHFFrequencies = {} + + -- first range + local _start = 200000 + while _start < 400000 do + + -- skip existing NDB frequencies# + local _found = false + for _, value in pairs(_skipFrequencies) do + if value * 1000 == _start then + _found = true + break + end + end + if _found == false then + table.insert(FreeVHFFrequencies, _start) + end + _start = _start + 10000 + end + + -- second range + _start = 400000 + while _start < 850000 do + -- skip existing NDB frequencies + local _found = false + for _, value in pairs(_skipFrequencies) do + if value * 1000 == _start then + _found = true + break + end + end + if _found == false then + table.insert(FreeVHFFrequencies, _start) + end + _start = _start + 10000 + end + + -- third range + _start = 850000 + while _start <= 999000 do -- adjusted for Gazelle + -- skip existing NDB frequencies + local _found = false + for _, value in pairs(_skipFrequencies) do + if value * 1000 == _start then + _found = true + break + end + end + if _found == false then + table.insert(FreeVHFFrequencies, _start) + end + _start = _start + 50000 + end + + return FreeVHFFrequencies +end + +--- Function to generate valid UHF Frequencies in mHz (AM). +-- @return #table UHF Frequencies +function UTILS.GenerateUHFrequencies() + + local FreeUHFFrequencies = {} + local _start = 220000000 + + while _start < 399000000 do + table.insert(FreeUHFFrequencies, _start) + _start = _start + 500000 + end + + return FreeUHFFrequencies +end + +--- Function to generate valid laser codes for JTAC. +-- @return #table Laser Codes. +function UTILS.GenerateLaserCodes() + local jtacGeneratedLaserCodes = {} + + -- helper function + local function ContainsDigit(_number, _numberToFind) + local _thisNumber = _number + local _thisDigit = 0 + while _thisNumber ~= 0 do + _thisDigit = _thisNumber % 10 + _thisNumber = math.floor(_thisNumber / 10) + if _thisDigit == _numberToFind then + return true + end + end + return false + end + + -- generate list of laser codes + local _code = 1111 + local _count = 1 + while _code < 1777 and _count < 30 do + while true do + _code = _code + 1 + if not self:_ContainsDigit(_code, 8) + and not ContainsDigit(_code, 9) + and not ContainsDigit(_code, 0) then + table.insert(jtacGeneratedLaserCodes, _code) + break + end + end + _count = _count + 1 + end + return jtacGeneratedLaserCodes +end \ No newline at end of file diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index 62036f59e..0d8387985 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -74,7 +74,7 @@ AIRBASE = { --- Enumeration to identify the airbases in the Caucasus region. -- --- These are all airbases of Caucasus: +-- Airbases of the Caucasus map: -- -- * AIRBASE.Caucasus.Gelendzhik -- * AIRBASE.Caucasus.Krasnodar_Pashkovsky @@ -123,7 +123,7 @@ AIRBASE.Caucasus = { ["Beslan"] = "Beslan", } ---- These are all airbases of Nevada: +--- Airbases of the Nevada map: -- -- * AIRBASE.Nevada.Creech_AFB -- * AIRBASE.Nevada.Groom_Lake_AFB @@ -142,6 +142,7 @@ AIRBASE.Caucasus = { -- * AIRBASE.Nevada.Pahute_Mesa_Airstrip -- * AIRBASE.Nevada.Tonopah_Airport -- * AIRBASE.Nevada.Tonopah_Test_Range_Airfield +-- -- @field Nevada AIRBASE.Nevada = { ["Creech_AFB"] = "Creech AFB", @@ -163,7 +164,7 @@ AIRBASE.Nevada = { ["Tonopah_Test_Range_Airfield"] = "Tonopah Test Range Airfield", } ---- These are all airbases of Normandy: +--- Airbases of the Normandy map: -- -- * AIRBASE.Normandy.Saint_Pierre_du_Mont -- * AIRBASE.Normandy.Lignerolles @@ -196,6 +197,7 @@ AIRBASE.Nevada = { -- * AIRBASE.Normandy.Funtington -- * AIRBASE.Normandy.Tangmere -- * AIRBASE.Normandy.Ford_AF +-- -- @field Normandy AIRBASE.Normandy = { ["Saint_Pierre_du_Mont"] = "Saint Pierre du Mont", @@ -238,7 +240,7 @@ AIRBASE.Normandy = { ["Conches"] = "Conches", } ---- These are all airbases of the Persion Gulf Map: +--- Airbases of the Persion Gulf Map: -- -- * AIRBASE.PersianGulf.Abu_Dhabi_International_Airport -- * AIRBASE.PersianGulf.Abu_Musa_Island_Airport @@ -269,6 +271,7 @@ AIRBASE.Normandy = { -- * AIRBASE.PersianGulf.Sirri_Island -- * AIRBASE.PersianGulf.Tunb_Island_AFB -- * AIRBASE.PersianGulf.Tunb_Kochak +-- -- @field PersianGulf AIRBASE.PersianGulf = { ["Abu_Dhabi_International_Airport"] = "Abu Dhabi Intl", @@ -302,7 +305,7 @@ AIRBASE.PersianGulf = { ["Tunb_Kochak"] = "Tunb Kochak", } ---- These are all airbases of the The Channel Map: +--- Airbases of The Channel Map: -- -- * AIRBASE.TheChannel.Abbeville_Drucat -- * AIRBASE.TheChannel.Merville_Calonne @@ -327,7 +330,7 @@ AIRBASE.TheChannel = { ["High_Halden"] = "High Halden", } ---- Airbases of Syria +--- Airbases of the Syria map: -- -- * AIRBASE.Syria.Kuweires -- * AIRBASE.Syria.Marj_Ruhayyil @@ -337,41 +340,53 @@ AIRBASE.TheChannel = { -- * AIRBASE.Syria.Incirlik -- * AIRBASE.Syria.Damascus -- * AIRBASE.Syria.Bassel_Al_Assad +-- * AIRBASE.Syria.Rosh_Pina -- * AIRBASE.Syria.Aleppo --- * AIRBASE.Syria.Qabr_as_Sitt +-- * AIRBASE.Syria.Al_Qusayr -- * AIRBASE.Syria.Wujah_Al_Hajar -- * AIRBASE.Syria.Al_Dumayr +-- * AIRBASE.Syria.Gazipasa +-- * AIRBASE.Syria.Ru_Convoy_4 -- * AIRBASE.Syria.Hatay +-- * AIRBASE.Syria.Nicosia +-- * AIRBASE.Syria.Pinarbashi +-- * AIRBASE.Syria.Paphos +-- * AIRBASE.Syria.Kingsfield +-- * AIRBASE.Syria.Thalah -- * AIRBASE.Syria.Haifa -- * AIRBASE.Syria.Khalkhalah -- * AIRBASE.Syria.Megiddo +-- * AIRBASE.Syria.Lakatamia -- * AIRBASE.Syria.Rayak +-- * AIRBASE.Syria.Larnaca -- * AIRBASE.Syria.Mezzeh --- * AIRBASE.Syria.King_Hussein_Air_College --- * AIRBASE.Syria.Jirah +-- * AIRBASE.Syria.Gecitkale +-- * AIRBASE.Syria.Akrotiri +-- * AIRBASE.Syria.Naqoura +-- * AIRBASE.Syria.Gaziantep +-- * AIRBASE.Syria.CVN_71 +-- * AIRBASE.Syria.Sayqal +-- * AIRBASE.Syria.Tiyas +-- * AIRBASE.Syria.Shayrat -- * AIRBASE.Syria.Taftanaz +-- * AIRBASE.Syria.H4 +-- * AIRBASE.Syria.King_Hussein_Air_College -- * AIRBASE.Syria.Rene_Mouawad +-- * AIRBASE.Syria.Jirah -- * AIRBASE.Syria.Ramat_David +-- * AIRBASE.Syria.Qabr_as_Sitt -- * AIRBASE.Syria.Minakh -- * AIRBASE.Syria.Adana_Sakirpasa --- * AIRBASE.Syria.Marj_as_Sultan_South --- * AIRBASE.Syria.Hama --- * AIRBASE.Syria.Al_Qusayr -- * AIRBASE.Syria.Palmyra +-- * AIRBASE.Syria.Hama +-- * AIRBASE.Syria.Ercan +-- * AIRBASE.Syria.Marj_as_Sultan_South -- * AIRBASE.Syria.Tabqa -- * AIRBASE.Syria.Beirut_Rafic_Hariri -- * AIRBASE.Syria.An_Nasiriyah -- * AIRBASE.Syria.Abu_al_Duhur --- * AIRBASE.Syria.H4 --- * AIRBASE.Syria.Gaziantep --- * AIRBASE.Syria.Rosh_Pina --- * AIRBASE.Syria.Sayqal --- * AIRBASE.Syria.Shayrat --- * AIRBASE.Syria.Tiyas --- * AIRBASE.Syria.Tha_lah --- * AIRBASE.Syria.Naqoura -- --- @field Syria +--@field Syria AIRBASE.Syria={ ["Kuweires"]="Kuweires", ["Marj_Ruhayyil"]="Marj Ruhayyil", @@ -381,39 +396,71 @@ AIRBASE.Syria={ ["Incirlik"]="Incirlik", ["Damascus"]="Damascus", ["Bassel_Al_Assad"]="Bassel Al-Assad", + ["Rosh_Pina"]="Rosh Pina", ["Aleppo"]="Aleppo", - ["Qabr_as_Sitt"]="Qabr as Sitt", + ["Al_Qusayr"]="Al Qusayr", ["Wujah_Al_Hajar"]="Wujah Al Hajar", ["Al_Dumayr"]="Al-Dumayr", + ["Gazipasa"]="Gazipasa", + ["Ru_Convoy_4"]="Ru Convoy-4", ["Hatay"]="Hatay", + ["Nicosia"]="Nicosia", + ["Pinarbashi"]="Pinarbashi", + ["Paphos"]="Paphos", + ["Kingsfield"]="Kingsfield", + ["Thalah"]="Tha'lah", ["Haifa"]="Haifa", ["Khalkhalah"]="Khalkhalah", ["Megiddo"]="Megiddo", + ["Lakatamia"]="Lakatamia", ["Rayak"]="Rayak", + ["Larnaca"]="Larnaca", ["Mezzeh"]="Mezzeh", - ["King_Hussein_Air_College"]="King Hussein Air College", - ["Jirah"]="Jirah", + ["Gecitkale"]="Gecitkale", + ["Akrotiri"]="Akrotiri", + ["Naqoura"]="Naqoura", + ["Gaziantep"]="Gaziantep", + ["Sayqal"]="Sayqal", + ["Tiyas"]="Tiyas", + ["Shayrat"]="Shayrat", ["Taftanaz"]="Taftanaz", + ["H4"]="H4", + ["King_Hussein_Air_College"]="King Hussein Air College", ["Rene_Mouawad"]="Rene Mouawad", + ["Jirah"]="Jirah", ["Ramat_David"]="Ramat David", + ["Qabr_as_Sitt"]="Qabr as Sitt", ["Minakh"]="Minakh", ["Adana_Sakirpasa"]="Adana Sakirpasa", - ["Marj_as_Sultan_South"]="Marj as Sultan South", - ["Hama"]="Hama", - ["Al_Qusayr"]="Al Qusayr", ["Palmyra"]="Palmyra", + ["Hama"]="Hama", + ["Ercan"]="Ercan", + ["Marj_as_Sultan_South"]="Marj as Sultan South", ["Tabqa"]="Tabqa", ["Beirut_Rafic_Hariri"]="Beirut-Rafic Hariri", ["An_Nasiriyah"]="An Nasiriyah", ["Abu_al_Duhur"]="Abu al-Duhur", - ["H4"]="H4", - ["Gaziantep"]="Gaziantep", - ["Rosh_Pina"]="Rosh Pina", - ["Sayqal"]="Sayqal", - ["Shayrat"]="Shayrat", - ["Tiyas"]="Tiyas", - ["Tha_lah"]="Tha'lah", - ["Naqoura"]="Naqoura", +} + + + +--- Airbases of the Mariana Islands map: +-- +-- * AIRBASE.MarianaIslands.Rota_Intl +-- * AIRBASE.MarianaIslands.Andersen_AFB +-- * AIRBASE.MarianaIslands.Antonio_B_Won_Pat_Intl +-- * AIRBASE.MarianaIslands.Saipan_Intl +-- * AIRBASE.MarianaIslands.Tinian_Intl +-- * AIRBASE.MarianaIslands.Olf_Orote +-- +--@field MarianaIslands +AIRBASE.MarianaIslands={ + ["Rota_Intl"]="Rota Intl", + ["Andersen_AFB"]="Andersen AFB", + ["Antonio_B_Won_Pat_Intl"]="Antonio B. Won Pat Intl", + ["Saipan_Intl"]="Saipan Intl", + ["Tinian_Intl"]="Tinian Intl", + ["Olf_Orote"]="Olf Orote", } @@ -1388,7 +1435,8 @@ function AIRBASE:GetRunwayData(magvar, mark) name==AIRBASE.PersianGulf.Abu_Dhabi_International_Airport or name==AIRBASE.PersianGulf.Dubai_Intl or name==AIRBASE.PersianGulf.Shiraz_International_Airport or - name==AIRBASE.PersianGulf.Kish_International_Airport then + name==AIRBASE.PersianGulf.Kish_International_Airport or + name==AIRBASE.MarianaIslands.Andersen_AFB then -- 1-->4, 2-->3, 3-->2, 4-->1 exception=1 diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 165835229..01e3867ba 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -1832,7 +1832,7 @@ end do -- Patrol methods - --- (GROUND) Patrol iteratively using the waypoints the for the (parent) group. + --- (GROUND) Patrol iteratively using the waypoints of the (parent) group. -- @param #CONTROLLABLE self -- @return #CONTROLLABLE function CONTROLLABLE:PatrolRoute() @@ -1851,8 +1851,27 @@ do -- Patrol methods -- Calculate the new Route. local FromCoord = PatrolGroup:GetCoordinate() - local From = FromCoord:WaypointGround( 120 ) - + + -- test for submarine + local depth = 0 + local IsSub = false + if PatrolGroup:IsShip() then + local navalvec3 = FromCoord:GetVec3() + if navalvec3.y < 0 then + depth = navalvec3.y + IsSub = true + end + end + + + local Waypoint = Waypoints[1] + local Speed = Waypoint.speed or (20 / 3.6) + local From = FromCoord:WaypointGround( Speed ) + + if IsSub then + From = FromCoord:WaypointNaval( Speed, Waypoint.alt ) + end + table.insert( Waypoints, 1, From ) local TaskRoute = PatrolGroup:TaskFunction( "CONTROLLABLE.PatrolRoute" ) @@ -1892,7 +1911,16 @@ do -- Patrol methods if ToWaypoint then FromWaypoint = ToWaypoint end - + -- test for submarine + local depth = 0 + local IsSub = false + if PatrolGroup:IsShip() then + local navalvec3 = FromCoord:GetVec3() + if navalvec3.y < 0 then + depth = navalvec3.y + IsSub = true + end + end -- Loop until a waypoint has been found that is not the same as the current waypoint. -- Otherwise the object zon't move or drive in circles and the algorithm would not do exactly -- what it is supposed to do, which is making groups drive around. @@ -1907,9 +1935,13 @@ do -- Patrol methods local ToCoord = COORDINATE:NewFromVec2( { x = Waypoint.x, y = Waypoint.y } ) -- Create a "ground route point", which is a "point" structure that can be given as a parameter to a Task local Route = {} - Route[#Route+1] = FromCoord:WaypointGround( Speed, Formation ) - Route[#Route+1] = ToCoord:WaypointGround( Speed, Formation ) - + if IsSub then + Route[#Route+1] = FromCoord:WaypointNaval( Speed, depth ) + Route[#Route+1] = ToCoord:WaypointNaval( Speed, Waypoint.alt ) + else + Route[#Route+1] = FromCoord:WaypointGround( Speed, Formation ) + Route[#Route+1] = ToCoord:WaypointGround( Speed, Formation ) + end local TaskRouteToZone = PatrolGroup:TaskFunction( "CONTROLLABLE.PatrolRouteRandom", Speed, Formation, ToWaypoint ) @@ -1950,9 +1982,20 @@ do -- Patrol methods self:F( { PatrolGroup = PatrolGroup:GetName() } ) if PatrolGroup:IsGround() or PatrolGroup:IsShip() then - + -- Calculate the new Route. local FromCoord = PatrolGroup:GetCoordinate() + + -- test for submarine + local depth = 0 + local IsSub = false + if PatrolGroup:IsShip() then + local navalvec3 = FromCoord:GetVec3() + if navalvec3.y < 0 then + depth = navalvec3.y + IsSub = true + end + end -- Select a random Zone and get the Coordinate of the new Zone. local RandomZone = ZoneList[ math.random( 1, #ZoneList ) ] -- Core.Zone#ZONE @@ -1960,9 +2003,13 @@ do -- Patrol methods -- Create a "ground route point", which is a "point" structure that can be given as a parameter to a Task local Route = {} - Route[#Route+1] = FromCoord:WaypointGround( Speed, Formation ) - Route[#Route+1] = ToCoord:WaypointGround( Speed, Formation ) - + if IsSub then + Route[#Route+1] = FromCoord:WaypointNaval( Speed, depth ) + Route[#Route+1] = ToCoord:WaypointNaval( Speed, depth ) + else + Route[#Route+1] = FromCoord:WaypointGround( Speed, Formation ) + Route[#Route+1] = ToCoord:WaypointGround( Speed, Formation ) + end local TaskRouteToZone = PatrolGroup:TaskFunction( "CONTROLLABLE.PatrolZones", ZoneList, Speed, Formation, DelayMin, DelayMax ) @@ -3770,10 +3817,30 @@ function CONTROLLABLE:OptionDisperseOnAttack(Seconds) local Controller = self:_GetController() if Controller then if self:IsGround() then - self:SetOption(AI.Option.GROUND.id.DISPERSE_ON_ATTACK, seconds) + self:SetOption(AI.Option.Ground.id.DISPERSE_ON_ATTACK, seconds) end end return self end return nil end + +--- Returns if the unit is a submarine. +-- @param #POSITIONABLE self +-- @return #boolean Submarines attributes result. +function POSITIONABLE:IsSubmarine() + self:F2() + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitDescriptor = DCSUnit:getDesc() + if UnitDescriptor.attributes["Submarines"] == true then + return true + else + return false + end + end + + return nil +end diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index 1a2f00fa8..a9c678ab5 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -2551,9 +2551,10 @@ do -- Players end ---- GROUND - Switch on/off radar emissions +--- GROUND - Switch on/off radar emissions for the group. -- @param #GROUP self --- @param #boolean switch +-- @param #boolean switch If true, emission is enabled. If false, emission is disabled. +-- @return #GROUP self function GROUP:EnableEmission(switch) self:F2( self.GroupName ) local switch = switch or false @@ -2566,6 +2567,31 @@ function GROUP:EnableEmission(switch) end + return self +end + +--- Switch on/off invisible flag for the group. +-- @param #GROUP self +-- @param #boolean switch If true, emission is enabled. If false, emission is disabled. +-- @return #GROUP self +function GROUP:SetCommandInvisible(switch) + self:F2( self.GroupName ) + local switch = switch or false + local SetInvisible = {id = 'SetInvisible', params = {value = true}} + self:SetCommand(SetInvisible) + return self +end + +--- Switch on/off immortal flag for the group. +-- @param #GROUP self +-- @param #boolean switch If true, emission is enabled. If false, emission is disabled. +-- @return #GROUP self +function GROUP:SetCommandImmortal(switch) + self:F2( self.GroupName ) + local switch = switch or false + local SetInvisible = {id = 'SetImmortal', params = {value = true}} + self:SetCommand(SetInvisible) + return self end --do -- Smoke diff --git a/Moose Development/Moose/Wrapper/Marker.lua b/Moose Development/Moose/Wrapper/Marker.lua index c37b1b263..695d2179d 100644 --- a/Moose Development/Moose/Wrapper/Marker.lua +++ b/Moose Development/Moose/Wrapper/Marker.lua @@ -646,7 +646,7 @@ function MARKER:OnEventMarkRemoved(EventData) local MarkID=EventData.MarkID - self:T3(self.lid..string.format("Captured event MarkAdded for Mark ID=%s", tostring(MarkID))) + self:T3(self.lid..string.format("Captured event MarkRemoved for Mark ID=%s", tostring(MarkID))) if MarkID==self.mid then @@ -673,9 +673,9 @@ function MARKER:OnEventMarkChange(EventData) if MarkID==self.mid then - self:Changed(EventData) + self.text=tostring(EventData.MarkText) - self:TextChanged(tostring(EventData.MarkText)) + self:Changed(EventData) end diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index 1a12d7206..ac4e49ad9 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -684,6 +684,27 @@ function POSITIONABLE:IsShip() end +--- Returns if the unit is a submarine. +-- @param #POSITIONABLE self +-- @return #boolean Submarines attributes result. +function POSITIONABLE:IsSubmarine() + self:F2() + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitDescriptor = DCSUnit:getDesc() + if UnitDescriptor.attributes["Submarines"] == true then + return true + else + return false + end + end + + return nil +end + + --- Returns true if the POSITIONABLE is in the air. -- Polymorphic, is overridden in GROUP and UNIT. -- @param Wrapper.Positionable#POSITIONABLE self @@ -817,8 +838,7 @@ end -- @return #number The velocity in knots. function POSITIONABLE:GetVelocityKNOTS() self:F2( self.PositionableName ) - local velmps=self:GetVelocityMPS() - return UTILS.MpsToKnots(velmps) + return UTILS.MpsToKnots(self:GetVelocityMPS()) end --- Returns the Angle of Attack of a positionable. diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index 65199d97d..eb6d472d8 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -761,40 +761,6 @@ function UNIT:GetFuel() return nil end ---- Sets the passed group or unit objects radar emitters on or off. Can be used on sam sites for example to shut down the radar without setting AI off or changing the alarm state. --- @param #UNIT self --- @param #boolean Switch If `true` or `nil`, emission is enabled. If `false`, emission is turned off. --- @return #UNIT self -function UNIT:SetEmission(Switch) - - if Switch==nil then - Switch=true - end - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - DCSUnit:enableEmission(Switch) - end - - return self -end - ---- Sets the passed group or unit objects radar emitters ON. Can be used on sam sites for example to shut down the radar without setting AI off or changing the alarm state. --- @param #UNIT self --- @return #UNIT self -function UNIT:EnableEmission() - self:SetEmission(true) - return self -end - ---- Sets the passed group or unit objects radar emitters OFF. Can be used on sam sites for example to shut down the radar without setting AI off or changing the alarm state. --- @param #UNIT self --- @return #UNIT self -function UNIT:DisableEmission() - self:SetEmission(false) - return self -end --- Returns a list of one @{Wrapper.Unit}. -- @param #UNIT self @@ -1429,9 +1395,10 @@ function UNIT:GetTemplateFuel() return nil end ---- GROUND - Switch on/off radar emissions. +--- GROUND - Switch on/off radar emissions of a unit. -- @param #UNIT self --- @param #boolean switch +-- @param #boolean switch If true, emission is enabled. If false, emission is disabled. +-- @return #UNIT self function UNIT:EnableEmission(switch) self:F2( self.UnitName ) @@ -1445,4 +1412,5 @@ function UNIT:EnableEmission(switch) end + return self end diff --git a/Moose Setup/Moose.files b/Moose Setup/Moose.files index da76cfd49..077e75168 100644 --- a/Moose Setup/Moose.files +++ b/Moose Setup/Moose.files @@ -3,10 +3,12 @@ Utilities/Routines.lua Utilities/Utils.lua Utilities/Enums.lua Utilities/Profiler.lua +Utilities/Templates.lua +Utilities/STTS.lua Core/Base.lua +Core/Beacon.lua Core/UserFlag.lua -Core/UserSound.lua Core/Report.lua Core/Scheduler.lua Core/ScheduleDispatcher.lua @@ -21,9 +23,6 @@ Core/Point.lua Core/Velocity.lua Core/Message.lua Core/Fsm.lua -Core/Radio.lua -Core/RadioQueue.lua -Core/RadioSpeech.lua Core/Spawn.lua Core/SpawnStatic.lua Core/Timer.lua @@ -82,6 +81,8 @@ Ops/NavyGroup.lua Ops/Squadron.lua Ops/AirWing.lua Ops/Intelligence.lua +Ops/CSAR.lua +Ops/CTLD.lua AI/AI_Balancer.lua AI/AI_Air.lua @@ -120,6 +121,13 @@ Actions/Act_Route.lua Actions/Act_Account.lua Actions/Act_Assist.lua +Sound/UserSound.lua +Sound/SoundOutput.lua +Sound/Radio.lua +Sound/RadioQueue.lua +Sound/RadioSpeech.lua +Sound/SRS.lua + Tasking/CommandCenter.lua Tasking/Mission.lua Tasking/Task.lua diff --git a/README.md b/README.md index 976a47f96..b622326a8 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ This repository contains the source lua code of the MOOSE framework. ### [MOOSE_INCLUDE](https://github.com/FlightControl-Master/MOOSE_INCLUDE) - For use and generated -This repository contains the Moose.lua file to be included within your missions. +This repository contains the Moose.lua file to be included within your missions. Note that the Moose\_.lua is technically the same as Moose.lua, but without any commentary or unnecessary whitespace in it. You only need to load **one** of those at the beginning of your mission. ### [MOOSE_DOCS](https://github.com/FlightControl-Master/MOOSE_DOCS) - Not for use