Merge branch 'FlightControl-Master:develop' into develop

This commit is contained in:
Tony Goodale 2021-07-21 22:44:11 -07:00 committed by GitHub
commit f68d3209ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 10852 additions and 2069 deletions

View File

@ -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

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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

View File

@ -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 )

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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.

View File

@ -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/".

View File

@ -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

View File

@ -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

View File

@ -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
-----------------------------------------------------------------------
-----------------------------------------------------------------------

View File

@ -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)

View File

@ -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 <DCS install folder>/Scripts/MissionScripting.lua and comment out the lines with sanitizeModule(''). Use at your own risk!)")
end

View File

@ -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' )

View File

@ -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
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

View File

@ -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

View File

@ -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)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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.

View File

@ -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
----------------------------------------------------------------------------------------------

View File

@ -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.

View File

@ -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

View File

@ -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()

View File

@ -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 },
["Ñ<EFBFBD>Ñепени"] = { "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 },
["возвращаÑ<EFBFBD>Ñ<EFBFBD>ÑŒ на базу"] = { "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 },
["колеÑ<EFBFBD>а вверх"] = { "wheels_up", 0.92 },
["поÑ<EFBFBD>адка на базу"] = { "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 },
}

View File

@ -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
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"]
}
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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