Merge branch 'FF/Ops' into FF/OpsDev

This commit is contained in:
Frank 2021-08-02 11:59:32 +02:00
commit a1c9c1538d
86 changed files with 25224 additions and 5635 deletions

View File

@ -26,24 +26,25 @@ init:
# - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
install:
- cmd:
# Outcomment if lua environment invalidates and needs to be reinstalled, otherwise all will run from the cache.
# - call choco install 7zip.commandline
# - call choco install lua51
# - call choco install luarocks
# - call refreshenv
# - call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat"
# - cmd: PATH = %PATH%;C:\ProgramData\chocolatey\lib\luarocks\luarocks-2.4.3-win32\systree\bin
# - cmd: set LUA_PATH = %LUA_PATH%;C:\ProgramData\chocolatey\lib\luarocks\luarocks-2.4.3-win32\systree\share\lua\5.1\?.lua;C:\ProgramData\chocolatey\lib\luarocks\luarocks-2.4.3-win32\systree\share\lua\5.1\?\init.lua
# - cmd: set LUA_CPATH = %LUA_CPATH%;C:\ProgramData\chocolatey\lib\luarocks\luarocks-2.4.3-win32\systree\lib\lua\5.1\?.dll
# - call luarocks install luasrcdiet
# - call luarocks install checks
# - call luarocks install luadocumentor
# - call luarocks install luacheck
call choco install 7zip.commandline
call choco install lua51
call choco install luarocks
call refreshenv
call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat"
cmd: PATH = %PATH%;C:\ProgramData\chocolatey\lib\luarocks\luarocks-2.4.3-win32\systree\bin
cmd: set LUA_PATH = %LUA_PATH%;C:\ProgramData\chocolatey\lib\luarocks\luarocks-2.4.3-win32\systree\share\lua\5.1\?.lua;C:\ProgramData\chocolatey\lib\luarocks\luarocks-2.4.3-win32\systree\share\lua\5.1\?\init.lua
cmd: set LUA_CPATH = %LUA_CPATH%;C:\ProgramData\chocolatey\lib\luarocks\luarocks-2.4.3-win32\systree\lib\lua\5.1\?.dll
call luarocks install luasrcdiet
call luarocks install checks
call luarocks install luadocumentor
call luarocks install luacheck
#cache:
# - C:\ProgramData\chocolatey\lib
# - C:\ProgramData\chocolatey\bin
cache:
C:\ProgramData\chocolatey\lib
C:\ProgramData\chocolatey\bin
@ -51,8 +52,9 @@ build_script:
- ps: |
if( $env:appveyor_repo_branch -eq 'master' -or $env:appveyor_repo_branch -eq 'develop' )
{
echo "Hello World!"
$apiUrl = 'https://ci.appveyor.com/api'
$token = 'qts80b5kpq0ooj4x6vvw'
$token = 'v2.6hcv3ige78kg3yvg4ge8'
$headers = @{
"Authorization" = "Bearer $token"
"Content-type" = "application/json"
@ -65,7 +67,7 @@ build_script:
if( $env:appveyor_repo_branch -eq 'master' -or $env:appveyor_repo_branch -eq 'develop' )
{
$apiUrl = 'https://ci.appveyor.com/api'
$token = 'qts80b5kpq0ooj4x6vvw'
$token = 'v2.6hcv3ige78kg3yvg4ge8'
$headers = @{
"Authorization" = "Bearer $token"
"Content-type" = "application/json"

9
.gitignore vendored
View File

@ -18,6 +18,8 @@ local.properties
# External tool builders
.externalToolBuilders/
# AppVeyor
.appveyor/
# CDT-specific
.cproject
@ -26,6 +28,13 @@ local.properties
.buildpath
#####################
## Visual Studio Code
#####################
*.code-workspace
.vscode/
#################
## Visual Studio
#################

54
.luacheckrc Normal file
View File

@ -0,0 +1,54 @@
ignore = {
"011", -- A syntax error.
"021", -- An invalid inline option.
"022", -- An unpaired inline push directive.
"023", -- An unpaired inline pop directive.
"111", -- Setting an undefined global variable.
"112", -- Mutating an undefined global variable.
"113", -- Accessing an undefined global variable.
"121", -- Setting a read-only global variable.
"122", -- Setting a read-only field of a global variable.
"131", -- Unused implicitly defined global variable.
"142", -- Setting an undefined field of a global variable.
"143", -- Accessing an undefined field of a global variable.
"211", -- Unused local variable.
"212", -- Unused argument.
"213", -- Unused loop variable.
"221", -- Local variable is accessed but never set.
"231", -- Local variable is set but never accessed.
"232", -- An argument is set but never accessed.
"233", -- Loop variable is set but never accessed.
"241", -- Local variable is mutated but never accessed.
"311", -- Value assigned to a local variable is unused.
"312", -- Value of an argument is unused.
"313", -- Value of a loop variable is unused.
"314", -- Value of a field in a table literal is unused.
"321", -- Accessing uninitialized local variable.
"331", -- Value assigned to a local variable is mutated but never accessed.
"341", -- Mutating uninitialized local variable.
"411", -- Redefining a local variable.
"412", -- Redefining an argument.
"413", -- Redefining a loop variable.
"421", -- Shadowing a local variable.
"422", -- Shadowing an argument.
"423", -- Shadowing a loop variable.
"431", -- Shadowing an upvalue.
"432", -- Shadowing an upvalue argument.
"433", -- Shadowing an upvalue loop variable.
"511", -- Unreachable code.
"512", -- Loop can be executed at most once.
"521", -- Unused label.
"531", -- Left-hand side of an assignment is too short.
"532", -- Left-hand side of an assignment is too long.
"541", -- An empty do end block.
"542", -- An empty if branch.
"551", -- An empty statement.
"561", -- Cyclomatic complexity of a function is too high.
"571", -- A numeric for loop goes from #(expr) down to 1 or less without negative step.
"611", -- A line consists of nothing but whitespace.
"612", -- A line contains trailing whitespace.
"613", -- Trailing whitespace in a string.
"614", -- Trailing whitespace in a comment.
"621", -- Inconsistent indentation (SPACE followed by TAB).
"631", -- Line is too long.
}

View File

@ -143,7 +143,7 @@ end
-- @return #AI_A2A_CAP
function AI_A2A_CAP:New( AICap, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, PatrolAltType )
return self:New2( AICap, EngageMinSpeed, EngageMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolZone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, PatrolAltType )
return self:New2( AICap, EngageMinSpeed, EngageMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, PatrolZone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType )
end

View File

@ -658,7 +658,9 @@ do -- AI_A2A_DISPATCHER
-- of the race track will randomly selected between 90 (West to East) and 180 (North to South) degrees.
-- After a random duration between 10 and 20 minutes, the flight will get a new random orbit location.
--
-- Note that all parameters except the squadron name are optional. If not specified, default values are taken. Speed and altitude are taken from the
-- Note that all parameters except the squadron name are optional. If not specified, default values are taken. Speed and altitude are taken from the CAP command used earlier on, e.g.
--
-- A2ADispatcher:SetSquadronCap( "Mineralnye", CAPZoneEast, 4000, 10000, 500, 600, 800, 900 )
--
-- Also note that the center of the race track pattern is chosen randomly within the patrol zone and can be close the the boarder of the zone. Hence, it cannot be guaranteed that the
-- whole pattern lies within the patrol zone.
@ -929,6 +931,8 @@ do -- AI_A2A_DISPATCHER
self.DefenderTasks = {} -- The Defenders Tasks.
self.DefenderDefault = {} -- The Defender Default Settings over all Squadrons.
self.SetSendPlayerMessages = false --#boolean Flash messages to player
-- TODO: Check detection through radar.
self.Detection:FilterCategories( { Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } )
--self.Detection:InitDetectRadar( true )
@ -949,7 +953,6 @@ do -- AI_A2A_DISPATCHER
self:SetDefaultCapTimeInterval( 180, 600 ) -- Between 180 and 600 seconds.
self:SetDefaultCapLimit( 1 ) -- Maximum one CAP per squadron.
self:AddTransition( "Started", "Assign", "Started" )
--- OnAfter Transition Handler for Event Assign.
@ -2356,6 +2359,12 @@ do -- AI_A2A_DISPATCHER
return self
end
--- Set flashing player messages on or off
-- @param #AI_A2G_DISPATCHER self
-- @param #boolean onoff Set messages on (true) or off (false)
function AI_A2A_DISPATCHER:SetSendMessages( onoff )
self.SetSendPlayerMessages = onoff
end
--- Sets flights to take-off in the air, as part of the defense system.
-- @param #AI_A2A_DISPATCHER self
@ -2919,9 +2928,13 @@ do -- AI_A2A_DISPATCHER
-- @return #AI_A2A_DISPATCHER.Squadron Squadron The squadron.
function AI_A2A_DISPATCHER:GetSquadronFromDefender( Defender )
self.Defenders = self.Defenders or {}
local DefenderName = Defender:GetName()
self:F( { DefenderName = DefenderName } )
return self.Defenders[ DefenderName ]
if Defender ~= nil then
local DefenderName = Defender:GetName()
self:F( { DefenderName = DefenderName } )
return self.Defenders[ DefenderName ]
else
return nil
end
end
@ -3240,7 +3253,9 @@ do -- AI_A2A_DISPATCHER
local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup )
if Squadron then
Dispatcher:MessageToPlayers( Squadron, DefenderName .. " Wheels up.", DefenderGroup )
if self.SetSendPlayerMessages then
Dispatcher:MessageToPlayers( Squadron, DefenderName .. " Wheels up.", DefenderGroup )
end
AI_A2A_Fsm:__Patrol( 2 ) -- Start Patrolling
end
end
@ -3252,10 +3267,10 @@ do -- AI_A2A_DISPATCHER
self:GetParent(self).onafterPatrolRoute( self, DefenderGroup, From, Event, To )
local DefenderName = DefenderGroup:GetCallsign()
local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER
local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER
local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup )
if Squadron then
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", patrolling.", DefenderGroup )
if Squadron and self.SetSendPlayerMessages then
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", patrolling.", DefenderGroup )
end
Dispatcher:ClearDefenderTaskTarget( DefenderGroup )
@ -3271,7 +3286,7 @@ do -- AI_A2A_DISPATCHER
local DefenderName = DefenderGroup:GetCallsign()
local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER
local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup )
if Squadron then
if Squadron and self.SetSendPlayerMessages then
Dispatcher:MessageToPlayers( Squadron, DefenderName .. " returning to base.", DefenderGroup )
end
Dispatcher:ClearDefenderTaskTarget( DefenderGroup )
@ -3459,10 +3474,10 @@ do -- AI_A2A_DISPATCHER
local DefenderTarget = Dispatcher:GetDefenderTaskTarget( DefenderGroup )
if DefenderTarget then
if Squadron.Language == "EN" then
if Squadron.Language == "EN" and self.SetSendPlayerMessages then
Dispatcher:MessageToPlayers( Squadron, DefenderName .. " wheels up.", DefenderGroup )
elseif Squadron.Language == "RU" then
Dispatcher:MessageToPlayers( Squadron, DefenderName .. " колеса вверх.", DefenderGroup )
elseif Squadron.Language == "RU" and self.SetSendPlayerMessages then
Dispatcher:MessageToPlayers( Squadron, DefenderName .. " колеÑ<EFBFBD>а вверх.", DefenderGroup )
end
--Fsm:__Engage( 2, DefenderTarget.Set ) -- Engage on the TargetSetUnit
Fsm:EngageRoute( DefenderTarget.Set ) -- Engage on the TargetSetUnit
@ -3480,11 +3495,11 @@ do -- AI_A2A_DISPATCHER
local FirstUnit = AttackSetUnit:GetFirst()
local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE
if Squadron.Language == "EN" then
if Squadron.Language == "EN" and self.SetSendPlayerMessages then
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", intercepting bogeys at " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup )
elseif Squadron.Language == "RU" then
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", перехват самолетов в " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup )
elseif Squadron.Language == "DE" then
elseif Squadron.Language == "RU" and self.SetSendPlayerMessages then
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", перехват Ñ<>амолеÑов в " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup )
elseif Squadron.Language == "DE" and self.SetSendPlayerMessages then
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", Eindringlinge abfangen bei" .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup )
end
end
@ -3502,10 +3517,10 @@ do -- AI_A2A_DISPATCHER
local FirstUnit = AttackSetUnit:GetFirst()
local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE
if Squadron.Language == "EN" then
if Squadron.Language == "EN" and self.SetSendPlayerMessages then
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", engaging bogeys at " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup )
elseif Squadron.Language == "RU" then
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", захватывающие самолеты в " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup )
elseif Squadron.Language == "RU" and self.SetSendPlayerMessages then
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", захватывающие Ñ<>амолеÑÑ Ð² " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup )
end
end
self:GetParent( Fsm ).onafterEngage( self, DefenderGroup, From, Event, To, AttackSetUnit )
@ -3520,10 +3535,10 @@ do -- AI_A2A_DISPATCHER
local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup )
if Squadron then
if Squadron.Language == "EN" then
if Squadron.Language == "EN" and self.SetSendPlayerMessages then
Dispatcher:MessageToPlayers( Squadron, DefenderName .. " returning to base.", DefenderGroup )
elseif Squadron.Language == "RU" then
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", возвращаясь на базу.", DefenderGroup )
elseif Squadron.Language == "RU" and self.SetSendPlayerMessages then
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", возвращаÑ<EFBFBD>Ñ<EFBFBD>ÑŒ на базу.", DefenderGroup )
end
end
Dispatcher:ClearDefenderTaskTarget( DefenderGroup )
@ -3551,10 +3566,10 @@ do -- AI_A2A_DISPATCHER
local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER
local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup )
if Squadron.Language == "EN" then
if Squadron.Language == "EN" and self.SetSendPlayerMessages then
Dispatcher:MessageToPlayers( Squadron, DefenderName .. " landing at base.", DefenderGroup )
elseif Squadron.Language == "RU" then
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", захватывающие самолеты в посадка на базу.", DefenderGroup )
elseif Squadron.Language == "RU" and self.SetSendPlayerMessages then
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", захватывающие Ñ<>амолеÑÑ Ð² поÑ<C2BE>адка на базу.", DefenderGroup )
end
if Action and Action == "Destroy" then
@ -4523,5 +4538,5 @@ do
return self
end
end

View File

@ -431,7 +431,7 @@ do -- AI_A2G_DISPATCHER
--
-- ### 2.2. The **Defense Reactivity**.
--
-- There are 5 levels that can be configured to tweak the defense reactivity. As explained above, the threat to a defense coordinate is
-- There are three levels that can be configured to tweak the defense reactivity. As explained above, the threat to a defense coordinate is
-- also determined by the distance of the enemy ground target to the defense coordinate.
-- If you want to have a **low** defense reactivity, that is, the probability that an A2G defense will engage to the enemy ground target, then
-- use the @{#AI_A2G_DISPATCHER.SetDefenseReactivityLow}() method. For medium and high reactivity, use the methods
@ -999,7 +999,9 @@ do -- AI_A2G_DISPATCHER
-- self.Detection:InitDetectRadar( false )
-- self.Detection:InitDetectVisual( true )
-- self.Detection:SetRefreshTimeInterval( 30 )
self.SetSendPlayerMessages = false --flash messages to players
self:SetDefenseRadius()
self:SetDefenseLimit( nil )
self:SetDefenseApproach( AI_A2G_DISPATCHER.DefenseApproach.Random )
@ -3699,7 +3701,9 @@ do -- AI_A2G_DISPATCHER
local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup )
if Squadron then
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", wheels up.", DefenderGroup )
if self.SetSendPlayerMessages then
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", wheels up.", DefenderGroup )
end
AI_A2G_Fsm:Patrol() -- Engage on the TargetSetUnit
end
end
@ -3711,7 +3715,7 @@ do -- AI_A2G_DISPATCHER
local DefenderName = DefenderGroup:GetCallsign()
local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER
local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup )
if Squadron then
if Squadron and self.SetSendPlayerMessages then
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", patrolling.", DefenderGroup )
end
@ -3730,8 +3734,9 @@ do -- AI_A2G_DISPATCHER
if Squadron and AttackSetUnit:Count() > 0 then
local FirstUnit = AttackSetUnit:GetFirst()
local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", moving on to ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup )
if self.SetSendPlayerMessages then
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", moving on to ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup )
end
end
end
@ -3745,8 +3750,9 @@ do -- AI_A2G_DISPATCHER
local FirstUnit = AttackSetUnit:GetFirst()
if FirstUnit then
local Coordinate = FirstUnit:GetCoordinate()
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", engaging ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup )
if self.SetSendPlayerMessages then
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", engaging ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup )
end
end
end
@ -3757,8 +3763,9 @@ do -- AI_A2G_DISPATCHER
local DefenderName = DefenderGroup:GetCallsign()
local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER
local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup )
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", returning to base.", DefenderGroup )
if self.SetSendPlayerMessages then
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", returning to base.", DefenderGroup )
end
Dispatcher:ClearDefenderTaskTarget( DefenderGroup )
end
@ -3770,7 +3777,9 @@ do -- AI_A2G_DISPATCHER
local DefenderName = DefenderGroup:GetCallsign()
local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER
local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup )
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", lost control." )
if self.SetSendPlayerMessages then
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", lost control." )
end
if DefenderGroup:IsAboveRunway() then
Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup )
DefenderGroup:Destroy()
@ -3785,8 +3794,9 @@ do -- AI_A2G_DISPATCHER
local DefenderName = DefenderGroup:GetCallsign()
local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER
local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup )
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", landing at base.", DefenderGroup )
if self.SetSendPlayerMessages then
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", landing at base.", DefenderGroup )
end
if Action and Action == "Destroy" then
Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup )
DefenderGroup:Destroy()
@ -3842,7 +3852,9 @@ do -- AI_A2G_DISPATCHER
self:F( { DefenderTarget = DefenderTarget } )
if DefenderTarget then
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", wheels up.", DefenderGroup )
if self.SetSendPlayerMessages then
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", wheels up.", DefenderGroup )
end
AI_A2G_Fsm:EngageRoute( DefenderTarget.Set ) -- Engage on the TargetSetUnit
end
end
@ -3857,8 +3869,9 @@ do -- AI_A2G_DISPATCHER
if Squadron then
local FirstUnit = AttackSetUnit:GetFirst()
local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", on route to ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup )
if self.SetSendPlayerMessages then
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", on route to ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup )
end
end
self:GetParent(self).onafterEngageRoute( self, DefenderGroup, From, Event, To, AttackSetUnit )
end
@ -3873,8 +3886,9 @@ do -- AI_A2G_DISPATCHER
local FirstUnit = AttackSetUnit:GetFirst()
if FirstUnit then
local Coordinate = FirstUnit:GetCoordinate()
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", engaging ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup )
if self.SetSendPlayerMessages then
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", engaging ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup )
end
end
end
@ -3884,8 +3898,9 @@ do -- AI_A2G_DISPATCHER
local DefenderName = DefenderGroup:GetCallsign()
local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER
local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup )
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", returning to base.", DefenderGroup )
if self.SetSendPlayerMessages then
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", returning to base.", DefenderGroup )
end
self:GetParent(self).onafterRTB( self, DefenderGroup, From, Event, To )
Dispatcher:ClearDefenderTaskTarget( DefenderGroup )
@ -3899,8 +3914,9 @@ do -- AI_A2G_DISPATCHER
local DefenderName = DefenderGroup:GetCallsign()
local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER
local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup )
--Dispatcher:MessageToPlayers( Squadron, "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " lost control." )
if self.SetSendPlayerMessages then
Dispatcher:MessageToPlayers( Squadron, "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " lost control." )
end
if DefenderGroup:IsAboveRunway() then
Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup )
DefenderGroup:Destroy()
@ -3915,8 +3931,9 @@ do -- AI_A2G_DISPATCHER
local DefenderName = DefenderGroup:GetCallsign()
local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER
local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup )
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", landing at base.", DefenderGroup )
if self.SetSendPlayerMessages then
Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", landing at base.", DefenderGroup )
end
if Action and Action == "Destroy" then
Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup )
DefenderGroup:Destroy()
@ -3956,7 +3973,7 @@ do -- AI_A2G_DISPATCHER
local EvaluateDistance = AttackCoordinate:Get2DDistance( DefenseCoordinate )
-- Now check if this coordinate is not in a danger zone, meaning, that the attack line is not crossing other coordinates.
-- (y1 y2)x + (x2 x1)y + (x1y2 x2y1) = 0
-- (y1 - y2)x + (x2 - x1)y + (x1y2 - x2y1) = 0
local c1 = DefenseCoordinate
local c2 = AttackCoordinate
@ -4017,7 +4034,7 @@ do -- AI_A2G_DISPATCHER
for DefenderID, DefenderGroup in pairs( DefenderFriendlies or {} ) do
-- Here we check if the defenders have a defense line to the attackers.
-- If the attackers are behind enemy lines or too close to an other defense line; then don´t engage.
-- If the attackers are behind enemy lines or too close to an other defense line; then don't engage.
local DefenseCoordinate = DefenderGroup:GetCoordinate()
local HasDefenseLine = self:HasDefenseLine( DefenseCoordinate, DetectedItem )
@ -4320,7 +4337,7 @@ do -- AI_A2G_DISPATCHER
-- Show tactical situation
local ThreatLevel = DetectedItem.Set:CalculateThreatLevelA2G()
Report:Add( string.format( " - %1s%s ( %04s ): ( #%02d - %-4s ) %s" , ( DetectedItem.IsDetected == true ) and "!" or " ", DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Type or " --- ", string.rep( "", ThreatLevel ) ) )
Report:Add( string.format( " - %1s%s ( %04s ): ( #%02d - %-4s ) %s" , ( DetectedItem.IsDetected == true ) and "!" or " ", DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Type or " --- ", string.rep( "", ThreatLevel ) ) )
for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do
local Defender = Defender -- Wrapper.Group#GROUP
if DefenderTask.Target and DefenderTask.Target.Index == DetectedItem.Index then
@ -4537,7 +4554,7 @@ do -- AI_A2G_DISPATCHER
if self.TacticalDisplay then
-- Show tactical situation
local ThreatLevel = DetectedItem.Set:CalculateThreatLevelA2G()
Report:Add( string.format( " - %1s%s ( %4s ): ( #%d - %4s ) %s" , ( DetectedItem.IsDetected == true ) and "!" or " ", DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Type or " --- ", string.rep( "", ThreatLevel ) ) )
Report:Add( string.format( " - %1s%s ( %4s ): ( #%d - %4s ) %s" , ( DetectedItem.IsDetected == true ) and "!" or " ", DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Type or " --- ", string.rep( "", ThreatLevel ) ) )
for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do
local Defender = Defender -- Wrapper.Group#GROUP
if DefenderTask.Target and DefenderTask.Target.Index == DetectedItem.Index then
@ -4706,6 +4723,12 @@ do
local PatrolTaskType = PatrolTaskTypes[math.random(1,3)]
self:Patrol( SquadronName, PatrolTaskType )
end
--- Set flashing player messages on or off
-- @param #AI_A2G_DISPATCHER self
-- @param #boolean onoff Set messages on (true) or off (false)
function AI_A2G_DISPATCHER:SetSendMessages( onoff )
self.SetSendPlayerMessages = onoff
end
end

View File

@ -595,19 +595,24 @@ function AI_AIR:onafterRTB( AIGroup, From, Event, To )
--- Calculate the target route point.
local FromCoord = AIGroup:GetCoordinate()
local ToTargetCoord = self.HomeAirbase:GetCoordinate()
if not self.RTBMinSpeed and not self.RTBMaxSpeed then
local ToTargetCoord = self.HomeAirbase:GetCoordinate() -- coordinate is on land height(!)
local ToTargetVec3 = ToTargetCoord:GetVec3()
ToTargetVec3.y = ToTargetCoord:GetLandHeight()+1000 -- let's set this 1000m/3000 feet above ground
local ToTargetCoord2 = COORDINATE:NewFromVec3( ToTargetVec3 )
if not self.RTBMinSpeed or not self.RTBMaxSpeed then
local RTBSpeedMax = AIGroup:GetSpeedMax()
self:SetRTBSpeed( RTBSpeedMax * 0.25, RTBSpeedMax * 0.25 )
self:SetRTBSpeed( RTBSpeedMax * 0.5, RTBSpeedMax * 0.6 )
end
local RTBSpeed = math.random( self.RTBMinSpeed, self.RTBMaxSpeed )
local ToAirbaseAngle = FromCoord:GetAngleDegrees( FromCoord:GetDirectionVec3( ToTargetCoord ) )
--local ToAirbaseAngle = FromCoord:GetAngleDegrees( FromCoord:GetDirectionVec3( ToTargetCoord2 ) )
local Distance = FromCoord:Get2DDistance( ToTargetCoord )
local Distance = FromCoord:Get2DDistance( ToTargetCoord2 )
local ToAirbaseCoord = FromCoord:Translate( 5000, ToAirbaseAngle )
--local ToAirbaseCoord = FromCoord:Translate( 5000, ToAirbaseAngle )
local ToAirbaseCoord = ToTargetCoord2
if Distance < 5000 then
self:I( "RTB and near the airbase!" )
self:Home()

View File

@ -418,6 +418,7 @@ end
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
-- @param Core.Set#SET_UNIT AttackSetUnit Unit set to be attacked.
function AI_AIR_ENGAGE:onafterEngageRoute( DefenderGroup, From, Event, To, AttackSetUnit )
self:I( { DefenderGroup, From, Event, To, AttackSetUnit } )
@ -425,7 +426,7 @@ function AI_AIR_ENGAGE:onafterEngageRoute( DefenderGroup, From, Event, To, Attac
self.AttackSetUnit = AttackSetUnit -- Kept in memory in case of resume from refuel in air!
local AttackCount = AttackSetUnit:Count()
local AttackCount = AttackSetUnit:CountAlive()
if AttackCount > 0 then
@ -510,6 +511,7 @@ end
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
-- @param Core.Set#SET_UNIT AttackSetUnit Set of units to be attacked.
function AI_AIR_ENGAGE:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit )
self:F( { DefenderGroup, From, Event, To, AttackSetUnit} )
@ -517,8 +519,8 @@ function AI_AIR_ENGAGE:onafterEngage( DefenderGroup, From, Event, To, AttackSetU
self.AttackSetUnit = AttackSetUnit -- Kept in memory in case of resume from refuel in air!
local AttackCount = AttackSetUnit:Count()
self:I({AttackCount = AttackCount})
local AttackCount = AttackSetUnit:CountAlive()
self:T({AttackCount = AttackCount})
if AttackCount > 0 then

View File

@ -1,4 +1,4 @@
--- **AI** -- (R2.4) - Models the intelligent transportation of infantry and other cargo.
--- **AI** - Models the intelligent transportation of infantry and other cargo.
--
-- ===
--
@ -35,10 +35,9 @@ AI_CARGO = {
--- Creates a new AI_CARGO object.
-- @param #AI_CARGO self
-- @param Wrapper.Group#GROUP Carrier
-- @param Core.Set#SET_CARGO CargoSet
-- @param #number CombatRadius
-- @return #AI_CARGO
-- @param Wrapper.Group#GROUP Carrier Cargo carrier group.
-- @param Core.Set#SET_CARGO CargoSet Set of cargo(s) to transport.
-- @return #AI_CARGO self
function AI_CARGO:New( Carrier, CargoSet )
local self = BASE:Inherit( self, FSM_CONTROLLABLE:New( Carrier ) ) -- #AI_CARGO
@ -52,7 +51,8 @@ function AI_CARGO:New( Carrier, CargoSet )
self:AddTransition( "Loaded", "Deploy", "*" )
self:AddTransition( "*", "Load", "Boarding" )
self:AddTransition( { "Boarding", "Loaded" }, "Board", "Boarding" )
self:AddTransition( "Boarding", "Board", "Boarding" )
self:AddTransition( "Loaded", "Board", "Loaded" )
self:AddTransition( "Boarding", "Loaded", "Boarding" )
self:AddTransition( "Boarding", "PickedUp", "Loaded" )
@ -393,7 +393,7 @@ end
function AI_CARGO:onafterBoard( Carrier, From, Event, To, Cargo, CarrierUnit, PickupZone )
self:F( { Carrier, From, Event, To, Cargo, CarrierUnit:GetName() } )
if Carrier and Carrier:IsAlive() then
if Carrier and Carrier:IsAlive() and From == "Boarding" then
self:F({ IsLoaded = Cargo:IsLoaded(), Cargo:GetName(), Carrier:GetName() } )
if not Cargo:IsLoaded() and not Cargo:IsDestroyed() then
self:__Board( -10, Cargo, CarrierUnit, PickupZone )
@ -509,7 +509,7 @@ end
function AI_CARGO:onafterUnboard( Carrier, From, Event, To, Cargo, CarrierUnit, DeployZone, Defend )
self:F( { Carrier, From, Event, To, Cargo:GetName(), DeployZone = DeployZone, Defend = Defend } )
if Carrier and Carrier:IsAlive() then
if Carrier and Carrier:IsAlive() and From == "Unboarding" then
if not Cargo:IsUnLoaded() then
self:__Unboard( 10, Cargo, CarrierUnit, DeployZone, Defend )
return

View File

@ -1,4 +1,4 @@
--- **AI** -- (R2.4) - Models the intelligent transportation of infantry and other cargo.
--- **AI** - Models the intelligent transportation of cargo using ground vehicles.
--
-- ===
--
@ -157,6 +157,45 @@ function AI_CARGO_APC:SetCarrier( CargoCarrier )
return self
end
--- Set whether or not the carrier will use roads to *pickup* and *deploy* the cargo.
-- @param #AI_CARGO_APC self
-- @param #boolean Offroad If true, carrier will not use roads. If `nil` or `false` the carrier will use roads when available.
-- @param #number Formation Offroad formation used. Default is `ENUMS.Formation.Vehicle.Offroad`.
-- @return #AI_CARGO_APC self
function AI_CARGO_APC:SetOffRoad(Offroad, Formation)
self:SetPickupOffRoad(Offroad, Formation)
self:SetDeployOffRoad(Offroad, Formation)
return self
end
--- Set whether the carrier will *not* use roads to *pickup* the cargo.
-- @param #AI_CARGO_APC self
-- @param #boolean Offroad If true, carrier will not use roads.
-- @param #number Formation Offroad formation used. Default is `ENUMS.Formation.Vehicle.Offroad`.
-- @return #AI_CARGO_APC self
function AI_CARGO_APC:SetPickupOffRoad(Offroad, Formation)
self.pickupOffroad=Offroad
self.pickupFormation=Formation or ENUMS.Formation.Vehicle.OffRoad
return self
end
--- Set whether the carrier will *not* use roads to *deploy* the cargo.
-- @param #AI_CARGO_APC self
-- @param #boolean Offroad If true, carrier will not use roads.
-- @param #number Formation Offroad formation used. Default is `ENUMS.Formation.Vehicle.Offroad`.
-- @return #AI_CARGO_APC self
function AI_CARGO_APC:SetDeployOffRoad(Offroad, Formation)
self.deployOffroad=Offroad
self.deployFormation=Formation or ENUMS.Formation.Vehicle.OffRoad
return self
end
--- Find a free Carrier within a radius.
-- @param #AI_CARGO_APC self
@ -350,10 +389,13 @@ function AI_CARGO_APC:onafterFollow( APC, From, Event, To )
end
--- @param #AI_CARGO_APC
-- @param Wrapper.Group#GROUP APC
function AI_CARGO_APC._Pickup( APC, self, Coordinate, Speed, PickupZone )
--- Pickup task function. Triggers Load event.
-- @param Wrapper.Group#GROUP APC The cargo carrier group.
-- @param #AI_CARGO_APC sel `AI_CARGO_APC` class.
-- @param Core.Point#COORDINATE Coordinate. The coordinate (not used).
-- @param #number Speed Speed (not used).
-- @param Core.Zone#ZONE PickupZone Pickup zone.
function AI_CARGO_APC._Pickup(APC, self, Coordinate, Speed, PickupZone)
APC:F( { "AI_CARGO_APC._Pickup:", APC:GetName() } )
@ -362,8 +404,12 @@ function AI_CARGO_APC._Pickup( APC, self, Coordinate, Speed, PickupZone )
end
end
function AI_CARGO_APC._Deploy( APC, self, Coordinate, DeployZone )
--- Deploy task function. Triggers Unload event.
-- @param Wrapper.Group#GROUP APC The cargo carrier group.
-- @param #AI_CARGO_APC self `AI_CARGO_APC` class.
-- @param Core.Point#COORDINATE Coordinate. The coordinate (not used).
-- @param Core.Zone#ZONE DeployZone Deploy zone.
function AI_CARGO_APC._Deploy(APC, self, Coordinate, DeployZone)
APC:F( { "AI_CARGO_APC._Deploy:", APC } )
@ -392,12 +438,20 @@ function AI_CARGO_APC:onafterPickup( APC, From, Event, To, Coordinate, Speed, He
self.RoutePickup = true
local _speed=Speed or APC:GetSpeedMax()*0.5
-- Route on road.
local Waypoints = {}
if self.pickupOffroad then
Waypoints[1]=APC:GetCoordinate():WaypointGround(Speed, self.pickupFormation)
Waypoints[2]=Coordinate:WaypointGround(_speed, self.pickupFormation, DCSTasks)
else
Waypoints=APC:TaskGroundOnRoad(Coordinate, _speed, ENUMS.Formation.Vehicle.OffRoad, true)
end
local Waypoints = APC:TaskGroundOnRoad( Coordinate, _speed, "Line abreast", true )
local TaskFunction = APC:TaskFunction( "AI_CARGO_APC._Pickup", self, Coordinate, Speed, PickupZone )
self:F({Waypoints = Waypoints})
local Waypoint = Waypoints[#Waypoints]
APC:SetTaskWaypoint( Waypoint, TaskFunction ) -- Set for the given Route at Waypoint 2 the TaskRouteToZone.
@ -428,18 +482,34 @@ function AI_CARGO_APC:onafterDeploy( APC, From, Event, To, Coordinate, Speed, He
self.RouteDeploy = true
local _speed=Speed or APC:GetSpeedMax()*0.5
local Waypoints = APC:TaskGroundOnRoad( Coordinate, _speed, "Line abreast", true )
-- Set speed in km/h.
local speedmax=APC:GetSpeedMax()
local _speed=Speed or speedmax*0.5
_speed=math.min(_speed, speedmax)
-- Route on road.
local Waypoints = {}
if self.deployOffroad then
Waypoints[1]=APC:GetCoordinate():WaypointGround(Speed, self.deployFormation)
Waypoints[2]=Coordinate:WaypointGround(_speed, self.deployFormation, DCSTasks)
else
Waypoints=APC:TaskGroundOnRoad(Coordinate, _speed, ENUMS.Formation.Vehicle.OffRoad, true)
end
-- Task function
local TaskFunction = APC:TaskFunction( "AI_CARGO_APC._Deploy", self, Coordinate, DeployZone )
self:F({Waypoints = Waypoints})
-- Last waypoint
local Waypoint = Waypoints[#Waypoints]
APC:SetTaskWaypoint( Waypoint, TaskFunction ) -- Set for the given Route at Waypoint 2 the TaskRouteToZone.
-- Set task function
APC:SetTaskWaypoint(Waypoint, TaskFunction) -- Set for the given Route at Waypoint 2 the TaskRouteToZone.
-- Route group
APC:Route( Waypoints, 1 ) -- Move after a random seconds to the Route. See the Route method for details.
-- Call parent function.
self:GetParent( self, AI_CARGO_APC ).onafterDeploy( self, APC, From, Event, To, Coordinate, Speed, Height, DeployZone )
end

View File

@ -1,4 +1,4 @@
--- **AI** -- (R2.4) - Models the intelligent transportation of infantry (cargo).
--- **AI** - Models the intelligent transportation of cargo using airplanes.
--
-- ===
--
@ -408,9 +408,6 @@ function AI_CARGO_AIRPLANE:onafterUnload( Airplane, From, Event, To, DeployZone
end
--- Route the airplane from one airport or it's current position to another airbase.
-- @param #AI_CARGO_AIRPLANE self
-- @param Wrapper.Group#GROUP Airplane Airplane group to be routed.
@ -438,14 +435,10 @@ function AI_CARGO_AIRPLANE:Route( Airplane, Airbase, Speed, Height, Uncontrolled
-- To point.
local AirbasePointVec2 = Airbase:GetPointVec2()
local ToWaypoint = AirbasePointVec2:WaypointAir(
POINT_VEC3.RoutePointAltType.BARO,
"Land",
"Landing",
Speed or Airplane:GetSpeedMax()*0.8
)
ToWaypoint["airdromeId"] = Airbase:GetID()
ToWaypoint["speed_locked"] = true
local ToWaypoint = AirbasePointVec2:WaypointAir(POINT_VEC3.RoutePointAltType.BARO, "Land", "Landing", Speed or Airplane:GetSpeedMax()*0.8, true, Airbase)
--ToWaypoint["airdromeId"] = Airbase:GetID()
--ToWaypoint["speed_locked"] = true
-- If self.Airbase~=nil then group is currently at an airbase, where it should be respawned.

View File

@ -1,4 +1,4 @@
--- **AI** -- (R2.4) - Models the intelligent transportation of infantry and other cargo.
--- **AI** - Models the intelligent transportation of infantry and other cargo.
--
-- ## Features:
--
@ -1104,7 +1104,7 @@ function AI_CARGO_DISPATCHER:onafterMonitor()
-- The Pickup sequence ...
-- Check if this Carrier need to go and Pickup something...
-- So, if the cargo bay is not full yet with cargo to be loaded ...
self:I( { Carrier = CarrierGroupName, IsRelocating = AI_Cargo:IsRelocating(), IsTransporting = AI_Cargo:IsTransporting() } )
self:T( { Carrier = CarrierGroupName, IsRelocating = AI_Cargo:IsRelocating(), IsTransporting = AI_Cargo:IsTransporting() } )
if AI_Cargo:IsRelocating() == false and AI_Cargo:IsTransporting() == false then
-- ok, so there is a free Carrier
-- now find the first cargo that is Unloaded

View File

@ -1,4 +1,4 @@
--- **AI** -- (2.4) - Models the intelligent transportation of infantry and other cargo using APCs.
--- **AI** - Models the intelligent transportation of infantry and other cargo using APCs.
--
-- ## Features:
--
@ -181,25 +181,36 @@ function AI_CARGO_DISPATCHER_APC:New( APCSet, CargoSet, PickupZoneSet, DeployZon
return self
end
--- AI cargo
-- @param #AI_CARGO_DISPATCHER_APC self
-- @param Wrapper.Group#GROUP APC The APC carrier.
-- @param Core.Set#SET_CARGO CargoSet Cargo set.
-- @return AI.AI_Cargo_APC#AI_CARGO_DISPATCHER_APC AI cargo APC object.
function AI_CARGO_DISPATCHER_APC:AICargo( APC, CargoSet )
return AI_CARGO_APC:New( APC, CargoSet, self.CombatRadius )
local aicargoapc=AI_CARGO_APC:New(APC, CargoSet, self.CombatRadius)
aicargoapc:SetDeployOffRoad(self.deployOffroad, self.deployFormation)
aicargoapc:SetPickupOffRoad(self.pickupOffroad, self.pickupFormation)
return aicargoapc
end
--- Enable/Disable unboarding of cargo (infantry) when enemies are nearby (to help defend the carrier).
-- This is only valid for APCs and trucks etc, thus ground vehicles.
-- @param #AI_CARGO_DISPATCHER_APC self
-- @param #number CombatRadius Provide the combat radius to defend the carrier by unboarding the cargo when enemies are nearby.
-- When the combat radius is 0, no defense will happen of the carrier.
-- When the combat radius is 0 (default), no defense will happen of the carrier.
-- When the combat radius is not provided, no defense will happen!
-- @return #AI_CARGO_DISPATCHER_APC
-- @usage
--
-- -- Disembark the infantry when the carrier is under attack.
-- AICargoDispatcher:SetCombatRadius( true )
-- AICargoDispatcher:SetCombatRadius( 500 )
--
-- -- Keep the cargo in the carrier when the carrier is under attack.
-- AICargoDispatcher:SetCombatRadius( false )
-- AICargoDispatcher:SetCombatRadius( 0 )
function AI_CARGO_DISPATCHER_APC:SetCombatRadius( CombatRadius )
self.CombatRadius = CombatRadius or 0
@ -207,3 +218,41 @@ function AI_CARGO_DISPATCHER_APC:SetCombatRadius( CombatRadius )
return self
end
--- Set whether the carrier will *not* use roads to *pickup* and *deploy* the cargo.
-- @param #AI_CARGO_DISPATCHER_APC self
-- @param #boolean Offroad If true, carrier will not use roads.
-- @param #number Formation Offroad formation used. Default is `ENUMS.Formation.Vehicle.Offroad`.
-- @return #AI_CARGO_DISPATCHER_APC self
function AI_CARGO_DISPATCHER_APC:SetOffRoad(Offroad, Formation)
self:SetPickupOffRoad(Offroad, Formation)
self:SetDeployOffRoad(Offroad, Formation)
return self
end
--- Set whether the carrier will *not* use roads to *pickup* the cargo.
-- @param #AI_CARGO_DISPATCHER_APC self
-- @param #boolean Offroad If true, carrier will not use roads.
-- @param #number Formation Offroad formation used. Default is `ENUMS.Formation.Vehicle.Offroad`.
-- @return #AI_CARGO_DISPATCHER_APC self
function AI_CARGO_DISPATCHER_APC:SetPickupOffRoad(Offroad, Formation)
self.pickupOffroad=Offroad
self.pickupFormation=Formation or ENUMS.Formation.Vehicle.OffRoad
return self
end
--- Set whether the carrier will *not* use roads to *deploy* the cargo.
-- @param #AI_CARGO_DISPATCHER_APC self
-- @param #boolean Offroad If true, carrier will not use roads.
-- @param #number Formation Offroad formation used. Default is `ENUMS.Formation.Vehicle.Offroad`.
-- @return #AI_CARGO_DISPATCHER_APC self
function AI_CARGO_DISPATCHER_APC:SetDeployOffRoad(Offroad, Formation)
self.deployOffroad=Offroad
self.deployFormation=Formation or ENUMS.Formation.Vehicle.OffRoad
return self
end

View File

@ -0,0 +1,193 @@
--- **AI** -- (2.5.1) - Models the intelligent transportation of infantry and other cargo using Ships
--
-- ## Features:
--
-- * Transport cargo to various deploy zones using naval vehicles.
-- * Various @{Cargo.Cargo#CARGO} types can be transported, including infantry, vehicles, and crates.
-- * Define a deploy zone of various types to determine the destination of the cargo.
-- * Ships will follow shipping lanes as defined in the Mission Editor.
-- * Multiple ships can transport multiple cargo as a single group.
--
-- ===
--
-- ## Test Missions:
--
-- NEED TO DO
--
-- ===
--
-- ### Author: **acrojason** (derived from AI_Cargo_Dispatcher_APC by FlightControl)
--
-- ===
--
-- @module AI.AI_Cargo_Dispatcher_Ship
-- @image AI_Cargo_Dispatcher.JPG
--- @type AI_CARGO_DISPATCHER_SHIP
-- @extends AI.AI_Cargo_Dispatcher#AI_CARGO_DISPATCHER
--- A dynamic cargo transportation capability for AI groups.
--
-- Naval vessels can be mobilized to semi-intelligently transport cargo within the simulation.
--
-- The AI_CARGO_DISPATCHER_SHIP module is derived from the AI_CARGO_DISPATCHER module.
--
-- ## Note! In order to fully understand the mechanisms of the AI_CARGO_DISPATCHER_SHIP class, it is recommended that you first consult and READ the documentation of the @{AI.AI_Cargo_Dispatcher} module!!!
--
-- This will be particularly helpful in order to determine how to **Tailor the different cargo handling events**.
--
-- The AI_CARGO_DISPATCHER_SHIP class uses the @{Cargo.Cargo} capabilities within the MOOSE framwork.
-- Also ensure that you fully understand how to declare and setup Cargo objects within the MOOSE framework before using this class.
-- CARGO derived objects must generally be declared within the mission to make the AI_CARGO_DISPATCHER_SHIP object recognize the cargo.
--
--
-- # 1) AI_CARGO_DISPATCHER_SHIP constructor.
--
-- * @{AI_CARGO_DISPATCHER_SHIP.New}(): Creates a new AI_CARGO_DISPATCHER_SHIP object.
--
-- ---
--
-- # 2) AI_CARGO_DISPATCHER_SHIP is a Finite State Machine.
--
-- This section must be read as follows... Each of the rows indicate a state transition, triggered through an event, and with an ending state of the event was executed.
-- The first column is the **From** state, the second column the **Event**, and the third column the **To** state.
--
-- So, each of the rows have the following structure.
--
-- * **From** => **Event** => **To**
--
-- Important to know is that an event can only be executed if the **current state** is the **From** state.
-- This, when an **Event** that is being triggered has a **From** state that is equal to the **Current** state of the state machine, the event will be executed,
-- and the resulting state will be the **To** state.
--
-- These are the different possible state transitions of this state machine implementation:
--
-- * Idle => Start => Monitoring
-- * Monitoring => Monitor => Monitoring
-- * Monitoring => Stop => Idle
--
-- * Monitoring => Pickup => Monitoring
-- * Monitoring => Load => Monitoring
-- * Monitoring => Loading => Monitoring
-- * Monitoring => Loaded => Monitoring
-- * Monitoring => PickedUp => Monitoring
-- * Monitoring => Deploy => Monitoring
-- * Monitoring => Unload => Monitoring
-- * Monitoring => Unloaded => Monitoring
-- * Monitoring => Deployed => Monitoring
-- * Monitoring => Home => Monitoring
--
--
-- ## 2.1) AI_CARGO_DISPATCHER States.
--
-- * **Monitoring**: The process is dispatching.
-- * **Idle**: The process is idle.
--
-- ## 2.2) AI_CARGO_DISPATCHER Events.
--
-- * **Start**: Start the transport process.
-- * **Stop**: Stop the transport process.
-- * **Monitor**: Monitor and take action.
--
-- * **Pickup**: Pickup cargo.
-- * **Load**: Load the cargo.
-- * **Loading**: The dispatcher is coordinating the loading of a cargo.
-- * **Loaded**: Flag that the cargo is loaded.
-- * **PickedUp**: The dispatcher has loaded all requested cargo into the CarrierGroup.
-- * **Deploy**: Deploy cargo to a location.
-- * **Unload**: Unload the cargo.
-- * **Unloaded**: Flag that the cargo is unloaded.
-- * **Deployed**: All cargo is unloaded from the carriers in the group.
-- * **Home**: A Carrier is going home.
--
-- ## 2.3) Enhance your mission scripts with **Tailored** Event Handling!
--
-- Within your mission, you can capture these events when triggered, and tailor the events with your own code!
-- Check out the @{AI.AI_Cargo_Dispatcher#AI_CARGO_DISPATCHER} class at chapter 3 for details on the different event handlers that are available and how to use them.
--
-- **There are a lot of templates available that allows you to quickly setup an event handler for a specific event type!**
--
-- ---
--
-- # 3) Set the pickup parameters.
--
-- Several parameters can be set to pickup cargo:
--
-- * @{#AI_CARGO_DISPATCHER_SHIP.SetPickupRadius}(): Sets or randomizes the pickup location for the Ship around the cargo coordinate in a radius defined an outer and optional inner radius.
-- * @{#AI_CARGO_DISPATCHER_SHIP.SetPickupSpeed}(): Set the speed or randomizes the speed in km/h to pickup the cargo.
--
-- # 4) Set the deploy parameters.
--
-- Several parameters can be set to deploy cargo:
--
-- * @{#AI_CARGO_DISPATCHER_SHIP.SetDeployRadius}(): Sets or randomizes the deploy location for the Ship around the cargo coordinate in a radius defined an outer and an optional inner radius.
-- * @{#AI_CARGO_DISPATCHER_SHIP.SetDeploySpeed}(): Set the speed or randomizes the speed in km/h to deploy the cargo.
--
-- # 5) Set the home zone when there isn't any more cargo to pickup.
--
-- A home zone can be specified to where the Ship will move when there isn't any cargo left for pickup.
-- Use @{#AI_CARGO_DISPATCHER_SHIP.SetHomeZone}() to specify the home zone.
--
-- If no home zone is specified, the Ship will wait near the deploy zone for a new pickup command.
--
-- ===
--
-- @field #AI_CARGO_DISPATCHER_SHIP
AI_CARGO_DISPATCHER_SHIP = {
ClassName = "AI_CARGO_DISPATCHER_SHIP"
}
--- Creates a new AI_CARGO_DISPATCHER_SHIP object.
-- @param #AI_CARGO_DISPATCHER_SHIP self
-- @param Core.Set#SET_GROUP ShipSet The set of @{Wrapper.Group#GROUP} objects of Ships that will transport the cargo
-- @param Core.Set#SET_CARGO CargoSet The set of @{Cargo.Cargo#CARGO} objects, which can be CARGO_GROUP, CARGO_CRATE, or CARGO_SLINGLOAD objects.
-- @param Core.Set#SET_ZONE PickupZoneSet The set of pickup zones which are used to determine from where the cargo can be picked up by the Ship.
-- @param Core.Set#SET_ZONE DeployZoneSet The set of deploy zones which determine where the cargo will be deployed by the Ship.
-- @param #table ShippingLane Table containing list of Shipping Lanes to be used
-- @return #AI_CARGO_DISPATCHER_SHIP
-- @usage
--
-- -- An AI dispatcher object for a naval group, moving cargo from pickup zones to deploy zones via a predetermined Shipping Lane
--
-- local SetCargoInfantry = SET_CARGO:New():FilterTypes( "Infantry" ):FilterStart()
-- local SetShip = SET_GROUP:New():FilterPrefixes( "Ship" ):FilterStart()
-- local SetPickupZones = SET_ZONE:New():FilterPrefixes( "Pickup" ):FilterStart()
-- local SetDeployZones = SET_ZONE:New():FilterPrefixes( "Deploy" ):FilterStart()
-- NEED MORE THOUGHT - ShippingLane is part of Warehouse.......
-- local ShippingLane = GROUP:New():FilterPrefixes( "ShippingLane" ):FilterStart()
--
-- AICargoDispatcherShip = AI_CARGO_DISPATCHER_SHIP:New( SetShip, SetCargoInfantry, SetPickupZones, SetDeployZones, ShippingLane )
-- AICargoDispatcherShip:Start()
--
function AI_CARGO_DISPATCHER_SHIP:New( ShipSet, CargoSet, PickupZoneSet, DeployZoneSet, ShippingLane )
local self = BASE:Inherit( self, AI_CARGO_DISPATCHER:New( ShipSet, CargoSet, PickupZoneSet, DeployZoneSet ) )
self:SetPickupSpeed( 60, 10 )
self:SetDeploySpeed( 60, 10 )
self:SetPickupRadius( 500, 6000 )
self:SetDeployRadius( 500, 6000 )
self:SetPickupHeight( 0, 0 )
self:SetDeployHeight( 0, 0 )
self:SetShippingLane( ShippingLane )
self:SetMonitorTimeInterval( 600 )
return self
end
function AI_CARGO_DISPATCHER_SHIP:SetShippingLane( ShippingLane )
self.ShippingLane = ShippingLane
return self
end
function AI_CARGO_DISPATCHER_SHIP:AICargo( Ship, CargoSet )
return AI_CARGO_SHIP:New( Ship, CargoSet, 0, self.ShippingLane )
end

View File

@ -1,4 +1,4 @@
--- **AI** -- (R2.4) - Models the intelligent transportation of infantry (cargo).
--- **AI** - Models the intelligent transportation of cargo using helicopters.
--
-- ===
--
@ -33,7 +33,7 @@
--
-- ## Infantry health.
--
-- When infantry is unboarded from the APCs, the infantry is actually respawned into the battlefield.
-- When infantry is unboarded from the helicopters, the infantry is actually respawned into the battlefield.
-- As a result, the unboarding infantry is very _healthy_ every time it unboards.
-- This is due to the limitation of the DCS simulator, which is not able to specify the health of new spawned units as a parameter.
-- However, infantry that was destroyed when unboarded, won't be respawned again. Destroyed is destroyed.
@ -67,16 +67,11 @@ function AI_CARGO_HELICOPTER:New( Helicopter, CargoSet )
self:AddTransition( "Unloaded", "Pickup", "*" )
self:AddTransition( "Loaded", "Deploy", "*" )
self:AddTransition( { "Unloaded", "Loading" }, "Load", "Boarding" )
self:AddTransition( "Boarding", "Board", "Boarding" )
self:AddTransition( "Boarding", "Loaded", "Boarding" )
self:AddTransition( "Boarding", "PickedUp", "Loaded" )
self:AddTransition( "Loaded", "Unload", "Unboarding" )
self:AddTransition( "Unboarding", "Unboard", "Unboarding" )
self:AddTransition( "Unboarding", "Unloaded", "Unboarding" )
self:AddTransition( "Unboarding", "Deployed", "Unloaded" )
self:AddTransition( "*", "Loaded", "Loaded" )
self:AddTransition( "Unboarding", "Pickup", "Unloaded" )
self:AddTransition( "Unloaded", "Unboard", "Unloaded" )
self:AddTransition( "Unloaded", "Unloaded", "Unloaded" )
self:AddTransition( "*", "PickedUp", "*" )
self:AddTransition( "*", "Landed", "*" )
self:AddTransition( "*", "Queue", "*" )
self:AddTransition( "*", "Orbit" , "*" )
@ -100,13 +95,31 @@ function AI_CARGO_HELICOPTER:New( Helicopter, CargoSet )
-- @param #string Event
-- @param #string To
-- @param Core.Point#COORDINATE Coordinate
-- @param #number Speed Speed in km/h to drive to the pickup coordinate. Default is 50% of max possible speed the unit can go.
-- @param #number Speed Speed in km/h to fly to the pickup coordinate. Default is 50% of max possible speed the unit can go.
--- PickedUp Handler OnAfter for AI_CARGO_HELICOPTER - Cargo set has been picked up, ready to deploy
-- @function [parent=#AI_CARGO_HELICOPTER] OnAfterPickedUp
-- @param #AI_CARGO_HELICOPTER self
-- @param Wrapper.Group#GROUP Helicopter The helicopter #GROUP object
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param Wrapper.Unit#UNIT Unit The helicopter #UNIT object
--- Unloaded Handler OnAfter for AI_CARGO_HELICOPTER - Cargo unloaded, carrier is empty
-- @function [parent=#AI_CARGO_HELICOPTER] OnAfterUnloaded
-- @param #AI_CARGO_HELICOPTER self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param Cargo.CargoGroup#CARGO_GROUP Cargo The #CARGO_GROUP object.
-- @param Wrapper.Unit#UNIT Unit The helicopter #UNIT object
--- Pickup Trigger for AI_CARGO_HELICOPTER
-- @function [parent=#AI_CARGO_HELICOPTER] Pickup
-- @param #AI_CARGO_HELICOPTER self
-- @param Core.Point#COORDINATE Coordinate
-- @param #number Speed Speed in km/h to drive to the pickup coordinate. Default is 50% of max possible speed the unit can go.
-- @param #number Speed Speed in km/h to fly to the pickup coordinate. Default is 50% of max possible speed the unit can go.
--- Pickup Asynchronous Trigger for AI_CARGO_HELICOPTER
-- @function [parent=#AI_CARGO_HELICOPTER] __Pickup
@ -122,7 +135,7 @@ function AI_CARGO_HELICOPTER:New( Helicopter, CargoSet )
-- @param #string Event
-- @param #string To
-- @param Core.Point#COORDINATE Coordinate Place at which cargo is deployed.
-- @param #number Speed Speed in km/h to drive to the pickup coordinate. Default is 50% of max possible speed the unit can go.
-- @param #number Speed Speed in km/h to fly to the pickup coordinate. Default is 50% of max possible speed the unit can go.
-- @return #boolean
--- Deploy Handler OnAfter for AI_CARGO_HELICOPTER
@ -132,21 +145,42 @@ function AI_CARGO_HELICOPTER:New( Helicopter, CargoSet )
-- @param #string Event
-- @param #string To
-- @param Core.Point#COORDINATE Coordinate
-- @param #number Speed Speed in km/h to drive to the pickup coordinate. Default is 50% of max possible speed the unit can go.
-- @param #number Speed Speed in km/h to fly to the pickup coordinate. Default is 50% of max possible speed the unit can go.
--- Deployed Handler OnAfter for AI_CARGO_HELICOPTER
-- @function [parent=#AI_CARGO_HELICOPTER] OnAfterDeployed
-- @param #AI_CARGO_HELICOPTER self
-- @param #string From
-- @param #string Event
-- @param #string To
--- Deploy Trigger for AI_CARGO_HELICOPTER
-- @function [parent=#AI_CARGO_HELICOPTER] Deploy
-- @param #AI_CARGO_HELICOPTER self
-- @param Core.Point#COORDINATE Coordinate Place at which the cargo is deployed.
-- @param #number Speed Speed in km/h to drive to the pickup coordinate. Default is 50% of max possible speed the unit can go.
-- @param #number Speed Speed in km/h to fly to the pickup coordinate. Default is 50% of max possible speed the unit can go.
--- Deploy Asynchronous Trigger for AI_CARGO_HELICOPTER
-- @function [parent=#AI_CARGO_HELICOPTER] __Deploy
-- @param #number Delay Delay in seconds.
-- @param #AI_CARGO_HELICOPTER self
-- @param Core.Point#COORDINATE Coordinate Place at which the cargo is deployed.
-- @param #number Speed Speed in km/h to drive to the pickup coordinate. Default is 50% of max possible speed the unit can go.
-- @param #number Speed Speed in km/h to fly to the pickup coordinate. Default is 50% of max possible speed the unit can go.
--- Home Trigger for AI_CARGO_HELICOPTER
-- @function [parent=#AI_CARGO_HELICOPTER] Home
-- @param #AI_CARGO_HELICOPTER self
-- @param Core.Point#COORDINATE Coordinate Place to which the helicopter will go.
-- @param #number Speed (optional) Speed in km/h to fly to the pickup coordinate. Default is 50% of max possible speed the unit can go.
-- @param #number Height (optional) Height the Helicopter should be flying at.
--- Home Asynchronous Trigger for AI_CARGO_HELICOPTER
-- @function [parent=#AI_CARGO_HELICOPTER] __Home
-- @param #number Delay Delay in seconds.
-- @param #AI_CARGO_HELICOPTER self
-- @param Core.Point#COORDINATE Coordinate Place to which the helicopter will go.
-- @param #number Speed (optional) Speed in km/h to fly to the pickup coordinate. Default is 50% of max possible speed the unit can go.
-- @param #number Height (optional) Height the Helicopter should be flying at.
-- We need to capture the Crash events for the helicopters.
-- The helicopter reference is used in the semaphore AI_CARGO_QUEUE.
@ -228,7 +262,7 @@ end
-- @param Event
-- @param To
function AI_CARGO_HELICOPTER:onafterLanded( Helicopter, From, Event, To )
self:F({From, Event, To})
Helicopter:F( { Name = Helicopter:GetName() } )
if Helicopter and Helicopter:IsAlive() then
@ -243,7 +277,7 @@ function AI_CARGO_HELICOPTER:onafterLanded( Helicopter, From, Event, To )
self:F( { Helicopter:GetName(), Height = Helicopter:GetHeight( true ), Velocity = Helicopter:GetVelocityKMH() } )
if self.RoutePickup == true then
if Helicopter:GetHeight( true ) <= 5 and Helicopter:GetVelocityKMH() < 10 then
if Helicopter:GetHeight( true ) <= 5.5 and Helicopter:GetVelocityKMH() < 15 then
--self:Load( Helicopter:GetPointVec2() )
self:Load( self.PickupZone )
self.RoutePickup = false
@ -251,7 +285,7 @@ function AI_CARGO_HELICOPTER:onafterLanded( Helicopter, From, Event, To )
end
if self.RouteDeploy == true then
if Helicopter:GetHeight( true ) <= 5 and Helicopter:GetVelocityKMH() < 10 then
if Helicopter:GetHeight( true ) <= 5.5 and Helicopter:GetVelocityKMH() < 15 then
self:Unload( self.DeployZone )
self.RouteDeploy = false
end
@ -269,7 +303,7 @@ end
-- @param Core.Point#COORDINATE Coordinate
-- @param #number Speed
function AI_CARGO_HELICOPTER:onafterQueue( Helicopter, From, Event, To, Coordinate, Speed, DeployZone )
self:F({From, Event, To, Coordinate, Speed, DeployZone})
local HelicopterInZone = false
if Helicopter and Helicopter:IsAlive() == true then
@ -308,7 +342,11 @@ function AI_CARGO_HELICOPTER:onafterQueue( Helicopter, From, Event, To, Coordina
-- true
-- )
-- Route[#Route+1] = WaypointFrom
local CoordinateTo = Coordinate
local CoordinateTo = Coordinate
local landheight = CoordinateTo:GetLandHeight() -- get target height
CoordinateTo.y = landheight + 50 -- flight height should be 50m above ground
local WaypointTo = CoordinateTo:WaypointAir(
"RADIO",
POINT_VEC3.RoutePointType.TurningPoint,
@ -348,28 +386,17 @@ end
-- @param Core.Point#COORDINATE Coordinate
-- @param #number Speed
function AI_CARGO_HELICOPTER:onafterOrbit( Helicopter, From, Event, To, Coordinate )
self:F({From, Event, To, Coordinate})
if Helicopter and Helicopter:IsAlive() then
local Route = {}
-- local CoordinateFrom = Helicopter:GetCoordinate()
-- local WaypointFrom = CoordinateFrom:WaypointAir(
-- "RADIO",
-- POINT_VEC3.RoutePointType.TurningPoint,
-- POINT_VEC3.RoutePointAction.TurningPoint,
-- Speed,
-- true
-- )
-- Route[#Route+1] = WaypointFrom
local CoordinateTo = Coordinate
local WaypointTo = CoordinateTo:WaypointAir(
"RADIO",
POINT_VEC3.RoutePointType.TurningPoint,
POINT_VEC3.RoutePointAction.TurningPoint,
50,
true
)
local CoordinateTo = Coordinate
local landheight = CoordinateTo:GetLandHeight() -- get target height
CoordinateTo.y = landheight + 50 -- flight height should be 50m above ground
local WaypointTo = CoordinateTo:WaypointAir("RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, 50, true)
Route[#Route+1] = WaypointTo
local Tasks = {}
@ -379,7 +406,7 @@ function AI_CARGO_HELICOPTER:onafterOrbit( Helicopter, From, Event, To, Coordina
Route[#Route+1] = WaypointTo
-- Now route the helicopter
Helicopter:Route( Route, 0 )
Helicopter:Route(Route, 0)
end
end
@ -395,7 +422,7 @@ end
-- @param #boolean Deployed Cargo is deployed.
-- @return #boolean True if all cargo has been unloaded.
function AI_CARGO_HELICOPTER:onafterDeployed( Helicopter, From, Event, To, DeployZone )
self:F( { Helicopter, From, Event, To, DeployZone = DeployZone } )
self:F( { From, Event, To, DeployZone = DeployZone } )
self:Orbit( Helicopter:GetCoordinate(), 50 )
@ -408,7 +435,6 @@ function AI_CARGO_HELICOPTER:onafterDeployed( Helicopter, From, Event, To, Deplo
self:GetParent( self, AI_CARGO_HELICOPTER ).onafterDeployed( self, Helicopter, From, Event, To, DeployZone )
end
--- On after Pickup event.
@ -418,11 +444,12 @@ end
-- @param Event
-- @param To
-- @param Core.Point#COORDINATE Coordinate Pickup place.
-- @param #number Speed Speed in km/h to drive to the pickup coordinate. Default is 50% of max possible speed the unit can go.
-- @param #number Speed Speed in km/h to fly to the pickup coordinate. Default is 50% of max possible speed the unit can go.
-- @param #number Height Height in meters to move to the pickup coordinate. This parameter is ignored for APCs.
-- @param Core.Zone#ZONE PickupZone (optional) The zone where the cargo will be picked up. The PickupZone can be nil, if there wasn't any PickupZoneSet provided.
function AI_CARGO_HELICOPTER:onafterPickup( Helicopter, From, Event, To, Coordinate, Speed, Height, PickupZone )
self:F({Coordinate, Speed, Height, PickupZone })
if Helicopter and Helicopter:IsAlive() ~= nil then
Helicopter:Activate()
@ -436,25 +463,16 @@ function AI_CARGO_HELICOPTER:onafterPickup( Helicopter, From, Event, To, Coordin
--- Calculate the target route point.
local CoordinateFrom = Helicopter:GetCoordinate()
local CoordinateTo = Coordinate
--- Create a route point of type air.
local WaypointFrom = CoordinateFrom:WaypointAir(
"RADIO",
POINT_VEC3.RoutePointType.TurningPoint,
POINT_VEC3.RoutePointAction.TurningPoint,
_speed,
true
)
local WaypointFrom = CoordinateFrom:WaypointAir("RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, _speed, true)
--- Create a route point of type air.
local WaypointTo = CoordinateTo:WaypointAir(
"RADIO",
POINT_VEC3.RoutePointType.TurningPoint,
POINT_VEC3.RoutePointAction.TurningPoint,
_speed,
true
)
local CoordinateTo = Coordinate
local landheight = CoordinateTo:GetLandHeight() -- get target height
CoordinateTo.y = landheight + 50 -- flight height should be 50m above ground
local WaypointTo = CoordinateTo:WaypointAir("RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint,_speed, true)
Route[#Route+1] = WaypointFrom
Route[#Route+1] = WaypointTo
@ -495,10 +513,10 @@ end
-- @param Event
-- @param To
-- @param Core.Point#COORDINATE Coordinate Place at which the cargo is deployed.
-- @param #number Speed Speed in km/h to drive to the pickup coordinate. Default is 50% of max possible speed the unit can go.
-- @param #number Speed Speed in km/h to fly to the pickup coordinate. Default is 50% of max possible speed the unit can go.
-- @param #number Height Height in meters to move to the deploy coordinate.
function AI_CARGO_HELICOPTER:onafterDeploy( Helicopter, From, Event, To, Coordinate, Speed, Height, DeployZone )
self:F({From, Event, To, Coordinate, Speed, Height, DeployZone})
if Helicopter and Helicopter:IsAlive() ~= nil then
self.RouteDeploy = true
@ -514,25 +532,17 @@ function AI_CARGO_HELICOPTER:onafterDeploy( Helicopter, From, Event, To, Coordin
--- Create a route point of type air.
local CoordinateFrom = Helicopter:GetCoordinate()
local WaypointFrom = CoordinateFrom:WaypointAir(
"RADIO",
POINT_VEC3.RoutePointType.TurningPoint,
POINT_VEC3.RoutePointAction.TurningPoint,
_speed,
true
)
local WaypointFrom = CoordinateFrom:WaypointAir("RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, _speed, true)
Route[#Route+1] = WaypointFrom
Route[#Route+1] = WaypointFrom
--- Create a route point of type air.
local CoordinateTo = Coordinate
local WaypointTo = CoordinateTo:WaypointAir(
"RADIO",
POINT_VEC3.RoutePointType.TurningPoint,
POINT_VEC3.RoutePointAction.TurningPoint,
_speed,
true
)
local CoordinateTo = Coordinate
local landheight = CoordinateTo:GetLandHeight() -- get target height
CoordinateTo.y = landheight + 50 -- flight height should be 50m above ground
local WaypointTo = CoordinateTo:WaypointAir("RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, _speed, true)
Route[#Route+1] = WaypointTo
Route[#Route+1] = WaypointTo
@ -542,7 +552,9 @@ function AI_CARGO_HELICOPTER:onafterDeploy( Helicopter, From, Event, To, Coordin
local Tasks = {}
-- The _Deploy function does not exist.
Tasks[#Tasks+1] = Helicopter:TaskFunction( "AI_CARGO_HELICOPTER._Deploy", self, Coordinate, DeployZone )
Tasks[#Tasks+1] = Helicopter:TaskOrbitCircle( math.random( 30, 100 ), _speed, CoordinateTo:GetRandomCoordinateInRadius( 800, 500 ) )
--Tasks[#Tasks+1] = Helicopter:TaskLandAtVec2( CoordinateTo:GetVec2() )
@ -566,11 +578,12 @@ end
-- @param Event
-- @param To
-- @param Core.Point#COORDINATE Coordinate Home place.
-- @param #number Speed Speed in km/h to drive to the pickup coordinate. Default is 50% of max possible speed the unit can go.
-- @param #number Speed Speed in km/h to fly to the pickup coordinate. Default is 50% of max possible speed the unit can go.
-- @param #number Height Height in meters to move to the home coordinate.
-- @param Core.Zone#ZONE HomeZone The zone wherein the carrier will return when all cargo has been transported. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE.
function AI_CARGO_HELICOPTER:onafterHome( Helicopter, From, Event, To, Coordinate, Speed, Height, HomeZone )
self:F({From, Event, To, Coordinate, Speed, Height})
if Helicopter and Helicopter:IsAlive() ~= nil then
self.RouteHome = true
@ -579,30 +592,23 @@ function AI_CARGO_HELICOPTER:onafterHome( Helicopter, From, Event, To, Coordinat
--- Calculate the target route point.
Coordinate.y = Height
--Coordinate.y = Height
Height = Height or 50
Speed = Speed or Helicopter:GetSpeedMax()*0.5
--- Create a route point of type air.
local CoordinateFrom = Helicopter:GetCoordinate()
local WaypointFrom = CoordinateFrom:WaypointAir(
"RADIO",
POINT_VEC3.RoutePointType.TurningPoint,
POINT_VEC3.RoutePointAction.TurningPoint,
Speed ,
true
)
local WaypointFrom = CoordinateFrom:WaypointAir("RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, Speed, true)
Route[#Route+1] = WaypointFrom
--- Create a route point of type air.
local CoordinateTo = Coordinate
local WaypointTo = CoordinateTo:WaypointAir(
"RADIO",
POINT_VEC3.RoutePointType.TurningPoint,
POINT_VEC3.RoutePointAction.TurningPoint,
Speed ,
true
)
local CoordinateTo = Coordinate
local landheight = CoordinateTo:GetLandHeight() -- get target height
CoordinateTo.y = landheight + Height -- flight height should be 50m above ground
local WaypointTo = CoordinateTo:WaypointAir("RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, Speed, true)
Route[#Route+1] = WaypointTo
@ -613,12 +619,11 @@ function AI_CARGO_HELICOPTER:onafterHome( Helicopter, From, Event, To, Coordinat
Tasks[#Tasks+1] = Helicopter:TaskLandAtVec2( CoordinateTo:GetVec2() )
Route[#Route].task = Helicopter:TaskCombo( Tasks )
Route[#Route+1] = WaypointTo
-- Now route the helicopter
Helicopter:Route( Route, 0 )
Helicopter:Route(Route, 0)
end
end

View File

@ -0,0 +1,397 @@
--- **AI** -- (R2.5.1) - Models the intelligent transportation of infantry and other cargo.
--
-- ===
--
-- ### Author: **acrojason** (derived from AI_Cargo_APC by FlightControl)
--
-- ===
--
-- @module AI.AI_Cargo_Ship
-- @image AI_Cargo_Dispatcher.JPG
--- @type AI_CARGO_SHIP
-- @extends AI.AI_Cargo#AI_CARGO
--- Brings a dynamic cargo handling capability for an AI naval group.
--
-- Naval ships can be utilized to transport cargo around the map following naval shipping lanes.
-- The AI_CARGO_SHIP class uses the @{Cargo.Cargo} capabilities within the MOOSE framework.
-- @{Cargo.Cargo} must be declared within the mission or warehouse to make the AI_CARGO_SHIP recognize the cargo.
-- Please consult the @{Cargo.Cargo} module for more information.
--
-- ## Cargo loading.
--
-- The module will automatically load cargo when the Ship is within boarding or loading radius.
-- The boarding or loading radius is specified when the cargo is created in the simulation and depends on the type of
-- cargo and the specified boarding radius.
--
-- ## Defending the Ship when enemies are nearby
-- This is not supported for naval cargo because most tanks don't float. Protect your transports...
--
-- ## Infantry or cargo **health**.
-- When cargo is unboarded from the Ship, the cargo is actually respawned into the battlefield.
-- As a result, the unboarding cargo is very _healthy_ every time it unboards.
-- This is due to the limitation of the DCS simulator, which is not able to specify the health of newly spawned units as a parameter.
-- However, cargo that was destroyed when unboarded and following the Ship won't be respawned again (this is likely not a thing for
-- naval cargo due to the lack of support for defending the Ship mentioned above). Destroyed is destroyed.
-- As a result, there is some additional strength that is gained when an unboarding action happens, but in terms of simulation balance
-- this has marginal impact on the overall battlefield simulation. Given the relatively short duration of DCS missions and the somewhat
-- lengthy naval transport times, most units entering the Ship as cargo will be freshly en route to an amphibious landing or transporting
-- between warehouses.
--
-- ## Control the Ships on the map.
--
-- Currently, naval transports can only be controlled via scripts due to their reliance upon predefined Shipping Lanes created in the Mission
-- Editor. An interesting future enhancement could leverage new pathfinding functionality for ships in the Ops module.
--
-- ## Cargo deployment.
--
-- Using the @{AI_CARGO_SHIP.Deploy}() method, you are able to direct the Ship towards a Deploy zone to unboard/unload the cargo at the
-- specified coordinate. The Ship will follow the Shipping Lane to ensure consistent cargo transportation within the simulation environment.
--
-- ## Cargo pickup.
--
-- Using the @{AI_CARGO_SHIP.Pickup}() method, you are able to direct the Ship towards a Pickup zone to board/load the cargo at the specified
-- coordinate. The Ship will follow the Shipping Lane to ensure consistent cargo transportation within the simulation environment.
--
--
-- @field #AI_CARGO_SHIP
AI_CARGO_SHIP = {
ClassName = "AI_CARGO_SHIP",
Coordinate = nil -- Core.Point#COORDINATE
}
--- Creates a new AI_CARGO_SHIP object.
-- @param #AI_CARGO_SHIP self
-- @param Wrapper.Group#GROUP Ship The carrier Ship group
-- @param Core.Set#SET_CARGO CargoSet The set of cargo to be transported
-- @param #number CombatRadius Provide the combat radius to defend the carrier by unboarding the cargo when enemies are nearby. When CombatRadius is 0, no defense will occur.
-- @param #table ShippingLane Table containing list of Shipping Lanes to be used
-- @return #AI_CARGO_SHIP
function AI_CARGO_SHIP:New( Ship, CargoSet, CombatRadius, ShippingLane )
local self = BASE:Inherit( self, AI_CARGO:New( Ship, CargoSet ) ) -- #AI_CARGO_SHIP
self:AddTransition( "*", "Monitor", "*" )
self:AddTransition( "*", "Destroyed", "Destroyed" )
self:AddTransition( "*", "Home", "*" )
self:SetCombatRadius( 0 ) -- Don't want to deploy cargo in middle of water to defend Ship, so set CombatRadius to 0
self:SetShippingLane ( ShippingLane )
self:SetCarrier( Ship )
return self
end
--- Set the Carrier
-- @param #AI_CARGO_SHIP self
-- @param Wrapper.Group#GROUP CargoCarrier
-- @return #AI_CARGO_SHIP
function AI_CARGO_SHIP:SetCarrier( CargoCarrier )
self.CargoCarrier = CargoCarrier -- Wrapper.Group#GROUIP
self.CargoCarrier:SetState( self.CargoCarrier, "AI_CARGO_SHIP", self )
CargoCarrier:HandleEvent( EVENTS.Dead )
function CargoCarrier:OnEventDead( EventData )
self:F({"dead"})
local AICargoTroops = self:GetState( self, "AI_CARGO_SHIP" )
self:F({AICargoTroops=AICargoTroops})
if AICargoTroops then
self:F({})
if not AICargoTroops:Is( "Loaded" ) then
-- Better hope they can swim!
AICargoTroops:Destroyed()
end
end
end
self.Zone = ZONE_UNIT:New( self.CargoCarrier:GetName() .. "-Zone", self.CargoCarrier, self.CombatRadius )
self.Coalition = self.CargoCarrier:GetCoalition()
self:SetControllable( CargoCarrier )
return self
end
--- FInd a free Carrier within a radius
-- @param #AI_CARGO_SHIP self
-- @param Core.Point#COORDINATE Coordinate
-- @param #number Radius
-- @return Wrapper.Group#GROUP NewCarrier
function AI_CARGO_SHIP:FindCarrier( Coordinate, Radius )
local CoordinateZone = ZONE_RADIUS:New( "Zone", Coordinate:GetVec2(), Radius )
CoordinateZone:Scan( { Object.Category.UNIT } )
for _, DCSUnit in pairs( CoordinateZone:GetScannedUnits() ) do
local NearUnit = UNIT:Find( DCSUnit )
self:F({NearUnit=NearUnit})
if not NearUnit:GetState( NearUnit, "AI_CARGO_SHIP" ) then
local Attributes = NearUnit:GetDesc()
self:F({Desc=Attributes})
if NearUnit:HasAttributes( "Trucks" ) then
return NearUnit:GetGroup()
end
end
end
return nil
end
function AI_CARGO_SHIP:SetShippingLane( ShippingLane )
self.ShippingLane = ShippingLane
return self
end
function AI_CARGO_SHIP:SetCombatRadius( CombatRadius )
self.CombatRadius = CombatRadius or 0
return self
end
--- Follow Infantry to the Carrier
-- @param #AI_CARGO_SHIP self
-- @param #AI_CARGO_SHIP Me
-- @param Wrapper.Unit#UNIT ShipUnit
-- @param Cargo.CargoGroup#CARGO_GROUP Cargo
-- @return #AI_CARGO_SHIP
function AI_CARGO_SHIP:FollowToCarrier( Me, ShipUnit, CargoGroup )
local InfantryGroup = CargoGroup:GetGroup()
self:F( { self=self:GetClassNameAndID(), InfantryGroup = InfantryGroup:GetName() } )
if ShipUnit:IsAlive() then
-- Check if the Cargo is near the CargoCarrier
if InfantryGroup:IsPartlyInZone( ZONE_UNIT:New( "Radius", ShipUnit, 1000 ) ) then
-- Cargo does not need to navigate to Carrier
Me:Guard()
else
self:F( { InfantryGroup = InfantryGroup:GetName() } )
if InfantryGroup:IsAlive() then
self:F( { InfantryGroup = InfantryGroup:GetName() } )
local Waypoints = {}
-- Calculate new route
local FromCoord = InfantryGroup:GetCoordinate()
local FromGround = FromCoord:WaypointGround( 10, "Diamond" )
self:F({FromGround=FromGround})
table.insert( Waypoints, FromGround )
local ToCoord = ShipUnit:GetCoordinate():GetRandomCoordinateInRadius( 10, 5 )
local ToGround = ToCoord:WaypointGround( 10, "Diamond" )
self:F({ToGround=ToGround})
table.insert( Waypoints, ToGround )
local TaskRoute = InfantryGroup:TaskFunction( "AI_CARGO_SHIP.FollowToCarrier", Me, ShipUnit, CargoGroup )
self:F({Waypoints=Waypoints})
local Waypoint = Waypoints[#Waypoints]
InfantryGroup:SetTaskWaypoint( Waypoint, TaskRoute ) -- Set for the given Route at Waypoint 2 the TaskRouteToZone
InfantryGroup:Route( Waypoints, 1 ) -- Move after a random number of seconds to the Route. See Route method for details
end
end
end
end
function AI_CARGO_SHIP:onafterMonitor( Ship, From, Event, To )
self:F( { Ship, From, Event, To, IsTransporting = self:IsTransporting() } )
if self.CombatRadius > 0 then
-- We really shouldn't find ourselves in here for Ships since the CombatRadius should always be 0.
-- This is to avoid Unloading the Ship in the middle of the sea.
if Ship and Ship:IsAlive() then
if self.CarrierCoordinate then
if self:IsTransporting() == true then
local Coordinate = Ship:GetCoordinate()
if self:Is( "Unloaded" ) or self:Is( "Loaded" ) then
self.Zone:Scan( { Object.Category.UNIT } )
if self.Zone:IsAllInZoneOfCoalition( self.Coalition ) then
if self:Is( "Unloaded" ) then
-- There are no enemies within combat radius. Reload the CargoCarrier.
self:Reload()
end
else
if self:Is( "Loaded" ) then
-- There are enemies within combat radius. Unload the CargoCarrier.
self:__Unload( 1, nil, true ) -- The 2nd parameter is true, which means that the unload is for defending the carrier, not to deploy!
else
if self:Is( "Unloaded" ) then
--self:Follow()
end
self:F( "I am here" .. self:GetCurrentState() )
if self:Is( "Following" ) then
for Cargo, ShipUnit in pairs( self.Carrier_Cargo ) do
local Cargo = Cargo -- Cargo.Cargo#CARGO
local ShipUnit = ShipUnit -- Wrapper.Unit#UNIT
if Cargo:IsAlive() then
if not Cargo:IsNear( ShipUnit, 40 ) then
ShipUnit:RouteStop()
self.CarrierStopped = true
else
if self.CarrierStopped then
if Cargo:IsNear( ShipUnit, 25 ) then
ShipUnit:RouteResume()
self.CarrierStopped = nil
end
end
end
end
end
end
end
end
end
end
end
self.CarrierCoordinate = Ship:GetCoordinate()
end
self:__Monitor( -5 )
end
end
--- Check if cargo ship is alive and trigger Load event
-- @param Wrapper.Group#Group Ship
-- @param #AI_CARGO_SHIP self
function AI_CARGO_SHIP._Pickup( Ship, self, Coordinate, Speed, PickupZone )
Ship:F( { "AI_CARGO_Ship._Pickup:", Ship:GetName() } )
if Ship:IsAlive() then
self:Load( PickupZone )
end
end
--- Check if cargo ship is alive and trigger Unload event. Good time to remind people that Lua is case sensitive and Unload != UnLoad
-- @param Wrapper.Group#GROUP Ship
-- @param #AI_CARGO_SHIP self
function AI_CARGO_SHIP._Deploy( Ship, self, Coordinate, DeployZone )
Ship:F( { "AI_CARGO_Ship._Deploy:", Ship } )
if Ship:IsAlive() then
self:Unload( DeployZone )
end
end
--- on after Pickup event.
-- @param AI_CARGO_SHIP Ship
-- @param From
-- @param Event
-- @param To
-- @param Core.Point#COORDINATE Coordinate of the pickup point
-- @param #number Speed Speed in km/h to sail to the pickup coordinate. Default is 50% of max speed for the unit
-- @param #number Height Altitude in meters to move to the pickup coordinate. This parameter is ignored for Ships
-- @param Core.Zone#ZONE PickupZone (optional) The zone where the cargo will be picked up. The PickupZone can be nil if there was no PickupZoneSet provided
function AI_CARGO_SHIP:onafterPickup( Ship, From, Event, To, Coordinate, Speed, Height, PickupZone )
if Ship and Ship:IsAlive() then
AI_CARGO_SHIP._Pickup( Ship, self, Coordinate, Speed, PickupZone )
self:GetParent( self, AI_CARGO_SHIP ).onafterPickup( self, Ship, From, Event, To, Coordinate, Speed, Height, PickupZone )
end
end
--- On after Deploy event.
-- @param #AI_CARGO_SHIP self
-- @param Wrapper.Group#GROUP SHIP
-- @param From
-- @param Event
-- @param To
-- @param Core.Point#COORDINATE Coordinate Coordinate of the deploy point
-- @param #number Speed Speed in km/h to sail to the deploy coordinate. Default is 50% of max speed for the unit
-- @param #number Height Altitude in meters to move to the deploy coordinate. This parameter is ignored for Ships
-- @param Core.Zone#ZONE DeployZone The zone where the cargo will be deployed.
function AI_CARGO_SHIP:onafterDeploy( Ship, From, Event, To, Coordinate, Speed, Height, DeployZone )
if Ship and Ship:IsAlive() then
Speed = Speed or Ship:GetSpeedMax()*0.8
local lane = self.ShippingLane
if lane then
local Waypoints = {}
for i=1, #lane do
local coord = lane[i]
local Waypoint = coord:WaypointGround(_speed)
table.insert(Waypoints, Waypoint)
end
local TaskFunction = Ship:TaskFunction( "AI_CARGO_SHIP._Deploy", self, Coordinate, DeployZone )
local Waypoint = Waypoints[#Waypoints]
Ship:SetTaskWaypoint( Waypoint, TaskFunction )
Ship:Route(Waypoints, 1)
self:GetParent( self, AI_CARGO_SHIP ).onafterDeploy( self, Ship, From, Event, To, Coordinate, Speed, Height, DeployZone )
else
self:E(self.lid.."ERROR: No shipping lane defined for Naval Transport!")
end
end
end
--- On after Unload event.
-- @param #AI_CARGO_SHIP self
-- @param Wrapper.Group#GROUP Ship
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE.
function AI_CARGO_SHIP:onafterUnload( Ship, From, Event, To, DeployZone, Defend )
self:F( { Ship, From, Event, To, DeployZone, Defend = Defend } )
local UnboardInterval = 5
local UnboardDelay = 5
if Ship and Ship:IsAlive() then
for _, ShipUnit in pairs( Ship:GetUnits() ) do
local ShipUnit = ShipUnit -- Wrapper.Unit#UNIT
Ship:RouteStop()
for _, Cargo in pairs( ShipUnit:GetCargo() ) do
self:F( { Cargo = Cargo:GetName(), Isloaded = Cargo:IsLoaded() } )
if Cargo:IsLoaded() then
local unboardCoord = DeployZone:GetRandomPointVec2()
Cargo:__UnBoard( UnboardDelay, unboardCoord, 1000)
UnboardDelay = UnboardDelay + Cargo:GetCount() * UnboardInterval
self:__Unboard( UnboardDelay, Cargo, ShipUnit, DeployZone, Defend )
if not Defend == true then
Cargo:SetDeployed( true )
end
end
end
end
end
end
function AI_CARGO_SHIP:onafterHome( Ship, From, Event, To, Coordinate, Speed, Height, HomeZone )
if Ship and Ship:IsAlive() then
self.RouteHome = true
Speed = Speed or Ship:GetSpeedMax()*0.8
local lane = self.ShippingLane
if lane then
local Waypoints = {}
-- Need to find a more generalized way to do this instead of reversing the shipping lane.
-- This only works if the Source/Dest route waypoints are numbered 1..n and not n..1
for i=#lane, 1, -1 do
local coord = lane[i]
local Waypoint = coord:WaypointGround(_speed)
table.insert(Waypoints, Waypoint)
end
local Waypoint = Waypoints[#Waypoints]
Ship:Route(Waypoints, 1)
else
self:E(self.lid.."ERROR: No shipping lane defined for Naval Transport!")
end
end
end

View File

@ -1140,8 +1140,8 @@ end
-- @param DCS#Vec3 CV2 Vec3
function AI_FORMATION:FollowMe(FollowGroup, ClientUnit, CT1, CV1, CT2, CV2)
if FollowGroup:GetState( FollowGroup, "Mode" ) == self.__Enum.Mode.Formation then
if FollowGroup:GetState( FollowGroup, "Mode" ) == self.__Enum.Mode.Formation and not self:Is("Stopped") then
self:T({Mode=FollowGroup:GetState( FollowGroup, "Mode" )})
FollowGroup:OptionROTEvadeFire()

View File

@ -190,7 +190,7 @@ do -- ACT_ROUTE
self:F( { ZoneName = ZoneName } )
local Zone = Zone -- Core.Zone#ZONE
local ZoneCoord = Zone:GetCoordinate()
local ZoneDistance = ZoneCoord:Get2DDistance( self.Coordinate )
local ZoneDistance = ZoneCoord:Get2DDistance( Coordinate )
self:F( { ShortestDistance, ShortestReferenceName } )
if ShortestDistance == 0 or ZoneDistance < ShortestDistance then
ShortestDistance = ZoneDistance

View File

@ -1,4 +1,4 @@
--- **Core** -- Management of CARGO logistics, that can be transported from and to transportation carriers.
--- **Cargo** - Management of CARGO logistics, that can be transported from and to transportation carriers.
--
-- ===
--
@ -1066,18 +1066,26 @@ do -- CARGO_REPRESENTABLE
--- CARGO_REPRESENTABLE Constructor.
-- @param #CARGO_REPRESENTABLE self
-- @param #string Type
-- @param #string Name
-- @param #number LoadRadius (optional)
-- @param #number NearRadius (optional)
-- @param Wrapper.Positionable#POSITIONABLE CargoObject The cargo object.
-- @param #string Type Type name
-- @param #string Name Name.
-- @param #number LoadRadius (optional) Radius in meters.
-- @param #number NearRadius (optional) Radius in meters when the cargo is loaded into the carrier.
-- @return #CARGO_REPRESENTABLE
function CARGO_REPRESENTABLE:New( CargoObject, Type, Name, LoadRadius, NearRadius )
-- Inherit CARGO.
local self = BASE:Inherit( self, CARGO:New( Type, Name, 0, LoadRadius, NearRadius ) ) -- #CARGO_REPRESENTABLE
self:F( { Type, Name, LoadRadius, NearRadius } )
local Desc = CargoObject:GetDesc()
self:I( { Desc = Desc } )
-- Descriptors.
local Desc=CargoObject:GetDesc()
self:T({Desc=Desc})
-- Weight.
local Weight = math.random( 80, 120 )
-- Adjust weight..
if Desc then
if Desc.typeName == "2B11 mortar" then
Weight = 210
@ -1086,13 +1094,8 @@ do -- CARGO_REPRESENTABLE
end
end
-- Set weight.
self:SetWeight( Weight )
-- local Box = CargoUnit:GetBoundingBox()
-- local VolumeUnit = ( Box.max.x - Box.min.x ) * ( Box.max.y - Box.min.y ) * ( Box.max.z - Box.min.z )
-- self:I( { VolumeUnit = VolumeUnit, WeightUnit = WeightUnit } )
--self:SetVolume( VolumeUnit )
return self
end

View File

@ -1,4 +1,4 @@
--- **Cargo** -- Management of grouped cargo logistics, which are based on a @{Wrapper.Group} object.
--- **Cargo** - Management of grouped cargo logistics, which are based on a @{Wrapper.Group} object.
--
-- ===
--
@ -56,6 +56,8 @@ do -- CARGO_GROUP
-- @param #number NearRadius (optional) Once the units are within this radius of the carrier, they are actually loaded, i.e. disappear from the scene.
-- @return #CARGO_GROUP Cargo group object.
function CARGO_GROUP:New( CargoGroup, Type, Name, LoadRadius, NearRadius )
-- Inherit CAROG_REPORTABLE
local self = BASE:Inherit( self, CARGO_REPORTABLE:New( Type, Name, 0, LoadRadius, NearRadius ) ) -- #CARGO_GROUP
self:F( { Type, Name, LoadRadius } )
@ -76,6 +78,9 @@ do -- CARGO_GROUP
local GroupName = CargoGroup:GetName()
self.CargoName = Name
self.CargoTemplate = UTILS.DeepCopy( _DATABASE:GetGroupTemplate( GroupName ) )
-- Deactivate late activation.
self.CargoTemplate.lateActivation=false
self.GroupTemplate = UTILS.DeepCopy( self.CargoTemplate )
self.GroupTemplate.name = self.CargoName .. "#CARGO"
@ -481,7 +486,7 @@ do -- CARGO_GROUP
-- @param #string Event
-- @param #string From
-- @param #string To
-- @param Core.Point#POINT_VEC2
-- @param Core.Point#POINT_VEC2 ToPointVec2
function CARGO_GROUP:onafterUnLoad( From, Event, To, ToPointVec2, ... )
--self:F( { From, Event, To, ToPointVec2 } )
@ -491,7 +496,10 @@ do -- CARGO_GROUP
self.CargoSet:ForEach(
function( Cargo )
--Cargo:UnLoad( ToPointVec2 )
local RandomVec2=ToPointVec2:GetRandomPointVec2InRadius(20, 10)
local RandomVec2=nil
if ToPointVec2 then
RandomVec2=ToPointVec2:GetRandomPointVec2InRadius(20, 10)
end
Cargo:UnBoard( RandomVec2 )
end
)

View File

@ -1,4 +1,4 @@
--- **Cargo** -- Management of single cargo logistics, which are based on a @{Wrapper.Unit} object.
--- **Cargo** - Management of single cargo logistics, which are based on a @{Wrapper.Unit} object.
--
-- ===
--
@ -46,14 +46,17 @@ do -- CARGO_UNIT
-- @param #number NearRadius (optional)
-- @return #CARGO_UNIT
function CARGO_UNIT:New( CargoUnit, Type, Name, LoadRadius, NearRadius )
local self = BASE:Inherit( self, CARGO_REPRESENTABLE:New( CargoUnit, Type, Name, LoadRadius, NearRadius ) ) -- #CARGO_UNIT
self:I( { Type, Name, LoadRadius, NearRadius } )
self:T( CargoUnit )
-- Inherit CARGO_REPRESENTABLE.
local self = BASE:Inherit( self, CARGO_REPRESENTABLE:New( CargoUnit, Type, Name, LoadRadius, NearRadius ) ) -- #CARGO_UNIT
-- Debug info.
self:T({Type=Type, Name=Name, LoadRadius=LoadRadius, NearRadius=NearRadius})
-- Set cargo object.
self.CargoObject = CargoUnit
self:T( self.ClassName )
-- Set event prio.
self:SetEventPriority( 5 )
return self
@ -100,7 +103,12 @@ do -- CARGO_UNIT
-- Respawn the group...
if self.CargoObject then
self.CargoObject:ReSpawnAt( FromPointVec2, CargoDeployHeading )
if CargoCarrier:IsShip() then
-- If CargoCarrier is a ship, we don't want to spawn the units in the water next to the boat. Use destination coord instead.
self.CargoObject:ReSpawnAt( ToPointVec2, CargoDeployHeading )
else
self.CargoObject:ReSpawnAt( FromPointVec2, CargoDeployHeading )
end
self:F( { "CargoUnits:", self.CargoObject:GetGroup():GetName() } )
self.CargoCarrier = nil

View File

@ -676,6 +676,44 @@ do -- Event Handling
-- @param #BASE self
-- @param Core.Event#EVENTDATA EventData The EventData structure.
--- Paratrooper landing.
-- @function [parent=#BASE] OnEventParatrooperLanding
-- @param #BASE self
-- @param Core.Event#EVENTDATA EventData The EventData structure.
--- Discard chair after ejection.
-- @function [parent=#BASE] OnEventDiscardChairAfterEjection
-- @param #BASE self
-- @param Core.Event#EVENTDATA EventData The EventData structure.
--- Weapon add. Fires when entering a mission per pylon with the name of the weapon (double pylons not counted, infinite wep reload not counted.
-- @function [parent=#BASE] OnEventParatrooperLanding
-- @param #BASE self
-- @param Core.Event#EVENTDATA EventData The EventData structure.
--- Trigger zone.
-- @function [parent=#BASE] OnEventTriggerZone
-- @param #BASE self
-- @param Core.Event#EVENTDATA EventData The EventData structure.
--- Landing quality mark.
-- @function [parent=#BASE] OnEventLandingQualityMark
-- @param #BASE self
-- @param Core.Event#EVENTDATA EventData The EventData structure.
--- BDA.
-- @function [parent=#BASE] OnEventBDA
-- @param #BASE self
-- @param Core.Event#EVENTDATA EventData The EventData structure.
--- Occurs when a player enters a slot and takes control of an aircraft.
-- **NOTE**: This is a workaround of a long standing DCS bug with the PLAYER_ENTER_UNIT event.
-- initiator : The unit that is being taken control of.
-- @function [parent=#BASE] OnEventPlayerEnterAircraft
-- @param #BASE self
-- @param Core.Event#EVENTDATA EventData The EventData structure.
end
@ -781,31 +819,48 @@ function BASE:CreateEventTakeoff( EventTime, Initiator )
world.onEvent( Event )
end
--- Creation of a `S_EVENT_PLAYER_ENTER_AIRCRAFT` event.
-- @param #BASE self
-- @param Wrapper.Unit#UNIT PlayerUnit The aircraft unit the player entered.
function BASE:CreateEventPlayerEnterAircraft( PlayerUnit )
self:F( { PlayerUnit } )
local Event = {
id = EVENTS.PlayerEnterAircraft,
time = timer.getTime(),
initiator = PlayerUnit:GetDCSObject()
}
world.onEvent(Event)
end
-- TODO: Complete DCS#Event structure.
--- The main event handling function... This function captures all events generated for the class.
-- @param #BASE self
-- @param DCS#Event event
function BASE:onEvent(event)
--self:F( { BaseEventCodes[event.id], event } )
if self then
for EventID, EventObject in pairs( self.Events ) do
for EventID, EventObject in pairs(self.Events) do
if EventObject.EventEnabled then
--env.info( 'onEvent Table EventObject.Self = ' .. tostring(EventObject.Self) )
--env.info( 'onEvent event.id = ' .. tostring(event.id) )
--env.info( 'onEvent EventObject.Event = ' .. tostring(EventObject.Event) )
if event.id == EventObject.Event then
if self == EventObject.Self then
if event.initiator and event.initiator:isExist() then
event.IniUnitName = event.initiator:getName()
end
if event.target and event.target:isExist() then
event.TgtUnitName = event.target:getName()
end
--self:T( { BaseEventCodes[event.id], event } )
--EventObject.EventFunction( self, event )
end
end
end
end
end

View File

@ -0,0 +1,442 @@
--- **Core** - TACAN and other beacons.
--
-- ===
--
-- ## Features:
--
-- * Provide beacon functionality to assist pilots.
--
-- ===
--
-- ### Authors: Hugues "Grey_Echo" Bousquet, funkyfranky
--
-- @module Core.Beacon
-- @image Core_Radio.JPG
--- *In order for the light to shine so brightly, the darkness must be present.* -- Francis Bacon
--
-- After attaching a @{#BEACON} to your @{Wrapper.Positionable#POSITIONABLE}, you need to select the right function to activate the kind of beacon you want.
-- There are two types of BEACONs available : the AA TACAN Beacon and the general purpose Radio Beacon.
-- Note that in both case, you can set an optional parameter : the `BeaconDuration`. This can be very usefull to simulate the battery time if your BEACON is
-- attach to a cargo crate, for exemple.
--
-- ## AA TACAN Beacon usage
--
-- This beacon only works with airborne @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}. Use @{#BEACON:AATACAN}() to set the beacon parameters and start the beacon.
-- Use @#BEACON:StopAATACAN}() to stop it.
--
-- ## General Purpose Radio Beacon usage
--
-- This beacon will work with any @{Wrapper.Positionable#POSITIONABLE}, but **it won't follow the @{Wrapper.Positionable#POSITIONABLE}** ! This means that you should only use it with
-- @{Wrapper.Positionable#POSITIONABLE} that don't move, or move very slowly. Use @{#BEACON:RadioBeacon}() to set the beacon parameters and start the beacon.
-- Use @{#BEACON:StopRadioBeacon}() to stop it.
--
-- @type BEACON
-- @field #string ClassName Name of the class "BEACON".
-- @field Wrapper.Controllable#CONTROLLABLE Positionable The @{#CONTROLLABLE} that will receive radio capabilities.
-- @extends Core.Base#BASE
BEACON = {
ClassName = "BEACON",
Positionable = nil,
name = nil,
}
--- Beacon types supported by DCS.
-- @type BEACON.Type
-- @field #number NULL
-- @field #number VOR
-- @field #number DME
-- @field #number VOR_DME
-- @field #number TACAN TACtical Air Navigation system.
-- @field #number VORTAC
-- @field #number RSBN
-- @field #number BROADCAST_STATION
-- @field #number HOMER
-- @field #number AIRPORT_HOMER
-- @field #number AIRPORT_HOMER_WITH_MARKER
-- @field #number ILS_FAR_HOMER
-- @field #number ILS_NEAR_HOMER
-- @field #number ILS_LOCALIZER
-- @field #number ILS_GLIDESLOPE
-- @field #number PRMG_LOCALIZER
-- @field #number PRMG_GLIDESLOPE
-- @field #number ICLS Same as ICLS glideslope.
-- @field #number ICLS_LOCALIZER
-- @field #number ICLS_GLIDESLOPE
-- @field #number NAUTICAL_HOMER
BEACON.Type={
NULL = 0,
VOR = 1,
DME = 2,
VOR_DME = 3,
TACAN = 4,
VORTAC = 5,
RSBN = 128,
BROADCAST_STATION = 1024,
HOMER = 8,
AIRPORT_HOMER = 4104,
AIRPORT_HOMER_WITH_MARKER = 4136,
ILS_FAR_HOMER = 16408,
ILS_NEAR_HOMER = 16424,
ILS_LOCALIZER = 16640,
ILS_GLIDESLOPE = 16896,
PRMG_LOCALIZER = 33024,
PRMG_GLIDESLOPE = 33280,
ICLS = 131584, --leaving this in here but it is the same as ICLS_GLIDESLOPE
ICLS_LOCALIZER = 131328,
ICLS_GLIDESLOPE = 131584,
NAUTICAL_HOMER = 65536,
}
--- Beacon systems supported by DCS. https://wiki.hoggitworld.com/view/DCS_command_activateBeacon
-- @type BEACON.System
-- @field #number PAR_10 ?
-- @field #number RSBN_5 Russian VOR/DME system.
-- @field #number TACAN TACtical Air Navigation system on ground.
-- @field #number TACAN_TANKER_X TACtical Air Navigation system for tankers on X band.
-- @field #number TACAN_TANKER_Y TACtical Air Navigation system for tankers on Y band.
-- @field #number VOR Very High Frequency Omni-Directional Range
-- @field #number ILS_LOCALIZER ILS localizer
-- @field #number ILS_GLIDESLOPE ILS glideslope.
-- @field #number PRGM_LOCALIZER PRGM localizer.
-- @field #number PRGM_GLIDESLOPE PRGM glideslope.
-- @field #number BROADCAST_STATION Broadcast station.
-- @field #number VORTAC Radio-based navigational aid for aircraft pilots consisting of a co-located VHF omnidirectional range (VOR) beacon and a tactical air navigation system (TACAN) beacon.
-- @field #number TACAN_AA_MODE_X TACtical Air Navigation for aircraft on X band.
-- @field #number TACAN_AA_MODE_Y TACtical Air Navigation for aircraft on Y band.
-- @field #number VORDME Radio beacon that combines a VHF omnidirectional range (VOR) with a distance measuring equipment (DME).
-- @field #number ICLS_LOCALIZER Carrier landing system.
-- @field #number ICLS_GLIDESLOPE Carrier landing system.
BEACON.System={
PAR_10 = 1,
RSBN_5 = 2,
TACAN = 3,
TACAN_TANKER_X = 4,
TACAN_TANKER_Y = 5,
VOR = 6,
ILS_LOCALIZER = 7,
ILS_GLIDESLOPE = 8,
PRMG_LOCALIZER = 9,
PRMG_GLIDESLOPE = 10,
BROADCAST_STATION = 11,
VORTAC = 12,
TACAN_AA_MODE_X = 13,
TACAN_AA_MODE_Y = 14,
VORDME = 15,
ICLS_LOCALIZER = 16,
ICLS_GLIDESLOPE = 17,
}
--- Create a new BEACON Object. This doesn't activate the beacon, though, use @{#BEACON.ActivateTACAN} etc.
-- If you want to create a BEACON, you probably should use @{Wrapper.Positionable#POSITIONABLE.GetBeacon}() instead.
-- @param #BEACON self
-- @param Wrapper.Positionable#POSITIONABLE Positionable The @{Positionable} that will receive radio capabilities.
-- @return #BEACON Beacon object or #nil if the positionable is invalid.
function BEACON:New(Positionable)
-- Inherit BASE.
local self=BASE:Inherit(self, BASE:New()) --#BEACON
-- Debug.
self:F(Positionable)
-- Set positionable.
if Positionable:GetPointVec2() then -- It's stupid, but the only way I found to make sure positionable is valid
self.Positionable = Positionable
self.name=Positionable:GetName()
self:I(string.format("New BEACON %s", tostring(self.name)))
return self
end
self:E({"The passed positionable is invalid, no BEACON created", Positionable})
return nil
end
--- Activates a TACAN BEACON.
-- @param #BEACON self
-- @param #number Channel TACAN channel, i.e. the "10" part in "10Y".
-- @param #string Mode TACAN mode, i.e. the "Y" part in "10Y".
-- @param #string Message The Message that is going to be coded in Morse and broadcasted by the beacon.
-- @param #boolean Bearing If true, beacon provides bearing information. If false (or nil), only distance information is available.
-- @param #number Duration How long will the beacon last in seconds. Omit for forever.
-- @return #BEACON self
-- @usage
-- -- Let's create a TACAN Beacon for a tanker
-- local myUnit = UNIT:FindByName("MyUnit")
-- local myBeacon = myUnit:GetBeacon() -- Creates the beacon
--
-- myBeacon:ActivateTACAN(20, "Y", "TEXACO", true) -- Activate the beacon
function BEACON:ActivateTACAN(Channel, Mode, Message, Bearing, Duration)
self:T({channel=Channel, mode=Mode, callsign=Message, bearing=Bearing, duration=Duration})
-- Get frequency.
local Frequency=UTILS.TACANToFrequency(Channel, Mode)
-- Check.
if not Frequency then
self:E({"The passed TACAN channel is invalid, the BEACON is not emitting"})
return self
end
-- Beacon type.
local Type=BEACON.Type.TACAN
-- Beacon system.
local System=BEACON.System.TACAN
-- Check if unit is an aircraft and set system accordingly.
local AA=self.Positionable:IsAir()
if AA then
System=5 --NOTE: 5 is how you cat the correct tanker behaviour! --BEACON.System.TACAN_TANKER
-- Check if "Y" mode is selected for aircraft.
if Mode~="Y" then
self:E({"WARNING: The POSITIONABLE you want to attach the AA Tacan Beacon is an aircraft: Mode should Y !The BEACON is not emitting.", self.Positionable})
end
end
-- Attached unit.
local UnitID=self.Positionable:GetID()
-- Debug.
self:I({string.format("BEACON Activating TACAN %s: Channel=%d%s, Morse=%s, Bearing=%s, Duration=%s!", tostring(self.name), Channel, Mode, Message, tostring(Bearing), tostring(Duration))})
-- Start beacon.
self.Positionable:CommandActivateBeacon(Type, System, Frequency, UnitID, Channel, Mode, AA, Message, Bearing)
-- Stop sheduler.
if Duration then
self.Positionable:DeactivateBeacon(Duration)
end
return self
end
--- Activates an ICLS BEACON. The unit the BEACON is attached to should be an aircraft carrier supporting this system.
-- @param #BEACON self
-- @param #number Channel ICLS channel.
-- @param #string Callsign The Message that is going to be coded in Morse and broadcasted by the beacon.
-- @param #number Duration How long will the beacon last in seconds. Omit for forever.
-- @return #BEACON self
function BEACON:ActivateICLS(Channel, Callsign, Duration)
self:F({Channel=Channel, Callsign=Callsign, Duration=Duration})
-- Attached unit.
local UnitID=self.Positionable:GetID()
-- Debug
self:T2({"ICLS BEACON started!"})
-- Start beacon.
self.Positionable:CommandActivateICLS(Channel, UnitID, Callsign)
-- Stop sheduler
if Duration then -- Schedule the stop of the BEACON if asked by the MD
self.Positionable:DeactivateBeacon(Duration)
end
return self
end
--- Activates a TACAN BEACON on an Aircraft.
-- @param #BEACON self
-- @param #number TACANChannel (the "10" part in "10Y"). Note that AA TACAN are only available on Y Channels
-- @param #string Message The Message that is going to be coded in Morse and broadcasted by the beacon
-- @param #boolean Bearing Can the BEACON be homed on ?
-- @param #number BeaconDuration How long will the beacon last in seconds. Omit for forever.
-- @return #BEACON self
-- @usage
-- -- Let's create a TACAN Beacon for a tanker
-- local myUnit = UNIT:FindByName("MyUnit")
-- local myBeacon = myUnit:GetBeacon() -- Creates the beacon
--
-- myBeacon:AATACAN(20, "TEXACO", true) -- Activate the beacon
function BEACON:AATACAN(TACANChannel, Message, Bearing, BeaconDuration)
self:F({TACANChannel, Message, Bearing, BeaconDuration})
local IsValid = true
if not self.Positionable:IsAir() then
self:E({"The POSITIONABLE you want to attach the AA Tacan Beacon is not an aircraft ! The BEACON is not emitting", self.Positionable})
IsValid = false
end
local Frequency = self:_TACANToFrequency(TACANChannel, "Y")
if not Frequency then
self:E({"The passed TACAN channel is invalid, the BEACON is not emitting"})
IsValid = false
end
-- I'm using the beacon type 4 (BEACON_TYPE_TACAN). For System, I'm using 5 (TACAN_TANKER_MODE_Y) if the bearing shows its bearing
-- or 14 (TACAN_AA_MODE_Y) if it does not
local System
if Bearing then
System = 5
else
System = 14
end
if IsValid then -- Starts the BEACON
self:T2({"AA TACAN BEACON started !"})
self.Positionable:SetCommand({
id = "ActivateBeacon",
params = {
type = 4,
system = System,
callsign = Message,
frequency = Frequency,
}
})
if BeaconDuration then -- Schedule the stop of the BEACON if asked by the MD
SCHEDULER:New(nil,
function()
self:StopAATACAN()
end, {}, BeaconDuration)
end
end
return self
end
--- Stops the AA TACAN BEACON
-- @param #BEACON self
-- @return #BEACON self
function BEACON:StopAATACAN()
self:F()
if not self.Positionable then
self:E({"Start the beacon first before stoping it !"})
else
self.Positionable:SetCommand({
id = 'DeactivateBeacon',
params = {
}
})
end
end
--- Activates a general pupose Radio Beacon
-- This uses the very generic singleton function "trigger.action.radioTransmission()" provided by DCS to broadcast a sound file on a specific frequency.
-- Although any frequency could be used, only 2 DCS Modules can home on radio beacons at the time of writing : the Huey and the Mi-8.
-- They can home in on these specific frequencies :
-- * **Mi8**
-- * R-828 -> 20-60MHz
-- * ARKUD -> 100-150MHz (canal 1 : 114166, canal 2 : 114333, canal 3 : 114583, canal 4 : 121500, canal 5 : 123100, canal 6 : 124100) AM
-- * ARK9 -> 150-1300KHz
-- * **Huey**
-- * AN/ARC-131 -> 30-76 Mhz FM
-- @param #BEACON self
-- @param #string FileName The name of the audio file
-- @param #number Frequency in MHz
-- @param #number Modulation either radio.modulation.AM or radio.modulation.FM
-- @param #number Power in W
-- @param #number BeaconDuration How long will the beacon last in seconds. Omit for forever.
-- @return #BEACON self
-- @usage
-- -- Let's create a beacon for a unit in distress.
-- -- Frequency will be 40MHz FM (home-able by a Huey's AN/ARC-131)
-- -- The beacon they use is battery-powered, and only lasts for 5 min
-- local UnitInDistress = UNIT:FindByName("Unit1")
-- local UnitBeacon = UnitInDistress:GetBeacon()
--
-- -- Set the beacon and start it
-- UnitBeacon:RadioBeacon("MySoundFileSOS.ogg", 40, radio.modulation.FM, 20, 5*60)
function BEACON:RadioBeacon(FileName, Frequency, Modulation, Power, BeaconDuration)
self:F({FileName, Frequency, Modulation, Power, BeaconDuration})
local IsValid = false
-- Check the filename
if type(FileName) == "string" then
if FileName:find(".ogg") or FileName:find(".wav") then
if not FileName:find("l10n/DEFAULT/") then
FileName = "l10n/DEFAULT/" .. FileName
end
IsValid = true
end
end
if not IsValid then
self:E({"File name invalid. Maybe something wrong with the extension ? ", FileName})
end
-- Check the Frequency
if type(Frequency) ~= "number" and IsValid then
self:E({"Frequency invalid. ", Frequency})
IsValid = false
end
Frequency = Frequency * 1000000 -- Conversion to Hz
-- Check the modulation
if Modulation ~= radio.modulation.AM and Modulation ~= radio.modulation.FM and IsValid then --TODO Maybe make this future proof if ED decides to add an other modulation ?
self:E({"Modulation is invalid. Use DCS's enum radio.modulation.", Modulation})
IsValid = false
end
-- Check the Power
if type(Power) ~= "number" and IsValid then
self:E({"Power is invalid. ", Power})
IsValid = false
end
Power = math.floor(math.abs(Power)) --TODO Find what is the maximum power allowed by DCS and limit power to that
if IsValid then
self:T2({"Activating Beacon on ", Frequency, Modulation})
-- Note that this is looped. I have to give this transmission a unique name, I use the class ID
trigger.action.radioTransmission(FileName, self.Positionable:GetPositionVec3(), Modulation, true, Frequency, Power, tostring(self.ID))
if BeaconDuration then -- Schedule the stop of the BEACON if asked by the MD
SCHEDULER:New( nil,
function()
self:StopRadioBeacon()
end, {}, BeaconDuration)
end
end
end
--- Stops the AA TACAN BEACON
-- @param #BEACON self
-- @return #BEACON self
function BEACON:StopRadioBeacon()
self:F()
-- The unique name of the transmission is the class ID
trigger.action.stopRadioTransmission(tostring(self.ID))
return self
end
--- Converts a TACAN Channel/Mode couple into a frequency in Hz
-- @param #BEACON self
-- @param #number TACANChannel
-- @param #string TACANMode
-- @return #number Frequecy
-- @return #nil if parameters are invalid
function BEACON:_TACANToFrequency(TACANChannel, TACANMode)
self:F3({TACANChannel, TACANMode})
if type(TACANChannel) ~= "number" then
if TACANMode ~= "X" and TACANMode ~= "Y" then
return nil -- error in arguments
end
end
-- This code is largely based on ED's code, in DCS World\Scripts\World\Radio\BeaconTypes.lua, line 137.
-- I have no idea what it does but it seems to work
local A = 1151 -- 'X', channel >= 64
local B = 64 -- channel >= 64
if TACANChannel < 64 then
B = 1
end
if TACANMode == 'Y' then
A = 1025
if TACANChannel < 64 then
A = 1088
end
else -- 'X'
if TACANChannel < 64 then
A = 962
end
end
return (A + TACANChannel - B) * 1000000
end

View File

@ -24,7 +24,7 @@
-- ===
--
-- ### Author: **FlightControl**
-- ### Contributions:
-- ### Contributions: **funkyfranky**
--
-- ===
--
@ -33,6 +33,9 @@
--- @type DATABASE
-- @field #string ClassName Name of the class.
-- @field #table Templates Templates: Units, Groups, Statics, ClientsByName, ClientsByID.
-- @field #table CLIENTS Clients.
-- @extends Core.Base#BASE
--- Contains collections of wrapper objects defined within MOOSE that reflect objects within the simulator.
@ -126,8 +129,6 @@ function DATABASE:New()
self:HandleEvent( EVENTS.DeleteCargo )
self:HandleEvent( EVENTS.NewZone )
self:HandleEvent( EVENTS.DeleteZone )
-- Follow alive players and clients
--self:HandleEvent( EVENTS.PlayerEnterUnit, self._EventOnPlayerEnterUnit ) -- This is not working anymore!, handling this through the birth event.
self:HandleEvent( EVENTS.PlayerLeaveUnit, self._EventOnPlayerLeaveUnit )
@ -140,37 +141,6 @@ function DATABASE:New()
self.UNITS_Position = 0
--- @param #DATABASE self
local function CheckPlayers( self )
local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ), AlivePlayersNeutral = coalition.getPlayers( coalition.side.NEUTRAL )}
for CoalitionId, CoalitionData in pairs( CoalitionsData ) do
--self:E( { "CoalitionData:", CoalitionData } )
for UnitId, UnitData in pairs( CoalitionData ) do
if UnitData and UnitData:isExist() then
local UnitName = UnitData:getName()
local PlayerName = UnitData:getPlayerName()
local PlayerUnit = UNIT:Find( UnitData )
--self:T( { "UnitData:", UnitData, UnitName, PlayerName, PlayerUnit } )
if PlayerName and PlayerName ~= "" then
if self.PLAYERS[PlayerName] == nil or self.PLAYERS[PlayerName] ~= UnitName then
--self:E( { "Add player for unit:", UnitName, PlayerName } )
self:AddPlayer( UnitName, PlayerName )
--_EVENTDISPATCHER:CreateEventPlayerEnterUnit( PlayerUnit )
local Settings = SETTINGS:Set( PlayerName )
Settings:SetPlayerMenu( PlayerUnit )
end
end
end
end
end
end
--self:E( "Scheduling" )
--PlayerCheckSchedule = SCHEDULER:New( nil, CheckPlayers, { self }, 1, 1 )
return self
end
@ -187,14 +157,22 @@ end
--- Adds a Unit based on the Unit Name in the DATABASE.
-- @param #DATABASE self
-- @param #string DCSUnitName Unit name.
-- @return Wrapper.Unit#UNIT The added unit.
function DATABASE:AddUnit( DCSUnitName )
if not self.UNITS[DCSUnitName] then
self:T( { "Add UNIT:", DCSUnitName } )
local UnitRegister = UNIT:Register( DCSUnitName )
self.UNITS[DCSUnitName] = UNIT:Register( DCSUnitName )
if not self.UNITS[DCSUnitName] then
table.insert( self.UNITS_Index, DCSUnitName )
-- Debug info.
self:T( { "Add UNIT:", DCSUnitName } )
--local UnitRegister = UNIT:Register( DCSUnitName )
-- Register unit
self.UNITS[DCSUnitName]=UNIT:Register(DCSUnitName)
-- This is not used anywhere in MOOSE as far as I can see so I remove it until there comes an error somewhere.
--table.insert(self.UNITS_Index, DCSUnitName )
end
return self.UNITS[DCSUnitName]
@ -210,6 +188,8 @@ end
--- Adds a Static based on the Static Name in the DATABASE.
-- @param #DATABASE self
-- @param #string DCSStaticName Name of the static.
-- @return Wrapper.Static#STATIC The static object.
function DATABASE:AddStatic( DCSStaticName )
if not self.STATICS[DCSStaticName] then
@ -224,8 +204,7 @@ end
--- Deletes a Static from the DATABASE based on the Static Name.
-- @param #DATABASE self
function DATABASE:DeleteStatic( DCSStaticName )
--self.STATICS[DCSStaticName] = nil
self.STATICS[DCSStaticName] = nil
end
--- Finds a STATIC based on the StaticName.
@ -319,24 +298,78 @@ do -- Zones
-- @return #DATABASE self
function DATABASE:_RegisterZones()
for ZoneID, ZoneData in pairs( env.mission.triggers.zones ) do
for ZoneID, ZoneData in pairs(env.mission.triggers.zones) do
local ZoneName = ZoneData.name
-- Color
local color=ZoneData.color or {1, 0, 0, 0.15}
-- Create new Zone
local Zone=nil --Core.Zone#ZONE_BASE
if ZoneData.type==0 then
---
-- Circular zone
---
self:I(string.format("Register ZONE: %s (Circular)", ZoneName))
Zone=ZONE:New(ZoneName)
else
self:I( { "Register ZONE:", Name = ZoneName } )
local Zone = ZONE:New( ZoneName )
self.ZONENAMES[ZoneName] = ZoneName
self:AddZone( ZoneName, Zone )
---
-- Quad-point zone
---
self:I(string.format("Register ZONE: %s (Polygon, Quad)", ZoneName))
Zone=ZONE_POLYGON_BASE:New(ZoneName, ZoneData.verticies)
--for i,vec2 in pairs(ZoneData.verticies) do
-- local coord=COORDINATE:NewFromVec2(vec2)
-- coord:MarkToAll(string.format("%s Point %d", ZoneName, i))
--end
end
if Zone then
-- Store color of zone.
Zone.Color=color
-- Store in DB.
self.ZONENAMES[ZoneName] = ZoneName
-- Add zone.
self:AddZone(ZoneName, Zone)
end
end
-- Polygon zones defined by late activated groups.
for ZoneGroupName, ZoneGroup in pairs( self.GROUPS ) do
if ZoneGroupName:match("#ZONE_POLYGON") then
local ZoneName1 = ZoneGroupName:match("(.*)#ZONE_POLYGON")
local ZoneName2 = ZoneGroupName:match(".*#ZONE_POLYGON(.*)")
local ZoneName = ZoneName1 .. ( ZoneName2 or "" )
self:I( { "Register ZONE_POLYGON:", Name = ZoneName } )
-- Debug output
self:I(string.format("Register ZONE: %s (Polygon)", ZoneName))
-- Create a new polygon zone.
local Zone_Polygon = ZONE_POLYGON:New( ZoneName, ZoneGroup )
-- Set color.
Zone_Polygon:SetColor({1, 0, 0}, 0.15)
-- Store name in DB.
self.ZONENAMES[ZoneName] = ZoneName
-- Add zone to DB.
self:AddZone( ZoneName, Zone_Polygon )
end
end
@ -432,7 +465,6 @@ do -- cargo
local Groups = UTILS.DeepCopy( self.GROUPS ) -- This is a very important statement. CARGO_GROUP:New creates a new _DATABASE.GROUP entry, which will confuse the loop. I searched 4 hours on this to find the bug!
for CargoGroupName, CargoGroup in pairs( Groups ) do
self:I( { Cargo = CargoGroupName } )
if self:IsCargo( CargoGroupName ) then
local CargoInfo = CargoGroupName:match("#CARGO(.*)")
local CargoParam = CargoInfo and CargoInfo:match( "%((.*)%)")
@ -489,6 +521,8 @@ end
--- Adds a CLIENT based on the ClientName in the DATABASE.
-- @param #DATABASE self
-- @param #string ClientName Name of the Client unit.
-- @return Wrapper.Client#CLIENT The client object.
function DATABASE:AddClient( ClientName )
if not self.CLIENTS[ClientName] then
@ -626,6 +660,9 @@ function DATABASE:Spawn( SpawnTemplate )
end
--- Set a status to a Group within the Database, this to check crossing events for example.
-- @param #DATABASE self
-- @param #string GroupName Group name.
-- @param #string Status Status.
function DATABASE:SetStatusGroup( GroupName, Status )
self:F2( Status )
@ -633,8 +670,11 @@ function DATABASE:SetStatusGroup( GroupName, Status )
end
--- Get a status to a Group within the Database, this to check crossing events for example.
-- @param #DATABASE self
-- @param #string GroupName Group name.
-- @return #string Status or an empty string "".
function DATABASE:GetStatusGroup( GroupName )
self:F2( Status )
self:F2( GroupName )
if self.Templates.Groups[GroupName] then
return self.Templates.Groups[GroupName].Status
@ -648,7 +688,8 @@ end
-- @param #table GroupTemplate
-- @param DCS#coalition.side CoalitionSide The coalition.side of the object.
-- @param DCS#Object.Category CategoryID The Object.category of the object.
-- @param DCS#country.id CountryID the country.id of the object
-- @param DCS#country.id CountryID the country ID of the object.
-- @param #string GroupName (Optional) The name of the group. Default is `GroupTemplate.name`.
-- @return #DATABASE self
function DATABASE:_RegisterGroupTemplate( GroupTemplate, CoalitionSide, CategoryID, CountryID, GroupName )
@ -704,6 +745,7 @@ function DATABASE:_RegisterGroupTemplate( GroupTemplate, CoalitionSide, Category
UnitNames[#UnitNames+1] = self.Templates.Units[UnitTemplate.name].UnitName
end
-- Debug info.
self:T( { Group = self.Templates.Groups[GroupTemplateName].GroupName,
Coalition = self.Templates.Groups[GroupTemplateName].CoalitionID,
Category = self.Templates.Groups[GroupTemplateName].CategoryID,
@ -713,6 +755,10 @@ function DATABASE:_RegisterGroupTemplate( GroupTemplate, CoalitionSide, Category
)
end
--- Get group template.
-- @param #DATABASE self
-- @param #string GroupName Group name.
-- @return #table Group template table.
function DATABASE:GetGroupTemplate( GroupName )
local GroupTemplate = self.Templates.Groups[GroupName].Template
GroupTemplate.SpawnCoalitionID = self.Templates.Groups[GroupName].CoalitionID
@ -723,7 +769,10 @@ end
--- Private method that registers new Static Templates within the DATABASE Object.
-- @param #DATABASE self
-- @param #table StaticTemplate
-- @param #table StaticTemplate Template table.
-- @param #number CoalitionID Coalition ID.
-- @param #number CategoryID Category ID.
-- @param #number CountryID Country ID.
-- @return #DATABASE self
function DATABASE:_RegisterStaticTemplate( StaticTemplate, CoalitionID, CategoryID, CountryID )
@ -744,7 +793,8 @@ function DATABASE:_RegisterStaticTemplate( StaticTemplate, CoalitionID, Category
self.Templates.Statics[StaticTemplateName].CoalitionID = CoalitionID
self.Templates.Statics[StaticTemplateName].CountryID = CountryID
self:I( { Static = self.Templates.Statics[StaticTemplateName].StaticName,
-- Debug info.
self:T( { Static = self.Templates.Statics[StaticTemplateName].StaticName,
Coalition = self.Templates.Statics[StaticTemplateName].CoalitionID,
Category = self.Templates.Statics[StaticTemplateName].CategoryID,
Country = self.Templates.Statics[StaticTemplateName].CountryID
@ -753,48 +803,101 @@ function DATABASE:_RegisterStaticTemplate( StaticTemplate, CoalitionID, Category
self:AddStatic( StaticTemplateName )
return self
end
--- @param #DATABASE self
--- Get static group template.
-- @param #DATABASE self
-- @param #string StaticName Name of the static.
-- @return #table Static template table.
function DATABASE:GetStaticGroupTemplate( StaticName )
local StaticTemplate = self.Templates.Statics[StaticName].GroupTemplate
return StaticTemplate, self.Templates.Statics[StaticName].CoalitionID, self.Templates.Statics[StaticName].CategoryID, self.Templates.Statics[StaticName].CountryID
if self.Templates.Statics[StaticName] then
local StaticTemplate = self.Templates.Statics[StaticName].GroupTemplate
return StaticTemplate, self.Templates.Statics[StaticName].CoalitionID, self.Templates.Statics[StaticName].CategoryID, self.Templates.Statics[StaticName].CountryID
else
self:E("ERROR: Static group template does NOT exist for static "..tostring(StaticName))
return nil
end
end
--- @param #DATABASE self
--- Get static unit template.
-- @param #DATABASE self
-- @param #string StaticName Name of the static.
-- @return #table Static template table.
function DATABASE:GetStaticUnitTemplate( StaticName )
local UnitTemplate = self.Templates.Statics[StaticName].UnitTemplate
return UnitTemplate, self.Templates.Statics[StaticName].CoalitionID, self.Templates.Statics[StaticName].CategoryID, self.Templates.Statics[StaticName].CountryID
if self.Templates.Statics[StaticName] then
local UnitTemplate = self.Templates.Statics[StaticName].UnitTemplate
return UnitTemplate, self.Templates.Statics[StaticName].CoalitionID, self.Templates.Statics[StaticName].CategoryID, self.Templates.Statics[StaticName].CountryID
else
self:E("ERROR: Static unit template does NOT exist for static "..tostring(StaticName))
return nil
end
end
--- Get group name from unit name.
-- @param #DATABASE self
-- @param #string UnitName Name of the unit.
-- @return #string Group name.
function DATABASE:GetGroupNameFromUnitName( UnitName )
return self.Templates.Units[UnitName].GroupName
if self.Templates.Units[UnitName] then
return self.Templates.Units[UnitName].GroupName
else
self:E("ERROR: Unit template does not exist for unit "..tostring(UnitName))
return nil
end
end
--- Get group template from unit name.
-- @param #DATABASE self
-- @param #string UnitName Name of the unit.
-- @return #table Group template.
function DATABASE:GetGroupTemplateFromUnitName( UnitName )
return self.Templates.Units[UnitName].GroupTemplate
if self.Templates.Units[UnitName] then
return self.Templates.Units[UnitName].GroupTemplate
else
self:E("ERROR: Unit template does not exist for unit "..tostring(UnitName))
return nil
end
end
--- Get coalition ID from client name.
-- @param #DATABASE self
-- @param #string ClientName Name of the Client.
-- @return #number Coalition ID.
function DATABASE:GetCoalitionFromClientTemplate( ClientName )
return self.Templates.ClientsByName[ClientName].CoalitionID
end
--- Get category ID from client name.
-- @param #DATABASE self
-- @param #string ClientName Name of the Client.
-- @return #number Category ID.
function DATABASE:GetCategoryFromClientTemplate( ClientName )
return self.Templates.ClientsByName[ClientName].CategoryID
end
--- Get country ID from client name.
-- @param #DATABASE self
-- @param #string ClientName Name of the Client.
-- @return #number Country ID.
function DATABASE:GetCountryFromClientTemplate( ClientName )
return self.Templates.ClientsByName[ClientName].CountryID
end
--- Airbase
--- Get coalition ID from airbase name.
-- @param #DATABASE self
-- @param #string AirbaseName Name of the airbase.
-- @return #number Coalition ID.
function DATABASE:GetCoalitionFromAirbase( AirbaseName )
return self.AIRBASES[AirbaseName]:GetCoalition()
end
--- Get category from airbase name.
-- @param #DATABASE self
-- @param #string AirbaseName Name of the airbase.
-- @return #number Category.
function DATABASE:GetCategoryFromAirbase( AirbaseName )
return self.AIRBASES[AirbaseName]:GetCategory()
end
@ -831,33 +934,38 @@ end
function DATABASE:_RegisterGroupsAndUnits()
local CoalitionsData = { GroupsRed = coalition.getGroups( coalition.side.RED ), GroupsBlue = coalition.getGroups( coalition.side.BLUE ), GroupsNeutral = coalition.getGroups( coalition.side.NEUTRAL ) }
for CoalitionId, CoalitionData in pairs( CoalitionsData ) do
for DCSGroupId, DCSGroup in pairs( CoalitionData ) do
if DCSGroup:isExist() then
-- Group name.
local DCSGroupName = DCSGroup:getName()
self:I( { "Register Group:", DCSGroupName } )
-- Add group.
self:I(string.format("Register Group: %s", tostring(DCSGroupName)))
self:AddGroup( DCSGroupName )
-- Loop over units in group.
for DCSUnitId, DCSUnit in pairs( DCSGroup:getUnits() ) do
-- Get unit name.
local DCSUnitName = DCSUnit:getName()
self:I( { "Register Unit:", DCSUnitName } )
-- Add unit.
self:I(string.format("Register Unit: %s", tostring(DCSUnitName)))
self:AddUnit( DCSUnitName )
end
else
self:E( { "Group does not exist: ", DCSGroup } )
self:E({"Group does not exist: ", DCSGroup})
end
end
end
self:T("Groups:")
for GroupName, Group in pairs( self.GROUPS ) do
self:T( { "Group:", GroupName } )
end
return self
end
@ -867,7 +975,7 @@ end
function DATABASE:_RegisterClients()
for ClientName, ClientTemplate in pairs( self.Templates.ClientsByName ) do
self:T( { "Register Client:", ClientName } )
self:I(string.format("Register Client: %s", tostring(ClientName)))
self:AddClient( ClientName )
end
@ -877,15 +985,15 @@ end
--- @param #DATABASE self
function DATABASE:_RegisterStatics()
local CoalitionsData = { GroupsRed = coalition.getStaticObjects( coalition.side.RED ), GroupsBlue = coalition.getStaticObjects( coalition.side.BLUE ) }
self:I( { Statics = CoalitionsData } )
local CoalitionsData={GroupsRed=coalition.getStaticObjects(coalition.side.RED), GroupsBlue=coalition.getStaticObjects(coalition.side.BLUE), GroupsNeutral=coalition.getStaticObjects(coalition.side.NEUTRAL)}
for CoalitionId, CoalitionData in pairs( CoalitionsData ) do
for DCSStaticId, DCSStatic in pairs( CoalitionData ) do
if DCSStatic:isExist() then
local DCSStaticName = DCSStatic:getName()
self:T( { "Register Static:", DCSStaticName } )
self:I(string.format("Register Static: %s", tostring(DCSStaticName)))
self:AddStatic( DCSStaticName )
else
self:E( { "Static does not exist: ", DCSStatic } )
@ -912,8 +1020,23 @@ function DATABASE:_RegisterAirbases()
-- Add and register airbase.
local airbase=self:AddAirbase( DCSAirbaseName )
-- Unique ID.
local airbaseUID=airbase:GetID(true)
-- Debug output.
self:I(string.format("Register Airbase: %s, getID=%d, GetID=%d (unique=%d)", DCSAirbaseName, DCSAirbase:getID(), airbase:GetID(), airbase:GetID(true)))
local text=string.format("Register %s: %s (ID=%d UID=%d), parking=%d [", AIRBASE.CategoryName[airbase.category], tostring(DCSAirbaseName), airbaseID, airbaseUID, airbase.NparkingTotal)
for _,terminalType in pairs(AIRBASE.TerminalType) do
if airbase.NparkingTerminal and airbase.NparkingTerminal[terminalType] then
text=text..string.format("%d=%d ", terminalType, airbase.NparkingTerminal[terminalType])
end
end
text=text.."]"
self:I(text)
-- Check for DCS bug IDs.
if airbaseID~=airbase:GetID() then
--self:E("WARNING: :getID does NOT match :GetID!")
end
end
@ -930,36 +1053,74 @@ function DATABASE:_EventOnBirth( Event )
self:F( { Event } )
if Event.IniDCSUnit then
if Event.IniObjectCategory == 3 then
self:AddStatic( Event.IniDCSUnitName )
else
if Event.IniObjectCategory == 1 then
self:AddUnit( Event.IniDCSUnitName )
self:AddGroup( Event.IniDCSGroupName )
-- Add airbase if it was spawned later in the mission.
local DCSAirbase = Airbase.getByName(Event.IniDCSUnitName)
if DCSAirbase then
self:I(string.format("Adding airbase %s", tostring(Event.IniDCSUnitName)))
self:AddAirbase(Event.IniDCSUnitName)
end
end
end
if Event.IniObjectCategory == 1 then
Event.IniUnit = self:FindUnit( Event.IniDCSUnitName )
Event.IniGroup = self:FindGroup( Event.IniDCSGroupName )
-- Client
local client=self.CLIENTS[Event.IniDCSUnitName] --Wrapper.Client#CLIENT
if client then
-- TODO: create event ClientAlive
end
-- Get player name.
local PlayerName = Event.IniUnit:GetPlayerName()
if PlayerName then
self:I( { "Player Joined:", PlayerName } )
self:AddClient( Event.IniDCSUnitName )
-- Debug info.
self:I(string.format("Player '%s' joint unit '%s' of group '%s'", tostring(PlayerName), tostring(Event.IniDCSUnitName), tostring(Event.IniDCSGroupName)))
-- Add client in case it does not exist already.
if not client then
client=self:AddClient(Event.IniDCSUnitName)
end
-- Add player.
client:AddPlayer(PlayerName)
-- Add player.
if not self.PLAYERS[PlayerName] then
self:AddPlayer( Event.IniUnitName, PlayerName )
end
-- Player settings.
local Settings = SETTINGS:Set( PlayerName )
Settings:SetPlayerMenu( Event.IniUnit )
--MENU_INDEX:Refresh( Event.IniGroup )
Settings:SetPlayerMenu(Event.IniUnit)
-- Create an event.
self:CreateEventPlayerEnterAircraft(Event.IniUnit)
end
end
end
end
@ -967,22 +1128,53 @@ end
-- @param #DATABASE self
-- @param Core.Event#EVENTDATA Event
function DATABASE:_EventOnDeadOrCrash( Event )
self:F2( { Event } )
if Event.IniDCSUnit then
local name=Event.IniDCSUnitName
if Event.IniObjectCategory == 3 then
---
-- STATICS
---
if self.STATICS[Event.IniDCSUnitName] then
self:DeleteStatic( Event.IniDCSUnitName )
end
else
if Event.IniObjectCategory == 1 then
---
-- UNITS
---
-- Delete unit.
if self.UNITS[Event.IniDCSUnitName] then
self:DeleteUnit( Event.IniDCSUnitName )
self:DeleteUnit(Event.IniDCSUnitName)
end
-- Remove client players.
local client=self.CLIENTS[name] --Wrapper.Client#CLIENT
if client then
client:RemovePlayers()
end
end
end
-- Add airbase if it was spawned later in the mission.
local airbase=self.AIRBASES[Event.IniDCSUnitName] --Wrapper.Airbase#AIRBASE
if airbase and (airbase:IsHelipad() or airbase:IsShip()) then
self:DeleteAirbase(Event.IniDCSUnitName)
end
end
-- Account destroys.
self:AccountDestroys( Event )
end
@ -995,15 +1187,31 @@ function DATABASE:_EventOnPlayerEnterUnit( Event )
if Event.IniDCSUnit then
if Event.IniObjectCategory == 1 then
-- Add unit.
self:AddUnit( Event.IniDCSUnitName )
-- Ini unit.
Event.IniUnit = self:FindUnit( Event.IniDCSUnitName )
-- Add group.
self:AddGroup( Event.IniDCSGroupName )
-- Get player unit.
local PlayerName = Event.IniDCSUnit:getPlayerName()
if not self.PLAYERS[PlayerName] then
self:AddPlayer( Event.IniDCSUnitName, PlayerName )
if PlayerName then
if not self.PLAYERS[PlayerName] then
self:AddPlayer( Event.IniDCSUnitName, PlayerName )
end
local Settings = SETTINGS:Set( PlayerName )
Settings:SetPlayerMenu( Event.IniUnit )
else
self:E("ERROR: getPlayerName() returned nil for event PlayerEnterUnit")
end
local Settings = SETTINGS:Set( PlayerName )
Settings:SetPlayerMenu( Event.IniUnit )
end
end
end
@ -1016,13 +1224,30 @@ function DATABASE:_EventOnPlayerLeaveUnit( Event )
self:F2( { Event } )
if Event.IniUnit then
if Event.IniObjectCategory == 1 then
-- Try to get the player name. This can be buggy for multicrew aircraft!
local PlayerName = Event.IniUnit:GetPlayerName()
if PlayerName and self.PLAYERS[PlayerName] then
self:I( { "Player Left:", PlayerName } )
if PlayerName then --and self.PLAYERS[PlayerName] then
-- Debug info.
self:I(string.format("Player '%s' left unit %s", tostring(PlayerName), tostring(Event.IniUnitName)))
-- Remove player menu.
local Settings = SETTINGS:Set( PlayerName )
Settings:RemovePlayerMenu( Event.IniUnit )
self:DeletePlayer( Event.IniUnit, PlayerName )
Settings:RemovePlayerMenu(Event.IniUnit)
-- Delete player.
self:DeletePlayer(Event.IniUnit, PlayerName)
-- Client stuff.
local client=self.CLIENTS[Event.IniDCSUnitName] --Wrapper.Client#CLIENT
if client then
client:RemovePlayer(PlayerName)
end
end
end
end
@ -1249,19 +1474,19 @@ function DATABASE:SetPlayerSettings( PlayerName, Settings )
self.PLAYERSETTINGS[PlayerName] = Settings
end
--- Add a flight group to the data base.
--- Add an OPS group (FLIGHTGROUP, ARMYGROUP, NAVYGROUP) to the data base.
-- @param #DATABASE self
-- @param Ops.FlightGroup#FLIGHTGROUP flightgroup
function DATABASE:AddFlightGroup(flightgroup)
self:T({NewFlightGroup=flightgroup.groupname})
self.FLIGHTGROUPS[flightgroup.groupname]=flightgroup
-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group added to the DB.
function DATABASE:AddOpsGroup(opsgroup)
--env.info("Adding OPSGROUP "..tostring(opsgroup.groupname))
self.FLIGHTGROUPS[opsgroup.groupname]=opsgroup
end
--- Get a flight group from the data base.
--- Get an OPS group (FLIGHTGROUP, ARMYGROUP, NAVYGROUP) from the data base.
-- @param #DATABASE self
-- @param #string groupname Group name of the flight group. Can also be passed as GROUP object.
-- @return Ops.FlightGroup#FLIGHTGROUP Flight group object.
function DATABASE:GetFlightGroup(groupname)
-- @param #string groupname Group name of the group. Can also be passed as GROUP object.
-- @return Ops.OpsGroup#OPSGROUP OPS group object.
function DATABASE:GetOpsGroup(groupname)
-- Get group and group name.
if type(groupname)=="string" then
@ -1269,6 +1494,23 @@ function DATABASE:GetFlightGroup(groupname)
groupname=groupname:GetName()
end
--env.info("Getting OPSGROUP "..tostring(groupname))
return self.FLIGHTGROUPS[groupname]
end
--- Find an OPSGROUP (FLIGHTGROUP, ARMYGROUP, NAVYGROUP) in the data base.
-- @param #DATABASE self
-- @param #string groupname Group name of the group. Can also be passed as GROUP object.
-- @return Ops.OpsGroup#OPSGROUP OPS group object.
function DATABASE:FindOpsGroup(groupname)
-- Get group and group name.
if type(groupname)=="string" then
else
groupname=groupname:GetName()
end
--env.info("Getting OPSGROUP "..tostring(groupname))
return self.FLIGHTGROUPS[groupname]
end
@ -1355,19 +1597,13 @@ function DATABASE:_RegisterTemplates()
for group_num, Template in pairs(obj_type_data.group) do
if obj_type_name ~= "static" and Template and Template.units and type(Template.units) == 'table' then --making sure again- this is a valid group
self:_RegisterGroupTemplate(
Template,
CoalitionSide,
_DATABASECategory[string.lower(CategoryName)],
CountryID
)
self:_RegisterGroupTemplate(Template, CoalitionSide, _DATABASECategory[string.lower(CategoryName)], CountryID)
else
self:_RegisterStaticTemplate(
Template,
CoalitionSide,
_DATABASECategory[string.lower(CategoryName)],
CountryID
)
self:_RegisterStaticTemplate(Template, CoalitionSide, _DATABASECategory[string.lower(CategoryName)], CountryID)
end --if GroupTemplate and GroupTemplate.units then
end --for group_num, GroupTemplate in pairs(obj_type_data.group) do
end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then

File diff suppressed because it is too large Load Diff

View File

@ -410,7 +410,7 @@ do -- FSM
Transition.To = To
-- Debug message.
self:T2( Transition )
self:T3( Transition )
self._Transitions[Transition] = Transition
self:_eventmap( self.Events, Transition )
@ -432,7 +432,7 @@ do -- FSM
-- @param #table ReturnEvents A table indicating for which returned events of the SubFSM which Event must be triggered in the FSM.
-- @return Core.Fsm#FSM_PROCESS The SubFSM.
function FSM:AddProcess( From, Event, Process, ReturnEvents )
self:T( { From, Event } )
self:T3( { From, Event } )
local Sub = {}
Sub.From = From
@ -533,14 +533,14 @@ do -- FSM
Process._Scores[State].ScoreText = ScoreText
Process._Scores[State].Score = Score
self:T( Process._Scores )
self:T3( Process._Scores )
return Process
end
--- Returns a table with the scores defined.
-- @param #FSM self
-- @param #table Scores.
-- @return #table Scores.
function FSM:GetScores()
return self._Scores or {}
end
@ -576,7 +576,7 @@ do -- FSM
self[__Event] = self[__Event] or self:_delayed_transition(Event)
-- Debug message.
self:T2( "Added methods: " .. Event .. ", " .. __Event )
self:T3( "Added methods: " .. Event .. ", " .. __Event )
Events[Event] = self.Events[Event] or { map = {} }
self:_add_to_map( Events[Event].map, EventStructure )
@ -825,7 +825,7 @@ do -- FSM
end
-- Debug.
self:T2( { CallID = CallID } )
self:T3( { CallID = CallID } )
end
end
@ -846,7 +846,7 @@ do -- FSM
function FSM:_gosub( ParentFrom, ParentEvent )
local fsmtable = {}
if self.subs[ParentFrom] and self.subs[ParentFrom][ParentEvent] then
self:T( { ParentFrom, ParentEvent, self.subs[ParentFrom], self.subs[ParentFrom][ParentEvent] } )
self:T3( { ParentFrom, ParentEvent, self.subs[ParentFrom], self.subs[ParentFrom][ParentEvent] } )
return self.subs[ParentFrom][ParentEvent]
else
return {}
@ -1150,7 +1150,7 @@ do -- FSM_PROCESS
-- @param #FSM_PROCESS self
-- @return #FSM_PROCESS
function FSM_PROCESS:Copy( Controllable, Task )
self:T( { self:GetClassNameAndID() } )
self:T3( { self:GetClassNameAndID() } )
local NewFsm = self:New( Controllable, Task ) -- Core.Fsm#FSM_PROCESS
@ -1176,13 +1176,13 @@ do -- FSM_PROCESS
-- Copy End States
for EndStateID, EndState in pairs( self:GetEndStates() ) do
self:T( EndState )
self:T3( EndState )
NewFsm:AddEndState( EndState )
end
-- Copy the score tables
for ScoreID, Score in pairs( self:GetScores() ) do
self:T( Score )
self:T3( Score )
NewFsm:AddScore( ScoreID, Score.ScoreText, Score.Score )
end

View File

@ -0,0 +1,251 @@
--- **Core** - MarkerOps_Base.
--
-- **Main Features:**
--
-- * Create an easy way to tap into markers added to the F10 map by users.
-- * Recognize own tag and list of keywords.
-- * Matched keywords are handed down to functions.
--
-- ===
--
-- ### Author: **Applevangelist**
--
-- Date: 5 May 2021
--
-- ===
---
-- @module Core.MarkerOps_Base
-- @image MOOSE_Core.JPG
--------------------------------------------------------------------------
-- MARKEROPS_BASE Class Definition.
--------------------------------------------------------------------------
--- MARKEROPS_BASE class.
-- @type MARKEROPS_BASE
-- @field #string ClassName Name of the class.
-- @field #string Tag Tag to identify commands.
-- @field #table Keywords Table of keywords to recognize.
-- @field #string version Version of #MARKEROPS_BASE.
-- @extends Core.Fsm#FSM
--- *Fiat lux.* -- Latin proverb.
--
-- ===
--
-- # The MARKEROPS_BASE Concept
--
-- This class enable scripting text-based actions from markers.
--
-- @field #MARKEROPS_BASE
MARKEROPS_BASE = {
ClassName = "MARKEROPS",
Tag = "mytag",
Keywords = {},
version = "0.0.1",
debug = false,
}
--- Function to instantiate a new #MARKEROPS_BASE object.
-- @param #MARKEROPS_BASE self
-- @param #string Tagname Name to identify us from the event text.
-- @param #table Keywords Table of keywords recognized from the event text.
-- @return #MARKEROPS_BASE self
function MARKEROPS_BASE:New(Tagname,Keywords)
-- Inherit FSM
local self=BASE:Inherit(self, FSM:New()) -- #MARKEROPS_BASE
-- Set some string id for output to DCS.log file.
self.lid=string.format("MARKEROPS_BASE %s | ", tostring(self.version))
self.Tag = Tagname or "mytag"-- #string
self.Keywords = Keywords or {} -- #table - might want to use lua regex here, too
self.debug = false
-----------------------
--- FSM Transitions ---
-----------------------
-- Start State.
self:SetStartState("Stopped")
-- Add FSM transitions.
-- From State --> Event --> To State
self:AddTransition("Stopped", "Start", "Running") -- Start the FSM.
self:AddTransition("*", "MarkAdded", "*") -- Start the FSM.
self:AddTransition("*", "MarkChanged", "*") -- Start the FSM.
self:AddTransition("*", "MarkDeleted", "*") -- Start the FSM.
self:AddTransition("Running", "Stop", "Stopped") -- Stop the FSM.
self:HandleEvent(EVENTS.MarkAdded, self.OnEventMark)
self:HandleEvent(EVENTS.MarkChange, self.OnEventMark)
self:HandleEvent(EVENTS.MarkRemoved, self.OnEventMark)
-- start
self:I(self.lid..string.format("started for %s",self.Tag))
self:__Start(1)
return self
-------------------
-- PSEUDO Functions
-------------------
--- On after "MarkAdded" event. Triggered when a Marker is added to the F10 map.
-- @function [parent=#MARKEROPS_BASE] OnAfterMarkAdded
-- @param #MARKEROPS_BASE self
-- @param #string From The From state
-- @param #string Event The Event called
-- @param #string To The To state
-- @param #string Text The text on the marker
-- @param #table Keywords Table of matching keywords found in the Event text
-- @param Core.Point#COORDINATE Coord Coordinate of the marker.
--- On after "MarkChanged" event. Triggered when a Marker is changed on the F10 map.
-- @function [parent=#MARKEROPS_BASE] OnAfterMarkChanged
-- @param #MARKEROPS_BASE self
-- @param #string From The From state
-- @param #string Event The Event called
-- @param #string To The To state
-- @param #string Text The text on the marker
-- @param #table Keywords Table of matching keywords found in the Event text
-- @param Core.Point#COORDINATE Coord Coordinate of the marker.
--- On after "MarkDeleted" event. Triggered when a Marker is deleted from the F10 map.
-- @function [parent=#MARKEROPS_BASE] OnAfterMarkDeleted
-- @param #MARKEROPS_BASE self
-- @param #string From The From state
-- @param #string Event The Event called
-- @param #string To The To state
--- "Stop" trigger. Used to stop the function an unhandle events
-- @function [parent=#MARKEROPS_BASE] Stop
end
--- (internal) Handle events.
-- @param #MARKEROPS self
-- @param Core.Event#EVENTDATA Event
function MARKEROPS_BASE:OnEventMark(Event)
self:T({Event})
if Event == nil or Event.idx == nil then
self:E("Skipping onEvent. Event or Event.idx unknown.")
return true
end
--position
local vec3={y=Event.pos.y, x=Event.pos.x, z=Event.pos.z}
local coord=COORDINATE:NewFromVec3(vec3)
if self.debug then
local coordtext = coord:ToStringLLDDM()
local text = tostring(Event.text)
local m = MESSAGE:New(string.format("Mark added at %s with text: %s",coordtext,text),10,"Info",false):ToAll()
end
-- decision
if Event.id==world.event.S_EVENT_MARK_ADDED then
self:T({event="S_EVENT_MARK_ADDED", carrier=self.groupname, vec3=Event.pos})
-- Handle event
local Eventtext = tostring(Event.text)
if Eventtext~=nil then
if self:_MatchTag(Eventtext) then
local matchtable = self:_MatchKeywords(Eventtext)
self:MarkAdded(Eventtext,matchtable,coord)
end
end
elseif Event.id==world.event.S_EVENT_MARK_CHANGE then
self:T({event="S_EVENT_MARK_CHANGE", carrier=self.groupname, vec3=Event.pos})
-- Handle event.
local Eventtext = tostring(Event.text)
if Eventtext~=nil then
if self:_MatchTag(Eventtext) then
local matchtable = self:_MatchKeywords(Eventtext)
self:MarkChanged(Eventtext,matchtable,coord)
end
end
elseif Event.id==world.event.S_EVENT_MARK_REMOVED then
self:T({event="S_EVENT_MARK_REMOVED", carrier=self.groupname, vec3=Event.pos})
-- Hande event.
local Eventtext = tostring(Event.text)
if Eventtext~=nil then
if self:_MatchTag(Eventtext) then
self:MarkDeleted()
end
end
end
end
--- (internal) Match tag.
-- @param #MARKEROPS self
-- @param #string Eventtext Text added to the marker.
-- @return #boolean
function MARKEROPS_BASE:_MatchTag(Eventtext)
local matches = false
local type = string.lower(self.Tag) -- #string
if string.find(string.lower(Eventtext),type) then
matches = true --event text contains tag
end
return matches
end
--- (internal) Match keywords table.
-- @param #MARKEROPS self
-- @param #string Eventtext Text added to the marker.
-- @return #table
function MARKEROPS_BASE:_MatchKeywords(Eventtext)
local matchtable = {}
local keytable = self.Keywords
for _index,_word in pairs (keytable) do
if string.find(string.lower(Eventtext),string.lower(_word))then
table.insert(matchtable,_word)
end
end
return matchtable
end
--- On before "MarkAdded" event. Triggered when a Marker is added to the F10 map.
-- @param #MARKEROPS_BASE self
-- @param #string From The From state
-- @param #string Event The Event called
-- @param #string To The To state
-- @param #string Text The text on the marker
-- @param #table Keywords Table of matching keywords found in the Event text
-- @param Core.Point#COORDINATE Coord Coordinate of the marker.
function MARKEROPS_BASE:onbeforeMarkAdded(From,Event,To,Text,Keywords,Coord)
self:T({self.lid,From,Event,To,Text,Keywords,Coord:ToStringLLDDM()})
end
--- On before "MarkChanged" event. Triggered when a Marker is changed on the F10 map.
-- @param #MARKEROPS_BASE self
-- @param #string From The From state
-- @param #string Event The Event called
-- @param #string To The To state
-- @param #string Text The text on the marker
-- @param #table Keywords Table of matching keywords found in the Event text
-- @param Core.Point#COORDINATE Coord Coordinate of the marker.
function MARKEROPS_BASE:onbeforeMarkChanged(From,Event,To,Text,Keywords,Coord)
self:T({self.lid,From,Event,To,Text,Keywords,Coord:ToStringLLDDM()})
end
--- On before "MarkDeleted" event. Triggered when a Marker is removed from the F10 map.
-- @param #MARKEROPS_BASE self
-- @param #string From The From state
-- @param #string Event The Event called
-- @param #string To The To state
function MARKEROPS_BASE:onbeforeMarkDeleted(From,Event,To)
self:T({self.lid,From,Event,To})
end
--- On enter "Stopped" event. Unsubscribe events.
-- @param #MARKEROPS_BASE self
-- @param #string From The From state
-- @param #string Event The Event called
-- @param #string To The To state
function MARKEROPS_BASE:onenterStopped(From,Event,To)
self:T({self.lid,From,Event,To})
-- unsubscribe from events
self:UnHandleEvent(EVENTS.MarkAdded)
self:UnHandleEvent(EVENTS.MarkChange)
self:UnHandleEvent(EVENTS.MarkRemoved)
end
--------------------------------------------------------------------------
-- MARKEROPS_BASE Class Definition End.
--------------------------------------------------------------------------

File diff suppressed because it is too large Load Diff

View File

@ -122,7 +122,7 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr
self.Schedule[Scheduler][CallID].Function = ScheduleFunction
self.Schedule[Scheduler][CallID].Arguments = ScheduleArguments
self.Schedule[Scheduler][CallID].StartTime = timer.getTime() + ( Start or 0 )
self.Schedule[Scheduler][CallID].Start = Start + 0.1
self.Schedule[Scheduler][CallID].Start = Start + 0.001
self.Schedule[Scheduler][CallID].Repeat = Repeat or 0
self.Schedule[Scheduler][CallID].Randomize = Randomize or 0
self.Schedule[Scheduler][CallID].Stop = Stop

View File

@ -233,7 +233,9 @@ do -- SET_BASE
-- @param Core.Base#BASE Object The object itself.
-- @return Core.Base#BASE The added BASE Object.
function SET_BASE:Add( ObjectName, Object )
self:F2( { ObjectName = ObjectName, Object = Object } )
-- Debug info.
self:T( { ObjectName = ObjectName, Object = Object } )
-- Ensure that the existing element is removed from the Set before a new one is inserted to the Set
if self.Set[ObjectName] then
@ -880,8 +882,8 @@ do -- SET_GROUP
--
-- * @{#SET_GROUP.FilterCoalitions}: Builds the SET_GROUP with the groups belonging to the coalition(s).
-- * @{#SET_GROUP.FilterCategories}: Builds the SET_GROUP with the groups belonging to the category(ies).
-- * @{#SET_GROUP.FilterCountries}: Builds the SET_GROUP with the gruops belonging to the country(ies).
-- * @{#SET_GROUP.FilterPrefixes}: Builds the SET_GROUP with the groups starting with the same prefix string(s).
-- * @{#SET_GROUP.FilterCountries}: Builds the SET_GROUP with the groups belonging to the country(ies).
-- * @{#SET_GROUP.FilterPrefixes}: Builds the SET_GROUP with the groups *containing* the given string in the group name. **Attention!** Bad naming convention, as this not really filtering *prefixes*.
-- * @{#SET_GROUP.FilterActive}: Builds the SET_GROUP with the groups that are only active. Groups that are inactive (late activation) won't be included in the set!
--
-- For the Category Filter, extra methods have been added:
@ -1241,10 +1243,10 @@ do -- SET_GROUP
end
--- Builds a set of groups of defined GROUP prefixes.
-- All the groups starting with the given prefixes will be included within the set.
--- Builds a set of groups that contain the given string in their group name.
-- **Attention!** Bad naming convention as this **does not** filter only **prefixes** but all groups that **contain** the string.
-- @param #SET_GROUP self
-- @param #string Prefixes The prefix of which the group name starts with.
-- @param #string Prefixes The string pattern(s) that needs to be contained in the group name. Can also be passed as a `#table` of strings.
-- @return #SET_GROUP self
function SET_GROUP:FilterPrefixes( Prefixes )
if not self.Filter.GroupPrefixes then
@ -1400,6 +1402,22 @@ do -- SET_GROUP
return self
end
--- Activate late activated groups.
-- @param #SET_GROUP self
-- @param #number Delay Delay in seconds.
-- @return #SET_GROUP self
function SET_GROUP:Activate(Delay)
local Set = self:GetSet()
for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP
local group=GroupData --Wrapper.Group#GROUP
if group and group:IsAlive()==false then
group:Activate(Delay)
end
end
return self
end
--- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence completely in a @{Zone}, providing the GROUP and optional parameters to the called function.
-- @param #SET_GROUP self
-- @param Core.Zone#ZONE ZoneObject The Zone to be tested for.
@ -1843,7 +1861,7 @@ do -- SET_UNIT
-- * @{#SET_UNIT.FilterCategories}: Builds the SET_UNIT with the units belonging to the category(ies).
-- * @{#SET_UNIT.FilterTypes}: Builds the SET_UNIT with the units belonging to the unit type(s).
-- * @{#SET_UNIT.FilterCountries}: Builds the SET_UNIT with the units belonging to the country(ies).
-- * @{#SET_UNIT.FilterPrefixes}: Builds the SET_UNIT with the units starting with the same prefix string(s).
-- * @{#SET_UNIT.FilterPrefixes}: Builds the SET_UNIT with the units sharing the same string(s) in their name. **ATTENTION!** Bad naming convention as this *does not* only filter *prefixes*.
-- * @{#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!
--
-- Once the filter criteria have been set for the SET_UNIT, you can start filtering using:
@ -2105,10 +2123,10 @@ do -- SET_UNIT
end
--- Builds a set of units of defined unit prefixes.
-- All the units starting with the given prefixes will be included within the set.
--- Builds a set of UNITs that contain a given string in their unit name.
-- **Attention!** Bad naming convention as this **does not** filter only **prefixes** but all units that **contain** the string.
-- @param #SET_UNIT self
-- @param #string Prefixes The prefix of which the unit name starts with.
-- @param #string Prefixes The string pattern(s) that needs to be contained in the unit name. Can also be passed as a `#table` of strings.
-- @return #SET_UNIT self
function SET_UNIT:FilterPrefixes( Prefixes )
if not self.Filter.UnitPrefixes then
@ -2963,7 +2981,7 @@ do -- SET_STATIC
-- * @{#SET_STATIC.FilterCategories}: Builds the SET_STATIC with the units belonging to the category(ies).
-- * @{#SET_STATIC.FilterTypes}: Builds the SET_STATIC with the units belonging to the unit type(s).
-- * @{#SET_STATIC.FilterCountries}: Builds the SET_STATIC with the units belonging to the country(ies).
-- * @{#SET_STATIC.FilterPrefixes}: Builds the SET_STATIC with the units starting with the same prefix string(s).
-- * @{#SET_STATIC.FilterPrefixes}: Builds the SET_STATIC with the units containing the same string(s) in their name. **ATTENTION** bad naming convention as this *does not** only filter *prefixes*.
--
-- Once the filter criteria have been set for the SET_STATIC, you can start filtering using:
--
@ -3176,10 +3194,10 @@ do -- SET_STATIC
end
--- Builds a set of units of defined unit prefixes.
-- All the units starting with the given prefixes will be included within the set.
--- Builds a set of STATICs that contain the given string in their name.
-- **Attention!** Bad naming convention as this **does not** filter only **prefixes** but all statics that **contain** the string.
-- @param #SET_STATIC self
-- @param #string Prefixes The prefix of which the unit name starts with.
-- @param #string Prefixes The string pattern(s) that need to be contained in the static name. Can also be passed as a `#table` of strings.
-- @return #SET_STATIC self
function SET_STATIC:FilterPrefixes( Prefixes )
if not self.Filter.StaticPrefixes then
@ -3267,7 +3285,7 @@ do -- SET_STATIC
-- @param #SET_STATIC self
-- @param Core.Zone#ZONE Zone The Zone to be tested for.
-- @return #boolean
function SET_STATIC:IsPatriallyInZone( Zone )
function SET_STATIC:IsPartiallyInZone( Zone )
local IsPartiallyInZone = false
@ -3675,7 +3693,7 @@ do -- SET_CLIENT
-- * @{#SET_CLIENT.FilterCategories}: Builds the SET_CLIENT with the clients belonging to the category(ies).
-- * @{#SET_CLIENT.FilterTypes}: Builds the SET_CLIENT with the clients belonging to the client type(s).
-- * @{#SET_CLIENT.FilterCountries}: Builds the SET_CLIENT with the clients belonging to the country(ies).
-- * @{#SET_CLIENT.FilterPrefixes}: Builds the SET_CLIENT with the clients starting with the same prefix string(s).
-- * @{#SET_CLIENT.FilterPrefixes}: Builds the SET_CLIENT with the clients containing the same string(s) in their unit/pilot name. **ATTENTION!** Bad naming convention as this *does not* only filter *prefixes*.
-- * @{#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!
--
-- Once the filter criteria have been set for the SET_CLIENT, you can start filtering using:
@ -3858,10 +3876,10 @@ do -- SET_CLIENT
end
--- Builds a set of clients of defined client prefixes.
-- All the clients starting with the given prefixes will be included within the set.
--- Builds a set of CLIENTs that contain the given string in their unit/pilot name.
-- **Attention!** Bad naming convention as this **does not** filter only **prefixes** but all clients that **contain** the string.
-- @param #SET_CLIENT self
-- @param #string Prefixes The prefix of which the client name starts with.
-- @param #string Prefixes The string pattern(s) that needs to be contained in the unit/pilot name. Can also be passed as a `#table` of strings.
-- @return #SET_CLIENT self
function SET_CLIENT:FilterPrefixes( Prefixes )
if not self.Filter.ClientPrefixes then
@ -4114,7 +4132,7 @@ do -- SET_PLAYER
-- * @{#SET_PLAYER.FilterCategories}: Builds the SET_PLAYER with the clients belonging to the category(ies).
-- * @{#SET_PLAYER.FilterTypes}: Builds the SET_PLAYER with the clients belonging to the client type(s).
-- * @{#SET_PLAYER.FilterCountries}: Builds the SET_PLAYER with the clients belonging to the country(ies).
-- * @{#SET_PLAYER.FilterPrefixes}: Builds the SET_PLAYER with the clients starting with the same prefix string(s).
-- * @{#SET_PLAYER.FilterPrefixes}: Builds the SET_PLAYER with the clients sharing the same string(s) in their unit/pilot name. **ATTENTION** Bad naming convention as this *does not* only filter prefixes.
--
-- Once the filter criteria have been set for the SET_PLAYER, you can start filtering using:
--
@ -4293,10 +4311,10 @@ do -- SET_PLAYER
end
--- Builds a set of clients of defined client prefixes.
-- All the clients starting with the given prefixes will be included within the set.
--- Builds a set of PLAYERs that contain the given string in their unit/pilot name.
-- **Attention!** Bad naming convention as this **does not** filter only **prefixes** but all player clients that **contain** the string.
-- @param #SET_PLAYER self
-- @param #string Prefixes The prefix of which the client name starts with.
-- @param #string Prefixes The string pattern(s) that needs to be contained in the unit/pilot name. Can also be passed as a `#table` of strings.
-- @return #SET_PLAYER self
function SET_PLAYER:FilterPrefixes( Prefixes )
if not self.Filter.ClientPrefixes then
@ -4705,7 +4723,8 @@ do -- SET_AIRBASE
if _DATABASE then
-- We use the BaseCaptured event, which is generated by DCS when a base got captured.
self:HandleEvent( EVENTS.BaseCaptured )
self:HandleEvent(EVENTS.BaseCaptured)
self:HandleEvent(EVENTS.Dead)
-- We initialize the first set.
for ObjectName, Object in pairs( self.Database ) do
@ -4720,10 +4739,9 @@ do -- SET_AIRBASE
return self
end
--- Starts the filtering.
--- Base capturing event.
-- @param #SET_AIRBASE self
-- @param Core.Event#EVENT EventData
-- @return #SET_AIRBASE self
function SET_AIRBASE:OnEventBaseCaptured(EventData)
-- When a base got captured, we reevaluate the set.
@ -4739,24 +4757,36 @@ do -- SET_AIRBASE
end
--- Dead event.
-- @param #SET_AIRBASE self
-- @param Core.Event#EVENT EventData
function SET_AIRBASE:OnEventDead(EventData)
local airbaseName, airbase=self:FindInDatabase(EventData)
if airbase and airbase:IsShip() or airbase:IsHelipad() then
self:RemoveAirbasesByName(airbaseName)
end
end
--- 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_AIRBASE self
-- @param Core.Event#EVENTDATA Event
-- @return #string The name of the AIRBASE
-- @return #table The AIRBASE
-- @param Core.Event#EVENTDATA Event Event data.
-- @return #string The name of the AIRBASE.
-- @return Wrapper.Airbase#AIRBASE The AIRBASE object.
function SET_AIRBASE:AddInDatabase( Event )
self:F3( { Event } )
return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName]
end
--- Handles the Database to check on any event that Object exists in the Database.
-- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa!
-- @param #SET_AIRBASE self
-- @param Core.Event#EVENTDATA Event
-- @return #string The name of the AIRBASE
-- @return #table The AIRBASE
-- @param Core.Event#EVENTDATA Event Event data.
-- @return #string The name of the AIRBASE.
-- @return Wrapper.Airbase#AIRBASE The AIRBASE object.
function SET_AIRBASE:FindInDatabase( Event )
self:F3( { Event } )
@ -4862,7 +4892,7 @@ do -- SET_CARGO
-- Filter criteria are defined by:
--
-- * @{#SET_CARGO.FilterCoalitions}: Builds the SET_CARGO with the cargos belonging to the coalition(s).
-- * @{#SET_CARGO.FilterPrefixes}: Builds the SET_CARGO with the cargos containing the prefix string(s).
-- * @{#SET_CARGO.FilterPrefixes}: Builds the SET_CARGO with the cargos containing the same string(s). **ATTENTION** Bad naming convention as this *does not* only filter *prefixes*.
-- * @{#SET_CARGO.FilterTypes}: Builds the SET_CARGO with the cargos belonging to the cargo type(s).
-- * @{#SET_CARGO.FilterCountries}: Builds the SET_CARGO with the cargos belonging to the country(ies).
--
@ -4916,7 +4946,7 @@ do -- SET_CARGO
--- (R2.1) Add CARGO to SET_CARGO.
-- @param Core.Set#SET_CARGO self
-- @param Cargo.Cargo#CARGO Cargo A single cargo.
-- @return self
-- @return Core.Set#SET_CARGO self
function SET_CARGO:AddCargo( Cargo ) --R2.4
self:Add( Cargo:GetName(), Cargo )
@ -4928,7 +4958,7 @@ do -- SET_CARGO
--- (R2.1) Add CARGOs to SET_CARGO.
-- @param Core.Set#SET_CARGO self
-- @param #string AddCargoNames A single name or an array of CARGO names.
-- @return self
-- @return Core.Set#SET_CARGO self
function SET_CARGO:AddCargosByName( AddCargoNames ) --R2.1
local AddCargoNamesArray = ( type( AddCargoNames ) == "table" ) and AddCargoNames or { AddCargoNames }
@ -4943,7 +4973,7 @@ do -- SET_CARGO
--- (R2.1) Remove CARGOs from SET_CARGO.
-- @param Core.Set#SET_CARGO self
-- @param Wrapper.Cargo#CARGO RemoveCargoNames A single name or an array of CARGO names.
-- @return self
-- @return Core.Set#SET_CARGO self
function SET_CARGO:RemoveCargosByName( RemoveCargoNames ) --R2.1
local RemoveCargoNamesArray = ( type( RemoveCargoNames ) == "table" ) and RemoveCargoNames or { RemoveCargoNames }
@ -5024,10 +5054,10 @@ do -- SET_CARGO
end
--- (R2.1) Builds a set of cargos of defined cargo prefixes.
-- All the cargos starting with the given prefixes will be included within the set.
--- Builds a set of CARGOs that contain a given string in their name.
-- **Attention!** Bad naming convention as this **does not** filter only **prefixes** but all cargos that **contain** the string.
-- @param #SET_CARGO self
-- @param #string Prefixes The prefix of which the cargo name starts with.
-- @param #string Prefixes The string pattern(s) that need to be in the cargo name. Can also be passed as a `#table` of strings.
-- @return #SET_CARGO self
function SET_CARGO:FilterPrefixes( Prefixes ) --R2.1
if not self.Filter.CargoPrefixes then
@ -5303,7 +5333,7 @@ do -- SET_ZONE
-- You can set filter criteria to build the collection of zones in SET_ZONE.
-- Filter criteria are defined by:
--
-- * @{#SET_ZONE.FilterPrefixes}: Builds the SET_ZONE with the zones having a certain text pattern of prefix.
-- * @{#SET_ZONE.FilterPrefixes}: Builds the SET_ZONE with the zones having a certain text pattern in their name. **ATTENTION!** Bad naming convention as this *does not* only filter *prefixes*.
--
-- Once the filter criteria have been set for the SET_ZONE, you can start filtering using:
--
@ -5399,10 +5429,12 @@ do -- SET_ZONE
--- Get a random zone from the set.
-- @param #SET_ZONE self
-- @param #number margin Number of tries to find a zone
-- @return Core.Zone#ZONE_BASE The random Zone.
-- @return #nil if no zone in the collection.
function SET_ZONE:GetRandomZone()
function SET_ZONE:GetRandomZone(margin)
local margin = margin or 100
if self:Count() ~= 0 then
local Index = self.Index
@ -5411,9 +5443,11 @@ do -- SET_ZONE
-- Loop until a zone has been found.
-- The :GetZoneMaybe() call will evaluate the probability for the zone to be selected.
-- If the zone is not selected, then nil is returned by :GetZoneMaybe() and the loop continues!
while not ZoneFound do
local counter = 0
while (not ZoneFound) or (counter < margin) do
local ZoneRandom = math.random( 1, #Index )
ZoneFound = self.Set[Index[ZoneRandom]]:GetZoneMaybe()
counter = counter + 1
end
return ZoneFound
@ -5434,10 +5468,10 @@ do -- SET_ZONE
--- Builds a set of zones of defined zone prefixes.
-- All the zones starting with the given prefixes will be included within the set.
--- Builds a set of ZONEs that contain the given string in their name.
-- **ATTENTION!** Bad naming convention as this **does not** filter only **prefixes** but all zones that **contain** the string.
-- @param #SET_ZONE self
-- @param #string Prefixes The prefix of which the zone name starts with.
-- @param #string Prefixes The string pattern(s) that need to be contained in the zone name. Can also be passed as a `#table` of strings.
-- @return #SET_ZONE self
function SET_ZONE:FilterPrefixes( Prefixes )
if not self.Filter.Prefixes then
@ -5636,7 +5670,7 @@ do -- SET_ZONE_GOAL
-- You can set filter criteria to build the collection of zones in SET_ZONE_GOAL.
-- Filter criteria are defined by:
--
-- * @{#SET_ZONE_GOAL.FilterPrefixes}: Builds the SET_ZONE_GOAL with the zones having a certain text pattern of prefix.
-- * @{#SET_ZONE_GOAL.FilterPrefixes}: Builds the SET_ZONE_GOAL with the zones having a certain text pattern in their name. **ATTENTION!** Bad naming convention as this *does not* only filter *prefixes*.
--
-- Once the filter criteria have been set for the SET_ZONE_GOAL, you can start filtering using:
--
@ -5752,10 +5786,10 @@ do -- SET_ZONE_GOAL
--- Builds a set of zones of defined zone prefixes.
-- All the zones starting with the given prefixes will be included within the set.
--- Builds a set of ZONE_GOALs that contain the given string in their name.
-- **ATTENTION!** Bad naming convention as this **does not** filter only **prefixes** but all zones that **contain** the string.
-- @param #SET_ZONE_GOAL self
-- @param #string Prefixes The prefix of which the zone name starts with.
-- @param #string Prefixes The string pattern(s) that needs to be contained in the zone name. Can also be passed as a `#table` of strings.
-- @return #SET_ZONE_GOAL self
function SET_ZONE_GOAL:FilterPrefixes( Prefixes )
if not self.Filter.Prefixes then
@ -5876,12 +5910,13 @@ do -- SET_ZONE_GOAL
-- @param Core.Event#EVENTDATA EventData
function SET_ZONE_GOAL:OnEventNewZoneGoal( EventData )
self:I( { "New Zone Capture Coalition", EventData } )
self:I( { "Zone Capture Coalition", EventData.ZoneGoal } )
-- Debug info.
self:T( { "New Zone Capture Coalition", EventData } )
self:T( { "Zone Capture Coalition", EventData.ZoneGoal } )
if EventData.ZoneGoal then
if EventData.ZoneGoal and self:IsIncludeObject( EventData.ZoneGoal ) then
self:I( { "Adding Zone Capture Coalition", EventData.ZoneGoal.ZoneName, EventData.ZoneGoal } )
self:T( { "Adding Zone Capture Coalition", EventData.ZoneGoal.ZoneName, EventData.ZoneGoal } )
self:Add( EventData.ZoneGoal.ZoneName , EventData.ZoneGoal )
end
end
@ -5932,3 +5967,618 @@ do -- SET_ZONE_GOAL
end
end
do -- SET_OPSGROUP
--- @type SET_OPSGROUP
-- @extends Core.Set#SET_BASE
--- Mission designers can use the @{Core.Set#SET_OPSGROUP} class to build sets of OPS groups belonging to certain:
--
-- * Coalitions
-- * Categories
-- * Countries
-- * Contain a certain string pattern
--
-- ## SET_OPSGROUP constructor
--
-- Create a new SET_OPSGROUP object with the @{#SET_OPSGROUP.New} method:
--
-- * @{#SET_OPSGROUP.New}: Creates a new SET_OPSGROUP object.
--
-- ## Add or Remove GROUP(s) from SET_OPSGROUP
--
-- GROUPS can be added and removed using the @{Core.Set#SET_OPSGROUP.AddGroupsByName} and @{Core.Set#SET_OPSGROUP.RemoveGroupsByName} respectively.
-- These methods take a single GROUP name or an array of GROUP names to be added or removed from SET_OPSGROUP.
--
-- ## SET_OPSGROUP filter criteria
--
-- You can set filter criteria to define the set of groups within the SET_OPSGROUP.
-- Filter criteria are defined by:
--
-- * @{#SET_OPSGROUP.FilterCoalitions}: Builds the SET_OPSGROUP with the groups belonging to the coalition(s).
-- * @{#SET_OPSGROUP.FilterCategories}: Builds the SET_OPSGROUP with the groups belonging to the category(ies).
-- * @{#SET_OPSGROUP.FilterCountries}: Builds the SET_OPSGROUP with the groups belonging to the country(ies).
-- * @{#SET_OPSGROUP.FilterPrefixes}: Builds the SET_OPSGROUP with the groups *containing* the given string in the group name. **Attention!** Bad naming convention, as this not really filtering *prefixes*.
-- * @{#SET_OPSGROUP.FilterActive}: Builds the SET_OPSGROUP with the groups that are only active. Groups that are inactive (late activation) won't be included in the set!
--
-- For the Category Filter, extra methods have been added:
--
-- * @{#SET_OPSGROUP.FilterCategoryAirplane}: Builds the SET_OPSGROUP from airplanes.
-- * @{#SET_OPSGROUP.FilterCategoryHelicopter}: Builds the SET_OPSGROUP from helicopters.
-- * @{#SET_OPSGROUP.FilterCategoryGround}: Builds the SET_OPSGROUP from ground vehicles or infantry.
-- * @{#SET_OPSGROUP.FilterCategoryShip}: Builds the SET_OPSGROUP from ships.
--
--
-- Once the filter criteria have been set for the SET_OPSGROUP, you can start filtering using:
--
-- * @{#SET_OPSGROUP.FilterStart}: Starts the filtering of the groups within the SET_OPSGROUP and add or remove GROUP objects **dynamically**.
-- * @{#SET_OPSGROUP.FilterOnce}: Filters of the groups **once**.
--
--
-- ## SET_OPSGROUP iterators
--
-- Once the filters have been defined and the SET_OPSGROUP has been built, you can iterate the SET_OPSGROUP with the available iterator methods.
-- The iterator methods will walk the SET_OPSGROUP set, and call for each element within the set a function that you provide.
-- The following iterator methods are currently available within the SET_OPSGROUP:
--
-- * @{#SET_OPSGROUP.ForEachGroup}: Calls a function for each alive group it finds within the SET_OPSGROUP.
--
-- ## SET_OPSGROUP trigger events on the GROUP objects.
--
-- The SET is derived from the FSM class, which provides extra capabilities to track the contents of the GROUP objects in the SET_OPSGROUP.
--
-- ### When a GROUP object crashes or is dead, the SET_OPSGROUP will trigger a **Dead** event.
--
-- You can handle the event using the OnBefore and OnAfter event handlers.
-- The event handlers need to have the paramters From, Event, To, GroupObject.
-- The GroupObject is the GROUP object that is dead and within the SET_OPSGROUP, and is passed as a parameter to the event handler.
-- See the following example:
--
-- -- Create the SetCarrier SET_OPSGROUP collection.
--
-- local SetHelicopter = SET_OPSGROUP:New():FilterPrefixes( "Helicopter" ):FilterStart()
--
-- -- Put a Dead event handler on SetCarrier, to ensure that when a carrier is destroyed, that all internal parameters are reset.
--
-- function SetHelicopter:OnAfterDead( From, Event, To, GroupObject )
-- self:F( { GroupObject = GroupObject:GetName() } )
-- end
--
--
-- ===
--
-- @field #SET_OPSGROUP SET_OPSGROUP
--
SET_OPSGROUP = {
ClassName = "SET_OPSGROUP",
Filter = {
Coalitions = nil,
Categories = nil,
Countries = nil,
GroupPrefixes = nil,
},
FilterMeta = {
Coalitions = {
red = coalition.side.RED,
blue = coalition.side.BLUE,
neutral = coalition.side.NEUTRAL,
},
Categories = {
plane = Group.Category.AIRPLANE,
helicopter = Group.Category.HELICOPTER,
ground = Group.Category.GROUND,
ship = Group.Category.SHIP,
},
}, -- FilterMeta
}
--- Creates a new SET_OPSGROUP object, building a set of groups belonging to a coalitions, categories, countries, types or with defined prefix names.
-- @param #SET_OPSGROUP self
-- @return #SET_OPSGROUP
function SET_OPSGROUP:New()
-- Inherit SET_BASE.
local self = BASE:Inherit(self, SET_BASE:New(_DATABASE.GROUPS)) -- #SET_OPSGROUP
-- Include non activated
self:FilterActive( false )
return self
end
--- Gets the Set.
-- @param #SET_OPSGROUP self
-- @return #SET_OPSGROUP self
function SET_OPSGROUP:GetAliveSet()
local AliveSet = SET_OPSGROUP:New()
-- Clean the Set before returning with only the alive Groups.
for GroupName, GroupObject in pairs(self.Set) do
local GroupObject=GroupObject --Wrapper.Group#GROUP
if GroupObject and GroupObject:IsAlive() then
AliveSet:Add(GroupName, GroupObject)
end
end
return AliveSet.Set or {}
end
--- Adds a @{Core.Base#BASE} object in the @{Core.Set#SET_BASE}, using a given ObjectName as the index.
-- @param #SET_BASE self
-- @param #string ObjectName The name of the object.
-- @param Core.Base#BASE Object The object itself.
-- @return Core.Base#BASE The added BASE Object.
function SET_OPSGROUP:Add(ObjectName, Object)
self:T( { ObjectName = ObjectName, Object = Object } )
-- Ensure that the existing element is removed from the Set before a new one is inserted to the Set
if self.Set[ObjectName] then
self:Remove(ObjectName, true)
end
local object=nil --Ops.OpsGroup#OPSGROUP
if Object:IsInstanceOf("GROUP") then
---
-- GROUP Object
---
-- Fist, look up in the DATABASE if an OPSGROUP already exists.
object=_DATABASE:FindOpsGroup(ObjectName)
if not object then
if Object:IsShip() then
object=NAVYGROUP:New(Object)
elseif Object:IsGround() then
object=ARMYGROUP:New(Object)
elseif Object:IsAir() then
object=FLIGHTGROUP:New(Object)
else
env.error("ERROR: Unknown category of group object!")
end
end
elseif Object:IsInstanceOf("OPSGROUP") then
-- We already have an OPSGROUP.
object=Object
else
env.error("ERROR: Object must be a GROUP or OPSGROUP!")
end
-- Add object to set.
self.Set[ObjectName]=object
-- Add Object name to Index.
table.insert(self.Index, ObjectName)
-- Trigger Added event.
self:Added(ObjectName, object)
end
--- Add a GROUP or OPSGROUP object to the set.
-- **NOTE** that an OPSGROUP is automatically created from the GROUP if it does not exist already.
-- @param Core.Set#SET_OPSGROUP self
-- @param Wrapper.Group#GROUP group The GROUP which should be added to the set. Can also be given as an #OPSGROUP object.
-- @return Core.Set#SET_OPSGROUP self
function SET_OPSGROUP:AddGroup(group)
local groupname=group:GetName()
self:Add(groupname, group )
return self
end
--- Add GROUP(s) or OPSGROUP(s) to the set.
-- @param Core.Set#SET_OPSGROUP self
-- @param #string AddGroupNames A single name or an array of GROUP names.
-- @return Core.Set#SET_OPSGROUP self
function SET_OPSGROUP:AddGroupsByName( AddGroupNames )
local AddGroupNamesArray = ( type( AddGroupNames ) == "table" ) and AddGroupNames or { AddGroupNames }
for AddGroupID, AddGroupName in pairs( AddGroupNamesArray ) do
self:Add(AddGroupName, GROUP:FindByName(AddGroupName))
end
return self
end
--- Remove GROUP(s) or OPSGROUP(s) from the set.
-- @param Core.Set#SET_OPSGROUP self
-- @param Wrapper.Group#GROUP RemoveGroupNames A single name or an array of GROUP names.
-- @return Core.Set#SET_OPSGROUP self
function SET_OPSGROUP:RemoveGroupsByName( RemoveGroupNames )
local RemoveGroupNamesArray = ( type( RemoveGroupNames ) == "table" ) and RemoveGroupNames or { RemoveGroupNames }
for RemoveGroupID, RemoveGroupName in pairs( RemoveGroupNamesArray ) do
self:Remove( RemoveGroupName )
end
return self
end
--- Finds an OPSGROUP based on the group name.
-- @param #SET_OPSGROUP self
-- @param #string GroupName Name of the group.
-- @return Ops.OpsGroup#OPSGROUP The found OPSGROUP (FLIGHTGROUP, ARMYGROUP or NAVYGROUP) or `#nil` if the group is not in the set.
function SET_OPSGROUP:FindGroup(GroupName)
local GroupFound = self.Set[GroupName]
return GroupFound
end
--- Finds a FLIGHTGROUP based on the group name.
-- @param #SET_OPSGROUP self
-- @param #string GroupName Name of the group.
-- @return Ops.FlightGroup#FLIGHTGROUP The found FLIGHTGROUP or `#nil` if the group is not in the set.
function SET_OPSGROUP:FindFlightGroup(GroupName)
local GroupFound = self:FindGroup(GroupName)
return GroupFound
end
--- Finds a ARMYGROUP based on the group name.
-- @param #SET_OPSGROUP self
-- @param #string GroupName Name of the group.
-- @return Ops.ArmyGroup#ARMYGROUP The found ARMYGROUP or `#nil` if the group is not in the set.
function SET_OPSGROUP:FindArmyGroup(GroupName)
local GroupFound = self:FindGroup(GroupName)
return GroupFound
end
--- Finds a NAVYGROUP based on the group name.
-- @param #SET_OPSGROUP self
-- @param #string GroupName Name of the group.
-- @return Ops.NavyGroup#NAVYGROUP The found NAVYGROUP or `#nil` if the group is not in the set.
function SET_OPSGROUP:FindNavyGroup(GroupName)
local GroupFound = self:FindGroup(GroupName)
return GroupFound
end
--- Builds a set of groups of coalitions.
-- Possible current coalitions are red, blue and neutral.
-- @param #SET_OPSGROUP self
-- @param #string Coalitions Can take the following values: "red", "blue", "neutral" or combinations as a table, for example `{"red", "neutral"}`.
-- @return #SET_OPSGROUP self
function SET_OPSGROUP:FilterCoalitions(Coalitions)
-- Create an empty set.
if not self.Filter.Coalitions then
self.Filter.Coalitions={}
end
-- Ensure we got a table.
if type(Coalitions)~="table" then
Coalitions = {Coalitions}
end
-- Set filter.
for CoalitionID, Coalition in pairs( Coalitions ) do
self.Filter.Coalitions[Coalition] = Coalition
end
return self
end
--- Builds a set of groups out of categories.
--
-- Possible current categories are:
--
-- * "plane" for fixed wing groups
-- * "helicopter" for rotary wing groups
-- * "ground" for ground groups
-- * "ship" for naval groups
--
-- @param #SET_OPSGROUP self
-- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship" or combinations as a table, for example `{"plane", "helicopter"}`.
-- @return #SET_OPSGROUP self
function SET_OPSGROUP:FilterCategories( Categories )
if not self.Filter.Categories then
self.Filter.Categories={}
end
if type(Categories)~="table" then
Categories={Categories}
end
for CategoryID, Category in pairs( Categories ) do
self.Filter.Categories[Category] = Category
end
return self
end
--- Builds a set of groups out of ground category.
-- @param #SET_OPSGROUP self
-- @return #SET_OPSGROUP self
function SET_OPSGROUP:FilterCategoryGround()
self:FilterCategories("ground")
return self
end
--- Builds a set of groups out of airplane category.
-- @param #SET_OPSGROUP self
-- @return #SET_OPSGROUP self
function SET_OPSGROUP:FilterCategoryAirplane()
self:FilterCategories("plane")
return self
end
--- Builds a set of groups out of aicraft category (planes and helicopters).
-- @param #SET_OPSGROUP self
-- @return #SET_OPSGROUP self
function SET_OPSGROUP:FilterCategoryAircraft()
self:FilterCategories({"plane", "helicopter"})
return self
end
--- Builds a set of groups out of helicopter category.
-- @param #SET_OPSGROUP self
-- @return #SET_OPSGROUP self
function SET_OPSGROUP:FilterCategoryHelicopter()
self:FilterCategories("helicopter")
return self
end
--- Builds a set of groups out of ship category.
-- @param #SET_OPSGROUP self
-- @return #SET_OPSGROUP self
function SET_OPSGROUP:FilterCategoryShip()
self:FilterCategories("ship")
return self
end
--- Builds a set of groups of defined countries.
-- @param #SET_OPSGROUP self
-- @param #string Countries Can take those country strings known within DCS world.
-- @return #SET_OPSGROUP self
function SET_OPSGROUP:FilterCountries(Countries)
-- Create empty table if necessary.
if not self.Filter.Countries then
self.Filter.Countries = {}
end
-- Ensure input is a table.
if type(Countries)~="table" then
Countries={Countries}
end
-- Set filter.
for CountryID, Country in pairs( Countries ) do
self.Filter.Countries[Country] = Country
end
return self
end
--- Builds a set of groups that contain the given string in their group name.
-- **Attention!** Bad naming convention as this **does not** filter only **prefixes** but all groups that **contain** the string.
-- @param #SET_OPSGROUP self
-- @param #string Prefixes The string pattern(s) that needs to be contained in the group name. Can also be passed as a `#table` of strings.
-- @return #SET_OPSGROUP self
function SET_OPSGROUP:FilterPrefixes(Prefixes)
-- Create emtpy table if necessary.
if not self.Filter.GroupPrefixes then
self.Filter.GroupPrefixes={}
end
-- Ensure we have a table.
if type(Prefixes)~="table" then
Prefixes={Prefixes}
end
-- Set group prefixes.
for PrefixID, Prefix in pairs(Prefixes) do
self.Filter.GroupPrefixes[Prefix]=Prefix
end
return self
end
--- Builds a set of groups that are only active.
-- Only the groups that are active will be included within the set.
-- @param #SET_OPSGROUP self
-- @param #boolean Active (optional) Include only active groups to the set.
-- Include inactive groups if you provide false.
-- @return #SET_OPSGROUP self
-- @usage
--
-- -- Include only active groups to the set.
-- GroupSet = SET_OPSGROUP:New():FilterActive():FilterStart()
--
-- -- Include only active groups to the set of the blue coalition, and filter one time.
-- GroupSet = SET_OPSGROUP:New():FilterActive():FilterCoalition( "blue" ):FilterOnce()
--
-- -- Include only active groups to the set of the blue coalition, and filter one time.
-- -- Later, reset to include back inactive groups to the set.
-- GroupSet = SET_OPSGROUP:New():FilterActive():FilterCoalition( "blue" ):FilterOnce()
-- ... logic ...
-- GroupSet = SET_OPSGROUP:New():FilterActive( false ):FilterCoalition( "blue" ):FilterOnce()
--
function SET_OPSGROUP:FilterActive( Active )
Active = Active or not ( Active == false )
self.Filter.Active = Active
return self
end
--- Starts the filtering.
-- @param #SET_OPSGROUP self
-- @return #SET_OPSGROUP self
function SET_OPSGROUP:FilterStart()
if _DATABASE then
self:_FilterStart()
self:HandleEvent( EVENTS.Birth, self._EventOnBirth )
self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash )
self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash )
self:HandleEvent( EVENTS.RemoveUnit, self._EventOnDeadOrCrash )
end
return self
end
--- Activate late activated groups in the set.
-- @param #SET_OPSGROUP self
-- @param #number Delay Delay in seconds.
-- @return #SET_OPSGROUP self
function SET_OPSGROUP:Activate(Delay)
local Set = self:GetSet()
for GroupID, GroupData in pairs(Set) do
local group=GroupData --Ops.OpsGroup#OPSGROUP
if group and group:IsAlive()==false then
group:Activate(Delay)
end
end
return self
end
--- Handles the OnDead or OnCrash event for alive groups set.
-- Note: The GROUP object in the SET_OPSGROUP collection will only be removed if the last unit is destroyed of the GROUP.
-- @param #SET_OPSGROUP self
-- @param Core.Event#EVENTDATA Event
function SET_OPSGROUP:_EventOnDeadOrCrash( Event )
self:F( { Event } )
if Event.IniDCSUnit then
local ObjectName, Object = self:FindInDatabase( Event )
if ObjectName then
if Event.IniDCSGroup:getSize() == 1 then -- Only remove if the last unit of the group was destroyed.
self:Remove( ObjectName )
end
end
end
end
--- 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_OPSGROUP self
-- @param Core.Event#EVENTDATA Event
-- @return #string The name of the GROUP
-- @return #table The GROUP
function SET_OPSGROUP:AddInDatabase( Event )
if Event.IniObjectCategory==1 then
if not self.Database[Event.IniDCSGroupName] then
self.Database[Event.IniDCSGroupName] = GROUP:Register( Event.IniDCSGroupName )
end
end
return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName]
end
--- Handles the Database to check on any event that Object exists in the Database.
-- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa!
-- @param #SET_OPSGROUP self
-- @param Core.Event#EVENTDATA Event Event data table.
-- @return #string The name of the GROUP
-- @return #table The GROUP
function SET_OPSGROUP:FindInDatabase(Event)
return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName]
end
--- Iterate the set and call an iterator function for each OPSGROUP object.
-- @param #SET_OPSGROUP self
-- @param #function IteratorFunction The function that will be called for all OPSGROUPs in the set. **NOTE** that the function must have the OPSGROUP as first parameter!
-- @param ... (Optional) arguments passed to the `IteratorFunction`.
-- @return #SET_OPSGROUP self
function SET_OPSGROUP:ForEachGroup( IteratorFunction, ... )
self:ForEach(IteratorFunction, arg, self:GetSet())
return self
end
--- Check include object.
-- @param #SET_OPSGROUP self
-- @param Wrapper.Group#GROUP MGroup The group that is checked for inclusion.
-- @return #SET_OPSGROUP self
function SET_OPSGROUP:IsIncludeObject(MGroup)
-- Assume it is and check later if not.
local MGroupInclude=true
-- Filter active.
if self.Filter.Active~=nil then
local MGroupActive = false
if self.Filter.Active==false or (self.Filter.Active==true and MGroup:IsActive()==true) then
MGroupActive = true
end
MGroupInclude = MGroupInclude and MGroupActive
end
-- Filter coalitions.
if self.Filter.Coalitions then
local MGroupCoalition = false
for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do
if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName]==MGroup:GetCoalition() then
MGroupCoalition = true
end
end
MGroupInclude = MGroupInclude and MGroupCoalition
end
-- Filter categories.
if self.Filter.Categories then
local MGroupCategory = false
for CategoryID, CategoryName in pairs( self.Filter.Categories ) do
if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName]==MGroup:GetCategory() then
MGroupCategory = true
end
end
MGroupInclude = MGroupInclude and MGroupCategory
end
-- Filter countries.
if self.Filter.Countries then
local MGroupCountry = false
for CountryID, CountryName in pairs( self.Filter.Countries ) do
if country.id[CountryName] == MGroup:GetCountry() then
MGroupCountry = true
end
end
MGroupInclude = MGroupInclude and MGroupCountry
end
-- Filter "prefixes".
if self.Filter.GroupPrefixes 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 "%-" ?!
MGroupPrefix = true
end
end
MGroupInclude = MGroupInclude and MGroupPrefix
end
return MGroupInclude
end
end

View File

@ -236,6 +236,7 @@ do -- SETTINGS
--- SETTINGS constructor.
-- @param #SETTINGS self
-- @param #string PlayerName (Optional) Set settings for this player.
-- @return #SETTINGS
function SETTINGS:Set( PlayerName )

View File

@ -1718,27 +1718,29 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT
self:T(string.format("Group %s is spawned on farp/ship/runway %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName()))
nfree=SpawnAirbase:GetFreeParkingSpotsNumber(termtype, true)
spots=SpawnAirbase:GetFreeParkingSpotsTable(termtype, true)
--[[
elseif Parkingdata~=nil then
-- Parking data explicitly set by user as input parameter.
nfree=#Parkingdata
spots=Parkingdata
]]
else
if ishelo then
if termtype==nil then
-- Helo is spawned. Try exclusive helo spots first.
self:T(string.format("Helo group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), AIRBASE.TerminalType.HelicopterOnly))
spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.HelicopterOnly, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits)
spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.HelicopterOnly, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, Parkingdata)
nfree=#spots
if nfree<nunits then
-- Not enough helo ports. Let's try also other terminal types.
self:T(string.format("Helo group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), AIRBASE.TerminalType.HelicopterUsable))
spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.HelicopterUsable, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits)
spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.HelicopterUsable, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, Parkingdata)
nfree=#spots
end
else
-- No terminal type specified. We try all spots except shelters.
self:T(string.format("Helo group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), termtype))
spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, termtype, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits)
spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, termtype, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, Parkingdata)
nfree=#spots
end
else
@ -1747,29 +1749,30 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT
if isbomber or istransport or istanker or isawacs then
-- First we fill the potentially bigger spots.
self:T(string.format("Transport/bomber group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), AIRBASE.TerminalType.OpenBig))
spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.OpenBig, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits)
spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.OpenBig, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, Parkingdata)
nfree=#spots
if nfree<nunits then
-- Now we try the smaller ones.
self:T(string.format("Transport/bomber group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), AIRBASE.TerminalType.OpenMedOrBig))
spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.OpenMedOrBig, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits)
spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.OpenMedOrBig, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, Parkingdata)
nfree=#spots
end
else
self:T(string.format("Fighter group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), AIRBASE.TerminalType.FighterAircraft))
spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.FighterAircraft, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits)
spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.FighterAircraft, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, Parkingdata)
nfree=#spots
end
else
-- Terminal type explicitly given.
self:T(string.format("Plane group %s is at %s using terminal type %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), tostring(termtype)))
spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, termtype, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits)
spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, termtype, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, Parkingdata)
nfree=#spots
end
end
end
-- Get parking data.
-- Debug: Get parking data.
--[[
local parkingdata=SpawnAirbase:GetParkingSpotsTable(termtype)
self:T(string.format("Parking at %s, terminal type %s:", SpawnAirbase:GetName(), tostring(termtype)))
for _,_spot in pairs(parkingdata) do
@ -1777,6 +1780,7 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT
SpawnAirbase:GetName(), _spot.TerminalID, _spot.TerminalType,tostring(_spot.Free),tostring(_spot.TOAC),_spot.TerminalID0,_spot.DistToRwy))
end
self:T(string.format("%s at %s: free parking spots = %d - number of units = %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), nfree, nunits))
]]
-- Set this to true if not enough spots are available for emergency air start.
local _notenough=false
@ -2144,27 +2148,29 @@ function SPAWN:ParkAircraft( SpawnAirbase, TerminalType, Parkingdata, SpawnIndex
self:T(string.format("Group %s is spawned on farp/ship/runway %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName()))
nfree=SpawnAirbase:GetFreeParkingSpotsNumber(termtype, true)
spots=SpawnAirbase:GetFreeParkingSpotsTable(termtype, true)
--[[
elseif Parkingdata~=nil then
-- Parking data explicitly set by user as input parameter.
nfree=#Parkingdata
spots=Parkingdata
]]
else
if ishelo then
if termtype==nil then
-- Helo is spawned. Try exclusive helo spots first.
self:T(string.format("Helo group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), AIRBASE.TerminalType.HelicopterOnly))
spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.HelicopterOnly, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits)
spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.HelicopterOnly, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, Parkingdata)
nfree=#spots
if nfree<nunits then
-- Not enough helo ports. Let's try also other terminal types.
self:T(string.format("Helo group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), AIRBASE.TerminalType.HelicopterUsable))
spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.HelicopterUsable, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits)
spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.HelicopterUsable, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, Parkingdata)
nfree=#spots
end
else
-- No terminal type specified. We try all spots except shelters.
self:T(string.format("Helo group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), termtype))
spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, termtype, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits)
spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, termtype, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, Parkingdata)
nfree=#spots
end
else
@ -2176,29 +2182,30 @@ function SPAWN:ParkAircraft( SpawnAirbase, TerminalType, Parkingdata, SpawnIndex
if isbomber or istransport then
-- First we fill the potentially bigger spots.
self:T(string.format("Transport/bomber group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), AIRBASE.TerminalType.OpenBig))
spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.OpenBig, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits)
spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.OpenBig, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, Parkingdata)
nfree=#spots
if nfree<nunits then
-- Now we try the smaller ones.
self:T(string.format("Transport/bomber group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), AIRBASE.TerminalType.OpenMedOrBig))
spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.OpenMedOrBig, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits)
spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.OpenMedOrBig, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, Parkingdata)
nfree=#spots
end
else
self:T(string.format("Fighter group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), AIRBASE.TerminalType.FighterAircraft))
spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.FighterAircraft, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits)
spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.FighterAircraft, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, Parkingdata)
nfree=#spots
end
else
-- Terminal type explicitly given.
self:T(string.format("Plane group %s is at %s using terminal type %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), tostring(termtype)))
spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, termtype, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits)
spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, termtype, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, Parkingdata)
nfree=#spots
end
end
end
-- Get parking data.
-- Debug: Get parking data.
--[[
local parkingdata=SpawnAirbase:GetParkingSpotsTable(termtype)
self:T2(string.format("Parking at %s, terminal type %s:", SpawnAirbase:GetName(), tostring(termtype)))
for _,_spot in pairs(parkingdata) do
@ -2206,6 +2213,7 @@ function SPAWN:ParkAircraft( SpawnAirbase, TerminalType, Parkingdata, SpawnIndex
SpawnAirbase:GetName(), _spot.TerminalID, _spot.TerminalType,tostring(_spot.Free),tostring(_spot.TOAC),_spot.TerminalID0,_spot.DistToRwy))
end
self:T(string.format("%s at %s: free parking spots = %d - number of units = %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), nfree, nunits))
]]
-- Set this to true if not enough spots are available for emergency air start.
local _notenough=false
@ -2415,8 +2423,7 @@ end
-- @param #SPAWN self
-- @param DCS#Vec3 Vec3 The Vec3 coordinates where to spawn the group.
-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone.
-- @return Wrapper.Group#GROUP that was spawned.
-- @return #nil Nothing was spawned.
-- @return Wrapper.Group#GROUP that was spawned or #nil if nothing was spawned.
function SPAWN:SpawnFromVec3( Vec3, SpawnIndex )
self:F( { self.SpawnTemplatePrefix, Vec3, SpawnIndex } )
@ -2485,8 +2492,7 @@ end
-- @param #SPAWN self
-- @param Core.Point#Coordinate Coordinate The Coordinate coordinates where to spawn the group.
-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone.
-- @return Wrapper.Group#GROUP that was spawned.
-- @return #nil Nothing was spawned.
-- @return Wrapper.Group#GROUP that was spawned or #nil if nothing was spawned.
function SPAWN:SpawnFromCoordinate( Coordinate, SpawnIndex )
self:F( { self.SpawnTemplatePrefix, SpawnIndex } )
@ -2502,8 +2508,7 @@ end
-- @param #SPAWN self
-- @param Core.Point#POINT_VEC3 PointVec3 The PointVec3 coordinates where to spawn the group.
-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone.
-- @return Wrapper.Group#GROUP that was spawned.
-- @return #nil Nothing was spawned.
-- @return Wrapper.Group#GROUP that was spawned or #nil if nothing was spawned.
-- @usage
--
-- local SpawnPointVec3 = ZONE:New( ZoneName ):GetPointVec3( 2000 ) -- Get the center of the ZONE object at 2000 meters from the ground.
@ -2527,8 +2532,7 @@ end
-- @param #number MinHeight (optional) The minimum height to spawn an airborne group into the zone.
-- @param #number MaxHeight (optional) The maximum height to spawn an airborne group into the zone.
-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone.
-- @return Wrapper.Group#GROUP that was spawned.
-- @return #nil Nothing was spawned.
-- @return Wrapper.Group#GROUP that was spawned or #nil if nothing was spawned.
-- @usage
--
-- local SpawnVec2 = ZONE:New( ZoneName ):GetVec2()
@ -2561,8 +2565,7 @@ end
-- @param #number MinHeight (optional) The minimum height to spawn an airborne group into the zone.
-- @param #number MaxHeight (optional) The maximum height to spawn an airborne group into the zone.
-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone.
-- @return Wrapper.Group#GROUP that was spawned.
-- @return #nil Nothing was spawned.
-- @return Wrapper.Group#GROUP that was spawned or #nil if nothing was spawned.
-- @usage
--
-- local SpawnPointVec2 = ZONE:New( ZoneName ):GetPointVec2()
@ -2618,8 +2621,7 @@ end
-- @param #number MinHeight (optional) The minimum height to spawn an airborne group into the zone.
-- @param #number MaxHeight (optional) The maximum height to spawn an airborne group into the zone.
-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone.
-- @return Wrapper.Group#GROUP that was spawned.
-- @return #nil Nothing was spawned.
-- @return Wrapper.Group#GROUP that was spawned or #nil if nothing was spawned.
-- @usage
--
-- local SpawnStatic = STATIC:FindByName( StaticName )
@ -2650,8 +2652,7 @@ end
-- @param #number MinHeight (optional) The minimum height to spawn an airborne group into the zone.
-- @param #number MaxHeight (optional) The maximum height to spawn an airborne group into the zone.
-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone.
-- @return Wrapper.Group#GROUP that was spawned.
-- @return #nil when nothing was spawned.
-- @return Wrapper.Group#GROUP that was spawned or #nil if nothing was spawned.
-- @usage
--
-- local SpawnZone = ZONE:New( ZoneName )
@ -3400,8 +3401,8 @@ function SPAWN:_SpawnCleanUpScheduler()
if Stamp.Vec2 then
if SpawnUnit:InAir() == false and SpawnUnit:GetVelocityKMH() < 1 then
local NewVec2 = SpawnUnit:GetVec2()
if Stamp.Vec2.x == NewVec2.x and Stamp.Vec2.y == NewVec2.y then
-- If the plane is not moving, and is on the ground, assign it with a timestamp...
if (Stamp.Vec2.x == NewVec2.x and Stamp.Vec2.y == NewVec2.y) or (SpawnUnit:GetLife() <= 1) then
-- If the plane is not moving or dead , and is on the ground, assign it with a timestamp...
if Stamp.Time + self.SpawnCleanUpInterval < timer.getTime() then
self:T( { "CleanUp Scheduler:", "ReSpawning:", SpawnGroup:GetName() } )
self:ReSpawn( SpawnCursor )
@ -3419,7 +3420,7 @@ function SPAWN:_SpawnCleanUpScheduler()
else
if SpawnUnit:InAir() == false then
Stamp.Vec2 = SpawnUnit:GetVec2()
if SpawnUnit:GetVelocityKMH() < 1 then
if (SpawnUnit:GetVelocityKMH() < 1) then
Stamp.Time = timer.getTime()
end
else

View File

@ -241,6 +241,20 @@ function SPAWNSTATIC:InitShape(StaticShape)
return self
end
--- Initialize parameters for spawning FARPs.
-- @param #SPAWNSTATIC self
-- @param #number CallsignID Callsign ID. Default 1 (="London").
-- @param #number Frequency Frequency in MHz. Default 127.5 MHz.
-- @param #number Modulation Modulation 0=AM, 1=FM.
-- @return #SPAWNSTATIC self
function SPAWNSTATIC:InitFARP(CallsignID, Frequency, Modulation)
self.InitFarp=true
self.InitFarpCallsignID=CallsignID or 1
self.InitFarpFreq=Frequency or 127.5
self.InitFarpModu=Modulation or 0
return self
end
--- Initialize cargo mass.
-- @param #SPAWNSTATIC self
-- @param #number Mass Mass of the cargo in kg.
@ -420,22 +434,49 @@ function SPAWNSTATIC:_SpawnStatic(Template, CountryID)
Template.offsets.angle=self.InitOffsetAngle and math.rad(self.InitOffsetAngle) or 0
end
if self.InitFarp then
Template.heliport_callsign_id = self.InitFarpCallsignID
Template.heliport_frequency = self.InitFarpFreq
Template.heliport_modulation = self.InitFarpModu
Template.unitId=nil
end
-- Increase spawn index counter.
self.SpawnIndex = self.SpawnIndex + 1
-- Name of the spawned static.
Template.name = self.InitStaticName or string.format("%s#%05d", self.SpawnTemplatePrefix, self.SpawnIndex)
-- Register the new static.
--_DATABASE:_RegisterStaticTemplate(Template, self.CoalitionID, self.CategoryID, CountryID)
_DATABASE:AddStatic(Template.name)
-- Add and register the new static.
local mystatic=_DATABASE:AddStatic(Template.name)
-- Debug output.
self:T(Template)
-- Add static to the game.
local Static=coalition.addStaticObject(CountryID, Template)
local Static=nil
if self.InitFarp then
return _DATABASE:FindStatic(Static:getName())
local TemplateGroup={}
TemplateGroup.units={}
TemplateGroup.units[1]=Template
TemplateGroup.visible=true
TemplateGroup.hidden=false
TemplateGroup.x=Template.x
TemplateGroup.y=Template.y
TemplateGroup.name=Template.name
self:T("Spawning FARP")
self:T({Template=Template})
self:T({TemplateGroup=TemplateGroup})
-- ED's dirty way to spawn FARPS.
Static=coalition.addGroup(CountryID, -1, TemplateGroup)
else
Static=coalition.addStaticObject(CountryID, Template)
end
return mystatic
end

File diff suppressed because it is too large Load Diff

View File

@ -295,6 +295,17 @@ do -- country
-- @field QATAR
-- @field OMAN
-- @field UNITED_ARAB_EMIRATES
-- @field SOUTH_AFRICA
-- @field CUBA
-- @field PORTUGAL
-- @field GDR
-- @field LEBANON
-- @field CJTF_BLUE
-- @field CJTF_RED
-- @field UN_PEACEKEEPERS
-- @field Argentinia
-- @field Cyprus
-- @field Slovenia
country = {} --#country
@ -460,6 +471,22 @@ do -- Types
--@field #boolean lateActivated
--@field #boolean uncontrolled
--- DCS template data structure.
-- @type Template
-- @field #boolean uncontrolled Aircraft is uncontrolled.
-- @field #boolean lateActivation Group is late activated.
-- @field #number x 2D Position on x-axis in meters.
-- @field #number y 2D Position on y-axis in meters.
-- @field #table units Unit list.
--
--- Unit data structure.
--@type Template.Unit
--@field #string name Name of the unit.
--@field #number x
--@field #number y
--@field #number alt
end --
@ -1152,6 +1179,10 @@ do -- Unit
-- @param #Unit self
-- @return #Unit.Desc
--- GROUND - Switch on/off radar emissions
-- @function [parent=#Unit] enableEmission
-- @param #Unit self
-- @param #boolean switch
Unit = {} --#Unit
@ -1222,7 +1253,7 @@ do -- Group
-- @param #Group self
-- @return #number
--- Returns initial size of the group. If some of the units will be destroyed, initial size of the group will not be changed. Initial size limits the unitNumber parameter for Group.getUnit() function.
--- Returns initial size of the group. If some of the units will be destroyed, initial size of the group will not be changed; Initial size limits the unitNumber parameter for Group.getUnit() function.
-- @function [parent=#Group] getInitialSize
-- @param #Group self
-- @return #number
@ -1237,6 +1268,11 @@ do -- Group
-- @param #Group self
-- @return #Controller
--- GROUND - Switch on/off radar emissions
-- @function [parent=#Group] enableEmission
-- @param #Group self
-- @param #boolean switch
Group = {} --#Group
end -- Group
@ -1452,4 +1488,4 @@ do -- AI
AI = {} --#AI
end -- AI
end -- AI

View File

@ -59,7 +59,13 @@ function ATC_GROUND:New( Airbases, AirbaseList )
for AirbaseID, Airbase in pairs( self.Airbases ) do
Airbase.ZoneBoundary = _DATABASE:FindAirbase( AirbaseID ):GetZone()
-- Specified ZoneBoundary is used if setted or Airbase radius by default
if Airbase.ZoneBoundary then
Airbase.ZoneBoundary = ZONE_POLYGON_BASE:New( "Boundary " .. AirbaseID, Airbase.ZoneBoundary )
else
Airbase.ZoneBoundary = _DATABASE:FindAirbase( AirbaseID ):GetZone()
end
Airbase.ZoneRunways = {}
for PointsRunwayID, PointsRunway in pairs( Airbase.PointsRunways ) do
Airbase.ZoneRunways[PointsRunwayID] = ZONE_POLYGON_BASE:New( "Runway " .. PointsRunwayID, PointsRunway )
@ -3263,5 +3269,310 @@ function ATC_GROUND_PERSIANGULF:Start( RepeatScanSeconds )
end
--- @type ATC_GROUND_MARIANAISLANDS
-- @extends #ATC_GROUND
--- # ATC\_GROUND\_MARIANA, extends @{#ATC_GROUND}
--
-- The ATC\_GROUND\_MARIANA class monitors the speed of the airplanes at the airbase during taxi.
-- The pilots may not drive faster than the maximum speed for the airbase, or they will be despawned.
--
-- ---
--
-- ![Banner Image](..\Presentations\ATC_GROUND\Dia1.JPG)
--
-- ---
--
-- The default maximum speed for the airbases at Persian Gulf is **50 km/h**. Warnings are given if this speed limit is trespassed.
-- Players will be immediately kicked when driving faster than **150 km/h** on the taxi way.
--
-- The ATC\_GROUND\_MARIANA class monitors the speed of the airplanes at the airbase during taxi.
-- The pilots may not drive faster than the maximum speed for the airbase, or they will be despawned.
--
-- The pilot will receive 3 times a warning during speeding. After the 3rd warning, if the pilot is still driving
-- faster than the maximum allowed speed, the pilot will be kicked.
--
-- Different airbases have different maximum speeds, according safety regulations.
--
-- # Airbases monitored
--
-- The following airbases are monitored at the Mariana Island region.
-- Use the @{Wrapper.Airbase#AIRBASE.MarianaIslands} enumeration to select the airbases to be monitored.
--
-- * AIRBASE.MarianaIslands.Rota_Intl
-- * AIRBASE.MarianaIslands.Andersen_AFB
-- * AIRBASE.MarianaIslands.Antonio_B_Won_Pat_Intl
-- * AIRBASE.MarianaIslands.Saipan_Intl
-- * AIRBASE.MarianaIslands.Tinian_Intl
-- * AIRBASE.MarianaIslands.Olf_Orote
--
-- # Installation
--
-- ## In Single Player Missions
--
-- ATC\_GROUND is fully functional in single player.
--
-- ## In Multi Player Missions
--
-- ATC\_GROUND is functional in multi player, however ...
--
-- Due to a bug in DCS since release 1.5, the despawning of clients are not anymore working in multi player.
-- To **work around this problem**, a much better solution has been made, using the **slot blocker** script designed
-- by Ciribob.
--
-- With the help of __Ciribob__, this script has been extended to also kick client players while in flight.
-- ATC\_GROUND is communicating with this modified script to kick players!
--
-- Install the file **SimpleSlotBlockGameGUI.lua** on the server, following the installation instructions described by Ciribob.
--
-- [Simple Slot Blocker from Ciribob & FlightControl](https://github.com/ciribob/DCS-SimpleSlotBlock)
--
-- # Script it!
--
-- ## 1. ATC_GROUND_MARIANAISLANDS Constructor
--
-- Creates a new ATC_GROUND_MARIANAISLANDS object that will monitor pilots taxiing behaviour.
--
-- -- This creates a new ATC_GROUND_MARIANAISLANDS object.
--
-- -- Monitor for these clients the airbases.
-- AirbasePoliceCaucasus = ATC_GROUND_MARIANAISLANDS:New()
--
-- ATC_Ground = ATC_GROUND_MARIANAISLANDS:New(
-- { AIRBASE.MarianaIslands.Andersen_AFB,
-- AIRBASE.MarianaIslands.Saipan_Intl
-- }
-- )
--
--
-- ## 2. Set various options
--
-- There are various methods that you can use to tweak the behaviour of the ATC\_GROUND classes.
--
-- ### 2.1 Speed limit at an airbase.
--
-- * @{#ATC_GROUND.SetKickSpeed}(): Set the speed limit allowed at an airbase in meters per second.
-- * @{#ATC_GROUND.SetKickSpeedKmph}(): Set the speed limit allowed at an airbase in kilometers per hour.
-- * @{#ATC_GROUND.SetKickSpeedMiph}(): Set the speed limit allowed at an airbase in miles per hour.
--
-- ### 2.2 Prevent Takeoff at an airbase. Players will be kicked immediately.
--
-- * @{#ATC_GROUND.SetMaximumKickSpeed}(): Set the maximum speed allowed at an airbase in meters per second.
-- * @{#ATC_GROUND.SetMaximumKickSpeedKmph}(): Set the maximum speed allowed at an airbase in kilometers per hour.
-- * @{#ATC_GROUND.SetMaximumKickSpeedMiph}(): Set the maximum speed allowed at an airbase in miles per hour.
--
---- @field #ATC_GROUND_MARIANAISLANDS
ATC_GROUND_MARIANAISLANDS = {
ClassName = "ATC_GROUND_MARIANAISLANDS",
Airbases = {
[AIRBASE.MarianaIslands.Andersen_AFB] = {
ZoneBoundary = {
[1]={["y"]=16534.138036037,["x"]=11357.42159178,},
[2]={["y"]=16193.406442738,["x"]=12080.012957533,},
[3]={["y"]=13846.966851869,["x"]=12017.348398727,},
[4]={["y"]=13085.815989171,["x"]=11686.317876875,},
[5]={["y"]=13157.991797443,["x"]=11307.826209991,},
[6]={["y"]=12055.725179065,["x"]=10795.955695916,},
[7]={["y"]=12762.455491112,["x"]=8890.9830441032,},
[8]={["y"]=15955.829493693,["x"]=10333.527220132,},
[9]={["y"]=16537.500532414,["x"]=11302.009499603,},
},
PointsRunways = {
[1]={
[1]={["y"]=12586.683049611,["x"]=10224.374497932,},
[2]={["y"]=16191.720475696,["x"]=11791.299100017,},
[3]={["y"]=16126.93956642,["x"]=11938.855615591,},
[4]={["y"]=12520.758127164,["x"]=10385.177131701,},
[5]={["y"]=12584.654720512,["x"]=10227.416991581,},
},
[2]={
[1]={["y"]=12663.030391743,["x"]=9661.9623015306,},
[2]={["y"]=16478.347303358,["x"]=11328.665745976,},
[3]={["y"]=16405.4731048,["x"]=11479.11570429,},
[4]={["y"]=12597.277684174,["x"]=9817.9733769647,},
[5]={["y"]=12661.894752524,["x"]=9674.4462086962,},
},
},
},
[AIRBASE.MarianaIslands.Antonio_B_Won_Pat_Intl] = {
ZoneBoundary = {
[1]={["y"]=2288.5182403943,["x"]=1469.0170841716,},
[2]={["y"]=1126.2025877996,["x"]=1174.37135631,},
[3]={["y"]=-2015.6461924287,["x"]=-484.62000718931,},
[4]={["y"]=-2102.1292389114,["x"]=-988.03393750566,},
[5]={["y"]=476.03853524366,["x"]=-1220.1783269883,},
[6]={["y"]=2059.2220058047,["x"]=78.889693514402,},
[7]={["y"]=1898.1396965104,["x"]=705.67531284795,},
[8]={["y"]=2760.1768681934,["x"]=1026.0681119777,},
[9]={["y"]=2317.2278959994,["x"]=1460.8143254273,},
},
PointsRunways = {
[1]={
[1]={["y"]=-1872.6620108821,["x"]=-924.3572605835,},
[2]={["y"]=1763.4754603305,["x"]=735.35988877983,},
[3]={["y"]=1700.6941677961,["x"]=866.32615476157,},
[4]={["y"]=-1934.0078007732,["x"]=-779.8149298453,},
[5]={["y"]=-1875.0113982627,["x"]=-914.95971106094,},
},
[2]={
[1]={["y"]=-1512.9403660377,["x"]=-1005.5903386188,},
[2]={["y"]=1577.9055714735,["x"]=413.22750176368,},
[3]={["y"]=1523.1182807849,["x"]=543.89726442232,},
[4]={["y"]=-1572.5102998047,["x"]=-867.04004322806,},
[5]={["y"]=-1514.2790162347,["x"]=-1003.5823633233,},
},
},
},
[AIRBASE.MarianaIslands.Rota_Intl] = {
ZoneBoundary = {
[1]={["y"]=47237.615412849,["x"]=76048.890408862,},
[2]={["y"]=49938.030053628,["x"]=75921.721582932,},
[3]={["y"]=49931.24873272,["x"]=75735.184004851,},
[4]={["y"]=49295.999227075,["x"]=75754.716414519,},
[5]={["y"]=49286.963307515,["x"]=75510.037806569,},
[6]={["y"]=48774.280745707,["x"]=75513.331990155,},
[7]={["y"]=48785.021396773,["x"]=75795.691662161,},
[8]={["y"]=47232.749278491,["x"]=75839.239059146,},
[9]={["y"]=47236.687866223,["x"]=76042.706764692,},
},
PointsRunways = {
[1]={
[1]={["y"]=49741.295228062,["x"]=75901.50955922,},
[2]={["y"]=49739.033213305,["x"]=75768.333440425,},
[3]={["y"]=47448.460520408,["x"]=75857.400271466,},
[4]={["y"]=47452.270177742,["x"]=75999.965448133,},
[5]={["y"]=49738.502011054,["x"]=75905.338915708,},
},
},
},
[AIRBASE.MarianaIslands.Saipan_Intl] = {
ZoneBoundary = {
[1]={["y"]=100489.08491445,["x"]=179799.05158855,},
[2]={["y"]=100869.73415313,["x"]=179948.98719903,},
[3]={["y"]=101364.78967515,["x"]=180831.98517043,},
[4]={["y"]=101563.85713359,["x"]=180885.21496237,},
[5]={["y"]=101733.92591034,["x"]=180457.73296886,},
[6]={["y"]=103340.30228775,["x"]=180990.08362622,},
[7]={["y"]=103459.55080438,["x"]=180453.77747027,},
[8]={["y"]=100406.63048095,["x"]=179266.60983762,},
[9]={["y"]=100225.55027532,["x"]=179423.9380961,},
[10]={["y"]=100477.48558937,["x"]=179791.9827288,},
},
PointsRunways = {
[1]={
[1]={["y"]=103170.38882002,["x"]=180654.56630524,},
[2]={["y"]=103235.37868835,["x"]=180497.25368418,},
[3]={["y"]=100564.72969504,["x"]=179435.41443498,},
[4]={["y"]=100509.30718722,["x"]=179584.65394733,},
[5]={["y"]=103163.53918905,["x"]=180651.82645285,},
},
[2]={
[1]={["y"]=103048.83223261,["x"]=180819.94107128,},
[2]={["y"]=103087.60579257,["x"]=180720.06315265,},
[3]={["y"]=101037.52694966,["x"]=179899.50061624,},
[4]={["y"]=100994.61708907,["x"]=180009.33151758,},
[5]={["y"]=103043.26643227,["x"]=180820.40488798,},
},
},
},
[AIRBASE.MarianaIslands.Tinian_Intl] = {
ZoneBoundary = {
[1]={["y"]=88393.477575413,["x"]=166704.16076438,},
[2]={["y"]=91581.732441809,["x"]=167402.54409276,},
[3]={["y"]=91533.451647402,["x"]=166826.23670062,},
[4]={["y"]=90827.604136952,["x"]=166699.75590414,},
[5]={["y"]=90894.853975623,["x"]=166375.37836304,},
[6]={["y"]=89995.027922869,["x"]=166224.92495935,},
[7]={["y"]=88937.62899352,["x"]=166244.48573911,},
[8]={["y"]=88408.916178231,["x"]=166480.39896864,},
[9]={["y"]=88387.745481732,["x"]=166685.82715656,},
},
PointsRunways = {
[1]={
[1]={["y"]=91329.480937912,["x"]=167204.44064529,},
[2]={["y"]=91363.95475433,["x"]=167038.15603429,},
[3]={["y"]=88585.849307337,["x"]=166520.3807647,},
[4]={["y"]=88554.422227212,["x"]=166686.49505251,},
[5]={["y"]=91318.8152578,["x"]=167203.31794212,},
},
},
},
},
}
--- Creates a new ATC_GROUND_MARIANAISLANDS object.
-- @param #ATC_GROUND_MARIANAISLANDS self
-- @param AirbaseNames A list {} of airbase names (Use AIRBASE.MarianaIslands enumerator).
-- @return #ATC_GROUND_MARIANAISLANDS self
function ATC_GROUND_MARIANAISLANDS:New( AirbaseNames )
-- Inherits from BASE
local self = BASE:Inherit( self, ATC_GROUND:New( self.Airbases, AirbaseNames ) )
self:SetKickSpeedKmph( 50 )
self:SetMaximumKickSpeedKmph( 150 )
-- -- Andersen
-- local AndersenBoundary = GROUP:FindByName( "Andersen Boundary" )
-- self.Airbases[AIRBASE.MarianaIslands.Andersen_AFB].ZoneBoundary = ZONE_POLYGON:New( "Andersen Boundary", AndersenBoundary ):SmokeZone(SMOKECOLOR.White):Flush()
--
-- local AndersenRunway1 = GROUP:FindByName( "Andersen Runway 1" )
-- self.Airbases[AIRBASE.MarianaIslands.Andersen_AFB].ZoneRunways[1] = ZONE_POLYGON:New( "Andersen Runway 1", AndersenRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
--
-- local AndersenRunway2 = GROUP:FindByName( "Andersen Runway 2" )
-- self.Airbases[AIRBASE.MarianaIslands.Andersen_AFB].ZoneRunways[2] = ZONE_POLYGON:New( "Andersen Runway 2", AndersenRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush()
--
--
-- -- Antonio_B_Won_Pat_International_Airport
-- local AntonioBoundary = GROUP:FindByName( "Antonio Boundary" )
-- self.Airbases[AIRBASE.MarianaIslands.Antonio_B_Won_Pat_Intl].ZoneBoundary = ZONE_POLYGON:New( "Antonio Boundary", AntonioBoundary ):SmokeZone(SMOKECOLOR.White):Flush()
--
-- local AntonioRunway1 = GROUP:FindByName( "Antonio Runway 1" )
-- self.Airbases[AIRBASE.MarianaIslands.Antonio_B_Won_Pat_Intl].ZoneRunways[1] = ZONE_POLYGON:New( "Antonio Runway 1", AntonioRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
--
-- local AntonioRunway2 = GROUP:FindByName( "Antonio Runway 2" )
-- self.Airbases[AIRBASE.MarianaIslands.Antonio_B_Won_Pat_Intl].ZoneRunways[2] = ZONE_POLYGON:New( "Antonio Runway 2", AntonioRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush()
--
--
-- -- Rota_International_Airport
-- local RotaBoundary = GROUP:FindByName( "Rota Boundary" )
-- self.Airbases[AIRBASE.MarianaIslands.Rota_Intl].ZoneBoundary = ZONE_POLYGON:New( "Rota Boundary", RotaBoundary ):SmokeZone(SMOKECOLOR.White):Flush()
--
-- local RotaRunway1 = GROUP:FindByName( "Rota Runway 1" )
-- self.Airbases[AIRBASE.MarianaIslands.Rota_Intl].ZoneRunways[1] = ZONE_POLYGON:New( "Rota Runway 1", RotaRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
--
--
-- -- Saipan_International_Airport
-- local SaipanBoundary = GROUP:FindByName( "Saipan Boundary" )
-- self.Airbases[AIRBASE.MarianaIslands.Saipan_Intl].ZoneBoundary = ZONE_POLYGON:New( "Saipan Boundary", SaipanBoundary ):SmokeZone(SMOKECOLOR.White):Flush()
--
-- local SaipanRunway1 = GROUP:FindByName( "Saipan Runway 1" )
-- self.Airbases[AIRBASE.MarianaIslands.Saipan_Intl].ZoneRunways[1] = ZONE_POLYGON:New( "Saipan Runway 1", SaipanRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
--
-- local SaipanRunway2 = GROUP:FindByName( "Saipan Runway 2" )
-- self.Airbases[AIRBASE.MarianaIslands.Saipan_Intl].ZoneRunways[2] = ZONE_POLYGON:New( "Saipan Runway 2", SaipanRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush()
--
--
-- -- Tinian_International_Airport
-- local TinianBoundary = GROUP:FindByName( "Tinian Boundary" )
-- self.Airbases[AIRBASE.MarianaIslands.Tinian_Intl].ZoneBoundary = ZONE_POLYGON:New( "Tinian Boundary", TinianBoundary ):SmokeZone(SMOKECOLOR.White):Flush()
--
-- local TinianRunway1 = GROUP:FindByName( "Tinian Runway 1" )
-- self.Airbases[AIRBASE.MarianaIslands.Tinian_Intl].ZoneRunways[1] = ZONE_POLYGON:New( "Tinian Runway 1", TinianRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
return self
end
--- Start SCHEDULER for ATC_GROUND_MARIANAISLANDS object.
-- @param #ATC_GROUND_MARIANAISLANDS self
-- @param RepeatScanSeconds Time in second for defining occurency of alerts.
-- @return nothing
function ATC_GROUND_MARIANAISLANDS:Start( RepeatScanSeconds )
RepeatScanSeconds = RepeatScanSeconds or 0.05
self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, RepeatScanSeconds )
end

View File

@ -693,7 +693,7 @@ ARTY.db={
--- Arty script version.
-- @field #string version
ARTY.version="1.1.8"
ARTY.version="1.2.0"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
@ -1146,12 +1146,11 @@ end
-- @param alias (Optional) Alias name the group will be calling itself when sending messages. Default is the group name.
-- @return #ARTY ARTY object or nil if group does not exist or is not a ground or naval group.
function ARTY:NewFromCargoGroup(cargogroup, alias)
BASE:F2({cargogroup=cargogroup, alias=alias})
if cargogroup then
BASE:T(self.lid..string.format("ARTY script version %s. Added CARGO group %s.", ARTY.version, cargogroup:GetName()))
BASE:T(string.format("ARTY script version %s. Added CARGO group %s.", ARTY.version, cargogroup:GetName()))
else
BASE:E(self.lid.."ERROR: Requested ARTY CARGO GROUP does not exist! (Has to be a MOOSE CARGO(!) group.)")
BASE:E("ERROR: Requested ARTY CARGO GROUP does not exist! (Has to be a MOOSE CARGO(!) group.)")
return nil
end
@ -2766,38 +2765,32 @@ function ARTY:onafterStatus(Controllable, From, Event, To)
self:_EventFromTo("onafterStatus", Event, From, To)
-- Get ammo.
local ntot, nshells, nrockets, nmissiles=self:GetAmmo()
local nammo, nshells, nrockets, nmissiles=self:GetAmmo()
-- We have a cargo group ==> check if group was loaded into a carrier.
if self.iscargo and self.cargogroup then
if self.cargogroup:IsLoaded() and not self:is("InTransit") then
-- Group is now InTransit state. Current target is canceled.
self:T(self.lid..string.format("Group %s has been loaded into a carrier and is now transported.", self.alias))
self:Loaded()
elseif self.cargogroup:IsUnLoaded() then
-- Group has been unloaded and is combat ready again.
self:T(self.lid..string.format("Group %s has been unloaded from the carrier.", self.alias))
self:UnLoaded()
end
end
-- FSM state.
local fsmstate=self:GetState()
self:T(self.lid..string.format("Status %s, Ammo total=%d: shells=%d [smoke=%d, illu=%d, nukes=%d*%.3f kT], rockets=%d, missiles=%d", fsmstate, ntot, nshells, self.Nsmoke, self.Nillu, self.Nukes, self.nukewarhead/1000000, nrockets, nmissiles))
self:T(self.lid..string.format("Status %s, Ammo total=%d: shells=%d [smoke=%d, illu=%d, nukes=%d*%.3f kT], rockets=%d, missiles=%d", fsmstate, nammo, nshells, self.Nsmoke, self.Nillu, self.Nukes, self.nukewarhead/1000000, nrockets, nmissiles))
if self.Controllable and self.Controllable:IsAlive() then
-- We have a cargo group ==> check if group was loaded into a carrier.
if self.cargogroup then
if self.cargogroup:IsLoaded() and not self:is("InTransit") then
-- Group is now InTransit state. Current target is canceled.
self:T(self.lid..string.format("Group %s has been loaded into a carrier and is now transported.", self.alias))
self:Loaded()
elseif self.cargogroup:IsUnLoaded() then
-- Group has been unloaded and is combat ready again.
self:T(self.lid..string.format("Group %s has been unloaded from the carrier.", self.alias))
self:UnLoaded()
end
end
-- Debug current status info.
if self.Debug then
self:_StatusReport()
end
-- Group is being transported as cargo ==> skip everything and check again in 5 seconds.
if self:is("InTransit") then
self:__Status(-5)
return
end
-- Group on the move.
if self:is("Moving") then
self:T2(self.lid..string.format("%s: Moving", Controllable:GetName()))
@ -2884,7 +2877,7 @@ function ARTY:onafterStatus(Controllable, From, Event, To)
end
-- Get ammo.
local nammo, nshells, nrockets, nmissiles=self:GetAmmo()
--local nammo, nshells, nrockets, nmissiles=self:GetAmmo()
-- Check if we have a target in the queue for which weapons are still available.
local gotsome=false
@ -2914,9 +2907,23 @@ function ARTY:onafterStatus(Controllable, From, Event, To)
-- Call status again in ~10 sec.
self:__Status(self.StatusInterval)
elseif self.iscargo then
-- We have a cargo group ==> check if group was loaded into a carrier.
if self.cargogroup and self.cargogroup:IsAlive() then
-- Group is being transported as cargo ==> skip everything and check again in 5 seconds.
if self:is("InTransit") then
self:__Status(-5)
end
end
else
self:E(self.lid..string.format("Arty group %s is not alive!", self.groupname))
end
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

View File

@ -1001,7 +1001,13 @@ do -- DESIGNATE
local ID = self.Detection:GetDetectedItemID( DetectedItem )
local MenuText = ID --.. ", " .. Coord:ToStringA2G( AttackGroup )
MenuText = string.format( "(%3s) %s", Designating, MenuText )
-- Use injected MenuName from TaskA2GDispatcher if using same Detection Object
if DetectedItem.DesignateMenuName then
MenuText = string.format( "(%3s) %s", Designating, DetectedItem.DesignateMenuName )
else
MenuText = string.format( "(%3s) %s", Designating, MenuText )
end
local DetectedMenu = MENU_GROUP_DELAYED:New( AttackGroup, MenuText, MenuDesignate ):SetTime( MenuTime ):SetTag( self.DesignateName )
-- Build the Lasing menu.

File diff suppressed because it is too large Load Diff

View File

@ -5786,6 +5786,8 @@ end
-- If desired, the @{#RATMANAGER} can be stopped by the @{#RATMANAGER.Stop}(stoptime) function. The parameter "stoptime" specifies the time delay in seconds after which the manager stops.
-- When this happens, no new aircraft will be spawned and the population will eventually decrease to zero.
--
-- When you are using a time intervall like @{#RATMANAGER.dTspawn}(delay), @{#RATMANAGER} will ignore the amount set with @{#RATMANAGER.New}(). @{#RATMANAGER.dTspawn}(delay) will spawn infinite groups.
--
-- ## Example
-- In this example, three different @{#RAT} objects are created (but not spawned manually). The @{#RATMANAGER} takes care that at least five aircraft of each type are alive and that the total number of aircraft
-- spawned is 25. The @{#RATMANAGER} is started after 30 seconds and stopped after two hours.

View File

@ -36,7 +36,7 @@
--
-- ===
--
-- ## Sound files: Check out the pinned messages in the Moose discord *#func-range* channel.
-- ## Sound files: [MOOSE Sound Files](https://github.com/FlightControl-Master/MOOSE_SOUND/releases)
--
-- ===
--
@ -53,6 +53,7 @@
-- @type RANGE
-- @field #string ClassName Name of the Class.
-- @field #boolean Debug If true, debug info is send as messages on the screen.
-- @field #boolean verbose Verbosity level. Higher means more output to DCS log file.
-- @field #string id String id of range for output in DCS log.
-- @field #string rangename Name of the range.
-- @field Core.Point#COORDINATE location Coordinate of the range location.
@ -90,9 +91,9 @@
-- @field #boolean defaultsmokebomb If true, initialize player settings to smoke bomb.
-- @field #boolean autosave If true, automatically save results every X seconds.
-- @field #number instructorfreq Frequency on which the range control transmitts.
-- @field Core.RadioQueue#RADIOQUEUE instructor Instructor radio queue.
-- @field Sound.RadioQueue#RADIOQUEUE instructor Instructor radio queue.
-- @field #number rangecontrolfreq Frequency on which the range control transmitts.
-- @field Core.RadioQueue#RADIOQUEUE rangecontrol Range control radio queue.
-- @field Sound.RadioQueue#RADIOQUEUE rangecontrol Range control radio queue.
-- @field #string rangecontrolrelayname Name of relay unit.
-- @field #string instructorrelayname Name of relay unit.
-- @field #string soundpath Path inside miz file where the sound files are located. Default is "Range Soundfiles/".
@ -289,6 +290,7 @@
RANGE={
ClassName = "RANGE",
Debug = false,
verbose = 0,
id = nil,
rangename = nil,
location = nil,
@ -518,7 +520,7 @@ RANGE.MenuF10Root=nil
--- Range script version.
-- @field #string version
RANGE.version="2.2.3"
RANGE.version="2.3.0"
--TODO list:
--TODO: Verbosity level for messages.
@ -556,7 +558,6 @@ function RANGE:New(rangename)
-- Debug info.
local text=string.format("Script version %s - creating new RANGE object %s.", RANGE.version, self.rangename)
self:I(self.id..text)
MESSAGE:New(text, 10):ToAllIf(self.Debug)
-- Defaults
self:SetDefaultPlayerSmokeBomb()
@ -723,7 +724,6 @@ function RANGE:onafterStart()
-- Starting range.
local text=string.format("Starting RANGE %s. Number of strafe targets = %d. Number of bomb targets = %d.", self.rangename, self.nstrafetargets, self.nbombtargets)
self:I(self.id..text)
MESSAGE:New(text,10):ToAllIf(self.Debug)
-- Event handling.
if self.eventmoose then
@ -757,6 +757,7 @@ function RANGE:onafterStart()
-- Radio queue.
self.rangecontrol=RADIOQUEUE:New(self.rangecontrolfreq, nil, self.rangename)
self.rangecontrol.schedonce=true
-- Init numbers.
self.rangecontrol:SetDigit(0, RANGE.Sound.RC0.filename, RANGE.Sound.RC0.duration, self.soundpath)
@ -781,7 +782,8 @@ function RANGE:onafterStart()
if self.instructorfreq then
-- Radio queue.
self.instructor=RADIOQUEUE:New(self.instructorfreq, nil, self.rangename)
self.instructor=RADIOQUEUE:New(self.instructorfreq, nil, self.rangename)
self.instructor.schedonce=true
-- Init numbers.
self.instructor:SetDigit(0, RANGE.Sound.IR0.filename, RANGE.Sound.IR0.duration, self.soundpath)
@ -1148,7 +1150,6 @@ function RANGE:AddStrafePit(targetnames, boxlength, boxwidth, heading, inversehe
-- Neither unit nor static object with this name could be found.
local text=string.format("ERROR! Could not find ANY strafe target object with name %s.", _name)
self:E(self.id..text)
MESSAGE:New(text, 10):ToAllIf(self.Debug)
end
@ -1168,7 +1169,6 @@ function RANGE:AddStrafePit(targetnames, boxlength, boxwidth, heading, inversehe
if ntargets==0 then
local text=string.format("ERROR! No strafe target could be found when calling RANGE:AddStrafePit() for range %s", self.rangename)
self:E(self.id..text)
MESSAGE:New(text, 10):ToAllIf(self.Debug)
return
end
@ -1238,7 +1238,6 @@ function RANGE:AddStrafePit(targetnames, boxlength, boxwidth, heading, inversehe
-- Debug info
local text=string.format("Adding new strafe target %s with %d targets: heading = %03d, box_L = %.1f, box_W = %.1f, goodpass = %d, foul line = %.1f", _name, ntargets, heading, l, w, goodpass, foulline)
self:T(self.id..text)
MESSAGE:New(text, 5):ToAllIf(self.Debug)
return self
end
@ -1567,13 +1566,13 @@ function RANGE:OnEventBirth(EventData)
-- Debug output.
local text=string.format("Player %s, callsign %s entered unit %s (UID %d) of group %s (GID %d)", _playername, _callsign, _unitName, _uid, _group:GetName(), _gid)
self:T(self.id..text)
MESSAGE:New(text, 5):ToAllIf(self.Debug)
-- Reset current strafe status.
self.strafeStatus[_uid] = nil
-- Add Menu commands after a delay of 0.1 seconds.
SCHEDULER:New(nil, self._AddF10Commands, {self,_unitName}, 0.1)
--SCHEDULER:New(nil, self._AddF10Commands, {self,_unitName}, 0.1)
self:ScheduleOnce(0.1, self._AddF10Commands, self, _unitName)
-- By default, some bomb impact points and do not flare each hit on target.
self.PlayerSettings[_playername]={} --#RANGE.PlayerData
@ -1591,7 +1590,8 @@ function RANGE:OnEventBirth(EventData)
-- Start check in zone timer.
if self.planes[_uid] ~= true then
SCHEDULER:New(nil, self._CheckInZone, {self, EventData.IniUnitName}, 1, 1)
--SCHEDULER:New(nil, self._CheckInZone, {self, EventData.IniUnitName}, 1, 1)
self.timerCheckZone=TIMER:New(self._CheckInZone, self, EventData.IniUnitName):Start(1, 1)
self.planes[_uid] = true
end
@ -1674,15 +1674,12 @@ function RANGE:OnEventHit(EventData)
if _unit and _playername then
-- Position of target.
local targetPos = _target:GetCoordinate()
-- Message to player.
--local text=string.format("%s, direct hit on target %s.", self:_myname(_unitName), targetname)
--self:DisplayMessageToGroup(_unit, text, 10, true)
-- Flare target.
if self.PlayerSettings[_playername].flaredirecthits then
-- Position of target.
local targetPos = _target:GetCoordinate()
targetPos:Flare(self.PlayerSettings[_playername].flarecolor)
end
@ -1724,9 +1721,6 @@ function RANGE:OnEventShot(EventData)
self:T(self.id.."EVENT SHOT: Weapon name = ".._weaponName)
self:T(self.id.."EVENT SHOT: Weapon cate = "..weaponcategory)
-- Special cases:
--local _viggen=string.match(_weapon, "ROBOT") or string.match(_weapon, "RB75") or string.match(_weapon, "BK90") or string.match(_weapon, "RB15") or string.match(_weapon, "RB04")
-- Tracking conditions for bombs, rockets and missiles.
local _bombs = weaponcategory==Weapon.Category.BOMB --string.match(_weapon, "weapons.bombs")
local _rockets = weaponcategory==Weapon.Category.ROCKET --string.match(_weapon, "weapons.nurs")
@ -1760,7 +1754,7 @@ function RANGE:OnEventShot(EventData)
self:T(self.id..string.format("RANGE %s: Tracking %s - %s.", self.rangename, _weapon, EventData.weapon:getName()))
-- Init bomb position.
local _lastBombPos = {x=0,y=0,z=0}
local _lastBombPos = {x=0,y=0,z=0} --DCS#Vec3
-- Function monitoring the position of a bomb until impact.
local function trackBomb(_ordnance)
@ -1916,35 +1910,39 @@ end
-- @param #string To To state.
function RANGE:onafterStatus(From, Event, To)
local fsmstate=self:GetState()
local text=string.format("Range status: %s", fsmstate)
if self.instructor then
local alive="N/A"
if self.instructorrelayname then
local relay=UNIT:FindByName(self.instructorrelayname)
if relay then
alive=tostring(relay:IsAlive())
end
end
text=text..string.format(", Instructor %.3f MHz (Relay=%s alive=%s)", self.instructorfreq, tostring(self.instructorrelayname), alive)
end
if self.rangecontrol then
local alive="N/A"
if self.rangecontrolrelayname then
local relay=UNIT:FindByName(self.rangecontrolrelayname)
if relay then
alive=tostring(relay:IsAlive())
end
end
text=text..string.format(", Control %.3f MHz (Relay=%s alive=%s)", self.rangecontrolfreq, tostring(self.rangecontrolrelayname), alive)
end
if self.verbose>0 then
-- Check range status.
self:I(self.id..text)
local fsmstate=self:GetState()
local text=string.format("Range status: %s", fsmstate)
if self.instructor then
local alive="N/A"
if self.instructorrelayname then
local relay=UNIT:FindByName(self.instructorrelayname)
if relay then
alive=tostring(relay:IsAlive())
end
end
text=text..string.format(", Instructor %.3f MHz (Relay=%s alive=%s)", self.instructorfreq, tostring(self.instructorrelayname), alive)
end
if self.rangecontrol then
local alive="N/A"
if self.rangecontrolrelayname then
local relay=UNIT:FindByName(self.rangecontrolrelayname)
if relay then
alive=tostring(relay:IsAlive())
end
end
text=text..string.format(", Control %.3f MHz (Relay=%s alive=%s)", self.rangecontrolfreq, tostring(self.rangecontrolrelayname), alive)
end
-- Check range status.
self:I(self.id..text)
end
-- Check player status.
self:_CheckPlayers()
@ -2736,6 +2734,32 @@ function RANGE:_CheckInZone(_unitName)
if _unit and _playername then
--- Function to check if unit is in zone and facing in the right direction and is below the max alt.
local function checkme(targetheading, _zone)
local zone=_zone --Core.Zone#ZONE
-- Heading check.
local unitheading = _unit:GetHeading()
local pitheading = targetheading-180
local deltaheading = unitheading-pitheading
local towardspit = math.abs(deltaheading)<=90 or math.abs(deltaheading-360)<=90
if towardspit then
local vec3=_unit:GetVec3()
local vec2={x=vec3.x, y=vec3.z} --DCS#Vec2
local landheight=land.getHeight(vec2)
local unitalt=vec3.y-landheight
if unitalt<=self.strafemaxalt then
local unitinzone=zone:IsVec2InZone(vec2)
return unitinzone
end
end
return false
end
-- Current position of player unit.
local _unitID = _unit:GetID()
@ -2747,18 +2771,8 @@ function RANGE:_CheckInZone(_unitName)
-- Get the current approach zone and check if player is inside.
local zone=_currentStrafeRun.zone.polygon --Core.Zone#ZONE_POLYGON_BASE
local unitheading = _unit:GetHeading()
local pitheading = _currentStrafeRun.zone.heading - 180
local deltaheading = unitheading-pitheading
local towardspit = math.abs(deltaheading)<=90 or math.abs(deltaheading-360)<=90
local unitalt=_unit:GetHeight()-_unit:GetCoordinate():GetLandHeight()
-- Check if unit is inside zone and below max height AGL.
local unitinzone=_unit:IsInZone(zone) and unitalt <= self.strafemaxalt and towardspit
-- Debug output
local text=string.format("Checking still in zone. Unit = %s, player = %s in zone = %s. alt = %d, delta heading = %d", _unitName, _playername, tostring(unitinzone), unitalt, deltaheading)
self:T2(self.id..text)
-- Check if unit in zone and facing the right direction.
local unitinzone=checkme(_currentStrafeRun.zone.heading, zone)
-- Check if player is in strafe zone and below max alt.
if unitinzone then
@ -2858,22 +2872,10 @@ function RANGE:_CheckInZone(_unitName)
for _,_targetZone in pairs(self.strafeTargets) do
-- Get the current approach zone and check if player is inside.
local zonenname=_targetZone.name
local zone=_targetZone.polygon --Core.Zone#ZONE_POLYGON_BASE
-- Check if player is in zone and below max alt and flying towards the target.
local unitheading = _unit:GetHeading()
local pitheading = _targetZone.heading - 180
local deltaheading = unitheading-pitheading
local towardspit = math.abs(deltaheading)<=90 or math.abs(deltaheading-360)<=90
local unitalt =_unit:GetHeight()-_unit:GetCoordinate():GetLandHeight()
-- Check if unit is inside zone and below max height AGL.
local unitinzone=_unit:IsInZone(zone) and unitalt <= self.strafemaxalt and towardspit
-- Debug info.
local text=string.format("Checking zone %s. Unit = %s, player = %s in zone = %s. alt = %d, delta heading = %d", _targetZone.name, _unitName, _playername, tostring(unitinzone), unitalt, deltaheading)
self:T2(self.id..text)
-- Check if unit in zone and facing the right direction.
local unitinzone=checkme(_targetZone.heading, zone)
-- Player is inside zone.
if unitinzone then
@ -2882,7 +2884,7 @@ function RANGE:_CheckInZone(_unitName)
local _ammo=self:_GetAmmo(_unitName)
-- Init strafe status for this player.
self.strafeStatus[_unitID] = {hits = 0, zone = _targetZone, time = 1, ammo=_ammo, pastfoulline=false }
self.strafeStatus[_unitID] = {hits = 0, zone = _targetZone, time = 1, ammo=_ammo, pastfoulline=false}
-- Rolling in!
local _msg=string.format("%s, rolling in on strafe pit %s.", self:_myname(_unitName), _targetZone.name)
@ -3089,11 +3091,9 @@ function RANGE:_GetAmmo(unitname)
local text=string.format("Player %s has %d rounds ammo of type %s", playername, Nammo, Tammo)
self:T(self.id..text)
MESSAGE:New(text, 10):ToAllIf(self.Debug)
else
local text=string.format("Player %s has %d ammo of type %s", playername, Nammo, Tammo)
self:T(self.id..text)
MESSAGE:New(text, 10):ToAllIf(self.Debug)
end
end
end

View File

@ -667,8 +667,6 @@ function SCORING:_AddPlayerFromUnit( UnitData )
self.Players[PlayerName].ThreatLevel = UnitThreatLevel
self.Players[PlayerName].ThreatType = UnitThreatType
-- TODO: DCS bug concerning Units with skill level client don't get destroyed in multi player. This logic is deactivated until this bug gets fixed.
--[[
if self.Players[PlayerName].Penalty > self.Fratricide * 0.50 then
if self.Players[PlayerName].PenaltyWarning < 1 then
MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "': WARNING! If you continue to commit FRATRICIDE and have a PENALTY score higher than " .. self.Fratricide .. ", you will be COURT MARTIALED and DISMISSED from this mission! \nYour total penalty is: " .. self.Players[PlayerName].Penalty,
@ -684,8 +682,6 @@ function SCORING:_AddPlayerFromUnit( UnitData )
):ToAll()
UnitData:GetGroup():Destroy()
end
--]]
end
end

View File

@ -15,7 +15,9 @@
--
-- ===
--
-- ### Authors: **FlightControl**
-- ### Authors: **FlightControl**, **applevangelist**
--
-- Last Update: July 2021
--
-- ===
--
@ -37,20 +39,40 @@
--
-- @field #SEAD
SEAD = {
ClassName = "SEAD",
TargetSkill = {
Average = { Evade = 50, DelayOff = { 10, 25 }, DelayOn = { 10, 30 } } ,
Good = { Evade = 30, DelayOff = { 8, 20 }, DelayOn = { 20, 40 } } ,
High = { Evade = 15, DelayOff = { 5, 17 }, DelayOn = { 30, 50 } } ,
Excellent = { Evade = 10, DelayOff = { 3, 10 }, DelayOn = { 30, 60 } }
},
SEADGroupPrefixes = {}
ClassName = "SEAD",
TargetSkill = {
Average = { Evade = 30, DelayOn = { 40, 60 } } ,
Good = { Evade = 20, DelayOn = { 30, 50 } } ,
High = { Evade = 15, DelayOn = { 20, 40 } } ,
Excellent = { Evade = 10, DelayOn = { 10, 30 } }
},
SEADGroupPrefixes = {},
SuppressedGroups = {},
EngagementRange = 75 -- default 75% engagement range Feature Request #1355
}
--- Missile enumerators
-- @field Harms
SEAD.Harms = {
["AGM_88"] = "AGM_88",
["AGM_45"] = "AGM_45",
["AGM_122"] = "AGM_122",
["AGM_84"] = "AGM_84",
["AGM_45"] = "AGM_45",
["ALARN"] = "ALARM",
["LD-10"] = "LD-10",
["X_58"] = "X_58",
["X_28"] = "X_28",
["X_25"] = "X_25",
["X_31"] = "X_31",
["Kh25"] = "Kh25",
}
--- Creates the main object which is handling defensive actions for SA sites or moving SA vehicles.
-- When an anti radiation missile is fired (KH-58, KH-31P, KH-31A, KH-25MPU, HARM missiles), the SA will shut down their radars and will take evasive actions...
-- Chances are big that the missile will miss.
-- @param table{string,...}|string SEADGroupPrefixes which is a table of Prefixes of the SA Groups in the DCSRTE on which evasive actions need to be taken.
-- @param #SEAD self
-- @param table{string,...}|string SEADGroupPrefixes which is a table of Prefixes of the SA Groups in the DCS mission editor on which evasive actions need to be taken.
-- @return SEAD
-- @usage
-- -- CCCP SEAD Defenses
@ -58,144 +80,150 @@ SEAD = {
-- SEAD_RU_SAM_Defenses = SEAD:New( { 'RU SA-6 Kub', 'RU SA-6 Defenses', 'RU MI-26 Troops', 'RU Attack Gori' } )
function SEAD:New( SEADGroupPrefixes )
local self = BASE:Inherit( self, BASE:New() )
self:F( SEADGroupPrefixes )
if type( SEADGroupPrefixes ) == 'table' then
for SEADGroupPrefixID, SEADGroupPrefix in pairs( SEADGroupPrefixes ) do
self.SEADGroupPrefixes[SEADGroupPrefix] = SEADGroupPrefix
end
else
self.SEADGroupNames[SEADGroupPrefixes] = SEADGroupPrefixes
end
self:HandleEvent( EVENTS.Shot )
return self
local self = BASE:Inherit( self, BASE:New() )
self:F( SEADGroupPrefixes )
if type( SEADGroupPrefixes ) == 'table' then
for SEADGroupPrefixID, SEADGroupPrefix in pairs( SEADGroupPrefixes ) do
self.SEADGroupPrefixes[SEADGroupPrefix] = SEADGroupPrefix
end
else
self.SEADGroupPrefixes[SEADGroupPrefixes] = SEADGroupPrefixes
end
self:HandleEvent( EVENTS.Shot, self.HandleEventShot )
self:I("*** SEAD - Started Version 0.2.8")
return self
end
--- Detects if an SA site was shot with an anti radiation missile. In this case, take evasive actions based on the skill level set within the ME.
--- Update the active SEAD Set
-- @param #SEAD self
-- @param #table SEADGroupPrefixes The prefixes to add, note: can also be a single #string
-- @return #SEAD self
function SEAD:UpdateSet( SEADGroupPrefixes )
self:F( SEADGroupPrefixes )
if type( SEADGroupPrefixes ) == 'table' then
for SEADGroupPrefixID, SEADGroupPrefix in pairs( SEADGroupPrefixes ) do
self.SEADGroupPrefixes[SEADGroupPrefix] = SEADGroupPrefix
end
else
self.SEADGroupPrefixes[SEADGroupPrefixes] = SEADGroupPrefixes
end
return self
end
--- Sets the engagement range of the SAMs. Defaults to 75% to make it more deadly. Feature Request #1355
-- @param #SEAD self
-- @param #number range Set the engagement range in percent, e.g. 50
-- @return self
function SEAD:SetEngagementRange(range)
self:F( { range } )
range = range or 75
if range < 0 or range > 100 then
range = 75
end
self.EngagementRange = range
self:T(string.format("*** SEAD - Engagement range set to %s",range))
return self
end
--- Check if a known HARM was fired
-- @param #SEAD self
-- @param #string WeaponName
-- @return #boolean Returns true for a match
function SEAD:_CheckHarms(WeaponName)
self:F( { WeaponName } )
local hit = false
for _,_name in pairs (SEAD.Harms) do
if string.find(WeaponName,_name,1) then hit = true end
end
return hit
end
--- Detects if an SAM site was shot with an anti radiation missile. In this case, take evasive actions based on the skill level set within the ME.
-- @see SEAD
-- @param #SEAD
-- @param Core.Event#EVENTDATA EventData
function SEAD:OnEventShot( EventData )
self:F( { EventData } )
function SEAD:HandleEventShot( EventData )
self:T( { EventData } )
local SEADUnit = EventData.IniDCSUnit
local SEADUnitName = EventData.IniDCSUnitName
local SEADWeapon = EventData.Weapon -- Identify the weapon fired
local SEADWeaponName = EventData.WeaponName -- return weapon type
-- Start of the 2nd loop
self:T( "Missile Launched = " .. SEADWeaponName )
--if SEADWeaponName == "KH-58" or SEADWeaponName == "KH-25MPU" or SEADWeaponName == "AGM-88" or SEADWeaponName == "KH-31A" or SEADWeaponName == "KH-31P" then -- Check if the missile is a SEAD
if SEADWeaponName == "weapons.missiles.X_58" --Kh-58U anti-radiation missiles fired
or
SEADWeaponName == "weapons.missiles.Kh25MP_PRGS1VP" --Kh-25MP anti-radiation missiles fired
or
SEADWeaponName == "weapons.missiles.X_25MP" --Kh-25MPU anti-radiation missiles fired
or
SEADWeaponName == "weapons.missiles.X_28" --Kh-28 anti-radiation missiles fired
or
SEADWeaponName == "weapons.missiles.X_31P" --Kh-31P anti-radiation missiles fired
or
SEADWeaponName == "weapons.missiles.AGM_45A" --AGM-45A anti-radiation missiles fired
or
SEADWeaponName == "weapons.missiles.AGM_45" --AGM-45B anti-radiation missiles fired
or
SEADWeaponName == "weapons.missiles.AGM_88" --AGM-88C anti-radiation missiles fired
or
SEADWeaponName == "weapons.missiles.AGM_122" --AGM-122 Sidearm anti-radiation missiles fired
or
SEADWeaponName == "weapons.missiles.ALARM" --ALARM anti-radiation missiles fired
then
local _evade = math.random (1,100) -- random number for chance of evading action
local _targetMim = EventData.Weapon:getTarget() -- Identify target
local _targetMimname = Unit.getName(_targetMim)
local _targetMimgroup = Unit.getGroup(Weapon.getTarget(SEADWeapon))
local _targetMimgroupName = _targetMimgroup:getName()
local _targetMimcont= _targetMimgroup:getController()
local _targetskill = _DATABASE.Templates.Units[_targetMimname].Template.skill
self:T( self.SEADGroupPrefixes )
self:T( _targetMimgroupName )
local SEADGroupFound = false
for SEADGroupPrefixID, SEADGroupPrefix in pairs( self.SEADGroupPrefixes ) do
if string.find( _targetMimgroupName, SEADGroupPrefix, 1, true ) then
SEADGroupFound = true
self:T( 'Group Found' )
break
end
end
if SEADGroupFound == true then
if _targetskill == "Random" then -- when skill is random, choose a skill
local Skills = { "Average", "Good", "High", "Excellent" }
_targetskill = Skills[ math.random(1,4) ]
end
self:T( _targetskill )
if self.TargetSkill[_targetskill] then
if (_evade > self.TargetSkill[_targetskill].Evade) then
self:T( string.format("Evading, target skill " ..string.format(_targetskill)) )
local _targetMim = Weapon.getTarget(SEADWeapon)
local _targetMimname = Unit.getName(_targetMim)
local _targetMimgroup = Unit.getGroup(Weapon.getTarget(SEADWeapon))
local _targetMimcont= _targetMimgroup:getController()
routines.groupRandomDistSelf(_targetMimgroup,300,'Diamond',250,20) -- move randomly
local SuppressedGroups1 = {} -- unit suppressed radar off for a random time
local function SuppressionEnd1(id)
id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN)
SuppressedGroups1[id.groupName] = nil
end
local id = {
groupName = _targetMimgroup,
ctrl = _targetMimcont
}
local delay1 = math.random(self.TargetSkill[_targetskill].DelayOff[1], self.TargetSkill[_targetskill].DelayOff[2])
if SuppressedGroups1[id.groupName] == nil then
SuppressedGroups1[id.groupName] = {
SuppressionEndTime1 = timer.getTime() + delay1,
SuppressionEndN1 = SuppressionEndCounter1 --Store instance of SuppressionEnd() scheduled function
}
Controller.setOption(_targetMimcont, AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN)
timer.scheduleFunction(SuppressionEnd1, id, SuppressedGroups1[id.groupName].SuppressionEndTime1) --Schedule the SuppressionEnd() function
--trigger.action.outText( string.format("Radar Off " ..string.format(delay1)), 20)
end
local SuppressedGroups = {}
local function SuppressionEnd(id)
id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.RED)
SuppressedGroups[id.groupName] = nil
end
local id = {
groupName = _targetMimgroup,
ctrl = _targetMimcont
}
local delay = math.random(self.TargetSkill[_targetskill].DelayOn[1], self.TargetSkill[_targetskill].DelayOn[2])
if SuppressedGroups[id.groupName] == nil then
SuppressedGroups[id.groupName] = {
SuppressionEndTime = timer.getTime() + delay,
SuppressionEndN = SuppressionEndCounter --Store instance of SuppressionEnd() scheduled function
}
timer.scheduleFunction(SuppressionEnd, id, SuppressedGroups[id.groupName].SuppressionEndTime) --Schedule the SuppressionEnd() function
--trigger.action.outText( string.format("Radar On " ..string.format(delay)), 20)
end
end
end
end
end
local SEADUnit = EventData.IniDCSUnit
local SEADUnitName = EventData.IniDCSUnitName
local SEADWeapon = EventData.Weapon -- Identify the weapon fired
local SEADWeaponName = EventData.WeaponName -- return weapon type
self:T( "*** SEAD - Missile Launched = " .. SEADWeaponName)
self:T({ SEADWeapon })
if self:_CheckHarms(SEADWeaponName) then
local _targetskill = "Random"
local _targetMimgroupName = "none"
local _evade = math.random (1,100) -- random number for chance of evading action
local _targetMim = EventData.Weapon:getTarget() -- Identify target
local _targetUnit = UNIT:Find(_targetMim) -- Unit name by DCS Object
if _targetUnit and _targetUnit:IsAlive() then
local _targetMimgroup = _targetUnit:GetGroup()
local _targetMimgroupName = _targetMimgroup:GetName() -- group name
--local _targetskill = _DATABASE.Templates.Units[_targetUnit].Template.skill
self:T( self.SEADGroupPrefixes )
self:T( _targetMimgroupName )
end
-- see if we are shot at
local SEADGroupFound = false
for SEADGroupPrefixID, SEADGroupPrefix in pairs( self.SEADGroupPrefixes ) do
if string.find( _targetMimgroupName, SEADGroupPrefix, 1, true ) then
SEADGroupFound = true
self:T( '*** SEAD - Group Found' )
break
end
end
if SEADGroupFound == true then -- yes we are being attacked
if _targetskill == "Random" then -- when skill is random, choose a skill
local Skills = { "Average", "Good", "High", "Excellent" }
_targetskill = Skills[ math.random(1,4) ]
end
self:T( _targetskill )
if self.TargetSkill[_targetskill] then
if (_evade > self.TargetSkill[_targetskill].Evade) then
self:T( string.format("*** SEAD - Evading, target skill " ..string.format(_targetskill)) )
local _targetMimgroup = Unit.getGroup(Weapon.getTarget(SEADWeapon))
local _targetMimcont= _targetMimgroup:getController()
routines.groupRandomDistSelf(_targetMimgroup,300,'Diamond',250,20) -- move randomly
--tracker ID table to switch groups off and on again
local id = {
groupName = _targetMimgroup,
ctrl = _targetMimcont
}
local function SuppressionEnd(id) --switch group back on
local range = self.EngagementRange -- Feature Request #1355
self:T(string.format("*** SEAD - Engagement Range is %d", range))
id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.RED)
--id.groupName:enableEmission(true)
id.ctrl:setOption(AI.Option.Ground.id.AC_ENGAGEMENT_RANGE_RESTRICTION,range) --Feature Request #1355
self.SuppressedGroups[id.groupName] = nil --delete group id from table when done
end
-- randomize switch-on time
local delay = math.random(self.TargetSkill[_targetskill].DelayOn[1], self.TargetSkill[_targetskill].DelayOn[2])
local SuppressionEndTime = timer.getTime() + delay
--create entry
if self.SuppressedGroups[id.groupName] == nil then --no timer entry for this group yet
self.SuppressedGroups[id.groupName] = {
SuppressionEndTime = delay
}
Controller.setOption(_targetMimcont, AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN)
--_targetMimgroup:enableEmission(false)
timer.scheduleFunction(SuppressionEnd, id, SuppressionEndTime) --Schedule the SuppressionEnd() function
end
end
end
end
end
end

View File

@ -0,0 +1,548 @@
--- **Functional** -- Short Range Air Defense System
--
-- ===
--
-- **SHORAD** - Short Range Air Defense System
-- Controls a network of short range air/missile defense groups.
--
-- ===
--
-- ## Missions:
--
-- ### [SHORAD - Short Range Air Defense](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/SRD%20-%20SHORAD%20Defense)
--
-- ===
--
-- ### Author : **applevangelist **
--
-- @module Functional.Shorad
-- @image Functional.Shorad.jpg
--
-- Date: July 2021
-------------------------------------------------------------------------
--- **SHORAD** class, extends Core.Base#BASE
-- @type SHORAD
-- @field #string ClassName
-- @field #string name Name of this Shorad
-- @field #boolean debug Set the debug state
-- @field #string Prefixes String to be used to build the @{#Core.Set#SET_GROUP}
-- @field #number Radius Shorad defense radius in meters
-- @field Core.Set#SET_GROUP Groupset The set of Shorad groups
-- @field Core.Set#SET_GROUP Samset The set of SAM groups to defend
-- @field #string Coalition The coalition of this Shorad
-- @field #number ActiveTimer How long a Shorad stays active after wake-up in seconds
-- @field #table ActiveGroups Table for the timer function
-- @field #string lid The log ID for the dcs.log
-- @field #boolean DefendHarms Default true, intercept incoming HARMS
-- @field #boolean DefendMavs Default true, intercept incoming AG-Missiles
-- @field #number DefenseLowProb Default 70, minimum detection limit
-- @field #number DefenseHighProb Default 90, maximim detection limit
-- @field #boolean UseEmOnOff Decide if we are using Emission on/off (default) or AlarmState red/green.
-- @extends Core.Base#BASE
--- *Good friends are worth defending.* Mr Tushman, Wonder (the Movie)
--
-- Simple Class for a more intelligent Short Range Air Defense System
--
-- #SHORAD
-- Moose derived missile intercepting short range defense system.
-- Protects a network of SAM sites. Uses events to switch on the defense groups closest to the enemy.
-- Easily integrated with @{Functional.Mantis#MANTIS} to complete the defensive system setup.
--
-- ## Usage
--
-- Set up a #SET_GROUP for the SAM sites to be protected:
--
-- `local SamSet = SET_GROUP:New():FilterPrefixes("Red SAM"):FilterCoalitions("red"):FilterStart()`
--
-- By default, SHORAD will defense against both HARMs and AG-Missiles with short to medium range. The default defense probability is 70-90%.
-- When a missile is detected, SHORAD will activate defense groups in the given radius around the target for 10 minutes. It will *not* react to friendly fire.
--
-- ### Start a new SHORAD system, parameters are:
--
-- * Name: Name of this SHORAD.
-- * ShoradPrefix: Filter for the Shorad #SET_GROUP.
-- * Samset: The #SET_GROUP of SAM sites to defend.
-- * Radius: Defense radius in meters.
-- * ActiveTimer: Determines how many seconds the systems stay on red alert after wake-up call.
-- * Coalition: Coalition, i.e. "blue", "red", or "neutral".*
--
-- `myshorad = SHORAD:New("RedShorad", "Red SHORAD", SamSet, 25000, 600, "red")`
--
-- ## Customize options
--
-- * SHORAD:SwitchDebug(debug)
-- * SHORAD:SwitchHARMDefense(onoff)
-- * SHORAD:SwitchAGMDefense(onoff)
-- * SHORAD:SetDefenseLimits(low,high)
-- * SHORAD:SetActiveTimer(seconds)
-- * SHORAD:SetDefenseRadius(meters)
--
-- @field #SHORAD
SHORAD = {
ClassName = "SHORAD",
name = "MyShorad",
debug = false,
Prefixes = "",
Radius = 20000,
Groupset = nil,
Samset = nil,
Coalition = nil,
ActiveTimer = 600, --stay on 10 mins
ActiveGroups = {},
lid = "",
DefendHarms = true,
DefendMavs = true,
DefenseLowProb = 70,
DefenseHighProb = 90,
UseEmOnOff = false,
}
-----------------------------------------------------------------------
-- SHORAD System
-----------------------------------------------------------------------
do
-- TODO Complete list?
--- Missile enumerators
-- @field Harms
SHORAD.Harms = {
["AGM_88"] = "AGM_88",
["AGM_45"] = "AGM_45",
["AGM_122"] = "AGM_122",
["AGM_84"] = "AGM_84",
["AGM_45"] = "AGM_45",
["ALARN"] = "ALARM",
["LD-10"] = "LD-10",
["X_58"] = "X_58",
["X_28"] = "X_28",
["X_25"] = "X_25",
["X_31"] = "X_31",
["Kh25"] = "Kh25",
}
--- TODO complete list?
-- @field Mavs
SHORAD.Mavs = {
["AGM"] = "AGM",
["C-701"] = "C-701",
["Kh25"] = "Kh25",
["Kh29"] = "Kh29",
["Kh31"] = "Kh31",
["Kh66"] = "Kh66",
}
--- Instantiates a new SHORAD object
-- @param #SHORAD self
-- @param #string Name Name of this SHORAD
-- @param #string ShoradPrefix Filter for the Shorad #SET_GROUP
-- @param Core.Set#SET_GROUP Samset The #SET_GROUP of SAM sites to defend
-- @param #number Radius Defense radius in meters, used to switch on groups
-- @param #number ActiveTimer Determines how many seconds the systems stay on red alert after wake-up call
-- @param #string Coalition Coalition, i.e. "blue", "red", or "neutral"
-- @param #boolean UseEmOnOff Use Emissions On/Off rather than Alarm State Red/Green (default: use Emissions switch)
-- @retunr #SHORAD self
function SHORAD:New(Name, ShoradPrefix, Samset, Radius, ActiveTimer, Coalition, UseEmOnOff)
local self = BASE:Inherit( self, BASE:New() )
self:T({Name, ShoradPrefix, Samset, Radius, ActiveTimer, Coalition})
local GroupSet = SET_GROUP:New():FilterPrefixes(ShoradPrefix):FilterCoalitions(Coalition):FilterCategoryGround():FilterStart()
self.name = Name or "MyShorad"
self.Prefixes = ShoradPrefix or "SAM SHORAD"
self.Radius = Radius or 20000
self.Coalition = Coalition or "blue"
self.Samset = Samset or GroupSet
self.ActiveTimer = ActiveTimer or 600
self.ActiveGroups = {}
self.Groupset = GroupSet
self.DefendHarms = true
self.DefendMavs = true
self.DefenseLowProb = 70 -- probability to detect a missile shot, low margin
self.DefenseHighProb = 90 -- probability to detect a missile shot, high margin
self.UseEmOnOff = UseEmOnOff or false -- Decide if we are using Emission on/off (default) or AlarmState red/green
self:I("*** SHORAD - Started Version 0.2.8")
-- Set the string id for output to DCS.log file.
self.lid=string.format("SHORAD %s | ", self.name)
self:_InitState()
self:HandleEvent(EVENTS.Shot, self.HandleEventShot)
return self
end
--- Initially set all groups to alarm state GREEN
-- @param #SHORAD self
function SHORAD:_InitState()
self:T(self.lid .. " _InitState")
local table = {}
local set = self.Groupset
self:T({set = set})
local aliveset = set:GetAliveSet() --#table
for _,_group in pairs (aliveset) do
if self.UseEmOnOff then
--_group:SetAIOff()
_group:EnableEmission(false)
_group:OptionAlarmStateRed() --Wrapper.Group#GROUP
else
_group:OptionAlarmStateGreen() --Wrapper.Group#GROUP
end
_group:OptionDisperseOnAttack(30)
end
-- gather entropy
for i=1,100 do
math.random()
end
return self
end
--- Switch debug state on
-- @param #SHORAD self
-- @param #boolean debug Switch debug on (true) or off (false)
function SHORAD:SwitchDebug(onoff)
self:T( { onoff } )
if onoff then
self:SwitchDebugOn()
else
self.SwitchDebugOff()
end
return self
end
--- Switch debug state on
-- @param #SHORAD self
function SHORAD:SwitchDebugOn()
self.debug = true
--tracing
BASE:TraceOn()
BASE:TraceClass("SHORAD")
return self
end
--- Switch debug state off
-- @param #SHORAD self
function SHORAD:SwitchDebugOff()
self.debug = false
BASE:TraceOff()
return self
end
--- Switch defense for HARMs
-- @param #SHORAD self
-- @param #boolean onoff
function SHORAD:SwitchHARMDefense(onoff)
self:T( { onoff } )
local onoff = onoff or true
self.DefendHarms = onoff
return self
end
--- Switch defense for AGMs
-- @param #SHORAD self
-- @param #boolean onoff
function SHORAD:SwitchAGMDefense(onoff)
self:T( { onoff } )
local onoff = onoff or true
self.DefendMavs = onoff
return self
end
--- Set defense probability limits
-- @param #SHORAD self
-- @param #number low Minimum detection limit, integer 1-100
-- @param #number high Maximum detection limit integer 1-100
function SHORAD:SetDefenseLimits(low,high)
self:T( { low, high } )
local low = low or 70
local high = high or 90
if (low < 0) or (low > 100) or (low > high) then
low = 70
end
if (high < 0) or (high > 100) or (high < low ) then
high = 90
end
self.DefenseLowProb = low
self.DefenseHighProb = high
return self
end
--- Set the number of seconds a SHORAD site will stay active
-- @param #SHORAD self
-- @param #number seconds Number of seconds systems stay active
function SHORAD:SetActiveTimer(seconds)
self:T(self.lid .. " SetActiveTimer")
local timer = seconds or 600
if timer < 0 then
timer = 600
end
self.ActiveTimer = timer
return self
end
--- Set the number of meters for the SHORAD defense zone
-- @param #SHORAD self
-- @param #number meters Radius of the defense search zone in meters. #SHORADs in this range around a targeted group will go active
function SHORAD:SetDefenseRadius(meters)
self:T(self.lid .. " SetDefenseRadius")
local radius = meters or 20000
if radius < 0 then
radius = 20000
end
self.Radius = radius
return self
end
--- Set using Emission on/off instead of changing alarm state
-- @param #SHORAD self
-- @param #boolean switch Decide if we are changing alarm state or AI state
function SHORAD:SetUsingEmOnOff(switch)
self:T(self.lid .. " SetUsingEmOnOff")
self.UseEmOnOff = switch or false
return self
end
--- Check if a HARM was fired
-- @param #SHORAD self
-- @param #string WeaponName
-- @return #boolean Returns true for a match
function SHORAD:_CheckHarms(WeaponName)
self:T(self.lid .. " _CheckHarms")
self:T( { WeaponName } )
local hit = false
if self.DefendHarms then
for _,_name in pairs (SHORAD.Harms) do
if string.find(WeaponName,_name,1) then hit = true end
end
end
return hit
end
--- Check if an AGM was fired
-- @param #SHORAD self
-- @param #string WeaponName
-- @return #boolean Returns true for a match
function SHORAD:_CheckMavs(WeaponName)
self:T(self.lid .. " _CheckMavs")
self:T( { WeaponName } )
local hit = false
if self.DefendMavs then
for _,_name in pairs (SHORAD.Mavs) do
if string.find(WeaponName,_name,1) then hit = true end
end
end
return hit
end
--- Check the coalition of the attacker
-- @param #SHORAD self
-- @param #string Coalition name
-- @return #boolean Returns false for a match
function SHORAD:_CheckCoalition(Coalition)
self:T(self.lid .. " _CheckCoalition")
local owncoalition = self.Coalition
local othercoalition = ""
if Coalition == 0 then
othercoalition = "neutral"
elseif Coalition == 1 then
othercoalition = "red"
else
othercoalition = "blue"
end
self:T({owncoalition = owncoalition, othercoalition = othercoalition})
if owncoalition ~= othercoalition then
return true
else
return false
end
end
--- Check if the missile is aimed at a SHORAD
-- @param #SHORAD self
-- @param #string TargetGroupName Name of the target group
-- @return #boolean Returns true for a match, else false
function SHORAD:_CheckShotAtShorad(TargetGroupName)
self:T(self.lid .. " _CheckShotAtShorad")
local tgtgrp = TargetGroupName
local shorad = self.Groupset
local shoradset = shorad:GetAliveSet() --#table
local returnname = false
for _,_groups in pairs (shoradset) do
local groupname = _groups:GetName()
if string.find(groupname, tgtgrp, 1) then
returnname = true
--_groups:RelocateGroundRandomInRadius(7,100,false,false) -- be a bit evasive
end
end
return returnname
end
--- Check if the missile is aimed at a SAM site
-- @param #SHORAD self
-- @param #string TargetGroupName Name of the target group
-- @return #boolean Returns true for a match, else false
function SHORAD:_CheckShotAtSams(TargetGroupName)
self:T(self.lid .. " _CheckShotAtSams")
local tgtgrp = TargetGroupName
local shorad = self.Samset
--local shoradset = shorad:GetAliveSet() --#table
local shoradset = shorad:GetSet() --#table
local returnname = false
for _,_groups in pairs (shoradset) do
local groupname = _groups:GetName()
if string.find(groupname, tgtgrp, 1) then
returnname = true
end
end
return returnname
end
--- Calculate if the missile shot is detected
-- @param #SHORAD self
-- @return #boolean Returns true for a detection, else false
function SHORAD:_ShotIsDetected()
self:T(self.lid .. " _ShotIsDetected")
local IsDetected = false
local DetectionProb = math.random(self.DefenseLowProb, self.DefenseHighProb) -- reference value
local ActualDetection = math.random(1,100) -- value for this shot
if ActualDetection <= DetectionProb then
IsDetected = true
end
return IsDetected
end
--- Wake up #SHORADs in a zone with diameter Radius for ActiveTimer seconds
-- @param #SHORAD self
-- @param #string TargetGroup Name of the target group used to build the #ZONE
-- @param #number Radius Radius of the #ZONE
-- @param #number ActiveTimer Number of seconds to stay active
-- @param #number TargetCat (optional) Category, i.e. Object.Category.UNIT or Object.Category.STATIC
-- @usage Use this function to integrate with other systems, example
--
-- local SamSet = SET_GROUP:New():FilterPrefixes("Blue SAM"):FilterCoalitions("blue"):FilterStart()
-- myshorad = SHORAD:New("BlueShorad", "Blue SHORAD", SamSet, 22000, 600, "blue")
-- myshorad:SwitchDebug(true)
-- mymantis = MANTIS:New("BlueMantis","Blue SAM","Blue EWR",nil,"blue",false,"Blue Awacs")
-- mymantis:AddShorad(myshorad,720)
-- mymantis:Start()
function SHORAD:WakeUpShorad(TargetGroup, Radius, ActiveTimer, TargetCat)
self:T(self.lid .. " WakeUpShorad")
self:T({TargetGroup, Radius, ActiveTimer, TargetCat})
local targetcat = TargetCat or Object.Category.UNIT
local targetgroup = TargetGroup
local targetvec2 = nil
if targetcat == Object.Category.UNIT then
targetvec2 = GROUP:FindByName(targetgroup):GetVec2()
elseif targetcat == Object.Category.STATIC then
targetvec2 = STATIC:FindByName(targetgroup,false):GetVec2()
else
local samset = self.Samset
local sam = samset:GetRandom()
targetvec2 = sam:GetVec2()
end
local targetzone = ZONE_RADIUS:New("Shorad",targetvec2,Radius) -- create a defense zone to check
local groupset = self.Groupset --Core.Set#SET_GROUP
local shoradset = groupset:GetAliveSet() --#table
-- local function to switch off shorad again
local function SleepShorad(group)
local groupname = group:GetName()
self.ActiveGroups[groupname] = nil
if self.UseEmOnOff then
group:EnableEmission(false)
--group:SetAIOff()
else
group:OptionAlarmStateGreen()
end
local text = string.format("Sleeping SHORAD %s", group:GetName())
self:T(text)
local m = MESSAGE:New(text,10,"SHORAD"):ToAllIf(self.debug)
end
-- go through set and find the one(s) to activate
for _,_group in pairs (shoradset) do
if _group:IsAnyInZone(targetzone) then
local text = string.format("Waking up SHORAD %s", _group:GetName())
self:T(text)
local m = MESSAGE:New(text,10,"SHORAD"):ToAllIf(self.debug)
if self.UseEmOnOff then
--_group:SetAIOn()
_group:EnableEmission(true)
end
_group:OptionAlarmStateRed()
local groupname = _group:GetName()
if self.ActiveGroups[groupname] == nil then -- no timer yet for this group
self.ActiveGroups[groupname] = { Timing = ActiveTimer }
local endtime = timer.getTime() + (ActiveTimer * math.random(75,100) / 100 ) -- randomize wakeup a bit
timer.scheduleFunction(SleepShorad, _group, endtime)
end
end
end
return self
end
--- Main function - work on the EventData
-- @param #SHORAD self
-- @param Core.Event#EVENTDATA EventData The event details table data set
function SHORAD:HandleEventShot( EventData )
self:T( { EventData } )
self:T(self.lid .. " HandleEventShot")
--local ShootingUnit = EventData.IniDCSUnit
--local ShootingUnitName = EventData.IniDCSUnitName
local ShootingWeapon = EventData.Weapon -- Identify the weapon fired
local ShootingWeaponName = EventData.WeaponName -- return weapon type
-- get firing coalition
local weaponcoalition = EventData.IniGroup:GetCoalition()
-- get detection probability
if self:_CheckCoalition(weaponcoalition) then --avoid overhead on friendly fire
local IsDetected = self:_ShotIsDetected()
-- convert to text
local DetectedText = "false"
if IsDetected then
DetectedText = "true"
end
local text = string.format("%s Missile Launched = %s | Detected probability state is %s", self.lid, ShootingWeaponName, DetectedText)
self:T( text )
local m = MESSAGE:New(text,10,"Info"):ToAllIf(self.debug)
--
if (self:_CheckHarms(ShootingWeaponName) or self:_CheckMavs(ShootingWeaponName)) and IsDetected then
-- get target data
local targetdata = EventData.Weapon:getTarget() -- Identify target
local targetcat = targetdata:getCategory() -- Identify category
self:T(string.format("Target Category (3=STATIC, 1=UNIT)= %s",tostring(targetcat)))
local targetunit = nil
if targetcat == Object.Category.UNIT then -- UNIT
targetunit = UNIT:Find(targetdata)
elseif targetcat == Object.Category.STATIC then -- STATIC
targetunit = STATIC:Find(targetdata)
end
--local targetunitname = Unit.getName(targetdata) -- Unit name
if targetunit and targetunit:IsAlive() then
local targetunitname = targetunit:GetName()
--local targetgroup = Unit.getGroup(Weapon.getTarget(ShootingWeapon)) --targeted group
local targetgroup = nil
local targetgroupname = "none"
if targetcat == Object.Category.UNIT then
targetgroup = targetunit:GetGroup()
targetgroupname = targetgroup:GetName() -- group name
elseif targetcat == Object.Category.STATIC then
targetgroup = targetunit
targetgroupname = targetunitname
end
local text = string.format("%s Missile Target = %s", self.lid, tostring(targetgroupname))
self:T( text )
local m = MESSAGE:New(text,10,"Info"):ToAllIf(self.debug)
-- check if we or a SAM site are the target
--local TargetGroup = EventData.TgtGroup -- Wrapper.Group#GROUP
local shotatus = self:_CheckShotAtShorad(targetgroupname) --#boolean
local shotatsams = self:_CheckShotAtSams(targetgroupname) --#boolean
-- if being shot at, find closest SHORADs to activate
if shotatsams or shotatus then
self:T({shotatsams=shotatsams,shotatus=shotatus})
self:WakeUpShorad(targetgroupname, self.Radius, self.ActiveTimer, targetcat)
end
end
end
end
end
--
end
-----------------------------------------------------------------------
-- SHORAD end
-----------------------------------------------------------------------

View File

@ -199,7 +199,13 @@
-- warehouseBatumi:AddAsset("Huey", 5, WAREHOUSE.Attribute.AIR_TRANSPORTHELO)
--
-- This becomes important when assets are requested from other warehouses as described below. In this case, the five Hueys are now marked as transport helicopters and
-- not attack helicopters.
-- not attack helicopters. This is also particularly useful when adding assets to a warehouse with the intention of using them to transport other units that are part of
-- a subsequent request (see below). Setting the attribute will help to ensure that warehouse module can find the correct unit when attempting to service a request in its
-- queue. For example, if we want to add an Amphibious Landing Ship, even though most are indeed armed, it's recommended to do the following:
--
-- warehouseBatumi:AddAsset("Landing Ship", 1, WAREHOUSE.Attribute.NAVAL_UNARMEDSHIP)
--
-- Then when adding the request, you can simply specify WAREHOUSE.TransportType.SHIP (which corresponds to NAVAL_UNARMEDSHIP) as the TransportType.
--
-- ### Setting the Cargo Bay Weight Limit
-- You can ajust the cargo bay weight limit, in case it is not calculated correctly automatically. For example, the cargo bay of a C-17A is much smaller in DCS than that of a C-130, which is
@ -1575,6 +1581,7 @@ WAREHOUSE = {
delivered = {},
defending = {},
portzone = nil,
harborzone = nil,
shippinglanes = {},
offroadpaths = {},
autodefence = false,
@ -1733,12 +1740,15 @@ WAREHOUSE.Attribute = {
-- @field #string TRAIN Transports are conducted by trains. Not implemented yet. Also trains are buggy in DCS.
-- @field #string SELFPROPELLED Assets go to their destination by themselves. No transport carrier needed.
WAREHOUSE.TransportType = {
AIRPLANE = "Air_TransportPlane",
HELICOPTER = "Air_TransportHelo",
APC = "Ground_APC",
TRAIN = "Ground_Train",
SHIP = "Naval_UnarmedShip",
SELFPROPELLED = "Selfpropelled",
AIRPLANE = "Air_TransportPlane",
HELICOPTER = "Air_TransportHelo",
APC = "Ground_APC",
TRAIN = "Ground_Train",
SHIP = "Naval_UnarmedShip",
AIRCRAFTCARRIER = "Naval_AircraftCarrier",
WARSHIP = "Naval_WarShip",
ARMEDSHIP = "Naval_ArmedShip",
SELFPROPELLED = "Selfpropelled",
}
--- Warehouse quantity enumerator for selecting number of assets, e.g. all, half etc. of what is in stock rather than an absolute number.
@ -1783,7 +1793,7 @@ WAREHOUSE.version="1.0.2"
-- TODO: Make more examples: ARTY, CAP, ...
-- TODO: Check also general requests like all ground. Is this a problem for self propelled if immobile units are among the assets? Check if transport.
-- TODO: Handle the case when units of a group die during the transfer.
-- TODO: Added habours as interface for transport to from warehouses? Could make a rudimentary shipping dispatcher.
-- DONE: Added harbours as interface for transport to/from warehouses. Simplifies process of spawning units near the ship, especially if cargo not self-propelled.
-- DONE: Test capturing a neutral warehouse.
-- DONE: Add save/load capability of warehouse <==> persistance after mission restart. Difficult in lua!
-- DONE: Get cargo bay and weight from CARGO_GROUP and GROUP. No necessary any more!
@ -2623,6 +2633,7 @@ end
-- @param Wrapper.Airbase#AIRBASE.ParkingSpot spot Parking spot.
-- @return #boolean If true, parking is valid.
function WAREHOUSE:_CheckParkingValid(spot)
if self.parkingIDs==nil then
return true
end
@ -2636,6 +2647,25 @@ function WAREHOUSE:_CheckParkingValid(spot)
return false
end
--- Check parking ID for an asset.
-- @param #WAREHOUSE self
-- @param Wrapper.Airbase#AIRBASE.ParkingSpot spot Parking spot.
-- @return #boolean If true, parking is valid.
function WAREHOUSE:_CheckParkingAsset(spot, asset)
if asset.parkingIDs==nil then
return true
end
for _,id in pairs(asset.parkingIDs or {}) do
if spot.TerminalID==id then
return true
end
end
return false
end
--- Enable auto save of warehouse assets at mission end event.
-- @param #WAREHOUSE self
@ -2728,6 +2758,18 @@ function WAREHOUSE:SetPortZone(zone)
return self
end
--- Add a Harbor Zone for this warehouse where naval cargo units will spawn and be received.
-- Both warehouses must have the harbor zone defined for units to properly spawn on both the
-- sending and receiving side. The harbor zone should be within 3km of the port zone used for
-- warehouse in order to facilitate the boarding process.
-- @param #WAREHOUSE self
-- @param Core.Zone#ZONE zone The zone defining the naval embarcation/debarcation point for cargo units
-- @return #WAREHOUSE self
function WAREHOUSE:SetHarborZone(zone)
self.harborzone=zone
return self
end
--- Add a shipping lane from this warehouse to another remote warehouse.
-- Note that both warehouses must have a port zone defined before a shipping lane can be added!
-- Shipping lane is taken from the waypoints of a (late activated) template group. So set up a group, e.g. a ship or a helicopter, and place its
@ -3060,6 +3102,21 @@ function WAREHOUSE:GetCoordinate()
return self.warehouse:GetCoordinate()
end
--- Get 3D vector of warehouse static.
-- @param #WAREHOUSE self
-- @return DCS#Vec3 The 3D vector of the warehouse.
function WAREHOUSE:GetVec3()
return self.warehouse:GetVec3()
end
--- Get 2D vector of warehouse static.
-- @param #WAREHOUSE self
-- @return DCS#Vec2 The 2D vector of the warehouse.
function WAREHOUSE:GetVec2()
return self.warehouse:GetVec2()
end
--- Get coalition side of warehouse static.
-- @param #WAREHOUSE self
-- @return #number Coalition side, i.e. number of @{DCS#coalition.side}.
@ -4413,10 +4470,10 @@ function WAREHOUSE:onafterRequest(From, Event, To, Request)
self:_ErrorMessage("ERROR: Cargo transport by train not supported yet!")
return
elseif Request.transporttype==WAREHOUSE.TransportType.SHIP then
elseif Request.transporttype==WAREHOUSE.TransportType.SHIP or Request.transporttype==WAREHOUSE.TransportType.NAVALCARRIER then
self:_ErrorMessage("ERROR: Cargo transport by ship not supported yet!")
return
-- Spawn Ship in port zone
spawngroup=self:_SpawnAssetGroundNaval(_alias, _assetitem, Request, self.portzone)
elseif Request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then
@ -4544,6 +4601,9 @@ function WAREHOUSE:onafterRequestSpawned(From, Event, To, Request, CargoGroupSet
--_boardradius=nil
elseif Request.transporttype==WAREHOUSE.TransportType.APC then
--_boardradius=nil
elseif Request.transporttype==WAREHOUSE.TransportType.SHIP or Request.transporttype==WAREHOUSE.TransportType.AIRCRAFTCARRIER
or Request.transporttype==WAREHOUSE.TransportType.ARMEDSHIP or Request.transporttype==WAREHOUSE.TransportType.WARSHIP then
_boardradius=6000
end
-- Empty cargo group set.
@ -4554,7 +4614,6 @@ function WAREHOUSE:onafterRequestSpawned(From, Event, To, Request, CargoGroupSet
-- Find asset belonging to this group.
local asset=self:FindAssetInDB(_group)
-- New cargo group object.
local cargogroup=CARGO_GROUP:New(_group, _cargotype,_group:GetName(),_boardradius, asset.loadradius)
@ -4563,6 +4622,7 @@ function WAREHOUSE:onafterRequestSpawned(From, Event, To, Request, CargoGroupSet
-- Add group to group set.
CargoGroups:AddCargo(cargogroup)
end
------------------------
@ -4608,23 +4668,54 @@ function WAREHOUSE:onafterRequestSpawned(From, Event, To, Request, CargoGroupSet
-- Set home zone.
CargoTransport:SetHomeZone(self.spawnzone)
elseif Request.transporttype==WAREHOUSE.TransportType.SHIP or Request.transporttype==WAREHOUSE.TransportType.AIRCRAFTCARRIER
or Request.transporttype==WAREHOUSE.TransportType.ARMEDSHIP or Request.transporttype==WAREHOUSE.TransportType.WARSHIP then
-- Pickup and deploy zones.
local PickupZoneSet = SET_ZONE:New():AddZone(self.portzone)
PickupZoneSet:AddZone(self.harborzone)
local DeployZoneSet = SET_ZONE:New():AddZone(Request.warehouse.harborzone)
-- Get the shipping lane to use and pass it to the Dispatcher
local remotename = Request.warehouse.warehouse:GetName()
local ShippingLane = self.shippinglanes[remotename][math.random(#self.shippinglanes[remotename])]
-- Define dispatcher for this task.
CargoTransport = AI_CARGO_DISPATCHER_SHIP:New(TransportGroupSet, CargoGroups, PickupZoneSet, DeployZoneSet, ShippingLane)
-- Set home zone
CargoTransport:SetHomeZone(self.portzone)
else
self:E(self.lid.."ERROR: Unknown transporttype!")
end
-- Set pickup and deploy radii.
-- The 20 m inner radius are to ensure that the helo does not land on the warehouse itself in the middle of the default spawn zone.
local pickupouter=200
local pickupinner=0
if self.spawnzone.Radius~=nil then
pickupouter=self.spawnzone.Radius
local pickupouter = 200
local pickupinner = 0
local deployouter = 200
local deployinner = 0
if Request.transporttype==WAREHOUSE.TransportType.SHIP or Request.transporttype==WAREHOUSE.TransportType.AIRCRAFTCARRIER
or Request.transporttype==WAREHOUSE.TransportType.ARMEDSHIP or Request.transporttype==WAREHOUSE.TransportType.WARSHIP then
pickupouter=1000
pickupinner=20
end
local deployouter=200
local deployinner=0
if self.spawnzone.Radius~=nil then
deployouter=Request.warehouse.spawnzone.Radius
deployinner=20
deployouter=1000
deployinner=0
else
pickupouter=200
pickupinner=0
if self.spawnzone.Radius~=nil then
pickupouter=self.spawnzone.Radius
pickupinner=20
end
deployouter=200
deployinner=0
if self.spawnzone.Radius~=nil then
deployouter=Request.warehouse.spawnzone.Radius
deployinner=20
end
end
CargoTransport:SetPickupRadius(pickupouter, pickupinner)
CargoTransport:SetDeployRadius(deployouter, deployinner)
@ -4703,7 +4794,7 @@ function WAREHOUSE:onafterRequestSpawned(From, Event, To, Request, CargoGroupSet
-- Get cargo group object.
local group=Cargo:GetObject() --Wrapper.Group#GROUP
-- Get request.
-- Get request.
local request=warehouse:_GetRequestOfGroup(group, warehouse.pending)
-- Add cargo group to this carrier.
@ -4810,6 +4901,13 @@ function WAREHOUSE:onbeforeArrived(From, Event, To, group)
local asset=self:FindAssetInDB(group)
if asset then
if asset.flightgroup and not asset.arrived then
--env.info("FF asset has a flightgroup. arrival will be handled there!")
asset.arrived=true
return false
end
if asset.arrived==true then
-- Asset already arrived (e.g. if multiple units trigger the event via landing).
return false
@ -4817,6 +4915,7 @@ function WAREHOUSE:onbeforeArrived(From, Event, To, group)
asset.arrived=true --ensure this is not called again from the same asset group.
return true
end
end
end
@ -5717,21 +5816,32 @@ end
-- @param #WAREHOUSE.Queueitem request Request belonging to this asset. Needed for the name/alias.
-- @param #table parking Parking data for this asset.
-- @param #boolean uncontrolled Spawn aircraft in uncontrolled state.
-- @param #boolean hotstart Spawn aircraft with engines already on. Default is a cold start with engines off.
-- @return Wrapper.Group#GROUP The spawned group or nil if the group could not be spawned.
function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrolled, hotstart)
function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrolled)
if asset and asset.category==Group.Category.AIRPLANE or asset.category==Group.Category.HELICOPTER then
-- Prepare the spawn template.
local template=self:_SpawnAssetPrepareTemplate(asset, alias)
-- Cold start (default).
local _type=COORDINATE.WaypointType.TakeOffParking
local _action=COORDINATE.WaypointAction.FromParkingArea
-- Hot start.
if asset.takeoffType and asset.takeoffType==COORDINATE.WaypointType.TakeOffParkingHot then
_type=COORDINATE.WaypointType.TakeOffParkingHot
_action=COORDINATE.WaypointAction.FromParkingAreaHot
uncontrolled=false
end
-- Set route points.
if request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then
-- Get flight path if the group goes to another warehouse by itself.
if request.toself then
local wp=self.airbase:GetCoordinate():WaypointAir("RADIO", COORDINATE.WaypointType.TakeOffParking, COORDINATE.WaypointAction.FromParkingArea, 0, false, self.airbase, {}, "Parking")
local wp=self.airbase:GetCoordinate():WaypointAir("RADIO", _type, _action, 0, false, self.airbase, {}, "Parking")
template.route.points={wp}
else
template.route.points=self:_GetFlightplan(asset, self.airbase, request.warehouse.airbase)
@ -5739,18 +5849,8 @@ function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrol
else
-- Cold start (default).
local _type=COORDINATE.WaypointType.TakeOffParking
local _action=COORDINATE.WaypointAction.FromParkingArea
-- Hot start.
if hotstart then
_type=COORDINATE.WaypointType.TakeOffParkingHot
_action=COORDINATE.WaypointAction.FromParkingAreaHot
end
-- First route point is the warehouse airbase.
template.route.points[1]=self.airbase:GetCoordinate():WaypointAir("BARO",_type,_action, 0, true, self.airbase, nil, "Spawnpoint")
template.route.points[1]=self.airbase:GetCoordinate():WaypointAir("BARO", _type, _action, 0, true, self.airbase, nil, "Spawnpoint")
end
@ -5969,7 +6069,7 @@ function WAREHOUSE:_RouteGround(group, request)
end
for n,wp in ipairs(Waypoints) do
env.info(n)
--env.info(n)
local tf=self:_SimpleTaskFunctionWP("warehouse:_PassingWaypoint",group, n, #Waypoints)
group:SetTaskWaypoint(wp, tf)
end
@ -7035,11 +7135,16 @@ function WAREHOUSE:_CheckRequestValid(request)
valid=false
end
elseif request.transporttype==WAREHOUSE.TransportType.SHIP then
elseif request.transporttype==WAREHOUSE.TransportType.SHIP or request.transporttype==WAREHOUSE.TransportType.AIRCRAFTCARRIER
or request.transporttype==WAREHOUSE.TransportType.ARMEDSHIP or request.transporttype==WAREHOUSE.TransportType.WARSHIP then
-- Transport by ship.
self:E("ERROR: Incorrect request. Transport by SHIP not implemented yet!")
valid=false
local shippinglane=self:HasConnectionNaval(request.warehouse)
if not shippinglane then
self:E("ERROR: Incorrect request. No shipping lane has been defined between warehouses!")
valid=false
end
elseif request.transporttype==WAREHOUSE.TransportType.TRAIN then
@ -7164,24 +7269,35 @@ function WAREHOUSE:_CheckRequestNow(request)
_assetcategory=_assets[1].category
-- Check available parking for air asset units.
if self.airbase and (_assetcategory==Group.Category.AIRPLANE or _assetcategory==Group.Category.HELICOPTER) then
if _assetcategory==Group.Category.AIRPLANE or _assetcategory==Group.Category.HELICOPTER then
if self:IsRunwayOperational() then
local Parking=self:_FindParkingForAssets(self.airbase,_assets)
if self.airbase and self.airbase:GetCoalition()==self:GetCoalition() then
if self:IsRunwayOperational() then
--if Parking==nil and not (self.category==Airbase.Category.HELIPAD) then
if Parking==nil then
local text=string.format("Warehouse %s: Request denied! Not enough free parking spots for all requested assets at the moment.", self.alias)
local Parking=self:_FindParkingForAssets(self.airbase,_assets)
--if Parking==nil and not (self.category==Airbase.Category.HELIPAD) then
if Parking==nil then
local text=string.format("Warehouse %s: Request denied! Not enough free parking spots for all requested assets at the moment.", self.alias)
self:_InfoMessage(text, 5)
return false
end
else
-- Runway destroyed.
local text=string.format("Warehouse %s: Request denied! Runway is still destroyed", self.alias)
self:_InfoMessage(text, 5)
return false
return false
end
else
-- Runway destroyed.
local text=string.format("Warehouse %s: Request denied! Runway is still destroyed", self.alias)
-- No airbase!
local text=string.format("Warehouse %s: Request denied! No airbase", self.alias)
self:_InfoMessage(text, 5)
return false
end
end
@ -7205,14 +7321,37 @@ function WAREHOUSE:_CheckRequestNow(request)
local _transportcategory=_transports[1].category
-- Check available parking for transport units.
if self.airbase and (_transportcategory==Group.Category.AIRPLANE or _transportcategory==Group.Category.HELICOPTER) then
local Parking=self:_FindParkingForAssets(self.airbase,_transports)
if Parking==nil then
local text=string.format("Warehouse %s: Request denied! Not enough free parking spots for all transports at the moment.", self.alias)
self:_InfoMessage(text, 5)
return false
if _transportcategory==Group.Category.AIRPLANE or _transportcategory==Group.Category.HELICOPTER then
if self.airbase and self.airbase:GetCoalition()==self:GetCoalition() then
if self:IsRunwayOperational() then
local Parking=self:_FindParkingForAssets(self.airbase,_transports)
-- No parking ==> return false
if Parking==nil then
local text=string.format("Warehouse %s: Request denied! Not enough free parking spots for all transports at the moment.", self.alias)
self:_InfoMessage(text, 5)
return false
end
else
-- Runway destroyed.
local text=string.format("Warehouse %s: Request denied! Runway is still destroyed", self.alias)
self:_InfoMessage(text, 5)
return false
end
else
-- No airbase
local text=string.format("Warehouse %s: Request denied! No airbase currently!", self.alias)
self:_InfoMessage(text, 5)
return false
end
end
else
@ -7488,7 +7627,7 @@ function WAREHOUSE:_CheckQueue()
-- Check if request is possible now.
local okay=false
if valid then
if valid then
okay=self:_CheckRequestNow(qitem)
else
-- Remember invalid request and delete later in order not to confuse the loop.
@ -7728,7 +7867,7 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets)
local parkingspot=_parkingspot --Wrapper.Airbase#AIRBASE.ParkingSpot
-- Check correct terminal type for asset. We don't want helos in shelters etc.
if AIRBASE._CheckTerminalType(parkingspot.TerminalType, terminaltype) and self:_CheckParkingValid(parkingspot) then
if AIRBASE._CheckTerminalType(parkingspot.TerminalType, terminaltype) and self:_CheckParkingValid(parkingspot) and self:_CheckParkingAsset(parkingspot, asset) and airbase:_CheckParkingLists(parkingspot.TerminalID) then
-- Coordinate of the parking spot.
local _spot=parkingspot.Coordinate -- Core.Point#COORDINATE
@ -8163,10 +8302,9 @@ function WAREHOUSE:_GetAttribute(group)
-- Ships
local aircraftcarrier=group:HasAttribute("Aircraft Carriers")
local warship=group:HasAttribute("Heavy armed ships")
local armedship=group:HasAttribute("Armed ships")
local armedship=group:HasAttribute("Armed ships") or group:HasAttribute("Armed Ship")
local unarmedship=group:HasAttribute("Unarmed ships")
-- Define attribute. Order is important.
if transportplane then
attribute=WAREHOUSE.Attribute.AIR_TRANSPORTPLANE
@ -8929,9 +9067,23 @@ function WAREHOUSE:_GetFlightplan(asset, departure, destination)
local wp={}
local c={}
-- Cold start (default).
local _type=COORDINATE.WaypointType.TakeOffParking
local _action=COORDINATE.WaypointAction.FromParkingArea
-- Hot start.
if asset.takeoffType and asset.takeoffType==COORDINATE.WaypointType.TakeOffParkingHot then
--env.info("FF hot")
_type=COORDINATE.WaypointType.TakeOffParkingHot
_action=COORDINATE.WaypointAction.FromParkingAreaHot
else
--env.info("FF cold")
end
--- Departure/Take-off
c[#c+1]=Pdeparture
wp[#wp+1]=Pdeparture:WaypointAir("RADIO", COORDINATE.WaypointType.TakeOffParking, COORDINATE.WaypointAction.FromParkingArea, VxClimb*3.6, true, departure, nil, "Departure")
wp[#wp+1]=Pdeparture:WaypointAir("RADIO", _type, _action, VxClimb*3.6, true, departure, nil, "Departure")
--- Begin of Cruise
local Pcruise=Pdeparture:Translate(d_climb, heading)

View File

@ -1,5 +1,4 @@
-- The order of the declarations is important here. Don't touch it.
--- GLOBALS: The order of the declarations is important here. Don't touch it.
--- Declare the event dispatcher based on the EVENT class
_EVENTDISPATCHER = EVENT:New() -- Core.Event#EVENT
@ -10,10 +9,38 @@ _SCHEDULEDISPATCHER = SCHEDULEDISPATCHER:New() -- Core.ScheduleDispatcher#SCHEDU
--- Declare the main database object, which is used internally by the MOOSE classes.
_DATABASE = DATABASE:New() -- Core.Database#DATABASE
--- Settings
_SETTINGS = SETTINGS:Set()
_SETTINGS:SetPlayerMenuOn()
--- Register cargos.
_DATABASE:_RegisterCargos()
--- Register zones.
_DATABASE:_RegisterZones()
_DATABASE:_RegisterAirbases()
--- Check if os etc is available.
BASE:I("Checking de-sanitization of os, io and lfs:")
local __na=false
if os then
BASE:I("- os available")
else
BASE:I("- os NOT available! Some functions may not work.")
__na=true
end
if io then
BASE:I("- io available")
else
BASE:I("- io NOT available! Some functions may not work.")
__na=true
end
if lfs then
BASE:I("- lfs available")
else
BASE:I("- lfs NOT available! Some functions may not work.")
__na=true
end
if __na then
BASE:I("Check <DCS install folder>/Scripts/MissionScripting.lua and comment out the lines with sanitizeModule(''). Use at your own risk!)")
end

View File

@ -2,10 +2,12 @@ __Moose.Include( 'Scripts/Moose/Utilities/Enums.lua' )
__Moose.Include( 'Scripts/Moose/Utilities/Routines.lua' )
__Moose.Include( 'Scripts/Moose/Utilities/Utils.lua' )
__Moose.Include( 'Scripts/Moose/Utilities/Profiler.lua' )
__Moose.Include( 'Scripts/Moose/Utilities/Templates.lua' )
__Moose.Include( 'Scripts/Moose/Utilities/STTS.lua' )
__Moose.Include( 'Scripts/Moose/Core/Base.lua' )
__Moose.Include( 'Scripts/Moose/Core/Beacon.lua' )
__Moose.Include( 'Scripts/Moose/Core/UserFlag.lua' )
__Moose.Include( 'Scripts/Moose/Core/UserSound.lua' )
__Moose.Include( 'Scripts/Moose/Core/Report.lua' )
__Moose.Include( 'Scripts/Moose/Core/Scheduler.lua' )
__Moose.Include( 'Scripts/Moose/Core/ScheduleDispatcher.lua' )
@ -20,15 +22,13 @@ __Moose.Include( 'Scripts/Moose/Core/Point.lua' )
__Moose.Include( 'Scripts/Moose/Core/Velocity.lua' )
__Moose.Include( 'Scripts/Moose/Core/Message.lua' )
__Moose.Include( 'Scripts/Moose/Core/Fsm.lua' )
__Moose.Include( 'Scripts/Moose/Core/Radio.lua' )
__Moose.Include( 'Scripts/Moose/Core/RadioQueue.lua' )
__Moose.Include( 'Scripts/Moose/Core/RadioSpeech.lua' )
__Moose.Include( 'Scripts/Moose/Core/Spawn.lua' )
__Moose.Include( 'Scripts/Moose/Core/SpawnStatic.lua' )
__Moose.Include( 'Scripts/Moose/Core/Timer.lua' )
__Moose.Include( 'Scripts/Moose/Core/Goal.lua' )
__Moose.Include( 'Scripts/Moose/Core/Spot.lua' )
__Moose.Include( 'Scripts/Moose/Core/Astar.lua' )
__Moose.Include( 'Scripts/Moose/Core/MarkerOps_Base.lua' )
__Moose.Include( 'Scripts/Moose/Wrapper/Object.lua' )
__Moose.Include( 'Scripts/Moose/Wrapper/Identifiable.lua' )
@ -68,6 +68,8 @@ __Moose.Include( 'Scripts/Moose/Functional/Suppression.lua' )
__Moose.Include( 'Scripts/Moose/Functional/PseudoATC.lua' )
__Moose.Include( 'Scripts/Moose/Functional/Warehouse.lua' )
__Moose.Include( 'Scripts/Moose/Functional/Fox.lua' )
__Moose.Include( 'Scripts/Moose/Functional/Mantis.lua' )
__Moose.Include( 'Scripts/Moose/Functional/Shorad.lua' )
__Moose.Include( 'Scripts/Moose/Ops/Airboss.lua' )
__Moose.Include( 'Scripts/Moose/Ops/RecoveryTanker.lua' )
@ -85,6 +87,9 @@ __Moose.Include( 'Scripts/Moose/Ops/Intelligence.lua' )
__Moose.Include( 'Scripts/Moose/Ops/ChiefOfStaff.lua' )
__Moose.Include( 'Scripts/Moose/Ops/Army.lua' )
__Moose.Include( 'Scripts/Moose/Ops/Platoon.lua' )
__Moose.Include( 'Scripts/Moose/Ops/OpsTransport.lua' )
__Moose.Include( 'Scripts/Moose/Ops/CSAR.lua' )
__Moose.Include( 'Scripts/Moose/Ops/CTLD.lua' )
__Moose.Include( 'Scripts/Moose/AI/AI_Balancer.lua' )
__Moose.Include( 'Scripts/Moose/AI/AI_Air.lua' )
@ -111,16 +116,25 @@ __Moose.Include( 'Scripts/Moose/AI/AI_Cargo.lua' )
__Moose.Include( 'Scripts/Moose/AI/AI_Cargo_APC.lua' )
__Moose.Include( 'Scripts/Moose/AI/AI_Cargo_Helicopter.lua' )
__Moose.Include( 'Scripts/Moose/AI/AI_Cargo_Airplane.lua' )
__Moose.Include( 'Scripts/Moose/AI/AI_Cargo_Ship.lua' )
__Moose.Include( 'Scripts/Moose/AI/AI_Cargo_Dispatcher.lua' )
__Moose.Include( 'Scripts/Moose/AI/AI_Cargo_Dispatcher_APC.lua' )
__Moose.Include( 'Scripts/Moose/AI/AI_Cargo_Dispatcher_Helicopter.lua' )
__Moose.Include( 'Scripts/Moose/AI/AI_Cargo_Dispatcher_Airplane.lua' )
__Moose.Include( 'Scripts/Moose/AI/AI_Cargo_Dispatcher_Ship.lua' )
__Moose.Include( 'Scripts/Moose/Actions/Act_Assign.lua' )
__Moose.Include( 'Scripts/Moose/Actions/Act_Route.lua' )
__Moose.Include( 'Scripts/Moose/Actions/Act_Account.lua' )
__Moose.Include( 'Scripts/Moose/Actions/Act_Assist.lua' )
__Moose.Include( 'Scripts/Moose/Sound/UserSound.lua' )
__Moose.Include( 'Scripts/Moose/Sound/SoundOutput.lua' )
__Moose.Include( 'Scripts/Moose/Sound/Radio.lua' )
__Moose.Include( 'Scripts/Moose/Sound/RadioQueue.lua' )
__Moose.Include( 'Scripts/Moose/Sound/RadioSpeech.lua' )
__Moose.Include( 'Scripts/Moose/Sound/SRS.lua' )
__Moose.Include( 'Scripts/Moose/Tasking/CommandCenter.lua' )
__Moose.Include( 'Scripts/Moose/Tasking/Mission.lua' )
__Moose.Include( 'Scripts/Moose/Tasking/Task.lua' )

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -27,14 +27,19 @@
-- **Supported Carriers:**
--
-- * [USS John C. Stennis](https://en.wikipedia.org/wiki/USS_John_C._Stennis) (CVN-74)
-- * [USS Theodore Roosevelt](https://en.wikipedia.org/wiki/USS_Theodore_Roosevelt_(CVN-71)) (CVN-71) [Super Carrier Module]
-- * [USS Abraham Lincoln](https://en.wikipedia.org/wiki/USS_Abraham_Lincoln_(CVN-72)) (CVN-72) [Super Carrier Module]
-- * [USS George Washington](https://en.wikipedia.org/wiki/USS_George_Washington_(CVN-73)) (CVN-73) [Super Carrier Module]
-- * [USS Harry S. Truman](https://en.wikipedia.org/wiki/USS_Harry_S._Truman) (CVN-75) [Super Carrier Module]
-- * [USS Tarawa](https://en.wikipedia.org/wiki/USS_Tarawa_(LHA-1)) (LHA-1) [**WIP**]
--
-- **Supported Aircraft:**
--
-- * [F/A-18C Hornet Lot 20](https://forums.eagle.ru/forumdisplay.php?f=557) (Player & AI)
-- * [F-14B Tomcat](https://forums.eagle.ru/forumdisplay.php?f=395) (Player & AI)
-- * [F-14A/B Tomcat](https://forums.eagle.ru/forumdisplay.php?f=395) (Player & AI)
-- * [A-4E Skyhawk Community Mod](https://forums.eagle.ru/showthread.php?t=224989) (Player & AI)
-- * [AV-8B N/A Harrier](https://forums.eagle.ru/forumdisplay.php?f=555) (Player & AI) [**WIP**]
-- * [T-45C Goshawk](https://www.vnao-cvw-7.com/t-45-goshawk) (VNAO)(Player & AI) [**WIP**]
-- * F/A-18C Hornet (AI)
-- * F-14A Tomcat (AI)
-- * E-2D Hawkeye (AI)
@ -47,7 +52,9 @@
-- the no other fixed wing aircraft (human or AI controlled) are supposed to land on the Tarawa. Currently only Case I is supported. Case II/III take slightly steps from the CVN carrier.
-- However, the two Case II/III pattern are very similar so this is not a big drawback.
--
-- Heatblur's mighty F-14B Tomcat has been added (March 13th 2019) as well.
-- Heatblur's mighty F-14B Tomcat has been added (March 13th 2019) as well. Same goes for the A version.
--
-- The [DCS Supercarriers](https://forums.eagle.ru/forum/151-dcs-supercarrier/) are also supported.
--
-- ## Discussion
--
@ -294,7 +301,7 @@
--
-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case3.png)
--
-- A Case III recovery is conducted during nighttime. The holding position and the landing pattern are rather different from a Case I recovery as can be seen in the image above.
-- A Case III recovery is conducted during nighttime or when the visibility is below CASE II minima during the day. The holding position and the landing pattern are rather different from a Case I recovery as can be seen in the image above.
--
-- The first holding zone starts 21 NM astern the carrier at angels 6. The separation between the stacks is 1000 ft just like in Case I. However, the distance to the boat
-- increases by 1 NM with each stack. The general form can be written as D=15+6+(N-1), where D is the distance to the boat in NM and N the number of the stack starting at N=1.
@ -566,7 +573,10 @@
-- * **L**ined **U**p **L**eft or **R**ight: LUL, LUR
-- * Too **H**igh or too **LO**w: H, LO
-- * Too **F**ast or too **SLO**w: F, SLO
-- * **Fly through** glideslope **down** or **up**: \\ , /
-- * **O**ver**S**hoot: OS, only referenced during **X**
-- * **Fly through** glideslope **down** or **up**: \\ , /, advisory only
-- * **D**rift **L**eft or **R**ight:DL, DR, advisory only
-- * **A**ngled **A**pproach: Angled approach (wings level and LUL): AA, advisory only
--
-- Each grading, x, is subdivided by
--
@ -627,7 +637,7 @@
--
-- ## Foul Deck Waveoff
--
-- A foul deck waveoff is called by the LSO if an aircraft is detected within the landing area when an approaching aircraft is crossing the ship's wake during Case I/II operations,
-- A foul deck waveoff is called by the LSO if an aircraft is detected within the landing area when an approaching aircraft is at position IM-IC during Case I/II operations,
-- or with an aircraft approaching the 3/4 NM during Case III operations.
--
-- The approaching aircraft will be notified via LSO radio comms and is supposed to overfly the landing area to enter the Bolter pattern. **This pass is not graded**.
@ -1257,6 +1267,7 @@ AIRBOSS = {
-- @field #string S3BTANKER Lockheed S-3B Viking tanker.
-- @field #string E2D Grumman E-2D Hawkeye AWACS.
-- @field #string C2A Grumman C-2A Greyhound from Military Aircraft Mod.
-- @field #string T45C T-45C by VNAO
AIRBOSS.AircraftCarrier={
AV8B="AV8BNA",
HORNET="FA-18C_hornet",
@ -1265,6 +1276,7 @@ AIRBOSS.AircraftCarrier={
F14B="F-14B",
F14A_AI="F-14A",
FA18C="F/A-18C",
T45C="T-45",
S3B="S-3B",
S3BTANKER="S-3B Tanker",
E2D="E-2C",
@ -1332,6 +1344,8 @@ AIRBOSS.CarrierType={
-- @field #number _min Min _OK_ value. Default -0.5 deg.
-- @field #number Left (LUR) threshold. Default -1.0 deg.
-- @field #number Right (LUL) threshold. Default 1.0 deg.
-- @field #number LeftMed threshold for AA/OS measuring. Default -2.0 deg.
-- @field #number RightMed threshold for AA/OS measuring. Default 2.0 deg.
-- @field #number LEFT LUR threshold. Default -3.0 deg.
-- @field #number RIGHT LUL threshold. Default 3.0 deg.
@ -1699,7 +1713,7 @@ AIRBOSS.MenuF10Root=nil
--- Airboss class version.
-- @field #string version
AIRBOSS.version="1.1.5"
AIRBOSS.version="1.1.6"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
@ -1933,7 +1947,7 @@ function AIRBOSS:New(carriername, alias)
-- Welcome players.
self:SetWelcomePlayers(true)
-- Coordinates
self.landingcoord=COORDINATE:New(0,0,0) --Core.Point#COORDINATE
self.sterncoord=COORDINATE:New(0, 0, 0) --Core.Point#COORDINATE
@ -1945,11 +1959,11 @@ function AIRBOSS:New(carriername, alias)
elseif self.carriertype==AIRBOSS.CarrierType.ROOSEVELT then
self:_InitNimitz()
elseif self.carriertype==AIRBOSS.CarrierType.LINCOLN then
self:_InitNimitz()
elseif self.carriertype==AIRBOSS.CarrierType.WASHINGTON then
self:_InitNimitz()
elseif self.carriertype==AIRBOSS.CarrierType.TRUMAN then
self:_InitNimitz()
elseif self.carriertype==AIRBOSS.CarrierType.WASHINGTON then
self:_InitNimitz()
elseif self.carriertype==AIRBOSS.CarrierType.TRUMAN then
self:_InitNimitz()
elseif self.carriertype==AIRBOSS.CarrierType.VINSON then
-- TODO: Carl Vinson parameters.
self:_InitStennis()
@ -1987,7 +2001,7 @@ function AIRBOSS:New(carriername, alias)
self:_GetZoneGroove():SmokeZone(SMOKECOLOR.Red, 5)
self:_GetZoneLineup():SmokeZone(SMOKECOLOR.Green, 5)
self:_GetZoneBullseye(case):SmokeZone(SMOKECOLOR.White, 45)
self:_GetZoneDirtyUp(case):SmokeZone(SMOKECOLOR.Orange, 45)
self:_GetZoneDirtyUp(case):SmokeZone(SMOKECOLOR.Orange, 45)
self:_GetZoneArcIn(case):SmokeZone(SMOKECOLOR.Blue, 45)
self:_GetZoneArcOut(case):SmokeZone(SMOKECOLOR.Blue, 45)
self:_GetZonePlatform(case):SmokeZone(SMOKECOLOR.Blue, 45)
@ -2027,7 +2041,7 @@ function AIRBOSS:New(carriername, alias)
-- Bow
bow:FlareYellow()
-- Runway half width = 10 m.
local r1=stern:Translate(self.carrierparam.rwywidth*0.5, FB+90)
local r2=stern:Translate(self.carrierparam.rwywidth*0.5, FB-90)
@ -2640,6 +2654,15 @@ function AIRBOSS:SetRecoveryTurnTime(interval)
return self
end
--- Set multiplayer environment wire correction.
-- @param #AIRBOSS self
-- @param #number Dcorr Correction distance in meters. Default 12 m.
-- @return #AIRBOSS self
function AIRBOSS:SetMPWireCorrection(Dcorr)
self.mpWireCorrection=Dcorr or 12
return self
end
--- Set time interval for updating queues and other stuff.
-- @param #AIRBOSS self
-- @param #number interval Time interval in seconds. Default 30 sec.
@ -2798,16 +2821,20 @@ end
-- @param #number _max
-- @param #number _min
-- @param #number Left
-- @param #number LeftMed
-- @param #number LEFT
-- @param #number Right
-- @param #number RightMed
-- @param #number RIGHT
-- @return #AIRBOSS self
function AIRBOSS:SetLineupErrorThresholds(_max,_min, Left, LEFT, Right, RIGHT)
function AIRBOSS:SetLineupErrorThresholds(_max,_min, Left, LeftMed, LEFT, Right, RightMed, RIGHT)
self.lue._max=_max or 0.5
self.lue._min=_min or -0.5
self.lue.Left=Left or -1.0
self.lue.LeftMed=LeftMed or -2.0
self.lue.LEFT=LEFT or -3.0
self.lue.Right=Right or 1.0
self.lue.RightMed=RightMed or 2.0
self.lue.RIGHT=RIGHT or 3.0
return self
end
@ -3278,7 +3305,7 @@ function AIRBOSS:GetNextRecoveryTime(InSeconds)
if InSeconds then
return self.recoverywindow.START, self.recoverywindow.STOP
else
return UTILS.SecondsToClock(self.recoverywindow.START), UTILS.SecondsToClock(self.recoverywindow.STOP)
return UTILS.SecondsToClock(self.recoverywindow.START), UTILS.SecondsToClock(self.recoverywindow.STOP)
end
else
if InSeconds then
@ -3392,7 +3419,7 @@ function AIRBOSS:onafterStart(From, Event, To)
--self.StatusScheduler=SCHEDULER:New(self)
--self.StatusScheduler:Schedule(self, self._Status, {}, 1, 0.5)
self.StatusTimer=TIMER:New(self._Status, self):Start(2, 0.5)
-- Start status check in 1 second.
@ -3478,9 +3505,9 @@ function AIRBOSS:onafterStatus(From, Event, To)
-- Disable turn into the wind for this window so that we do not do this all over again.
self.recoverywindow.WIND=false
end
end
end
@ -3574,36 +3601,39 @@ function AIRBOSS:_CheckAIStatus()
-- Unit
local unit=element.unit
-- Get lineup and distance to carrier.
local lineup=self:_Lineup(unit, true)
local unitcoord=unit:GetCoord()
local dist=unitcoord:Get2DDistance(self:GetCoord())
if unit and unit:IsAlive() then
-- Distance in NM.
local distance=UTILS.MetersToNM(dist)
-- Get lineup and distance to carrier.
local lineup=self:_Lineup(unit, true)
-- Altitude in ft.
local alt=UTILS.MetersToFeet(unitcoord.y)
local unitcoord=unit:GetCoord()
-- Check if parameters are right and flight is in the groove.
if lineup<2 and distance<=0.75 and alt<500 and not element.ballcall then
local dist=unitcoord:Get2DDistance(self:GetCoord())
-- Paddles: Call the ball!
self:RadioTransmission(self.LSORadio, self.LSOCall.CALLTHEBALL, nil, nil, nil, true)
-- Distance in NM.
local distance=UTILS.MetersToNM(dist)
-- Pilot: "405, Hornet Ball, 3.2"
self:_LSOCallAircraftBall(element.onboard,self:_GetACNickname(unit:GetTypeName()), self:_GetFuelState(unit)/1000)
-- Altitude in ft.
local alt=UTILS.MetersToFeet(unitcoord.y)
-- Paddles: Roger ball after 0.5 seconds.
self:RadioTransmission(self.LSORadio, self.LSOCall.ROGERBALL, nil, nil, 0.5, true)
-- Check if parameters are right and flight is in the groove.
if lineup<2 and distance<=0.75 and alt<500 and not element.ballcall then
-- Flight element called the ball.
element.ballcall=true
-- Paddles: Call the ball!
self:RadioTransmission(self.LSORadio, self.LSOCall.CALLTHEBALL, nil, nil, nil, true)
-- This is for the whole flight. Maybe we need it.
flight.ballcall=true
-- Pilot: "405, Hornet Ball, 3.2"
self:_LSOCallAircraftBall(element.onboard,self:_GetACNickname(unit:GetTypeName()), self:_GetFuelState(unit)/1000)
-- Paddles: Roger ball after 0.5 seconds.
self:RadioTransmission(self.LSORadio, self.LSOCall.ROGERBALL, nil, nil, 0.5, true)
-- Flight element called the ball.
element.ballcall=true
-- This is for the whole flight. Maybe we need it.
flight.ballcall=true
end
end
end
@ -4139,7 +4169,7 @@ end
-- @param #string To To state.
function AIRBOSS:onafterStop(From, Event, To)
self:I(self.lid..string.format("Stopping airboss script."))
-- Unhandle events.
self:UnHandleEvent(EVENTS.Birth)
self:UnHandleEvent(EVENTS.Land)
@ -4163,7 +4193,7 @@ function AIRBOSS:_InitStennis()
-- Carrier Parameters.
self.carrierparam.sterndist =-153
self.carrierparam.deckheight = 19
self.carrierparam.deckheight = 19.06
-- Total size of the carrier (approx as rectangle).
self.carrierparam.totlength=310 -- Wiki says 332.8 meters overall length.
@ -4171,7 +4201,7 @@ function AIRBOSS:_InitStennis()
self.carrierparam.totwidthstarboard=30
-- Landing runway.
self.carrierparam.rwyangle = -9
self.carrierparam.rwyangle = -9.1359
self.carrierparam.rwylength = 225
self.carrierparam.rwywidth = 20
@ -4314,7 +4344,7 @@ function AIRBOSS:_InitNimitz()
-- Carrier Parameters.
self.carrierparam.sterndist =-164
self.carrierparam.deckheight = 20
self.carrierparam.deckheight = 20.1494 --DCS World OpenBeta\CoreMods\tech\USS_Nimitz\Database\USS_CVN_7X.lua
-- Total size of the carrier (approx as rectangle).
self.carrierparam.totlength=332.8 -- Wiki says 332.8 meters overall length.
@ -4322,7 +4352,7 @@ function AIRBOSS:_InitNimitz()
self.carrierparam.totwidthstarboard=35
-- Landing runway.
self.carrierparam.rwyangle = -9
self.carrierparam.rwyangle = -9.1359 --DCS World OpenBeta\CoreMods\tech\USS_Nimitz\scripts\USS_Nimitz_RunwaysAndRoutes.lua
self.carrierparam.rwylength = 250
self.carrierparam.rwywidth = 25
@ -5465,6 +5495,7 @@ function AIRBOSS:_GetAircraftAoA(playerData)
-- Get AC type.
local hornet=playerData.actype==AIRBOSS.AircraftCarrier.HORNET
local goshawk=playerData.actype==AIRBOSS.AircraftCarrier.T45C
local skyhawk=playerData.actype==AIRBOSS.AircraftCarrier.A4EC
local harrier=playerData.actype==AIRBOSS.AircraftCarrier.AV8B
local tomcat=playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B
@ -5491,6 +5522,15 @@ function AIRBOSS:_GetAircraftAoA(playerData)
aoa.OnSpeedMin = self:_AoAUnit2Deg(playerData, 14.5) --14.17 --14.5 units
aoa.Fast = self:_AoAUnit2Deg(playerData, 14.0) --13.33 --14.0 units
aoa.FAST = self:_AoAUnit2Deg(playerData, 13.0) --11.67 --13.0 units
elseif goshawk then
-- T-45C Goshawk parameters.
aoa.SLOW = 8.00 --19
aoa.Slow = 7.75 --18
aoa.OnSpeedMax = 7.25 --17.5
aoa.OnSpeed = 7.00 --17
aoa.OnSpeedMin = 6.75 --16.5
aoa.Fast = 6.25 --16
aoa.FAST = 6.00 --15
elseif skyhawk then
-- A-4E-C Skyhawk parameters from https://forums.eagle.ru/showpost.php?p=3703467&postcount=390
-- Note that these are arbitrary UNITS and not degrees. We need a conversion formula!
@ -5675,6 +5715,9 @@ function AIRBOSS:_GetAircraftParameters(playerData, step)
elseif skyhawk then
alt=UTILS.FeetToMeters(600)
speed=UTILS.KnotsToMps(250)
elseif goshawk then
alt=UTILS.FeetToMeters(800)
speed=UTILS.KnotsToMps(300)
end
elseif step==AIRBOSS.PatternStep.BREAKENTRY then
@ -5685,11 +5728,14 @@ function AIRBOSS:_GetAircraftParameters(playerData, step)
elseif skyhawk then
alt=UTILS.FeetToMeters(600)
speed=UTILS.KnotsToMps(250)
elseif goshawk then
alt=UTILS.FeetToMeters(800)
speed=UTILS.KnotsToMps(300)
end
elseif step==AIRBOSS.PatternStep.EARLYBREAK then
if hornet or tomcat or harrier then
if hornet or tomcat or harrier or goshawk then
alt=UTILS.FeetToMeters(800)
elseif skyhawk then
alt=UTILS.FeetToMeters(600)
@ -5697,7 +5743,7 @@ function AIRBOSS:_GetAircraftParameters(playerData, step)
elseif step==AIRBOSS.PatternStep.LATEBREAK then
if hornet or tomcat or harrier then
if hornet or tomcat or harrier or goshawk then
alt=UTILS.FeetToMeters(800)
elseif skyhawk then
alt=UTILS.FeetToMeters(600)
@ -5705,7 +5751,7 @@ function AIRBOSS:_GetAircraftParameters(playerData, step)
elseif step==AIRBOSS.PatternStep.ABEAM then
if hornet or tomcat or harrier then
if hornet or tomcat or harrier or goshawk then
alt=UTILS.FeetToMeters(600)
elseif skyhawk then
alt=UTILS.FeetToMeters(500)
@ -5720,10 +5766,19 @@ function AIRBOSS:_GetAircraftParameters(playerData, step)
dist=UTILS.NMToMeters(1.2)
end
if goshawk then
-- 0.9 to 1.1 NM per natops ch.4 page 48
dist=UTILS.NMToMeters(0.9)
else
dist=UTILS.NMToMeters(1.1)
end
elseif step==AIRBOSS.PatternStep.NINETY then
if hornet or tomcat then
alt=UTILS.FeetToMeters(500)
elseif goshawk then
alt=UTILS.FeetToMeters(450)
elseif skyhawk then
alt=UTILS.FeetToMeters(500)
elseif harrier then
@ -5734,7 +5789,7 @@ function AIRBOSS:_GetAircraftParameters(playerData, step)
elseif step==AIRBOSS.PatternStep.WAKE then
if hornet then
if hornet or goshawk then
alt=UTILS.FeetToMeters(370)
elseif tomcat then
alt=UTILS.FeetToMeters(430) -- Tomcat should be a bit higher as it intercepts the GS a bit higher.
@ -5747,7 +5802,7 @@ function AIRBOSS:_GetAircraftParameters(playerData, step)
elseif step==AIRBOSS.PatternStep.FINAL then
if hornet then
if hornet or goshawk then
alt=UTILS.FeetToMeters(300)
elseif tomcat then
alt=UTILS.FeetToMeters(360)
@ -6066,6 +6121,11 @@ function AIRBOSS:_ScanCarrierZone()
-- Get flight group if possible.
local knownflight=self:_GetFlightFromGroupInQueue(group, self.flights)
-- Unknown new AI flight. Create a new flight group.
if not knownflight and not self:_IsHuman(group) then
knownflight=self:_CreateFlightGroup(group)
end
-- Get aircraft type name.
local actype=group:GetTypeName()
@ -6079,10 +6139,10 @@ function AIRBOSS:_ScanCarrierZone()
local putintomarshal=false
-- Get flight group.
local flight=_DATABASE:GetFlightGroup(groupname)
local flight=_DATABASE:GetOpsGroup(groupname)
if flight and flight:IsInbound() and flight.destbase:GetName()==self.carrier:GetName() then
if flight.ishelo then
if flight.isHelo then
else
putintomarshal=true
end
@ -6117,16 +6177,13 @@ function AIRBOSS:_ScanCarrierZone()
-- Break the loop to not have all flights at once! Spams the message screen.
break
end -- Closed in or tanker/AWACS
end -- Closed in or tanker/AWACS
end
else
-- Unknown new AI flight. Create a new flight group.
if not self:_IsHuman(group) then
self:_CreateFlightGroup(group)
end
end
@ -6223,7 +6280,7 @@ function AIRBOSS:_MarshalPlayer(playerData, stack)
-- Set stack flag.
flight.flag=stack
-- Trigger Marshal event.
self:Marshal(flight)
end
@ -6482,7 +6539,7 @@ function AIRBOSS:_MarshalAI(flight, nstack, respawn)
-- Route group.
flight.group:Route(wp, 1)
-- Trigger Marshal event.
self:Marshal(flight)
@ -7018,7 +7075,7 @@ function AIRBOSS:_GetFreeStack(ai, case, empty)
-- Recovery case.
case=case or self.case
if case==1 then
return self:_GetFreeStack_Old(ai, case, empty)
end
@ -7034,7 +7091,7 @@ function AIRBOSS:_GetFreeStack(ai, case, empty)
for i=1,nmaxstacks do
stack[i]=self.NmaxStack -- Number of human flights per stack.
end
local nmax=1
-- Loop over all flights in marshal stack.
@ -7046,7 +7103,7 @@ function AIRBOSS:_GetFreeStack(ai, case, empty)
-- Get stack of flight.
local n=flight.flag
if n>nmax then
nmax=n
end
@ -7063,7 +7120,7 @@ function AIRBOSS:_GetFreeStack(ai, case, empty)
end
end
local nfree=nil
if stack[nmax]==0 then
-- Max occupied stack is completely full!
@ -7079,7 +7136,7 @@ function AIRBOSS:_GetFreeStack(ai, case, empty)
-- Case II/III return next stack
nfree=nmax+1
end
elseif stack[nmax]==self.NmaxStack then
-- Max occupied stack is completely empty! This should happen only when there is no other flight in the marshal queue.
self:E(self.lid..string.format("ERROR: Max occupied stack is empty. Should not happen! Nmax=%d, stack[nmax]=%d", nmax, stack[nmax]))
@ -7091,7 +7148,7 @@ function AIRBOSS:_GetFreeStack(ai, case, empty)
else
nfree=nmax
end
end
self:I(self.lid..string.format("Returning free stack %s", tostring(nfree)))
@ -10054,6 +10111,27 @@ function AIRBOSS:_Groove(playerData)
-- Distance in NM.
local d=UTILS.MetersToNM(rho)
-- Drift on lineup.
if rho>=RAR and rho<=RIM then
if gd.LUE>0.22 and lineupError<-0.22 then
env.info" Drift Right across centre ==> DR-"
gd.Drift=" DR"
self:T(self.lid..string.format("Got Drift Right across centre step %s, d=%.3f: Max LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError))
elseif gd.LUE<-0.22 and lineupError>0.22 then
env.info" Drift Left ==> DL-"
gd.Drift=" DL"
self:T(self.lid..string.format("Got Drift Left across centre at step %s, d=%.3f: Min LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError))
elseif gd.LUE>0.13 and lineupError<-0.14 then
env.info" Little Drift Right across centre ==> (DR-)"
gd.Drift=" (DR)"
self:T(self.lid..string.format("Got Little Drift Right across centre at step %s, d=%.3f: Max LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError))
elseif gd.LUE<-0.13 and lineupError>0.14 then
env.info" Little Drift Left across centre ==> (DL-)"
gd.Drift=" (DL)"
self:E(self.lid..string.format("Got Little Drift Left across centre at step %s, d=%.3f: Min LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError))
end
end
-- Update max deviation of line up error.
if math.abs(lineupError)>math.abs(gd.LUE) then
self:T(self.lid..string.format("Got bigger LUE at step %s, d=%.3f: LUE %.3f>%.3f", gs, d, lineupError, gd.LUE))
@ -10364,7 +10442,7 @@ function AIRBOSS:_GetSternCoord()
self.sterncoord:Translate(self.carrierparam.sterndist, hdg, true, true):Translate(7, FB+90, true, true)
else
-- Nimitz SC: translate 8 meters starboard wrt Final bearing.
self.sterncoord:Translate(self.carrierparam.sterndist, hdg, true, true):Translate(8.5, FB+90, true, true)
self.sterncoord:Translate(self.carrierparam.sterndist, hdg, true, true):Translate(9.5, FB+90, true, true)
end
-- Set altitude.
@ -10394,6 +10472,11 @@ function AIRBOSS:_GetWire(Lcoord, dc)
-- Corrected landing distance wrt to stern. Landing distance needs to be reduced due to delayed landing event for human players.
local d=Ldist-dc
-- Multiplayer wire correction.
if self.mpWireCorrection then
d=d-self.mpWireCorrection
end
-- Shift wires from stern to their correct position.
local w1=self.carrierparam.wire1
@ -10485,6 +10568,9 @@ function AIRBOSS:_Trapped(playerData)
elseif playerData.actype==AIRBOSS.AircraftCarrier.A4EC then
-- A-4E gets slowed down much faster the the F/A-18C!
dcorr=56
elseif playerData.actype==AIRBOSS.AircraftCarrier.T45C then
-- T-45 also gets slowed down much faster the the F/A-18C.
dcorr=56
end
-- Get wire.
@ -10611,7 +10697,7 @@ function AIRBOSS:_GetZoneInitial(case)
-- Polygon zone.
--local zone=ZONE_POLYGON_BASE:New("Zone CASE I/II Initial", vec2)
self.zoneInitial:UpdateFromVec2(vec2)
--return zone
@ -10640,13 +10726,13 @@ function AIRBOSS:_GetZoneLineup()
-- Vec2 array.
local vec2={c1:GetVec2(), c2:GetVec2(), c3:GetVec2(), c4:GetVec2(), c5:GetVec2()}
self.zoneLineup:UpdateFromVec2(vec2)
-- Polygon zone.
--local zone=ZONE_POLYGON_BASE:New("Zone Lineup", vec2)
--return zone
return self.zoneLineup
end
@ -10681,13 +10767,13 @@ function AIRBOSS:_GetZoneGroove(l, w, b)
-- Vec2 array.
local vec2={c1:GetVec2(), c2:GetVec2(), c3:GetVec2(), c4:GetVec2(), c5:GetVec2(), c6:GetVec2()}
self.zoneGroove:UpdateFromVec2(vec2)
-- Polygon zone.
--local zone=ZONE_POLYGON_BASE:New("Zone Groove", vec2)
--return zone
return self.zoneGroove
end
@ -10713,7 +10799,7 @@ function AIRBOSS:_GetZoneBullseye(case)
-- Create zone.
local zone=ZONE_RADIUS:New("Zone Bullseye", vec2, radius)
return zone
--self.zoneBullseye=self.zoneBullseye or ZONE_RADIUS:New("Zone Bullseye", vec2, radius)
end
@ -10942,9 +11028,9 @@ function AIRBOSS:_GetZoneCarrierBox()
-- Create polygon zone.
--local zone=ZONE_POLYGON_BASE:New("Carrier Box Zone", vec2)
--return zone
self.zoneCarrierbox:UpdateFromVec2(vec2)
return self.zoneCarrierbox
end
@ -10979,9 +11065,9 @@ function AIRBOSS:_GetZoneRunwayBox()
-- Create polygon zone.
--local zone=ZONE_POLYGON_BASE:New("Landing Runway Zone", vec2)
--return zone
self.zoneRunwaybox:UpdateFromVec2(vec2)
return self.zoneRunwaybox
end
@ -11112,7 +11198,7 @@ function AIRBOSS:_GetZoneHolding(case, stack)
-- Square zone length=7NM width=6 NM behind the carrier starting at angels+15 NM behind the carrier.
-- So stay 0-5 NM (+1 NM error margin) port of carrier.
self.zoneHolding=self.zoneHolding or ZONE_POLYGON_BASE:New("CASE II/III Holding Zone")
self.zoneHolding:UpdateFromVec2(p)
end
@ -11158,12 +11244,12 @@ function AIRBOSS:_GetZoneCommence(case, stack)
-- Create holding zone.
self.zoneCommence=self.zoneCommence or ZONE_RADIUS:New("CASE I Commence Zone")
self.zoneCommence:UpdateFromVec2(Three:GetVec2(), R)
else
-- Case II/III
stack=stack or 1
-- Start point at 21 NM for stack=1.
@ -11191,7 +11277,7 @@ function AIRBOSS:_GetZoneCommence(case, stack)
-- Zone polygon.
self.zoneCommence=self.zoneCommence or ZONE_POLYGON_BASE:New("CASE II/III Commence Zone")
self.zoneCommence:UpdateFromVec2(p)
end
@ -11439,7 +11525,7 @@ end
-- @param #AIRBOSS self
-- @return Core.Point#COORDINATE Optimal landing coordinate.
function AIRBOSS:_GetOptLandingCoordinate()
-- Start with stern coordiante.
self.landingcoord:UpdateFromCoordinate(self:_GetSternCoord())
@ -11558,7 +11644,7 @@ end
--- Get wind speed on carrier deck parallel and perpendicular to runway.
-- @param #AIRBOSS self
-- @param #number alt Altitude in meters. Default 50 m.
-- @param #number alt Altitude in meters. Default 15 m. (change made from 50m from Discord discussion from Sickdog)
-- @return #number Wind component parallel to runway im m/s.
-- @return #number Wind component perpendicular to runway in m/s.
-- @return #number Total wind strength in m/s.
@ -11581,7 +11667,7 @@ function AIRBOSS:GetWindOnDeck(alt)
zc=UTILS.Rotate2D(zc, -self.carrierparam.rwyangle)
-- Wind (from) vector
local vw=cv:GetWindWithTurbulenceVec3(alt or 50)
local vw=cv:GetWindWithTurbulenceVec3(alt or 15)
-- Total wind velocity vector.
-- Carrier velocity has to be negative. If carrier drives in the direction the wind is blowing from, we have less wind in total.
@ -11995,15 +12081,15 @@ function AIRBOSS:_EvalGrooveTime(playerData)
local grade=""
if t<9 then
grade="--"
elseif t<12 then
grade="(OK)"
elseif t<22 then
grade="OK"
grade="_NESA_"
elseif t<15 then
grade="NESA"
elseif t<19 then
grade="OK Groove"
elseif t<=24 then
grade="(OK)"
grade="(LIG)"
else
grade="--"
grade="LIG"
end
-- The unicorn!
@ -12042,9 +12128,9 @@ function AIRBOSS:_LSOgrade(playerData)
local nS=count(G, '%(')
local nN=N-nS-nL
-- Groove time 16-18 sec for a unicorn.
-- Groove time 15-18.99 sec for a unicorn.
local Tgroove=playerData.Tgroove
local TgrooveUnicorn=Tgroove and (Tgroove>=16.0 and Tgroove<=18.0) or false
local TgrooveUnicorn=Tgroove and (Tgroove>=15.0 and Tgroove<=18.99) or false
local grade
local points
@ -12177,7 +12263,35 @@ function AIRBOSS:_Flightdata2Text(playerData, groovestep)
-- Aircraft specific AoA values.
local acaoa=self:_GetAircraftAoA(playerData)
--Angled Approach.
local P=nil
if step==AIRBOSS.PatternStep.GROOVE_XX and ROL<=4.0 and playerData.case<3 then
if LUE>self.lue.RIGHT then
P=underline("AA")
elseif
LUE>self.lue.RightMed then
P="AA "
elseif
LUE>self.lue.Right then
P=little("AA")
end
end
--Overshoot Start.
local O=nil
if step==AIRBOSS.PatternStep.GROOVE_XX then
if LUE<self.lue.LEFT then
O=underline("OS")
elseif
LUE<self.lue.Left then
O="OS"
elseif
LUE<self.lue._min then
O=little("OS")
end
end
-- Speed via AoA. Depends on aircraft type.
local S=nil
if AOA>acaoa.SLOW then
@ -12210,7 +12324,7 @@ function AIRBOSS:_Flightdata2Text(playerData, groovestep)
A=little("LO")
end
-- Line up. Good [-0.5, 0.5]
-- Line up. XX Step replaced by Overshoot start (OS). Good [-0.5, 0.5]
local D=nil
if LUE>self.lue.RIGHT then
D=underline("LUL")
@ -12218,12 +12332,22 @@ function AIRBOSS:_Flightdata2Text(playerData, groovestep)
D="LUL"
elseif LUE>self.lue._max then
D=little("LUL")
elseif LUE<self.lue.LEFT then
D=underline("LUR")
elseif LUE<self.lue.Left then
D="LUR"
elseif LUE<self.lue._min then
D=little("LUR")
elseif playerData.case<3 then
if LUE<self.lue.LEFT and step~=AIRBOSS.PatternStep.GROOVE_XX then
D=underline("LUR")
elseif LUE<self.lue.Left and step~=AIRBOSS.PatternStep.GROOVE_XX then
D="LUR"
elseif LUE<self.lue._min and step~=AIRBOSS.PatternStep.GROOVE_XX then
D=little("LUR")
end
elseif playerData.case==3 then
if LUE<self.lue.LEFT then
D=underline("LUR")
elseif LUE<self.lue.Left then
D="LUR"
elseif LUE<self.lue._min then
D=little("LUR")
end
end
-- Compile.
@ -12233,6 +12357,11 @@ function AIRBOSS:_Flightdata2Text(playerData, groovestep)
if fdata.FlyThrough then
G=G..fdata.FlyThrough
end
-- Angled Approach - doesn't affect score, advisory only.
if P then
G=G..P
n=n
end
-- Speed.
if S then
G=G..S
@ -12248,7 +12377,17 @@ function AIRBOSS:_Flightdata2Text(playerData, groovestep)
G=G..D
n=n+1
end
--Drift in Lineup
if fdata.Drift then
G=G..fdata.Drift
n=n -- Drift doesn't affect score, advisory only.
end
-- Overshoot.
if O then
G=G..O
n=n+1
end
-- Add current step.
local step=self:_GS(step)
step=step:gsub("XX","X")
@ -14074,6 +14213,8 @@ function AIRBOSS:_GetACNickname(actype)
local nickname="unknown"
if actype==AIRBOSS.AircraftCarrier.A4EC then
nickname="Skyhawk"
elseif actype==AIRBOSS.AircraftCarrier.T45C then
nickname="Goshawk"
elseif actype==AIRBOSS.AircraftCarrier.AV8B then
nickname="Harrier"
elseif actype==AIRBOSS.AircraftCarrier.E2D then
@ -15581,8 +15722,12 @@ function AIRBOSS:_MarshalCallRecoveryStart(case)
-- Debug output.
local text=string.format("Starting aircraft recovery Case %d ops.", case)
if case>1 then
text=text..string.format(" Marshal radial %03d°.", radial)
if case==1 then
text=text..string.format(" BRC %03d°.", self:GetBRC())
elseif case==2 then
text=text..string.format(" Marshal radial %03d°. BRC %03d°.", radial, self:GetBRC())
elseif case==3 then
text=text..string.format(" Marshal radial %03d°. Final heading %03d°.", radial, self:GetFinalBearing(false))
end
self:T(self.lid..text)
@ -17794,7 +17939,7 @@ function AIRBOSS:onbeforeSave(From, Event, To, path, filename)
-- Check default path.
if path==nil and not lfs then
self:E(self.lid.."WARNING: lfs not desanitized. Results will be saved in DCS installation root directory rather than your \"Saved Games\DCS\" folder.")
self:E(self.lid.."WARNING: lfs not desanitized. Results will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder.")
end
return true
@ -17901,7 +18046,7 @@ function AIRBOSS:onbeforeLoad(From, Event, To, path, filename)
-- Check default path.
if path==nil and not lfs then
self:E(self.lid.."WARNING: lfs not desanitized. Results will be saved in DCS installation root directory rather than your \"Saved Games\DCS\" folder.")
self:E(self.lid.."WARNING: lfs not desanitized. Results will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder.")
end
-- Set path or default.

View File

@ -38,7 +38,7 @@
-- @field Core.Set#SET_ZONE retreatZones Set of retreat zones.
-- @extends Ops.OpsGroup#OPSGROUP
--- *Your soul may belong to Jesus, but your ass belongs to the marines.* -- Eugene B. Sledge
--- *Your soul may belong to Jesus, but your ass belongs to the marines.* -- Eugene B Sledge
--
-- ===
--
@ -55,16 +55,6 @@ ARMYGROUP = {
engage = {},
}
--- Army group element.
-- @type ARMYGROUP.Element
-- @field #string name Name of the element, i.e. the unit.
-- @field Wrapper.Unit#UNIT unit The UNIT object.
-- @field #string status The element status.
-- @field #string typename Type name.
-- @field #number length Length of element in meters.
-- @field #number width Width of element in meters.
-- @field #number height Height of element in meters.
--- Target
-- @type ARMYGROUP.Target
-- @field Ops.Target#TARGET Target The target.
@ -72,16 +62,16 @@ ARMYGROUP = {
--- Army Group version.
-- @field #string version
ARMYGROUP.version="0.4.0"
ARMYGROUP.version="0.7.0"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO: Retreat.
-- TODO: Suppression of fire.
-- TODO: Check if group is mobile.
-- TODO: F10 menu.
-- DONE: Retreat.
-- DONE: Rearm. Specify a point where to go and wait until ammo is full.
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
@ -90,17 +80,25 @@ ARMYGROUP.version="0.4.0"
--- Create a new ARMYGROUP class object.
-- @param #ARMYGROUP self
-- @param Wrapper.Group#GROUP Group The group object. Can also be given by its group name as `#string`.
-- @param Wrapper.Group#GROUP group The GROUP object. Can also be given by its group name as `#string`.
-- @return #ARMYGROUP self
function ARMYGROUP:New(Group)
function ARMYGROUP:New(group)
-- First check if we already have an OPS group for this group.
local og=_DATABASE:GetOpsGroup(group)
if og then
og:I(og.lid..string.format("WARNING: OPS group already exists in data base!"))
return og
end
-- Inherit everything from FSM class.
local self=BASE:Inherit(self, OPSGROUP:New(Group)) -- #ARMYGROUP
local self=BASE:Inherit(self, OPSGROUP:New(group)) -- #ARMYGROUP
-- Set some string id for output to DCS.log file.
self.lid=string.format("ARMYGROUP %s | ", self.groupname)
-- Defaults
self.isArmygroup=true
self:SetDefaultROE()
self:SetDefaultAlarmstate()
self:SetDetection()
@ -115,13 +113,13 @@ function ARMYGROUP:New(Group)
self:AddTransition("*", "Detour", "OnDetour") -- Make a detour to a coordinate and resume route afterwards.
self:AddTransition("OnDetour", "DetourReached", "Cruising") -- Group reached the detour coordinate.
self:AddTransition("*", "Retreat", "Retreating") --
self:AddTransition("Retreating", "Retreated", "Retreated") --
self:AddTransition("*", "Retreat", "Retreating") -- Order a retreat.
self:AddTransition("Retreating", "Retreated", "Retreated") -- Group retreated.
self:AddTransition("Cruising", "EngageTarget", "Engaging") -- Engage a target
self:AddTransition("Holding", "EngageTarget", "Engaging") -- Engage a target
self:AddTransition("OnDetour", "EngageTarget", "Engaging") -- Engage a target
self:AddTransition("Engaging", "Disengage", "Cruising") -- Engage a target
self:AddTransition("Cruising", "EngageTarget", "Engaging") -- Engage a target from Cruising state
self:AddTransition("Holding", "EngageTarget", "Engaging") -- Engage a target from Holding state
self:AddTransition("OnDetour", "EngageTarget", "Engaging") -- Engage a target from OnDetour state
self:AddTransition("Engaging", "Disengage", "Cruising") -- Disengage and back to cruising.
self:AddTransition("*", "Rearm", "Rearm") -- Group is send to a coordinate and waits until ammo is refilled.
self:AddTransition("Rearm", "Rearming", "Rearming") -- Group has arrived at the rearming coodinate and is waiting to be fully rearmed.
@ -143,7 +141,7 @@ function ARMYGROUP:New(Group)
-- Init waypoints.
self:InitWaypoints()
self:_InitWaypoints()
-- Initialize the group.
self:_InitGroup()
@ -151,8 +149,7 @@ function ARMYGROUP:New(Group)
-- Handle events:
self:HandleEvent(EVENTS.Birth, self.OnEventBirth)
self:HandleEvent(EVENTS.Dead, self.OnEventDead)
self:HandleEvent(EVENTS.RemoveUnit, self.OnEventRemoveUnit)
self:HandleEvent(EVENTS.RemoveUnit, self.OnEventRemoveUnit)
--self:HandleEvent(EVENTS.Hit, self.OnEventHit)
-- Start the status monitoring.
@ -163,6 +160,9 @@ function ARMYGROUP:New(Group)
-- Start check zone timer.
self.timerCheckZone=TIMER:New(self._CheckInZones, self):Start(2, 30)
-- Add OPSGROUP to _DATABASE.
_DATABASE:AddOpsGroup(self)
return self
end
@ -263,6 +263,26 @@ function ARMYGROUP:AddTaskAttackGroup(TargetGroup, WeaponExpend, WeaponType, Clo
return task
end
--- Add a *scheduled* task to transport group(s).
-- @param #ARMYGROUP self
-- @param Core.Set#SET_GROUP GroupSet Set of cargo groups. Can also be a singe @{Wrapper.Group#GROUP} object.
-- @param Core.Zone#ZONE PickupZone Zone where the cargo is picked up.
-- @param Core.Zone#ZONE DeployZone Zone where the cargo is delivered to.
-- @param #string Clock Time when to start the attack.
-- @param #number Prio Priority of the task.
-- @return Ops.OpsGroup#OPSGROUP.Task The task table.
function ARMYGROUP:AddTaskCargoGroup(GroupSet, PickupZone, DeployZone, Clock, Prio)
local DCStask={}
DCStask.id="CargoTransport"
DCStask.params={}
DCStask.params.cargoqueu=1
local task=self:AddTask(DCStask, Clock, nil, Prio)
return task
end
--- Define a set of possible retreat zones.
-- @param #ARMYGROUP self
-- @param Core.Set#SET_ZONE RetreatZoneSet The retreat zone set. Default is an empty set.
@ -342,7 +362,9 @@ function ARMYGROUP:onafterStatus(From, Event, To)
-- FSM state.
local fsmstate=self:GetState()
if self:IsAlive() then
local alive=self:IsAlive()
if alive then
---
-- Detection
@ -370,6 +392,21 @@ function ARMYGROUP:onafterStatus(From, Event, To)
self:_UpdateEngageTarget()
end
-- Check if group is waiting.
if self:IsWaiting() then
if self.Twaiting and self.dTwait then
if timer.getAbsTime()>self.Twaiting+self.dTwait then
self.Twaiting=nil
self.dTwait=nil
self:Cruise()
end
end
end
end
if alive~=nil then
if self.verbose>=1 then
-- Get number of tasks and missions.
@ -378,14 +415,20 @@ function ARMYGROUP:onafterStatus(From, Event, To)
local roe=self:GetROE()
local alarm=self:GetAlarmstate()
local speed=UTILS.MpsToKnots(self.velocity)
local speed=UTILS.MpsToKnots(self.velocity or 0)
local speedEx=UTILS.MpsToKnots(self:GetExpectedSpeed())
local formation=self.option.Formation or "unknown"
local ammo=self:GetAmmoTot()
local cargo=0
for _,_element in pairs(self.elements) do
local element=_element --Ops.OpsGroup#OPSGROUP.Element
cargo=cargo+element.weightCargo
end
-- Info text.
local text=string.format("%s [ROE-AS=%d-%d T/M=%d/%d]: Wp=%d/%d-->%d (final %s), Life=%.1f, Speed=%.1f (%d), Heading=%03d, Ammo=%d",
fsmstate, roe, alarm, nTaskTot, nMissions, self.currentwp, #self.waypoints, self:GetWaypointIndexNext(), tostring(self.passedfinalwp), self.life or 0, speed, speedEx, self.heading, ammo.Total)
local text=string.format("%s [ROE-AS=%d-%d T/M=%d/%d]: Wp=%d/%d-->%d (final %s), Life=%.1f, Speed=%.1f (%d), Heading=%03d, Ammo=%d, Cargo=%.1f",
fsmstate, roe, alarm, nTaskTot, nMissions, self.currentwp, #self.waypoints, self:GetWaypointIndexNext(), tostring(self.passedfinalwp), self.life or 0, speed, speedEx, self.heading or 0, ammo.Total, cargo)
self:I(self.lid..text)
end
@ -393,11 +436,50 @@ function ARMYGROUP:onafterStatus(From, Event, To)
else
-- Info text.
local text=string.format("State %s: Alive=%s", fsmstate, tostring(self:IsAlive()))
self:T2(self.lid..text)
if self.verbose>=1 then
local text=string.format("State %s: Alive=%s", fsmstate, tostring(self:IsAlive()))
self:I(self.lid..text)
end
end
---
-- Elements
---
if self.verbose>=2 then
local text="Elements:"
for i,_element in pairs(self.elements) do
local element=_element --Ops.OpsGroup#OPSGROUP.Element
local name=element.name
local status=element.status
local unit=element.unit
--local life=unit:GetLifeRelative() or 0
local life,life0=self:GetLifePoints(element)
local life0=element.life0
-- Get ammo.
local ammo=self:GetAmmoElement(element)
-- Output text for element.
text=text..string.format("\n[%d] %s: status=%s, life=%.1f/%.1f, guns=%d, rockets=%d, bombs=%d, missiles=%d, cargo=%d/%d kg",
i, name, status, life, life0, ammo.Guns, ammo.Rockets, ammo.Bombs, ammo.Missiles, element.weightCargo, element.weightMaxCargo)
end
if #self.elements==0 then
text=text.." none!"
end
self:I(self.lid..text)
end
---
-- Cargo
---
self:_CheckCargoTransport()
---
-- Tasks & Missions
@ -410,6 +492,26 @@ function ARMYGROUP:onafterStatus(From, Event, To)
self:__Status(-30)
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- DCS Events ==> See OPSGROUP
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Event function handling when a unit is hit.
-- @param #ARMYGROUP self
-- @param Core.Event#EVENTDATA EventData Event data.
function ARMYGROUP:OnEventHit(EventData)
-- Check that this is the right group.
if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then
local unit=EventData.IniUnit
local group=EventData.IniGroup
local unitname=EventData.IniUnitName
-- TODO: suppression
end
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM Events
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
@ -419,7 +521,7 @@ end
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #ARMYGROUP.Element Element The group element.
-- @param Ops.OpsGroup#OPSGROUP.Element Element The group element.
function ARMYGROUP:onafterElementSpawned(From, Event, To, Element)
self:T(self.lid..string.format("Element spawned %s", Element.name))
@ -436,8 +538,30 @@ end
function ARMYGROUP:onafterSpawned(From, Event, To)
self:T(self.lid..string.format("Group spawned!"))
-- Debug info.
if self.verbose>=1 then
local text=string.format("Initialized Army Group %s:\n", self.groupname)
text=text..string.format("Unit type = %s\n", self.actype)
text=text..string.format("Speed max = %.1f Knots\n", UTILS.KmphToKnots(self.speedMax))
text=text..string.format("Speed cruise = %.1f Knots\n", UTILS.KmphToKnots(self.speedCruise))
text=text..string.format("Weight = %.1f kg\n", self:GetWeightTotal())
text=text..string.format("Cargo bay = %.1f kg\n", self:GetFreeCargobay())
text=text..string.format("Elements = %d\n", #self.elements)
text=text..string.format("Waypoints = %d\n", #self.waypoints)
text=text..string.format("Radio = %.1f MHz %s %s\n", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu), tostring(self.radio.On))
text=text..string.format("Ammo = %d (G=%d/R=%d/M=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Rockets, self.ammo.Missiles)
text=text..string.format("FSM state = %s\n", self:GetState())
text=text..string.format("Is alive = %s\n", tostring(self:IsAlive()))
text=text..string.format("LateActivate = %s\n", tostring(self:IsLateActivated()))
self:I(self.lid..text)
end
-- Update position.
self:_UpdatePosition()
-- Not dead or destroyed yet.
self.isDead=false
self.isDestroyed=false
if self.isAI then
@ -461,16 +585,34 @@ function ARMYGROUP:onafterSpawned(From, Event, To)
if not self.option.Formation then
self.option.Formation=self.optionDefault.Formation
end
-- Update route.
if #self.waypoints>1 then
self:Cruise(nil, self.option.Formation or self.optionDefault.Formation)
else
self:FullStop()
end
-- Update status.
self:__Status(-0.1)
end
-- Update route.
if #self.waypoints>1 then
self:Cruise(nil, self.option.Formation or self.optionDefault.Formation)
else
self:FullStop()
end
--- On before "UpdateRoute" event.
-- @param #ARMYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #number n Waypoint number. Default is next waypoint.
-- @param #number Speed Speed in knots. Default cruise speed.
-- @param #number Formation Formation of the group.
function ARMYGROUP:onbeforeUpdateRoute(From, Event, To, n, Speed, Formation)
if self:IsWaiting() then
return false
end
return true
end
--- On after "UpdateRoute" event.
@ -830,7 +972,7 @@ function ARMYGROUP:onafterEngageTarget(From, Event, To, Target)
end
--- On after "EngageTarget" event.
--- Update engage target.
-- @param #ARMYGROUP self
function ARMYGROUP:_UpdateEngageTarget()
@ -926,131 +1068,14 @@ end
-- @param #number Formation Formation.
function ARMYGROUP:onafterCruise(From, Event, To, Speed, Formation)
-- Not waiting anymore.
self.Twaiting=nil
self.dTwait=nil
self:__UpdateRoute(-1, nil, Speed, Formation)
end
--- On after "Stop" event.
-- @param #ARMYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function ARMYGROUP:onafterStop(From, Event, To)
-- Handle events:
self:UnHandleEvent(EVENTS.Birth)
self:UnHandleEvent(EVENTS.Dead)
self:UnHandleEvent(EVENTS.RemoveUnit)
-- Call OPSGROUP function.
self:GetParent(self).onafterStop(self, From, Event, To)
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Events DCS
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Event function handling the birth of a unit.
-- @param #ARMYGROUP self
-- @param Core.Event#EVENTDATA EventData Event data.
function ARMYGROUP:OnEventBirth(EventData)
-- Check that this is the right group.
if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then
local unit=EventData.IniUnit
local group=EventData.IniGroup
local unitname=EventData.IniUnitName
if self.respawning then
local function reset()
self.respawning=nil
end
-- Reset switch in 1 sec. This should allow all birth events of n>1 groups to have passed.
-- TODO: Can I do this more rigorously?
self:ScheduleOnce(1, reset)
else
-- Get element.
local element=self:GetElementByName(unitname)
-- Set element to spawned state.
self:T3(self.lid..string.format("EVENT: Element %s born ==> spawned", element.name))
self:ElementSpawned(element)
end
end
end
--- Event function handling the crash of a unit.
-- @param #ARMYGROUP self
-- @param Core.Event#EVENTDATA EventData Event data.
function ARMYGROUP:OnEventDead(EventData)
-- Check that this is the right group.
if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then
self:T(self.lid..string.format("EVENT: Unit %s dead!", EventData.IniUnitName))
local unit=EventData.IniUnit
local group=EventData.IniGroup
local unitname=EventData.IniUnitName
-- Get element.
local element=self:GetElementByName(unitname)
if element then
self:T(self.lid..string.format("EVENT: Element %s dead ==> destroyed", element.name))
self:ElementDestroyed(element)
end
end
end
--- Event function handling when a unit is removed from the game.
-- @param #ARMYGROUP self
-- @param Core.Event#EVENTDATA EventData Event data.
function ARMYGROUP:OnEventRemoveUnit(EventData)
-- Check that this is the right group.
if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then
local unit=EventData.IniUnit
local group=EventData.IniGroup
local unitname=EventData.IniUnitName
-- Get element.
local element=self:GetElementByName(unitname)
if element then
self:T(self.lid..string.format("EVENT: Element %s removed ==> dead", element.name))
self:ElementDead(element)
end
end
end
--- Event function handling when a unit is hit.
-- @param #ARMYGROUP self
-- @param Core.Event#EVENTDATA EventData Event data.
function ARMYGROUP:OnEventHit(EventData)
-- Check that this is the right group.
if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then
local unit=EventData.IniUnit
local group=EventData.IniGroup
local unitname=EventData.IniUnitName
-- TODO: suppression
end
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Routing
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
@ -1108,17 +1133,18 @@ end
--- Initialize group parameters. Also initializes waypoints if self.waypoints is nil.
-- @param #ARMYGROUP self
-- @param #table Template Template used to init the group. Default is `self.template`.
-- @return #ARMYGROUP self
function ARMYGROUP:_InitGroup()
function ARMYGROUP:_InitGroup(Template)
-- First check if group was already initialized.
if self.groupinitialized then
self:E(self.lid.."WARNING: Group was already initialized!")
self:T(self.lid.."WARNING: Group was already initialized! Will NOT do it again!")
return
end
-- Get template of group.
self.template=self.group:GetTemplate()
local template=Template or self:_GetTemplate()
-- Define category.
self.isAircraft=false
@ -1129,7 +1155,7 @@ function ARMYGROUP:_InitGroup()
self.isAI=true
-- Is (template) group late activated.
self.isLateActivated=self.template.lateActivation
self.isLateActivated=template.lateActivation
-- Ground groups cannot be uncontrolled.
self.isUncontrolled=false
@ -1161,64 +1187,16 @@ function ARMYGROUP:_InitGroup()
-- Units of the group.
local units=self.group:GetUnits()
for _,_unit in pairs(units) do
local unit=_unit --Wrapper.Unit#UNIT
-- TODO: this is wrong when grouping is used!
local unittemplate=unit:GetTemplate()
local element={} --#ARMYGROUP.Element
element.name=unit:GetName()
element.unit=unit
element.status=OPSGROUP.ElementStatus.INUTERO
element.typename=unit:GetTypeName()
element.skill=unittemplate.skill or "Unknown"
element.ai=true
element.category=element.unit:GetUnitCategory()
element.categoryname=element.unit:GetCategoryName()
element.size, element.length, element.height, element.width=unit:GetObjectSize()
element.ammo0=self:GetAmmoUnit(unit, false)
element.life0=unit:GetLife0()
element.life=element.life0
-- Debug text.
if self.verbose>=2 then
local text=string.format("Adding element %s: status=%s, skill=%s, life=%.3f category=%s (%d), size: %.1f (L=%.1f H=%.1f W=%.1f)",
element.name, element.status, element.skill, element.life, element.categoryname, element.category, element.size, element.length, element.height, element.width)
self:I(self.lid..text)
end
-- Add elemets.
for _,unit in pairs(units) do
self:_AddElementByName(unit:GetName())
end
-- Get Descriptors.
self.descriptors=units[1]:GetDesc()
-- Add element to table.
table.insert(self.elements, element)
-- Get Descriptors.
self.descriptors=self.descriptors or unit:GetDesc()
-- Set type name.
self.actype=self.actype or unit:GetTypeName()
if unit:IsAlive() then
-- Trigger spawned event.
self:ElementSpawned(element)
end
end
-- Debug info.
if self.verbose>=1 then
local text=string.format("Initialized Army Group %s:\n", self.groupname)
text=text..string.format("Unit type = %s\n", self.actype)
text=text..string.format("Speed max = %.1f Knots\n", UTILS.KmphToKnots(self.speedMax))
text=text..string.format("Speed cruise = %.1f Knots\n", UTILS.KmphToKnots(self.speedCruise))
text=text..string.format("Elements = %d\n", #self.elements)
text=text..string.format("Waypoints = %d\n", #self.waypoints)
text=text..string.format("Radio = %.1f MHz %s %s\n", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu), tostring(self.radio.On))
text=text..string.format("Ammo = %d (G=%d/R=%d/M=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Rockets, self.ammo.Missiles)
text=text..string.format("FSM state = %s\n", self:GetState())
text=text..string.format("Is alive = %s\n", tostring(self:IsAlive()))
text=text..string.format("LateActivate = %s\n", tostring(self:IsLateActivated()))
self:I(self.lid..text)
end
-- Set type name.
self.actype=units[1]:GetTypeName()
-- Init done.
self.groupinitialized=true

View File

@ -7,7 +7,7 @@
-- * Set mission start/stop times
-- * Set mission priority and urgency (can cancel running missions)
-- * Specific mission options for ROE, ROT, formation, etc.
-- * Compatible with FLIGHTGROUP, NAVYGROUP, ARMYGROUP, AIRWING, WINGCOMMANDER and CHIEF classes
-- * Compatible with OPS classes like FLIGHTGROUP, NAVYGROUP, ARMYGROUP, AIRWING, etc.
-- * FSM events when a mission is done, successful or failed
--
-- ===
@ -39,9 +39,10 @@
-- @field #number prio Mission priority.
-- @field #boolean urgent Mission is urgent. Running missions with lower prio might be cancelled.
-- @field #number importance Importance.
-- @field #number Tstart Mission start time in seconds.
-- @field #number Tstop Mission stop time in seconds.
-- @field #number Tstart Mission start time in abs. seconds.
-- @field #number Tstop Mission stop time in abs. seconds.
-- @field #number duration Mission duration in seconds.
-- @field #number Tpush Mission push/execute time in abs. seconds.
-- @field Wrapper.Marker#MARKER marker F10 map marker.
-- @field #boolean markerOn If true, display marker on F10 map with the AUFTRAG status.
-- @field #number markerCoaliton Coalition to which the marker is dispayed.
@ -52,8 +53,9 @@
-- @field #number dTevaluate Time interval in seconds before the mission result is evaluated after mission is over.
-- @field #number Tover Mission abs. time stamp, when mission was over.
-- @field #table conditionStart Condition(s) that have to be true, before the mission will be started.
-- @field #table conditionSuccess If all stop conditions are true, the mission is cancelled.
-- @field #table conditionFailure If all stop conditions are true, the mission is cancelled.
-- @field #table conditionSuccess If all conditions are true, the mission is cancelled.
-- @field #table conditionFailure If all conditions are true, the mission is cancelled.
-- @field #table conditionPush If all conditions are true, the mission is executed. Before, the group(s) wait at the mission execution waypoint.
--
-- @field #number orbitSpeed Orbit speed in m/s.
-- @field #number orbitAltitude Orbit altitude in meters.
@ -101,9 +103,11 @@
--
-- @field #string missionTask Mission task. See `ENUMS.MissionTask`.
-- @field #number missionAltitude Mission altitude in meters.
-- @field #number missionSpeed Mission speed in km/h.
-- @field #number missionFraction Mission coordiante fraction. Default is 0.5.
-- @field #number missionRange Mission range in meters. Used in AIRWING class.
-- @field Core.Point#COORDINATE missionWaypointCoord Mission waypoint coordinate.
-- @field Core.Point#COORDINATE missionEgressCoord Mission egress waypoint coordinate.
--
-- @field #table enrouteTasks Mission enroute tasks.
--
@ -285,6 +289,7 @@ AUFTRAG = {
conditionStart = {},
conditionSuccess = {},
conditionFailure = {},
conditionPush = {},
}
--- Global mission counter.
@ -315,6 +320,7 @@ _AUFTRAGSNR=0
-- @field #string TANKER Tanker mission.
-- @field #string TROOPTRANSPORT Troop transport mission.
-- @field #string ARTY Fire at point.
-- @field #string PATROLZONE Patrol a zone.
AUFTRAG.Type={
ANTISHIP="Anti Ship",
AWACS="AWACS",
@ -338,6 +344,7 @@ AUFTRAG.Type={
TANKER="Tanker",
TROOPTRANSPORT="Troop Transport",
ARTY="Fire At Point",
PATROLZONE="Patrol Zone",
}
--- Mission status.
@ -430,8 +437,9 @@ AUFTRAG.TargetType={
--- Group specific data. Each ops group subscribed to this mission has different data for this.
-- @type AUFTRAG.GroupData
-- @field Ops.OpsGroup#OPSGROUP opsgroup The OPS group.
-- @field Core.Point#COORDINATE waypointcoordinate Waypoint coordinate.
-- @field Core.Point#COORDINATE waypointcoordinate Ingress waypoint coordinate.
-- @field #number waypointindex Waypoint index.
-- @field Core.Point#COORDINATE wpegresscoordinate Egress waypoint coordinate.
-- @field Ops.OpsGroup#OPSGROUP.Task waypointtask Waypoint task.
-- @field #string status Group mission status.
-- @field Ops.AirWing#AIRWING.SquadronAsset asset The squadron asset.
@ -439,7 +447,7 @@ AUFTRAG.TargetType={
--- AUFTRAG class version.
-- @field #string version
AUFTRAG.version="0.5.0"
AUFTRAG.version="0.7.1"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
@ -934,7 +942,7 @@ end
--- Create a STRIKE mission. Flight will attack the closest map object to the specified coordinate.
-- @param #AUFTRAG self
-- @param Core.Point#COORDINATE Target The target coordinate. Can also be given as a GROUP, UNIT or STATIC object.
-- @param Core.Point#COORDINATE Target The target coordinate. Can also be given as a GROUP, UNIT, STATIC or TARGET object.
-- @param #number Altitude Engage altitude in feet. Default 2000 ft.
-- @return #AUFTRAG self
function AUFTRAG:NewSTRIKE(Target, Altitude)
@ -962,7 +970,7 @@ end
--- Create a BOMBING mission. Flight will drop bombs a specified coordinate.
-- @param #AUFTRAG self
-- @param Core.Point#COORDINATE Target Target coordinate. Can also be specified as a GROUP, UNIT or STATIC object.
-- @param Core.Point#COORDINATE Target Target coordinate. Can also be specified as a GROUP, UNIT, STATIC or TARGET object.
-- @param #number Altitude Engage altitude in feet. Default 25000 ft.
-- @return #AUFTRAG self
function AUFTRAG:NewBOMBING(Target, Altitude)
@ -1002,10 +1010,6 @@ function AUFTRAG:NewBOMBRUNWAY(Airdrome, Altitude)
if type(Airdrome)=="string" then
Airdrome=AIRBASE:FindByName(Airdrome)
end
if Airdrome:IsInstanceOf("AIRBASE") then
end
local mission=AUFTRAG:New(AUFTRAG.Type.BOMBRUNWAY)
@ -1106,9 +1110,7 @@ end
function AUFTRAG:NewRESCUEHELO(Carrier)
local mission=AUFTRAG:New(AUFTRAG.Type.RESCUEHELO)
--mission.carrier=Carrier
mission:_TargetFromObject(Carrier)
-- Mission options:
@ -1191,6 +1193,32 @@ function AUFTRAG:NewARTY(Target, Nshots, Radius)
return mission
end
--- Create a PATROLZONE mission. Group(s) will go to the zone and patrol it randomly.
-- @param #AUFTRAG self
-- @param Core.Zone#ZONE Zone The patrol zone.
-- @param #number Speed Speed in knots.
-- @param #number Altitude Altitude in feet. Only for airborne units. Default 2000 feet ASL.
-- @return #AUFTRAG self
function AUFTRAG:NewPATROLZONE(Zone, Speed, Altitude)
local mission=AUFTRAG:New(AUFTRAG.Type.PATROLZONE)
mission:_TargetFromObject(Zone)
mission.optionROE=ENUMS.ROE.OpenFire
mission.optionROT=ENUMS.ROT.PassiveDefense
mission.optionAlarm=ENUMS.AlarmState.Auto
mission.missionFraction=1.0
mission.missionSpeed=Speed and UTILS.KnotsToKmph(Speed) or nil
mission.missionAltitude=Altitude and UTILS.FeetToMeters(Altitude) or nil
mission.DCStask=mission:GetDCSMissionTask()
return mission
end
--- Create a mission to attack a group. Mission type is automatically chosen from the group category.
-- @param #AUFTRAG self
-- @param Ops.Target#TARGET Target The target.
@ -1407,6 +1435,24 @@ function AUFTRAG:SetTime(ClockStart, ClockStop)
return self
end
--- Set mission push time. This is the time the mission is executed. If the push time is not passed, the group will wait at the mission execution waypoint.
-- @param #AUFTRAG self
-- @param #string ClockPush Time the mission is executed, e.g. "05:00" for 5 am. Can also be given as a `#number`, where it is interpreted as relative push time in seconds.
-- @return #AUFTRAG self
function AUFTRAG:SetPushTime(ClockPush)
if ClockPush then
if type(ClockPush)=="string" then
self.Tpush=UTILS.ClockToSeconds(ClockPush)
elseif type(ClockPush)=="number" then
self.Tpush=timer.getAbsTime()+ClockPush
end
end
return self
end
--- Set mission priority and (optional) urgency. Urgent missions can cancel other running missions.
-- @param #AUFTRAG self
-- @param #number Prio Priority 1=high, 100=low. Default 50.
@ -1781,6 +1827,26 @@ function AUFTRAG:AddConditionFailure(ConditionFunction, ...)
return self
end
--- Add push condition.
-- @param #AUFTRAG self
-- @param #function ConditionFunction If this function returns `true`, the mission is executed.
-- @param ... Condition function arguments if any.
-- @return #AUFTRAG self
function AUFTRAG:AddConditionPush(ConditionFunction, ...)
local condition={} --#AUFTRAG.Condition
condition.func=ConditionFunction
condition.arg={}
if arg then
condition.arg=arg
end
table.insert(self.conditionPush, condition)
return self
end
--- Assign airwing squadron(s) to the mission. Only these squads will be considered for the job.
-- @param #AUFTRAG self
@ -1794,6 +1860,8 @@ function AUFTRAG:AssignSquadrons(Squadrons)
end
self.squadrons=Squadrons
return self
end
--- Add a required payload for this mission. Only these payloads will be used for this mission. If they are not available, the mission cannot start. Only available for use with an AIRWING.
@ -1806,12 +1874,14 @@ function AUFTRAG:AddRequiredPayload(Payload)
table.insert(self.payloads, Payload)
return self
end
--- Add a Ops group to the mission.
-- @param #AUFTRAG self
-- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPSGROUP object.
-- @return #AUFTRAG self
function AUFTRAG:AddOpsGroup(OpsGroup)
self:T(self.lid..string.format("Adding Ops group %s", OpsGroup.groupname))
@ -1824,11 +1894,13 @@ function AUFTRAG:AddOpsGroup(OpsGroup)
self.groupdata[OpsGroup.groupname]=groupdata
return self
end
--- Remove an Ops group from the mission.
-- @param #AUFTRAG self
-- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPSGROUP object.
-- @return #AUFTRAG self
function AUFTRAG:DelOpsGroup(OpsGroup)
self:T(self.lid..string.format("Removing OPS group %s", OpsGroup and OpsGroup.groupname or "nil (ERROR)!"))
@ -1841,6 +1913,7 @@ function AUFTRAG:DelOpsGroup(OpsGroup)
end
return self
end
--- Check if mission is PLANNED.
@ -1932,12 +2005,12 @@ function AUFTRAG:IsReadyToGo()
local Tnow=timer.getAbsTime()
-- Start time did not pass yet.
if self.Tstart and Tnow<self.Tstart or false then
if self.Tstart and Tnow<self.Tstart then
return false
end
-- Stop time already passed.
if self.Tstop and Tnow>self.Tstop or false then
if self.Tstop and Tnow>self.Tstop then
return false
end
@ -1963,7 +2036,7 @@ function AUFTRAG:IsReadyToCancel()
local Tnow=timer.getAbsTime()
-- Stop time already passed.
if self.Tstop and Tnow>self.Tstop then
if self.Tstop and Tnow>=self.Tstop then
return true
end
@ -1987,6 +2060,26 @@ function AUFTRAG:IsReadyToCancel()
return false
end
--- Check if mission is ready to be pushed.
-- * Mission push time already passed.
-- * All push conditions are true.
-- @param #AUFTRAG self
-- @return #boolean If true, mission groups can push.
function AUFTRAG:IsReadyToPush()
local Tnow=timer.getAbsTime()
-- Push time passed?
if self.Tpush and Tnow<self.Tpush then
return false
end
-- Evaluate push condition(s) if any. All need to be true.
local push=self:EvalConditionsAll(self.conditionPush)
return push
end
--- Check if all given condition are true.
-- @param #AUFTRAG self
-- @param #table Conditions Table of conditions.
@ -2926,7 +3019,7 @@ end
-- @param #AUFTRAG self
-- @return Wrapper.Positionable#POSITIONABLE The target object. Could be many things.
function AUFTRAG:GetObjective()
return self:GetTargetData().Target
return self:GetTargetData():GetObject()
end
--- Get type of target.
@ -3083,19 +3176,53 @@ function AUFTRAG:GetMissionTypesText(MissionTypes)
return text
end
--- Set the mission waypoint coordinate where the mission is executed.
--- Set the mission waypoint coordinate where the mission is executed. Note that altitude is set via `:SetMissionAltitude`.
-- @param #AUFTRAG self
-- @return Core.Point#COORDINATE Coordinate where the mission is executed.
-- @param Core.Point#COORDINATE Coordinate Coordinate where the mission is executed.
-- @return #AUFTRAG self
function AUFTRAG:SetMissionWaypointCoord(Coordinate)
-- Obviously a zone was passed. We get the coordinate.
if Coordinate:IsInstanceOf("ZONE_BASE") then
Coordinate=Coordinate:GetCoordinate()
end
self.missionWaypointCoord=Coordinate
return self
end
--- Set the mission egress coordinate. This is the coordinate where the assigned group will go once the mission is finished.
-- @param #AUFTRAG self
-- @param Core.Point#COORDINATE Coordinate Egrees coordinate.
-- @param #number Altitude (Optional) Altitude in feet. Default is y component of coordinate.
-- @return #AUFTRAG self
function AUFTRAG:SetMissionEgressCoord(Coordinate, Altitude)
-- Obviously a zone was passed. We get the coordinate.
if Coordinate:IsInstanceOf("ZONE_BASE") then
Coordinate=Coordinate:GetCoordinate()
end
self.missionEgressCoord=Coordinate
if Altitude then
self.missionEgressCoord.y=UTILS.FeetToMeters(Altitude)
end
end
--- Get the mission egress coordinate if this was defined.
-- @param #AUFTRAG self
-- @return Core.Point#COORDINATE Coordinate Coordinate or nil.
function AUFTRAG:GetMissionEgressCoord()
return self.missionEgressCoord
end
--- Get coordinate of target. First unit/group of the set is used.
-- @param #AUFTRAG self
-- @param Wrapper.Group#GROUP group Group.
-- @param #number randomradius Random radius in meters.
-- @return Core.Point#COORDINATE Coordinate where the mission is executed.
function AUFTRAG:GetMissionWaypointCoord(group)
function AUFTRAG:GetMissionWaypointCoord(group, randomradius)
-- Check if a coord has been explicitly set.
if self.missionWaypointCoord then
@ -3111,7 +3238,9 @@ function AUFTRAG:GetMissionWaypointCoord(group)
local alt=waypointcoord.y
-- Add some randomization.
waypointcoord=ZONE_RADIUS:New("Temp", waypointcoord:GetVec2(), 1000):GetRandomCoordinate():SetAltitude(alt, false)
if randomradius then
waypointcoord=ZONE_RADIUS:New("Temp", waypointcoord:GetVec2(), randomradius):GetRandomCoordinate():SetAltitude(alt, false)
end
-- Set altitude of mission waypoint.
if self.missionAltitude then
@ -3388,6 +3517,26 @@ function AUFTRAG:GetDCSMissionTask(TaskControllable)
local DCStask=CONTROLLABLE.TaskFireAtPoint(nil, self:GetTargetVec2(), self.artyRadius, self.artyShots, self.engageWeaponType)
table.insert(DCStasks, DCStask)
elseif self.type==AUFTRAG.Type.PATROLZONE then
-------------------------
-- PATROL ZONE Mission --
-------------------------
local DCStask={}
DCStask.id="PatrolZone"
-- We create a "fake" DCS task and pass the parameters to the FLIGHTGROUP.
local param={}
param.zone=self:GetObjective()
param.altitude=self.missionAltitude
param.speed=self.missionSpeed
DCStask.params=param
table.insert(DCStasks, DCStask)
else
self:E(self.lid..string.format("ERROR: Unknown mission task!"))

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -65,12 +65,7 @@ NAVYGROUP = {
pathCorridor = 400,
}
--- Navy group element.
-- @type NAVYGROUP.Element
-- @field #string name Name of the element, i.e. the unit.
-- @field #string typename Type name.
--- Navy group element.
--- Turn into wind parameters.
-- @type NAVYGROUP.IntoWind
-- @field #number Tstart Time to start.
-- @field #number Tstop Time to stop.
@ -87,7 +82,7 @@ NAVYGROUP = {
--- NavyGroup version.
-- @field #string version
NAVYGROUP.version="0.5.0"
NAVYGROUP.version="0.7.0"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
@ -107,12 +102,19 @@ NAVYGROUP.version="0.5.0"
--- Create a new NAVYGROUP class object.
-- @param #NAVYGROUP self
-- @param #string GroupName Name of the group.
-- @param Wrapper.Group#GROUP group The group object. Can also be given by its group name as `#string`.
-- @return #NAVYGROUP self
function NAVYGROUP:New(GroupName)
function NAVYGROUP:New(group)
-- First check if we already have an OPS group for this group.
local og=_DATABASE:GetOpsGroup(group)
if og then
og:I(og.lid..string.format("WARNING: OPS group already exists in data base!"))
return og
end
-- Inherit everything from FSM class.
local self=BASE:Inherit(self, OPSGROUP:New(GroupName)) -- #NAVYGROUP
local self=BASE:Inherit(self, OPSGROUP:New(group)) -- #NAVYGROUP
-- Set some string id for output to DCS.log file.
self.lid=string.format("NAVYGROUP %s | ", self.groupname)
@ -123,6 +125,7 @@ function NAVYGROUP:New(GroupName)
self:SetDefaultAlarmstate()
self:SetPatrolAdInfinitum(true)
self:SetPathfinding(false)
self.isNavygroup=true
-- Add FSM transitions.
-- From State --> Event --> To State
@ -162,7 +165,7 @@ function NAVYGROUP:New(GroupName)
-- Init waypoints.
self:InitWaypoints()
self:_InitWaypoints()
-- Initialize the group.
self:_InitGroup()
@ -180,6 +183,9 @@ function NAVYGROUP:New(GroupName)
-- Start check zone timer.
self.timerCheckZone=TIMER:New(self._CheckInZones, self):Start(2, 60)
-- Add OPSGROUP to _DATABASE.
_DATABASE:AddOpsGroup(self)
return self
end
@ -487,7 +493,8 @@ function NAVYGROUP:onafterStatus(From, Event, To)
-- Check if group started or stopped turning.
self:_CheckTurning()
local freepath=UTILS.NMToMeters(10)
local disttoWP=math.min(self:GetDistanceToWaypoint(), UTILS.NMToMeters(10))
local freepath=disttoWP
-- Only check if not currently turning.
if not self:IsTurning() then
@ -495,7 +502,7 @@ function NAVYGROUP:onafterStatus(From, Event, To)
-- Check free path ahead.
freepath=self:_CheckFreePath(freepath, 100)
if freepath<5000 then
if disttoWP>1 and freepath<disttoWP then
if not self.collisionwarning then
-- Issue a collision warning event.
@ -515,6 +522,17 @@ function NAVYGROUP:onafterStatus(From, Event, To)
-- Check if group got stuck.
self:_CheckStuck()
-- Check if group is waiting.
if self:IsWaiting() then
if self.Twaiting and self.dTwait then
if timer.getAbsTime()>self.Twaiting+self.dTwait then
self.Twaiting=nil
self.dTwait=nil
self:Cruise()
end
end
end
if self.verbose>=1 then
@ -571,7 +589,7 @@ function NAVYGROUP:onafterStatus(From, Event, To)
-- Recovery Windows
---
if self.verbose>=2 then
if self.verbose>=2 and #self.Qintowind>0 then
-- Debug output:
local text=string.format(self.lid.."Turn into wind time windows:")
@ -598,6 +616,11 @@ function NAVYGROUP:onafterStatus(From, Event, To)
end
---
-- Cargo
---
self:_CheckCargoTransport()
---
-- Tasks & Missions
@ -610,6 +633,12 @@ function NAVYGROUP:onafterStatus(From, Event, To)
self:__Status(-30)
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- DCS Events ==> See OPSGROUP
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- See OPSGROUP!
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM Events
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
@ -619,7 +648,7 @@ end
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #NAVYGROUP.Element Element The group element.
-- @param Ops.OpsGroup#OPSGROUP.Element Element The group element.
function NAVYGROUP:onafterElementSpawned(From, Event, To, Element)
self:T(self.lid..string.format("Element spawned %s", Element.name))
@ -636,8 +665,30 @@ end
function NAVYGROUP:onafterSpawned(From, Event, To)
self:T(self.lid..string.format("Group spawned!"))
-- Debug info.
if self.verbose>=1 then
local text=string.format("Initialized Navy Group %s:\n", self.groupname)
text=text..string.format("Unit type = %s\n", self.actype)
text=text..string.format("Speed max = %.1f Knots\n", UTILS.KmphToKnots(self.speedMax))
text=text..string.format("Speed cruise = %.1f Knots\n", UTILS.KmphToKnots(self.speedCruise))
text=text..string.format("Weight = %.1f kg\n", self:GetWeightTotal())
text=text..string.format("Cargo bay = %.1f kg\n", self:GetFreeCargobay())
text=text..string.format("Elements = %d\n", #self.elements)
text=text..string.format("Waypoints = %d\n", #self.waypoints)
text=text..string.format("Radio = %.1f MHz %s %s\n", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu), tostring(self.radio.On))
text=text..string.format("Ammo = %d (G=%d/R=%d/M=%d/T=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Rockets, self.ammo.Missiles, self.ammo.Torpedos)
text=text..string.format("FSM state = %s\n", self:GetState())
text=text..string.format("Is alive = %s\n", tostring(self:IsAlive()))
text=text..string.format("LateActivate = %s\n", tostring(self:IsLateActivated()))
self:I(self.lid..text)
end
-- Update position.
self:_UpdatePosition()
-- Not dead or destroyed yet.
self.isDead=false
self.isDestroyed=false
if self.isAI then
@ -655,20 +706,39 @@ function NAVYGROUP:onafterSpawned(From, Event, To)
-- Set radio.
if self.radioDefault then
self:SwitchRadio()
-- CAREFUL: This makes DCS crash for some ships like speed boats or Higgins boats! (On a respawn for example). Looks like the command SetFrequency is causing this.
--self:SwitchRadio()
else
self:SetDefaultRadio(self.radio.Freq, self.radio.Modu, false)
end
-- Update route.
if #self.waypoints>1 then
self:Cruise()
else
self:FullStop()
end
-- Update status.
self:__Status(-0.1)
end
-- Update route.
if #self.waypoints>1 then
self:Cruise()
else
self:FullStop()
end
--- On before "UpdateRoute" event.
-- @param #NAVYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #number n Waypoint number. Default is next waypoint.
-- @param #number Speed Speed in knots to the next waypoint.
-- @param #number Depth Depth in meters to the next waypoint.
function NAVYGROUP:onbeforeUpdateRoute(From, Event, To, n, Speed, Depth)
if self:IsWaiting() then
return false
end
return true
end
--- On after "UpdateRoute" event.
@ -710,6 +780,7 @@ function NAVYGROUP:onafterUpdateRoute(From, Event, To, n, Speed, Depth)
wp.alt=-self.depth
else
-- Take default waypoint alt.
wp.alt=wp.alt or 0
end
-- Current set speed in m/s.
@ -939,6 +1010,10 @@ end
-- @param #number Speed Speed in knots until next waypoint is reached. Default is speed set for waypoint.
function NAVYGROUP:onafterCruise(From, Event, To, Speed)
-- Not waiting anymore.
self.Twaiting=nil
self.dTwait=nil
-- No set depth.
self.depth=nil
@ -957,7 +1032,7 @@ function NAVYGROUP:onafterDive(From, Event, To, Depth, Speed)
Depth=Depth or 50
self:T(self.lid..string.format("Diving to %d meters", Depth))
self:I(self.lid..string.format("Diving to %d meters", Depth))
self.depth=Depth
@ -1014,110 +1089,6 @@ function NAVYGROUP:onafterCollisionWarning(From, Event, To, Distance)
self.collisionwarning=true
end
--- On after Start event. Starts the NAVYGROUP FSM and event handlers.
-- @param #NAVYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function NAVYGROUP:onafterStop(From, Event, To)
-- Handle events:
self:UnHandleEvent(EVENTS.Birth)
self:UnHandleEvent(EVENTS.Dead)
self:UnHandleEvent(EVENTS.RemoveUnit)
-- Call OPSGROUP function.
self:GetParent(self).onafterStop(self, From, Event, To)
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Events DCS
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Event function handling the birth of a unit.
-- @param #NAVYGROUP self
-- @param Core.Event#EVENTDATA EventData Event data.
function NAVYGROUP:OnEventBirth(EventData)
-- Check that this is the right group.
if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then
local unit=EventData.IniUnit
local group=EventData.IniGroup
local unitname=EventData.IniUnitName
if self.respawning then
local function reset()
self.respawning=nil
end
-- Reset switch in 1 sec. This should allow all birth events of n>1 groups to have passed.
-- TODO: Can I do this more rigorously?
self:ScheduleOnce(1, reset)
else
-- Get element.
local element=self:GetElementByName(unitname)
-- Set element to spawned state.
self:T3(self.lid..string.format("EVENT: Element %s born ==> spawned", element.name))
self:ElementSpawned(element)
end
end
end
--- Flightgroup event function handling the crash of a unit.
-- @param #NAVYGROUP self
-- @param Core.Event#EVENTDATA EventData Event data.
function NAVYGROUP:OnEventDead(EventData)
-- Check that this is the right group.
if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then
self:T(self.lid..string.format("EVENT: Unit %s dead!", EventData.IniUnitName))
local unit=EventData.IniUnit
local group=EventData.IniGroup
local unitname=EventData.IniUnitName
-- Get element.
local element=self:GetElementByName(unitname)
if element then
self:T(self.lid..string.format("EVENT: Element %s dead ==> destroyed", element.name))
self:ElementDestroyed(element)
end
end
end
--- Flightgroup event function handling the crash of a unit.
-- @param #NAVYGROUP self
-- @param Core.Event#EVENTDATA EventData Event data.
function NAVYGROUP:OnEventRemoveUnit(EventData)
-- Check that this is the right group.
if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then
local unit=EventData.IniUnit
local group=EventData.IniGroup
local unitname=EventData.IniUnitName
-- Get element.
local element=self:GetElementByName(unitname)
if element then
self:T(self.lid..string.format("EVENT: Element %s removed ==> dead", element.name))
self:ElementDead(element)
end
end
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Routing
@ -1177,17 +1148,18 @@ end
--- Initialize group parameters. Also initializes waypoints if self.waypoints is nil.
-- @param #NAVYGROUP self
-- @param #table Template Template used to init the group. Default is `self.template`.
-- @return #NAVYGROUP self
function NAVYGROUP:_InitGroup()
function NAVYGROUP:_InitGroup(Template)
-- First check if group was already initialized.
if self.groupinitialized then
self:E(self.lid.."WARNING: Group was already initialized!")
self:T(self.lid.."WARNING: Group was already initialized! Will NOT do it again!")
return
end
-- Get template of group.
self.template=self.group:GetTemplate()
local template=Template or self:_GetTemplate()
-- Define category.
self.isAircraft=false
@ -1201,7 +1173,7 @@ function NAVYGROUP:_InitGroup()
self.isAI=true
-- Is (template) group late activated.
self.isLateActivated=self.template.lateActivation
self.isLateActivated=template.lateActivation
-- Naval groups cannot be uncontrolled.
self.isUncontrolled=false
@ -1217,8 +1189,8 @@ function NAVYGROUP:_InitGroup()
-- Radio parameters from template. Default is set on spawn if not modified by the user.
self.radio.On=true -- Radio is always on for ships.
self.radio.Freq=tonumber(self.template.units[1].frequency)/1000000
self.radio.Modu=tonumber(self.template.units[1].modulation)
self.radio.Freq=tonumber(template.units[1].frequency)/1000000
self.radio.Modu=tonumber(template.units[1].modulation)
-- Set default formation. No really applicable for ships.
self.optionDefault.Formation="Off Road"
@ -1235,64 +1207,17 @@ function NAVYGROUP:_InitGroup()
-- Get all units of the group.
local units=self.group:GetUnits()
for _,_unit in pairs(units) do
local unit=_unit --Wrapper.Unit#UNIT
-- Get unit template.
local unittemplate=unit:GetTemplate()
local element={} --#NAVYGROUP.Element
element.name=unit:GetName()
element.unit=unit
element.status=OPSGROUP.ElementStatus.INUTERO
element.typename=unit:GetTypeName()
element.skill=unittemplate.skill or "Unknown"
element.ai=true
element.category=element.unit:GetUnitCategory()
element.categoryname=element.unit:GetCategoryName()
element.size, element.length, element.height, element.width=unit:GetObjectSize()
element.ammo0=self:GetAmmoUnit(unit, false)
-- Debug text.
if self.verbose>=2 then
local text=string.format("Adding element %s: status=%s, skill=%s, category=%s (%d), size: %.1f (L=%.1f H=%.1f W=%.1f)",
element.name, element.status, element.skill, element.categoryname, element.category, element.size, element.length, element.height, element.width)
self:I(self.lid..text)
end
-- Add element to table.
table.insert(self.elements, element)
-- Get Descriptors.
self.descriptors=self.descriptors or unit:GetDesc()
-- Set type name.
self.actype=self.actype or unit:GetTypeName()
if unit:IsAlive() then
-- Trigger spawned event.
self:ElementSpawned(element)
end
end
-- Debug info.
if self.verbose>=1 then
local text=string.format("Initialized Navy Group %s:\n", self.groupname)
text=text..string.format("Unit type = %s\n", self.actype)
text=text..string.format("Speed max = %.1f Knots\n", UTILS.KmphToKnots(self.speedMax))
text=text..string.format("Speed cruise = %.1f Knots\n", UTILS.KmphToKnots(self.speedCruise))
text=text..string.format("Elements = %d\n", #self.elements)
text=text..string.format("Waypoints = %d\n", #self.waypoints)
text=text..string.format("Radio = %.1f MHz %s %s\n", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu), tostring(self.radio.On))
text=text..string.format("Ammo = %d (G=%d/R=%d/M=%d/T=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Rockets, self.ammo.Missiles, self.ammo.Torpedos)
text=text..string.format("FSM state = %s\n", self:GetState())
text=text..string.format("Is alive = %s\n", tostring(self:IsAlive()))
text=text..string.format("LateActivate = %s\n", tostring(self:IsLateActivated()))
self:I(self.lid..text)
-- Add elemets.
for _,unit in pairs(units) do
self:_AddElementByName(unit:GetName())
end
-- Get Descriptors.
self.descriptors=units[1]:GetDesc()
-- Set type name.
self.actype=units[1]:GetTypeName()
-- Init done.
self.groupinitialized=true
@ -1345,8 +1270,6 @@ function NAVYGROUP:_CheckFreePath(DistanceMax, dx)
--coordinate=coordinate:Translate(500, heading, true)
local function LoS(dist)
--local checkcoord=coordinate:Translate(dist, heading, true)
--return coordinate:IsLOS(checkcoord, offsetY)
local checkvec3=UTILS.VecTranslate(vec3, dist, heading)
local los=land.isVisible(vec3, checkvec3)
return los
@ -1374,7 +1297,7 @@ function NAVYGROUP:_CheckFreePath(DistanceMax, dx)
local los=LoS(x)
-- Debug message.
self:T2(self.lid..string.format("N=%d: xmin=%.1f xmax=%.1f x=%.1f d=%.3f los=%s", N, xmin, xmax, x, d, tostring(los)))
self:T(self.lid..string.format("N=%d: xmin=%.1f xmax=%.1f x=%.1f d=%.3f los=%s", N, xmin, xmax, x, d, tostring(los)))
if los and d<=eps then
return x
@ -1548,6 +1471,7 @@ end
-- @param #NAVYGROUP self
-- @return #boolean If true, a path was found.
function NAVYGROUP:_FindPathToNextWaypoint()
env.info("FF Path finding")
-- Pathfinding A*
local astar=ASTAR:New()
@ -1558,6 +1482,11 @@ function NAVYGROUP:_FindPathToNextWaypoint()
-- Next waypoint.
local wpnext=self:GetWaypointNext()
-- No next waypoint.
if wpnext==nil then
return
end
-- Next waypoint coordinate.
local nextwp=wpnext.coordinate
@ -1574,16 +1503,21 @@ function NAVYGROUP:_FindPathToNextWaypoint()
-- Set end coordinate.
astar:SetEndCoordinate(nextwp)
-- Distance to next waypoint.
local dist=position:Get2DDistance(nextwp)
-- Check distance >= 5 meters.
if dist<5 then
return
end
local boxwidth=dist*2
local spacex=dist*0.1
local delta=dist/10
-- Create a grid of nodes. We only want nodes of surface type water.
astar:CreateGrid({land.SurfaceType.WATER}, boxwidth, spacex, delta, delta*2, self.Debug)
astar:CreateGrid({land.SurfaceType.WATER}, boxwidth, spacex, delta, delta, self.verbose>10)
-- Valid neighbour nodes need to have line of sight.
astar:SetValidNeighbourLoS(self.pathCorridor)
@ -1610,7 +1544,9 @@ function NAVYGROUP:_FindPathToNextWaypoint()
uid=wp.uid
-- Debug: smoke and mark path.
--node.coordinate:MarkToAll(string.format("Path node #%d", i))
if self.verbose>=10 then
node.coordinate:MarkToAll(string.format("Path node #%d", i))
end
end

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -45,6 +45,8 @@
-- @field #table tacanChannel List of TACAN channels available to the squadron.
-- @field #number radioFreq Radio frequency in MHz the squad uses.
-- @field #number radioModu Radio modulation the squad uses.
-- @field #number takeoffType Take of type.
-- @field #table parkingIDs Parking IDs for this squadron.
-- @extends Core.Fsm#FSM
--- *It is unbelievable what a squadron of twelve aircraft did to tip the balance.* -- Adolf Galland
@ -87,12 +89,13 @@ SQUADRON = {
--- SQUADRON class version.
-- @field #string version
SQUADRON.version="0.5.0"
SQUADRON.version="0.7.0"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO: Parking spots for squadrons?
-- DONE: Engage radius.
-- DONE: Modex.
-- DONE: Call signs.
@ -134,7 +137,6 @@ function SQUADRON:New(TemplateGroupName, Ngroups, SquadronName)
self.Ngroups=Ngroups or 3
self:SetMissionRange()
self:SetSkill(AI.Skill.GOOD)
--self:SetVerbosity(0)
-- Everyone can ORBIT.
self:AddMissionCapability(AUFTRAG.Type.ORBIT)
@ -282,6 +284,53 @@ function SQUADRON:SetGrouping(nunits)
return self
end
--- Set valid parking spot IDs. Assets of this squad are only allowed to be spawned at these parking spots. **Note** that the IDs are different from the ones displayed in the mission editor!
-- @param #SQUADRON self
-- @param #table ParkingIDs Table of parking ID numbers or a single `#number`.
-- @return #SQUADRON self
function SQUADRON:SetParkingIDs(ParkingIDs)
if type(ParkingIDs)~="table" then
ParkingIDs={ParkingIDs}
end
self.parkingIDs=ParkingIDs
return self
end
--- Set takeoff type. All assets of this squadron will be spawned with cold (default) or hot engines.
-- Spawning on runways is not supported.
-- @param #SQUADRON self
-- @param #string TakeoffType Take off type: "Cold" (default) or "Hot" with engines on.
-- @return #SQUADRON self
function SQUADRON:SetTakeoffType(TakeoffType)
TakeoffType=TakeoffType or "Cold"
if TakeoffType:lower()=="hot" then
self.takeoffType=COORDINATE.WaypointType.TakeOffParkingHot
elseif TakeoffType:lower()=="cold" then
self.takeoffType=COORDINATE.WaypointType.TakeOffParking
else
self.takeoffType=COORDINATE.WaypointType.TakeOffParking
end
return self
end
--- Set takeoff type cold (default). All assets of this squadron will be spawned with engines off (cold).
-- @param #SQUADRON self
-- @return #SQUADRON self
function SQUADRON:SetTakeoffCold()
self:SetTakeoffType("Cold")
return self
end
--- Set takeoff type hot. All assets of this squadron will be spawned with engines on (hot).
-- @param #SQUADRON self
-- @return #SQUADRON self
function SQUADRON:SetTakeoffHot()
self:SetTakeoffType("Hot")
return self
end
--- Set mission types this squadron is able to perform.
-- @param #SQUADRON self
-- @param #table MissionTypes Table of mission types. Can also be passed as a #string if only one type.
@ -571,15 +620,22 @@ end
-- @return #number TACAN channel or *nil* if no channel is free.
function SQUADRON:FetchTacan()
-- Get the smallest free channel if there is one.
local freechannel=nil
for channel,free in pairs(self.tacanChannel) do
if free then
self:T(self.lid..string.format("Checking out Tacan channel %d", channel))
self.tacanChannel[channel]=false
return channel
if free then
if freechannel==nil or channel<freechannel then
freechannel=channel
end
end
end
if freechannel then
self:T(self.lid..string.format("Checking out Tacan channel %d", freechannel))
self.tacanChannel[freechannel]=false
end
return nil
return freechannel
end
--- "Return" a used TACAN channel.
@ -764,7 +820,8 @@ end
-- @param #string To To state.
function SQUADRON:onafterStop(From, Event, To)
self:I(self.lid.."STOPPING Squadron!")
-- Debug info.
self:I(self.lid.."STOPPING Squadron and removing all assets!")
-- Remove all assets.
for i=#self.assets,1,-1 do
@ -772,6 +829,7 @@ function SQUADRON:onafterStop(From, Event, To)
self:DelAsset(asset)
end
-- Clear call scheduler.
self.CallScheduler:Clear()
end
@ -830,8 +888,9 @@ end
--- Count assets in airwing (warehous) stock.
-- @param #SQUADRON self
-- @param #table MissionTypes (Optional) Count only assest that can perform certain mission type(s). Default is all types.
-- @return #number Assets not spawned.
function SQUADRON:CountAssetsInStock()
function SQUADRON:CountAssetsInStock(MissionTypes)
local N=0
for _,_asset in pairs(self.assets) do
@ -839,7 +898,9 @@ function SQUADRON:CountAssetsInStock()
if asset.spawned then
else
N=N+1
if MissionTypes==nil or self:CheckMissionCapability(MissionTypes, self.missiontypes) then
N=N+1
end
end
end
@ -1017,16 +1078,22 @@ end
--- Check if a mission type is contained in a list of possible capabilities.
-- @param #SQUADRON self
-- @param #string MissionType The requested mission type.
-- @param #table MissionTypes The requested mission type. Can also be passed as a single mission type `#string`.
-- @param #table Capabilities A table with possible capabilities.
-- @return #boolean If true, the requested mission type is part of the possible mission types.
function SQUADRON:CheckMissionCapability(MissionType, Capabilities)
function SQUADRON:CheckMissionCapability(MissionTypes, Capabilities)
if type(MissionTypes)~="table" then
MissionTypes={MissionTypes}
end
for _,cap in pairs(Capabilities) do
local capability=cap --Ops.Auftrag#AUFTRAG.Capability
if capability.MissionType==MissionType then
return true
end
for _,MissionType in pairs(MissionTypes) do
if capability.MissionType==MissionType then
return true
end
end
end
return false

View File

@ -72,6 +72,7 @@ TARGET = {
-- @field #string SCENERY Target is a SCENERY object.
-- @field #string COORDINATE Target is a COORDINATE.
-- @field #string AIRBASE Target is an AIRBASE.
-- @field #string ZONE Target is a ZONE object.
TARGET.ObjectType={
GROUP="Group",
UNIT="Unit",
@ -79,6 +80,7 @@ TARGET.ObjectType={
SCENERY="Scenery",
COORDINATE="Coordinate",
AIRBASE="Airbase",
ZONE="Zone",
}
@ -89,12 +91,14 @@ TARGET.ObjectType={
-- @field #string NAVAL
-- @field #string AIRBASE
-- @field #string COORDINATE
-- @field #string ZONE
TARGET.Category={
AIRCRAFT="Aircraft",
GROUND="Ground",
NAVAL="Naval",
AIRBASE="Airbase",
COORDINATE="Coordinate",
ZONE="Zone",
}
--- Object status.
@ -124,7 +128,7 @@ _TARGETID=0
--- TARGET class version.
-- @field #string version
TARGET.version="0.3.0"
TARGET.version="0.5.0"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
@ -227,12 +231,24 @@ end
-- User functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Create target data from a given object.
--- Create target data from a given object. Valid objects are:
--
-- * GROUP
-- * UNIT
-- * STATIC
-- * AIRBASE
-- * COORDINATE
-- * ZONE
-- * SET_GROUP
-- * SET_UNIT
-- * SET_STATIC
-- * SET_OPSGROUP
--
-- @param #TARGET self
-- @param Wrapper.Positionable#POSITIONABLE Object The target GROUP, UNIT, STATIC, AIRBASE or COORDINATE.
function TARGET:AddObject(Object)
if Object:IsInstanceOf("SET_GROUP") or Object:IsInstanceOf("SET_UNIT") then
if Object:IsInstanceOf("SET_GROUP") or Object:IsInstanceOf("SET_UNIT") or Object:IsInstanceOf("SET_STATIC") or Object:IsInstanceOf("SET_OPSGROUP") then
---
-- Sets
@ -243,14 +259,18 @@ function TARGET:AddObject(Object)
for _,object in pairs(set.Set) do
self:AddObject(object)
end
else
---
-- Groups, Units, Statics, Airbases, Coordinates
---
self:_AddObject(Object)
if Object:IsInstanceOf("OPSGROUP") then
self:_AddObject(Object:GetGroup()) -- We add the MOOSE GROUP object not the OPSGROUP object.
else
self:_AddObject(Object)
end
end
@ -658,12 +678,12 @@ function TARGET:_AddObject(Object)
elseif Object:IsInstanceOf("ZONE_BASE") then
local zone=Object --Core.Zone#ZONE_BASE
Object=zone:GetCoordinate()
Object=zone --:GetCoordinate()
target.Type=TARGET.ObjectType.COORDINATE
target.Type=TARGET.ObjectType.ZONE
target.Name=zone:GetName()
target.Coordinate=Object
target.Coordinate=zone:GetCoordinate()
target.Life0=1
target.Life=1
@ -773,7 +793,13 @@ function TARGET:GetTargetLife(Target)
elseif Target.Type==TARGET.ObjectType.COORDINATE then
return 1
elseif Target.Type==TARGET.ObjectType.ZONE then
return 1
else
self:E("ERROR: unknown target object type in GetTargetLife!")
end
end
@ -865,6 +891,13 @@ function TARGET:GetTargetVec3(Target)
local vec3={x=object.x, y=object.y, z=object.z}
return vec3
elseif Target.Type==TARGET.ObjectType.ZONE then
local object=Target.Object --Core.Zone#ZONE
local vec3=object:GetVec3()
return vec3
end
self:E(self.lid.."ERROR: Unknown TARGET type! Cannot get Vec3")
@ -1032,7 +1065,13 @@ function TARGET:GetTargetCategory(Target)
elseif Target.Type==TARGET.ObjectType.COORDINATE then
return TARGET.Category.COORDINATE
elseif Target.Type==TARGET.ObjectType.ZONE then
return TARGET.Category.ZONE
else
self:E("ERROR: unknown target category!")
end
return category
@ -1141,6 +1180,10 @@ function TARGET:CountObjectives(Target)
-- No target we can check!
elseif Target.Type==TARGET.ObjectType.ZONE then
-- No target we can check!
else
self:E(self.lid.."ERROR: Unknown target type! Cannot count targets")
end

View File

@ -1,13 +1,10 @@
--- **Core** - Is responsible for everything that is related to radio transmission and you can hear in DCS, be it TACAN beacons, Radio transmissions.
--- **Sound** - Radio transmissions.
--
-- ===
--
-- ## Features:
--
-- * Provide radio functionality to broadcast radio transmissions.
-- * Provide beacon functionality to assist pilots.
--
-- The Radio contains 2 classes : RADIO and BEACON
--
-- What are radio communications in DCS?
--
@ -35,13 +32,13 @@
--
-- ### Authors: Hugues "Grey_Echo" Bousquet, funkyfranky
--
-- @module Core.Radio
-- @module Sound.Radio
-- @image Core_Radio.JPG
--- Models the radio capability.
--- *It's not true I had nothing on, I had the radio on.* -- Marilyn Monroe
--
-- ## RADIO usage
-- # RADIO usage
--
-- There are 3 steps to a successful radio transmission.
--
@ -87,15 +84,15 @@
-- @field #string alias Name of the radio transmitter.
-- @extends Core.Base#BASE
RADIO = {
ClassName = "RADIO",
FileName = "",
Frequency = 0,
Modulation = radio.modulation.AM,
Subtitle = "",
ClassName = "RADIO",
FileName = "",
Frequency = 0,
Modulation = radio.modulation.AM,
Subtitle = "",
SubtitleDuration = 0,
Power = 100,
Loop = false,
alias=nil,
Power = 100,
Loop = false,
alias = nil,
}
--- Create a new RADIO Object. This doesn't broadcast a transmission, though, use @{#RADIO.Broadcast} to actually broadcast.
@ -395,438 +392,3 @@ function RADIO:StopBroadcast()
end
return self
end
--- After attaching a @{#BEACON} to your @{Wrapper.Positionable#POSITIONABLE}, you need to select the right function to activate the kind of beacon you want.
-- There are two types of BEACONs available : the AA TACAN Beacon and the general purpose Radio Beacon.
-- Note that in both case, you can set an optional parameter : the `BeaconDuration`. This can be very usefull to simulate the battery time if your BEACON is
-- attach to a cargo crate, for exemple.
--
-- ## AA TACAN Beacon usage
--
-- This beacon only works with airborne @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}. Use @{#BEACON:AATACAN}() to set the beacon parameters and start the beacon.
-- Use @#BEACON:StopAATACAN}() to stop it.
--
-- ## General Purpose Radio Beacon usage
--
-- This beacon will work with any @{Wrapper.Positionable#POSITIONABLE}, but **it won't follow the @{Wrapper.Positionable#POSITIONABLE}** ! This means that you should only use it with
-- @{Wrapper.Positionable#POSITIONABLE} that don't move, or move very slowly. Use @{#BEACON:RadioBeacon}() to set the beacon parameters and start the beacon.
-- Use @{#BEACON:StopRadioBeacon}() to stop it.
--
-- @type BEACON
-- @field #string ClassName Name of the class "BEACON".
-- @field Wrapper.Controllable#CONTROLLABLE Positionable The @{#CONTROLLABLE} that will receive radio capabilities.
-- @extends Core.Base#BASE
BEACON = {
ClassName = "BEACON",
Positionable = nil,
name=nil,
}
--- Beacon types supported by DCS.
-- @type BEACON.Type
-- @field #number NULL
-- @field #number VOR
-- @field #number DME
-- @field #number VOR_DME
-- @field #number TACAN TACtical Air Navigation system.
-- @field #number VORTAC
-- @field #number RSBN
-- @field #number BROADCAST_STATION
-- @field #number HOMER
-- @field #number AIRPORT_HOMER
-- @field #number AIRPORT_HOMER_WITH_MARKER
-- @field #number ILS_FAR_HOMER
-- @field #number ILS_NEAR_HOMER
-- @field #number ILS_LOCALIZER
-- @field #number ILS_GLIDESLOPE
-- @field #number PRMG_LOCALIZER
-- @field #number PRMG_GLIDESLOPE
-- @field #number ICLS Same as ICLS glideslope.
-- @field #number ICLS_LOCALIZER
-- @field #number ICLS_GLIDESLOPE
-- @field #number NAUTICAL_HOMER
BEACON.Type={
NULL = 0,
VOR = 1,
DME = 2,
VOR_DME = 3,
TACAN = 4,
VORTAC = 5,
RSBN = 128,
BROADCAST_STATION = 1024,
HOMER = 8,
AIRPORT_HOMER = 4104,
AIRPORT_HOMER_WITH_MARKER = 4136,
ILS_FAR_HOMER = 16408,
ILS_NEAR_HOMER = 16424,
ILS_LOCALIZER = 16640,
ILS_GLIDESLOPE = 16896,
PRMG_LOCALIZER = 33024,
PRMG_GLIDESLOPE = 33280,
ICLS = 131584, --leaving this in here but it is the same as ICLS_GLIDESLOPE
ICLS_LOCALIZER = 131328,
ICLS_GLIDESLOPE = 131584,
NAUTICAL_HOMER = 65536,
}
--- Beacon systems supported by DCS. https://wiki.hoggitworld.com/view/DCS_command_activateBeacon
-- @type BEACON.System
-- @field #number PAR_10 ?
-- @field #number RSBN_5 Russian VOR/DME system.
-- @field #number TACAN TACtical Air Navigation system on ground.
-- @field #number TACAN_TANKER_X TACtical Air Navigation system for tankers on X band.
-- @field #number TACAN_TANKER_Y TACtical Air Navigation system for tankers on Y band.
-- @field #number VOR Very High Frequency Omni-Directional Range
-- @field #number ILS_LOCALIZER ILS localizer
-- @field #number ILS_GLIDESLOPE ILS glideslope.
-- @field #number PRGM_LOCALIZER PRGM localizer.
-- @field #number PRGM_GLIDESLOPE PRGM glideslope.
-- @field #number BROADCAST_STATION Broadcast station.
-- @field #number VORTAC Radio-based navigational aid for aircraft pilots consisting of a co-located VHF omnidirectional range (VOR) beacon and a tactical air navigation system (TACAN) beacon.
-- @field #number TACAN_AA_MODE_X TACtical Air Navigation for aircraft on X band.
-- @field #number TACAN_AA_MODE_Y TACtical Air Navigation for aircraft on Y band.
-- @field #number VORDME Radio beacon that combines a VHF omnidirectional range (VOR) with a distance measuring equipment (DME).
-- @field #number ICLS_LOCALIZER Carrier landing system.
-- @field #number ICLS_GLIDESLOPE Carrier landing system.
BEACON.System={
PAR_10 = 1,
RSBN_5 = 2,
TACAN = 3,
TACAN_TANKER_X = 4,
TACAN_TANKER_Y = 5,
VOR = 6,
ILS_LOCALIZER = 7,
ILS_GLIDESLOPE = 8,
PRMG_LOCALIZER = 9,
PRMG_GLIDESLOPE = 10,
BROADCAST_STATION = 11,
VORTAC = 12,
TACAN_AA_MODE_X = 13,
TACAN_AA_MODE_Y = 14,
VORDME = 15,
ICLS_LOCALIZER = 16,
ICLS_GLIDESLOPE = 17,
}
--- Create a new BEACON Object. This doesn't activate the beacon, though, use @{#BEACON.ActivateTACAN} etc.
-- If you want to create a BEACON, you probably should use @{Wrapper.Positionable#POSITIONABLE.GetBeacon}() instead.
-- @param #BEACON self
-- @param Wrapper.Positionable#POSITIONABLE Positionable The @{Positionable} that will receive radio capabilities.
-- @return #BEACON Beacon object or #nil if the positionable is invalid.
function BEACON:New(Positionable)
-- Inherit BASE.
local self=BASE:Inherit(self, BASE:New()) --#BEACON
-- Debug.
self:F(Positionable)
-- Set positionable.
if Positionable:GetPointVec2() then -- It's stupid, but the only way I found to make sure positionable is valid
self.Positionable = Positionable
self.name=Positionable:GetName()
self:I(string.format("New BEACON %s", tostring(self.name)))
return self
end
self:E({"The passed positionable is invalid, no BEACON created", Positionable})
return nil
end
--- Activates a TACAN BEACON.
-- @param #BEACON self
-- @param #number Channel TACAN channel, i.e. the "10" part in "10Y".
-- @param #string Mode TACAN mode, i.e. the "Y" part in "10Y".
-- @param #string Message The Message that is going to be coded in Morse and broadcasted by the beacon.
-- @param #boolean Bearing If true, beacon provides bearing information. If false (or nil), only distance information is available.
-- @param #number Duration How long will the beacon last in seconds. Omit for forever.
-- @return #BEACON self
-- @usage
-- -- Let's create a TACAN Beacon for a tanker
-- local myUnit = UNIT:FindByName("MyUnit")
-- local myBeacon = myUnit:GetBeacon() -- Creates the beacon
--
-- myBeacon:ActivateTACAN(20, "Y", "TEXACO", true) -- Activate the beacon
function BEACON:ActivateTACAN(Channel, Mode, Message, Bearing, Duration)
self:T({channel=Channel, mode=Mode, callsign=Message, bearing=Bearing, duration=Duration})
-- Get frequency.
local Frequency=UTILS.TACANToFrequency(Channel, Mode)
-- Check.
if not Frequency then
self:E({"The passed TACAN channel is invalid, the BEACON is not emitting"})
return self
end
-- Beacon type.
local Type=BEACON.Type.TACAN
-- Beacon system.
local System=BEACON.System.TACAN
-- Check if unit is an aircraft and set system accordingly.
local AA=self.Positionable:IsAir()
if AA then
System=5 --NOTE: 5 is how you cat the correct tanker behaviour! --BEACON.System.TACAN_TANKER
-- Check if "Y" mode is selected for aircraft.
if Mode~="Y" then
self:E({"WARNING: The POSITIONABLE you want to attach the AA Tacan Beacon is an aircraft: Mode should Y !The BEACON is not emitting.", self.Positionable})
end
end
-- Attached unit.
local UnitID=self.Positionable:GetID()
-- Debug.
self:I({string.format("BEACON Activating TACAN %s: Channel=%d%s, Morse=%s, Bearing=%s, Duration=%s!", tostring(self.name), Channel, Mode, Message, tostring(Bearing), tostring(Duration))})
-- Start beacon.
self.Positionable:CommandActivateBeacon(Type, System, Frequency, UnitID, Channel, Mode, AA, Message, Bearing)
-- Stop sheduler.
if Duration then
self.Positionable:DeactivateBeacon(Duration)
end
return self
end
--- Activates an ICLS BEACON. The unit the BEACON is attached to should be an aircraft carrier supporting this system.
-- @param #BEACON self
-- @param #number Channel ICLS channel.
-- @param #string Callsign The Message that is going to be coded in Morse and broadcasted by the beacon.
-- @param #number Duration How long will the beacon last in seconds. Omit for forever.
-- @return #BEACON self
function BEACON:ActivateICLS(Channel, Callsign, Duration)
self:F({Channel=Channel, Callsign=Callsign, Duration=Duration})
-- Attached unit.
local UnitID=self.Positionable:GetID()
-- Debug
self:T2({"ICLS BEACON started!"})
-- Start beacon.
self.Positionable:CommandActivateICLS(Channel, UnitID, Callsign)
-- Stop sheduler
if Duration then -- Schedule the stop of the BEACON if asked by the MD
self.Positionable:DeactivateBeacon(Duration)
end
return self
end
--- Activates a TACAN BEACON on an Aircraft.
-- @param #BEACON self
-- @param #number TACANChannel (the "10" part in "10Y"). Note that AA TACAN are only available on Y Channels
-- @param #string Message The Message that is going to be coded in Morse and broadcasted by the beacon
-- @param #boolean Bearing Can the BEACON be homed on ?
-- @param #number BeaconDuration How long will the beacon last in seconds. Omit for forever.
-- @return #BEACON self
-- @usage
-- -- Let's create a TACAN Beacon for a tanker
-- local myUnit = UNIT:FindByName("MyUnit")
-- local myBeacon = myUnit:GetBeacon() -- Creates the beacon
--
-- myBeacon:AATACAN(20, "TEXACO", true) -- Activate the beacon
function BEACON:AATACAN(TACANChannel, Message, Bearing, BeaconDuration)
self:F({TACANChannel, Message, Bearing, BeaconDuration})
local IsValid = true
if not self.Positionable:IsAir() then
self:E({"The POSITIONABLE you want to attach the AA Tacan Beacon is not an aircraft ! The BEACON is not emitting", self.Positionable})
IsValid = false
end
local Frequency = self:_TACANToFrequency(TACANChannel, "Y")
if not Frequency then
self:E({"The passed TACAN channel is invalid, the BEACON is not emitting"})
IsValid = false
end
-- I'm using the beacon type 4 (BEACON_TYPE_TACAN). For System, I'm using 5 (TACAN_TANKER_MODE_Y) if the bearing shows its bearing
-- or 14 (TACAN_AA_MODE_Y) if it does not
local System
if Bearing then
System = 5
else
System = 14
end
if IsValid then -- Starts the BEACON
self:T2({"AA TACAN BEACON started !"})
self.Positionable:SetCommand({
id = "ActivateBeacon",
params = {
type = 4,
system = System,
callsign = Message,
frequency = Frequency,
}
})
if BeaconDuration then -- Schedule the stop of the BEACON if asked by the MD
SCHEDULER:New(nil,
function()
self:StopAATACAN()
end, {}, BeaconDuration)
end
end
return self
end
--- Stops the AA TACAN BEACON
-- @param #BEACON self
-- @return #BEACON self
function BEACON:StopAATACAN()
self:F()
if not self.Positionable then
self:E({"Start the beacon first before stoping it !"})
else
self.Positionable:SetCommand({
id = 'DeactivateBeacon',
params = {
}
})
end
end
--- Activates a general pupose Radio Beacon
-- This uses the very generic singleton function "trigger.action.radioTransmission()" provided by DCS to broadcast a sound file on a specific frequency.
-- Although any frequency could be used, only 2 DCS Modules can home on radio beacons at the time of writing : the Huey and the Mi-8.
-- They can home in on these specific frequencies :
-- * **Mi8**
-- * R-828 -> 20-60MHz
-- * ARKUD -> 100-150MHz (canal 1 : 114166, canal 2 : 114333, canal 3 : 114583, canal 4 : 121500, canal 5 : 123100, canal 6 : 124100) AM
-- * ARK9 -> 150-1300KHz
-- * **Huey**
-- * AN/ARC-131 -> 30-76 Mhz FM
-- @param #BEACON self
-- @param #string FileName The name of the audio file
-- @param #number Frequency in MHz
-- @param #number Modulation either radio.modulation.AM or radio.modulation.FM
-- @param #number Power in W
-- @param #number BeaconDuration How long will the beacon last in seconds. Omit for forever.
-- @return #BEACON self
-- @usage
-- -- Let's create a beacon for a unit in distress.
-- -- Frequency will be 40MHz FM (home-able by a Huey's AN/ARC-131)
-- -- The beacon they use is battery-powered, and only lasts for 5 min
-- local UnitInDistress = UNIT:FindByName("Unit1")
-- local UnitBeacon = UnitInDistress:GetBeacon()
--
-- -- Set the beacon and start it
-- UnitBeacon:RadioBeacon("MySoundFileSOS.ogg", 40, radio.modulation.FM, 20, 5*60)
function BEACON:RadioBeacon(FileName, Frequency, Modulation, Power, BeaconDuration)
self:F({FileName, Frequency, Modulation, Power, BeaconDuration})
local IsValid = false
-- Check the filename
if type(FileName) == "string" then
if FileName:find(".ogg") or FileName:find(".wav") then
if not FileName:find("l10n/DEFAULT/") then
FileName = "l10n/DEFAULT/" .. FileName
end
IsValid = true
end
end
if not IsValid then
self:E({"File name invalid. Maybe something wrong with the extension ? ", FileName})
end
-- Check the Frequency
if type(Frequency) ~= "number" and IsValid then
self:E({"Frequency invalid. ", Frequency})
IsValid = false
end
Frequency = Frequency * 1000000 -- Conversion to Hz
-- Check the modulation
if Modulation ~= radio.modulation.AM and Modulation ~= radio.modulation.FM and IsValid then --TODO Maybe make this future proof if ED decides to add an other modulation ?
self:E({"Modulation is invalid. Use DCS's enum radio.modulation.", Modulation})
IsValid = false
end
-- Check the Power
if type(Power) ~= "number" and IsValid then
self:E({"Power is invalid. ", Power})
IsValid = false
end
Power = math.floor(math.abs(Power)) --TODO Find what is the maximum power allowed by DCS and limit power to that
if IsValid then
self:T2({"Activating Beacon on ", Frequency, Modulation})
-- Note that this is looped. I have to give this transmission a unique name, I use the class ID
trigger.action.radioTransmission(FileName, self.Positionable:GetPositionVec3(), Modulation, true, Frequency, Power, tostring(self.ID))
if BeaconDuration then -- Schedule the stop of the BEACON if asked by the MD
SCHEDULER:New( nil,
function()
self:StopRadioBeacon()
end, {}, BeaconDuration)
end
end
end
--- Stops the AA TACAN BEACON
-- @param #BEACON self
-- @return #BEACON self
function BEACON:StopRadioBeacon()
self:F()
-- The unique name of the transmission is the class ID
trigger.action.stopRadioTransmission(tostring(self.ID))
return self
end
--- Converts a TACAN Channel/Mode couple into a frequency in Hz
-- @param #BEACON self
-- @param #number TACANChannel
-- @param #string TACANMode
-- @return #number Frequecy
-- @return #nil if parameters are invalid
function BEACON:_TACANToFrequency(TACANChannel, TACANMode)
self:F3({TACANChannel, TACANMode})
if type(TACANChannel) ~= "number" then
if TACANMode ~= "X" and TACANMode ~= "Y" then
return nil -- error in arguments
end
end
-- This code is largely based on ED's code, in DCS World\Scripts\World\Radio\BeaconTypes.lua, line 137.
-- I have no idea what it does but it seems to work
local A = 1151 -- 'X', channel >= 64
local B = 64 -- channel >= 64
if TACANChannel < 64 then
B = 1
end
if TACANMode == 'Y' then
A = 1025
if TACANChannel < 64 then
A = 1088
end
else -- 'X'
if TACANChannel < 64 then
A = 962
end
end
return (A + TACANChannel - B) * 1000000
end

View File

@ -1,20 +1,24 @@
--- **Core** - Queues Radio Transmissions.
--- **Sound** - Queues Radio Transmissions.
--
-- ===
--
-- ## Features:
--
-- * Managed Radio Transmissions.
-- * Manage Radio Transmissions
--
-- ===
--
-- ### Authors: funkyfranky
--
-- @module Core.RadioQueue
-- @module Sound.RadioQueue
-- @image Core_Radio.JPG
--- Manages radio transmissions.
--
-- The main goal of the RADIOQUEUE class is to string together multiple sound files to play a complete sentence.
-- The underlying problem is that radio transmissions in DCS are not queued but played "on top" of each other.
-- Therefore, to achive the goal, it is vital to know the precise duration how long it takes to play the sound file.
--
-- @type RADIOQUEUE
-- @field #string ClassName Name of the class "RADIOQUEUE".
-- @field #boolean Debugmode Debug mode. More info.
@ -35,6 +39,7 @@
-- @field #table numbers Table of number transmission parameters.
-- @field #boolean checking Scheduler is checking the radio queue.
-- @field #boolean schedonce Call ScheduleOnce instead of normal scheduler.
-- @field Sound.SRS#MSRS msrs Moose SRS class.
-- @extends Core.Base#BASE
RADIOQUEUE = {
ClassName = "RADIOQUEUE",
@ -69,12 +74,14 @@ RADIOQUEUE = {
-- @field #boolean isplaying If true, transmission is currently playing.
-- @field #number Tplay Mission time (abs) in seconds when the transmission should be played.
-- @field #number interval Interval in seconds before next transmission.
-- @field Sound.SoundOutput#SOUNDFILE soundfile Sound file object to play via SRS.
-- @field Sound.SoundOutput#SOUNDTEXT soundtext Sound TTS object to play via SRS.
--- Create a new RADIOQUEUE object for a given radio frequency/modulation.
-- @param #RADIOQUEUE self
-- @param #number frequency The radio frequency in MHz.
-- @param #number modulation (Optional) The radio modulation. Default radio.modulation.AM.
-- @param #number modulation (Optional) The radio modulation. Default `radio.modulation.AM` (=0).
-- @param #string alias (Optional) Name of the radio queue.
-- @return #RADIOQUEUE self The RADIOQUEUE object.
function RADIOQUEUE:New(frequency, modulation, alias)
@ -125,9 +132,9 @@ function RADIOQUEUE:Start(delay, dt)
-- Start Scheduler.
if self.schedonce then
self:_CheckRadioQueueDelayed(delay)
self:_CheckRadioQueueDelayed(self.delay)
else
self.RQid=self.scheduler:Schedule(nil, RADIOQUEUE._CheckRadioQueue, {self}, delay, dt)
self.RQid=self.scheduler:Schedule(nil, RADIOQUEUE._CheckRadioQueue, {self}, self.delay, self.dt)
end
return self
@ -170,6 +177,17 @@ function RADIOQUEUE:SetRadioPower(power)
return self
end
--- Set SRS.
-- @param #RADIOQUEUE self
-- @param #string PathToSRS Path to SRS.
-- @param #number Port SRS port. Default 5002.
-- @return #RADIOQUEUE self The RADIOQUEUE object.
function RADIOQUEUE:SetSRS(PathToSRS, Port)
self.msrs=MSRS:New(PathToSRS, self.frequency/1000000, self.modulation)
self.msrs:SetPort(Port)
return self
end
--- Set parameters of a digit.
-- @param #RADIOQUEUE self
-- @param #number digit The digit 0-9.
@ -202,7 +220,7 @@ end
--- Add a transmission to the radio queue.
-- @param #RADIOQUEUE self
-- @param #RADIOQUEUE.Transmission transmission The transmission data table.
-- @return #RADIOQUEUE self The RADIOQUEUE object.
-- @return #RADIOQUEUE self
function RADIOQUEUE:AddTransmission(transmission)
self:F({transmission=transmission})
@ -221,7 +239,7 @@ function RADIOQUEUE:AddTransmission(transmission)
return self
end
--- Add a transmission to the radio queue.
--- Create a new transmission and add it to the radio queue.
-- @param #RADIOQUEUE self
-- @param #string filename Name of the sound file. Usually an ogg or wav file type.
-- @param #number duration Duration in seconds the file lasts.
@ -230,7 +248,7 @@ end
-- @param #number interval Interval in seconds after the last transmission finished.
-- @param #string subtitle Subtitle of the transmission.
-- @param #number subduration Duration [sec] of the subtitle being displayed. Default 5 sec.
-- @return #RADIOQUEUE self The RADIOQUEUE object.
-- @return #RADIOQUEUE.Transmission Radio transmission table.
function RADIOQUEUE:NewTransmission(filename, duration, path, tstart, interval, subtitle, subduration)
-- Sanity checks.
@ -269,9 +287,36 @@ function RADIOQUEUE:NewTransmission(filename, duration, path, tstart, interval,
-- Add transmission to queue.
self:AddTransmission(transmission)
return transmission
end
--- Add a SOUNDFILE to the radio queue.
-- @param #RADIOQUEUE self
-- @param Sound.SoundOutput#SOUNDFILE soundfile Sound file object to be added.
-- @param #number tstart Start time (abs) seconds. Default now.
-- @param #number interval Interval in seconds after the last transmission finished.
-- @return #RADIOQUEUE self
function RADIOQUEUE:AddSoundFile(soundfile, tstart, interval)
--env.info(string.format("FF add soundfile: name=%s%s", soundfile:GetPath(), soundfile:GetFileName()))
local transmission=self:NewTransmission(soundfile:GetFileName(), soundfile.duration, soundfile:GetPath(), tstart, interval, soundfile.subtitle, soundfile.subduration)
transmission.soundfile=soundfile
return self
end
--- Add a SOUNDTEXT to the radio queue.
-- @param #RADIOQUEUE self
-- @param Sound.SoundOutput#SOUNDTEXT soundtext Text-to-speech text.
-- @param #number tstart Start time (abs) seconds. Default now.
-- @param #number interval Interval in seconds after the last transmission finished.
-- @return #RADIOQUEUE self
function RADIOQUEUE:AddSoundText(soundtext, tstart, interval)
local transmission=self:NewTransmission("SoundText.ogg", soundtext.duration, nil, tstart, interval, soundtext.subtitle, soundtext.subduration)
transmission.soundtext=soundtext
return self
end
--- Convert a number (as string) into a radio transmission.
-- E.g. for board number or headings.
-- @param #RADIOQUEUE self
@ -280,19 +325,9 @@ end
-- @param #number interval Interval between the next call.
-- @return #number Duration of the call in seconds.
function RADIOQUEUE:Number2Transmission(number, delay, interval)
--- Split string into characters.
local function _split(str)
local chars={}
for i=1,#str do
local c=str:sub(i,i)
table.insert(chars, c)
end
return chars
end
-- Split string into characters.
local numbers=_split(number)
local numbers=UTILS.GetCharacters(number)
local wait=0
for i=1,#numbers do
@ -325,6 +360,11 @@ end
-- @param #RADIOQUEUE.Transmission transmission The transmission.
function RADIOQUEUE:Broadcast(transmission)
if ((transmission.soundfile and transmission.soundfile.useSRS) or transmission.soundtext) and self.msrs then
self:_BroadcastSRS(transmission)
return
end
-- Get unit sending the transmission.
local sender=self:_GetRadioSender()
@ -416,6 +456,19 @@ function RADIOQUEUE:Broadcast(transmission)
end
end
--- Broadcast radio message.
-- @param #RADIOQUEUE self
-- @param #RADIOQUEUE.Transmission transmission The transmission.
function RADIOQUEUE:_BroadcastSRS(transmission)
if transmission.soundfile and transmission.soundfile.useSRS then
self.msrs:PlaySoundFile(transmission.soundfile)
elseif transmission.soundtext then
self.msrs:PlaySoundText(transmission.soundtext)
end
end
--- Start checking the radio queue.
-- @param #RADIOQUEUE self
-- @param #number delay Delay in seconds before checking.
@ -547,7 +600,7 @@ function RADIOQUEUE:_GetRadioSender()
return nil
end
--- Get unit from which we want to transmit a radio message. This has to be an aircraft for subtitles to work.
--- Get unit from which we want to transmit a radio message. This has to be an aircraft or ground unit for subtitles to work.
-- @param #RADIOQUEUE self
-- @return DCS#Vec3 Vector 3D.
function RADIOQUEUE:_GetRadioSenderCoord()

View File

@ -11,7 +11,7 @@
--
-- ### Authors: FlightControl
--
-- @module Core.RadioSpeech
-- @module Sound.RadioSpeech
-- @image Core_Radio.JPG
--- Makes the radio speak.
@ -162,31 +162,31 @@ RADIOSPEECH.Vocabulary.RU = {
["8000"] = { "8000", 0.92 },
["9000"] = { "9000", 0.87 },
["степени"] = { "degrees", 0.5 },
["километров"] = { "kilometers", 0.65 },
["Ñ<EFBFBD>Ñепени"] = { "degrees", 0.5 },
["километров"] = { "kilometers", 0.65 },
["km"] = { "kilometers", 0.65 },
["миль"] = { "miles", 0.45 },
["миль"] = { "miles", 0.45 },
["mi"] = { "miles", 0.45 },
["метры"] = { "meters", 0.41 },
["метры"] = { "meters", 0.41 },
["m"] = { "meters", 0.41 },
["ноги"] = { "feet", 0.37 },
["ноги"] = { "feet", 0.37 },
["br"] = { "br", 1.1 },
["bra"] = { "bra", 0.3 },
["возвращаясь на базу"] = { "returning_to_base", 1.40 },
["на пути к наземной цели"] = { "on_route_to_ground_target", 1.45 },
["перехват самолетов"] = { "intercepting_bogeys", 1.22 },
["поражение наземной цели"] = { "engaging_ground_target", 1.53 },
["захватывающие самолеты"] = { "engaging_bogeys", 1.68 },
["колеса вверх"] = { "wheels_up", 0.92 },
["посадка на базу"] = { "landing at base", 1.04 },
["патрулирующий"] = { "patrolling", 0.96 },
["возвращаÑ<EFBFBD>Ñ<EFBFBD>ÑŒ на базу"] = { "returning_to_base", 1.40 },
["на пути к наземной цели"] = { "on_route_to_ground_target", 1.45 },
["перехват Ñ<>амолеÑов"] = { "intercepting_bogeys", 1.22 },
["поражение наземной цели"] = { "engaging_ground_target", 1.53 },
["захватывающие Ñ<>амолеÑÑ"] = { "engaging_bogeys", 1.68 },
["колеÑ<EFBFBD>а вверх"] = { "wheels_up", 0.92 },
["поÑ<EFBFBD>адка на базу"] = { "landing at base", 1.04 },
["патрулирующий"] = { "patrolling", 0.96 },
["за"] = { "for", 0.27 },
["и"] = { "and", 0.17 },
["в"] = { "at", 0.19 },
["за"] = { "for", 0.27 },
["и"] = { "and", 0.17 },
["в"] = { "at", 0.19 },
["dot"] = { "dot", 0.51 },
["defender"] = { "defender", 0.45 },
}

View File

@ -0,0 +1,692 @@
--- **Sound** - Simple Radio Standalone (SRS) Integration.
--
-- ===
--
-- **Main Features:**
--
-- * Play sound files via SRS
-- * Play text-to-speach via SRS
--
-- ===
--
-- ## Youtube Videos: None yet
--
-- ===
--
-- ## Missions: None yet
--
-- ===
--
-- ## Sound files: [MOOSE Sound Files](https://github.com/FlightControl-Master/MOOSE_SOUND/releases)
--
-- ===
--
-- The goal of the [SRS](https://github.com/ciribob/DCS-SimpleRadioStandalone) project is to bring VoIP communication into DCS and to make communication as frictionless as possible.
--
-- ===
--
-- ### Author: **funkyfranky**
-- @module Sound.MSRS
-- @image Sound_MSRS.png
--- MSRS class.
-- @type MSRS
-- @field #string ClassName Name of the class.
-- @field #string lid Class id string for output to DCS log file.
-- @field #table frequencies Frequencies used in the transmissions.
-- @field #table modulations Modulations used in the transmissions.
-- @field #number coalition Coalition of the transmission.
-- @field #number port Port. Default 5002.
-- @field #string name Name. Default "DCS-STTS".
-- @field #number volume Volume between 0 (min) and 1 (max). Default 1.
-- @field #string culture Culture. Default "en-GB".
-- @field #string gender Gender. Default "female".
-- @field #string voice Specifc voce.
-- @field Core.Point#COORDINATE coordinate Coordinate from where the transmission is send.
-- @field #string path Path to the SRS exe. This includes the final slash "/".
-- @field #string google Full path google credentials JSON file, e.g. "C:\Users\username\Downloads\service-account-file.json".
-- @extends Core.Base#BASE
--- *It is a very sad thing that nowadays there is so little useless information.* - Oscar Wilde
--
-- ===
--
-- ![Banner Image](..\Presentations\ATIS\ATIS_Main.png)
--
-- # The MSRS Concept
--
-- This class allows to broadcast sound files or text via Simple Radio Standalone (SRS).
--
-- ## Prerequisites
--
-- This script needs SRS version >= 1.9.6.
--
-- # Play Sound Files
--
-- local soundfile=SOUNDFILE:New("My Soundfile.ogg", "D:\\Sounds For DCS")
-- local msrs=MSRS:New("C:\\Path To SRS", 251, radio.modulation.AM)
-- msrs:PlaySoundFile(soundfile)
--
-- # Play Text-To-Speech
--
-- Basic example:
--
-- -- Create a SOUNDTEXT object.
-- local text=SOUNDTEXT:New("All Enemies destroyed")
--
-- -- MOOSE SRS
-- local msrs=MSRS:New("D:\\DCS\\_SRS\\", 305, radio.modulation.AM)
--
-- -- Text-to speech with default voice after 2 seconds.
-- msrs:PlaySoundText(text, 2)
--
-- ## Set Gender
--
-- Use a specific gender with the @{#MSRS.SetGender} function, e.g. `SetGender("male")` or `:SetGender("female")`.
--
-- ## Set Culture
--
-- Use a specific "culture" with the @{#MSRS.SetCulture} function, e.g. `:SetCulture("en-US")` or `:SetCulture("de-DE")`.
--
-- ## Set Voice
--
-- Use a specifc voice with the @{#MSRS.SetVoice} function, e.g, `:SetVoice("Microsoft Hedda Desktop")`.
-- Note that this must be installed on your windows system.
--
-- ## Set Coordinate
--
-- Use @{#MSRS.SetCoordinate} to define the origin from where the transmission is broadcasted.
--
-- @field #MSRS
MSRS = {
ClassName = "MSRS",
lid = nil,
port = 5002,
name = "MSRS",
frequencies = {},
modulations = {},
coalition = 0,
gender = "female",
culture = nil,
voice = nil,
volume = 1,
speed = 1,
coordinate = nil,
}
--- MSRS class version.
-- @field #string version
MSRS.version="0.0.3"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO: Add functions to add/remove freqs and modulations.
-- DONE: Add coordinate.
-- DONE: Add google.
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Create a new MSRS object.
-- @param #MSRS self
-- @param #string PathToSRS Path to the directory, where SRS is located.
-- @param #number Frequency Radio frequency in MHz. Default 143.00 MHz. Can also be given as a #table of multiple frequencies.
-- @param #number Modulation Radio modulation: 0=AM (default), 1=FM. See `radio.modulation.AM` and `radio.modulation.FM` enumerators. Can also be given as a #table of multiple modulations.
-- @return #MSRS self
function MSRS:New(PathToSRS, Frequency, Modulation)
-- Defaults.
Frequency =Frequency or 143
Modulation= Modulation or radio.modulation.AM
-- Inherit everything from FSM class.
local self=BASE:Inherit(self, BASE:New()) -- #MSRS
self:SetPath(PathToSRS)
self:SetPort()
self:SetFrequencies(Frequency)
self:SetModulations(Modulation)
self:SetGender()
self:SetCoalition()
return self
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Set path to SRS install directory. More precisely, path to where the DCS-
-- @param #MSRS self
-- @param #string Path Path to the directory, where the sound file is located. This does **not** contain a final backslash or slash.
-- @return #MSRS self
function MSRS:SetPath(Path)
if Path==nil then
self:E("ERROR: No path to SRS directory specified!")
return nil
end
-- Set path.
self.path=Path
-- Remove (back)slashes.
local n=1 ; local nmax=1000
while (self.path:sub(-1)=="/" or self.path:sub(-1)==[[\]]) and n<=nmax do
self.path=self.path:sub(1,#self.path-1)
n=n+1
end
-- Debug output.
self:T(string.format("SRS path=%s", self:GetPath()))
return self
end
--- Get path to SRS directory.
-- @param #MSRS self
-- @return #string Path to the directory. This includes the final slash "/".
function MSRS:GetPath()
return self.path
end
--- Set port.
-- @param #MSRS self
-- @param #number Port Port. Default 5002.
-- @return #MSRS self
function MSRS:SetPort(Port)
self.port=Port or 5002
end
--- Get port.
-- @param #MSRS self
-- @return #number Port.
function MSRS:GetPort()
return self.port
end
--- Set coalition.
-- @param #MSRS self
-- @param #number Coalition Coalition. Default 0.
-- @return #MSRS self
function MSRS:SetCoalition(Coalition)
self.coalition=Coalition or 0
end
--- Get coalition.
-- @param #MSRS self
-- @return #number Coalition.
function MSRS:GetCoalition()
return self.coalition
end
--- Set frequencies.
-- @param #MSRS self
-- @param #table Frequencies Frequencies in MHz. Can also be given as a #number if only one frequency should be used.
-- @return #MSRS self
function MSRS:SetFrequencies(Frequencies)
-- Ensure table.
if type(Frequencies)~="table" then
Frequencies={Frequencies}
end
self.frequencies=Frequencies
return self
end
--- Get frequencies.
-- @param #MSRS self
-- @param #table Frequencies in MHz.
function MSRS:GetFrequencies()
return self.frequencies
end
--- Set modulations.
-- @param #MSRS self
-- @param #table Modulations Modulations. Can also be given as a #number if only one modulation should be used.
-- @return #MSRS self
function MSRS:SetModulations(Modulations)
-- Ensure table.
if type(Modulations)~="table" then
Modulations={Modulations}
end
self.modulations=Modulations
return self
end
--- Get modulations.
-- @param #MSRS self
-- @param #table Modulations.
function MSRS:GetModulations()
return self.modulations
end
--- Set gender.
-- @param #MSRS self
-- @param #string Gender Gender: "male" or "female" (default).
-- @return #MSRS self
function MSRS:SetGender(Gender)
Gender=Gender or "female"
self.gender=Gender:lower()
-- Debug output.
self:T("Setting gender to "..tostring(self.gender))
return self
end
--- Set culture.
-- @param #MSRS self
-- @param #string Culture Culture, e.g. "en-GB" (default).
-- @return #MSRS self
function MSRS:SetCulture(Culture)
self.culture=Culture
return self
end
--- Set to use a specific voice. Will override gender and culture settings.
-- @param #MSRS self
-- @param #string Voice Voice.
-- @return #MSRS self
function MSRS:SetVoice(Voice)
self.voice=Voice
return self
end
--- Set the coordinate from which the transmissions will be broadcasted.
-- @param #MSRS self
-- @param Core.Point#COORDINATE Coordinate Origin of the transmission.
-- @return #MSRS self
function MSRS:SetCoordinate(Coordinate)
self.coordinate=Coordinate
return self
end
--- Use google text-to-speech.
-- @param #MSRS self
-- @param PathToCredentials Full path to the google credentials JSON file, e.g. "C:\Users\username\Downloads\service-account-file.json".
-- @return #MSRS self
function MSRS:SetGoogle(PathToCredentials)
self.google=PathToCredentials
return self
end
--- Print SRS STTS help to DCS log file.
-- @param #MSRS self
-- @return #MSRS self
function MSRS:Help()
-- Path and exe.
local path=self:GetPath() or STTS.DIRECTORY
local exe=STTS.EXECUTABLE or "DCS-SR-ExternalAudio.exe"
-- Text file for output.
local filename = os.getenv('TMP') .. "\\MSRS-help-"..STTS.uuid()..".txt"
-- Print help.
local command=string.format("%s/%s --help > %s", path, exe, filename)
os.execute(command)
local f=assert(io.open(filename, "rb"))
local data=f:read("*all")
f:close()
-- Print to log file.
env.info("SRS STTS help output:")
env.info("======================================================================")
env.info(data)
env.info("======================================================================")
return self
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Transmission Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Play sound file (ogg or mp3) via SRS.
-- @param #MSRS self
-- @param Sound.SoundFile#SOUNDFILE Soundfile Sound file to play.
-- @param #number Delay Delay in seconds, before the sound file is played.
-- @return #MSRS self
function MSRS:PlaySoundFile(Soundfile, Delay)
if Delay and Delay>0 then
self:ScheduleOnce(Delay, MSRS.PlaySoundFile, self, Soundfile, 0)
else
-- Sound file name.
local soundfile=Soundfile:GetName()
-- Get command.
local command=self:_GetCommand()
-- Append file.
command=command.." --file="..tostring(soundfile)
self:_ExecCommand(command)
--[[
command=command.." > bla.txt"
-- Debug output.
self:I(string.format("MSRS PlaySoundfile command=%s", command))
-- Execute SRS command.
local x=os.execute(command)
]]
end
return self
end
--- Play a SOUNDTEXT text-to-speech object.
-- @param #MSRS self
-- @param Sound.SoundFile#SOUNDTEXT SoundText Sound text.
-- @param #number Delay Delay in seconds, before the sound file is played.
-- @return #MSRS self
function MSRS:PlaySoundText(SoundText, Delay)
if Delay and Delay>0 then
self:ScheduleOnce(Delay, MSRS.PlaySoundText, self, SoundText, 0)
else
-- Get command.
local command=self:_GetCommand(nil, nil, nil, SoundText.gender, SoundText.voice, SoundText.culture, SoundText.volume, SoundText.speed)
-- Append text.
command=command..string.format(" --text=\"%s\"", tostring(SoundText.text))
-- Execute command.
self:_ExecCommand(command)
--[[
command=command.." > bla.txt"
-- Debug putput.
self:I(string.format("MSRS PlaySoundfile command=%s", command))
-- Execute SRS command.
local x=os.execute(command)
]]
end
return self
end
--- Play text message via STTS.
-- @param #MSRS self
-- @param #string Text Text message.
-- @param #number Delay Delay in seconds, before the message is played.
-- @return #MSRS self
function MSRS:PlayText(Text, Delay)
if Delay and Delay>0 then
self:ScheduleOnce(Delay, MSRS.PlayText, self, Text, 0)
else
-- Get command line.
local command=self:_GetCommand()
-- Append text.
command=command..string.format(" --text=\"%s\"", tostring(Text))
-- Execute command.
self:_ExecCommand(command)
--[[
-- Check that length of command is max 255 chars or os.execute() will not work!
if string.len(command)>255 then
-- Create a tmp file.
local filename = os.getenv('TMP') .. "\\MSRS-"..STTS.uuid()..".bat"
local script = io.open(filename, "w+")
script:write(command.." && exit")
script:close()
-- Play command.
command=string.format("\"%s\"", filename)
-- Play file in 0.05 seconds
timer.scheduleFunction(os.execute, command, timer.getTime()+0.05)
-- Remove file in 1 second.
timer.scheduleFunction(os.remove, filename, timer.getTime()+1)
else
-- Debug output.
self:I(string.format("MSRS Text command=%s", command))
-- Execute SRS command.
local x=os.execute(command)
end
]]
end
return self
end
--- Play text file via STTS.
-- @param #MSRS self
-- @param #string TextFile Full path to the file.
-- @param #number Delay Delay in seconds, before the message is played.
-- @return #MSRS self
function MSRS:PlayTextFile(TextFile, Delay)
if Delay and Delay>0 then
self:ScheduleOnce(Delay, MSRS.PlayTextFile, self, TextFile, 0)
else
-- First check if text file exists!
local exists=UTILS.FileExists(TextFile)
if not exists then
self:E("ERROR: MSRS Text file does not exist! File="..tostring(TextFile))
return self
end
-- Get command line.
local command=self:_GetCommand()
-- Append text file.
command=command..string.format(" --textFile=\"%s\"", tostring(TextFile))
-- Debug output.
self:T(string.format("MSRS TextFile command=%s", command))
-- Count length of command.
local l=string.len(command)
-- Execute command.
self:_ExecCommand(command)
end
return self
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Misc Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Execute SRS command to play sound using the `DCS-SR-ExternalAudio.exe`.
-- @param #MSRS self
-- @param #string command Command to executer
-- @return #number Return value of os.execute() command.
function MSRS:_ExecCommand(command)
-- Create a tmp file.
local filename=os.getenv('TMP').."\\MSRS-"..STTS.uuid()..".bat"
local script=io.open(filename, "w+")
script:write(command.." && exit")
script:close()
-- Play command.
command=string.format('start /b "" "%s"', filename)
local res=nil
if true then
-- Create a tmp file.
local filenvbs = os.getenv('TMP') .. "\\MSRS-"..STTS.uuid()..".vbs"
-- VBS script
local script = io.open(filenvbs, "w+")
script:write(string.format('Dim WinScriptHost\n'))
script:write(string.format('Set WinScriptHost = CreateObject("WScript.Shell")\n'))
script:write(string.format('WinScriptHost.Run Chr(34) & "%s" & Chr(34), 0\n', filename))
script:write(string.format('Set WinScriptHost = Nothing'))
script:close()
-- Run visual basic script. This still pops up a window but very briefly and does not put the DCS window out of focus.
local runvbs=string.format('cscript.exe //Nologo //B "%s"', filenvbs)
-- Debug output.
self:T("MSRS execute command="..command)
self:T("MSRS execute VBS command="..runvbs)
-- Play file in 0.01 seconds
res=os.execute(runvbs)
-- Remove file in 1 second.
timer.scheduleFunction(os.remove, filename, timer.getTime()+1)
timer.scheduleFunction(os.remove, filenvbs, timer.getTime()+1)
else
-- Debug output.
self:T("MSRS execute command="..command)
-- Execute command
res=os.execute(command)
-- Remove file in 1 second.
timer.scheduleFunction(os.remove, filename, timer.getTime()+1)
end
return res
end
--- Get lat, long and alt from coordinate.
-- @param #MSRS self
-- @param Core.Point#Coordinate Coordinate Coordinate. Can also be a DCS#Vec3.
-- @return #number Latitude.
-- @return #number Longitude.
-- @return #number Altitude.
function MSRS:_GetLatLongAlt(Coordinate)
local lat, lon, alt=coord.LOtoLL(Coordinate)
return lat, lon, math.floor(alt)
end
--- Get SRS command to play sound using the `DCS-SR-ExternalAudio.exe`.
-- @param #MSRS self
-- @param #table freqs Frequencies in MHz.
-- @param #table modus Modulations.
-- @param #number coal Coalition.
-- @param #string gender Gender.
-- @param #string voice Voice.
-- @param #string culture Culture.
-- @param #number volume Volume.
-- @param #number speed Speed.
-- @param #number port Port.
-- @return #string Command.
function MSRS:_GetCommand(freqs, modus, coal, gender, voice, culture, volume, speed, port)
local path=self:GetPath() or STTS.DIRECTORY
local exe=STTS.EXECUTABLE or "DCS-SR-ExternalAudio.exe"
freqs=table.concat(freqs or self.frequencies, ",")
modus=table.concat(modus or self.modulations, ",")
coal=coal or self.coalition
gender=gender or self.gender
voice=voice or self.voice
culture=culture or self.culture
volume=volume or self.volume
speed=speed or self.speed
port=port or self.port
-- Replace modulation
modus=modus:gsub("0", "AM")
modus=modus:gsub("1", "FM")
-- This did not work well. Stopped if the transmission was a bit longer with no apparent error.
--local command=string.format("%s --freqs=%s --modulations=%s --coalition=%d --port=%d --volume=%.2f --speed=%d", exe, freqs, modus, coal, port, volume, speed)
-- Command from orig STTS script. Works better for some unknown reason!
local command=string.format("start /min \"\" /d \"%s\" /b \"%s\" -f %s -m %s -c %s -p %s -n \"%s\" -h", path, exe, freqs, modus, coal, port, "ROBOT")
--local command=string.format('start /b "" /d "%s" "%s" -f %s -m %s -c %s -p %s -n "%s" > bla.txt', path, exe, freqs, modus, coal, port, "ROBOT")
-- Command.
local command=string.format('%s/%s -f %s -m %s -c %s -p %s -n "%s"', path, exe, freqs, modus, coal, port, "ROBOT")
-- Set voice or gender/culture.
if voice then
-- Use a specific voice (no need for gender and/or culture.
command=command..string.format(" --voice=\"%s\"", tostring(voice))
else
-- Add gender.
if gender and gender~="female" then
command=command..string.format(" -g %s", tostring(gender))
end
-- Add culture.
if culture and culture~="en-GB" then
command=command..string.format(" -l %s", tostring(culture))
end
end
-- Set coordinate.
if self.coordinate then
local lat,lon,alt=self:_GetLatLongAlt(self.coordinate)
command=command..string.format(" -L %.4f -O %.4f -A %d", lat, lon, alt)
end
-- Set google.
if self.google then
command=command..string.format(' -G "%s"', self.google)
end
-- Debug output.
self:T("MSRS command="..command)
return command
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

View File

@ -0,0 +1,408 @@
--- **Sound** - Sound output classes.
--
-- ===
--
-- ## Features:
--
-- * Create a SOUNDFILE object (mp3 or ogg) to be played via DCS or SRS transmissions
-- * Create a SOUNDTEXT object for text-to-speech output vis SRS Simple-Text-To-Speech (STTS)
--
-- ===
--
-- ### Author: **funkyfranky**
--
-- ===
--
-- There are two classes, SOUNDFILE and SOUNDTEXT, defined in this section that deal with playing
-- sound files or arbitrary text (via SRS Simple-Text-To-Speech), respectively.
--
-- The SOUNDFILE and SOUNDTEXT objects can be defined and used in other MOOSE classes.
--
--
-- @module Sound.SoundOutput
-- @image Sound_SoundOutput.png
do -- Sound Base
--- @type SOUNDBASE
-- @field #string ClassName Name of the class.
-- @extends Core.Base#BASE
--- Basic sound output inherited by other classes suche as SOUNDFILE and SOUNDTEXT.
--
-- This class is **not** meant to be used by "ordinary" users.
--
-- @field #SOUNDBASE
SOUNDBASE={
ClassName = "SOUNDBASE",
}
--- Constructor to create a new SOUNDBASE object.
-- @param #SOUNDBASE self
-- @return #SOUNDBASE self
function SOUNDBASE:New()
-- Inherit BASE.
local self=BASE:Inherit(self, BASE:New()) -- #SOUNDBASE
return self
end
--- Function returns estimated speech time in seconds.
-- Assumptions for time calc: 100 Words per min, avarage of 5 letters for english word so
--
-- * 5 chars * 100wpm = 500 characters per min = 8.3 chars per second
--
-- So lengh of msg / 8.3 = number of seconds needed to read it. rounded down to 8 chars per sec map function:
--
-- * (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
--
-- @param #string Text The text string to analyze.
-- @param #number Speed Speed factor. Default 1.
-- @param #boolean isGoogle If true, google text-to-speech is used.
function SOUNDBASE:GetSpeechTime(length,speed,isGoogle)
local maxRateRatio = 3
speed = speed or 1.0
isGoogle = isGoogle or false
local speedFactor = 1.0
if isGoogle then
speedFactor = speed
else
if speed ~= 0 then
speedFactor = math.abs(speed) * (maxRateRatio - 1) / 10 + 1
end
if speed < 0 then
speedFactor = 1/speedFactor
end
end
-- Words per minute.
local wpm = math.ceil(100 * speedFactor)
-- Characters per second.
local cps = math.floor((wpm * 5)/60)
if type(length) == "string" then
length = string.len(length)
end
return math.ceil(length/cps)
end
end
do -- Sound File
--- @type SOUNDFILE
-- @field #string ClassName Name of the class
-- @field #string filename Name of the flag.
-- @field #string path Directory path, where the sound file is located. This includes the final slash "/".
-- @field #string duration Duration of the sound file in seconds.
-- @field #string subtitle Subtitle of the transmission.
-- @field #number subduration Duration in seconds how long the subtitle is displayed.
-- @field #boolean useSRS If true, sound file is played via SRS. Sound file needs to be on local disk not inside the miz file!
-- @extends Core.Base#BASE
--- Sound files used by other classes.
--
-- # The SOUNDFILE Concept
--
-- A SOUNDFILE object hold the important properties that are necessary to play the sound file, e.g. its file name, path, duration.
--
-- It can be created with the @{#SOUNDFILE.New}(*FileName*, *Path*, *Duration*) function:
--
-- local soundfile=SOUNDFILE:New("My Soundfile.ogg", "Sound File/", 3.5)
--
-- ## SRS
--
-- If sound files are supposed to be played via SRS, you need to use the @{#SOUNDFILE.SetPlayWithSRS}() function.
--
-- # Location/Path
--
-- ## DCS
--
-- DCS can only play sound files that are located inside the mission (.miz) file. In particular, DCS cannot make use of files that are stored on
-- your hard drive.
--
-- The default location where sound files are stored in DCS is the directory "l10n/DEFAULT/". This is where sound files are placed, if they are
-- added via the mission editor (TRIGGERS-->ACTIONS-->SOUND TO ALL). Note however, that sound files which are not added with a trigger command,
-- will be deleted each time the mission is saved! Therefore, this directory is not ideal to be used especially if many sound files are to
-- be included since for each file a trigger action needs to be created. Which is cumbersome, to say the least.
--
-- The recommended way is to create a new folder inside the mission (.miz) file (a miz file is essentially zip file and can be opened, e.g., with 7-Zip)
-- and to place the sound files in there. Sound files in these folders are not wiped out by DCS on the next save.
--
-- ## SRS
--
-- SRS sound files need to be located on your local drive (not inside the miz). Therefore, you need to specify the full path.
--
-- @field #SOUNDFILE
SOUNDFILE={
ClassName = "SOUNDFILE",
filename = nil,
path = "l10n/DEFAULT/",
duration = 3,
subtitle = nil,
subduration = 0,
useSRS = false,
}
--- Constructor to create a new SOUNDFILE object.
-- @param #SOUNDFILE self
-- @param #string FileName The name of the sound file, e.g. "Hello World.ogg".
-- @param #string Path The path of the directory, where the sound file is located. Default is "l10n/DEFAULT/" within the miz file.
-- @param #number Duration Duration in seconds, how long it takes to play the sound file. Default is 3 seconds.
-- @return #SOUNDFILE self
function SOUNDFILE:New(FileName, Path, Duration)
-- Inherit BASE.
local self=BASE:Inherit(self, BASE:New()) -- #SOUNDFILE
-- Set file name.
self:SetFileName(FileName)
-- Set path.
self:SetPath(Path)
-- Set duration.
self:SetDuration(Duration)
-- Debug info:
self:T(string.format("New SOUNDFILE: file name=%s, path=%s", self.filename, self.path))
return self
end
--- Set path, where the sound file is located.
-- @param #SOUNDFILE self
-- @param #string Path Path to the directory, where the sound file is located.
-- @return #SOUNDFILE self
function SOUNDFILE:SetPath(Path)
-- Init path.
self.path=Path or "l10n/DEFAULT/"
-- Remove (back)slashes.
local nmax=1000 ; local n=1
while (self.path:sub(-1)=="/" or self.path:sub(-1)==[[\]]) and n<=nmax do
self.path=self.path:sub(1,#self.path-1)
n=n+1
end
-- Append slash.
self.path=self.path.."/"
return self
end
--- Get path of the directory, where the sound file is located.
-- @param #SOUNDFILE self
-- @return #string Path.
function SOUNDFILE:GetPath()
local path=self.path or "l10n/DEFAULT/"
return path
end
--- Set sound file name. This must be a .ogg or .mp3 file!
-- @param #SOUNDFILE self
-- @param #string FileName Name of the file. Default is "Hello World.mp3".
-- @return #SOUNDFILE self
function SOUNDFILE:SetFileName(FileName)
--TODO: check that sound file is really .ogg or .mp3
self.filename=FileName or "Hello World.mp3"
return self
end
--- Get the sound file name.
-- @param #SOUNDFILE self
-- @return #string Name of the soud file. This does *not* include its path.
function SOUNDFILE:GetFileName()
return self.filename
end
--- Set duration how long it takes to play the sound file.
-- @param #SOUNDFILE self
-- @param #string Duration Duration in seconds. Default 3 seconds.
-- @return #SOUNDFILE self
function SOUNDFILE:SetDuration(Duration)
self.duration=Duration or 3
return self
end
--- Get duration how long the sound file takes to play.
-- @param #SOUNDFILE self
-- @return #number Duration in seconds.
function SOUNDFILE:GetDuration()
return self.duration or 3
end
--- Get the complete sound file name inlcuding its path.
-- @param #SOUNDFILE self
-- @return #string Name of the sound file.
function SOUNDFILE:GetName()
local path=self:GetPath()
local filename=self:GetFileName()
local name=string.format("%s%s", path, filename)
return name
end
--- Set whether sound files should be played via SRS.
-- @param #SOUNDFILE self
-- @param #boolean Switch If true or nil, use SRS. If false, use DCS transmission.
-- @return #SOUNDFILE self
function SOUNDFILE:SetPlayWithSRS(Switch)
if Switch==true or Switch==nil then
self.useSRS=true
else
self.useSRS=false
end
return self
end
end
do -- Text-To-Speech
--- @type SOUNDTEXT
-- @field #string ClassName Name of the class
-- @field #string text Text to speak.
-- @field #number duration Duration in seconds.
-- @field #string gender Gender: "male", "female".
-- @field #string culture Culture, e.g. "en-GB".
-- @field #string voice Specific voice to use. Overrules `gender` and `culture` settings.
-- @extends Core.Base#BASE
--- Text-to-speech objects for other classes.
--
-- # The SOUNDTEXT Concept
--
-- A SOUNDTEXT object holds all necessary information to play a general text via SRS Simple-Text-To-Speech.
--
-- It can be created with the @{#SOUNDTEXT.New}(*Text*, *Duration*) function.
--
-- * @{#SOUNDTEXT.New}(*Text, Duration*): Creates a new SOUNDTEXT object.
--
-- # Options
--
-- ## Gender
--
-- You can choose a gender ("male" or "femal") with the @{#SOUNDTEXT.SetGender}(*Gender*) function.
-- Note that the gender voice needs to be installed on your windows machine for the used culture (see below).
--
-- ## Culture
--
-- You can choose a "culture" (accent) with the @{#SOUNDTEXT.SetCulture}(*Culture*) function, where the default (SRS) culture is "en-GB".
--
-- Other examples for culture are: "en-US" (US accent), "de-DE" (German), "it-IT" (Italian), "ru-RU" (Russian), "zh-CN" (Chinese).
--
-- Note that the chosen culture needs to be installed on your windows machine.
--
-- ## Specific Voice
--
-- You can use a specific voice for the transmission with the @{SOUNDTEXT.SetVoice}(*VoiceName*) function. Here are some examples
--
-- * Name: Microsoft Hazel Desktop, Culture: en-GB, Gender: Female, Age: Adult, Desc: Microsoft Hazel Desktop - English (Great Britain)
-- * Name: Microsoft David Desktop, Culture: en-US, Gender: Male, Age: Adult, Desc: Microsoft David Desktop - English (United States)
-- * Name: Microsoft Zira Desktop, Culture: en-US, Gender: Female, Age: Adult, Desc: Microsoft Zira Desktop - English (United States)
-- * Name: Microsoft Hedda Desktop, Culture: de-DE, Gender: Female, Age: Adult, Desc: Microsoft Hedda Desktop - German
-- * Name: Microsoft Helena Desktop, Culture: es-ES, Gender: Female, Age: Adult, Desc: Microsoft Helena Desktop - Spanish (Spain)
-- * Name: Microsoft Hortense Desktop, Culture: fr-FR, Gender: Female, Age: Adult, Desc: Microsoft Hortense Desktop - French
-- * Name: Microsoft Elsa Desktop, Culture: it-IT, Gender: Female, Age: Adult, Desc: Microsoft Elsa Desktop - Italian (Italy)
-- * Name: Microsoft Irina Desktop, Culture: ru-RU, Gender: Female, Age: Adult, Desc: Microsoft Irina Desktop - Russian
-- * Name: Microsoft Huihui Desktop, Culture: zh-CN, Gender: Female, Age: Adult, Desc: Microsoft Huihui Desktop - Chinese (Simplified)
--
-- Note that this must be installed on your windos machine. Also note that this overrides any culture and gender settings.
--
-- @field #SOUNDTEXT
SOUNDTEXT={
ClassName = "SOUNDTEXT",
}
--- Constructor to create a new SOUNDTEXT object.
-- @param #SOUNDTEXT self
-- @param #string Text The text to speak.
-- @param #number Duration Duration in seconds, how long it takes to play the text. Default is 3 seconds.
-- @return #SOUNDTEXT self
function SOUNDTEXT:New(Text, Duration)
-- Inherit BASE.
local self=BASE:Inherit(self, BASE:New()) -- #SOUNDTEXT
self:SetText(Text)
self:SetDuration(Duration or STTS.getSpeechTime(Text))
--self:SetGender()
--self:SetCulture()
-- Debug info:
self:T(string.format("New SOUNDTEXT: text=%s, duration=%.1f sec", self.text, self.duration))
return self
end
--- Set text.
-- @param #SOUNDTEXT self
-- @param #string Text Text to speak. Default "Hello World!".
-- @return #SOUNDTEXT self
function SOUNDTEXT:SetText(Text)
self.text=Text or "Hello World!"
return self
end
--- Set duration, how long it takes to speak the text.
-- @param #SOUNDTEXT self
-- @param #number Duration Duration in seconds. Default 3 seconds.
-- @return #SOUNDTEXT self
function SOUNDTEXT:SetDuration(Duration)
self.duration=Duration or 3
return self
end
--- Set gender.
-- @param #SOUNDTEXT self
-- @param #string Gender Gender: "male" or "female" (default).
-- @return #SOUNDTEXT self
function SOUNDTEXT:SetGender(Gender)
self.gender=Gender or "female"
return self
end
--- Set TTS culture - local for the voice.
-- @param #SOUNDTEXT self
-- @param #string Culture TTS culture. Default "en-GB".
-- @return #SOUNDTEXT self
function SOUNDTEXT:SetCulture(Culture)
self.culture=Culture or "en-GB"
return self
end
--- Set to use a specific voice name.
-- See the list from `DCS-SR-ExternalAudio.exe --help` or if using google see [google voices](https://cloud.google.com/text-to-speech/docs/voices).
-- @param #SOUNDTEXT self
-- @param #string VoiceName Voice name. Note that this will overrule `Gender` and `Culture`.
-- @return #SOUNDTEXT self
function SOUNDTEXT:SetVoice(VoiceName)
self.voice=VoiceName
return self
end
end

View File

@ -1,4 +1,4 @@
--- **Core** - Manage user sound.
--- **Sound** - Manage user sound.
--
-- ===
--
@ -16,7 +16,7 @@
--
-- ===
--
-- @module Core.UserSound
-- @module Sound.UserSound
-- @image Core_Usersound.JPG
do -- UserSound

View File

@ -202,6 +202,7 @@ function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName )
self:SetAutoAcceptTasks( true )
self:SetAutoAssignMethod( COMMANDCENTER.AutoAssignMethods.Distance )
self:SetFlashStatus( false )
self:SetMessageDuration(10)
self:HandleEvent( EVENTS.Birth,
--- @param #COMMANDCENTER self
@ -682,7 +683,7 @@ end
-- @param #string Message The message text.
function COMMANDCENTER:MessageToAll( Message )
self:GetPositionable():MessageToAll( Message, 20, self:GetName() )
self:GetPositionable():MessageToAll( Message, self.MessageDuration, self:GetName() )
end
@ -692,7 +693,7 @@ end
-- @param Wrapper.Group#GROUP MessageGroup The group to receive the message.
function COMMANDCENTER:MessageToGroup( Message, MessageGroup )
self:GetPositionable():MessageToGroup( Message, 15, MessageGroup, self:GetShortText() )
self:GetPositionable():MessageToGroup( Message, self.MessageDuration, MessageGroup, self:GetShortText() )
end
@ -715,7 +716,7 @@ function COMMANDCENTER:MessageToCoalition( Message )
local CCCoalition = self:GetPositionable():GetCoalition()
--TODO: Fix coalition bug!
self:GetPositionable():MessageToCoalition( Message, 15, CCCoalition, self:GetShortText() )
self:GetPositionable():MessageToCoalition( Message, self.MessageDuration, CCCoalition, self:GetShortText() )
end
@ -795,9 +796,18 @@ end
--- Let the command center flash a report of the status of the subscribed task to a group.
-- @param #COMMANDCENTER self
-- @param Flash #boolean
function COMMANDCENTER:SetFlashStatus( Flash )
self:F()
self.FlashStatus = Flash or true
self.FlashStatus = Flash and true
end
--- Duration a command center message is shown.
-- @param #COMMANDCENTER self
-- @param seconds #number
function COMMANDCENTER:SetMessageDuration(seconds)
self:F()
self.MessageDuration = 10 or seconds
end

View File

@ -881,6 +881,9 @@ do -- Group Assignment
local Mission = self:GetMission()
local CommandCenter = Mission:GetCommandCenter()
CommandCenter:SetMenu()
self:MenuFlashTaskStatus( TaskGroup, false ) -- stop message flashing, if any #1383 & #1312
end
end
@ -1252,7 +1255,7 @@ function TASK:MenuFlashTaskStatus( TaskGroup, Flash )
self.FlashTaskStatus = Flash
if self.FlashTaskStatus then
self.FlashTaskScheduler, self.FlashTaskScheduleID = SCHEDULER:New( self, self.MenuTaskStatus, { TaskGroup }, 0, 60 )
self.FlashTaskScheduler, self.FlashTaskScheduleID = SCHEDULER:New( self, self.MenuTaskStatus, { TaskGroup }, 0, 60) --Issue #1383 never ending flash messages
else
if self.FlashTaskScheduler then
self.FlashTaskScheduler:Stop( self.FlashTaskScheduleID )

View File

@ -344,7 +344,9 @@ function TASKINFO:Report( Report, Detail, ReportGroup, Task )
Text = DataText
else
local DataText = Data.Data -- #string
Text = DataText
if type(DataText) == "string" then --Issue #1388 - don't just assume this is a string
Text = DataText
end
end
if Line < math.floor( Data.Order / 10 ) then

View File

@ -253,7 +253,12 @@ do -- TASK_A2A_DISPATCHER
return self
end
--- Set flashing player messages on or off
-- @param #TASK_A2A_DISPATCHER self
-- @param #boolean onoff Set messages on (true) or off (false)
function TASK_A2A_DISPATCHER:SetSendMessages( onoff )
self.FlashNewTask = onoff
end
--- Creates an INTERCEPT task when there are targets for it.
-- @param #TASK_A2A_DISPATCHER self
@ -610,7 +615,7 @@ do -- TASK_A2A_DISPATCHER
local TaskText = TaskReport:Text(", ")
for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do
if ( not Mission:IsGroupAssigned(TaskGroup) ) and TaskText ~= "" and ( not not self.FlashNewTask) then
if ( not Mission:IsGroupAssigned(TaskGroup) ) and TaskText ~= "" and (self.FlashNewTask) then
Mission:GetCommandCenter():MessageToGroup( string.format( "%s has tasks %s. Subscribe to a task using the radio menu.", Mission:GetShortText(), TaskText ), TaskGroup )
end
end

View File

@ -451,6 +451,7 @@ do -- TASK_A2G_DISPATCHER
self.Detection = Detection
self.Mission = Mission
self.FlashNewTask = true --set to false to suppress flash messages
self.Detection:FilterCategories( { Unit.Category.GROUND_UNIT } )
@ -471,6 +472,12 @@ do -- TASK_A2G_DISPATCHER
return self
end
--- Set flashing player messages on or off
-- @param #TASK_A2G_DISPATCHER self
-- @param #boolean onoff Set messages on (true) or off (false)
function TASK_A2G_DISPATCHER:SetSendMessages( onoff )
self.FlashNewTask = onoff
end
--- Creates a SEAD task when there are targets for it.
-- @param #TASK_A2G_DISPATCHER self
@ -616,7 +623,9 @@ do -- TASK_A2G_DISPATCHER
if not DetectedItem then
local TaskText = Task:GetName()
for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do
Mission:GetCommandCenter():MessageToGroup( string.format( "Obsolete A2G task %s for %s removed.", TaskText, Mission:GetShortText() ), TaskGroup )
if self.FlashNewTask then
Mission:GetCommandCenter():MessageToGroup( string.format( "Obsolete A2G task %s for %s removed.", TaskText, Mission:GetShortText() ), TaskGroup )
end
end
Task = self:RemoveTask( TaskIndex )
end
@ -686,7 +695,7 @@ do -- TASK_A2G_DISPATCHER
-- Now we send to each group the changes, if any.
for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do
local TargetsText = TargetsReport:Text(", ")
if ( Mission:IsGroupAssigned(TaskGroup) ) and TargetsText ~= "" then
if ( Mission:IsGroupAssigned(TaskGroup) ) and TargetsText ~= "" and self.FlashNewTask then
Mission:GetCommandCenter():MessageToGroup( string.format( "Task %s has change of targets:\n %s", Task:GetName(), TargetsText ), TaskGroup )
end
end
@ -744,6 +753,7 @@ do -- TASK_A2G_DISPATCHER
local TargetSetUnit = self:EvaluateSEAD( DetectedItem ) -- Returns a SetUnit if there are targets to be SEADed...
if TargetSetUnit then
Task = TASK_A2G_SEAD:New( Mission, self.SetGroup, string.format( "SEAD.%03d", DetectedItemID ), TargetSetUnit )
DetectedItem.DesignateMenuName = string.format( "SEAD.%03d", DetectedItemID ) --inject a name for DESIGNATE, if using same DETECTION object
Task:SetDetection( Detection, DetectedItem )
end
@ -752,6 +762,7 @@ do -- TASK_A2G_DISPATCHER
local TargetSetUnit = self:EvaluateCAS( DetectedItem ) -- Returns a SetUnit if there are targets to be CASed...
if TargetSetUnit then
Task = TASK_A2G_CAS:New( Mission, self.SetGroup, string.format( "CAS.%03d", DetectedItemID ), TargetSetUnit )
DetectedItem.DesignateMenuName = string.format( "CAS.%03d", DetectedItemID ) --inject a name for DESIGNATE, if using same DETECTION object
Task:SetDetection( Detection, DetectedItem )
end
@ -760,6 +771,7 @@ do -- TASK_A2G_DISPATCHER
local TargetSetUnit = self:EvaluateBAI( DetectedItem, self.Mission:GetCommandCenter():GetPositionable():GetCoalition() ) -- Returns a SetUnit if there are targets to be BAIed...
if TargetSetUnit then
Task = TASK_A2G_BAI:New( Mission, self.SetGroup, string.format( "BAI.%03d", DetectedItemID ), TargetSetUnit )
DetectedItem.DesignateMenuName = string.format( "BAI.%03d", DetectedItemID ) --inject a name for DESIGNATE, if using same DETECTION object
Task:SetDetection( Detection, DetectedItem )
end
end
@ -805,7 +817,7 @@ do -- TASK_A2G_DISPATCHER
local TaskText = TaskReport:Text(", ")
for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do
if ( not Mission:IsGroupAssigned(TaskGroup) ) and TaskText ~= "" then
if ( not Mission:IsGroupAssigned(TaskGroup) ) and TaskText ~= "" and self.FlashNewTask then
Mission:GetCommandCenter():MessageToGroup( string.format( "%s has tasks %s. Subscribe to a task using the radio menu.", Mission:GetShortText(), TaskText ), TaskGroup )
end
end
@ -815,4 +827,4 @@ do -- TASK_A2G_DISPATCHER
return true
end
end
end

View File

@ -310,4 +310,53 @@ ENUMS.Morse.N7="- - * * *"
ENUMS.Morse.N8="- - - * *"
ENUMS.Morse.N9="- - - - *"
ENUMS.Morse.N0="- - - - -"
ENUMS.Morse[" "]=" "
ENUMS.Morse[" "]=" "
--- ISO (639-1) 2-letter Language Codes. See the [Wikipedia](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes).
--
-- @type ENUMS.ISOLang
ENUMS.ISOLang =
{
Arabic = 'AR',
Chinese = 'ZH',
English = 'EN',
French = 'FR',
German = 'DE',
Russian = 'RU',
Spanish = 'ES',
Japanese = 'JA',
Italian = 'IT',
}
--- Phonetic Alphabet (NATO). See the [Wikipedia](https://en.wikipedia.org/wiki/NATO_phonetic_alphabet).
--
-- @type ENUMS.Phonetic
ENUMS.Phonetic =
{
A = 'Alpha',
B = 'Bravo',
C = 'Charlie',
D = 'Delta',
E = 'Echo',
F = 'Foxtrot',
G = 'Golf',
H = 'Hotel',
I = 'India',
J = 'Juliett',
K = 'Kilo',
L = 'Lima',
M = 'Mike',
N = 'November',
O = 'Oscar',
P = 'Papa',
Q = 'Quebec',
R = 'Romeo',
S = 'Sierra',
T = 'Tango',
U = 'Uniform',
V = 'Victor',
W = 'Whiskey',
X = 'Xray',
Y = 'Yankee',
Z = 'Zulu',
}

View File

@ -0,0 +1,256 @@
--- **Utilities** DCS Simple Text-To-Speech (STTS).
--
--
--
-- @module Utils.STTS
-- @image MOOSE.JPG
--- [DCS Enum world](https://wiki.hoggitworld.com/view/DCS_enum_world)
-- @type STTS
-- @field #string DIRECTORY Path of the SRS directory.
--- Simple Text-To-Speech
--
-- Version 0.4 - Compatible with SRS version 1.9.6.0+
--
-- # DCS Modification Required
--
-- You will need to edit MissionScripting.lua in DCS World/Scripts/MissionScripting.lua and remove the sanitisation.
-- To do this remove all the code below the comment - the line starts "local function sanitizeModule(name)"
-- Do this without DCS running to allow mission scripts to use os functions.
--
-- *You WILL HAVE TO REAPPLY AFTER EVERY DCS UPDATE*
--
-- # USAGE:
--
-- Add this script into the mission as a DO SCRIPT or DO SCRIPT FROM FILE to initialise it
-- Make sure to edit the STTS.SRS_PORT and STTS.DIRECTORY to the correct values before adding to the mission.
-- Then its as simple as calling the correct function in LUA as a DO SCRIPT or in your own scripts.
--
-- Example calls:
--
-- STTS.TextToSpeech("Hello DCS WORLD","251","AM","1.0","SRS",2)
--
-- Arguments in order are:
--
-- * Message to say, make sure not to use a newline (\n) !
-- * Frequency in MHz
-- * Modulation - AM/FM
-- * Volume - 1.0 max, 0.5 half
-- * Name of the transmitter - ATC, RockFM etc
-- * Coalition - 0 spectator, 1 red 2 blue
-- * OPTIONAL - Vec3 Point i.e Unit.getByName("A UNIT"):getPoint() - needs Vec3 for Height! OR null if not needed
-- * OPTIONAL - Speed -10 to +10
-- * OPTIONAL - Gender male, female or neuter
-- * OPTIONAL - Culture - en-US, en-GB etc
-- * OPTIONAL - Voice - a specfic voice by name. Run DCS-SR-ExternalAudio.exe with --help to get the ones you can use on the command line
-- * OPTIONAL - Google TTS - Switch to Google Text To Speech - Requires STTS.GOOGLE_CREDENTIALS path and Google project setup correctly
--
--
-- ## Example
--
-- This example will say the words "Hello DCS WORLD" on 251 MHz AM at maximum volume with a client called SRS and to the Blue coalition only
--
-- STTS.TextToSpeech("Hello DCS WORLD","251","AM","1.0","SRS",2,null,-5,"male","en-GB")
--
-- ## Example
--
--This example will say the words "Hello DCS WORLD" on 251 MHz AM at maximum volume with a client called SRS and to the Blue coalition only centered on the position of the Unit called "A UNIT"
--
-- STTS.TextToSpeech("Hello DCS WORLD","251","AM","1.0","SRS",2,Unit.getByName("A UNIT"):getPoint(),-5,"male","en-GB")
--
-- Arguments in order are:
--
-- * FULL path to the MP3 OR OGG to play
-- * Frequency in MHz - to use multiple separate with a comma - Number of frequencies MUST match number of Modulations
-- * Modulation - AM/FM - to use multiple
-- * Volume - 1.0 max, 0.5 half
-- * Name of the transmitter - ATC, RockFM etc
-- * Coalition - 0 spectator, 1 red 2 blue
--
-- ## Example
--
-- This will play that MP3 on 255MHz AM & 31 FM at half volume with a client called "Multiple" and to Spectators only
--
-- STTS.PlayMP3("C:\\Users\\Ciaran\\Downloads\\PR-Music.mp3","255,31","AM,FM","0.5","Multiple",0)
--
-- @field #STTS
STTS={
ClassName="STTS",
DIRECTORY="",
SRS_PORT=5002,
GOOGLE_CREDENTIALS="C:\\Users\\Ciaran\\Downloads\\googletts.json",
EXECUTABLE="DCS-SR-ExternalAudio.exe",
}
--- FULL Path to the FOLDER containing DCS-SR-ExternalAudio.exe - EDIT TO CORRECT FOLDER
STTS.DIRECTORY = "D:/DCS/_SRS"
--- LOCAL SRS PORT - DEFAULT IS 5002
STTS.SRS_PORT = 5002
--- Google credentials file
STTS.GOOGLE_CREDENTIALS = "C:\\Users\\Ciaran\\Downloads\\googletts.json"
--- DONT CHANGE THIS UNLESS YOU KNOW WHAT YOU'RE DOING
STTS.EXECUTABLE = "DCS-SR-ExternalAudio.exe"
--- Function for UUID.
function STTS.uuid()
local random = math.random
local template ='yxxx-xxxxxxxxxxxx'
return string.gsub(template, '[xy]', function (c)
local v = (c == 'x') and random(0, 0xf) or random(8, 0xb)
return string.format('%x', v)
end)
end
--- Round a number.
-- @param #number x Number.
-- @param #number n Precision.
function STTS.round(x, n)
n = math.pow(10, n or 0)
x = x * n
if x >= 0 then x = math.floor(x + 0.5) else x = math.ceil(x - 0.5) end
return x / n
end
--- Function returns estimated speech time in seconds.
-- Assumptions for time calc: 100 Words per min, avarage of 5 letters for english word so
--
-- * 5 chars * 100wpm = 500 characters per min = 8.3 chars per second
--
-- So lengh of msg / 8.3 = number of seconds needed to read it. rounded down to 8 chars per sec map function:
--
-- * (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
--
function STTS.getSpeechTime(length,speed,isGoogle)
local maxRateRatio = 3
speed = speed or 1.0
isGoogle = isGoogle or false
local speedFactor = 1.0
if isGoogle then
speedFactor = speed
else
if speed ~= 0 then
speedFactor = math.abs(speed) * (maxRateRatio - 1) / 10 + 1
end
if speed < 0 then
speedFactor = 1/speedFactor
end
end
local wpm = math.ceil(100 * speedFactor)
local cps = math.floor((wpm * 5)/60)
if type(length) == "string" then
length = string.len(length)
end
return math.ceil(length/cps)
end
--- Text to speech function.
function STTS.TextToSpeech(message, freqs, modulations, volume, name, coalition, point, speed, gender, culture, voice, googleTTS)
if os == nil or io == nil then
env.info("[DCS-STTS] LUA modules os or io are sanitized. skipping. ")
return
end
speed = speed or 1
gender = gender or "female"
culture = culture or ""
voice = voice or ""
coalition=coalition or "0"
name=name or "ROBOT"
volume=1
speed=1
message = message:gsub("\"","\\\"")
local cmd = string.format("start /min \"\" /d \"%s\" /b \"%s\" -f %s -m %s -c %s -p %s -n \"%s\" -h", STTS.DIRECTORY, STTS.EXECUTABLE, freqs or "305", modulations or "AM", coalition, STTS.SRS_PORT, name)
if voice ~= "" then
cmd = cmd .. string.format(" -V \"%s\"",voice)
else
if culture ~= "" then
cmd = cmd .. string.format(" -l %s",culture)
end
if gender ~= "" then
cmd = cmd .. string.format(" -g %s",gender)
end
end
if googleTTS == true then
cmd = cmd .. string.format(" -G \"%s\"",STTS.GOOGLE_CREDENTIALS)
end
if speed ~= 1 then
cmd = cmd .. string.format(" -s %s",speed)
end
if volume ~= 1.0 then
cmd = cmd .. string.format(" -v %s",volume)
end
if point and type(point) == "table" and point.x then
local lat, lon, alt = coord.LOtoLL(point)
lat = STTS.round(lat,4)
lon = STTS.round(lon,4)
alt = math.floor(alt)
cmd = cmd .. string.format(" -L %s -O %s -A %s",lat,lon,alt)
end
cmd = cmd ..string.format(" -t \"%s\"",message)
if string.len(cmd) > 255 then
local filename = os.getenv('TMP') .. "\\DCS_STTS-" .. STTS.uuid() .. ".bat"
local script = io.open(filename,"w+")
script:write(cmd .. " && exit" )
script:close()
cmd = string.format("\"%s\"",filename)
timer.scheduleFunction(os.remove, filename, timer.getTime() + 1)
end
if string.len(cmd) > 255 then
env.info("[DCS-STTS] - cmd string too long")
env.info("[DCS-STTS] TextToSpeech Command :\n" .. cmd.."\n")
end
os.execute(cmd)
return STTS.getSpeechTime(message,speed,googleTTS)
end
--- Play mp3 function.
-- @param #string pathToMP3 Path to the sound file.
-- @param #string freqs Frequencies, e.g. "305, 256".
-- @param #string modulations Modulations, e.g. "AM, FM".
-- @param #string volume Volume, e.g. "0.5".
function STTS.PlayMP3(pathToMP3, freqs, modulations, volume, name, coalition, point)
local cmd = string.format("start \"\" /d \"%s\" /b /min \"%s\" -i \"%s\" -f %s -m %s -c %s -p %s -n \"%s\" -v %s -h",
STTS.DIRECTORY, STTS.EXECUTABLE, pathToMP3, freqs or "305", modulations or "AM", coalition or "0", STTS.SRS_PORT, name or "ROBOT", volume or "1")
if point and type(point) == "table" and point.x then
local lat, lon, alt = coord.LOtoLL(point)
lat = STTS.round(lat,4)
lon = STTS.round(lon,4)
alt = math.floor(alt)
cmd = cmd .. string.format(" -L %s -O %s -A %s",lat,lon,alt)
end
env.info("[DCS-STTS] MP3/OGG Command :\n" .. cmd.."\n")
os.execute(cmd)
end

View File

@ -0,0 +1,612 @@
--- **Utils** Templates
--
-- DCS unit templates
--
-- @module Utilities.Templates
-- @image MOOSE.JPG
--- TEMPLATE class.
-- @type TEMPLATE
-- @field #string ClassName Name of the class.
--- *Templates*
--
-- ===
--
-- ![Banner Image](..\Presentations\Utilities\PROFILER_Main.jpg)
--
-- Get DCS templates from thin air.
--
-- # Ground Units
--
-- Ground units.
--
-- # Naval Units
--
-- Ships are not implemented yet.
--
-- # Aircraft
--
-- ## Airplanes
--
-- Airplanes are not implemented yet.
--
-- ## Helicopters
--
-- Helicopters are not implemented yet.
--
-- @field #TEMPLATE
TEMPLATE = {
ClassName = "TEMPLATE",
Ground = {},
Naval = {},
Airplane = {},
Helicopter = {},
}
--- Ground unit type names.
-- @type TEMPLATE.TypeGround
-- @param #string InfantryAK
TEMPLATE.TypeGround={
InfantryAK="Infantry AK",
ParatrooperAKS74="Paratrooper AKS-74",
ParatrooperRPG16="Paratrooper RPG-16",
SoldierWWIIUS="soldier_wwii_us",
InfantryM248="Infantry M249",
SoldierM4="Soldier M4",
}
--- Naval unit type names.
-- @type TEMPLATE.TypeNaval
-- @param #string Ticonderoga
TEMPLATE.TypeNaval={
Ticonderoga="TICONDEROG",
}
--- Rotary wing unit type names.
-- @type TEMPLATE.TypeAirplane
-- @param #string A10C
TEMPLATE.TypeAirplane={
A10C="A-10C",
}
--- Rotary wing unit type names.
-- @type TEMPLATE.TypeHelicopter
-- @param #string AH1W
TEMPLATE.TypeHelicopter={
AH1W="AH-1W",
}
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Ground Template
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Get template for ground units.
-- @param #string TypeName Type name of the unit(s) in the groups. See `TEMPLATE.Ground`.
-- @param #string GroupName Name of the spawned group. **Must be unique!**
-- @param #number CountryID Country ID. Default `country.id.USA`. Coalition is automatically determined by the one the country belongs to.
-- @param DCS#Vec3 Vec3 Position of the group and the first unit.
-- @param #number Nunits Number of units. Default 1.
-- @param #number Radius Spawn radius for additonal units in meters. Default 50 m.
-- @return #table Template Template table.
function TEMPLATE.GetGround(TypeName, GroupName, CountryID, Vec3, Nunits, Radius)
-- Defaults.
TypeName=TypeName or TEMPLATE.TypeGround.SoldierM4
GroupName=GroupName or "Ground-1"
CountryID=CountryID or country.id.USA
Vec3=Vec3 or {x=0, y=0, z=0}
Nunits=Nunits or 1
Radius=Radius or 50
-- Get generic template.
local template=UTILS.DeepCopy(TEMPLATE.GenericGround)
-- Set group name.
template.name=GroupName
-- These are additional entries required by the MOOSE _DATABASE:Spawn() function.
template.CountryID=CountryID
template.CoalitionID=coalition.getCountryCoalition(template.CountryID)
template.CategoryID=Unit.Category.GROUND_UNIT
-- Set first unit.
template.units[1].type=TypeName
template.units[1].name=GroupName.."-1"
if Vec3 then
TEMPLATE.SetPositionFromVec3(template, Vec3)
end
TEMPLATE.SetUnits(template, Nunits, COORDINATE:NewFromVec3(Vec3), Radius)
return template
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Naval Template
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Get template for ground units.
-- @param #string TypeName Type name of the unit(s) in the groups. See `TEMPLATE.Ground`.
-- @param #string GroupName Name of the spawned group. **Must be unique!**
-- @param #number CountryID Country ID. Default `country.id.USA`. Coalition is automatically determined by the one the country belongs to.
-- @param DCS#Vec3 Vec3 Position of the group and the first unit.
-- @param #number Nunits Number of units. Default 1.
-- @param #number Radius Spawn radius for additonal units in meters. Default 500 m.
-- @return #table Template Template table.
function TEMPLATE.GetNaval(TypeName, GroupName, CountryID, Vec3, Nunits, Radius)
-- Defaults.
TypeName=TypeName or TEMPLATE.TypeNaval.Ticonderoga
GroupName=GroupName or "Naval-1"
CountryID=CountryID or country.id.USA
Vec3=Vec3 or {x=0, y=0, z=0}
Nunits=Nunits or 1
Radius=Radius or 500
-- Get generic template.
local template=UTILS.DeepCopy(TEMPLATE.GenericNaval)
-- Set group name.
template.name=GroupName
-- These are additional entries required by the MOOSE _DATABASE:Spawn() function.
template.CountryID=CountryID
template.CoalitionID=coalition.getCountryCoalition(template.CountryID)
template.CategoryID=Unit.Category.SHIP
-- Set first unit.
template.units[1].type=TypeName
template.units[1].name=GroupName.."-1"
if Vec3 then
TEMPLATE.SetPositionFromVec3(template, Vec3)
end
TEMPLATE.SetUnits(template, Nunits, COORDINATE:NewFromVec3(Vec3), Radius)
return template
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Aircraft Template
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Get template for fixed wing units.
-- @param #string TypeName Type name of the unit(s) in the groups. See `TEMPLATE.Ground`.
-- @param #string GroupName Name of the spawned group. **Must be unique!**
-- @param #number CountryID Country ID. Default `country.id.USA`. Coalition is automatically determined by the one the country belongs to.
-- @param DCS#Vec3 Vec3 Position of the group and the first unit.
-- @param #number Nunits Number of units. Default 1.
-- @param #number Radius Spawn radius for additonal units in meters. Default 500 m.
-- @return #table Template Template table.
function TEMPLATE.GetAirplane(TypeName, GroupName, CountryID, Vec3, Nunits, Radius)
-- Defaults.
TypeName=TypeName or TEMPLATE.TypeAirplane.A10C
GroupName=GroupName or "Airplane-1"
CountryID=CountryID or country.id.USA
Vec3=Vec3 or {x=0, y=1000, z=0}
Nunits=Nunits or 1
Radius=Radius or 100
local template=TEMPLATE._GetAircraft(true, TypeName, GroupName, CountryID, Vec3, Nunits, Radius)
return template
end
--- Get template for fixed wing units.
-- @param #string TypeName Type name of the unit(s) in the groups. See `TEMPLATE.Ground`.
-- @param #string GroupName Name of the spawned group. **Must be unique!**
-- @param #number CountryID Country ID. Default `country.id.USA`. Coalition is automatically determined by the one the country belongs to.
-- @param DCS#Vec3 Vec3 Position of the group and the first unit.
-- @param #number Nunits Number of units. Default 1.
-- @param #number Radius Spawn radius for additonal units in meters. Default 500 m.
-- @return #table Template Template table.
function TEMPLATE.GetHelicopter(TypeName, GroupName, CountryID, Vec3, Nunits, Radius)
-- Defaults.
TypeName=TypeName or TEMPLATE.TypeHelicopter.AH1W
GroupName=GroupName or "Helicopter-1"
CountryID=CountryID or country.id.USA
Vec3=Vec3 or {x=0, y=500, z=0}
Nunits=Nunits or 1
Radius=Radius or 100
-- Limit unis to 4.
Nunits=math.min(Nunits, 4)
local template=TEMPLATE._GetAircraft(false, TypeName, GroupName, CountryID, Vec3, Nunits, Radius)
return template
end
--- Get template for aircraft units.
-- @param #boolean Airplane If true, this is a fixed wing. Else, rotary wing.
-- @param #string TypeName Type name of the unit(s) in the groups. See `TEMPLATE.Ground`.
-- @param #string GroupName Name of the spawned group. **Must be unique!**
-- @param #number CountryID Country ID. Default `country.id.USA`. Coalition is automatically determined by the one the country belongs to.
-- @param DCS#Vec3 Vec3 Position of the group and the first unit.
-- @param #number Nunits Number of units. Default 1.
-- @param #number Radius Spawn radius for additonal units in meters. Default 500 m.
-- @return #table Template Template table.
function TEMPLATE._GetAircraft(Airplane, TypeName, GroupName, CountryID, Vec3, Nunits, Radius)
-- Defaults.
TypeName=TypeName
GroupName=GroupName or "Aircraft-1"
CountryID=CountryID or country.id.USA
Vec3=Vec3 or {x=0, y=0, z=0}
Nunits=Nunits or 1
Radius=Radius or 100
-- Get generic template.
local template=UTILS.DeepCopy(TEMPLATE.GenericAircraft)
-- Set group name.
template.name=GroupName
-- These are additional entries required by the MOOSE _DATABASE:Spawn() function.
template.CountryID=CountryID
template.CoalitionID=coalition.getCountryCoalition(template.CountryID)
if Airplane then
template.CategoryID=Unit.Category.AIRPLANE
else
template.CategoryID=Unit.Category.HELICOPTER
end
-- Set first unit.
template.units[1].type=TypeName
template.units[1].name=GroupName.."-1"
-- Set position.
if Vec3 then
TEMPLATE.SetPositionFromVec3(template, Vec3)
end
-- Set number of units.
TEMPLATE.SetUnits(template, Nunits, COORDINATE:NewFromVec3(Vec3), Radius)
return template
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Misc Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Set the position of the template.
-- @param #table Template The template to be modified.
-- @param DCS#Vec2 Vec2 2D Position vector with x and y components of the group.
function TEMPLATE.SetPositionFromVec2(Template, Vec2)
Template.x=Vec2.x
Template.y=Vec2.y
for _,unit in pairs(Template.units) do
unit.x=Vec2.x
unit.y=Vec2.y
end
Template.route.points[1].x=Vec2.x
Template.route.points[1].y=Vec2.y
Template.route.points[1].alt=0 --TODO: Use land height.
end
--- Set the position of the template.
-- @param #table Template The template to be modified.
-- @param DCS#Vec3 Vec3 Position vector of the group.
function TEMPLATE.SetPositionFromVec3(Template, Vec3)
local Vec2={x=Vec3.x, y=Vec3.z}
TEMPLATE.SetPositionFromVec2(Template, Vec2)
end
--- Set the position of the template.
-- @param #table Template The template to be modified.
-- @param #number N Total number of units in the group.
-- @param Core.Point#COORDINATE Coordinate Position of the first unit.
-- @param #number Radius Radius in meters to randomly place the additional units.
function TEMPLATE.SetUnits(Template, N, Coordinate, Radius)
local units=Template.units
local unit1=units[1]
local Vec3=Coordinate:GetVec3()
unit1.x=Vec3.x
unit1.y=Vec3.z
unit1.alt=Vec3.y
for i=2,N do
units[i]=UTILS.DeepCopy(unit1)
end
for i=1,N do
local unit=units[i]
unit.name=string.format("%s-%d", Template.name, i)
if i>1 then
local vec2=Coordinate:GetRandomCoordinateInRadius(Radius, 5):GetVec2()
unit.x=vec2.x
unit.y=vec2.y
unit.alt=unit1.alt
end
end
end
--- Set the position of the template.
-- @param #table Template The template to be modified.
-- @param Wrapper.Airbase#AIRBASE AirBase The airbase where the aircraft are spawned.
-- @param #table ParkingSpots List of parking spot IDs. Every unit needs one!
-- @param #boolean EngineOn If true, aircraft are spawned hot.
function TEMPLATE.SetAirbase(Template, AirBase, ParkingSpots, EngineOn)
-- Airbase ID.
local AirbaseID=AirBase:GetID()
-- Spawn point.
local point=Template.route.points[1]
-- Set ID.
if AirBase:IsAirdrome() then
point.airdromeId=AirbaseID
else
point.helipadId=AirbaseID
point.linkUnit=AirbaseID
end
if EngineOn then
point.action=COORDINATE.WaypointAction.FromParkingAreaHot
point.type=COORDINATE.WaypointType.TakeOffParkingHot
else
point.action=COORDINATE.WaypointAction.FromParkingArea
point.type=COORDINATE.WaypointType.TakeOffParking
end
for i,unit in ipairs(Template.units) do
unit.parking_id=ParkingSpots[i]
end
end
--- Add a waypoint.
-- @param #table Template The template to be modified.
-- @param #table Waypoint Waypoint table.
function TEMPLATE.AddWaypoint(Template, Waypoint)
table.insert(Template.route.points, Waypoint)
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Generic Ground Template
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
TEMPLATE.GenericGround=
{
["visible"] = false,
["tasks"] = {}, -- end of ["tasks"]
["uncontrollable"] = false,
["task"] = "Ground Nothing",
["route"] =
{
["spans"] = {}, -- end of ["spans"]
["points"] =
{
[1] =
{
["alt"] = 0,
["type"] = "Turning Point",
["ETA"] = 0,
["alt_type"] = "BARO",
["formation_template"] = "",
["y"] = 0,
["x"] = 0,
["ETA_locked"] = true,
["speed"] = 0,
["action"] = "Off Road",
["task"] =
{
["id"] = "ComboTask",
["params"] =
{
["tasks"] =
{
}, -- end of ["tasks"]
}, -- end of ["params"]
}, -- end of ["task"]
["speed_locked"] = true,
}, -- end of [1]
}, -- end of ["points"]
}, -- end of ["route"]
["groupId"] = nil,
["hidden"] = false,
["units"] =
{
[1] =
{
["transportable"] =
{
["randomTransportable"] = false,
}, -- end of ["transportable"]
["skill"] = "Average",
["type"] = "Infantry AK",
["unitId"] = nil,
["y"] = 0,
["x"] = 0,
["name"] = "Infantry AK-47 Rus",
["heading"] = 0,
["playerCanDrive"] = false,
}, -- end of [1]
}, -- end of ["units"]
["y"] = 0,
["x"] = 0,
["name"] = "Infantry AK-47 Rus",
["start_time"] = 0,
}
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Generic Ship Template
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
TEMPLATE.GenericNaval=
{
["visible"] = false,
["tasks"] = {}, -- end of ["tasks"]
["uncontrollable"] = false,
["route"] =
{
["points"] =
{
[1] =
{
["alt"] = 0,
["type"] = "Turning Point",
["ETA"] = 0,
["alt_type"] = "BARO",
["formation_template"] = "",
["y"] = 0,
["x"] = 0,
["ETA_locked"] = true,
["speed"] = 0,
["action"] = "Turning Point",
["task"] =
{
["id"] = "ComboTask",
["params"] =
{
["tasks"] =
{
}, -- end of ["tasks"]
}, -- end of ["params"]
}, -- end of ["task"]
["speed_locked"] = true,
}, -- end of [1]
}, -- end of ["points"]
}, -- end of ["route"]
["groupId"] = nil,
["hidden"] = false,
["units"] =
{
[1] =
{
["transportable"] =
{
["randomTransportable"] = false,
}, -- end of ["transportable"]
["skill"] = "Average",
["type"] = "TICONDEROG",
["unitId"] = nil,
["y"] = 0,
["x"] = 0,
["name"] = "Naval-1-1",
["heading"] = 0,
["modulation"] = 0,
["frequency"] = 127500000,
}, -- end of [1]
}, -- end of ["units"]
["y"] = 0,
["x"] = 0,
["name"] = "Naval-1",
["start_time"] = 0,
}
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Generic Aircraft Template
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
TEMPLATE.GenericAircraft=
{
["groupId"] = nil,
["name"] = "Rotary-1",
["uncontrolled"] = false,
["hidden"] = false,
["task"] = "Nothing",
["y"] = 0,
["x"] = 0,
["start_time"] = 0,
["communication"] = true,
["radioSet"] = false,
["frequency"] = 127.5,
["modulation"] = 0,
["taskSelected"] = true,
["tasks"] = {}, -- end of ["tasks"]
["route"] =
{
["points"] =
{
[1] =
{
["y"] = 0,
["x"] = 0,
["alt"] = 1000,
["alt_type"] = "BARO",
["action"] = "Turning Point",
["type"] = "Turning Point",
["airdromeId"] = nil,
["task"] =
{
["id"] = "ComboTask",
["params"] =
{
["tasks"] = {}, -- end of ["tasks"]
}, -- end of ["params"]
}, -- end of ["task"]
["ETA"] = 0,
["ETA_locked"] = true,
["speed"] = 100,
["speed_locked"] = true,
["formation_template"] = "",
}, -- end of [1]
}, -- end of ["points"]
}, -- end of ["route"]
["units"] =
{
[1] =
{
["name"] = "Rotary-1-1",
["unitId"] = nil,
["type"] = "AH-1W",
["onboard_num"] = "050",
["livery_id"] = "USA X Black",
["skill"] = "High",
["ropeLength"] = 15,
["speed"] = 0,
["x"] = 0,
["y"] = 0,
["alt"] = 10,
["alt_type"] = "BARO",
["heading"] = 0,
["psi"] = 0,
["parking"] = nil,
["parking_id"] = nil,
["payload"] =
{
["pylons"] = {}, -- end of ["pylons"]
["fuel"] = "1250.0",
["flare"] = 30,
["chaff"] = 30,
["gun"] = 100,
}, -- end of ["payload"]
["callsign"] =
{
[1] = 2,
[2] = 1,
[3] = 1,
["name"] = "Springfield11",
}, -- end of ["callsign"]
}, -- end of [1]
}, -- end of ["units"]
}
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

File diff suppressed because it is too large Load Diff

View File

@ -26,6 +26,8 @@
-- @field #table parking Parking spot data.
-- @field #table parkingByID Parking spot data table with ID as key.
-- @field #number activerwyno Active runway number (forced).
-- @field #table parkingWhitelist List of parking spot terminal IDs considered for spawning.
-- @field #table parkingBlacklist List of parking spot terminal IDs **not** considered for spawning.
-- @extends Wrapper.Positionable#POSITIONABLE
--- Wrapper class to handle the DCS Airbase objects:
@ -72,7 +74,7 @@ AIRBASE = {
--- Enumeration to identify the airbases in the Caucasus region.
--
-- These are all airbases of Caucasus:
-- Airbases of the Caucasus map:
--
-- * AIRBASE.Caucasus.Gelendzhik
-- * AIRBASE.Caucasus.Krasnodar_Pashkovsky
@ -121,7 +123,7 @@ AIRBASE.Caucasus = {
["Beslan"] = "Beslan",
}
--- These are all airbases of Nevada:
--- Airbases of the Nevada map:
--
-- * AIRBASE.Nevada.Creech_AFB
-- * AIRBASE.Nevada.Groom_Lake_AFB
@ -140,6 +142,7 @@ AIRBASE.Caucasus = {
-- * AIRBASE.Nevada.Pahute_Mesa_Airstrip
-- * AIRBASE.Nevada.Tonopah_Airport
-- * AIRBASE.Nevada.Tonopah_Test_Range_Airfield
--
-- @field Nevada
AIRBASE.Nevada = {
["Creech_AFB"] = "Creech AFB",
@ -161,7 +164,7 @@ AIRBASE.Nevada = {
["Tonopah_Test_Range_Airfield"] = "Tonopah Test Range Airfield",
}
--- These are all airbases of Normandy:
--- Airbases of the Normandy map:
--
-- * AIRBASE.Normandy.Saint_Pierre_du_Mont
-- * AIRBASE.Normandy.Lignerolles
@ -194,6 +197,7 @@ AIRBASE.Nevada = {
-- * AIRBASE.Normandy.Funtington
-- * AIRBASE.Normandy.Tangmere
-- * AIRBASE.Normandy.Ford_AF
--
-- @field Normandy
AIRBASE.Normandy = {
["Saint_Pierre_du_Mont"] = "Saint Pierre du Mont",
@ -236,7 +240,7 @@ AIRBASE.Normandy = {
["Conches"] = "Conches",
}
--- These are all airbases of the Persion Gulf Map:
--- Airbases of the Persion Gulf Map:
--
-- * AIRBASE.PersianGulf.Abu_Dhabi_International_Airport
-- * AIRBASE.PersianGulf.Abu_Musa_Island_Airport
@ -267,40 +271,41 @@ AIRBASE.Normandy = {
-- * AIRBASE.PersianGulf.Sirri_Island
-- * AIRBASE.PersianGulf.Tunb_Island_AFB
-- * AIRBASE.PersianGulf.Tunb_Kochak
--
-- @field PersianGulf
AIRBASE.PersianGulf = {
["Abu_Dhabi_International_Airport"] = "Abu Dhabi International Airport",
["Abu_Musa_Island_Airport"] = "Abu Musa Island Airport",
["Al_Ain_International_Airport"] = "Al Ain International Airport",
["Al_Bateen_Airport"] = "Al-Bateen Airport",
["Al_Dhafra_AB"] = "Al Dhafra AB",
["Abu_Dhabi_International_Airport"] = "Abu Dhabi Intl",
["Abu_Musa_Island_Airport"] = "Abu Musa Island",
["Al_Ain_International_Airport"] = "Al Ain Intl",
["Al_Bateen_Airport"] = "Al-Bateen",
["Al_Dhafra_AB"] = "Al Dhafra AFB",
["Al_Maktoum_Intl"] = "Al Maktoum Intl",
["Al_Minhad_AB"] = "Al Minhad AB",
["Al_Minhad_AB"] = "Al Minhad AFB",
["Bandar_Abbas_Intl"] = "Bandar Abbas Intl",
["Bandar_Lengeh"] = "Bandar Lengeh",
["Bandar_e_Jask_airfield"] = "Bandar-e-Jask airfield",
["Bandar_e_Jask_airfield"] = "Bandar-e-Jask",
["Dubai_Intl"] = "Dubai Intl",
["Fujairah_Intl"] = "Fujairah Intl",
["Havadarya"] = "Havadarya",
["Jiroft_Airport"] = "Jiroft Airport",
["Kerman_Airport"] = "Kerman Airport",
["Jiroft_Airport"] = "Jiroft",
["Kerman_Airport"] = "Kerman",
["Khasab"] = "Khasab",
["Kish_International_Airport"] = "Kish International Airport",
["Lar_Airbase"] = "Lar Airbase",
["Lavan_Island_Airport"] = "Lavan Island Airport",
["Liwa_Airbase"] = "Liwa Airbase",
["Kish_International_Airport"] = "Kish Intl",
["Lar_Airbase"] = "Lar",
["Lavan_Island_Airport"] = "Lavan Island",
["Liwa_Airbase"] = "Liwa AFB",
["Qeshm_Island"] = "Qeshm Island",
["Ras_Al_Khaimah"] = "Ras Al Khaimah",
["Sas_Al_Nakheel_Airport"] = "Sas Al Nakheel Airport",
["Ras_Al_Khaimah"] = "Ras Al Khaimah Intl",
["Sas_Al_Nakheel_Airport"] = "Sas Al Nakheel",
["Sharjah_Intl"] = "Sharjah Intl",
["Shiraz_International_Airport"] = "Shiraz International Airport",
["Shiraz_International_Airport"] = "Shiraz Intl",
["Sir_Abu_Nuayr"] = "Sir Abu Nuayr",
["Sirri_Island"] = "Sirri Island",
["Tunb_Island_AFB"] = "Tunb Island AFB",
["Tunb_Kochak"] = "Tunb Kochak",
}
--- These are all airbases of the The Channel Map:
--- Airbases of The Channel Map:
--
-- * AIRBASE.TheChannel.Abbeville_Drucat
-- * AIRBASE.TheChannel.Merville_Calonne
@ -325,7 +330,7 @@ AIRBASE.TheChannel = {
["High_Halden"] = "High Halden",
}
--- Airbases of Syria
--- Airbases of the Syria map:
--
-- * AIRBASE.Syria.Kuweires
-- * AIRBASE.Syria.Marj_Ruhayyil
@ -335,33 +340,53 @@ AIRBASE.TheChannel = {
-- * AIRBASE.Syria.Incirlik
-- * AIRBASE.Syria.Damascus
-- * AIRBASE.Syria.Bassel_Al_Assad
-- * AIRBASE.Syria.Rosh_Pina
-- * AIRBASE.Syria.Aleppo
-- * AIRBASE.Syria.Qabr_as_Sitt
-- * AIRBASE.Syria.Al_Qusayr
-- * AIRBASE.Syria.Wujah_Al_Hajar
-- * AIRBASE.Syria.Al_Dumayr
-- * AIRBASE.Syria.Gazipasa
-- * AIRBASE.Syria.Ru_Convoy_4
-- * AIRBASE.Syria.Hatay
-- * AIRBASE.Syria.Nicosia
-- * AIRBASE.Syria.Pinarbashi
-- * AIRBASE.Syria.Paphos
-- * AIRBASE.Syria.Kingsfield
-- * AIRBASE.Syria.Thalah
-- * AIRBASE.Syria.Haifa
-- * AIRBASE.Syria.Khalkhalah
-- * AIRBASE.Syria.Megiddo
-- * AIRBASE.Syria.Lakatamia
-- * AIRBASE.Syria.Rayak
-- * AIRBASE.Syria.Larnaca
-- * AIRBASE.Syria.Mezzeh
-- * AIRBASE.Syria.King_Hussein_Air_College
-- * AIRBASE.Syria.Jirah
-- * AIRBASE.Syria.Gecitkale
-- * AIRBASE.Syria.Akrotiri
-- * AIRBASE.Syria.Naqoura
-- * AIRBASE.Syria.Gaziantep
-- * AIRBASE.Syria.CVN_71
-- * AIRBASE.Syria.Sayqal
-- * AIRBASE.Syria.Tiyas
-- * AIRBASE.Syria.Shayrat
-- * AIRBASE.Syria.Taftanaz
-- * AIRBASE.Syria.H4
-- * AIRBASE.Syria.King_Hussein_Air_College
-- * AIRBASE.Syria.Rene_Mouawad
-- * AIRBASE.Syria.Jirah
-- * AIRBASE.Syria.Ramat_David
-- * AIRBASE.Syria.Qabr_as_Sitt
-- * AIRBASE.Syria.Minakh
-- * AIRBASE.Syria.Adana_Sakirpasa
-- * AIRBASE.Syria.Marj_as_Sultan_South
-- * AIRBASE.Syria.Hama
-- * AIRBASE.Syria.Al_Qusayr
-- * AIRBASE.Syria.Palmyra
-- * AIRBASE.Syria.Hama
-- * AIRBASE.Syria.Ercan
-- * AIRBASE.Syria.Marj_as_Sultan_South
-- * AIRBASE.Syria.Tabqa
-- * AIRBASE.Syria.Beirut_Rafic_Hariri
-- * AIRBASE.Syria.An_Nasiriyah
-- * AIRBASE.Syria.Abu_al_Duhur
--
-- @field Syria
--@field Syria
AIRBASE.Syria={
["Kuweires"]="Kuweires",
["Marj_Ruhayyil"]="Marj Ruhayyil",
@ -371,27 +396,46 @@ AIRBASE.Syria={
["Incirlik"]="Incirlik",
["Damascus"]="Damascus",
["Bassel_Al_Assad"]="Bassel Al-Assad",
["Rosh_Pina"]="Rosh Pina",
["Aleppo"]="Aleppo",
["Qabr_as_Sitt"]="Qabr as Sitt",
["Al_Qusayr"]="Al Qusayr",
["Wujah_Al_Hajar"]="Wujah Al Hajar",
["Al_Dumayr"]="Al-Dumayr",
["Gazipasa"]="Gazipasa",
["Ru_Convoy_4"]="Ru Convoy-4",
["Hatay"]="Hatay",
["Nicosia"]="Nicosia",
["Pinarbashi"]="Pinarbashi",
["Paphos"]="Paphos",
["Kingsfield"]="Kingsfield",
["Thalah"]="Tha'lah",
["Haifa"]="Haifa",
["Khalkhalah"]="Khalkhalah",
["Megiddo"]="Megiddo",
["Lakatamia"]="Lakatamia",
["Rayak"]="Rayak",
["Larnaca"]="Larnaca",
["Mezzeh"]="Mezzeh",
["King_Hussein_Air_College"]="King Hussein Air College",
["Jirah"]="Jirah",
["Gecitkale"]="Gecitkale",
["Akrotiri"]="Akrotiri",
["Naqoura"]="Naqoura",
["Gaziantep"]="Gaziantep",
["Sayqal"]="Sayqal",
["Tiyas"]="Tiyas",
["Shayrat"]="Shayrat",
["Taftanaz"]="Taftanaz",
["H4"]="H4",
["King_Hussein_Air_College"]="King Hussein Air College",
["Rene_Mouawad"]="Rene Mouawad",
["Jirah"]="Jirah",
["Ramat_David"]="Ramat David",
["Qabr_as_Sitt"]="Qabr as Sitt",
["Minakh"]="Minakh",
["Adana_Sakirpasa"]="Adana Sakirpasa",
["Marj_as_Sultan_South"]="Marj as Sultan South",
["Hama"]="Hama",
["Al_Qusayr"]="Al Qusayr",
["Palmyra"]="Palmyra",
["Hama"]="Hama",
["Ercan"]="Ercan",
["Marj_as_Sultan_South"]="Marj as Sultan South",
["Tabqa"]="Tabqa",
["Beirut_Rafic_Hariri"]="Beirut-Rafic Hariri",
["An_Nasiriyah"]="An Nasiriyah",
@ -399,6 +443,27 @@ AIRBASE.Syria={
}
--- Airbases of the Mariana Islands map:
--
-- * AIRBASE.MarianaIslands.Rota_Intl
-- * AIRBASE.MarianaIslands.Andersen_AFB
-- * AIRBASE.MarianaIslands.Antonio_B_Won_Pat_Intl
-- * AIRBASE.MarianaIslands.Saipan_Intl
-- * AIRBASE.MarianaIslands.Tinian_Intl
-- * AIRBASE.MarianaIslands.Olf_Orote
--
--@field MarianaIslands
AIRBASE.MarianaIslands={
["Rota_Intl"]="Rota Intl",
["Andersen_AFB"]="Andersen AFB",
["Antonio_B_Won_Pat_Intl"]="Antonio B. Won Pat Intl",
["Saipan_Intl"]="Saipan Intl",
["Tinian_Intl"]="Tinian Intl",
["Olf_Orote"]="Olf Orote",
}
--- AIRBASE.ParkingSpot ".Coordinate, ".TerminalID", ".TerminalType", ".TOAC", ".Free", ".TerminalID0", ".DistToRwy".
-- @type AIRBASE.ParkingSpot
-- @field Core.Point#COORDINATE Coordinate Coordinate of the parking spot.
@ -471,6 +536,9 @@ function AIRBASE:Register(AirbaseName)
-- Get descriptors.
self.descriptors=self:GetDesc()
-- Debug info.
--self:I({airbase=AirbaseName, descriptors=self.descriptors})
-- Category.
self.category=self.descriptors and self.descriptors.category or Airbase.Category.AIRDROME
@ -482,12 +550,21 @@ function AIRBASE:Register(AirbaseName)
self.isHelipad=true
elseif self.category==Airbase.Category.SHIP then
self.isShip=true
-- DCS bug: Oil rigs and gas platforms have category=2 (ship). Also they cannot be retrieved by coalition.getStaticObjects()
if self.descriptors.typeName=="Oil rig" or self.descriptors.typeName=="Ga" then
self.isHelipad=true
self.isShip=false
self.category=Airbase.Category.HELIPAD
_DATABASE:AddStatic(AirbaseName)
end
else
self:E("ERROR: Unknown airbase category!")
end
-- Init parking spots.
self:_InitParkingSpots()
-- Get 2D position vector.
local vec2=self:GetVec2()
-- Init coordinate.
@ -597,7 +674,7 @@ end
--- Get all airbase names of the current map. This includes ships and FARPS.
-- @param DCS#Coalition coalition (Optional) Return only airbases belonging to the specified coalition. By default, all airbases of the map are returned.
-- @param #number category (Optional) Return only airbases of a certain category, e.g. Airbase.Category.FARP
-- @param #number category (Optional) Return only airbases of a certain category, e.g. `Airbase.Category.HELIPAD`.
-- @return #table Table containing all airbase names of the current map.
function AIRBASE.GetAllAirbaseNames(coalition, category)
@ -652,6 +729,54 @@ function AIRBASE:GetID(unique)
return nil
end
--- Set parking spot whitelist. Only these spots will be considered for spawning.
-- Black listed spots overrule white listed spots.
-- **NOTE** that terminal IDs are not necessarily the same as those displayed in the mission editor!
-- @param #AIRBASE self
-- @param #table TerminalIdBlacklist Table of white listed terminal IDs.
-- @return #AIRBASE self
-- @usage AIRBASE:FindByName("Batumi"):SetParkingSpotWhitelist({2, 3, 4}) --Only allow terminal IDs 2, 3, 4
function AIRBASE:SetParkingSpotWhitelist(TerminalIdWhitelist)
if TerminalIdWhitelist==nil then
self.parkingWhitelist={}
return self
end
-- Ensure we got a table.
if type(TerminalIdWhitelist)~="table" then
TerminalIdWhitelist={TerminalIdWhitelist}
end
self.parkingWhitelist=TerminalIdWhitelist
return self
end
--- Set parking spot blacklist. These parking spots will *not* be used for spawning.
-- Black listed spots overrule white listed spots.
-- **NOTE** that terminal IDs are not necessarily the same as those displayed in the mission editor!
-- @param #AIRBASE self
-- @param #table TerminalIdBlacklist Table of black listed terminal IDs.
-- @return #AIRBASE self
-- @usage AIRBASE:FindByName("Batumi"):SetParkingSpotBlacklist({2, 3, 4}) --Forbit terminal IDs 2, 3, 4
function AIRBASE:SetParkingSpotBlacklist(TerminalIdBlacklist)
if TerminalIdBlacklist==nil then
self.parkingBlacklist={}
return self
end
-- Ensure we got a table.
if type(TerminalIdBlacklist)~="table" then
TerminalIdBlacklist={TerminalIdBlacklist}
end
self.parkingBlacklist=TerminalIdBlacklist
return self
end
--- Get category of airbase.
-- @param #AIRBASE self
@ -836,25 +961,27 @@ function AIRBASE:_InitParkingSpots()
-- Put coordinates of parking spots into table.
for _,spot in pairs(parkingdata) do
-- New parking spot.
local park={} --#AIRBASE.ParkingSpot
park.Vec3=spot.vTerminalPos
park.Coordinate=COORDINATE:NewFromVec3(spot.vTerminalPos)
park.DistToRwy=spot.fDistToRW
park.Free=nil
park.TerminalID=spot.Term_Index
park.TerminalID0=spot.Term_Index_0
park.TerminalType=spot.Term_Type
park.TOAC=spot.TO_AC
-- New parking spot.
local park={} --#AIRBASE.ParkingSpot
park.Vec3=spot.vTerminalPos
park.Coordinate=COORDINATE:NewFromVec3(spot.vTerminalPos)
park.DistToRwy=spot.fDistToRW
park.Free=nil
park.TerminalID=spot.Term_Index
park.TerminalID0=spot.Term_Index_0
park.TerminalType=spot.Term_Type
park.TOAC=spot.TO_AC
for _,terminalType in pairs(AIRBASE.TerminalType) do
if self._CheckTerminalType(terminalType, park.TerminalType) then
self.NparkingTerminal[terminalType]=self.NparkingTerminal[terminalType]+1
end
self.NparkingTotal=self.NparkingTotal+1
for _,terminalType in pairs(AIRBASE.TerminalType) do
if self._CheckTerminalType(terminalType, park.TerminalType) then
self.NparkingTerminal[terminalType]=self.NparkingTerminal[terminalType]+1
end
end
self.parkingByID[park.TerminalID]=park
table.insert(self.parking, park)
self.parkingByID[park.TerminalID]=park
table.insert(self.parking, park)
end
return self
@ -898,10 +1025,19 @@ function AIRBASE:GetParkingSpotsTable(termtype)
local spot=self:_GetParkingSpotByID(_spot.Term_Index)
spot.Free=_isfree(_spot) -- updated
spot.TOAC=_spot.TO_AC -- updated
if spot then
spot.Free=_isfree(_spot) -- updated
spot.TOAC=_spot.TO_AC -- updated
table.insert(spots, spot)
else
self:E(string.format("ERROR: Parking spot %s is nil!", tostring(_spot.Term_Index)))
end
table.insert(spots, spot)
end
end
@ -1082,9 +1218,8 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius,
local _spot=parkingspot.Coordinate -- Core.Point#COORDINATE
local _termid=parkingspot.TerminalID
self:T2({_termid=_termid})
if AIRBASE._CheckTerminalType(parkingspot.TerminalType, terminaltype) then
-- Check terminal type and black/white listed parking spots.
if AIRBASE._CheckTerminalType(parkingspot.TerminalType, terminaltype) and self:_CheckParkingLists(_termid) then
-- Very safe uses the DCS getParking() info to check if a spot is free. Unfortunately, the function returns free=false until the aircraft has actually taken-off.
if verysafe and (parkingspot.Free==false or parkingspot.TOAC==true) then
@ -1186,6 +1321,39 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius,
return validspots
end
--- Check black and white lists.
-- @param #AIRBASE self
-- @param #number TerminalID Terminal ID to check.
-- @return #boolean `true` if this is a valid spot.
function AIRBASE:_CheckParkingLists(TerminalID)
-- First check the black list. If we find a match, this spot is forbidden!
if self.parkingBlacklist and #self.parkingBlacklist>0 then
for _,terminalID in pairs(self.parkingBlacklist or {}) do
if terminalID==TerminalID then
-- This is a invalid spot.
return false
end
end
end
-- Check if a whitelist was defined.
if self.parkingWhitelist and #self.parkingWhitelist>0 then
for _,terminalID in pairs(self.parkingWhitelist or {}) do
if terminalID==TerminalID then
-- This is a valid spot.
return true
end
end
-- No match ==> invalid spot
return false
end
-- Neither black nor white lists were defined or spot is not in black list.
return true
end
--- Helper function to check for the correct terminal type including "artificial" ones.
-- @param #number Term_Type Termial type from getParking routine.
-- @param #AIRBASE.TerminalType termtype Terminal type from AIRBASE.TerminalType enumerator.
@ -1279,7 +1447,8 @@ function AIRBASE:GetRunwayData(magvar, mark)
name==AIRBASE.PersianGulf.Abu_Dhabi_International_Airport or
name==AIRBASE.PersianGulf.Dubai_Intl or
name==AIRBASE.PersianGulf.Shiraz_International_Airport or
name==AIRBASE.PersianGulf.Kish_International_Airport then
name==AIRBASE.PersianGulf.Kish_International_Airport or
name==AIRBASE.MarianaIslands.Andersen_AFB then
-- 1-->4, 2-->3, 3-->2, 4-->1
exception=1

View File

@ -3,8 +3,7 @@
-- ===
--
-- ### Author: **FlightControl**
--
-- ### Contributions:
-- ### Contributions: **funkyfranky**
--
-- ===
--
@ -52,13 +51,6 @@
--
-- @field #CLIENT
CLIENT = {
ONBOARDSIDE = {
NONE = 0,
LEFT = 1,
RIGHT = 2,
BACK = 3,
FRONT = 4
},
ClassName = "CLIENT",
ClientName = nil,
ClientAlive = false,
@ -66,27 +58,20 @@ CLIENT = {
ClientBriefingShown = false,
_Menus = {},
_Tasks = {},
Messages = {
}
Messages = {},
Players = {},
}
--- Finds a CLIENT from the _DATABASE using the relevant DCS Unit.
-- @param #CLIENT self
-- @param #string ClientName Name of the DCS **Unit** as defined within the Mission Editor.
-- @param #string ClientBriefing Text that describes the briefing of the mission when a Player logs into the Client.
-- @return #CLIENT
-- @usage
-- -- Create new Clients.
-- local Mission = MISSIONSCHEDULER.AddMission( 'Russia Transport Troops SA-6', 'Operational', 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', 'Russia' )
-- Mission:AddGoal( DeploySA6TroopsGoal )
--
-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 1' ):Transport() )
-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 3' ):Transport() )
-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 2' ):Transport() )
-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 4' ):Transport() )
function CLIENT:Find( DCSUnit, Error )
-- @param DCS#Unit DCSUnit The DCS unit of the client.
-- @param #boolean Error Throw an error message.
-- @return #CLIENT The CLIENT found in the _DATABASE.
function CLIENT:Find(DCSUnit, Error)
local ClientName = DCSUnit:getName()
local ClientFound = _DATABASE:FindClient( ClientName )
if ClientFound then
@ -117,11 +102,15 @@ end
-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 2' ):Transport() )
-- Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 4' ):Transport() )
function CLIENT:FindByName( ClientName, ClientBriefing, Error )
-- Client
local ClientFound = _DATABASE:FindClient( ClientName )
if ClientFound then
ClientFound:F( { ClientName, ClientBriefing } )
ClientFound:AddBriefing( ClientBriefing )
ClientFound:AddBriefing(ClientBriefing)
ClientFound.MessageSwitch = true
return ClientFound
@ -132,51 +121,105 @@ function CLIENT:FindByName( ClientName, ClientBriefing, Error )
end
end
function CLIENT:Register( ClientName )
--- Transport defines that the Client is a Transport. Transports show cargo.
-- @param #CLIENT self
-- @param #string ClientName Name of the client unit.
-- @return #CLIENT self
function CLIENT:Register(ClientName)
local self = BASE:Inherit( self, UNIT:Register( ClientName ) ) -- #CLIENT
self:F( ClientName )
self.ClientName = ClientName
self.MessageSwitch = true
self.ClientAlive2 = false
-- Inherit unit.
local self = BASE:Inherit( self, UNIT:Register(ClientName )) -- #CLIENT
--self.AliveCheckScheduler = routines.scheduleFunction( self._AliveCheckScheduler, { self }, timer.getTime() + 1, 5 )
self.AliveCheckScheduler = SCHEDULER:New( self, self._AliveCheckScheduler, { "Client Alive " .. ClientName }, 1, 5, 0.5 )
self.AliveCheckScheduler:NoTrace()
-- Set client name.
self.ClientName = ClientName
-- Message switch.
self.MessageSwitch = true
-- Alive2.
self.ClientAlive2 = false
self:F( self )
return self
end
--- Transport defines that the Client is a Transport. Transports show cargo.
-- @param #CLIENT self
-- @return #CLIENT
-- @return #CLIENT self
function CLIENT:Transport()
self:F()
self.ClientTransport = true
return self
end
--- AddBriefing adds a briefing to a CLIENT when a player joins a mission.
--- Adds a briefing to a CLIENT when a player joins a mission.
-- @param #CLIENT self
-- @param #string ClientBriefing is the text defining the Mission briefing.
-- @return #CLIENT self
function CLIENT:AddBriefing( ClientBriefing )
self:F( ClientBriefing )
self.ClientBriefing = ClientBriefing
self.ClientBriefingShown = false
return self
end
--- Add player name.
-- @param #CLIENT self
-- @param #string PlayerName Name of the player.
-- @return #CLIENT self
function CLIENT:AddPlayer(PlayerName)
table.insert(self.Players, PlayerName)
return self
end
--- Get player name(s).
-- @param #CLIENT self
-- @return #table List of player names or an empty table `{}`.
function CLIENT:GetPlayers()
return self.Players
end
--- Get name of player.
-- @param #CLIENT self
-- @return #string Player name or `nil`.
function CLIENT:GetPlayer()
if #self.Players>0 then
return self.Players[1]
end
return nil
end
--- Remove player.
-- @param #CLIENT self
-- @param #string PlayerName Name of the player.
-- @return #CLIENT self
function CLIENT:RemovePlayer(PlayerName)
for i,playername in pairs(self.Players) do
if PlayerName==playername then
table.remove(self.Players, i)
break
end
end
return self
end
--- Remove all players.
-- @param #CLIENT self
-- @return #CLIENT self
function CLIENT:RemovePlayers()
self.Players={}
return self
end
--- Show the briefing of a CLIENT.
-- @param #CLIENT self
-- @return #CLIENT self
function CLIENT:ShowBriefing()
self:F( { self.ClientName, self.ClientBriefingShown } )
if not self.ClientBriefingShown then
self.ClientBriefingShown = true
@ -204,8 +247,6 @@ function CLIENT:ShowMissionBriefing( MissionBriefing )
return self
end
--- Resets a CLIENT.
-- @param #CLIENT self
-- @param #string ClientName Name of the Group as defined within the Mission Editor. The Group must have a Unit with the type Client.
@ -241,6 +282,7 @@ end
--- Checks for a client alive event and calls a function on a continuous basis.
-- @param #CLIENT self
-- @param #function CallBackFunction Create a function that will be called when a player joins the slot.
-- @param ... (Optional) Arguments for callback function as comma separated list.
-- @return #CLIENT
function CLIENT:Alive( CallBackFunction, ... )
self:F()
@ -248,6 +290,9 @@ function CLIENT:Alive( CallBackFunction, ... )
self.ClientCallBack = CallBackFunction
self.ClientParameters = arg
self.AliveCheckScheduler = SCHEDULER:New( self, self._AliveCheckScheduler, { "Client Alive " .. self.ClientName }, 0.1, 5, 0.5 )
self.AliveCheckScheduler:NoTrace()
return self
end
@ -255,19 +300,29 @@ end
function CLIENT:_AliveCheckScheduler( SchedulerName )
self:F3( { SchedulerName, self.ClientName, self.ClientAlive2, self.ClientBriefingShown, self.ClientCallBack } )
if self:IsAlive() then
if self:IsAlive() then
if self.ClientAlive2 == false then
-- Show briefing.
self:ShowBriefing()
-- Callback function.
if self.ClientCallBack then
self:T("Calling Callback function")
self.ClientCallBack( self, unpack( self.ClientParameters ) )
end
-- Alive.
self.ClientAlive2 = true
end
else
if self.ClientAlive2 == true then
self.ClientAlive2 = false
end
end
return true
@ -291,6 +346,7 @@ function CLIENT:GetDCSGroup()
local ClientUnit = Unit.getByName( self.ClientName )
local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ) }
for CoalitionId, CoalitionData in pairs( CoalitionsData ) do
self:T3( { "CoalitionData:", CoalitionData } )
for UnitId, UnitData in pairs( CoalitionData ) do
@ -299,10 +355,14 @@ function CLIENT:GetDCSGroup()
--self:F(self.ClientName)
if ClientUnit then
local ClientGroup = ClientUnit:getGroup()
if ClientGroup then
self:T3( "ClientGroup = " .. self.ClientName )
if ClientGroup:isExist() and UnitData:getGroup():isExist() then
if ClientGroup:isExist() and UnitData:getGroup():isExist() then
if ClientGroup:getID() == UnitData:getGroup():getID() then
self:T3( "Normal logic" )
self:T3( self.ClientName .. " : group found!" )
@ -310,15 +370,22 @@ function CLIENT:GetDCSGroup()
self.ClientGroupName = ClientGroup:getName()
return ClientGroup
end
else
-- Now we need to resolve the bugs in DCS 1.5 ...
-- Consult the database for the units of the Client Group. (ClientGroup:getUnits() returns nil)
self:T3( "Bug 1.5 logic" )
local ClientGroupTemplate = _DATABASE.Templates.Units[self.ClientName].GroupTemplate
self.ClientGroupID = ClientGroupTemplate.groupId
self.ClientGroupName = _DATABASE.Templates.Units[self.ClientName].GroupName
self:T3( self.ClientName .. " : group found in bug 1.5 resolvement logic!" )
return ClientGroup
end
-- else
-- error( "Client " .. self.ClientName .. " not found!" )
@ -343,22 +410,22 @@ function CLIENT:GetDCSGroup()
end
end
-- Nothing could be found :(
self.ClientGroupID = nil
self.ClientGroupUnit = nil
self.ClientGroupName = nil
return nil
end
-- TODO: Check DCS#Group.ID
--- Get the group ID of the client.
-- @param #CLIENT self
-- @return DCS#Group.ID
-- @return #number DCS#Group ID.
function CLIENT:GetClientGroupID()
local ClientGroup = self:GetDCSGroup()
-- This updates the ID.
self:GetDCSGroup()
--self:F( self.ClientGroupID ) -- Determined in GetDCSGroup()
return self.ClientGroupID
end
@ -368,26 +435,28 @@ end
-- @return #string
function CLIENT:GetClientGroupName()
local ClientGroup = self:GetDCSGroup()
self:T( self.ClientGroupName ) -- Determined in GetDCSGroup()
-- This updates the group name.
self:GetDCSGroup()
return self.ClientGroupName
end
--- Returns the UNIT of the CLIENT.
-- @param #CLIENT self
-- @return Wrapper.Unit#UNIT
-- @return Wrapper.Unit#UNIT The client UNIT or `nil`.
function CLIENT:GetClientGroupUnit()
self:F2()
local ClientDCSUnit = Unit.getByName( self.ClientName )
self:T( self.ClientDCSUnit )
if ClientDCSUnit and ClientDCSUnit:isExist() then
local ClientUnit = _DATABASE:FindUnit( self.ClientName )
self:T2( ClientUnit )
local ClientUnit=_DATABASE:FindUnit( self.ClientName )
return ClientUnit
end
return nil
end
--- Returns the DCSUnit of the CLIENT.

View File

@ -129,6 +129,7 @@
-- * @{#CONTROLLABLE.Route}(): Make the Controllable to follow a given route.
-- * @{#CONTROLLABLE.RouteGroundTo}(): Make the GROUND Controllable to drive towards a specific coordinate.
-- * @{#CONTROLLABLE.RouteAirTo}(): Make the AIR Controllable to fly towards a specific coordinate.
-- * @{#CONTROLLABLE.RelocateGroundRandomInRadius}(): Relocate the GROUND controllable to a random point in a given radius.
--
-- # 5) Option methods
--
@ -173,6 +174,9 @@
-- * @{#CONTROLLABLE.OptionAllowJettisonWeaponsOnThreat}
-- * @{#CONTROLLABLE.OptionKeepWeaponsOnThreat}
--
-- ## 5.5) Air-2-Air missile attack range:
-- * @{#CONTROLLABLE.OptionAAAttackRange}(): Defines the usage of A2A missiles against possible targets .
--
-- @field #CONTROLLABLE
CONTROLLABLE = {
ClassName = "CONTROLLABLE",
@ -500,7 +504,7 @@ function CONTROLLABLE:TaskCombo( DCSTasks )
tasks = DCSTasks
}
}
return DCSTaskCombo
end
@ -798,12 +802,12 @@ end
-- @return #CONTROLLABLE self
function CONTROLLABLE:CommandSetFrequency(Frequency, Modulation, Delay)
local CommandSetFrequency = {
id = 'SetFrequency',
params = {
frequency = Frequency*1000000,
modulation = Modulation or radio.modulation.AM,
}
local CommandSetFrequency = {
id = 'SetFrequency',
params = {
frequency = Frequency*1000000,
modulation = Modulation or radio.modulation.AM,
}
}
if Delay and Delay>0 then
@ -877,7 +881,7 @@ function CONTROLLABLE:TaskAttackGroup( AttackGroup, WeaponType, WeaponExpend, At
groupId = AttackGroup:GetID(),
weaponType = WeaponType or 1073741822,
expend = WeaponExpend or "Auto",
attackQtyLimit = AttackQty and true or false,
attackQtyLimit = AttackQty and true or false,
attackQty = AttackQty or 1,
directionEnabled = Direction and true or false,
direction = Direction and math.rad(Direction) or 0,
@ -917,7 +921,7 @@ function CONTROLLABLE:TaskAttackUnit(AttackUnit, GroupAttack, WeaponExpend, Atta
weaponType = WeaponType or 1073741822,
}
}
return DCSTask
end
@ -1081,7 +1085,7 @@ function CONTROLLABLE:TaskEmbarking(Coordinate, GroupSetForEmbarking, Duration,
-- Distribution
--local distribution={}
--distribution[id]=gids
local groupID=self and self:GetID()
local DCSTask = {
@ -1310,7 +1314,7 @@ function CONTROLLABLE:TaskLandAtVec2(Vec2, Duration)
duration = Duration,
},
}
return DCSTask
end
@ -1424,16 +1428,22 @@ end
-- @param DCS#Distance Radius The radius of the zone to deploy the fire at.
-- @param #number AmmoCount (optional) Quantity of ammunition to expand (omit to fire until ammunition is depleted).
-- @param #number WeaponType (optional) Enum for weapon type ID. This value is only required if you want the group firing to use a specific weapon, for instance using the task on a ship to force it to fire guided missiles at targets within cannon range. See http://wiki.hoggit.us/view/DCS_enum_weapon_flag
-- @param #number Altitude (Optional) Altitude in meters.
-- @param #number ASL Altitude is above mean sea level. Default is above ground level.
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:TaskFireAtPoint( Vec2, Radius, AmmoCount, WeaponType )
function CONTROLLABLE:TaskFireAtPoint( Vec2, Radius, AmmoCount, WeaponType, Altitude, ASL )
local DCSTask = {
id = 'FireAtPoint',
params = {
point = Vec2,
x=Vec2.x,
y=Vec2.y,
zoneRadius = Radius,
radius = Radius,
expendQty = 100, -- dummy value
expendQtyEnabled = false,
alt_type = ASL and 0 or 1
}
}
@ -1442,10 +1452,16 @@ function CONTROLLABLE:TaskFireAtPoint( Vec2, Radius, AmmoCount, WeaponType )
DCSTask.params.expendQtyEnabled = true
end
if Altitude then
DCSTask.params.altitude=Altitude
end
if WeaponType then
DCSTask.params.weaponType=WeaponType
end
--self:I(DCSTask)
return DCSTask
end
@ -1463,6 +1479,7 @@ end
--- (AIR + GROUND) The task makes the controllable/unit a FAC and orders the FAC to control the target (enemy ground controllable) destruction.
-- The killer is player-controlled allied CAS-aircraft that is in contact with the FAC.
-- If the task is assigned to the controllable lead unit will be a FAC.
-- It's important to note that depending on the type of unit that is being assigned the task (AIR or GROUND), you must choose the correct type of callsign enumerator. For airborne controllables use CALLSIGN.Aircraft and for ground based use CALLSIGN.JTAC enumerators.
-- @param #CONTROLLABLE self
-- @param Wrapper.Group#GROUP AttackGroup Target GROUP object.
-- @param #number WeaponType Bitmask of weapon types, which are allowed to use.
@ -1470,7 +1487,7 @@ end
-- @param #boolean Datalink (Optional) Allows to use datalink to send the target information to attack aircraft. Enabled by default.
-- @param #number Frequency Frequency in MHz used to communicate with the FAC. Default 133 MHz.
-- @param #number Modulation Modulation of radio for communication. Default 0=AM.
-- @param #number CallsignName Callsign enumerator name of the FAC.
-- @param #number CallsignName Callsign enumerator name of the FAC. (CALLSIGN.Aircraft.{name} for airborne controllables, CALLSIGN.JTACS.{name} for ground units)
-- @param #number CallsignNumber Callsign number, e.g. Axeman-**1**.
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:TaskFAC_AttackGroup( AttackGroup, WeaponType, Designation, Datalink, Frequency, Modulation, CallsignName, CallsignNumber )
@ -1792,7 +1809,7 @@ function CONTROLLABLE:TaskFunction( FunctionString, ... )
-- DCS task.
local DCSTask = self:TaskWrappedAction(self:CommandDoScript(table.concat( DCSScript )))
return DCSTask
end
@ -1815,7 +1832,7 @@ end
do -- Patrol methods
--- (GROUND) Patrol iteratively using the waypoints the for the (parent) group.
--- (GROUND) Patrol iteratively using the waypoints of the (parent) group.
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE
function CONTROLLABLE:PatrolRoute()
@ -1834,8 +1851,27 @@ do -- Patrol methods
-- Calculate the new Route.
local FromCoord = PatrolGroup:GetCoordinate()
local From = FromCoord:WaypointGround( 120 )
-- test for submarine
local depth = 0
local IsSub = false
if PatrolGroup:IsShip() then
local navalvec3 = FromCoord:GetVec3()
if navalvec3.y < 0 then
depth = navalvec3.y
IsSub = true
end
end
local Waypoint = Waypoints[1]
local Speed = Waypoint.speed or (20 / 3.6)
local From = FromCoord:WaypointGround( Speed )
if IsSub then
From = FromCoord:WaypointNaval( Speed, Waypoint.alt )
end
table.insert( Waypoints, 1, From )
local TaskRoute = PatrolGroup:TaskFunction( "CONTROLLABLE.PatrolRoute" )
@ -1875,7 +1911,16 @@ do -- Patrol methods
if ToWaypoint then
FromWaypoint = ToWaypoint
end
-- test for submarine
local depth = 0
local IsSub = false
if PatrolGroup:IsShip() then
local navalvec3 = FromCoord:GetVec3()
if navalvec3.y < 0 then
depth = navalvec3.y
IsSub = true
end
end
-- Loop until a waypoint has been found that is not the same as the current waypoint.
-- Otherwise the object zon't move or drive in circles and the algorithm would not do exactly
-- what it is supposed to do, which is making groups drive around.
@ -1890,9 +1935,13 @@ do -- Patrol methods
local ToCoord = COORDINATE:NewFromVec2( { x = Waypoint.x, y = Waypoint.y } )
-- Create a "ground route point", which is a "point" structure that can be given as a parameter to a Task
local Route = {}
Route[#Route+1] = FromCoord:WaypointGround( 0 )
Route[#Route+1] = ToCoord:WaypointGround( Speed, Formation )
if IsSub then
Route[#Route+1] = FromCoord:WaypointNaval( Speed, depth )
Route[#Route+1] = ToCoord:WaypointNaval( Speed, Waypoint.alt )
else
Route[#Route+1] = FromCoord:WaypointGround( Speed, Formation )
Route[#Route+1] = ToCoord:WaypointGround( Speed, Formation )
end
local TaskRouteToZone = PatrolGroup:TaskFunction( "CONTROLLABLE.PatrolRouteRandom", Speed, Formation, ToWaypoint )
@ -1933,9 +1982,20 @@ do -- Patrol methods
self:F( { PatrolGroup = PatrolGroup:GetName() } )
if PatrolGroup:IsGround() or PatrolGroup:IsShip() then
-- Calculate the new Route.
local FromCoord = PatrolGroup:GetCoordinate()
-- test for submarine
local depth = 0
local IsSub = false
if PatrolGroup:IsShip() then
local navalvec3 = FromCoord:GetVec3()
if navalvec3.y < 0 then
depth = navalvec3.y
IsSub = true
end
end
-- Select a random Zone and get the Coordinate of the new Zone.
local RandomZone = ZoneList[ math.random( 1, #ZoneList ) ] -- Core.Zone#ZONE
@ -1943,9 +2003,13 @@ do -- Patrol methods
-- Create a "ground route point", which is a "point" structure that can be given as a parameter to a Task
local Route = {}
Route[#Route+1] = FromCoord:WaypointGround( 20 )
Route[#Route+1] = ToCoord:WaypointGround( Speed, Formation )
if IsSub then
Route[#Route+1] = FromCoord:WaypointNaval( Speed, depth )
Route[#Route+1] = ToCoord:WaypointNaval( Speed, depth )
else
Route[#Route+1] = FromCoord:WaypointGround( Speed, Formation )
Route[#Route+1] = ToCoord:WaypointGround( Speed, Formation )
end
local TaskRouteToZone = PatrolGroup:TaskFunction( "CONTROLLABLE.PatrolZones", ZoneList, Speed, Formation, DelayMin, DelayMax )
@ -1969,7 +2033,7 @@ function CONTROLLABLE:TaskRoute( Points )
id = 'Mission',
params = {
airborne = self:IsAir(),
route = {points = Points},
route = {points = Points},
},
}
@ -2891,13 +2955,13 @@ end
--- Set option for Rules of Engagement (ROE).
-- @param Wrapper.Controllable#CONTROLLABLE self
-- @param #number ROEvalue ROE value. See ENUMS.ROE.
-- @return Wrapper.Controllable#CONTROLLABLE self
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionROE(ROEvalue)
local DCSControllable = self:GetDCSObject()
if DCSControllable then
local Controller = self:_GetController()
if self:IsAir() then
@ -2933,8 +2997,8 @@ function CONTROLLABLE:OptionROEHoldFirePossible()
end
--- Weapons Hold: AI will hold fire under all circumstances.
-- @param Wrapper.Controllable#CONTROLLABLE self
-- @return Wrapper.Controllable#CONTROLLABLE self
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionROEHoldFire()
self:F2( { self.ControllableName } )
@ -3461,13 +3525,13 @@ end
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionProhibitAfterburner(Prohibit)
self:F2( { self.ControllableName } )
if Prohibit==nil then
Prohibit=true
end
if self:IsAir() then
self:SetOption(AI.Option.Air.id.PROHIBIT_AB, Prohibit)
self:SetOption(AI.Option.Air.id.PROHIBIT_AB, Prohibit)
end
return self
@ -3478,9 +3542,9 @@ end
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionECM_Never()
self:F2( { self.ControllableName } )
if self:IsAir() then
self:SetOption(AI.Option.Air.id.ECM_USING, 0)
self:SetOption(AI.Option.Air.id.ECM_USING, 0)
end
return self
@ -3491,9 +3555,9 @@ end
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionECM_OnlyLockByRadar()
self:F2( { self.ControllableName } )
if self:IsAir() then
self:SetOption(AI.Option.Air.id.ECM_USING, 1)
self:SetOption(AI.Option.Air.id.ECM_USING, 1)
end
return self
@ -3505,9 +3569,9 @@ end
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionECM_DetectedLockByRadar()
self:F2( { self.ControllableName } )
if self:IsAir() then
self:SetOption(AI.Option.Air.id.ECM_USING, 2)
self:SetOption(AI.Option.Air.id.ECM_USING, 2)
end
return self
@ -3518,9 +3582,9 @@ end
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionECM_AlwaysOn()
self:F2( { self.ControllableName } )
if self:IsAir() then
self:SetOption(AI.Option.Air.id.ECM_USING, 3)
self:SetOption(AI.Option.Air.id.ECM_USING, 3)
end
return self
@ -3532,7 +3596,7 @@ end
-- Note that when WayPointInitialize is called, the Mission of the controllable is RESTARTED!
-- @param #CONTROLLABLE self
-- @param #table WayPoints If WayPoints is given, then use the route.
-- @return #CONTROLLABLE
-- @return #CONTROLLABLE self
function CONTROLLABLE:WayPointInitialize( WayPoints )
self:F( { WayPoints } )
@ -3563,7 +3627,7 @@ end
-- @param #number WayPoint The waypoint number. Note that the start waypoint on the route is WayPoint 1!
-- @param #number WayPointIndex When defining multiple WayPoint functions for one WayPoint, use WayPointIndex to set the sequence of actions.
-- @param #function WayPointFunction The waypoint function to be called when the controllable moves over the waypoint. The waypoint function takes variable parameters.
-- @return #CONTROLLABLE
-- @return #CONTROLLABLE self
function CONTROLLABLE:WayPointFunction( WayPoint, WayPointIndex, WayPointFunction, ... )
self:F2( { WayPoint, WayPointIndex, WayPointFunction } )
@ -3579,7 +3643,7 @@ end
-- @param #CONTROLLABLE self
-- @param #number WayPoint The WayPoint from where to execute the mission.
-- @param #number WaitTime The amount seconds to wait before initiating the mission.
-- @return #CONTROLLABLE
-- @return #CONTROLLABLE self
function CONTROLLABLE:WayPointExecute( WayPoint, WaitTime )
self:F( { WayPoint, WaitTime } )
@ -3659,3 +3723,124 @@ function CONTROLLABLE:OptionRestrictBurner(RestrictBurner)
end
end
--- Sets Controllable Option for A2A attack range for AIR FIGHTER units.
-- @param #CONTROLLABLE self
-- @param #number range Defines the range
-- @return #CONTROLLABLE self
-- @usage Range can be one of MAX_RANGE = 0, NEZ_RANGE = 1, HALF_WAY_RMAX_NEZ = 2, TARGET_THREAT_EST = 3, RANDOM_RANGE = 4. Defaults to 3. See: https://wiki.hoggitworld.com/view/DCS_option_missileAttack
function CONTROLLABLE:OptionAAAttackRange(range)
self:F2( { self.ControllableName } )
-- defaults to 3
local range = range or 3
if range < 0 or range > 4 then
range = 3
end
local DCSControllable = self:GetDCSObject()
if DCSControllable then
local Controller = self:_GetController()
if Controller then
if self:IsAir() then
self:SetOption(AI.Option.Air.val.MISSILE_ATTACK, range)
end
end
return self
end
return nil
end
--- Defines the range at which a GROUND unit/group is allowed to use its weapons automatically.
-- @param #CONTROLLABLE self
-- @param #number EngageRange Engage range limit in percent (a number between 0 and 100). Default 100.
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionEngageRange(EngageRange)
self:F2( { self.ControllableName } )
-- Set default if not specified.
EngageRange=EngageRange or 100
if EngageRange < 0 or EngageRange > 100 then
EngageRange = 100
end
local DCSControllable = self:GetDCSObject()
if DCSControllable then
local Controller = self:_GetController()
if Controller then
if self:IsGround() then
self:SetOption(AI.Option.Ground.id.AC_ENGAGEMENT_RANGE_RESTRICTION, EngageRange)
end
end
return self
end
return nil
end
--- (GROUND) Relocate controllable to a random point within a given radius; use e.g.for evasive actions; Note that not all ground controllables can actually drive, also the alarm state of the controllable might stop it from moving.
-- @param #CONTROLLABLE self
-- @param #number speed Speed of the controllable, default 20
-- @param #number radius Radius of the relocation zone, default 500
-- @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
-- @return #CONTROLLABLE self
function CONTROLLABLE:RelocateGroundRandomInRadius(speed, radius, onroad, shortcut)
self:F2( { self.ControllableName } )
local _coord = self:GetCoordinate()
local _radius = radius or 500
local _speed = speed or 20
local _tocoord = _coord:GetRandomCoordinateInRadius(_radius,100)
local _onroad = onroad or true
local _grptsk = {}
local _candoroad = false
local _shortcut = shortcut or false
-- create a DCS Task an push it on the group
-- TaskGroundOnRoad(ToCoordinate,Speed,OffRoadFormation,Shortcut,FromCoordinate,WaypointFunction,WaypointFunctionArguments)
if onroad then
_grptsk, _candoroad = self:TaskGroundOnRoad(_tocoord,_speed,"Off Road",_shortcut)
self:Route(_grptsk,5)
else
self:TaskRouteToVec2(_tocoord:GetVec2(),_speed,"Off Road")
end
return self
end
--- Defines how long a GROUND unit/group will move to avoid an ongoing attack.
-- @param #CONTROLLABLE self
-- @param #number Seconds Any positive number: AI will disperse, but only for the specified time before continuing their route. 0: AI will not disperse.
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionDisperseOnAttack(Seconds)
self:F2( { self.ControllableName } )
-- Set default if not specified.
local seconds = Seconds or 0
local DCSControllable = self:GetDCSObject()
if DCSControllable then
local Controller = self:_GetController()
if Controller then
if self:IsGround() then
self:SetOption(AI.Option.Ground.id.DISPERSE_ON_ATTACK, seconds)
end
end
return self
end
return nil
end
--- Returns if the unit is a submarine.
-- @param #POSITIONABLE self
-- @return #boolean Submarines attributes result.
function POSITIONABLE:IsSubmarine()
self:F2()
local DCSUnit = self:GetDCSObject()
if DCSUnit then
local UnitDescriptor = DCSUnit:getDesc()
if UnitDescriptor.attributes["Submarines"] == true then
return true
else
return false
end
end
return nil
end

View File

@ -310,8 +310,7 @@ end
--- Returns the @{DCS#Position3} position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission.
-- @param Wrapper.Positionable#POSITIONABLE self
-- @return DCS#Position The 3D position vectors of the POSITIONABLE.
-- @return #nil The POSITIONABLE is not existing or alive.
-- @return DCS#Position The 3D position vectors of the POSITIONABLE or #nil if the groups not existing or alive.
function GROUP:GetPositionVec3() -- Overridden from POSITIONABLE:GetPositionVec3()
self:F2( self.PositionableName )
@ -340,8 +339,7 @@ end
--
-- @param #GROUP self
-- @return #boolean true if the group is alive and active.
-- @return #boolean false if the group is alive but inactive.
-- @return #nil if the group does not exist anymore.
-- @return #boolean false if the group is alive but inactive or #nil if the group does not exist anymore.
function GROUP:IsAlive()
self:F2( self.GroupName )
@ -363,8 +361,7 @@ end
--- Returns if the group is activated.
-- @param #GROUP self
-- @return #boolean true if group is activated.
-- @return #nil The group is not existing or alive.
-- @return #boolean true if group is activated or #nil The group is not existing or alive.
function GROUP:IsActive()
self:F2( self.GroupName )
@ -412,7 +409,6 @@ function GROUP:Destroy( GenerateEvent, delay )
self:F2( self.GroupName )
if delay and delay>0 then
--SCHEDULER:New(nil, GROUP.Destroy, {self, GenerateEvent}, delay)
self:ScheduleOnce(delay, GROUP.Destroy, self, GenerateEvent)
else
@ -2169,40 +2165,6 @@ function GROUP:GetThreatLevel()
return threatlevelMax
end
--- Get the unit in the group with the highest threat level, which is still alive.
-- @param #GROUP self
-- @return Wrapper.Unit#UNIT The most dangerous unit in the group.
-- @return #number Threat level of the unit.
function GROUP:GetHighestThreat()
-- Get units of the group.
local units=self:GetUnits()
if units then
local threat=nil ; local maxtl=0
for _,_unit in pairs(units or {}) do
local unit=_unit --Wrapper.Unit#UNIT
if unit and unit:IsAlive() then
-- Threat level of group.
local tl=unit:GetThreatLevel()
-- Check if greater the current threat.
if tl>maxtl then
maxtl=tl
threat=unit
end
end
end
return threat, maxtl
end
return nil, nil
end
--- Returns true if the first unit of the GROUP is in the air.
-- @param Wrapper.Group#GROUP self
@ -2585,6 +2547,49 @@ do -- Players
end
--- GROUND - Switch on/off radar emissions for the group.
-- @param #GROUP self
-- @param #boolean switch If true, emission is enabled. If false, emission is disabled.
-- @return #GROUP self
function GROUP:EnableEmission(switch)
self:F2( self.GroupName )
local switch = switch or false
local DCSUnit = self:GetDCSObject()
if DCSUnit then
DCSUnit:enableEmission(switch)
end
return self
end
--- Switch on/off invisible flag for the group.
-- @param #GROUP self
-- @param #boolean switch If true, emission is enabled. If false, emission is disabled.
-- @return #GROUP self
function GROUP:SetCommandInvisible(switch)
self:F2( self.GroupName )
local switch = switch or false
local SetInvisible = {id = 'SetInvisible', params = {value = true}}
self:SetCommand(SetInvisible)
return self
end
--- Switch on/off immortal flag for the group.
-- @param #GROUP self
-- @param #boolean switch If true, emission is enabled. If false, emission is disabled.
-- @return #GROUP self
function GROUP:SetCommandImmortal(switch)
self:F2( self.GroupName )
local switch = switch or false
local SetInvisible = {id = 'SetImmortal', params = {value = true}}
self:SetCommand(SetInvisible)
return self
end
--do -- Smoke
--
----- Signal a flare at the position of the GROUP.
@ -2675,4 +2680,4 @@ end
--
--
--
--end
--end

View File

@ -13,7 +13,7 @@
--
-- ### Author: **funkyfranky**
-- @module Wrapper.Marker
-- @image Wrapper_Marker.png
-- @image MOOSE_Core.JPG
--- Marker class.
@ -646,7 +646,7 @@ function MARKER:OnEventMarkRemoved(EventData)
local MarkID=EventData.MarkID
self:T3(self.lid..string.format("Captured event MarkAdded for Mark ID=%s", tostring(MarkID)))
self:T3(self.lid..string.format("Captured event MarkRemoved for Mark ID=%s", tostring(MarkID)))
if MarkID==self.mid then
@ -673,9 +673,9 @@ function MARKER:OnEventMarkChange(EventData)
if MarkID==self.mid then
self:Changed(EventData)
self.text=tostring(EventData.MarkText)
self:TextChanged(tostring(EventData.MarkText))
self:Changed(EventData)
end

View File

@ -590,26 +590,29 @@ end
--- Returns the POSITIONABLE heading in degrees.
-- @param Wrapper.Positionable#POSITIONABLE self
-- @return #number The POSITIONABLE heading
-- @return #nil The POSITIONABLE is not existing or alive.
-- @return #number The POSITIONABLE heading in degrees or `nil` if not existing or alive.
function POSITIONABLE:GetHeading()
local DCSPositionable = self:GetDCSObject()
if DCSPositionable then
local PositionablePosition = DCSPositionable:getPosition()
if PositionablePosition then
local PositionableHeading = math.atan2( PositionablePosition.x.z, PositionablePosition.x.x )
if PositionableHeading < 0 then
PositionableHeading = PositionableHeading + 2 * math.pi
end
PositionableHeading = PositionableHeading * 180 / math.pi
self:T2( PositionableHeading )
return PositionableHeading
end
end
BASE:E( { "Cannot GetHeading", Positionable = self, Alive = self:IsAlive() } )
self:E({"Cannot GetHeading", Positionable = self, Alive = self:IsAlive()})
return nil
end
@ -681,6 +684,27 @@ function POSITIONABLE:IsShip()
end
--- Returns if the unit is a submarine.
-- @param #POSITIONABLE self
-- @return #boolean Submarines attributes result.
function POSITIONABLE:IsSubmarine()
self:F2()
local DCSUnit = self:GetDCSObject()
if DCSUnit then
local UnitDescriptor = DCSUnit:getDesc()
if UnitDescriptor.attributes["Submarines"] == true then
return true
else
return false
end
end
return nil
end
--- Returns true if the POSITIONABLE is in the air.
-- Polymorphic, is overridden in GROUP and UNIT.
-- @param Wrapper.Positionable#POSITIONABLE self
@ -814,8 +838,7 @@ end
-- @return #number The velocity in knots.
function POSITIONABLE:GetVelocityKNOTS()
self:F2( self.PositionableName )
local velmps=self:GetVelocityMPS()
return UTILS.MpsToKnots(velmps)
return UTILS.MpsToKnots(self:GetVelocityMPS())
end
--- Returns the Angle of Attack of a positionable.
@ -1391,18 +1414,6 @@ do -- Cargo
return ItemCount
end
-- --- Get Cargo Bay Free Volume in m3.
-- -- @param #POSITIONABLE self
-- -- @return #number CargoBayFreeVolume
-- function POSITIONABLE:GetCargoBayFreeVolume()
-- local CargoVolume = 0
-- for CargoName, Cargo in pairs( self.__.Cargo ) do
-- CargoVolume = CargoVolume + Cargo:GetVolume()
-- end
-- return self.__.CargoBayVolumeLimit - CargoVolume
-- end
--
--- Get Cargo Bay Free Weight in kg.
-- @param #POSITIONABLE self
-- @return #number CargoBayFreeWeight
@ -1420,13 +1431,6 @@ do -- Cargo
return self.__.CargoBayWeightLimit - CargoWeight
end
-- --- Get Cargo Bay Volume Limit in m3.
-- -- @param #POSITIONABLE self
-- -- @param #number VolumeLimit
-- function POSITIONABLE:SetCargoBayVolumeLimit( VolumeLimit )
-- self.__.CargoBayVolumeLimit = VolumeLimit
-- end
--- Set Cargo Bay Weight Limit in kg.
-- @param #POSITIONABLE self
-- @param #number WeightLimit
@ -1445,55 +1449,82 @@ do -- Cargo
self:F({Desc=Desc})
local Weights = {
["C-17A"] = 35000, --77519 cannot be used, because it loads way too much apcs and infantry.,
["C-130"] = 22000 --The real value cannot be used, because it loads way too much apcs and infantry.,
["C-17A"] = 35000, --77519 cannot be used, because it loads way too much apcs and infantry.
["C-130"] = 22000 --The real value cannot be used, because it loads way too much apcs and infantry.
}
self.__.CargoBayWeightLimit = Weights[Desc.typeName] or ( Desc.massMax - ( Desc.massEmpty + Desc.fuelMassMax ) )
elseif self:IsShip() then
local Desc = self:GetDesc()
self:F({Desc=Desc})
local Weights = {
["Type_071"] = 245000,
["LHA_Tarawa"] = 500000,
["Ropucha-class"] = 150000,
["Dry-cargo ship-1"] = 70000,
["Dry-cargo ship-2"] = 70000,
["Higgins_boat"] = 3700, -- Higgins Boat can load 3700 kg of general cargo or 36 men (source wikipedia).
["USS_Samuel_Chase"] = 25000, -- Let's say 25 tons for now. Wiki says 33 Higgins boats, which would be 264 tons (can't be right!) and/or 578 troops.
["LST_Mk2"] = 2100000, -- Can carry 2100 tons according to wiki source!
["speedboat"] = 500, -- 500 kg ~ 5 persons
["Seawise_Giant"] =261000000, -- Gross tonnage is 261,000 tonns.
}
self.__.CargoBayWeightLimit = ( Weights[Desc.typeName] or 50000 )
else
local Desc = self:GetDesc()
local Weights = {
["M1126 Stryker ICV"] = 9,
["M-113"] = 9,
["AAV7"] = 25,
["M2A1_halftrack"] = 9,
["BMD-1"] = 9,
["Bedford_MWD"] = 8, -- new by kappa
["Blitz_36-6700A"] = 10, -- new by kappa
["BMD-1"] = 9, -- IRL should be 4 passengers
["BMP-1"] = 8,
["BMP-2"] = 7,
["BMP-3"] = 8,
["BMP-3"] = 8, -- IRL should be 7+2 passengers
["Boman"] = 25,
["BTR-80"] = 9,
["BTR_D"] = 12,
["BTR-80"] = 9, -- IRL should be 7 passengers
["BTR-82A"] = 9, -- new by kappa -- IRL should be 7 passengers
["BTR_D"] = 12, -- IRL should be 10 passengers
["Cobra"] = 8,
["Land_Rover_101_FC"] = 11, -- new by kappa
["Land_Rover_109_S3"] = 7, -- new by kappa
["LAV-25"] = 6,
["M-2 Bradley"] = 6,
["M1043 HMMWV Armament"] = 4,
["M1045 HMMWV TOW"] = 4,
["M1126 Stryker ICV"] = 9,
["M1134 Stryker ATGM"] = 9,
["M2A1_halftrack"] = 9,
["M-113"] = 9, -- IRL should be 11 passengers
["Marder"] = 6,
["MCV-80"] = 9,
["MCV-80"] = 9, -- IRL should be 7 passengers
["MLRS FDDM"] = 4,
["MTLB"] = 25,
["TPZ"] = 10,
["Ural-4320 APA-5D"] = 10,
["MTLB"] = 25, -- IRL should be 11 passengers
["GAZ-66"] = 8,
["GAZ-3307"] = 12,
["GAZ-3308"] = 14,
["Tigr_233036"] = 6,
["Grad_FDDM"] = 6, -- new by kappa
["KAMAZ Truck"] = 12,
["KrAZ6322"] = 12,
["M 818"] = 12,
["Tigr_233036"] = 6,
["TPZ"] = 10,
["UAZ-469"] = 4, -- new by kappa
["Ural-375"] = 12,
["Ural-4320-31"] = 14,
["Ural-4320 APA-5D"] = 10,
["Ural-4320T"] = 14,
["ZBD04A"] = 7, -- new by kappa
}
local CargoBayWeightLimit = ( Weights[Desc.typeName] or 0 ) * 95
self.__.CargoBayWeightLimit = CargoBayWeightLimit
end
end
self:F({CargoBayWeightLimit = self.__.CargoBayWeightLimit})
end
end --- Cargo

View File

@ -57,3 +57,40 @@ end
function SCENERY:GetThreatLevel()
return 0, "Scenery"
end
--- Find a SCENERY object by it's name/id.
--@param #SCENERY self
--@param #string name The name/id of the scenery object as taken from the ME. Ex. '595785449'
--@return #SCENERY Scenery Object or nil if not found.
function SCENERY:FindByName(name)
local findAirbase = function ()
local airbases = AIRBASE.GetAllAirbases()
for index,airbase in pairs(airbases) do
local surftype = airbase:GetCoordinate():GetSurfaceType()
if surftype ~= land.SurfaceType.SHALLOW_WATER and surftype ~= land.SurfaceType.WATER then
return airbase:GetCoordinate()
end
end
return nil
end
local sceneryScan = function (scancoord)
if scancoord ~= nil then
local _,_,sceneryfound,_,_,scenerylist = scancoord:ScanObjects(200, false, false, true)
if sceneryfound == true then
scenerylist[1].id_ = name
SCENERY.SceneryObject = SCENERY:Register(scenerylist[1].id_, scenerylist[1])
return SCENERY.SceneryObject
end
end
return nil
end
if SCENERY.SceneryObject then
SCENERY.SceneryObject.SceneryObject.id_ = name
SCENERY.SceneryObject.SceneryName = name
return SCENERY:Register(SCENERY.SceneryObject.SceneryObject.id_, SCENERY.SceneryObject.SceneryObject)
else
return sceneryScan(findAirbase())
end
end

View File

@ -13,7 +13,7 @@
--
-- ### Author: **FlightControl**
--
-- ### Contributions:
-- ### Contributions: **funkyfranky**
--
-- ===
--
@ -22,6 +22,8 @@
--- @type UNIT
-- @field #string ClassName Name of the class.
-- @field #string UnitName Name of the unit.
-- @extends Wrapper.Controllable#CONTROLLABLE
--- For each DCS Unit object alive within a running mission, a UNIT wrapper object (instance) will be created within the _@{DATABASE} object.
@ -61,7 +63,7 @@
--
-- The UNIT class provides methods to obtain the current point or position of the DCS Unit.
-- The @{#UNIT.GetPointVec2}(), @{#UNIT.GetVec3}() will obtain the current **location** of the DCS Unit in a Vec2 (2D) or a **point** in a Vec3 (3D) vector respectively.
-- If you want to obtain the complete **3D position** including ori<EFBFBD>ntation and direction vectors, consult the @{#UNIT.GetPositionVec3}() method respectively.
-- If you want to obtain the complete **3D position** including orientation and direction vectors, consult the @{#UNIT.GetPositionVec3}() method respectively.
--
-- ## Test if alive
--
@ -87,6 +89,7 @@
-- @field #UNIT UNIT
UNIT = {
ClassName="UNIT",
UnitName=nil,
}
@ -103,12 +106,18 @@ UNIT = {
--- Create a new UNIT from DCSUnit.
-- @param #UNIT self
-- @param #string UnitName The name of the DCS unit.
-- @return #UNIT
-- @return #UNIT self
function UNIT:Register( UnitName )
-- Inherit CONTROLLABLE.
local self = BASE:Inherit( self, CONTROLLABLE:New( UnitName ) )
-- Set unit name.
self.UnitName = UnitName
-- Set event prio.
self:SetEventPriority( 3 )
return self
end
@ -145,8 +154,8 @@ function UNIT:Name()
return self.UnitName
end
--- @param #UNIT self
--- Get the DCS unit object.
-- @param #UNIT self
-- @return DCS#Unit
function UNIT:GetDCSObject()
@ -373,6 +382,32 @@ function UNIT:GetPlayerName()
end
--- Checks is the unit is a *Player* or *Client* slot.
-- @param #UNIT self
-- @return #boolean If true, unit is a player or client aircraft
function UNIT:IsClient()
if _DATABASE.CLIENTS[self.UnitName] then
return true
end
return false
end
--- Get the CLIENT of the unit
-- @param #UNIT self
-- @return Wrapper.Client#CLIENT
function UNIT:GetClient()
local client=_DATABASE.CLIENTS[self.UnitName]
if client then
return client
end
return nil
end
--- Returns the unit's number in the group.
-- The number is the same number the unit has in ME.
-- It may not be changed during the mission.
@ -726,6 +761,7 @@ function UNIT:GetFuel()
return nil
end
--- Returns a list of one @{Wrapper.Unit}.
-- @param #UNIT self
-- @return #list<Wrapper.Unit#UNIT> A list of one @{Wrapper.Unit}.
@ -1139,8 +1175,9 @@ end
--- Returns true if the UNIT is in the air.
-- @param #UNIT self
-- @param #boolean NoHeloCheck If true, no additonal checks for helos are performed.
-- @return #boolean Return true if in the air or #nil if the UNIT is not existing or alive.
function UNIT:InAir()
function UNIT:InAir(NoHeloCheck)
self:F2( self.UnitName )
-- Get DCS unit object.
@ -1150,14 +1187,14 @@ function UNIT:InAir()
-- Get DCS result of whether unit is in air or not.
local UnitInAir = DCSUnit:inAir()
-- Get unit category.
local UnitCategory = DCSUnit:getDesc().category
-- If DCS says that it is in air, check if this is really the case, since we might have landed on a building where inAir()=true but actually is not.
-- This is a workaround since DCS currently does not acknoledge that helos land on buildings.
-- Note however, that the velocity check will fail if the ground is moving, e.g. on an aircraft carrier!
if UnitInAir==true and UnitCategory == Unit.Category.HELICOPTER then
if UnitInAir==true and UnitCategory == Unit.Category.HELICOPTER and (not NoHeloCheck) then
local VelocityVec3 = DCSUnit:getVelocity()
local Velocity = UTILS.VecNorm(VelocityVec3)
local Coordinate = DCSUnit:getPoint()
@ -1357,3 +1394,23 @@ function UNIT:GetTemplateFuel()
return nil
end
--- GROUND - Switch on/off radar emissions of a unit.
-- @param #UNIT self
-- @param #boolean switch If true, emission is enabled. If false, emission is disabled.
-- @return #UNIT self
function UNIT:EnableEmission(switch)
self:F2( self.UnitName )
local switch = switch or false
local DCSUnit = self:GetDCSObject()
if DCSUnit then
DCSUnit:enableEmission(switch)
end
return self
end

View File

@ -1,11 +1,14 @@
Utilities/Enums.lua
Utilities/Routines.lua
Utilities/Utils.lua
Utilities/Enums.lua
Utilities/Profiler.lua
Utilities/Templates.lua
Utilities/STTS.lua
Core/Base.lua
Core/Beacon.lua
Core/UserFlag.lua
Core/UserSound.lua
Core/Report.lua
Core/Scheduler.lua
Core/ScheduleDispatcher.lua
@ -13,13 +16,13 @@ Core/Event.lua
Core/Settings.lua
Core/Menu.lua
Core/Zone.lua
Core/Zone_Detection.lua
Core/Database.lua
Core/Set.lua
Core/Point.lua
Core/Velocity.lua
Core/Message.lua
Core/Fsm.lua
Core/Radio.lua
Core/Spawn.lua
Core/SpawnStatic.lua
Core/Timer.lua
@ -52,6 +55,7 @@ Functional/Escort.lua
Functional/MissileTrainer.lua
Functional/ATC_Ground.lua
Functional/Detection.lua
Functional/DetectionZones.lua
Functional/Designate.lua
Functional/RAT.lua
Functional/Range.lua
@ -63,6 +67,8 @@ Functional/Suppression.lua
Functional/PseudoATC.lua
Functional/Warehouse.lua
Functional/Fox.lua
Functional/Mantis.lua
Functional/Shorad.lua
Ops/Airboss.lua
Ops/RecoveryTanker.lua
@ -77,40 +83,53 @@ Ops/AirWing.lua
Ops/Intelligence.lua
Ops/WingCommander.lua
Ops/ChiefOfStaff.lua
Ops/CSAR.lua
Ops/CTLD.lua
AI/AI_Balancer.lua
AI/AI_Air.lua
AI/AI_A2A.lua
AI/AI_Air_Patrol.lua
AI/AI_Air_Engage.lua
AI/AI_A2A_Patrol.lua
AI/AI_A2A_Cap.lua
AI/AI_A2A_Gci.lua
AI/AI_A2A_Dispatcher.lua
AI/AI_A2G.lua
AI/AI_A2G_Engage.lua
AI/AI_A2G_BAI.lua
AI/AI_A2G_CAS.lua
AI/AI_A2G_SEAD.lua
AI/AI_A2G_Patrol.lua
AI/AI_A2G_Dispatcher.lua
AI/AI_Patrol.lua
AI/AI_Cap.lua
AI/AI_Cas.lua
AI/AI_Bai.lua
AI/AI_CAP.lua
AI/AI_CAS.lua
AI/AI_BAI.lua
AI/AI_Formation.lua
AI/AI_Escort.lua
AI/AI_Escort_Request.lua
AI/AI_Escort_Dispatcher.lua
AI/AI_Escort_Dispatcher_Request.lua
AI/AI_Cargo.lua
AI/AI_Cargo_APC.lua
AI/AI_Cargo_Helicopter.lua
AI/AI_Cargo_Airplane.lua
AI/AI_Cargo_Ship.lua
AI/AI_Cargo_Dispatcher.lua
AI/AI_Cargo_Dispatcher_APC.lua
AI/AI_Cargo_Dispatcher_Helicopter.lua
AI/AI_Cargo_Dispatcher_Airplane.lua
AI/AI_Cargo_Dispatcher_Ship.lua
Actions/Act_Assign.lua
Actions/Act_Route.lua
Actions/Act_Account.lua
Actions/Act_Assist.lua
Sound/UserSound.lua
Sound/SoundOutput.lua
Sound/Radio.lua
Sound/RadioQueue.lua
Sound/RadioSpeech.lua
Sound/SRS.lua
Tasking/CommandCenter.lua
Tasking/Mission.lua
Tasking/Task.lua
@ -121,10 +140,11 @@ Tasking/Task_A2G_Dispatcher.lua
Tasking/Task_A2G.lua
Tasking/Task_A2A_Dispatcher.lua
Tasking/Task_A2A.lua
Tasking/Task_Cargo.lua
Tasking/Task_CARGO.lua
Tasking/Task_Cargo_Transport.lua
Tasking/Task_Cargo_CSAR.lua
Tasking/Task_Cargo_Dispatcher.lua
Tasking/TaskZoneCapture.lua
Tasking/Task_Capture_Zone.lua
Tasking/Task_Capture_Dispatcher.lua
Globals.lua

View File

@ -1,3 +1,5 @@
[![Build status](https://ci.appveyor.com/api/projects/status/1y8nfmx7lwsn33tt?svg=true)](https://ci.appveyor.com/project/Applevangelist/MOOSE)
# MOOSE framework
MOOSE is a **M**ission **O**bject **O**riented **S**cripting **E**nvironment, and is meant for mission designers in DCS World.
@ -28,7 +30,7 @@ This repository contains the source lua code of the MOOSE framework.
### [MOOSE_INCLUDE](https://github.com/FlightControl-Master/MOOSE_INCLUDE) - For use and generated
This repository contains the Moose.lua file to be included within your missions.
This repository contains the Moose.lua file to be included within your missions. Note that the Moose\_.lua is technically the same as Moose.lua, but without any commentary or unnecessary whitespace in it. You only need to load **one** of those at the beginning of your mission.
### [MOOSE_DOCS](https://github.com/FlightControl-Master/MOOSE_DOCS) - Not for use
@ -50,9 +52,7 @@ This repository contains all the demonstration missions in packed format (*.miz)
This repository contains all the demonstration missions in unpacked format. That means that there is no .miz file included, but all the .miz contents are unpacked.
## [MOOSE Web Site](https://flightcontrol-master.github.io/MOOSE_DOCS/)
Documentation on the MOOSE class hierarchy, usage guides and background information can be found here for normal users, beta testers and contributors.