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 = {
ClassName = "CLIENTMENUMANAGER",
lid = "",
version = "0.1.3",
version = "0.1.4",
name = nil,
clientset = nil,
menutree = {},
@ -439,18 +439,18 @@ function CLIENTMENUMANAGER:_EventHandler(EventData)
--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
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
self:ResetMenu(Client)
end
elseif (EventData.id == EVENTS.PlayerEnterAircraft) and EventData.IniCoalition == self.Coalition 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)
return self
end
--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)
end
elseif EventData.id == EVENTS.PlayerEnterUnit then
@ -668,7 +668,7 @@ function CLIENTMENUMANAGER:Propagate(Client)
for _,_client in pairs(Set) do
local client = _client -- Wrapper.Client#CLIENT
if client and client:IsAlive() then
local playername = client:GetPlayerName()
local playername = client:GetPlayerName() or "none"
if not self.playertree[playername] then
self.playertree[playername] = {}
end

View File

@ -2783,7 +2783,7 @@ end
-- @return Wrapper.Group#GROUP that was spawned or #nil if nothing was spawned.
-- @usage
--
-- local SpawnPointVec2 = ZONE:New( ZoneName ):GetPointVec2()
-- local SpawnPointVec2 = ZONE:New( ZoneName ):GetPointVec2()
--
-- -- Spawn at the zone center position at the height specified in the ME of the group template!
-- SpawnAirplanes:SpawnFromPointVec2( SpawnPointVec2 )
@ -3283,6 +3283,7 @@ function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) -- R2.2
local CallsignName = SpawnTemplate.units[UnitID].callsign["name"] -- #string
CallsignName = string.match(CallsignName,"^(%a+)") -- 2.8 - only the part w/o numbers
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]
else
SpawnTemplate.units[UnitID].callsign = Callsign + SpawnIndex

View File

