diff --git a/.github/workflows/build-includes.yml b/.github/workflows/build-includes.yml index 4bd22bdbe..15065ba9e 100644 --- a/.github/workflows/build-includes.yml +++ b/.github/workflows/build-includes.yml @@ -95,10 +95,6 @@ jobs: export COMMIT_TIME=$(git show -s --format=%cd ${{ github.sha }} --date=iso-strict) lua5.3 "./Moose Setup/Moose_Create.lua" D "$COMMIT_TIME-${{ github.sha }}" "./Moose Development/Moose" "./Moose Setup" "./build/result/Moose_Include_Dynamic" - - name: Run LuaSrcDiet - run: | - luasrcdiet --basic --opt-emptylines ./build/result/Moose_Include_Static/Moose.lua -o ./build/result/Moose_Include_Static/Moose_.lua - ######################################################################### # Run LuaCheck ######################################################################### @@ -108,6 +104,10 @@ jobs: run: | luacheck --std=lua51c --config=.luacheckrc -gurasqq "Moose Development/Moose" + - name: Run LuaSrcDiet + run: | + luasrcdiet --basic --opt-emptylines ./build/result/Moose_Include_Static/Moose.lua -o ./build/result/Moose_Include_Static/Moose_.lua + ######################################################################### # Push to MOOSE_INCLUDE ######################################################################### diff --git a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua index 59941b899..a548e83d9 100644 --- a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua @@ -23,7 +23,7 @@ -- -- ## Missions: -- --- [AID-A2A - AI A2A Dispatching](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AID%20-%20AI%20Dispatching/AID-A2A%20-%20AI%20A2A%20Dispatching) +-- [AID-A2A - AI A2A Dispatching](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AI/AI_A2A_Dispatcher) -- -- === -- @@ -310,7 +310,7 @@ do -- AI_A2A_DISPATCHER -- Use the method @{#AI_A2A_DISPATCHER.SetEngageRadius}() to set a specific Engage Radius. -- **The Engage Radius is defined for ALL squadrons which are operational.** -- - -- Demonstration Mission: [AID-019 - AI_A2A - Engage Range Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AID%20-%20AI%20Dispatching/AID-A2A%20-%20AI%20A2A%20Dispatching/AID-A2A-019%20-%20Engage%20Range%20Test) + -- Demonstration Mission: [AID-019 - AI_A2A - Engage Range Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AI/AI_A2A_Dispatcher/AID-A2A-019%20-%20Engage%20Range%20Test) -- -- In this example an Engage Radius is set to various values. -- @@ -333,7 +333,7 @@ do -- AI_A2A_DISPATCHER -- Use the method @{#AI_A2A_DISPATCHER.SetGciRadius}() to set a specific controlled ground intercept radius. -- **The Ground Controlled Intercept radius is defined for ALL squadrons which are operational.** -- - -- Demonstration Mission: [AID-013 - AI_A2A - Intercept Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AID%20-%20AI%20Dispatching/AID-A2A%20-%20AI%20A2A%20Dispatching/AID-A2A-013%20-%20Intercept%20Test) + -- Demonstration Mission: [AID-013 - AI_A2A - Intercept Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AI/AI_A2A_Dispatcher/AID-A2A-013%20-%20Intercept%20Test) -- -- In these examples, the Gci Radius is set to various values: -- @@ -366,7 +366,7 @@ do -- AI_A2A_DISPATCHER -- it makes it easier sometimes for the mission maker to envisage where the red and blue territories roughly are. -- In a hot war the borders are effectively defined by the ground based radar coverage of a coalition. -- - -- Demonstration Mission: [AID-009 - AI_A2A - Border Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AID%20-%20AI%20Dispatching/AID-A2A%20-%20AI%20A2A%20Dispatching/AID-A2A-009%20-%20Border%20Test) + -- Demonstration Mission: [AID-009 - AI_A2A - Border Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AI/AI_A2A_Dispatcher/AID-A2A-009%20-%20Border%20Test) -- -- In this example a border is set for the CCCP A2A dispatcher: -- @@ -1233,7 +1233,7 @@ do -- AI_A2A_DISPATCHER -- -- **Use the method @{#AI_A2A_DISPATCHER.SetEngageRadius}() to modify the default Engage Radius for ALL squadrons.** -- - -- Demonstration Mission: [AID-019 - AI_A2A - Engage Range Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AID%20-%20AI%20Dispatching/AID-A2A%20-%20AI%20A2A%20Dispatching/AID-A2A-019%20-%20Engage%20Range%20Test) + -- Demonstration Mission: [AID-019 - AI_A2A - Engage Range Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AI/AI_A2A_Dispatcher/AID-A2A-019%20-%20Engage%20Range%20Test) -- -- @param #AI_A2A_DISPATCHER self -- @param #number EngageRadius (Optional, Default = 100000) The radius to report friendlies near the target. @@ -1283,7 +1283,7 @@ do -- AI_A2A_DISPATCHER -- Use the method @{#AI_A2A_DISPATCHER.SetGciRadius}() to set a specific controlled ground intercept radius. -- **The Ground Controlled Intercept radius is defined for ALL squadrons which are operational.** -- - -- Demonstration Mission: [AID-013 - AI_A2A - Intercept Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AID%20-%20AI%20Dispatching/AID-A2A%20-%20AI%20A2A%20Dispatching/AID-A2A-013%20-%20Intercept%20Test) + -- Demonstration Mission: [AID-013 - AI_A2A - Intercept Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AI/AI_A2A_Dispatcher/AID-A2A-013%20-%20Intercept%20Test) -- -- @param #AI_A2A_DISPATCHER self -- @param #number GciRadius (Optional, Default = 200000) The radius to ground control intercept detected targets from the nearest airbase. @@ -3257,7 +3257,8 @@ do -- AI_A2A_DISPATCHER end end - --- @param #AI_A2A_DISPATCHER self + --- AI_A2A_Fsm:onafterHome + -- @param #AI_A2A_DISPATCHER self function AI_A2A_Fsm:onafterHome( Defender, From, Event, To, Action ) if Defender and Defender:IsAlive() then self:F( { "CAP Home", Defender:GetName() } ) @@ -3505,7 +3506,8 @@ do -- AI_A2A_DISPATCHER Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) end - --- @param #AI_A2A_DISPATCHER self + --- function Fsm:onafterLostControl + -- @param #AI_A2A_DISPATCHER self function Fsm:onafterLostControl( Defender, From, Event, To ) self:F( { "GCI LostControl", Defender:GetName() } ) self:GetParent( self ).onafterHome( self, Defender, From, Event, To ) @@ -3518,7 +3520,8 @@ do -- AI_A2A_DISPATCHER end end - --- @param #AI_A2A_DISPATCHER self + --- function Fsm:onafterHome + -- @param #AI_A2A_DISPATCHER self function Fsm:onafterHome( DefenderGroup, From, Event, To, Action ) self:F( { "GCI Home", DefenderGroup:GetName() } ) self:GetParent( self ).onafterHome( self, DefenderGroup, From, Event, To ) @@ -3959,7 +3962,7 @@ do -- -- # Demo Missions -- - -- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AID%20-%20AI%20Dispatching/AID-A2A%20-%20AI%20A2A%20Dispatching) + -- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AI/AI_A2A_Dispatcher) -- -- === -- diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 1fe9b4c5a..1dad17759 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -24,7 +24,7 @@ -- -- ## Missions: -- --- [AID-A2G - AI A2G Dispatching](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AID%20-%20AI%20Dispatching/AID-A2G%20-%20AI%20A2G%20Dispatching) +-- [AID-A2G - AI A2G Dispatching](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AI/AI_A2G_Dispatcher) -- -- === -- diff --git a/Moose Development/Moose/AI/AI_Air_Dispatcher.lua b/Moose Development/Moose/AI/AI_Air_Dispatcher.lua index b422ac03e..8d0bbd9cf 100644 --- a/Moose Development/Moose/AI/AI_Air_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_Air_Dispatcher.lua @@ -24,7 +24,7 @@ -- -- ## Missions: -- --- [AID-AIR - AI AIR Dispatching](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AID%20-%20AI%20Dispatching) +-- [AI_A2A_Dispatcher](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AI/AI_A2A_Dispatcher) -- -- === -- diff --git a/Moose Development/Moose/AI/AI_Balancer.lua b/Moose Development/Moose/AI/AI_Balancer.lua index fad0746ff..b64e27057 100644 --- a/Moose Development/Moose/AI/AI_Balancer.lua +++ b/Moose Development/Moose/AI/AI_Balancer.lua @@ -9,7 +9,7 @@ -- -- === -- --- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AIB%20-%20AI%20Balancing) +-- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AI/AI_Balancer) -- -- === -- @@ -168,7 +168,8 @@ function AI_BALANCER:ReturnToHomeAirbase( ReturnThresholdRange ) self.ReturnThresholdRange = ReturnThresholdRange end ---- @param #AI_BALANCER self +--- AI_BALANCER:onenterSpawning +-- @param #AI_BALANCER self -- @param Core.Set#SET_GROUP SetGroup -- @param #string ClientName -- @param Wrapper.Group#GROUP AIGroup @@ -190,7 +191,8 @@ function AI_BALANCER:onenterSpawning( SetGroup, From, Event, To, ClientName ) end end ---- @param #AI_BALANCER self +--- AI_BALANCER:onenterDestroying +-- @param #AI_BALANCER self -- @param Core.Set#SET_GROUP SetGroup -- @param Wrapper.Group#GROUP AIGroup function AI_BALANCER:onenterDestroying( SetGroup, From, Event, To, ClientName, AIGroup ) @@ -233,15 +235,16 @@ function AI_BALANCER:onenterReturning( SetGroup, From, Event, To, AIGroup ) end - ---- @param #AI_BALANCER self +--- AI_BALANCER:onenterMonitoring +-- @param #AI_BALANCER self function AI_BALANCER:onenterMonitoring( SetGroup ) self:T2( { self.SetClient:Count() } ) --self.SetClient:Flush() self.SetClient:ForEachClient( - --- @param Wrapper.Client#CLIENT Client + --- SetClient:ForEachClient + -- @param Wrapper.Client#CLIENT Client function( Client ) self:T3(Client.ClientName) @@ -264,7 +267,8 @@ function AI_BALANCER:onenterMonitoring( SetGroup ) self:T2( RangeZone ) _DATABASE:ForEachPlayerUnit( - --- @param Wrapper.Unit#UNIT RangeTestUnit + --- Nameless function + -- @param Wrapper.Unit#UNIT RangeTestUnit function( RangeTestUnit, RangeZone, AIGroup, PlayerInRange ) self:T2( { PlayerInRange, RangeTestUnit.UnitName, RangeZone.ZoneName } ) if RangeTestUnit:IsInZone( RangeZone ) == true then @@ -276,7 +280,8 @@ function AI_BALANCER:onenterMonitoring( SetGroup ) end end, - --- @param Core.Zone#ZONE_RADIUS RangeZone + --- Nameless function + -- @param Core.Zone#ZONE_RADIUS RangeZone -- @param Wrapper.Group#GROUP AIGroup function( RangeZone, AIGroup, PlayerInRange ) if PlayerInRange.Value == false then @@ -307,6 +312,3 @@ function AI_BALANCER:onenterMonitoring( SetGroup ) self:__Monitor( 10 ) end - - - diff --git a/Moose Development/Moose/AI/AI_Cargo_Dispatcher.lua b/Moose Development/Moose/AI/AI_Cargo_Dispatcher.lua index ea954b97d..0fedc9643 100644 --- a/Moose Development/Moose/AI/AI_Cargo_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_Cargo_Dispatcher.lua @@ -18,7 +18,7 @@ -- -- Test missions can be located on the main GITHUB site. -- --- [FlightControl-Master/MOOSE_MISSIONS/AID - AI Dispatching/AID-CGO - AI Cargo Dispatching/](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/AID%20-%20AI%20Dispatching/AID-CGO%20-%20AI%20Cargo%20Dispatching) +-- [FlightControl-Master/MOOSE_MISSIONS/AID - AI Dispatching/AID-CGO - AI Cargo Dispatching/](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AI/AI_Cargo_Dispatcher) -- -- === -- @@ -572,7 +572,7 @@ -- A home zone can be specified to where the Carriers will move when there isn't any cargo left for pickup. -- Use @{#AI_CARGO_DISPATCHER.SetHomeZone}() to specify the home zone. -- --- If no home zone is specified, the carriers will wait near the deploy zone for a new pickup command. +-- If no home zone is specified, the carriers will wait near the deploy zone for a new pickup command. -- -- === -- @@ -583,10 +583,12 @@ AI_CARGO_DISPATCHER = { PickupCargo = {} } ---- @field #list +--- List of AI_Cargo +-- @field #list AI_CARGO_DISPATCHER.AI_Cargo = {} ---- @field #list +--- List of PickupCargo +-- @field #list AI_CARGO_DISPATCHER.PickupCargo = {} diff --git a/Moose Development/Moose/Cargo/Cargo.lua b/Moose Development/Moose/Cargo/Cargo.lua index c9297378a..8b7d6040e 100644 --- a/Moose Development/Moose/Cargo/Cargo.lua +++ b/Moose Development/Moose/Cargo/Cargo.lua @@ -370,7 +370,7 @@ CARGOS = {} do -- CARGO - --- @type CARGO + -- @type CARGO -- @extends Core.Fsm#FSM_PROCESS -- @field #string Type A string defining the type of the cargo. eg. Engineers, Equipment, Screwdrivers. -- @field #string Name A string defining the name of the cargo. The name is the unique identifier of the cargo. @@ -433,7 +433,7 @@ do -- CARGO Reported = {}, } - --- @type CARGO.CargoObjects + -- @type CARGO.CargoObjects -- @map < #string, Wrapper.Positionable#POSITIONABLE > The alive POSITIONABLE objects representing the the cargo. --- CARGO Constructor. This class is an abstract class and should not be instantiated. @@ -447,7 +447,7 @@ do -- CARGO function CARGO:New( Type, Name, Weight, LoadRadius, NearRadius ) --R2.1 local self = BASE:Inherit( self, FSM:New() ) -- #CARGO - self:F( { Type, Name, Weight, LoadRadius, NearRadius } ) + self:T( { Type, Name, Weight, LoadRadius, NearRadius } ) self:SetStartState( "UnLoaded" ) self:AddTransition( { "UnLoaded", "Boarding" }, "Board", "Boarding" ) @@ -711,7 +711,7 @@ do -- CARGO -- @param #CARGO self -- @return #CARGO function CARGO:Spawn( PointVec2 ) - self:F() + self:T() end @@ -812,7 +812,7 @@ do -- CARGO -- @param Core.Point#COORDINATE Coordinate -- @return #boolean true if the CargoGroup is within the loading radius. function CARGO:IsInLoadRadius( Coordinate ) - self:F( { Coordinate, LoadRadius = self.LoadRadius } ) + self:T( { Coordinate, LoadRadius = self.LoadRadius } ) local Distance = 0 if self:IsUnLoaded() then @@ -832,7 +832,7 @@ do -- CARGO -- @param Core.Point#COORDINATE Coordinate -- @return #boolean true if the Cargo can report itself. function CARGO:IsInReportRadius( Coordinate ) - self:F( { Coordinate } ) + self:T( { Coordinate } ) local Distance = 0 if self:IsUnLoaded() then @@ -853,23 +853,23 @@ do -- CARGO -- @param #number NearRadius The radius when the cargo will board the Carrier (to avoid collision). -- @return #boolean function CARGO:IsNear( Coordinate, NearRadius ) - --self:F( { PointVec2 = PointVec2, NearRadius = NearRadius } ) + --self:T( { PointVec2 = PointVec2, NearRadius = NearRadius } ) if self.CargoObject:IsAlive() then --local Distance = PointVec2:Get2DDistance( self.CargoObject:GetPointVec2() ) - --self:F( { CargoObjectName = self.CargoObject:GetName() } ) - --self:F( { CargoObjectVec2 = self.CargoObject:GetVec2() } ) - --self:F( { PointVec2 = PointVec2:GetVec2() } ) + --self:T( { CargoObjectName = self.CargoObject:GetName() } ) + --self:T( { CargoObjectVec2 = self.CargoObject:GetVec2() } ) + --self:T( { PointVec2 = PointVec2:GetVec2() } ) local Distance = Coordinate:Get2DDistance( self.CargoObject:GetCoordinate() ) - --self:F( { Distance = Distance, NearRadius = NearRadius or "nil" } ) + --self:T( { Distance = Distance, NearRadius = NearRadius or "nil" } ) if Distance <= NearRadius then - --self:F( { PointVec2 = PointVec2, NearRadius = NearRadius, IsNear = true } ) + --self:T( { PointVec2 = PointVec2, NearRadius = NearRadius, IsNear = true } ) return true end end - --self:F( { PointVec2 = PointVec2, NearRadius = NearRadius, IsNear = false } ) + --self:T( { PointVec2 = PointVec2, NearRadius = NearRadius, IsNear = false } ) return false end @@ -878,12 +878,12 @@ do -- CARGO -- @param Core.Zone#ZONE_BASE Zone -- @return #boolean **true** if cargo is in the Zone, **false** if cargo is not in the Zone. function CARGO:IsInZone( Zone ) - --self:F( { Zone } ) + --self:T( { Zone } ) if self:IsLoaded() then return Zone:IsPointVec2InZone( self.CargoCarrier:GetPointVec2() ) else - --self:F( { Size = self.CargoObject:GetSize(), Units = self.CargoObject:GetUnits() } ) + --self:T( { Size = self.CargoObject:GetSize(), Units = self.CargoObject:GetUnits() } ) if self.CargoObject:GetSize() ~= 0 then return Zone:IsPointVec2InZone( self.CargoObject:GetPointVec2() ) else @@ -1034,7 +1034,7 @@ end -- CARGO do -- CARGO_REPRESENTABLE - --- @type CARGO_REPRESENTABLE + -- @type CARGO_REPRESENTABLE -- @extends #CARGO -- @field test @@ -1056,7 +1056,7 @@ do -- CARGO_REPRESENTABLE -- Inherit CARGO. local self = BASE:Inherit( self, CARGO:New( Type, Name, 0, LoadRadius, NearRadius ) ) -- #CARGO_REPRESENTABLE - self:F( { Type, Name, LoadRadius, NearRadius } ) + self:T( { Type, Name, LoadRadius, NearRadius } ) -- Descriptors. local Desc=CargoObject:GetDesc() @@ -1086,7 +1086,7 @@ do -- CARGO_REPRESENTABLE function CARGO_REPRESENTABLE:Destroy() -- Cargo objects are deleted from the _DATABASE and SET_CARGO objects. - self:F( { CargoName = self:GetName() } ) + self:T( { CargoName = self:GetName() } ) --_EVENTDISPATCHER:CreateEventDeleteCargo( self ) return self @@ -1123,12 +1123,12 @@ do -- CARGO_REPRESENTABLE CoordinateZone:Scan( { Object.Category.UNIT } ) for _, DCSUnit in pairs( CoordinateZone:GetScannedUnits() ) do local NearUnit = UNIT:Find( DCSUnit ) - self:F({NearUnit=NearUnit}) + self:T({NearUnit=NearUnit}) local NearUnitCoalition = NearUnit:GetCoalition() local CargoCoalition = self:GetCoalition() if NearUnitCoalition == CargoCoalition then local Attributes = NearUnit:GetDesc() - self:F({Desc=Attributes}) + self:T({Desc=Attributes}) if NearUnit:HasAttribute( "Trucks" ) then MESSAGE:New( Message, 20, NearUnit:GetCallsign() .. " reporting - Cargo " .. self:GetName() ):ToGroup( TaskGroup ) break @@ -1142,7 +1142,7 @@ end -- CARGO_REPRESENTABLE do -- CARGO_REPORTABLE - --- @type CARGO_REPORTABLE + -- @type CARGO_REPORTABLE -- @extends #CARGO CARGO_REPORTABLE = { ClassName = "CARGO_REPORTABLE" @@ -1158,7 +1158,7 @@ do -- CARGO_REPORTABLE -- @return #CARGO_REPORTABLE function CARGO_REPORTABLE:New( Type, Name, Weight, LoadRadius, NearRadius ) local self = BASE:Inherit( self, CARGO:New( Type, Name, Weight, LoadRadius, NearRadius ) ) -- #CARGO_REPORTABLE - self:F( { Type, Name, Weight, LoadRadius, NearRadius } ) + self:T( { Type, Name, Weight, LoadRadius, NearRadius } ) return self end @@ -1178,7 +1178,7 @@ end do -- CARGO_PACKAGE - --- @type CARGO_PACKAGE + -- @type CARGO_PACKAGE -- @extends #CARGO_REPRESENTABLE CARGO_PACKAGE = { ClassName = "CARGO_PACKAGE" @@ -1195,7 +1195,7 @@ do -- CARGO_PACKAGE -- @return #CARGO_PACKAGE function CARGO_PACKAGE:New( CargoCarrier, Type, Name, Weight, LoadRadius, NearRadius ) local self = BASE:Inherit( self, CARGO_REPRESENTABLE:New( CargoCarrier, Type, Name, Weight, LoadRadius, NearRadius ) ) -- #CARGO_PACKAGE - self:F( { Type, Name, Weight, LoadRadius, NearRadius } ) + self:T( { Type, Name, Weight, LoadRadius, NearRadius } ) self:T( CargoCarrier ) self.CargoCarrier = CargoCarrier @@ -1213,7 +1213,7 @@ end -- @param #number BoardDistance -- @param #number Angle function CARGO_PACKAGE:onafterOnBoard( From, Event, To, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) - self:F() + self:T() self.CargoInAir = self.CargoCarrier:InAir() @@ -1246,7 +1246,7 @@ end -- @param Wrapper.Unit#UNIT CargoCarrier -- @return #boolean function CARGO_PACKAGE:IsNear( CargoCarrier ) - self:F() + self:T() local CargoCarrierPoint = CargoCarrier:GetCoordinate() @@ -1271,7 +1271,7 @@ end -- @param #number LoadDistance -- @param #number Angle function CARGO_PACKAGE:onafterOnBoarded( From, Event, To, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) - self:F() + self:T() if self:IsNear( CargoCarrier ) then self:__Load( 1, CargoCarrier, Speed, LoadDistance, Angle ) @@ -1292,7 +1292,7 @@ end -- @param #number Radius -- @param #number Angle function CARGO_PACKAGE:onafterUnBoard( From, Event, To, CargoCarrier, Speed, UnLoadDistance, UnBoardDistance, Radius, Angle ) - self:F() + self:T() self.CargoInAir = self.CargoCarrier:InAir() @@ -1331,7 +1331,7 @@ end -- @param Wrapper.Unit#UNIT CargoCarrier -- @param #number Speed function CARGO_PACKAGE:onafterUnBoarded( From, Event, To, CargoCarrier, Speed ) - self:F() + self:T() if self:IsNear( CargoCarrier ) then self:__UnLoad( 1, CargoCarrier, Speed ) @@ -1350,7 +1350,7 @@ end -- @param #number LoadDistance -- @param #number Angle function CARGO_PACKAGE:onafterLoad( From, Event, To, CargoCarrier, Speed, LoadDistance, Angle ) - self:F() + self:T() self.CargoCarrier = CargoCarrier @@ -1378,7 +1378,7 @@ end -- @param #number Distance -- @param #number Angle function CARGO_PACKAGE:onafterUnLoad( From, Event, To, CargoCarrier, Speed, Distance, Angle ) - self:F() + self:T() local StartPointVec2 = self.CargoCarrier:GetPointVec2() local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. diff --git a/Moose Development/Moose/Cargo/CargoCrate.lua b/Moose Development/Moose/Cargo/CargoCrate.lua index 2abc9365b..c0fbdd631 100644 --- a/Moose Development/Moose/Cargo/CargoCrate.lua +++ b/Moose Development/Moose/Cargo/CargoCrate.lua @@ -59,7 +59,7 @@ do -- CARGO_CRATE -- @return #CARGO_CRATE function CARGO_CRATE:New( CargoStatic, Type, Name, LoadRadius, NearRadius ) local self = BASE:Inherit( self, CARGO_REPRESENTABLE:New( CargoStatic, Type, Name, nil, LoadRadius, NearRadius ) ) -- #CARGO_CRATE - self:F( { Type, Name, NearRadius } ) + self:T( { Type, Name, NearRadius } ) self.CargoObject = CargoStatic -- Wrapper.Static#STATIC @@ -116,7 +116,7 @@ do -- CARGO_CRATE -- @param #string To -- @param Core.Point#POINT_VEC2 function CARGO_CRATE:onenterUnLoaded( From, Event, To, ToPointVec2 ) - --self:F( { ToPointVec2, From, Event, To } ) + --self:T( { ToPointVec2, From, Event, To } ) local Angle = 180 local Speed = 10 @@ -153,7 +153,7 @@ do -- CARGO_CRATE -- @param #string To -- @param Wrapper.Unit#UNIT CargoCarrier function CARGO_CRATE:onenterLoaded( From, Event, To, CargoCarrier ) - --self:F( { From, Event, To, CargoCarrier } ) + --self:T( { From, Event, To, CargoCarrier } ) self.CargoCarrier = CargoCarrier @@ -190,7 +190,7 @@ do -- CARGO_CRATE -- @param Core.Point#COORDINATE Coordinate -- @return #boolean true if the Cargo Crate is within the report radius. function CARGO_CRATE:IsInReportRadius( Coordinate ) - --self:F( { Coordinate, LoadRadius = self.LoadRadius } ) + --self:T( { Coordinate, LoadRadius = self.LoadRadius } ) local Distance = 0 if self:IsUnLoaded() then @@ -210,7 +210,7 @@ do -- CARGO_CRATE -- @param Core.Point#Coordinate Coordinate -- @return #boolean true if the Cargo Crate is within the loading radius. function CARGO_CRATE:IsInLoadRadius( Coordinate ) - --self:F( { Coordinate, LoadRadius = self.NearRadius } ) + --self:T( { Coordinate, LoadRadius = self.NearRadius } ) local Distance = 0 if self:IsUnLoaded() then @@ -231,7 +231,7 @@ do -- CARGO_CRATE -- @return Core.Point#COORDINATE The current Coordinate of the first Cargo of the CargoGroup. -- @return #nil There is no valid Cargo in the CargoGroup. function CARGO_CRATE:GetCoordinate() - --self:F() + --self:T() return self.CargoObject:GetCoordinate() end @@ -261,7 +261,7 @@ do -- CARGO_CRATE -- @param #CARGO_CRATE self -- @param Core.Point#COORDINATE Coordinate function CARGO_CRATE:RouteTo( Coordinate ) - self:F( {Coordinate = Coordinate } ) + self:T( {Coordinate = Coordinate } ) end @@ -274,7 +274,7 @@ do -- CARGO_CRATE -- @return #boolean The Cargo is near to the Carrier. -- @return #nil The Cargo is not near to the Carrier. function CARGO_CRATE:IsNear( CargoCarrier, NearRadius ) - self:F( {NearRadius = NearRadius } ) + self:T( {NearRadius = NearRadius } ) return self:IsNear( CargoCarrier:GetCoordinate(), NearRadius ) end @@ -283,7 +283,7 @@ do -- CARGO_CRATE -- @param #CARGO_CRATE self function CARGO_CRATE:Respawn() - self:F( { "Respawning crate " .. self:GetName() } ) + self:T( { "Respawning crate " .. self:GetName() } ) -- Respawn the group... @@ -300,7 +300,7 @@ do -- CARGO_CRATE -- @param #CARGO_CRATE self function CARGO_CRATE:onafterReset() - self:F( { "Reset crate " .. self:GetName() } ) + self:T( { "Reset crate " .. self:GetName() } ) -- Respawn the group... diff --git a/Moose Development/Moose/Cargo/CargoGroup.lua b/Moose Development/Moose/Cargo/CargoGroup.lua index 4f76aac4c..ca6a96a69 100644 --- a/Moose Development/Moose/Cargo/CargoGroup.lua +++ b/Moose Development/Moose/Cargo/CargoGroup.lua @@ -64,7 +64,7 @@ do -- CARGO_GROUP -- Inherit CAROG_REPORTABLE local self = BASE:Inherit( self, CARGO_REPORTABLE:New( Type, Name, 0, LoadRadius, NearRadius ) ) -- #CARGO_GROUP - self:F( { Type, Name, LoadRadius } ) + self:T( { Type, Name, LoadRadius } ) self.CargoSet = SET_CARGO:New() self.CargoGroup = CargoGroup @@ -146,7 +146,7 @@ do -- CARGO_GROUP -- @param #CARGO_GROUP self function CARGO_GROUP:Respawn() - self:F( { "Respawning" } ) + self:T( { "Respawning" } ) for CargoID, CargoData in pairs( self.CargoSet:GetSet() ) do local Cargo = CargoData -- Cargo.Cargo#CARGO @@ -227,7 +227,7 @@ do -- CARGO_GROUP -- @param #CARGO_GROUP self function CARGO_GROUP:Regroup() - self:F("Regroup") + self:T("Regroup") if self.Grouped == false then @@ -241,7 +241,7 @@ do -- CARGO_GROUP for CargoUnitName, CargoUnit in pairs( self.CargoSet:GetSet() ) do local CargoUnit = CargoUnit -- Cargo.CargoUnit#CARGO_UNIT - self:F( { CargoUnit:GetName(), UnLoaded = CargoUnit:IsUnLoaded() } ) + self:T( { CargoUnit:GetName(), UnLoaded = CargoUnit:IsUnLoaded() } ) if CargoUnit:IsUnLoaded() then @@ -258,7 +258,7 @@ do -- CARGO_GROUP -- Then we register the new group in the database self.CargoGroup = GROUP:NewTemplate( GroupTemplate, GroupTemplate.CoalitionID, GroupTemplate.CategoryID, GroupTemplate.CountryID ) - self:F( { "Regroup", GroupTemplate } ) + self:T( { "Regroup", GroupTemplate } ) -- Now we spawn the new group based on the template created. self.CargoObject = _DATABASE:Spawn( GroupTemplate ) @@ -271,7 +271,7 @@ do -- CARGO_GROUP -- @param Core.Event#EVENTDATA EventData function CARGO_GROUP:OnEventCargoDead( EventData ) - self:E(EventData) + self:T(EventData) local Destroyed = false @@ -296,7 +296,7 @@ do -- CARGO_GROUP if Destroyed then self:Destroyed() - self:E( { "Cargo group destroyed" } ) + self:T( { "Cargo group destroyed" } ) end end @@ -309,14 +309,14 @@ do -- CARGO_GROUP -- @param Wrapper.Unit#UNIT CargoCarrier -- @param #number NearRadius If distance is smaller than this number, cargo is loaded into the carrier. function CARGO_GROUP:onafterBoard( From, Event, To, CargoCarrier, NearRadius, ... ) - self:F( { CargoCarrier.UnitName, From, Event, To, NearRadius = NearRadius } ) + self:T( { CargoCarrier.UnitName, From, Event, To, NearRadius = NearRadius } ) NearRadius = NearRadius or self.NearRadius -- For each Cargo object within the CARGO_GROUPED, route each object to the CargoLoadPointVec2 self.CargoSet:ForEach( function( Cargo, ... ) - self:F( { "Board Unit", Cargo:GetName( ), Cargo:IsDestroyed(), Cargo.CargoObject:IsAlive() } ) + self:T( { "Board Unit", Cargo:GetName( ), Cargo:IsDestroyed(), Cargo.CargoObject:IsAlive() } ) local CargoGroup = Cargo.CargoObject --Wrapper.Group#GROUP CargoGroup:OptionAlarmStateGreen() Cargo:__Board( 1, CargoCarrier, NearRadius, ... ) @@ -334,7 +334,7 @@ do -- CARGO_GROUP -- @param #string To -- @param Wrapper.Unit#UNIT CargoCarrier function CARGO_GROUP:onafterLoad( From, Event, To, CargoCarrier, ... ) - --self:F( { From, Event, To, CargoCarrier, ...} ) + --self:T( { From, Event, To, CargoCarrier, ...} ) if From == "UnLoaded" then -- For each Cargo object within the CARGO_GROUP, load each cargo to the CargoCarrier. @@ -359,7 +359,7 @@ do -- CARGO_GROUP -- @param Wrapper.Unit#UNIT CargoCarrier -- @param #number NearRadius If distance is smaller than this number, cargo is loaded into the carrier. function CARGO_GROUP:onafterBoarding( From, Event, To, CargoCarrier, NearRadius, ... ) - --self:F( { CargoCarrier.UnitName, From, Event, To } ) + --self:T( { CargoCarrier.UnitName, From, Event, To } ) local Boarded = true local Cancelled = false @@ -393,7 +393,7 @@ do -- CARGO_GROUP if not Boarded then self:__Boarding( -5, CargoCarrier, NearRadius, ... ) else - self:F("Group Cargo is loaded") + self:T("Group Cargo is loaded") self:__Load( 1, CargoCarrier, ... ) end else @@ -413,7 +413,7 @@ do -- CARGO_GROUP -- @param Core.Point#POINT_VEC2 ToPointVec2 -- @param #number NearRadius If distance is smaller than this number, cargo is loaded into the carrier. function CARGO_GROUP:onafterUnBoard( From, Event, To, ToPointVec2, NearRadius, ... ) - self:F( {From, Event, To, ToPointVec2, NearRadius } ) + self:T( {From, Event, To, ToPointVec2, NearRadius } ) NearRadius = NearRadius or 25 @@ -456,7 +456,7 @@ do -- CARGO_GROUP -- @param Core.Point#POINT_VEC2 ToPointVec2 -- @param #number NearRadius If distance is smaller than this number, cargo is loaded into the carrier. function CARGO_GROUP:onafterUnBoarding( From, Event, To, ToPointVec2, NearRadius, ... ) - --self:F( { From, Event, To, ToPointVec2, NearRadius } ) + --self:T( { From, Event, To, ToPointVec2, NearRadius } ) --local NearRadius = NearRadius or 25 @@ -493,7 +493,7 @@ do -- CARGO_GROUP -- @param #string To -- @param Core.Point#POINT_VEC2 ToPointVec2 function CARGO_GROUP:onafterUnLoad( From, Event, To, ToPointVec2, ... ) - --self:F( { From, Event, To, ToPointVec2 } ) + --self:T( { From, Event, To, ToPointVec2 } ) if From == "Loaded" then @@ -611,7 +611,7 @@ do -- CARGO_GROUP -- @param #CARGO_GROUP self -- @param Core.Point#COORDINATE Coordinate function CARGO_GROUP:RouteTo( Coordinate ) - --self:F( {Coordinate = Coordinate } ) + --self:T( {Coordinate = Coordinate } ) -- For each Cargo within the CargoSet, route each object to the Coordinate self.CargoSet:ForEach( @@ -629,13 +629,13 @@ do -- CARGO_GROUP -- @param #number NearRadius -- @return #boolean The Cargo is near to the Carrier or #nil if the Cargo is not near to the Carrier. function CARGO_GROUP:IsNear( CargoCarrier, NearRadius ) - self:F( {NearRadius = NearRadius } ) + self:T( {NearRadius = NearRadius } ) for _, Cargo in pairs( self.CargoSet:GetSet() ) do local Cargo = Cargo -- Cargo.Cargo#CARGO if Cargo:IsAlive() then if Cargo:IsNear( CargoCarrier:GetCoordinate(), NearRadius ) then - self:F( "Near" ) + self:T( "Near" ) return true end end @@ -649,7 +649,7 @@ do -- CARGO_GROUP -- @param Core.Point#COORDINATE Coordinate -- @return #boolean true if the Cargo Group is within the load radius. function CARGO_GROUP:IsInLoadRadius( Coordinate ) - --self:F( { Coordinate } ) + --self:T( { Coordinate } ) local Cargo = self:GetFirstAlive() -- Cargo.Cargo#CARGO @@ -669,7 +669,7 @@ do -- CARGO_GROUP return false end - self:F( { Distance = Distance, LoadRadius = self.LoadRadius } ) + self:T( { Distance = Distance, LoadRadius = self.LoadRadius } ) if Distance <= self.LoadRadius then return true else @@ -687,12 +687,12 @@ do -- CARGO_GROUP -- @param Core.Point#Coordinate Coordinate -- @return #boolean true if the Cargo Group is within the report radius. function CARGO_GROUP:IsInReportRadius( Coordinate ) - --self:F( { Coordinate } ) + --self:T( { Coordinate } ) local Cargo = self:GetFirstAlive() -- Cargo.Cargo#CARGO if Cargo then - self:F( { Cargo } ) + self:T( { Cargo } ) local Distance = 0 if Cargo:IsUnLoaded() then Distance = Coordinate:Get2DDistance( Cargo.CargoObject:GetCoordinate() ) @@ -738,7 +738,7 @@ do -- CARGO_GROUP -- @return #boolean **true** if the first element of the CargoGroup is in the Zone -- @return #boolean **false** if there is no element of the CargoGroup in the Zone. function CARGO_GROUP:IsInZone( Zone ) - --self:F( { Zone } ) + --self:T( { Zone } ) local Cargo = self.CargoSet:GetFirst() -- Cargo.Cargo#CARGO diff --git a/Moose Development/Moose/Cargo/CargoSlingload.lua b/Moose Development/Moose/Cargo/CargoSlingload.lua index ff6d81f17..ad26e8868 100644 --- a/Moose Development/Moose/Cargo/CargoSlingload.lua +++ b/Moose Development/Moose/Cargo/CargoSlingload.lua @@ -52,7 +52,7 @@ do -- CARGO_SLINGLOAD -- @return #CARGO_SLINGLOAD function CARGO_SLINGLOAD:New( CargoStatic, Type, Name, LoadRadius, NearRadius ) local self = BASE:Inherit( self, CARGO_REPRESENTABLE:New( CargoStatic, Type, Name, nil, LoadRadius, NearRadius ) ) -- #CARGO_SLINGLOAD - self:F( { Type, Name, NearRadius } ) + self:T( { Type, Name, NearRadius } ) self.CargoObject = CargoStatic @@ -130,7 +130,7 @@ do -- CARGO_SLINGLOAD -- @param Core.Point#COORDINATE Coordinate -- @return #boolean true if the Cargo Crate is within the report radius. function CARGO_SLINGLOAD:IsInReportRadius( Coordinate ) - --self:F( { Coordinate, LoadRadius = self.LoadRadius } ) + --self:T( { Coordinate, LoadRadius = self.LoadRadius } ) local Distance = 0 if self:IsUnLoaded() then @@ -149,7 +149,7 @@ do -- CARGO_SLINGLOAD -- @param Core.Point#COORDINATE Coordinate -- @return #boolean true if the Cargo Slingload is within the loading radius. function CARGO_SLINGLOAD:IsInLoadRadius( Coordinate ) - --self:F( { Coordinate } ) + --self:T( { Coordinate } ) local Distance = 0 if self:IsUnLoaded() then @@ -169,7 +169,7 @@ do -- CARGO_SLINGLOAD -- @return Core.Point#COORDINATE The current Coordinate of the first Cargo of the CargoGroup. -- @return #nil There is no valid Cargo in the CargoGroup. function CARGO_SLINGLOAD:GetCoordinate() - --self:F() + --self:T() return self.CargoObject:GetCoordinate() end @@ -199,7 +199,7 @@ do -- CARGO_SLINGLOAD -- @param #CARGO_SLINGLOAD self -- @param Core.Point#COORDINATE Coordinate function CARGO_SLINGLOAD:RouteTo( Coordinate ) - --self:F( {Coordinate = Coordinate } ) + --self:T( {Coordinate = Coordinate } ) end @@ -212,7 +212,7 @@ do -- CARGO_SLINGLOAD -- @return #boolean The Cargo is near to the Carrier. -- @return #nil The Cargo is not near to the Carrier. function CARGO_SLINGLOAD:IsNear( CargoCarrier, NearRadius ) - --self:F( {NearRadius = NearRadius } ) + --self:T( {NearRadius = NearRadius } ) return self:IsNear( CargoCarrier:GetCoordinate(), NearRadius ) end @@ -222,7 +222,7 @@ do -- CARGO_SLINGLOAD -- @param #CARGO_SLINGLOAD self function CARGO_SLINGLOAD:Respawn() - --self:F( { "Respawning slingload " .. self:GetName() } ) + --self:T( { "Respawning slingload " .. self:GetName() } ) -- Respawn the group... @@ -239,7 +239,7 @@ do -- CARGO_SLINGLOAD -- @param #CARGO_SLINGLOAD self function CARGO_SLINGLOAD:onafterReset() - --self:F( { "Reset slingload " .. self:GetName() } ) + --self:T( { "Reset slingload " .. self:GetName() } ) -- Respawn the group... diff --git a/Moose Development/Moose/Cargo/CargoUnit.lua b/Moose Development/Moose/Cargo/CargoUnit.lua index 830f02662..a1d86dd49 100644 --- a/Moose Development/Moose/Cargo/CargoUnit.lua +++ b/Moose Development/Moose/Cargo/CargoUnit.lua @@ -75,7 +75,7 @@ do -- CARGO_UNIT -- @param Core.Point#POINT_VEC2 ToPointVec2 -- @param #number NearRadius (optional) Defaut 25 m. function CARGO_UNIT:onenterUnBoarding( From, Event, To, ToPointVec2, NearRadius ) - self:F( { From, Event, To, ToPointVec2, NearRadius } ) + self:T( { From, Event, To, ToPointVec2, NearRadius } ) local Angle = 180 local Speed = 60 @@ -114,7 +114,7 @@ do -- CARGO_UNIT else self.CargoObject:ReSpawnAt( FromPointVec2, CargoDeployHeading ) end - self:F( { "CargoUnits:", self.CargoObject:GetGroup():GetName() } ) + self:T( { "CargoUnits:", self.CargoObject:GetGroup():GetName() } ) self.CargoCarrier = nil local Points = {} @@ -148,7 +148,7 @@ do -- CARGO_UNIT -- @param Core.Point#POINT_VEC2 ToPointVec2 -- @param #number NearRadius (optional) Defaut 100 m. function CARGO_UNIT:onleaveUnBoarding( From, Event, To, ToPointVec2, NearRadius ) - self:F( { From, Event, To, ToPointVec2, NearRadius } ) + self:T( { From, Event, To, ToPointVec2, NearRadius } ) local Angle = 180 local Speed = 10 @@ -174,7 +174,7 @@ do -- CARGO_UNIT -- @param Core.Point#POINT_VEC2 ToPointVec2 -- @param #number NearRadius (optional) Defaut 100 m. function CARGO_UNIT:onafterUnBoarding( From, Event, To, ToPointVec2, NearRadius ) - self:F( { From, Event, To, ToPointVec2, NearRadius } ) + self:T( { From, Event, To, ToPointVec2, NearRadius } ) self.CargoInAir = self.CargoObject:InAir() @@ -199,7 +199,7 @@ do -- CARGO_UNIT -- @param #string To -- @param Core.Point#POINT_VEC2 function CARGO_UNIT:onenterUnLoaded( From, Event, To, ToPointVec2 ) - self:F( { ToPointVec2, From, Event, To } ) + self:T( { ToPointVec2, From, Event, To } ) local Angle = 180 local Speed = 10 @@ -236,7 +236,7 @@ do -- CARGO_UNIT -- @param Wrapper.Group#GROUP CargoCarrier -- @param #number NearRadius function CARGO_UNIT:onafterBoard( From, Event, To, CargoCarrier, NearRadius, ... ) - self:F( { From, Event, To, CargoCarrier, NearRadius = NearRadius } ) + self:T( { From, Event, To, CargoCarrier, NearRadius = NearRadius } ) self.CargoInAir = self.CargoObject:InAir() @@ -244,7 +244,7 @@ do -- CARGO_UNIT local MaxSpeed = Desc.speedMaxOffRoad local TypeName = Desc.typeName - --self:F({Unit=self.CargoObject:GetName()}) + --self:T({Unit=self.CargoObject:GetName()}) -- A cargo unit can only be boarded if it is not dead @@ -298,9 +298,9 @@ do -- CARGO_UNIT -- @param Wrapper.Client#CLIENT CargoCarrier -- @param #number NearRadius Default 25 m. function CARGO_UNIT:onafterBoarding( From, Event, To, CargoCarrier, NearRadius, ... ) - self:F( { From, Event, To, CargoCarrier:GetName(), NearRadius = NearRadius } ) + self:T( { From, Event, To, CargoCarrier:GetName(), NearRadius = NearRadius } ) - self:F( { IsAlive=self.CargoObject:IsAlive() } ) + self:T( { IsAlive=self.CargoObject:IsAlive() } ) if CargoCarrier and CargoCarrier:IsAlive() then -- and self.CargoObject and self.CargoObject:IsAlive() then if (CargoCarrier:IsAir() and not CargoCarrier:InAir()) or true then @@ -321,7 +321,7 @@ do -- CARGO_UNIT local Angle = 180 local Distance = 0 - --self:F({Unit=self.CargoObject:GetName()}) + --self:T({Unit=self.CargoObject:GetName()}) local CargoCarrierPointVec2 = CargoCarrier:GetPointVec2() local CargoCarrierHeading = CargoCarrier:GetHeading() -- Get Heading of object in degrees. @@ -348,7 +348,7 @@ do -- CARGO_UNIT self.CargoObject:SetCommand( self.CargoObject:CommandStopRoute( true ) ) end else - self:E("Something is wrong") + self:T("Something is wrong") end end @@ -361,11 +361,11 @@ do -- CARGO_UNIT -- @param #string To -- @param Wrapper.Unit#UNIT CargoCarrier function CARGO_UNIT:onenterLoaded( From, Event, To, CargoCarrier ) - self:F( { From, Event, To, CargoCarrier } ) + self:T( { From, Event, To, CargoCarrier } ) self.CargoCarrier = CargoCarrier - --self:F({Unit=self.CargoObject:GetName()}) + --self:T({Unit=self.CargoObject:GetName()}) -- Only destroy the CargoObject if there is a CargoObject (packages don't have CargoObjects). if self.CargoObject then diff --git a/Moose Development/Moose/Core/Database.lua b/Moose Development/Moose/Core/Database.lua index e3e7c17c1..ac290aa2f 100644 --- a/Moose Development/Moose/Core/Database.lua +++ b/Moose Development/Moose/Core/Database.lua @@ -449,10 +449,10 @@ do -- Zones and Pathlines -- Loop over layers. for layerID, layerData in pairs(env.mission.drawings.layers or {}) do - + -- Loop over objects in layers. for objectID, objectData in pairs(layerData.objects or {}) do - + -- Check for polygon which has at least 4 points (we would need 3 but the origin seems to be there twice) if objectData.polygonMode and (objectData.polygonMode=="free") and objectData.points and #objectData.points>=4 then @@ -488,10 +488,32 @@ do -- Zones and Pathlines -- Create new polygon zone. local Zone=ZONE_POLYGON:NewFromPointsArray(ZoneName, points) - + + --Zone.DrawID = objectID + -- Set color. Zone:SetColor({1, 0, 0}, 0.15) - + Zone:SetFillColor({1, 0, 0}, 0.15) + + if objectData.colorString then + -- eg colorString = 0xff0000ff + local color = string.gsub(objectData.colorString,"^0x","") + local r = tonumber(string.sub(color,1,2),16)/255 + local g = tonumber(string.sub(color,3,4),16)/255 + local b = tonumber(string.sub(color,5,6),16)/255 + local a = tonumber(string.sub(color,7,8),16)/255 + Zone:SetColor({r, g, b}, a) + end + if objectData.fillColorString then + -- eg fillColorString = 0xff00004b + local color = string.gsub(objectData.colorString,"^0x","") + local r = tonumber(string.sub(color,1,2),16)/255 + local g = tonumber(string.sub(color,3,4),16)/255 + local b = tonumber(string.sub(color,5,6),16)/255 + local a = tonumber(string.sub(color,7,8),16)/255 + Zone:SetFillColor({r, g, b}, a) + end + -- Store in DB. self.ZONENAMES[ZoneName] = ZoneName @@ -532,7 +554,26 @@ do -- Zones and Pathlines -- Set color. Zone:SetColor({1, 0, 0}, 0.15) - + + if objectData.colorString then + -- eg colorString = 0xff0000ff + local color = string.gsub(objectData.colorString,"^0x","") + local r = tonumber(string.sub(color,1,2),16)/255 + local g = tonumber(string.sub(color,3,4),16)/255 + local b = tonumber(string.sub(color,5,6),16)/255 + local a = tonumber(string.sub(color,7,8),16)/255 + Zone:SetColor({r, g, b}, a) + end + if objectData.fillColorString then + -- eg fillColorString = 0xff00004b + local color = string.gsub(objectData.colorString,"^0x","") + local r = tonumber(string.sub(color,1,2),16)/255 + local g = tonumber(string.sub(color,3,4),16)/255 + local b = tonumber(string.sub(color,5,6),16)/255 + local a = tonumber(string.sub(color,7,8),16)/255 + Zone:SetFillColor({r, g, b}, a) + end + -- Store in DB. self.ZONENAMES[ZoneName] = ZoneName @@ -756,7 +797,7 @@ end -- cargo --- Finds a CLIENT based on the ClientName. -- @param #DATABASE self --- @param #string ClientName +-- @param #string ClientName - Note this is the UNIT name of the client! -- @return Wrapper.Client#CLIENT The found CLIENT. function DATABASE:FindClient( ClientName ) diff --git a/Moose Development/Moose/Core/Message.lua b/Moose Development/Moose/Core/Message.lua index 8b6da3cdd..46c896f0d 100644 --- a/Moose Development/Moose/Core/Message.lua +++ b/Moose Development/Moose/Core/Message.lua @@ -98,7 +98,7 @@ function MESSAGE:New( MessageText, MessageDuration, MessageCategory, ClearScreen self.MessageType = nil - -- When no MessageCategory is given, we don't show it as a title... + -- When no MessageCategory is given, we don't show it as a title... if MessageCategory and MessageCategory ~= "" then if MessageCategory:sub( -1 ) ~= "\n" then self.MessageCategory = MessageCategory .. ": " @@ -498,7 +498,6 @@ function MESSAGE.SetMSRS(PathToSRS,Port,PathToCredentials,Frequency,Modulation,G _MESSAGESRS.Gender = Gender or "female" _MESSAGESRS.MSRS:SetGoogle(PathToCredentials) - _MESSAGESRS.google = PathToCredentials _MESSAGESRS.MSRS:SetLabel(Label or "MESSAGE") _MESSAGESRS.label = Label or "MESSAGE" @@ -512,8 +511,6 @@ function MESSAGE.SetMSRS(PathToSRS,Port,PathToCredentials,Frequency,Modulation,G if Voice then _MESSAGESRS.MSRS:SetVoice(Voice) end _MESSAGESRS.voice = Voice --or MSRS.Voices.Microsoft.Hedda - --if _MESSAGESRS.google and not Voice then _MESSAGESRS.Voice = MSRS.Voices.Google.Standard.en_GB_Standard_A end - --_MESSAGESRS.MSRS:SetVoice(Voice or _MESSAGESRS.voice) _MESSAGESRS.SRSQ = MSRSQUEUE:New(Label or "MESSAGE") end diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index a3c83da1f..89807b436 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -2456,15 +2456,18 @@ do -- COORDINATE -- Write command as string and execute that. Idea by Grimes https://forum.dcs.world/topic/324201-mark-to-all-function/#comment-5273793 local s=string.format("trigger.action.markupToAll(7, %d, %d,", Coalition, MarkID) for _,vec in pairs(vecs) do - s=s..string.format("%s,", UTILS._OneLineSerialize(vec)) + --s=s..string.format("%s,", UTILS._OneLineSerialize(vec)) + s=s..string.format("{x=%.1f, y=%.1f, z=%.1f},", vec.x, vec.y, vec.z) end - s=s..string.format("%s, %s, %s, %s", UTILS._OneLineSerialize(Color), UTILS._OneLineSerialize(FillColor), tostring(LineType), tostring(ReadOnly)) - if Text and Text~="" then - s=s..string.format(", \"%s\"", Text) + s=s..string.format("{%.3f, %.3f, %.3f, %.3f},", Color[1], Color[2], Color[3], Color[4]) + s=s..string.format("{%.3f, %.3f, %.3f, %.3f},", FillColor[1], FillColor[2], FillColor[3], FillColor[4]) + s=s..string.format("%d,", LineType or 1) + s=s..string.format("%s", tostring(ReadOnly)) + if Text and type(Text)=="string" and string.len(Text)>0 then + s=s..string.format(", \"%s\"", tostring(Text)) end s=s..")" - -- Execute string command local success=UTILS.DoString(s) diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index 43fbd8fce..f4e5a0ed7 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -146,7 +146,45 @@ do -- SET_BASE return self end - + + --- [Internal] Add a functional filter + -- @param #SET_BASE self + -- @param #function ConditionFunction If this function returns `true`, the object is added to the SET. The function needs to take a CONTROLLABLE object as first argument. + -- @param ... Condition function arguments, if any. + -- @return #boolean If true, at least one condition is true + function SET_BASE:FilterFunction(ConditionFunction, ...) + + local condition={} + condition.func=ConditionFunction + condition.arg={} + + if arg then + condition.arg=arg + end + + if not self.Filter.Functions then self.Filter.Functions = {} end + table.insert(self.Filter.Functions, condition) + + return self + end + + --- [Internal] Check if the condition functions returns true. + -- @param #SET_BASE self + -- @param Wrapper.Controllable#CONTROLLABLE Object The object to filter for + -- @return #boolean If true, if **all** conditions are true + function SET_BASE:_EvalFilterFunctions(Object) + -- All conditions must be true. + for _,_condition in pairs(self.Filter.Functions or {}) do + local condition=_condition + -- Call function. + if condition.func(Object,unpack(condition.arg)) == false then + return false + end + end + -- No condition was true. + return true + end + --- Clear the Objects in the Set. -- @param #SET_BASE self -- @param #boolean TriggerEvent If `true`, an event remove is triggered for each group that is removed from the set. @@ -967,6 +1005,7 @@ do -- * @{#SET_GROUP.FilterCategoryShip}: Builds the SET_GROUP from ships. -- * @{#SET_GROUP.FilterCategoryStructure}: Builds the SET_GROUP from structures. -- * @{#SET_GROUP.FilterZones}: Builds the SET_GROUP with the groups within a @{Core.Zone#ZONE}. + -- * @{#SET_GROUP.FilterFunction}: Builds the SET_GROUP with a custom condition. -- -- Once the filter criteria have been set for the SET_GROUP, you can start filtering using: -- @@ -1040,6 +1079,7 @@ do Countries = nil, GroupPrefixes = nil, Zones = nil, + Functions = nil, }, FilterMeta = { Coalitions = { @@ -1253,7 +1293,26 @@ do return self end + + --- [User] Add a custom condition function. + -- @function [parent=#SET_GROUP] FilterFunction + -- @param #SET_GROUP self + -- @param #function ConditionFunction If this function returns `true`, the object is added to the SET. The function needs to take a GROUP object as first argument. + -- @param ... Condition function arguments if any. + -- @return #SET_GROUP self + -- @usage + -- -- Image you want to exclude a specific GROUP from a SET: + -- local groundset = SET_GROUP:New():FilterCoalitions("blue"):FilterCategoryGround():FilterFunction( + -- -- The function needs to take a GROUP object as first - and in this case, only - argument. + -- function(grp) + -- local isinclude = true + -- if grp:GetName() == "Exclude Me" then isinclude = false end + -- return isinclude + -- end + -- ):FilterOnce() + -- BASE:I(groundset:Flush()) + --- Builds a set of groups of coalitions. -- Possible current coalitions are red, blue and neutral. -- @param #SET_GROUP self @@ -1927,7 +1986,7 @@ do MGroupInclude = MGroupInclude and MGroupActive end - if self.Filter.Coalitions then + if self.Filter.Coalitions and MGroupInclude then local MGroupCoalition = false for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do self:T3( { "Coalition:", MGroup:GetCoalition(), self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) @@ -1938,7 +1997,7 @@ do MGroupInclude = MGroupInclude and MGroupCoalition end - if self.Filter.Categories then + if self.Filter.Categories and MGroupInclude then local MGroupCategory = false for CategoryID, CategoryName in pairs( self.Filter.Categories ) do self:T3( { "Category:", MGroup:GetCategory(), self.FilterMeta.Categories[CategoryName], CategoryName } ) @@ -1949,7 +2008,7 @@ do MGroupInclude = MGroupInclude and MGroupCategory end - if self.Filter.Countries then + if self.Filter.Countries and MGroupInclude then local MGroupCountry = false for CountryID, CountryName in pairs( self.Filter.Countries ) do self:T3( { "Country:", MGroup:GetCountry(), CountryName } ) @@ -1960,7 +2019,7 @@ do MGroupInclude = MGroupInclude and MGroupCountry end - if self.Filter.GroupPrefixes then + if self.Filter.GroupPrefixes and MGroupInclude then local MGroupPrefix = false for GroupPrefixId, GroupPrefix in pairs( self.Filter.GroupPrefixes ) do self:T3( { "Prefix:", string.find( MGroup:GetName(), GroupPrefix, 1 ), GroupPrefix } ) @@ -1971,7 +2030,7 @@ do MGroupInclude = MGroupInclude and MGroupPrefix end - if self.Filter.Zones then + if self.Filter.Zones and MGroupInclude then local MGroupZone = false for ZoneName, Zone in pairs( self.Filter.Zones ) do --self:T( "Zone:", ZoneName ) @@ -1981,6 +2040,12 @@ do end MGroupInclude = MGroupInclude and MGroupZone end + + if self.Filter.Functions and MGroupInclude then + local MGroupFunc = false + MGroupFunc = self:_EvalFilterFunctions(MGroup) + MGroupInclude = MGroupInclude and MGroupFunc + end self:T2( MGroupInclude ) return MGroupInclude @@ -2080,6 +2145,7 @@ do -- SET_UNIT -- Have a read through here to understand the application of regular expressions: [LUA regular expressions](https://riptutorial.com/lua/example/20315/lua-pattern-matching) -- * @{#SET_UNIT.FilterActive}: Builds the SET_UNIT with the units that are only active. Units that are inactive (late activation) won't be included in the set! -- * @{#SET_UNIT.FilterZones}: Builds the SET_UNIT with the units within a @{Core.Zone#ZONE}. + -- * @{#SET_UNIT.FilterFunction}: Builds the SET_UNIT with a custom condition. -- -- Once the filter criteria have been set for the SET_UNIT, you can start filtering using: -- @@ -2158,6 +2224,7 @@ do -- SET_UNIT Countries = nil, UnitPrefixes = nil, Zones = nil, + Functions = nil, }, FilterMeta = { Coalitions = { @@ -2529,6 +2596,25 @@ do -- SET_UNIT return self end + --- [User] Add a custom condition function. + -- @function [parent=#SET_UNIT] FilterFunction + -- @param #SET_UNIT self + -- @param #function ConditionFunction If this function returns `true`, the object is added to the SET. The function needs to take a UNIT object as first argument. + -- @param ... Condition function arguments if any. + -- @return #SET_UNIT self + -- @usage + -- -- Image you want to exclude a specific UNIT from a SET: + -- local groundset = SET_UNIT:New():FilterCoalitions("blue"):FilterCategories("ground"):FilterFunction( + -- -- The function needs to take a UNIT object as first - and in this case, only - argument. + -- function(unit) + -- local isinclude = true + -- if unit:GetName() == "Exclude Me" then isinclude = false end + -- return isinclude + -- end + -- ):FilterOnce() + -- BASE:I(groundset:Flush()) + + --- Handles the Database to check on an event (birth) that the Object was added in the Database. -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! -- @param #SET_UNIT self @@ -3106,7 +3192,7 @@ do -- SET_UNIT MUnitInclude = MUnitInclude and MUnitActive end - if self.Filter.Coalitions then + if self.Filter.Coalitions and MUnitInclude then local MUnitCoalition = false for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do self:F( { "Coalition:", MUnit:GetCoalition(), self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) @@ -3117,7 +3203,7 @@ do -- SET_UNIT MUnitInclude = MUnitInclude and MUnitCoalition end - if self.Filter.Categories then + if self.Filter.Categories and MUnitInclude then local MUnitCategory = false for CategoryID, CategoryName in pairs( self.Filter.Categories ) do self:T3( { "Category:", MUnit:GetDesc().category, self.FilterMeta.Categories[CategoryName], CategoryName } ) @@ -3128,7 +3214,7 @@ do -- SET_UNIT MUnitInclude = MUnitInclude and MUnitCategory end - if self.Filter.Types then + if self.Filter.Types and MUnitInclude then local MUnitType = false for TypeID, TypeName in pairs( self.Filter.Types ) do self:T3( { "Type:", MUnit:GetTypeName(), TypeName } ) @@ -3139,7 +3225,7 @@ do -- SET_UNIT MUnitInclude = MUnitInclude and MUnitType end - if self.Filter.Countries then + if self.Filter.Countries and MUnitInclude then local MUnitCountry = false for CountryID, CountryName in pairs( self.Filter.Countries ) do self:T3( { "Country:", MUnit:GetCountry(), CountryName } ) @@ -3150,7 +3236,7 @@ do -- SET_UNIT MUnitInclude = MUnitInclude and MUnitCountry end - if self.Filter.UnitPrefixes then + if self.Filter.UnitPrefixes and MUnitInclude then local MUnitPrefix = false for UnitPrefixId, UnitPrefix in pairs( self.Filter.UnitPrefixes ) do self:T3( { "Prefix:", string.find( MUnit:GetName(), UnitPrefix, 1 ), UnitPrefix } ) @@ -3161,7 +3247,7 @@ do -- SET_UNIT MUnitInclude = MUnitInclude and MUnitPrefix end - if self.Filter.RadarTypes then + if self.Filter.RadarTypes and MUnitInclude then local MUnitRadar = false for RadarTypeID, RadarType in pairs( self.Filter.RadarTypes ) do self:T3( { "Radar:", RadarType } ) @@ -3175,7 +3261,7 @@ do -- SET_UNIT MUnitInclude = MUnitInclude and MUnitRadar end - if self.Filter.SEAD then + if self.Filter.SEAD and MUnitInclude then local MUnitSEAD = false if MUnit:HasSEAD() == true then self:T3( "SEAD Found" ) @@ -3185,7 +3271,7 @@ do -- SET_UNIT end end - if self.Filter.Zones then + if self.Filter.Zones and MUnitInclude then local MGroupZone = false for ZoneName, Zone in pairs( self.Filter.Zones ) do self:T3( "Zone:", ZoneName ) @@ -3196,6 +3282,11 @@ do -- SET_UNIT MUnitInclude = MUnitInclude and MGroupZone end + if self.Filter.Functions and MUnitInclude then + local MUnitFunc = self:_EvalFilterFunctions(MUnit) + MUnitInclude = MUnitInclude and MUnitFunc + end + self:T2( MUnitInclude ) return MUnitInclude end @@ -3277,6 +3368,7 @@ do -- SET_STATIC -- * @{#SET_STATIC.FilterPrefixes}: Builds the SET_STATIC with the units containing the same string(s) in their name. **Attention!** LUA regular expression apply here, so special characters in names like minus, dot, hash (#) etc might lead to unexpected results. -- Have a read through here to understand the application of regular expressions: [LUA regular expressions](https://riptutorial.com/lua/example/20315/lua-pattern-matching) -- * @{#SET_STATIC.FilterZones}: Builds the SET_STATIC with the units within a @{Core.Zone#ZONE}. + -- * @{#SET_STATIC.FilterFunction}: Builds the SET_STATIC with a custom condition. -- -- Once the filter criteria have been set for the SET_STATIC, you can start filtering using: -- @@ -3479,7 +3571,25 @@ do -- SET_STATIC end return self end - + + --- [User] Add a custom condition function. + -- @function [parent=#SET_STATIC] FilterFunction + -- @param #SET_STATIC self + -- @param #function ConditionFunction If this function returns `true`, the object is added to the SET. The function needs to take a STATIC object as first argument. + -- @param ... Condition function arguments if any. + -- @return #SET_STATIC self + -- @usage + -- -- Image you want to exclude a specific CLIENT from a SET: + -- local groundset = SET_STATIC:New():FilterCoalitions("blue"):FilterActive(true):FilterFunction( + -- -- The function needs to take a STATIC object as first - and in this case, only - argument. + -- function(static) + -- local isinclude = true + -- if static:GetName() == "Exclude Me" then isinclude = false end + -- return isinclude + -- end + -- ):FilterOnce() + -- BASE:I(groundset:Flush()) + --- Builds a set of units of defined countries. -- Possible current countries are those known within DCS world. -- @param #SET_STATIC self @@ -4036,6 +4146,7 @@ do -- SET_CLIENT -- Have a read through here to understand the application of regular expressions: [LUA regular expressions](https://riptutorial.com/lua/example/20315/lua-pattern-matching) -- * @{#SET_CLIENT.FilterActive}: Builds the SET_CLIENT with the units that are only active. Units that are inactive (late activation) won't be included in the set! -- * @{#SET_CLIENT.FilterZones}: Builds the SET_CLIENT with the clients within a @{Core.Zone#ZONE}. + -- * @{#SET_CLIENT.FilterFunction}: Builds the SET_CLIENT with a custom condition. -- -- Once the filter criteria have been set for the SET_CLIENT, you can start filtering using: -- @@ -4538,6 +4649,25 @@ do -- SET_CLIENT return AliveSet.Set or {} end + --- [User] Add a custom condition function. + -- @function [parent=#SET_CLIENT] FilterFunction + -- @param #SET_CLIENT self + -- @param #function ConditionFunction If this function returns `true`, the object is added to the SET. The function needs to take a CLIENT object as first argument. + -- @param ... Condition function arguments if any. + -- @return #SET_CLIENT self + -- @usage + -- -- Image you want to exclude a specific CLIENT from a SET: + -- local groundset = SET_CLIENT:New():FilterCoalitions("blue"):FilterActive(true):FilterFunction( + -- -- The function needs to take a UNIT object as first - and in this case, only - argument. + -- function(client) + -- local isinclude = true + -- if client:GetPlayerName() == "Exclude Me" then isinclude = false end + -- return isinclude + -- end + -- ):FilterOnce() + -- BASE:I(groundset:Flush()) + + --- -- @param #SET_CLIENT self -- @param Wrapper.Client#CLIENT MClient @@ -4559,7 +4689,7 @@ do -- SET_CLIENT MClientInclude = MClientInclude and MClientActive end - if self.Filter.Coalitions then + if self.Filter.Coalitions and MClientInclude then local MClientCoalition = false for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do local ClientCoalitionID = _DATABASE:GetCoalitionFromClientTemplate( MClientName ) @@ -4572,7 +4702,7 @@ do -- SET_CLIENT MClientInclude = MClientInclude and MClientCoalition end - if self.Filter.Categories then + if self.Filter.Categories and MClientInclude then local MClientCategory = false for CategoryID, CategoryName in pairs( self.Filter.Categories ) do local ClientCategoryID = _DATABASE:GetCategoryFromClientTemplate( MClientName ) @@ -4585,7 +4715,7 @@ do -- SET_CLIENT MClientInclude = MClientInclude and MClientCategory end - if self.Filter.Types then + if self.Filter.Types and MClientInclude then local MClientType = false for TypeID, TypeName in pairs( self.Filter.Types ) do self:T3( { "Type:", MClient:GetTypeName(), TypeName } ) @@ -4597,7 +4727,7 @@ do -- SET_CLIENT MClientInclude = MClientInclude and MClientType end - if self.Filter.Countries then + if self.Filter.Countries and MClientInclude then local MClientCountry = false for CountryID, CountryName in pairs( self.Filter.Countries ) do local ClientCountryID = _DATABASE:GetCountryFromClientTemplate( MClientName ) @@ -4610,7 +4740,7 @@ do -- SET_CLIENT MClientInclude = MClientInclude and MClientCountry end - if self.Filter.ClientPrefixes then + if self.Filter.ClientPrefixes and MClientInclude then local MClientPrefix = false for ClientPrefixId, ClientPrefix in pairs( self.Filter.ClientPrefixes ) do self:T3( { "Prefix:", string.find( MClient.UnitName, ClientPrefix, 1 ), ClientPrefix } ) @@ -4622,7 +4752,7 @@ do -- SET_CLIENT MClientInclude = MClientInclude and MClientPrefix end - if self.Filter.Zones then + if self.Filter.Zones and MClientInclude then local MClientZone = false for ZoneName, Zone in pairs( self.Filter.Zones ) do self:T3( "Zone:", ZoneName ) @@ -4634,7 +4764,7 @@ do -- SET_CLIENT MClientInclude = MClientInclude and MClientZone end - if self.Filter.Playernames then + if self.Filter.Playernames and MClientInclude then local MClientPlayername = false local playername = MClient:GetPlayerName() or "Unknown" --self:T(playername) @@ -4647,7 +4777,7 @@ do -- SET_CLIENT MClientInclude = MClientInclude and MClientPlayername end - if self.Filter.Callsigns then + if self.Filter.Callsigns and MClientInclude then local MClientCallsigns = false local callsign = MClient:GetCallsign() --self:I(callsign) @@ -4660,6 +4790,11 @@ do -- SET_CLIENT MClientInclude = MClientInclude and MClientCallsigns end + if self.Filter.Functions and MClientInclude then + local MClientFunc = self:_EvalFilterFunctions(MClient) + MClientInclude = MClientInclude and MClientFunc + end + end self:T2( MClientInclude ) return MClientInclude @@ -5253,7 +5388,7 @@ do -- SET_AIRBASE function SET_AIRBASE:GetRandomAirbase() local RandomAirbase = self:GetRandom() - self:F( { RandomAirbase = RandomAirbase:GetName() } ) + --self:F( { RandomAirbase = RandomAirbase:GetName() } ) return RandomAirbase end @@ -5419,7 +5554,7 @@ do -- SET_AIRBASE MAirbaseInclude = MAirbaseInclude and MAirbaseCoalition end - if self.Filter.Categories then + if self.Filter.Categories and MAirbaseInclude then local MAirbaseCategory = false for CategoryID, CategoryName in pairs( self.Filter.Categories ) do local AirbaseCategoryID = _DATABASE:GetCategoryFromAirbase( MAirbaseName ) @@ -7765,7 +7900,7 @@ do -- SET_OPSGROUP end -- Filter coalitions. - if self.Filter.Coalitions then + if self.Filter.Coalitions and MGroupInclude then local MGroupCoalition = false @@ -7779,7 +7914,7 @@ do -- SET_OPSGROUP end -- Filter categories. - if self.Filter.Categories then + if self.Filter.Categories and MGroupInclude then local MGroupCategory = false @@ -7793,7 +7928,7 @@ do -- SET_OPSGROUP end -- Filter countries. - if self.Filter.Countries then + if self.Filter.Countries and MGroupInclude then local MGroupCountry = false for CountryID, CountryName in pairs( self.Filter.Countries ) do if country.id[CountryName] == MGroup:GetCountry() then @@ -7804,12 +7939,12 @@ do -- SET_OPSGROUP end -- Filter "prefixes". - if self.Filter.GroupPrefixes then + if self.Filter.GroupPrefixes and MGroupInclude then local MGroupPrefix = false for GroupPrefixId, GroupPrefix in pairs( self.Filter.GroupPrefixes ) do - if string.find( MGroup:GetName(), GroupPrefix:gsub ("-", "%%-"), 1 ) then --Not sure why "-" is replaced by "%-" ?! + if string.find( MGroup:GetName(), GroupPrefix:gsub ("-", "%%-"), 1 ) then --Not sure why "-" is replaced by "%-" ?! - So we can still match group names with a dash in them MGroupPrefix = true end end diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index 09ef67c2f..3317df260 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -2020,7 +2020,7 @@ _ZONE_TRIANGLE = { Coords={}, CenterVec2={x=0, y=0}, SurfaceArea=0, - DrawIDs={} + DrawID={} } --- -- @param #_ZONE_TRIANGLE self @@ -2100,15 +2100,35 @@ function _ZONE_TRIANGLE:Draw(Coalition, Color, Alpha, FillColor, FillAlpha, Line for i=1, #self.Coords do local c1 = self.Coords[i] local c2 = self.Coords[i % #self.Coords + 1] - table.add(self.DrawIDs, c1:LineToAll(c2, Coalition, Color, Alpha, LineType, ReadOnly)) + local id = c1:LineToAll(c2, Coalition, Color, Alpha, LineType, ReadOnly) + self.DrawID[#self.DrawID+1] = id end - return self.DrawIDs + local newID = self.Coords[1]:MarkupToAllFreeForm({self.Coords[2],self.Coords[3]},Coalition,Color,Alpha,FillColor,FillAlpha,LineType,ReadOnly) + self.DrawID[#self.DrawID+1] = newID + return self.DrawID +end + +--- Draw the triangle +-- @param #_ZONE_TRIANGLE self +-- @return #table of draw IDs +function _ZONE_TRIANGLE:Fill(Coalition, FillColor, FillAlpha, ReadOnly) + Coalition=Coalition or -1 + FillColor = FillColor + FillAlpha = FillAlpha + local newID = self.Coords[1]:MarkupToAllFreeForm({self.Coords[2],self.Coords[3]},Coalition,nil,nil,FillColor,FillAlpha,0,nil) + self.DrawID[#self.DrawID+1] = newID + return self.DrawID end --- -- @type ZONE_POLYGON_BASE -- @field #ZONE_POLYGON_BASE.ListVec2 Polygon The polygon defined by an array of @{DCS#Vec2}. +-- @field #number SurfaceArea +-- @field #table DrawID +-- @field #table FillTriangles +-- @field #table _Triangles +-- @field #table Borderlines -- @extends #ZONE_BASE @@ -2133,9 +2153,11 @@ end -- @field #ZONE_POLYGON_BASE ZONE_POLYGON_BASE = { ClassName="ZONE_POLYGON_BASE", - _Triangles={}, -- _ZONE_TRIANGLES + _Triangles={}, -- #table of #_ZONE_TRIANGLE SurfaceArea=0, - DrawID={} -- making a table out of the MarkID so its easier to draw an n-sided polygon, see ZONE_POLYGON_BASE:Draw() + DrawID={}, -- making a table out of the MarkID so its easier to draw an n-sided polygon, see ZONE_POLYGON_BASE:Draw() + FillTriangles = {}, + Borderlines = {}, } --- A 2D points array. @@ -2172,7 +2194,7 @@ function ZONE_POLYGON_BASE:New( ZoneName, PointsArray ) self._Triangles = self:_Triangulate() -- set the polygon's surface area self.SurfaceArea = self:_CalculateSurfaceArea() - + end return self @@ -2470,57 +2492,113 @@ end -- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. -- doesn't seem to work -- @param #number FillAlpha Transparency [0,1]. Default 0.15. -- doesn't seem to work -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. --- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. +-- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false.s -- @return #ZONE_POLYGON_BASE self function ZONE_POLYGON_BASE:DrawZone(Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly, IncludeTriangles) - if self._.Polygon and #self._.Polygon >= 3 then - Coalition = Coalition or self:GetDrawCoalition() - -- Set draw coalition. - self:SetDrawCoalition(Coalition) - - Color = Color or self:GetColorRGB() - Alpha = Alpha or 1 - - -- Set color. - self:SetColor(Color, Alpha) - - FillColor = FillColor or self:GetFillColorRGB() - if not FillColor then - UTILS.DeepCopy(Color) - end - FillAlpha = FillAlpha or self:GetFillColorAlpha() - if not FillAlpha then - FillAlpha = 0.15 - end - - -- Set fill color -----------> has fill color worked in recent versions of DCS? - -- doing something like - -- - -- trigger.action.markupToAll(7, -1, 501, p.Coords[1]:GetVec3(), p.Coords[2]:GetVec3(),p.Coords[3]:GetVec3(),p.Coords[4]:GetVec3(),{1,0,0, 1}, {1,0,0, 1}, 4, false, Text or "") - -- - -- doesn't seem to fill in the shape for an n-sided polygon - self:SetFillColor(FillColor, FillAlpha) - - IncludeTriangles = IncludeTriangles or false - - -- just draw the triangles, we get the outline for free - if IncludeTriangles then - for _, triangle in pairs(self._Triangles) do - local draw_ids = triangle:Draw() - table.combine(self.DrawID, draw_ids) - end - -- draw outline only - else - local coords = self:GetVerticiesCoordinates() - for i = 1, #coords do - local c1 = coords[i] - local c2 = coords[i % #coords + 1] - table.add(self.DrawID, c1:LineToAll(c2, Coalition, Color, Alpha, LineType, ReadOnly)) - end - end + + if self._.Polygon and #self._.Polygon >= 3 then + Coalition = Coalition or self:GetDrawCoalition() + + -- Set draw coalition. + self:SetDrawCoalition(Coalition) + + Color = Color or self:GetColorRGB() + Alpha = Alpha or self:GetColorAlpha() + + FillColor = FillColor or self:GetFillColorRGB() + FillAlpha = FillAlpha or self:GetFillColorAlpha() + + if FillColor then + self:ReFill(FillColor,FillAlpha) end - return self + + if Color then + self:ReDrawBorderline(Color,Alpha,LineType) + end + end + + + if false then + local coords = self:GetVerticiesCoordinates() + + local coord=coords[1] --Core.Point#COORDINATE + + table.remove(coords, 1) + + coord:MarkupToAllFreeForm(coords, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly, "Drew Polygon") + + if true then + return + end + end + + return self +end + +--- Change/Re-fill a Polygon Zone +-- @param #ZONE_POLYGON_BASE self +-- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red. +-- @param #number Alpha Transparency [0,1]. Default 1. +-- @return #ZONE_POLYGON_BASE self +function ZONE_POLYGON_BASE:ReFill(Color,Alpha) + local color = Color or self:GetFillColorRGB() or {1,0,0} + local alpha = Alpha or self:GetFillColorAlpha() or 1 + local coalition = self:GetDrawCoalition() or -1 + -- undraw if already filled + if #self.FillTriangles > 0 then + for _, triangle in pairs(self._Triangles) do + triangle:UndrawZone() + end + -- remove mark IDs + for _,_value in pairs(self.FillTriangles) do + table.remove_by_value(self.DrawID, _value) + end + self.FillTriangles = nil + self.FillTriangles = {} + end + -- refill + for _, triangle in pairs(self._Triangles) do + local draw_ids = triangle:Fill(coalition,color,alpha,nil) + self.FillTriangles = draw_ids + table.combine(self.DrawID, draw_ids) + end + return self +end + +--- Change/Re-draw the border of a Polygon Zone +-- @param #ZONE_POLYGON_BASE self +-- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red. +-- @param #number Alpha Transparency [0,1]. Default 1. +-- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. +-- @return #ZONE_POLYGON_BASE +function ZONE_POLYGON_BASE:ReDrawBorderline(Color, Alpha, LineType) + local color = Color or self:GetFillColorRGB() or {1,0,0} + local alpha = Alpha or self:GetFillColorAlpha() or 1 + local coalition = self:GetDrawCoalition() or -1 + local linetype = LineType or 1 + -- undraw if already drawn + if #self.Borderlines > 0 then + for _, MarkID in pairs(self.Borderlines) do + trigger.action.removeMark(MarkID) + end + -- remove mark IDs + for _,_value in pairs(self.Borderlines) do + table.remove_by_value(self.DrawID, _value) + end + self.Borderlines = nil + self.Borderlines = {} + end + -- Redraw border + local coords = self:GetVerticiesCoordinates() + for i = 1, #coords do + local c1 = coords[i] + local c2 = coords[i % #coords + 1] + local newID = c1:LineToAll(c2, coalition, color, alpha, linetype, nil) + self.DrawID[#self.DrawID+1]=newID + self.Borderlines[#self.Borderlines+1] = newID + end + return self end --- Get the surface area of this polygon @@ -2856,6 +2934,7 @@ function ZONE_POLYGON_BASE:Boundary(Coalition, Color, Radius, Alpha, Segments, C Alpha = Alpha or 1 Segments = Segments or 10 Closed = Closed or false + local Limit local i = 1 local j = #self._.Polygon if (Closed) then @@ -3054,18 +3133,18 @@ function ZONE_POLYGON:Scan( ObjectCategories, UnitCategories ) self.ScanData.Scenery = {} self.ScanData.SceneryTable = {} self.ScanData.Units = {} - + local vectors = self:GetBoundingSquare() - + local minVec3 = {x=vectors.x1, y=0, z=vectors.y1} local maxVec3 = {x=vectors.x2, y=0, z=vectors.y2} - + local minmarkcoord = COORDINATE:NewFromVec3(minVec3) local maxmarkcoord = COORDINATE:NewFromVec3(maxVec3) local ZoneRadius = minmarkcoord:Get2DDistance(maxmarkcoord)/2 -- self:I("Scan Radius:" ..ZoneRadius) local CenterVec3 = self:GetCoordinate():GetVec3() - + --[[ this a bit shaky in functionality it seems local VolumeBox = { id = world.VolumeType.BOX, @@ -3075,7 +3154,7 @@ function ZONE_POLYGON:Scan( ObjectCategories, UnitCategories ) } } --]] - + local SphereSearch = { id = world.VolumeType.SPHERE, params = { @@ -3083,13 +3162,13 @@ function ZONE_POLYGON:Scan( ObjectCategories, UnitCategories ) radius = ZoneRadius, } } - + local function EvaluateZone( ZoneObject ) if ZoneObject then local ObjectCategory = Object.getCategory(ZoneObject) - + if ( ObjectCategory == Object.Category.UNIT and ZoneObject:isExist() and ZoneObject:isActive() ) or (ObjectCategory == Object.Category.STATIC and ZoneObject:isExist()) then local CoalitionDCSUnit = ZoneObject:getCoalition() @@ -3123,7 +3202,7 @@ function ZONE_POLYGON:Scan( ObjectCategories, UnitCategories ) self:F2( { Name = ZoneObject:getName(), Coalition = CoalitionDCSUnit } ) end end - + -- trying with box search if ObjectCategory == Object.Category.SCENERY and self:IsVec3InZone(ZoneObject:getPoint()) then local SceneryType = ZoneObject:getTypeName() @@ -3142,7 +3221,7 @@ function ZONE_POLYGON:Scan( ObjectCategories, UnitCategories ) -- Search objects. local inzoneunits = SET_UNIT:New():FilterZones({self}):FilterOnce() local inzonestatics = SET_STATIC:New():FilterZones({self}):FilterOnce() - + inzoneunits:ForEach( function(unit) local Unit = unit --Wrapper.Unit#UNIT @@ -3150,7 +3229,7 @@ function ZONE_POLYGON:Scan( ObjectCategories, UnitCategories ) EvaluateZone(DCS) end ) - + inzonestatics:ForEach( function(static) local Static = static --Wrapper.Static#STATIC @@ -3158,19 +3237,19 @@ function ZONE_POLYGON:Scan( ObjectCategories, UnitCategories ) EvaluateZone(DCS) end ) - + local searchscenery = false for _,_type in pairs(ObjectCategories) do if _type == Object.Category.SCENERY then searchscenery = true end end - + if searchscenery then -- Search objects. world.searchObjects({Object.Category.SCENERY}, SphereSearch, EvaluateZone ) end - + end --- Count the number of different coalitions inside the zone. @@ -3386,7 +3465,7 @@ end end do -- ZONE_ELASTIC - + --- -- @type ZONE_ELASTIC -- @field #table points Points in 2D. @@ -3413,14 +3492,14 @@ do -- ZONE_ELASTIC function ZONE_ELASTIC:New(ZoneName, Points) local self=BASE:Inherit(self, ZONE_POLYGON_BASE:New(ZoneName, Points)) --#ZONE_ELASTIC - + -- Zone objects are added to the _DATABASE and SET_ZONE objects. _EVENTDISPATCHER:CreateEventNewZone( self ) - + if Points then self.points=Points end - + return self end @@ -3429,10 +3508,10 @@ do -- ZONE_ELASTIC -- @param DCS#Vec2 Vec2 Point in 2D (with x and y coordinates). -- @return #ZONE_ELASTIC self function ZONE_ELASTIC:AddVertex2D(Vec2) - + -- Add vec2 to points. table.insert(self.points, Vec2) - + return self end @@ -3442,10 +3521,10 @@ do -- ZONE_ELASTIC -- @param DCS#Vec3 Vec3 Point in 3D (with x, y and z coordinates). Only the x and z coordinates are used. -- @return #ZONE_ELASTIC self function ZONE_ELASTIC:AddVertex3D(Vec3) - + -- Add vec2 from vec3 to points. table.insert(self.points, {x=Vec3.x, y=Vec3.z}) - + return self end @@ -3455,10 +3534,10 @@ do -- ZONE_ELASTIC -- @param Core.Set#SET_GROUP GroupSet Set of groups. -- @return #ZONE_ELASTIC self function ZONE_ELASTIC:AddSetGroup(GroupSet) - + -- Add set to table. table.insert(self.setGroups, GroupSet) - + return self end @@ -3470,13 +3549,13 @@ do -- ZONE_ELASTIC -- @param #boolean Draw Draw the zone. Default `nil`. -- @return #ZONE_ELASTIC self function ZONE_ELASTIC:Update(Delay, Draw) - + -- Debug info. self:T(string.format("Updating ZONE_ELASTIC %s", tostring(self.ZoneName))) - + -- Copy all points. local points=UTILS.DeepCopy(self.points or {}) - + if self.setGroups then for _,_setGroup in pairs(self.setGroups) do local setGroup=_setGroup --Core.Set#SET_GROUP @@ -3491,7 +3570,7 @@ do -- ZONE_ELASTIC -- Update polygon verticies from points. self._.Polygon=self:_ConvexHull(points) - + if Draw~=false then if self.DrawID or Draw==true then self:UndrawZone() @@ -3501,7 +3580,7 @@ do -- ZONE_ELASTIC return self end - + --- Start the updating scheduler. -- @param #ZONE_ELASTIC self -- @param #number Tstart Time in seconds before the updating starts. @@ -3510,9 +3589,9 @@ do -- ZONE_ELASTIC -- @param #boolean Draw Draw the zone. Default `nil`. -- @return #ZONE_ELASTIC self function ZONE_ELASTIC:StartUpdate(Tstart, dT, Tstop, Draw) - + self.updateID=self:ScheduleRepeat(Tstart, dT, 0, Tstop, ZONE_ELASTIC.Update, self, 0, Draw) - + return self end @@ -3521,46 +3600,46 @@ do -- ZONE_ELASTIC -- @param #number Delay Delay in seconds before the scheduler will be stopped. Default 0. -- @return #ZONE_ELASTIC self function ZONE_ELASTIC:StopUpdate(Delay) - + if Delay and Delay>0 then self:ScheduleOnce(Delay, ZONE_ELASTIC.StopUpdate, self) else - + if self.updateID then - + self:ScheduleStop(self.updateID) - + self.updateID=nil - + end - + end - + return self end - + --- Create a convec hull. -- @param #ZONE_ELASTIC self -- @param #table pl Points -- @return #table Points function ZONE_ELASTIC:_ConvexHull(pl) - + if #pl == 0 then return {} end - + table.sort(pl, function(left,right) return left.x < right.x end) - + local h = {} - + -- Function: ccw > 0 if three points make a counter-clockwise turn, clockwise if ccw < 0, and collinear if ccw = 0. local function ccw(a,b,c) return (b.x - a.x) * (c.y - a.y) > (b.y - a.y) * (c.x - a.x) end - + -- lower hull for i,pt in pairs(pl) do while #h >= 2 and not ccw(h[#h-1], h[#h], pt) do @@ -3568,7 +3647,7 @@ do -- ZONE_ELASTIC end table.insert(h,pt) end - + -- upper hull local t = #h + 1 for i=#pl, 1, -1 do @@ -3578,12 +3657,12 @@ do -- ZONE_ELASTIC end table.insert(h, pt) end - + table.remove(h, #h) - + return h - end - + end + end @@ -3610,13 +3689,17 @@ ZONE_OVAL = { --- Creates a new ZONE_OVAL from a center point, major axis, minor axis, and angle. --- ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Oval.lua +-- @param #ZONE_OVAL self +-- @param #string name Name of the zone. -- @param #table vec2 The center point of the oval -- @param #number major_axis The major axis of the oval -- @param #number minor_axis The minor axis of the oval -- @param #number angle The angle of the oval -- @return #ZONE_OVAL The new oval function ZONE_OVAL:New(name, vec2, major_axis, minor_axis, angle) + self = BASE:Inherit(self, ZONE_BASE:New()) + self.ZoneName = name self.CenterVec2 = vec2 self.MajorAxis = major_axis @@ -3654,7 +3737,7 @@ function ZONE_OVAL:NewFromDrawing(DrawingName) return self end ---- Gets the major axis of the oval. +--- Gets the major axis of the oval. -- @param #ZONE_OVAL self -- @return #number The major axis of the oval function ZONE_OVAL:GetMajorAxis() @@ -3850,7 +3933,7 @@ do -- ZONE_AIRBASE self._.ZoneAirbase = Airbase self._.ZoneVec2Cache = self._.ZoneAirbase:GetVec2() - + if Airbase:IsShip() then self.isShip=true self.isHelipad=false @@ -3858,11 +3941,11 @@ do -- ZONE_AIRBASE elseif Airbase:IsHelipad() then self.isShip=false self.isHelipad=true - self.isAirdrome=false + self.isAirdrome=false elseif Airbase:IsAirdrome() then self.isShip=false self.isHelipad=false - self.isAirdrome=true + self.isAirdrome=true end -- Zone objects are added to the _DATABASE and SET_ZONE objects. diff --git a/Moose Development/Moose/Functional/AICSAR.lua b/Moose Development/Moose/Functional/AICSAR.lua index 5f158932f..322959dea 100644 --- a/Moose Development/Moose/Functional/AICSAR.lua +++ b/Moose Development/Moose/Functional/AICSAR.lua @@ -606,8 +606,10 @@ function AICSAR:SetPilotTTSVoice(Voice,Culture,Gender) self.SRSPilot:SetCulture(Culture or "en-US") self.SRSPilot:SetGender(Gender or "male") self.SRSPilot:SetLabel("PILOT") - if self.SRS.google then - self.SRSPilot:SetGoogle(self.SRS.google) + if self.SRSGoogle then + local poptions = self.SRS:GetProviderOptions(MSRS.Provider.GOOGLE) -- Sound.SRS#MSRS.ProviderOptions + self.SRSPilot:SetGoogle(poptions.credentials) + self.SRSPilot:SetGoogleAPIKey(poptions.key) end return self end @@ -627,9 +629,11 @@ function AICSAR:SetOperatorTTSVoice(Voice,Culture,Gender) self.SRSOperator:SetVoice(Voice) self.SRSOperator:SetCulture(Culture or "en-GB") self.SRSOperator:SetGender(Gender or "female") - self.SRSPilot:SetLabel("RESCUE") - if self.SRS.google then - self.SRSOperator:SetGoogle(self.SRS.google) + self.SRSOperator:SetLabel("RESCUE") + if self.SRSGoogle then + local poptions = self.SRS:GetProviderOptions(MSRS.Provider.GOOGLE) -- Sound.SRS#MSRS.ProviderOptions + self.SRSOperator:SetGoogle(poptions.credentials) + self.SRSOperator:SetGoogleAPIKey(poptions.key) end return self end diff --git a/Moose Development/Moose/Functional/ATC_Ground.lua b/Moose Development/Moose/Functional/ATC_Ground.lua index fc65fcce6..cdb90e712 100644 --- a/Moose Development/Moose/Functional/ATC_Ground.lua +++ b/Moose Development/Moose/Functional/ATC_Ground.lua @@ -10,9 +10,7 @@ -- -- === -- --- ## Missions: --- --- [ABP - Airbase Police](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/ABP%20-%20Airbase%20Police) +-- ## Missions: None -- -- === -- @@ -699,7 +697,8 @@ end function ATC_GROUND_UNIVERSAL:_AirbaseMonitor() self:I("_AirbaseMonitor") self.SetClient:ForEachClient( - --- @param Wrapper.Client#CLIENT Client + --- Nameless function + -- @param Wrapper.Client#CLIENT Client function( Client ) if Client:IsAlive() then diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index a71e194ed..edafc2eb6 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -95,7 +95,7 @@ do -- DETECTION_BASE -- -- ## Radar Blur - use to make the radar less exact, e.g. for WWII scenarios -- - -- * @{DETECTION_BASE.SetRadarBlur}(): Set the radar blur to be used. + -- * @{#DETECTION_BASE.SetRadarBlur}(): Set the radar blur to be used. -- -- ## **DETECTION_ derived classes** group the detected units into a **DetectedItems[]** list -- diff --git a/Moose Development/Moose/Functional/Mantis.lua b/Moose Development/Moose/Functional/Mantis.lua index ce95806b1..a9c4ddd79 100644 --- a/Moose Development/Moose/Functional/Mantis.lua +++ b/Moose Development/Moose/Functional/Mantis.lua @@ -22,7 +22,7 @@ -- @module Functional.Mantis -- @image Functional.Mantis.jpg -- --- Last Update: Nov 2023 +-- Last Update: Dec 2023 ------------------------------------------------------------------------- --- **MANTIS** class, extends Core.Base#BASE @@ -94,7 +94,7 @@ -- Known SAM types at the time of writing are: -- -- * Avenger --- * Chaparrel +-- * Chaparral -- * Hawk -- * Linebacker -- * NASAMS @@ -365,7 +365,7 @@ MANTIS.SamData = { ["SA-15"] = { Range=11, Blindspot=0, Height=6, Type="Short", Radar="Tor 9A331" }, ["SA-13"] = { Range=5, Blindspot=0, Height=3, Type="Short", Radar="Strela" }, ["Avenger"] = { Range=4, Blindspot=0, Height=3, Type="Short", Radar="Avenger" }, - ["Chaparrel"] = { Range=8, Blindspot=0, Height=3, Type="Short", Radar="Chaparral" }, + ["Chaparral"] = { Range=8, Blindspot=0, Height=3, Type="Short", Radar="Chaparral" }, ["Linebacker"] = { Range=4, Blindspot=0, Height=3, Type="Short", Radar="Linebacker" }, ["Silkworm"] = { Range=90, Blindspot=1, Height=0.2, Type="Long", Radar="Silkworm" }, -- units from HDS Mod, multi launcher options is tricky @@ -631,7 +631,7 @@ do -- TODO Version -- @field #string version - self.version="0.8.15" + self.version="0.8.16" self:I(string.format("***** Starting MANTIS Version %s *****", self.version)) --- FSM Functions --- @@ -1149,7 +1149,7 @@ do --self:T(self.lid.." Relocating HQ") local text = self.lid.." Relocating HQ" --local m= MESSAGE:New(text,10,"MANTIS"):ToAll() - _hqgrp:RelocateGroundRandomInRadius(20,500,true,true) + _hqgrp:RelocateGroundRandomInRadius(20,500,true,true,nil,true) end --relocate EWR -- TODO: maybe dependent on AlarmState? Observed: SA11 SR only relocates if no objects in reach @@ -1163,7 +1163,7 @@ do local text = self.lid.." Relocating EWR ".._grp:GetName() local m= MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) if self.verbose then self:I(text) end - _grp:RelocateGroundRandomInRadius(20,500,true,true) + _grp:RelocateGroundRandomInRadius(20,500,true,true,nil,true) end end end diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 57a0783da..ae6408d76 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -170,7 +170,7 @@ -- -- * A specific departure and/or destination airport can be chosen. -- * Valid coalitions can be set, e.g. only red, blue or neutral, all three "colours". --- * It is possible to start in air within a zone defined in the mission editor or within a zone above an airport of the map. +-- * It is possible to start in air within a zone or within a zone above an airport of the map. -- -- ## Flight Plan -- @@ -1179,13 +1179,13 @@ function RAT:SetTakeoffAir() return self end ---- Set possible departure ports. This can be an airport or a zone defined in the mission editor. +--- Set possible departure ports. This can be an airport or a zone. -- @param #RAT self -- @param #string departurenames Name or table of names of departure airports or zones. -- @return #RAT RAT self object. -- @usage RAT:SetDeparture("Sochi-Adler") will spawn RAT objects at Sochi-Adler airport. -- @usage RAT:SetDeparture({"Sochi-Adler", "Gudauta"}) will spawn RAT aircraft radomly at Sochi-Adler or Gudauta airport. --- @usage RAT:SetDeparture({"Zone A", "Gudauta"}) will spawn RAT aircraft in air randomly within Zone A, which has to be defined in the mission editor, or within a zone around Gudauta airport. Note that this also requires RAT:takeoff("air") to be set. +-- @usage RAT:SetDeparture({"Zone A", "Gudauta"}) will spawn RAT aircraft in air randomly within Zone A, or within a zone around Gudauta airport. Note that this also requires RAT:takeoff("air") to be set. function RAT:SetDeparture(departurenames) self:F2(departurenames) @@ -2537,7 +2537,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) end elseif self:_ZoneExists(_departure) then -- If it's not an airport, check whether it's a zone. - departure=ZONE:New(_departure) + departure=ZONE:FindByName(_departure) else local text=string.format("ERROR! Specified departure airport %s does not exist for %s.", _departure, self.alias) self:E(RAT.id..text) @@ -2635,7 +2635,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) end elseif self:_ZoneExists(_destination) then - destination=ZONE:New(_destination) + destination=ZONE:FindByName(_destination) else local text=string.format("ERROR: Specified destination airport/zone %s does not exist for %s!", _destination, self.alias) self:E(RAT.id.."ERROR: "..text) @@ -3142,7 +3142,7 @@ function RAT:_PickDeparture(takeoff) end elseif self:_ZoneExists(name) then if takeoff==RAT.wp.air then - dep=ZONE:New(name) + dep=ZONE:FindByName(name) else self:E(RAT.id..string.format("ERROR! Takeoff is not in air. Cannot use %s as departure.", name)) end @@ -3254,7 +3254,7 @@ function RAT:_PickDestination(departure, q, minrange, maxrange, random, landing) end elseif self:_ZoneExists(name) then if landing==RAT.wp.air then - dest=ZONE:New(name) + dest=ZONE:FindByName(name) else self:E(RAT.id..string.format("ERROR! Landing is not in air. Cannot use zone %s as destination!", name)) end @@ -4930,12 +4930,12 @@ function RAT:_AirportExists(name) return false end ---- Test if a trigger zone defined in the mission editor exists. +--- Test if a zone exists. -- @param #RAT self -- @param #string name -- @return #boolean True if zone exsits, false otherwise. function RAT:_ZoneExists(name) - local z=trigger.misc.getZone(name) + local z=ZONE:FindByName(name) --trigger.misc.getZone(name) as suggested by @Viking on MOOSE discord #rat if z then return true end diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index 8da132f14..5b4d52623 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -1737,7 +1737,9 @@ end -- @param Core.Event#EVENTDATA EventData function RANGE:OnEventBirth( EventData ) self:F( { eventbirth = EventData } ) - + + if not EventData.IniPlayerName then return end + local _unitName = EventData.IniUnitName local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) diff --git a/Moose Development/Moose/Functional/Sead.lua b/Moose Development/Moose/Functional/Sead.lua index 07fb312c5..4c02b7b7f 100644 --- a/Moose Development/Moose/Functional/Sead.lua +++ b/Moose Development/Moose/Functional/Sead.lua @@ -17,9 +17,9 @@ -- -- === -- --- ### Authors: **FlightControl**, **applevangelist** +-- ### Authors: **applevangelist**, **FlightControl** -- --- Last Update: Oct 2023 +-- Last Update: Dec 2023 -- -- === -- @@ -144,7 +144,7 @@ function SEAD:New( SEADGroupPrefixes, Padding ) self:AddTransition("*", "ManageEvasion", "*") self:AddTransition("*", "CalculateHitZone", "*") - self:I("*** SEAD - Started Version 0.4.5") + self:I("*** SEAD - Started Version 0.4.6") return self end @@ -401,7 +401,7 @@ function SEAD:onafterManageEvasion(From,Event,To,_targetskill,_targetgroup,SEADP grp:EnableEmission(false) end grp:OptionAlarmStateGreen() -- needed else we cannot move around - grp:RelocateGroundRandomInRadius(20,300,false,false,"Diamond") + grp:RelocateGroundRandomInRadius(20,300,false,false,"Diamond",true) if self.UseCallBack then local object = self.CallBack object:SeadSuppressionStart(grp,name,attacker) diff --git a/Moose Development/Moose/Functional/Tiresias.lua b/Moose Development/Moose/Functional/Tiresias.lua new file mode 100644 index 000000000..078bfda56 --- /dev/null +++ b/Moose Development/Moose/Functional/Tiresias.lua @@ -0,0 +1,590 @@ +--- **Functional** - TIRESIAS - manages AI behaviour. +-- +-- === +-- +-- The @{#TIRESIAS} class is working in the back to keep your large-scale ground units in check. +-- +-- ## Features: +-- +-- * Designed to keep CPU and Network usage lower on missions with a lot of ground units. +-- * Does not affect ships to keep the Navy guys happy. +-- * Does not affect OpsGroup type groups. +-- * Distinguishes between SAM groups, AAA groups and other ground groups. +-- * Exceptions can be defined to keep certain actions going. +-- * Works coalition-independent in the back +-- * Easy setup. +-- +-- === +-- +-- ## Missions: +-- +-- ### [TIRESIAS](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master) +-- +-- === +-- +-- ### Author : **applevangelist ** +-- +-- @module Functional.Tiresias +-- @image Functional.Tiresias.jpg +-- +-- Last Update: Dec 2023 + +------------------------------------------------------------------------- +--- **TIRESIAS** class, extends Core.Base#BASE +-- @type TIRESIAS +-- @field #string ClassName +-- @field #booelan debug +-- @field #string version +-- @field #number Interval +-- @field Core.Set#SET_GROUP GroundSet +-- @field #number Coalition +-- @field Core.Set#SET_GROUP VehicleSet +-- @field Core.Set#SET_GROUP AAASet +-- @field Core.Set#SET_GROUP SAMSet +-- @field Core.Set#SET_GROUP ExceptionSet +-- @field Core.Set#SET_OPSGROUP OpsGroupSet +-- @field #number AAARange +-- @field #number HeloSwitchRange +-- @field #number PlaneSwitchRange +-- @field Core.Set#SET_GROUP FlightSet +-- @field #boolean SwitchAAA +-- @extends Core.Fsm#FSM + +--- +-- @type TIRESIAS.Data +-- @field #string type +-- @field #number range +-- @field #boolean invisible +-- @field #boolean AIOff +-- @field #boolean exception + + +--- *Tiresias, Greek demi-god and shapeshifter, blinded by the Gods, works as oracle for you.* (Wiki) +-- +-- === +-- +-- ## TIRESIAS Concept +-- +-- * Designed to keep CPU and Network usage lower on missions with a lot of ground units. +-- * Does not affect ships to keep the Navy guys happy. +-- * Does not affect OpsGroup type groups. +-- * Distinguishes between SAM groups, AAA groups and other ground groups. +-- * Exceptions can be defined in SET_GROUP objects to keep certain actions going. +-- * Works coalition-independent in the back +-- * Easy setup. +-- +-- ## Setup +-- +-- Setup is a one-liner: +-- +-- local blinder = TIRESIAS:New() +-- +-- Optionally you can set up exceptions, e.g. for convoys driving around +-- +-- local exceptionset = SET_GROUP:New():FilterCoalitions("red"):FilterPrefixes("Convoy"):FilterStart() +-- local blinder = TIRESIAS:New() +-- blinder:AddExceptionSet(exceptionset) +-- +-- Options +-- +-- -- Setup different radius for activation around helo and airplane groups (applies to AI and humans) +-- blinder:SetActivationRanges(10,25) -- defaults are 10, and 25 +-- +-- -- Setup engagement ranges for AAA (non-advanced SAM units like Flaks etc) and if you want them to be AIOff +-- blinder:SetAAARanges(60,true) -- defaults are 60, and true +-- +-- @field #TIRESIAS +TIRESIAS = { + ClassName = "TIRESIAS", + debug = false, + version = "0.0.4", + Interval = 20, + GroundSet = nil, + VehicleSet = nil, + AAASet = nil, + SAMSet = nil, + ExceptionSet = nil, + AAARange = 60, -- 60% + HeloSwitchRange = 10, -- NM + PlaneSwitchRange = 25, -- NM + SwitchAAA = true, +} + +--- [USER] Create a new Tiresias object and start it up. +-- @param #TIRESIAS self +-- @return #TIRESIAS self +function TIRESIAS:New() + + -- Inherit everything from FSM class. + local self = BASE:Inherit(self, FSM:New()) -- #TIRESIAS + + --- FSM Functions --- + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start FSM. + self:AddTransition("*", "Status", "*") -- TIRESIAS status update. + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + + self.ExceptionSet = SET_GROUP:New():Clear(false) + + self:HandleEvent(EVENTS.PlayerEnterAircraft,self._EventHandler) + + self.lid = string.format("TIRESIAS %s | ",self.version) + + self:I(self.lid.."Managing ground groups!") + + --- Triggers the FSM event "Stop". Stops TIRESIAS and all its event handlers. + -- @function [parent=#TIRESIAS] Stop + -- @param #TIRESIAS self + + --- Triggers the FSM event "Stop" after a delay. Stops TIRESIAS and all its event handlers. + -- @function [parent=#TIRESIAS] __Stop + -- @param #TIRESIAS self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Start". Starts TIRESIAS and all its event handlers. Note - `:New()` already starts the instance. + -- @function [parent=#TIRESIAS] Start + -- @param #TIRESIAS self + + --- Triggers the FSM event "Start" after a delay. Starts TIRESIAS and all its event handlers. Note - `:New()` already starts the instance. + -- @function [parent=#TIRESIAS] __Start + -- @param #TIRESIAS self + -- @param #number delay Delay in seconds. + + self:__Start(1) + return self +end + +------------------------------------------------------------------------------------------------------------- +-- +-- Helper Functions +-- +------------------------------------------------------------------------------------------------------------- + +---[USER] Set activation radius for Helos and Planes in Nautical Miles. +-- @param #TIRESIAS self +-- @param #number HeloMiles Radius around a Helicopter in which AI ground units will be activated. Defaults to 10NM. +-- @param #number PlaneMiles Radius around an Airplane in which AI ground units will be activated. Defaults to 25NM. +-- @return #TIRESIAS self +function TIRESIAS:SetActivationRanges(HeloMiles,PlaneMiles) + self.HeloSwitchRange = HeloMiles or 10 + self.PlaneSwitchRange = PlaneMiles or 25 + return self +end + +---[USER] Set AAA Ranges - AAA equals non-SAM systems which qualify as AAA in DCS world. +-- @param #TIRESIAS self +-- @param #number FiringRange The engagement range that AAA units will be set to. Can be 0 to 100 (percent). Defaults to 60. +-- @param #boolean SwitchAAA Decide if these system will have their AI switched off, too. Defaults to true. +-- @return #TIRESIAS self +function TIRESIAS:SetAAARanges(FiringRange,SwitchAAA) + self.AAARange = FiringRange or 60 + self.SwitchAAA = (SwitchAAA == false) and false or true + return self +end + +--- [USER] Add a SET_GROUP of GROUP objects as exceptions. Can be done multiple times. +-- @param #TIRESIAS self +-- @param Core.Set#SET_GROUP Set to add to the exception list. +-- @return #TIRESIAS self +function TIRESIAS:AddExceptionSet(Set) + self:T(self.lid.."AddExceptionSet") + local exceptions = self.ExceptionSet + Set:ForEachGroupAlive( + function(grp) + if not grp.Tiresias then + grp.Tiresias = { -- #TIRESIAS.Data + type = "Exception", + exception = true, + } + exceptions:AddGroup(grp,true) + end + BASE:I("TIRESIAS: Added exception group: "..grp:GetName()) + end + ) + return self +end + +--- [INTERNAL] Filter Function +-- @param Wrapper.Group#GROUP Group +-- @return #boolean isin +function TIRESIAS._FilterNotAAA(Group) + local grp = Group -- Wrapper.Group#GROUP + local isaaa = grp:IsAAA() + if isaaa == true and grp:IsGround() and not grp:IsShip() then + return false -- remove from SET + else + return true -- keep in SET + end +end + +--- [INTERNAL] Filter Function +-- @param Wrapper.Group#GROUP Group +-- @return #boolean isin +function TIRESIAS._FilterNotSAM(Group) + local grp = Group -- Wrapper.Group#GROUP + local issam = grp:IsSAM() + if issam == true and grp:IsGround() and not grp:IsShip() then + return false -- remove from SET + else + return true -- keep in SET + end +end + +--- [INTERNAL] Filter Function +-- @param Wrapper.Group#GROUP Group +-- @return #boolean isin +function TIRESIAS._FilterAAA(Group) + local grp = Group -- Wrapper.Group#GROUP + local isaaa = grp:IsAAA() + if isaaa == true and grp:IsGround() and not grp:IsShip() then + return true -- remove from SET + else + return false -- keep in SET + end +end + +--- [INTERNAL] Filter Function +-- @param Wrapper.Group#GROUP Group +-- @return #boolean isin +function TIRESIAS._FilterSAM(Group) + local grp = Group -- Wrapper.Group#GROUP + local issam = grp:IsSAM() + if issam == true and grp:IsGround() and not grp:IsShip() then + return true -- remove from SET + else + return false -- keep in SET + end +end + +--- [INTERNAL] Init Groups +-- @param #TIRESIAS self +-- @return #TIRESIAS self +function TIRESIAS:_InitGroups() + self:T(self.lid.."_InitGroups") + -- Set all groups invisible/motionless + local EngageRange = self.AAARange + local SwitchAAA = self.SwitchAAA + --- AAA + self.AAASet:ForEachGroupAlive( + function(grp) + if not grp.Tiresias then + grp:OptionEngageRange(EngageRange) + grp:SetCommandInvisible(true) + if SwitchAAA then + grp:SetAIOff() + grp:EnableEmission(false) + end + grp.Tiresias = { -- #TIRESIAS.Data + type = "AAA", + invisible = true, + range = EngageRange, + exception = false, + AIOff = SwitchAAA, + } + end + if grp.Tiresias and (not grp.Tiresias.exception == true) then + if grp.Tiresias.invisible and grp.Tiresias.invisible == false then + grp:SetCommandInvisible(true) + grp.Tiresias.invisible = true + if SwitchAAA then + grp:SetAIOff() + grp:EnableEmission(false) + grp.Tiresias.AIOff = true + end + end + end + --BASE:I(string.format("Init/Switch off AAA %s (Exception %s)",grp:GetName(),tostring(grp.Tiresias.exception))) + end + ) + --- Vehicles + self.VehicleSet:ForEachGroupAlive( + function(grp) + if not grp.Tiresias then + grp:SetAIOff() + grp:SetCommandInvisible(true) + grp.Tiresias = { -- #TIRESIAS.Data + type = "Vehicle", + invisible = true, + AIOff = true, + exception = false, + } + end + if grp.Tiresias and (not grp.Tiresias.exception == true) then + if grp.Tiresias and grp.Tiresias.invisible and grp.Tiresias.invisible == false then + grp:SetCommandInvisible(true) + grp:SetAIOff() + grp.Tiresias.invisible = true + end + end + --BASE:I(string.format("Init/Switch off Vehicle %s (Exception %s)",grp:GetName(),tostring(grp.Tiresias.exception))) + end + ) + --- SAM + self.SAMSet:ForEachGroupAlive( + function(grp) + if not grp.Tiresias then + grp:SetCommandInvisible(true) + grp.Tiresias = { -- #TIRESIAS.Data + type = "SAM", + invisible = true, + exception = false, + } + end + if grp.Tiresias and (not grp.Tiresias.exception == true) then + if grp.Tiresias and grp.Tiresias.invisible and grp.Tiresias.invisible == false then + grp:SetCommandInvisible(true) + grp.Tiresias.invisible = true + end + end + --BASE:I(string.format("Init/Switch off SAM %s (Exception %s)",grp:GetName(),tostring(grp.Tiresias.exception))) + end + ) + return self +end + +--- [INTERNAL] Event handler function +-- @param #TIRESIAS self +-- @param Core.Event#EVENTDATA EventData +-- @return #TIRESIAS self +function TIRESIAS:_EventHandler(EventData) + self:T(string.format("%s Event = %d",self.lid, EventData.id)) + local event = EventData -- Core.Event#EVENTDATA + if event.id == EVENTS.PlayerEnterAircraft or event.id == EVENTS.PlayerEnterUnit then + --local _coalition = event.IniCoalition + --if _coalition ~= self.Coalition then + -- return --ignore! + --end + local unitname = event.IniUnitName or "none" + local _unit = event.IniUnit + local _group = event.IniGroup + if _group and _group:IsAlive() then + local radius = self.PlaneSwitchRange + if _group:IsHelicopter() then + radius = self.HeloSwitchRange + end + self:_SwitchOnGroups(_group,radius) + end + end + return self +end + +--- [INTERNAL] Switch Groups Behaviour +-- @param #TIRESIAS self +-- @param Wrapper.Group#GROUP group +-- @param #number radius Radius in NM +-- @return #TIRESIAS self +function TIRESIAS:_SwitchOnGroups(group,radius) + self:T(self.lid.."_SwitchOnGroups "..group:GetName().." Radius "..radius.." NM") + local zone = ZONE_GROUP:New("Zone-"..group:GetName(),group,UTILS.NMToMeters(radius)) + local ground = SET_GROUP:New():FilterCategoryGround():FilterZones({zone}):FilterOnce() + local count = ground:CountAlive() + if self.debug then + local text = string.format("There are %d groups around this plane or helo!",count) + self:I(text) + end + local SwitchAAA = self.SwitchAAA + if ground:CountAlive() > 0 then + ground:ForEachGroupAlive( + function(grp) + if grp.Tiresias and grp.Tiresias.type and (not grp.Tiresias.exception == true ) then + if grp.Tiresias.invisible == true then + grp:SetCommandInvisible(false) + grp.Tiresias.invisible = false + end + if grp.Tiresias.type == "Vehicle" and grp.Tiresias.AIOff and grp.Tiresias.AIOff == true then + grp:SetAIOn() + grp.Tiresias.AIOff = false + end + if SwitchAAA and grp.Tiresias.type == "AAA" and grp.Tiresias.AIOff and grp.Tiresias.AIOff == true then + grp:SetAIOn() + grp:EnableEmission(true) + grp.Tiresias.AIOff = false + end + --BASE:I(string.format("TIRESIAS - Switch on %s %s (Exception %s)",tostring(grp.Tiresias.type),grp:GetName(),tostring(grp.Tiresias.exception))) + else + BASE:E("TIRESIAS - This group has not been initialized or is an exception!") + end + end + ) + end + return self +end + +------------------------------------------------------------------------------------------------------------- +-- +-- FSM Functions +-- +------------------------------------------------------------------------------------------------------------- + +--- [INTERNAL] FSM Function +-- @param #TIRESIAS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #TIRESIAS self +function TIRESIAS:onafterStart(From, Event, To) + self:T({From, Event, To}) + + local VehicleSet = SET_GROUP:New():FilterCategoryGround():FilterFunction(TIRESIAS._FilterNotAAA):FilterFunction(TIRESIAS._FilterNotSAM):FilterStart() + local AAASet = SET_GROUP:New():FilterCategoryGround():FilterFunction(TIRESIAS._FilterAAA):FilterStart() + local SAMSet = SET_GROUP:New():FilterCategoryGround():FilterFunction(TIRESIAS._FilterSAM):FilterStart() + local OpsGroupSet = SET_OPSGROUP:New():FilterActive(true):FilterStart() + self.FlightSet = SET_GROUP:New():FilterCategories({"plane","helicopter"}):FilterStart() + + local EngageRange = self.AAARange + + local ExceptionSet = self.ExceptionSet + if self.ExceptionSet then + function ExceptionSet:OnAfterAdded(From,Event,To,ObjectName,Object) + BASE:I("TIRESIAS: EXCEPTION Object Added: "..Object:GetName()) + if Object and Object:IsAlive() then + Object.Tiresias = { -- #TIRESIAS.Data + type = "Exception", + exception = true, + } + Object:SetAIOn() + Object:SetCommandInvisible(false) + Object:EnableEmission(true) + end + end + + local OGS = OpsGroupSet:GetAliveSet() + for _,_OG in pairs(OGS or {}) do + local OG = _OG -- Ops.OpsGroup#OPSGROUP + local grp = OG:GetGroup() + ExceptionSet:AddGroup(grp,true) + end + + function OpsGroupSet:OnAfterAdded(From,Event,To,ObjectName,Object) + local grp = Object:GetGroup() + ExceptionSet:AddGroup(grp,true) + end + end + + function VehicleSet:OnAfterAdded(From,Event,To,ObjectName,Object) + BASE:I("TIRESIAS: VEHCILE Object Added: "..Object:GetName()) + if Object and Object:IsAlive() then + Object:SetAIOff() + Object:SetCommandInvisible(true) + Object.Tiresias = { -- #TIRESIAS.Data + type = "Vehicle", + invisible = true, + AIOff = true, + exception = false, + } + end + end + + local SwitchAAA = self.SwitchAAA + + function AAASet:OnAfterAdded(From,Event,To,ObjectName,Object) + if Object and Object:IsAlive() then + BASE:I("TIRESIAS: AAA Object Added: "..Object:GetName()) + Object:OptionEngageRange(EngageRange) + Object:SetCommandInvisible(true) + if SwitchAAA then + Object:SetAIOff() + Object:EnableEmission(false) + end + Object.Tiresias = { -- #TIRESIAS.Data + type = "AAA", + invisible = true, + range = EngageRange, + exception = false, + AIOff = SwitchAAA, + } + end + end + + function SAMSet:OnAfterAdded(From,Event,To,ObjectName,Object) + if Object and Object:IsAlive() then + BASE:I("TIRESIAS: SAM Object Added: "..Object:GetName()) + Object:SetCommandInvisible(true) + Object.Tiresias = { -- #TIRESIAS.Data + type = "SAM", + invisible = true, + exception = false, + } + end + end + + self.VehicleSet = VehicleSet + self.AAASet = AAASet + self.SAMSet = SAMSet + self.OpsGroupSet = OpsGroupSet + + self:_InitGroups() + + self:__Status(1) + return self +end + +--- [INTERNAL] FSM Function +-- @param #TIRESIAS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #TIRESIAS self +function TIRESIAS:onbeforeStatus(From, Event, To) + self:T({From, Event, To}) + if self:GetState() == "Stopped" then + return false + end + return self +end + +--- [INTERNAL] FSM Function +-- @param #TIRESIAS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #TIRESIAS self +function TIRESIAS:onafterStatus(From, Event, To) + self:T({From, Event, To}) + if self.debug then + local count = self.VehicleSet:CountAlive() + local AAAcount = self.AAASet:CountAlive() + local SAMcount = self.SAMSet:CountAlive() + local text = string.format("Overall: %d | Vehicles: %d | AAA: %d | SAM: %d",count+AAAcount+SAMcount,count,AAAcount,SAMcount) + self:I(text) + end + self:_InitGroups() + if self.FlightSet:CountAlive() > 0 then + local Set = self.FlightSet:GetAliveSet() + for _,_plane in pairs(Set) do + local plane = _plane -- Wrapper.Group#GROUP + local radius = self.PlaneSwitchRange + if plane:IsHelicopter() then + radius = self.HeloSwitchRange + end + self:_SwitchOnGroups(_plane,radius) + end + end + if self:GetState() ~= "Stopped" then + self:__Status(self.Interval) + end + return self +end + +--- [INTERNAL] FSM Function +-- @param #TIRESIAS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #TIRESIAS self +function TIRESIAS:onafterStop(From, Event, To) + self:T({From, Event, To}) + self:UnHandleEvent(EVENTS.PlayerEnterAircraft) + return self +end + +------------------------------------------------------------------------------------------------------------- +-- +-- End +-- +------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index 99ca565a1..7ab4ad2e8 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -83,6 +83,7 @@ __Moose.Include( 'Scripts/Moose/Functional/ZoneCaptureCoalition.lua' ) __Moose.Include( 'Scripts/Moose/Functional/ZoneGoal.lua' ) __Moose.Include( 'Scripts/Moose/Functional/ZoneGoalCargo.lua' ) __Moose.Include( 'Scripts/Moose/Functional/ZoneGoalCoalition.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/Tiresias.lua' ) __Moose.Include( 'Scripts/Moose/Ops/Airboss.lua' ) __Moose.Include( 'Scripts/Moose/Ops/AirWing.lua' ) diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index 6819ee36c..064ab1880 100644 --- a/Moose Development/Moose/Ops/ATIS.lua +++ b/Moose Development/Moose/Ops/ATIS.lua @@ -890,7 +890,7 @@ _ATIS = {} --- ATIS class version. -- @field #string version -ATIS.version = "0.10.4" +ATIS.version = "1.0.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1528,12 +1528,12 @@ end --- Use SRS Simple-Text-To-Speech for transmissions. No sound files necessary. -- @param #ATIS self --- @param #string PathToSRS Path to SRS directory. +-- @param #string PathToSRS Path to SRS directory (only necessary if SRS exe backend is used). -- @param #string Gender Gender: "male" or "female" (default). -- @param #string Culture Culture, e.g. "en-GB" (default). -- @param #string Voice Specific voice. Overrides `Gender` and `Culture`. -- @param #number Port SRS port. Default 5002. --- @param #string GoogleKey Path to Google JSON-Key. +-- @param #string GoogleKey Path to Google JSON-Key (SRS exe backend) or Google API key (DCS-gRPC backend). -- @return #ATIS self function ATIS:SetSRS(PathToSRS, Gender, Culture, Voice, Port, GoogleKey) if PathToSRS or MSRS.path then diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index efdbfbbb7..c3c6c9f2a 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -8212,7 +8212,7 @@ function AIRBOSS:OnEventBirth( EventData ) self:E( EventData ) return end - if EventData.IniUnit == nil then + if EventData.IniUnit == nil and (not EventData.IniObjectCategory == Object.Category.STATIC) then self:E( self.lid .. "ERROR: EventData.IniUnit=nil in event BIRTH!" ) self:E( EventData ) return @@ -11197,7 +11197,7 @@ function AIRBOSS:_AttitudeMonitor( playerData ) end text = text .. string.format( "\nPitch=%.1f° | Roll=%.1f° | Yaw=%.1f°", pitch, roll, yaw ) text = text .. string.format( "\nClimb Angle=%.1f° | Rate=%d ft/min", unit:GetClimbAngle(), velo.y * 196.85 ) - local dist = self:_GetOptLandingCoordinate():Get3DDistance( playerData.unit ) + local dist = self:_GetOptLandingCoordinate():Get3DDistance( playerData.unit:GetVec3() ) -- Get player velocity in km/h. local vplayer = playerData.unit:GetVelocityKMH() -- Get carrier velocity in km/h. @@ -14957,7 +14957,7 @@ function AIRBOSS:SetSRSPilotVoice( Voice, Gender, Culture ) self.PilotRadio.gender = Gender or "male" self.PilotRadio.culture = Culture or "en-US" - if (not Voice) and self.SRS and self.SRS.google then + if (not Voice) and self.SRS and self.SRS:GetProvider() == MSRS.Provider.GOOGLE then self.PilotRadio.voice = MSRS.Voices.Google.Standard.en_US_Standard_J end diff --git a/Moose Development/Moose/Ops/ArmyGroup.lua b/Moose Development/Moose/Ops/ArmyGroup.lua index 3d105fa34..39c412181 100644 --- a/Moose Development/Moose/Ops/ArmyGroup.lua +++ b/Moose Development/Moose/Ops/ArmyGroup.lua @@ -1861,6 +1861,7 @@ function ARMYGROUP:_UpdateEngageTarget() else -- Could not get position of target (not alive any more?) ==> Disengage. + self:T(self.lid.."Could not get position of target ==> Disengage!") self:Disengage() end @@ -1868,6 +1869,7 @@ function ARMYGROUP:_UpdateEngageTarget() else -- Target not alive any more ==> Disengage. + self:T(self.lid.."Target not ALIVE ==> Disengage!") self:Disengage() end diff --git a/Moose Development/Moose/Ops/CSAR.lua b/Moose Development/Moose/Ops/CSAR.lua index becdf6ec3..ee2d58a9b 100644 --- a/Moose Development/Moose/Ops/CSAR.lua +++ b/Moose Development/Moose/Ops/CSAR.lua @@ -31,7 +31,7 @@ -- @image OPS_CSAR.jpg -- Date: May 2023 --- Last: Update Oct 2024 +-- Last: Update Dec 2024 ------------------------------------------------------------------------- --- **CSAR** class, extends Core.Base#BASE, Core.Fsm#FSM diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index 095eb28de..805be85cc 100644 --- a/Moose Development/Moose/Ops/CTLD.lua +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -1228,7 +1228,7 @@ CTLD.UnitTypeCapabilities = { --- CTLD class version. -- @field #string version -CTLD.version="1.0.44" +CTLD.version="1.0.45" --- Instantiate a new CTLD. -- @param #CTLD self @@ -1443,6 +1443,7 @@ function CTLD:New(Coalition, Prefixes, Alias) -- @param #number delay Delay in seconds. --- Triggers the FSM event "Stop". Stops the CTLD and all its event handlers. + -- @function [parent=#CTLD] Stop -- @param #CTLD self --- Triggers the FSM event "Stop" after a delay. Stops the CTLD and all its event handlers. @@ -2454,11 +2455,13 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack) realcargo = CTLD_CARGO:New(self.CargoCounter,cratename,templ,sorte,true,false,cratesneeded,self.Spawned_Crates[self.CrateCounter],true,cargotype.PerCrateMass,nil,subcat) table.insert(droppedcargo,realcargo) else - realcargo = CTLD_CARGO:New(self.CargoCounter,cratename,templ,sorte,false,false,cratesneeded,self.Spawned_Crates[self.CrateCounter],false,cargotype.PerCrateMass,nil,subcat) - Cargo:RemoveStock() + realcargo = CTLD_CARGO:New(self.CargoCounter,cratename,templ,sorte,false,false,cratesneeded,self.Spawned_Crates[self.CrateCounter],false,cargotype.PerCrateMass,nil,subcat) end table.insert(self.Spawned_Cargo, realcargo) end + if not (drop or pack) then + Cargo:RemoveStock() + end local text = string.format("Crates for %s have been positioned near you!",cratename) if drop then text = string.format("Crates for %s have been dropped!",cratename) @@ -3824,7 +3827,7 @@ end -- @param #CTLD_CARGO.Enum Type Type of cargo. I.e. VEHICLE or FOB. VEHICLE will move to destination zones when dropped/build, FOB stays put. -- @param #number NoCrates Number of crates needed to build this cargo. -- @param #number PerCrateMass Mass in kg of each crate --- @param #number Stock Number of groups in stock. Nil for unlimited. +-- @param #number Stock Number of buildable groups in stock. Nil for unlimited. -- @param #string SubCategory Name of sub-category (optional). function CTLD:AddCratesCargo(Name,Templates,Type,NoCrates,PerCrateMass,Stock,SubCategory) self:T(self.lid .. " AddCratesCargo") diff --git a/Moose Development/Moose/Ops/EasyGCICAP.lua b/Moose Development/Moose/Ops/EasyGCICAP.lua index 0d2ce7d99..34ec1301b 100644 --- a/Moose Development/Moose/Ops/EasyGCICAP.lua +++ b/Moose Development/Moose/Ops/EasyGCICAP.lua @@ -141,14 +141,14 @@ -- -- **Note** If you need different tanker types, i.e. Boom and Drogue, set them up at different AirWings! -- -- Add a tanker point -- mywing:AddPatrolPointTanker(AIRBASE.Caucasus.Kutaisi,ZONE:FindByName("Blue Zone Tanker"):GetCoordinate(),20000,280,270,50) --- -- Add an AWACS squad - Radio 251 AM, TACAN 51Y +-- -- Add a tanker squad - Radio 251 AM, TACAN 51Y -- mywing:AddTankerSquadron("Blue Tanker","Tanker Ops Kutaisi",AIRBASE.Caucasus.Kutaisi,20,AI.Skill.EXCELLENT,602,nil,251,radio.modulation.AM,51) -- -- ### Add an AWACS (optional) -- -- -- Add an AWACS point -- mywing:AddPatrolPointAwacs(AIRBASE.Caucasus.Kutaisi,ZONE:FindByName("Blue Zone AWACS"):GetCoordinate(),25000,300,270,50) --- -- Add a tanker squad - Radio 251 AM, TACAN 51Y +-- -- Add an AWACS squad - Radio 251 AM, TACAN 51Y -- mywing:AddAWACSSquadron("Blue AWACS","AWACS Ops Kutaisi",AIRBASE.Caucasus.Kutaisi,20,AI.Skill.AVERAGE,702,nil,271,radio.modulation.AM) -- -- # Fine-Tuning diff --git a/Moose Development/Moose/Ops/FlightControl.lua b/Moose Development/Moose/Ops/FlightControl.lua index 8403e0c24..112834e53 100644 --- a/Moose Development/Moose/Ops/FlightControl.lua +++ b/Moose Development/Moose/Ops/FlightControl.lua @@ -4424,14 +4424,11 @@ end -- Misc Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Add parking guard in front of a parking aircraft. +--- [INTERNAL] Add parking guard in front of a parking aircraft - delayed for MP. -- @param #FLIGHTCONTROL self -- @param Wrapper.Unit#UNIT unit The aircraft. -function FLIGHTCONTROL:SpawnParkingGuard(unit) - - if unit and self.parkingGuard then - - -- Position of the unit. +function FLIGHTCONTROL:_SpawnParkingGuard(unit) + -- Position of the unit. local coordinate=unit:GetCoordinate() -- Parking spot. @@ -4478,6 +4475,17 @@ function FLIGHTCONTROL:SpawnParkingGuard(unit) else self:E(self.lid.."ERROR: Parking Guard already exists!") end +end + +--- Add parking guard in front of a parking aircraft. +-- @param #FLIGHTCONTROL self +-- @param Wrapper.Unit#UNIT unit The aircraft. +function FLIGHTCONTROL:SpawnParkingGuard(unit) + + if unit and self.parkingGuard then + + -- Schedule delay so in MP we get the heading of the client's plane + self:ScheduleOnce(1,FLIGHTCONTROL._SpawnParkingGuard,self,unit) end diff --git a/Moose Development/Moose/Ops/Target.lua b/Moose Development/Moose/Ops/Target.lua index 4cdbbdbfa..c5b9ca558 100644 --- a/Moose Development/Moose/Ops/Target.lua +++ b/Moose Development/Moose/Ops/Target.lua @@ -526,11 +526,11 @@ function TARGET:IsAlive() for _,_target in pairs(self.targets) do local target=_target --Ops.Target#TARGET.Object - if target.Status==TARGET.ObjectStatus.ALIVE then + if target.Status~=TARGET.ObjectStatus.DEAD then return true end end - + return false end diff --git a/Moose Development/Moose/Sound/SRS.lua b/Moose Development/Moose/Sound/SRS.lua index aed01cb84..45dec9a27 100644 --- a/Moose Development/Moose/Sound/SRS.lua +++ b/Moose Development/Moose/Sound/SRS.lua @@ -4,8 +4,9 @@ -- -- **Main Features:** -- +-- * Incease immersion of your missions with more sound output -- * Play sound files via SRS --- * Play text-to-speach via SRS +-- * Play text-to-speech via SRS -- -- === -- @@ -13,7 +14,7 @@ -- -- === -- --- ## Missions: None yet +-- ## Example Missions: [GitHub](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Sound/MSRS). -- -- === -- @@ -37,20 +38,20 @@ -- @field #table modulations Modulations used in the transmissions. -- @field #number coalition Coalition of the transmission. -- @field #number port Port. Default 5002. --- @field #string name Name. Default "DCS-STTS". +-- @field #string name Name. Default "MSRS". -- @field #number volume Volume between 0 (min) and 1 (max). Default 1. -- @field #string culture Culture. Default "en-GB". -- @field #string gender Gender. Default "female". --- @field #string voice Specifc voce. +-- @field #string voice Specific voice. Only used if no explicit provider voice specified. -- @field Core.Point#COORDINATE coordinate Coordinate from where the transmission is send. --- @field #string path Path to the SRS exe. This includes the final slash "/". --- @field #string google Full path google credentials JSON file, e.g. "C:\Users\username\Downloads\service-account-file.json". +-- @field #string path Path to the SRS exe. -- @field #string Label Label showing up on the SRS radio overlay. Default is "ROBOT". No spaces allowed. --- @field #table AltBackend Table containing functions and variables to enable an alternate backend to transmit to SRS. --- @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 +-- @field #string ConfigFileName Name of the standard config file. +-- @field #string ConfigFilePath Path to the standard config file. +-- @field #boolean ConfigLoaded If `true` if config file was loaded. +-- @field #table poptions Provider options. Each element is a data structure of type `MSRS.ProvierOptions`. +-- @field #string provider Provider of TTS (win, gcloud, azure, amazon). +-- @field #string backend Backend used as interface to SRS (MSRS.Backend.SRSEXE or MSRS.Backend.GRPC). -- @extends Core.Base#BASE --- *It is a very sad thing that nowadays there is so little useless information.* - Oscar Wilde @@ -60,94 +61,151 @@ -- # The MSRS Concept -- -- This class allows to broadcast sound files or text via Simple Radio Standalone (SRS). --- +-- -- ## Prerequisites --- --- This script needs SRS version >= 1.9.6. --- +-- +-- * This script needs SRS version >= 1.9.6 +-- * You need to de-sanitize os, io and lfs in the missionscripting.lua +-- * Optional: DCS-gRPC as backend to communicate with SRS (vide infra) +-- +-- ## Knwon Issues +-- +-- ### Pop-up Window +-- +-- The text-to-speech conversion of SRS is done via an external exe file. When this file is called, a windows `cmd` window is briefly opended. That puts DCS out of focus, which is annoying, +-- expecially in VR but unavoidable (if you have a solution, please feel free to share!). +-- +-- NOTE that this is not an issue if the mission is running on a server. +-- Also NOTE that using DCS-gRPC as backend will avoid the pop-up window. +-- -- # Play Sound Files --- +-- -- local soundfile=SOUNDFILE:New("My Soundfile.ogg", "D:\\Sounds For DCS") -- local msrs=MSRS:New("C:\\Path To SRS", 251, radio.modulation.AM) -- msrs:PlaySoundFile(soundfile) --- +-- -- # Play Text-To-Speech --- +-- -- Basic example: --- +-- -- -- Create a SOUNDTEXT object. -- local text=SOUNDTEXT:New("All Enemies destroyed") --- --- -- MOOSE SRS +-- +-- -- MOOSE SRS -- local msrs=MSRS:New("D:\\DCS\\_SRS\\", 305, radio.modulation.AM) -- -- -- Text-to speech with default voice after 2 seconds. -- msrs:PlaySoundText(text, 2) -- -- ## Set Gender --- +-- -- Use a specific gender with the @{#MSRS.SetGender} function, e.g. `SetGender("male")` or `:SetGender("female")`. --- +-- -- ## Set Culture --- +-- -- Use a specific "culture" with the @{#MSRS.SetCulture} function, e.g. `:SetCulture("en-US")` or `:SetCulture("de-DE")`. --- --- ## Set Google --- --- Use Google's text-to-speech engine with the @{#MSRS.SetGoogle} function, e.g. ':SetGoogle()'. --- By enabling this it also allows you to utilize SSML in your text for added flexibility. --- For more information on setting up a cloud account, visit: https://cloud.google.com/text-to-speech --- Google's supported SSML reference: https://cloud.google.com/text-to-speech/docs/ssml --- --- --- **Pro-Tipp** - use the command line with power shell to call DCS-SR-ExternalAudio.exe - it will tell you what is missing. --- and also the Google Console error, in case you have missed a step in setting up your Google TTS. --- E.g. `.\DCS-SR-ExternalAudio.exe -t "Text Message" -f 255 -m AM -c 2 -s 2 -z -G "Path_To_You_Google.Json"` --- Plays a message on 255AM for the blue coalition in-game. --- +-- -- ## Set Voice --- +-- -- Use a specific voice with the @{#MSRS.SetVoice} function, e.g, `:SetVoice("Microsoft Hedda Desktop")`. -- Note that this must be installed on your windows system. --- If enabling SetGoogle(), you can use voices provided by Google --- Google's supported voices: https://cloud.google.com/text-to-speech/docs/voices +-- +-- Note that you can set voices for each provider via the @{#MSRS.SetVoiceProvider} function. Also shortcuts are available, *i.e.* +-- @{#MSRS.SetVoiceWindows}, @{#MSRS.SetVoiceGoogle}, @{#MSRS.SetVoiceAzure} and @{#MSRS.SetVoiceAmazon}. +-- -- For voices there are enumerators in this class to help you out on voice names: --- --- MSRS.Voices.Microsoft -- e.g. MSRS.Voices.Microsoft.Hedda - the Microsoft enumerator contains all voices known to work with SRS --- MSRS.Voices.Google -- e.g. MSRS.Voices.Google.Standard.en_AU_Standard_A or MSRS.Voices.Google.Wavenet.de_DE_Wavenet_C - The Google enumerator contains voices for EN, DE, IT, FR and ES. --- +-- +-- MSRS.Voices.Microsoft -- e.g. MSRS.Voices.Microsoft.Hedda - the Microsoft enumerator contains all voices known to work with SRS +-- MSRS.Voices.Google -- e.g. MSRS.Voices.Google.Standard.en_AU_Standard_A or MSRS.Voices.Google.Wavenet.de_DE_Wavenet_C - The Google enumerator contains voices for EN, DE, IT, FR and ES. +-- -- ## Set Coordinate --- +-- -- Use @{#MSRS.SetCoordinate} to define the origin from where the transmission is broadcasted. +-- Note that this is only a factor if SRS server has line-of-sight and/or distance limit enabled. -- -- ## Set SRS Port --- +-- -- Use @{#MSRS.SetPort} to define the SRS port. Defaults to 5002. --- +-- -- ## Set SRS Volume --- +-- -- 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. +-- +-- ## TTS Providers +-- +-- The default provider for generating speech from text is the native Windows TTS service. Note that you need to install the voices you want to use. -- --- ## Set DCS-gRPC as an alternative to 'DCS-SR-ExternalAudio.exe' for TTS +-- **Pro-Tip** - use the command line with power shell to call `DCS-SR-ExternalAudio.exe` - it will tell you what is missing +-- and also the Google Console error, in case you have missed a step in setting up your Google TTS. +-- For example, `.\DCS-SR-ExternalAudio.exe -t "Text Message" -f 255 -m AM -c 2 -s 2 -z -G "Path_To_You_Google.Json"` +-- plays a message on 255 MHz AM for the blue coalition in-game. +-- +-- ### Google +-- +-- In order to use Google Cloud for TTS you need to use @{#MSRS.SetProvider} and @{#MSRS.SetProviderOptionsGoogle} functions: +-- +-- msrs:SetProvider(MSRS.Provider.GOOGLE) +-- msrs:SetProviderOptionsGoogle(CredentialsFile, AccessKey) +-- +-- The parameter `CredentialsFile` is used with the default 'DCS-SR-ExternalAudio.exe' backend and must be the full path to the credentials JSON file. +-- The `AccessKey` parameter is used with the DCS-gRPC backend (see below). +-- +-- You can set the voice to use with Google via @{#MSRS.SetVoiceGoogle}. +-- +-- When using Google it also allows you to utilize SSML in your text for more flexibility. +-- For more information on setting up a cloud account, visit: https://cloud.google.com/text-to-speech +-- Google's supported SSML reference: https://cloud.google.com/text-to-speech/docs/ssml +-- +-- ### Amazon Web Service [Only DCS-gRPC backend] +-- +-- In order to use Amazon Web Service (AWS) for TTS you need to use @{#MSRS.SetProvider} and @{#MSRS.SetProviderOptionsAmazon} functions: +-- +-- msrs:SetProvider(MSRS.Provider.AMAZON) +-- msrs:SetProviderOptionsAmazon(AccessKey, SecretKey, Region) +-- +-- The parameters `AccessKey` and `SecretKey` are your AWS access and secret keys, respectively. The parameter `Region` is your [AWS region](https://docs.aws.amazon.com/general/latest/gr/pol.html). +-- +-- You can set the voice to use with AWS via @{#MSRS.SetVoiceAmazon}. +-- +-- ### Microsoft Azure [Only DCS-gRPC backend] +-- +-- In order to use Microsoft Azure for TTS you need to use @{#MSRS.SetProvider} and @{#MSRS.SetProviderOptionsAzure} functions: +-- +-- msrs:SetProvider(MSRS.Provider.AZURE) +-- msrs:SetProviderOptionsAmazon(AccessKey, Region) +-- +-- The parameter `AccessKey` is your Azure access key. The parameter `Region` is your [Azure region](https://learn.microsoft.com/en-us/azure/cognitive-services/speech-service/regions). +-- +-- You can set the voice to use with Azure via @{#MSRS.SetVoiceAzure}. +-- +-- ## Backend +-- +-- The default interface to SRS is via calling the 'DCS-SR-ExternalAudio.exe'. As noted above, this has the unavoidable drawback that a pop-up briefly appears +-- and DCS might be put out of focus. +-- +-- ## DCS-gRPC as an alternative to 'DCS-SR-ExternalAudio.exe' for TTS +-- +-- Another interface to SRS is [DCS-gRPC](https://github.com/DCS-gRPC/rust-server). This does not call an exe file and therefore avoids the annoying pop-up window. +-- In addition to Windows and Google cloud, it also offers Microsoft Azure and Amazon Web Service as providers 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. --- This can be useful if 'DCS-SR-ExternalAudio.exe' cannot be used in the environment, or to use Azure or AWS clouds for TTS. Note that DCS-gRPC does not (yet?) support +-- This can be useful if 'DCS-SR-ExternalAudio.exe' cannot be used in the environment or to use Azure or AWS clouds for TTS. Note that DCS-gRPC does not (yet?) support -- all of the features and options available with 'DCS-SR-ExternalAudio.exe'. Of note, only text-to-speech is supported and it it cannot be used to transmit audio files. -- --- DCS-gRPC must be installed and configured per the [DCS-gRPC documentation](https://github.com/DCS-gRPC/rust-server) and already running via either the 'autostart' mechanism --- or a Lua call to 'GRPC.load()' prior to use of the alternate DCS-gRPC backend. If a cloud TTS provider is being used, the API key must be set via the 'Config\dcs-grpc.lua' +-- DCS-gRPC must be installed and configured per the [DCS-gRPC documentation](https://github.com/DCS-gRPC/rust-server) and already running via either the 'autostart' mechanism +-- or a Lua call to 'GRPC.load()' prior to use of the alternate DCS-gRPC backend. If a cloud TTS provider is being used, the API key must be set via the 'Config\dcs-grpc.lua' -- configuration file prior DCS-gRPC being started. DCS-gRPC can be used both with DCS dedicated server and regular DCS installations. --- +-- -- To use the default local Windows TTS with DCS-gRPC, Windows 2019 Server (or newer) or Windows 10/11 are required. Voices for non-local languages and dialects may need to -- be explicitly installed. -- -- To set the MSRS class to use the DCS-gRPC backend for all future instances, call the function `MSRS.SetDefaultBackendGRPC()`. -- --- **Note** - When using other classes that use MSRS with the alternate DCS-gRPC backend, pass them strings instead of nil values for non-applicable fields with filesystem paths, +-- **Note** - When using other classes that use MSRS with the alternate DCS-gRPC backend, pass them strings instead of nil values for non-applicable fields with filesystem paths, -- such as the SRS path or Google credential path. This will help maximize compatibility with other classes that were written for the default backend. -- -- Basic Play Text-To-Speech example using alternate DCS-gRPC backend (DCS-gRPC not previously started): @@ -157,8 +215,8 @@ -- -- Select the alternate DCS-gRPC backend for new MSRS instances -- MSRS.SetDefaultBackendGRPC() -- -- Create a SOUNDTEXT object. --- local text=SOUNDTEXT:New("All Enemies destroyed") --- -- MOOSE SRS +-- local text=SOUNDTEXT:New("All Enemies destroyed") +-- -- MOOSE SRS -- local msrs=MSRS:New('', 305.0) -- -- Text-to speech with default voice after 30 seconds. -- msrs:PlaySoundText(text, 30) @@ -182,26 +240,27 @@ MSRS = { lid = nil, port = 5002, name = "MSRS", + backend = "srsexe", frequencies = {}, modulations = {}, coalition = 0, gender = "female", - culture = nil, + culture = nil, voice = nil, - volume = 1, + volume = 1, speed = 1, coordinate = nil, + provider = "win", Label = "ROBOT", - AltBackend = nil, ConfigFileName = "Moose_MSRS.lua", ConfigFilePath = "Config\\", ConfigLoaded = false, - ttsprovider = "Microsoft", + poptions = {}, } --- MSRS class version. -- @field #string version -MSRS.version="0.1.3" +MSRS.version="0.3.0" --- Voices -- @type MSRS.Voices @@ -292,7 +351,7 @@ MSRS.Voices = { ["de_DE_Wavenet_C"] = "de-DE-Wavenet-C", -- Female ["de_DE_Wavenet_D"] = "de-DE-Wavenet-D", -- Male ["de_DE_Wavenet_E"] = "de-DE-Wavenet-E", -- Male - ["de_DE_Wavenet_F"] = "de-DE-Wavenet-F", -- Female + ["de_DE_Wavenet_F"] = "de-DE-Wavenet-F", -- Female ["es_ES_Wavenet_B"] = "es-ES-Wavenet-B", -- Male ["es_ES_Wavenet_C"] = "es-ES-Wavenet-C", -- Female ["es_ES_Wavenet_D"] = "es-ES-Wavenet-D", -- Female @@ -300,19 +359,54 @@ MSRS.Voices = { ["it_IT_Wavenet_B"] = "it-IT-Wavenet-B", -- Female ["it_IT_Wavenet_C"] = "it-IT-Wavenet-C", -- Male ["it_IT_Wavenet_D"] = "it-IT-Wavenet-D", -- Male - } , + } , }, } ---- --- @type MSRS.ProviderOptions --- @field #string key --- @field #string secret --- @field #string region --- @field #string defaultVoice --- @field #string voice ---- GRPC options +--- Backend options to communicate with SRS. +-- @type MSRS.Backend +-- @field #string SRSEXE Use `DCS-SR-ExternalAudio.exe`. +-- @field #string GRPC Use DCS-gRPC. +MSRS.Backend = { + SRSEXE = "srsexe", + GRPC = "grpc", +} + +--- Text-to-speech providers. These are compatible with the DCS-gRPC conventions. +-- @type MSRS.Provider +-- @field #string WINDOWS Microsoft windows (`win`). +-- @field #string GOOGLE Google (`gcloud`). +-- @field #string AZURE Microsoft Azure (`azure`). Only possible with DCS-gRPC backend. +-- @field #string AMAZON Amazon Web Service (`aws`). Only possible with DCS-gRPC backend. +MSRS.Provider = { + WINDOWS = "win", + GOOGLE = "gcloud", + AZURE = "azure", + AMAZON = "aws", +} + +--- Function for UUID. +function MSRS.uuid() + local random = math.random + local template = 'yxxx-xxxxxxxxxxxx' + return string.gsub( template, '[xy]', function( c ) + local v = (c == 'x') and random( 0, 0xf ) or random( 8, 0xb ) + return string.format( '%x', v ) + end ) +end + +--- Provider options. +-- @type MSRS.ProviderOptions +-- @field #string provider Provider. +-- @field #string credentials Google credentials JSON file (full path). +-- @field #string key Access key (DCS-gRPC with Google, AWS, AZURE as provider). +-- @field #string secret Secret key (DCS-gRPC with AWS as provider) +-- @field #string region Region. +-- @field #string defaultVoice Default voice (not used). +-- @field #string voice Voice used. + +--- GRPC options. -- @type MSRS.GRPCOptions -- @field #string plaintext -- @field #string srsClientName @@ -324,21 +418,12 @@ MSRS.Voices = { -- @field #MSRS.ProviderOptions aws -- @field #string DefaultProvider -MSRS.GRPCOptions = {} -- #MSRS.GRPCOptions -MSRS.GRPCOptions.gcloud = {} -- #MSRS.ProviderOptions -MSRS.GRPCOptions.win = {} -- #MSRS.ProviderOptions -MSRS.GRPCOptions.azure = {} -- #MSRS.ProviderOptions -MSRS.GRPCOptions.aws = {} -- #MSRS.ProviderOptions - -MSRS.GRPCOptions.win.defaultVoice = "Hedda" -MSRS.GRPCOptions.win.voice = "Hedda" - -MSRS.GRPCOptions.DefaultProvider = "win" - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- DONE: Refactoring of input/config file. +-- DONE: Refactoring gRPC backend. -- TODO: Add functions to remove freqs and modulations. -- DONE: Add coordinate. -- DONE: Add google. @@ -349,77 +434,70 @@ MSRS.GRPCOptions.DefaultProvider = "win" -- Constructor ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Create a new MSRS object. +--- Create a new MSRS object. Required argument is the frequency and modulation. +-- Other parameters are read from the `Moose_MSRS.lua` config file. If you do not have that file set up you must set up and use the `DCS-SR-ExternalAudio.exe` (not DCS-gRPC) as backend, you need to still +-- set the path to the exe file via @{#MSRS.SetPath}. +-- -- @param #MSRS self --- @param #string PathToSRS Path to the directory, where SRS is located. +-- @param #string Path Path to SRS directory. Default `C:\\Program Files\\DCS-SimpleRadio-Standalone`. -- @param #number Frequency Radio frequency in MHz. Default 143.00 MHz. Can also be given as a #table of multiple frequencies. -- @param #number Modulation Radio modulation: 0=AM (default), 1=FM. See `radio.modulation.AM` and `radio.modulation.FM` enumerators. Can also be given as a #table of multiple modulations. --- @param #number Volume Volume - 1.0 is max, 0.0 is silence --- @param #table AltBackend Optional table containing tables 'Functions' and 'Vars' which add/replace functions and variables for the MSRS instance to allow alternate backends for transmitting to SRS. +-- @param #string Backend Backend used: `MSRS.Backend.SRSEXE` (default) or `MSRS.Backend.GRPC`. -- @return #MSRS self -function MSRS:New(PathToSRS, Frequency, Modulation, Volume, AltBackend) +function MSRS:New(Path, Frequency, Modulation, Backend) + + -- Inherit everything from BASE class. + local self=BASE:Inherit(self, BASE:New()) -- #MSRS + + self:F( {Path, Frequency, Modulation, Backend} ) -- Defaults. Frequency = Frequency or 143 Modulation = Modulation or radio.modulation.AM - -- Inherit everything from FSM class. - local self=BASE:Inherit(self, BASE:New()) -- #MSRS + self.lid = string.format("%s-%s | ", "unknown", self.version) - -- If AltBackend is supplied, initialize it, which will add/replace functions and variables in this MSRS instance. - if type( AltBackend ) == "table" or type( self.AltBackend ) == "table" then - - local Backend = UTILS.DeepCopy(AltBackend) or UTILS.DeepCopy(self.AltBackend) - - -- Add parameters to vars so alternate backends can use them if applicable - Backend.Vars = Backend.Vars or {} - Backend.Vars.PathToSRS = PathToSRS - Backend.Vars.Frequency = UTILS.DeepCopy(Frequency) - Backend.Vars.Modulation = UTILS.DeepCopy(Modulation) - Backend.Vars.Volume = Volume - - Backend.Functions = Backend.Functions or {} - - return self:_NewAltBackend(Backend) - end - if not self.ConfigLoaded then - - -- If no AltBackend table, the proceed with default initialisation - self:SetPath(PathToSRS) + + -- Defaults. + self:SetPath(Path) self:SetPort() self:SetFrequencies(Frequency) self:SetModulations(Modulation) self:SetGender() self:SetCoalition() self:SetLabel() - self:SetVolume(Volume) - + self:SetVolume() + self:SetBackend(Backend) + else + + -- Default overwrites from :New() - -- there might be some overwrites from :New() - - if PathToSRS then - self:SetPath(PathToSRS) + if Path then + self:SetPath(Path) end - + if Frequency then self:SetFrequencies(Frequency) + end + + if Modulation then self:SetModulations(Modulation) end - - if Volume then - self:SetVolume(Volume) + + if Backend then + self:SetBackend(Backend) end - + end - + self.lid = string.format("%s-%s | ", self.name, self.version) - + if not io or not os then self:E(self.lid.."***** ERROR - io or os NOT desanitized! MSRS will not work!") end - + return self end @@ -427,31 +505,83 @@ end -- User Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Set path to SRS install directory. More precisely, path to where the DCS- +--- Set backend to communicate with SRS. +-- There are two options: +-- +-- - `MSRS.Backend.SRSEXE`: This is the default and uses the `DCS-SR-ExternalAudio.exe`. +-- - `MSRS.Backend.GRPC`: Via DCS-gRPC. +-- -- @param #MSRS self --- @param #string Path Path to the directory, where the sound file is located. This does **not** contain a final backslash or slash. +-- @param #string Backend Backend used. Default is `MSRS.Backend.SRSEXE`. +-- @return #MSRS self +function MSRS:SetBackend(Backend) + self:F( {Backend=Backend} ) + self.backend=Backend or MSRS.Backend.SRSEXE + + return self +end + +--- Set DCS-gRPC as backend to communicate with SRS. +-- @param #MSRS self +-- @return #MSRS self +function MSRS:SetBackendGRPC() + self:F() + self:SetBackend(MSRS.Backend.GRPC) + + return self +end + +--- Set `DCS-SR-ExternalAudio.exe` as backend to communicate with SRS. +-- @param #MSRS self +-- @return #MSRS self +function MSRS:SetBackendSRSEXE() + self:F() + self:SetBackend(MSRS.Backend.SRSEXE) + + return self +end + +--- Set the default backend. +-- @param #MSRS self +function MSRS.SetDefaultBackend(Backend) + self:F( {Backend=Backend} ) + MSRS.backend=Backend or MSRS.Backend.SRSEXE +end + +--- Set DCS-gRPC to be the default backend. +-- @param #MSRS self +function MSRS.SetDefaultBackendGRPC() + self:F() + MSRS.backend=MSRS.Backend.GRPC +end + +--- Get currently set backend. +-- @param #MSRS self +-- @return #string Backend. +function MSRS:GetBackend() + return self.backend +end + +--- Set path to SRS install directory. More precisely, path to where the `DCS-SR-ExternalAudio.exe` is located. +-- @param #MSRS self +-- @param #string Path Path to the directory, where the sound file is located. Default is `C:\\Program Files\\DCS-SimpleRadio-Standalone`. -- @return #MSRS self function MSRS:SetPath(Path) + self:F( {Path=Path} ) - if Path==nil and not self.path then - self:E("ERROR: No path to SRS directory specified!") - return nil + -- Set path. + self.path=Path or "C:\\Program Files\\DCS-SimpleRadio-Standalone" + + -- Remove (back)slashes. + local n=1 ; local nmax=1000 + while (self.path:sub(-1)=="/" or self.path:sub(-1)==[[\]]) and n<=nmax do + self.path=self.path:sub(1,#self.path-1) + n=n+1 end + + -- Debug output. + self:F(string.format("SRS path=%s", self:GetPath())) - if Path then - -- Set path. - self.path=Path - - -- Remove (back)slashes. - local n=1 ; local nmax=1000 - while (self.path:sub(-1)=="/" or self.path:sub(-1)==[[\]]) and n<=nmax do - self.path=self.path:sub(1,#self.path-1) - n=n+1 - end - - -- Debug output. - self:T(string.format("SRS path=%s", self:GetPath())) - end return self end @@ -467,6 +597,7 @@ end -- @param #number Volume Volume - 1.0 is max, 0.0 is silence -- @return #MSRS self function MSRS:SetVolume(Volume) + self:F( {Volume=Volume} ) local volume = Volume or 1 if volume > 1 then volume = 1 elseif volume < 0 then volume = 0 end self.volume = volume @@ -474,7 +605,7 @@ function MSRS:SetVolume(Volume) end --- Get SRS volume. --- @param #MSRS self +-- @param #MSRS self -- @return #number Volume Volume - 1.0 is max, 0.0 is silence function MSRS:GetVolume() return self.volume @@ -485,6 +616,7 @@ end -- @param #number Label. Default "ROBOT" -- @return #MSRS self function MSRS:SetLabel(Label) + self:F( {Label=Label} ) self.Label=Label or "ROBOT" return self end @@ -501,7 +633,9 @@ end -- @param #number Port Port. Default 5002. -- @return #MSRS self function MSRS:SetPort(Port) + self:F( {Port=Port} ) self.port=Port or 5002 + self:T(string.format("SRS port=%s", self:GetPort())) return self end @@ -517,6 +651,7 @@ end -- @param #number Coalition Coalition. Default 0. -- @return #MSRS self function MSRS:SetCoalition(Coalition) + self:F( {Coalition=Coalition} ) self.coalition=Coalition or 0 return self end @@ -534,14 +669,9 @@ end -- @param #table Frequencies Frequencies in MHz. Can also be given as a #number if only one frequency should be used. -- @return #MSRS self function MSRS:SetFrequencies(Frequencies) + self:F( Frequencies ) + self.frequencies=UTILS.EnsureTable(Frequencies, false) - -- Ensure table. - if type(Frequencies)~="table" then - Frequencies={Frequencies} - end - - self.frequencies=Frequencies - return self end @@ -550,22 +680,18 @@ end -- @param #table Frequencies Frequencies in MHz. Can also be given as a #number if only one frequency should be used. -- @return #MSRS self function MSRS:AddFrequencies(Frequencies) - - -- Ensure table. - if type(Frequencies)~="table" then - Frequencies={Frequencies} - end - - for _,_freq in pairs(Frequencies) do + self:F( Frequencies ) + for _,_freq in pairs(UTILS.EnsureTable(Frequencies, false)) do + self:T(self.lid..string.format("Adding frequency %s", tostring(_freq))) table.insert(self.frequencies,_freq) end - + return self end --- Get frequencies. -- @param #MSRS self --- @param #table Frequencies in MHz. +-- @return #table Frequencies in MHz. function MSRS:GetFrequencies() return self.frequencies end @@ -576,14 +702,13 @@ end -- @param #table Modulations Modulations. Can also be given as a #number if only one modulation should be used. -- @return #MSRS self function MSRS:SetModulations(Modulations) + self:F( Modulations ) + self.modulations=UTILS.EnsureTable(Modulations, false) + + -- Debug info. + self:T(self.lid.."Modulations:") + self:T(self.modulations) - -- Ensure table. - if type(Modulations)~="table" then - Modulations={Modulations} - end - - self.modulations=Modulations - return self end @@ -592,22 +717,17 @@ end -- @param #table Modulations Modulations. Can also be given as a #number if only one modulation should be used. -- @return #MSRS self function MSRS:AddModulations(Modulations) - - -- Ensure table. - if type(Modulations)~="table" then - Modulations={Modulations} - end - - for _,_mod in pairs(Modulations) do + self:F( Modulations ) + for _,_mod in pairs(UTILS.EnsureTable(Modulations, false)) do table.insert(self.modulations,_mod) end - + return self end --- Get modulations. -- @param #MSRS self --- @param #table Modulations. +-- @return #table Modulations. function MSRS:GetModulations() return self.modulations end @@ -617,173 +737,344 @@ end -- @param #string Gender Gender: "male" or "female" (default). -- @return #MSRS self function MSRS:SetGender(Gender) - + self:F( {Gender=Gender} ) Gender=Gender or "female" - + self.gender=Gender:lower() - + -- Debug output. self:T("Setting gender to "..tostring(self.gender)) - + return self end --- Set culture. -- @param #MSRS self --- @param #string Culture Culture, e.g. "en-GB" (default). +-- @param #string Culture Culture, *e.g.* "en-GB". -- @return #MSRS self function MSRS:SetCulture(Culture) - + self:F( {Culture=Culture} ) self.culture=Culture - + return self end ---- Set to use a specific voice. Will override gender and culture settings. +--- Set to use a specific voice. Note that this will override any gender and culture settings as a voice already has a certain gender/culture. -- @param #MSRS self -- @param #string Voice Voice. -- @return #MSRS self function MSRS:SetVoice(Voice) - + self:F( {Voice=Voice} ) self.voice=Voice - - --local defaultprovider = self.provider or self.GRPCOptions.DefaultProvider or MSRS.GRPCOptions.DefaultProvider or "win" - - --self.GRPCOptions[defaultprovider].voice = Voice - + return self end ---- Set to use a specific voice. Will override gender and culture settings. +--- Set to use a specific voice for a given provider. Note that this will override any gender and culture settings. -- @param #MSRS self -- @param #string Voice Voice. +-- @param #string Provider Provider. Default is as set by @{#MSRS.SetProvider}, which itself defaults to `MSRS.Provider.WINDOWS` if not set. -- @return #MSRS self -function MSRS:SetDefaultVoice(Voice) +function MSRS:SetVoiceProvider(Voice, Provider) + self:F( {Voice=Voice, Provider=Provider} ) + self.poptions=self.poptions or {} + + self.poptions[Provider or self:GetProvider()]=Voice - self.defaultVoice=Voice - local provider = self.provider or self.GRPCOptions.DefaultProvider or MSRS.GRPCOptions.DefaultProvider or "win" - self.GRPCOptions[provider].defaultVoice = Voice - return self end ---- Set the coordinate from which the transmissions will be broadcasted. +--- Set to use a specific voice if Microsoft Windows' native TTS is use as provider. Note that this will override any gender and culture settings. +-- @param #MSRS self +-- @param #string Voice Voice. Default `"Microsoft Hazel Desktop"`. +-- @return #MSRS self +function MSRS:SetVoiceWindows(Voice) + self:F( {Voice=Voice} ) + self:SetVoiceProvider(Voice or "Microsoft Hazel Desktop", MSRS.Provider.WINDOWS) + + return self +end + +--- Set to use a specific voice if Google is use as provider. Note that this will override any gender and culture settings. +-- @param #MSRS self +-- @param #string Voice Voice. Default `MSRS.Voices.Google.Standard.en_GB_Standard_A`. +-- @return #MSRS self +function MSRS:SetVoiceGoogle(Voice) + self:F( {Voice=Voice} ) + self:SetVoiceProvider(Voice or MSRS.Voices.Google.Standard.en_GB_Standard_A, MSRS.Provider.GOOGLE) + + return self +end + + +--- Set to use a specific voice if Microsoft Azure is use as provider (only DCS-gRPC backend). Note that this will override any gender and culture settings. +-- @param #MSRS self +-- @param #string Voice [Azure Voice](https://learn.microsoft.com/azure/cognitive-services/speech-service/language-support). Default `"en-US-AriaNeural"`. +-- @return #MSRS self +function MSRS:SetVoiceAzure(Voice) + self:F( {Voice=Voice} ) + self:SetVoiceProvider(Voice or "en-US-AriaNeural", MSRS.Provider.AZURE) + + return self +end + +--- Set to use a specific voice if Amazon Web Service is use as provider (only DCS-gRPC backend). Note that this will override any gender and culture settings. +-- @param #MSRS self +-- @param #string Voice [AWS Voice](https://docs.aws.amazon.com/polly/latest/dg/voicelist.html). Default `"Brian"`. +-- @return #MSRS self +function MSRS:SetVoiceAmazon(Voice) + self:F( {Voice=Voice} ) + self:SetVoiceProvider(Voice or "Brian", MSRS.Provider.AMAZON) + + return self +end + +--- Get voice. +-- @param #MSRS self +-- @param #string Provider Provider. Default is the currently set provider (`self.provider`). +-- @return #string Voice. +function MSRS:GetVoice(Provider) + + Provider=Provider or self.provider + + if Provider and self.poptions[Provider] and self.poptions[Provider].voice then + return self.poptions[Provider].voice + else + return self.voice + end + +end + +--- Set the coordinate from which the transmissions will be broadcasted. Note that this is only a factor if SRS has line-of-sight or distance enabled. -- @param #MSRS self -- @param Core.Point#COORDINATE Coordinate Origin of the transmission. -- @return #MSRS self function MSRS:SetCoordinate(Coordinate) - + self:F( Coordinate ) self.coordinate=Coordinate - + return self end ---- Use google text-to-speech credentials. Also sets Google as default TTS provider. +--- **[Deprecated]** 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 function MSRS:SetGoogle(PathToCredentials) - + self:F( {PathToCredentials=PathToCredentials} ) if PathToCredentials then - - self.google=PathToCredentials - self.APIKey=PathToCredentials - self.provider = "gcloud" - - self.GRPCOptions.DefaultProvider = "gcloud" - self.GRPCOptions.gcloud.key = PathToCredentials - self.ttsprovider = "Google" - + + self.provider = MSRS.Provider.GOOGLE + + self:SetProviderOptionsGoogle(PathToCredentials, PathToCredentials) + end - + return self end ---- gRPC Backend: Use google text-to-speech set the API key. +--- **[Deprecated]** Use google text-to-speech set the API key (only for DCS-gRPC). -- @param #MSRS self -- @param #string APIKey API Key, usually a string of length 40 with characters and numbers. -- @return #MSRS self function MSRS:SetGoogleAPIKey(APIKey) + self:F( {APIKey=APIKey} ) if APIKey then - self.APIKey=APIKey - self.provider = "gcloud" - self.GRPCOptions.DefaultProvider = "gcloud" - self.GRPCOptions.gcloud.key = APIKey + + self.provider = MSRS.Provider.GOOGLE + + if self.poptions[MSRS.Provider.GOOGLE] then + self.poptions[MSRS.Provider.GOOGLE].key=APIKey + else + self:SetProviderOptionsGoogle(nil ,APIKey) + end + end return self end ---- Use Google text-to-speech as default. + +--- Set provider used to generate text-to-speech. +-- These options are available: +-- +-- - `MSRS.Provider.WINDOWS`: Microsoft Windows (default) +-- - `MSRS.Provider.GOOGLE`: Google Cloud +-- - `MSRS.Provider.AZURE`: Microsoft Azure (only with DCS-gRPC backend) +-- - `MSRS.Provier.AMAZON`: Amazone Web Service (only with DCS-gRPC backend) +-- +-- Note that all providers except Microsoft Windows need as additonal information the credentials of your account. +-- +-- @param #MSRS self +-- @param #string Provider +-- @return #MSRS self +function MSRS:SetProvider(Provider) + self:F( {Provider=Provider} ) + self.provider = Provider or MSRS.Provider.WINDOWS + return self +end + + +--- Get provider. +-- @param #MSRS self +-- @return #MSRS self +function MSRS:GetProvider() + return self.provider or MSRS.Provider.WINDOWS +end + +--- Set provider options and credentials. +-- @param #MSRS self +-- @param #string Provider Provider. +-- @param #string CredentialsFile Full path to your credentials file. For Google this is the path to a JSON file. +-- @param #string AccessKey Your API access key. +-- @param #string SecretKey Your secret key. +-- @param #string Region Region to use. +-- @return #MSRS.ProviderOptions Provider optionas table. +function MSRS:SetProviderOptions(Provider, CredentialsFile, AccessKey, SecretKey, Region) + self:F( {Provider, CredentialsFile, AccessKey, SecretKey, Region} ) + local option=MSRS._CreateProviderOptions(Provider, CredentialsFile, AccessKey, SecretKey, Region) + + if self then + self.poptions=self.poptions or {} + self.poptions[Provider]=option + else + MSRS.poptions=MSRS.poptions or {} + MSRS.poptions[Provider]=option + end + + return option +end + +--- Create MSRS.ProviderOptions. +-- @param #string Provider Provider. +-- @param #string CredentialsFile Full path to your credentials file. For Google this is the path to a JSON file. +-- @param #string AccessKey Your API access key. +-- @param #string SecretKey Your secret key. +-- @param #string Region Region to use. +-- @return #MSRS.ProviderOptions Provider optionas table. +function MSRS._CreateProviderOptions(Provider, CredentialsFile, AccessKey, SecretKey, Region) + self:F( {Provider, CredentialsFile, AccessKey, SecretKey, Region} ) + local option={} --#MSRS.ProviderOptions + + option.provider=Provider + option.credentials=CredentialsFile + option.key=AccessKey + option.secret=SecretKey + option.region=Region + + return option +end + +--- Set provider options and credentials for Google Cloud. +-- @param #MSRS self +-- @param #string CredentialsFile Full path to your credentials file. For Google this is the path to a JSON file. This is used if `DCS-SR-ExternalAudio.exe` is used as backend. +-- @param #string AccessKey Your API access key. This is necessary if DCS-gRPC is used as backend. +-- @return #MSRS self +function MSRS:SetProviderOptionsGoogle(CredentialsFile, AccessKey) + self:F( {CredentialsFile, AccessKey} ) + self:SetProviderOptions(MSRS.Provider.GOOGLE, CredentialsFile, AccessKey) + return self +end + +--- Set provider options and credentials for Amazon Web Service (AWS). Only supported in combination with DCS-gRPC as backend. +-- @param #MSRS self +-- @param #string AccessKey Your API access key. +-- @param #string SecretKey Your secret key. +-- @param #string Region Your AWS [region](https://docs.aws.amazon.com/general/latest/gr/pol.html). +-- @return #MSRS self +function MSRS:SetProviderOptionsAmazon(AccessKey, SecretKey, Region) + self:F( {AccessKey, SecretKey, Region} ) + self:SetProviderOptions(MSRS.Provider.AMAZON, nil, AccessKey, SecretKey, Region) + return self +end + +--- Set provider options and credentials for Microsoft Azure. Only supported in combination with DCS-gRPC as backend. +-- @param #MSRS self +-- @param #string AccessKey Your API access key. +-- @param #string Region Your Azure [region](https://learn.microsoft.com/en-us/azure/cognitive-services/speech-service/regions). +-- @return #MSRS self +function MSRS:SetProviderOptionsAzure(AccessKey, Region) + self:F( {AccessKey, Region} ) + self:SetProviderOptions(MSRS.Provider.AZURE, nil, AccessKey, nil, Region) + return self +end + + +--- Get provider options. +-- @param #MSRS self +-- @param #string Provider Provider. Default is as set via @{#MSRS.SetProvider}. +-- @return #MSRS.ProviderOptions Provider options. +function MSRS:GetProviderOptions(Provider) + return self.poptions[Provider or self.provider] or {} +end + + +--- Use Google to provide text-to-speech. -- @param #MSRS self -- @return #MSRS self function MSRS:SetTTSProviderGoogle() - self.ttsprovider = "Google" + self:F() + self:SetProvider(MSRS.Provider.GOOGLE) return self end ---- Use Microsoft text-to-speech as default. +--- Use Microsoft to provide text-to-speech. -- @param #MSRS self -- @return #MSRS self function MSRS:SetTTSProviderMicrosoft() - self.ttsprovider = "Microsoft" + self:F() + self:SetProvider(MSRS.Provider.WINDOWS) return self end ---- Print SRS STTS help to DCS log file. + +--- Use Microsoft Azure to provide text-to-speech. Only supported if used in combination with DCS-gRPC as backend. +-- @param #MSRS self +-- @return #MSRS self +function MSRS:SetTTSProviderAzure() + self:F() + self:SetProvider(MSRS.Provider.AZURE) + return self +end + +--- Use Amazon Web Service (AWS) to provide text-to-speech. Only supported if used in combination with DCS-gRPC as backend. +-- @param #MSRS self +-- @return #MSRS self +function MSRS:SetTTSProviderAmazon() + self:F() + self:SetProvider(MSRS.Provider.AMAZON) + return self +end + + +--- Print SRS help to DCS log file. -- @param #MSRS self -- @return #MSRS self function MSRS:Help() - + self:F() -- Path and exe. - local path=self:GetPath() or STTS.DIRECTORY - local exe=STTS.EXECUTABLE or "DCS-SR-ExternalAudio.exe" - + local path=self:GetPath() + local exe="DCS-SR-ExternalAudio.exe" + -- Text file for output. - local filename = os.getenv('TMP') .. "\\MSRS-help-"..STTS.uuid()..".txt" - + local filename = os.getenv('TMP') .. "\\MSRS-help-"..MSRS.uuid()..".txt" + -- Print help. - local command=string.format("%s/%s --help > %s", path, exe, filename) + local command=string.format("%s/%s --help > %s", path, exe, filename) os.execute(command) - + local f=assert(io.open(filename, "rb")) local data=f:read("*all") f:close() - + -- Print to log file. - env.info("SRS STTS help output:") + env.info("SRS help output:") env.info("======================================================================") env.info(data) env.info("======================================================================") - + return self end ---- Sets an alternate SRS backend to be used by MSRS to transmit over SRS for all new MSRS class instances. --- @param #table Backend A table containing a table `Functions` with new/replacement class functions and `Vars` with new/replacement variables. --- @return #boolean Returns 'true' on success. -function MSRS.SetDefaultBackend(Backend) - if type(Backend) == "table" then - MSRS.AltBackend = UTILS.DeepCopy(Backend) - else - return false - end - - return true -end - ---- Restores default SRS backend (DCS-SR-ExternalAudio.exe) to be used by all new MSRS class instances to transmit over SRS. --- @return #boolean Returns 'true' on success. -function MSRS.ResetDefaultBackend() - MSRS.AltBackend = nil - return true -end - ---- Sets DCS-gRPC as the default SRS backend for all new MSRS class instances. --- @return #boolean Returns 'true' on success. -function MSRS.SetDefaultBackendGRPC() - return MSRS.SetDefaultBackend(MSRS_BACKEND_DCSGRPC) -end - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Transmission Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -794,23 +1085,31 @@ end -- @param #number Delay Delay in seconds, before the sound file is played. -- @return #MSRS self function MSRS:PlaySoundFile(Soundfile, Delay) + self:F( {Soundfile, Delay} ) + + -- Sound file name. + local soundfile=Soundfile:GetName() + + -- First check if text file exists! + local exists=UTILS.FileExists(soundfile) + if not exists then + self:E("ERROR: MSRS sound file does not exist! File="..soundfile) + return self + end if Delay and Delay>0 then self:ScheduleOnce(Delay, MSRS.PlaySoundFile, self, Soundfile, 0) else - -- Sound file name. - local soundfile=Soundfile:GetName() - -- Get command. local command=self:_GetCommand() - + -- Append file. command=command..' --file="'..tostring(soundfile)..'"' - + -- Execute command. self:_ExecCommand(command) - + end return self @@ -822,52 +1121,58 @@ end -- @param #number Delay Delay in seconds, before the sound file is played. -- @return #MSRS self function MSRS:PlaySoundText(SoundText, Delay) + self:F( {SoundText, Delay} ) if Delay and Delay>0 then self:ScheduleOnce(Delay, MSRS.PlaySoundText, self, SoundText, 0) else - -- Get command. - local command=self:_GetCommand(nil, nil, nil, SoundText.gender, SoundText.voice, SoundText.culture, SoundText.volume, SoundText.speed) - - -- Append text. - command=command..string.format(" --text=\"%s\"", tostring(SoundText.text)) - - -- Execute command. - self:_ExecCommand(command) - + if self.backend==MSRS.Backend.GRPC then + self:_DCSgRPCtts(SoundText.text, nil, SoundText.gender, SoundText.culture, SoundText.voice, SoundText.volume, SoundText.label, SoundText.coordinate) + else + + -- Get command. + local command=self:_GetCommand(nil, nil, nil, SoundText.gender, SoundText.voice, SoundText.culture, SoundText.volume, SoundText.speed) + + -- Append text. + command=command..string.format(" --text=\"%s\"", tostring(SoundText.text)) + + -- Execute command. + self:_ExecCommand(command) + + end + end return self end ---- Play text message via STTS. +--- Play text message via MSRS. -- @param #MSRS self -- @param #string Text Text message. -- @param #number Delay Delay in seconds, before the message is played. -- @param Core.Point#COORDINATE Coordinate Coordinate. -- @return #MSRS self function MSRS:PlayText(Text, Delay, Coordinate) + self:F( {Text, Delay, Coordinate} ) if Delay and Delay>0 then self:ScheduleOnce(Delay, MSRS.PlayText, self, Text, nil, Coordinate) else - -- Get command line. - local command=self:_GetCommand(nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,Coordinate) + if self.backend==MSRS.Backend.GRPC then + self:T(self.lid.."Transmitting") + self:_DCSgRPCtts(Text, nil, nil , nil, nil, nil, nil, Coordinate) + else + self:PlayTextExt(Text, Delay, nil, nil, nil, nil, nil, nil, nil, Coordinate) + end - -- Append text. - command=command..string.format(" --text=\"%s\"", tostring(Text)) - - -- Execute command. - self:_ExecCommand(command) - end - + return self end ---- Play text message via STTS with explicitly specified options. +--- Play text message via MSRS with explicitly specified options. -- @param #MSRS self -- @param #string Text Text message. -- @param #number Delay Delay in seconds, before the message is played. @@ -881,71 +1186,75 @@ end -- @param Core.Point#COORDINATE Coordinate Coordinate. -- @return #MSRS self function MSRS:PlayTextExt(Text, Delay, Frequencies, Modulations, Gender, Culture, Voice, Volume, Label, Coordinate) + self:F( {Text, Delay, Frequencies, Modulations, Gender, Culture, Voice, Volume, Label, Coordinate} ) if Delay and Delay>0 then self:ScheduleOnce(Delay, MSRS.PlayTextExt, self, Text, 0, Frequencies, Modulations, Gender, Culture, Voice, Volume, Label, Coordinate) else - - -- Ensure table. - if Frequencies and type(Frequencies)~="table" then - Frequencies={Frequencies} + + Frequencies = Frequencies or self:GetFrequencies() + Modulations = Modulations or self:GetModulations() + + if self.backend==MSRS.Backend.SRSEXE then + + -- Get command line. + local command=self:_GetCommand(UTILS.EnsureTable(Frequencies, false), UTILS.EnsureTable(Modulations, false), nil, Gender, Voice, Culture, Volume, nil, nil, Label, Coordinate) + + -- Append text. + command=command..string.format(" --text=\"%s\"", tostring(Text)) + + -- Execute command. + self:_ExecCommand(command) + + elseif self.backend==MSRS.Backend.GRPC then + + self:_DCSgRPCtts(Text, Frequencies, Gender, Culture, Voice, Volume, Label, Coordinate) + end - -- Ensure table. - if Modulations and type(Modulations)~="table" then - Modulations={Modulations} - end - - -- Get command line. - local command=self:_GetCommand(Frequencies, Modulations, nil, Gender, Voice, Culture, Volume, nil, nil, Label, Coordinate) - - -- Append text. - command=command..string.format(" --text=\"%s\"", tostring(Text)) - - -- Execute command. - self:_ExecCommand(command) - end - + return self end ---- Play text file via STTS. +--- Play text file via MSRS. -- @param #MSRS self -- @param #string TextFile Full path to the file. -- @param #number Delay Delay in seconds, before the message is played. -- @return #MSRS self function MSRS:PlayTextFile(TextFile, Delay) + self:F( {TextFile, Delay} ) if Delay and Delay>0 then self:ScheduleOnce(Delay, MSRS.PlayTextFile, self, TextFile, 0) else - + -- First check if text file exists! - local exists=UTILS.FileExists(TextFile) + local exists=UTILS.FileExists(TextFile) if not exists then self:E("ERROR: MSRS Text file does not exist! File="..tostring(TextFile)) return self end - -- Get command line. + -- Get command line. local command=self:_GetCommand() -- Append text file. command=command..string.format(" --textFile=\"%s\"", tostring(TextFile)) - + -- Debug output. self:T(string.format("MSRS TextFile command=%s", command)) - + -- Count length of command. local l=string.len(command) + self:T(string.format("Command length=%d", l)) -- Execute command. self:_ExecCommand(command) - + end - + return self end @@ -954,127 +1263,29 @@ end -- Misc Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Adds or replaces functions and variables in the current MSRS class instance to enable an alternate backends for transmitting to SRS. --- @param #MSRS self --- @param #table A table containing a table `Functions` with new/replacement class functions and `Vars` with new/replacement variables. --- @return #MSRS self -function MSRS:_NewAltBackend(Backend) - BASE:T('Entering MSRS:_NewAltBackend()') - - -- Add/replace class instance functions with those defined in the alternate backend in Functions table - for funcName,funcDef in pairs(Backend.Functions) do - if type(funcDef) == 'function' then - BASE:T('MSRS (re-)defining function MSRS:' .. funcName ) - self[funcName] = funcDef - end - end - - -- Add/replace class instance variables with those defined in the alternate backend in Vars table - for varName,varVal in pairs(Backend.Vars) do - BASE:T('MSRS setting self.' .. varName) - self[varName] = UTILS.DeepCopy(varVal) - end - - -- If _MSRSbackendInit() is defined in the backend, then run it (it should return self) - if self._MSRSbackendInit and type(self._MSRSbackendInit) == 'function' then - return self:_MSRSbackendInit() - end - - return self -end - ---- Execute SRS command to play sound using the `DCS-SR-ExternalAudio.exe`. --- @param #MSRS self --- @param #string command Command to executer --- @return #number Return value of os.execute() command. -function MSRS:_ExecCommand(command) - - -- Create a tmp file. - local filename=os.getenv('TMP').."\\MSRS-"..STTS.uuid()..".bat" - - local script=io.open(filename, "w+") - script:write(command.." && exit") - script:close() - - -- Play command. - command=string.format('start /b "" "%s"', filename) - - local res=nil - if true then - - -- Create a tmp file. - local filenvbs = os.getenv('TMP') .. "\\MSRS-"..STTS.uuid()..".vbs" - - -- VBS script - local script = io.open(filenvbs, "w+") - script:write(string.format('Dim WinScriptHost\n')) - script:write(string.format('Set WinScriptHost = CreateObject("WScript.Shell")\n')) - script:write(string.format('WinScriptHost.Run Chr(34) & "%s" & Chr(34), 0\n', filename)) - script:write(string.format('Set WinScriptHost = Nothing')) - script:close() - - -- Run visual basic script. This still pops up a window but very briefly and does not put the DCS window out of focus. - local runvbs=string.format('cscript.exe //Nologo //B "%s"', filenvbs) - - -- Debug output. - self:T("MSRS execute command="..command) - self:T("MSRS execute VBS command="..runvbs) - - -- Play file in 0.01 seconds - res=os.execute(runvbs) - - -- Remove file in 1 second. - timer.scheduleFunction(os.remove, filename, timer.getTime()+1) - timer.scheduleFunction(os.remove, filenvbs, timer.getTime()+1) - - elseif false then - - -- Create a tmp file. - local filenvbs = os.getenv('TMP') .. "\\MSRS-"..STTS.uuid()..".vbs" - - -- VBS script - local script = io.open(filenvbs, "w+") - script:write(string.format('Set oShell = CreateObject ("Wscript.Shell")\n')) - script:write(string.format('Dim strArgs\n')) - script:write(string.format('strArgs = "cmd /c %s"\n', filename)) - script:write(string.format('oShell.Run strArgs, 0, false')) - script:close() - - local runvbs=string.format('cscript.exe //Nologo //B "%s"', filenvbs) - - -- Play file in 0.01 seconds - res=os.execute(runvbs) - - else - - -- Debug output. - self:T("MSRS execute command="..command) - - -- Execute command - res=os.execute(command) - - -- Remove file in 1 second. - timer.scheduleFunction(os.remove, filename, timer.getTime()+1) - - end - - - return res -end - --- Get lat, long and alt from coordinate. -- @param #MSRS self -- @param Core.Point#Coordinate Coordinate Coordinate. Can also be a DCS#Vec3. --- @return #number Latitude. --- @return #number Longitude. --- @return #number Altitude. +-- @return #number Latitude (or 0 if no input coordinate was given). +-- @return #number Longitude (or 0 if no input coordinate was given). +-- @return #number Altitude (or 0 if no input coordinate was given). function MSRS:_GetLatLongAlt(Coordinate) - - local lat, lon, alt=coord.LOtoLL(Coordinate) - + self:F( {Coordinate=Coordinate} ) + + local lat=0.0 + local lon=0.0 + local alt=0.0 + + if Coordinate then + lat, lon, alt=coord.LOtoLL(Coordinate) + end + return lat, lon, math.floor(alt) end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Backend ExternalAudio.exe +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Get SRS command to play sound using the `DCS-SR-ExternalAudio.exe`. -- @param #MSRS self @@ -1090,26 +1301,30 @@ end -- @param #string label Label, defaults to "ROBOT" (displayed sender name in the radio overlay of SRS) - No spaces allowed! -- @param Core.Point#COORDINATE coordinate Coordinate. -- @return #string Command. -function MSRS:_GetCommand(freqs, modus, coal, gender, voice, culture, volume, speed, port,label,coordinate) +function MSRS:_GetCommand(freqs, modus, coal, gender, voice, culture, volume, speed, port, label, coordinate) + self:F( {freqs, modus, coal, gender, voice, culture, volume, speed, port, label, coordinate} ) + + local path=self:GetPath() + local exe="DCS-SR-ExternalAudio.exe" + local fullPath = string.format("%s\\%s", path, exe) - local path=self:GetPath() or STTS.DIRECTORY - local exe=STTS.EXECUTABLE or "DCS-SR-ExternalAudio.exe" freqs=table.concat(freqs or self.frequencies, ",") modus=table.concat(modus or self.modulations, ",") + coal=coal or self.coalition gender=gender or self.gender - voice=voice or self.voice + voice=voice or self:GetVoice(self.provider) or self.voice culture=culture or self.culture volume=volume or self.volume speed=speed or self.speed port=port or self.port label=label or self.Label coordinate=coordinate or self.coordinate - + -- Replace modulation modus=modus:gsub("0", "AM") modus=modus:gsub("1", "FM") - + -- Command. local command=string.format('"%s\\%s" -f "%s" -m "%s" -c %s -p %s -n "%s" -v "%.1f"', path, exe, freqs, modus, coal, port, label,volume) @@ -1127,369 +1342,123 @@ function MSRS:_GetCommand(freqs, modus, coal, gender, voice, culture, volume, sp command=command..string.format(" -l %s", tostring(culture)) end end - + -- Set coordinate. if coordinate then local lat,lon,alt=self:_GetLatLongAlt(coordinate) command=command..string.format(" -L %.4f -O %.4f -A %d", lat, lon, alt) end - - -- Set google. - if self.google and self.ttsprovider == "Google" then - command=command..string.format(' --ssml -G "%s"', self.google) + + -- Set provider options + if self.provider==MSRS.Provider.GOOGLE then + local pops=self:GetProviderOptions() + command=command..string.format(' --ssml -G "%s"', pops.credentials) + elseif self.provider==MSRS.Provider.WINDOWS then + -- Nothing to do. + else + self:E("ERROR: SRS only supports WINWOWS and GOOGLE as TTS providers! Use DCS-gRPC backend for other providers such as ") end - + + if not UTILS.FileExists(fullPath) then + self:E("ERROR: MSRS SRS executable does not exist! FullPath="..fullPath) + command="CommandNotFound" + end + -- Debug output. - self:T("MSRS command="..command) + self:T("MSRS command from _GetCommand="..command) return command end ---- Get central SRS configuration to be able to play tts over SRS radio using the `DCS-SR-ExternalAudio.exe`. +--- Execute SRS command to play sound using the `DCS-SR-ExternalAudio.exe`. -- @param #MSRS self --- @param #string Path Path to config file, defaults to "C:\Users\\Saved Games\DCS\Config" --- @param #string Filename File to load, defaults to "Moose_MSRS.lua" --- @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, --- and also make config easier to use in the code. --- 1) Create a config file named "Moose_MSRS.lua" at this location "C:\Users\\Saved Games\DCS\Config" (or wherever your Saved Games folder resides). --- 2) The file needs the following structure: --- --- -- Moose MSRS default Config --- MSRS_Config = { --- 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, -- 0.0 to 1.0 --- Coalition = 0, -- 0 = Neutral, 1 = Red, 2 = Blue --- 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. --- 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 --- DefaultProvider = "gcloud", -- win, gcloud, aws, or azure, some of the values below depend on your cloud provider --- gcloud = { --- 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", --- }, --- } --- } --- --- 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: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: --- --- -- Needed once only --- MESSAGE.SetMSRS(MSRS.path,nil,MSRS.google,243,radio.modulation.AM,nil,nil, --- MSRS.Voices.Google.Standard.de_DE_Standard_B,coalition.side.BLUE) --- --- -- later on in your code --- --- MESSAGE:New("Test message!",15,"SPAWN"):ToSRS(243,radio.modulation.AM,nil,nil,MSRS.Voices.Google.Standard.fr_FR_Standard_C) --- --- -- Create new ATIS as usual --- atis=ATIS:New(AIRBASE.Caucasus.Batumi, 123, radio.modulation.AM) --- atis:SetSRS(nil,nil,nil,MSRS.Voices.Google.Standard.en_US_Standard_H) --- --Start ATIS --- atis:Start() -function MSRS:LoadConfigFile(Path,Filename) +-- @param #string command Command to executer +-- @return #number Return value of os.execute() command. +function MSRS:_ExecCommand(command) + self:F( {command=command} ) - if lfs == nil then - env.info("*****Note - lfs and os need to be desanitized for MSRS to work!") - return false - end - 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 filexsists and not MSRS.ConfigLoaded then - assert(loadfile(path..file))() - -- now we should have a global var MSRS_Config - if MSRS_Config then - if self then - self.path = MSRS_Config.Path or "C:\\Program Files\\DCS-SimpleRadio-Standalone" - self.port = MSRS_Config.Port or 5002 - self.frequencies = MSRS_Config.Frequency or {127,243} - self.modulations = MSRS_Config.Modulation or {0,0} - self.coalition = MSRS_Config.Coalition or 0 - if MSRS_Config.Coordinate then - self.coordinate = COORDINATE:New( MSRS_Config.Coordinate[1],MSRS_Config.Coordinate[2],MSRS_Config.Coordinate[3]) - end - 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 - self.provider = MSRS_Config.GRPC.DefaultProvider - if MSRS_Config.GRPC[MSRS_Config.GRPC.DefaultProvider] then - self.APIKey = MSRS_Config.GRPC[MSRS_Config.GRPC.DefaultProvider].key - self.defaultVoice = MSRS_Config.GRPC[MSRS_Config.GRPC.DefaultProvider].defaultVoice - self.region = MSRS_Config.GRPC[MSRS_Config.GRPC.DefaultProvider].secret - self.secret = MSRS_Config.GRPC[MSRS_Config.GRPC.DefaultProvider].region - end - end - self.ConfigLoaded = true - else - MSRS.path = MSRS_Config.Path or "C:\\Program Files\\DCS-SimpleRadio-Standalone" - MSRS.port = MSRS_Config.Port or 5002 - MSRS.frequencies = MSRS_Config.Frequency or {127,243} - MSRS.modulations = MSRS_Config.Modulation or {0,0} - MSRS.coalition = MSRS_Config.Coalition or 0 - if MSRS_Config.Coordinate then - MSRS.coordinate = COORDINATE:New( MSRS_Config.Coordinate[1],MSRS_Config.Coordinate[2],MSRS_Config.Coordinate[3]) - end - 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 - MSRS.provider = MSRS_Config.GRPC.DefaultProvider - if MSRS_Config.GRPC[MSRS_Config.GRPC.DefaultProvider] then - MSRS.APIKey = MSRS_Config.GRPC[MSRS_Config.GRPC.DefaultProvider].key - MSRS.defaultVoice = MSRS_Config.GRPC[MSRS_Config.GRPC.DefaultProvider].defaultVoice - MSRS.region = MSRS_Config.GRPC[MSRS_Config.GRPC.DefaultProvider].secret - MSRS.secret = MSRS_Config.GRPC[MSRS_Config.GRPC.DefaultProvider].region - end - end - MSRS.ConfigLoaded = true - end - end - 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 - - return true -end + -- Skip this function if _GetCommand was not able to find the executable + if string.find(command, "CommandNotFound") then return 0 end -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- MSRS DCS-gRPC alternate backend -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + local batContent = command.." && exit" + -- Create a tmp file. + local filename=os.getenv('TMP').."\\MSRS-"..MSRS.uuid()..".bat" ---- Alternate backend for MSRS to enable text-to-speech via DCS-gRPC. --- ### Author: **dogjutsu** --- A table containing functions and variables for MSRS to use DCS-gRPC [DCS-gRPC](https://github.com/DCS-gRPC/rust-server) 0.7.0 or newer as a backend to transmit over SRS. --- This is not a standalone class. Instead, variables and functions under the `Vars` and `Functions` tables get added to or replace MSRS variables/functions when activated. --- --- @type MSRS_BACKEND_DCSGRPC --- @field #number version Version number of this alternate backend. --- @field #table Functions A table of functions that will add or replace the default MSRS class functions. --- @field #table Vars A table of variables that will add or replace the default MSRS class variables. -MSRS_BACKEND_DCSGRPC = {} -MSRS_BACKEND_DCSGRPC.version = 0.1 + local script=io.open(filename, "w+") + script:write(batContent) + script:close() -MSRS_BACKEND_DCSGRPC.Functions = {} -MSRS_BACKEND_DCSGRPC.Vars = { provider = 'win' } + self:T("MSRS batch file created: "..filename) + self:T("MSRS batch content: "..batContent) ---- Called by @{#MSRS._NewAltBackend} (if present) immediately after an alternate backend functions and variables for MSRS are added/replaced. --- @param #MSRS self --- @return #MSRS self -MSRS_BACKEND_DCSGRPC.Functions._MSRSbackendInit = function (self) - BASE:I('Loaded MSRS DCS-gRPC alternate backend version ' .. self.AltBackend.version or 'unspecified') + local res=nil + if true then - return self -end + -- Create a tmp file. + local filenvbs = os.getenv('TMP') .. "\\MSRS-"..MSRS.uuid()..".vbs" -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- MSRS DCS-gRPC alternate backend User Functions -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + -- VBS script + local script = io.open(filenvbs, "w+") + script:write(string.format('Dim WinScriptHost\n')) + script:write(string.format('Set WinScriptHost = CreateObject("WScript.Shell")\n')) + script:write(string.format('WinScriptHost.Run Chr(34) & "%s" & Chr(34), 0\n', filename)) + script:write(string.format('Set WinScriptHost = Nothing')) + script:close() + self:T("MSRS vbs file created to start batch="..filenvbs) ---- No-op replacement function for @{#MSRS.SetPath} (Not Applicable) --- @param #MSRS self --- @return #MSRS self -MSRS_BACKEND_DCSGRPC.Functions.SetPath = function (self) - return self -end + -- Run visual basic script. This still pops up a window but very briefly and does not put the DCS window out of focus. + local runvbs=string.format('cscript.exe //Nologo //B "%s"', filenvbs) ---- No-op replacement function for @{#MSRS.GetPath} (Not Applicable) --- @param #MSRS self --- @return #string Empty string -MSRS_BACKEND_DCSGRPC.Functions.GetPath = function (self) - return '' -end + -- Debug output. + self:T("MSRS execute VBS command="..runvbs) ---- No-op replacement function for @{#MSRS.SetVolume} (Not Applicable) --- @param #MSRS self --- @return #MSRS self -MSRS_BACKEND_DCSGRPC.Functions.SetVolume = function (self) - BASE:I('NOTE: MSRS:SetVolume() not used with DCS-gRPC backend.') - return self -end + -- Play file in 0.01 seconds + res=os.execute(runvbs) ---- No-op replacement function for @{#MSRS.GetVolume} (Not Applicable) --- @param #MSRS self --- @return #MSRS self -MSRS_BACKEND_DCSGRPC.Functions.GetVolume = function (self) - BASE:I('NOTE: MSRS:GetVolume() not used with DCS-gRPC backend.') - return 1 -end + -- Remove file in 1 second. + timer.scheduleFunction(os.remove, filename, timer.getTime()+1) + timer.scheduleFunction(os.remove, filenvbs, timer.getTime()+1) + self:T("MSRS vbs and batch file removed") ---- No-op replacement function for @{#MSRS.SetGender} (Not Applicable) --- @param #MSRS self --- #string Gender Gender: "male" or "female" --- @return #MSRS self -MSRS_BACKEND_DCSGRPC.Functions.SetGender = function (self, Gender) - -- Use DCS-gRPC default if not specified + elseif false then - if Gender then - self.gender=Gender:lower() - end - - -- Debug output. - self:T("Setting gender to "..tostring(self.gender)) - return self -end + -- Create a tmp file. + local filenvbs = os.getenv('TMP') .. "\\MSRS-"..MSRS.uuid()..".vbs" ---- Replacement function for @{#MSRS.SetGoogle} to use google text-to-speech. (API key set as part of DCS-gRPC configuration) --- @param #MSRS self --- @return #MSRS self -MSRS_BACKEND_DCSGRPC.Functions.SetGoogle = function (self) - self.provider = 'gcloud' - return self -end + -- VBS script + local script = io.open(filenvbs, "w+") + script:write(string.format('Set oShell = CreateObject ("Wscript.Shell")\n')) + script:write(string.format('Dim strArgs\n')) + script:write(string.format('strArgs = "cmd /c %s"\n', filename)) + script:write(string.format('oShell.Run strArgs, 0, false')) + script:close() ---- MSRS:SetAWS() Use AWS text-to-speech. (API key set as part of DCS-gRPC configuration) --- @param #MSRS self --- @return #MSRS self -MSRS_BACKEND_DCSGRPC.Functions.SetAWS = function (self) - self.provider = 'aws' - return self -end + local runvbs=string.format('cscript.exe //Nologo //B "%s"', filenvbs) ---- MSRS:SetAzure() Use Azure text-to-speech. (API key set as part of DCS-gRPC configuration) --- @param #MSRS self --- @return #MSRS self -MSRS_BACKEND_DCSGRPC.Functions.SetAzure = function (self) - self.provider = 'azure' - return self -end + -- Play file in 0.01 seconds + res=os.execute(runvbs) ---- MSRS:SetWin() Use local Windows OS text-to-speech (Windows Server 2019 / Windows 11 / Windows 10? or newer). (Default) --- @param #MSRS self --- @return #MSRS self -MSRS_BACKEND_DCSGRPC.Functions.SetWin = function (self) - self.provider = 'win' - return self -end - ---- Replacement function for @{#MSRS.Help} to display help. --- @param #MSRS self --- @return #MSRS self -MSRS_BACKEND_DCSGRPC.Functions.Help = function (self) - env.info('For DCS-gRPC help, please see: https://github.com/DCS-gRPC/rust-server') - return self -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- MSRS DCS-gRPC alternate backend Transmission Functions -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- No-op replacement function for @{#MSRS.PlaySoundFile} (Not Applicable) --- @param #MSRS self --- @return #MSRS self -MSRS_BACKEND_DCSGRPC.Functions.PlaySoundFile = function (self) - BASE:E("ERROR: MSRS:PlaySoundFile() is not supported by the DCS-gRPC backend.") - return self -end - ---- Replacement function for @{#MSRS.PlaySoundText} --- @param #MSRS self --- @param Sound.SoundOutput#SOUNDTEXT SoundText Sound text. --- @param #number Delay Delay in seconds, before the sound file is played. --- @return #MSRS self -MSRS_BACKEND_DCSGRPC.Functions.PlaySoundText = function (self, SoundText, Delay) - - if Delay and Delay>0 then - self:ScheduleOnce(Delay, self.PlaySoundText, self, SoundText, 0) - else - self:_DCSgRPCtts(tostring(SoundText.text)) - end - - return self -end - ---- Replacement function for @{#MSRS.PlayText} --- @param #MSRS self --- @param #string Text Text message. --- @param #number Delay Delay in seconds, before the message is played. --- @return #MSRS self -MSRS_BACKEND_DCSGRPC.Functions.PlayText = function (self, Text, Delay) - - if Delay and Delay>0 then - self:ScheduleOnce(Delay, self.PlayText, self, Text, 0) - else - self:_DCSgRPCtts(tostring(Text)) - end - - return self -end - ---- Replacement function for @{#MSRS.PlayText} --- @param #MSRS self --- @param #string Text Text message. --- @param #number Delay Delay in seconds, before the message is played. --- @param #table Frequencies Radio frequencies. --- @param #table Modulations Radio modulations. (Non-functional, DCS-gRPC sets automatically) --- @param #string Gender Gender. (Non-functional, only 'Voice' supported) --- @param #string Culture Culture. (Non-functional, only 'Voice' supported) --- @param #string Voice Voice. --- @param #number Volume Volume. (Non-functional, all transmissions full volume with DCS-gRPC) --- @param #string Label Label. --- @return #MSRS self -MSRS_BACKEND_DCSGRPC.Functions.PlayTextExt = function (self, Text, Delay, Frequencies, Modulations, Gender, Culture, Voice, Volume, Label) - if Delay and Delay>0 then - self:ScheduleOnce(Delay, self.PlayTextExt, self, Text, 0, Frequencies, Modulations, Gender, Culture, Voice, Volume, Label) else - self:_DCSgRPCtts(tostring(Text), nil, Frequencies, Voice, Label) - end - - return self -end + -- Play command. + command=string.format('start /b "" "%s"', filename) ---- No-op replacement function for @{#MSRS.PlayTextFile} (Not Applicable) --- @param #MSRS self --- @return #MSRS self -MSRS_BACKEND_DCSGRPC.Functions.PlayTextFile = function (self, TextFile, Delay) - BASE:E("ERROR: MSRS:PlayTextFile() is not supported by the DCS-gRPC backend.") - return self + -- Debug output. + self:T("MSRS execute command="..command) + + -- Execute command + res=os.execute(command) + + -- Remove file in 1 second. + timer.scheduleFunction(os.remove, filename, timer.getTime()+1) + + end + + + return res end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- MSRS DCS-gRPC alternate backend Misc Functions +-- DCS-gRPC Backend Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- DCS-gRPC v0.70 TTS API call: @@ -1530,101 +1499,281 @@ end --- Make DCS-gRPC API call to transmit text-to-speech over SRS. -- @param #MSRS self -- @param #string Text Text of message to transmit (can also be SSML). --- @param #string Optional plaintext version of message (for accessiblity) -- @param #table Frequencies Radio frequencies to transmit on. Can also accept a number in MHz. --- @param #string Voice Voice for the TTS provider to user. --- @param #string Label Label (SRS diplays as name of the transmitter). +-- @param #string Gender Gender. +-- @param #string Culture Culture. +-- @param #string Voice Voice. +-- @param #number Volume Volume. +-- @param #string Label Label. +-- @param Core.Point#COORDINATE Coordinate Coordinate. -- @return #MSRS self -MSRS_BACKEND_DCSGRPC.Functions._DCSgRPCtts = function (self, Text, Plaintext, Frequencies, Voice, Label) +function MSRS:_DCSgRPCtts(Text, Frequencies, Gender, Culture, Voice, Volume, Label, Coordinate) - BASE:T("MSRS_BACKEND_DCSGRPC:_DCSgRPCtts()") - BASE:T({Text, Plaintext, Frequencies, Voice, Label}) + -- Debug info. + self:F("MSRS_BACKEND_DCSGRPC:_DCSgRPCtts()") + self:F({Text, Frequencies, Gender, Culture, Voice, Volume, Label, Coordinate}) - local options = self.ProviderOptions or MSRS.ProviderOptions or {} -- #MSRS.GRPCOptions - local ssml = Text or '' + local options = {} -- #MSRS.GRPCOptions - local XmitFrequencies = Frequencies or self.Frequency - if type(XmitFrequencies)~="table" then - XmitFrequencies={XmitFrequencies} - end + local ssml = Text or '' - options.plaintext = Plaintext - options.srsClientName = Label or self.Label + -- Get frequenceies. + Frequencies = UTILS.EnsureTable(Frequencies, true) or self:GetFrequencies() + + -- Plain text (not really used. + options.plaintext=Text + + -- Name shows as sender. + options.srsClientName = Label or self.Label + + -- Set position. + if self.coordinate then options.position = {} - if self.coordinate then - options.position.lat, options.position.lon, options.position.alt = self:_GetLatLongAlt(self.coordinate) + options.position.lat, options.position.lon, options.position.alt = self:_GetLatLongAlt(self.coordinate) + end + + -- Coalition (gRPC expects lower case) + options.coalition = UTILS.GetCoalitionName(self.coalition):lower() + + -- Provider (win, gcloud, ...) + local provider = self.provider or MSRS.Provider.WINDOWS + self:F({provider=provider}) + + -- Provider options: voice, credentials + options.provider = {} + options.provider[provider] = self:GetProviderOptions(provider) + + -- Voice + Voice=Voice or self:GetVoice(self.provider) or self.voice + + if Voice then + -- We use a specific voice + options.provider[provider].voice = Voice + else + + -- DCS-gRPC doesn't directly support language/gender, but can use SSML + + local preTag, genderProp, langProp, postTag = '', '', '', '' + + local gender="" + if self.gender then + gender=string.format(' gender=\"%s\"', self.gender) + end + local language="" + if self.culture then + language=string.format(' language=\"%s\"', self.culture) end - options.position.lat = options.position.lat or 0.0 - options.position.lon = options.position.lon or 0.0 - options.position.alt = options.position.alt or 0.0 + if self.gender or self.culture then + ssml=string.format("%s", gender, language, Text) + end + end - if UTILS.GetCoalitionName(self.coalition) == 'Blue' then - options.coalition = 'blue' - elseif UTILS.GetCoalitionName(self.coalition) == 'Red' then - options.coalition = 'red' - end - - local provider = self.provider or self.GRPCOptions.DefaultProvider or MSRS.GRPCOptions.DefaultProvider - - options.provider = {} - - options.provider[provider] = {} - - if self.APIKey then - options.provider[provider].key = self.APIKey - end - - if self.defaultVoice then - options.provider[provider].defaultVoice = self.defaultVoice - end - - if self.voice then - options.provider[provider].voice = Voice or self.voice or self.defaultVoice - elseif ssml then - -- DCS-gRPC doesn't directly support language/gender, but can use SSML - -- Only use if a voice isn't explicitly set - local preTag, genderProp, langProp, postTag = '', '', '', '' + for _,freq in pairs(Frequencies) do + self:F("Calling GRPC.tts with the following parameter:") + self:F({ssml=ssml, freq=freq, options=options}) + self:F(options.provider[provider]) + GRPC.tts(ssml, freq*1e6, options) + end - if self.gender then - genderProp = ' gender=\"' .. self.gender .. '\"' +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Config File +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get central SRS configuration to be able to play tts over SRS radio using the `DCS-SR-ExternalAudio.exe`. +-- @param #MSRS self +-- @param #string Path Path to config file, defaults to "C:\Users\\Saved Games\DCS\Config" +-- @param #string Filename File to load, defaults to "Moose_MSRS.lua" +-- @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, +-- and also make config easier to use in the code. +-- 1) Create a config file named "Moose_MSRS.lua" at this location "C:\Users\\Saved Games\DCS\Config" (or wherever your Saved Games folder resides). +-- 2) The file needs the following structure: +-- +-- -- Moose MSRS default Config +-- MSRS_Config = { +-- Path = C:\\Program Files\\DCS-SimpleRadio-Standalone, -- Path to SRS install directory. +-- Port = 5002, -- Port of SRS server. Default 5002. +-- Backend = "srsexe", -- Interface to SRS: "srsexe" or "grpc". +-- Frequency = {127, 243}, -- Default frequences. Must be a table 1..n entries! +-- Modulation = {0,0}, -- Default modulations. Must be a table, 1..n entries, one for each frequency! +-- Volume = 1.0, -- Default volume [0,1]. +-- Coalition = 0, -- 0 = Neutral, 1 = Red, 2 = Blue (only a factor if SRS server has encryption enabled). +-- Coordinate = {0,0,0}, -- x, y, alt (only a factor if SRS server has line-of-sight and/or distance limit enabled). +-- Culture = "en-GB", +-- Gender = "male", +-- Voice = "Microsoft Hazel Desktop", -- Voice that is used if no explicit provider voice is specified. +-- Label = "MSRS", +-- Provider = "win", --Provider for generating TTS (win, gcloud, azure, aws). +-- +-- -- Windows +-- win = { +-- voice = "Microsoft Hazel Desktop", +-- }, +-- -- Google Cloud +-- gcloud = { +-- voice = "en-GB-Standard-A", -- The Google Cloud voice to use (see https://cloud.google.com/text-to-speech/docs/voices). +-- credentials="C:\\Program Files\\DCS-SimpleRadio-Standalone\\yourfilename.json", -- Full path to credentials JSON file (only for SRS-TTS.exe backend) +-- key="Your access Key", -- Google API access key (only for DCS-gRPC backend) +-- }, +-- -- Amazon Web Service +-- aws = { +-- voice = "Brian", -- The default AWS voice to use (see https://docs.aws.amazon.com/polly/latest/dg/voicelist.html). +-- key="Your access Key", -- Your AWS key. +-- secret="Your secret key", -- Your AWS secret key. +-- region="eu-central-1", -- Your AWS region (see https://docs.aws.amazon.com/general/latest/gr/pol.html). +-- }, +-- -- Microsoft Azure +-- azure = { +-- voice="en-US-AriaNeural", --The default Azure voice to use (see https://learn.microsoft.com/azure/cognitive-services/speech-service/language-support). +-- key="Your access key", -- Your Azure access key. +-- region="westeurope", -- The Azure region to use (see https://learn.microsoft.com/en-us/azure/cognitive-services/speech-service/regions). +-- }, +-- } +-- +-- 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: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: +-- +-- -- Needed once only +-- MESSAGE.SetMSRS(MSRS.path,nil,MSRS.google,243,radio.modulation.AM,nil,nil, +-- MSRS.Voices.Google.Standard.de_DE_Standard_B,coalition.side.BLUE) +-- +-- -- later on in your code +-- +-- MESSAGE:New("Test message!",15,"SPAWN"):ToSRS(243,radio.modulation.AM,nil,nil,MSRS.Voices.Google.Standard.fr_FR_Standard_C) +-- +-- -- Create new ATIS as usual +-- atis=ATIS:New(AIRBASE.Caucasus.Batumi, 123, radio.modulation.AM) +-- atis:SetSRS(nil,nil,nil,MSRS.Voices.Google.Standard.en_US_Standard_H) +-- --Start ATIS +-- atis:Start() +function MSRS:LoadConfigFile(Path,Filename) + + if lfs == nil then + env.info("*****Note - lfs and os need to be desanitized for MSRS to work!") + return false + end + + 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 filexsists and not MSRS.ConfigLoaded then + + env.info("FF reading config file") + + -- Load global MSRS_Config + assert(loadfile(path..file))() + + if MSRS_Config then + + local Self = self or MSRS --#MSRS + + Self.path = MSRS_Config.Path or "C:\\Program Files\\DCS-SimpleRadio-Standalone" + Self.port = MSRS_Config.Port or 5002 + Self.backend = MSRS_Config.Backend or MSRS.Backend.SRSEXE + Self.frequencies = MSRS_Config.Frequency or {127,243} + Self.modulations = MSRS_Config.Modulation or {0,0} + Self.coalition = MSRS_Config.Coalition or 0 + if MSRS_Config.Coordinate then + Self.coordinate = COORDINATE:New( MSRS_Config.Coordinate[1], MSRS_Config.Coordinate[2], MSRS_Config.Coordinate[3] ) end - if self.culture then - langProp = ' language=\"' .. self.culture .. '\"' + Self.culture = MSRS_Config.Culture or "en-GB" + Self.gender = MSRS_Config.Gender or "male" + Self.Label = MSRS_Config.Label or "MSRS" + Self.voice = MSRS_Config.Voice --or MSRS.Voices.Microsoft.Hazel + + Self.provider = MSRS_Config.Provider or MSRS.Provider.WINDOWS + for _,provider in pairs(MSRS.Provider) do + if MSRS_Config[provider] then + Self.poptions[provider]=MSRS_Config[provider] + end end - if self.culture or self.gender then - preTag = '' - postTag = '' - ssml = preTag .. Text .. postTag - end + Self.ConfigLoaded = true end + env.info("MSRS - Successfully loaded default configuration from disk!",false) + end - for _,_freq in ipairs(XmitFrequencies) do - local freq = _freq*1000000 - BASE:T("GRPC.tts") - BASE:T(ssml) - BASE:T(freq) - BASE:T({options}) - GRPC.tts(ssml, freq, options) + if not filexsists then + env.info("MSRS - Cannot find default configuration file!",false) + return false + end + + return true +end + +--- Function returns estimated speech time in seconds. +-- Assumptions for time calc: 100 Words per min, average of 5 letters for english word so +-- +-- * 5 chars * 100wpm = 500 characters per min = 8.3 chars per second +-- +-- So length of msg / 8.3 = number of seconds needed to read it. rounded down to 8 chars per sec map function: +-- +-- * (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min +-- +-- @param #number length can also be passed as #string +-- @param #number speed Defaults to 1.0 +-- @param #boolean isGoogle We're using Google TTS +function MSRS.getSpeechTime(length,speed,isGoogle) + + local maxRateRatio = 3 + + speed = speed or 1.0 + isGoogle = isGoogle or false + + local speedFactor = 1.0 + if isGoogle then + speedFactor = speed + else + if speed ~= 0 then + speedFactor = math.abs( speed ) * (maxRateRatio - 1) / 10 + 1 end + if speed < 0 then + speedFactor = 1 / speedFactor + end + end + local wpm = math.ceil( 100 * speedFactor ) + local cps = math.floor( (wpm * 5) / 60 ) + + if type( length ) == "string" then + length = string.len( length ) + end + + return length/cps --math.ceil(length/cps) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Manages radio transmissions. --- +-- -- The purpose of the MSRSQUEUE class is to manage SRS text-to-speech (TTS) messages using the MSRS class. -- This can be used to submit multiple TTS messages and the class takes care that they are transmitted one after the other (and not overlapping). --- +-- -- @type MSRSQUEUE -- @field #string ClassName Name of the class "MSRSQUEUE". -- @field #string lid ID for dcs.log. -- @field #table queue The queue of transmissions. -- @field #string alias Name of the radio queue. --- @field #number dt Time interval in seconds for checking the radio queue. +-- @field #number dt Time interval in seconds for checking the radio queue. -- @field #number Tlast Time (abs) when the last transmission finished. -- @field #boolean checking If `true`, the queue update function is scheduled to be called again. -- @extends Core.Base#BASE @@ -1670,13 +1819,13 @@ function MSRSQUEUE:New(alias) -- Inherit base local self=BASE:Inherit(self, BASE:New()) --#MSRSQUEUE - + self.alias=alias or "My Radio" - + self.dt=1.0 - + self.lid=string.format("MSRSQUEUE %s | ", self.alias) - + return self end @@ -1692,17 +1841,17 @@ end --- Add a transmission to the radio queue. -- @param #MSRSQUEUE self --- @param #MSRSQUEUE.Transmission transmission The transmission data table. +-- @param #MSRSQUEUE.Transmission transmission The transmission data table. -- @return #MSRSQUEUE self function MSRSQUEUE:AddTransmission(transmission) - + -- Init. transmission.isplaying=false transmission.Tstarted=nil -- Add to queue. table.insert(self.queue, transmission) - + -- Start checking. if not self.checking then self:_CheckRadioQueue() @@ -1748,13 +1897,13 @@ end -- @param Core.Point#COORDINATE coordinate Coordinate to be used -- @return #MSRSQUEUE.Transmission Radio transmission table. function MSRSQUEUE:NewTransmission(text, duration, msrs, tstart, interval, subgroups, subtitle, subduration, frequency, modulation, gender, culture, voice, volume, label,coordinate) - + if self.TransmitOnlyWithPlayers then if self.PlayerSet and self.PlayerSet:CountAlive() == 0 then return self end end - + -- Sanity checks. if not text then self:E(self.lid.."ERROR: No text specified.") @@ -1762,14 +1911,14 @@ function MSRSQUEUE:NewTransmission(text, duration, msrs, tstart, interval, subgr end if type(text)~="string" then self:E(self.lid.."ERROR: Text specified is NOT a string.") - return nil + return nil end - + -- Create a new transmission object. local transmission={} --#MSRSQUEUE.Transmission transmission.text=text - transmission.duration=duration or STTS.getSpeechTime(text) + transmission.duration=duration or MSRS.getSpeechTime(text) transmission.msrs=msrs transmission.Tplay=tstart or timer.getAbsTime() transmission.subtitle=subtitle @@ -1788,10 +1937,10 @@ function MSRSQUEUE:NewTransmission(text, duration, msrs, tstart, interval, subgr transmission.volume = volume transmission.label = label transmission.coordinate = coordinate - - -- Add transmission to queue. + + -- Add transmission to queue. self:AddTransmission(transmission) - + return transmission end @@ -1805,27 +1954,27 @@ function MSRSQUEUE:Broadcast(transmission) else transmission.msrs:PlayText(transmission.text,nil,transmission.coordinate) end - + local function texttogroup(gid) -- Text to group. - trigger.action.outTextForGroup(gid, transmission.subtitle, transmission.subduration, true) + trigger.action.outTextForGroup(gid, transmission.subtitle, transmission.subduration, true) end - + if transmission.subgroups and #transmission.subgroups>0 then - + for _,_group in pairs(transmission.subgroups) do local group=_group --Wrapper.Group#GROUP - + if group and group:IsAlive() then local gid=group:GetID() - - self:ScheduleOnce(4, texttogroup, gid) + + self:ScheduleOnce(4, texttogroup, gid) end - + end - + end - + end --- Calculate total transmission duration of all transmission in the queue. @@ -1838,18 +1987,18 @@ function MSRSQUEUE:CalcTransmisstionDuration() local T=0 for _,_transmission in pairs(self.queue) do local transmission=_transmission --#MSRSQUEUE.Transmission - + if transmission.isplaying then - + -- Playing for dt seconds. local dt=Tnow-transmission.Tstarted - + T=T+transmission.duration-dt - + else T=T+transmission.duration end - + end return T @@ -1860,146 +2009,146 @@ end -- @param #number delay Delay in seconds before checking. function MSRSQUEUE:_CheckRadioQueue(delay) - -- Transmissions in queue. + -- Transmissions in queue. local N=#self.queue -- Debug info. self:T2(self.lid..string.format("Check radio queue %s: delay=%.3f sec, N=%d, checking=%s", self.alias, delay or 0, N, tostring(self.checking))) - + if delay and delay>0 then - + -- Delayed call. self:ScheduleOnce(delay, MSRSQUEUE._CheckRadioQueue, self) - + -- Checking on. self.checking=true - + else -- Check if queue is empty. if N==0 then - + -- Debug info. self:T(self.lid..string.format("Check radio queue %s empty ==> disable checking", self.alias)) - + -- Queue is now empty. Nothing to else to do. We start checking again, if a transmission is added. self.checking=false - + return end -- Get current abs time. local time=timer.getAbsTime() - + -- Checking on. self.checking=true - + -- Set dt. local dt=self.dt - - + + local playing=false local next=nil --#MSRSQUEUE.Transmission local remove=nil for i,_transmission in ipairs(self.queue) do local transmission=_transmission --#MSRSQUEUE.Transmission - + -- Check if transmission time has passed. - if time>=transmission.Tplay then - + if time>=transmission.Tplay then + -- Check if transmission is currently playing. if transmission.isplaying then - + -- Check if transmission is finished. if time>=transmission.Tstarted+transmission.duration then - + -- Transmission over. transmission.isplaying=false - + -- Remove ith element in queue. remove=i - + -- Store time last transmission finished. self.Tlast=time - + else -- still playing - + -- Transmission is still playing. playing=true - + dt=transmission.duration-(time-transmission.Tstarted) - + end - + else -- not playing yet - + local Tlast=self.Tlast - + if transmission.interval==nil then - + -- Not playing ==> this will be next. if next==nil then next=transmission end - + else - + if Tlast==nil or time-Tlast>=transmission.interval then - next=transmission + next=transmission else - + end end - + -- We got a transmission or one with an interval that is not due yet. No need for anything else. if next or Tlast then break end - + end - + else - + -- Transmission not due yet. - - end + + end end - + -- Found a new transmission. if next~=nil and not playing then -- Debug info. self:T(self.lid..string.format("Broadcasting text=\"%s\" at T=%.3f", next.text, time)) - + -- Call SRS. self:Broadcast(next) - + next.isplaying=true next.Tstarted=time dt=next.duration end - + -- Remove completed call from queue. if remove then -- Remove from queue. table.remove(self.queue, remove) N=N-1 - + -- Check if queue is empty. if #self.queue==0 then -- Debug info. self:T(self.lid..string.format("Check radio queue %s empty ==> disable checking", self.alias)) - + self.checking=false - + return end end - + -- Check queue. self:_CheckRadioQueue(dt) - + end - + end MSRS.LoadConfigFile() diff --git a/Moose Development/Moose/Sound/SoundOutput.lua b/Moose Development/Moose/Sound/SoundOutput.lua index 38a8337fe..408be7b96 100644 --- a/Moose Development/Moose/Sound/SoundOutput.lua +++ b/Moose Development/Moose/Sound/SoundOutput.lua @@ -160,52 +160,63 @@ do -- Sound File -- @param #string FileName The name of the sound file, e.g. "Hello World.ogg". -- @param #string Path The path of the directory, where the sound file is located. Default is "l10n/DEFAULT/" within the miz file. -- @param #number Duration Duration in seconds, how long it takes to play the sound file. Default is 3 seconds. + -- @param #bolean UseSrs Set if SRS should be used to play this file. Default is false. -- @return #SOUNDFILE self - function SOUNDFILE:New(FileName, Path, Duration) - + function SOUNDFILE:New(FileName, Path, Duration, UseSrs) + -- Inherit BASE. local self=BASE:Inherit(self, BASE:New()) -- #SOUNDFILE + -- Debug info: + self:F( {FileName, Path, Duration, UseSrs} ) + -- Set file name. self:SetFileName(FileName) - + + -- Set if SRS should be used to play this file + self:SetPlayWithSRS(UseSrs or false) + -- Set path. self:SetPath(Path) - + -- Set duration. self:SetDuration(Duration) - - -- Debug info: - self:T(string.format("New SOUNDFILE: file name=%s, path=%s", self.filename, self.path)) return self end - + --- Set path, where the sound file is located. -- @param #SOUNDFILE self -- @param #string Path Path to the directory, where the sound file is located. In case this is nil, it defaults to the DCS mission temp directory. -- @return #SOUNDFILE self function SOUNDFILE:SetPath(Path) - + self:F( {Path} ) + -- Init path. - self.path=Path or "l10n/DEFAULT/" - - if not Path and self.useSRS then -- use path to mission temp dir - self.path = os.getenv('TMP') .. "\\DCS\\Mission\\l10n\\DEFAULT" - end - + if not Path then + if self.useSRS then -- use path to mission temp dir + self.path = lfs.tempdir() .. "Mission\\l10n\\DEFAULT" + else -- use internal path in miz file + self.path="l10n/DEFAULT/" + end + else + self.path = Path + end + -- Remove (back)slashes. local nmax=1000 ; local n=1 while (self.path:sub(-1)=="/" or self.path:sub(-1)==[[\]]) and n<=nmax do self.path=self.path:sub(1,#self.path-1) n=n+1 end - + -- Append slash. self.path=self.path.."/" - + + self:T("self.path=".. self.path) + return self - end + end --- Get path of the directory, where the sound file is located. -- @param #SOUNDFILE self @@ -228,7 +239,7 @@ do -- Sound File --- Get the sound file name. -- @param #SOUNDFILE self -- @return #string Name of the soud file. This does *not* include its path. - function SOUNDFILE:GetFileName() + function SOUNDFILE:GetFileName() return self.filename end @@ -264,14 +275,16 @@ do -- Sound File -- @param #boolean Switch If true or nil, use SRS. If false, use DCS transmission. -- @return #SOUNDFILE self function SOUNDFILE:SetPlayWithSRS(Switch) + self:F( {Switch} ) if Switch==true or Switch==nil then self.useSRS=true else self.useSRS=false end + self:T("self.useSRS=".. tostring(self.useSRS)) return self - end - + end + end do -- Text-To-Speech diff --git a/Moose Development/Moose/Utilities/Enums.lua b/Moose Development/Moose/Utilities/Enums.lua index beab1f0fb..d3b63882f 100644 --- a/Moose Development/Moose/Utilities/Enums.lua +++ b/Moose Development/Moose/Utilities/Enums.lua @@ -523,6 +523,7 @@ ENUMS.ReportingName = Hawkeye = "E-2D", Sentry = "E-3A", Stratotanker = "KC-135", + Gasstation = "KC-135MPRS", Extender = "KC-10", Orion = "P-3C", Viking = "S-3B", diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 9fe3d9823..49739f2a6 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -1064,9 +1064,9 @@ function UTILS.BeaufortScale(speed) return bn,bd end ---- Split string at seperators. C.f. [split-string-in-lua](http://stackoverflow.com/questions/1426954/split-string-in-lua). +--- Split string at separators. C.f. [split-string-in-lua](http://stackoverflow.com/questions/1426954/split-string-in-lua). -- @param #string str Sting to split. --- @param #string sep Speparator for split. +-- @param #string sep Separator for split. -- @return #table Split text. function UTILS.Split(str, sep) local result = {} @@ -2146,17 +2146,17 @@ function UTILS.IsLoadingDoorOpen( unit_name ) return true end - if string.find(type_name, "Bell-47") then -- bell aint got no doors so always ready to load injured soldiers + if type_name == "Bell-47" then -- bell aint got no doors so always ready to load injured soldiers BASE:T(unit_name .. " door is open") return true end - - if string.find(type_name, "UH-60L") and (unit:getDrawArgumentValue(401) == 1 or unit:getDrawArgumentValue(402) == 1) then + + if type_name == "UH-60L" and (unit:getDrawArgumentValue(401) == 1 or unit:getDrawArgumentValue(402) == 1) then BASE:T(unit_name .. " cargo door is open") return true end - if string.find(type_name, "UH-60L" ) and (unit:getDrawArgumentValue(38) == 1 or unit:getDrawArgumentValue(400) == 1 ) then + if type_name == "UH-60L" and (unit:getDrawArgumentValue(38) > 0 or unit:getDrawArgumentValue(400) == 1 ) then BASE:T(unit_name .. " front door(s) are open") return true end diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index e013df17b..67a3cdcfe 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -2937,7 +2937,7 @@ function CONTROLLABLE:CopyRoute( Begin, End, Randomize, Radius ) end --- Return the detected targets of the controllable. --- The optional parametes specify the detection methods that can be applied. +-- The optional parameters specify the detection methods that can be applied. -- If no detection method is given, the detection will use all the available methods by default. -- @param #CONTROLLABLE self -- @param #boolean DetectVisual (optional) @@ -3772,54 +3772,66 @@ function CONTROLLABLE:OptionProhibitAfterburner( Prohibit ) return self end ---- Defines the usage of Electronic Counter Measures by airborne forces. Disables the ability for AI to use their ECM. +--- [Air] Defines the usage of Electronic Counter Measures by airborne forces. -- @param #CONTROLLABLE self +-- @param #number ECMvalue Can be - 0=Never on, 1=if locked by radar, 2=if detected by radar, 3=always on, defaults to 1 -- @return #CONTROLLABLE self -function CONTROLLABLE:OptionECM_Never() +function CONTROLLABLE:OptionECM( ECMvalue ) self:F2( { self.ControllableName } ) - if self:IsAir() then - self:SetOption( AI.Option.Air.id.ECM_USING, 0 ) + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + + if self:IsAir() then + Controller:setOption( AI.Option.Air.id.ECM_USING, ECMvalue or 1 ) + end + end return self end ---- Defines the usage of Electronic Counter Measures by airborne forces. If the AI is actively being locked by an enemy radar they will enable their ECM jammer. +--- [Air] Defines the usage of Electronic Counter Measures by airborne forces. Disables the ability for AI to use their ECM. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionECM_Never() + self:F2( { self.ControllableName } ) + + self:OptionECM(0) + + return self +end + +--- [Air] Defines the usage of Electronic Counter Measures by airborne forces. If the AI is actively being locked by an enemy radar they will enable their ECM jammer. -- @param #CONTROLLABLE self -- @return #CONTROLLABLE self function CONTROLLABLE:OptionECM_OnlyLockByRadar() self:F2( { self.ControllableName } ) - if self:IsAir() then - self:SetOption( AI.Option.Air.id.ECM_USING, 1 ) - end + self:OptionECM(1) return self end ---- Defines the usage of Electronic Counter Measures by airborne forces. If the AI is being detected by a radar they will enable their ECM. +--- [Air] Defines the usage of Electronic Counter Measures by airborne forces. If the AI is being detected by a radar they will enable their ECM. -- @param #CONTROLLABLE self -- @return #CONTROLLABLE self function CONTROLLABLE:OptionECM_DetectedLockByRadar() self:F2( { self.ControllableName } ) - if self:IsAir() then - self:SetOption( AI.Option.Air.id.ECM_USING, 2 ) - end + self:OptionECM(2) return self end ---- Defines the usage of Electronic Counter Measures by airborne forces. AI will leave their ECM on all the time. +--- [Air] Defines the usage of Electronic Counter Measures by airborne forces. AI will leave their ECM on all the time. -- @param #CONTROLLABLE self -- @return #CONTROLLABLE self function CONTROLLABLE:OptionECM_AlwaysOn() self:F2( { self.ControllableName } ) - if self:IsAir() then - self:SetOption( AI.Option.Air.id.ECM_USING, 3 ) - end + self:OptionECM(3) return self end @@ -4013,14 +4025,22 @@ end -- @param #boolean onroad If true, route on road (less problems with AI way finding), default true -- @param #boolean shortcut If true and onroad is set, take a shorter route - if available - off road, default false -- @param #string formation Formation string as in the mission editor, e.g. "Vee", "Diamond", "Line abreast", etc. Defaults to "Off Road" +-- @param #boolean onland (optional) If true, try up to 50 times to get a coordinate on land.SurfaceType.LAND. Note - this descriptor value is not reliably implemented on all maps. -- @return #CONTROLLABLE self -function CONTROLLABLE:RelocateGroundRandomInRadius( speed, radius, onroad, shortcut, formation ) +function CONTROLLABLE:RelocateGroundRandomInRadius( speed, radius, onroad, shortcut, formation, onland ) self:F2( { self.ControllableName } ) local _coord = self:GetCoordinate() local _radius = radius or 500 local _speed = speed or 20 local _tocoord = _coord:GetRandomCoordinateInRadius( _radius, 100 ) + if onland then + for i=1,50 do + local island = _tocoord:GetSurfaceType() == land.SurfaceType.LAND and true or false + if island then break end + _tocoord = _coord:GetRandomCoordinateInRadius( _radius, 100 ) + end + end local _onroad = onroad or true local _grptsk = {} local _candoroad = false diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index d5fc018f3..9e09c5c64 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -302,7 +302,7 @@ end --- Find the first(!) GROUP matching using patterns. Note that this is **a lot** slower than `:FindByName()`! -- @param #GROUP self --- @param #string Pattern The pattern to look for. Refer to [LUA patterns](http://www.easyuo.com/openeuo/wiki/index.php/Lua_Patterns_and_Captures_(Regular_Expressions)) for regular expressions in LUA. +-- @param #string Pattern The pattern to look for. Refer to [LUA patterns](http://www.easyuo.com/openeuo/wiki/index.php/Lua_Patterns_and_Captures_\(Regular_Expressions\)) for regular expressions in LUA. -- @return #GROUP The GROUP. -- @usage -- -- Find a group with a partial group name @@ -327,7 +327,7 @@ end --- Find all GROUP objects matching using patterns. Note that this is **a lot** slower than `:FindByName()`! -- @param #GROUP self --- @param #string Pattern The pattern to look for. Refer to [LUA patterns](http://www.easyuo.com/openeuo/wiki/index.php/Lua_Patterns_and_Captures_(Regular_Expressions)) for regular expressions in LUA. +-- @param #string Pattern The pattern to look for. Refer to [LUA patterns](http://www.easyuo.com/openeuo/wiki/index.php/Lua_Patterns_and_Captures_\(Regular_Expressions\)) for regular expressions in LUA. -- @return #table Groups Table of matching #GROUP objects found -- @usage -- -- Find all group with a partial group name @@ -2765,7 +2765,7 @@ end --- Switch on/off invisible flag for the group. -- @param #GROUP self --- @param #boolean switch If true, emission is enabled. If false, emission is disabled. +-- @param #boolean switch If true, Invisible is enabled. If false, Invisible is disabled. -- @return #GROUP self function GROUP:SetCommandInvisible(switch) self:F2( self.GroupName ) @@ -2779,7 +2779,7 @@ end --- Switch on/off immortal flag for the group. -- @param #GROUP self --- @param #boolean switch If true, emission is enabled. If false, emission is disabled. +-- @param #boolean switch If true, Immortal is enabled. If false, Immortal is disabled. -- @return #GROUP self function GROUP:SetCommandImmortal(switch) self:F2( self.GroupName ) @@ -2985,3 +2985,36 @@ function GROUP:GetGroupSTN() return tSTN,text end +--- [GROUND] Determine if a GROUP is a SAM unit, i.e. has radar or optical tracker and is no mobile AAA. +-- @param #GROUP self +-- @return #boolean IsSAM True if SAM, else false +function GROUP:IsSAM() + local issam = false + local units = self:GetUnits() + for _,_unit in pairs(units or {}) do + local unit = _unit -- Wrapper.Unit#UNIT + if unit:HasSEAD() and unit:IsGround() and (not unit:HasAttribute("Mobile AAA")) then + issam = true + break + end + end + return issam +end + +--- [GROUND] Determine if a GROUP has a AAA unit, i.e. has no radar or optical tracker but the AAA = true or the "Mobile AAA" = true attribute. +-- @param #GROUP self +-- @return #boolean IsSAM True if AAA, else false +function GROUP:IsAAA() + local issam = false + local units = self:GetUnits() + for _,_unit in pairs(units or {}) do + local unit = _unit -- Wrapper.Unit#UNIT + local desc = unit:GetDesc() or {} + local attr = desc.attributes or {} + if unit:HasSEAD() then return false end + if attr["AAA"] or attr["SAM related"] then + issam = true + end + end + return issam +end diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index 0b3e3300f..322d23540 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -164,7 +164,7 @@ end --- Find the first(!) UNIT matching using patterns. Note that this is **a lot** slower than `:FindByName()`! -- @param #UNIT self --- @param #string Pattern The pattern to look for. Refer to [LUA patterns](http://www.easyuo.com/openeuo/wiki/index.php/Lua_Patterns_and_Captures_(Regular_Expressions)) for regular expressions in LUA. +-- @param #string Pattern The pattern to look for. Refer to [LUA patterns](http://www.easyuo.com/openeuo/wiki/index.php/Lua_Patterns_and_Captures_\(Regular_Expressions\)) for regular expressions in LUA. -- @return #UNIT The UNIT. -- @usage -- -- Find a group with a partial group name @@ -189,7 +189,7 @@ end --- Find all UNIT objects matching using patterns. Note that this is **a lot** slower than `:FindByName()`! -- @param #UNIT self --- @param #string Pattern The pattern to look for. Refer to [LUA patterns](http://www.easyuo.com/openeuo/wiki/index.php/Lua_Patterns_and_Captures_(Regular_Expressions)) for regular expressions in LUA. +-- @param #string Pattern The pattern to look for. Refer to [LUA patterns](http://www.easyuo.com/openeuo/wiki/index.php/Lua_Patterns_and_Captures_\(Regular_Expressions\)) for regular expressions in LUA. -- @return #table Units Table of matching #UNIT objects found -- @usage -- -- Find all group with a partial group name diff --git a/Moose Setup/Moose.files b/Moose Setup/Moose.files index 90a6e761d..a863db719 100644 --- a/Moose Setup/Moose.files +++ b/Moose Setup/Moose.files @@ -83,6 +83,7 @@ Functional/Autolase.lua Functional/AICSAR.lua Functional/AmmoTruck.lua Functional/ZoneGoalCargo.lua +Functional/Tiresias.lua Ops/Airboss.lua Ops/RecoveryTanker.lua diff --git a/docs/beginner/ask-for-help.md b/docs/beginner/ask-for-help.md new file mode 100644 index 000000000..bf8a9c8f6 --- /dev/null +++ b/docs/beginner/ask-for-help.md @@ -0,0 +1,77 @@ +--- +parent: Beginner +nav_order: 06 +--- +# How to ask for help +{: .no_toc } + +1. Table of contents +{:toc} + +After you have tried to solve the problem on your own, you can also get help +from the community. + +{: .highlight } +> But it is important to follow certain rules! Read them below. + +## Communities + +There are two ways to communicate with the community. +The fastest way is to use Discord: + +- {:target="_blank"} + +But if you don't like Discord, you are able to post in the DCS forum. +Check out the MOOSE thread here: + +- + +## How to post requests + +MOOSE is a community project and support is community based. + +Please remember when posting a question: + +- Before posting anything follow the [troubleshooting steps]. +- **Read your logs**. + +A post should contain the following: + +1. A describtion what you expected to happen and what actually happened. + - Do not use vague words this stuff is hard to help with! Be specific. + +2. Describe what happens instead. + - The less detail you offer, the less chance you can be helped. + - Don’t say it doesn’t work. Or is it broken. Say what it actually does. + +3. Post your code in Discord as formatted code: + + - Wrap a single line of code in backticks \` like this: + + ![discord-single-line-code.png](../images/beginner/discord-single-line-code.png) + + - Multiple lines of code should be posted like this: + + ![discord-multi-line-code.png](../images/beginner/discord-multi-line-code.png) + +- Post your log lines with the error or warning messages. Format them like this: + + ![discord-fomat-logs.png](../images/beginner/discord-fomat-logs.png) + +- Some complex problems need the mission (.miz file) also. + + - But post your mission only when requested. + - Try to simplify your mission if it is complex! + +There are people in the Discord and in the forum, who spend their free time to +help you.
+It is your responsibility to make their "work" as easy as possible. + +Welcome to MOOSE and good luck! + +## Next step + +Last but not least some [tipps and tricks]. + +[troubleshooting steps]: problems.md +[tipps and tricks]: tipps-and-tricks.md diff --git a/docs/beginner/demo-missions.md b/docs/beginner/demo-missions.md index 073ae42fd..30ba6593d 100644 --- a/docs/beginner/demo-missions.md +++ b/docs/beginner/demo-missions.md @@ -9,5 +9,50 @@ nav_order: 04 1. Table of contents {:toc} -{: .warning } -> THIS DOCUMENT IS STILL WORK IN PROGRESS! +The best way to get comfortable with a Moose class is to try the demo missions +of the class you want to learn. The Moose team created a lot of demo missions +for most of the classes. + +## Download demo missions + +Go to the repository [MOOSE_MISSIONS]{:target="_blank"}, search the folder of +the class, download the mission (`.miz`) and run them. + +## Read the mission script + +In the same folder a `.lua` file with the same name is placed which is the +included mission script. You can watch these mission scripts easily online at +GitHub to understand what is happening in the mission. + +## Read documentation + +Next step is to read the [documentation]{:target="_blank"} of the class to +understand the code of the demo mission. + +{: .note } +> The documentation is quite long and might be confusing for beginners. +> Start by looking at the description at the top of the documentation of a +> class. It often contains examples and explanations.

