From a61059242c2935091fe37665cd9b6aefce25693b Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sat, 10 Sep 2022 11:38:50 +0200 Subject: [PATCH 1/4] #AI * Merge changes from DEV --- .../Moose/AI/AI_A2A_Dispatcher.lua | 80 +++++++++-------- .../Moose/AI/AI_A2G_Dispatcher.lua | 86 ++++++++++++------- Moose Development/Moose/AI/AI_CAP.lua | 4 +- Moose Development/Moose/AI/AI_CAS.lua | 2 +- .../AI/AI_Cargo_Dispatcher_Helicopter.lua | 9 +- .../Moose/AI/AI_Cargo_Dispatcher_Ship.lua | 2 +- Moose Development/Moose/AI/AI_Cargo_Ship.lua | 2 +- Moose Development/Moose/AI/AI_Formation.lua | 4 +- Moose Development/Moose/AI/AI_Patrol.lua | 3 +- 9 files changed, 113 insertions(+), 79 deletions(-) diff --git a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua index 0ba77d4ea..9776bf3f0 100644 --- a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua @@ -881,8 +881,9 @@ do -- AI_A2A_DISPATCHER --- Enumerator for spawns at airbases -- @type AI_A2A_DISPATCHER.Takeoff -- @extends Wrapper.Group#GROUP.Takeoff - - --- @field #AI_A2A_DISPATCHER.Takeoff Takeoff + + --- + -- @field #AI_A2A_DISPATCHER.Takeoff Takeoff AI_A2A_DISPATCHER.Takeoff = GROUP.Takeoff --- Defines Landing type/location. @@ -928,6 +929,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 ) @@ -2319,6 +2322,13 @@ do -- AI_A2A_DISPATCHER return self end + --- Set flashing player messages on or off + -- @param #AI_A2A_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 -- @param #string SquadronName The name of the squadron. @@ -3188,7 +3198,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 @@ -3200,10 +3212,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 ) @@ -3219,8 +3231,8 @@ do -- AI_A2A_DISPATCHER local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - if Squadron then - Dispatcher:MessageToPlayers( Squadron, DefenderName .. " returning to base.", DefenderGroup ) + if Squadron and self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. " returning to base.", DefenderGroup ) end Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) end @@ -3404,10 +3416,10 @@ do -- AI_A2A_DISPATCHER local DefenderTarget = Dispatcher:GetDefenderTaskTarget( DefenderGroup ) if DefenderTarget then - if Squadron.Language == "EN" then - Dispatcher:MessageToPlayers( Squadron, DefenderName .. " wheels up.", DefenderGroup ) - elseif Squadron.Language == "RU" then - Dispatcher:MessageToPlayers( Squadron, DefenderName .. " колеса вверх.", DefenderGroup ) + if Squadron.Language == "EN" and self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. " wheels up.", 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 @@ -3425,12 +3437,12 @@ do -- AI_A2A_DISPATCHER local FirstUnit = AttackSetUnit:GetFirst() local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE - if Squadron.Language == "EN" 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 - Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", Eindringlinge abfangen bei" .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup ) + 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" 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 self:GetParent( Fsm ).onafterEngageRoute( self, DefenderGroup, From, Event, To, AttackSetUnit ) @@ -3447,10 +3459,10 @@ do -- AI_A2A_DISPATCHER local FirstUnit = AttackSetUnit:GetFirst() local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE - if Squadron.Language == "EN" 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 ) + 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" 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 ) @@ -3465,10 +3477,10 @@ do -- AI_A2A_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) if Squadron then - if Squadron.Language == "EN" then - Dispatcher:MessageToPlayers( Squadron, DefenderName .. " returning to base.", DefenderGroup ) - elseif Squadron.Language == "RU" then - Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", возвращаясь на базу.", DefenderGroup ) + if Squadron.Language == "EN" and self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. " returning to base.", DefenderGroup ) + elseif Squadron.Language == "RU" and self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", возвращение на базу.", DefenderGroup ) end end Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) @@ -3496,11 +3508,11 @@ do -- AI_A2A_DISPATCHER local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) - if Squadron.Language == "EN" then - Dispatcher:MessageToPlayers( Squadron, DefenderName .. " landing at base.", DefenderGroup ) - elseif Squadron.Language == "RU" then - Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", захватывающие самолеты в посадка на базу.", DefenderGroup ) - end + if Squadron.Language == "EN" and self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. " landing at base.", DefenderGroup ) + elseif Squadron.Language == "RU" and self.SetSendPlayerMessages then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", посадка на базу.", DefenderGroup ) + end if Action and Action == "Destroy" then Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) @@ -3890,7 +3902,7 @@ do self:CAP( SquadronName ) end - --- Add resources to a Squadron + --- Add resources to a Squadron -- @param #AI_A2A_DISPATCHER self -- @param #string Squadron The squadron name. -- @param #number Amount Number of resources to add. @@ -3913,7 +3925,7 @@ do end self:T({Squadron = Squadron.Name,SquadronResourceCount = Squadron.ResourceCount}) end - + end do @@ -4484,5 +4496,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 b030466e4..ac1a06b7d 100644 --- a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -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 ) @@ -3718,7 +3720,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 @@ -3730,7 +3734,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 @@ -3749,8 +3753,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 @@ -3764,8 +3769,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 @@ -3776,8 +3782,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 @@ -3789,7 +3796,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() @@ -3804,8 +3813,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() @@ -3861,7 +3871,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 @@ -3876,8 +3888,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 @@ -3892,8 +3905,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 @@ -3903,8 +3917,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 ) @@ -3918,8 +3933,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() @@ -3934,8 +3950,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() @@ -3975,7 +3992,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 @@ -4036,7 +4053,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 ) @@ -4341,7 +4358,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 @@ -4559,7 +4576,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 @@ -4729,7 +4746,15 @@ do self:Patrol( SquadronName, PatrolTaskType ) end - --- Add resources to a Squadron + --- 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 + + --- Add resources to a Squadron -- @param #AI_A2G_DISPATCHER self -- @param #string Squadron The squadron name. -- @param #number Amount Number of resources to add. @@ -4751,7 +4776,4 @@ do Squadron.ResourceCount = Squadron.ResourceCount - Amount end self:T({Squadron = Squadron.Name,SquadronResourceCount = Squadron.ResourceCount}) - end - -end - + end \ No newline at end of file diff --git a/Moose Development/Moose/AI/AI_CAP.lua b/Moose Development/Moose/AI/AI_CAP.lua index 02dc40a49..f89f2f88f 100644 --- a/Moose Development/Moose/AI/AI_CAP.lua +++ b/Moose Development/Moose/AI/AI_CAP.lua @@ -428,9 +428,7 @@ function AI_CAP_ZONE:onafterEngage( Controllable, From, Event, To ) --- Calculate the current route point. local CurrentVec2 = self.Controllable:GetVec2() - if not CurrentVec2 then -- flight dead at this point - return self - end + if not CurrentVec2 then return self end --DONE: Create GetAltitude function for GROUP, and delete GetUnit(1). local CurrentAltitude = self.Controllable:GetAltitude() diff --git a/Moose Development/Moose/AI/AI_CAS.lua b/Moose Development/Moose/AI/AI_CAS.lua index 2829a7962..de6ab65c1 100644 --- a/Moose Development/Moose/AI/AI_CAS.lua +++ b/Moose Development/Moose/AI/AI_CAS.lua @@ -459,7 +459,7 @@ function AI_CAS_ZONE:onafterEngage( Controllable, From, Event, To, --- Calculate the current route point. local CurrentVec2 = self.Controllable:GetVec2() - --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). + --DONE: Create GetAltitude function for GROUP, and delete GetUnit(1). local CurrentAltitude = self.Controllable:GetAltitude() local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) local ToEngageZoneSpeed = self.PatrolMaxSpeed diff --git a/Moose Development/Moose/AI/AI_Cargo_Dispatcher_Helicopter.lua b/Moose Development/Moose/AI/AI_Cargo_Dispatcher_Helicopter.lua index 127c67dbb..bba9b7108 100644 --- a/Moose Development/Moose/AI/AI_Cargo_Dispatcher_Helicopter.lua +++ b/Moose Development/Moose/AI/AI_Cargo_Dispatcher_Helicopter.lua @@ -174,8 +174,8 @@ function AI_CARGO_DISPATCHER_HELICOPTER:New( HelicopterSet, CargoSet, PickupZone self:SetPickupSpeed( 350, 150 ) self:SetDeploySpeed( 350, 150 ) - self:SetPickupRadius( 0, 0 ) - self:SetDeployRadius( 0, 0 ) + self:SetPickupRadius( 40, 12 ) + self:SetDeployRadius( 40, 12 ) self:SetPickupHeight( 500, 200 ) self:SetDeployHeight( 500, 200 ) @@ -186,6 +186,9 @@ end function AI_CARGO_DISPATCHER_HELICOPTER:AICargo( Helicopter, CargoSet ) - return AI_CARGO_HELICOPTER:New( Helicopter, CargoSet ) + local dispatcher = AI_CARGO_HELICOPTER:New( Helicopter, CargoSet ) + dispatcher:SetLandingSpeedAndHeight(27, 6) + return dispatcher + end diff --git a/Moose Development/Moose/AI/AI_Cargo_Dispatcher_Ship.lua b/Moose Development/Moose/AI/AI_Cargo_Dispatcher_Ship.lua index d20cca9eb..70f325f62 100644 --- a/Moose Development/Moose/AI/AI_Cargo_Dispatcher_Ship.lua +++ b/Moose Development/Moose/AI/AI_Cargo_Dispatcher_Ship.lua @@ -21,7 +21,7 @@ -- === -- -- @module AI.AI_Cargo_Dispatcher_Ship --- @image AI_Cargo_Dispatching_For_Ship.JPG +-- @image AI_Cargo_Dispatcher.JPG --- @type AI_CARGO_DISPATCHER_SHIP -- @extends AI.AI_Cargo_Dispatcher#AI_CARGO_DISPATCHER diff --git a/Moose Development/Moose/AI/AI_Cargo_Ship.lua b/Moose Development/Moose/AI/AI_Cargo_Ship.lua index e28e52aa2..37c7da57a 100644 --- a/Moose Development/Moose/AI/AI_Cargo_Ship.lua +++ b/Moose Development/Moose/AI/AI_Cargo_Ship.lua @@ -7,7 +7,7 @@ -- === -- -- @module AI.AI_Cargo_Ship --- @image AI_Cargo_Dispatching_For_Ship.JPG +-- @image AI_Cargo_Dispatcher.JPG --- @type AI_CARGO_SHIP -- @extends AI.AI_Cargo#AI_CARGO 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/AI/AI_Patrol.lua b/Moose Development/Moose/AI/AI_Patrol.lua index 02b4744e0..9fd07539a 100644 --- a/Moose Development/Moose/AI/AI_Patrol.lua +++ b/Moose Development/Moose/AI/AI_Patrol.lua @@ -725,7 +725,6 @@ function AI_PATROL_ZONE:onafterRoute( Controllable, From, Event, To ) return end - local life = self.Controllable:GetLife() or 0 if self.Controllable:IsAlive() and life > 1 then -- Determine if the AIControllable is within the PatrolZone. @@ -745,7 +744,7 @@ function AI_PATROL_ZONE:onafterRoute( Controllable, From, Event, To ) self:T( "Not in the air, finding route path within PatrolZone" ) local CurrentVec2 = self.Controllable:GetVec2() if not CurrentVec2 then return end - --DONE: Create GetAltitude function for GROUP, and delete GetUnit(1). + --Done: Create GetAltitude function for GROUP, and delete GetUnit(1). local CurrentAltitude = self.Controllable:GetAltitude() local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y ) local ToPatrolZoneSpeed = self.PatrolMaxSpeed From c58e91b956f8cad33d04e9ea88fe5c672bf60868 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sat, 10 Sep 2022 11:41:41 +0200 Subject: [PATCH 2/4] #TASKING * Merge changes from Develop --- .../Moose/Tasking/CommandCenter.lua | 8 +- .../Moose/Tasking/Task_Cargo_Dispatcher.lua | 14 ++-- .../Moose/Tasking/Task_Cargo_Transport.lua | 82 +++++++++---------- 3 files changed, 54 insertions(+), 50 deletions(-) diff --git a/Moose Development/Moose/Tasking/CommandCenter.lua b/Moose Development/Moose/Tasking/CommandCenter.lua index 2bcfe972c..8df64d063 100644 --- a/Moose Development/Moose/Tasking/CommandCenter.lua +++ b/Moose Development/Moose/Tasking/CommandCenter.lua @@ -216,7 +216,7 @@ function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) local MenuReporting = MENU_GROUP:New( EventGroup, "Missions Reports", CommandCenterMenu ) local MenuMissionsSummary = MENU_GROUP_COMMAND:New( EventGroup, "Missions Status Report", MenuReporting, self.ReportSummary, self, EventGroup ) local MenuMissionsDetails = MENU_GROUP_COMMAND:New( EventGroup, "Missions Players Report", MenuReporting, self.ReportMissionsPlayers, self, EventGroup ) - self:ReportSummary( EventGroup ) + --self:ReportSummary( EventGroup ) local PlayerUnit = EventData.IniUnit for MissionID, Mission in pairs( self:GetMissions() ) do local Mission = Mission -- Tasking.Mission#MISSION @@ -560,9 +560,11 @@ function COMMANDCENTER:SetAutoAssignTasks( AutoAssign ) self.AutoAssignTasks = AutoAssign or false if self.AutoAssignTasks == true then - self:ScheduleRepeat( 10, 30, 0, nil, self.AssignTasks, self ) + self.autoAssignTasksScheduleID=self:ScheduleRepeat( 10, 30, 0, nil, self.AssignTasks, self ) else - self:ScheduleStop( self.AssignTasks ) + self:ScheduleStop() + -- FF this is not the schedule ID + --self:ScheduleStop( self.AssignTasks ) end end diff --git a/Moose Development/Moose/Tasking/Task_Cargo_Dispatcher.lua b/Moose Development/Moose/Tasking/Task_Cargo_Dispatcher.lua index 6833f197c..fd3be1018 100644 --- a/Moose Development/Moose/Tasking/Task_Cargo_Dispatcher.lua +++ b/Moose Development/Moose/Tasking/Task_Cargo_Dispatcher.lua @@ -683,6 +683,7 @@ do -- TASK_CARGO_DISPATCHER -- If no TaskPrefix is given, then "Transport" will be used as the prefix. -- @param Core.SetCargo#SET_CARGO SetCargo The SetCargo to be transported. -- @param #string Briefing The briefing of the task transport to be shown to the player. + -- @param #boolean Silent If true don't send a message that a new task is available. -- @return Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT -- @usage -- @@ -705,10 +706,12 @@ do -- TASK_CARGO_DISPATCHER -- -- Here we set a TransportDeployZone. We use the WorkplaceTask as the reference, and provide a ZONE object. -- TaskDispatcher:SetTransportDeployZone( WorkplaceTask, ZONE:New( "Workplace" ) ) -- - function TASK_CARGO_DISPATCHER:AddTransportTask( TaskPrefix, SetCargo, Briefing ) + function TASK_CARGO_DISPATCHER:AddTransportTask( TaskPrefix, SetCargo, Briefing, Silent ) self.TransportCount = self.TransportCount + 1 + local verbose = Silent or false + local TaskName = string.format( ( TaskPrefix or "Transport" ) .. ".%03d", self.TransportCount ) self.Transport[TaskName] = {} @@ -717,7 +720,7 @@ do -- TASK_CARGO_DISPATCHER self.Transport[TaskName].Task = nil self.Transport[TaskName].TaskPrefix = TaskPrefix - self:ManageTasks() + self:ManageTasks(verbose) return self.Transport[TaskName] and self.Transport[TaskName].Task end @@ -785,10 +788,11 @@ do -- TASK_CARGO_DISPATCHER --- Assigns tasks to the @{Core.Set#SET_GROUP}. -- @param #TASK_CARGO_DISPATCHER self + -- @param #boolean Silent Announce new task (nil/false) or not (true). -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop. - function TASK_CARGO_DISPATCHER:ManageTasks() + function TASK_CARGO_DISPATCHER:ManageTasks(Silent) self:F() - + local verbose = Silent and true local AreaMsg = {} local TaskMsg = {} local ChangeMsg = {} @@ -897,7 +901,7 @@ do -- TASK_CARGO_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 not verbose 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_Cargo_Transport.lua b/Moose Development/Moose/Tasking/Task_Cargo_Transport.lua index 484363978..8629871b2 100644 --- a/Moose Development/Moose/Tasking/Task_Cargo_Transport.lua +++ b/Moose Development/Moose/Tasking/Task_Cargo_Transport.lua @@ -2,7 +2,7 @@ -- -- **Specific features:** -- --- * Creates a task to transport @{Cargo.Cargo} to and between deployment zones. +-- * Creates a task to transport #Cargo.Cargo to and between deployment zones. -- * Derived from the TASK_CARGO class, which is derived from the TASK class. -- * Orchestrate the task flow, so go from Planned to Assigned to Success, Failed or Cancelled. -- * Co-operation tasking, so a player joins a group of players executing the same task. @@ -44,7 +44,7 @@ -- -- === -- --- Please read through the @{Tasking.Task_Cargo} process to understand the mechanisms of tasking and cargo tasking and handling. +-- Please read through the #Tasking.Task_Cargo process to understand the mechanisms of tasking and cargo tasking and handling. -- -- Enjoy! -- FC @@ -57,7 +57,7 @@ do -- TASK_CARGO_TRANSPORT - --- @type TASK_CARGO_TRANSPORT + -- @type TASK_CARGO_TRANSPORT -- @extends Tasking.Task_CARGO#TASK_CARGO --- Orchestrates the task for players to transport cargo to or between deployment zones. @@ -76,7 +76,7 @@ do -- TASK_CARGO_TRANSPORT -- -- ## 1.1) Create a command center. -- - -- First you need to create a command center using the @{Tasking.CommandCenter#COMMANDCENTER.New}() constructor. + -- First you need to create a command center using the Tasking.CommandCenter#COMMANDCENTER.New constructor. -- -- local CommandCenter = COMMANDCENTER -- :New( HQ, "Lima" ) -- Create the CommandCenter. @@ -85,7 +85,7 @@ do -- TASK_CARGO_TRANSPORT -- -- Tasks work in a mission, which groups these tasks to achieve a joint mission goal. -- A command center can govern multiple missions. - -- Create a new mission, using the @{Tasking.Mission#MISSION.New}() constructor. + -- Create a new mission, using the Tasking.Mission#MISSION.New constructor. -- -- -- Declare the Mission for the Command Center. -- local Mission = MISSION @@ -99,7 +99,7 @@ do -- TASK_CARGO_TRANSPORT -- ## 1.3) Create the transport cargo task. -- -- So, now that we have a command center and a mission, we now create the transport task. - -- We create the transport task using the @{#TASK_CARGO_TRANSPORT.New}() constructor. + -- We create the transport task using the #TASK_CARGO_TRANSPORT.New constructor. -- -- Because a transport task will not generate the cargo itself, you'll need to create it first. -- The cargo in this case will be the downed pilot! @@ -118,7 +118,7 @@ do -- TASK_CARGO_TRANSPORT -- -- The cargoset "CargoSet" will embed all defined cargo of type "Pilots" (prefix) into its set. -- local CargoGroup = CARGO_GROUP:New( PilotGroup, "Cargo", "Engineer Team 1", 500 ) -- - -- What is also needed, is to have a set of @{Core.Group}s defined that contains the clients of the players. + -- What is also needed, is to have a set of Core.Groups defined that contains the clients of the players. -- -- -- Allocate the Transport, which are the helicopter to retrieve the pilot, that can be manned by players. -- local GroupSet = SET_GROUP:New():FilterPrefixes( "Transport" ):FilterStart() @@ -139,48 +139,48 @@ do -- TASK_CARGO_TRANSPORT -- By doing this, cargo transport tasking will become a dynamic experience. -- -- - -- # 2) Create a task using the @{Tasking.Task_Cargo_Dispatcher} module. + -- # 2) Create a task using the Tasking.Task_Cargo_Dispatcher module. -- - -- Actually, it is better to **GENERATE** these tasks using the @{Tasking.Task_Cargo_Dispatcher} module. - -- Using the dispatcher module, transport tasks can be created much more easy. + -- Actually, it is better to **GENERATE** these tasks using the Tasking.Task_Cargo_Dispatcher module. + -- Using the dispatcher module, transport tasks can be created easier. -- -- Find below an example how to use the TASK_CARGO_DISPATCHER class: -- -- - -- -- Find the HQ group. - -- HQ = GROUP:FindByName( "HQ", "Bravo" ) + -- -- Find the HQ group. + -- HQ = GROUP:FindByName( "HQ", "Bravo" ) -- - -- -- Create the command center with the name "Lima". - -- CommandCenter = COMMANDCENTER - -- :New( HQ, "Lima" ) + -- -- Create the command center with the name "Lima". + -- CommandCenter = COMMANDCENTER + -- :New( HQ, "Lima" ) -- - -- -- Create the mission, for the command center, with the name "Operation Cargo Fun", a "Tactical" mission, with the mission briefing "Transport Cargo", for the BLUE coalition. - -- Mission = MISSION - -- :New( CommandCenter, "Operation Cargo Fun", "Tactical", "Transport Cargo", coalition.side.BLUE ) + -- -- Create the mission, for the command center, with the name "Operation Cargo Fun", a "Tactical" mission, with the mission briefing "Transport Cargo", for the BLUE coalition. + -- Mission = MISSION + -- :New( CommandCenter, "Operation Cargo Fun", "Tactical", "Transport Cargo", coalition.side.BLUE ) -- - -- -- Create the SET of GROUPs containing clients (players) that will transport the cargo. - -- -- These are have a name that start with "Transport" and are of the "blue" coalition. - -- TransportGroups = SET_GROUP:New():FilterCoalitions( "blue" ):FilterPrefixes( "Transport" ):FilterStart() + -- -- Create the SET of GROUPs containing clients (players) that will transport the cargo. + -- -- These are have a name that start with "Transport" and are of the "blue" coalition. + -- TransportGroups = SET_GROUP:New():FilterCoalitions( "blue" ):FilterPrefixes( "Transport" ):FilterStart() -- -- - -- -- Here we create the TASK_CARGO_DISPATCHER object! This is where we assign the dispatcher to generate tasks in the Mission for the TransportGroups. - -- TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, TransportGroups ) + -- -- Here we create the TASK_CARGO_DISPATCHER object! This is where we assign the dispatcher to generate tasks in the Mission for the TransportGroups. + -- TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, TransportGroups ) -- -- - -- -- Here we declare the SET of CARGOs called "Workmaterials". - -- local CargoSetWorkmaterials = SET_CARGO:New():FilterTypes( "Workmaterials" ):FilterStart() + -- -- Here we declare the SET of CARGOs called "Workmaterials". + -- local CargoSetWorkmaterials = SET_CARGO:New():FilterTypes( "Workmaterials" ):FilterStart() -- - -- -- Here we declare (add) CARGO_GROUP objects of various types, that are filtered and added in the CargoSetworkmaterials cargo set. - -- -- These cargo objects have the type "Workmaterials" which is exactly the type of cargo the CargoSetworkmaterials is filtering on. - -- local EngineerCargoGroup = CARGO_GROUP:New( GROUP:FindByName( "Engineers" ), "Workmaterials", "Engineers", 250 ) - -- local ConcreteCargo = CARGO_SLINGLOAD:New( STATIC:FindByName( "Concrete" ), "Workmaterials", "Concrete", 150, 50 ) - -- local CrateCargo = CARGO_CRATE:New( STATIC:FindByName( "Crate" ), "Workmaterials", "Crate", 150, 50 ) - -- local EnginesCargo = CARGO_CRATE:New( STATIC:FindByName( "Engines" ), "Workmaterials", "Engines", 150, 50 ) - -- local MetalCargo = CARGO_CRATE:New( STATIC:FindByName( "Metal" ), "Workmaterials", "Metal", 150, 50 ) + -- -- Here we declare (add) CARGO_GROUP objects of various types, that are filtered and added in the CargoSetworkmaterials cargo set. + -- -- These cargo objects have the type "Workmaterials" which is exactly the type of cargo the CargoSetworkmaterials is filtering on. + -- local EngineerCargoGroup = CARGO_GROUP:New( GROUP:FindByName( "Engineers" ), "Workmaterials", "Engineers", 250 ) + -- local ConcreteCargo = CARGO_SLINGLOAD:New( STATIC:FindByName( "Concrete" ), "Workmaterials", "Concrete", 150, 50 ) + -- local CrateCargo = CARGO_CRATE:New( STATIC:FindByName( "Crate" ), "Workmaterials", "Crate", 150, 50 ) + -- local EnginesCargo = CARGO_CRATE:New( STATIC:FindByName( "Engines" ), "Workmaterials", "Engines", 150, 50 ) + -- local MetalCargo = CARGO_CRATE:New( STATIC:FindByName( "Metal" ), "Workmaterials", "Metal", 150, 50 ) -- - -- -- And here we create a new WorkplaceTask, using the :AddTransportTask method of the TaskDispatcher. - -- local WorkplaceTask = TaskDispatcher:AddTransportTask( "Build a Workplace", CargoSetWorkmaterials, "Transport the workers, engineers and the equipment near the Workplace." ) - -- TaskDispatcher:SetTransportDeployZone( WorkplaceTask, ZONE:New( "Workplace" ) ) + -- -- And here we create a new WorkplaceTask, using the :AddTransportTask method of the TaskDispatcher. + -- local WorkplaceTask = TaskDispatcher:AddTransportTask( "Build a Workplace", CargoSetWorkmaterials, "Transport the workers, engineers and the equipment near the Workplace." ) + -- TaskDispatcher:SetTransportDeployZone( WorkplaceTask, ZONE:New( "Workplace" ) ) -- -- # 3) Handle cargo task events. -- @@ -189,7 +189,7 @@ do -- TASK_CARGO_TRANSPORT -- In order to properly capture the events and avoid mistakes using the documentation, it is advised that you execute the following actions: -- -- * **Copy / Paste** the code section into your script. - -- * **Change** the CLASS literal to the task object name you have in your script. + -- * **Change** the "myclass" literal to the task object name you have in your script. -- * Within the function, you can now **write your own code**! -- * **IntelliSense** will recognize the type of the variables provided by the function. Note: the From, Event and To variables can be safely ignored, -- but you need to declare them as they are automatically provided by the event handling system of MOOSE. @@ -210,14 +210,13 @@ do -- TASK_CARGO_TRANSPORT -- If you want to code your own event handler, use this code fragment to tailor the event when a player carrier has picked up a cargo object in the CarrierGroup. -- You can use this event handler to post messages to players, or provide status updates etc. -- - -- --- CargoPickedUp event handler OnAfter for CLASS. - -- -- @param #CLASS self + -- --- CargoPickedUp event handler OnAfter for "myclass". -- -- @param #string From A string that contains the "*from state name*" when the event was triggered. -- -- @param #string Event A string that contains the "*event name*" when the event was triggered. -- -- @param #string To A string that contains the "*to state name*" when the event was triggered. -- -- @param Wrapper.Unit#UNIT TaskUnit The unit (client) of the player that has picked up the cargo. -- -- @param Cargo.Cargo#CARGO Cargo The cargo object that has been picked up. Note that this can be a CARGO_GROUP, CARGO_CRATE or CARGO_SLINGLOAD object! - -- function CLASS:OnAfterCargoPickedUp( From, Event, To, TaskUnit, Cargo ) + -- function myclass:OnAfterCargoPickedUp( From, Event, To, TaskUnit, Cargo ) -- -- -- Write here your own code. -- @@ -240,15 +239,14 @@ do -- TASK_CARGO_TRANSPORT -- You can use this event handler to post messages to players, or provide status updates etc. -- -- - -- --- CargoDeployed event handler OnAfter for CLASS. - -- -- @param #CLASS self + -- --- CargoDeployed event handler OnAfter foR "myclass". -- -- @param #string From A string that contains the "*from state name*" when the event was triggered. -- -- @param #string Event A string that contains the "*event name*" when the event was triggered. -- -- @param #string To A string that contains the "*to state name*" when the event was triggered. -- -- @param Wrapper.Unit#UNIT TaskUnit The unit (client) of the player that has deployed the cargo. -- -- @param Cargo.Cargo#CARGO Cargo The cargo object that has been deployed. Note that this can be a CARGO_GROUP, CARGO_CRATE or CARGO_SLINGLOAD object! -- -- @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 CLASS:OnAfterCargoDeployed( From, Event, To, TaskUnit, Cargo, DeployZone ) + -- function myclass:OnAfterCargoDeployed( From, Event, To, TaskUnit, Cargo, DeployZone ) -- -- -- Write here your own code. -- From 7c22e9fe6945eee74c4f6523ba87f2a16323eaae Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sat, 10 Sep 2022 11:49:40 +0200 Subject: [PATCH 3/4] #Changes from Develop --- Moose Development/Moose/Core/Astar.lua | 943 +++++++++ Moose Development/Moose/Core/Base.lua | 267 ++- Moose Development/Moose/Core/Beacon.lua | 7 +- Moose Development/Moose/Core/Condition.lua | 295 +++ Moose Development/Moose/Core/Database.lua | 658 ++++--- Moose Development/Moose/Core/Fsm.lua | 74 +- .../Moose/Core/MarkerOps_Base.lua | 26 +- Moose Development/Moose/Core/Menu.lua | 27 +- Moose Development/Moose/Core/Message.lua | 43 +- Moose Development/Moose/Core/Point.lua | 39 +- .../Moose/Core/ScheduleDispatcher.lua | 11 +- Moose Development/Moose/Core/Scheduler.lua | 10 +- Moose Development/Moose/Core/Set.lua | 490 +++-- Moose Development/Moose/Core/Settings.lua | 17 +- Moose Development/Moose/Core/Spawn.lua | 43 +- Moose Development/Moose/Core/TextAndSound.lua | 202 ++ Moose Development/Moose/Core/Timer.lua | 31 +- Moose Development/Moose/Core/Zone.lua | 422 +++- .../Moose/Functional/Detection.lua | 249 +-- Moose Development/Moose/Functional/Fox.lua | 11 +- Moose Development/Moose/Functional/Mantis.lua | 835 ++++++-- .../Moose/Functional/MissileTrainer.lua | 120 +- .../Moose/Functional/PseudoATC.lua | 3 - Moose Development/Moose/Functional/RAT.lua | 1701 +++++++++-------- Moose Development/Moose/Functional/Range.lua | 127 +- .../Moose/Functional/Scoring.lua | 77 +- Moose Development/Moose/Functional/Sead.lua | 2 + Moose Development/Moose/Functional/Shorad.lua | 141 +- .../Moose/Functional/Warehouse.lua | 1643 +++++++++------- Moose Development/Moose/Sound/Radio.lua | 118 +- Moose Development/Moose/Sound/RadioSpeech.lua | 131 +- Moose Development/Moose/Sound/SRS.lua | 2 + Moose Development/Moose/Sound/UserSound.lua | 2 +- Moose Development/Moose/Utilities/Enums.lua | 2 +- Moose Development/Moose/Utilities/FiFo.lua | 20 +- .../Moose/Utilities/Profiler.lua | 315 +-- .../Moose/Utilities/Routines.lua | 545 +++--- Moose Development/Moose/Utilities/STTS.lua | 9 +- Moose Development/Moose/Utilities/Socket.lua | 152 ++ Moose Development/Moose/Utilities/Utils.lua | 24 +- Moose Development/Moose/Wrapper/Airbase.lua | 1252 ++++++++---- Moose Development/Moose/Wrapper/Client.lua | 11 + .../Moose/Wrapper/Controllable.lua | 116 +- .../Moose/Wrapper/Identifiable.lua | 28 +- Moose Development/Moose/Wrapper/Marker.lua | 36 +- .../Moose/Wrapper/Positionable.lua | 211 +- Moose Development/Moose/Wrapper/Scenery.lua | 37 + Moose Development/Moose/Wrapper/Unit.lua | 6 +- 48 files changed, 7792 insertions(+), 3739 deletions(-) create mode 100644 Moose Development/Moose/Core/Astar.lua create mode 100644 Moose Development/Moose/Core/Condition.lua create mode 100644 Moose Development/Moose/Core/TextAndSound.lua create mode 100644 Moose Development/Moose/Utilities/Socket.lua diff --git a/Moose Development/Moose/Core/Astar.lua b/Moose Development/Moose/Core/Astar.lua new file mode 100644 index 000000000..9514d179e --- /dev/null +++ b/Moose Development/Moose/Core/Astar.lua @@ -0,0 +1,943 @@ +--- **Core** - A* Pathfinding. +-- +-- **Main Features:** +-- +-- * Find path from A to B. +-- * Pre-defined as well as custom valid neighbour functions. +-- * Pre-defined as well as custom cost functions. +-- * Easy rectangular grid setup. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- === +-- @module Core.Astar +-- @image CORE_Astar.png + + +--- ASTAR class. +-- @type ASTAR +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode. Messages to all about status. +-- @field #string lid Class id string for output to DCS log file. +-- @field #table nodes Table of nodes. +-- @field #number counter Node counter. +-- @field #number Nnodes Number of nodes. +-- @field #number nvalid Number of nvalid calls. +-- @field #number nvalidcache Number of cached valid evals. +-- @field #number ncost Number of cost evaluations. +-- @field #number ncostcache Number of cached cost evals. +-- @field #ASTAR.Node startNode Start node. +-- @field #ASTAR.Node endNode End node. +-- @field Core.Point#COORDINATE startCoord Start coordinate. +-- @field Core.Point#COORDINATE endCoord End coordinate. +-- @field #function ValidNeighbourFunc Function to check if a node is valid. +-- @field #table ValidNeighbourArg Optional arguments passed to the valid neighbour function. +-- @field #function CostFunc Function to calculate the heuristic "cost" to go from one node to another. +-- @field #table CostArg Optional arguments passed to the cost function. +-- @extends Core.Base#BASE + +--- *When nothing goes right... Go left!* +-- +-- === +-- +-- # The ASTAR Concept +-- +-- Pathfinding algorithm. +-- +-- +-- # Start and Goal +-- +-- The first thing we need to define is obviously the place where we want to start and where we want to go eventually. +-- +-- ## Start +-- +-- The start +-- +-- ## Goal +-- +-- +-- # Nodes +-- +-- ## Rectangular Grid +-- +-- A rectangular grid can be created using the @{#ASTAR.CreateGrid}(*ValidSurfaceTypes, BoxHY, SpaceX, deltaX, deltaY, MarkGrid*), where +-- +-- * *ValidSurfaceTypes* is a table of valid surface types. By default all surface types are valid. +-- * *BoxXY* is the width of the grid perpendicular the the line between start and end node. Default is 40,000 meters (40 km). +-- * *SpaceX* is the additional space behind the start and end nodes. Default is 20,000 meters (20 km). +-- * *deltaX* is the grid spacing between nodes in the direction of start and end node. Default is 2,000 meters (2 km). +-- * *deltaY* is the grid spacing perpendicular to the direction of start and end node. Default is the same as *deltaX*. +-- * *MarkGrid* If set to *true*, this places marker on the F10 map on each grid node. Note that this can stall DCS if too many nodes are created. +-- +-- ## Valid Surfaces +-- +-- Certain unit types can only travel on certain surfaces types, for example +-- +-- * Naval units can only travel on water (that also excludes shallow water in DCS currently), +-- * Ground units can only traval on land. +-- +-- By restricting the surface type in the grid construction, we also reduce the number of nodes, which makes the algorithm more efficient. +-- +-- ## Box Width (BoxHY) +-- +-- The box width needs to be large enough to capture all paths you want to consider. +-- +-- ## Space in X +-- +-- The space in X value is important if the algorithm needs to to backwards from the start node or needs to extend even further than the end node. +-- +-- ## Grid Spacing +-- +-- The grid spacing is an important factor as it determines the number of nodes and hence the performance of the algorithm. It should be as large as possible. +-- However, if the value is too large, the algorithm might fail to get a valid path. +-- +-- A good estimate of the grid spacing is to set it to be smaller (~ half the size) of the smallest gap you need to path. +-- +-- # Valid Neighbours +-- +-- The A* algorithm needs to know if a transition from one node to another is allowed or not. By default, hopping from one node to another is always possible. +-- +-- ## Line of Sight +-- +-- For naval +-- +-- +-- # Heuristic Cost +-- +-- In order to determine the optimal path, the pathfinding algorithm needs to know, how costly it is to go from one node to another. +-- Often, this can simply be determined by the distance between two nodes. Therefore, the default cost function is set to be the 2D distance between two nodes. +-- +-- +-- # Calculate the Path +-- +-- Finally, we have to calculate the path. This is done by the @{ASTAR.GetPath}(*ExcludeStart, ExcludeEnd*) function. This function returns a table of nodes, which +-- describe the optimal path from the start node to the end node. +-- +-- By default, the start and end node are include in the table that is returned. +-- +-- Note that a valid path must not always exist. So you should check if the function returns *nil*. +-- +-- Common reasons that a path cannot be found are: +-- +-- * The grid is too small ==> increase grid size, e.g. *BoxHY* and/or *SpaceX* if you use a rectangular grid. +-- * The grid spacing is too large ==> decrease *deltaX* and/or *deltaY* +-- * There simply is no valid path ==> you are screwed :( +-- +-- +-- # Examples +-- +-- ## Strait of Hormuz +-- +-- Carrier Group finds its way through the Stait of Hormuz. +-- +-- ## +-- +-- +-- +-- @field #ASTAR +ASTAR = { + ClassName = "ASTAR", + Debug = nil, + lid = nil, + nodes = {}, + counter = 1, + Nnodes = 0, + ncost = 0, + ncostcache = 0, + nvalid = 0, + nvalidcache = 0, +} + +--- Node data. +-- @type ASTAR.Node +-- @field #number id Node id. +-- @field Core.Point#COORDINATE coordinate Coordinate of the node. +-- @field #number surfacetype Surface type. +-- @field #table valid Cached valid/invalid nodes. +-- @field #table cost Cached cost. + +--- ASTAR infinity. +-- @field #number INF +ASTAR.INF=1/0 + +--- ASTAR class version. +-- @field #string version +ASTAR.version="0.4.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Add more valid neighbour functions. +-- TODO: Write docs. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new ASTAR object. +-- @param #ASTAR self +-- @return #ASTAR self +function ASTAR:New() + + -- Inherit everything from INTEL class. + local self=BASE:Inherit(self, BASE:New()) --#ASTAR + + self.lid="ASTAR | " + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set coordinate from where to start. +-- @param #ASTAR self +-- @param Core.Point#COORDINATE Coordinate Start coordinate. +-- @return #ASTAR self +function ASTAR:SetStartCoordinate(Coordinate) + + self.startCoord=Coordinate + + return self +end + +--- Set coordinate where you want to go. +-- @param #ASTAR self +-- @param Core.Point#COORDINATE Coordinate end coordinate. +-- @return #ASTAR self +function ASTAR:SetEndCoordinate(Coordinate) + + self.endCoord=Coordinate + + return self +end + +--- Create a node from a given coordinate. +-- @param #ASTAR self +-- @param Core.Point#COORDINATE Coordinate The coordinate where to create the node. +-- @return #ASTAR.Node The node. +function ASTAR:GetNodeFromCoordinate(Coordinate) + + local node={} --#ASTAR.Node + + node.coordinate=Coordinate + node.surfacetype=Coordinate:GetSurfaceType() + node.id=self.counter + + node.valid={} + node.cost={} + + self.counter=self.counter+1 + + return node +end + + +--- Add a node to the table of grid nodes. +-- @param #ASTAR self +-- @param #ASTAR.Node Node The node to be added. +-- @return #ASTAR self +function ASTAR:AddNode(Node) + + self.nodes[Node.id]=Node + self.Nnodes=self.Nnodes+1 + + return self +end + +--- Add a node to the table of grid nodes specifying its coordinate. +-- @param #ASTAR self +-- @param Core.Point#COORDINATE Coordinate The coordinate where the node is created. +-- @return #ASTAR.Node The node. +function ASTAR:AddNodeFromCoordinate(Coordinate) + + local node=self:GetNodeFromCoordinate(Coordinate) + + self:AddNode(node) + + return node +end + +--- Check if the coordinate of a node has is at a valid surface type. +-- @param #ASTAR self +-- @param #ASTAR.Node Node The node to be added. +-- @param #table SurfaceTypes Surface types, for example `{land.SurfaceType.WATER}`. By default all surface types are valid. +-- @return #boolean If true, surface type of node is valid. +function ASTAR:CheckValidSurfaceType(Node, SurfaceTypes) + + if SurfaceTypes then + + if type(SurfaceTypes)~="table" then + SurfaceTypes={SurfaceTypes} + end + + for _,surface in pairs(SurfaceTypes) do + if surface==Node.surfacetype then + return true + end + end + + return false + + else + return true + end + +end + +--- Add a function to determine if a neighbour of a node is valid. +-- @param #ASTAR self +-- @param #function NeighbourFunction Function that needs to return *true* for a neighbour to be valid. +-- @param ... Condition function arguments if any. +-- @return #ASTAR self +function ASTAR:SetValidNeighbourFunction(NeighbourFunction, ...) + + self.ValidNeighbourFunc=NeighbourFunction + + self.ValidNeighbourArg={} + if arg then + self.ValidNeighbourArg=arg + end + + return self +end + + +--- Set valid neighbours to require line of sight between two nodes. +-- @param #ASTAR self +-- @param #number CorridorWidth Width of LoS corridor in meters. +-- @return #ASTAR self +function ASTAR:SetValidNeighbourLoS(CorridorWidth) + + self:SetValidNeighbourFunction(ASTAR.LoS, CorridorWidth) + + return self +end + +--- Set valid neighbours to be in a certain distance. +-- @param #ASTAR self +-- @param #number MaxDistance Max distance between nodes in meters. Default is 2000 m. +-- @return #ASTAR self +function ASTAR:SetValidNeighbourDistance(MaxDistance) + + self:SetValidNeighbourFunction(ASTAR.DistMax, MaxDistance) + + return self +end + +--- Set valid neighbours to be in a certain distance. +-- @param #ASTAR self +-- @param #number MaxDistance Max distance between nodes in meters. Default is 2000 m. +-- @return #ASTAR self +function ASTAR:SetValidNeighbourRoad(MaxDistance) + + self:SetValidNeighbourFunction(ASTAR.Road, MaxDistance) + + return self +end + +--- Set the function which calculates the "cost" to go from one to another node. +-- The first to arguments of this function are always the two nodes under consideration. But you can add optional arguments. +-- Very often the distance between nodes is a good measure for the cost. +-- @param #ASTAR self +-- @param #function CostFunction Function that returns the "cost". +-- @param ... Condition function arguments if any. +-- @return #ASTAR self +function ASTAR:SetCostFunction(CostFunction, ...) + + self.CostFunc=CostFunction + + self.CostArg={} + if arg then + self.CostArg=arg + end + + return self +end + +--- Set heuristic cost to go from one node to another to be their 2D distance. +-- @param #ASTAR self +-- @return #ASTAR self +function ASTAR:SetCostDist2D() + + self:SetCostFunction(ASTAR.Dist2D) + + return self +end + +--- Set heuristic cost to go from one node to another to be their 3D distance. +-- @param #ASTAR self +-- @return #ASTAR self +function ASTAR:SetCostDist3D() + + self:SetCostFunction(ASTAR.Dist3D) + + return self +end + +--- Set heuristic cost to go from one node to another to be their 3D distance. +-- @param #ASTAR self +-- @return #ASTAR self +function ASTAR:SetCostRoad() + + self:SetCostFunction(ASTAR) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Grid functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a rectangular grid of nodes between star and end coordinate. +-- The coordinate system is oriented along the line between start and end point. +-- @param #ASTAR self +-- @param #table ValidSurfaceTypes Valid surface types. By default is all surfaces are allowed. +-- @param #number BoxHY Box "height" in meters along the y-coordinate. Default 40000 meters (40 km). +-- @param #number SpaceX Additional space in meters before start and after end coordinate. Default 10000 meters (10 km). +-- @param #number deltaX Increment in the direction of start to end coordinate in meters. Default 2000 meters. +-- @param #number deltaY Increment perpendicular to the direction of start to end coordinate in meters. Default is same as deltaX. +-- @param #boolean MarkGrid If true, create F10 map markers at grid nodes. +-- @return #ASTAR self +function ASTAR:CreateGrid(ValidSurfaceTypes, BoxHY, SpaceX, deltaX, deltaY, MarkGrid) + + -- Note that internally + -- x coordinate is z: x-->z Line from start to end + -- y coordinate is x: y-->x Perpendicular + + -- Grid length and width. + local Dz=SpaceX or 10000 + local Dx=BoxHY and BoxHY/2 or 20000 + + -- Increments. + local dz=deltaX or 2000 + local dx=deltaY or dz + + -- Heading from start to end coordinate. + local angle=self.startCoord:HeadingTo(self.endCoord) + + --Distance between start and end. + local dist=self.startCoord:Get2DDistance(self.endCoord)+2*Dz + + -- Origin of map. Needed to translate back to wanted position. + local co=COORDINATE:New(0, 0, 0) + local do1=co:Get2DDistance(self.startCoord) + local ho1=co:HeadingTo(self.startCoord) + + -- Start of grid. + local xmin=-Dx + local zmin=-Dz + + -- Number of grid points. + local nz=dist/dz+1 + local nx=2*Dx/dx+1 + + -- Debug info. + local text=string.format("Building grid with nx=%d ny=%d => total=%d nodes", nx, nz, nx*nz) + self:T(self.lid..text) + + -- Loop over x and z coordinate to create a 2D grid. + for i=1,nx do + + -- x coordinate perpendicular to z. + local x=xmin+dx*(i-1) + + for j=1,nz do + + -- z coordinate connecting start and end. + local z=zmin+dz*(j-1) + + -- Rotate 2D. + local vec3=UTILS.Rotate2D({x=x, y=0, z=z}, angle) + + -- Coordinate of the node. + local c=COORDINATE:New(vec3.z, vec3.y, vec3.x):Translate(do1, ho1, true) + + -- Create a node at this coordinate. + local node=self:GetNodeFromCoordinate(c) + + -- Check if node has valid surface type. + if self:CheckValidSurfaceType(node, ValidSurfaceTypes) then + + if MarkGrid then + c:MarkToAll(string.format("i=%d, j=%d surface=%d", i, j, node.surfacetype)) + end + + -- Add node to grid. + self:AddNode(node) + + end + + end + end + + -- Debug info. + local text=string.format("Done building grid!") + self:T2(self.lid..text) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Valid neighbour functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Function to check if two nodes have line of sight (LoS). +-- @param #ASTAR.Node nodeA First node. +-- @param #ASTAR.Node nodeB Other node. +-- @param #number corridor (Optional) Width of corridor in meters. +-- @return #boolean If true, two nodes have LoS. +function ASTAR.LoS(nodeA, nodeB, corridor) + + local offset=1 + + local dx=corridor and corridor/2 or nil + local dy=dx + + local cA=nodeA.coordinate:GetVec3() + local cB=nodeB.coordinate:GetVec3() + cA.y=offset + cB.y=offset + + local los=land.isVisible(cA, cB) + + if los and corridor then + + -- Heading from A to B. + local heading=nodeA.coordinate:HeadingTo(nodeB.coordinate) + + local Ap=UTILS.VecTranslate(cA, dx, heading+90) + local Bp=UTILS.VecTranslate(cB, dx, heading+90) + + los=land.isVisible(Ap, Bp) + + if los then + + local Am=UTILS.VecTranslate(cA, dx, heading-90) + local Bm=UTILS.VecTranslate(cB, dx, heading-90) + + los=land.isVisible(Am, Bm) + end + + end + + return los +end + +--- Function to check if two nodes have a road connection. +-- @param #ASTAR.Node nodeA First node. +-- @param #ASTAR.Node nodeB Other node. +-- @return #boolean If true, two nodes are connected via a road. +function ASTAR.Road(nodeA, nodeB) + + local path=land.findPathOnRoads("roads", nodeA.coordinate.x, nodeA.coordinate.z, nodeB.coordinate.x, nodeB.coordinate.z) + + if path then + return true + else + return false + end + +end + +--- Function to check if distance between two nodes is less than a threshold distance. +-- @param #ASTAR.Node nodeA First node. +-- @param #ASTAR.Node nodeB Other node. +-- @param #number distmax Max distance in meters. Default is 2000 m. +-- @return #boolean If true, distance between the two nodes is below threshold. +function ASTAR.DistMax(nodeA, nodeB, distmax) + + distmax=distmax or 2000 + + local dist=nodeA.coordinate:Get2DDistance(nodeB.coordinate) + + return dist<=distmax +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Heuristic cost functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Heuristic cost is given by the 2D distance between the nodes. +-- @param #ASTAR.Node nodeA First node. +-- @param #ASTAR.Node nodeB Other node. +-- @return #number Distance between the two nodes. +function ASTAR.Dist2D(nodeA, nodeB) + local dist=nodeA.coordinate:Get2DDistance(nodeB) + return dist +end + +--- Heuristic cost is given by the 3D distance between the nodes. +-- @param #ASTAR.Node nodeA First node. +-- @param #ASTAR.Node nodeB Other node. +-- @return #number Distance between the two nodes. +function ASTAR.Dist3D(nodeA, nodeB) + local dist=nodeA.coordinate:Get3DDistance(nodeB.coordinate) + return dist +end + +--- Heuristic cost is given by the distance between the nodes on road. +-- @param #ASTAR.Node nodeA First node. +-- @param #ASTAR.Node nodeB Other node. +-- @return #number Distance between the two nodes. +function ASTAR.DistRoad(nodeA, nodeB) + + -- Get the path. + local path=land.findPathOnRoads("roads", nodeA.coordinate.x, nodeA.coordinate.z, nodeB.coordinate.x, nodeB.coordinate.z) + + if path then + + local dist=0 + + for i=2,#path do + local b=path[i] --DCS#Vec2 + local a=path[i-1] --DCS#Vec2 + + dist=dist+UTILS.VecDist2D(a,b) + + end + + return dist + end + + + return math.huge +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Find the closest node from a given coordinate. +-- @param #ASTAR self +-- @param Core.Point#COORDINATE Coordinate. +-- @return #ASTAR.Node Cloest node to the coordinate. +-- @return #number Distance to closest node in meters. +function ASTAR:FindClosestNode(Coordinate) + + local distMin=math.huge + local closeNode=nil + + for _,_node in pairs(self.nodes) do + local node=_node --#ASTAR.Node + + local dist=node.coordinate:Get2DDistance(Coordinate) + + if dist1000 then + self:T(self.lid.."Adding start node to node grid!") + self:AddNode(node) + end + + return self +end + +--- Add a node. +-- @param #ASTAR self +-- @param #ASTAR.Node Node The node to be added to the nodes table. +-- @return #ASTAR self +function ASTAR:FindEndNode() + + local node, dist=self:FindClosestNode(self.endCoord) + + self.endNode=node + + if dist>1000 then + self:T(self.lid.."Adding end node to node grid!") + self:AddNode(node) + end + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Main A* pathfinding function +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- A* pathfinding function. This seaches the path along nodes between start and end nodes/coordinates. +-- @param #ASTAR self +-- @param #boolean ExcludeStartNode If *true*, do not include start node in found path. Default is to include it. +-- @param #boolean ExcludeEndNode If *true*, do not include end node in found path. Default is to include it. +-- @return #table Table of nodes from start to finish. +function ASTAR:GetPath(ExcludeStartNode, ExcludeEndNode) + + self:FindStartNode() + self:FindEndNode() + + local nodes=self.nodes + local start=self.startNode + local goal=self.endNode + + -- Sets. + local openset = {} + local closedset = {} + local came_from = {} + local g_score = {} + local f_score = {} + + openset[start.id]=true + local Nopen=1 + + -- Initial scores. + g_score[start.id]=0 + f_score[start.id]=g_score[start.id]+self:_HeuristicCost(start, goal) + + -- Set start time. + local T0=timer.getAbsTime() + + -- Debug message. + local text=string.format("Starting A* pathfinding with %d Nodes", self.Nnodes) + self:T(self.lid..text) + + local Tstart=UTILS.GetOSTime() + + -- Loop while we still have an open set. + while Nopen > 0 do + + -- Get current node. + local current=self:_LowestFscore(openset, f_score) + + -- Check if we are at the end node. + if current.id==goal.id then + + local path=self:_UnwindPath({}, came_from, goal) + + if not ExcludeEndNode then + table.insert(path, goal) + end + + if ExcludeStartNode then + table.remove(path, 1) + end + + local Tstop=UTILS.GetOSTime() + + local dT=nil + if Tstart and Tstop then + dT=Tstop-Tstart + end + + -- Debug message. + local text=string.format("Found path with %d nodes (%d total)", #path, self.Nnodes) + if dT then + text=text..string.format(", OS Time %.6f sec", dT) + end + text=text..string.format(", Nvalid=%d [%d cached]", self.nvalid, self.nvalidcache) + text=text..string.format(", Ncost=%d [%d cached]", self.ncost, self.ncostcache) + self:T(self.lid..text) + + return path + end + + -- Move Node from open to closed set. + openset[current.id]=nil + Nopen=Nopen-1 + closedset[current.id]=true + + -- Get neighbour nodes. + local neighbors=self:_NeighbourNodes(current, nodes) + + -- Loop over neighbours. + for _,neighbor in pairs(neighbors) do + + if self:_NotIn(closedset, neighbor.id) then + + local tentative_g_score=g_score[current.id]+self:_DistNodes(current, neighbor) + + if self:_NotIn(openset, neighbor.id) or tentative_g_score < g_score[neighbor.id] then + + came_from[neighbor]=current + + g_score[neighbor.id]=tentative_g_score + f_score[neighbor.id]=g_score[neighbor.id]+self:_HeuristicCost(neighbor, goal) + + if self:_NotIn(openset, neighbor.id) then + -- Add to open set. + openset[neighbor.id]=true + Nopen=Nopen+1 + end + + end + end + end + end + + -- Debug message. + local text=string.format("WARNING: Could NOT find valid path!") + self:E(self.lid..text) + MESSAGE:New(text, 60, "ASTAR"):ToAllIf(self.Debug) + + return nil -- no valid path +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- A* pathfinding helper functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Heuristic "cost" function to go from node A to node B. Default is the distance between the nodes. +-- @param #ASTAR self +-- @param #ASTAR.Node nodeA Node A. +-- @param #ASTAR.Node nodeB Node B. +-- @return #number "Cost" to go from node A to node B. +function ASTAR:_HeuristicCost(nodeA, nodeB) + + -- Counter. + self.ncost=self.ncost+1 + + -- Get chached cost if available. + local cost=nodeA.cost[nodeB.id] + if cost~=nil then + self.ncostcache=self.ncostcache+1 + return cost + end + + local cost=nil + if self.CostFunc then + cost=self.CostFunc(nodeA, nodeB, unpack(self.CostArg)) + else + cost=self:_DistNodes(nodeA, nodeB) + end + + nodeA.cost[nodeB.id]=cost + nodeB.cost[nodeA.id]=cost -- Symmetric problem. + + return cost +end + +--- Check if going from a node to a neighbour is possible. +-- @param #ASTAR self +-- @param #ASTAR.Node node A node. +-- @param #ASTAR.Node neighbor Neighbour node. +-- @return #boolean If true, transition between nodes is possible. +function ASTAR:_IsValidNeighbour(node, neighbor) + + -- Counter. + self.nvalid=self.nvalid+1 + + local valid=node.valid[neighbor.id] + if valid~=nil then + --env.info(string.format("Node %d has valid=%s neighbour %d", node.id, tostring(valid), neighbor.id)) + self.nvalidcache=self.nvalidcache+1 + return valid + end + + local valid=nil + if self.ValidNeighbourFunc then + valid=self.ValidNeighbourFunc(node, neighbor, unpack(self.ValidNeighbourArg)) + else + valid=true + end + + node.valid[neighbor.id]=valid + neighbor.valid[node.id]=valid -- Symmetric problem. + + return valid +end + +--- Calculate 2D distance between two nodes. +-- @param #ASTAR self +-- @param #ASTAR.Node nodeA Node A. +-- @param #ASTAR.Node nodeB Node B. +-- @return #number Distance between nodes in meters. +function ASTAR:_DistNodes(nodeA, nodeB) + return nodeA.coordinate:Get2DDistance(nodeB.coordinate) +end + +--- Function that calculates the lowest F score. +-- @param #ASTAR self +-- @param #table set The set of nodes IDs. +-- @param #number f_score F score. +-- @return #ASTAR.Node Best node. +function ASTAR:_LowestFscore(set, f_score) + + local lowest, bestNode = ASTAR.INF, nil + + for nid,node in pairs(set) do + + local score=f_score[nid] + + if score result=%s", tostring(isGen), tostring(isAny), tostring(isAll), tostring(self.negateResult), tostring(result))) + + return result +end + +--- Check if all given condition are true. +-- @param #CONDITION self +-- @param #table functions Functions to evaluate. +-- @return #boolean If true, all conditions were true (or functions was empty/nil). Returns false if at least one condition returned false. +function CONDITION:_EvalConditionsAll(functions) + + -- At least one condition? + local gotone=false + + + -- Any stop condition must be true. + for _,_condition in pairs(functions or {}) do + local condition=_condition --#CONDITION.Function + + -- At least one condition was defined. + gotone=true + + -- 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 + + +--- Check if any of the given conditions is true. +-- @param #CONDITION self +-- @param #table functions Functions to evaluate. +-- @return #boolean If true, at least one condition is true (or functions was emtpy/nil). +function CONDITION:_EvalConditionsAny(functions) + + -- At least one condition? + local gotone=false + + -- Any stop condition must be true. + for _,_condition in pairs(functions or {}) do + local condition=_condition --#CONDITION.Function + + -- At least one condition was defined. + gotone=true + + -- Call function. + local istrue=condition.func(unpack(condition.arg)) + + -- Any true will return true. + if istrue then + return true + end + + end + + -- No condition was true. + if gotone then + return false + else + -- No functions passed. + return true + end +end + +--- Create conditon function object. +-- @param #CONDITION self +-- @param #function Function The function to call. +-- @param ... (Optional) Parameters passed to the function (if any). +-- @return #CONDITION.Function Condition function. +function CONDITION:_CreateCondition(Function, ...) + + local condition={} --#CONDITION.Function + + condition.func=Function + condition.arg={} + if arg then + condition.arg=arg + end + + return condition +end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- \ No newline at end of file diff --git a/Moose Development/Moose/Core/Database.lua b/Moose Development/Moose/Core/Database.lua index 1cc49fde7..e80f36558 100644 --- a/Moose Development/Moose/Core/Database.lua +++ b/Moose Development/Moose/Core/Database.lua @@ -31,6 +31,7 @@ -- @module Core.Database -- @image Core_Database.JPG + --- @type DATABASE -- @field #string ClassName Name of the class. -- @field #table Templates Templates: Units, Groups, Statics, ClientsByName, ClientsByID. @@ -50,7 +51,7 @@ -- * PLAYERS -- * CARGOS -- --- On top, for internal MOOSE administration purposes, the DATABASE administers the Unit and Group TEMPLATES as defined within the Mission Editor. +-- On top, for internal MOOSE administration purposes, the DATBASE administers the Unit and Group TEMPLATES as defined within the Mission Editor. -- -- The singleton object **_DATABASE** is automatically created by MOOSE, that administers all objects within the mission. -- Moose refers to **_DATABASE** within the framework extensively, but you can also refer to the _DATABASE object within your missions if required. @@ -89,19 +90,22 @@ DATABASE = { FLIGHTCONTROLS = {}, } -local _DATABASECoalition = { - [1] = "Red", - [2] = "Blue", - [3] = "Neutral", -} +local _DATABASECoalition = + { + [1] = "Red", + [2] = "Blue", + [3] = "Neutral", + } + +local _DATABASECategory = + { + ["plane"] = Unit.Category.AIRPLANE, + ["helicopter"] = Unit.Category.HELICOPTER, + ["vehicle"] = Unit.Category.GROUND_UNIT, + ["ship"] = Unit.Category.SHIP, + ["static"] = Unit.Category.STRUCTURE, + } -local _DATABASECategory = { - ["plane"] = Unit.Category.AIRPLANE, - ["helicopter"] = Unit.Category.HELICOPTER, - ["vehicle"] = Unit.Category.GROUND_UNIT, - ["ship"] = Unit.Category.SHIP, - ["static"] = Unit.Category.STRUCTURE, -} --- Creates a new DATABASE object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. -- @param #DATABASE self @@ -120,20 +124,21 @@ function DATABASE:New() self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) self:HandleEvent( EVENTS.RemoveUnit, self._EventOnDeadOrCrash ) + --self:HandleEvent( EVENTS.UnitLost, self._EventOnDeadOrCrash ) -- DCS 2.7.1 for Aerial units no dead event ATM self:HandleEvent( EVENTS.Hit, self.AccountHits ) self:HandleEvent( EVENTS.NewCargo ) self:HandleEvent( EVENTS.DeleteCargo ) self:HandleEvent( EVENTS.NewZone ) self:HandleEvent( EVENTS.DeleteZone ) - -- self:HandleEvent( EVENTS.PlayerEnterUnit, self._EventOnPlayerEnterUnit ) -- This is not working anymore!, handling this through the birth event. + --self:HandleEvent( EVENTS.PlayerEnterUnit, self._EventOnPlayerEnterUnit ) -- This is not working anymore!, handling this through the birth event. self:HandleEvent( EVENTS.PlayerLeaveUnit, self._EventOnPlayerLeaveUnit ) self:_RegisterTemplates() self:_RegisterGroupsAndUnits() self:_RegisterClients() self:_RegisterStatics() - -- self:_RegisterAirbases() - -- self:_RegisterPlayers() + --self:_RegisterPlayers() + --self:_RegisterAirbases() self.UNITS_Position = 0 @@ -150,6 +155,7 @@ function DATABASE:FindUnit( UnitName ) return UnitFound end + --- Adds a Unit based on the Unit Name in the DATABASE. -- @param #DATABASE self -- @param #string DCSUnitName Unit name. @@ -157,26 +163,20 @@ end function DATABASE:AddUnit( DCSUnitName ) if not self.UNITS[DCSUnitName] then - -- 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 ) + self.UNITS[DCSUnitName]=UNIT:Register(DCSUnitName) end return self.UNITS[DCSUnitName] end + --- Deletes a Unit from the DATABASE based on the Unit Name. -- @param #DATABASE self function DATABASE:DeleteUnit( DCSUnitName ) - self.UNITS[DCSUnitName] = nil end @@ -194,6 +194,7 @@ function DATABASE:AddStatic( DCSStaticName ) return nil end + --- Deletes a Static from the DATABASE based on the Static Name. -- @param #DATABASE self function DATABASE:DeleteStatic( DCSStaticName ) @@ -210,16 +211,6 @@ function DATABASE:FindStatic( StaticName ) return StaticFound end ---- Finds a AIRBASE based on the AirbaseName. --- @param #DATABASE self --- @param #string AirbaseName --- @return Wrapper.Airbase#AIRBASE The found AIRBASE. -function DATABASE:FindAirbase( AirbaseName ) - - local AirbaseFound = self.AIRBASES[AirbaseName] - return AirbaseFound -end - --- Adds a Airbase based on the Airbase Name in the DATABASE. -- @param #DATABASE self -- @param #string AirbaseName The name of the airbase. @@ -233,6 +224,7 @@ function DATABASE:AddAirbase( AirbaseName ) return self.AIRBASES[AirbaseName] end + --- Deletes a Airbase from the DATABASE based on the Airbase Name. -- @param #DATABASE self -- @param #string AirbaseName The name of the airbase @@ -241,6 +233,17 @@ function DATABASE:DeleteAirbase( AirbaseName ) self.AIRBASES[AirbaseName] = nil end +--- Finds an AIRBASE based on the AirbaseName. +-- @param #DATABASE self +-- @param #string AirbaseName +-- @return Wrapper.Airbase#AIRBASE The found AIRBASE. +function DATABASE:FindAirbase( AirbaseName ) + + local AirbaseFound = self.AIRBASES[AirbaseName] + return AirbaseFound +end + + do -- Zones --- Finds a @{Zone} based on the zone name. @@ -264,6 +267,7 @@ do -- Zones end end + --- Deletes a @{Zone} from the DATABASE based on the zone name. -- @param #DATABASE self -- @param #string ZoneName The name of the zone. @@ -272,29 +276,30 @@ do -- Zones self.ZONES[ZoneName] = nil end + --- Private method that registers new ZONE_BASE derived objects within the DATABASE Object. -- @param #DATABASE self -- @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 } + local color=ZoneData.color or {1, 0, 0, 0.15} -- Create new Zone - local Zone = nil -- Core.Zone#ZONE_BASE + local Zone=nil --Core.Zone#ZONE_BASE - if ZoneData.type == 0 then + if ZoneData.type==0 then --- -- Circular zone --- - self:I( string.format( "Register ZONE: %s (Circular)", ZoneName ) ) + self:I(string.format("Register ZONE: %s (Circular)", ZoneName)) - Zone = ZONE:New( ZoneName ) + Zone=ZONE:New(ZoneName) else @@ -302,27 +307,41 @@ do -- Zones -- Quad-point zone --- - self:I( string.format( "Register ZONE: %s (Polygon, Quad)", ZoneName ) ) + self:I(string.format("Register ZONE: %s (Polygon, Quad)", ZoneName)) - Zone = ZONE_POLYGON_BASE:New( ZoneName, ZoneData.verticies ) + Zone=ZONE_POLYGON_BASE:New(ZoneName, ZoneData.verticies) - -- for i,vec2 in pairs(ZoneData.verticies) do + --for i,vec2 in pairs(ZoneData.verticies) do -- local coord=COORDINATE:NewFromVec2(vec2) -- coord:MarkToAll(string.format("%s Point %d", ZoneName, i)) - -- end + --end end if Zone then - -- Store color of zone. - Zone.Color = color + -- Store color of zone. + Zone.Color=color + + -- Store zone ID. + Zone.ZoneID=ZoneData.zoneId + + -- Store zone properties (if any) + local ZoneProperties = ZoneData.properties or nil + Zone.Properties = {} + if ZoneName and ZoneProperties then + for _,ZoneProp in ipairs(ZoneProperties) do + if ZoneProp.key then + Zone.Properties[ZoneProp.key] = ZoneProp.value + end + end + end -- Store in DB. self.ZONENAMES[ZoneName] = ZoneName -- Add zone. - self:AddZone( ZoneName, Zone ) + self:AddZone(ZoneName, Zone) end @@ -330,20 +349,20 @@ do -- Zones -- Polygon zones defined by late activated groups. for ZoneGroupName, ZoneGroup in pairs( self.GROUPS ) do - if ZoneGroupName:match( "#ZONE_POLYGON" ) then + if ZoneGroupName:match("#ZONE_POLYGON") then - local ZoneName1 = ZoneGroupName:match( "(.*)#ZONE_POLYGON" ) - local ZoneName2 = ZoneGroupName:match( ".*#ZONE_POLYGON(.*)" ) - local ZoneName = ZoneName1 .. (ZoneName2 or "") + local ZoneName1 = ZoneGroupName:match("(.*)#ZONE_POLYGON") + local ZoneName2 = ZoneGroupName:match(".*#ZONE_POLYGON(.*)") + local ZoneName = ZoneName1 .. ( ZoneName2 or "" ) -- Debug output - self:I( string.format( "Register ZONE: %s (Polygon)", ZoneName ) ) + 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 ) + Zone_Polygon:SetColor({1, 0, 0}, 0.15) -- Store name in DB. self.ZONENAMES[ZoneName] = ZoneName @@ -355,6 +374,7 @@ do -- Zones end + end -- zone do -- Zone_Goal @@ -380,6 +400,7 @@ do -- Zone_Goal end end + --- Deletes a @{Zone} from the DATABASE based on the zone name. -- @param #DATABASE self -- @param #string ZoneName The name of the zone. @@ -401,6 +422,7 @@ do -- cargo end end + --- Deletes a Cargo from the DATABASE based on the Cargo Name. -- @param #DATABASE self -- @param #string CargoName The name of the airbase @@ -442,38 +464,38 @@ do -- cargo for CargoGroupName, CargoGroup in pairs( Groups ) do if self:IsCargo( CargoGroupName ) then - local CargoInfo = CargoGroupName:match( "#CARGO(.*)" ) - local CargoParam = CargoInfo and CargoInfo:match( "%((.*)%)" ) - local CargoName1 = CargoGroupName:match( "(.*)#CARGO%(.*%)" ) - local CargoName2 = CargoGroupName:match( ".*#CARGO%(.*%)(.*)" ) - local CargoName = CargoName1 .. (CargoName2 or "") - local Type = CargoParam and CargoParam:match( "T=([%a%d ]+),?" ) - local Name = CargoParam and CargoParam:match( "N=([%a%d]+),?" ) or CargoName - local LoadRadius = CargoParam and tonumber( CargoParam:match( "RR=([%a%d]+),?" ) ) - local NearRadius = CargoParam and tonumber( CargoParam:match( "NR=([%a%d]+),?" ) ) + local CargoInfo = CargoGroupName:match("#CARGO(.*)") + local CargoParam = CargoInfo and CargoInfo:match( "%((.*)%)") + local CargoName1 = CargoGroupName:match("(.*)#CARGO%(.*%)") + local CargoName2 = CargoGroupName:match(".*#CARGO%(.*%)(.*)") + local CargoName = CargoName1 .. ( CargoName2 or "" ) + local Type = CargoParam and CargoParam:match( "T=([%a%d ]+),?") + local Name = CargoParam and CargoParam:match( "N=([%a%d]+),?") or CargoName + local LoadRadius = CargoParam and tonumber( CargoParam:match( "RR=([%a%d]+),?") ) + local NearRadius = CargoParam and tonumber( CargoParam:match( "NR=([%a%d]+),?") ) - self:I( { "Register CargoGroup:", Type = Type, Name = Name, LoadRadius = LoadRadius, NearRadius = NearRadius } ) + self:I({"Register CargoGroup:",Type=Type,Name=Name,LoadRadius=LoadRadius,NearRadius=NearRadius}) CARGO_GROUP:New( CargoGroup, Type, Name, LoadRadius, NearRadius ) end end for CargoStaticName, CargoStatic in pairs( self.STATICS ) do if self:IsCargo( CargoStaticName ) then - local CargoInfo = CargoStaticName:match( "#CARGO(.*)" ) - local CargoParam = CargoInfo and CargoInfo:match( "%((.*)%)" ) - local CargoName = CargoStaticName:match( "(.*)#CARGO" ) - local Type = CargoParam and CargoParam:match( "T=([%a%d ]+),?" ) - local Category = CargoParam and CargoParam:match( "C=([%a%d ]+),?" ) - local Name = CargoParam and CargoParam:match( "N=([%a%d]+),?" ) or CargoName - local LoadRadius = CargoParam and tonumber( CargoParam:match( "RR=([%a%d]+),?" ) ) - local NearRadius = CargoParam and tonumber( CargoParam:match( "NR=([%a%d]+),?" ) ) + local CargoInfo = CargoStaticName:match("#CARGO(.*)") + local CargoParam = CargoInfo and CargoInfo:match( "%((.*)%)") + local CargoName = CargoStaticName:match("(.*)#CARGO") + local Type = CargoParam and CargoParam:match( "T=([%a%d ]+),?") + local Category = CargoParam and CargoParam:match( "C=([%a%d ]+),?") + local Name = CargoParam and CargoParam:match( "N=([%a%d]+),?") or CargoName + local LoadRadius = CargoParam and tonumber( CargoParam:match( "RR=([%a%d]+),?") ) + local NearRadius = CargoParam and tonumber( CargoParam:match( "NR=([%a%d]+),?") ) if Category == "SLING" then - self:I( { "Register CargoSlingload:", Type = Type, Name = Name, LoadRadius = LoadRadius, NearRadius = NearRadius } ) + self:I({"Register CargoSlingload:",Type=Type,Name=Name,LoadRadius=LoadRadius,NearRadius=NearRadius}) CARGO_SLINGLOAD:New( CargoStatic, Type, Name, LoadRadius, NearRadius ) else if Category == "CRATE" then - self:I( { "Register CargoCrate:", Type = Type, Name = Name, LoadRadius = LoadRadius, NearRadius = NearRadius } ) + self:I({"Register CargoCrate:",Type=Type,Name=Name,LoadRadius=LoadRadius,NearRadius=NearRadius}) CARGO_CRATE:New( CargoStatic, Type, Name, LoadRadius, NearRadius ) end end @@ -494,6 +516,7 @@ function DATABASE:FindClient( ClientName ) return ClientFound end + --- Adds a CLIENT based on the ClientName in the DATABASE. -- @param #DATABASE self -- @param #string ClientName Name of the Client unit. @@ -507,6 +530,7 @@ function DATABASE:AddClient( ClientName ) return self.CLIENTS[ClientName] end + --- Finds a GROUP based on the GroupName. -- @param #DATABASE self -- @param #string GroupName @@ -517,6 +541,7 @@ function DATABASE:FindGroup( GroupName ) return GroupFound end + --- Adds a GROUP based on the GroupName in the DATABASE. -- @param #DATABASE self function DATABASE:AddGroup( GroupName ) @@ -564,6 +589,7 @@ function DATABASE:GetPlayers() return self.PLAYERS end + --- Get the player table from the DATABASE, which contains all UNIT objects. -- The player table contains all UNIT objects of the player with the key the name of the player (PlayerName). -- @param #DATABASE self @@ -576,6 +602,7 @@ function DATABASE:GetPlayerUnits() return self.PLAYERUNITS end + --- Get the player table from the DATABASE which have joined in the mission historically. -- The player table contains all UNIT objects with the key the name of the player (PlayerName). -- @param #DATABASE self @@ -588,6 +615,7 @@ function DATABASE:GetPlayersJoined() return self.PLAYERSJOINED end + --- Instantiate new Groups within the DCSRTE. -- This method expects EXACTLY the same structure as a structure within the ME, and needs 2 additional fields defined: -- SpawnCountryID, SpawnCategoryID @@ -610,7 +638,7 @@ function DATABASE:Spawn( SpawnTemplate ) SpawnTemplate.CountryID = nil SpawnTemplate.CategoryID = nil - self:_RegisterGroupTemplate( SpawnTemplate, SpawnCoalitionID, SpawnCategoryID, SpawnCountryID ) + self:_RegisterGroupTemplate( SpawnTemplate, SpawnCoalitionID, SpawnCategoryID, SpawnCountryID ) self:T3( SpawnTemplate ) coalition.addGroup( SpawnCountryID, SpawnCategoryID, SpawnTemplate ) @@ -692,7 +720,7 @@ function DATABASE:_RegisterGroupTemplate( GroupTemplate, CoalitionSide, Category for unit_num, UnitTemplate in pairs( GroupTemplate.units ) do - UnitTemplate.name = env.getValueDictByKey( UnitTemplate.name ) + UnitTemplate.name = env.getValueDictByKey(UnitTemplate.name) self.Templates.Units[UnitTemplate.name] = {} self.Templates.Units[UnitTemplate.name].UnitName = UnitTemplate.name @@ -712,16 +740,17 @@ function DATABASE:_RegisterGroupTemplate( GroupTemplate, CoalitionSide, Category self.Templates.ClientsByID[UnitTemplate.unitId] = UnitTemplate end - UnitNames[#UnitNames + 1] = self.Templates.Units[UnitTemplate.name].UnitName + UnitNames[#UnitNames+1] = self.Templates.Units[UnitTemplate.name].UnitName end -- Debug info. - self:T( { Group = self.Templates.Groups[GroupTemplateName].GroupName, + self:T( { Group = self.Templates.Groups[GroupTemplateName].GroupName, Coalition = self.Templates.Groups[GroupTemplateName].CoalitionID, - Category = self.Templates.Groups[GroupTemplateName].CategoryID, - Country = self.Templates.Groups[GroupTemplateName].CountryID, - Units = UnitNames, - } ) + Category = self.Templates.Groups[GroupTemplateName].CategoryID, + Country = self.Templates.Groups[GroupTemplateName].CountryID, + Units = UnitNames + } + ) end --- Get group template. @@ -747,7 +776,7 @@ function DATABASE:_RegisterStaticTemplate( StaticTemplate, CoalitionID, Category local StaticTemplate = UTILS.DeepCopy( StaticTemplate ) - local StaticTemplateGroupName = env.getValueDictByKey( StaticTemplate.name ) + local StaticTemplateGroupName = env.getValueDictByKey(StaticTemplate.name) local StaticTemplateName=StaticTemplate.units[1].name @@ -768,8 +797,9 @@ function DATABASE:_RegisterStaticTemplate( StaticTemplate, CoalitionID, Category 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, - } ) + Country = self.Templates.Statics[StaticTemplateName].CountryID + } + ) self:AddStatic( StaticTemplateName ) @@ -785,7 +815,7 @@ 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 else - self:E( "ERROR: Static group template does NOT exist for static " .. tostring( StaticName ) ) + self:E("ERROR: Static group template does NOT exist for static "..tostring(StaticName)) return nil end end @@ -799,7 +829,7 @@ 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 else - self:E( "ERROR: Static unit template does NOT exist for static " .. tostring( StaticName ) ) + self:E("ERROR: Static unit template does NOT exist for static "..tostring(StaticName)) return nil end end @@ -812,7 +842,7 @@ function DATABASE:GetGroupNameFromUnitName( UnitName ) 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 ) ) + self:E("ERROR: Unit template does not exist for unit "..tostring(UnitName)) return nil end end @@ -825,11 +855,25 @@ function DATABASE:GetGroupTemplateFromUnitName( UnitName ) 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 ) ) + 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:GetUnitTemplateFromUnitName( UnitName ) + if self.Templates.Units[UnitName] then + return self.Templates.Units[UnitName] + 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. @@ -872,6 +916,8 @@ function DATABASE:GetCategoryFromAirbase( AirbaseName ) return self.AIRBASES[AirbaseName]:GetCategory() end + + --- Private method that registers all alive players in the mission. -- @param #DATABASE self -- @return #DATABASE self @@ -895,12 +941,13 @@ function DATABASE:_RegisterPlayers() return self end ---- Private method that registers all Groups and Units within the mission. + +--- Private method that registers all Groups and Units within in the mission. -- @param #DATABASE self -- @return #DATABASE self function DATABASE:_RegisterGroupsAndUnits() - local CoalitionsData = { GroupsRed = coalition.getGroups( coalition.side.RED ), GroupsBlue = coalition.getGroups( coalition.side.BLUE ), GroupsNeutral = coalition.getGroups( coalition.side.NEUTRAL ) } + 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 @@ -912,7 +959,7 @@ function DATABASE:_RegisterGroupsAndUnits() local DCSGroupName = DCSGroup:getName() -- Add group. - self:I( string.format( "Register Group: %s", tostring( DCSGroupName ) ) ) + self:I(string.format("Register Group: %s", tostring(DCSGroupName))) self:AddGroup( DCSGroupName ) -- Loop over units in group. @@ -922,12 +969,12 @@ function DATABASE:_RegisterGroupsAndUnits() local DCSUnitName = DCSUnit:getName() -- Add unit. - self:I( string.format( "Register Unit: %s", tostring( DCSUnitName ) ) ) + 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 @@ -936,24 +983,25 @@ function DATABASE:_RegisterGroupsAndUnits() return self end ---- Private method that registers all Units of skill Client or Player within the mission. +--- Private method that registers all Units of skill Client or Player within in the mission. -- @param #DATABASE self -- @return #DATABASE self function DATABASE:_RegisterClients() for ClientName, ClientTemplate in pairs( self.Templates.ClientsByName ) do - self:I( string.format( "Register Client: %s", tostring( ClientName ) ) ) - self:AddClient( ClientName ) + self:I(string.format("Register Client: %s", tostring(ClientName))) + local client=self:AddClient( ClientName ) + client.SpawnCoord=COORDINATE:New(ClientTemplate.x, ClientTemplate.alt, ClientTemplate.y) end return self end ---- Private method that registers all Statics within the mission. +--- Private method that registeres all static objects. -- @param #DATABASE self function DATABASE:_RegisterStatics() - local CoalitionsData = { GroupsRed = coalition.getStaticObjects( coalition.side.RED ), GroupsBlue = coalition.getStaticObjects( coalition.side.BLUE ), GroupsNeutral = coalition.getStaticObjects( coalition.side.NEUTRAL ) } + 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 @@ -961,10 +1009,10 @@ function DATABASE:_RegisterStatics() if DCSStatic:isExist() then local DCSStaticName = DCSStatic:getName() - self:I( string.format( "Register Static: %s", tostring( DCSStaticName ) ) ) + self:I(string.format("Register Static: %s", tostring(DCSStaticName))) self:AddStatic( DCSStaticName ) else - self:E( { "Static does not exist: ", DCSStatic } ) + self:E( { "Static does not exist: ", DCSStatic } ) end end end @@ -977,40 +1025,36 @@ end -- @return #DATABASE self function DATABASE:_RegisterAirbases() - for DCSAirbaseId, DCSAirbase in pairs( world.getAirbases() ) do + for DCSAirbaseId, DCSAirbase in pairs(world.getAirbases()) do -- Get the airbase name. local DCSAirbaseName = DCSAirbase:getName() -- This gave the incorrect value to be inserted into the airdromeID for DCS 2.5.6. Is fixed now. - local airbaseID = DCSAirbase:getID() + local airbaseID=DCSAirbase:getID() -- Add and register airbase. - local airbase = self:AddAirbase( DCSAirbaseName ) + local airbase=self:AddAirbase( DCSAirbaseName ) -- Unique ID. - local airbaseUID = airbase:GetID( true ) + local airbaseUID=airbase:GetID(true) -- Debug output. - 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 + local text=string.format("Register %s: %s (UID=%d), Runways=%d, Parking=%d [", AIRBASE.CategoryName[airbase.category], tostring(DCSAirbaseName), airbaseUID, #airbase.runways, 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] ) + 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 + text=text.."]" + self:I(text) end return self end + --- Events --- Handles the OnBirth event for the alive units set. @@ -1033,10 +1077,10 @@ function DATABASE:_EventOnBirth( Event ) self:AddGroup( Event.IniDCSGroupName ) -- Add airbase if it was spawned later in the mission. - local DCSAirbase = Airbase.getByName( Event.IniDCSUnitName ) + local DCSAirbase = Airbase.getByName(Event.IniDCSUnitName) if DCSAirbase then - self:I( string.format( "Adding airbase %s", tostring( Event.IniDCSUnitName ) ) ) - self:AddAirbase( Event.IniDCSUnitName ) + self:I(string.format("Adding airbase %s", tostring(Event.IniDCSUnitName))) + self:AddAirbase(Event.IniDCSUnitName) end end @@ -1048,27 +1092,27 @@ function DATABASE:_EventOnBirth( Event ) Event.IniGroup = self:FindGroup( Event.IniDCSGroupName ) -- Client - local client = self.CLIENTS[Event.IniDCSUnitName] -- Wrapper.Client#CLIENT + local client=self.CLIENTS[Event.IniDCSUnitName] --Wrapper.Client#CLIENT if client then -- TODO: create event ClientAlive end - -- Get player name. + -- Get player name. local PlayerName = Event.IniUnit:GetPlayerName() if PlayerName then -- Debug info. - self:I( string.format( "Player '%s' joint unit '%s' of group '%s'", tostring( PlayerName ), tostring( Event.IniDCSUnitName ), tostring( Event.IniDCSGroupName ) ) ) + 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 ) + client=self:AddClient(Event.IniDCSUnitName) end -- Add player. - client:AddPlayer( PlayerName ) + client:AddPlayer(PlayerName) -- Add player. if not self.PLAYERS[PlayerName] then @@ -1077,10 +1121,10 @@ function DATABASE:_EventOnBirth( Event ) -- Player settings. local Settings = SETTINGS:Set( PlayerName ) - Settings:SetPlayerMenu( Event.IniUnit ) + Settings:SetPlayerMenu(Event.IniUnit) -- Create an event. - self:CreateEventPlayerEnterAircraft( Event.IniUnit ) + self:CreateEventPlayerEnterAircraft(Event.IniUnit) end @@ -1090,6 +1134,7 @@ function DATABASE:_EventOnBirth( Event ) end + --- Handles the OnDead or OnCrash event for alive units set. -- @param #DATABASE self -- @param Core.Event#EVENTDATA Event @@ -1097,7 +1142,7 @@ function DATABASE:_EventOnDeadOrCrash( Event ) if Event.IniDCSUnit then - local name = Event.IniDCSUnitName + local name=Event.IniDCSUnitName if Event.IniObjectCategory == 3 then @@ -1108,6 +1153,22 @@ function DATABASE:_EventOnDeadOrCrash( Event ) if self.STATICS[Event.IniDCSUnitName] then self:DeleteStatic( Event.IniDCSUnitName ) end + + --- + -- Maybe a UNIT? + --- + + -- Delete unit. + if self.UNITS[Event.IniDCSUnitName] then + self:T("STATIC Event for UNIT "..tostring(Event.IniDCSUnitName)) + local DCSUnit = _DATABASE:FindUnit( Event.IniDCSUnitName ) + self:T({DCSUnit}) + if DCSUnit then + --self:I("Creating DEAD Event for UNIT "..tostring(Event.IniDCSUnitName)) + --DCSUnit:Destroy(true) + return + end + end else @@ -1119,11 +1180,11 @@ function DATABASE:_EventOnDeadOrCrash( Event ) -- 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 + local client=self.CLIENTS[name] --Wrapper.Client#CLIENT if client then client:RemovePlayers() @@ -1133,9 +1194,9 @@ function DATABASE:_EventOnDeadOrCrash( Event ) end -- Add airbase if it was spawned later in the mission. - local airbase = self.AIRBASES[Event.IniDCSUnitName] -- Wrapper.Airbase#AIRBASE + local airbase=self.AIRBASES[Event.IniDCSUnitName] --Wrapper.Airbase#AIRBASE if airbase and (airbase:IsHelipad() or airbase:IsShip()) then - self:DeleteAirbase( Event.IniDCSUnitName ) + self:DeleteAirbase(Event.IniDCSUnitName) end end @@ -1144,6 +1205,7 @@ function DATABASE:_EventOnDeadOrCrash( Event ) self:AccountDestroys( Event ) end + --- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). -- @param #DATABASE self -- @param Core.Event#EVENTDATA Event @@ -1175,12 +1237,13 @@ function DATABASE:_EventOnPlayerEnterUnit( Event ) Settings:SetPlayerMenu( Event.IniUnit ) else - self:E( "ERROR: getPlayerName() returned nil for event PlayerEnterUnit" ) + self:E("ERROR: getPlayerName() returned nil for event PlayerEnterUnit") end end end end + --- Handles the OnPlayerLeaveUnit event to clean the active players table. -- @param #DATABASE self -- @param Core.Event#EVENTDATA Event @@ -1194,22 +1257,22 @@ function DATABASE:_EventOnPlayerLeaveUnit( Event ) -- Try to get the player name. This can be buggy for multicrew aircraft! local PlayerName = Event.IniUnit:GetPlayerName() - if PlayerName then -- and self.PLAYERS[PlayerName] then + if PlayerName then --and self.PLAYERS[PlayerName] then -- Debug info. - self:I( string.format( "Player '%s' left unit %s", tostring( PlayerName ), tostring( Event.IniUnitName ) ) ) + 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 ) + Settings:RemovePlayerMenu(Event.IniUnit) -- Delete player. - self:DeletePlayer( Event.IniUnit, PlayerName ) + self:DeletePlayer(Event.IniUnit, PlayerName) -- Client stuff. - local client = self.CLIENTS[Event.IniDCSUnitName] -- Wrapper.Client#CLIENT + local client=self.CLIENTS[Event.IniDCSUnitName] --Wrapper.Client#CLIENT if client then - client:RemovePlayer( PlayerName ) + client:RemovePlayer(PlayerName) end end @@ -1229,22 +1292,22 @@ function DATABASE:ForEach( IteratorFunction, FinalizeFunction, arg, Set ) local function CoRoutine() local Count = 0 for ObjectID, Object in pairs( Set ) do - self:T2( Object ) - IteratorFunction( Object, unpack( arg ) ) - Count = Count + 1 - -- if Count % 100 == 0 then - -- coroutine.yield( false ) - -- end + self:T2( Object ) + IteratorFunction( Object, unpack( arg ) ) + Count = Count + 1 +-- if Count % 100 == 0 then +-- coroutine.yield( false ) +-- end end return true end - -- local co = coroutine.create( CoRoutine ) +-- local co = coroutine.create( CoRoutine ) local co = CoRoutine local function Schedule() - -- local status, res = coroutine.resume( co ) +-- local status, res = coroutine.resume( co ) local status, res = co() self:T3( { status, res } ) @@ -1260,17 +1323,18 @@ function DATABASE:ForEach( IteratorFunction, FinalizeFunction, arg, Set ) return false end - -- local Scheduler = SCHEDULER:New( self, Schedule, {}, 0.001, 0.001, 0 ) + --local Scheduler = SCHEDULER:New( self, Schedule, {}, 0.001, 0.001, 0 ) Schedule() return self end + --- Iterate the DATABASE and call an iterator function for each **alive** STATIC, providing the STATIC and optional parameters. -- @param #DATABASE self -- @param #function IteratorFunction The function that will be called for each object in the database. The function needs to accept a STATIC parameter. -- @return #DATABASE self -function DATABASE:ForEachStatic( IteratorFunction, FinalizeFunction, ... ) -- R2.1 +function DATABASE:ForEachStatic( IteratorFunction, FinalizeFunction, ... ) --R2.1 self:F2( arg ) self:ForEach( IteratorFunction, FinalizeFunction, arg, self.STATICS ) @@ -1278,6 +1342,7 @@ function DATABASE:ForEachStatic( IteratorFunction, FinalizeFunction, ... ) -- R2 return self end + --- Iterate the DATABASE and call an iterator function for each **alive** UNIT, providing the UNIT and optional parameters. -- @param #DATABASE self -- @param #function IteratorFunction The function that will be called for each object in the database. The function needs to accept a UNIT parameter. @@ -1290,6 +1355,7 @@ function DATABASE:ForEachUnit( IteratorFunction, FinalizeFunction, ... ) return self end + --- Iterate the DATABASE and call an iterator function for each **alive** GROUP, providing the GROUP and optional parameters. -- @param #DATABASE self -- @param #function IteratorFunction The function that will be called for each object in the database. The function needs to accept a GROUP parameter. @@ -1302,6 +1368,7 @@ function DATABASE:ForEachGroup( IteratorFunction, FinalizeFunction, ... ) return self end + --- Iterate the DATABASE and call an iterator function for each **ALIVE** player, providing the player name and optional parameters. -- @param #DATABASE self -- @param #function IteratorFunction The function that will be called for each object in the database. The function needs to accept the player name. @@ -1314,6 +1381,7 @@ function DATABASE:ForEachPlayer( IteratorFunction, FinalizeFunction, ... ) return self end + --- Iterate the DATABASE and call an iterator function for each player who has joined the mission, providing the Unit of the player and optional parameters. -- @param #DATABASE self -- @param #function IteratorFunction The function that will be called for each object in the database. The function needs to accept a UNIT parameter. @@ -1338,6 +1406,7 @@ function DATABASE:ForEachPlayerUnit( IteratorFunction, FinalizeFunction, ... ) return self end + --- Iterate the DATABASE and call an iterator function for each CLIENT, providing the CLIENT to the function and optional parameters. -- @param #DATABASE self -- @param #function IteratorFunction The function that will be called object in the database. The function needs to accept a CLIENT parameter. @@ -1362,6 +1431,7 @@ function DATABASE:ForEachCargo( IteratorFunction, ... ) return self end + --- Handles the OnEventNewCargo event. -- @param #DATABASE self -- @param Core.Event#EVENTDATA EventData @@ -1373,6 +1443,7 @@ function DATABASE:OnEventNewCargo( EventData ) end end + --- Handles the OnEventDeleteCargo. -- @param #DATABASE self -- @param Core.Event#EVENTDATA EventData @@ -1384,6 +1455,7 @@ function DATABASE:OnEventDeleteCargo( EventData ) end end + --- Handles the OnEventNewZone event. -- @param #DATABASE self -- @param Core.Event#EVENTDATA EventData @@ -1395,6 +1467,7 @@ function DATABASE:OnEventNewZone( EventData ) end end + --- Handles the OnEventDeleteZone. -- @param #DATABASE self -- @param Core.Event#EVENTDATA EventData @@ -1406,6 +1479,8 @@ function DATABASE:OnEventDeleteZone( EventData ) end end + + --- Gets the player settings -- @param #DATABASE self -- @param #string PlayerName @@ -1415,6 +1490,7 @@ function DATABASE:GetPlayerSettings( PlayerName ) return self.PLAYERSETTINGS[PlayerName] end + --- Sets the player settings -- @param #DATABASE self -- @param #string PlayerName @@ -1425,42 +1501,86 @@ 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:I( { 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 + if type(groupname)=="string" then else - groupname = groupname:GetName() + 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 + +--- Find an OPSGROUP (FLIGHTGROUP, ARMYGROUP, NAVYGROUP) in the data base for a given unit. +-- @param #DATABASE self +-- @param #string unitname Unit name. Can also be passed as UNIT object. +-- @return Ops.OpsGroup#OPSGROUP OPS group object. +function DATABASE:FindOpsGroupFromUnit(unitname) + + local unit=nil --Wrapper.Unit#UNIT + local groupname + + -- Get group and group name. + if type(unitname)=="string" then + unit=UNIT:FindByName(unitname) + else + unit=unitname + end + + if unit then + groupname=unit:GetGroup():GetName() + end + + if groupname then + return self.FLIGHTGROUPS[groupname] + else + return nil + end +end + --- Add a flight control to the data base. -- @param #DATABASE self -- @param Ops.FlightControl#FLIGHTCONTROL flightcontrol -function DATABASE:AddFlightControl( flightcontrol ) +function DATABASE:AddFlightControl(flightcontrol) self:F2( { flightcontrol } ) - self.FLIGHTCONTROLS[flightcontrol.airbasename] = flightcontrol + self.FLIGHTCONTROLS[flightcontrol.airbasename]=flightcontrol end --- Get a flight control object from the data base. -- @param #DATABASE self -- @param #string airbasename Name of the associated airbase. -- @return Ops.FlightControl#FLIGHTCONTROL The FLIGHTCONTROL object.s -function DATABASE:GetFlightControl( airbasename ) +function DATABASE:GetFlightControl(airbasename) return self.FLIGHTCONTROLS[airbasename] end @@ -1470,32 +1590,32 @@ function DATABASE:_RegisterTemplates() self.Navpoints = {} self.UNITS = {} - -- Build routines.db.units and self.Navpoints - for CoalitionName, coa_data in pairs( env.mission.coalition ) do - self:T( { CoalitionName = CoalitionName } ) + --Build routines.db.units and self.Navpoints + for CoalitionName, coa_data in pairs(env.mission.coalition) do + self:T({CoalitionName=CoalitionName}) - if (CoalitionName == 'red' or CoalitionName == 'blue' or CoalitionName == 'neutrals') and type( coa_data ) == 'table' then - -- self.Units[coa_name] = {} + if (CoalitionName == 'red' or CoalitionName == 'blue' or CoalitionName == 'neutrals') and type(coa_data) == 'table' then + --self.Units[coa_name] = {} - local CoalitionSide = coalition.side[string.upper( CoalitionName )] - if CoalitionName == "red" then - CoalitionSide = coalition.side.RED - elseif CoalitionName == "blue" then - CoalitionSide = coalition.side.BLUE + local CoalitionSide = coalition.side[string.upper(CoalitionName)] + if CoalitionName=="red" then + CoalitionSide=coalition.side.RED + elseif CoalitionName=="blue" then + CoalitionSide=coalition.side.BLUE else - CoalitionSide = coalition.side.NEUTRAL + CoalitionSide=coalition.side.NEUTRAL end -- build nav points DB self.Navpoints[CoalitionName] = {} - if coa_data.nav_points then -- navpoints - for nav_ind, nav_data in pairs( coa_data.nav_points ) do + if coa_data.nav_points then --navpoints + for nav_ind, nav_data in pairs(coa_data.nav_points) do - if type( nav_data ) == 'table' then - self.Navpoints[CoalitionName][nav_ind] = routines.utils.deepCopy( nav_data ) + if type(nav_data) == 'table' then + self.Navpoints[CoalitionName][nav_ind] = routines.utils.deepCopy(nav_data) - self.Navpoints[CoalitionName][nav_ind]['name'] = nav_data.callsignStr -- name is a little bit more self-explanatory. - self.Navpoints[CoalitionName][nav_ind]['point'] = {} -- point is used by SSE, support it. + self.Navpoints[CoalitionName][nav_ind]['name'] = nav_data.callsignStr -- name is a little bit more self-explanatory. + self.Navpoints[CoalitionName][nav_ind]['point'] = {} -- point is used by SSE, support it. self.Navpoints[CoalitionName][nav_ind]['point']['x'] = nav_data.x self.Navpoints[CoalitionName][nav_ind]['point']['y'] = 0 self.Navpoints[CoalitionName][nav_ind]['point']['z'] = nav_data.y @@ -1504,138 +1624,138 @@ function DATABASE:_RegisterTemplates() end ------------------------------------------------- - if coa_data.country then -- there is a country table - for cntry_id, cntry_data in pairs( coa_data.country ) do + if coa_data.country then --there is a country table + for cntry_id, cntry_data in pairs(coa_data.country) do - local CountryName = string.upper( cntry_data.name ) + local CountryName = string.upper(cntry_data.name) local CountryID = cntry_data.id self.COUNTRY_ID[CountryName] = CountryID self.COUNTRY_NAME[CountryID] = CountryName - -- self.Units[coa_name][countryName] = {} - -- self.Units[coa_name][countryName]["countryId"] = cntry_data.id + --self.Units[coa_name][countryName] = {} + --self.Units[coa_name][countryName]["countryId"] = cntry_data.id - if type( cntry_data ) == 'table' then -- just making sure + if type(cntry_data) == 'table' then --just making sure - for obj_type_name, obj_type_data in pairs( cntry_data ) do + for obj_type_name, obj_type_data in pairs(cntry_data) do - if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then -- should be an unncessary check + if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then --should be an unncessary check local CategoryName = obj_type_name - 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 -- there's a group! + 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 --there's a group! - -- self.Units[coa_name][countryName][category] = {} + --self.Units[coa_name][countryName][category] = {} - for group_num, Template in pairs( obj_type_data.group ) do + 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 + 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 - end -- if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then - end -- for obj_type_name, obj_type_data in pairs(cntry_data) do - end -- if type(cntry_data) == 'table' then - end -- for cntry_id, cntry_data in pairs(coa_data.country) do - end -- if coa_data.country then --there is a country table - end -- if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then - end -- for coa_name, coa_data in pairs(mission.coalition) do + 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 + end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then + end --for obj_type_name, obj_type_data in pairs(cntry_data) do + end --if type(cntry_data) == 'table' then + end --for cntry_id, cntry_data in pairs(coa_data.country) do + end --if coa_data.country then --there is a country table + end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then + end --for coa_name, coa_data in pairs(mission.coalition) do return self end ---- Account the Hits of the Players. --- @param #DATABASE self --- @param Core.Event#EVENTDATA Event -function DATABASE:AccountHits( Event ) - self:F( { Event } ) + --- Account the Hits of the Players. + -- @param #DATABASE self + -- @param Core.Event#EVENTDATA Event + function DATABASE:AccountHits( Event ) + self:F( { Event } ) - if Event.IniPlayerName ~= nil then -- It is a player that is hitting something - self:T( "Hitting Something" ) + if Event.IniPlayerName ~= nil then -- It is a player that is hitting something + self:T( "Hitting Something" ) - -- What is he hitting? - if Event.TgtCategory then + -- What is he hitting? + if Event.TgtCategory then - -- A target got hit - self.HITS[Event.TgtUnitName] = self.HITS[Event.TgtUnitName] or {} - local Hit = self.HITS[Event.TgtUnitName] - - Hit.Players = Hit.Players or {} - Hit.Players[Event.IniPlayerName] = true - end - end - - -- It is a weapon initiated by a player, that is hitting something - -- This seems to occur only with scenery and static objects. - if Event.WeaponPlayerName ~= nil then - self:T( "Hitting Scenery" ) - - -- What is he hitting? - if Event.TgtCategory then - - if Event.WeaponCoalition then -- A coalition object was hit, probably a static. -- A target got hit self.HITS[Event.TgtUnitName] = self.HITS[Event.TgtUnitName] or {} local Hit = self.HITS[Event.TgtUnitName] Hit.Players = Hit.Players or {} - Hit.Players[Event.WeaponPlayerName] = true - else -- A scenery object was hit. + Hit.Players[Event.IniPlayerName] = true + end + end + + -- It is a weapon initiated by a player, that is hitting something + -- This seems to occur only with scenery and static objects. + if Event.WeaponPlayerName ~= nil then + self:T( "Hitting Scenery" ) + + -- What is he hitting? + if Event.TgtCategory then + + if Event.WeaponCoalition then -- A coalition object was hit, probably a static. + -- A target got hit + self.HITS[Event.TgtUnitName] = self.HITS[Event.TgtUnitName] or {} + local Hit = self.HITS[Event.TgtUnitName] + + Hit.Players = Hit.Players or {} + Hit.Players[Event.WeaponPlayerName] = true + else -- A scenery object was hit. + end end end end -end ---- Account the destroys. --- @param #DATABASE self --- @param Core.Event#EVENTDATA Event -function DATABASE:AccountDestroys( Event ) - self:F( { Event } ) + --- Account the destroys. + -- @param #DATABASE self + -- @param Core.Event#EVENTDATA Event + function DATABASE:AccountDestroys( Event ) + self:F( { Event } ) - local TargetUnit = nil - local TargetGroup = nil - local TargetUnitName = "" - local TargetGroupName = "" - local TargetPlayerName = "" - local TargetCoalition = nil - local TargetCategory = nil - local TargetType = nil - local TargetUnitCoalition = nil - local TargetUnitCategory = nil - local TargetUnitType = nil + local TargetUnit = nil + local TargetGroup = nil + local TargetUnitName = "" + local TargetGroupName = "" + local TargetPlayerName = "" + local TargetCoalition = nil + local TargetCategory = nil + local TargetType = nil + local TargetUnitCoalition = nil + local TargetUnitCategory = nil + local TargetUnitType = nil - if Event.IniDCSUnit then + if Event.IniDCSUnit then - TargetUnit = Event.IniUnit - TargetUnitName = Event.IniDCSUnitName - TargetGroup = Event.IniDCSGroup - TargetGroupName = Event.IniDCSGroupName - TargetPlayerName = Event.IniPlayerName + TargetUnit = Event.IniUnit + TargetUnitName = Event.IniDCSUnitName + TargetGroup = Event.IniDCSGroup + TargetGroupName = Event.IniDCSGroupName + TargetPlayerName = Event.IniPlayerName - TargetCoalition = Event.IniCoalition - -- TargetCategory = TargetUnit:getCategory() - -- TargetCategory = TargetUnit:getDesc().category -- Workaround - TargetCategory = Event.IniCategory - TargetType = Event.IniTypeName + TargetCoalition = Event.IniCoalition + --TargetCategory = TargetUnit:getCategory() + --TargetCategory = TargetUnit:getDesc().category -- Workaround + TargetCategory = Event.IniCategory + TargetType = Event.IniTypeName - TargetUnitType = TargetType + TargetUnitType = TargetType - self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType } ) + self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType } ) + end + + local Destroyed = false + + -- What is the player destroying? + if self.HITS[Event.IniUnitName] then -- Was there a hit for this unit for this player before registered??? + self.DESTROYS[Event.IniUnitName] = self.DESTROYS[Event.IniUnitName] or {} + self.DESTROYS[Event.IniUnitName] = true + end end - - local Destroyed = false - - -- What is the player destroying? - if self.HITS[Event.IniUnitName] then -- Was there a hit for this unit for this player before registered??? - self.DESTROYS[Event.IniUnitName] = self.DESTROYS[Event.IniUnitName] or {} - self.DESTROYS[Event.IniUnitName] = true - end -end diff --git a/Moose Development/Moose/Core/Fsm.lua b/Moose Development/Moose/Core/Fsm.lua index cca8c509b..89f42e88f 100644 --- a/Moose Development/Moose/Core/Fsm.lua +++ b/Moose Development/Moose/Core/Fsm.lua @@ -405,8 +405,8 @@ do -- FSM Transition.To = To -- Debug message. - self:T2( Transition ) - + --self:T3( Transition ) + self._Transitions[Transition] = Transition self:_eventmap( self.Events, Transition ) end @@ -426,8 +426,8 @@ 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 Sub.Event = Event @@ -524,9 +524,9 @@ do -- FSM Process._Scores[State] = Process._Scores[State] or {} Process._Scores[State].ScoreText = ScoreText Process._Scores[State].Score = Score - - self:T( Process._Scores ) - + + --self:T3( Process._Scores ) + return Process end @@ -560,19 +560,19 @@ do -- FSM -- @param #table Events Events. -- @param #table EventStructure Event structure. function FSM:_eventmap( Events, EventStructure ) - - local Event = EventStructure.Event - local __Event = "__" .. EventStructure.Event - - self[Event] = self[Event] or self:_create_transition( Event ) - self[__Event] = self[__Event] or self:_delayed_transition( Event ) - - -- Debug message. - self:T2( "Added methods: " .. Event .. ", " .. __Event ) - - Events[Event] = self.Events[Event] or { map = {} } - self:_add_to_map( Events[Event].map, EventStructure ) - + + local Event = EventStructure.Event + local __Event = "__" .. EventStructure.Event + + self[Event] = self[Event] or self:_create_transition(Event) + self[__Event] = self[__Event] or self:_delayed_transition(Event) + + -- Debug message. + --self:T3( "Added methods: " .. Event .. ", " .. __Event ) + + Events[Event] = self.Events[Event] or { map = {} } + self:_add_to_map( Events[Event].map, EventStructure ) + end --- Sub maps. @@ -784,8 +784,8 @@ do -- FSM return function( self, DelaySeconds, ... ) -- Debug. - self:T2( "Delayed Event: " .. EventName ) - + self:T3( "Delayed Event: " .. EventName ) + local CallID = 0 if DelaySeconds ~= nil then @@ -802,23 +802,23 @@ do -- FSM self._EventSchedules[EventName] = CallID -- Debug output. - self:T2( string.format( "NEGATIVE Event %s delayed by %.1f sec SCHEDULED with CallID=%s", EventName, DelaySeconds, tostring( CallID ) ) ) + self:T2(string.format("NEGATIVE Event %s delayed by %.3f sec SCHEDULED with CallID=%s", EventName, DelaySeconds, tostring(CallID))) else - self:T2( string.format( "NEGATIVE Event %s delayed by %.1f sec CANCELLED as we already have such an event in the queue.", EventName, DelaySeconds ) ) + self:T2(string.format("NEGATIVE Event %s delayed by %.3f sec CANCELLED as we already have such an event in the queue.", EventName, DelaySeconds)) -- reschedule end else CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1, nil, nil, nil, 4, true ) - - self:T2( string.format( "Event %s delayed by %.1f sec SCHEDULED with CallID=%s", EventName, DelaySeconds, tostring( CallID ) ) ) + + self:T2(string.format("Event %s delayed by %.3f sec SCHEDULED with CallID=%s", EventName, DelaySeconds, tostring(CallID))) end else error( "FSM: An asynchronous event trigger requires a DelaySeconds parameter!!! This can be positive or negative! Sorry, but will not process this." ) end -- Debug. - self:T2( { CallID = CallID } ) + --self:T3( { CallID = CallID } ) end end @@ -841,7 +841,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 {} @@ -887,8 +887,8 @@ do -- FSM Map[From] = Event.To end end - - self:T3( { Map, Event } ) + + --self:T3( { Map, Event } ) end --- Get current state. @@ -908,7 +908,7 @@ do -- FSM --- Check if FSM is in state. -- @param #FSM self -- @param #string State State name. - -- @param #boolean If true, FSM is in this state. + -- @return #boolean If true, FSM is in this state. function FSM:Is( State ) return self.current == State end @@ -916,8 +916,8 @@ do -- FSM --- Check if FSM is in state. -- @param #FSM self -- @param #string State State name. - -- @param #boolean If true, FSM is in this state. - function FSM:is( state ) + -- @return #boolean If true, FSM is in this state. + function FSM:is(state) return self.current == state end @@ -1146,7 +1146,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 @@ -1171,13 +1171,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 @@ -1422,7 +1422,7 @@ do -- FSM_SET -- @param #FSM_SET self -- @return Core.Set#SET_BASE function FSM_SET:Get() - return self.Controllable + return self.Set end function FSM_SET:_call_handler( step, trigger, params, EventName ) diff --git a/Moose Development/Moose/Core/MarkerOps_Base.lua b/Moose Development/Moose/Core/MarkerOps_Base.lua index 8e318a7a7..12095c5d9 100644 --- a/Moose Development/Moose/Core/MarkerOps_Base.lua +++ b/Moose Development/Moose/Core/MarkerOps_Base.lua @@ -11,6 +11,7 @@ -- ### Author: **Applevangelist** -- -- Date: 5 May 2021 +-- Last Update: Sep 2022 -- -- === --- @@ -27,6 +28,7 @@ -- @field #string Tag Tag to identify commands. -- @field #table Keywords Table of keywords to recognize. -- @field #string version Version of #MARKEROPS_BASE. +-- @field #boolean Casesensitive Enforce case when identifying the Tag, i.e. tag ~= Tag -- @extends Core.Fsm#FSM --- *Fiat lux.* -- Latin proverb. @@ -42,16 +44,18 @@ MARKEROPS_BASE = { ClassName = "MARKEROPS", Tag = "mytag", Keywords = {}, - version = "0.0.1", + version = "0.1.0", debug = false, + Casesensitive = true, } --- 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. +-- @param #boolean Casesensitive (Optional) Switch case sensitive identification of Tagname. Defaults to true. -- @return #MARKEROPS_BASE self -function MARKEROPS_BASE:New(Tagname,Keywords) +function MARKEROPS_BASE:New(Tagname,Keywords,Casesensitive) -- Inherit FSM local self=BASE:Inherit(self, FSM:New()) -- #MARKEROPS_BASE @@ -61,6 +65,11 @@ function MARKEROPS_BASE:New(Tagname,Keywords) self.Tag = Tagname or "mytag"-- #string self.Keywords = Keywords or {} -- #table - might want to use lua regex here, too self.debug = false + self.Casesensitive = true + + if Casesensitive and Casesensitive == false then + self.Casesensitive = false + end ----------------------- --- FSM Transitions --- @@ -178,9 +187,16 @@ end -- @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 + if not self.Casesensitive then + local type = string.lower(self.Tag) -- #string + if string.find(string.lower(Eventtext),type) then + matches = true --event text contains tag + end + else + local type = self.Tag -- #string + if string.find(Eventtext,type) then + matches = true --event text contains tag + end end return matches end diff --git a/Moose Development/Moose/Core/Menu.lua b/Moose Development/Moose/Core/Menu.lua index ab62ed465..8a6edf657 100644 --- a/Moose Development/Moose/Core/Menu.lua +++ b/Moose Development/Moose/Core/Menu.lua @@ -239,6 +239,7 @@ do -- MENU_BASE function MENU_BASE:GetMenu( MenuText ) return self.Menus[MenuText] end + --- Sets a menu stamp for later prevention of menu removal. -- @param #MENU_BASE self -- @param MenuStamp @@ -376,6 +377,7 @@ do -- MENU_MISSION end end + --- Refreshes a radio item for a mission -- @param #MENU_MISSION self -- @return #MENU_MISSION @@ -813,6 +815,7 @@ do end end + --- Refreshes a new radio item for a group and submenus -- @param #MENU_GROUP self -- @return #MENU_GROUP @@ -829,6 +832,29 @@ do return self end + --- Refreshes a new radio item for a group and submenus, ordering by (numerical) MenuTag + -- @param #MENU_GROUP self + -- @return #MENU_GROUP + function MENU_GROUP:RefreshAndOrderByTag() + + do + missionCommands.removeItemForGroup( self.GroupID, self.MenuPath ) + missionCommands.addSubMenuForGroup( self.GroupID, self.MenuText, self.MenuParentPath ) + + local MenuTable = {} + for MenuText, Menu in pairs( self.Menus or {} ) do + local tag = Menu.MenuTag or math.random(1,10000) + MenuTable[#MenuTable+1] = {Tag=tag, Enty=Menu} + end + table.sort(MenuTable, function (k1, k2) return k1.tag < k2.tag end ) + for _, Menu in pairs( MenuTable ) do + Menu.Entry:Refresh() + end + end + + return self + end + --- Removes the sub menus recursively of this MENU_GROUP. -- @param #MENU_GROUP self -- @param MenuStamp @@ -1180,4 +1206,3 @@ do return self end end - diff --git a/Moose Development/Moose/Core/Message.lua b/Moose Development/Moose/Core/Message.lua index 662a3926c..888ae3779 100644 --- a/Moose Development/Moose/Core/Message.lua +++ b/Moose Development/Moose/Core/Message.lua @@ -11,8 +11,7 @@ -- * Send messages to a coalition. -- * Send messages to a specific group. -- * Send messages to a specific unit or client. --- --- +-- -- === -- -- @module Core.Message @@ -205,14 +204,14 @@ function MESSAGE:ToClient( Client, Settings ) local Unit = Client:GetClient() if self.MessageDuration ~= 0 then - local ClientGroupID = Client:GetClientGroupID() - self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) - --trigger.action.outTextForGroup( ClientGroupID, self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration , self.ClearScreen) - trigger.action.outTextForUnit( Unit:GetID(), self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration , self.ClearScreen) - end - end - - return self + local ClientGroupID = Client:GetClientGroupID() + self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) + --trigger.action.outTextForGroup( ClientGroupID, self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration , self.ClearScreen) + trigger.action.outTextForUnit( Unit:GetID(), self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration , self.ClearScreen) + end + end + + return self end --- Sends a MESSAGE to a Group. @@ -263,6 +262,30 @@ function MESSAGE:ToUnit( Unit, Settings ) return self end +--- Sends a MESSAGE to a Unit. +-- @param #MESSAGE self +-- @param Wrapper.Unit#UNIT Unit to which the message is displayed. +-- @return #MESSAGE Message object. +function MESSAGE:ToUnit( Unit, Settings ) + self:F( Unit.IdentifiableName ) + + if Unit then + + if self.MessageType then + local Settings = Settings or ( Unit and _DATABASE:GetPlayerSettings( Unit:GetPlayerName() ) ) or _SETTINGS -- Core.Settings#SETTINGS + self.MessageDuration = Settings:GetMessageTime( self.MessageType ) + self.MessageCategory = "" -- self.MessageType .. ": " + end + + if self.MessageDuration ~= 0 then + self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) + trigger.action.outTextForUnit( Unit:GetID(), self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration, self.ClearScreen ) + end + end + + return self +end + --- Sends a MESSAGE to the Blue coalition. -- @param #MESSAGE self -- @return #MESSAGE diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index 3981f0625..6d6ac18df 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -825,7 +825,11 @@ do -- COORDINATE -- @param #COORDINATE TargetCoordinate The target COORDINATE. -- @return DCS#Vec3 DirectionVec3 The direction vector in Vec3 format. function COORDINATE:GetDirectionVec3( TargetCoordinate ) - return { x = TargetCoordinate.x - self.x, y = TargetCoordinate.y - self.y, z = TargetCoordinate.z - self.z } + if TargetCoordinate then + return { x = TargetCoordinate.x - self.x, y = TargetCoordinate.y - self.y, z = TargetCoordinate.z - self.z } + else + return { x=0,y=0,z=0} + end end @@ -874,6 +878,11 @@ do -- COORDINATE -- Get the vector from A to B local vec=UTILS.VecSubstract(ToCoordinate, self) + + if f>1 then + local norm=UTILS.VecNorm(vec) + f=Fraction/norm + end -- Scale the vector. vec.x=f*vec.x @@ -883,7 +892,9 @@ do -- COORDINATE -- Move the vector to start at the end of A. vec=UTILS.VecAdd(self, vec) + -- Create a new coordiante object. local coord=COORDINATE:New(vec.x,vec.y,vec.z) + return coord end @@ -2267,7 +2278,7 @@ do -- COORDINATE 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. + -- **NOTE**: A free form polygon must have **at least three points** in total and currently only **up to 15 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. @@ -2320,8 +2331,28 @@ do -- COORDINATE 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 "") + elseif #vecs==11 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], + vecs[11], + Color, FillColor, LineType, ReadOnly, Text or "") + elseif #vecs==12 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], + vecs[11], vecs[12], + Color, FillColor, LineType, ReadOnly, Text or "") + elseif #vecs==13 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], + vecs[11], vecs[12], vecs[13], + Color, FillColor, LineType, ReadOnly, Text or "") + elseif #vecs==14 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], + vecs[11], vecs[12], vecs[13], vecs[14], + Color, FillColor, LineType, ReadOnly, Text or "") + elseif #vecs==15 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], + vecs[11], vecs[12], vecs[13], vecs[14], vecs[15], + Color, FillColor, LineType, ReadOnly, Text or "") else - self:E("ERROR: Currently a free form polygon can only have 10 points in total!") + self:E("ERROR: Currently a free form polygon can only have 15 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 @@ -2751,7 +2782,7 @@ do -- COORDINATE return "BR, " .. self:GetBRText( AngleRadians, Distance, Settings ) end - --- Return a BRAA string from a COORDINATE to the COORDINATE. + --- Return a BRA string from a COORDINATE to the COORDINATE. -- @param #COORDINATE self -- @param #COORDINATE FromCoordinate The coordinate to measure the distance and the bearing from. -- @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. diff --git a/Moose Development/Moose/Core/ScheduleDispatcher.lua b/Moose Development/Moose/Core/ScheduleDispatcher.lua index 145759bca..6a96d92ff 100644 --- a/Moose Development/Moose/Core/ScheduleDispatcher.lua +++ b/Moose Development/Moose/Core/ScheduleDispatcher.lua @@ -109,9 +109,11 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr self.ObjectSchedulers = self.ObjectSchedulers or setmetatable( {}, { __mode = "v" } ) if Scheduler.MasterObject then + --env.info("FF Object Scheduler") self.ObjectSchedulers[CallID] = Scheduler self:F3( { CallID = CallID, ObjectScheduler = tostring( self.ObjectSchedulers[CallID] ), MasterObject = tostring( Scheduler.MasterObject ) } ) else + --env.info("FF Persistent Scheduler") self.PersistentSchedulers[CallID] = Scheduler self:F3( { CallID = CallID, PersistentScheduler = self.PersistentSchedulers[CallID] } ) end @@ -121,8 +123,8 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr self.Schedule[Scheduler][CallID] = {} -- #SCHEDULEDISPATCHER.ScheduleData 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].StartTime = timer.getTime() + ( Start or 0 ) + 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 @@ -217,7 +219,8 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr if ShowTrace then SchedulerObject:T( Prefix .. Name .. ":" .. Line .. " (" .. Source .. ")" ) end - return ScheduleFunction( SchedulerObject, unpack( ScheduleArguments ) ) + -- The master object is passed as first parameter. A few :Schedule() calls in MOOSE expect this currently. But in principle it should be removed. + return ScheduleFunction( SchedulerObject, unpack( ScheduleArguments ) ) end Status, Result = xpcall( Timer, ErrorHandler ) else @@ -314,7 +317,7 @@ end --- Stop dispatcher. -- @param #SCHEDULEDISPATCHER self -- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object. --- @param #table CallID Call ID. +-- @param #string CallID (Optional) Scheduler Call ID. If nil, all pending schedules are stopped recursively. function SCHEDULEDISPATCHER:Stop( Scheduler, CallID ) self:F2( { Stop = CallID, Scheduler = Scheduler } ) diff --git a/Moose Development/Moose/Core/Scheduler.lua b/Moose Development/Moose/Core/Scheduler.lua index 6ba33425e..e228fe628 100644 --- a/Moose Development/Moose/Core/Scheduler.lua +++ b/Moose Development/Moose/Core/Scheduler.lua @@ -1,7 +1,7 @@ --- **Core** - Prepares and handles the execution of functions over scheduled time (intervals). -- -- === --- +-- -- ## Features: -- -- * Schedule functions over time, @@ -13,9 +13,9 @@ -- === -- -- # Demo Missions --- +-- -- ### [SCHEDULER Demo Missions source code](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/SCH%20-%20Scheduler) --- +-- -- ### [SCHEDULER Demo Missions, only for beta testers](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/SCH%20-%20Scheduler) -- -- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases) @@ -237,7 +237,7 @@ end -- @param #number Stop Time interval in seconds after which the scheduler will be stopped. -- @param #number TraceLevel Trace level [0,3]. Default 3. -- @param Core.Fsm#FSM Fsm Finite state model. --- @return #table The ScheduleID of the planned schedule. +-- @return #string The Schedule ID of the planned schedule. function SCHEDULER:Schedule( MasterObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop, TraceLevel, Fsm ) self:F2( { Start, Repeat, RandomizeFactor, Stop } ) self:T3( { SchedulerArguments } ) @@ -271,7 +271,7 @@ end --- (Re-)Starts the schedules or a specific schedule if a valid ScheduleID is provided. -- @param #SCHEDULER self --- @param #string ScheduleID (Optional) The ScheduleID of the planned (repeating) schedule. +-- @param #string ScheduleID (Optional) The Schedule ID of the planned (repeating) schedule. function SCHEDULER:Start( ScheduleID ) self:F3( { ScheduleID } ) self:T( string.format( "Starting scheduler ID=%s", tostring( ScheduleID ) ) ) diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index be1432348..1f43c2659 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -199,12 +199,10 @@ do -- SET_BASE -- @param NoTriggerEvent (Optional) When `true`, the :Remove() method will not trigger a **Removed** event. function SET_BASE:Remove( ObjectName, NoTriggerEvent ) self:F2( { ObjectName = ObjectName } ) - + local TriggerEvent = true - if NoTriggerEvent then - TriggerEvent = false - end - + if NoTriggerEvent then TriggerEvent = false end + local Object = self.Set[ObjectName] if Object then @@ -228,7 +226,7 @@ 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 ) - + -- Debug info. self:T( { ObjectName = ObjectName, Object = Object } ) @@ -264,28 +262,29 @@ do -- SET_BASE -- @param #SET_BASE self -- @return Core.Base#BASE The added BASE Object. function SET_BASE:SortByName() - - local function sort( a, b ) - return a < b + + local function sort(a, b) + return a=3 then - Color=Color or self:GetColorRGB() - Alpha=Alpha or 1 + local coordinate=COORDINATE:NewFromVec2(self._.Polygon[1]) + + Coalition=Coalition or self:GetDrawCoalition() + + -- Set draw coalition. + self:SetDrawCoalition(Coalition) - FillColor=FillColor or UTILS.DeepCopy(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) - + Color=Color or self:GetColorRGB() + Alpha=Alpha or 1 + + -- Set color. + self:SetColor(Color, Alpha) + + FillColor=FillColor or self:GetFillColorRGB() + if not FillColor then UTILS.DeepCopy(Color) end + FillAlpha=FillAlpha or self:GetFillColorAlpha() + if not FillAlpha then FillAlpha=0.15 end + + -- Set fill color. + self:SetFillColor(FillColor, FillAlpha) + + 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 + end - return self end @@ -2045,7 +2165,7 @@ end -- @return #boolean true if the location is within the zone. function ZONE_POLYGON_BASE:IsVec2InZone( Vec2 ) self:F2( Vec2 ) - if not Vec2 then return false end + if not Vec2 then return false end local Next local Prev local InPolygon = false @@ -2077,7 +2197,7 @@ function ZONE_POLYGON_BASE:IsVec3InZone( Vec3 ) self:F2( Vec3 ) if not Vec3 then return false end - + local InZone = self:IsVec2InZone( { x = Vec3.x, y = Vec3.z } ) return InZone @@ -2297,6 +2417,206 @@ function ZONE_POLYGON:FindByName( ZoneName ) return ZoneFound end +do -- ZONE_ELASTIC + + --- @type ZONE_ELASTIC + -- @field #table points Points in 2D. + -- @field #table setGroups Set of GROUPs. + -- @field #table setOpsGroups Set of OPSGROUPS. + -- @field #table setUnits Set of UNITs. + -- @field #number updateID Scheduler ID for updating. + -- @extends #ZONE_POLYGON_BASE + + --- The ZONE_ELASTIC class defines a dynamic polygon zone, where only the convex hull is used. + -- + -- @field #ZONE_ELASTIC + ZONE_ELASTIC = { + ClassName="ZONE_ELASTIC", + points={}, + setGroups={} + } + + --- Constructor to create a ZONE_ELASTIC instance. + -- @param #ZONE_ELASTIC self + -- @param #string ZoneName Name of the zone. + -- @param DCS#Vec2 Points (Optional) Fixed points. + -- @return #ZONE_ELASTIC self + function ZONE_ELASTIC:New(ZoneName, Points) + + local self=BASE:Inherit(self, ZONE_POLYGON_BASE:New(ZoneName, Points)) --#ZONE_ELASTIC + + -- Zone objects are added to the _DATABASE and SET_ZONE objects. + _EVENTDISPATCHER:CreateEventNewZone( self ) + + if Points then + self.points=Points + end + + return self + end + + --- Add a vertex (point) to the polygon. + -- @param #ZONE_ELASTIC self + -- @param DCS#Vec2 Vec2 Point in 2D (with x and y coordinates). + -- @return #ZONE_ELASTIC self + function ZONE_ELASTIC:AddVertex2D(Vec2) + + -- Add vec2 to points. + table.insert(self.points, Vec2) + + return self + end + + + --- Add a vertex (point) to the polygon. + -- @param #ZONE_ELASTIC self + -- @param DCS#Vec3 Vec3 Point in 3D (with x, y and z coordinates). Only the x and z coordinates are used. + -- @return #ZONE_ELASTIC self + function ZONE_ELASTIC:AddVertex3D(Vec3) + + -- Add vec2 from vec3 to points. + table.insert(self.points, {x=Vec3.x, y=Vec3.z}) + + return self + end + + + --- Add a set of groups. Positions of the group will be considered as polygon vertices when contructing the convex hull. + -- @param #ZONE_ELASTIC self + -- @param Core.Set#SET_GROUP SetGroup Set of groups. + -- @return #ZONE_ELASTIC self + function ZONE_ELASTIC:AddSetGroup(GroupSet) + + -- Add set to table. + table.insert(self.setGroups, GroupSet) + + return self + end + + + --- Update the convex hull of the polygon. + -- This uses the [Graham scan](https://en.wikipedia.org/wiki/Graham_scan). + -- @param #ZONE_ELASTIC self + -- @param #number Delay Delay in seconds before the zone is updated. Default 0. + -- @param #boolean Draw Draw the zone. Default `nil`. + -- @return #ZONE_ELASTIC self + function ZONE_ELASTIC:Update(Delay, Draw) + + -- Debug info. + self:T(string.format("Updating ZONE_ELASTIC %s", tostring(self.ZoneName))) + + -- Copy all points. + local points=UTILS.DeepCopy(self.points or {}) + + if self.setGroups then + for _,_setGroup in pairs(self.setGroups) do + local setGroup=_setGroup --Core.Set#SET_GROUP + for _,_group in pairs(setGroup.Set) do + local group=_group --Wrapper.Group#GROUP + if group and group:IsAlive() then + table.insert(points, group:GetVec2()) + end + end + end + end + + -- Update polygon verticies from points. + self._.Polygon=self:_ConvexHull(points) + + if Draw~=false then + if self.DrawID or Draw==true then + self:UndrawZone() + self:DrawZone() + end + end + + return self + end + + --- Start the updating scheduler. + -- @param #ZONE_ELASTIC self + -- @param #number Tstart Time in seconds before the updating starts. + -- @param #number dT Time interval in seconds between updates. Default 60 sec. + -- @param #number Tstop Time in seconds after which the updating stops. Default `nil`. + -- @param #boolean Draw Draw the zone. Default `nil`. + -- @return #ZONE_ELASTIC self + function ZONE_ELASTIC:StartUpdate(Tstart, dT, Tstop, Draw) + + self.updateID=self:ScheduleRepeat(Tstart, dT, 0, Tstop, ZONE_ELASTIC.Update, self, 0, Draw) + + return self + end + + --- Stop the updating scheduler. + -- @param #ZONE_ELASTIC self + -- @param #number Delay Delay in seconds before the scheduler will be stopped. Default 0. + -- @return #ZONE_ELASTIC self + function ZONE_ELASTIC:StopUpdate(Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, ZONE_ELASTIC.StopUpdate, self) + else + + if self.updateID then + + self:ScheduleStop(self.updateID) + + self.updateID=nil + + end + + end + + return self + end + + + --- Create a convec hull. + -- @param #ZONE_ELASTIC self + -- @param #table pl Points + -- @return #table Points + function ZONE_ELASTIC:_ConvexHull(pl) + + if #pl == 0 then + return {} + end + + table.sort(pl, function(left,right) + return left.x < right.x + end) + + local h = {} + + -- Function: ccw > 0 if three points make a counter-clockwise turn, clockwise if ccw < 0, and collinear if ccw = 0. + local function ccw(a,b,c) + return (b.x - a.x) * (c.y - a.y) > (b.y - a.y) * (c.x - a.x) + end + + -- lower hull + for i,pt in pairs(pl) do + while #h >= 2 and not ccw(h[#h-1], h[#h], pt) do + table.remove(h,#h) + end + table.insert(h,pt) + end + + -- upper hull + local t = #h + 1 + for i=#pl, 1, -1 do + local pt = pl[i] + while #h >= t and not ccw(h[#h-1], h[#h], pt) do + table.remove(h, #h) + end + table.insert(h, pt) + end + + table.remove(h, #h) + + return h + end + +end + do -- ZONE_AIRBASE --- @type ZONE_AIRBASE diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index c122e1c47..04f8e237c 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -653,131 +653,132 @@ do -- DETECTION_BASE -- self:T2( { TargetIsDetected = TargetIsDetected, TargetIsVisible = TargetIsVisible, TargetLastTime = TargetLastTime, TargetKnowType = TargetKnowType, TargetKnowDistance = TargetKnowDistance, TargetLastPos = TargetLastPos, TargetLastVelocity = TargetLastVelocity } ) -- Only process if the target is visible. Detection also returns invisible units. - -- if Detection.visible == true then - - local DetectionAccepted = true - - local DetectedObjectName = DetectedObject:getName() - local DetectedObjectType = DetectedObject:getTypeName() - - local DetectedObjectVec3 = DetectedObject:getPoint() - local DetectedObjectVec2 = { x = DetectedObjectVec3.x, y = DetectedObjectVec3.z } - local DetectionGroupVec3 = Detection:GetVec3() or {x=0,y=0,z=0} - local DetectionGroupVec2 = { x = DetectionGroupVec3.x, y = DetectionGroupVec3.z } - - local Distance = ((DetectedObjectVec3.x - DetectionGroupVec3.x) ^ 2 + - (DetectedObjectVec3.y - DetectionGroupVec3.y) ^ 2 + - (DetectedObjectVec3.z - DetectionGroupVec3.z) ^ 2) ^ 0.5 / 1000 - - local DetectedUnitCategory = DetectedObject:getDesc().category - - -- self:F( { "Detected Target:", DetectionGroupName, DetectedObjectName, DetectedObjectType, Distance, DetectedUnitCategory } ) - - -- Calculate Acceptance - - DetectionAccepted = self._.FilterCategories[DetectedUnitCategory] ~= nil and DetectionAccepted or false - - -- if Distance > 15000 then - -- if DetectedUnitCategory == Unit.Category.GROUND_UNIT or DetectedUnitCategory == Unit.Category.SHIP then - -- if DetectedObject:hasSensors( Unit.SensorType.RADAR, Unit.RadarType.AS ) == false then - -- DetectionAccepted = false - -- end - -- end - -- end - - if self.AcceptRange and Distance * 1000 > self.AcceptRange then - DetectionAccepted = false - end - - if self.AcceptZones then - local AnyZoneDetection = false - for AcceptZoneID, AcceptZone in pairs( self.AcceptZones ) do - local AcceptZone = AcceptZone -- Core.Zone#ZONE_BASE - if AcceptZone:IsVec2InZone( DetectedObjectVec2 ) then - AnyZoneDetection = true + --if Detection.visible == true then + + local DetectionAccepted = true + + local DetectedObjectName = DetectedObject:getName() + local DetectedObjectType = DetectedObject:getTypeName() + + local DetectedObjectVec3 = DetectedObject:getPoint() + local DetectedObjectVec2 = { x = DetectedObjectVec3.x, y = DetectedObjectVec3.z } + local DetectionGroupVec3 = Detection:GetVec3() or {x=0,y=0,z=0} + local DetectionGroupVec2 = { x = DetectionGroupVec3.x, y = DetectionGroupVec3.z } + + local Distance = ( ( DetectedObjectVec3.x - DetectionGroupVec3.x )^2 + + ( DetectedObjectVec3.y - DetectionGroupVec3.y )^2 + + ( DetectedObjectVec3.z - DetectionGroupVec3.z )^2 + ) ^ 0.5 / 1000 + + local DetectedUnitCategory = DetectedObject:getDesc().category + + --self:F( { "Detected Target:", DetectionGroupName, DetectedObjectName, DetectedObjectType, Distance, DetectedUnitCategory } ) + + -- Calculate Acceptance + + DetectionAccepted = self._.FilterCategories[DetectedUnitCategory] ~= nil and DetectionAccepted or false + + -- if Distance > 15000 then + -- if DetectedUnitCategory == Unit.Category.GROUND_UNIT or DetectedUnitCategory == Unit.Category.SHIP then + -- if DetectedObject:hasSensors( Unit.SensorType.RADAR, Unit.RadarType.AS ) == false then + -- DetectionAccepted = false + -- end + -- end + -- end + + if self.AcceptRange and Distance * 1000 > self.AcceptRange then + DetectionAccepted = false + end + + if self.AcceptZones then + local AnyZoneDetection = false + for AcceptZoneID, AcceptZone in pairs( self.AcceptZones ) do + local AcceptZone = AcceptZone -- Core.Zone#ZONE_BASE + if AcceptZone:IsVec2InZone( DetectedObjectVec2 ) then + AnyZoneDetection = true + end + end + if not AnyZoneDetection then + DetectionAccepted = false end end - if not AnyZoneDetection then - DetectionAccepted = false - end - end - - if self.RejectZones then - for RejectZoneID, RejectZone in pairs( self.RejectZones ) do - local RejectZone = RejectZone -- Core.Zone#ZONE_BASE - if RejectZone:IsVec2InZone( DetectedObjectVec2 ) == true then - DetectionAccepted = false - end - end - end - - -- Calculate additional probabilities - - if not self.DetectedObjects[DetectedObjectName] and TargetIsVisible and self.DistanceProbability then - local DistanceFactor = Distance / 4 - local DistanceProbabilityReversed = (1 - self.DistanceProbability) * DistanceFactor - local DistanceProbability = 1 - DistanceProbabilityReversed - DistanceProbability = DistanceProbability * 30 / 300 - local Probability = math.random() -- Selects a number between 0 and 1 - -- self:T( { Probability, DistanceProbability } ) - if Probability > DistanceProbability then - DetectionAccepted = false - end - end - - if not self.DetectedObjects[DetectedObjectName] and TargetIsVisible and self.AlphaAngleProbability then - local NormalVec2 = { x = DetectedObjectVec2.x - DetectionGroupVec2.x, y = DetectedObjectVec2.y - DetectionGroupVec2.y } - local AlphaAngle = math.atan2( NormalVec2.y, NormalVec2.x ) - local Sinus = math.sin( AlphaAngle ) - local AlphaAngleProbabilityReversed = (1 - self.AlphaAngleProbability) * (1 - Sinus) - local AlphaAngleProbability = 1 - AlphaAngleProbabilityReversed - - AlphaAngleProbability = AlphaAngleProbability * 30 / 300 - - local Probability = math.random() -- Selects a number between 0 and 1 - -- self:T( { Probability, AlphaAngleProbability } ) - if Probability > AlphaAngleProbability then - DetectionAccepted = false - end - - end - - if not self.DetectedObjects[DetectedObjectName] and TargetIsVisible and self.ZoneProbability then - - for ZoneDataID, ZoneData in pairs( self.ZoneProbability ) do - self:F( { ZoneData } ) - local ZoneObject = ZoneData[1] -- Core.Zone#ZONE_BASE - local ZoneProbability = ZoneData[2] -- #number - ZoneProbability = ZoneProbability * 30 / 300 - - if ZoneObject:IsVec2InZone( DetectedObjectVec2 ) == true then - local Probability = math.random() -- Selects a number between 0 and 1 - -- self:T( { Probability, ZoneProbability } ) - if Probability > ZoneProbability then + + if self.RejectZones then + for RejectZoneID, RejectZone in pairs( self.RejectZones ) do + local RejectZone = RejectZone -- Core.Zone#ZONE_BASE + if RejectZone:IsVec2InZone( DetectedObjectVec2 ) == true then DetectionAccepted = false - break end end end - end - - if DetectionAccepted then - - HasDetectedObjects = true - - self.DetectedObjects[DetectedObjectName] = self.DetectedObjects[DetectedObjectName] or {} - self.DetectedObjects[DetectedObjectName].Name = DetectedObjectName - - if TargetIsDetected and TargetIsDetected == true then - self.DetectedObjects[DetectedObjectName].IsDetected = TargetIsDetected + + -- Calculate additional probabilities + + if not self.DetectedObjects[DetectedObjectName] and TargetIsVisible and self.DistanceProbability then + local DistanceFactor = Distance / 4 + local DistanceProbabilityReversed = ( 1 - self.DistanceProbability ) * DistanceFactor + local DistanceProbability = 1 - DistanceProbabilityReversed + DistanceProbability = DistanceProbability * 30 / 300 + local Probability = math.random() -- Selects a number between 0 and 1 + --self:T( { Probability, DistanceProbability } ) + if Probability > DistanceProbability then + DetectionAccepted = false + end end - - if TargetIsDetected and TargetIsVisible and TargetIsVisible == true then - self.DetectedObjects[DetectedObjectName].IsVisible = TargetIsDetected and TargetIsVisible + + if not self.DetectedObjects[DetectedObjectName] and TargetIsVisible and self.AlphaAngleProbability then + local NormalVec2 = { x = DetectedObjectVec2.x - DetectionGroupVec2.x, y = DetectedObjectVec2.y - DetectionGroupVec2.y } + local AlphaAngle = math.atan2( NormalVec2.y, NormalVec2.x ) + local Sinus = math.sin( AlphaAngle ) + local AlphaAngleProbabilityReversed = ( 1 - self.AlphaAngleProbability ) * ( 1 - Sinus ) + local AlphaAngleProbability = 1 - AlphaAngleProbabilityReversed + + AlphaAngleProbability = AlphaAngleProbability * 30 / 300 + + local Probability = math.random() -- Selects a number between 0 and 1 + --self:T( { Probability, AlphaAngleProbability } ) + if Probability > AlphaAngleProbability then + DetectionAccepted = false + end + end - - if TargetIsDetected and not self.DetectedObjects[DetectedObjectName].KnowType then - self.DetectedObjects[DetectedObjectName].KnowType = TargetIsDetected and TargetKnowType + + if not self.DetectedObjects[DetectedObjectName] and TargetIsVisible and self.ZoneProbability then + + for ZoneDataID, ZoneData in pairs( self.ZoneProbability ) do + self:F({ZoneData}) + local ZoneObject = ZoneData[1] -- Core.Zone#ZONE_BASE + local ZoneProbability = ZoneData[2] -- #number + ZoneProbability = ZoneProbability * 30 / 300 + + if ZoneObject:IsVec2InZone( DetectedObjectVec2 ) == true then + local Probability = math.random() -- Selects a number between 0 and 1 + --self:T( { Probability, ZoneProbability } ) + if Probability > ZoneProbability then + DetectionAccepted = false + break + end + end + end + end + + if DetectionAccepted then + + HasDetectedObjects = true + + self.DetectedObjects[DetectedObjectName] = self.DetectedObjects[DetectedObjectName] or {} + self.DetectedObjects[DetectedObjectName].Name = DetectedObjectName + + if TargetIsDetected and TargetIsDetected == true then + self.DetectedObjects[DetectedObjectName].IsDetected = TargetIsDetected + end + + if TargetIsDetected and TargetIsVisible and TargetIsVisible == true then + self.DetectedObjects[DetectedObjectName].IsVisible = TargetIsDetected and TargetIsVisible + end + + if TargetIsDetected and not self.DetectedObjects[DetectedObjectName].KnowType then + self.DetectedObjects[DetectedObjectName].KnowType = TargetIsDetected and TargetKnowType end self.DetectedObjects[DetectedObjectName].KnowDistance = TargetKnowDistance -- Detection.distance -- TargetKnowDistance self.DetectedObjects[DetectedObjectName].LastTime = (TargetIsDetected and TargetIsVisible == false) and TargetLastTime @@ -2439,16 +2440,16 @@ do -- DETECTION_AREAS -- A set with multiple detected zones will be created as there are groups of units detected. -- -- ## 4.1) Retrieve the Detected Unit Sets and Detected Zones - -- - -- The methods to manage the DetectedItems[].Set(s) are implemented in @{Functional.Detection#DETECTION_BASE} and + -- + -- The methods to manage the DetectedItems[].Set(s) are implemented in @{Functional.Detection#DECTECTION_BASE} and -- the methods to manage the DetectedItems[].Zone(s) are implemented in @{Functional.Detection#DETECTION_AREAS}. - -- + -- -- Retrieve the DetectedItems[].Set with the method @{Functional.Detection#DETECTION_BASE.GetDetectedSet}(). A @{Core.Set#SET_UNIT} object will be returned. - -- + -- -- Retrieve the formed @{Zone@ZONE_UNIT}s as a result of the grouping the detected units within the DetectionZoneRange, use the method @{Functional.Detection#DETECTION_AREAS.GetDetectionZones}(). - -- To understand the amount of zones created, use the method @{Functional.Detection#DETECTION_AREAS.GetDetectionZoneCount}(). + -- To understand the amount of zones created, use the method @{Functional.Detection#DETECTION_AREAS.GetDetectionZoneCount}(). -- If you want to obtain a specific zone from the DetectedZones, use the method @{Functional.Detection#DETECTION_AREAS.GetDetectionZoneByID}() with a given index. - -- + -- -- ## 4.4) Flare or Smoke detected units -- -- Use the methods @{Functional.Detection#DETECTION_AREAS.FlareDetectedUnits}() or @{Functional.Detection#DETECTION_AREAS.SmokeDetectedUnits}() to flare or smoke the detected units when a new detection has taken place. @@ -2489,7 +2490,7 @@ do -- DETECTION_AREAS return self end - + --- Retrieve set of detected zones. -- @param #DETECTION_AREAS self -- @return Core.Set#SET_ZONE The @{Set} of ZONE_UNIT objects detected. diff --git a/Moose Development/Moose/Functional/Fox.lua b/Moose Development/Moose/Functional/Fox.lua index 6624c352e..55b6e8548 100644 --- a/Moose Development/Moose/Functional/Fox.lua +++ b/Moose Development/Moose/Functional/Fox.lua @@ -792,7 +792,7 @@ function FOX:onafterMissileLaunch(From, Event, To, missile) local text=string.format("Missile launch detected! Distance %.1f NM, bearing %03d°.", UTILS.MetersToNM(distance), bearing) -- Say notching headings. - BASE:ScheduleOnce(5, FOX._SayNotchingHeadings, self, player, missile.weapon) + self:ScheduleOnce(5, FOX._SayNotchingHeadings, self, player, missile.weapon) --TODO: ALERT or INFO depending on whether this is a direct target. --TODO: lauchalertall option. @@ -1114,6 +1114,13 @@ end -- Event Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- FOX event handler for event birth. +-- @param #FOX self +-- @param Core.Event#EVENTDATA EventData +function FOX:OnEventPlayerEnterAircraft(EventData) + +end + --- FOX event handler for event birth. -- @param #FOX self -- @param Core.Event#EVENTDATA EventData @@ -1155,7 +1162,7 @@ function FOX:OnEventBirth(EventData) -- Add F10 radio menu for player. if not self.menudisabled then - SCHEDULER:New(nil, self._AddF10Commands, {self,_unitName}, 0.1) + self:ScheduleOnce(0.1, FOX._AddF10Commands, self, _unitname) end -- Player data. diff --git a/Moose Development/Moose/Functional/Mantis.lua b/Moose Development/Moose/Functional/Mantis.lua index f9cea279f..2dde61915 100644 --- a/Moose Development/Moose/Functional/Mantis.lua +++ b/Moose Development/Moose/Functional/Mantis.lua @@ -3,9 +3,9 @@ -- === -- -- **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 +-- Controls a network of SAM sites. Uses detection to switch on the AA site closest to the enemy. +-- Automatic mode (default since 0.8) can set-up your SAM site network automatically for you. +-- Leverage evasiveness from SEAD, leverage attack range setting. -- -- === -- @@ -20,7 +20,7 @@ -- @module Functional.Mantis -- @image Functional.Mantis.jpg -- --- Date: Nov 2021 +-- Date: Dec 2021 ------------------------------------------------------------------------- --- **MANTIS** class, extends Core.Base#BASE @@ -65,12 +65,63 @@ -- -- #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. +-- * Controls a network of SAM sites. Uses detection to switch on the SAM site closest to the enemy. +-- * **Automatic mode** (default since 0.8) can set-up your SAM site network automatically for you +-- * **Classic mode** behaves like before +-- * Leverage evasiveness from SEAD, leverage attack range setting +-- * Automatic setup of SHORAD based on groups of the class "short-range" -- --- 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. +-- # 0. Base considerations and naming conventions +-- +-- **Before** you start to set up your SAM sites in the mission editor, please think of naming conventions. This is especially critical to make +-- eveything work as intended, also if you have both a blue and a red installation! +-- +-- You need three **non-overlapping** "name spaces" for everything to work properly: +-- +-- * SAM sites, e.g. each **group name** begins with "Red SAM" +-- * EWR network and AWACS, e.g. each **group name** begins with "Red EWR" and *not* e.g. "Red SAM EWR" (overlap with "Red SAM"), "Red EWR Awacs" will be found by "Red EWR" +-- * SHORAD, e.g. each **group name** begins with "Red SHORAD" and *not" e.g. just "SHORAD" because you might also have "Blue SHORAD" +-- +-- It's important to get this right because of the nature of the filter-system in @{Core.Set#SET_GROUP}. Filters are "greedy", that is they +-- will match *any* string that contains the search string - hence we need to avoid that SAMs, EWR and SHORAD step on each other\'s toes. +-- +-- Second, for auto-mode to work, the SAMs need the **SAM Type Name** in their group name, as MANTIS will determine their capabilities from this. +-- This is case-sensitive, so "sa-11" is not equal to "SA-11" is not equal to "Sa-11"! +-- +-- Known SAM types at the time of writing are: +-- +-- * Avenger +-- * Chaparrel +-- * Hawk +-- * Linebacker +-- * NASAMS +-- * Patriot +-- * Rapier +-- * Roland +-- * Silkworm (though strictly speaking this is a surface to ship missile) +-- * SA-2, SA-3, SA-5, SA-6, SA-7, SA-8, SA-9, SA-10, SA-11, SA-13, SA-15, SA-19 +-- * and from HDS (see note below): SA-2, SA-3, SA-10B, SA-10C, SA-12, SA-17, SA-20A, SA-20B, SA-23, HQ-2 +-- +-- Following the example started above, an SA-6 site group name should start with "Red SAM SA-6" then, or a blue Patriot installation with e.g. "Blue SAM Patriot". +-- **NOTE** If you are using the High-Digit-Sam Mod, please note that the **group name** for the following SAM types also needs to contain the keyword "HDS": +-- +-- * SA-2 (with V759 missile, e.g. "Red SAM SA-2 HDS") +-- * SA-2 (with HQ-2 launcher, use HQ-2 in the group name, e.g. "Red SAM HQ-2" ) +-- * SA-3 (with V601P missile, e.g. "Red SAM SA-3 HDS") +-- * SA-10B (overlap with other SA-10 types, e.g. "Red SAM SA-10B HDS") +-- * SA-10C (overlap with other SA-10 types, e.g. "Red SAM SA-10C HDS") +-- * SA-12 (launcher dependent range, e.g. "Red SAM SA-12 HDS") +-- * SA-23 (launcher dependent range, e.g. "Red SAM SA-23 HDS") +-- +-- The other HDS types work like the rest of the known SAM systems. +-- +-- # 0.1 Set-up in the mission editor +-- +-- Set up your SAM sites in the mission editor. Name the groups using a systematic approach like above. +-- Set up your EWR system in the mission editor. Name the groups using a systematic approach like above. Can be e.g. AWACS or a combination of AWACS and Search Radars like e.g. EWR 1L13 etc. +-- Search Radars usually have "SR" or "STR" in their names. Use the encyclopedia in the mission editor to inform yourself. +-- Set up your SHORAD systems. They need to be **close** to (i.e. around) the SAM sites to be effective. Use **one** group per SAM location. SA-15 TOR systems offer a good missile defense. +-- -- [optional] Set up your HQ. Can be any group, e.g. a command vehicle. -- -- # 1. Basic tactical considerations when setting up your SAM sites @@ -78,13 +129,13 @@ -- ## 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 +-- **Location** is of highest importance 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 +-- 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-5/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. -- @@ -94,9 +145,9 @@ -- -- * 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. +-- * not taking into account that a SAM site will take (e.g for a SA-6) 30-40 seconds between switching on, 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 +-- 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 mission 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 @@ -104,32 +155,62 @@ -- myredmantis = MANTIS:New("myredmantis","Red SAM","Red EWR",nil,"red",false) -- myredmantis:Start() -- --- [optional] Use +-- Use -- --- * MANTIS:SetEWRGrouping(radius) --- * MANTIS:SetEWRRange(radius) --- * MANTIS:SetSAMRadius(radius) --- * MANTIS:SetDetectInterval(interval) --- * MANTIS:SetAutoRelocate(hq, ewr) +-- * MANTIS:SetEWRGrouping(radius) [classic mode] +-- * MANTIS:SetSAMRadius(radius) [classic mode] +-- * MANTIS:SetDetectInterval(interval) [classic & auto modes] +-- * MANTIS:SetAutoRelocate(hq, ewr) [classic & auto modes] -- -- 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: +-- If you want to use a separate AWACS unit 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 +-- +-- ## 2.1 Auto mode features +-- +-- ### 2.1.1 You can now add Accept-, Reject- and Conflict-Zones to your setup, e.g. to consider borders or de-militarized zones: +-- +-- -- Parameters are tables of Core.Zone#ZONE objects! +-- -- This is effectively a 3-stage filter allowing for zone overlap. A coordinate is accepted first when +-- -- it is inside any AcceptZone. Then RejectZones are checked, which enforces both borders, but also overlaps of +-- -- Accept- and RejectZones. Last, if it is inside a conflict zone, it is accepted. +-- `mybluemantis:AddZones(AcceptZones,RejectZones,ConflictZones)` +-- +-- +-- ### 2.1.2 Change the number of long-, mid- and short-range systems going live on a detected target: +-- +-- -- parameters are numbers. Defaults are 1,2,2,6 respectively +-- `mybluemantis:SetMaxActiveSAMs(Short,Mid,Long,Classic)` +-- +-- ### 2.1.3 SHORAD will automatically be added from SAM sites of type "short-range" +-- +-- ### 2.1.4 Advanced features +-- +-- -- switch off auto mode **before** you start MANTIS. +-- `mybluemantis.automode = false` +-- +-- -- switch off auto shorad **before** you start MANTIS. +-- `mybluemantis.autoshorad = false` +-- +-- -- scale of the activation range, i.e. don't activate at the fringes of max range, defaults below. +-- -- also see engagerange below. +-- ` self.radiusscale[MANTIS.SamType.LONG] = 1.1` +-- ` self.radiusscale[MANTIS.SamType.MEDIUM] = 1.2` +-- ` self.radiusscale[MANTIS.SamType.SHORT] = 1.3` +-- +-- # 3. Default settings [both modes unless stated otherwise] -- -- 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)` +-- * [classic mode] 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)` +-- * engagerange = 95 (percent) - SAMs will only fire if flights are inside of a 95% 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)` @@ -142,7 +223,7 @@ -- -- Use this option if you want to make use of or allow advanced SEAD tactics. -- --- # 5. Integrate SHORAD +-- # 5. Integrate SHORAD [classic mode] -- -- 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: @@ -153,8 +234,10 @@ -- 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 +-- +-- If you systematically name your SHORAD groups starting with "Blue SHORAD" you'll need exactly **one** SHORAD instance to manage all SHORAD groups. +-- +-- (Optionally) you can remove the link later on with -- -- mymantis:RemoveShorad() -- @@ -190,9 +273,12 @@ MANTIS = { EWR_Templates_Prefix = "", EWR_Group = nil, Adv_EWR_Group = nil, - HQ_Template_CC = "", - HQ_CC = nil, + HQ_Template_CC = "", + HQ_CC = nil, SAM_Table = {}, + SAM_Table_Long = {}, + SAM_Table_Medium = {}, + SAM_Table_Short = {}, lid = "", Detection = nil, AWACS_Detection = nil, @@ -201,7 +287,7 @@ MANTIS = { grouping = 5000, acceptrange = 80000, detectinterval = 30, - engagerange = 75, + engagerange = 95, autorelocate = false, advanced = false, adv_ratio = 100, @@ -213,7 +299,7 @@ MANTIS = { Shorad = nil, ShoradLink = false, ShoradTime = 600, - ShoradActDistance = 15000, + ShoradActDistance = 25000, UseEmOnOff = false, TimeStamp = 0, state2flag = false, @@ -221,7 +307,10 @@ MANTIS = { DLink = false, DLTimeStamp = 0, Padding = 10, - SuppressedGroups = {}, + SuppressedGroups = {}, + automode = true, + autoshorad = true, + ShoradGroupSet = nil, } --- Advanced state enumerator @@ -232,6 +321,72 @@ MANTIS.AdvancedState = { RED = 2, } +--- SAM Type +-- @type MANTIS.SamType +MANTIS.SamType = { + SHORT = "Short", + MEDIUM = "Medium", + LONG = "Long", +} + +--- SAM data +-- @type MANTIS.SamData +-- @field #number Range Max firing range in km +-- @field #number Blindspot no-firing range (green circle) +-- @field #number Height Max firing height in km +-- @field #string Type #MANTIS.SamType of SAM, i.e. SHORT, MEDIUM or LONG (range) +-- @field #string Radar Radar typename on unit level (used as key) +MANTIS.SamData = { + ["Hawk"] = { Range=44, Blindspot=0, Height=9, Type="Medium", Radar="Hawk" }, -- measures in km + ["NASAMS"] = { Range=14, Blindspot=0, Height=3, Type="Short", Radar="NSAMS" }, + ["Patriot"] = { Range=99, Blindspot=0, Height=9, Type="Long", Radar="Patriot" }, + ["Rapier"] = { Range=6, Blindspot=0, Height=3, Type="Short", Radar="rapier" }, + ["SA-2"] = { Range=40, Blindspot=7, Height=25, Type="Medium", Radar="S_75M_Volhov" }, + ["SA-3"] = { Range=18, Blindspot=6, Height=18, Type="Short", Radar="5p73 s-125 ln" }, + ["SA-5"] = { Range=250, Blindspot=7, Height=40, Type="Long", Radar="5N62V" }, + ["SA-6"] = { Range=25, Blindspot=0, Height=8, Type="Medium", Radar="1S91" }, + ["SA-10"] = { Range=119, Blindspot=0, Height=18, Type="Long" , Radar="S-300PS 4"}, + ["SA-11"] = { Range=35, Blindspot=0, Height=20, Type="Medium", Radar="SA-11" }, + ["Roland"] = { Range=8, Blindspot=0, Height=3, Type="Short", Radar="Roland" }, + ["HQ-7"] = { Range=12, Blindspot=0, Height=3, Type="Short", Radar="HQ-7" }, + ["SA-9"] = { Range=4, Blindspot=0, Height=3, Type="Short", Radar="Strela" }, + ["SA-8"] = { Range=10, Blindspot=0, Height=5, Type="Short", Radar="Osa 9A33" }, + ["SA-19"] = { Range=8, Blindspot=0, Height=3, Type="Short", Radar="Tunguska" }, + ["SA-15"] = { Range=11, Blindspot=0, Height=6, Type="Short", Radar="Tor 9A331" }, + ["SA-13"] = { Range=5, Blindspot=0, Height=3, Type="Short", Radar="Strela" }, + ["Avenger"] = { Range=4, Blindspot=0, Height=3, Type="Short", Radar="Avenger" }, + ["Chaparrel"] = { Range=8, Blindspot=0, Height=3, Type="Short", Radar="Chaparral" }, + ["Linebacker"] = { Range=4, Blindspot=0, Height=3, Type="Short", Radar="Linebacker" }, + ["Silkworm"] = { Range=90, Blindspot=1, Height=0.2, Type="Long", Radar="Silkworm" }, + -- units from HDS Mod, multi launcher options is tricky + ["SA-10B"] = { Range=75, Blindspot=0, Height=18, Type="Medium" , Radar="SA-10B"}, + ["SA-17"] = { Range=50, Blindspot=3, Height=30, Type="Medium", Radar="SA-17" }, + ["SA-20A"] = { Range=150, Blindspot=5, Height=27, Type="Long" , Radar="S-300PMU1"}, + ["SA-20B"] = { Range=200, Blindspot=4, Height=27, Type="Long" , Radar="S-300PMU2"}, + ["HQ-2"] = { Range=50, Blindspot=6, Height=35, Type="Medium", Radar="HQ_2_Guideline_LN" }, +} + +--- SAM data HDS +-- @type MANTIS.SamDataHDS +-- @field #number Range Max firing range in km +-- @field #number Blindspot no-firing range (green circle) +-- @field #number Height Max firing height in km +-- @field #string Type #MANTIS.SamType of SAM, i.e. SHORT, MEDIUM or LONG (range) +-- @field #string Radar Radar typename on unit level (used as key) +MANTIS.SamDataHDS = { + -- units from HDS Mod, multi launcher options is tricky + -- group name MUST contain HDS to ID launcher type correctly! + ["SA-2 HDS"] = { Range=56, Blindspot=7, Height=30, Type="Medium", Radar="V759" }, + ["SA-3 HDS"] = { Range=20, Blindspot=6, Height=30, Type="Short", Radar="V-601P" }, + ["SA-10C HDS 2"] = { Range=90, Blindspot=5, Height=25, Type="Long" , Radar="5P85DE ln"}, -- V55RUD + ["SA-10C HDS 1"] = { Range=90, Blindspot=5, Height=25, Type="Long" , Radar="5P85CE ln"}, -- V55RUD + ["SA-12 HDS 2"] = { Range=100, Blindspot=10, Height=25, Type="Long" , Radar="S-300V 9A82 l"}, + ["SA-12 HDS 1"] = { Range=75, Blindspot=1, Height=25, Type="Long" , Radar="S-300V 9A83 l"}, + ["SA-23 HDS 2"] = { Range=200, Blindspot=5, Height=37, Type="Long", Radar="S-300VM 9A82ME" }, + ["SA-23 HDS 1"] = { Range=100, Blindspot=1, Height=50, Type="Long", Radar="S-300VM 9A83ME" }, + ["HQ-2 HDS"] = { Range=50, Blindspot=6, Height=35, Type="Medium", Radar="HQ_2_Guideline_LN" }, +} + ----------------------------------------------------------------------- -- MANTIS System ----------------------------------------------------------------------- @@ -256,11 +411,8 @@ do -- -- [optional] Use -- - -- * MANTIS:SetEWRGrouping(radius) - -- * MANTIS:SetEWRRange(radius) - -- * MANTIS:SetSAMRadius(radius) - -- * MANTIS:SetDetectInterval(interval) - -- * MANTIS:SetAutoRelocate(hq, ewr) + -- myredmantis:SetDetectInterval(interval) + -- myredmantis:SetAutoRelocate(hq, ewr) -- -- before starting #MANTIS to fine-tune your setup. -- @@ -276,19 +428,23 @@ do -- 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 + -- DONE: Allow tables of prefixes for the setup + -- DONE: Auto-Mode with range setups for various known SAM types. - 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.SAM_Table_Long = {} + self.SAM_Table_Medium = {} + self.SAM_Table_Short = {} self.dynamic = dynamic or false self.checkradius = 25000 self.grouping = 5000 self.acceptrange = 80000 self.detectinterval = 30 - self.engagerange = 75 + self.engagerange = 95 self.autorelocate = false self.autorelocateunits = { HQ = false, EWR = false} self.advanced = false @@ -301,7 +457,7 @@ do self.Shorad = nil self.ShoradLink = false self.ShoradTime = 600 - self.ShoradActDistance = 15000 + self.ShoradActDistance = 25000 self.TimeStamp = timer.getAbsTime() self.relointerval = math.random(1800,3600) -- random between 30 and 60 mins self.state2flag = false @@ -309,13 +465,27 @@ do self.DLink = false self.Padding = Padding or 10 self.SuppressedGroups = {} - - if EmOnOff then - if EmOnOff == false then - self.UseEmOnOff = false - else - self.UseEmOnOff = true - end + -- 0.8 additions + self.automode = true + self.radiusscale = {} + self.radiusscale[MANTIS.SamType.LONG] = 1.1 + self.radiusscale[MANTIS.SamType.MEDIUM] = 1.2 + self.radiusscale[MANTIS.SamType.SHORT] = 1.3 + --self.SAMCheckRanges = {} + self.usezones = false + self.AcceptZones = {} + self.RejectZones = {} + self.ConflictZones = {} + self.maxlongrange = 1 + self.maxmidrange = 2 + self.maxshortrange = 2 + self.maxclassic = 6 + self.autoshorad = true + self.ShoradGroupSet = SET_GROUP:New() -- Core.Set#SET_GROUP + + self.UseEmOnOff = true + if EmOnOff == false then + self.UseEmOnOff = false end if type(awacs) == "string" then @@ -337,26 +507,50 @@ do --BASE:TraceClass("SEAD") BASE:TraceLevel(1) end - + + self.ewr_templates = {} + if type(samprefix) ~= "table" then + self.SAM_Templates_Prefix = {samprefix} + end + + if type(ewrprefix) ~= "table" then + self.EWR_Templates_Prefix = {ewrprefix} + end + + for _,_group in pairs (self.SAM_Templates_Prefix) do + table.insert(self.ewr_templates,_group) + end + + for _,_group in pairs (self.EWR_Templates_Prefix) do + table.insert(self.ewr_templates,_group) + end + + if self.advAwacs then + table.insert(self.ewr_templates,awacs) + end + + self:T({self.ewr_templates}) + 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() + self.EWR_Group = SET_GROUP:New():FilterPrefixes(self.ewr_templates):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() + self.EWR_Group = SET_GROUP:New():FilterPrefixes(self.ewr_templates):FilterCoalitions(self.Coalition):FilterOnce() end -- set up CC if self.HQ_Template_CC then self.HQ_CC = GROUP:FindByName(self.HQ_Template_CC) end - + + -- TODO Version -- @field #string version - self.version="0.7.1" + self.version="0.8.8" self:I(string.format("***** Starting MANTIS Version %s *****", self.version)) --- FSM Functions --- @@ -465,6 +659,7 @@ do -- @param #string Name Name of the suppressed group -- @param #number SuppressionStartTime Model start time of the suppression from `timer.getTime()` -- @param #number SuppressionEndTime Model end time of the suppression from `timer.getTime()` + -- @param Wrapper.Group#GROUP Attacker The attacking GROUP object --- On After "SeadSuppressionStart" event. Mantis has switched off a site to defend a SEAD attack. -- @function [parent=#MANTIS] OnAfterSeadSuppressionStart @@ -473,7 +668,8 @@ do -- @param #string Event The Event -- @param #string To The To State -- @param Wrapper.Group#GROUP Group The suppressed GROUP object - -- @param #string Name Name of the suppressed groupe + -- @param #string Name Name of the suppressed group + -- @param Wrapper.Group#GROUP Attacker The attacking GROUP object --- On After "SeadSuppressionEnd" event. Mantis has switched on a site after a SEAD attack. -- @function [parent=#MANTIS] OnAfterSeadSuppressionEnd @@ -517,20 +713,42 @@ do self.grouping = radius return self end - - --- Function to set the detection radius of the EWR in meters + + --- Function to set accept and reject zones. + -- @param #MANTIS self + -- @param #table AcceptZones Table of @{Core.Zone#ZONE} objects + -- @param #table RejectZones Table of @{Core.Zone#ZONE} objects + -- @param #table ConflictZones Table of @{Core.Zone#ZONE} objects + -- @return #MANTIS self + -- @usage + -- Parameters are **tables of Core.Zone#ZONE** objects! + -- This is effectively a 3-stage filter allowing for zone overlap. A coordinate is accepted first when + -- it is inside any AcceptZone. Then RejectZones are checked, which enforces both borders, but also overlaps of + -- Accept- and RejectZones. Last, if it is inside a conflict zone, it is accepted. + function MANTIS:AddZones(AcceptZones,RejectZones, ConflictZones) + self:T(self.lid .. "AddZones") + self.AcceptZones = AcceptZones or {} + self.RejectZones = RejectZones or {} + self.ConflictZones = ConflictZones or {} + if #AcceptZones > 0 or #RejectZones > 0 or #ConflictZones > 0 then + self.usezones = true + end + return self + end + + --- Function to set the detection radius of the EWR in meters. (Deprecated, SAM range is used) -- @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 + --local radius = radius or 80000 + -- self.acceptrange = radius return self end - --- Function to set switch-on/off zone for the SAM sites in meters + --- Function to set switch-on/off zone for the SAM sites in meters. Overwritten per SAM in automode. -- @param #MANTIS self - -- @param #number radius Radius of the firing zone + -- @param #number radius Radius of the firing zone in classic mode function MANTIS:SetSAMRadius(radius) self:T(self.lid .. "SetSAMRadius") local radius = radius or 25000 @@ -538,27 +756,43 @@ do return self end - --- Function to set SAM firing engage range, 0-100 percent, e.g. 75 + --- Function to set SAM firing engage range, 0-100 percent, e.g. 85 -- @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 + local range = range or 95 if range < 0 or range > 100 then - range = 75 + range = 95 end self.engagerange = range return self end + + --- Function to set number of SAMs going active on a valid, detected thread + -- @param #MANTIS self + -- @param #number Short Number of short-range systems activated, defaults to 1. + -- @param #number Mid Number of mid-range systems activated, defaults to 2. + -- @param #number Long Number of long-range systems activated, defaults to 2. + -- @param #number Classic (non-automode) Number of overall systems activated, defaults to 6. + -- @return #MANTIS self + function MANTIS:SetMaxActiveSAMs(Short,Mid,Long,Classic) + self:T(self.lid .. "SetMaxActiveSAMs") + self.maxclassic = Classic or 6 + self.maxlongrange = Long or 1 + self.maxmidrange = Mid or 2 + self.maxshortrange = Short or 2 + 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 + local range = range or 95 if range < 0 or range > 100 then - range = 75 + range = 95 end self.engagerange = range self:_RefreshSAMTable() @@ -682,7 +916,7 @@ do return self end - --- Set using an #INTEL_DLINK object instead of #DETECTION + --- Set using your own #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) @@ -803,6 +1037,7 @@ do --- [Internal] Function to execute the relocation -- @param #MANTIS self + -- @return #MANTIS self function MANTIS:_RelocateGroups() self:T(self.lid .. "RelocateGroups") local text = self.lid.." Relocating Groups" @@ -837,38 +1072,124 @@ do end return self end - + + --- [Internal] Function to check accept and reject zones + -- @param #MANTIS self + -- @param Core.Point#COORDINATE coord The coordinate to check + -- @return #boolean outcome + function MANTIS:_CheckCoordinateInZones(coord) + -- DEBUG + self:T(self.lid.."_CheckCoordinateInZones") + local inzone = false + -- acceptzones + if #self.AcceptZones > 0 then + for _,_zone in pairs(self.AcceptZones) do + local zone = _zone -- Core.Zone#ZONE + if zone:IsCoordinateInZone(coord) then + inzone = true + self:T(self.lid.."Target coord in Accept Zone!") + break + end + end + end + -- rejectzones + if #self.RejectZones > 0 and inzone then -- maybe in accept zone, but check the overlaps + for _,_zone in pairs(self.RejectZones) do + local zone = _zone -- Core.Zone#ZONE + if zone:IsCoordinateInZone(coord) then + inzone = false + self:T(self.lid.."Target coord in Reject Zone!") + break + end + end + end + -- conflictzones + if #self.ConflictZones > 0 and not inzone then -- if not already accepted, might be in conflict zones + for _,_zone in pairs(self.ConflictZones) do + local zone = _zone -- Core.Zone#ZONE + if zone:IsCoordinateInZone(coord) then + inzone = true + self:T(self.lid.."Target coord in Conflict Zone!") + break + end + end + end + return inzone + end + + --- [Internal] Function to prefilter height based + -- @param #MANTIS self + -- @param #number height + -- @return #table set + function MANTIS:_PreFilterHeight(height) + self:T(self.lid.."_PreFilterHeight") + local set = {} + local dlink = self.Detection -- Ops.Intelligence#INTEL_DLINK + local detectedgroups = dlink:GetContactTable() + for _,_contact in pairs(detectedgroups) do + local contact = _contact -- Ops.Intelligence#INTEL.Contact + local grp = contact.group -- Wrapper.Group#GROUP + if grp:IsAlive() then + if grp:GetHeight(true) < height then + local coord = grp:GetCoordinate() + table.insert(set,coord) + end + end + end + return set + 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. + -- @param #number radius Radius to check. + -- @param #number height Height to check. + -- @param #boolean dlink Data from DLINK. -- @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") + function MANTIS:_CheckObjectInZone(dectset, samcoordinate, radius, height, dlink) + self:T(self.lid.."_CheckObjectInZone") -- check if non of the coordinate is in the given defense zone - local radius = self.checkradius + local rad = radius or self.checkradius local set = dectset + if dlink then + -- DEBUG + set = self:_PreFilterHeight(height) + end 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 + if not targetdistance then + targetdistance = samcoordinate:Get2DDistance(coord) + end + -- check accept/reject zones + local zonecheck = true + if self.usezones then + -- DONE + zonecheck = self:_CheckCoordinateInZones(coord) + end + if self.verbose and 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 inrange = "false" + if targetdistance <= rad then + inrange = "true" + end + local text = string.format("Checking SAM at %s | Targetdist %d | Rad %d | Inrange %s", samstring, targetdistance, rad, inrange) local m = MESSAGE:New(text,10,"Check"):ToAllIf(self.debug) - self:I(self.lid..text) + self:T(self.lid..text) end -- end output to cross-check - if targetdistance <= radius then + if targetdistance <= rad and zonecheck then return true, targetdistance end end return false, 0 end - --- [Internal] Function to start the detection via EWR groups + --- [Internal] Function to start the detection via EWR groups - if INTEL isn\'t available -- @param #MANTIS self -- @return Functional.Detection #DETECTION_AREAS The running detection set function MANTIS:StartDetection() @@ -877,29 +1198,52 @@ do -- 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 acceptrange = self.acceptrange or 80000 + local interval = self.detectinterval or 20 + 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:SetAcceptRange(acceptrange) -- deprecated - in range of SAMs is used anyway MANTISdetection:SetRefreshTimeInterval(interval) - MANTISdetection:Start() + MANTISdetection:__Start(2) - 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 + + --- [Internal] Function to start the detection with INTEL via EWR groups + -- @param #MANTIS self + -- @return Ops.Intel#INTEL_DLINK The running detection set + function MANTIS:StartIntelDetection() + self:T(self.lid.."Starting Intel Detection") + -- DEBUG + -- start detection + local groupset = self.EWR_Group + local samset = self.SAM_Group + + self.intelset = {} + + local IntelOne = INTEL:New(groupset,self.Coalition,self.name.." IntelOne") + --IntelOne:SetClusterAnalysis(true,true) + --IntelOne:SetClusterRadius(5000) + IntelOne:Start() + + local IntelTwo = INTEL:New(samset,self.Coalition,self.name.." IntelTwo") + --IntelTwo:SetClusterAnalysis(true,true) + --IntelTwo:SetClusterRadius(5000) + IntelTwo:Start() + + local IntelDlink = INTEL_DLINK:New({IntelOne,IntelTwo},self.name.." DLINK",22,300) + IntelDlink:__Start(1) + + self:SetUsingDLink(IntelDlink) + + table.insert(self.intelset, IntelOne) + table.insert(self.intelset, IntelTwo) + + return IntelDlink + end + + --- [Internal] Function to start the detection via AWACS if defined as separate (classic) -- @param #MANTIS self -- @return Functional.Detection #DETECTION_AREAS The running detection set function MANTIS:StartAwacsDetection() @@ -919,34 +1263,124 @@ do 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 get SAM firing data from units types. + -- @param #MANTIS self + -- @param #string grpname Name of the group + -- @param #boolean mod HDS mod flag + -- @return #number range Max firing range + -- @return #number height Max firing height + -- @return #string type Long, medium or short range + -- @return #number blind "blind" spot + function MANTIS:_GetSAMDataFromUnits(grpname,mod) + self:T(self.lid.."_GetSAMRangeFromUnits") + local found = false + local range = self.checkradius + local height = 3000 + local type = MANTIS.SamType.MEDIUM + local radiusscale = self.radiusscale[type] + local blind = 0 + local group = GROUP:FindByName(grpname) -- Wrapper.Group#GROUP + local units = group:GetUnits() + local SAMData = self.SamData + if mod then + SAMData = self.SamDataHDS + end + --self:I("Looking to auto-match for "..grpname) + for _,_unit in pairs(units) do + local unit = _unit -- Wrapper.Unit#UNIT + local type = string.lower(unit:GetTypeName()) + --self:I(string.format("Matching typename: %s",type)) + for idx,entry in pairs(SAMData) do + local _entry = entry -- #MANTIS.SamData + local _radar = string.lower(_entry.Radar) + --self:I(string.format("Trying typename: %s",_radar)) + if string.find(type,_radar,1,true) then + type = _entry.Type + radiusscale = self.radiusscale[type] + range = _entry.Range * 1000 * radiusscale -- max firing range used as switch-on + height = _entry.Height * 1000 -- max firing height + blind = _entry.Blindspot * 100 -- blind spot range + --self:I(string.format("Match: %s - %s",_radar,type)) + found = true + break + end + end + if found then break end + end + if not found then + self:E(self.lid .. string.format("*****Could not match radar data for %s! Will default to midrange values!",grpname)) + end + return range, height, type, blind + end + + --- [Internal] Function to get SAM firing data + -- @param #MANTIS self + -- @param #string grpname Name of the group + -- @return #number range Max firing range + -- @return #number height Max firing height + -- @return #string type Long, medium or short range + -- @return #number blind "blind" spot + function MANTIS:_GetSAMRange(grpname) + self:T(self.lid.."_GetSAMRange") + local range = self.checkradius + local height = 3000 + local type = MANTIS.SamType.MEDIUM + local radiusscale = self.radiusscale[type] + local blind = 0 + local found = false + local HDSmod = false + if string.find(grpname,"HDS",1,true) then + HDSmod = true + end + if self.automode then + for idx,entry in pairs(self.SamData) do + --self:I("ID = " .. idx) + if string.find(grpname,idx,1,true) then + local _entry = entry -- #MANTIS.SamData + type = _entry.Type + radiusscale = self.radiusscale[type] + range = _entry.Range * 1000 * radiusscale -- max firing range + height = _entry.Height * 1000 -- max firing height + blind = _entry.Blindspot + --self:I("Matching Groupname = " .. grpname .. " Range= " .. range) + found = true + break + end + end + end + -- secondary filter if not found + if (not found and self.automode) or HDSmod then + range, height, type = self:_GetSAMDataFromUnits(grpname,HDSmod) + elseif not found then + self:E(self.lid .. string.format("*****Could not match radar data for %s! Will default to midrange values!",grpname)) + end + return range, height, type, blind + 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 + -- DONE: Auto mode 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 SAM_Tbl_lg = {} -- table of long range SAM defense zones + local SAM_Tbl_md = {} -- table of mid range SAM defense zones + local SAM_Tbl_sh = {} -- table of short range 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 + if _group:IsGround() and _group:IsAlive() then local group = _group -- Wrapper.Group#GROUP - -- TODO: add emissions on/off + -- DONE: add emissions on/off if self.UseEmOnOff then group:OptionAlarmStateRed() group:EnableEmission(false) @@ -954,16 +1388,35 @@ do 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}) + group:OptionEngageRange(engagerange) --default engagement will be 95% of firing range + local grpname = group:GetName() + local grpcoord = group:GetCoordinate() + local grprange,grpheight,type,blind = self:_GetSAMRange(grpname) + table.insert( SAM_Tbl, {grpname, grpcoord, grprange, grpheight, blind}) + --table.insert( SEAD_Grps, grpname ) + if type == MANTIS.SamType.LONG then + table.insert( SAM_Tbl_lg, {grpname, grpcoord, grprange, grpheight, blind}) table.insert( SEAD_Grps, grpname ) - self.SamStateTracker[grpname] = "GREEN" + --self:T("SAM "..grpname.." is type LONG") + elseif type == MANTIS.SamType.MEDIUM then + table.insert( SAM_Tbl_md, {grpname, grpcoord, grprange, grpheight, blind}) + table.insert( SEAD_Grps, grpname ) + --self:T("SAM "..grpname.." is type MEDIUM") + elseif type == MANTIS.SamType.SHORT then + table.insert( SAM_Tbl_sh, {grpname, grpcoord, grprange, grpheight, blind}) + --self:T("SAM "..grpname.." is type SHORT") + self.ShoradGroupSet:Add(grpname,group) + if not self.autoshorad then + table.insert( SEAD_Grps, grpname ) + end + end + self.SamStateTracker[grpname] = "GREEN" end end self.SAM_Table = SAM_Tbl + self.SAM_Table_Long = SAM_Tbl_lg + self.SAM_Table_Medium = SAM_Tbl_md + self.SAM_Table_Short = SAM_Tbl_sh -- make SAMs evasive local mysead = SEAD:New( SEAD_Grps, self.Padding ) -- Functional.Sead#SEAD mysead:SetEngagementRange(engagerange) @@ -985,20 +1438,41 @@ do local SAM_SET = self.SAM_Group local SAM_Grps = SAM_SET.Set --table of objects local SAM_Tbl = {} -- table of SAM defense zones + local SAM_Tbl_lg = {} -- table of long range SAM defense zones + local SAM_Tbl_md = {} -- table of mid range SAM defense zones + local SAM_Tbl_sh = {} -- table of short range SAM defense zon 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 + local group = _group -- Wrapper.Group#GROUP + group:OptionEngageRange(engagerange) --engagement will be 95% 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 + local grprange, grpheight,type,blind = self:_GetSAMRange(grpname) + table.insert( SAM_Tbl, {grpname, grpcoord, grprange, grpheight, blind}) -- make the table lighter, as I don't really use the zone here table.insert( SEAD_Grps, grpname ) + if type == MANTIS.SamType.LONG then + table.insert( SAM_Tbl_lg, {grpname, grpcoord, grprange, grpheight, blind}) + --self:I({grpname,grprange, grpheight}) + elseif type == MANTIS.SamType.MEDIUM then + table.insert( SAM_Tbl_md, {grpname, grpcoord, grprange, grpheight, blind}) + --self:I({grpname,grprange, grpheight}) + elseif type == MANTIS.SamType.SHORT then + table.insert( SAM_Tbl_sh, {grpname, grpcoord, grprange, grpheight, blind}) + -- self:I({grpname,grprange, grpheight}) + self.ShoradGroupSet:Add(grpname,group) + if self.autoshorad then + self.Shorad.Groupset = self.ShoradGroupSet + end + end end end self.SAM_Table = SAM_Tbl + self.SAM_Table_Long = SAM_Tbl_lg + self.SAM_Table_Medium = SAM_Tbl_md + self.SAM_Table_Short = SAM_Tbl_sh -- make SAMs evasive if self.mysead ~= nil then local mysead = self.mysead @@ -1035,46 +1509,48 @@ do ----------------------------------------------------------------------- -- MANTIS main functions ----------------------------------------------------------------------- - + --- [Internal] Check detection function -- @param #MANTIS self - -- @param Functional.Detection#DETECTION_AREAS detection Detection object + -- @param #table samset Table of SAM data + -- @param #table detset Table of COORDINATES + -- @param #boolean dlink Using DLINK + -- @param #number limit of SAM sites to go active on a contact -- @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 + function MANTIS:_CheckLoop(samset,detset,dlink,limit) + self:T(self.lid .. "CheckLoop " .. #detset .. " Coordinates") + local switchedon = 0 for _,_data in pairs (samset) do local samcoordinate = _data[2] local name = _data[1] + local radius = _data[3] + local height = _data[4] + local blind = _data[5] * 1.25 + 1 local samgroup = GROUP:FindByName(name) - local IsInZone, Distance = self:CheckObjectInZone(detset, samcoordinate) + local IsInZone, Distance = self:_CheckObjectInZone(detset, samcoordinate, radius, height, dlink) local suppressed = self.SuppressedGroups[name] or false - if IsInZone then --check any target in zone and not curr managed by SEAD + local activeshorad = self.Shorad.ActiveGroups[name] or false + if IsInZone and not suppressed and not activeshorad then --check any target in zone and not currently managed by SEAD if samgroup:IsAlive() then -- switch on SAM - if self.UseEmOnOff and not suppressed then + local switch = false + if self.UseEmOnOff and switchedon < limit then -- DONE: add emissions on/off - --samgroup:SetAIOn() samgroup:EnableEmission(true) - elseif not self.UseEmOnOff and not suppressed then + switchedon = switchedon + 1 + switch = true + elseif (not self.UseEmOnOff) and switchedon < limit then samgroup:OptionAlarmStateRed() + switchedon = switchedon + 1 + switch = true end - if self.SamStateTracker[name] ~= "RED" and not suppressed then + if self.SamStateTracker[name] ~= "RED" and switch 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 or suppressed) then -- don't give SHORAD position away too early + if self.ShoradLink and (Distance < self.ShoradActDistance or Distance < blind ) then -- don't give SHORAD position away too early local Shorad = self.Shorad local radius = self.checkradius local ontime = self.ShoradTime @@ -1082,26 +1558,26 @@ do self:__ShoradActivated(1,name, radius, ontime) end -- debug output - if self.debug or self.verbose and not suppressed then - local text = string.format("SAM %s switched to alarm state RED!", name) + if (self.debug or self.verbose) and switch then + local text = string.format("SAM %s in 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 + if samgroup:IsAlive() and not suppressed and not activeshorad then -- switch off SAM - if self.UseEmOnOff and not suppressed then + if self.UseEmOnOff then samgroup:EnableEmission(false) - elseif not self.UseEmOnOff and not suppressed then + else samgroup:OptionAlarmStateGreen() end - if self.SamStateTracker[name] ~= "GREEN" and not suppressed then + if self.SamStateTracker[name] ~= "GREEN" then self:__GreenState(1,samgroup) self.SamStateTracker[name] = "GREEN" end - if self.debug or self.verbose and not suppressed then - local text = string.format("SAM %s switched to alarm state GREEN!", name) + if self.debug or self.verbose then + local text = string.format("SAM %s in 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 @@ -1110,6 +1586,36 @@ do end --for for loop return self end + + --- [Internal] Check detection function + -- @param #MANTIS self + -- @param Functional.Detection#DETECTION_AREAS detection Detection object + -- @param #boolean dlink + -- @return #MANTIS self + function MANTIS:_Check(detection,dlink) + 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 + if self.automode then + local samset = self.SAM_Table_Long -- table of i.1=names, i.2=coordinates, i.3=firing range, i.4=firing height + self:_CheckLoop(samset,detset,dlink,self.maxlongrange) + local samset = self.SAM_Table_Medium -- table of i.1=names, i.2=coordinates, i.3=firing range, i.4=firing height + self:_CheckLoop(samset,detset,dlink,self.maxmidrange) + local samset = self.SAM_Table_Short -- table of i.1=names, i.2=coordinates, i.3=firing range, i.4=firing height + self:_CheckLoop(samset,detset,dlink,self.maxshortrange) + else + local samset = self:_GetSAMTable() -- table of i.1=names, i.2=coordinates, i.3=firing range, i.4=firing height + self:_CheckLoop(samset,detset,dlink,self.maxclassic) + end + return self + end --- [Internal] Relocation relay function -- @param #MANTIS self @@ -1139,11 +1645,12 @@ do local samgroup = GROUP:FindByName(name) if samgroup:IsAlive() then if self.UseEmOnOff then - -- TODO: add emissions on/off + -- DONE: add emissions on/off --samgroup:SetAIOn() samgroup:EnableEmission(true) + else + samgroup:OptionAlarmStateRed() end - samgroup:OptionAlarmStateRed() end -- end alive end -- end for loop elseif newstate <= 1 then @@ -1179,12 +1686,22 @@ do self:T({From, Event, To}) self:T(self.lid.."Starting MANTIS") self:SetSAMStartState() - if not self.DLink then + if not INTEL then self.Detection = self:StartDetection() + else + self.Detection = self:StartIntelDetection() end - if self.advAwacs then + --[[ + if self.advAwacs and not self.automode then self.AWACS_Detection = self:StartAwacsDetection() end + --]] + if self.autoshorad then + self.Shorad = SHORAD:New(self.name.."-SHORAD",self.name.."-SHORAD",self.SAM_Group,self.ShoradActDistance,self.ShoradTime,self.coalition,self.UseEmOnOff) + self.Shorad:SetDefenseLimits(80,95) + self.ShoradLink = true + self.Shorad.Groupset=self.ShoradGroupSet + end self:__Status(-math.random(1,10)) return self end @@ -1199,14 +1716,15 @@ do self:T({From, Event, To}) -- check detection if not self.state2flag then - self:_Check(self.Detection) + self:_Check(self.Detection,self.DLink) end - -- check Awacs + --[[ check Awacs if self.advAwacs and not self.state2flag then - self:_Check(self.AWACS_Detection) + self:_Check(self.AWACS_Detection,false) end - + --]] + -- relocate HQ and EWR if self.autorelocate then local relointerval = self.relointerval @@ -1246,7 +1764,7 @@ do function MANTIS:onafterStatus(From,Event,To) self:T({From, Event, To}) -- Display some states - if self.debug then + if self.debug and self.verbose 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)) @@ -1287,7 +1805,7 @@ do -- @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}) + self:T({From, Event, To, Group:GetName()}) return self end @@ -1299,7 +1817,7 @@ do -- @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}) + self:T({From, Event, To, Group:GetName()}) return self end @@ -1337,9 +1855,17 @@ do -- @param #string To The To State -- @param Wrapper.Group#GROUP Group The suppressed GROUP object -- @param #string Name Name of the suppressed group - function MANTIS:onafterSeadSuppressionStart(From, Event, To, Group, Name) + -- @param Wrapper.Group#GROUP Attacker The attacking GROUP object + function MANTIS:onafterSeadSuppressionStart(From, Event, To, Group, Name, Attacker) self:T({From, Event, To, Name}) self.SuppressedGroups[Name] = true + if self.ShoradLink then + local Shorad = self.Shorad + local radius = self.checkradius + local ontime = self.ShoradTime + Shorad:WakeUpShorad(Name, radius, ontime) + self:__ShoradActivated(1,Name, radius, ontime) + end return self end @@ -1365,7 +1891,8 @@ do -- @param #string Name Name of the suppressed group -- @param #number SuppressionStartTime Model start time of the suppression from `timer.getTime()` -- @param #number SuppressionEndTime Model end time of the suppression from `timer.getTime()` - function MANTIS:onafterSeadSuppressionPlanned(From, Event, To, Group, Name, SuppressionStartTime, SuppressionEndTime) + -- @param Wrapper.Group#GROUP Attacker The attacking GROUP object + function MANTIS:onafterSeadSuppressionPlanned(From, Event, To, Group, Name, SuppressionStartTime, SuppressionEndTime, Attacker) self:T({From, Event, To, Name}) return self end diff --git a/Moose Development/Moose/Functional/MissileTrainer.lua b/Moose Development/Moose/Functional/MissileTrainer.lua index 2844b701d..cc8de571b 100644 --- a/Moose Development/Moose/Functional/MissileTrainer.lua +++ b/Moose Development/Moose/Functional/MissileTrainer.lua @@ -1,28 +1,28 @@ --- **Functional** -- Train missile defence and deflection. --- +-- -- === -- -- ## Features: --- +-- -- * Track the missiles fired at you and other players, providing bearing and range information of the missiles towards the airplanes. --- * Provide alerts of missile launches, including detailed information of the units launching, including bearing, range ° +-- * Provide alerts of missile launches, including detailed information of the units launching, including bearing, range -- * Provide alerts when a missile would have killed your aircraft. -- * Provide alerts when the missile self destructs. -- * Enable / Disable and Configure the Missile Trainer using the various menu options. --- +-- -- === --- +-- -- ## Missions: --- +-- -- [MIT - Missile Trainer](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/MIT%20-%20Missile%20Trainer) --- +-- -- === --- +-- -- Uses the MOOSE messaging system to be alerted of any missiles fired, and when a missile would hit your aircraft, -- the class will destroy the missile within a certain range, to avoid damage to your aircraft. --- +-- -- When running a mission where the missile trainer is used, the following radio menu structure ( 'Radio Menu' -> 'Other (F10)' -> 'MissileTrainer' ) options are available for the players: --- +-- -- * **Messages**: Menu to configure all messages. -- * **Messages On**: Show all messages. -- * **Messages Off**: Disable all messages. @@ -45,23 +45,23 @@ -- * **Range Off**: Disable range information when a missile is fired to a target. -- * **Bearing On**: Shows bearing information when a missile is fired to a target. -- * **Bearing Off**: Disable bearing information when a missile is fired to a target. --- * **Distance**: Menu to configure the distance when a missile needs to be destroyed when near to a player, during tracking. This will improve/influence hit calculation accuracy, but has the risk of damaging the aircraft when the missile reaches the aircraft before the distance is measured. +-- * **Distance**: Menu to configure the distance when a missile needs to be destroyed when near to a player, during tracking. This will improve/influence hit calculation accuracy, but has the risk of damaging the aircraft when the missile reaches the aircraft before the distance is measured. -- * **50 meter**: Destroys the missile when the distance to the aircraft is below or equal to 50 meter. -- * **100 meter**: Destroys the missile when the distance to the aircraft is below or equal to 100 meter. -- * **150 meter**: Destroys the missile when the distance to the aircraft is below or equal to 150 meter. -- * **200 meter**: Destroys the missile when the distance to the aircraft is below or equal to 200 meter. --- +-- -- === --- +-- -- ### Authors: **FlightControl** --- +-- -- ### Contributions: --- --- * **Stuka (Danny)**: Who you can search on the Eagle Dynamics Forums. Working together with Danny has resulted in the MISSILETRAINER class. --- Danny has shared his ideas and together we made a design. +-- +-- * **Stuka (Danny)**: Who you can search on the Eagle Dynamics Forums. Working together with Danny has resulted in the MISSILETRAINER class. +-- Danny has shared his ideas and together we made a design. -- Together with the **476 virtual team**, we tested the MISSILETRAINER class, and got much positive feedback! -- * **132nd Squadron**: Testing and optimizing the logic. --- +-- -- === -- -- @module Functional.MissileTrainer @@ -76,7 +76,7 @@ --- -- -- # Constructor: --- +-- -- Create a new MISSILETRAINER object with the @{#MISSILETRAINER.New} method: -- -- * @{#MISSILETRAINER.New}: Creates a new MISSILETRAINER object taking the maximum distance to your aircraft to evaluate when a missile needs to be destroyed. @@ -84,7 +84,7 @@ -- MISSILETRAINER will collect each unit declared in the mission with a skill level "Client" and "Player", and will monitor the missiles shot at those. -- -- # Initialization: --- +-- -- A MISSILETRAINER object will behave differently based on the usage of initialization methods: -- -- * @{#MISSILETRAINER.InitMessagesOnOff}: Sets by default the display of any message to be ON or OFF. @@ -97,8 +97,8 @@ -- * @{#MISSILETRAINER.InitRangeOnOff}: Sets by default the display of range information of missiles ON of OFF. -- * @{#MISSILETRAINER.InitBearingOnOff}: Sets by default the display of bearing information of missiles ON of OFF. -- * @{#MISSILETRAINER.InitMenusOnOff}: Allows to configure the options through the radio menu. --- --- @field #MISSILETRAINER +-- +-- @field #MISSILETRAINER MISSILETRAINER = { ClassName = "MISSILETRAINER", TrackingMissiles = {}, @@ -167,7 +167,7 @@ end -- When a missile is fired a SCHEDULER is set off that follows the missile. When near a certain a client player, the missile will be destroyed. -- @param #MISSILETRAINER self -- @param #number Distance The distance in meters when a tracked missile needs to be destroyed when close to a player. --- @param #string Briefing (Optional) Will show a text to the players when starting their mission. Can be used for briefing purposes. +-- @param #string Briefing (Optional) Will show a text to the players when starting their mission. Can be used for briefing purposes. -- @return #MISSILETRAINER function MISSILETRAINER:New( Distance, Briefing ) local self = BASE:Inherit( self, BASE:New() ) @@ -194,8 +194,8 @@ function MISSILETRAINER:New( Distance, Briefing ) -- self:F( "ForEach:" .. Client.UnitName ) -- Client:Alive( self._Alive, self ) -- end --- - self.DBClients:ForEachClient( +-- + self.DBClients:ForEachClient( function( Client ) self:F( "ForEach:" .. Client.UnitName ) Client:Alive( self._Alive, self ) @@ -207,9 +207,9 @@ function MISSILETRAINER:New( Distance, Briefing ) -- self.DB:ForEachClient( -- --- @param Wrapper.Client#CLIENT Client -- function( Client ) --- +-- -- ... actions ... --- +-- -- end -- ) @@ -225,7 +225,7 @@ function MISSILETRAINER:New( Distance, Briefing ) self.DetailsRangeOnOff = true self.DetailsBearingOnOff = true - + self.MenusOnOff = true self.TrackingMissiles = {} @@ -293,7 +293,7 @@ end --- Increases, decreases the missile tracking message display frequency with the provided time interval in seconds. -- The default frequency is a 3 second interval, so the Tracking Frequency parameter specifies the increase or decrease from the default 3 seconds or the last frequency update. -- @param #MISSILETRAINER self --- @param #number TrackingFrequency Provide a negative or positive value in seconds to incraese or decrease the display frequency. +-- @param #number TrackingFrequency Provide a negative or positive value in seconds to incraese or decrease the display frequency. -- @return #MISSILETRAINER self function MISSILETRAINER:InitTrackingFrequency( TrackingFrequency ) self:F( TrackingFrequency ) @@ -478,30 +478,30 @@ function MISSILETRAINER:OnEventShot( EVentData ) if TrainerTargetDCSUnit then local TrainerTargetDCSUnitName = Unit.getName( TrainerTargetDCSUnit ) local TrainerTargetSkill = _DATABASE.Templates.Units[TrainerTargetDCSUnitName].Template.skill - + self:T(TrainerTargetDCSUnitName ) - + local Client = self.DBClients:FindClient( TrainerTargetDCSUnitName ) if Client then - + local TrainerSourceUnit = UNIT:Find( TrainerSourceDCSUnit ) local TrainerTargetUnit = UNIT:Find( TrainerTargetDCSUnit ) - + if self.MessagesOnOff == true and self.AlertsLaunchesOnOff == true then - + local Message = MESSAGE:New( string.format( "%s launched a %s", TrainerSourceUnit:GetTypeName(), TrainerWeaponName ) .. self:_AddRange( Client, TrainerWeapon ) .. self:_AddBearing( Client, TrainerWeapon ), 5, "Launch Alert" ) - + if self.AlertsToAll then Message:ToAll() else Message:ToClient( Client ) end end - + local ClientID = Client:GetID() self:T( ClientID ) local MissileData = {} @@ -579,52 +579,52 @@ function MISSILETRAINER:_TrackMissiles() end -- ALERTS PART - + -- Loop for all Player Clients to check the alerts and deletion of missiles. for ClientDataID, ClientData in pairs( self.TrackingMissiles ) do local Client = ClientData.Client - + if Client and Client:IsAlive() then for MissileDataID, MissileData in pairs( ClientData.MissileData ) do self:T3( MissileDataID ) - + local TrainerSourceUnit = MissileData.TrainerSourceUnit local TrainerWeapon = MissileData.TrainerWeapon local TrainerTargetUnit = MissileData.TrainerTargetUnit local TrainerWeaponTypeName = MissileData.TrainerWeaponTypeName local TrainerWeaponLaunched = MissileData.TrainerWeaponLaunched - + if Client and Client:IsAlive() and TrainerSourceUnit and TrainerSourceUnit:IsAlive() and TrainerWeapon and TrainerWeapon:isExist() and TrainerTargetUnit and TrainerTargetUnit:IsAlive() then local PositionMissile = TrainerWeapon:getPosition().p local TargetVec3 = Client:GetVec3() - + local Distance = ( ( PositionMissile.x - TargetVec3.x )^2 + ( PositionMissile.y - TargetVec3.y )^2 + ( PositionMissile.z - TargetVec3.z )^2 ) ^ 0.5 / 1000 - + if Distance <= self.Distance then -- Hit alert TrainerWeapon:destroy() if self.MessagesOnOff == true and self.AlertsHitsOnOff == true then - + self:T( "killed" ) - + local Message = MESSAGE:New( string.format( "%s launched by %s killed %s", TrainerWeapon:getTypeName(), TrainerSourceUnit:GetTypeName(), TrainerTargetUnit:GetPlayerName() ), 15, "Hit Alert" ) - + if self.AlertsToAll == true then Message:ToAll() else Message:ToClient( Client ) end - + MissileData = nil table.remove( ClientData.MissileData, MissileDataID ) self:T(ClientData.MissileData) @@ -639,7 +639,7 @@ function MISSILETRAINER:_TrackMissiles() TrainerWeaponTypeName, TrainerSourceUnit:GetTypeName() ), 5, "Tracking" ) - + if self.AlertsToAll == true then Message:ToAll() else @@ -660,41 +660,41 @@ function MISSILETRAINER:_TrackMissiles() if ShowMessages == true and self.MessagesOnOff == true and self.TrackingOnOff == true then -- Only do this when tracking information needs to be displayed. -- TRACKING PART - + -- For the current client, the missile range and bearing details are displayed To the Player Client. -- For the other clients, the missile range and bearing details are displayed To the other Player Clients. - -- To achieve this, a cross loop is done for each Player Client <-> Other Player Client missile information. - + -- To achieve this, a cross loop is done for each Player Client <-> Other Player Client missile information. + -- Main Player Client loop for ClientDataID, ClientData in pairs( self.TrackingMissiles ) do - + local Client = ClientData.Client --self:T2( { Client:GetName() } ) - - + + ClientData.MessageToClient = "" ClientData.MessageToAll = "" - + -- Other Players Client loop for TrackingDataID, TrackingData in pairs( self.TrackingMissiles ) do - + for MissileDataID, MissileData in pairs( TrackingData.MissileData ) do --self:T3( MissileDataID ) - + local TrainerSourceUnit = MissileData.TrainerSourceUnit local TrainerWeapon = MissileData.TrainerWeapon local TrainerTargetUnit = MissileData.TrainerTargetUnit local TrainerWeaponTypeName = MissileData.TrainerWeaponTypeName local TrainerWeaponLaunched = MissileData.TrainerWeaponLaunched - + if Client and Client:IsAlive() and TrainerSourceUnit and TrainerSourceUnit:IsAlive() and TrainerWeapon and TrainerWeapon:isExist() and TrainerTargetUnit and TrainerTargetUnit:IsAlive() then - + if ShowMessages == true then local TrackingTo TrackingTo = string.format( " -> %s", TrainerWeaponTypeName ) - + if ClientDataID == TrackingDataID then if ClientData.MessageToClient == "" then ClientData.MessageToClient = "Missiles to You:\n" @@ -712,7 +712,7 @@ function MISSILETRAINER:_TrackMissiles() end end end - + -- Once the Player Client and the Other Player Client tracking messages are prepared, show them. if ClientData.MessageToClient ~= "" or ClientData.MessageToAll ~= "" then local Message = MESSAGE:New( ClientData.MessageToClient .. ClientData.MessageToAll, 1, "Tracking" ):ToClient( Client ) diff --git a/Moose Development/Moose/Functional/PseudoATC.lua b/Moose Development/Moose/Functional/PseudoATC.lua index 7e26f74e7..a203ec8fe 100644 --- a/Moose Development/Moose/Functional/PseudoATC.lua +++ b/Moose Development/Moose/Functional/PseudoATC.lua @@ -1044,6 +1044,3 @@ function PSEUDOATC:_myname(unitname) return string.format("%s (%s)", csign, pname) end - - - diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 139189959..b33285764 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -1,13 +1,13 @@ --- **Functional** - Create random airtraffic in your missions. --- +-- -- === --- --- The aim of the RAT class is to fill the empty DCS world with randomized air traffic and bring more life to your airports. --- In particular, it is designed to spawn AI air units at random airports. These units will be assigned a random flight path to another random airport on the map. +-- +-- The aim of the RAT class is to fill the empty DCS world with randomized air traffic and bring more life to your airports. +-- In particular, it is designed to spawn AI air units at random airports. These units will be assigned a random flight path to another random airport on the map. -- Even the mission designer will not know where aircraft will be spawned and which route they follow. --- +-- -- ## Features: --- +-- -- * Very simple interface. Just one unit and two lines of Lua code needed to fill your map. -- * High degree of randomization. Aircraft will spawn at random airports, have random routes and random destinations. -- * Specific departure and/or destination airports can be chosen. @@ -19,37 +19,37 @@ -- * Aircraft can report their status during the route. -- * All of the above can be customized by the user if necessary. -- * All current (Caucasus, Nevada, Normandy, Persian Gulf) and future maps are supported. --- +-- -- The RAT class creates an entry in the F10 radio menu which allows to: --- +-- -- * Create new groups on-the-fly, i.e. at run time within the mission, -- * Destroy specific groups (e.g. if they get stuck or damaged and block a runway), -- * Request the status of all RAT aircraft or individual groups, -- * Place markers at waypoints on the F10 map for each group. --- +-- -- Note that by its very nature, this class is suited best for civil or transport aircraft. However, it also works perfectly fine for military aircraft of any kind. --- +-- -- More of the documentation include some simple examples can be found further down this page. --- +-- -- === --- +-- -- ## Missions: -- -- ### [RAT - Random Air Traffic](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/RAT%20-%20Random%20Air%20Traffic) --- +-- -- === --- +-- -- # YouTube Channel --- --- ### [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) +-- +-- ### [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) -- ### [MOOSE - RAT - Random Air Traffic](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0u4Zxywtg-mx_ov4vi68CO) --- +-- -- === --- +-- -- ### Author: **[funkyfranky](https://forums.eagle.ru/member.php?u=115026)** --- +-- -- ### Contributions: [FlightControl](https://forums.eagle.ru/member.php?u=89536) --- +-- -- === -- @module Functional.Rat -- @image RAT.JPG @@ -108,7 +108,7 @@ -- @field #number FLminuser Minimum flight level set by user. -- @field #number FLmaxuser Maximum flight level set by user. -- @field #boolean commute Aircraft commute between departure and destination, i.e. when respawned the departure airport becomes the new destiation. --- @field #boolean starshape If true, aircraft travel A-->B-->A-->C-->A-->D... for commute. +-- @field #boolean starshape If true, aircraft travel A-->B-->A-->C-->A-->D... for commute. -- @field #string homebase Home base for commute and return zone. Aircraft will always return to this base but otherwise travel in a star shaped way. -- @field #boolean continuejourney Aircraft will continue their journey, i.e. get respawned at their destination with a new random destination. -- @field #number ngroups Number of groups to be spawned in total. @@ -143,7 +143,7 @@ -- @field #number onboardnum0 (Optional) Starting value of the automatically appended numbering of aircraft within a flight. Default is 1. -- @field #boolean checkonrunway Aircraft are checked if they were accidentally spawned on the runway. Default is true. -- @field #number onrunwayradius Distance (in meters) from a runway spawn point until a unit is considered to have accidentally been spawned on a runway. Default is 75 m. --- @field #number onrunwaymaxretry Number of respawn retries (on ground) at other airports if a group gets accidentally spawned on the runway. Default is 3. +-- @field #number onrunwaymaxretry Number of respawn retries (on ground) at other airports if a group gets accidentally spawned on the runway. Default is 3. -- @field #boolean checkontop Aircraft are checked if they were accidentally spawned on top of another unit. Default is true. -- @field #number ontopradius Radius in meters until which a unit is considered to be on top of another. Default is 2 m. -- @field Wrapper.Airbase#AIRBASE.TerminalType termtype Type of terminal to be used when spawning at an airbase. @@ -157,25 +157,25 @@ --- Implements an easy to use way to randomly fill your map with AI aircraft. -- -- ## Airport Selection --- +-- -- ![Process](..\Presentations\RAT\RAT_Airport_Selection.png) --- +-- -- ### Default settings: --- +-- -- * By default, aircraft are spawned at airports of their own coalition (blue or red) or neutral airports. -- * Destination airports are by default also of neutral or of the same coalition as the template group of the spawned aircraft. -- * Possible destinations are restricted by their distance to the departure airport. The maximal distance depends on the max range of spawned aircraft type and its initial fuel amount. --- +-- -- ### The default behavior can be changed: --- +-- -- * A specific departure and/or destination airport can be chosen. -- * Valid coalitions can be set, e.g. only red, blue or neutral, all three "colours". -- * It is possible to start in air within a zone defined in the mission editor or within a zone above an airport of the map. --- +-- -- ## Flight Plan --- +-- -- ![Process](..\Presentations\RAT\RAT_Flight_Plan.png) --- +-- -- * A general flight plan has five main airborne segments: Climb, cruise, descent, holding and final approach. -- * Events monitored during the flight are: birth, engine-start, take-off, landing and engine-shutdown. -- * The default flight level (FL) is set to ~FL200, i.e. 20000 feet ASL but randomized for each aircraft. @@ -187,56 +187,56 @@ -- * The altitude of theholding point is ~1200 m AGL. Holding patterns might or might not happen with variable duration. -- * If an aircraft is spawned in air, the procedure omitts taxi and take-off and starts with the climb/cruising part. -- * All values are randomized for each spawned aircraft. --- +-- -- ## Mission Editor Setup --- +-- -- ![Process](..\Presentations\RAT\RAT_Mission_Setup.png) --- +-- -- Basic mission setup is very simple and essentially a three step process: --- +-- -- * Place your aircraft **anywhere** on the map. It really does not matter where you put it. -- * Give the group a good name. In the example above the group is named "RAT_YAK". --- * Activate the "LATE ACTIVATION" tick box. Note that this aircraft will not be spawned itself but serves a template for each RAT aircraft spawned when the mission starts. --- +-- * Activate the "LATE ACTIVATION" tick box. Note that this aircraft will not be spawned itself but serves a template for each RAT aircraft spawned when the mission starts. +-- -- Voilà, your already done! --- +-- -- Optionally, you can set a specific livery for the aircraft or give it some weapons. -- However, the aircraft will by default not engage any enemies. Think of them as beeing on a peaceful or ferry mission. --- +-- -- ## Basic Lua Script --- +-- -- ![Process](..\Presentations\RAT\RAT_Basic_Lua_Script.png) --- +-- -- The basic Lua script for one template group consits of two simple lines as shown in the picture above. --- +-- -- * **Line 2** creates a new RAT object "yak". The only required parameter for the constructor @{#RAT.New}() is the name of the group as defined in the mission editor. In this example it is "RAT_YAK". -- * **Line 5** trigger the command to spawn the aircraft. The (optional) parameter for the @{#RAT.Spawn}() function is the number of aircraft to be spawned of this object. -- By default each of these aircraft gets a random departure airport anywhere on the map and a random destination airport, which lies within range of the of the selected aircraft type. --- +-- -- In this simple example aircraft are respawned with a completely new flightplan when they have reached their destination airport. -- The "old" aircraft is despawned (destroyed) after it has shut-down its engines and a new aircraft of the same type is spawned at a random departure airport anywhere on the map. -- Hence, the default flight plan for a RAT aircraft will be: Fly from airport A to B, get respawned at C and fly to D, get respawned at E and fly to F, ... -- This ensures that you always have a constant number of AI aircraft on your map. --- +-- -- ## Parking Problems --- --- One big issue in DCS is that not all aircraft can be spawned on every airport or airbase. In particular, bigger aircraft might not have a valid parking spot at smaller airports and +-- +-- One big issue in DCS is that not all aircraft can be spawned on every airport or airbase. In particular, bigger aircraft might not have a valid parking spot at smaller airports and -- airstripes. This can lead to multiple problems in DCS. --- +-- -- * Landing: When an aircraft tries to land at an airport where it does not have a valid parking spot, it is immidiately despawned the moment its wheels touch the runway, i.e. -- when a landing event is triggered. This leads to the loss of the RAT aircraft. On possible way to circumvent the this problem is to let another RAT aircraft spawn at landing -- and not when it shuts down its engines. See the @{RAT.RespawnAfterLanding}() function. -- * Spawning: When a big aircraft is dynamically spawned on a small airbase a few things can go wrong. For example, it could be spawned at a parking spot with a shelter. -- Or it could be damaged by a scenery object when it is taxiing out to the runway, or it could overlap with other aircraft on parking spots near by. --- +-- -- You can check yourself if an aircraft has a valid parking spot at an airbase by dragging its group on the airport in the mission editor and set it to start from ramp. -- If it stays at the airport, it has a valid parking spot, if it jumps to another airport, it does not have a valid parking spot on that airbase. --- +-- -- ### Setting the Terminal Type -- Each parking spot has a specific type depending on its size or if a helicopter spot or a shelter etc. The classification is not perfect but it is the best we have. -- If you encounter problems described above, you can request a specific terminal type for the RAT aircraft. This can be done by the @{#RAT.SetTerminalType}(*terminaltype*) -- function. The parameter *terminaltype* can be set as follows --- +-- -- * AIRBASE.TerminalType.HelicopterOnly: Special spots for Helicopers. -- * AIRBASE.TerminalType.Shelter: Hardened Air Shelter. Currently only on Caucaus map. -- * AIRBASE.TerminalType.OpenMed: Open/Shelter air airplane only. @@ -244,96 +244,96 @@ -- * AIRBASE.TerminalType.OpenMedOrBig: Combines OpenMed and OpenBig spots. -- * AIRBASE.TerminalType.HelicopterUsable: Combines HelicopterOnly, OpenMed and OpenBig. -- * AIRBASE.TerminalType.FighterAircraft: Combines Shelter, OpenMed and OpenBig spots. So effectively all spots usable by fixed wing aircraft. --- +-- -- So for example -- c17=RAT:New("C-17") -- c17:SetTerminalType(AIRBASE.TerminalType.OpenBig) -- c17:Spawn(5) --- +-- -- This would randomly spawn five C-17s but only on airports which have big open air parking spots. Note that also only destination airports are allowed -- which do have this type of parking spot. This should ensure that the aircraft is able to land at the destination without beeing despawned immidiately. --- +-- -- Also, the aircraft are spawned only on the requested parking spot types and not on any other type. If no parking spot of this type is availabe at the -- moment of spawning, the group is automatically spawned in air above the selected airport. --- +-- -- ## Examples --- +-- -- Here are a few examples, how you can modify the default settings of RAT class objects. --- +-- -- ### Specify Departure and Destinations --- +-- -- ![Process](..\Presentations\RAT\RAT_Examples_Specify_Departure_and_Destination.png) --- +-- -- In the picture above you find a few possibilities how to modify the default behaviour to spawn at random airports and fly to random destinations. --- +-- -- In particular, you can specify fixed departure and/or destination airports. This is done via the @{#RAT.SetDeparture}() or @{#RAT.SetDestination}() functions, respectively. --- +-- -- * If you only fix a specific departure airport via @{#RAT.SetDeparture}() all aircraft will be spawned at that airport and get random destination airports. -- * If you only fix the destination airport via @{#RAT.SetDestination}(), aircraft a spawned at random departure airports but will all fly to the destination airport. -- * If you fix departure and destination airports, aircraft will only travel from between those airports. --- When the aircraft reaches its destination, it will be respawned at its departure and fly again to its destination. --- +-- When the aircraft reaches its destination, it will be respawned at its departure and fly again to its destination. +-- -- There is also an option that allows aircraft to "continue their journey" from their destination. This is achieved by the @{#RAT.ContinueJourney}() function. -- In that case, when the aircraft arrives at its first destination it will be respawned at that very airport and get a new random destination. -- So the flight plan in this case would be: Fly from airport A to B, then from B to C, then from C to D, ... --- +-- -- It is also possible to make aircraft "commute" between two airports, i.e. flying from airport A to B and then back from B to A, etc. -- This can be done by the @{#RAT.Commute}() function. Note that if no departure or destination airports are specified, the first departure and destination are chosen randomly. -- Then the aircraft will fly back and forth between those two airports indefinetly. --- --- +-- +-- -- ### Spawn in Air --- +-- -- ![Process](..\Presentations\RAT\RAT_Examples_Spawn_in_Air.png) --- +-- -- Aircraft can also be spawned in air rather than at airports on the ground. This is done by setting @{#RAT.SetTakeoff}() to "air". --- +-- -- By default, aircraft are spawned randomly above airports of the map. --- +-- -- The @{#RAT.SetDeparture}() option can be used to specify zones, which have been defined in the mission editor as departure zones. -- Aircraft will then be spawned at a random point within the zone or zones. --- +-- -- Note that @{#RAT.SetDeparture}() also accepts airport names. For an air takeoff these are treated like zones with a radius of XX kilometers. -- Again, aircraft are spawned at random points within these zones around the airport. --- +-- -- ### Misc Options --- +-- -- ![Process](..\Presentations\RAT\RAT_Examples_Misc.png) --- +-- -- The default "takeoff" type of RAT aircraft is that they are spawned with hot or cold engines. -- The choice is random, so 50% of aircraft will be spawned with hot engines while the other 50% will be spawned with cold engines. -- This setting can be changed using the @{#RAT.SetTakeoff}() function. The possible parameters for starting on ground are: --- +-- -- * @{#RAT.SetTakeoff}("cold"), which means that all aircraft are spawned with their engines off, -- * @{#RAT.SetTakeoff}("hot"), which means that all aircraft are spawned with their engines on, -- * @{#RAT.SetTakeoff}("runway"), which means that all aircraft are spawned already at the runway ready to takeoff. -- Note that in this case the default spawn intervall is set to 180 seconds in order to avoid aircraft jamms on the runway. Generally, this takeoff at runways should be used with care and problems are to be expected. --- --- +-- +-- -- The options @{#RAT.SetMinDistance}() and @{#RAT.SetMaxDistance}() can be used to restrict the range from departure to destination. For example --- +-- -- * @{#RAT.SetMinDistance}(100) will cause only random destination airports to be selected which are **at least** 100 km away from the departure airport. -- * @{#RAT.SetMaxDistance}(150) will allow only destination airports which are **less than** 150 km away from the departure airport. --- +-- -- ![Process](..\Presentations\RAT\RAT_Gaussian.png) --- +-- -- By default planes get a cruise altitude of ~20,000 ft ASL. The actual altitude is sampled from a Gaussian distribution. The picture shows this distribution -- if one would spawn 1000 planes. As can be seen most planes get a cruising alt of around FL200. Other values are possible but less likely the further away -- one gets from the expectation value. --- +-- -- The expectation value, i.e. the altitude most aircraft get, can be set with the function @{#RAT.SetFLcruise}(). -- It is possible to restrict the minimum cruise altitude by @{#RAT.SetFLmin}() and the maximum cruise altitude by @{#RAT.SetFLmax}() --- +-- -- The cruise altitude can also be given in meters ASL by the functions @{#RAT.SetCruiseAltitude}(), @{#RAT.SetMinCruiseAltitude}() and @{#RAT.SetMaxCruiseAltitude}(). --- +-- -- For example: --- +-- -- * @{#RAT.SetFLcruise}(300) will cause most planes fly around FL300. -- * @{#RAT.SetFLmin}(100) restricts the cruising alt such that no plane will fly below FL100. Note that this automatically changes the minimum distance from departure to destination. --- That means that only destinations are possible for which the aircraft has had enought time to reach that flight level and descent again. +-- That means that only destinations are possible for which the aircraft has had enought time to reach that flight level and descent again. -- * @{#RAT.SetFLmax}(200) will restrict the cruise alt to maximum FL200, i.e. no aircraft will travel above this height. --- --- +-- +-- -- @field #RAT RAT={ ClassName = "RAT", -- Name of class: RAT = Random Air Traffic. @@ -391,7 +391,7 @@ RAT={ homebase=nil, -- Home base for commute. continuejourney=false, -- Aircraft will continue their journey, i.e. get respawned at their destination with a new random destination. alive=0, -- Number of groups which are alive. - ngroups=nil, -- Number of groups to be spawned in total. + ngroups=nil, -- Number of groups to be spawned in total. f10menu=false, -- Add an F10 menu for RAT. Menu={}, -- F10 menu items for this RAT object. SubMenuName=nil, -- Submenu name for RAT object. @@ -414,11 +414,11 @@ RAT={ uncontrolled=false, -- Spawn uncontrolled aircraft. invisible=false, -- Spawn aircraft as invisible. immortal=false, -- Spawn aircraft as indestructible. - activate_uncontrolled=false, -- Activate uncontrolled aircraft (randomly). + activate_uncontrolled=false, -- Activate uncontrolled aircraft (randomly). activate_delay=5, -- Delay in seconds before first uncontrolled group is activated. activate_delta=5, -- Time interval in seconds between activation of uncontrolled groups. activate_frand=0, -- Randomization factor of time interval (activate_delta) between activating uncontrolled groups. - activate_max=1, -- Max number of uncontrolle aircraft, which will be activated at a time. + activate_max=1, -- Max number of uncontrolle aircraft, which will be activated at a time. onboardnum=nil, -- Tail number. onboardnum0=1, -- (Optional) Starting value of the automatically appended numbering of aircraft within a flight. Default is one. checkonrunway=true, -- Check whether aircraft have been spawned on the runway. @@ -436,7 +436,7 @@ RAT={ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Categories of the RAT class. +--- Categories of the RAT class. -- @list cat -- @field #string plane Plane. -- @field #string heli Heli. @@ -452,7 +452,7 @@ RAT.wp={ air=1, runway=2, hot=3, - cold=4, + cold=4, climb=5, cruise=6, descent=7, @@ -597,7 +597,7 @@ RAT.version={ --- Create a new RAT object. -- @param #RAT self -- @param #string groupname Name of the group as defined in the mission editor. This group is serving as a template for all spawned units. --- @param #string alias (Optional) Alias of the group. This is and optional parameter but must(!) be used if the same template group is used for more than one RAT object. +-- @param #string alias (Optional) Alias of the group. This is and optional parameter but must(!) be used if the same template group is used for more than one RAT object. -- @return #RAT Object of RAT class or nil if the group does not exist in the mission editor. -- @usage yak1:RAT("RAT_YAK") will create a RAT object called "yak1". The template group in the mission editor must have the name "RAT_YAK". -- @usage yak2:RAT("RAT_YAK", "Yak2") will create a RAT object "yak2". The template group in the mission editor must have the name "RAT_YAK" but the group will be called "Yak2" in e.g. the F10 menu. @@ -615,22 +615,22 @@ function RAT:New(groupname, alias) -- Welcome message. self:F(RAT.id..string.format("Creating new RAT object from template: %s.", groupname)) - + -- Set alias. alias=alias or groupname - + -- Alias of groupname. self.alias=alias - + -- Get template group defined in the mission editor. local DCSgroup=Group.getByName(groupname) - + -- Check the group actually exists. if DCSgroup==nil then self:E(RAT.id..string.format("ERROR: Group with name %s does not exist in the mission editor!", groupname)) return nil end - + -- Store template group. self.templategroup=GROUP:FindByName(groupname) @@ -639,13 +639,13 @@ function RAT:New(groupname, alias) -- Set own coalition. self.coalition=DCSgroup:getCoalition() - + -- Initialize aircraft parameters based on ME group template. self:_InitAircraft(DCSgroup) - + -- Get all airports of current map (Caucasus, NTTR, Normandy, ...). self:_GetAirportsOfMap() - + return self end @@ -670,46 +670,46 @@ function RAT:Spawn(naircraft) -- Number of aircraft to spawn. Default is one. self.ngroups=naircraft or 1 - + -- Init RAT ATC if not already done. if self.ATCswitch and not RAT.ATC.init then self:_ATCInit(self.airports_map) end - + -- Create F10 main menu if it does not exists yet. if self.f10menu and not RAT.MenuF10 then RAT.MenuF10 = MENU_MISSION:New("RAT") end - + -- Set the coalition table based on choice of self.coalition and self.friendly. self:_SetCoalitionTable() - + -- Get all airports of this map beloning to friendly coalition(s). self:_GetAirportsOfCoalition() - + -- Set submenuname if it has not been set by user. if not self.SubMenuName then self.SubMenuName=self.alias end - -- Get all departure airports inside a Moose zone. + -- Get all departure airports inside a Moose zone. if self.departure_Azone~=nil then self.departure_ports=self:_GetAirportsInZone(self.departure_Azone) end - - -- Get all destination airports inside a Moose zone. + + -- Get all destination airports inside a Moose zone. if self.destination_Azone~=nil then self.destination_ports=self:_GetAirportsInZone(self.destination_Azone) end - + -- Add all friendly airports to possible departures/destinations if self.addfriendlydepartures then self:_AddFriendlyAirports(self.departure_ports) end if self.addfriendlydestinations then self:_AddFriendlyAirports(self.destination_ports) - end - + end + -- Setting and possibly correction min/max/cruise flight levels. if self.FLcruise==nil then -- Default flight level (ASL). @@ -722,11 +722,11 @@ function RAT:Spawn(naircraft) end end - -- Enable helos to go to destinations 100 meters away. + -- Enable helos to go to destinations 100 meters away. if self.category==RAT.cat.heli then self.mindist=50 - end - + end + -- Run consistency checks. self:_CheckConsistency() @@ -759,7 +759,7 @@ function RAT:Spawn(naircraft) text=text..string.format("Return Zone: %s\n", tostring(self.returnzone)) text=text..string.format("Spawn delay: %4.1f\n", self.spawndelay) text=text..string.format("Spawn interval: %4.1f\n", self.spawninterval) - text=text..string.format("Respawn delay: %s\n", tostring(self.respawn_delay)) + text=text..string.format("Respawn delay: %s\n", tostring(self.respawn_delay)) text=text..string.format("Respawn off: %s\n", tostring(self.norespawn)) text=text..string.format("Respawn after landing: %s\n", tostring(self.respawn_at_landing)) text=text..string.format("Respawn after take-off: %s\n", tostring(self.respawn_after_takeoff)) @@ -805,7 +805,7 @@ function RAT:Spawn(naircraft) end text=text..string.format("******************************************************\n") self:T(RAT.id..text) - + -- Create submenus. if self.f10menu then self.Menu[self.SubMenuName]=MENU_MISSION:New(self.SubMenuName, RAT.MenuF10) @@ -814,7 +814,7 @@ function RAT:Spawn(naircraft) MENU_MISSION_COMMAND:New("Delete markers", self.Menu[self.SubMenuName], self._DeleteMarkers, self) MENU_MISSION_COMMAND:New("Status report", self.Menu[self.SubMenuName], self.Status, self, true) end - + -- Schedule spawning of aircraft. local Tstart=self.spawndelay local dt=self.spawninterval @@ -823,10 +823,10 @@ function RAT:Spawn(naircraft) dt=math.max(dt, 180) end local Tstop=Tstart+dt*(self.ngroups-1) - + -- Status check and report scheduler. SCHEDULER:New(nil, self.Status, {self}, Tstart+1, self.statusinterval) - + -- Handle events. self:HandleEvent(EVENTS.Birth, self._OnBirth) self:HandleEvent(EVENTS.EngineStartup, self._OnEngineStartup) @@ -841,15 +841,15 @@ function RAT:Spawn(naircraft) if self.ngroups==0 then return nil end - + -- Start scheduled spawning. SCHEDULER:New(nil, self._SpawnWithRoute, {self}, Tstart, dt, 0.0, Tstop) - + -- Start scheduled activation of uncontrolled groups. if self.uncontrolled and self.activate_uncontrolled then SCHEDULER:New(nil, self._ActivateUncontrolled, {self}, self.activate_delay, self.activate_delta, self.activate_frand) end - + return true end @@ -864,7 +864,7 @@ function RAT:_CheckConsistency() -- User has used SetDeparture() if not self.random_departure then - + -- Count departure airports and zones. for _,name in pairs(self.departure_ports) do if self:_AirportExists(name) then @@ -873,7 +873,7 @@ function RAT:_CheckConsistency() self.Ndeparture_Zones=self.Ndeparture_Zones+1 end end - + -- What can go wrong? -- Only zones but not takeoff air == > Enable takeoff air. if self.Ndeparture_Zones>0 and self.takeoff~=RAT.wp.air then @@ -891,7 +891,7 @@ function RAT:_CheckConsistency() -- User has used SetDestination() if not self.random_destination then - + -- Count destination airports and zones. for _,name in pairs(self.destination_ports) do if self:_AirportExists(name) then @@ -899,10 +899,10 @@ function RAT:_CheckConsistency() elseif self:_ZoneExists(name) then self.Ndestination_Zones=self.Ndestination_Zones+1 end - end - + end + -- One zone specified as destination ==> Enable destination zone. - -- This does not apply to return zone because the destination is the zone and not the final destination which can be an airport. + -- This does not apply to return zone because the destination is the zone and not the final destination which can be an airport. if self.Ndestination_Zones>0 and self.landing~=RAT.wp.air and not self.returnzone then self.landing=RAT.wp.air self.destinationzone=true @@ -915,19 +915,19 @@ function RAT:_CheckConsistency() self:E(RAT.id.."ERROR: "..text) MESSAGE:New(text, 30):ToAll() end - end - + end + -- Destination zone and return zone should not be used together. if self.destinationzone and self.returnzone then self:E(RAT.id.."ERROR: Destination zone _and_ return to zone not possible! Disabling return to zone.") self.returnzone=false end - -- If returning to a zone, we set the landing type to "air" if takeoff is in air. + -- If returning to a zone, we set the landing type to "air" if takeoff is in air. -- Because if we start in air we want to end in air. But default landing is ground. if self.returnzone and self.takeoff==RAT.wp.air then self.landing=RAT.wp.air end - + -- Ensure that neither FLmin nor FLmax are above the aircrafts service ceiling. if self.FLminuser then self.FLminuser=math.min(self.FLminuser, self.aircraft.ceiling) @@ -938,17 +938,17 @@ function RAT:_CheckConsistency() if self.FLcruise then self.FLcruise=math.min(self.FLcruise, self.aircraft.ceiling) end - + -- FL min > FL max case ==> spaw values if self.FLminuser and self.FLmaxuser then if self.FLminuser > self.FLmaxuser then local min=self.FLminuser local max=self.FLmaxuser self.FLminuser=max - self.FLmaxuser=min + self.FLmaxuser=min end end - + -- Cruise alt < FL min if self.FLminuser and self.FLcruise FL max if self.FLmaxuser and self.FLcruise>self.FLmaxuser then self.FLcruise=self.FLmaxuser end - + -- Uncontrolled aircraft must start with engines off. if self.uncontrolled then -- SOLVED: Strangly, it does not work with RAT.wp.cold only with RAT.wp.hot! @@ -995,11 +995,11 @@ end --- Set coalition of RAT group. You can make red templates blue and vice versa. -- Note that a country is also set automatically if it has not done before via RAT:SetCountry. --- +-- -- * For blue, the country is set to USA. -- * For red, the country is set to RUSSIA. -- * For neutral, the country is set to SWITZERLAND. --- +-- -- This is important, since it is ultimately the COUNTRY that determines the coalition of the aircraft. -- You can set the country explicitly via the RAT:SetCountry() function if necessary. -- @param #RAT self @@ -1021,14 +1021,14 @@ function RAT:SetCoalitionAircraft(color) self.coalition=coalition.side.NEUTRAL if not self.country then self.country=country.id.SWITZERLAND - end + end end return self end --- Set country of RAT group. -- See [DCS_enum_country](https://wiki.hoggitworld.com/view/DCS_enum_country). --- +-- -- This overrules the coalition settings. So if you want your group to be of a specific coalition, you have to set a country that is part of that coalition. -- @param #RAT self -- @param DCS#country.id id DCS country enumerator ID. For example country.id.USA or country.id.RUSSIA. @@ -1045,7 +1045,7 @@ end -- @param #RAT self -- @param Wrapper.Airbase#AIRBASE.TerminalType termtype Type of terminal. Use enumerator AIRBASE.TerminalType.XXX. -- @return #RAT RAT self object. --- +-- -- @usage -- c17=RAT:New("C-17 BIG Plane") -- c17:SetTerminalType(AIRBASE.TerminalType.OpenBig) -- Only very big parking spots are used. @@ -1111,35 +1111,35 @@ function RAT:SetDespawnAirOFF() end --- Set takeoff type. Starting cold at airport, starting hot at airport, starting at runway, starting in the air. --- Default is "takeoff-coldorhot". So there is a 50% chance that the aircraft starts with cold engines and 50% that it starts with hot engines. +-- Default is "takeoff-coldorhot". So there is a 50% chance that the aircraft starts with cold engines and 50% that it starts with hot engines. -- @param #RAT self -- @param #string type Type can be "takeoff-cold" or "cold", "takeoff-hot" or "hot", "takeoff-runway" or "runway", "air". -- @return #RAT RAT self object. -- @usage RAT:Takeoff("hot") will spawn RAT objects at airports with engines started. -- @usage RAT:Takeoff("cold") will spawn RAT objects at airports with engines off. --- @usage RAT:Takeoff("air") will spawn RAT objects in air over random airports or within pre-defined zones. +-- @usage RAT:Takeoff("air") will spawn RAT objects in air over random airports or within pre-defined zones. function RAT:SetTakeoff(type) self:F2(type) - + local _Type if type:lower()=="takeoff-cold" or type:lower()=="cold" then _Type=RAT.wp.cold elseif type:lower()=="takeoff-hot" or type:lower()=="hot" then _Type=RAT.wp.hot - elseif type:lower()=="takeoff-runway" or type:lower()=="runway" then + elseif type:lower()=="takeoff-runway" or type:lower()=="runway" then _Type=RAT.wp.runway elseif type:lower()=="air" then _Type=RAT.wp.air else _Type=RAT.wp.coldorhot end - + self.takeoff=_Type - + return self end ---- Set takeoff type cold. Aircraft will spawn at a parking spot with engines off. +--- Set takeoff type cold. Aircraft will spawn at a parking spot with engines off. -- @param #RAT self -- @return #RAT RAT self object. function RAT:SetTakeoffCold() @@ -1147,7 +1147,7 @@ function RAT:SetTakeoffCold() return self end ---- Set takeoff type to hot. Aircraft will spawn at a parking spot with engines on. +--- Set takeoff type to hot. Aircraft will spawn at a parking spot with engines on. -- @param #RAT self -- @return #RAT RAT self object. function RAT:SetTakeoffHot() @@ -1155,7 +1155,7 @@ function RAT:SetTakeoffHot() return self end ---- Set takeoff type to runway. Aircraft will spawn directly on the runway. +--- Set takeoff type to runway. Aircraft will spawn directly on the runway. -- @param #RAT self -- @return #RAT RAT self object. function RAT:SetTakeoffRunway() @@ -1163,7 +1163,7 @@ function RAT:SetTakeoffRunway() return self end ---- Set takeoff type to cold or hot. Aircraft will spawn at a parking spot with 50:50 change of engines on or off. +--- Set takeoff type to cold or hot. Aircraft will spawn at a parking spot with 50:50 change of engines on or off. -- @param #RAT self -- @return #RAT RAT self object. function RAT:SetTakeoffColdOrHot() @@ -1171,7 +1171,7 @@ function RAT:SetTakeoffColdOrHot() return self end ---- Set takeoff type to air. Aircraft will spawn in the air. +--- Set takeoff type to air. Aircraft will spawn in the air. -- @param #RAT self -- @return #RAT RAT self object. function RAT:SetTakeoffAir() @@ -1191,21 +1191,21 @@ function RAT:SetDeparture(departurenames) -- Random departure is deactivated now that user specified departure ports. self.random_departure=false - + -- Convert input to table. local names if type(departurenames)=="table" then - names=departurenames + names=departurenames elseif type(departurenames)=="string" then names={departurenames} else -- error message self:E(RAT.id.."ERROR: Input parameter must be a string or a table in SetDeparture()!") end - + -- Put names into arrays. for _,name in pairs(names) do - + if self:_AirportExists(name) then -- If an airport with this name exists, we put it in the ports array. table.insert(self.departure_ports, name) @@ -1215,9 +1215,9 @@ function RAT:SetDeparture(departurenames) else self:E(RAT.id.."ERROR: No departure airport or zone found with name "..name) end - + end - + return self end @@ -1231,7 +1231,7 @@ function RAT:SetDestination(destinationnames) -- Random departure is deactivated now that user specified departure ports. self.random_destination=false - + -- Convert input to table local names if type(destinationnames)=="table" then @@ -1242,10 +1242,10 @@ function RAT:SetDestination(destinationnames) -- Error message. self:E(RAT.id.."ERROR: Input parameter must be a string or a table in SetDestination()!") end - + -- Put names into arrays. for _,name in pairs(names) do - + if self:_AirportExists(name) then -- If an airport with this name exists, we put it in the ports array. table.insert(self.destination_ports, name) @@ -1255,7 +1255,7 @@ function RAT:SetDestination(destinationnames) else self:E(RAT.id.."ERROR: No destination airport or zone found with name "..name) end - + end return self @@ -1266,13 +1266,13 @@ end -- @return #RAT RAT self object. function RAT:DestinationZone() self:F2() - + -- Destination is a zone. Needs special care. self.destinationzone=true - + -- Landing type is "air" because we don't actually land at the airport. self.landing=RAT.wp.air - + return self end @@ -1296,10 +1296,10 @@ function RAT:SetDestinationsFromZone(zone) -- Random departure is deactivated now that user specified departure ports. self.random_destination=false - + -- Set zone. self.destination_Azone=zone - + return self end @@ -1309,13 +1309,13 @@ end -- @return #RAT RAT self object. function RAT:SetDeparturesFromZone(zone) self:F2(zone) - + -- Random departure is deactivated now that user specified departure ports. self.random_departure=false -- Set zone. self.departure_Azone=zone - + return self end @@ -1351,7 +1351,7 @@ function RAT:ExcludedAirports(ports) return self end ---- Set skill of AI aircraft. Default is "High". +--- Set skill of AI aircraft. Default is "High". -- @param #RAT self -- @param #string skill Skill, options are "Average", "Good", "High", "Excellent" and "Random". Parameter is case insensitive. -- @return #RAT RAT self object. @@ -1422,7 +1422,7 @@ function RAT:Commute(starshape) return self end ---- Set the delay before first group is spawned. +--- Set the delay before first group is spawned. -- @param #RAT self -- @param #number delay Delay in seconds. Default is 5 seconds. Minimum delay is 0.5 seconds. -- @return #RAT RAT self object. @@ -1537,7 +1537,7 @@ end --- Check if aircraft have accidentally been spawned on the runway. If so they will be removed immediatly. -- @param #RAT self -- @param #boolean switch If true, check is performed. If false, this check is omitted. --- @param #number radius Distance in meters until a unit is considered to have spawned accidentally on the runway. Default is 75 m. +-- @param #number radius Distance in meters until a unit is considered to have spawned accidentally on the runway. Default is 75 m. -- @return #RAT RAT self object. function RAT:CheckOnRunway(switch, distance) self:F2(switch) @@ -1617,7 +1617,7 @@ function RAT:RadioModulation(modulation) return self end ---- Radio menu On. Default is off. +--- Radio menu On. Default is off. -- @param #RAT self -- @return #RAT RAT self object. function RAT:RadioMenuON() @@ -1626,7 +1626,7 @@ function RAT:RadioMenuON() return self end ---- Radio menu Off. This is the default setting. +--- Radio menu Off. This is the default setting. -- @param #RAT self -- @return #RAT RAT self object. function RAT:RadioMenuOFF() @@ -1635,7 +1635,7 @@ function RAT:RadioMenuOFF() return self end ---- Aircraft are invisible. +--- Aircraft are invisible. -- @param #RAT self -- @return #RAT RAT self object. function RAT:Invisible() @@ -1644,7 +1644,7 @@ function RAT:Invisible() return self end ---- Turn EPLRS datalink on/off. +--- Turn EPLRS datalink on/off. -- @param #RAT self -- @param #boolean switch If true (or nil), turn EPLRS on. -- @return #RAT RAT self object. @@ -1657,7 +1657,7 @@ function RAT:SetEPLRS(switch) return self end ---- Aircraft are immortal. +--- Aircraft are immortal. -- @param #RAT self -- @return #RAT RAT self object. function RAT:Immortal() @@ -1675,7 +1675,7 @@ function RAT:Uncontrolled() return self end ---- Activate uncontrolled aircraft. +--- Activate uncontrolled aircraft. -- @param #RAT self -- @param #number maxactivated Maximal numnber of activated aircraft. Absolute maximum will be the number of spawned groups. Default is 1. -- @param #number delay Time delay in seconds before (first) aircraft is activated. Default is 1 second. @@ -1690,21 +1690,21 @@ function RAT:ActivateUncontrolled(maxactivated, delay, delta, frand) self.activate_delay=delay or 1 self.activate_delta=delta or 1 self.activate_frand=frand or 0 - + -- Ensure min delay is one second. self.activate_delay=math.max(self.activate_delay,1) - + -- Ensure min delta is one second. self.activate_delta=math.max(self.activate_delta,0) - + -- Ensure frand is in [0,...,1] self.activate_frand=math.max(self.activate_frand,0) self.activate_frand=math.min(self.activate_frand,1) - + return self end ---- Set the time after which inactive groups will be destroyed. +--- Set the time after which inactive groups will be destroyed. -- @param #RAT self -- @param #number time Time in seconds. Default is 600 seconds = 10 minutes. Minimum is 60 seconds. -- @return #RAT RAT self object. @@ -1759,7 +1759,7 @@ end -- @return #RAT RAT self object. function RAT:SetROE(roe) self:F2(roe) - if roe=="return" then + if roe=="return" then self.roe=RAT.ROE.returnfire elseif roe=="free" then self.roe=RAT.ROE.weaponfree @@ -1811,7 +1811,7 @@ end --- Turn messages from ATC on or off. Default is on. This setting effects all RAT objects and groups! -- @param #RAT self -- @param #boolean switch Enable (true) or disable (false) messages from ATC. --- @return #RAT RAT self object. +-- @return #RAT RAT self object. function RAT:ATC_Messages(switch) self:F2(switch) if switch==nil then @@ -1821,7 +1821,7 @@ function RAT:ATC_Messages(switch) return self end ---- Max number of planes that get landing clearance of the RAT ATC. This setting effects all RAT objects and groups! +--- Max number of planes that get landing clearance of the RAT ATC. This setting effects all RAT objects and groups! -- @param #RAT self -- @param #number n Number of aircraft that are allowed to land simultaniously. Default is 2. -- @return #RAT RAT self object. @@ -1831,7 +1831,7 @@ function RAT:ATC_Clearance(n) return self end ---- Delay between granting landing clearance for simultanious landings. This setting effects all RAT objects and groups! +--- Delay between granting landing clearance for simultanious landings. This setting effects all RAT objects and groups! -- @param #RAT self -- @param #number time Delay time when the next aircraft will get landing clearance event if the previous one did not land yet. Default is 240 sec. -- @return #RAT RAT self object. @@ -1987,7 +1987,7 @@ end --- Set onboard number prefix. Same as setting "TAIL #" in the mission editor. Note that if you dont use this function, the values defined in the template group of the ME are taken. -- @param #RAT self --- @param #string tailnumprefix String of the tail number prefix. If flight consists of more than one aircraft, two digits are appended automatically, i.e. 001, 002, ... +-- @param #string tailnumprefix String of the tail number prefix. If flight consists of more than one aircraft, two digits are appended automatically, i.e. 001, 002, ... -- @param #number zero (Optional) Starting value of the automatically appended numbering of aircraft within a flight. Default is 0. -- @return #RAT RAT self object. function RAT:SetOnboardNum(tailnumprefix, zero) @@ -2013,7 +2013,7 @@ function RAT:_InitAircraft(DCSgroup) local DCSdesc=DCSunit:getDesc() local DCScategory=DCSgroup:getCategory() local DCStype=DCSunit:getTypeName() - + -- set category if DCScategory==Group.Category.AIRPLANE then self.category=RAT.cat.plane @@ -2023,37 +2023,43 @@ function RAT:_InitAircraft(DCSgroup) self.category="other" self:E(RAT.id.."ERROR: Group of RAT is neither airplane nor helicopter!") end - + -- Get type of aircraft. self.aircraft.type=DCStype - + -- inital fuel in % self.aircraft.fuel=DCSunit:getFuel() -- operational range in NM converted to m self.aircraft.Rmax = DCSdesc.range*RAT.unit.nm2m - + -- effective range taking fuel into accound and a 5% reserve self.aircraft.Reff = self.aircraft.Rmax*self.aircraft.fuel*0.95 - + -- max airspeed from group self.aircraft.Vmax = DCSdesc.speedMax - + -- max climb speed in m/s self.aircraft.Vymax=DCSdesc.VyMax - + -- service ceiling in meters self.aircraft.ceiling=DCSdesc.Hmax - + -- Store all descriptors. --self.aircraft.descriptors=DCSdesc - + -- aircraft dimensions - self.aircraft.length=DCSdesc.box.max.x - self.aircraft.height=DCSdesc.box.max.y - self.aircraft.width=DCSdesc.box.max.z + if DCSdesc.box then + self.aircraft.length=DCSdesc.box.max.x + self.aircraft.height=DCSdesc.box.max.y + self.aircraft.width=DCSdesc.box.max.z + elseif DCStype == "Mirage-F1CE" then + self.aircraft.length=16 + self.aircraft.height=5 + self.aircraft.width=9 + end self.aircraft.box=math.max(self.aircraft.length,self.aircraft.width) - + -- info message local text=string.format("\n******************************************************\n") text=text..string.format("Aircraft parameters:\n") @@ -2099,7 +2105,7 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live -- Set takeoff type. local takeoff=self.takeoff local landing=self.landing - + -- Overrule takeoff/landing by what comes in. if _takeoff then takeoff=_takeoff @@ -2107,27 +2113,27 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live if _landing then landing=_landing end - + -- Random choice between cold and hot. if takeoff==RAT.wp.coldorhot then local temp={RAT.wp.cold, RAT.wp.hot} takeoff=temp[math.random(2)] end - + -- Number of respawn attempts after spawning on runway. local nrespawn=0 if _nrespawn then nrespawn=_nrespawn end - + -- Set flight plan. local departure, destination, waypoints, WPholding, WPfinal = self:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) - + -- Return nil if we could not find a departure destination or waypoints if not (departure and destination and waypoints) then return nil end - + -- Set (another) livery. local livery if _livery then @@ -2141,20 +2147,20 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live else livery=nil end - + -- Modify the spawn template to follow the flight plan. local successful=self:_ModifySpawnTemplate(waypoints, livery, _lastpos, departure, takeoff, parkingdata) if not successful then return nil end - + -- Actually spawn the group. local group=self:SpawnWithIndex(self.SpawnIndex) -- Wrapper.Group#GROUP - + -- Increase counter of alive groups (also uncontrolled ones). self.alive=self.alive+1 self:T(RAT.id..string.format("Alive groups counter now = %d.",self.alive)) - + -- ATC is monitoring this flight (if it is supposed to land). if self.ATCswitch and landing==RAT.wp.landing then if self.returnzone then @@ -2163,34 +2169,34 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live self:_ATCAddFlight(group:GetName(), destination:GetName()) end end - + -- Place markers of waypoints on F10 map. if self.placemarkers then self:_PlaceMarkers(waypoints, self.SpawnIndex) end - + -- Set group to be invisible. if self.invisible then self:_CommandInvisible(group, true) end - + -- Set group to be immortal. if self.immortal then self:_CommandImmortal(group, true) end - + -- Set group to be immortal. if self.eplrs then group:CommandEPLRS(true, 1) - end - + end + -- Set ROE, default is "weapon hold". self:_SetROE(group, self.roe) - + -- Set ROT, default is "no reaction". self:_SetROT(group, self.rot) - -- Init ratcraft array. + -- Init ratcraft array. self.ratcraft[self.SpawnIndex]={} self.ratcraft[self.SpawnIndex]["group"]=group self.ratcraft[self.SpawnIndex]["destination"]=destination @@ -2218,28 +2224,28 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live self.ratcraft[self.SpawnIndex]["P0"]=group:GetCoordinate() self.ratcraft[self.SpawnIndex]["Pnow"]=group:GetCoordinate() self.ratcraft[self.SpawnIndex]["Distance"]=0 - + -- Each aircraft gets its own takeoff type. self.ratcraft[self.SpawnIndex].takeoff=takeoff self.ratcraft[self.SpawnIndex].landing=landing self.ratcraft[self.SpawnIndex].wpholding=WPholding self.ratcraft[self.SpawnIndex].wpfinal=WPfinal - + -- Aircraft is active or spawned in uncontrolled state. self.ratcraft[self.SpawnIndex].active=not self.uncontrolled - + -- Set status to spawned. This will be overwritten in birth event. self.ratcraft[self.SpawnIndex]["status"]=RAT.status.Spawned - + -- Livery self.ratcraft[self.SpawnIndex].livery=livery - + -- If this switch is set to true, the aircraft will be despawned the next time the status function is called. self.ratcraft[self.SpawnIndex].despawnme=false - + -- Number of preformed spawn attempts for this group. self.ratcraft[self.SpawnIndex].nrespawn=nrespawn - + -- Create submenu for this group. if self.f10menu then local name=self.aircraft.type.." ID "..tostring(self.SpawnIndex) @@ -2254,13 +2260,13 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"]=MENU_MISSION:New("Set ROT", self.Menu[self.SubMenuName].groups[self.SpawnIndex]) MENU_MISSION_COMMAND:New("No reaction", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"], self._SetROT, self, group, RAT.ROT.noreaction) MENU_MISSION_COMMAND:New("Passive defense", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"], self._SetROT, self, group, RAT.ROT.passive) - MENU_MISSION_COMMAND:New("Evade on fire", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"], self._SetROT, self, group, RAT.ROT.evade) + MENU_MISSION_COMMAND:New("Evade on fire", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"], self._SetROT, self, group, RAT.ROT.evade) -- F10/RAT//Group X/ MENU_MISSION_COMMAND:New("Despawn group", self.Menu[self.SubMenuName].groups[self.SpawnIndex], self._Despawn, self, group) MENU_MISSION_COMMAND:New("Place markers", self.Menu[self.SubMenuName].groups[self.SpawnIndex], self._PlaceMarkers, self, waypoints, self.SpawnIndex) MENU_MISSION_COMMAND:New("Status report", self.Menu[self.SubMenuName].groups[self.SpawnIndex], self.Status, self, true, self.SpawnIndex) end - + return self.SpawnIndex end @@ -2280,10 +2286,10 @@ end -- @param Core.Point#COORDINATE lastpos Last known position of the group. -- @param #number delay Delay before respawn function RAT:_Respawn(index, lastpos, delay) - + -- Get the spawn index from group --local index=self:GetSpawnIndexFromGroup(group) - + -- Get departure and destination from previous journey. local departure=self.ratcraft[index].departure local destination=self.ratcraft[index].destination @@ -2292,7 +2298,7 @@ function RAT:_Respawn(index, lastpos, delay) local livery=self.ratcraft[index].livery local lastwp=self.ratcraft[index].waypoints[#self.ratcraft[index].waypoints] --local lastpos=group:GetCoordinate() - + local _departure=nil local _destination=nil local _takeoff=nil @@ -2300,15 +2306,15 @@ function RAT:_Respawn(index, lastpos, delay) local _livery=nil local _lastwp=nil local _lastpos=nil - + if self.continuejourney then - + -- We continue our journey from the old departure airport. _departure=destination:GetName() - + -- Use the same livery for next aircraft. _livery=livery - + -- Last known position of the aircraft, which should be the sparking spot location. -- Note: we have to check that it was supposed to land and not respawned directly after landing or after takeoff. -- TODO: Need to think if continuejourney with respawn_after_takeoff actually makes sense. @@ -2318,15 +2324,15 @@ function RAT:_Respawn(index, lastpos, delay) _lastpos=lastpos end end - + if self.destinationzone then - + -- Case: X --> Zone --> Zone --> Zone _takeoff=RAT.wp.air _landing=RAT.wp.air - + elseif self.returnzone then - + -- Case: X --> Zone --> X, X --> Zone --> X -- We flew to a zone and back. Takeoff type does not change. _takeoff=self.takeoff @@ -2337,22 +2343,22 @@ function RAT:_Respawn(index, lastpos, delay) else _landing=RAT.wp.landing end - + -- Departure stays the same. (The destination is the zone here.) _departure=departure:GetName() - + else - + -- Default case. Takeoff and landing type does not change. _takeoff=self.takeoff _landing=self.landing - + end - + elseif self.commute then - + -- We commute between departure and destination. - + if self.starshape==true then if destination:GetName()==self.homebase then -- We are at our home base ==> destination is again randomly selected. @@ -2368,10 +2374,10 @@ function RAT:_Respawn(index, lastpos, delay) _departure=destination:GetName() _destination=departure:GetName() end - + -- Use the same livery for next aircraft. _livery=livery - + -- Last known position of the aircraft, which should be the sparking spot location. -- Note: we have to check that it was supposed to land and not respawned directly after landing or after takeoff. -- TODO: Need to think if commute with respawn_after_takeoff actually makes sense. @@ -2379,22 +2385,22 @@ function RAT:_Respawn(index, lastpos, delay) -- Check that we have landed on an airport or FARP but not a ship (which would be categroy 1). if destination:GetCategory()==4 then _lastpos=lastpos - end + end end - + -- Handle takeoff type. if self.destinationzone then -- self.takeoff is either RAT.wp.air or RAT.wp.cold -- self.landing is RAT.wp.Air - + if self.takeoff==RAT.wp.air then - + -- Case: Zone <--> Zone (both have takeoff air) _takeoff=RAT.wp.air -- = self.takeoff (because we just checked) _landing=RAT.wp.air -- = self.landing (because destinationzone) - + else - + -- Case: Airport <--> Zone if takeoff==RAT.wp.air then -- Last takeoff was air so we are at the airport now, takeoff is from ground. @@ -2405,31 +2411,31 @@ function RAT:_Respawn(index, lastpos, delay) _takeoff=RAT.wp.air _landing=RAT.wp.landing end - + end - + elseif self.returnzone then - + -- We flew to a zone and back. No need to swap departure and destination. _departure=departure:GetName() _destination=destination:GetName() - + -- Takeoff and landing should also not change. _takeoff=self.takeoff _landing=self.landing - - end - + + end + end - + -- Take the last waypoint as initial waypoint for next plane. if _takeoff==RAT.wp.air and (self.continuejourney or self.commute) then _lastwp=lastwp end - + -- Debug self:T2({departure=_departure, destination=_destination, takeoff=_takeoff, landing=_landing, livery=_livery, lastwp=_lastwp}) - + -- We should give it at least 3 sec since this seems to be the time until free parking spots after despawn are available again (Sirri Island test). local respawndelay if delay then @@ -2439,7 +2445,7 @@ function RAT:_Respawn(index, lastpos, delay) else respawndelay=3 end - + -- Spawn new group. local arg={} arg.self=self @@ -2452,7 +2458,7 @@ function RAT:_Respawn(index, lastpos, delay) arg.lastpos=_lastpos self:T(RAT.id..string.format("%s delayed respawn in %.1f seconds.", self.alias, respawndelay)) SCHEDULER:New(nil, self._SpawnWithRouteTimer, {arg}, respawndelay) - + end --- Delayed spawn function called by scheduler. @@ -2476,7 +2482,7 @@ end -- @return #table Table of flight plan waypoints. -- @return #nil If no valid departure or destination airport could be found. function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) - + -- Max cruise speed. local VxCruiseMax if self.Vcruisemax then @@ -2486,39 +2492,39 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) -- Max cruise speed 90% of Vmax or 900 km/h whichever is lower. VxCruiseMax = math.min(self.aircraft.Vmax*0.90, 250) end - + -- Min cruise speed 70% of max cruise or 600 km/h whichever is lower. local VxCruiseMin = math.min(VxCruiseMax*0.70, 166) - + -- Cruise speed (randomized). Expectation value at midpoint between min and max. local VxCruise = UTILS.RandomGaussian((VxCruiseMax-VxCruiseMin)/2+VxCruiseMin, (VxCruiseMax-VxCruiseMax)/4, VxCruiseMin, VxCruiseMax) - + -- Climb speed 90% ov Vmax but max 720 km/h. local VxClimb = math.min(self.aircraft.Vmax*0.90, 200) - + -- Descent speed 60% of Vmax but max 500 km/h. local VxDescent = math.min(self.aircraft.Vmax*0.60, 140) - + -- Holding speed is 90% of descent speed. local VxHolding = VxDescent*0.9 - + -- Final leg is 90% of holding speed. local VxFinal = VxHolding*0.9 - + -- Reasonably civil climb speed Vy=1500 ft/min = 7.6 m/s but max aircraft specific climb rate. local VyClimb=math.min(self.Vclimb*RAT.unit.ft2meter/60, self.aircraft.Vymax) - + -- Climb angle in rad. local AlphaClimb=math.asin(VyClimb/VxClimb) - + -- Descent angle in rad. local AlphaDescent=math.rad(self.AlphaDescent) - + -- Expected cruise level (peak of Gaussian distribution) local FLcruise_expect=self.FLcruise - - - -- DEPARTURE AIRPORT + + + -- DEPARTURE AIRPORT -- Departure airport or zone. local departure=nil if _departure then @@ -2535,15 +2541,15 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) else local text=string.format("ERROR! Specified departure airport %s does not exist for %s.", _departure, self.alias) self:E(RAT.id..text) - end - + end + else departure=self:_PickDeparture(takeoff) if self.commute and self.starshape==true and self.homebase==nil then self.homebase=departure:GetName() end end - + -- Return nil if no departure could be found. if not departure then local text=string.format("ERROR! No valid departure airport could be found for %s.", self.alias) @@ -2561,11 +2567,11 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) -- For an air start, we take a random point within the spawn zone. local vec2=departure:GetRandomVec2() Pdeparture=COORDINATE:NewFromVec2(vec2) - end + end else Pdeparture=departure:GetCoordinate() end - + -- Height ASL of departure point. local H_departure if takeoff==RAT.wp.air then @@ -2576,7 +2582,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) else Hmin=50 end - -- Departure altitude is 70% of default cruise with 30% variation and limited to 1000 m AGL (50 m for helos). + -- Departure altitude is 70% of default cruise with 30% variation and limited to 1000 m AGL (50 m for helos). H_departure=self:_Randomize(FLcruise_expect*0.7, 0.3, Pdeparture.y+Hmin, FLcruise_expect) if self.FLminuser then H_departure=math.max(H_departure,self.FLminuser) @@ -2588,18 +2594,18 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) else H_departure=Pdeparture.y end - + -- Adjust min distance between departure and destination for user set min flight level. local mindist=self.mindist if self.FLminuser then - + -- We can conly consider the symmetric case, because no destination selected yet. local hclimb=self.FLminuser-H_departure local hdescent=self.FLminuser-H_departure - + -- Minimum distance for l local Dclimb, Ddescent, Dtot=self:_MinDistance(AlphaClimb, AlphaDescent, hclimb, hdescent) - + if takeoff==RAT.wp.air and landing==RAT.wpair then mindist=0 -- Takeoff and landing are in air. No mindist required. elseif takeoff==RAT.wp.air then @@ -2609,32 +2615,32 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) else mindist=Dtot -- Takeoff and landing on ground. Need both space to climb and descent. end - + -- Mindist is at least self.mindist. mindist=math.max(self.mindist, mindist) - + local text=string.format("Adjusting min distance to %d km (for given min FL%03d)", mindist/1000, self.FLminuser/RAT.unit.FL2m) self:T(RAT.id..text) end - + -- DESTINATION AIRPORT local destination=nil if _destination then - + if self:_AirportExists(_destination) then - + destination=AIRBASE:FindByName(_destination) if landing==RAT.wp.air or self.returnzone then destination=destination:GetZone() end - + elseif self:_ZoneExists(_destination) then destination=ZONE:New(_destination) else local text=string.format("ERROR: Specified destination airport/zone %s does not exist for %s!", _destination, self.alias) self:E(RAT.id.."ERROR: "..text) end - + else -- This handles the case where we have a journey and the first flight is done, i.e. _departure is set. @@ -2644,7 +2650,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) if self.continuejourney and _departure and #self.destination_ports<3 then random=true end - + -- In case of a returnzone the destination (i.e. return point) is always a zone. local mylanding=landing local acrange=self.aircraft.Reff @@ -2652,11 +2658,11 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) mylanding=RAT.wp.air acrange=self.aircraft.Reff/2 -- Aircraft needs to go to zone and back home. end - + -- Pick a destination airport. destination=self:_PickDestination(departure, Pdeparture, mindist, math.min(acrange, self.maxdist), random, mylanding) end - + -- Return nil if no departure could be found. if not destination then local text=string.format("No valid destination airport could be found for %s!", self.alias) @@ -2664,14 +2670,14 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) self:E(RAT.id.."ERROR: "..text) return nil end - + -- Check that departure and destination are not the same. Should not happen due to mindist. if destination:GetName()==departure:GetName() then local text=string.format("%s: Destination and departure are identical. Airport/zone %s.", self.alias, destination:GetName()) MESSAGE:New(text, 30):ToAll() self:E(RAT.id.."ERROR: "..text) end - + -- Get a random point inside zone return zone. local Preturn local destination_returnzone @@ -2684,7 +2690,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) -- Set departure to destination. destination=departure end - + -- Get destination coordinate. Either in a zone or exactly at the airport. local Pdestination if landing==RAT.wp.air then @@ -2693,10 +2699,10 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) else Pdestination=destination:GetCoordinate() end - + -- Height ASL of destination airport/zone. local H_destination=Pdestination.y - + -- DESCENT/HOLDING POINT -- Get a random point between 5 and 10 km away from the destination. local Rhmin=8000 @@ -2706,14 +2712,14 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) Rhmin=500 Rhmax=1000 end - + -- Coordinates of the holding point. y is the land height at that point. local Vholding=Pdestination:GetRandomVec2InRadius(Rhmax, Rhmin) local Pholding=COORDINATE:NewFromVec2(Vholding) - + -- AGL height of holding point. local H_holding=Pholding.y - + -- Holding point altitude. For planes between 1600 and 2400 m AGL. For helos 160 to 240 m AGL. local h_holding if self.category==RAT.cat.plane then @@ -2722,38 +2728,38 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) h_holding=150 end h_holding=self:_Randomize(h_holding, 0.2) - + -- This is the actual height ASL of the holding point we want to fly to local Hh_holding=H_holding+h_holding - + -- When we dont land, we set the holding altitude to the departure or cruise alt. -- This is used in the calculations. if landing==RAT.wp.air then Hh_holding=H_departure end - + -- Distance from holding point to final destination. local d_holding=Pholding:Get2DDistance(Pdestination) - + -- GENERAL local heading local d_total if self.returnzone then - + -- Heading from departure to destination in return zone. heading=self:_Course(Pdeparture, Preturn) - + -- Total distance to return zone and back. d_total=Pdeparture:Get2DDistance(Preturn) + Preturn:Get2DDistance(Pholding) - + else -- Heading from departure to holding point of destination. heading=self:_Course(Pdeparture, Pholding) - + -- Total distance between departure and holding point near destination. d_total=Pdeparture:Get2DDistance(Pholding) end - + -- Max height in case of air start, i.e. if we only would descent to holding point for the given distance. if takeoff==RAT.wp.air then local H_departure_max @@ -2764,15 +2770,15 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) end H_departure=math.min(H_departure, H_departure_max) end - + -------------------------------------------- - + -- Height difference between departure and destination. local deltaH=math.abs(H_departure-Hh_holding) - + -- Slope between departure and destination. local phi = math.atan(deltaH/d_total) - + -- Adjusted climb/descent angles. local phi_climb local phi_descent @@ -2791,18 +2797,18 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) else D_total = math.sqrt(deltaH*deltaH+d_total*d_total) end - + -- SSA triangle for sloped case. local gamma=math.rad(180)-phi_climb-phi_descent local a = D_total*math.sin(phi_climb)/math.sin(gamma) local b = D_total*math.sin(phi_descent)/math.sin(gamma) local hphi_max = b*math.sin(phi_climb) local hphi_max2 = a*math.sin(phi_descent) - + -- Height of triangle. local h_max1 = b*math.sin(AlphaClimb) local h_max2 = a*math.sin(AlphaDescent) - + -- Max height relative to departure or destination. local h_max if (H_departure > Hh_holding) then @@ -2810,23 +2816,23 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) else h_max=math.max(h_max1, h_max2) end - + -- Max flight level aircraft can reach for given angles and distance. local FLmax = h_max+H_departure - - --CRUISE + + --CRUISE -- Min cruise alt is just above holding point at destination or departure height, whatever is larger. local FLmin=math.max(H_departure, Hh_holding) - + -- For helicopters we take cruise alt between 50 to 1000 meters above ground. Default cruise alt is ~150 m. - if self.category==RAT.cat.heli then + if self.category==RAT.cat.heli then FLmin=math.max(H_departure, H_destination)+50 FLmax=math.max(H_departure, H_destination)+1000 end - + -- Ensure that FLmax not above its service ceiling. FLmax=math.min(FLmax, self.aircraft.ceiling) - + -- Overrule setting if user specified min/max flight level explicitly. if self.FLminuser then FLmin=math.max(self.FLminuser, FLmin) -- Still take care that we dont fly too high. @@ -2834,12 +2840,12 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) if self.FLmaxuser then FLmax=math.min(self.FLmaxuser, FLmax) -- Still take care that we dont fly too low. end - + -- If the route is very short we set FLmin a bit lower than FLmax. if FLmin>FLmax then FLmin=FLmax end - + -- Expected cruise altitude - peak of gaussian distribution. if FLcruise_expectFLmax then FLcruise_expect=FLmax end - + -- Set cruise altitude. Selected from Gaussian distribution but limited to FLmin and FLmax. local FLcruise=UTILS.RandomGaussian(FLcruise_expect, math.abs(FLmax-FLmin)/4, FLmin, FLmax) - + -- Overrule setting if user specified a flight level explicitly. if self.FLuser then FLcruise=self.FLuser @@ -2862,12 +2868,12 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) -- Climb and descent heights. local h_climb = FLcruise - H_departure local h_descent = FLcruise - Hh_holding - + -- Distances. local d_climb = h_climb/math.tan(AlphaClimb) local d_descent = h_descent/math.tan(AlphaDescent) - local d_cruise = d_total-d_climb-d_descent - + local d_cruise = d_total-d_climb-d_descent + -- debug message local text=string.format("\n******************************************************\n") text=text..string.format("Template = %s\n", self.SpawnTemplatePrefix) @@ -2899,7 +2905,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) text=text..string.format("FLmin = %6.1f m ASL = FL%03d\n", FLmin, FLmin/RAT.unit.FL2m) text=text..string.format("FLcruise = %6.1f m ASL = FL%03d\n", FLcruise, FLcruise/RAT.unit.FL2m) text=text..string.format("FLmax = %6.1f m ASL = FL%03d\n", FLmax, FLmax/RAT.unit.FL2m) - text=text..string.format("\nAngles:\n") + text=text..string.format("\nAngles:\n") text=text..string.format("Alpha climb = %6.2f Deg\n", math.deg(AlphaClimb)) text=text..string.format("Alpha descent = %6.2f Deg\n", math.deg(AlphaDescent)) text=text..string.format("Phi (slope) = %6.2f Deg\n", math.deg(phi)) @@ -2909,7 +2915,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) -- Max heights and distances if we would travel at FLmax. local h_climb_max = FLmax - H_departure local h_descent_max = FLmax - Hh_holding - local d_climb_max = h_climb_max/math.tan(AlphaClimb) + local d_climb_max = h_climb_max/math.tan(AlphaClimb) local d_descent_max = h_descent_max/math.tan(AlphaDescent) local d_cruise_max = d_total-d_climb_max-d_descent_max text=text..string.format("Heading = %6.1f Deg\n", heading) @@ -2932,7 +2938,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) end text=text..string.format("******************************************************\n") self:T2(RAT.id..text) - + -- Ensure that cruise distance is positve. Can be slightly negative in special cases. And we don't want to turn back. if d_cruise<0 then d_cruise=100 @@ -2943,80 +2949,80 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) local c={} local wpholding=nil local wpfinal=nil - + -- Departure/Take-off c[#c+1]=Pdeparture wp[#wp+1]=self:_Waypoint(#wp+1, "Departure", takeoff, c[#wp+1], VxClimb, H_departure, departure) self.waypointdescriptions[#wp]="Departure" self.waypointstatus[#wp]=RAT.status.Departure - + -- Climb if takeoff==RAT.wp.air then - + -- Air start. if d_climb < 5000 or d_cruise < 5000 then -- We omit the climb phase completely and add it to the cruise part. d_cruise=d_cruise+d_climb - else + else -- Only one waypoint at the end of climb = begin of cruise. c[#c+1]=c[#c]:Translate(d_climb, heading) - + wp[#wp+1]=self:_Waypoint(#wp+1, "Begin of Cruise", RAT.wp.cruise, c[#wp+1], VxCruise, FLcruise) self.waypointdescriptions[#wp]="Begin of Cruise" self.waypointstatus[#wp]=RAT.status.Cruise end - + else - + -- Ground start. c[#c+1]=c[#c]:Translate(d_climb/2, heading) c[#c+1]=c[#c]:Translate(d_climb/2, heading) - + wp[#wp+1]=self:_Waypoint(#wp+1, "Climb", RAT.wp.climb, c[#wp+1], VxClimb, H_departure+(FLcruise-H_departure)/2) self.waypointdescriptions[#wp]="Climb" self.waypointstatus[#wp]=RAT.status.Climb - + wp[#wp+1]=self:_Waypoint(#wp+1, "Begin of Cruise", RAT.wp.cruise, c[#wp+1], VxCruise, FLcruise) self.waypointdescriptions[#wp]="Begin of Cruise" self.waypointstatus[#wp]=RAT.status.Cruise - + end - + -- Cruise - + -- First add the little bit from begin of cruise to the return point. - if self.returnzone then - c[#c+1]=Preturn + if self.returnzone then + c[#c+1]=Preturn wp[#wp+1]=self:_Waypoint(#wp+1, "Return Zone", RAT.wp.cruise, c[#wp+1], VxCruise, FLcruise) self.waypointdescriptions[#wp]="Return Zone" self.waypointstatus[#wp]=RAT.status.Uturn end - + if landing==RAT.wp.air then - + -- Next waypoint is already the final destination. c[#c+1]=Pdestination wp[#wp+1]=self:_Waypoint(#wp+1, "Final Destination", RAT.wp.finalwp, c[#wp+1], VxCruise, FLcruise) self.waypointdescriptions[#wp]="Final Destination" self.waypointstatus[#wp]=RAT.status.Destination - + elseif self.returnzone then - - -- The little bit back to end of cruise. - c[#c+1]=c[#c]:Translate(d_cruise/2, heading-180) + + -- The little bit back to end of cruise. + c[#c+1]=c[#c]:Translate(d_cruise/2, heading-180) wp[#wp+1]=self:_Waypoint(#wp+1, "End of Cruise", RAT.wp.cruise, c[#wp+1], VxCruise, FLcruise) self.waypointdescriptions[#wp]="End of Cruise" self.waypointstatus[#wp]=RAT.status.Descent - + else - + c[#c+1]=c[#c]:Translate(d_cruise, heading) wp[#wp+1]=self:_Waypoint(#wp+1, "End of Cruise", RAT.wp.cruise, c[#wp+1], VxCruise, FLcruise) self.waypointdescriptions[#wp]="End of Cruise" self.waypointstatus[#wp]=RAT.status.Descent - + end - + -- Descent (only if we acually want to land) if landing==RAT.wp.landing then if self.returnzone then @@ -3031,45 +3037,45 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) self.waypointstatus[#wp]=RAT.status.DescentHolding end end - + -- Holding and final destination. if landing==RAT.wp.landing then -- Holding point - c[#c+1]=Pholding + c[#c+1]=Pholding wp[#wp+1]=self:_Waypoint(#wp+1, "Holding Point", RAT.wp.holding, c[#wp+1], VxHolding, H_holding+h_holding) self.waypointdescriptions[#wp]="Holding Point" self.waypointstatus[#wp]=RAT.status.Holding wpholding=#wp -- Final destination. - c[#c+1]=Pdestination + c[#c+1]=Pdestination wp[#wp+1]=self:_Waypoint(#wp+1, "Final Destination", landing, c[#wp+1], VxFinal, H_destination, destination) self.waypointdescriptions[#wp]="Final Destination" self.waypointstatus[#wp]=RAT.status.Destination - + end - + -- Final Waypoint wpfinal=#wp - + -- Fill table with waypoints. local waypoints={} for _,p in ipairs(wp) do table.insert(waypoints, p) end - + -- Some info on the route. self:_Routeinfo(waypoints, "Waypoint info in set_route:") - + -- Return departure, destination and waypoints. if self.returnzone then -- We return the actual zone here because returning the departure leads to problems with commute. - return departure, destination_returnzone, waypoints, wpholding, wpfinal + return departure, destination_returnzone, waypoints, wpholding, wpfinal else return departure, destination, waypoints, wpholding, wpfinal end - + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -3084,41 +3090,41 @@ function RAT:_PickDeparture(takeoff) -- Array of possible departure airports or zones. local departures={} - + if self.random_departure then - + -- Airports of friendly coalitions. for _,_airport in pairs(self.airports) do - + local airport=_airport --Wrapper.Airbase#AIRBASE - + local name=airport:GetName() if not self:_Excluded(name) then if takeoff==RAT.wp.air then - + table.insert(departures, airport:GetZone()) -- insert zone object. - + else - + -- Check if airbase has the right terminals. local nspots=1 if self.termtype~=nil then nspots=airport:GetParkingSpotsNumber(self.termtype) end - + if nspots>0 then table.insert(departures, airport) -- insert airport object. end end end - + end - + else - + -- Destination airports or zones specified by user. for _,name in pairs(self.departure_ports) do - + local dep=nil if self:_AirportExists(name) then if takeoff==RAT.wp.air then @@ -3143,22 +3149,22 @@ function RAT:_PickDeparture(takeoff) else self:E(RAT.id..string.format("ERROR: No airport or zone found with name %s.", name)) end - + -- Add to departures table. if dep then table.insert(departures, dep) end - - end - + + end + end - + -- Info message. self:T(RAT.id..string.format("Number of possible departures for %s= %d", self.alias, #departures)) - + -- Select departure airport or zone. local departure=departures[math.random(#departures)] - + local text if departure and departure:GetName() then if takeoff==RAT.wp.air then @@ -3172,7 +3178,7 @@ function RAT:_PickDeparture(takeoff) self:E(RAT.id..string.format("ERROR! No departure airport or zone found for %s.", self.alias)) departure=nil end - + return departure end @@ -3193,18 +3199,18 @@ function RAT:_PickDestination(departure, q, minrange, maxrange, random, landing) -- All possible destinations. local destinations={} - + if random then - + -- Airports of friendly coalitions. for _,_airport in pairs(self.airports) do local airport=_airport --Wrapper.Airbase#AIRBASE local name=airport:GetName() if self:_IsFriendly(name) and not self:_Excluded(name) and name~=departure:GetName() then - + -- Distance from departure to possible destination local distance=q:Get2DDistance(airport:GetCoordinate()) - + -- Check if distance form departure to destination is within min/max range. if distance>=minrange and distance<=maxrange then if landing==RAT.wp.air then @@ -3222,12 +3228,12 @@ function RAT:_PickDestination(departure, q, minrange, maxrange, random, landing) end end end - + else - + -- Destination airports or zones specified by user. for _,name in pairs(self.destination_ports) do - + -- Make sure departure and destination are not identical. if name ~= departure:GetName() then @@ -3255,11 +3261,11 @@ function RAT:_PickDestination(departure, q, minrange, maxrange, random, landing) else self:E(RAT.id..string.format("ERROR! No airport or zone found with name %s", name)) end - + if dest then -- Distance from departure to possible destination local distance=q:Get2DDistance(dest:GetCoordinate()) - + -- Add as possible destination if zone is within range. if distance>=minrange and distance<=maxrange then table.insert(destinations, dest) @@ -3268,14 +3274,14 @@ function RAT:_PickDestination(departure, q, minrange, maxrange, random, landing) self:T(RAT.id..text) end end - + end - end + end end - + -- Info message. self:T(RAT.id..string.format("Number of possible destinations = %s.", #destinations)) - + if #destinations > 0 then --- Compare distance of destination airports. -- @param Core.Point#COORDINATE a Coordinate of point a. @@ -3290,15 +3296,15 @@ function RAT:_PickDestination(departure, q, minrange, maxrange, random, landing) else destinations=nil end - - + + -- Randomly select one possible destination. local destination if destinations and #destinations>0 then - + -- Random selection. destination=destinations[math.random(#destinations)] -- Wrapper.Airbase#AIRBASE - + -- Debug message. local text if landing==RAT.wp.air then @@ -3308,15 +3314,15 @@ function RAT:_PickDestination(departure, q, minrange, maxrange, random, landing) end self:T(RAT.id..text) --MESSAGE:New(text, 30):ToAllIf(self.Debug) - + else self:E(RAT.id.."ERROR! No destination airport or zone found.") destination=nil end - + -- Return the chosen destination. - return destination - + return destination + end --- Find airports within a zone. @@ -3328,7 +3334,7 @@ function RAT:_GetAirportsInZone(zone) for _,airport in pairs(self.airports) do local name=airport:GetName() local coord=airport:GetCoordinate() - + if zone:IsPointVec3InZone(coord) then table.insert(airports, name) end @@ -3369,9 +3375,9 @@ end -- @param #RAT self function RAT:_GetAirportsOfMap() local _coalition - + for i=0,2 do -- cycle coalition.side 0=NEUTRAL, 1=RED, 2=BLUE - + -- set coalition if i==0 then _coalition=coalition.side.NEUTRAL @@ -3380,33 +3386,33 @@ function RAT:_GetAirportsOfMap() elseif i==2 then _coalition=coalition.side.BLUE end - + -- get airbases of coalition local ab=coalition.getAirbases(i) - + -- loop over airbases and put them in a table for _,airbase in pairs(ab) do - + local _id=airbase:getID() local _p=airbase:getPosition().p local _name=airbase:getName() local _myab=AIRBASE:FindByName(_name) - + if _myab then - + -- Add airport to table. table.insert(self.airports_map, _myab) - + local text="MOOSE: Airport ID = ".._myab:GetID().." and Name = ".._myab:GetName()..", Category = ".._myab:GetCategory()..", TypeName = ".._myab:GetTypeName() self:T(RAT.id..text) - + else - + self:E(RAT.id..string.format("WARNING: Airbase %s does not exsist as MOOSE object!", tostring(_name))) - + end end - + end end @@ -3424,7 +3430,7 @@ function RAT:_GetAirportsOfCoalition() -- Planes cannot land on ships. --local condition2=self.category==RAT.cat.plane and airport:GetCategory()==1 local condition2=self.category==RAT.cat.plane and category==Airbase.Category.SHIP - + -- Check that airport has the requested terminal types. -- NOT good here because we would also not allow any airport zones! --[[ @@ -3434,14 +3440,14 @@ function RAT:_GetAirportsOfCoalition() end local condition3 = nspots==0 ]] - + if not (condition1 or condition2) then table.insert(self.airports, airport) end end end end - + if #self.airports==0 then local text=string.format("No possible departure/destination airports found for RAT %s.", tostring(self.alias)) MESSAGE:New(text, 10):ToAll() @@ -3460,26 +3466,26 @@ function RAT:Status(message, forID) -- Optional arguments. if message==nil then message=false - end + end if forID==nil then forID=false end - + -- Current time. local Tnow=timer.getTime() - + -- Alive counter. local nalive=0 - + -- Loop over all ratcraft. for spawnindex,ratcraft in ipairs(self.ratcraft) do - + -- Get group. local group=ratcraft.group --Wrapper.Group#GROUP - + if group and group:IsAlive() then nalive=nalive+1 - + -- Gather some information. local prefix=self:_GetPrefixFromGroup(group) local life=self:_GetLife(group) @@ -3495,7 +3501,7 @@ function RAT:Status(message, forID) local active=ratcraft.active local Nunits=ratcraft.nunits -- group:GetSize() local N0units=group:GetInitialSize() - + -- Monitor time and distance on ground. local Tg=0 local Dg=0 @@ -3512,51 +3518,51 @@ function RAT:Status(message, forID) if ratcraft["Tground"] then -- Aircraft was already on ground. Calculate total time on ground. Tg=Tnow-ratcraft["Tground"] - + -- Distance on ground since last check. Dg=coords:Get2DDistance(ratcraft["Pground"]) - + -- Time interval since last check. dTlast=Tnow-ratcraft["Tlastcheck"] - + -- If more than Tinactive seconds passed since last check ==> check how much we moved meanwhile. if dTlast > self.Tinactive then - + --[[ if Dg<50 and active and status~=RAT.status.EventBirth then stationary=true end ]] - + -- Loop over all units. for _,_unit in pairs(group:GetUnits()) do - + if _unit and _unit:IsAlive() then - + -- Unit name, coord and distance since last check. local unitname=_unit:GetName() local unitcoord=_unit:GetCoordinate() local Ug=unitcoord:Get2DDistance(ratcraft.Uground[unitname]) - + -- Debug info self:T2(RAT.id..string.format("Unit %s travelled distance on ground %.1f m since %d seconds.", unitname, Ug, dTlast)) - + -- If aircraft did not move more than 50 m since last check, we call it stationary and despawn it. - -- Aircraft which are spawned uncontrolled or starting their engines are not counted. + -- Aircraft which are spawned uncontrolled or starting their engines are not counted. if Ug<50 and active and status~=RAT.status.EventBirth then stationary=true end - + -- Update coords. ratcraft["Uground"][unitname]=unitcoord end end - + -- Set the current time to know when the next check is necessary. ratcraft["Tlastcheck"]=Tnow - ratcraft["Pground"]=coords + ratcraft["Pground"]=coords end - + else -- First time we see that the aircraft is on ground. Initialize the times and position. ratcraft["Tground"]=Tnow @@ -3569,18 +3575,18 @@ function RAT:Status(message, forID) end end end - + -- Monitor travelled distance since last check. local Pn=coords local Dtravel=Pn:Get2DDistance(ratcraft["Pnow"]) ratcraft["Pnow"]=Pn - + -- Add up the travelled distance. ratcraft["Distance"]=ratcraft["Distance"]+Dtravel - + -- Distance remaining to destination. local Ddestination=Pn:Get2DDistance(ratcraft.destination:GetCoordinate()) - + -- Status report. if (forID and spawnindex==forID) or (not forID) then local text=string.format("ID %i of flight %s", spawnindex, prefix) @@ -3617,44 +3623,44 @@ function RAT:Status(message, forID) MESSAGE:New(text, 20):ToAll() end end - + -- Despawn groups if they are on ground and don't move or are damaged. if not airborne then - + -- Despawn unit if it did not move more then 50 m in the last 180 seconds. if stationary then local text=string.format("Group %s is despawned after being %d seconds inaktive on ground.", self.alias, dTlast) self:T(RAT.id..text) self:_Despawn(group) end - + -- Despawn group if life is < 10% and distance travelled < 100 m. if life<10 and Dtravel<100 then local text=string.format("Damaged group %s is despawned. Life = %3.0f", self.alias, life) self:T(RAT.id..text) self:_Despawn(group) end - + end - + -- Despawn groups after they have reached their destination zones. if ratcraft.despawnme then - + local text=string.format("Flight %s will be despawned NOW!", self.alias) self:T(RAT.id..text) - + -- Respawn group if (not self.norespawn) and (not self.respawn_after_takeoff) then local idx=self:GetSpawnIndexFromGroup(group) - local coord=group:GetCoordinate() + local coord=group:GetCoordinate() self:_Respawn(idx, coord, 0) end - + -- Despawn old group. if self.despawnair then self:_Despawn(group, 0) end - + end else @@ -3662,14 +3668,14 @@ function RAT:Status(message, forID) local text=string.format("Group does not exist in loop ratcraft status.") self:T2(RAT.id..text) end - + end - + -- Alive groups. local text=string.format("Alive groups of %s: %d, nalive=%d/%d", self.alias, self.alive, nalive, self.ngroups) self:T(RAT.id..text) MESSAGE:New(text, 20):ToAllIf(message and not forID) - + end --- Get (relative) life of first unit of a group. @@ -3701,26 +3707,26 @@ function RAT:_SetStatus(group, status) -- Get index from groupname. local index=self:GetSpawnIndexFromGroup(group) - + if self.ratcraft[index] then - + -- Set new status. self.ratcraft[index].status=status - + -- No status update message for "first waypoint", "holding" local no1 = status==RAT.status.Departure local no2 = status==RAT.status.EventBirthAir local no3 = status==RAT.status.Holding - + local text=string.format("Flight %s: %s.", group:GetName(), status) self:T(RAT.id..text) - + if not (no1 or no2 or no3) then MESSAGE:New(text, 10):ToAllIf(self.reportstatus) end - + end - + end end @@ -3734,16 +3740,16 @@ function RAT:GetStatus(group) -- Get index from groupname. local index=self:GetSpawnIndexFromGroup(group) - + if self.ratcraft[index] then - + -- Set new status. return self.ratcraft[index].status - + end - + end - + return "nonexistant" end @@ -3758,20 +3764,20 @@ function RAT:_OnBirth(EventData) self:T3(RAT.id.."Captured event birth!") local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP - + if SpawnGroup then - + -- Get the template name of the group. This can be nil if this was not a spawned group. local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) - + if EventPrefix then - + -- Check that the template name actually belongs to this object. if EventPrefix == self.alias then - + local text="Event: Group "..SpawnGroup:GetName().." was born." self:T(RAT.id..text) - + -- Set status. local status="unknown in birth" if SpawnGroup:InAir() then @@ -3782,7 +3788,7 @@ function RAT:_OnBirth(EventData) status=RAT.status.EventBirth end self:_SetStatus(SpawnGroup, status) - + -- Get some info ablout this flight. local i=self:GetSpawnIndexFromGroup(SpawnGroup) local _departure=self.ratcraft[i].departure:GetName() @@ -3791,10 +3797,10 @@ function RAT:_OnBirth(EventData) local _takeoff=self.ratcraft[i].takeoff local _landing=self.ratcraft[i].landing local _livery=self.ratcraft[i].livery - + -- Some is only useful for an actual airbase (not a zone). local _airbase=AIRBASE:FindByName(_departure) - + -- Check if aircraft group was accidentally spawned on the runway. -- This can happen due to no parking slots available and other DCS bugs. local onrunway=false @@ -3802,12 +3808,12 @@ function RAT:_OnBirth(EventData) -- Check that we did not want to spawn at a runway or in air. if self.checkonrunway and _takeoff ~= RAT.wp.runway and _takeoff ~= RAT.wp.air then onrunway=_airbase:CheckOnRunWay(SpawnGroup, self.onrunwayradius, false) - end + end end - + -- Workaround if group was spawned on runway. if onrunway then - + -- Error message. local text=string.format("ERROR: RAT group of %s was spawned on runway. Group #%d will be despawned immediately!", self.alias, i) MESSAGE:New(text,30):ToAllIf(self.Debug) @@ -3815,42 +3821,42 @@ function RAT:_OnBirth(EventData) if self.Debug then SpawnGroup:FlareRed() end - + -- Despawn the group. self:_Despawn(SpawnGroup) - + -- Try to respawn the group if there is at least another airport or random airport selection is used. if (self.Ndeparture_Airports>=2 or self.random_departure) and _nrespawn new state %s.", SpawnGroup:GetName(), currentstate, status) self:T(RAT.id..text) - + -- Respawn group. local idx=self:GetSpawnIndexFromGroup(SpawnGroup) local coord=SpawnGroup:GetCoordinate() @@ -4041,13 +4047,13 @@ function RAT:_OnEngineShutdown(EventData) -- Despawn group. text="Event: Group "..SpawnGroup:GetName().." will be destroyed now." self:T(RAT.id..text) - self:_Despawn(SpawnGroup) + self:_Despawn(SpawnGroup) end end end - + else self:T2(RAT.id.."ERROR: Group does not exist in RAT:_OnEngineShutdown().") end @@ -4059,19 +4065,19 @@ end function RAT:_OnHit(EventData) self:F3(EventData) self:T(RAT.id..string.format("Captured event Hit by %s! Initiator %s. Target %s", self.alias, tostring(EventData.IniUnitName), tostring(EventData.TgtUnitName))) - + local SpawnGroup = EventData.TgtGroup --Wrapper.Group#GROUP - + if SpawnGroup then - + -- Get the template name of the group. This can be nil if this was not a spawned group. local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) - + -- Check that the template name actually belongs to this object. if EventPrefix and EventPrefix == self.alias then -- Debug info. self:T(RAT.id..string.format("Event: Group %s was hit. Unit %s.", SpawnGroup:GetName(), tostring(EventData.TgtUnitName))) - + local text=string.format("%s, unit %s was hit!", self.alias, EventData.TgtUnitName) MESSAGE:New(text, 10):ToAllIf(self.reportstatus or self.Debug) end @@ -4084,37 +4090,37 @@ end function RAT:_OnDeadOrCrash(EventData) self:F3(EventData) self:T3(RAT.id.."Captured event DeadOrCrash!") - + local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP - + if SpawnGroup then - + -- Get the template name of the group. This can be nil if this was not a spawned group. local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) - + if EventPrefix then - + -- Check that the template name actually belongs to this object. if EventPrefix == self.alias then - + -- Decrease group alive counter. self.alive=self.alive-1 - + -- Debug info. - local text=string.format("Event: Group %s crashed or died. Alive counter = %d.", SpawnGroup:GetName(), self.alive) + local text=string.format("Event: Group %s crashed or died. Alive counter = %d.", SpawnGroup:GetName(), self.alive) self:T(RAT.id..text) - + -- Split crash and dead events. if EventData.id == world.event.S_EVENT_CRASH then - - -- Call crash event. This handles when a group crashed or + + -- Call crash event. This handles when a group crashed or self:_OnCrash(EventData) - + elseif EventData.id == world.event.S_EVENT_DEAD then - + -- Call dead event. self:_OnDead(EventData) - + end end end @@ -4127,26 +4133,26 @@ end function RAT:_OnDead(EventData) self:F3(EventData) self:T3(RAT.id.."Captured event Dead!") - + local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP - + if SpawnGroup then - + -- Get the template name of the group. This can be nil if this was not a spawned group. local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) - + if EventPrefix then - + -- Check that the template name actually belongs to this object. if EventPrefix == self.alias then - - local text=string.format("Event: Group %s died. Unit %s.", SpawnGroup:GetName(), EventData.IniUnitName) + + local text=string.format("Event: Group %s died. Unit %s.", SpawnGroup:GetName(), EventData.IniUnitName) self:T(RAT.id..text) - + -- Set status. local status=RAT.status.EventDead self:_SetStatus(SpawnGroup, status) - + end end @@ -4163,29 +4169,29 @@ function RAT:_OnCrash(EventData) self:T3(RAT.id.."Captured event Crash!") local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP - + if SpawnGroup then -- Get the template name of the group. This can be nil if this was not a spawned group. local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) - + -- Check that the template name actually belongs to this object. if EventPrefix and EventPrefix == self.alias then - + -- Update number of alive units in the group. local _i=self:GetSpawnIndexFromGroup(SpawnGroup) self.ratcraft[_i].nunits=self.ratcraft[_i].nunits-1 local _n=self.ratcraft[_i].nunits local _n0=SpawnGroup:GetInitialSize() - - -- Debug info. + + -- Debug info. local text=string.format("Event: Group %s crashed. Unit %s. Units still alive %d of %d.", SpawnGroup:GetName(), EventData.IniUnitName, _n, _n0) self:T(RAT.id..text) - + -- Set status. local status=RAT.status.EventCrash self:_SetStatus(SpawnGroup, status) - + -- Respawn group if all units are dead. if _n==0 and self.respawn_after_crash and not self.norespawn then local text=string.format("No units left of group %s. Group will be respawned now.", SpawnGroup:GetName()) @@ -4197,7 +4203,7 @@ function RAT:_OnCrash(EventData) end end - + else if self.Debug then self:E(RAT.id.."ERROR: Group does not exist in RAT:_OnCrash().") @@ -4213,16 +4219,16 @@ end function RAT:_Despawn(group, delay) if group ~= nil then - + -- Get spawnindex of group. local index=self:GetSpawnIndexFromGroup(group) - + if index ~= nil then - + self.ratcraft[index].group=nil self.ratcraft[index]["status"]="Dead" - - --TODO: Maybe here could be some more arrays deleted? + + --TODO: Maybe here could be some more arrays deleted? --TODO: Somehow this causes issues. --[[ --self.ratcraft[index]["group"]=group @@ -4248,8 +4254,8 @@ function RAT:_Despawn(group, delay) ]] -- Remove ratcraft table entry. --table.remove(self.ratcraft, index) - - + + -- We should give it at least 3 sec since this seems to be the time until free parking spots after despawn are available again (Sirri Island test). local despawndelay=0 if delay then @@ -4259,20 +4265,20 @@ function RAT:_Despawn(group, delay) -- Despawn afer respawn_delay. Actual respawn happens in +3 seconds to allow for free parking. despawndelay=self.respawn_delay end - - -- This will destroy the DCS group and create a single DEAD event. + + -- This will destroy the DCS group and create a single DEAD event. --if despawndelay>0.5 then self:T(RAT.id..string.format("%s delayed despawn in %.1f seconds.", self.alias, despawndelay)) SCHEDULER:New(nil, self._Destroy, {self, group}, despawndelay) --else --self:_Destroy(group) - --end + --end -- Remove submenu for this group. if self.f10menu and self.SubMenuName ~= nil then self.Menu[self.SubMenuName]["groups"][index]:Remove() end - + end end end @@ -4288,23 +4294,23 @@ function RAT:_Destroy(group) local DCSGroup = group:GetDCSObject() -- DCS#Group if DCSGroup and DCSGroup:isExist() then - + -- Cread one single Dead event and delete units from database. local triggerdead=true for _,DCSUnit in pairs(DCSGroup:getUnits()) do - + -- Dead event. if DCSUnit then if triggerdead then self:_CreateEventDead(timer.getTime(), DCSUnit) triggerdead=false end - + -- Delete from data base. _DATABASE:DeleteUnit(DCSUnit:getName()) end end - + -- Destroy DCS group. DCSGroup:destroy() DCSGroup = nil @@ -4345,10 +4351,10 @@ function RAT:_Waypoint(index, description, Type, Coord, Speed, Altitude, Airport -- Altitude of input parameter or y-component of 3D-coordinate. local _Altitude=Altitude or Coord.y - + -- Land height at given coordinate. local Hland=Coord:GetLandHeight() - + -- convert type and action in DCS format local _Type=nil local _Action=nil @@ -4361,7 +4367,7 @@ function RAT:_Waypoint(index, description, Type, Coord, Speed, Altitude, Airport _Altitude = 10 _alttype="RADIO" elseif Type==RAT.wp.hot then - -- take-off with engine on + -- take-off with engine on _Type="TakeOffParkingHot" _Action="From Parking Area Hot" _Altitude = 10 @@ -4434,7 +4440,7 @@ function RAT:_Waypoint(index, description, Type, Coord, Speed, Altitude, Airport end text=text.."******************************************************\n" self:T2(RAT.id..text) - + -- define waypoint local RoutePoint = {} -- coordinates and altitude @@ -4443,7 +4449,7 @@ function RAT:_Waypoint(index, description, Type, Coord, Speed, Altitude, Airport RoutePoint.alt = _Altitude -- altitude type: BARO=ASL or RADIO=AGL RoutePoint.alt_type = _alttype - -- type + -- type RoutePoint.type = _Type RoutePoint.action = _Action -- speed in m/s @@ -4454,7 +4460,7 @@ function RAT:_Waypoint(index, description, Type, Coord, Speed, Altitude, Airport RoutePoint.ETA_locked = false -- waypoint description RoutePoint.name=description - + if (Airport~=nil) and (Type~=RAT.wp.air) then local AirbaseID = Airport:GetID() local AirbaseCategory = Airport:GetAirbaseCategory() @@ -4468,7 +4474,7 @@ function RAT:_Waypoint(index, description, Type, Coord, Speed, Altitude, Airport RoutePoint.airdromeId = AirbaseID else self:T(RAT.id.."Unknown Airport category in _Waypoint()!") - end + end end -- properties RoutePoint.properties = { @@ -4482,16 +4488,16 @@ function RAT:_Waypoint(index, description, Type, Coord, Speed, Altitude, Airport local TaskCombo = {} local TaskHolding = self:_TaskHolding({x=Coord.x, y=Coord.z}, Altitude, Speed, self:_Randomize(90,0.9)) local TaskWaypoint = self:_TaskFunction("RAT._WaypointFunction", self, index) - + RoutePoint.task = {} RoutePoint.task.id = "ComboTask" RoutePoint.task.params = {} - + TaskCombo[#TaskCombo+1]=TaskWaypoint if Type==RAT.wp.holding then TaskCombo[#TaskCombo+1]=TaskHolding end - + RoutePoint.task.params.tasks = TaskCombo -- Return waypoint. @@ -4533,10 +4539,10 @@ function RAT:_Routeinfo(waypoints, comment) end text=text..string.format("Total distance = %6.1f km\n", total/1000) text=text..string.format("******************************************************\n") - + -- Debug info. self:T2(RAT.id..text) - + -- return total route length in meters return total end @@ -4562,7 +4568,7 @@ function RAT:_TaskHolding(P1, Altitude, Speed, Duration) dx=200 dy=0 end - + local P2={} P2.x=P1.x+dx P2.y=P1.y+dy @@ -4577,12 +4583,12 @@ function RAT:_TaskHolding(P1, Altitude, Speed, Duration) altitude = Altitude } } - + local DCSTask={} DCSTask.id="ControlledTask" DCSTask.params={} DCSTask.params.task=Task - + if self.ATCswitch then -- Set stop condition for holding. Either flag=1 or after max. X min holding. local userflagname=string.format("%s#%03d", self.alias, self.SpawnIndex+1) @@ -4591,7 +4597,7 @@ function RAT:_TaskHolding(P1, Altitude, Speed, Duration) else DCSTask.params.stopCondition={duration=Duration} end - + return DCSTask end @@ -4604,32 +4610,32 @@ function RAT._WaypointFunction(group, rat, wp) -- Current time and Spawnindex. local Tnow=timer.getTime() local sdx=rat:GetSpawnIndexFromGroup(group) - + -- Departure and destination names. local departure=rat.ratcraft[sdx].departure:GetName() local destination=rat.ratcraft[sdx].destination:GetName() local landing=rat.ratcraft[sdx].landing local WPholding=rat.ratcraft[sdx].wpholding local WPfinal=rat.ratcraft[sdx].wpfinal - - + + -- For messages local text - + -- Info on passing waypoint. text=string.format("Flight %s passing waypoint #%d %s.", group:GetName(), wp, rat.waypointdescriptions[wp]) BASE.T(rat, RAT.id..text) - + -- New status. local status=rat.waypointstatus[wp] rat:_SetStatus(group, status) - + if wp==WPholding then - + -- Aircraft arrived at holding point text=string.format("Flight %s to %s ATC: Holding and awaiting landing clearance.", group:GetName(), destination) MESSAGE:New(text, 10):ToAllIf(rat.reportstatus) - + -- Register aircraft at ATC. if rat.ATCswitch then if rat.f10menu then @@ -4638,12 +4644,12 @@ function RAT._WaypointFunction(group, rat, wp) rat._ATCRegisterFlight(rat, group:GetName(), Tnow) end end - + if wp==WPfinal then text=string.format("Flight %s arrived at final destination %s.", group:GetName(), destination) MESSAGE:New(text, 10):ToAllIf(rat.reportstatus) BASE.T(rat, RAT.id..text) - + if landing==RAT.wp.air then text=string.format("Activating despawn switch for flight %s! Group will be detroyed soon.", group:GetName()) MESSAGE:New(text, 10):ToAllIf(rat.Debug) @@ -4659,14 +4665,14 @@ end -- @param #string FunctionString Name of the function to be called. function RAT:_TaskFunction(FunctionString, ... ) self:F2({FunctionString, arg}) - + local DCSTask local ArgumentKey - + -- Templatename and anticipated name the group will get local templatename=self.templategroup:GetName() local groupname=self:_AnticipatedGroupName() - + local DCSScript = {} DCSScript[#DCSScript+1] = "local MissionControllable = GROUP:FindByName(\""..groupname.."\") " DCSScript[#DCSScript+1] = "local RATtemplateControllable = GROUP:FindByName(\""..templatename.."\") " @@ -4679,7 +4685,7 @@ function RAT:_TaskFunction(FunctionString, ... ) else DCSScript[#DCSScript+1] = FunctionString .. "( MissionControllable )" end - + DCSTask = self.templategroup:TaskWrappedAction(self.templategroup:CommandDoScript(table.concat(DCSScript))) return DCSTask @@ -4687,7 +4693,7 @@ end --- Anticipated group name from alias and spawn index. -- @param #RAT self --- @param #number index Spawnindex of group if given or self.SpawnIndex+1 by default. +-- @param #number index Spawnindex of group if given or self.SpawnIndex+1 by default. -- @return #string Name the group will get after it is spawned. function RAT:_AnticipatedGroupName(index) local index=index or self.SpawnIndex+1 @@ -4700,21 +4706,21 @@ end -- @param #RAT self function RAT:_ActivateUncontrolled() self:F() - - -- Spawn indices of uncontrolled inactive aircraft. + + -- Spawn indices of uncontrolled inactive aircraft. local idx={} local rat={} - + -- Number of active aircraft. local nactive=0 - + -- Loop over RAT groups and count the active ones. for spawnindex,ratcraft in pairs(self.ratcraft) do - + local group=ratcraft.group --Wrapper.Group#GROUP - + if group and group:IsAlive() then - + local text=string.format("Uncontrolled: Group = %s (spawnindex = %d), active = %s.", ratcraft.group:GetName(), spawnindex, tostring(ratcraft.active)) self:T2(RAT.id..text) @@ -4723,22 +4729,22 @@ function RAT:_ActivateUncontrolled() else table.insert(idx, spawnindex) end - + end end - + -- Debug message. local text=string.format("Uncontrolled: Ninactive = %d, Nactive = %d (of max %d).", #idx, nactive, self.activate_max) self:T(RAT.id..text) - + if #idx>0 and nactive Less effort. @@ -5231,7 +5237,7 @@ function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace, departure, take -- Terminal type specified explicitly. self:T(RAT.id..string.format("Helo group %s is at %s using terminal type %d.", self.alias, departure:GetName(), termtype)) spots=departure:FindFreeParkingSpotForAircraft(TemplateGroup, termtype, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits) - nfree=#spots + nfree=#spots end else -- Fixed wing aircraft is spawned. @@ -5248,15 +5254,15 @@ function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace, departure, take nfree=#spots if nfree=1 then - + -- All units get the same spot. DCS takes care of the rest. for i=1,nunits do table.insert(parkingspots, spots[1].Coordinate) @@ -5292,46 +5298,46 @@ function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace, departure, take end -- This is actually used... PointVec3=spots[1].Coordinate - + else -- If there is absolutely not spot ==> air start! _notenough=true end - + elseif spawnonairport then - + if nfree>=nunits then - + for i=1,nunits do table.insert(parkingspots, spots[i].Coordinate) table.insert(parkingindex, spots[i].TerminalID) end - + else -- Not enough spots for the whole group ==> air start! - _notenough=true - end + _notenough=true + end end - + -- Not enough spots ==> Prepare airstart. if _notenough then - - if self.respawn_inair and not self.SpawnUnControlled then + + if self.respawn_inair and not self.SpawnUnControlled then self:E(RAT.id..string.format("WARNING: Group %s has no parking spots at %s ==> air start!", self.SpawnTemplatePrefix, departure:GetName())) - + -- Not enough parking spots at the airport ==> Spawn in air. spawnonground=false spawnonship=false spawnonfarp=false spawnonrunway=false - + -- Set waypoint type/action to turning point. waypoints[1].type = GROUPTEMPLATE.Takeoff[GROUP.Takeoff.Air][1] -- type = Turning Point waypoints[1].action = GROUPTEMPLATE.Takeoff[GROUP.Takeoff.Air][2] -- action = Turning Point - + -- Adjust altitude to be 500-1000 m above the airbase. PointVec3.x=PointVec3.x+math.random(-1500,1500) - PointVec3.z=PointVec3.z+math.random(-1500,1500) + PointVec3.z=PointVec3.z+math.random(-1500,1500) if self.category==RAT.cat.heli then PointVec3.y=PointVec3:GetLandHeight()+math.random(100,1000) else @@ -5343,122 +5349,122 @@ function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace, departure, take return nil end end - + else - + -- Air start requested initially! - + --PointVec3.y is already set from first waypoint here! - + end --- new - + -- Translate the position of the Group Template to the Vec3. for UnitID = 1, nunits do - + -- Template of the current unit. local UnitTemplate = SpawnTemplate.units[UnitID] - - -- Tranlate position and preserve the relative position/formation of all aircraft. + + -- Tranlate position and preserve the relative position/formation of all aircraft. local SX = UnitTemplate.x - local SY = UnitTemplate.y + local SY = UnitTemplate.y local BX = SpawnTemplate.route.points[1].x local BY = SpawnTemplate.route.points[1].y local TX = PointVec3.x + (SX-BX) local TY = PointVec3.z + (SY-BY) - + if spawnonground then - - -- Sh°ps and FARPS seem to have a build in queue. + + -- Ships and FARPS seem to have a build in queue. if spawnonship or spawnonfarp or spawnonrunway or automatic then self:T(RAT.id..string.format("RAT group %s spawning at farp, ship or runway %s.", self.alias, departure:GetName())) -- Spawn on ship. We take only the position of the ship. SpawnTemplate.units[UnitID].x = PointVec3.x --TX SpawnTemplate.units[UnitID].y = PointVec3.z --TY - SpawnTemplate.units[UnitID].alt = PointVec3.y + SpawnTemplate.units[UnitID].alt = PointVec3.y else self:T(RAT.id..string.format("RAT group %s spawning at airbase %s on parking spot id %d", self.alias, departure:GetName(), parkingindex[UnitID])) - + -- Get coordinates of parking spot. SpawnTemplate.units[UnitID].x = parkingspots[UnitID].x SpawnTemplate.units[UnitID].y = parkingspots[UnitID].z - SpawnTemplate.units[UnitID].alt = parkingspots[UnitID].y + SpawnTemplate.units[UnitID].alt = parkingspots[UnitID].y end - - else + + else self:T(RAT.id..string.format("RAT group %s spawning in air at %s.", self.alias, departure:GetName())) - + -- Spawn in air as requested initially. Original template orientation is perserved, altitude is already correctly set. SpawnTemplate.units[UnitID].x = TX SpawnTemplate.units[UnitID].y = TY - SpawnTemplate.units[UnitID].alt = PointVec3.y + SpawnTemplate.units[UnitID].alt = PointVec3.y end - - -- Place marker at spawn position. + + -- Place marker at spawn position. if self.Debug then local unitspawn=COORDINATE:New(SpawnTemplate.units[UnitID].x, SpawnTemplate.units[UnitID].alt, SpawnTemplate.units[UnitID].y) unitspawn:MarkToAll(string.format("RAT %s Spawnplace unit #%d", self.alias, UnitID)) end - + -- Parking spot id. UnitTemplate.parking = nil UnitTemplate.parking_id = nil if parkingindex[UnitID] and not automatic then UnitTemplate.parking = parkingindex[UnitID] end - + -- Debug info. self:T2(RAT.id..string.format("RAT group %s unit number %d: Parking = %s",self.alias, UnitID, tostring(UnitTemplate.parking))) self:T2(RAT.id..string.format("RAT group %s unit number %d: Parking ID = %s",self.alias, UnitID, tostring(UnitTemplate.parking_id))) - + -- Set initial heading. SpawnTemplate.units[UnitID].heading = heading SpawnTemplate.units[UnitID].psi = -heading - + -- Set livery (will be the same for all units of the group). if livery then SpawnTemplate.units[UnitID].livery_id = livery end - + -- Set type of aircraft. if self.actype then SpawnTemplate.units[UnitID]["type"] = self.actype end - + -- Set AI skill. SpawnTemplate.units[UnitID]["skill"] = self.skill - + -- Onboard number. if self.onboardnum then SpawnTemplate.units[UnitID]["onboard_num"] = string.format("%s%d%02d", self.onboardnum, (self.SpawnIndex-1)%10, (self.onboardnum0-1)+UnitID) end - + -- Modify coaltion and country of template. SpawnTemplate.CoalitionID=self.coalition if self.country then SpawnTemplate.CountryID=self.country end - + end - + -- Copy waypoints into spawntemplate. By this we avoid the nasty DCS "landing bug" :) for i,wp in ipairs(waypoints) do SpawnTemplate.route.points[i]=wp end - + -- Also modify x,y of the template. Not sure why. SpawnTemplate.x = PointVec3.x SpawnTemplate.y = PointVec3.z - + -- Enable/disable radio. Same as checking the COMM box in the ME if self.radio then SpawnTemplate.communication=self.radio end - + -- Set radio frequency and modulation. if self.frequency then SpawnTemplate.frequency=self.frequency @@ -5466,12 +5472,12 @@ function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace, departure, take if self.modulation then SpawnTemplate.modulation=self.modulation end - + -- Debug output. self:T(SpawnTemplate) end end - + return true end @@ -5544,15 +5550,15 @@ function RAT:_ATCStatus() -- Current time. local Tnow=timer.getTime() - + for name,_ in pairs(RAT.ATC.flight) do -- Holding time at destination. local hold=RAT.ATC.flight[name].holding local dest=RAT.ATC.flight[name].destination - + if hold >= 0 then - + -- Some string whether the runway is busy or not. local busy="Runway state is unknown" if RAT.ATC.airport[dest].Nonfinal>0 then @@ -5560,29 +5566,29 @@ function RAT:_ATCStatus() else busy="Runway is currently clear" end - + -- Aircraft is holding. local text=string.format("ATC %s: Flight %s is holding for %i:%02d. %s.", dest, name, hold/60, hold%60, busy) BASE:T(RAT.id..text) - + elseif hold==RAT.ATC.onfinal then - + -- Aircarft is on final approach for landing. local Tfinal=Tnow-RAT.ATC.flight[name].Tonfinal - + local text=string.format("ATC %s: Flight %s is on final. Waiting %i:%02d for landing event.", dest, name, Tfinal/60, Tfinal%60) BASE:T(RAT.id..text) - + elseif hold==RAT.ATC.unregistered then - + -- Aircraft has not arrived at holding point. --self:T(string.format("ATC %s: Flight %s is not registered yet (hold %d).", dest, name, hold)) - + else BASE:E(RAT.id.."ERROR: Unknown holding time in RAT:_ATCStatus().") end end - + end --- Main ATC function. Updates the landing queue of all airports and inceases holding time for all flights. @@ -5591,17 +5597,17 @@ function RAT:_ATCCheck() -- Init queue of flights at all airports. RAT:_ATCQueue() - + -- Current time. local Tnow=timer.getTime() - + for name,_ in pairs(RAT.ATC.airport) do - + for qID,flight in ipairs(RAT.ATC.airport[name].queue) do - + -- Number of aircraft in queue. local nqueue=#RAT.ATC.airport[name].queue - + -- Conditions to clear an aircraft for landing local landing1 if RAT.ATC.airport[name].Tlastclearance then @@ -5615,31 +5621,31 @@ function RAT:_ATCCheck() if not landing1 and not landing2 then - + -- Update holding time. RAT.ATC.flight[flight].holding=Tnow-RAT.ATC.flight[flight].Tarrive - + -- Debug message. local text=string.format("ATC %s: Flight %s runway is busy. You are #%d of %d in landing queue. Your holding time is %i:%02d.", name, flight,qID, nqueue, RAT.ATC.flight[flight].holding/60, RAT.ATC.flight[flight].holding%60) BASE:T(RAT.id..text) - + else - + local text=string.format("ATC %s: Flight %s was cleared for landing. Your holding time was %i:%02d.", name, flight, RAT.ATC.flight[flight].holding/60, RAT.ATC.flight[flight].holding%60) BASE:T(RAT.id..text) - + -- Clear flight for landing. RAT:_ATCClearForLanding(name, flight) - + end - + end - + end - + -- Update queue of flights at all airports. RAT:_ATCQueue() - + end --- Giving landing clearance for aircraft by setting user flag. @@ -5656,13 +5662,13 @@ function RAT:_ATCClearForLanding(airport, flight) -- Number of planes on final approach. RAT.ATC.airport[airport].Nonfinal=RAT.ATC.airport[airport].Nonfinal+1 -- Last time an aircraft got landing clearance. - RAT.ATC.airport[airport].Tlastclearance=timer.getTime() + RAT.ATC.airport[airport].Tlastclearance=timer.getTime() -- Current time. RAT.ATC.flight[flight].Tonfinal=timer.getTime() -- Set user flag to 1 ==> stop condition for holding. trigger.action.setUserFlag(flight, 1) local flagvalue=trigger.misc.getUserFlag(flight) - + -- Debug message. local text1=string.format("ATC %s: Flight %s cleared for landing (flag=%d).", airport, flight, flagvalue) local text2=string.format("ATC %s: Flight %s you are cleared for landing.", airport, flight) @@ -5676,33 +5682,33 @@ end function RAT:_ATCFlightLanded(name) if RAT.ATC.flight[name] then - + -- Destination airport. local dest=RAT.ATC.flight[name].destination - + -- Times for holding and final approach. local Tnow=timer.getTime() local Tfinal=Tnow-RAT.ATC.flight[name].Tonfinal local Thold=RAT.ATC.flight[name].Tonfinal-RAT.ATC.flight[name].Tarrive - + -- Airport is not busy any more. RAT.ATC.airport[dest].busy=false - + -- No aircraft on final any more. RAT.ATC.airport[dest].onfinal[name]=nil - + -- Decrease number of aircraft on final. RAT.ATC.airport[dest].Nonfinal=RAT.ATC.airport[dest].Nonfinal-1 - + -- Remove this flight from list of flights. RAT:_ATCDelFlight(RAT.ATC.flight, name) - + -- Increase landing counter to monitor traffic. RAT.ATC.airport[dest].traffic=RAT.ATC.airport[dest].traffic+1 - + -- Number of planes landing per hour. local TrafficPerHour=RAT.ATC.airport[dest].traffic/(timer.getTime()-RAT.ATC.T0)*3600 - + -- Debug info local text1=string.format("ATC %s: Flight %s landed. Tholding = %i:%02d, Tfinal = %i:%02d.", dest, name, Thold/60, Thold%60, Tfinal/60, Tfinal%60) local text2=string.format("ATC %s: Number of flights still on final %d.", dest, RAT.ATC.airport[dest].Nonfinal) @@ -5713,7 +5719,7 @@ function RAT:_ATCFlightLanded(name) BASE:T(RAT.id..text3) MESSAGE:New(text4, 10):ToAllIf(RAT.ATC.messages) end - + end --- Creates a landing queue for all flights holding at airports. Aircraft with longest holding time gets first permission to land. @@ -5721,7 +5727,7 @@ end function RAT:_ATCQueue() for airport,_ in pairs(RAT.ATC.airport) do - + -- Local airport queue. local _queue={} @@ -5729,26 +5735,26 @@ function RAT:_ATCQueue() for name,_ in pairs(RAT.ATC.flight) do --fvh local Tnow=timer.getTime() - + -- Update holding time (unless holing is set to onfinal=-100) if RAT.ATC.flight[name].holding>=0 then RAT.ATC.flight[name].holding=Tnow-RAT.ATC.flight[name].Tarrive end local hold=RAT.ATC.flight[name].holding local dest=RAT.ATC.flight[name].destination - + -- Flight is holding at this airport. if hold>=0 and airport==dest then _queue[#_queue+1]={name,hold} end end - + -- Sort queue w.r.t holding time in ascending order. local function compare(a,b) return a[2] > b[2] end table.sort(_queue, compare) - + -- Transfer queue to airport queue. RAT.ATC.airport[airport].queue={} for k,v in ipairs(_queue) do @@ -5776,36 +5782,36 @@ end -- @extends Core.Base#BASE ---# RATMANAGER class, extends @{Core.Base#BASE} --- The RATMANAGER class manages spawning of multiple RAT objects in a very simple way. It is created by the @{#RATMANAGER.New}() contructor. +-- The RATMANAGER class manages spawning of multiple RAT objects in a very simple way. It is created by the @{#RATMANAGER.New}() contructor. -- RAT objects with different "tasks" can be defined as usual. However, they **must not** be spawned via the @{#RAT.Spawn}() function. --- +-- -- Instead, these objects can be added to the manager via the @{#RATMANAGER.Add}(ratobject, min) function, where the first parameter "ratobject" is the @{#RAT} object, while the second parameter "min" defines the -- minimum number of RAT aircraft of that object, which are alive at all time. --- +-- -- The @{#RATMANAGER} must be started by the @{#RATMANAGER.Start}(startime) function, where the optional argument "startime" specifies the delay time in seconds after which the manager is started and the spawning beginns. -- 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. --- +-- -- local a10c=RAT:New("RAT_A10C", "A-10C managed") -- a10c:SetDeparture({"Batumi"}) --- +-- -- local f15c=RAT:New("RAT_F15C", "F15C managed") -- f15c:SetDeparture({"Sochi-Adler"}) -- f15c:DestinationZone() -- f15c:SetDestination({"Zone C"}) --- +-- -- local av8b=RAT:New("RAT_AV8B", "AV8B managed") -- av8b:SetDeparture({"Zone C"}) -- av8b:SetTakeoff("air") -- av8b:DestinationZone() -- av8b:SetDestination({"Zone A"}) --- +-- -- local manager=RATMANAGER:New(25) -- manager:Add(a10c, 5) -- manager:Add(f15c, 5) @@ -5841,13 +5847,13 @@ function RATMANAGER:New(ntot) -- Inherit BASE. local self=BASE:Inherit(self, BASE:New()) -- #RATMANAGER - + -- Total number of RAT groups. self.ntot=ntot or 1 - + -- Debug info self:E(RATMANAGER.id..string.format("Creating manager for %d groups.", ntot)) - + return self end @@ -5862,21 +5868,21 @@ function RATMANAGER:Add(ratobject,min) --Automatic respawning is disabled. ratobject.norespawn=true ratobject.f10menu=false - + -- Increase RAT object counter. self.nrat=self.nrat+1 - + self.rat[self.nrat]=ratobject self.alive[self.nrat]=0 self.name[self.nrat]=ratobject.alias self.min[self.nrat]=min or 1 - + -- Debug info. self:T(RATMANAGER.id..string.format("Adding ratobject %s with min flights = %d", self.name[self.nrat],self.min[self.nrat])) - + -- Call spawn to initialize RAT parameters. ratobject:Spawn(0) - + return self end @@ -5900,7 +5906,7 @@ function RATMANAGER:Start(delay) -- Start scheduler. SCHEDULER:New(nil, self._Start, {self}, delay) - + return self end @@ -5918,7 +5924,7 @@ function RATMANAGER:_Start() -- Get randum number of new RAT groups. local N=self:_RollDice(self.nrat, self.ntot, self.min, self.alive) - + -- Loop over all RAT objects and spawn groups. local time=0.0 for i=1,self.nrat do @@ -5927,26 +5933,26 @@ function RATMANAGER:_Start() SCHEDULER:New(nil, RAT._SpawnWithRoute, {self.rat[i]}, time) end end - + -- Start activation scheduler for uncontrolled aircraft. - for i=1,self.nrat do + for i=1,self.nrat do if self.rat[i].uncontrolled and self.rat[i].activate_uncontrolled then -- Start activating stuff but not before the latest spawn has happend. - local Tactivate=math.max(time+1, self.rat[i].activate_delay) + local Tactivate=math.max(time+1, self.rat[i].activate_delay) SCHEDULER:New(self.rat[i], self.rat[i]._ActivateUncontrolled, {self.rat[i]}, Tactivate, self.rat[i].activate_delta, self.rat[i].activate_frand) end end - + -- Start the manager. But not earlier than the latest spawn has happened! local TstartManager=math.max(time+1, self.Tcheck) - + -- Start manager scheduler. self.manager, self.managerid = SCHEDULER:New(self, self._Manage, {self}, TstartManager, self.Tcheck) --Core.Scheduler#SCHEDULER - + -- Info local text=string.format(RATMANAGER.id.."Starting RAT manager with scheduler ID %s in %d seconds. Repeat interval %d seconds.", self.managerid, TstartManager, self.Tcheck) self:E(text) - + return self end @@ -5995,14 +6001,14 @@ function RATMANAGER:_Manage() -- Count total number of groups. local ntot=self:_Count() - + -- Debug info. local text=string.format("Number of alive groups %d. New groups to be spawned %d.", ntot, self.ntot-ntot) self:T(RATMANAGER.id..text) - + -- Get number of necessary spawns. local N=self:_RollDice(self.nrat, self.ntot, self.min, self.alive) - + -- Loop over all RAT objects and spawn new groups if necessary. local time=0.0 for i=1,self.nrat do @@ -6019,13 +6025,13 @@ function RATMANAGER:_Count() -- Init total counter. local ntotal=0 - + -- Loop over all RAT objects. for i=1,self.nrat do local n=0 - + local ratobject=self.rat[i] --#RAT - + -- Loop over the RAT groups of this object. for spawnindex,ratcraft in pairs(ratobject.ratcraft) do local group=ratcraft.group --Wrapper.Group#GROUP @@ -6033,18 +6039,18 @@ function RATMANAGER:_Count() n=n+1 end end - + -- Alive groups of this RAT object. self.alive[i]=n - + -- Grand total. ntotal=ntotal+n - + -- Debug output. local text=string.format("Number of alive groups of %s = %d", self.name[i], n) self:T(RATMANAGER.id..text) end - + -- Return grand total. return ntotal end @@ -6056,7 +6062,7 @@ end -- @param #table min Minimum number of groups for each RAT object. -- @param #table alive Number of alive groups of each RAT object. function RATMANAGER:_RollDice(nrat,ntot,min,alive) - + -- Calculate sum. local function sum(A,index) local summe=0 @@ -6064,8 +6070,8 @@ function RATMANAGER:_RollDice(nrat,ntot,min,alive) summe=summe+A[i] end return summe - end - + end + -- Table of number of groups. local N={} local M={} @@ -6075,57 +6081,57 @@ function RATMANAGER:_RollDice(nrat,ntot,min,alive) M[#M+1]=math.max(alive[i], min[i]) P[#P+1]=math.max(min[i]-alive[i],0) end - + -- Min/max group arrays. local mini={} local maxi={} - + -- Arrays. local rattab={} for i=1,nrat do table.insert(rattab,i) end local done={} - + -- Number of new groups to be added. local nnew=ntot for i=1,nrat do nnew=nnew-alive[i] end - + for i=1,nrat-1 do - + -- Random entry from . local r=math.random(#rattab) -- Get value local j=rattab[r] - + table.remove(rattab, r) table.insert(done,j) - + -- Sum up the number of already distributed groups. local sN=sum(N, done) - -- Sum up the minimum number of yet to be distributed groups. + -- Sum up the minimum number of yet to be distributed groups. local sP=sum(P, rattab) - + -- Max number that can be distributed for this object. maxi[j]=nnew-sN-sP - + -- Min number that should be distributed for this object mini[j]=P[j] - + -- Random number of new groups for this RAT object. if maxi[j] >= mini[j] then N[j]=math.random(mini[j], maxi[j]) else N[j]=0 end - + -- Debug info self:T3(string.format("RATMANAGER: i=%d, alive=%d, min=%d, mini=%d, maxi=%d, add=%d, sumN=%d, sumP=%d", j, alive[j], min[j], mini[j], maxi[j], N[j],sN, sP)) - + end - + -- Last RAT object, number of groups is determined from number of already distributed groups and nnew. local j=rattab[1] N[j]=nnew-sum(N, done) @@ -6133,7 +6139,7 @@ function RATMANAGER:_RollDice(nrat,ntot,min,alive) maxi[j]=nnew-sum(N, done) table.remove(rattab, 1) table.insert(done,j) - + -- Debug info local text=RATMANAGER.id.."\n" for i=1,nrat do @@ -6141,8 +6147,7 @@ function RATMANAGER:_RollDice(nrat,ntot,min,alive) end text=text..string.format("Total # of groups to add = %d", sum(N, done)) self:T(text) - + -- Return number of groups to be spawned. return N end - diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index 948b23cc4..35cbdddd6 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -429,6 +429,25 @@ RANGE.TargetType = { -- @field #string airframe Aircraft type of player. -- @field #number time Time via timer.getAbsTime() in seconds of impact. -- @field #string date OS date. +-- @field #number attackHdg Attack heading in degrees. +-- @field #number attackVel Attack velocity in knots. +-- @field #number attackAlt Attack altitude in feet. +-- @field #string clock Time of the run. +-- @field #string rangename Name of the range. + +--- Strafe result. +-- @type RANGE.StrafeResult +-- @field #string player Player name. +-- @field #string airframe Aircraft type of player. +-- @field #number time Time via timer.getAbsTime() in seconds of impact. +-- @field #string date OS date. +-- @field #string name Name of the target pit. +-- @field #number roundsFired Number of rounds fired. +-- @field #number roundsHit Number of rounds that hit the target. +-- @field #number strafeAccuracy Accuracy of the run in percent. +-- @field #string clock Time of the run. +-- @field #string rangename Name of the range. +-- @field #boolean invalid Invalid pass. --- Strafe result. -- @type RANGE.StrafeResult @@ -569,17 +588,16 @@ RANGE.version = "2.4.0" --- RANGE contructor. Creates a new RANGE object. -- @param #RANGE self --- @param #string rangename Name of the range. Has to be unique. Will we used to create F10 menu items etc. +-- @param #string RangeName Name of the range. Has to be unique. Will we used to create F10 menu items etc. -- @return #RANGE RANGE object. -function RANGE:New( rangename ) - BASE:F( { rangename = rangename } ) +function RANGE:New( RangeName ) -- Inherit BASE. local self = BASE:Inherit( self, FSM:New() ) -- #RANGE -- Get range name. -- TODO: make sure that the range name is not given twice. This would lead to problems in the F10 radio menu. - self.rangename = rangename or "Practice Range" + self.rangename = RangeName or "Practice Range" -- Log id. self.id = string.format( "RANGE %s | ", self.rangename ) @@ -950,6 +968,19 @@ function RANGE:SetTargetSheet( path, prefix ) return self end +--- Set FunkMan socket. Bombing and strafing results will be send to your Discord bot. +-- **Requires running FunkMan program**. +-- @param #RANGE self +-- @param #number Port Port. Default `10042`. +-- @param #string Host Host. Default "127.0.0.1". +-- @return #RANGE self +function RANGE:SetFunkManOn(Port, Host) + + self.funkmanSocket=SOCKET:New(Port, Host) + + return self +end + --- Set messages to examiner. The examiner will receive messages from all clients. -- @param #RANGE self -- @param #string examinergroupname Name of the group of the examiner. @@ -1728,7 +1759,6 @@ function RANGE:OnEventHit( EventData ) self:_DisplayMessageToGroup( _unit, text ) self:T2( self.id .. text ) _currentTarget.pastfoulline = true - invalidStrafe = true -- Rangeboss Edit end end @@ -1760,6 +1790,13 @@ function RANGE:OnEventHit( EventData ) end end +--- Range event handler for event shot (when a unit releases a rocket or bomb (but not a fast firing gun). +-- @param #RANGE self +-- @param #table weapon Weapon +function RANGE:_TrackWeapon(weapon) + +end + --- Range event handler for event shot (when a unit releases a rocket or bomb (but not a fast firing gun). -- @param #RANGE self -- @param Core.Event#EVENTDATA EventData @@ -1806,6 +1843,11 @@ function RANGE:OnEventShot( EventData ) -- Get player unit and name. local _unit, _playername = self:_GetPlayerUnitAndName( _unitName ) + + -- Attack parameters. + local attackHdg=_unit:GetHeading() + local attackAlt=_unit:GetHeight() + local attackVel=_unit:GetVelocityKNOTS() -- Set this to larger value than the threshold. local dPR = self.BombtrackThreshold * 2 @@ -1848,7 +1890,6 @@ function RANGE:OnEventShot( EventData ) -- Check again in ~0.005 seconds ==> 200 checks per second. return timer.getTime() + self.dtBombtrack - else ----------------------------- @@ -1858,7 +1899,7 @@ function RANGE:OnEventShot( EventData ) -- Get closet target to last position. local _closetTarget = nil -- #RANGE.BombTarget local _distance = nil - local _closeCoord = nil + local _closeCoord = nil --Core.Point#COORDINATE local _hitquality = "POOR" -- Get callsign. @@ -1886,6 +1927,7 @@ function RANGE:OnEventShot( EventData ) -- Loop over defined bombing targets. for _, _bombtarget in pairs( self.bombingTargets ) do + local bombtarget=_bombtarget --#RANGE.BombTarget -- Get target coordinate. local targetcoord = self:_GetBombTargetCoordinate( _bombtarget ) @@ -1898,15 +1940,15 @@ function RANGE:OnEventShot( EventData ) -- Find closest target to last known position of the bomb. if _distance == nil or _temp < _distance then _distance = _temp - _closetTarget = _bombtarget - _closeCoord = targetcoord + _closetTarget = bombtarget + _closeCoord = targetcoord if _distance <= 1.53 then -- Rangeboss Edit _hitquality = "SHACK" -- Rangeboss Edit - elseif _distance <= 0.5 * _bombtarget.goodhitrange then -- Rangeboss Edit + elseif _distance <= 0.5 * bombtarget.goodhitrange then -- Rangeboss Edit _hitquality = "EXCELLENT" - elseif _distance <= _bombtarget.goodhitrange then + elseif _distance <= bombtarget.goodhitrange then _hitquality = "GOOD" - elseif _distance <= 2 * _bombtarget.goodhitrange then + elseif _distance <= 2 * bombtarget.goodhitrange then _hitquality = "INEFFECTIVE" else _hitquality = "POOR" @@ -1927,6 +1969,7 @@ function RANGE:OnEventShot( EventData ) local _results = self.bombPlayerResults[_playername] local result = {} -- #RANGE.BombResult + result.command=SOCKET.DataType.BOMBRESULT result.name = _closetTarget.name or "unknown" result.distance = _distance result.radial = _closeCoord:HeadingTo( impactcoord ) @@ -1934,11 +1977,17 @@ function RANGE:OnEventShot( EventData ) result.quality = _hitquality result.player = playerData.playername result.time = timer.getAbsTime() + result.clock = UTILS.SecondsToClock(result.time, true) + result.midate = UTILS.GetDCSMissionDate() + result.theatre = env.mission.theatre result.airframe = playerData.airframe result.roundsFired = 0 -- Rangeboss Edit result.roundsHit = 0 -- Rangeboss Edit result.roundsQuality = "N/A" -- Rangeboss Edit result.rangename = self.rangename + result.attackHdg = attackHdg + result.attackVel = attackVel + result.attackAlt = attackAlt -- Add to table. table.insert( _results, result ) @@ -2078,13 +2127,13 @@ function RANGE:onafterImpact( From, Event, To, result, player ) -- Only display target name if there is more than one bomb target. local targetname = nil if #self.bombingTargets > 1 then - local targetname = result.name + targetname = result.name end -- Send message to player. local text = string.format( "%s, impact %03d° for %d ft", player.playername, result.radial, UTILS.MetersToFeet( result.distance ) ) if targetname then - text = text .. string.format( " from bulls of target %s." ) + text = text .. string.format( " from bulls of target %s.", targetname ) else text = text .. "." end @@ -2110,16 +2159,40 @@ function RANGE:onafterImpact( From, Event, To, result, player ) end -- Unit. - local unit = UNIT:FindByName( player.unitname ) - - -- Send message. - self:_DisplayMessageToGroup( unit, text, nil, true ) - self:T( self.id .. text ) + if player.unitname then + + -- Get unit. + local unit = UNIT:FindByName( player.unitname ) + + -- Send message. + self:_DisplayMessageToGroup( unit, text, nil, true ) + self:T( self.id .. text ) + end -- Save results. if self.autosave then self:Save() end + + -- Send result to FunkMan, which creates fancy MatLab figures and sends them to Discord via a bot. + if self.funkmanSocket then + self.funkmanSocket:SendTable(result) + end + +end + +--- Function called after strafing run. +-- @param #RANGE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #RANGE.PlayerData player Player data table. +-- @param #RANGE.StrafeResult result Result of run. +function RANGE:onafterStrafeResult( From, Event, To, player, result) + + if self.funkmanSocket then + self.funkmanSocket:SendTable(result) + end end @@ -2175,7 +2248,7 @@ function RANGE:onafterSave( From, Event, To ) local target = result.name local radial = result.radial local quality = result.quality - local time = UTILS.SecondsToClock( result.time ) + local time = UTILS.SecondsToClock(result.time, true) local airframe = result.airframe local date = "n/a" if os then @@ -2353,7 +2426,8 @@ end --- Start smoking a coordinate with a delay. -- @param #table _args Argements passed. function RANGE._DelayedSmoke( _args ) - trigger.action.smoke( _args.coord:GetVec3(), _args.color ) + _args.coord:Smoke(_args.color) + --trigger.action.smoke( _args.coord:GetVec3(), _args.color ) end --- Display top 10 stafing results of a specific player. @@ -3045,9 +3119,13 @@ function RANGE:_CheckInZone( _unitName ) -- Strafe result. local result = {} -- #RANGE.StrafeResult + result.command=SOCKET.DataType.STRAFERESULT result.player=_playername result.name=_result.zone.name or "unknown" result.time = timer.getAbsTime() + result.clock = UTILS.SecondsToClock(result.time) + result.midate = UTILS.GetDCSMissionDate() + result.theatre = env.mission.theatre result.roundsFired = shots result.roundsHit = _result.hits result.roundsQuality = resulttext @@ -3473,6 +3551,7 @@ function RANGE:_DisplayMessageToGroup( _unit, _text, _time, _clear, display ) -- Group ID. local _gid = _unit:GetGroup():GetID() + local _grp = _unit:GetGroup() -- Get playername and player settings local _, playername = self:_GetPlayerUnitAndName( _unit:GetName() ) @@ -3480,14 +3559,14 @@ function RANGE:_DisplayMessageToGroup( _unit, _text, _time, _clear, display ) -- Send message to player if messages enabled and not only for the examiner. if _gid and (playermessage == true or display) and (not self.examinerexclusive) then - trigger.action.outTextForGroup( _gid, _text, _time, _clear ) + local m = MESSAGE:New(_text,_time,nil,_clear):ToUnit(_unit) end -- Send message to examiner. if self.examinergroupname ~= nil then - local _examinerid = GROUP:FindByName( self.examinergroupname ):GetID() + local _examinerid = GROUP:FindByName( self.examinergroupname ) if _examinerid then - trigger.action.outTextForGroup( _examinerid, _text, _time, _clear ) + local m = MESSAGE:New(_text,_time,nil,_clear):ToGroup(_examinerid) end end end diff --git a/Moose Development/Moose/Functional/Scoring.lua b/Moose Development/Moose/Functional/Scoring.lua index c2c777df9..6b6e05619 100644 --- a/Moose Development/Moose/Functional/Scoring.lua +++ b/Moose Development/Moose/Functional/Scoring.lua @@ -280,11 +280,11 @@ function SCORING:New( GameName ) -- Default fratricide penalty level (maximum penalty that can be assigned to a player before he gets kicked). self:SetFratricide( self.ScaleDestroyPenalty * 3 ) self.penaltyonfratricide = true - + -- Default penalty when a player changes coalition. self:SetCoalitionChangePenalty( self.ScaleDestroyPenalty ) self.penaltyoncoalitionchange = true - + self:SetDisplayMessagePrefix() -- Event handlers @@ -656,11 +656,12 @@ function SCORING:_AddPlayerFromUnit( UnitData ) if self.Players[PlayerName].UnitCoalition ~= UnitCoalition and self.penaltyoncoalitionchange then self.Players[PlayerName].Penalty = self.Players[PlayerName].Penalty + self.CoalitionChangePenalty or 50 self.Players[PlayerName].PenaltyCoalition = self.Players[PlayerName].PenaltyCoalition + 1 - MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' changed coalition from " .. _SCORINGCoalition[self.Players[PlayerName].UnitCoalition] .. " to " .. _SCORINGCoalition[UnitCoalition] .. "(changed " .. self.Players[PlayerName].PenaltyCoalition .. " times the coalition). " .. - self.CoalitionChangePenalty .. "Penalty points added.", - MESSAGE.Type.Information ) - :ToAll() - self:ScoreCSV( PlayerName, "", "COALITION_PENALTY", 1, -1 * self.CoalitionChangePenalty, self.Players[PlayerName].UnitName, _SCORINGCoalition[self.Players[PlayerName].UnitCoalition], _SCORINGCategory[self.Players[PlayerName].UnitCategory], self.Players[PlayerName].UnitType, UnitName, _SCORINGCoalition[UnitCoalition], _SCORINGCategory[UnitCategory], UnitData:GetTypeName() ) + MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' changed coalition from " .. _SCORINGCoalition[self.Players[PlayerName].UnitCoalition] .. " to " .. _SCORINGCoalition[UnitCoalition] .. + "(changed " .. self.Players[PlayerName].PenaltyCoalition .. " times the coalition). ".. self.CoalitionChangePenalty .."Penalty points added.", + MESSAGE.Type.Information + ):ToAll() + self:ScoreCSV( PlayerName, "", "COALITION_PENALTY", 1, -1*self.CoalitionChangePenalty, self.Players[PlayerName].UnitName, _SCORINGCoalition[self.Players[PlayerName].UnitCoalition], _SCORINGCategory[self.Players[PlayerName].UnitCategory], self.Players[PlayerName].UnitType, + UnitName, _SCORINGCoalition[UnitCoalition], _SCORINGCategory[UnitCategory], UnitData:GetTypeName() ) end end @@ -671,7 +672,7 @@ function SCORING:_AddPlayerFromUnit( UnitData ) self.Players[PlayerName].UNIT = UnitData self.Players[PlayerName].ThreatLevel = UnitThreatLevel self.Players[PlayerName].ThreatType = UnitThreatType - + -- TODO: make fratricide switchable if self.Players[PlayerName].Penalty > self.Fratricide * 0.50 and self.penaltyonfratricide then if self.Players[PlayerName].PenaltyWarning < 1 then @@ -1112,16 +1113,18 @@ function SCORING:_EventOnHit( Event ) if InitCoalition then -- A coalition object was hit, probably a static. if InitCoalition == TargetCoalition then -- TODO: Penalty according scale - Player.Penalty = Player.Penalty + 10 -- * self.ScaleDestroyPenalty - PlayerHit.Penalty = PlayerHit.Penalty + 10 -- * self.ScaleDestroyPenalty + Player.Penalty = Player.Penalty + 10 --* self.ScaleDestroyPenalty + PlayerHit.Penalty = PlayerHit.Penalty + 10 --* self.ScaleDestroyPenalty PlayerHit.PenaltyHit = PlayerHit.PenaltyHit + 1 * self.ScaleDestroyPenalty - - MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. Event.WeaponPlayerName .. "' hit friendly target " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. - "Penalty: -" .. PlayerHit.Penalty .. " = " .. Player.Score - Player.Penalty, - MESSAGE.Type.Update ) - :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) - :ToCoalitionIf( Event.WeaponCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) - + + MESSAGE + :NewType( self.DisplayMessagePrefix .. "Player '" .. Event.WeaponPlayerName .. "' hit friendly target " .. + TargetUnitCategory .. " ( " .. TargetType .. " ) " .. + "Penalty: -" .. PlayerHit.Penalty .. " = " .. Player.Score - Player.Penalty, + MESSAGE.Type.Update + ) + :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() ) + :ToCoalitionIf( Event.WeaponCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() ) self:ScoreCSV( Event.WeaponPlayerName, TargetPlayerName, "HIT_PENALTY", 1, -10, Event.WeaponName, Event.WeaponCoalition, Event.WeaponCategory, Event.WeaponTypeName, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) else Player.Score = Player.Score + 1 @@ -1651,18 +1654,19 @@ function SCORING:ReportScoreGroupDetailed( PlayerGroup ) local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + PenaltyGoals + PenaltyMissions - - PlayerMessage = string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )%s%s%s%s%s", - PlayerName, - PlayerScore - PlayerPenalty, - PlayerScore, - PlayerPenalty, - ReportHits, - ReportDestroys, - ReportCoalitionChanges, - ReportGoals, - ReportMissions - ) + + PlayerMessage = + string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )%s%s%s%s%s", + PlayerName, + PlayerScore - PlayerPenalty, + PlayerScore, + PlayerPenalty, + ReportHits, + ReportDestroys, + ReportCoalitionChanges, + ReportGoals, + ReportMissions + ) MESSAGE:NewType( PlayerMessage, MESSAGE.Type.Detailed ):ToGroup( PlayerGroup ) end end @@ -1706,13 +1710,14 @@ function SCORING:ReportScoreAllSummary( PlayerGroup ) local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + PenaltyGoals + PenaltyMissions - - PlayerMessage = string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )", - PlayerName, - PlayerScore - PlayerPenalty, - PlayerScore, - PlayerPenalty - ) + + PlayerMessage = + string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )", + PlayerName, + PlayerScore - PlayerPenalty, + PlayerScore, + PlayerPenalty + ) MESSAGE:NewType( PlayerMessage, MESSAGE.Type.Overview ):ToGroup( PlayerGroup ) end end diff --git a/Moose Development/Moose/Functional/Sead.lua b/Moose Development/Moose/Functional/Sead.lua index 2982f478e..2af6c64f3 100644 --- a/Moose Development/Moose/Functional/Sead.lua +++ b/Moose Development/Moose/Functional/Sead.lua @@ -79,6 +79,7 @@ SEAD = { ["Kh25"] = "Kh25", ["BGM_109"] = "BGM_109", ["AGM_154"] = "AGM_154", + ["HY-2"] = "HY-2", } --- Missile enumerators - from DCS ME and Wikipedia @@ -98,6 +99,7 @@ SEAD = { ["Kh25"] = {25, 0.8}, ["BGM_109"] = {460, 0.705}, --in-game ~465kn ["AGM_154"] = {130, 0.61}, + ["HY-2"] = {90,1}, } --- Creates the main object which is handling defensive actions for SA sites or moving SA vehicles. diff --git a/Moose Development/Moose/Functional/Shorad.lua b/Moose Development/Moose/Functional/Shorad.lua index ce2d93d20..f70bb11a5 100644 --- a/Moose Development/Moose/Functional/Shorad.lua +++ b/Moose Development/Moose/Functional/Shorad.lua @@ -18,7 +18,7 @@ -- @module Functional.Shorad -- @image Functional.Shorad.jpg -- --- Date: July 2021 +-- Date: Nov 2021 ------------------------------------------------------------------------- --- **SHORAD** class, extends Core.Base#BASE @@ -41,6 +41,7 @@ -- @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 @@ -131,6 +132,7 @@ do ["Kh29"] = "Kh29", ["Kh31"] = "Kh31", ["Kh66"] = "Kh66", + --["BGM_109"] = "BGM_109", } --- Instantiates a new SHORAD object @@ -138,13 +140,13 @@ do -- @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 Radius Defense radius in meters, used to switch on SHORAD groups **within** this radius -- @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() ) + local self = BASE:Inherit( self, FSM:New() ) self:T({Name, ShoradPrefix, Samset, Radius, ActiveTimer, Coalition}) local GroupSet = SET_GROUP:New():FilterPrefixes(ShoradPrefix):FilterCoalitions(Coalition):FilterCategoryGround():FilterStart() @@ -162,11 +164,17 @@ do 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") + self:I("*** SHORAD - Started Version 0.3.1") -- 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) + + -- Start State. + self:SetStartState("Running") + self:AddTransition("*", "WakeUpShorad", "*") + self:AddTransition("*", "CalculateHitZone", "*") + return self end @@ -310,7 +318,7 @@ do local hit = false if self.DefendHarms then for _,_name in pairs (SHORAD.Harms) do - if string.find(WeaponName,_name,1) then hit = true end + if string.find(WeaponName,_name,1,true) then hit = true end end end return hit @@ -326,7 +334,7 @@ do local hit = false if self.DefendMavs then for _,_name in pairs (SHORAD.Mavs) do - if string.find(WeaponName,_name,1) then hit = true end + if string.find(WeaponName,_name,1,true) then hit = true end end end return hit @@ -367,7 +375,7 @@ do local returnname = false for _,_groups in pairs (shoradset) do local groupname = _groups:GetName() - if string.find(groupname, tgtgrp, 1) then + if string.find(groupname, tgtgrp, 1, true) then returnname = true --_groups:RelocateGroundRandomInRadius(7,100,false,false) -- be a bit evasive end @@ -388,7 +396,7 @@ do local returnname = false for _,_groups in pairs (shoradset) do local groupname = _groups:GetName() - if string.find(groupname, tgtgrp, 1) then + if string.find(groupname, tgtgrp, 1, true) then returnname = true end end @@ -400,6 +408,7 @@ do -- @return #boolean Returns true for a detection, else false function SHORAD:_ShotIsDetected() self:T(self.lid .. " _ShotIsDetected") + if self.debug then return true end local IsDetected = false local DetectionProb = math.random(self.DefenseLowProb, self.DefenseHighProb) -- reference value local ActualDetection = math.random(1,100) -- value for this shot @@ -423,7 +432,7 @@ do -- 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) + function SHORAD:onafterWakeUpShorad(From, Event, To, TargetGroup, Radius, ActiveTimer, TargetCat) self:T(self.lid .. " WakeUpShorad") self:T({TargetGroup, Radius, ActiveTimer, TargetCat}) local targetcat = TargetCat or Object.Category.UNIT @@ -477,6 +486,76 @@ do return self end +--- (Internal) Calculate hit zone of an AGM-88 +-- @param #SHORAD self +-- @param #table SEADWeapon DCS.Weapon object +-- @param Core.Point#COORDINATE pos0 Position of the plane when it fired +-- @param #number height Height when the missile was fired +-- @param Wrapper.Group#GROUP SEADGroup Attacker group +-- @return #SHORAD self +function SHORAD:onafterCalculateHitZone(From,Event,To,SEADWeapon,pos0,height,SEADGroup) + self:T("**** Calculating hit zone") + if SEADWeapon and SEADWeapon:isExist() then + --local pos = SEADWeapon:getPoint() + + -- postion and height + local position = SEADWeapon:getPosition() + local mheight = height + -- heading + local wph = math.atan2(position.x.z, position.x.x) + if wph < 0 then + wph=wph+2*math.pi + end + wph=math.deg(wph) + + -- velocity + local wpndata = SEAD.HarmData["AGM_88"] + local mveloc = math.floor(wpndata[2] * 340.29) + local c1 = (2*mheight*9.81)/(mveloc^2) + local c2 = (mveloc^2) / 9.81 + local Ropt = c2 * math.sqrt(c1+1) + if height <= 5000 then + Ropt = Ropt * 0.72 + elseif height <= 7500 then + Ropt = Ropt * 0.82 + elseif height <= 10000 then + Ropt = Ropt * 0.87 + elseif height <= 12500 then + Ropt = Ropt * 0.98 + end + + -- look at a couple of zones across the trajectory + for n=1,3 do + local dist = Ropt - ((n-1)*20000) + local predpos= pos0:Translate(dist,wph) + if predpos then + + local targetzone = ZONE_RADIUS:New("Target Zone",predpos:GetVec2(),20000) + + if self.debug then + predpos:MarkToAll(string.format("height=%dm | heading=%d | velocity=%ddeg | Ropt=%dm",mheight,wph,mveloc,Ropt),false) + targetzone:DrawZone(coalition.side.BLUE,{0,0,1},0.2,nil,nil,3,true) + end + + local seadset = self.Groupset + local tgtcoord = targetzone:GetRandomPointVec2() + local tgtgrp = seadset:FindNearestGroupFromPointVec2(tgtcoord) + local _targetgroup = nil + local _targetgroupname = "none" + local _targetskill = "Random" + if tgtgrp and tgtgrp:IsAlive() then + _targetgroup = tgtgrp + _targetgroupname = tgtgrp:GetName() -- group name + _targetskill = tgtgrp:GetUnit(1):GetSkill() + self:T("*** Found Target = ".. _targetgroupname) + self:WakeUpShorad(_targetgroupname, self.Radius, self.ActiveTimer, Object.Category.UNIT) + end + 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 @@ -504,13 +583,48 @@ do if (self:_CheckHarms(ShootingWeaponName) or self:_CheckMavs(ShootingWeaponName)) and IsDetected then -- get target data local targetdata = EventData.Weapon:getTarget() -- Identify target + -- Is there target data? + if not targetdata or self.debug then + if string.find(ShootingWeaponName,"AGM_88",1,true) then + self:I("**** Tracking AGM-88 with no target data.") + local pos0 = EventData.IniUnit:GetCoordinate() + local fheight = EventData.IniUnit:GetHeight() + self:__CalculateHitZone(20,ShootingWeapon,pos0,fheight,EventData.IniGroup) + end + return self + end + local targetcat = targetdata:getCategory() -- Identify category self:T(string.format("Target Category (3=STATIC, 1=UNIT)= %s",tostring(targetcat))) + self:T({targetdata}) 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) + --self:T("Static Target Data") + --self:T({targetdata:isExist()}) + --self:T({targetdata:getPoint()}) + local tgtcoord = COORDINATE:NewFromVec3(targetdata:getPoint()) + --tgtcoord:MarkToAll("Missile Target",true) + + local tgtgrp1 = self.Samset:FindNearestGroupFromPointVec2(tgtcoord) + local tgtcoord1 = tgtgrp1:GetCoordinate() + --tgtcoord1:MarkToAll("Close target SAM",true) + + local tgtgrp2 = self.Groupset:FindNearestGroupFromPointVec2(tgtcoord) + local tgtcoord2 = tgtgrp2:GetCoordinate() + --tgtcoord2:MarkToAll("Close target SHORAD",true) + + local dist1 = tgtcoord:Get2DDistance(tgtcoord1) + local dist2 = tgtcoord:Get2DDistance(tgtcoord2) + + if dist1 < dist2 then + targetunit = tgtgrp1 + targetcat = Object.Category.UNIT + else + targetunit = tgtgrp2 + targetcat = Object.Category.UNIT + end end --local targetunitname = Unit.getName(targetdata) -- Unit name if targetunit and targetunit:IsAlive() then @@ -519,7 +633,11 @@ do local targetgroup = nil local targetgroupname = "none" if targetcat == Object.Category.UNIT then - targetgroup = targetunit:GetGroup() + if targetunit.ClassName == "UNIT" then + targetgroup = targetunit:GetGroup() + elseif targetunit.ClassName == "GROUP" then + targetgroup = targetunit + end targetgroupname = targetgroup:GetName() -- group name elseif targetcat == Object.Category.STATIC then targetgroup = targetunit @@ -540,6 +658,7 @@ do end end end + return self end -- end diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 2bd6d1e8d..c753a7b0a 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -46,6 +46,7 @@ -- @type WAREHOUSE -- @field #string ClassName Name of the class. -- @field #boolean Debug If true, send debug messages to all. +-- @field #number verbosity Verbosity level. -- @field #string wid Identifier of the warehouse printed before other output to DCS.log file. -- @field #boolean Report If true, send status messages to coalition. -- @field Wrapper.Static#STATIC warehouse The phyical warehouse structure. @@ -57,6 +58,10 @@ -- @field Core.Point#COORDINATE rail Closest point to warehouse on rail. -- @field Core.Zone#ZONE spawnzone Zone in which assets are spawned. -- @field #number uid Unique ID of the warehouse. +-- @field #boolean markerOn If true, markers are displayed on the F10 map. +-- @field Wrapper.Marker#MARKER markerWarehouse Marker warehouse. +-- @field Wrapper.Marker#MARKER markerRoad Road connection. +-- @field Wrapper.Marker#MARKER markerRail Rail road connection. -- @field #number markerid ID of the warehouse marker at the airbase. -- @field #number dTstatus Time interval in seconds of updating the warehouse status and processing new events. Default 30 seconds. -- @field #number queueid Unit id of each request in the queue. Essentially a running number starting at one and incremented when a new request is added. @@ -75,11 +80,14 @@ -- @field #string autosavepath Path where the asset file is saved on auto save. -- @field #string autosavefile File name of the auto asset save file. Default is auto generated from warehouse id and name. -- @field #boolean safeparking If true, parking spots for aircraft are considered as occupied if e.g. a client aircraft is parked there. Default false. --- @field #boolean isunit If true, warehouse is represented by a unit instead of a static. +-- @field #boolean isUnit If `true`, warehouse is represented by a unit instead of a static. +-- @field #boolean isShip If `true`, warehouse is represented by a ship unit. -- @field #number lowfuelthresh Low fuel threshold. Triggers the event AssetLowFuel if for any unit fuel goes below this number. -- @field #boolean respawnafterdestroyed If true, warehouse is respawned after it was destroyed. Assets are kept. -- @field #number respawndelay Delay before respawn in seconds. --- @field #boolean markerOn If true, markers are displayed on the F10 map. +-- @field #number runwaydestroyed Time stamp timer.getAbsTime() when the runway was destroyed. +-- @field #number runwayrepairtime Time in seconds until runway will be repaired after it was destroyed. Default is 3600 sec (one hour). +-- @field Ops.FlightControl#FLIGHTCONTROL flightcontrol Flight control of this warehouse. -- @extends Core.Fsm#FSM --- Have your assets at the right place at the right time - or not! @@ -289,7 +297,7 @@ -- -- Assets of the warehouse can be requested by other MOOSE warehouses. A request will first be scrutinized to check if can be fulfilled at all. If the request is valid, it is -- put into the warehouse queue and processed as soon as possible. --- +-- -- Requested assets spawn in various "Rule of Engagement Rules" (ROE) and Alerts modes. If your assets will cross into dangerous areas, be sure to change these states. You can do this in @{#WAREHOUSE:OnAfterAssetSpawned}(*From, *Event, *To, *group, *asset, *request)) function. -- -- Initial Spawn states is as follows: @@ -341,6 +349,7 @@ -- * @{#WAREHOUSE.Attribute.GROUND_APC} Infantry carriers, in particular Amoured Personell Carrier. This can be used to transport other assets. -- * @{#WAREHOUSE.Attribute.GROUND_TRUCK} Unarmed ground vehicles, which has the DCS "Truck" attribute. -- * @{#WAREHOUSE.Attribute.GROUND_INFANTRY} Ground infantry assets. +-- * @{#WAREHOUSE.Attribute.GROUND_IFV} Ground infantry fighting vehicle. -- * @{#WAREHOUSE.Attribute.GROUND_ARTILLERY} Artillery assets. -- * @{#WAREHOUSE.Attribute.GROUND_TANK} Tanks (modern or old). -- * @{#WAREHOUSE.Attribute.GROUND_TRAIN} Trains. Not that trains are **not** yet properly implemented in DCS and cannot be used currently. @@ -1554,6 +1563,7 @@ WAREHOUSE = { ClassName = "WAREHOUSE", Debug = false, + verbosity = 0, lid = nil, Report = true, warehouse = nil, @@ -1565,7 +1575,6 @@ WAREHOUSE = { rail = nil, spawnzone = nil, uid = nil, - markerid = nil, dTstatus = 30, queueid = 0, stock = {}, @@ -1584,7 +1593,8 @@ WAREHOUSE = { autosavepath = nil, autosavefile = nil, saveparking = false, - isunit = false, + isUnit = false, + isShip = false, lowfuelthresh = 0.15, respawnafterdestroyed=false, respawndelay = nil, @@ -1593,6 +1603,8 @@ WAREHOUSE = { --- Item of the warehouse stock table. -- @type WAREHOUSE.Assetitem -- @field #number uid Unique id of the asset. +-- @field #number wid ID of the warehouse this asset belongs to. +-- @field #number rid Request ID of this asset (if any). -- @field #string templatename Name of the template group. -- @field #table template The spawn template of the group. -- @field DCS#Group.Category category Category of the group. @@ -1614,8 +1626,17 @@ WAREHOUSE = { -- @field #boolean spawned If true, asset was spawned into the cruel world. If false, it is still in stock. -- @field #string spawngroupname Name of the spawned group. -- @field #boolean iscargo If true, asset is cargo. If false asset is transport. Nil if in stock. --- @field #number rid The request ID of this asset. -- @field #boolean arrived If true, asset arrived at its destination. +-- +-- @field #number damage Damage of asset group in percent. +-- @field Ops.AirWing#AIRWING.Payload payload The payload of the asset. +-- @field Ops.OpsGroup#OPSGROUP flightgroup The flightgroup object. +-- @field Ops.Cohort#COHORT cohort The cohort this asset belongs to. +-- @field Ops.Legion#LEGION legion The legion this asset belonts to. +-- @field #string squadname Name of the squadron this asset belongs to. +-- @field #number Treturned Time stamp when asset returned to its legion (airwing, brigade). +-- @field #boolean requested If `true`, asset was requested and cannot be selected by another request. +-- @field #boolean isReserved If `true`, asset was reserved and cannot be selected by another request. --- Item of the warehouse queue table. -- @type WAREHOUSE.Queueitem @@ -1638,6 +1659,7 @@ WAREHOUSE = { -- @field #table transportassets Table of transport carrier assets. Each element of the table is a @{#WAREHOUSE.Assetitem}. -- @field #number transportattribute Attribute of transport assets of type @{#WAREHOUSE.Attribute}. -- @field #number transportcategory Category of transport assets of type @{#WAREHOUSE.Category}. +-- @field #boolean lateActivation Assets are spawned in late activated state. --- Item of the warehouse pending queue table. -- @type WAREHOUSE.Pendingitem @@ -1683,6 +1705,7 @@ WAREHOUSE.Descriptor = { -- @field #string GROUND_APC Infantry carriers, in particular Amoured Personell Carrier. This can be used to transport other assets. -- @field #string GROUND_TRUCK Unarmed ground vehicles, which has the DCS "Truck" attribute. -- @field #string GROUND_INFANTRY Ground infantry assets. +-- @field #string GROUND_IFV Ground infantry fighting vehicle. -- @field #string GROUND_ARTILLERY Artillery assets. -- @field #string GROUND_TANK Tanks (modern or old). -- @field #string GROUND_TRAIN Trains. Not that trains are **not** yet properly implemented in DCS and cannot be used currently. @@ -1709,6 +1732,7 @@ WAREHOUSE.Attribute = { GROUND_APC="Ground_APC", GROUND_TRUCK="Ground_Truck", GROUND_INFANTRY="Ground_Infantry", + GROUND_IFV="Ground_IFV", GROUND_ARTILLERY="Ground_Artillery", GROUND_TANK="Ground_Tank", GROUND_TRAIN="Ground_Train", @@ -1835,41 +1859,50 @@ WAREHOUSE.version="1.0.2" --- The WAREHOUSE constructor. Creates a new WAREHOUSE object from a static object. Parameters like the coalition and country are taken from the static object structure. -- @param #WAREHOUSE self --- @param Wrapper.Static#STATIC warehouse The physical structure representing the warehouse. --- @param #string alias (Optional) Alias of the warehouse, i.e. the name it will be called when sending messages etc. Default is the name of the static +-- @param Wrapper.Static#STATIC warehouse The physical structure representing the warehouse. Can also be a @{Wrapper.Unit#UNIT}. +-- @param #string alias (Optional) Alias of the warehouse, i.e. the name it will be called when sending messages etc. Default is the name of the static/unit representing the warehouse. -- @return #WAREHOUSE self function WAREHOUSE:New(warehouse, alias) + -- Inherit everthing from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #WAREHOUSE + -- Check if just a string was given and convert to static. if type(warehouse)=="string" then - local warehousename=warehouse + local warehousename=warehouse warehouse=UNIT:FindByName(warehousename) if warehouse==nil then - --env.info(string.format("No warehouse unit with name %s found trying static.", tostring(warehousename))) warehouse=STATIC:FindByName(warehousename, true) - self.isunit=false - else - self.isunit=true end end -- Nil check. if warehouse==nil then - BASE:E("ERROR: Warehouse does not exist!") + env.error("ERROR: Warehouse does not exist!") return nil end + + -- Check if we have a STATIC or UNIT object. + if warehouse:IsInstanceOf("STATIC") then + self.isUnit=false + elseif warehouse:IsInstanceOf("UNIT") then + self.isUnit=true + if warehouse:IsShip() then + self.isShip=true + end + else + env.error("ERROR: Warehouse is neither STATIC nor UNIT object!") + return nil + end -- Set alias. self.alias=alias or warehouse:GetName() - -- Print version. - env.info(string.format("Adding warehouse v%s for structure %s with alias %s", WAREHOUSE.version, warehouse:GetName(), self.alias)) - - -- Inherit everthing from FSM class. - local self = BASE:Inherit(self, FSM:New()) -- #WAREHOUSE - -- Set some string id for output to DCS.log file. self.lid=string.format("WAREHOUSE %s | ", self.alias) + + -- Print version. + self:I(self.lid..string.format("Adding warehouse v%s for structure %s [isUnit=%s, isShip=%s]", WAREHOUSE.version, warehouse:GetName(), tostring(self:IsUnit()), tostring(self:IsShip()))) -- Set some variables. self.warehouse=warehouse @@ -1879,10 +1912,10 @@ function WAREHOUSE:New(warehouse, alias) -- Set unique ID for this warehouse. self.uid=_WAREHOUSEDB.WarehouseID - + -- Coalition of the warehouse. self.coalition=self.warehouse:GetCoalition() - + -- Country of the warehouse. self.countryid=self.warehouse:GetCountry() @@ -1893,11 +1926,20 @@ function WAREHOUSE:New(warehouse, alias) end -- Define warehouse and default spawn zone. - self.zone=ZONE_RADIUS:New(string.format("Warehouse zone %s", self.warehouse:GetName()), warehouse:GetVec2(), 500) - self.spawnzone=ZONE_RADIUS:New(string.format("Warehouse %s spawn zone", self.warehouse:GetName()), warehouse:GetVec2(), 250) - + if self.isShip then + self.zone=ZONE_AIRBASE:New(self.warehouse:GetName(), 1000) + self.spawnzone=ZONE_AIRBASE:New(self.warehouse:GetName(), 1000) + else + self.zone=ZONE_RADIUS:New(string.format("Warehouse zone %s", self.warehouse:GetName()), warehouse:GetVec2(), 500) + self.spawnzone=ZONE_RADIUS:New(string.format("Warehouse %s spawn zone", self.warehouse:GetName()), warehouse:GetVec2(), 250) + end + + -- Defaults self:SetMarker(true) + self:SetReportOff() + self:SetRunwayRepairtime() + self.allowSpawnOnClientSpots=false -- Add warehouse to database. _WAREHOUSEDB.Warehouses[self.uid]=self @@ -1913,26 +1955,26 @@ function WAREHOUSE:New(warehouse, alias) -- From State --> Event --> To State self:AddTransition("NotReadyYet", "Load", "Loaded") -- Load the warehouse state from scatch. self:AddTransition("Stopped", "Load", "Loaded") -- Load the warehouse state stopped state. - + self:AddTransition("NotReadyYet", "Start", "Running") -- Start the warehouse from scratch. self:AddTransition("Loaded", "Start", "Running") -- Start the warehouse when loaded from disk. - + self:AddTransition("*", "Status", "*") -- Status update. - + self:AddTransition("*", "AddAsset", "*") -- Add asset to warehouse stock. self:AddTransition("*", "NewAsset", "*") -- New asset was added to warehouse stock. - + self:AddTransition("*", "AddRequest", "*") -- New request from other warehouse. self:AddTransition("Running", "Request", "*") -- Process a request. Only in running mode. self:AddTransition("Running", "RequestSpawned", "*") -- Assets of request were spawned. self:AddTransition("Attacked", "Request", "*") -- Process a request. Only in running mode. - + self:AddTransition("*", "Unloaded", "*") -- Cargo has been unloaded from the carrier (unused ==> unnecessary?). self:AddTransition("*", "AssetSpawned", "*") -- Asset has been spawned into the world. self:AddTransition("*", "AssetLowFuel", "*") -- Asset is low on fuel. - + self:AddTransition("*", "Arrived", "*") -- Cargo or transport group has arrived. - + self:AddTransition("*", "Delivered", "*") -- All cargo groups of a request have been delivered to the requesting warehouse. self:AddTransition("Running", "SelfRequest", "*") -- Request to warehouse itself. Requested assets are only spawned but not delivered anywhere. self:AddTransition("Attacked", "SelfRequest", "*") -- Request to warehouse itself. Also possible when warehouse is under attack! @@ -1948,6 +1990,8 @@ function WAREHOUSE:New(warehouse, alias) self:AddTransition("Attacked", "Captured", "Running") -- Warehouse was captured by another coalition. It must have been attacked first. self:AddTransition("*", "AirbaseCaptured", "*") -- Airbase was captured by other coalition. self:AddTransition("*", "AirbaseRecaptured", "*") -- Airbase was re-captured from other coalition. + self:AddTransition("*", "RunwayDestroyed", "*") -- Runway of the airbase was destroyed. + self:AddTransition("*", "RunwayRepaired", "*") -- Runway of the airbase was repaired. self:AddTransition("*", "AssetDead", "*") -- An asset group died. self:AddTransition("*", "Destroyed", "Destroyed") -- Warehouse was destroyed. All assets in stock are gone and warehouse is stopped. self:AddTransition("Destroyed", "Respawn", "Running") -- Respawn warehouse after it was destroyed. @@ -2541,6 +2585,14 @@ function WAREHOUSE:SetSafeParkingOff() return self end +--- Set wether client parking spots can be used for spawning. +-- @param #WAREHOUSE self +-- @return #WAREHOUSE self +function WAREHOUSE:SetAllowSpawnOnClientParking() + self.allowSpawnOnClientSpots=true + return self +end + --- Set low fuel threshold. If one unit of an asset has less fuel than this number, the event AssetLowFuel will be fired. -- @param #WAREHOUSE self -- @param #number threshold Relative low fuel threshold, i.e. a number in [0,1]. Default 0.15 (15%). @@ -2559,6 +2611,15 @@ function WAREHOUSE:SetStatusUpdate(timeinterval) return self end +--- Set verbosity level. +-- @param #WAREHOUSE self +-- @param #number VerbosityLevel Level of output (higher=more). Default 0. +-- @return #WAREHOUSE self +function WAREHOUSE:SetVerbosityLevel(VerbosityLevel) + self.verbosity=VerbosityLevel or 0 + return self +end + --- Set a zone where the (ground) assets of the warehouse are spawned once requested. -- @param #WAREHOUSE self -- @param Core.Zone#ZONE zone The spawn zone. @@ -2570,6 +2631,12 @@ function WAREHOUSE:SetSpawnZone(zone, maxdist) return self end +--- Get the spawn zone. +-- @param #WAREHOUSE self +-- @return Core.Zone#ZONE The spawn zone. +function WAREHOUSE:GetSpawnZone() + return self.spawnzone +end --- Set a warehouse zone. If this zone is captured, the warehouse and all its assets fall into the hands of the enemy. -- @param #WAREHOUSE self @@ -2611,9 +2678,8 @@ end --- Check parking ID. -- @param #WAREHOUSE self -- @param Wrapper.Airbase#AIRBASE.ParkingSpot spot Parking spot. --- @param Wrapper.Airbase#AIRBASE airbase The airbase. -- @return #boolean If true, parking is valid. -function WAREHOUSE:_CheckParkingValid(spot, airbase) +function WAREHOUSE:_CheckParkingValid(spot) if self.parkingIDs==nil then return true @@ -2628,6 +2694,25 @@ function WAREHOUSE:_CheckParkingValid(spot, airbase) 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 @@ -3064,6 +3149,23 @@ 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() + local vec3=self.warehouse:GetVec3() + return vec3 +end + +--- Get 2D vector of warehouse static. +-- @param #WAREHOUSE self +-- @return DCS#Vec2 The 2D vector of the warehouse. +function WAREHOUSE:GetVec2() + local vec2=self.warehouse:GetVec2() + return vec2 +end + + --- Get coalition side of warehouse static. -- @param #WAREHOUSE self -- @return #number Coalition side, i.e. number of @{DCS#coalition.side}. @@ -3129,18 +3231,6 @@ function WAREHOUSE:GetAssignment(request) return tostring(request.assignment) end ---[[ ---- Get warehouse unique ID from static warehouse object. This is the ID under which you find the @{#WAREHOUSE} object in the global data base. --- @param #WAREHOUSE self --- @param #string staticname Name of the warehouse static object. --- @return #number Warehouse unique ID. -function WAREHOUSE:GetWarehouseID(staticname) - local warehouse=STATIC:FindByName(staticname, true) - local uid=tonumber(warehouse:GetID()) - return uid -end -]] - --- Find a warehouse in the global warehouse data base. -- @param #WAREHOUSE self -- @param #number uid The unique ID of the warehouse. @@ -3254,6 +3344,65 @@ function WAREHOUSE:FindAssetInDB(group) return nil end +--- Check if runway is operational. +-- @param #WAREHOUSE self +-- @return #boolean If `true`, runway is operational. +function WAREHOUSE:IsRunwayOperational() + if self.airbase then + if self.runwaydestroyed then + return false + else + return true + end + end + return nil +end + +--- Set the time until the runway(s) of an airdrome are repaired after it has been destroyed. +-- Note that this is the time, the DCS engine uses not something we can control on a user level or we could get via scripting. +-- You need to input the value. On the DCS forum it was stated that this is currently one hour. Hence this is the default value. +-- @param #WAREHOUSE self +-- @param #number RepairTime Time in seconds until the runway is repaired. Default 3600 sec (one hour). +-- @return #WAREHOUSE self +function WAREHOUSE:SetRunwayRepairtime(RepairTime) + self.runwayrepairtime=RepairTime or 3600 + return self +end + +--- Check if runway is operational. +-- @param #WAREHOUSE self +-- @return #number Time in seconds until the runway is repaired. Will return 0 if runway is repaired. +function WAREHOUSE:GetRunwayRepairtime() + if self.runwaydestroyed then + local Tnow=timer.getAbsTime() + local Tsince=Tnow-self.runwaydestroyed + local Trepair=math.max(self.runwayrepairtime-Tsince, 0) + return Trepair + end + return 0 +end + +--- Check if warehouse physical representation is a unit (not a static) object. +-- @param #WAREHOUSE self +-- @return #boolean If `true`, warehouse object is a unit. +function WAREHOUSE:IsUnit() + return self.isUnit +end + +--- Check if warehouse physical representation is a static (not a unit) object. +-- @param #WAREHOUSE self +-- @return #boolean If `true`, warehouse object is a static. +function WAREHOUSE:IsStatic() + return not self.isUnit +end + +--- Check if warehouse physical representation is a ship. +-- @param #WAREHOUSE self +-- @return #boolean If `true`, warehouse object is a ship. +function WAREHOUSE:IsShip() + return self.isShip +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- FSM states ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -3275,16 +3424,6 @@ function WAREHOUSE:onafterStart(From, Event, To) -- Save self in static object. Easier to retrieve later. self.warehouse:SetState(self.warehouse, "WAREHOUSE", self) - -- THIS! caused aircraft to be spawned and started but they would never begin their route! - -- VERY strange. Need to test more. - --[[ - -- Debug mark warehouse & spawn zone. - self.zone:BoundZone(30, self.country) - self.spawnzone:BoundZone(30, self.country) - ]] - - --self.spawnzone:GetCoordinate():MarkToCoalition(string.format("Warehouse %s spawn zone", self.alias), self:GetCoalition()) - -- Get the closest point on road wrt spawnzone of ground assets. local _road=self.spawnzone:GetCoordinate():GetClosestPointToRoad() if _road and self.road==nil then @@ -3394,7 +3533,7 @@ function WAREHOUSE:onafterStop(From, Event, To) self:_UpdateWarehouseMarkText() -- Clear all pending schedules. - --self.CallScheduler:Clear() + self.CallScheduler:Clear() end --- On after "Pause" event. Pauses the warehouse, i.e. no requests are processed. However, new requests and new assets can be added in this state. @@ -3424,13 +3563,17 @@ end -- @param #string To To state. function WAREHOUSE:onafterStatus(From, Event, To) - local FSMstate=self:GetState() + -- General info. + if self.verbosity>=1 then - local coalition=self:GetCoalitionName() - local country=self:GetCountryName() - - -- Info. - self:I(self.lid..string.format("State=%s %s [%s]: Assets=%d, Requests: waiting=%d, pending=%d", FSMstate, country, coalition, #self.stock, #self.queue, #self.pending)) + local FSMstate=self:GetState() + + local coalition=self:GetCoalitionName() + local country=self:GetCountryName() + + -- Info. + self:I(self.lid..string.format("State=%s %s [%s]: Assets=%d, Requests: waiting=%d, pending=%d", FSMstate, country, coalition, #self.stock, #self.queue, #self.pending)) + end -- Check if any pending jobs are done and can be deleted from the queue. self:_JobDone() @@ -3440,6 +3583,14 @@ function WAREHOUSE:onafterStatus(From, Event, To) -- Check if warehouse is being attacked or has even been captured. self:_CheckConquered() + + if self:IsRunwayOperational()==false then + local Trepair=self:GetRunwayRepairtime() + self:I(self.lid..string.format("Runway destroyed! Will be repaired in %d sec", Trepair)) + if Trepair==0 then + self:RunwayRepaired() + end + end -- Check if requests are valid and remove invalid one. self:_CheckRequestConsistancy(self.queue) @@ -3462,7 +3613,7 @@ function WAREHOUSE:onafterStatus(From, Event, To) self:_PrintQueue(self.pending, "Queue pending") -- Check fuel for all assets. - self:_CheckFuel() + --self:_CheckFuel() -- Update warhouse marker on F10 map. self:_UpdateWarehouseMarkText() @@ -3487,170 +3638,174 @@ function WAREHOUSE:_JobDone() -- Loop over all pending requests of this warehouse. for _,request in pairs(self.pending) do local request=request --#WAREHOUSE.Pendingitem + + if request.born then - -- Count number of cargo groups. - local ncargo=0 - if request.cargogroupset then - ncargo=request.cargogroupset:Count() - end - - -- Count number of transport groups (if any). - local ntransport=0 - if request.transportgroupset then - ntransport=request.transportgroupset:Count() - end - - local ncargotot=request.nasset - local ncargodelivered=request.ndelivered - - -- Dead cargo: Ndead=Ntot-Ndeliverd-Nalive, - local ncargodead=ncargotot-ncargodelivered-ncargo - - - local ntransporttot=request.ntransport - local ntransporthome=request.ntransporthome - - -- Dead transport: Ndead=Ntot-Nhome-Nalive. - local ntransportdead=ntransporttot-ntransporthome-ntransport - - local text=string.format("Request id=%d: Cargo: Ntot=%d, Nalive=%d, Ndelivered=%d, Ndead=%d | Transport: Ntot=%d, Nalive=%d, Nhome=%d, Ndead=%d", - request.uid, ncargotot, ncargo, ncargodelivered, ncargodead, ntransporttot, ntransport, ntransporthome, ntransportdead) - self:T(self.lid..text) - - - -- Handle different cases depending on what asset are still around. - if ncargo==0 then - --------------------- - -- Cargo delivered -- - --------------------- - - -- Trigger delivered event. - if not self.delivered[request.uid] then - self:Delivered(request) + -- Count number of cargo groups. + local ncargo=0 + if request.cargogroupset then + ncargo=request.cargogroupset:Count() end - - -- Check if transports are back home? - if ntransport==0 then - --------------- - -- Job done! -- - --------------- - - -- Info on job. - local text=string.format("Warehouse %s: Job on request id=%d for warehouse %s done!\n", self.alias, request.uid, request.warehouse.alias) - text=text..string.format("- %d of %d assets delivered. Casualties %d.", ncargodelivered, ncargotot, ncargodead) - if request.ntransport>0 then - text=text..string.format("\n- %d of %d transports returned home. Casualties %d.", ntransporthome, ntransporttot, ntransportdead) + + -- Count number of transport groups (if any). + local ntransport=0 + if request.transportgroupset then + ntransport=request.transportgroupset:Count() + end + + local ncargotot=request.nasset + local ncargodelivered=request.ndelivered + + -- Dead cargo: Ndead=Ntot-Ndeliverd-Nalive, + local ncargodead=ncargotot-ncargodelivered-ncargo + + + local ntransporttot=request.ntransport + local ntransporthome=request.ntransporthome + + -- Dead transport: Ndead=Ntot-Nhome-Nalive. + local ntransportdead=ntransporttot-ntransporthome-ntransport + + local text=string.format("Request id=%d: Cargo: Ntot=%d, Nalive=%d, Ndelivered=%d, Ndead=%d | Transport: Ntot=%d, Nalive=%d, Nhome=%d, Ndead=%d", + request.uid, ncargotot, ncargo, ncargodelivered, ncargodead, ntransporttot, ntransport, ntransporthome, ntransportdead) + self:T(self.lid..text) + + + -- Handle different cases depending on what asset are still around. + if ncargo==0 then + --------------------- + -- Cargo delivered -- + --------------------- + + -- Trigger delivered event. + if not self.delivered[request.uid] then + self:Delivered(request) end - self:_InfoMessage(text, 20) - - -- Mark request for deletion. - table.insert(done, request) - + + -- Check if transports are back home? + if ntransport==0 then + --------------- + -- Job done! -- + --------------- + + -- Info on job. + if self.verbosity>=1 then + local text=string.format("Warehouse %s: Job on request id=%d for warehouse %s done!\n", self.alias, request.uid, request.warehouse.alias) + text=text..string.format("- %d of %d assets delivered. Casualties %d.", ncargodelivered, ncargotot, ncargodead) + if request.ntransport>0 then + text=text..string.format("\n- %d of %d transports returned home. Casualties %d.", ntransporthome, ntransporttot, ntransportdead) + end + self:_InfoMessage(text, 20) + end + + -- Mark request for deletion. + table.insert(done, request) + + else + ----------------------------------- + -- No cargo but still transports -- + ----------------------------------- + + -- This is difficult! How do I know if transports were unused? They could also be just on their way back home. + -- ==> Need to do a lot of checks. + + -- All transports are dead but there is still cargo left ==> Put cargo back into stock. + for _,_group in pairs(request.transportgroupset:GetSetObjects()) do + local group=_group --Wrapper.Group#GROUP + + -- Check if group is alive. + if group and group:IsAlive() then + + -- Check if group is in the spawn zone? + local category=group:GetCategory() + + -- Get current speed. + local speed=group:GetVelocityKMH() + local notmoving=speed<1 + + -- Closest airbase. + local airbase=group:GetCoordinate():GetClosestAirbase():GetName() + local athomebase=self.airbase and self.airbase:GetName()==airbase + + -- On ground + local onground=not group:InAir() + + -- In spawn zone. + local inspawnzone=group:IsPartlyOrCompletelyInZone(self.spawnzone) + + -- Check conditions for being back home. + local ishome=false + if category==Group.Category.GROUND or category==Group.Category.HELICOPTER then + -- Units go back to the spawn zone, helicopters land and they should not move any more. + ishome=inspawnzone and onground and notmoving + elseif category==Group.Category.AIRPLANE then + -- Planes need to be on ground at their home airbase and should not move any more. + ishome=athomebase and onground and notmoving + end + + -- Debug text. + local text=string.format("Group %s: speed=%d km/h, onground=%s , airbase=%s, spawnzone=%s ==> ishome=%s", group:GetName(), speed, tostring(onground), airbase, tostring(inspawnzone), tostring(ishome)) + self:T(self.lid..text) + + if ishome then + + -- Info message. + local text=string.format("Warehouse %s: Transport group arrived back home and no cargo left for request id=%d.\nSending transport group %s back to stock.", self.alias, request.uid, group:GetName()) + self:T(self.lid..text) + + -- Debug smoke. + if self.Debug then + group:SmokeRed() + end + + -- Group arrived. + self:Arrived(group) + end + end + end + + end + else - ----------------------------------- - -- No cargo but still transports -- - ----------------------------------- - - -- This is difficult! How do I know if transports were unused? They could also be just on their way back home. - -- ==> Need to do a lot of checks. - - -- All transports are dead but there is still cargo left ==> Put cargo back into stock. - for _,_group in pairs(request.transportgroupset:GetSetObjects()) do - local group=_group --Wrapper.Group#GROUP - - -- Check if group is alive. - if group and group:IsAlive() then - - -- Check if group is in the spawn zone? - local category=group:GetCategory() - - -- Get current speed. - local speed=group:GetVelocityKMH() - local notmoving=speed<1 - - -- Closest airbase. - local airbase=group:GetCoordinate():GetClosestAirbase():GetName() - local athomebase=self.airbase and self.airbase:GetName()==airbase - - -- On ground - local onground=not group:InAir() - - -- In spawn zone. - local inspawnzone=group:IsPartlyOrCompletelyInZone(self.spawnzone) - - -- Check conditions for being back home. - local ishome=false - if category==Group.Category.GROUND or category==Group.Category.HELICOPTER then - -- Units go back to the spawn zone, helicopters land and they should not move any more. - ishome=inspawnzone and onground and notmoving - elseif category==Group.Category.AIRPLANE then - -- Planes need to be on ground at their home airbase and should not move any more. - ishome=athomebase and onground and notmoving - end - - -- Debug text. - local text=string.format("Group %s: speed=%d km/h, onground=%s , airbase=%s, spawnzone=%s ==> ishome=%s", group:GetName(), speed, tostring(onground), airbase, tostring(inspawnzone), tostring(ishome)) - self:I(self.lid..text) - - if ishome then - - -- Info message. - local text=string.format("Warehouse %s: Transport group arrived back home and no cargo left for request id=%d.\nSending transport group %s back to stock.", self.alias, request.uid, group:GetName()) - self:_InfoMessage(text) - - -- Debug smoke. - if self.Debug then - group:SmokeRed() + + if ntransport==0 and request.ntransport>0 then + ----------------------------------- + -- Still cargo but no transports -- + ----------------------------------- + + local ncargoalive=0 + + -- All transports are dead but there is still cargo left ==> Put cargo back into stock. + for _,_group in pairs(request.cargogroupset:GetSetObjects()) do + --local group=group --Wrapper.Group#GROUP + + -- These groups have been respawned as cargo, i.e. their name changed! + local groupname=_group:GetName() + local group=GROUP:FindByName(groupname.."#CARGO") + + -- Check if group is alive. + if group and group:IsAlive() then + + -- Check if group is in spawn zone? + if group:IsPartlyOrCompletelyInZone(self.spawnzone) then + -- Debug smoke. + if self.Debug then + group:SmokeBlue() + end + -- Add asset group back to stock. + self:AddAsset(group) + ncargoalive=ncargoalive+1 end - - -- Group arrived. - self:Arrived(group) end + end + + -- Info message. + self:_InfoMessage(string.format("Warehouse %s: All transports of request id=%s dead! Putting remaining %s cargo assets back into warehouse!", self.alias, request.uid, ncargoalive)) end - + end - - else - - if ntransport==0 and request.ntransport>0 then - ----------------------------------- - -- Still cargo but no transports -- - ----------------------------------- - - local ncargoalive=0 - - -- All transports are dead but there is still cargo left ==> Put cargo back into stock. - for _,_group in pairs(request.cargogroupset:GetSetObjects()) do - --local group=group --Wrapper.Group#GROUP - - -- These groups have been respawned as cargo, i.e. their name changed! - local groupname=_group:GetName() - local group=GROUP:FindByName(groupname.."#CARGO") - - -- Check if group is alive. - if group and group:IsAlive() then - - -- Check if group is in spawn zone? - if group:IsPartlyOrCompletelyInZone(self.spawnzone) then - -- Debug smoke. - if self.Debug then - group:SmokeBlue() - end - -- Add asset group back to stock. - self:AddAsset(group) - ncargoalive=ncargoalive+1 - end - end - - end - - -- Info message. - self:_InfoMessage(string.format("Warehouse %s: All transports of request id=%s dead! Putting remaining %s cargo assets back into warehouse!", self.alias, request.uid, ncargoalive)) - end - - end - + end -- born check end -- loop over requests -- Remove pending requests if done. @@ -3774,7 +3929,7 @@ function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribu local wid,aid,rid=self:_GetIDsFromGroup(group) if wid and aid and rid then - + --------------------------- -- This is a KNOWN asset -- --------------------------- @@ -3790,7 +3945,7 @@ function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribu -- Increase number of cargo delivered and transports home. local istransport=warehouse:_GroupIsTransport(group,request) - + if istransport==true then request.ntransporthome=request.ntransporthome+1 request.transportgroupset:Remove(group:GetName(), true) @@ -3800,7 +3955,7 @@ function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribu request.ndelivered=request.ndelivered+1 local namewo=self:_GetNameWithOut(group) request.cargogroupset:Remove(namewo, true) - local ncargo=request.cargogroupset:Count() + local ncargo=request.cargogroupset:Count() self:T2(warehouse.lid..string.format("Cargo %s: %d of %s delivered. CargoSet=%d", namewo, request.ndelivered, tostring(request.nasset), ncargo)) else self:E(warehouse.lid..string.format("WARNING: Group %s is neither cargo nor transport! Need to investigate...", group:GetName())) @@ -3830,10 +3985,10 @@ function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribu asset.livery=liveries end end - + -- Set skill. asset.skill=skill or asset.skill - + -- Asset now belongs to this warehouse. Set warehouse ID. asset.wid=self.uid @@ -3842,8 +3997,15 @@ function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribu -- Asset is not spawned. asset.spawned=false + asset.requested=false + asset.isReserved=false asset.iscargo=nil asset.arrived=nil + + -- Destroy group if it is alive. + if group:IsAlive()==true then + asset.damage=asset.life0-group:GetLife() + end -- Add asset to stock. table.insert(self.stock, asset) @@ -3855,7 +4017,7 @@ function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribu end else - + ------------------------- -- This is a NEW asset -- ------------------------- @@ -3887,8 +4049,21 @@ function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribu -- Destroy group if it is alive. if group:IsAlive()==true then self:_DebugMessage(string.format("Removing group %s", group:GetName()), 5) - -- Setting parameter to false, i.e. creating NO dead or remove unit event, seems to not confuse the dispatcher logic. - group:Destroy(false) + + local opsgroup=_DATABASE:GetOpsGroup(group:GetName()) + if opsgroup then + opsgroup:Despawn(0, true) + opsgroup:__Stop(-0.01) + else + -- Setting parameter to false, i.e. creating NO dead or remove unit event, seems to not confuse the dispatcher logic. + -- TODO: It would be nice, however, to have the remove event. + group:Destroy() --(false) + end + else + local opsgroup=_DATABASE:GetOpsGroup(group:GetName()) + if opsgroup then + opsgroup:Stop() + end end else @@ -3944,6 +4119,7 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, local cargobay={} local cargobaytot=0 local cargobaymax=0 + local weights={} for _i,_unit in pairs(group:GetUnits()) do local unit=_unit --Wrapper.Unit#UNIT local Desc=unit:GetDesc() @@ -3952,8 +4128,9 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, local unitweight=forceweight or Desc.massEmpty if unitweight then weight=weight+unitweight + weights[_i]=unitweight end - + local cargomax=0 local massfuel=Desc.fuelMassMax or 0 local massempty=Desc.massEmpty or 0 @@ -4002,6 +4179,7 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, asset.speedmax=SpeedMax asset.size=smax asset.weight=weight + asset.weights=weights asset.DCSdesc=Descriptors asset.attribute=attribute asset.cargobay=cargobay @@ -4014,6 +4192,10 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, asset.skill=skill asset.assignment=assignment asset.spawned=false + asset.requested=false + asset.isReserved=false + asset.life0=group:GetLife0() + asset.damage=0 asset.spawngroupname=string.format("%s_AID-%d", templategroupname, asset.uid) if i==1 then @@ -4050,7 +4232,7 @@ function WAREHOUSE:_AssetItemInfo(asset) text=text..string.format("Cargo bay max = %5.2f kg\n", asset.cargobaymax) text=text..string.format("Load radius = %s m\n", tostring(asset.loadradius)) text=text..string.format("Skill = %s\n", tostring(asset.skill)) - text=text..string.format("Livery = %s", tostring(asset.livery)) + text=text..string.format("Livery = %s", tostring(asset.livery)) self:I(self.lid..text) self:T({DCSdesc=asset.DCSdesc}) self:T3({Template=asset.template}) @@ -4136,7 +4318,7 @@ function WAREHOUSE:onbeforeAddRequest(From, Event, To, warehouse, AssetDescripto self:_ErrorMessage("ERROR: Invalid request. Asset assignment type must be passed as a string!", 5) okay=false end - + elseif AssetDescriptor==WAREHOUSE.Descriptor.ASSETLIST then if type(AssetDescriptorValue)~="table" then @@ -4221,9 +4403,16 @@ function WAREHOUSE:onafterAddRequest(From, Event, To, warehouse, AssetDescriptor -- Add request to queue. table.insert(self.queue, request) + + local descval="assetlist" + if request.assetdesc==WAREHOUSE.Descriptor.ASSETLIST then + + else + descval=tostring(request.assetdescval) + end - local text=string.format("Warehouse %s: New request from warehouse %s.\nDescriptor %s=%s, #assets=%s; Transport=%s, #transports =%s.", - self.alias, warehouse.alias, request.assetdesc, tostring(request.assetdescval), tostring(request.nasset), request.transporttype, tostring(request.ntransport)) + local text=string.format("Warehouse %s: New request from warehouse %s.\nDescriptor %s=%s, #assets=%s; Transport=%s, #transports=%s.", + self.alias, warehouse.alias, request.assetdesc, descval, tostring(request.nasset), request.transporttype, tostring(request.ntransport)) self:_DebugMessage(text, 5) end @@ -4263,7 +4452,7 @@ function WAREHOUSE:onbeforeRequest(From, Event, To, Request) -- Delete request from queue because it will never be possible. -- Unless(!) at least one is a moving warehouse, which could, e.g., be an aircraft carrier. - if not (self.isunit or Request.warehouse.isunit) then + if not (self.isUnit or Request.warehouse.isUnit) then self:_DeleteQueueItem(Request, self.queue) end @@ -4285,10 +4474,12 @@ end function WAREHOUSE:onafterRequest(From, Event, To, Request) -- Info message. - local text=string.format("Warehouse %s: Processing request id=%d from warehouse %s.\n", self.alias, Request.uid, Request.warehouse.alias) - text=text..string.format("Requested %s assets of %s=%s.\n", tostring(Request.nasset), Request.assetdesc, Request.assetdesc==WAREHOUSE.Descriptor.ASSETLIST and "Asset list" or Request.assetdescval) - text=text..string.format("Transports %s of type %s.", tostring(Request.ntransport), tostring(Request.transporttype)) - self:_InfoMessage(text, 5) + if self.verbosity>=1 then + local text=string.format("Warehouse %s: Processing request id=%d from warehouse %s.\n", self.alias, Request.uid, Request.warehouse.alias) + text=text..string.format("Requested %s assets of %s=%s.\n", tostring(Request.nasset), Request.assetdesc, Request.assetdesc==WAREHOUSE.Descriptor.ASSETLIST and "Asset list" or Request.assetdescval) + text=text..string.format("Transports %s of type %s.", tostring(Request.ntransport), tostring(Request.transporttype)) + self:_InfoMessage(text, 5) + end ------------------------------------------------------------------------------------------------------------------------------------ -- Cargo assets. @@ -4338,7 +4529,7 @@ function WAREHOUSE:onafterRequest(From, Event, To, Request) _assetitem.arrived=false local spawngroup=nil --Wrapper.Group#GROUP - + -- Add asset by id to all assets table. Request.assets[_assetitem.uid]=_assetitem @@ -4378,6 +4569,11 @@ function WAREHOUSE:onafterRequest(From, Event, To, Request) return end + -- Trigger event. + if spawngroup then + self:__AssetSpawned(0.01, spawngroup, _assetitem, Request) + end + end -- Init problem table. @@ -4792,8 +4988,15 @@ end 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 @@ -4801,8 +5004,9 @@ 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 --- On after "Arrived" event. Triggered when a group has arrived at its destination warehouse. @@ -4846,23 +5050,9 @@ function WAREHOUSE:onafterArrived(From, Event, To, group) if group:IsGround() and group:GetSpeedMax()>1 then group:RouteGroundTo(warehouse:GetCoordinate(), group:GetSpeedMax()*0.3, "Off Road") end - - -- NOTE: This is done in the AddAsset() function. Dont know, why we do it also here. - --[[ - if istransport==true then - request.ntransporthome=request.ntransporthome+1 - request.transportgroupset:Remove(group:GetName(), true) - self:T2(warehouse.lid..string.format("Transport %d of %s returned home.", request.ntransporthome, tostring(request.ntransport))) - elseif istransport==false then - request.ndelivered=request.ndelivered+1 - request.cargogroupset:Remove(self:_GetNameWithOut(group), true) - self:T2(warehouse.lid..string.format("Cargo %d of %s delivered.", request.ndelivered, tostring(request.nasset))) - else - self:E(warehouse.lid..string.format("ERROR: Group %s is neither cargo nor transport!", group:GetName())) - end - ]] - + -- Move asset from pending queue into new warehouse. + self:T(self.lid.."Asset arrived at warehouse adding in 60 sec") warehouse:__AddAsset(60, group) end @@ -4877,8 +5067,10 @@ end function WAREHOUSE:onafterDelivered(From, Event, To, request) -- Debug info - local text=string.format("Warehouse %s: All assets delivered to warehouse %s!", self.alias, request.warehouse.alias) - self:_InfoMessage(text, 5) + if self.verbosity>=1 then + local text=string.format("Warehouse %s: All assets delivered to warehouse %s!", self.alias, request.warehouse.alias) + self:_InfoMessage(text, 5) + end -- Make some noise :) if self.Debug then @@ -5068,15 +5260,15 @@ function WAREHOUSE:onafterChangeCountry(From, Event, To, Country) -- Delete all waiting requests because they are not valid any more. self.queue=nil self.queue={} - + if self.airbasename then - + -- Get airbase of this warehouse. local airbase=AIRBASE:FindByName(self.airbasename) - + -- Get coalition of the airbase. - local airbaseCoalition=airbase:GetCoalition() - + local airbaseCoalition=airbase:GetCoalition() + if CoalitionNew==airbaseCoalition then -- Airbase already owned by the coalition that captured the warehouse. Airbase can be used by this warehouse. self.airbase=airbase @@ -5084,7 +5276,7 @@ function WAREHOUSE:onafterChangeCountry(From, Event, To, Country) -- Airbase is owned by other coalition. So this warehouse does not have an airbase until it is captured. self.airbase=nil end - + end -- Debug smoke. @@ -5124,7 +5316,7 @@ function WAREHOUSE:onafterCaptured(From, Event, To, Coalition, Country) -- Message. local text=string.format("Warehouse %s: We were captured by enemy coalition (side=%d)!", self.alias, Coalition) self:_InfoMessage(text) - + end @@ -5179,24 +5371,37 @@ function WAREHOUSE:onafterAirbaseRecaptured(From, Event, To, Coalition) end ---- On before "AssetSpawned" event. Checks whether the asset was already set to "spawned" for groups with multiple units. +--- On after "RunwayDestroyed" event. -- @param #WAREHOUSE self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param Wrapper.Group#GROUP group The group spawned. --- @param #WAREHOUSE.Assetitem asset The asset that is dead. --- @param #WAREHOUSE.Pendingitem request The request of the dead asset. -function WAREHOUSE:onbeforeAssetSpawned(From, Event, To, group, asset, request) - if asset.spawned then - --return false - else - --return true - end - - return true +function WAREHOUSE:onafterRunwayDestroyed(From, Event, To) + + -- Message. + local text=string.format("Warehouse %s: Runway %s destroyed!", self.alias, self.airbasename) + self:_InfoMessage(text) + + self.runwaydestroyed=timer.getAbsTime() + end +--- On after "RunwayRepaired" event. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function WAREHOUSE:onafterRunwayRepaired(From, Event, To) + + -- Message. + local text=string.format("Warehouse %s: Runway %s repaired!", self.alias, self.airbasename) + self:_InfoMessage(text) + + self.runwaydestroyed=nil + +end + + --- On after "AssetSpawned" event triggered when an asset group is spawned into the cruel world. -- @param #WAREHOUSE self -- @param #string From From state. @@ -5207,10 +5412,28 @@ end -- @param #WAREHOUSE.Pendingitem request The request of the dead asset. function WAREHOUSE:onafterAssetSpawned(From, Event, To, group, asset, request) local text=string.format("Asset %s from request id=%d was spawned!", asset.spawngroupname, request.uid) - self:I(self.lid..text) + self:T(self.lid..text) -- Sete asset state to spawned. asset.spawned=true + + -- Set spawn group name. + asset.spawngroupname=group:GetName() + + -- Remove asset from stock. + self:_DeleteStockItem(asset) + + -- Add group. + if asset.iscargo==true then + request.cargogroupset=request.cargogroupset or SET_GROUP:New() + request.cargogroupset:AddGroup(group) + else + request.transportgroupset=request.transportgroupset or SET_GROUP:New() + request.transportgroupset:AddGroup(group) + end + + -- Set warehouse state. + group:SetState(group, "WAREHOUSE", self) -- Check if all assets groups are spawned and trigger events. local n=0 @@ -5218,22 +5441,23 @@ function WAREHOUSE:onafterAssetSpawned(From, Event, To, group, asset, request) local assetitem=_asset --#WAREHOUSE.Assetitem -- Debug info. - self:T2(self.lid..string.format("Asset %s spawned %s as %s", assetitem.templatename, tostring(assetitem.spawned), tostring(assetitem.spawngroupname))) - + self:T(self.lid..string.format("Asset %s spawned %s as %s", assetitem.templatename, tostring(assetitem.spawned), tostring(assetitem.spawngroupname))) + if assetitem.spawned then n=n+1 else - self:E(self.lid.."FF What?! This should not happen!") + -- Now this can happend if multiple groups need to be spawned in one request. + --self:I(self.lid.."FF What?! This should not happen!") end end -- Trigger event. if n==request.nasset+request.ntransport then - self:T3(self.lid..string.format("All assets %d (ncargo=%d + ntransport=%d) of request rid=%d spawned. Calling RequestSpawned", n, request.nasset, request.ntransport, request.uid)) + self:T(self.lid..string.format("All assets %d (ncargo=%d + ntransport=%d) of request rid=%d spawned. Calling RequestSpawned", n, request.nasset, request.ntransport, request.uid)) self:RequestSpawned(request, request.cargogroupset, request.transportgroupset) else - self:T3(self.lid..string.format("Not all assets %d (ncargo=%d + ntransport=%d) of request rid=%d spawned YET", n, request.nasset, request.ntransport, request.uid)) + self:T(self.lid..string.format("Not all assets %d (ncargo=%d + ntransport=%d) of request rid=%d spawned YET", n, request.nasset, request.ntransport, request.uid)) end end @@ -5246,9 +5470,65 @@ end -- @param #WAREHOUSE.Assetitem asset The asset that is dead. -- @param #WAREHOUSE.Pendingitem request The request of the dead asset. function WAREHOUSE:onafterAssetDead(From, Event, To, asset, request) - local text=string.format("Asset %s from request id=%d is dead!", asset.templatename, request.uid) - self:T(self.lid..text) - self:_DebugMessage(text) + + if asset and request then + + -- Debug message. + local text=string.format("Asset %s from request id=%d is dead!", asset.templatename, request.uid) + self:T(self.lid..text) + + -- Here I need to get rid of the #CARGO at the end to obtain the original name again! + local groupname=asset.spawngroupname --self:_GetNameWithOut(group) + + -- Dont trigger a Remove event for the group sets. + local NoTriggerEvent=true + + if request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then + + --- + -- Easy case: Group can simply be removed from the cargogroupset. + --- + + -- Remove dead group from cargo group set. + request.cargogroupset:Remove(groupname, NoTriggerEvent) + self:T(self.lid..string.format("Removed selfpropelled cargo %s: ncargo=%d.", groupname, request.cargogroupset:Count())) + + else + + --- + -- Complicated case: Dead unit could be: + -- 1.) A Cargo unit (e.g. waiting to be picked up). + -- 2.) A Transport unit which itself holds cargo groups. + --- + + -- Check if this a cargo or transport group. + local istransport=not asset.iscargo --self:_GroupIsTransport(group, request) + + if istransport==true then + + -- Whole carrier group is dead. Remove it from the carrier group set. + request.transportgroupset:Remove(groupname, NoTriggerEvent) + self:T(self.lid..string.format("Removed transport %s: ntransport=%d", groupname, request.transportgroupset:Count())) + + elseif istransport==false then + + -- This must have been an alive cargo group that was killed outside the carrier, e.g. waiting to be transported or waiting to be put back. + -- Remove dead group from cargo group set. + request.cargogroupset:Remove(groupname, NoTriggerEvent) + self:T(self.lid..string.format("Removed transported cargo %s outside carrier: ncargo=%d", groupname, request.cargogroupset:Count())) + -- This as well? + --request.transportcargoset:RemoveCargosByName(RemoveCargoNames) + + else + --self:E(self.lid..string.format("ERROR: Group %s is neither cargo nor transport!", group:GetName())) + end + end + + else + self:E(self.lid.."ERROR: Asset and/or Request is nil in onafterAssetDead") + + end + end @@ -5278,11 +5558,11 @@ function WAREHOUSE:onafterDestroyed(From, Event, To) for k,_ in pairs(self.queue) do self.queue[k]=nil end - + for k,_ in pairs(self.stock) do --self.stock[k]=nil end - + for k=#self.stock,1,-1 do --local asset=self.stock[k] --#WAREHOUSE.Assetitem --self:AssetDead(asset, nil) @@ -5545,30 +5825,30 @@ function WAREHOUSE:_SpawnAssetRequest(Request) -- Set asset status to not spawned until we capture its birth event. asset.spawned=false asset.iscargo=true - + -- Set request ID. asset.rid=Request.uid -- Spawn group name. local _alias=asset.spawngroupname - + --Request add asset by id. - Request.assets[asset.uid]=asset + Request.assets[asset.uid]=asset -- Spawn an asset group. local _group=nil --Wrapper.Group#GROUP if asset.category==Group.Category.GROUND then -- Spawn ground troops. - _group=self:_SpawnAssetGroundNaval(_alias, asset, Request, self.spawnzone) + _group=self:_SpawnAssetGroundNaval(_alias, asset, Request, self.spawnzone, Request.lateActivation) elseif asset.category==Group.Category.AIRPLANE or asset.category==Group.Category.HELICOPTER then -- Spawn air units. if Parking[asset.uid] then - _group=self:_SpawnAssetAircraft(_alias, asset, Request, Parking[asset.uid], UnControlled) + _group=self:_SpawnAssetAircraft(_alias, asset, Request, Parking[asset.uid], UnControlled, Request.lateActivation) else - _group=self:_SpawnAssetAircraft(_alias, asset, Request, nil, UnControlled) + _group=self:_SpawnAssetAircraft(_alias, asset, Request, nil, UnControlled, Request.lateActivation) end elseif asset.category==Group.Category.TRAIN then @@ -5578,7 +5858,7 @@ function WAREHOUSE:_SpawnAssetRequest(Request) --TODO: Rail should only get one asset because they would spawn on top! -- Spawn naval assets. - _group=self:_SpawnAssetGroundNaval(_alias, asset, Request, self.spawnzone) + _group=self:_SpawnAssetGroundNaval(_alias, asset, Request, self.spawnzone, Request.lateActivation) end --self:E(self.lid.."ERROR: Spawning of TRAIN assets not possible yet!") @@ -5586,11 +5866,16 @@ function WAREHOUSE:_SpawnAssetRequest(Request) elseif asset.category==Group.Category.SHIP then -- Spawn naval assets. - _group=self:_SpawnAssetGroundNaval(_alias, asset, Request, self.portzone) + _group=self:_SpawnAssetGroundNaval(_alias, asset, Request, self.portzone, Request.lateActivation) else self:E(self.lid.."ERROR: Unknown asset category!") end + + -- Trigger event. + if _group then + self:__AssetSpawned(0.01, _group, asset, Request) + end end @@ -5603,9 +5888,9 @@ end -- @param #WAREHOUSE.Assetitem asset Ground asset that will be spawned. -- @param #WAREHOUSE.Queueitem request Request belonging to this asset. Needed for the name/alias. -- @param Core.Zone#ZONE spawnzone Zone where the assets should be spawned. --- @param #boolean aioff If true, AI of ground units are set to off. +-- @param #boolean lateactivated If true, groups are spawned late activated. -- @return Wrapper.Group#GROUP The spawned group or nil if the group could not be spawned. -function WAREHOUSE:_SpawnAssetGroundNaval(alias, asset, request, spawnzone, aioff) +function WAREHOUSE:_SpawnAssetGroundNaval(alias, asset, request, spawnzone, lateactivated) if asset and (asset.category==Group.Category.GROUND or asset.category==Group.Category.SHIP or asset.category==Group.Category.TRAIN) then @@ -5645,9 +5930,12 @@ function WAREHOUSE:_SpawnAssetGroundNaval(alias, asset, request, spawnzone, aiof end if asset.skill then unit.skill= asset.skill - end + end end + + -- Late activation. + template.lateActivation=lateactivated template.route.points[1].x = coord.x template.route.points[1].y = coord.z @@ -5659,14 +5947,6 @@ function WAREHOUSE:_SpawnAssetGroundNaval(alias, asset, request, spawnzone, aiof -- Spawn group. local group=_DATABASE:Spawn(template) --Wrapper.Group#GROUP - -- Activate group. Should only be necessary for late activated groups. - --group:Activate() - - -- Switch AI off if desired. This works only for ground and naval groups. - if aioff then - group:SetAIOff() - end - return group end @@ -5680,21 +5960,49 @@ 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. +-- @param #boolean lateactivated If true, groups are spawned late activated. -- @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, lateactivated) 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 + + local airstart=asset.takeoffType and asset.takeoffType==COORDINATE.WaypointType.TurningPoint or false + + if airstart then + _type=COORDINATE.WaypointType.TurningPoint + _action=COORDINATE.WaypointAction.TurningPoint + 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 coord=self.airbase:GetCoordinate() + + if airstart then + coord:SetAltitude(math.random(1000, 2000)) + end + + -- Single waypoint. + local wp=coord: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) @@ -5702,18 +6010,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 @@ -5728,7 +6026,7 @@ function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrol else - if #parking<#template.units then + if #parking<#template.units and not airstart then local text=string.format("ERROR: Not enough parking! Free parking = %d < %d aircraft to be spawned.", #parking, #template.units) self:_DebugMessage(text) return nil @@ -5750,14 +6048,25 @@ function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrol unit.x=coord.x unit.y=coord.z unit.alt=coord.y + + if airstart then + unit.alt=math.random(1000, 2000) + end unit.parking_id = nil unit.parking = nil else - local coord=parking[i].Coordinate --Core.Point#COORDINATE - local terminal=parking[i].TerminalID --#number + local coord=nil --Core.Point#COORDINATE + local terminal=nil --#number + + if airstart then + coord=self.airbase:GetCoordinate():SetAltitude(math.random(1000, 2000)) + else + coord=parking[i].Coordinate + terminal=parking[i].TerminalID + end if self.Debug then coord:MarkToAll(string.format("Spawnplace unit %s terminal %d.", unit.name, terminal)) @@ -5781,15 +6090,15 @@ function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrol end if asset.payload then - unit.payload=asset.payload.pylons + unit.payload=asset.payload.pylons end - + if asset.modex then unit.onboard_num=asset.modex[i] end if asset.callsign then unit.callsign=asset.callsign[i] - end + end end @@ -5842,12 +6151,12 @@ function WAREHOUSE:_SpawnAssetPrepareTemplate(asset, alias) -- No late activation. template.lateActivation=false - + if asset.missionTask then - self:I(self.lid..string.format("Setting mission task to %s", tostring(asset.missionTask))) + self:T(self.lid..string.format("Setting mission task to %s", tostring(asset.missionTask))) template.task=asset.missionTask end - + -- No predefined task. --template.taskSelected=false @@ -5932,18 +6241,10 @@ function WAREHOUSE:_RouteGround(group, request) end for n,wp in ipairs(Waypoints) do - env.info(n) local tf=self:_SimpleTaskFunctionWP("warehouse:_PassingWaypoint",group, n, #Waypoints) group:SetTaskWaypoint(wp, tf) end - -- Task function triggering the arrived event at the last waypoint. - --local TaskFunction = self:_SimpleTaskFunction("warehouse:_Arrived", group) - - -- Put task function on last waypoint. - --local Waypoint = Waypoints[#Waypoints] - --group:SetTaskWaypoint(Waypoint, TaskFunction) - -- Route group to destination. group:Route(Waypoints, 1) @@ -6021,9 +6322,12 @@ function WAREHOUSE:_RouteAir(aircraft) self:T2(self.lid..string.format("RouteAir aircraft group %s alive=%s", aircraft:GetName(), tostring(aircraft:IsAlive()))) -- Give start command to activate uncontrolled aircraft within the next 60 seconds. - local starttime=math.random(60) - - aircraft:StartUncontrolled(starttime) + if self.flightcontrol then + local fg=FLIGHTGROUP:New(aircraft) + fg:SetReadyForTakeoff(true) + else + aircraft:StartUncontrolled(math.random(60)) + end -- Debug info. self:T2(self.lid..string.format("RouteAir aircraft group %s alive=%s (after start command)", aircraft:GetName(), tostring(aircraft:IsAlive()))) @@ -6126,7 +6430,7 @@ end --- Get a warehouse request from its unique id. -- @param #WAREHOUSE self -- @param #number id Request ID. --- @return #WAREHOUSE.Pendingitem The warehouse requested - either queued or pending. +-- @return #WAREHOUSE.Pendingitem The warehouse requested - either queued or pending. -- @return #boolean If *true*, request is queued, if *false*, request is pending, if *nil*, request could not be found. function WAREHOUSE:GetRequestByID(id) @@ -6164,40 +6468,21 @@ function WAREHOUSE:_OnEventBirth(EventData) local wid,aid,rid=self:_GetIDsFromGroup(group) if wid==self.uid then - + -- Get asset and request from id. local asset=self:GetAssetByID(aid) local request=self:GetRequestByID(rid) - - -- Debug message. - self:T(self.lid..string.format("Warehouse %s captured event birth of its asset unit %s. spawned=%s", self.alias, EventData.IniUnitName, tostring(asset.spawned))) - - -- Birth is triggered for each unit. We need to make sure not to call this too often! - if not asset.spawned then - - -- Remove asset from stock. - self:_DeleteStockItem(asset) - - -- Set spawned switch. - asset.spawned=true - asset.spawngroupname=group:GetName() - - -- Add group. - if asset.iscargo==true then - request.cargogroupset=request.cargogroupset or SET_GROUP:New() - request.cargogroupset:AddGroup(group) - else - request.transportgroupset=request.transportgroupset or SET_GROUP:New() - request.transportgroupset:AddGroup(group) - end - - -- Set warehouse state. - group:SetState(group, "WAREHOUSE", self) - - -- Asset spawned FSM function. - --self:__AssetSpawned(1, group, asset, request) - self:AssetSpawned(group, asset, request) + + if asset and request then + -- Debug message. + self:T(self.lid..string.format("Warehouse %s captured event birth of request ID=%d, asset ID=%d, unit %s spawned=%s", self.alias, request.uid, asset.uid, EventData.IniUnitName, tostring(asset.spawned))) + + -- Set born to true. + request.born=true + + else + self:E(self.lid..string.format("ERROR: Either asset AID=%s or request RID=%s are nil in event birth of unit %s", tostring(aid), tostring(rid), tostring(EventData.IniUnitName))) end else @@ -6317,7 +6602,6 @@ function WAREHOUSE:_OnEventArrived(EventData) local istransport=self:_GroupIsTransport(group, request) -- Get closest airbase. - -- Note, this crashed at somepoint when the Tarawa was in the mission. Don't know why. Deleting the Tarawa and adding it again solved the problem. local closest=group:GetCoordinate():GetClosestAirbase() -- Check if engine shutdown happend at right airbase because the event is also triggered in other situations. @@ -6326,15 +6610,19 @@ function WAREHOUSE:_OnEventArrived(EventData) -- Check that group is cargo and not transport. if istransport==false and rightairbase then - -- Debug info. - local text=string.format("Air asset group %s from warehouse %s arrived at its destination.", group:GetName(), self.alias) - self:_InfoMessage(text) - -- Trigger arrived event for this group. Note that each unit of a group will trigger this event. So the onafterArrived function needs to take care of that. -- Actually, we only take the first unit of the group that arrives. If it does, we assume the whole group arrived, which might not be the case, since -- some units might still be taxiing or whatever. Therefore, we add 10 seconds for each additional unit of the group until the first arrived event is triggered. local nunits=#group:GetUnits() local dt=10*(nunits-1)+1 -- one unit = 1 sec, two units = 11 sec, three units = 21 sec before we call the group arrived. + + -- Debug info. + if self.verbosity>=1 then + local text=string.format("Air asset group %s from warehouse %s arrived at its destination. Trigger Arrived event in %d sec", group:GetName(), self.alias, dt) + self:_InfoMessage(text) + end + + -- Arrived event. self:__Arrived(dt, group) end @@ -6368,9 +6656,13 @@ function WAREHOUSE:_OnEventCrashOrDead(EventData) -- Trigger Destroyed event. self:Destroyed() end + if self.airbase and self.airbasename and self.airbasename==EventData.IniUnitName then + self:RunwayDestroyed() + end end - --self:I(self.lid..string.format("Warehouse %s captured event dead or crash or unit %s.", self.alias, tostring(EventData.IniUnitName))) + -- Debug info. + self:T2(self.lid..string.format("Warehouse %s captured event dead or crash or unit %s", self.alias, tostring(EventData.IniUnitName))) -- Check if an asset unit was destroyed. if EventData.IniGroup then @@ -6385,7 +6677,7 @@ function WAREHOUSE:_OnEventCrashOrDead(EventData) if wid==self.uid then -- Debug message. - self:T(self.lid..string.format("Warehouse %s captured event dead or crash of its asset unit %s.", self.alias, EventData.IniUnitName)) + self:T(self.lid..string.format("Warehouse %s captured event dead or crash of its asset unit %s", self.alias, EventData.IniUnitName)) -- Loop over all pending requests and get the one belonging to this unit. for _,request in pairs(self.pending) do @@ -6395,7 +6687,7 @@ function WAREHOUSE:_OnEventCrashOrDead(EventData) if request.uid==rid then -- Update cargo and transport group sets of this request. We need to know if this job is finished. - self:_UnitDead(EventData.IniUnit, request) + self:_UnitDead(EventData.IniUnit, EventData.IniGroup, request) end end @@ -6408,38 +6700,46 @@ end -- This is important in order to determine if a job is done and can be removed from the (pending) queue. -- @param #WAREHOUSE self -- @param Wrapper.Unit#UNIT deadunit Unit that died. +-- @param Wrapper.Group#GROUP deadgroup Group of unit that died. -- @param #WAREHOUSE.Pendingitem request Request that needs to be updated. -function WAREHOUSE:_UnitDead(deadunit, request) +function WAREHOUSE:_UnitDead(deadunit, deadgroup, request) + self:F(self.lid.."FF unit dead "..deadunit:GetName()) - -- Flare unit. - if self.Debug then - deadunit:FlareRed() + -- Find opsgroup. + local opsgroup=_DATABASE:FindOpsGroup(deadgroup) + + -- Check if we have an opsgroup. + if opsgroup then + -- Handled in OPSGROUP:onafterDead() now. + return nil end - -- Group the dead unit belongs to. - local group=deadunit:GetGroup() - -- Number of alive units in group. - local nalive=group:CountAliveUnits() + local nalive=deadgroup:CountAliveUnits() -- Whole group is dead? - local groupdead=true + local groupdead=false if nalive>0 then groupdead=false + else + groupdead=true end + + -- Find asset. + local asset=self:FindAssetInDB(deadgroup) -- Here I need to get rid of the #CARGO at the end to obtain the original name again! local unitname=self:_GetNameWithOut(deadunit) - local groupname=self:_GetNameWithOut(group) + local groupname=self:_GetNameWithOut(deadgroup) -- Group is dead! if groupdead then - self:T(self.lid..string.format("Group %s (transport=%s) is dead!", groupname, tostring(self:_GroupIsTransport(group,request)))) + -- Debug output. + self:T(self.lid..string.format("Group %s (transport=%s) is dead!", groupname, tostring(self:_GroupIsTransport(deadgroup,request)))) if self.Debug then - group:SmokeWhite() + deadgroup:SmokeWhite() end - -- Trigger AssetDead event. - local asset=self:FindAssetInDB(group) + -- Trigger AssetDead event. self:AssetDead(asset, request) end @@ -6447,19 +6747,7 @@ function WAREHOUSE:_UnitDead(deadunit, request) -- Dont trigger a Remove event for the group sets. local NoTriggerEvent=true - if request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then - - --- - -- Easy case: Group can simply be removed from the cargogroupset. - --- - - -- Remove dead group from cargo group set. - if groupdead==true then - request.cargogroupset:Remove(groupname, NoTriggerEvent) - self:T(self.lid..string.format("Removed selfpropelled cargo %s: ncargo=%d.", groupname, request.cargogroupset:Count())) - end - - else + if not request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then --- -- Complicated case: Dead unit could be: @@ -6467,10 +6755,7 @@ function WAREHOUSE:_UnitDead(deadunit, request) -- 2.) A Transport unit which itself holds cargo groups. --- - -- Check if this a cargo or transport group. - local istransport=self:_GroupIsTransport(group,request) - - if istransport==true then + if not asset.iscargo then -- Get the carrier unit table holding the cargo groups inside this carrier. local cargogroupnames=request.carriercargo[unitname] @@ -6485,25 +6770,8 @@ function WAREHOUSE:_UnitDead(deadunit, request) end - -- Whole carrier group is dead. Remove it from the carrier group set. - if groupdead then - request.transportgroupset:Remove(groupname, NoTriggerEvent) - self:T(self.lid..string.format("Removed transport %s: ntransport=%d", groupname, request.transportgroupset:Count())) - end - - elseif istransport==false then - - -- This must have been an alive cargo group that was killed outside the carrier, e.g. waiting to be transported or waiting to be put back. - -- Remove dead group from cargo group set. - if groupdead==true then - request.cargogroupset:Remove(groupname, NoTriggerEvent) - self:T(self.lid..string.format("Removed transported cargo %s outside carrier: ncargo=%d", groupname, request.cargogroupset:Count())) - -- This as well? - --request.transportcargoset:RemoveCargosByName(RemoveCargoNames) - end - else - self:E(self.lid..string.format("ERROR: Group %s is neither cargo nor transport!", group:GetName())) + self:E(self.lid..string.format("ERROR: Group %s is neither cargo nor transport!", deadgroup:GetName())) end end @@ -6889,10 +7157,9 @@ function WAREHOUSE:_CheckRequestValid(request) -- Check that both spawn zones are not in water. local inwater=self.spawnzone:GetCoordinate():IsSurfaceTypeWater() or request.warehouse.spawnzone:GetCoordinate():IsSurfaceTypeWater() - if inwater then + if inwater and not request.lateActivation then self:E("ERROR: Incorrect request. Ground asset requested but at least one spawn zone is in water!") - --valid=false - valid=false + return false end -- No ground assets directly to or from ships. @@ -7116,16 +7383,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 - - 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) + if _assetcategory==Group.Category.AIRPLANE or _assetcategory==Group.Category.HELICOPTER then + + if self.airbase and self.airbase:GetCoalition()==self:GetCoalition() then + + if self:IsRunwayOperational() then + + 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 + end + + else + + -- No airbase! + local text=string.format("Warehouse %s: Request denied! No airbase", self.alias) self:_InfoMessage(text, 5) - - return false + return false + end end @@ -7149,14 +7435,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 @@ -7170,7 +7479,9 @@ function WAREHOUSE:_CheckRequestNow(request) else - -- Self propelled case. Nothing to do for now. + --- + -- Self propelled case + --- -- Ground asset checks. if _assetcategory==Group.Category.GROUND then @@ -7430,7 +7741,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. @@ -7470,7 +7781,7 @@ function WAREHOUSE:_SimpleTaskFunction(Function, group) local DCSScript = {} DCSScript[#DCSScript+1] = string.format('local mygroup = GROUP:FindByName(\"%s\") ', groupname) -- The group that executes the task function. Very handy with the "...". - if self.isunit then + if self.isUnit then DCSScript[#DCSScript+1] = string.format("local mywarehouse = UNIT:FindByName(\"%s\") ", warehouse) -- The unit that holds the warehouse self object. else DCSScript[#DCSScript+1] = string.format("local mywarehouse = STATIC:FindByName(\"%s\") ", warehouse) -- The static that holds the warehouse self object. @@ -7501,7 +7812,7 @@ function WAREHOUSE:_SimpleTaskFunctionWP(Function, group, n, N) local DCSScript = {} DCSScript[#DCSScript+1] = string.format('local mygroup = GROUP:FindByName(\"%s\") ', groupname) -- The group that executes the task function. Very handy with the "...". - if self.isunit then + if self.isUnit then DCSScript[#DCSScript+1] = string.format("local mywarehouse = UNIT:FindByName(\"%s\") ", warehouse) -- The unit that holds the warehouse self object. else DCSScript[#DCSScript+1] = string.format("local mywarehouse = STATIC:FindByName(\"%s\") ", warehouse) -- The static that holds the warehouse self object. @@ -7559,7 +7870,7 @@ end function WAREHOUSE:_FindParkingForAssets(airbase, assets) -- Init default - local scanradius=100 + local scanradius=25 local scanunits=true local scanstatics=true local scanscenery=false @@ -7575,31 +7886,36 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) -- Get client coordinates. local function _clients() - local clients=_DATABASE.CLIENTS local coords={} - for clientname, client in pairs(clients) do - local template=_DATABASE:GetGroupTemplateFromUnitName(clientname) - local units=template.units - for i,unit in pairs(units) do - local coord=COORDINATE:New(unit.x, unit.alt, unit.y) - coords[unit.name]=coord - --[[ - local airbase=coord:GetClosestAirbase() - local _,TermID, dist, spot=coord:GetClosestParkingSpot(airbase) - if dist<=10 then - env.info(string.format("Found client %s on parking spot %d at airbase %s", unit.name, TermID, airbase:GetName())) + if not self.allowSpawnOnClientSpots then + local clients=_DATABASE.CLIENTS + for clientname, client in pairs(clients) do + local template=_DATABASE:GetGroupTemplateFromUnitName(clientname) + local units=template.units + for i,unit in pairs(units) do + local coord=COORDINATE:New(unit.x, unit.alt, unit.y) + coords[unit.name]=coord end - ]] end end return coords end -- Get parking spot data table. This contains all free and "non-free" spots. - local parkingdata=airbase:GetParkingSpotsTable() + local parkingdata=airbase.parking --airbase:GetParkingSpotsTable() + + --- + -- Find all obstacles + --- -- List of obstacles. local obstacles={} + + -- Check all clients. Clients dont change so we can put that out of the loop. + self.clientcoords=self.clientcoords or _clients() + for clientname,_coord in pairs(self.clientcoords) do + table.insert(obstacles, {coord=_coord, size=15, name=clientname, type="client"}) + end -- Loop over all parking spots and get the currently present obstacles. -- How long does this take on very large airbases, i.e. those with hundereds of parking spots? Seems to be okay! @@ -7615,22 +7931,18 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) -- Check all units. for _,_unit in pairs(_units) do local unit=_unit --Wrapper.Unit#UNIT - local _coord=unit:GetCoordinate() + local _coord=unit:GetVec3() local _size=self:_GetObjectSize(unit:GetDCSObject()) local _name=unit:GetName() - table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="unit"}) - end - - -- Check all clients. - local clientcoords=_clients() - for clientname,_coord in pairs(clientcoords) do - table.insert(obstacles, {coord=_coord, size=15, name=clientname, type="client"}) + if unit and unit:IsAlive() then + table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="unit"}) + end end -- Check all statics. for _,static in pairs(_statics) do - local _vec3=static:getPoint() - local _coord=COORDINATE:NewFromVec3(_vec3) + local _coord=static:getPoint() + --local _coord=COORDINATE:NewFromVec3(_vec3) local _name=static:getName() local _size=self:_GetObjectSize(static) table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="static"}) @@ -7638,14 +7950,18 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) -- Check all scenery. for _,scenery in pairs(_sceneries) do - local _vec3=scenery:getPoint() - local _coord=COORDINATE:NewFromVec3(_vec3) + local _coord=scenery:getPoint() + --local _coord=COORDINATE:NewFromVec3(_vec3) local _name=scenery:getTypeName() local _size=self:_GetObjectSize(scenery) - table.insert(obstacles,{coord=_coord, size=_size, name=_name, type="scenery"}) + table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="scenery"}) end end + + --- + -- Get Parking Spots + --- -- Parking data for all assets. local parking={} @@ -7662,6 +7978,9 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) -- Loop over all units - each one needs a spot. for i=1,_asset.nunits do + + -- Asset name + local assetname=_asset.spawngroupname.."-"..tostring(i) -- Loop over all parking spots. local gotit=false @@ -7669,25 +7988,14 @@ 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, airbase) and airbase:_CheckParkingLists(parkingspot.TerminalID) 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 local _termid=parkingspot.TerminalID - local _toac=parkingspot.TOAC - - --env.info(string.format("FF asset=%s (id=%d): needs terminal type=%d, id=%d, #obstacles=%d", _asset.templatename, _asset.uid, terminaltype, _termid, #obstacles)) - local free=true local problem=nil - -- Safe parking using TO_AC from DCS result. - self:I(self.lid..string.format("Parking spot %d TOAC=%s (safe park=%s).", _termid, tostring(_toac), tostring(self.safeparking))) - if self.safeparking and _toac then - free=false - self:I(self.lid..string.format("Parking spot %d is occupied by other aircraft taking off (TOAC).", _termid)) - end - -- Loop over all obstacles. for _,obstacle in pairs(obstacles) do @@ -7697,13 +8005,13 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) -- Spot is blocked. if not safe then - --env.info(string.format("FF asset=%s (id=%d): spot id=%d dist=%.1fm is NOT SAFE", _asset.templatename, _asset.uid, _termid, dist)) + self:T3(self.lid..string.format("FF asset=%s (id=%d): spot id=%d dist=%.1fm is NOT SAFE", assetname, _asset.uid, _termid, dist)) free=false problem=obstacle problem.dist=dist break else - --env.info(string.format("FF asset=%s (id=%d): spot id=%d dist=%.1fm is SAFE", _asset.templatename, _asset.uid, _termid, dist)) + --env.info(string.format("FF asset=%s (id=%d): spot id=%d dist=%.1fm is SAFE", assetname, _asset.uid, _termid, dist)) end end @@ -7714,32 +8022,37 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) -- Add parkingspot for this asset unit. table.insert(parking[_asset.uid], parkingspot) - self:I(self.lid..string.format("Parking spot %d is free for asset id=%d!", _termid, _asset.uid)) + -- Debug + self:T(self.lid..string.format("Parking spot %d is free for asset %s [id=%d]!", _termid, assetname, _asset.uid)) -- Add the unit as obstacle so that this spot will not be available for the next unit. - table.insert(obstacles, {coord=_spot, size=_asset.size, name=_asset.templatename, type="asset"}) + table.insert(obstacles, {coord=_spot, size=_asset.size, name=assetname, type="asset"}) gotit=true break else - -- Debug output for occupied spots. - self:I(self.lid..string.format("Parking spot %d is occupied or not big enough!", _termid)) + -- Debug output for occupied spots. if self.Debug then local coord=problem.coord --Core.Point#COORDINATE - local text=string.format("Obstacle blocking spot #%d is %s type %s with size=%.1f m and distance=%.1f m.", _termid, problem.name, problem.type, problem.size, problem.dist) + local text=string.format("Obstacle %s [type=%s] blocking spot=%d! Size=%.1f m and distance=%.1f m.", problem.name, problem.type, _termid, problem.size, problem.dist) + self:I(self.lid..text) coord:MarkToAll(string.format(text)) + else + self:T(self.lid..string.format("Parking spot %d is occupied or not big enough!", _termid)) end end + else + self:T2(self.lid..string.format("Terminal ID=%d: type=%s not supported", parkingspot.TerminalID, parkingspot.TerminalType)) end -- check terminal type end -- loop over parking spots -- No parking spot for at least one asset :( if not gotit then - self:I(self.lid..string.format("WARNING: No free parking spot for asset id=%d",_asset.uid)) + self:I(self.lid..string.format("WARNING: No free parking spot for asset %s [id=%d]", assetname, _asset.uid)) return nil end end -- loop over asset units @@ -7777,27 +8090,27 @@ end function WAREHOUSE:_GroupIsTransport(group, request) local asset=self:FindAssetInDB(group) - + if asset and asset.iscargo~=nil then return not asset.iscargo else -- Name of the group under question. local groupname=self:_GetNameWithOut(group) - + if request.transportgroupset then local transporters=request.transportgroupset:GetSetObjects() - + for _,transport in pairs(transporters) do if transport:GetName()==groupname then return true end end end - + if request.cargogroupset then local cargos=request.cargogroupset:GetSetObjects() - + for _,cargo in pairs(cargos) do if self:_GetNameWithOut(cargo)==groupname then return false @@ -7817,14 +8130,14 @@ end function WAREHOUSE:_GetNameWithOut(group) local groupname=type(group)=="string" and group or group:GetName() - + if groupname:find("CARGO") then local name=groupname:gsub("#CARGO", "") return name else return groupname end - + end @@ -7836,57 +8149,12 @@ end -- @return #number Request ID. function WAREHOUSE:_GetIDsFromGroup(group) - ---@param #string text The text to analyse. - local function analyse(text) - - -- Get rid of #0001 tail from spawn. - local unspawned=UTILS.Split(text, "#")[1] - - -- Split keywords. - local keywords=UTILS.Split(unspawned, "_") - local _wid=nil -- warehouse UID - local _aid=nil -- asset UID - local _rid=nil -- request UID - - -- Loop over keys. - for _,keys in pairs(keywords) do - local str=UTILS.Split(keys, "-") - local key=str[1] - local val=str[2] - if key:find("WID") then - _wid=tonumber(val) - elseif key:find("AID") then - _aid=tonumber(val) - elseif key:find("RID") then - _rid=tonumber(val) - end - end - - return _wid,_aid,_rid - end - if group then - + -- Group name - local name=group:GetName() - - -- Get asset id from group name. - local wid,aid,rid=analyse(name) - - -- Get Asset. - local asset=self:GetAssetByID(aid) - - -- Get warehouse and request id from asset table. - if asset then - wid=asset.wid - rid=asset.rid - end - - -- Debug info - self:T(self.lid..string.format("Group Name = %s", tostring(name))) - self:T(self.lid..string.format("Warehouse ID = %s", tostring(wid))) - self:T(self.lid..string.format("Asset ID = %s", tostring(aid))) - self:T(self.lid..string.format("Request ID = %s", tostring(rid))) + local groupname=group:GetName() + + local wid, aid, rid=self:_GetIDsFromGroupName(groupname) return wid,aid,rid else @@ -7895,14 +8163,13 @@ function WAREHOUSE:_GetIDsFromGroup(group) end - --- Get warehouse id, asset id and request id from group name (alias). -- @param #WAREHOUSE self --- @param Wrapper.Group#GROUP group The group from which the info is gathered. +-- @param #string groupname Name of the group from which the info is gathered. -- @return #number Warehouse ID. -- @return #number Asset ID. -- @return #number Request ID. -function WAREHOUSE:_GetIDsFromGroupOLD(group) +function WAREHOUSE:_GetIDsFromGroupName(groupname) ---@param #string text The text to analyse. local function analyse(text) @@ -7933,25 +8200,26 @@ function WAREHOUSE:_GetIDsFromGroupOLD(group) return _wid,_aid,_rid end - if group then - -- Group name - local name=group:GetName() + -- Get asset id from group name. + local wid,aid,rid=analyse(groupname) - -- Get ids - local wid,aid,rid=analyse(name) + -- Get Asset. + local asset=self:GetAssetByID(aid) - -- Debug info - self:T3(self.lid..string.format("Group Name = %s", tostring(name))) - self:T3(self.lid..string.format("Warehouse ID = %s", tostring(wid))) - self:T3(self.lid..string.format("Asset ID = %s", tostring(aid))) - self:T3(self.lid..string.format("Request ID = %s", tostring(rid))) - - return wid,aid,rid - else - self:E("WARNING: Group not found in GetIDsFromGroup() function!") + -- Get warehouse and request id from asset table. + if asset then + wid=asset.wid + rid=asset.rid end + -- Debug info + self:T3(self.lid..string.format("Group Name = %s", tostring(groupname))) + self:T3(self.lid..string.format("Warehouse ID = %s", tostring(wid))) + self:T3(self.lid..string.format("Asset ID = %s", tostring(aid))) + self:T3(self.lid..string.format("Request ID = %s", tostring(rid))) + + return wid,aid,rid end --- Filter stock assets by descriptor and attribute. @@ -7987,23 +8255,23 @@ function WAREHOUSE:_FilterStock(stock, descriptor, attribute, nmax, mobile) -- Filtered array. local filtered={} - + -- A specific list of assets was required. if descriptor==WAREHOUSE.Descriptor.ASSETLIST then -- Count total number in stock. local ntot=0 for _,_rasset in pairs(attribute) do - local rasset=_rasset --#WAREHOUSE.Assetitem + local rasset=_rasset --#WAREHOUSE.Assetitem for _,_asset in ipairs(stock) do - local asset=_asset --#WAREHOUSE.Assetitem + local asset=_asset --#WAREHOUSE.Assetitem if rasset.uid==asset.uid then table.insert(filtered, asset) break end - end + end end - + return filtered, #filtered, #filtered>=#attribute end @@ -8097,9 +8365,10 @@ function WAREHOUSE:_GetAttribute(group) --- Ground --- -------------- -- Ground - local apc=group:HasAttribute("Infantry carriers") + local apc=group:HasAttribute("APC") --("Infantry carriers") local truck=group:HasAttribute("Trucks") and group:GetCategory()==Group.Category.GROUND local infantry=group:HasAttribute("Infantry") + local ifv=group:HasAttribute("IFV") local artillery=group:HasAttribute("Artillery") local tank=group:HasAttribute("Old Tanks") or group:HasAttribute("Modern Tanks") local aaa=group:HasAttribute("AAA") @@ -8136,6 +8405,8 @@ function WAREHOUSE:_GetAttribute(group) attribute=WAREHOUSE.Attribute.AIR_UAV elseif apc then attribute=WAREHOUSE.Attribute.GROUND_APC + elseif ifv then + attribute=WAREHOUSE.Attribute.GROUND_IFV elseif infantry then attribute=WAREHOUSE.Attribute.GROUND_INFANTRY elseif artillery then @@ -8349,67 +8620,70 @@ end -- @param #string name Name of the queue for info reasons. function WAREHOUSE:_PrintQueue(queue, name) - local total="Empty" - if #queue>0 then - total=string.format("Total = %d", #queue) - end + if self.verbosity>=2 then - -- Init string. - local text=string.format("%s at %s: %s",name, self.alias, total) - - for i,qitem in ipairs(queue) do - local qitem=qitem --#WAREHOUSE.Pendingitem - - local uid=qitem.uid - local prio=qitem.prio - local clock="N/A" - if qitem.timestamp then - clock=tostring(UTILS.SecondsToClock(qitem.timestamp)) + local total="Empty" + if #queue>0 then + total=string.format("Total = %d", #queue) end - local assignment=tostring(qitem.assignment) - local requestor=qitem.warehouse.alias - local airbasename=qitem.warehouse:GetAirbaseName() - local requestorAirbaseCat=qitem.warehouse:GetAirbaseCategory() - local assetdesc=qitem.assetdesc - local assetdescval=qitem.assetdescval - if assetdesc==WAREHOUSE.Descriptor.ASSETLIST then - assetdescval="Asset list" + + -- Init string. + local text=string.format("%s at %s: %s",name, self.alias, total) + + for i,qitem in ipairs(queue) do + local qitem=qitem --#WAREHOUSE.Pendingitem + + local uid=qitem.uid + local prio=qitem.prio + local clock="N/A" + if qitem.timestamp then + clock=tostring(UTILS.SecondsToClock(qitem.timestamp)) + end + local assignment=tostring(qitem.assignment) + local requestor=qitem.warehouse.alias + local airbasename=qitem.warehouse:GetAirbaseName() + local requestorAirbaseCat=qitem.warehouse:GetAirbaseCategory() + local assetdesc=qitem.assetdesc + local assetdescval=qitem.assetdescval + if assetdesc==WAREHOUSE.Descriptor.ASSETLIST then + assetdescval="Asset list" + end + local nasset=tostring(qitem.nasset) + local ndelivered=tostring(qitem.ndelivered) + local ncargogroupset="N/A" + if qitem.cargogroupset then + ncargogroupset=tostring(qitem.cargogroupset:Count()) + end + local transporttype="N/A" + if qitem.transporttype then + transporttype=qitem.transporttype + end + local ntransport="N/A" + if qitem.ntransport then + ntransport=tostring(qitem.ntransport) + end + local ntransportalive="N/A" + if qitem.transportgroupset then + ntransportalive=tostring(qitem.transportgroupset:Count()) + end + local ntransporthome="N/A" + if qitem.ntransporthome then + ntransporthome=tostring(qitem.ntransporthome) + end + + -- Output text: + text=text..string.format( + "\n%d) UID=%d, Prio=%d, Clock=%s, Assignment=%s | Requestor=%s [Airbase=%s, category=%d] | Assets(%s)=%s: #requested=%s / #alive=%s / #delivered=%s | Transport=%s: #requested=%s / #alive=%s / #home=%s", + i, uid, prio, clock, assignment, requestor, airbasename, requestorAirbaseCat, assetdesc, assetdescval, nasset, ncargogroupset, ndelivered, transporttype, ntransport, ntransportalive, ntransporthome) + end - local nasset=tostring(qitem.nasset) - local ndelivered=tostring(qitem.ndelivered) - local ncargogroupset="N/A" - if qitem.cargogroupset then - ncargogroupset=tostring(qitem.cargogroupset:Count()) - end - local transporttype="N/A" - if qitem.transporttype then - transporttype=qitem.transporttype - end - local ntransport="N/A" - if qitem.ntransport then - ntransport=tostring(qitem.ntransport) - end - local ntransportalive="N/A" - if qitem.transportgroupset then - ntransportalive=tostring(qitem.transportgroupset:Count()) - end - local ntransporthome="N/A" - if qitem.ntransporthome then - ntransporthome=tostring(qitem.ntransporthome) - end - - -- Output text: - text=text..string.format( - "\n%d) UID=%d, Prio=%d, Clock=%s, Assignment=%s | Requestor=%s [Airbase=%s, category=%d] | Assets(%s)=%s: #requested=%s / #alive=%s / #delivered=%s | Transport=%s: #requested=%s / #alive=%s / #home=%s", - i, uid, prio, clock, assignment, requestor, airbasename, requestorAirbaseCat, assetdesc, assetdescval, nasset, ncargogroupset, ndelivered, transporttype, ntransport, ntransportalive, ntransporthome) - - end - - if #queue==0 then - self:T(self.lid..text) - else - if total~="Empty" then + + if #queue==0 then self:I(self.lid..text) + else + if total~="Empty" then + self:I(self.lid..text) + end end end end @@ -8417,17 +8691,19 @@ end --- Display status of warehouse. -- @param #WAREHOUSE self function WAREHOUSE:_DisplayStatus() - local text=string.format("\n------------------------------------------------------\n") - text=text..string.format("Warehouse %s status: %s\n", self.alias, self:GetState()) - text=text..string.format("------------------------------------------------------\n") - text=text..string.format("Coalition name = %s\n", self:GetCoalitionName()) - text=text..string.format("Country name = %s\n", self:GetCountryName()) - text=text..string.format("Airbase name = %s (category=%d)\n", self:GetAirbaseName(), self:GetAirbaseCategory()) - text=text..string.format("Queued requests = %d\n", #self.queue) - text=text..string.format("Pending requests = %d\n", #self.pending) - text=text..string.format("------------------------------------------------------\n") - text=text..self:_GetStockAssetsText() - self:T(text) + if self.verbosity>=3 then + local text=string.format("\n------------------------------------------------------\n") + text=text..string.format("Warehouse %s status: %s\n", self.alias, self:GetState()) + text=text..string.format("------------------------------------------------------\n") + text=text..string.format("Coalition name = %s\n", self:GetCoalitionName()) + text=text..string.format("Country name = %s\n", self:GetCountryName()) + text=text..string.format("Airbase name = %s (category=%d)\n", self:GetAirbaseName(), self:GetAirbaseCategory()) + text=text..string.format("Queued requests = %d\n", #self.queue) + text=text..string.format("Pending requests = %d\n", #self.pending) + text=text..string.format("------------------------------------------------------\n") + text=text..self:_GetStockAssetsText() + self:I(text) + end end --- Get text about warehouse stock. @@ -8467,29 +8743,49 @@ function WAREHOUSE:_UpdateWarehouseMarkText() if self.markerOn then - -- Create a mark with the current assets in stock. - if self.markerid~=nil then - trigger.action.removeMark(self.markerid) - end - - -- Get assets in stock. - local _data=self:GetStockInfo(self.stock) - - -- Text. + -- Marker text. local text=string.format("Warehouse state: %s\nTotal assets in stock %d:\n", self:GetState(), #self.stock) - - for _attribute,_count in pairs(_data) do + for _attribute,_count in pairs(self:GetStockInfo(self.stock) or {}) do if _count>0 then local attribute=tostring(UTILS.Split(_attribute, "_")[2]) text=text..string.format("%s=%d, ", attribute,_count) end end - - -- Create/update marker at warehouse in F10 map. - self.markerid=self:GetCoordinate():MarkToCoalition(text, self:GetCoalition(), true) - end + local coordinate=self:GetCoordinate() + local coalition=self:GetCoalition() + + if not self.markerWarehouse then + + -- Create a new marker. + self.markerWarehouse=MARKER:New(coordinate, text):ToCoalition(coalition) + + else + local refresh=false + + if self.markerWarehouse.text~=text then + self.markerWarehouse.text=text + refresh=true + end + + if self.markerWarehouse.coordinate~=coordinate then + self.markerWarehouse.coordinate=coordinate + refresh=true + end + + if self.markerWarehouse.coalition~=coalition then + self.markerWarehouse.coalition=coalition + refresh=true + end + + if refresh then + self.markerWarehouse:Refresh() + end + + end + end + end --- Display stock items of warehouse. @@ -8540,8 +8836,8 @@ end -- @param #number duration Message display duration in seconds. Default 20 sec. If duration is zero, no message is displayed. function WAREHOUSE:_InfoMessage(text, duration) duration=duration or 20 - if duration>0 then - MESSAGE:New(text, duration):ToCoalitionIf(self:GetCoalition(), self.Debug or self.Report) + if duration>0 and self.Debug or self.Report then + MESSAGE:New(text, duration):ToCoalition(self:GetCoalition()) end self:I(self.lid..text) end @@ -8553,7 +8849,7 @@ end -- @param #number duration Message display duration in seconds. Default 20 sec. If duration is zero, no message is displayed. function WAREHOUSE:_DebugMessage(text, duration) duration=duration or 20 - if duration>0 then + if self.Debug and duration>0 then MESSAGE:New(text, duration):ToAllIf(self.Debug) end self:T(self.lid..text) @@ -8854,9 +9150,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) @@ -8895,7 +9205,6 @@ function WAREHOUSE:_GetFlightplan(asset, departure, destination) return wp,c end - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Sound/Radio.lua b/Moose Development/Moose/Sound/Radio.lua index 6eb3ee2e2..40343e2ac 100644 --- a/Moose Development/Moose/Sound/Radio.lua +++ b/Moose Development/Moose/Sound/Radio.lua @@ -1,28 +1,28 @@ --- **Sound** - Radio transmissions. --- +-- -- === --- +-- -- ## Features: --- +-- -- * Provide radio functionality to broadcast radio transmissions. --- +-- -- What are radio communications in DCS? --- +-- -- * Radio transmissions consist of **sound files** that are broadcasted on a specific **frequency** (e.g. 115MHz) and **modulation** (e.g. AM), -- * They can be **subtitled** for a specific **duration**, the **power** in Watts of the transmiter's antenna can be set, and the transmission can be **looped**. --- +-- -- How to supply DCS my own Sound Files? --- +-- -- * Your sound files need to be encoded in **.ogg** or .wav, -- * Your sound files should be **as tiny as possible**. It is suggested you encode in .ogg with low bitrate and sampling settings, -- * They need to be added in .\l10n\DEFAULT\ in you .miz file (wich can be decompressed like a .zip file), -- * For simplicity sake, you can **let DCS' Mission Editor add the file** itself, by creating a new Trigger with the action "Sound to Country", and choosing your sound file and a country you don't use in your mission. --- +-- -- Due to weird DCS quirks, **radio communications behave differently** if sent by a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP} or by any other @{Wrapper.Positionable#POSITIONABLE} --- +-- -- * If the transmitter is a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}, DCS will set the power of the transmission automatically, -- * If the transmitter is any other @{Wrapper.Positionable#POSITIONABLE}, the transmisison can't be subtitled or looped. --- +-- -- Note that obviously, the **frequency** and the **modulation** of the transmission are important only if the players are piloting an **Advanced System Modelling** enabled aircraft, -- like the A10C or the Mirage 2000C. They will **hear the transmission** if they are tuned on the **right frequency and modulation** (and if they are close enough - more on that below). -- If an FC3 aircraft is used, it will **hear every communication, whatever the frequency and the modulation** is set to. The same is true for TACAN beacons. If your aircraft isn't compatible, @@ -37,41 +37,41 @@ --- *It's not true I had nothing on, I had the radio on.* -- Marilyn Monroe --- +-- -- # RADIO usage --- +-- -- There are 3 steps to a successful radio transmission. --- +-- -- * First, you need to **"add a @{#RADIO} object** to your @{Wrapper.Positionable#POSITIONABLE}. This is done using the @{Wrapper.Positionable#POSITIONABLE.GetRadio}() function, -- * Then, you will **set the relevant parameters** to the transmission (see below), -- * When done, you can actually **broadcast the transmission** (i.e. play the sound) with the @{RADIO.Broadcast}() function. --- +-- -- Methods to set relevant parameters for both a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP} or any other @{Wrapper.Positionable#POSITIONABLE} --- +-- -- * @{#RADIO.SetFileName}() : Sets the file name of your sound file (e.g. "Noise.ogg"), -- * @{#RADIO.SetFrequency}() : Sets the frequency of your transmission. -- * @{#RADIO.SetModulation}() : Sets the modulation of your transmission. -- * @{#RADIO.SetLoop}() : Choose if you want the transmission to be looped. If you need your transmission to be looped, you might need a @{#BEACON} instead... --- +-- -- Additional Methods to set relevant parameters if the transmitter is a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP} --- +-- -- * @{#RADIO.SetSubtitle}() : Set both the subtitle and its duration, -- * @{#RADIO.NewUnitTransmission}() : Shortcut to set all the relevant parameters in one method call --- +-- -- Additional Methods to set relevant parameters if the transmitter is any other @{Wrapper.Positionable#POSITIONABLE} --- +-- -- * @{#RADIO.SetPower}() : Sets the power of the antenna in Watts -- * @{#RADIO.NewGenericTransmission}() : Shortcut to set all the relevant parameters in one method call --- +-- -- What is this power thing? --- +-- -- * If your transmission is sent by a @{Wrapper.Positionable#POSITIONABLE} other than a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}, you can set the power of the antenna, -- * Otherwise, DCS sets it automatically, depending on what's available on your Unit, -- * If the player gets **too far** from the transmitter, or if the antenna is **too weak**, the transmission will **fade** and **become noisyer**, -- * This an automated DCS calculation you have no say on, -- * For reference, a standard VOR station has a 100 W antenna, a standard AA TACAN has a 120 W antenna, and civilian ATC's antenna usually range between 300 and 500 W, --- * Note that if the transmission has a subtitle, it will be readable, regardless of the quality of the transmission. --- +-- * Note that if the transmission has a subtitle, it will be readable, regardless of the quality of the transmission. +-- -- @type RADIO -- @field Wrapper.Controllable#CONTROLLABLE Positionable The @{#CONTROLLABLE} that will transmit the radio calls. -- @field #string FileName Name of the sound file played. @@ -105,12 +105,12 @@ function RADIO:New(Positionable) -- Inherit base local self = BASE:Inherit( self, BASE:New() ) -- Core.Radio#RADIO self:F(Positionable) - + if Positionable:GetPointVec2() then -- It's stupid, but the only way I found to make sure positionable is valid self.Positionable = Positionable return self end - + self:E({error="The passed positionable is invalid, no RADIO created!", positionable=Positionable}) return nil end @@ -137,19 +137,19 @@ end -- @return #RADIO self function RADIO:SetFileName(FileName) self:F2(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 - + self.FileName = FileName return self end end - + self:E({"File name invalid. Maybe something wrong with the extension?", FileName}) return self end @@ -161,34 +161,34 @@ end -- @return #RADIO self function RADIO:SetFrequency(Frequency) self:F2(Frequency) - + if type(Frequency) == "number" then - + -- If frequency is in range - -- if (Frequency >= 30 and Frequency <= 87.995) or (Frequency >= 108 and Frequency <= 173.995) or (Frequency >= 225 and Frequency <= 399.975) then - + --if (Frequency >= 30 and Frequency <= 87.995) or (Frequency >= 108 and Frequency <= 173.995) or (Frequency >= 225 and Frequency <= 399.975) then + -- Convert frequency from MHz to Hz self.Frequency = Frequency * 1000000 - + -- If the RADIO is attached to a UNIT or a GROUP, we need to send the DCS Command "SetFrequency" to change the UNIT or GROUP frequency if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then - + local commandSetFrequency={ id = "SetFrequency", params = { frequency = self.Frequency, modulation = self.Modulation, } - } - + } + self:T2(commandSetFrequency) self.Positionable:SetCommand(commandSetFrequency) end - + return self - -- end + --end end - + self:E({"Frequency is not a number. Frequency unchanged.", Frequency}) return self end @@ -215,13 +215,13 @@ end -- @return #RADIO self function RADIO:SetPower(Power) self:F2(Power) - + if type(Power) == "number" then self.Power = math.floor(math.abs(Power)) --TODO Find what is the maximum power allowed by DCS and limit power to that else self:E({"Power is invalid. Power unchanged.", self.Power}) end - + return self end @@ -249,7 +249,7 @@ end -- -- create the broadcaster and attaches it a RADIO -- local MyUnit = UNIT:FindByName("MyUnit") -- local MyUnitRadio = MyUnit:GetRadio() --- +-- -- -- add a subtitle for the next transmission, which will be up for 10s -- MyUnitRadio:SetSubtitle("My Subtitle, 10) function RADIO:SetSubtitle(Subtitle, SubtitleDuration) @@ -264,14 +264,14 @@ function RADIO:SetSubtitle(Subtitle, SubtitleDuration) self.SubtitleDuration = SubtitleDuration else self.SubtitleDuration = 0 - self:E({"SubtitleDuration is invalid. SubtitleDuration reset.", self.SubtitleDuration}) + self:E({"SubtitleDuration is invalid. SubtitleDuration reset.", self.SubtitleDuration}) end return self end --- Create a new transmission, that is to say, populate the RADIO with relevant data -- In this function the data is especially relevant if the broadcaster is anything but a UNIT or a GROUP, --- but it will work with a UNIT or a GROUP anyway. +-- but it will work with a UNIT or a GROUP anyway. -- Only the #RADIO and the Filename are mandatory -- @param #RADIO self -- @param #string FileName Name of the sound file that will be transmitted. @@ -281,20 +281,20 @@ end -- @return #RADIO self function RADIO:NewGenericTransmission(FileName, Frequency, Modulation, Power, Loop) self:F({FileName, Frequency, Modulation, Power}) - + self:SetFileName(FileName) if Frequency then self:SetFrequency(Frequency) end if Modulation then self:SetModulation(Modulation) end if Power then self:SetPower(Power) end if Loop then self:SetLoop(Loop) end - + return self end --- Create a new transmission, that is to say, populate the RADIO with relevant data -- In this function the data is especially relevant if the broadcaster is a UNIT or a GROUP, --- but it will work for any @{Wrapper.Positionable#POSITIONABLE}. +-- but it will work for any @{Wrapper.Positionable#POSITIONABLE}. -- Only the RADIO and the Filename are mandatory. -- @param #RADIO self -- @param #string FileName Name of sound file. @@ -316,20 +316,20 @@ function RADIO:NewUnitTransmission(FileName, Subtitle, SubtitleDuration, Frequen end -- Set frequency. - if Frequency then + if Frequency then self:SetFrequency(Frequency) end - + -- Set subtitle. if Subtitle then self:SetSubtitle(Subtitle, SubtitleDuration or 0) end - + -- Set Looping. - if Loop then + if Loop then self:SetLoop(Loop) end - + return self end @@ -346,7 +346,7 @@ end -- @return #RADIO self function RADIO:Broadcast(viatrigger) self:F({viatrigger=viatrigger}) - + -- If the POSITIONABLE is actually a UNIT or a GROUP, use the more complicated DCS command system. if (self.Positionable.ClassName=="UNIT" or self.Positionable.ClassName=="GROUP") and (not viatrigger) then self:T("Broadcasting from a UNIT or a GROUP") @@ -359,7 +359,7 @@ function RADIO:Broadcast(viatrigger) subtitle = self.Subtitle, loop = self.Loop, }} - + self:T3(commandTransmitMessage) self.Positionable:SetCommand(commandTransmitMessage) else @@ -368,7 +368,7 @@ function RADIO:Broadcast(viatrigger) self:T("Broadcasting from a POSITIONABLE") trigger.action.radioTransmission(self.FileName, self.Positionable:GetPositionVec3(), self.Modulation, self.Loop, self.Frequency, self.Power, tostring(self.ID)) end - + return self end @@ -380,11 +380,11 @@ end -- @return #RADIO self function RADIO:StopBroadcast() self:F() - -- If the POSITIONABLE is a UNIT or a GROUP, stop the transmission with the DCS "StopTransmission" command + -- If the POSITIONABLE is a UNIT or a GROUP, stop the transmission with the DCS "StopTransmission" command if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then - + local commandStopTransmission={id="StopTransmission", params={}} - + self.Positionable:SetCommand(commandStopTransmission) else -- Else, we use the appropriate singleton funciton diff --git a/Moose Development/Moose/Sound/RadioSpeech.lua b/Moose Development/Moose/Sound/RadioSpeech.lua index 5d748cae8..ae951053f 100644 --- a/Moose Development/Moose/Sound/RadioSpeech.lua +++ b/Moose Development/Moose/Sound/RadioSpeech.lua @@ -1,9 +1,9 @@ --- **Core** - Makes the radio talk. --- +-- -- === --- +-- -- ## Features: --- +-- -- * Send text strings using a vocabulary that is converted in spoken language. -- * Possiblity to implement multiple language. -- @@ -15,10 +15,10 @@ -- @image Core_Radio.JPG --- Makes the radio speak. --- +-- -- # RADIOSPEECH usage --- --- +-- +-- -- @type RADIOSPEECH -- @extends Core.RadioQueue#RADIOQUEUE RADIOSPEECH = { @@ -59,24 +59,24 @@ RADIOSPEECH.Vocabulary.EN = { ["70"] = { "70", 0.48 }, ["80"] = { "80", 0.26 }, ["90"] = { "90", 0.36 }, - ["100"] = { "100", 0.55 }, - ["200"] = { "200", 0.55 }, - ["300"] = { "300", 0.61 }, - ["400"] = { "400", 0.60 }, - ["500"] = { "500", 0.61 }, - ["600"] = { "600", 0.65 }, - ["700"] = { "700", 0.70 }, - ["800"] = { "800", 0.54 }, - ["900"] = { "900", 0.60 }, - ["1000"] = { "1000", 0.60 }, - ["2000"] = { "2000", 0.61 }, - ["3000"] = { "3000", 0.64 }, - ["4000"] = { "4000", 0.62 }, - ["5000"] = { "5000", 0.69 }, - ["6000"] = { "6000", 0.69 }, - ["7000"] = { "7000", 0.75 }, - ["8000"] = { "8000", 0.59 }, - ["9000"] = { "9000", 0.65 }, + ["100"] = { "100", 0.55 }, + ["200"] = { "200", 0.55 }, + ["300"] = { "300", 0.61 }, + ["400"] = { "400", 0.60 }, + ["500"] = { "500", 0.61 }, + ["600"] = { "600", 0.65 }, + ["700"] = { "700", 0.70 }, + ["800"] = { "800", 0.54 }, + ["900"] = { "900", 0.60 }, + ["1000"] = { "1000", 0.60 }, + ["2000"] = { "2000", 0.61 }, + ["3000"] = { "3000", 0.64 }, + ["4000"] = { "4000", 0.62 }, + ["5000"] = { "5000", 0.69 }, + ["6000"] = { "6000", 0.69 }, + ["7000"] = { "7000", 0.75 }, + ["8000"] = { "8000", 0.59 }, + ["9000"] = { "9000", 0.65 }, ["chevy"] = { "chevy", 0.35 }, ["colt"] = { "colt", 0.35 }, @@ -94,10 +94,10 @@ RADIOSPEECH.Vocabulary.EN = { ["meters"] = { "meters", 0.41 }, ["mi"] = { "miles", 0.45 }, ["feet"] = { "feet", 0.29 }, - + ["br"] = { "br", 1.1 }, ["bra"] = { "bra", 0.3 }, - + ["returning to base"] = { "returning_to_base", 0.85 }, ["on route to ground target"] = { "on_route_to_ground_target", 1.05 }, @@ -143,24 +143,24 @@ RADIOSPEECH.Vocabulary.RU = { ["70"] = { "70", 0.68 }, ["80"] = { "80", 0.84 }, ["90"] = { "90", 0.71 }, - ["100"] = { "100", 0.35 }, - ["200"] = { "200", 0.59 }, - ["300"] = { "300", 0.53 }, - ["400"] = { "400", 0.70 }, - ["500"] = { "500", 0.50 }, - ["600"] = { "600", 0.58 }, - ["700"] = { "700", 0.64 }, - ["800"] = { "800", 0.77 }, - ["900"] = { "900", 0.75 }, - ["1000"] = { "1000", 0.87 }, - ["2000"] = { "2000", 0.83 }, - ["3000"] = { "3000", 0.84 }, - ["4000"] = { "4000", 1.00 }, - ["5000"] = { "5000", 0.77 }, - ["6000"] = { "6000", 0.90 }, - ["7000"] = { "7000", 0.77 }, - ["8000"] = { "8000", 0.92 }, - ["9000"] = { "9000", 0.87 }, + ["100"] = { "100", 0.35 }, + ["200"] = { "200", 0.59 }, + ["300"] = { "300", 0.53 }, + ["400"] = { "400", 0.70 }, + ["500"] = { "500", 0.50 }, + ["600"] = { "600", 0.58 }, + ["700"] = { "700", 0.64 }, + ["800"] = { "800", 0.77 }, + ["900"] = { "900", 0.75 }, + ["1000"] = { "1000", 0.87 }, + ["2000"] = { "2000", 0.83 }, + ["3000"] = { "3000", 0.84 }, + ["4000"] = { "4000", 1.00 }, + ["5000"] = { "5000", 0.77 }, + ["6000"] = { "6000", 0.90 }, + ["7000"] = { "7000", 0.77 }, + ["8000"] = { "8000", 0.92 }, + ["9000"] = { "9000", 0.87 }, ["градусы"] = { "degrees", 0.5 }, ["километры"] = { "kilometers", 0.65 }, @@ -170,10 +170,11 @@ RADIOSPEECH.Vocabulary.RU = { ["метров"] = { "meters", 0.41 }, ["m"] = { "meters", 0.41 }, ["ноги"] = { "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 }, @@ -199,11 +200,11 @@ function RADIOSPEECH:New(frequency, modulation) -- Inherit base local self = BASE:Inherit( self, RADIOQUEUE:New( frequency, modulation ) ) -- #RADIOSPEECH - + self.Language = "EN" - + self:BuildTree() - + return self end @@ -261,7 +262,7 @@ end function RADIOSPEECH:BuildTree() self.Speech = {} - + for Language, Sentences in pairs( self.Vocabulary ) do self:I( { Language = Language, Sentences = Sentences }) self.Speech[Language] = {} @@ -270,7 +271,7 @@ function RADIOSPEECH:BuildTree() self:AddSentenceToSpeech( Sentence, self.Speech[Language], Sentence, Data ) end end - + self:I( { Speech = self.Speech } ) return self @@ -289,7 +290,7 @@ function RADIOSPEECH:SpeakWords( Sentence, Speech, Language ) local Word, RemainderSentence = Sentence:match( "^[., ]*([^ .,]+)(.*)" ) self:I( { Word = Word, Speech = Speech[Word], RemainderSentence = RemainderSentence } ) - + if Word then if Word ~= "" and tonumber(Word) == nil then @@ -301,7 +302,7 @@ function RADIOSPEECH:SpeakWords( Sentence, Speech, Language ) if Speech[Word].Next == nil then self:I( { Sentence = Speech[Word].Sentence, Data = Speech[Word].Data } ) self:NewTransmission( Speech[Word].Data[1] .. ".wav", Speech[Word].Data[2], Language .. "/" ) - else + else if RemainderSentence and RemainderSentence ~= "" then return self:SpeakWords( RemainderSentence, Speech[Word].Next, Language ) end @@ -309,11 +310,11 @@ function RADIOSPEECH:SpeakWords( Sentence, Speech, Language ) end return RemainderSentence end - return OriginalSentence + return OriginalSentence else return "" - end - + end + end --- Speak a sentence. @@ -332,7 +333,7 @@ function RADIOSPEECH:SpeakDigits( Sentence, Speech, Langauge ) if Digits then if Digits ~= "" and tonumber( Digits ) ~= nil then - + -- Construct numbers local Number = tonumber( Digits ) local Multiple = nil @@ -356,7 +357,7 @@ function RADIOSPEECH:SpeakDigits( Sentence, Speech, Langauge ) end return RemainderSentence end - return OriginalSentence + return OriginalSentence else return "" end @@ -373,26 +374,26 @@ function RADIOSPEECH:Speak( Sentence, Language ) self:I( { Sentence, Language } ) local Language = Language or "EN" - + self:I( { Language = Language } ) - + -- If there is no node for Speech, then we start at the first nodes of the language. local Speech = self.Speech[Language] - + self:I( { Speech = Speech, Language = Language } ) - + self:NewTransmission( "_In.wav", 0.52, Language .. "/" ) - + repeat Sentence = self:SpeakWords( Sentence, Speech, Language ) - + self:I( { Sentence = Sentence } ) Sentence = self:SpeakDigits( Sentence, Speech, Language ) self:I( { Sentence = Sentence } ) - + -- Sentence = self:SpeakSymbols( Sentence, Speech ) -- -- self:I( { Sentence = Sentence } ) diff --git a/Moose Development/Moose/Sound/SRS.lua b/Moose Development/Moose/Sound/SRS.lua index 1c66e30ea..0d91736c0 100644 --- a/Moose Development/Moose/Sound/SRS.lua +++ b/Moose Development/Moose/Sound/SRS.lua @@ -1098,3 +1098,5 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + diff --git a/Moose Development/Moose/Sound/UserSound.lua b/Moose Development/Moose/Sound/UserSound.lua index b5669770d..cbccffe12 100644 --- a/Moose Development/Moose/Sound/UserSound.lua +++ b/Moose Development/Moose/Sound/UserSound.lua @@ -144,7 +144,7 @@ do -- UserSound -- @return #USERSOUND The usersound instance. -- @usage -- local BlueVictory = USERSOUND:New( "BlueVictory.ogg" ) - -- local PlayerUnit = UNIT:FindByName( "PlayerUnit" ) -- Search for the active group named "PlayerUnit", a human player. + -- local PlayerUnit = UNIT:FindByName( "PlayerUnit" ) -- Search for the active unit named "PlayerUnit", a human player. -- BlueVictory:ToUnit( PlayerUnit ) -- Play the victory sound to the player unit. -- function USERSOUND:ToUnit( Unit, Delay ) diff --git a/Moose Development/Moose/Utilities/Enums.lua b/Moose Development/Moose/Utilities/Enums.lua index f48f584a3..3a20ad388 100644 --- a/Moose Development/Moose/Utilities/Enums.lua +++ b/Moose Development/Moose/Utilities/Enums.lua @@ -509,7 +509,7 @@ ENUMS.ReportingName = Atlas = "A400", Lancer = "B1-B", Stratofortress = "B-52H", - Hercules = "C-130", + Hercules = "C-130", Super_Hercules = "Hercules", Globemaster = "C-17", Greyhound = "C-2A", diff --git a/Moose Development/Moose/Utilities/FiFo.lua b/Moose Development/Moose/Utilities/FiFo.lua index b004de791..323cc7526 100644 --- a/Moose Development/Moose/Utilities/FiFo.lua +++ b/Moose Development/Moose/Utilities/FiFo.lua @@ -20,11 +20,11 @@ do -- @type FIFO -- @field #string ClassName Name of the class. -- @field #string lid Class id string for output to DCS log file. --- @field #string version Version of FiFo --- @field #number counter --- @field #number pointer --- @field #table stackbypointer --- @field #table stackbyid +-- @field #string version Version of FiFo. +-- @field #number counter Counter. +-- @field #number pointer Pointer. +-- @field #table stackbypointer Stack by pointer. +-- @field #table stackbyid Stack by ID. -- @extends Core.Base#BASE --- @@ -45,12 +45,12 @@ FIFO = { stackbyid = {} } ---- Instantiate a new FIFO Stack +--- Instantiate a new FIFO Stack. -- @param #FIFO self -- @return #FIFO self function FIFO:New() -- Inherit everything from BASE class. - local self=BASE:Inherit(self, BASE:New()) + local self=BASE:Inherit(self, BASE:New()) --#FIFO self.pointer = 0 self.counter = 0 self.stackbypointer = {} @@ -62,7 +62,7 @@ function FIFO:New() return self end ---- Empty FIFO Stack +--- Empty FIFO Stack. -- @param #FIFO self -- @return #FIFO self function FIFO:Clear() @@ -77,7 +77,7 @@ function FIFO:Clear() return self end ---- FIFO Push Object to Stack +--- FIFO Push Object to Stack. -- @param #FIFO self -- @param #table Object -- @param #string UniqueID (optional) - will default to current pointer + 1. Note - if you intend to use `FIFO:GetIDStackSorted()` keep the UniqueID numerical! @@ -97,7 +97,7 @@ function FIFO:Push(Object,UniqueID) return self end ---- FIFO Pull Object from Stack +--- FIFO Pull Object from Stack. -- @param #FIFO self -- @return #table Object or nil if stack is empty function FIFO:Pull() diff --git a/Moose Development/Moose/Utilities/Profiler.lua b/Moose Development/Moose/Utilities/Profiler.lua index 7df211b0c..66240179d 100644 --- a/Moose Development/Moose/Utilities/Profiler.lua +++ b/Moose Development/Moose/Utilities/Profiler.lua @@ -7,7 +7,7 @@ -- ### Author: **TAW CougarNL**, *funkyfranky* -- -- @module Utilities.PROFILER --- @image MOOSE.JPG +-- @image Utils_Profiler.jpg --- PROFILER class. -- @type PROFILER @@ -24,7 +24,8 @@ -- @field #number ThreshTtot Total time threshold. Only write output if total function CPU time is more than this value. -- @field #string fileNamePrefix Output file name prefix, e.g. "MooseProfiler". -- @field #string fileNameSuffix Output file name prefix, e.g. "txt" ---- *The emperor counsels simplicity. First principles. Of each particular thing, ask: What is it in itself, in its own constitution? What is its causal nature? * + +--- *The emperor counsels simplicity.* *First principles. Of each particular thing, ask: What is it in itself, in its own constitution? What is its causal nature?* -- -- === -- @@ -33,11 +34,24 @@ -- # The PROFILER Concept -- -- Profile your lua code. This tells you, which functions are called very often and which consume most real time. --- With this information you can optimize the performance of your code. +-- With this information you can optimize the perfomance of your code. -- -- # Prerequisites -- --- The modules **os** and **lfs** need to be de-sanitized. +-- The modules **os**, **io** and **lfs** need to be desanizied. Comment out the lines +-- +-- --sanitizeModule('os') +-- --sanitizeModule('io') +-- --sanitizeModule('lfs') +-- +-- in your *"DCS World OpenBeta/Scripts/MissionScripting.lua"* file. +-- +-- But be aware that these changes can make you system vulnerable to attacks. +-- +-- # Disclaimer +-- +-- **Profiling itself is CPU expensive!** Don't use this when you want to fly or host a mission. +-- -- -- # Start -- @@ -123,12 +137,12 @@ function PROFILER.Start( Delay, Duration ) go = false end if not io then - env.error( "ERROR: Profiler needs io to be de-sanitized!" ) - go = false + env.error("ERROR: Profiler needs io to be desanitized!") + go=false end if not lfs then - env.error( "ERROR: Profiler needs lfs to be de-sanitized!" ) - go = false + env.error("ERROR: Profiler needs lfs to be desanitized!") + go=false end if not go then return @@ -139,11 +153,11 @@ function PROFILER.Start( Delay, Duration ) else -- Set start time. - PROFILER.TstartGame = timer.getTime() - PROFILER.TstartOS = os.clock() + PROFILER.TstartGame=timer.getTime() + PROFILER.TstartOS=os.clock() -- Add event handler. - world.addEventHandler( PROFILER.eventHandler ) + world.addEventHandler(PROFILER.eventHandler) -- Info in log. env.info( '############################ Profiler Started ############################' ) @@ -152,18 +166,19 @@ function PROFILER.Start( Delay, Duration ) else env.info( string.format( "- Will be stopped when mission ends" ) ) end - env.info( string.format( "- Calls per second threshold %.3f/sec", PROFILER.ThreshCPS ) ) - env.info( string.format( "- Total function time threshold %.3f sec", PROFILER.ThreshTtot ) ) - env.info( string.format( "- Output file \"%s\" in your DCS log file folder", PROFILER.getfilename( PROFILER.fileNameSuffix ) ) ) - env.info( string.format( "- Output file \"%s\" in CSV format", PROFILER.getfilename( "csv" ) ) ) - env.info( '###############################################################################' ) + env.info(string.format("- Calls per second threshold %.3f/sec", PROFILER.ThreshCPS)) + env.info(string.format("- Total function time threshold %.3f sec", PROFILER.ThreshTtot)) + env.info(string.format("- Output file \"%s\" in your DCS log file folder", PROFILER.getfilename(PROFILER.fileNameSuffix))) + env.info(string.format("- Output file \"%s\" in CSV format", PROFILER.getfilename("csv"))) + env.info('###############################################################################') + -- Message on screen - local duration = Duration or 600 - trigger.action.outText( "### Profiler running ###", duration ) + local duration=Duration or 600 + trigger.action.outText("### Profiler running ###", duration) -- Set hook. - debug.sethook( PROFILER.hook, "cr" ) + debug.sethook(PROFILER.hook, "cr") -- Auto stop profiler. if Duration then @@ -181,20 +196,29 @@ function PROFILER.Stop( Delay ) if Delay and Delay > 0 then BASE:ScheduleOnce( Delay, PROFILER.Stop ) + end +end + +function PROFILER.Stop(Delay) + + if Delay and Delay>0 then + + BASE:ScheduleOnce(Delay, PROFILER.Stop) else -- Remove hook. debug.sethook() + -- Run time game. - local runTimeGame = timer.getTime() - PROFILER.TstartGame + local runTimeGame=timer.getTime()-PROFILER.TstartGame -- Run time real OS. - local runTimeOS = os.clock() - PROFILER.TstartOS + local runTimeOS=os.clock()-PROFILER.TstartOS -- Show info. - PROFILER.showInfo( runTimeGame, runTimeOS ) + PROFILER.showInfo(runTimeGame, runTimeOS) end @@ -213,34 +237,34 @@ end --- Debug hook. -- @param #table event Event. -function PROFILER.hook( event ) +function PROFILER.hook(event) - local f = debug.getinfo( 2, "f" ).func + local f=debug.getinfo(2, "f").func - if event == 'call' then + if event=='call' then - if PROFILER.Counters[f] == nil then + if PROFILER.Counters[f]==nil then - PROFILER.Counters[f] = 1 - PROFILER.dInfo[f] = debug.getinfo( 2, "Sn" ) + PROFILER.Counters[f]=1 + PROFILER.dInfo[f]=debug.getinfo(2,"Sn") - if PROFILER.fTimeTotal[f] == nil then - PROFILER.fTimeTotal[f] = 0 + if PROFILER.fTimeTotal[f]==nil then + PROFILER.fTimeTotal[f]=0 end else PROFILER.Counters[f] = PROFILER.Counters[f] + 1 end - if PROFILER.fTime[f] == nil then - PROFILER.fTime[f] = os.clock() + if PROFILER.fTime[f]==nil then + PROFILER.fTime[f]=os.clock() end - elseif (event == 'return') then + elseif (event=='return') then - if PROFILER.fTime[f] ~= nil then - PROFILER.fTimeTotal[f] = PROFILER.fTimeTotal[f] + (os.clock() - PROFILER.fTime[f]) - PROFILER.fTime[f] = nil + if PROFILER.fTime[f]~=nil then + PROFILER.fTimeTotal[f]=PROFILER.fTimeTotal[f]+(os.clock()-PROFILER.fTime[f]) + PROFILER.fTime[f]=nil end end @@ -259,9 +283,9 @@ end -- @return #number Function time in seconds. function PROFILER.getData( func ) - local n = PROFILER.dInfo[func] + local n=PROFILER.dInfo[func] - if n.what == "C" then + if n.what=="C" then return n.name, "?", "?", PROFILER.fTimeTotal[func] end @@ -282,20 +306,20 @@ end function PROFILER.showTable( data, f, runTimeGame ) -- Loop over data. - for i = 1, #data do - local t = data[i] -- #PROFILER.Data + for i=1, #data do + local t=data[i] --#PROFILER.Data -- Calls per second. - local cps = t.count / runTimeGame + local cps=t.count/runTimeGame - local threshCPS = cps >= PROFILER.ThreshCPS - local threshTot = t.tm >= PROFILER.ThreshTtot + local threshCPS=cps>=PROFILER.ThreshCPS + local threshTot=t.tm>=PROFILER.ThreshTtot if threshCPS and threshTot then -- Output - local text = string.format( "%30s: %8d calls %8.1f/sec - Time Total %8.3f sec (%.3f %%) %5.3f sec/call %s line %s", t.func, t.count, cps, t.tm, t.tm / runTimeGame * 100, t.tm / t.count, tostring( t.src ), tostring( t.line ) ) - PROFILER._flog( f, text ) + local text=string.format("%30s: %8d calls %8.1f/sec - Time Total %8.3f sec (%.3f %%) %5.3f sec/call %s line %s", t.func, t.count, cps, t.tm, t.tm/runTimeGame*100, t.tm/t.count, tostring(t.src), tostring(t.line)) + PROFILER._flog(f, text) end end @@ -312,19 +336,19 @@ function PROFILER.printCSV( data, runTimeGame ) local g = io.open( file, 'w' ) -- Header. - local text = "Function,Total Calls,Calls per Sec,Total Time,Total in %,Sec per Call,Source File;Line Number," - g:write( text .. "\r\n" ) + local text="Function,Total Calls,Calls per Sec,Total Time,Total in %,Sec per Call,Source File;Line Number," + g:write(text.."\r\n") -- Loop over data. - for i = 1, #data do - local t = data[i] -- #PROFILER.Data + for i=1, #data do + local t=data[i] --#PROFILER.Data -- Calls per second. local cps = t.count / runTimeGame -- Output - local txt = string.format( "%s,%d,%.1f,%.3f,%.3f,%.3f,%s,%s,", t.func, t.count, cps, t.tm, t.tm / runTimeGame * 100, t.tm / t.count, tostring( t.src ), tostring( t.line ) ) - g:write( txt .. "\r\n" ) + local txt=string.format("%s,%d,%.1f,%.3f,%.3f,%.3f,%s,%s,", t.func, t.count, cps, t.tm, t.tm/runTimeGame*100, t.tm/t.count, tostring(t.src), tostring(t.line)) + g:write(txt.."\r\n") end @@ -335,15 +359,15 @@ end --- Write info to output file. -- @param #string ext Extension. -- @return #string File name. -function PROFILER.getfilename( ext ) +function PROFILER.getfilename(ext) - local dir = lfs.writedir() .. [[Logs\]] + local dir=lfs.writedir()..[[Logs\]] - ext = ext or PROFILER.fileNameSuffix + ext=ext or PROFILER.fileNameSuffix - local file = dir .. PROFILER.fileNamePrefix .. "." .. ext + local file=dir..PROFILER.fileNamePrefix.."."..ext - if not UTILS.FileExists( file ) then + if not UTILS.FileExists(file) then return file end @@ -365,34 +389,39 @@ end function PROFILER.showInfo( runTimeGame, runTimeOS ) -- Output file. - local file = PROFILER.getfilename( PROFILER.fileNameSuffix ) - local f = io.open( file, 'w' ) + local file=PROFILER.getfilename(PROFILER.fileNameSuffix) + local f=io.open(file, 'w') -- Gather data. - local Ttot = 0 - local Calls = 0 + local Ttot=0 + local Calls=0 - local t = {} + local t={} - local tcopy = nil -- #PROFILER.Data - local tserialize = nil -- #PROFILER.Data - local tforgen = nil -- #PROFILER.Data - local tpairs = nil -- #PROFILER.Data + local tcopy=nil --#PROFILER.Data + local tserialize=nil --#PROFILER.Data + local tforgen=nil --#PROFILER.Data + local tpairs=nil --#PROFILER.Data - for func, count in pairs( PROFILER.Counters ) do - local s, src, line, tm = PROFILER.getData( func ) + for func, count in pairs(PROFILER.Counters) do - if PROFILER.logUnknown == true then - if s == nil then - s = "" - end + local s,src,line,tm=PROFILER.getData(func) + + if PROFILER.logUnknown==true then + if s==nil then s="" end end - if s ~= nil then + if s~=nil then -- Profile data. - local T = { func = s, src = src, line = line, count = count, tm = tm } -- #PROFILER.Data + local T= + { func=s, + src=src, + line=line, + count=count, + tm=tm, + } --#PROFILER.Data -- Collect special cases. Somehow, e.g. "_copy" appears multiple times so we try to gather all data. if s == "_copy" then @@ -406,119 +435,113 @@ function PROFILER.showInfo( runTimeGame, runTimeOS ) if tserialize == nil then tserialize = T else - tserialize.count = tserialize.count + T.count - tserialize.tm = tserialize.tm + T.tm + tserialize.count=tserialize.count+T.count + tserialize.tm=tserialize.tm+T.tm end - elseif s == "(for generator)" then - if tforgen == nil then - tforgen = T + elseif s=="(for generator)" then + if tforgen==nil then + tforgen=T else - tforgen.count = tforgen.count + T.count - tforgen.tm = tforgen.tm + T.tm + tforgen.count=tforgen.count+T.count + tforgen.tm=tforgen.tm+T.tm end - elseif s == "pairs" then - if tpairs == nil then - tpairs = T + elseif s=="pairs" then + if tpairs==nil then + tpairs=T else - tpairs.count = tpairs.count + T.count - tpairs.tm = tpairs.tm + T.tm + tpairs.count=tpairs.count+T.count + tpairs.tm=tpairs.tm+T.tm end else table.insert( t, T ) end -- Total function time. - Ttot = Ttot + tm + Ttot=Ttot+tm -- Total number of calls. - Calls = Calls + count + Calls=Calls+count end end - -- Add special cases. + -- Add special cases. if tcopy then table.insert( t, tcopy ) end if tserialize then - table.insert( t, tserialize ) + table.insert(t, tserialize) end if tforgen then table.insert( t, tforgen ) end if tpairs then - table.insert( t, tpairs ) + table.insert(t, tpairs) end - env.info( '############################ Profiler Stopped ############################' ) - env.info( string.format( "* Runtime Game : %s = %d sec", UTILS.SecondsToClock( runTimeGame, true ), runTimeGame ) ) - env.info( string.format( "* Runtime Real : %s = %d sec", UTILS.SecondsToClock( runTimeOS, true ), runTimeOS ) ) - env.info( string.format( "* Function time : %s = %.1f sec (%.1f percent of runtime game)", UTILS.SecondsToClock( Ttot, true ), Ttot, Ttot / runTimeGame * 100 ) ) - env.info( string.format( "* Total functions : %d", #t ) ) - env.info( string.format( "* Total func calls : %d", Calls ) ) - env.info( string.format( "* Writing to file : \"%s\"", file ) ) - env.info( string.format( "* Writing to file : \"%s\"", PROFILER.getfilename( "csv" ) ) ) - env.info( "##############################################################################" ) + env.info('############################ Profiler Stopped ############################') + env.info(string.format("* Runtime Game : %s = %d sec", UTILS.SecondsToClock(runTimeGame, true), runTimeGame)) + env.info(string.format("* Runtime Real : %s = %d sec", UTILS.SecondsToClock(runTimeOS, true), runTimeOS)) + env.info(string.format("* Function time : %s = %.1f sec (%.1f percent of runtime game)", UTILS.SecondsToClock(Ttot, true), Ttot, Ttot/runTimeGame*100)) + env.info(string.format("* Total functions : %d", #t)) + env.info(string.format("* Total func calls : %d", Calls)) + env.info(string.format("* Writing to file : \"%s\"", file)) + env.info(string.format("* Writing to file : \"%s\"", PROFILER.getfilename("csv"))) + env.info("##############################################################################") -- Sort by total time. - table.sort( t, function( a, b ) - return a.tm > b.tm - end ) + table.sort(t, function(a,b) return a.tm>b.tm end) -- Write data. - PROFILER._flog( f, "" ) - PROFILER._flog( f, "************************************************************************************************************************" ) - PROFILER._flog( f, "************************************************************************************************************************" ) - PROFILER._flog( f, "************************************************************************************************************************" ) - PROFILER._flog( f, "" ) - PROFILER._flog( f, "-------------------------" ) - PROFILER._flog( f, "---- Profiler Report ----" ) - PROFILER._flog( f, "-------------------------" ) - PROFILER._flog( f, "" ) - PROFILER._flog( f, string.format( "* Runtime Game : %s = %.1f sec", UTILS.SecondsToClock( runTimeGame, true ), runTimeGame ) ) - PROFILER._flog( f, string.format( "* Runtime Real : %s = %.1f sec", UTILS.SecondsToClock( runTimeOS, true ), runTimeOS ) ) - PROFILER._flog( f, string.format( "* Function time : %s = %.1f sec (%.1f %% of runtime game)", UTILS.SecondsToClock( Ttot, true ), Ttot, Ttot / runTimeGame * 100 ) ) - PROFILER._flog( f, "" ) - PROFILER._flog( f, string.format( "* Total functions = %d", #t ) ) - PROFILER._flog( f, string.format( "* Total func calls = %d", Calls ) ) - PROFILER._flog( f, "" ) - PROFILER._flog( f, string.format( "* Calls per second threshold = %.3f/sec", PROFILER.ThreshCPS ) ) - PROFILER._flog( f, string.format( "* Total func time threshold = %.3f sec", PROFILER.ThreshTtot ) ) - PROFILER._flog( f, "" ) - PROFILER._flog( f, "************************************************************************************************************************" ) - PROFILER._flog( f, "" ) - PROFILER.showTable( t, f, runTimeGame ) + PROFILER._flog(f,"") + PROFILER._flog(f,"************************************************************************************************************************") + PROFILER._flog(f,"************************************************************************************************************************") + PROFILER._flog(f,"************************************************************************************************************************") + PROFILER._flog(f,"") + PROFILER._flog(f,"-------------------------") + PROFILER._flog(f,"---- Profiler Report ----") + PROFILER._flog(f,"-------------------------") + PROFILER._flog(f,"") + PROFILER._flog(f,string.format("* Runtime Game : %s = %.1f sec", UTILS.SecondsToClock(runTimeGame, true), runTimeGame)) + PROFILER._flog(f,string.format("* Runtime Real : %s = %.1f sec", UTILS.SecondsToClock(runTimeOS, true), runTimeOS)) + PROFILER._flog(f,string.format("* Function time : %s = %.1f sec (%.1f %% of runtime game)", UTILS.SecondsToClock(Ttot, true), Ttot, Ttot/runTimeGame*100)) + PROFILER._flog(f,"") + PROFILER._flog(f,string.format("* Total functions = %d", #t)) + PROFILER._flog(f,string.format("* Total func calls = %d", Calls)) + PROFILER._flog(f,"") + PROFILER._flog(f,string.format("* Calls per second threshold = %.3f/sec", PROFILER.ThreshCPS)) + PROFILER._flog(f,string.format("* Total func time threshold = %.3f sec", PROFILER.ThreshTtot)) + PROFILER._flog(f,"") + PROFILER._flog(f,"************************************************************************************************************************") + PROFILER._flog(f,"") + PROFILER.showTable(t, f, runTimeGame) -- Sort by number of calls. - table.sort( t, function( a, b ) - return a.tm / a.count > b.tm / b.count - end ) + table.sort(t, function(a,b) return a.tm/a.count>b.tm/b.count end) -- Detailed data. - PROFILER._flog( f, "" ) - PROFILER._flog( f, "************************************************************************************************************************" ) - PROFILER._flog( f, "" ) - PROFILER._flog( f, "--------------------------------------" ) - PROFILER._flog( f, "---- Data Sorted by Time per Call ----" ) - PROFILER._flog( f, "--------------------------------------" ) - PROFILER._flog( f, "" ) - PROFILER.showTable( t, f, runTimeGame ) + PROFILER._flog(f,"") + PROFILER._flog(f,"************************************************************************************************************************") + PROFILER._flog(f,"") + PROFILER._flog(f,"--------------------------------------") + PROFILER._flog(f,"---- Data Sorted by Time per Call ----") + PROFILER._flog(f,"--------------------------------------") + PROFILER._flog(f,"") + PROFILER.showTable(t, f, runTimeGame) -- Sort by number of calls. - table.sort( t, function( a, b ) - return a.count > b.count - end ) + table.sort(t, function(a,b) return a.count>b.count end) -- Detailed data. - PROFILER._flog( f, "" ) - PROFILER._flog( f, "************************************************************************************************************************" ) - PROFILER._flog( f, "" ) - PROFILER._flog( f, "------------------------------------" ) - PROFILER._flog( f, "---- Data Sorted by Total Calls ----" ) - PROFILER._flog( f, "------------------------------------" ) - PROFILER._flog( f, "" ) - PROFILER.showTable( t, f, runTimeGame ) + PROFILER._flog(f,"") + PROFILER._flog(f,"************************************************************************************************************************") + PROFILER._flog(f,"") + PROFILER._flog(f,"------------------------------------") + PROFILER._flog(f,"---- Data Sorted by Total Calls ----") + PROFILER._flog(f,"------------------------------------") + PROFILER._flog(f,"") + PROFILER.showTable(t, f, runTimeGame) -- Closing. PROFILER._flog( f, "" ) diff --git a/Moose Development/Moose/Utilities/Routines.lua b/Moose Development/Moose/Utilities/Routines.lua index 83d52180d..926c8686d 100644 --- a/Moose Development/Moose/Utilities/Routines.lua +++ b/Moose Development/Moose/Utilities/Routines.lua @@ -494,330 +494,343 @@ routines.ground = {} routines.fixedWing = {} routines.heli = {} -routines.ground.buildWP = function( point, overRideForm, overRideSpeed ) +routines.ground.buildWP = function(point, overRideForm, overRideSpeed) - local wp = {} - wp.x = point.x + local wp = {} + wp.x = point.x - if point.z then - wp.y = point.z - else - wp.y = point.y - end - local form, speed + if point.z then + wp.y = point.z + else + wp.y = point.y + end + local form, speed - if point.speed and not overRideSpeed then - wp.speed = point.speed - elseif type( overRideSpeed ) == 'number' then - wp.speed = overRideSpeed - else - wp.speed = routines.utils.kmphToMps( 20 ) - end + if point.speed and not overRideSpeed then + wp.speed = point.speed + elseif type(overRideSpeed) == 'number' then + wp.speed = overRideSpeed + else + wp.speed = routines.utils.kmphToMps(20) + end - if point.form and not overRideForm then - form = point.form - else - form = overRideForm - end + if point.form and not overRideForm then + form = point.form + else + form = overRideForm + end - if not form then - wp.action = 'Cone' - else - form = string.lower( form ) - if form == 'off_road' or form == 'off road' then - wp.action = 'Off Road' - elseif form == 'on_road' or form == 'on road' then - wp.action = 'On Road' - elseif form == 'rank' or form == 'line_abrest' or form == 'line abrest' or form == 'lineabrest' then - wp.action = 'Rank' - elseif form == 'cone' then - wp.action = 'Cone' - elseif form == 'diamond' then - wp.action = 'Diamond' - elseif form == 'vee' then - wp.action = 'Vee' - elseif form == 'echelon_left' or form == 'echelon left' or form == 'echelonl' then - wp.action = 'EchelonL' - elseif form == 'echelon_right' or form == 'echelon right' or form == 'echelonr' then - wp.action = 'EchelonR' - else - wp.action = 'Cone' -- if nothing matched - end - end + if not form then + wp.action = 'Cone' + else + form = string.lower(form) + if form == 'off_road' or form == 'off road' then + wp.action = 'Off Road' + elseif form == 'on_road' or form == 'on road' then + wp.action = 'On Road' + elseif form == 'rank' or form == 'line_abrest' or form == 'line abrest' or form == 'lineabrest'then + wp.action = 'Rank' + elseif form == 'cone' then + wp.action = 'Cone' + elseif form == 'diamond' then + wp.action = 'Diamond' + elseif form == 'vee' then + wp.action = 'Vee' + elseif form == 'echelon_left' or form == 'echelon left' or form == 'echelonl' then + wp.action = 'EchelonL' + elseif form == 'echelon_right' or form == 'echelon right' or form == 'echelonr' then + wp.action = 'EchelonR' + else + wp.action = 'Cone' -- if nothing matched + end + end + + wp.type = 'Turning Point' + + return wp - wp.type = 'Turning Point' - return wp end -routines.fixedWing.buildWP = function( point, WPtype, speed, alt, altType ) +routines.fixedWing.buildWP = function(point, WPtype, speed, alt, altType) - local wp = {} - wp.x = point.x + local wp = {} + wp.x = point.x - if point.z then - wp.y = point.z - else - wp.y = point.y - end + if point.z then + wp.y = point.z + else + wp.y = point.y + end - if alt and type( alt ) == 'number' then - wp.alt = alt - else - wp.alt = 2000 - end + if alt and type(alt) == 'number' then + wp.alt = alt + else + wp.alt = 2000 + end - if altType then - altType = string.lower( altType ) - if altType == 'radio' or 'agl' then - wp.alt_type = 'RADIO' - elseif altType == 'baro' or 'asl' then - wp.alt_type = 'BARO' - end - else - wp.alt_type = 'RADIO' - end + if altType then + altType = string.lower(altType) + if altType == 'radio' or 'agl' then + wp.alt_type = 'RADIO' + elseif altType == 'baro' or 'asl' then + wp.alt_type = 'BARO' + end + else + wp.alt_type = 'RADIO' + end - if point.speed then - speed = point.speed - end + if point.speed then + speed = point.speed + end - if point.type then - WPtype = point.type - end + if point.type then + WPtype = point.type + end - if not speed then - wp.speed = routines.utils.kmphToMps( 500 ) - else - wp.speed = speed - end + if not speed then + wp.speed = routines.utils.kmphToMps(500) + else + wp.speed = speed + end - if not WPtype then - wp.action = 'Turning Point' - else - WPtype = string.lower( WPtype ) - if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then - wp.action = 'Fly Over Point' - elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then - wp.action = 'Turning Point' - else - wp.action = 'Turning Point' - end - end + if not WPtype then + wp.action = 'Turning Point' + else + WPtype = string.lower(WPtype) + if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then + wp.action = 'Fly Over Point' + elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then + wp.action = 'Turning Point' + else + wp.action = 'Turning Point' + end + end - wp.type = 'Turning Point' - return wp + wp.type = 'Turning Point' + return wp end -routines.heli.buildWP = function( point, WPtype, speed, alt, altType ) +routines.heli.buildWP = function(point, WPtype, speed, alt, altType) - local wp = {} - wp.x = point.x + local wp = {} + wp.x = point.x - if point.z then - wp.y = point.z - else - wp.y = point.y - end + if point.z then + wp.y = point.z + else + wp.y = point.y + end - if alt and type( alt ) == 'number' then - wp.alt = alt - else - wp.alt = 500 - end + if alt and type(alt) == 'number' then + wp.alt = alt + else + wp.alt = 500 + end - if altType then - altType = string.lower( altType ) - if altType == 'radio' or 'agl' then - wp.alt_type = 'RADIO' - elseif altType == 'baro' or 'asl' then - wp.alt_type = 'BARO' - end - else - wp.alt_type = 'RADIO' - end + if altType then + altType = string.lower(altType) + if altType == 'radio' or 'agl' then + wp.alt_type = 'RADIO' + elseif altType == 'baro' or 'asl' then + wp.alt_type = 'BARO' + end + else + wp.alt_type = 'RADIO' + end - if point.speed then - speed = point.speed - end + if point.speed then + speed = point.speed + end - if point.type then - WPtype = point.type - end + if point.type then + WPtype = point.type + end - if not speed then - wp.speed = routines.utils.kmphToMps( 200 ) - else - wp.speed = speed - end + if not speed then + wp.speed = routines.utils.kmphToMps(200) + else + wp.speed = speed + end - if not WPtype then - wp.action = 'Turning Point' - else - WPtype = string.lower( WPtype ) - if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then - wp.action = 'Fly Over Point' - elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then - wp.action = 'Turning Point' - else - wp.action = 'Turning Point' - end - end + if not WPtype then + wp.action = 'Turning Point' + else + WPtype = string.lower(WPtype) + if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then + wp.action = 'Fly Over Point' + elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then + wp.action = 'Turning Point' + else + wp.action = 'Turning Point' + end + end - wp.type = 'Turning Point' - return wp + wp.type = 'Turning Point' + return wp end -routines.groupToRandomPoint = function( vars ) - local group = vars.group -- Required - local point = vars.point -- required - local radius = vars.radius or 0 - local innerRadius = vars.innerRadius - local form = vars.form or 'Cone' - local heading = vars.heading or math.random() * 2 * math.pi - local headingDegrees = vars.headingDegrees - local speed = vars.speed or routines.utils.kmphToMps( 20 ) +routines.groupToRandomPoint = function(vars) + local group = vars.group --Required + local point = vars.point --required + local radius = vars.radius or 0 + local innerRadius = vars.innerRadius + local form = vars.form or 'Cone' + local heading = vars.heading or math.random()*2*math.pi + local headingDegrees = vars.headingDegrees + local speed = vars.speed or routines.utils.kmphToMps(20) - local useRoads - if not vars.disableRoads then - useRoads = true - else - useRoads = false - end - local path = {} + local useRoads + if not vars.disableRoads then + useRoads = true + else + useRoads = false + end - if headingDegrees then - heading = headingDegrees * math.pi / 180 - end + local path = {} - if heading >= 2 * math.pi then - heading = heading - 2 * math.pi - end + if headingDegrees then + heading = headingDegrees*math.pi/180 + end - local rndCoord = routines.getRandPointInCircle( point, radius, innerRadius ) + if heading >= 2*math.pi then + heading = heading - 2*math.pi + end - local offset = {} - local posStart = routines.getLeadPos( group ) + local rndCoord = routines.getRandPointInCircle(point, radius, innerRadius) - offset.x = routines.utils.round( math.sin( heading - (math.pi / 2) ) * 50 + rndCoord.x, 3 ) - offset.z = routines.utils.round( math.cos( heading + (math.pi / 2) ) * 50 + rndCoord.y, 3 ) - path[#path + 1] = routines.ground.buildWP( posStart, form, speed ) + local offset = {} + local posStart = routines.getLeadPos(group) - if useRoads == true and ((point.x - posStart.x) ^ 2 + (point.z - posStart.z) ^ 2) ^ 0.5 > radius * 1.3 then - path[#path + 1] = routines.ground.buildWP( { ['x'] = posStart.x + 11, ['z'] = posStart.z + 11 }, 'off_road', speed ) - path[#path + 1] = routines.ground.buildWP( posStart, 'on_road', speed ) - path[#path + 1] = routines.ground.buildWP( offset, 'on_road', speed ) - else - path[#path + 1] = routines.ground.buildWP( { ['x'] = posStart.x + 25, ['z'] = posStart.z + 25 }, form, speed ) - end + offset.x = routines.utils.round(math.sin(heading - (math.pi/2)) * 50 + rndCoord.x, 3) + offset.z = routines.utils.round(math.cos(heading + (math.pi/2)) * 50 + rndCoord.y, 3) + path[#path + 1] = routines.ground.buildWP(posStart, form, speed) - path[#path + 1] = routines.ground.buildWP( offset, form, speed ) - path[#path + 1] = routines.ground.buildWP( rndCoord, form, speed ) - routines.goRoute( group, path ) + if useRoads == true and ((point.x - posStart.x)^2 + (point.z - posStart.z)^2)^0.5 > radius * 1.3 then + path[#path + 1] = routines.ground.buildWP({['x'] = posStart.x + 11, ['z'] = posStart.z + 11}, 'off_road', speed) + path[#path + 1] = routines.ground.buildWP(posStart, 'on_road', speed) + path[#path + 1] = routines.ground.buildWP(offset, 'on_road', speed) + else + path[#path + 1] = routines.ground.buildWP({['x'] = posStart.x + 25, ['z'] = posStart.z + 25}, form, speed) + end + + path[#path + 1] = routines.ground.buildWP(offset, form, speed) + path[#path + 1] = routines.ground.buildWP(rndCoord, form, speed) + + routines.goRoute(group, path) + + return end -routines.groupRandomDistSelf = function( gpData, dist, form, heading, speed ) - local pos = routines.getLeadPos( gpData ) - local fakeZone = {} - fakeZone.radius = dist or math.random( 300, 1000 ) - fakeZone.point = { x = pos.x, y, pos.y, z = pos.z } - routines.groupToRandomZone( gpData, fakeZone, form, heading, speed ) +routines.groupRandomDistSelf = function(gpData, dist, form, heading, speed) + local pos = routines.getLeadPos(gpData) + local fakeZone = {} + fakeZone.radius = dist or math.random(300, 1000) + fakeZone.point = {x = pos.x, y = pos.y, z = pos.z} + routines.groupToRandomZone(gpData, fakeZone, form, heading, speed) + + return end -routines.groupToRandomZone = function( gpData, zone, form, heading, speed ) - if type( gpData ) == 'string' then - gpData = Group.getByName( gpData ) - end +routines.groupToRandomZone = function(gpData, zone, form, heading, speed) + if type(gpData) == 'string' then + gpData = Group.getByName(gpData) + end - if type( zone ) == 'string' then - zone = trigger.misc.getZone( zone ) - elseif type( zone ) == 'table' and not zone.radius then - zone = trigger.misc.getZone( zone[math.random( 1, #zone )] ) - end + if type(zone) == 'string' then + zone = trigger.misc.getZone(zone) + elseif type(zone) == 'table' and not zone.radius then + zone = trigger.misc.getZone(zone[math.random(1, #zone)]) + end - if speed then - speed = routines.utils.kmphToMps( speed ) - end + if speed then + speed = routines.utils.kmphToMps(speed) + end - local vars = {} - vars.group = gpData - vars.radius = zone.radius - vars.form = form - vars.headingDegrees = heading - vars.speed = speed - vars.point = routines.utils.zoneToVec3( zone ) + local vars = {} + vars.group = gpData + vars.radius = zone.radius + vars.form = form + vars.headingDegrees = heading + vars.speed = speed + vars.point = routines.utils.zoneToVec3(zone) - routines.groupToRandomPoint( vars ) + routines.groupToRandomPoint(vars) + + return end -routines.isTerrainValid = function( coord, terrainTypes ) -- vec2/3 and enum or table of acceptable terrain types - if coord.z then - coord.y = coord.z - end - local typeConverted = {} +routines.isTerrainValid = function(coord, terrainTypes) -- vec2/3 and enum or table of acceptable terrain types + if coord.z then + coord.y = coord.z + end + local typeConverted = {} - if type( terrainTypes ) == 'string' then -- if its a string it does this check - for constId, constData in pairs( land.SurfaceType ) do - if string.lower( constId ) == string.lower( terrainTypes ) or string.lower( constData ) == string.lower( terrainTypes ) then - table.insert( typeConverted, constId ) - end - end - elseif type( terrainTypes ) == 'table' then -- if its a table it does this check - for typeId, typeData in pairs( terrainTypes ) do - for constId, constData in pairs( land.SurfaceType ) do - if string.lower( constId ) == string.lower( typeData ) or string.lower( constData ) == string.lower( typeId ) then - table.insert( typeConverted, constId ) - end - end - end - end - for validIndex, validData in pairs( typeConverted ) do - if land.getSurfaceType( coord ) == land.SurfaceType[validData] then - return true - end - end - return false + if type(terrainTypes) == 'string' then -- if its a string it does this check + for constId, constData in pairs(land.SurfaceType) do + if string.lower(constId) == string.lower(terrainTypes) or string.lower(constData) == string.lower(terrainTypes) then + table.insert(typeConverted, constId) + end + end + elseif type(terrainTypes) == 'table' then -- if its a table it does this check + for typeId, typeData in pairs(terrainTypes) do + for constId, constData in pairs(land.SurfaceType) do + if string.lower(constId) == string.lower(typeData) or string.lower(constData) == string.lower(typeId) then + table.insert(typeConverted, constId) + end + end + end + end + for validIndex, validData in pairs(typeConverted) do + if land.getSurfaceType(coord) == land.SurfaceType[validData] then + return true + end + end + return false end -routines.groupToPoint = function( gpData, point, form, heading, speed, useRoads ) - if type( point ) == 'string' then - point = trigger.misc.getZone( point ) - end - if speed then - speed = routines.utils.kmphToMps( speed ) - end +routines.groupToPoint = function(gpData, point, form, heading, speed, useRoads) + if type(point) == 'string' then + point = trigger.misc.getZone(point) + end + if speed then + speed = routines.utils.kmphToMps(speed) + end - local vars = {} - vars.group = gpData - vars.form = form - vars.headingDegrees = heading - vars.speed = speed - vars.disableRoads = useRoads - vars.point = routines.utils.zoneToVec3( point ) - routines.groupToRandomPoint( vars ) + local vars = {} + vars.group = gpData + vars.form = form + vars.headingDegrees = heading + vars.speed = speed + vars.disableRoads = useRoads + vars.point = routines.utils.zoneToVec3(point) + routines.groupToRandomPoint(vars) + + return end -routines.getLeadPos = function( group ) - if type( group ) == 'string' then -- group name - group = Group.getByName( group ) - end - local units = group:getUnits() +routines.getLeadPos = function(group) + if type(group) == 'string' then -- group name + group = Group.getByName(group) + end - local leader = units[1] - if not leader then -- SHOULD be good, but if there is a bug, this code future-proofs it then. - local lowestInd = math.huge - for ind, unit in pairs( units ) do - if ind < lowestInd then - lowestInd = ind - leader = unit - end - end - end - if leader and Unit.isExist( leader ) then -- maybe a little too paranoid now... - return leader:getPosition().p - end + local units = group:getUnits() + + local leader = units[1] + if not leader then -- SHOULD be good, but if there is a bug, this code future-proofs it then. + local lowestInd = math.huge + for ind, unit in pairs(units) do + if ind < lowestInd then + lowestInd = ind + leader = unit + end + end + end + if leader and Unit.isExist(leader) then -- maybe a little too paranoid now... + return leader:getPosition().p + end end --[[ vars for routines.getMGRSString: diff --git a/Moose Development/Moose/Utilities/STTS.lua b/Moose Development/Moose/Utilities/STTS.lua index 5c28dd329..36aa2cc43 100644 --- a/Moose Development/Moose/Utilities/STTS.lua +++ b/Moose Development/Moose/Utilities/STTS.lua @@ -126,8 +126,11 @@ end -- So length of msg / 8.3 = number of seconds needed to read it. rounded down to 8 chars per sec map function: -- -- * (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min --- -function STTS.getSpeechTime( length, speed, isGoogle ) +-- +-- @param #number length can also be passed as #string +-- @param #number speed Defaults to 1.0 +-- @param #boolean isGoogle We're using Google TTS +function STTS.getSpeechTime(length,speed,isGoogle) local maxRateRatio = 3 @@ -153,7 +156,7 @@ function STTS.getSpeechTime( length, speed, isGoogle ) length = string.len( length ) end - return math.ceil( length / cps ) + return length/cps --math.ceil(length/cps) end --- Text to speech function. diff --git a/Moose Development/Moose/Utilities/Socket.lua b/Moose Development/Moose/Utilities/Socket.lua new file mode 100644 index 000000000..a5626600e --- /dev/null +++ b/Moose Development/Moose/Utilities/Socket.lua @@ -0,0 +1,152 @@ +--- **Utilities** - Socket. +-- +-- **Main Features:** +-- +-- * Creates UDP Sockets +-- * Send messages to Discord +-- * Compatible with [FunkMan](https://github.com/funkyfranky/FunkMan) +-- * Compatible with [DCSServerBot](https://github.com/Special-K-s-Flightsim-Bots/DCSServerBot) +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Utilities.Socket +-- @image Utilities_Socket.png + + +--- SOCKET class. +-- @type SOCKET +-- @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 #table socket The socket. +-- @field #number port The port. +-- @field #string host The host. +-- @field #table json JSON. +-- @extends Core.Fsm#FSM + +--- **At times I feel like a socket that remembers its tooth.** -- Saul Bellow +-- +-- === +-- +-- # The SOCKET Concept +-- +-- Create a UDP socket server. It enables you to send messages to discord servers via discord bots. +-- +-- **Note** that you have to **de-sanitize** `require` and `package` in your `MissionScripting.lua` file, which is in your `DCS/Scripts` folder. +-- +-- +-- @field #SOCKET +SOCKET = { + ClassName = "SOCKET", + verbose = 0, + lid = nil, +} + +--- Data type. This is the keyword the socket listener uses. +-- @field #string TEXT Plain text. +-- @field #string BOMBRESULT Range bombing. +-- @field #string STRAFERESULT Range strafeing result. +-- @field #string LSOGRADE Airboss LSO grade. +SOCKET.DataType={ + TEXT="moose_text", + BOMBRESULT="moose_bomb_result", + STRAFERESULT="moose_strafe_result", + LSOGRADE="moose_lso_grade", +} + + +--- SOCKET class version. +-- @field #string version +SOCKET.version="0.1.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: A lot! +-- TODO: Messages as spoiler. +-- TODO: Send images? + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new SOCKET object. +-- @param #SOCKET self +-- @param #number Port UDP port. Default `10042`. +-- @param #string Host Host. Default `"127.0.0.1"`. +-- @return #SOCKET self +function SOCKET:New(Port, Host) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) --#SOCKET + + package.path = package.path..";.\\LuaSocket\\?.lua;" + package.cpath = package.cpath..";.\\LuaSocket\\?.dll;" + + self.socket = require("socket") + + self.port=Port or 10042 + self.host=Host or "127.0.0.1" + + self.json=loadfile("Scripts\\JSON.lua")() + + self.UDPSendSocket=self.socket.udp() + self.UDPSendSocket:settimeout(0) + + return self +end + +--- Set port. +-- @param #SOCKET self +-- @param #number Port Port. Default 10042. +-- @return #SOCKET self +function SOCKET:SetPort(Port) + self.port=Port or 10042 +end + +--- Set host. +-- @param #SOCKET self +-- @param #string Host Host. Default `"127.0.0.1"`. +-- @return #SOCKET self +function SOCKET:SetHost(Host) + self.host=Host or "127.0.0.1" +end + + +--- Send a table. +-- @param #SOCKET self +-- @param #table Table Table to send. +-- @return #SOCKET self +function SOCKET:SendTable(Table) + + local json= self.json:encode(Table) + + -- Debug info. + self:T("Json table:") + self:T(json) + + -- Send data. + self.socket.try(self.UDPSendSocket:sendto(json, self.host, self.port)) + + return self +end + +--- Send a text message. +-- @param #SOCKET self +-- @param #string Text Test message. +-- @return #SOCKET self +function SOCKET:SendText(Text) + + local message={} + + message.command = SOCKET.DataType.TEXT + message.text = Text + + self:SendTable(message) + + return self +end + + diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index e2606b296..5efc91010 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -1155,7 +1155,7 @@ function UTILS.VecHdg(a) end --- Calculate "heading" of a 2D vector in the X-Y plane. --- @param DCS#Vec2 a Vector in "D with x, y components. +-- @param DCS#Vec2 a Vector in 2D 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)) @@ -1724,17 +1724,17 @@ end --- Get OS time. Needs os to be desanitized! -- @return #number Os time in seconds. function UTILS.GetOSTime() - if os then - local ts = 0 - local t = os.date("*t") - local s = t.sec - local m = t.min * 60 - local h = t.hour * 3600 - ts = s+m+h - return ts - else - return nil - end + if os then + local ts = 0 + local t = os.date("*t") + local s = t.sec + local m = t.min * 60 + local h = t.hour * 3600 + ts = s+m+h + return ts + else + return nil + end end --- Shuffle a table accoring to Fisher Yeates algorithm diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index dcf5b598b..2036df9cc 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -11,6 +11,7 @@ -- @module Wrapper.Airbase -- @image Wrapper_Airbase.JPG + --- @type AIRBASE -- @field #string ClassName Name of the class, i.e. "AIRBASE". -- @field #table CategoryName Names of airbase categories. @@ -24,9 +25,11 @@ -- @field #boolean isShip Airbase is a ship. -- @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. +-- @field #table runways Runways of airdromes. +-- @field #AIRBASE.Runway runwayLanding Runway used for landing. +-- @field #AIRBASE.Runway runwayTakeoff Runway used for takeoff. -- @extends Wrapper.Positionable#POSITIONABLE --- Wrapper class to handle the DCS Airbase objects: @@ -416,7 +419,7 @@ AIRBASE.Syria={ ["Wujah_Al_Hajar"]="Wujah Al Hajar", ["Al_Dumayr"]="Al-Dumayr", ["Gazipasa"]="Gazipasa", - -- ["Ru_Convoy_4"]="Ru Convoy-4", + --["Ru_Convoy_4"]="Ru Convoy-4", ["Hatay"]="Hatay", ["Nicosia"]="Nicosia", ["Pinarbashi"]="Pinarbashi", @@ -519,7 +522,6 @@ AIRBASE.SouthAtlantic={ ["El_Calafate"]="El Calafate", } - --- AIRBASE.ParkingSpot ".Coordinate, ".TerminalID", ".TerminalType", ".TOAC", ".Free", ".TerminalID0", ".DistToRwy". -- @type AIRBASE.ParkingSpot -- @field Core.Point#COORDINATE Coordinate Coordinate of the parking spot. @@ -529,6 +531,14 @@ AIRBASE.SouthAtlantic={ -- @field #boolean Free This spot is currently free, i.e. there is no alive aircraft on it at the present moment. -- @field #number TerminalID0 Unknown what this means. If you know, please tell us! -- @field #number DistToRwy Distance to runway in meters. Currently bugged and giving the same number as the TerminalID. +-- @field #string AirbaseName Name of the airbase. +-- @field #number MarkerID Numerical ID of marker placed at parking spot. +-- @field Wrapper.Marker#MARKER Marker The marker on the F10 map. +-- @field #string ClientSpot If `true`, this is a parking spot of a client aircraft. +-- @field #string ClientName Client unit name of this spot. +-- @field #string Status Status of spot e.g. `AIRBASE.SpotStatus.FREE`. +-- @field #string OccupiedBy Name of the aircraft occupying the spot or "unknown". Can be *nil* if spot is not occupied. +-- @field #string ReservedBy Name of the aircraft for which this spot is reserved. Can be *nil* if spot is not reserved. --- Terminal Types of parking spots. See also https://wiki.hoggitworld.com/view/DCS_func_getParking -- @@ -553,23 +563,40 @@ AIRBASE.SouthAtlantic={ -- @field #number HelicopterUsable 216: Combines HelicopterOnly, OpenMed and OpenBig. -- @field #number FighterAircraft 244: Combines Shelter. OpenMed and OpenBig spots. So effectively all spots usable by fixed wing aircraft. AIRBASE.TerminalType = { - Runway = 16, - HelicopterOnly = 40, - Shelter = 68, - OpenMed = 72, - OpenBig = 104, - OpenMedOrBig = 176, - HelicopterUsable = 216, - FighterAircraft = 244, + Runway=16, + HelicopterOnly=40, + Shelter=68, + OpenMed=72, + OpenBig=104, + OpenMedOrBig=176, + HelicopterUsable=216, + FighterAircraft=244, +} + +--- Status of a parking spot. +-- @type AIRBASE.SpotStatus +-- @field #string FREE Spot is free. +-- @field #string OCCUPIED Spot is occupied. +-- @field #string RESERVED Spot is reserved. +AIRBASE.SpotStatus = { + FREE="Free", + OCCUPIED="Occupied", + RESERVED="Reserved", } --- Runway data. -- @type AIRBASE.Runway --- @field #number heading Heading of the runway in degrees. +-- @field #string name Runway name. -- @field #string idx Runway ID: heading 070° ==> idx="07". +-- @field #number heading True heading of the runway in degrees. +-- @field #number magheading Magnetic heading of the runway in degrees. This is what is marked on the runway. -- @field #number length Length of runway in meters. +-- @field #number width Width of runway in meters. +-- @field Core.Zone#ZONE_POLYGON zone Runway zone. +-- @field Core.Point#COORDINATE center Center of the runway. -- @field Core.Point#COORDINATE position Position of runway start. -- @field Core.Point#COORDINATE endpoint End point of runway. +-- @field #boolean isLeft If `true`, this is the left of two parallel runways. If `false`, this is the right of two runways. If `nil`, no parallel runway exists. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Registration @@ -579,59 +606,72 @@ AIRBASE.TerminalType = { -- @param #AIRBASE self -- @param #string AirbaseName The name of the airbase. -- @return #AIRBASE self -function AIRBASE:Register( AirbaseName ) +function AIRBASE:Register(AirbaseName) -- Inherit everything from positionable. - local self = BASE:Inherit( self, POSITIONABLE:New( AirbaseName ) ) -- #AIRBASE + local self=BASE:Inherit(self, POSITIONABLE:New(AirbaseName)) --#AIRBASE -- Set airbase name. - self.AirbaseName = AirbaseName + self.AirbaseName=AirbaseName -- Set airbase ID. - self.AirbaseID = self:GetID( true ) + self.AirbaseID=self:GetID(true) -- Get descriptors. - self.descriptors = self:GetDesc() + 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 + self.category=self.descriptors and self.descriptors.category or Airbase.Category.AIRDROME -- Set category. - if self.category == Airbase.Category.AIRDROME then - self.isAirdrome = true - elseif self.category == Airbase.Category.HELIPAD then - self.isHelipad = true - elseif self.category == Airbase.Category.SHIP then - self.isShip = true + if self.category==Airbase.Category.AIRDROME then + self.isAirdrome=true + elseif self.category==Airbase.Category.HELIPAD then + 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 ) + 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!" ) + self:E("ERROR: Unknown airbase category!") + end + + -- Init Runways. + self:_InitRunways() + + -- Set the active runways based on wind direction. + if self.isAirdrome then + self:SetActiveRunway() end + -- Init parking spots. self:_InitParkingSpots() - local vec2 = self:GetVec2() + -- Get 2D position vector. + local vec2=self:GetVec2() -- Init coordinate. self:GetCoordinate() if vec2 then if self.isShip then - local unit = UNIT:FindByName( AirbaseName ) + local unit=UNIT:FindByName(AirbaseName) if unit then - self.AirbaseZone = ZONE_UNIT:New( AirbaseName, unit, 2500 ) + self.AirbaseZone=ZONE_UNIT:New(AirbaseName, unit, 2500) end else - self.AirbaseZone = ZONE_RADIUS:New( AirbaseName, vec2, 2500 ) + self.AirbaseZone=ZONE_RADIUS:New(AirbaseName, vec2, 2500) end else - self:E( string.format( "ERROR: Cound not get position Vec2 of airbase %s", AirbaseName ) ) + self:E(string.format("ERROR: Cound not get position Vec2 of airbase %s", AirbaseName)) end return self @@ -666,14 +706,14 @@ end -- @param #AIRBASE self -- @param #number id Airbase ID. -- @return #AIRBASE self -function AIRBASE:FindByID( id ) +function AIRBASE:FindByID(id) - for name, _airbase in pairs( _DATABASE.AIRBASES ) do - local airbase = _airbase -- #AIRBASE + for name,_airbase in pairs(_DATABASE.AIRBASES) do + local airbase=_airbase --#AIRBASE - local aid = tonumber( airbase:GetID( true ) ) + local aid=tonumber(airbase:GetID(true)) - if aid == id then + if aid==id then return airbase end @@ -688,7 +728,7 @@ end function AIRBASE:GetDCSObject() -- Get the DCS object. - local DCSAirbase = Airbase.getByName( self.AirbaseName ) + local DCSAirbase = Airbase.getByName(self.AirbaseName) if DCSAirbase then return DCSAirbase @@ -708,14 +748,14 @@ end -- @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 -- @return #table Table containing all airbase objects of the current map. -function AIRBASE.GetAllAirbases( coalition, category ) +function AIRBASE.GetAllAirbases(coalition, category) - local airbases = {} - for _, _airbase in pairs( _DATABASE.AIRBASES ) do - local airbase = _airbase -- #AIRBASE - if coalition == nil or airbase:GetCoalition() == coalition then - if category == nil or category == airbase:GetAirbaseCategory() then - table.insert( airbases, airbase ) + local airbases={} + for _,_airbase in pairs(_DATABASE.AIRBASES) do + local airbase=_airbase --#AIRBASE + if coalition==nil or airbase:GetCoalition()==coalition then + if category==nil or category==airbase:GetAirbaseCategory() then + table.insert(airbases, airbase) end end end @@ -727,14 +767,14 @@ end -- @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.HELIPAD`. -- @return #table Table containing all airbase names of the current map. -function AIRBASE.GetAllAirbaseNames( coalition, category ) +function AIRBASE.GetAllAirbaseNames(coalition, category) - local airbases = {} - for airbasename, _airbase in pairs( _DATABASE.AIRBASES ) do - local airbase = _airbase -- #AIRBASE - if coalition == nil or airbase:GetCoalition() == coalition then - if category == nil or category == airbase:GetAirbaseCategory() then - table.insert( airbases, airbasename ) + local airbases={} + for airbasename,_airbase in pairs(_DATABASE.AIRBASES) do + local airbase=_airbase --#AIRBASE + if coalition==nil or airbase:GetCoalition()==coalition then + if category==nil or category==airbase:GetAirbaseCategory() then + table.insert(airbases, airbasename) end end end @@ -746,26 +786,26 @@ end -- @param #AIRBASE self -- @param #boolean unique (Optional) If true, ships will get a negative sign as the unit ID might be the same as an airbase ID. Default off! -- @return #number The airbase ID. -function AIRBASE:GetID( unique ) +function AIRBASE:GetID(unique) if self.AirbaseID then - return unique and self.AirbaseID or math.abs( self.AirbaseID ) + return unique and self.AirbaseID or math.abs(self.AirbaseID) else - for DCSAirbaseId, DCSAirbase in ipairs( world.getAirbases() ) do + for DCSAirbaseId, DCSAirbase in ipairs(world.getAirbases()) do -- Get the airbase name. local AirbaseName = DCSAirbase:getName() -- This gives the incorrect value to be inserted into the airdromeID for DCS 2.5.6! - local airbaseID = tonumber( DCSAirbase:getID() ) + local airbaseID=tonumber(DCSAirbase:getID()) - local airbaseCategory = self:GetAirbaseCategory() + local airbaseCategory=self:GetAirbaseCategory() - if AirbaseName == self.AirbaseName then - if airbaseCategory == Airbase.Category.SHIP or airbaseCategory == Airbase.Category.HELIPAD then + if AirbaseName==self.AirbaseName then + if airbaseCategory==Airbase.Category.SHIP or airbaseCategory==Airbase.Category.HELIPAD then -- Ships get a negative sign as their unit number might be the same as the ID of another airbase. return unique and -airbaseID or airbaseID else @@ -787,19 +827,19 @@ end -- @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 ) +function AIRBASE:SetParkingSpotWhitelist(TerminalIdWhitelist) - if TerminalIdWhitelist == nil then - self.parkingWhitelist = {} + if TerminalIdWhitelist==nil then + self.parkingWhitelist={} return self end -- Ensure we got a table. - if type( TerminalIdWhitelist ) ~= "table" then - TerminalIdWhitelist = { TerminalIdWhitelist } + if type(TerminalIdWhitelist)~="table" then + TerminalIdWhitelist={TerminalIdWhitelist} end - self.parkingWhitelist = TerminalIdWhitelist + self.parkingWhitelist=TerminalIdWhitelist return self end @@ -811,23 +851,60 @@ end -- @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 ) +function AIRBASE:SetParkingSpotBlacklist(TerminalIdBlacklist) - if TerminalIdBlacklist == nil then - self.parkingBlacklist = {} + if TerminalIdBlacklist==nil then + self.parkingBlacklist={} return self end -- Ensure we got a table. - if type( TerminalIdBlacklist ) ~= "table" then - TerminalIdBlacklist = { TerminalIdBlacklist } + if type(TerminalIdBlacklist)~="table" then + TerminalIdBlacklist={TerminalIdBlacklist} end - self.parkingBlacklist = TerminalIdBlacklist + self.parkingBlacklist=TerminalIdBlacklist return self end +--- Sets the ATC belonging to an airbase object to be silent and unresponsive. This is useful for disabling the award winning ATC behavior in DCS. +-- Note that this DOES NOT remove the airbase from the list. It just makes it unresponsive and silent to any radio calls to it. +-- @param #AIRBASE self +-- @param #boolean Silent If `true`, enable silent mode. If `false` or `nil`, disable silent mode. +-- @return #AIRBASE self +function AIRBASE:SetRadioSilentMode(Silent) + + -- Get DCS airbase object. + local airbase=self:GetDCSObject() + + -- Set mode. + if airbase then + airbase:setRadioSilentMode(Silent) + end + + return self +end + +--- Check whether or not the airbase has been silenced. +-- @param #AIRBASE self +-- @return #boolean If `true`, silent mode is enabled. +function AIRBASE:GetRadioSilentMode() + + -- Is silent? + local silent=nil + + -- Get DCS airbase object. + local airbase=self:GetDCSObject() + + -- Set mode. + if airbase then + silent=airbase:getRadioSilentMode() + end + + return silent +end + --- Get category of airbase. -- @param #AIRBASE self -- @return #number Category of airbase from GetDesc().category. @@ -880,19 +957,19 @@ end -- @param #AIRBASE self -- @param #boolean available If true, only available parking spots will be returned. -- @return #table Table with parking data. See https://wiki.hoggitworld.com/view/DCS_func_getParking -function AIRBASE:GetParkingData( available ) - self:F2( available ) +function AIRBASE:GetParkingData(available) + self:F2(available) -- Get DCS airbase object. - local DCSAirbase = self:GetDCSObject() + local DCSAirbase=self:GetDCSObject() -- Get parking data. - local parkingdata = nil + local parkingdata=nil if DCSAirbase then - parkingdata = DCSAirbase:getParking( available ) + parkingdata=DCSAirbase:getParking(available) end - self:T2( { parkingdata = parkingdata } ) + self:T2({parkingdata=parkingdata}) return parkingdata end @@ -900,15 +977,15 @@ end -- @param #AIRBASE self -- @param #AIRBASE.TerminalType termtype Terminal type of which the number of spots is counted. Default all spots but spawn points on runway. -- @return #number Number of parking spots at this airbase. -function AIRBASE:GetParkingSpotsNumber( termtype ) +function AIRBASE:GetParkingSpotsNumber(termtype) -- Get free parking spots data. - local parkingdata = self:GetParkingData( false ) + local parkingdata=self:GetParkingData(false) - local nspots = 0 - for _, parkingspot in pairs( parkingdata ) do - if AIRBASE._CheckTerminalType( parkingspot.Term_Type, termtype ) then - nspots = nspots + 1 + local nspots=0 + for _,parkingspot in pairs(parkingdata) do + if AIRBASE._CheckTerminalType(parkingspot.Term_Type, termtype) then + nspots=nspots+1 end end @@ -920,17 +997,17 @@ end -- @param #AIRBASE.TerminalType termtype Terminal type. -- @param #boolean allowTOAC If true, spots are considered free even though TO_AC is true. Default is off which is saver to avoid spawning aircraft on top of each other. Option might be enabled for FARPS and ships. -- @return #number Number of free parking spots at this airbase. -function AIRBASE:GetFreeParkingSpotsNumber( termtype, allowTOAC ) +function AIRBASE:GetFreeParkingSpotsNumber(termtype, allowTOAC) -- Get free parking spots data. - local parkingdata = self:GetParkingData( true ) + local parkingdata=self:GetParkingData(true) - local nfree = 0 - for _, parkingspot in pairs( parkingdata ) do + local nfree=0 + for _,parkingspot in pairs(parkingdata) do -- Spots on runway are not counted unless explicitly requested. - if AIRBASE._CheckTerminalType( parkingspot.Term_Type, termtype ) then - if (allowTOAC and allowTOAC == true) or parkingspot.TO_AC == false then - nfree = nfree + 1 + if AIRBASE._CheckTerminalType(parkingspot.Term_Type, termtype) then + if (allowTOAC and allowTOAC==true) or parkingspot.TO_AC==false then + nfree=nfree+1 end end end @@ -943,18 +1020,18 @@ end -- @param #AIRBASE.TerminalType termtype Terminal type. -- @param #boolean allowTOAC If true, spots are considered free even though TO_AC is true. Default is off which is saver to avoid spawning aircraft on top of each other. Option might be enabled for FARPS and ships. -- @return #table Table of coordinates of the free parking spots. -function AIRBASE:GetFreeParkingSpotsCoordinates( termtype, allowTOAC ) +function AIRBASE:GetFreeParkingSpotsCoordinates(termtype, allowTOAC) -- Get free parking spots data. - local parkingdata = self:GetParkingData( true ) + local parkingdata=self:GetParkingData(true) -- Put coordinates of free spots into table. - local spots = {} - for _, parkingspot in pairs( parkingdata ) do + local spots={} + for _,parkingspot in pairs(parkingdata) do -- Coordinates on runway are not returned unless explicitly requested. - if AIRBASE._CheckTerminalType( parkingspot.Term_Type, termtype ) then - if (allowTOAC and allowTOAC == true) or parkingspot.TO_AC == false then - table.insert( spots, COORDINATE:NewFromVec3( parkingspot.vTerminalPos ) ) + if AIRBASE._CheckTerminalType(parkingspot.Term_Type, termtype) then + if (allowTOAC and allowTOAC==true) or parkingspot.TO_AC==false then + table.insert(spots, COORDINATE:NewFromVec3(parkingspot.vTerminalPos)) end end end @@ -966,23 +1043,23 @@ end -- @param #AIRBASE self -- @param #AIRBASE.TerminalType termtype (Optional) Terminal type. Default all. -- @return #table Table of coordinates of parking spots. -function AIRBASE:GetParkingSpotsCoordinates( termtype ) +function AIRBASE:GetParkingSpotsCoordinates(termtype) -- Get all parking spots data. - local parkingdata = self:GetParkingData( false ) + local parkingdata=self:GetParkingData(false) -- Put coordinates of free spots into table. - local spots = {} - for _, parkingspot in ipairs( parkingdata ) do + local spots={} + for _,parkingspot in ipairs(parkingdata) do -- Coordinates on runway are not returned unless explicitly requested. - if AIRBASE._CheckTerminalType( parkingspot.Term_Type, termtype ) then + if AIRBASE._CheckTerminalType(parkingspot.Term_Type, termtype) then -- Get coordinate from Vec3 terminal position. - local _coord = COORDINATE:NewFromVec3( parkingspot.vTerminalPos ) + local _coord=COORDINATE:NewFromVec3(parkingspot.vTerminalPos) -- Add to table. - table.insert( spots, _coord ) + table.insert(spots, _coord) end end @@ -996,42 +1073,59 @@ end function AIRBASE:_InitParkingSpots() -- Get parking data of all spots (free or occupied) - local parkingdata = self:GetParkingData( false ) + local parkingdata=self:GetParkingData(false) -- Init table. - self.parking = {} - self.parkingByID = {} + self.parking={} + self.parkingByID={} - self.NparkingTotal = 0 - self.NparkingTerminal = {} - for _, terminalType in pairs( AIRBASE.TerminalType ) do - self.NparkingTerminal[terminalType] = 0 + self.NparkingTotal=0 + self.NparkingTerminal={} + for _,terminalType in pairs(AIRBASE.TerminalType) do + self.NparkingTerminal[terminalType]=0 + end + + -- Get client coordinates. + local function isClient(coord) + local clients=_DATABASE.CLIENTS + for clientname, _client in pairs(clients) do + local client=_client --Wrapper.Client#CLIENT + if client and client.SpawnCoord then + local dist=client.SpawnCoord:Get2DDistance(coord) + if dist<2 then + return true, clientname + end + end + end + return false, nil end -- Put coordinates of parking spots into table. - for _, spot in pairs( parkingdata ) do + 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 + 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 + park.ClientSpot, park.ClientName=isClient(park.Coordinate) + park.AirbaseName=self.AirbaseName - self.NparkingTotal = self.NparkingTotal + 1 + 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 + 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 @@ -1041,7 +1135,7 @@ end -- @param #AIRBASE self -- @param #number TerminalID Terminal ID. -- @return #AIRBASE.ParkingSpot Parking spot. -function AIRBASE:_GetParkingSpotByID( TerminalID ) +function AIRBASE:_GetParkingSpotByID(TerminalID) return self.parkingByID[TerminalID] end @@ -1049,18 +1143,18 @@ end -- @param #AIRBASE self -- @param #AIRBASE.TerminalType termtype Terminal type. -- @return #table Table free parking spots. Table has the elements ".Coordinate, ".TerminalID", ".TerminalType", ".TOAC", ".Free", ".TerminalID0", ".DistToRwy". -function AIRBASE:GetParkingSpotsTable( termtype ) +function AIRBASE:GetParkingSpotsTable(termtype) -- Get parking data of all spots (free or occupied) - local parkingdata = self:GetParkingData( false ) + local parkingdata=self:GetParkingData(false) -- Get parking data of all free spots. - local parkingfree = self:GetParkingData( true ) + local parkingfree=self:GetParkingData(true) -- Function to ckeck if any parking spot is free. - local function _isfree( _tocheck ) - for _, _spot in pairs( parkingfree ) do - if _spot.Term_Index == _tocheck.Term_Index then + local function _isfree(_tocheck) + for _,_spot in pairs(parkingfree) do + if _spot.Term_Index==_tocheck.Term_Index then return true end end @@ -1068,23 +1162,24 @@ function AIRBASE:GetParkingSpotsTable( termtype ) end -- Put coordinates of parking spots into table. - local spots = {} - for _, _spot in pairs( parkingdata ) do + local spots={} + for _,_spot in pairs(parkingdata) do - if AIRBASE._CheckTerminalType( _spot.Term_Type, termtype ) then + if AIRBASE._CheckTerminalType(_spot.Term_Type, termtype) then - local spot = self:_GetParkingSpotByID( _spot.Term_Index ) + local spot=self:_GetParkingSpotByID(_spot.Term_Index) if spot then - spot.Free = _isfree( _spot ) -- updated - spot.TOAC = _spot.TO_AC -- updated + spot.Free=_isfree(_spot) -- updated + spot.TOAC=_spot.TO_AC -- updated + spot.AirbaseName=self.AirbaseName - table.insert( spots, spot ) + table.insert(spots, spot) else - self:E( string.format( "ERROR: Parking spot %s is nil!", tostring( _spot.Term_Index ) ) ) + self:E(string.format("ERROR: Parking spot %s is nil!", tostring(_spot.Term_Index))) end @@ -1100,23 +1195,24 @@ end -- @param #AIRBASE.TerminalType termtype Terminal type. -- @param #boolean allowTOAC If true, spots are considered free even though TO_AC is true. Default is off which is saver to avoid spawning aircraft on top of each other. Option might be enabled for FARPS and ships. -- @return #table Table free parking spots. Table has the elements ".Coordinate, ".TerminalID", ".TerminalType", ".TOAC", ".Free", ".TerminalID0", ".DistToRwy". -function AIRBASE:GetFreeParkingSpotsTable( termtype, allowTOAC ) +function AIRBASE:GetFreeParkingSpotsTable(termtype, allowTOAC) -- Get parking data of all free spots. - local parkingfree = self:GetParkingData( true ) + local parkingfree=self:GetParkingData(true) -- Put coordinates of free spots into table. - local freespots = {} - for _, _spot in pairs( parkingfree ) do - if AIRBASE._CheckTerminalType( _spot.Term_Type, termtype ) and _spot.Term_Index > 0 then - if (allowTOAC and allowTOAC == true) or _spot.TO_AC == false then + local freespots={} + for _,_spot in pairs(parkingfree) do + if AIRBASE._CheckTerminalType(_spot.Term_Type, termtype) and _spot.Term_Index>0 then + if (allowTOAC and allowTOAC==true) or _spot.TO_AC==false then - local spot = self:_GetParkingSpotByID( _spot.Term_Index ) + local spot=self:_GetParkingSpotByID(_spot.Term_Index) - spot.Free = true -- updated - spot.TOAC = _spot.TO_AC -- updated + spot.Free=true -- updated + spot.TOAC=_spot.TO_AC -- updated + spot.AirbaseName=self.AirbaseName - table.insert( freespots, spot ) + table.insert(freespots, spot) end end @@ -1129,20 +1225,20 @@ end -- @param #AIRBASE self -- @param #number TerminalID The terminal ID of the parking spot. -- @return #AIRBASE.ParkingSpot Table free parking spots. Table has the elements ".Coordinate, ".TerminalID", ".TerminalType", ".TOAC", ".Free", ".TerminalID0", ".DistToRwy". -function AIRBASE:GetParkingSpotData( TerminalID ) +function AIRBASE:GetParkingSpotData(TerminalID) -- Get parking data. - local parkingdata = self:GetParkingSpotsTable() + local parkingdata=self:GetParkingSpotsTable() - for _, _spot in pairs( parkingdata ) do - local spot = _spot -- #AIRBASE.ParkingSpot - self:T( { TerminalID = spot.TerminalID, TerminalType = spot.TerminalType } ) - if TerminalID == spot.TerminalID then + for _,_spot in pairs(parkingdata) do + local spot=_spot --#AIRBASE.ParkingSpot + self:T({TerminalID=spot.TerminalID,TerminalType=spot.TerminalType}) + if TerminalID==spot.TerminalID then return spot end end - self:E( "ERROR: Could not find spot with Terminal ID=" .. tostring( TerminalID ) ) + self:E("ERROR: Could not find spot with Terminal ID="..tostring(TerminalID)) return nil end @@ -1150,33 +1246,35 @@ end -- @param #AIRBASE self -- @param #AIRBASE.TerminalType termtype Terminal type for which marks should be placed. -- @param #boolean mark If false, do not place markers but only give output to DCS.log file. Default true. -function AIRBASE:MarkParkingSpots( termtype, mark ) +function AIRBASE:MarkParkingSpots(termtype, mark) -- Default is true. - if mark == nil then - mark = true + if mark==nil then + mark=true end -- Get parking data from getParking() wrapper function. - local parkingdata = self:GetParkingSpotsTable( termtype ) + local parkingdata=self:GetParkingSpotsTable(termtype) -- Get airbase name. - local airbasename = self:GetName() - self:E( string.format( "Parking spots at %s for terminal type %s:", airbasename, tostring( termtype ) ) ) + local airbasename=self:GetName() + self:E(string.format("Parking spots at %s for terminal type %s:", airbasename, tostring(termtype))) - for _, _spot in pairs( parkingdata ) do + for _,_spot in pairs(parkingdata) do -- Mark text. - local _text = string.format( "Term Index=%d, Term Type=%d, Free=%s, TOAC=%s, Term ID0=%d, Dist2Rwy=%.1f m", _spot.TerminalID, _spot.TerminalType, tostring( _spot.Free ), tostring( _spot.TOAC ), _spot.TerminalID0, _spot.DistToRwy ) + local _text=string.format("Term Index=%d, Term Type=%d, Free=%s, TOAC=%s, Term ID0=%d, Dist2Rwy=%.1f m", + _spot.TerminalID, _spot.TerminalType,tostring(_spot.Free),tostring(_spot.TOAC),_spot.TerminalID0,_spot.DistToRwy) -- Create mark on the F10 map. if mark then - _spot.Coordinate:MarkToAll( _text ) + _spot.Coordinate:MarkToAll(_text) end -- Info to DCS.log file. - local _text = string.format( "%s, Term Index=%3d, Term Type=%03d, Free=%5s, TOAC=%5s, Term ID0=%3d, Dist2Rwy=%.1f m", airbasename, _spot.TerminalID, _spot.TerminalType, tostring( _spot.Free ), tostring( _spot.TOAC ), _spot.TerminalID0, _spot.DistToRwy ) - self:E( _text ) + local _text=string.format("%s, Term Index=%3d, Term Type=%03d, Free=%5s, TOAC=%5s, Term ID0=%3d, Dist2Rwy=%.1f m", + airbasename, _spot.TerminalID, _spot.TerminalType,tostring(_spot.Free),tostring(_spot.TOAC),_spot.TerminalID0,_spot.DistToRwy) + self:E(_text) end end @@ -1193,33 +1291,33 @@ end -- @param #number nspots (Optional) Number of freeparking spots requested. Default is the number of aircraft in the group. -- @param #table parkingdata (Optional) Parking spots data table. If not given it is automatically derived from the GetParkingSpotsTable() function. -- @return #table Table of coordinates and terminal IDs of free parking spots. Each table entry has the elements .Coordinate and .TerminalID. -function AIRBASE:FindFreeParkingSpotForAircraft( group, terminaltype, scanradius, scanunits, scanstatics, scanscenery, verysafe, nspots, parkingdata ) +function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, scanunits, scanstatics, scanscenery, verysafe, nspots, parkingdata) -- Init default - scanradius = scanradius or 50 - if scanunits == nil then - scanunits = true + scanradius=scanradius or 50 + if scanunits==nil then + scanunits=true end - if scanstatics == nil then - scanstatics = true + if scanstatics==nil then + scanstatics=true end - if scanscenery == nil then - scanscenery = false + if scanscenery==nil then + scanscenery=false end - if verysafe == nil then - verysafe = false + if verysafe==nil then + verysafe=false end -- Function calculating the overlap of two (square) objects. - local function _overlap( object1, object2, dist ) - local pos1 = object1 -- Wrapper.Positionable#POSITIONABLE - local pos2 = object2 -- Wrapper.Positionable#POSITIONABLE - local r1 = pos1:GetBoundingRadius() - local r2 = pos2:GetBoundingRadius() + local function _overlap(object1, object2, dist) + local pos1=object1 --Wrapper.Positionable#POSITIONABLE + local pos2=object2 --Wrapper.Positionable#POSITIONABLE + local r1=pos1:GetBoundingRadius() + local r2=pos2:GetBoundingRadius() if r1 and r2 then - local safedist = (r1 + r2) * 1.1 + local safedist=(r1+r2)*1.1 local safe = (dist > safedist) - self:T2( string.format( "r1=%.1f r2=%.1f s=%.1f d=%.1f ==> safe=%s", r1, r2, safedist, dist, tostring( safe ) ) ) + self:T2(string.format("r1=%.1f r2=%.1f s=%.1f d=%.1f ==> safe=%s", r1, r2, safedist, dist, tostring(safe))) return safe else return true @@ -1227,21 +1325,21 @@ function AIRBASE:FindFreeParkingSpotForAircraft( group, terminaltype, scanradius end -- Get airport name. - local airport = self:GetName() + local airport=self:GetName() -- Get parking spot data table. This contains free and "non-free" spots. -- Note that there are three major issues with the DCS getParking() function: -- 1. A spot is considered as NOT free until an aircraft that is present has finally taken off. This might be a bit long especiall at smaller airports. -- 2. A "free" spot does not take the aircraft size into accound. So if two big aircraft are spawned on spots next to each other, they might overlap and get destroyed. -- 3. The routine return a free spot, if there a static objects placed on the spot. - parkingdata = parkingdata or self:GetParkingSpotsTable( terminaltype ) + parkingdata=parkingdata or self:GetParkingSpotsTable(terminaltype) -- Get the aircraft size, i.e. it's longest side of x,z. local aircraft = nil -- fix local problem below - local _aircraftsize, ax, ay, az + local _aircraftsize, ax,ay,az if group and group.ClassName == "GROUP" then - aircraft = group:GetUnit( 1 ) - _aircraftsize, ax, ay, az = aircraft:GetObjectSize() + aircraft=group:GetUnit(1) + _aircraftsize, ax,ay,az=aircraft:GetObjectSize() else -- SU27 dimensions _aircraftsize = 23 @@ -1250,126 +1348,127 @@ function AIRBASE:FindFreeParkingSpotForAircraft( group, terminaltype, scanradius az = 17 -- width end + -- Number of spots we are looking for. Note that, e.g. grouping can require a number different from the group size! - local _nspots = nspots or group:GetSize() + local _nspots=nspots or group:GetSize() -- Debug info. - self:E( string.format( "%s: Looking for %d parking spot(s) for aircraft of size %.1f m (x=%.1f,y=%.1f,z=%.1f) at terminal type %s.", airport, _nspots, _aircraftsize, ax, ay, az, tostring( terminaltype ) ) ) + self:E(string.format("%s: Looking for %d parking spot(s) for aircraft of size %.1f m (x=%.1f,y=%.1f,z=%.1f) at terminal type %s.", airport, _nspots, _aircraftsize, ax, ay, az, tostring(terminaltype))) -- Table of valid spots. - local validspots = {} - local nvalid = 0 + local validspots={} + local nvalid=0 -- Test other stuff if no parking spot is available. - local _test = false + local _test=false if _test then return validspots end -- Mark all found obstacles on F10 map for debugging. - local markobstacles = false + local markobstacles=false -- Loop over all known parking spots - for _, parkingspot in pairs( parkingdata ) do + for _,parkingspot in pairs(parkingdata) do -- Coordinate of the parking spot. - local _spot = parkingspot.Coordinate -- Core.Point#COORDINATE - local _termid = parkingspot.TerminalID + local _spot=parkingspot.Coordinate -- Core.Point#COORDINATE + local _termid=parkingspot.TerminalID -- Check terminal type and black/white listed parking spots. - if AIRBASE._CheckTerminalType( parkingspot.TerminalType, terminaltype ) and self:_CheckParkingLists( _termid ) then + 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 + if verysafe and (parkingspot.Free==false or parkingspot.TOAC==true) then -- DCS getParking() routine returned that spot is not free. - self:T( string.format( "%s: Parking spot id %d NOT free (or aircraft has not taken off yet). Free=%s, TOAC=%s.", airport, parkingspot.TerminalID, tostring( parkingspot.Free ), tostring( parkingspot.TOAC ) ) ) + self:T(string.format("%s: Parking spot id %d NOT free (or aircraft has not taken off yet). Free=%s, TOAC=%s.", airport, parkingspot.TerminalID, tostring(parkingspot.Free), tostring(parkingspot.TOAC))) else -- Scan a radius of 50 meters around the spot. - local _, _, _, _units, _statics, _sceneries = _spot:ScanObjects( scanradius, scanunits, scanstatics, scanscenery ) + local _,_,_,_units,_statics,_sceneries=_spot:ScanObjects(scanradius, scanunits, scanstatics, scanscenery) -- Loop over objects within scan radius. - local occupied = false + local occupied=false -- Check all units. - for _, unit in pairs( _units ) do - local _coord = unit:GetCoordinate() - local _dist = _coord:Get2DDistance( _spot ) - local _safe = _overlap( aircraft, unit, _dist ) + for _,unit in pairs(_units) do + local _coord=unit:GetCoordinate() + local _dist=_coord:Get2DDistance(_spot) + local _safe=_overlap(aircraft, unit, _dist) if markobstacles then - local l, x, y, z = unit:GetObjectSize() - _coord:MarkToAll( string.format( "Unit %s\nx=%.1f y=%.1f z=%.1f\nl=%.1f d=%.1f\nspot %d safe=%s", unit:GetName(), x, y, z, l, _dist, _termid, tostring( _safe ) ) ) + local l,x,y,z=unit:GetObjectSize() + _coord:MarkToAll(string.format("Unit %s\nx=%.1f y=%.1f z=%.1f\nl=%.1f d=%.1f\nspot %d safe=%s", unit:GetName(),x,y,z,l,_dist, _termid, tostring(_safe))) end if scanunits and not _safe then - occupied = true + occupied=true end end -- Check all statics. - for _, static in pairs( _statics ) do - local _static = STATIC:Find( static ) - local _vec3 = static:getPoint() - local _coord = COORDINATE:NewFromVec3( _vec3 ) - local _dist = _coord:Get2DDistance( _spot ) - local _safe = _overlap( aircraft, _static, _dist ) + for _,static in pairs(_statics) do + local _static=STATIC:Find(static) + local _vec3=static:getPoint() + local _coord=COORDINATE:NewFromVec3(_vec3) + local _dist=_coord:Get2DDistance(_spot) + local _safe=_overlap(aircraft,_static,_dist) if markobstacles then - local l, x, y, z = _static:GetObjectSize() - _coord:MarkToAll( string.format( "Static %s\nx=%.1f y=%.1f z=%.1f\nl=%.1f d=%.1f\nspot %d safe=%s", static:getName(), x, y, z, l, _dist, _termid, tostring( _safe ) ) ) + local l,x,y,z=_static:GetObjectSize() + _coord:MarkToAll(string.format("Static %s\nx=%.1f y=%.1f z=%.1f\nl=%.1f d=%.1f\nspot %d safe=%s", static:getName(),x,y,z,l,_dist, _termid, tostring(_safe))) end if scanstatics and not _safe then - occupied = true + occupied=true end end -- Check all scenery. - for _, scenery in pairs( _sceneries ) do - local _scenery = SCENERY:Register( scenery:getTypeName(), scenery ) - local _vec3 = scenery:getPoint() - local _coord = COORDINATE:NewFromVec3( _vec3 ) - local _dist = _coord:Get2DDistance( _spot ) - local _safe = _overlap( aircraft, _scenery, _dist ) + for _,scenery in pairs(_sceneries) do + local _scenery=SCENERY:Register(scenery:getTypeName(), scenery) + local _vec3=scenery:getPoint() + local _coord=COORDINATE:NewFromVec3(_vec3) + local _dist=_coord:Get2DDistance(_spot) + local _safe=_overlap(aircraft,_scenery,_dist) if markobstacles then - local l, x, y, z = scenery:GetObjectSize( scenery ) - _coord:MarkToAll( string.format( "Scenery %s\nx=%.1f y=%.1f z=%.1f\nl=%.1f d=%.1f\nspot %d safe=%s", scenery:getTypeName(), x, y, z, l, _dist, _termid, tostring( _safe ) ) ) + local l,x,y,z=scenery:GetObjectSize(scenery) + _coord:MarkToAll(string.format("Scenery %s\nx=%.1f y=%.1f z=%.1f\nl=%.1f d=%.1f\nspot %d safe=%s", scenery:getTypeName(),x,y,z,l,_dist, _termid, tostring(_safe))) end if scanscenery and not _safe then - occupied = true + occupied=true end end -- Now check the already given spots so that we do not put a large aircraft next to one we already assigned a nearby spot. - for _, _takenspot in pairs( validspots ) do - local _dist = _takenspot.Coordinate:Get2DDistance( _spot ) - local _safe = _overlap( aircraft, aircraft, _dist ) + for _,_takenspot in pairs(validspots) do + local _dist=_takenspot.Coordinate:Get2DDistance(_spot) + local _safe=_overlap(aircraft, aircraft, _dist) if not _safe then - occupied = true + occupied=true end end - -- _spot:MarkToAll(string.format("Parking spot %d free=%s", parkingspot.TerminalID, tostring(not occupied))) + --_spot:MarkToAll(string.format("Parking spot %d free=%s", parkingspot.TerminalID, tostring(not occupied))) if occupied then - self:I( string.format( "%s: Parking spot id %d occupied.", airport, _termid ) ) + self:I(string.format("%s: Parking spot id %d occupied.", airport, _termid)) else - self:I( string.format( "%s: Parking spot id %d free.", airport, _termid ) ) - if nvalid < _nspots then - table.insert( validspots, { Coordinate = _spot, TerminalID = _termid } ) + self:I(string.format("%s: Parking spot id %d free.", airport, _termid)) + if nvalid<_nspots then + table.insert(validspots, {Coordinate=_spot, TerminalID=_termid}) end - nvalid = nvalid + 1 - self:I( string.format( "%s: Parking spot id %d free. Nfree=%d/%d.", airport, _termid, nvalid, _nspots ) ) + nvalid=nvalid+1 + self:I(string.format("%s: Parking spot id %d free. Nfree=%d/%d.", airport, _termid, nvalid,_nspots)) end end -- loop over units -- We found enough spots. - if nvalid >= _nspots then + if nvalid>=_nspots then return validspots end end -- check terminal type @@ -1384,22 +1483,23 @@ end -- @param #AIRBASE self -- @param #number TerminalID Terminal ID to check. -- @return #boolean `true` if this is a valid spot. -function AIRBASE:_CheckParkingLists( TerminalID ) +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 + 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 + 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 @@ -1416,16 +1516,16 @@ end -- @param #number Term_Type Termial type from getParking routine. -- @param #AIRBASE.TerminalType termtype Terminal type from AIRBASE.TerminalType enumerator. -- @return #boolean True if terminal types match. -function AIRBASE._CheckTerminalType( Term_Type, termtype ) +function AIRBASE._CheckTerminalType(Term_Type, termtype) -- Nill check for Term_Type. - if Term_Type == nil then + if Term_Type==nil then return false end -- If no terminal type is requested, we return true. BUT runways are excluded unless explicitly requested. - if termtype == nil then - if Term_Type == AIRBASE.TerminalType.Runway then + if termtype==nil then + if Term_Type==AIRBASE.TerminalType.Runway then return false else return true @@ -1433,25 +1533,25 @@ function AIRBASE._CheckTerminalType( Term_Type, termtype ) end -- Init no match. - local match = false + local match=false -- Standar case. - if Term_Type == termtype then - match = true + if Term_Type==termtype then + match=true end -- Artificial cases. Combination of terminal types. - if termtype == AIRBASE.TerminalType.OpenMedOrBig then - if Term_Type == AIRBASE.TerminalType.OpenMed or Term_Type == AIRBASE.TerminalType.OpenBig then - match = true + if termtype==AIRBASE.TerminalType.OpenMedOrBig then + if Term_Type==AIRBASE.TerminalType.OpenMed or Term_Type==AIRBASE.TerminalType.OpenBig then + match=true end - elseif termtype == AIRBASE.TerminalType.HelicopterUsable then - if Term_Type == AIRBASE.TerminalType.OpenMed or Term_Type == AIRBASE.TerminalType.OpenBig or Term_Type == AIRBASE.TerminalType.HelicopterOnly then - match = true - end - elseif termtype == AIRBASE.TerminalType.FighterAircraft then - if Term_Type == AIRBASE.TerminalType.OpenMed or Term_Type == AIRBASE.TerminalType.OpenBig or Term_Type == AIRBASE.TerminalType.Shelter then - match = true + elseif termtype==AIRBASE.TerminalType.HelicopterUsable then + if Term_Type==AIRBASE.TerminalType.OpenMed or Term_Type==AIRBASE.TerminalType.OpenBig or Term_Type==AIRBASE.TerminalType.HelicopterOnly then + match=true + end + elseif termtype==AIRBASE.TerminalType.FighterAircraft then + if Term_Type==AIRBASE.TerminalType.OpenMed or Term_Type==AIRBASE.TerminalType.OpenBig or Term_Type==AIRBASE.TerminalType.Shelter then + match=true end end @@ -1462,303 +1562,635 @@ end -- Runway ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Get runways. +-- @param #AIRBASE self +-- @return #table Runway data. +function AIRBASE:GetRunways() + return self.runways or {} +end + +--- Get runway by its name. +-- @param #AIRBASE self +-- @param #string Name Name of the runway, e.g. "31" or "21L". +-- @return #AIRBASE.Runway Runway data. +function AIRBASE:GetRunwayByName(Name) + + if Name==nil then + return + end + + if Name then + for _,_runway in pairs(self.runways) do + local runway=_runway --#AIRBASE.Runway + + -- Name including L or R, e.g. "31L". + local name=self:GetRunwayName(runway) + + if name==Name:upper() then + return runway + end + end + end + + self:E("ERROR: Could not find runway with name "..tostring(Name)) + return nil +end + +--- Init runways. +-- @param #AIRBASE self +-- @param #boolean IncludeInverse If `true` or `nil`, include inverse runways. +-- @return #table Runway data. +function AIRBASE:_InitRunways(IncludeInverse) + + -- Default is true. + if IncludeInverse==nil then + IncludeInverse=true + end + + -- Runway table. + local Runways={} + + if self:GetAirbaseCategory()~=Airbase.Category.AIRDROME then + self.runways={} + return {} + end + + --- Function to create a runway data table. + local function _createRunway(name, course, width, length, center) + + -- Bearing in rad. + local bearing=-1*course + + -- Heading in degrees. + local heading=math.deg(bearing) + + -- Data table. + local runway={} --#AIRBASE.Runway + runway.name=string.format("%02d", tonumber(name)) + runway.magheading=tonumber(runway.name)*10 + runway.heading=heading + runway.width=width or 0 + runway.length=length or 0 + runway.center=COORDINATE:NewFromVec3(center) + + -- Ensure heading is [0,360] + if runway.heading>360 then + runway.heading=runway.heading-360 + elseif runway.heading<0 then + runway.heading=runway.heading+360 + end + + -- For example at Nellis, DCS reports two runways, i.e. 03 and 21, BUT the "course" of both is -0.700 rad = 40 deg! + -- As a workaround, I check the difference between the "magnetic" heading derived from the name and the true heading. + -- If this is too large then very likely the "inverse" heading is the one we are looking for. + if math.abs(runway.heading-runway.magheading)>60 then + self:T(string.format("WARNING: Runway %s: heading=%.1f magheading=%.1f", runway.name, runway.heading, runway.magheading)) + runway.heading=runway.heading-180 + end + + -- Ensure heading is [0,360] + if runway.heading>360 then + runway.heading=runway.heading-360 + elseif runway.heading<0 then + runway.heading=runway.heading+360 + end + + -- Start and endpoint of runway. + runway.position=runway.center:Translate(-runway.length/2, runway.heading) + runway.endpoint=runway.center:Translate( runway.length/2, runway.heading) + + local init=runway.center:GetVec3() + local width = runway.width/2 + local L2=runway.length/2 + + local offset1 = {x = init.x + (math.cos(bearing + math.pi) * L2), y = init.z + (math.sin(bearing + math.pi) * L2)} + local offset2 = {x = init.x - (math.cos(bearing + math.pi) * L2), y = init.z - (math.sin(bearing + math.pi) * L2)} + + local points={} + points[1] = {x = offset1.x + (math.cos(bearing + (math.pi/2)) * width), y = offset1.y + (math.sin(bearing + (math.pi/2)) * width)} + points[2] = {x = offset1.x + (math.cos(bearing - (math.pi/2)) * width), y = offset1.y + (math.sin(bearing - (math.pi/2)) * width)} + points[3] = {x = offset2.x + (math.cos(bearing - (math.pi/2)) * width), y = offset2.y + (math.sin(bearing - (math.pi/2)) * width)} + points[4] = {x = offset2.x + (math.cos(bearing + (math.pi/2)) * width), y = offset2.y + (math.sin(bearing + (math.pi/2)) * width)} + + -- Runway zone. + runway.zone=ZONE_POLYGON_BASE:New(string.format("%s Runway %s", self.AirbaseName, runway.name), points) + + return runway + end + + + -- Get DCS object. + local airbase=self:GetDCSObject() + + if airbase then + + + -- Get DCS runways. + local runways=airbase:getRunways() + + -- Debug info. + self:T2(runways) + + if runways then + + -- Loop over runways. + for _,rwy in pairs(runways) do + + -- Debug info. + self:T(rwy) + + -- Get runway data. + local runway=_createRunway(rwy.Name, rwy.course, rwy.width, rwy.length, rwy.position) --#AIRBASE.Runway + + -- Add to table. + table.insert(Runways, runway) + + -- Include "inverse" runway. + if IncludeInverse then + + -- Create "inverse". + local idx=tonumber(runway.name) + local name2=tostring(idx-18) + if idx<18 then + name2=tostring(idx+18) + end + + -- Create "inverse" runway. + local runway=_createRunway(name2, rwy.course-math.pi, rwy.width, rwy.length, rwy.position) --#AIRBASE.Runway + + -- Add inverse to table. + table.insert(Runways, runway) + + end + + end + + end + + end + + -- Look for identical (parallel) runways, e.g. 03L and 03R at Nellis. + local rpairs={} + for i,_ri in pairs(Runways) do + local ri=_ri --#AIRBASE.Runway + for j,_rj in pairs(Runways) do + local rj=_rj --#AIRBASE.Runway + if i 0 + return ((b.z - a.z)*(c.x - a.x) - (b.x - a.x)*(c.z - a.z)) > 0 + end + + for i,j in pairs(rpairs) do + local ri=Runways[i] --#AIRBASE.Runway + local rj=Runways[j] --#AIRBASE.Runway + + -- Draw arrow. + --ri.center:ArrowToAll(rj.center) + + local c0=ri.center + + -- Vector in the direction of the runway. + local a=UTILS.VecTranslate(c0, 1000, ri.heading) + + -- Vector from runway i to runway j. + local b=UTILS.VecSubstract(rj.center, ri.center) + b=UTILS.VecAdd(ri.center, b) + + -- Check if rj is left of ri. + local left=isLeft(c0, a, b) + + --env.info(string.format("Found pair %s: i=%d, j=%d, left==%s", ri.name, i, j, tostring(left))) + + if left then + ri.isLeft=false + rj.isLeft=true + else + ri.isLeft=true + rj.isLeft=false + end + + --break + end + + -- Set runways. + self.runways=Runways + + return Runways +end + + --- Get runways data. Only for airdromes! -- @param #AIRBASE self -- @param #number magvar (Optional) Magnetic variation in degrees. -- @param #boolean mark (Optional) Place markers with runway data on F10 map. -- @return #table Runway data. -function AIRBASE:GetRunwayData( magvar, mark ) +function AIRBASE:GetRunwayData(magvar, mark) -- Runway table. - local runways = {} + local runways={} - if self:GetAirbaseCategory() ~= Airbase.Category.AIRDROME then + if self:GetAirbaseCategory()~=Airbase.Category.AIRDROME then return {} end -- Get spawn points on runway. These can be used to determine the runway heading. - local runwaycoords = self:GetParkingSpotsCoordinates( AIRBASE.TerminalType.Runway ) + local runwaycoords=self:GetParkingSpotsCoordinates(AIRBASE.TerminalType.Runway) -- Debug: For finding the numbers of the spawn points belonging to each runway. if false then - for i, _coord in pairs( runwaycoords ) do - local coord = _coord -- Core.Point#COORDINATE - coord:Translate( 100, 0 ):MarkToAll( "Runway i=" .. i ) + for i,_coord in pairs(runwaycoords) do + local coord=_coord --Core.Point#COORDINATE + coord:Translate(100, 0):MarkToAll("Runway i="..i) end end -- Magnetic declination. - magvar = magvar or UTILS.GetMagneticDeclination() + magvar=magvar or UTILS.GetMagneticDeclination() -- Number of runways. - local N = #runwaycoords - local N2 = N / 2 - local exception = false + local N=#runwaycoords + local N2=N/2 + local exception=false -- Airbase name. - local name = self:GetName() + local name=self:GetName() + -- Exceptions - if name == AIRBASE.Nevada.Jean_Airport or - name == AIRBASE.Nevada.Creech_AFB or - 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 or - name == AIRBASE.MarianaIslands.Andersen_AFB then + if name==AIRBASE.Nevada.Jean_Airport or + name==AIRBASE.Nevada.Creech_AFB or + 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 or + name==AIRBASE.MarianaIslands.Andersen_AFB then -- 1-->4, 2-->3, 3-->2, 4-->1 - exception = 1 + exception=1 - elseif UTILS.GetDCSMap() == DCSMAP.Syria and N >= 2 and - name ~= AIRBASE.Syria.Minakh and - name ~= AIRBASE.Syria.Damascus and - name ~= AIRBASE.Syria.Khalkhalah and - name ~= AIRBASE.Syria.Marj_Ruhayyil and - name ~= AIRBASE.Syria.Beirut_Rafic_Hariri then + elseif UTILS.GetDCSMap()==DCSMAP.Syria and N>=2 and + name~=AIRBASE.Syria.Minakh and + name~=AIRBASE.Syria.Damascus and + name~=AIRBASE.Syria.Khalkhalah and + name~=AIRBASE.Syria.Marj_Ruhayyil and + name~=AIRBASE.Syria.Beirut_Rafic_Hariri then -- 1-->3, 2-->4, 3-->1, 4-->2 - exception = 2 + exception=2 end --- Function returning the index of the runway coordinate belonding to the given index i. - local function f( i ) + local function f(i) local j - if exception == 1 then + if exception==1 then - j = N - (i - 1) -- 1-->4, 2-->3 + j=N-(i-1) -- 1-->4, 2-->3 - elseif exception == 2 then + elseif exception==2 then - if i <= N2 then - j = i + N2 -- 1-->3, 2-->4 + if i<=N2 then + j=i+N2 -- 1-->3, 2-->4 else - j = i - N2 -- 3-->1, 4-->3 + j=i-N2 -- 3-->1, 4-->3 end else - if i % 2 == 0 then - j = i - 1 -- even 2-->1, 4-->3 + if i%2==0 then + j=i-1 -- even 2-->1, 4-->3 else - j = i + 1 -- odd 1-->2, 3-->4 + j=i+1 -- odd 1-->2, 3-->4 end end -- Special case where there is no obvious order. - if name == AIRBASE.Syria.Beirut_Rafic_Hariri then - if i == 1 then - j = 3 - elseif i == 2 then - j = 6 - elseif i == 3 then - j = 1 - elseif i == 4 then - j = 5 - elseif i == 5 then - j = 4 - elseif i == 6 then - j = 2 + if name==AIRBASE.Syria.Beirut_Rafic_Hariri then + if i==1 then + j=3 + elseif i==2 then + j=6 + elseif i==3 then + j=1 + elseif i==4 then + j=5 + elseif i==5 then + j=4 + elseif i==6 then + j=2 end end - if name == AIRBASE.Syria.Ramat_David then - if i == 1 then - j = 4 - elseif i == 2 then - j = 6 - elseif i == 3 then - j = 5 - elseif i == 4 then - j = 1 - elseif i == 5 then - j = 3 - elseif i == 6 then - j = 2 + if name==AIRBASE.Syria.Ramat_David then + if i==1 then + j=4 + elseif i==2 then + j=6 + elseif i==3 then + j=5 + elseif i==4 then + j=1 + elseif i==5 then + j=3 + elseif i==6 then + j=2 end end return j end - for i = 1, N do + + for i=1,N do -- Get the other spawn point coordinate. - local j = f( i ) + local j=f(i) -- Debug info. - -- env.info(string.format("Runway i=%s j=%s (N=%d #runwaycoord=%d)", tostring(i), tostring(j), N, #runwaycoords)) + --env.info(string.format("Runway i=%s j=%s (N=%d #runwaycoord=%d)", tostring(i), tostring(j), N, #runwaycoords)) -- Coordinates of the two runway points. - local c1 = runwaycoords[i] -- Core.Point#COORDINATE - local c2 = runwaycoords[j] -- Core.Point#COORDINATE + local c1=runwaycoords[i] --Core.Point#COORDINATE + local c2=runwaycoords[j] --Core.Point#COORDINATE -- Heading of runway. - local hdg = c1:HeadingTo( c2 ) + local hdg=c1:HeadingTo(c2) -- Runway ID: heading=070° ==> idx="07" - local idx = string.format( "%02d", UTILS.Round( (hdg - magvar) / 10, 0 ) ) + local idx=string.format("%02d", UTILS.Round((hdg-magvar)/10, 0)) -- Runway table. - local runway = {} -- #AIRBASE.Runway - runway.heading = hdg - runway.idx = idx - runway.length = c1:Get2DDistance( c2 ) - runway.position = c1 - runway.endpoint = c2 + local runway={} --#AIRBASE.Runway + runway.heading=hdg + runway.idx=idx + runway.length=c1:Get2DDistance(c2) + runway.position=c1 + runway.endpoint=c2 -- Debug info. - -- self:I(string.format("Airbase %s: Adding runway id=%s, heading=%03d, length=%d m i=%d j=%d", self:GetName(), runway.idx, runway.heading, runway.length, i, j)) + --self:I(string.format("Airbase %s: Adding runway id=%s, heading=%03d, length=%d m i=%d j=%d", self:GetName(), runway.idx, runway.heading, runway.length, i, j)) -- Debug mark if mark then - runway.position:MarkToAll( string.format( "Runway %s: true heading=%03d (magvar=%d), length=%d m, i=%d, j=%d", runway.idx, runway.heading, magvar, runway.length, i, j ) ) + runway.position:MarkToAll(string.format("Runway %s: true heading=%03d (magvar=%d), length=%d m, i=%d, j=%d", runway.idx, runway.heading, magvar, runway.length, i, j)) end -- Add runway. - table.insert( runways, runway ) + table.insert(runways, runway) end return runways end ---- Set the active runway in case it cannot be determined by the wind direction. +--- Set the active runway for landing and takeoff. -- @param #AIRBASE self --- @param #number iactive Number of the active runway in the runway data table. -function AIRBASE:SetActiveRunway( iactive ) - self.activerwyno = iactive +-- @param #string Name Name of the runway, e.g. "31" or "02L" or "90R". If not given, the runway is determined from the wind direction. +-- @param #boolean PreferLeft If `true`, perfer the left runway. If `false`, prefer the right runway. If `nil` (default), do not care about left or right. +function AIRBASE:SetActiveRunway(Name, PreferLeft) + + self:SetActiveRunwayTakeoff(Name, PreferLeft) + + self:SetActiveRunwayLanding(Name,PreferLeft) + end ---- Get the active runway based on current wind direction. +--- Set the active runway for landing. -- @param #AIRBASE self --- @param #number magvar (Optional) Magnetic variation in degrees. --- @return #AIRBASE.Runway Active runway data table. -function AIRBASE:GetActiveRunway( magvar ) +-- @param #string Name Name of the runway, e.g. "31" or "02L" or "90R". If not given, the runway is determined from the wind direction. +-- @param #boolean PreferLeft If `true`, perfer the left runway. If `false`, prefer the right runway. If `nil` (default), do not care about left or right. +-- @return #AIRBASE.Runway The active runway for landing. +function AIRBASE:SetActiveRunwayLanding(Name, PreferLeft) - -- Get runways data (initialize if necessary). - local runways = self:GetRunwayData( magvar ) - - -- Return user forced active runway if it was set. - if self.activerwyno then - return runways[self.activerwyno] + local runway=self:GetRunwayByName(Name) + + if not runway then + runway=self:GetRunwayIntoWind(PreferLeft) end + + if runway then + self:I(string.format("%s: Setting active runway for landing as %s", self.AirbaseName, self:GetRunwayName(runway))) + else + self:E("ERROR: Could not set the runway for landing!") + end + + self.runwayLanding=runway + + return runway +end + +--- Get the active runways. +-- @param #AIRBASE self +-- @return #AIRBASE.Runway The active runway for landing. +-- @return #AIRBASE.Runway The active runway for takeoff. +function AIRBASE:GetActiveRunway() + return self.runwayLanding, self.runwayTakeoff +end + + +--- Get the active runway for landing. +-- @param #AIRBASE self +-- @return #AIRBASE.Runway The active runway for landing. +function AIRBASE:GetActiveRunwayLanding() + return self.runwayLanding +end + +--- Get the active runway for takeoff. +-- @param #AIRBASE self +-- @return #AIRBASE.Runway The active runway for takeoff. +function AIRBASE:GetActiveRunwayTakeoff() + return self.runwayTakeoff +end + + +--- Set the active runway for takeoff. +-- @param #AIRBASE self +-- @param #string Name Name of the runway, e.g. "31" or "02L" or "90R". If not given, the runway is determined from the wind direction. +-- @param #boolean PreferLeft If `true`, perfer the left runway. If `false`, prefer the right runway. If `nil` (default), do not care about left or right. +-- @return #AIRBASE.Runway The active runway for landing. +function AIRBASE:SetActiveRunwayTakeoff(Name, PreferLeft) + + local runway=self:GetRunwayByName(Name) + + if not runway then + runway=self:GetRunwayIntoWind(PreferLeft) + end + + if runway then + self:I(string.format("%s: Setting active runway for takeoff as %s", self.AirbaseName, self:GetRunwayName(runway))) + else + self:E("ERROR: Could not set the runway for takeoff!") + end + + self.runwayTakeoff=runway + + return runway +end + + +--- Get the runway where aircraft would be taking of or landing into the direction of the wind. +-- NOTE that this requires the wind to be non-zero as set in the mission editor. +-- @param #AIRBASE self +-- @param #boolean PreferLeft If `true`, perfer the left runway. If `false`, prefer the right runway. If `nil` (default), do not care about left or right. +-- @return #AIRBASE.Runway Active runway data table. +function AIRBASE:GetRunwayIntoWind(PreferLeft) + + -- Get runway data. + local runways=self:GetRunways() -- Get wind vector. - local Vwind = self:GetCoordinate():GetWindWithTurbulenceVec3() - local norm = UTILS.VecNorm( Vwind ) + local Vwind=self:GetCoordinate():GetWindWithTurbulenceVec3() + local norm=UTILS.VecNorm(Vwind) -- Active runway number. - local iact = 1 + local iact=1 -- Check if wind is blowing (norm>0). - if norm > 0 then + if norm>0 then -- Normalize wind (not necessary). - Vwind.x = Vwind.x / norm - Vwind.y = 0 - Vwind.z = Vwind.z / norm + Vwind.x=Vwind.x/norm + Vwind.y=0 + Vwind.z=Vwind.z/norm -- Loop over runways. - local dotmin = nil - for i, _runway in pairs( runways ) do - local runway = _runway -- #AIRBASE.Runway + local dotmin=nil + for i,_runway in pairs(runways) do + local runway=_runway --#AIRBASE.Runway + + if PreferLeft==nil or PreferLeft==runway.isLeft then - -- Angle in rad. - local alpha = math.rad( runway.heading ) - - -- Runway vector. - local Vrunway = { x = math.cos( alpha ), y = 0, z = math.sin( alpha ) } - - -- Dot product: parallel component of the two vectors. - local dot = UTILS.VecDot( Vwind, Vrunway ) - - -- Debug. - -- env.info(string.format("runway=%03d° dot=%.3f", runway.heading, dot)) - - -- New min? - if dotmin == nil or dot < dotmin then - dotmin = dot - iact = i + -- Angle in rad. + local alpha=math.rad(runway.heading) + + -- Runway vector. + local Vrunway={x=math.cos(alpha), y=0, z=math.sin(alpha)} + + -- Dot product: parallel component of the two vectors. + local dot=UTILS.VecDot(Vwind, Vrunway) + + -- New min? + if dotmin==nil or dot radius %.1f m. Despawn = %s.", self:GetName(), unit:GetName(), group:GetName(), _i, dist, radius, tostring( despawn ) ) ) - -- unit:FlareGreen() + self:T(string.format("%s, unit %s of group %s was NOT spawned on runway #%d. Distance %.1f > radius %.1f m. Despawn = %s.", self:GetName(), unit:GetName(), group:GetName(),_i, dist, radius, tostring(despawn))) + --unit:FlareGreen() end end else - self:T( string.format( "%s, checking if unit %s of group %s is on runway. Unit is NOT alive.", self:GetName(), unit:GetName(), group:GetName() ) ) + self:T(string.format("%s, checking if unit %s of group %s is on runway. Unit is NOT alive.",self:GetName(), unit:GetName(), group:GetName())) end end else - self:T( string.format( "%s, checking if group %s is on runway. Group is NOT alive.", self:GetName(), group:GetName() ) ) + self:T(string.format("%s, checking if group %s is on runway. Group is NOT alive.",self:GetName(), group:GetName())) end return false diff --git a/Moose Development/Moose/Wrapper/Client.lua b/Moose Development/Moose/Wrapper/Client.lua index 445c655ee..529accdcf 100644 --- a/Moose Development/Moose/Wrapper/Client.lua +++ b/Moose Development/Moose/Wrapper/Client.lua @@ -13,6 +13,17 @@ --- The CLIENT class -- @type CLIENT +-- @field #string ClassName Name of the class. +-- @field #string ClientName Name of the client. +-- @field #string ClientBriefing Briefing. +-- @field #function ClientCallBack Callback function. +-- @field #table ClientParameters Parameters of the callback function. +-- @field #number ClientGroupID Group ID of the client. +-- @field #string ClientGroupName Group name. +-- @field #boolean ClientAlive Client alive. +-- @field #boolean ClientAlive2 Client alive 2. +-- @field #table Players Player table. +-- @field Core.Point#COORDINATE SpawnCoord Spawn coordinate from the template. -- @extends Wrapper.Unit#UNIT diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 5a59a28e2..156fb8be2 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -671,7 +671,7 @@ end --- Activate ICLS system of the CONTROLLABLE. The controllable should be an aircraft carrier! -- @param #CONTROLLABLE self -- @param #number Channel ICLS channel. --- @param #number UnitID The ID of the unit the ICLS system is attached to. Useful if more units are in one group. +-- @param #number UnitID The DCS UNIT ID of the unit the ICLS system is attached to. Useful if more units are in one group. -- @param #string Callsign Morse code identification callsign. -- @param #number Delay (Optional) Delay in seconds before the ICLS is deactivated. -- @return #CONTROLLABLE self @@ -725,6 +725,34 @@ function CONTROLLABLE:CommandActivateLink4(Frequency, UnitID, Callsign, Delay) return self end +--- Activate LINK4 system of the CONTROLLABLE. The controllable should be an aircraft carrier! +-- @param #CONTROLLABLE self +-- @param #number Frequency Link4 Frequency in MHz, e.g. 336 +-- @param #number UnitID The DCS UNIT ID of the unit the LINK4 system is attached to. Useful if more units are in one group. +-- @param #string Callsign Morse code identification callsign. +-- @param #number Delay (Optional) Delay in seconds before the LINK4 is deactivated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandActivateLink4(Frequency, UnitID, Callsign, Delay) + + -- Command to activate Link4 system. + local CommandActivateLink4= { + id = "ActivateLink4", + params= { + ["frequency "] = Frequency*1000, + ["unitId"] = UnitID, + ["name"] = Callsign, + } + } + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandActivateLink4, {self}, Delay) + else + self:SetCommand(CommandActivateLink4) + end + + return self +end + --- Deactivate the active beacon of the CONTROLLABLE. -- @param #CONTROLLABLE self -- @param #number Delay (Optional) Delay in seconds before the beacon is deactivated. @@ -734,8 +762,10 @@ function CONTROLLABLE:CommandDeactivateBeacon( Delay ) -- Command to deactivate local CommandDeactivateBeacon = { id = 'DeactivateBeacon', params = {} } - if Delay and Delay > 0 then - SCHEDULER:New( nil, self.CommandDeactivateBeacon, { self }, Delay ) + local CommandDeactivateBeacon={id='DeactivateBeacon', params={}} + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandDeactivateBeacon, {self}, Delay) else self:SetCommand( CommandDeactivateBeacon ) end @@ -743,6 +773,24 @@ function CONTROLLABLE:CommandDeactivateBeacon( Delay ) return self end +--- Deactivate the active Link4 of the CONTROLLABLE. +-- @param #CONTROLLABLE self +-- @param #number Delay (Optional) Delay in seconds before the Link4 is deactivated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandDeactivateLink4(Delay) + + -- Command to deactivate + local CommandDeactivateLink4={id='DeactivateLink4', params={}} + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandDeactivateLink4, {self}, Delay) + else + self:SetCommand(CommandDeactivateLink4) + end + + return self +end + --- Deactivate the ICLS of the CONTROLLABLE. -- @param #CONTROLLABLE self -- @param #number Delay (Optional) Delay in seconds before the ICLS is deactivated. @@ -1424,7 +1472,7 @@ function CONTROLLABLE:TaskEscort( FollowControllable, Vec3, LastWaypointIndex, E DCSTask = { id = 'Escort', params = { - groupId = FollowControllable:GetID(), + groupId = FollowControllable and FollowControllable:GetID() or nil, pos = Vec3, lastWptIndexFlag = LastWaypointIndex and true or false, lastWptIndex = LastWaypointIndex, @@ -1453,11 +1501,11 @@ function CONTROLLABLE:TaskFireAtPoint( Vec2, Radius, AmmoCount, WeaponType, Alti id = 'FireAtPoint', params = { point = Vec2, - x=Vec2.x, - y=Vec2.y, + x = Vec2.x, + y = Vec2.y, zoneRadius = Radius, radius = Radius, - expendQty = 100, -- dummy value + expendQty = 1, -- dummy value expendQtyEnabled = false, alt_type = ASL and 0 or 1, }, @@ -1476,7 +1524,8 @@ function CONTROLLABLE:TaskFireAtPoint( Vec2, Radius, AmmoCount, WeaponType, Alti DCSTask.params.weaponType = WeaponType end - -- self:I(DCSTask) + --env.info("FF fireatpoint") + --BASE:I(DCSTask) return DCSTask end @@ -1569,6 +1618,28 @@ function CONTROLLABLE:EnRouteTaskEngageTargetsInZone( Vec2, Radius, TargetTypes, return DCSTask end +--- (AIR) Enroute anti-ship task. +-- @param #CONTROLLABLE self +-- @param DCS#AttributeNameArray TargetTypes Array of target categories allowed to engage. Default `{"Ships"}`. +-- @param #number Priority (Optional) All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. Default 0. +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:EnRouteTaskAntiShip(TargetTypes, Priority) + + local DCSTask = { + id = 'EngageTargets', + key = "AntiShip", + --auto = false, + --enabled = true, + params = { + targetTypes = TargetTypes or {"Ships"}, + priority = Priority or 0 + } + } + + return DCSTask +end + + --- (AIR) Engaging a controllable. The task does not assign the target controllable to the unit/controllable to attack now; it just allows the unit/controllable to engage the target controllable as well as other assigned targets. -- @param #CONTROLLABLE self -- @param Wrapper.Controllable#CONTROLLABLE AttackGroup The Controllable to be attacked. @@ -1835,7 +1906,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() @@ -1866,12 +1937,13 @@ do -- Patrol methods end end + local Waypoint = Waypoints[1] local Speed = Waypoint.speed or (20 / 3.6) - local From = FromCoord:WaypointGround( Speed ) + local From = FromCoord:WaypointGround( Speed ) if IsSub then - From = FromCoord:WaypointNaval( Speed, Waypoint.alt ) + From = FromCoord:WaypointNaval( Speed, Waypoint.alt ) end table.insert( Waypoints, 1, From ) @@ -2023,22 +2095,21 @@ do -- Patrol methods end ---- Return a Mission task to follow a given route defined by Points. + +--- Return a "Misson" task to follow a given route defined by Points. -- @param #CONTROLLABLE self -- @param #table Points A table of route points. --- @return DCS#Task +-- @return DCS#Task DCS mission task. Has entries `.id="Mission"`, `params`, were params has entries `airborne` and `route`, which is a table of `points`. function CONTROLLABLE:TaskRoute( Points ) - self:F2( Points ) local DCSTask = { id = 'Mission', params = { - airborne = self:IsAir(), + airborne = self:IsAir(), -- This is important to make aircraft land without respawning them (which was a long standing DCS issue). route = {points = Points}, }, } - - self:T3( { DCSTask } ) + return DCSTask end @@ -3746,10 +3817,10 @@ 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 +-- @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 -- @param #string formation Formation string as in the mission editor, e.g. "Vee", "Diamond", "Line abreast", etc. Defaults to "Off Road" -- @return #CONTROLLABLE self function CONTROLLABLE:RelocateGroundRandomInRadius( speed, radius, onroad, shortcut, formation ) @@ -3817,9 +3888,10 @@ function POSITIONABLE:IsSubmarine() return nil end + --- Sets the controlled group to go at the specified speed in meters per second. -- @param #CONTROLLABLE self --- @param #number Speed Speed in meters per second. +-- @param #number Speed Speed in meters per second -- @param #boolean Keep (Optional) When set to true, will maintain the speed on passing waypoints. If not present or false, the controlled group will return to the speed as defined by their route. -- @return #CONTROLLABLE self function CONTROLLABLE:SetSpeed(Speed, Keep) diff --git a/Moose Development/Moose/Wrapper/Identifiable.lua b/Moose Development/Moose/Wrapper/Identifiable.lua index a450c4ef3..7b77c9512 100644 --- a/Moose Development/Moose/Wrapper/Identifiable.lua +++ b/Moose Development/Moose/Wrapper/Identifiable.lua @@ -35,7 +35,7 @@ IDENTIFIABLE = { local _CategoryName = { [Unit.Category.AIRPLANE] = "Airplane", - [Unit.Category.HELICOPTER] = "Helicoper", + [Unit.Category.HELICOPTER] = "Helicopter", [Unit.Category.GROUND_UNIT] = "Ground Identifiable", [Unit.Category.SHIP] = "Ship", [Unit.Category.STRUCTURE] = "Structure", @@ -56,8 +56,7 @@ end -- If the Identifiable is not alive, nil is returned. -- If the Identifiable is alive, true is returned. -- @param #IDENTIFIABLE self --- @return #boolean true if Identifiable is alive. --- @return #nil if the Identifiable is not existing or is not alive. +-- @return #boolean true if Identifiable is alive or `#nil` if the Identifiable is not existing or is not alive. function IDENTIFIABLE:IsAlive() self:F3( self.IdentifiableName ) @@ -77,11 +76,8 @@ end --- Returns DCS Identifiable object name. -- The function provides access to non-activated objects too. -- @param #IDENTIFIABLE self --- @return #string The name of the DCS Identifiable. --- @return #nil The DCS Identifiable is not existing or alive. +-- @return #string The name of the DCS Identifiable or `#nil`. function IDENTIFIABLE:GetName() - self:F2( self.IdentifiableName ) - local IdentifiableName = self.IdentifiableName return IdentifiableName end @@ -148,8 +144,7 @@ end --- Returns coalition of the Identifiable. -- @param #IDENTIFIABLE self --- @return DCS#coalition.side The side of the coalition. --- @return #nil The DCS Identifiable is not existing or alive. +-- @return DCS#coalition.side The side of the coalition or `#nil` The DCS Identifiable is not existing or alive. function IDENTIFIABLE:GetCoalition() self:F2( self.IdentifiableName ) @@ -190,8 +185,7 @@ end --- Returns country of the Identifiable. -- @param #IDENTIFIABLE self --- @return DCS#country.id The country identifier. --- @return #nil The DCS Identifiable is not existing or alive. +-- @return DCS#country.id The country identifier or `#nil` The DCS Identifiable is not existing or alive. function IDENTIFIABLE:GetCountry() self:F2( self.IdentifiableName ) @@ -222,8 +216,7 @@ end --- Returns Identifiable descriptor. Descriptor type depends on Identifiable category. -- @param #IDENTIFIABLE self --- @return DCS#Object.Desc The Identifiable descriptor. --- @return #nil The DCS Identifiable is not existing or alive. +-- @return DCS#Object.Desc The Identifiable descriptor or `#nil` The DCS Identifiable is not existing or alive. function IDENTIFIABLE:GetDesc() self:F2( self.IdentifiableName ) @@ -242,8 +235,7 @@ end --- Check if the Object has the attribute. -- @param #IDENTIFIABLE self -- @param #string AttributeName The attribute name. --- @return #boolean true if the attribute exists. --- @return #nil The DCS Identifiable is not existing or alive. +-- @return #boolean true if the attribute exists or `#nil` The DCS Identifiable is not existing or alive. function IDENTIFIABLE:HasAttribute( AttributeName ) self:F2( self.IdentifiableName ) @@ -266,8 +258,10 @@ function IDENTIFIABLE:GetCallsign() return '' end - +--- Gets the threat level. +-- @param #IDENTIFIABLE self +-- @return #number Threat level. +-- @return #string Type. function IDENTIFIABLE:GetThreatLevel() - return 0, "Scenery" end diff --git a/Moose Development/Moose/Wrapper/Marker.lua b/Moose Development/Moose/Wrapper/Marker.lua index b5dcfc5df..21936ae46 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. -- @type MARKER @@ -150,7 +150,7 @@ _MARKERID = 0 --- Marker class version. -- @field #string version -MARKER.version = "0.1.0" +MARKER.version="0.1.1" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -175,7 +175,9 @@ function MARKER:New( Coordinate, Text ) -- Inherit everything from FSM class. local self = BASE:Inherit( self, FSM:New() ) -- #MARKER - self.coordinate = Coordinate + local self=BASE:Inherit(self, FSM:New()) -- #MARKER + + self.coordinate=UTILS.DeepCopy(Coordinate) self.text = Text @@ -315,6 +317,16 @@ function MARKER:ReadOnly() return self end +--- Marker is readonly. Text cannot be changed and marker cannot be removed. +-- @param #MARKER self +-- @return #MARKER self +function MARKER:ReadWrite() + + self.readonly=false + + return self +end + --- Set message that is displayed on screen if the marker is added. -- @param #MARKER self -- @param #string Text Message displayed when the marker is added. @@ -580,7 +592,7 @@ end --- Set text that is displayed in the marker panel. Note this does not show the marker. -- @param #MARKER self --- @param #string Text Marker text. Default is an empty sting "". +-- @param #string Text Marker text. Default is an empty string "". -- @return #MARKER self function MARKER:SetText( Text ) self.text = Text and tostring( Text ) or "" @@ -637,7 +649,9 @@ function MARKER:OnEventMarkRemoved( EventData ) local MarkID = EventData.MarkID - self:T3( self.lid .. string.format( "Captured event MarkAdded for Mark ID=%s", tostring( MarkID ) ) ) + local MarkID=EventData.MarkID + + self:T3(self.lid..string.format("Captured event MarkRemoved for Mark ID=%s", tostring(MarkID))) if MarkID == self.mid then @@ -664,16 +678,22 @@ function MARKER:OnEventMarkChange( EventData ) if MarkID == self.mid then - self:Changed( EventData ) + local MarkID=EventData.MarkID - self:TextChanged( tostring( EventData.MarkText ) ) + self:T3(self.lid..string.format("Captured event MarkChange for Mark ID=%s", tostring(MarkID))) + + if MarkID==self.mid then + + self.text=tostring(EventData.MarkText) + + self:Changed(EventData) end end end - +end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- FSM Event Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index 60f3687a6..87e199c9a 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -381,10 +381,10 @@ function POSITIONABLE:GetCoordinate() -- Get the current position. local PositionableVec3 = self:GetVec3() - local coord = COORDINATE:NewFromVec3( PositionableVec3 ) + local coord=COORDINATE:NewFromVec3(PositionableVec3) local heading = self:GetHeading() coord.Heading = heading - -- Return a new coordinate object. + -- Return a new coordiante object. return coord end @@ -703,11 +703,11 @@ function POSITIONABLE:IsSubmarine() if DCSUnit then local UnitDescriptor = DCSUnit:getDesc() - if UnitDescriptor.attributes["Submarines"] == true then - return true - else - return false - end + if UnitDescriptor.attributes["Submarines"] == true then + return true + else + return false + end end self:E( { "Cannot check IsSubmarine", Positionable = self, Alive = self:IsAlive() } ) @@ -779,11 +779,11 @@ function POSITIONABLE:GetRelativeVelocity( Positionable ) return UTILS.VecNorm( vtot ) end + --- Returns the POSITIONABLE height above sea level in meters. -- @param Wrapper.Positionable#POSITIONABLE self --- @return DCS#Vec3 The height of the POSITIONABLE in meters. --- @return #nil The POSITIONABLE is not existing or alive. -function POSITIONABLE:GetHeight() +-- @return DCS#Vec3 Height of the positionable in meters (or nil, if the object does not exist). +function POSITIONABLE:GetHeight() --R2.1 self:F2( self.PositionableName ) local DCSPositionable = self:GetDCSObject() @@ -1232,6 +1232,33 @@ function POSITIONABLE:MessageToGroup( Message, Duration, MessageGroup, Name ) return nil end +--- Send a message to a @{Wrapper.Unit}. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #POSITIONABLE self +-- @param #string Message The message text +-- @param DCS#Duration Duration The duration of the message. +-- @param Wrapper.Unit#UNIT MessageUnit The UNIT object receiving the message. +-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +function POSITIONABLE:MessageToUnit( Message, Duration, MessageUnit, Name ) + self:F2( { Message, Duration } ) + + local DCSObject = self:GetDCSObject() + if DCSObject then + if DCSObject:isExist() then + if MessageUnit:IsAlive() then + self:GetMessage( Message, Duration, Name ):ToUnit( MessageUnit ) + else + BASE:E( { "Message not sent to Unit; Unit is not alive...", Message = Message, MessageUnit = MessageUnit } ) + end + else + BASE:E( { "Message not sent to Unit; Positionable is not alive ...", Message = Message, Positionable = self, MessageUnit = MessageUnit } ) + end + end + + + return nil +end + --- Send a message of a message type to a @{Wrapper.Group}. -- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. -- @param #POSITIONABLE self @@ -1298,6 +1325,30 @@ function POSITIONABLE:MessageToSetUnit( Message, Duration, MessageSetUnit, Name return nil end +--- Send a message to a @{Core.Set#SET_UNIT}. +-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. +-- @param #POSITIONABLE self +-- @param #string Message The message text +-- @param DCS#Duration Duration The duration of the message. +-- @param Core.Set#SET_UNIT MessageSetUnit The SET_UNIT collection receiving the message. +-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +function POSITIONABLE:MessageToSetUnit( Message, Duration, MessageSetUnit, Name ) + self:F2( { Message, Duration } ) + + local DCSObject = self:GetDCSObject() + if DCSObject then + if DCSObject:isExist() then + MessageSetUnit:ForEachUnit( + function( MessageGroup ) + self:GetMessage( Message, Duration, Name ):ToUnit( MessageGroup ) + end + ) + end + end + + return nil +end + --- Send a message to the players in the @{Wrapper.Group}. -- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. -- @param #POSITIONABLE self @@ -1480,17 +1531,15 @@ 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 the number of infantry soldiers that can be embarked into an aircraft (airplane or helicopter). + -- Returns `nil` for ground or ship units. + -- @param #POSITIONABLE self + -- @return #number Descent number of soldiers that fit into the unit. Returns `#nil` for ground and ship units. + function POSITIONABLE:GetTroopCapacity() + local DCSunit=self:GetDCSObject() --DCS#Unit + local capacity=DCSunit:getDescentCapacity() + return capacity + end --- Get Cargo Bay Free Weight in kg. -- @param #POSITIONABLE self @@ -1509,55 +1558,97 @@ 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 + -- @param #number WeightLimit (Optional) Weight limit in kg. If not given, the value is taken from the descriptors or hard coded. function POSITIONABLE:SetCargoBayWeightLimit( WeightLimit ) - if WeightLimit then + if WeightLimit then + --- + -- User defined value + --- self.__.CargoBayWeightLimit = WeightLimit elseif self.__.CargoBayWeightLimit ~= nil then -- Value already set ==> Do nothing! else - -- If WeightLimit is not provided, we will calculate it depending on the type of unit. + --- + -- Weightlimit is not provided, we will calculate it depending on the type of unit. + --- + + -- Descriptors that contain the type name and for aircraft also weights. + local Desc = self:GetDesc() + self:F({Desc=Desc}) + + -- Unit type name. + local TypeName=Desc.typeName or "Unknown Type" -- When an airplane or helicopter, we calculate the WeightLimit based on the descriptor. if self:IsAir() then - local Desc = self:GetDesc() - self:F( { Desc = Desc } ) + -- Max takeoff weight if DCS descriptors have unrealstic values. local Weights = { - ["C-17A"] = 35000, -- 77519 cannot be used, because it loads way too many APCs and infantry. - ["C-130"] = 22000 -- The real value cannot be used, because it loads way too many APCs and infantry. + -- C-17A + -- Wiki says: max=265,352, empty=128,140, payload=77,516 (134 troops, 1 M1 Abrams tank, 2 M2 Bradley or 3 Stryker) + -- DCS says: max=265,350, empty=125,645, fuel=132,405 ==> Cargo Bay=7300 kg with a full fuel load (lot of fuel!) and 73300 with half a fuel load. + --["C-17A"] = 35000, --77519 cannot be used, because it loads way too much apcs and infantry. + -- C-130: + -- DCS says: max=79,380, empty=36,400, fuel=10,415 kg ==> Cargo Bay=32,565 kg with fuel load. + -- Wiki says: max=70,307, empty=34,382, payload=19,000 kg (92 passengers, 2-3 Humvees or 2 M113s), max takeoff weight 70,037 kg. + -- Here we say two M113s should be transported. Each one weights 11,253 kg according to DCS. So the cargo weight should be 23,000 kg with a full load of fuel. + -- This results in a max takeoff weight of 69,815 kg (23,000+10,415+36,400), which is very close to the Wiki value of 70,037 kg. + ["C-130"] = 70000, } - - self.__.CargoBayWeightLimit = Weights[Desc.typeName] or (Desc.massMax - (Desc.massEmpty + Desc.fuelMassMax)) + + -- Max (takeoff) weight (empty+fuel+cargo weight). + local massMax= Desc.massMax or 0 + + -- Adjust value if set above. + local maxTakeoff=Weights[TypeName] + if maxTakeoff then + massMax=maxTakeoff + end + + -- Empty weight. + local massEmpty=Desc.massEmpty or 0 + + -- Fuel. The descriptor provides the max fuel mass in kg. This needs to be multiplied by the relative fuel amount to calculate the actual fuel mass on board. + local massFuelMax=Desc.fuelMassMax or 0 + local relFuel=math.min(self:GetFuel() or 1.0, 1.0) -- We take 1.0 as max in case of external fuel tanks. + local massFuel=massFuelMax*relFuel + + -- Number of soldiers according to DCS function + --local troopcapacity=self:GetTroopCapacity() or 0 + + -- Calculate max cargo weight, which is the max (takeoff) weight minus the empty weight minus the actual fuel weight. + local CargoWeight=massMax-(massEmpty+massFuel) + + -- Debug info. + self:T(string.format("Setting Cargo bay weight limit [%s]=%d kg (Mass max=%d, empty=%d, fuelMax=%d kg (rel=%.3f), fuel=%d kg", TypeName, CargoWeight, massMax, massEmpty, massFuelMax, relFuel, massFuel)) + --self:T(string.format("Descent Troop Capacity=%d ==> %d kg (for 95 kg soldier)", troopcapacity, troopcapacity*95)) + + -- Set value. + self.__.CargoBayWeightLimit = CargoWeight + elseif self:IsShip() then - local Desc = self:GetDesc() - self:F( { Desc = Desc } ) + -- Hard coded cargo weights in kg. 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! + ["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) + self.__.CargoBayWeightLimit = ( Weights[TypeName] or 50000 ) else - local Desc = self:GetDesc() + -- Hard coded number of soldiers. local Weights = { ["AAV7"] = 25, ["Bedford_MWD"] = 8, -- new by kappa @@ -1593,7 +1684,7 @@ do -- Cargo ["KrAZ6322"] = 12, ["M 818"] = 12, ["Tigr_233036"] = 6, - ["TPZ"] = 10, + ["TPZ"] = 10, -- Fuchs ["UAZ-469"] = 4, -- new by kappa ["Ural-375"] = 12, ["Ural-4320-31"] = 14, @@ -1607,12 +1698,28 @@ do -- Cargo ["HL_DSHK"] = 6, } - local CargoBayWeightLimit = (Weights[Desc.typeName] or 0) * 95 + -- Assuming that each passenger weighs 95 kg on average. + local CargoBayWeightLimit = ( Weights[TypeName] or 0 ) * 95 + self.__.CargoBayWeightLimit = CargoBayWeightLimit end end - self:F( { CargoBayWeightLimit = self.__.CargoBayWeightLimit } ) + + self:F({CargoBayWeightLimit = self.__.CargoBayWeightLimit}) end + + --- Get Cargo Bay Weight Limit in kg. + -- @param #POSITIONABLE self + -- @return #number Max cargo weight in kg. + function POSITIONABLE:GetCargoBayWeightLimit() + + if self.__.CargoBayWeightLimit==nil then + self:SetCargoBayWeightLimit() + end + + return self.__.CargoBayWeightLimit + end + end --- Cargo --- Signal a flare at the position of the POSITIONABLE. 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 5fd3abc3a..6195f03e5 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -89,9 +89,9 @@ -- -- @field #UNIT UNIT = { - ClassName="UNIT", - UnitName=nil, - GroupName=nil, + ClassName="UNIT", + UnitName=nil, + GroupName=nil, } From a73818a61579aeb6c84c1ec78427516c2977026c Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sat, 10 Sep 2022 12:08:30 +0200 Subject: [PATCH 4/4] #BUILD files --- Moose Development/Moose/Modules.lua | 4 ++++ Moose Setup/Moose.files | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index f56995e1b..c0ed4dd90 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -5,9 +5,12 @@ __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/Utilities/FiFo.lua' ) +__Moose.Include( 'Scripts/Moose/Utilities/Socket.lua' ) __Moose.Include( 'Scripts/Moose/Core/Base.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Astar.lua' ) __Moose.Include( 'Scripts/Moose/Core/Beacon.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Condition.lua' ) __Moose.Include( 'Scripts/Moose/Core/UserFlag.lua' ) __Moose.Include( 'Scripts/Moose/Core/Report.lua' ) __Moose.Include( 'Scripts/Moose/Core/Scheduler.lua' ) @@ -29,6 +32,7 @@ __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/MarkerOps_Base.lua' ) +__Moose.Include( 'Scripts/Moose/Core/TextAndSound.lua' ) __Moose.Include( 'Scripts/Moose/Wrapper/Object.lua' ) __Moose.Include( 'Scripts/Moose/Wrapper/Identifiable.lua' ) diff --git a/Moose Setup/Moose.files b/Moose Setup/Moose.files index 27d7addf7..9531ed658 100644 --- a/Moose Setup/Moose.files +++ b/Moose Setup/Moose.files @@ -6,6 +6,7 @@ Utilities/Profiler.lua Utilities/Templates.lua Utilities/STTS.lua Utilities/FiFo.lua +Utilities/Socket.lua Core/Base.lua Core/Beacon.lua @@ -30,6 +31,9 @@ Core/Timer.lua Core/Goal.lua Core/Spot.lua Core/MarkerOps_Base.lua +Core/Astar.lua +Core/Condition.lua +Core/TextAndSound.lua Wrapper/Object.lua Wrapper/Identifiable.lua