diff --git a/.appveyor/appveyor.yml b/.appveyor/appveyor.yml index 7fcd96a10..2af7fd529 100644 --- a/.appveyor/appveyor.yml +++ b/.appveyor/appveyor.yml @@ -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" diff --git a/.gitignore b/.gitignore index 5e42c0c93..c84f22bdf 100644 --- a/.gitignore +++ b/.gitignore @@ -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 ################# diff --git a/.luacheckrc b/.luacheckrc new file mode 100644 index 000000000..b29cbbc22 --- /dev/null +++ b/.luacheckrc @@ -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. +} diff --git a/Moose Development/Moose/AI/AI_A2A_Cap.lua b/Moose Development/Moose/AI/AI_A2A_Cap.lua index 08062cc11..f86bed9c0 100644 --- a/Moose Development/Moose/AI/AI_A2A_Cap.lua +++ b/Moose Development/Moose/AI/AI_A2A_Cap.lua @@ -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 diff --git a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua index 05ef48758..b7b3b641a 100644 --- a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua @@ -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 .. " колеÑ�а вверх.", 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 .. ", возвращаÑ�Ñ�ÑŒ на базу.", 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 .. ", захватывающие Ñ�амолеты в поÑ�адка на базу.", DefenderGroup ) end if Action and Action == "Destroy" then @@ -4523,5 +4538,5 @@ do return self end - + end diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua index 91d990afc..df2bafbf0 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -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 diff --git a/Moose Development/Moose/AI/AI_Air.lua b/Moose Development/Moose/AI/AI_Air.lua index 213e58757..6c8c9579c 100644 --- a/Moose Development/Moose/AI/AI_Air.lua +++ b/Moose Development/Moose/AI/AI_Air.lua @@ -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() diff --git a/Moose Development/Moose/AI/AI_Air_Engage.lua b/Moose Development/Moose/AI/AI_Air_Engage.lua index 2679d472d..c9f6bc562 100644 --- a/Moose Development/Moose/AI/AI_Air_Engage.lua +++ b/Moose Development/Moose/AI/AI_Air_Engage.lua @@ -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 diff --git a/Moose Development/Moose/AI/AI_Cargo.lua b/Moose Development/Moose/AI/AI_Cargo.lua index 586c0ab51..5bcd94437 100644 --- a/Moose Development/Moose/AI/AI_Cargo.lua +++ b/Moose Development/Moose/AI/AI_Cargo.lua @@ -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 diff --git a/Moose Development/Moose/AI/AI_Cargo_APC.lua b/Moose Development/Moose/AI/AI_Cargo_APC.lua index c20475711..bee08e1b1 100644 --- a/Moose Development/Moose/AI/AI_Cargo_APC.lua +++ b/Moose Development/Moose/AI/AI_Cargo_APC.lua @@ -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 diff --git a/Moose Development/Moose/AI/AI_Cargo_Airplane.lua b/Moose Development/Moose/AI/AI_Cargo_Airplane.lua index 2b3277bde..ede3d20bd 100644 --- a/Moose Development/Moose/AI/AI_Cargo_Airplane.lua +++ b/Moose Development/Moose/AI/AI_Cargo_Airplane.lua @@ -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. diff --git a/Moose Development/Moose/AI/AI_Cargo_Dispatcher.lua b/Moose Development/Moose/AI/AI_Cargo_Dispatcher.lua index 424a0f814..ec725d5f8 100644 --- a/Moose Development/Moose/AI/AI_Cargo_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_Cargo_Dispatcher.lua @@ -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 diff --git a/Moose Development/Moose/AI/AI_Cargo_Dispatcher_APC.lua b/Moose Development/Moose/AI/AI_Cargo_Dispatcher_APC.lua index b4bef21bc..cd530ab24 100644 --- a/Moose Development/Moose/AI/AI_Cargo_Dispatcher_APC.lua +++ b/Moose Development/Moose/AI/AI_Cargo_Dispatcher_APC.lua @@ -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 \ No newline at end of file diff --git a/Moose Development/Moose/AI/AI_Cargo_Dispatcher_Ship.lua b/Moose Development/Moose/AI/AI_Cargo_Dispatcher_Ship.lua new file mode 100644 index 000000000..70f325f62 --- /dev/null +++ b/Moose Development/Moose/AI/AI_Cargo_Dispatcher_Ship.lua @@ -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 \ No newline at end of file diff --git a/Moose Development/Moose/AI/AI_Cargo_Helicopter.lua b/Moose Development/Moose/AI/AI_Cargo_Helicopter.lua index 4b838fde5..3a83a48ae 100644 --- a/Moose Development/Moose/AI/AI_Cargo_Helicopter.lua +++ b/Moose Development/Moose/AI/AI_Cargo_Helicopter.lua @@ -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 diff --git a/Moose Development/Moose/AI/AI_Cargo_Ship.lua b/Moose Development/Moose/AI/AI_Cargo_Ship.lua new file mode 100644 index 000000000..37c7da57a --- /dev/null +++ b/Moose Development/Moose/AI/AI_Cargo_Ship.lua @@ -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 diff --git a/Moose Development/Moose/AI/AI_Formation.lua b/Moose Development/Moose/AI/AI_Formation.lua index 17c1b5345..aaf4fbe54 100644 --- a/Moose Development/Moose/AI/AI_Formation.lua +++ b/Moose Development/Moose/AI/AI_Formation.lua @@ -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() diff --git a/Moose Development/Moose/Actions/Act_Route.lua b/Moose Development/Moose/Actions/Act_Route.lua index f2c5ebcda..7f075b420 100644 --- a/Moose Development/Moose/Actions/Act_Route.lua +++ b/Moose Development/Moose/Actions/Act_Route.lua @@ -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 diff --git a/Moose Development/Moose/Cargo/Cargo.lua b/Moose Development/Moose/Cargo/Cargo.lua index 6a06e52f6..df796aed1 100644 --- a/Moose Development/Moose/Cargo/Cargo.lua +++ b/Moose Development/Moose/Cargo/Cargo.lua @@ -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 diff --git a/Moose Development/Moose/Cargo/CargoGroup.lua b/Moose Development/Moose/Cargo/CargoGroup.lua index b64d205c2..2d59f75d3 100644 --- a/Moose Development/Moose/Cargo/CargoGroup.lua +++ b/Moose Development/Moose/Cargo/CargoGroup.lua @@ -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 ) diff --git a/Moose Development/Moose/Cargo/CargoUnit.lua b/Moose Development/Moose/Cargo/CargoUnit.lua index 61319476d..92baf40a4 100644 --- a/Moose Development/Moose/Cargo/CargoUnit.lua +++ b/Moose Development/Moose/Cargo/CargoUnit.lua @@ -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 diff --git a/Moose Development/Moose/Core/Base.lua b/Moose Development/Moose/Core/Base.lua index fe186131a..e085f0081 100644 --- a/Moose Development/Moose/Core/Base.lua +++ b/Moose Development/Moose/Core/Base.lua @@ -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 diff --git a/Moose Development/Moose/Core/Beacon.lua b/Moose Development/Moose/Core/Beacon.lua new file mode 100644 index 000000000..37dba8c76 --- /dev/null +++ b/Moose Development/Moose/Core/Beacon.lua @@ -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 \ No newline at end of file diff --git a/Moose Development/Moose/Core/Database.lua b/Moose Development/Moose/Core/Database.lua index 7babe2481..df836f7dd 100644 --- a/Moose Development/Moose/Core/Database.lua +++ b/Moose Development/Moose/Core/Database.lua @@ -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 diff --git a/Moose Development/Moose/Core/Event.lua b/Moose Development/Moose/Core/Event.lua index 1f0b99b0a..02dab0afc 100644 --- a/Moose Development/Moose/Core/Event.lua +++ b/Moose Development/Moose/Core/Event.lua @@ -1,98 +1,98 @@ --- **Core** - Models DCS event dispatching using a publish-subscribe model. --- +-- -- === --- +-- -- ## Features: --- +-- -- * Capture DCS events and dispatch them to the subscribed objects. -- * Generate DCS events to the subscribed objects from within the code. --- +-- -- === --- +-- -- # Event Handling Overview --- +-- -- ![Objects](..\Presentations\EVENT\Dia2.JPG) --- +-- -- Within a running mission, various DCS events occur. Units are dynamically created, crash, die, shoot stuff, get hit etc. -- This module provides a mechanism to dispatch those events occuring within your running mission, to the different objects orchestrating your mission. --- +-- -- ![Objects](..\Presentations\EVENT\Dia3.JPG) --- +-- -- Objects can subscribe to different events. The Event dispatcher will publish the received DCS events to the subscribed MOOSE objects, in a specified order. -- In this way, the subscribed MOOSE objects are kept in sync with your evolving running mission. --- +-- -- ## 1. Event Dispatching --- +-- -- ![Objects](..\Presentations\EVENT\Dia4.JPG) --- --- The _EVENTDISPATCHER object is automatically created within MOOSE, --- and handles the dispatching of DCS Events occurring --- in the simulator to the subscribed objects +-- +-- The _EVENTDISPATCHER object is automatically created within MOOSE, +-- and handles the dispatching of DCS Events occurring +-- in the simulator to the subscribed objects -- in the correct processing order. -- -- ![Objects](..\Presentations\EVENT\Dia5.JPG) --- +-- -- There are 5 levels of kind of objects that the _EVENTDISPATCHER services: --- +-- -- * _DATABASE object: The core of the MOOSE objects. Any object that is created, deleted or updated, is done in this database. -- * SET_ derived classes: Subsets of the _DATABASE object. These subsets are updated by the _EVENTDISPATCHER as the second priority. -- * UNIT objects: UNIT objects can subscribe to DCS events. Each DCS event will be directly published to teh subscribed UNIT object. -- * GROUP objects: GROUP objects can subscribe to DCS events. Each DCS event will be directly published to the subscribed GROUP object. -- * Any other object: Various other objects can subscribe to DCS events. Each DCS event triggered will be published to each subscribed object. --- +-- -- ![Objects](..\Presentations\EVENT\Dia6.JPG) --- +-- -- For most DCS events, the above order of updating will be followed. --- +-- -- ![Objects](..\Presentations\EVENT\Dia7.JPG) --- +-- -- But for some DCS events, the publishing order is reversed. This is due to the fact that objects need to be **erased** instead of added. --- +-- -- # 2. Event Handling --- +-- -- ![Objects](..\Presentations\EVENT\Dia8.JPG) --- +-- -- The actual event subscribing and handling is not facilitated through the _EVENTDISPATCHER, but it is done through the @{BASE} class, @{UNIT} class and @{GROUP} class. -- The _EVENTDISPATCHER is a component that is quietly working in the background of MOOSE. --- +-- -- ![Objects](..\Presentations\EVENT\Dia9.JPG) --- --- The BASE class provides methods to catch DCS Events. These are events that are triggered from within the DCS simulator, +-- +-- The BASE class provides methods to catch DCS Events. These are events that are triggered from within the DCS simulator, -- and handled through lua scripting. MOOSE provides an encapsulation to handle these events more efficiently. --- +-- -- ## 2.1. Subscribe to / Unsubscribe from DCS Events. --- +-- -- At first, the mission designer will need to **Subscribe** to a specific DCS event for the class. -- So, when the DCS event occurs, the class will be notified of that event. -- There are two functions which you use to subscribe to or unsubscribe from an event. --- +-- -- * @{Core.Base#BASE.HandleEvent}(): Subscribe to a DCS Event. -- * @{Core.Base#BASE.UnHandleEvent}(): Unsubscribe from a DCS Event. --- +-- -- Note that for a UNIT, the event will be handled **for that UNIT only**! -- Note that for a GROUP, the event will be handled **for all the UNITs in that GROUP only**! --- +-- -- For all objects of other classes, the subscribed events will be handled for **all UNITs within the Mission**! --- So if a UNIT within the mission has the subscribed event for that object, +-- So if a UNIT within the mission has the subscribed event for that object, -- then the object event handler will receive the event for that UNIT! --- +-- -- ## 2.2 Event Handling of DCS Events --- +-- -- Once the class is subscribed to the event, an **Event Handling** method on the object or class needs to be written that will be called -- when the DCS event occurs. The Event Handling method receives an @{Core.Event#EVENTDATA} structure, which contains a lot of information -- about the event that occurred. --- --- Find below an example of the prototype how to write an event handling function for two units: +-- +-- Find below an example of the prototype how to write an event handling function for two units: -- -- local Tank1 = UNIT:FindByName( "Tank A" ) -- local Tank2 = UNIT:FindByName( "Tank B" ) --- +-- -- -- Here we subscribe to the Dead events. So, if one of these tanks dies, the Tank1 or Tank2 objects will be notified. -- Tank1:HandleEvent( EVENTS.Dead ) -- Tank2:HandleEvent( EVENTS.Dead ) --- +-- -- --- This function is an Event Handling function that will be called when Tank1 is Dead. --- -- @param Wrapper.Unit#UNIT self +-- -- @param Wrapper.Unit#UNIT self -- -- @param Core.Event#EVENTDATA EventData -- function Tank1:OnEventDead( EventData ) -- @@ -100,73 +100,73 @@ -- end -- -- --- This function is an Event Handling function that will be called when Tank2 is Dead. --- -- @param Wrapper.Unit#UNIT self +-- -- @param Wrapper.Unit#UNIT self -- -- @param Core.Event#EVENTDATA EventData -- function Tank2:OnEventDead( EventData ) -- -- self:SmokeBlue() -- end --- +-- -- ## 2.3 Event Handling methods that are automatically called upon subscribed DCS events. --- +-- -- ![Objects](..\Presentations\EVENT\Dia10.JPG) --- +-- -- The following list outlines which EVENTS item in the structure corresponds to which Event Handling method. -- Always ensure that your event handling methods align with the events being subscribed to, or nothing will be executed. --- +-- -- # 3. EVENTS type --- --- The EVENTS structure contains names for all the different DCS events that objects can subscribe to using the +-- +-- The EVENTS structure contains names for all the different DCS events that objects can subscribe to using the -- @{Core.Base#BASE.HandleEvent}() method. --- +-- -- # 4. EVENTDATA type --- --- The @{Core.Event#EVENTDATA} structure contains all the fields that are populated with event information before +-- +-- The @{Core.Event#EVENTDATA} structure contains all the fields that are populated with event information before -- an Event Handler method is being called by the event dispatcher. -- The Event Handler received the EVENTDATA object as a parameter, and can be used to investigate further the different events. -- There are basically 4 main categories of information stored in the EVENTDATA structure: --- +-- -- * Initiator Unit data: Several fields documenting the initiator unit related to the event. -- * Target Unit data: Several fields documenting the target unit related to the event. -- * Weapon data: Certain events populate weapon information. -- * Place data: Certain events populate place information. --- +-- -- --- This function is an Event Handling function that will be called when Tank1 is Dead. -- -- EventData is an EVENTDATA structure. -- -- We use the EventData.IniUnit to smoke the tank Green. --- -- @param Wrapper.Unit#UNIT self +-- -- @param Wrapper.Unit#UNIT self -- -- @param Core.Event#EVENTDATA EventData -- function Tank1:OnEventDead( EventData ) -- -- EventData.IniUnit:SmokeGreen() -- end --- --- +-- +-- -- Find below an overview which events populate which information categories: --- +-- -- ![Objects](..\Presentations\EVENT\Dia14.JPG) --- --- **IMPORTANT NOTE:** Some events can involve not just UNIT objects, but also STATIC objects!!! +-- +-- **IMPORTANT NOTE:** Some events can involve not just UNIT objects, but also STATIC objects!!! -- In that case the initiator or target unit fields will refer to a STATIC object! -- In case a STATIC object is involved, the documentation indicates which fields will and won't not be populated. -- The fields **IniObjectCategory** and **TgtObjectCategory** contain the indicator which **kind of object is involved** in the event. -- You can use the enumerator **Object.Category.UNIT** and **Object.Category.STATIC** to check on IniObjectCategory and TgtObjectCategory. -- Example code snippet: --- +-- -- if Event.IniObjectCategory == Object.Category.UNIT then -- ... -- end -- if Event.IniObjectCategory == Object.Category.STATIC then -- ... --- end --- +-- end +-- -- When a static object is involved in the event, the Group and Player fields won't be populated. --- +-- -- === --- +-- -- ### Author: **FlightControl** --- ### Contributions: --- +-- ### Contributions: +-- -- === -- -- @module Core.Event @@ -192,6 +192,7 @@ world.event.S_EVENT_DELETE_ZONE = world.event.S_EVENT_MAX + 1003 world.event.S_EVENT_NEW_ZONE_GOAL = world.event.S_EVENT_MAX + 1004 world.event.S_EVENT_DELETE_ZONE_GOAL = world.event.S_EVENT_MAX + 1005 world.event.S_EVENT_REMOVE_UNIT = world.event.S_EVENT_MAX + 1006 +world.event.S_EVENT_PLAYER_ENTER_AIRCRAFT = world.event.S_EVENT_MAX + 1007 --- The different types of events supported by MOOSE. @@ -233,23 +234,31 @@ EVENTS = { NewZoneGoal = world.event.S_EVENT_NEW_ZONE_GOAL, DeleteZoneGoal = world.event.S_EVENT_DELETE_ZONE_GOAL, RemoveUnit = world.event.S_EVENT_REMOVE_UNIT, + PlayerEnterAircraft = world.event.S_EVENT_PLAYER_ENTER_AIRCRAFT, -- Added with DCS 2.5.6 - DetailedFailure = world.event.S_EVENT_DETAILED_FAILURE or -1, --We set this to -1 for backward compatibility to DCS 2.5.5 and earlier - Kill = world.event.S_EVENT_KILL or -1, - Score = world.event.S_EVENT_SCORE or -1, - UnitLost = world.event.S_EVENT_UNIT_LOST or -1, - LandingAfterEjection = world.event.S_EVENT_LANDING_AFTER_EJECTION or -1, + DetailedFailure = world.event.S_EVENT_DETAILED_FAILURE or -1, --We set this to -1 for backward compatibility to DCS 2.5.5 and earlier + Kill = world.event.S_EVENT_KILL or -1, + Score = world.event.S_EVENT_SCORE or -1, + UnitLost = world.event.S_EVENT_UNIT_LOST or -1, + LandingAfterEjection = world.event.S_EVENT_LANDING_AFTER_EJECTION or -1, + -- Added with DCS 2.7.0 + ParatrooperLanding = world.event.S_EVENT_PARATROOPER_LENDING or -1, + DiscardChairAfterEjection = world.event.S_EVENT_DISCARD_CHAIR_AFTER_EJECTION or -1, + WeaponAdd = world.event.S_EVENT_WEAPON_ADD or -1, + TriggerZone = world.event.S_EVENT_TRIGGER_ZONE or -1, + LandingQualityMark = world.event.S_EVENT_LANDING_QUALITY_MARK or -1, + BDA = world.event.S_EVENT_BDA or -1, } --- The Event structure -- Note that at the beginning of each field description, there is an indication which field will be populated depending on the object type involved in the Event: --- +-- -- * A (Object.Category.)UNIT : A UNIT object type is involved in the Event. --- * A (Object.Category.)STATIC : A STATIC object type is involved in the Event.µ --- +-- * A (Object.Category.)STATIC : A STATIC object type is involved in the Event. +-- -- @type EVENTDATA -- @field #number id The identifier of the event. --- +-- -- @field DCS#Unit initiator (UNIT/STATIC/SCENERY) The initiating @{DCS#Unit} or @{DCS#StaticObject}. -- @field DCS#Object.Category IniObjectCategory (UNIT/STATIC/SCENERY) The initiator object category ( Object.Category.UNIT or Object.Category.STATIC ). -- @field DCS#Unit IniDCSUnit (UNIT/STATIC) The initiating @{DCS#Unit} or @{DCS#StaticObject}. @@ -264,7 +273,7 @@ EVENTS = { -- @field DCS#coalition.side IniCoalition (UNIT) The coalition of the initiator. -- @field DCS#Unit.Category IniCategory (UNIT) The category of the initiator. -- @field #string IniTypeName (UNIT) The type name of the initiator. --- +-- -- @field DCS#Unit target (UNIT/STATIC) The target @{DCS#Unit} or @{DCS#StaticObject}. -- @field DCS#Object.Category TgtObjectCategory (UNIT/STATIC) The target object category ( Object.Category.UNIT or Object.Category.STATIC ). -- @field DCS#Unit TgtDCSUnit (UNIT/STATIC) The target @{DCS#Unit} or @{DCS#StaticObject}. @@ -279,19 +288,19 @@ EVENTS = { -- @field DCS#coalition.side TgtCoalition (UNIT) The coalition of the target. -- @field DCS#Unit.Category TgtCategory (UNIT) The category of the target. -- @field #string TgtTypeName (UNIT) The type name of the target. --- +-- -- @field DCS#Airbase place The @{DCS#Airbase} -- @field Wrapper.Airbase#AIRBASE Place The MOOSE airbase object. -- @field #string PlaceName The name of the airbase. --- +-- -- @field #table weapon The weapon used during the event. -- @field #table Weapon -- @field #string WeaponName Name of the weapon. -- @field DCS#Unit WeaponTgtDCSUnit Target DCS unit of the weapon. --- +-- -- @field Cargo.Cargo#CARGO Cargo The cargo object. -- @field #string CargoName The name of the cargo object. --- +-- -- @field Core.ZONE#ZONE Zone The zone object. -- @field #string ZoneName The name of the zone. @@ -302,219 +311,255 @@ local _EVENTMETA = { Order = 1, Side = "I", Event = "OnEventShot", - Text = "S_EVENT_SHOT" + Text = "S_EVENT_SHOT" }, [world.event.S_EVENT_HIT] = { Order = 1, Side = "T", Event = "OnEventHit", - Text = "S_EVENT_HIT" + Text = "S_EVENT_HIT" }, [world.event.S_EVENT_TAKEOFF] = { Order = 1, Side = "I", Event = "OnEventTakeoff", - Text = "S_EVENT_TAKEOFF" + Text = "S_EVENT_TAKEOFF" }, [world.event.S_EVENT_LAND] = { Order = 1, Side = "I", Event = "OnEventLand", - Text = "S_EVENT_LAND" + Text = "S_EVENT_LAND" }, [world.event.S_EVENT_CRASH] = { Order = -1, Side = "I", Event = "OnEventCrash", - Text = "S_EVENT_CRASH" + Text = "S_EVENT_CRASH" }, [world.event.S_EVENT_EJECTION] = { Order = 1, Side = "I", Event = "OnEventEjection", - Text = "S_EVENT_EJECTION" + Text = "S_EVENT_EJECTION" }, [world.event.S_EVENT_REFUELING] = { Order = 1, Side = "I", Event = "OnEventRefueling", - Text = "S_EVENT_REFUELING" + Text = "S_EVENT_REFUELING" }, [world.event.S_EVENT_DEAD] = { Order = -1, Side = "I", Event = "OnEventDead", - Text = "S_EVENT_DEAD" + Text = "S_EVENT_DEAD" }, [world.event.S_EVENT_PILOT_DEAD] = { Order = 1, Side = "I", Event = "OnEventPilotDead", - Text = "S_EVENT_PILOT_DEAD" + Text = "S_EVENT_PILOT_DEAD" }, [world.event.S_EVENT_BASE_CAPTURED] = { Order = 1, Side = "I", Event = "OnEventBaseCaptured", - Text = "S_EVENT_BASE_CAPTURED" + Text = "S_EVENT_BASE_CAPTURED" }, [world.event.S_EVENT_MISSION_START] = { Order = 1, Side = "N", Event = "OnEventMissionStart", - Text = "S_EVENT_MISSION_START" + Text = "S_EVENT_MISSION_START" }, [world.event.S_EVENT_MISSION_END] = { Order = 1, Side = "N", Event = "OnEventMissionEnd", - Text = "S_EVENT_MISSION_END" + Text = "S_EVENT_MISSION_END" }, [world.event.S_EVENT_TOOK_CONTROL] = { Order = 1, Side = "N", Event = "OnEventTookControl", - Text = "S_EVENT_TOOK_CONTROL" + Text = "S_EVENT_TOOK_CONTROL" }, [world.event.S_EVENT_REFUELING_STOP] = { Order = 1, Side = "I", Event = "OnEventRefuelingStop", - Text = "S_EVENT_REFUELING_STOP" + Text = "S_EVENT_REFUELING_STOP" }, [world.event.S_EVENT_BIRTH] = { Order = 1, Side = "I", Event = "OnEventBirth", - Text = "S_EVENT_BIRTH" + Text = "S_EVENT_BIRTH" }, [world.event.S_EVENT_HUMAN_FAILURE] = { Order = 1, Side = "I", Event = "OnEventHumanFailure", - Text = "S_EVENT_HUMAN_FAILURE" + Text = "S_EVENT_HUMAN_FAILURE" }, [world.event.S_EVENT_ENGINE_STARTUP] = { Order = 1, Side = "I", Event = "OnEventEngineStartup", - Text = "S_EVENT_ENGINE_STARTUP" + Text = "S_EVENT_ENGINE_STARTUP" }, [world.event.S_EVENT_ENGINE_SHUTDOWN] = { Order = 1, Side = "I", Event = "OnEventEngineShutdown", - Text = "S_EVENT_ENGINE_SHUTDOWN" + Text = "S_EVENT_ENGINE_SHUTDOWN" }, [world.event.S_EVENT_PLAYER_ENTER_UNIT] = { Order = 1, Side = "I", Event = "OnEventPlayerEnterUnit", - Text = "S_EVENT_PLAYER_ENTER_UNIT" + Text = "S_EVENT_PLAYER_ENTER_UNIT" }, [world.event.S_EVENT_PLAYER_LEAVE_UNIT] = { Order = -1, Side = "I", Event = "OnEventPlayerLeaveUnit", - Text = "S_EVENT_PLAYER_LEAVE_UNIT" + Text = "S_EVENT_PLAYER_LEAVE_UNIT" }, [world.event.S_EVENT_PLAYER_COMMENT] = { Order = 1, Side = "I", Event = "OnEventPlayerComment", - Text = "S_EVENT_PLAYER_COMMENT" + Text = "S_EVENT_PLAYER_COMMENT" }, [world.event.S_EVENT_SHOOTING_START] = { Order = 1, Side = "I", Event = "OnEventShootingStart", - Text = "S_EVENT_SHOOTING_START" + Text = "S_EVENT_SHOOTING_START" }, [world.event.S_EVENT_SHOOTING_END] = { Order = 1, Side = "I", Event = "OnEventShootingEnd", - Text = "S_EVENT_SHOOTING_END" + Text = "S_EVENT_SHOOTING_END" }, [world.event.S_EVENT_MARK_ADDED] = { Order = 1, Side = "I", Event = "OnEventMarkAdded", - Text = "S_EVENT_MARK_ADDED" + Text = "S_EVENT_MARK_ADDED" }, [world.event.S_EVENT_MARK_CHANGE] = { Order = 1, Side = "I", Event = "OnEventMarkChange", - Text = "S_EVENT_MARK_CHANGE" + Text = "S_EVENT_MARK_CHANGE" }, [world.event.S_EVENT_MARK_REMOVED] = { Order = 1, Side = "I", Event = "OnEventMarkRemoved", - Text = "S_EVENT_MARK_REMOVED" + Text = "S_EVENT_MARK_REMOVED" }, [EVENTS.NewCargo] = { Order = 1, Event = "OnEventNewCargo", - Text = "S_EVENT_NEW_CARGO" + Text = "S_EVENT_NEW_CARGO" }, [EVENTS.DeleteCargo] = { Order = 1, Event = "OnEventDeleteCargo", - Text = "S_EVENT_DELETE_CARGO" + Text = "S_EVENT_DELETE_CARGO" }, [EVENTS.NewZone] = { Order = 1, Event = "OnEventNewZone", - Text = "S_EVENT_NEW_ZONE" + Text = "S_EVENT_NEW_ZONE" }, [EVENTS.DeleteZone] = { Order = 1, Event = "OnEventDeleteZone", - Text = "S_EVENT_DELETE_ZONE" + Text = "S_EVENT_DELETE_ZONE" }, [EVENTS.NewZoneGoal] = { Order = 1, Event = "OnEventNewZoneGoal", - Text = "S_EVENT_NEW_ZONE_GOAL" + Text = "S_EVENT_NEW_ZONE_GOAL" }, [EVENTS.DeleteZoneGoal] = { Order = 1, Event = "OnEventDeleteZoneGoal", - Text = "S_EVENT_DELETE_ZONE_GOAL" + Text = "S_EVENT_DELETE_ZONE_GOAL" }, [EVENTS.RemoveUnit] = { Order = -1, Event = "OnEventRemoveUnit", - Text = "S_EVENT_REMOVE_UNIT" + Text = "S_EVENT_REMOVE_UNIT" + }, + [EVENTS.PlayerEnterAircraft] = { + Order = 1, + Event = "OnEventPlayerEnterAircraft", + Text = "S_EVENT_PLAYER_ENTER_AIRCRAFT" }, -- Added with DCS 2.5.6 [EVENTS.DetailedFailure] = { Order = 1, Event = "OnEventDetailedFailure", - Text = "S_EVENT_DETAILED_FAILURE" + Text = "S_EVENT_DETAILED_FAILURE" }, [EVENTS.Kill] = { Order = 1, Event = "OnEventKill", - Text = "S_EVENT_KILL" + Text = "S_EVENT_KILL" }, [EVENTS.Score] = { Order = 1, Event = "OnEventScore", - Text = "S_EVENT_SCORE" + Text = "S_EVENT_SCORE" }, [EVENTS.UnitLost] = { Order = 1, Event = "OnEventUnitLost", - Text = "S_EVENT_UNIT_LOST" + Text = "S_EVENT_UNIT_LOST" }, [EVENTS.LandingAfterEjection] = { Order = 1, Event = "OnEventLandingAfterEjection", - Text = "S_EVENT_LANDING_AFTER_EJECTION" - }, + Text = "S_EVENT_LANDING_AFTER_EJECTION" + }, + -- Added with DCS 2.7.0 + [EVENTS.ParatrooperLanding] = { + Order = 1, + Event = "OnEventParatrooperLanding", + Text = "S_EVENT_PARATROOPER_LENDING" + }, + [EVENTS.DiscardChairAfterEjection] = { + Order = 1, + Event = "OnEventDiscardChairAfterEjection", + Text = "S_EVENT_DISCARD_CHAIR_AFTER_EJECTION" + }, + [EVENTS.WeaponAdd] = { + Order = 1, + Event = "OnEventWeaponAdd", + Text = "S_EVENT_WEAPON_ADD" + }, + [EVENTS.TriggerZone] = { + Order = 1, + Event = "OnEventTriggerZone", + Text = "S_EVENT_TRIGGER_ZONE" + }, + [EVENTS.LandingQualityMark] = { + Order = 1, + Event = "OnEventLandingQualityMark", + Text = "S_EVENT_LANDING_QUALITYMARK" + }, + [EVENTS.BDA] = { + Order = 1, + Event = "OnEventBDA", + Text = "S_EVENT_BDA" + }, } @@ -529,10 +574,10 @@ function EVENT:New() -- Inherit base. local self = BASE:Inherit( self, BASE:New() ) - + -- Add world event handler. self.EventHandler = world.addEventHandler(self) - + return self end @@ -545,22 +590,22 @@ end function EVENT:Init( EventID, EventClass ) self:F3( { _EVENTMETA[EventID].Text, EventClass } ) - if not self.Events[EventID] then + if not self.Events[EventID] then -- Create a WEAK table to ensure that the garbage collector is cleaning the event links when the object usage is cleaned. self.Events[EventID] = {} end - + -- Each event has a subtable of EventClasses, ordered by EventPriority. local EventPriority = EventClass:GetEventPriority() - + if not self.Events[EventID][EventPriority] then self.Events[EventID][EventPriority] = setmetatable( {}, { __mode = "k" } ) - end + end if not self.Events[EventID][EventPriority][EventClass] then self.Events[EventID][EventPriority][EventClass] = {} end - + return self.Events[EventID][EventPriority][EventClass] end @@ -580,11 +625,11 @@ function EVENT:RemoveEvent( EventClass, EventID ) -- Events. self.Events = self.Events or {} self.Events[EventID] = self.Events[EventID] or {} - self.Events[EventID][EventPriority] = self.Events[EventID][EventPriority] or {} - + self.Events[EventID][EventPriority] = self.Events[EventID][EventPriority] or {} + -- Remove self.Events[EventID][EventPriority][EventClass] = nil - + return self end @@ -598,7 +643,7 @@ function EVENT:Reset( EventObject ) --R2.1 self:F( { "Resetting subscriptions for class: ", EventObject:GetClassNameAndID() } ) local EventPriority = EventObject:GetEventPriority() - + for EventID, EventData in pairs( self.Events ) do if self.EventsDead then if self.EventsDead[EventID] then @@ -620,14 +665,14 @@ end function EVENT:RemoveAll(EventClass) local EventClassName = EventClass:GetClassNameAndID() - + -- Get Event prio. local EventPriority = EventClass:GetEventPriority() - + for EventID, EventData in pairs( self.Events ) do self.Events[EventID][EventPriority][EventClass] = nil end - + return self end @@ -639,7 +684,7 @@ end -- @param #function EventFunction The function to be called when the event occurs for the unit. -- @param EventClass The instance of the class for which the event is. -- @param #function OnEventFunction --- @return #EVENT +-- @return #EVENT self function EVENT:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EventID ) self:F2( EventTemplate.name ) @@ -660,7 +705,7 @@ function EVENT:OnEventGeneric( EventFunction, EventClass, EventID ) local EventData = self:Init( EventID, EventClass ) EventData.EventFunction = EventFunction - + return self end @@ -686,8 +731,9 @@ end -- @param #string GroupName The name of the GROUP. -- @param #function EventFunction The function to be called when the event occurs for the GROUP. -- @param Core.Base#BASE EventClass The self instance of the class for which the event is. --- @param EventID --- @return #EVENT +-- @param #number EventID Event ID. +-- @param ... Optional arguments passed to the event function. +-- @return #EVENT self function EVENT:OnEventForGroup( GroupName, EventFunction, EventClass, EventID, ... ) local Event = self:Init( EventID, EventClass ) @@ -704,15 +750,15 @@ do -- OnBirth -- @param Wrapper.Group#GROUP EventGroup -- @param #function EventFunction The function to be called when the event occurs for the unit. -- @param EventClass The self instance of the class for which the event is. - -- @return #EVENT + -- @return #EVENT self function EVENT:OnBirthForTemplate( EventTemplate, EventFunction, EventClass ) self:F2( EventTemplate.name ) - + self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EVENTS.Birth ) - + return self end - + end do -- OnCrash @@ -725,16 +771,16 @@ do -- OnCrash -- @return #EVENT function EVENT:OnCrashForTemplate( EventTemplate, EventFunction, EventClass ) self:F2( EventTemplate.name ) - + self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EVENTS.Crash ) - + return self end end do -- OnDead - + --- Create an OnDead event handler for a group -- @param #EVENT self -- @param Wrapper.Group#GROUP EventGroup The GROUP object. @@ -743,12 +789,12 @@ do -- OnDead -- @return #EVENT self function EVENT:OnDeadForTemplate( EventTemplate, EventFunction, EventClass ) self:F2( EventTemplate.name ) - + self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EVENTS.Dead ) - + return self end - + end @@ -762,12 +808,12 @@ do -- OnLand -- @return #EVENT self function EVENT:OnLandForTemplate( EventTemplate, EventFunction, EventClass ) self:F2( EventTemplate.name ) - + self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EVENTS.Land ) - + return self end - + end do -- OnTakeOff @@ -780,12 +826,12 @@ do -- OnTakeOff -- @return #EVENT self function EVENT:OnTakeOffForTemplate( EventTemplate, EventFunction, EventClass ) self:F2( EventTemplate.name ) - + self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EVENTS.Takeoff ) - + return self end - + end do -- OnEngineShutDown @@ -798,12 +844,12 @@ do -- OnEngineShutDown -- @return #EVENT function EVENT:OnEngineShutDownForTemplate( EventTemplate, EventFunction, EventClass ) self:F2( EventTemplate.name ) - + self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EVENTS.EngineShutdown ) - + return self end - + end do -- Event Creation @@ -812,14 +858,14 @@ do -- Event Creation -- @param #EVENT self -- @param AI.AI_Cargo#AI_CARGO Cargo The Cargo created. function EVENT:CreateEventNewCargo( Cargo ) - self:I( { Cargo } ) - + self:F( { Cargo } ) + local Event = { id = EVENTS.NewCargo, time = timer.getTime(), cargo = Cargo, } - + world.onEvent( Event ) end @@ -828,13 +874,13 @@ do -- Event Creation -- @param AI.AI_Cargo#AI_CARGO Cargo The Cargo created. function EVENT:CreateEventDeleteCargo( Cargo ) self:F( { Cargo } ) - + local Event = { id = EVENTS.DeleteCargo, time = timer.getTime(), cargo = Cargo, } - + world.onEvent( Event ) end @@ -843,13 +889,13 @@ do -- Event Creation -- @param Core.Zone#ZONE_BASE Zone The Zone created. function EVENT:CreateEventNewZone( Zone ) self:F( { Zone } ) - + local Event = { id = EVENTS.NewZone, time = timer.getTime(), zone = Zone, } - + world.onEvent( Event ) end @@ -858,13 +904,13 @@ do -- Event Creation -- @param Core.Zone#ZONE_BASE Zone The Zone created. function EVENT:CreateEventDeleteZone( Zone ) self:F( { Zone } ) - + local Event = { id = EVENTS.DeleteZone, time = timer.getTime(), zone = Zone, } - + world.onEvent( Event ) end @@ -873,13 +919,13 @@ do -- Event Creation -- @param Core.Functional#ZONE_GOAL ZoneGoal The ZoneGoal created. function EVENT:CreateEventNewZoneGoal( ZoneGoal ) self:F( { ZoneGoal } ) - + local Event = { id = EVENTS.NewZoneGoal, time = timer.getTime(), ZoneGoal = ZoneGoal, } - + world.onEvent( Event ) end @@ -889,13 +935,13 @@ do -- Event Creation -- @param Core.ZoneGoal#ZONE_GOAL ZoneGoal The ZoneGoal created. function EVENT:CreateEventDeleteZoneGoal( ZoneGoal ) self:F( { ZoneGoal } ) - + local Event = { id = EVENTS.DeleteZoneGoal, time = timer.getTime(), ZoneGoal = ZoneGoal, } - + world.onEvent( Event ) end @@ -905,13 +951,28 @@ do -- Event Creation -- @param Wrapper.Unit#UNIT PlayerUnit. function EVENT:CreateEventPlayerEnterUnit( PlayerUnit ) self:F( { PlayerUnit } ) - + local Event = { id = EVENTS.PlayerEnterUnit, time = timer.getTime(), initiator = PlayerUnit:GetDCSObject() } - + + world.onEvent( Event ) + end + + --- Creation of a S_EVENT_PLAYER_ENTER_AIRCRAFT event. + -- @param #EVENT self + -- @param Wrapper.Unit#UNIT PlayerUnit The aircraft unit the player entered. + function EVENT:CreateEventPlayerEnterAircraft( PlayerUnit ) + self:F( { PlayerUnit } ) + + local Event = { + id = EVENTS.PlayerEnterAircraft, + time = timer.getTime(), + initiator = PlayerUnit:GetDCSObject() + } + world.onEvent( Event ) end @@ -928,31 +989,27 @@ function EVENT:onEvent( Event ) if BASE.Debug ~= nil then env.info( debug.traceback() ) end - + return errmsg end -- Get event meta data. local EventMeta = _EVENTMETA[Event.id] - + -- Check if this is a known event? if EventMeta then - - if self and - self.Events and - self.Events[Event.id] and - self.MissionEnd == false and - ( Event.initiator ~= nil or ( Event.initiator == nil and Event.id ~= EVENTS.PlayerLeaveUnit ) ) then - + + if self and self.Events and self.Events[Event.id] and self.MissionEnd==false and (Event.initiator~=nil or (Event.initiator==nil and Event.id~=EVENTS.PlayerLeaveUnit)) then + if Event.id and Event.id == EVENTS.MissionEnd then self.MissionEnd = true end - - if Event.initiator then - + + if Event.initiator then + Event.IniObjectCategory = Event.initiator:getCategory() - + if Event.IniObjectCategory == Object.Category.UNIT then Event.IniDCSUnit = Event.initiator Event.IniDCSUnitName = Event.IniDCSUnit:getName() @@ -976,11 +1033,12 @@ function EVENT:onEvent( Event ) Event.IniTypeName = Event.IniDCSUnit:getTypeName() Event.IniCategory = Event.IniDCSUnit:getDesc().category end - + if Event.IniObjectCategory == Object.Category.STATIC then + if Event.id==31 then - --env.info("FF event 31") - -- Event.initiator is a Static object representing the pilot. But getName() error due to DCS bug. + + -- Event.initiator is a Static object representing the pilot. But getName() errors due to DCS bug. Event.IniDCSUnit = Event.initiator local ID=Event.initiator.id_ Event.IniDCSUnitName = string.format("Ejected Pilot ID %s", tostring(ID)) @@ -988,6 +1046,14 @@ function EVENT:onEvent( Event ) Event.IniCoalition = 0 Event.IniCategory = 0 Event.IniTypeName = "Ejected Pilot" + elseif Event.id == 33 then -- ejection seat discarded + Event.IniDCSUnit = Event.initiator + local ID=Event.initiator.id_ + Event.IniDCSUnitName = string.format("Ejection Seat ID %s", tostring(ID)) + Event.IniUnitName = Event.IniDCSUnitName + Event.IniCoalition = 0 + Event.IniCategory = 0 + Event.IniTypeName = "Ejection Seat" else Event.IniDCSUnit = Event.initiator Event.IniDCSUnitName = Event.IniDCSUnit:getName() @@ -998,7 +1064,7 @@ function EVENT:onEvent( Event ) Event.IniTypeName = Event.IniDCSUnit:getTypeName() end end - + if Event.IniObjectCategory == Object.Category.CARGO then Event.IniDCSUnit = Event.initiator Event.IniDCSUnitName = Event.IniDCSUnit:getName() @@ -1008,7 +1074,7 @@ function EVENT:onEvent( Event ) Event.IniCategory = Event.IniDCSUnit:getDesc().category Event.IniTypeName = Event.IniDCSUnit:getTypeName() end - + if Event.IniObjectCategory == Object.Category.SCENERY then Event.IniDCSUnit = Event.initiator Event.IniDCSUnitName = Event.IniDCSUnit:getName() @@ -1017,7 +1083,7 @@ function EVENT:onEvent( Event ) Event.IniCategory = Event.IniDCSUnit:getDesc().category Event.IniTypeName = Event.initiator:isExist() and Event.IniDCSUnit:getTypeName() or "SCENERY" -- TODO: Bug fix for 2.1! end - + if Event.IniObjectCategory == Object.Category.BASE then Event.IniDCSUnit = Event.initiator Event.IniDCSUnitName = Event.IniDCSUnit:getName() @@ -1025,15 +1091,15 @@ function EVENT:onEvent( Event ) Event.IniUnit = AIRBASE:FindByName(Event.IniDCSUnitName) Event.IniCoalition = Event.IniDCSUnit:getCoalition() Event.IniCategory = Event.IniDCSUnit:getDesc().category - Event.IniTypeName = Event.IniDCSUnit:getTypeName() + Event.IniTypeName = Event.IniDCSUnit:getTypeName() end end - + if Event.target then - + Event.TgtObjectCategory = Event.target:getCategory() - - if Event.TgtObjectCategory == Object.Category.UNIT then + + if Event.TgtObjectCategory == Object.Category.UNIT then Event.TgtDCSUnit = Event.target Event.TgtDCSGroup = Event.TgtDCSUnit:getGroup() Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() @@ -1052,17 +1118,37 @@ function EVENT:onEvent( Event ) Event.TgtCategory = Event.TgtDCSUnit:getDesc().category Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() end - + if Event.TgtObjectCategory == Object.Category.STATIC then + -- get base data Event.TgtDCSUnit = Event.target - Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() - Event.TgtUnitName = Event.TgtDCSUnitName - Event.TgtUnit = STATIC:FindByName( Event.TgtDCSUnitName, false ) - Event.TgtCoalition = Event.TgtDCSUnit:getCoalition() - Event.TgtCategory = Event.TgtDCSUnit:getDesc().category - Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() + if Event.target:isExist() and Event.id ~= 33 then -- leave out ejected seat object + Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() + Event.TgtUnitName = Event.TgtDCSUnitName + Event.TgtUnit = STATIC:FindByName( Event.TgtDCSUnitName, false ) + Event.TgtCoalition = Event.TgtDCSUnit:getCoalition() + Event.TgtCategory = Event.TgtDCSUnit:getDesc().category + Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() + else + Event.TgtDCSUnitName = string.format("No target object for Event ID %s", tostring(Event.id)) + Event.TgtUnitName = Event.TgtDCSUnitName + Event.TgtUnit = nil + Event.TgtCoalition = 0 + Event.TgtCategory = 0 + if Event.id == 6 then + Event.TgtTypeName = "Ejected Pilot" + Event.TgtDCSUnitName = string.format("Ejected Pilot ID %s", tostring(Event.IniDCSUnitName)) + Event.TgtUnitName = Event.TgtDCSUnitName + elseif Event.id == 33 then + Event.TgtTypeName = "Ejection Seat" + Event.TgtDCSUnitName = string.format("Ejection Seat ID %s", tostring(Event.IniDCSUnitName)) + Event.TgtUnitName = Event.TgtDCSUnitName + else + Event.TgtTypeName = "Static" + end + end end - + if Event.TgtObjectCategory == Object.Category.SCENERY then Event.TgtDCSUnit = Event.target Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() @@ -1072,7 +1158,7 @@ function EVENT:onEvent( Event ) Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() end end - + if Event.weapon then Event.Weapon = Event.weapon Event.WeaponName = Event.Weapon:getTypeName() @@ -1083,20 +1169,20 @@ function EVENT:onEvent( Event ) Event.WeaponTypeName = Event.WeaponUNIT and Event.Weapon:getTypeName() --Event.WeaponTgtDCSUnit = Event.Weapon:getTarget() end - - -- Place should be given for takeoff and landing events as well as base captured. It should be a DCS airbase. - if Event.place then + + -- Place should be given for takeoff and landing events as well as base captured. It should be a DCS airbase. + if Event.place then if Event.id==EVENTS.LandingAfterEjection then -- Place is here the UNIT of which the pilot ejected. --local name=Event.place:getName() -- This returns a DCS error "Airbase doesn't exit" :( -- However, this is not a big thing, as the aircraft the pilot ejected from is usually long crashed before the ejected pilot touches the ground. --Event.Place=UNIT:Find(Event.place) - else + else Event.Place=AIRBASE:Find(Event.place) Event.PlaceName=Event.Place:GetName() end end - + -- Mark points. if Event.idx then Event.MarkID=Event.idx @@ -1106,80 +1192,80 @@ function EVENT:onEvent( Event ) Event.MarkCoalition=Event.coalition Event.MarkGroupID = Event.groupID end - + if Event.cargo then Event.Cargo = Event.cargo Event.CargoName = Event.cargo.Name end - + if Event.zone then Event.Zone = Event.zone Event.ZoneName = Event.zone.ZoneName end - + local PriorityOrder = EventMeta.Order local PriorityBegin = PriorityOrder == -1 and 5 or 1 local PriorityEnd = PriorityOrder == -1 and 1 or 5 - + if Event.IniObjectCategory ~= Object.Category.STATIC then self:F( { EventMeta.Text, Event, Event.IniDCSUnitName, Event.TgtDCSUnitName, PriorityOrder } ) end - + for EventPriority = PriorityBegin, PriorityEnd, PriorityOrder do - + if self.Events[Event.id][EventPriority] then - + -- Okay, we got the event from DCS. Now loop the SORTED self.EventSorted[] table for the received Event.id, and for each EventData registered, check if a function needs to be called. for EventClass, EventData in pairs( self.Events[Event.id][EventPriority] ) do - + --if Event.IniObjectCategory ~= Object.Category.STATIC then -- self:E( { "Evaluating: ", EventClass:GetClassNameAndID() } ) --end - + Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) Event.TgtGroup = GROUP:FindByName( Event.TgtDCSGroupName ) - + -- If the EventData is for a UNIT, the call directly the EventClass EventFunction for that UNIT. if EventData.EventUnit then - + -- So now the EventClass must be a UNIT class!!! We check if it is still "Alive". if EventClass:IsAlive() or - Event.id == EVENTS.PlayerEnterUnit or - Event.id == EVENTS.Crash or - Event.id == EVENTS.Dead or + Event.id == EVENTS.PlayerEnterUnit or + Event.id == EVENTS.Crash or + Event.id == EVENTS.Dead or Event.id == EVENTS.RemoveUnit then - + local UnitName = EventClass:GetName() - - if ( EventMeta.Side == "I" and UnitName == Event.IniDCSUnitName ) or + + if ( EventMeta.Side == "I" and UnitName == Event.IniDCSUnitName ) or ( EventMeta.Side == "T" and UnitName == Event.TgtDCSUnitName ) then - + -- First test if a EventFunction is Set, otherwise search for the default function if EventData.EventFunction then - + if Event.IniObjectCategory ~= 3 then self:F( { "Calling EventFunction for UNIT ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventPriority } ) end - - local Result, Value = xpcall( - function() - return EventData.EventFunction( EventClass, Event ) + + local Result, Value = xpcall( + function() + return EventData.EventFunction( EventClass, Event ) end, ErrorHandler ) - + else - + -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. local EventFunction = EventClass[ EventMeta.Event ] if EventFunction and type( EventFunction ) == "function" then - + -- Now call the default event function. if Event.IniObjectCategory ~= 3 then self:F( { "Calling " .. EventMeta.Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } ) end - - local Result, Value = xpcall( - function() - return EventFunction( EventClass, Event ) + + local Result, Value = xpcall( + function() + return EventFunction( EventClass, Event ) end, ErrorHandler ) end end @@ -1188,102 +1274,102 @@ function EVENT:onEvent( Event ) -- The EventClass is not alive anymore, we remove it from the EventHandlers... self:RemoveEvent( EventClass, Event.id ) end - + else - + --- If the EventData is for a GROUP, the call directly the EventClass EventFunction for the UNIT in that GROUP. if EventData.EventGroup then - + -- So now the EventClass must be a GROUP class!!! We check if it is still "Alive". if EventClass:IsAlive() or Event.id == EVENTS.PlayerEnterUnit or Event.id == EVENTS.Crash or Event.id == EVENTS.Dead or Event.id == EVENTS.RemoveUnit then - + -- We can get the name of the EventClass, which is now always a GROUP object. local GroupName = EventClass:GetName() - - if ( EventMeta.Side == "I" and GroupName == Event.IniDCSGroupName ) or + + if ( EventMeta.Side == "I" and GroupName == Event.IniDCSGroupName ) or ( EventMeta.Side == "T" and GroupName == Event.TgtDCSGroupName ) then - + -- First test if a EventFunction is Set, otherwise search for the default function if EventData.EventFunction then - + if Event.IniObjectCategory ~= 3 then self:F( { "Calling EventFunction for GROUP ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventPriority } ) end - - local Result, Value = xpcall( - function() - return EventData.EventFunction( EventClass, Event, unpack( EventData.Params ) ) + + local Result, Value = xpcall( + function() + return EventData.EventFunction( EventClass, Event, unpack( EventData.Params ) ) end, ErrorHandler ) - + else - + -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. local EventFunction = EventClass[ EventMeta.Event ] if EventFunction and type( EventFunction ) == "function" then - + -- Now call the default event function. if Event.IniObjectCategory ~= 3 then self:F( { "Calling " .. EventMeta.Event .. " for GROUP ", EventClass:GetClassNameAndID(), EventPriority } ) end - - local Result, Value = xpcall( - function() - return EventFunction( EventClass, Event, unpack( EventData.Params ) ) + + local Result, Value = xpcall( + function() + return EventFunction( EventClass, Event, unpack( EventData.Params ) ) end, ErrorHandler ) end end end else -- The EventClass is not alive anymore, we remove it from the EventHandlers... - --self:RemoveEvent( EventClass, Event.id ) + --self:RemoveEvent( EventClass, Event.id ) end else - + -- If the EventData is not bound to a specific unit, then call the EventClass EventFunction. -- Note that here the EventFunction will need to implement and determine the logic for the relevant source- or target unit, or weapon. if not EventData.EventUnit then - + -- First test if a EventFunction is Set, otherwise search for the default function if EventData.EventFunction then - + -- There is an EventFunction defined, so call the EventFunction. if Event.IniObjectCategory ~= 3 then self:F2( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), EventPriority } ) - end - local Result, Value = xpcall( - function() - return EventData.EventFunction( EventClass, Event ) + end + local Result, Value = xpcall( + function() + return EventData.EventFunction( EventClass, Event ) end, ErrorHandler ) else - + -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. local EventFunction = EventClass[ EventMeta.Event ] if EventFunction and type( EventFunction ) == "function" then - + -- Now call the default event function. if Event.IniObjectCategory ~= 3 then self:F2( { "Calling " .. EventMeta.Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } ) end - - local Result, Value = xpcall( - function() + + local Result, Value = xpcall( + function() local Result, Value = EventFunction( EventClass, Event ) - return Result, Value + return Result, Value end, ErrorHandler ) end end - + end end end end end end - + -- When cargo was deleted, it may probably be because of an S_EVENT_DEAD. -- However, in the loading logic, an S_EVENT_DEAD is also generated after a Destroy() call. -- And this is a problem because it will remove all entries from the SET_CARGOs. @@ -1294,12 +1380,12 @@ function EVENT:onEvent( Event ) Event.Cargo.NoDestroy = nil end else - self:T( { EventMeta.Text, Event } ) + self:T( { EventMeta.Text, Event } ) end else self:E(string.format("WARNING: Could not get EVENTMETA data for event ID=%d! Is this an unknown/new DCS event?", tostring(Event.id))) end - + Event = nil end diff --git a/Moose Development/Moose/Core/Fsm.lua b/Moose Development/Moose/Core/Fsm.lua index 0b49c3449..68807904b 100644 --- a/Moose Development/Moose/Core/Fsm.lua +++ b/Moose Development/Moose/Core/Fsm.lua @@ -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 diff --git a/Moose Development/Moose/Core/MarkerOps_Base.lua b/Moose Development/Moose/Core/MarkerOps_Base.lua new file mode 100644 index 000000000..eb5e17110 --- /dev/null +++ b/Moose Development/Moose/Core/MarkerOps_Base.lua @@ -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. +-------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index 1b2603f00..b450eebbf 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -1,11 +1,11 @@ --- **Core** - Defines an extensive API to manage 3D points in the DCS World 3D simulation space. -- -- ## Features: --- +-- -- * Provides a COORDINATE class, which allows to manage points in 3D space and perform various operations on it. -- * Provides a POINT\_VEC2 class, which is derived from COORDINATE, and allows to manage points in 3D space, but from a Lat/Lon and Altitude perspective. -- * Provides a POINT\_VEC3 class, which is derived from COORDINATE, and allows to manage points in 3D space, but from a X, Z and Y vector perspective. --- +-- -- === -- -- # Demo Missions @@ -26,22 +26,31 @@ -- -- ### Authors: -- --- * FlightControl : Design & Programming +-- * FlightControl (Design & Programming) -- -- ### Contributions: +-- +-- * funkyfranky +-- * Applevangelist +-- +-- === -- -- @module Core.Point -- @image Core_Coordinate.JPG - - do -- COORDINATE --- @type COORDINATE + -- @field #string ClassName Name of the class + -- @field #number x Component of the 3D vector. + -- @field #number y Component of the 3D vector. + -- @field #number z Component of the 3D vector. + -- @field #number Heading Heading in degrees. Needs to be set first. + -- @field #number Velocity Velocity in meters per second. Needs to be set first. -- @extends Core.Base#BASE - - + + --- Defines a 3D point in the simulator and with its methods, you can use or manipulate the point in 3D space. -- -- # 1) Create a COORDINATE object. @@ -84,17 +93,17 @@ do -- COORDINATE -- -- -- # 3) Create markings on the map. - -- - -- Place markers (text boxes with clarifications for briefings, target locations or any other reference point) + -- + -- Place markers (text boxes with clarifications for briefings, target locations or any other reference point) -- on the map for all players, coalitions or specific groups: - -- + -- -- * @{#COORDINATE.MarkToAll}(): Place a mark to all players. -- * @{#COORDINATE.MarkToCoalition}(): Place a mark to a coalition. -- * @{#COORDINATE.MarkToCoalitionRed}(): Place a mark to the red coalition. -- * @{#COORDINATE.MarkToCoalitionBlue}(): Place a mark to the blue coalition. -- * @{#COORDINATE.MarkToGroup}(): Place a mark to a group (needs to have a client in it or a CA group (CA group is bugged)). -- * @{#COORDINATE.RemoveMark}(): Removes a mark from the map. - -- + -- -- # 4) Coordinate calculation methods. -- -- Various calculation methods exist to use or manipulate 3D space. Find below a short description of each method: @@ -124,37 +133,37 @@ do -- COORDINATE -- -- * @{#COORDINATE.GetRandomVec2InRadius}(): Provides a random 2D vector around the current 3D point, in the given inner to outer band. -- * @{#COORDINATE.GetRandomVec3InRadius}(): Provides a random 3D vector around the current 3D point, in the given inner to outer band. - -- + -- -- ## 4.6) LOS between coordinates. - -- + -- -- Calculate if the coordinate has Line of Sight (LOS) with the other given coordinate. -- Mountains, trees and other objects can be positioned between the two 3D points, preventing visibilty in a straight continuous line. -- The method @{#COORDINATE.IsLOS}() returns if the two coodinates have LOS. - -- + -- -- ## 4.7) Check the coordinate position. - -- + -- -- Various methods are available that allow to check if a coordinate is: - -- + -- -- * @{#COORDINATE.IsInRadius}(): in a give radius. -- * @{#COORDINATE.IsInSphere}(): is in a given sphere. -- * @{#COORDINATE.IsAtCoordinate2D}(): is in a given coordinate within a specific precision. - -- - -- + -- + -- -- -- # 5) Measure the simulation environment at the coordinate. - -- + -- -- ## 5.1) Weather specific. - -- + -- -- Within the DCS simulator, a coordinate has specific environmental properties, like wind, temperature, humidity etc. - -- + -- -- * @{#COORDINATE.GetWind}(): Retrieve the wind at the specific coordinate within the DCS simulator. -- * @{#COORDINATE.GetTemperature}(): Retrieve the temperature at the specific height within the DCS simulator. -- * @{#COORDINATE.GetPressure}(): Retrieve the pressure at the specific height within the DCS simulator. - -- + -- -- ## 5.2) Surface specific. - -- + -- -- Within the DCS simulator, the surface can have various objects placed at the coordinate, and the surface height will vary. - -- + -- -- * @{#COORDINATE.GetLandHeight}(): Retrieve the height of the surface (on the ground) within the DCS simulator. -- * @{#COORDINATE.GetSurfaceType}(): Retrieve the surface type (on the ground) within the DCS simulator. -- @@ -164,17 +173,18 @@ do -- COORDINATE -- -- * @{#COORDINATE.WaypointAir}(): Build an air route point. -- * @{#COORDINATE.WaypointGround}(): Build a ground route point. + -- * @{#COORDINATE.WaypointNaval}(): Build a naval route point. -- -- Route points can be used in the Route methods of the @{Wrapper.Group#GROUP} class. -- -- ## 7) Manage the roads. - -- + -- -- Important for ground vehicle transportation and movement, the method @{#COORDINATE.GetClosestPointToRoad}() will calculate -- the closest point on the nearest road. - -- + -- -- In order to use the most optimal road system to transport vehicles, the method @{#COORDINATE.GetPathOnRoad}() will calculate -- the most optimal path following the road between two coordinates. - -- + -- -- ## 8) Metric or imperial system -- -- * @{#COORDINATE.IsMetric}(): Returns if the 3D point is Metric or Nautical Miles. @@ -182,23 +192,42 @@ do -- COORDINATE -- -- -- ## 9) Coordinate text generation - -- -- -- * @{#COORDINATE.ToStringBR}(): Generates a Bearing & Range text in the format of DDD for DI where DDD is degrees and DI is distance. -- * @{#COORDINATE.ToStringLL}(): Generates a Latutude & Longutude text. -- + -- ## 10) Drawings on F10 map + -- + -- * @{#COORDINATE.CircleToAll}(): Draw a circle on the F10 map. + -- * @{#COORDINATE.LineToAll}(): Draw a line on the F10 map. + -- * @{#COORDINATE.RectToAll}(): Draw a rectangle on the F10 map. + -- * @{#COORDINATE.QuadToAll}(): Draw a shape with four points on the F10 map. + -- * @{#COORDINATE.TextToAll}(): Write some text on the F10 map. + -- * @{#COORDINATE.ArrowToAll}(): Draw an arrow on the F10 map. + -- -- @field #COORDINATE COORDINATE = { ClassName = "COORDINATE", } - --- @field COORDINATE.WaypointAltType + --- Waypoint altitude types. + -- @type COORDINATE.WaypointAltType + -- @field #string BARO Barometric altitude. + -- @field #string RADIO Radio altitude. COORDINATE.WaypointAltType = { BARO = "BARO", RADIO = "RADIO", } - - --- @field COORDINATE.WaypointAction + + --- Waypoint actions. + -- @type COORDINATE.WaypointAction + -- @field #string TurningPoint Turning point. + -- @field #string FlyoverPoint Fly over point. + -- @field #string FromParkingArea From parking area. + -- @field #string FromParkingAreaHot From parking area hot. + -- @field #string FromRunway From runway. + -- @field #string Landing Landing. + -- @field #string LandingReFuAr Landing and refuel and rearm. COORDINATE.WaypointAction = { TurningPoint = "Turning Point", FlyoverPoint = "Fly Over Point", @@ -209,7 +238,14 @@ do -- COORDINATE LandingReFuAr = "LandingReFuAr", } - --- @field COORDINATE.WaypointType + --- Waypoint types. + -- @type COORDINATE.WaypointType + -- @field #string TakeOffParking Take of parking. + -- @field #string TakeOffParkingHot Take of parking hot. + -- @field #string TakeOff Take off parking hot. + -- @field #string TurningPoint Turning point. + -- @field #string Land Landing point. + -- @field #string LandingReFuAr Landing and refuel and rearm. COORDINATE.WaypointType = { TakeOffParking = "TakeOffParking", TakeOffParkingHot = "TakeOffParkingHot", @@ -223,50 +259,48 @@ do -- COORDINATE --- COORDINATE constructor. -- @param #COORDINATE self -- @param DCS#Distance x The x coordinate of the Vec3 point, pointing to the North. - -- @param DCS#Distance y The y coordinate of the Vec3 point, pointing to the Right. - -- @param DCS#Distance z The z coordinate of the Vec3 point, pointing to the Right. - -- @return #COORDINATE - function COORDINATE:New( x, y, z ) + -- @param DCS#Distance y The y coordinate of the Vec3 point, pointing to up. + -- @param DCS#Distance z The z coordinate of the Vec3 point, pointing to the right. + -- @return #COORDINATE self + function COORDINATE:New( x, y, z ) - --env.info("FF COORDINATE New") - local self = BASE:Inherit( self, BASE:New() ) -- #COORDINATE + local self=BASE:Inherit(self, BASE:New()) -- #COORDINATE + self.x = x self.y = y self.z = z - + return self end --- COORDINATE constructor. -- @param #COORDINATE self -- @param #COORDINATE Coordinate. - -- @return #COORDINATE - function COORDINATE:NewFromCoordinate( Coordinate ) + -- @return #COORDINATE self + function COORDINATE:NewFromCoordinate( Coordinate ) local self = BASE:Inherit( self, BASE:New() ) -- #COORDINATE self.x = Coordinate.x self.y = Coordinate.y self.z = Coordinate.z - + return self end --- Create a new COORDINATE object from Vec2 coordinates. -- @param #COORDINATE self -- @param DCS#Vec2 Vec2 The Vec2 point. - -- @param DCS#Distance LandHeightAdd (optional) The default height if required to be evaluated will be the land height of the x, y coordinate. You can specify an extra height to be added to the land height. - -- @return #COORDINATE - function COORDINATE:NewFromVec2( Vec2, LandHeightAdd ) + -- @param DCS#Distance LandHeightAdd (Optional) The default height if required to be evaluated will be the land height of the x, y coordinate. You can specify an extra height to be added to the land height. + -- @return #COORDINATE self + function COORDINATE:NewFromVec2( Vec2, LandHeightAdd ) local LandHeight = land.getHeight( Vec2 ) - + LandHeightAdd = LandHeightAdd or 0 LandHeight = LandHeight + LandHeightAdd local self = self:New( Vec2.x, LandHeight, Vec2.y ) -- #COORDINATE - self:F2( self ) - return self end @@ -274,8 +308,8 @@ do -- COORDINATE --- Create a new COORDINATE object from Vec3 coordinates. -- @param #COORDINATE self -- @param DCS#Vec3 Vec3 The Vec3 point. - -- @return #COORDINATE - function COORDINATE:NewFromVec3( Vec3 ) + -- @return #COORDINATE self + function COORDINATE:NewFromVec3( Vec3 ) local self = self:New( Vec3.x, Vec3.y, Vec3.z ) -- #COORDINATE @@ -283,13 +317,29 @@ do -- COORDINATE return self end + + --- Create a new COORDINATE object from a waypoint. This uses the components + -- + -- * `waypoint.x` + -- * `waypoint.alt` + -- * `waypoint.y` + -- + -- @param #COORDINATE self + -- @param DCS#Waypoint Waypoint The waypoint. + -- @return #COORDINATE self + function COORDINATE:NewFromWaypoint(Waypoint) + + local self=self:New(Waypoint.x, Waypoint.alt, Waypoint.y) -- #COORDINATE + + return self + end --- Return the coordinates itself. Sounds stupid but can be useful for compatibility. -- @param #COORDINATE self -- @return #COORDINATE self function COORDINATE:GetCoordinate() return self - end + end --- Return the coordinates of the COORDINATE in Vec3 format. -- @param #COORDINATE self @@ -311,39 +361,39 @@ do -- COORDINATE -- @param DCS#Vec3 Vec3 The 3D vector with x,y,z components. -- @return #COORDINATE The modified COORDINATE itself. function COORDINATE:UpdateFromVec3(Vec3) - + self.x=Vec3.x self.y=Vec3.y self.z=Vec3.z - + return self end - + --- Update x,y,z coordinates from another given COORDINATE. -- @param #COORDINATE self -- @param #COORDINATE Coordinate The coordinate with the new x,y,z positions. -- @return #COORDINATE The modified COORDINATE itself. function COORDINATE:UpdateFromCoordinate(Coordinate) - + self.x=Coordinate.x self.y=Coordinate.y self.z=Coordinate.z - + return self - end + end --- Update x and z coordinates from a given 2D vector. -- @param #COORDINATE self -- @param DCS#Vec2 Vec2 The 2D vector with x,y components. x is overwriting COORDINATE.x while y is overwriting COORDINATE.z. -- @return #COORDINATE The modified COORDINATE itself. function COORDINATE:UpdateFromVec2(Vec2) - + self.x=Vec2.x self.z=Vec2.y - + return self end - + --- Returns the coordinate from the latitude and longitude given in decimal degrees. -- @param #COORDINATE self @@ -352,40 +402,40 @@ do -- COORDINATE -- @param #number altitude (Optional) Altitude in meters. Default is the land height at the coordinate. -- @return #COORDINATE function COORDINATE:NewFromLLDD( latitude, longitude, altitude) - + -- Returns a point from latitude and longitude in the vec3 format. local vec3=coord.LLtoLO(latitude, longitude) - + -- Convert vec3 to coordinate object. local _coord=self:NewFromVec3(vec3) - + -- Adjust height if altitude==nil then - _coord.y=altitude - else _coord.y=self:GetLandHeight() + else + _coord.y=altitude end return _coord end - + --- Returns if the 2 coordinates are at the same 2D position. -- @param #COORDINATE self -- @param #COORDINATE Coordinate -- @param #number Precision -- @return #boolean true if at the same position. function COORDINATE:IsAtCoordinate2D( Coordinate, Precision ) - + self:F( { Coordinate = Coordinate:GetVec2() } ) self:F( { self = self:GetVec2() } ) - + local x = Coordinate.x local z = Coordinate.z - - return x - Precision <= self.x and x + Precision >= self.x and z - Precision <= self.z and z + Precision >= self.z + + return x - Precision <= self.x and x + Precision >= self.x and z - Precision <= self.z and z + Precision >= self.z end - + --- Scan/find objects (units, statics, scenery) within a certain radius around the coordinate using the world.searchObjects() DCS API function. -- @param #COORDINATE self -- @param #number radius (Optional) Scan radius in meters. Default 100 m. @@ -420,7 +470,7 @@ do -- COORDINATE if scanscenery==nil then scanscenery=false end - + --{Object.Category.UNIT, Object.Category.STATIC, Object.Category.SCENERY} local scanobjects={} if scanunits then @@ -432,7 +482,7 @@ do -- COORDINATE if scanscenery then table.insert(scanobjects, Object.Category.SCENERY) end - + -- Found stuff. local Units = {} local Statics = {} @@ -440,40 +490,40 @@ do -- COORDINATE local gotstatics=false local gotunits=false local gotscenery=false - + local function EvaluateZone(ZoneObject) - + if ZoneObject then - + -- Get category of scanned object. local ObjectCategory = ZoneObject:getCategory() - + -- Check for unit or static objects if ObjectCategory==Object.Category.UNIT and ZoneObject:isExist() then - + table.insert(Units, UNIT:Find(ZoneObject)) gotunits=true - + elseif ObjectCategory==Object.Category.STATIC and ZoneObject:isExist() then - + table.insert(Statics, ZoneObject) gotstatics=true - + elseif ObjectCategory==Object.Category.SCENERY then - + table.insert(Scenery, ZoneObject) gotscenery=true - + end - + end - + return true end - + -- Search the world. world.searchObjects(scanobjects, SphereSearch, EvaluateZone) - + for _,unit in pairs(Units) do self:T(string.format("Scan found unit %s", unit:GetName())) end @@ -485,35 +535,35 @@ do -- COORDINATE self:T(string.format("Scan found scenery %s typename=%s", scenery:getName(), scenery:getTypeName())) --SCENERY:Register(scenery:getName(), scenery) end - + return gotunits, gotstatics, gotscenery, Units, Statics, Scenery end - + --- Scan/find UNITS within a certain radius around the coordinate using the world.searchObjects() DCS API function. -- @param #COORDINATE self -- @param #number radius (Optional) Scan radius in meters. Default 100 m. -- @return Core.Set#SET_UNIT Set of units. function COORDINATE:ScanUnits(radius) - + local _,_,_,units=self:ScanObjects(radius, true, false, false) - + local set=SET_UNIT:New() - + for _,unit in pairs(units) do set:AddUnit(unit) end - + return set end - - --- Find the closest unit to the COORDINATE within a certain radius. + + --- Find the closest unit to the COORDINATE within a certain radius. -- @param #COORDINATE self -- @param #number radius Scan radius in meters. Default 100 m. -- @return Wrapper.Unit#UNIT The closest unit or #nil if no unit is inside the given radius. function COORDINATE:FindClosestUnit(radius) - + local units=self:ScanUnits(radius) - + local umin=nil --Wrapper.Unit#UNIT local dmin=math.huge for _,_unit in pairs(units.Set) do @@ -524,41 +574,41 @@ do -- COORDINATE dmin=d umin=unit end - end - + end + return umin - end + end --- Scan/find SCENERY objects within a certain radius around the coordinate using the world.searchObjects() DCS API function. -- @param #COORDINATE self -- @param #number radius (Optional) Scan radius in meters. Default 100 m. -- @return table Set of scenery objects. function COORDINATE:ScanScenery(radius) - + local _,_,_,_,_,scenerys=self:ScanObjects(radius, false, false, true) - + local set={} - + for _,_scenery in pairs(scenerys) do local scenery=_scenery --DCS#Object - + local name=scenery:getName() local s=SCENERY:Register(name, scenery) table.insert(set, s) end - - return set - end - --- Find the closest scenery to the COORDINATE within a certain radius. + return set + end + + --- Find the closest scenery to the COORDINATE within a certain radius. -- @param #COORDINATE self -- @param #number radius Scan radius in meters. Default 100 m. -- @return Wrapper.Scenery#SCENERY The closest scenery or #nil if no object is inside the given radius. function COORDINATE:FindClosestScenery(radius) - + local sceneries=self:ScanScenery(radius) - + local umin=nil --Wrapper.Scenery#SCENERY local dmin=math.huge for _,_scenery in pairs(sceneries) do @@ -569,11 +619,11 @@ do -- COORDINATE dmin=d umin=scenery end - end - + end + return umin - end - + end + --- Calculate the distance from a reference @{#COORDINATE}. -- @param #COORDINATE self -- @param #COORDINATE PointVec2Reference The reference @{#COORDINATE}. @@ -596,14 +646,14 @@ do -- COORDINATE -- @return #COORDINATE The new calculated COORDINATE. function COORDINATE:Translate( Distance, Angle, Keepalt, Overwrite ) - -- Angle in rad. + -- Angle in rad. local alpha = math.rad((Angle or 0)) - + local x = Distance * math.cos(alpha) + self.x -- New x local z = Distance * math.sin(alpha) + self.z -- New z - + local y=Keepalt and self.y or land.getHeight({x=x, y=z}) - + if Overwrite then self.x=x self.y=y @@ -614,7 +664,7 @@ do -- COORDINATE local coord=COORDINATE:New(x, y, z) return coord end - + end --- Rotate coordinate in 2D (x,z) space. @@ -622,25 +672,26 @@ do -- COORDINATE -- @param DCS#Angle Angle Angle of rotation in degrees. -- @return Core.Point#COORDINATE The rotated coordinate. function COORDINATE:Rotate2D(Angle) - + if not Angle then return self end local phi=math.rad(Angle) - + local X=self.z local Y=self.x - + --slocal R=math.sqrt(X*X+Y*Y) - + local x=X*math.cos(phi)-Y*math.sin(phi) local y=X*math.sin(phi)+Y*math.cos(phi) -- Coordinate assignment looks bit strange but is correct. - return COORDINATE:NewFromVec3({x=y, y=self.y, z=x}) + local coord=COORDINATE:NewFromVec3({x=y, y=self.y, z=x}) + return coord end - + --- Return a random Vec2 within an Outer Radius and optionally NOT within an Inner Radius of the COORDINATE. -- @param #COORDINATE self -- @param DCS#Distance OuterRadius @@ -675,13 +726,14 @@ do -- COORDINATE --- Return a random Coordinate within an Outer Radius and optionally NOT within an Inner Radius of the COORDINATE. -- @param #COORDINATE self - -- @param DCS#Distance OuterRadius - -- @param DCS#Distance InnerRadius - -- @return #COORDINATE + -- @param DCS#Distance OuterRadius Outer radius in meters. + -- @param DCS#Distance InnerRadius Inner radius in meters. + -- @return #COORDINATE self function COORDINATE:GetRandomCoordinateInRadius( OuterRadius, InnerRadius ) self:F2( { OuterRadius, InnerRadius } ) - return COORDINATE:NewFromVec2( self:GetRandomVec2InRadius( OuterRadius, InnerRadius ) ) + local coord=COORDINATE:NewFromVec2( self:GetRandomVec2InRadius( OuterRadius, InnerRadius ) ) + return coord end @@ -698,7 +750,7 @@ do -- COORDINATE return RandomVec3 end - + --- Return the height of the land at the coordinate. -- @param #COORDINATE self -- @return #number Land height (ASL) in meters. @@ -713,8 +765,8 @@ do -- COORDINATE function COORDINATE:SetHeading( Heading ) self.Heading = Heading end - - + + --- Get the heading of the coordinate, if applicable. -- @param #COORDINATE self -- @return #number or nil @@ -722,7 +774,7 @@ do -- COORDINATE return self.Heading end - + --- Set the velocity of the COORDINATE. -- @param #COORDINATE self -- @param #string Velocity Velocity in meters per second. @@ -730,7 +782,7 @@ do -- COORDINATE self.Velocity = Velocity end - + --- Return the velocity of the COORDINATE. -- @param #COORDINATE self -- @return #number Velocity in meters per second. @@ -739,14 +791,14 @@ do -- COORDINATE return Velocity or 0 end - --- Return the "name" of the COORDINATE. Obviously, a coordinate does not have a name like a unit, static or group. So here we take the MGRS coordinates of the position. + --- Return the "name" of the COORDINATE. Obviously, a coordinate does not have a name like a unit, static or group. So here we take the MGRS coordinates of the position. -- @param #COORDINATE self -- @return #string MGRS coordinates. function COORDINATE:GetName() local name=self:ToStringMGRS() return name end - + --- Return velocity text of the COORDINATE. -- @param #COORDINATE self -- @return #string @@ -805,20 +857,20 @@ do -- COORDINATE -- @param #number Fraction The fraction (0,1) where the new coordinate is created. Default 0.5, i.e. in the middle. -- @return #COORDINATE Coordinate between this and the other coordinate. function COORDINATE:GetIntermediateCoordinate( ToCoordinate, Fraction ) - + local f=Fraction or 0.5 - -- Get the vector from A to B + -- Get the vector from A to B local vec=UTILS.VecSubstract(ToCoordinate, self) - + -- Scale the vector. vec.x=f*vec.x vec.y=f*vec.y vec.z=f*vec.z - + -- Move the vector to start at the end of A. vec=UTILS.VecAdd(self, vec) - + local coord=COORDINATE:New(vec.x,vec.y,vec.z) return coord end @@ -827,14 +879,14 @@ do -- COORDINATE -- @param #COORDINATE self -- @param #COORDINATE TargetCoordinate The target COORDINATE. Can also be a DCS#Vec3. -- @return DCS#Distance Distance The distance in meters. - function COORDINATE:Get2DDistance( TargetCoordinate ) + function COORDINATE:Get2DDistance(TargetCoordinate) local a={x=TargetCoordinate.x-self.x, y=0, z=TargetCoordinate.z-self.z} - + local norm=UTILS.VecNorm(a) return norm end - + --- Returns the temperature in Degrees Celsius. -- @param #COORDINATE self -- @param height (Optional) parameter specifying the height ASL. @@ -851,22 +903,22 @@ do -- COORDINATE --- Returns a text of the temperature according the measurement system @{Settings}. -- The text will reflect the temperature like this: - -- + -- -- - For Russian and European aircraft using the metric system - Degrees Celcius (°C) -- - For Americain aircraft we link to the imperial system - Degrees Farenheit (°F) - -- - -- A text containing a pressure will look like this: - -- - -- - `Temperature: %n.d °C` + -- + -- A text containing a pressure will look like this: + -- + -- - `Temperature: %n.d °C` -- - `Temperature: %n.d °F` - -- + -- -- @param #COORDINATE self -- @param height (Optional) parameter specifying the height ASL. -- @return #string Temperature according the measurement system @{Settings}. function COORDINATE:GetTemperatureText( height, Settings ) - + local DegreesCelcius = self:GetTemperature( height ) - + local Settings = Settings or _SETTINGS if DegreesCelcius then @@ -878,7 +930,7 @@ do -- COORDINATE else return " no temperature" end - + return nil end @@ -894,18 +946,18 @@ do -- COORDINATE -- Return Pressure in hPa. return P/100 end - + --- Returns a text of the pressure according the measurement system @{Settings}. -- The text will contain always the pressure in hPa and: - -- + -- -- - For Russian and European aircraft using the metric system - hPa and mmHg -- - For Americain and European aircraft we link to the imperial system - hPa and inHg - -- - -- A text containing a pressure will look like this: - -- - -- - `QFE: x hPa (y mmHg)` + -- + -- A text containing a pressure will look like this: + -- + -- - `QFE: x hPa (y mmHg)` -- - `QFE: x hPa (y inHg)` - -- + -- -- @param #COORDINATE self -- @param height (Optional) parameter specifying the height ASL. E.g. set height=0 for QNH. -- @return #string Pressure in hPa and mmHg or inHg depending on the measurement system @{Settings}. @@ -914,7 +966,7 @@ do -- COORDINATE local Pressure_hPa = self:GetPressure( height ) local Pressure_mmHg = Pressure_hPa * 0.7500615613030 local Pressure_inHg = Pressure_hPa * 0.0295299830714 - + local Settings = Settings or _SETTINGS if Pressure_hPa then @@ -926,14 +978,14 @@ do -- COORDINATE else return " no pressure" end - + return nil end - + --- Returns the heading from this to another coordinate. -- @param #COORDINATE self -- @param #COORDINATE ToCoordinate - -- @return #number Heading in degrees. + -- @return #number Heading in degrees. function COORDINATE:HeadingTo(ToCoordinate) local dz=ToCoordinate.z-self.z local dx=ToCoordinate.x-self.x @@ -943,7 +995,7 @@ do -- COORDINATE end return heading end - + --- Returns the wind direction (from) and strength. -- @param #COORDINATE self -- @param height (Optional) parameter specifying the height ASL. The minimum height will be always be the land height since the wind is zero below the ground. @@ -953,12 +1005,12 @@ do -- COORDINATE local landheight=self:GetLandHeight()+0.1 -- we at 0.1 meters to be sure to be above ground since wind is zero below ground level. local point={x=self.x, y=math.max(height or self.y, landheight), z=self.z} -- get wind velocity vector - local wind = atmosphere.getWind(point) + local wind = atmosphere.getWind(point) local direction = math.deg(math.atan2(wind.z, wind.x)) if direction < 0 then direction = 360 + direction end - -- Convert to direction to from direction + -- Convert to direction to from direction if direction > 180 then direction = direction-180 else @@ -968,37 +1020,37 @@ do -- COORDINATE -- Return wind direction and strength km/h. return direction, strength end - + --- Returns the wind direction (from) and strength. -- @param #COORDINATE self -- @param height (Optional) parameter specifying the height ASL. The minimum height will be always be the land height since the wind is zero below the ground. -- @return Direction the wind is blowing from in degrees. function COORDINATE:GetWindWithTurbulenceVec3(height) - - -- AGL height if + + -- AGL height if local landheight=self:GetLandHeight()+0.1 -- we at 0.1 meters to be sure to be above ground since wind is zero below ground level. - - -- Point at which the wind is evaluated. + + -- Point at which the wind is evaluated. local point={x=self.x, y=math.max(height or self.y, landheight), z=self.z} - + -- Get wind velocity vector including turbulences. local vec3 = atmosphere.getWindWithTurbulence(point) - + return vec3 - end + end --- Returns a text documenting the wind direction (from) and strength according the measurement system @{Settings}. -- The text will reflect the wind like this: - -- + -- -- - For Russian and European aircraft using the metric system - Wind direction in degrees (°) and wind speed in meters per second (mps). -- - For Americain aircraft we link to the imperial system - Wind direction in degrees (°) and wind speed in knots per second (kps). - -- - -- A text containing a pressure will look like this: - -- - -- - `Wind: %n ° at n.d mps` + -- + -- A text containing a pressure will look like this: + -- + -- - `Wind: %n ° at n.d mps` -- - `Wind: %n ° at n.d kps` - -- + -- -- @param #COORDINATE self -- @param height (Optional) parameter specifying the height ASL. The minimum height will be always be the land height since the wind is zero below the ground. -- @return #string Wind direction and strength according the measurement system @{Settings}. @@ -1017,7 +1069,7 @@ do -- COORDINATE else return " no wind" end - + return nil end @@ -1043,9 +1095,9 @@ do -- COORDINATE local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS local AngleDegrees = UTILS.Round( UTILS.ToDegree( AngleRadians ), Precision ) - - local s = string.format( '%03d°', AngleDegrees ) - + + local s = string.format( '%03d°', AngleDegrees ) + return s end @@ -1074,7 +1126,7 @@ do -- COORDINATE DistanceText = " за " .. UTILS.Round( UTILS.MetersToNM( Distance ), 2 ) .. " миль" end end - + return DistanceText end @@ -1085,7 +1137,7 @@ do -- COORDINATE local Altitude = self.y local Settings = Settings or _SETTINGS local Language = Language or "EN" - + if Altitude ~= 0 then if Settings:IsMetric() then if Language == "EN" then @@ -1150,7 +1202,7 @@ do -- COORDINATE local BearingText = self:GetBearingText( AngleRadians, 0, Settings, Language ) local DistanceText = self:GetDistanceText( Distance, Settings, Language ) - + local BRText = BearingText .. DistanceText return BRText @@ -1206,44 +1258,44 @@ do -- COORDINATE -- @return #table The route point. function COORDINATE:WaypointAir( AltType, Type, Action, Speed, SpeedLocked, airbase, DCSTasks, description, timeReFuAr ) self:F2( { AltType, Type, Action, Speed, SpeedLocked } ) - + -- Set alttype or "RADIO" which is AGL. AltType=AltType or "RADIO" - + -- Speedlocked by default if SpeedLocked==nil then SpeedLocked=true end - + -- Speed or default 500 km/h. Speed=Speed or 500 - + -- Waypoint array. local RoutePoint = {} - + -- Coordinates. RoutePoint.x = self.x RoutePoint.y = self.z - + -- Altitude. RoutePoint.alt = self.y RoutePoint.alt_type = AltType - + -- Waypoint type. RoutePoint.type = Type or nil - RoutePoint.action = Action or nil - + RoutePoint.action = Action or nil + -- Speed. RoutePoint.speed = Speed/3.6 RoutePoint.speed_locked = SpeedLocked - + -- ETA. RoutePoint.ETA=0 - RoutePoint.ETA_locked=true - + RoutePoint.ETA_locked=false + -- Waypoint description. RoutePoint.name=description - + -- Airbase parameters for takeoff and landing points. if airbase then local AirbaseID = airbase:GetID() @@ -1257,26 +1309,26 @@ do -- COORDINATE self:E("ERROR: Unknown airbase category in COORDINATE:WaypointAir()!") end end - - -- Time in minutes to stay at the airbase before resuming route. + + -- Time in minutes to stay at the airbase before resuming route. if Type==COORDINATE.WaypointType.LandingReFuAr then RoutePoint.timeReFuAr=timeReFuAr or 10 end - + -- Waypoint tasks. RoutePoint.task = {} RoutePoint.task.id = "ComboTask" RoutePoint.task.params = {} RoutePoint.task.params.tasks = DCSTasks or {} - + --RoutePoint.properties={} --RoutePoint.properties.addopt={} - + --RoutePoint.formation_template="" -- Debug. self:T({RoutePoint=RoutePoint}) - + -- Return waypoint. return RoutePoint end @@ -1293,7 +1345,7 @@ do -- COORDINATE return self:WaypointAir( AltType, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, Speed, true, nil, DCSTasks, description ) end - + --- Build a Waypoint Air "Fly Over Point". -- @param #COORDINATE self -- @param #COORDINATE.WaypointAltType AltType The altitude type. @@ -1302,8 +1354,8 @@ do -- COORDINATE function COORDINATE:WaypointAirFlyOverPoint( AltType, Speed ) return self:WaypointAir( AltType, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.FlyoverPoint, Speed ) end - - + + --- Build a Waypoint Air "Take Off Parking Hot". -- @param #COORDINATE self -- @param #COORDINATE.WaypointAltType AltType The altitude type. @@ -1312,7 +1364,7 @@ do -- COORDINATE function COORDINATE:WaypointAirTakeOffParkingHot( AltType, Speed ) return self:WaypointAir( AltType, COORDINATE.WaypointType.TakeOffParkingHot, COORDINATE.WaypointAction.FromParkingAreaHot, Speed ) end - + --- Build a Waypoint Air "Take Off Parking". -- @param #COORDINATE self @@ -1322,8 +1374,8 @@ do -- COORDINATE function COORDINATE:WaypointAirTakeOffParking( AltType, Speed ) return self:WaypointAir( AltType, COORDINATE.WaypointType.TakeOffParking, COORDINATE.WaypointAction.FromParkingArea, Speed ) end - - + + --- Build a Waypoint Air "Take Off Runway". -- @param #COORDINATE self -- @param #COORDINATE.WaypointAltType AltType The altitude type. @@ -1332,8 +1384,8 @@ do -- COORDINATE function COORDINATE:WaypointAirTakeOffRunway( AltType, Speed ) return self:WaypointAir( AltType, COORDINATE.WaypointType.TakeOff, COORDINATE.WaypointAction.FromRunway, Speed ) end - - + + --- Build a Waypoint Air "Landing". -- @param #COORDINATE self -- @param DCS#Speed Speed Airspeed in km/h. @@ -1342,17 +1394,17 @@ do -- COORDINATE -- @param #string description A text description of the waypoint, which will be shown on the F10 map. -- @return #table The route point. -- @usage - -- + -- -- LandingZone = ZONE:New( "LandingZone" ) -- LandingCoord = LandingZone:GetCoordinate() -- LandingWaypoint = LandingCoord:WaypointAirLanding( 60 ) -- HeliGroup:Route( { LandWaypoint }, 1 ) -- Start landing the helicopter in one second. - -- + -- function COORDINATE:WaypointAirLanding( Speed, airbase, DCSTasks, description ) return self:WaypointAir(nil, COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, Speed, false, airbase, DCSTasks, description) end - - --- Build a Waypoint Air "LandingReFuAr". Mimics the aircraft ReFueling and ReArming. + + --- Build a Waypoint Air "LandingReFuAr". Mimics the aircraft ReFueling and ReArming. -- @param #COORDINATE self -- @param DCS#Speed Speed Airspeed in km/h. -- @param Wrapper.Airbase#AIRBASE airbase The airbase for takeoff and landing points. @@ -1362,9 +1414,9 @@ do -- COORDINATE -- @return #table The route point. function COORDINATE:WaypointAirLandingReFu( Speed, airbase, timeReFuAr, DCSTasks, description ) return self:WaypointAir(nil, COORDINATE.WaypointType.LandingReFuAr, COORDINATE.WaypointAction.LandingReFuAr, Speed, false, airbase, DCSTasks, description, timeReFuAr or 10) - end - - + end + + --- Build an ground type route point. -- @param #COORDINATE self -- @param #number Speed (Optional) Speed in km/h. The default speed is 20 km/h. @@ -1375,20 +1427,20 @@ do -- COORDINATE self:F2( { Speed, Formation, DCSTasks } ) local RoutePoint = {} - + RoutePoint.x = self.x RoutePoint.y = self.z - + RoutePoint.alt = self:GetLandHeight()+1 RoutePoint.alt_type = COORDINATE.WaypointAltType.BARO - + RoutePoint.type = "Turning Point" - + RoutePoint.action = Formation or "Off Road" RoutePoint.formation_template="" - + RoutePoint.ETA=0 - RoutePoint.ETA_locked=true + RoutePoint.ETA_locked=false RoutePoint.speed = ( Speed or 20 ) / 3.6 RoutePoint.speed_locked = true @@ -1397,10 +1449,10 @@ do -- COORDINATE RoutePoint.task.id = "ComboTask" RoutePoint.task.params = {} RoutePoint.task.params.tasks = DCSTasks or {} - + return RoutePoint end - + --- Build route waypoint point for Naval units. -- @param #COORDINATE self -- @param #number Speed (Optional) Speed in km/h. The default speed is 20 km/h. @@ -1411,10 +1463,10 @@ do -- COORDINATE self:F2( { Speed, Depth, DCSTasks } ) local RoutePoint = {} - + RoutePoint.x = self.x RoutePoint.y = self.z - + RoutePoint.alt = Depth or self.y -- Depth is for submarines only. Ships should have alt=0. RoutePoint.alt_type = "BARO" @@ -1423,7 +1475,7 @@ do -- COORDINATE RoutePoint.formation_template = "" RoutePoint.ETA=0 - RoutePoint.ETA_locked=true + RoutePoint.ETA_locked=false RoutePoint.speed = ( Speed or 20 ) / 3.6 RoutePoint.speed_locked = true @@ -1443,10 +1495,10 @@ do -- COORDINATE -- @return Wrapper.Airbase#AIRBASE Closest Airbase to the given coordinate. -- @return #number Distance to the closest airbase in meters. function COORDINATE:GetClosestAirbase2(Category, Coalition) - + -- Get all airbases of the map. local airbases=AIRBASE.GetAllAirbases(Coalition) - + local closest=nil local distmin=nil -- Loop over all airbases. @@ -1456,9 +1508,9 @@ do -- COORDINATE local category=airbase:GetAirbaseCategory() if Category and Category==category or Category==nil then - -- Distance to airbase. + -- Distance to airbase. local dist=self:Get2DDistance(airbase:GetCoordinate()) - + if closest==nil then distmin=dist closest=airbase @@ -1466,13 +1518,13 @@ do -- COORDINATE if dist=2 then for i=1,#Path-1 do @@ -1684,8 +1740,8 @@ do -- COORDINATE else -- There are cases where no path on road can be found. return nil,nil,false - end - + end + return Path, Way, GotPath end @@ -1705,7 +1761,7 @@ do -- COORDINATE return self:GetSurfaceType()==land.SurfaceType.LAND end - --- Checks if the surface type is road. + --- Checks if the surface type is land. -- @param #COORDINATE self -- @return #boolean If true, the surface type at the coordinate is land. function COORDINATE:IsSurfaceTypeLand() @@ -1745,10 +1801,9 @@ do -- COORDINATE --- Creates an explosion at the point of a certain intensity. -- @param #COORDINATE self -- @param #number ExplosionIntensity Intensity of the explosion in kg TNT. Default 100 kg. - -- @param #number Delay Delay before explosion in seconds. + -- @param #number Delay (Optional) Delay before explosion is triggered in seconds. -- @return #COORDINATE self function COORDINATE:Explosion( ExplosionIntensity, Delay ) - self:F2( { ExplosionIntensity } ) ExplosionIntensity=ExplosionIntensity or 100 if Delay and Delay>0 then self:ScheduleOnce(Delay, self.Explosion, self, ExplosionIntensity) @@ -1760,11 +1815,17 @@ do -- COORDINATE --- Creates an illumination bomb at the point. -- @param #COORDINATE self - -- @param #number power Power of illumination bomb in Candela. + -- @param #number Power Power of illumination bomb in Candela. Default 1000 cd. + -- @param #number Delay (Optional) Delay before bomb is ignited in seconds. -- @return #COORDINATE self - function COORDINATE:IlluminationBomb(power) - self:F2() - trigger.action.illuminationBomb( self:GetVec3(), power ) + function COORDINATE:IlluminationBomb(Power, Delay) + Power=Power or 1000 + if Delay and Delay>0 then + self:ScheduleOnce(Delay, self.IlluminationBomb, self, Power) + else + trigger.action.illuminationBomb(self:GetVec3(), Power) + end + return self end @@ -1838,7 +1899,7 @@ do -- COORDINATE density=density or 0.5 self:BigSmokeAndFire(BIGSMOKEPRESET.MediumSmokeAndFire, density) end - + --- Large smoke and fire at the coordinate. -- @param #COORDINATE self -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. @@ -1856,7 +1917,7 @@ do -- COORDINATE density=density or 0.5 self:BigSmokeAndFire(BIGSMOKEPRESET.HugeSmokeAndFire, density) end - + --- Small smoke at the coordinate. -- @param #COORDINATE self -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. @@ -1865,7 +1926,7 @@ do -- COORDINATE density=density or 0.5 self:BigSmokeAndFire(BIGSMOKEPRESET.SmallSmoke, density) end - + --- Medium smoke at the coordinate. -- @param #COORDINATE self -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. @@ -1883,7 +1944,7 @@ do -- COORDINATE density=density or 0.5 self:BigSmokeAndFire(BIGSMOKEPRESET.LargeSmoke, density) end - + --- Huge smoke at the coordinate. -- @param #COORDINATE self -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. @@ -1891,7 +1952,7 @@ do -- COORDINATE self:F2( { density=density } ) density=density or 0.5 self:BigSmokeAndFire(BIGSMOKEPRESET.HugeSmoke, density) - end + end --- Flares the point in a color. -- @param #COORDINATE self @@ -1932,9 +1993,9 @@ do -- COORDINATE self:F2( Azimuth ) self:Flare( FLARECOLOR.Red, Azimuth ) end - + do -- Markings - + --- Mark to All -- @param #COORDINATE self -- @param #string MarkText Free format text that shows the marking clarification. @@ -2020,7 +2081,7 @@ do -- COORDINATE trigger.action.markToGroup( MarkID, MarkText, self:GetVec3(), MarkGroup:GetID(), ReadOnly, text ) return MarkID end - + --- Remove a mark -- @param #COORDINATE self -- @param #number MarkID The ID of the mark to be removed. @@ -2033,9 +2094,243 @@ do -- COORDINATE function COORDINATE:RemoveMark( MarkID ) trigger.action.removeMark( MarkID ) end - + + --- Line to all. + -- Creates a line on the F10 map from one point to another. + -- @param #COORDINATE self + -- @param #COORDINATE Endpoint COORDIANTE to where the line is drawn. + -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. + -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red (default). + -- @param #number Alpha Transparency [0,1]. Default 1. + -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. + -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. + -- @param #string Text (Optional) Text displayed when mark is added. Default none. + -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again. + function COORDINATE:LineToAll(Endpoint, Coalition, Color, Alpha, LineType, ReadOnly, Text) + local MarkID = UTILS.GetMarkID() + if ReadOnly==nil then + ReadOnly=false + end + local vec3=Endpoint:GetVec3() + Coalition=Coalition or -1 + Color=Color or {1,0,0} + Color[4]=Alpha or 1.0 + LineType=LineType or 1 + trigger.action.lineToAll(Coalition, MarkID, self:GetVec3(), vec3, Color, LineType, ReadOnly, Text or "") + return MarkID + end + + --- Circle to all. + -- Creates a circle on the map with a given radius, color, fill color, and outline. + -- @param #COORDINATE self + -- @param #number Radius Radius in meters. Default 1000 m. + -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. + -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red (default). + -- @param #number Alpha Transparency [0,1]. Default 1. + -- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. + -- @param #number FillAlpha Transparency [0,1]. Default 0.15. + -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. + -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. + -- @param #string Text (Optional) Text displayed when mark is added. Default none. + -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again. + function COORDINATE:CircleToAll(Radius, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly, Text) + local MarkID = UTILS.GetMarkID() + if ReadOnly==nil then + ReadOnly=false + end + local vec3=self:GetVec3() + Radius=Radius or 1000 + Coalition=Coalition or -1 + Color=Color or {1,0,0} + Color[4]=Alpha or 1.0 + LineType=LineType or 1 + FillColor=FillColor or Color + FillColor[4]=FillAlpha or 0.15 + trigger.action.circleToAll(Coalition, MarkID, vec3, Radius, Color, FillColor, LineType, ReadOnly, Text or "") + return MarkID + end + end -- Markings - + + --- Rectangle to all. Creates a rectangle on the map from the COORDINATE in one corner to the end COORDINATE in the opposite corner. + -- Creates a line on the F10 map from one point to another. + -- @param #COORDINATE self + -- @param #COORDINATE Endpoint COORDIANTE in the opposite corner. + -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. + -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red (default). + -- @param #number Alpha Transparency [0,1]. Default 1. + -- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. + -- @param #number FillAlpha Transparency [0,1]. Default 0.15. + -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. + -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. + -- @param #string Text (Optional) Text displayed when mark is added. Default none. + -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again. + function COORDINATE:RectToAll(Endpoint, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly, Text) + local MarkID = UTILS.GetMarkID() + if ReadOnly==nil then + ReadOnly=false + end + local vec3=Endpoint:GetVec3() + Coalition=Coalition or -1 + Color=Color or {1,0,0} + Color[4]=Alpha or 1.0 + LineType=LineType or 1 + FillColor=FillColor or Color + FillColor[4]=FillAlpha or 0.15 + trigger.action.rectToAll(Coalition, MarkID, self:GetVec3(), vec3, Color, FillColor, LineType, ReadOnly, Text or "") + return MarkID + end + + --- Creates a shape defined by 4 points on the F10 map. The first point is the current COORDINATE. The remaining three points need to be specified. + -- @param #COORDINATE self + -- @param #COORDINATE Coord2 Second COORDIANTE of the quad shape. + -- @param #COORDINATE Coord3 Third COORDIANTE of the quad shape. + -- @param #COORDINATE Coord4 Fourth COORDIANTE of the quad shape. + -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. + -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red (default). + -- @param #number Alpha Transparency [0,1]. Default 1. + -- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. + -- @param #number FillAlpha Transparency [0,1]. Default 0.15. + -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. + -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. + -- @param #string Text (Optional) Text displayed when mark is added. Default none. + -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again. + function COORDINATE:QuadToAll(Coord2, Coord3, Coord4, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly, Text) + local MarkID = UTILS.GetMarkID() + if ReadOnly==nil then + ReadOnly=false + end + local point1=self:GetVec3() + local point2=Coord2:GetVec3() + local point3=Coord3:GetVec3() + local point4=Coord4:GetVec3() + Coalition=Coalition or -1 + Color=Color or {1,0,0} + Color[4]=Alpha or 1.0 + LineType=LineType or 1 + FillColor=FillColor or Color + FillColor[4]=FillAlpha or 0.15 + trigger.action.quadToAll(Coalition, MarkID, self:GetVec3(), point2, point3, point4, Color, FillColor, LineType, ReadOnly, Text or "") + return MarkID + end + + --- Creates a free form shape on the F10 map. The first point is the current COORDINATE. The remaining points need to be specified. + -- **NOTE**: A free form polygon must have **at least three points** in total and currently only **up to 10 points** in total are supported. + -- @param #COORDINATE self + -- @param #table Coordinates Table of coordinates of the remaining points of the shape. + -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. + -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red (default). + -- @param #number Alpha Transparency [0,1]. Default 1. + -- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. + -- @param #number FillAlpha Transparency [0,1]. Default 0.15. + -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. + -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. + -- @param #string Text (Optional) Text displayed when mark is added. Default none. + -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again. + function COORDINATE:MarkupToAllFreeForm(Coordinates, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly, Text) + + local MarkID = UTILS.GetMarkID() + if ReadOnly==nil then + ReadOnly=false + end + + Coalition=Coalition or -1 + + Color=Color or {1,0,0} + Color[4]=Alpha or 1.0 + + LineType=LineType or 1 + + FillColor=FillColor or UTILS.DeepCopy(Color) + FillColor[4]=FillAlpha or 0.15 + + local vecs={} + vecs[1]=self:GetVec3() + for i,coord in ipairs(Coordinates) do + vecs[i+1]=coord:GetVec3() + end + + if #vecs<3 then + self:E("ERROR: A free form polygon needs at least three points!") + elseif #vecs==3 then + trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], Color, FillColor, LineType, ReadOnly, Text or "") + elseif #vecs==4 then + trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], Color, FillColor, LineType, ReadOnly, Text or "") + elseif #vecs==5 then + trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], Color, FillColor, LineType, ReadOnly, Text or "") + elseif #vecs==6 then + trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], vecs[6], Color, FillColor, LineType, ReadOnly, Text or "") + elseif #vecs==7 then + trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], vecs[6], vecs[7], Color, FillColor, LineType, ReadOnly, Text or "") + elseif #vecs==8 then + trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], vecs[6], vecs[7], vecs[8], Color, FillColor, LineType, ReadOnly, Text or "") + elseif #vecs==9 then + trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], vecs[6], vecs[7], vecs[8], vecs[9], Color, FillColor, LineType, ReadOnly, Text or "") + elseif #vecs==10 then + trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], vecs[6], vecs[7], vecs[8], vecs[9], vecs[10], Color, FillColor, LineType, ReadOnly, Text or "") + else + self:E("ERROR: Currently a free form polygon can only have 10 points in total!") + -- Unfortunately, unpack(vecs) does not work! So no idea how to generalize this :( + trigger.action.markupToAll(7, Coalition, MarkID, unpack(vecs), Color, FillColor, LineType, ReadOnly, Text or "") + end + + return MarkID + end + + --- Text to all. Creates a text imposed on the map at the COORDINATE. Text scales with the map. + -- @param #COORDINATE self + -- @param #string Text Text displayed on the F10 map. + -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. + -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red (default). + -- @param #number Alpha Transparency [0,1]. Default 1. + -- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. + -- @param #number FillAlpha Transparency [0,1]. Default 0.3. + -- @param #number FontSize Font size. Default 14. + -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. + -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again. + function COORDINATE:TextToAll(Text, Coalition, Color, Alpha, FillColor, FillAlpha, FontSize, ReadOnly) + local MarkID = UTILS.GetMarkID() + if ReadOnly==nil then + ReadOnly=false + end + Coalition=Coalition or -1 + Color=Color or {1,0,0} + Color[4]=Alpha or 1.0 + FillColor=FillColor or Color + FillColor[4]=FillAlpha or 0.3 + FontSize=FontSize or 14 + trigger.action.textToAll(Coalition, MarkID, self:GetVec3(), Color, FillColor, FontSize, ReadOnly, Text or "Hello World") + return MarkID + end + + --- Arrow to all. Creates an arrow from the COORDINATE to the endpoint COORDINATE on the F10 map. There is no control over other dimensions of the arrow. + -- @param #COORDINATE self + -- @param #COORDINATE Endpoint COORDINATE where the tip of the arrow is pointing at. + -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. + -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red (default). + -- @param #number Alpha Transparency [0,1]. Default 1. + -- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. + -- @param #number FillAlpha Transparency [0,1]. Default 0.15. + -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. + -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. + -- @param #string Text (Optional) Text displayed when mark is added. Default none. + -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again. + function COORDINATE:ArrowToAll(Endpoint, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly, Text) + local MarkID = UTILS.GetMarkID() + if ReadOnly==nil then + ReadOnly=false + end + local vec3=Endpoint:GetVec3() + Coalition=Coalition or -1 + Color=Color or {1,0,0} + Color[4]=Alpha or 1.0 + LineType=LineType or 1 + FillColor=FillColor or Color + FillColor[4]=FillAlpha or 0.15 + --trigger.action.textToAll(Coalition, MarkID, self:GetVec3(), Color, FillColor, FontSize, ReadOnly, Text or "Hello World") + trigger.action.arrowToAll(Coalition, MarkID, vec3, self:GetVec3(), Color, FillColor, LineType, ReadOnly, Text or "") + return MarkID + end --- Returns if a Coordinate has Line of Sight (LOS) with the ToCoordinate. -- @param #COORDINATE self @@ -2043,7 +2338,7 @@ do -- COORDINATE -- @param #number Offset Height offset in meters. Default 2 m. -- @return #boolean true If the ToCoordinate has LOS with the Coordinate, otherwise false. function COORDINATE:IsLOS( ToCoordinate, Offset ) - + Offset=Offset or 2 -- Measurement of visibility should not be from the ground, so Adding a hypotethical 2 meters to each Coordinate. @@ -2068,7 +2363,7 @@ do -- COORDINATE local InVec2 = self:GetVec2() local Vec2 = Coordinate:GetVec2() - + local InRadius = UTILS.IsInRadius( InVec2, Vec2, Radius) return InRadius @@ -2085,7 +2380,7 @@ do -- COORDINATE local InVec3 = self:GetVec3() local Vec3 = Coordinate:GetVec3() - + local InSphere = UTILS.IsInSphere( InVec3, Vec3, Radius) return InSphere @@ -2096,17 +2391,17 @@ do -- COORDINATE -- @param #number Day The day. -- @param #number Month The month. -- @param #number Year The year. - -- @param #boolean InSeconds If true, return the sun rise time in seconds. + -- @param #boolean InSeconds If true, return the sun rise time in seconds. -- @return #string Sunrise time, e.g. "05:41". function COORDINATE:GetSunriseAtDate(Day, Month, Year, InSeconds) - - -- Day of the year. + + -- Day of the year. local DayOfYear=UTILS.GetDayOfYear(Year, Month, Day) - + local Latitude, Longitude=self:GetLLDDM() - + local Tdiff=UTILS.GMTToLocalTimeDifference() - + local sunrise=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, true, Tdiff) if InSeconds then @@ -2114,20 +2409,20 @@ do -- COORDINATE else return UTILS.SecondsToClock(sunrise, true) end - + end - + --- Get sun rise time for a specific day of the year at the coordinate. -- @param #COORDINATE self -- @param #number DayOfYear The day of the year. - -- @param #boolean InSeconds If true, return the sun rise time in seconds. + -- @param #boolean InSeconds If true, return the sun rise time in seconds. -- @return #string Sunrise time, e.g. "05:41". function COORDINATE:GetSunriseAtDayOfYear(DayOfYear, InSeconds) - + local Latitude, Longitude=self:GetLLDDM() - + local Tdiff=UTILS.GMTToLocalTimeDifference() - + local sunrise=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, true, Tdiff) if InSeconds then @@ -2135,38 +2430,38 @@ do -- COORDINATE else return UTILS.SecondsToClock(sunrise, true) end - + end - + --- Get todays sun rise time. -- @param #COORDINATE self - -- @param #boolean InSeconds If true, return the sun rise time in seconds. + -- @param #boolean InSeconds If true, return the sun rise time in seconds. -- @return #string Sunrise time, e.g. "05:41". function COORDINATE:GetSunrise(InSeconds) - - -- Get current day of the year. + + -- Get current day of the year. local DayOfYear=UTILS.GetMissionDayOfYear() - + -- Lat and long at this point. local Latitude, Longitude=self:GetLLDDM() - + -- GMT time diff. local Tdiff=UTILS.GMTToLocalTimeDifference() - + -- Sunrise in seconds of the day. local sunrise=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, true, Tdiff) - + local date=UTILS.GetDCSMissionDate() - + -- Debug output. --self:I(string.format("Sun rise at lat=%.3f long=%.3f on %s (DayOfYear=%d): %s (%d sec of the day) (GMT %d)", Latitude, Longitude, date, DayOfYear, tostring(UTILS.SecondsToClock(sunrise)), sunrise, Tdiff)) - + if InSeconds then return sunrise else return UTILS.SecondsToClock(sunrise, true) end - + end --- Get minutes until the next sun rise at this coordinate. @@ -2174,103 +2469,103 @@ do -- COORDINATE -- @param OnlyToday If true, only calculate the sun rise of today. If sun has already risen, the time in negative minutes since sunrise is reported. -- @return #number Minutes to the next sunrise. function COORDINATE:GetMinutesToSunrise(OnlyToday) - + -- Seconds of today local time=UTILS.SecondsOfToday() -- Next Sunrise in seconds. local sunrise=nil - + -- Time to sunrise. local delta=nil - + if OnlyToday then - + --- -- Sunrise of today --- - + sunrise=self:GetSunrise(true) - + delta=sunrise-time - + else --- -- Sunrise of tomorrow --- - + -- Tomorrows day of the year. local DayOfYear=UTILS.GetMissionDayOfYear()+1 local Latitude, Longitude=self:GetLLDDM() - + local Tdiff=UTILS.GMTToLocalTimeDifference() - + sunrise=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, true, Tdiff) - + delta=sunrise+UTILS.SecondsToMidnight() end return delta/60 end - + --- Check if it is day, i.e. if the sun has risen about the horizon at this coordinate. -- @param #COORDINATE self -- @param #string Clock (Optional) Time in format "HH:MM:SS+D", e.g. "05:40:00+3" to check if is day at 5:40 at third day after mission start. Default is to check right now. -- @return #boolean If true, it is day. If false, it is night time. function COORDINATE:IsDay(Clock) - + if Clock then - + local Time=UTILS.ClockToSeconds(Clock) - + local clock=UTILS.Split(Clock, "+")[1] - + -- Tomorrows day of the year. local DayOfYear=UTILS.GetMissionDayOfYear(Time) local Latitude, Longitude=self:GetLLDDM() - + local Tdiff=UTILS.GMTToLocalTimeDifference() - + local sunrise=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, true, Tdiff) local sunset=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, false, Tdiff) - + local time=UTILS.ClockToSeconds(clock) - - -- Check if time is between sunrise and sunset. - if time>sunrise and time<=sunset then - return true - else - return false - end - - else - - -- Todays sun rise in sec. - local sunrise=self:GetSunrise(true) - - -- Todays sun set in sec. - local sunset=self:GetSunset(true) - - -- Seconds passed since midnight. - local time=UTILS.SecondsOfToday() - + -- Check if time is between sunrise and sunset. if time>sunrise and time<=sunset then return true else return false end - - end - + + else + + -- Todays sun rise in sec. + local sunrise=self:GetSunrise(true) + + -- Todays sun set in sec. + local sunset=self:GetSunset(true) + + -- Seconds passed since midnight. + local time=UTILS.SecondsOfToday() + + -- Check if time is between sunrise and sunset. + if time>sunrise and time<=sunset then + return true + else + return false + end + + end + end - + --- Check if it is night, i.e. if the sun has set below the horizon at this coordinate. - -- @param #COORDINATE self + -- @param #COORDINATE self -- @param #string Clock (Optional) Time in format "HH:MM:SS+D", e.g. "05:40:00+3" to check if is night at 5:40 at third day after mission start. Default is to check right now. -- @return #boolean If true, it is night. If false, it is day time. function COORDINATE:IsNight(Clock) @@ -2282,17 +2577,17 @@ do -- COORDINATE -- @param #number Day The day. -- @param #number Month The month. -- @param #number Year The year. - -- @param #boolean InSeconds If true, return the sun rise time in seconds. + -- @param #boolean InSeconds If true, return the sun rise time in seconds. -- @return #string Sunset time, e.g. "20:41". function COORDINATE:GetSunsetAtDate(Day, Month, Year, InSeconds) - - -- Day of the year. + + -- Day of the year. local DayOfYear=UTILS.GetDayOfYear(Year, Month, Day) - + local Latitude, Longitude=self:GetLLDDM() - + local Tdiff=UTILS.GMTToLocalTimeDifference() - + local sunset=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, false, Tdiff) if InSeconds then @@ -2300,80 +2595,80 @@ do -- COORDINATE else return UTILS.SecondsToClock(sunset, true) end - + end --- Get todays sun set time. -- @param #COORDINATE self - -- @param #boolean InSeconds If true, return the sun set time in seconds. + -- @param #boolean InSeconds If true, return the sun set time in seconds. -- @return #string Sunrise time, e.g. "20:41". function COORDINATE:GetSunset(InSeconds) - - -- Get current day of the year. + + -- Get current day of the year. local DayOfYear=UTILS.GetMissionDayOfYear() - + -- Lat and long at this point. local Latitude, Longitude=self:GetLLDDM() - + -- GMT time diff. local Tdiff=UTILS.GMTToLocalTimeDifference() - + -- Sunrise in seconds of the day. local sunrise=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, false, Tdiff) - + local date=UTILS.GetDCSMissionDate() - + -- Debug output. --self:I(string.format("Sun set at lat=%.3f long=%.3f on %s (DayOfYear=%d): %s (%d sec of the day) (GMT %d)", Latitude, Longitude, date, DayOfYear, tostring(UTILS.SecondsToClock(sunrise)), sunrise, Tdiff)) - + if InSeconds then return sunrise else return UTILS.SecondsToClock(sunrise, true) end - + end - + --- Get minutes until the next sun set at this coordinate. -- @param #COORDINATE self -- @param OnlyToday If true, only calculate the sun set of today. If sun has already set, the time in negative minutes since sunset is reported. -- @return #number Minutes to the next sunrise. function COORDINATE:GetMinutesToSunset(OnlyToday) - + -- Seconds of today local time=UTILS.SecondsOfToday() -- Next Sunset in seconds. local sunset=nil - + -- Time to sunrise. local delta=nil - + if OnlyToday then - + --- -- Sunset of today --- - + sunset=self:GetSunset(true) - + delta=sunset-time - + else --- -- Sunset of tomorrow --- - + -- Tomorrows day of the year. local DayOfYear=UTILS.GetMissionDayOfYear()+1 local Latitude, Longitude=self:GetLLDDM() - + local Tdiff=UTILS.GMTToLocalTimeDifference() - + sunset=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, false, Tdiff) - + delta=sunset+UTILS.SecondsToMidnight() end @@ -2429,7 +2724,7 @@ do -- COORDINATE local Heading = self.Heading local DirectionVec3 = self:GetDirectionVec3( TargetCoordinate ) local Angle = self:GetAngleDegrees( DirectionVec3 ) - + if Heading then local Aspect = Angle - Heading if Aspect > -135 and Aspect <= -45 then @@ -2452,7 +2747,7 @@ do -- COORDINATE -- @param #COORDINATE self -- @return #number Latitude in DDM. -- @return #number Lontitude in DDM. - function COORDINATE:GetLLDDM() + function COORDINATE:GetLLDDM() return coord.LOtoLL( self:GetVec3() ) end @@ -2460,7 +2755,7 @@ do -- COORDINATE -- @param #COORDINATE self -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. -- @return #string The LL DMS Text - function COORDINATE:ToStringLLDMS( Settings ) + function COORDINATE:ToStringLLDMS( Settings ) local LL_Accuracy = Settings and Settings.LL_Accuracy or _SETTINGS.LL_Accuracy local lat, lon = coord.LOtoLL( self:GetVec3() ) @@ -2500,11 +2795,11 @@ do -- COORDINATE -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. -- @return #string The coordinate Text in the configured coordinate system. function COORDINATE:ToStringFromRP( ReferenceCoord, ReferenceName, Controllable, Settings ) - + self:F2( { ReferenceCoord = ReferenceCoord, ReferenceName = ReferenceName } ) local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS - + local IsAir = Controllable and Controllable:IsAirPlane() or false if IsAir then @@ -2518,7 +2813,7 @@ do -- COORDINATE local Distance = self:Get2DDistance( ReferenceCoord ) return "Target are located " .. self:GetBRText( AngleRadians, Distance, Settings ) .. " from " .. ReferenceName end - + return nil end @@ -2528,8 +2823,8 @@ do -- COORDINATE -- @param Wrapper.Controllable#CONTROLLABLE Controllable -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. -- @return #string The coordinate Text in the configured coordinate system. - function COORDINATE:ToStringA2G( Controllable, Settings ) - + function COORDINATE:ToStringA2G( Controllable, Settings ) + self:F2( { Controllable = Controllable and Controllable:GetName() } ) local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS @@ -2564,7 +2859,7 @@ do -- COORDINATE -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. -- @return #string The coordinate Text in the configured coordinate system. function COORDINATE:ToStringA2A( Controllable, Settings, Language ) -- R2.2 - + self:F2( { Controllable = Controllable and Controllable:GetName() } ) local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS @@ -2572,7 +2867,7 @@ do -- COORDINATE if Settings:IsA2A_BRAA() then if Controllable then local Coordinate = Controllable:GetCoordinate() - return self:ToStringBRA( Coordinate, Settings, Language ) + return self:ToStringBRA( Coordinate, Settings, Language ) else return self:ToStringMGRS( Settings, Language ) end @@ -2604,13 +2899,13 @@ do -- COORDINATE -- @param Tasking.Task#TASK Task The task for which coordinates need to be calculated. -- @return #string The coordinate Text in the configured coordinate system. function COORDINATE:ToString( Controllable, Settings, Task ) - + -- self:E( { Controllable = Controllable and Controllable:GetName() } ) local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS local ModeA2A = nil - + if Task then if Task:IsInstanceOf( TASK_A2A ) then ModeA2A = true @@ -2627,8 +2922,8 @@ do -- COORDINATE end end end - - + + if ModeA2A == nil then local IsAir = Controllable and ( Controllable:IsAirPlane() or Controllable:IsHelicopter() ) or false if IsAir then @@ -2637,14 +2932,14 @@ do -- COORDINATE ModeA2A = false end end - + if ModeA2A == true then return self:ToStringA2A( Controllable, Settings ) else return self:ToStringA2G( Controllable, Settings ) end - + return nil end @@ -2657,7 +2952,7 @@ do -- COORDINATE -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. -- @return #string The pressure text in the configured measurement system. function COORDINATE:ToStringPressure( Controllable, Settings ) -- R2.3 - + self:F2( { Controllable = Controllable and Controllable:GetName() } ) local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS @@ -2673,7 +2968,7 @@ do -- COORDINATE -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. -- @return #string The wind text in the configured measurement system. function COORDINATE:ToStringWind( Controllable, Settings ) - + self:F2( { Controllable = Controllable and Controllable:GetName() } ) local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS @@ -2686,10 +2981,10 @@ do -- COORDINATE -- * Can be overridden if for a GROUP containing x clients, a menu was selected to override the default. -- @param #COORDINATE self -- @param Wrapper.Controllable#CONTROLLABLE Controllable - -- @param Core.Settings#SETTINGS + -- @param Core.Settings#SETTINGS -- @return #string The temperature text in the configured measurement system. function COORDINATE:ToStringTemperature( Controllable, Settings ) - + self:F2( { Controllable = Controllable and Controllable:GetName() } ) local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS @@ -2712,8 +3007,8 @@ do -- POINT_VEC3 -- @field #POINT_VEC3.RoutePointType RoutePointType -- @field #POINT_VEC3.RoutePointAction RoutePointAction -- @extends #COORDINATE - - + + --- Defines a 3D point in the simulator and with its methods, you can use or manipulate the point in 3D space. -- -- **Important Note:** Most of the functions in this section were taken from MIST, and reworked to OO concepts. @@ -2799,7 +3094,7 @@ do -- POINT_VEC3 local self = BASE:Inherit( self, COORDINATE:New( x, y, z ) ) -- Core.Point#POINT_VEC3 self:F2( self ) - + return self end @@ -2825,7 +3120,7 @@ do -- POINT_VEC3 local self = BASE:Inherit( self, COORDINATE:NewFromVec3( Vec3 ) ) -- Core.Point#POINT_VEC3 self:F2( self ) - + return self end @@ -2924,7 +3219,7 @@ do -- POINT_VEC2 -- @field DCS#Distance x The x coordinate in meters. -- @field DCS#Distance y the y coordinate in meters. -- @extends Core.Point#COORDINATE - + --- Defines a 2D point in the simulator. The height coordinate (if needed) will be the land height + an optional added height specified. -- -- ## POINT_VEC2 constructor @@ -2952,7 +3247,7 @@ do -- POINT_VEC2 POINT_VEC2 = { ClassName = "POINT_VEC2", } - + --- POINT_VEC2 constructor. @@ -3137,5 +3432,3 @@ do -- POINT_VEC2 end end - - diff --git a/Moose Development/Moose/Core/ScheduleDispatcher.lua b/Moose Development/Moose/Core/ScheduleDispatcher.lua index bfd1dabb4..db04a1500 100644 --- a/Moose Development/Moose/Core/ScheduleDispatcher.lua +++ b/Moose Development/Moose/Core/ScheduleDispatcher.lua @@ -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 diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index df5c431df..e6ab285ba 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -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 diff --git a/Moose Development/Moose/Core/Settings.lua b/Moose Development/Moose/Core/Settings.lua index 3a557de65..74b5fa54b 100644 --- a/Moose Development/Moose/Core/Settings.lua +++ b/Moose Development/Moose/Core/Settings.lua @@ -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 ) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index ebcfb1f99..272dc386d 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -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 nfree0 then + self:ScheduleOnce(Delay, ZONE_BASE.UndrawZone, self) + else + if self.DrawID then + UTILS.RemoveMark(self.DrawID) + end + end + return self +end + +--- Get ID of the zone object drawn on the F10 map. +-- The ID can be used to remove the drawn object from the F10 map view via `UTILS.RemoveMark(MarkID)`. +-- @param #ZONE_BASE self +-- @return #number Unique ID of the +function ZONE_BASE:GetDrawID() + return self.DrawID +end + + --- Smokes the zone boundaries in a color. -- @param #ZONE_BASE self -- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. function ZONE_BASE:SmokeZone( SmokeColor ) self:F2( SmokeColor ) - + end --- Set the randomization probability of a zone to be selected. @@ -335,7 +409,7 @@ end -- @return #ZONE_BASE self function ZONE_BASE:SetZoneProbability( ZoneProbability ) self:F( { self:GetName(), ZoneProbability = ZoneProbability } ) - + self.ZoneProbability = ZoneProbability or 1 return self end @@ -344,8 +418,8 @@ end -- @param #ZONE_BASE self -- @return #number A value between 0 and 1. 0 = 0% and 1 = 100% probability. function ZONE_BASE:GetZoneProbability() - self:F2() - + self:F2() + return self.ZoneProbability end @@ -354,15 +428,15 @@ end -- @return #ZONE_BASE The zone is selected taking into account the randomization probability factor. -- @return #nil The zone is not selected taking into account the randomization probability factor. -- @usage --- +-- -- local ZoneArray = { ZONE:New( "Zone1" ), ZONE:New( "Zone2" ) } --- +-- -- -- We set a zone probability of 70% to the first zone and 30% to the second zone. -- ZoneArray[1]:SetZoneProbability( 0.5 ) -- ZoneArray[2]:SetZoneProbability( 0.5 ) --- +-- -- local ZoneSelected = nil --- +-- -- while ZoneSelected == nil do -- for _, Zone in pairs( ZoneArray ) do -- ZoneSelected = Zone:GetZoneMaybe() @@ -371,12 +445,12 @@ end -- end -- end -- end --- +-- -- -- The result should be that Zone1 would be more probable selected than Zone2. --- +-- function ZONE_BASE:GetZoneMaybe() self:F2() - + local Randomization = math.random() if Randomization <= self.ZoneProbability then return self @@ -394,30 +468,34 @@ end --- The ZONE_RADIUS class defined by a zone name, a location and a radius. -- This class implements the inherited functions from Core.Zone#ZONE_BASE taking into account the own zone format and properties. --- +-- -- ## ZONE_RADIUS constructor --- +-- -- * @{#ZONE_RADIUS.New}(): Constructor. --- +-- -- ## Manage the radius of the zone --- +-- -- * @{#ZONE_RADIUS.SetRadius}(): Sets the radius of the zone. -- * @{#ZONE_RADIUS.GetRadius}(): Returns the radius of the zone. --- +-- -- ## Manage the location of the zone --- +-- -- * @{#ZONE_RADIUS.SetVec2}(): Sets the @{DCS#Vec2} of the zone. -- * @{#ZONE_RADIUS.GetVec2}(): Returns the @{DCS#Vec2} of the zone. -- * @{#ZONE_RADIUS.GetVec3}(): Returns the @{DCS#Vec3} of the zone, taking an additional height parameter. --- +-- -- ## Zone point randomization --- +-- -- Various functions exist to find random points within the zone. --- +-- -- * @{#ZONE_RADIUS.GetRandomVec2}(): Gets a random 2D point in the zone. -- * @{#ZONE_RADIUS.GetRandomPointVec2}(): Gets a @{Core.Point#POINT_VEC2} object representing a random 2D point in the zone. -- * @{#ZONE_RADIUS.GetRandomPointVec3}(): Gets a @{Core.Point#POINT_VEC3} object representing a random 3D point in the zone. Note that the height of the point is at landheight. --- +-- +-- ## Draw zone +-- +-- * @{#ZONE_RADIUS.DrawZone}(): Draws the zone on the F10 map. +-- -- @field #ZONE_RADIUS ZONE_RADIUS = { ClassName="ZONE_RADIUS", @@ -437,9 +515,9 @@ function ZONE_RADIUS:New( ZoneName, Vec2, Radius ) self.Radius = Radius self.Vec2 = Vec2 - + --self.Coordinate=COORDINATE:NewFromVec2(Vec2) - + return self end @@ -491,18 +569,44 @@ function ZONE_RADIUS:MarkZone(Points) local Angle local RadialBase = math.pi*2 - + for Angle = 0, 360, (360 / Points ) do - + local Radial = Angle * RadialBase / 360 - + Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() - + COORDINATE:NewFromVec2(Point):MarkToAll(self:GetName()) end - + +end + +--- Draw the zone circle on the F10 map. +-- @param #ZONE_RADIUS self +-- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. +-- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red. +-- @param #number Alpha Transparency [0,1]. Default 1. +-- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. +-- @param #number FillAlpha Transparency [0,1]. Default 0.15. +-- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. +-- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. +-- @return #ZONE_RADIUS self +function ZONE_RADIUS:DrawZone(Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) + + local coordinate=self:GetCoordinate() + + local Radius=self:GetRadius() + + Color=Color or self:GetColorRGB() + Alpha=Alpha or 1 + FillColor=FillColor or Color + FillAlpha=FillAlpha or self:GetColorAlpha() + + self.DrawID=coordinate:CircleToAll(Radius, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) + + return self end --- Bounds the zone with tires. @@ -520,17 +624,17 @@ function ZONE_RADIUS:BoundZone( Points, CountryID, UnBound ) local Angle local RadialBase = math.pi*2 - + -- for Angle = 0, 360, (360 / Points ) do local Radial = Angle * RadialBase / 360 Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() - + local CountryName = _DATABASE.COUNTRY_NAME[CountryID] - + local Tire = { - ["country"] = CountryName, + ["country"] = CountryName, ["category"] = "Fortifications", ["canCargo"] = false, ["shape_name"] = "H-tyre_B_WF", @@ -564,7 +668,7 @@ function ZONE_RADIUS:SmokeZone( SmokeColor, Points, AddHeight, AngleOffset ) local Point = {} local Vec2 = self:GetVec2() - + AddHeight = AddHeight or 0 AngleOffset = AngleOffset or 0 @@ -572,7 +676,7 @@ function ZONE_RADIUS:SmokeZone( SmokeColor, Points, AddHeight, AngleOffset ) local Angle local RadialBase = math.pi*2 - + for Angle = 0, 360, 360 / Points do local Radial = ( Angle + AngleOffset ) * RadialBase / 360 Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() @@ -596,14 +700,14 @@ function ZONE_RADIUS:FlareZone( FlareColor, Points, Azimuth, AddHeight ) local Point = {} local Vec2 = self:GetVec2() - + AddHeight = AddHeight or 0 - + Points = Points and Points or 360 local Angle local RadialBase = math.pi*2 - + for Angle = 0, 360, 360 / Points do local Radial = Angle * RadialBase / 360 Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() @@ -645,8 +749,8 @@ function ZONE_RADIUS:GetVec2() self:F2( self.ZoneName ) self:T2( { self.Vec2 } ) - - return self.Vec2 + + return self.Vec2 end --- Sets the @{DCS#Vec2} of the zone. @@ -655,12 +759,12 @@ end -- @return DCS#Vec2 The new location of the zone. function ZONE_RADIUS:SetVec2( Vec2 ) self:F2( self.ZoneName ) - + self.Vec2 = Vec2 self:T2( { self.Vec2 } ) - - return self.Vec2 + + return self.Vec2 end --- Returns the @{DCS#Vec3} of the ZONE_RADIUS. @@ -676,8 +780,8 @@ function ZONE_RADIUS:GetVec3( Height ) local Vec3 = { x = Vec2.x, y = land.getHeight( self:GetVec2() ) + Height, z = Vec2.y } self:T2( { Vec3 } ) - - return Vec3 + + return Vec3 end @@ -686,7 +790,7 @@ end --- Scan the zone for the presence of units of the given ObjectCategories. -- Note that after a zone has been scanned, the zone can be evaluated by: --- +-- -- * @{ZONE_RADIUS.IsAllInZoneOfCoalition}(): Scan the presence of units in the zone of a coalition. -- * @{ZONE_RADIUS.IsAllInZoneOfOtherCoalition}(): Scan the presence of units in the zone of an other coalition. -- * @{ZONE_RADIUS.IsSomeInZoneOfCoalition}(): Scan if there is some presence of units in the zone of the given coalition. @@ -708,7 +812,7 @@ function ZONE_RADIUS:Scan( ObjectCategories, UnitCategories ) local ZoneCoord = self:GetCoordinate() local ZoneRadius = self:GetRadius() - + self:F({ZoneCoord = ZoneCoord, ZoneRadius = ZoneRadius, ZoneCoordLL = ZoneCoord:ToStringLLDMS()}) local SphereSearch = { @@ -721,18 +825,18 @@ function ZONE_RADIUS:Scan( ObjectCategories, UnitCategories ) local function EvaluateZone( ZoneObject ) --if ZoneObject:isExist() then --FF: isExist always returns false for SCENERY objects since DCS 2.2 and still in DCS 2.5 - if ZoneObject then - + if ZoneObject then + local ObjectCategory = ZoneObject:getCategory() - + --local name=ZoneObject:getName() --env.info(string.format("Zone object %s", tostring(name))) --self:E(ZoneObject) - + if ( ObjectCategory == Object.Category.UNIT and ZoneObject:isExist() and ZoneObject:isActive() ) or (ObjectCategory == Object.Category.STATIC and ZoneObject:isExist()) then - + local CoalitionDCSUnit = ZoneObject:getCoalition() - + local Include = false if not UnitCategories then -- Anythink found is included. @@ -740,29 +844,29 @@ function ZONE_RADIUS:Scan( ObjectCategories, UnitCategories ) else -- Check if found object is in specified categories. local CategoryDCSUnit = ZoneObject:getDesc().category - + for UnitCategoryID, UnitCategory in pairs( UnitCategories ) do if UnitCategory == CategoryDCSUnit then Include = true break end end - + end - + if Include then - + local CoalitionDCSUnit = ZoneObject:getCoalition() - + -- This coalition is inside the zone. self.ScanData.Coalitions[CoalitionDCSUnit] = true - + self.ScanData.Units[ZoneObject] = ZoneObject - + self:F2( { Name = ZoneObject:getName(), Coalition = CoalitionDCSUnit } ) end end - + if ObjectCategory == Object.Category.SCENERY then local SceneryType = ZoneObject:getTypeName() local SceneryName = ZoneObject:getName() @@ -770,15 +874,15 @@ function ZONE_RADIUS:Scan( ObjectCategories, UnitCategories ) self.ScanData.Scenery[SceneryType][SceneryName] = SCENERY:Register( SceneryName, ZoneObject ) self:F2( { SCENERY = self.ScanData.Scenery[SceneryType][SceneryName] } ) end - + end - + return true end -- Search objects. world.searchObjects( ObjectCategories, SphereSearch, EvaluateZone ) - + end --- Count the number of different coalitions inside the zone. @@ -823,14 +927,14 @@ end function ZONE_RADIUS:GetScannedSetGroup() self.ScanSetGroup=self.ScanSetGroup or SET_GROUP:New() --Core.Set#SET_GROUP - + self.ScanSetGroup.Set={} if self.ScanData then for ObjectID, UnitObject in pairs( self.ScanData.Units ) do local UnitObject = UnitObject -- DCS#Unit if UnitObject:isExist() then - + local FoundUnit=UNIT:FindByName(UnitObject:getName()) if FoundUnit then local group=FoundUnit:GetGroup() @@ -850,11 +954,11 @@ end function ZONE_RADIUS:CountScannedCoalitions() local Count = 0 - + for CoalitionID, Coalition in pairs( self.ScanData.Coalitions ) do Count = Count + 1 end - + return Count end @@ -882,16 +986,16 @@ function ZONE_RADIUS:GetScannedCoalition( Coalition ) else local Count = 0 local ReturnCoalition = nil - + for CoalitionID, Coalition in pairs( self.ScanData.Coalitions ) do Count = Count + 1 ReturnCoalition = CoalitionID end - + if Count ~= 1 then ReturnCoalition = nil end - + return ReturnCoalition end end @@ -1002,7 +1106,7 @@ function ZONE_RADIUS:SearchZone( EvaluateFunction, ObjectCategories ) local ZoneCoord = self:GetCoordinate() local ZoneRadius = self:GetRadius() - + self:F({ZoneCoord = ZoneCoord, ZoneRadius = ZoneRadius, ZoneCoordLL = ZoneCoord:ToStringLLDMS()}) local SphereSearch = { @@ -1014,8 +1118,8 @@ function ZONE_RADIUS:SearchZone( EvaluateFunction, ObjectCategories ) } local function EvaluateZone( ZoneDCSUnit ) - - + + local ZoneUnit = UNIT:Find( ZoneDCSUnit ) return EvaluateFunction( ZoneUnit ) @@ -1031,15 +1135,15 @@ end -- @return #boolean true if the location is within the zone. function ZONE_RADIUS:IsVec2InZone( Vec2 ) self:F2( Vec2 ) - + local ZoneVec2 = self:GetVec2() - + if ZoneVec2 then if (( Vec2.x - ZoneVec2.x )^2 + ( Vec2.y - ZoneVec2.y ) ^2 ) ^ 0.5 <= self:GetRadius() then return true end end - + return false end @@ -1057,24 +1161,48 @@ end --- Returns a random Vec2 location within the zone. -- @param #ZONE_RADIUS self --- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. --- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. +-- @param #number inner (Optional) Minimal distance from the center of the zone. Default is 0. +-- @param #number outer (Optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. +-- @param #table surfacetypes (Optional) Table of surface types. Can also be a single surface type. We will try max 1000 times to find the right type! -- @return DCS#Vec2 The random location within the zone. -function ZONE_RADIUS:GetRandomVec2( inner, outer ) - self:F( self.ZoneName, inner, outer ) +function ZONE_RADIUS:GetRandomVec2(inner, outer, surfacetypes) - local Point = {} local Vec2 = self:GetVec2() local _inner = inner or 0 local _outer = outer or self:GetRadius() - local angle = math.random() * math.pi * 2; - Point.x = Vec2.x + math.cos( angle ) * math.random(_inner, _outer); - Point.y = Vec2.y + math.sin( angle ) * math.random(_inner, _outer); - - self:T( { Point } ) - - return Point + if surfacetypes and type(surfacetypes)~="table" then + surfacetypes={surfacetypes} + end + + local function _getpoint() + local point = {} + local angle = math.random() * math.pi * 2 + point.x = Vec2.x + math.cos(angle) * math.random(_inner, _outer) + point.y = Vec2.y + math.sin(angle) * math.random(_inner, _outer) + return point + end + + local function _checkSurface(point) + for _,sf in pairs(surfacetypes) do + if sf==land.getSurfaceType(point) then + return true + end + end + return false + end + + local point=_getpoint() + + if surfacetypes then + local N=1 ; local Nmax=1000 + while _checkSurface(point)==false and N<=Nmax do + point=_getpoint() + N=N+1 + end + end + + return point end --- Returns a @{Core.Point#POINT_VEC2} object reflecting a random 2D location within the zone. @@ -1088,7 +1216,7 @@ function ZONE_RADIUS:GetRandomPointVec2( inner, outer ) local PointVec2 = POINT_VEC2:NewFromVec2( self:GetRandomVec2( inner, outer ) ) self:T3( { PointVec2 } ) - + return PointVec2 end @@ -1103,7 +1231,7 @@ function ZONE_RADIUS:GetRandomVec3( inner, outer ) local Vec2 = self:GetRandomVec2( inner, outer ) self:T3( { x = Vec2.x, y = self.y, z = Vec2.y } ) - + return { x = Vec2.x, y = self.y, z = Vec2.y } end @@ -1119,23 +1247,23 @@ function ZONE_RADIUS:GetRandomPointVec3( inner, outer ) local PointVec3 = POINT_VEC3:NewFromVec2( self:GetRandomVec2( inner, outer ) ) self:T3( { PointVec3 } ) - + return PointVec3 end --- Returns a @{Core.Point#COORDINATE} object reflecting a random 3D location within the zone. -- @param #ZONE_RADIUS self --- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. --- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. --- @return Core.Point#COORDINATE -function ZONE_RADIUS:GetRandomCoordinate( inner, outer ) - self:F( self.ZoneName, inner, outer ) +-- @param #number inner (Optional) Minimal distance from the center of the zone. Default is 0. +-- @param #number outer (Optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. +-- @param #table surfacetypes (Optional) Table of surface types. Can also be a single surface type. We will try max 1000 times to find the right type! +-- @return Core.Point#COORDINATE The random coordinate. +function ZONE_RADIUS:GetRandomCoordinate(inner, outer, surfacetypes) - local Coordinate = COORDINATE:NewFromVec2( self:GetRandomVec2(inner, outer) ) + local vec2=self:GetRandomVec2(inner, outer, surfacetypes) + + local Coordinate = COORDINATE:NewFromVec2(vec2) - self:T3( { Coordinate = Coordinate } ) - return Coordinate end @@ -1147,55 +1275,70 @@ end --- The ZONE class, defined by the zone name as defined within the Mission Editor. -- This class implements the inherited functions from @{#ZONE_RADIUS} taking into account the own zone format and properties. --- +-- -- ## ZONE constructor --- +-- -- * @{#ZONE.New}(): Constructor. This will search for a trigger zone with the name given, and will return for you a ZONE object. --- +-- -- ## Declare a ZONE directly in the DCS mission editor! --- +-- -- You can declare a ZONE using the DCS mission editor by adding a trigger zone in the mission editor. --- +-- -- Then during mission startup, when loading Moose.lua, this trigger zone will be detected as a ZONE declaration. -- Within the background, a ZONE object will be created within the @{Core.Database}. -- The ZONE name will be the trigger zone name. --- +-- -- So, you can search yourself for the ZONE object by using the @{#ZONE.FindByName}() method. -- In this example, `local TriggerZone = ZONE:FindByName( "DefenseZone" )` would return the ZONE object --- that was created at mission startup, and reference it into the `TriggerZone` local object. --- +-- that was created at mission startup, and reference it into the `TriggerZone` local object. +-- -- Refer to mission `ZON-110` for a demonstration. --- +-- -- This is especially handy if you want to quickly setup a SET_ZONE... -- So when you would declare `local SetZone = SET_ZONE:New():FilterPrefixes( "Defense" ):FilterStart()`, -- then SetZone would contain the ZONE object `DefenseZone` as part of the zone collection, --- without much scripting overhead!!! --- --- --- @field #ZONE +-- without much scripting overhead!!! +-- +-- +-- @field #ZONE ZONE = { ClassName="ZONE", } ---- Constructor of ZONE, taking the zone name. +--- Constructor of ZONE taking the zone name. -- @param #ZONE self -- @param #string ZoneName The name of the zone as defined within the mission editor. --- @return #ZONE +-- @return #ZONE self function ZONE:New( ZoneName ) + -- First try to find the zone in the DB. + local zone=_DATABASE:FindZone(ZoneName) + + if zone then + --env.info("FF found zone in DB") + return zone + end + + -- Get zone from DCS trigger function. local Zone = trigger.misc.getZone( ZoneName ) - + + -- Error! if not Zone then error( "Zone " .. ZoneName .. " does not exist." ) return nil end - local self = BASE:Inherit( self, ZONE_RADIUS:New( ZoneName, { x = Zone.point.x, y = Zone.point.z }, Zone.radius ) ) - self:F( ZoneName ) + -- Create a new ZONE_RADIUS. + local self=BASE:Inherit( self, ZONE_RADIUS:New(ZoneName, {x=Zone.point.x, y=Zone.point.z}, Zone.radius)) + self:F(ZoneName) + -- Color of zone. + self.Color={1, 0, 0, 0.15} + + -- DCS zone. self.Zone = Zone - + return self end @@ -1204,7 +1347,7 @@ end -- @param #string ZoneName The name of the zone. -- @return #ZONE_BASE self function ZONE:FindByName( ZoneName ) - + local ZoneFound = _DATABASE:FindZone( ZoneName ) return ZoneFound end @@ -1217,15 +1360,15 @@ end --- # ZONE_UNIT class, extends @{Zone#ZONE_RADIUS} --- +-- -- The ZONE_UNIT class defined by a zone attached to a @{Wrapper.Unit#UNIT} with a radius and optional offsets. -- This class implements the inherited functions from @{#ZONE_RADIUS} taking into account the own zone format and properties. --- +-- -- @field #ZONE_UNIT ZONE_UNIT = { ClassName="ZONE_UNIT", } - + --- Constructor to create a ZONE_UNIT instance, taking the zone name, a zone unit and a radius and optional offsets in X and Y directions. -- @param #ZONE_UNIT self -- @param #string ZoneName Name of the zone. @@ -1240,30 +1383,30 @@ ZONE_UNIT = { -- dx, dy OR rho, theta may be used, not both. -- @return #ZONE_UNIT self function ZONE_UNIT:New( ZoneName, ZoneUNIT, Radius, Offset) - + if Offset then - -- check if the inputs was reasonable, either (dx, dy) or (rho, theta) can be given, else raise an exception. + -- check if the inputs was reasonable, either (dx, dy) or (rho, theta) can be given, else raise an exception. if (Offset.dx or Offset.dy) and (Offset.rho or Offset.theta) then - error("Cannot use (dx, dy) with (rho, theta)") + error("Cannot use (dx, dy) with (rho, theta)") end - + self.dy = Offset.dy or 0.0 self.dx = Offset.dx or 0.0 self.rho = Offset.rho or 0.0 self.theta = (Offset.theta or 0.0) * math.pi / 180.0 self.relative_to_unit = Offset.relative_to_unit or false end - + local self = BASE:Inherit( self, ZONE_RADIUS:New( ZoneName, ZoneUNIT:GetVec2(), Radius ) ) self:F( { ZoneName, ZoneUNIT:GetVec2(), Radius } ) self.ZoneUNIT = ZoneUNIT self.LastVec2 = ZoneUNIT:GetVec2() - + -- Zone objects are added to the _DATABASE and SET_ZONE objects. _EVENTDISPATCHER:CreateEventNewZone( self ) - + return self end @@ -1273,32 +1416,32 @@ end -- @return DCS#Vec2 The location of the zone based on the @{Wrapper.Unit#UNIT}location and the offset, if any. function ZONE_UNIT:GetVec2() self:F2( self.ZoneName ) - + local ZoneVec2 = self.ZoneUNIT:GetVec2() if ZoneVec2 then - + local heading if self.relative_to_unit then heading = ( self.ZoneUNIT:GetHeading() or 0.0 ) * math.pi / 180.0 else heading = 0.0 end - + -- update the zone position with the offsets. if (self.dx or self.dy) then - + -- use heading to rotate offset relative to unit using rotation matrix in 2D. -- see: https://en.wikipedia.org/wiki/Rotation_matrix - ZoneVec2.x = ZoneVec2.x + self.dx * math.cos( -heading ) + self.dy * math.sin( -heading ) - ZoneVec2.y = ZoneVec2.y - self.dx * math.sin( -heading ) + self.dy * math.cos( -heading ) + ZoneVec2.x = ZoneVec2.x + self.dx * math.cos( -heading ) + self.dy * math.sin( -heading ) + ZoneVec2.y = ZoneVec2.y - self.dx * math.sin( -heading ) + self.dy * math.cos( -heading ) end - + -- if using the polar coordinates - if (self.rho or self.theta) then + if (self.rho or self.theta) then ZoneVec2.x = ZoneVec2.x + self.rho * math.cos( self.theta + heading ) ZoneVec2.y = ZoneVec2.y + self.rho * math.sin( self.theta + heading ) end - + self.LastVec2 = ZoneVec2 return ZoneVec2 else @@ -1307,7 +1450,7 @@ function ZONE_UNIT:GetVec2() self:T2( { ZoneVec2 } ) - return nil + return nil end --- Returns a random location within the zone. @@ -1319,7 +1462,7 @@ function ZONE_UNIT:GetRandomVec2() local RandomVec2 = {} --local Vec2 = self.ZoneUNIT:GetVec2() -- FF: This does not take care of the new offset feature! local Vec2 = self:GetVec2() - + if not Vec2 then Vec2 = self.LastVec2 end @@ -1327,9 +1470,9 @@ function ZONE_UNIT:GetRandomVec2() local angle = math.random() * math.pi*2; RandomVec2.x = Vec2.x + math.cos( angle ) * math.random() * self:GetRadius(); RandomVec2.y = Vec2.y + math.sin( angle ) * math.random() * self:GetRadius(); - + self:T( { RandomVec2 } ) - + return RandomVec2 end @@ -1339,16 +1482,16 @@ end -- @return DCS#Vec3 The point of the zone. function ZONE_UNIT:GetVec3( Height ) self:F2( self.ZoneName ) - + Height = Height or 0 - + local Vec2 = self:GetVec2() local Vec3 = { x = Vec2.x, y = land.getHeight( self:GetVec2() ) + Height, z = Vec2.y } self:T2( { Vec3 } ) - - return Vec3 + + return Vec3 end --- @type ZONE_GROUP @@ -1357,12 +1500,12 @@ end --- The ZONE_GROUP class defines by a zone around a @{Wrapper.Group#GROUP} with a radius. The current leader of the group defines the center of the zone. -- This class implements the inherited functions from @{Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. --- +-- -- @field #ZONE_GROUP ZONE_GROUP = { ClassName="ZONE_GROUP", } - + --- Constructor to create a ZONE_GROUP instance, taking the zone name, a zone @{Wrapper.Group#GROUP} and a radius. -- @param #ZONE_GROUP self -- @param #string ZoneName Name of the zone. @@ -1378,7 +1521,7 @@ function ZONE_GROUP:New( ZoneName, ZoneGROUP, Radius ) -- Zone objects are added to the _DATABASE and SET_ZONE objects. _EVENTDISPATCHER:CreateEventNewZone( self ) - + return self end @@ -1388,9 +1531,9 @@ end -- @return DCS#Vec2 The location of the zone based on the @{Wrapper.Group} location. function ZONE_GROUP:GetVec2() self:F( self.ZoneName ) - + local ZoneVec2 = nil - + if self._.ZoneGROUP:IsAlive() then ZoneVec2 = self._.ZoneGROUP:GetVec2() self._.ZoneVec2Cache = ZoneVec2 @@ -1399,7 +1542,7 @@ function ZONE_GROUP:GetVec2() end self:T( { ZoneVec2 } ) - + return ZoneVec2 end @@ -1415,9 +1558,9 @@ function ZONE_GROUP:GetRandomVec2() local angle = math.random() * math.pi*2; Point.x = Vec2.x + math.cos( angle ) * math.random() * self:GetRadius(); Point.y = Vec2.y + math.sin( angle ) * math.random() * self:GetRadius(); - + self:T( { Point } ) - + return Point end @@ -1432,7 +1575,7 @@ function ZONE_GROUP:GetRandomPointVec2( inner, outer ) local PointVec2 = POINT_VEC2:NewFromVec2( self:GetRandomVec2() ) self:T3( { PointVec2 } ) - + return PointVec2 end @@ -1445,15 +1588,21 @@ end --- The ZONE_POLYGON_BASE class defined by a sequence of @{Wrapper.Group#GROUP} waypoints within the Mission Editor, forming a polygon. -- This class implements the inherited functions from @{Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. -- This class is an abstract BASE class for derived classes, and is not meant to be instantiated. --- +-- -- ## Zone point randomization --- +-- -- Various functions exist to find random points within the zone. --- +-- -- * @{#ZONE_POLYGON_BASE.GetRandomVec2}(): Gets a random 2D point in the zone. -- * @{#ZONE_POLYGON_BASE.GetRandomPointVec2}(): Return a @{Core.Point#POINT_VEC2} object representing a random 2D point within the zone. -- * @{#ZONE_POLYGON_BASE.GetRandomPointVec3}(): Return a @{Core.Point#POINT_VEC3} object representing a random 3D point at landheight within the zone. --- +-- +-- ## Draw zone +-- +-- * @{#ZONE_POLYGON_BASE.DrawZone}(): Draws the zone on the F10 map. +-- * @{#ZONE_POLYGON_BASE.Boundary}(): Draw a frontier on the F10 map with small filled circles. +-- +-- -- @field #ZONE_POLYGON_BASE ZONE_POLYGON_BASE = { ClassName="ZONE_POLYGON_BASE", @@ -1482,13 +1631,13 @@ function ZONE_POLYGON_BASE:New( ZoneName, PointsArray ) if PointsArray then self._.Polygon = {} - + for i = 1, #PointsArray do self._.Polygon[i] = {} self._.Polygon[i].x = PointsArray[i].x self._.Polygon[i].y = PointsArray[i].y end - + end return self @@ -1501,7 +1650,7 @@ end function ZONE_POLYGON_BASE:UpdateFromVec2(Vec2Array) self._.Polygon = {} - + for i=1,#Vec2Array do self._.Polygon[i] = {} self._.Polygon[i].x=Vec2Array[i].x @@ -1518,7 +1667,7 @@ end function ZONE_POLYGON_BASE:UpdateFromVec3(Vec3Array) self._.Polygon = {} - + for i=1,#Vec3Array do self._.Polygon[i] = {} self._.Polygon[i].x=Vec3Array[i].x @@ -1529,14 +1678,86 @@ function ZONE_POLYGON_BASE:UpdateFromVec3(Vec3Array) end --- Returns the center location of the polygon. --- @param #ZONE_GROUP self +-- @param #ZONE_POLYGON_BASE self -- @return DCS#Vec2 The location of the zone based on the @{Wrapper.Group} location. function ZONE_POLYGON_BASE:GetVec2() self:F( self.ZoneName ) local Bounds = self:GetBoundingSquare() - - return { x = ( Bounds.x2 + Bounds.x1 ) / 2, y = ( Bounds.y2 + Bounds.y1 ) / 2 } + + return { x = ( Bounds.x2 + Bounds.x1 ) / 2, y = ( Bounds.y2 + Bounds.y1 ) / 2 } +end + +--- Get a vertex of the polygon. +-- @param #ZONE_POLYGON_BASE self +-- @param #number Index Index of the vertex. Default 1. +-- @return DCS#Vec2 Vertex of the polygon. +function ZONE_POLYGON_BASE:GetVertexVec2(Index) + return self._.Polygon[Index or 1] +end + +--- Get a vertex of the polygon. +-- @param #ZONE_POLYGON_BASE self +-- @param #number Index Index of the vertex. Default 1. +-- @return DCS#Vec3 Vertex of the polygon. +function ZONE_POLYGON_BASE:GetVertexVec3(Index) + local vec2=self:GetVertexVec2(Index) + if vec2 then + local vec3={x=vec2.x, y=land.getHeight(vec2), z=vec2.y} + return vec3 + end + return nil +end + +--- Get a vertex of the polygon. +-- @param #ZONE_POLYGON_BASE self +-- @param #number Index Index of the vertex. Default 1. +-- @return Core.Point#COORDINATE Vertex of the polygon. +function ZONE_POLYGON_BASE:GetVertexCoordinate(Index) + local vec2=self:GetVertexVec2(Index) + if vec2 then + local coord=COORDINATE:NewFromVec2(vec2) + return coord + end + return nil +end + + +--- Get a list of verticies of the polygon. +-- @param #ZONE_POLYGON_BASE self +-- @return List of DCS#Vec2 verticies defining the edges of the polygon. +function ZONE_POLYGON_BASE:GetVerticiesVec2() + return self._.Polygon +end + +--- Get a list of verticies of the polygon. +-- @param #ZONE_POLYGON_BASE self +-- @return #table List of DCS#Vec3 verticies defining the edges of the polygon. +function ZONE_POLYGON_BASE:GetVerticiesVec3() + + local coords={} + + for i,vec2 in ipairs(self._.Polygon) do + local vec3={x=vec2.x, y=land.getHeight(vec2), z=vec2.y} + table.insert(coords, vec3) + end + + return coords +end + +--- Get a list of verticies of the polygon. +-- @param #ZONE_POLYGON_BASE self +-- @return #table List of COORDINATES verticies defining the edges of the polygon. +function ZONE_POLYGON_BASE:GetVerticiesCoordinates() + + local coords={} + + for i,vec2 in ipairs(self._.Polygon) do + local coord=COORDINATE:NewFromVec2(vec2) + table.insert(coords, coord) + end + + return coords end --- Flush polygon coordinates as a table in DCS.log. @@ -1556,24 +1777,24 @@ end -- @return #ZONE_POLYGON_BASE self function ZONE_POLYGON_BASE:BoundZone( UnBound ) - local i - local j + local i + local j local Segments = 10 - + i = 1 j = #self._.Polygon - + while i <= #self._.Polygon do self:T( { i, j, self._.Polygon[i], self._.Polygon[j] } ) - + local DeltaX = self._.Polygon[j].x - self._.Polygon[i].x local DeltaY = self._.Polygon[j].y - self._.Polygon[i].y - + for Segment = 0, Segments do -- We divide each line in 5 segments and smoke a point on the line. local PointX = self._.Polygon[i].x + ( Segment * DeltaX / Segments ) local PointY = self._.Polygon[i].y + ( Segment * DeltaY / Segments ) local Tire = { - ["country"] = "USA", + ["country"] = "USA", ["category"] = "Fortifications", ["canCargo"] = false, ["shape_name"] = "H-tyre_B_WF", @@ -1583,12 +1804,12 @@ function ZONE_POLYGON_BASE:BoundZone( UnBound ) ["name"] = string.format( "%s-Tire #%0d", self:GetName(), ((i - 1) * Segments) + Segment ), ["heading"] = 0, } -- end of ["group"] - + local Group = coalition.addStaticObject( country.id.USA, Tire ) if UnBound and UnBound == true then Group:destroy() end - + end j = i i = i + 1 @@ -1598,6 +1819,46 @@ function ZONE_POLYGON_BASE:BoundZone( UnBound ) end +--- Draw the zone on the F10 map. **NOTE** Currently, only polygons with **exactly four points** are supported! +-- @param #ZONE_POLYGON_BASE self +-- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All. +-- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red. +-- @param #number Alpha Transparency [0,1]. Default 1. +-- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. +-- @param #number FillAlpha Transparency [0,1]. Default 0.15. +-- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid. +-- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. +-- @return #ZONE_POLYGON_BASE self +function ZONE_POLYGON_BASE:DrawZone(Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) + + local coordinate=COORDINATE:NewFromVec2(self._.Polygon[1]) + + Color=Color or self:GetColorRGB() + Alpha=Alpha or 1 + FillColor=FillColor or Color + FillAlpha=FillAlpha or self:GetColorAlpha() + + + if #self._.Polygon==4 then + + local Coord2=COORDINATE:NewFromVec2(self._.Polygon[2]) + local Coord3=COORDINATE:NewFromVec2(self._.Polygon[3]) + local Coord4=COORDINATE:NewFromVec2(self._.Polygon[4]) + + self.DrawID=coordinate:QuadToAll(Coord2, Coord3, Coord4, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) + + else + + local Coordinates=self:GetVerticiesCoordinates() + table.remove(Coordinates, 1) + + self.DrawID=coordinate:MarkupToAllFreeForm(Coordinates, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly) + + end + + + return self +end --- Smokes the zone boundaries in a color. -- @param #ZONE_POLYGON_BASE self @@ -1608,16 +1869,16 @@ function ZONE_POLYGON_BASE:SmokeZone( SmokeColor, Segments ) self:F2( SmokeColor ) Segments=Segments or 10 - + local i=1 local j=#self._.Polygon - + while i <= #self._.Polygon do self:T( { i, j, self._.Polygon[i], self._.Polygon[j] } ) - + local DeltaX = self._.Polygon[j].x - self._.Polygon[i].x local DeltaY = self._.Polygon[j].y - self._.Polygon[i].y - + for Segment = 0, Segments do -- We divide each line in 5 segments and smoke a point on the line. local PointX = self._.Polygon[i].x + ( Segment * DeltaX / Segments ) local PointY = self._.Polygon[i].y + ( Segment * DeltaY / Segments ) @@ -1642,18 +1903,18 @@ function ZONE_POLYGON_BASE:FlareZone( FlareColor, Segments, Azimuth, AddHeight ) self:F2(FlareColor) Segments=Segments or 10 - + AddHeight = AddHeight or 0 - + local i=1 local j=#self._.Polygon - + while i <= #self._.Polygon do self:T( { i, j, self._.Polygon[i], self._.Polygon[j] } ) - + local DeltaX = self._.Polygon[j].x - self._.Polygon[i].x local DeltaY = self._.Polygon[j].y - self._.Polygon[i].y - + for Segment = 0, Segments do -- We divide each line in 5 segments and smoke a point on the line. local PointX = self._.Polygon[i].x + ( Segment * DeltaX / Segments ) local PointY = self._.Polygon[i].y + ( Segment * DeltaY / Segments ) @@ -1677,17 +1938,17 @@ end function ZONE_POLYGON_BASE:IsVec2InZone( Vec2 ) self:F2( Vec2 ) - local Next - local Prev + local Next + local Prev local InPolygon = false - + Next = 1 Prev = #self._.Polygon - + while Next <= #self._.Polygon do self:T( { Next, Prev, self._.Polygon[Next], self._.Polygon[Prev] } ) if ( ( ( self._.Polygon[Next].y > Vec2.y ) ~= ( self._.Polygon[Prev].y > Vec2.y ) ) and - ( Vec2.x < ( self._.Polygon[Prev].x - self._.Polygon[Next].x ) * ( Vec2.y - self._.Polygon[Next].y ) / ( self._.Polygon[Prev].y - self._.Polygon[Next].y ) + self._.Polygon[Next].x ) + ( Vec2.x < ( self._.Polygon[Prev].x - self._.Polygon[Next].x ) * ( Vec2.y - self._.Polygon[Next].y ) / ( self._.Polygon[Prev].y - self._.Polygon[Next].y ) + self._.Polygon[Next].x ) ) then InPolygon = not InPolygon end @@ -1704,26 +1965,28 @@ end -- @param #ZONE_POLYGON_BASE self -- @return DCS#Vec2 The Vec2 coordinate. function ZONE_POLYGON_BASE:GetRandomVec2() - self:F2() - --- It is a bit tricky to find a random point within a polygon. Right now i am doing it the dirty and inefficient way... - local Vec2Found = false - local Vec2 + -- It is a bit tricky to find a random point within a polygon. Right now i am doing it the dirty and inefficient way... + + -- Get the bounding square. local BS = self:GetBoundingSquare() - - self:T2( BS ) - - while Vec2Found == false do - Vec2 = { x = math.random( BS.x1, BS.x2 ), y = math.random( BS.y1, BS.y2 ) } - self:T2( Vec2 ) - if self:IsVec2InZone( Vec2 ) then - Vec2Found = true - end - end - - self:T2( Vec2 ) - return Vec2 + local Nmax=1000 ; local n=0 + while n self._.Polygon[i].x ) and self._.Polygon[i].x or x1 x2 = ( x2 < self._.Polygon[i].x ) and self._.Polygon[i].x or x2 y1 = ( y1 > self._.Polygon[i].y ) and self._.Polygon[i].y or y1 y2 = ( y2 < self._.Polygon[i].y ) and self._.Polygon[i].y or y2 - + end return { x1 = x1, y1 = y1, x2 = x2, y2 = y2 } end +--- Draw a frontier on the F10 map with small filled circles. +-- @param #ZONE_POLYGON_BASE self +-- @param #number Coalition (Optional) Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1= All. +-- @param #table Color (Optional) RGB color table {r, g, b}, e.g. {1, 0, 0} for red. Default {1, 1, 1}= White. +-- @param #number Radius (Optional) Radius of the circles in meters. Default 1000. +-- @param #number Alpha (Optional) Alpha transparency [0,1]. Default 1. +-- @param #number Segments (Optional) Number of segments within boundary line. Default 10. +-- @param #boolean Closed (Optional) Link the last point with the first one to obtain a closed boundary. Default false +-- @return #ZONE_POLYGON_BASE self +function ZONE_POLYGON_BASE:Boundary(Coalition, Color, Radius, Alpha, Segments, Closed) + Coalition = Coalition or -1 + Color = Color or {1, 1, 1} + Radius = Radius or 1000 + Alpha = Alpha or 1 + Segments = Segments or 10 + Closed = Closed or false + local i = 1 + local j = #self._.Polygon + if (Closed) then + Limit = #self._.Polygon + 1 + else + Limit = #self._.Polygon + end + while i <= #self._.Polygon do + self:T( { i, j, self._.Polygon[i], self._.Polygon[j] } ) + if j ~= Limit then + local DeltaX = self._.Polygon[j].x - self._.Polygon[i].x + local DeltaY = self._.Polygon[j].y - self._.Polygon[i].y + for Segment = 0, Segments do + local PointX = self._.Polygon[i].x + ( Segment * DeltaX / Segments ) + local PointY = self._.Polygon[i].y + ( Segment * DeltaY / Segments ) + ZONE_RADIUS:New( "Zone", {x = PointX, y = PointY}, Radius ):DrawZone(Coalition, Color, 1, Color, Alpha, nil, true) + end + end + j = i + i = i + 1 + end + return self +end --- @type ZONE_POLYGON -- @extends #ZONE_POLYGON_BASE @@ -1796,27 +2098,27 @@ end --- The ZONE_POLYGON class defined by a sequence of @{Wrapper.Group#GROUP} waypoints within the Mission Editor, forming a polygon. -- This class implements the inherited functions from @{Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. --- +-- -- ## Declare a ZONE_POLYGON directly in the DCS mission editor! --- +-- -- You can declare a ZONE_POLYGON using the DCS mission editor by adding the ~ZONE_POLYGON tag in the group name. --- +-- -- So, imagine you have a group declared in the mission editor, with group name `DefenseZone~ZONE_POLYGON`. -- Then during mission startup, when loading Moose.lua, this group will be detected as a ZONE_POLYGON declaration. -- Within the background, a ZONE_POLYGON object will be created within the @{Core.Database} using the properties of the group. -- The ZONE_POLYGON name will be the group name without the ~ZONE_POLYGON tag. --- +-- -- So, you can search yourself for the ZONE_POLYGON by using the @{#ZONE_POLYGON.FindByName}() method. -- In this example, `local PolygonZone = ZONE_POLYGON:FindByName( "DefenseZone" )` would return the ZONE_POLYGON object -- that was created at mission startup, and reference it into the `PolygonZone` local object. --- +-- -- Mission `ZON-510` shows a demonstration of this feature or method. --- +-- -- This is especially handy if you want to quickly setup a SET_ZONE... -- So when you would declare `local SetZone = SET_ZONE:New():FilterPrefixes( "Defense" ):FilterStart()`, -- then SetZone would contain the ZONE_POLYGON object `DefenseZone` as part of the zone collection, --- without much scripting overhead!!! --- +-- without much scripting overhead! +-- -- @field #ZONE_POLYGON ZONE_POLYGON = { ClassName="ZONE_POLYGON", @@ -1868,7 +2170,7 @@ end -- @param #string ZoneName The name of the polygon zone. -- @return #ZONE_POLYGON self function ZONE_POLYGON:FindByName( ZoneName ) - + local ZoneFound = _DATABASE:FindZone( ZoneName ) return ZoneFound end @@ -1877,85 +2179,85 @@ do -- ZONE_AIRBASE --- @type ZONE_AIRBASE -- @extends #ZONE_RADIUS - - + + --- The ZONE_AIRBASE class defines by a zone around a @{Wrapper.Airbase#AIRBASE} with a radius. -- This class implements the inherited functions from @{Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. - -- + -- -- @field #ZONE_AIRBASE ZONE_AIRBASE = { ClassName="ZONE_AIRBASE", } - - - + + + --- Constructor to create a ZONE_AIRBASE instance, taking the zone name, a zone @{Wrapper.Airbase#AIRBASE} and a radius. -- @param #ZONE_AIRBASE self -- @param #string AirbaseName Name of the airbase. -- @param DCS#Distance Radius (Optional)The radius of the zone in meters. Default 4000 meters. -- @return #ZONE_AIRBASE self function ZONE_AIRBASE:New( AirbaseName, Radius ) - + Radius=Radius or 4000 - + local Airbase = AIRBASE:FindByName( AirbaseName ) - + local self = BASE:Inherit( self, ZONE_RADIUS:New( AirbaseName, Airbase:GetVec2(), Radius ) ) - + self._.ZoneAirbase = Airbase self._.ZoneVec2Cache = self._.ZoneAirbase:GetVec2() - + -- Zone objects are added to the _DATABASE and SET_ZONE objects. _EVENTDISPATCHER:CreateEventNewZone( self ) - + return self end - + --- Get the airbase as part of the ZONE_AIRBASE object. -- @param #ZONE_AIRBASE self -- @return Wrapper.Airbase#AIRBASE The airbase. function ZONE_AIRBASE:GetAirbase() return self._.ZoneAirbase - end - + end + --- Returns the current location of the @{Wrapper.Group}. -- @param #ZONE_AIRBASE self -- @return DCS#Vec2 The location of the zone based on the @{Wrapper.Group} location. function ZONE_AIRBASE:GetVec2() self:F( self.ZoneName ) - + local ZoneVec2 = nil - + if self._.ZoneAirbase:IsAlive() then ZoneVec2 = self._.ZoneAirbase:GetVec2() self._.ZoneVec2Cache = ZoneVec2 else ZoneVec2 = self._.ZoneVec2Cache end - + self:T( { ZoneVec2 } ) - + return ZoneVec2 end - + --- Returns a random location within the zone of the @{Wrapper.Group}. -- @param #ZONE_AIRBASE self -- @return DCS#Vec2 The random location of the zone based on the @{Wrapper.Group} location. function ZONE_AIRBASE:GetRandomVec2() self:F( self.ZoneName ) - + local Point = {} local Vec2 = self._.ZoneAirbase:GetVec2() - + local angle = math.random() * math.pi*2; Point.x = Vec2.x + math.cos( angle ) * math.random() * self:GetRadius(); Point.y = Vec2.y + math.sin( angle ) * math.random() * self:GetRadius(); - + self:T( { Point } ) - + return Point end - + --- Returns a @{Core.Point#POINT_VEC2} object reflecting a random 2D location within the zone. -- @param #ZONE_AIRBASE self -- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. @@ -1963,11 +2265,11 @@ do -- ZONE_AIRBASE -- @return Core.Point#POINT_VEC2 The @{Core.Point#POINT_VEC2} object reflecting the random 3D location within the zone. function ZONE_AIRBASE:GetRandomPointVec2( inner, outer ) self:F( self.ZoneName, inner, outer ) - + local PointVec2 = POINT_VEC2:NewFromVec2( self:GetRandomVec2() ) - + self:T3( { PointVec2 } ) - + return PointVec2 end diff --git a/Moose Development/Moose/DCS.lua b/Moose Development/Moose/DCS.lua index 2d190d215..8575e1f10 100644 --- a/Moose Development/Moose/DCS.lua +++ b/Moose Development/Moose/DCS.lua @@ -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 \ No newline at end of file +end -- AI diff --git a/Moose Development/Moose/Functional/ATC_Ground.lua b/Moose Development/Moose/Functional/ATC_Ground.lua index 455f3eaf5..e5d17ea14 100644 --- a/Moose Development/Moose/Functional/ATC_Ground.lua +++ b/Moose Development/Moose/Functional/ATC_Ground.lua @@ -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 diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index 8a3d8ebce..8f7192641 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -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 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Functional/Designate.lua b/Moose Development/Moose/Functional/Designate.lua index a11aa21b8..72aaf9959 100644 --- a/Moose Development/Moose/Functional/Designate.lua +++ b/Moose Development/Moose/Functional/Designate.lua @@ -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. diff --git a/Moose Development/Moose/Functional/Mantis.lua b/Moose Development/Moose/Functional/Mantis.lua new file mode 100644 index 000000000..6891fd3fd --- /dev/null +++ b/Moose Development/Moose/Functional/Mantis.lua @@ -0,0 +1,1267 @@ +--- **Functional** -- Modular, Automatic and Network capable Targeting and Interception System for Air Defenses +-- +-- === +-- +-- **MANTIS** - Moose derived Modular, Automatic and Network capable Targeting and Interception System +-- Controls a network of SAM sites. Use detection to switch on the AA site closest to the enemy +-- Leverage evasiveness from SEAD +-- Leverage attack range setup added by DCS in 11/20 +-- +-- === +-- +-- ## Missions: +-- +-- ### [MANTIS - Modular, Automatic and Network capable Targeting and Interception System](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/MTS%20-%20Mantis/MTS-010%20-%20Basic%20Mantis%20Demo) +-- +-- === +-- +-- ### Author : **applevangelist ** +-- +-- @module Functional.Mantis +-- @image Functional.Mantis.jpg + +-- Date: July 2021 + +------------------------------------------------------------------------- +--- **MANTIS** class, extends #Core.Base#BASE +-- @type MANTIS +-- @field #string Classname +-- @field #string name Name of this Mantis +-- @field #string SAM_Templates_Prefix Prefix to build the #SET_GROUP for SAM sites +-- @field Core.Set#SET_GROUP SAM_Group The SAM #SET_GROUP +-- @field #string EWR_Templates_Prefix Prefix to build the #SET_GROUP for EWR group +-- @field Core.Set#SET_GROUP EWR_Group The EWR #SET_GROUP +-- @field Core.Set#SET_GROUP Adv_EWR_Group The EWR #SET_GROUP used for advanced mode +-- @field #string HQ_Template_CC The ME name of the HQ object +-- @field Wrapper.Group#GROUP HQ_CC The #GROUP object of the HQ +-- @field #table SAM_Table Table of SAM sites +-- @field #string lid Prefix for logging +-- @field Functional.Detection#DETECTION_AREAS Detection The #DETECTION_AREAS object for EWR +-- @field Functional.Detection#DETECTION_AREAS AWACS_Detection The #DETECTION_AREAS object for AWACS +-- @field #boolean debug Switch on extra messages +-- @field #boolean verbose Switch on extra logging +-- @field #number checkradius Radius of the SAM sites +-- @field #number grouping Radius to group detected objects +-- @field #number acceptrange Radius of the EWR detection +-- @field #number detectinterval Interval in seconds for the target detection +-- @field #number engagerange Firing engage range of the SAMs, see [https://wiki.hoggitworld.com/view/DCS_option_engagementRange] +-- @field #boolean autorelocate Relocate HQ and EWR groups in random intervals. Note: You need to select units for this which are *actually mobile* +-- @field #boolean advanced Use advanced mode, will decrease reactivity of MANTIS, if HQ and/or EWR network dies. Set SAMs to RED state if both are dead. Requires usage of an HQ object +-- @field #number adv_ratio Percentage to use for advanced mode, defaults to 100% +-- @field #number adv_state Advanced mode state tracker +-- @field #boolean advAwacs Boolean switch to use Awacs as a separate detection stream +-- @field #number awacsrange Detection range of an optional Awacs unit +-- @field #boolean UseEmOnOff Decide if we are using Emissions on/off (true) or AlarmState red/green (default) +-- @field Functional.Shorad#SHORAD Shorad SHORAD Object, if available +-- @field #boolean ShoradLink If true, #MANTIS has #SHORAD enabled +-- @field #number ShoradTime Timer in seconds, how long #SHORAD will be active after a detection inside of the defense range +-- @field #number ShoradActDistance Distance of an attacker in meters from a Mantis SAM site, on which Shorad will be switched on. Useful to not give away Shorad sites too early. Default 15km. Should be smaller than checkradius. +-- @extends Core.Base#BASE + + +--- *The worst thing that can happen to a good cause is, not to be skillfully attacked, but to be ineptly defended.* - Frdric Bastiat +-- +-- Simple Class for a more intelligent Air Defense System +-- +-- #MANTIS +-- Moose derived Modular, Automatic and Network capable Targeting and Interception System. +-- Controls a network of SAM sites. Use detection to switch on the AA site closest to the enemy. +-- Leverage evasiveness from @{Functional.Sead#SEAD}. +-- Leverage attack range setup added by DCS in 11/20. +-- +-- Set up your SAM sites in the mission editor. Name the groups with common prefix like "Red SAM". +-- Set up your EWR system in the mission editor. Name the groups with common prefix like "Red EWR". Can be e.g. AWACS or a combination of AWACS and Search Radars like e.g. EWR 1L13 etc. +-- [optional] Set up your HQ. Can be any group, e.g. a command vehicle. +-- +-- # 1. Basic tactical considerations when setting up your SAM sites +-- +-- ## 1.1 Radar systems and AWACS +-- +-- Typically, your setup should consist of EWR (early warning) radars to detect and track targets, accompanied by AWACS if your scenario forsees that. Ensure that your EWR radars have a good coverage of the area you want to track. +-- **Location** is of highest importantance here. Whilst AWACS in DCS has almost the "all seeing eye", EWR don't have that. Choose your location wisely, against a mountain backdrop or inside a valley even the best EWR system +-- doesn't work well. Prefer higher-up locations with a good view; use F7 in-game to check where you actually placed your EWR and have a look around. Apart from the obvious choice, do also consider other radar units +-- for this role, most have "SR" (search radar) or "STR" (search and track radar) in their names, use the encyclopedia to see what they actually do. +-- +-- ## 1.2 SAM sites +-- +-- Typically your SAM should cover all attack ranges. The closer the enemy gets, the more systems you will need to deploy to defend your location. Use a combination of long-range systems like the SA-10/11, midrange like SA-6 and short-range like +-- SA-2 for defense (Patriot, Hawk, Gepard, Blindfire for the blue side). For close-up defense and defense against HARMs or low-flying aircraft, helicopters it is also advisable to deploy SA-15 TOR systems, Shilka, Strela and Tunguska units, as well as manpads (Think Gepard, Avenger, Chaparral, +-- Linebacker, Roland systems for the blue side). If possible, overlap ranges for mutual coverage. +-- +-- ## 1.3 Typical problems +-- +-- Often times, people complain because the detection cannot "see" oncoming targets and/or Mantis switches on too late. Three typial problems here are +-- +-- * bad placement of radar units, +-- * overestimation how far units can "see" and +-- * not taking into account that a SAM site will take (e.g for a SA-6) 30-40 seconds between switching to RED, acquiring the target and firing. +-- +-- An attacker doing 350knots will cover ca 180meters/second or thus more than 6km until the SA-6 fires. Use triggers zones and the ruler in the missione editor to understand distances and zones. Take into account that the ranges given by the circles +-- in the mission editor are absolute maximum ranges; in-game this is rather 50-75% of that depending on the system. Fiddle with placement and options to see what works best for your scenario, and remember **everything in here is in meters**. +-- +-- # 2. Start up your MANTIS with a basic setting +-- +-- `myredmantis = MANTIS:New("myredmantis","Red SAM","Red EWR",nil,"red",false)` +-- `myredmantis:Start()` +-- +-- [optional] Use +-- +-- * `MANTIS:SetEWRGrouping(radius)` +-- * `MANTIS:SetEWRRange(radius)` +-- * `MANTIS:SetSAMRadius(radius)` +-- * `MANTIS:SetDetectInterval(interval)` +-- * `MANTIS:SetAutoRelocate(hq, ewr)` +-- +-- before starting #MANTIS to fine-tune your setup. +-- +-- If you want to use a separate AWACS unit (default detection range: 250km) to support your EWR system, use e.g. the following setup: +-- +-- `mybluemantis = MANTIS:New("bluemantis","Blue SAM","Blue EWR",nil,"blue",false,"Blue Awacs")` +-- `mybluemantis:Start()` +-- +-- # 3. Default settings +-- +-- By default, the following settings are active: +-- +-- * SAM_Templates_Prefix = "Red SAM" - SAM site group names in the mission editor begin with "Red SAM" +-- * EWR_Templates_Prefix = "Red EWR" - EWR group names in the mission editor begin with "Red EWR" - can also be combined with an AWACS unit +-- * checkradius = 25000 (meters) - SAMs will engage enemy flights, if they are within a 25km around each SAM site - `MANTIS:SetSAMRadius(radius)` +-- * grouping = 5000 (meters) - Detection (EWR) will group enemy flights to areas of 5km for tracking - `MANTIS:SetEWRGrouping(radius)` +-- * acceptrange = 80000 (meters) - Detection (EWR) will on consider flights inside a 80km radius - `MANTIS:SetEWRRange(radius)` +-- * detectinterval = 30 (seconds) - MANTIS will decide every 30 seconds which SAM to activate - `MANTIS:SetDetectInterval(interval)` +-- * engagerange = 85 (percent) - SAMs will only fire if flights are inside of a 85% radius of their max firerange - `MANTIS:SetSAMRange(range)` +-- * dynamic = false - Group filtering is set to once, i.e. newly added groups will not be part of the setup by default - `MANTIS:New(name,samprefix,ewrprefix,hq,coaltion,dynamic)` +-- * autorelocate = false - HQ and (mobile) EWR system will not relocate in random intervals between 30mins and 1 hour - `MANTIS:SetAutoRelocate(hq, ewr)` +-- * debug = false - Debugging reports on screen are set to off - `MANTIS:Debug(onoff)` +-- +-- # 4. Advanced Mode +-- +-- Advanced mode will *decrease* reactivity of MANTIS, if HQ and/or EWR network dies. Awacs is counted as one EWR unit. It will set SAMs to RED state if both are dead. Requires usage of an **HQ** object and the **dynamic** option. +-- +-- E.g. `mymantis:SetAdvancedMode( true, 90 )` +-- +-- Use this option if you want to make use of or allow advanced SEAD tactics. +-- +-- # 5. Integrate SHORAD +-- +-- You can also choose to integrate Mantis with @{Functional.Shorad#SHORAD} for protection against HARMs and AGMs. When SHORAD detects a missile fired at one of MANTIS' SAM sites, it will activate SHORAD systems in +-- the given defense checkradius around that SAM site. Create a SHORAD object first, then integrate with MANTIS like so: +-- +-- `local SamSet = SET_GROUP:New():FilterPrefixes("Blue SAM"):FilterCoalitions("blue"):FilterStart()` +-- `myshorad = SHORAD:New("BlueShorad", "Blue SHORAD", SamSet, 22000, 600, "blue")` +-- `-- now set up MANTIS` +-- `mymantis = MANTIS:New("BlueMantis","Blue SAM","Blue EWR",nil,"blue",false,"Blue Awacs")` +-- `mymantis:AddShorad(myshorad,720)` +-- `mymantis:Start()` +-- +-- and (optionally) remove the link later on with +-- +-- `mymantis:RemoveShorad()` +-- +-- @field #MANTIS +MANTIS = { + ClassName = "MANTIS", + name = "mymantis", + SAM_Templates_Prefix = "", + SAM_Group = nil, + EWR_Templates_Prefix = "", + EWR_Group = nil, + Adv_EWR_Group = nil, + HQ_Template_CC = "", + HQ_CC = nil, + SAM_Table = {}, + lid = "", + Detection = nil, + AWACS_Detection = nil, + debug = false, + checkradius = 25000, + grouping = 5000, + acceptrange = 80000, + detectinterval = 30, + engagerange = 75, + autorelocate = false, + advanced = false, + adv_ratio = 100, + adv_state = 0, + AWACS_Prefix = "", + advAwacs = false, + verbose = false, + awacsrange = 250000, + Shorad = nil, + ShoradLink = false, + ShoradTime = 600, + ShoradActDistance = 15000, + UseEmOnOff = false, + TimeStamp = 0, + state2flag = false, + SamStateTracker = {}, + DLink = false, + DLTimeStamp = 0, +} + +--- Advanced state enumerator +-- @type MANTIS.AdvancedState +MANTIS.AdvancedState = { + GREEN = 0, + AMBER = 1, + RED = 2, +} + +----------------------------------------------------------------------- +-- MANTIS System +----------------------------------------------------------------------- + +do + --- Function to instantiate a new object of class MANTIS + --@param #MANTIS self + --@param #string name Name of this MANTIS for reporting + --@param #string samprefix Prefixes for the SAM groups from the ME, e.g. all groups starting with "Red Sam..." + --@param #string ewrprefix Prefixes for the EWR groups from the ME, e.g. all groups starting with "Red EWR..." + --@param #string hq Group name of your HQ (optional) + --@param #string coaltion Coalition side of your setup, e.g. "blue", "red" or "neutral" + --@param #boolean dynamic Use constant (true) filtering or just filter once (false, default) (optional) + --@param #string awacs Group name of your Awacs (optional) + --@param #boolean EmOnOff Make MANTIS switch Emissions on and off instead of changing the alarm state between RED and GREEN + --@return #MANTIS self + --@usage Start up your MANTIS with a basic setting + -- + -- `myredmantis = MANTIS:New("myredmantis","Red SAM","Red EWR",nil,"red",false)` + -- `myredmantis:Start()` + -- + -- [optional] Use + -- + -- * `MANTIS:SetEWRGrouping(radius)` + -- * `MANTIS:SetEWRRange(radius)` + -- * `MANTIS:SetSAMRadius(radius)` + -- * `MANTIS:SetDetectInterval(interval)` + -- * `MANTIS:SetAutoRelocate(hq, ewr)` + -- + -- before starting #MANTIS to fine-tune your setup. + -- + -- If you want to use a separate AWACS unit (default detection range: 250km) to support your EWR system, use e.g. the following setup: + -- + -- `mybluemantis = MANTIS:New("bluemantis","Blue SAM","Blue EWR",nil,"blue",false,"Blue Awacs")` + -- `mybluemantis:Start()` + -- + function MANTIS:New(name,samprefix,ewrprefix,hq,coaltion,dynamic,awacs, EmOnOff) + + -- DONE: Create some user functions for these + -- DONE: Make HQ useful + -- DONE: Set SAMs to auto if EWR dies + -- DONE: Refresh SAM table in dynamic mode + -- DONE: Treat Awacs separately, since they might be >80km off site + + self.name = name or "mymantis" + self.SAM_Templates_Prefix = samprefix or "Red SAM" + self.EWR_Templates_Prefix = ewrprefix or "Red EWR" + self.HQ_Template_CC = hq or nil + self.Coalition = coaltion or "red" + self.SAM_Table = {} + self.dynamic = dynamic or false + self.checkradius = 25000 + self.grouping = 5000 + self.acceptrange = 80000 + self.detectinterval = 30 + self.engagerange = 75 + self.autorelocate = false + self.autorelocateunits = { HQ = false, EWR = false} + self.advanced = false + self.adv_ratio = 100 + self.adv_state = 0 + self.verbose = false + self.Adv_EWR_Group = nil + self.AWACS_Prefix = awacs or nil + self.awacsrange = 250000 --DONE: 250km, User Function to change + self.Shorad = nil + self.ShoradLink = false + self.ShoradTime = 600 + self.ShoradActDistance = 15000 + self.TimeStamp = timer.getAbsTime() + self.relointerval = math.random(1800,3600) -- random between 30 and 60 mins + self.state2flag = false + self.SamStateTracker = {} -- table to hold alert states, so we don't trigger state changes twice in adv mode + self.DLink = false + + if EmOnOff then + if EmOnOff == false then + self.UseEmOnOff = false + else + self.UseEmOnOff = true + end + end + + if type(awacs) == "string" then + self.advAwacs = true + else + self.advAwacs = false + end + + -- Inherit everything from BASE class. + local self = BASE:Inherit(self, FSM:New()) -- #MANTIS + + -- Set the string id for output to DCS.log file. + self.lid=string.format("MANTIS %s | ", self.name) + + -- Debug trace. + if self.debug then + BASE:TraceOnOff(true) + BASE:TraceClass(self.ClassName) + --BASE:TraceClass("SEAD") + BASE:TraceLevel(1) + end + + if self.dynamic then + -- Set SAM SET_GROUP + self.SAM_Group = SET_GROUP:New():FilterPrefixes(self.SAM_Templates_Prefix):FilterCoalitions(self.Coalition):FilterStart() + -- Set EWR SET_GROUP + self.EWR_Group = SET_GROUP:New():FilterPrefixes({self.SAM_Templates_Prefix,self.EWR_Templates_Prefix}):FilterCoalitions(self.Coalition):FilterStart() + else + -- Set SAM SET_GROUP + self.SAM_Group = SET_GROUP:New():FilterPrefixes(self.SAM_Templates_Prefix):FilterCoalitions(self.Coalition):FilterOnce() + -- Set EWR SET_GROUP + self.EWR_Group = SET_GROUP:New():FilterPrefixes({self.SAM_Templates_Prefix,self.EWR_Templates_Prefix}):FilterCoalitions(self.Coalition):FilterOnce() + end + + -- set up CC + if self.HQ_Template_CC then + self.HQ_CC = GROUP:FindByName(self.HQ_Template_CC) + end + + -- @field #string version + self.version="0.6.2" + self:I(string.format("***** Starting MANTIS Version %s *****", self.version)) + + --- FSM Functions --- + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start FSM. + self:AddTransition("*", "Status", "*") -- MANTIS status update. + self:AddTransition("*", "Relocating", "*") -- MANTIS HQ and EWR are relocating. + self:AddTransition("*", "GreenState", "*") -- MANTIS A SAM switching to GREEN state. + self:AddTransition("*", "RedState", "*") -- MANTIS A SAM switching to RED state. + self:AddTransition("*", "AdvStateChange", "*") -- MANTIS advanced mode state change. + self:AddTransition("*", "ShoradActivated", "*") -- MANTIS woke up a connected SHORAD. + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the MANTIS. Initializes parameters and starts event handlers. + -- @function [parent=#MANTIS] Start + -- @param #MANTIS self + + --- Triggers the FSM event "Start" after a delay. Starts the MANTIS. Initializes parameters and starts event handlers. + -- @function [parent=#MANTIS] __Start + -- @param #MANTIS self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the MANTIS and all its event handlers. + -- @param #MANTIS self + + --- Triggers the FSM event "Stop" after a delay. Stops the MANTIS and all its event handlers. + -- @function [parent=#MANTIS] __Stop + -- @param #MANTIS self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Status". + -- @function [parent=#MANTIS] Status + -- @param #MANTIS self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#MANTIS] __Status + -- @param #MANTIS self + -- @param #number delay Delay in seconds. + + --- On After "Relocating" event. HQ and/or EWR moved. + -- @function [parent=#MANTIS] OnAfterRelocating + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @return #MANTIS self + + --- On After "GreenState" event. A SAM group was switched to GREEN alert. + -- @function [parent=#MANTIS] OnAfterGreenState + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @param Wrapper.Group#GROUP Group The GROUP object whose state was changed + -- @return #MANTIS self + + --- On After "RedState" event. A SAM group was switched to RED alert. + -- @function [parent=#MANTIS] OnAfterRedState + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @param Wrapper.Group#GROUP Group The GROUP object whose state was changed + -- @return #MANTIS self + + --- On After "AdvStateChange" event. Advanced state changed, influencing detection speed. + -- @function [parent=#MANTIS] OnAfterAdvStateChange + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @param #number Oldstate Old state - 0 = green, 1 = amber, 2 = red + -- @param #number Newstate New state - 0 = green, 1 = amber, 2 = red + -- @param #number Interval Calculated detection interval based on state and advanced feature setting + -- @return #MANTIS self + + --- On After "ShoradActivated" event. Mantis has activated a SHORAD. + -- @function [parent=#MANTIS] OnAfterShoradActivated + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @param #string Name Name of the GROUP which SHORAD shall protect + -- @param #number Radius Radius around the named group to find SHORAD groups + -- @param #number Ontime Seconds the SHORAD will stay active + + return self + end + +----------------------------------------------------------------------- +-- MANTIS helper functions +----------------------------------------------------------------------- + + --- [Internal] Function to get the self.SAM_Table + -- @param #MANTIS self + -- @return #table table + function MANTIS:_GetSAMTable() + self:T(self.lid .. "GetSAMTable") + return self.SAM_Table + end + + --- [Internal] Function to set the self.SAM_Table + -- @param #MANTIS self + -- @return #MANTIS self + function MANTIS:_SetSAMTable(table) + self:T(self.lid .. "SetSAMTable") + self.SAM_Table = table + return self + end + + --- Function to set the grouping radius of the detection in meters + -- @param #MANTIS self + -- @param #number radius Radius upon which detected objects will be grouped + function MANTIS:SetEWRGrouping(radius) + self:T(self.lid .. "SetEWRGrouping") + local radius = radius or 5000 + self.grouping = radius + return self + end + + --- Function to set the detection radius of the EWR in meters + -- @param #MANTIS self + -- @param #number radius Radius of the EWR detection zone + function MANTIS:SetEWRRange(radius) + self:T(self.lid .. "SetEWRRange") + local radius = radius or 80000 + self.acceptrange = radius + return self + end + + --- Function to set switch-on/off zone for the SAM sites in meters + -- @param #MANTIS self + -- @param #number radius Radius of the firing zone + function MANTIS:SetSAMRadius(radius) + self:T(self.lid .. "SetSAMRadius") + local radius = radius or 25000 + self.checkradius = radius + return self + end + + --- Function to set SAM firing engage range, 0-100 percent, e.g. 75 + -- @param #MANTIS self + -- @param #number range Percent of the max fire range + function MANTIS:SetSAMRange(range) + self:T(self.lid .. "SetSAMRange") + local range = range or 75 + if range < 0 or range > 100 then + range = 75 + end + self.engagerange = range + return self + end + + --- Function to set a new SAM firing engage range, use this method to adjust range while running MANTIS, e.g. for different setups day and night + -- @param #MANTIS self + -- @param #number range Percent of the max fire range + function MANTIS:SetNewSAMRangeWhileRunning(range) + self:T(self.lid .. "SetNewSAMRangeWhileRunning") + local range = range or 75 + if range < 0 or range > 100 then + range = 75 + end + self.engagerange = range + self:_RefreshSAMTable() + self.mysead.EngagementRange = range + return self + end + + --- Function to set switch-on/off the debug state + -- @param #MANTIS self + -- @param #boolean onoff Set true to switch on + function MANTIS:Debug(onoff) + self:T(self.lid .. "SetDebug") + local onoff = onoff or false + self.debug = onoff + if onoff then + -- Debug trace. + BASE:TraceOn() + BASE:TraceClass("MANTIS") + BASE:TraceLevel(1) + else + BASE:TraceOff() + end + return self + end + + --- Function to get the HQ object for further use + -- @param #MANTIS self + -- @return Wrapper.GROUP#GROUP The HQ #GROUP object or *nil* if it doesn't exist + function MANTIS:GetCommandCenter() + self:T(self.lid .. "GetCommandCenter") + if self.HQ_CC then + return self.HQ_CC + else + return nil + end + end + + --- Function to set separate AWACS detection instance + -- @param #MANTIS self + -- @param #string prefix Name of the AWACS group in the mission editor + function MANTIS:SetAwacs(prefix) + self:T(self.lid .. "SetAwacs") + if prefix ~= nil then + if type(prefix) == "string" then + self.AWACS_Prefix = prefix + self.advAwacs = true + end + end + return self + end + + --- Function to set AWACS detection range. Defaults to 250.000m (250km) - use **before** starting your Mantis! + -- @param #MANTIS self + -- @param #number range Detection range of the AWACS group + function MANTIS:SetAwacsRange(range) + self:T(self.lid .. "SetAwacsRange") + local range = range or 250000 + self.awacsrange = range + return self + end + + --- Function to set the HQ object for further use + -- @param #MANTIS self + -- @param Wrapper.GROUP#GROUP group The #GROUP object to be set as HQ + function MANTIS:SetCommandCenter(group) + self:T(self.lid .. "SetCommandCenter") + local group = group or nil + if group ~= nil then + if type(group) == "string" then + self.HQ_CC = GROUP:FindByName(group) + self.HQ_Template_CC = group + else + self.HQ_CC = group + self.HQ_Template_CC = group:GetName() + end + end + return self + end + + --- Function to set the detection interval + -- @param #MANTIS self + -- @param #number interval The interval in seconds + function MANTIS:SetDetectInterval(interval) + self:T(self.lid .. "SetDetectInterval") + local interval = interval or 30 + self.detectinterval = interval + return self + end + + --- Function to set Advanded Mode + -- @param #MANTIS self + -- @param #boolean onoff If true, will activate Advanced Mode + -- @param #number ratio [optional] Percentage to use for advanced mode, defaults to 100% + -- @usage Advanced mode will *decrease* reactivity of MANTIS, if HQ and/or EWR network dies. Set SAMs to RED state if both are dead. Requires usage of an **HQ** object and the **dynamic** option. + -- E.g. `mymantis:SetAdvancedMode(true, 90)` + function MANTIS:SetAdvancedMode(onoff, ratio) + self:T(self.lid .. "SetAdvancedMode") + --self.T({onoff, ratio}) + local onoff = onoff or false + local ratio = ratio or 100 + if (type(self.HQ_Template_CC) == "string") and onoff and self.dynamic then + self.adv_ratio = ratio + self.advanced = true + self.adv_state = 0 + self.Adv_EWR_Group = SET_GROUP:New():FilterPrefixes(self.EWR_Templates_Prefix):FilterCoalitions(self.Coalition):FilterStart() + self:I(string.format("***** Starting Advanced Mode MANTIS Version %s *****", self.version)) + else + local text = self.lid.." Advanced Mode requires a HQ and dynamic to be set. Revisit your MANTIS:New() statement to add both." + local m= MESSAGE:New(text,10,"MANTIS",true):ToAll() + self:E(text) + end + return self + end + + --- Set using Emissions on/off instead of changing alarm state + -- @param #MANTIS self + -- @param #boolean switch Decide if we are changing alarm state or Emission state + function MANTIS:SetUsingEmOnOff(switch) + self:T(self.lid .. "SetUsingEmOnOff") + self.UseEmOnOff = switch or false + return self + end + + --- Set using an #INTEL_DLINK object instead of #DETECTION + -- @param #MANTIS self + -- @param Ops.Intelligence#INTEL_DLINK DLink The data link object to be used. + function MANTIS:SetUsingDLink(DLink) + self:T(self.lid .. "SetUsingDLink") + self.DLink = true + self.Detection = DLink + self.DLTimeStamp = timer.getAbsTime() + return self + end + + --- [Internal] Function to check if HQ is alive + -- @param #MANTIS self + -- @return #boolean True if HQ is alive, else false + function MANTIS:_CheckHQState() + self:T(self.lid .. "CheckHQState") + local text = self.lid.." Checking HQ State" + local m= MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) + if self.verbose then self:I(text) end + -- start check + if self.advanced then + local hq = self.HQ_Template_CC + local hqgrp = GROUP:FindByName(hq) + if hqgrp then + if hqgrp:IsAlive() then -- ok we're on, hq exists and as alive + --self.T(self.lid.." HQ is alive!") + return true + else + --self.T(self.lid.." HQ is dead!") + return false + end + end + end + return self + end + + --- [Internal] Function to check if EWR is (at least partially) alive + -- @param #MANTIS self + -- @return #boolean True if EWR is alive, else false + function MANTIS:_CheckEWRState() + self:T(self.lid .. "CheckEWRState") + local text = self.lid.." Checking EWR State" + --self.T(text) + local m= MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) + if self.verbose then self:I(text) end + -- start check + if self.advanced then + local EWR_Group = self.Adv_EWR_Group + --local EWR_Set = EWR_Group.Set + local nalive = EWR_Group:CountAlive() + if self.advAwacs then + local awacs = GROUP:FindByName(self.AWACS_Prefix) + if awacs ~= nil then + if awacs:IsAlive() then + nalive = nalive+1 + end + end + end + --self.T(self.lid..string.format(" No of EWR alive is %d", nalive)) + if nalive > 0 then + return true + else + return false + end + end + return self + end + + --- [Internal] Function to determine state of the advanced mode + -- @param #MANTIS self + -- @return #number Newly calculated interval + -- @return #number Previous state for tracking 0, 1, or 2 + function MANTIS:_CalcAdvState() + self:T(self.lid .. "CalcAdvState") + local m=MESSAGE:New(self.lid.." Calculating Advanced State",10,"MANTIS"):ToAllIf(self.debug) + if self.verbose then self:I(self.lid.." Calculating Advanced State") end + -- start check + local currstate = self.adv_state -- save curr state for comparison later + local EWR_State = self:_CheckEWRState() + local HQ_State = self:_CheckHQState() + -- set state + if EWR_State and HQ_State then -- both alive + self.adv_state = 0 --everything is fine + elseif EWR_State or HQ_State then -- one alive + self.adv_state = 1 --slow down level 1 + else -- none alive + self.adv_state = 2 --slow down level 2 + end + -- calculate new detectioninterval + local interval = self.detectinterval -- e.g. 30 + local ratio = self.adv_ratio / 100 -- e.g. 80/100 = 0.8 + ratio = ratio * self.adv_state -- e.g 0.8*2 = 1.6 + local newinterval = interval + (interval * ratio) -- e.g. 30+(30*1.6) = 78 + if self.debug or self.verbose then + local text = self.lid..string.format(" Calculated OldState/NewState/Interval: %d / %d / %d", currstate, self.adv_state, newinterval) + --self.T(text) + local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) + if self.verbose then self:I(text) end + end + return newinterval, currstate + end + + --- Function to set autorelocation for HQ and EWR objects. Note: Units must be actually mobile in DCS! + -- @param #MANTIS self + -- @param #boolean hq If true, will relocate HQ object + -- @param #boolean ewr If true, will relocate EWR objects + function MANTIS:SetAutoRelocate(hq, ewr) + self:T(self.lid .. "SetAutoRelocate") + --self.T({hq, ewr}) + local hqrel = hq or false + local ewrel = ewr or false + if hqrel or ewrel then + self.autorelocate = true + self.autorelocateunits = { HQ = hqrel, EWR = ewrel } + --self.T({self.autorelocate, self.autorelocateunits}) + end + return self + end + + --- [Internal] Function to execute the relocation + -- @param #MANTIS self + function MANTIS:_RelocateGroups() + self:T(self.lid .. "RelocateGroups") + local text = self.lid.." Relocating Groups" + local m= MESSAGE:New(text,10,"MANTIS",true):ToAllIf(self.debug) + if self.verbose then self:I(text) end + if self.autorelocate then + -- relocate HQ + local HQGroup = self.HQ_CC + if self.autorelocateunits.HQ and self.HQ_CC and HQGroup:IsAlive() then --only relocate if HQ exists + local _hqgrp = self.HQ_CC + --self.T(self.lid.." Relocating HQ") + local text = self.lid.." Relocating HQ" + --local m= MESSAGE:New(text,10,"MANTIS"):ToAll() + _hqgrp:RelocateGroundRandomInRadius(20,500,true,true) + end + --relocate EWR + -- TODO: maybe dependent on AlarmState? Observed: SA11 SR only relocates if no objects in reach + if self.autorelocateunits.EWR then + -- get EWR Group + local EWR_GRP = SET_GROUP:New():FilterPrefixes(self.EWR_Templates_Prefix):FilterCoalitions(self.Coalition):FilterOnce() + local EWR_Grps = EWR_GRP.Set --table of objects in SET_GROUP + for _,_grp in pairs (EWR_Grps) do + if _grp:IsAlive() and _grp:IsGround() then + --self.T(self.lid.." Relocating EWR ".._grp:GetName()) + local text = self.lid.." Relocating EWR ".._grp:GetName() + local m= MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) + if self.verbose then self:I(text) end + _grp:RelocateGroundRandomInRadius(20,500,true,true) + end + end + end + end + return self + end + + --- [Internal] Function to check if any object is in the given SAM zone + -- @param #MANTIS self + -- @param #table dectset Table of coordinates of detected items + -- @param Core.Point#COORDINATE samcoordinate Coordinate object. + -- @return #boolean True if in any zone, else false + -- @return #number Distance Target distance in meters or zero when no object is in zone + function MANTIS:CheckObjectInZone(dectset, samcoordinate) + self:T(self.lid.."CheckObjectInZone") + -- check if non of the coordinate is in the given defense zone + local radius = self.checkradius + local set = dectset + for _,_coord in pairs (set) do + local coord = _coord -- get current coord to check + -- output for cross-check + local targetdistance = samcoordinate:DistanceFromPointVec2(coord) + if self.verbose or self.debug then + local dectstring = coord:ToStringLLDMS() + local samstring = samcoordinate:ToStringLLDMS() + local text = string.format("Checking SAM at % s - Distance %d m - Target %s", samstring, targetdistance, dectstring) + local m = MESSAGE:New(text,10,"Check"):ToAllIf(self.debug) + self:I(self.lid..text) + end + -- end output to cross-check + if targetdistance <= radius then + return true, targetdistance + end + end + return false, 0 + end + + --- [Internal] Function to start the detection via EWR groups + -- @param #MANTIS self + -- @return Functional.Detection #DETECTION_AREAS The running detection set + function MANTIS:StartDetection() + self:T(self.lid.."Starting Detection") + + -- start detection + local groupset = self.EWR_Group + local grouping = self.grouping or 5000 + local acceptrange = self.acceptrange or 80000 + local interval = self.detectinterval or 60 + + --@param Functional.Detection #DETECTION_AREAS _MANTISdetection [Internal] The MANTIS detection object + local MANTISdetection = DETECTION_AREAS:New( groupset, grouping ) --[Internal] Grouping detected objects to 5000m zones + MANTISdetection:FilterCategories({ Unit.Category.AIRPLANE, Unit.Category.HELICOPTER }) + MANTISdetection:SetAcceptRange(acceptrange) + MANTISdetection:SetRefreshTimeInterval(interval) + MANTISdetection:Start() + + function MANTISdetection:OnAfterDetectedItem(From,Event,To,DetectedItem) + --BASE:I( { From, Event, To, DetectedItem }) + local debug = false + if DetectedItem.IsDetected and debug then + local Coordinate = DetectedItem.Coordinate -- Core.Point#COORDINATE + local text = "MANTIS: Detection at "..Coordinate:ToStringLLDMS() + local m = MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) + end + end + return MANTISdetection + end + + --- [Internal] Function to start the detection via AWACS if defined as separate + -- @param #MANTIS self + -- @return Functional.Detection #DETECTION_AREAS The running detection set + function MANTIS:StartAwacsDetection() + self:T(self.lid.."Starting Awacs Detection") + + -- start detection + local group = self.AWACS_Prefix + local groupset = SET_GROUP:New():FilterPrefixes(group):FilterCoalitions(self.Coalition):FilterStart() + local grouping = self.grouping or 5000 + --local acceptrange = self.acceptrange or 80000 + local interval = self.detectinterval or 60 + + --@param Functional.Detection #DETECTION_AREAS _MANTISdetection [Internal] The MANTIS detection object + local MANTISAwacs = DETECTION_AREAS:New( groupset, grouping ) --[Internal] Grouping detected objects to 5000m zones + MANTISAwacs:FilterCategories({ Unit.Category.AIRPLANE, Unit.Category.HELICOPTER }) + MANTISAwacs:SetAcceptRange(self.awacsrange) --250km + MANTISAwacs:SetRefreshTimeInterval(interval) + MANTISAwacs:Start() + + function MANTISAwacs:OnAfterDetectedItem(From,Event,To,DetectedItem) + --BASE:I( { From, Event, To, DetectedItem }) + local debug = false + if DetectedItem.IsDetected and debug then + local Coordinate = DetectedItem.Coordinate -- Core.Point#COORDINATE + local text = "Awacs Detection at "..Coordinate:ToStringLLDMS() + local m = MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) + end + end + return MANTISAwacs + end + + --- [Internal] Function to set the SAM start state + -- @param #MANTIS self + -- @return #MANTIS self + function MANTIS:SetSAMStartState() + -- DONE: if using dynamic filtering, update SAM_Table and the (active) SEAD groups, pull req #1405/#1406 + self:T(self.lid.."Setting SAM Start States") + -- get SAM Group + local SAM_SET = self.SAM_Group + local SAM_Grps = SAM_SET.Set --table of objects + local SAM_Tbl = {} -- table of SAM defense zones + local SEAD_Grps = {} -- table of SAM names to make evasive + local engagerange = self.engagerange -- firing range in % of max + --cycle through groups and set alarm state etc + for _i,_group in pairs (SAM_Grps) do + local group = _group -- Wrapper.Group#GROUP + -- TODO: add emissions on/off + if self.UseEmOnOff then + group:EnableEmission(false) + --group:SetAIOff() + else + group:OptionAlarmStateGreen() -- AI off + end + group:SetOption(AI.Option.Ground.id.AC_ENGAGEMENT_RANGE_RESTRICTION,engagerange) --default engagement will be 75% of firing range + if group:IsGround() and group:IsAlive() then + local grpname = group:GetName() + local grpcoord = group:GetCoordinate() + table.insert( SAM_Tbl, {grpname, grpcoord}) + table.insert( SEAD_Grps, grpname ) + self.SamStateTracker[grpname] = "GREEN" + end + end + self.SAM_Table = SAM_Tbl + -- make SAMs evasive + local mysead = SEAD:New( SEAD_Grps ) + mysead:SetEngagementRange(engagerange) + self.mysead = mysead + return self + end + + --- [Internal] Function to update SAM table and SEAD state + -- @param #MANTIS self + -- @return #MANTIS self + function MANTIS:_RefreshSAMTable() + self:T(self.lid.."RefreshSAMTable") + -- Requires SEAD 0.2.2 or better + -- get SAM Group + local SAM_SET = self.SAM_Group + local SAM_Grps = SAM_SET.Set --table of objects + local SAM_Tbl = {} -- table of SAM defense zones + local SEAD_Grps = {} -- table of SAM names to make evasive + local engagerange = self.engagerange -- firing range in % of max + --cycle through groups and set alarm state etc + for _i,_group in pairs (SAM_Grps) do + local group = _group + group:SetOption(AI.Option.Ground.id.AC_ENGAGEMENT_RANGE_RESTRICTION,engagerange) --engagement will be 75% of firing range + if group:IsGround() and group:IsAlive() then + local grpname = group:GetName() + local grpcoord = group:GetCoordinate() + table.insert( SAM_Tbl, {grpname, grpcoord}) -- make the table lighter, as I don't really use the zone here + table.insert( SEAD_Grps, grpname ) + end + end + self.SAM_Table = SAM_Tbl + -- make SAMs evasive + if self.mysead ~= nil then + local mysead = self.mysead + mysead:UpdateSet( SEAD_Grps ) + end + return self + end + + --- Function to link up #MANTIS with a #SHORAD installation + -- @param #MANTIS self + -- @param Functional.Shorad#SHORAD Shorad The #SHORAD object + -- @param #number Shoradtime Number of seconds #SHORAD stays active post wake-up + function MANTIS:AddShorad(Shorad,Shoradtime) + self:T(self.lid.."AddShorad") + local Shorad = Shorad or nil + local ShoradTime = Shoradtime or 600 + local ShoradLink = true + if Shorad:IsInstanceOf("SHORAD") then + self.ShoradLink = ShoradLink + self.Shorad = Shorad --#SHORAD + self.ShoradTime = Shoradtime -- #number + end + return self + end + + --- Function to unlink #MANTIS from a #SHORAD installation + -- @param #MANTIS self + function MANTIS:RemoveShorad() + self:T(self.lid.."RemoveShorad") + self.ShoradLink = false + return self + end + +----------------------------------------------------------------------- +-- MANTIS main functions +----------------------------------------------------------------------- + + --- [Internal] Check detection function + -- @param #MANTIS self + -- @param Functional.Detection#DETECTION_AREAS detection Detection object + -- @return #MANTIS self + function MANTIS:_Check(detection) + self:T(self.lid .. "Check") + --get detected set + local detset = detection:GetDetectedItemCoordinates() + self:T("Check:", {detset}) + -- randomly update SAM Table + local rand = math.random(1,100) + if rand > 65 then -- 1/3 of cases + self:_RefreshSAMTable() + end + -- switch SAMs on/off if (n)one of the detected groups is inside their reach + local samset = self:_GetSAMTable() -- table of i.1=names, i.2=coordinates + for _,_data in pairs (samset) do + local samcoordinate = _data[2] + local name = _data[1] + local samgroup = GROUP:FindByName(name) + local IsInZone, Distance = self:CheckObjectInZone(detset, samcoordinate) + if IsInZone then --check any target in zone + if samgroup:IsAlive() then + -- switch on SAM + if self.UseEmOnOff then + -- TODO: add emissions on/off + --samgroup:SetAIOn() + samgroup:EnableEmission(true) + end + samgroup:OptionAlarmStateRed() + if self.SamStateTracker[name] ~= "RED" then + self:__RedState(1,samgroup) + self.SamStateTracker[name] = "RED" + end + -- link in to SHORAD if available + -- DONE: Test integration fully + if self.ShoradLink and Distance < self.ShoradActDistance then -- don't give SHORAD position away too early + local Shorad = self.Shorad + local radius = self.checkradius + local ontime = self.ShoradTime + Shorad:WakeUpShorad(name, radius, ontime) + self:__ShoradActivated(1,name, radius, ontime) + end + -- debug output + if self.debug or self.verbose then + local text = string.format("SAM %s switched to alarm state RED!", name) + local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) + if self.verbose then self:I(self.lid..text) end + end + end --end alive + else + if samgroup:IsAlive() then + -- switch off SAM + if self.UseEmOnOff then + samgroup:EnableEmission(false) + end + samgroup:OptionAlarmStateGreen() + if self.SamStateTracker[name] ~= "GREEN" then + self:__GreenState(1,samgroup) + self.SamStateTracker[name] = "GREEN" + end + if self.debug or self.verbose then + local text = string.format("SAM %s switched to alarm state GREEN!", name) + local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug) + if self.verbose then self:I(self.lid..text) end + end + end --end alive + end --end check + end --for for loop + return self + end + + --- [Internal] Relocation relay function + -- @param #MANTIS self + -- @return #MANTIS self + function MANTIS:_Relocate() + self:T(self.lid .. "Relocate") + self:_RelocateGroups() + return self + end + + --- [Internal] Check advanced state + -- @param #MANTIS self + -- @return #MANTIS self + function MANTIS:_CheckAdvState() + self:T(self.lid .. "CheckAdvSate") + local interval, oldstate = self:_CalcAdvState() + local newstate = self.adv_state + if newstate ~= oldstate then + -- deal with new state + self:__AdvStateChange(1,oldstate,newstate,interval) + if newstate == 2 then + -- switch alarm state RED + self.state2flag = true + local samset = self:_GetSAMTable() -- table of i.1=names, i.2=coordinates + for _,_data in pairs (samset) do + local name = _data[1] + local samgroup = GROUP:FindByName(name) + if samgroup:IsAlive() then + if self.UseEmOnOff then + -- TODO: add emissions on/off + --samgroup:SetAIOn() + samgroup:EnableEmission(true) + end + samgroup:OptionAlarmStateRed() + end -- end alive + end -- end for loop + elseif newstate <= 1 then + -- change MantisTimer to slow down or speed up + self.detectinterval = interval + self.state2flag = false + end + end -- end newstate vs oldstate + return self + end + + --- [Internal] Check DLink state + -- @param #MANTIS self + -- @return #MANTIS self + function MANTIS:_CheckDLinkState() + self:T(self.lid .. "_CheckDLinkState") + local dlink = self.Detection -- Ops.Intelligence#INTEL_DLINK + local TS = timer.getAbsTime() + if not dlink:Is("Running") and (TS - self.DLTimeStamp > 29) then + self.DLink = false + self.Detection = self:StartDetection() -- fall back + self:I(self.lid .. "Intel DLink not running - switching back to single detection!") + end + end + + --- [Internal] Function to set start state + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @return #MANTIS self + function MANTIS:onafterStart(From, Event, To) + self:T({From, Event, To}) + self:T(self.lid.."Starting MANTIS") + self:SetSAMStartState() + if not self.DLink then + self.Detection = self:StartDetection() + end + if self.advAwacs then + self.AWACS_Detection = self:StartAwacsDetection() + end + self:__Status(-math.random(1,10)) + return self + end + + --- [Internal] Before status function for MANTIS + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @return #MANTIS self + function MANTIS:onbeforeStatus(From, Event, To) + self:T({From, Event, To}) + -- check detection + if not self.state2flag then + self:_Check(self.Detection) + end + + -- check Awacs + if self.advAwacs and not self.state2flag then + self:_Check(self.AWACS_Detection) + end + + -- relocate HQ and EWR + if self.autorelocate then + local relointerval = self.relointerval + local thistime = timer.getAbsTime() + local timepassed = thistime - self.TimeStamp + + local halfintv = math.floor(timepassed / relointerval) + + --self:T({timepassed=timepassed, halfintv=halfintv}) + + if halfintv >= 1 then + self.TimeStamp = timer.getAbsTime() + self:_Relocate() + self:__Relocating(1) + end + end + + -- advanced state check + if self.advanced then + self:_CheckAdvState() + end + + -- check DLink state + if self.DLink then + self:_CheckDLinkState() + end + + return self + end + + --- [Internal] Status function for MANTIS + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @return #MANTIS self + function MANTIS:onafterStatus(From,Event,To) + self:T({From, Event, To}) + -- Display some states + if self.debug then + self:I(self.lid .. "Status Report") + for _name,_state in pairs(self.SamStateTracker) do + self:I(string.format("Site %s\tStatus %s",_name,_state)) + end + end + local interval = self.detectinterval * -1 + self:__Status(interval) + return self + end + + --- [Internal] Function to stop MANTIS + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @return #MANTIS self + function MANTIS:onafterStop(From, Event, To) + self:T({From, Event, To}) + return self + end + + --- [Internal] Function triggered by Event Relocating + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @return #MANTIS self + function MANTIS:onafterRelocating(From, Event, To) + self:T({From, Event, To}) + return self + end + + --- [Internal] Function triggered by Event GreenState + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @param Wrapper.Group#GROUP Group The GROUP object whose state was changed + -- @return #MANTIS self + function MANTIS:onafterGreenState(From, Event, To, Group) + self:T({From, Event, To, Group}) + return self + end + + --- [Internal] Function triggered by Event RedState + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @param Wrapper.Group#GROUP Group The GROUP object whose state was changed + -- @return #MANTIS self + function MANTIS:onafterRedState(From, Event, To, Group) + self:T({From, Event, To, Group}) + return self + end + + --- [Internal] Function triggered by Event AdvStateChange + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @param #number Oldstate Old state - 0 = green, 1 = amber, 2 = red + -- @param #number Newstate New state - 0 = green, 1 = amber, 2 = red + -- @param #number Interval Calculated detection interval based on state and advanced feature setting + -- @return #MANTIS self + function MANTIS:onafterAdvStateChange(From, Event, To, Oldstate, Newstate, Interval) + self:T({From, Event, To, Oldstate, Newstate, Interval}) + return self + end + + --- [Internal] Function triggered by Event ShoradActivated + -- @param #MANTIS self + -- @param #string From The From State + -- @param #string Event The Event + -- @param #string To The To State + -- @param #string Name Name of the GROUP which SHORAD shall protect + -- @param #number Radius Radius around the named group to find SHORAD groups + -- @param #number Ontime Seconds the SHORAD will stay active + function MANTIS:onafterShoradActivated(From, Event, To, Name, Radius, Ontime) + self:T({From, Event, To, Name, Radius, Ontime}) + return self + end +end +----------------------------------------------------------------------- +-- MANTIS end +----------------------------------------------------------------------- diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 327e2e518..691f53338 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -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. diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index 0f4b164fe..7ba9dcfc7 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -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 diff --git a/Moose Development/Moose/Functional/Scoring.lua b/Moose Development/Moose/Functional/Scoring.lua index b5c8da81b..b92c7e789 100644 --- a/Moose Development/Moose/Functional/Scoring.lua +++ b/Moose Development/Moose/Functional/Scoring.lua @@ -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 diff --git a/Moose Development/Moose/Functional/Sead.lua b/Moose Development/Moose/Functional/Sead.lua index 6edb99312..c99e1f241 100644 --- a/Moose Development/Moose/Functional/Sead.lua +++ b/Moose Development/Moose/Functional/Sead.lua @@ -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 diff --git a/Moose Development/Moose/Functional/Shorad.lua b/Moose Development/Moose/Functional/Shorad.lua new file mode 100644 index 000000000..958877ec8 --- /dev/null +++ b/Moose Development/Moose/Functional/Shorad.lua @@ -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 +----------------------------------------------------------------------- \ No newline at end of file diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index f6bb9c8da..e352dce84 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -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) diff --git a/Moose Development/Moose/Globals.lua b/Moose Development/Moose/Globals.lua index d972cf38b..f63f57e84 100644 --- a/Moose Development/Moose/Globals.lua +++ b/Moose Development/Moose/Globals.lua @@ -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 /Scripts/MissionScripting.lua and comment out the lines with sanitizeModule(''). Use at your own risk!)") +end diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index 2474442f4..9ccb7ebb5 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -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' ) diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index 29b2918b2..f865f98be 100644 --- a/Moose Development/Moose/Ops/ATIS.lua +++ b/Moose Development/Moose/Ops/ATIS.lua @@ -7,7 +7,7 @@ -- * Wind direction and speed -- * Visibility -- * Cloud coverage, base and ceiling --- * Temprature +-- * Temperature -- * Dew point (approximate as there is no relative humidity in DCS yet) -- * Pressure QNH/QFE -- * Weather phenomena: rain, thunderstorm, fog, dust @@ -18,6 +18,7 @@ -- * Option to present information in imperial or metric units -- * Runway length and airfield elevation (optional) -- * Frequencies/channels of nav aids (ILS, VOR, NDB, TACAN, PRMG, RSBN) (optional) +-- * SRS Simple-Text-To-Speech (STTS) integration (no sound files necessary) -- -- === -- @@ -34,7 +35,7 @@ -- -- === -- --- ## Sound files: Check out the pinned messages in the Moose discord #ops-atis channel. +-- ## Sound files: [MOOSE Sound Files](https://github.com/FlightControl-Master/MOOSE_SOUND/releases) -- -- === -- @@ -59,7 +60,7 @@ -- @field #number frequency Radio frequency in MHz. -- @field #number modulation Radio modulation 0=AM or 1=FM. -- @field #number power Radio power in Watts. Default 100 W. --- @field Core.RadioQueue#RADIOQUEUE radioqueue Radio queue for broadcasing messages. +-- @field Sound.RadioQueue#RADIOQUEUE radioqueue Radio queue for broadcasing messages. -- @field #string soundpath Path to sound files. -- @field #string relayunitname Name of the radio relay unit. -- @field #table towerfrequency Table with tower frequencies. @@ -88,6 +89,9 @@ -- @field #boolean usemarker Use mark on the F10 map. -- @field #number markerid Numerical ID of the F10 map mark point. -- @field #number relHumidity Relative humidity (used to approximately calculate the dew point). +-- @field #boolean useSRS If true, use SRS for transmission. +-- @field Sound.SRS#MSRS msrs Moose SRS object. +-- @field #number dTQueueCheck Time interval to check the radio queue. Default 5 sec or 90 sec if SRS is used. -- @extends Core.Fsm#FSM --- *It is a very sad thing that nowadays there is so little useless information.* - Oscar Wilde @@ -252,6 +256,16 @@ -- # Marks on the F10 Map -- -- You can place marks on the F10 map via the @{#ATIS.SetMapMarks}() function. These will contain info about the ATIS frequency, the currently active runway and some basic info about the weather (wind, pressure and temperature). +-- +-- # Text-To-Speech +-- +-- You can enable text-to-speech ATIS information with the @{#ATIS.SetSRS}() function. This uses [SRS](http://dcssimpleradio.com/) (Version >= 1.9.6.0) for broadcasing. +-- Advantages are that **no sound files** or radio relay units are necessary. Also the issue that FC3 aircraft hear all transmissions will be circumvented. +-- +-- The @{#ATIS.SetSRS}() requires you to specify the path to the SRS install directory or more specifically the path to the DCS-SR-ExternalAudio.exe file. +-- +-- Unfortunately, it is not possible to determine the duration of the complete transmission. So once the transmission is finished, there might be some radio silence before +-- the next iteration begins. You can fine tune the time interval between transmissions with the @{#ATIS.SetQueueUpdateTime}() function. The default interval is 90 seconds. -- -- # Examples -- @@ -283,7 +297,14 @@ -- atisAbuDhabi:SetTowerFrequencies({250.5, 119.2}) -- atisAbuDhabi:SetVOR(114.25) -- atisAbuDhabi:Start() +-- +-- ## SRS +-- +-- atis=ATIS:New("Batumi", 305, radio.modulation.AM) +-- atis:SetSRS("D:\\DCS\\_SRS\\", "male", "en-US") +-- atis:Start() -- +-- This uses a male voice with US accent. It requires SRS to be installed in the `D:\DCS\_SRS\` directory. Not that backslashes need to be escaped or simply use slashes (as in linux). -- -- @field #ATIS ATIS = { @@ -368,6 +389,7 @@ ATIS.Alphabet = { -- @field #number PersianGulf +2° (East). -- @field #number TheChannel -10° (West). -- @field #number Syria +5° (East). +-- @field #number MarianaIslands +2° (East). ATIS.RunwayM2T={ Caucasus=0, Nevada=12, @@ -375,6 +397,7 @@ ATIS.RunwayM2T={ PersianGulf=2, TheChannel=-10, Syria=5, + MarianaIslands=2, } --- Whether ICAO phraseology is used for ATIS broadcasts. @@ -385,6 +408,7 @@ ATIS.RunwayM2T={ -- @field #boolean PersianGulf true. -- @field #boolean TheChannel true. -- @field #boolean Syria true. +-- @field #boolean MarianaIslands true. ATIS.ICAOPhraseology={ Caucasus=true, Nevada=false, @@ -392,6 +416,7 @@ ATIS.ICAOPhraseology={ PersianGulf=true, TheChannel=true, Syria=true, + MarianaIslands=true, } --- Nav point data. @@ -564,7 +589,7 @@ _ATIS={} --- ATIS class version. -- @field #string version -ATIS.version="0.9.0" +ATIS.version="0.9.6" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -580,6 +605,8 @@ ATIS.version="0.9.0" -- DONE: Metric units. -- DONE: Set UTC correction. -- DONE: Set magnetic variation. +-- DONE: New DCS 2.7 weather presets. +-- DONE: whatever ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor @@ -626,6 +653,7 @@ function ATIS:New(airbasename, frequency, modulation) self:SetAltimeterQNH(true) self:SetMapMarks(false) self:SetRelativeHumidity() + self:SetQueueUpdateTime() -- Start State. self:SetStartState("Stopped") @@ -953,7 +981,9 @@ end -- * 170° on the Normany map -- * 182° on the Persian Gulf map -- --- Likewise, to convert *magnetic* into *true* heading, one has to substract easterly and add westerly variation. +-- Likewise, to convert *true* into *magnetic* heading, one has to substract easterly and add westerly variation. +-- +-- Or you make your life simple and just include the sign so you don't have to bother about East/West. -- -- @param #ATIS self -- @param #number magvar Magnetic variation in degrees. Positive for easterly and negative for westerly variation. Default is magnatic declinaton of the used map, c.f. @{Utilities.UTils#UTILS.GetMagneticDeclination}. @@ -1098,6 +1128,44 @@ function ATIS:MarkRunways(markall) end end +--- Use SRS Simple-Text-To-Speech for transmissions. No sound files necessary. +-- @param #ATIS self +-- @param #string PathToSRS Path to SRS directory. +-- @param #string Gender Gender: "male" or "female" (default). +-- @param #string Culture Culture, e.g. "en-GB" (default). +-- @param #string Voice Specific voice. Overrides `Gender` and `Culture`. +-- @param #number Port SRS port. Default 5002. +-- @return #ATIS self +function ATIS:SetSRS(PathToSRS, Gender, Culture, Voice, Port) + self.useSRS=true + self.msrs=MSRS:New(PathToSRS, self.frequency, self.modulation) + self.msrs:SetGender(Gender) + self.msrs:SetCulture(Culture) + self.msrs:SetVoice(Voice) + self.msrs:SetPort(Port) + self.msrs:SetCoalition(self:GetCoalition()) + if self.dTQueueCheck<=10 then + self:SetQueueUpdateTime(90) + end + return self +end + +--- Set the time interval between radio queue updates. +-- @param #ATIS self +-- @param #number TimeInterval Interval in seconds. Default 5 sec. +-- @return #ATIS self +function ATIS:SetQueueUpdateTime(TimeInterval) + self.dTQueueCheck=TimeInterval or 5 +end + +--- Get the coalition of the associated airbase. +-- @param #ATIS self +-- @return #number Coalition of the associcated airbase. +function ATIS:GetCoalition() + local coal=self.airbase and self.airbase:GetCoalition() or nil + return coal +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Start & Status ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1144,6 +1212,10 @@ function ATIS:onafterStart(From, Event, To) -- Start radio queue. self.radioqueue:Start(1, 0.1) + + -- Handle airbase capture + -- Handle events. + self:HandleEvent(EVENTS.BaseCaptured) -- Init status updates. self:__Status(-2) @@ -1169,7 +1241,13 @@ function ATIS:onafterStatus(From, Event, To) end -- Info text. - local text=string.format("State %s: Freq=%.3f MHz %s, Relay unit=%s (alive=%s)", fsmstate, self.frequency, UTILS.GetModulationName(self.modulation), tostring(self.relayunitname), relayunitstatus) + local text=string.format("State %s: Freq=%.3f MHz %s", fsmstate, self.frequency, UTILS.GetModulationName(self.modulation)) + if self.useSRS then + text=text..string.format(", SRS path=%s (%s), gender=%s, culture=%s, voice=%s", + tostring(self.msrs.path), tostring(self.msrs.port), tostring(self.msrs.gender), tostring(self.msrs.culture), tostring(self.msrs.voice)) + else + text=text..string.format(", Relay unit=%s (alive=%s)", tostring(self.relayunitname), relayunitstatus) + end self:I(self.lid..text) self:__Status(-60) @@ -1186,15 +1264,25 @@ end -- @param #string To To state. function ATIS:onafterCheckQueue(From, Event, To) - if #self.radioqueue.queue==0 then - self:T(self.lid..string.format("Radio queue empty. Repeating message.")) + if self.useSRS then + self:Broadcast() + else - self:T2(self.lid..string.format("Radio queue %d transmissions queued.", #self.radioqueue.queue)) + + if #self.radioqueue.queue==0 then + self:T(self.lid..string.format("Radio queue empty. Repeating message.")) + self:Broadcast() + else + self:T2(self.lid..string.format("Radio queue %d transmissions queued.", #self.radioqueue.queue)) + end + + + end - -- Check back in 5 seconds. - self:__CheckQueue(-5) + -- Check back in 5 seconds. + self:__CheckQueue(-math.abs(self.dTQueueCheck)) end --- Broadcast ATIS radio message. @@ -1318,9 +1406,16 @@ function ATIS:onafterBroadcast(From, Event, To) time=time-UTILS.GMTToLocalTimeDifference()*60*60 end + if time < 0 then + time = 24*60*60 + time --avoid negative time around midnight + end + local clock=UTILS.SecondsToClock(time) local zulu=UTILS.Split(clock, ":") local ZULU=string.format("%s%s", zulu[1], zulu[2]) + if self.useSRS then + ZULU=string.format("%s hours", zulu[1]) + end -- NATO time stamp. 0=Alfa, 1=Bravo, 2=Charlie, etc. @@ -1340,10 +1435,17 @@ function ATIS:onafterBroadcast(From, Event, To) local sunrise=coord:GetSunrise() sunrise=UTILS.Split(sunrise, ":") local SUNRISE=string.format("%s%s", sunrise[1], sunrise[2]) + if self.useSRS then + SUNRISE=string.format("%s %s hours", sunrise[1], sunrise[2]) + end local sunset=coord:GetSunset() sunset=UTILS.Split(sunset, ":") local SUNSET=string.format("%s%s", sunset[1], sunset[2]) + if self.useSRS then + SUNSET=string.format("%s %s hours", sunset[1], sunset[2]) + end + --------------------------------- --- Temperature and Dew Point --- @@ -1427,9 +1529,127 @@ function ATIS:onafterBroadcast(From, Event, To) local cloudceil=clouds.base+clouds.thickness local clouddens=clouds.density + -- Cloud preset (DCS 2.7) + local cloudspreset=clouds.preset or "Nothing" + -- Precepitation: 0=None, 1=Rain, 2=Thunderstorm, 3=Snow, 4=Snowstorm. - local precepitation=tonumber(clouds.iprecptns) + local precepitation=0 + if cloudspreset:find("Preset10") then + -- Scattered 5 + clouddens=4 + elseif cloudspreset:find("Preset11") then + -- Scattered 6 + clouddens=4 + elseif cloudspreset:find("Preset12") then + -- Scattered 7 + clouddens=4 + elseif cloudspreset:find("Preset13") then + -- Broken 1 + clouddens=7 + elseif cloudspreset:find("Preset14") then + -- Broken 2 + clouddens=7 + elseif cloudspreset:find("Preset15") then + -- Broken 3 + clouddens=7 + elseif cloudspreset:find("Preset16") then + -- Broken 4 + clouddens=7 + elseif cloudspreset:find("Preset17") then + -- Broken 5 + clouddens=7 + elseif cloudspreset:find("Preset18") then + -- Broken 6 + clouddens=7 + elseif cloudspreset:find("Preset19") then + -- Broken 7 + clouddens=7 + elseif cloudspreset:find("Preset20") then + -- Broken 8 + clouddens=7 + elseif cloudspreset:find("Preset21") then + -- Overcast 1 + clouddens=9 + elseif cloudspreset:find("Preset22") then + -- Overcast 2 + clouddens=9 + elseif cloudspreset:find("Preset23") then + -- Overcast 3 + clouddens=9 + elseif cloudspreset:find("Preset24") then + -- Overcast 4 + clouddens=9 + elseif cloudspreset:find("Preset25") then + -- Overcast 5 + clouddens=9 + elseif cloudspreset:find("Preset26") then + -- Overcast 6 + clouddens=9 + elseif cloudspreset:find("Preset27") then + -- Overcast 7 + clouddens=9 + elseif cloudspreset:find("Preset1") then + -- Light Scattered 1 + clouddens=1 + elseif cloudspreset:find("Preset2") then + -- Light Scattered 2 + clouddens=1 + elseif cloudspreset:find("Preset3") then + -- High Scattered 1 + clouddens=4 + elseif cloudspreset:find("Preset4") then + -- High Scattered 2 + clouddens=4 + elseif cloudspreset:find("Preset5") then + -- Scattered 1 + clouddens=4 + elseif cloudspreset:find("Preset6") then + -- Scattered 2 + clouddens=4 + elseif cloudspreset:find("Preset7") then + -- Scattered 3 + clouddens=4 + elseif cloudspreset:find("Preset8") then + -- High Scattered 3 + clouddens=4 + elseif cloudspreset:find("Preset9") then + -- Scattered 4 + clouddens=4 + elseif cloudspreset:find("RainyPreset") then + -- Overcast + Rain + clouddens=9 + if temperature>5 then + precepitation=1 -- rain + else + precepitation=3 -- snow + end + elseif cloudspreset:find("RainyPreset1") then + -- Overcast + Rain + clouddens=9 + if temperature>5 then + precepitation=1 -- rain + else + precepitation=3 -- snow + end + elseif cloudspreset:find("RainyPreset2") then + -- Overcast + Rain + clouddens=9 + if temperature>5 then + precepitation=1 -- rain + else + precepitation=3 -- snow + end + elseif cloudspreset:find("RainyPreset3") then + -- Overcast + Rain + clouddens=9 + if temperature>5 then + precepitation=1 -- rain + else + precepitation=3 -- snow + end + end + local CLOUDBASE=string.format("%d", UTILS.MetersToFeet(cloudbase)) local CLOUDCEIL=string.format("%d", UTILS.MetersToFeet(cloudceil)) @@ -1491,36 +1711,46 @@ function ATIS:onafterBroadcast(From, Event, To) if self.airbasename:find("AFB")==nil and self.airbasename:find("Airport")==nil and self.airbasename:find("Airstrip")==nil and self.airbasename:find("airfield")==nil and self.airbasename:find("AB")==nil then subtitle=subtitle.." Airport" end - self.radioqueue:NewTransmission(string.format("%s/%s.ogg", self.theatre, self.airbasename), 3.0, self.soundpath, nil, nil, subtitle, self.subduration) + if not self.useSRS then + self.radioqueue:NewTransmission(string.format("%s/%s.ogg", self.theatre, self.airbasename), 3.0, self.soundpath, nil, nil, subtitle, self.subduration) + end local alltext=subtitle -- Information tag subtitle=string.format("Information %s", NATO) local _INFORMATION=subtitle - self:Transmission(ATIS.Sound.Information, 0.5, subtitle) - self.radioqueue:NewTransmission(string.format("NATO Alphabet/%s.ogg", NATO), 0.75, self.soundpath) + if not self.useSRS then + self:Transmission(ATIS.Sound.Information, 0.5, subtitle) + self.radioqueue:NewTransmission(string.format("NATO Alphabet/%s.ogg", NATO), 0.75, self.soundpath) + end alltext=alltext..";\n"..subtitle -- Zulu Time subtitle=string.format("%s Zulu", ZULU) - self.radioqueue:Number2Transmission(ZULU, nil, 0.5) - self:Transmission(ATIS.Sound.Zulu, 0.2, subtitle) + if not self.useSRS then + self.radioqueue:Number2Transmission(ZULU, nil, 0.5) + self:Transmission(ATIS.Sound.Zulu, 0.2, subtitle) + end alltext=alltext..";\n"..subtitle if not self.zulutimeonly then -- Sunrise Time subtitle=string.format("Sunrise at %s local time", SUNRISE) - self:Transmission(ATIS.Sound.SunriseAt, 0.5, subtitle) - self.radioqueue:Number2Transmission(SUNRISE, nil, 0.2) - self:Transmission(ATIS.Sound.TimeLocal, 0.2) + if not self.useSRS then + self:Transmission(ATIS.Sound.SunriseAt, 0.5, subtitle) + self.radioqueue:Number2Transmission(SUNRISE, nil, 0.2) + self:Transmission(ATIS.Sound.TimeLocal, 0.2) + end alltext=alltext..";\n"..subtitle -- Sunset Time subtitle=string.format("Sunset at %s local time", SUNSET) - self:Transmission(ATIS.Sound.SunsetAt, 0.5, subtitle) - self.radioqueue:Number2Transmission(SUNSET, nil, 0.5) - self:Transmission(ATIS.Sound.TimeLocal, 0.2) + if not self.useSRS then + self:Transmission(ATIS.Sound.SunsetAt, 0.5, subtitle) + self.radioqueue:Number2Transmission(SUNSET, nil, 0.5) + self:Transmission(ATIS.Sound.TimeLocal, 0.2) + end alltext=alltext..";\n"..subtitle end @@ -1534,17 +1764,19 @@ function ATIS:onafterBroadcast(From, Event, To) subtitle=subtitle..", gusting" end local _WIND=subtitle - self:Transmission(ATIS.Sound.WindFrom, 1.0, subtitle) - self.radioqueue:Number2Transmission(WINDFROM) - self:Transmission(ATIS.Sound.At, 0.2) - self.radioqueue:Number2Transmission(WINDSPEED) - if self.metric then - self:Transmission(ATIS.Sound.MetersPerSecond, 0.2) - else - self:Transmission(ATIS.Sound.Knots, 0.2) - end - if turbulence>0 then - self:Transmission(ATIS.Sound.Gusting, 0.2) + if not self.useSRS then + self:Transmission(ATIS.Sound.WindFrom, 1.0, subtitle) + self.radioqueue:Number2Transmission(WINDFROM) + self:Transmission(ATIS.Sound.At, 0.2) + self.radioqueue:Number2Transmission(WINDSPEED) + if self.metric then + self:Transmission(ATIS.Sound.MetersPerSecond, 0.2) + else + self:Transmission(ATIS.Sound.Knots, 0.2) + end + if turbulence>0 then + self:Transmission(ATIS.Sound.Gusting, 0.2) + end end alltext=alltext..";\n"..subtitle @@ -1554,12 +1786,14 @@ function ATIS:onafterBroadcast(From, Event, To) else subtitle=string.format("Visibility %s SM", VISIBILITY) end - self:Transmission(ATIS.Sound.Visibilty, 1.0, subtitle) - self.radioqueue:Number2Transmission(VISIBILITY) - if self.metric then - self:Transmission(ATIS.Sound.Kilometers, 0.2) - else - self:Transmission(ATIS.Sound.StatuteMiles, 0.2) + if not self.useSRS then + self:Transmission(ATIS.Sound.Visibilty, 1.0, subtitle) + self.radioqueue:Number2Transmission(VISIBILITY) + if self.metric then + self:Transmission(ATIS.Sound.Kilometers, 0.2) + else + self:Transmission(ATIS.Sound.StatuteMiles, 0.2) + end end alltext=alltext..";\n"..subtitle @@ -1599,57 +1833,67 @@ function ATIS:onafterBroadcast(From, Event, To) -- Actual output if wp then subtitle=string.format("Weather phenomena:%s", wpsub) - self:Transmission(ATIS.Sound.WeatherPhenomena, 1.0, subtitle) - if precepitation==1 then - self:Transmission(ATIS.Sound.Rain, 0.5) - elseif precepitation==2 then - self:Transmission(ATIS.Sound.ThunderStorm, 0.5) - elseif precepitation==3 then - self:Transmission(ATIS.Sound.Snow, 0.5) - elseif precepitation==4 then - self:Transmission(ATIS.Sound.SnowStorm, 0.5) - end - if fog then - self:Transmission(ATIS.Sound.Fog, 0.5) - end - if dust then - self:Transmission(ATIS.Sound.Dust, 0.5) + if not self.useSRS then + self:Transmission(ATIS.Sound.WeatherPhenomena, 1.0, subtitle) + if precepitation==1 then + self:Transmission(ATIS.Sound.Rain, 0.5) + elseif precepitation==2 then + self:Transmission(ATIS.Sound.ThunderStorm, 0.5) + elseif precepitation==3 then + self:Transmission(ATIS.Sound.Snow, 0.5) + elseif precepitation==4 then + self:Transmission(ATIS.Sound.SnowStorm, 0.5) + end + if fog then + self:Transmission(ATIS.Sound.Fog, 0.5) + end + if dust then + self:Transmission(ATIS.Sound.Dust, 0.5) + end end alltext=alltext..";\n"..subtitle end -- Cloud base - self:Transmission(CloudCover, 1.0, CLOUDSsub) + if not self.useSRS then + self:Transmission(CloudCover, 1.0, CLOUDSsub) + end if CLOUDBASE and static then -- Base + local cbase=tostring(tonumber(CLOUDBASE1000)*1000+tonumber(CLOUDBASE0100)*100) + local cceil=tostring(tonumber(CLOUDCEIL1000)*1000+tonumber(CLOUDCEIL0100)*100) if self.metric then - subtitle=string.format("Cloudbase %s, ceiling %s meters", CLOUDBASE, CLOUDCEIL) + --subtitle=string.format("Cloud base %s, ceiling %s meters", CLOUDBASE, CLOUDCEIL) + subtitle=string.format("Cloud base %s, ceiling %s meters", cbase, cceil) else - subtitle=string.format("Cloudbase %s, ceiling %s ft", CLOUDBASE, CLOUDCEIL) + --subtitle=string.format("Cloud base %s, ceiling %s feet", CLOUDBASE, CLOUDCEIL) + subtitle=string.format("Cloud base %s, ceiling %s feet", cbase, cceil) end - self:Transmission(ATIS.Sound.CloudBase, 1.0, subtitle) - if tonumber(CLOUDBASE1000)>0 then - self.radioqueue:Number2Transmission(CLOUDBASE1000) - self:Transmission(ATIS.Sound.Thousand, 0.1) - end - if tonumber(CLOUDBASE0100)>0 then - self.radioqueue:Number2Transmission(CLOUDBASE0100) - self:Transmission(ATIS.Sound.Hundred, 0.1) - end - -- Ceiling - self:Transmission(ATIS.Sound.CloudCeiling, 0.5) - if tonumber(CLOUDCEIL1000)>0 then - self.radioqueue:Number2Transmission(CLOUDCEIL1000) - self:Transmission(ATIS.Sound.Thousand, 0.1) - end - if tonumber(CLOUDCEIL0100)>0 then - self.radioqueue:Number2Transmission(CLOUDCEIL0100) - self:Transmission(ATIS.Sound.Hundred, 0.1) - end - if self.metric then - self:Transmission(ATIS.Sound.Meters, 0.1) - else - self:Transmission(ATIS.Sound.Feet, 0.1) + if not self.useSRS then + self:Transmission(ATIS.Sound.CloudBase, 1.0, subtitle) + if tonumber(CLOUDBASE1000)>0 then + self.radioqueue:Number2Transmission(CLOUDBASE1000) + self:Transmission(ATIS.Sound.Thousand, 0.1) + end + if tonumber(CLOUDBASE0100)>0 then + self.radioqueue:Number2Transmission(CLOUDBASE0100) + self:Transmission(ATIS.Sound.Hundred, 0.1) + end + -- Ceiling + self:Transmission(ATIS.Sound.CloudCeiling, 0.5) + if tonumber(CLOUDCEIL1000)>0 then + self.radioqueue:Number2Transmission(CLOUDCEIL1000) + self:Transmission(ATIS.Sound.Thousand, 0.1) + end + if tonumber(CLOUDCEIL0100)>0 then + self.radioqueue:Number2Transmission(CLOUDCEIL0100) + self:Transmission(ATIS.Sound.Hundred, 0.1) + end + if self.metric then + self:Transmission(ATIS.Sound.Meters, 0.1) + else + self:Transmission(ATIS.Sound.Feet, 0.1) + end end end alltext=alltext..";\n"..subtitle @@ -1669,15 +1913,17 @@ function ATIS:onafterBroadcast(From, Event, To) end end local _TEMPERATURE=subtitle - self:Transmission(ATIS.Sound.Temperature, 1.0, subtitle) - if temperature<0 then - self:Transmission(ATIS.Sound.Minus, 0.2) - end - self.radioqueue:Number2Transmission(TEMPERATURE) - if self.TDegF then - self:Transmission(ATIS.Sound.DegreesFahrenheit, 0.2) - else - self:Transmission(ATIS.Sound.DegreesCelsius, 0.2) + if not self.useSRS then + self:Transmission(ATIS.Sound.Temperature, 1.0, subtitle) + if temperature<0 then + self:Transmission(ATIS.Sound.Minus, 0.2) + end + self.radioqueue:Number2Transmission(TEMPERATURE) + if self.TDegF then + self:Transmission(ATIS.Sound.DegreesFahrenheit, 0.2) + else + self:Transmission(ATIS.Sound.DegreesCelsius, 0.2) + end end alltext=alltext..";\n"..subtitle @@ -1696,15 +1942,17 @@ function ATIS:onafterBroadcast(From, Event, To) end end local _DEWPOINT=subtitle - self:Transmission(ATIS.Sound.DewPoint, 1.0, subtitle) - if dewpoint<0 then - self:Transmission(ATIS.Sound.Minus, 0.2) - end - self.radioqueue:Number2Transmission(DEWPOINT) - if self.TDegF then - self:Transmission(ATIS.Sound.DegreesFahrenheit, 0.2) - else - self:Transmission(ATIS.Sound.DegreesCelsius, 0.2) + if not self.useSRS then + self:Transmission(ATIS.Sound.DewPoint, 1.0, subtitle) + if dewpoint<0 then + self:Transmission(ATIS.Sound.Minus, 0.2) + end + self.radioqueue:Number2Transmission(DEWPOINT) + if self.TDegF then + self:Transmission(ATIS.Sound.DegreesFahrenheit, 0.2) + else + self:Transmission(ATIS.Sound.DegreesCelsius, 0.2) + end end alltext=alltext..";\n"..subtitle @@ -1713,51 +1961,53 @@ function ATIS:onafterBroadcast(From, Event, To) if self.qnhonly then subtitle=string.format("Altimeter %s.%s mmHg", QNH[1], QNH[2]) else - subtitle=string.format("Altimeter QNH %s.%s, QFE %s.%s mmHg", QNH[1], QNH[2], QFE[1], QFE[2]) + subtitle=string.format("Altimeter: QNH %s.%s, QFE %s.%s mmHg", QNH[1], QNH[2], QFE[1], QFE[2]) end else if self.metric then if self.qnhonly then subtitle=string.format("Altimeter %s.%s hPa", QNH[1], QNH[2]) else - subtitle=string.format("Altimeter QNH %s.%s, QFE %s.%s hPa", QNH[1], QNH[2], QFE[1], QFE[2]) + subtitle=string.format("Altimeter: QNH %s.%s, QFE %s.%s hPa", QNH[1], QNH[2], QFE[1], QFE[2]) end else if self.qnhonly then subtitle=string.format("Altimeter %s.%s inHg", QNH[1], QNH[2]) else - subtitle=string.format("Altimeter QNH %s.%s, QFE %s.%s inHg", QNH[1], QNH[2], QFE[1], QFE[2]) + subtitle=string.format("Altimeter: QNH %s.%s, QFE %s.%s inHg", QNH[1], QNH[2], QFE[1], QFE[2]) end end end local _ALTIMETER=subtitle - self:Transmission(ATIS.Sound.Altimeter, 1.0, subtitle) - if not self.qnhonly then - self:Transmission(ATIS.Sound.QNH, 0.5) - end - self.radioqueue:Number2Transmission(QNH[1]) - - if ATIS.ICAOPhraseology[UTILS.GetDCSMap()] then - self:Transmission(ATIS.Sound.Decimal, 0.2) - end - self.radioqueue:Number2Transmission(QNH[2]) - - if not self.qnhonly then - self:Transmission(ATIS.Sound.QFE, 0.75) - self.radioqueue:Number2Transmission(QFE[1]) - if ATIS.ICAOPhraseology[UTILS.GetDCSMap()] then - self:Transmission(ATIS.Sound.Decimal, 0.2) + if not self.useSRS then + self:Transmission(ATIS.Sound.Altimeter, 1.0, subtitle) + if not self.qnhonly then + self:Transmission(ATIS.Sound.QNH, 0.5) end - self.radioqueue:Number2Transmission(QFE[2]) - end + self.radioqueue:Number2Transmission(QNH[1]) + + if ATIS.ICAOPhraseology[UTILS.GetDCSMap()] then + self:Transmission(ATIS.Sound.Decimal, 0.2) + end + self.radioqueue:Number2Transmission(QNH[2]) - if self.PmmHg then - self:Transmission(ATIS.Sound.MillimetersOfMercury, 0.1) - else - if self.metric then - self:Transmission(ATIS.Sound.HectoPascal, 0.1) + if not self.qnhonly then + self:Transmission(ATIS.Sound.QFE, 0.75) + self.radioqueue:Number2Transmission(QFE[1]) + if ATIS.ICAOPhraseology[UTILS.GetDCSMap()] then + self:Transmission(ATIS.Sound.Decimal, 0.2) + end + self.radioqueue:Number2Transmission(QFE[2]) + end + + if self.PmmHg then + self:Transmission(ATIS.Sound.MillimetersOfMercury, 0.1) else - self:Transmission(ATIS.Sound.InchesOfMercury, 0.1) + if self.metric then + self:Transmission(ATIS.Sound.HectoPascal, 0.1) + else + self:Transmission(ATIS.Sound.InchesOfMercury, 0.1) + end end end alltext=alltext..";\n"..subtitle @@ -1770,12 +2020,14 @@ function ATIS:onafterBroadcast(From, Event, To) subtitle=subtitle.." Right" end local _RUNACT=subtitle - self:Transmission(ATIS.Sound.ActiveRunway, 1.0, subtitle) - self.radioqueue:Number2Transmission(runway) - if rwyLeft==true then - self:Transmission(ATIS.Sound.Left, 0.2) - elseif rwyLeft==false then - self:Transmission(ATIS.Sound.Right, 0.2) + if not self.useSRS then + self:Transmission(ATIS.Sound.ActiveRunway, 1.0, subtitle) + self.radioqueue:Number2Transmission(runway) + if rwyLeft==true then + self:Transmission(ATIS.Sound.Left, 0.2) + elseif rwyLeft==false then + self:Transmission(ATIS.Sound.Right, 0.2) + end end alltext=alltext..";\n"..subtitle @@ -1800,21 +2052,22 @@ function ATIS:onafterBroadcast(From, Event, To) end -- Transmit. - self:Transmission(ATIS.Sound.RunwayLength, 1.0, subtitle) - if tonumber(L1000)>0 then - self.radioqueue:Number2Transmission(L1000) - self:Transmission(ATIS.Sound.Thousand, 0.1) + if not self.useSRS then + self:Transmission(ATIS.Sound.RunwayLength, 1.0, subtitle) + if tonumber(L1000)>0 then + self.radioqueue:Number2Transmission(L1000) + self:Transmission(ATIS.Sound.Thousand, 0.1) + end + if tonumber(L0100)>0 then + self.radioqueue:Number2Transmission(L0100) + self:Transmission(ATIS.Sound.Hundred, 0.1) + end + if self.metric then + self:Transmission(ATIS.Sound.Meters, 0.1) + else + self:Transmission(ATIS.Sound.Feet, 0.1) + end end - if tonumber(L0100)>0 then - self.radioqueue:Number2Transmission(L0100) - self:Transmission(ATIS.Sound.Hundred, 0.1) - end - if self.metric then - self:Transmission(ATIS.Sound.Meters, 0.1) - else - self:Transmission(ATIS.Sound.Feet, 0.1) - end - alltext=alltext..";\n"..subtitle end @@ -1837,22 +2090,23 @@ function ATIS:onafterBroadcast(From, Event, To) subtitle=subtitle.." feet" end - -- Transmitt. - self:Transmission(ATIS.Sound.Elevation, 1.0, subtitle) - if tonumber(L1000)>0 then - self.radioqueue:Number2Transmission(L1000) - self:Transmission(ATIS.Sound.Thousand, 0.1) + -- Transmit. + if not self.useSRS then + self:Transmission(ATIS.Sound.Elevation, 1.0, subtitle) + if tonumber(L1000)>0 then + self.radioqueue:Number2Transmission(L1000) + self:Transmission(ATIS.Sound.Thousand, 0.1) + end + if tonumber(L0100)>0 then + self.radioqueue:Number2Transmission(L0100) + self:Transmission(ATIS.Sound.Hundred, 0.1) + end + if self.metric then + self:Transmission(ATIS.Sound.Meters, 0.1) + else + self:Transmission(ATIS.Sound.Feet, 0.1) + end end - if tonumber(L0100)>0 then - self.radioqueue:Number2Transmission(L0100) - self:Transmission(ATIS.Sound.Hundred, 0.1) - end - if self.metric then - self:Transmission(ATIS.Sound.Meters, 0.1) - else - self:Transmission(ATIS.Sound.Feet, 0.1) - end - alltext=alltext..";\n"..subtitle end @@ -1866,9 +2120,47 @@ function ATIS:onafterBroadcast(From, Event, To) end end subtitle=string.format("Tower frequency %s", freqs) - self:Transmission(ATIS.Sound.TowerFrequency, 1.0, subtitle) - for _,freq in pairs(self.towerfrequency) do - local f=string.format("%.3f", freq) + if not self.useSRS then + self:Transmission(ATIS.Sound.TowerFrequency, 1.0, subtitle) + for _,freq in pairs(self.towerfrequency) do + local f=string.format("%.3f", freq) + f=UTILS.Split(f, ".") + self.radioqueue:Number2Transmission(f[1], nil, 0.5) + if tonumber(f[2])>0 then + self:Transmission(ATIS.Sound.Decimal, 0.2) + self.radioqueue:Number2Transmission(f[2]) + end + self:Transmission(ATIS.Sound.MegaHertz, 0.2) + end + end + alltext=alltext..";\n"..subtitle + end + + -- ILS + local ils=self:GetNavPoint(self.ils, runway, rwyLeft) + if ils then + subtitle=string.format("ILS frequency %.2f MHz", ils.frequency) + if not self.useSRS then + self:Transmission(ATIS.Sound.ILSFrequency, 1.0, subtitle) + local f=string.format("%.2f", ils.frequency) + f=UTILS.Split(f, ".") + self.radioqueue:Number2Transmission(f[1], nil, 0.5) + if tonumber(f[2])>0 then + self:Transmission(ATIS.Sound.Decimal, 0.2) + self.radioqueue:Number2Transmission(f[2]) + end + self:Transmission(ATIS.Sound.MegaHertz, 0.2) + end + alltext=alltext..";\n"..subtitle + end + + -- Outer NDB + local ndb=self:GetNavPoint(self.ndbouter, runway, rwyLeft) + if ndb then + subtitle=string.format("Outer NDB frequency %.2f MHz", ndb.frequency) + if not self.useSRS then + self:Transmission(ATIS.Sound.OuterNDBFrequency, 1.0, subtitle) + local f=string.format("%.2f", ndb.frequency) f=UTILS.Split(f, ".") self.radioqueue:Number2Transmission(f[1], nil, 0.5) if tonumber(f[2])>0 then @@ -1877,41 +2169,6 @@ function ATIS:onafterBroadcast(From, Event, To) end self:Transmission(ATIS.Sound.MegaHertz, 0.2) end - - alltext=alltext..";\n"..subtitle - end - - -- ILS - local ils=self:GetNavPoint(self.ils, runway, rwyLeft) - if ils then - subtitle=string.format("ILS frequency %.2f MHz", ils.frequency) - self:Transmission(ATIS.Sound.ILSFrequency, 1.0, subtitle) - local f=string.format("%.2f", ils.frequency) - f=UTILS.Split(f, ".") - self.radioqueue:Number2Transmission(f[1], nil, 0.5) - if tonumber(f[2])>0 then - self:Transmission(ATIS.Sound.Decimal, 0.2) - self.radioqueue:Number2Transmission(f[2]) - end - self:Transmission(ATIS.Sound.MegaHertz, 0.2) - - alltext=alltext..";\n"..subtitle - end - - -- Outer NDB - local ndb=self:GetNavPoint(self.ndbouter, runway, rwyLeft) - if ndb then - subtitle=string.format("Outer NDB frequency %.2f MHz", ndb.frequency) - self:Transmission(ATIS.Sound.OuterNDBFrequency, 1.0, subtitle) - local f=string.format("%.2f", ndb.frequency) - f=UTILS.Split(f, ".") - self.radioqueue:Number2Transmission(f[1], nil, 0.5) - if tonumber(f[2])>0 then - self:Transmission(ATIS.Sound.Decimal, 0.2) - self.radioqueue:Number2Transmission(f[2]) - end - self:Transmission(ATIS.Sound.MegaHertz, 0.2) - alltext=alltext..";\n"..subtitle end @@ -1919,51 +2176,58 @@ function ATIS:onafterBroadcast(From, Event, To) local ndb=self:GetNavPoint(self.ndbinner, runway, rwyLeft) if ndb then subtitle=string.format("Inner NDB frequency %.2f MHz", ndb.frequency) - self:Transmission(ATIS.Sound.InnerNDBFrequency, 1.0, subtitle) - local f=string.format("%.2f", ndb.frequency) - f=UTILS.Split(f, ".") - self.radioqueue:Number2Transmission(f[1], nil, 0.5) - if tonumber(f[2])>0 then - self:Transmission(ATIS.Sound.Decimal, 0.2) - self.radioqueue:Number2Transmission(f[2]) - end - self:Transmission(ATIS.Sound.MegaHertz, 0.2) - + if not self.useSRS then + self:Transmission(ATIS.Sound.InnerNDBFrequency, 1.0, subtitle) + local f=string.format("%.2f", ndb.frequency) + f=UTILS.Split(f, ".") + self.radioqueue:Number2Transmission(f[1], nil, 0.5) + if tonumber(f[2])>0 then + self:Transmission(ATIS.Sound.Decimal, 0.2) + self.radioqueue:Number2Transmission(f[2]) + end + self:Transmission(ATIS.Sound.MegaHertz, 0.2) + end alltext=alltext..";\n"..subtitle end -- VOR if self.vor then subtitle=string.format("VOR frequency %.2f MHz", self.vor) - self:Transmission(ATIS.Sound.VORFrequency, 1.0, subtitle) - local f=string.format("%.2f", self.vor) - f=UTILS.Split(f, ".") - self.radioqueue:Number2Transmission(f[1], nil, 0.5) - if tonumber(f[2])>0 then - self:Transmission(ATIS.Sound.Decimal, 0.2) - self.radioqueue:Number2Transmission(f[2]) + if self.useSRS then + subtitle=string.format("V O R frequency %.2f MHz", self.vor) + end + if not self.useSRS then + self:Transmission(ATIS.Sound.VORFrequency, 1.0, subtitle) + local f=string.format("%.2f", self.vor) + f=UTILS.Split(f, ".") + self.radioqueue:Number2Transmission(f[1], nil, 0.5) + if tonumber(f[2])>0 then + self:Transmission(ATIS.Sound.Decimal, 0.2) + self.radioqueue:Number2Transmission(f[2]) + end + self:Transmission(ATIS.Sound.MegaHertz, 0.2) end - self:Transmission(ATIS.Sound.MegaHertz, 0.2) - alltext=alltext..";\n"..subtitle end -- TACAN if self.tacan then subtitle=string.format("TACAN channel %dX", self.tacan) - self:Transmission(ATIS.Sound.TACANChannel, 1.0, subtitle) - self.radioqueue:Number2Transmission(tostring(self.tacan), nil, 0.2) - self.radioqueue:NewTransmission("NATO Alphabet/Xray.ogg", 0.75, self.soundpath, nil, 0.2) - + if not self.useSRS then + self:Transmission(ATIS.Sound.TACANChannel, 1.0, subtitle) + self.radioqueue:Number2Transmission(tostring(self.tacan), nil, 0.2) + self.radioqueue:NewTransmission("NATO Alphabet/Xray.ogg", 0.75, self.soundpath, nil, 0.2) + end alltext=alltext..";\n"..subtitle end -- RSBN if self.rsbn then subtitle=string.format("RSBN channel %d", self.rsbn) - self:Transmission(ATIS.Sound.RSBNChannel, 1.0, subtitle) - self.radioqueue:Number2Transmission(tostring(self.rsbn), nil, 0.2) - + if not self.useSRS then + self:Transmission(ATIS.Sound.RSBNChannel, 1.0, subtitle) + self.radioqueue:Number2Transmission(tostring(self.rsbn), nil, 0.2) + end alltext=alltext..";\n"..subtitle end @@ -1971,17 +2235,19 @@ function ATIS:onafterBroadcast(From, Event, To) local ndb=self:GetNavPoint(self.prmg, runway, rwyLeft) if ndb then subtitle=string.format("PRMG channel %d", ndb.frequency) - self:Transmission(ATIS.Sound.PRMGChannel, 1.0, subtitle) - self.radioqueue:Number2Transmission(tostring(ndb.frequency), nil, 0.5) - + if not self.useSRS then + self:Transmission(ATIS.Sound.PRMGChannel, 1.0, subtitle) + self.radioqueue:Number2Transmission(tostring(ndb.frequency), nil, 0.5) + end alltext=alltext..";\n"..subtitle end -- Advice on initial... subtitle=string.format("Advise on initial contact, you have information %s", NATO) - self:Transmission(ATIS.Sound.AdviceOnInitial, 0.5, subtitle) - self.radioqueue:NewTransmission(string.format("NATO Alphabet/%s.ogg", NATO), 0.75, self.soundpath) - + if not self.useSRS then + self:Transmission(ATIS.Sound.AdviceOnInitial, 0.5, subtitle) + self.radioqueue:NewTransmission(string.format("NATO Alphabet/%s.ogg", NATO), 0.75, self.soundpath) + end alltext=alltext..";\n"..subtitle -- Report ATIS text. @@ -2002,6 +2268,62 @@ end -- @param #string Text Report text. function ATIS:onafterReport(From, Event, To, Text) self:T(self.lid..string.format("Report:\n%s", Text)) + + if self.useSRS and self.msrs then + + -- Remove line breaks + local text=string.gsub(Text, "[\r\n]", "") + + -- Replace other stuff. + local text=string.gsub(text, "SM", "statute miles") + local text=string.gsub(text, "°C", "degrees Celsius") + local text=string.gsub(text, "°F", "degrees Fahrenheit") + local text=string.gsub(text, "inHg", "inches of Mercury") + local text=string.gsub(text, "mmHg", "millimeters of Mercury") + local text=string.gsub(text, "hPa", "hecto Pascals") + local text=string.gsub(text, "m/s", "meters per second") + + -- Replace ";" by "." + local text=string.gsub(text, ";", " . ") + + --Debug output. + self:T("SRS TTS: "..text) + + -- Play text-to-speech report. + self.msrs:PlayText(text) + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Event Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Base captured +-- @param #ATIS self +-- @param Core.Event#EVENTDATA EventData Event data. +function ATIS:OnEventBaseCaptured(EventData) + + if EventData and EventData.Place then + + -- Place is the airbase that was captured. + local airbase=EventData.Place --Wrapper.Airbase#AIRBASE + + -- Check that this airbase belongs or did belong to this warehouse. + if EventData.PlaceName==self.airbasename then + + -- New coalition of airbase after it was captured. + local NewCoalitionAirbase=airbase:GetCoalition() + + if self.useSRS and self.msrs and self.msrs.coalition~=NewCoalitionAirbase then + self.msrs:SetCoalition(NewCoalitionAirbase) + end + + end + + end + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/AirWing.lua b/Moose Development/Moose/Ops/AirWing.lua index 0ea78cdc2..58e8ebb95 100644 --- a/Moose Development/Moose/Ops/AirWing.lua +++ b/Moose Development/Moose/Ops/AirWing.lua @@ -19,7 +19,7 @@ -- @field #table menu Table of menu items. -- @field #table squadrons Table of squadrons. -- @field #table missionqueue Mission queue table. --- @field #table payloads Playloads for specific aircraft and mission types. +-- @field #table payloads Playloads for specific aircraft and mission types. -- @field #number payloadcounter Running index of payloads. -- @field Core.Set#SET_ZONE zonesetCAP Set of CAP zones. -- @field Core.Set#SET_ZONE zonesetTANKER Set of TANKER zones. @@ -27,16 +27,17 @@ -- @field #number nflightsCAP Number of CAP flights constantly in the air. -- @field #number nflightsAWACS Number of AWACS flights constantly in the air. -- @field #number nflightsTANKERboom Number of TANKER flights with BOOM constantly in the air. --- @field #number nflightsTANKERprobe Number of TANKER flights with PROBE constantly in the air. +-- @field #number nflightsTANKERprobe Number of TANKER flights with PROBE constantly in the air. -- @field #number nflightsRescueHelo Number of Rescue helo flights constantly in the air. -- @field #table pointsCAP Table of CAP points. -- @field #table pointsTANKER Table of Tanker points. -- @field #table pointsAWACS Table of AWACS points. +-- @field #boolean markpoints Display markers on the F10 map. -- @field Ops.WingCommander#WINGCOMMANDER wingcommander The wing commander responsible for this airwing. --- +-- -- @field Ops.RescueHelo#RESCUEHELO rescuehelo The rescue helo. -- @field Ops.RecoveryTanker#RECOVERYTANKER recoverytanker The recoverytanker. --- +-- -- @extends Functional.Warehouse#WAREHOUSE --- Be surprised! @@ -46,62 +47,62 @@ -- ![Banner Image](..\Presentations\OPS\AirWing\_Main.png) -- -- # The AIRWING Concept --- +-- -- An AIRWING consists of multiple SQUADRONS. These squadrons "live" in a WAREHOUSE, i.e. a physical structure that is connected to an airbase (airdrome, FRAP or ship). -- For an airwing to be operational, it needs airframes, weapons/fuel and an airbase. --- +-- -- # Create an Airwing --- +-- -- ## Constructing the Airwing --- +-- -- airwing=AIRWING:New("Warehouse Batumi", "8th Fighter Wing") -- airwing:Start() --- +-- -- The first parameter specified the warehouse, i.e. the static building housing the airwing (or the name of the aircraft carrier). The second parameter is optional -- and sets an alias. --- +-- -- ## Adding Squadrons --- +-- -- At this point the airwing does not have any assets (aircraft). In order to add these, one needs to first define SQUADRONS. --- +-- -- VFA151=SQUADRON:New("F-14 Group", 8, "VFA-151 (Vigilantes)") -- VFA151:AddMissionCapability({AUFTRAG.Type.GCICAP, AUFTRAG.Type.INTERCEPT}) --- +-- -- airwing:AddSquadron(VFA151) --- +-- -- This adds eight Tomcat groups beloning to VFA-151 to the airwing. This squadron has the ability to perform combat air patrols and intercepts. --- +-- -- ## Adding Payloads --- +-- -- Adding pure airframes is not enough. The aircraft also need weapons (and fuel) for certain missions. These must be given to the airwing from template groups -- defined in the Mission Editor. --- +-- -- -- F-14 payloads for CAP and INTERCEPT. Phoenix are first, sparrows are second choice. -- airwing:NewPayload(GROUP:FindByName("F-14 Payload AIM-54C"), 2, {AUFTRAG.Type.INTERCEPT, AUFTRAG.Type.GCICAP}, 80) -- airwing:NewPayload(GROUP:FindByName("F-14 Payload AIM-7M"), 20, {AUFTRAG.Type.INTERCEPT, AUFTRAG.Type.GCICAP}) --- +-- -- This will add two AIM-54C and 20 AIM-7M payloads. --- +-- -- If the airwing gets an intercept or patrol mission assigned, it will first use the AIM-54s. Once these are consumed, the AIM-7s are attached to the aircraft. --- +-- -- When an airwing does not have a payload for a certain mission type, the mission cannot be carried out. --- +-- -- You can set the number of payloads to "unlimited" by setting its quantity to -1. --- +-- -- # Adding Missions --- +-- -- Various mission types can be added easily via the AUFTRAG class. --- +-- -- Once you created an AUFTRAG you can add it to the AIRWING with the :AddMission(mission) function. --- +-- -- This mission will be put into the AIRWING queue. Once the mission start time is reached and all resources (airframes and pylons) are available, the mission is started. -- If the mission stop time is over (and the mission is not finished), it will be cancelled and removed from the queue. This applies also to mission that were not even -- started. --- +-- -- # Command an Airwing --- +-- -- An airwing can receive missions from a WINGCOMMANDER. See docs of that class for details. --- +-- -- However, you are still free to add missions at anytime. -- -- @@ -119,6 +120,7 @@ AIRWING = { pointsTANKER = {}, pointsAWACS = {}, wingcommander = nil, + markpoints = false, } --- Squadron asset. @@ -147,12 +149,13 @@ AIRWING = { -- @field #number heading Heading in degrees. -- @field #number leg Leg length in NM. -- @field #number speed Speed in knots. +-- @field #number refuelsystem Refueling system type: `0=Unit.RefuelingSystem.BOOM_AND_RECEPTACLE`, `1=Unit.RefuelingSystem.PROBE_AND_DROGUE`. -- @field #number noccupied Number of flights on this patrol point. -- @field Wrapper.Marker#MARKER marker F10 marker. --- AIRWING class version. -- @field #string version -AIRWING.version="0.5.1" +AIRWING.version="0.7.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list @@ -196,9 +199,9 @@ function AIRWING:New(warehousename, airwingname) -- From State --> Event --> To State self:AddTransition("*", "MissionRequest", "*") -- Add a (mission) request to the warehouse. self:AddTransition("*", "MissionCancel", "*") -- Cancel mission. - + self:AddTransition("*", "SquadAssetReturned", "*") -- Flight was spawned with a mission. - + self:AddTransition("*", "FlightOnMission", "*") -- Flight was spawned with a mission. -- Defaults: @@ -209,6 +212,7 @@ function AIRWING:New(warehousename, airwingname) self.nflightsTANKERprobe=0 self.nflightsRecoveryTanker=0 self.nflightsRescueHelo=0 + self.markpoints=false ------------------------ --- Pseudo Functions --- @@ -231,6 +235,24 @@ function AIRWING:New(warehousename, airwingname) -- @param #AIRWING self -- @param #number delay Delay in seconds. + --- On after "FlightOnMission" event. Triggered when an asset group starts a mission. + -- @function [parent=#AIRWING] OnAfterFlightOnMission + -- @param #AIRWING self + -- @param #string From The From state + -- @param #string Event The Event called + -- @param #string To The To state + -- @param Ops.FlightGroup#FLIGHTGROUP Flightgroup The Flightgroup on mission + -- @param Ops.Auftrag#AUFTRAG Mission The Auftrag of the Flightgroup + + --- On after "AssetReturned" event. Triggered when an asset group returned to its airwing. + -- @function [parent=#AIRWING] OnAfterAssetReturned + -- @param #AIRWING self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Squadron#SQUADRON Squadron The asset squadron. + -- @param #AIRWING.SquadronAsset Asset The asset that returned. + return self end @@ -246,10 +268,10 @@ function AIRWING:AddSquadron(Squadron) -- Add squadron to airwing. table.insert(self.squadrons, Squadron) - + -- Add assets to squadron. self:AddAssetToSquadron(Squadron, Squadron.Ngroups) - + -- Tanker and AWACS get unlimited payloads. if Squadron.attribute==GROUP.Attribute.AIR_AWACS then self:NewPayload(Squadron.templategroup, -1, AUFTRAG.Type.AWACS) @@ -259,7 +281,7 @@ function AIRWING:AddSquadron(Squadron) -- Set airwing to squadron. Squadron:SetAirwing(self) - + -- Start squadron. if Squadron:IsStopped() then Squadron:Start() @@ -294,17 +316,17 @@ function AIRWING:NewPayload(Unit, Npayloads, MissionTypes, Performance) if Unit:IsInstanceOf("GROUP") then Unit=Unit:GetUnit(1) end - + -- Ensure Missiontypes is a table. if MissionTypes and type(MissionTypes)~="table" then MissionTypes={MissionTypes} end - + -- Create payload. local payload={} --#AIRWING.Payload payload.uid=self.payloadcounter payload.unitname=Unit:GetName() - payload.aircrafttype=Unit:GetTypeName() + payload.aircrafttype=Unit:GetTypeName() payload.pylons=Unit:GetTemplatePayload() payload.unlimited=Npayloads<0 if payload.unlimited then @@ -312,7 +334,7 @@ function AIRWING:NewPayload(Unit, Npayloads, MissionTypes, Performance) else payload.navail=Npayloads or 99 end - + payload.capabilities={} for _,missiontype in pairs(MissionTypes) do local capability={} --Ops.Auftrag#AUFTRAG.Capability @@ -320,27 +342,27 @@ function AIRWING:NewPayload(Unit, Npayloads, MissionTypes, Performance) capability.Performance=Performance table.insert(payload.capabilities, capability) end - - -- Add ORBIT for all. + + -- Add ORBIT for all. if not self:CheckMissionType(AUFTRAG.Type.ORBIT, MissionTypes) then local capability={} --Ops.Auftrag#AUFTRAG.Capability capability.MissionType=AUFTRAG.Type.ORBIT capability.Performance=50 table.insert(payload.capabilities, capability) - end - + end + -- Info - self:T(self.lid..string.format("Adding new payload from unit %s for aircraft type %s: ID=%d, N=%d (unlimited=%s), performance=%d, missions: %s", + self:T(self.lid..string.format("Adding new payload from unit %s for aircraft type %s: ID=%d, N=%d (unlimited=%s), performance=%d, missions: %s", payload.unitname, payload.aircrafttype, payload.uid, payload.navail, tostring(payload.unlimited), Performance, table.concat(MissionTypes, ", "))) -- Add payload table.insert(self.payloads, payload) - + -- Increase counter self.payloadcounter=self.payloadcounter+1 - + return payload - + end self:E(self.lid.."ERROR: No UNIT found to create PAYLOAD!") @@ -361,15 +383,15 @@ function AIRWING:AddPayloadCapability(Payload, MissionTypes, Performance) end Payload.capabilities=Payload.capabilities or {} - + for _,missiontype in pairs(MissionTypes) do - + local capability={} --Ops.Auftrag#AUFTRAG.Capability capability.MissionType=missiontype capability.Performance=Performance - + --TODO: check that capability does not already exist! - + table.insert(Payload.capabilities, capability) end @@ -390,7 +412,7 @@ function AIRWING:FetchPayloadFromStock(UnitType, MissionType, Payloads) self:T(self.lid.."WARNING: No payloads in stock!") return nil end - + -- Debug. if self.verbose>=4 then self:I(self.lid..string.format("Looking for payload for unit type=%s and mission type=%s", UnitType, MissionType)) @@ -403,7 +425,7 @@ function AIRWING:FetchPayloadFromStock(UnitType, MissionType, Payloads) --- Sort payload wrt the following criteria: -- 1) Highest performance is the main selection criterion. - -- 2) If payloads have the same performance, unlimited payloads are preferred over limited ones. + -- 2) If payloads have the same performance, unlimited payloads are preferred over limited ones. -- 3) If payloads have the same performance _and_ are limited, the more abundant one is preferred. local function sortpayloads(a,b) local pA=a --#AIRWING.Payload @@ -436,7 +458,7 @@ function AIRWING:FetchPayloadFromStock(UnitType, MissionType, Payloads) return nil end return false - end + end -- Pre-selection: filter out only those payloads that are valid for the airframe and mission type and are available. local payloads={} @@ -445,14 +467,14 @@ function AIRWING:FetchPayloadFromStock(UnitType, MissionType, Payloads) local specialpayload=_checkPayloads(payload) local compatible=self:CheckMissionCapability(MissionType, payload.capabilities) - + local goforit = specialpayload or (specialpayload==nil and compatible) if payload.aircrafttype==UnitType and payload.navail>0 and goforit then table.insert(payloads, payload) end end - + -- Debug. if self.verbose>=4 then self:I(self.lid..string.format("Sorted payloads for mission type X and aircraft type=Y:")) @@ -464,7 +486,7 @@ function AIRWING:FetchPayloadFromStock(UnitType, MissionType, Payloads) end end end - + -- Cases: if #payloads==0 then -- No payload available. @@ -483,10 +505,10 @@ function AIRWING:FetchPayloadFromStock(UnitType, MissionType, Payloads) local payload=payloads[1] --#AIRWING.Payload if not payload.unlimited then payload.navail=payload.navail-1 - end + end return payload end - + end --- Return payload from asset back to stock. @@ -495,9 +517,9 @@ end function AIRWING:ReturnPayloadFromAsset(asset) local payload=asset.payload - + if payload then - + -- Increase count if not unlimited. if not payload.unlimited then payload.navail=payload.navail+1 @@ -505,11 +527,11 @@ function AIRWING:ReturnPayloadFromAsset(asset) -- Remove asset payload. asset.payload=nil - + else self:E(self.lid.."ERROR: asset had no payload attached!") end - + end @@ -521,23 +543,23 @@ end function AIRWING:AddAssetToSquadron(Squadron, Nassets) if Squadron then - + -- Get the template group of the squadron. local Group=GROUP:FindByName(Squadron.templatename) - + if Group then - + -- Debug text. local text=string.format("Adding asset %s to squadron %s", Group:GetName(), Squadron.name) self:T(self.lid..text) - + -- Add assets to airwing warehouse. self:AddAsset(Group, Nassets, nil, nil, nil, nil, Squadron.skill, Squadron.livery, Squadron.name) - + else self:E(self.lid.."ERROR: Group does not exist!") end - + else self:E(self.lid.."ERROR: Squadron does not exit!") end @@ -553,11 +575,11 @@ function AIRWING:GetSquadron(SquadronName) for _,_squadron in pairs(self.squadrons) do local squadron=_squadron --Ops.Squadron#SQUADRON - + if squadron.name==SquadronName then return squadron end - + end return nil @@ -590,23 +612,23 @@ function AIRWING:RemoveAssetFromSquadron(Asset) end end ---- Add mission to queue. +--- Add a mission for the airwing. The airwing will pick the best available assets for the mission and lauch it when ready. -- @param #AIRWING self --- @param Ops.Auftrag#AUFTRAG Mission for this group. +-- @param Ops.Auftrag#AUFTRAG Mission Mission for this airwing. -- @return #AIRWING self function AIRWING:AddMission(Mission) - + -- Set status to QUEUED. This also attaches the airwing to this mission. Mission:Queued(self) - + -- Add mission to queue. table.insert(self.missionqueue, Mission) - + -- Info text. - local text=string.format("Added mission %s (type=%s). Starting at %s. Stopping at %s", + local text=string.format("Added mission %s (type=%s). Starting at %s. Stopping at %s", tostring(Mission.name), tostring(Mission.type), UTILS.SecondsToClock(Mission.Tstart, true), Mission.Tstop and UTILS.SecondsToClock(Mission.Tstop, true) or "INF") self:T(self.lid..text) - + return self end @@ -618,12 +640,12 @@ function AIRWING:RemoveMission(Mission) for i,_mission in pairs(self.missionqueue) do local mission=_mission --Ops.Auftrag#AUFTRAG - + if mission.auftragsnummer==Mission.auftragsnummer then table.remove(self.missionqueue, i) break end - + end return self @@ -647,6 +669,19 @@ function AIRWING:SetNumberTankerBoom(Nboom) return self end +--- Set markers on the map for Patrol Points. +-- @param #AIRWING self +-- @param #boolean onoff Set to true to switch markers on. +-- @return #AIRWING self +function AIRWING:ShowPatrolPointMarkers(onoff) + if onoff then + self.markpoints = true + else + self.markpoints = false + end + return self +end + --- Set number of TANKER flights with Probe constantly in the air. -- @param #AIRWING self -- @param #number Nprobe Number of flights. Default 1. @@ -674,13 +709,13 @@ function AIRWING:SetNumberRescuehelo(n) return self end ---- +--- -- @param #AIRWING self -- @param #AIRWING.PatrolData point Patrol point table. -- @return #string Marker text. function AIRWING:_PatrolPointMarkerText(point) - local text=string.format("%s Occupied=%d, \nheading=%03d, leg=%d NM, alt=%d ft, speed=%d kts", + local text=string.format("%s Occupied=%d, \nheading=%03d, leg=%d NM, alt=%d ft, speed=%d kts", point.type, point.noccupied, point.heading, point.leg, point.altitude, point.speed) return text @@ -688,13 +723,13 @@ end --- Update marker of the patrol point. -- @param #AIRWING.PatrolData point Patrol point table. -function AIRWING.UpdatePatrolPointMarker(point) - - local text=string.format("%s Occupied=%d\nheading=%03d, leg=%d NM, alt=%d ft, speed=%d kts", - point.type, point.noccupied, point.heading, point.leg, point.altitude, point.speed) - - point.marker:UpdateText(text, 1) +function AIRWING:UpdatePatrolPointMarker(point) + if self.markpoints then -- sometimes there's a direct call from #OPSGROUP + local text=string.format("%s Occupied=%d\nheading=%03d, leg=%d NM, alt=%d ft, speed=%d kts", + point.type, point.noccupied, point.heading, point.leg, point.altitude, point.speed) + point.marker:UpdateText(text, 1) + end end @@ -706,8 +741,14 @@ end -- @param #number Heading Heading in degrees. Default random (0, 360] degrees. -- @param #number LegLength Length of race-track orbit in NM. Default 15 NM. -- @param #number Speed Orbit speed in knots. Default 350 knots. +-- @param #number RefuelSystem Refueling system: 0=Boom, 1=Probe. Default nil=any. -- @return #AIRWING.PatrolData Patrol point table. -function AIRWING:NewPatrolPoint(Type, Coordinate, Altitude, Speed, Heading, LegLength) +function AIRWING:NewPatrolPoint(Type, Coordinate, Altitude, Speed, Heading, LegLength, RefuelSystem) + + -- Check if a zone was passed instead of a coordinate. + if Coordinate:IsInstanceOf("ZONE_BASE") then + Coordinate=Coordinate:GetCoordinate() + end local patrolpoint={} --#AIRWING.PatrolData patrolpoint.type=Type or "Unknown" @@ -717,9 +758,12 @@ function AIRWING:NewPatrolPoint(Type, Coordinate, Altitude, Speed, Heading, LegL patrolpoint.altitude=Altitude or math.random(10,20)*1000 patrolpoint.speed=Speed or 350 patrolpoint.noccupied=0 - patrolpoint.marker=MARKER:New(Coordinate, "New Patrol Point"):ToAll() - - AIRWING.UpdatePatrolPointMarker(patrolpoint) + patrolpoint.refuelsystem=RefuelSystem + + if self.markpoints then + patrolpoint.marker=MARKER:New(Coordinate, "New Patrol Point"):ToAll() + AIRWING.UpdatePatrolPointMarker(patrolpoint) + end return patrolpoint end @@ -733,7 +777,7 @@ end -- @param #number LegLength Length of race-track orbit in NM. -- @return #AIRWING self function AIRWING:AddPatrolPointCAP(Coordinate, Altitude, Speed, Heading, LegLength) - + local patrolpoint=self:NewPatrolPoint("CAP", Coordinate, Altitude, Speed, Heading, LegLength) table.insert(self.pointsCAP, patrolpoint) @@ -748,10 +792,11 @@ end -- @param #number Speed Orbit speed in knots. -- @param #number Heading Heading in degrees. -- @param #number LegLength Length of race-track orbit in NM. +-- @param #number RefuelSystem Set refueling system of tanker: 0=boom, 1=probe. Default any (=nil). -- @return #AIRWING self -function AIRWING:AddPatrolPointTANKER(Coordinate, Altitude, Speed, Heading, LegLength) - - local patrolpoint=self:NewPatrolPoint("Tanker", Coordinate, Altitude, Speed, Heading, LegLength) +function AIRWING:AddPatrolPointTANKER(Coordinate, Altitude, Speed, Heading, LegLength, RefuelSystem) + + local patrolpoint=self:NewPatrolPoint("Tanker", Coordinate, Altitude, Speed, Heading, LegLength, RefuelSystem) table.insert(self.pointsTANKER, patrolpoint) @@ -767,7 +812,7 @@ end -- @param #number LegLength Length of race-track orbit in NM. -- @return #AIRWING self function AIRWING:AddPatrolPointAWACS(Coordinate, Altitude, Speed, Heading, LegLength) - + local patrolpoint=self:NewPatrolPoint("AWACS", Coordinate, Altitude, Speed, Heading, LegLength) table.insert(self.pointsAWACS, patrolpoint) @@ -799,39 +844,39 @@ function AIRWING:onafterStatus(From, Event, To) self:GetParent(self).onafterStatus(self, From, Event, To) local fsmstate=self:GetState() - + -- Check CAP missions. self:CheckCAP() - + -- Check TANKER missions. self:CheckTANKER() - + -- Check AWACS missions. self:CheckAWACS() - + -- Check Rescue Helo missions. self:CheckRescuhelo() - - + + -- General info: if self.verbose>=1 then -- Count missions not over yet. local Nmissions=self:CountMissionsInQueue() - + -- Count ALL payloads in stock. If any payload is unlimited, this gives 999. local Npayloads=self:CountPayloadsInStock(AUFTRAG.Type) - + -- Assets tot local Npq, Np, Nq=self:CountAssetsOnMission() - + local assets=string.format("%d (OnMission: Total=%d, Active=%d, Queued=%d)", self:CountAssets(), Npq, Np, Nq) -- Output. local text=string.format("%s: Missions=%d, Payloads=%d (%d), Squads=%d, Assets=%s", fsmstate, Nmissions, Npayloads, #self.payloads, #self.squadrons, assets) self:I(self.lid..text) end - + ------------------ -- Mission Info -- ------------------ @@ -839,16 +884,16 @@ function AIRWING:onafterStatus(From, Event, To) local text=string.format("Missions Total=%d:", #self.missionqueue) for i,_mission in pairs(self.missionqueue) do local mission=_mission --Ops.Auftrag#AUFTRAG - + local prio=string.format("%d/%s", mission.prio, tostring(mission.importance)) ; if mission.urgent then prio=prio.." (!)" end local assets=string.format("%d/%d", mission:CountOpsGroups(), mission.nassets) local target=string.format("%d/%d Damage=%.1f", mission:CountMissionTargets(), mission:GetTargetInitialNumber(), mission:GetTargetDamage()) - + text=text..string.format("\n[%d] %s %s: Status=%s, Prio=%s, Assets=%s, Targets=%s", i, mission.name, mission.type, mission.status, prio, assets, target) end self:I(self.lid..text) end - + ------------------- -- Squadron Info -- ------------------- @@ -856,17 +901,17 @@ function AIRWING:onafterStatus(From, Event, To) local text="Squadrons:" for i,_squadron in pairs(self.squadrons) do local squadron=_squadron --Ops.Squadron#SQUADRON - + local callsign=squadron.callsignName and UTILS.GetCallsignName(squadron.callsignName) or "N/A" local modex=squadron.modex and squadron.modex or -1 local skill=squadron.skill and tostring(squadron.skill) or "N/A" - + -- Squadron text text=text..string.format("\n* %s %s: %s*%d/%d, Callsign=%s, Modex=%d, Skill=%s", squadron.name, squadron:GetState(), squadron.aircrafttype, squadron:CountAssetsInStock(), #squadron.assets, callsign, modex, skill) end self:I(self.lid..text) end - + -------------- -- Mission --- -------------- @@ -877,18 +922,19 @@ function AIRWING:onafterStatus(From, Event, To) -- Get next mission. local mission=self:_GetNextMission() - -- Request mission execution. + -- Request mission execution. if mission then self:MissionRequest(mission) end end ---- Get patrol data +--- Get patrol data. -- @param #AIRWING self -- @param #table PatrolPoints Patrol data points. --- @return #AIRWING.PatrolData -function AIRWING:_GetPatrolData(PatrolPoints) +-- @param #number RefuelSystem If provided, only return points with the specific refueling system. +-- @return #AIRWING.PatrolData Patrol point data table. +function AIRWING:_GetPatrolData(PatrolPoints, RefuelSystem) -- Sort wrt lowest number of flights on this point. local function sort(a,b) @@ -896,17 +942,21 @@ function AIRWING:_GetPatrolData(PatrolPoints) end if PatrolPoints and #PatrolPoints>0 then - + -- Sort data wrt number of flights at that point. table.sort(PatrolPoints, sort) - return PatrolPoints[1] - else - - return self:NewPatrolPoint() - + for _,_patrolpoint in pairs(PatrolPoints) do + local patrolpoint=_patrolpoint --#AIRWING.PatrolData + if (RefuelSystem and patrolpoint.refuelsystem and RefuelSystem==patrolpoint.refuelsystem) or RefuelSystem==nil or patrolpoint.refuelsystem==nil then + return patrolpoint + end + end + end - + + -- Return a new point. + return self:NewPatrolPoint() end --- Check how many CAP missions are assigned and add number of missing missions. @@ -915,25 +965,25 @@ end function AIRWING:CheckCAP() local Ncap=self:CountMissionsInQueue({AUFTRAG.Type.GCICAP, AUFTRAG.Type.INTERCEPT}) - + for i=1,self.nflightsCAP-Ncap do - + local patrol=self:_GetPatrolData(self.pointsCAP) - + local altitude=patrol.altitude+1000*patrol.noccupied - + local missionCAP=AUFTRAG:NewGCICAP(patrol.coord, altitude, patrol.speed, patrol.heading, patrol.leg) - + missionCAP.patroldata=patrol - + patrol.noccupied=patrol.noccupied+1 - - AIRWING.UpdatePatrolPointMarker(patrol) - + + if self.markpoints then AIRWING.UpdatePatrolPointMarker(patrol) end + self:AddMission(missionCAP) - + end - + return self end @@ -944,58 +994,60 @@ function AIRWING:CheckTANKER() local Nboom=0 local Nprob=0 - - -- Count tanker mission. + + -- Count tanker missions. for _,_mission in pairs(self.missionqueue) do local mission=_mission --Ops.Auftrag#AUFTRAG - - if mission:IsNotOver() and mission.type==AUFTRAG.Type.TANKER then - if mission.refuelSystem==0 then + + if mission:IsNotOver() and mission.type==AUFTRAG.Type.TANKER and mission.patroldata then + if mission.refuelSystem==Unit.RefuelingSystem.BOOM_AND_RECEPTACLE then Nboom=Nboom+1 - elseif mission.refuelSystem==1 then + elseif mission.refuelSystem==Unit.RefuelingSystem.PROBE_AND_DROGUE then Nprob=Nprob+1 end - + end - + end - + + -- Check missing boom tankers. for i=1,self.nflightsTANKERboom-Nboom do - + local patrol=self:_GetPatrolData(self.pointsTANKER) - + local altitude=patrol.altitude+1000*patrol.noccupied - - local mission=AUFTRAG:NewTANKER(patrol.coord, altitude, patrol.speed, patrol.heading, patrol.leg, 0) - + + local mission=AUFTRAG:NewTANKER(patrol.coord, altitude, patrol.speed, patrol.heading, patrol.leg, Unit.RefuelingSystem.BOOM_AND_RECEPTACLE) + mission.patroldata=patrol - + patrol.noccupied=patrol.noccupied+1 - - AIRWING.UpdatePatrolPointMarker(patrol) - + + if self.markpoints then AIRWING.UpdatePatrolPointMarker(patrol) end + self:AddMission(mission) - + end - + + -- Check missing probe tankers. for i=1,self.nflightsTANKERprobe-Nprob do - + local patrol=self:_GetPatrolData(self.pointsTANKER) - + local altitude=patrol.altitude+1000*patrol.noccupied - - local mission=AUFTRAG:NewTANKER(patrol.coord, altitude, patrol.speed, patrol.heading, patrol.leg, 1) - + + local mission=AUFTRAG:NewTANKER(patrol.coord, altitude, patrol.speed, patrol.heading, patrol.leg, Unit.RefuelingSystem.PROBE_AND_DROGUE) + mission.patroldata=patrol - + patrol.noccupied=patrol.noccupied+1 - - AIRWING.UpdatePatrolPointMarker(patrol) - + + if self.markpoints then AIRWING.UpdatePatrolPointMarker(patrol) end + self:AddMission(mission) - - end - + + end + return self end @@ -1005,25 +1057,25 @@ end function AIRWING:CheckAWACS() local N=self:CountMissionsInQueue({AUFTRAG.Type.AWACS}) - + for i=1,self.nflightsAWACS-N do - + local patrol=self:_GetPatrolData(self.pointsAWACS) - + local altitude=patrol.altitude+1000*patrol.noccupied - + local mission=AUFTRAG:NewAWACS(patrol.coord, altitude, patrol.speed, patrol.heading, patrol.leg) - + mission.patroldata=patrol - + patrol.noccupied=patrol.noccupied+1 - - AIRWING.UpdatePatrolPointMarker(patrol) - + + if self.markpoints then AIRWING.UpdatePatrolPointMarker(patrol) end + self:AddMission(mission) - + end - + return self end @@ -1033,19 +1085,19 @@ end function AIRWING:CheckRescuhelo() local N=self:CountMissionsInQueue({AUFTRAG.Type.RESCUEHELO}) - + local name=self.airbase:GetName() - + local carrier=UNIT:FindByName(name) - + for i=1,self.nflightsRescueHelo-N do - + local mission=AUFTRAG:NewRESCUEHELO(carrier) - + self:AddMission(mission) - + end - + return self end @@ -1056,32 +1108,32 @@ end function AIRWING:GetTankerForFlight(flightgroup) local tankers=self:GetAssetsOnMission(AUFTRAG.Type.TANKER) - + if #tankers>0 then - + local tankeropt={} for _,_tanker in pairs(tankers) do local tanker=_tanker --#AIRWING.SquadronAsset - + -- Check that donor and acceptor use the same refuelling system. if flightgroup.refueltype and flightgroup.refueltype==tanker.flightgroup.tankertype then - + local tankercoord=tanker.flightgroup.group:GetCoordinate() local assetcoord=flightgroup.group:GetCoordinate() - + local dist=assetcoord:Get2DDistance(tankercoord) - + -- Ensure that the flight does not find itself. Asset could be a tanker! if dist>5 then table.insert(tankeropt, {tanker=tanker, dist=dist}) end - + end end - + -- Sort tankers wrt to distance. table.sort(tankeropt, function(a,b) return a.dist0 then return tankeropt[1].tanker @@ -1101,12 +1153,12 @@ function AIRWING:_CheckMissions() -- Loop over missions in queue. for _,_mission in pairs(self.missionqueue) do local mission=_mission --Ops.Auftrag#AUFTRAG - - if mission:IsNotOver() and mission:IsReadyToCancel() then + + if mission:IsNotOver() and mission:IsReadyToCancel() then mission:Cancel() end end - + end --- Get next mission. -- @param #AIRWING self @@ -1128,41 +1180,41 @@ function AIRWING:_GetNextMission() return (taskA.prio0 then self:E(self.lid..string.format("ERROR: mission %s of type %s has already assets attached!", mission.name, mission.type)) end mission.assets={} - + -- Assign assets to mission. for i=1,mission.nassets do local asset=assets[i] --#AIRWING.SquadronAsset - + -- Should not happen as we just checked! if not asset.payload then self:E(self.lid.."ERROR: No payload for asset! This should not happen!") end - + -- Add asset to mission. mission:AddAsset(asset) end - + -- Now return the remaining payloads. for i=mission.nassets+1,#assets do local asset=assets[i] --#AIRWING.SquadronAsset @@ -1223,7 +1275,7 @@ function AIRWING:_GetNextMission() end end end - + return mission end @@ -1242,7 +1294,7 @@ end function AIRWING:CalculateAssetMissionScore(asset, Mission, includePayload) local score=0 - + -- Prefer highly skilled assets. if asset.skill==AI.Skill.AVERAGE then score=score+0 @@ -1253,17 +1305,17 @@ function AIRWING:CalculateAssetMissionScore(asset, Mission, includePayload) elseif asset.skill==AI.Skill.EXCELLENT then score=score+30 end - + -- Add mission performance to score. local squad=self:GetSquadronOfAsset(asset) local missionperformance=squad:GetMissionPeformance(Mission.type) score=score+missionperformance - + -- Add payload performance to score. if includePayload and asset.payload then score=score+self:GetPayloadPeformance(asset.payload, Mission.type) end - + -- Intercepts need to be carried out quickly. We prefer spawned assets. if Mission.type==AUFTRAG.Type.INTERCEPT then if asset.spawned then @@ -1271,13 +1323,13 @@ function AIRWING:CalculateAssetMissionScore(asset, Mission, includePayload) score=score+25 end end - + -- TODO: This could be vastly improved. Need to gather ideas during testing. -- Calculate ETA? Assets on orbit missions should arrive faster even if they are further away. -- Max speed of assets. -- Fuel amount? - -- Range of assets? - + -- Range of assets? + return score end @@ -1288,51 +1340,54 @@ end -- @param #boolean includePayload If true, include the payload in the calulation if the asset has one attached. function AIRWING:_OptimizeAssetSelection(assets, Mission, includePayload) - local TargetCoordinate=Mission:GetTargetCoordinate() + local TargetVec2=Mission:GetTargetVec2() + + --local dStock=self:GetCoordinate():Get2DDistance(TargetCoordinate) + + local dStock=UTILS.VecDist2D(TargetVec2, self:GetVec2()) - local dStock=self:GetCoordinate():Get2DDistance(TargetCoordinate) - -- Calculate distance to mission target. local distmin=math.huge local distmax=0 for _,_asset in pairs(assets) do local asset=_asset --#AIRWING.SquadronAsset - + if asset.spawned then local group=GROUP:FindByName(asset.spawngroupname) - asset.dist=group:GetCoordinate():Get2DDistance(TargetCoordinate) + --asset.dist=group:GetCoordinate():Get2DDistance(TargetCoordinate) + asset.dist=UTILS.VecDist2D(group:GetVec2(), TargetVec2) else asset.dist=dStock end - + if asset.distdistmax then distmax=asset.dist end - + end - + -- Calculate the mission score of all assets. for _,_asset in pairs(assets) do local asset=_asset --#AIRWING.SquadronAsset --self:I(string.format("FF asset %s has payload %s", asset.spawngroupname, asset.payload and "yes" or "no!")) asset.score=self:CalculateAssetMissionScore(asset, Mission, includePayload) end - - --- Sort assets wrt to their mission score. Higher is better. + + --- Sort assets wrt to their mission score. Higher is better. local function optimize(a, b) local assetA=a --#AIRWING.SquadronAsset local assetB=b --#AIRWING.SquadronAsset - + -- Higher score wins. If equal score ==> closer wins. -- TODO: Need to include the distance in a smarter way! return (assetA.score>assetB.score) or (assetA.score==assetB.score and assetA.dist0 then - + --local text=string.format("Requesting assets for mission %s:", Mission.name) for i,_asset in pairs(Assetlist) do local asset=_asset --#AIRWING.SquadronAsset - + -- Set asset to requested! Important so that new requests do not use this asset! asset.requested=true - + if Mission.missionTask then asset.missionTask=Mission.missionTask end - + end - + -- Add request to airwing warehouse. -- TODO: better Assignment string. self:AddRequest(self, WAREHOUSE.Descriptor.ASSETLIST, Assetlist, #Assetlist, nil, nil, Mission.prio, tostring(Mission.auftragsnummer)) - + -- The queueid has been increased in the onafterAddRequest function. So we can simply use it here. Mission.requestID=self.queueid end @@ -1424,36 +1479,38 @@ end -- @param #string To To state. -- @param Ops.Auftrag#AUFTRAG Mission The mission to be cancelled. function AIRWING:onafterMissionCancel(From, Event, To, Mission) - + -- Info message. self:I(self.lid..string.format("Cancel mission %s", Mission.name)) - - if Mission:IsPlanned() or Mission:IsQueued() or Mission:IsRequested() then - + + local Ngroups = Mission:CountOpsGroups() + + if Mission:IsPlanned() or Mission:IsQueued() or Mission:IsRequested() or Ngroups == 0 then + Mission:Done() - + else - + for _,_asset in pairs(Mission.assets) do local asset=_asset --#AIRWING.SquadronAsset - + local flightgroup=asset.flightgroup - + if flightgroup then flightgroup:MissionCancel(Mission) end - + -- Not requested any more (if it was). asset.requested=nil end - + end - + -- Remove queued request (if any). if Mission.requestID then self:_DeleteQueueItemByID(Mission.requestID, self.queue) end - + end --- On after "NewAsset" event. Asset is added to the given squadron (asset assignment). @@ -1467,11 +1524,11 @@ function AIRWING:onafterNewAsset(From, Event, To, asset, assignment) -- Call parent warehouse function first. self:GetParent(self).onafterNewAsset(self, From, Event, To, asset, assignment) - + -- Debug text. local text=string.format("New asset %s with assignment %s and request assignment %s", asset.spawngroupname, tostring(asset.assignment), tostring(assignment)) self:T3(self.lid..text) - + -- Get squadron. local squad=self:GetSquadron(asset.assignment) @@ -1479,58 +1536,64 @@ function AIRWING:onafterNewAsset(From, Event, To, asset, assignment) if squad then if asset.assignment==assignment then - + local nunits=#asset.template.units - + -- Debug text. local text=string.format("Adding asset to squadron %s: assignment=%s, type=%s, attribute=%s, nunits=%d %s", squad.name, assignment, asset.unittype, asset.attribute, nunits, tostring(squad.ngrouping)) self:T(self.lid..text) - + -- Adjust number of elements in the group. if squad.ngrouping then local template=asset.template - + local N=math.max(#template.units, squad.ngrouping) - + -- Handle units. for i=1,N do - + -- Unit template. local unit = template.units[i] - - -- If grouping is larger than units present, copy first unit. + + -- If grouping is larger than units present, copy first unit. if i>nunits then table.insert(template.units, UTILS.DeepCopy(template.units[1])) end - + -- Remove units if original template contains more than in grouping. if squad.ngroupingnunits then unit=nil end end - + asset.nunits=squad.ngrouping end + -- Set takeoff type. + asset.takeoffType=squad.takeoffType + + -- Set parking IDs. + asset.parkingIDs=squad.parkingIDs + -- Create callsign and modex (needs to be after grouping). squad:GetCallsign(asset) squad:GetModex(asset) - + -- Set spawn group name. This has to include "AID-" for warehouse. asset.spawngroupname=string.format("%s_AID-%d", squad.name, asset.uid) -- Add asset to squadron. squad:AddAsset(asset) - + -- TODO --asset.terminalType=AIRBASE.TerminalType.OpenBig else - + --env.info("FF squad asset returned") self:SquadAssetReturned(squad, asset) - + end - + end end @@ -1544,26 +1607,26 @@ end function AIRWING:onafterSquadAssetReturned(From, Event, To, Squadron, Asset) -- Debug message. self:T(self.lid..string.format("Asset %s from squadron %s returned! asset.assignment=\"%s\"", Asset.spawngroupname, Squadron.name, tostring(Asset.assignment))) - + -- Stop flightgroup. if Asset.flightgroup and not Asset.flightgroup:IsStopped() then Asset.flightgroup:Stop() end - + -- Return payload. self:ReturnPayloadFromAsset(Asset) - + -- Return tacan channel. if Asset.tacan then Squadron:ReturnTacan(Asset.tacan) end - + -- Set timestamp. Asset.Treturned=timer.getAbsTime() end ---- On after "AssetSpawned" event triggered when an asset group is spawned into the cruel world. +--- On after "AssetSpawned" event triggered when an asset group is spawned into the cruel world. -- Creates a new flightgroup element and adds the mission to the flightgroup queue. -- @param #AIRWING self -- @param #string From From state. @@ -1577,85 +1640,89 @@ function AIRWING:onafterAssetSpawned(From, Event, To, group, asset, request) -- Call parent warehouse function first. self:GetParent(self).onafterAssetSpawned(self, From, Event, To, group, asset, request) - -- Create a flight group. - local flightgroup=self:_CreateFlightGroup(asset) - - - --- - -- Asset - --- - - -- Set asset flightgroup. - asset.flightgroup=flightgroup - - -- Not requested any more. - asset.requested=nil - - -- Did not return yet. - asset.Treturned=nil - - --- - -- Squadron - --- - -- Get the SQUADRON of the asset. local squadron=self:GetSquadronOfAsset(asset) - - -- Get TACAN channel. - local Tacan=squadron:FetchTacan() - if Tacan then - asset.tacan=Tacan - end - - -- Set radio frequency and modulation - local radioFreq, radioModu=squadron:GetRadio() - if radioFreq then - flightgroup:SwitchRadio(radioFreq, radioModu) - end - - if squadron.fuellow then - flightgroup:SetFuelLowThreshold(squadron.fuellow) - end - - if squadron.fuellowRefuel then - flightgroup:SetFuelLowRefuel(squadron.fuellowRefuel) - end - --- - -- Mission - --- - - -- Get Mission (if any). - local mission=self:GetMissionByID(request.assignment) + -- Check if we have a squadron or if this was some other request. + if squadron then - -- Add mission to flightgroup queue. - if mission then - + -- Create a flight group. + local flightgroup=self:_CreateFlightGroup(asset) + + --- + -- Asset + --- + + -- Set asset flightgroup. + asset.flightgroup=flightgroup + + -- Not requested any more. + asset.requested=nil + + -- Did not return yet. + asset.Treturned=nil + + --- + -- Squadron + --- + + -- Get TACAN channel. + local Tacan=squadron:FetchTacan() if Tacan then - mission:SetTACAN(Tacan, Morse, UnitName, Band) + asset.tacan=Tacan + --flightgroup:SetDefaultTACAN(Tacan,Morse,UnitName,Band,OffSwitch) + flightgroup:SwitchTACAN(Tacan, Morse, UnitName, Band) end - + + -- Set radio frequency and modulation + local radioFreq, radioModu=squadron:GetRadio() + if radioFreq then + flightgroup:SwitchRadio(radioFreq, radioModu) + end + + if squadron.fuellow then + flightgroup:SetFuelLowThreshold(squadron.fuellow) + end + + if squadron.fuellowRefuel then + flightgroup:SetFuelLowRefuel(squadron.fuellowRefuel) + end + + --- + -- Mission + --- + + -- Get Mission (if any). + local mission=self:GetMissionByID(request.assignment) + -- Add mission to flightgroup queue. - asset.flightgroup:AddMission(mission) - - -- Trigger event. - self:FlightOnMission(flightgroup, mission) - - else - - if Tacan then - flightgroup:SwitchTACAN(Tacan, Morse, UnitName, Band) + if mission then + + if Tacan then + --mission:SetTACAN(Tacan, Morse, UnitName, Band) + end + + -- Add mission to flightgroup queue. + asset.flightgroup:AddMission(mission) + + -- Trigger event. + self:__FlightOnMission(5, flightgroup, mission) + + else + + if Tacan then + --flightgroup:SwitchTACAN(Tacan, Morse, UnitName, Band) + end + end - + + -- Add group to the detection set of the WINGCOMMANDER. + if self.wingcommander and self.wingcommander.chief then + self.wingcommander.chief.detectionset:AddGroup(asset.flightgroup.group) + end + end - - - - -- Add group to the detection set of the WINGCOMMANDER. - if self.wingcommander and self.wingcommander.chief then - self.wingcommander.chief.detectionset:AddGroup(asset.flightgroup.group) - end - + end --- On after "AssetDead" event triggered when an asset group died. @@ -1674,7 +1741,7 @@ function AIRWING:onafterAssetDead(From, Event, To, asset, request) if self.wingcommander and self.wingcommander.chief then self.wingcommander.chief.detectionset:RemoveGroupsByName({asset.spawngroupname}) end - + -- Remove asset from mission is done via Mission:AssetDead() call from flightgroup onafterFlightDead function -- Remove asset from squadron same end @@ -1686,6 +1753,7 @@ end -- @param #string To To state. function AIRWING:onafterDestroyed(From, Event, To) + -- Debug message. self:I(self.lid.."Airwing warehouse destroyed!") -- Cancel all missions. @@ -1717,22 +1785,22 @@ function AIRWING:onafterRequest(From, Event, To, Request) -- Assets local assets=Request.cargoassets - + -- Get Mission local Mission=self:GetMissionByID(Request.assignment) - + if Mission and assets then - + for _,_asset in pairs(assets) do - local asset=_asset --#AIRWING.SquadronAsset + local asset=_asset --#AIRWING.SquadronAsset -- This would be the place to modify the asset table before the asset is spawned. end - + end -- Call parent warehouse function after assets have been adjusted. self:GetParent(self).onafterRequest(self, From, Event, To, Request) - + end --- On after "SelfRequest" event. @@ -1749,13 +1817,13 @@ function AIRWING:onafterSelfRequest(From, Event, To, groupset, request) -- Get Mission local mission=self:GetMissionByID(request.assignment) - + for _,_asset in pairs(request.assets) do local asset=_asset --#AIRWING.SquadronAsset end - + for _,_group in pairs(groupset:GetSet()) do - local group=_group --Wrapper.Group#GROUP + local group=_group --Wrapper.Group#GROUP end end @@ -1775,41 +1843,13 @@ function AIRWING:_CreateFlightGroup(asset) -- Set airwing. flightgroup:SetAirwing(self) - + -- Set squadron. flightgroup.squadron=self:GetSquadronOfAsset(asset) -- Set home base. flightgroup.homebase=self.airbase - - --[[ - - --- Check if out of missiles. For A2A missions ==> RTB. - function flightgroup:OnAfterOutOfMissiles() - local airwing=flightgroup:GetAirWing() - - end - - --- Check if out of missiles. For A2G missions ==> RTB. But need to check A2G missiles, rockets as well. - function flightgroup:OnAfterOutOfBombs() - local airwing=flightgroup:GetAirWing() - - end - --- Mission started. - function flightgroup:OnAfterMissionStart(From, Event, To, Mission) - local airwing=flightgroup:GetAirWing() - - end - - --- Flight is DEAD. - function flightgroup:OnAfterFlightDead(From, Event, To) - local airwing=flightgroup:GetAirWing() - - end - - ]] - return flightgroup end @@ -1831,46 +1871,46 @@ function AIRWING:IsAssetOnMission(asset, MissionTypes) end if asset.flightgroup and asset.flightgroup:IsAlive() then - + -- Loop over mission queue. for _,_mission in pairs(asset.flightgroup.missionqueue or {}) do local mission=_mission --Ops.Auftrag#AUFTRAG - + if mission:IsNotOver() then - + -- Get flight status. local status=mission:GetGroupStatus(asset.flightgroup) - + -- Only if mission is started or executing. if (status==AUFTRAG.GroupStatus.STARTED or status==AUFTRAG.GroupStatus.EXECUTING) and self:CheckMissionType(mission.type, MissionTypes) then return true end - + end - + end - + end - + -- Alternative: run over all missions and compare to mission assets. --[[ for _,_mission in pairs(self.missionqueue) do local mission=_mission --Ops.Auftrag#AUFTRAG - + if mission:IsNotOver() then for _,_asset in pairs(mission.assets) do local sqasset=_asset --#AIRWING.SquadronAsset - + if sqasset.uid==asset.uid then return true end - + end end - + end ]] - + return false end @@ -1880,8 +1920,8 @@ end -- @return Ops.Auftrag#AUFTRAG Current mission or *nil*. function AIRWING:GetAssetCurrentMission(asset) - if asset.flightgroup then - return asset.flightgroup:GetMissionCurrent() + if asset.flightgroup then + return asset.flightgroup:GetMissionCurrent() end return nil @@ -1906,7 +1946,7 @@ function AIRWING:CountPayloadsInStock(MissionTypes, UnitTypes, Payloads) UnitTypes={UnitTypes} end end - + local function _checkUnitTypes(payload) if UnitTypes then for _,unittype in pairs(UnitTypes) do @@ -1920,7 +1960,7 @@ function AIRWING:CountPayloadsInStock(MissionTypes, UnitTypes, Payloads) end return false end - + local function _checkPayloads(payload) if Payloads then for _,Payload in pairs(Payloads) do @@ -1933,30 +1973,30 @@ function AIRWING:CountPayloadsInStock(MissionTypes, UnitTypes, Payloads) return nil end return false - end + end local n=0 for _,_payload in pairs(self.payloads) do local payload=_payload --#AIRWING.Payload - + for _,MissionType in pairs(MissionTypes) do - + local specialpayload=_checkPayloads(payload) local compatible=self:CheckMissionCapability(MissionType, payload.capabilities) - + local goforit = specialpayload or (specialpayload==nil and compatible) - + if goforit and _checkUnitTypes(payload) then - + if payload.unlimited then -- Payload is unlimited. Return a BIG number. return 999 else n=n+payload.navail end - + end - + end end @@ -1974,12 +2014,12 @@ function AIRWING:CountMissionsInQueue(MissionTypes) local N=0 for _,_mission in pairs(self.missionqueue) do local mission=_mission --Ops.Auftrag#AUFTRAG - + -- Check if this mission type is requested. if mission:IsNotOver() and self:CheckMissionType(mission.type, MissionTypes) then N=N+1 end - + end return N @@ -1991,7 +2031,7 @@ end function AIRWING:CountAssets() local N=0 - + for _,_squad in pairs(self.squadrons) do local squad=_squad --Ops.Squadron#SQUADRON N=N+#squad.assets @@ -2000,6 +2040,23 @@ function AIRWING:CountAssets() return N end + +--- Count total number of assets that are in the warehouse stock (not spawned). +-- @param #AIRWING self +-- @param #table MissionTypes (Optional) Count only assest that can perform certain mission type(s). Default is all types. +-- @return #number Amount of asset groups in stock. +function AIRWING:CountAssetsInStock(MissionTypes) + + local N=0 + + for _,_squad in pairs(self.squadrons) do + local squad=_squad --Ops.Squadron#SQUADRON + N=N+squad:CountAssetsInStock(MissionTypes) + end + + return N +end + --- Count assets on mission. -- @param #AIRWING self -- @param #table MissionTypes Types on mission to be checked. Default all. @@ -2008,32 +2065,32 @@ end -- @return #number Number of pending assets. -- @return #number Number of queued assets. function AIRWING:CountAssetsOnMission(MissionTypes, Squadron) - + local Nq=0 local Np=0 for _,_mission in pairs(self.missionqueue) do local mission=_mission --Ops.Auftrag#AUFTRAG - + -- Check if this mission type is requested. if self:CheckMissionType(mission.type, MissionTypes or AUFTRAG.Type) then - + for _,_asset in pairs(mission.assets or {}) do local asset=_asset --#AIRWING.SquadronAsset - + if Squadron==nil or Squadron.name==asset.squadname then - + local request, isqueued=self:GetRequestByID(mission.requestID) - + if isqueued then Nq=Nq+1 else Np=Np+1 end - + end - - end + + end end end @@ -2046,22 +2103,22 @@ end -- @param #table MissionTypes Types on mission to be checked. Default all. -- @return #table Assets on pending requests. function AIRWING:GetAssetsOnMission(MissionTypes) - + local assets={} local Np=0 for _,_mission in pairs(self.missionqueue) do local mission=_mission --Ops.Auftrag#AUFTRAG - + -- Check if this mission type is requested. if self:CheckMissionType(mission.type, MissionTypes) then - + for _,_asset in pairs(mission.assets or {}) do local asset=_asset --#AIRWING.SquadronAsset - table.insert(assets, asset) - - end + table.insert(assets, asset) + + end end end @@ -2077,13 +2134,13 @@ function AIRWING:GetAircraftTypes(onlyactive, squadrons) -- Get all unit types that can do the job. local unittypes={} - + -- Loop over all squadrons. for _,_squadron in pairs(squadrons or self.squadrons) do local squadron=_squadron --Ops.Squadron#SQUADRON - - if (not onlyactive) or squadron:IsOnDuty() then - + + if (not onlyactive) or squadron:IsOnDuty() then + local gotit=false for _,unittype in pairs(unittypes) do if squadron.aircrafttype==unittype then @@ -2094,9 +2151,9 @@ function AIRWING:GetAircraftTypes(onlyactive, squadrons) if not gotit then table.insert(unittypes, squadron.aircrafttype) end - + end - end + end return unittypes end @@ -2111,16 +2168,16 @@ function AIRWING:CanMission(Mission) -- Assume we CAN and NO assets are available. local Can=true local Assets={} - + -- Squadrons for the job. If user assigned to mission or simply all. local squadrons=Mission.squadrons or self.squadrons -- Get aircraft unit types for the job. local unittypes=self:GetAircraftTypes(true, squadrons) - + -- Count all payloads in stock. local Npayloads=self:CountPayloadsInStock(Mission.type, unittypes, Mission.payloads) - + if Npayloadsnmax 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 LUEacaoa.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 LUE1 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. diff --git a/Moose Development/Moose/Ops/ArmyGroup.lua b/Moose Development/Moose/Ops/ArmyGroup.lua index 28857197e..2bcef539a 100644 --- a/Moose Development/Moose/Ops/ArmyGroup.lua +++ b/Moose Development/Moose/Ops/ArmyGroup.lua @@ -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 diff --git a/Moose Development/Moose/Ops/Auftrag.lua b/Moose Development/Moose/Ops/Auftrag.lua index b7e19f41c..de8f0e82b 100644 --- a/Moose Development/Moose/Ops/Auftrag.lua +++ b/Moose Development/Moose/Ops/Auftrag.lua @@ -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 Tnowself.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 1 for stats output for debugging. +-- -- (added 0.1.4) limit amount of downed pilots spawned by **ejection** events +-- self.limitmaxdownedpilots = true +-- self.maxdownedpilots = 10 +-- -- (added 0.1.8) - allow to set far/near distance for approach and optionally pilot must open doors +-- self.approachdist_far = 5000 -- switch do 10 sec interval approach mode, meters +-- self.approachdist_near = 3000 -- switch to 5 sec interval approach mode, meters +-- self.pilotmustopendoors = false -- switch to true to enable check of open doors +-- +-- ## 2.1 Experimental Features +-- +-- WARNING - Here\'ll be dragons! +-- DANGER - For this to work you need to de-sanitize your mission environment (all three entries) in \Scripts\MissionScripting.lua +-- Needs SRS => 1.9.6 to work (works on the **server** side of SRS) +-- self.useSRS = false -- Set true to use FF\'s SRS integration +-- self.SRSPath = "E:\\Progra~1\\DCS-SimpleRadio-Standalone\\" -- adjust your own path in your SRS installation -- server(!) +-- self.SRSchannel = 300 -- radio channel +-- self.SRSModulation = radio.modulation.AM -- modulation +-- +-- ## 3. Results +-- +-- Number of successful landings with save pilots and aggregated number of saved pilots is stored in these variables in the object: +-- +-- self.rescues -- number of successful landings *with* saved pilots +-- self.rescuedpilots -- aggregated number of pilots rescued from the field (of *all* players) +-- +-- ## 4. Events +-- +-- The class comes with a number of FSM-based events that missions designers can use to shape their mission. +-- These are: +-- +-- ### 4.1. PilotDown. +-- +-- The event is triggered when a new downed pilot is detected. Use e.g. `function my_csar:OnAfterPilotDown(...)` to link into this event: +-- +-- function my_csar:OnAfterPilotDown(from, event, to, spawnedgroup, frequency, groupname, coordinates_text) +-- ... your code here ... +-- end +-- +-- ### 4.2. Approach. +-- +-- A CSAR helicpoter is closing in on a downed pilot. Use e.g. `function my_csar:OnAfterApproach(...)` to link into this event: +-- +-- function my_csar:OnAfterApproach(from, event, to, heliname, groupname) +-- ... your code here ... +-- end +-- +-- ### 4.3. Boarded. +-- +-- The pilot has been boarded to the helicopter. Use e.g. `function my_csar:OnAfterBoarded(...)` to link into this event: +-- +-- function my_csar:OnAfterBoarded(from, event, to, heliname, groupname) +-- ... your code here ... +-- end +-- +-- ### 4.4. Returning. +-- +-- The CSAR helicopter is ready to return to an Airbase, FARP or MASH. Use e.g. `function my_csar:OnAfterReturning(...)` to link into this event: +-- +-- function my_csar:OnAfterReturning(from, event, to, heliname, groupname) +-- ... your code here ... +-- end +-- +-- ### 4.5. Rescued. +-- +-- The CSAR helicopter has landed close to an Airbase/MASH/FARP and the pilots are safe. Use e.g. `function my_csar:OnAfterRescued(...)` to link into this event: +-- +-- function my_csar:OnAfterRescued(from, event, to, heliunit, heliname, pilotssaved) +-- ... your code here ... +-- end +-- +-- ## 5. Spawn downed pilots at location to be picked up. +-- +-- If missions designers want to spawn downed pilots into the field, e.g. at mission begin to give the helicopter guys works, they can do this like so: +-- +-- -- Create downed "Pilot Wagner" in #ZONE "CSAR_Start_1" at a random point for the blue coalition +-- my_csar:SpawnCSARAtZone( "CSAR_Start_1", coalition.side.BLUE, "Pilot Wagner", true ) +-- +-- +-- @field #CSAR +CSAR = { + ClassName = "CSAR", + verbose = 0, + lid = "", + coalition = 1, + coalitiontxt = "blue", + FreeVHFFrequencies = {}, + UsedVHFFrequencies = {}, + takenOff = {}, + csarUnits = {}, -- table of unit names + downedPilots = {}, + woundedGroups = {}, + landedStatus = {}, + addedTo = {}, + woundedGroups = {}, -- contains the new group of units + inTransitGroups = {}, -- contain a table for each SAR with all units he has with the original names + smokeMarkers = {}, -- tracks smoke markers for groups + heliVisibleMessage = {}, -- tracks if the first message has been sent of the heli being visible + heliCloseMessage = {}, -- tracks heli close message ie heli < 500m distance + max_units = 6, --number of pilots that can be carried + hoverStatus = {}, -- tracks status of a helis hover above a downed pilot + pilotDisabled = {}, -- tracks what aircraft a pilot is disabled for + pilotLives = {}, -- tracks how many lives a pilot has + useprefix = true, -- Use the Prefixed defined below, Requires Unit have the Prefix defined below + csarPrefix = {}, + template = nil, + mash = {}, + smokecolor = 4, + rescues = 0, + rescuedpilots = 0, + limitmaxdownedpilots = true, + maxdownedpilots = 10, +} + +--- Downed pilots info. +-- @type CSAR.DownedPilot +-- @field #number index Pilot index. +-- @field #string name Name of the spawned group. +-- @field #number side Coalition. +-- @field #string originalUnit Name of the original unit. +-- @field #string desc Description. +-- @field #string typename Typename of Unit. +-- @field #number frequency Frequency of the NDB. +-- @field #string player Player name if applicable. +-- @field Wrapper.Group#GROUP group Spawned group object. +-- @field #number timestamp Timestamp for approach process +-- @field #boolean alive Group is alive or dead/rescued +-- +--- Updated and sorted list of known NDB beacons (in kHz!) from the available maps. + +--[[ Moved to Utils +-- @field #CSAR.SkipFrequencies +CSAR.SkipFrequencies = { + 214,274,291.5,295,297.5, + 300.5,304,307,309.5,311,312,312.5,316, + 320,324,328,329,330,336,337, + 342,343,348,351,352,353,358, + 363,365,368,372.5,374, + 380,381,384,389,395,396, + 414,420,430,432,435,440,450,455,462,470,485, + 507,515,520,525,528,540,550,560,570,577,580,602,625,641,662,670,680,682,690, + 705,720,722,730,735,740,745,750,770,795, + 822,830,862,866, + 905,907,920,935,942,950,995, + 1000,1025,1030,1050,1065,1116,1175,1182,1210 + } +--]] + +--- All slot / Limit settings +-- @type CSAR.AircraftType +-- @field #string typename Unit type name. +CSAR.AircraftType = {} -- Type and limit +CSAR.AircraftType["SA342Mistral"] = 2 +CSAR.AircraftType["SA342Minigun"] = 2 +CSAR.AircraftType["SA342L"] = 4 +CSAR.AircraftType["SA342M"] = 4 +CSAR.AircraftType["UH-1H"] = 8 +CSAR.AircraftType["Mi-8MTV2"] = 12 +CSAR.AircraftType["Mi-8MT"] = 12 +CSAR.AircraftType["Mi-24P"] = 8 +CSAR.AircraftType["Mi-24V"] = 8 + +--- CSAR class version. +-- @field #string version +CSAR.version="0.1.8r3" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ToDo list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- DONE: SRS Integration (to be tested) +-- TODO: Maybe - add option to smoke/flare closest MASH + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new CSAR object and start the FSM. +-- @param #CSAR self +-- @param #number Coalition Coalition side. Can also be passed as a string "red", "blue" or "neutral". +-- @param #string Template Name of the late activated infantry unit standing in for the downed pilot. +-- @param #string Alias An *optional* alias how this object is called in the logs etc. +-- @return #CSAR self +function CSAR:New(Coalition, Template, Alias) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #CSAR + + --set Coalition + if Coalition and type(Coalition)=="string" then + if Coalition=="blue" then + self.coalition=coalition.side.BLUE + self.coalitiontxt = Coalition + elseif Coalition=="red" then + self.coalition=coalition.side.RED + self.coalitiontxt = Coalition + elseif Coalition=="neutral" then + self.coalition=coalition.side.NEUTRAL + self.coalitiontxt = Coalition + else + self:E("ERROR: Unknown coalition in CSAR!") + end + else + self.coalition = Coalition + self.coalitiontxt = string.lower(UTILS.GetCoalitionName(self.coalition)) + end + + -- Set alias. + if Alias then + self.alias=tostring(Alias) + else + self.alias="Red Cross" + if self.coalition then + if self.coalition==coalition.side.RED then + self.alias="IFRC" + elseif self.coalition==coalition.side.BLUE then + self.alias="CSAR" + end + end + end + + -- Set some string id for output to DCS.log file. + self.lid=string.format("%s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown") + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start FSM. + self:AddTransition("*", "Status", "*") -- CSAR status update. + self:AddTransition("*", "PilotDown", "*") -- Downed Pilot added + self:AddTransition("*", "Approach", "*") -- CSAR heli closing in. + self:AddTransition("*", "Boarded", "*") -- Pilot boarded. + self:AddTransition("*", "Returning", "*") -- CSAR able to return to base. + self:AddTransition("*", "Rescued", "*") -- Pilot at MASH. + self:AddTransition("*", "KIA", "*") -- Pilot killed in action. + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + + -- tables, mainly for tracking actions + self.addedTo = {} + self.allheligroupset = {} -- GROUP_SET of all helis + self.csarUnits = {} -- table of CSAR unit names + self.FreeVHFFrequencies = {} + self.heliVisibleMessage = {} -- tracks if the first message has been sent of the heli being visible + self.heliCloseMessage = {} -- tracks heli close message ie heli < 500m distance + self.hoverStatus = {} -- tracks status of a helis hover above a downed pilot + self.inTransitGroups = {} -- contain a table for each SAR with all units he has with the original names + self.landedStatus = {} + self.lastCrash = {} + self.takenOff = {} + self.smokeMarkers = {} -- tracks smoke markers for groups + self.UsedVHFFrequencies = {} + self.woundedGroups = {} -- contains the new group of units + self.downedPilots = {} -- Replacement woundedGroups + self.downedpilotcounter = 1 + + -- settings, counters etc + self.rescues = 0 -- counter for successful rescue landings at FARP/AFB/MASH + self.rescuedpilots = 0 -- counter for saved pilots + self.csarOncrash = false -- If set to true, will generate a csar when a plane crashes as well. + self.allowDownedPilotCAcontrol = false -- Set to false if you don\'t want to allow control by Combined arms. + self.enableForAI = false -- set to false to disable AI units from being rescued. + self.smokecolor = 4 -- Color of smokemarker for blue side, 0 is green, 1 is red, 2 is white, 3 is orange and 4 is blue + self.coordtype = 2 -- Use Lat/Long DDM (0), Lat/Long DMS (1), MGRS (2), Bullseye imperial (3) or Bullseye metric (4) for coordinates. + self.immortalcrew = true -- Set to true to make wounded crew immortal + self.invisiblecrew = false -- Set to true to make wounded crew insvisible + self.messageTime = 15 -- Time to show longer messages for in seconds + self.pilotRuntoExtractPoint = true -- Downed Pilot will run to the rescue helicopter up to self.extractDistance METERS + self.loadDistance = 75 -- configure distance for pilot to get in helicopter in meters. + self.extractDistance = 500 -- Distance the Downed pilot will run to the rescue helicopter + self.loadtimemax = 135 -- seconds + self.radioSound = "beacon.ogg" -- the name of the sound file to use for the Pilot radio beacons. If this isnt added to the mission BEACONS WONT WORK! + self.allowFARPRescue = true --allows pilot to be rescued by landing at a FARP or Airbase + self.max_units = 6 --max number of pilots that can be carried + self.useprefix = true -- Use the Prefixed defined below, Requires Unit have the Prefix defined below + self.csarPrefix = { "helicargo", "MEDEVAC"} -- prefixes used for useprefix=true - DON\'T use # in names! + self.template = Template or "generic" -- template for downed pilot + self.mashprefix = {"MASH"} -- prefixes used to find MASHes + self.mash = SET_GROUP:New():FilterCoalitions(self.coalition):FilterPrefixes(self.mashprefix):FilterOnce() -- currently only GROUP objects, maybe support STATICs also? + self.autosmoke = false -- automatically smoke location when heli is near + self.autosmokedistance = 2000 -- distance for autosmoke + -- added 0.1.4 + self.limitmaxdownedpilots = true + self.maxdownedpilots = 25 + -- generate Frequencies + self:_GenerateVHFrequencies() + -- added 0.1.8 + self.approachdist_far = 5000 -- switch do 10 sec interval approach mode, meters + self.approachdist_near = 3000 -- switch to 5 sec interval approach mode, meters + self.pilotmustopendoors = false -- switch to true to enable check on open doors + + -- WARNING - here\'ll be dragons + -- for this to work you need to de-sanitize your mission environment in \Scripts\MissionScripting.lua + -- needs SRS => 1.9.6 to work (works on the *server* side) + self.useSRS = false -- Use FF\'s SRS integration + self.SRSPath = "E:\\Progra~1\\DCS-SimpleRadio-Standalone\\" -- adjust your own path in your server(!) + self.SRSchannel = 300 -- radio channel + self.SRSModulation = radio.modulation.AM -- modulation + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the CSAR. Initializes parameters and starts event handlers. + -- @function [parent=#CSAR] Start + -- @param #CSAR self + + --- Triggers the FSM event "Start" after a delay. Starts the CSAR. Initializes parameters and starts event handlers. + -- @function [parent=#CSAR] __Start + -- @param #CSAR self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the CSAR and all its event handlers. + -- @param #CSAR self + + --- Triggers the FSM event "Stop" after a delay. Stops the CSAR and all its event handlers. + -- @function [parent=#CSAR] __Stop + -- @param #CSAR self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Status". + -- @function [parent=#CSAR] Status + -- @param #CSAR self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#CSAR] __Status + -- @param #CSAR self + -- @param #number delay Delay in seconds. + + --- On After "PilotDown" event. Downed Pilot detected. + -- @function [parent=#CSAR] OnAfterPilotDown + -- @param #CSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Group#GROUP Group Group object of the downed pilot. + -- @param #number Frequency Beacon frequency in kHz. + -- @param #string Leadername Name of the #UNIT of the downed pilot. + -- @param #string CoordinatesText String of the position of the pilot. Format determined by self.coordtype. + + --- On After "Aproach" event. Heli close to downed Pilot. + -- @function [parent=#CSAR] OnAfterApproach + -- @param #CSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string Heliname Name of the helicopter group. + -- @param #string Woundedgroupname Name of the downed pilot\'s group. + + --- On After "Boarded" event. Downed pilot boarded heli. + -- @function [parent=#CSAR] OnAfterBoarded + -- @param #CSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string Heliname Name of the helicopter group. + -- @param #string Woundedgroupname Name of the downed pilot\'s group. + + --- On After "Returning" event. Heli can return home with downed pilot(s). + -- @function [parent=#CSAR] OnAfterReturning + -- @param #CSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string Heliname Name of the helicopter group. + -- @param #string Woundedgroupname Name of the downed pilot\'s group. + + --- On After "Rescued" event. Pilot(s) have been brought to the MASH/FARP/AFB. + -- @function [parent=#CSAR] OnAfterRescued + -- @param #CSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Unit#UNIT HeliUnit Unit of the helicopter. + -- @param #string HeliName Name of the helicopter group. + -- @param #number PilotsSaved Number of the saved pilots on board when landing. + + --- On After "KIA" event. Pilot is dead. + -- @function [parent=#CSAR] OnAfterKIA + -- @param #CSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string Pilotname Name of the pilot KIA. + + return self +end + +------------------------ +--- Helper Functions --- +------------------------ + +--- (Internal) Function to insert downed pilot tracker object. +-- @param #CSAR self +-- @param Wrapper.Group#GROUP Group The #GROUP object +-- @param #string Groupname Name of the spawned group. +-- @param #number Side Coalition. +-- @param #string OriginalUnit Name of original Unit. +-- @param #string Description Descriptive text. +-- @param #string Typename Typename of unit. +-- @param #number Frequency Frequency of the NDB in Hz +-- @param #string Playername Name of Player (if applicable) +-- @return #CSAR self. +function CSAR:_CreateDownedPilotTrack(Group,Groupname,Side,OriginalUnit,Description,Typename,Frequency,Playername) + self:T({"_CreateDownedPilotTrack",Groupname,Side,OriginalUnit,Description,Typename,Frequency,Playername}) + + -- create new entry + local DownedPilot = {} -- #CSAR.DownedPilot + DownedPilot.desc = Description or "" + DownedPilot.frequency = Frequency or 0 + DownedPilot.index = self.downedpilotcounter + DownedPilot.name = Groupname or "" + DownedPilot.originalUnit = OriginalUnit or "" + DownedPilot.player = Playername or "" + DownedPilot.side = Side or 0 + DownedPilot.typename = Typename or "" + DownedPilot.group = Group + DownedPilot.timestamp = 0 + DownedPilot.alive = true + + -- Add Pilot + local PilotTable = self.downedPilots + local counter = self.downedpilotcounter + PilotTable[counter] = {} + PilotTable[counter] = DownedPilot + self:T({Table=PilotTable}) + self.downedPilots = PilotTable + -- Increase counter + self.downedpilotcounter = self.downedpilotcounter+1 + return self +end + +--- (Internal) Count pilots on board. +-- @param #CSAR self +-- @param #string _heliName +-- @return #number count +function CSAR:_PilotsOnboard(_heliName) + self:T(self.lid .. " _PilotsOnboard") + local count = 0 + if self.inTransitGroups[_heliName] then + for _, _group in pairs(self.inTransitGroups[_heliName]) do + count = count + 1 + end + end + return count +end + +--- (Internal) Function to check for dupe eject events. +-- @param #CSAR self +-- @param #string _unitname Name of unit. +-- @return #boolean Outcome +function CSAR:_DoubleEjection(_unitname) + if self.lastCrash[_unitname] then + local _time = self.lastCrash[_unitname] + if timer.getTime() - _time < 10 then + self:E(self.lid.."Caught double ejection!") + return true + end + end + self.lastCrash[_unitname] = timer.getTime() + return false +end + +--- (Internal) Spawn a downed pilot +-- @param #CSAR self +-- @param #number country Country for template. +-- @param Core.Point#COORDINATE point Coordinate to spawn at. +-- @param #number frequency Frequency of the pilot's beacon +-- @return Wrapper.Group#GROUP group The #GROUP object. +-- @return #string alias The alias name. +function CSAR:_SpawnPilotInField(country,point,frequency) + self:T({country,point,frequency}) + local freq = frequency or 1000 + local freq = freq / 1000 -- kHz + for i=1,10 do + math.random(i,10000) + end + local template = self.template + local alias = string.format("Pilot %.2fkHz-%d", freq, math.random(1,99)) + local coalition = self.coalition + local pilotcacontrol = self.allowDownedPilotCAcontrol -- Switch AI on/oof - is this really correct for CA? + local _spawnedGroup = SPAWN + :NewWithAlias(template,alias) + :InitCoalition(coalition) + :InitCountry(country) + :InitAIOnOff(pilotcacontrol) + :InitDelayOff() + :SpawnFromCoordinate(point) + + return _spawnedGroup, alias -- Wrapper.Group#GROUP object +end + +--- (Internal) Add options to a downed pilot +-- @param #CSAR self +-- @param Wrapper.Group#GROUP group Group to use. +function CSAR:_AddSpecialOptions(group) + self:T(self.lid.." _AddSpecialOptions") + self:T({group}) + + local immortalcrew = self.immortalcrew + local invisiblecrew = self.invisiblecrew + if immortalcrew then + local _setImmortal = { + id = 'SetImmortal', + params = { + value = true + } + } + group:SetCommand(_setImmortal) + end + + if invisiblecrew then + local _setInvisible = { + id = 'SetInvisible', + params = { + value = true + } + } + group:SetCommand(_setInvisible) + end + + group:OptionAlarmStateGreen() + group:OptionROEHoldFire() + return self +end + +--- (Internal) Function to spawn a CSAR object into the scene. +-- @param #CSAR self +-- @param #number _coalition Coalition +-- @param DCS#country.id _country Country ID +-- @param Core.Point#COORDINATE _point Coordinate +-- @param #string _typeName Typename +-- @param #string _unitName Unitname +-- @param #string _playerName Playername +-- @param #number _freq Frequency +-- @param #boolean noMessage +-- @param #string _description Description +-- @param #boolean forcedesc Use the description only for the pilot track entry +function CSAR:_AddCsar(_coalition , _country, _point, _typeName, _unitName, _playerName, _freq, noMessage, _description, forcedesc ) + self:T(self.lid .. " _AddCsar") + self:T({_coalition , _country, _point, _typeName, _unitName, _playerName, _freq, noMessage, _description}) + + local template = self.template + + if not _freq then + _freq = self:_GenerateADFFrequency() + if not _freq then _freq = 333000 end --noob catch + end + + local _spawnedGroup, _alias = self:_SpawnPilotInField(_country,_point,_freq) + + local _typeName = _typeName or "Pilot" + + if not noMessage then + self:_DisplayToAllSAR("MAYDAY MAYDAY! " .. _typeName .. " is down. ", self.coalition, 10) + end + + if _freq then + self:_AddBeaconToGroup(_spawnedGroup, _freq) + end + + self:_AddSpecialOptions(_spawnedGroup) + + local _text = _description + if not forcedesc then + if _playerName ~= nil then + _text = "Pilot " .. _playerName + elseif _unitName ~= nil then + _text = "AI Pilot of " .. _unitName + end + end + self:T({_spawnedGroup, _alias}) + + local _GroupName = _spawnedGroup:GetName() or _alias + + self:_CreateDownedPilotTrack(_spawnedGroup,_GroupName,_coalition,_unitName,_text,_typeName,_freq,_playerName) + + self:_InitSARForPilot(_spawnedGroup, _GroupName, _freq, noMessage) + + return self +end + +--- (Internal) Function to add a CSAR object into the scene at a zone coordinate. For mission designers wanting to add e.g. PoWs to the scene. +-- @param #CSAR self +-- @param #string _zone Name of the zone. +-- @param #number _coalition Coalition. +-- @param #string _description (optional) Description. +-- @param #boolean _randomPoint (optional) Random yes or no. +-- @param #boolean _nomessage (optional) If true, don\'t send a message to SAR. +-- @param #string unitname (optional) Name of the lost unit. +-- @param #string typename (optional) Type of plane. +-- @param #boolean forcedesc (optional) Force to use the description passed only for the pilot track entry. Use to have fully custom names. +function CSAR:_SpawnCsarAtZone( _zone, _coalition, _description, _randomPoint, _nomessage, unitname, typename, forcedesc) + self:T(self.lid .. " _SpawnCsarAtZone") + local freq = self:_GenerateADFFrequency() + local _triggerZone = ZONE:New(_zone) -- trigger to use as reference position + if _triggerZone == nil then + self:E(self.lid.."ERROR: Can\'t find zone called " .. _zone, 10) + return + end + + local _description = _description or "PoW" + local unitname = unitname or "Old Rusty" + local typename = typename or "Phantom II" + + local pos = {} + if _randomPoint then + local _pos = _triggerZone:GetRandomPointVec3() + pos = COORDINATE:NewFromVec3(_pos) + else + pos = _triggerZone:GetCoordinate() + end + + local _country = 0 + if _coalition == coalition.side.BLUE then + _country = country.id.USA + elseif _coalition == coalition.side.RED then + _country = country.id.RUSSIA + else + _country = country.id.UN_PEACEKEEPERS + end + + self:_AddCsar(_coalition, _country, pos, typename, unitname, _description, freq, _nomessage, _description, forcedesc) + + return self +end + +--- Function to add a CSAR object into the scene at a zone coordinate. For mission designers wanting to add e.g. PoWs to the scene. +-- @param #CSAR self +-- @param #string Zone Name of the zone. +-- @param #number Coalition Coalition. +-- @param #string Description (optional) Description. +-- @param #boolean RandomPoint (optional) Random yes or no. +-- @param #boolean Nomessage (optional) If true, don\'t send a message to SAR. +-- @param #string Unitname (optional) Name of the lost unit. +-- @param #string Typename (optional) Type of plane. +-- @param #boolean Forcedesc (optional) Force to use the **description passed only** for the pilot track entry. Use to have fully custom names. +-- @usage If missions designers want to spawn downed pilots into the field, e.g. at mission begin, to give the helicopter guys work, they can do this like so: +-- +-- -- Create downed "Pilot Wagner" in #ZONE "CSAR_Start_1" at a random point for the blue coalition +-- my_csar:SpawnCSARAtZone( "CSAR_Start_1", coalition.side.BLUE, "Wagner", true, false, "Charly-1-1", "F5E" ) +function CSAR:SpawnCSARAtZone(Zone, Coalition, Description, RandomPoint, Nomessage, Unitname, Typename, Forcedesc) + self:_SpawnCsarAtZone(Zone, Coalition, Description, RandomPoint, Nomessage, Unitname, Typename, Forcedesc) + return self +end + +-- TODO: Split in functions per Event type +--- (Internal) Event handler. +-- @param #CSAR self +function CSAR:_EventHandler(EventData) + self:T(self.lid .. " _EventHandler") + self:T({Event = EventData.id}) + + local _event = EventData -- Core.Event#EVENTDATA + + -- no event + if _event == nil or _event.initiator == nil then + return false + + -- take off + elseif _event.id == EVENTS.Takeoff then -- taken off + self:T(self.lid .. " Event unit - Takeoff") + + local _coalition = _event.IniCoalition + if _coalition ~= self.coalition then + return --ignore! + end + + if _event.IniGroupName then + self.takenOff[_event.IniUnitName] = true + end + + return true + + -- player enter unit + elseif _event.id == EVENTS.PlayerEnterAircraft or _event.id == EVENTS.PlayerEnterUnit then --player entered unit + self:T(self.lid .. " Event unit - Player Enter") + + local _coalition = _event.IniCoalition + if _coalition ~= self.coalition then + return --ignore! + end + + if _event.IniPlayerName then + self.takenOff[_event.IniPlayerName] = nil + end + + local _unit = _event.IniUnit + local _group = _event.IniGroup + if _unit:IsHelicopter() or _group:IsHelicopter() then + self:_AddMedevacMenuItem() + end + + return true + + elseif (_event.id == EVENTS.PilotDead and self.csarOncrash == false) then + -- Pilot dead + + self:T(self.lid .. " Event unit - Pilot Dead") + + local _unit = _event.IniUnit + local _unitname = _event.IniUnitName + local _group = _event.IniGroup + + if _unit == nil then + return -- error! + end + + local _coalition = _event.IniCoalition + if _coalition ~= self.coalition then + return --ignore! + end + + -- Catch multiple events here? + if self.takenOff[_event.IniUnitName] == true or _group:IsAirborne() then + if self:_DoubleEjection(_unitname) then + return + end + self:_DisplayToAllSAR("MAYDAY MAYDAY! " .. _unit:GetTypeName() .. " shot down. No Chute!", self.coalition, 10) + --local m = MESSAGE:New("MAYDAY MAYDAY! " .. _unit:GetTypeName() .. " shot down. No Chute!",10,"Info"):ToCoalition(self.coalition) + else + self:T(self.lid .. " Pilot has not taken off, ignore") + end + + return + + elseif _event.id == EVENTS.PilotDead or _event.id == EVENTS.Ejection then + if _event.id == EVENTS.PilotDead and self.csarOncrash == false then + return + end + self:T(self.lid .. " Event unit - Pilot Ejected") + + local _unit = _event.IniUnit + local _unitname = _event.IniUnitName + local _group = _event.IniGroup + + if _unit == nil then + return -- error! + end + + local _coalition = _unit:GetCoalition() + if _coalition ~= self.coalition then + return --ignore! + end + + if self.enableForAI == false and _event.IniPlayerName == nil then + return + end + + if not self.takenOff[_event.IniUnitName] and not _group:IsAirborne() then + self:T(self.lid .. " Pilot has not taken off, ignore") + return -- give up, pilot hasnt taken off + end + + if self:_DoubleEjection(_unitname) then + return + end + + -- limit no of pilots in the field. + if self.limitmaxdownedpilots and self:_ReachedPilotLimit() then + return + end + + -- all checks passed, get going. + local _freq = self:_GenerateADFFrequency() + self:_AddCsar(_coalition, _unit:GetCountry(), _unit:GetCoordinate() , _unit:GetTypeName(), _unit:GetName(), _event.IniPlayerName, _freq, false, "none") + + return true + + elseif _event.id == EVENTS.Land then + self:T(self.lid .. " Landing") + + if _event.IniUnitName then + self.takenOff[_event.IniUnitName] = nil + end + + if self.allowFARPRescue then + + local _unit = _event.IniUnit -- Wrapper.Unit#UNIT + + if _unit == nil then + self:T(self.lid .. " Unit nil on landing") + return -- error! + end + + local _coalition = _event.IniCoalition + if _coalition ~= self.coalition then + return --ignore! + end + + self.takenOff[_event.IniUnitName] = nil + + local _place = _event.Place -- Wrapper.Airbase#AIRBASE + + if _place == nil then + self:T(self.lid .. " Landing Place Nil") + return -- error! + end + + if _place:GetCoalition() == self.coalition or _place:GetCoalition() == coalition.side.NEUTRAL then + if self.pilotmustopendoors and not self:_IsLoadingDoorOpen(_event.IniUnitName) then + self:_DisplayMessageToSAR(_unit, "Open the door to let me out!", self.messageTime, true) + else + self:_RescuePilots(_unit) + end + else + self:T(string.format("Airfield %d, Unit %d", _place:GetCoalition(), _unit:GetCoalition())) + end + end + + return true + end + return self +end + +--- (Internal) Initialize the action for a pilot. +-- @param #CSAR self +-- @param Wrapper.Group#GROUP _downedGroup The group to rescue. +-- @param #string _GroupName Name of the Group +-- @param #number _freq Beacon frequency. +-- @param #boolean _nomessage Send message true or false. +function CSAR:_InitSARForPilot(_downedGroup, _GroupName, _freq, _nomessage) + self:T(self.lid .. " _InitSARForPilot") + local _leader = _downedGroup:GetUnit(1) + --local _groupName = _downedGroup:GetName() + local _groupName = _GroupName + local _freqk = _freq / 1000 + local _coordinatesText = self:_GetPositionOfWounded(_downedGroup) + local _leadername = _leader:GetName() + + if not _nomessage then + local _text = string.format("%s requests SAR at %s, beacon at %.2f KHz", _leadername, _coordinatesText, _freqk) + self:_DisplayToAllSAR(_text,self.coalition,self.messageTime) + end + + for _,_heliName in pairs(self.csarUnits) do + self:_CheckWoundedGroupStatus(_heliName, _groupName) + end + + -- trigger FSM event + self:__PilotDown(2,_downedGroup, _freqk, _leadername, _coordinatesText) + + return self +end + +--- (Internal) Check if a name is in downed pilot table +-- @param #CSAR self +-- @param #string name Name to search for. +-- @return #boolean Outcome. +-- @return #CSAR.DownedPilot Table if found else nil. +function CSAR:_CheckNameInDownedPilots(name) + local PilotTable = self.downedPilots --#CSAR.DownedPilot + local found = false + local table = nil + for _,_pilot in pairs(PilotTable) do + if _pilot.name == name and _pilot.alive == true then + found = true + table = _pilot + break + end + end + return found, table +end + +--- (Internal) Check if a name is in downed pilot table and remove it. +-- @param #CSAR self +-- @param #string name Name to search for. +-- @param #boolean force Force removal. +-- @return #boolean Outcome. +function CSAR:_RemoveNameFromDownedPilots(name,force) + local PilotTable = self.downedPilots --#CSAR.DownedPilot + local found = false + for _index,_pilot in pairs(PilotTable) do + if _pilot.name == name then + self.downedPilots[_index].alive = false + end + end + return found +end + +--- (Internal) Check state of wounded group. +-- @param #CSAR self +-- @param #string heliname heliname +-- @param #string woundedgroupname woundedgroupname +function CSAR:_CheckWoundedGroupStatus(heliname,woundedgroupname) + self:T(self.lid .. " _CheckWoundedGroupStatus") + local _heliName = heliname + local _woundedGroupName = woundedgroupname + self:T({Heli = _heliName, Downed = _woundedGroupName}) + -- if wounded group is not here then message already been sent to SARs + -- stop processing any further + local _found, _downedpilot = self:_CheckNameInDownedPilots(_woundedGroupName) + if not _found then + self:T("...not found in list!") + return + end + + local _woundedGroup = _downedpilot.group + if _woundedGroup ~= nil and _woundedGroup:IsAlive() then + local _heliUnit = self:_GetSARHeli(_heliName) -- Wrapper.Unit#UNIT + + local _lookupKeyHeli = _heliName .. "_" .. _woundedGroupName --lookup key for message state tracking + + if _heliUnit == nil then + self.heliVisibleMessage[_lookupKeyHeli] = nil + self.heliCloseMessage[_lookupKeyHeli] = nil + self.landedStatus[_lookupKeyHeli] = nil + self:T("...helinunit nil!") + return + end + + local _heliCoord = _heliUnit:GetCoordinate() + local _leaderCoord = _woundedGroup:GetCoordinate() + local _distance = self:_GetDistance(_heliCoord,_leaderCoord) + -- autosmoke + if (self.autosmoke == true) and (_distance < self.autosmokedistance) and (_distance ~= -1) then + self:_PopSmokeForGroup(_woundedGroupName, _woundedGroup) + end + + if _distance < self.approachdist_near and _distance > 0 then + if self:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedGroup, _woundedGroupName) == true then + -- we\'re close, reschedule + _downedpilot.timestamp = timer.getAbsTime() + self:__Approach(-5,heliname,woundedgroupname) + end + elseif _distance >= self.approachdist_near and _distance < self.approachdist_far then + -- message once + if self.heliVisibleMessage[_lookupKeyHeli] == nil then + local _pilotName = _downedpilot.desc + if self.autosmoke == true then + local dist = self.autosmokedistance / 1000 + local disttext = string.format("%.0fkm",dist) + if _SETTINGS:IsImperial() then + local dist = UTILS.MetersToNM(self.autosmokedistance) + disttext = string.format("%.0fnm",dist) + end + self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. I hear you! Damn, that thing is loud!\nI'll pop a smoke when you are %s away.\nLand or hover by the smoke.", _heliName, _pilotName, disttext), self.messageTime,false,true) + else + self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. I hear you! Damn, that thing is loud!\nRequest a flare or smoke if you need.", _heliName, _pilotName), self.messageTime,false,true) + end + --mark as shown for THIS heli and THIS group + self.heliVisibleMessage[_lookupKeyHeli] = true + end + self.heliCloseMessage[_lookupKeyHeli] = nil + self.landedStatus[_lookupKeyHeli] = nil + --reschedule as units aren\'t dead yet , schedule for a bit slower though as we\'re far away + _downedpilot.timestamp = timer.getAbsTime() + self:__Approach(-10,heliname,woundedgroupname) + end + else + self:T("...Downed Pilot KIA?!") + if not _downedpilot.alive then + --self:__KIA(1,_downedpilot.name) + self:_RemoveNameFromDownedPilots(_downedpilot.name, true) + end + end + return self +end + +--- (Internal) Function to pop a smoke at a wounded pilot\'s positions. +-- @param #CSAR self +-- @param #string _woundedGroupName Name of the group. +-- @param Wrapper.Group#GROUP _woundedLeader Object of the group. +function CSAR:_PopSmokeForGroup(_woundedGroupName, _woundedLeader) + self:T(self.lid .. " _PopSmokeForGroup") + -- have we popped smoke already in the last 5 mins + local _lastSmoke = self.smokeMarkers[_woundedGroupName] + if _lastSmoke == nil or timer.getTime() > _lastSmoke then + + local _smokecolor = self.smokecolor + local _smokecoord = _woundedLeader:GetCoordinate() + _smokecoord:Smoke(_smokecolor) + self.smokeMarkers[_woundedGroupName] = timer.getTime() + 300 -- next smoke time + end + return self +end + +--- (Internal) Function to pickup the wounded pilot from the ground. +-- @param #CSAR self +-- @param Wrapper.Unit#UNIT _heliUnit Object of the group. +-- @param #string _pilotName Name of the pilot. +-- @param Wrapper.Group#GROUP _woundedGroup Object of the group. +-- @param #string _woundedGroupName Name of the group. +function CSAR:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) + self:T(self.lid .. " _PickupUnit") + -- board + local _heliName = _heliUnit:GetName() + local _groups = self.inTransitGroups[_heliName] + local _unitsInHelicopter = self:_PilotsOnboard(_heliName) + + -- init table if there is none for this helicopter + if not _groups then + self.inTransitGroups[_heliName] = {} + _groups = self.inTransitGroups[_heliName] + end + + -- if the heli can\'t pick them up, show a message and return + local _maxUnits = self.AircraftType[_heliUnit:GetTypeName()] + if _maxUnits == nil then + _maxUnits = self.max_units + end + if _unitsInHelicopter + 1 > _maxUnits then + self:_DisplayMessageToSAR(_heliUnit, string.format("%s, %s. We\'re already crammed with %d guys! Sorry!", _pilotName, _heliName, _unitsInHelicopter, _unitsInHelicopter), self.messageTime) + return true + end + + local found,downedgrouptable = self:_CheckNameInDownedPilots(_woundedGroupName) + local grouptable = downedgrouptable --#CSAR.DownedPilot + self.inTransitGroups[_heliName][_woundedGroupName] = + { + -- DONE: Fix with #CSAR.DownedPilot + originalUnit = grouptable.originalUnit, + woundedGroup = _woundedGroupName, + side = self.coalition, + desc = grouptable.desc, + player = grouptable.player, + } + + _woundedGroup:Destroy() + self:_RemoveNameFromDownedPilots(_woundedGroupName,true) + + self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s I\'m in! Get to the MASH ASAP! ", _heliName, _pilotName), self.messageTime,true,true) + + self:__Boarded(5,_heliName,_woundedGroupName) + + return true +end + +--- (Internal) Move group to destination. +-- @param #CSAR self +-- @param Wrapper.Group#GROUP _leader +-- @param Core.Point#COORDINATE _destination +function CSAR:_OrderGroupToMoveToPoint(_leader, _destination) + self:T(self.lid .. " _OrderGroupToMoveToPoint") + local group = _leader + local coordinate = _destination:GetVec2() + + group:SetAIOn() + group:RouteToVec2(coordinate,5) + return self +end + + +--- (internal) Function to check if the heli door(s) are open. Thanks to Shadowze. +-- @param #CSAR self +-- @param #string unit_name Name of unit. +-- @return #boolean outcome The outcome. +function CSAR:_IsLoadingDoorOpen( unit_name ) + self:T(self.lid .. " _IsLoadingDoorOpen") + local ret_val = false + local unit = Unit.getByName(unit_name) + if unit ~= nil then + local type_name = unit:getTypeName() + + if type_name == "Mi-8MT" and unit:getDrawArgumentValue(86) == 1 or unit:getDrawArgumentValue(250) == 1 then + self:T(unit_name .. " Cargo doors are open or cargo door not present") + ret_val = true + end + + if type_name == "Mi-24P" and unit:getDrawArgumentValue(38) == 1 or unit:getDrawArgumentValue(86) == 1 then + self:T(unit_name .. " a side door is open") + ret_val = true + end + + if type_name == "UH-1H" and unit:getDrawArgumentValue(43) == 1 or unit:getDrawArgumentValue(44) == 1 then + self:T(unit_name .. " a side door is open ") + ret_val = true + end + + if string.find(type_name, "SA342" ) and unit:getDrawArgumentValue(34) == 1 or unit:getDrawArgumentValue(38) == 1 then + self:T(unit_name .. " front door(s) are open") + ret_val = true + end + + if ret_val == false then + self:T(unit_name .. " all doors are closed") + end + return ret_val + + end -- nil + + return false +end + +--- (Internal) Function to check if heli is close to group. +-- @param #CSAR self +-- @param #number _distance +-- @param Wrapper.Unit#UNIT _heliUnit +-- @param #string _heliName +-- @param Wrapper.Group#GROUP _woundedGroup +-- @param #string _woundedGroupName +-- @return #boolean Outcome +function CSAR:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedGroup, _woundedGroupName) + self:T(self.lid .. " _CheckCloseWoundedGroup") + + local _woundedLeader = _woundedGroup + local _lookupKeyHeli = _heliUnit:GetName() .. "_" .. _woundedGroupName --lookup key for message state tracking + + local _found, _pilotable = self:_CheckNameInDownedPilots(_woundedGroupName) -- #boolean, #CSAR.DownedPilot + local _pilotName = _pilotable.desc + + + local _reset = true + + if (_distance < 500) then + + if self.heliCloseMessage[_lookupKeyHeli] == nil then + if self.autosmoke == true then + self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. You\'re close now! Land or hover at the smoke.", _heliName, _pilotName), self.messageTime,false,true) + else + self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. You\'re close now! Land in a safe place, I will go there ", _heliName, _pilotName), self.messageTime,false,true) + end + --mark as shown for THIS heli and THIS group + self.heliCloseMessage[_lookupKeyHeli] = true + end + + -- have we landed close enough? + if not _heliUnit:InAir() then + + -- if you land on them, doesnt matter if they were heading to someone else as you\'re closer, you win! :) + if self.pilotRuntoExtractPoint == true then + if (_distance < self.extractDistance) then + local _time = self.landedStatus[_lookupKeyHeli] + if _time == nil then + self.landedStatus[_lookupKeyHeli] = math.floor( (_distance - self.loadDistance) / 3.6 ) + _time = self.landedStatus[_lookupKeyHeli] + self:_OrderGroupToMoveToPoint(_woundedGroup, _heliUnit:GetCoordinate()) + self:_DisplayMessageToSAR(_heliUnit, "Wait till " .. _pilotName .. " gets in. \nETA " .. _time .. " more seconds.", self.messageTime, false) + else + _time = self.landedStatus[_lookupKeyHeli] - 10 + self.landedStatus[_lookupKeyHeli] = _time + end + if _time <= 0 or _distance < self.loadDistance then + if self.pilotmustopendoors and not self:_IsLoadingDoorOpen(_heliName) then + self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true) + return true + else + self.landedStatus[_lookupKeyHeli] = nil + self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) + return false + end + end + end + else + if (_distance < self.loadDistance) then + if self.pilotmustopendoors and not self:_IsLoadingDoorOpen(_heliName) then + self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true) + return true + else + self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) + return false + end + end + end + else + + local _unitsInHelicopter = self:_PilotsOnboard(_heliName) + local _maxUnits = self.AircraftType[_heliUnit:GetTypeName()] + if _maxUnits == nil then + _maxUnits = self.max_units + end + + if _heliUnit:InAir() and _unitsInHelicopter + 1 <= _maxUnits then + + if _distance < 8.0 then + + --check height! + local leaderheight = _woundedLeader:GetHeight() + if leaderheight < 0 then leaderheight = 0 end + local _height = _heliUnit:GetHeight() - leaderheight + + if _height <= 20.0 then + + local _time = self.hoverStatus[_lookupKeyHeli] + + if _time == nil then + self.hoverStatus[_lookupKeyHeli] = 10 + _time = 10 + else + _time = self.hoverStatus[_lookupKeyHeli] - 10 + self.hoverStatus[_lookupKeyHeli] = _time + end + + if _time > 0 then + self:_DisplayMessageToSAR(_heliUnit, "Hovering above " .. _pilotName .. ". \n\nHold hover for " .. _time .. " seconds to winch them up. \n\nIf the countdown stops you\'re too far away!", self.messageTime, true) + else + if self.pilotmustopendoors and not self:_IsLoadingDoorOpen(_heliName) then + self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true) + return true + else + self.hoverStatus[_lookupKeyHeli] = nil + self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName) + return false + end + end + _reset = false + else + self:_DisplayMessageToSAR(_heliUnit, "Too high to winch " .. _pilotName .. " \nReduce height and hover for 10 seconds!", self.messageTime, true,true) + end + end + + end + end + end + + if _reset then + self.hoverStatus[_lookupKeyHeli] = nil + end + + if _distance < 500 then + return true + else + return false + end +end + +--- (Internal) Monitor in-flight returning groups. +-- @param #CSAR self +-- @param #string heliname Heli name +-- @param #string groupname Group name +function CSAR:_ScheduledSARFlight(heliname,groupname) + self:T(self.lid .. " _ScheduledSARFlight") + self:T({heliname,groupname}) + local _heliUnit = self:_GetSARHeli(heliname) + local _woundedGroupName = groupname + + if (_heliUnit == nil) then + --helicopter crashed? + self.inTransitGroups[heliname] = nil + return + end + + if self.inTransitGroups[heliname] == nil or self.inTransitGroups[heliname][_woundedGroupName] == nil then + -- Groups already rescued + return + end + + local _dist = self:_GetClosestMASH(_heliUnit) + + if _dist == -1 then + return + end + + if _dist < 200 and _heliUnit:InAir() == false then + if self.pilotmustopendoors and not self:_IsLoadingDoorOpen(heliname) then + self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me out!", self.messageTime, true) + else + self:_RescuePilots(_heliUnit) + return + end + end + + --queue up + self:__Returning(-5,heliname,_woundedGroupName) + return self +end + +--- (Internal) Mark pilot as rescued and remove from tables. +-- @param #CSAR self +-- @param Wrapper.Unit#UNIT _heliUnit +function CSAR:_RescuePilots(_heliUnit) + self:T(self.lid .. " _RescuePilots") + local _heliName = _heliUnit:GetName() + local _rescuedGroups = self.inTransitGroups[_heliName] + + if _rescuedGroups == nil then + -- Groups already rescued + return + end + + -- DONE: count saved units? + local PilotsSaved = self:_PilotsOnboard(_heliName) + + self.inTransitGroups[_heliName] = nil + + local _txt = string.format("%s: The %d pilot(s) have been taken to the\nmedical clinic. Good job!", _heliName, PilotsSaved) + + self:_DisplayMessageToSAR(_heliUnit, _txt, self.messageTime) + -- trigger event + self:__Rescued(-1,_heliUnit,_heliName, PilotsSaved) + return self +end + +--- (Internal) Check and return Wrappe.Unit#UNIT based on the name if alive. +-- @param #CSAR self +-- @param #string _unitname Name of Unit +-- @return Wrapper.Unit#UNIT The unit or nil +function CSAR:_GetSARHeli(_unitName) + self:T(self.lid .. " _GetSARHeli") + local unit = UNIT:FindByName(_unitName) + if unit and unit:IsAlive() then + return unit + else + return nil + end +end + +--- (Internal) Display message to single Unit. +-- @param #CSAR self +-- @param Wrapper.Unit#UNIT _unit Unit #UNIT to display to. +-- @param #string _text Text of message. +-- @param #number _time Message show duration. +-- @param #boolean _clear (optional) Clear screen. +-- @param #boolean _speak (optional) Speak message via SRS. +function CSAR:_DisplayMessageToSAR(_unit, _text, _time, _clear, _speak) + self:T(self.lid .. " _DisplayMessageToSAR") + local group = _unit:GetGroup() + local _clear = _clear or nil + local _time = _time or self.messageTime + local m = MESSAGE:New(_text,_time,"Info",_clear):ToGroup(group) + -- integrate SRS + if _speak and self.useSRS then + local srstext = SOUNDTEXT:New(_text) + local path = self.SRSPath + local modulation = self.SRSModulation + local channel = self.SRSchannel + local msrs = MSRS:New(path,channel,modulation) + msrs:PlaySoundText(srstext, 2) + end + return self +end + +--- (Internal) Function to get string of a group\'s position. +-- @param #CSAR self +-- @param Wrapper.Controllable#CONTROLLABLE _woundedGroup Group or Unit object. +-- @return #string Coordinates as Text +function CSAR:_GetPositionOfWounded(_woundedGroup) + self:T(self.lid .. " _GetPositionOfWounded") + local _coordinate = _woundedGroup:GetCoordinate() + local _coordinatesText = "None" + if _coordinate then + if self.coordtype == 0 then -- Lat/Long DMTM + _coordinatesText = _coordinate:ToStringLLDDM() + elseif self.coordtype == 1 then -- Lat/Long DMS + _coordinatesText = _coordinate:ToStringLLDMS() + elseif self.coordtype == 2 then -- MGRS + _coordinatesText = _coordinate:ToStringMGRS() + else -- Bullseye Metric --(medevac.coordtype == 4 or 3) + _coordinatesText = _coordinate:ToStringBULLS(self.coalition) + end + end + return _coordinatesText +end + +--- (Internal) Display active SAR tasks to player. +-- @param #CSAR self +-- @param #string _unitName Unit to display to +function CSAR:_DisplayActiveSAR(_unitName) + self:T(self.lid .. " _DisplayActiveSAR") + local _msg = "Active MEDEVAC/SAR:" + local _heli = self:_GetSARHeli(_unitName) -- Wrapper.Unit#UNIT + if _heli == nil then + return + end + + local _heliSide = self.coalition + local _csarList = {} + + local _DownedPilotTable = self.downedPilots + self:T({Table=_DownedPilotTable}) + for _, _value in pairs(_DownedPilotTable) do + local _groupName = _value.name + self:T(string.format("Display Active Pilot: %s", tostring(_groupName))) + self:T({Table=_value}) + --local _woundedGroup = GROUP:FindByName(_groupName) + local _woundedGroup = _value.group + if _woundedGroup and _value.alive then + local _coordinatesText = self:_GetPositionOfWounded(_woundedGroup) + local _helicoord = _heli:GetCoordinate() + local _woundcoord = _woundedGroup:GetCoordinate() + local _distance = self:_GetDistance(_helicoord, _woundcoord) + self:T({_distance = _distance}) + local distancetext = "" + if _SETTINGS:IsImperial() then + distancetext = string.format("%.1fnm",UTILS.MetersToNM(_distance)) + else + distancetext = string.format("%.1fkm", _distance/1000.0) + end + table.insert(_csarList, { dist = _distance, msg = string.format("%s at %s - %.2f KHz ADF - %s ", _value.desc, _coordinatesText, _value.frequency / 1000, distancetext) }) + end + end + + local function sortDistance(a, b) + return a.dist < b.dist + end + + table.sort(_csarList, sortDistance) + + for _, _line in pairs(_csarList) do + _msg = _msg .. "\n" .. _line.msg + end + + self:_DisplayMessageToSAR(_heli, _msg, self.messageTime*2) + return self +end + +--- (Internal) Find the closest downed pilot to a heli. +-- @param #CSAR self +-- @param Wrapper.Unit#UNIT _heli Helicopter #UNIT +-- @return #table Table of results +function CSAR:_GetClosestDownedPilot(_heli) + self:T(self.lid .. " _GetClosestDownedPilot") + local _side = self.coalition + local _closestGroup = nil + local _shortestDistance = -1 + local _distance = 0 + local _closestGroupInfo = nil + local _heliCoord = _heli:GetCoordinate() + + local DownedPilotsTable = self.downedPilots + for _, _groupInfo in pairs(DownedPilotsTable) do + local _woundedName = _groupInfo.name + local _tempWounded = _groupInfo.group + + -- check group exists and not moving to someone else + if _tempWounded then + local _tempCoord = _tempWounded:GetCoordinate() + _distance = self:_GetDistance(_heliCoord, _tempCoord) + + if _distance ~= nil and (_shortestDistance == -1 or _distance < _shortestDistance) then + _shortestDistance = _distance + _closestGroup = _tempWounded + _closestGroupInfo = _groupInfo + end + end + end + + return { pilot = _closestGroup, distance = _shortestDistance, groupInfo = _closestGroupInfo } +end + +--- (Internal) Fire a flare at the point of a downed pilot. +-- @param #CSAR self +-- @param #string _unitName Name of the unit. +function CSAR:_SignalFlare(_unitName) + self:T(self.lid .. " _SignalFlare") + local _heli = self:_GetSARHeli(_unitName) + if _heli == nil then + return + end + + local _closest = self:_GetClosestDownedPilot(_heli) + local smokedist = 8000 + if self.approachdist_far > smokedist then smokedist = self.approachdist_far end + if _closest ~= nil and _closest.pilot ~= nil and _closest.distance < smokedist then + + local _clockDir = self:_GetClockDirection(_heli, _closest.pilot) + local _distance = 0 + if _SETTINGS:IsImperial() then + _distance = string.format("%.1fnm",UTILS.MetersToNM(_closest.distance)) + else + _distance = string.format("%.1fkm",_closest.distance) + end + local _msg = string.format("%s - Popping signal flare at your %s o\'clock. Distance %s", _unitName, _clockDir, _distance) + self:_DisplayMessageToSAR(_heli, _msg, self.messageTime, false, true) + + local _coord = _closest.pilot:GetCoordinate() + _coord:FlareRed(_clockDir) + else + local _distance = smokedist + if _SETTINGS:IsImperial() then + _distance = string.format("%.1fnm",UTILS.MetersToNM(smokedist)) + else + _distance = string.format("%.1fkm",smokedist/1000) + end + self:_DisplayMessageToSAR(_heli, string.format("No Pilots within %s",_distance), self.messageTime) + end + return self +end + +--- (Internal) Display info to all SAR groups. +-- @param #CSAR self +-- @param #string _message Message to display. +-- @param #number _side Coalition of message. +-- @param #number _messagetime How long to show. +function CSAR:_DisplayToAllSAR(_message, _side, _messagetime) + self:T(self.lid .. " _DisplayToAllSAR") + for _, _unitName in pairs(self.csarUnits) do + local _unit = self:_GetSARHeli(_unitName) + if _unit then + if not _messagetime then + self:_DisplayMessageToSAR(_unit, _message, _messagetime) + end + end + end + return self +end + +---(Internal) Request smoke at closest downed pilot. +--@param #CSAR self +--@param #string _unitName Name of the helicopter +function CSAR:_Reqsmoke( _unitName ) + self:T(self.lid .. " _Reqsmoke") + local _heli = self:_GetSARHeli(_unitName) + if _heli == nil then + return + end + local smokedist = 8000 + if smokedist < self.approachdist_far then smokedist = self.approachdist_far end + local _closest = self:_GetClosestDownedPilot(_heli) + if _closest ~= nil and _closest.pilot ~= nil and _closest.distance < smokedist then + local _clockDir = self:_GetClockDirection(_heli, _closest.pilot) + local _distance = 0 + if _SETTINGS:IsImperial() then + _distance = string.format("%.1fnm",UTILS.MetersToNM(_closest.distance)) + else + _distance = string.format("%.1fkm",_closest.distance) + end + local _msg = string.format("%s - Popping signal smoke at your %s o\'clock. Distance %s", _unitName, _clockDir, _distance) + self:_DisplayMessageToSAR(_heli, _msg, self.messageTime, false, true) + local _coord = _closest.pilot:GetCoordinate() + local color = self.smokecolor + _coord:Smoke(color) + else + local _distance = 0 + if _SETTINGS:IsImperial() then + _distance = string.format("%.1fnm",UTILS.MetersToNM(smokedist)) + else + _distance = string.format("%.1fkm",smokedist/1000) + end + self:_DisplayMessageToSAR(_heli, string.format("No Pilots within %s",_distance), self.messageTime) + end + return self +end + +--- (Internal) Determine distance to closest MASH. +-- @param #CSAR self +-- @param Wrapper.Unit#UNIT _heli Helicopter #UNIT +-- @retunr +function CSAR:_GetClosestMASH(_heli) + self:T(self.lid .. " _GetClosestMASH") + local _mashset = self.mash -- Core.Set#SET_GROUP + local _mashes = _mashset:GetSetObjects() -- #table + local _shortestDistance = -1 + local _distance = 0 + local _helicoord = _heli:GetCoordinate() + + local function GetCloseAirbase(coordinate,Coalition,Category) + + local a=coordinate:GetVec3() + local distmin=math.huge + local airbase=nil + for DCSairbaseID, DCSairbase in pairs(world.getAirbases(Coalition)) do + local b=DCSairbase:getPoint() + + local c=UTILS.VecSubstract(a,b) + local dist=UTILS.VecNorm(c) + + if dist 0 then + local PilotTable = self.downedPilots + for _,_pilot in pairs (PilotTable) do + self:T({_pilot}) + local pilot = _pilot -- #CSAR.DownedPilot + local group = pilot.group + local frequency = pilot.frequency or 0 -- thanks to @Thrud + if group and group:IsAlive() and frequency > 0 then + self:_AddBeaconToGroup(group,frequency) + end + end + end + return self +end + +--- (Internal) Helper function to count active downed pilots. +-- @param #CSAR self +-- @return #number Number of pilots in the field. +function CSAR:_CountActiveDownedPilots() + self:T(self.lid .. " _CountActiveDownedPilots") + local PilotsInFieldN = 0 + for _, _unitName in pairs(self.downedPilots) do + self:T({_unitName.desc}) + if _unitName.alive == true then + PilotsInFieldN = PilotsInFieldN + 1 + end + end + return PilotsInFieldN +end + +--- (Internal) Helper to decide if we're over max limit. +-- @param #CSAR self +-- @return #boolean True or false. +function CSAR:_ReachedPilotLimit() + self:T(self.lid .. " _ReachedPilotLimit") + local limit = self.maxdownedpilots + local islimited = self.limitmaxdownedpilots + local count = self:_CountActiveDownedPilots() + if islimited and (count >= limit) then + return true + else + return false + end +end + + ------------------------------ + --- FSM internal Functions --- + ------------------------------ + +--- (Internal) Function called after Start() event. +-- @param #CSAR self. +-- @param #string From From state. +-- @param #string Event Event triggered. +-- @param #string To To state. +function CSAR:onafterStart(From, Event, To) + self:T({From, Event, To}) + self:I(self.lid .. "Started.") + -- event handler + self:HandleEvent(EVENTS.Takeoff, self._EventHandler) + self:HandleEvent(EVENTS.Land, self._EventHandler) + self:HandleEvent(EVENTS.Ejection, self._EventHandler) + self:HandleEvent(EVENTS.PlayerEnterAircraft, self._EventHandler) + self:HandleEvent(EVENTS.PlayerEnterUnit, self._EventHandler) + self:HandleEvent(EVENTS.PilotDead, self._EventHandler) + if self.useprefix then + local prefixes = self.csarPrefix or {} + self.allheligroupset = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(prefixes):FilterCategoryHelicopter():FilterStart() + else + self.allheligroupset = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterCategoryHelicopter():FilterStart() + end + self:__Status(-10) + return self +end + +--- (Internal) Function called before Status() event. +-- @param #CSAR self +function CSAR:_CheckDownedPilotTable() + local pilots = self.downedPilots + for _,_entry in pairs (pilots) do + self:T("Checking for " .. _entry.name) + self:T({entry=_entry}) + local group = _entry.group + if not group:IsAlive() then + self:T("Group is dead") + if _entry.alive == true then + self:T("Switching .alive to false") + self:__KIA(1,_entry.desc) + self:_RemoveNameFromDownedPilots(_entry.name,true) + end + end + end + return self +end + +--- (Internal) Function called before Status() event. +-- @param #CSAR self. +-- @param #string From From state. +-- @param #string Event Event triggered. +-- @param #string To To state. +function CSAR:onbeforeStatus(From, Event, To) + self:T({From, Event, To}) + -- housekeeping + self:_AddMedevacMenuItem() + self:_RefreshRadioBeacons() + self:_CheckDownedPilotTable() + for _,_sar in pairs (self.csarUnits) do + local PilotTable = self.downedPilots + for _,_entry in pairs (PilotTable) do + if _entry.alive then + local entry = _entry -- #CSAR.DownedPilot + local name = entry.name + local timestamp = entry.timestamp or 0 + local now = timer.getAbsTime() + if now - timestamp > 17 then -- only check if we\'re not in approach mode, which is iterations of 5 and 10. + self:_CheckWoundedGroupStatus(_sar,name) + end + end + end + end + return self +end + +--- (Internal) Function called after Status() event. +-- @param #CSAR self. +-- @param #string From From state. +-- @param #string Event Event triggered. +-- @param #string To To state. +function CSAR:onafterStatus(From, Event, To) + self:T({From, Event, To}) + -- collect some stats + local NumberOfSARPilots = 0 + for _, _unitName in pairs(self.csarUnits) do + NumberOfSARPilots = NumberOfSARPilots + 1 + end + + local PilotsInFieldN = self:_CountActiveDownedPilots() + + local PilotsBoarded = 0 + for _, _unitName in pairs(self.inTransitGroups) do + for _,_units in pairs(_unitName) do + PilotsBoarded = PilotsBoarded + 1 + end + end + + if self.verbose > 0 then + local text = string.format("%s Active SAR: %d | Downed Pilots in field: %d (max %d) | Pilots boarded: %d | Landings: %d | Pilots rescued: %d", + self.lid,NumberOfSARPilots,PilotsInFieldN,self.maxdownedpilots,PilotsBoarded,self.rescues,self.rescuedpilots) + self:T(text) + if self.verbose < 2 then + self:I(text) + elseif self.verbose > 1 then + self:I(text) + local m = MESSAGE:New(text,"10","Status",true):ToCoalition(self.coalition) + end + end + self:__Status(-20) + return self +end + +--- (Internal) Function called after Stop() event. +-- @param #CSAR self. +-- @param #string From From state. +-- @param #string Event Event triggered. +-- @param #string To To state. +function CSAR:onafterStop(From, Event, To) + self:T({From, Event, To}) + -- event handler + self:UnHandleEvent(EVENTS.Takeoff) + self:UnHandleEvent(EVENTS.Land) + self:UnHandleEvent(EVENTS.Ejection) + self:UnHandleEvent(EVENTS.PlayerEnterUnit) + self:UnHandleEvent(EVENTS.PlayerEnterAircraft) + self:UnHandleEvent(EVENTS.PilotDead) + self:T(self.lid .. "Stopped.") + return self +end + +--- (Internal) Function called before Approach() event. +-- @param #CSAR self. +-- @param #string From From state. +-- @param #string Event Event triggered. +-- @param #string To To state. +-- @param #string Heliname Name of the helicopter group. +-- @param #string Woundedgroupname Name of the downed pilot\'s group. +function CSAR:onbeforeApproach(From, Event, To, Heliname, Woundedgroupname) + self:T({From, Event, To, Heliname, Woundedgroupname}) + self:_CheckWoundedGroupStatus(Heliname,Woundedgroupname) + return self +end + +--- (Internal) Function called before Boarded() event. +-- @param #CSAR self. +-- @param #string From From state. +-- @param #string Event Event triggered. +-- @param #string To To state. +-- @param #string Heliname Name of the helicopter group. +-- @param #string Woundedgroupname Name of the downed pilot\'s group. +function CSAR:onbeforeBoarded(From, Event, To, Heliname, Woundedgroupname) + self:T({From, Event, To, Heliname, Woundedgroupname}) + self:_ScheduledSARFlight(Heliname,Woundedgroupname) + return self +end + +--- (Internal) Function called before Returning() event. +-- @param #CSAR self. +-- @param #string From From state. +-- @param #string Event Event triggered. +-- @param #string To To state. +-- @param #string Heliname Name of the helicopter group. +-- @param #string Woundedgroupname Name of the downed pilot\'s group. +function CSAR:onbeforeReturning(From, Event, To, Heliname, Woundedgroupname) + self:T({From, Event, To, Heliname, Woundedgroupname}) + self:_ScheduledSARFlight(Heliname,Woundedgroupname) + return self +end + +--- (Internal) Function called before Rescued() event. +-- @param #CSAR self. +-- @param #string From From state. +-- @param #string Event Event triggered. +-- @param #string To To state. +-- @param Wrapper.Unit#UNIT HeliUnit Unit of the helicopter. +-- @param #string HeliName Name of the helicopter group. +-- @param #number PilotsSaved Number of the saved pilots on board when landing. +function CSAR:onbeforeRescued(From, Event, To, HeliUnit, HeliName, PilotsSaved) + self:T({From, Event, To, HeliName, HeliUnit}) + self.rescues = self.rescues + 1 + self.rescuedpilots = self.rescuedpilots + PilotsSaved + return self +end + +--- (Internal) Function called before PilotDown() event. +-- @param #CSAR self. +-- @param #string From From state. +-- @param #string Event Event triggered. +-- @param #string To To state. +-- @param Wrapper.Group#GROUP Group Group object of the downed pilot. +-- @param #number Frequency Beacon frequency in kHz. +-- @param #string Leadername Name of the #UNIT of the downed pilot. +-- @param #string CoordinatesText String of the position of the pilot. Format determined by self.coordtype. +function CSAR:onbeforePilotDown(From, Event, To, Group, Frequency, Leadername, CoordinatesText) + self:T({From, Event, To, Group, Frequency, Leadername, CoordinatesText}) + return self +end +-------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- End Ops.CSAR +-------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua new file mode 100644 index 000000000..3ab505402 --- /dev/null +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -0,0 +1,2806 @@ +--- **Ops** -- Combat Troops & Logistics Department. +-- +-- === +-- +-- **CTLD** - MOOSE based Helicopter CTLD Operations. +-- +-- === +-- +-- ## Missions: +-- +-- ### [CTLD - Combat Troop & Logistics Deployment](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20CTLD) +-- +-- === +-- +-- **Main Features:** +-- +-- * MOOSE-based Helicopter CTLD Operations for Players. +-- +-- === +-- +-- ### Author: **Applevangelist** (Moose Version), ***Ciribob*** (original), Thanks to: Shadowze, Cammel (testing) +-- @module Ops.CTLD +-- @image OPS_CTLD.jpg + +-- Date: July 2021 + +do +------------------------------------------------------ +--- **CTLD_CARGO** class, extends #Core.Base#BASE +-- @type CTLD_CARGO +-- @field #number ID ID of this cargo. +-- @field #string Name Name for menu. +-- @field #table Templates Table of #POSITIONABLE objects. +-- @field #CTLD_CARGO.Enum Type Enumerator of Type. +-- @field #boolean HasBeenMoved Flag for moving. +-- @field #boolean LoadDirectly Flag for direct loading. +-- @field #number CratesNeeded Crates needed to build. +-- @field Wrapper.Positionable#POSITIONABLE Positionable Representation of cargo in the mission. +-- @field #boolean HasBeenDropped True if dropped from heli. +-- @extends Core.Fsm#FSM +CTLD_CARGO = { + ClassName = "CTLD_CARGO", + ID = 0, + Name = "none", + Templates = {}, + CargoType = "none", + HasBeenMoved = false, + LoadDirectly = false, + CratesNeeded = 0, + Positionable = nil, + HasBeenDropped = false, + } + + --- Define cargo types. + -- @type CTLD_CARGO.Enum + -- @field #string Type Type of Cargo. + CTLD_CARGO.Enum = { + ["VEHICLE"] = "Vehicle", -- #string vehicles + ["TROOPS"] = "Troops", -- #string troops + ["FOB"] = "FOB", -- #string FOB + ["CRATE"] = "Crate", -- #string crate + ["REPAIR"] = "Repair", -- #string repair + } + + --- Function to create new CTLD_CARGO object. + -- @param #CTLD_CARGO self + -- @param #number ID ID of this #CTLD_CARGO + -- @param #string Name Name for menu. + -- @param #table Templates Table of #POSITIONABLE objects. + -- @param #CTLD_CARGO.Enum Sorte Enumerator of Type. + -- @param #boolean HasBeenMoved Flag for moving. + -- @param #boolean LoadDirectly Flag for direct loading. + -- @param #number CratesNeeded Crates needed to build. + -- @param Wrapper.Positionable#POSITIONABLE Positionable Representation of cargo in the mission. + -- @param #boolean Dropped Cargo/Troops have been unloaded from a chopper. + -- @return #CTLD_CARGO self + function CTLD_CARGO:New(ID, Name, Templates, Sorte, HasBeenMoved, LoadDirectly, CratesNeeded, Positionable, Dropped) + -- Inherit everything from BASE class. + local self=BASE:Inherit(self, BASE:New()) -- #CTLD + self:T({ID, Name, Templates, Sorte, HasBeenMoved, LoadDirectly, CratesNeeded, Positionable, Dropped}) + self.ID = ID or math.random(100000,1000000) + self.Name = Name or "none" -- #string + self.Templates = Templates or {} -- #table + self.CargoType = Sorte or "type" -- #CTLD_CARGO.Enum + self.HasBeenMoved = HasBeenMoved or false -- #boolean + self.LoadDirectly = LoadDirectly or false -- #boolean + self.CratesNeeded = CratesNeeded or 0 -- #number + self.Positionable = Positionable or nil -- Wrapper.Positionable#POSITIONABLE + self.HasBeenDropped = Dropped or false --#boolean + return self + end + + --- Query ID. + -- @param #CTLD_CARGO self + -- @return #number ID + function CTLD_CARGO:GetID() + return self.ID + end + + --- Query Name. + -- @param #CTLD_CARGO self + -- @return #string Name + function CTLD_CARGO:GetName() + return self.Name + end + + --- Query Templates. + -- @param #CTLD_CARGO self + -- @return #table Templates + function CTLD_CARGO:GetTemplates() + return self.Templates + end + + --- Query has moved. + -- @param #CTLD_CARGO self + -- @return #boolean Has moved + function CTLD_CARGO:HasMoved() + return self.HasBeenMoved + end + + --- Query was dropped. + -- @param #CTLD_CARGO self + -- @return #boolean Has been dropped. + function CTLD_CARGO:WasDropped() + return self.HasBeenDropped + end + + --- Query directly loadable. + -- @param #CTLD_CARGO self + -- @return #boolean loadable + function CTLD_CARGO:CanLoadDirectly() + return self.LoadDirectly + end + + --- Query number of crates or troopsize. + -- @param #CTLD_CARGO self + -- @return #number Crates or size of troops. + function CTLD_CARGO:GetCratesNeeded() + return self.CratesNeeded + end + + --- Query type. + -- @param #CTLD_CARGO self + -- @return #CTLD_CARGO.Enum Type + function CTLD_CARGO:GetType() + return self.CargoType + end + + --- Query type. + -- @param #CTLD_CARGO self + -- @return Wrapper.Positionable#POSITIONABLE Positionable + function CTLD_CARGO:GetPositionable() + return self.Positionable + end + + --- Set HasMoved. + -- @param #CTLD_CARGO self + -- @param #boolean moved + function CTLD_CARGO:SetHasMoved(moved) + self.HasBeenMoved = moved or false + end + + --- Query if cargo has been loaded. + -- @param #CTLD_CARGO self + -- @param #boolean loaded + function CTLD_CARGO:Isloaded() + if self.HasBeenMoved and not self.WasDropped() then + return true + else + return false + end + end + + --- Set WasDropped. + -- @param #CTLD_CARGO self + -- @param #boolean dropped + function CTLD_CARGO:SetWasDropped(dropped) + self.HasBeenDropped = dropped or false + end + + --- Query crate type for REPAIR + -- @param #CTLD_CARGO self + -- @param #boolean + function CTLD_CARGO:IsRepair() + if self.CargoType == "Repair" then + return true + else + return false + end + end + +end + +do +------------------------------------------------------------------------- +--- **CTLD** class, extends Core.Base#BASE, Core.Fsm#FSM +-- @type CTLD +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity level. +-- @field #string lid Class id string for output to DCS log file. +-- @field #number coalition Coalition side number, e.g. `coalition.side.RED`. +-- @extends Core.Fsm#FSM + +--- *Combat Troop & Logistics Deployment (CTLD): Everyone wants to be a POG, until there\'s POG stuff to be done.* (Mil Saying) +-- +-- === +-- +-- ![Banner Image](OPS_CTLD.jpg) +-- +-- # CTLD Concept +-- +-- * MOOSE-based CTLD for Players. +-- * Object oriented refactoring of Ciribob\'s fantastic CTLD script. +-- * No need for extra MIST loading. +-- * Additional events to tailor your mission. +-- * ANY late activated group can serve as cargo, either as troops or crates, which have to be build on-location. +-- +-- ## 0. Prerequisites +-- +-- You need to load an .ogg soundfile for the pilot\'s beacons into the mission, e.g. "beacon.ogg", use a once trigger, "sound to country" for that. +-- Create the late-activated troops, vehicles (no statics at this point!) that will make up your deployable forces. +-- +-- ## 1. Basic Setup +-- +-- ## 1.1 Create and start a CTLD instance +-- +-- A basic setup example is the following: +-- +-- -- Instantiate and start a CTLD for the blue side, using helicopter groups named "Helicargo" and alias "Lufttransportbrigade I" +-- local my_ctld = CTLD:New(coalition.side.BLUE,{"Helicargo"},"Lufttransportbrigade I") +-- my_ctld:__Start(5) +-- +-- ## 1.2 Add cargo types available +-- +-- Add *generic* cargo types that you need for your missions, here infantry units, vehicles and a FOB. These need to be late-activated Wrapper.Group#GROUP objects: +-- +-- -- add infantry unit called "Anti-Tank Small" using template "ATS", of type TROOP with size 3 +-- -- infantry units will be loaded directly from LOAD zones into the heli (matching number of free seats needed) +-- my_ctld:AddTroopsCargo("Anti-Tank Small",{"ATS"},CTLD_CARGO.Enum.TROOPS,3) +-- +-- -- add infantry unit called "Anti-Tank" using templates "AA" and "AA"", of type TROOP with size 4 +-- my_ctld:AddTroopsCargo("Anti-Air",{"AA","AA2"},CTLD_CARGO.Enum.TROOPS,4) +-- +-- -- add vehicle called "Humvee" using template "Humvee", of type VEHICLE, size 2, i.e. needs two crates to be build +-- -- vehicles and FOB will be spawned as crates in a LOAD zone first. Once transported to DROP zones, they can be build into the objects +-- my_ctld:AddCratesCargo("Humvee",{"Humvee"},CTLD_CARGO.Enum.VEHICLE,2) +-- +-- -- add infantry unit called "Forward Ops Base" using template "FOB", of type FOB, size 4, i.e. needs four crates to be build: +-- my_ctld:AddCratesCargo("Forward Ops Base",{"FOB"},CTLD_CARGO.Enum.FOB,4) +-- +-- -- add crates to repair FOB or VEHICLE type units - the 2nd parameter needs to match the template you want to repair +-- my_ctld:AddCratesRepair("Humvee Repair","Humvee",CTLD_CARGO.Enum.REPAIR,1) +-- +-- ## 1.3 Add logistics zones +-- +-- Add zones for loading troops and crates and dropping, building crates +-- +-- -- Add a zone of type LOAD to our setup. Players can load troops and crates. +-- -- "Loadzone" is the name of the zone from the ME. Players can load, if they are inside of the zone. +-- -- Smoke and Flare color for this zone is blue, it is active (can be used) and has a radio beacon. +-- my_ctld:AddCTLDZone("Loadzone",CTLD.CargoZoneType.LOAD,SMOKECOLOR.Blue,true,true) +-- +-- -- Add a zone of type DROP. Players can drop crates here. +-- -- Smoke and Flare color for this zone is blue, it is active (can be used) and has a radio beacon. +-- -- NOTE: Troops can be unloaded anywhere, also when hovering in parameters. +-- my_ctld:AddCTLDZone("Dropzone",CTLD.CargoZoneType.DROP,SMOKECOLOR.Red,true,true) +-- +-- -- Add two zones of type MOVE. Dropped troops and vehicles will move to the nearest one. See options. +-- -- Smoke and Flare color for this zone is blue, it is active (can be used) and has a radio beacon. +-- my_ctld:AddCTLDZone("Movezone",CTLD.CargoZoneType.MOVE,SMOKECOLOR.Orange,false,false) +-- +-- my_ctld:AddCTLDZone("Movezone2",CTLD.CargoZoneType.MOVE,SMOKECOLOR.White,true,true) +-- +-- +-- ## 2. Options +-- +-- The following options are available (with their defaults). Only set the ones you want changed: +-- +-- my_ctld.useprefix = true -- (DO NOT SWITCH THIS OFF UNLESS YOU KNOW WHAT YOU ARE DOING!) Adjust **before** starting CTLD. If set to false, *all* choppers of the coalition side will be enabled for CTLD. +-- my_ctld.CrateDistance = 30 -- List and Load crates in this radius only. +-- my_ctld.dropcratesanywhere = false -- Option to allow crates to be dropped anywhere. +-- my_ctld.maximumHoverHeight = 15 -- Hover max this high to load. +-- my_ctld.minimumHoverHeight = 4 -- Hover min this low to load. +-- my_ctld.forcehoverload = true -- Crates (not: troops) can only be loaded while hovering. +-- my_ctld.hoverautoloading = true -- Crates in CrateDistance in a LOAD zone will be loaded automatically if space allows. +-- my_ctld.smokedistance = 2000 -- Smoke or flares can be request for zones this far away (in meters). +-- my_ctld.movetroopstowpzone = true -- Troops and vehicles will move to the nearest MOVE zone... +-- my_ctld.movetroopsdistance = 5000 -- .. but only if this far away (in meters) +-- my_ctld.smokedistance = 2000 -- Only smoke or flare zones if requesting player unit is this far away (in meters) +-- my_ctld.suppressmessages = false -- Set to true if you want to script your own messages. +-- my_ctld.repairtime = 300 -- Number of seconds it takes to repair a unit. +-- +-- ## 2.1 User functions +-- +-- ### 2.1.1 Adjust or add chopper unit-type capabilities +-- +-- Use this function to adjust what a heli type can or cannot do: +-- +-- -- E.g. update unit capabilities for testing. Please stay realistic in your mission design. +-- -- Make a Gazelle into a heavy truck, this type can load both crates and troops and eight of each type: +-- my_ctld:UnitCapabilities("SA342L", true, true, 8, 8) +-- +-- Default unit type capabilities are: +-- +-- ["SA342Mistral"] = {type="SA342Mistral", crates=false, troops=true, cratelimit = 0, trooplimit = 4}, +-- ["SA342L"] = {type="SA342L", crates=false, troops=true, cratelimit = 0, trooplimit = 2}, +-- ["SA342M"] = {type="SA342M", crates=false, troops=true, cratelimit = 0, trooplimit = 4}, +-- ["SA342Minigun"] = {type="SA342Minigun", crates=false, troops=true, cratelimit = 0, trooplimit = 2}, +-- ["UH-1H"] = {type="UH-1H", crates=true, troops=true, cratelimit = 1, trooplimit = 8}, +-- ["Mi-8MT"] = {type="Mi-8MT", crates=true, troops=true, cratelimit = 2, trooplimit = 12}, +-- ["Ka-50"] = {type="Ka-50", crates=false, troops=false, cratelimit = 0, trooplimit = 0}, +-- ["Mi-24P"] = {type="Mi-24P", crates=true, troops=true, cratelimit = 1, trooplimit = 8}, +-- ["Mi-24V"] = {type="Mi-24V", crates=true, troops=true, cratelimit = 1, trooplimit = 8}, +-- +-- +-- ### 2.1.2 Activate and deactivate zones +-- +-- Activate a zone: +-- +-- -- Activate zone called Name of type #CTLD.CargoZoneType ZoneType: +-- my_ctld:ActivateZone(Name,CTLD.CargoZoneType.MOVE) +-- +-- Deactivate a zone: +-- +-- -- Deactivate zone called Name of type #CTLD.CargoZoneType ZoneType: +-- my_ctld:DeactivateZone(Name,CTLD.CargoZoneType.DROP) +-- +-- ## 3. Events +-- +-- The class comes with a number of FSM-based events that missions designers can use to shape their mission. +-- These are: +-- +-- ## 3.1 OnAfterTroopsPickedUp +-- +-- This function is called when a player has loaded Troops: +-- +-- function my_ctld:OnAfterTroopsPickedUp(From, Event, To, Group, Unit, Cargo) +-- ... your code here ... +-- end +-- +-- ## 3.2 OnAfterCratesPickedUp +-- +-- This function is called when a player has picked up crates: +-- +-- function my_ctld:OnAfterCratesPickedUp(From, Event, To, Group, Unit, Cargo) +-- ... your code here ... +-- end +-- +-- ## 3.3 OnAfterTroopsDeployed +-- +-- This function is called when a player has deployed troops into the field: +-- +-- function my_ctld:OnAfterTroopsDeployed(From, Event, To, Group, Unit, Troops) +-- ... your code here ... +-- end +-- +-- ## 3.4 OnAfterTroopsExtracted +-- +-- This function is called when a player has re-boarded already deployed troops from the field: +-- +-- function my_ctld:OnAfterTroopsExtracted(From, Event, To, Group, Unit, Troops) +-- ... your code here ... +-- end +-- +-- ## 3.5 OnAfterCratesDropped +-- +-- This function is called when a player has deployed crates to a DROP zone: +-- +-- function my_ctld:OnAfterCratesDropped(From, Event, To, Group, Unit, Cargotable) +-- ... your code here ... +-- end +-- +-- ## 3.6 OnAfterCratesBuild, OnAfterCratesRepaired +-- +-- This function is called when a player has build a vehicle or FOB: +-- +-- function my_ctld:OnAfterCratesBuild(From, Event, To, Group, Unit, Vehicle) +-- ... your code here ... +-- end +-- +-- function my_ctld:OnAfterCratesRepaired(From, Event, To, Group, Unit, Vehicle) +-- ... your code here ... +-- end + -- +-- ## 3.7 A simple SCORING example: +-- +-- To award player with points, using the SCORING Class (SCORING: my_Scoring, CTLD: CTLD_Cargotransport) +-- +-- function CTLD_Cargotransport:OnAfterCratesDropped(From, Event, To, Group, Unit, Cargotable) +-- local points = 10 +-- local PlayerName = Unit:GetPlayerName() +-- my_scoring:_AddPlayerFromUnit( Unit ) +-- my_scoring:AddGoalScore(Unit, "CTLD", string.format("Pilot %s has been awarded %d points for transporting cargo crates!", PlayerName, points), points) +-- end +-- +-- function CTLD_Cargotransport:OnAfterCratesBuild(From, Event, To, Group, Unit, Vehicle) +-- local points = 5 +-- local PlayerName = Unit:GetPlayerName() +-- my_scoring:_AddPlayerFromUnit( Unit ) +-- my_scoring:AddGoalScore(Unit, "CTLD", string.format("Pilot %s has been awarded %d points for the construction of Units!", PlayerName, points), points) +-- end +-- +-- ## 4. F10 Menu structure +-- +-- CTLD management menu is under the F10 top menu and called "CTLD" +-- +-- ## 4.1 Manage Crates +-- +-- Use this entry to get, load, list nearby, drop, build and repair crates. Also @see options. +-- +-- ## 4.2 Manage Troops +-- +-- Use this entry to load, drop and extract troops. NOTE - with extract you can only load troops from the field that were deployed prior. +-- Currently limited CTLD_CARGO troops, which are build from **one** template. Also, this will heal/complete your units as they are respawned. +-- +-- ## 4.3 List boarded cargo +-- +-- Lists what you have loaded. Shows load capabilities for number of crates and number of seats for troops. +-- +-- ## 4.4 Smoke & Flare zones nearby +-- +-- Does what it says. +-- +-- ## 4.5 List active zone beacons +-- +-- Lists active radio beacons for all zones, where zones are both active and have a beacon. @see `CTLD:AddCTLDZone()` +-- +-- ## 4.6 Show hover parameters +-- +-- Lists hover parameters and indicates if these are curently fulfilled. Also @see options on hover heights. +-- +-- ## 5. Support for Hercules mod by Anubis +-- +-- Basic support for the Hercules mod By Anubis has been build into CTLD. Currently this does **not** cover objects and troops which can +-- be loaded from the Rearm/Refuel menu, i.e. you can drop them into the field, but you cannot use them in functions scripted with this class. +-- +-- local my_ctld = CTLD:New(coalition.side.BLUE,{"Helicargo", "Hercules"},"Lufttransportbrigade I") +-- +-- Enable these options for Hercules support: +-- +-- my_ctld.enableHercules = true +-- my_ctld.HercMinAngels = 155 -- for troop/cargo drop via chute in meters, ca 470 ft +-- my_ctld.HercMaxAngels = 2000 -- for troop/cargo drop via chute in meters, ca 6000 ft +-- my_ctld.HercMaxSpeed = 77 -- 77mps or 270 kph or 150 kn +-- +-- Also, the following options need to be set to `true`: +-- +-- my_ctld.useprefix = true -- this is true by default and MUST BE ON. +-- +-- Standard transport capabilities as per the real Hercules are: +-- +-- ["Hercules"] = {type="Hercules", crates=true, troops=true, cratelimit = 7, trooplimit = 64}, -- 19t cargo, 64 paratroopers +-- +-- @field #CTLD +CTLD = { + ClassName = "CTLD", + verbose = 0, + lid = "", + coalition = 1, + coalitiontxt = "blue", + PilotGroups = {}, -- #GROUP_SET of heli pilots + CtldUnits = {}, -- Table of helicopter #GROUPs + FreeVHFFrequencies = {}, -- Table of VHF + FreeUHFFrequencies = {}, -- Table of UHF + FreeFMFrequencies = {}, -- Table of FM + CargoCounter = 0, + dropOffZones = {}, + wpZones = {}, + Cargo_Troops = {}, -- generic troops objects + Cargo_Crates = {}, -- generic crate objects + Loaded_Cargo = {}, -- cargo aboard units + Spawned_Crates = {}, -- Holds objects for crates spawned generally + Spawned_Cargo = {}, -- Binds together spawned_crates and their CTLD_CARGO objects + CrateDistance = 30, -- list crates in this radius + debug = false, + wpZones = {}, + pickupZones = {}, + dropOffZones = {}, +} + +------------------------------ +-- DONE: Zone Checks +-- DONE: TEST Hover load and unload +-- DONE: Crate unload +-- DONE: Hover (auto-)load +-- TODO: (More) Housekeeping +-- DONE: Troops running to WP Zone +-- DONE: Zone Radio Beacons +-- DONE: Stats Running +-- DONE: Added support for Hercules +-- TODO: Possibly - either/or loading crates and troops +-- TODO: Limit of troops, crates buildable? +------------------------------ + +--- Radio Beacons +-- @type CTLD.ZoneBeacon +-- @field #string name -- Name of zone for the coordinate +-- @field #number frequency -- in mHz +-- @field #number modulation -- i.e.radio.modulation.FM or radio.modulation.AM + +--- Zone Info. +-- @type CTLD.CargoZone +-- @field #string name Name of Zone. +-- @field #string color Smoke color for zone, e.g. SMOKECOLOR.Red. +-- @field #boolean active Active or not. +-- @field #string type Type of zone, i.e. load,drop,move +-- @field #boolean hasbeacon Create and run radio beacons if active. +-- @field #table fmbeacon Beacon info as #CTLD.ZoneBeacon +-- @field #table uhfbeacon Beacon info as #CTLD.ZoneBeacon +-- @field #table vhfbeacon Beacon info as #CTLD.ZoneBeacon + +--- Zone Type Info. +-- @type CTLD.CargoZoneType +CTLD.CargoZoneType = { + LOAD = "load", + DROP = "drop", + MOVE = "move", +} + +--- Buildable table info. +-- @type CTLD.Buildable +-- @field #string Name Name of the object. +-- @field #number Required Required crates. +-- @field #number Found Found crates. +-- @field #table Template Template names for this build. +-- @field #boolean CanBuild Is buildable or not. +-- @field #CTLD_CARGO.Enum Type Type enumerator (for moves). + +--- Unit capabilities. +-- @type CTLD.UnitCapabilities +-- @field #string type Unit type. +-- @field #boolean crates Can transport crate. +-- @field #boolean troops Can transport troops. +-- @field #number cratelimit Number of crates transportable. +-- @field #number trooplimit Number of troop units transportable. +CTLD.UnitTypes = { + ["SA342Mistral"] = {type="SA342Mistral", crates=false, troops=true, cratelimit = 0, trooplimit = 4}, + ["SA342L"] = {type="SA342L", crates=false, troops=true, cratelimit = 0, trooplimit = 2}, + ["SA342M"] = {type="SA342M", crates=false, troops=true, cratelimit = 0, trooplimit = 4}, + ["SA342Minigun"] = {type="SA342Minigun", crates=false, troops=true, cratelimit = 0, trooplimit = 2}, + ["UH-1H"] = {type="UH-1H", crates=true, troops=true, cratelimit = 1, trooplimit = 8}, + ["Mi-8MTV2"] = {type="Mi-8MTV2", crates=true, troops=true, cratelimit = 2, trooplimit = 12}, + ["Mi-8MT"] = {type="Mi-8MTV2", crates=true, troops=true, cratelimit = 2, trooplimit = 12}, + ["Ka-50"] = {type="Ka-50", crates=false, troops=false, cratelimit = 0, trooplimit = 0}, + ["Mi-24P"] = {type="Mi-24P", crates=true, troops=true, cratelimit = 2, trooplimit = 8}, + ["Mi-24V"] = {type="Mi-24V", crates=true, troops=true, cratelimit = 2, trooplimit = 8}, + ["Hercules"] = {type="Hercules", crates=true, troops=true, cratelimit = 7, trooplimit = 64}, -- 19t cargo, 64 paratroopers +} + +--- CTLD class version. +-- @field #string version +CTLD.version="0.1.4r3" + +--- Instantiate a new CTLD. +-- @param #CTLD self +-- @param #string Coalition Coalition of this CTLD. I.e. coalition.side.BLUE or coalition.side.RED or coalition.side.NEUTRAL +-- @param #table Prefixes Table of pilot prefixes. +-- @param #string Alias Alias of this CTLD for logging. +-- @return #CTLD self +function CTLD:New(Coalition, Prefixes, Alias) + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #CTLD + + BASE:T({Coalition, Prefixes, Alias}) + + --set Coalition + if Coalition and type(Coalition)=="string" then + if Coalition=="blue" then + self.coalition=coalition.side.BLUE + self.coalitiontxt = Coalition + elseif Coalition=="red" then + self.coalition=coalition.side.RED + self.coalitiontxt = Coalition + elseif Coalition=="neutral" then + self.coalition=coalition.side.NEUTRAL + self.coalitiontxt = Coalition + else + self:E("ERROR: Unknown coalition in CTLD!") + end + else + self.coalition = Coalition + self.coalitiontxt = string.lower(UTILS.GetCoalitionName(self.coalition)) + end + + -- Set alias. + if Alias then + self.alias=tostring(Alias) + else + self.alias="UNHCR" + if self.coalition then + if self.coalition==coalition.side.RED then + self.alias="Red CTLD" + elseif self.coalition==coalition.side.BLUE then + self.alias="Blue CTLD" + end + end + end + + -- Set some string id for output to DCS.log file. + self.lid=string.format("%s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown") + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start FSM. + self:AddTransition("*", "Status", "*") -- CTLD status update. + self:AddTransition("*", "TroopsPickedUp", "*") -- CTLD pickup event. + self:AddTransition("*", "TroopsExtracted", "*") -- CTLD extract event. + self:AddTransition("*", "CratesPickedUp", "*") -- CTLD pickup event. + self:AddTransition("*", "TroopsDeployed", "*") -- CTLD deploy event. + self:AddTransition("*", "TroopsRTB", "*") -- CTLD deploy event. + self:AddTransition("*", "CratesDropped", "*") -- CTLD deploy event. + self:AddTransition("*", "CratesBuild", "*") -- CTLD build event. + self:AddTransition("*", "CratesRepaired", "*") -- CTLD repair event. + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + + -- tables + self.PilotGroups ={} + self.CtldUnits = {} + + -- Beacons + self.FreeVHFFrequencies = {} + self.FreeUHFFrequencies = {} + self.FreeFMFrequencies = {} + self.UsedVHFFrequencies = {} + self.UsedUHFFrequencies = {} + self.UsedFMFrequencies = {} + + -- radio beacons + self.RadioSound = "beacon.ogg" + + -- zones stuff + self.pickupZones = {} + self.dropOffZones = {} + self.wpZones = {} + + -- Cargo + self.Cargo_Crates = {} + self.Cargo_Troops = {} + self.Loaded_Cargo = {} + self.Spawned_Crates = {} + self.Spawned_Cargo = {} + self.MenusDone = {} + self.DroppedTroops = {} + self.DroppedCrates = {} + self.CargoCounter = 0 + self.CrateCounter = 0 + self.TroopCounter = 0 + + -- setup + self.CrateDistance = 30 -- list/load crates in this radius + self.prefixes = Prefixes or {"Cargoheli"} + --self.I({prefixes = self.prefixes}) + self.useprefix = true + + self.maximumHoverHeight = 15 + self.minimumHoverHeight = 4 + self.forcehoverload = true + self.hoverautoloading = true + self.dropcratesanywhere = false -- #1570 + + self.smokedistance = 2000 + self.movetroopstowpzone = true + self.movetroopsdistance = 5000 + + -- added support Hercules Mod + self.enableHercules = false + self.HercMinAngels = 165 -- for troop/cargo drop via chute + self.HercMaxAngels = 2000 -- for troop/cargo drop via chute + self.HercMaxSpeed = 77 -- 280 kph or 150kn eq 77 mps + + -- message suppression + self.suppressmessages = false + + -- time to repair a unit/group + self.repairtime = 300 + + for i=1,100 do + math.random() + end + + self:_GenerateVHFrequencies() + self:_GenerateUHFrequencies() + self:_GenerateFMFrequencies() + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the CTLD. Initializes parameters and starts event handlers. + -- @function [parent=#CTLD] Start + -- @param #CTLD self + + --- Triggers the FSM event "Start" after a delay. Starts the CTLD. Initializes parameters and starts event handlers. + -- @function [parent=#CTLD] __Start + -- @param #CTLD self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the CTLD and all its event handlers. + -- @param #CTLD self + + --- Triggers the FSM event "Stop" after a delay. Stops the CTLD and all its event handlers. + -- @function [parent=#CTLD] __Stop + -- @param #CTLD self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Status". + -- @function [parent=#CTLD] Status + -- @param #CTLD self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#CTLD] __Status + -- @param #CTLD self + -- @param #number delay Delay in seconds. + + --- FSM Function OnAfterTroopsPickedUp. + -- @function [parent=#CTLD] OnAfterTroopsPickedUp + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param #CTLD_CARGO Cargo Cargo troops. + -- @return #CTLD self + + --- FSM Function OnAfterTroopsExtracted. + -- @function [parent=#CTLD] OnAfterTroopsExtracted + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param #CTLD_CARGO Cargo Cargo troops. + -- @return #CTLD self + + --- FSM Function OnAfterCratesPickedUp. + -- @function [parent=#CTLD] OnAfterCratesPickedUp + -- @param #CTLD self + -- @param #string From State . + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param #CTLD_CARGO Cargo Cargo crate. + -- @return #CTLD self + + --- FSM Function OnAfterTroopsDeployed. + -- @function [parent=#CTLD] OnAfterTroopsDeployed + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param Wrapper.Group#GROUP Troops Troops #GROUP Object. + -- @return #CTLD self + + --- FSM Function OnAfterCratesDropped. + -- @function [parent=#CTLD] OnAfterCratesDropped + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param #table Cargotable Table of #CTLD_CARGO objects dropped. + -- @return #CTLD self + + --- FSM Function OnAfterCratesBuild. + -- @function [parent=#CTLD] OnAfterCratesBuild + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param Wrapper.Group#GROUP Vehicle The #GROUP object of the vehicle or FOB build. + -- @return #CTLD self + + --- FSM Function OnAfterCratesRepaired. + -- @function [parent=#CTLD] OnAfterCratesRepaired + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param Wrapper.Group#GROUP Vehicle The #GROUP object of the vehicle or FOB repaired. + -- @return #CTLD self + + --- FSM Function OnAfterTroopsRTB. + -- @function [parent=#CTLD] OnAfterTroopsRTB + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + + return self +end + +------------------------------------------------------------------- +-- Helper and User Functions +------------------------------------------------------------------- + +--- (Internal) Function to get capabilities of a chopper +-- @param #CTLD self +-- @param Wrapper.Unit#UNIT Unit The unit +-- @return #table Capabilities Table of caps +function CTLD:_GetUnitCapabilities(Unit) + self:T(self.lid .. " _GetUnitCapabilities") + local _unit = Unit -- Wrapper.Unit#UNIT + local unittype = _unit:GetTypeName() + local capabilities = self.UnitTypes[unittype] -- #CTLD.UnitCapabilities + if not capabilities or capabilities == {} then + -- e.g. ["Ka-50"] = {type="Ka-50", crates=false, troops=false, cratelimit = 0, trooplimit = 0}, + capabilities = {} + capabilities.troops = false + capabilities.crates = false + capabilities.cratelimit = 0 + capabilities.trooplimit = 0 + capabilities.type = "generic" + end + return capabilities +end + + +--- (Internal) Function to generate valid UHF Frequencies +-- @param #CTLD self +function CTLD:_GenerateUHFrequencies() + self:T(self.lid .. " _GenerateUHFrequencies") + self.FreeUHFFrequencies = {} + self.FreeUHFFrequencies = UTILS.GenerateUHFrequencies() + return self +end + +--- (Internal) Function to generate valid FM Frequencies +-- @param #CTLD self +function CTLD:_GenerateFMFrequencies() + self:T(self.lid .. " _GenerateFMrequencies") + self.FreeFMFrequencies = {} + self.FreeFMFrequencies = UTILS.GenerateFMFrequencies() + return self +end + +--- (Internal) Populate table with available VHF beacon frequencies. +-- @param #CTLD self +function CTLD:_GenerateVHFrequencies() + self:T(self.lid .. " _GenerateVHFrequencies") + self.FreeVHFFrequencies = {} + self.UsedVHFFrequencies = {} + self.FreeVHFFrequencies = UTILS.GenerateVHFrequencies() + return self +end + +--- (Internal) Event handler function +-- @param #CTLD self +-- @param Core.Event#EVENTDATA EventData +function CTLD:_EventHandler(EventData) + self:T(string.format("%s Event = %d",self.lid, EventData.id)) + local event = EventData -- Core.Event#EVENTDATA + if event.id == EVENTS.PlayerEnterAircraft or event.id == EVENTS.PlayerEnterUnit then + local _coalition = event.IniCoalition + if _coalition ~= self.coalition then + return --ignore! + end + -- check is Helicopter + local _unit = event.IniUnit + local _group = event.IniGroup + if _unit:IsHelicopter() or _group:IsHelicopter() then + local unitname = event.IniUnitName or "none" + self.Loaded_Cargo[unitname] = nil + self:_RefreshF10Menus() + end + -- Herc support + --self:T_unit:GetTypeName()) + if _unit:GetTypeName() == "Hercules" and self.enableHercules then + self.Loaded_Cargo[unitname] = nil + self:_RefreshF10Menus() + end + return + elseif event.id == EVENTS.PlayerLeaveUnit then + -- remove from pilot table + local unitname = event.IniUnitName or "none" + self.CtldUnits[unitname] = nil + self.Loaded_Cargo[unitname] = nil + end + return self +end + +--- (Internal) Function to message a group. +-- @param #CTLD self +-- @param #string Text The text to display. +-- @param #number Time Number of seconds to display the message. +-- @param #boolean Clearscreen Clear screen or not. +-- @param Wrapper.Group#GROUP Group The group receiving the message. +function CTLD:_SendMessage(Text, Time, Clearscreen, Group) + self:T(self.lid .. " _SendMessage") + if not self.suppressmessages then + local m = MESSAGE:New(Text,Time,"CTLD",Clearscreen):ToGroup(Group) + end + return self +end + +--- (Internal) Function to load troops into a heli. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Unit#UNIT Unit +-- @param #CTLD_CARGO Cargotype +function CTLD:_LoadTroops(Group, Unit, Cargotype) + self:T(self.lid .. " _LoadTroops") + -- landed or hovering over load zone? + local grounded = not self:IsUnitInAir(Unit) + local hoverload = self:CanHoverLoad(Unit) + -- check if we are in LOAD zone + local inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) + if not inzone then + self:_SendMessage("You are not close enough to a logistics zone!", 10, false, Group) + if not self.debug then return self end + elseif not grounded and not hoverload then + self:_SendMessage("You need to land or hover in position to load!", 10, false, Group) + if not self.debug then return self end + end + -- load troops into heli + local group = Group -- Wrapper.Group#GROUP + local unit = Unit -- Wrapper.Unit#UNIT + local unitname = unit:GetName() + local cargotype = Cargotype -- #CTLD_CARGO + local cratename = cargotype:GetName() -- #string + -- see if this heli can load troops + local unittype = unit:GetTypeName() + local capabilities = self:_GetUnitCapabilities(Unit) + local cantroops = capabilities.troops -- #boolean + local trooplimit = capabilities.trooplimit -- #number + local troopsize = cargotype:GetCratesNeeded() -- #number + -- have we loaded stuff already? + local numberonboard = 0 + local loaded = {} + if self.Loaded_Cargo[unitname] then + loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo + numberonboard = loaded.Troopsloaded or 0 + else + loaded = {} -- #CTLD.LoadedCargo + loaded.Troopsloaded = 0 + loaded.Cratesloaded = 0 + loaded.Cargo = {} + end + if troopsize + numberonboard > trooplimit then + self:_SendMessage("Sorry, we\'re crammed already!", 10, false, Group) + return + else + self.CargoCounter = self.CargoCounter + 1 + local loadcargotype = CTLD_CARGO:New(self.CargoCounter, Cargotype.Name, Cargotype.Templates, CTLD_CARGO.Enum.TROOPS, true, true, Cargotype.CratesNeeded) + self:T({cargotype=loadcargotype}) + loaded.Troopsloaded = loaded.Troopsloaded + troopsize + table.insert(loaded.Cargo,loadcargotype) + self.Loaded_Cargo[unitname] = loaded + self:_SendMessage("Troops boarded!", 10, false, Group) + self:__TroopsPickedUp(1,Group, Unit, Cargotype) + end + return self +end + +function CTLD:_FindRepairNearby(Group, Unit, Repairtype) + self:T(self.lid .. " _FindRepairNearby") + local unitcoord = Unit:GetCoordinate() + + -- find nearest group of deployed groups + local nearestGroup = nil + local nearestGroupIndex = -1 + local nearestDistance = 10000000 + for k,v in pairs(self.DroppedTroops) do + local distance = self:_GetDistance(v:GetCoordinate(),unitcoord) + if distance < nearestDistance and distance ~= -1 then + nearestGroup = v + nearestGroupIndex = k + nearestDistance = distance + end + end + + -- found one and matching distance? + if nearestGroup == nil or nearestDistance > 1000 then + self:_SendMessage("No unit close enough to repair!", 10, false, Group) + return nil, nil + end + + local groupname = nearestGroup:GetName() + --self:I(string.format("***** Found Group %s",groupname)) + + -- helper to find matching template + local function matchstring(String,Table) + local match = false + if type(Table) == "table" then + for _,_name in pairs (Table) do + if string.find(String,_name) then + match = true + break + end + end + else + if type(String) == "string" then + if string.find(String,Table) then match = true end + end + end + return match + end + + -- walk through generics and find matching type + local Cargotype = nil + for k,v in pairs(self.Cargo_Crates) do + --self:I({groupname,v.Templates}) + if matchstring(groupname,v.Templates) and matchstring(groupname,Repairtype) then + Cargotype = v -- #CTLD_CARGO + break + end + end + + if Cargotype == nil then + --self:_SendMessage("Can't find a matching group for " .. Repairtype, 10, false, Group) + return nil, nil + else + return nearestGroup, Cargotype + end + +end + +--- (Internal) Function to repair an object. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Unit#UNIT Unit +-- @param #table Crates Table of #CTLD_CARGO objects near the unit. +-- @param #CTLD.Buildable Build Table build object. +-- @param #number Number Number of objects in Crates (found) to limit search. +function CTLD:_RepairObjectFromCrates(Group,Unit,Crates,Build,Number) + self:T(self.lid .. " _RepairObjectFromCrates") + local build = Build -- -- #CTLD.Buildable + --self:I({Build=Build}) + local Repairtype = build.Template -- #string + local NearestGroup, CargoType = self:_FindRepairNearby(Group,Unit,Repairtype) -- Wrapper.Group#GROUP, #CTLD_CARGO + --self:I({Repairtype=Repairtype, CargoType=CargoType, NearestGroup=NearestGroup}) + if NearestGroup ~= nil then + if self.repairtime < 2 then self.repairtime = 30 end -- noob catch + self:_SendMessage(string.format("Repair started using %s taking %d secs", build.Name, self.repairtime), 10, false, Group) + -- now we can build .... + --NearestGroup:Destroy(false) + local name = CargoType:GetName() + local required = CargoType:GetCratesNeeded() + local template = CargoType:GetTemplates() + local ctype = CargoType:GetType() + local object = {} -- #CTLD.Buildable + object.Name = CargoType:GetName() + object.Required = required + object.Found = required + object.Template = template + object.CanBuild = true + object.Type = ctype -- #CTLD_CARGO.Enum + self:_CleanUpCrates(Crates,Build,Number) + local desttimer = TIMER:New(function() NearestGroup:Destroy(false) end, self) + desttimer:Start(self.repairtime - 1) + local buildtimer = TIMER:New(self._BuildObjectFromCrates,self,Group,Unit,object,true) + buildtimer:Start(self.repairtime) + --self:_BuildObjectFromCrates(Group,Unit,object) + else + self:_SendMessage("Can't repair this unit with " .. build.Name, 10, false, Group) + end + return self +end + + --- (Internal) Function to extract (load from the field) troops into a heli. + -- @param #CTLD self + -- @param Wrapper.Group#GROUP Group + -- @param Wrapper.Unit#UNIT Unit + -- @param #CTLD_CARGO Cargotype + function CTLD:_ExtractTroops(Group, Unit) -- #1574 thanks to @bbirchnz! + self:T(self.lid .. " _ExtractTroops") + -- landed or hovering over load zone? + local grounded = not self:IsUnitInAir(Unit) + local hoverload = self:CanHoverLoad(Unit) + + if not grounded and not hoverload then + self:_SendMessage("You need to land or hover in position to load!", 10, false, Group) + if not self.debug then return self end + end + -- load troops into heli + local unit = Unit -- Wrapper.Unit#UNIT + local unitname = unit:GetName() + -- see if this heli can load troops + local unittype = unit:GetTypeName() + local capabilities = self:_GetUnitCapabilities(Unit) + local cantroops = capabilities.troops -- #boolean + local trooplimit = capabilities.trooplimit -- #number + local unitcoord = unit:GetCoordinate() + + -- find nearest group of deployed troops + local nearestGroup = nil + local nearestGroupIndex = -1 + local nearestDistance = 10000000 + for k,v in pairs(self.DroppedTroops) do + local distance = self:_GetDistance(v:GetCoordinate(),unitcoord) + if distance < nearestDistance and distance ~= -1 then + nearestGroup = v + nearestGroupIndex = k + nearestDistance = distance + end + end + + if nearestGroup == nil or nearestDistance > self.CrateDistance then + self:_SendMessage("No units close enough to extract!", 10, false, Group) + return self + end + -- find matching cargo type + local groupType = string.match(nearestGroup:GetName(), "(.+)-(.+)$") + local Cargotype = nil + for k,v in pairs(self.Cargo_Troops) do + if v.Name == groupType then + Cargotype = v + break + end + end + + if Cargotype == nil then + self:_SendMessage("Can't find a matching cargo type for " .. groupType, 10, false, Group) + return self + end + + local troopsize = Cargotype:GetCratesNeeded() -- #number + -- have we loaded stuff already? + local numberonboard = 0 + local loaded = {} + if self.Loaded_Cargo[unitname] then + loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo + numberonboard = loaded.Troopsloaded or 0 + else + loaded = {} -- #CTLD.LoadedCargo + loaded.Troopsloaded = 0 + loaded.Cratesloaded = 0 + loaded.Cargo = {} + end + if troopsize + numberonboard > trooplimit then + self:_SendMessage("Sorry, we\'re crammed already!", 10, false, Group) + return + else + self.CargoCounter = self.CargoCounter + 1 + local loadcargotype = CTLD_CARGO:New(self.CargoCounter, Cargotype.Name, Cargotype.Templates, CTLD_CARGO.Enum.TROOPS, true, true, Cargotype.CratesNeeded) + self:T({cargotype=loadcargotype}) + loaded.Troopsloaded = loaded.Troopsloaded + troopsize + table.insert(loaded.Cargo,loadcargotype) + self.Loaded_Cargo[unitname] = loaded + self:_SendMessage("Troops boarded!", 10, false, Group) + self:__TroopsExtracted(1,Group, Unit, nearestGroup) + + -- clean up: + table.remove(self.DroppedTroops, nearestGroupIndex) + nearestGroup:Destroy() + end + return self + end + +--- (Internal) Function to spawn crates in front of the heli. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Unit#UNIT Unit +-- @param #CTLD_CARGO Cargo +-- @param #number number Number of crates to generate (for dropping) +-- @param #boolean drop If true we\'re dropping from heli rather than loading. +function CTLD:_GetCrates(Group, Unit, Cargo, number, drop) + self:T(self.lid .. " _GetCrates") + local cgoname = Cargo:GetName() + -- check if we are in LOAD zone + local inzone = false + local drop = drop or false + if not drop then + inzone = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) + else + if self.dropcratesanywhere then -- #1570 + inzone = true + else + inzone = self:IsUnitInZone(Unit,CTLD.CargoZoneType.DROP) + end + end + + if not inzone then + self:_SendMessage("You are not close enough to a logistics zone!", 10, false, Group) + if not self.debug then return self end + end + + -- avoid crate spam + local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitCapabilities + local canloadcratesno = capabilities.cratelimit + local loaddist = self.CrateDistance or 30 + local nearcrates, numbernearby = self:_FindCratesNearby(Group,Unit,loaddist) + if numbernearby >= canloadcratesno and not drop then + self:_SendMessage("There are enough crates nearby already! Take care of those first!", 10, false, Group) + return self + end + -- spawn crates in front of helicopter + local IsHerc = self:IsHercules(Unit) -- Herc + local cargotype = Cargo -- #CTLD_CARGO + local number = number or cargotype:GetCratesNeeded() --#number + local cratesneeded = cargotype:GetCratesNeeded() --#number + local cratename = cargotype:GetName() + local cratetemplate = "Container"-- #string + -- get position and heading of heli + local position = Unit:GetCoordinate() + local heading = Unit:GetHeading() + 1 + local height = Unit:GetHeight() + local droppedcargo = {} + -- loop crates needed + for i=1,number do + local cratealias = string.format("%s-%d", cratetemplate, math.random(1,100000)) + local cratedistance = i*4 + 8 + if IsHerc then + -- wider radius + cratedistance = i*4 + 12 + end + for i=1,50 do + math.random(90,270) + end + local rheading = math.floor(math.random(90,270) * heading + 1 / 360) + if not IsHerc then + rheading = rheading + 180 -- mirror for Helis + end + if rheading > 360 then rheading = rheading - 360 end -- catch > 360 + local cratecoord = position:Translate(cratedistance,rheading) + local cratevec2 = cratecoord:GetVec2() + self.CrateCounter = self.CrateCounter + 1 + self.Spawned_Crates[self.CrateCounter] = SPAWNSTATIC:NewFromType("container_cargo","Cargos",country.id.GERMANY) + :InitCoordinate(cratecoord) + :Spawn(270,cratealias) + + local templ = cargotype:GetTemplates() + local sorte = cargotype:GetType() + self.CargoCounter = self.CargoCounter +1 + local realcargo = nil + if drop then + realcargo = CTLD_CARGO:New(self.CargoCounter,cratename,templ,sorte,true,false,cratesneeded,self.Spawned_Crates[self.CrateCounter],true) + table.insert(droppedcargo,realcargo) + else + realcargo = CTLD_CARGO:New(self.CargoCounter,cratename,templ,sorte,false,false,cratesneeded,self.Spawned_Crates[self.CrateCounter]) + end + table.insert(self.Spawned_Cargo, realcargo) + end + local text = string.format("Crates for %s have been positioned near you!",cratename) + if drop then + text = string.format("Crates for %s have been dropped!",cratename) + self:__CratesDropped(1, Group, Unit, droppedcargo) + end + self:_SendMessage(text, 10, false, Group) + return self +end + +--- (Internal) Function to find and list nearby crates. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Unit#UNIT Unit +-- @return #CTLD self +function CTLD:_ListCratesNearby( _group, _unit) + self:T(self.lid .. " _ListCratesNearby") + local finddist = self.CrateDistance or 30 + local crates,number = self:_FindCratesNearby(_group,_unit, finddist) -- #table + if number > 0 then + local text = REPORT:New("Crates Found Nearby:") + text:Add("------------------------------------------------------------") + for _,_entry in pairs (crates) do + local entry = _entry -- #CTLD_CARGO + local name = entry:GetName() --#string + local dropped = entry:WasDropped() + if dropped then + text:Add(string.format("Dropped crate for %s",name)) + else + text:Add(string.format("Crate for %s",name)) + end + end + if text:GetCount() == 1 then + text:Add(" N O N E") + end + text:Add("------------------------------------------------------------") + self:_SendMessage(text:Text(), 30, true, _group) + else + self:_SendMessage(string.format("No (loadable) crates within %d meters!",finddist), 10, false, _group) + end + return self +end + +--- (Internal) Return distance in meters between two coordinates. +-- @param #CTLD self +-- @param Core.Point#COORDINATE _point1 Coordinate one +-- @param Core.Point#COORDINATE _point2 Coordinate two +-- @return #number Distance in meters +function CTLD:_GetDistance(_point1, _point2) + self:T(self.lid .. " _GetDistance") + if _point1 and _point2 then + local distance = _point1:DistanceFromPointVec2(_point2) + return distance + else + return -1 + end +end + +--- (Internal) Function to find and return nearby crates. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP _group Group +-- @param Wrapper.Unit#UNIT _unit Unit +-- @param #number _dist Distance +-- @return #table Table of crates +-- @return #number Number Number of crates found +function CTLD:_FindCratesNearby( _group, _unit, _dist) + self:T(self.lid .. " _FindCratesNearby") + local finddist = _dist + local location = _group:GetCoordinate() + local existingcrates = self.Spawned_Cargo -- #table + -- cycle + local index = 0 + local found = {} + for _,_cargoobject in pairs (existingcrates) do + local cargo = _cargoobject -- #CTLD_CARGO + local static = cargo:GetPositionable() -- Wrapper.Static#STATIC -- crates + local staticid = cargo:GetID() + if static and static:IsAlive() then + local staticpos = static:GetCoordinate() + local distance = self:_GetDistance(location,staticpos) + if distance <= finddist and static then + index = index + 1 + table.insert(found, staticid, cargo) + end + end + end + return found, index +end + +--- (Internal) Function to get and load nearby crates. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Unit#UNIT Unit +-- @return #CTLD self +function CTLD:_LoadCratesNearby(Group, Unit) + self:T(self.lid .. " _LoadCratesNearby") + -- load crates into heli + local group = Group -- Wrapper.Group#GROUP + local unit = Unit -- Wrapper.Unit#UNIT + local unitname = unit:GetName() + -- see if this heli can load crates + local unittype = unit:GetTypeName() + local capabilities = self:_GetUnitCapabilities(Unit) + --local capabilities = self.UnitTypes[unittype] -- #CTLD.UnitCapabilities + local cancrates = capabilities.crates -- #boolean + local cratelimit = capabilities.cratelimit -- #number + local grounded = not self:IsUnitInAir(Unit) + local canhoverload = self:CanHoverLoad(Unit) + --- cases ------------------------------- + -- Chopper can\'t do crates - bark & return + -- Chopper can do crates - + -- --> hover if forcedhover or bark and return + -- --> hover or land if not forcedhover + ----------------------------------------- + if not cancrates then + self:_SendMessage("Sorry this chopper cannot carry crates!", 10, false, Group) + elseif self.forcehoverload and not canhoverload then + self:_SendMessage("Hover over the crates to pick them up!", 10, false, Group) + elseif not grounded and not canhoverload then + self:_SendMessage("Land or hover over the crates to pick them up!", 10, false, Group) + else + -- have we loaded stuff already? + local numberonboard = 0 + local loaded = {} + if self.Loaded_Cargo[unitname] then + loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo + numberonboard = loaded.Cratesloaded or 0 + else + loaded = {} -- #CTLD.LoadedCargo + loaded.Troopsloaded = 0 + loaded.Cratesloaded = 0 + loaded.Cargo = {} + end + -- get nearby crates + local finddist = self.CrateDistance or 30 + local nearcrates,number = self:_FindCratesNearby(Group,Unit,finddist) -- #table + if number == 0 or numberonboard == cratelimit then + self:_SendMessage("Sorry no loadable crates nearby or fully loaded!", 10, false, Group) + return -- exit + else + -- go through crates and load + local capacity = cratelimit - numberonboard + local crateidsloaded = {} + local loops = 0 + while loaded.Cratesloaded < cratelimit and loops < number do + loops = loops + 1 + local crateind = 0 + -- get crate with largest index + for _ind,_crate in pairs (nearcrates) do + if not _crate:HasMoved() and not _crate:WasDropped() and _crate:GetID() > crateind then + crateind = _crate:GetID() + end + end + -- load one if we found one + if crateind > 0 then + local crate = nearcrates[crateind] -- #CTLD_CARGO + loaded.Cratesloaded = loaded.Cratesloaded + 1 + crate:SetHasMoved(true) + table.insert(loaded.Cargo, crate) + table.insert(crateidsloaded,crate:GetID()) + -- destroy crate + crate:GetPositionable():Destroy() + crate.Positionable = nil + self:_SendMessage(string.format("Crate ID %d for %s loaded!",crate:GetID(),crate:GetName()), 10, false, Group) + self:__CratesPickedUp(1, Group, Unit, crate) + end + end + self.Loaded_Cargo[unitname] = loaded + -- clean up real world crates + local existingcrates = self.Spawned_Cargo -- #table + local newexcrates = {} + for _,_crate in pairs(existingcrates) do + local excrate = _crate -- #CTLD_CARGO + local ID = excrate:GetID() + for _,_ID in pairs(crateidsloaded) do + if ID ~= _ID then + table.insert(newexcrates,_crate) + end + end + end + self.Spawned_Cargo = nil + self.Spawned_Cargo = newexcrates + end + end + return self +end + +--- (Internal) Function to list loaded cargo. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Unit#UNIT Unit +-- @return #CTLD self +function CTLD:_ListCargo(Group, Unit) + self:T(self.lid .. " _ListCargo") + local unitname = Unit:GetName() + local unittype = Unit:GetTypeName() + local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitCapabilities + local trooplimit = capabilities.trooplimit -- #boolean + local cratelimit = capabilities.cratelimit -- #number + local loadedcargo = self.Loaded_Cargo[unitname] or {} -- #CTLD.LoadedCargo + if self.Loaded_Cargo[unitname] then + local no_troops = loadedcargo.Troopsloaded or 0 + local no_crates = loadedcargo.Cratesloaded or 0 + local cargotable = loadedcargo.Cargo or {} -- #table + local report = REPORT:New("Transport Checkout Sheet") + report:Add("------------------------------------------------------------") + report:Add(string.format("Troops: %d(%d), Crates: %d(%d)",no_troops,trooplimit,no_crates,cratelimit)) + report:Add("------------------------------------------------------------") + report:Add(" -- TROOPS --") + for _,_cargo in pairs(cargotable) do + local cargo = _cargo -- #CTLD_CARGO + local type = cargo:GetType() -- #CTLD_CARGO.Enum + if type == CTLD_CARGO.Enum.TROOPS and not cargo:WasDropped() then + report:Add(string.format("Troop: %s size %d",cargo:GetName(),cargo:GetCratesNeeded())) + end + end + if report:GetCount() == 4 then + report:Add(" N O N E") + end + report:Add("------------------------------------------------------------") + report:Add(" -- CRATES --") + local cratecount = 0 + for _,_cargo in pairs(cargotable) do + local cargo = _cargo -- #CTLD_CARGO + local type = cargo:GetType() -- #CTLD_CARGO.Enum + if type ~= CTLD_CARGO.Enum.TROOPS then + report:Add(string.format("Crate: %s size 1",cargo:GetName())) + cratecount = cratecount + 1 + end + end + if cratecount == 0 then + report:Add(" N O N E") + end + report:Add("------------------------------------------------------------") + local text = report:Text() + self:_SendMessage(text, 30, true, Group) + else + self:_SendMessage(string.format("Nothing loaded!\nTroop limit: %d | Crate limit %d",trooplimit,cratelimit), 10, false, Group) + end + return self +end + +--- (Internal) Function to check if a unit is a Hercules C-130. +-- @param #CTLD self +-- @param Wrapper.Unit#UNIT Unit +-- @return #boolean Outcome +function CTLD:IsHercules(Unit) + if Unit:GetTypeName() == "Hercules" then + return true + else + return false + end +end + +--- (Internal) Function to unload troops from heli. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Unit#UNIT Unit +function CTLD:_UnloadTroops(Group, Unit) + self:T(self.lid .. " _UnloadTroops") + -- check if we are in LOAD zone + local droppingatbase = false + local inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) + if inzone then + droppingatbase = true + end + -- check for hover unload + local hoverunload = self:IsCorrectHover(Unit) --if true we\'re hovering in parameters + local IsHerc = self:IsHercules(Unit) + if IsHerc then + -- no hover but airdrop here + hoverunload = self:IsCorrectFlightParameters(Unit) + end + -- check if we\'re landed + local grounded = not self:IsUnitInAir(Unit) + -- Get what we have loaded + local unitname = Unit:GetName() + if self.Loaded_Cargo[unitname] and (grounded or hoverunload) then + if not droppingatbase or self.debug then + local loadedcargo = self.Loaded_Cargo[unitname] or {} -- #CTLD.LoadedCargo + -- looking for troops + local cargotable = loadedcargo.Cargo + for _,_cargo in pairs (cargotable) do + local cargo = _cargo -- #CTLD_CARGO + local type = cargo:GetType() -- #CTLD_CARGO.Enum + if type == CTLD_CARGO.Enum.TROOPS and not cargo:WasDropped() then + -- unload troops + local name = cargo:GetName() or "none" + local temptable = cargo:GetTemplates() or {} + local position = Group:GetCoordinate() + local zoneradius = 100 -- drop zone radius + local factor = 1 + if IsHerc then + factor = cargo:GetCratesNeeded() or 1 -- spread a bit more if airdropping + zoneradius = Unit:GetVelocityMPS() or 100 + end + local zone = ZONE_GROUP:New(string.format("Unload zone-%s",unitname),Group,zoneradius*factor) + local randomcoord = zone:GetRandomCoordinate(10,30*factor):GetVec2() + for _,_template in pairs(temptable) do + self.TroopCounter = self.TroopCounter + 1 + local alias = string.format("%s-%d", _template, math.random(1,100000)) + self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) + :InitRandomizeUnits(true,20,2) + :InitDelayOff() + :SpawnFromVec2(randomcoord) + if self.movetroopstowpzone then + self:_MoveGroupToZone(self.DroppedTroops[self.TroopCounter]) + end + end -- template loop + cargo:SetWasDropped(true) + self:_SendMessage(string.format("Dropped Troops %s into action!",name), 10, false, Group) + self:__TroopsDeployed(1, Group, Unit, self.DroppedTroops[self.TroopCounter]) + end -- if type end + end -- cargotable loop + else -- droppingatbase + self:_SendMessage("Troops have returned to base!", 10, false, Group) + self:__TroopsRTB(1, Group, Unit) + end + -- cleanup load list + local loaded = {} -- #CTLD.LoadedCargo + loaded.Troopsloaded = 0 + loaded.Cratesloaded = 0 + loaded.Cargo = {} + local loadedcargo = self.Loaded_Cargo[unitname] or {} -- #CTLD.LoadedCargo + local cargotable = loadedcargo.Cargo or {} + for _,_cargo in pairs (cargotable) do + local cargo = _cargo -- #CTLD_CARGO + local type = cargo:GetType() -- #CTLD_CARGO.Enum + local dropped = cargo:WasDropped() + if type ~= CTLD_CARGO.Enum.TROOP and not dropped then + table.insert(loaded.Cargo,_cargo) + loaded.Cratesloaded = loaded.Cratesloaded + 1 + end + end + self.Loaded_Cargo[unitname] = nil + self.Loaded_Cargo[unitname] = loaded + else + if IsHerc then + self:_SendMessage("Nothing loaded or not within airdrop parameters!", 10, false, Group) + else + self:_SendMessage("Nothing loaded or not hovering within parameters!", 10, false, Group) + end + end + return self +end + +--- (Internal) Function to unload crates from heli. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrappe.Unit#UNIT Unit +function CTLD:_UnloadCrates(Group, Unit) + self:T(self.lid .. " _UnloadCrates") + + if not self.dropcratesanywhere then -- #1570 + -- check if we are in DROP zone + local inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.DROP) + if not inzone then + self:_SendMessage("You are not close enough to a drop zone!", 10, false, Group) + if not self.debug then + return self + end + end + end + -- check for hover unload + local hoverunload = self:IsCorrectHover(Unit) --if true we\'re hovering in parameters + local IsHerc = self:IsHercules(Unit) + if IsHerc then + -- no hover but airdrop here + hoverunload = self:IsCorrectFlightParameters(Unit) + end + -- check if we\'re landed + local grounded = not self:IsUnitInAir(Unit) + -- Get what we have loaded + local unitname = Unit:GetName() + if self.Loaded_Cargo[unitname] and (grounded or hoverunload) then + local loadedcargo = self.Loaded_Cargo[unitname] or {} -- #CTLD.LoadedCargo + -- looking for troops + local cargotable = loadedcargo.Cargo + for _,_cargo in pairs (cargotable) do + local cargo = _cargo -- #CTLD_CARGO + local type = cargo:GetType() -- #CTLD_CARGO.Enum + if type ~= CTLD_CARGO.Enum.TROOPS and not cargo:WasDropped() then + -- unload crates + self:_GetCrates(Group, Unit, cargo, 1, true) + cargo:SetWasDropped(true) + cargo:SetHasMoved(true) + end + end + -- cleanup load list + local loaded = {} -- #CTLD.LoadedCargo + loaded.Troopsloaded = 0 + loaded.Cratesloaded = 0 + loaded.Cargo = {} + for _,_cargo in pairs (cargotable) do + local cargo = _cargo -- #CTLD_CARGO + local type = cargo:GetType() -- #CTLD_CARGO.Enum + local size = cargo:GetCratesNeeded() + if type == CTLD_CARGO.Enum.TROOP then + table.insert(loaded.Cargo,_cargo) + loaded.Cratesloaded = loaded.Troopsloaded + size + end + end + self.Loaded_Cargo[unitname] = nil + self.Loaded_Cargo[unitname] = loaded + else + if IsHerc then + self:_SendMessage("Nothing loaded or not within airdrop parameters!", 10, false, Group) + else + self:_SendMessage("Nothing loaded or not hovering within parameters!", 10, false, Group) + end + end + return self +end + +--- (Internal) Function to build nearby crates. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrappe.Unit#UNIT Unit +function CTLD:_BuildCrates(Group, Unit) + self:T(self.lid .. " _BuildCrates") + -- get nearby crates + local finddist = self.CrateDistance or 30 + local crates,number = self:_FindCratesNearby(Group,Unit, finddist) -- #table + local buildables = {} + local foundbuilds = false + local canbuild = false + if number > 0 then + -- get dropped crates + for _,_crate in pairs(crates) do + local Crate = _crate -- #CTLD_CARGO + if Crate:WasDropped() and not Crate:IsRepair() then + -- we can build these - maybe + local name = Crate:GetName() + local required = Crate:GetCratesNeeded() + local template = Crate:GetTemplates() + local ctype = Crate:GetType() + if not buildables[name] then + local object = {} -- #CTLD.Buildable + object.Name = name + object.Required = required + object.Found = 1 + object.Template = template + object.CanBuild = false + object.Type = ctype -- #CTLD_CARGO.Enum + buildables[name] = object + foundbuilds = true + else + buildables[name].Found = buildables[name].Found + 1 + foundbuilds = true + end + if buildables[name].Found >= buildables[name].Required then + buildables[name].CanBuild = true + canbuild = true + end + self:T({buildables = buildables}) + end -- end dropped + end -- end crate loop + -- ok let\'s list what we have + local report = REPORT:New("Checklist Buildable Crates") + report:Add("------------------------------------------------------------") + for _,_build in pairs(buildables) do + local build = _build -- Object table from above + local name = build.Name + local needed = build.Required + local found = build.Found + local txtok = "NO" + if build.CanBuild then + txtok = "YES" + end + local text = string.format("Type: %s | Required %d | Found %d | Can Build %s", name, needed, found, txtok) + report:Add(text) + end -- end list buildables + if not foundbuilds then report:Add(" --- None Found ---") end + report:Add("------------------------------------------------------------") + local text = report:Text() + self:_SendMessage(text, 30, true, Group) + -- let\'s get going + if canbuild then + -- loop again + for _,_build in pairs(buildables) do + local build = _build -- #CTLD.Buildable + if build.CanBuild then + self:_CleanUpCrates(crates,build,number) + self:_BuildObjectFromCrates(Group,Unit,build) + end + end + end + else + self:_SendMessage(string.format("No crates within %d meters!",finddist), 10, false, Group) + end -- number > 0 + return self +end + +--- (Internal) Function to repair nearby vehicles / FOBs +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrappe.Unit#UNIT Unit +function CTLD:_RepairCrates(Group, Unit) + self:T(self.lid .. " _RepairCrates") + -- get nearby crates + local finddist = self.CrateDistance or 30 + local crates,number = self:_FindCratesNearby(Group,Unit, finddist) -- #table + local buildables = {} + local foundbuilds = false + local canbuild = false + if number > 0 then + -- get dropped crates + for _,_crate in pairs(crates) do + local Crate = _crate -- #CTLD_CARGO + if Crate:WasDropped() and Crate:IsRepair() then + -- we can build these - maybe + local name = Crate:GetName() + local required = Crate:GetCratesNeeded() + local template = Crate:GetTemplates() + local ctype = Crate:GetType() + if not buildables[name] then + local object = {} -- #CTLD.Buildable + object.Name = name + object.Required = required + object.Found = 1 + object.Template = template + object.CanBuild = false + object.Type = ctype -- #CTLD_CARGO.Enum + buildables[name] = object + foundbuilds = true + else + buildables[name].Found = buildables[name].Found + 1 + foundbuilds = true + end + if buildables[name].Found >= buildables[name].Required then + buildables[name].CanBuild = true + canbuild = true + end + self:T({repair = buildables}) + end -- end dropped + end -- end crate loop + -- ok let\'s list what we have + local report = REPORT:New("Checklist Repairs") + report:Add("------------------------------------------------------------") + for _,_build in pairs(buildables) do + local build = _build -- Object table from above + local name = build.Name + local needed = build.Required + local found = build.Found + local txtok = "NO" + if build.CanBuild then + txtok = "YES" + end + local text = string.format("Type: %s | Required %d | Found %d | Can Repair %s", name, needed, found, txtok) + report:Add(text) + end -- end list buildables + if not foundbuilds then report:Add(" --- None Found ---") end + report:Add("------------------------------------------------------------") + local text = report:Text() + self:_SendMessage(text, 30, true, Group) + -- let\'s get going + if canbuild then + -- loop again + for _,_build in pairs(buildables) do + local build = _build -- #CTLD.Buildable + if build.CanBuild then + self:_RepairObjectFromCrates(Group,Unit,crates,build,number) + end + end + end + else + self:_SendMessage(string.format("No crates within %d meters!",finddist), 10, false, Group) + end -- number > 0 + return self +end + +--- (Internal) Function to actually SPAWN buildables in the mission. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Group#UNIT Unit +-- @param #CTLD.Buildable Build +-- @param #boolean Repair If true this is a repair and not a new build +function CTLD:_BuildObjectFromCrates(Group,Unit,Build,Repair) + self:T(self.lid .. " _BuildObjectFromCrates") + -- Spawn-a-crate-content + local position = Unit:GetCoordinate() or Group:GetCoordinate() + local unitname = Unit:GetName() or Group:GetName() + local name = Build.Name + local type = Build.Type -- #CTLD_CARGO.Enum + local canmove = false + if type == CTLD_CARGO.Enum.VEHICLE then canmove = true end + local temptable = Build.Template or {} + local zone = ZONE_GROUP:New(string.format("Unload zone-%s",unitname),Group,100) + local randomcoord = zone:GetRandomCoordinate(35):GetVec2() + for _,_template in pairs(temptable) do + self.TroopCounter = self.TroopCounter + 1 + if canmove then + local alias = string.format("%s-%d", _template, math.random(1,100000)) + self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) + :InitRandomizeUnits(true,20,2) + :InitDelayOff() + :SpawnFromVec2(randomcoord) + else -- don't random position of e.g. SAM units build as FOB + self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) + --:InitRandomizeUnits(true,20,2) + :InitDelayOff() + :SpawnFromVec2(randomcoord) + end + if self.movetroopstowpzone and canmove then + self:_MoveGroupToZone(self.DroppedTroops[self.TroopCounter]) + end + if Repair then + self:__CratesRepaired(1,Group,Unit,self.DroppedTroops[self.TroopCounter]) + else + self:__CratesBuild(1,Group,Unit,self.DroppedTroops[self.TroopCounter]) + end + end -- template loop + return self +end + +--- (Internal) Function to move group to WP zone. +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group The Group to move. +function CTLD:_MoveGroupToZone(Group) + self:T(self.lid .. " _MoveGroupToZone") + local groupname = Group:GetName() or "none" + local groupcoord = Group:GetCoordinate() + -- Get closest zone of type + local outcome, name, zone, distance = self:IsUnitInZone(Group,CTLD.CargoZoneType.MOVE) + --self:Tstring.format("Closest WP zone %s is %d meters",name,distance)) + if (distance <= self.movetroopsdistance) and zone then + -- yes, we can ;) + local groupname = Group:GetName() + local zonecoord = zone:GetRandomCoordinate(20,125) -- Core.Point#COORDINATE + local coordinate = zonecoord:GetVec2() + Group:SetAIOn() + Group:OptionAlarmStateAuto() + Group:OptionDisperseOnAttack(30) + Group:OptionROEOpenFirePossible() + Group:RouteToVec2(coordinate,5) + end + return self +end + +--- (Internal) Housekeeping - Cleanup crates when build +-- @param #CTLD self +-- @param #table Crates Table of #CTLD_CARGO objects near the unit. +-- @param #CTLD.Buildable Build Table build object. +-- @param #number Number Number of objects in Crates (found) to limit search. +function CTLD:_CleanUpCrates(Crates,Build,Number) + self:T(self.lid .. " _CleanUpCrates") + -- clean up real world crates + local build = Build -- #CTLD.Buildable + local existingcrates = self.Spawned_Cargo -- #table of exising crates + local newexcrates = {} + -- get right number of crates to destroy + local numberdest = Build.Required + local nametype = Build.Name + local found = 0 + local rounds = Number + local destIDs = {} + + -- loop and find matching IDs in the set + for _,_crate in pairs(Crates) do + local nowcrate = _crate -- #CTLD_CARGO + local name = nowcrate:GetName() + local thisID = nowcrate:GetID() + if name == nametype then -- matching crate type + table.insert(destIDs,thisID) + found = found + 1 + nowcrate:GetPositionable():Destroy() + nowcrate.Positionable = nil + end + if found == numberdest then break end -- got enough + end + -- loop and remove from real world representation + for _,_crate in pairs(existingcrates) do + local excrate = _crate -- #CTLD_CARGO + local ID = excrate:GetID() + for _,_ID in pairs(destIDs) do + if ID ~= _ID then + table.insert(newexcrates,_crate) + end + end + end + + -- reset Spawned_Cargo + self.Spawned_Cargo = nil + self.Spawned_Cargo = newexcrates + return self +end + +--- (Internal) Housekeeping - Function to refresh F10 menus. +-- @param #CTLD self +-- @return #CTLD self +function CTLD:_RefreshF10Menus() + self:T(self.lid .. " _RefreshF10Menus") + local PlayerSet = self.PilotGroups -- Core.Set#SET_GROUP + local PlayerTable = PlayerSet:GetSetObjects() -- #table of #GROUP objects + -- rebuild units table + local _UnitList = {} + for _key, _group in pairs (PlayerTable) do + local _unit = _group:GetUnit(1) -- Wrapper.Unit#UNIT Asume that there is only one unit in the flight for players + if _unit then + if _unit:IsAlive() and _unit:IsPlayer() then + if _unit:IsHelicopter() or (_unit:GetTypeName() == "Hercules" and self.enableHercules) then --ensure no stupid unit entries here + local unitName = _unit:GetName() + _UnitList[unitName] = unitName + end + end -- end isAlive + end -- end if _unit + end -- end for + self.CtldUnits = _UnitList + + -- build unit menus + local menucount = 0 + local menus = {} + for _, _unitName in pairs(self.CtldUnits) do + if not self.MenusDone[_unitName] then + local _unit = UNIT:FindByName(_unitName) -- Wrapper.Unit#UNIT + if _unit then + local _group = _unit:GetGroup() -- Wrapper.Group#GROUP + if _group then + -- get chopper capabilities + local unittype = _unit:GetTypeName() + local capabilities = self:_GetUnitCapabilities(_unit) -- #CTLD.UnitCapabilities + local cantroops = capabilities.troops + local cancrates = capabilities.crates + -- top menu + local topmenu = MENU_GROUP:New(_group,"CTLD",nil) + local topcrates = MENU_GROUP:New(_group,"Manage Crates",topmenu) + local toptroops = MENU_GROUP:New(_group,"Manage Troops",topmenu) + local listmenu = MENU_GROUP_COMMAND:New(_group,"List boarded cargo",topmenu, self._ListCargo, self, _group, _unit) + local smokemenu = MENU_GROUP_COMMAND:New(_group,"Smoke zones nearby",topmenu, self.SmokeZoneNearBy, self, _unit, false) + local smokemenu = MENU_GROUP_COMMAND:New(_group,"Flare zones nearby",topmenu, self.SmokeZoneNearBy, self, _unit, true):Refresh() + -- sub menus + -- sub menu crates management + if cancrates then + local loadmenu = MENU_GROUP_COMMAND:New(_group,"Load crates",topcrates, self._LoadCratesNearby, self, _group, _unit) + local cratesmenu = MENU_GROUP:New(_group,"Get Crates",topcrates) + for _,_entry in pairs(self.Cargo_Crates) do + local entry = _entry -- #CTLD_CARGO + menucount = menucount + 1 + local menutext = string.format("Get crate for %s",entry.Name) + menus[menucount] = MENU_GROUP_COMMAND:New(_group,menutext,cratesmenu,self._GetCrates, self, _group, _unit, entry) + end + listmenu = MENU_GROUP_COMMAND:New(_group,"List crates nearby",topcrates, self._ListCratesNearby, self, _group, _unit) + local unloadmenu = MENU_GROUP_COMMAND:New(_group,"Drop crates",topcrates, self._UnloadCrates, self, _group, _unit) + local buildmenu = MENU_GROUP_COMMAND:New(_group,"Build crates",topcrates, self._BuildCrates, self, _group, _unit) + local repairmenu = MENU_GROUP_COMMAND:New(_group,"Repair",topcrates, self._RepairCrates, self, _group, _unit):Refresh() + end + -- sub menu troops management + if cantroops then + local troopsmenu = MENU_GROUP:New(_group,"Load troops",toptroops) + for _,_entry in pairs(self.Cargo_Troops) do + local entry = _entry -- #CTLD_CARGO + menucount = menucount + 1 + menus[menucount] = MENU_GROUP_COMMAND:New(_group,entry.Name,troopsmenu,self._LoadTroops, self, _group, _unit, entry) + end + local unloadmenu1 = MENU_GROUP_COMMAND:New(_group,"Drop troops",toptroops, self._UnloadTroops, self, _group, _unit):Refresh() + local extractMenu1 = MENU_GROUP_COMMAND:New(_group, "Extract troops", toptroops, self._ExtractTroops, self, _group, _unit):Refresh() + end + local rbcns = MENU_GROUP_COMMAND:New(_group,"List active zone beacons",topmenu, self._ListRadioBeacons, self, _group, _unit) + if unittype == "Hercules" then + local hoverpars = MENU_GROUP_COMMAND:New(_group,"Show flight parameters",topmenu, self._ShowFlightParams, self, _group, _unit):Refresh() + else + local hoverpars = MENU_GROUP_COMMAND:New(_group,"Show hover parameters",topmenu, self._ShowHoverParams, self, _group, _unit):Refresh() + end + self.MenusDone[_unitName] = true + end -- end group + end -- end unit + else -- menu build check + self:T(self.lid .. " Menus already done for this group!") + end -- end menu build check + end -- end for + return self + end + +--- User function - Add *generic* troop type loadable as cargo. This type will load directly into the heli without crates. +-- @param #CTLD self +-- @param #string Name Unique name of this type of troop. E.g. "Anti-Air Small". +-- @param #table Templates Table of #string names of late activated Wrapper.Group#GROUP making up this troop. +-- @param #CTLD_CARGO.Enum Type Type of cargo, here TROOPS - these will move to a nearby destination zone when dropped/build. +-- @param #number NoTroops Size of the group in number of Units across combined templates (for loading). +function CTLD:AddTroopsCargo(Name,Templates,Type,NoTroops) + self:T(self.lid .. " AddTroopsCargo") + self.CargoCounter = self.CargoCounter + 1 + -- Troops are directly loadable + local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Templates,Type,false,true,NoTroops) + table.insert(self.Cargo_Troops,cargo) + return self +end + +--- User function - Add *generic* crate-type loadable as cargo. This type will create crates that need to be loaded, moved, dropped and built. +-- @param #CTLD self +-- @param #string Name Unique name of this type of cargo. E.g. "Humvee". +-- @param #table Templates Table of #string names of late activated Wrapper.Group#GROUP building this cargo. +-- @param #CTLD_CARGO.Enum Type Type of cargo. I.e. VEHICLE or FOB. VEHICLE will move to destination zones when dropped/build, FOB stays put. +-- @param #number NoCrates Number of crates needed to build this cargo. +function CTLD:AddCratesCargo(Name,Templates,Type,NoCrates) + self:T(self.lid .. " AddCratesCargo") + self.CargoCounter = self.CargoCounter + 1 + -- Crates are not directly loadable + local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Templates,Type,false,false,NoCrates) + table.insert(self.Cargo_Crates,cargo) + return self +end + +--- User function - Add *generic* repair crates loadable as cargo. This type will create crates that need to be loaded, moved, dropped and built. +-- @param #CTLD self +-- @param #string Name Unique name of this type of cargo. E.g. "Humvee". +-- @param #string Template Template of VEHICLE or FOB cargo that this can repair. +-- @param #CTLD_CARGO.Enum Type Type of cargo, here REPAIR. +-- @param #number NoCrates Number of crates needed to build this cargo. +function CTLD:AddCratesRepair(Name,Template,Type,NoCrates) + self:T(self.lid .. " AddCratesRepair") + self.CargoCounter = self.CargoCounter + 1 + -- Crates are not directly loadable + local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Template,Type,false,false,NoCrates) + table.insert(self.Cargo_Crates,cargo) + return self +end + +--- User function - Add a #CTLD.CargoZoneType zone for this CTLD instance. +-- @param #CTLD self +-- @param #CTLD.CargoZone Zone Zone #CTLD.CargoZone describing the zone. +function CTLD:AddZone(Zone) + self:T(self.lid .. " AddZone") + local zone = Zone -- #CTLD.CargoZone + if zone.type == CTLD.CargoZoneType.LOAD then + table.insert(self.pickupZones,zone) + elseif zone.type == CTLD.CargoZoneType.DROP then + table.insert(self.dropOffZones,zone) + else + table.insert(self.wpZones,zone) + end + return self +end + +--- User function - Activate Name #CTLD.CargoZone.Type ZoneType for this CTLD instance. +-- @param #CTLD self +-- @param #string Name Name of the zone to change in the ME. +-- @param #CTLD.CargoZoneTyp ZoneType Type of zone this belongs to. +-- @param #boolean NewState (Optional) Set to true to activate, false to switch off. +function CTLD:ActivateZone(Name,ZoneType,NewState) + self:T(self.lid .. " AddZone") + local newstate = true + -- set optional in case we\'re deactivating + if NewState ~= nil then + newstate = NewState + end + + -- get correct table + local table = {} + if ZoneType == CTLD.CargoZoneType.LOAD then + table = self.pickupZones + elseif ZoneType == CTLD.CargoZoneType.DROP then + table = self.dropOffZones + else + table = self.wpZones + end + -- loop table + for _,_zone in pairs(table) do + local thiszone = _zone --#CTLD.CargoZone + if thiszone.name == Name then + thiszone.active = newstate + break + end + end + return self +end + + +--- User function - Deactivate Name #CTLD.CargoZoneType ZoneType for this CTLD instance. +-- @param #CTLD self +-- @param #string Name Name of the zone to change in the ME. +-- @param #CTLD.CargoZoneTyp ZoneType Type of zone this belongs to. +function CTLD:DeactivateZone(Name,ZoneType) + self:T(self.lid .. " AddZone") + self:ActivateZone(Name,ZoneType,false) + return self +end + +--- (Internal) Function to obtain a valid FM frequency. +-- @param #CTLD self +-- @param #string Name Name of zone. +-- @return #CTLD.ZoneBeacon Beacon Beacon table. +function CTLD:_GetFMBeacon(Name) + self:T(self.lid .. " _GetFMBeacon") + local beacon = {} -- #CTLD.ZoneBeacon + if #self.FreeFMFrequencies <= 1 then + self.FreeFMFrequencies = self.UsedFMFrequencies + self.UsedFMFrequencies = {} + end + --random + local FM = table.remove(self.FreeFMFrequencies, math.random(#self.FreeFMFrequencies)) + table.insert(self.UsedFMFrequencies, FM) + beacon.name = Name + beacon.frequency = FM / 1000000 + beacon.modulation = radio.modulation.FM + return beacon +end + +--- (Internal) Function to obtain a valid UHF frequency. +-- @param #CTLD self +-- @param #string Name Name of zone. +-- @return #CTLD.ZoneBeacon Beacon Beacon table. +function CTLD:_GetUHFBeacon(Name) + self:T(self.lid .. " _GetUHFBeacon") + local beacon = {} -- #CTLD.ZoneBeacon + if #self.FreeUHFFrequencies <= 1 then + self.FreeUHFFrequencies = self.UsedUHFFrequencies + self.UsedUHFFrequencies = {} + end + --random + local UHF = table.remove(self.FreeUHFFrequencies, math.random(#self.FreeUHFFrequencies)) + table.insert(self.UsedUHFFrequencies, UHF) + beacon.name = Name + beacon.frequency = UHF / 1000000 + beacon.modulation = radio.modulation.AM + + return beacon +end + +--- (Internal) Function to obtain a valid VHF frequency. +-- @param #CTLD self +-- @param #string Name Name of zone. +-- @return #CTLD.ZoneBeacon Beacon Beacon table. +function CTLD:_GetVHFBeacon(Name) + self:T(self.lid .. " _GetVHFBeacon") + local beacon = {} -- #CTLD.ZoneBeacon + if #self.FreeVHFFrequencies <= 3 then + self.FreeVHFFrequencies = self.UsedVHFFrequencies + self.UsedVHFFrequencies = {} + end + --get random + local VHF = table.remove(self.FreeVHFFrequencies, math.random(#self.FreeVHFFrequencies)) + table.insert(self.UsedVHFFrequencies, VHF) + beacon.name = Name + beacon.frequency = VHF / 1000000 + beacon.modulation = radio.modulation.FM + return beacon +end + + +--- User function - Crates and adds a #CTLD.CargoZone zone for this CTLD instance. +-- Zones of type LOAD: Players load crates and troops here. +-- Zones of type DROP: Players can drop crates here. Note that troops can be unloaded anywhere. +-- Zone of type MOVE: Dropped troops and vehicles will start moving to the nearest zone of this type (also see options). +-- @param #CTLD self +-- @param #string Name Name of this zone, as in Mission Editor. +-- @param #string Type Type of this zone, #CTLD.CargoZoneType +-- @param #number Color Smoke/Flare color e.g. #SMOKECOLOR.Red +-- @param #string Active Is this zone currently active? +-- @param #string HasBeacon Does this zone have a beacon if it is active? +-- @return #CTLD self +function CTLD:AddCTLDZone(Name, Type, Color, Active, HasBeacon) + self:T(self.lid .. " AddCTLDZone") + + local ctldzone = {} -- #CTLD.CargoZone + ctldzone.active = Active or false + ctldzone.color = Color or SMOKECOLOR.Red + ctldzone.name = Name or "NONE" + ctldzone.type = Type or CTLD.CargoZoneType.MOVE -- #CTLD.CargoZoneType + ctldzone.hasbeacon = HasBeacon or false + + if HasBeacon then + ctldzone.fmbeacon = self:_GetFMBeacon(Name) + ctldzone.uhfbeacon = self:_GetUHFBeacon(Name) + ctldzone.vhfbeacon = self:_GetVHFBeacon(Name) + else + ctldzone.fmbeacon = nil + ctldzone.uhfbeacon = nil + ctldzone.vhfbeacon = nil + end + + self:AddZone(ctldzone) + return self +end + +--- (Internal) Function to show list of radio beacons +-- @param #CTLD self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Unit#UNIT Unit +function CTLD:_ListRadioBeacons(Group, Unit) + self:T(self.lid .. " _ListRadioBeacons") + local report = REPORT:New("Active Zone Beacons") + report:Add("------------------------------------------------------------") + local zones = {[1] = self.pickupZones, [2] = self.wpZones, [3] = self.dropOffZones} + for i=1,3 do + for index,cargozone in pairs(zones[i]) do + -- Get Beacon object from zone + local czone = cargozone -- #CTLD.CargoZone + if czone.active and czone.hasbeacon then + local FMbeacon = czone.fmbeacon -- #CTLD.ZoneBeacon + local VHFbeacon = czone.vhfbeacon -- #CTLD.ZoneBeacon + local UHFbeacon = czone.uhfbeacon -- #CTLD.ZoneBeacon + local Name = czone.name + local FM = FMbeacon.frequency -- MHz + local VHF = VHFbeacon.frequency * 1000 -- KHz + local UHF = UHFbeacon.frequency -- MHz + report:AddIndent(string.format(" %s | FM %s Mhz | VHF %s KHz | UHF %s Mhz ", Name, FM, VHF, UHF),"|") + end + end + end + if report:GetCount() == 1 then + report:Add(" N O N E") + end + report:Add("------------------------------------------------------------") + self:_SendMessage(report:Text(), 30, true, Group) + return self +end + +--- (Internal) Add radio beacon to zone. Runs 30 secs. +-- @param #CTLD self +-- @param #string Name Name of zone. +-- @param #string Sound Name of soundfile. +-- @param #number Mhz Frequency in Mhz. +-- @param #number Modulation Modulation AM or FM. +function CTLD:_AddRadioBeacon(Name, Sound, Mhz, Modulation) + self:T(self.lid .. " _AddRadioBeacon") + local Zone = ZONE:FindByName(Name) + local Sound = Sound or "beacon.ogg" + if Zone then + local ZoneCoord = Zone:GetCoordinate() + local ZoneVec3 = ZoneCoord:GetVec3() + local Frequency = Mhz * 1000000 -- Freq in Hertz + local Sound = "l10n/DEFAULT/"..Sound + trigger.action.radioTransmission(Sound, ZoneVec3, Modulation, false, Frequency, 1000) -- Beacon in MP only runs for 30secs straight + end + return self +end + +--- (Internal) Function to refresh radio beacons +-- @param #CTLD self +function CTLD:_RefreshRadioBeacons() + self:T(self.lid .. " _RefreshRadioBeacons") + + local zones = {[1] = self.pickupZones, [2] = self.wpZones, [3] = self.dropOffZones} + for i=1,3 do + for index,cargozone in pairs(zones[i]) do + -- Get Beacon object from zone + local czone = cargozone -- #CTLD.CargoZone + local Sound = self.RadioSound + if czone.active and czone.hasbeacon then + local FMbeacon = czone.fmbeacon -- #CTLD.ZoneBeacon + local VHFbeacon = czone.vhfbeacon -- #CTLD.ZoneBeacon + local UHFbeacon = czone.uhfbeacon -- #CTLD.ZoneBeacon + local Name = czone.name + local FM = FMbeacon.frequency -- MHz + local VHF = VHFbeacon.frequency -- KHz + local UHF = UHFbeacon.frequency -- MHz + self:_AddRadioBeacon(Name,Sound,FM,radio.modulation.FM) + self:_AddRadioBeacon(Name,Sound,VHF,radio.modulation.FM) + self:_AddRadioBeacon(Name,Sound,UHF,radio.modulation.AM) + end + end + end + return self +end + +--- (Internal) Function to see if a unit is in a specific zone type. +-- @param #CTLD self +-- @param Wrapper.Unit#UNIT Unit Unit +-- @param #CTLD.CargoZoneType Zonetype Zonetype +-- @return #boolean Outcome Is in zone or not +-- @return #string name Closest zone name +-- @return #string zone Closest Core.Zone#ZONE object +-- @return #number distance Distance to closest zone +function CTLD:IsUnitInZone(Unit,Zonetype) + self:T(self.lid .. " IsUnitInZone") + local unitname = Unit:GetName() + local zonetable = {} + local outcome = false + if Zonetype == CTLD.CargoZoneType.LOAD then + zonetable = self.pickupZones -- #table + elseif Zonetype == CTLD.CargoZoneType.DROP then + zonetable = self.dropOffZones -- #table + else + zonetable = self.wpZones -- #table + end + --- now see if we\'re in + local zonecoord = nil + local colorret = nil + local maxdist = 1000000 -- 100km + local zoneret = nil + local zonenameret = nil + for _,_cargozone in pairs(zonetable) do + local czone = _cargozone -- #CTLD.CargoZone + local unitcoord = Unit:GetCoordinate() + local zonename = czone.name + local zone = ZONE:FindByName(zonename) + zonecoord = zone:GetCoordinate() + local active = czone.active + local color = czone.color + local zoneradius = zone:GetRadius() + local distance = self:_GetDistance(zonecoord,unitcoord) + if distance <= zoneradius and active then + outcome = true + end + if maxdist > distance then + maxdist = distance + zoneret = zone + zonenameret = zonename + colorret = color + end + end + return outcome, zonenameret, zoneret, maxdist +end + +--- User function - Start smoke in a zone close to the Unit. +-- @param #CTLD self +-- @param Wrapper.Unit#UNIT Unit The Unit. +-- @param #boolean Flare If true, flare instead. +function CTLD:SmokeZoneNearBy(Unit, Flare) + self:T(self.lid .. " SmokeZoneNearBy") + -- table of #CTLD.CargoZone table + local unitcoord = Unit:GetCoordinate() + local Group = Unit:GetGroup() + local smokedistance = self.smokedistance + local smoked = false + local zones = {[1] = self.pickupZones, [2] = self.wpZones, [3] = self.dropOffZones} + for i=1,3 do + for index,cargozone in pairs(zones[i]) do + local CZone = cargozone --#CTLD.CargoZone + local zonename = CZone.name + local zone = ZONE:FindByName(zonename) + local zonecoord = zone:GetCoordinate() + local active = CZone.active + local color = CZone.color + local distance = self:_GetDistance(zonecoord,unitcoord) + if distance < smokedistance and active then + -- smoke zone since we\'re nearby + if not Flare then + zonecoord:Smoke(color or SMOKECOLOR.White) + else + if color == SMOKECOLOR.Blue then color = FLARECOLOR.White end + zonecoord:Flare(color or FLARECOLOR.White) + end + local txt = "smoking" + if Flare then txt = "flaring" end + self:_SendMessage(string.format("Roger, %s zone %s!",txt, zonename), 10, false, Group) + smoked = true + end + end + end + if not smoked then + local distance = UTILS.MetersToNM(self.smokedistance) + self:_SendMessage(string.format("Negative, need to be closer than %dnm to a zone!",distance), 10, false, Group) + end + return self +end + + --- User - Function to add/adjust unittype capabilities. + -- @param #CTLD self + -- @param #string Unittype The unittype to adjust. If passed as Wrapper.Unit#UNIT, it will search for the unit in the mission. + -- @param #boolean Cancrates Unit can load crates. + -- @param #boolean Cantroops Unit can load troops. + -- @param #number Cratelimit Unit can carry number of crates. + -- @param #number Trooplimit Unit can carry number of troops. + function CTLD:UnitCapabilities(Unittype, Cancrates, Cantroops, Cratelimit, Trooplimit) + self:T(self.lid .. " UnitCapabilities") + local unittype = nil + local unit = nil + if type(Unittype) == "string" then + unittype = Unittype + elseif type(Unittype) == "table" then + unit = UNIT:FindByName(Unittype) -- Wrapper.Unit#UNIT + unittype = unit:GetTypeName() + else + return self + end + -- set capabilities + local capabilities = {} -- #CTLD.UnitCapabilities + capabilities.type = unittype + capabilities.crates = Cancrates or false + capabilities.troops = Cantroops or false + capabilities.cratelimit = Cratelimit or 0 + capabilities.trooplimit = Trooplimit or 0 + self.UnitTypes[unittype] = capabilities + return self + end + + --- (Internal) Check if a unit is hovering *in parameters*. + -- @param #CTLD self + -- @param Wrapper.Unit#UNIT Unit + -- @return #boolean Outcome + function CTLD:IsCorrectHover(Unit) + self:T(self.lid .. " IsCorrectHover") + local outcome = false + -- see if we are in air and within parameters. + if self:IsUnitInAir(Unit) then + -- get speed and height + local uspeed = Unit:GetVelocityMPS() + local uheight = Unit:GetHeight() + local ucoord = Unit:GetCoordinate() + local gheight = ucoord:GetLandHeight() + local aheight = uheight - gheight -- height above ground + local maxh = self.maximumHoverHeight -- 15 + local minh = self.minimumHoverHeight -- 5 + local mspeed = 2 -- 2 m/s + if (uspeed <= mspeed) and (aheight <= maxh) and (aheight >= minh) then + -- yep within parameters + outcome = true + end + end + return outcome + end + + --- (Internal) Check if a Hercules is flying *in parameters* for air drops. + -- @param #CTLD self + -- @param Wrapper.Unit#UNIT Unit + -- @return #boolean Outcome + function CTLD:IsCorrectFlightParameters(Unit) + self:T(self.lid .. " IsCorrectFlightParameters") + local outcome = false + -- see if we are in air and within parameters. + if self:IsUnitInAir(Unit) then + -- get speed and height + local uspeed = Unit:GetVelocityMPS() + local uheight = Unit:GetHeight() + local ucoord = Unit:GetCoordinate() + local gheight = ucoord:GetLandHeight() + local aheight = uheight - gheight -- height above ground + local maxh = self.HercMinAngels-- 1500m + local minh = self.HercMaxAngels -- 5000m + local maxspeed = self.HercMaxSpeed -- 77 mps + -- DONE: TEST - Speed test for Herc, should not be above 280kph/150kn + local kmspeed = uspeed * 3.6 + local knspeed = kmspeed / 1.86 + self:T(string.format("%s Unit parameters: at %dm AGL with %dmps | %dkph | %dkn",self.lid,aheight,uspeed,kmspeed,knspeed)) + if (aheight <= maxh) and (aheight >= minh) and (uspeed <= maxspeed) then + -- yep within parameters + outcome = true + end + end + return outcome + end + + --- (Internal) List if a unit is hovering *in parameters*. + -- @param #CTLD self + -- @param Wrapper.Group#GROUP Group + -- @param Wrapper.Unit#UNIT Unit + function CTLD:_ShowHoverParams(Group,Unit) + local inhover = self:IsCorrectHover(Unit) + local htxt = "true" + if not inhover then htxt = "false" end + local text = "" + if _SETTINGS:IsMetric() then + text = string.format("Hover parameters (autoload/drop):\n - Min height %dm \n - Max height %dm \n - Max speed 2mps \n - In parameter: %s", self.minimumHoverHeight, self.maximumHoverHeight, htxt) + else + local minheight = UTILS.MetersToFeet(self.minimumHoverHeight) + local maxheight = UTILS.MetersToFeet(self.maximumHoverHeight) + text = string.format("Hover parameters (autoload/drop):\n - Min height %dm \n - Max height %dm \n - Max speed 6fts \n - In parameter: %s", minheight, maxheight, htxt) + end + self:_SendMessage(text, 10, false, Group) + return self + end + + --- (Internal) List if a Herc unit is flying *in parameters*. + -- @param #CTLD self + -- @param Wrapper.Group#GROUP Group + -- @param Wrapper.Unit#UNIT Unit + function CTLD:_ShowFlightParams(Group,Unit) + local inhover = self:IsCorrectFlightParameters(Unit) + local htxt = "true" + if not inhover then htxt = "false" end + local text = "" + if _SETTINGS:IsImperial() then + local minheight = UTILS.MetersToFeet(self.HercMinAngels) + local maxheight = UTILS.MetersToFeet(self.HercMaxAngels) + text = string.format("Flight parameters (airdrop):\n - Min height %dft \n - Max height %dft \n - In parameter: %s", minheight, maxheight, htxt) + else + local minheight = self.HercMinAngels + local maxheight = self.HercMaxAngels + text = string.format("Flight parameters (airdrop):\n - Min height %dm \n - Max height %dm \n - In parameter: %s", minheight, maxheight, htxt) + end + self:_SendMessage(text, 10, false, Group) + return self + end + + + --- (Internal) Check if a unit is in a load zone and is hovering in parameters. + -- @param #CTLD self + -- @param Wrapper.Unit#UNIT Unit + -- @return #boolean Outcome + function CTLD:CanHoverLoad(Unit) + self:T(self.lid .. " CanHoverLoad") + if self:IsHercules(Unit) then return false end + local outcome = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) and self:IsCorrectHover(Unit) + return outcome + end + + --- (Internal) Check if a unit is above ground. + -- @param #CTLD self + -- @param Wrapper.Unit#UNIT Unit + -- @return #boolean Outcome + function CTLD:IsUnitInAir(Unit) + -- get speed and height + local minheight = self.minimumHoverHeight + if self.enableHercules and Unit:GetTypeName() == "Hercules" then + minheight = 5.1 -- herc is 5m AGL on the ground + end + local uheight = Unit:GetHeight() + local ucoord = Unit:GetCoordinate() + local gheight = ucoord:GetLandHeight() + local aheight = uheight - gheight -- height above ground + if aheight >= minheight then + return true + else + return false + end + end + + --- (Internal) Autoload if we can do crates, have capacity free and are in a load zone. + -- @param #CTLD self + -- @param Wrapper.Unit#UNIT Unit + -- @return #CTLD self + function CTLD:AutoHoverLoad(Unit) + self:T(self.lid .. " AutoHoverLoad") + -- get capabilities and current load + local unittype = Unit:GetTypeName() + local unitname = Unit:GetName() + local Group = Unit:GetGroup() + local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitCapabilities + local cancrates = capabilities.crates -- #boolean + local cratelimit = capabilities.cratelimit -- #number + if cancrates then + -- get load + local numberonboard = 0 + local loaded = {} + if self.Loaded_Cargo[unitname] then + loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo + numberonboard = loaded.Cratesloaded or 0 + end + local load = cratelimit - numberonboard + local canload = self:CanHoverLoad(Unit) + if canload and load > 0 then + self:_LoadCratesNearby(Group,Unit) + end + end + return self + end + + --- (Internal) Run through all pilots and see if we autoload. + -- @param #CTLD self + -- @return #CTLD self + function CTLD:CheckAutoHoverload() + if self.hoverautoloading then + for _,_pilot in pairs (self.CtldUnits) do + local Unit = UNIT:FindByName(_pilot) + self:AutoHoverLoad(Unit) + end + end + return self + end + + --- (Internal) Run through DroppedTroops and capture alive units + -- @param #CTLD self + -- @return #CTLD self + function CTLD:CleanDroppedTroops() + local troops = self.DroppedTroops + local newtable = {} + for _index, _group in pairs (troops) do + if _group and _group:IsAlive() then + newtable[_index] = _group + end + end + self.DroppedTroops = newtable + return self + end +------------------------------------------------------------------- +-- FSM functions +------------------------------------------------------------------- + + --- (Internal) FSM Function onafterStart. + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @return #CTLD self + function CTLD:onafterStart(From, Event, To) + self:T({From, Event, To}) + self:I(self.lid .. "Started.") + if self.useprefix or self.enableHercules then + local prefix = self.prefixes + if self.enableHercules then + self.PilotGroups = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(prefix):FilterStart() + else + self.PilotGroups = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(prefix):FilterCategories("helicopter"):FilterStart() + end + else + self.PilotGroups = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterCategories("helicopter"):FilterStart() + end + -- Events + self:HandleEvent(EVENTS.PlayerEnterAircraft, self._EventHandler) + self:HandleEvent(EVENTS.PlayerEnterUnit, self._EventHandler) + self:HandleEvent(EVENTS.PlayerLeaveUnit, self._EventHandler) + self:__Status(-5) + return self + end + + --- (Internal) FSM Function onbeforeStatus. + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @return #CTLD self + function CTLD:onbeforeStatus(From, Event, To) + self:T({From, Event, To}) + self:CleanDroppedTroops() + self:_RefreshF10Menus() + self:_RefreshRadioBeacons() + self:CheckAutoHoverload() + return self + end + + --- (Internal) FSM Function onafterStatus. + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @return #CTLD self + function CTLD:onafterStatus(From, Event, To) + self:T({From, Event, To}) + -- gather some stats + -- pilots + local pilots = 0 + for _,_pilot in pairs (self.CtldUnits) do + pilots = pilots + 1 + end + + -- spawned cargo boxes curr in field + local boxes = 0 + for _,_pilot in pairs (self.Spawned_Cargo) do + boxes = boxes + 1 + end + + local cc = self.CargoCounter + local tc = self.TroopCounter + + if self.debug or self.verbose > 0 then + local text = string.format("%s Pilots %d | Live Crates %d |\nCargo Counter %d | Troop Counter %d", self.lid, pilots, boxes, cc, tc) + local m = MESSAGE:New(text,10,"CTLD"):ToAll() + end + self:__Status(-30) + return self + end + + --- (Internal) FSM Function onafterStop. + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @return #CTLD self + function CTLD:onafterStop(From, Event, To) + self:T({From, Event, To}) + self:UnhandleEvent(EVENTS.PlayerEnterAircraft) + self:UnhandleEvent(EVENTS.PlayerEnterUnit) + self:UnhandleEvent(EVENTS.PlayerLeaveUnit) + return self + end + + --- (Internal) FSM Function onbeforeTroopsPickedUp. + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param #CTLD_CARGO Cargo Cargo crate. + -- @return #CTLD self + function CTLD:onbeforeTroopsPickedUp(From, Event, To, Group, Unit, Cargo) + self:T({From, Event, To}) + return self + end + + --- (Internal) FSM Function onbeforeCratesPickedUp. + -- @param #CTLD self + -- @param #string From State . + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param #CTLD_CARGO Cargo Cargo crate. + -- @return #CTLD self + function CTLD:onbeforeCratesPickedUp(From, Event, To, Group, Unit, Cargo) + self:T({From, Event, To}) + return self + end + + --- (Internal) FSM Function onbeforeTroopsExtracted. + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param Wrapper.Group#GROUP Troops Troops #GROUP Object. + -- @return #CTLD self + function CTLD:onbeforeTroopsExtracted(From, Event, To, Group, Unit, Troops) + self:T({From, Event, To}) + return self + end + + + --- (Internal) FSM Function onbeforeTroopsDeployed. + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param Wrapper.Group#GROUP Troops Troops #GROUP Object. + -- @return #CTLD self + function CTLD:onbeforeTroopsDeployed(From, Event, To, Group, Unit, Troops) + self:T({From, Event, To}) + return self + end + + --- (Internal) FSM Function onbeforeCratesDropped. + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param #table Cargotable Table of #CTLD_CARGO objects dropped. + -- @return #CTLD self + function CTLD:onbeforeCratesDropped(From, Event, To, Group, Unit, Cargotable) + self:T({From, Event, To}) + return self + end + + --- (Internal) FSM Function onbeforeCratesBuild. + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @param Wrapper.Group#GROUP Vehicle The #GROUP object of the vehicle or FOB build. + -- @return #CTLD self + function CTLD:onbeforeCratesBuild(From, Event, To, Group, Unit, Vehicle) + self:T({From, Event, To}) + return self + end + + --- (Internal) FSM Function onbeforeTroopsRTB. + -- @param #CTLD self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Group#GROUP Group Group Object. + -- @param Wrapper.Unit#UNIT Unit Unit Object. + -- @return #CTLD self + function CTLD:onbeforeTroopsRTB(From, Event, To, Group, Unit) + self:T({From, Event, To}) + return self + end + +end -- end do +------------------------------------------------------------------- +-- End Ops.CTLD.lua +------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/FlightGroup.lua b/Moose Development/Moose/Ops/FlightGroup.lua index d71f76d0e..aeed5a782 100644 --- a/Moose Development/Moose/Ops/FlightGroup.lua +++ b/Moose Development/Moose/Ops/FlightGroup.lua @@ -17,13 +17,13 @@ -- === -- -- ## Example Missions: --- +-- -- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20Flightgroup). --- +-- -- === -- -- ### Author: **funkyfranky** --- +-- -- === -- @module Ops.FlightGroup -- @image OPS_FlightGroup.png @@ -31,10 +31,6 @@ --- FLIGHTGROUP class. -- @type FLIGHTGROUP --- @field Wrapper.Airbase#AIRBASE homebase The home base of the flight group. --- @field Wrapper.Airbase#AIRBASE destbase The destination base of the flight group. --- @field Core.Zone#ZONE homezone The home zone of the flight group. Set when spawn happens in air. --- @field Core.Zone#ZONE destzone The destination zone of the flight group. Set when final waypoint is in air. -- @field #string actype Type name of the aircraft. -- @field #number rangemax Max range in km. -- @field #number ceiling Max altitude the aircraft can fly at in meters. @@ -57,10 +53,8 @@ -- @field #number Tparking Abs. mission time stamp when the group was spawned uncontrolled and is parking. -- @field #table menu F10 radio menu. -- @field #string controlstatus Flight control status. --- @field #boolean ishelo If true, the is a helicopter group. --- @field #number callsignName Callsign name. --- @field #number callsignNumber Callsign number. -- @field #boolean despawnAfterLanding If true, group is despawned after landed at an airbase. +-- @field #number RTBRecallCount Number that counts RTB calls. -- -- @extends Ops.OpsGroup#OPSGROUP @@ -140,13 +134,17 @@ FLIGHTGROUP = { fuelcritical = nil, fuelcriticalthresh = nil, fuelcriticalrtb = false, + outofAAMrtb = false, + outofAGMrtb = false, squadron = nil, flightcontrol = nil, flaghold = nil, Tholding = nil, Tparking = nil, + Twaiting = nil, menu = nil, - ishelo = nil, + isHelo = nil, + RTBRecallCount = 0, } @@ -173,44 +171,21 @@ FLIGHTGROUP.Attribute = { OTHER="Other", } ---- Flight group element. --- @type FLIGHTGROUP.Element --- @field #string name Name of the element, i.e. the unit/client. --- @field Wrapper.Unit#UNIT unit Element unit object. --- @field Wrapper.Group#GROUP group Group object of the element. --- @field #string modex Tail number. --- @field #string skill Skill level. --- @field #boolean ai If true, element is AI. --- @field Wrapper.Client#CLIENT client The client if element is occupied by a human player. --- @field #table pylons Table of pylons. --- @field #number fuelmass Mass of fuel in kg. --- @field #number category Aircraft category. --- @field #string categoryname Aircraft category name. --- @field #string callsign Call sign, e.g. "Uzi 1-1". --- @field #string status Status, i.e. born, parking, taxiing. See @{#OPSGROUP.ElementStatus}. --- @field #number damage Damage of element in percent. --- @field Wrapper.Airbase#AIRBASE.ParkingSpot parking The parking spot table the element is parking on. - - --- FLIGHTGROUP class version. -- @field #string version -FLIGHTGROUP.version="0.6.0" +FLIGHTGROUP.version="0.7.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: VTOL aircraft. --- TODO: Use new UnitLost event instead of crash/dead. --- TODO: Options EPLRS, Afterburner restrict etc. --- DONE: Add TACAN beacon. --- TODO: Damage? --- TODO: shot events? --- TODO: Marks to add waypoints/tasks on-the-fly. -- TODO: Mark assigned parking spot on F10 map. -- TODO: Let user request a parking spot via F10 marker :) --- TODO: Monitor traveled distance in air ==> calculate fuel consumption ==> calculate range remaining. Will this give half way accurate results? --- TODO: Out of AG/AA missiles. Safe state of out-of-ammo. +-- DONE: Use new UnitLost event instead of crash/dead. +-- DONE: Monitor traveled distance in air ==> calculate fuel consumption ==> calculate range remaining. Will this give half way accurate results? +-- DONE: Out of AG/AA missiles. Safe state of out-of-ammo. +-- DONE: Add TACAN beacon. -- DONE: Add tasks. -- DONE: Waypoints, read, add, insert, detour. -- DONE: Get ammo. @@ -230,10 +205,10 @@ FLIGHTGROUP.version="0.6.0" function FLIGHTGROUP:New(group) -- First check if we already have a flight group for this group. - local fg=_DATABASE:GetFlightGroup(group) - if fg then - fg:I(fg.lid..string.format("WARNING: Flight group already exists in data base!")) - return fg + 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. @@ -243,7 +218,7 @@ function FLIGHTGROUP:New(group) self.lid=string.format("FLIGHTGROUP %s | ", self.groupname) -- Defaults - --self:SetVerbosity(0) + self.isFlightgroup=true self:SetFuelLowThreshold() self:SetFuelLowRTB() self:SetFuelCriticalThreshold() @@ -258,6 +233,7 @@ function FLIGHTGROUP:New(group) -- Add FSM transitions. -- From State --> Event --> To State + self:AddTransition("*", "LandAtAirbase", "Inbound") -- Helo group is ordered to land at a specific point. self:AddTransition("*", "RTB", "Inbound") -- Group is returning to destination base. self:AddTransition("*", "RTZ", "Inbound") -- Group is returning to destination zone. Not implemented yet! self:AddTransition("Inbound", "Holding", "Holding") -- Group is in holding pattern. @@ -268,19 +244,16 @@ function FLIGHTGROUP:New(group) self:AddTransition("*", "LandAt", "LandingAt") -- Helo group is ordered to land at a specific point. self:AddTransition("LandingAt", "LandedAt", "LandedAt") -- Helo group landed landed at a specific point. - self:AddTransition("*", "Wait", "*") -- Group is orbiting. - self:AddTransition("*", "FuelLow", "*") -- Fuel state of group is low. Default ~25%. self:AddTransition("*", "FuelCritical", "*") -- Fuel state of group is critical. Default ~10%. - self:AddTransition("*", "OutOfMissilesAA", "*") -- Group is out of A2A missiles. Not implemented yet! - self:AddTransition("*", "OutOfMissilesAG", "*") -- Group is out of A2G missiles. Not implemented yet! - self:AddTransition("*", "OutOfMissilesAS", "*") -- Group is out of A2G missiles. Not implemented yet! + self:AddTransition("*", "OutOfMissilesAA", "*") -- Group is out of A2A (air) missiles. + self:AddTransition("*", "OutOfMissilesAG", "*") -- Group is out of A2G (ground) missiles. + self:AddTransition("*", "OutOfMissilesAS", "*") -- Group is out of A2S (ship) missiles. - self:AddTransition("Airborne", "EngageTargets", "Engaging") -- Engage targets. + self:AddTransition("Airborne", "EngageTarget", "Engaging") -- Engage targets. self:AddTransition("Engaging", "Disengage", "Airborne") -- Engagement over. - self:AddTransition("*", "ElementParking", "*") -- An element is parking. self:AddTransition("*", "ElementEngineOn", "*") -- An element spooled up the engines. self:AddTransition("*", "ElementTaxiing", "*") -- An element is taxiing to the runway. @@ -291,11 +264,11 @@ function FLIGHTGROUP:New(group) self:AddTransition("*", "ElementOutOfAmmo", "*") -- An element is completely out of ammo. - self:AddTransition("*", "Parking", "Parking") -- The whole flight group is parking. self:AddTransition("*", "Taxiing", "Taxiing") -- The whole flight group is taxiing. self:AddTransition("*", "Takeoff", "Airborne") -- The whole flight group is airborne. self:AddTransition("*", "Airborne", "Airborne") -- The whole flight group is airborne. + self:AddTransition("*", "Cruise", "Cruising") -- The whole flight group is cruising. self:AddTransition("*", "Landing", "Landing") -- The whole flight group is landing. self:AddTransition("*", "Landed", "Landed") -- The whole flight group has landed. self:AddTransition("*", "Arrived", "Arrived") -- The whole flight group has arrived. @@ -315,16 +288,6 @@ function FLIGHTGROUP:New(group) -- TODO: Add pseudo functions. - -- Debug trace. - if false then - BASE:TraceOnOff(true) - BASE:TraceClass(self.ClassName) - BASE:TraceLevel(1) - end - - -- Add to data base. - _DATABASE:AddFlightGroup(self) - -- Handle events: self:HandleEvent(EVENTS.Birth, self.OnEventBirth) self:HandleEvent(EVENTS.EngineStartup, self.OnEventEngineStartup) @@ -339,20 +302,23 @@ function FLIGHTGROUP:New(group) self:HandleEvent(EVENTS.Kill, self.OnEventKill) -- Init waypoints. - self:InitWaypoints() + self:_InitWaypoints() -- Initialize group. self:_InitGroup() -- Start the status monitoring. self:__Status(-1) - + -- Start queue update timer. self.timerQueueUpdate=TIMER:New(self._QueueUpdate, self):Start(2, 5) - + -- Start check zone timer. self.timerCheckZone=TIMER:New(self._CheckInZones, self):Start(3, 10) + -- Add OPSGROUP to _DATABASE. + _DATABASE:AddOpsGroup(self) + return self end @@ -380,6 +346,14 @@ function FLIGHTGROUP:SetAirwing(airwing) return self end +--- Set if aircraft is VTOL capable. Unfortunately, there is no DCS way to determine this via scripting. +-- @param #FLIGHTGROUP self +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetVTOL() + self.isVTOL=true + return self +end + --- Get airwing the flight group belongs to. -- @param #FLIGHTGROUP self -- @return Ops.AirWing#AIRWING The AIRWING object. @@ -432,6 +406,9 @@ end -- @param Wrapper.Airbase#AIRBASE HomeAirbase The home airbase. -- @return #FLIGHTGROUP self function FLIGHTGROUP:SetHomebase(HomeAirbase) + if type(HomeAirbase)=="string" then + HomeAirbase=AIRBASE:FindByName(HomeAirbase) + end self.homebase=HomeAirbase return self end @@ -441,6 +418,9 @@ end -- @param Wrapper.Airbase#AIRBASE DestinationAirbase The destination airbase. -- @return #FLIGHTGROUP self function FLIGHTGROUP:SetDestinationbase(DestinationAirbase) + if type(DestinationAirbase)=="string" then + DestinationAirbase=AIRBASE:FindByName(DestinationAirbase) + end self.destbase=DestinationAirbase return self end @@ -477,6 +457,32 @@ function FLIGHTGROUP:SetFuelLowRTB(switch) return self end +--- Set if flight is out of Air-Air-Missiles, flight goes RTB. +-- @param #FLIGHTGROUP self +-- @param #boolean switch If true or nil, flight goes RTB. If false, turn this off. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetOutOfAAMRTB(switch) + if switch==false then + self.outofAAMrtb=false + else + self.outofAAMrtb=true + end + return self +end + +--- Set if flight is out of Air-Ground-Missiles, flight goes RTB. +-- @param #FLIGHTGROUP self +-- @param #boolean switch If true or nil, flight goes RTB. If false, turn this off. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetOutOfAGMRTB(switch) + if switch==false then + self.outofAGMrtb=false + else + self.outofAGMrtb=true + end + return self +end + --- Set if low fuel threshold is reached, flight tries to refuel at the neares tanker. -- @param #FLIGHTGROUP self -- @param #boolean switch If true or nil, flight goes for refuelling. If false, turn this off. @@ -512,6 +518,56 @@ function FLIGHTGROUP:SetFuelCriticalRTB(switch) return self end +--- Enable to automatically engage detected targets. +-- @param #FLIGHTGROUP self +-- @param #number RangeMax Max range in NM. Only detected targets within this radius from the group will be engaged. Default is 25 NM. +-- @param #table TargetTypes Types of target attributes that will be engaged. See [DCS enum attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes). Default "All". +-- @param Core.Set#SET_ZONE EngageZoneSet Set of zones in which targets are engaged. Default is anywhere. +-- @param Core.Set#SET_ZONE NoEngageZoneSet Set of zones in which targets are *not* engaged. Default is nowhere. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetEngageDetectedOn(RangeMax, TargetTypes, EngageZoneSet, NoEngageZoneSet) + + -- Ensure table. + if TargetTypes then + if type(TargetTypes)~="table" then + TargetTypes={TargetTypes} + end + else + TargetTypes={"All"} + end + + -- Ensure SET_ZONE if ZONE is provided. + if EngageZoneSet and EngageZoneSet:IsInstanceOf("ZONE_BASE") then + local zoneset=SET_ZONE:New():AddZone(EngageZoneSet) + EngageZoneSet=zoneset + end + if NoEngageZoneSet and NoEngageZoneSet:IsInstanceOf("ZONE_BASE") then + local zoneset=SET_ZONE:New():AddZone(NoEngageZoneSet) + NoEngageZoneSet=zoneset + end + + -- Set parameters. + self.engagedetectedOn=true + self.engagedetectedRmax=UTILS.NMToMeters(RangeMax or 25) + self.engagedetectedTypes=TargetTypes + self.engagedetectedEngageZones=EngageZoneSet + self.engagedetectedNoEngageZones=NoEngageZoneSet + + -- Ensure detection is ON or it does not make any sense. + self:SetDetection(true) + + return self +end + +--- Disable to automatically engage detected targets. +-- @param #FLIGHTGROUP self +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetEngageDetectedOff() + self.engagedetectedOn=false + return self +end + + --- Enable that the group is despawned after landing. This can be useful to avoid DCS taxi issues with other AI or players or jamming taxiways. -- @param #FLIGHTGROUP self -- @return #FLIGHTGROUP self @@ -535,18 +591,18 @@ function FLIGHTGROUP:IsTaxiing() return self:Is("Taxiing") end ---- Check if flight is airborne. +--- Check if flight is airborne or cruising. -- @param #FLIGHTGROUP self -- @return #boolean If true, flight is airborne. function FLIGHTGROUP:IsAirborne() - return self:Is("Airborne") + return self:Is("Airborne") or self:Is("Cruising") end ---- Check if flight is waiting after passing final waypoint. +--- Check if flight is airborne or cruising. -- @param #FLIGHTGROUP self --- @return #boolean If true, flight is waiting. -function FLIGHTGROUP:IsWaiting() - return self:Is("Waiting") +-- @return #boolean If true, flight is airborne. +function FLIGHTGROUP:IsCruising() + return self:Is("Cruising") end --- Check if flight is landing. @@ -658,11 +714,18 @@ function FLIGHTGROUP:StartUncontrolled(delay) self:ScheduleOnce(delay, FLIGHTGROUP.StartUncontrolled, self) else - if self:IsAlive() then - --TODO: check Alive==true and Alive==false ==> Activate first + local alive=self:IsAlive() + + if alive~=nil then + -- Check if group is already active. + local _delay=0 + if alive==false then + self:Activate() + _delay=1 + end self:T(self.lid.."Starting uncontrolled group") - self.group:StartUncontrolled(delay) - self.isUncontrolled=true + self.group:StartUncontrolled(_delay) + self.isUncontrolled=false else self:E(self.lid.."ERROR: Could not start uncontrolled group as it is NOT alive!") end @@ -698,7 +761,7 @@ function FLIGHTGROUP:GetFuelMin() local fuelmin=math.huge for i,_element in pairs(self.elements) do - local element=_element --#FLIGHTGROUP.Element + local element=_element --Ops.OpsGroup#OPSGROUP.Element local unit=element.unit @@ -732,46 +795,46 @@ function FLIGHTGROUP:onbeforeStatus(From, Event, To) -- First we check if elements are still alive. Could be that they were despawned without notice, e.g. when landing on a too small airbase. for i,_element in pairs(self.elements) do - local element=_element --#FLIGHTGROUP.Element - + local element=_element --Ops.OpsGroup#OPSGROUP.Element + -- Check that element is not already dead or not yet alive. if element.status~=OPSGROUP.ElementStatus.DEAD and element.status~=OPSGROUP.ElementStatus.INUTERO then - + -- Unit shortcut. local unit=element.unit - - local isdead=false + + local isdead=false if unit and unit:IsAlive() then - + -- Get life points. local life=unit:GetLife() or 0 - + -- Units with life <=1 are dead. if life<=1 then --env.info(string.format("FF unit %s: live<=1 in status at T=%.3f", unit:GetName(), timer.getTime())) isdead=true end - + else -- Not alive any more. --env.info(string.format("FF unit %s: NOT alive in status at T=%.3f", unit:GetName(), timer.getTime())) isdead=true end - + -- This one is dead. if isdead then - local text=string.format("Element %s is dead at t=%.3f! Maybe despawned without notice or landed at a too small airbase. Calling ElementDead in 60 sec to give other events a chance", - tostring(element.name), timer.getTime()) + local text=string.format("Element %s is dead at t=%.3f but has status %s! Maybe despawned without notice or landed at a too small airbase. Calling ElementDead in 60 sec to give other events a chance", + tostring(element.name), timer.getTime(), tostring(element.status)) self:E(self.lid..text) self:__ElementDead(60, element) end - - end + + end end - if self:IsDead() then + if self:IsDead() then self:T(self.lid..string.format("Onbefore Status DEAD ==> false")) - return false + return false elseif self:IsStopped() then self:T(self.lid..string.format("Onbefore Status STOPPED ==> false")) return false @@ -789,7 +852,7 @@ function FLIGHTGROUP:onafterStatus(From, Event, To) -- FSM state. local fsmstate=self:GetState() - + -- Update position. self:_UpdatePosition() @@ -809,7 +872,7 @@ function FLIGHTGROUP:onafterStatus(From, Event, To) -- Check if flight began to taxi (if it was parking). if self:IsParking() then for _,_element in pairs(self.elements) do - local element=_element --#FLIGHTGROUP.Element + local element=_element --Ops.OpsGroup#OPSGROUP.Element if element.parking then -- Get distance to assigned parking spot. @@ -836,32 +899,34 @@ function FLIGHTGROUP:onafterStatus(From, Event, To) -- Short info. if self.verbose>=1 then - + local nTaskTot, nTaskSched, nTaskWP=self:CountRemainingTasks() local nMissions=self:CountRemainingMissison() - - + + local text=string.format("Status %s [%d/%d]: Tasks=%d (%d,%d) Curr=%d, Missions=%s, Waypoint=%d/%d, Detected=%d, Home=%s, Destination=%s", fsmstate, #self.elements, #self.elements, nTaskTot, nTaskSched, nTaskWP, self.taskcurrent, nMissions, self.currentwp or 0, self.waypoints and #self.waypoints or 0, self.detectedunits:Count(), self.homebase and self.homebase:GetName() or "unknown", self.destbase and self.destbase:GetName() or "unknown") self:I(self.lid..text) - + end --- -- Elements --- - + if self.verbose>=2 then local text="Elements:" for i,_element in pairs(self.elements) do - local element=_element --#FLIGHTGROUP.Element + local element=_element --Ops.OpsGroup#OPSGROUP.Element local name=element.name local status=element.status local unit=element.unit local fuel=unit:GetFuel() or 0 local life=unit:GetLifeRelative() or 0 + local lp=unit:GetLife() + local lp0=unit:GetLife0() local parking=element.parking and tostring(element.parking.TerminalID) or "X" -- Check if element is not dead and we missed an event. @@ -873,8 +938,8 @@ function FLIGHTGROUP:onafterStatus(From, Event, To) local ammo=self:GetAmmoElement(element) -- Output text for element. - text=text..string.format("\n[%d] %s: status=%s, fuel=%.1f, life=%.1f, guns=%d, rockets=%d, bombs=%d, missiles=%d (AA=%d, AG=%d, AS=%s), parking=%s", - i, name, status, fuel*100, life*100, ammo.Guns, ammo.Rockets, ammo.Bombs, ammo.Missiles, ammo.MissilesAA, ammo.MissilesAG, ammo.MissilesAS, parking) + text=text..string.format("\n[%d] %s: status=%s, fuel=%.1f, life=%.1f [%.1f/%.1f], guns=%d, rockets=%d, bombs=%d, missiles=%d (AA=%d, AG=%d, AS=%s), parking=%s", + i, name, status, fuel*100, life*100, lp, lp0, ammo.Guns, ammo.Rockets, ammo.Bombs, ammo.Missiles, ammo.MissilesAA, ammo.MissilesAG, ammo.MissilesAS, parking) end if #self.elements==0 then text=text.." none!" @@ -886,7 +951,7 @@ function FLIGHTGROUP:onafterStatus(From, Event, To) -- Distance travelled --- - if self.verbose>=3 and self:IsAlive() then + if self.verbose>=4 and self:IsAlive() then -- Travelled distance since last check. local ds=self.travelds @@ -902,7 +967,7 @@ function FLIGHTGROUP:onafterStatus(From, Event, To) local TmaxFuel=math.huge for _,_element in pairs(self.elements) do - local element=_element --#FLIGHTGROUP.Element + local element=_element --Ops.OpsGroup#OPSGROUP.Element -- Get relative fuel of element. local fuel=element.unit:GetFuel() or 0 @@ -929,7 +994,7 @@ function FLIGHTGROUP:onafterStatus(From, Event, To) -- Log outut. self:I(self.lid..string.format("Travelled ds=%.1f km dt=%.1f s ==> v=%.1f knots. Fuel left for %.1f min", self.traveldist/1000, dt, UTILS.MpsToKnots(v), TmaxFuel/60)) - + end --- @@ -947,6 +1012,9 @@ function FLIGHTGROUP:onafterStatus(From, Event, To) local fuelmin=self:GetFuelMin() + -- Debug info. + self:T2(self.lid..string.format("Fuel state=%d", fuelmin)) + if fuelmin>=self.fuellowthresh then self.fuellow=false end @@ -966,17 +1034,135 @@ function FLIGHTGROUP:onafterStatus(From, Event, To) self:FuelCritical() end + -- This causes severe problems as OutOfMissiles is called over and over again leading to many RTB calls. + if false then + + -- Out of AA Missiles? CAP, GCICAP, INTERCEPT + local CurrIsCap = false + -- Out of AG Missiles? BAI, SEAD, CAS, STRIKE + local CurrIsA2G = false + -- Check AUFTRAG Type + local CurrAuftrag = self:GetMissionCurrent() + if CurrAuftrag then + local CurrAuftragType = CurrAuftrag:GetType() + if CurrAuftragType == "CAP" or CurrAuftragType == "GCICAP" or CurrAuftragType == "INTERCEPT" then CurrIsCap = true end + if CurrAuftragType == "BAI" or CurrAuftragType == "CAS" or CurrAuftragType == "SEAD" or CurrAuftragType == "STRIKE" then CurrIsA2G = true end + end + + -- Check A2A + if (not self:CanAirToAir(true)) and CurrIsCap then + self:OutOfMissilesAA() + end + + -- Check A2G + if (not self:CanAirToGround(false)) and CurrIsA2G then + self:OutOfMissilesAG() + end + + end + end --- -- Airboss Helo --- - if self.ishelo and self.airboss and self:IsHolding() then + if self.isHelo and self.airboss and self:IsHolding() then if self.airboss:IsRecovering() or self:IsFuelCritical() then self:ClearToLand() end end + --- + -- Engage Detected Targets + --- + if self:IsAirborne() and self.detectionOn and self.engagedetectedOn and not (self.fuellow or self.fuelcritical) then + + -- Target. + local targetgroup=nil --Wrapper.Group#GROUP + local targetdist=math.huge + + -- Loop over detected groups. + for _,_group in pairs(self.detectedgroups:GetSet()) do + local group=_group --Wrapper.Group#GROUP + + if group and group:IsAlive() then + + -- Get 3D vector of target. + local targetVec3=group:GetVec3() + + -- Distance to target. + local distance=UTILS.VecDist3D(self.position, targetVec3) + + if distance<=self.engagedetectedRmax and distance See also OPSGROUP ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Flightgroup event function, handling the birth of a unit. --- @param #FLIGHTGROUP self --- @param Core.Event#EVENTDATA EventData Event data. -function FLIGHTGROUP:OnEventBirth(EventData) - - --env.info(string.format("EVENT: Birth for unit %s", tostring(EventData.IniUnitName))) - - -- 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 - - -- Set group. - self.group=self.group or EventData.IniGroup - - 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 - - -- Set homebase if not already set. - if EventData.Place then - self.homebase=self.homebase or EventData.Place - end - - if self.homebase and not self.destbase then - self.destbase=self.homebase - end - - -- Get element. - local element=self:GetElementByName(unitname) - - -- Create element spawned event if not already present. - if not self:_IsElement(unitname) then - element=self:AddElementByName(unitname) - end - - -- Set element to spawned state. - self:T(self.lid..string.format("EVENT: Element %s born at airbase %s==> spawned", element.name, self.homebase and self.homebase:GetName() or "unknown")) - self:ElementSpawned(element) - - end - - end - -end - --- Flightgroup event function handling the crash of a unit. -- @param #FLIGHTGROUP self -- @param Core.Event#EVENTDATA EventData Event data. @@ -1189,7 +1320,7 @@ function FLIGHTGROUP:OnEventCrash(EventData) local element=self:GetElementByName(unitname) if element and element.status~=OPSGROUP.ElementStatus.DEAD then - self:T(self.lid..string.format("EVENT: Element %s crashed ==> destroyed", element.name)) + self:T(self.lid..string.format("EVENT: Element %s crashed ==> destroyed", element.name)) self:ElementDestroyed(element) end @@ -1205,7 +1336,7 @@ function FLIGHTGROUP:OnEventUnitLost(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:T2(self.lid..string.format("EVENT: Unit %s lost at t=%.3f", EventData.IniUnitName, timer.getTime())) - + local unit=EventData.IniUnit local group=EventData.IniGroup local unitname=EventData.IniUnitName @@ -1217,75 +1348,13 @@ function FLIGHTGROUP:OnEventUnitLost(EventData) self:T(self.lid..string.format("EVENT: Element %s unit lost ==> destroyed t=%.3f", element.name, timer.getTime())) self:ElementDestroyed(element) end - - end - -end - ---- Flightgroup event function handling the crash of a unit. --- @param #FLIGHTGROUP self --- @param Core.Event#EVENTDATA EventData Event data. -function FLIGHTGROUP:OnEventKill(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 - - -- Target name - local targetname=tostring(EventData.TgtUnitName) - - -- Debug info. - self:T2(self.lid..string.format("EVENT: Unit %s killed object %s!", tostring(EventData.IniUnitName), targetname)) - - -- Check if this was a UNIT or STATIC object. - local target=UNIT:FindByName(targetname) - if not target then - target=STATIC:FindByName(targetname, false) - end - - -- Only count UNITS and STATICs (not SCENERY) - if target then - - -- Debug info. - self:T(self.lid..string.format("EVENT: Unit %s killed unit/static %s!", tostring(EventData.IniUnitName), targetname)) - - -- Kill counter. - self.Nkills=self.Nkills+1 - - -- Check if on a mission. - local mission=self:GetMissionCurrent() - if mission then - mission.Nkills=mission.Nkills+1 -- Increase mission kill counter. - end - - end - - end - -end - ---- Flightgroup event function handling the crash of a unit. --- @param #FLIGHTGROUP self --- @param Core.Event#EVENTDATA EventData Event data. -function FLIGHTGROUP: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:T3(self.lid..string.format("EVENT: Element %s removed ==> dead", element.name)) - self:ElementDead(element) - end end end + + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- FSM functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1295,14 +1364,16 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. function FLIGHTGROUP:onafterElementSpawned(From, Event, To, Element) + + -- Debug info. self:T(self.lid..string.format("Element spawned %s", Element.name)) -- Set element status. self:_UpdateStatus(Element, OPSGROUP.ElementStatus.SPAWNED) - if Element.unit:InAir() then + if Element.unit:InAir(true) then -- Trigger ElementAirborne event. Add a little delay because spawn is also delayed! self:__ElementAirborne(0.11, Element) else @@ -1321,6 +1392,7 @@ function FLIGHTGROUP:onafterElementSpawned(From, Event, To, Element) self:__ElementParking(0.11, Element) end end + end --- On after "ElementParking" event. @@ -1328,9 +1400,11 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. -- @param Wrapper.Airbase#AIRBASE.ParkingSpot Spot Parking Spot. function FLIGHTGROUP:onafterElementParking(From, Event, To, Element, Spot) + + -- Debug info. self:T(self.lid..string.format("Element parking %s at spot %s", Element.name, Element.parking and tostring(Element.parking.TerminalID) or "N/A")) -- Set element status. @@ -1347,6 +1421,7 @@ function FLIGHTGROUP:onafterElementParking(From, Event, To, Element, Spot) elseif self:IsTakeoffRunway() then self:__ElementEngineOn(0.5, Element) end + end --- On after "ElementEngineOn" event. @@ -1354,7 +1429,7 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. function FLIGHTGROUP:onafterElementEngineOn(From, Event, To, Element) -- Debug info. @@ -1362,6 +1437,7 @@ function FLIGHTGROUP:onafterElementEngineOn(From, Event, To, Element) -- Set element status. self:_UpdateStatus(Element, OPSGROUP.ElementStatus.ENGINEON) + end --- On after "ElementTaxiing" event. @@ -1369,7 +1445,7 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. function FLIGHTGROUP:onafterElementTaxiing(From, Event, To, Element) -- Get terminal ID. @@ -1383,6 +1459,7 @@ function FLIGHTGROUP:onafterElementTaxiing(From, Event, To, Element) -- Set element status. self:_UpdateStatus(Element, OPSGROUP.ElementStatus.TAXIING) + end --- On after "ElementTakeoff" event. @@ -1390,7 +1467,7 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. -- @param Wrapper.Airbase#AIRBASE airbase The airbase if applicable or nil. function FLIGHTGROUP:onafterElementTakeoff(From, Event, To, Element, airbase) self:T(self.lid..string.format("Element takeoff %s at %s airbase.", Element.name, airbase and airbase:GetName() or "unknown")) @@ -1404,7 +1481,8 @@ function FLIGHTGROUP:onafterElementTakeoff(From, Event, To, Element, airbase) self:_UpdateStatus(Element, OPSGROUP.ElementStatus.TAKEOFF, airbase) -- Trigger element airborne event. - self:__ElementAirborne(2, Element) + self:__ElementAirborne(0.01, Element) + end --- On after "ElementAirborne" event. @@ -1412,12 +1490,15 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. function FLIGHTGROUP:onafterElementAirborne(From, Event, To, Element) + + -- Debug info. self:T2(self.lid..string.format("Element airborne %s", Element.name)) -- Set element status. self:_UpdateStatus(Element, OPSGROUP.ElementStatus.AIRBORNE) + end --- On after "ElementLanded" event. @@ -1425,31 +1506,37 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. -- @param Wrapper.Airbase#AIRBASE airbase The airbase if applicable or nil. function FLIGHTGROUP:onafterElementLanded(From, Event, To, Element, airbase) + + -- Debug info. self:T2(self.lid..string.format("Element landed %s at %s airbase", Element.name, airbase and airbase:GetName() or "unknown")) - + if self.despawnAfterLanding then - + -- Despawn the element. self:DespawnElement(Element) - + else - -- Helos with skids land directly on parking spots. - if self.ishelo then - - local Spot=self:GetParkingSpot(Element, 10, airbase) - - self:_SetElementParkingAt(Element, Spot) - - end - -- Set element status. self:_UpdateStatus(Element, OPSGROUP.ElementStatus.LANDED, airbase) - + + -- Helos with skids land directly on parking spots. + if self.isHelo then + + local Spot=self:GetParkingSpot(Element, 10, airbase) + + if Spot then + self:_SetElementParkingAt(Element, Spot) + self:_UpdateStatus(Element, OPSGROUP.ElementStatus.ARRIVED) + end + + end + end + end --- On after "ElementArrived" event. @@ -1457,12 +1544,13 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. -- @param Wrapper.Airbase#AIRBASE airbase The airbase, where the element arrived. -- @param Wrapper.Airbase#AIRBASE.ParkingSpot Parking The Parking spot the element has. function FLIGHTGROUP:onafterElementArrived(From, Event, To, Element, airbase, Parking) self:T(self.lid..string.format("Element arrived %s at %s airbase using parking spot %d", Element.name, airbase and airbase:GetName() or "unknown", Parking and Parking.TerminalID or -99)) + -- Set element parking. self:_SetElementParkingAt(Element, Parking) -- Set element status. @@ -1474,12 +1562,12 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. function FLIGHTGROUP:onafterElementDestroyed(From, Event, To, Element) -- Call OPSGROUP function. self:GetParent(self).onafterElementDestroyed(self, From, Event, To, Element) - + end --- On after "ElementDead" event. @@ -1487,7 +1575,7 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #FLIGHTGROUP.Element Element The flight group element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. function FLIGHTGROUP:onafterElementDead(From, Event, To, Element) -- Call OPSGROUP function. @@ -1499,21 +1587,53 @@ function FLIGHTGROUP:onafterElementDead(From, Event, To, Element) -- Not parking any more. Element.parking=nil - + end ---- On after "Spawned" event. Sets the template, initializes the waypoints. +--- On after "Spawned" event. -- @param #FLIGHTGROUP self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. function FLIGHTGROUP:onafterSpawned(From, Event, To) self:T(self.lid..string.format("Flight spawned")) + + -- Debug info. + if self.verbose>=1 then + local text=string.format("Initialized Flight 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("Range max = %.1f km\n", self.rangemax/1000) + text=text..string.format("Ceiling = %.1f feet\n", UTILS.MetersToFeet(self.ceiling)) + 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("Tanker type = %s\n", tostring(self.tankertype)) + text=text..string.format("Refuel type = %s\n", tostring(self.refueltype)) + text=text..string.format("AI = %s\n", tostring(self.isAI)) + text=text..string.format("Helicopter = %s\n", tostring(self.group:IsHelicopter())) + 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/B=%d/M=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Rockets, self.ammo.Bombs, self.ammo.Missiles) + text=text..string.format("FSM state = %s\n", self:GetState()) + text=text..string.format("Is alive = %s\n", tostring(self.group:IsAlive())) + text=text..string.format("LateActivate = %s\n", tostring(self:IsLateActivated())) + text=text..string.format("Uncontrolled = %s\n", tostring(self:IsUncontrolled())) + text=text..string.format("Start Air = %s\n", tostring(self:IsTakeoffAir())) + text=text..string.format("Start Cold = %s\n", tostring(self:IsTakeoffCold())) + text=text..string.format("Start Hot = %s\n", tostring(self:IsTakeoffHot())) + text=text..string.format("Start Rwy = %s\n", tostring(self:IsTakeoffRunway())) + self:I(self.lid..text) + end - -- Update position. + -- Update position. self:_UpdatePosition() + -- Not dead or destroyed yet. + self.isDead=false + self.isDestroyed=false + if self.isAI then -- Set ROE. @@ -1521,32 +1641,35 @@ function FLIGHTGROUP:onafterSpawned(From, Event, To) -- Set ROT. self:SwitchROT(self.option.ROT) - + -- Set Formation self:SwitchFormation(self.option.Formation) - + -- Set TACAN beacon. self:_SwitchTACAN() - + -- Set radio freq and modu. if self.radioDefault then self:SwitchRadio() else self:SetDefaultRadio(self.radio.Freq, self.radio.Modu, self.radio.On) end - + -- Set callsign. if self.callsignDefault then self:SwitchCallsign(self.callsignDefault.NumberSquad, self.callsignDefault.NumberGroup) else self:SetDefaultCallsign(self.callsign.NumberSquad, self.callsign.NumberGroup) end - + -- TODO: make this input. self:GetGroup():SetOption(AI.Option.Air.id.PROHIBIT_JETT, true) self:GetGroup():SetOption(AI.Option.Air.id.PROHIBIT_AB, true) -- Does not seem to work. AI still used the after burner. self:GetGroup():SetOption(AI.Option.Air.id.RTB_ON_BINGO, false) - --self.group:SetOption(AI.Option.Air.id.RADAR_USING, AI.Option.Air.val.RADAR_USING.FOR_CONTINUOUS_SEARCH) + --self.group:SetOption(AI.Option.Air.id.RADAR_USING, AI.Option.Air.val.RADAR_USING.FOR_CONTINUOUS_SEARCH) + + -- Update status. + self:__Status(-0.1) -- Update route. self:__UpdateRoute(-0.5) @@ -1609,7 +1732,7 @@ function FLIGHTGROUP:onafterTaxiing(From, Event, To) self.Tparking=nil -- TODO: need a better check for the airbase. - local airbase=self:GetClosestAirbase() --self.group:GetCoordinate():GetClosestAirbase(nil, self.group:GetCoalition()) + local airbase=self:GetClosestAirbase() if self.flightcontrol and airbase and self.flightcontrol.airbasename==airbase:GetName() then @@ -1620,6 +1743,7 @@ function FLIGHTGROUP:onafterTaxiing(From, Event, To) else -- Human flights go to TAXI OUT queue. They will go to the ready for takeoff queue when they request it. self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.TAXIOUT) + -- Update menu. self:_UpdateMenu() end @@ -1653,11 +1777,56 @@ end function FLIGHTGROUP:onafterAirborne(From, Event, To) self:T(self.lid..string.format("Flight airborne")) + -- No current airbase any more. + self.currbase=nil + + -- Cruising. + self:__Cruise(-0.01) + +end + +--- On after "Cruising" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterCruise(From, Event, To) + self:T(self.lid..string.format("Flight cruising")) + + -- Not waiting anymore. + self.Twaiting=nil + self.dTwait=nil + if self.isAI then - self:_CheckGroupDone(1) + + --- + -- AI + --- + + if self:IsTransporting() then + if self.cargoTransport and self.cargoTransport.deployzone and self.cargoTransport.deployzone:IsInstanceOf("ZONE_AIRBASE") then + local airbase=self.cargoTransport.deployzone:GetAirbase() + self:LandAtAirbase(airbase) + end + elseif self:IsPickingup() then + if self.cargoTransport and self.cargoTransport.pickupzone and self.cargoTransport.pickupzone:IsInstanceOf("ZONE_AIRBASE") then + local airbase=self.cargoTransport.pickupzone:GetAirbase() + self:LandAtAirbase(airbase) + end + else + self:_CheckGroupDone(nil, 120) + end + else + + --- + -- CLIENT + --- + self:_UpdateMenu() + end + end --- On after "Landing" event. @@ -1686,7 +1855,7 @@ function FLIGHTGROUP:onafterLanded(From, Event, To, airbase) -- Add flight to taxiinb queue. self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.TAXIINB) end - + end --- On after "LandedAt" event. @@ -1695,9 +1864,16 @@ end -- @param #string Event Event. -- @param #string To To state. function FLIGHTGROUP:onafterLandedAt(From, Event, To) - self:T(self.lid..string.format("Flight landed at")) -end + self:T(self.lid..string.format("Flight landed at")) + -- Trigger (un-)loading process. + if self:IsPickingup() then + self:__Loading(-1) + elseif self:IsTransporting() then + self:__Unloading(-1) + end + +end --- On after "Arrived" event. -- @param #FLIGHTGROUP self @@ -1713,8 +1889,87 @@ function FLIGHTGROUP:onafterArrived(From, Event, To) self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.ARRIVED) end - -- Despawn in 5 min. - if not self.airwing then + -- Check what to do. + if self.airwing then + -- Add the asset back to the airwing. + self.airwing:AddAsset(self.group, 1) + elseif self.isLandingAtAirbase then + + local Template=UTILS.DeepCopy(self.template) --DCS#Template + + -- No late activation. + self.isLateActivated=false + Template.lateActivation=self.isLateActivated + + -- Spawn in uncontrolled state. + self.isUncontrolled=true + Template.uncontrolled=self.isUncontrolled + + -- First waypoint of the group. + local SpawnPoint=Template.route.points[1] + + -- These are only for ships and FARPS. + SpawnPoint.linkUnit = nil + SpawnPoint.helipadId = nil + SpawnPoint.airdromeId = nil + + -- Airbase. + local airbase=self.isLandingAtAirbase --Wrapper.Airbase#AIRBASE + + -- Get airbase ID and category. + local AirbaseID = airbase:GetID() + + -- Set airdromeId. + if airbase:IsShip() then + SpawnPoint.linkUnit = AirbaseID + SpawnPoint.helipadId = AirbaseID + elseif airbase:IsHelipad() then + SpawnPoint.linkUnit = AirbaseID + SpawnPoint.helipadId = AirbaseID + elseif airbase:IsAirdrome() then + SpawnPoint.airdromeId = AirbaseID + end + + -- Set waypoint type/action. + SpawnPoint.alt = 0 + SpawnPoint.type = COORDINATE.WaypointType.TakeOffParking + SpawnPoint.action = COORDINATE.WaypointAction.FromParkingArea + + local units=Template.units + + for i=#units,1,-1 do + local unit=units[i] + local element=self:GetElementByName(unit.name) + if element and element.status~=OPSGROUP.ElementStatus.DEAD then + unit.parking=element.parking and element.parking.TerminalID or nil + unit.parking_id=nil + local vec3=element.unit:GetVec3() + local heading=element.unit:GetHeading() + unit.x=vec3.x + unit.y=vec3.z + unit.alt=vec3.y + unit.heading=math.rad(heading) + unit.psi=-unit.heading + else + table.remove(units, i) + end + end + + -- Respawn with this template. + self:_Respawn(0, Template) + + -- Reset. + self.isLandingAtAirbase=nil + + -- Init (un-)loading process. + if self:IsPickingup() then + self:__Loading(-1) + elseif self:IsTransporting() then + self:__Unloading(-1) + end + + else + -- Depawn after 5 min. Important to trigger dead events before DCS despawns on its own without any notification. self:Despawn(5*60) end end @@ -1731,22 +1986,22 @@ function FLIGHTGROUP:onafterDead(From, Event, To) self.flightcontrol:_RemoveFlight(self) self.flightcontrol=nil end - + if self.Ndestroyed==#self.elements then if self.squadron then -- All elements were destroyed ==> Asset group is gone. self.squadron:DelGroup(self.groupname) - end + end else if self.airwing then -- Not all assets were destroyed (despawn) ==> Add asset back to airwing. - self.airwing:AddAsset(self.group, 1) + --self.airwing:AddAsset(self.group, 1) end - end + end -- Call OPSGROUP function. self:GetParent(self).onafterDead(self, From, Event, To) - + end @@ -1763,7 +2018,7 @@ function FLIGHTGROUP:onbeforeUpdateRoute(From, Event, To, n) local allowed=true local trepeat=nil - if self:IsAlive() then -- and (self:IsAirborne() or self:IsWaiting() or self:IsInbound() or self:IsHolding()) then + if self:IsAlive() then -- Alive & Airborne ==> Update route possible. self:T3(self.lid.."Update route possible. Group is ALIVE") elseif self:IsDead() then @@ -1795,8 +2050,24 @@ function FLIGHTGROUP:onbeforeUpdateRoute(From, Event, To, n) end if self.taskcurrent>0 then - self:E(self.lid.."Update route denied because taskcurrent>0") - allowed=false + + --local task=self:GetTaskCurrent() + local task=self:GetTaskByID(self.taskcurrent) + + if task then + if task.dcstask.id=="PatrolZone" then + -- For patrol zone, we need to allow the update. + else + local taskname=task and task.description or "No description" + self:E(self.lid..string.format("WARNING: Update route denied because taskcurrent=%d>0! Task description = %s", self.taskcurrent, tostring(taskname))) + allowed=false + end + else + -- Now this can happen, if we directly use TaskExecute as the task is not in the task queue and cannot be removed. + self:T(self.lid..string.format("WARNING: before update route taskcurrent=%d>0 but no task?!", self.taskcurrent)) + -- Anyhow, a task is running so we do not allow to update the route! + allowed=false + end end -- Not good, because mission will never start. Better only check if there is a current task! @@ -1838,10 +2109,17 @@ function FLIGHTGROUP:onafterUpdateRoute(From, Event, To, n) -- Current velocity. local speed=self.group and self.group:GetVelocityKMH() or 100 + -- Waypoint type. + local waypointType=COORDINATE.WaypointType.TurningPoint + if self:IsLanded() or self:IsLandedAt() or self:IsAirborne()==false then + -- Had some issues with passing waypoint function of the next WP called too ealy when the type is TurningPoint. Setting it to TakeOff solved it! + waypointType=COORDINATE.WaypointType.TakeOff + end + -- Set current waypoint or we get problem that the _PassingWaypoint function is triggered too early, i.e. right now and not when passing the next WP. - local current=self.group:GetCoordinate():WaypointAir(COORDINATE.WaypointAltType.BARO, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, speed, true, nil, {}, "Current") + local current=self.group:GetCoordinate():WaypointAir(COORDINATE.WaypointAltType.BARO, waypointType, COORDINATE.WaypointAction.TurningPoint, speed, true, nil, {}, "Current") table.insert(wp, current) - + local Nwp=self.waypoints and #self.waypoints or 0 -- Add remaining waypoints to route. @@ -1858,14 +2136,14 @@ function FLIGHTGROUP:onafterUpdateRoute(From, Event, To, n) if #wp>1 then -- Route group to all defined waypoints remaining. - self:Route(wp, 1) + self:Route(wp) else --- -- No waypoints left --- - + if self:IsAirborne() then self:T(self.lid.."No waypoints left ==> CheckGroupDone") self:_CheckGroupDone() @@ -1875,24 +2153,32 @@ function FLIGHTGROUP:onafterUpdateRoute(From, Event, To, n) end ---- On after "Respawn" event. +--- On after "OutOfMissilesAA" event. -- @param #FLIGHTGROUP self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #table Template The template used to respawn the group. -function FLIGHTGROUP:onafterRespawn(From, Event, To, Template) - - self:T(self.lid.."Respawning group!") - - local template=UTILS.DeepCopy(Template or self.template) - - if self.group and self.group:InAir() then - template.lateActivation=false - self.respawning=true - self.group=self.group:Respawn(template) +function FLIGHTGROUP:onafterOutOfMissilesAA(From, Event, To) + self:I(self.lid.."Group is out of AA Missiles!") + if self.outofAAMrtb then + -- Back to destination or home. + local airbase=self.destbase or self.homebase + self:__RTB(-5, airbase) end +end +--- On after "OutOfMissilesAG" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterOutOfMissilesAG(From, Event, To) + self:I(self.lid.."Group is out of AG Missiles!") + if self.outofAGMrtb then + -- Back to destination or home. + local airbase=self.destbase or self.homebase + self:__RTB(-5, airbase) + end end --- Check if flight is done, i.e. @@ -1905,7 +2191,8 @@ end -- -- @param #FLIGHTGROUP self -- @param #number delay Delay in seconds. -function FLIGHTGROUP:_CheckGroupDone(delay) +-- @param #number waittime Time to wait if group is done. +function FLIGHTGROUP:_CheckGroupDone(delay, waittime) if self:IsAlive() and self.isAI then @@ -1914,37 +2201,72 @@ function FLIGHTGROUP:_CheckGroupDone(delay) self:ScheduleOnce(delay, FLIGHTGROUP._CheckGroupDone, self) else + -- Debug info. + self:T(self.lid.."Check FLIGHTGROUP done?") + -- First check if there is a paused mission that if self.missionpaused then self:UnpauseMission() return end + -- Group is currently engaging. + if self:IsEngaging() then + self:T(self.lid.."Engaging! Group NOT done...") + return + end + + -- Group is ordered to land at an airbase. + if self.isLandingAtAirbase then + self:T(self.lid.."Landing at airbase! Group NOT done...") + return + end + + -- Group is waiting. + if self:IsWaiting() then + self:T(self.lid.."Waiting! Group NOT done...") + return + end + -- Number of tasks remaining. local nTasks=self:CountRemainingTasks() -- Number of mission remaining. local nMissions=self:CountRemainingMissison() + -- Number of cargo transports remaining. + local nTransports=self:CountRemainingTransports() + + -- Debug info. + self:T(self.lid..string.format("Remaining (final=%s): missions=%d, tasks=%d, transports=%d", tostring(self.passedfinalwp), nMissions, nTasks, nTransports)) + -- Final waypoint passed? if self.passedfinalwp then + + --- + -- Final Waypoint PASSED + --- -- Got current mission or task? - if self.currentmission==nil and self.taskcurrent==0 then + if self.currentmission==nil and self.taskcurrent==0 and self.cargoTransport==nil then -- Number of remaining tasks/missions? - if nTasks==0 and nMissions==0 then - + if nTasks==0 and nMissions==0 and nTransports==0 then + local destbase=self.destbase or self.homebase local destzone=self.destzone or self.homezone -- Send flight to destination. - if destbase then - self:T(self.lid.."Passed Final WP and No current and/or future missions/task ==> RTB!") - self:__RTB(-3, destbase) + if waittime then + self:T(self.lid..string.format("Passed Final WP and No current and/or future missions/tasks/transports. Waittime given ==> Waiting for %d sec!", waittime)) + self:Wait(waittime) + elseif destbase then + self:T(self.lid.."Passed Final WP and No current and/or future missions/tasks/transports ==> RTB!") + --self:RTB(destbase) + self:__RTB(-0.1, destbase) elseif destzone then - self:T(self.lid.."Passed Final WP and No current and/or future missions/task ==> RTZ!") - self:__RTZ(-3, destzone) + self:T(self.lid.."Passed Final WP and No current and/or future missions/tasks/transports ==> RTZ!") + self:__RTZ(-0.1, destzone) else self:T(self.lid.."Passed Final WP and NO Tasks/Missions left. No DestBase or DestZone ==> Wait!") self:__Wait(-1) @@ -1958,8 +2280,17 @@ function FLIGHTGROUP:_CheckGroupDone(delay) self:T(self.lid..string.format("Passed Final WP but still have current Task (#%s) or Mission (#%s) left to do", tostring(self.taskcurrent), tostring(self.currentmission))) end else + + --- + -- Final Waypoint NOT PASSED + --- + + -- Debug info. self:T(self.lid..string.format("Flight (status=%s) did NOT pass the final waypoint yet ==> update route", self:GetState())) + + -- Update route. self:__UpdateRoute(-1) + end end @@ -1989,14 +2320,36 @@ function FLIGHTGROUP:onbeforeRTB(From, Event, To, airbase, SpeedTo, SpeedHold) -- Check that coaliton is okay. We allow same (blue=blue, red=red) or landing on neutral bases. if airbase and airbase:GetCoalition()~=self.group:GetCoalition() and airbase:GetCoalition()>0 then - self:E(self.lid..string.format("ERROR: Wrong airbase coalition %d in RTB() call! We allow only same as group %d or neutral airbases 0.", airbase:GetCoalition(), self.group:GetCoalition())) - allowed=false + self:E(self.lid..string.format("ERROR: Wrong airbase coalition %d in RTB() call! We allow only same as group %d or neutral airbases 0", airbase:GetCoalition(), self.group:GetCoalition())) + return false + end + + if self.currbase and self.currbase:GetName()==airbase:GetName() then + self:E(self.lid.."WARNING: Currbase is already same as RTB airbase. RTB canceled!") + return false + end + + -- Check if the group has landed at an airbase. If so, we lost control and RTBing is not possible (only after a respawn). + if self:IsLanded() then + self:E(self.lid.."WARNING: Flight has already landed. RTB canceled!") + return false end if not self.group:IsAirborne(true) then - self:I(self.lid..string.format("WARNING: Group is not AIRBORNE ==> RTB event is suspended for 10 sec.")) + -- this should really not happen, either the AUFTRAG is cancelled before the group was airborne or it is stuck at the ground for some reason + self:I(self.lid..string.format("WARNING: Group is not AIRBORNE ==> RTB event is suspended for 20 sec")) allowed=false - Tsuspend=-10 + Tsuspend=-20 + local groupspeed = self.group:GetVelocityMPS() + if groupspeed<=1 and not self:IsParking() then + self.RTBRecallCount = self.RTBRecallCount+1 + end + if self.RTBRecallCount>6 then + self:I(self.lid..string.format("WARNING: Group is not moving and was called RTB %d times. Assuming a problem and despawning!", self.RTBRecallCount)) + self.RTBRecallCount=0 + self:Despawn(5) + return + end end -- Only if fuel is not low or critical. @@ -2006,22 +2359,27 @@ function FLIGHTGROUP:onbeforeRTB(From, Event, To, airbase, SpeedTo, SpeedHold) local Ntot,Nsched, Nwp=self:CountRemainingTasks() if self.taskcurrent>0 then - self:I(self.lid..string.format("WARNING: Got current task ==> RTB event is suspended for 10 sec.")) + self:I(self.lid..string.format("WARNING: Got current task ==> RTB event is suspended for 10 sec")) Tsuspend=-10 allowed=false end if Nsched>0 then - self:I(self.lid..string.format("WARNING: Still got %d SCHEDULED tasks in the queue ==> RTB event is suspended for 10 sec.", Nsched)) + self:I(self.lid..string.format("WARNING: Still got %d SCHEDULED tasks in the queue ==> RTB event is suspended for 10 sec", Nsched)) Tsuspend=-10 allowed=false end if Nwp>0 then - self:I(self.lid..string.format("WARNING: Still got %d WAYPOINT tasks in the queue ==> RTB event is suspended for 10 sec.", Nwp)) + self:I(self.lid..string.format("WARNING: Still got %d WAYPOINT tasks in the queue ==> RTB event is suspended for 10 sec", Nwp)) Tsuspend=-10 allowed=false end + + if self.Twaiting and self.dTwait then + self:I(self.lid..string.format("WARNING: Group is Waiting for a specific duration ==> RTB event is canceled", Nwp)) + allowed=false + end end @@ -2055,33 +2413,69 @@ function FLIGHTGROUP:onafterRTB(From, Event, To, airbase, SpeedTo, SpeedHold, Sp -- Set the destination base. self.destbase=airbase - -- Clear holding time in any case. - self.Tholding=nil - -- Cancel all missions. for _,_mission in pairs(self.missionqueue) do local mission=_mission --Ops.Auftrag#AUFTRAG local mystatus=mission:GetGroupStatus(self) - + -- Check if mission is already over! if not (mystatus==AUFTRAG.GroupStatus.DONE or mystatus==AUFTRAG.GroupStatus.CANCELLED) then local text=string.format("Canceling mission %s in state=%s", mission.name, mission.status) self:T(self.lid..text) self:MissionCancel(mission) end - + end + self:_LandAtAirbase(airbase, SpeedTo, SpeedHold, SpeedLand) + +end + +--- On after "LandAtAirbase" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Airbase#AIRBASE airbase The airbase to hold at. +function FLIGHTGROUP:onafterLandAtAirbase(From, Event, To, airbase) + + self.isLandingAtAirbase=airbase + + self:_LandAtAirbase(airbase) + +end + +--- Land at an airbase. +-- @param #FLIGHTGROUP self +-- @param Wrapper.Airbase#AIRBASE airbase Airbase where the group shall land. +-- @param #number SpeedTo Speed used for travelling from current position to holding point in knots. +-- @param #number SpeedHold Holding speed in knots. +-- @param #number SpeedLand Landing speed in knots. Default 170 kts. +function FLIGHTGROUP:_LandAtAirbase(airbase, SpeedTo, SpeedHold, SpeedLand) + + -- Set current airbase. + self.currbase=airbase + + -- Passed final waypoint! + self.passedfinalwp=true + + -- Not waiting any more. + self.Twaiting=nil + self.dTwait=nil + -- Defaults: SpeedTo=SpeedTo or UTILS.KmphToKnots(self.speedCruise) - SpeedHold=SpeedHold or (self.ishelo and 80 or 250) - SpeedLand=SpeedLand or (self.ishelo and 40 or 170) + SpeedHold=SpeedHold or (self.isHelo and 80 or 250) + SpeedLand=SpeedLand or (self.isHelo and 40 or 170) + + -- Clear holding time in any case. + self.Tholding=nil -- Debug message. local text=string.format("Flight group set to hold at airbase %s. SpeedTo=%d, SpeedHold=%d, SpeedLand=%d", airbase:GetName(), SpeedTo, SpeedHold, SpeedLand) self:T(self.lid..text) - local althold=self.ishelo and 1000+math.random(10)*100 or math.random(4,10)*1000 + local althold=self.isHelo and 1000+math.random(10)*100 or math.random(4,10)*1000 -- Holding points. local c0=self.group:GetCoordinate() @@ -2111,8 +2505,8 @@ function FLIGHTGROUP:onafterRTB(From, Event, To, airbase, SpeedTo, SpeedHold, Sp end -- Altitude above ground for a glide slope of 3 degrees. - local x1=self.ishelo and UTILS.NMToMeters(5.0) or UTILS.NMToMeters(10) - local x2=self.ishelo and UTILS.NMToMeters(2.5) or UTILS.NMToMeters(5) + local x1=self.isHelo and UTILS.NMToMeters(5.0) or UTILS.NMToMeters(10) + local x2=self.isHelo and UTILS.NMToMeters(2.5) or UTILS.NMToMeters(5) local alpha=math.rad(3) local h1=x1*math.tan(alpha) local h2=x2*math.tan(alpha) @@ -2122,7 +2516,7 @@ function FLIGHTGROUP:onafterRTB(From, Event, To, airbase, SpeedTo, SpeedHold, Sp -- Set holding flag to 0=false. self.flaghold:Set(0) - local holdtime=5*60 + local holdtime=2*60 if fc or self.airboss then holdtime=nil end @@ -2142,7 +2536,7 @@ function FLIGHTGROUP:onafterRTB(From, Event, To, airbase, SpeedTo, SpeedHold, Sp wp[#wp+1]=p0:WaypointAir(nil, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, UTILS.KnotsToKmph(SpeedTo), true , nil, {TaskArrived, TaskHold, TaskKlar}, "Holding Point") -- Approach point: 10 NN in direction of runway. - if airbase:GetAirbaseCategory()==Airbase.Category.AIRDROME then + if airbase:IsAirdrome() then --- -- Airdrome @@ -2155,10 +2549,10 @@ function FLIGHTGROUP:onafterRTB(From, Event, To, airbase, SpeedTo, SpeedHold, Sp local pland=airbase:GetCoordinate():Translate(x2, runway.heading-180):SetAltitude(h2) wp[#wp+1]=pland:WaypointAirLanding(UTILS.KnotsToKmph(SpeedLand), airbase, {}, "Landing") - elseif airbase:GetAirbaseCategory()==Airbase.Category.SHIP then + elseif airbase:IsShip() or airbase:IsHelipad() then --- - -- Ship + -- Ship or Helipad --- local pland=airbase:GetCoordinate() @@ -2168,33 +2562,12 @@ function FLIGHTGROUP:onafterRTB(From, Event, To, airbase, SpeedTo, SpeedHold, Sp if self.isAI then - local routeto=false - if fc or world.event.S_EVENT_KILL then - routeto=true - end - -- Clear all tasks. -- Warning, looks like this can make DCS CRASH! Had this after calling RTB once passed the final waypoint. --self:ClearTasks() - -- Respawn? - if routeto then - - -- Just route the group. Respawn might happen when going from holding to final. - self:Route(wp, 1) - - else - - -- Get group template. - local Template=self.group:GetTemplate() - - -- Set route points. - Template.route.points=wp - - --Respawn the group with new waypoints. - self:Respawn(Template) - - end + -- Just route the group. Respawn might happen when going from holding to final. + self:Route(wp) end @@ -2205,37 +2578,31 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param Core.Point#COORDINATE Coord Coordinate where to orbit. Default current position. --- @param #number Altitude Altitude in feet. Default 10000 ft. --- @param #number Speed Speed in knots. Default 250 kts. -function FLIGHTGROUP:onbeforeWait(From, Event, To, Coord, Altitude, Speed) +-- @param #number Duration Duration how long the group will be waiting in seconds. Default `nil` (=forever). +-- @param #number Altitude Altitude in feet. Default 10,000 ft for airplanes and 1,000 feet for helos. +-- @param #number Speed Speed in knots. Default 250 kts for airplanes and 20 kts for helos. +function FLIGHTGROUP:onbeforeWait(From, Event, To, Duration, Altitude, Speed) local allowed=true local Tsuspend=nil - -- Check if there are remaining tasks. - local Ntot,Nsched, Nwp=self:CountRemainingTasks() - + -- Check for a current task. if self.taskcurrent>0 then - self:I(self.lid..string.format("WARNING: Got current task ==> WAIT event is suspended for 10 sec.")) - Tsuspend=-10 + self:I(self.lid..string.format("WARNING: Got current task ==> WAIT event is suspended for 30 sec!")) + Tsuspend=-30 allowed=false end - - if Nsched>0 then - self:I(self.lid..string.format("WARNING: Still got %d SCHEDULED tasks in the queue ==> WAIT event is suspended for 10 sec.", Nsched)) - Tsuspend=-10 - allowed=false - end - - if Nwp>0 then - self:I(self.lid..string.format("WARNING: Still got %d WAYPOINT tasks in the queue ==> WAIT event is suspended for 10 sec.", Nwp)) - Tsuspend=-10 - allowed=false + + -- Check for a current transport assignment. + if self.cargoTransport then + self:I(self.lid..string.format("WARNING: Got current TRANSPORT assignment ==> WAIT event is suspended for 30 sec!")) + Tsuspend=-30 + allowed=false end + -- Call wait again. if Tsuspend and not allowed then - self:__Wait(Tsuspend, Coord, Altitude, Speed) + self:__Wait(Tsuspend, Duration, Altitude, Speed) end return allowed @@ -2247,14 +2614,19 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param Core.Point#COORDINATE Coord Coordinate where to orbit. Default current position. --- @param #number Altitude Altitude in feet. Default 10000 ft. --- @param #number Speed Speed in knots. Default 250 kts. -function FLIGHTGROUP:onafterWait(From, Event, To, Coord, Altitude, Speed) +-- @param #number Duration Duration how long the group will be waiting in seconds. Default `nil` (=forever). +-- @param #number Altitude Altitude in feet. Default 10,000 ft for airplanes and 1,000 feet for helos. +-- @param #number Speed Speed in knots. Default 250 kts for airplanes and 20 kts for helos. +function FLIGHTGROUP:onafterWait(From, Event, To, Duration, Altitude, Speed) - Coord=Coord or self.group:GetCoordinate() - Altitude=Altitude or (self.ishelo and 1000 or 10000) - Speed=Speed or (self.ishelo and 80 or 250) + -- Group will orbit at its current position. + local Coord=self.group:GetCoordinate() + + -- Set altitude: 1000 ft for helos and 10,000 ft for panes. + Altitude=Altitude or (self.isHelo and 1000 or 10000) + + -- Set speed. + Speed=Speed or (self.isHelo and 20 or 250) -- Debug message. local text=string.format("Flight group set to wait/orbit at altitude %d m and speed %.1f km/h", Altitude, Speed) @@ -2264,9 +2636,34 @@ function FLIGHTGROUP:onafterWait(From, Event, To, Coord, Altitude, Speed) -- Orbit task. local TaskOrbit=self.group:TaskOrbit(Coord, UTILS.FeetToMeters(Altitude), UTILS.KnotsToMps(Speed)) + + -- Orbit task. + local TaskFunction=self.group:TaskFunction("FLIGHTGROUP._FinishedWaiting", self) + local DCSTasks=self.group:TaskCombo({TaskOrbit, TaskFunction}) + + -- Orbit until flaghold=1 (true) but max 5 min if no FC is giving the landing clearance. + local TaskOrbit = self.group:TaskOrbit(Coord, UTILS.FeetToMeters(Altitude), UTILS.KnotsToMps(Speed)) + local TaskStop = self.group:TaskCondition(nil, nil, nil, nil, Duration) + local TaskCntr = self.group:TaskControlled(TaskOrbit, TaskStop) + local TaskOver = self.group:TaskFunction("FLIGHTGROUP._FinishedWaiting", self) + + local DCSTasks + if Duration then + DCSTasks=self.group:TaskCombo({TaskCntr, TaskOver}) + else + DCSTasks=self.group:TaskCombo({TaskOrbit, TaskOver}) + end + + -- Set task. - self:SetTask(TaskOrbit) + self:SetTask(DCSTasks) + + -- Set time stamp. + self.Twaiting=timer.getAbsTime() + + -- Max waiting + self.dTwait=Duration end @@ -2352,7 +2749,7 @@ function FLIGHTGROUP:onafterHolding(From, Event, To) elseif self.airboss then - if self.ishelo then + if self.isHelo then local carrierpos=self.airboss:GetCoordinate() local carrierheading=self.airboss:GetHeading() @@ -2377,30 +2774,85 @@ function FLIGHTGROUP:onafterHolding(From, Event, To) end ---- On after "EngageTargets" event. Order to engage a set of units. +--- On after "EngageTarget" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #table Target Target object. Can be a UNIT, STATIC, GROUP, SET_UNIT or SET_GROUP object. +function FLIGHTGROUP:onafterEngageTarget(From, Event, To, Target) + + -- DCS task. + local DCStask=nil + + -- Check target object. + if Target:IsInstanceOf("UNIT") or Target:IsInstanceOf("STATIC") then + + DCStask=self:GetGroup():TaskAttackUnit(Target, true) + + elseif Target:IsInstanceOf("GROUP") then + + DCStask=self:GetGroup():TaskAttackGroup(Target, nil, nil, nil, nil, nil, nil, true) + + elseif Target:IsInstanceOf("SET_UNIT") then + + local DCSTasks={} + + for _,_unit in pairs(Target:GetSet()) do --detected by =HRP= Zero + local unit=_unit --Wrapper.Unit#UNIT + local task=self:GetGroup():TaskAttackUnit(unit, true) + table.insert(DCSTasks) + end + + -- Task combo. + DCStask=self:GetGroup():TaskCombo(DCSTasks) + + elseif Target:IsInstanceOf("SET_GROUP") then + + local DCSTasks={} + + for _,_unit in pairs(Target:GetSet()) do --detected by =HRP= Zero + local unit=_unit --Wrapper.Unit#UNIT + local task=self:GetGroup():TaskAttackGroup(Target, nil, nil, nil, nil, nil, nil, true) + table.insert(DCSTasks) + end + + -- Task combo. + DCStask=self:GetGroup():TaskCombo(DCSTasks) + + else + self:E("ERROR: unknown Target in EngageTarget! Needs to be a UNIT, STATIC, GROUP, SET_UNIT or SET_GROUP") + return + end + + -- Create new task.The description "Engage_Target" is checked so do not change that lightly. + local Task=self:NewTaskScheduled(DCStask, 1, "Engage_Target", 0) + + -- Backup ROE setting. + Task.backupROE=self:GetROE() + + -- Switch ROE to open fire + self:SwitchROE(ENUMS.ROE.OpenFire) + + -- Pause current mission. + local mission=self:GetMissionCurrent() + if mission then + self:PauseMission() + end + + -- Execute task. + self:TaskExecute(Task) + +end + +--- On after "Disengage" event. -- @param #FLIGHTGROUP self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -- @param Core.Set#SET_UNIT TargetUnitSet -function FLIGHTGROUP:onafterEngageTargets(From, Event, To, TargetUnitSet) - - local DCSTasks={} - - for _,_unit in paris(TargetUnitSet:GetSet()) do - local unit=_unit --Wrapper.Unit#UNIT - local task=self.group:TaskAttackUnit(unit, true) - table.insert(DCSTasks) - end - - -- Task combo. - local DCSTask=self.group:TaskCombo(DCSTasks) - - --TODO needs a task function that calls EngageDone or so event and updates the route again. - - -- Lets try if pushtask actually leaves the remaining tasks untouched. - self:SetTask(DCSTask) - +function FLIGHTGROUP:onafterDisengage(From, Event, To) + self:T(self.lid.."Disengage target") end --- On before "LandAt" event. Check we have a helo group. @@ -2411,7 +2863,7 @@ end -- @param Core.Point#COORDINATE Coordinate The coordinate where to land. Default is current position. -- @param #number Duration The duration in seconds to remain on ground. Default 600 sec (10 min). function FLIGHTGROUP:onbeforeLandAt(From, Event, To, Coordinate, Duration) - return self.ishelo + return self.isHelo end --- On after "LandAt" event. Order helicopter to land at a specific point. @@ -2432,9 +2884,6 @@ function FLIGHTGROUP:onafterLandAt(From, Event, To, Coordinate, Duration) local Task=self:NewTaskScheduled(DCStask, 1, "Task_Land_At", 0) - -- Add task with high priority. - --self:AddTask(task, 1, "Task_Land_At", 0) - self:TaskExecute(Task) end @@ -2446,8 +2895,10 @@ end -- @param #string To To state. function FLIGHTGROUP:onafterFuelLow(From, Event, To) + local fuel=self:GetFuelMin() or 0 + -- Debug message. - local text=string.format("Low fuel for flight group %s", self.groupname) + local text=string.format("Low fuel %d for flight group %s", fuel, self.groupname) self:I(self.lid..text) -- Set switch to true. @@ -2461,10 +2912,11 @@ function FLIGHTGROUP:onafterFuelLow(From, Event, To) -- Get closest tanker from airwing that can refuel this flight. local tanker=self.airwing:GetTankerForFlight(self) - if tanker then - + if tanker and self.fuellowrefuel then + + -- Debug message. self:I(self.lid..string.format("Send to refuel at tanker %s", tanker.flightgroup:GetName())) - + -- Get a coordinate towards the tanker. local coordinate=self:GetCoordinate():GetIntermediateCoordinate(tanker.flightgroup:GetCoordinate(), 0.75) @@ -2531,44 +2983,6 @@ function FLIGHTGROUP:onafterFuelCritical(From, Event, To) end end ---- On after "Stop" event. --- @param #FLIGHTGROUP self --- @param #string From From state. --- @param #string Event Event. --- @param #string To To state. -function FLIGHTGROUP:onafterStop(From, Event, To) - - -- Check if group is still alive. - if self:IsAlive() then - - -- Set element parking spot to FREE (after arrived for example). - if self.flightcontrol then - for _,_element in pairs(self.elements) do - local element=_element --#FLIGHTGROUP.Element - self:_SetElementParkingFree(element) - end - end - - end - - -- Handle events: - self:UnHandleEvent(EVENTS.Birth) - self:UnHandleEvent(EVENTS.EngineStartup) - self:UnHandleEvent(EVENTS.Takeoff) - self:UnHandleEvent(EVENTS.Land) - self:UnHandleEvent(EVENTS.EngineShutdown) - self:UnHandleEvent(EVENTS.PilotDead) - self:UnHandleEvent(EVENTS.Ejection) - self:UnHandleEvent(EVENTS.Crash) - self:UnHandleEvent(EVENTS.RemoveUnit) - - -- Call OPSGROUP function. - self:GetParent(self).onafterStop(self, From, Event, To) - - -- Remove flight from data base. - _DATABASE.FLIGHTGROUPS[self.groupname]=nil -end - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Task functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2615,26 +3029,41 @@ function FLIGHTGROUP._FinishedRefuelling(group, flightgroup) flightgroup:__Refueled(-1) end +--- Function called when flight finished waiting. +-- @param Wrapper.Group#GROUP group Group object. +-- @param #FLIGHTGROUP flightgroup Flight group object. +function FLIGHTGROUP._FinishedWaiting(group, flightgroup) + flightgroup:T(flightgroup.lid..string.format("Group finished waiting")) + + -- Not waiting any more. + flightgroup.Twaiting=nil + flightgroup.dTwait=nil + + -- Trigger Holding event. + flightgroup:_CheckGroupDone(1) +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Misc functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Initialize group parameters. Also initializes waypoints if self.waypoints is nil. -- @param #FLIGHTGROUP self +-- @param #table Template Template used to init the group. Default is `self.template`. -- @return #FLIGHTGROUP self -function FLIGHTGROUP:_InitGroup() +function FLIGHTGROUP:_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 - + -- Group object. local group=self.group --Wrapper.Group#GROUP -- Get template of group. - self.template=group:GetTemplate() + local template=Template or self:_GetTemplate() -- Define category. self.isAircraft=true @@ -2642,19 +3071,19 @@ function FLIGHTGROUP:_InitGroup() self.isGround=false -- Helo group. - self.ishelo=group:IsHelicopter() + self.isHelo=group:IsHelicopter() -- Is (template) group uncontrolled. - self.isUncontrolled=self.template.uncontrolled + self.isUncontrolled=template.uncontrolled -- Is (template) group late activated. - self.isLateActivated=self.template.lateActivation + self.isLateActivated=template.lateActivation -- Max speed in km/h. self.speedMax=group:GetSpeedMax() -- Cruise speed limit 350 kts for fixed and 80 knots for rotary wings. - local speedCruiseLimit=self.ishelo and UTILS.KnotsToKmph(80) or UTILS.KnotsToKmph(350) + local speedCruiseLimit=self.isHelo and UTILS.KnotsToKmph(80) or UTILS.KnotsToKmph(350) -- Cruise speed: 70% of max speed but within limit. self.speedCruise=math.min(self.speedMax*0.7, speedCruiseLimit) @@ -2663,12 +3092,12 @@ function FLIGHTGROUP:_InitGroup() self.ammo=self:GetAmmoTot() -- Radio parameters from template. Default is set on spawn if not modified by user. - self.radio.Freq=tonumber(self.template.frequency) - self.radio.Modu=tonumber(self.template.modulation) - self.radio.On=self.template.communication - + self.radio.Freq=tonumber(template.frequency) + self.radio.Modu=tonumber(template.modulation) + self.radio.On=template.communication + -- Set callsign. Default is set on spawn if not modified by user. - local callsign=self.template.units[1].callsign + local callsign=template.units[1].callsign if type(callsign)=="number" then -- Sometimes callsign is just "101". local cs=tostring(callsign) callsign={} @@ -2678,16 +3107,15 @@ function FLIGHTGROUP:_InitGroup() end self.callsign.NumberSquad=callsign[1] self.callsign.NumberGroup=callsign[2] - self.callsign.NumberElement=callsign[3] -- First element only self.callsign.NameSquad=UTILS.GetCallsignName(self.callsign.NumberSquad) -- Set default formation. - if self.ishelo then + if self.isHelo then self.optionDefault.Formation=ENUMS.Formation.RotaryWing.EchelonLeft.D300 else self.optionDefault.Formation=ENUMS.Formation.FixedWing.EchelonLeft.Group end - + -- Default TACAN off. self:SetDefaultTACAN(nil, nil, nil, nil, true) self.tacan=UTILS.DeepCopy(self.tacanDefault) @@ -2704,7 +3132,7 @@ function FLIGHTGROUP:_InitGroup() -- Add elemets. for _,unit in pairs(self.group:GetUnits()) do - local element=self:AddElementByName(unit:GetName()) + self:_AddElementByName(unit:GetName()) end -- Get first unit. This is used to extract other parameters. @@ -2723,97 +3151,19 @@ function FLIGHTGROUP:_InitGroup() self.tankertype=select(2, unit:IsTanker()) self.refueltype=select(2, unit:IsRefuelable()) - -- Debug info. - if self.verbose>=1 then - local text=string.format("Initialized Flight 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("Range max = %.1f km\n", self.rangemax/1000) - text=text..string.format("Ceiling = %.1f feet\n", UTILS.MetersToFeet(self.ceiling)) - text=text..string.format("Tanker type = %s\n", tostring(self.tankertype)) - text=text..string.format("Refuel type = %s\n", tostring(self.refueltype)) - text=text..string.format("AI = %s\n", tostring(self.isAI)) - text=text..string.format("Helicopter = %s\n", tostring(self.group:IsHelicopter())) - 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/B=%d/M=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Rockets, self.ammo.Bombs, self.ammo.Missiles) - text=text..string.format("FSM state = %s\n", self:GetState()) - text=text..string.format("Is alive = %s\n", tostring(self.group:IsAlive())) - text=text..string.format("LateActivate = %s\n", tostring(self:IsLateActivated())) - text=text..string.format("Uncontrolled = %s\n", tostring(self:IsUncontrolled())) - text=text..string.format("Start Air = %s\n", tostring(self:IsTakeoffAir())) - text=text..string.format("Start Cold = %s\n", tostring(self:IsTakeoffCold())) - text=text..string.format("Start Hot = %s\n", tostring(self:IsTakeoffHot())) - text=text..string.format("Start Rwy = %s\n", tostring(self:IsTakeoffRunway())) - self:I(self.lid..text) - end + --env.info("DCS Unit BOOM_AND_RECEPTACLE="..tostring(Unit.RefuelingSystem.BOOM_AND_RECEPTACLE)) + --env.info("DCS Unit PROBE_AND_DROGUE="..tostring(Unit.RefuelingSystem.PROBE_AND_DROGUE)) -- Init done. self.groupinitialized=true - + + else + self:E(self.lid.."ERROR: no unit in _InigGroup!") end return self end ---- Add an element to the flight group. --- @param #FLIGHTGROUP self --- @param #string unitname Name of unit. --- @return #FLIGHTGROUP.Element The element or nil. -function FLIGHTGROUP:AddElementByName(unitname) - - local unit=UNIT:FindByName(unitname) - - if unit then - - local element={} --#FLIGHTGROUP.Element - - element.name=unitname - element.unit=unit - element.status=OPSGROUP.ElementStatus.INUTERO - element.group=unit:GetGroup() - - -- TODO: this is wrong when grouping is used! - local unittemplate=element.unit:GetTemplate() - - element.modex=unittemplate.onboard_num - element.skill=unittemplate.skill - element.payload=unittemplate.payload - element.pylons=unittemplate.payload and unittemplate.payload.pylons or nil --element.unit:GetTemplatePylons() - element.fuelmass0=unittemplate.payload and unittemplate.payload.fuel or 0 --element.unit:GetTemplatePayload().fuel - element.fuelmass=element.fuelmass0 - element.fuelrel=element.unit:GetFuel() - element.category=element.unit:GetUnitCategory() - element.categoryname=element.unit:GetCategoryName() - element.callsign=element.unit:GetCallsign() - element.size=element.unit:GetObjectSize() - - if element.skill=="Client" or element.skill=="Player" then - element.ai=false - element.client=CLIENT:FindByName(unitname) - else - element.ai=true - end - - -- Debug text. - local text=string.format("Adding element %s: status=%s, skill=%s, modex=%s, fuelmass=%.1f (%d), category=%d, categoryname=%s, callsign=%s, ai=%s", - element.name, element.status, element.skill, element.modex, element.fuelmass, element.fuelrel*100, element.category, element.categoryname, element.callsign, tostring(element.ai)) - self:T(self.lid..text) - - -- Add element to table. - table.insert(self.elements, element) - - if unit:IsAlive() then - self:ElementSpawned(element) - end - - return element - end - - return nil -end - --- Check if a unit is and element of the flightgroup. -- @param #FLIGHTGROUP self @@ -2830,7 +3180,7 @@ function FLIGHTGROUP:GetHomebaseFromWaypoints() -- Get airbase ID depending on airbase category. local airbaseID=nil - + if wp.airdromeId then airbaseID=wp.airdromeId else @@ -2838,7 +3188,7 @@ function FLIGHTGROUP:GetHomebaseFromWaypoints() end local airbase=AIRBASE:FindByID(airbaseID) - + return airbase end @@ -3067,7 +3417,7 @@ function FLIGHTGROUP:IsLandingAirbase(wp) if wp then - if wp.action and wp.action==COORDINATE.WaypointAction.LANDING then + if wp.action and wp.action==COORDINATE.WaypointAction.Landing then return true else return false @@ -3086,22 +3436,23 @@ function FLIGHTGROUP:InitWaypoints() -- Template waypoints. self.waypoints0=self.group:GetTemplateRoutePoints() - -- Waypoints + -- Waypoints self.waypoints={} - + for index,wp in pairs(self.waypoints0) do - - local waypoint=self:_CreateWaypoint(wp) + + local waypoint=self:_CreateWaypoint(wp) self:_AddWaypoint(waypoint) - + end -- Get home and destination airbases from waypoints. self.homebase=self.homebase or self:GetHomebaseFromWaypoints() self.destbase=self.destbase or self:GetDestinationFromWaypoints() + self.currbase=self:GetHomebaseFromWaypoints() -- Remove the landing waypoint. We use RTB for that. It makes adding new waypoints easier as we do not have to check if the last waypoint is the landing waypoint. - if self.destbase then + if self.destbase and #self.waypoints>1 then table.remove(self.waypoints, #self.waypoints) else self.destbase=self.homebase @@ -3141,14 +3492,62 @@ function FLIGHTGROUP:AddWaypoint(Coordinate, Speed, AfterWaypointWithID, Altitud end -- Speed in knots. - Speed=Speed or 350 + Speed=Speed or self.speedCruise -- Create air waypoint. local wp=Coordinate:WaypointAir(COORDINATE.WaypointAltType.BARO, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, UTILS.KnotsToKmph(Speed), true, nil, {}) -- Create waypoint data table. local waypoint=self:_CreateWaypoint(wp) - + + -- Set altitude. + if Altitude then + waypoint.alt=UTILS.FeetToMeters(Altitude) + end + + -- Add waypoint to table. + self:_AddWaypoint(waypoint, wpnumber) + + -- Debug info. + self:T(self.lid..string.format("Adding AIR waypoint #%d, speed=%.1f knots. Last waypoint passed was #%s. Total waypoints #%d", wpnumber, Speed, self.currentwp, #self.waypoints)) + + -- Update route. + if Updateroute==nil or Updateroute==true then + self:__UpdateRoute(-1) + end + + return waypoint +end + +--- Add an LANDING waypoint to the flight plan. +-- @param #FLIGHTGROUP self +-- @param Wrapper.Airbase#AIRBASE Airbase The airbase where the group should land. +-- @param #number Speed Speed in knots. Default 350 kts. +-- @param #number AfterWaypointWithID Insert waypoint after waypoint given ID. Default is to insert as last waypoint. +-- @param #number Altitude Altitude in feet. Default is y-component of Coordinate. Note that these altitudes are wrt to sea level (barometric altitude). +-- @param #boolean Updateroute If true or nil, call UpdateRoute. If false, no call. +-- @return Ops.OpsGroup#OPSGROUP.Waypoint Waypoint table. +function FLIGHTGROUP:AddWaypointLanding(Airbase, Speed, AfterWaypointWithID, Altitude, Updateroute) + + -- Set waypoint index. + local wpnumber=self:GetWaypointIndexAfterID(AfterWaypointWithID) + + if wpnumber>self.currentwp then + self.passedfinalwp=false + end + + -- Speed in knots. + Speed=Speed or self.speedCruise + + -- Get coordinate of airbase. + local Coordinate=Airbase:GetCoordinate() + + -- Create air waypoint. + local wp=Coordinate:WaypointAir(COORDINATE.WaypointAltType.BARO,COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, Speed, nil, Airbase, {}, "Landing Temp", nil) + + -- Create waypoint data table. + local waypoint=self:_CreateWaypoint(wp) + -- Set altitude. if Altitude then waypoint.alt=UTILS.FeetToMeters(Altitude) @@ -3169,30 +3568,9 @@ function FLIGHTGROUP:AddWaypoint(Coordinate, Speed, AfterWaypointWithID, Altitud end - ---- Check if a unit is an element of the flightgroup. --- @param #FLIGHTGROUP self --- @param #string unitname Name of unit. --- @return #boolean If true, unit is element of the flight group or false if otherwise. -function FLIGHTGROUP:_IsElement(unitname) - - for _,_element in pairs(self.elements) do - local element=_element --#FLIGHTGROUP.Element - - if element.name==unitname then - return true - end - - end - - return false -end - - - --- Set parking spot of element. -- @param #FLIGHTGROUP self --- @param #FLIGHTGROUP.Element Element The element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The element. -- @param Wrapper.Airbase#AIRBASE.ParkingSpot Spot Parking Spot. function FLIGHTGROUP:_SetElementParkingAt(Element, Spot) @@ -3216,7 +3594,7 @@ end --- Set parking spot of element to free -- @param #FLIGHTGROUP self --- @param #FLIGHTGROUP.Element Element The element. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The element. function FLIGHTGROUP:_SetElementParkingFree(Element) if Element.parking then @@ -3326,25 +3704,35 @@ end --- Returns the parking spot of the element. -- @param #FLIGHTGROUP self --- @param #FLIGHTGROUP.Element element Element of the flight group. +-- @param Ops.OpsGroup#OPSGROUP.Element element Element of the flight group. -- @param #number maxdist Distance threshold in meters. Default 5 m. -- @param Wrapper.Airbase#AIRBASE airbase (Optional) The airbase to check for parking. Default is closest airbase to the element. -- @return Wrapper.Airbase#AIRBASE.ParkingSpot Parking spot or nil if no spot is within distance threshold. function FLIGHTGROUP:GetParkingSpot(element, maxdist, airbase) + -- Coordinate of unit landed local coord=element.unit:GetCoordinate() + -- Airbase. airbase=airbase or self:GetClosestAirbase() --coord:GetClosestAirbase(nil, self:GetCoalition()) -- TODO: replace by airbase.parking if AIRBASE is updated. local parking=airbase:GetParkingSpotsTable() + -- If airbase is ship, translate parking coords. Alternatively, we just move the coordinate of the unit to the origin of the map, which is way more efficient. + if airbase and airbase:IsShip() then + coord.x=0 + coord.z=0 + maxdist=500 -- 100 meters was not enough, e.g. on the Seawise Giant, where the spot is 139 meters from the "center" + end + local spot=nil --Wrapper.Airbase#AIRBASE.ParkingSpot local dist=nil local distmin=math.huge for _,_parking in pairs(parking) do local parking=_parking --Wrapper.Airbase#AIRBASE.ParkingSpot dist=coord:Get2DDistance(parking.Coordinate) + --env.info(string.format("FF parking %d dist=%.1f", parking.TerminalID, dist)) if dist return groups of generalized attributes in a zone. -- DONE: Loose units only if they remain undetected for a given time interval. We want to avoid fast oscillation between detected/lost states. Maybe 1-5 min would be a good time interval?! -- DONE: Combine units to groups for all, new and lost. --- TODO: process detected set asynchroniously for better performance. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor @@ -159,7 +203,14 @@ function INTEL:New(DetectionSet, Coalition, Alias) self.alias="CIA" end end - end + end + + self.DetectVisual = true + self.DetectOptical = true + self.DetectRadar = true + self.DetectIRST = true + self.DetectRWR = true + self.DetectDLINK = true -- Set some string id for output to DCS.log file. self.lid=string.format("INTEL %s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown") @@ -177,10 +228,14 @@ function INTEL:New(DetectionSet, Coalition, Alias) self:AddTransition("*", "NewContact", "*") -- New contact has been detected. self:AddTransition("*", "LostContact", "*") -- Contact could not be detected any more. + self:AddTransition("*", "NewCluster", "*") -- New cluster has been detected. + self:AddTransition("*", "LostCluster", "*") -- Cluster could not be detected any more. + self:AddTransition("*", "Stop", "Stopped") -- Defaults self:SetForgetTime() self:SetAcceptZones() + self:SetRejectZones() ------------------------ --- Pseudo Functions --- @@ -211,33 +266,57 @@ function INTEL:New(DetectionSet, Coalition, Alias) -- @function [parent=#INTEL] __Status -- @param #INTEL self -- @param #number delay Delay in seconds. - - - -- Debug trace. - if false then - self.Debug=true - BASE:TraceOnOff(true) - BASE:TraceClass(self.ClassName) - BASE:TraceLevel(1) - end + + --- On After "NewContact" event. + -- @function [parent=#INTEL] OnAfterNewContact + -- @param #INTEL self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #INTEL.Contact Contact Detected contact. + + --- On After "LostContact" event. + -- @function [parent=#INTEL] OnAfterLostContact + -- @param #INTEL self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #INTEL.Contact Contact Lost contact. + + --- On After "NewCluster" event. + -- @function [parent=#INTEL] OnAfterNewCluster + -- @param #INTEL self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #INTEL.Contact Contact Detected contact. + -- @param #INTEL.Cluster Cluster Detected cluster + + --- On After "LostCluster" event. + -- @function [parent=#INTEL] OnAfterLostCluster + -- @param #INTEL self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #INTEL.Cluster Cluster Lost cluster + -- @param Ops.Auftrag#AUFTRAG Mission The Auftrag connected with this cluster or nil return self end - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- User functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Set accept zones. Only contacts detected in this/these zone(s) are considered. -- @param #INTEL self --- @param Core.Set#SET_ZONE AcceptZoneSet Set of accept zones +-- @param Core.Set#SET_ZONE AcceptZoneSet Set of accept zones. -- @return #INTEL self function INTEL:SetAcceptZones(AcceptZoneSet) self.acceptzoneset=AcceptZoneSet or SET_ZONE:New() return self end ---- Set accept zones. Only contacts detected in this zone are considered. +--- Add an accept zone. Only contacts detected in this zone are considered. -- @param #INTEL self -- @param Core.Zone#ZONE AcceptZone Add a zone to the accept zone set. -- @return #INTEL self @@ -246,6 +325,44 @@ function INTEL:AddAcceptZone(AcceptZone) return self end +--- Remove an accept zone from the accept zone set. +-- @param #INTEL self +-- @param Core.Zone#ZONE AcceptZone Remove a zone from the accept zone set. +-- @return #INTEL self +function INTEL:RemoveAcceptZone(AcceptZone) + self.acceptzoneset:Remove(AcceptZone:GetName(), true) + return self +end + +--- Set reject zones. Contacts detected in this/these zone(s) are rejected and not reported by the detection. +-- Note that reject zones overrule accept zones, i.e. if a unit is inside and accept zone and inside a reject zone, it is rejected. +-- @param #INTEL self +-- @param Core.Set#SET_ZONE RejectZoneSet Set of reject zone(s). +-- @return #INTEL self +function INTEL:SetRejectZones(RejectZoneSet) + self.rejectzoneset=RejectZoneSet or SET_ZONE:New() + return self +end + +--- Add a reject zone. Contacts detected in this zone are rejected and not reported by the detection. +-- Note that reject zones overrule accept zones, i.e. if a unit is inside and accept zone and inside a reject zone, it is rejected. +-- @param #INTEL self +-- @param Core.Zone#ZONE RejectZone Add a zone to the reject zone set. +-- @return #INTEL self +function INTEL:AddRejectZone(RejectZone) + self.rejectzoneset:AddZone(RejectZone) + return self +end + +--- Remove a reject zone from the reject zone set. +-- @param #INTEL self +-- @param Core.Zone#ZONE RejectZone Remove a zone from the reject zone set. +-- @return #INTEL self +function INTEL:RemoveRejectZone(RejectZone) + self.rejectzoneset:Remove(RejectZone:GetName(), true) + return self +end + --- Set forget contacts time interval. -- Previously known contacts that are not detected any more, are "lost" after this time. -- This avoids fast oscillations between a contact being detected and undetected. @@ -279,7 +396,7 @@ function INTEL:SetFilterCategory(Categories) for _,category in pairs(self.filterCategory) do text=text..string.format("%d,", category) end - self:I(self.lid..text) + self:T(self.lid..text) return self end @@ -306,7 +423,7 @@ function INTEL:FilterCategoryGroup(GroupCategories) for _,category in pairs(self.filterCategoryGroup) do text=text..string.format("%d,", category) end - self:I(self.lid..text) + self:T(self.lid..text) return self end @@ -323,6 +440,90 @@ function INTEL:SetClusterAnalysis(Switch, Markers) return self end +--- Set verbosity level for debugging. +-- @param #INTEL self +-- @param #number Verbosity The higher, the noisier, e.g. 0=off, 2=debug +-- @return #INTEL self +function INTEL:SetVerbosity(Verbosity) + self.verbose=Verbosity or 2 + return self +end + +--- Add a Mission (Auftrag) to a contact for tracking. +-- @param #INTEL self +-- @param #INTEL.Contact Contact The contact +-- @param Ops.Auftrag#AUFTRAG Mission The mission connected with this contact +-- @return #INTEL self +function INTEL:AddMissionToContact(Contact, Mission) + if Mission and Contact then + Contact.mission = Mission + end + return self +end + +--- Add a Mission (Auftrag) to a cluster for tracking. +-- @param #INTEL self +-- @param #INTEL.Cluster Cluster The cluster +-- @param Ops.Auftrag#AUFTRAG Mission The mission connected with this cluster +-- @return #INTEL self +function INTEL:AddMissionToCluster(Cluster, Mission) + if Mission and Cluster then + Cluster.mission = Mission + end + return self +end + +--- Change radius of the Clusters +-- @param #INTEL self +-- @param #number radius The radius of the clusters +-- @return #INTEL self +function INTEL:SetClusterRadius(radius) + local radius = radius or 15 + self.clusterradius = radius + return self +end + +--- Set detection types for this #INTEL - all default to true. +-- @param #INTEL self +-- @param #boolean DetectVisual Visual detection +-- @param #boolean DetectOptical Optical detection +-- @param #boolean DetectRadar Radar detection +-- @param #boolean DetectIRST IRST detection +-- @param #boolean DetectRWR RWR detection +-- @param #boolean DetectDLINK Data link detection +-- @return self +function INTEL:SetDetectionTypes(DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) + self.DetectVisual = DetectVisual and true + self.DetectOptical = DetectOptical and true + self.DetectRadar = DetectRadar and true + self.DetectIRST = DetectIRST and true + self.DetectRWR = DetectRWR and true + self.DetectDLINK = DetectDLINK and true + return self +end + +--- Get table of #INTEL.Contact objects +-- @param #INTEL self +-- @return #table Contacts or nil if not running +function INTEL:GetContactTable() + if self:Is("Running") then + return self.Contacts + else + return nil + end +end + +--- Get table of #INTEL.Cluster objects +-- @param #INTEL self +-- @return #table Clusters or nil if not running +function INTEL:GetClusterTable() + if self:Is("Running") and self.clusteranalysis then + return self.Clusters + else + return nil + end +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Start & Status ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -361,10 +562,11 @@ function INTEL:onafterStatus(From, Event, To) -- Number of total contacts. local Ncontacts=#self.Contacts + local Nclusters=#self.Clusters -- Short info. if self.verbose>=1 then - local text=string.format("Status %s [Agents=%s]: Contacts=%d, New=%d, Lost=%d", fsmstate, self.detectionset:CountAlive(), Ncontacts, #self.ContactsUnknown, #self.ContactsLost) + local text=string.format("Status %s [Agents=%s]: Contacts=%d, Clusters=%d, New=%d, Lost=%d", fsmstate, self.detectionset:CountAlive(), Ncontacts, Nclusters, #self.ContactsUnknown, #self.ContactsLost) self:I(self.lid..text) end @@ -377,7 +579,7 @@ function INTEL:onafterStatus(From, Event, To) text=text..string.format("\n- %s (%s): %s, units=%d, T=%d sec", contact.categoryname, contact.attribute, contact.groupname, contact.group:CountAliveUnits(), dT) if contact.mission then local mission=contact.mission --Ops.Auftrag#AUFTRAG - text=text..string.format(" mission name=%s type=%s target=%s", mission.name, mission.type, mission:GetTargetName() or "unkown") + text=text..string.format(" mission name=%s type=%s target=%s", mission.name, mission.type, mission:GetTargetName() or "unknown") end end self:I(self.lid..text) @@ -393,7 +595,8 @@ function INTEL:UpdateIntel() -- Set of all detected units. local DetectedUnits={} - + -- Set of which units was detected by which recce + local RecceDetecting = {} -- Loop over all units providing intel. for _,_group in pairs(self.detectionset.Set or {}) do local group=_group --Wrapper.Group#GROUP @@ -404,15 +607,13 @@ function INTEL:UpdateIntel() local recce=_recce --Wrapper.Unit#UNIT -- Get detected units. - self:GetDetectedUnits(recce, DetectedUnits) + self:GetDetectedUnits(recce, DetectedUnits, RecceDetecting, self.DetectVisual, self.DetectOptical, self.DetectRadar, self.DetectIRST, self.DetectRWR, self.DetectDLINK) end end end - -- TODO: Filter units from reject zones. - -- TODO: Filter detection methods? local remove={} for unitname,_unit in pairs(DetectedUnits) do local unit=_unit --Wrapper.Unit#UNIT @@ -433,6 +634,23 @@ function INTEL:UpdateIntel() table.insert(remove, unitname) end end + + -- Check if unit is in any of the reject zones. + if self.rejectzoneset:Count()>0 then + local inzone=false + for _,_zone in pairs(self.rejectzoneset.Set) do + local zone=_zone --Core.Zone#ZONE + if unit:IsInZone(zone) then + inzone=true + break + end + end + + -- Unit is inside a reject zone ==> remove! + if inzone then + table.insert(remove, unitname) + end + end -- Filter unit categories. if #self.filterCategory>0 then @@ -445,7 +663,7 @@ function INTEL:UpdateIntel() end end if not keepit then - self:I(self.lid..string.format("Removing unit %s category=%d", unitname, unit:GetCategory())) + self:T(self.lid..string.format("Removing unit %s category=%d", unitname, unit:GetCategory())) table.insert(remove, unitname) end end @@ -458,17 +676,20 @@ function INTEL:UpdateIntel() end -- Create detected groups. - local DetectedGroups={} + local DetectedGroups={} + local RecceGroups={} for unitname,_unit in pairs(DetectedUnits) do local unit=_unit --Wrapper.Unit#UNIT local group=unit:GetGroup() if group then - DetectedGroups[group:GetName()]=group + local groupname = group:GetName() + DetectedGroups[groupname]=group + RecceGroups[groupname]=RecceDetecting[unitname] end end -- Create detected contacts. - self:CreateDetectedItems(DetectedGroups) + self:CreateDetectedItems(DetectedGroups, RecceGroups) -- Paint a picture of the battlefield. if self.clusteranalysis then @@ -484,8 +705,9 @@ end --- Create detected items. -- @param #INTEL self -- @param #table DetectedGroups Table of detected Groups -function INTEL:CreateDetectedItems(DetectedGroups) - +-- @param #table RecceDetecting Table of detecting recce names +function INTEL:CreateDetectedItems(DetectedGroups, RecceDetecting) + self:F({RecceDetecting=RecceDetecting}) -- Current time. local Tnow=timer.getAbsTime() @@ -525,7 +747,8 @@ function INTEL:CreateDetectedItems(DetectedGroups) item.position=group:GetCoordinate() item.velocity=group:GetVelocityVec3() item.speed=group:GetVelocityMPS() - + item.recce=RecceDetecting[groupname] + self:T(string.format("%s group detect by %s/%s", groupname, RecceDetecting[groupname] or "unknown", item.recce or "unknown")) -- Add contact to table. self:AddContact(item) @@ -553,22 +776,25 @@ function INTEL:CreateDetectedItems(DetectedGroups) end ---- Return the detected target groups of the controllable as a @{SET_GROUP}. +--- (Internal) Return the detected target groups of the controllable as a @{SET_GROUP}. -- The optional parametes specify the detection methods that can be applied. -- If no detection method is given, the detection will use all the available methods by default. -- @param #INTEL self -- @param Wrapper.Unit#UNIT Unit The unit detecting. +-- @param #table DetectedUnits Table of detected units to be filled +-- @param #table RecceDetecting Table of recce per unit to be filled -- @param #boolean DetectVisual (Optional) If *false*, do not include visually detected targets. -- @param #boolean DetectOptical (Optional) If *false*, do not include optically detected targets. -- @param #boolean DetectRadar (Optional) If *false*, do not include targets detected by radar. -- @param #boolean DetectIRST (Optional) If *false*, do not include targets detected by IRST. -- @param #boolean DetectRWR (Optional) If *false*, do not include targets detected by RWR. -- @param #boolean DetectDLINK (Optional) If *false*, do not include targets detected by data link. -function INTEL:GetDetectedUnits(Unit, DetectedUnits, DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) +function INTEL:GetDetectedUnits(Unit, DetectedUnits, RecceDetecting, DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) -- Get detected DCS units. local detectedtargets=Unit:GetDetectedTargets(DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) - + local reccename = Unit:GetName() + for DetectionObjectID, Detection in pairs(detectedtargets or {}) do local DetectedObject=Detection.object -- DCS#Object @@ -581,7 +807,8 @@ function INTEL:GetDetectedUnits(Unit, DetectedUnits, DetectVisual, DetectOptical local unitname=unit:GetName() DetectedUnits[unitname]=unit - + RecceDetecting[unitname]=reccename + self:T(string.format("Unit %s detect by %s", unitname, reccename)) end end end @@ -599,7 +826,7 @@ end -- @param #string To To state. -- @param #INTEL.Contact Contact Detected contact. function INTEL:onafterNewContact(From, Event, To, Contact) - self:I(self.lid..string.format("NEW contact %s", Contact.groupname)) + self:F(self.lid..string.format("NEW contact %s", Contact.groupname)) table.insert(self.ContactsUnknown, Contact) end @@ -610,10 +837,37 @@ end -- @param #string To To state. -- @param #INTEL.Contact Contact Detected contact. function INTEL:onafterLostContact(From, Event, To, Contact) - self:I(self.lid..string.format("LOST contact %s", Contact.groupname)) + self:F(self.lid..string.format("LOST contact %s", Contact.groupname)) table.insert(self.ContactsLost, Contact) end +--- On after "NewCluster" event. +-- @param #INTEL self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #INTEL.Contact Contact Detected contact. +-- @param #INTEL.Cluster Cluster Detected cluster +function INTEL:onafterNewCluster(From, Event, To, Contact, Cluster) + self:F(self.lid..string.format("NEW cluster %d size %d with contact %s", Cluster.index, Cluster.size, Contact.groupname)) +end + +--- On after "LostCluster" event. +-- @param #INTEL self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #INTEL.Cluster Cluster Lost cluster +-- @param Ops.Auftrag#AUFTRAG Mission The Auftrag connected with this cluster or nil +function INTEL:onafterLostCluster(From, Event, To, Cluster, Mission) + local text = self.lid..string.format("LOST cluster %d", Cluster.index) + if Mission then + local mission=Mission --Ops.Auftrag#AUFTRAG + text=text..string.format(" mission name=%s type=%s target=%s", mission.name, mission.type, mission:GetTargetName() or "unknown") + end + self:T(text) +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Misc Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -696,7 +950,7 @@ end -- Cluster Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Paint picture of the battle field. +--- [Internal] Paint picture of the battle field. Does Cluster analysis and updates clusters. Sets markers if markers are enabled. -- @param #INTEL self function INTEL:PaintPicture() @@ -708,11 +962,31 @@ function INTEL:PaintPicture() self:RemoveContactFromCluster(contact, cluster) end end - + -- clean up cluster table + local ClusterSet = {} + for _i,_cluster in pairs(self.Clusters) do + if (_cluster.size > 0) and (self:ClusterCountUnits(_cluster) > 0) then + table.insert(ClusterSet,_cluster) + else + local mission = _cluster.mission or nil + local marker = _cluster.marker + local markerID = _cluster.markerID + if marker then + marker:Remove() + end + if markerID then + COORDINATE:RemoveMark(markerID) + end + self:LostCluster(_cluster, mission) + end + end + self.Clusters = ClusterSet + -- update positions + self:_UpdateClusterPositions() for _,_contact in pairs(self.Contacts) do local contact=_contact --#INTEL.Contact - + self:T(string.format("Paint Picture: checking for %s",contact.groupname)) -- Check if this contact is in any cluster. local isincluster=self:CheckContactInClusters(contact) @@ -720,7 +994,7 @@ function INTEL:PaintPicture() local currentcluster=self:GetClusterOfContact(contact) if currentcluster then - + --self:I(string.format("Paint Picture: %s has current cluster",contact.groupname)) --- -- Contact is currently part of a cluster. --- @@ -728,8 +1002,8 @@ function INTEL:PaintPicture() -- Check if the contact is still connected to the cluster. local isconnected=self:IsContactConnectedToCluster(contact, currentcluster) - if not isconnected then - + if (not isconnected) and (currentcluster.size > 1) then + --self:I(string.format("Paint Picture: %s has LOST current cluster",contact.groupname)) local cluster=self:IsContactPartOfAnyClusters(contact) if cluster then @@ -738,6 +1012,7 @@ function INTEL:PaintPicture() local newcluster=self:CreateCluster(contact.position) self:AddContactToCluster(contact, newcluster) + self:NewCluster(contact, newcluster) end end @@ -748,7 +1023,7 @@ function INTEL:PaintPicture() --- -- Contact is not in any cluster yet. --- - + --self:I(string.format("Paint Picture: %s has NO current cluster",contact.groupname)) local cluster=self:IsContactPartOfAnyClusters(contact) if cluster then @@ -757,6 +1032,7 @@ function INTEL:PaintPicture() local newcluster=self:CreateCluster(contact.position) self:AddContactToCluster(contact, newcluster) + self:NewCluster(contact, newcluster) end end @@ -766,16 +1042,15 @@ function INTEL:PaintPicture() -- Update F10 marker text if cluster has changed. - for _,_cluster in pairs(self.Clusters) do - local cluster=_cluster --#INTEL.Cluster - - local coordinate=self:GetClusterCoordinate(cluster) - - + if self.clustermarkers then + for _,_cluster in pairs(self.Clusters) do + local cluster=_cluster --#INTEL.Cluster + --local coordinate=self:GetClusterCoordinate(cluster) -- Update F10 marker. self:UpdateClusterMarker(cluster) + self:CalcClusterFuturePosition(cluster,self.prediction) + end end - end --- Create a new cluster. @@ -862,7 +1137,7 @@ function INTEL:CalcClusterThreatlevelSum(cluster) threatlevel=threatlevel+contact.threatlevel end - + cluster.threatlevelSum = threatlevel return threatlevel end @@ -874,7 +1149,7 @@ function INTEL:CalcClusterThreatlevelAverage(cluster) local threatlevel=self:CalcClusterThreatlevelSum(cluster) threatlevel=threatlevel/cluster.size - + cluster.threatlevelAve = threatlevel return threatlevel end @@ -887,6 +1162,7 @@ function INTEL:CalcClusterThreatlevelMax(cluster) local threatlevel=0 for _,_contact in pairs(cluster.Contacts) do + local contact=_contact --#INTEL.Contact if contact.threatlevel>threatlevel then @@ -894,10 +1170,69 @@ function INTEL:CalcClusterThreatlevelMax(cluster) end end - + cluster.threatlevelMax = threatlevel return threatlevel end +--- Calculate cluster heading. +-- @param #INTEL self +-- @param #INTEL.Cluster cluster The cluster of contacts. +-- @return #number Heading average of all groups in the cluster. +function INTEL:CalcClusterDirection(cluster) + + local direction = 0 + local n=0 + for _,_contact in pairs(cluster.Contacts) do + local group = _contact.group -- Wrapper.Group#GROUP + if group:IsAlive() then + direction = direction + group:GetHeading() + n=n+1 + end + end + return math.floor(direction / n) + +end + +--- Calculate cluster speed. +-- @param #INTEL self +-- @param #INTEL.Cluster cluster The cluster of contacts. +-- @return #number Speed average of all groups in the cluster in MPS. +function INTEL:CalcClusterSpeed(cluster) + + local velocity = 0 + local n=0 + for _,_contact in pairs(cluster.Contacts) do + local group = _contact.group -- Wrapper.Group#GROUP + if group:IsAlive() then + velocity = velocity + group:GetVelocityMPS() + n=n+1 + end + end + return math.floor(velocity / n) + +end + +--- Calculate cluster future position after given seconds. +-- @param #INTEL self +-- @param #INTEL.Cluster cluster The cluster of contacts. +-- @param #number seconds Timeframe in seconds. +-- @return Core.Point#COORDINATE Calculated future position of the cluster. +function INTEL:CalcClusterFuturePosition(cluster,seconds) + local speed = self:CalcClusterSpeed(cluster) -- #number MPS + local direction = self:CalcClusterDirection(cluster) -- #number heading + -- local currposition = cluster.coordinate -- Core.Point#COORDINATE + local currposition = self:GetClusterCoordinate(cluster) -- Core.Point#COORDINATE + local distance = speed * seconds -- #number in meters the cluster will travel + local futureposition = currposition:Translate(distance,direction,true,false) + if self.clustermarkers and (self.verbose > 1) then + if cluster.markerID then + COORDINATE:RemoveMark(cluster.markerID) + end + cluster.markerID = currposition:ArrowToAll(futureposition,self.coalition,{1,0,0},1,{1,1,0},0.5,2,true,"Postion Calc") + end + return futureposition +end + --- Check if contact is in any known cluster. -- @param #INTEL self @@ -932,9 +1267,11 @@ function INTEL:IsContactConnectedToCluster(contact, cluster) if Contact.groupname~=contact.groupname then - local dist=Contact.position:Get2DDistance(contact.position) + --local dist=Contact.position:Get2DDistance(contact.position) + local dist=Contact.position:DistanceFromPointVec2(contact.position) - if dist<10*1000 then + local radius = self.clusterradius or 15 + if dist1000 then return true @@ -1029,6 +1373,27 @@ function INTEL:CheckClusterCoordinateChanged(cluster, coordinate) end +--- Update coordinates of the known clusters. +-- @param #INTEL self +function INTEL:_UpdateClusterPositions() + for _,_cluster in pairs (self.Clusters) do + local coord = self:GetClusterCoordinate(_cluster) + _cluster.coordinate = coord + self:T(self.lid..string.format("Cluster size: %s", _cluster.size)) + end +end + +--- Count number of units in cluster +-- @param #INTEL self +-- @param #INTEL.Cluster Cluster The cluster +-- @return #number unitcount +function INTEL:ClusterCountUnits(Cluster) + local unitcount = 0 + for _,_group in pairs (Cluster.Contacts) do -- get Wrapper.GROUP#GROUP _group + unitcount = unitcount + _group.group:CountAliveUnits() + end + return unitcount +end --- Update cluster F10 marker. -- @param #INTEL self @@ -1037,10 +1402,17 @@ end function INTEL:UpdateClusterMarker(cluster) -- Create a marker. - local text=string.format("Cluster #%d. Size %d, TLsum=%d", cluster.index, cluster.size, cluster.threatlevelSum) + local unitcount = self:ClusterCountUnits(cluster) + local text=string.format("Cluster #%d. Size %d, Units %d, TLsum=%d", cluster.index, cluster.size, unitcount, cluster.threatlevelSum) if not cluster.marker then - cluster.marker=MARKER:New(cluster.coordinate, text):ToAll() + if self.coalition == coalition.side.RED then + cluster.marker=MARKER:New(cluster.coordinate, text):ToRed() + elseif self.coalition == coalition.side.BLUE then + cluster.marker=MARKER:New(cluster.coordinate, text):ToBlue() + else + cluster.marker=MARKER:New(cluster.coordinate, text):ToNeutral() + end else local refresh=false @@ -1067,3 +1439,306 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +---------------------------------------------------------------------------------------------- +-- Start INTEL_DLINK +---------------------------------------------------------------------------------------------- + +--- **Ops_DLink** - Support for Office of Military Intelligence. +-- +-- **Main Features:** +-- +-- * Overcome limitations of (non-available) datalinks between ground radars +-- * Detect and track contacts consistently across INTEL instances +-- * Use FSM events to link functionality into your scripts +-- * Easy setup +-- +--- === +-- +-- ### Author: **applevangelist** + +--- INTEL_DLINK class. +-- @type INTEL_DLINK +-- @field #string ClassName Name of the class. +-- @field #string lid Class id string for output to DCS log file. +-- @field #number verbose Make the logging verbose. +-- @field #string alias Alias name for logging. +-- @field #number cachetime Number of seconds to keep an object. +-- @field #number interval Number of seconds between collection runs. +-- @field #table contacts Table of Ops.Intelligence#INTEL.Contact contacts. +-- @field #table clusters Table of Ops.Intelligence#INTEL.Cluster clusters. +-- @field #table contactcoords Table of contacts' Core.Point#COORDINATE objects. +-- @extends Core.Fsm#FSM + +--- INTEL_DLINK data aggregator +-- @field #INTEL_DLINK +INTEL_DLINK = { + ClassName = "INTEL_DLINK", + verbose = 0, + lid = nil, + alias = nil, + cachetime = 300, + interval = 20, + contacts = {}, + clusters = {}, + contactcoords = {}, +} + +--- Version string +-- @field #string version +INTEL_DLINK.version = "0.0.1" + +--- Function to instantiate a new object +-- @param #INTEL_DLINK self +-- @param #table Intels Table of Ops.Intelligence#INTEL objects. +-- @param #string Alias (optional) Name of this instance. Default "SPECTRE" +-- @param #number Interval (optional) When to query #INTEL objects for detected items (default 20 seconds). +-- @param #number Cachetime (optional) How long to cache detected items (default 300 seconds). +-- @usage Use #INTEL_DLINK if you want to merge data from a number of #INTEL objects into one. This might be useful to simulate a +-- Data Link, e.g. for Russian-tech based EWR, realising a Star Topology @{https://en.wikipedia.org/wiki/Network_topology#Star} +-- in a basic setup. It will collect the contacts and clusters from the #INTEL objects. +-- Contact duplicates are removed. Clusters might contain duplicates (Might fix that later, WIP). +-- +-- Basic setup: +-- local datalink = INTEL_DLINK:New({myintel1,myintel2}), "FSB", 20, 300) +-- datalink:__Start(2) +-- +-- Add an Intel while running: +-- datalink:AddIntel(myintel3) +-- +-- Gather the data: +-- datalink:GetContactTable() -- #table of #INTEL.Contact contacts. +-- datalink:GetClusterTable() -- #table of #INTEL.Cluster clusters. +-- datalink:GetDetectedItemCoordinates() -- #table of contact coordinates, to be compatible with @{Functional.Detection#DETECTION}. +-- +-- Gather data with the event function: +-- function datalink:OnAfterCollected(From, Event, To, Contacts, Clusters) +-- ... ... +-- end +-- +function INTEL_DLINK:New(Intels, Alias, Interval, Cachetime) + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #INTEL + + self.intels = Intels or {} + self.contacts = {} + self.clusters = {} + self.contactcoords = {} + + -- Set alias. + if Alias then + self.alias=tostring(Alias) + else + self.alias="SPECTRE" + end + + -- Cache time + self.cachetime = Cachetime or 300 + + -- Interval + self.interval = Interval or 20 + + -- Set some string id for output to DCS.log file. + self.lid=string.format("INTEL_DLINK %s | ", self.alias) + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start FSM. + self:AddTransition("*", "Collect", "*") -- Collect data. + self:AddTransition("*", "Collected", "*") -- Collection of data done. + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + + ---------------------------------------------------------------------------------------------- + -- Pseudo Functions + ---------------------------------------------------------------------------------------------- + --- Triggers the FSM event "Start". Starts the INTEL_DLINK. + -- @function [parent=#INTEL_DLINK] Start + -- @param #INTEL_DLINK self + + --- Triggers the FSM event "Start" after a delay. Starts the INTEL_DLINK. + -- @function [parent=#INTEL_DLINK] __Start + -- @param #INTEL_DLINK self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the INTEL_DLINK. + -- @param #INTEL_DLINK self + + --- Triggers the FSM event "Stop" after a delay. Stops the INTEL_DLINK. + -- @function [parent=#INTEL_DLINK] __Stop + -- @param #INTEL_DLINK self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Collect". Used internally to collect all data. + -- @function [parent=#INTEL_DLINK] Collect + -- @param #INTEL_DLINK self + + --- Triggers the FSM event "Collect" after a delay. + -- @function [parent=#INTEL_DLINK] __Status + -- @param #INTEL_DLINK self + -- @param #number delay Delay in seconds. + + --- On After "Collected" event. Data tables have been refreshed. + -- @function [parent=#INTEL_DLINK] OnAfterCollected + -- @param #INTEL_DLINK self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #table Contacts Table of #INTEL.Contact Contacts. + -- @param #table Clusters Table of #INTEL.Cluster Clusters. + + return self +end +---------------------------------------------------------------------------------------------- +-- Helper & User Functions +---------------------------------------------------------------------------------------------- + +--- Function to add an #INTEL object to the aggregator +-- @param #INTEL_DLINK self +-- @param Ops.Intelligence#INTEL Intel the #INTEL object to add +-- @return #INTEL_DLINK self +function INTEL_DLINK:AddIntel(Intel) + self:T(self.lid .. "AddIntel") + if Intel then + table.insert(self.intels,Intel) + end + return self +end + +---------------------------------------------------------------------------------------------- +-- FSM Functions +---------------------------------------------------------------------------------------------- + +--- Function to start the work. +-- @param #INTEL_DLINK self +-- @param #string From The From state +-- @param #string Event The Event triggering this call +-- @param #string To The To state +-- @return #INTEL_DLINK self +function INTEL_DLINK:onafterStart(From, Event, To) + self:T({From, Event, To}) + local text = string.format("Version %s started.", self.version) + self:I(self.lid .. text) + self:__Collect(-math.random(1,10)) + return self +end + +--- Function to collect data from the various #INTEL +-- @param #INTEL_DLINK self +-- @param #string From The From state +-- @param #string Event The Event triggering this call +-- @param #string To The To state +-- @return #INTEL_DLINK self +function INTEL_DLINK:onbeforeCollect(From, Event, To) + self:T({From, Event, To}) + -- run through our #INTEL objects and gather the contacts tables + self:T("Contacts Data Gathering") + local newcontacts = {} + local intels = self.intels -- #table + for _,_intel in pairs (intels) do + _intel = _intel -- #INTEL + if _intel:Is("Running") then + local ctable = _intel:GetContactTable() or {} -- #INTEL.Contact + for _,_contact in pairs (ctable) do + local _ID = string.format("%s-%d",_contact.groupname, _contact.Tdetected) + self:T(string.format("Adding %s",_ID)) + newcontacts[_ID] = _contact + end + end + end + -- clean up for stale contacts and dupes + self:T("Cleanup") + local contacttable = {} + local coordtable = {} + local TNow = timer.getAbsTime() + local Tcache = self.cachetime + for _ind, _contact in pairs(newcontacts) do -- #string, #INTEL.Contact + if TNow - _contact.Tdetected < Tcache then + if (not contacttable[_contact.groupname]) or (contacttable[_contact.groupname] and contacttable[_contact.groupname].Tdetected < _contact.Tdetected) then + self:T(string.format("Adding %s",_contact.groupname)) + contacttable[_contact.groupname] = _contact + table.insert(coordtable,_contact.position) + end + end + end + -- run through our #INTEL objects and gather the clusters tables + self:T("Clusters Data Gathering") + local newclusters = {} + local intels = self.intels -- #table + for _,_intel in pairs (intels) do + _intel = _intel -- #INTEL + if _intel:Is("Running") then + local ctable = _intel:GetClusterTable() or {} -- #INTEL.Cluster + for _,_cluster in pairs (ctable) do + local _ID = string.format("%s-%d", _intel.alias, _cluster.index) + self:T(string.format("Adding %s",_ID)) + table.insert(newclusters,_cluster) + end + end + end + -- update self tables + self.contacts = contacttable + self.contactcoords = coordtable + self.clusters = newclusters + self:__Collected(1, contacttable, newclusters) -- make table available via FSM Event + -- schedule next round + local interv = self.interval * -1 + self:__Collect(interv) + return self +end + +--- Function called after collection is done +-- @param #INTEL_DLINK self +-- @param #string From The From state +-- @param #string Event The Event triggering this call +-- @param #string To The To state +-- @param #table Contacts The table of collected #INTEL.Contact contacts +-- @param #table Clusters The table of collected #INTEL.Cluster clusters +-- @return #INTEL_DLINK self +function INTEL_DLINK:onbeforeCollected(From, Event, To, Contacts, Clusters) + self:T({From, Event, To}) + return self +end + +--- Function to stop +-- @param #INTEL_DLINK self +-- @param #string From The From state +-- @param #string Event The Event triggering this call +-- @param #string To The To state +-- @return #INTEL_DLINK self +function INTEL_DLINK:onafterStop(From, Event, To) + self:T({From, Event, To}) + local text = string.format("Version %s stopped.", self.version) + self:I(self.lid .. text) + return self +end + +--- Function to query the detected contacts +-- @param #INTEL_DLINK self +-- @return #table Table of #INTEL.Contact contacts +function INTEL_DLINK:GetContactTable() + self:T(self.lid .. "GetContactTable") + return self.contacts +end + +--- Function to query the detected clusters +-- @param #INTEL_DLINK self +-- @return #table Table of #INTEL.Cluster clusters +function INTEL_DLINK:GetClusterTable() + self:T(self.lid .. "GetClusterTable") + return self.clusters +end + +--- Function to query the detected contact coordinates +-- @param #INTEL_DLINK self +-- @return #table Table of the contacts' Core.Point#COORDINATE objects. +function INTEL_DLINK:GetDetectedItemCoordinates() + self:T(self.lid .. "GetDetectedItemCoordinates") + return self.contactcoords +end + +---------------------------------------------------------------------------------------------- +-- End INTEL_DLINK +---------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/NavyGroup.lua b/Moose Development/Moose/Ops/NavyGroup.lua index 6c6f54d44..4925626c2 100644 --- a/Moose Development/Moose/Ops/NavyGroup.lua +++ b/Moose Development/Moose/Ops/NavyGroup.lua @@ -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 freepathself.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 diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua index 48145683b..e5d7feb93 100644 --- a/Moose Development/Moose/Ops/OpsGroup.lua +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -1,11 +1,11 @@ --- **Ops** - Generic group enhancement. --- +-- -- This class is **not** meant to be used itself by the end user. It contains common functionalities of derived classes for air, ground and sea. --- +-- -- === -- -- ### Author: **funkyfranky** --- +-- -- === -- @module Ops.OpsGroup -- @image OPS_OpsGroup.png @@ -19,18 +19,32 @@ -- @field #string lid Class id string for output to DCS log file. -- @field #string groupname Name of the group. -- @field Wrapper.Group#GROUP group Group object. --- @field #table template Template of the group. +-- @field DCS#Template template Template table of the group. -- @field #boolean isLateActivated Is the group late activated. -- @field #boolean isUncontrolled Is the group uncontrolled. +-- @field #boolean isFlightgroup Is a FLIGHTGROUP. +-- @field #boolean isArmygroup Is an ARMYGROUP. +-- @field #boolean isNavygroup Is a NAVYGROUP. +-- @field #boolean isHelo If true, the is a helicopter group. +-- @field #boolean isVTOL If true, the is capable of Vertical TakeOff and Landing (VTOL). -- @field #table elements Table of elements, i.e. units of the group. -- @field #boolean isAI If true, group is purely AI. -- @field #boolean isAircraft If true, group is airplane or helicopter. -- @field #boolean isNaval If true, group is ships or submarine. -- @field #boolean isGround If true, group is some ground unit. +-- @field #boolean isDestroyed If true, the whole group was destroyed. +-- @field #boolean isDead If true, the whole group is dead. -- @field #table waypoints Table of waypoints. -- @field #table waypoints0 Table of initial waypoints. +-- @field Wrapper.Airbase#AIRBASE homebase The home base of the flight group. +-- @field Wrapper.Airbase#AIRBASE destbase The destination base of the flight group. +-- @field Wrapper.Airbase#AIRBASE currbase The current airbase of the flight group, i.e. where it is currently located or landing at. +-- @field Core.Zone#ZONE homezone The home zone of the flight group. Set when spawn happens in air. +-- @field Core.Zone#ZONE destzone The destination zone of the flight group. Set when final waypoint is in air. -- @field #number currentwp Current waypoint index. This is the index of the last passed waypoint. -- @field #boolean adinfinitum Resume route at first waypoint when final waypoint is reached. +-- @field #number Twaiting Abs. mission time stamp when the group was ordered to wait. +-- @field #number dTwait Time to wait in seconds. Default `nil` (for ever). -- @field #table taskqueue Queue of tasks. -- @field #number taskcounter Running number of task ids. -- @field #number taskcurrent ID of current task. If 0, there is no current task assigned. @@ -56,9 +70,9 @@ -- @field Ops.Auftrag#AUFTRAG missionpaused Paused mission. -- @field #number Ndestroyed Number of destroyed units. -- @field #number Nkills Number kills of this groups. --- +-- -- @field Core.Point#COORDINATE coordinate Current coordinate. --- +-- -- @field DCS#Vec3 position Position of the group at last status check. -- @field DCS#Vec3 positionLast Backup of last position vec to monitor changes. -- @field #number heading Heading of the group at last status check. @@ -67,31 +81,48 @@ -- @field DCS#Vec3 orientXLast Backup of last orientation to monitor changes. -- @field #number traveldist Distance traveled in meters. This is a lower bound. -- @field #number traveltime Time. --- +-- -- @field Core.Astar#ASTAR Astar path finding. -- @field #boolean ispathfinding If true, group is on pathfinding route. --- +-- -- @field #OPSGROUP.Radio radio Current radio settings. -- @field #OPSGROUP.Radio radioDefault Default radio settings. -- @field Core.RadioQueue#RADIOQUEUE radioQueue Radio queue. --- +-- -- @field #OPSGROUP.Beacon tacan Current TACAN settings. -- @field #OPSGROUP.Beacon tacanDefault Default TACAN settings. --- +-- -- @field #OPSGROUP.Beacon icls Current ICLS settings. -- @field #OPSGROUP.Beacon iclsDefault Default ICLS settings. --- +-- -- @field #OPSGROUP.Option option Current optional settings. -- @field #OPSGROUP.Option optionDefault Default option settings. --- +-- -- @field #OPSGROUP.Callsign callsign Current callsign settings. -- @field #OPSGROUP.Callsign callsignDefault Default callsign settings. --- +-- @field #string callsignName Callsign name. +-- @field #string callsignAlias Callsign alias. +-- -- @field #OPSGROUP.Spot spot Laser and IR spot. --- +-- -- @field #OPSGROUP.Ammo ammo Initial ammount of ammo. -- @field #OPSGROUP.WeaponData weaponData Weapon data table with key=BitType. +-- +-- @field #OPSGROUP.Element carrier Carrier the group is loaded into as cargo. +-- @field #OPSGROUP carrierGroup Carrier group transporting this group as cargo. +-- @field #OPSGROUP.MyCarrier mycarrier Carrier group for this group. +-- @field #table cargoqueue Table containing cargo groups to be transported. +-- @field #table cargoBay Table containing OPSGROUP loaded into this group. +-- @field Ops.OpsTransport#OPSTRANSPORT cargoTransport Current cargo transport assignment. +-- @field #string cargoStatus Cargo status of this group acting as cargo. +-- @field #number cargoTransportUID Unique ID of the transport assignment this cargo group is associated with. +-- @field #string carrierStatus Carrier status of this group acting as cargo carrier. +-- @field #OPSGROUP.CarrierLoader carrierLoader Carrier loader parameters. +-- @field #OPSGROUP.CarrierLoader carrierUnloader Carrier unloader parameters. -- +-- @field #boolean useSRS Use SRS for transmissions. +-- @field Sound.SRS#MSRS msrs MOOSE SRS wrapper. +-- -- @extends Core.Fsm#FSM --- *A small group of determined and like-minded people can change the course of history.* --- Mahatma Gandhi @@ -101,13 +132,13 @@ -- ![Banner Image](..\Presentations\OPS\OpsGroup\_Main.png) -- -- # The OPSGROUP Concept --- --- The OPSGROUP class contains common functions used by other classes such as FLIGHGROUP, NAVYGROUP and ARMYGROUP. --- Those classes inherit everything of this class and extend it with features specific to their unit category. --- +-- +-- The OPSGROUP class contains common functions used by other classes such as FLIGHTGROUP, NAVYGROUP and ARMYGROUP. +-- Those classes inherit everything of this class and extend it with features specific to their unit category. +-- -- This class is **NOT** meant to be used by the end user itself. --- --- +-- +-- -- @field #OPSGROUP OPSGROUP = { ClassName = "OPSGROUP", @@ -128,7 +159,7 @@ OPSGROUP = { taskenroute = nil, taskpaused = {}, missionqueue = {}, - currentmission = nil, + currentmission = nil, detectedunits = {}, detectedgroups = {}, attribute = nil, @@ -146,20 +177,60 @@ OPSGROUP = { Ndestroyed = 0, Nkills = 0, weaponData = {}, + cargoqueue = {}, + cargoBay = {}, + mycarrier = {}, + carrierLoader = {}, + carrierUnloader = {}, } --- OPS group element. -- @type OPSGROUP.Element -- @field #string name Name of the element, i.e. the unit. +-- @field #string status The element status. See @{#OPSGROUP.ElementStatus}. -- @field Wrapper.Unit#UNIT unit The UNIT object. --- @field #string status The element status. +-- @field Wrapper.Group#GROUP group The GROUP object. +-- @field DCS#Unit DCSunit The DCS unit object. +-- @field #boolean ai If true, element is AI. +-- @field #string skill Skill level. +-- +-- @field Core.Zone#ZONE_POLYGON_BASE zoneBoundingbox Bounding box zone of the element unit. +-- @field Core.Zone#ZONE_POLYGON_BASE zoneLoad Loading zone. +-- @field Core.Zone#ZONE_POLYGON_BASE zoneUnload Unloading zone. +-- -- @field #string typename Type name. +-- @field #number category Aircraft category. +-- @field #string categoryname Aircraft category name. +-- +-- @field #number size Size (max of length, width, height) in meters. -- @field #number length Length of element in meters. -- @field #number width Width of element in meters. -- @field #number height Height of element in meters. +-- +-- @field DCS#Vec3 vec3 Last known 3D position vector. +-- @field DCS#Vec3 orientX Last known ordientation vector in the direction of the nose X. +-- @field #number heading Last known heading in degrees. +-- -- @field #number life0 Initial life points. -- @field #number life Life points when last updated. +-- @field #number damage Damage of element in percent. +-- +-- @field DCS#Object.Desc descriptors Descriptors table. +-- @field #number weightEmpty Empty weight in kg. +-- @field #number weightMaxTotal Max. total weight in kg. +-- @field #number weightMaxCargo Max. cargo weight in kg. +-- @field #number weightCargo Current cargo weight in kg. +-- @field #number weight Current weight including cargo in kg. +-- @field #table cargoBay Cargo bay. +-- +-- @field #string modex Tail number. +-- @field Wrapper.Client#CLIENT client The client if element is occupied by a human player. +-- @field #table pylons Table of pylons. +-- @field #number fuelmass Mass of fuel in kg. +-- @field #string callsign Call sign, e.g. "Uzi 1-1". +-- @field Wrapper.Airbase#AIRBASE.ParkingSpot parking The parking spot table the element is parking on. + --- Status of group element. -- @type OPSGROUP.ElementStatus @@ -213,6 +284,7 @@ OPSGROUP.TaskType={ --- Task structure. -- @type OPSGROUP.Task -- @field #string type Type of task: either SCHEDULED or WAYPOINT. +-- @field #boolean ismission This is an AUFTRAG task. -- @field #number id Task ID. Running number to get the task. -- @field #number prio Priority. -- @field #number time Abs. mission time when to execute the task. @@ -223,6 +295,7 @@ OPSGROUP.TaskType={ -- @field #number timestamp Abs. mission time, when task was started. -- @field #number waypoint Waypoint index if task is a waypoint task. -- @field Core.UserFlag#USERFLAG stopflag If flag is set to 1 (=true), the task is stopped. +-- @field #number backupROE Rules of engagement that are restored once the task is over. --- Enroute task. -- @type OPSGROUP.EnrouteTask @@ -248,9 +321,7 @@ OPSGROUP.TaskType={ -- @type OPSGROUP.Callsign -- @field #number NumberSquad Squadron number corresponding to a name like "Uzi". -- @field #number NumberGroup Group number. First number after name, e.g. "Uzi-**1**-1". --- @field #number NumberElement Element number.Second number after name, e.g. "Uzi-1-**1**" -- @field #string NameSquad Name of the squad, e.g. "Uzi". --- @field #string NameElement Name of group element, e.g. Uzi 11. --- Option data. -- @type OPSGROUP.Option @@ -302,6 +373,13 @@ OPSGROUP.TaskType={ -- @field #number MissilesAS Amount of anti-ship missiles. -- @field #number MissilesCR Amount of cruise missiles. -- @field #number MissilesBM Amount of ballistic missiles. +-- @field #number MissilesSA Amount of surfe-to-air missiles. + +--- Spawn point data. +-- @type OPSGROUP.Spawnpoint +-- @field Core.Point#COORDINATE Coordinate Coordinate where to spawn +-- @field Wrapper.Airbase#AIRBASE Airport Airport where to spawn. +-- @field #table TerminalIDs Terminal IDs, where to spawn the group. It is a table of `#number`s because a group can consist of multiple units. --- Waypoint data. -- @type OPSGROUP.Waypoint @@ -317,6 +395,7 @@ OPSGROUP.TaskType={ -- @field #boolean detour If true, this waypoint is not part of the normal route. -- @field #boolean intowind If true, this waypoint is a turn into wind route point. -- @field #boolean astar If true, this waypint was found by A* pathfinding algorithm. +-- @field #boolean temp If true, this is a temporary waypoint and will be deleted when passed. Also the passing waypoint FSM event is not triggered. -- @field #number npassed Number of times a groups passed this waypoint. -- @field Core.Point#COORDINATE coordinate Waypoint coordinate. -- @field Core.Point#COORDINATE roadcoord Closest point to road. @@ -324,18 +403,80 @@ OPSGROUP.TaskType={ -- @field Wrapper.Marker#MARKER marker Marker on the F10 map. -- @field #string formation Ground formation. Similar to action but on/off road. ---- NavyGroup version. +--- Cargo Carrier status. +-- @type OPSGROUP.CarrierStatus +-- @field #string NOTCARRIER This group is not a carrier yet. +-- @field #string PICKUP Carrier is on its way to pickup cargo. +-- @field #string LOADING Carrier is loading cargo. +-- @field #string LOADED Carrier has loaded cargo. +-- @field #string TRANSPORTING Carrier is transporting cargo. +-- @field #string UNLOADING Carrier is unloading cargo. +OPSGROUP.CarrierStatus={ + NOTCARRIER="not carrier", + PICKUP="pickup", + LOADING="loading", + LOADED="loaded", + TRANSPORTING="transporting", + UNLOADING="unloading", +} + +--- Cargo status. +-- @type OPSGROUP.CargoStatus +-- @field #string NOTCARGO This group is no cargo yet. +-- @field #string ASSIGNED Cargo is assigned to a carrier. +-- @field #string BOARDING Cargo is boarding a carrier. +-- @field #string LOADED Cargo is loaded into a carrier. +OPSGROUP.CargoStatus={ + NOTCARGO="not cargo", + ASSIGNED="assigned to carrier", + BOARDING="boarding", + LOADED="loaded", +} + +--- Cargo carrier loader parameters. +-- @type OPSGROUP.CarrierLoader +-- @field #string type Loader type "Front", "Back", "Left", "Right", "All". +-- @field #number length Length of (un-)loading zone in meters. +-- @field #number width Width of (un-)loading zone in meters. + +--- Data of the carrier that has loaded this group. +-- @type OPSGROUP.MyCarrier +-- @field #OPSGROUP group The carrier group. +-- @field #OPSGROUP.Element element The carrier element. +-- @field #boolean reserved If `true`, the carrier has caro space reserved for me. + +--- Element cargo bay data. +-- @type OPSGROUP.MyCargo +-- @field #OPSGROUP group The cargo group. +-- @field #boolean reserved If `true`, the cargo bay space is reserved but cargo has not actually been loaded yet. + +--- Cargo group data. +-- @type OPSGROUP.CargoGroup +-- @field #OPSGROUP opsgroup The cargo opsgroup. +-- @field #boolean delivered If `true`, group was delivered. +-- @field #OPSGROUP disembarkCarrierGroup Carrier group where the cargo group is directly loaded to. +-- @field #OPSGROUP disembarkCarrierElement Carrier element to which the cargo group is directly loaded to. +-- @field #string status Status of the cargo group. Not used yet. + +--- OpsGroup version. -- @field #string version -OPSGROUP.version="0.7.0" +OPSGROUP.version="0.7.5" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - + -- TODO: AI on/off. +-- TODO: Emission on/off. -- TODO: Invisible/immortal. -- TODO: F10 menu. -- TODO: Add pseudo function. +-- TODO: Options EPLRS +-- TODO: Afterburner restrict +-- TODO: What more options? +-- TODO: Damage? +-- TODO: Shot events? +-- TODO: Marks to add waypoints/tasks on-the-fly. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor @@ -343,25 +484,25 @@ OPSGROUP.version="0.7.0" --- Create a new OPSGROUP class object. -- @param #OPSGROUP 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 #OPSGROUP self -function OPSGROUP:New(Group) +function OPSGROUP:New(group) -- Inherit everything from FSM class. local self=BASE:Inherit(self, FSM:New()) -- #OPSGROUP - + -- Get group and group name. - if type(Group)=="string" then - self.groupname=Group + if type(group)=="string" then + self.groupname=group self.group=GROUP:FindByName(self.groupname) else - self.group=Group - self.groupname=Group:GetName() + self.group=group + self.groupname=group:GetName() end - + -- Set some string id for output to DCS.log file. self.lid=string.format("OPSGROUP %s | ", tostring(self.groupname)) - + if self.group then if not self:IsExist() then self:E(self.lid.."ERROR: GROUP does not exist! Returning nil") @@ -369,15 +510,18 @@ function OPSGROUP:New(Group) end end + -- Set the template. + self:_SetTemplate() + -- Init set of detected units. self.detectedunits=SET_UNIT:New() - + -- Init set of detected groups. self.detectedgroups=SET_GROUP:New() - + -- Init inzone set. self.inzones=SET_ZONE:New() - + -- Laser. self.spot={} self.spot.On=false @@ -385,28 +529,37 @@ function OPSGROUP:New(Group) self.spot.Coordinate=COORDINATE:New(0, 0, 0) self:SetLaser(1688, true, false, 0.5) + -- Cargo. + self.cargoStatus=OPSGROUP.CargoStatus.NOTCARGO + self.carrierStatus=OPSGROUP.CarrierStatus.NOTCARRIER + self:SetCarrierLoaderAllAspect() + self:SetCarrierUnloaderAllAspect() + -- Init task counter. self.taskcurrent=0 self.taskcounter=0 - + -- Start state. self:SetStartState("InUtero") -- Add FSM transitions. -- From State --> Event --> To State self:AddTransition("InUtero", "Spawned", "Spawned") -- The whole group was spawned. + self:AddTransition("*", "Respawn", "InUtero") -- Respawn group. self:AddTransition("*", "Dead", "Dead") -- The whole group is dead. + self:AddTransition("*", "InUtero", "InUtero") -- Deactivated group goes back to mummy. self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. self:AddTransition("*", "Status", "*") -- Status update. - - self:AddTransition("*", "Destroyed", "*") -- The whole group is dead. + + self:AddTransition("*", "Destroyed", "*") -- The whole group is dead. self:AddTransition("*", "Damaged", "*") -- Someone in the group took damage. self:AddTransition("*", "UpdateRoute", "*") -- Update route of group. Only if airborne. - self:AddTransition("*", "Respawn", "*") -- Respawn group. self:AddTransition("*", "PassingWaypoint", "*") -- Passing waypoint. - + + self:AddTransition("*", "Wait", "*") -- Group will wait for further orders. + self:AddTransition("*", "DetectedUnit", "*") -- Unit was detected (again) in this detection cycle. self:AddTransition("*", "DetectedUnitNew", "*") -- Add a newly detected unit to the detected units set. self:AddTransition("*", "DetectedUnitKnown", "*") -- A known unit is still detected. @@ -415,8 +568,8 @@ function OPSGROUP:New(Group) self:AddTransition("*", "DetectedGroup", "*") -- Unit was detected (again) in this detection cycle. self:AddTransition("*", "DetectedGroupNew", "*") -- Add a newly detected unit to the detected units set. self:AddTransition("*", "DetectedGroupKnown", "*") -- A known unit is still detected. - self:AddTransition("*", "DetectedGroupLost", "*") -- Group lost a detected target group. - + self:AddTransition("*", "DetectedGroupLost", "*") -- Group lost a detected target group. + self:AddTransition("*", "PassingWaypoint", "*") -- Group passed a waypoint. self:AddTransition("*", "GotoWaypoint", "*") -- Group switches to a specific waypoint. @@ -441,7 +594,7 @@ function OPSGROUP:New(Group) self:AddTransition("*", "TaskPause", "*") -- Pause current task. Not implemented yet! self:AddTransition("*", "TaskCancel", "*") -- Cancel current task. self:AddTransition("*", "TaskDone", "*") -- Task is over. - + self:AddTransition("*", "MissionStart", "*") -- Mission is started. self:AddTransition("*", "MissionExecute", "*") -- Mission execution began. self:AddTransition("*", "MissionCancel", "*") -- Cancel current mission. @@ -449,11 +602,27 @@ function OPSGROUP:New(Group) self:AddTransition("*", "UnpauseMission", "*") -- Unpause the the paused mission. self:AddTransition("*", "MissionDone", "*") -- Mission is over. + self:AddTransition("*", "ElementInUtero", "*") -- An element is in utero again. self:AddTransition("*", "ElementSpawned", "*") -- An element was spawned. self:AddTransition("*", "ElementDestroyed", "*") -- An element was destroyed. self:AddTransition("*", "ElementDead", "*") -- An element is dead. self:AddTransition("*", "ElementDamaged", "*") -- An element was damaged. + self:AddTransition("*", "Board", "*") -- Group is ordered to board the carrier. + self:AddTransition("*", "Embarked", "*") -- Group was loaded into a cargo carrier. + self:AddTransition("*", "Disembarked", "*") -- Group was unloaded from a cargo carrier. + + self:AddTransition("*", "Pickup", "*") -- Carrier and is on route to pick up cargo. + self:AddTransition("*", "Loading", "*") -- Carrier is loading cargo. + self:AddTransition("*", "Load", "*") -- Carrier loads cargo into carrier. + self:AddTransition("*", "Loaded", "*") -- Carrier loaded all assigned/possible cargo into carrier. + self:AddTransition("*", "Transport", "*") -- Carrier is transporting cargo. + self:AddTransition("*", "Unloading", "*") -- Carrier is unloading the cargo. + self:AddTransition("*", "Unload", "*") -- Carrier unload a cargo group. + self:AddTransition("*", "Unloaded", "*") -- Carrier unloaded all its current cargo. + self:AddTransition("*", "UnloadingDone", "*") -- Carrier is unloading the cargo. + self:AddTransition("*", "Delivered", "*") -- Carrier delivered ALL cargo of the transport assignment. + ------------------------ --- Pseudo Functions --- ------------------------ @@ -477,7 +646,7 @@ function OPSGROUP:New(Group) -- TODO: Add pseudo functions. - return self + return self end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -493,12 +662,34 @@ end --- Returns the absolute (average) life points of the group. -- @param #OPSGROUP self +-- @param #OPSGROUP.Element Element (Optional) Only get life points of this element. -- @return #number Life points. If group contains more than one element, the average is given. -- @return #number Initial life points. -function OPSGROUP:GetLifePoints() - if self.group then - return self.group:GetLife(), self.group:GetLife0() +function OPSGROUP:GetLifePoints(Element) + + local life=0 + local life0=0 + + if Element then + + local unit=Element.unit + + if unit then + life=unit:GetLife() + life0=unit:GetLife0() + end + + else + + for _,element in pairs(self.elements) do + local l,l0=self:GetLifePoints(element) + life=life+l + life0=life+l0 + end + end + + return life, life0 end @@ -530,7 +721,7 @@ function OPSGROUP:GetSpeedCruise() end --- Set detection on or off. --- If detection is on, detected targets of the group will be evaluated and FSM events triggered. +-- If detection is on, detected targets of the group will be evaluated and FSM events triggered. -- @param #OPSGROUP self -- @param #boolean Switch If `true`, detection is on. If `false` or `nil`, detection is off. Default is off. -- @return #OPSGROUP self @@ -539,7 +730,7 @@ function OPSGROUP:SetDetection(Switch) return self end ---- Set LASER parameters. +--- Set LASER parameters. -- @param #OPSGROUP self -- @param #number Code Laser code. Default 1688. -- @param #boolean CheckLOS Check if lasing unit has line of sight to target coordinate. Default is `true`. @@ -558,7 +749,7 @@ function OPSGROUP:SetLaser(Code, CheckLOS, IROff, UpdateTime) return self end ---- Get LASER code. +--- Get LASER code. -- @param #OPSGROUP self -- @return #number Current Laser code. function OPSGROUP:GetLaserCode() @@ -601,7 +792,7 @@ function OPSGROUP:AddCheckZone(CheckZone) end ---- Add a weapon range for ARTY auftrag. +--- Add a weapon range for ARTY auftrag. -- @param #OPSGROUP self -- @param #number RangeMin Minimum range in nautical miles. Default 0 NM. -- @param #number RangeMax Maximum range in nautical miles. Default 10 NM. @@ -618,9 +809,9 @@ function OPSGROUP:AddWeaponRange(RangeMin, RangeMax, BitType) weapon.RangeMax=RangeMax weapon.RangeMin=RangeMin - self.weaponData=self.weaponData or {} + self.weaponData=self.weaponData or {} self.weaponData[weapon.BitType]=weapon - + return self end @@ -632,7 +823,7 @@ function OPSGROUP:GetWeaponData(BitType) BitType=BitType or ENUMS.WeaponFlag.Auto - if self.weaponData[BitType] then + if self.weaponData[BitType] then return self.weaponData[BitType] else return self.weaponData[ENUMS.WeaponFlag.Auto] @@ -676,22 +867,22 @@ function OPSGROUP:GetThreat(ThreatLevelMin, ThreatLevelMax) local level=0 for _,_unit in pairs(self.detectedunits:GetSet()) do local unit=_unit --Wrapper.Unit#UNIT - + -- Get threatlevel of unit. local threatlevel=unit:GetThreatLevel() - + -- Check if withing threasholds. if threatlevel>=ThreatLevelMin and threatlevel<=ThreatLevelMax then - + if threatlevellevelmax then threat=unit levelmax=threatlevel end - + end return threat, levelmax @@ -736,8 +927,8 @@ function OPSGROUP:HasLoS(Coordinate, Element, OffsetElement, OffsetCoordinate) end --- Function to check LoS for an element of the group. - local function checklos(element) - local vec3=element.unit:GetVec3() + local function checklos(element) + local vec3=element.unit:GetVec3() if OffsetElement then vec3=UTILS.VecAdd(vec3, OffsetElement) end @@ -746,19 +937,19 @@ function OPSGROUP:HasLoS(Coordinate, Element, OffsetElement, OffsetCoordinate) return _los end - if Element then + if Element then local los=checklos(Element) return los else - + for _,element in pairs(self.elements) do -- Get LoS of this element. - local los=checklos(element) + local los=checklos(element) if los then return true end end - + return false end @@ -794,12 +985,12 @@ end function OPSGROUP:GetUnit(UnitNumber) local DCSUnit=self:GetDCSUnit(UnitNumber) - + if DCSUnit then local unit=UNIT:Find(DCSUnit) return unit end - + return nil end @@ -810,12 +1001,12 @@ end function OPSGROUP:GetDCSUnit(UnitNumber) local DCSGroup=self:GetDCSGroup() - + if DCSGroup then local unit=DCSGroup:getUnit(UnitNumber or 1) return unit end - + return nil end @@ -825,132 +1016,24 @@ end function OPSGROUP:GetDCSUnits() local DCSGroup=self:GetDCSGroup() - + if DCSGroup then local units=DCSGroup:getUnits() return units end - + return nil end ---- Despawn the group. The whole group is despawned and (optionally) a "Remove Unit" event is generated for all current units of the group. --- @param #OPSGROUP self --- @param #number Delay Delay in seconds before the group will be despawned. Default immediately. --- @param #boolean NoEventRemoveUnit If true, no event "Remove Unit" is generated. --- @return #OPSGROUP self -function OPSGROUP:Despawn(Delay, NoEventRemoveUnit) - - if Delay and Delay>0 then - self:ScheduleOnce(Delay, OPSGROUP.Despawn, self, 0, NoEventRemoveUnit) - else - - local DCSGroup=self:GetDCSGroup() - - if DCSGroup then - - -- Destroy DCS group. - DCSGroup:destroy() - - if not NoEventRemoveUnit then - - -- Get all units. - local units=self:GetDCSUnits() - - -- Create a "Remove Unit" event. - local EventTime=timer.getTime() - for i=1,#units do - self:CreateEventRemoveUnit(EventTime, units[i]) - end - - end - end - end - - return self -end - ---- Destroy group. The whole group is despawned and a *Unit Lost* for aircraft or *Dead* event for ground/naval units is generated for all current units. --- @param #OPSGROUP self --- @param #number Delay Delay in seconds before the group will be destroyed. Default immediately. --- @return #OPSGROUP self -function OPSGROUP:Destroy(Delay) - - if Delay and Delay>0 then - self:ScheduleOnce(Delay, OPSGROUP.Destroy, self) - else - - local DCSGroup=self:GetDCSGroup() - - if DCSGroup then - - self:T(self.lid.."Destroying group") - - -- Destroy DCS group. - DCSGroup:destroy() - - -- Get all units. - local units=self:GetDCSUnits() - - -- Create a "Unit Lost" event. - local EventTime=timer.getTime() - for i=1,#units do - if self.isAircraft then - self:CreateEventUnitLost(EventTime, units[i]) - else - self:CreateEventDead(EventTime, units[i]) - end - end - end - - end - - return self -end - ---- Despawn an element/unit of the group. --- @param #OPSGROUP self --- @param #OPSGROUP.Element Element The element that will be despawned. --- @param #number Delay Delay in seconds before the element will be despawned. Default immediately. --- @param #boolean NoEventRemoveUnit If true, no event "Remove Unit" is generated. --- @return #OPSGROUP self -function OPSGROUP:DespawnElement(Element, Delay, NoEventRemoveUnit) - - if Delay and Delay>0 then - self:ScheduleOnce(Delay, OPSGROUP.DespawnElement, self, Element, 0, NoEventRemoveUnit) - else - - if Element then - - -- Get DCS unit object. - local DCSunit=Unit.getByName(Element.name) - - if DCSunit then - - -- Destroy object. - DCSunit:destroy() - - -- Create a remove unit event. - if not NoEventRemoveUnit then - self:CreateEventRemoveUnit(timer.getTime(), DCSunit) - end - - end - - end - - end - - return self -end --- Get current 2D position vector of the group. -- @param #OPSGROUP self +-- @param #string UnitName (Optional) Get position of a specifc unit of the group. Default is the first existing unit in the group. -- @return DCS#Vec2 Vector with x,y components. -function OPSGROUP:GetVec2() +function OPSGROUP:GetVec2(UnitName) + + local vec3=self:GetVec3(UnitName) - local vec3=self:GetVec3() - if vec3 then local vec2={x=vec3.x, y=vec3.z} return vec2 @@ -962,34 +1045,54 @@ end --- Get current 3D position vector of the group. -- @param #OPSGROUP self +-- @param #string UnitName (Optional) Get position of a specifc unit of the group. Default is the first existing unit in the group. -- @return DCS#Vec3 Vector with x,y,z components. -function OPSGROUP:GetVec3() - if self:IsExist() then - - local unit=self:GetDCSUnit() - - if unit then - local vec3=unit:getPoint() - +function OPSGROUP:GetVec3(UnitName) + + local vec3=nil --DCS#Vec3 + + -- First check if this group is loaded into a carrier + local carrier=self:_GetMyCarrierElement() + if carrier and carrier.status~=OPSGROUP.ElementStatus.DEAD and self:IsLoaded() then + local unit=carrier.unit + if unit and unit:IsAlive()~=nil then + vec3=unit:GetVec3() return vec3 end - end + + if self:IsExist() then + + local unit=nil --DCS#Unit + if UnitName then + unit=Unit.getByName(UnitName) + else + unit=self:GetDCSUnit() + end + + + if unit then + local vec3=unit:getPoint() + return vec3 + end + + end + return nil end ---- Get current coordinate of the group. +--- Get current coordinate of the group. If the current position cannot be determined, the last known position is returned. -- @param #OPSGROUP self -- @param #boolean NewObject Create a new coordiante object. -- @return Core.Point#COORDINATE The coordinate (of the first unit) of the group. function OPSGROUP:GetCoordinate(NewObject) - local vec3=self:GetVec3() + local vec3=self:GetVec3() or self.position --DCS#Vec3 if vec3 then - + self.coordinate=self.coordinate or COORDINATE:New(0,0,0) - + self.coordinate.x=vec3.x self.coordinate.y=vec3.y self.coordinate.z=vec3.z @@ -999,100 +1102,126 @@ function OPSGROUP:GetCoordinate(NewObject) return coord else return self.coordinate - end + end else - self:E(self.lid.."WARNING: Group is not alive. Cannot get coordinate!") + self:E(self.lid.."WARNING: Cannot get coordinate!") end - + return nil end --- Get current velocity of the group. -- @param #OPSGROUP self +-- @param #string UnitName (Optional) Get heading of a specific unit of the group. Default is from the first existing unit in the group. -- @return #number Velocity in m/s. -function OPSGROUP:GetVelocity() +function OPSGROUP:GetVelocity(UnitName) + if self:IsExist() then - - local unit=self:GetDCSUnit(1) - - if unit then - - local velvec3=unit:getVelocity() - - local vel=UTILS.VecNorm(velvec3) - - return vel - + + local unit=nil --DCS#Unit + + if UnitName then + unit=Unit.getByName(UnitName) + else + unit=self:GetDCSUnit() end + + if unit then + + local velvec3=unit:getVelocity() + + local vel=UTILS.VecNorm(velvec3) + + return vel + + else + self:E(self.lid.."WARNING: Unit does not exist. Cannot get velocity!") + end + else self:E(self.lid.."WARNING: Group does not exist. Cannot get velocity!") end + return nil end ---- Get current heading of the group. +--- Get current heading of the group or (optionally) of a specific unit of the group. -- @param #OPSGROUP self +-- @param #string UnitName (Optional) Get heading of a specific unit of the group. Default is from the first existing unit in the group. -- @return #number Current heading of the group in degrees. -function OPSGROUP:GetHeading() +function OPSGROUP:GetHeading(UnitName) if self:IsExist() then - - local unit=self:GetDCSUnit() - + + local unit=nil --DCS#Unit + if UnitName then + unit=Unit.getByName(UnitName) + else + unit=self:GetDCSUnit() + end + if unit then - + local pos=unit:getPosition() - + local heading=math.atan2(pos.x.z, pos.x.x) - + if heading<0 then heading=heading+ 2*math.pi end - + heading=math.deg(heading) - + return heading end - + else self:E(self.lid.."WARNING: Group does not exist. Cannot get heading!") end - + return nil end ---- Get current orientation of the first unit in the group. +--- Get current orientation of the group. -- @param #OPSGROUP self +-- @param #string UnitName (Optional) Get orientation of a specific unit of the group. Default is the first existing unit of the group. -- @return DCS#Vec3 Orientation X parallel to where the "nose" is pointing. -- @return DCS#Vec3 Orientation Y pointing "upwards". -- @return DCS#Vec3 Orientation Z perpendicular to the "nose". -function OPSGROUP:GetOrientation() +function OPSGROUP:GetOrientation(UnitName) if self:IsExist() then - - local unit=self:GetDCSUnit() - + + local unit=nil --DCS#Unit + + if UnitName then + unit=Unit.getByName(UnitName) + else + unit=self:GetDCSUnit() + end + if unit then - + local pos=unit:getPosition() - + return pos.x, pos.y, pos.z end - + else self:E(self.lid.."WARNING: Group does not exist. Cannot get orientation!") end - + return nil end ---- Get current orientation of the first unit in the group. +--- Get current "X" orientation of the first unit in the group. -- @param #OPSGROUP self +-- @param #string UnitName (Optional) Get orientation of a specific unit of the group. Default is the first existing unit of the group. -- @return DCS#Vec3 Orientation X parallel to where the "nose" is pointing. -function OPSGROUP:GetOrientationX() +function OPSGROUP:GetOrientationX(UnitName) + + local X,Y,Z=self:GetOrientation(UnitName) - local X,Y,Z=self:GetOrientation() - return X end @@ -1116,6 +1245,176 @@ function OPSGROUP:CheckTaskDescriptionUnique(description) end +--- Despawn a unit of the group. A "Remove Unit" event is generated by default. +-- @param #OPSGROUP self +-- @param #string UnitName Name of the unit +-- @param #number Delay Delay in seconds before the group will be despawned. Default immediately. +-- @param #boolean NoEventRemoveUnit If true, no event "Remove Unit" is generated. +-- @return #OPSGROUP self +function OPSGROUP:DespawnUnit(UnitName, Delay, NoEventRemoveUnit) + + -- Debug info. + self:T(self.lid.."Despawn element "..tostring(UnitName)) + + -- Get element. + local element=self:GetElementByName(UnitName) + + if element then + + -- Get DCS unit object. + local DCSunit=Unit.getByName(UnitName) + + if DCSunit then + + -- Despawn unit. + DCSunit:destroy() + + -- Element goes back in utero. + self:ElementInUtero(element) + + if not NoEventRemoveUnit then + self:CreateEventRemoveUnit(timer.getTime(), DCSunit) + end + + end + + end + +end + +--- Despawn an element/unit of the group. +-- @param #OPSGROUP self +-- @param #OPSGROUP.Element Element The element that will be despawned. +-- @param #number Delay Delay in seconds before the element will be despawned. Default immediately. +-- @param #boolean NoEventRemoveUnit If true, no event "Remove Unit" is generated. +-- @return #OPSGROUP self +function OPSGROUP:DespawnElement(Element, Delay, NoEventRemoveUnit) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP.DespawnElement, self, Element, 0, NoEventRemoveUnit) + else + + if Element then + + -- Get DCS unit object. + local DCSunit=Unit.getByName(Element.name) + + if DCSunit then + + -- Destroy object. + DCSunit:destroy() + + -- Create a remove unit event. + if not NoEventRemoveUnit then + self:CreateEventRemoveUnit(timer.getTime(), DCSunit) + end + + end + + end + + end + + return self +end + +--- Despawn the group. The whole group is despawned and (optionally) a "Remove Unit" event is generated for all current units of the group. +-- @param #OPSGROUP self +-- @param #number Delay Delay in seconds before the group will be despawned. Default immediately. +-- @param #boolean NoEventRemoveUnit If true, no event "Remove Unit" is generated. +-- @return #OPSGROUP self +function OPSGROUP:Despawn(Delay, NoEventRemoveUnit) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP.Despawn, self, 0, NoEventRemoveUnit) + else + + local DCSGroup=self:GetDCSGroup() + + if DCSGroup then + + -- Clear any task ==> makes DCS crash! + --self.group:ClearTasks() + + -- Get all units. + local units=self:GetDCSUnits() + + for i=1,#units do + local unit=units[i] + if unit then + local name=unit:getName() + if name then + -- Despawn the unit. + self:DespawnUnit(name, 0, NoEventRemoveUnit) + end + end + end + + end + end + + return self +end + +--- Destroy a unit of the group. A *Unit Lost* for aircraft or *Dead* event for ground/naval units is generated. +-- @param #OPSGROUP self +-- @param #string UnitName Name of the unit which should be destroyed. +-- @param #number Delay Delay in seconds before the group will be destroyed. Default immediately. +-- @return #OPSGROUP self +function OPSGROUP:DestroyUnit(UnitName, Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP.DestroyUnit, self, UnitName, 0) + else + + local unit=Unit.getByName(UnitName) + + if unit then + + -- Create a "Unit Lost" event. + local EventTime=timer.getTime() + + if self.isAircraft then + self:CreateEventUnitLost(EventTime, unit) + else + self:CreateEventDead(EventTime, unit) + end + + end + + end + +end + +--- Destroy group. The whole group is despawned and a *Unit Lost* for aircraft or *Dead* event for ground/naval units is generated for all current units. +-- @param #OPSGROUP self +-- @param #number Delay Delay in seconds before the group will be destroyed. Default immediately. +-- @return #OPSGROUP self +function OPSGROUP:Destroy(Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP.Destroy, self, 0) + else + + -- Get all units. + local units=self:GetDCSUnits() + + if units then + + -- Create a "Unit Lost" event. + for _,unit in pairs(units) do + if unit then + self:DestroyUnit(unit:getName()) + end + end + + end + + end + + return self +end + --- Activate a *late activated* group. -- @param #OPSGROUP self -- @param #number delay (Optional) Delay in seconds before the group is activated. Default is immediately. @@ -1124,21 +1423,46 @@ function OPSGROUP:Activate(delay) if delay and delay>0 then self:T2(self.lid..string.format("Activating late activated group in %d seconds", delay)) - self:ScheduleOnce(delay, OPSGROUP.Activate, self) + self:ScheduleOnce(delay, OPSGROUP.Activate, self) else - + if self:IsAlive()==false then - + self:T(self.lid.."Activating late activated group") self.group:Activate() self.isLateActivated=false - + elseif self:IsAlive()==true then self:E(self.lid.."WARNING: Activating group that is already activated") else self:E(self.lid.."ERROR: Activating group that is does not exist!") end - + + end + + return self +end + +--- Deactivate the group. Group will be respawned in late activated state. +-- @param #OPSGROUP self +-- @param #number delay (Optional) Delay in seconds before the group is deactivated. Default is immediately. +-- @return #OPSGROUP self +function OPSGROUP:Deactivate(delay) + + if delay and delay>0 then + self:ScheduleOnce(delay, OPSGROUP.Deactivate, self) + else + + if self:IsAlive()==true then + + self.template.lateActivation=true + + local template=UTILS.DeepCopy(self.template) + + self:_Respawn(0, template) + + end + end return self @@ -1147,26 +1471,218 @@ end --- Self destruction of group. An explosion is created at the position of each element. -- @param #OPSGROUP self -- @param #number Delay Delay in seconds. Default now. --- @param #number ExplosionPower (Optional) Explosion power in kg TNT. Default 500 kg. --- @return #number Relative fuel in percent. +-- @param #number ExplosionPower (Optional) Explosion power in kg TNT. Default 100 kg. +-- @return #OPSGROUP self function OPSGROUP:SelfDestruction(Delay, ExplosionPower) if Delay and Delay>0 then self:ScheduleOnce(Delay, OPSGROUP.SelfDestruction, self, 0, ExplosionPower) else - + -- Loop over all elements. for i,_element in pairs(self.elements) do local element=_element --#OPSGROUP.Element - + local unit=element.unit - + if unit and unit:IsAlive() then - unit:Explode(ExplosionPower) + unit:Explode(ExplosionPower or 100) end end end + return self +end + +--- Use SRS Simple-Text-To-Speech for transmissions. +-- @param #OPSGROUP self +-- @param #string PathToSRS Path to SRS directory. +-- @param #string Gender Gender: "male" or "female" (default). +-- @param #string Culture Culture, e.g. "en-GB" (default). +-- @param #string Voice Specific voice. Overrides `Gender` and `Culture`. +-- @param #number Port SRS port. Default 5002. +-- @return #OPSGROUP self +function OPSGROUP:SetSRS(PathToSRS, Gender, Culture, Voice, Port) + self.useSRS=true + self.msrs=MSRS:New(PathToSRS, self.frequency, self.modulation) + self.msrs:SetGender(Gender) + self.msrs:SetCulture(Culture) + self.msrs:SetVoice(Voice) + self.msrs:SetPort(Port) + self.msrs:SetCoalition(self:GetCoalition()) + return self +end + +--- Send a radio transmission via SRS Text-To-Speech. +-- @param #OPSGROUP self +-- @param #string Text Text of transmission. +-- @param #number Delay Delay in seconds before the transmission is started. +-- @return #OPSGROUP self +function OPSGROUP:RadioTransmission(Text, Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP.RadioTransmission, self, Text, 0) + else + + if self.useSRS and self.msrs then + + local freq, modu, radioon=self:GetRadio() + + self.msrs:SetFrequencies(freq) + self.msrs:SetModulations(modu) + + -- Debug info. + self:I(self.lid..string.format("Radio transmission on %.3f MHz %s: %s", freq, UTILS.GetModulationName(modu), Text)) + + self.msrs:PlayText(Text) + end + + end + + return self +end + +--- Set that this carrier is an all aspect loader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierLoaderAllAspect(Length, Width) + self.carrierLoader.type="front" + self.carrierLoader.length=Length or 50 + self.carrierLoader.width=Width or 20 + return self +end + +--- Set that this carrier is a front loader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierLoaderFront(Length, Width) + self.carrierLoader.type="front" + self.carrierLoader.length=Length or 50 + self.carrierLoader.width=Width or 20 + return self +end + +--- Set that this carrier is a back loader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierLoaderBack(Length, Width) + self.carrierLoader.type="back" + self.carrierLoader.length=Length or 50 + self.carrierLoader.width=Width or 20 + return self +end + +--- Set that this carrier is a starboard (right side) loader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierLoaderStarboard(Length, Width) + self.carrierLoader.type="right" + self.carrierLoader.length=Length or 50 + self.carrierLoader.width=Width or 20 + return self +end + +--- Set that this carrier is a port (left side) loader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierLoaderPort(Length, Width) + self.carrierLoader.type="left" + self.carrierLoader.length=Length or 50 + self.carrierLoader.width=Width or 20 + return self +end + + +--- Set that this carrier is an all aspect unloader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierUnloaderAllAspect(Length, Width) + self.carrierUnloader.type="front" + self.carrierUnloader.length=Length or 50 + self.carrierUnloader.width=Width or 20 + return self +end + +--- Set that this carrier is a front unloader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierUnloaderFront(Length, Width) + self.carrierUnloader.type="front" + self.carrierUnloader.length=Length or 50 + self.carrierUnloader.width=Width or 20 + return self +end + +--- Set that this carrier is a back unloader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierUnloaderBack(Length, Width) + self.carrierUnloader.type="back" + self.carrierUnloader.length=Length or 50 + self.carrierUnloader.width=Width or 20 + return self +end + +--- Set that this carrier is a starboard (right side) unloader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierUnloaderStarboard(Length, Width) + self.carrierUnloader.type="right" + self.carrierUnloader.length=Length or 50 + self.carrierUnloader.width=Width or 20 + return self +end + +--- Set that this carrier is a port (left side) unloader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierUnloaderPort(Length, Width) + self.carrierUnloader.type="left" + self.carrierUnloader.length=Length or 50 + self.carrierUnloader.width=Width or 20 + return self +end + + +--- Check if this is a FLIGHTGROUP. +-- @param #OPSGROUP self +-- @return #boolean If true, this is an airplane or helo group. +function OPSGROUP:IsFlightgroup() + return self.isFlightgroup +end + +--- Check if this is a ARMYGROUP. +-- @param #OPSGROUP self +-- @return #boolean If true, this is a ground group. +function OPSGROUP:IsArmygroup() + return self.isArmygroup +end + +--- Check if this is a NAVYGROUP. +-- @param #OPSGROUP self +-- @return #boolean If true, this is a ship group. +function OPSGROUP:IsNavygroup() + return self.isNavygroup end @@ -1176,7 +1692,7 @@ end function OPSGROUP:IsExist() local DCSGroup=self:GetDCSGroup() - + if DCSGroup then local exists=DCSGroup:isExist() return exists @@ -1190,6 +1706,12 @@ end -- @return #boolean If true, the group exists or false if the group does not exist. If nil, the DCS group could not be found. function OPSGROUP:IsActive() + if self.group then + local active=self.group:IsActive() + return active + end + + return nil end --- Check if group is alive. @@ -1216,28 +1738,43 @@ end -- @param #OPSGROUP self -- @return #boolean If true, group is not spawned yet. function OPSGROUP:IsInUtero() - return self:Is("InUtero") + local is=self:Is("InUtero") + return is end --- Check if group is in state spawned. -- @param #OPSGROUP self -- @return #boolean If true, group is spawned. function OPSGROUP:IsSpawned() - return self:Is("Spawned") + local is=self:Is("Spawned") + return is end --- Check if group is dead. -- @param #OPSGROUP self -- @return #boolean If true, all units/elements of the group are dead. function OPSGROUP:IsDead() - return self:Is("Dead") + if self.isDead then + return true + else + local is=self:Is("Dead") + return is + end +end + +--- Check if group was destroyed. +-- @param #OPSGROUP self +-- @return #boolean If true, all units/elements of the group were destroyed. +function OPSGROUP:IsDestroyed() + return self.isDestroyed end --- Check if FSM is stopped. -- @param #OPSGROUP self -- @return #boolean If true, FSM state is stopped. function OPSGROUP:IsStopped() - return self:Is("Stopped") + local is=self:Is("Stopped") + return is end --- Check if this group is currently "uncontrolled" and needs to be "started" to begin its route. @@ -1273,14 +1810,142 @@ end -- @param #OPSGROUP self -- @return #boolean If true, group is retreating. function OPSGROUP:IsRetreating() - return self:is("Retreating") + local is=self:is("Retreating") + return is end --- Check if the group is engaging another unit or group. -- @param #OPSGROUP self -- @return #boolean If true, group is engaging. function OPSGROUP:IsEngaging() - return self:is("Engaging") + local is=self:is("Engaging") + return is +end + +--- Check if group is currently waiting. +-- @param #OPSGROUP self +-- @return #boolean If true, group is currently waiting. +function OPSGROUP:IsWaiting() + if self.Twaiting then + return true + end + return false +end + +--- Check if the group is not a carrier yet. +-- @param #OPSGROUP self +-- @return #boolean If true, group is not a carrier. +function OPSGROUP:IsNotCarrier() + return self.carrierStatus==OPSGROUP.CarrierStatus.NOTCARRIER +end + +--- Check if the group is a carrier. +-- @param #OPSGROUP self +-- @return #boolean If true, group is a carrier. +function OPSGROUP:IsCarrier() + return not self:IsNotCarrier() +end + +--- Check if the group is picking up cargo. +-- @param #OPSGROUP self +-- @return #boolean If true, group is picking up. +function OPSGROUP:IsPickingup() + return self.carrierStatus==OPSGROUP.CarrierStatus.PICKUP +end + +--- Check if the group is loading cargo. +-- @param #OPSGROUP self +-- @return #boolean If true, group is loading. +function OPSGROUP:IsLoading() + return self.carrierStatus==OPSGROUP.CarrierStatus.LOADING +end + +--- Check if the group is transporting cargo. +-- @param #OPSGROUP self +-- @return #boolean If true, group is transporting. +function OPSGROUP:IsTransporting() + return self.carrierStatus==OPSGROUP.CarrierStatus.TRANSPORTING +end + +--- Check if the group is unloading cargo. +-- @param #OPSGROUP self +-- @return #boolean If true, group is unloading. +function OPSGROUP:IsUnloading() + return self.carrierStatus==OPSGROUP.CarrierStatus.UNLOADING +end + + +--- Check if the group is assigned as cargo. +-- @param #OPSGROUP self +-- @return #boolean If true, group is cargo. +function OPSGROUP:IsCargo() + return not self:IsNotCargo() +end + +--- Check if the group is **not** cargo. +-- @param #OPSGROUP self +-- @param #boolean CheckTransport If `true` or `nil`, also check if cargo is associated with a transport assignment. If not, we consider it not cargo. +-- @return #boolean If true, group is *not* cargo. +function OPSGROUP:IsNotCargo(CheckTransport) + local notcargo=self.cargoStatus==OPSGROUP.CargoStatus.NOTCARGO + + if notcargo then + -- Not cargo. + return true + else + -- Is cargo (e.g. loaded or boarding) + + if CheckTransport then + -- Check if transport UID was set. + if self.cargoTransportUID==nil then + return true + else + -- Some transport UID was assigned. + return false + end + else + -- Is cargo. + return false + end + + end + + + return notcargo +end + +--- Check if the group is currently boarding a carrier. +-- @param #OPSGROUP self +-- @param #string CarrierGroupName (Optional) Additionally check if group is boarding this particular carrier group. +-- @return #boolean If true, group is boarding. +function OPSGROUP:IsBoarding(CarrierGroupName) + if CarrierGroupName then + local carrierGroup=self:_GetMyCarrierGroup() + if carrierGroup and carrierGroup.groupname~=CarrierGroupName then + return false + end + end + return self.cargoStatus==OPSGROUP.CargoStatus.BOARDING +end + +--- Check if the group is currently loaded into a carrier. +-- @param #OPSGROUP self +-- @param #string CarrierGroupName (Optional) Additionally check if group is loaded into a particular carrier group(s). +-- @return #boolean If true, group is loaded. +function OPSGROUP:IsLoaded(CarrierGroupName) + if CarrierGroupName then + if type(CarrierGroupName)~="table" then + CarrierGroupName={CarrierGroupName} + end + for _,CarrierName in pairs(CarrierGroupName) do + local carrierGroup=self:_GetMyCarrierGroup() + if carrierGroup and carrierGroup.groupname==CarrierName then + return true + end + end + return false + end + return self.cargoStatus==OPSGROUP.CargoStatus.LOADED end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1302,21 +1967,21 @@ function OPSGROUP:MarkWaypoints(Duration) for i,_waypoint in pairs(self.waypoints or {}) do local waypoint=_waypoint --#OPSGROUP.Waypoint - + local text=string.format("Waypoint ID=%d of %s", waypoint.uid, self.groupname) text=text..string.format("\nSpeed=%.1f kts, Alt=%d ft (%s)", UTILS.MpsToKnots(waypoint.speed), UTILS.MetersToFeet(waypoint.alt), "BARO") - + if waypoint.marker then if waypoint.marker.text~=text then waypoint.marker.text=text end - + else waypoint.marker=MARKER:New(waypoint.coordinate, text):ToCoalition(self:GetCoalition()) end end - - + + if Duration then self:RemoveWaypointMarkers(Duration) end @@ -1336,14 +2001,14 @@ function OPSGROUP:RemoveWaypointMarkers(Delay) for i,_waypoint in pairs(self.waypoints or {}) do local waypoint=_waypoint --#OPSGROUP.Waypoint - + if waypoint.marker then waypoint.marker:Remove() end end - + end - + return self end @@ -1424,24 +2089,24 @@ function OPSGROUP:GetWaypointIndexNext(cyclic, i) if cyclic==nil then cyclic=self.adinfinitum end - + local N=#self.waypoints - + i=i or self.currentwp local n=math.min(i+1, N) - + if cyclic and i==N then n=1 end - + return n end --- Get current waypoint index. This is the index of the last passed waypoint. -- @param #OPSGROUP self -- @return #number Current waypoint index. -function OPSGROUP:GetWaypointIndexCurrent() +function OPSGROUP:GetWaypointIndexCurrent() return self.currentwp or 1 end @@ -1456,8 +2121,8 @@ function OPSGROUP:GetWaypointIndexAfterID(uid) return index+1 else return #self.waypoints+1 - end - + end + end --- Get waypoint. @@ -1482,7 +2147,7 @@ end function OPSGROUP:GetWaypointNext(cyclic) local n=self:GetWaypointIndexNext(cyclic) - + return self.waypoints[n] end @@ -1499,7 +2164,7 @@ end -- @return Core.Point#COORDINATE Coordinate of the next waypoint. function OPSGROUP:GetNextWaypointCoordinate(cyclic) - -- Get next waypoint + -- Get next waypoint local waypoint=self:GetWaypointNext(cyclic) return waypoint.coordinate @@ -1524,7 +2189,7 @@ end function OPSGROUP:GetWaypointSpeed(indx) local waypoint=self:GetWaypoint(indx) - + if waypoint then return UTILS.MpsToKnots(waypoint.speed) end @@ -1547,7 +2212,7 @@ end function OPSGROUP:GetWaypointID(indx) local waypoint=self:GetWaypoint(indx) - + if waypoint then return waypoint.uid end @@ -1563,7 +2228,7 @@ end function OPSGROUP:GetSpeedToWaypoint(indx) local speed=self:GetWaypointSpeed(indx) - + if speed<=0.1 then speed=self:GetSpeedCruise() end @@ -1577,22 +2242,22 @@ end -- @return #number Distance in meters. function OPSGROUP:GetDistanceToWaypoint(indx) local dist=0 - + if #self.waypoints>0 then indx=indx or self:GetWaypointIndexNext() - + local wp=self:GetWaypoint(indx) - + if wp then - + local coord=self:GetCoordinate() - + dist=coord:Get2DDistance(wp.coordinate) end - + end - + return dist end @@ -1601,21 +2266,21 @@ end -- @param #number indx Waypoint index. Default is the next waypoint. -- @return #number Time in seconds. If velocity is 0 function OPSGROUP:GetTimeToWaypoint(indx) - + local s=self:GetDistanceToWaypoint(indx) - + local v=self:GetVelocity() - + local t=s/v - + if t==math.inf then return 365*24*60*60 elseif t==math.nan then return 0 - else + else return t end - + end --- Returns the currently expected speed. @@ -1628,7 +2293,7 @@ function OPSGROUP:GetExpectedSpeed() else return self.speedWp or 0 end - + end --- Remove a waypoint with a ceratin UID. @@ -1638,9 +2303,9 @@ end function OPSGROUP:RemoveWaypointByID(uid) local index=self:GetWaypointIndex(uid) - + if index then - self:RemoveWaypoint(index) + self:RemoveWaypoint(index) end return self @@ -1653,10 +2318,10 @@ end function OPSGROUP:RemoveWaypoint(wpindex) if self.waypoints then - + -- Number of waypoints before delete. local N=#self.waypoints - + -- Remove waypoint marker. local wp=self:GetWaypoint(wpindex) if wp and wp.marker then @@ -1665,22 +2330,22 @@ function OPSGROUP:RemoveWaypoint(wpindex) -- Remove waypoint. table.remove(self.waypoints, wpindex) - + -- Number of waypoints after delete. local n=#self.waypoints - + -- Debug info. self:T(self.lid..string.format("Removing waypoint index %d, current wp index %d. N %d-->%d", wpindex, self.currentwp, N, n)) - + -- Waypoint was not reached yet. if wpindex > self.currentwp then - + --- -- Removed a FUTURE waypoint --- - + -- TODO: patrol adinfinitum. - + if self.currentwp>=n then self.passedfinalwp=true end @@ -1688,34 +2353,173 @@ function OPSGROUP:RemoveWaypoint(wpindex) self:_CheckGroupDone(1) else - + --- -- Removed a waypoint ALREADY PASSED --- - + -- If an already passed waypoint was deleted, we do not need to update the route. - + -- If current wp = 1 it stays 1. Otherwise decrease current wp. - + if self.currentwp==1 then - + if self.adinfinitum then self.currentwp=#self.waypoints else self.currentwp=1 end - + else self.currentwp=self.currentwp-1 end - + end - + end return self end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- DCS Events +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Event function handling the birth of a unit. +-- @param #OPSGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function OPSGROUP: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 + + + -- Set homebase if not already set. + if self.isFlightgroup then + + if EventData.Place then + self.homebase=self.homebase or EventData.Place + self.currbase=EventData.Place + else + self.currbase=nil + end + + if self.homebase and not self.destbase then + self.destbase=self.homebase + end + + self:T(self.lid..string.format("EVENT: Element %s born at airbase %s ==> spawned", unitname, self.currbase and self.currbase:GetName() or "unknown")) + else + self:T3(self.lid..string.format("EVENT: Element %s born ==> spawned", unitname)) + end + + -- Get element. + local element=self:GetElementByName(unitname) + + if element then + + -- Set element to spawned state. + self:ElementSpawned(element) + + end + + end + +end + +--- Event function handling the crash of a unit. +-- @param #OPSGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function OPSGROUP: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:T2(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 and element.status~=OPSGROUP.ElementStatus.DEAD 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 #OPSGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function OPSGROUP: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 + self:T2(self.lid..string.format("EVENT: Unit %s removed!", EventData.IniUnitName)) + + local unit=EventData.IniUnit + local group=EventData.IniGroup + local unitname=EventData.IniUnitName + + -- Get element. + local element=self:GetElementByName(unitname) + + if element and element.status~=OPSGROUP.ElementStatus.DEAD then + self:T(self.lid..string.format("EVENT: Element %s removed ==> dead", element.name)) + self:ElementDead(element) + end + + end + +end + +--- Event function handling the event that a unit achieved a kill. +-- @param #OPSGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function OPSGROUP:OnEventKill(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 + + -- Target name + local targetname=tostring(EventData.TgtUnitName) + + -- Debug info. + self:T2(self.lid..string.format("EVENT: Unit %s killed object %s!", tostring(EventData.IniUnitName), targetname)) + + -- Check if this was a UNIT or STATIC object. + local target=UNIT:FindByName(targetname) + if not target then + target=STATIC:FindByName(targetname, false) + end + + -- Only count UNITS and STATICs (not SCENERY) + if target then + + -- Debug info. + self:T(self.lid..string.format("EVENT: Unit %s killed unit/static %s!", tostring(EventData.IniUnitName), targetname)) + + -- Kill counter. + self.Nkills=self.Nkills+1 + + -- Check if on a mission. + local mission=self:GetMissionCurrent() + if mission then + mission.Nkills=mission.Nkills+1 -- Increase mission kill counter. + end + + end + + end + +end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Task Functions @@ -1728,16 +2532,16 @@ end function OPSGROUP:SetTask(DCSTask) if self:IsAlive() then - + if self.taskcurrent>0 then - + -- TODO: Why the hell did I do this? It breaks scheduled tasks. I comment it out for now to see where it fails. --local task=self:GetTaskCurrent() --self:RemoveTask(task) --self.taskcurrent=0 - + end - + -- Inject enroute tasks. if self.taskenroute and #self.taskenroute>0 then if tostring(DCSTask.id)=="ComboTask" then @@ -1747,14 +2551,14 @@ function OPSGROUP:SetTask(DCSTask) else local tasks=UTILS.DeepCopy(self.taskenroute) table.insert(tasks, DCSTask) - + DCSTask=self.group.TaskCombo(self, tasks) end end - + -- Set task. self.group:SetTask(DCSTask) - + -- Debug info. local text=string.format("SETTING Task %s", tostring(DCSTask.id)) if tostring(DCSTask.id)=="ComboTask" then @@ -1762,9 +2566,9 @@ function OPSGROUP:SetTask(DCSTask) text=text..string.format("\n[%d] %s", i, tostring(task.id)) end end - self:T(self.lid..text) + self:T(self.lid..text) end - + return self end @@ -1775,10 +2579,10 @@ end function OPSGROUP:PushTask(DCSTask) if self:IsAlive() then - + -- Push task. self.group:PushTask(DCSTask) - + -- Debug info. local text=string.format("PUSHING Task %s", tostring(DCSTask.id)) if tostring(DCSTask.id)=="ComboTask" then @@ -1786,9 +2590,9 @@ function OPSGROUP:PushTask(DCSTask) text=text..string.format("\n[%d] %s", i, tostring(task.id)) end end - self:T(self.lid..text) + self:T(self.lid..text) end - + return self end @@ -1818,7 +2622,7 @@ function OPSGROUP:AddTask(task, clock, description, prio, duration) -- Add to table. table.insert(self.taskqueue, newtask) - + -- Info. self:T(self.lid..string.format("Adding SCHEDULED task %s starting at %s", newtask.description, UTILS.SecondsToClock(newtask.time, true))) self:T3({newtask=newtask}) @@ -1853,14 +2657,14 @@ function OPSGROUP:NewTaskScheduled(task, clock, description, prio, duration) local newtask={} --#OPSGROUP.Task newtask.status=OPSGROUP.TaskStatus.SCHEDULED newtask.dcstask=task - newtask.description=description or task.id + newtask.description=description or task.id newtask.prio=prio or 50 newtask.time=time newtask.id=self.taskcounter newtask.duration=duration newtask.waypoint=-1 newtask.type=OPSGROUP.TaskType.SCHEDULED - newtask.stopflag=USERFLAG:New(string.format("%s StopTaskFlag %d", self.groupname, newtask.id)) + newtask.stopflag=USERFLAG:New(string.format("%s StopTaskFlag %d", self.groupname, newtask.id)) newtask.stopflag:Set(0) return newtask @@ -1870,15 +2674,15 @@ end -- @param #OPSGROUP self -- @param #table task DCS task table structure. -- @param #OPSGROUP.Waypoint Waypoint where the task is executed. Default is the at *next* waypoint. --- @param #string description Brief text describing the task, e.g. "Attack SAM". +-- @param #string description Brief text describing the task, e.g. "Attack SAM". -- @param #number prio Priority of the task. Number between 1 and 100. Default is 50. -- @param #number duration Duration before task is cancelled in seconds counted after task started. Default never. -- @return #OPSGROUP.Task The task structure. function OPSGROUP:AddTaskWaypoint(task, Waypoint, description, prio, duration) - + -- Waypoint of task. Waypoint=Waypoint or self:GetWaypointNext() - + if Waypoint then -- Increase counter. @@ -1895,22 +2699,22 @@ function OPSGROUP:AddTaskWaypoint(task, Waypoint, description, prio, duration) newtask.time=0 newtask.waypoint=Waypoint.uid newtask.type=OPSGROUP.TaskType.WAYPOINT - newtask.stopflag=USERFLAG:New(string.format("%s StopTaskFlag %d", self.groupname, newtask.id)) + newtask.stopflag=USERFLAG:New(string.format("%s StopTaskFlag %d", self.groupname, newtask.id)) newtask.stopflag:Set(0) - + -- Add to table. table.insert(self.taskqueue, newtask) - + -- Info. self:T(self.lid..string.format("Adding WAYPOINT task %s at WP ID=%d", newtask.description, newtask.waypoint)) self:T3({newtask=newtask}) - + -- Update route. self:__UpdateRoute(-1) - - return newtask + + return newtask end - + return nil end @@ -1922,7 +2726,7 @@ function OPSGROUP:AddTaskEnroute(task) if not self.taskenroute then self.taskenroute={} end - + -- Check not to add the same task twice! local gotit=false for _,Task in pairs(self.taskenroute) do @@ -1931,11 +2735,11 @@ function OPSGROUP:AddTaskEnroute(task) break end end - + if not gotit then table.insert(self.taskenroute, task) end - + end --- Get the unfinished waypoint tasks @@ -1944,7 +2748,7 @@ end -- @return #table Table of tasks. Table could also be empty {}. function OPSGROUP:GetTasksWaypoint(id) - -- Tasks table. + -- Tasks table. local tasks={} -- Sort queue. @@ -1957,7 +2761,7 @@ function OPSGROUP:GetTasksWaypoint(id) table.insert(tasks, task) end end - + return tasks end @@ -1967,7 +2771,7 @@ end -- @return #number Number of waypoint tasks. function OPSGROUP:CountTasksWaypoint(id) - -- Tasks table. + -- Tasks table. local n=0 -- Look for first task that SCHEDULED. @@ -1977,7 +2781,7 @@ function OPSGROUP:CountTasksWaypoint(id) n=n+1 end end - + return n end @@ -1991,7 +2795,7 @@ function OPSGROUP:_SortTaskQueue() local taskB=b --#OPSGROUP.Task return (taskA.prio0) then + + if Mission:IsReadyToPush() then + + -- Not waiting any more. + self.Twaiting=nil + self.dTwait=nil + + else + + --- + -- Not ready to push yet + --- + + if self:IsWaiting() then + -- Group is already waiting + else + self:Wait() + end + + -- Time to for the next try. + local dt=Mission.Tpush and Mission.Tpush-timer.getAbsTime() or 20 + + + -- Debug info. + self:T(self.lid..string.format("Mission %s task execute suspended for %d seconds", Mission.name, dt)) + + -- Reexecute task. + self:__TaskExecute(-dt, Task) + + -- Deny transition. + return false + end + + end + + return true +end + --- On after "TaskExecute" event. -- @param #OPSGROUP self -- @param #string From From state. @@ -2127,7 +2982,7 @@ function OPSGROUP:onafterTaskExecute(From, Event, To, Task) -- Debug message. local text=string.format("Task %s ID=%d execute", tostring(Task.description), Task.id) self:T(self.lid..text) - + -- Cancel current task if there is any. if self.taskcurrent>0 then self:TaskCancel() @@ -2142,74 +2997,110 @@ function OPSGROUP:onafterTaskExecute(From, Event, To, Task) -- Task status executing. Task.status=OPSGROUP.TaskStatus.EXECUTING + -- Insert into task queue. Not sure any more, why I added this. But probably if a task is just executed without having been put into the queue. + if self:GetTaskCurrent()==nil then + table.insert(self.taskqueue, Task) + end + if Task.dcstask.id=="Formation" then -- Set of group(s) to follow Mother. local followSet=SET_GROUP:New():AddGroup(self.group) - + local param=Task.dcstask.params - + local followUnit=UNIT:FindByName(param.unitname) - + -- Define AI Formation object. Task.formation=AI_FORMATION:New(followUnit, followSet, "Formation", "Follow X at given parameters.") - + -- Formation parameters. Task.formation:FormationCenterWing(-param.offsetX, 50, math.abs(param.altitude), 50, param.offsetZ, 50) - + -- Set follow time interval. Task.formation:SetFollowTimeInterval(param.dtFollow) - + -- Formation mode. Task.formation:SetFlightModeFormation(self.group) - + -- Start formation FSM. - Task.formation:Start() - + Task.formation:Start() + + elseif Task.dcstask.id=="PatrolZone" then + + --- + -- Task patrol zone. + --- + + -- Parameters. + local zone=Task.dcstask.params.zone --Core.Zone#ZONE + local Coordinate=zone:GetRandomCoordinate() + Coordinate:MarkToAll("Random Patrol Zone Coordinate") + local Speed=UTILS.KmphToKnots(Task.dcstask.params.speed or self.speedCruise) + local Altitude=Task.dcstask.params.altitude and UTILS.MetersToFeet(Task.dcstask.params.altitude) or nil + + -- New waypoint. + if self.isFlightgroup then + FLIGHTGROUP.AddWaypoint(self, Coordinate, Speed, AfterWaypointWithID, Altitude) + elseif self.isNavygroup then + ARMYGROUP.AddWaypoint(self, Coordinate, Speed, AfterWaypointWithID, Formation) + elseif self.isArmygroup then + NAVYGROUP.AddWaypoint(self, Coordinate, Speed, AfterWaypointWithID, Altitude) + end + else -- If task is scheduled (not waypoint) set task. - if Task.type==OPSGROUP.TaskType.SCHEDULED then - + if Task.type==OPSGROUP.TaskType.SCHEDULED or Task.ismission then + local DCStasks={} if Task.dcstask.id=='ComboTask' then -- Loop over all combo tasks. for TaskID, Task in ipairs(Task.dcstask.params.tasks) do table.insert(DCStasks, Task) - end + end else table.insert(DCStasks, Task.dcstask) end - + -- Combo task. local TaskCombo=self.group:TaskCombo(DCStasks) - - -- Stop condition! + + -- Stop condition! local TaskCondition=self.group:TaskCondition(nil, Task.stopflag:GetName(), 1, nil, Task.duration) - - -- Controlled task. + + -- Controlled task. local TaskControlled=self.group:TaskControlled(TaskCombo, TaskCondition) - + -- Task done. local TaskDone=self.group:TaskFunction("OPSGROUP._TaskDone", self, Task) - + -- Final task. local TaskFinal=self.group:TaskCombo({TaskControlled, TaskDone}) - - -- Set task for group. - self:SetTask(TaskFinal, 1) - - end - - end + -- Set task for group. + -- NOTE: I am pushing the task instead of setting it as it seems to keep the mission task alive. + -- There were issues that flights did not proceed to a later waypoint because the task did not finish until the fired missiles + -- impacted (took rather long). Then the flight flew to the nearest airbase and one lost completely the control over the group. + self:PushTask(TaskFinal) + --self:SetTask(TaskFinal) + + + elseif Task.type==OPSGROUP.TaskType.WAYPOINT then + -- Waypoint tasks are executed elsewhere! + else + self:E(self.lid.."ERROR: Unknown task type: ") + end + + end + -- Get mission of this task (if any). local Mission=self:GetMissionByTaskID(self.taskcurrent) if Mission then -- Set AUFTRAG status. self:MissionExecute(Mission) end - + end --- On after "TaskCancel" event. Cancels the current task or simply sets the status to DONE if the task is not the current one. @@ -2219,58 +3110,60 @@ end -- @param #string To To state. -- @param #OPSGROUP.Task Task The task to cancel. Default is the current task (if any). function OPSGROUP:onafterTaskCancel(From, Event, To, Task) - + -- Get current task. local currenttask=self:GetTaskCurrent() - + -- If no task, we take the current task. But this could also be *nil*! Task=Task or currenttask - + if Task then - + -- Check if the task is the current task? if currenttask and Task.id==currenttask.id then - + -- Current stop flag value. I noticed cases, where setting the flag to 1 would not cancel the task, e.g. when firing HARMS on a dead ship. local stopflag=Task.stopflag:Get() - + -- Debug info. local text=string.format("Current task %s ID=%d cancelled (flag %s=%d)", Task.description, Task.id, Task.stopflag:GetName(), stopflag) self:T(self.lid..text) - + -- Set stop flag. When the flag is true, the _TaskDone function is executed and calls :TaskDone() Task.stopflag:Set(1) - + local done=false if Task.dcstask.id=="Formation" then Task.formation:Stop() done=true + elseif Task.dcstask.id=="PatrolZone" then + done=true elseif stopflag==1 or (not self:IsAlive()) or self:IsDead() or self:IsStopped() then -- Manual call TaskDone if setting flag to one was not successful. done=true end - + if done then self:TaskDone(Task) end - + else - + -- Debug info. self:T(self.lid..string.format("TaskCancel: Setting task %s ID=%d to DONE", Task.description, Task.id)) - - -- Call task done function. + + -- Call task done function. self:TaskDone(Task) end - + else - + local text=string.format("WARNING: No (current) task to cancel!") self:E(self.lid..text) - + end - + end --- On before "TaskDone" event. Deny transition if task status is PAUSED. @@ -2306,17 +3199,22 @@ function OPSGROUP:onafterTaskDone(From, Event, To, Task) if Task.id==self.taskcurrent then self.taskcurrent=0 end - + -- Task status done. Task.status=OPSGROUP.TaskStatus.DONE - + + -- Restore old ROE. + if Task.backupROE then + self:SwitchROE(Task.backupROE) + end + -- Check if this task was the task of the current mission ==> Mission Done! local Mission=self:GetMissionByTaskID(Task.id) - + if Mission and Mission:IsNotOver() then - - local status=Mission:GetGroupStatus(self) - + + local status=Mission:GetGroupStatus(self) + if status~=AUFTRAG.GroupStatus.PAUSED then self:T(self.lid.."Task Done ==> Mission Done!") self:MissionDone(Mission) @@ -2324,10 +3222,15 @@ function OPSGROUP:onafterTaskDone(From, Event, To, Task) --Mission paused. Do nothing! end else + + if Task.description=="Engage_Target" then + self:Disengage() + end + self:T(self.lid.."Task Done but NO mission found ==> _CheckGroupDone in 1 sec") self:_CheckGroupDone(1) end - + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2339,27 +3242,27 @@ end -- @param Ops.Auftrag#AUFTRAG Mission Mission for this group. -- @return #OPSGROUP self function OPSGROUP:AddMission(Mission) - + -- Add group to mission. Mission:AddOpsGroup(self) - + -- Set group status to SCHEDULED.. Mission:SetGroupStatus(self, AUFTRAG.GroupStatus.SCHEDULED) - + -- Set mission status to SCHEDULED. Mission:Scheduled() - + -- Add elements. Mission.Nelements=Mission.Nelements+#self.elements -- Add mission to queue. table.insert(self.missionqueue, Mission) - + -- Info text. - local text=string.format("Added %s mission %s starting at %s, stopping at %s", + local text=string.format("Added %s mission %s starting at %s, stopping at %s", tostring(Mission.type), tostring(Mission.name), UTILS.SecondsToClock(Mission.Tstart, true), Mission.Tstop and UTILS.SecondsToClock(Mission.Tstop, true) or "INF") self:T(self.lid..text) - + return self end @@ -2371,22 +3274,22 @@ function OPSGROUP:RemoveMission(Mission) for i,_mission in pairs(self.missionqueue) do local mission=_mission --Ops.Auftrag#AUFTRAG - + if mission.auftragsnummer==Mission.auftragsnummer then - + -- Remove mission waypoint task. local Task=Mission:GetGroupWaypointTask(self) - + if Task then self:RemoveTask(Task) end - + -- Remove mission from queue. table.remove(self.missionqueue, i) - + return self end - + end return self @@ -2402,19 +3305,42 @@ function OPSGROUP:CountRemainingMissison() -- Loop over mission queue. for _,_mission in pairs(self.missionqueue) do local mission=_mission --Ops.Auftrag#AUFTRAG - + if mission and mission:IsNotOver() then - + -- Get group status. local status=mission:GetGroupStatus(self) - + if status~=AUFTRAG.GroupStatus.DONE and status~=AUFTRAG.GroupStatus.CANCELLED then N=N+1 end - + end end - + + return N +end + +--- Count remaining cargo transport assignments. +-- @param #OPSGROUP self +-- @return #number Number of unfinished transports in the queue. +function OPSGROUP:CountRemainingTransports() + + local N=0 + + -- Loop over mission queue. + for _,_transport in pairs(self.cargoqueue) do + local transport=_transport --Ops.OpsTransport#OPSTRANSPORT + + -- Debug info. + self:T(self.lid..string.format("Transport status=%s [%s]", transport:GetCarrierTransportStatus(self), transport:GetState())) + + -- Count not delivered (executing or scheduled) assignments. + if transport and transport:GetCarrierTransportStatus(self)==OPSTRANSPORT.Status.SCHEDULED and transport:GetState()~=OPSTRANSPORT.Status.DELIVERED then + N=N+1 + end + end + return N end @@ -2423,6 +3349,11 @@ end -- @return Ops.Auftrag#AUFTRAG Next mission or *nil*. function OPSGROUP:_GetNextMission() + -- Check if group is acting as carrier or cargo at the moment. + if self:IsPickingup() or self:IsLoading() or self:IsTransporting() or self:IsUnloading() or self:IsLoaded() then + return nil + end + -- Number of missions. local Nmissions=#self.missionqueue @@ -2438,14 +3369,14 @@ function OPSGROUP:_GetNextMission() return (taskA.prioweapondata.RangeMax then - + local d=(dist-weapondata.RangeMax)*1.1 - + -- New waypoint coord. waypointcoord=self:GetCoordinate():Translate(d, heading) - + self:T(self.lid..string.format("Out of max range = %.1f km for weapon %d", weapondata.RangeMax/1000, mission.engageWeaponType)) elseif dist0 then + self:I(self.lid..string.format("WARNING: Got current task ==> WAIT event is suspended for 30 sec!")) + Tsuspend=-30 + allowed=false + end + + -- Check for a current transport assignment. + if self.cargoTransport then + self:I(self.lid..string.format("WARNING: Got current TRANSPORT assignment ==> WAIT event is suspended for 30 sec!")) + Tsuspend=-30 + allowed=false + end + + -- Call wait again. + if Tsuspend and not allowed then + self:__Wait(Tsuspend, Duration) + end + + return allowed +end + +--- On after "Wait" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number Duration Duration in seconds how long the group will be waiting. Default `nil` (for ever). +function OPSGROUP:onafterWait(From, Event, To, Duration) + + -- Order Group to hold. + self:FullStop() + + -- Set time stamp. + self.Twaiting=timer.getAbsTime() + + -- Max waiting + self.dTwait=Duration + +end + + --- On after "PassingWaypoint" event. -- @param #OPSGROUP self -- @param #string From From state. @@ -2984,34 +3994,59 @@ end -- @param #string To To state. -- @param #OPSGROUP.Waypoint Waypoint Waypoint data passed. function OPSGROUP:onafterPassingWaypoint(From, Event, To, Waypoint) - - -- Apply tasks of this waypoint. - local ntasks=self:_SetWaypointTasks(Waypoint) - - -- Get waypoint index. - local wpindex=self:GetWaypointIndex(Waypoint.uid) - -- Final waypoint reached? - if wpindex==nil or wpindex==#self.waypoints then + -- Get the current task. + local task=self:GetTaskCurrent() - -- Set switch to true. - if not self.adinfinitum or #self.waypoints<=1 then - self.passedfinalwp=true + if task and task.dcstask.id=="PatrolZone" then + + -- Remove old waypoint. + self:RemoveWaypointByID(Waypoint.uid) + + local zone=task.dcstask.params.zone --Core.Zone#ZONE + local Coordinate=zone:GetRandomCoordinate() + local Speed=UTILS.KmphToKnots(task.dcstask.params.speed or self.speedCruise) + local Altitude=task.dcstask.params.altitude and UTILS.MetersToFeet(task.dcstask.params.altitude) or nil + + if self.isFlightgroup then + FLIGHTGROUP.AddWaypoint(self, Coordinate, Speed, AfterWaypointWithID, Altitude) + elseif self.isNavygroup then + ARMYGROUP.AddWaypoint(self, Coordinate, Speed, AfterWaypointWithID, Formation) + elseif self.isArmygroup then + NAVYGROUP.AddWaypoint(self, Coordinate, Speed, AfterWaypointWithID, Altitude) end - + + else + + -- Apply tasks of this waypoint. + local ntasks=self:_SetWaypointTasks(Waypoint) + + -- Get waypoint index. + local wpindex=self:GetWaypointIndex(Waypoint.uid) + + -- Final waypoint reached? + if wpindex==nil or wpindex==#self.waypoints then + + -- Set switch to true. + if not self.adinfinitum or #self.waypoints<=1 then + self.passedfinalwp=true + end + + end + + -- Check if all tasks/mission are done? + -- Note, we delay it for a second to let the OnAfterPassingwaypoint function to be executed in case someone wants to add another waypoint there. + if ntasks==0 then + self:_CheckGroupDone(0.1) + end + + -- Debug info. + local text=string.format("Group passed waypoint %s/%d ID=%d: final=%s detour=%s astar=%s", + tostring(wpindex), #self.waypoints, Waypoint.uid, tostring(self.passedfinalwp), tostring(Waypoint.detour), tostring(Waypoint.astar)) + self:T(self.lid..text) + end - -- Check if all tasks/mission are done? - -- Note, we delay it for a second to let the OnAfterPassingwaypoint function to be executed in case someone wants to add another waypoint there. - if ntasks==0 then - self:_CheckGroupDone(0.1) - end - - -- Debug info. - local text=string.format("Group passed waypoint %s/%d ID=%d: final=%s detour=%s astar=%s", - tostring(wpindex), #self.waypoints, Waypoint.uid, tostring(self.passedfinalwp), tostring(Waypoint.detour), tostring(Waypoint.astar)) - self:T(self.lid..text) - end --- Set tasks at this waypoint @@ -3025,37 +4060,47 @@ function OPSGROUP:_SetWaypointTasks(Waypoint) -- Debug info. local text=string.format("WP uid=%d tasks:", Waypoint.uid) + local missiontask=nil --Ops.OpsGroup#OPSGROUP.Task if #tasks>0 then for i,_task in pairs(tasks) do local task=_task --#OPSGROUP.Task text=text..string.format("\n[%d] %s", i, task.description) + if task.ismission then + missiontask=task + end end else text=text.." None" end self:T(self.lid..text) + -- Check if there is mission task + if missiontask then + self:T(self.lid.."Executing mission task") + self:TaskExecute(missiontask) + return 1 + end + + -- TODO: maybe set waypoint enroute tasks? -- Tasks at this waypoints. local taskswp={} - - -- TODO: maybe set waypoint enroute tasks? - + for _,task in pairs(tasks) do - local Task=task --Ops.OpsGroup#OPSGROUP.Task - + local Task=task --Ops.OpsGroup#OPSGROUP.Task + -- Task execute. table.insert(taskswp, self.group:TaskFunction("OPSGROUP._TaskExecute", self, Task)) -- Stop condition if userflag is set to 1 or task duration over. local TaskCondition=self.group:TaskCondition(nil, Task.stopflag:GetName(), 1, nil, Task.duration) - - -- Controlled task. + + -- Controlled task. table.insert(taskswp, self.group:TaskControlled(Task.dcstask, TaskCondition)) - + -- Task done. table.insert(taskswp, self.group:TaskFunction("OPSGROUP._TaskDone", self, Task)) - + end -- Execute waypoint tasks. @@ -3075,27 +4120,27 @@ end function OPSGROUP:onafterGotoWaypoint(From, Event, To, UID) local n=self:GetWaypointIndex(UID) - + if n then - - -- TODO: switch to re-enable waypoint tasks. + + -- TODO: Switch to re-enable waypoint tasks? if false then local tasks=self:GetTasksWaypoint(n) - + for _,_task in pairs(tasks) do local task=_task --#OPSGROUP.Task task.status=OPSGROUP.TaskStatus.SCHEDULED end - + end - + local Speed=self:GetSpeedToWaypoint(n) - + -- Update the route. self:__UpdateRoute(-1, n, Speed) - + end - + end --- On after "DetectedUnit" event. @@ -3111,7 +4156,7 @@ function OPSGROUP:onafterDetectedUnit(From, Event, To, Unit) -- Debug. self:T2(self.lid..string.format("Detected unit %s", unitname)) - + if self.detectedunits:FindUnit(unitname) then -- Unit is already in the detected unit set ==> Trigger "DetectedUnitKnown" event. self:DetectedUnitKnown(Unit) @@ -3129,10 +4174,10 @@ end -- @param #string To To state. -- @param Wrapper.Unit#UNIT Unit The detected unit. function OPSGROUP:onafterDetectedUnitNew(From, Event, To, Unit) - + -- Debug info. self:T(self.lid..string.format("Detected New unit %s", Unit:GetName())) - + -- Add unit to detected unit set. self.detectedunits:AddUnit(Unit) end @@ -3150,7 +4195,7 @@ function OPSGROUP:onafterDetectedGroup(From, Event, To, Group) -- Debug info. self:T(self.lid..string.format("Detected group %s", groupname)) - + if self.detectedgroups:FindGroup(groupname) then -- Group is already in the detected set ==> Trigger "DetectedGroupKnown" event. self:DetectedGroupKnown(Group) @@ -3158,7 +4203,7 @@ function OPSGROUP:onafterDetectedGroup(From, Event, To, Group) -- Group is was not detected ==> Trigger "DetectedGroupNew" event. self:DetectedGroupNew(Group) end - + end --- On after "DetectedGroupNew" event. Add newly detected group to detected group set. @@ -3171,7 +4216,7 @@ function OPSGROUP:onafterDetectedGroupNew(From, Event, To, Group) -- Debug info. self:T(self.lid..string.format("Detected New group %s", Group:GetName())) - + -- Add unit to detected unit set. self.detectedgroups:AddGroup(Group) end @@ -3215,51 +4260,51 @@ function OPSGROUP:onbeforeLaserOn(From, Event, To, Target) if Target then - -- Target specified ==> set target. + -- Target specified ==> set target. self:SetLaserTarget(Target) - + else -- No target specified. self:E(self.lid.."ERROR: No target provided for LASER!") return false end - + -- Get the first element alive. local element=self:GetElementAlive() - + if element then - + -- Set element. - self.spot.element=element - + self.spot.element=element + -- Height offset. No offset for aircraft. We take the height for ground or naval. local offsetY=0 if self.isGround or self.isNaval then offsetY=element.height end - + -- Local offset of the LASER source. self.spot.offset={x=0, y=offsetY, z=0} - + -- Check LOS. if self.spot.CheckLOS then - + -- Check LOS. local los=self:HasLoS(self.spot.Coordinate, self.spot.element, self.spot.offset) - + --self:I({los=los, coord=self.spot.Coordinate, offset=self.spot.offset}) if los then self:LaserGotLOS() else -- Try to switch laser on again in 10 sec. - self:I(self.lid.."LASER got no LOS currently. Trying to switch the laser on again in 10 sec") + self:T(self.lid.."LASER got no LOS currently. Trying to switch the laser on again in 10 sec") self:__LaserOn(-10, Target) return false end - + end - + else self:E(self.lid.."ERROR: No element alive for lasing") return false @@ -3289,16 +4334,16 @@ function OPSGROUP:onafterLaserOn(From, Event, To, Target) if self.spot.IRon then self.spot.IR=Spot.createInfraRed(DCSunit, self.spot.offset, self.spot.vec3) end - + -- Laser is on. self.spot.On=true - + -- No paused in case it was. self.spot.Paused=false -- Debug message. self:T(self.lid.."Switching LASER on") - + end --- On before "LaserOff" event. Check if LASER is on. @@ -3324,7 +4369,7 @@ function OPSGROUP:onafterLaserOff(From, Event, To) if self.spot.On then self.spot.Laser:destroy() self.spot.IR:destroy() - + -- Set to nil. self.spot.Laser=nil self.spot.IR=nil @@ -3332,13 +4377,13 @@ function OPSGROUP:onafterLaserOff(From, Event, To) -- Stop update timer. self.spot.timer:Stop() - + -- No target unit. self.spot.TargetUnit=nil -- Laser is off. self.spot.On=false - + -- Not paused if it was. self.spot.Paused=false end @@ -3356,17 +4401,17 @@ function OPSGROUP:onafterLaserPause(From, Event, To) -- "Destroy" the laser beam. self.spot.Laser:destroy() self.spot.IR:destroy() - + -- Set to nil. self.spot.Laser=nil self.spot.IR=nil -- Laser is off. self.spot.On=false - + -- Laser is paused. self.spot.Paused=true - + end --- On before "LaserResume" event. @@ -3387,12 +4432,12 @@ function OPSGROUP:onafterLaserResume(From, Event, To) -- Debug info. self:T(self.lid.."Resuming LASER") - + -- Unset paused. self.spot.Paused=false -- Set target. - local target=nil + local target=nil if self.spot.TargetType==0 then target=self.spot.Coordinate elseif self.spot.TargetType==1 or self.spot.TargetType==2 then @@ -3406,7 +4451,7 @@ function OPSGROUP:onafterLaserResume(From, Event, To) -- Debug message. self:T(self.lid.."Switching LASER on again") - + self:LaserOn(target) end @@ -3425,16 +4470,16 @@ function OPSGROUP:onafterLaserCode(From, Event, To, Code) -- Debug message. self:T2(self.lid..string.format("Setting LASER Code to %d", self.spot.Code)) - + if self.spot.On then - + -- Debug info. self:T(self.lid..string.format("New LASER Code is %d", self.spot.Code)) - + -- Set LASER code. self.spot.Laser:setCode(self.spot.Code) end - + end --- On after "LaserLostLOS" event. @@ -3444,23 +4489,19 @@ end -- @param #string To To state. function OPSGROUP:onafterLaserLostLOS(From, Event, To) - --env.info("FF lost LOS") - -- No of sight. self.spot.LOS=false - + -- Lost line of sight. self.spot.lostLOS=true if self.spot.On then - - --env.info("FF lost LOS ==> pause laser") -- Switch laser off. self:LaserPause() - + end - + end --- On after "LaserGotLOS" event. @@ -3472,24 +4513,19 @@ function OPSGROUP:onafterLaserGotLOS(From, Event, To) -- Has line of sight. self.spot.LOS=true - - --env.info("FF Laser Got LOS") if self.spot.lostLOS then - + -- Did not loose LOS anymore. self.spot.lostLOS=false - - --env.info("FF had lost LOS and regained it") -- Resume laser if currently paused. if self.spot.Paused then - --env.info("FF laser was paused ==> resume") self:LaserResume() end end - + end --- Set LASER target. @@ -3501,17 +4537,17 @@ function OPSGROUP:SetLaserTarget(Target) -- Check object type. if Target:IsInstanceOf("SCENERY") then - + -- Scenery as target. Treat it like a coordinate. Set offset to 1 meter above ground. self.spot.TargetType=0 self.spot.offsetTarget={x=0, y=1, z=0} - + elseif Target:IsInstanceOf("POSITIONABLE") then - + local target=Target --Wrapper.Positionable#POSITIONABLE - + if target:IsAlive() then - + if target:IsInstanceOf("GROUP") then -- We got a GROUP as target. self.spot.TargetGroup=target @@ -3526,39 +4562,37 @@ function OPSGROUP:SetLaserTarget(Target) self.spot.TargetType=2 end end - + -- Get object size. local size,x,y,z=self.spot.TargetUnit:GetObjectSize() - + if y then self.spot.offsetTarget={x=0, y=y*0.75, z=0} else self.spot.offsetTarget={x=0, 2, z=0} end - - --env.info(string.format("Target offset %.3f", y)) - + else self:E("WARNING: LASER target is not alive!") return end - + elseif Target:IsInstanceOf("COORDINATE") then - + -- Coordinate as target. self.spot.TargetType=0 self.spot.offsetTarget={x=0, y=0, z=0} - + else self:E(self.lid.."ERROR: LASER target should be a POSITIONABLE (GROUP, UNIT or STATIC) or a COORDINATE object!") return end - + -- Set vec3 and account for target offset. self.spot.vec3=UTILS.VecAdd(Target:GetVec3(), self.spot.offsetTarget) - + -- Set coordinate. - self.spot.Coordinate:UpdateFromVec3(self.spot.vec3) + self.spot.Coordinate:UpdateFromVec3(self.spot.vec3) end end @@ -3569,31 +4603,31 @@ function OPSGROUP:_UpdateLaser() -- Check if we have a POSITIONABLE to lase. if self.spot.TargetUnit then - + --- -- Lasing a possibly moving target --- - + if self.spot.TargetUnit:IsAlive() then - -- Get current target position. + -- Get current target position. local vec3=self.spot.TargetUnit:GetVec3() - + -- Add target offset. vec3=UTILS.VecAdd(vec3, self.spot.offsetTarget) - - -- Calculate distance + + -- Calculate distance local dist=UTILS.VecDist3D(vec3, self.spot.vec3) -- Store current position. self.spot.vec3=vec3 - + -- Update beam coordinate. self.spot.Coordinate:UpdateFromVec3(vec3) - + -- Update laser if target moved more than one meter. if dist>1 then - + -- If the laser is ON, set the new laser target point. if self.spot.On then self.spot.Laser:setPoint(vec3) @@ -3601,16 +4635,16 @@ function OPSGROUP:_UpdateLaser() self.spot.IR:setPoint(vec3) end end - + end - + else - + if self.spot.TargetGroup and self.spot.TargetGroup:IsAlive() then - + -- Get first alive unit in the group. local unit=self.spot.TargetGroup:GetHighestThreat() - + if unit then self:T(self.lid..string.format("Switching to target unit %s in the group", unit:GetName())) self.spot.TargetUnit=unit @@ -3620,47 +4654,58 @@ function OPSGROUP:_UpdateLaser() -- Switch laser off. self:T(self.lid.."Target is not alive any more ==> switching LASER off") self:LaserOff() - return + return end - + else - + -- Switch laser off. self:T(self.lid.."Target is not alive any more ==> switching LASER off") self:LaserOff() return end - - end + + end end - + -- Check LOS. if self.spot.CheckLOS then - + -- Check current LOS. local los=self:HasLoS(self.spot.Coordinate, self.spot.element, self.spot.offset) - - --env.info(string.format("FF check LOS current=%s previous=%s", tostring(los), tostring(self.spot.LOS))) - - if los then - -- Got LOS + + if los then + -- Got LOS if self.spot.lostLOS then --self:I({los=self.spot.LOS, coord=self.spot.Coordinate, offset=self.spot.offset}) self:LaserGotLOS() end - - else - -- No LOS currently + + else + -- No LOS currently if not self.spot.lostLOS then self:LaserLostLOS() - end - + end + end - + end - + end +--- On after "ElementInUtero" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP.Element Element The flight group element. +function OPSGROUP:onafterElementInUtero(From, Event, To, Element) + self:T(self.lid..string.format("Element in utero %s", Element.name)) + + -- Set element status. + self:_UpdateStatus(Element, OPSGROUP.ElementStatus.INUTERO) + +end --- On after "ElementDestroyed" event. -- @param #OPSGROUP self @@ -3670,7 +4715,7 @@ end -- @param #OPSGROUP.Element Element The flight group element. function OPSGROUP:onafterElementDestroyed(From, Event, To, Element) self:T(self.lid..string.format("Element destroyed %s", Element.name)) - + -- Cancel all missions. for _,_mission in pairs(self.missionqueue) do local mission=_mission --Ops.Auftrag#AUFTRAG @@ -3678,13 +4723,13 @@ function OPSGROUP:onafterElementDestroyed(From, Event, To, Element) mission:ElementDestroyed(self, Element) end - + -- Increase counter. self.Ndestroyed=self.Ndestroyed+1 - -- Set element status. - self:_UpdateStatus(Element, OPSGROUP.ElementStatus.DEAD) - + -- Element is dead. + self:ElementDead(Element) + end --- On after "ElementDead" event. @@ -3694,23 +4739,25 @@ end -- @param #string To To state. -- @param #OPSGROUP.Element Element The flight group element. function OPSGROUP:onafterElementDead(From, Event, To, Element) + + -- Debug info. self:T(self.lid..string.format("Element dead %s at t=%.3f", Element.name, timer.getTime())) - + -- Set element status. self:_UpdateStatus(Element, OPSGROUP.ElementStatus.DEAD) - + -- Check if element was lasing and if so, switch to another unit alive to lase. if self.spot.On and self.spot.element.name==Element.name then - + -- Switch laser off. self:LaserOff() - - -- If there is another element alive, switch laser on again. + + -- If there is another element alive, switch laser on again. if self:GetNelements()>0 then - + -- New target if any. local target=nil - + if self.spot.TargetType==0 then -- Coordinate target=self.spot.Coordinate @@ -3718,21 +4765,209 @@ function OPSGROUP:onafterElementDead(From, Event, To, Element) -- Static or unit if self.spot.TargetUnit and self.spot.TargetUnit:IsAlive() then target=self.spot.TargetUnit - end + end elseif self.spot.TargetType==3 then -- Group if self.spot.TargetGroup and self.spot.TargetGroup:IsAlive() then target=self.spot.TargetGroup - end + end end - + -- Switch laser on again. if target then self:__LaserOn(-1, target) end end end + + + -- Clear cargo bay of element. + for i=#Element.cargoBay,1,-1 do + local cargo=Element.cargoBay[i] --#OPSGROUP.MyCargo + -- Remove from cargo bay. + self:_DelCargobay(cargo.group) + + if cargo.group and not (cargo.group:IsDead() or cargo.group:IsStopped()) then + + -- Remove my carrier + cargo.group:_RemoveMyCarrier() + + if cargo.reserved then + + -- This group was not loaded yet ==> Not cargo any more. + cargo.group:_NewCargoStatus(OPSGROUP.CargoStatus.NOTCARGO) + + else + + -- Carrier dead ==> cargo dead. + for _,cargoelement in pairs(cargo.group.elements) do + + -- Debug info. + self:T2(self.lid.."Cargo element dead "..cargoelement.name) + + -- Trigger dead event. + cargo.group:ElementDead(cargoelement) + + end + end + + end + end + +end + +--- On after "Respawn" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #table Template The template used to respawn the group. Default is the inital template of the group. +function OPSGROUP:onafterRespawn(From, Event, To, Template) + + -- Debug info. + self:I(self.lid.."Respawning group!") + + -- Copy template. + local template=UTILS.DeepCopy(Template or self.template) + + -- Late activation off. + template.lateActivation=false + + self:_Respawn(0, template) + +end + +--- Respawn the group. +-- @param #OPSGROUP self +-- @param #number Delay Delay in seconds before respawn happens. Default 0. +-- @param DCS#Template Template (optional) The template of the Group retrieved with GROUP:GetTemplate(). If the template is not provided, the template will be retrieved of the group itself. +-- @param #boolean Reset Reset waypoints and reinit group if `true`. +-- @return #OPSGROUP self +function OPSGROUP:_Respawn(Delay, Template, Reset) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP._Respawn, self, 0, Template, Reset) + else + + -- Debug message. + self:T2(self.lid.."FF _Respawn") + + -- Given template or get old. + Template=Template or self:_GetTemplate(true) + + if self:IsAlive() then + + --- + -- Group is ALIVE + --- + + --[[ + + -- Get units. + local units=self.group:GetUnits() + + -- Loop over template units. + for UnitID, Unit in pairs(Template.units) do + + for _,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + + if unit:GetName()==Unit.name then + local vec3=unit:GetVec3() + local heading=unit:GetHeading() + Unit.x=vec3.x + Unit.y=vec3.z + Unit.alt=vec3.y + Unit.heading=math.rad(heading) + Unit.psi=-Unit.heading + end + end + + end + + ]] + + local units=Template.units + + for i=#units,1,-1 do + local unit=units[i] + local element=self:GetElementByName(unit.name) + if element and element.status~=OPSGROUP.ElementStatus.DEAD then + unit.parking=element.parking and element.parking.TerminalID or unit.parking + unit.parking_id=nil + local vec3=element.unit:GetVec3() + local heading=element.unit:GetHeading() + unit.x=vec3.x + unit.y=vec3.z + unit.alt=vec3.y + unit.heading=math.rad(heading) + unit.psi=-unit.heading + else + table.remove(units, i) + end + end + + + -- Despawn old group. Dont trigger any remove unit event since this is a respawn. + self:Despawn(0, true) + + else + + --- + -- Group is DESPAWNED + --- + + -- Ensure elements in utero. + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + self:ElementInUtero(element) + end + + end + + -- Debug output. + self:T({Template=Template}) + + -- Spawn new group. + _DATABASE:Spawn(Template) + + -- Set activation and controlled state. + self.isLateActivated=Template.lateActivation + self.isUncontrolled=Template.uncontrolled + + -- Not dead or destroyed any more. + self.isDead=false + self.isDestroyed=false + + + self.groupinitialized=false + self.Ndestroyed=0 + self.wpcounter=1 + self.currentwp=1 + + -- Init waypoints. + self:_InitWaypoints() + + -- Init Group. + self:_InitGroup() + + -- Reset events. + --self:ResetEvents() + + end + + return self +end + +--- On after "Destroyed" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterDestroyed(From, Event, To) + self:T(self.lid..string.format("Group destroyed at t=%.3f", timer.getTime())) + self.isDestroyed=true end --- On before "Dead" event. @@ -3752,11 +4987,12 @@ end -- @param #string Event Event. -- @param #string To To state. function OPSGROUP:onafterDead(From, Event, To) - self:T(self.lid..string.format("Group dead at t=%.3f", timer.getTime())) - -- Delete waypoints so they are re-initialized at the next spawn. - self.waypoints=nil - self.groupinitialized=false + -- Debug info. + self:I(self.lid..string.format("Group dead at t=%.3f", timer.getTime())) + + -- Is dead now. + self.isDead=true -- Cancel all missions. for _,_mission in pairs(self.missionqueue) do @@ -3768,9 +5004,53 @@ function OPSGROUP:onafterDead(From, Event, To) mission:GroupDead(self) end + + -- Delete waypoints so they are re-initialized at the next spawn. + self:ClearWaypoints() + self.groupinitialized=false + + -- Set cargo status to NOTCARGO. + self.cargoStatus=OPSGROUP.CargoStatus.NOTCARGO + self.carrierStatus=OPSGROUP.CarrierStatus.NOTCARRIER + -- Remove from cargo bay of carrier. + local mycarrier=self:_GetMyCarrierGroup() + if mycarrier and not mycarrier:IsDead() then + mycarrier:_DelCargobay(self) + self:_RemoveMyCarrier() + end + + -- Inform all transports in the queue that this carrier group is dead now. + for i,_transport in pairs(self.cargoqueue) do + local transport=_transport --Ops.OpsTransport#OPSTRANSPORT + transport:__DeadCarrierGroup(1, self) + end + + -- Cargo queue empty + self.cargoqueue={} + + -- No current cargo transport. + self.cargoTransport=nil + + -- Stop in a sec. - self:__Stop(-5) + --self:__Stop(-5) +end + +--- On before "Stop" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onbeforeStop(From, Event, To) + + -- We check if + if self:IsAlive() then + self:E(self.lid..string.format("WARNING: Group is still alive! Will not stop the FSM. Use :Despawn() instead")) + return false + end + + return true end --- On after "Stop" event. @@ -3779,14 +5059,31 @@ end -- @param #string Event Event. -- @param #string To To state. function OPSGROUP:onafterStop(From, Event, To) - + + -- Handle events: + self:UnHandleEvent(EVENTS.Birth) + self:UnHandleEvent(EVENTS.Dead) + self:UnHandleEvent(EVENTS.RemoveUnit) + + -- Handle events: + if self.isFlightgroup then + self:UnHandleEvent(EVENTS.EngineStartup) + self:UnHandleEvent(EVENTS.Takeoff) + self:UnHandleEvent(EVENTS.Land) + self:UnHandleEvent(EVENTS.EngineShutdown) + self:UnHandleEvent(EVENTS.PilotDead) + self:UnHandleEvent(EVENTS.Ejection) + self:UnHandleEvent(EVENTS.Crash) + self.currbase=nil + end + -- Stop check timers. self.timerCheckZone:Stop() self.timerQueueUpdate:Stop() -- Stop FSM scheduler. self.CallScheduler:Clear() - + if self:IsAlive() and not (self:IsDead() or self:IsStopped()) then local life, life0=self:GetLifePoints() local state=self:GetState() @@ -3794,8 +5091,1788 @@ function OPSGROUP:onafterStop(From, Event, To) self:E(self.lid..text) end + -- Remove flight from data base. + _DATABASE.FLIGHTGROUPS[self.groupname]=nil + -- Debug output. - self:I(self.lid.."STOPPED! Unhandled events, cleared scheduler and removed from database.") + self:I(self.lid.."STOPPED! Unhandled events, cleared scheduler and removed from _DATABASE") +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Cargo Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check cargo transport assignments. +-- @param #OPSGROUP self +-- @return #OPSGROUP self +function OPSGROUP:_CheckCargoTransport() + + -- Abs. missin time in seconds. + local Time=timer.getAbsTime() + + -- Cargo bay debug info. + -- Check cargo bay and declare cargo groups dead. + if self.verbose>=1 then + local text="" + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + for _,_cargo in pairs(element.cargoBay) do + local cargo=_cargo --#OPSGROUP.MyCargo + text=text..string.format("\n- %s in carrier %s, reserved=%s", tostring(cargo.group:GetName()), tostring(element.name), tostring(cargo.reserved)) + end + end + if text=="" then + text=" empty" + end + self:I(self.lid.."Cargo bay:"..text) + end + + -- Cargo queue debug info. + if self.verbose>=3 then + local text="" + for i,_transport in pairs(self.cargoqueue) do + local transport=_transport --#Ops.OpsTransport#OPSTRANSPORT + text=text..string.format("\n[%d] UID=%d Status=%s: %s --> %s", i, transport.uid, transport:GetState(), transport.pickupzone:GetName(), transport.deployzone:GetName()) + for j,_cargo in pairs(transport.cargos) do + local cargo=_cargo --#OPSGROUP.CargoGroup + local state=cargo.opsgroup:GetState() + local status=cargo.opsgroup.cargoStatus + local name=cargo.opsgroup.groupname + local carriergroup, carrierelement, reserved=cargo.opsgroup:_GetMyCarrier() + local carrierGroupname=carriergroup and carriergroup.groupname or "none" + local carrierElementname=carrierelement and carrierelement.name or "none" + text=text..string.format("\n (%d) %s [%s]: %s, carrier=%s(%s), delivered=%s", j, name, state, status, carrierGroupname, carrierElementname, tostring(cargo.delivered)) + end + end + if text~="" then + self:I(self.lid.."Cargo queue:"..text) + end + end + + -- Check if there is anything in the queue. + if not self.cargoTransport then + self.cargoTransport=self:_GetNextCargoTransport() + if self.cargoTransport and not self:IsActive() then + self:Activate() + end + end + + -- Now handle the transport. + if self.cargoTransport then + + -- Debug info. + if self.verbose>=2 then + local text=string.format("Carrier [%s]: %s --> %s", self.carrierStatus, self.cargoTransport.pickupzone:GetName(), self.cargoTransport.deployzone:GetName()) + for _,_cargo in pairs(self.cargoTransport.cargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + local name=cargo.opsgroup:GetName() + local gstatus=cargo.opsgroup:GetState() + local cstatus=cargo.opsgroup.cargoStatus + local weight=cargo.opsgroup:GetWeightTotal() + local carriergroup, carrierelement, reserved=cargo.opsgroup:_GetMyCarrier() + local carrierGroupname=carriergroup and carriergroup.groupname or "none" + local carrierElementname=carrierelement and carrierelement.name or "none" + text=text..string.format("\n- %s (%.1f kg) [%s]: %s, carrier=%s (%s), delivered=%s", name, weight, gstatus, cstatus, carrierElementname, carrierGroupname, tostring(cargo.delivered)) + end + self:I(self.lid..text) + end + + + if self:IsNotCarrier() then + + -- Debug info. + self:T(self.lid.."Not carrier ==> pickup") + + -- Initiate the cargo transport process. + self:__Pickup(-1) + + elseif self:IsPickingup() then + + -- Debug Info. + self:T(self.lid.."Picking up...") + + elseif self:IsLoading() then + + -- Set loading time stamp. + --TODO: Check max loading time. If exceeded ==> abort transport. + self.Tloading=self.Tloading or Time + + -- Debug info. + self:T(self.lid.."Loading...") + + local boarding=false + local gotcargo=false + for _,_cargo in pairs(self.cargoTransport.cargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + + -- Check if anyone is still boarding. + if cargo.opsgroup:IsBoarding(self.groupname) then + boarding=true + end + + -- Check if we have any cargo to transport. + if cargo.opsgroup:IsLoaded(self.groupname) then + gotcargo=true + end + + end + + -- Boarding finished ==> Transport cargo. + if gotcargo and self.cargoTransport:_CheckRequiredCargos() and not boarding then + self:T(self.lid.."Boarding finished ==> Loaded") + self:Loaded() + else + -- No cargo and no one is boarding ==> check again if we can make anyone board. + self:Loading() + end + + -- No cargo and no one is boarding ==> check again if we can make anyone board. + if not gotcargo and not boarding then + --self:Loading() + end + + elseif self:IsTransporting() then + + -- Debug info. + self:T(self.lid.."Transporting (nothing to do)") + + elseif self:IsUnloading() then + + -- Debug info. + self:T(self.lid.."Unloading ==> Checking if all cargo was delivered") + + local delivered=true + for _,_cargo in pairs(self.cargoTransport.cargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + + local carrierGroup=cargo.opsgroup:_GetMyCarrierGroup() + + -- Check that this group is + if (carrierGroup and carrierGroup:GetName()==self:GetName()) and not cargo.delivered then + delivered=false + break + end + + end + + -- Unloading finished ==> pickup next batch or call it a day. + if delivered then + self:T(self.lid.."Unloading finished ==> UnloadingDone") + self:UnloadingDone() + else + self:Unloading() + end + + end + + end + + return self +end + + +--- Check if a group is in the cargo bay. +-- @param #OPSGROUP self +-- @param #OPSGROUP OpsGroup Group to check. +-- @return #boolean If `true`, group is in the cargo bay. +function OPSGROUP:_IsInCargobay(OpsGroup) + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + for _,_cargo in pairs(element.cargoBay) do + local cargo=_cargo --#OPSGROUP.MyCargo + if cargo.group.groupname==OpsGroup.groupname then + return true + end + end + end + + return false +end + +--- Add OPSGROUP to cargo bay of a carrier. +-- @param #OPSGROUP self +-- @param #OPSGROUP CargoGroup Cargo group. +-- @param #OPSGROUP.Element CarrierElement The element of the carrier. +-- @param #boolean Reserved Only reserve the cargo bay space. +function OPSGROUP:_AddCargobay(CargoGroup, CarrierElement, Reserved) + + --TODO: Check group is not already in cargobay of this carrier or any other carrier. + + local cargo=self:_GetCargobay(CargoGroup) + + if cargo then + cargo.reserved=Reserved + else + + cargo={} --#OPSGROUP.MyCargo + cargo.group=CargoGroup + cargo.reserved=Reserved + + table.insert(CarrierElement.cargoBay, cargo) + end + + + -- Set my carrier. + CargoGroup:_SetMyCarrier(self, CarrierElement, Reserved) + + -- Fill cargo bay (obsolete). + self.cargoBay[CargoGroup.groupname]=CarrierElement.name + + if not Reserved then + + -- Cargo weight. + local weight=CargoGroup:GetWeightTotal() + + -- Add weight to carrier. + self:AddWeightCargo(CarrierElement.name, weight) + + end + + return self +end + +--- Get cargo bay item. +-- @param #OPSGROUP self +-- @param #OPSGROUP CargoGroup Cargo group. +-- @return #OPSGROUP.MyCargo Cargo bay item or `nil` if the group is not in the carrier. +-- @return #number CargoBayIndex Index of item in the cargo bay table. +-- @return #OPSGROUP.Element Carrier element. +function OPSGROUP:_GetCargobay(CargoGroup) + + -- Loop over elements and their cargo bay items. + local CarrierElement=nil --#OPSGROUP.Element + local cargobayIndex=nil + local reserved=nil + for i,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + for j,_cargo in pairs(element.cargoBay) do + local cargo=_cargo --#OPSGROUP.MyCargo + if cargo.group and cargo.group.groupname==CargoGroup.groupname then + return cargo, j, element + end + end + end + + return nil, nil, nil +end + +--- Remove OPSGROUP from cargo bay of a carrier. +-- @param #OPSGROUP self +-- @param #OPSGROUP CargoGroup Cargo group. +-- @return #boolean If `true`, cargo could be removed. +function OPSGROUP:_DelCargobay(CargoGroup) + + if self.cargoBay[CargoGroup.groupname] then + + -- Not in cargo bay any more. + self.cargoBay[CargoGroup.groupname]=nil + + end + + -- Get cargo bay info. + local cargoBayItem, cargoBayIndex, CarrierElement=self:_GetCargobay(CargoGroup) + + if cargoBayItem and cargoBayIndex then + + -- Debug info. + self:T(self.lid..string.format("Removing cargo group %s from cargo bay (index=%d) of carrier %s", CargoGroup:GetName(), cargoBayIndex, CarrierElement.name)) + + -- Remove + table.remove(CarrierElement.cargoBay, cargoBayIndex) + + -- Reduce weight (if cargo space was not just reserved). + if not cargoBayItem.reserved then + local weight=CargoGroup:GetWeightTotal() + self:RedWeightCargo(CarrierElement.name, weight) + end + + return true + end + + self:E(self.lid.."ERROR: Group is not in cargo bay. Cannot remove it!") + return false +end + +--- Get cargo transport from cargo queue. +-- @param #OPSGROUP self +-- @return Ops.OpsTransport#OPSTRANSPORT The next due cargo transport or `nil`. +function OPSGROUP:_GetNextCargoTransport() + + -- Current position. + local coord=self:GetCoordinate() + + -- Sort results table wrt prio and distance to pickup zone. + local function _sort(a, b) + local transportA=a --Ops.OpsTransport#OPSTRANSPORT + local transportB=b --Ops.OpsTransport#OPSTRANSPORT + local distA=transportA.pickupzone:GetCoordinate():Get2DDistance(coord) + local distB=transportB.pickupzone:GetCoordinate():Get2DDistance(coord) + return (transportA.priomaxweight then + maxweight=weight + end + + end + end + + return maxweight +end + + +--- Get weight of the internal cargo the group is carriing right now. +-- @param #OPSGROUP self +-- @param #string UnitName Name of the unit. Default is of the whole group. +-- @param #boolean IncludeReserved If `false`, cargo weight that is only *reserved* is **not** counted. By default (`true` or `nil`), the reserved cargo is included. +-- @return #number Cargo weight in kg. +function OPSGROUP:GetWeightCargo(UnitName, IncludeReserved) + + -- Calculate weight based on actual cargo weight. + local weight=0 + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + + if (UnitName==nil or UnitName==element.name) and element.status~=OPSGROUP.ElementStatus.DEAD then + + weight=weight+element.weightCargo + + end + + end + + -- Calculate weight from stuff in cargo bay. By default this includes the reserved weight if a cargo group was assigned and is currently boarding. + local gewicht=0 + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + if (UnitName==nil or UnitName==element.name) and (element and element.status~=OPSGROUP.ElementStatus.DEAD) then + for _,_cargo in pairs(element.cargoBay) do + local cargo=_cargo --#OPSGROUP.MyCargo + if (not cargo.reserved) or (cargo.reserved==true and (IncludeReserved==true or IncludeReserved==nil)) then + local cargoweight=cargo.group:GetWeightTotal() + gewicht=gewicht+cargoweight + --self:I(self.lid..string.format("unit=%s (reserved=%s): cargo=%s weight=%d, total weight=%d", tostring(UnitName), tostring(IncludeReserved), cargo.group:GetName(), cargoweight, weight)) + end + end + end + end + + -- Debug info. + self:T2(self.lid..string.format("Unit=%s (reserved=%s): weight=%d, gewicht=%d", tostring(UnitName), tostring(IncludeReserved), weight, gewicht)) + + -- Quick check. + if IncludeReserved==false and gewicht~=weight then + self:E(self.lid..string.format("ERROR: FF weight!=gewicht: weight=%.1f, gewicht=%.1f", weight, gewicht)) + end + + return gewicht +end + +--- Get max weight of the internal cargo the group can carry. Optionally, the max cargo weight of a specific unit can be requested. +-- @param #OPSGROUP self +-- @param #string UnitName Name of the unit. Default is of the whole group. +-- @return #number Max cargo weight in kg. This does **not** include any cargo loaded or reserved currently. +function OPSGROUP:GetWeightCargoMax(UnitName) + + local weight=0 + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + + if (UnitName==nil or UnitName==element.name) and element.status~=OPSGROUP.ElementStatus.DEAD then + + weight=weight+element.weightMaxCargo + + end + + end + + return weight +end + +--- Get OPSGROUPs in the cargo bay. +-- @param #OPSGROUP self +-- @return #table Cargo OPSGROUPs. +function OPSGROUP:GetCargoOpsGroups() + + local opsgroups={} + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + for _,_cargo in pairs(element.cargoBay) do + local cargo=_cargo --#OPSGROUP.MyCargo + table.insert(opsgroups, cargo.group) + end + end + + return opsgroups +end + +--- Add weight to the internal cargo of an element of the group. +-- @param #OPSGROUP self +-- @param #string UnitName Name of the unit. Default is of the whole group. +-- @param #number Weight Cargo weight to be added in kg. +function OPSGROUP:AddWeightCargo(UnitName, Weight) + + local element=self:GetElementByName(UnitName) + + if element then --we do not check if the element is actually alive because we need to remove cargo from dead units + + -- Add weight. + element.weightCargo=element.weightCargo+Weight + + -- Debug info. + self:T(self.lid..string.format("%s: Adding %.1f kg cargo weight. New cargo weight=%.1f kg", UnitName, Weight, element.weightCargo)) + + -- For airborne units, we set the weight in game. + if self.isFlightgroup then + trigger.action.setUnitInternalCargo(element.name, element.weightCargo) --https://wiki.hoggitworld.com/view/DCS_func_setUnitInternalCargo + end + + end + + return self +end + +--- Reduce weight to the internal cargo of an element of the group. +-- @param #OPSGROUP self +-- @param #string UnitName Name of the unit. +-- @param #number Weight Cargo weight to be reduced in kg. +function OPSGROUP:RedWeightCargo(UnitName, Weight) + + -- Reduce weight by adding negative weight. + self:AddWeightCargo(UnitName, -Weight) + + return self +end + +--- Check if the group can *in principle* be carrier of a cargo group. This checks the max cargo capacity of the group but *not* how much cargo is already loaded (if any). +-- **Note** that the cargo group *cannot* be split into units, i.e. the largest cargo bay of any element of the group must be able to load the whole cargo group in one piece. +-- @param #OPSGROUP self +-- @param #OPSGROUP CargoGroup Cargo group, which needs a carrier. +-- @return #boolean If `true`, there is an element of the group that can load the whole cargo group. +function OPSGROUP:CanCargo(CargoGroup) + + if CargoGroup then + + local weight=CargoGroup:GetWeightTotal() + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + + -- Check that element is not dead and has + if element and element.status~=OPSGROUP.ElementStatus.DEAD and element.weightMaxCargo>=weight then + return true + end + + end + + end + + return false +end + +--- Add weight to the internal cargo of an element of the group. +-- @param #OPSGROUP self +-- @param #OPSGROUP CargoGroup Cargo group, which needs a carrier. +-- @return #OPSGROUP.Element Carrier able to transport the cargo. +function OPSGROUP:FindCarrierForCargo(CargoGroup) + + local weight=CargoGroup:GetWeightTotal() + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + + local free=self:GetFreeCargobay(element.name) + + if free>=weight then + return element + else + self:T3(self.lid..string.format("%s: Weight %d>%d free cargo bay", element.name, weight, free)) + end + + end + + return nil +end + +--- Set my carrier. +-- @param #OPSGROUP self +-- @param #OPSGROUP CarrierGroup Carrier group. +-- @param #OPSGROUP.Element CarrierElement Carrier element. +-- @param #boolean Reserved If `true`, reserve space for me. +function OPSGROUP:_SetMyCarrier(CarrierGroup, CarrierElement, Reserved) + + -- Debug info. + self:T(self.lid..string.format("Setting My Carrier: %s (%s), reserved=%s", CarrierGroup:GetName(), tostring(CarrierElement.name), tostring(Reserved))) + + self.mycarrier.group=CarrierGroup + self.mycarrier.element=CarrierElement + self.mycarrier.reserved=Reserved + + self.cargoTransportUID=CarrierGroup.cargoTransport and CarrierGroup.cargoTransport.uid or nil + +end + +--- Get my carrier group. +-- @param #OPSGROUP self +-- @return #OPSGROUP Carrier group. +function OPSGROUP:_GetMyCarrierGroup() + if self.mycarrier and self.mycarrier.group then + return self.mycarrier.group + end + return nil +end + +--- Get my carrier element. +-- @param #OPSGROUP self +-- @return #OPSGROUP.Element Carrier element. +function OPSGROUP:_GetMyCarrierElement() + if self.mycarrier and self.mycarrier.element then + return self.mycarrier.element + end + return nil +end + +--- Is my carrier reserved. +-- @param #OPSGROUP self +-- @return #boolean If `true`, space for me was reserved. +function OPSGROUP:_IsMyCarrierReserved() + if self.mycarrier then + return self.mycarrier.reserved + end + return nil +end + + + +--- Get my carrier. +-- @param #OPSGROUP self +-- @return #OPSGROUP Carrier group. +-- @return #OPSGROUP.Element Carrier element. +-- @return #boolean If `true`, space is reserved for me +function OPSGROUP:_GetMyCarrier() + return self.mycarrier.group, self.mycarrier.element, self.mycarrier.reserved +end + + +--- Remove my carrier. +-- @param #OPSGROUP self +-- @return #OPSGROUP self +function OPSGROUP:_RemoveMyCarrier() + self:T(self.lid..string.format("Removing my carrier!")) + self.mycarrier.group=nil + self.mycarrier.element=nil + self.mycarrier.reserved=nil + self.mycarrier={} + self.cargoTransportUID=nil + return self +end + +--- On after "Pickup" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterPickup(From, Event, To) + + -- Set carrier status. + self:_NewCarrierStatus(OPSGROUP.CarrierStatus.PICKUP) + + -- Pickup zone. + local Zone=self.cargoTransport.pickupzone + + -- Check if already in the pickup zone. + local inzone=Zone:IsCoordinateInZone(self:GetCoordinate()) + + local airbasePickup=nil --Wrapper.Airbase#AIRBASE + if Zone and Zone:IsInstanceOf("ZONE_AIRBASE") then + airbasePickup=Zone:GetAirbase() + end + + -- Check if group is already ready for loading. + local ready4loading=false + if self:IsArmygroup() or self:IsNavygroup() then + ready4loading=inzone + else + + -- Aircraft is already parking at the pickup airbase. + ready4loading=self.currbase and self.currbase:GetName()==Zone:GetName() and self:IsParking() + + -- If a helo is landed in the zone, we also are ready for loading. + if ready4loading==false and self.isHelo and self:IsLandedAt() and inzone then + ready4loading=true + end + end + + if ready4loading then + + -- We are already in the pickup zone ==> wait and initiate loading. + if (self:IsArmygroup() or self:IsNavygroup()) and not self:IsHolding() then + self:FullStop() + end + + -- Start loading. + self:__Loading(-5) + + else + + -- Get a random coordinate in the pickup zone and let the carrier go there. + local Coordinate=Zone:GetRandomCoordinate() + + -- Add waypoint. + if self.isFlightgroup then + + --- + -- Flight Group + --- + + if airbasePickup then + + --- + -- Pickup at airbase + --- + + -- Current airbase. + local airbaseCurrent=self.currbase + + if airbaseCurrent then + + -- Activate uncontrolled group. + if self:IsParking() then + self:StartUncontrolled() + end + + else + + -- Order group to land at an airbase. + self:LandAtAirbase(airbasePickup) + + end + + elseif self.isHelo then + + --- + -- Helo can also land in a zone (NOTE: currently VTOL cannot!) + --- + + -- Activate uncontrolled group. + if self:IsParking() then + self:StartUncontrolled() + end + + -- If this is a helo and no ZONE_AIRBASE was given, we make the helo land in the pickup zone. + Coordinate:SetAltitude(200) + local waypoint=FLIGHTGROUP.AddWaypoint(self, Coordinate) ; waypoint.detour=1 + + else + self:E(self.lid.."ERROR: Carrier aircraft cannot land in Pickup zone! Specify a ZONE_AIRBASE as pickup zone") + end + + elseif self.isNavygroup then + + --- + -- Navy Group + --- + + local cwp=self:GetWaypointCurrent() + local uid=cwp and cwp.uid or nil + + -- Get a (random) pre-defined transport path. + local path=self.cargoTransport:_GetPathPickup() + + if path then + -- Loop over coordinates. + for i,coordinate in pairs(path) do + local waypoint=NAVYGROUP.AddWaypoint(self, coordinate, nil, uid) ; waypoint.temp=true + uid=waypoint.uid + --coordinate:MarkToAll(string.format("Path i=%d, UID=%d", i, uid)) + end + end + + -- NAVYGROUP + local waypoint=NAVYGROUP.AddWaypoint(self, Coordinate, nil, uid) ; waypoint.detour=1 + + -- Give cruise command. + self:__Cruise(-2) + + + elseif self.isArmygroup then + + --- + -- Army Group + --- + + local cwp=self:GetWaypointCurrent() + local uid=cwp and cwp.uid or nil + + -- Get a (random) pre-defined transport path. + local path=self.cargoTransport:_GetPathPickup() + + if path then + -- Loop over coordinates. + for i,coordinate in pairs(path) do + local waypoint=ARMYGROUP.AddWaypoint(self, coordinate, nil, uid) ; waypoint.temp=true + uid=waypoint.uid + --coordinate:MarkToAll(string.format("Path i=%d, UID=%d", i, uid)) + end + end + + -- ARMYGROUP + local waypoint=ARMYGROUP.AddWaypoint(self, Coordinate, nil, uid) ; waypoint.detour=1 + + self:__Cruise(-2) + + end + + end + +end + + +--- On after "Loading" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterLoading(From, Event, To) + + -- Set carrier status. + self:_NewCarrierStatus(OPSGROUP.CarrierStatus.LOADING) + + -- Loading time stamp. + self.Tloading=timer.getAbsTime() + + --TODO: sort cargos wrt weight. + + -- Loop over all cargos. + for _,_cargo in pairs(self.cargoTransport.cargos) do + local cargo=_cargo --#OPSGROUP.CargoGroup + + -- Check that cargo weight is + if self:CanCargo(cargo.opsgroup) and not (cargo.delivered or cargo.opsgroup:IsDead()) then + + -- Check that group is NOT cargo and NOT acting as carrier already + -- TODO: Need a better :IsBusy() function or :IsReadyForMission() :IsReadyForBoarding() :IsReadyForTransport() + if cargo.opsgroup:IsNotCargo(true) and not (cargo.opsgroup:IsPickingup() or cargo.opsgroup:IsLoading() or cargo.opsgroup:IsTransporting() or cargo.opsgroup:IsUnloading()) then + + -- Check if cargo is in embark/pickup zone. + local inzone=self.cargoTransport.embarkzone:IsCoordinateInZone(cargo.opsgroup:GetCoordinate()) + + -- Cargo MUST be inside zone or it will not be loaded! + if inzone then + + -- Find a carrier for this cargo. + local carrier=self:FindCarrierForCargo(cargo.opsgroup) + + if carrier then + + -- Set cargo status. + cargo.opsgroup:_NewCargoStatus(OPSGROUP.CargoStatus.ASSIGNED) + + -- Order cargo group to board the carrier. + cargo.opsgroup:Board(self, carrier) + + else + -- Debug info. + self:T(self.lid..string.format("Cannot board carrier! Group %s is NOT (yet) in zone %s", cargo.opsgroup:GetName(), self.cargoTransport.embarkzone:GetName())) + end + + else + -- Debug info. + self:T(self.lid..string.format("Cargo %s NOT in embark zone %s", cargo.opsgroup:GetName(), self.cargoTransport.embarkzone:GetName())) + end + + end + + else + -- Debug info. + self:T3(self.lid.."Cargo already delivered, is dead or carrier cannot") + end + + end + +end + +--- Clear waypoints. +-- @param #OPSGROUP self +-- @param #number IndexMin Clear waypoints up to this min WP index. Default 1. +-- @param #number IndexMax Clear waypoints up to this max WP index. Default `#self.waypoints`. +function OPSGROUP:ClearWaypoints(IndexMin, IndexMax) + + IndexMin=IndexMin or 1 + IndexMax=IndexMax or #self.waypoints + + -- Clear all waypoints. + for i=IndexMax,IndexMin,-1 do + table.remove(self.waypoints, i) + end + --self.waypoints={} +end + +--- Set (new) cargo status. +-- @param #OPSGROUP self +-- @param #string Status New status. +function OPSGROUP:_NewCargoStatus(Status) + + -- Debug info. + if self.verbose>=2 then + self:I(self.lid..string.format("New cargo status: %s --> %s", tostring(self.cargoStatus), tostring(Status))) + end + + -- Set cargo status. + self.cargoStatus=Status + +end + +--- Set (new) carrier status. +-- @param #OPSGROUP self +-- @param #string Status New status. +function OPSGROUP:_NewCarrierStatus(Status) + + -- Debug info. + if self.verbose>=2 then + self:I(self.lid..string.format("New carrier status: %s --> %s", tostring(self.carrierStatus), tostring(Status))) + end + + -- Set cargo status. + self.carrierStatus=Status + +end + +--- Transfer cargo from to another carrier. +-- @param #OPSGROUP self +-- @param #OPSGROUP CargoGroup The cargo group to be transferred. +-- @param #OPSGROUP CarrierGroup The new carrier group. +-- @param #OPSGROUP.Element CarrierElement The new carrier element. +function OPSGROUP:_TransferCargo(CargoGroup, CarrierGroup, CarrierElement) + + -- Debug info. + self:T(self.lid..string.format("Transferring cargo %s to new carrier group %s", CargoGroup:GetName(), CarrierGroup:GetName())) + + -- Unload from this and directly load into the other carrier. + self:Unload(CargoGroup) + CarrierGroup:Load(CargoGroup, CarrierElement) + +end + +--- On after "Load" event. Carrier loads a cargo group into ints cargo bay. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP CargoGroup The OPSGROUP loaded as cargo. +-- @param #OPSGROUP.Element Carrier The carrier element/unit. +function OPSGROUP:onafterLoad(From, Event, To, CargoGroup, Carrier) + + -- Debug info. + self:T(self.lid..string.format("Loading group %s", tostring(CargoGroup.groupname))) + + -- Carrier element. + local carrier=Carrier or CargoGroup:_GetMyCarrierElement() --#OPSGROUP.Element + + -- No carrier provided. + if not carrier then + -- Try to find a carrier manually. + carrier=self:FindCarrierForCargo(CargoGroup) + end + + if carrier then + + --- + -- Embark Cargo + --- + + -- New cargo status. + CargoGroup:_NewCargoStatus(OPSGROUP.CargoStatus.LOADED) + + -- Clear all waypoints. + CargoGroup:ClearWaypoints() + + -- Add into carrier bay. + self:_AddCargobay(CargoGroup, carrier, false) + + -- Despawn this group. + if CargoGroup:IsAlive() then + CargoGroup:Despawn(0, true) + end + + -- Trigger embarked event for cargo group. + CargoGroup:Embarked(self, carrier) + + -- Trigger "Loaded" event for current cargo transport. + if self.cargoTransport then + self.cargoTransport:Loaded(CargoGroup, self, carrier) + else + self:T(self.lid..string.format("WARNING: Loaded cargo but no current OPSTRANSPORT assignment!")) + end + + else + self:E(self.lid.."ERROR: Cargo has no carrier on Load event!") + end + +end + +--- On after "Loaded" event. Carrier has loaded all (possible) cargo at the pickup zone. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterLoaded(From, Event, To) + + -- Debug info. + self:T(self.lid.."Carrier Loaded ==> Transport") + + -- Cancel landedAt task. + if self:IsFlightgroup() and self:IsLandedAt() then + local Task=self:GetTaskCurrent() + self:TaskCancel(Task) + end + + -- Order group to transport. + self:__Transport(1) + +end + +--- On before "Transport" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onbeforeTransport(From, Event, To) + + if self.cargoTransport==nil then + return false + elseif self.cargoTransport:IsDelivered() then --could be if all cargo was dead on boarding + return false + end + + return true +end + + +--- On after "Transport" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterTransport(From, Event, To) + + -- Set carrier status. + self:_NewCarrierStatus(OPSGROUP.CarrierStatus.TRANSPORTING) + + --TODO: This is all very similar to the onafterPickup() function. Could make it general. + + -- Deploy zone. + local Zone=self.cargoTransport.deployzone + + -- Check if already in deploy zone. + local inzone=Zone:IsCoordinateInZone(self:GetCoordinate()) + + local airbaseDeploy=nil --Wrapper.Airbase#AIRBASE + if Zone and Zone:IsInstanceOf("ZONE_AIRBASE") then + airbaseDeploy=Zone:GetAirbase() + end + + -- Check if group is already ready for loading. + local ready2deploy=false + if self:IsArmygroup() or self:IsNavygroup() then + ready2deploy=inzone + else + -- Aircraft is already parking at the pickup airbase. + ready2deploy=self.currbase and self.currbase:GetName()==Zone:GetName() and self:IsParking() + + -- If a helo is landed in the zone, we also are ready for loading. + if ready2deploy==false and (self.isHelo or self.isVTOL) and self:IsLandedAt() and inzone then + ready2deploy=true + end + end + + if inzone then + + -- We are already in the pickup zone ==> wait and initiate unloading. + if (self:IsArmygroup() or self:IsNavygroup()) and not self:IsHolding() then + self:FullStop() + end + + -- Start loading. + self:__UnLoading(-5) + + else + + -- Coord where the carrier goes to unload. + local Coordinate=nil --Core.Point#COORDINATE + + if self.cargoTransport.carrierGroup and self.cargoTransport.carrierGroup:IsLoading() then + -- Coordinate of the new carrier. + Coordinate=self.cargoTransport.carrierGroup:GetCoordinate() + else + -- Get a random coordinate in the deploy zone and let the carrier go there. + Coordinate=Zone:GetRandomCoordinate() + end + + -- Add waypoint. + if self.isFlightgroup then + + --- + -- Deploy at airbase + --- + + if airbaseDeploy then + + local airbaseCurrent=self.currbase + + if airbaseCurrent then + + -- Activate uncontrolled group. + if self:IsParking() then + self:StartUncontrolled() + end + + else + -- Order group to land at an airbase. + self:LandAtAirbase(airbaseDeploy) + end + + elseif self.isHelo or self.isVTOL then + + --- + -- Helo or VTOL can also land in a zone + --- + + -- If this is a helo and no ZONE_AIRBASE was given, we make the helo land in the pickup zone. + Coordinate:SetAltitude(200) + local waypoint=FLIGHTGROUP.AddWaypoint(self, Coordinate) ; waypoint.detour=1 + + else + self:E(self.lid.."ERROR: Carrier aircraft cannot land in Deploy zone! Specify a ZONE_AIRBASE as deploy zone") + end + + elseif self.isArmygroup then + + local cwp=self:GetWaypointCurrent() + local uid=cwp and cwp.uid or nil + + local path=self.cargoTransport:_GetPathTransport() + + if path then + -- Loop over coordinates. + for i,coordinate in pairs(path) do + local waypoint=ARMYGROUP.AddWaypoint(self, coordinate, nil, uid) ; waypoint.temp=true + uid=waypoint.uid + --coordinate:MarkToAll(string.format("Path i=%d, UID=%d", i, uid)) + end + end + + -- ARMYGROUP + local waypoint=ARMYGROUP.AddWaypoint(self, Coordinate, nil, self:GetWaypointCurrent().uid) ; waypoint.detour=1 + + -- Give cruise command. + self:Cruise() + + elseif self.isNavygroup then + + local cwp=self:GetWaypointCurrent() + local uid=cwp and cwp.uid or nil + + -- Get a (random) pre-defined transport path. + local path=self.cargoTransport:_GetPathTransport() + + if path then + -- Loop over coordinates. + for i,coordinate in pairs(path) do + local waypoint=NAVYGROUP.AddWaypoint(self, coordinate, nil, uid) ; waypoint.temp=true + uid=waypoint.uid + --coordinate:MarkToAll(string.format("Path i=%d, UID=%d", i, uid)) + end + end + + -- NAVYGROUP + local waypoint=NAVYGROUP.AddWaypoint(self, Coordinate, nil, uid) ; waypoint.detour=1 + + -- Give cruise command. + self:Cruise() + + end + + end + +end + +--- On after "Unloading" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterUnloading(From, Event, To) + + -- Set carrier status to UNLOADING. + self:_NewCarrierStatus(OPSGROUP.CarrierStatus.UNLOADING) + + -- Deploy zone. + local zone=self.cargoTransport.disembarkzone or self.cargoTransport.deployzone --Core.Zone#ZONE + + for _,_cargo in pairs(self.cargoTransport.cargos) do + local cargo=_cargo --#OPSGROUP.CargoGroup + + -- Check that cargo is loaded into this group. + -- NOTE: Could be that the element carriing this cargo group is DEAD, which would mean that the cargo group is also DEAD. + if cargo.opsgroup:IsLoaded(self.groupname) and not cargo.opsgroup:IsDead() then + + -- Disembark to carrier. + local needscarrier=false + local carrier=nil + local carrierGroup=nil + + + if self.cargoTransport.disembarkCarriers and #self.cargoTransport.disembarkCarriers>0 then + + needscarrier=true + + carrier, carrierGroup=self.cargoTransport:FindTransferCarrierForCargo(cargo.opsgroup, zone) + + --TODO: max unloading time if transfer carrier does not arrive in the zone. + + end + + if needscarrier==false or (needscarrier and carrier and carrierGroup) then + + -- Cargo was delivered (somehow). + cargo.delivered=true + + -- Increase number of delivered cargos. + self.cargoTransport.Ndelivered=self.cargoTransport.Ndelivered+1 + + if carrier and carrierGroup then + + --- + -- Delivered to another carrier group. + --- + + self:_TransferCargo(cargo.opsgroup, carrierGroup, carrier) + + elseif zone and zone:IsInstanceOf("ZONE_AIRBASE") and zone:GetAirbase():IsShip() then + + --- + -- Delivered to a ship via helo or VTOL + --- + + -- Issue warning. + self:E(self.lid.."ERROR: Deploy/disembark zone is a ZONE_AIRBASE of a ship! Where to put the cargo? Dumping into the sea, sorry!") + --TODO: Dumb into sea. + + else + + --- + -- Delivered to deploy zone + --- + + if self.cargoTransport.disembarkInUtero then + + -- Unload but keep "in utero" (no coordinate provided). + self:Unload(cargo.opsgroup) + + else + + local Coordinate=nil + + if self.cargoTransport.disembarkzone then + + -- Random coordinate in disembark zone. + Coordinate=self.cargoTransport.disembarkzone:GetRandomCoordinate() + + else + + -- Get random point in disembark zone. + local zoneCarrier=self:GetElementZoneUnload(cargo.opsgroup:_GetMyCarrierElement().name) + + -- Random coordinate/heading in the zone. + Coordinate=zoneCarrier:GetRandomCoordinate() + + end + + -- Random heading of the group. + local Heading=math.random(0,359) + + -- Unload to Coordinate. + self:Unload(cargo.opsgroup, Coordinate, self.cargoTransport.disembarkActivation, Heading) + + end + + -- Trigger "Unloaded" event for current cargo transport + self.cargoTransport:Unloaded(cargo.opsgroup, self) + + end + + else + self:T(self.lid.."Cargo needs carrier but no carrier is avaiable (yet)!") + end + + else + -- Not loaded or dead + end + + end -- loop over cargos + +end + +--- On before "Unload" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP OpsGroup The OPSGROUP loaded as cargo. +-- @param Core.Point#COORDINATE Coordinate Coordinate were the group is unloaded to. +-- @param #number Heading Heading of group. +function OPSGROUP:onbeforeUnload(From, Event, To, OpsGroup, Coordinate, Heading) + + -- Remove group from carrier bay. If group is not in cargo bay, function will return false and transition is denied. + local removed=self:_DelCargobay(OpsGroup) + + return removed +end + +--- On after "Unload" event. Carrier unloads a cargo group from its cargo bay. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP OpsGroup The OPSGROUP loaded as cargo. +-- @param Core.Point#COORDINATE Coordinate Coordinate were the group is unloaded to. +-- @param #boolean Activated If `true`, group is active. If `false`, group is spawned in late activated state. +-- @param #number Heading (Optional) Heading of group in degrees. Default is random heading for each unit. +function OPSGROUP:onafterUnload(From, Event, To, OpsGroup, Coordinate, Activated, Heading) + + -- New cargo status. + OpsGroup:_NewCargoStatus(OPSGROUP.CargoStatus.NOTCARGO) + + --TODO: Unload flightgroup. Find parking spot etc. + + if Coordinate then + + --- + -- Respawn at a coordinate. + --- + + -- Template for the respawned group. + local Template=UTILS.DeepCopy(OpsGroup.template) --DCS#Template + + -- No late activation. + if Activated==false then + Template.lateActivation=true + else + Template.lateActivation=false + end + + -- Loop over template units. + for _,Unit in pairs(Template.units) do + + local element=OpsGroup:GetElementByName(Unit.name) + + if element then + + local vec3=element.vec3 + + -- Relative pos vector. + local rvec2={x=Unit.x-Template.x, y=Unit.y-Template.y} --DCS#Vec2 + + local cvec2={x=Coordinate.x, y=Coordinate.z} --DCS#Vec2 + + -- Position. + Unit.x=cvec2.x+rvec2.x + Unit.y=cvec2.y+rvec2.y + Unit.alt=land.getHeight({x=Unit.x, y=Unit.y}) + + -- Heading. + Unit.heading=Heading and math.rad(Heading) or Unit.heading + Unit.psi=-Unit.heading + + end + + end + + -- Respawn group. + OpsGroup:_Respawn(0, Template) + + -- Add current waypoint. These have been cleard on loading. + if OpsGroup:IsNavygroup() then + OpsGroup:ClearWaypoints() + OpsGroup.currentwp=1 + OpsGroup.passedfinalwp=true + NAVYGROUP.AddWaypoint(OpsGroup, Coordinate, nil, nil, nil, false) + elseif OpsGroup:IsArmygroup() then + OpsGroup:ClearWaypoints() + OpsGroup.currentwp=1 + OpsGroup.passedfinalwp=true + ARMYGROUP.AddWaypoint(OpsGroup, Coordinate, nil, nil, nil, false) + end + + else + + --- + -- Just remove from this carrier. + --- + + -- Nothing to do. + + OpsGroup.position=self:GetVec3() + + end + + -- Trigger "Disembarked" event. + OpsGroup:Disembarked(OpsGroup:_GetMyCarrierGroup(), OpsGroup:_GetMyCarrierElement()) + + -- Trigger "Unloaded" event. + self:Unloaded(OpsGroup) + + -- Remove my carrier. + OpsGroup:_RemoveMyCarrier() + +end + +--- On after "Unloaded" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP OpsGroupCargo Cargo OPSGROUP that was unloaded from a carrier. +function OPSGROUP:onafterUnloaded(From, Event, To, OpsGroupCargo) + self:I(self.lid..string.format("Unloaded OPSGROUP %s", OpsGroupCargo:GetName())) +end + + +--- On after "UnloadingDone" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterUnloadingDone(From, Event, To) + + -- Debug info + self:T(self.lid.."Cargo unloading done..") + + -- Cancel landedAt task. + if self:IsFlightgroup() and self:IsLandedAt() then + local Task=self:GetTaskCurrent() + self:TaskCancel(Task) + end + + -- Check everything was delivered (or is dead). + local delivered=self:_CheckGoPickup(self.cargoTransport) + + if not delivered then + + -- Pickup the next batch. + self:I(self.lid.."Unloaded: Still cargo left ==> Pickup") + self:Pickup(self.cargoTransport.pickupzone) + + else + + -- Everything delivered. + self:I(self.lid.."Unloaded: ALL cargo unloaded ==> Delivered (current)") + self:Delivered(self.cargoTransport) + + end + +end + +--- On after "Delivered" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsTransport#OPSTRANSPORT CargoTransport The cargo transport assignment. +function OPSGROUP:onafterDelivered(From, Event, To, CargoTransport) + + -- Check if this was the current transport. + if self.cargoTransport and self.cargoTransport.uid==CargoTransport.uid then + + -- This is not a carrier anymore. + self:_NewCarrierStatus(OPSGROUP.CarrierStatus.NOTCARRIER) + + -- Checks + if self:IsPickingup() then + -- Delete pickup waypoint? + local wpindex=self:GetWaypointIndexNext(false) + if wpindex then + self:RemoveWaypoint(wpindex) + end + elseif self:IsLoading() then + -- Nothing to do? + elseif self:IsTransporting() then + -- This should not happen. Carrier is transporting, how can the cargo be delivered? + elseif self:IsUnloading() then + -- Nothing to do? + end + + -- Startup uncontrolled aircraft to allow it to go back. + if self:IsFlightgroup() then + if self:IsUncontrolled() then + self:StartUncontrolled() + elseif self:IsLandedAt() then + local Task=self:GetTaskCurrent() + self:TaskCancel(Task) + end + else + -- Army & Navy: give Cruise command to "wake up" from waiting status. + self:__Cruise(0.1) + end + + -- Check group done. + self:T(self.lid.."All cargo delivered ==> check group done") + self:_CheckGroupDone(0.2) + + -- No current transport any more. + self.cargoTransport=nil + end + + -- Remove cargo transport from cargo queue. + self:DelOpsTransport(CargoTransport) + +end + + +--- +-- Cargo Group Functions +--- + +--- On before "Board" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP CarrierGroup The carrier group. +-- @param #OPSGROUP.Element Carrier The OPSGROUP element +function OPSGROUP:onbeforeBoard(From, Event, To, CarrierGroup, Carrier) + + if self:IsDead() then + self:I(self.lid.."Group DEAD ==> Deny Board transition!") + return false + elseif CarrierGroup:IsDead() then + self:I(self.lid.."Carrier Group DEAD ==> Deny Board transition!") + self:_NewCargoStatus(OPSGROUP.CargoStatus.NOTCARGO) + return false + elseif Carrier.status==OPSGROUP.ElementStatus.DEAD then + self:I(self.lid.."Carrier Element DEAD ==> Deny Board transition!") + self:_NewCargoStatus(OPSGROUP.CargoStatus.NOTCARGO) + return false + end + + return true +end + +--- On after "Board" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP CarrierGroup The carrier group. +-- @param #OPSGROUP.Element Carrier The OPSGROUP element +function OPSGROUP:onafterBoard(From, Event, To, CarrierGroup, Carrier) + + -- Set cargo status. + self:_NewCargoStatus(OPSGROUP.CargoStatus.BOARDING) + + -- Army or Navy group. + local CarrierIsArmyOrNavy=CarrierGroup:IsArmygroup() or CarrierGroup:IsNavygroup() + local CargoIsArmyOrNavy=self:IsArmygroup() or self:IsNavygroup() + + -- Check that carrier is standing still. + if (CarrierIsArmyOrNavy and (CarrierGroup:IsHolding() and CarrierGroup:GetVelocity(Carrier.name)<=1)) or (CarrierGroup:IsFlightgroup() and (CarrierGroup:IsParking() or CarrierGroup:IsLandedAt())) then + + -- Board if group is mobile, not late activated and army or navy. Everything else is loaded directly. + local board=self.speedMax>0 and CargoIsArmyOrNavy and self:IsAlive() and CarrierGroup:IsAlive() + + -- Armygroup cannot board ship ==> Load directly. + if self:IsArmygroup() and CarrierGroup:IsNavygroup() then + board=false + end + + if board then + + -- Debug info. + self:T(self.lid..string.format("Boarding group=%s [%s], carrier=%s", CarrierGroup:GetName(), CarrierGroup:GetState(), Carrier.name)) + + -- TODO: Implement embarkzone. + local Coordinate=Carrier.unit:GetCoordinate() + + -- Clear all waypoints. + self:ClearWaypoints(self.currentwp+1) + + if self.isArmygroup then + local waypoint=ARMYGROUP.AddWaypoint(self, Coordinate) ; waypoint.detour=1 + self:Cruise() + else + local waypoint=NAVYGROUP.AddWaypoint(self, Coordinate) ; waypoint.detour=1 + self:Cruise() + end + + -- Set carrier. As long as the group is not loaded, we only reserve the cargo space. + CarrierGroup:_AddCargobay(self, Carrier, true) + + else + + --- + -- Direct load into carrier. + --- + + -- Debug info. + self:T(self.lid..string.format("Board with direct load to carrier %s", CarrierGroup:GetName())) + + local mycarriergroup=self:_GetMyCarrierGroup() + + -- Unload cargo first. + if mycarriergroup then + mycarriergroup:Unload(self) + end + + -- Trigger Load event. + CarrierGroup:Load(self) + + end + + else + + -- Redo boarding call. + self:T(self.lid.."Carrier not ready for boarding yet ==> repeating boarding call in 10 sec") + self:__Board(-10, CarrierGroup, Carrier) + + -- Set carrier. As long as the group is not loaded, we only reserve the cargo space. + CarrierGroup:_AddCargobay(self, Carrier, true) + + end + + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -3807,37 +6884,37 @@ end function OPSGROUP:_CheckInZones() if self.checkzones and self:IsAlive() then - + local Ncheck=self.checkzones:Count() local Ninside=self.inzones:Count() - + -- Debug info. self:T(self.lid..string.format("Check if group is in %d zones. Currently it is in %d zones.", self.checkzones:Count(), self.inzones:Count())) -- Firstly, check if group is still inside zone it was already in. If not, remove zones and trigger LeaveZone() event. local leftzones={} for inzonename, inzone in pairs(self.inzones:GetSet()) do - + -- Check if group is still inside the zone. local isstillinzone=self.group:IsInZone(inzone) --:IsPartlyOrCompletelyInZone(inzone) - + -- If not, trigger, LeaveZone event. if not isstillinzone then table.insert(leftzones, inzone) - end + end end - + -- Trigger leave zone event. for _,leftzone in pairs(leftzones) do self:LeaveZone(leftzone) end - - + + -- Now, run of all check zones and see if the group entered a zone. local enterzones={} for checkzonename,_checkzone in pairs(self.checkzones:GetSet()) do local checkzone=_checkzone --Core.Zone#ZONE - + -- Is group currtently in this check zone? local isincheckzone=self.group:IsInZone(checkzone) --:IsPartlyOrCompletelyInZone(checkzone) @@ -3845,12 +6922,12 @@ function OPSGROUP:_CheckInZones() table.insert(enterzones, checkzone) end end - + -- Trigger enter zone event. for _,enterzone in pairs(enterzones) do self:EnterZone(enterzone) end - + end end @@ -3870,33 +6947,33 @@ function OPSGROUP:_CheckDetectedUnits() local DetectedObject=Detection.object -- DCS#Object if DetectedObject and DetectedObject:isExist() and DetectedObject.id_<50000000 then - + -- Unit. local unit=UNIT:Find(DetectedObject) - + if unit and unit:IsAlive() then - + -- Name of detected unit local unitname=unit:GetName() - -- Add unit to detected table of this run. + -- Add unit to detected table of this run. table.insert(detected, unit) - + -- Trigger detected unit event ==> This also triggers the DetectedUnitNew and DetectedUnitKnown events. self:DetectedUnit(unit) - + -- Get group of unit. local group=unit:GetGroup() - + -- Add group to table. - if group then - groups[group:GetName()]=group + if group then + groups[group:GetName()]=group end - + end end end - + -- Call detected group event. for groupname, group in pairs(groups) do self:DetectedGroup(group) @@ -3922,7 +6999,7 @@ function OPSGROUP:_CheckDetectedUnits() end end - + -- Remove lost units from detected set. self.detectedunits:RemoveUnitsByName(lost) @@ -3947,7 +7024,7 @@ function OPSGROUP:_CheckDetectedUnits() end end - + -- Remove lost units from detected set. self.detectedgroups:RemoveGroupsByName(lost) @@ -3966,91 +7043,101 @@ function OPSGROUP:_CheckGroupDone(delay) -- Delayed call. self:ScheduleOnce(delay, self._CheckGroupDone, self) else - + + -- Debug info. + self:T(self.lid.."Check OPSGROUP done?") + + -- Group is engaging something. if self:IsEngaging() then self:UpdateRoute() return end - + + -- Group is waiting. We deny all updates. + if self:IsWaiting() then + -- If group is waiting, we assume that is the way it is meant to be. + return + end + -- Get current waypoint. local waypoint=self:GetWaypoint(self.currentwp) - - --env.info("FF CheckGroupDone") if waypoint then - + -- Number of tasks remaining for this waypoint. local ntasks=self:CountTasksWaypoint(waypoint.uid) - + -- We only want to update the route if there are no more tasks to be done. if ntasks>0 then self:T(self.lid..string.format("Still got %d tasks for the current waypoint UID=%d ==> RETURN (no action)", ntasks, waypoint.uid)) return end - end - + end + if self.adinfinitum then - + --- -- Parol Ad Infinitum --- if #self.waypoints>0 then - + -- Next waypoint index. local i=self:GetWaypointIndexNext(true) - + -- Get positive speed to first waypoint. local speed=self:GetSpeedToWaypoint(i) - - -- Start route at first waypoint. - self:UpdateRoute(i, speed) - + + -- Cruise. + self:Cruise(speed) + + -- Debug info. self:T(self.lid..string.format("Adinfinitum=TRUE ==> Goto WP index=%d at speed=%d knots", i, speed)) - + else self:E(self.lid..string.format("WARNING: No waypoints left! Commanding a Full Stop")) - self:__FullStop(-1) + self:__FullStop(-1) end else - + --- -- Finite Patrol --- - + if self.passedfinalwp then - + --- -- Passed FINAL waypoint --- - + -- No further waypoints. Command a full stop. self:__FullStop(-1) - + self:T(self.lid..string.format("Passed final WP, adinfinitum=FALSE ==> Full Stop")) - + else - + --- -- Final waypoint NOT passed yet --- - + if #self.waypoints>0 then self:T(self.lid..string.format("NOT Passed final WP, #WP>0 ==> Update Route")) - self:UpdateRoute() + --self:UpdateRoute() + self:Cruise() else self:E(self.lid..string.format("WARNING: No waypoints left! Commanding a Full Stop")) self:__FullStop(-1) end - + end - + end - - end + + end end - + end --- Check if group got stuck. @@ -4061,25 +7148,25 @@ function OPSGROUP:_CheckStuck() if self:IsHolding() or self:Is("Rearming") then return end - + -- Current time. local Tnow=timer.getTime() - + -- Expected speed in m/s. local ExpectedSpeed=self:GetExpectedSpeed() - + -- Current speed in m/s. local speed=self:GetVelocity() - + -- Check speed. if speed<0.5 then - + if ExpectedSpeed>0 and not self.stuckTimestamp then self:T2(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected", speed, ExpectedSpeed)) self.stuckTimestamp=Tnow self.stuckVec3=self:GetVec3() end - + else -- Moving (again). self.stuckTimestamp=nil @@ -4087,20 +7174,20 @@ function OPSGROUP:_CheckStuck() -- Somehow we are not moving... if self.stuckTimestamp then - + -- Time we are holding. local holdtime=Tnow-self.stuckTimestamp - + if holdtime>=10*60 then - + self:E(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected for %d sec", speed, ExpectedSpeed, holdtime)) - + --TODO: Stuck event! - + end - + end - + end @@ -4112,25 +7199,25 @@ function OPSGROUP:_CheckDamage() self.life=0 local damaged=false for _,_element in pairs(self.elements) do - local element=_element --Ops.OpsGroup#OPSGROUP - + local element=_element --Ops.OpsGroup#OPSGROUP.Element + -- Current life points. local life=element.unit:GetLife() - + self.life=self.life+life - + if life0 then - + -- Get current ammo. local ammo=self:GetAmmoTot() - + -- Check if rearming is completed. if self:IsRearming() then if ammo.Total==self.ammo.Total then self:Rearmed() end - end - + end + -- Total. if self.outofAmmo and ammo.Total>0 then self.outofAmmo=false end if ammo.Total==0 and not self.outofAmmo then - env.info("FF out of ammo") self.outofAmmo=true self:OutOfAmmo() end -- Guns. if self.outofGuns and ammo.Guns>0 then - self.outoffGuns=false + self.outofGuns=false end if ammo.Guns==0 and self.ammo.Guns>0 and not self.outofGuns then self.outofGuns=true @@ -4192,7 +7278,7 @@ function OPSGROUP:_CheckAmmoStatus() -- Rockets. if self.outofRockets and ammo.Rockets>0 then - self.outoffRockets=false + self.outofRockets=false end if ammo.Rockets==0 and self.ammo.Rockets>0 and not self.outofRockets then self.outofRockets=true @@ -4201,29 +7287,57 @@ function OPSGROUP:_CheckAmmoStatus() -- Bombs. if self.outofBombs and ammo.Bombs>0 then - self.outoffBombs=false + self.outofBombs=false end if ammo.Bombs==0 and self.ammo.Bombs>0 and not self.outofBombs then self.outofBombs=true self:OutOfBombs() end - -- Missiles. + -- Missiles (All). if self.outofMissiles and ammo.Missiles>0 then - self.outoffMissiles=false + self.outofMissiles=false end if ammo.Missiles==0 and self.ammo.Missiles>0 and not self.outofMissiles then self.outofMissiles=true self:OutOfMissiles() end + + -- Missiles AA. + if self.outofMissilesAA and ammo.MissilesAA>0 then + self.outofMissilesAA=false + end + if ammo.MissilesAA and self.ammo.MissilesAA>0 and not self.outofMissilesAA then + self.outofMissilesAA=true + self:OutOfMissilesAA() + end + -- Missiles AG. + if self.outofMissilesAG and ammo.MissilesAG>0 then + self.outofMissilesAG=false + end + if ammo.MissilesAG and self.ammo.MissilesAG>0 and not self.outofMissilesAG then + self.outofMissilesAG=true + self:OutOfMissilesAG() + end + + -- Missiles AS. + if self.outofMissilesAS and ammo.MissilesAS>0 then + self.outofMissilesAS=false + end + if ammo.MissilesAS and self.ammo.MissilesAS>0 and not self.outofMissilesAS then + self.outofMissilesAS=true + self:OutOfMissilesAS() + end + + -- Check if group is engaging. if self:IsEngaging() and ammo.Total==0 then self:Disengage() end end - + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -4237,9 +7351,9 @@ function OPSGROUP:_PrintTaskAndMissionStatus() --- -- Tasks: verbose >= 3 --- - + -- Task queue. - if self.verbose>=3 and #self.taskqueue>0 then + if self.verbose>=3 and #self.taskqueue>0 then local text=string.format("Tasks #%d", #self.taskqueue) for i,_task in pairs(self.taskqueue) do local task=_task --Ops.OpsGroup#OPSGROUP.Task @@ -4269,22 +7383,22 @@ function OPSGROUP:_PrintTaskAndMissionStatus() end self:I(self.lid..text) end - + --- -- Missions: verbose>=2 --- - + -- Current mission name. - if self.verbose>=2 then + if self.verbose>=2 then local Mission=self:GetMissionByID(self.currentmission) - + -- Current status. local text=string.format("Missions %d, Current: %s", self:CountRemainingMissison(), Mission and Mission.name or "none") for i,_mission in pairs(self.missionqueue) do local mission=_mission --Ops.Auftrag#AUFTRAG local Cstart= UTILS.SecondsToClock(mission.Tstart, true) local Cstop = mission.Tstop and UTILS.SecondsToClock(mission.Tstop, true) or "INF" - text=text..string.format("\n[%d] %s (%s) status=%s (%s), Time=%s-%s, prio=%d wp=%s targets=%d", + text=text..string.format("\n[%d] %s (%s) status=%s (%s), Time=%s-%s, prio=%d wp=%s targets=%d", i, tostring(mission.name), mission.type, mission:GetGroupStatus(self), tostring(mission.status), Cstart, Cstop, mission.prio, tostring(mission:GetGroupWaypointIndex(self)), mission:CountMissionTargets()) end self:I(self.lid..text) @@ -4301,27 +7415,28 @@ end -- @param #OPSGROUP.Waypoint Waypoint data. -- @return #OPSGROUP.Waypoint Modified waypoint data. function OPSGROUP:_CreateWaypoint(waypoint) - + -- Set uid. waypoint.uid=self.wpcounter - + -- Waypoint has not been passed yet. waypoint.npassed=0 - + -- Coordinate. waypoint.coordinate=COORDINATE:New(waypoint.x, waypoint.alt, waypoint.y) -- Set waypoint name. - waypoint.name=string.format("Waypoint UID=%d", waypoint.uid) - + waypoint.name=string.format("Waypoint UID=%d", waypoint.uid) + -- Set types. waypoint.patrol=false waypoint.detour=false waypoint.astar=false + waypoint.temp=false -- Increase UID counter. self.wpcounter=self.wpcounter+1 - + return waypoint end @@ -4338,59 +7453,101 @@ function OPSGROUP:_AddWaypoint(waypoint, wpnumber) table.insert(self.waypoints, wpnumber, waypoint) -- Debug info. - self:T(self.lid..string.format("Adding waypoint at index=%d id=%d", wpnumber, waypoint.uid)) - + self:T(self.lid..string.format("Adding waypoint at index=%d with UID=%d", wpnumber, waypoint.uid)) + -- Now we obviously did not pass the final waypoint. - self.passedfinalwp=false - - -- Switch to cruise mode. - if self:IsHolding() then - self:Cruise() + if self.currentwp and wpnumber>self.currentwp then + self.passedfinalwp=false end + end --- Initialize Mission Editor waypoints. -- @param #OPSGROUP self +-- @param #number WpIndexMin +-- @param #number WpIndexMax -- @return #OPSGROUP self -function OPSGROUP:InitWaypoints() +function OPSGROUP:_InitWaypoints(WpIndexMin, WpIndexMax) -- Template waypoints. self.waypoints0=self.group:GetTemplateRoutePoints() - -- Waypoints + -- Waypoints empty! self.waypoints={} - for index,wp in pairs(self.waypoints0) do + WpIndexMin=WpIndexMin or 1 + WpIndexMax=WpIndexMax or #self.waypoints0 + WpIndexMax=math.min(WpIndexMax, #self.waypoints0) --Ensure max is not out of bounce. + + --for index,wp in pairs(self.waypoints0) do + + for i=WpIndexMin,WpIndexMax do + + local wp=self.waypoints0[i] --DCS#Waypoint + + -- Coordinate of the waypoint. + local coordinate=COORDINATE:NewFromWaypoint(wp) - -- Coordinate of the waypoint. - local coordinate=COORDINATE:New(wp.x, wp.alt, wp.y) - -- Strange! wp.speed=wp.speed or 0 - + -- Speed at the waypoint. local speedknots=UTILS.MpsToKnots(wp.speed) - - if index==1 then + + if i==1 then self.speedWp=wp.speed end + local waypoint=self:_CreateWaypoint(wp) + + self:_AddWaypoint(waypoint) + -- Add waypoint. - self:AddWaypoint(coordinate, speedknots, index-1, nil, false) - + --[[ + if self:IsFlightgroup() then + FLIGHTGROUP.AddWaypoint(self, coordinate, speedknots, index-1, Altitude, false) + elseif self:IsArmygroup() then + ARMYGROUP.AddWaypoint(self, coordinate, speedknots, index-1, Formation, false) + elseif self:IsNavygroup() then + NAVYGROUP.AddWaypoint(self, coordinate, speedknots, index-1, Depth, false) + else + -- Should not happen! + self:AddWaypoint(coordinate, speedknots, index-1, nil, false) + end + ]] + end - + -- Debug info. self:T(self.lid..string.format("Initializing %d waypoints", #self.waypoints)) + -- Flight group specific. + if self:IsFlightgroup() then + + -- Get home and destination airbases from waypoints. + self.homebase=self.homebase or self:GetHomebaseFromWaypoints() + self.destbase=self.destbase or self:GetDestinationFromWaypoints() + self.currbase=self:GetHomebaseFromWaypoints() + + -- Remove the landing waypoint. We use RTB for that. It makes adding new waypoints easier as we do not have to check if the last waypoint is the landing waypoint. + if self.destbase and #self.waypoints>1 then + table.remove(self.waypoints, #self.waypoints) + else + self.destbase=self.homebase + end + + end + -- Update route. if #self.waypoints>0 then - + -- Check if only 1 wp? if #self.waypoints==1 then self.passedfinalwp=true end - + + else + self:E(self.lid.."WARNING: No waypoints initialized. Number of waypoints is 0!") end return self @@ -4408,29 +7565,29 @@ function OPSGROUP:Route(waypoints, delay) else if self:IsAlive() then - + -- DCS task combo. local Tasks={} - + -- Route (Mission) task. local TaskRoute=self.group:TaskRoute(waypoints) table.insert(Tasks, TaskRoute) - + -- TaskCombo of enroute and mission tasks. local TaskCombo=self.group:TaskCombo(Tasks) - + -- Set tasks. if #Tasks>1 then self:SetTask(TaskCombo) else self:SetTask(TaskRoute) end - + else self:E(self.lid.."ERROR: Group is not alive! Cannot route group.") end end - + return self end @@ -4445,25 +7602,25 @@ function OPSGROUP:_UpdateWaypointTasks(n) local nwaypoints=#waypoints for i,_wp in pairs(waypoints) do - local wp=_wp --Ops.OpsGroup#OPSGROUP.Waypoint - + local wp=_wp --Ops.OpsGroup#OPSGROUP.Waypoint + if i>=n or nwaypoints==1 then - + -- Debug info. self:T2(self.lid..string.format("Updating waypoint task for waypoint %d/%d ID=%d. Last waypoint passed %d", i, nwaypoints, wp.uid, self.currentwp)) - + -- Tasks of this waypoint local taskswp={} - + -- At each waypoint report passing. local TaskPassingWaypoint=self.group:TaskFunction("OPSGROUP._PassingWaypoint", self, wp.uid) - table.insert(taskswp, TaskPassingWaypoint) - + table.insert(taskswp, TaskPassingWaypoint) + -- Waypoint task combo. wp.task=self.group:TaskCombo(taskswp) - + end - + end end @@ -4477,89 +7634,146 @@ end --@param #OPSGROUP opsgroup Ops group object. --@param #number uid Waypoint UID. function OPSGROUP._PassingWaypoint(group, opsgroup, uid) - + -- Get waypoint data. local waypoint=opsgroup:GetWaypointByID(uid) - + if waypoint then - + -- Current wp. local currentwp=opsgroup.currentwp - + -- Get the current waypoint index. opsgroup.currentwp=opsgroup:GetWaypointIndex(uid) - + -- Set expected speed and formation from the next WP. - local wpnext=opsgroup:GetWaypointNext() + local wpnext=opsgroup:GetWaypointNext() if wpnext then - + -- Set formation. if opsgroup.isGround then opsgroup.formation=wpnext.action end - + -- Set speed. opsgroup.speed=wpnext.speed - + end - + -- Debug message. local text=string.format("Group passing waypoint uid=%d", uid) opsgroup:T(opsgroup.lid..text) - + -- Trigger PassingWaypoint event. - if waypoint.astar then - + if waypoint.temp then + + -- Remove temp waypoint. + opsgroup:RemoveWaypointByID(uid) + + if opsgroup:IsNavygroup() or opsgroup:IsArmygroup() then + --TODO: not sure if this works with FLIGHTGROUPS + opsgroup:Cruise() + end + + elseif waypoint.astar then + -- Remove Astar waypoint. opsgroup:RemoveWaypointByID(uid) - + -- Cruise. opsgroup:Cruise() - + elseif waypoint.detour then - + -- Remove detour waypoint. opsgroup:RemoveWaypointByID(uid) - + if opsgroup:IsRearming() then - + -- Trigger Rearming event. opsgroup:Rearming() - + elseif opsgroup:IsRetreating() then - + -- Trigger Retreated event. opsgroup:Retreated() - + + elseif opsgroup:IsPickingup() then + + if opsgroup.isFlightgroup then + + -- Land at current pos and wait for 60 min max. + opsgroup:LandAt(opsgroup:GetCoordinate(), 60*60) + + else + -- Wait and load cargo. + opsgroup:FullStop() + opsgroup:__Loading(-5) + end + + elseif opsgroup:IsTransporting() then + + if opsgroup.isFlightgroup then + + -- Land at current pos and wait for 60 min max. + opsgroup:LandAt(opsgroup:GetCoordinate(), 60*60) + + else + -- Stop and unload. + opsgroup:FullStop() + opsgroup:Unloading() + end + + elseif opsgroup:IsBoarding() then + + local carrierGroup=opsgroup:_GetMyCarrierGroup() + local carrier=opsgroup:_GetMyCarrierElement() + + if carrierGroup and carrierGroup:IsAlive() then + + if carrier and carrier.unit and carrier.unit:IsAlive() then + + -- Load group into the carrier. + carrierGroup:Load(opsgroup) + + else + opsgroup:E(opsgroup.lid.."ERROR: Group cannot board assigned carrier UNIT as it is NOT alive!") + end + + else + opsgroup:E(opsgroup.lid.."ERROR: Group cannot board assigned carrier GROUP as it is NOT alive!") + end + elseif opsgroup:IsEngaging() then - + -- Nothing to do really. - + else - + -- Trigger DetourReached event. opsgroup:DetourReached() - + if waypoint.detour==0 then opsgroup:FullStop() elseif waypoint.detour==1 then opsgroup:Cruise() else opsgroup:E("ERROR: waypoint.detour should be 0 or 1") + opsgroup:FullStop() end - + end - + else -- Check if the group is still pathfinding. if opsgroup.ispathfinding then opsgroup.ispathfinding=false - end + end -- Increase passing counter. - waypoint.npassed=waypoint.npassed+1 - + waypoint.npassed=waypoint.npassed+1 + -- Call event function. opsgroup:PassingWaypoint(waypoint) end @@ -4592,7 +7806,7 @@ function OPSGROUP._TaskDone(group, opsgroup, task) -- Debug message. local text=string.format("_TaskDone %s", task.description) - opsgroup:T3(opsgroup.lid..text) + opsgroup:T(opsgroup.lid..text) -- Set current task to nil so that the next in line can be executed. if opsgroup then @@ -4618,25 +7832,25 @@ end -- @param #string roe ROE of group. Default is value set in `SetDefaultROE` (usually `ENUMS.ROE.ReturnFire`). -- @return #OPSGROUP self function OPSGROUP:SwitchROE(roe) - + if self:IsAlive() or self:IsInUtero() then self.option.ROE=roe or self.optionDefault.ROE - + if self:IsInUtero() then self:T2(self.lid..string.format("Setting current ROE=%d when GROUP is SPAWNED", self.option.ROE)) else - + self.group:OptionROE(self.option.ROE) - + self:T(self.lid..string.format("Setting current ROE=%d (%s)", self.option.ROE, self:_GetROEName(self.option.ROE))) end - - + + else self:E(self.lid.."WARNING: Cannot switch ROE! Group is not alive") end - + return self end @@ -4680,25 +7894,26 @@ end -- @param #string rot ROT of group. Default is value set in `:SetDefaultROT` (usually `ENUMS.ROT.PassiveDefense`). -- @return #OPSGROUP self function OPSGROUP:SwitchROT(rot) - + if self:IsAlive() or self:IsInUtero() then - + self.option.ROT=rot or self.optionDefault.ROT - + if self:IsInUtero() then - self:T2(self.lid..string.format("Setting current ROT=%d when GROUP is SPAWNED", self.option.ROT)) + self:T2(self.lid..string.format("Setting current ROT=%d when GROUP is SPAWNED", self.option.ROT)) else - + self.group:OptionROT(self.option.ROT) - + + -- Debug info. self:T(self.lid..string.format("Setting current ROT=%d (0=NoReaction, 1=Passive, 2=Evade, 3=ByPass, 4=AllowAbort)", self.option.ROT)) end - + else self:E(self.lid.."WARNING: Cannot switch ROT! Group is not alive") end - + return self end @@ -4720,43 +7935,47 @@ function OPSGROUP:SetDefaultAlarmstate(alarmstate) end --- Set current Alarm State of the group. --- +-- -- * 0 = "Auto" -- * 1 = "Green" -- * 2 = "Red" --- +-- -- @param #OPSGROUP self -- @param #number alarmstate Alarm state of group. Default is 0="Auto". -- @return #OPSGROUP self function OPSGROUP:SwitchAlarmstate(alarmstate) - + if self:IsAlive() or self:IsInUtero() then - - self.option.Alarm=alarmstate or self.optionDefault.Alarm - - if self:IsInUtero() then - self:T2(self.lid..string.format("Setting current Alarm State=%d when GROUP is SPAWNED", self.option.Alarm)) - else - - if self.option.Alarm==0 then - self.group:OptionAlarmStateAuto() - elseif self.option.Alarm==1 then - self.group:OptionAlarmStateGreen() - elseif self.option.Alarm==2 then - self.group:OptionAlarmStateRed() + + if self.isArmygroup or self.isNavygroup then + + self.option.Alarm=alarmstate or self.optionDefault.Alarm + + if self:IsInUtero() then + self:T2(self.lid..string.format("Setting current Alarm State=%d when GROUP is SPAWNED", self.option.Alarm)) else - self:E("ERROR: Unknown Alarm State! Setting to AUTO") - self.group:OptionAlarmStateAuto() - self.option.Alarm=0 + + if self.option.Alarm==0 then + self.group:OptionAlarmStateAuto() + elseif self.option.Alarm==1 then + self.group:OptionAlarmStateGreen() + elseif self.option.Alarm==2 then + self.group:OptionAlarmStateRed() + else + self:E("ERROR: Unknown Alarm State! Setting to AUTO") + self.group:OptionAlarmStateAuto() + self.option.Alarm=0 + end + + self:T(self.lid..string.format("Setting current Alarm State=%d (0=Auto, 1=Green, 2=Red)", self.option.Alarm)) + end - - self:T(self.lid..string.format("Setting current Alarm State=%d (0=Auto, 1=Green, 2=Red)", self.option.Alarm)) - + end else self:E(self.lid.."WARNING: Cannot switch Alarm State! Group is not alive.") end - + return self end @@ -4776,7 +7995,7 @@ end -- @param #boolean OffSwitch If true, TACAN is off by default. -- @return #OPSGROUP self function OPSGROUP:SetDefaultTACAN(Channel, Morse, UnitName, Band, OffSwitch) - + self.tacanDefault={} self.tacanDefault.Channel=Channel or 74 self.tacanDefault.Morse=Morse or "XXX" @@ -4787,9 +8006,9 @@ function OPSGROUP:SetDefaultTACAN(Channel, Morse, UnitName, Band, OffSwitch) else Band=Band or "X" end - self.tacanDefault.Band=Band - - + self.tacanDefault.Band=Band + + if OffSwitch then self.tacanDefault.On=false else @@ -4807,19 +8026,19 @@ end function OPSGROUP:_SwitchTACAN(Tacan) if Tacan then - + self:SwitchTACAN(Tacan.Channel, Tacan.Morse, Tacan.BeaconName, Tacan.Band) - + else - + if self.tacanDefault.On then self:SwitchTACAN() else self:TurnOffTACAN() end - + end - + end --- Activate/switch TACAN beacon settings. @@ -4832,12 +8051,12 @@ end function OPSGROUP:SwitchTACAN(Channel, Morse, UnitName, Band) if self:IsInUtero() then - + self:T(self.lid..string.format("Switching TACAN to DEFAULT when group is spawned")) self:SetDefaultTACAN(Channel, Morse, UnitName, Band) elseif self:IsAlive() then - + Channel=Channel or self.tacanDefault.Channel Morse=Morse or self.tacanDefault.Morse Band=Band or self.tacanDefault.Band @@ -4856,7 +8075,7 @@ function OPSGROUP:SwitchTACAN(Channel, Morse, UnitName, Band) self:T(self.lid.."WARNING: Could not get TACAN unit. Trying first unit in the group") unit=self:GetUnit(1) end - + if unit and unit:IsAlive() then -- Unit ID. @@ -4864,16 +8083,16 @@ function OPSGROUP:SwitchTACAN(Channel, Morse, UnitName, Band) -- Type local Type=BEACON.Type.TACAN - + -- System - local System=BEACON.System.TACAN + local System=BEACON.System.TACAN if self.isAircraft then System=BEACON.System.TACAN_TANKER_Y end - + -- Tacan frequency. local Frequency=UTILS.TACANToFrequency(Channel, Band) - + -- Activate beacon. unit:CommandActivateBeacon(Type, System, Frequency, UnitID, Channel, Band, true, Morse, true) @@ -4884,10 +8103,10 @@ function OPSGROUP:SwitchTACAN(Channel, Morse, UnitName, Band) self.tacan.BeaconName=unit:GetName() self.tacan.BeaconUnit=unit self.tacan.On=true - - -- Debug info. + + -- Debug info. self:T(self.lid..string.format("Switching TACAN to Channel %d%s Morse %s on unit %s", self.tacan.Channel, self.tacan.Band, tostring(self.tacan.Morse), self.tacan.BeaconName)) - + else self:E(self.lid.."ERROR: Cound not set TACAN! Unit is not alive") end @@ -4924,6 +8143,12 @@ function OPSGROUP:GetTACAN() return self.tacan.Channel, self.tacan.Morse, self.tacan.Band, self.tacan.On, self.tacan.BeaconName end +--- Get current TACAN parameters. +-- @param #OPSGROUP self +-- @return #OPSGROUP.Beacon TACAN beacon. +function OPSGROUP:GetBeaconTACAN() + return self.tacan +end --- Set default ICLS parameters. @@ -4934,12 +8159,12 @@ end -- @param #boolean OffSwitch If true, TACAN is off by default. -- @return #OPSGROUP self function OPSGROUP:SetDefaultICLS(Channel, Morse, UnitName, OffSwitch) - + self.iclsDefault={} self.iclsDefault.Channel=Channel or 1 self.iclsDefault.Morse=Morse or "XXX" self.iclsDefault.BeaconName=UnitName - + if OffSwitch then self.iclsDefault.On=false else @@ -4957,17 +8182,17 @@ end function OPSGROUP:_SwitchICLS(Icls) if Icls then - + self:SwitchICLS(Icls.Channel, Icls.Morse, Icls.BeaconName) - + else - + if self.iclsDefault.On then self:SwitchICLS() else self:TurnOffICLS() end - + end end @@ -4981,17 +8206,17 @@ end function OPSGROUP:SwitchICLS(Channel, Morse, UnitName) if self:IsInUtero() then - + self:SetDefaultICLS(Channel,Morse,UnitName) - + self:T2(self.lid..string.format("Switching ICLS to Channel %d Morse %s on unit %s when GROUP is SPAWNED", self.iclsDefault.Channel, tostring(self.iclsDefault.Morse), tostring(self.iclsDefault.BeaconName))) elseif self:IsAlive() then - + Channel=Channel or self.iclsDefault.Channel Morse=Morse or self.iclsDefault.Morse local unit=self:GetUnit(1) --Wrapper.Unit#UNIT - + if UnitName then if type(UnitName)=="number" then unit=self:GetUnit(UnitName) @@ -4999,7 +8224,7 @@ function OPSGROUP:SwitchICLS(Channel, Morse, UnitName) unit=UNIT:FindByName(UnitName) end end - + if not unit then self:T(self.lid.."WARNING: Could not get ICLS unit. Trying first unit in the group") unit=self:GetUnit(1) @@ -5008,11 +8233,11 @@ function OPSGROUP:SwitchICLS(Channel, Morse, UnitName) if unit and unit:IsAlive() then -- Unit ID. - local UnitID=unit:GetID() + local UnitID=unit:GetID() -- Activate beacon. unit:CommandActivateICLS(Channel, UnitID, Morse) - + -- Update info. self.icls.Channel=Channel self.icls.Morse=Morse @@ -5020,10 +8245,10 @@ function OPSGROUP:SwitchICLS(Channel, Morse, UnitName) self.icls.BeaconName=unit:GetName() self.icls.BeaconUnit=unit self.icls.On=true - + -- Debug info. self:T(self.lid..string.format("Switching ICLS to Channel %d Morse %s on unit %s", self.icls.Channel, tostring(self.icls.Morse), self.icls.BeaconName)) - + else self:E(self.lid.."ERROR: Cound not set ICLS! Unit is not alive.") end @@ -5055,7 +8280,7 @@ end -- @param #boolean OffSwitch If true, radio is OFF by default. -- @return #OPSGROUP self function OPSGROUP:SetDefaultRadio(Frequency, Modulation, OffSwitch) - + self.radioDefault={} self.radioDefault.Freq=Frequency or 251 self.radioDefault.Modu=Modulation or radio.modulation.AM @@ -5064,7 +8289,7 @@ function OPSGROUP:SetDefaultRadio(Frequency, Modulation, OffSwitch) else self.radioDefault.On=true end - + return self end @@ -5085,33 +8310,33 @@ end function OPSGROUP:SwitchRadio(Frequency, Modulation) if self:IsInUtero() then - + -- Set default radio. self:SetDefaultRadio(Frequency, Modulation) - + -- Debug info. self:T2(self.lid..string.format("Switching radio to frequency %.3f MHz %s when GROUP is SPAWNED", self.radioDefault.Freq, UTILS.GetModulationName(self.radioDefault.Modu))) - + elseif self:IsAlive() then - + Frequency=Frequency or self.radioDefault.Freq Modulation=Modulation or self.radioDefault.Modu if self.isAircraft and not self.radio.On then self.group:SetOption(AI.Option.Air.id.SILENCE, false) - end - + end + -- Give command self.group:CommandSetFrequency(Frequency, Modulation) - + -- Update current settings. self.radio.Freq=Frequency - self.radio.Modu=Modulation + self.radio.Modu=Modulation self.radio.On=true - + -- Debug info. self:T(self.lid..string.format("Switching radio to frequency %.3f MHz %s", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu))) - + else self:E(self.lid.."ERROR: Cound not set Radio! Group is not alive or not in utero any more") end @@ -5127,13 +8352,13 @@ function OPSGROUP:TurnOffRadio() if self:IsAlive() then if self.isAircraft then - + -- Set group to be silient. self.group:SetOption(AI.Option.Air.id.SILENCE, true) - + -- Radio is off. self.radio.On=false - + self:T(self.lid..string.format("Switching radio OFF")) else self:E(self.lid.."ERROR: Radio can only be turned off for aircraft!") @@ -5151,7 +8376,7 @@ end -- @param #number Formation The formation the groups flies in. -- @return #OPSGROUP self function OPSGROUP:SetDefaultFormation(Formation) - + self.optionDefault.Formation=Formation return self @@ -5164,25 +8389,25 @@ end function OPSGROUP:SwitchFormation(Formation) if self:IsAlive() then - + Formation=Formation or self.optionDefault.Formation - + if self.isAircraft then self.group:SetOption(AI.Option.Air.id.FORMATION, Formation) - + elseif self.isGround then - + -- Polymorphic and overwritten in ARMYGROUP. - + else self:E(self.lid.."ERROR: Formation can only be set for aircraft or ground units!") return self end - + -- Set current formation. self.option.Formation=Formation - + -- Debug info. self:T(self.lid..string.format("Switching formation to %d", self.option.Formation)) @@ -5196,13 +8421,14 @@ end --- Set default callsign. -- @param #OPSGROUP self -- @param #number CallsignName Callsign name. --- @param #number CallsignNumber Callsign number. +-- @param #number CallsignNumber Callsign number. Default 1. -- @return #OPSGROUP self function OPSGROUP:SetDefaultCallsign(CallsignName, CallsignNumber) - self.callsignDefault={} + self.callsignDefault={} --#OPSGROUP.Callsign self.callsignDefault.NumberSquad=CallsignName self.callsignDefault.NumberGroup=CallsignNumber or 1 + self.callsignDefault.NameSquad=UTILS.GetCallsignName(self.callsign.NumberSquad) return self end @@ -5215,9 +8441,10 @@ end function OPSGROUP:SwitchCallsign(CallsignName, CallsignNumber) if self:IsInUtero() then - + -- Set default callsign. We switch to this when group is spawned. self:SetDefaultCallsign(CallsignName, CallsignNumber) + --self.callsign=UTILS.DeepCopy(self.callsignDefault) elseif self:IsAlive() then @@ -5230,12 +8457,24 @@ function OPSGROUP:SwitchCallsign(CallsignName, CallsignNumber) -- Debug. self:T(self.lid..string.format("Switching callsign to %d-%d", self.callsign.NumberSquad, self.callsign.NumberGroup)) - + -- Give command to change the callsign. self.group:CommandSetCallsign(self.callsign.NumberSquad, self.callsign.NumberGroup) + + -- Callsign of the group, e.g. Colt-1 + self.callsignName=UTILS.GetCallsignName(self.callsign.NumberSquad).."-"..self.callsign.NumberGroup + self.callsign.NameSquad=UTILS.GetCallsignName(self.callsign.NumberSquad) + + -- Set callsign of elements. + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + if element.status~=OPSGROUP.ElementStatus.DEAD then + element.callsign=element.unit:GetCallsign() + end + end else - --TODO: Error + self:E(self.lid.."ERROR: Group is not alive and not in utero! Cannot switch callsign") end return self @@ -5250,38 +8489,40 @@ end -- @return #OPSGROUP self function OPSGROUP:_UpdatePosition() - if self:IsAlive() then - + if self:IsExist() then + -- Backup last state to monitor differences. self.positionLast=self.position or self:GetVec3() self.headingLast=self.heading or self:GetHeading() self.orientXLast=self.orientX or self:GetOrientationX() self.velocityLast=self.velocity or self.group:GetVelocityMPS() - + -- Current state. self.position=self:GetVec3() self.heading=self:GetHeading() self.orientX=self:GetOrientationX() self.velocity=self:GetVelocity() - + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + element.vec3=self:GetVec3(element.name) + end + -- Update time. local Tnow=timer.getTime() self.dTpositionUpdate=self.TpositionUpdate and Tnow-self.TpositionUpdate or 0 self.TpositionUpdate=Tnow - + if not self.traveldist then self.traveldist=0 end - + + -- Travel distance since last check. self.travelds=UTILS.VecNorm(UTILS.VecSubstract(self.position, self.positionLast)) - + -- Add up travelled distance. - self.traveldist=self.traveldist+self.travelds - - -- Debug info. - --env.info(string.format("FF Traveled %.1f m", self.traveldist)) - + end return self @@ -5327,12 +8568,12 @@ function OPSGROUP:_AllSimilarStatus(status) for _,_element in pairs(self.elements) do local element=_element --#OPSGROUP.Element - + self:T2(self.lid..string.format("Status=%s, element %s status=%s", status, element.name, element.status)) -- Dead units dont count ==> We wont return false for those. if element.status~=OPSGROUP.ElementStatus.DEAD then - + ---------- -- ALIVE ---------- @@ -5373,7 +8614,7 @@ function OPSGROUP:_AllSimilarStatus(status) element.status==OPSGROUP.ElementStatus.PARKING or element.status==OPSGROUP.ElementStatus.ENGINEON) then return false - end + end elseif status==OPSGROUP.ElementStatus.TAKEOFF then @@ -5395,7 +8636,7 @@ function OPSGROUP:_AllSimilarStatus(status) element.status==OPSGROUP.ElementStatus.SPAWNED or element.status==OPSGROUP.ElementStatus.PARKING or element.status==OPSGROUP.ElementStatus.ENGINEON or - element.status==OPSGROUP.ElementStatus.TAXIING or + element.status==OPSGROUP.ElementStatus.TAXIING or element.status==OPSGROUP.ElementStatus.TAKEOFF) then return false end @@ -5420,7 +8661,7 @@ function OPSGROUP:_AllSimilarStatus(status) end end - + else -- Element is dead. We don't care unless all are dead. end --DEAD @@ -5429,7 +8670,7 @@ function OPSGROUP:_AllSimilarStatus(status) -- Debug info. self:T2(self.lid..string.format("All %d elements have similar status %s ==> returning TRUE", #self.elements, status)) - + return true end @@ -5445,30 +8686,39 @@ function OPSGROUP:_UpdateStatus(element, newstatus, airbase) -- Update status of element. element.status=newstatus - + -- Debug - self:T3(self.lid..string.format("UpdateStatus element=%s: %s --> %s", element.name, oldstatus, newstatus)) + self:T3(self.lid..string.format("UpdateStatus element=%s: %s --> %s", element.name, oldstatus, newstatus)) for _,_element in pairs(self.elements) do local Element=_element -- #OPSGROUP.Element self:T3(self.lid..string.format("Element %s: %s", Element.name, Element.status)) end - if newstatus==OPSGROUP.ElementStatus.SPAWNED then + if newstatus==OPSGROUP.ElementStatus.INUTERO then + --- + -- INUTERO + --- + + if self:_AllSimilarStatus(newstatus) then + self:InUtero() + end + + elseif newstatus==OPSGROUP.ElementStatus.SPAWNED then --- -- SPAWNED --- if self:_AllSimilarStatus(newstatus) then - self:__Spawned(-0.5) + self:Spawned() end - + elseif newstatus==OPSGROUP.ElementStatus.PARKING then --- -- PARKING --- if self:_AllSimilarStatus(newstatus) then - self:__Parking(-0.5) + self:Parking() end elseif newstatus==OPSGROUP.ElementStatus.ENGINEON then @@ -5484,9 +8734,9 @@ function OPSGROUP:_UpdateStatus(element, newstatus, airbase) --- if self:_AllSimilarStatus(newstatus) then - self:__Taxiing(-0.5) + self:Taxiing() end - + elseif newstatus==OPSGROUP.ElementStatus.TAKEOFF then --- -- TAKEOFF @@ -5494,7 +8744,7 @@ function OPSGROUP:_UpdateStatus(element, newstatus, airbase) if self:_AllSimilarStatus(newstatus) then -- Trigger takeoff event. Also triggers airborne event. - self:__Takeoff(-0.5, airbase) + self:Takeoff(airbase) end elseif newstatus==OPSGROUP.ElementStatus.AIRBORNE then @@ -5503,7 +8753,7 @@ function OPSGROUP:_UpdateStatus(element, newstatus, airbase) --- if self:_AllSimilarStatus(newstatus) then - self:__Airborne(-0.5) + self:Airborne() end elseif newstatus==OPSGROUP.ElementStatus.LANDED then @@ -5541,7 +8791,7 @@ function OPSGROUP:_UpdateStatus(element, newstatus, airbase) --- if self:_AllSimilarStatus(newstatus) then - self:__Dead(-1) + self:Dead() end end @@ -5567,11 +8817,15 @@ end -- @return #OPSGROUP.Element The element. function OPSGROUP:GetElementByName(unitname) - for _,_element in pairs(self.elements) do - local element=_element --#OPSGROUP.Element + if unitname and type(unitname)=="string" then + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + + if element.name==unitname then + return element + end - if element.name==unitname then - return element end end @@ -5579,6 +8833,181 @@ function OPSGROUP:GetElementByName(unitname) return nil end +--- Get the bounding box of the element. +-- @param #OPSGROUP self +-- @param #string UnitName Name of unit. +-- @return Core.Zone#ZONE_POLYGON Bounding box polygon zone. +function OPSGROUP:GetElementZoneBoundingBox(UnitName) + + local element=self:GetElementByName(UnitName) + + if element and element.status~=OPSGROUP.ElementStatus.DEAD then + + -- Create a new zone if necessary. + element.zoneBoundingbox=element.zoneBoundingbox or ZONE_POLYGON_BASE:New(element.name.." Zone Bounding Box", {}) + + -- Length in meters. + local l=element.length + -- Width in meters. + local w=element.width + + -- Orientation vector. + local X=self:GetOrientationX(element.name) + + -- Heading in degrees. + local heading=math.deg(math.atan2(X.z, X.x)) + + -- Debug info. + self:T(self.lid..string.format("Element %s bouding box: l=%d w=%d heading=%d", element.name, l, w, heading)) + + -- Set of edges facing "North" at the origin of the map. + local b={} + b[1]={x=l/2, y=-w/2} --DCS#Vec2 + b[2]={x=l/2, y=w/2} --DCS#Vec2 + b[3]={x=-l/2, y=w/2} --DCS#Vec2 + b[4]={x=-l/2, y=-w/2} --DCS#Vec2 + + -- Rotate box to match current heading of the unit. + for i,p in pairs(b) do + b[i]=UTILS.Vec2Rotate2D(p, heading) + end + + -- Translate the zone to the positon of the unit. + local vec2=self:GetVec2(element.name) + local d=UTILS.Vec2Norm(vec2) + local h=UTILS.Vec2Hdg(vec2) + for i,p in pairs(b) do + b[i]=UTILS.Vec2Translate(p, d, h) + end + + -- Update existing zone. + element.zoneBoundingbox:UpdateFromVec2(b) + + return element.zoneBoundingbox + end + + return nil +end + +--- Get the loading zone of the element. +-- @param #OPSGROUP self +-- @param #string UnitName Name of unit. +-- @return Core.Zone#ZONE_POLYGON Bounding box polygon zone. +function OPSGROUP:GetElementZoneLoad(UnitName) + + local element=self:GetElementByName(UnitName) + + if element and element.status~=OPSGROUP.ElementStatus.DEAD then + + element.zoneLoad=element.zoneLoad or ZONE_POLYGON_BASE:New(element.name.." Zone Load", {}) + + self:_GetElementZoneLoader(element, element.zoneLoad, self.carrierLoader) + + return element.zoneLoad + end + + return nil +end + +--- Get the unloading zone of the element. +-- @param #OPSGROUP self +-- @param #string UnitName Name of unit. +-- @return Core.Zone#ZONE_POLYGON Bounding box polygon zone. +function OPSGROUP:GetElementZoneUnload(UnitName) + + local element=self:GetElementByName(UnitName) + + if element and element.status~=OPSGROUP.ElementStatus.DEAD then + + element.zoneUnload=element.zoneUnload or ZONE_POLYGON_BASE:New(element.name.." Zone Unload", {}) + + self:_GetElementZoneLoader(element, element.zoneUnload, self.carrierUnloader) + + return element.zoneUnload + end + + return nil +end + +--- Get/update the (un-)loading zone of the element. +-- @param #OPSGROUP self +-- @param #OPSGROUP.Element Element Element. +-- @param Core.Zone#ZONE_POLYGON Zone The zone. +-- @param #OPSGROUP.CarrierLoader Loader Loader parameters. +-- @return Core.Zone#ZONE_POLYGON Bounding box polygon zone. +function OPSGROUP:_GetElementZoneLoader(Element, Zone, Loader) + + if Element.status~=OPSGROUP.ElementStatus.DEAD then + + local l=Element.length + local w=Element.width + + -- Orientation 3D vector where the "nose" is pointing. + local X=self:GetOrientationX(Element.name) + + -- Heading in deg. + local heading=math.deg(math.atan2(X.z, X.x)) + + -- Bounding box at the origin of the map facing "North". + local b={} + + -- Create polygon rectangles. + if Loader.type:lower()=="front" then + table.insert(b, {x= l/2, y=-Loader.width/2}) -- left, low + table.insert(b, {x= l/2+Loader.length, y=-Loader.width/2}) -- left, up + table.insert(b, {x= l/2+Loader.length, y= Loader.width/2}) -- right, up + table.insert(b, {x= l/2, y= Loader.width/2}) -- right, low + elseif Loader.type:lower()=="back" then + table.insert(b, {x=-l/2, y=-Loader.width/2}) -- left, low + table.insert(b, {x=-l/2-Loader.length, y=-Loader.width/2}) -- left, up + table.insert(b, {x=-l/2-Loader.length, y= Loader.width/2}) -- right, up + table.insert(b, {x=-l/2, y= Loader.width/2}) -- right, low + elseif Loader.type:lower()=="left" then + table.insert(b, {x= Loader.length/2, y= -w/2}) -- right, up + table.insert(b, {x= Loader.length/2, y= -w/2-Loader.width}) -- left, up + table.insert(b, {x=-Loader.length/2, y= -w/2-Loader.width}) -- left, down + table.insert(b, {x=-Loader.length/2, y= -w/2}) -- right, down + elseif Loader.type:lower()=="right" then + table.insert(b, {x= Loader.length/2, y= w/2}) -- right, up + table.insert(b, {x= Loader.length/2, y= w/2+Loader.width}) -- left, up + table.insert(b, {x=-Loader.length/2, y= w/2+Loader.width}) -- left, down + table.insert(b, {x=-Loader.length/2, y= w/2}) -- right, down + else + -- All aspect. Rectangle around the unit but need to cut out the area of the unit itself. + b[1]={x= l/2, y=-w/2} --DCS#Vec2 + b[2]={x= l/2, y= w/2} --DCS#Vec2 + b[3]={x=-l/2, y= w/2} --DCS#Vec2 + b[4]={x=-l/2, y=-w/2} --DCS#Vec2 + table.insert(b, {x=b[1].x+Loader.length, y=b[1].y-Loader.width}) + table.insert(b, {x=b[2].x+Loader.length, y=b[2].y+Loader.width}) + table.insert(b, {x=b[3].x-Loader.length, y=b[3].y+Loader.width}) + table.insert(b, {x=b[4].x-Loader.length, y=b[4].y-Loader.width}) + end + + -- Rotate edges to match the current heading of the unit. + for i,p in pairs(b) do + b[i]=UTILS.Vec2Rotate2D(p, heading) + end + + -- Translate box to the current position of the unit. + local vec2=self:GetVec2(Element.name) + local d=UTILS.Vec2Norm(vec2) + local h=UTILS.Vec2Hdg(vec2) + + for i,p in pairs(b) do + b[i]=UTILS.Vec2Translate(p, d, h) + end + + -- Update existing zone. + Zone:UpdateFromVec2(b) + + return Zone + end + + return nil +end + + --- Get the first element of a group, which is alive. -- @param #OPSGROUP self -- @return #OPSGROUP.Element The element or `#nil` if no element is alive any more. @@ -5588,7 +9017,7 @@ function OPSGROUP:GetElementAlive() local element=_element --#OPSGROUP.Element if element.status~=OPSGROUP.ElementStatus.DEAD then if element.unit and element.unit:IsAlive() then - return element + return element end end end @@ -5596,6 +9025,7 @@ function OPSGROUP:GetElementAlive() return nil end + --- Get number of elements alive. -- @param #OPSGROUP self -- @param #string status (Optional) Only count number, which are in a special status. @@ -5614,7 +9044,7 @@ function OPSGROUP:GetNelements(status) end end - + return n end @@ -5632,7 +9062,7 @@ end function OPSGROUP:GetAmmoTot() local units=self.group:GetUnits() - + local Ammo={} --#OPSGROUP.Ammo Ammo.Total=0 Ammo.Guns=0 @@ -5645,15 +9075,15 @@ function OPSGROUP:GetAmmoTot() Ammo.MissilesAS=0 Ammo.MissilesCR=0 Ammo.MissilesSA=0 - + for _,_unit in pairs(units) do local unit=_unit --Wrapper.Unit#UNIT - + if unit and unit:IsAlive()~=nil then - + -- Get ammo of the unit. local ammo=self:GetAmmoUnit(unit) - + -- Add up total. Ammo.Total=Ammo.Total+ammo.Total Ammo.Guns=Ammo.Guns+ammo.Guns @@ -5666,9 +9096,9 @@ function OPSGROUP:GetAmmoTot() Ammo.MissilesAS=Ammo.MissilesAS+ammo.MissilesAS Ammo.MissilesCR=Ammo.MissilesCR+ammo.MissilesCR Ammo.MissilesSA=Ammo.MissilesSA+ammo.MissilesSA - + end - + end return Ammo @@ -5764,7 +9194,7 @@ function OPSGROUP:GetAmmoUnit(unit, display) nmissilesAA=nmissilesAA+Nammo elseif MissileCategory==Weapon.MissileCategory.SAM then nmissiles=nmissiles+Nammo - nmissilesSA=nmissilesSA+Nammo + nmissilesSA=nmissilesSA+Nammo elseif MissileCategory==Weapon.MissileCategory.ANTI_SHIP then nmissiles=nmissiles+Nammo nmissilesAS=nmissilesAS+Nammo @@ -5773,7 +9203,7 @@ function OPSGROUP:GetAmmoUnit(unit, display) nmissilesAG=nmissilesAG+Nammo elseif MissileCategory==Weapon.MissileCategory.CRUISE then nmissiles=nmissiles+Nammo - nmissilesCR=nmissilesCR+Nammo + nmissilesCR=nmissilesCR+Nammo elseif MissileCategory==Weapon.MissileCategory.OTHER then nmissiles=nmissiles+Nammo nmissilesAG=nmissilesAG+Nammo @@ -5781,11 +9211,11 @@ function OPSGROUP:GetAmmoUnit(unit, display) -- Debug info. text=text..string.format("- %d %s missiles of type %s\n", Nammo, self:_MissileCategoryName(MissileCategory), _weaponName) - + elseif Category==Weapon.Category.TORPEDO then - + -- Add up all rockets. - ntorps=ntorps+Nammo + ntorps=ntorps+Nammo -- Debug info. text=text..string.format("- %d torpedos of type %s\n", Nammo, _weaponName) @@ -5854,21 +9284,203 @@ end -- @param Wrapper.Object#OBJECT Object The object. -- @return Core.Point#COORDINATE The coordinate of the object. function OPSGROUP:_CoordinateFromObject(Object) - - if Object:IsInstanceOf("COORDINATE") then - return Object - else - if Object:IsInstanceOf("POSITIONABLE") or Object:IsInstanceOf("ZONE_BASE") then - self:T(self.lid.."WARNING: Coordinate is not a COORDINATE but a POSITIONABLE or ZONE. Trying to get coordinate") - return Object:GetCoordinate() + + if Object then + if Object:IsInstanceOf("COORDINATE") then + return Object else - self:E(self.lid.."ERROR: Coordinate is neither a COORDINATE nor any POSITIONABLE or ZONE!") + if Object:IsInstanceOf("POSITIONABLE") or Object:IsInstanceOf("ZONE_BASE") then + self:T(self.lid.."WARNING: Coordinate is not a COORDINATE but a POSITIONABLE or ZONE. Trying to get coordinate") + return Object:GetCoordinate() + else + self:E(self.lid.."ERROR: Coordinate is neither a COORDINATE nor any POSITIONABLE or ZONE!") + end end - end + else + self:E(self.lid.."ERROR: Object passed is nil!") + end return nil end +--- Check if a unit is an element of the flightgroup. +-- @param #OPSGROUP self +-- @param #string unitname Name of unit. +-- @return #boolean If true, unit is element of the flight group or false if otherwise. +function OPSGROUP:_IsElement(unitname) + + for _,_element in pairs(self.elements) do + local element=_element --Ops.OpsGroup#OPSGROUP.Element + + if element.name==unitname then + return true + end + + end + + return false +end + +--- Add a unit/element to the OPS group. +-- @param #OPSGROUP self +-- @param #string unitname Name of unit. +-- @return #OPSGROUP.Element The element or nil. +function OPSGROUP:_AddElementByName(unitname) + + local unit=UNIT:FindByName(unitname) + + if unit then + + -- Get unit template. + local unittemplate=unit:GetTemplate() + + -- Element table. + local element={} --#OPSGROUP.Element + + -- Name and status. + element.name=unitname + element.status=OPSGROUP.ElementStatus.INUTERO + + -- Unit and group. + element.unit=unit + element.DCSunit=Unit.getByName(unitname) + element.gid=element.DCSunit:getNumber() + element.uid=element.DCSunit:getID() + element.group=unit:GetGroup() + element.opsgroup=self + + -- Skill etc. + element.skill=unittemplate.skill or "Unknown" + if element.skill=="Client" or element.skill=="Player" then + element.ai=false + element.client=CLIENT:FindByName(unitname) + else + element.ai=true + end + + -- Descriptors and type/category. + element.descriptors=unit:GetDesc() + element.category=unit:GetUnitCategory() + element.categoryname=unit:GetCategoryName() + element.typename=unit:GetTypeName() + --self:I({desc=element.descriptors}) + + -- Ammo. + element.ammo0=self:GetAmmoUnit(unit, false) + + -- Life points. + element.life=unit:GetLife() + element.life0=math.max(unit:GetLife0(), element.life) -- Some units report a life0 that is smaller than its initial life points. + + -- Size and dimensions. + element.size, element.length, element.height, element.width=unit:GetObjectSize() + + -- Weight and cargo. + element.weightEmpty=element.descriptors.massEmpty or 666 + element.weightCargo=0 + element.weight=element.weightEmpty+element.weightCargo + + -- Looks like only aircraft have a massMax value in the descriptors. + element.weightMaxTotal=element.descriptors.massMax or element.weightEmpty+8*95 --If max mass is not given, we assume 8 soldiers. + + + if self.isArmygroup then + + element.weightMaxTotal=element.weightEmpty+10*95 --If max mass is not given, we assume 10 soldiers. + + elseif self.isNavygroup then + + element.weightMaxTotal=element.weightEmpty+10*1000 + + end + + -- Max cargo weight: + unit:SetCargoBayWeightLimit() + element.weightMaxCargo=unit.__.CargoBayWeightLimit + + -- Cargo bay (empty). + element.cargoBay={} + + -- FLIGHTGROUP specific. + if self.isFlightgroup then + element.callsign=element.unit:GetCallsign() + element.modex=unittemplate.onboard_num + element.payload=unittemplate.payload + element.pylons=unittemplate.payload and unittemplate.payload.pylons or nil + element.fuelmass0=unittemplate.payload and unittemplate.payload.fuel or 0 + element.fuelmass=element.fuelmass0 + element.fuelrel=element.unit:GetFuel() + else + element.callsign="Peter-1-1" + element.modex="000" + element.payload={} + element.pylons={} + element.fuelmass0=99999 + element.fuelmass =99999 + element.fuelrel=1 + end + + -- Debug text. + local text=string.format("Adding element %s: status=%s, skill=%s, life=%.1f/%.1f category=%s (%d), type=%s, size=%.1f (L=%.1f H=%.1f W=%.1f), weight=%.1f/%.1f (cargo=%.1f/%.1f)", + element.name, element.status, element.skill, element.life, element.life0, element.categoryname, element.category, element.typename, + element.size, element.length, element.height, element.width, element.weight, element.weightMaxTotal, element.weightCargo, element.weightMaxCargo) + self:T(self.lid..text) + + -- Add element to table. + if not self:_IsElement(unitname) then + table.insert(self.elements, element) + end + + -- Trigger spawned event if alive. + if unit:IsAlive() then + -- This needs to be slightly delayed (or moved elsewhere) or the first element will always trigger the group spawned event as it is not known that more elements are in the group. + self:__ElementSpawned(0.05, element) + end + + return element + end + + return nil +end + +--- Set the template of the group. +-- @param #OPSGROUP self +-- @param #table Template Template to set. Default is from the GROUP. +-- @return #OPSGROUP self +function OPSGROUP:_SetTemplate(Template) + + -- Set the template. + self.template=Template or self.group:GetTemplate() + + -- Debug info. + self:T3(self.lid.."Setting group template") + + return self +end + +--- Get the template of the group. +-- @param #OPSGROUP self +-- @param #boolean Copy Get a deep copy of the template. +-- @return #table Template table. +function OPSGROUP:_GetTemplate(Copy) + + if self.template then + + if Copy then + local template=UTILS.DeepCopy(self.template) + return template + else + return self.template + end + + else + self:E(self.lid..string.format("ERROR: No template was set yet!")) + end + + return nil +end + + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/OpsTransport.lua b/Moose Development/Moose/Ops/OpsTransport.lua new file mode 100644 index 000000000..b6a2e029c --- /dev/null +++ b/Moose Development/Moose/Ops/OpsTransport.lua @@ -0,0 +1,1180 @@ +--- **Ops** - Troop transport assignment for OPS groups. +-- +-- ## Main Features: +-- +-- * Transport troops from A to B. +-- * Supports ground, naval and airborne (airplanes and helicopters) units as carriers +-- * Use combined forces (ground, naval, air) to transport the troops. +-- * Additional FSM events to hook into and customize your mission design. +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20Transport). +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- == +-- +-- @module Ops.OpsTransport +-- @image OPS_OpsTransport.png + + +--- OPSTRANSPORT class. +-- @type OPSTRANSPORT +-- @field #string ClassName Name of the class. +-- @field #string lid Log ID. +-- @field #number uid Unique ID of the transport. +-- @field #number verbose Verbosity level. +-- @field #table cargos Cargos. Each element is a @{Ops.OpsGroup#OPSGROUP.CargoGroup}. +-- @field #table carriers Carriers assigned for this transport. +-- @field #number prio Priority of this transport. Should be a number between 0 (high prio) and 100 (low prio). +-- @field #boolean urgent If true, transport is urgent. +-- @field #number importance Importance of this transport. Smaller=higher. +-- @field #number Tstart Start time in *abs.* seconds. +-- @field #number Tstop Stop time in *abs.* seconds. Default `#nil` (never stops). +-- @field #number duration Duration (`Tstop-Tstart`) of the transport in seconds. +-- @field #table conditionStart Start conditions. +-- @field Core.Zone#ZONE pickupzone Zone where the cargo is picked up. +-- @field Core.Zone#ZONE deployzone Zone where the cargo is dropped off. +-- @field Core.Zone#ZONE embarkzone (Optional) Zone where the cargo is supposed to embark. Default is the pickup zone. +-- @field Core.Zone#ZONE disembarkzone (Optional) Zone where the cargo is disembarked. Default is the deploy zone. +-- @field Core.Zone#ZONE unboardzone (Optional) Zone where the cargo is going to after disembarkment. +-- @field #boolean disembarkActivation Activation setting when group is disembared from carrier. +-- @field #boolean disembarkInUtero Do not spawn the group in any any state but leave it "*in utero*". For example, to directly load it into another carrier. +-- @field #table disembarkCarriers Table of carriers to which the cargo is disembared. This is a direct transfer from the old to the new carrier. +-- @field #table requiredCargos Table of cargo groups that must be loaded before the first transport is started. +-- @field #number Ncargo Total number of cargo groups. +-- @field #number Ncarrier Total number of assigned carriers. +-- @field #number Ndelivered Total number of cargo groups delivered. +-- @field #table pathsTransport Transport paths of `#OPSGROUP.Path`. +-- @field #table pathsPickup Pickup paths of `#OPSGROUP.Path`. +-- @extends Core.Fsm#FSM + +--- *Victory is the beautiful, bright-colored flower. Transport is the stem without which it could never have blossomed.* -- Winston Churchill +-- +-- === +-- +-- ![Banner Image](..\Presentations\OPS\Transport\_Main.png) +-- +-- # The OPSTRANSPORT Concept +-- +-- This class simulates troop transport using carriers such as APCs, ships, helicopters or airplanes. The carriers and transported groups need to be OPSGROUPS (see ARMYGROUP, NAVYGROUP and FLIGHTGROUP classes). +-- +-- **IMPORTANT NOTES** +-- +-- * Cargo groups are **not** split and distributed into different carrier *units*. That means that the whole cargo group **must fit** into one of the carrier units. +-- * Cargo groups must be inside the pickup zones to be considered for loading. Groups not inside the pickup zone will not get the command to board. +-- +-- # Constructor +-- +-- A new cargo transport assignment is created with the @{#OPSTRANSPORT.New}() function +-- +-- local opstransport=OPSTRANSPORT:New(Cargo, PickupZone, DeployZone) +-- +-- Here `Cargo` is an object of the troops to be transported. This can be a GROUP, OPSGROUP, SET_GROUP or SET_OPSGROUP object. +-- +-- `PickupZone` is the zone where the troops are picked up by the transport carriers. **Note** that troops *must* be inside this zone to be considered for loading! +-- +-- `DeployZone` is the zone where the troops are transported to. +-- +-- ## Assign to Carrier(s) +-- +-- A transport can be assigned to one or multiple carrier OPSGROUPS with this @{Ops.OpsGroup#OPSGROUP.AddOpsTransport}() function +-- +-- myopsgroup:AddOpsTransport(opstransport) +-- +-- There is no restriction to the type of the carrier. It can be a ground group (e.g. an APC), a helicopter, an airplane or even a ship. +-- +-- You can also mix carrier types. For instance, you can assign the same transport to APCs and helicopters. Or to helicopters and airplanes. +-- +-- # Examples +-- +-- A carrier group is assigned to transport infantry troops from zone "Zone Kobuleti X" to zone "Zone Alpha". +-- +-- -- Carrier group. +-- local carrier=ARMYGROUP:New("TPz Fuchs Group") +-- +-- -- Set of groups to transport. +-- local infantryset=SET_GROUP:New():FilterPrefixes("Infantry Platoon Alpha"):FilterOnce() +-- +-- -- Cargo transport assignment. +-- local opstransport=OPSTRANSPORT:New(infantryset, ZONE:New("Zone Kobuleti X"), ZONE:New("Zone Alpha")) +-- +-- -- Assign transport to carrier. +-- carrier:AddOpsTransport(opstransport) +-- +-- +-- @field #OPSTRANSPORT +OPSTRANSPORT = { + ClassName = "OPSTRANSPORT", + verbose = 0, + cargos = {}, + carriers = {}, + carrierTransportStatus = {}, + conditionStart = {}, + pathsTransport = {}, + pathsPickup = {}, + requiredCargos = {}, +} + +--- Cargo transport status. +-- @type OPSTRANSPORT.Status +-- @field #string PLANNED Planning state. +-- @field #string SCHEDULED Transport is scheduled in the cargo queue. +-- @field #string EXECUTING Transport is being executed. +-- @field #string DELIVERED Transport was delivered. +OPSTRANSPORT.Status={ + PLANNED="planned", + SCHEDULED="scheduled", + EXECUTING="executing", + DELIVERED="delivered", +} + +--- Path. +-- @type OPSTRANSPORT.Path +-- @field #table coords Table of coordinates. +-- @field #number radius Radomization radius in meters. Default 0 m. +-- @field #number altitude Altitude in feet AGL. Only for aircraft. + +--- Generic mission condition. +-- @type OPSTRANSPORT.Condition +-- @field #function func Callback function to check for a condition. Should return a #boolean. +-- @field #table arg Optional arguments passed to the condition callback function. + +--- Transport ID. +_OPSTRANSPORTID=0 + +--- Army Group version. +-- @field #string version +OPSTRANSPORT.version="0.3.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Stop/abort transport. +-- DONE: Add start conditions. +-- DONE: Check carrier(s) dead. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new OPSTRANSPORT class object. Essential input are the troops that should be transported and the zones where the troops are picked up and deployed. +-- @param #OPSTRANSPORT self +-- @param Core.Set#SET_GROUP GroupSet Set of groups to be transported. Can also be a single @{Wrapper.Group#GROUP} or @{Ops.OpsGroup#OPSGROUP} object. +-- @param Core.Zone#ZONE Pickupzone Pickup zone. This is the zone, where the carrier is going to pickup the cargo. **Important**: only cargo is considered, if it is in this zone when the carrier starts loading! +-- @param Core.Zone#ZONE Deployzone Deploy zone. This is the zone, where the carrier is going to drop off the cargo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:New(GroupSet, Pickupzone, Deployzone) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #OPSTRANSPORT + + -- Increase ID counter. + _OPSTRANSPORTID=_OPSTRANSPORTID+1 + + -- Set some string id for output to DCS.log file. + self.lid=string.format("OPSTRANSPORT [UID=%d] | ", _OPSTRANSPORTID) + + -- Defaults. + self.uid=_OPSTRANSPORTID + + self.pickupzone=Pickupzone + self.deployzone=Deployzone + self.embarkzone=Pickupzone + self.cargos={} + self.carriers={} + self.Ncargo=0 + self.Ncarrier=0 + self.Ndelivered=0 + + self:SetPriority() + self:SetTime() + + -- Add cargo groups (could also be added later). + if GroupSet then + self:AddCargoGroups(GroupSet) + end + + + -- FMS start state is PLANNED. + self:SetStartState(OPSTRANSPORT.Status.PLANNED) + + -- PLANNED --> SCHEDULED --> EXECUTING --> DELIVERED + self:AddTransition("*", "Planned", OPSTRANSPORT.Status.PLANNED) -- Cargo transport was planned. + self:AddTransition(OPSTRANSPORT.Status.PLANNED, "Scheduled", OPSTRANSPORT.Status.SCHEDULED) -- Cargo is queued at at least one carrier. + self:AddTransition(OPSTRANSPORT.Status.SCHEDULED, "Executing", OPSTRANSPORT.Status.EXECUTING) -- Cargo is being transported. + self:AddTransition("*", "Delivered", OPSTRANSPORT.Status.DELIVERED) -- Cargo was delivered. + + self:AddTransition("*", "Status", "*") + self:AddTransition("*", "Stop", "*") + + self:AddTransition("*", "Loaded", "*") + self:AddTransition("*", "Unloaded", "*") + + self:AddTransition("*", "DeadCarrierUnit", "*") + self:AddTransition("*", "DeadCarrierGroup", "*") + self:AddTransition("*", "DeadCarrierAll", "*") + + -- Call status update. + self:__Status(-1) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add cargo groups to be transported. +-- @param #OPSTRANSPORT self +-- @param Core.Set#SET_GROUP GroupSet Set of groups to be transported. Can also be passed as a single GROUP or OPSGROUP object. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:AddCargoGroups(GroupSet) + + -- Check type of GroupSet provided. + if GroupSet:IsInstanceOf("GROUP") or GroupSet:IsInstanceOf("OPSGROUP") then + + -- We got a single GROUP or OPSGROUP object. + local cargo=self:_CreateCargoGroupData(GroupSet) + + if cargo then + table.insert(self.cargos, cargo) + self.Ncargo=self.Ncargo+1 + end + + else + + -- We got a SET_GROUP object. + + for _,group in pairs(GroupSet.Set) do + + local cargo=self:_CreateCargoGroupData(group) + + if cargo then + table.insert(self.cargos, cargo) + self.Ncargo=self.Ncargo+1 + end + + end + end + + -- Debug info. + if self.verbose>=1 then + local text=string.format("Added cargo groups:") + local Weight=0 + for _,_cargo in pairs(self.cargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + local weight=cargo.opsgroup:GetWeightTotal() + Weight=Weight+weight + text=text..string.format("\n- %s [%s] weight=%.1f kg", cargo.opsgroup:GetName(), cargo.opsgroup:GetState(), weight) + end + text=text..string.format("\nTOTAL: Ncargo=%d, Weight=%.1f kg", #self.cargos, Weight) + self:I(self.lid..text) + end + + + return self +end + +--- Set embark zone. +-- @param #OPSTRANSPORT self +-- @param Core.Zone#ZONE EmbarkZone Zone where the troops are embarked. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetEmbarkZone(EmbarkZone) + self.embarkzone=EmbarkZone or self.pickupzone + return self +end + +--- Set disembark zone. +-- @param #OPSTRANSPORT self +-- @param Core.Zone#ZONE DisembarkZone Zone where the troops are disembarked. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetDisembarkZone(DisembarkZone) + self.disembarkzone=DisembarkZone + return self +end + +--- Set activation status of group when disembarked from transport carrier. +-- @param #OPSTRANSPORT self +-- @param #boolean Active If `true` or `nil`, group is activated when disembarked. If `false`, group is late activated and needs to be activated manually. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetDisembarkActivation(Active) + if Active==true or Active==nil then + self.disembarkActivation=true + else + self.disembarkActivation=false + end + return self +end + +--- Set transfer carrier(s). These are carrier groups, where the cargo is directly loaded into when disembarked. +-- @param #OPSTRANSPORT self +-- @param Core.Set#SET_GROUP Carriers Carrier set. Can also be passed as a #GROUP, #OPSGROUP or #SET_OPSGROUP object. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetDisembarkCarriers(Carriers) + + -- Debug info. + self:T(self.lid.."Setting transfer carriers!") + + -- Create table. + self.disembarkCarriers=self.disembarkCarriers or {} + + if Carriers:IsInstanceOf("GROUP") or Carriers:IsInstanceOf("OPSGROUP") then + + local carrier=self:_GetOpsGroupFromObject(Carriers) + if carrier then + table.insert(self.disembarkCarriers, carrier) + end + + elseif Carriers:IsInstanceOf("SET_GROUP") or Carriers:IsInstanceOf("SET_OPSGROUP") then + + for _,object in pairs(Carriers:GetSet()) do + local carrier=self:_GetOpsGroupFromObject(object) + if carrier then + table.insert(self.disembarkCarriers, carrier) + end + end + + else + self:E(self.lid.."ERROR: Carriers must be a GROUP, OPSGROUP, SET_GROUP or SET_OPSGROUP object!") + end + + return self +end + + +--- Set if group remains *in utero* after disembarkment from carrier. Can be used to directly load the group into another carrier. Similar to disembark in late activated state. +-- @param #OPSTRANSPORT self +-- @param #boolean InUtero If `true` or `nil`, group remains *in utero* after disembarkment. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetDisembarkInUtero(InUtero) + if InUtero==true or InUtero==nil then + self.disembarkInUtero=true + else + self.disembarkInUtero=false + end + return self +end + + +--- Set required cargo. This is a list of cargo groups that need to be loaded before the **first** transport will start. +-- @param #OPSTRANSPORT self +-- @param Core.Set#SET_GROUP Cargos Required cargo set. Can also be passed as a #GROUP, #OPSGROUP or #SET_OPSGROUP object. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetRequiredCargos(Cargos) + + -- Debug info. + self:T(self.lid.."Setting required cargos!") + + -- Create table. + self.requiredCargos=self.requiredCargos or {} + + if Cargos:IsInstanceOf("GROUP") or Cargos:IsInstanceOf("OPSGROUP") then + + local cargo=self:_GetOpsGroupFromObject(Cargos) + if cargo then + table.insert(self.requiredCargos, cargo) + end + + elseif Cargos:IsInstanceOf("SET_GROUP") or Cargos:IsInstanceOf("SET_OPSGROUP") then + + for _,object in pairs(Cargos:GetSet()) do + local cargo=self:_GetOpsGroupFromObject(object) + if cargo then + table.insert(self.requiredCargos, cargo) + end + end + + else + self:E(self.lid.."ERROR: Required Cargos must be a GROUP, OPSGROUP, SET_GROUP or SET_OPSGROUP object!") + end + + return self +end + + + +--- Add a carrier assigned for this transport. +-- @param #OPSTRANSPORT self +-- @param Ops.OpsGroup#OPSGROUP CarrierGroup Carrier OPSGROUP. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:_AddCarrier(CarrierGroup) + + -- Check that this is not already an assigned carrier. + if not self:IsCarrier(CarrierGroup) then + + -- Increase carrier count. + self.Ncarrier=self.Ncarrier+1 + + -- Set transport status to SCHEDULED. + self:SetCarrierTransportStatus(CarrierGroup, OPSTRANSPORT.Status.SCHEDULED) + + -- Call scheduled event. + self:Scheduled() + + -- Add carrier to table. + table.insert(self.carriers, CarrierGroup) + + end + + return self +end + +--- Remove group from the current carrier list/table. +-- @param #OPSTRANSPORT self +-- @param Ops.OpsGroup#OPSGROUP CarrierGroup Carrier OPSGROUP. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:_DelCarrier(CarrierGroup) + + if self:IsCarrier(CarrierGroup) then + + for i=#self.carriers,1,-1 do + local carrier=self.carriers[i] --Ops.OpsGroup#OPSGROUP + if carrier.groupname==CarrierGroup.groupname then + self:T(self.lid..string.format("Removing carrier %s", CarrierGroup.groupname)) + table.remove(self.carriers, i) + end + end + + if #self.carriers==0 then + self:DeadCarrierAll() + end + + end + + return self +end + +--- Get a list of alive carriers. +-- @param #OPSTRANSPORT self +-- @return #table Names of all carriers +function OPSTRANSPORT:_GetCarrierNames() + + local names={} + for _,_carrier in pairs(self.carriers) do + local carrier=_carrier --Ops.OpsGroup#OPSGROUP + if carrier:IsAlive()~=nil then + table.insert(names, carrier.groupname) + end + end + + return names +end + +--- Get (all) cargo @{Ops.OpsGroup#OPSGROUP}s. Optionally, only delivered or undelivered groups can be returned. +-- @param #OPSTRANSPORT self +-- @param #boolean Delivered If `true`, only delivered groups are returned. If `false` only undelivered groups are returned. If `nil`, all groups are returned. +-- @return #table Cargo Ops groups. +function OPSTRANSPORT:GetCargoOpsGroups(Delivered) + + local opsgroups={} + for _,_cargo in pairs(self.cargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + if Delivered==nil or cargo.delivered==Delivered then + if cargo.opsgroup and not (cargo.opsgroup:IsDead() or cargo.opsgroup:IsStopped()) then + table.insert(opsgroups, cargo.opsgroup) + end + end + end + + return opsgroups +end + +--- Get carrier @{Ops.OpsGroup#OPSGROUP}s. +-- @param #OPSTRANSPORT self +-- @return #table Carrier Ops groups. +function OPSTRANSPORT:GetCarrierOpsGroups() + return self.carriers +end + + +--- Set transport start and stop time. +-- @param #OPSTRANSPORT self +-- @param #string ClockStart Time the transport is started, e.g. "05:00" for 5 am. If specified as a #number, it will be relative (in seconds) to the current mission time. Default is 5 seconds after mission was added. +-- @param #string ClockStop (Optional) Time the transport is stopped, e.g. "13:00" for 1 pm. If mission could not be started at that time, it will be removed from the queue. If specified as a #number it will be relative (in seconds) to the current mission time. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetTime(ClockStart, ClockStop) + + -- Current mission time. + local Tnow=timer.getAbsTime() + + -- Set start time. Default in 5 sec. + local Tstart=Tnow+5 + if ClockStart and type(ClockStart)=="number" then + Tstart=Tnow+ClockStart + elseif ClockStart and type(ClockStart)=="string" then + Tstart=UTILS.ClockToSeconds(ClockStart) + end + + -- Set stop time. Default nil. + local Tstop=nil + if ClockStop and type(ClockStop)=="number" then + Tstop=Tnow+ClockStop + elseif ClockStop and type(ClockStop)=="string" then + Tstop=UTILS.ClockToSeconds(ClockStop) + end + + self.Tstart=Tstart + self.Tstop=Tstop + + if Tstop then + self.duration=self.Tstop-self.Tstart + end + + return self +end + +--- Set mission priority and (optional) urgency. Urgent missions can cancel other running missions. +-- @param #OPSTRANSPORT self +-- @param #number Prio Priority 1=high, 100=low. Default 50. +-- @param #number Importance Number 1-10. If missions with lower value are in the queue, these have to be finished first. Default is `nil`. +-- @param #boolean Urgent If *true*, another running mission might be cancelled if it has a lower priority. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetPriority(Prio, Importance, Urgent) + self.prio=Prio or 50 + self.urgent=Urgent + self.importance=Importance + return self +end + +--- Set verbosity. +-- @param #OPSTRANSPORT self +-- @param #number Verbosity Be more verbose. Default 0 +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetVerbosity(Verbosity) + self.verbose=Verbosity or 0 + return self +end + +--- Add start condition. +-- @param #OPSTRANSPORT self +-- @param #function ConditionFunction Function that needs to be true before the transport can be started. Must return a #boolean. +-- @param ... Condition function arguments if any. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:AddConditionStart(ConditionFunction, ...) + + if ConditionFunction then + + local condition={} --#OPSTRANSPORT.Condition + + condition.func=ConditionFunction + condition.arg={} + if arg then + condition.arg=arg + end + + table.insert(self.conditionStart, condition) + + end + + return self +end + +--- Add path used for transportation from the pickup to the deploy zone. If multiple paths are defined, a random one is chosen. +-- @param #OPSTRANSPORT self +-- @param Wrapper.Group#GROUP PathGroup A (late activated) GROUP defining a transport path by their waypoints. +-- @param #boolean Reversed If `true`, add waypoints of group in reversed order. +-- @param #number Radius Randomization radius in meters. Default 0 m. +-- @param #number Altitude Altitude in feet AGL. Only for aircraft. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:AddPathTransport(PathGroup, Reversed, Radius, Altitude) + + local path={} --#OPSTRANSPORT.Path + path.coords={} + path.radius=Radius or 0 + path.altitude=Altitude + + -- Get route points. + local waypoints=PathGroup:GetTaskRoute() + + if Reversed then + for i=#waypoints,1,-1 do + local wp=waypoints[i] + local coord=COORDINATE:New(wp.x, wp.alt, wp.y) + table.insert(path.coords, coord) + end + else + for i=1,#waypoints do + local wp=waypoints[i] + local coord=COORDINATE:New(wp.x, wp.alt, wp.y) + table.insert(path.coords, coord) + end + end + + + -- Add path. + table.insert(self.pathsTransport, path) + + return self +end + +--- Get a path for transportation. +-- @param #OPSTRANSPORT self +-- @return #table The path of COORDINATEs. +function OPSTRANSPORT:_GetPathTransport() + + if self.pathsTransport and #self.pathsTransport>0 then + + -- Get a random path for transport. + local path=self.pathsTransport[math.random(#self.pathsTransport)] --#OPSTRANSPORT.Path + + + local coordinates={} + for _,_coord in ipairs(path.coords) do + local coord=_coord --Core.Point#COORDINATE + + -- Get random coordinate. + local c=coord:GetRandomCoordinateInRadius(path.radius) + + table.insert(coordinates, c) + end + + return coordinates + end + + return nil +end + + +--- Add path used to go to the pickup zone. If multiple paths are defined, a random one is chosen. +-- @param #OPSTRANSPORT self +-- @param Wrapper.Group#GROUP PathGroup A (late activated) GROUP defining a transport path by their waypoints. +-- @param #boolean Reversed If `true`, add waypoints of group in reversed order. +-- @param #number Radius Randomization radius in meters. Default 0 m. +-- @param #number Altitude Altitude in feet AGL. Only for aircraft. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:AddPathPickup(PathGroup, Reversed, Radius, Altitude) + + local path={} --#OPSTRANSPORT.Path + path.coords={} + path.radius=Radius or 0 + path.altitude=Altitude + + -- Get route points. + local waypoints=PathGroup:GetTaskRoute() + + if Reversed then + for i=#waypoints,1,-1 do + local wp=waypoints[i] + local coord=COORDINATE:New(wp.x, wp.alt, wp.y) + table.insert(path.coords, coord) + end + else + for i=1,#waypoints do + local wp=waypoints[i] + local coord=COORDINATE:New(wp.x, wp.alt, wp.y) + table.insert(path.coords, coord) + end + end + + -- Add path. + table.insert(self.pathsPickup, path) + + return self +end + +--- Get a path for pickup. +-- @param #OPSTRANSPORT self +-- @return #table The path of COORDINATEs. +function OPSTRANSPORT:_GetPathPickup() + + if self.pathsPickup and #self.pathsPickup>0 then + + -- Get a random path for transport. + local path=self.pathsPickup[math.random(#self.pathsPickup)] --#OPSTRANSPORT.Path + + + local coordinates={} + for _,_coord in ipairs(path.coords) do + local coord=_coord --Core.Point#COORDINATE + + -- Get random coordinate. + local c=coord:GetRandomCoordinateInRadius(path.radius) + + table.insert(coordinates, c) + end + + return coordinates + end + + return nil +end + + +--- Add a carrier assigned for this transport. +-- @param #OPSTRANSPORT self +-- @param Ops.OpsGroup#OPSGROUP CarrierGroup Carrier OPSGROUP. +-- @param #string Status Carrier Status. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetCarrierTransportStatus(CarrierGroup, Status) + + self.carrierTransportStatus[CarrierGroup.groupname]=Status + + return self +end + +--- Get carrier transport status. +-- @param #OPSTRANSPORT self +-- @param Ops.OpsGroup#OPSGROUP CarrierGroup Carrier OPSGROUP. +-- @return #string Carrier status. +function OPSTRANSPORT:GetCarrierTransportStatus(CarrierGroup) + return self.carrierTransportStatus[CarrierGroup.groupname] +end + +--- Get unique ID of the transport assignment. +-- @param #OPSTRANSPORT self +-- @return #number UID. +function OPSTRANSPORT:GetUID() + return self.uid +end + +--- Get number of delivered cargo groups. +-- @param #OPSTRANSPORT self +-- @return #number Total number of delivered cargo groups. +function OPSTRANSPORT:GetNcargoDelivered() + return self.Ndelivered +end + +--- Get number of cargo groups. +-- @param #OPSTRANSPORT self +-- @return #number Total number of cargo groups. +function OPSTRANSPORT:GetNcargoTotal() + return self.Ncargo +end + +--- Get number of carrier groups assigned for this transport. +-- @param #OPSTRANSPORT self +-- @return #number Total number of carrier groups. +function OPSTRANSPORT:GetNcarrier() + return self.Ncarrier +end + +--- Check if an OPS group is assigned as carrier for this transport. +-- @param #OPSTRANSPORT self +-- @param Ops.OpsGroup#OPSGROUP CarrierGroup Potential carrier OPSGROUP. +-- @return #boolean If true, group is an assigned carrier. +function OPSTRANSPORT:IsCarrier(CarrierGroup) + + for _,_carrier in pairs(self.carriers) do + local carrier=_carrier --Ops.OpsGroup#OPSGROUP + if carrier.groupname==CarrierGroup.groupname then + return true + end + end + + return false +end + +--- Check if transport is ready to be started. +-- * Start time passed. +-- * Stop time did not pass already. +-- * All start conditions are true. +-- @param #OPSTRANSPORT self +-- @return #boolean If true, mission can be started. +function OPSTRANSPORT:IsReadyToGo() + + -- Debug text. + local text=self.lid.."Is ReadyToGo? " + + -- Current abs time. + local Tnow=timer.getAbsTime() + + -- Start time did not pass yet. + if self.Tstart and Tnowself.Tstop or false then + text=text.."Nope, stop time already passed!" + self:T(text) + return false + end + + -- All start conditions true? + local startme=self:EvalConditionsAll(self.conditionStart) + + -- Nope, not yet. + if not startme then + text=text..("No way, at least one start condition is not true!") + self:T(text) + return false + end + + -- We're good to go! + text=text.."Yes!" + self:T(text) + return true +end + +--- Check if all cargo was delivered (or is dead). +-- @param #OPSTRANSPORT self +-- @return #boolean If true, all possible cargo was delivered. +function OPSTRANSPORT:IsDelivered() + return self:is(OPSTRANSPORT.Status.DELIVERED) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Status Update +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "Status" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSTRANSPORT:onafterStatus(From, Event, To) + + -- Current FSM state. + local fsmstate=self:GetState() + + if self.verbose>=1 then + + -- Info text. + local text=string.format("%s [%s --> %s]: Ncargo=%d/%d, Ncarrier=%d/%d", fsmstate:upper(), self.pickupzone:GetName(), self.deployzone:GetName(), self.Ncargo, self.Ndelivered, #self.carriers,self.Ncarrier) + + -- Info about cargo and carrier. + if self.verbose>=2 then + + text=text..string.format("\nCargos:") + for _,_cargo in pairs(self.cargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + local carrier=cargo.opsgroup:_GetMyCarrierElement() + local name=carrier and carrier.name or "none" + local cstate=carrier and carrier.status or "N/A" + text=text..string.format("\n- %s: %s [%s], weight=%d kg, carrier=%s [%s], delivered=%s [UID=%s]", + cargo.opsgroup:GetName(), cargo.opsgroup.cargoStatus:upper(), cargo.opsgroup:GetState(), cargo.opsgroup:GetWeightTotal(), name, cstate, tostring(cargo.delivered), tostring(cargo.opsgroup.cargoTransportUID)) + end + + text=text..string.format("\nCarriers:") + for _,_carrier in pairs(self.carriers) do + local carrier=_carrier --Ops.OpsGroup#OPSGROUP + text=text..string.format("\n- %s: %s [%s], Cargo Bay [current/reserved/total]=%d/%d/%d kg [free %d/%d/%d kg]", + carrier:GetName(), carrier.carrierStatus:upper(), carrier:GetState(), + carrier:GetWeightCargo(nil, false), carrier:GetWeightCargo(), carrier:GetWeightCargoMax(), + carrier:GetFreeCargobay(nil, false), carrier:GetFreeCargobay(), carrier:GetFreeCargobayMax()) + end + end + + self:I(self.lid..text) + end + + -- Check if all cargo was delivered (or is dead). + self:_CheckDelivered() + + -- Update status again. + if not self:IsDelivered() then + self:__Status(-30) + end +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Event Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "Planned" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSTRANSPORT:onafterPlanned(From, Event, To) + self:T(self.lid..string.format("New status: %s-->%s", From, To)) +end + +--- On after "Scheduled" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSTRANSPORT:onafterScheduled(From, Event, To) + self:T(self.lid..string.format("New status: %s-->%s", From, To)) +end + +--- On after "Executing" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSTRANSPORT:onafterExecuting(From, Event, To) + self:T(self.lid..string.format("New status: %s-->%s", From, To)) +end + +--- On before "Delivered" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSTRANSPORT:onbeforeDelivered(From, Event, To) + + -- Check that we do not call delivered again. + if From==OPSTRANSPORT.Status.DELIVERED then + return false + end + + return true +end + +--- On after "Delivered" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSTRANSPORT:onafterDelivered(From, Event, To) + self:T(self.lid..string.format("New status: %s-->%s", From, To)) + + -- Inform all assigned carriers that cargo was delivered. They can have this in the queue or are currently processing this transport. + for _,_carrier in pairs(self.carriers) do + local carrier=_carrier --Ops.OpsGroup#OPSGROUP + if self:GetCarrierTransportStatus(carrier)~=OPSTRANSPORT.Status.DELIVERED then + carrier:Delivered(self) + end + end + +end + +--- On after "Loaded" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsGroup#OPSGROUP OpsGroupCargo OPSGROUP that was loaded into a carrier. +-- @param Ops.OpsGroup#OPSGROUP OpsGroupCarrier OPSGROUP that was loaded into a carrier. +-- @param Ops.OpsGroup#OPSGROUP.Element CarrierElement Carrier element. +function OPSTRANSPORT:onafterLoaded(From, Event, To, OpsGroupCargo, OpsGroupCarrier, CarrierElement) + self:I(self.lid..string.format("Loaded OPSGROUP %s into carrier %s", OpsGroupCargo:GetName(), tostring(CarrierElement.name))) +end + +--- On after "Unloaded" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsGroup#OPSGROUP OpsGroupCargo Cargo OPSGROUP that was unloaded from a carrier. +-- @param Ops.OpsGroup#OPSGROUP OpsGroupCarrier Carrier OPSGROUP that unloaded the cargo. +function OPSTRANSPORT:onafterUnloaded(From, Event, To, OpsGroupCargo, OpsGroupCarrier) + self:I(self.lid..string.format("Unloaded OPSGROUP %s", OpsGroupCargo:GetName())) +end + +--- On after "DeadCarrierGroup" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsGroup#OPSGROUP OpsGroup Carrier OPSGROUP that is dead. +function OPSTRANSPORT:onafterDeadCarrierGroup(From, Event, To, OpsGroup) + self:I(self.lid..string.format("Carrier OPSGROUP %s dead!", OpsGroup:GetName())) + -- Remove group from carrier list/table. + self:_DelCarrier(OpsGroup) +end + +--- On after "DeadCarrierAll" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSTRANSPORT:onafterDeadCarrierAll(From, Event, To) + self:I(self.lid..string.format("ALL Carrier OPSGROUPs are dead! Setting stage to PLANNED if not all cargo was delivered.")) + self:_CheckDelivered() + if not self:IsDelivered() then + self:Planned() + end +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check if all cargo of this transport assignment was delivered. +-- @param #OPSTRANSPORT self +function OPSTRANSPORT:_CheckDelivered() + + -- First check that at least one cargo was added (as we allow to do that later). + if self.Ncargo>0 then + + local done=true + local dead=true + for _,_cargo in pairs(self.cargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + + if cargo.delivered then + -- This one is delivered. + dead=false + elseif cargo.opsgroup==nil then + -- This one is nil?! + dead=false + elseif cargo.opsgroup:IsDestroyed() then + -- This one was destroyed. + elseif cargo.opsgroup:IsDead() then + -- This one is dead. + elseif cargo.opsgroup:IsStopped() then + -- This one is stopped. + dead=false + else + done=false --Someone is not done! + dead=false + end + + end + + if dead then + --self:CargoDead() + self:I(self.lid.."All cargo DEAD!") + self:Delivered() + elseif done then + self:I(self.lid.."All cargo delivered") + self:Delivered() + end + + end + +end + +--- Check if all required cargos are loaded. +-- @param #OPSTRANSPORT self +-- @return #boolean If true, all required cargos are loaded or there is no required cargo. +function OPSTRANSPORT:_CheckRequiredCargos() + + if self.requiredCargos==nil or #self.requiredCargos==0 then + return true + end + + local carrierNames=self:_GetCarrierNames() + + local gotit=true + for _,_cargo in pairs(self.requiredCargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP + + + if not cargo:IsLoaded(carrierNames) then + return false + end + + end + + return true +end + +--- Check if all given condition are true. +-- @param #OPSTRANSPORT self +-- @param #table Conditions Table of conditions. +-- @return #boolean If true, all conditions were true. Returns false if at least one condition returned false. +function OPSTRANSPORT:EvalConditionsAll(Conditions) + + -- Any stop condition must be true. + for _,_condition in pairs(Conditions or {}) do + local condition=_condition --#OPSTRANSPORT.Condition + + -- Call function. + local istrue=condition.func(unpack(condition.arg)) + + -- Any false will return false. + if not istrue then + return false + end + + end + + -- All conditions were true. + return true +end + + + +--- Find transfer carrier element for cargo group. +-- @param #OPSTRANSPORT self +-- @param Ops.OpsGroup#OPSGROUP CargoGroup The cargo group that needs to be loaded into a carrier unit/element of the carrier group. +-- @param Core.Zone#ZONE Zone (Optional) Zone where the carrier must be in. +-- @return Ops.OpsGroup#OPSGROUP.Element New carrier element for cargo or nil. +-- @return Ops.OpsGroup#OPSGROUP New carrier group for cargo or nil. +function OPSTRANSPORT:FindTransferCarrierForCargo(CargoGroup, Zone) + + local carrier=nil --Ops.OpsGroup#OPSGROUP.Element + local carrierGroup=nil --Ops.OpsGroup#OPSGROUP + + --TODO: maybe sort the carriers wrt to largest free cargo bay. Or better smallest free cargo bay that can take the cargo group weight. + + for _,_carrier in pairs(self.disembarkCarriers or {}) do + local carrierGroup=_carrier --Ops.OpsGroup#OPSGROUP + + -- First check if carrier is alive and loading cargo. + if carrierGroup and carrierGroup:IsAlive() and (carrierGroup:IsLoading() or self.deployzone:IsInstanceOf("ZONE_AIRBASE")) then + + -- Find an element of the group that has enough free space. + carrier=carrierGroup:FindCarrierForCargo(CargoGroup) + + if carrier then + if Zone==nil or Zone:IsCoordinateInZone(carrier.unit:GetCoordinate()) then + return carrier, carrierGroup + else + self:T2(self.lid.."Got transfer carrier but carrier not in zone (yet)!") + end + else + self:T2(self.lid.."No transfer carrier available!") + end + + end + end + + return nil, nil +end + +--- Create a cargo group data structure. +-- @param #OPSTRANSPORT self +-- @param Wrapper.Group#GROUP group The GROUP or OPSGROUP object. +-- @return Ops.OpsGroup#OPSGROUP.CargoGroup Cargo group data. +function OPSTRANSPORT:_CreateCargoGroupData(group) + + local opsgroup=self:_GetOpsGroupFromObject(group) + + local cargo={} --Ops.OpsGroup#OPSGROUP.CargoGroup + + cargo.opsgroup=opsgroup + cargo.delivered=false + cargo.status="Unknown" + cargo.disembarkCarrierElement=nil + cargo.disembarkCarrierGroup=nil + + return cargo +end + +--- Get an OPSGROUP from a given OPSGROUP or GROUP object. If the object is a GROUUP, an OPSGROUP is created automatically. +-- @param #OPSTRANSPORT self +-- @param Core.Base#BASE Object The object, which can be a GROUP or OPSGROUP. +-- @return Ops.OpsGroup#OPSGROUP Ops Group. +function OPSTRANSPORT:_GetOpsGroupFromObject(Object) + + local opsgroup=nil + + if Object:IsInstanceOf("OPSGROUP") then + -- We already have an OPSGROUP + opsgroup=Object + elseif Object:IsInstanceOf("GROUP") then + + -- Look into DB and try to find an existing OPSGROUP. + opsgroup=_DATABASE:GetOpsGroup(Object) + + if not opsgroup then + if Object:IsAir() then + opsgroup=FLIGHTGROUP:New(Object) + elseif Object:IsShip() then + opsgroup=NAVYGROUP:New(Object) + else + opsgroup=ARMYGROUP:New(Object) + end + end + + else + self:E(self.lid.."ERROR: Object must be a GROUP or OPSGROUP object!") + return nil + end + + return opsgroup +end + diff --git a/Moose Development/Moose/Ops/Squadron.lua b/Moose Development/Moose/Ops/Squadron.lua index 5ed71fba7..cb95b93fa 100644 --- a/Moose Development/Moose/Ops/Squadron.lua +++ b/Moose Development/Moose/Ops/Squadron.lua @@ -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 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 - - diff --git a/Moose Development/Moose/Core/RadioQueue.lua b/Moose Development/Moose/Sound/RadioQueue.lua similarity index 84% rename from Moose Development/Moose/Core/RadioQueue.lua rename to Moose Development/Moose/Sound/RadioQueue.lua index a63677a98..80ac49752 100644 --- a/Moose Development/Moose/Core/RadioQueue.lua +++ b/Moose Development/Moose/Sound/RadioQueue.lua @@ -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() diff --git a/Moose Development/Moose/Core/RadioSpeech.lua b/Moose Development/Moose/Sound/RadioSpeech.lua similarity index 90% rename from Moose Development/Moose/Core/RadioSpeech.lua rename to Moose Development/Moose/Sound/RadioSpeech.lua index d4cf22af1..f77c446a2 100644 --- a/Moose Development/Moose/Core/RadioSpeech.lua +++ b/Moose Development/Moose/Sound/RadioSpeech.lua @@ -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 }, + ["Ñ�тепени"] = { "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 }, + ["возвращаÑ�Ñ�ÑŒ на базу"] = { "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 }, - ["за"] = { "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 }, } diff --git a/Moose Development/Moose/Sound/SRS.lua b/Moose Development/Moose/Sound/SRS.lua new file mode 100644 index 000000000..882d65a99 --- /dev/null +++ b/Moose Development/Moose/Sound/SRS.lua @@ -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 + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Sound/SoundOutput.lua b/Moose Development/Moose/Sound/SoundOutput.lua new file mode 100644 index 000000000..01fd00483 --- /dev/null +++ b/Moose Development/Moose/Sound/SoundOutput.lua @@ -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 \ No newline at end of file diff --git a/Moose Development/Moose/Core/UserSound.lua b/Moose Development/Moose/Sound/UserSound.lua similarity index 98% rename from Moose Development/Moose/Core/UserSound.lua rename to Moose Development/Moose/Sound/UserSound.lua index b0f6fb393..8b94ad114 100644 --- a/Moose Development/Moose/Core/UserSound.lua +++ b/Moose Development/Moose/Sound/UserSound.lua @@ -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 diff --git a/Moose Development/Moose/Tasking/CommandCenter.lua b/Moose Development/Moose/Tasking/CommandCenter.lua index 0673facb1..2bcfe972c 100644 --- a/Moose Development/Moose/Tasking/CommandCenter.lua +++ b/Moose Development/Moose/Tasking/CommandCenter.lua @@ -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 diff --git a/Moose Development/Moose/Tasking/Task.lua b/Moose Development/Moose/Tasking/Task.lua index 7efa0962d..7cd79f453 100644 --- a/Moose Development/Moose/Tasking/Task.lua +++ b/Moose Development/Moose/Tasking/Task.lua @@ -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 ) diff --git a/Moose Development/Moose/Tasking/TaskInfo.lua b/Moose Development/Moose/Tasking/TaskInfo.lua index 5ece5f005..8d825e309 100644 --- a/Moose Development/Moose/Tasking/TaskInfo.lua +++ b/Moose Development/Moose/Tasking/TaskInfo.lua @@ -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 diff --git a/Moose Development/Moose/Tasking/Task_A2A_Dispatcher.lua b/Moose Development/Moose/Tasking/Task_A2A_Dispatcher.lua index 573b8c4b0..76c7225a7 100644 --- a/Moose Development/Moose/Tasking/Task_A2A_Dispatcher.lua +++ b/Moose Development/Moose/Tasking/Task_A2A_Dispatcher.lua @@ -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 diff --git a/Moose Development/Moose/Tasking/Task_A2G_Dispatcher.lua b/Moose Development/Moose/Tasking/Task_A2G_Dispatcher.lua index b7ae6ba17..05bf2d11f 100644 --- a/Moose Development/Moose/Tasking/Task_A2G_Dispatcher.lua +++ b/Moose Development/Moose/Tasking/Task_A2G_Dispatcher.lua @@ -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 \ No newline at end of file +end diff --git a/Moose Development/Moose/Utilities/Enums.lua b/Moose Development/Moose/Utilities/Enums.lua index 84712dc7a..757b67ea1 100644 --- a/Moose Development/Moose/Utilities/Enums.lua +++ b/Moose Development/Moose/Utilities/Enums.lua @@ -310,4 +310,53 @@ ENUMS.Morse.N7="- - * * *" ENUMS.Morse.N8="- - - * *" ENUMS.Morse.N9="- - - - *" ENUMS.Morse.N0="- - - - -" -ENUMS.Morse[" "]=" " \ No newline at end of file +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', +} \ No newline at end of file diff --git a/Moose Development/Moose/Utilities/STTS.lua b/Moose Development/Moose/Utilities/STTS.lua new file mode 100644 index 000000000..97836a1ee --- /dev/null +++ b/Moose Development/Moose/Utilities/STTS.lua @@ -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 \ No newline at end of file diff --git a/Moose Development/Moose/Utilities/Templates.lua b/Moose Development/Moose/Utilities/Templates.lua new file mode 100644 index 000000000..182925689 --- /dev/null +++ b/Moose Development/Moose/Utilities/Templates.lua @@ -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"] +} +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index e0eb873da..9829b9ac1 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -1,13 +1,13 @@ --- This module contains derived utilities taken from the MIST framework, which are excellent tools to be reused in an OO environment. --- --- ### Authors: --- +-- +-- ### Authors: +-- -- * Grimes : Design & Programming of the MIST framework. --- +-- -- ### Contributions: --- --- * FlightControl : Rework to OO framework --- +-- +-- * FlightControl : Rework to OO framework. +-- -- @module Utils -- @image MOOSE.JPG @@ -18,7 +18,7 @@ -- @field White -- @field Orange -- @field Blue - + SMOKECOLOR = trigger.smokeColor -- #SMOKECOLOR --- @type FLARECOLOR @@ -50,6 +50,7 @@ BIGSMOKEPRESET = { -- @field #string PersianGulf Persian Gulf map. -- @field #string TheChannel The Channel map. -- @field #string Syria Syria map. +-- @field #string MarianaIslands Mariana Islands map. DCSMAP = { Caucasus="Caucasus", NTTR="Nevada", @@ -57,6 +58,7 @@ DCSMAP = { PersianGulf="PersianGulf", TheChannel="TheChannel", Syria="Syria", + MarianaIslands="MarianaIslands" } @@ -92,12 +94,12 @@ CALLSIGN={ Texaco=1, Arco=2, Shell=3, - }, + }, -- JTAC JTAC={ Axeman=1, Darknight=2, - Warrier=3, + Warrior=3, Pointer=4, Eyeball=5, Moonbeam=6, @@ -115,6 +117,19 @@ CALLSIGN={ Mantis=18, Badger=19, }, + -- FARP + FARP={ + London=1, + Dallas=2, + Paris=3, + Moscow=4, + Berlin=5, + Rome=6, + Madrid=7, + Warsaw=8, + Dublin=9, + Perth=10, + }, } --#CALLSIGN --- Utilities static class. @@ -148,31 +163,31 @@ UTILS = { UTILS.IsInstanceOf = function( object, className ) -- Is className NOT a string ? if not type( className ) == 'string' then - + -- Is className a Moose class ? if type( className ) == 'table' and className.IsInstanceOf ~= nil then - + -- Get the name of the Moose class as a string className = className.ClassName - + -- className is neither a string nor a Moose class, throw an error else - + -- I'm not sure if this should take advantage of MOOSE logging function, or throw an error for pcall local err_str = 'className parameter should be a string; parameter received: '..type( className ) return false -- error( err_str ) - + end end - + -- Is the object a Moose class instance ? if type( object ) == 'table' and object.IsInstanceOf ~= nil then - + -- Use the IsInstanceOf method of the BASE class return object:IsInstanceOf( className ) else - + -- If the object is not an instance of a Moose class, evaluate against lua basic data types local basicDataTypes = { 'string', 'number', 'function', 'boolean', 'nil', 'table' } for _, basicDataType in ipairs( basicDataTypes ) do @@ -181,7 +196,7 @@ UTILS.IsInstanceOf = function( object, className ) end end end - + -- Check failed return false end @@ -193,7 +208,7 @@ end UTILS.DeepCopy = function(object) local lookup_table = {} - + -- Copy function. local function _copy(object) if type(object) ~= "table" then @@ -201,20 +216,20 @@ UTILS.DeepCopy = function(object) elseif lookup_table[object] then return lookup_table[object] end - + local new_table = {} - + lookup_table[object] = new_table - + for index, value in pairs(object) do new_table[_copy(index)] = _copy(value) end - + return setmetatable(new_table, getmetatable(object)) end - + local objectreturn = _copy(object) - + return objectreturn end @@ -224,19 +239,19 @@ end UTILS.OneLineSerialize = function( tbl ) -- serialization of a table all on a single line, no comments, made to replace old get_table_string function lookup_table = {} - + local function _Serialize( tbl ) if type(tbl) == 'table' then --function only works for tables! - + if lookup_table[tbl] then return lookup_table[object] end local tbl_str = {} - + lookup_table[tbl] = tbl_str - + tbl_str[#tbl_str + 1] = '{' for ind,val in pairs(tbl) do -- serialize its fields @@ -284,7 +299,7 @@ UTILS.OneLineSerialize = function( tbl ) -- serialization of a table all on a s env.info('unable to serialize value type ' .. routines.utils.basicSerialize(type(val)) .. ' at index ' .. tostring(ind)) env.info( debug.traceback() ) end - + end tbl_str[#tbl_str + 1] = '}' return table.concat(tbl_str) @@ -292,7 +307,7 @@ UTILS.OneLineSerialize = function( tbl ) -- serialization of a table all on a s return tostring(tbl) end end - + local objectreturn = _Serialize(tbl) return objectreturn end @@ -401,7 +416,7 @@ end -- @param #number Celcius Temperature in degrees Celsius. -- @return #number Temperature in degrees Farenheit. UTILS.CelciusToFarenheit = function( Celcius ) - return Celcius * 9/5 + 32 + return Celcius * 9/5 + 32 end --- Convert pressure from hecto Pascal (hPa) to inches of mercury (inHg). @@ -411,6 +426,14 @@ UTILS.hPa2inHg = function( hPa ) return hPa * 0.0295299830714 end +--- Convert knots to alitude corrected KIAS, e.g. for tankers. +-- @param #number knots Speed in knots. +-- @param #number altitude Altitude in feet +-- @return #number Corrected KIAS +UTILS.KnotsToAltKIAS = function( knots, altitude ) + return (knots * 0.018 * (altitude / 1000)) + knots +end + --- Convert pressure from hecto Pascal (hPa) to millimeters of mercury (mmHg). -- @param #number hPa Pressure in hPa. -- @return #number Pressure in mmHg. @@ -527,23 +550,23 @@ UTILS.tostringMGRS = function(MGRS, acc) --R2.1 -- Test if Easting/Northing have less than 4 digits. --MGRS.Easting=123 -- should be 00123 --MGRS.Northing=5432 -- should be 05432 - + -- Truncate rather than round MGRS grid! local Easting=tostring(MGRS.Easting) local Northing=tostring(MGRS.Northing) - + -- Count number of missing digits. Easting/Northing should have 5 digits. However, it is passed as a number. Therefore, any leading zeros would not be displayed by lua. - local nE=5-string.len(Easting) + local nE=5-string.len(Easting) local nN=5-string.len(Northing) - + -- Get leading zeros (if any). for i=1,nE do Easting="0"..Easting end for i=1,nN do Northing="0"..Northing end - + -- Return MGRS string. return string.format("%s %s %s %s", MGRS.UTMZone, MGRS.MGRSDigraph, string.sub(Easting, 1, acc), string.sub(Northing, 1, acc)) end - + end @@ -571,7 +594,7 @@ function UTILS.spairs( t, order ) for k in pairs(t) do keys[#keys+1] = k end -- if order function given, sort by it by passing the table and keys a, b, - -- otherwise just sort the keys + -- otherwise just sort the keys if order then table.sort(keys, function(a,b) return order(t, a, b) end) else @@ -597,7 +620,7 @@ function UTILS.kpairs( t, getkey, order ) for k, o in pairs(t) do keys[#keys+1] = k keyso[#keyso+1] = getkey( o ) end -- if order function given, sort by it by passing the table and keys a, b, - -- otherwise just sort the keys + -- otherwise just sort the keys if order then table.sort(keys, function(a,b) return order(t, a, b) end) else @@ -617,7 +640,7 @@ end -- Here is a customized version of pairs, which I called rpairs because it iterates over the table in a random order. function UTILS.rpairs( t ) -- collect the keys - + local keys = {} for k in pairs(t) do keys[#keys+1] = k end @@ -628,7 +651,7 @@ function UTILS.rpairs( t ) random[i] = keys[k] table.remove( keys, k ) end - + -- return the iterator function local i = 0 return function() @@ -647,6 +670,17 @@ function UTILS.GetMarkID() end +--- Remove an object (marker, circle, arrow, text, quad, ...) on the F10 map. +-- @param #number MarkID Unique ID of the object. +-- @param #number Delay (Optional) Delay in seconds before the mark is removed. +function UTILS.RemoveMark(MarkID, Delay) + if Delay and Delay>0 then + TIMER:New(UTILS.RemoveMark, MarkID):Start(Delay) + else + trigger.action.removeMark(MarkID) + end +end + -- Test if a Vec2 is in a radius of another Vec2 function UTILS.IsInRadius( InVec2, Vec2, Radius ) @@ -664,7 +698,10 @@ function UTILS.IsInSphere( InVec3, Vec3, Radius ) return InSphere end --- Beaufort scale: returns Beaufort number and wind description as a function of wind speed in m/s. +--- Beaufort scale: returns Beaufort number and wind description as a function of wind speed in m/s. +-- @param #number speed Wind speed in m/s. +-- @return #number Beaufort number. +-- @return #string Beauford wind description. function UTILS.BeaufortScale(speed) local bn=nil local bd=nil @@ -724,20 +761,35 @@ function UTILS.Split(str, sep) return result end +--- Get a table of all characters in a string. +-- @param #string str Sting. +-- @return #table Individual characters. +function UTILS.GetCharacters(str) + + local chars={} + + for i=1,#str do + local c=str:sub(i,i) + table.insert(chars, c) + end + + return chars +end + --- Convert time in seconds to hours, minutes and seconds. -- @param #number seconds Time in seconds, e.g. from timer.getAbsTime() function. -- @param #boolean short (Optional) If true, use short output, i.e. (HH:)MM:SS without day. -- @return #string Time in format Hours:Minutes:Seconds+Days (HH:MM:SS+D). function UTILS.SecondsToClock(seconds, short) - + -- Nil check. if seconds==nil then return nil end - + -- Seconds local seconds = tonumber(seconds) - + -- Seconds of this day. local _seconds=seconds%(60*60*24) @@ -767,10 +819,10 @@ function UTILS.SecondsOfToday() -- Time in seconds. local time=timer.getAbsTime() - + -- Short format without days since mission start. local clock=UTILS.SecondsToClock(time, true) - + -- Time is now the seconds passed since last midnight. return UTILS.ClockToSeconds(clock) end @@ -785,24 +837,24 @@ end -- @param #string clock String of clock time. E.g., "06:12:35" or "5:1:30+1". Format is (H)H:(M)M:((S)S)(+D) H=Hours, M=Minutes, S=Seconds, D=Days. -- @return #number Seconds. Corresponds to what you cet from timer.getAbsTime() function. function UTILS.ClockToSeconds(clock) - + -- Nil check. if clock==nil then return nil end - + -- Seconds init. local seconds=0 - + -- Split additional days. local dsplit=UTILS.Split(clock, "+") - + -- Convert days to seconds. if #dsplit>1 then seconds=seconds+tonumber(dsplit[2])*60*60*24 end - -- Split hours, minutes, seconds + -- Split hours, minutes, seconds local tsplit=UTILS.Split(dsplit[1], ":") -- Get time in seconds @@ -820,7 +872,7 @@ function UTILS.ClockToSeconds(clock) end i=i+1 end - + return seconds end @@ -832,12 +884,12 @@ function UTILS.DisplayMissionTime(duration) local mission_time=Tnow-timer.getTime0() local mission_time_minutes=mission_time/60 local mission_time_seconds=mission_time%60 - local local_time=UTILS.SecondsToClock(Tnow) + local local_time=UTILS.SecondsToClock(Tnow) local text=string.format("Time: %s - %02d:%02d", local_time, mission_time_minutes, mission_time_seconds) MESSAGE:New(text, duration):ToAll() end ---- Replace illegal characters [<>|/?*:\\] in a string. +--- Replace illegal characters [<>|/?*:\\] in a string. -- @param #string Text Input text. -- @param #string ReplaceBy Replace illegal characters by this character or string. Default underscore "_". -- @return #string The input text with illegal chars replaced. @@ -858,28 +910,28 @@ function UTILS.RandomGaussian(x0, sigma, xmin, xmax, imax) -- Standard deviation. Default 10 if not given. sigma=sigma or 10 - + -- Max attempts. imax=imax or 100 - + local r local gotit=false local i=0 while not gotit do - + -- Uniform numbers in [0,1). We need two. local x1=math.random() local x2=math.random() - + -- Transform to Gaussian exp(-(x-x0)²/(2*sigma²). r = math.sqrt(-2*sigma*sigma * math.log(x1)) * math.cos(2*math.pi * x2) + x0 - + i=i+1 if (r>=xmin and r<=xmax) or i>imax then gotit=true end end - + return r end @@ -904,9 +956,9 @@ function UTILS.Randomize(value, fac, lower, upper) else max=value+value*fac end - + local r=math.random(min, max) - + return r end @@ -918,6 +970,15 @@ function UTILS.VecDot(a, b) return a.x*b.x + a.y*b.y + a.z*b.z end +--- Calculate the [dot product](https://en.wikipedia.org/wiki/Dot_product) of two 2D vectors. The result is a number. +-- @param DCS#Vec2 a Vector in 2D with x, y components. +-- @param DCS#Vec2 b Vector in 2D with x, y components. +-- @return #number Scalar product of the two vectors a*b. +function UTILS.Vec2Dot(a, b) + return a.x*b.x + a.y*b.y +end + + --- Calculate the [euclidean norm](https://en.wikipedia.org/wiki/Euclidean_distance) (length) of a 3D vector. -- @param DCS#Vec3 a Vector in 3D with x, y, z components. -- @return #number Norm of the vector. @@ -925,6 +986,13 @@ function UTILS.VecNorm(a) return math.sqrt(UTILS.VecDot(a, a)) end +--- Calculate the [euclidean norm](https://en.wikipedia.org/wiki/Euclidean_distance) (length) of a 2D vector. +-- @param DCS#Vec2 a Vector in 2D with x, y components. +-- @return #number Norm of the vector. +function UTILS.Vec2Norm(a) + return math.sqrt(UTILS.Vec2Dot(a, a)) +end + --- Calculate the distance between two 2D vectors. -- @param DCS#Vec2 a Vector in 3D with x, y components. -- @param DCS#Vec2 b Vector in 3D with x, y components. @@ -932,7 +1000,7 @@ end function UTILS.VecDist2D(a, b) local c={x=b.x-a.x, y=b.y-a.y} - + local d=math.sqrt(c.x*c.x+c.y*c.y) return d @@ -946,7 +1014,7 @@ end function UTILS.VecDist3D(a, b) local c={x=b.x-a.x, y=b.y-a.y, z=b.z-a.z} - + local d=math.sqrt(UTILS.VecDot(c, c)) return d @@ -960,7 +1028,7 @@ function UTILS.VecCross(a, b) return {x=a.y*b.z - a.z*b.y, y=a.z*b.x - a.x*b.z, z=a.x*b.y - a.y*b.x} end ---- Calculate the difference between two 3D vectors by substracting the x,y,z components from each other. +--- Calculate the difference between two 3D vectors by substracting the x,y,z components from each other. -- @param DCS#Vec3 a Vector in 3D with x, y, z components. -- @param DCS#Vec3 b Vector in 3D with x, y, z components. -- @return DCS#Vec3 Vector c=a-b with c(i)=a(i)-b(i), i=x,y,z. @@ -968,7 +1036,7 @@ function UTILS.VecSubstract(a, b) return {x=a.x-b.x, y=a.y-b.y, z=a.z-b.z} end ---- Calculate the total vector of two 3D vectors by adding the x,y,z components of each other. +--- Calculate the total vector of two 3D vectors by adding the x,y,z components of each other. -- @param DCS#Vec3 a Vector in 3D with x, y, z components. -- @param DCS#Vec3 b Vector in 3D with x, y, z components. -- @return DCS#Vec3 Vector c=a+b with c(i)=a(i)+b(i), i=x,y,z. @@ -976,14 +1044,14 @@ function UTILS.VecAdd(a, b) return {x=a.x+b.x, y=a.y+b.y, z=a.z+b.z} end ---- Calculate the angle between two 3D vectors. +--- Calculate the angle between two 3D vectors. -- @param DCS#Vec3 a Vector in 3D with x, y, z components. -- @param DCS#Vec3 b Vector in 3D with x, y, z components. --- @return #number Angle alpha between and b in degrees. alpha=acos(a*b)/(|a||b|), (* denotes the dot product). +-- @return #number Angle alpha between and b in degrees. alpha=acos(a*b)/(|a||b|), (* denotes the dot product). function UTILS.VecAngle(a, b) local cosalpha=UTILS.VecDot(a,b)/(UTILS.VecNorm(a)*UTILS.VecNorm(b)) - + local alpha=0 if cosalpha>=0.9999999999 then --acos(1) is not defined. alpha=0 @@ -991,8 +1059,8 @@ function UTILS.VecAngle(a, b) alpha=math.pi else alpha=math.acos(cosalpha) - end - + end + return math.deg(alpha) end @@ -1007,6 +1075,17 @@ function UTILS.VecHdg(a) return h end +--- Calculate "heading" of a 2D vector in the X-Y plane. +-- @param DCS#Vec2 a Vector in "D with x, y components. +-- @return #number Heading in degrees in [0,360). +function UTILS.Vec2Hdg(a) + local h=math.deg(math.atan2(a.y, a.x)) + if h<0 then + h=h+360 + end + return h +end + --- Calculate the difference between two "heading", i.e. angles in [0,360) deg. -- @param #number h1 Heading one. -- @param #number h2 Heading two. @@ -1016,18 +1095,18 @@ function UTILS.HdgDiff(h1, h2) -- Angle in rad. local alpha= math.rad(tonumber(h1)) local beta = math.rad(tonumber(h2)) - + -- Runway vector. local v1={x=math.cos(alpha), y=0, z=math.sin(alpha)} local v2={x=math.cos(beta), y=0, z=math.sin(beta)} local delta=UTILS.VecAngle(v1, v2) - + return math.abs(delta) end ---- Translate 3D vector in the 2D (x,z) plane. y-component (usually altitude) unchanged. +--- Translate 3D vector in the 2D (x,z) plane. y-component (usually altitude) unchanged. -- @param DCS#Vec3 a Vector in 3D with x, y, z components. -- @param #number distance The distance to translate. -- @param #number angle Rotation angle in degrees. @@ -1043,26 +1122,61 @@ function UTILS.VecTranslate(a, distance, angle) return {x=TX, y=a.y, z=TY} end ---- Rotate 3D vector in the 2D (x,z) plane. y-component (usually altitude) unchanged. +--- Translate 2D vector in the 2D (x,z) plane. +-- @param DCS#Vec2 a Vector in 2D with x, y components. +-- @param #number distance The distance to translate. +-- @param #number angle Rotation angle in degrees. +-- @return DCS#Vec2 Translated vector. +function UTILS.Vec2Translate(a, distance, angle) + + local SX = a.x + local SY = a.y + local Radians=math.rad(angle or 0) + local TX=distance*math.cos(Radians)+SX + local TY=distance*math.sin(Radians)+SY + + return {x=TX, y=TY} +end + +--- Rotate 3D vector in the 2D (x,z) plane. y-component (usually altitude) unchanged. -- @param DCS#Vec3 a Vector in 3D with x, y, z components. -- @param #number angle Rotation angle in degrees. -- @return DCS#Vec3 Vector rotated in the (x,z) plane. function UTILS.Rotate2D(a, angle) local phi=math.rad(angle) - + local x=a.z local y=a.x - + local Z=x*math.cos(phi)-y*math.sin(phi) local X=x*math.sin(phi)+y*math.cos(phi) local Y=a.y - + local A={x=X, y=Y, z=Z} return A end +--- Rotate 2D vector in the 2D (x,z) plane. +-- @param DCS#Vec2 a Vector in 2D with x, y components. +-- @param #number angle Rotation angle in degrees. +-- @return DCS#Vec2 Vector rotated in the (x,y) plane. +function UTILS.Vec2Rotate2D(a, angle) + + local phi=math.rad(angle) + + local x=a.x + local y=a.y + + local X=x*math.cos(phi)-y*math.sin(phi) + local Y=x*math.sin(phi)+y*math.cos(phi) + + local A={x=X, y=Y} + + return A +end + --- Converts a TACAN Channel/Mode couple into a frequency in Hz. -- @param #number TACANChannel The TACAN channel, i.e. the 10 in "10X". @@ -1075,17 +1189,17 @@ function UTILS.TACANToFrequency(TACANChannel, TACANMode) end 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 @@ -1096,7 +1210,7 @@ function UTILS.TACANToFrequency(TACANChannel, TACANMode) A = 962 end end - + return (A + TACANChannel - B) * 1000000 end @@ -1123,13 +1237,13 @@ end -- @param #number Time (Optional) Abs. time in seconds. Default now, i.e. the value return from timer.getAbsTime(). -- @return #number Day of the mission. Mission starts on day 0. function UTILS.GetMissionDay(Time) - + Time=Time or timer.getAbsTime() - + local clock=UTILS.SecondsToClock(Time, false) - + local x=tonumber(UTILS.Split(clock, "+")[2]) - + return x end @@ -1139,11 +1253,11 @@ end function UTILS.GetMissionDayOfYear(Time) local Date, Year, Month, Day=UTILS.GetDCSMissionDate() - + local d=UTILS.GetMissionDay(Time) - + return UTILS.GetDayOfYear(Year, Month, Day)+d - + end --- Returns the current date. @@ -1155,31 +1269,34 @@ function UTILS.GetDate() -- Mission start date local date, year, month, day=UTILS.GetDCSMissionDate() - + local time=timer.getAbsTime() - + local clock=UTILS.SecondsToClock(time, false) - + local x=tonumber(UTILS.Split(clock, "+")[2]) - + local day=day+x end --- Returns the magnetic declination of the map. -- Returned values for the current maps are: --- +-- -- * Caucasus +6 (East), year ~ 2011 -- * NTTR +12 (East), year ~ 2011 -- * Normandy -10 (West), year ~ 1944 -- * Persian Gulf +2 (East), year ~ 2011 +-- * The Cannel Map -10 (West) +-- * Syria +5 (East) +-- * Mariana Islands +2 (East) -- @param #string map (Optional) Map for which the declination is returned. Default is from env.mission.theatre -- @return #number Declination in degrees. function UTILS.GetMagneticDeclination(map) -- Map. map=map or UTILS.GetDCSMap() - + local declination=0 if map==DCSMAP.Caucasus then declination=6 @@ -1193,6 +1310,8 @@ function UTILS.GetMagneticDeclination(map) declination=-10 elseif map==DCSMAP.Syria then declination=5 + elseif map==DCSMAP.MarianaIslands then + declination=2 else declination=0 end @@ -1214,12 +1333,12 @@ function UTILS.FileExists(file) end else return nil - end + end end --- Checks the current memory usage collectgarbage("count"). Info is printed to the DCS log file. Time stamp is the current mission runtime. --- @param #boolean output If true, print to DCS log file. --- @return #number Memory usage in kByte. +-- @param #boolean output If true, print to DCS log file. +-- @return #number Memory usage in kByte. function UTILS.CheckMemory(output) local time=timer.getTime() local clock=UTILS.SecondsToClock(time) @@ -1249,7 +1368,7 @@ function UTILS.GetCoalitionName(Coalition) else return "Unknown" end - + end --- Get the modulation name from its numerical value. @@ -1268,7 +1387,7 @@ function UTILS.GetModulationName(Modulation) else return "Unknown" end - + end --- Get the callsign name from its enumerator value @@ -1281,7 +1400,7 @@ function UTILS.GetCallsignName(Callsign) return name end end - + for name, value in pairs(CALLSIGN.AWACS) do if value==Callsign then return name @@ -1293,7 +1412,7 @@ function UTILS.GetCallsignName(Callsign) return name end end - + for name, value in pairs(CALLSIGN.Tanker) do if value==Callsign then return name @@ -1321,6 +1440,8 @@ function UTILS.GMTToLocalTimeDifference() return 2 -- This map currently needs +2 elseif theatre==DCSMAP.Syria then return 3 -- Damascus is UTC+3 hours + elseif theatre==DCSMAP.MarianaIslands then + return 10 -- Guam is UTC+10 hours. else BASE:E(string.format("ERROR: Unknown Map %s in UTILS.GMTToLocal function. Returning 0", tostring(theatre))) return 0 @@ -1337,11 +1458,11 @@ end function UTILS.GetDayOfYear(Year, Month, Day) local floor = math.floor - + local n1 = floor(275 * Month / 9) local n2 = floor((Month + 9) / 12) local n3 = (1 + floor((Year - 4 * floor(Year / 4) + 2) / 3)) - + return n1 - (n2 * n3) + Day - 30 end @@ -1354,14 +1475,14 @@ end -- @return #number Sun rise/set in seconds of the day. function UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, Rising, Tlocal) - -- Defaults + -- Defaults local zenith=90.83 local latitude=Latitude local longitude=Longitude local rising=Rising local n=DayOfYear Tlocal=Tlocal or 0 - + -- Short cuts. local rad = math.rad @@ -1388,47 +1509,47 @@ function UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, Rising, Tlocal) return val end end - + -- Convert the longitude to hour value and calculate an approximate time local lng_hour = longitude / 15 - + local t if rising then -- Rising time is desired t = n + ((6 - lng_hour) / 24) else -- Setting time is desired t = n + ((18 - lng_hour) / 24) end - + -- Calculate the Sun's mean anomaly local M = (0.9856 * t) - 3.289 - + -- Calculate the Sun's true longitude local L = fit_into_range(M + (1.916 * sin(M)) + (0.020 * sin(2 * M)) + 282.634, 0, 360) - + -- Calculate the Sun's right ascension local RA = fit_into_range(atan(0.91764 * tan(L)), 0, 360) - + -- Right ascension value needs to be in the same quadrant as L local Lquadrant = floor(L / 90) * 90 local RAquadrant = floor(RA / 90) * 90 RA = RA + Lquadrant - RAquadrant - + -- Right ascension value needs to be converted into hours RA = RA / 15 - + -- Calculate the Sun's declination local sinDec = 0.39782 * sin(L) local cosDec = cos(asin(sinDec)) - + -- Calculate the Sun's local hour angle local cosH = (cos(zenith) - (sinDec * sin(latitude))) / (cosDec * cos(latitude)) - + if rising and cosH > 1 then return "N/R" -- The sun never rises on this location on the specified date elseif cosH < -1 then return "N/S" -- The sun never sets on this location on the specified date end - + -- Finish calculating H and convert into hours local H if rising then @@ -1437,13 +1558,13 @@ function UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, Rising, Tlocal) H = acos(cosH) end H = H / 15 - + -- Calculate local mean time of rising/setting local T = H + RA - (0.06571 * t) - 6.622 -- Adjust back to UTC local UT = fit_into_range(T - lng_hour +Tlocal, 0, 24) - + return floor(UT)*60*60+frac(UT)*60*60--+Tlocal*60*60 end @@ -1487,4 +1608,209 @@ function UTILS.GetOSTime() end return nil +end + +--- Shuffle a table accoring to Fisher Yeates algorithm +--@param #table table to be shuffled +--@return #table +function UTILS.ShuffleTable(t) + if t == nil or type(t) ~= "table" then + BASE:I("Error in ShuffleTable: Missing or wrong tyåe of Argument") + return + end + math.random() + math.random() + math.random() + local TempTable = {} + for i = 1, #t do + local r = math.random(1,#t) + TempTable[i] = t[r] + table.remove(t,r) + end + return TempTable +end + +--- (Helicopter) Check if one loading door is open. +--@param #string unit_name Unit name to be checked +--@return #boolean Outcome - true if a (loading door) is open, false if not, nil if none exists. +function UTILS.IsLoadingDoorOpen( unit_name ) + + local ret_val = false + local unit = Unit.getByName(unit_name) + if unit ~= nil then + local type_name = unit:getTypeName() + + if type_name == "Mi-8MT" and unit:getDrawArgumentValue(86) == 1 or unit:getDrawArgumentValue(250) == 1 then + BASE:T(unit_name .. " Cargo doors are open or cargo door not present") + ret_val = true + end + + if type_name == "Mi-24P" and unit:getDrawArgumentValue(38) == 1 or unit:getDrawArgumentValue(86) == 1 then + BASE:T(unit_name .. " a side door is open") + ret_val = true + end + + if type_name == "UH-1H" and unit:getDrawArgumentValue(43) == 1 or unit:getDrawArgumentValue(44) == 1 then + BASE:T(unit_name .. " a side door is open ") + ret_val = true + end + + if string.find(type_name, "SA342" ) and unit:getDrawArgumentValue(34) == 1 or unit:getDrawArgumentValue(38) == 1 then + BASE:T(unit_name .. " front door(s) are open") + ret_val = true + end + + if ret_val == false then + BASE:T(unit_name .. " all doors are closed") + end + return ret_val + + end -- nil + + return nil +end + +--- Function to generate valid FM frequencies in mHz for radio beacons (FM). +-- @return #table Table of frequencies. +function UTILS.GenerateFMFrequencies() + local FreeFMFrequencies = {} + for _first = 3, 7 do + for _second = 0, 5 do + for _third = 0, 9 do + local _frequency = ((100 * _first) + (10 * _second) + _third) * 100000 --extra 0 because we didnt bother with 4th digit + table.insert(FreeFMFrequencies, _frequency) + end + end + end + return FreeFMFrequencies +end + +--- Function to generate valid VHF frequencies in kHz for radio beacons (FM). +-- @return #table VHFrequencies +function UTILS.GenerateVHFrequencies() + + -- known and sorted map-wise NDBs in kHz + local _skipFrequencies = { + 214,274,291.5,295,297.5, + 300.5,304,307,309.5,311,312,312.5,316, + 320,324,328,329,330,332,336,337, + 342,343,348,351,352,353,358, + 363,365,368,372.5,374, + 380,381,384,385,389,395,396, + 414,420,430,432,435,440,450,455,462,470,485, + 507,515,520,525,528,540,550,560,570,577,580, + 602,625,641,662,670,680,682,690, + 705,720,722,730,735,740,745,750,770,795, + 822,830,862,866, + 905,907,920,935,942,950,995, + 1000,1025,1030,1050,1065,1116,1175,1182,1210 + } + + local FreeVHFFrequencies = {} + + -- first range + local _start = 200000 + while _start < 400000 do + + -- skip existing NDB frequencies# + local _found = false + for _, value in pairs(_skipFrequencies) do + if value * 1000 == _start then + _found = true + break + end + end + if _found == false then + table.insert(FreeVHFFrequencies, _start) + end + _start = _start + 10000 + end + + -- second range + _start = 400000 + while _start < 850000 do + -- skip existing NDB frequencies + local _found = false + for _, value in pairs(_skipFrequencies) do + if value * 1000 == _start then + _found = true + break + end + end + if _found == false then + table.insert(FreeVHFFrequencies, _start) + end + _start = _start + 10000 + end + + -- third range + _start = 850000 + while _start <= 999000 do -- adjusted for Gazelle + -- skip existing NDB frequencies + local _found = false + for _, value in pairs(_skipFrequencies) do + if value * 1000 == _start then + _found = true + break + end + end + if _found == false then + table.insert(FreeVHFFrequencies, _start) + end + _start = _start + 50000 + end + + return FreeVHFFrequencies +end + +--- Function to generate valid UHF Frequencies in mHz (AM). +-- @return #table UHF Frequencies +function UTILS.GenerateUHFrequencies() + + local FreeUHFFrequencies = {} + local _start = 220000000 + + while _start < 399000000 do + table.insert(FreeUHFFrequencies, _start) + _start = _start + 500000 + end + + return FreeUHFFrequencies +end + +--- Function to generate valid laser codes for JTAC. +-- @return #table Laser Codes. +function UTILS.GenerateLaserCodes() + local jtacGeneratedLaserCodes = {} + + -- helper function + local function ContainsDigit(_number, _numberToFind) + local _thisNumber = _number + local _thisDigit = 0 + while _thisNumber ~= 0 do + _thisDigit = _thisNumber % 10 + _thisNumber = math.floor(_thisNumber / 10) + if _thisDigit == _numberToFind then + return true + end + end + return false + end + + -- generate list of laser codes + local _code = 1111 + local _count = 1 + while _code < 1777 and _count < 30 do + while true do + _code = _code + 1 + if not self:_ContainsDigit(_code, 8) + and not ContainsDigit(_code, 9) + and not ContainsDigit(_code, 0) then + table.insert(jtacGeneratedLaserCodes, _code) + break + end + end + _count = _count + 1 + end + return jtacGeneratedLaserCodes end \ No newline at end of file diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index 928d6029d..6887ab79d 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -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 diff --git a/Moose Development/Moose/Wrapper/Client.lua b/Moose Development/Moose/Wrapper/Client.lua index 4e6cce0de..445c655ee 100644 --- a/Moose Development/Moose/Wrapper/Client.lua +++ b/Moose Development/Moose/Wrapper/Client.lua @@ -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. diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 7332271ab..01e3867ba 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -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 diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index 436e6f57f..f844a8ad2 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -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 \ No newline at end of file +--end diff --git a/Moose Development/Moose/Wrapper/Marker.lua b/Moose Development/Moose/Wrapper/Marker.lua index 88a868501..695d2179d 100644 --- a/Moose Development/Moose/Wrapper/Marker.lua +++ b/Moose Development/Moose/Wrapper/Marker.lua @@ -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 diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index 1b305e6e9..6589193df 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -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 diff --git a/Moose Development/Moose/Wrapper/Scenery.lua b/Moose Development/Moose/Wrapper/Scenery.lua index 9eccde422..445154037 100644 --- a/Moose Development/Moose/Wrapper/Scenery.lua +++ b/Moose Development/Moose/Wrapper/Scenery.lua @@ -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 diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index 64b656b52..eb6d472d8 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -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�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 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 diff --git a/Moose Setup/Moose.files b/Moose Setup/Moose.files index 85249125e..13c6da66a 100644 --- a/Moose Setup/Moose.files +++ b/Moose Setup/Moose.files @@ -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 diff --git a/README.md b/README.md index 7ba71d91c..b622326a8 100644 --- a/README.md +++ b/README.md @@ -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.