+> Then search for the function names and look at the description of the +> functions and its parameters. + +## Make small changes to the script + +Download the `.lua` file, change the parameters to suit your needs in +[Notepad++]{:target="_blank"}, add it to the mission and rerun the mission. +Observe what happens and adapt the code. + +If you want to use more functions combine them all up. + +{: .note } +> But it is wise to do this in small steps. So it is easier to find errors. + +## Next step + +If the mission does not show the expected behaviour take a look at section +[problems]. + +[MOOSE_MISSIONS]: https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop +[documentation]: https://flightcontrol-master.github.io/MOOSE_DOCS_DEVELOP/Documentation/index.html +[Notepad++]: https://notepad-plus-plus.org/downloads/ +[problems]: problems.md diff --git a/docs/beginner/hello-world-build.md b/docs/beginner/hello-world-build.md index 1f7509abd..5227a295d 100644 --- a/docs/beginner/hello-world-build.md +++ b/docs/beginner/hello-world-build.md @@ -144,9 +144,16 @@ have create everything on your own. - Change the text a little bit, like `Hello Dude! ...` and save the file. - Run the mission again. - The text will not be changed in the mission. Why? - The mission editor copies the script into the mission file when you add it. - Ever change on the script file on your hard disk is not recognized by mission editor. - You have to add the file after each change again. + +{: .important } +The mission editor copies the script into the mission file when you add it. +Every change on the script file on your hard disk is not recognized by mission +editor. **You have to add the file after each change again!** + +There is also another method available to dynamically load mission scripts. +But this method has some brawbacks and will be explained in the advanced section. + +Now we add the mission script again: - On the left side of the `TRIGGERS` dialog click on `Load Mission Script`. - On the right side under `ACTIONS` you need to add the script again: diff --git a/docs/beginner/problems.md b/docs/beginner/problems.md new file mode 100644 index 000000000..ebc71077a --- /dev/null +++ b/docs/beginner/problems.md @@ -0,0 +1,46 @@ +--- +parent: Beginner +nav_order: 05 +--- + +# Problems +{: .no_toc } + +1. Table of contents +{:toc} + +## Something went wrong + +If the mission shows not the expected behaviour do the following steps: + +1. Double check if you added the changed mission script to the mission again! +1. Check if the triggers are configured as requested in the last sections. + +## Read the logs + +The DCS log is a super important and useful log for the entire of DCS World. +All scripting and other errors are recorded here. It is the one stop shop for +things that occurred in your mission. It will tell you if there was a mistake. + +1. Open the file `dcs.log` in the `Logs` subfolder in your DCS + [Saved Games folder]. + +1. Search for the following line: `*** MOOSE INCLUDE END ***` + - If it is included in the log, Moose was loaded. + - If the line is not in the log check the triggers again! + +1. Search for lines with `SCRIPTING` and `WARNING` or `ERROR` and read them. + - This might help to find your error. + + {: .note } + > You will find a lot of warning and error lines in the log which are not + > related to `SCRIPTING`. They are related to stuff from Eagle Dynamics or + > Third Parties and you have to ignore them. EA does the same. ;o) + +## Next step + +If you don't find the error and/or don't understand the messages in the log file +you can [ask for help]. + +[Saved Games folder]: tipps-and-tricks.md#find-the-saved-games-folder +[ask for help]: ask-for-help.md diff --git a/docs/beginner/tipps-and-tricks.md b/docs/beginner/tipps-and-tricks.md index 3ea1bd781..f97615318 100644 --- a/docs/beginner/tipps-and-tricks.md +++ b/docs/beginner/tipps-and-tricks.md @@ -33,7 +33,9 @@ This folder can be found in your userprofile as subfolder of `Saved Games`. The easiest way to find it, is to open search and paste the text below into it and press Enter: -```%userprofile%\Saved Games``` +``` +%userprofile%\Saved Games +``` {: .note } > The text will work even if your Windows is installed with another language, diff --git a/docs/images/beginner/discord-fomat-logs.png b/docs/images/beginner/discord-fomat-logs.png new file mode 100644 index 000000000..a78cecce3 Binary files /dev/null and b/docs/images/beginner/discord-fomat-logs.png differ diff --git a/docs/images/beginner/discord-multi-line-code.png b/docs/images/beginner/discord-multi-line-code.png new file mode 100644 index 000000000..84b7f3841 Binary files /dev/null and b/docs/images/beginner/discord-multi-line-code.png differ diff --git a/docs/images/beginner/discord-single-line-code.png b/docs/images/beginner/discord-single-line-code.png new file mode 100644 index 000000000..fc1e7dafd Binary files /dev/null and b/docs/images/beginner/discord-single-line-code.png differ diff --git a/docs/repositories.md b/docs/repositories.md index eb181e646..b52c4cd37 100644 --- a/docs/repositories.md +++ b/docs/repositories.md @@ -26,14 +26,30 @@ You only need to load **one** of those files at the beginning of your mission. This repository contains the generated documentation and pictures and other references. The generated documentation is reflected in html and is published at: -- Stable `master` branch: +- `master` branch: - `develop` branch: +## [MOOSE_GUIDES](https://github.com/FlightControl-Master/MOOSE_GUIDES) - For external documentation and help + +This repository will be removed in future. + +## [MOOSE_PRESENTATIONS](https://github.com/FlightControl-Master/MOOSE_PRESENTATIONS) + +A collection of presentations used in the videos on the youtube channel of FlightControl. + ## [MOOSE_MISSIONS](https://github.com/FlightControl-Master/MOOSE_MISSIONS) - For users (provides demo missions) This repository contains all the demonstration missions in packed format (*.miz), and can be used without any further setup in DCS WORLD. +## [Moose_Community_Scripts](https://github.com/FlightControl-Master/Moose_Community_Scripts) + +This repository is for Moose based helper scripts, snippets, functional demos. + +## [MOOSE_SOUND](https://github.com/FlightControl-Master/MOOSE_SOUND) + +Sound packs for different MOOSE framework classes. + ## [MOOSE_MISSIONS_DYNAMIC](https://github.com/FlightControl-Master/MOOSE_MISSIONS_DYNAMIC) - Outdated This repository will be removed in future. @@ -41,3 +57,11 @@ This repository will be removed in future. ## [MOOSE_MISSIONS_UNPACKED](https://github.com/FlightControl-Master/MOOSE_MISSIONS_UNPACKED) - Outdated This repository will be removed in future. + +## [MOOSE_COMMUNITY_MISSIONS](https://github.com/FlightControl-Master/MOOSE_COMMUNITY_MISSIONS) - Outdated + +A database of missions created by the community, using MOOSE. + +## [MOOSE_TOOLS](https://github.com/FlightControl-Master/MOOSE_TOOLS) - Outdated + +A collection of the required tools to develop and contribute in the MOOSE framework for DCS World.