@ -20,13 +20,15 @@
-- ### Author: FlightControl - Framework Design & Programming
-- ### Refactoring to use the Runway auto-detection: Applevangelist
-- @date August 2022
-- Last Update Nov 2023
--
-- ===
--
-- @module Functional.ATC_Ground
-- @image Air_Traffic_Control_Ground_Operations.JPG
--- @type ATC_GROUND
---
-- @type ATC_GROUND
-- @field Core.Set#SET_CLIENT SetClient
-- @extends Core.Base#BASE
@ -39,7 +41,8 @@ ATC_GROUND = {
AirbaseNames = nil,
}
--- @type ATC_GROUND.AirbaseNames
---
-- @type ATC_GROUND.AirbaseNames
-- @list <#string>
@ -51,7 +54,7 @@ function ATC_GROUND:New( Airbases, AirbaseList )
-- Inherits from BASE
local self = BASE:Inherit( self, BASE:New() ) -- #ATC_GROUND
self:E( { self.ClassName, Airbases } )
self:T( { self.ClassName, Airbases } )
self.Airbases = Airbases
self.AirbaseList = AirbaseList
@ -82,7 +85,7 @@ function ATC_GROUND:New( Airbases, AirbaseList )
end
self.SetClient:ForEachClient(
--- @param Wrapper.Client#CLIENT Client
-- @param Wrapper.Client#CLIENT Client
function( Client )
Client:SetState( self, "Speeding", false )
Client:SetState( self, "Warnings", 0)
@ -246,11 +249,11 @@ function ATC_GROUND:SetMaximumKickSpeedMiph( MaximumKickSpeedMiph, Airbase )
return self
end
--- @param #ATC_GROUND self
-- @param #ATC_GROUND self
function ATC_GROUND:_AirbaseMonitor()
self.SetClient:ForEachClient(
--- @param Wrapper.Client#CLIENT Client
-- @param Wrapper.Client#CLIENT Client
function( Client )
if Client:IsAlive() then
@ -258,7 +261,7 @@ function ATC_GROUND:_AirbaseMonitor()
local IsOnGround = Client:InAir() == false
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
@ -271,7 +274,7 @@ function ATC_GROUND:_AirbaseMonitor()
if IsOnGround then
local Taxi = Client:GetState( self, "Taxi" )
self:E( Taxi )
self:T( Taxi )
if Taxi == false then
local Velocity = VELOCITY:New( AirbaseMeta.KickSpeed or self.KickSpeed )
Client:Message( "Welcome to " .. AirbaseID .. ". The maximum taxiing speed is " ..
@ -331,7 +334,7 @@ function ATC_GROUND:_AirbaseMonitor()
Client:SetState( self, "Warnings", SpeedingWarnings + 1 )
else
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:SetState( self, "Speeding", false )
Client:SetState( self, "Warnings", 0 )
@ -363,7 +366,7 @@ function ATC_GROUND:_AirbaseMonitor()
Client:SetState( self, "OffRunwayWarnings", OffRunwayWarnings + 1 )
else
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:SetState( self, "IsOffRunway", false )
Client:SetState( self, "OffRunwayWarnings", 0 )
@ -424,13 +427,20 @@ ATC_GROUND_UNIVERSAL = {
--- Creates a new ATC\_GROUND\_UNIVERSAL object. This works on any map.
-- @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
-- @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)
-- Inherits from BASE
local self = BASE:Inherit( self, BASE:New() ) -- #ATC_GROUND
self:E( { self.ClassName } )
self:T( { self.ClassName } )
self.Airbases = {}
@ -440,6 +450,13 @@ function ATC_GROUND_UNIVERSAL:New(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()
@ -460,8 +477,9 @@ function ATC_GROUND_UNIVERSAL:New(AirbaseList)
self.Airbases[AirbaseName].Monitor = true
end
self.SetClient:ForEachClient(
--- @param Wrapper.Client#CLIENT Client
-- @param Wrapper.Client#CLIENT Client
function( Client )
Client:SetState( self, "Speeding", false )
Client:SetState( self, "Warnings", 0)
@ -679,7 +697,7 @@ end
-- @param #ATC_GROUND_UNIVERSAL self
-- @return #ATC_GROUND_UNIVERSAL self
function ATC_GROUND_UNIVERSAL:_AirbaseMonitor()
self:I("_AirbaseMonitor")
self.SetClient:ForEachClient(
--- @param Wrapper.Client#CLIENT Client
function( Client )
@ -689,7 +707,7 @@ function ATC_GROUND_UNIVERSAL:_AirbaseMonitor()
local IsOnGround = Client:InAir() == false
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
@ -706,7 +724,7 @@ function ATC_GROUND_UNIVERSAL:_AirbaseMonitor()
if IsOnGround then
local Taxi = Client:GetState( self, "Taxi" )
self:E( Taxi )
self:T( Taxi )
if Taxi == false then
local Velocity = VELOCITY:New( AirbaseMeta.KickSpeed or self.KickSpeed )
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 )
else
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:SetState( self, "Speeding", false )
Client:SetState( self, "Warnings", 0 )
@ -798,7 +816,7 @@ function ATC_GROUND_UNIVERSAL:_AirbaseMonitor()
Client:SetState( self, "OffRunwayWarnings", OffRunwayWarnings + 1 )
else
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:SetState( self, "IsOffRunway", false )
Client:SetState( self, "OffRunwayWarnings", 0 )
@ -838,15 +856,16 @@ end
--- Start SCHEDULER for ATC_GROUND_UNIVERSAL object.
-- @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
function ATC_GROUND_UNIVERSAL:Start( RepeatScanSeconds )
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
end
--- @type ATC_GROUND_CAUCASUS
---
-- @type ATC_GROUND_CAUCASUS
-- @extends #ATC_GROUND
--- # ATC\_GROUND\_CAUCASUS, extends @{#ATC_GROUND_UNIVERSAL}
@ -981,12 +1000,12 @@ end
-- @return nothing
function ATC_GROUND_CAUCASUS:Start( RepeatScanSeconds )
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
--- @type ATC_GROUND_NEVADA
---
-- @type ATC_GROUND_NEVADA
-- @extends #ATC_GROUND
@ -1120,11 +1139,11 @@ end
-- @return nothing
function ATC_GROUND_NEVADA:Start( RepeatScanSeconds )
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
--- @type ATC_GROUND_NORMANDY
---
-- @type ATC_GROUND_NORMANDY
-- @extends #ATC_GROUND
@ -1277,10 +1296,11 @@ end
-- @return nothing
function ATC_GROUND_NORMANDY:Start( RepeatScanSeconds )
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
--- @type ATC_GROUND_PERSIANGULF
---
-- @type ATC_GROUND_PERSIANGULF
-- @extends #ATC_GROUND
@ -1419,11 +1439,11 @@ end
-- @return nothing
function ATC_GROUND_PERSIANGULF:Start( RepeatScanSeconds )
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
--- @type ATC_GROUND_MARIANAISLANDS
-- @type ATC_GROUND_MARIANAISLANDS
-- @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.SetMaximumKickSpeedMiph}(): Set the maximum speed allowed at an airbase in miles per hour.
--
---- @field #ATC_GROUND_MARIANAISLANDS
-- @field #ATC_GROUND_MARIANAISLANDS
ATC_GROUND_MARIANAISLANDS = {
ClassName = "ATC_GROUND_MARIANAISLANDS",
}
@ -1529,7 +1549,7 @@ ATC_GROUND_MARIANAISLANDS = {
function ATC_GROUND_MARIANAISLANDS:New( AirbaseNames )
-- 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:SetMaximumKickSpeedKmph( 150 )
@ -1543,5 +1563,5 @@ end
-- @return nothing
function ATC_GROUND_MARIANAISLANDS:Start( RepeatScanSeconds )
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

View File

@ -1579,7 +1579,7 @@ function PLAYERTASKCONTROLLER:New(Name, Coalition, Type, ClientFilter)
self.ClusterRadius = 0.5
self.TargetRadius = 500
self.ClientFilter = ClientFilter or ""
self.ClientFilter = ClientFilter --or ""
self.TargetQueue = 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 ConfigFilePath Path to the standard config file
-- @field #boolean ConfigLoaded
-- @field #string ttsprovider Default provider TTS backend, e.g. "Google" or "Microsoft", default is Microsoft
-- @extends Core.Base#BASE
--- *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.
--
-- ## 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
--
-- 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",
ConfigFilePath = "Config\\",
ConfigLoaded = false,
ttsprovider = "Microsoft",
}
--- MSRS class version.
-- @field #string version
MSRS.version="0.1.2"
MSRS.version="0.1.3"
--- Voices
-- @type MSRS.Voices
@ -377,9 +383,7 @@ function MSRS:New(PathToSRS, Frequency, Modulation, Volume, AltBackend)
return self:_NewAltBackend(Backend)
end
local success = self:LoadConfigFile(nil,nil,self.ConfigLoaded)
if (not success) and (not self.ConfigLoaded) then
if not self.ConfigLoaded then
-- If no AltBackend table, the proceed with default initialisation
self:SetPath(PathToSRS)
@ -446,7 +450,7 @@ function MSRS:SetPath(Path)
end
-- Debug output.
self:I(string.format("SRS path=%s", self:GetPath()))
self:T(string.format("SRS path=%s", self:GetPath()))
end
return self
end
@ -674,7 +678,7 @@ function MSRS:SetCoordinate(Coordinate)
return self
end
--- Use google text-to-speech.
--- Use google text-to-speech credentials. Also sets Google as default TTS provider.
-- @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.
-- @return #MSRS self
@ -688,13 +692,14 @@ function MSRS:SetGoogle(PathToCredentials)
self.GRPCOptions.DefaultProvider = "gcloud"
self.GRPCOptions.gcloud.key = PathToCredentials
self.ttsprovider = "Google"
end
return self
end
--- Use google text-to-speech.
--- gRPC Backend: Use google text-to-speech set the API key.
-- @param #MSRS self
-- @param #string APIKey API Key, usually a string of length 40 with characters and numbers.
-- @return #MSRS self
@ -708,6 +713,22 @@ function MSRS:SetGoogleAPIKey(APIKey)
return self
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.
-- @param #MSRS self
-- @return #MSRS self
@ -1114,7 +1135,7 @@ function MSRS:_GetCommand(freqs, modus, coal, gender, voice, culture, volume, sp
end
-- Set google.
if self.google then
if self.google and self.ttsprovider == "Google" then
command=command..string.format(' --ssml -G "%s"', self.google)
end
@ -1128,7 +1149,6 @@ end
-- @param #MSRS self
-- @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 #boolean ConfigLoaded - if true, skip the loading
-- @return #boolean success
-- @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,
@ -1138,18 +1158,19 @@ end
--
-- -- Moose MSRS default 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
-- 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,
-- Volume = 1.0, -- 0.0 to 1.0
-- 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",
-- 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",
-- Voice = "Microsoft Hazel Desktop",
-- Provider = "Microsoft", -- this is the default TTS provider, e.g. "Google" or "Microsoft"
-- -- gRPC (optional)
-- GRPC = { -- see https://github.com/DCS-gRPC/rust-server
-- 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
--
-- 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()`
-- 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:
--
@ -1190,46 +1215,17 @@ end
-- atis:SetSRS(nil,nil,nil,MSRS.Voices.Google.Standard.en_US_Standard_H)
-- --Start ATIS
-- atis:Start()
function MSRS:LoadConfigFile(Path,Filename,ConfigLoaded)
function MSRS:LoadConfigFile(Path,Filename)
local path = Path or lfs.writedir()..MSRS.ConfigFilePath
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))()
-- now we should have a global var MSRS_Config
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
self.path = MSRS_Config.Path or "C:\\Program Files\\DCS-SimpleRadio-Standalone"
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.gender = MSRS_Config.Gender or "male"
self.google = MSRS_Config.Google
if MSRS_Config.Provider then
self.ttsprovider = MSRS_Config.Provider
end
self.Label = MSRS_Config.Label or "MSRS"
self.voice = MSRS_Config.Voice --or MSRS.Voices.Microsoft.Hazel
if MSRS_Config.GRPC then
@ -1266,6 +1265,9 @@ function MSRS:LoadConfigFile(Path,Filename,ConfigLoaded)
MSRS.culture = MSRS_Config.Culture or "en-GB"
MSRS.gender = MSRS_Config.Gender or "male"
MSRS.google = MSRS_Config.Google
if MSRS_Config.Provider then
MSRS.ttsprovider = MSRS_Config.Provider
end
MSRS.Label = MSRS_Config.Label or "MSRS"
MSRS.voice = MSRS_Config.Voice --or MSRS.Voices.Microsoft.Hazel
if MSRS_Config.GRPC then
@ -1280,9 +1282,10 @@ function MSRS:LoadConfigFile(Path,Filename,ConfigLoaded)
MSRS.ConfigLoaded = true
end
end
env.info("MSRS - Sucessfully loaded default configuration from disk!",false)
else
env.info("MSRS - Cannot load default configuration from disk!",false)
env.info("MSRS - Successfully loaded default configuration from disk!",false)
end
if not filexsists then
env.info("MSRS - Cannot find default configuration file!",false)
return false
end
@ -1995,6 +1998,7 @@ function MSRSQUEUE:_CheckRadioQueue(delay)
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}
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.
-- @param DCS#Vec2 a 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}
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.
-- @param DCS#Vec3 a 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.
local exists=UTILS.CheckFileExists(Path,Filename)
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
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)
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

@ -223,7 +223,15 @@ function WEAPON:New(WeaponObject)
-- Set log ID.
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
self:SetTimeStepTrack()
self:SetDistanceInterceptPoint()
@ -552,6 +560,52 @@ function WEAPON:GetImpactCoordinate()
return self.impactCoord
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...
-- @param #WEAPON self
-- @return #boolean If `true`, weapon is in the air and `false` if not. Returns `nil` if weapon object itself is `nil`.
@ -712,7 +766,10 @@ function WEAPON:_TrackWeapon(time)
-- Update coordinate.
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.
self.tracking=true
@ -781,7 +838,10 @@ function WEAPON:_TrackWeapon(time)
-- Safe impact coordinate.
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.
if self.impactMark then
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:
- It is fast,
- it is portabel (Windows, Linux, MacOS),
- it is portable (Windows, Linux, MacOS),
- 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
on the fly. Scripts do not need to be compiled before execution, unlike exe