Merge branch 'develop' into FF/Ops

This commit is contained in:
Frank 2023-11-23 22:23:50 +01:00
commit bfaf88f017
8 changed files with 682 additions and 99 deletions

View File

@ -396,7 +396,7 @@ end
CLIENTMENUMANAGER = { CLIENTMENUMANAGER = {
ClassName = "CLIENTMENUMANAGER", ClassName = "CLIENTMENUMANAGER",
lid = "", lid = "",
version = "0.1.3", version = "0.1.4",
name = nil, name = nil,
clientset = nil, clientset = nil,
menutree = {}, menutree = {},
@ -439,18 +439,18 @@ function CLIENTMENUMANAGER:_EventHandler(EventData)
--self:I(self.lid.."_EventHandler: "..tostring(EventData.IniPlayerName)) --self:I(self.lid.."_EventHandler: "..tostring(EventData.IniPlayerName))
if EventData.id == EVENTS.PlayerLeaveUnit or EventData.id == EVENTS.Ejection or EventData.id == EVENTS.Crash or EventData.id == EVENTS.PilotDead then if EventData.id == EVENTS.PlayerLeaveUnit or EventData.id == EVENTS.Ejection or EventData.id == EVENTS.Crash or EventData.id == EVENTS.PilotDead then
self:T(self.lid.."Leave event for player: "..tostring(EventData.IniPlayerName)) self:T(self.lid.."Leave event for player: "..tostring(EventData.IniPlayerName))
local Client = _DATABASE:FindClient( EventData.IniPlayerName ) local Client = _DATABASE:FindClient( EventData.IniUnitName )
if Client then if Client then
self:ResetMenu(Client) self:ResetMenu(Client)
end end
elseif (EventData.id == EVENTS.PlayerEnterAircraft) and EventData.IniCoalition == self.Coalition then elseif (EventData.id == EVENTS.PlayerEnterAircraft) and EventData.IniCoalition == self.Coalition then
if EventData.IniPlayerName and EventData.IniGroup then if EventData.IniPlayerName and EventData.IniGroup then
if (not self.clientset:IsIncludeObject(_DATABASE:FindClient( EventData.IniPlayerName ))) then if (not self.clientset:IsIncludeObject(_DATABASE:FindClient( EventData.IniUnitName ))) then
self:T(self.lid.."Client not in SET: "..EventData.IniPlayerName) self:T(self.lid.."Client not in SET: "..EventData.IniPlayerName)
return self return self
end end
--self:I(self.lid.."Join event for player: "..EventData.IniPlayerName) --self:I(self.lid.."Join event for player: "..EventData.IniPlayerName)
local player = _DATABASE:FindClient( EventData.IniPlayerName ) local player = _DATABASE:FindClient( EventData.IniUnitName )
self:Propagate(player) self:Propagate(player)
end end
elseif EventData.id == EVENTS.PlayerEnterUnit then elseif EventData.id == EVENTS.PlayerEnterUnit then
@ -668,7 +668,7 @@ function CLIENTMENUMANAGER:Propagate(Client)
for _,_client in pairs(Set) do for _,_client in pairs(Set) do
local client = _client -- Wrapper.Client#CLIENT local client = _client -- Wrapper.Client#CLIENT
if client and client:IsAlive() then if client and client:IsAlive() then
local playername = client:GetPlayerName() local playername = client:GetPlayerName() or "none"
if not self.playertree[playername] then if not self.playertree[playername] then
self.playertree[playername] = {} self.playertree[playername] = {}
end end

View File

@ -3283,6 +3283,7 @@ function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) -- R2.2
local CallsignName = SpawnTemplate.units[UnitID].callsign["name"] -- #string local CallsignName = SpawnTemplate.units[UnitID].callsign["name"] -- #string
CallsignName = string.match(CallsignName,"^(%a+)") -- 2.8 - only the part w/o numbers CallsignName = string.match(CallsignName,"^(%a+)") -- 2.8 - only the part w/o numbers
local CallsignLen = CallsignName:len() local CallsignLen = CallsignName:len()
SpawnTemplate.units[UnitID].callsign[2] = UnitID
SpawnTemplate.units[UnitID].callsign["name"] = CallsignName:sub( 1, CallsignLen ) .. SpawnTemplate.units[UnitID].callsign[2] .. SpawnTemplate.units[UnitID].callsign[3] SpawnTemplate.units[UnitID].callsign["name"] = CallsignName:sub( 1, CallsignLen ) .. SpawnTemplate.units[UnitID].callsign[2] .. SpawnTemplate.units[UnitID].callsign[3]
else else
SpawnTemplate.units[UnitID].callsign = Callsign + SpawnIndex SpawnTemplate.units[UnitID].callsign = Callsign + SpawnIndex

View File

@ -20,13 +20,15 @@
-- ### Author: FlightControl - Framework Design & Programming -- ### Author: FlightControl - Framework Design & Programming
-- ### Refactoring to use the Runway auto-detection: Applevangelist -- ### Refactoring to use the Runway auto-detection: Applevangelist
-- @date August 2022 -- @date August 2022
-- Last Update Nov 2023
-- --
-- === -- ===
-- --
-- @module Functional.ATC_Ground -- @module Functional.ATC_Ground
-- @image Air_Traffic_Control_Ground_Operations.JPG -- @image Air_Traffic_Control_Ground_Operations.JPG
--- @type ATC_GROUND ---
-- @type ATC_GROUND
-- @field Core.Set#SET_CLIENT SetClient -- @field Core.Set#SET_CLIENT SetClient
-- @extends Core.Base#BASE -- @extends Core.Base#BASE
@ -39,7 +41,8 @@ ATC_GROUND = {
AirbaseNames = nil, AirbaseNames = nil,
} }
--- @type ATC_GROUND.AirbaseNames ---
-- @type ATC_GROUND.AirbaseNames
-- @list <#string> -- @list <#string>
@ -51,7 +54,7 @@ function ATC_GROUND:New( Airbases, AirbaseList )
-- Inherits from BASE -- Inherits from BASE
local self = BASE:Inherit( self, BASE:New() ) -- #ATC_GROUND local self = BASE:Inherit( self, BASE:New() ) -- #ATC_GROUND
self:E( { self.ClassName, Airbases } ) self:T( { self.ClassName, Airbases } )
self.Airbases = Airbases self.Airbases = Airbases
self.AirbaseList = AirbaseList self.AirbaseList = AirbaseList
@ -82,7 +85,7 @@ function ATC_GROUND:New( Airbases, AirbaseList )
end end
self.SetClient:ForEachClient( self.SetClient:ForEachClient(
--- @param Wrapper.Client#CLIENT Client -- @param Wrapper.Client#CLIENT Client
function( Client ) function( Client )
Client:SetState( self, "Speeding", false ) Client:SetState( self, "Speeding", false )
Client:SetState( self, "Warnings", 0) Client:SetState( self, "Warnings", 0)
@ -246,11 +249,11 @@ function ATC_GROUND:SetMaximumKickSpeedMiph( MaximumKickSpeedMiph, Airbase )
return self return self
end end
--- @param #ATC_GROUND self -- @param #ATC_GROUND self
function ATC_GROUND:_AirbaseMonitor() function ATC_GROUND:_AirbaseMonitor()
self.SetClient:ForEachClient( self.SetClient:ForEachClient(
--- @param Wrapper.Client#CLIENT Client -- @param Wrapper.Client#CLIENT Client
function( Client ) function( Client )
if Client:IsAlive() then if Client:IsAlive() then
@ -258,7 +261,7 @@ function ATC_GROUND:_AirbaseMonitor()
local IsOnGround = Client:InAir() == false local IsOnGround = Client:InAir() == false
for AirbaseID, AirbaseMeta in pairs( self.Airbases ) do for AirbaseID, AirbaseMeta in pairs( self.Airbases ) do
self:E( AirbaseID, AirbaseMeta.KickSpeed ) self:T( AirbaseID, AirbaseMeta.KickSpeed )
if AirbaseMeta.Monitor == true and Client:IsInZone( AirbaseMeta.ZoneBoundary ) then if AirbaseMeta.Monitor == true and Client:IsInZone( AirbaseMeta.ZoneBoundary ) then
@ -271,7 +274,7 @@ function ATC_GROUND:_AirbaseMonitor()
if IsOnGround then if IsOnGround then
local Taxi = Client:GetState( self, "Taxi" ) local Taxi = Client:GetState( self, "Taxi" )
self:E( Taxi ) self:T( Taxi )
if Taxi == false then if Taxi == false then
local Velocity = VELOCITY:New( AirbaseMeta.KickSpeed or self.KickSpeed ) local Velocity = VELOCITY:New( AirbaseMeta.KickSpeed or self.KickSpeed )
Client:Message( "Welcome to " .. AirbaseID .. ". The maximum taxiing speed is " .. Client:Message( "Welcome to " .. AirbaseID .. ". The maximum taxiing speed is " ..
@ -331,7 +334,7 @@ function ATC_GROUND:_AirbaseMonitor()
Client:SetState( self, "Warnings", SpeedingWarnings + 1 ) Client:SetState( self, "Warnings", SpeedingWarnings + 1 )
else else
MESSAGE:New( "Penalty! Player " .. Client:GetPlayerName() .. " has been kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll() MESSAGE:New( "Penalty! Player " .. Client:GetPlayerName() .. " has been kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll()
--- @param Wrapper.Client#CLIENT Client -- @param Wrapper.Client#CLIENT Client
Client:Destroy() Client:Destroy()
Client:SetState( self, "Speeding", false ) Client:SetState( self, "Speeding", false )
Client:SetState( self, "Warnings", 0 ) Client:SetState( self, "Warnings", 0 )
@ -363,7 +366,7 @@ function ATC_GROUND:_AirbaseMonitor()
Client:SetState( self, "OffRunwayWarnings", OffRunwayWarnings + 1 ) Client:SetState( self, "OffRunwayWarnings", OffRunwayWarnings + 1 )
else else
MESSAGE:New( "Penalty! Player " .. Client:GetPlayerName() .. " has been kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll() MESSAGE:New( "Penalty! Player " .. Client:GetPlayerName() .. " has been kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll()
--- @param Wrapper.Client#CLIENT Client -- @param Wrapper.Client#CLIENT Client
Client:Destroy() Client:Destroy()
Client:SetState( self, "IsOffRunway", false ) Client:SetState( self, "IsOffRunway", false )
Client:SetState( self, "OffRunwayWarnings", 0 ) Client:SetState( self, "OffRunwayWarnings", 0 )
@ -424,13 +427,20 @@ ATC_GROUND_UNIVERSAL = {
--- Creates a new ATC\_GROUND\_UNIVERSAL object. This works on any map. --- Creates a new ATC\_GROUND\_UNIVERSAL object. This works on any map.
-- @param #ATC_GROUND_UNIVERSAL self -- @param #ATC_GROUND_UNIVERSAL self
-- @param AirbaseList (Optional) A table of Airbase Names. -- @param AirbaseList A table of Airbase Names. Leave empty to cover **all** airbases of the map.
-- @return #ATC_GROUND_UNIVERSAL self -- @return #ATC_GROUND_UNIVERSAL self
-- @usage
-- -- define monitoring for one airbase
-- local atc=ATC_GROUND_UNIVERSAL:New({AIRBASE.Syria.Gecitkale})
-- -- set kick speed
-- atc:SetKickSpeed(UTILS.KnotsToMps(20))
-- -- start monitoring evey 10 secs
-- atc:Start(10)
function ATC_GROUND_UNIVERSAL:New(AirbaseList) function ATC_GROUND_UNIVERSAL:New(AirbaseList)
-- Inherits from BASE -- Inherits from BASE
local self = BASE:Inherit( self, BASE:New() ) -- #ATC_GROUND local self = BASE:Inherit( self, BASE:New() ) -- #ATC_GROUND
self:E( { self.ClassName } ) self:T( { self.ClassName } )
self.Airbases = {} self.Airbases = {}
@ -440,6 +450,13 @@ function ATC_GROUND_UNIVERSAL:New(AirbaseList)
self.AirbaseList = AirbaseList self.AirbaseList = AirbaseList
if not self.AirbaseList then
self.AirbaseList = {}
for _name,_ in pairs(_DATABASE.AIRBASES) do
self.AirbaseList[_name]=_name
end
end
self.SetClient = SET_CLIENT:New():FilterCategories( "plane" ):FilterStart() self.SetClient = SET_CLIENT:New():FilterCategories( "plane" ):FilterStart()
@ -460,8 +477,9 @@ function ATC_GROUND_UNIVERSAL:New(AirbaseList)
self.Airbases[AirbaseName].Monitor = true self.Airbases[AirbaseName].Monitor = true
end end
self.SetClient:ForEachClient( self.SetClient:ForEachClient(
--- @param Wrapper.Client#CLIENT Client -- @param Wrapper.Client#CLIENT Client
function( Client ) function( Client )
Client:SetState( self, "Speeding", false ) Client:SetState( self, "Speeding", false )
Client:SetState( self, "Warnings", 0) Client:SetState( self, "Warnings", 0)
@ -679,7 +697,7 @@ end
-- @param #ATC_GROUND_UNIVERSAL self -- @param #ATC_GROUND_UNIVERSAL self
-- @return #ATC_GROUND_UNIVERSAL self -- @return #ATC_GROUND_UNIVERSAL self
function ATC_GROUND_UNIVERSAL:_AirbaseMonitor() function ATC_GROUND_UNIVERSAL:_AirbaseMonitor()
self:I("_AirbaseMonitor")
self.SetClient:ForEachClient( self.SetClient:ForEachClient(
--- @param Wrapper.Client#CLIENT Client --- @param Wrapper.Client#CLIENT Client
function( Client ) function( Client )
@ -689,7 +707,7 @@ function ATC_GROUND_UNIVERSAL:_AirbaseMonitor()
local IsOnGround = Client:InAir() == false local IsOnGround = Client:InAir() == false
for AirbaseID, AirbaseMeta in pairs( self.Airbases ) do for AirbaseID, AirbaseMeta in pairs( self.Airbases ) do
self:E( AirbaseID, AirbaseMeta.KickSpeed ) self:T( AirbaseID, AirbaseMeta.KickSpeed )
if AirbaseMeta.Monitor == true and Client:IsInZone( AirbaseMeta.ZoneBoundary ) then if AirbaseMeta.Monitor == true and Client:IsInZone( AirbaseMeta.ZoneBoundary ) then
@ -706,7 +724,7 @@ function ATC_GROUND_UNIVERSAL:_AirbaseMonitor()
if IsOnGround then if IsOnGround then
local Taxi = Client:GetState( self, "Taxi" ) local Taxi = Client:GetState( self, "Taxi" )
self:E( Taxi ) self:T( Taxi )
if Taxi == false then if Taxi == false then
local Velocity = VELOCITY:New( AirbaseMeta.KickSpeed or self.KickSpeed ) local Velocity = VELOCITY:New( AirbaseMeta.KickSpeed or self.KickSpeed )
Client:Message( "Welcome to " .. AirbaseID .. ". The maximum taxiing speed is " .. Client:Message( "Welcome to " .. AirbaseID .. ". The maximum taxiing speed is " ..
@ -766,7 +784,7 @@ function ATC_GROUND_UNIVERSAL:_AirbaseMonitor()
Client:SetState( self, "Warnings", SpeedingWarnings + 1 ) Client:SetState( self, "Warnings", SpeedingWarnings + 1 )
else else
MESSAGE:New( "Penalty! Player " .. Client:GetPlayerName() .. " has been kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll() MESSAGE:New( "Penalty! Player " .. Client:GetPlayerName() .. " has been kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll()
--- @param Wrapper.Client#CLIENT Client -- @param Wrapper.Client#CLIENT Client
Client:Destroy() Client:Destroy()
Client:SetState( self, "Speeding", false ) Client:SetState( self, "Speeding", false )
Client:SetState( self, "Warnings", 0 ) Client:SetState( self, "Warnings", 0 )
@ -798,7 +816,7 @@ function ATC_GROUND_UNIVERSAL:_AirbaseMonitor()
Client:SetState( self, "OffRunwayWarnings", OffRunwayWarnings + 1 ) Client:SetState( self, "OffRunwayWarnings", OffRunwayWarnings + 1 )
else else
MESSAGE:New( "Penalty! Player " .. Client:GetPlayerName() .. " has been kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll() MESSAGE:New( "Penalty! Player " .. Client:GetPlayerName() .. " has been kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll()
--- @param Wrapper.Client#CLIENT Client -- @param Wrapper.Client#CLIENT Client
Client:Destroy() Client:Destroy()
Client:SetState( self, "IsOffRunway", false ) Client:SetState( self, "IsOffRunway", false )
Client:SetState( self, "OffRunwayWarnings", 0 ) Client:SetState( self, "OffRunwayWarnings", 0 )
@ -838,15 +856,16 @@ end
--- Start SCHEDULER for ATC_GROUND_UNIVERSAL object. --- Start SCHEDULER for ATC_GROUND_UNIVERSAL object.
-- @param #ATC_GROUND_UNIVERSAL self -- @param #ATC_GROUND_UNIVERSAL self
-- @param RepeatScanSeconds Time in second for defining occurency of alerts. -- @param RepeatScanSeconds Time in second for defining schedule of alerts.
-- @return #ATC_GROUND_UNIVERSAL self -- @return #ATC_GROUND_UNIVERSAL self
function ATC_GROUND_UNIVERSAL:Start( RepeatScanSeconds ) function ATC_GROUND_UNIVERSAL:Start( RepeatScanSeconds )
RepeatScanSeconds = RepeatScanSeconds or 0.05 RepeatScanSeconds = RepeatScanSeconds or 0.05
self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, RepeatScanSeconds ) self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, RepeatScanSeconds )
return self return self
end end
--- @type ATC_GROUND_CAUCASUS ---
-- @type ATC_GROUND_CAUCASUS
-- @extends #ATC_GROUND -- @extends #ATC_GROUND
--- # ATC\_GROUND\_CAUCASUS, extends @{#ATC_GROUND_UNIVERSAL} --- # ATC\_GROUND\_CAUCASUS, extends @{#ATC_GROUND_UNIVERSAL}
@ -981,12 +1000,12 @@ end
-- @return nothing -- @return nothing
function ATC_GROUND_CAUCASUS:Start( RepeatScanSeconds ) function ATC_GROUND_CAUCASUS:Start( RepeatScanSeconds )
RepeatScanSeconds = RepeatScanSeconds or 0.05 RepeatScanSeconds = RepeatScanSeconds or 0.05
self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, RepeatScanSeconds ) self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, RepeatScanSeconds )
end end
---
--- @type ATC_GROUND_NEVADA -- @type ATC_GROUND_NEVADA
-- @extends #ATC_GROUND -- @extends #ATC_GROUND
@ -1120,11 +1139,11 @@ end
-- @return nothing -- @return nothing
function ATC_GROUND_NEVADA:Start( RepeatScanSeconds ) function ATC_GROUND_NEVADA:Start( RepeatScanSeconds )
RepeatScanSeconds = RepeatScanSeconds or 0.05 RepeatScanSeconds = RepeatScanSeconds or 0.05
self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, RepeatScanSeconds ) self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, RepeatScanSeconds )
end end
---
--- @type ATC_GROUND_NORMANDY -- @type ATC_GROUND_NORMANDY
-- @extends #ATC_GROUND -- @extends #ATC_GROUND
@ -1277,10 +1296,11 @@ end
-- @return nothing -- @return nothing
function ATC_GROUND_NORMANDY:Start( RepeatScanSeconds ) function ATC_GROUND_NORMANDY:Start( RepeatScanSeconds )
RepeatScanSeconds = RepeatScanSeconds or 0.05 RepeatScanSeconds = RepeatScanSeconds or 0.05
self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, RepeatScanSeconds ) self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, RepeatScanSeconds )
end end
--- @type ATC_GROUND_PERSIANGULF ---
-- @type ATC_GROUND_PERSIANGULF
-- @extends #ATC_GROUND -- @extends #ATC_GROUND
@ -1419,11 +1439,11 @@ end
-- @return nothing -- @return nothing
function ATC_GROUND_PERSIANGULF:Start( RepeatScanSeconds ) function ATC_GROUND_PERSIANGULF:Start( RepeatScanSeconds )
RepeatScanSeconds = RepeatScanSeconds or 0.05 RepeatScanSeconds = RepeatScanSeconds or 0.05
self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, RepeatScanSeconds ) self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, RepeatScanSeconds )
end end
--- @type ATC_GROUND_MARIANAISLANDS -- @type ATC_GROUND_MARIANAISLANDS
-- @extends #ATC_GROUND -- @extends #ATC_GROUND
@ -1517,7 +1537,7 @@ end
-- * @{#ATC_GROUND.SetMaximumKickSpeedKmph}(): Set the maximum speed allowed at an airbase in kilometers per hour. -- * @{#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. -- * @{#ATC_GROUND.SetMaximumKickSpeedMiph}(): Set the maximum speed allowed at an airbase in miles per hour.
-- --
---- @field #ATC_GROUND_MARIANAISLANDS -- @field #ATC_GROUND_MARIANAISLANDS
ATC_GROUND_MARIANAISLANDS = { ATC_GROUND_MARIANAISLANDS = {
ClassName = "ATC_GROUND_MARIANAISLANDS", ClassName = "ATC_GROUND_MARIANAISLANDS",
} }
@ -1529,7 +1549,7 @@ ATC_GROUND_MARIANAISLANDS = {
function ATC_GROUND_MARIANAISLANDS:New( AirbaseNames ) function ATC_GROUND_MARIANAISLANDS:New( AirbaseNames )
-- Inherits from BASE -- Inherits from BASE
local self = BASE:Inherit( self, ATC_GROUND_UNIVERSAL:New( self.Airbases, AirbaseNames ) ) local self = BASE:Inherit( self, ATC_GROUND_UNIVERSAL:New( AirbaseNames ) )
self:SetKickSpeedKmph( 50 ) self:SetKickSpeedKmph( 50 )
self:SetMaximumKickSpeedKmph( 150 ) self:SetMaximumKickSpeedKmph( 150 )
@ -1543,5 +1563,5 @@ end
-- @return nothing -- @return nothing
function ATC_GROUND_MARIANAISLANDS:Start( RepeatScanSeconds ) function ATC_GROUND_MARIANAISLANDS:Start( RepeatScanSeconds )
RepeatScanSeconds = RepeatScanSeconds or 0.05 RepeatScanSeconds = RepeatScanSeconds or 0.05
self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, RepeatScanSeconds ) self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, RepeatScanSeconds )
end end

View File

@ -1579,7 +1579,7 @@ function PLAYERTASKCONTROLLER:New(Name, Coalition, Type, ClientFilter)
self.ClusterRadius = 0.5 self.ClusterRadius = 0.5
self.TargetRadius = 500 self.TargetRadius = 500
self.ClientFilter = ClientFilter or "" self.ClientFilter = ClientFilter --or ""
self.TargetQueue = FIFO:New() -- Utilities.FiFo#FIFO self.TargetQueue = FIFO:New() -- Utilities.FiFo#FIFO
self.TaskQueue = FIFO:New() -- Utilities.FiFo#FIFO self.TaskQueue = FIFO:New() -- Utilities.FiFo#FIFO

View File

@ -50,6 +50,7 @@
-- @field #string ConfigFileName Name of the standard config file -- @field #string ConfigFileName Name of the standard config file
-- @field #string ConfigFilePath Path to the standard config file -- @field #string ConfigFilePath Path to the standard config file
-- @field #boolean ConfigLoaded -- @field #boolean ConfigLoaded
-- @field #string ttsprovider Default provider TTS backend, e.g. "Google" or "Microsoft", default is Microsoft
-- @extends Core.Base#BASE -- @extends Core.Base#BASE
--- *It is a very sad thing that nowadays there is so little useless information.* - Oscar Wilde --- *It is a very sad thing that nowadays there is so little useless information.* - Oscar Wilde
@ -127,6 +128,10 @@
-- --
-- Use @{#MSRS.SetVolume} to define the SRS volume. Defaults to 1.0. Allowed values are between 0.0 and 1.0, from silent to loudest. -- Use @{#MSRS.SetVolume} to define the SRS volume. Defaults to 1.0. Allowed values are between 0.0 and 1.0, from silent to loudest.
-- --
-- ## Config file for many variables, auto-loaded by Moose
--
-- See @{#MSRS.LoadConfigFile} for details on how to set this up.
--
-- ## Set DCS-gRPC as an alternative to 'DCS-SR-ExternalAudio.exe' for TTS -- ## Set DCS-gRPC as an alternative to 'DCS-SR-ExternalAudio.exe' for TTS
-- --
-- Use @{#MSRS.SetDefaultBackendGRPC} to enable [DCS-gRPC](https://github.com/DCS-gRPC/rust-server) as an alternate backend for transmitting text-to-speech over SRS. -- Use @{#MSRS.SetDefaultBackendGRPC} to enable [DCS-gRPC](https://github.com/DCS-gRPC/rust-server) as an alternate backend for transmitting text-to-speech over SRS.
@ -191,11 +196,12 @@ MSRS = {
ConfigFileName = "Moose_MSRS.lua", ConfigFileName = "Moose_MSRS.lua",
ConfigFilePath = "Config\\", ConfigFilePath = "Config\\",
ConfigLoaded = false, ConfigLoaded = false,
ttsprovider = "Microsoft",
} }
--- MSRS class version. --- MSRS class version.
-- @field #string version -- @field #string version
MSRS.version="0.1.2" MSRS.version="0.1.3"
--- Voices --- Voices
-- @type MSRS.Voices -- @type MSRS.Voices
@ -377,9 +383,7 @@ function MSRS:New(PathToSRS, Frequency, Modulation, Volume, AltBackend)
return self:_NewAltBackend(Backend) return self:_NewAltBackend(Backend)
end end
local success = self:LoadConfigFile(nil,nil,self.ConfigLoaded) if not self.ConfigLoaded then
if (not success) and (not self.ConfigLoaded) then
-- If no AltBackend table, the proceed with default initialisation -- If no AltBackend table, the proceed with default initialisation
self:SetPath(PathToSRS) self:SetPath(PathToSRS)
@ -446,7 +450,7 @@ function MSRS:SetPath(Path)
end end
-- Debug output. -- Debug output.
self:I(string.format("SRS path=%s", self:GetPath())) self:T(string.format("SRS path=%s", self:GetPath()))
end end
return self return self
end end
@ -674,7 +678,7 @@ function MSRS:SetCoordinate(Coordinate)
return self return self
end end
--- Use google text-to-speech. --- Use google text-to-speech credentials. Also sets Google as default TTS provider.
-- @param #MSRS self -- @param #MSRS self
-- @param #string PathToCredentials Full path to the google credentials JSON file, e.g. "C:\Users\username\Downloads\service-account-file.json". Can also be the Google API key. -- @param #string PathToCredentials Full path to the google credentials JSON file, e.g. "C:\Users\username\Downloads\service-account-file.json". Can also be the Google API key.
-- @return #MSRS self -- @return #MSRS self
@ -688,13 +692,14 @@ function MSRS:SetGoogle(PathToCredentials)
self.GRPCOptions.DefaultProvider = "gcloud" self.GRPCOptions.DefaultProvider = "gcloud"
self.GRPCOptions.gcloud.key = PathToCredentials self.GRPCOptions.gcloud.key = PathToCredentials
self.ttsprovider = "Google"
end end
return self return self
end end
--- Use google text-to-speech. --- gRPC Backend: Use google text-to-speech set the API key.
-- @param #MSRS self -- @param #MSRS self
-- @param #string APIKey API Key, usually a string of length 40 with characters and numbers. -- @param #string APIKey API Key, usually a string of length 40 with characters and numbers.
-- @return #MSRS self -- @return #MSRS self
@ -708,6 +713,22 @@ function MSRS:SetGoogleAPIKey(APIKey)
return self return self
end end
--- Use Google text-to-speech as default.
-- @param #MSRS self
-- @return #MSRS self
function MSRS:SetTTSProviderGoogle()
self.ttsprovider = "Google"
return self
end
--- Use Microsoft text-to-speech as default.
-- @param #MSRS self
-- @return #MSRS self
function MSRS:SetTTSProviderMicrosoft()
self.ttsprovider = "Microsoft"
return self
end
--- Print SRS STTS help to DCS log file. --- Print SRS STTS help to DCS log file.
-- @param #MSRS self -- @param #MSRS self
-- @return #MSRS self -- @return #MSRS self
@ -1114,7 +1135,7 @@ function MSRS:_GetCommand(freqs, modus, coal, gender, voice, culture, volume, sp
end end
-- Set google. -- Set google.
if self.google then if self.google and self.ttsprovider == "Google" then
command=command..string.format(' --ssml -G "%s"', self.google) command=command..string.format(' --ssml -G "%s"', self.google)
end end
@ -1128,7 +1149,6 @@ end
-- @param #MSRS self -- @param #MSRS self
-- @param #string Path Path to config file, defaults to "C:\Users\<yourname>\Saved Games\DCS\Config" -- @param #string Path Path to config file, defaults to "C:\Users\<yourname>\Saved Games\DCS\Config"
-- @param #string Filename File to load, defaults to "Moose_MSRS.lua" -- @param #string Filename File to load, defaults to "Moose_MSRS.lua"
-- @param #boolean ConfigLoaded - if true, skip the loading
-- @return #boolean success -- @return #boolean success
-- @usage -- @usage
-- 0) Benefits: Centralize configuration of SRS, keep paths and keys out of the mission source code, making it safer and easier to move missions to/between servers, -- 0) Benefits: Centralize configuration of SRS, keep paths and keys out of the mission source code, making it safer and easier to move missions to/between servers,
@ -1138,18 +1158,19 @@ end
-- --
-- -- Moose MSRS default Config -- -- Moose MSRS default Config
-- MSRS_Config = { -- MSRS_Config = {
-- Path = "C:\\Program Files\\DCS-SimpleRadio-Standalone", -- adjust as needed -- Path = "C:\\Program Files\\DCS-SimpleRadio-Standalone", -- adjust as needed, note double \\
-- Port = 5002, -- adjust as needed -- Port = 5002, -- adjust as needed
-- Frequency = {127,243}, -- must be a table, 1..n entries! -- Frequency = {127,243}, -- must be a table, 1..n entries!
-- Modulation = {0,0}, -- must be a table, 1..n entries, one for each frequency! -- Modulation = {0,0}, -- must be a table, 1..n entries, one for each frequency!
-- Volume = 1.0, -- Volume = 1.0, -- 0.0 to 1.0
-- Coalition = 0, -- 0 = Neutral, 1 = Red, 2 = Blue -- Coalition = 0, -- 0 = Neutral, 1 = Red, 2 = Blue
-- Coordinate = {0,0,0}, -- x,y,alt - optional -- Coordinate = {0,0,0}, -- x,y,altitude - optional, all in meters
-- Culture = "en-GB", -- Culture = "en-GB",
-- Gender = "male", -- Gender = "male",
-- Google = "C:\\Program Files\\DCS-SimpleRadio-Standalone\\yourfilename.json", -- path to google json key file - optional -- Google = "C:\\Program Files\\DCS-SimpleRadio-Standalone\\yourfilename.json", -- path to google json key file - optional.
-- Label = "MSRS", -- Label = "MSRS",
-- Voice = "Microsoft Hazel Desktop", -- Voice = "Microsoft Hazel Desktop",
-- Provider = "Microsoft", -- this is the default TTS provider, e.g. "Google" or "Microsoft"
-- -- gRPC (optional) -- -- gRPC (optional)
-- GRPC = { -- see https://github.com/DCS-gRPC/rust-server -- GRPC = { -- see https://github.com/DCS-gRPC/rust-server
-- coalition = "blue", -- blue, red, neutral -- coalition = "blue", -- blue, red, neutral
@ -1166,14 +1187,18 @@ end
-- } -- }
-- } -- }
-- --
-- 3) Load the config into the MSRS raw class before you do anything else: -- 3) The config file is automatically loaded when Moose starts. YOu can also load the config into the MSRS raw class manually before you do anything else:
-- --
-- MSRS.LoadConfigFile() -- Note the "." here -- MSRS.LoadConfigFile() -- Note the "." here
-- --
-- Optionally, your might want to provide a specific path and filename:
--
-- MSRS.LoadConfigFile(nil,MyPath,MyFilename) -- Note the "." here
--
-- This will populate variables for the MSRS raw class and all instances you create with e.g. `mysrs = MSRS:New()` -- This will populate variables for the MSRS raw class and all instances you create with e.g. `mysrs = MSRS:New()`
-- Optionally you can also load this per **single instance** if so needed, i.e. -- Optionally you can also load this per **single instance** if so needed, i.e.
-- --
-- mysrs:LoadConfig(Path,Filename) -- mysrs:LoadConfigFile(Path,Filename)
-- --
-- 4) Use the config in your code like so, variable names are basically the same as in the config file, but all lower case, examples: -- 4) Use the config in your code like so, variable names are basically the same as in the config file, but all lower case, examples:
-- --
@ -1190,46 +1215,17 @@ end
-- atis:SetSRS(nil,nil,nil,MSRS.Voices.Google.Standard.en_US_Standard_H) -- atis:SetSRS(nil,nil,nil,MSRS.Voices.Google.Standard.en_US_Standard_H)
-- --Start ATIS -- --Start ATIS
-- atis:Start() -- atis:Start()
function MSRS:LoadConfigFile(Path,Filename,ConfigLoaded) function MSRS:LoadConfigFile(Path,Filename)
local path = Path or lfs.writedir()..MSRS.ConfigFilePath local path = Path or lfs.writedir()..MSRS.ConfigFilePath
local file = Filename or MSRS.ConfigFileName or "Moose_MSRS.lua" local file = Filename or MSRS.ConfigFileName or "Moose_MSRS.lua"
local pathandfile = path..file
local filexsists = UTILS.FileExists(pathandfile)
if UTILS.CheckFileExists(path,file) and not ConfigLoaded then if filexsists and not MSRS.ConfigLoaded then
assert(loadfile(path..file))() assert(loadfile(path..file))()
-- now we should have a global var MSRS_Config -- now we should have a global var MSRS_Config
if MSRS_Config then if MSRS_Config then
--[[
-- Moose MSRS default Config
MSRS_Config = {
Path = "C:\\Program Files\\DCS-SimpleRadio-Standalone", -- adjust as needed
Port = 5002, -- adjust as needed
Frequency = {127,243}, -- must be a table, 1..n entries!
Modulation = {0,0}, -- must be a table, 1..n entries, one for each frequency!
Volume = 1.0,
Coalition = 0, -- 0 = Neutral, 1 = Red, 2 = Blue
Coordinate = {0,0,0}, -- x,y,alt - optional
Culture = "en-GB",
Gender = "male",
Google = "C:\\Program Files\\DCS-SimpleRadio-Standalone\\yourfilename.json", -- path to google json key file - optional
Label = "MSRS",
Voice = "Microsoft Hazel Desktop",
-- gRPC (optional)
GRPC = { -- see https://github.com/DCS-gRPC/rust-server
coalition = "blue", -- blue, red, neutral
DefaultProvider = "gcloud", -- win, gcloud, aws, or azure, some of the values below depend on your cloud provider
gcloud = {
key = "<API Google Key>", -- for gRPC Google API key
--secret = "", -- needed for aws
--region = "",-- needed for aws
defaultVoice = MSRS.Voices.Google.Standard.en_GB_Standard_F,
},
win = {
defaultVoice = "Hazel",
},
}
}
--]]
if self then if self then
self.path = MSRS_Config.Path or "C:\\Program Files\\DCS-SimpleRadio-Standalone" self.path = MSRS_Config.Path or "C:\\Program Files\\DCS-SimpleRadio-Standalone"
self.port = MSRS_Config.Port or 5002 self.port = MSRS_Config.Port or 5002
@ -1242,6 +1238,9 @@ function MSRS:LoadConfigFile(Path,Filename,ConfigLoaded)
self.culture = MSRS_Config.Culture or "en-GB" self.culture = MSRS_Config.Culture or "en-GB"
self.gender = MSRS_Config.Gender or "male" self.gender = MSRS_Config.Gender or "male"
self.google = MSRS_Config.Google self.google = MSRS_Config.Google
if MSRS_Config.Provider then
self.ttsprovider = MSRS_Config.Provider
end
self.Label = MSRS_Config.Label or "MSRS" self.Label = MSRS_Config.Label or "MSRS"
self.voice = MSRS_Config.Voice --or MSRS.Voices.Microsoft.Hazel self.voice = MSRS_Config.Voice --or MSRS.Voices.Microsoft.Hazel
if MSRS_Config.GRPC then if MSRS_Config.GRPC then
@ -1266,6 +1265,9 @@ function MSRS:LoadConfigFile(Path,Filename,ConfigLoaded)
MSRS.culture = MSRS_Config.Culture or "en-GB" MSRS.culture = MSRS_Config.Culture or "en-GB"
MSRS.gender = MSRS_Config.Gender or "male" MSRS.gender = MSRS_Config.Gender or "male"
MSRS.google = MSRS_Config.Google MSRS.google = MSRS_Config.Google
if MSRS_Config.Provider then
MSRS.ttsprovider = MSRS_Config.Provider
end
MSRS.Label = MSRS_Config.Label or "MSRS" MSRS.Label = MSRS_Config.Label or "MSRS"
MSRS.voice = MSRS_Config.Voice --or MSRS.Voices.Microsoft.Hazel MSRS.voice = MSRS_Config.Voice --or MSRS.Voices.Microsoft.Hazel
if MSRS_Config.GRPC then if MSRS_Config.GRPC then
@ -1280,9 +1282,10 @@ function MSRS:LoadConfigFile(Path,Filename,ConfigLoaded)
MSRS.ConfigLoaded = true MSRS.ConfigLoaded = true
end end
end end
env.info("MSRS - Sucessfully loaded default configuration from disk!",false) env.info("MSRS - Successfully loaded default configuration from disk!",false)
else end
env.info("MSRS - Cannot load default configuration from disk!",false) if not filexsists then
env.info("MSRS - Cannot find default configuration file!",false)
return false return false
end end
@ -1995,6 +1998,7 @@ function MSRSQUEUE:_CheckRadioQueue(delay)
end end
MSRS.LoadConfigFile()
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

View File

@ -1345,6 +1345,11 @@ function UTILS.VecSubstract(a, b)
return {x=a.x-b.x, y=a.y-b.y, z=a.z-b.z} return {x=a.x-b.x, y=a.y-b.y, z=a.z-b.z}
end end
--- Substract is not a word, don't want to rename the original function because it's been around since forever
function UTILS.VecSubtract(a, b)
return UTILS.VecSubstract(a, b)
end
--- Calculate the difference between two 2D vectors by substracting the x,y components from each other. --- Calculate the difference between two 2D vectors by substracting the x,y components from each other.
-- @param DCS#Vec2 a Vector in 2D with x, y components. -- @param DCS#Vec2 a Vector in 2D with x, y components.
-- @param DCS#Vec2 b Vector in 2D with x, y components. -- @param DCS#Vec2 b Vector in 2D with x, y components.
@ -1353,6 +1358,11 @@ function UTILS.Vec2Substract(a, b)
return {x=a.x-b.x, y=a.y-b.y} return {x=a.x-b.x, y=a.y-b.y}
end end
--- Substract is not a word, don't want to rename the original function because it's been around since forever
function UTILS.Vec2Subtract(a, b)
return UTILS.Vec2Substract(a, b)
end
--- Calculate the total vector of two 3D vectors by adding the x,y,z components of each other. --- Calculate the total vector of two 3D vectors by adding the x,y,z components of each other.
-- @param DCS#Vec3 a Vector in 3D with x, y, z components. -- @param DCS#Vec3 a Vector in 3D with x, y, z components.
-- @param DCS#Vec3 b Vector in 3D with x, y, z components. -- @param DCS#Vec3 b Vector in 3D with x, y, z components.
@ -2391,7 +2401,7 @@ function UTILS.LoadFromFile(Path,Filename)
-- Check if file exists. -- Check if file exists.
local exists=UTILS.CheckFileExists(Path,Filename) local exists=UTILS.CheckFileExists(Path,Filename)
if not exists then if not exists then
BASE:E(string.format("ERROR: File %s does not exist!",filename)) BASE:I(string.format("ERROR: File %s does not exist!",filename))
return false return false
end end
@ -3101,3 +3111,491 @@ function UTILS.PlotRacetrack(Coordinate, Altitude, Speed, Heading, Leg, Coalitio
circle_center_two_three:CircleToAll(UTILS.NMToMeters(turn_radius), coalition, color, alpha, nil, 0, lineType)--, ReadOnly, Text) circle_center_two_three:CircleToAll(UTILS.NMToMeters(turn_radius), coalition, color, alpha, nil, 0, lineType)--, ReadOnly, Text)
end end
--- Get the current time in a "nice" format like 21:01:15
-- @return #string Returns string with the current time
function UTILS.TimeNow()
return UTILS.SecondsToClock(timer.getAbsTime(), false, false)
end
--- Given 2 "nice" time string, returns the difference between the two in seconds
-- @param #string start_time Time string like "07:15:22"
-- @param #string end_time Time string like "08:11:27"
-- @return #number Seconds between start_time and end_time
function UTILS.TimeDifferenceInSeconds(start_time, end_time)
return UTILS.ClockToSeconds(end_time) - UTILS.ClockToSeconds(start_time)
end
--- Check if the current time is later than time_string.
-- @param #string start_time Time string like "07:15:22"
-- @return #boolean True if later, False if before
function UTILS.TimeLaterThan(time_string)
if timer.getAbsTime() > UTILS.ClockToSeconds(time_string) then
return true
end
return false
end
--- Check if the current time is before time_string.
-- @param #string start_time Time string like "07:15:22"
-- @return #boolean False if later, True if before
function UTILS.TimeBefore(time_string)
if timer.getAbsTime() < UTILS.ClockToSeconds(time_string) then
return true
end
return false
end
--- Combines two time strings to give you a new time. For example "15:16:32" and "02:06:24" would return "17:22:56"
-- @param #string time_string_01 Time string like "07:15:22"
-- @param #string time_string_02 Time string like "08:11:27"
-- @return #string Result of the two time string combined
function UTILS.CombineTimeStrings(time_string_01, time_string_02)
local hours1, minutes1, seconds1 = time_string_01:match("(%d+):(%d+):(%d+)")
local hours2, minutes2, seconds2 = time_string_02:match("(%d+):(%d+):(%d+)")
local total_seconds = tonumber(seconds1) + tonumber(seconds2) + tonumber(minutes1) * 60 + tonumber(minutes2) * 60 + tonumber(hours1) * 3600 + tonumber(hours2) * 3600
total_seconds = total_seconds % (24 * 3600)
if total_seconds < 0 then
total_seconds = total_seconds + 24 * 3600
end
local hours = math.floor(total_seconds / 3600)
total_seconds = total_seconds - hours * 3600
local minutes = math.floor(total_seconds / 60)
local seconds = total_seconds % 60
return string.format("%02d:%02d:%02d", hours, minutes, seconds)
end
--- Subtracts two time string to give you a new time. For example "15:16:32" and "02:06:24" would return "13:10:08"
-- @param #string time_string_01 Time string like "07:15:22"
-- @param #string time_string_02 Time string like "08:11:27"
-- @return #string Result of the two time string subtracted
function UTILS.SubtractTimeStrings(time_string_01, time_string_02)
local hours1, minutes1, seconds1 = time_string_01:match("(%d+):(%d+):(%d+)")
local hours2, minutes2, seconds2 = time_string_02:match("(%d+):(%d+):(%d+)")
local total_seconds = tonumber(seconds1) - tonumber(seconds2) + tonumber(minutes1) * 60 - tonumber(minutes2) * 60 + tonumber(hours1) * 3600 - tonumber(hours2) * 3600
total_seconds = total_seconds % (24 * 3600)
if total_seconds < 0 then
total_seconds = total_seconds + 24 * 3600
end
local hours = math.floor(total_seconds / 3600)
total_seconds = total_seconds - hours * 3600
local minutes = math.floor(total_seconds / 60)
local seconds = total_seconds % 60
return string.format("%02d:%02d:%02d", hours, minutes, seconds)
end
--- Checks if the current time is in between start_time and end_time
-- @param #string time_string_01 Time string like "07:15:22"
-- @param #string time_string_02 Time string like "08:11:27"
-- @return #bool True if it is, False if it's not
function UTILS.TimeBetween(start_time, end_time)
return UTILS.TimeLaterThan(start_time) and UTILS.TimeBefore(end_time)
end
--- Easy to read one line to roll the dice on something. 1% is very unlikely to happen, 99% is very likely to happen
-- @param #number chance (optional) Percentage chance you want something to happen. Defaults to a random number if not given
-- @return #bool True if the dice roll was within the given percentage chance of happening
function UTILS.PercentageChance(chance)
chance = chance or math.random(0, 100)
chance = UTILS.Clamp(chance, 0, 100)
local percentage = math.random(0, 100)
if percentage < chance then
return true
end
return false
end
--- Easy to read one liner to clamp a value
-- @param #number value Input value
-- @param #number min Minimal value that should be respected
-- @param #number max Maximal value that should be respected
-- @return #number Clamped value
function UTILS.Clamp(value, min, max)
if value < min then value = min end
if value > max then value = max end
return value
end
--- Clamp an angle so that it's always between 0 and 360 while still being correct
-- @param #number value Input value
-- @return #number Clamped value
function UTILS.ClampAngle(value)
if value > 360 then return value - 360 end
if value < 0 then return value + 360 end
return value
end
--- Remap an input to a new value in a given range. For example:
--- UTILS.RemapValue(20, 10, 30, 0, 200) would return 100
--- 20 is 50% between 10 and 30
--- 50% between 0 and 200 is 100
-- @param #number value Input value
-- @param #number old_min Min value to remap from
-- @param #number old_max Max value to remap from
-- @param #number new_min Min value to remap to
-- @param #number new_max Max value to remap to
-- @return #number Remapped value
function UTILS.RemapValue(value, old_min, old_max, new_min, new_max)
new_min = new_min or 0
new_max = new_max or 100
local old_range = old_max - old_min
local new_range = new_max - new_min
local percentage = (value - old_min) / old_range
return (new_range * percentage) + new_min
end
--- Given a triangle made out of 3 vector 2s, return a vec2 that is a random number in this triangle
-- @param #Vec2 pt1 Min value to remap from
-- @param #Vec2 pt2 Max value to remap from
-- @param #Vec2 pt3 Max value to remap from
-- @return #Vec2 Random point in triangle
function UTILS.RandomPointInTriangle(pt1, pt2, pt3)
local pt = {math.random(), math.random()}
table.sort(pt)
local s = pt[1]
local t = pt[2] - pt[1]
local u = 1 - pt[2]
return {x = s * pt1.x + t * pt2.x + u * pt3.x,
y = s * pt1.y + t * pt2.y + u * pt3.y}
end
--- Checks if a given angle (heading) is between 2 other angles. Min and max have to be given in clockwise order For example:
--- UTILS.AngleBetween(350, 270, 15) would return True
--- UTILS.AngleBetween(22, 95, 20) would return False
-- @param #number angle Min value to remap from
-- @param #number min Max value to remap from
-- @param #number max Max value to remap from
-- @return #bool
function UTILS.AngleBetween(angle, min, max)
angle = (360 + (angle % 360)) % 360
min = (360 + min % 360) % 360
max = (360 + max % 360) % 360
if min < max then return min <= angle and angle <= max end
return min <= angle or angle <= max
end
--- Easy to read one liner to write a JSON file. Everything in @data should be serializable
--- json.lua exists in the DCS install Scripts folder
-- @param #table data table to write
-- @param #string file_path File path
function UTILS.WriteJSON(data, file_path)
package.path = package.path .. ";.\\Scripts\\?.lua"
local JSON = require("json")
local pretty_json_text = JSON:encode_pretty(data)
local write_file = io.open(file_path, "w")
write_file:write(pretty_json_text)
write_file:close()
end
--- Easy to read one liner to read a JSON file.
--- json.lua exists in the DCS install Scripts folder
-- @param #string file_path File path
-- @return #table
function UTILS.ReadJSON(file_path)
package.path = package.path .. ";.\\Scripts\\?.lua"
local JSON = require("json")
local read_file = io.open(file_path, "r")
local contents = read_file:read( "*a" )
io.close(read_file)
return JSON:decode(contents)
end
--- Get the properties names and values of properties set up on a Zone in the Mission Editor.
--- This doesn't work for any zones created in MOOSE
-- @param #string zone_name Name of the zone as set up in the Mission Editor
-- @return #table with all the properties on a zone
function UTILS.GetZoneProperties(zone_name)
local return_table = {}
for _, zone in pairs(env.mission.triggers.zones) do
if zone["name"] == zone_name then
if table.length(zone["properties"]) > 0 then
for _, property in pairs(zone["properties"]) do
return_table[property["key"]] = property["value"]
end
return return_table
else
BASE:I(string.format("%s doesn't have any properties", zone_name))
return {}
end
end
end
end
--- Rotates a point around another point with a given angle. Useful if you're loading in groups or
--- statics but you want to rotate them all as a collection. You can get the center point of everything
--- and then rotate all the positions of every object around this center point.
-- @param #Vec2 point Point that you want to rotate
-- @param #Vec2 pivot Pivot point of the rotation
-- @param #number angle How many degrees the point should be rotated
-- @return #Vec Rotated point
function UTILS.RotatePointAroundPivot(point, pivot, angle)
local radians = math.rad(angle)
local x = point.x - pivot.x
local y = point.y - pivot.y
local rotated_x = x * math.cos(radians) - y * math.sin(radians)
local rotatex_y = x * math.sin(radians) + y * math.cos(radians)
local original_x = rotated_x + pivot.x
local original_y = rotatex_y + pivot.y
return { x = original_x, y = original_y }
end
--- Makes a string semi-unique by attaching a random number between 0 and 1 million to it
-- @param #string base String you want to unique-fy
-- @return #string Unique string
function UTILS.UniqueName(base)
base = base or ""
local ran = tostring(math.random(0, 1000000))
if base == "" then
return ran
end
return base .. "_" .. ran
end
--- Check if a string starts with something
-- @param #string str String to check
-- @param #string value
-- @return #bool True if str starts with value
function string.startswith(str, value)
return string.sub(str,1,string.len(value)) == value
end
--- Check if a string ends with something
-- @param #string str String to check
-- @param #string value
-- @return #bool True if str ends with value
function string.endswith(str, value)
return value == "" or str:sub(-#value) == value
end
--- Splits a string on a separator. For example:
--- string.split("hello_dcs_world", "-") would return {"hello", "dcs", "world"}
-- @param #string input String to split
-- @param #string separator What to split on
-- @return #table individual strings
function string.split(input, separator)
local parts = {}
for part in input:gmatch("[^" .. separator .. "]+") do
table.insert(parts, part)
end
return parts
end
--- Checks if a string contains a substring. Easier to remember for Python people :)
--- string.split("hello_dcs_world", "-") would return {"hello", "dcs", "world"}
-- @param #string str
-- @param #string value
-- @return #bool True if str contains value
function string.contains(str, value)
return string.match(str, value)
end
--- Given tbl is a indexed table ({"hello", "dcs", "world"}), checks if element exists in the table.
--- The table can be made up out of complex tables or values as well
-- @param #table tbl
-- @param #string element
-- @return #bool True if tbl contains element
function table.contains(tbl, element)
if element == nil or tbl == nil then return false end
local index = 1
while tbl[index] do
if tbl[index] == element then
return true
end
index = index + 1
end
return false
end
--- Checks if a table contains a specific key.
-- @param #table tbl Table to check
-- @param #string key Key to look for
-- @return #bool True if tbl contains key
function table.contains_key(tbl, key)
if tbl[key] ~= nil then return true else return false end
end
--- Inserts a unique element into a table.
-- @param #table tbl Table to insert into
-- @param #string element Element to insert
function table.insert_unique(tbl, element)
if element == nil or tbl == nil then return end
if not table.contains(tbl, element) then
table.insert(tbl, element)
end
end
--- Removes an element from a table by its value.
-- @param #table tbl Table to remove from
-- @param #string element Element to remove
function table.remove_by_value(tbl, element)
local indices_to_remove = {}
local index = 1
for _, value in pairs(tbl) do
if value == element then
table.insert(indices_to_remove, index)
end
index = index + 1
end
for _, idx in pairs(indices_to_remove) do
table.remove(tbl, idx)
end
end
--- Removes an element from a table by its key.
-- @param #table table Table to remove from
-- @param #string key Key of the element to remove
-- @return #string Removed element
function table.remove_key(table, key)
local element = table[key]
table[key] = nil
return element
end
--- Finds the index of an element in a table.
-- @param #table table Table to search
-- @param #string element Element to find
-- @return #int Index of the element, or nil if not found
function table.index_of(table, element)
for i, v in ipairs(table) do
if v == element then
return i
end
end
return nil
end
--- Counts the number of elements in a table.
-- @param #table T Table to count
-- @return #int Number of elements in the table
function table.length(T)
local count = 0
for _ in pairs(T) do count = count + 1 end
return count
end
--- Slices a table between two indices, much like Python's my_list[2:-1]
-- @param #table tbl Table to slice
-- @param #int first Starting index
-- @param #int last Ending index
-- @return #table Sliced table
function table.slice(tbl, first, last)
local sliced = {}
local start = first or 1
local stop = last or table.length(tbl)
local count = 1
for key, value in pairs(tbl) do
if count >= start and count <= stop then
sliced[key] = value
end
count = count + 1
end
return sliced
end
--- Counts the number of occurrences of a value in a table.
-- @param #table tbl Table to search
-- @param #string value Value to count
-- @return #int Number of occurrences of the value
function table.count_value(tbl, value)
local count = 0
for _, item in pairs(tbl) do
if item == value then count = count + 1 end
end
return count
end
--- Add 2 table together, t2 gets added to t1
-- @param #table t1 First table
-- @param #table t2 Second table
-- @return #table Combined table
function table.combine(t1, t2)
if t1 == nil and t2 == nil then
BASE:E("Both tables were empty!")
end
if t1 == nil then return t2 end
if t2 == nil then return t1 end
for i=1,#t2 do
t1[#t1+1] = t2[i]
end
return t1
end
--- Merges two tables into one. If a key exists in both t1 and t2, the value of t1 with be overwritten by the value of t2
-- @param #table t1 First table
-- @param #table t2 Second table
-- @return #table Merged table
function table.merge(t1, t2)
for k, v in pairs(t2) do
if (type(v) == "table") and (type(t1[k] or false) == "table") then
table.merge(t1[k], t2[k])
else
t1[k] = v
end
end
return t1
end
--- Adds an item to the end of a table.
-- @param #table tbl Table to add to
-- @param #string item Item to add
function table.add(tbl, item)
tbl[#tbl + 1] = item
end
--- Shuffles the elements of a table.
-- @param #table tbl Table to shuffle
-- @return #table Shuffled table
function table.shuffle(tbl)
local new_table = {}
for _, value in ipairs(tbl) do
local pos = math.random(1, #new_table +1)
table.insert(new_table, pos, value)
end
return new_table
end
--- Finds a key-value pair in a table.
-- @param #table tbl Table to search
-- @param #string key Key to find
-- @param #string value Value to find
-- @return #table Table containing the key-value pair, or nil if not found
function table.find_key_value_pair(tbl, key, value)
for k, v in pairs(tbl) do
if type(v) == "table" then
local result = table.find_key_value_pair(v, key, value)
if result ~= nil then
return result
end
elseif k == key and v == value then
return tbl
end
end
return nil
end

View File

@ -224,6 +224,14 @@ function WEAPON:New(WeaponObject)
-- Set log ID. -- Set log ID.
self.lid=string.format("[%s] %s | ", self.typeName, self.name) self.lid=string.format("[%s] %s | ", self.typeName, self.name)
if self.launcherUnit then
self.releaseHeading = self.launcherUnit:GetHeading()
self.releaseAltitudeASL = self.launcherUnit:GetAltitude()
self.releaseAltitudeAGL = self.launcherUnit:GetAltitude(true)
self.releaseCoordinate = self.launcherUnit:GetCoordinate()
self.releasePitch = self.launcherUnit:GetPitch()
end
-- Set default parameters -- Set default parameters
self:SetTimeStepTrack() self:SetTimeStepTrack()
self:SetDistanceInterceptPoint() self:SetDistanceInterceptPoint()
@ -552,6 +560,52 @@ function WEAPON:GetImpactCoordinate()
return self.impactCoord return self.impactCoord
end end
--- Get the heading on which the weapon was released
-- @param #WEAPON self
-- @param #bool AccountForMagneticInclination (Optional) If true will account for the magnetic declination of the current map. Default is true
-- @return #number Heading
function WEAPON:GetReleaseHeading(AccountForMagneticInclination)
AccountForMagneticInclination = AccountForMagneticInclination or true
if AccountForMagneticInclination then return UTILS.ClampAngle(self.releaseHeading - UTILS.GetMagneticDeclination()) else return UTILS.ClampAngle(self.releaseHeading) end
end
--- Get the altitude above sea level at which the weapon was released
-- @param #WEAPON self
-- @return #number Altitude in meters
function WEAPON:GetReleaseAltitudeASL()
return self.releaseAltitudeASL
end
--- Get the altitude above ground level at which the weapon was released
-- @param #WEAPON self
-- @return #number Altitude in meters
function WEAPON:GetReleaseAltitudeAGL()
return self.releaseAltitudeAGL
end
--- Get the coordinate where the weapon was released
-- @param #WEAPON self
-- @return Core.Point#COORDINATE Impact coordinate (if any).
function WEAPON:GetReleaseCoordinate()
return self.releaseCoordinate
end
--- Get the pitch of the unit when the weapon was released
-- @param #WEAPON self
-- @return #number Degrees
function WEAPON:GetReleasePitch()
return self.releasePitch
end
--- Get the heading of the weapon when it impacted. Note that this might not exist if the weapon has not impacted yet!
-- @param #WEAPON self
-- @param #bool AccountForMagneticInclination (Optional) If true will account for the magnetic declination of the current map. Default is true
-- @return #number Heading
function WEAPON:GetImpactHeading(AccountForMagneticInclination)
AccountForMagneticInclination = AccountForMagneticInclination or true
if AccountForMagneticInclination then return UTILS.ClampAngle(self.impactHeading - UTILS.GetMagneticDeclination()) else return self.impactHeading end
end
--- Check if weapon is in the air. Obviously not really useful for torpedos. Well, then again, this is DCS... --- Check if weapon is in the air. Obviously not really useful for torpedos. Well, then again, this is DCS...
-- @param #WEAPON self -- @param #WEAPON self
-- @return #boolean If `true`, weapon is in the air and `false` if not. Returns `nil` if weapon object itself is `nil`. -- @return #boolean If `true`, weapon is in the air and `false` if not. Returns `nil` if weapon object itself is `nil`.
@ -713,6 +767,9 @@ function WEAPON:_TrackWeapon(time)
-- Update coordinate. -- Update coordinate.
self.coordinate:UpdateFromVec3(self.vec3) self.coordinate:UpdateFromVec3(self.vec3)
-- Safe the last velocity of the weapon. This is needed to get the impact heading
self.last_velocity = self.weapon:getVelocity()
-- Keep on tracking by returning the next time below. -- Keep on tracking by returning the next time below.
self.tracking=true self.tracking=true
@ -782,6 +839,9 @@ function WEAPON:_TrackWeapon(time)
-- Safe impact coordinate. -- Safe impact coordinate.
self.impactCoord=COORDINATE:NewFromVec3(self.vec3) self.impactCoord=COORDINATE:NewFromVec3(self.vec3)
-- Safe impact heading, using last_velocity because self:GetVelocityVec3() is no longer possible
self.impactHeading = UTILS.VecHdg(self.last_velocity)
-- Mark impact point on F10 map. -- Mark impact point on F10 map.
if self.impactMark then if self.impactMark then
self.impactCoord:MarkToAll(string.format("Impact point of weapon %s\ntype=%s\nlauncher=%s", self.name, self.typeName, self.launcherName)) self.impactCoord:MarkToAll(string.format("Impact point of weapon %s\ntype=%s\nlauncher=%s", self.name, self.typeName, self.launcherName))

View File

@ -37,12 +37,12 @@ videos on YouTube.
in applications. It's main advantages are: in applications. It's main advantages are:
- It is fast, - It is fast,
- it is portabel (Windows, Linux, MacOS), - it is portable (Windows, Linux, MacOS),
- it is easy to use. - it is easy to use.
[Lua] is embedded in DCS, so we can use it without any modifacation to the game. [Lua] is embedded in DCS, so we can use it without any modification to the game.
## What is are scripts, frameworks and classes? ## What are scripts, frameworks and classes?
A script is a set of instructions in plain text read by a computer and processed A script is a set of instructions in plain text read by a computer and processed
on the fly. Scripts do not need to be compiled before execution, unlike exe on the fly. Scripts do not need to be compiled before execution, unlike exe