diff --git a/Moose Development/Moose/AI/AI_Formation.lua b/Moose Development/Moose/AI/AI_Formation.lua index 489e69cf3..c02096609 100644 --- a/Moose Development/Moose/AI/AI_Formation.lua +++ b/Moose Development/Moose/AI/AI_Formation.lua @@ -36,6 +36,7 @@ -- @field #boolean ReportTargets If true, nearby targets are reported. -- @Field DCSTypes#AI.Option.Air.val.ROE OptionROE Which ROE is set to the FollowGroup. -- @field DCSTypes#AI.Option.Air.val.REACTION_ON_THREAT OptionReactionOnThreat Which REACTION_ON_THREAT is set to the FollowGroup. +-- @field #number dtFollow Time step between position updates. --- Build large formations, make AI follow a @{Wrapper.Client#CLIENT} (player) leader or a @{Wrapper.Unit#UNIT} (AI) leader. @@ -106,6 +107,7 @@ AI_FORMATION = { FollowScheduler = nil, OptionROE = AI.Option.Air.val.ROE.OPEN_FIRE, OptionReactionOnThreat = AI.Option.Air.val.REACTION_ON_THREAT.ALLOW_ABORT_MISSION, + dtFollow = 0.5, } --- AI_FORMATION.Mode class @@ -125,6 +127,7 @@ AI_FORMATION = { -- @param Wrapper.Unit#UNIT FollowUnit The UNIT leading the FolllowGroupSet. -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. -- @param #string FollowName Name of the escort. +-- @param #string FollowBriefing Briefing. -- @return #AI_FORMATION self function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefing ) --R2.1 local self = BASE:Inherit( self, FSM_SET:New( FollowGroupSet ) ) @@ -139,7 +142,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin self:AddTransition( "*", "Stop", "Stopped" ) - self:AddTransition( "None", "Start", "Following" ) + self:AddTransition( {"None", "Stopped"}, "Start", "Following" ) self:AddTransition( "*", "FormationLine", "*" ) --- FormationLine Handler OnBefore for AI_FORMATION @@ -620,6 +623,16 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin return self end + +--- Set time interval between updates of the formation. +-- @param #AI_FORMATION self +-- @param #number dt Time step in seconds between formation updates. Default is every 0.5 seconds. +-- @return #AI_FORMATION +function AI_FORMATION:SetFollowTimeInterval(dt) --R2.1 + self.dtFollow=dt or 0.5 + return self +end + --- This function is for test, it will put on the frequency of the FollowScheduler a red smoke at the direction vector calculated for the escort to fly to. -- This allows to visualize where the escort is flying to. -- @param #AI_FORMATION self @@ -893,7 +906,30 @@ function AI_FORMATION:SetFlightRandomization( FlightRandomization ) --R2.1 end ---- @param Follow#AI_FORMATION self +--- Stop function. Formation will not be updated any more. +-- @param #AI_FORMATION self +-- @param Core.Set#SET_GROUP FollowGroupSet The following set of groups. +-- @param #string From From state. +-- @param #string Event Event. +-- @pram #string To The to state. +function AI_FORMATION:onafterStop(FollowGroupSet, From, Event, To) --R2.1 + self:E("Stopping formation.") +end + +--- Follow event fuction. Check if coming from state "stopped". If so the transition is rejected. +-- @param #AI_FORMATION self +-- @param Core.Set#SET_GROUP FollowGroupSet The following set of groups. +-- @param #string From From state. +-- @param #string Event Event. +-- @pram #string To The to state. +function AI_FORMATION:onbeforeFollow( FollowGroupSet, From, Event, To ) --R2.1 + if From=="Stopped" then + return false -- Deny transition. + end + return true +end + +--- @param #AI_FORMATION self function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1 self:F( ) @@ -1032,8 +1068,8 @@ function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1 end, self, ClientUnit, CT1, CV1, CT2, CV2 ) - - self:__Follow( -0.5 ) + + self:__Follow( -self.dtFollow ) end end diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index 528ac9a84..b5cd1ce26 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -342,18 +342,18 @@ do -- COORDINATE return x - Precision <= self.x and x + Precision >= self.x and z - Precision <= self.z and z + Precision >= self.z end - --- Returns if the 2 coordinates are at the same 2D position. + --- Scan/find objects (units, statics, scenery) within a certain radius around the coordinate using the world.searchObjects() DCS API function. -- @param #COORDINATE self -- @param #number radius (Optional) Scan radius in meters. Default 100 m. -- @param #boolean scanunits (Optional) If true scan for units. Default true. -- @param #boolean scanstatics (Optional) If true scan for static objects. Default true. -- @param #boolean scanscenery (Optional) If true scan for scenery objects. Default false. - -- @return True if units were found. - -- @return True if statics were found. - -- @return True if scenery objects were found. - -- @return Unit objects found. - -- @return Static objects found. - -- @return Scenery objects found. + -- @return #boolean True if units were found. + -- @return #boolean True if statics were found. + -- @return #boolean True if scenery objects were found. + -- @return #table Table of MOOSE @[#Wrapper.Unit#UNIT} objects found. + -- @return #table Table of DCS static objects found. + -- @return #table Table of DCS scenery objects found. function COORDINATE:ScanObjects(radius, scanunits, scanstatics, scanscenery) self:F(string.format("Scanning in radius %.1f m.", radius)) @@ -405,18 +405,17 @@ do -- COORDINATE local ObjectCategory = ZoneObject:getCategory() -- Check for unit or static objects - --if (ObjectCategory == Object.Category.UNIT and ZoneObject:isExist() and ZoneObject:isActive()) then - if (ObjectCategory == Object.Category.UNIT and ZoneObject:isExist()) then + if ObjectCategory==Object.Category.UNIT and ZoneObject:isExist() then table.insert(Units, UNIT:Find(ZoneObject)) gotunits=true - elseif (ObjectCategory == Object.Category.STATIC and ZoneObject:isExist()) then + elseif ObjectCategory==Object.Category.STATIC and ZoneObject:isExist() then table.insert(Statics, ZoneObject) gotstatics=true - elseif ObjectCategory == Object.Category.SCENERY then + elseif ObjectCategory==Object.Category.SCENERY then table.insert(Scenery, ZoneObject) gotscenery=true @@ -460,18 +459,47 @@ do -- COORDINATE --- Add a Distance in meters from the COORDINATE orthonormal plane, with the given angle, and calculate the new COORDINATE. -- @param #COORDINATE self -- @param DCS#Distance Distance The Distance to be added in meters. - -- @param DCS#Angle Angle The Angle in degrees. - -- @return #COORDINATE The new calculated COORDINATE. - function COORDINATE:Translate( Distance, Angle ) + -- @param DCS#Angle Angle The Angle in degrees. Defaults to 0 if not specified (nil). + -- @param #boolean Keepalt If true, keep altitude of original coordinate. Default is that the new coordinate is created at the translated land height. + -- @return Core.Point#COORDINATE The new calculated COORDINATE. + function COORDINATE:Translate( Distance, Angle, Keepalt ) local SX = self.x local SY = self.z - local Radians = Angle / 180 * math.pi + local Radians = (Angle or 0) / 180 * math.pi local TX = Distance * math.cos( Radians ) + SX local TY = Distance * math.sin( Radians ) + SY - - return COORDINATE:NewFromVec2( { x = TX, y = TY } ) + + if Keepalt then + return COORDINATE:NewFromVec3( { x = TX, y=self.y, z = TY } ) + else + return COORDINATE:NewFromVec2( { x = TX, y = TY } ) + end end + --- Rotate coordinate in 2D (x,z) space. + -- @param #COORDINATE self + -- @param DCS#Angle Angle Angle of rotation in degrees. + -- @return Core.Point#COORDINATE The rotated coordinate. + function COORDINATE:Rotate2D(Angle) + + if not Angle then + return self + end + + local phi=math.rad(Angle) + + local X=self.z + local Y=self.x + + --slocal R=math.sqrt(X*X+Y*Y) + + local x=X*math.cos(phi)-Y*math.sin(phi) + local y=X*math.sin(phi)+Y*math.cos(phi) + + -- Coordinate assignment looks bit strange but is correct. + return COORDINATE:NewFromVec3({x=y, y=self.y, z=x}) + end + --- Return a random Vec2 within an Outer Radius and optionally NOT within an Inner Radius of the COORDINATE. -- @param #COORDINATE self -- @param DCS#Distance OuterRadius @@ -1003,11 +1031,15 @@ do -- COORDINATE function COORDINATE:WaypointAir( AltType, Type, Action, Speed, SpeedLocked, airbase, DCSTasks, description ) self:F2( { AltType, Type, Action, Speed, SpeedLocked } ) - -- Defaults + -- Set alttype or "RADIO" which is AGL. AltType=AltType or "RADIO" + + -- Speedlocked by default if SpeedLocked==nil then SpeedLocked=true end + + -- Speed or default 500 km/h. Speed=Speed or 500 -- Waypoint array. @@ -1016,19 +1048,26 @@ do -- COORDINATE -- Coordinates. RoutePoint.x = self.x RoutePoint.y = self.z + -- Altitude. RoutePoint.alt = self.y RoutePoint.alt_type = AltType + -- Waypoint type. RoutePoint.type = Type or nil RoutePoint.action = Action or nil - -- Set speed/ETA. + + -- Speed. RoutePoint.speed = Speed/3.6 RoutePoint.speed_locked = SpeedLocked + + -- ETA. RoutePoint.ETA=nil - RoutePoint.ETA_locked = false + RoutePoint.ETA_locked = false + -- Waypoint description. RoutePoint.name=description + -- Airbase parameters for takeoff and landing points. if airbase then local AirbaseID = airbase:GetID() @@ -1037,31 +1076,24 @@ do -- COORDINATE RoutePoint.linkUnit = AirbaseID RoutePoint.helipadId = AirbaseID elseif AirbaseCategory == Airbase.Category.AIRDROME then - RoutePoint.airdromeId = AirbaseID + RoutePoint.airdromeId = AirbaseID else self:T("ERROR: Unknown airbase category in COORDINATE:WaypointAir()!") - end - end + end + + --self:MarkToAll(string.format("Landing waypoint at airbase %s", airbase:GetName())) + end - - -- ["task"] = - -- { - -- ["id"] = "ComboTask", - -- ["params"] = - -- { - -- ["tasks"] = - -- { - -- }, -- end of ["tasks"] - -- }, -- end of ["params"] - -- }, -- end of ["task"] - -- Waypoint tasks. RoutePoint.task = {} RoutePoint.task.id = "ComboTask" RoutePoint.task.params = {} RoutePoint.task.params.tasks = DCSTasks or {} + -- Debug. self:T({RoutePoint=RoutePoint}) + + -- Return waypoint. return RoutePoint end @@ -1121,6 +1153,9 @@ do -- COORDINATE --- Build a Waypoint Air "Landing". -- @param #COORDINATE self -- @param DCS#Speed Speed Airspeed in km/h. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase for takeoff and landing points. + -- @param #table DCSTasks A table of @{DCS#Task} items which are executed at the waypoint. + -- @param #string description A text description of the waypoint, which will be shown on the F10 map. -- @return #table The route point. -- @usage -- @@ -1129,8 +1164,8 @@ do -- COORDINATE -- LandingWaypoint = LandingCoord:WaypointAirLanding( 60 ) -- HeliGroup:Route( { LandWaypoint }, 1 ) -- Start landing the helicopter in one second. -- - function COORDINATE:WaypointAirLanding( Speed ) - return self:WaypointAir( nil, COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, Speed ) + function COORDINATE:WaypointAirLanding( Speed, airbase, DCSTasks, description ) + return self:WaypointAir(nil, COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, Speed, nil, airbase, DCSTasks, description) end diff --git a/Moose Development/Moose/Core/Radio.lua b/Moose Development/Moose/Core/Radio.lua index 954fc51a9..662b7b94f 100644 --- a/Moose Development/Moose/Core/Radio.lua +++ b/Moose Development/Moose/Core/Radio.lua @@ -9,12 +9,12 @@ -- -- The Radio contains 2 classes : RADIO and BEACON -- --- What are radio communications in DCS ? +-- 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 ? +-- 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, @@ -23,7 +23,7 @@ -- -- 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 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, @@ -33,7 +33,7 @@ -- -- === -- --- ### Author: Hugues "Grey_Echo" Bousquet +-- ### Authors: Hugues "Grey_Echo" Bousquet, funkyfranky -- -- @module Core.Radio -- @image Core_Radio.JPG @@ -66,24 +66,25 @@ -- * @{#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 ? +-- 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 transmiter, or if the antenna is **too weak**, the transmission will **fade** and **become noisyer**, +-- * 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 100W antenna, a standard AA TACAN has a 120W antenna, and civilian ATC's antenna usually range between 300 and 500W, +-- * 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. -- -- @type RADIO --- @field Positionable#POSITIONABLE Positionable The transmiter --- @field #string FileName Name of the sound file --- @field #number Frequency Frequency of the transmission in Hz --- @field #number Modulation Modulation of the transmission (either radio.modulation.AM or radio.modulation.FM) --- @field #string Subtitle Subtitle of the transmission --- @field #number SubtitleDuration Duration of the Subtitle in seconds --- @field #number Power Power of the antenna is Watts --- @field #boolean Loop (default true) +-- @field Wrapper.Controllable#CONTROLLABLE Positionable The @{#CONTROLLABLE} that will transmit the radio calls. +-- @field #string FileName Name of the sound file played. +-- @field #number Frequency Frequency of the transmission in Hz. +-- @field #number Modulation Modulation of the transmission (either radio.modulation.AM or radio.modulation.FM). +-- @field #string Subtitle Subtitle of the transmission. +-- @field #number SubtitleDuration Duration of the Subtitle in seconds. +-- @field #number Power Power of the antenna is Watts. +-- @field #boolean Loop Transmission is repeated (default true). +-- @field #string alias Name of the radio transmitter. -- @extends Core.Base#BASE RADIO = { ClassName = "RADIO", @@ -93,19 +94,19 @@ RADIO = { Subtitle = "", SubtitleDuration = 0, Power = 100, - Loop = true, + Loop = false, + alias=nil, } ---- Create a new RADIO Object. This doesn't broadcast a transmission, though, use @{#RADIO.Broadcast} to actually broadcast --- If you want to create a RADIO, you probably should use @{Wrapper.Positionable#POSITIONABLE.GetRadio}() instead +--- Create a new RADIO Object. This doesn't broadcast a transmission, though, use @{#RADIO.Broadcast} to actually broadcast. +-- If you want to create a RADIO, you probably should use @{Wrapper.Positionable#POSITIONABLE.GetRadio}() instead. -- @param #RADIO self -- @param Wrapper.Positionable#POSITIONABLE Positionable The @{Positionable} that will receive radio capabilities. --- @return #RADIO Radio --- @return #nil If Positionable is invalid +-- @return #RADIO The RADIO object or #nil if Positionable is invalid. function RADIO:New(Positionable) + + -- Inherit base local self = BASE:Inherit( self, BASE:New() ) -- Core.Radio#RADIO - - self.Loop = true -- default Loop to true (not sure the above RADIO definition actually is working) self:F(Positionable) if Positionable:GetPointVec2() then -- It's stupid, but the only way I found to make sure positionable is valid @@ -113,11 +114,27 @@ function RADIO:New(Positionable) return self end - self:E({"The passed positionable is invalid, no RADIO created", Positionable}) + self:E({error="The passed positionable is invalid, no RADIO created!", positionable=Positionable}) return nil end ---- Check validity of the filename passed and sets RADIO.FileName +--- Set alias of the transmitter. +-- @param #RADIO self +-- @param #string alias Name of the radio transmitter. +-- @return #RADIO self +function RADIO:SetAlias(alias) + self.alias=tostring(alias) + return self +end + +--- Get alias of the transmitter. +-- @param #RADIO self +-- @return #string Name of the transmitter. +function RADIO:GetAlias() + return tostring(self.alias) +end + +--- Set the file name for the radio transmission. -- @param #RADIO self -- @param #string FileName File name of the sound file (i.e. "Noise.ogg") -- @return #RADIO self @@ -125,49 +142,63 @@ 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 ?", self.FileName}) + self:E({"File name invalid. Maybe something wrong with the extension?", FileName}) return self end ---- Check validity of the frequency passed and sets RADIO.Frequency +--- Set the frequency for the radio transmission. +-- If the transmitting positionable is a unit or group, this also set the command "SetFrequency" with the defined frequency and modulation. -- @param #RADIO self --- @param #number Frequency in MHz (Ranges allowed for radio transmissions in DCS : 30-88 / 108-152 / 225-400MHz) +-- @param #number Frequency Frequency in MHz. Ranges allowed for radio transmissions in DCS : 30-88 / 108-152 / 225-400MHz. -- @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 < 88) or (Frequency >= 108 and Frequency < 152) or (Frequency >= 225 and Frequency < 400) then - self.Frequency = Frequency * 1000000 -- Conversion in Hz + + -- 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 - self.Positionable:SetCommand({ + + local commandSetFrequency={ id = "SetFrequency", params = { - frequency = self.Frequency, + frequency = self.Frequency, modulation = self.Modulation, } - }) + } + + self:T2(commandSetFrequency) + self.Positionable:SetCommand(commandSetFrequency) end + return self end end - self:E({"Frequency is outside of DCS Frequency ranges (30-80, 108-152, 225-400). Frequency unchanged.", self.Frequency}) + + self:E({"Frequency is outside of DCS Frequency ranges (30-80, 108-152, 225-400). Frequency unchanged.", Frequency}) return self end ---- Check validity of the frequency passed and sets RADIO.Modulation +--- Set AM or FM modulation of the radio transmitter. -- @param #RADIO self --- @param #number Modulation either radio.modulation.AM or radio.modulation.FM +-- @param #number Modulation Modulation is either radio.modulation.AM or radio.modulation.FM. -- @return #RADIO self function RADIO:SetModulation(Modulation) self:F2(Modulation) @@ -183,23 +214,24 @@ end --- Check validity of the power passed and sets RADIO.Power -- @param #RADIO self --- @param #number Power in W +-- @param #number Power Power in W. -- @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 - return self + else + self:E({"Power is invalid. Power unchanged.", self.Power}) end - self:E({"Power is invalid. Power unchanged.", self.Power}) + return self end ---- Check validity of the loop passed and sets RADIO.Loop +--- Set message looping on or off. -- @param #RADIO self --- @param #boolean Loop +-- @param #boolean Loop If true, message is repeated indefinitely. -- @return #RADIO self --- @usage function RADIO:SetLoop(Loop) self:F2(Loop) if type(Loop) == "boolean" then @@ -232,13 +264,12 @@ function RADIO:SetSubtitle(Subtitle, SubtitleDuration) self:E({"Subtitle is invalid. Subtitle reset.", self.Subtitle}) end if type(SubtitleDuration) == "number" then - if math.floor(math.abs(SubtitleDuration)) == SubtitleDuration then - self.SubtitleDuration = SubtitleDuration - return self - end + self.SubtitleDuration = SubtitleDuration + else + self.SubtitleDuration = 0 + self:E({"SubtitleDuration is invalid. SubtitleDuration reset.", self.SubtitleDuration}) end - self.SubtitleDuration = 0 - self:E({"SubtitleDuration is invalid. SubtitleDuration reset.", self.SubtitleDuration}) + return self end --- Create a new transmission, that is to say, populate the RADIO with relevant data @@ -246,10 +277,10 @@ end -- 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 --- @param #number Frequency in MHz --- @param #number Modulation either radio.modulation.AM or radio.modulation.FM --- @param #number Power in W +-- @param #string FileName Name of the sound file that will be transmitted. +-- @param #number Frequency Frequency in MHz. +-- @param #number Modulation Modulation of frequency, which is either radio.modulation.AM or radio.modulation.FM. +-- @param #number Power Power in W. -- @return #RADIO self function RADIO:NewGenericTransmission(FileName, Frequency, Modulation, Power, Loop) self:F({FileName, Frequency, Modulation, Power}) @@ -269,31 +300,43 @@ end -- but it will work for any @{Wrapper.Positionable#POSITIONABLE}. -- Only the RADIO and the Filename are mandatory. -- @param #RADIO self --- @param #string FileName --- @param #string Subtitle --- @param #number SubtitleDuration in s --- @param #number Frequency in MHz --- @param #number Modulation either radio.modulation.AM or radio.modulation.FM --- @param #boolean Loop +-- @param #string FileName Name of sound file. +-- @param #string Subtitle Subtitle to be displayed with sound file. +-- @param #number SubtitleDuration Duration of subtitle display in seconds. +-- @param #number Frequency Frequency in MHz. +-- @param #number Modulation Modulation which can be either radio.modulation.AM or radio.modulation.FM +-- @param #boolean Loop If true, loop message. -- @return #RADIO self function RADIO:NewUnitTransmission(FileName, Subtitle, SubtitleDuration, Frequency, Modulation, Loop) self:F({FileName, Subtitle, SubtitleDuration, Frequency, Modulation, Loop}) + -- Set file name. self:SetFileName(FileName) - local Duration = 5 - if SubtitleDuration then Duration = SubtitleDuration end - -- SubtitleDuration argument was missing, adding it - if Subtitle then self:SetSubtitle(Subtitle, Duration) end - -- self:SetSubtitleDuration is non existent, removing faulty line - -- if SubtitleDuration then self:SetSubtitleDuration(SubtitleDuration) end - if Frequency then self:SetFrequency(Frequency) end - if Modulation then self:SetModulation(Modulation) end - if Loop then self:SetLoop(Loop) end + + -- Set modulation AM/FM. + if Modulation then + self:SetModulation(Modulation) + end + + -- Set frequency. + if Frequency then + self:SetFrequency(Frequency) + end + + -- Set subtitle. + if Subtitle then + self:SetSubtitle(Subtitle, SubtitleDuration or 0) + end + + -- Set Looping. + if Loop then + self:SetLoop(Loop) + end return self end ---- Actually Broadcast the transmission +--- Broadcast the transmission. -- * The Radio has to be populated with the new transmission before broadcasting. -- * Please use RADIO setters or either @{#RADIO.NewGenericTransmission} or @{#RADIO.NewUnitTransmission} -- * This class is in fact pretty smart, it determines the right DCS function to use depending on the type of POSITIONABLE @@ -302,31 +345,38 @@ end -- * If your POSITIONABLE is a UNIT or a GROUP, the Power is ignored. -- * If your POSITIONABLE is not a UNIT or a GROUP, the Subtitle, SubtitleDuration are ignored -- @param #RADIO self +-- @param #boolean viatrigger Use trigger.action.radioTransmission() in any case, i.e. also for UNITS and GROUPS. -- @return #RADIO self -function RADIO:Broadcast() - self:F() +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" then - self:T2("Broadcasting from a UNIT or a GROUP") - self.Positionable:SetCommand({ + -- 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") + + local commandTransmitMessage={ id = "TransmitMessage", params = { file = self.FileName, duration = self.SubtitleDuration, subtitle = self.Subtitle, loop = self.Loop, - } - }) + }} + + self:T3(commandTransmitMessage) + self.Positionable:SetCommand(commandTransmitMessage) else -- If the POSITIONABLE is anything else, we revert to the general singleton function -- I need to give it a unique name, so that the transmission can be stopped later. I use the class ID - self:T2("Broadcasting from a POSITIONABLE") + 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 + + --- Stops a transmission -- This function is especially usefull to stop the broadcast of looped transmissions -- @param #RADIO self @@ -335,10 +385,10 @@ function RADIO:StopBroadcast() self:F() -- 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 - self.Positionable:SetCommand({ - id = "StopTransmission", - params = {} - }) + + local commandStopTransmission={id="StopTransmission", params={}} + + self.Positionable:SetCommand(commandStopTransmission) else -- Else, we use the appropriate singleton funciton trigger.action.stopRadioTransmission(tostring(self.ID)) @@ -364,22 +414,86 @@ end -- Use @{#BEACON:StopRadioBeacon}() to stop it. -- -- @type BEACON +-- @field #string ClassName Name of the class "BEACON". +-- @field Wrapper.Controllable#CONTROLLABLE Positionable The @{#CONTROLLABLE} that will receive radio capabilities. -- @extends Core.Base#BASE BEACON = { ClassName = "BEACON", + Positionable = nil, } ---- Create a new BEACON Object. This doesn't activate the beacon, though, use @{#BEACON.AATACAN} or @{#BEACON.Generic} +--- Beacon types supported by DCS. +-- @type BEACON.Type +-- @field #number NULL +-- @field #number VOR +-- @field #number DME +-- @field #number VOR_DME +-- @field #number TACAN +-- @field #number VORTAC +-- @field #number RSBN +-- @field #number BROADCAST_STATION +-- @field #number HOMER +-- @field #number AIRPORT_HOMER +-- @field #number AIRPORT_HOMER_WITH_MARKER +-- @field #number ILS_FAR_HOMER +-- @field #number ILS_NEAR_HOMER +-- @field #number ILS_LOCALIZER +-- @field #number ILS_GLIDESLOPE +-- @field #number NAUTICAL_HOMER +-- @field #number ICLS +BEACON.Type={ + NULL = 0, + VOR = 1, + DME = 2, + VOR_DME = 3, + TACAN = 4, + VORTAC = 5, + RSBN = 32, + BROADCAST_STATION = 1024, + HOMER = 8, + AIRPORT_HOMER = 4104, + AIRPORT_HOMER_WITH_MARKER = 4136, + ILS_FAR_HOMER = 16408, + ILS_NEAR_HOMER = 16456, + ILS_LOCALIZER = 16640, + ILS_GLIDESLOPE = 16896, + NAUTICAL_HOMER = 32776, + ICLS = 131584, +} + +--- Beacon systems supported by DCS. +-- @type BEACON.System +-- @field #number PAR_10 +-- @field #number RSBN_5 +-- @field #number TACAN +-- @field #number TACAN_TANKER +-- @field #number ILS_LOCALIZER +-- @field #number ILS_GLIDESLOPE +-- @field #number BROADCAST_STATION +BEACON.System={ + PAR_10 = 1, + RSBN_5 = 2, + TACAN = 3, + TACAN_TANKER = 4, + ILS_LOCALIZER = 5, + ILS_GLIDESLOPE = 6, + BROADCAST_STATION = 7, +} + +--- Create a new BEACON Object. This doesn't activate the beacon, though, use @{#BEACON.ActivateTACAN} etc. -- If you want to create a BEACON, you probably should use @{Wrapper.Positionable#POSITIONABLE.GetBeacon}() instead. -- @param #BEACON self -- @param Wrapper.Positionable#POSITIONABLE Positionable The @{Positionable} that will receive radio capabilities. --- @return #BEACON Beacon --- @return #nil If Positionable is invalid +-- @return #BEACON Beacon object or #nil if the positionable is invalid. function BEACON:New(Positionable) - local self = BASE:Inherit(self, BASE:New()) + + -- Inherit BASE. + local self=BASE:Inherit(self, BASE:New()) --#BEACON + -- Debug. self:F(Positionable) + -- Set positionable. if Positionable:GetPointVec2() then -- It's stupid, but the only way I found to make sure positionable is valid self.Positionable = Positionable return self @@ -390,44 +504,95 @@ function BEACON:New(Positionable) end ---- Converts a TACAN Channel/Mode couple into a frequency in Hz +--- Activates a TACAN BEACON. -- @param #BEACON self --- @param #number TACANChannel --- @param #string TACANMode --- @return #number Frequecy --- @return #nil if parameters are invalid -function BEACON:_TACANToFrequency(TACANChannel, TACANMode) - self:F3({TACANChannel, TACANMode}) - - if type(TACANChannel) ~= "number" then - if TACANMode ~= "X" and TACANMode ~= "Y" then - return nil -- error in arguments - end +-- @param #number Channel TACAN channel, i.e. the "10" part in "10Y". +-- @param #string Mode TACAN mode, i.e. the "Y" part in "10Y". +-- @param #string Message The Message that is going to be coded in Morse and broadcasted by the beacon. +-- @param #boolean Bearing If true, beacon provides bearing information. If false (or nil), only distance information is available. +-- @param #number Duration How long will the beacon last in seconds. Omit for forever. +-- @return #BEACON self +-- @usage +-- -- Let's create a TACAN Beacon for a tanker +-- local myUnit = UNIT:FindByName("MyUnit") +-- local myBeacon = myUnit:GetBeacon() -- Creates the beacon +-- +-- myBeacon:TACAN(20, "Y", "TEXACO", true) -- Activate the beacon +function BEACON:ActivateTACAN(Channel, Mode, Message, Bearing, Duration) + self:T({channel=Channel, mode=Mode, callsign=Message, bearing=Bearing, duration=Duration}) + + -- Get frequency. + local Frequency=UTILS.TACANToFrequency(Channel, Mode) + + -- Check. + if not Frequency then + self:E({"The passed TACAN channel is invalid, the BEACON is not emitting"}) + return self end --- This code is largely based on ED's code, in DCS World\Scripts\World\Radio\BeaconTypes.lua, line 137. --- I have no idea what it does but it seems to work - local A = 1151 -- 'X', channel >= 64 - local B = 64 -- channel >= 64 + -- Beacon type. + local Type=BEACON.Type.TACAN - if TACANChannel < 64 then - B = 1 - end + -- Beacon system. + local System=BEACON.System.TACAN - if TACANMode == 'Y' then - A = 1025 - if TACANChannel < 64 then - A = 1088 - end - else -- 'X' - if TACANChannel < 64 then - A = 962 + -- Check if unit is an aircraft and set system accordingly. + local AA=self.Positionable:IsAir() + if AA then + System=BEACON.System.TACAN_TANKER + -- Check if "Y" mode is selected for aircraft. + if Mode~="Y" then + self:E({"WARNING: The POSITIONABLE you want to attach the AA Tacan Beacon is an aircraft: Mode should Y !The BEACON is not emitting.", self.Positionable}) end end - return (A + TACANChannel - B) * 1000000 + -- Attached unit. + local UnitID=self.Positionable:GetID() + + -- Debug. + self:T({"TACAN BEACON started!"}) + + -- Start beacon. + self.Positionable:CommandActivateBeacon(Type, System, Frequency, UnitID, Channel, Mode, AA, Message, Bearing) + + -- Stop sheduler. + if Duration then + self.Positionable:DeactivateBeacon(Duration) + end + + return self end +--- Activates an ICLS BEACON. The unit the BEACON is attached to should be an aircraft carrier supporting this system. +-- @param #BEACON self +-- @param #number Channel ICLS channel. +-- @param #string Callsign The Message that is going to be coded in Morse and broadcasted by the beacon. +-- @param #number Duration How long will the beacon last in seconds. Omit for forever. +-- @return #BEACON self +function BEACON:ActivateICLS(Channel, Callsign, Duration) + self:F({Channel=Channel, Callsign=Callsign, Duration=Duration}) + + -- Attached unit. + local UnitID=self.Positionable:GetID() + + -- Debug + self:T2({"ICLS BEACON started!"}) + + -- Start beacon. + self.Positionable:CommandActivateICLS(Channel, UnitID, Callsign) + + -- Stop sheduler + if Duration then -- Schedule the stop of the BEACON if asked by the MD + self.Positionable:DeactivateBeacon(Duration) + end + + return self +end + + + + + --- Activates a TACAN BEACON on an Aircraft. -- @param #BEACON self @@ -480,7 +645,7 @@ function BEACON:AATACAN(TACANChannel, Message, Bearing, BeaconDuration) }) if BeaconDuration then -- Schedule the stop of the BEACON if asked by the MD - SCHEDULER:New( nil, + SCHEDULER:New(nil, function() self:StopAATACAN() end, {}, BeaconDuration) @@ -591,4 +756,44 @@ function BEACON:StopRadioBeacon() self:F() -- The unique name of the transmission is the class ID trigger.action.stopRadioTransmission(tostring(self.ID)) -end \ No newline at end of file + return self +end + +--- Converts a TACAN Channel/Mode couple into a frequency in Hz +-- @param #BEACON self +-- @param #number TACANChannel +-- @param #string TACANMode +-- @return #number Frequecy +-- @return #nil if parameters are invalid +function BEACON:_TACANToFrequency(TACANChannel, TACANMode) + self:F3({TACANChannel, TACANMode}) + + if type(TACANChannel) ~= "number" then + if TACANMode ~= "X" and TACANMode ~= "Y" then + return nil -- error in arguments + end + end + +-- This code is largely based on ED's code, in DCS World\Scripts\World\Radio\BeaconTypes.lua, line 137. +-- I have no idea what it does but it seems to work + local A = 1151 -- 'X', channel >= 64 + local B = 64 -- channel >= 64 + + if TACANChannel < 64 then + B = 1 + end + + if TACANMode == 'Y' then + A = 1025 + if TACANChannel < 64 then + A = 1088 + end + else -- 'X' + if TACANChannel < 64 then + A = 962 + end + end + + return (A + TACANChannel - B) * 1000000 +end + diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index 0864fb6c1..6acb294ab 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -409,9 +409,9 @@ do -- SET_BASE for ObjectID, ObjectData in pairs( self.Set ) do if NearestObject == nil then NearestObject = ObjectData - ClosestDistance = PointVec2:DistanceFromPointVec2( ObjectData:GetVec2() ) + ClosestDistance = PointVec2:DistanceFromPointVec2( ObjectData:GetCoordinate() ) else - local Distance = PointVec2:DistanceFromPointVec2( ObjectData:GetVec2() ) + local Distance = PointVec2:DistanceFromPointVec2( ObjectData:GetCoordinate() ) if Distance < ClosestDistance then NearestObject = ObjectData ClosestDistance = Distance diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 466e6a70f..0d4156260 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -2637,7 +2637,9 @@ function SPAWN:_OnEngineShutDown( EventData ) if Landed and self.RepeatOnEngineShutDown then local SpawnGroupIndex = self:GetSpawnIndexFromGroup( SpawnGroup ) self:T( { "EngineShutDown: ", "ReSpawn:", SpawnGroup:GetName(), SpawnGroupIndex } ) - self:ReSpawn( SpawnGroupIndex ) + --self:ReSpawn( SpawnGroupIndex ) + -- Delay respawn by three seconds due to DCS 2.5.4 OB bug https://github.com/FlightControl-Master/MOOSE/issues/1076 + SCHEDULER:New(self, self.ReSpawn, {SpawnGroupIndex}, 3) end end end diff --git a/Moose Development/Moose/Core/SpawnStatic.lua b/Moose Development/Moose/Core/SpawnStatic.lua index 0081195a5..e5a7b4e59 100644 --- a/Moose Development/Moose/Core/SpawnStatic.lua +++ b/Moose Development/Moose/Core/SpawnStatic.lua @@ -195,6 +195,49 @@ function SPAWNSTATIC:SpawnFromPointVec2( PointVec2, Heading, NewName ) --R2.1 end +--- Creates a new @{Static} from a COORDINATE. +-- @param #SPAWNSTATIC self +-- @param Core.Point#COORDINATE Coordinate The 3D coordinate where to spawn the static. +-- @param #number Heading (Optional) Heading The heading of the static, which is a number in degrees from 0 to 360. Default is 0 degrees. +-- @param #string NewName (Optional) The name of the new static. +-- @return #SPAWNSTATIC +function SPAWNSTATIC:SpawnFromCoordinate(Coordinate, Heading, NewName) --R2.4 + self:F( { PointVec2, Heading, NewName } ) + + local StaticTemplate, CoalitionID, CategoryID, CountryID = _DATABASE:GetStaticGroupTemplate( self.SpawnTemplatePrefix ) + + if StaticTemplate then + + Heading=Heading or 0 + + local StaticUnitTemplate = StaticTemplate.units[1] + + StaticUnitTemplate.x = Coordinate.x + StaticUnitTemplate.y = Coordinate.z + StaticUnitTemplate.alt = Coordinate.y + + StaticTemplate.route = nil + StaticTemplate.groupId = nil + + StaticTemplate.name = NewName or string.format("%s#%05d", self.SpawnTemplatePrefix, self.SpawnIndex ) + StaticUnitTemplate.name = StaticTemplate.name + StaticUnitTemplate.heading = ( Heading / 180 ) * math.pi + + _DATABASE:_RegisterStaticTemplate( StaticTemplate, CoalitionID, CategoryID, CountryID) + + self:F({StaticTemplate = StaticTemplate}) + + local Static = coalition.addStaticObject( self.CountryID or CountryID, StaticTemplate.units[1] ) + + self.SpawnIndex = self.SpawnIndex + 1 + + return _DATABASE:FindStatic(Static:getName()) + end + + return nil +end + + --- Respawns the original @{Static}. -- @param #SPAWNSTATIC self -- @return #SPAWNSTATIC diff --git a/Moose Development/Moose/Core/UserFlag.lua b/Moose Development/Moose/Core/UserFlag.lua index 88c1d0f60..bef5cefff 100644 --- a/Moose Development/Moose/Core/UserFlag.lua +++ b/Moose Development/Moose/Core/UserFlag.lua @@ -70,7 +70,7 @@ do -- UserFlag -- local BlueVictory = USERFLAG:New( "VictoryBlue" ) -- local BlueVictoryValue = BlueVictory:Get() -- Get the UserFlag VictoryBlue value. -- - function USERFLAG:Get( Number ) --R2.3 + function USERFLAG:Get() --R2.3 return trigger.misc.getUserFlag( self.UserFlagName ) end diff --git a/Moose Development/Moose/Core/UserSound.lua b/Moose Development/Moose/Core/UserSound.lua index a0547a5cf..b0f6fb393 100644 --- a/Moose Development/Moose/Core/UserSound.lua +++ b/Moose Development/Moose/Core/UserSound.lua @@ -118,15 +118,21 @@ do -- UserSound --- Play the usersound to the given @{Wrapper.Group}. -- @param #USERSOUND self -- @param Wrapper.Group#GROUP Group The @{Wrapper.Group} to play the usersound to. + -- @param #number Delay (Optional) Delay in seconds, before the sound is played. Default 0. -- @return #USERSOUND The usersound instance. -- @usage -- local BlueVictory = USERSOUND:New( "BlueVictory.ogg" ) -- local PlayerGroup = GROUP:FindByName( "PlayerGroup" ) -- Search for the active group named "PlayerGroup", that contains a human player. -- BlueVictory:ToGroup( PlayerGroup ) -- Play the sound that Blue has won to the player group. -- - function USERSOUND:ToGroup( Group ) --R2.3 - - trigger.action.outSoundForGroup( Group:GetID(), self.UserSoundFileName ) + function USERSOUND:ToGroup( Group, Delay ) --R2.3 + + Delay=Delay or 0 + if Delay>0 then + SCHEDULER:New(nil, USERSOUND.ToGroup,{self, Group}, Delay) + else + trigger.action.outSoundForGroup( Group:GetID(), self.UserSoundFileName ) + end return self end diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index 9b5b6827f..2c00f48f5 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -535,7 +535,7 @@ function ZONE_RADIUS:FlareZone( FlareColor, Points, Azimuth, AddHeight ) local Vec2 = self:GetVec2() AddHeight = AddHeight or 0 - + Points = Points and Points or 360 local Angle @@ -1398,16 +1398,15 @@ end --- Smokes the zone boundaries in a color. -- @param #ZONE_POLYGON_BASE self -- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. +-- @param #number Segments (Optional) Number of segments within boundary line. Default 10. -- @return #ZONE_POLYGON_BASE self -function ZONE_POLYGON_BASE:SmokeZone( SmokeColor ) +function ZONE_POLYGON_BASE:SmokeZone( SmokeColor, Segments ) self:F2( SmokeColor ) - local i - local j - local Segments = 10 + Segments=Segments or 10 - i = 1 - j = #self._.Polygon + local i=1 + local j=#self._.Polygon while i <= #self._.Polygon do self:T( { i, j, self._.Polygon[i], self._.Polygon[j] } ) @@ -1428,6 +1427,42 @@ function ZONE_POLYGON_BASE:SmokeZone( SmokeColor ) end +--- Flare the zone boundaries in a color. +-- @param #ZONE_POLYGON_BASE self +-- @param Utilities.Utils#FLARECOLOR FlareColor The flare color. +-- @param #number Segments (Optional) Number of segments within boundary line. Default 10. +-- @param DCS#Azimuth Azimuth (optional) Azimuth The azimuth of the flare. +-- @param #number AddHeight (optional) The height to be added for the smoke. +-- @return #ZONE_POLYGON_BASE self +function ZONE_POLYGON_BASE:FlareZone( FlareColor, Segments, Azimuth, AddHeight ) + self:F2(FlareColor) + + Segments=Segments or 10 + + AddHeight = AddHeight or 0 + + local i=1 + local j=#self._.Polygon + + while i <= #self._.Polygon do + self:T( { i, j, self._.Polygon[i], self._.Polygon[j] } ) + + local DeltaX = self._.Polygon[j].x - self._.Polygon[i].x + local DeltaY = self._.Polygon[j].y - self._.Polygon[i].y + + for Segment = 0, Segments do -- We divide each line in 5 segments and smoke a point on the line. + local PointX = self._.Polygon[i].x + ( Segment * DeltaX / Segments ) + local PointY = self._.Polygon[i].y + ( Segment * DeltaY / Segments ) + POINT_VEC2:New( PointX, PointY, AddHeight ):Flare(FlareColor, Azimuth) + end + j = i + i = i + 1 + end + + return self +end + + --- Returns if a location is within the zone. diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index 8509928fb..637560d03 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -216,7 +216,7 @@ -- One way to determin which types of ammo the unit carries, one can use the debug mode of the arty class via @{#ARTY.SetDebugON}(). -- In debug mode, the all ammo types of the group are printed to the monitor as message and can be found in the DCS.log file. -- --- ## Empoying Selected Weapons +-- ## Employing Selected Weapons -- -- If an ARTY group carries multiple weapons, which can be used for artillery task, a certain weapon type can be selected to attack the target. -- This is done via the *weapontype* parameter of the @{#ARTY.AssignTargetCoord}(..., *weapontype*, ...) function. @@ -674,11 +674,13 @@ ARTY.id="ARTY | " --- Arty script version. -- @field #string version -ARTY.version="1.0.6" +ARTY.version="1.0.7" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list: +-- TODO: Add hit event and make the arty group relocate. +-- TODO: Handle rearming for ships. How? -- DONE: Delete targets from queue user function. -- DONE: Delete entire target queue user function. -- DONE: Add weapon types. Done but needs improvements. @@ -697,11 +699,9 @@ ARTY.version="1.0.6" -- DONE: Add command move to make arty group move. -- DONE: remove schedulers for status event. -- DONE: Improve handling of special weapons. When winchester if using selected weapons? --- TODO: Handle rearming for ships. How? -- DONE: Make coordinate after rearming general, i.e. also work after the group has moved to anonther location. -- DONE: Add set commands via markers. E.g. set rearming place. -- DONE: Test stationary types like mortas ==> rearming etc. --- TODO: Add hit event and make the arty group relocate. -- DONE: Add illumination and smoke. --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2878,7 +2878,7 @@ function ARTY:onafterCeaseFire(Controllable, From, Event, To, target) self.Controllable:ClearTasks() else - self:E(ARTY.id.."ERROR: No target in cease fire for group %s.", self.groupname) + self:E(ARTY.id..string.format("ERROR: No target in cease fire for group %s.", self.groupname)) end -- Set number of shots to zero. @@ -4253,101 +4253,116 @@ end -- @param #ARTY self function ARTY:_CheckTargetsInRange() + local targets2delete={} + for i=1,#self.targets do local _target=self.targets[i] self:T3(ARTY.id..string.format("Before: Target %s - in range = %s", _target.name, tostring(_target.inrange))) -- Check if target is in range. - local _inrange,_toofar,_tooclose=self:_TargetInRange(_target) + local _inrange,_toofar,_tooclose,_remove=self:_TargetInRange(_target) self:T3(ARTY.id..string.format("Inbetw: Target %s - in range = %s, toofar = %s, tooclose = %s", _target.name, tostring(_target.inrange), tostring(_toofar), tostring(_tooclose))) - -- Init default for assigning moves into range. - local _movetowards=false - local _moveaway=false + if _remove then - if _target.inrange==nil then - - -- First time the check is performed. We call the function again and send a message. - _target.inrange,_toofar,_tooclose=self:_TargetInRange(_target, self.report or self.Debug) + -- The ARTY group is immobile and not cargo but the target is not in range! + table.insert(targets2delete, _target.name) - -- Send group towards/away from target. - if _toofar then - _movetowards=true - elseif _tooclose then - _moveaway=true - end + else - elseif _target.inrange==true then - - -- Target was in range at previous check... - - if _toofar then --...but is now too far away. - _movetowards=true - elseif _tooclose then --...but is now too close. - _moveaway=true - end - - elseif _target.inrange==false then - - -- Target was out of range at previous check. + -- Init default for assigning moves into range. + local _movetowards=false + local _moveaway=false - if _inrange then - -- Inform coalition that target is now in range. - local text=string.format("%s, target %s is now in range.", self.alias, _target.name) - self:T(ARTY.id..text) - MESSAGE:New(text,10):ToCoalitionIf(self.Controllable:GetCoalition(), self.report or self.Debug) - end - - end - - -- Assign a relocation command so that the unit will be in range of the requested target. - if self.autorelocate and (_movetowards or _moveaway) then - - -- Get current position. - local _from=self.Controllable:GetCoordinate() - local _dist=_from:Get2DDistance(_target.coord) + if _target.inrange==nil then - if _dist<=self.autorelocatemaxdist then - - local _tocoord --Core.Point#COORDINATE - local _name="" - local _safetymargin=500 - - if _movetowards then + -- First time the check is performed. We call the function again and send a message. + _target.inrange,_toofar,_tooclose=self:_TargetInRange(_target, self.report or self.Debug) - -- Target was in range on previous check but now we are too far away. - local _waytogo=_dist-self.maxrange+_safetymargin - local _heading=self:_GetHeading(_from,_target.coord) - _tocoord=_from:Translate(_waytogo, _heading) - _name=string.format("%s, relocation to within max firing range of target %s", self.alias, _target.name) - - elseif _moveaway then - - -- Target was in range on previous check but now we are too far away. - local _waytogo=_dist-self.minrange+_safetymargin - local _heading=self:_GetHeading(_target.coord,_from) - _tocoord=_from:Translate(_waytogo, _heading) - _name=string.format("%s, relocation to within min firing range of target %s", self.alias, _target.name) - + -- Send group towards/away from target. + if _toofar then + _movetowards=true + elseif _tooclose then + _moveaway=true end - - -- Send info message. - MESSAGE:New(_name.." assigned.", 10):ToCoalitionIf(self.Controllable:GetCoalition(), self.report or self.Debug) - - -- Assign relocation move. - self:AssignMoveCoord(_tocoord, nil, nil, self.autorelocateonroad, false, _name, true) + + elseif _target.inrange==true then + + -- Target was in range at previous check... + + if _toofar then --...but is now too far away. + _movetowards=true + elseif _tooclose then --...but is now too close. + _moveaway=true + end + + elseif _target.inrange==false then + + -- Target was out of range at previous check. + if _inrange then + -- Inform coalition that target is now in range. + local text=string.format("%s, target %s is now in range.", self.alias, _target.name) + self:T(ARTY.id..text) + MESSAGE:New(text,10):ToCoalitionIf(self.Controllable:GetCoalition(), self.report or self.Debug) + end + end + + -- Assign a relocation command so that the unit will be in range of the requested target. + if self.autorelocate and (_movetowards or _moveaway) then + + -- Get current position. + local _from=self.Controllable:GetCoordinate() + local _dist=_from:Get2DDistance(_target.coord) + + if _dist<=self.autorelocatemaxdist then + + local _tocoord --Core.Point#COORDINATE + local _name="" + local _safetymargin=500 + + if _movetowards then + + -- Target was in range on previous check but now we are too far away. + local _waytogo=_dist-self.maxrange+_safetymargin + local _heading=self:_GetHeading(_from,_target.coord) + _tocoord=_from:Translate(_waytogo, _heading) + _name=string.format("%s, relocation to within max firing range of target %s", self.alias, _target.name) + elseif _moveaway then + + -- Target was in range on previous check but now we are too far away. + local _waytogo=_dist-self.minrange+_safetymargin + local _heading=self:_GetHeading(_target.coord,_from) + _tocoord=_from:Translate(_waytogo, _heading) + _name=string.format("%s, relocation to within min firing range of target %s", self.alias, _target.name) + + end + + -- Send info message. + MESSAGE:New(_name.." assigned.", 10):ToCoalitionIf(self.Controllable:GetCoalition(), self.report or self.Debug) + + -- Assign relocation move. + self:AssignMoveCoord(_tocoord, nil, nil, self.autorelocateonroad, false, _name, true) + + end + + end + + -- Update value. + _target.inrange=_inrange + + self:T3(ARTY.id..string.format("After: Target %s - in range = %s", _target.name, tostring(_target.inrange))) end - - -- Update value. - _target.inrange=_inrange - - self:T3(ARTY.id..string.format("After: Target %s - in range = %s", _target.name, tostring(_target.inrange))) - end + + -- Remove targets not in range. + for _,targetname in pairs(targets2delete) do + self:RemoveTarget(targetname) + end + end --- Check all normal (untimed) targets and return the target with the highest priority which has been engaged the fewest times. @@ -4728,6 +4743,7 @@ end -- @return #boolean True if target is in range, false otherwise. -- @return #boolean True if ARTY group is too far away from the target, i.e. distance > max firing range. -- @return #boolean True if ARTY group is too close to the target, i.e. distance < min finring range. +-- @return #boolean True if target should be removed since ARTY group is immobile and not cargo. function ARTY:_TargetInRange(target, message) self:F3(target) @@ -4763,11 +4779,13 @@ function ARTY:_TargetInRange(target, message) end -- Remove target if ARTY group cannot move, e.g. Mortas. No chance to be ever in range - unless they are cargo. + local _remove=false if not (self.ismobile or self.iscargo) and _inrange==false then - self:RemoveTarget(target.name) + --self:RemoveTarget(target.name) + _remove=true end - return _inrange,_toofar,_tooclose + return _inrange,_toofar,_tooclose,_remove end --- Get the weapon type name, which should be used to attack the target. diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index 7402729ab..fd3029b5a 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -1012,7 +1012,7 @@ do -- DETECTION_BASE --- Set the parameters to calculate to optimal intercept point. -- @param #DETECTION_BASE self -- @param #boolean Intercept Intercept is true if an intercept point is calculated. Intercept is false if it is disabled. The default Intercept is false. - -- @param #number IntereptDelay If Intercept is true, then InterceptDelay is the average time it takes to get airplanes airborne. + -- @param #number InterceptDelay If Intercept is true, then InterceptDelay is the average time it takes to get airplanes airborne. -- @return #DETECTION_BASE self function DETECTION_BASE:SetIntercept( Intercept, InterceptDelay ) self:F2() diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 5b1616c81..9a1c9306b 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -5435,7 +5435,7 @@ function RAT:_ATCInit(airports_map) if not RAT.ATC.init then local text text="Starting RAT ATC.\nSimultanious = "..RAT.ATC.Nclearance.."\n".."Delay = "..RAT.ATC.delay - self:T(RAT.id..text) + BASE:T(RAT.id..text) RAT.ATC.init=true for _,ap in pairs(airports_map) do local name=ap:GetName() @@ -5458,7 +5458,7 @@ end -- @param #string name Group name of the flight. -- @param #string dest Name of the destination airport. function RAT:_ATCAddFlight(name, dest) - self:T(string.format("%sATC %s: Adding flight %s with destination %s.", RAT.id, dest, name, dest)) + BASE:T(string.format("%sATC %s: Adding flight %s with destination %s.", RAT.id, dest, name, dest)) RAT.ATC.flight[name]={} RAT.ATC.flight[name].destination=dest RAT.ATC.flight[name].Tarrive=-1 @@ -5483,7 +5483,7 @@ end -- @param #string name Group name of the flight. -- @param #number time Time the fight first registered. function RAT:_ATCRegisterFlight(name, time) - self:T(RAT.id.."Flight ".. name.." registered at ATC for landing clearance.") + BASE:T(RAT.id.."Flight ".. name.." registered at ATC for landing clearance.") RAT.ATC.flight[name].Tarrive=time RAT.ATC.flight[name].holding=0 end @@ -5514,7 +5514,7 @@ function RAT:_ATCStatus() -- Aircraft is holding. local text=string.format("ATC %s: Flight %s is holding for %i:%02d. %s.", dest, name, hold/60, hold%60, busy) - self:T(RAT.id..text) + BASE:T(RAT.id..text) elseif hold==RAT.ATC.onfinal then @@ -5522,7 +5522,7 @@ function RAT:_ATCStatus() 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) - self:T(RAT.id..text) + BASE:T(RAT.id..text) elseif hold==RAT.ATC.unregistered then @@ -5530,7 +5530,7 @@ function RAT:_ATCStatus() --self:T(string.format("ATC %s: Flight %s is not registered yet (hold %d).", dest, name, hold)) else - self:E(RAT.id.."ERROR: Unknown holding time in RAT:_ATCStatus().") + BASE:E(RAT.id.."ERROR: Unknown holding time in RAT:_ATCStatus().") end end @@ -5572,12 +5572,12 @@ function RAT:_ATCCheck() -- 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) - self:T(RAT.id..text) + 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) - self:T(RAT.id..text) + BASE:T(RAT.id..text) -- Clear flight for landing. RAT:_ATCClearForLanding(name, flight) @@ -5705,12 +5705,7 @@ function RAT:_ATCQueue() for k,v in ipairs(_queue) do table.insert(RAT.ATC.airport[airport].queue, v[1]) end - - --fvh - --for k,v in ipairs(RAT.ATC.airport[airport].queue) do - --print(string.format("queue #%02i flight \"%s\" holding %d seconds",k, v, RAT.ATC.flight[v].holding)) - --end - + end end diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index 95b800521..394f134f2 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -276,7 +276,7 @@ RANGE.id="RANGE | " --- Range script version. -- @field #string version -RANGE.version="1.2.1" +RANGE.version="1.2.3" --TODO list: --TODO: Add custom weapons, which can be specified by the user. @@ -460,9 +460,10 @@ function RANGE:SetBombtrackThreshold(distance) self.BombtrackThreshold=distance*1000 or 25*1000 end ---- Set range location. If this is not done, one (random) unit position of the range is used to determine the center of the range. +--- Set range location. If this is not done, one (random) unit position of the range is used to determine the location of the range. +-- The range location determines the position at which the weather data is evaluated. -- @param #RANGE self --- @param Core.Point#COORDINATE coordinate Coordinate of the center of the range. +-- @param Core.Point#COORDINATE coordinate Coordinate of the range. function RANGE:SetRangeLocation(coordinate) self.location=coordinate end @@ -471,7 +472,7 @@ end -- If a zone is not explicitly specified, the range zone is determined by its location and radius. -- @param #RANGE self -- @param Core.Zone#ZONE zone MOOSE zone defining the range perimeters. -function RANGE:SetRangeLocation(zone) +function RANGE:SetRangeZone(zone) self.rangezone=zone end @@ -1163,11 +1164,19 @@ function RANGE:OnEventShot(EventData) -- Coordinate of impact point. local impactcoord=COORDINATE:NewFromVec3(_lastBombPos) + -- Check if impact happend in range zone. + local insidezone=self.rangezone:IsCoordinateInZone(impactcoord) + -- Distance from range. We dont want to smoke targets outside of the range. local impactdist=impactcoord:Get2DDistance(self.location) + -- Impact point of bomb. + if self.Debug then + impactcoord:MarkToAll("Bomb impact point") + end + -- Smoke impact point of bomb. - if self.PlayerSettings[_playername].smokebombimpact and impactdistB) has been -- added two times while Path2(A->B) was added only once. Hence, the group will choose Path1 with a probability of 66.6 % while Path2 is only chosen with --- a probability of 33.3 %. --- +-- a probability of 33.3 %. +-- -- ## Rail Connections --- +-- -- A rail connection is automatically defined as the closest point on a railway measured from the center of the spawn zone. But only, if the distance is less than 3 km. --- +-- -- The mission designer can manually specify a rail connection with the @{#WAREHOUSE.SetRailConnection} function. --- +-- -- **NOTE** however, that trains in DCS are currently not implemented in a way so that they can be used. --- +-- -- ## Air Connections --- +-- -- In order to use airborne assets, a warehouse needs to have an associated airbase. This can be an airdrome, a FARP/HELOPAD or a ship. --- +-- -- If there is an airbase within 3 km range of the warehouse it is automatically set as the associated airbase. A user can set an airbase manually -- with the @{#WAREHOUSE.SetAirbase} function. Keep in mind that sometimes ground units need to walk/drive from the spawn zone to the airport -- to get to their transport carriers. --- +-- -- ## Naval Connections --- +-- -- Natively, DCS does not have the concept of a port/habour or shipping lanes. So in order to have a meaningful transfer of naval units between warehouses, these have to be -- defined by the mission designer. --- +-- -- ### Defining a Port --- +-- -- A port in this context is the zone where all naval assets are spawned. This zone can be defined with the function @{#WAREHOUSE.SetPortZone}(*zone*), where the parameter -- *zone* is a MOOSE zone. So again, this can be create from a trigger zone defined in the mission editor or if a general shape is desired by a @{Core.Zone#ZONE_POLYGON}. --- +-- -- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_PortZone.png) --- +-- -- ### Defining Shipping Lanes --- +-- -- A shipping lane between to warehouses can be defined by the @{#WAREHOUSE.AddShippingLane}(*remotewarehouse*, *group*, *oneway*) function. The first parameter *remotewarehouse* -- is the warehouse which should be connected to the present warehouse. --- +-- -- The parameter *group* should be a late activated group defined in the mission editor. The waypoints of this group are used as waypoints of the shipping lane. --- +-- -- By default, the reverse lane is automatically added to the remote warehouse. This can be disabled by setting the *oneway* parameter to *true*. --- +-- -- Similar to off road connections, you can also define multiple shipping lanes between two warehouse ports. If there are multiple lanes defined, one is chosen randomly. -- It is possible to add the same lane multiple times. By this you can influence the probability of the chosen lane. For example Lane_1(A->B) has been -- added two times while Lane_2(A->B) was added only once. Therefore, the ships will choose Lane_1 with a probability of 66.6 % while Path_2 is only chosen with --- a probability of 33.3 %. --- --- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_ShippingLane.png) --- +-- a probability of 33.3 %. +-- +-- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_ShippingLane.png) +-- -- === -- -- # Why is my request not processed? @@ -525,14 +527,14 @@ -- For each request, the warehouse class logic does a lot of consistancy and validation checks under the hood. -- This helps to circumvent a lot of DCS issues and shortcomings. For example, it is checked that enough free -- parking spots at an airport are available *before* the assets are spawned. --- However, this also means that sometimes a request is deemed to be *invalid* in which case they are deleted +-- However, this also means that sometimes a request is deemed to be *invalid* in which case they are deleted -- from the queue or considered to be valid but cannot be executed at this very moment. --- +-- -- ## Invalid Requests --- --- Invalid request are requests which can **never** be processes because there is some logical or physical argument against it. +-- +-- Invalid request are requests which can **never** be processes because there is some logical or physical argument against it. -- (Or simply because that feature was not implemented (yet).) --- +-- -- * All airborne assets need an associated airbase of any kind on the sending *and* receiving warhouse. -- * Airplanes need an airdrome at the sending and receiving warehouses. -- * Not enough parking spots of the right terminal type at the sending warehouse. This avoids planes spawning on runways or on top of each other. @@ -545,112 +547,113 @@ -- * If transport by airplane, both warehouses must have and airdrome. -- * If transport by APC, both warehouses must have a road connection. -- * If transport by helicopter, the sending airbase must have an associated airbase (airdrome or FARP). --- +-- -- All invalid requests are cancelled and **removed** from the warehouse queue! --- +-- -- ## Temporarily Unprocessable Requests --- +-- -- Temporarily unprocessable requests are possible in priciple, but cannot be processed at the given time the warehouse checks its queue. --- +-- -- * No enough parking spaces are available for all requested assets but the airbase has enough parking spots in total so that this request is possible once other aircraft have taken off. -- * The requesting warehouse is not in state "Running" (could be paused, not yet started or under attack). -- * Not enough cargo assets available at this moment. -- * Not enough free parking spots for all cargo or transport airborne assets at the moment. -- * Not enough transport assets to carry all cargo assets. --- +-- -- Temporarily unprocessable requests are held in the queue. If at some point in time, the situation changes so that these requests can be processed, they are executed. --- +-- -- ## Cargo Bay and Weight Limitations --- +-- -- The transporation of cargo is handled by the AI\_Dispatcher classes. These take the cargo bay of a carrier and the weight of -- the cargo into account so that a carrier can only load a realistic amount of cargo. --- +-- -- However, if troops are supposed to be transported between warehouses, there is one important limitations one has to keep in mind. -- This is that **cargo asset groups cannot be split** and devided into separate carrier units! --- +-- -- For example, a TPz Fuchs has a cargo bay large enough to carry up to 10 soldiers at once, which is a realistic number. -- If a group consisting of more than ten soldiers needs to be transported, it cannot be loaded into the APC. --- Even if two APCs are available, which could in principle carry up to 20 soldiers, a group of, let's say 12 soldiers will not --- be split into a group of ten soldiers using the first APC and a group two soldiers using the second APC. --- +-- Even if two APCs are available, which could in principle carry up to 20 soldiers, a group of, let's say 12 soldiers will not +-- be split into a group of ten soldiers using the first APC and a group two soldiers using the second APC. +-- -- In other words, **there must be at least one carrier unit available that has a cargo bay large enough to load the heaviest cargo group!** -- The warehouse logic will automatically search all available transport assets for a large enough carrier. -- But if none is available, the request will be queued until a suitable carrier becomes available. --- +-- -- The only realistic solution in this case is to either provide a transport carrier with a larger cargo bay or to reduce the number of soldiers -- in the group. --- +-- -- A better way would be to have two groups of max. 10 soldiers each and one TPz Fuchs for transport. In this case, the first group is -- loaded and transported to the receiving warehouse. Once this is done, the carrier will drive back and pick up the remaining -- group. --- +-- -- As an artificial workaround one can manually set the cargo bay size to a larger value or alternatively reduce the weight of the cargo -- when adding the assets via the @{#WAREHOUSE.AddAsset} function. This might even be unavoidable if, for example, a SAM group -- should be transported since SAM sites only work when all units are in the same group. --- +-- -- ## Processing Speed --- +-- -- A warehouse has a limited speed to process requests. Each time the status of the warehouse is updated only one requests is processed. -- The time interval between status updates is 30 seconds by default and can be adjusted via the @{#WAREHOUSE.SetStatusUpdate}(*interval*) function. -- However, the status is also updated on other occasions, e.g. when a new request was added. --- +-- -- === --- +-- -- # Strategic Considerations --- +-- -- Due to the fact that a warehouse holds (or can hold) a lot of valuable assets, it makes a (potentially) juicy target for enemy attacks. -- There are several interesting situations, which can occurr. --- +-- -- ## Capturing a Warehouses Airbase --- +-- -- If a warehouse has an associated airbase, it can be captured by the enemy. In this case, the warehouse looses its ability so employ all airborne assets and is also cut-off -- from supply by airplanes. Supply of ground troops via helicopters is still possible, because they deliver the troops into the spawn zone. --- --- Technically, the capturing of the airbase is triggered by the DCS [S\_EVENT\_BASE\_CAPTURED](https://wiki.hoggitworld.com/view/DCS_event_base_captured) event. +-- +-- Technically, the capturing of the airbase is triggered by the DCS [S\_EVENT\_BASE\_CAPTURED](https://wiki.hoggitworld.com/view/DCS_event_base_captured) event. -- So the capturing takes place when only enemy ground units are in the airbase zone whilst no ground units of the present airbase owner are in that zone. --- +-- -- The warehouse will also create an event **AirbaseCaptured**, which can be captured by the @{#WAREHOUSE.OnAfterAirbaseCaptured} function. So the warehouse chief can react on -- this attack and for example deploy ground groups to re-capture its airbase. --- +-- -- When an airbase is re-captured the event **AirbaseRecaptured** is triggered and can be captured by the @{#WAREHOUSE.OnAfterAirbaseRecaptured} function. -- This can be used to put the defending assets back into the warehouse stock. --- +-- -- ## Capturing the Warehouse --- +-- -- A warehouse can be captured by the enemy coalition. If enemy ground troops enter the warehouse zone the event **Attacked** is triggered which can be captured by the -- @{#WAREHOUSE.OnAfterAttacked} event. By default the warehouse zone circular zone with a radius of 500 meters located at the center of the physical warehouse. --- The warehouse zone can be set via the @{#WAREHOUSE.SetWarehouseZone}(*zone*) function. The parameter *zone* must also be a cirular zone. --- +-- The warehouse zone can be set via the @{#WAREHOUSE.SetWarehouseZone}(*zone*) function. The parameter *zone* must also be a cirular zone. +-- -- The @{#WAREHOUSE.OnAfterAttacked} function can be used by the mission designer to react to the enemy attack. For example by deploying some or all ground troops -- currently in stock to defend the warehouse. Note that the warehouse also has a self defence option which can be enabled by the @{#WAREHOUSE.SetAutoDefenceOn}() -- function. In this case, the warehouse will automatically spawn all ground troops. If the spawn zone is further away from the warehouse zone, all mobile troops --- are routed to the warehouse zone. --- +-- are routed to the warehouse zone. The self request which is triggered on an automatic defence has the assignment "AutoDefence". So you can use this to +-- give orders to the groups that were spawned using the @{#WAREHOUSE.OnAfterSelfRequest} function. +-- -- If only ground troops of the enemy coalition are present in the warehouse zone, the warehouse and all its assets falls into the hands of the enemy. -- In this case the event **Captured** is triggered which can be captured by the @{#WAREHOUSE.OnAfterCaptured} function. --- +-- -- The warehouse turns to the capturing coalition, i.e. its physical representation, and all assets as well. In paticular, all requests to the warehouse will -- spawn assets beloning to the new owner. --- --- If the enemy troops could be defeated, i.e. no more troops of the opposite coalition are in the warehouse zone, the event **Defeated** is triggered and +-- +-- If the enemy troops could be defeated, i.e. no more troops of the opposite coalition are in the warehouse zone, the event **Defeated** is triggered and -- the @{#WAREHOUSE.OnAfterDefeated} function can be used to adapt to the new situation. For example putting back all spawned defender troops back into -- the warehouse stock. Note that if the automatic defence is enabled, all defenders are automatically put back into the warehouse on the **Defeated** event. --- +-- -- ## Destroying a Warehouse --- +-- -- If an enemy destroy the physical warehouse structure, the warehouse will of course stop all its services. In priciple, all assets contained in the warehouse are -- gone as well. So a warehouse should be properly defended. --- +-- -- Upon destruction of the warehouse, the event **Destroyed** is triggered, which can be captured by the @{#WAREHOUSE.OnAfterDestroyed} function. -- So the mission designer can intervene at this point and for example choose to spawn all or paricular types of assets before the warehouse is gone for good. -- -- === --- +-- -- # Hook in and Take Control --- +-- -- The Finite State Machine implementation allows mission designers to hook into important events and add their own code. -- Most of these events have already been mentioned but here is the list at a glance: --- +-- -- * "NotReadyYet" --> "Start" --> "Running" (Starting the warehouse) -- * "*" --> "Status" --> "*" (status updated in regular intervals) -- * "*" --> "AddAsset" --> "*" (adding a new asset to the warehouse stock) @@ -672,23 +675,23 @@ -- * "Running" --> "Pause" --> "Paused" (warehouse is paused) -- * "Paused" --> "Unpause" --> "Running" (warehouse is unpaused) -- * "*" --> "Stop" --> "Stopped" (warehouse is stopped) --- +-- -- The transitions are of the general form "From State" --> "Event" --> "To State". The "*" star denotes that the transition is possible from *any* state. -- Some transitions, however, are only allowed from certain "From States". For example, no requests can be processed if the warehouse is in "Paused" or "Destroyed" or "Stopped" state. -- -- Mission designers can capture the events with OnAfterEvent functions, e.g. @{#WAREHOUSE.OnAfterDelivered} or @{#WAREHOUSE.OnAfterAirbaseCaptured}. --- +-- -- === --- +-- -- # Persistence of Assets --- +-- -- Assets in stock of a warehouse can be saved to a file on your hard drive and then loaded from that file at a later point. This enables to restart the mission -- and restore the warehouse stock. --- +-- -- ## Prerequisites --- +-- -- **Important** By default, DCS does not allow for writing data to files. Therefore, one first has to comment out the line "sanitizeModule('io')", i.e. --- +-- -- do -- sanitizeModule('os') -- --sanitizeModule('io') @@ -698,62 +701,62 @@ -- end -- -- in the file "MissionScripting.lua", which is located in the subdirectory "Scripts" of your DCS installation root directory. --- +-- -- ### Don't! --- +-- -- Do not use **semi-colons** or **equal signs** in the group names of your assets as these are used as separators in the saved and loaded files texts. -- If you do, it will cause problems and give you a headache! --- +-- -- ## Save Assets --- +-- -- Saving asset data to file is achieved by the @{WAREHOUSE.Save}(*path*, *filename*) function. The parameter *path* specifies the path on the file system where the -- warehouse data is saved. If you do not specify a path, the file is saved your the DCS installation root directory. -- The parameter *filename* is optional and defines the name of the saved file. By default this is automatically created from the warehouse id and name, for example -- "Warehouse-1234_Batumi.txt". --- +-- -- warehouseBatumi:Save("D:\\My Warehouse Data\\") --- +-- -- This will save all asset data to in "D:\\My Warehouse Data\\Warehouse-1234_Batumi.txt". --- +-- -- ### Automatic Save at Mission End --- +-- -- The assets can be saved automatically when the mission is ended via the @{WAREHOUSE.SetSaveOnMissionEnd}(*path*, *filename*) function, i.e. --- +-- -- warehouseBatumi:SetSaveOnMissionEnd("D:\\My Warehouse Data\\") --- +-- -- ## Load Assets --- +-- -- Loading assets data from file is achieved by the @{WAREHOUSE.Load}(*path*, *filename*) function. The parameter *path* specifies the path on the file system where the -- warehouse data is loaded from. If you do not specify a path, the file is loaded from your the DCS installation root directory. -- The parameter *filename* is optional and defines the name of the file to load. By default this is automatically generated from the warehouse id and name, for example -- "Warehouse-1234_Batumi.txt". --- +-- -- Note that the warehouse **must not be started** and in the *Running* state in order to load the assets. In other words, loading should happen after the -- @{#WAREHOUSE.New} command is specified in the code but before the @{#WAREHOUSE.Start} command is given. --- +-- -- Loading the assets is done by --- +-- -- warehouseBatumi:New(STATIC:FindByName("Warehouse Batumi")) -- warehouseBatumi:Load("D:\\My Warehouse Data\\") -- warehouseBatumi:Start() --- +-- -- This sequence loads all assets from file. If a warehouse was captured in the last mission, it also respawns the static warehouse structure with the right coaliton. -- However, it due to DCS limitations it is not possible to set the airbase coalition. This has to be done manually in the mission editor. Or alternatively, one could -- spawn some ground units via a self request and let them capture the airbase. --- +-- -- === -- -- # Examples --- +-- -- This section shows some examples how the WAREHOUSE class is used in practice. This is one of the best ways to explain things, in my opinion. --- +-- -- But first, let me introduce a convenient way to define several warehouses in a table. This is absolutely *not necessary* but quite handy if you have -- multiple WAREHOUSE objects in your mission. --- +-- -- ## Example 0: Setting up a Warehouse Array --- +-- -- If you have multiple warehouses, you can put them in a table. This makes it easier to access them or to loop over them. --- +-- -- -- Define Warehouses. -- local warehouse={} -- -- Blue warehouses @@ -771,118 +774,118 @@ -- warehouse.Sochi = WAREHOUSE:New(STATIC:FindByName("Warehouse Sochi"), "Sochi") --Functional.Warehouse#WAREHOUSE -- -- Remarks: --- +-- -- * I defined the array as local, i.e. local warehouse={}. This is personal preference and sometimes causes trouble with the lua garbage collection. You can also define it as a global array/table! -- * The "--Functional.Warehouse#WAREHOUSE" at the end is only to have the LDT intellisense working correctly. If you don't use LDT (which you should!), it can be omitted. -- -- **NOTE** that all examples below need this bit or code at the beginning - or at least the warehouses which are used. --- +-- -- The example mission is based on the same template mission, which has defined a lot of airborne, ground and naval assets as templates. Only few of those are used here. --- +-- -- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Assets.png) --- +-- -- ## Example 1: Self Request --- +-- -- Ground troops are taken from the Batumi warehouse stock and spawned in its spawn zone. After a short delay, they are added back to the warehouse stock. -- Also a new request is made. Hence, the groups will be spawned, added back to the warehouse, spawned again and so on and so forth... --- +-- -- -- Start warehouse Batumi. -- warehouse.Batumi:Start() --- +-- -- -- Add five groups of infantry as assets. -- warehouse.Batumi:AddAsset(GROUP:FindByName("Infantry Platoon Alpha"), 5) --- +-- -- -- Add self request for three infantry at Batumi. -- warehouse.Batumi:AddRequest(warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 3) --- --- +-- +-- -- --- Self request event. Triggered once the assets are spawned in the spawn zone or at the airbase. -- function warehouse.Batumi:OnAfterSelfRequest(From, Event, To, groupset, request) -- local mygroupset=groupset --Core.Set#SET_GROUP --- +-- -- -- Loop over all groups spawned from that request. -- for _,group in pairs(mygroupset:GetSetObjects()) do -- local group=group --Wrapper.Group#GROUP --- +-- -- -- Gree smoke on spawned group. -- group:SmokeGreen() --- +-- -- -- Put asset back to stock after 10 seconds. --- warehouse.Batumi:__AddAsset(10, group) +-- warehouse.Batumi:__AddAsset(10, group) -- end --- +-- -- -- Add new self request after 20 seconds. -- warehouse.Batumi:__AddRequest(20, warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 3) --- +-- -- end -- -- ## Example 2: Self propelled Ground Troops --- +-- -- Warehouse Berlin, which is a FARP near Batumi, requests infantry and troop transports from the warehouse at Batumi. -- The groups are spawned at Batumi and move by themselfs from Batumi to Berlin using the roads. -- Once the troops have arrived at Berlin, the troops are automatically added to the warehouse stock of Berlin. -- While on the road, Batumi has requested back two APCs from Berlin. Since Berlin does not have the assets in stock, -- the request is queued. After the troops have arrived, Berlin is sending back the APCs to Batumi. --- +-- -- -- Start Warehouse at Batumi. -- warehouse.Batumi:Start() --- +-- -- -- Add 20 infantry groups and ten APCs as assets at Batumi. -- warehouse.Batumi:AddAsset("Infantry Platoon Alpha", 20) -- warehouse.Batumi:AddAsset("TPz Fuchs", 10) --- --- -- Start Warehouse Berlin. +-- +-- -- Start Warehouse Berlin. -- warehouse.Berlin:Start() --- +-- -- -- Warehouse Berlin requests 10 infantry groups and 5 APCs from warehouse Batumi. -- warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 10) -- warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_APC, 5) --- +-- -- -- Request from Batumi for 2 APCs. Initially these are not in stock. When they become available, the request is executed. --- warehouse.Berlin:AddRequest(warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_APC, 2) +-- warehouse.Berlin:AddRequest(warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_APC, 2) -- -- ## Example 3: Self Propelled Airborne Assets --- +-- -- Warehouse Senaki receives a high priority request from Kutaisi for one Yak-52s. At the same time, Kobuleti requests half of -- all available Yak-52s. Request from Kutaisi is first executed and then Kobuleti gets half of the remaining assets. -- Additionally, London requests one third of all available UH-1H Hueys from Senaki. --- Once the units have arrived they are added to the stock of the receiving warehouses and can be used for further assignments. --- +-- Once the units have arrived they are added to the stock of the receiving warehouses and can be used for further assignments. +-- -- -- Start warehouses -- warehouse.Senaki:Start() -- warehouse.Kutaisi:Start() -- warehouse.Kobuleti:Start() -- warehouse.London:Start() --- +-- -- -- Add assets to Senaki warehouse. -- warehouse.Senaki:AddAsset("Yak-52", 10) -- warehouse.Senaki:AddAsset("Huey", 6) --- +-- -- -- Kusaisi requests 3 Yak-52 form Senaki while Kobuleti wants all the rest. -- warehouse.Senaki:AddRequest(warehouse.Kutaisi, WAREHOUSE.Descriptor.GROUPNAME, "Yak-52", 1, nil, nil, 10) -- warehouse.Senaki:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.GROUPNAME, "Yak-52", WAREHOUSE.Quantity.HALF, nil, nil, 70) --- +-- -- -- FARP London wants 1/3 of the six available Hueys. -- warehouse.Senaki:AddRequest(warehouse.London, WAREHOUSE.Descriptor.GROUPNAME, "Huey", WAREHOUSE.Quantity.THIRD) -- -- ## Example 4: Transport of Assets by APCs --- +-- -- Warehouse at FARP Berlin requests five infantry groups from Batumi. These assets shall be transported using two APC groups. --- Infantry and APC are spawned in the spawn zone at Batumi. The APCs have a cargo bay large enough to pick up four of the +-- Infantry and APC are spawned in the spawn zone at Batumi. The APCs have a cargo bay large enough to pick up four of the -- five infantry groups in the first run and will bring them to Berlin. There, they unboard and walk to the warehouse where they will be added to the stock. -- Meanwhile the APCs go back to Batumi and one will pick up the last remaining soldiers. --- Once the APCs have completed their mission, they return to Batumi and are added back to stock. --- +-- Once the APCs have completed their mission, they return to Batumi and are added back to stock. +-- -- -- Start Warehouse at Batumi. -- warehouse.Batumi:Start() --- --- -- Start Warehouse Berlin. +-- +-- -- Start Warehouse Berlin. -- warehouse.Berlin:Start() --- +-- -- -- Add 20 infantry groups and five APCs as assets at Batumi. -- warehouse.Batumi:AddAsset("Infantry Platoon Alpha", 20) -- warehouse.Batumi:AddAsset("TPz Fuchs", 5) --- +-- -- -- Warehouse Berlin requests 5 infantry groups from warehouse Batumi using 2 APCs for transport. -- warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 5, WAREHOUSE.TransportType.APC, 2) -- @@ -898,13 +901,13 @@ -- -- Start Warehouses. -- warehouse.Batumi:Start() -- warehouse.Berlin:Start() --- +-- -- -- Add 20 infantry groups as assets at Batumi. -- warehouse.Batumi:AddAsset("Infantry Platoon Alpha", 20) --- +-- -- -- Add five Hueys for transport. Note that a Huey in DCS is an attack and not a transport helo. So we force this attribute! -- warehouse.Batumi:AddAsset("Huey", 5, WAREHOUSE.Attribute.AIR_TRANSPORTHELO) --- +-- -- -- Warehouse Berlin requests 5 infantry groups from warehouse Batumi using all available helos for transport. -- warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 5, WAREHOUSE.TransportType.HELICOPTER, WAREHOUSE.Quantity.ALL) -- @@ -917,53 +920,53 @@ -- -- Start warehouses. -- warehouse.Batumi:Start() -- warehouse.Kobuleti:Start() --- +-- -- -- Add assets to Batumi warehouse. -- warehouse.Batumi:AddAsset("C-130", 1) -- warehouse.Batumi:AddAsset("TPz Fuchs", 3) --- +-- -- warehouse.Batumi:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_APC, WAREHOUSE.Quantity.ALL, WAREHOUSE.TransportType.AIRPLANE) -- -- ## Example 7: Capturing Airbase and Warehouse --- +-- -- A red BMP has made it through our defence lines and drives towards our unprotected airbase at Senaki. --- Once the BMP captures the airbase (DCS [S\_EVENT\_BASE\_CAPTURED](https://wiki.hoggitworld.com/view/DCS_event_base_captured) is evaluated) +-- Once the BMP captures the airbase (DCS [S\_EVENT\_BASE\_CAPTURED](https://wiki.hoggitworld.com/view/DCS_event_base_captured) is evaluated) -- the warehouse at Senaki lost its air infrastructure and it is not possible any more to spawn airborne units. All requests for airborne units are rejected and cancelled in this case. --- +-- -- The red BMP then drives further to the warehouse. Once it enters the warehouse zone (500 m radius around the warehouse building), the warehouse is -- considered to be under attack. This triggers the event **Attacked**. The @{#WAREHOUSE.OnAfterAttacked} function can be used to react to this situation. -- Here, we only broadcast a distress call and launch a flare. However, it would also be reasonable to spawn all or selected ground troops in order to defend -- the warehouse. Note, that the warehouse has a self defence option which can be activated via the @{#WAREHOUSE.SetAutoDefenceOn}() function. If activated, -- *all* ground assets are automatically spawned and assigned to defend the warehouse. Once/if the attack is defeated, these assets go automatically back -- into the warehouse stock. --- --- If the red coalition manages to capture our warehouse, all assets go into their possession. Now red tries to steal three F/A-18 flights and send them to +-- +-- If the red coalition manages to capture our warehouse, all assets go into their possession. Now red tries to steal three F/A-18 flights and send them to -- Sukhumi. These aircraft will be spawned and begin to taxi. However, ... --- --- A blue Bradley is in the area and will attemt to recapture the warehouse. It might also catch the red F/A-18s before they take off. --- --- -- Start warehouses. +-- +-- A blue Bradley is in the area and will attemt to recapture the warehouse. It might also catch the red F/A-18s before they take off. +-- +-- -- Start warehouses. -- warehouse.Senaki:Start() -- warehouse.Sukhumi:Start() --- +-- -- -- Add some assets. -- warehouse.Senaki:AddAsset("TPz Fuchs", 5) -- warehouse.Senaki:AddAsset("Infantry Platoon Alpha", 10) -- warehouse.Senaki:AddAsset("F/A-18C 2ship", 10) --- +-- -- -- Enable auto defence, i.e. spawn all group troups into the spawn zone. -- --warehouse.Senaki:SetAutoDefenceOn() --- +-- -- -- Activate Red BMP trying to capture the airfield and the warehouse. -- local red1=GROUP:FindByName("Red BMP-80 Senaki"):Activate() --- +-- -- -- The red BMP first drives to the airbase which gets captured and changes from blue to red. --- -- This triggers the "AirbaseCaptured" event where you can hook in and do things. +-- -- This triggers the "AirbaseCaptured" event where you can hook in and do things. -- function warehouse.Senaki:OnAfterAirbaseCaptured(From, Event, To, Coalition) -- -- This request cannot be processed since the warehouse has lost its airbase. In fact it is deleted from the queue. -- warehouse.Senaki:AddRequest(warehouse.Senaki,WAREHOUSE.Descriptor.CATEGORY, Group.Category.AIRPLANE, 1) -- end --- +-- -- -- Now the red BMP also captures the warehouse. This triggers the "Captured" event where you can hook in. -- -- So now the warehouse and the airbase are both red and aircraft can be spawned again. -- function warehouse.Senaki:OnAfterCaptured(From, Event, To, Coalition, Country) @@ -976,63 +979,63 @@ -- elseif Coalition==coalition.side.BLUE then -- warehouse.Senaki.warehouse:SmokeBlue() -- end --- +-- -- -- Activate a blue vehicle to re-capture the warehouse. It will drive to the warehouse zone and kill the red intruder. -- local blue1=GROUP:FindByName("blue1"):Activate() -- end -- -- ## Example 8: Destroying a Warehouse --- +-- -- FARP Berlin requests a Huey from Batumi warehouse. This helo is deployed and will be delivered. -- After 30 seconds into the mission we create and (artificial) big explosion - or a terrorist attack if you like - which completely destroys the -- the warehouse at Batumi. All assets are gone and requests cannot be processed anymore. --- +-- -- -- Start Batumi and Berlin warehouses. -- warehouse.Batumi:Start() -- warehouse.Berlin:Start() --- +-- -- -- Add some assets. -- warehouse.Batumi:AddAsset("Huey", 5, WAREHOUSE.Attribute.AIR_TRANSPORTHELO) -- warehouse.Berlin:AddAsset("Huey", 5, WAREHOUSE.Attribute.AIR_TRANSPORTHELO) --- +-- -- -- Big explosion at the warehose. It has a very nice damage model by the way :) -- local function DestroyWarehouse() -- warehouse.Batumi:GetCoordinate():Explosion(999) -- end -- SCHEDULER:New(nil, DestroyWarehouse, {}, 30) --- +-- -- -- First request is okay since warehouse is still alive. -- warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.AIR_TRANSPORTHELO, 1) --- --- -- These requests should both not be processed any more since the warehouse at Batumi is destroyed. +-- +-- -- These requests should both not be processed any more since the warehouse at Batumi is destroyed. -- warehouse.Batumi:__AddRequest(35, warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.AIR_TRANSPORTHELO, 1) -- warehouse.Berlin:__AddRequest(40, warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.AIR_TRANSPORTHELO, 1) -- -- ## Example 9: Self Propelled Naval Assets --- +-- -- Kobuleti requests all naval assets from Batumi. -- However, before naval assets can be exchanged, both warehouses need a port and at least one shipping lane defined by the user. -- See the @{#WAREHOUSE.SetPortZone}() and @{#WAREHOUSE.AddShippingLane}() functions. -- We do not want to spawn them all at once, because this will probably be a disaster -- in the port zone. Therefore, each ship is spawned with a delay of five minutes. --- +-- -- Batumi has quite a selection of different ships (for testing). --- +-- -- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Naval_Assets.png) --- +-- -- -- Start warehouses. -- warehouse.Batumi:Start() -- warehouse.Kobuleti:Start() --- +-- -- -- Define ports. These are polygon zones created by the waypoints of late activated units. -- warehouse.Batumi:SetPortZone(ZONE_POLYGON:NewFromGroupName("Warehouse Batumi Port Zone", "Warehouse Batumi Port Zone")) -- warehouse.Kobuleti:SetPortZone(ZONE_POLYGON:NewFromGroupName("Warehouse Kobuleti Port Zone", "Warehouse Kobuleti Port Zone")) --- +-- -- -- Shipping lane. Again, the waypoints of late activated units are taken as points defining the shipping lane. -- -- Some units will take lane 1 while others will take lane two. But both lead from Batumi to Kobuleti port. -- warehouse.Batumi:AddShippingLane(warehouse.Kobuleti, GROUP:FindByName("Warehouse Batumi-Kobuleti Shipping Lane 1")) -- warehouse.Batumi:AddShippingLane(warehouse.Kobuleti, GROUP:FindByName("Warehouse Batumi-Kobuleti Shipping Lane 2")) --- +-- -- -- Large selection of available naval units in DCS. -- warehouse.Batumi:AddAsset("Speedboat") -- warehouse.Batumi:AddAsset("Perry") @@ -1055,11 +1058,11 @@ -- warehouse.Batumi:AddAsset("Ivanov") -- warehouse.Batumi:AddAsset("Yantai") -- warehouse.Batumi:AddAsset("Type 052C") --- warehouse.Batumi:AddAsset("Guangzhou") --- +-- warehouse.Batumi:AddAsset("Guangzhou") +-- -- -- Get Number of ships at Batumi. -- local nships=warehouse.Batumi:GetNumberOfAssets(WAREHOUSE.Descriptor.CATEGORY, Group.Category.SHIP) --- +-- -- -- Send one ship every 3 minutes (ships do not evade each other well, so we need a bit space between them). -- for i=1, nships do -- warehouse.Batumi:__AddRequest(180*(i-1)+10, warehouse.Kobuleti, WAREHOUSE.Descriptor.CATEGORY, Group.Category.SHIP, 1) @@ -1067,129 +1070,129 @@ -- -- ## Example 10: Warehouse on Aircraft Carrier -- --- This example shows how to spawn assets from a warehouse located on an aircraft carrier. The warehouse must still be represented by a --- physical static object. However, on a carrier space is limit so we take a smaller static. In priciple one could also take something +-- This example shows how to spawn assets from a warehouse located on an aircraft carrier. The warehouse must still be represented by a +-- physical static object. However, on a carrier space is limit so we take a smaller static. In priciple one could also take something -- like a windsock. --- +-- -- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Carrier.png) --- +-- -- USS Stennis requests F/A-18s from Batumi. At the same time Kobuleti requests F/A-18s from the Stennis which currently does not have any. -- So first, Batumi delivers the fighters to the Stennis. After they arrived they are deployed again and send to Kobuleti. --- +-- -- -- Start warehouses. --- warehouse.Batumi:Start() +-- warehouse.Batumi:Start() -- warehouse.Stennis:Start() -- warehouse.Kobuleti:Start() --- +-- -- -- Add F/A-18 2-ship flight to Batmi. -- warehouse.Batumi:AddAsset("F/A-18C 2ship", 1) --- +-- -- -- USS Stennis requests F/A-18 from Batumi. -- warehouse.Batumi:AddRequest(warehouse.Stennis, WAREHOUSE.Descriptor.GROUPNAME, "F/A-18C 2ship") --- +-- -- -- Kobuleti requests F/A-18 from USS Stennis. -- warehouse.Stennis:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.GROUPNAME, "F/A-18C 2ship") -- -- ## Example 11: Aircraft Carrier - Rescue Helo and Escort --- +-- -- After 10 seconds we make a self request for a rescue helicopter. Note, that the @{#WAREHOUSE.AddRequest} function has a parameter which lets you -- specify an "Assignment". This can be later used to identify the request and take the right actions. --- +-- -- Once the request is processed, the @{#WAREHOUSE.OnAfterSelfRequest} function is called. This is where we hook in and postprocess the spawned assets. -- In particular, we use the @{AI.AI_Formation#AI_FORMATION} class to make some nice escorts for our carrier. --- +-- -- When the resue helo is spawned, we can check that this is the correct asset and make the helo go into formation with the carrier. -- Once the helo runs out of fuel, it will automatically return to the ship and land. For the warehouse, this means that the "cargo", i.e. the helicopter -- has been delivered - assets can be delivered to other warehouses and to the same warehouse - hence a *self* request. -- When that happens, the **Delivered** event is triggered and the @{#WAREHOUSE.OnAfterDelivered} function called. This can now be used to spawn -- a fresh helo. Effectively, there we created an infinite, never ending loop. So a rescue helo will be up at all times. --- +-- -- After 30 and 45 seconds requests for five groups of armed speedboats are made. These will be spawned in the port zone right behind the carrier. -- The first five groups will go port of the carrier an form a left wing formation. The seconds groups will to the analogue on the starboard side. -- **Note** that in order to spawn naval assets a warehouse needs a port (zone). Since the carrier and hence the warehouse is mobile, we define a moving -- zone as @{Core.Zone#ZONE_UNIT} with the carrier as reference unit. The "port" of the Stennis at its stern so all naval assets are spawned behing the carrier. --- +-- -- -- Start warehouse on USS Stennis. -- warehouse.Stennis:Start() --- +-- -- -- Aircraft carrier gets a moving zone right behind it as port. -- warehouse.Stennis:SetPortZone(ZONE_UNIT:New("Warehouse Stennis Port Zone", UNIT:FindByName("USS Stennis"), 100, {rho=250, theta=180, relative_to_unit=true})) --- +-- -- -- Add speedboat assets. -- warehouse.Stennis:AddAsset("Speedboat", 10) -- warehouse.Stennis:AddAsset("CH-53E", 1) --- +-- -- -- Self request of speed boats. -- warehouse.Stennis:__AddRequest(10, warehouse.Stennis, WAREHOUSE.Descriptor.GROUPNAME, "CH-53E", 1, nil, nil, nil, "Rescue Helo") -- warehouse.Stennis:__AddRequest(30, warehouse.Stennis, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.NAVAL_ARMEDSHIP, 5, nil, nil, nil, "Speedboats Left") -- warehouse.Stennis:__AddRequest(45, warehouse.Stennis, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.NAVAL_ARMEDSHIP, 5, nil, nil, nil, "Speedboats Right") --- +-- -- --- Function called after self request --- function warehouse.Stennis:OnAfterSelfRequest(From, Event, To,_groupset, request) +-- function warehouse.Stennis:OnAfterSelfRequest(From, Event, To,_groupset, request) -- local groupset=_groupset --Core.Set#SET_GROUP -- local request=request --Functional.Warehouse#WAREHOUSE.Pendingitem --- +-- -- -- USS Stennis is the mother ship. -- local Mother=UNIT:FindByName("USS Stennis") --- +-- -- -- Get assignment of the request. -- local assignment=warehouse.Stennis:GetAssignment(request) --- +-- -- if assignment=="Speedboats Left" then --- +-- -- -- Define AI Formation object. -- -- Note that this has to be a global variable or the garbage collector will remove it for some reason! -- CarrierFormationLeft = AI_FORMATION:New(Mother, groupset, "Left Formation with Carrier", "Escort Carrier.") --- +-- -- -- Formation parameters. --- CarrierFormationLeft:FormationLeftWing(200 ,50, 0, 0, 500, 50) +-- CarrierFormationLeft:FormationLeftWing(200 ,50, 0, 0, 500, 50) -- CarrierFormationLeft:__Start(2) --- +-- -- for _,group in pairs(groupset:GetSetObjects()) do -- local group=group --Wrapper.Group#GROUP --- group:FlareRed() --- end --- +-- group:FlareRed() +-- end +-- -- elseif assignment=="Speedboats Right" then --- +-- -- -- Define AI Formation object. -- -- Note that this has to be a global variable or the garbage collector will remove it for some reason! -- CarrierFormationRight = AI_FORMATION:New(Mother, groupset, "Right Formation with Carrier", "Escort Carrier.") --- +-- -- -- Formation parameters. --- CarrierFormationRight:FormationRightWing(200 ,50, 0, 0, 500, 50) +-- CarrierFormationRight:FormationRightWing(200 ,50, 0, 0, 500, 50) -- CarrierFormationRight:__Start(2) --- +-- -- for _,group in pairs(groupset:GetSetObjects()) do -- local group=group --Wrapper.Group#GROUP --- group:FlareGreen() --- end --- +-- group:FlareGreen() +-- end +-- -- elseif assignment=="Rescue Helo" then --- +-- -- -- Start uncontrolled helo. -- local group=groupset:GetFirst() --Wrapper.Group#GROUP -- group:StartUncontrolled() --- +-- -- -- Define AI Formation object. -- CarrierFormationHelo = AI_FORMATION:New(Mother, groupset, "Helo Formation with Carrier", "Fly Formation.") --- +-- -- -- Formation parameters. -- CarrierFormationHelo:FormationCenterWing(-150, 50, 20, 50, 100, 50) -- CarrierFormationHelo:__Start(2) --- +-- -- end --- +-- -- --- When the helo is out of fuel, it will return to the carrier and should be delivered. -- function warehouse.Stennis:OnAfterDelivered(From,Event,To,request) -- local request=request --Functional.Warehouse#WAREHOUSE.Pendingitem --- +-- -- -- So we start another request. -- if request.assignment=="Rescue Helo" then -- warehouse.Stennis:__AddRequest(10, warehouse.Stennis, WAREHOUSE.Descriptor.GROUPNAME, "CH-53E", 1, nil, nil, nil, "Rescue Helo") -- end -- end --- +-- -- end -- -- ## Example 12: Pause a Warehouse @@ -1208,72 +1211,72 @@ -- -- -- Start Warehouse at Batumi. -- warehouse.Batumi:Start() --- --- -- Start Warehouse Berlin. +-- +-- -- Start Warehouse Berlin. -- warehouse.Berlin:Start() --- +-- -- -- Add 20 infantry groups and 5 tank platoons as assets at Batumi. -- warehouse.Batumi:AddAsset("Infantry Platoon Alpha", 20) --- +-- -- -- Pause the warehouse after 10 seconds -- warehouse.Batumi:__Pause(10) --- +-- -- -- Add a request from Berlin after 15 seconds. A request can be added but not be processed while warehouse is paused. -- warehouse.Batumi:__AddRequest(15, warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 1) --- +-- -- -- New asset added after 20 seconds. This is possible even if the warehouse is paused. -- warehouse.Batumi:__AddAsset(20, "Abrams", 5) --- +-- -- -- Unpause warehouse after 30 seconds. Now the request from Berlin can be processed. -- warehouse.Batumi:__Unpause(30) --- +-- -- -- Pause warehouse Berlin -- warehouse.Berlin:__Pause(60) --- +-- -- -- After 90 seconds request from Berlin for tanks. -- warehouse.Batumi:__AddRequest(90, warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_TANK, 1) --- +-- -- -- After 120 seconds unpause Berlin. -- warehouse.Berlin:__Unpause(120) -- -- ## Example 13: Battlefield Air Interdiction --- +-- -- This example show how to couple the WAREHOUSE class with the @{AI.AI_Bai} class. --- Four enemy targets have been located at the famous Kobuleti X. All three available Viggen 2-ship flights are assigned to kill at least one of the BMPs to complete their mission. +-- Four enemy targets have been located at the famous Kobuleti X. All three available Viggen 2-ship flights are assigned to kill at least one of the BMPs to complete their mission. -- -- -- Start Warehouse at Kobuleti. -- warehouse.Kobuleti:Start() --- +-- -- -- Add three 2-ship groups of Viggens. -- warehouse.Kobuleti:AddAsset("Viggen 2ship", 3) --- +-- -- -- Self request for all Viggen assets. -- warehouse.Kobuleti:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.GROUPNAME, "Viggen 2ship", WAREHOUSE.Quantity.ALL, nil, nil, nil, "BAI") --- +-- -- -- Red targets at Kobuleti X (late activated). -- local RedTargets=GROUP:FindByName("Red IVF Alpha") --- +-- -- -- Activate the targets. -- RedTargets:Activate() --- +-- -- -- Do something with the spawned aircraft. -- function warehouse.Kobuleti:OnAfterSelfRequest(From,Event,To,groupset,request) -- local groupset=groupset --Core.Set#SET_GROUP -- local request=request --Functional.Warehouse#WAREHOUSE.Pendingitem --- +-- -- if request.assignment=="BAI" then --- +-- -- for _,group in pairs(groupset:GetSetObjects()) do -- local group=group --Wrapper.Group#GROUP --- +-- -- -- Start uncontrolled aircraft. -- group:StartUncontrolled() --- +-- -- local BAI=AI_BAI_ZONE:New(ZONE:New("Patrol Zone Kobuleti"), 500, 1000, 500, 600, ZONE:New("Patrol Zone Kobuleti")) --- +-- -- -- Tell the program to use the object (in this case called BAIPlane) as the group to use in the BAI function -- BAI:SetControllable(group) --- +-- -- -- Function checking if targets are still alive -- local function CheckTargets() -- local nTargets=RedTargets:GetSize() @@ -1285,76 +1288,76 @@ -- else -- MESSAGE:New("BAI Mission: The required red targets are destroyed.", 30):ToAll() -- BAI:__Accomplish(1) -- Now they should fly back to the patrolzone and patrol. --- end +-- end -- end --- +-- -- -- Start scheduler to monitor number of targets. -- local Check, CheckScheduleID = SCHEDULER:New(nil, CheckTargets, {}, 60, 60) --- +-- -- -- When the targets in the zone are destroyed, (see scheduled function), the planes will return home ... -- function BAI:OnAfterAccomplish( Controllable, From, Event, To ) -- MESSAGE:New( "BAI Mission: Sending the Viggens back to base.", 30):ToAll() -- Check:Stop(CheckScheduleID) -- BAI:__RTB(1) -- end --- +-- -- -- Start BAI -- BAI:Start() --- +-- -- -- Engage after 5 minutes. -- BAI:__Engage(300) --- +-- -- -- RTB after 30 min max. -- BAI:__RTB(-30*60) --- +-- -- end -- end --- +-- -- end -- -- ## Example 14: Strategic Bombing --- +-- -- This example shows how to employ stategic bombers in a mission. Three B-52s are lauched at Kobuleti with the assignment to wipe out the enemy warehouse at Sukhumi. -- The bombers will get a flight path and make their approach from the South at an altitude of 5000 m ASL. After their bombing run, they will return to Kobuleti and -- added back to stock. --- +-- -- -- Start warehouses --- warehouse.Kobuleti:Start() +-- warehouse.Kobuleti:Start() -- warehouse.Sukhumi:Start() --- +-- -- -- Add a strategic bomber assets -- warehouse.Kobuleti:AddAsset("B-52H", 3) --- --- -- Request bombers for specific task of bombing Sukhumi warehouse. +-- +-- -- Request bombers for specific task of bombing Sukhumi warehouse. -- warehouse.Kobuleti:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.AIR_BOMBER, WAREHOUSE.Quantity.ALL, nil, nil, nil, "Bomb Sukhumi") --- --- -- Specify assignment after bombers have been spawned. +-- +-- -- Specify assignment after bombers have been spawned. -- function warehouse.Kobuleti:OnAfterSelfRequest(From, Event, To, groupset, request) -- local groupset=groupset --Core.Set#SET_GROUP --- +-- -- -- Get assignment of this request. -- local assignment=warehouse.Kobuleti:GetAssignment(request) --- +-- -- if assignment=="Bomb Sukhumi" then --- +-- -- for _,_group in pairs(groupset:GetSet()) do -- local group=_group --Wrapper.Group#GROUP --- +-- -- -- Start uncontrolled aircraft. -- group:StartUncontrolled() --- +-- -- -- Target coordinate! -- local ToCoord=warehouse.Sukhumi:GetCoordinate():SetAltitude(5000) --- +-- -- -- Home coordinate. -- local HomeCoord=warehouse.Kobuleti:GetCoordinate():SetAltitude(3000) --- +-- -- -- Task bomb Sukhumi warehouse using all bombs (2032) from direction 180 at altitude 5000 m. -- local task=group:TaskBombing(warehouse.Sukhumi:GetCoordinate():GetVec2(), false, "All", nil , 180, 5000, 2032) --- --- -- Define waypoints. +-- +-- -- Define waypoints. -- local WayPoints={} --- +-- -- -- Take off position. -- WayPoints[1]=warehouse.Kobuleti:GetCoordinate():WaypointAirTakeOffParking() -- -- Begin bombing run 20 km south of target. @@ -1363,16 +1366,16 @@ -- WayPoints[3]=HomeCoord:WaypointAirTurningPoint() -- -- Land at homebase. Bombers are added back to stock and can be employed in later assignments. -- WayPoints[4]=warehouse.Kobuleti:GetCoordinate():WaypointAirLanding() --- +-- -- -- Route bombers. -- group:Route(WayPoints) -- end --- +-- -- end -- end -- -- ## Example 15: Defining Off-Road Paths --- +-- -- For self propelled assets it is possible to define custom off-road paths from one warehouse to another via the @{#WAREHOUSE.AddOffRoadPath} function. -- The waypoints of a path are taken from late activated units. In this example, two paths have been defined between the warehouses Kobuleti and FARP London. -- Trucks are spawned at each warehouse and are guided along the paths to the other warehouse. @@ -1381,21 +1384,21 @@ -- -- Start warehouses -- warehouse.Kobuleti:Start() -- warehouse.London:Start() --- +-- -- -- Define a polygon zone as spawn zone at Kobuleti. -- warehouse.Kobuleti:SetSpawnZone(ZONE_POLYGON:New("Warehouse Kobuleti Spawn Zone", GROUP:FindByName("Warehouse Kobuleti Spawn Zone"))) --- +-- -- -- Add assets. -- warehouse.Kobuleti:AddAsset("M978", 20) -- warehouse.London:AddAsset("M818", 20) --- +-- -- -- Off two road paths from Kobuleti to London. The reverse path from London to Kobuleti is added automatically. -- warehouse.Kobuleti:AddOffRoadPath(warehouse.London, GROUP:FindByName("Warehouse Kobuleti-London OffRoad Path 1")) -- warehouse.Kobuleti:AddOffRoadPath(warehouse.London, GROUP:FindByName("Warehouse Kobuleti-London OffRoad Path 2")) --- --- -- London requests all available trucks from Kobuleti. +-- +-- -- London requests all available trucks from Kobuleti. -- warehouse.Kobuleti:AddRequest(warehouse.London, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_TRUCK, WAREHOUSE.Quantity.ALL) --- +-- -- -- Kobuleti requests all available trucks from London. -- warehouse.London:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_TRUCK, WAREHOUSE.Quantity.HALF) -- @@ -1404,62 +1407,62 @@ -- Warehouse at FARP Berlin is located at the front line and sends infantry groups to the battle zone. -- Whenever a group dies, a new group is send from the warehouse to the battle zone. -- Additionally, for each dead group, Berlin requests resupply from Batumi. --- +-- -- -- Start warehouses. -- warehouse.Batumi:Start() -- warehouse.Berlin:Start() --- +-- -- -- Front line warehouse. -- warehouse.Berlin:AddAsset("Infantry Platoon Alpha", 6) --- +-- -- -- Resupply warehouse. -- warehouse.Batumi:AddAsset("Infantry Platoon Alpha", 50) --- +-- -- -- Battle zone near FARP Berlin. This is where the action is! -- local BattleZone=ZONE:New("Virtual Battle Zone") --- +-- -- -- Send infantry groups to the battle zone. Two groups every ~60 seconds. -- for i=1,2 do -- local time=(i-1)*60+10 -- warehouse.Berlin:__AddRequest(time, warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 2, nil, nil, nil, "To Battle Zone") -- end --- +-- -- -- Take care of the spawned units. -- function warehouse.Berlin:OnAfterSelfRequest(From,Event,To,groupset,request) -- local groupset=groupset --Core.Set#SET_GROUP -- local request=request --Functional.Warehouse#WAREHOUSE.Pendingitem --- +-- -- -- Get assignment of this request. -- local assignment=warehouse.Berlin:GetAssignment(request) --- +-- -- if assignment=="To Battle Zone" then --- +-- -- for _,group in pairs(groupset:GetSet()) do -- local group=group --Wrapper.Group#GROUP --- +-- -- -- Route group to Battle zone. -- local ToCoord=BattleZone:GetRandomCoordinate() -- group:RouteGroundOnRoad(ToCoord, group:GetSpeedMax()*0.8) --- +-- -- -- After 3-5 minutes we create an explosion to destroy the group. -- SCHEDULER:New(nil, Explosion, {group, 50}, math.random(180, 300)) -- end --- +-- -- end --- +-- -- end --- +-- -- -- An asset has died ==> request resupply for it. -- function warehouse.Berlin:OnAfterAssetDead(From, Event, To, asset, request) -- local asset=asset --Functional.Warehouse#WAREHOUSE.Assetitem -- local request=request --Functional.Warehouse#WAREHOUSE.Pendingitem --- +-- -- -- Get assignment. -- local assignment=warehouse.Berlin:GetAssignment(request) --- +-- -- -- Request resupply for dead asset from Batumi. -- warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, asset.attribute, nil, nil, nil, nil, "Resupply") --- +-- -- -- Send asset to Battle zone either now or when they arrive. -- warehouse.Berlin:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, asset.attribute, 1, nil, nil, nil, assignment) -- end @@ -1473,12 +1476,12 @@ -- Once infantry has arrived at Batumi, it will walk by itself to warehouse Pampa. -- The mortars can only be transported once the Mi-8 helos are available again, i.e. when the infantry has been delivered. -- Once the mortars arrive at Batumi, they will be transported by APCs to Pampa. --- +-- -- -- Start warehouses. -- warehouse.Kobuleti:Start() -- warehouse.Batumi:Start() -- warehouse.Pampa:Start() --- +-- -- -- Add assets to Kobuleti warehouse, which is our main hub. -- warehouse.Kobuleti:AddAsset("C-130", 2) -- warehouse.Kobuleti:AddAsset("C-17A", 2, nil, 77000) @@ -1486,32 +1489,32 @@ -- warehouse.Kobuleti:AddAsset("Leopard 2", 10, nil, nil, 62000, 500) -- warehouse.Kobuleti:AddAsset("Mortar Alpha", 10, nil, nil, 210) -- warehouse.Kobuleti:AddAsset("Infantry Platoon Alpha", 20) --- +-- -- -- Transports at Batumi. -- warehouse.Batumi:AddAsset("SPz Marder", 2) -- warehouse.Batumi:AddAsset("TPz Fuchs", 2) --- +-- -- -- Tanks transported by plane from from Kobuleti to Batumi. -- warehouse.Kobuleti:AddRequest(warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_TANK, 2, WAREHOUSE.TransportType.AIRPLANE, 2, 10, "Assets for Pampa") -- -- Artillery transported by helicopter from Kobuleti to Batumi. -- warehouse.Kobuleti:AddRequest(warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_ARTILLERY, 2, WAREHOUSE.TransportType.HELICOPTER, 2, 30, "Assets for Pampa via APC") -- -- Infantry transported by helicopter from Kobuleti to Batumi. -- warehouse.Kobuleti:AddRequest(warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 8, WAREHOUSE.TransportType.HELICOPTER, 2, 20, "Assets for Pampa") --- +-- -- --- Function handling assets delivered from Kobuleti warehouse. -- function warehouse.Kobuleti:OnAfterDelivered(From, Event, To, request) -- local request=request --Functional.Warehouse#WAREHOUSE.Pendingitem --- +-- -- -- Get assignment. -- local assignment=warehouse.Kobuleti:GetAssignment(request) --- +-- -- -- Check if these assets were meant for Warehouse Pampa. -- if assignment=="Assets for Pampa via APC" then -- -- Forward everything that arrived at Batumi to Pampa via APC. -- warehouse.Batumi:AddRequest(warehouse.Pampa, WAREHOUSE.Descriptor.ATTRIBUTE, request.cargoattribute, request.ndelivered, WAREHOUSE.TransportType.APC, WAREHOUSE.Quantity.ALL) -- end -- end --- +-- -- -- Forward all mobile ground assets to Pampa once they arrived. -- function warehouse.Batumi:OnAfterNewAsset(From, Event, To, asset, assignment) -- local asset=asset --Functional.Warehouse#WAREHOUSE.Assetitem @@ -1555,6 +1558,8 @@ WAREHOUSE = { autosave = false, autosavepath = nil, autosavefile = nil, + saveparking = false, + isunit = false, } --- Item of the warehouse stock table. @@ -1701,7 +1706,7 @@ WAREHOUSE.TransportType = { --- Warehouse quantity enumerator for selecting number of assets, e.g. all, half etc. of what is in stock rather than an absolute number. -- @type WAREHOUSE.Quantity -- @field #string ALL All "all" assets currently in stock. --- @field #string THREEQUARTERS Three quarters "3/4" of assets in stock. +-- @field #string THREEQUARTERS Three quarters "3/4" of assets in stock. -- @field #string HALF Half "1/2" of assets in stock. -- @field #string THIRD One third "1/3" of assets in stock. -- @field #string QUARTER One quarter "1/4" of assets in stock. @@ -1716,17 +1721,19 @@ WAREHOUSE.Quantity = { --- Warehouse database. Note that this is a global array to have easier exchange between warehouses. -- @type WAREHOUSE.db -- @field #number AssetID Unique ID of each asset. This is a running number, which is increased each time a new asset is added. --- @field #table Assets Table holding registered assets, which are of type @{Functional.Warehouse#WAREHOUSE.Assetitem}. +-- @field #table Assets Table holding registered assets, which are of type @{Functional.Warehouse#WAREHOUSE.Assetitem}.# +-- @field #number WarehouseID Unique ID of the warehouse. Running number. -- @field #table Warehouses Table holding all defined @{#WAREHOUSE} objects by their unique ids. WAREHOUSE.db = { - AssetID = 0, - Assets = {}, - Warehouses = {} + AssetID = 0, + Assets = {}, + WarehouseID = 0, + Warehouses = {} } --- Warehouse class version. -- @field #string version -WAREHOUSE.version="0.6.4" +WAREHOUSE.version="0.6.7" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: Warehouse todo list. @@ -1735,12 +1742,12 @@ WAREHOUSE.version="0.6.4" -- TODO: Add check if assets "on the move" are stationary. Can happen if ground units get stuck in buildings. If stationary auto complete transport by adding assets to request warehouse? Time? -- TODO: Optimize findpathonroad. Do it only once (first time) and safe paths between warehouses similar to off-road paths. -- TODO: Spawn assets only virtually, i.e. remove requested assets from stock but do NOT spawn them ==> Interface to A2A dispatcher! Maybe do a negative sign on asset number? --- TODO: Test capturing a neutral warehouse. -- TODO: Make more examples: ARTY, CAP, ... -- TODO: Check also general requests like all ground. Is this a problem for self propelled if immobile units are among the assets? Check if transport. -- TODO: Handle the case when units of a group die during the transfer. -- TODO: Added habours as interface for transport to from warehouses? Could make a rudimentary shipping dispatcher. --- TODO: Add save/load capability of warehouse <==> percistance after mission restart. Difficult in lua! +-- DONE: Test capturing a neutral warehouse. +-- DONE: Add save/load capability of warehouse <==> percistance after mission restart. Difficult in lua! -- DONE: Get cargo bay and weight from CARGO_GROUP and GROUP. No necessary any more! -- DONE: Add possibility to set weight and cargo bay manually in AddAsset function as optional parameters. -- DONE: Check overlapping aircraft sometimes. @@ -1759,7 +1766,7 @@ WAREHOUSE.version="0.6.4" -- DONE: Warehouse re-capturing not working?! -- DONE: Naval assets dont go back into stock once arrived. -- DONE: Take cargo weight into consideration, when selecting transport assets. --- DONE: Add ports for spawning naval assets. +-- DONE: Add ports for spawning naval assets. -- DONE: Add shipping lanes between warehouses. -- DONE: Handle cases with immobile units <== should be handled by dispatcher classes. -- DONE: Handle cases for aircraft carriers and other ships. Place warehouse on carrier possible? On others probably not - exclude them? @@ -1787,23 +1794,30 @@ WAREHOUSE.version="0.6.4" --- 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 of 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. +-- @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 -- @return #WAREHOUSE self function WAREHOUSE:New(warehouse, alias) BASE:T({warehouse=warehouse}) - + -- Check if just a string was given and convert to static. if type(warehouse)=="string" then - warehouse=STATIC:FindByName(warehouse, true) + warehouse=UNIT:FindByName(warehouse) + if warehouse==nil then + env.info(string.format("No warehouse unit with name %s found trying static.", warehouse)) + warehouse=STATIC:FindByName(warehouse, true) + self.isunit=false + else + self.isunit=true + end end - + -- Nil check. if warehouse==nil then BASE:E("ERROR: Warehouse does not exist!") return nil end - + -- Set alias. self.alias=alias or warehouse:GetName() @@ -1818,25 +1832,33 @@ function WAREHOUSE:New(warehouse, alias) -- Set some variables. self.warehouse=warehouse - self.uid=tonumber(warehouse:GetID()) + + -- Increase global warehouse counter. + WAREHOUSE.db.WarehouseID=WAREHOUSE.db.WarehouseID+1 + + -- Set unique ID for this warehouse. + self.uid=WAREHOUSE.db.WarehouseID + + -- As Kalbuth found out, this would fail when using SPAWNSTATIC https://forums.eagle.ru/showthread.php?p=3703488#post3703488 + --self.uid=tonumber(warehouse:GetID()) -- Closest of the same coalition but within a certain range. local _airbase=self:GetCoordinate():GetClosestAirbase(nil, self:GetCoalition()) if _airbase and _airbase:GetCoordinate():Get2DDistance(self:GetCoordinate()) < 3000 then self:SetAirbase(_airbase) 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) - + -- Add warehouse to database. WAREHOUSE.db.Warehouses[self.uid]=self - + ----------------------- --- FSM Transitions --- ----------------------- - + -- Start State. self:SetStartState("NotReadyYet") @@ -1845,7 +1867,7 @@ function WAREHOUSE:New(warehouse, alias) 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("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. @@ -1857,25 +1879,25 @@ function WAREHOUSE:New(warehouse, alias) 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! - self:AddTransition("Running", "Pause", "Paused") -- Pause the processing of new requests. Still possible to add assets and requests. - self:AddTransition("Paused", "Unpause", "Running") -- Unpause the warehouse. Queued requests are processed again. + self:AddTransition("Running", "Pause", "Paused") -- Pause the processing of new requests. Still possible to add assets and requests. + self:AddTransition("Paused", "Unpause", "Running") -- Unpause the warehouse. Queued requests are processed again. self:AddTransition("*", "Stop", "Stopped") -- Stop the warehouse. self:AddTransition("Stopped", "Restart", "Running") -- Restart the warehouse when it was stopped before. self:AddTransition("Loaded", "Restart", "Running") -- Restart the warehouse when assets were loaded from file before. - self:AddTransition("*", "Save", "*") -- TODO Save the warehouse state to disk. + self:AddTransition("*", "Save", "*") -- Save the warehouse state to disk. self:AddTransition("*", "Attacked", "Attacked") -- Warehouse is under attack by enemy coalition. self:AddTransition("Attacked", "Defeated", "Running") -- Attack by other coalition was defeated! - self:AddTransition("*", "ChangeCountry", "*") -- Change country (and coalition) of the warehouse. Warehouse is respawned! + self:AddTransition("*", "ChangeCountry", "*") -- Change country (and coalition) of the warehouse. Warehouse is respawned! 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("*", "AssetDead", "*") -- An asset group died. self:AddTransition("*", "Destroyed", "Destroyed") -- Warehouse was destroyed. All assets in stock are gone and warehouse is stopped. - + ------------------------ --- Pseudo Functions --- ------------------------ - + --- Triggers the FSM event "Start". Starts the warehouse. Initializes parameters and starts event handlers. -- @function [parent=#WAREHOUSE] Start -- @param #WAREHOUSE self @@ -2013,7 +2035,7 @@ function WAREHOUSE:New(warehouse, alias) -- @function [parent=#WAREHOUSE] Request -- @param #WAREHOUSE self -- @param #WAREHOUSE.Queueitem Request Information table of the request. - + --- Triggers the FSM event "Request" after a delay. Executes a request from the queue if possible. -- @function [parent=#WAREHOUSE] __Request -- @param #WAREHOUSE self @@ -2024,8 +2046,8 @@ function WAREHOUSE:New(warehouse, alias) --- Triggers the FSM event "Arrived" when a group has arrived at the destination warehouse. -- This function should always be called from the sending and not the receiving warehouse. -- If the group is a cargo asset, it is added to the receiving warehouse. If the group is a transporter it - -- is added to the sending warehouse since carriers are supposed to return to their home warehouse once - -- all cargo was delivered. + -- is added to the sending warehouse since carriers are supposed to return to their home warehouse once + -- all cargo was delivered. -- @function [parent=#WAREHOUSE] Arrived -- @param #WAREHOUSE self -- @param Wrapper.Group#GROUP group Group that has arrived. @@ -2033,7 +2055,7 @@ function WAREHOUSE:New(warehouse, alias) --- Triggers the FSM event "Arrived" after a delay when a group has arrived at the destination. -- This function should always be called from the sending and not the receiving warehouse. -- If the group is a cargo asset, it is added to the receiving warehouse. If the group is a transporter it - -- is added to the sending warehouse since carriers are supposed to return to their home warehouse once + -- is added to the sending warehouse since carriers are supposed to return to their home warehouse once -- @function [parent=#WAREHOUSE] __Arrived -- @param #WAREHOUSE self -- @param #number delay Delay in seconds. @@ -2088,24 +2110,24 @@ function WAREHOUSE:New(warehouse, alias) --- On after "SelfRequest" event. Request was initiated from the warehouse to itself. Groups are simply spawned at the warehouse or the associated airbase. -- All requested assets are passed as a @{Core.Set#SET_GROUP} and can be used for further tasks or in other MOOSE classes. -- Note that airborne assets are spawned in uncontrolled state so they do not simply "fly away" after spawning. - -- + -- -- @usage -- --- Self request event. Triggered once the assets are spawned in the spawn zone or at the airbase. -- function mywarehouse:OnAfterSelfRequest(From, Event, To, groupset, request) -- local groupset=groupset --Core.Set#SET_GROUP - -- + -- -- -- Loop over all groups spawned from that request. -- for _,group in pairs(groupset:GetSetObjects()) do -- local group=group --Wrapper.Group#GROUP - -- + -- -- -- Gree smoke on spawned group. -- group:SmokeGreen() - -- + -- -- -- Activate uncontrolled airborne group if necessary. -- group:StartUncontrolled() -- end - -- end - -- + -- end + -- -- @function [parent=#WAREHOUSE] OnAfterSelfRequest -- @param #WAREHOUSE self -- @param #string From From state. @@ -2159,7 +2181,7 @@ function WAREHOUSE:New(warehouse, alias) -- @function [parent=#WAREHOUSE] ChangeCountry -- @param #WAREHOUSE self -- @param DCS#country.id Country New country id of the warehouse. - + --- Triggers the FSM event "ChangeCountry" after a delay so the warehouse is respawned with the new country. -- @function [parent=#WAREHOUSE] __ChangeCountry -- @param #WAREHOUSE self @@ -2180,7 +2202,7 @@ function WAREHOUSE:New(warehouse, alias) -- @param #WAREHOUSE self -- @param DCS#coalition.side Coalition Coalition side which captured the warehouse. -- @param DCS#country.id Country Country id which has captured the warehouse. - + --- Triggers the FSM event "Captured" with a delay when a warehouse has been captured by another coalition. -- @function [parent=#WAREHOUSE] __Captured -- @param #WAREHOUSE self @@ -2196,13 +2218,13 @@ function WAREHOUSE:New(warehouse, alias) -- @param #string To To state. -- @param DCS#coalition.side Coalition Coalition side which captured the warehouse, i.e. a number of @{DCS#coalition.side} enumerator. -- @param DCS#country.id Country Country id which has captured the warehouse, i.e. a number @{DCS#country.id} enumerator. - -- + -- --- Triggers the FSM event "AirbaseCaptured" when the airbase of the warehouse has been captured by another coalition. -- @function [parent=#WAREHOUSE] AirbaseCaptured -- @param #WAREHOUSE self -- @param DCS#coalition.side Coalition Coalition side which captured the airbase, i.e. a number of @{DCS#coalition.side} enumerator. - + --- Triggers the FSM event "AirbaseCaptured" with a delay when the airbase of the warehouse has been captured by another coalition. -- @function [parent=#WAREHOUSE] __AirbaseCaptured -- @param #WAREHOUSE self @@ -2222,7 +2244,7 @@ function WAREHOUSE:New(warehouse, alias) -- @param #WAREHOUSE self -- @function [parent=#WAREHOUSE] AirbaseRecaptured -- @param DCS#coalition.side Coalition Coalition which re-captured the airbase, i.e. the same as the current warehouse owner coalition. - + --- Triggers the FSM event "AirbaseRecaptured" with a delay when the airbase of the warehouse has been re-captured from the other coalition. -- @function [parent=#WAREHOUSE] __AirbaseRecaptured -- @param #WAREHOUSE self @@ -2264,7 +2286,7 @@ function WAREHOUSE:New(warehouse, alias) --- Triggers the FSM event "Destroyed" when the warehouse was destroyed. Services are stopped. -- @function [parent=#WAREHOUSE] Destroyed -- @param #WAREHOUSE self - + --- Triggers the FSM event "Destroyed" with a delay when the warehouse was destroyed. Services are stopped. -- @function [parent=#WAREHOUSE] __Destroyed -- @param #WAREHOUSE self @@ -2283,7 +2305,7 @@ function WAREHOUSE:New(warehouse, alias) -- @param #WAREHOUSE self -- @param #string path Path where the file is saved. Default is the DCS installation root directory. -- @param #string filename (Optional) File name. Default is WAREHOUSE-_.txt. - + --- Triggers the FSM event "Save" with a delay when the warehouse assets are saved to a file. -- @function [parent=#WAREHOUSE] __Save -- @param #WAREHOUSE self @@ -2306,7 +2328,7 @@ function WAREHOUSE:New(warehouse, alias) -- @param #WAREHOUSE self -- @param #string path Path where the file is located. Default is the DCS installation root directory. -- @param #string filename (Optional) File name. Default is WAREHOUSE-_.txt. - + --- Triggers the FSM event "Load" with a delay when the warehouse assets are loaded from disk. -- @function [parent=#WAREHOUSE] __Load -- @param #WAREHOUSE self @@ -2363,6 +2385,24 @@ function WAREHOUSE:SetReportOff() return self end +--- Enable safe parking option, i.e. parking spots at an airbase will be considered as occupied when a client aircraft is parked there (even if the client slot is not taken by a player yet). +-- Note that also incoming aircraft can reserve/occupie parking spaces. +-- @param #WAREHOUSE self +-- @return #WAREHOUSE self +function WAREHOUSE:SetSafeParkingOn() + self.safeparking=true + return self +end + +--- Disable safe parking option. Note that is the default setting. +-- @param #WAREHOUSE self +-- @return #WAREHOUSE self +function WAREHOUSE:SetSafeParkingOff() + self.safeparking=false + return self +end + + --- Set interval of status updates. Note that normally only one request can be processed per time interval. -- @param #WAREHOUSE self -- @param #number timeinterval Time interval in seconds. @@ -2393,7 +2433,7 @@ function WAREHOUSE:SetWarehouseZone(zone) return self end ---- Set auto defence on. When the warehouse is under attack, all ground assets are spawned automatically and will defend the warehouse zone. +--- Set auto defence on. When the warehouse is under attack, all ground assets are spawned automatically and will defend the warehouse zone. -- @param #WAREHOUSE self -- @return #WAREHOUSE self function WAREHOUSE:SetAutoDefenceOn() @@ -2401,7 +2441,7 @@ function WAREHOUSE:SetAutoDefenceOn() return self end ---- Set auto defence off. This is the default. +--- Set auto defence off. This is the default. -- @param #WAREHOUSE self -- @return #WAREHOUSE self function WAREHOUSE:SetAutoDefenceOff() @@ -2409,7 +2449,7 @@ function WAREHOUSE:SetAutoDefenceOff() return self end ---- Set auto defence off. This is the default. +--- Set auto defence off. This is the default. -- @param #WAREHOUSE self -- @param #string path Path where to save the asset data file. -- @param #string filename File name. Default is generated automatically from warehouse id. @@ -2441,7 +2481,7 @@ end --- Set the connection of the warehouse to the road. -- Ground assets spawned in the warehouse spawn zone will first go to this point and from there travel on road to the requesting warehouse. -- Note that by default the road connection is set to the closest point on road from the center of the spawn zone if it is withing 3000 meters. --- Also note, that if the parameter "coordinate" is passed as nil, any road connection is disabled and ground assets cannot travel of be transportet on the ground. +-- Also note, that if the parameter "coordinate" is passed as nil, any road connection is disabled and ground assets cannot travel of be transportet on the ground. -- @param #WAREHOUSE self -- @param Core.Point#COORDINATE coordinate The road connection. Technically, the closest point on road from this coordinate is determined by DCS API function. So this point must not be exactly on the road. -- @return #WAREHOUSE self @@ -2469,7 +2509,7 @@ function WAREHOUSE:SetRailConnection(coordinate) end --- Set the port zone for this warehouse. --- The port zone is the zone, where all naval assets of the warehouse are spawned. +-- The port zone is the zone, where all naval assets of the warehouse are spawned. -- @param #WAREHOUSE self -- @param Core.Zone#ZONE zone The zone defining the naval port of the warehouse. -- @return #WAREHOUSE self @@ -2499,10 +2539,10 @@ function WAREHOUSE:AddShippingLane(remotewarehouse, group, oneway) -- Initial and final coordinates are random points within the port zones. local startcoord=self.portzone:GetRandomCoordinate() local finalcoord=remotewarehouse.portzone:GetRandomCoordinate() - + -- Create new lane from waypoints of the template group. local lane=self:_NewLane(group, startcoord, finalcoord) - + -- Debug info. Marks along shipping lane. if self.Debug then for i=1,#lane do @@ -2511,29 +2551,29 @@ function WAREHOUSE:AddShippingLane(remotewarehouse, group, oneway) coord:MarkToCoalition(text, self:GetCoalition()) end end - + -- Name of the remote warehouse. local remotename=remotewarehouse.warehouse:GetName() - + -- Create new table if no shipping lane exists yet. if self.shippinglanes[remotename]==nil then self.shippinglanes[remotename]={} - end - + end + -- Add shipping lane. table.insert(self.shippinglanes[remotename], lane) - + -- Add shipping lane in the opposite direction. if not oneway then remotewarehouse:AddShippingLane(self, group, true) end - + return self end --- Add an off-road path from this warehouse to another and back. --- The start and end points are automatically set to one random point in the respective spawn zones of the two warehouses. +-- The start and end points are automatically set to one random point in the respective spawn zones of the two warehouses. -- By default, the reverse path is also added as path from the remote warehouse to this warehouse. -- @param #WAREHOUSE self -- @param #WAREHOUSE remotewarehouse The remote warehouse to which the path leads. @@ -2545,15 +2585,15 @@ function WAREHOUSE:AddOffRoadPath(remotewarehouse, group, oneway) -- Initial and final points are random points within the spawn zone. local startcoord=self.spawnzone:GetRandomCoordinate() local finalcoord=remotewarehouse.spawnzone:GetRandomCoordinate() - + -- Create new path from template group waypoints. local path=self:_NewLane(group, startcoord, finalcoord) - + if path==nil then self:E(self.wid.."ERROR: Offroad path could not be added. Group present in ME?") return end - + -- Debug info. Marks along path. if path and self.Debug then for i=1,#path do @@ -2562,23 +2602,23 @@ function WAREHOUSE:AddOffRoadPath(remotewarehouse, group, oneway) coord:MarkToCoalition(text, self:GetCoalition()) end end - + -- Name of the remote warehouse. local remotename=remotewarehouse.warehouse:GetName() - + -- Create new table if no shipping lane exists yet. if self.offroadpaths[remotename]==nil then self.offroadpaths[remotename]={} - end - + end + -- Add off road path. table.insert(self.offroadpaths[remotename], path) - - -- Add off road path in the opposite direction (if not forbidden). + + -- Add off road path in the opposite direction (if not forbidden). if not oneway then remotewarehouse:AddOffRoadPath(self, group, true) end - + return self end @@ -2596,19 +2636,19 @@ function WAREHOUSE:_NewLane(group, startcoord, finalcoord) -- Get route from template. local lanepoints=group:GetTemplateRoutePoints() - + -- First and last waypoints local laneF=lanepoints[1] local laneL=lanepoints[#lanepoints] - + -- Get corresponding coordinates. local coordF=COORDINATE:New(laneF.x, 0, laneF.y) local coordL=COORDINATE:New(laneL.x, 0, laneL.y) - + -- Figure out which point is closer to the port of this warehouse. local distF=startcoord:Get2DDistance(coordF) local distL=startcoord:Get2DDistance(coordL) - + -- Add the lane. Need to take care of the wrong "direction". lane={} if distF0 then - + -- Check if coalition is right. local samecoalition=anycoalition or Coalition==warehouse:GetCoalition() - + -- Check that warehouse is in service. if samecoalition and not (warehouse:IsNotReadyYet() or warehouse:IsStopped() or warehouse:IsDestroyed()) then - + -- Get number of assets. Whole stock is returned if no descriptor/value is given. local nassets=warehouse:GetNumberOfAssets(Descriptor, DescriptorValue) - + --env.info(string.format(" FF warehouse %s nassets = %d for %s=%s", warehouse.alias, nassets, tostring(Descriptor), tostring(DescriptorValue))) - + -- Assume we have enough. local enough=true -- If specifc assets need to be present... if Descriptor and DescriptorValue then -- Check that enough assets (default 1) are available. enough = nassets>=MinAssets - end - + end + -- Check distance. if enough and (distmin==nil or dist 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. + + -- 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. @@ -3321,70 +3363,70 @@ function WAREHOUSE:_JobDone() -- 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.wid..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) - + self:_InfoMessage(text) + -- Debug smoke. if self.Debug then group:SmokeRed() end - + -- Group arrived. self:Arrived(group) end - end + end 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. + -- Debug smoke. if self.Debug then group:SmokeBlue() - end + 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)) + 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 -- loop over requests -- Remove pending requests if done. @@ -3401,15 +3443,15 @@ function WAREHOUSE:_CheckAssetStatus() local function _CheckGroup(_request, _group) local request=_request --#WAREHOUSE.Pendingitem local group=_group --Wrapper.Group#GROUP - + if group and group:IsAlive() then - + -- Category of group. local category=group:GetCategory() - + for _,_unit in pairs(group:GetUnits()) do local unit=_unit --Wrapper.Unit#UNIT - + if unit and unit:IsAlive() then local unitid=unit:GetID() local life9=unit:GetLife() @@ -3417,16 +3459,16 @@ function WAREHOUSE:_CheckAssetStatus() local life=life9/life0*100 local speed=unit:GetVelocityMPS() local onground=unit:InAir() - + local problem=false if life<10 then - self:T(string.format("Unit %s is heavily damaged!", unit:GetName())) + self:T(string.format("Unit %s is heavily damaged!", unit:GetName())) end if speed<1 and unit:GetSpeedMax()>1 and onground then self:T(string.format("Unit %s is not moving!", unit:GetName())) problem=true end - + if problem then if request.assetproblem[unitid] then local deltaT=timer.getAbsTime()-request.assetproblem[unitid] @@ -3439,33 +3481,33 @@ function WAREHOUSE:_CheckAssetStatus() end end end - + end end end - + for _,request in pairs(self.pending) do local request=request --#WAREHOUSE.Pendingitem - + -- Cargo groups. if request.cargogroupset then for _,_group in pairs(request.cargogroupset:GetSet()) do local group=_group --Wrapper.Group#GROUP - + _CheckGroup(request, group) - + end end - + -- Transport groups. if request.transportgroupset then for _,group in pairs(request.transportgroupset:GetSet()) do - - _CheckGroup(request, group) + + _CheckGroup(request, group) end end - + end end @@ -3491,32 +3533,32 @@ function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribu -- Set default. local n=ngroups or 1 - + -- Handle case where just a string is passed. if type(group)=="string" then group=GROUP:FindByName(group) end - + if liveries and type(liveries)=="string" then liveries={liveries} end - + if group then - + -- Try to get UIDs from group name. Is this group a known or a new asset? local wid,aid,rid=self:_GetIDsFromGroup(group) - + if wid and aid and rid then --------------------------- -- This is a KNOWN asset -- --------------------------- - + -- Get the original warehouse this group belonged to. local warehouse=self:FindWarehouseInDB(wid) if warehouse then local request=warehouse:_GetRequestOfGroup(group, warehouse.pending) if request then - + -- Increase number of cargo delivered and transports home. local istransport=warehouse:_GroupIsTransport(group,request) if istransport==true then @@ -3530,66 +3572,67 @@ function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribu else self:T(warehouse.wid..string.format("WARNING: Group %s is neither cargo nor transport!", group:GetName())) end - - end - - -- If no assignment was given we take the assignment of the request if there is any. - if assignment==nil and request.assignment~=nil then - assignment=request.assignment + + -- If no assignment was given we take the assignment of the request if there is any. + if assignment==nil and request.assignment~=nil then + assignment=request.assignment + end + end end -- Get the asset from the global DB. local asset=self:FindAssetInDB(group) - -- Set livery. + -- Set livery. if liveries then asset.livery=liveries[math.random(#liveries)] end - + -- Set skill. asset.skill=skill - + -- Note the group is only added once, i.e. the ngroups parameter is ignored here. -- This is because usually these request comes from an asset that has been transfered from another warehouse and hence should only be added once. - if asset~=nil then + if asset~=nil then self:_DebugMessage(string.format("Warehouse %s: Adding KNOWN asset uid=%d with attribute=%s to stock.", self.alias, asset.uid, asset.attribute), 5) table.insert(self.stock, asset) self:NewAsset(asset, assignment or "") else self:_ErrorMessage(string.format("ERROR: Known asset could not be found in global warehouse db!"), 0) - end - + end + else ------------------------- -- This is a NEW asset -- ------------------------- - + -- Debug info. self:_DebugMessage(string.format("Warehouse %s: Adding %d NEW assets of group %s to stock.", self.alias, n, tostring(group:GetName())), 5) - + -- This is a group that is not in the db yet. Add it n times. local assets=self:_RegisterAsset(group, n, forceattribute, forcecargobay, forceweight, loadradius, liveries, skill) - + -- Add created assets to stock of this warehouse. for _,asset in pairs(assets) do table.insert(self.stock, asset) self:NewAsset(asset, assignment or "") - end - - end - + end + + end + -- Destroy group if it is alive. if group:IsAlive()==true then self:_DebugMessage(string.format("Destroying 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) end - + else self:E(self.wid.."ERROR: Unknown group added as asset!") + self:E({unknowngroup=group}) end - + -- Update status. --self:__Status(-1) end @@ -3610,7 +3653,7 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, -- Set default. local n=ngroups or 1 - + -- Get the size of an object. local function _GetObjectSize(DCSdesc) if DCSdesc.box then @@ -3620,18 +3663,18 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, return math.max(x,z), x , y, z end return 0,0,0,0 - end - + end + -- Get name of template group. local templategroupname=group:GetName() - + local Descriptors=group:GetUnit(1):GetDesc() local Category=group:GetCategory() local TypeName=group:GetTypeName() local SpeedMax=group:GetSpeedMax() local RangeMin=group:GetRange() local smax,sx,sy,sz=_GetObjectSize(Descriptors) - + -- Get weight and cargo bay size in kg. local weight=0 local cargobay={} @@ -3640,31 +3683,31 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, for _i,_unit in pairs(group:GetUnits()) do local unit=_unit --Wrapper.Unit#UNIT local Desc=unit:GetDesc() - + -- Weight. We sum up all units in the group. local unitweight=forceweight or Desc.massEmpty if unitweight then weight=weight+unitweight end - + local cargomax=0 local massfuel=Desc.fuelMassMax or 0 local massempty=Desc.massEmpty or 0 local massmax=Desc.massMax or 0 - + -- Calcuate cargo bay limit value. cargomax=massmax-massfuel-massempty self:T3(self.wid..string.format("Unit name=%s: mass empty=%.1f kg, fuel=%.1f kg, max=%.1f kg ==> cargo=%.1f kg", unit:GetName(), unitweight, massfuel, massmax, cargomax)) - + -- Cargo bay size. local bay=forcecargobay or unit:GetCargoBayFreeWeight() - + -- Add bay size to table. table.insert(cargobay, bay) - + -- Sum up total bay size. cargobaytot=cargobaytot+bay - + -- Get max bay size. if bay>cargobaymax then cargobaymax=bay @@ -3680,20 +3723,20 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, -- Add this n times to the table. for i=1,n do local asset={} --#WAREHOUSE.Assetitem - + -- Increase asset unique id counter. WAREHOUSE.db.AssetID=WAREHOUSE.db.AssetID+1 - + -- Set parameters. asset.uid=WAREHOUSE.db.AssetID asset.templatename=templategroupname asset.template=UTILS.DeepCopy(_DATABASE.Templates.Groups[templategroupname].Template) asset.category=Category asset.unittype=TypeName - asset.nunits=#asset.template.units + asset.nunits=#asset.template.units asset.range=RangeMin asset.speedmax=SpeedMax - asset.size=smax + asset.size=smax asset.weight=weight asset.DCSdesc=Descriptors asset.attribute=attribute @@ -3705,14 +3748,14 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, asset.livery=liveries[math.random(#liveries)] end asset.skill=skill - + if i==1 then self:_AssetItemInfo(asset) end - + -- Add asset to global db. WAREHOUSE.db.Assets[asset.uid]=asset - + -- Add asset to the table that is retured. table.insert(assets,asset) end @@ -3773,12 +3816,12 @@ end -- @param #string Assignment A keyword or text that later be used to identify this request and postprocess the assets. -- @return #boolean If true, request is okay at first glance. function WAREHOUSE:onbeforeAddRequest(From, Event, To, warehouse, AssetDescriptor, AssetDescriptorValue, nAsset, TransportType, nTransport, Assignment, Prio) - + -- Request is okay. local okay=true - + if AssetDescriptor==WAREHOUSE.Descriptor.ATTRIBUTE then - + -- Check if a valid attibute was given. local gotit=false for _,attribute in pairs(WAREHOUSE.Attribute) do @@ -3790,7 +3833,7 @@ function WAREHOUSE:onbeforeAddRequest(From, Event, To, warehouse, AssetDescripto self:_ErrorMessage("ERROR: Invalid request. Asset attribute is unknown!", 5) okay=false end - + elseif AssetDescriptor==WAREHOUSE.Descriptor.CATEGORY then -- Check if a valid category was given. @@ -3804,21 +3847,21 @@ function WAREHOUSE:onbeforeAddRequest(From, Event, To, warehouse, AssetDescripto self:_ErrorMessage("ERROR: Invalid request. Asset category is unknown!", 5) okay=false end - + elseif AssetDescriptor==WAREHOUSE.Descriptor.GROUPNAME then - + if type(AssetDescriptorValue)~="string" then self:_ErrorMessage("ERROR: Invalid request. Asset template name must be passed as a string!", 5) - okay=false + okay=false end - + elseif AssetDescriptor==WAREHOUSE.Descriptor.UNITTYPE then if type(AssetDescriptorValue)~="string" then self:_ErrorMessage("ERROR: Invalid request. Asset unit type must be passed as a string!", 5) - okay=false + okay=false end - + else self:_ErrorMessage("ERROR: Invalid request. Asset descriptor is not ATTRIBUTE, CATEGORY, GROUPNAME or UNITTYPE!", 5) okay=false @@ -3827,7 +3870,7 @@ function WAREHOUSE:onbeforeAddRequest(From, Event, To, warehouse, AssetDescripto -- Warehouse is stopped? if self:IsStopped() then self:_ErrorMessage("ERROR: Invalid request. Warehouse is stopped!", 0) - okay=false + okay=false end -- Warehouse is destroyed? @@ -3835,7 +3878,7 @@ function WAREHOUSE:onbeforeAddRequest(From, Event, To, warehouse, AssetDescripto self:_ErrorMessage("ERROR: Invalid request. Warehouse is destroyed!", 0) okay=false end - + return okay end @@ -3849,7 +3892,7 @@ end -- @param AssetDescriptorValue Value of the asset descriptor. Type depends on descriptor, i.e. could be a string, etc. -- @param #number nAsset Number of groups requested that match the asset specification. -- @param #WAREHOUSE.TransportType TransportType Type of transport. --- @param #number nTransport Number of transport units requested. +-- @param #number nTransport Number of transport units requested. -- @param #number Prio Priority of the request. Number ranging from 1=high to 100=low. -- @param #string Assignment A keyword or text that later be used to identify this request and postprocess the assets. function WAREHOUSE:onafterAddRequest(From, Event, To, warehouse, AssetDescriptor, AssetDescriptorValue, nAsset, TransportType, nTransport, Prio, Assignment) @@ -3870,8 +3913,8 @@ function WAREHOUSE:onafterAddRequest(From, Event, To, warehouse, AssetDescriptor local toself=false if self.warehouse:GetName()==warehouse.warehouse:GetName() then toself=true - end - + end + -- Increase id. self.queueid=self.queueid+1 @@ -3887,17 +3930,17 @@ function WAREHOUSE:onafterAddRequest(From, Event, To, warehouse, AssetDescriptor ntransport=nTransport, assignment=tostring(Assignment), airbase=warehouse:GetAirbase(), - category=warehouse:GetAirbaseCategory(), + category=warehouse:GetAirbaseCategory(), ndelivered=0, ntransporthome=0, assets={}, toself=toself, } --#WAREHOUSE.Queueitem - + -- Add request to queue. table.insert(self.queue, request) - - local text=string.format("Warehouse %s: New request from warehouse %s.\nDescriptor %s=%s, #assets=%s; Transport=%s, #transports =%s.", + + 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)) self:_DebugMessage(text, 5) @@ -3922,29 +3965,29 @@ function WAREHOUSE:onbeforeRequest(From, Event, To, Request) -- Shortcut to cargoassets. local _assets=Request.cargoassets - + if Request.nasset==0 then local text=string.format("Warehouse %s: Request denied! Zero assets were requested.", self.alias) self:_InfoMessage(text, 10) return false end - + -- Check if destination is in range for all requested assets. for _,_asset in pairs(_assets) do local asset=_asset --#WAREHOUSE.Assetitem - -- Check if destination is in range. - if asset.range1 then @@ -4458,35 +4501,35 @@ function WAREHOUSE:onafterUnloaded(From, Event, To, group) self:Arrived(group) elseif group:IsShip() then -- Not sure if naval units will be allowed as cargo even though it might be possible. Best put them into warehouse immediately. - self:Arrived(group) + self:Arrived(group) end - + else self:E(self.wid..string.format("ERROR unloaded Cargo group is not alive!")) - end + end end --- On after "Arrived" event. Triggered when a group has arrived at its destination warehouse. -- The routine should be called by the warehouse sending this asset and not by the receiving warehouse. -- It is checked if this asset is cargo (or self propelled) or transport. If it is cargo it is put into the stock of receiving warehouse. --- If it is a transporter it is put back into the sending warehouse since transports are supposed to return their home warehouse. +-- If it is a transporter it is put back into the sending warehouse since transports are supposed to return their home warehouse. -- @param #WAREHOUSE self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -- @param Wrapper.Group#GROUP group The group that was delivered. function WAREHOUSE:onafterArrived(From, Event, To, group) - + -- Debug message and smoke. if self.Debug then group:SmokeOrange() end - + -- Get pending request this group belongs to. local request=self:_GetRequestOfGroup(group, self.pending) if request then - + -- Get the right warehouse to put the asset into -- Transports go back to the warehouse which called this function while cargo goes into the receiving warehouse. local warehouse=request.warehouse @@ -4499,15 +4542,15 @@ function WAREHOUSE:onafterArrived(From, Event, To, group) self:E(self.wid..string.format("ERROR: Group %s is neither cargo nor transport", group:GetName())) return end - + -- Debug message. self:_DebugMessage(string.format("Group %s arrived at warehouse %s!", tostring(group:GetName()), warehouse.alias), 5) - + -- Route mobile ground group to the warehouse. Group has 60 seconds to get there or it is despawned and added as asset to the new warehouse regardless. if group:IsGround() and group:GetSpeedMax()>1 then group:RouteGroundTo(warehouse:GetCoordinate(), group:GetSpeedMax()*0.3, "Off Road") end - + -- Increase number of cargo delivered and transports home. local istransport=warehouse:_GroupIsTransport(group,request) if istransport==true then @@ -4520,12 +4563,12 @@ function WAREHOUSE:onafterArrived(From, Event, To, group) self:T2(warehouse.wid..string.format("Cargo %d of %s delivered.", request.ndelivered, tostring(request.nasset))) else self:E(warehouse.wid..string.format("ERROR: Group %s is neither cargo nor transport!", group:GetName())) - end - + end + -- Move asset from pending queue into new warehouse. warehouse:__AddAsset(60, group) end - + end --- On after "Delivered" event. Triggered when all asset groups have reached their destination. Corresponding request is deleted from the pending queue. @@ -4544,10 +4587,10 @@ function WAREHOUSE:onafterDelivered(From, Event, To, request) if self.Debug then self:_Fireworks(request.warehouse:GetCoordinate()) end - + -- Set delivered status for this request uid. self.delivered[request.uid]=true - + end @@ -4564,7 +4607,7 @@ function WAREHOUSE:onafterSelfRequest(From, Event, To, groupset, request) -- Debug info. self:_DebugMessage(string.format("Assets spawned at warehouse %s after self request!", self.alias)) - + -- Debug info. for _,_group in pairs(groupset:GetSetObjects()) do local group=_group --Wrapper.Group#GROUP @@ -4572,7 +4615,7 @@ function WAREHOUSE:onafterSelfRequest(From, Event, To, groupset, request) group:FlareGreen() end end - + -- Add a "defender request" to be able to despawn all assets once defeated. if self:IsAttacked() then @@ -4584,13 +4627,13 @@ function WAREHOUSE:onafterSelfRequest(From, Event, To, groupset, request) if group:IsGround() and speedmax>1 and group:IsNotInZone(self.zone) then group:RouteGroundTo(self.zone:GetRandomCoordinate(), 0.8*speedmax, "Off Road") end - end - end - + end + end + -- Add request to defenders. table.insert(self.defending, request) end - + end --- On after "Attacked" event. Warehouse is under attack by an another coalition. @@ -4605,29 +4648,29 @@ function WAREHOUSE:onafterAttacked(From, Event, To, Coalition, Country) -- Warning. local text=string.format("Warehouse %s: We are under attack!", self.alias) self:_InfoMessage(text) - + -- Debug smoke. if self.Debug then self:GetCoordinate():SmokeOrange() - end - + end + -- Spawn all ground units in the spawnzone? if self.autodefence then local nground=self:GetNumberOfAssets(WAREHOUSE.Descriptor.CATEGORY, Group.Category.GROUND) local text=string.format("Warehouse auto defence activated.\n") - + if nground>0 then text=text..string.format("Deploying all %d ground assets.", nground) - + -- Add self request. - self:AddRequest(self, WAREHOUSE.Descriptor.CATEGORY, Group.Category.GROUND, WAREHOUSE.Quantity.ALL, nil, nil , 0) + self:AddRequest(self, WAREHOUSE.Descriptor.CATEGORY, Group.Category.GROUND, WAREHOUSE.Quantity.ALL, nil, nil , 0, "AutoDefence") else - text=text..string.format("No ground assets currently available.") + text=text..string.format("No ground assets currently available.") end self:_InfoMessage(text) else local text=string.format("Warehouse auto defence inactive.") - self:I(self.wid..text) + self:I(self.wid..text) end end @@ -4645,28 +4688,28 @@ function WAREHOUSE:onafterDefeated(From, Event, To) -- Debug smoke. if self.Debug then self:GetCoordinate():SmokeGreen() - end + end -- Auto defence: put assets back into stock. if self.autodefence then for _,request in pairs(self.defending) do - - -- Route defenders back to warehoue (for visual reasons only) and put them back into stock. + + -- Route defenders back to warehoue (for visual reasons only) and put them back into stock. for _,_group in pairs(request.cargogroupset:GetSetObjects()) do local group=_group --Wrapper.Group#GROUP - + -- Get max speed of group and route it back slowly to the warehouse. local speed=group:GetSpeedMax() if group:IsGround() and speed>1 then group:RouteGroundTo(self:GetCoordinate(), speed*0.3) - end - + end + -- Add asset group back to stock after 60 seconds. self:__AddAsset(60, group) end - + end - + self.defending=nil self.defending={} end @@ -4686,8 +4729,8 @@ function WAREHOUSE:onbeforeChangeCountry(From, Event, To, Country) -- Message. local text=string.format("Warehouse %s: request to change country %d-->%d", self.alias, currentCountry, Country) self:_DebugMessage(text, 10) - - -- Check if current or requested coalition or country match. + + -- Check if current or requested coalition or country match. if currentCountry~=Country then return true end @@ -4709,17 +4752,17 @@ function WAREHOUSE:onafterChangeCountry(From, Event, To, Country) -- Respawn warehouse with new coalition/country. self.warehouse:ReSpawn(Country) - + local CoalitionNew=self:GetCoalition() - + -- Delete all waiting requests because they are not valid any more. self.queue=nil self.queue={} - + -- Airbase could have been captured before and already belongs to the new coalition. local airbase=AIRBASE:FindByName(self.airbasename) local airbasecoaltion=airbase:GetCoalition() - + if CoalitionNew==airbasecoaltion then -- Airbase already owned by the coalition that captured the warehouse. Airbase can be used by this warehouse. self.airbase=airbase @@ -4727,7 +4770,7 @@ function WAREHOUSE:onafterChangeCountry(From, Event, To, Country) -- Airbase is owned by other coalition. So this warehouse does not have an airbase unil it is captured. self.airbase=nil end - + -- Debug smoke. if self.Debug then if CoalitionNew==coalition.side.RED then @@ -4736,7 +4779,7 @@ function WAREHOUSE:onafterChangeCountry(From, Event, To, Country) self:GetCoordinate():SmokeBlue() end end - + end --- On after "Captured" event. Warehouse has been captured by another coalition. @@ -4778,7 +4821,7 @@ function WAREHOUSE:onafterAirbaseCaptured(From, Event, To, Coalition) self.airbase:GetCoordinate():SmokeBlue() end end - + -- Set airbase to nil and category to no airbase. self.airbase=nil end @@ -4795,9 +4838,9 @@ function WAREHOUSE:onafterAirbaseRecaptured(From, Event, To, Coalition) local text=string.format("Warehouse %s: We recaptured our airbase %s from the enemy (coalition=%d)!", self.alias, self.airbasename, Coalition) self:_InfoMessage(text) - -- Set airbase and category. + -- Set airbase and category. self.airbase=AIRBASE:FindByName(self.airbasename) - + -- Debug smoke. if self.Debug then if Coalition==coalition.side.RED then @@ -4806,7 +4849,7 @@ function WAREHOUSE:onafterAirbaseRecaptured(From, Event, To, Coalition) self.airbase:GetCoordinate():SmokeBlue() end end - + end @@ -4834,7 +4877,7 @@ function WAREHOUSE:onafterDestroyed(From, Event, To) -- Message. local text=string.format("Warehouse %s was destroyed! Assets lost %d.", self.alias, #self.stock) self:_InfoMessage(text) - + -- Remove all table entries from waiting queue and stock. for k,_ in pairs(self.queue) do self.queue[k]=nil @@ -4866,32 +4909,32 @@ function WAREHOUSE:onafterSave(From, Event, To, path, filename) f:write(data) f:close() end - + -- Set file name. filename=filename or string.format("WAREHOUSE-%d_%s.txt", self.uid, self.alias) - + -- Set path. if path~=nil then filename=path.."\\"..filename end - + -- Info local text=string.format("Saving warehouse assets to file %s", filename) MESSAGE:New(text,30):ToAllIf(self.Debug or self.Report) self:I(self.wid..text) - + local warehouseassets="" warehouseassets=warehouseassets..string.format("coalition=%d\n", self:GetCoalition()) warehouseassets=warehouseassets..string.format("country=%d\n", self:GetCountry()) - + -- Loop over all assets in stock. for _,_asset in pairs(self.stock) do local asset=_asset -- #WAREHOUSE.Assetitem - + -- Loop over asset parameters. local assetstring="" for key,value in pairs(asset) do - + -- Only save keys which are needed to restore the asset. if key=="templatename" or key=="attribute" or key=="cargobay" or key=="weight" or key=="loadradius" or key=="livery" or key=="skill" or key=="assignment" then local name @@ -4904,13 +4947,13 @@ function WAREHOUSE:onafterSave(From, Event, To, path, filename) end self:I(string.format("Loaded asset: %s", assetstring)) end - + -- Add asset string. warehouseassets=warehouseassets..assetstring.."\n" end -- Save file. - _savefile(filename, warehouseassets) + _savefile(filename, warehouseassets) end @@ -4927,25 +4970,25 @@ function WAREHOUSE:onbeforeLoad(From, Event, To, path, filename) local function _fileexists(name) local f=io.open(name,"r") - if f~=nil then + if f~=nil then io.close(f) - return true - else + return true + else return false end end -- Set file name. filename=filename or string.format("WAREHOUSE-%d_%s.txt", self.uid, self.alias) - + -- Set path. if path~=nil then filename=path.."\\"..filename end - + -- Check if file exists. local exists=_fileexists(filename) - + if exists then return true else @@ -4974,40 +5017,40 @@ function WAREHOUSE:onafterLoad(From, Event, To, path, filename) -- Set file name. filename=filename or string.format("WAREHOUSE-%d_%s.txt", self.uid, self.alias) - + -- Set path. if path~=nil then filename=path.."\\"..filename end - + -- Info local text=string.format("Loading warehouse assets from file %s", filename) MESSAGE:New(text,30):ToAllIf(self.Debug or self.Report) - self:I(self.wid..text) + self:I(self.wid..text) -- Load asset data from file. local data=_loadfile(filename) -- Split by line break. local assetdata=UTILS.Split(data,"\n") - + -- Coalition and coutrny. local Coalition local Country - + -- Loop over asset lines. local assets={} for _,asset in pairs(assetdata) do - + -- Parameters are separated by semi-colons local descriptors=UTILS.Split(asset,";") - + local asset={} local isasset=false for _,descriptor in pairs(descriptors) do - + local keyval=UTILS.Split(descriptor,"=") - + if #keyval==2 then if keyval[1]=="coalition" then @@ -5017,20 +5060,20 @@ function WAREHOUSE:onafterLoad(From, Event, To, path, filename) -- Get country id. Country=tonumber(keyval[2]) else - + -- This is an asset. isasset=true - + local key=keyval[1] local val=keyval[2] - - --env.info(string.format("FF asset key=%s val=%s", key, val)) - + + --env.info(string.format("FF asset key=%s val=%s", key, val)) + -- Livery or skill could be "nil". if val=="nil" then val=nil - end - + end + -- Convert string to number where necessary. if key=="cargobay" or key=="weight" or key=="loadradius" then asset[key]=tonumber(val) @@ -5038,25 +5081,25 @@ function WAREHOUSE:onafterLoad(From, Event, To, path, filename) asset[key]=val end end - + end end - + -- Add to table. if isasset then table.insert(assets, asset) end end - + -- Respawn warehouse with prev coalition if necessary. if Country~=self:GetCountry() then self:T(self.wid..string.format("Changing warehouse country %d-->%d on loading assets.", self:GetCountry(), Country)) self:ChangeCountry(Country) end - + for _,_asset in pairs(assets) do local asset=_asset --#WAREHOUSE.Assetitem - + local group=GROUP:FindByName(asset.templatename) if group then self:AddAsset(group, 1, asset.attribute, asset.cargobay, asset.weight, asset.loadradius, asset.skill, asset.livery, asset.assignment) @@ -5078,67 +5121,67 @@ end function WAREHOUSE:_SpawnAssetRequest(Request) self:F2({requestUID=Request.uid}) - -- Shortcut to cargo assets. + -- Shortcut to cargo assets. local _assetstock=Request.cargoassets -- General type and category. local _cargotype=Request.cargoattribute --#WAREHOUSE.Attribute local _cargocategory=Request.cargocategory --DCS#Group.Category - + -- Now we try to find all parking spots for all cargo groups in advance. Due to the for loop, the parking spots do not get updated while spawning. local Parking={} if _cargocategory==Group.Category.AIRPLANE or _cargocategory==Group.Category.HELICOPTER then Parking=self:_FindParkingForAssets(self.airbase,_assetstock) or {} end - + -- Spawn aircraft in uncontrolled state. local UnControlled=true - + -- Create an empty group set. local _groupset=SET_GROUP:New() -- Table for all spawned assets. local _assets={} - + -- Loop over cargo requests. for i=1,#_assetstock do -- Get stock item. local _assetitem=_assetstock[i] --#WAREHOUSE.Assetitem - + -- Alias of the group. local _alias=self:_Alias(_assetitem, Request) -- Spawn an asset group. - local _group=nil --Wrapper.Group#GROUP + local _group=nil --Wrapper.Group#GROUP if _assetitem.category==Group.Category.GROUND then - - -- Spawn ground troops. + + -- Spawn ground troops. _group=self:_SpawnAssetGroundNaval(_alias,_assetitem, Request, self.spawnzone) - + elseif _assetitem.category==Group.Category.AIRPLANE or _assetitem.category==Group.Category.HELICOPTER then - + -- Spawn air units. if Parking[_assetitem.uid] then _group=self:_SpawnAssetAircraft(_alias,_assetitem, Request, Parking[_assetitem.uid], UnControlled) else _group=self:_SpawnAssetAircraft(_alias,_assetitem, Request, nil, UnControlled) end - + elseif _assetitem.category==Group.Category.TRAIN then - + -- Spawn train. if self.rail then --TODO: Rail should only get one asset because they would spawn on top! end - + self:E(self.wid.."ERROR: Spawning of TRAIN assets not possible yet!") - + elseif _assetitem.category==Group.Category.SHIP then - + -- Spawn naval assets. _group=self:_SpawnAssetGroundNaval(_alias,_assetitem, Request, self.portzone) - + else self:E(self.wid.."ERROR: Unknown asset category!") end @@ -5146,11 +5189,11 @@ function WAREHOUSE:_SpawnAssetRequest(Request) -- Add group to group set and asset list. if _group then _groupset:AddGroup(_group) - table.insert(_assets, _assetitem) + table.insert(_assets, _assetitem) else self:E(self.wid.."ERROR: Cargo asset could not be spawned!") end - + end -- Delete spawned items from warehouse stock. @@ -5159,12 +5202,12 @@ function WAREHOUSE:_SpawnAssetRequest(Request) Request.assets[asset.uid]=asset self:_DeleteStockItem(asset) end - + -- Overwrite the assets with the actually spawned ones. Request.cargoassets=_assets return _groupset -end +end --- Spawn a ground or naval asset in the corresponding spawn zone of the warehouse. @@ -5178,22 +5221,22 @@ end function WAREHOUSE:_SpawnAssetGroundNaval(alias, asset, request, spawnzone, aioff) if asset and (asset.category==Group.Category.GROUND or asset.category==Group.Category.SHIP) then - + -- Prepare spawn template. - local template=self:_SpawnAssetPrepareTemplate(asset, alias) - + local template=self:_SpawnAssetPrepareTemplate(asset, alias) + -- Initial spawn point. - template.route.points[1]={} - + template.route.points[1]={} + -- Get a random coordinate in the spawn zone. local coord=spawnzone:GetRandomCoordinate() -- Translate the position of the units. for i=1,#template.units do - + -- Unit template. local unit = template.units[i] - + -- Translate position. local SX = unit.x or 0 local SY = unit.y or 0 @@ -5201,40 +5244,40 @@ function WAREHOUSE:_SpawnAssetGroundNaval(alias, asset, request, spawnzone, aiof local BY = asset.template.route.points[1].y local TX = coord.x + (SX-BX) local TY = coord.z + (SY-BY) - + template.units[i].x = TX template.units[i].y = TY - + if asset.livery then unit.livery_id = asset.livery end if asset.skill then unit.skill= asset.skill end - + end - + template.route.points[1].x = coord.x template.route.points[1].y = coord.z - + template.x = coord.x template.y = coord.z template.alt = coord.y - + -- 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 - + return nil end @@ -5250,55 +5293,55 @@ end function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrolled, hotstart) 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) - + -- Set route points. if request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then - + -- Get flight path if the group goes to another warehouse by itself. template.route.points=self:_GetFlightplan(asset, self.airbase, request.warehouse.airbase) - + 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") - + end - + -- Get airbase ID and category. local AirbaseID = self.airbase:GetID() local AirbaseCategory = self:GetAirbaseCategory() - + -- Check enough parking spots. if AirbaseCategory==Airbase.Category.HELIPAD or AirbaseCategory==Airbase.Category.SHIP then - + --TODO Figure out what's necessary in this case. - + else - + if #parking<#template.units 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 end - + end - + -- Position the units. for i=1,#template.units do - + -- Unit template. local unit = template.units[i] @@ -5306,67 +5349,67 @@ function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrol -- Helipads we take the position of the airbase location, since the exact location of the spawn point does not make sense. local coord=self.airbase:GetCoordinate() - + unit.x=coord.x unit.y=coord.z unit.alt=coord.y - + unit.parking_id = nil unit.parking = nil - + else - + local coord=parking[i].Coordinate --Core.Point#COORDINATE local terminal=parking[i].TerminalID --#number - + if self.Debug then coord:MarkToAll(string.format("Spawnplace unit %s terminal %d.", unit.name, terminal)) end - + unit.x=coord.x unit.y=coord.z unit.alt=coord.y - + unit.parking_id = nil unit.parking = terminal - + end - + if asset.livery then unit.livery_id = asset.livery end if asset.skill then unit.skill= asset.skill end - + end - + -- And template position. template.x = template.units[1].x template.y = template.units[1].y - + -- DCS bug workaround. Spawning helos in uncontrolled state on carriers causes a big spash! -- See https://forums.eagle.ru/showthread.php?t=219550 -- Should be solved in latest OB update 2.5.3.21708 --if AirbaseCategory == Airbase.Category.SHIP and asset.category==Group.Category.HELICOPTER then -- uncontrolled=false --end - + -- Uncontrolled spawning. template.uncontrolled=uncontrolled - + -- Debug info. self:T2({airtemplate=template}) - + -- Spawn group. local group=_DATABASE:Spawn(template) --Wrapper.Group#GROUP - + -- Activate group - should only be necessary for late activated groups. --group:Activate() - + return group end - + return nil end @@ -5380,14 +5423,14 @@ function WAREHOUSE:_SpawnAssetPrepareTemplate(asset, alias) -- Create an own copy of the template! local template=UTILS.DeepCopy(asset.template) - + -- Set unique name. template.name=alias - - -- Set current(!) coalition and country. + + -- Set current(!) coalition and country. template.CoalitionID=self:GetCoalition() template.CountryID=self:GetCountry() - + -- Nillify the group ID. template.groupId=nil @@ -5395,7 +5438,7 @@ function WAREHOUSE:_SpawnAssetPrepareTemplate(asset, alias) if asset.category==Group.Category.GROUND then --template.visible=false end - + -- No late activation. template.lateActivation=false @@ -5406,16 +5449,16 @@ function WAREHOUSE:_SpawnAssetPrepareTemplate(asset, alias) -- Handle units. for i=1,#template.units do - + -- Unit template. local unit = template.units[i] - + -- Nillify the unit ID. unit.unitId=nil - + -- Set unit name: -01, -02, ... unit.name=string.format("%s-%02d", template.name , i) - + end return template @@ -5437,50 +5480,50 @@ function WAREHOUSE:_RouteGround(group, request) -- Set speed to 70% of max possible. local _speed=group:GetSpeedMax()*0.7 - + -- Route waypoints. local Waypoints={} - - -- Check if an off road path has been defined. + + -- Check if an off road path has been defined. local hasoffroad=self:HasConnectionOffRoad(request.warehouse, self.Debug) - + -- Check if any off road paths have be defined. They have priority! if hasoffroad then -- Get off road path to remote warehouse. If more have been defined, pick one randomly. local remotename=request.warehouse.warehouse:GetName() local path=self.offroadpaths[remotename][math.random(#self.offroadpaths[remotename])] - + -- Loop over user defined shipping lanes. for i=1,#path do - + -- Shortcut and coordinate intellisense. local coord=path[i] --Core.Point#COORDINATE - + -- Get waypoint for coordinate. local Waypoint=coord:WaypointGround(_speed, "Off Road") - + -- Add waypoint to route. - table.insert(Waypoints, Waypoint) - end - + table.insert(Waypoints, Waypoint) + end + else - + -- Waypoints for road-to-road connection. Waypoints = group:TaskGroundOnRoad(request.warehouse.road, _speed, "Off Road", false, self.road) - + -- First waypoint = current position of the group. local FromWP=group:GetCoordinate():WaypointGround(_speed, "Off Road") table.insert(Waypoints, 1, FromWP) - + -- Final coordinate. local ToWP=request.warehouse.spawnzone:GetRandomCoordinate():WaypointGround(_speed, "Off Road") table.insert(Waypoints, #Waypoints+1, ToWP) - + end - + -- Task function triggering the arrived event at the last waypoint. - local TaskFunction = self:_SimpleTaskFunction("warehouse:_Arrived", group) + local TaskFunction = self:_SimpleTaskFunction("warehouse:_Arrived", group) -- Put task function on last waypoint. local Waypoint = Waypoints[#Waypoints] @@ -5488,7 +5531,7 @@ function WAREHOUSE:_RouteGround(group, request) -- Route group to destination. group:Route(Waypoints, 1) - + -- Set ROE and alaram state. group:OptionROEReturnFire() group:OptionAlarmStateGreen() @@ -5506,47 +5549,47 @@ function WAREHOUSE:_RouteNaval(group, request) -- Set speed to 80% of max possible. local _speed=group:GetSpeedMax()*0.8 - + -- Get shipping lane to remote warehouse. If more have been defined, pick one randomly. local remotename=request.warehouse.warehouse:GetName() local lane=self.shippinglanes[remotename][math.random(#self.shippinglanes[remotename])] - + if lane then - + -- Route waypoints. local Waypoints={} - + -- Loop over user defined shipping lanes. for i=1,#lane do - + -- Shortcut and coordinate intellisense. local coord=lane[i] --Core.Point#COORDINATE - + -- Get waypoint for coordinate. local Waypoint=coord:WaypointGround(_speed) - + -- Add waypoint to route. - table.insert(Waypoints, Waypoint) + table.insert(Waypoints, Waypoint) 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) - + group:Route(Waypoints, 1) + -- Set ROE (Naval units dont have and alaram state.) group:OptionROEReturnFire() - + else -- This should not happen! Existance of shipping lane was checked before executing this request. self:E(self.wid..string.format("ERROR: No shipping lane defined for Naval asset!")) end - + end end @@ -5558,21 +5601,21 @@ end function WAREHOUSE:_RouteAir(aircraft) if aircraft and aircraft:IsAlive()~=nil then - + -- Debug info. self:T2(self.wid..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) - + -- Debug info. self:T2(self.wid..string.format("RouteAir aircraft group %s alive=%s (after start command)", aircraft:GetName(), tostring(aircraft:IsAlive()))) - + -- Set ROE and alaram state. aircraft:OptionROEReturnFire() aircraft:OptionROTPassiveDefense() - + else self:E(string.format("ERROR: aircraft %s cannot be routed since it does not exist or is not alive %s!", tostring(aircraft:GetName()), tostring(aircraft:IsAlive()))) end @@ -5609,12 +5652,12 @@ end -- @param Wrapper.Group#GROUP group The group that arrived. function WAREHOUSE:_Arrived(group) self:_DebugMessage(string.format("Group %s arrived!", tostring(group:GetName()))) - + if group then --Trigger "Arrived event. self:__Arrived(1, group) end - + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -5626,7 +5669,7 @@ end -- @param Core.Event#EVENTDATA EventData Event data. function WAREHOUSE:_OnEventBirth(EventData) self:T3(self.wid..string.format("Warehouse %s (id=%s) captured event birth!", self.alias, self.uid)) - + if EventData and EventData.IniGroup then local group=EventData.IniGroup -- Note: Remember, group:IsAlive might(?) not return true here. @@ -5653,7 +5696,7 @@ function WAREHOUSE:_OnEventEngineStartup(EventData) if wid==self.uid then self:T(self.wid..string.format("Warehouse %s captured event engine startup of its asset unit %s.", self.alias, EventData.IniUnitName)) end - end + end end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -5663,14 +5706,14 @@ end -- @param Core.Event#EVENTDATA EventData Event data. function WAREHOUSE:_OnEventTakeOff(EventData) self:T3(self.wid..string.format("Warehouse %s captured event takeoff!",self.alias)) - + if EventData and EventData.IniGroup then local group=EventData.IniGroup local wid,aid,rid=self:_GetIDsFromGroup(group) if wid==self.uid then self:T(self.wid..string.format("Warehouse %s captured event takeoff of its asset unit %s.", self.alias, EventData.IniUnitName)) end - end + end end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -5680,19 +5723,19 @@ end -- @param Core.Event#EVENTDATA EventData Event data. function WAREHOUSE:_OnEventLanding(EventData) self:T3(self.wid..string.format("Warehouse %s captured event landing!", self.alias)) - + if EventData and EventData.IniGroup then local group=EventData.IniGroup - + -- Try to get UIDs from group name. local wid,aid,rid=self:_GetIDsFromGroup(group) - + -- Check that this group belongs to this warehouse. if wid~=nil and wid==self.uid then - + -- Debug info. self:T(self.wid..string.format("Warehouse %s captured event landing of its asset unit %s.", self.alias, EventData.IniUnitName)) - + end end end @@ -5704,14 +5747,14 @@ end -- @param Core.Event#EVENTDATA EventData Event data. function WAREHOUSE:_OnEventEngineShutdown(EventData) self:T3(self.wid..string.format("Warehouse %s captured event engine shutdown!", self.alias)) - + if EventData and EventData.IniGroup then local group=EventData.IniGroup local wid,aid,rid=self:_GetIDsFromGroup(group) if wid==self.uid then self:T(self.wid..string.format("Warehouse %s captured event engine shutdown of its asset unit %s.", self.alias, EventData.IniUnitName)) end - end + end end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -5722,49 +5765,49 @@ end function WAREHOUSE:_OnEventArrived(EventData) if EventData and EventData.IniUnit then - + -- Unit that arrived. local unit=EventData.IniUnit - + -- Check if unit is alive and on the ground. Engine shutdown can also be triggered in other situations! if unit and unit:IsAlive()==true and unit:InAir()==false then - + -- Get group. local group=EventData.IniGroup - - -- Get unique IDs from group name. + + -- Get unique IDs from group name. local wid,aid,rid=self:_GetIDsFromGroup(group) - + -- If all IDs are good we can assume it is a warehouse asset. if wid~=nil and aid~=nil and rid~=nil then - + -- Check that warehouse ID is right. if self.uid==wid then - + local request=self:_GetRequestOfGroup(group, self.pending) local istransport=self:_GroupIsTransport(group,request) - + -- Check if engine shutdown happend at right airbase because the event is also triggered in other situations. local rightairbase=group:GetCoordinate():GetClosestAirbase():GetName()==request.warehouse:GetAirbase():GetName() - + -- Check that group is cargo and not transport. - if istransport==false and rightairbase then - + 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. self:__Arrived(dt, group) - + end - + end - + else self:T3(string.format("Group that arrived did not belong to a warehouse. Warehouse ID=%s, Asset ID=%s, Request ID=%s.", tostring(wid), tostring(aid), tostring(rid))) end @@ -5780,52 +5823,52 @@ end -- @param Core.Event#EVENTDATA EventData Event data. function WAREHOUSE:_OnEventCrashOrDead(EventData) self:T3(self.wid..string.format("Warehouse %s captured event dead or crash!", self.alias)) - + if EventData then - + -- Check if warehouse was destroyed. We compare the name of the destroyed unit. - if EventData.IniUnitName then + if EventData.IniUnitName then local warehousename=self.warehouse:GetName() if EventData.IniUnitName==warehousename then self:_DebugMessage(string.format("Warehouse %s alias %s was destroyed!", warehousename, self.alias)) - + -- Trigger Destroyed event. self:Destroyed() end end - + --self:I(self.wid..string.format("Warehouse %s captured event dead or crash or unit %s.", self.alias, tostring(EventData.IniUnitName))) - - -- Check if an asset unit was destroyed. + + -- Check if an asset unit was destroyed. if EventData.IniGroup then - - -- Group initiating the event. + + -- Group initiating the event. local group=EventData.IniGroup - + -- Get warehouse, asset and request IDs from the group name. local wid,aid,rid=self:_GetIDsFromGroup(group) - + -- Check that we have the right warehouse. if wid==self.uid then - + -- Debug message. self:T(self.wid..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 local request=request --#WAREHOUSE.Pendingitem - + -- This is the right request. 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) - - end + + end end end end - end + end end --- A unit of a group just died. Update group sets in request. @@ -5837,10 +5880,10 @@ function WAREHOUSE:_UnitDead(deadunit, request) -- Flare unit deadunit:FlareRed() - + -- Group the dead unit belongs to. local group=deadunit:GetGroup() - + -- Check if this was the last unit of the group ==> whole group dead. local groupdead=true local nunits=0 @@ -5849,17 +5892,17 @@ function WAREHOUSE:_UnitDead(deadunit, request) -- Get current size of group and substract the unit that just died because it is not counted yet! nunits=group:GetSize()-1 nunits0=group:GetInitialSize() - + if nunits > 0 then groupdead=false - end + end end - - + + -- 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) - + -- Debug message. local text=string.format("Unit %s died! #units=%d/%d ==> Group dead=%s (IsAlive=%s).", unitname, nunits, nunits0, tostring(groupdead), tostring(group:IsAlive())) self:T2(self.wid..text) @@ -5868,7 +5911,7 @@ function WAREHOUSE:_UnitDead(deadunit, request) if nunits<0 then self:E(self.wid.."ERROR: Number of units negative! This should not happen.") end - + -- Group is dead! if groupdead then self:T(self.wid..string.format("Group %s (transport=%s) is dead!", groupname, tostring(self:_GroupIsTransport(group,request)))) @@ -5879,57 +5922,57 @@ function WAREHOUSE:_UnitDead(deadunit, request) local asset=self:FindAssetInDB(group) self:AssetDead(asset, request) end - - + + -- Not sure what this does actually and if it would be better to set it to true. local NoTriggerEvent=true - + if request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then - + --- -- Easy case: Group can simply be removed from the cargogroupset. --- - + -- Remove dead group from carg group set. if groupdead==true then request.cargogroupset:Remove(groupname, NoTriggerEvent) self:T(self.wid..string.format("Removed selfpropelled cargo %s: ncargo=%d.", groupname, request.cargogroupset:Count())) end - + else - + --- - -- Complicated case: Dead unit could be: + -- 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=self:_GroupIsTransport(group,request) - + if istransport==true then - + -- Get the carrier unit table holding the cargo groups inside this carrier. local cargogroupnames=request.carriercargo[unitname] - + if cargogroupnames then - + -- Loop over all groups inside the destroyed carrier ==> all dead. for _,cargoname in pairs(cargogroupnames) do request.cargogroupset:Remove(cargoname, NoTriggerEvent) self:T(self.wid..string.format("Removed transported cargo %s inside dead carrier %s: ncargo=%d", cargoname, unitname, request.cargogroupset:Count())) end - + end - + -- Whole carrier group is dead. Remove it from the carrier group set. if groupdead then request.transportgroupset:Remove(groupname, NoTriggerEvent) self:T(self.wid..string.format("Removed transport %s: ntransport=%d", groupname, request.transportgroupset:Count())) - end - + 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 @@ -5938,12 +5981,12 @@ function WAREHOUSE:_UnitDead(deadunit, request) -- This as well? --request.transportcargoset:RemoveCargosByName(RemoveCargoNames) end - - else + + else self:E(self.wid..string.format("ERROR: Group %s is neither cargo nor transport!", group:GetName())) end end - + end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -5954,29 +5997,29 @@ end -- @param Core.Event#EVENTDATA EventData Event data. function WAREHOUSE:_OnEventBaseCaptured(EventData) self:T3(self.wid..string.format("Warehouse %s captured event base captured!",self.alias)) - + -- This warehouse does not have an airbase and never had one. So it could not have been captured. if self.airbasename==nil then return end - + if EventData and EventData.Place then - + -- Place is the airbase that was captured. local airbase=EventData.Place --Wrapper.Airbase#AIRBASE - + -- Check that this airbase belongs or did belong to this warehouse. if EventData.PlaceName==self.airbasename then - + -- New coalition of airbase after it was captured. local NewCoalitionAirbase=airbase:GetCoalition() - + -- Debug info self:T(self.wid..string.format("Airbase of warehouse %s (coalition ID=%d) was captured! New owner coalition ID=%d.",self.alias, self:GetCoalition(), NewCoalitionAirbase)) - + -- So what can happen? -- Warehouse is blue, airbase is blue and belongs to warehouse and red captures it ==> self.airbase=nil - -- Warehouse is blue, airbase is blue self.airbase is nil and blue (re-)captures it ==> self.airbase=Event.Place + -- Warehouse is blue, airbase is blue self.airbase is nil and blue (re-)captures it ==> self.airbase=Event.Place if self.airbase==nil then -- New coalition is the same as of the warehouse ==> warehouse previously lost this airbase and now it was re-captured. if NewCoalitionAirbase == self:GetCoalition() then @@ -5988,7 +6031,7 @@ function WAREHOUSE:_OnEventBaseCaptured(EventData) self:AirbaseCaptured(NewCoalitionAirbase) end end - + end end end @@ -5999,7 +6042,7 @@ end -- @param Core.Event#EVENTDATA EventData Event data. function WAREHOUSE:_OnEventMissionEnd(EventData) self:T3(self.wid..string.format("Warehouse %s captured event mission end!",self.alias)) - + if self.autosave then self:Save(self.autosavepath, self.autosavefile) end @@ -6016,35 +6059,35 @@ function WAREHOUSE:_CheckConquered() -- Get coordinate and radius to check. local coord=self.zone:GetCoordinate() local radius=self.zone:GetRadius() - + -- Scan units in zone. local gotunits,_,_,units,_,_=coord:ScanObjects(radius, true, false, false) - + local Nblue=0 local Nred=0 local Nneutral=0 - + local CountryBlue=nil local CountryRed=nil local CountryNeutral=nil - + if gotunits then -- Loop over all units. for _,_unit in pairs(units) do local unit=_unit --Wrapper.Unit#UNIT - + local distance=coord:Get2DDistance(unit:GetCoordinate()) - + -- Filter only alive groud units. Also check distance again, because the scan routine might give some larger distances. if unit:IsGround() and unit:IsAlive() and distance <= radius then - + -- Get coalition and country. local _coalition=unit:GetCoalition() local _country=unit:GetCountry() - + -- Debug info. self:T2(self.wid..string.format("Unit %s in warehouse zone of radius=%d m. Coalition=%d, country=%d. Distance = %d m.",unit:GetName(), radius,_coalition,_country, distance)) - + -- Add up units for each side. if _coalition==coalition.side.BLUE then Nblue=Nblue+1 @@ -6056,15 +6099,15 @@ function WAREHOUSE:_CheckConquered() Nneutral=Nneutral+1 CountryNeutral=_country end - - end + + end end end - + -- Debug info. self:T(self.wid..string.format("Ground troops in warehouse zone: blue=%d, red=%d, neutral=%d", Nblue, Nred, Nneutral)) - - + + -- Figure out the new coalition if any. -- Condition is that only units of one coalition are within the zone. local newcoalition=self:GetCoalition() @@ -6088,7 +6131,7 @@ function WAREHOUSE:_CheckConquered() self:Captured(newcoalition, newcountry) return end - + -- Before a warehouse can be captured, it has to be attacked. -- That is, even if only enemy units are present it is not immediately captured in order to spawn all ground assets for defence. if self:GetCoalition()==coalition.side.BLUE then @@ -6099,7 +6142,7 @@ function WAREHOUSE:_CheckConquered() -- Blue warehouse was under attack by blue but no more blue units in zone. if self:IsAttacked() and Nred==0 then self:Defeated() - end + end elseif self:GetCoalition()==coalition.side.RED then -- Red Warehouse is running and we have blue units in the zone. if self:IsRunning() and Nblue>0 then @@ -6117,7 +6160,7 @@ function WAREHOUSE:_CheckConquered() self:Attacked(coalition.side.BLUE, CountryBlue) end end - + end --- Checks if the associated airbase still belongs to the warehouse. @@ -6125,26 +6168,26 @@ end function WAREHOUSE:_CheckAirbaseOwner() -- The airbasename is set at start and not deleted if the airbase was captured. if self.airbasename then - + local airbase=AIRBASE:FindByName(self.airbasename) local airbasecurrentcoalition=airbase:GetCoalition() - + if self.airbase then - + -- Warehouse has lost its airbase. if self:GetCoalition()~=airbasecurrentcoalition then self.airbase=nil end - + else - + -- Warehouse has re-captured the airbase. if self:GetCoalition()==airbasecurrentcoalition then self.airbase=airbase - end - + end + end - + end end @@ -6158,47 +6201,47 @@ function WAREHOUSE:_CheckRequestConsistancy(queue) -- Requests to delete. local invalid={} - + for _,_request in pairs(queue) do local request=_request --#WAREHOUSE.Queueitem - + -- Debug info. self:T2(self.wid..string.format("Checking request id=%d.", request.uid)) - + -- Let's assume everything is fine. local valid=true - + -- Check if at least one asset was requested. if request.nasset==0 then self:E(self.wid..string.format("ERROR: INVALID request. Request for zero assets not possible. Can happen when, e.g. \"all\" ground assets are requests but none in stock.")) valid=false end - + -- Request from enemy coalition? if self:GetCoalition()~=request.warehouse:GetCoalition() then self:E(self.wid..string.format("ERROR: INVALID request. Requesting warehouse is of wrong coaltion! Own coalition %s != %s of requesting warehouse.", self:GetCoalitionName(), request.warehouse:GetCoalitionName())) valid=false end - + -- Is receiving warehouse stopped? if request.warehouse:IsStopped() then self:E(self.wid..string.format("ERROR: INVALID request. Requesting warehouse is stopped!")) - valid=false + valid=false end -- Is receiving warehouse destroyed? if request.warehouse:IsDestroyed() then self:E(self.wid..string.format("ERROR: INVALID request. Requesting warehouse is destroyed!")) - valid=false + valid=false end - + -- Add request as unvalid and delete it later. if valid==false then self:E(self.wid..string.format("Got invalid request id=%d.", request.uid)) - table.insert(invalid, request) + table.insert(invalid, request) else self:T3(self.wid..string.format("Got valid request id=%d.", request.uid)) - end + end end -- Delete invalid requests. @@ -6206,7 +6249,7 @@ function WAREHOUSE:_CheckRequestConsistancy(queue) self:E(self.wid..string.format("Deleting INVALID request id=%d.",_request.uid)) self:_DeleteQueueItem(_request, self.queue) end - + end --- Check if a request is valid in general. If not, it will be removed from the queue. @@ -6219,12 +6262,12 @@ function WAREHOUSE:_CheckRequestValid(request) -- Check if number of requested assets is in stock. local _assets,_nassets,_enough=self:_FilterStock(self.stock, request.assetdesc, request.assetdescval, request.nasset) - + -- No assets in stock? Checks cannot be performed. if #_assets==0 then return true end - + -- Convert relative to absolute number if necessary. local nasset=request.nasset if type(request.nasset)=="string" then @@ -6234,10 +6277,10 @@ function WAREHOUSE:_CheckRequestValid(request) -- Debug check, request.nasset might be a string Quantity enumerator. local text=string.format("Request valid? Number of assets: requested=%s=%d, selected=%d, total=%d, enough=%s.", tostring(request.nasset), nasset,#_assets,_nassets, tostring(_enough)) self:T(text) - + -- First asset. Is representative for all filtered items in stock. local asset=_assets[1] --#WAREHOUSE.Assetitem - + -- Asset is air, ground etc. local asset_plane = asset.category==Group.Category.AIRPLANE local asset_helo = asset.category==Group.Category.HELICOPTER @@ -6250,158 +6293,159 @@ function WAREHOUSE:_CheckRequestValid(request) -- Assume everything is okay. local valid=true - + -- Category of the requesting warehouse airbase. local requestcategory=request.warehouse:GetAirbaseCategory() - + if request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then ------------------------------------------- -- Case where the units go my themselves -- ------------------------------------------- if asset_air then - + if asset_plane then - + -- No airplane to or from FARPS. if requestcategory==Airbase.Category.HELIPAD or self:GetAirbaseCategory()==Airbase.Category.HELIPAD then self:E("ERROR: Incorrect request. Asset airplane requested but warehouse or requestor is HELIPAD/FARP!") valid=false end - + -- Category SHIP is not general enough! Fighters can go to carriers. Which fighters, is there an attibute? -- Also for carriers, attibute? - + elseif asset_helo then - + -- Helos need a FARP or AIRBASE or SHIP for spawning. Also at the the receiving warehouse. So even if they could go there they "cannot" be spawned again. -- Unless I allow spawning of helos in the the spawn zone. But one should place at least a FARP there. if self:GetAirbaseCategory()==-1 or requestcategory==-1 then self:E("ERROR: Incorrect request. Helos need a AIRBASE/HELIPAD/SHIP as home/destination base!") - valid=false + valid=false end - + end - + -- All aircraft need an airbase of any type at depature and destination. if self.airbase==nil or request.airbase==nil then - + self:E("ERROR: Incorrect request. Either warehouse or requesting warehouse does not have any kind of airbase!") valid=false - + else - + -- Check if enough parking spots are available. This checks the spots available in general, i.e. not the free spots. -- TODO: For FARPS/ships, is it possible to send more assets than parking spots? E.g. a FARPS has only four (or even one). -- TODO: maybe only check if spots > 0 for the necessary terminal type? At least for FARPS. - + -- Get necessary terminal type. - local termtype=self:_GetTerminal(asset.attribute) - + local termtype_dep=self:_GetTerminal(asset.attribute, self:GetAirbaseCategory()) + local termtype_des=self:_GetTerminal(asset.attribute, request.warehouse:GetAirbaseCategory()) + -- Get number of parking spots. - local np_departure=self.airbase:GetParkingSpotsNumber(termtype) - local np_destination=request.airbase:GetParkingSpotsNumber(termtype) - + local np_departure=self.airbase:GetParkingSpotsNumber(termtype_dep) + local np_destination=request.airbase:GetParkingSpotsNumber(termtype_des) + -- Debug info. - self:T(string.format("Asset attribute = %s, terminal type = %d, spots at departure = %d, destination = %d", asset.attribute, termtype, np_departure, np_destination)) - + self:T(string.format("Asset attribute = %s, DEPARTURE: terminal type = %d, spots = %d, DESTINATION: terminal type = %d, spots = %d", asset.attribute, termtype_dep, np_departure, termtype_des, np_destination)) + -- Not enough parking at sending warehouse. --if (np_departure < request.nasset) and not (self.category==Airbase.Category.SHIP or self.category==Airbase.Category.HELIPAD) then if np_departure < nasset then - self:E(string.format("ERROR: Incorrect request. Not enough parking spots of terminal type %d at warehouse. Available spots %d < %d necessary.", termtype, np_departure, nasset)) - valid=false + self:E(string.format("ERROR: Incorrect request. Not enough parking spots of terminal type %d at warehouse. Available spots %d < %d necessary.", termtype_dep, np_departure, nasset)) + valid=false end -- No parking at requesting warehouse. if np_destination == 0 then - self:E(string.format("ERROR: Incorrect request. No parking spots of terminal type %d at requesting warehouse. Available spots = %d!", termtype, np_destination)) - valid=false - end - + self:E(string.format("ERROR: Incorrect request. No parking spots of terminal type %d at requesting warehouse. Available spots = %d!", termtype_des, np_destination)) + valid=false + end + end - + elseif asset_ground then - + -- Check that both spawn zones are not in water. local inwater=self.spawnzone:GetCoordinate():IsSurfaceTypeWater() or request.warehouse.spawnzone:GetCoordinate():IsSurfaceTypeWater() - + if inwater then self:E("ERROR: Incorrect request. Ground asset requested but at least one spawn zone is in water!") valid=false end - + -- No ground assets directly to or from ships. -- TODO: May needs refinement if warehouse is on land and requestor is ship in harbour?! --if (requestcategory==Airbase.Category.SHIP or self:GetAirbaseCategory()==Airbase.Category.SHIP) then -- self:E("ERROR: Incorrect request. Ground asset requested but warehouse or requestor is SHIP!") -- valid=false --end - + if asset_train then - + -- Check if there is a valid path on rail. local hasrail=self:HasConnectionRail(request.warehouse) if not hasrail then self:E("ERROR: Incorrect request. No valid path on rail for train assets!") valid=false end - + else - + if self.warehouse:GetName()~=request.warehouse.warehouse:GetName() then - + -- Check if there is a valid path on road. local hasroad=self:HasConnectionRoad(request.warehouse) - + -- Check if there is a valid off road path. local hasoffroad=self:HasConnectionOffRoad(request.warehouse) - + if not (hasroad or hasoffroad) then self:E("ERROR: Incorrect request. No valid path on or off road for ground assets!") valid=false end - + end - + end - + elseif asset_naval then - + -- Check shipping lane. local shippinglane=self:HasConnectionNaval(request.warehouse) - + if not shippinglane then self:E("ERROR: Incorrect request. No shipping lane has been defined between warehouses!") valid=false - end - + end + end - - else + + else ------------------------------- -- Assests need a transport --- ------------------------------- if request.transporttype==WAREHOUSE.TransportType.AIRPLANE then - + -- Airplanes only to AND from airdromes. if self:GetAirbaseCategory()~=Airbase.Category.AIRDROME or requestcategory~=Airbase.Category.AIRDROME then self:E("ERROR: Incorrect request. Warehouse or requestor does not have an airdrome. No transport by plane possible!") valid=false end - + --TODO: Not sure if there are any transport planes that can land on a carrier? - + elseif request.transporttype==WAREHOUSE.TransportType.APC then - + -- Transport by ground units. - + -- No transport to or from ships if self:GetAirbaseCategory()==Airbase.Category.SHIP or requestcategory==Airbase.Category.SHIP then self:E("ERROR: Incorrect request. Warehouse or requestor is SHIP. No transport by APC possible!") valid=false end - + -- Check if there is a valid path on road. local hasroad=self:HasConnectionRoad(request.warehouse) if not hasroad then @@ -6410,37 +6454,37 @@ function WAREHOUSE:_CheckRequestValid(request) end elseif request.transporttype==WAREHOUSE.TransportType.HELICOPTER then - + -- Transport by helicopters ==> need airbase for spawning but not for delivering to the spawn zone of the receiver. if self:GetAirbaseCategory()==-1 then self:E("ERROR: Incorrect request. Warehouse has no airbase. Transport by helicopter not possible!") valid=false end - + elseif request.transporttype==WAREHOUSE.TransportType.SHIP then - + -- Transport by ship. self:E("ERROR: Incorrect request. Transport by SHIP not implemented yet!") valid=false - + elseif request.transporttype==WAREHOUSE.TransportType.TRAIN then - + -- Transport by train. self:E("ERROR: Incorrect request. Transport by TRAIN not implemented yet!") valid=false - + else -- No match. self:E("ERROR: Incorrect request. Transport type unknown!") valid=false end - + -- Airborne assets: check parking situation. if request.transporttype==WAREHOUSE.TransportType.AIRPLANE or request.transporttype==WAREHOUSE.TransportType.HELICOPTER then - + -- Check if number of requested assets is in stock. local _assets,_nassets,_enough=self:_FilterStock(self.stock, WAREHOUSE.Descriptor.ATTRIBUTE, request.transporttype, request.ntransport) - + -- Convert relative to absolute number if necessary. local nasset=request.ntransport if type(request.ntransport)=="string" then @@ -6452,49 +6496,50 @@ function WAREHOUSE:_CheckRequestValid(request) self:T(text) -- Get necessary terminal type for helos or transport aircraft. - local termtype=self:_GetTerminal(request.transporttype) - + local termtype=self:_GetTerminal(request.transporttype, self:GetAirbaseCategory()) + -- Get number of parking spots. local np_departure=self.airbase:GetParkingSpotsNumber(termtype) - + -- Debug info. self:T(self.wid..string.format("Transport attribute = %s, terminal type = %d, spots at departure = %d.", request.transporttype, termtype, np_departure)) - + -- Not enough parking at sending warehouse. --if (np_departure < request.nasset) and not (self.category==Airbase.Category.SHIP or self.category==Airbase.Category.HELIPAD) then if np_departure < nasset then self:E(self.wid..string.format("ERROR: Incorrect request. Not enough parking spots of terminal type %d at warehouse. Available spots %d < %d necessary.", termtype, np_departure, nasset)) valid=false end - + -- Planes also need parking at the receiving warehouse. if request.transporttype==WAREHOUSE.TransportType.AIRPLANE then - + -- Total number of parking spots for transport planes at destination. + termtype=self:_GetTerminal(request.transporttype, request.warehouse:GetAirbaseCategory()) local np_destination=request.airbase:GetParkingSpotsNumber(termtype) -- Debug info. self:T(self.wid..string.format("Transport attribute = %s: total # of spots (type=%d) at destination = %d.", asset.attribute, termtype, np_destination)) - + -- No parking at requesting warehouse. if np_destination == 0 then self:E(string.format("ERROR: Incorrect request. No parking spots of terminal type %d at requesting warehouse for transports. Available spots = %d!", termtype, np_destination)) - valid=false + valid=false end end - + end - + end - + -- Add request as unvalid and delete it later. if valid==false then self:E(self.wid..string.format("ERROR: Got invalid request id=%d.", request.uid)) else self:T3(self.wid..string.format("Request id=%d valid :)", request.uid)) end - + return valid end @@ -6510,20 +6555,20 @@ function WAREHOUSE:_CheckRequestNow(request) if (request.warehouse:IsRunning()==false) and not (request.toself and self:IsAttacked()) then local text=string.format("Warehouse %s: Request denied! Receiving warehouse %s is not running. Current state %s.", self.alias, request.warehouse.alias, request.warehouse:GetState()) self:_InfoMessage(text, 5) - + return false end - + -- If no transport is requested, assets need to be mobile unless it is a self request. local onlymobile=false if type(request.transport)=="number" and request.ntransport==0 and not request.toself then onlymobile=true end - + -- Check if number of requested assets is in stock. local _assets,_nassets,_enough=self:_FilterStock(self.stock, request.assetdesc, request.assetdescval, request.nasset, onlymobile) - - + + -- Check if enough assets are in stock. if not _enough then local text=string.format("Warehouse %s: Request ID=%d denied! Not enough (cargo) assets currently available.", self.alias, request.uid) @@ -6532,107 +6577,107 @@ function WAREHOUSE:_CheckRequestNow(request) self:T(self.wid..text) return false end - + local _transports local _assetattribute local _assetcategory - + -- Check if at least one (cargo) asset is available. if _nassets>0 then -- Get the attibute of the requested asset. _assetattribute=_assets[1].attribute - _assetcategory=_assets[1].category - - -- Check available parking for air asset units. + _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) self:_InfoMessage(text, 5) - + return false end - + end - + -- Add this here or gettransport fails request.cargoassets=_assets - - end - + + end + -- Check that a transport units. if request.transporttype ~= WAREHOUSE.TransportType.SELFPROPELLED then -- Get best transports for this asset pack. _transports=self:_GetTransportsForAssets(request) - + -- Check if at least one transport asset is available. if #_transports>0 then - + -- Get the attibute of the transport units. local _transportattribute=_transports[1].attribute 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 end end - + else -- Not enough or the right transport carriers. local text=string.format("Warehouse %s: Request denied! Not enough transport carriers available at the moment.", self.alias) self:_InfoMessage(text, 5) - - return false - end + + return false + end else - + -- Self propelled case. Nothing to do for now. - + -- Ground asset checks. if _assetcategory==Group.Category.GROUND then - + -- Distance between warehouse and spawn zone. local dist=self.warehouse:GetCoordinate():Get2DDistance(self.spawnzone:GetCoordinate()) - + -- Check min dist to spawn zone. if dist>self.spawnzonemaxdist then -- Not close enough to spawn zone. local text=string.format("Warehouse %s: Request denied! Not close enough to spawn zone. Distance = %d m. We need to be at least within %d m range to spawn.", self.alias, dist, self.spawnzonemaxdist) - self:_InfoMessage(text, 5) + self:_InfoMessage(text, 5) return false end - + end - + end -- Set chosen cargo assets. request.cargoassets=_assets request.cargoattribute=_assets[1].attribute - request.cargocategory=_assets[1].category + request.cargocategory=_assets[1].category request.nasset=#_assets -- Debug info: - local text=string.format("Selected cargo assets, attibute=%s, category=%d:\n", request.cargoattribute, request.cargocategory) + local text=string.format("Selected cargo assets, attibute=%s, category=%d:\n", request.cargoattribute, request.cargocategory) for _i,_asset in pairs(_assets) do local asset=_asset --#WAREHOUSE.Assetitem text=text..string.format("%d) name=%s, type=%s, category=%d, #units=%d",_i, asset.templatename, asset.unittype, asset.category, asset.nunits) end - self:T(self.wid..text) + self:T(self.wid..text) if request.transporttype ~= WAREHOUSE.TransportType.SELFPROPELLED then @@ -6641,21 +6686,21 @@ function WAREHOUSE:_CheckRequestNow(request) request.transportattribute=_transports[1].attribute request.transportcategory=_transports[1].category request.ntransport=#_transports - + -- Debug info: - local text=string.format("Selected transport assets, attibute=%s, category=%d:\n", request.transportattribute, request.transportcategory) + local text=string.format("Selected transport assets, attibute=%s, category=%d:\n", request.transportattribute, request.transportcategory) for _i,_asset in pairs(_transports) do local asset=_asset --#WAREHOUSE.Assetitem text=text..string.format("%d) name=%s, type=%s, category=%d, #units=%d\n",_i, asset.templatename, asset.unittype, asset.category, asset.nunits) end self:T(self.wid..text) - + end - + return true end ----Get (optimized) transport carriers for the given assets to be transported. +---Get (optimized) transport carriers for the given assets to be transported. -- @param #WAREHOUSE self -- @param #WAREHOUSE.Pendingitem Chosen request. function WAREHOUSE:_GetTransportsForAssets(request) @@ -6668,29 +6713,29 @@ function WAREHOUSE:_GetTransportsForAssets(request) local cargoset=request.transportcargoset -- TODO: Get weight and cargo bay from CARGO_GROUP - --local cargogroup=CARGO_GROUP:New(CargoGroup,Type,Name,LoadRadius,NearRadius) + --local cargogroup=CARGO_GROUP:New(CargoGroup,Type,Name,LoadRadius,NearRadius) --cargogroup:GetWeight() - + -- Sort transport carriers w.r.t. cargo bay size. local function sort_transports(a,b) return a.cargobaymax>b.cargobaymax end - + -- Sort cargo assets w.r.t. weight in assending order. local function sort_cargoassets(a,b) return a.weight>b.weight end - + -- Sort tables. table.sort(transports, sort_transports) table.sort(cargoassets, sort_cargoassets) - + -- Total cargo bay size of all groups. self:T2(self.wid.."Transport capability:") local totalbay=0 for i=1,#transports do local transport=transports[i] --#WAREHOUSE.Assetitem - for j=1,transport.nunits do + for j=1,transport.nunits do totalbay=totalbay+transport.cargobay[j] self:T2(self.wid..string.format("Cargo bay = %d (unit=%d)", transport.cargobay[j], j)) end @@ -6704,91 +6749,91 @@ function WAREHOUSE:_GetTransportsForAssets(request) local asset=cargoassets[i] --#WAREHOUSE.Assetitem totalcargoweight=totalcargoweight+asset.weight self:T2(self.wid..string.format("weight = %d", asset.weight)) - end + end self:T2(self.wid..string.format("Total weight = %d", totalcargoweight)) - + -- Transports used. local used_transports={} - + -- Loop over all transport groups, largest cargobaymax to smallest. for i=1,#transports do - + -- Shortcut for carrier and cargo bay local transport=transports[i] - -- Cargo put into carrier. + -- Cargo put into carrier. local putintocarrier={} - + -- Cargo assigned to this transport group? local used=false - + -- Loop over all units for k=1,transport.nunits do - + -- Get cargo bay of this carrier. local cargobay=transport.cargobay[k] - + -- Loop over cargo assets. for j,asset in pairs(cargoassets) do local asset=asset --#WAREHOUSE.Assetitem - + -- How many times does the cargo fit into the carrier? local delta=cargobay-asset.weight --env.info(string.format("k=%d, j=%d delta=%d cargobay=%d weight=%d", k, j, delta, cargobay, asset.weight)) - + --self:E(self.wid..string.format("%s unit %d loads cargo uid=%d: bayempty=%02d, bayloaded = %02d - weight=%02d", transport.templatename, k, asset.uid, transport.cargobay[k], cargobay, asset.weight)) - + -- Cargo fits into carrier if delta>=0 then -- Reduce remaining cargobay. cargobay=cargobay-asset.weight self:T3(self.wid..string.format("%s unit %d loads cargo uid=%d: bayempty=%02d, bayloaded = %02d - weight=%02d", transport.templatename, k, asset.uid, transport.cargobay[k], cargobay, asset.weight)) - + -- Remember this cargo and remove it so it does not get loaded into other carriers. table.insert(putintocarrier, j) - + -- This transport group is used. used=true - else + else self:T2(self.wid..string.format("Carrier unit %s too small for cargo asset %s ==> cannot be used! Cargo bay - asset weight = %d kg", transport.templatename, asset.templatename, delta)) end - - end -- loop over assets + + end -- loop over assets end -- loop over units - + -- Remove cargo assets from list. Needs to be done back-to-front in order not to confuse the loop. for j=#putintocarrier,1, -1 do - + local nput=putintocarrier[j] local cargo=cargoassets[nput] - + -- Need to check if multiple units in a group and the group has already been removed! -- TODO: This might need to be improved but is working okay so far. if cargo then -- Remove this group because it was used. - self:T2(self.wid..string.format("Cargo id=%d assigned for carrier id=%d", cargo.uid, transport.uid)) + self:T2(self.wid..string.format("Cargo id=%d assigned for carrier id=%d", cargo.uid, transport.uid)) table.remove(cargoassets, nput) end end - + -- Cargo was assined for this carrier. if used then table.insert(used_transports, transport) end - + -- Convert relative quantity (all, half) to absolute number if necessary. local ntrans=self:_QuantityRel2Abs(request.ntransport, #transports) - + -- Max number of transport groups reached? if #used_transports >= ntrans then request.ntransport=#used_transports break end end - + -- Debug info. local text=string.format("Used Transports for request %d to warehouse %s:\n", request.uid, request.warehouse.alias) - local totalcargobay=0 + local totalcargobay=0 for _i,_transport in pairs(used_transports) do local transport=_transport --#WAREHOUSE.Assetitem text=text..string.format("%d) %s: cargobay tot = %d kg, cargobay max = %d kg, nunits=%d\n", _i, transport.unittype, transport.cargobaytot, transport.cargobaymax, transport.nunits) @@ -6800,7 +6845,7 @@ function WAREHOUSE:_GetTransportsForAssets(request) text=text..string.format("Total cargo bay capacity = %.1f kg\n", totalcargobay) text=text..string.format("Total cargo weight = %.1f kg\n", totalcargoweight) text=text..string.format("Minimum number of runs = %.1f", totalcargoweight/totalcargobay) - self:_DebugMessage(text) + self:_DebugMessage(text) return used_transports end @@ -6823,7 +6868,7 @@ function WAREHOUSE:_QuantityRel2Abs(relative, ntot) elseif relative==WAREHOUSE.Quantity.HALF then nabs=UTILS.Round(ntot/2) elseif relative==WAREHOUSE.Quantity.THIRD then - nabs=UTILS.Round(ntot/3) + nabs=UTILS.Round(ntot/3) elseif relative==WAREHOUSE.Quantity.QUARTER then nabs=UTILS.Round(ntot/4) else @@ -6832,7 +6877,7 @@ function WAREHOUSE:_QuantityRel2Abs(relative, ntot) else nabs=relative end - + self:T2(self.wid..string.format("Relative %s: tot=%d, abs=%.2f", tostring(relative), ntot, nabs)) return nabs @@ -6848,24 +6893,24 @@ function WAREHOUSE:_CheckQueue() -- Search for a request we can execute. local request=nil --#WAREHOUSE.Queueitem - + local invalid={} local gotit=false for _,_qitem in ipairs(self.queue) do local qitem=_qitem --#WAREHOUSE.Queueitem - + -- Check if request is valid in general. local valid=self:_CheckRequestValid(qitem) - + -- 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. table.insert(invalid, qitem) end - + -- Get the first valid request that can be executed now. if okay and valid and not gotit then request=qitem @@ -6873,7 +6918,7 @@ function WAREHOUSE:_CheckQueue() break end end - + -- Delete invalid requests. for _,_request in pairs(invalid) do self:T(self.wid..string.format("Deleting invalid request id=%d.",_request.uid)) @@ -6898,27 +6943,31 @@ function WAREHOUSE:_SimpleTaskFunction(Function, group) -- Task script. local DCSScript = {} --DCSScript[#DCSScript+1] = string.format('env.info(\"WAREHOUSE: Simple task function called!\") ') - DCSScript[#DCSScript+1] = string.format('local mygroup = GROUP:FindByName(\"%s\") ', groupname) -- The group that executes the task function. Very handy with the "...". - DCSScript[#DCSScript+1] = string.format("local mystatic = STATIC:FindByName(\"%s\") ", warehouse) -- The static that holds the warehouse self object. - DCSScript[#DCSScript+1] = string.format('local warehouse = mystatic:GetState(mystatic, \"WAREHOUSE\") ') -- Get the warehouse self object from the static. - DCSScript[#DCSScript+1] = string.format('%s(mygroup)', Function) -- Call the function, e.g. myfunction.(warehouse,mygroup) + 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 + 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. + end + DCSScript[#DCSScript+1] = string.format('local warehouse = mywarehouse:GetState(mywarehouse, \"WAREHOUSE\") ') -- Get the warehouse self object from the static. + DCSScript[#DCSScript+1] = string.format('%s(mygroup)', Function) -- Call the function, e.g. myfunction.(warehouse,mygroup) -- Create task. local DCSTask = CONTROLLABLE.TaskWrappedAction(self, CONTROLLABLE.CommandDoScript(self, table.concat(DCSScript))) - + return DCSTask end --- Get the proper terminal type based on generalized attribute of the group. --@param #WAREHOUSE self --@param #WAREHOUSE.Attribute _attribute Generlized attibute of unit. +--@param #number _category Airbase category. --@return Wrapper.Airbase#AIRBASE.TerminalType Terminal type for this group. -function WAREHOUSE:_GetTerminal(_attribute) +function WAREHOUSE:_GetTerminal(_attribute, _category) -- Default terminal is "large". local _terminal=AIRBASE.TerminalType.OpenBig - - + if _attribute==WAREHOUSE.Attribute.AIR_FIGHTER then -- Fighter ==> small. _terminal=AIRBASE.TerminalType.FighterAircraft @@ -6928,8 +6977,17 @@ function WAREHOUSE:_GetTerminal(_attribute) elseif _attribute==WAREHOUSE.Attribute.AIR_TRANSPORTHELO or _attribute==WAREHOUSE.Attribute.AIR_ATTACKHELO then -- Helicopter. _terminal=AIRBASE.TerminalType.HelicopterUsable + else + --_terminal=AIRBASE.TerminalType.OpenMedOrBig end - + + -- For ships, we allow medium spots for all fixed wing aircraft. There are smaller tankers and AWACS aircraft that can use a carrier. + if _category==Airbase.Category.SHIP then + if not (_attribute==WAREHOUSE.Attribute.AIR_TRANSPORTHELO or _attribute==WAREHOUSE.Attribute.AIR_ATTACKHELO) then + _terminal=AIRBASE.TerminalType.OpenMedOrBig + end + end + return _terminal end @@ -6955,27 +7013,27 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) local safedist=(l1/2+l2/2)*1.05 -- 5% safety margine added to safe distance! local safe = (dist > safedist) self:T3(string.format("l1=%.1f l2=%.1f s=%.1f d=%.1f ==> safe=%s", l1,l2,safedist,dist,tostring(safe))) - return safe + return safe end - + -- Get parking spot data table. This contains all free and "non-free" spots. local parkingdata=airbase:GetParkingSpotsTable() - + -- List of obstacles. local obstacles={} - + -- 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! for _,parkingspot in pairs(parkingdata) do - + -- Coordinate of the parking spot. local _spot=parkingspot.Coordinate -- Core.Point#COORDINATE local _termid=parkingspot.TerminalID - + -- Scan a radius of 100 meters around the spot. local _,_,_,_units,_statics,_sceneries=_spot:ScanObjects(scanradius, scanunits, scanstatics, scanscenery) - -- Check all units. + -- Check all units. for _,_unit in pairs(_units) do local unit=_unit --Wrapper.Unit#UNIT local _coord=unit:GetCoordinate() @@ -6983,7 +7041,7 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) local _name=unit:GetName() table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="unit"}) end - + -- Check all statics. for _,static in pairs(_statics) do local _vec3=static:getPoint() @@ -6992,7 +7050,7 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) local _size=self:_GetObjectSize(static) table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="static"}) end - + -- Check all scenery. for _,scenery in pairs(_sceneries) do local _vec3=scenery:getPoint() @@ -7001,63 +7059,56 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) local _size=self:_GetObjectSize(scenery) table.insert(obstacles,{coord=_coord, size=_size, name=_name, type="scenery"}) end - - --[[ - -- TODO Clients? Unoccupied client aircraft are also important! Are they already included in scanned units maybe? - local clients=_DATABASE.CLIENTS - for _,_client in pairs(clients) do - local client=_client --Wrapper.Client#CLIENT - env.info(string.format("FF Client name %s", client:GetName())) - local unit=UNIT:FindByName(client:GetName()) - --local unit=client:GetClientGroupUnit() - local _coord=unit:GetCoordinate() - local _name=unit:GetName() - local _size=self:_GetObjectSize(client:GetClientGroupDCSUnit()) - table.insert(obstacles,{coord=_coord, size=_size, name=_name, type="client"}) - end - ]] + end - + -- Parking data for all assets. local parking={} -- Loop over all assets that need a parking psot. for _,asset in pairs(assets) do local _asset=asset --#WAREHOUSE.Assetitem - + -- Get terminal type of this asset - local terminaltype=self:_GetTerminal(asset.attribute) - + local terminaltype=self:_GetTerminal(asset.attribute, self:GetAirbaseCategory()) + -- Asset specific parking. parking[_asset.uid]={} - + -- Loop over all units - each one needs a spot. for i=1,_asset.nunits do - + -- Loop over all parking spots. local gotit=false - for _,_parkingspot in pairs(parkingdata) do + for _,_parkingspot in pairs(parkingdata) do 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) then - + if AIRBASE._CheckTerminalType(parkingspot.TerminalType, terminaltype) 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)) - - -- Loop over all obstacles. + local free=true local problem=nil + + -- Safe parking using TO_AC from DCS result. + if self.safeparking and _toac then + free=false + self:T("Parking spot %d is occupied by other aircraft taking off or landing.", _termid) + end + + -- Loop over all obstacles. for _,obstacle in pairs(obstacles) do - + -- Check if aircraft overlaps with any obstacle. local dist=_spot:Get2DDistance(obstacle.coord) local safe=_overlap(_asset.size, obstacle.size, dist) - + -- 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)) @@ -7068,25 +7119,25 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) else --env.info(string.format("FF asset=%s (id=%d): spot id=%d dist=%.1fm is SAFE", _asset.templatename, _asset.uid, _termid, dist)) end - + end - + -- Check if spot is free if free then - + -- Add parkingspot for this asset unit. table.insert(parking[_asset.uid], parkingspot) - + self:T(self.wid..string.format("Parking spot #%d is free for asset id=%d!", _termid, _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"}) - + gotit=true break - + else - + -- Debug output for occupied spots. self:T(self.wid..string.format("Parking spot #%d is occupied or not big enough!", _termid)) if self.Debug then @@ -7094,20 +7145,20 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) 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) coord:MarkToAll(string.format(text)) end - + end - + end -- check terminal type end -- loop over parking spots - + -- No parking spot for at least one asset :( if not gotit then self:T(self.wid..string.format("WARNING: No free parking spot for asset id=%d",_asset.uid)) return nil - end + end end -- loop over asset units end -- loop over asset groups - + return parking end @@ -7121,7 +7172,7 @@ function WAREHOUSE:_GetRequestOfGroup(group, queue) -- Get warehouse, asset and request ID from group name. local wid,aid,rid=self:_GetIDsFromGroup(group) - + -- Find the request. for _,_request in pairs(queue) do local request=_request --#WAREHOUSE.Queueitem @@ -7129,7 +7180,7 @@ function WAREHOUSE:_GetRequestOfGroup(group, queue) return request end end - + end --- Is the group a used as transporter for a given request? @@ -7142,31 +7193,31 @@ function WAREHOUSE:_GroupIsTransport(group, request) -- Name of the group under question. local groupname=self:_GetNameWithOut(group) - if request.transportgroupset then + 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 end end - end - + end + return nil end ---- Creates a unique name for spawned assets. From the group name the original warehouse, global asset and the request can be derived. +--- Creates a unique name for spawned assets. From the group name the original warehouse, global asset and the request can be derived. -- @param #WAREHOUSE self -- @param #WAREHOUSE.Assetitem _assetitem Asset for which the name is created. -- @param #WAREHOUSE.Queueitem _queueitem (Optional) Request specific name. @@ -7227,16 +7278,16 @@ 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. + + -- 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, "-") @@ -7248,26 +7299,26 @@ function WAREHOUSE:_GetIDsFromGroup(group) _aid=tonumber(val) elseif key:find("RID") then _rid=tonumber(val) - end + end end - + return _wid,_aid,_rid end - + if group then - + -- Group name local name=group:GetName() - + -- Get ids local wid,aid,rid=analyse(name) - + -- Debug info - self:T3(self.wid..string.format("Group Name = %s", tostring(name))) + self:T3(self.wid..string.format("Group Name = %s", tostring(name))) self:T3(self.wid..string.format("Warehouse ID = %s", tostring(wid))) self:T3(self.wid..string.format("Asset ID = %s", tostring(aid))) self:T3(self.wid..string.format("Request ID = %s", tostring(rid))) - + return wid,aid,rid else self:E("WARNING: Group not found in GetIDsFromGroup() function!") @@ -7307,34 +7358,34 @@ function WAREHOUSE:_FilterStock(stock, descriptor, attribute, nmax, mobile) end end end - + -- Treat case where ntot=0, i.e. no assets at all. if ntot==0 then return filtered, ntot, false end - + -- Convert relative to absolute number if necessary. nmax=self:_QuantityRel2Abs(nmax,ntot) -- Loop over stock items. for _i,_asset in ipairs(stock) do local asset=_asset --#WAREHOUSE.Assetitem - + -- Check if asset has the right attribute. if asset[descriptor]==attribute then - + -- Check if asset has to be mobile. if (mobile and asset.speedmax>0) or (not mobile) then - + -- Add asset to filtered table. table.insert(filtered, asset) - + -- Break loop if nmax was reached. if nmax~=nil and #filtered>=nmax then return filtered, ntot, true end - - end + + end end end @@ -7367,24 +7418,24 @@ function WAREHOUSE:_GetAttribute(group) local attribute=WAREHOUSE.Attribute.OTHER_UNKNOWN --#WAREHOUSE.Attribute if group then - + ----------- --- Air --- - ----------- + ----------- -- Planes local transportplane=group:HasAttribute("Transports") and group:HasAttribute("Planes") local awacs=group:HasAttribute("AWACS") - local fighter=group:HasAttribute("Fighters") or group:HasAttribute("Interceptors") or group:HasAttribute("Multirole fighters") or (group:HasAttribute("Bombers") and not group:HasAttribute("Strategic bombers")) + local fighter=group:HasAttribute("Fighters") or group:HasAttribute("Interceptors") or group:HasAttribute("Multirole fighters") or (group:HasAttribute("Bombers") and not group:HasAttribute("Strategic bombers")) local bomber=group:HasAttribute("Strategic bombers") - local tanker=group:HasAttribute("Tankers") - local uav=group:HasAttribute("UAVs") + local tanker=group:HasAttribute("Tankers") + local uav=group:HasAttribute("UAVs") -- Helicopters local transporthelo=group:HasAttribute("Transport helicopters") local attackhelicopter=group:HasAttribute("Attack helicopters") -------------- --- Ground --- - -------------- + -------------- -- Ground local apc=group:HasAttribute("Infantry carriers") local truck=group:HasAttribute("Trucks") and group:GetCategory()==Group.Category.GROUND @@ -7399,13 +7450,13 @@ function WAREHOUSE:_GetAttribute(group) ------------- --- Naval --- - ------------- + ------------- -- Ships local aircraftcarrier=group:HasAttribute("Aircraft Carriers") local warship=group:HasAttribute("Heavy armed ships") local armedship=group:HasAttribute("Armed ships") local unarmedship=group:HasAttribute("Unarmed ships") - + -- Define attribute. Order is important. if transportplane then @@ -7439,7 +7490,7 @@ function WAREHOUSE:_GetAttribute(group) elseif sam then attribute=WAREHOUSE.Attribute.GROUND_SAM elseif truck then - attribute=WAREHOUSE.Attribute.GROUND_TRUCK + attribute=WAREHOUSE.Attribute.GROUND_TRUCK elseif train then attribute=WAREHOUSE.Attribute.GROUND_TRAIN elseif aircraftcarrier then @@ -7447,7 +7498,7 @@ function WAREHOUSE:_GetAttribute(group) elseif warship then attribute=WAREHOUSE.Attribute.NAVAL_WARSHIP elseif armedship then - attribute=WAREHOUSE.Attribute.NAVAL_ARMEDSHIP + attribute=WAREHOUSE.Attribute.NAVAL_ARMEDSHIP elseif unarmedship then attribute=WAREHOUSE.Attribute.NAVAL_UNARMEDSHIP else @@ -7459,7 +7510,7 @@ function WAREHOUSE:_GetAttribute(group) attribute=WAREHOUSE.Attribute.AIR_OTHER else attribute=WAREHOUSE.Attribute.OTHER_UNKNOWN - end + end end end @@ -7482,7 +7533,7 @@ function WAREHOUSE:_GetObjectSize(DCSobject) return math.max(x,z), x , y, z end return 0,0,0,0 -end +end --- Returns the number of assets for each generalized attribute. -- @param #WAREHOUSE self @@ -7526,7 +7577,7 @@ end -- @param #table queue The queue from which the item should be deleted. function WAREHOUSE:_DeleteQueueItem(qitem, queue) self:F({qitem=qitem, queue=queue}) - + for i=1,#queue do local _item=queue[i] --#WAREHOUSE.Queueitem if _item.uid==qitem.uid then @@ -7561,14 +7612,14 @@ function WAREHOUSE:_PrintQueue(queue, name) -- 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 + if qitem.timestamp then clock=tostring(UTILS.SecondsToClock(qitem.timestamp)) end local assignment=tostring(qitem.assignment) @@ -7578,19 +7629,19 @@ function WAREHOUSE:_PrintQueue(queue, name) local assetdesc=qitem.assetdesc local assetdescval=qitem.assetdescval local nasset=tostring(qitem.nasset) - local ndelivered=tostring(qitem.ndelivered) + local ndelivered=tostring(qitem.ndelivered) local ncargogroupset="N/A" if qitem.cargogroupset then - ncargogroupset=tostring(qitem.cargogroupset:Count()) - end + ncargogroupset=tostring(qitem.cargogroupset:Count()) + end local transporttype="N/A" if qitem.transporttype then transporttype=qitem.transporttype - end + end local ntransport="N/A" if qitem.ntransport then - ntransport=tostring(qitem.ntransport) - end + ntransport=tostring(qitem.ntransport) + end local ntransportalive="N/A" if qitem.transportgroupset then ntransportalive=tostring(qitem.transportgroupset:Count()) @@ -7598,21 +7649,21 @@ function WAREHOUSE:_PrintQueue(queue, name) local ntransporthome="N/A" if qitem.ntransporthome then ntransporthome=tostring(qitem.ntransporthome) - end - - -- Output text: + 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 - + self:I(self.wid..text) end --- Display status of warehouse. -- @param #WAREHOUSE self -function WAREHOUSE:_DisplayStatus() +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") @@ -7634,8 +7685,8 @@ function WAREHOUSE:_GetStockAssetsText(messagetoall) -- Get assets in stock. local _data=self:GetStockInfo(self.stock) - - -- Text. + + -- Text. local text="Stock:\n" local total=0 for _attribute,_count in pairs(_data) do @@ -7648,10 +7699,10 @@ function WAREHOUSE:_GetStockAssetsText(messagetoall) text=text..string.format("===================\n") text=text..string.format("Total = %d\n", total) text=text..string.format("------------------------------------------------------\n") - + -- Send message? MESSAGE:New(text, 10):ToAllIf(messagetoall) - + return text end @@ -7665,11 +7716,11 @@ function WAREHOUSE:_UpdateWarehouseMarkText() if self.markerid~=nil then trigger.action.removeMark(self.markerid) end - + -- Get assets in stock. local _data=self:GetStockInfo(self.stock) - -- Text. + -- 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 @@ -7678,7 +7729,7 @@ function WAREHOUSE:_UpdateWarehouseMarkText() 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 @@ -7701,7 +7752,7 @@ function WAREHOUSE:_DisplayStockItems(stock) local speed=mystock.speedmax local uid=mystock.uid local unittype=mystock.unittype - local weight=mystock.weight + local weight=mystock.weight local attribute=mystock.attribute text=text..string.format("\n%02d) uid=%d, name=%s, unittype=%s, category=%d, attribute=%s, nunits=%d, speed=%.1f km/h, range=%.1f km, size=%.1f m, weight=%.1f kg, cargobax max=%.1f kg tot=%.1f kg", _i, uid, name, unittype, category, attribute, nunits, speed, range/1000, size, weight, cargobaymax, cargobaytot) @@ -7777,23 +7828,23 @@ function WAREHOUSE:_GetMaxHeight(D, alphaC, alphaD, Hdep, Hdest, Deltahhold) local Hhold=Hdest+Deltahhold local hdest=Hdest-Hdep local hhold=hdest+Deltahhold - + local Dp=math.sqrt(D^2 + hhold^2) - + local alphaS=math.atan(hdest/D) -- slope angle local alphaH=math.atan(hhold/D) -- angle to holding point (could be necative!) - + local alphaCp=alphaC-alphaH -- climb angle with slope local alphaDp=alphaD+alphaH -- descent angle with slope - + -- ASA triangle. local gammap=math.pi-alphaCp-alphaDp local sCp=Dp*math.sin(alphaDp)/math.sin(gammap) local sDp=Dp*math.sin(alphaCp)/math.sin(gammap) - + -- Max height from departure. local hmax=sCp*math.sin(alphaC) - + -- Debug info. if self.Debug then env.info(string.format("Hdep = %.3f km", Hdep/1000)) @@ -7817,14 +7868,14 @@ function WAREHOUSE:_GetMaxHeight(D, alphaC, alphaD, Hdep, Hdest, Deltahhold) env.info() env.info(string.format("hmax = %.3f km", hmax/1000)) env.info() - + -- Descent height local hdescent=hmax-hhold - + local dClimb = hmax/math.tan(alphaC) local dDescent = (hmax-hhold)/math.tan(alphaD) local dCruise = D-dClimb-dDescent - + env.info(string.format("hmax = %.3f km", hmax/1000)) env.info(string.format("hdescent = %.3f km", hdescent/1000)) env.info(string.format("Dclimb = %.3f km", dClimb/1000)) @@ -7832,84 +7883,84 @@ function WAREHOUSE:_GetMaxHeight(D, alphaC, alphaD, Hdep, Hdest, Deltahhold) env.info(string.format("Ddescent = %.3f km", dDescent/1000)) env.info() end - + return hmax end ---- Make a flight plan from a departure to a destination airport. +--- Make a flight plan from a departure to a destination airport. -- @param #WAREHOUSE self --- @param #WAREHOUSE.Assetitem asset +-- @param #WAREHOUSE.Assetitem asset -- @param Wrapper.Airbase#AIRBASE departure Departure airbase. -- @param Wrapper.Airbase#AIRBASE destination Destination airbase. -- @return #table Table of flightplan waypoints. --- @return #table Table of flightplan coordinates. +-- @return #table Table of flightplan coordinates. function WAREHOUSE:_GetFlightplan(asset, departure, destination) - + -- Parameters in SI units (m/s, m). local Vmax=asset.speedmax/3.6 local Range=asset.range local category=asset.category local ceiling=asset.DCSdesc.Hmax local Vymax=asset.DCSdesc.VyMax - + -- Max cruise speed 90% of max speed. local VxCruiseMax=0.90*Vmax -- 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(Vmax*0.90, 200) - + -- Descent speed 60% of Vmax but max 500 km/h. local VxDescent = math.min(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(7.6, Vymax) - + -- Climb angle in rad. --local AlphaClimb=math.asin(VyClimb/VxClimb) local AlphaClimb=math.rad(4) - + -- Descent angle in rad. Moderate 4 degrees. local AlphaDescent=math.rad(4) - + -- Expected cruise level (peak of Gaussian distribution) local FLcruise_expect=150*RAT.unit.FL2m - if category==Group.Category.HELICOPTER then + if category==Group.Category.HELICOPTER then FLcruise_expect=1000 -- 1000 m ASL end - + ------------------------- --- DEPARTURE AIRPORT --- ------------------------- - + -- Coordinates of departure point. local Pdeparture=departure:GetCoordinate() - + -- Height ASL of departure point. local H_departure=Pdeparture.y - - --------------------------- + + --------------------------- --- DESTINATION AIRPORT --- --------------------------- - + -- Position of destination airport. local Pdestination=destination:GetCoordinate() - + -- Height ASL of destination airport/zone. local H_destination=Pdestination.y - + ----------------------------- --- DESCENT/HOLDING POINT --- ----------------------------- @@ -7917,26 +7968,26 @@ function WAREHOUSE:_GetFlightplan(asset, departure, destination) -- Get a random point between 5 and 10 km away from the destination. local Rhmin=5000 local Rhmax=10000 - + -- For helos we set a distance between 500 to 1000 m. - if category==Group.Category.HELICOPTER then + if category==Group.Category.HELICOPTER then Rhmin=500 Rhmax=1000 end - + -- Coordinates of the holding point. y is the land height at that point. local Pholding=Pdestination:GetRandomCoordinateInRadius(Rhmax, Rhmin) -- Distance from holding point to final destination (not used). local d_holding=Pholding:Get2DDistance(Pdestination) - + -- AGL height of holding point. local H_holding=Pholding.y - + --------------- --- GENERAL --- --------------- - + -- We go directly to the holding point not the destination airport. From there, planes are guided by DCS to final approach. local heading=Pdeparture:HeadingTo(Pholding) local d_total=Pdeparture:Get2DDistance(Pholding) @@ -7944,46 +7995,46 @@ function WAREHOUSE:_GetFlightplan(asset, departure, destination) ------------------------------ --- Holding Point Altitude --- ------------------------------ - + -- Holding point altitude. For planes between 1600 and 2400 m AGL. For helos 160 to 240 m AGL. local h_holding=1200 if category==Group.Category.HELICOPTER then h_holding=150 end h_holding=UTILS.Randomize(h_holding, 0.2) - + -- Max holding altitude. local DeltaholdingMax=self:_GetMaxHeight(d_total, AlphaClimb, AlphaDescent, H_departure, H_holding, 0) - + if h_holding>DeltaholdingMax then h_holding=math.abs(DeltaholdingMax) end - + -- This is the height ASL of the holding point we want to fly to. local Hh_holding=H_holding+h_holding - + --------------------------- --- Max Flight Altitude --- - --------------------------- - + --------------------------- + -- Get max flight altitude relative to H_departure. local h_max=self:_GetMaxHeight(d_total, AlphaClimb, AlphaDescent, H_departure, H_holding, h_holding) -- Max flight level ASL 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) - + -- Ensure that FLmax not above its service ceiling. FLmax=math.min(FLmax, ceiling) - + -- 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) -- Climb and descent heights. local h_climb = FLcruise - H_departure local h_descent = FLcruise - Hh_holding - + -- Get 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 - + -- Debug. local text=string.format("Flight plan:\n") text=text..string.format("Vx max = %.2f km/h\n", Vmax*3.6) @@ -8031,7 +8082,7 @@ function WAREHOUSE:_GetFlightplan(asset, departure, destination) text=text..string.format("Ceiling = %.3f km\n", ceiling/1000) text=text..string.format("Max range = %.3f km\n", Range/1000) self:T(self.wid..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 @@ -8044,32 +8095,32 @@ function WAREHOUSE:_GetFlightplan(asset, departure, destination) -- Waypoints and coordinates local wp={} local c={} - + --- Departure/Take-off c[#c+1]=Pdeparture wp[#wp+1]=Pdeparture:WaypointAir("RADIO", COORDINATE.WaypointType.TakeOffParking, COORDINATE.WaypointAction.FromParkingArea, VxClimb, true, departure, nil, "Departure") - + --- Begin of Cruise local Pcruise=Pdeparture:Translate(d_climb, heading) Pcruise.y=FLcruise c[#c+1]=Pcruise wp[#wp+1]=Pcruise:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxCruise, true, nil, nil, "Cruise") - --- Descent + --- Descent local Pdescent=Pcruise:Translate(d_cruise, heading) Pdescent.y=FLcruise c[#c+1]=Pdescent wp[#wp+1]=Pdescent:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxDescent, true, nil, nil, "Descent") - + --- Holding point Pholding.y=H_holding+h_holding c[#c+1]=Pholding - wp[#wp+1]=Pholding:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxHolding, true, nil, nil, "Holding") + wp[#wp+1]=Pholding:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxHolding, true, nil, nil, "Holding") - --- Final destination. + --- Final destination. c[#c+1]=Pdestination wp[#wp+1]=Pdestination:WaypointAir("RADIO", COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, VxFinal, true, destination, nil, "Final Destination") - + -- Mark points at waypoints for debugging. if self.Debug then @@ -8080,9 +8131,9 @@ function WAREHOUSE:_GetFlightplan(asset, departure, destination) dist=coord:Get2DDistance(c[i-1]) end coord:MarkToAll(string.format("Waypoint %i, distance = %.2f km",i, dist/1000)) - end + end end - + return wp,c end @@ -8095,37 +8146,37 @@ end --- Departure/Take-off c[#c+1]=Pdeparture wp[#wp+1]=Pdeparture:WaypointAir("RADIO", COORDINATE.WaypointType.TakeOffParking, COORDINATE.WaypointAction.FromParkingArea, VxClimb, true, departure, nil, "Departure") - - --- Climb + + --- Climb local Pclimb=Pdeparture:Translate(d_climb/2, heading) Pclimb.y=H_departure+(FLcruise-H_departure)/2 c[#c+1]=Pclimb wp[#wp+1]=Pclimb:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxClimb, true, nil, nil, "Climb") - + --- Begin of Cruise local Pcruise1=Pclimb:Translate(d_climb/2, heading) Pcruise1.y=FLcruise c[#c+1]=Pcruise1 wp[#wp+1]=Pcruise1:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxCruise, true, nil, nil, "Begin of Cruise") - --- End of Cruise + --- End of Cruise local Pcruise2=Pcruise1:Translate(d_cruise, heading) Pcruise2.y=FLcruise c[#c+1]=Pcruise2 wp[#wp+1]=Pcruise2:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxCruise, true, nil, nil, "End of Cruise") - --- Descent + --- Descent local Pdescent=Pcruise2:Translate(d_descent/2, heading) Pdescent.y=FLcruise-(FLcruise-(h_holding+H_holding))/2 c[#c+1]=Pdescent wp[#wp+1]=Pcruise2:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxDescent, true, nil, nil, "Descent") - - --- Holding point - Pholding.y=H_holding+h_holding - c[#c+1]=Pholding - wp[#wp+1]=Pholding:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxHolding, true, nil, nil, "Holding") - --- Final destination. + --- Holding point + Pholding.y=H_holding+h_holding + c[#c+1]=Pholding + wp[#wp+1]=Pholding:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxHolding, true, nil, nil, "Holding") + + --- Final destination. c[#c+1]=Pdestination wp[#wp+1]=Pdestination:WaypointAir("RADIO", COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, VxFinal, true, destination, nil, "Final Destination") ]] diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua new file mode 100644 index 000000000..729e53218 --- /dev/null +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -0,0 +1,9375 @@ +--- **Ops** - (R2.5) - Manages aircraft recoveries for carrier operations. +-- +-- The AIRBOSS class manages recoveries of human pilots and AI aircraft on aircraft carriers. +-- +-- **Main Features:** +-- +-- * CASE I, II and III recoveries. +-- * Supports human pilots as well as AI flight groups. +-- * Automatic LSO grading (WIP). +-- * Different skill levels from on-the-fly tips for flight students to ziplip for pros. +-- * Define recovery time windows with individual recovery cases. +-- * Automatic TACAN and ICLS channel setting of carrier. +-- * Separate radio channels for LSO and Marshal transmissions. +-- * Voice over support for LSO and Marshal radio transmissions. +-- * F10 radio menu including carrier info (weather, radio frequencies, TACAN/ICLS channels), player LSO grades, +-- help function (player aircraft attitude, marking of pattern zones etc). +-- * Recovery tanker and refueling option via integration of @{Ops.RecoveryTanker} class. +-- * Rescue helicopter option via @{Ops.RescueHelo} class. +-- * Many parameters customizable by convenient user API functions. +-- * Multiple carrier support due to object oriented approach. +-- * Unlimited number of players. +-- * Finite State Machine (FSM) implementation. +-- +-- **Supported Carriers:** +-- +-- * [USS John C. Stennis](https://en.wikipedia.org/wiki/USS_John_C._Stennis) (CVN-74) +-- +-- **Supported Aircraft:** +-- +-- * [F/A-18C Hornet Lot 20](https://forums.eagle.ru/forumdisplay.php?f=557) (Player & AI) +-- * [A-4E Skyhawk Community Mod](https://forums.eagle.ru/showthread.php?t=224989) (Player & AI) +-- * F/A-18C Hornet (AI) +-- * F-14A Tomcat (AI) +-- * E-2D Hawkeye (AI) +-- * S-3B Viking & tanker version (AI) +-- +-- At the moment, optimized parameters are available for the F/A-18C Hornet (Lot 20) as aircraft and the USS John C. Stennis as carrier. +-- The A-4E community mod is also supported in priciple but may need further tweaking of parameters. +-- +-- The implemenation is kept general. So other aircraft and carriers possible in future. [*Winter is coming!*](https://forums.eagle.ru/forumdisplay.php?f=395) +-- But each aircraft or carrier needs a different set of optimized individual parameters. +-- +-- **PLEASE NOTE** that his class is work in progress and in an early **alpha** stage. Many/most things work already very nicely but there a lot of cases I did not run into yet. +-- Therefore, your *constructive* feedback is both necessary and appreciated! +-- +-- ### Some Open Questions? +-- +-- * What are the conditions for a foul deck wave off? +-- * What is the next step after a pattern wave off during Case II or III recovery? +-- * What is the condition for a "fly through" (\\ or /) LSO grade? +-- * The above question is one of many regarding LSO grade. If you have more info, please share. +-- +-- If you know the answer to any of this, please get in touch with me! +-- The necessary infrastructure to implement it is most likely already there, but I am not 100% sure about the exact conditions. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- ### Special Thanks To **Bankler** +-- For his great [Recovery Trainer](https://forums.eagle.ru/showthread.php?t=221412) mission and script! +-- His work was the initial inspiration for this class. Also note that this implementation uses some routines for determining the player position in Case I recoveries he developed. +-- Bankler was kind enough to allow me to add this to the class - thanks again! +-- +-- @module Ops.Airboss +-- @image MOOSE.JPG + +--- AIRBOSS class. +-- @type AIRBOSS +-- @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 Wrapper.Unit#UNIT carrier Aircraft carrier unit on which we want to practice. +-- @field #string carriertype Type name of aircraft carrier. +-- @field #AIRBOSS.CarrierParameters carrierparam Carrier specifc parameters. +-- @field #string alias Alias of the carrier. +-- @field Wrapper.Airbase#AIRBASE airbase Carrier airbase object. +-- @field #table waypoints Waypoint coordinates of carrier. +-- @field #number currentwp Current waypoint, i.e. the one that has been passed last. +-- @field Core.Radio#BEACON beacon Carrier beacon for TACAN and ICLS. +-- @field #boolean TACANon Automatic TACAN is activated. +-- @field #number TACANchannel TACAN channel. +-- @field #string TACANmode TACAN mode, i.e. "X" or "Y". +-- @field #string TACANmorse TACAN morse code, e.g. "STN". +-- @field #boolean ICLSon Automatic ICLS is activated. +-- @field #number ICLSchannel ICLS channel. +-- @field #string ICLSmorse ICLS morse code, e.g. "STN". +-- @field Core.Radio#RADIO LSORadio Radio for LSO calls. +-- @field #number LSOFreq LSO radio frequency in MHz. +-- @field #string LSOModu LSO radio modulation "AM" or "FM". +-- @field Core.Radio#RADIO MarshalRadio Radio for carrier calls. +-- @field #number MarshalFreq Marshal radio frequency in MHz. +-- @field #string MarshalModu Marshal radio modulation "AM" or "FM". +-- @field Core.Scheduler#SCHEDULER radiotimer Radio queue scheduler. +-- @field Core.Zone#ZONE_UNIT zoneCCA Carrier controlled area (CCA), i.e. a zone of 50 NM radius around the carrier. +-- @field Core.Zone#ZONE_UNIT zoneCCZ Carrier controlled zone (CCZ), i.e. a zone of 5 NM radius around the carrier. +-- @field Core.Zone#ZONE_UNIT zoneInitial Zone usually 3 NM astern of carrier where pilots start their CASE I pattern. +-- @field #table players Table of players. +-- @field #table menuadded Table of units where the F10 radio menu was added. +-- @field #AIRBOSS.Checkpoint BreakEntry Break entry checkpoint. +-- @field #AIRBOSS.Checkpoint BreakEarly Early break checkpoint. +-- @field #AIRBOSS.Checkpoint BreakLate Late brak checkpoint. +-- @field #AIRBOSS.Checkpoint Abeam Abeam checkpoint. +-- @field #AIRBOSS.Checkpoint Ninety At the ninety checkpoint. +-- @field #AIRBOSS.Checkpoint Wake Checkpoint right behind the carrier. +-- @field #AIRBOSS.Checkpoint Final Checkpoint when turning to final. +-- @field #AIRBOSS.Checkpoint Groove In the groove checkpoint. +-- @field #AIRBOSS.Checkpoint Platform Case II/III descent at 2000 ft/min at 5000 ft platform. +-- @field #AIRBOSS.Checkpoint DirtyUp Case II/III dirty up and on speed position at 1200 ft and 10-12 NM from the carrier. +-- @field #AIRBOSS.Checkpoint Bullseye Case III intercept glideslope and follow ICLS aka "bullseye". +-- @field #number defaultcase Default recovery case. This is the case used if not specified otherwise. +-- @field #number case Recovery case I, II or III currently in progress. +-- @field #table recoverytimes List of time windows when aircraft are recovered including the recovery case and holding offset. +-- @field #number defaultoffset Default holding pattern update if not specified otherwise. +-- @field #number holdingoffset Offset [degrees] of Case II/III holding pattern. +-- @field #table flights List of all flights in the CCA. +-- @field #table Qmarshal Queue of marshalling aircraft groups. +-- @field #table Qpattern Queue of aircraft groups in the landing pattern. +-- @field #table RQMarshal Radio queue of marshal. +-- @field #table RQLSO Radio queue of LSO. +-- @field #number Nmaxpattern Max number of aircraft in landing pattern. +-- @field #boolean handleai If true (default), handle AI aircraft. +-- @field Ops.RecoveryTanker#RECOVERYTANKER tanker Recovery tanker flying overhead of carrier. +-- @field Functional.Warehouse#WAREHOUSE warehouse Warehouse object of the carrier. +-- @field DCS#Vec3 Corientation Carrier orientation in space. +-- @field DCS#Vec3 Corientlast Last known carrier orientation. +-- @field Core.Point#COORDINATE Cposition Carrier position. +-- @field #string defaultskill Default player skill @{#AIRBOSS.Difficulty}. +-- @field #boolean adinfinitum If true, carrier patrols ad infinitum, i.e. when reaching its last waypoint it starts at waypoint one again. +-- @field #number magvar Magnetic declination in degrees. +-- @field #number Tcollapse Last time timer.gettime() the stack collapsed. +-- @extends Core.Fsm#FSM + +--- Be the boss! +-- +-- === +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Main.png) +-- +-- # The AIRBOSS Concept +-- +-- On a carrier, the AIRBOSS is guy who is really in charge - don't mess with him! +-- +-- # Recovery Cases +-- +-- The AIRBOSS class supports all three commonly used recovery cases, i.e. +-- +-- * **CASE I** during daytime and good weather, +-- * **CASE II** during daytime but poor visibility conditions, +-- * **CASE III** during nighttime recoveries. +-- +-- That being said, this script allows you to use any of the three cases to be used at any time. Or, in other words, *you* need to specify when which case is safe and appropriate. +-- +-- This is a lot of responsability. *You* are the boss, but *you* need to make the right decisions or things will go terribly wrong! +-- +-- Recovery windows can be set up via the @{#AIRBOSS.AddRecoveryWindow} function as explained below. With this it is possible to seamlessly (within reason!) switch recovery cases in the same mission. +-- +-- ## CASE I +-- +-- As mentioned before, Case I recovery is the standard procedure during daytime and good visibility conditions. +-- +-- ### Holding Pattern +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case1_Holding.png) +-- +-- The graphic depicts a the standard holding pattern during a Case I recovery. Incoming aircraft enter the holding pattern, which is a counter clockwise turn with a +-- diameter of 5 NM, at their assigned altiude. The holding altitude of the first stack is 2000 ft. The inverval between stacks is 1000 ft. +-- +-- Once a recovery window opens, the aircraft of the lowest stack commence their landing approach and the rest of the Marshal stack collapses, i.e. aircraft switch from +-- their current stack to the next lower stack. +-- +-- The flight that transitions form the holding pattern to the landing approach, it should leave the Marshal stack at the 3 position and make a left hand turn to the *Initial* +-- position, which is 3 NM astern of the boat. +-- +-- ### Landing Pattern +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case1_Landing.png) +-- +-- Once the aircraft reaches the Inital, the landing pattern begins. The important steps of the pattern are shown in the image above. +-- +-- ## CASE III +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case3.png) +-- +-- A Case III recovery is conducted during nighttime. The holding positon and the landing pattern are rather different from a Case I recovery as can be seen in the image above. +-- +-- The first holding zone starts 21 NM astern the carrier at angels 6. The interval between the stacks is 1000 ft just like in Case I. However, the distance to the boat +-- increases by 1 NM with each stack. The general form can be written as D=15+6+(N-1), where D is the distance to the boat in NM and N the number of the stack starting at N=1. +-- +-- Once the aircraft of the lowest stack is allowed to commence to the landing pattern, it starts a descent at 4000 ft/min until it reaches the "*Platform*" at 5000 ft and +-- ~19 NM DME. From there a shallower descent at 2000 ft/min should be performed. At an altitude of 1200 ft the aircraft should level out and "*Dirty Up*" (gear & hook down). +-- +-- At 3 NM distance to the carrier, the aircraft should intercept the 3.5 degrees glide slope at the "*Bullseye*". From there the pilot should "follow the needes" of the ICLS. +-- +-- ## CASE II +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case2.png) +-- +-- Case II is the common recovery procedure at daytime if visibilty conditions are poor. It can be viewed as hybrid between Case I and III. +-- The holding pattern is very similar to that of the Case III recovery with the difference the the radial is the inverse of the BRC instead of the FB. +-- From the holding zone aircraft are follow the Case III path until they reach the Initial position 3 NM astern the boat. From there a standard Case I recovery procedure is +-- in place. +-- +-- Note that the image depicts the case, where the holding zone has an angle offset of 30 degrees with respect to the BRC. This is optional. Commonly used offset angles +-- are 0 (no offset), +-15 or +-30 degrees. The AIRBOSS class supports all these scenarios which are used during Case II and III recoveries. +-- +-- +-- # Scripting +-- +-- Writing a basic script is easy and can be done in two lines. +-- +-- local airbossStennis=AIRBOSS:New("USS Stennis", "Stennis") +-- airbossStennis:Start() +-- +-- The **first line** creates and AIRBOSS object via the @{#AIRBOSS.New}(*carriername*, *alias*) constructor. The first parameter *carriername* is name of the carrier unit as +-- defined in the mission editor. The second parameter *alias* is optional. This name will, e.g., be used for the F10 radio menu entry. If not given, the alias is identical +-- to the *carriername* of the first parameter. +-- +-- This simple script initializes a lot of parameters with default values: +-- +-- * TACAN channel is set to 74X, see @{#AIRBOSS.SetTACAN}, +-- * ICSL channel is set to 1, see @{#AIRBOSS.SetICLS}, +-- * LSO radio is set to 264 MHz FM, see @{#AIRBOSS.SetLSORadio}, +-- * Marshal radio is set to 305 MHz FM, see @{#AIRBOSS.SetMarshalRadio}, +-- * Default recovery case is set to 1, see @{#AIRBOSS.SetRecoveryCase}, +-- * Carrier Controlled Area (CCA) is set to 50 NM, see @{#AIRBOSS.SetCarrierControlledArea}, +-- * Default player skill "Flight Student" (easy), see @{#AIRBOSS.SetDefaultPlayerSkill}, +-- * Once the carrier reaches its final waypoint, it will restart its route, see @{#AIRBOSS.SetPatrolAdInfinitum}. +-- +-- The **second line** starts the AIRBOSS class. If you set options this should happen after the @{#AIRBOSS.New} and before @{#AIRBOSS.Start} command. +-- +-- If no recovery window is set like in the basic example, a window will automatically open 15 minutes after mission start and close again after three hours. +-- The next section explains how to set your own recovery times. +-- +-- ## Recovery Windows +-- +-- Recovery of aircraft is only allowed during defined time slots. You can define these slots via the @{#AIRBOSS.AddRecoveryWindow}(*start*, *stop*, *case*, *holdingoffset*) function. +-- The parameters are: +-- +-- * *start*: The start time as a string. For example "8:00" for a window opening at 8 am. Or "13:30+1" for half past one on the next day. Default (nil) is ASAP. +-- * *stop*: Time when the window closes as a string. Same format as *start*. Default is 90 minutes after start time. +-- * *case*: The recovery case during that window (1, 2 or 3). Default 1. +-- * *holdingoffset*: Holding offset angle in degrees. Only for Case II or III recoveries. Default 0 deg. Common +-15 deg or +-30 deg. +-- +-- If recovery is closed, AI flights will be send to marshal stacks and orbit there until the next window opens. +-- Players can request marshal via the F10 menu and will also be given a marshal stack. Currently, human players can request commence via the F10 radio regarless of +-- whether a window is open or not and will be alowed to enter the pattern (if not already full). This will probably change in the future. +-- +-- At the moment there is no autmatic recovery case set depending on weather or daytime. So it is the AIRBOSS (you) who needs to make that descision. +-- It is probably a good idea to synchronize the timing with the waypoints of the carrier. For example, setting up the waypoints such that the carrier +-- already has turning into the wind, when a recovery window opens. +-- +-- The code for setting up multiple recovery windows could look like this +-- local airbossStennis=AIRBOSS:New("USS Stennis", "Stennis") +-- airbossStennis:AddRecoveryWindow("8:30", "9:30", 1) +-- airbossStennis:AddRecoveryWindow("12:00", "13:15", 2, 15) +-- airbossStennis:AddRecoveryWindow("23:30", "00:30+1", 3, -30) +-- airbossStennis:Start() +-- +-- This will open a Case I recovery window from 8:30 to 9:30. Then a Case II recovery from 12:00 to 13:15, where the holing offset is +15 degrees wrt BRC. +-- Finally, a Case III window opens 23:30 on the day the mission starts and closes 0:30 on the following day. The holding offset is -30 degrees wrt FB. +-- +-- Note that incoming flights will be assigned a holding pattern for the next opening window case if no window is open at the moment. So in the above example, +-- all flights incoming after 13:15 will be assigned to a Case III marshal stack. Therefore, you should make sure that no flights are incoming long before the +-- next window opens or adjust the recovery planning accordingly. +-- +-- # The F10 Radio Menu +-- +-- The F10 radio menu can be used to post requests to Marshal but also provides information about the player and carrier status. Additionally, helper functions +-- can be called. +-- +-- ## Main Menu +-- +-- The general structure +-- +-- * **F1 Help...**: Help submenu, see below. +-- * **F2 Kneeboard...**: Kneeboard submenu, see below. Carrier information, weather report, player status. +-- * **F3 Request Marshal** +-- * **F4 Request Commence** +-- * **F5 Request Refueling** +-- +-- ### Request Marshal +-- +-- This radio command can be used to request a stack in the holding pattern from Marshal. Necessary conditions are that the flight is inside the Carrier Controlled Area (CCA) +-- (see @{#AIRBOSS.SetCarrierControlledArea}). +-- Marshal will assign an individual stack for each player group depending on the current or next open recovery case window. +-- If multiple players have registered as a section, the section lead will be assigned a stack and is responsible to guide his section to the assigned holding position. +-- +-- ### Request Commence +-- +-- This command can be used to request commencing from the marshal stack to the landing pattern. Necessary condition is that the player is in the lowest marshal stack +-- and that the number of aircraft in the landing pattern is smaller than four. +-- +-- A player can also request commencing if he is not registered in a marshal stack yet. If the pattern is free, Marshal will allow him to directly enter the landing pattern. +-- +-- ### Request Refueling +-- +-- If a recovery tanker was setup via the @{#AIRBOSS.SetRecoveryTanker} function, the player can request refueling. If the tanker is ready, refueling is granted and the player +-- can leave the marshal stack for refueling. The stack will collapse and the player needs to request marshal again, when refueling is finished. +-- +-- ## Help Menu +-- +-- This menu provides commands to help the player. +-- +-- ### Skill Level Submenu +-- +-- The player can choose between three skill or difficulty levels. +-- +-- * **Flight Student**: The player receives tips at certain stages of the pattern, e.g. if he is at the right altitude, speed, etc. +-- * **Naval Aviator**: Less tips are show. Player should be familiar with the procedures and its aircraft parameters. +-- * **TOPGUN Graduate**: Only very few information is provided to the player. This is for pros. +-- +-- ### Mark Zones Submenu +-- +-- These commands can be used to mark marshal or landing pattern zones. +-- +-- * **Smoke Pattern Zones** Smoke is used to mark the landing pattern zone of the player depending on his recovery case. +-- For Case I this is the initial zone. For Case II/III and three these are the Platform, Arc turn, Dirty Up, Bullseye/Initial zones as well as the approach corridor. +-- * **Flare Pattern Zones** Similar to smoke but uses flares to mark the pattern zones. +-- * **Smoke Marshal Zone** This smokes the surrounding area of the currently assigned Marshal zone of the player. Player has to be registered in Marshal queue. +-- * **Flare Marshal Zone** Similar to smoke but uses flares to mark the Marshal zone. +-- +-- ### My Status +-- +-- This command provides information about the current player status. For example, his current step in the pattern. +-- +-- ### Attitude Monitor +-- +-- This command displays the current aircraft attitude of the player in short intervals as message on the screen. +-- It provides information about current pitch, roll, yaw, lineup and glideslope error, orientation of the plane wrt to carrier etc. +-- +-- ### LSO Radio Check +-- +-- LSO will transmit a short message on his radio frequency. See @{#AIRBOSS.SetLSORadio}. +-- +-- ### Marshal Radio Check +-- +-- Marshal will transmit a short message on his radio frequency. See @{#AIRBOSS.SetMarshalRadio}. +-- +-- ### [Reset My Status] +-- +-- This will reset the current player status. If player is currently in a marshal stack, he will be removed from the marshal queue and the stack will collapse. +-- The player needs to re-register later if desired. If player is currently in the landing pattern, he will be removed from the pattern queue. +-- +-- ## Kneeboard Menu +-- +-- The Kneeboard menu provides information about the carrier, weather and player results. +-- +-- ### Results Submenu +-- +-- Here you find your LSO grading results as well as scores of other players. +-- +-- * **Greenie Board** lists average scores of all players obtained during landing approaches. +-- * **My LSO Grades** lists all grades the player has received for his approaches in this mission. +-- * **Last Debrief** shows the detailed debriefing of the player's last approach. +-- +-- ### Carrier Info +-- +-- Information about the current carrier status is displayed. This includes current BRC, FB, LSO and Marshal frequences, list of next recovery windows. +-- +-- ### Weather Report +-- +-- Displays information about the current weather at the carrier such as QFE, wind and temperature. +-- +-- ### Set Section +-- +-- With this command, you can define a section of human flights. The player how issues the command becomes the section lead and all other human players +-- within a radius of 200 meters become members of the section. +-- +-- # Landing Signal Officer (LSO) +-- +-- The LSO will first contact you on his radio channel when you are at the the abeam position (Case I) with the phrase "Paddles, contact.". +-- Once you are in the groove the LSO will ask you to "Call the ball." and then acknoledge your ball call by "Roger Ball." +-- +-- During the groove the LSO will give you advice if you deviate from the correct landing path. These advices will be given when you are +-- +-- * too low or too high with respect to the glideslope, +-- * too fast or too slow with respect to the optimal AoA, +-- * too far left or too far right wirth respect to the lineup of the (angled) runway. +-- +-- ## LSO Grading +-- +-- LSO grading starts when the player enters the groove. The flight path and aircraft attitude is evaluated at certain steps +-- +-- * **X** At the Start +-- * **IM** In the Middle +-- * **IC** In Close +-- * **AR** At the Ramp +-- * **IW** In the Wiress +-- +-- Grading at each step includes the above calls, i.e. +-- +-- * **L**ined **U**p **L**eft or **R**ight: LUL, LUR +-- * Too **H**igh or too **L**ow: H, L +-- * Too **F**ast or too **SLO**w: F, SLO +-- +-- Each grading, x, is subdivided by +-- +-- * (x): parenthesis, indicating "a little" for a minor deviation and +-- * \_x\_: underline, indicating "a lot" for major deviations. +-- +-- The position at the landing event is analyzed and the corresponding trapped wire calculated. If no wire was caught, the LSO will give the bolter call. +-- +-- If a player is sigifiantly off from the ideal parameters in close or at the ramp, the LSO will wave the player off. +-- +-- ## Pattern Wave Off +-- +-- The player's aircraft position is evaluated at certain critical locations in the landing pattern. If the player is far off from the ideal approach, the LSO will +-- issue a pattern wave off. Currently, this is only implemented for Case I recoveries and the Case I part in the Case II recovery, i.e. +-- +-- * Break Entry +-- * Early Break +-- * Late Break +-- * Abeam +-- * Ninety +-- * Wake +-- * Groove +-- +-- At these points it is also checked if a player comes too close to another aircraft ahead of him in the pattern. +-- +-- ## Grading Points +-- +-- Currently grades are given by as follows +-- +-- * 5.0 Points **\_OK\_**: "Okay underline", given only for a perfect pass, i.e. when no deviations at all were observed by the LSO. The unicorn! +-- * 4.0 Points **OK**: "Okay pass" when only minor () deviations happend. +-- * 3.0 Points **(OK)**: "Fair pass", when only "normal" deviations were detected. +-- * 2.0 Points **--**: "No grade, for larger deviations. +-- +-- Furthermore, we have the cases: +-- +-- * 2.5 Points **B**: "Bolder", when the player landed but did not catch a wire. +-- * 1.0 Points **WO**: "Wave-Off": Player got waved off in the final parts of the groove. +-- * 1.0 Points **PWO**: "Pattern Wave-Off", when pilot was far away from where he should be in the pattern. For example, being long in the groove gives a "LIG PWO". +-- * 0.0 Point **CUT**: "Cut pass", when player was waved off but landed anyway. +-- +-- # AI Handling +-- +-- The @{#AIRBOSS} class allows to handle incoming AI units and integrate them into the marshal and landing pattern. +-- +-- By default, incoming carrier capable aircraft which are detecting inside the Carrier Controlled Area (CCA) and approach the carrier by more than 5 NM are automatically guided to the holding zone. +-- Each AI group gets its own marshal stack in the holding pattern. Once a recovery window opens, the AI group of the lowest stack is transitioning to the landing pattern +-- and the Marshal stack collapses. +-- +-- If no AI handling is desired, this can be turned off via the @{#AIRBOSS.SetHandleAIOFF} function. +-- +-- ## Known Issues +-- +-- Dealing with the DCS AI is a big challenge and there is only so much one can do. Please bear this in mind! +-- +-- ### Pattern Updates +-- +-- The holding position of the AI is updated regularly when the carrier has changed its position by more then 2.5 NM or changed its course significantly. +-- The patterns are realized by orbit or racetrack patterns of the DCS scripting API. +-- However, when the position is updated or the marshal stack collapses, it comes to disruptions of the regular orbit because a new waypoint with a new +-- orbit task needs to be created. +-- +-- ### Recovery Cases +-- +-- The AI performs a very realistic Case I recovery. Therefore, we already have a good Case I and II recovery simulation since the final part of Case II is a +-- Case I recovery. However, I don't think the AI can do a proper Case III recovery. If you give the AI the landing command, it is out of our hands and will +-- always go for a Case I in the final pattern part. Maybe this will improve in future DCS version but right now, there is not much we can do about it. +-- +-- # Debugging +-- +-- In case you have problems, it is always a good idea to have a look at your DCS log file. You find it in your "Saved Games" folder, so for example in +-- C:\Users\\Saved Games\DCS\Logs\dcs.log +-- All output concerning the @{#AIRBOSS} class should have the string "AIRBOSS" in the corresponding line. +-- Searching for lines that contain the string "error" or "nil" can also give you a hint what's wrong. +-- +-- The verbosity of the output can be increased by adding the following lines to your script: +-- +-- BASE:TraceOnOff(true) +-- BASE:TraceLevel(1) +-- BASE:TraceClass("AIRBOSS") +-- +-- To get even more output you can increase the trace level to 2 or even 3, c.f. @{Core.Base#BASE} for more details. +-- +-- ## Debug Mode +-- +-- You have the option to enable the debug mode for this class via the @{#AIRBOSS.SetDebugModeON} function. +-- If enabled, status and debug text messages will be displayed on the screen. Also informative marks on the F10 map are created. +-- +-- @field #AIRBOSS +AIRBOSS = { + ClassName = "AIRBOSS", + Debug = false, + lid = nil, + carrier = nil, + carriertype = nil, + carrierparam = {}, + alias = nil, + airbase = nil, + waypoints = {}, + currentwp = nil, + beacon = nil, + TACANon = nil, + TACANchannel = nil, + TACANmode = nil, + TACANmorse = nil, + ICLSon = nil, + ICLSchannel = nil, + ICLSmorse = nil, + LSORadio = nil, + LSOFreq = nil, + LSOModu = nil, + MarshalRadio = nil, + MarshalFreq = nil, + MarshalModu = nil, + radiotimer = nil, + zoneCCA = nil, + zoneCCZ = nil, + zoneInitial = nil, + players = {}, + menuadded = {}, + BreakEntry = {}, + BreakEarly = {}, + BreakLate = {}, + Abeam = {}, + Ninety = {}, + Wake = {}, + Final = {}, + Groove = {}, + Platform = {}, + DirtyUp = {}, + Bullseye = {}, + defaultcase = nil, + case = nil, + defaultoffset = nil, + holdingoffset = nil, + recoverytimes = {}, + flights = {}, + Qpattern = {}, + Qmarshal = {}, + RQMarshal = {}, + RQLSO = {}, + Nmaxpattern = nil, + handleai = nil, + tanker = nil, + warehouse = nil, + Corientation = nil, + Corientlast = nil, + Cposition = nil, + defaultskill = nil, + adinfinitum = nil, + magvar = nil, + Tcollapse = nil, +} + +--- Player aircraft types capable of landing on carriers. +-- @type AIRBOSS.AircraftPlayer +-- @field #string AV8B AV-8B Night Harrier (not yet supported). +-- @field #string HORNET F/A-18C Lot 20 Hornet. +-- @field #string A4EC Community A-4E-C mod. +AIRBOSS.AircraftPlayer={ + --AV8B="AV8BNA", + HORNET="FA-18C_hornet", + A4EC="A-4E-C", +} + +--- Aircraft types capable of landing on carrier (human+AI). +-- @type AIRBOSS.AircraftCarrier +-- @field #string AV8B AV-8B Night Harrier (not yet supported). +-- @field #string HORNET F/A-18C Lot 20 Hornet. +-- @field #string A4EC Community A-4E mod. +-- @field #string S3B Lockheed S-3B Viking. +-- @field #string S3BTANKER Lockheed S-3B Viking tanker. +-- @field #string E2D Grumman E-2D Hawkeye AWACS. +-- @field #string FA18C F/A-18C Hornet (AI). +-- @field #string F14A F-14A Tomcat (AI). +AIRBOSS.AircraftCarrier={ + --AV8B="AV8BNA", + HORNET="FA-18C_hornet", + A4EC="A-4E-C", + S3B="S-3B", + S3BTANKER="S-3B Tanker", + E2D="E-2C", + FA18C="F/A-18C", + F14A="F-14A", +} + +--- Carrier types. +-- @type AIRBOSS.CarrierType +-- @field #string STENNIS USS John C. Stennis (CVN-74) +-- @field #string VINSON USS Carl Vinson (CVN-70) +-- @field #string TARAWA USS Tarawa (LHA-1) +-- @field #string KUZNETSOV Admiral Kuznetsov (CV 1143.5) +AIRBOSS.CarrierType={ + STENNIS="Stennis", + VINSON="Vinson", + TARAWA="LHA_Tarawa", + KUZNETSOV="KUZNECOW", +} + +--- Carrier specific parameters. +-- @type AIRBOSS.CarrierParameters +-- @field #number rwyangle Runway angle in degrees. for carriers with angled deck. For USS Stennis -9 degrees. +-- @field #number sterndist Distance in meters from carrier position to stern of carrier. For USS Stennis -150 meters. +-- @field #number deckheight Height of deck in meters. For USS Stennis ~63 ft = 19 meters. +-- @field #number wire1 Distance in meters from carrier position to first wire. +-- @field #number wire2 Distance in meters from carrier position to second wire. +-- @field #number wire3 Distance in meters from carrier position to third wire. +-- @field #number wire4 Distance in meters from carrier position to fourth wire. +-- @field #number rwylength Length of the landing runway in meters. +-- @field #number rwywidth Width of the landing runway in meters. +-- @field #number totlength Total length of carrier. +-- @field #number totwidthstarboard Total with of the carrier from stern position to starboard side (asymmetric carriers). +-- @field #number totwidthport Total with of the carrier from stern position to port side (asymmetric carriers). + +--- Aircraft specific Angle of Attack (AoA) (or alpha) parameters. +-- @type AIRBOSS.AircraftAoA +-- @field #number OnSpeedMin Minimum on speed AoA. Values below are fast +-- @field #number OnSpeedMax Maximum on speed AoA. Values above are slow. +-- @field #number OnSpeed Optimal on-speed AoA. +-- @field #number Fast Fast AoA threshold. Smaller means faster. +-- @field #number Slow Slow AoA threshold. Larger means slower. +-- @field #number FAST Really fast AoA threshold. +-- @field #number SLOW Really slow AoA threshold. + +--- Pattern steps. +-- @type AIRBOSS.PatternStep +-- @field #string UNDEFINED "Undefined". +-- @field #string REFUELING "Refueling". +-- @field #string SPINNING "Spinning". +-- @field #string COMMENCING "Commencing". +-- @field #string HOLDING "Holding". +-- @field #string PLATFORM "Platform". +-- @field #string ARCIN "Arc Turn In". +-- @field #string ARCOUT "Arc Turn Out". +-- @field #string DIRTYUP "Dirty Up". +-- @field #string BULLSEYE "Bullseye". +-- @field #string INITIAL "Initial". +-- @field #string BREAKENTRY "Break Entry". +-- @field #string EARLYBREAK "Early Break". +-- @field #string LATEBREAK "Late Break". +-- @field #string ABEAM "Abeam". +-- @field #string NINETY "Ninety". +-- @field #string WAKE "Wake". +-- @field #string FINAL "Final". +-- @field #string GROOVE_XX "Groove X". +-- @field #string GROOVE_RB "Groove Roger Ball". +-- @field #string GROOVE_IM "Groove In the Middle". +-- @field #string GROOVE_IC "Groove In Close". +-- @field #string GROOVE_AR "Groove At the Ramp". +-- @field #string GROOVE_IW "Groove In the Wires". +-- @field #string DEBRIEF "Debrief". +AIRBOSS.PatternStep={ + UNDEFINED="Undefined", + REFUELING="Refueling", + SPINNING="Spinning", + COMMENCING="Commencing", + HOLDING="Holding", + PLATFORM="Platform", + ARCIN="Arc Turn In", + ARCOUT="Arc Turn Out", + DIRTYUP="Dirty Up", + BULLSEYE="Bullseye", + INITIAL="Initial", + BREAKENTRY="Break Entry", + EARLYBREAK="Early Break", + LATEBREAK="Late Break", + ABEAM="Abeam", + NINETY="Ninety", + WAKE="Wake", + FINAL="Turn Final", + GROOVE_XX="Groove X", + GROOVE_RB="Groove Roger Ball", + GROOVE_IM="Groove In the Middle", + GROOVE_IC="Groove In Close", + GROOVE_AR="Groove At the Ramp", + GROOVE_IW="Groove In the Wires", + DEBRIEF="Debrief", +} + +--- Radio sound file and subtitle. +-- @type AIRBOSS.RadioCall +-- @field #string file Sound file name without suffix. +-- @field #string suffix File suffix/extention, e.g. "ogg". +-- @field #boolean loud Loud version of sound file available. +-- @field #string subtitle Subtitle displayed during transmission. +-- @field #number duration Duration of the sound in seconds. This is also the duration the subtitle is displayed. + +--- LSO radio calls. +-- @type AIRBOSS.LSOCall +-- @field #AIRBOSS.RadioCall RADIOCHECK "Paddles, radio check" call. +-- @field #AIRBOSS.RadioCall RIGHTFORLINEUP "Right for line up" call. +-- @field #AIRBOSS.RadioCall COMELEFT "Come left" call. +-- @field #AIRBOSS.RadioCall HIGH "You're high" call. +-- @field #AIRBOSS.RadioCall LOW "You're low" call. +-- @field #AIRBOSS.RadioCall POWER "Power" call. +-- @field #AIRBOSS.RadioCall FAST "You're fast" call. +-- @field #AIRBOSS.RadioCall SLOW "You're slow" call. +-- @field #AIRBOSS.RadioCall PADDLESCONTACT "Paddles, contact" call. +-- @field #AIRBOSS.RadioCall CALLTHEBALL "Call the Ball" +-- @field #AIRBOSS.RadioCall ROGERBALL "Roger ball" call. +-- @field #AIRBOSS.RadioCall WAVEOFF "Wave off" call +-- @field #AIRBOSS.RadioCall BOLTER "Bolter, Bolter" call +-- @field #AIRBOSS.RadioCall LONGINGROOVE "You're long in the groove" call. +-- @field #AIRBOSS.RadioCall DEPARTANDREENTER "Depart and re-enter" call. +-- @field #AIRBOSS.RadioCall WELCOMEABOARD "Welcome aboard" call. +-- @field #AIRBOSS.RadioCall N0 "Zero" call. +-- @field #AIRBOSS.RadioCall N1 "One" call. +-- @field #AIRBOSS.RadioCall N2 "Two" call. +-- @field #AIRBOSS.RadioCall N3 "Three" call. +-- @field #AIRBOSS.RadioCall N4 "Four" call. +-- @field #AIRBOSS.RadioCall N5 "Five" call. +-- @field #AIRBOSS.RadioCall N6 "Six" call. +-- @field #AIRBOSS.RadioCall N7 "Seven" call. +-- @field #AIRBOSS.RadioCall N8 "Eight" call. +-- @field #AIRBOSS.RadioCall N9 "Nine" call. +AIRBOSS.LSOCall={ + RADIOCHECK={ + file="LSO-RadioCheck", + suffix="ogg", + loud=false, + subtitle="Paddles, radio check", + duration=1.1, + }, + RIGHTFORLINEUP={ + file="LSO-RightForLineup", + suffix="ogg", + loud=true, + subtitle="Right for line up", + duration=0.80, + }, + COMELEFT={ + file="LSO-ComeLeft", + suffix="ogg", + loud=true, + subtitle="Come left", + duration=0.60, + }, + HIGH={ + file="LSO-High", + suffix="ogg", + loud=true, + subtitle="You're high", + duration=0.65, + }, + LOW={ + file="LSO-Low", + suffix="ogg", + loud=true, + subtitle="You're low", + duration=0.50, + }, + POWER={ + file="LSO-Power", + suffix="ogg", + loud=true, + subtitle="Power", + duration=0.50, --0.45 was too short + }, + SLOW={ + file="LSO-Slow", + suffix="ogg", + loud=true, + subtitle="You're slow", + duration=0.65, + }, + FAST={ + file="LSO-Fast", + suffix="ogg", + loud=true, + subtitle="You're fast", + duration=0.7, + }, + CALLTHEBALL={ + file="LSO-CallTheBall", + suffix="ogg", + loud=false, + subtitle="Call the ball", + duration=0.6, + }, + ROGERBALL={ + file="LSO-RogerBall", + suffix="ogg", + loud=false, + subtitle="Roger ball", + duration=0.7, + }, + WAVEOFF={ + file="LSO-WaveOff", + suffix="ogg", + loud=false, + subtitle="Wave off", + duration=0.6, + }, + BOLTER={ + file="LSO-BolterBolter", + suffix="ogg", + loud=false, + subtitle="Bolter, Bolter", + duration=0.75, + }, + LONGINGROOVE={ + file="LSO-LongInTheGroove", + suffix="ogg", + loud=false, + subtitle="You're long in the groove", + duration=1.2, + }, + DEPARTANDREENTER={ + file="LSO-DepartAndReenter", + suffix="ogg", + loud=false, + subtitle="Depart and re-enter", + duration=1.1, + }, + PADDLESCONTACT={ + file="LSO-PaddlesContact", + suffix="ogg", + loud=false, + subtitle="Paddles, contact", + duration=1.0, + }, + WELCOMEABOARD={ + file="LSO-WelcomeAboard", + suffix="ogg", + loud=false, + subtitle="Welcome aboard", + duration=0.9, + }, + N0={ + file="LSO-N0", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N1={ + file="LSO-N1", + suffix="ogg", + loud=false, + subtitle="", + duration=0.25, + }, + N2={ + file="LSO-N2", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N3={ + file="LSO-N3", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N4={ + file="LSO-N4", + suffix="ogg", + loud=false, + subtitle="", + duration=0.39, + }, + N5={ + file="LSO-N5", + suffix="ogg", + loud=false, + subtitle="", + duration=0.38, + }, + N6={ + file="LSO-N6", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N7={ + file="LSO-N7", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N8={ + file="LSO-N8", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N9={ + file="LSO-N9", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, --0.38 too short + }, +} + +--- Marshal radio calls. +-- @type AIRBOSS.MarshalCall +-- @field #AIRBOSS.RadioCall RADIOCHECK "Radio check" call. +-- @field #AIRBOSS.RadioCall SAYNEEDLES "Say needles" call. +-- @field #AIRBOSS.RadioCall FLYNEEDLES "Fly your needles" call. +-- @field #AIRBOSS.RadioCall N0 "Zero" call. +-- @field #AIRBOSS.RadioCall N1 "One" call. +-- @field #AIRBOSS.RadioCall N2 "Two" call. +-- @field #AIRBOSS.RadioCall N3 "Three" call. +-- @field #AIRBOSS.RadioCall N4 "Four" call. +-- @field #AIRBOSS.RadioCall N5 "Five" call. +-- @field #AIRBOSS.RadioCall N6 "Six" call. +-- @field #AIRBOSS.RadioCall N7 "Seven" call. +-- @field #AIRBOSS.RadioCall N8 "Eight" call. +-- @field #AIRBOSS.RadioCall N9 "Nine" call. +AIRBOSS.MarshalCall={ + RADIOCHECK={ + file="MARSHAL-RadioCheck", + suffix="ogg", + loud=false, + subtitle="Radio check", + duration=1.0, + }, + SAYNEEDLES={ + file="MARSHAL-SayNeedles", + suffix="ogg", + loud=false, + subtitle="Say needles", + duration=0.9, + }, + FLYNEEDLES={ + file="MARSHAL-FlyYourNeedles", + suffix="ogg", + loud=false, + subtitle="Fly your needles", + duration=0.9, + }, + -- TODO: Other voice overs for marshal. + N0={ + file="LSO-N0", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N1={ + file="LSO-N1", + suffix="ogg", + loud=false, + subtitle="", + duration=0.25, + }, + N2={ + file="LSO-N2", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N3={ + file="LSO-N3", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N4={ + file="LSO-N4", + suffix="ogg", + loud=false, + subtitle="", + duration=0.39, + }, + N5={ + file="LSO-N5", + suffix="ogg", + loud=false, + subtitle="", + duration=0.38, + }, + N6={ + file="LSO-N6", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N7={ + file="LSO-N7", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N8={ + file="LSO-N8", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N9={ + file="LSO-N9", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, --0.38 too short + }, +} + +--- Difficulty level. +-- @type AIRBOSS.Difficulty +-- @field #string EASY Flight Stutdent. Shows tips and hints in important phases of the approach. +-- @field #string NORMAL Naval aviator. Moderate number of hints but not really zip lip. +-- @field #string HARD TOPGUN graduate. For people who know what they are doing. Nearly ziplip. +AIRBOSS.Difficulty={ + EASY="Flight Student", + NORMAL="Naval Aviator", + HARD="TOPGUN Graduate", +} + +--- Recovery window parameters. +-- @type AIRBOSS.Recovery +-- @field #number START Start of recovery in seconds of abs mission time. +-- @field #number STOP End of recovery in seconds of abs mission time. +-- @field #number CASE Recovery case (1-3) of that time slot. +-- @field #number OFFSET Angle offset of the holding pattern in degrees. Usually 0, +-15, or +-30 degrees. +-- @field #boolean OPEN Recovery window is currently open. +-- @field #boolean OVER Recovery window is over and closed. + +--- Groove position. +-- @type AIRBOSS.GroovePos +-- @field #string X0 Entering the groove. +-- @field #string XX At the start, i.e. 3/4 from the run down. +-- @field #string RB Roger ball. +-- @field #string IM In the middle. +-- @field #string IC In close. +-- @field #string AR At the ramp. +-- @field #string IW In the wires. +AIRBOSS.GroovePos={ + X0="X0", + XX="X", + RB="RB", + IM="IM", + IC="IC", + AR="AR", + IW="IW", +} + +--- Groove data. +-- @type AIRBOSS.GrooveData +-- @field #number Step Current step. +-- @field #number AoA Angle of Attack. +-- @field #number Alt Altitude in meters. +-- @field #number GSE Glide slope error in degrees. +-- @field #number LUE Lineup error in degrees. +-- @field #number Roll Roll angle. +-- @field #number Rhdg Relative heading player to carrier. 0=parallel, +-90=perpendicular. +-- @field #number TGroove Time stamp when pilot entered the groove. + +--- LSO grade +-- @type AIRBOSS.LSOgrade +-- @field #string grade LSO grade, i.e. _OK_, OK, (OK), --, CUT +-- @field #number points Points received. +-- @field #string details Detailed flight analysis. +-- @field #number wire Wire caught. +-- @field #number Tgroove Time in the groove in seconds. + +--- Checkpoint parameters triggering the next step in the pattern. +-- @type AIRBOSS.Checkpoint +-- @field #string name Name of checkpoint. +-- @field #number Xmin Minimum allowed longitual distance to carrier. +-- @field #number Xmax Maximum allowed longitual distance to carrier. +-- @field #number Zmin Minimum allowed latitudal distance to carrier. +-- @field #number Zmax Maximum allowed latitudal distance to carrier. +-- @field #number LimitXmin Latitudal threshold for triggering the next step if XXmax. +-- @field #number LimitZmin Latitudal threshold for triggering the next step if ZZmax. + +--- Parameters of a flight group. +-- @type AIRBOSS.FlightGroup +-- @field Wrapper.Group#GROUP group Flight group. +-- @field #string groupname Name of the group. +-- @field #number nunits Number of units in group. +-- @field #number dist0 Distance to carrier in meters when the group was first detected inside the CCA. +-- @field #number time Time the flight was added to the queue. +-- @field Core.UserFlag#USERFLAG flag User flag for triggering events for the flight. +-- @field #boolean ai If true, flight is AI. +-- @field #boolean player If true, flight is a human player. +-- @field #string actype Aircraft type name. +-- @field #table onboardnumbers Onboard numbers of aircraft in the group. +-- @field #string onboard Onboard number of player or first unit in group. +-- @field #number case Recovery case of flight. +-- @field #string seclead Name of section lead. +-- @field #table section Other human flight groups belonging to this flight. This flight is the lead. +-- @field #boolean holding If true, flight is in holding zone. +-- @field #boolean ballcall If true, flight called the ball in the groove. +-- @field #table elements Flight group elements. + +--- Parameters of an element in a flight group. +-- @type AIRBOSS.FlightElement +-- @field Wrapper.Unit#UNIT unit Aircraft unit. +-- @field #boolean ai If true, AI sits inside. If false, human player is flying. +-- @field #string onboard Onboard number of the aircraft. +-- @field #boolean ballcall If true, flight called the ball in the groove. + +--- Player data table holding all important parameters of each player. +-- @type AIRBOSS.PlayerData +-- @field Wrapper.Unit#UNIT unit Aircraft of the player. +-- @field #string name Player name. +-- @field Wrapper.Client#CLIENT client Client object of player. +-- @field #string callsign Callsign of player. +-- @field #string difficulty Difficulty level. +-- @field #string step Current/next pattern step. +-- @field #boolean warning Set true once the player got a warning. +-- @field #number passes Number of passes. +-- @field #boolean attitudemonitor If true, display aircraft attitude and other parameters constantly. +-- @field #table debrief Debrief analysis of the current step of this pass. +-- @field #table lastdebrief Debrief of player performance of last completed pass. +-- @field #table grades LSO grades of player passes. +-- @field #boolean landed If true, player landed or attempted to land. +-- @field #boolean boltered If true, player boltered. +-- @field #boolean waveoff If true, player was waved off during final approach. +-- @field #boolean patternwo If true, player was waved of during the pattern. +-- @field #boolean lig If true, player was long in the groove. +-- @field #number Tlso Last time the LSO gave an advice. +-- @field #number Tgroove Time in the groove in seconds. +-- @field #number wire Wire caught by player when trapped. +-- @field #AIRBOSS.GroovePos groove Data table at each position in the groove. Elemets are of type @{#AIRBOSS.GrooveData}. +-- @field #table menu F10 radio menu +-- @extends #AIRBOSS.FlightGroup + +--- Main radio menu. +-- @field #table MenuF10 +AIRBOSS.MenuF10={} + +--- Airboss class version. +-- @field #string version +AIRBOSS.version="0.6.3" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Check player heading at zones, e.g. initial. +-- TODO: Include recovery tanker into next stack calculation. Angels six should be empty. +-- TODO: Get charly time estimate function. +-- TODO: Player eject and crash debrief "gradings". +-- TODO: Subtitles off options on player level. +-- TODO: PWO during case 2/3. Also when too close to other player. +-- TODO: Option to filter AI groups for recovery. +-- TODO: Spin pattern. Add radio menu entry. Not sure what to add though?! +-- TODO: Persistence of results. +-- DONE: Fix bug that player leaves the approach zone if he boltered or was waved off during Case II or III. NOTE: Partly due to increasing approach zone size. +-- DONE: Fix bug that player gets an altitude warning if stack collapses. NOTE: Would not work if two stacks Case I and II/III are used. +-- DONE: Improve radio messages. Maybe usersound for messages which are only meant for players? +-- DONE: Add voice over fly needs and welcome aboard. +-- DONE: Improve trapped wire calculation. +-- DONE: Carrier zone with dimensions of carrier. to check if landing happend on deck. +-- DONE: Carrier runway zone for fould deck check. +-- DONE: More Hints for Case II/III. +-- DONE: Set magnetic declination function. +-- DONE: First send AI to marshal and then allow them into the landing pattern ==> task function when reaching the waypoint. +-- DONE: Extract (static) weather from mission for cloud covery etc. +-- DONE: Check distance to players during approach. +-- DONE: Option to turn AI handling off. +-- DONE: Add user functions. +-- DONE: Update AI holding pattern wrt to moving carrier. +-- DONE: Generalize parameters for other carriers. +-- DONE: Generalize parameters for other aircraft. +-- DONE: Add radio check (LSO, AIRBOSS) to F10 radio menu. +-- DONE: Right pattern step after bolter/wo/patternWO? Guess so. +-- DONE: Set case II and III times (via recovery time). +-- DONE: Get correct wire when trapped. DONE but might need further tweaking. +-- DONE: Add radio transmission queue for LSO and airboss. +-- TONE: CASE II. +-- DONE: CASE III. +-- NOPE: Strike group with helo bringing cargo etc. Not yet. +-- DONE: Handle crash event. Delete A/C from queue, send rescue helo. +-- DONE: Get fuel state in pounds. (working for the hornet, did not check others) +-- DONE: Add aircraft numbers in queue to carrier info F10 radio output. +-- DONE: Monitor holding of players/AI in zoneHolding. +-- DONE: Transmission via radio. +-- DONE: Get board numbers. +-- DONE: Get an _OK_ pass if long in groove. Possible other pattern wave offs as well?! +-- DONE: Add scoring to radio menu. +-- DONE: Optimized debrief. +-- DONE: Add automatic grading. +-- DONE: Fix radio menu. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new AIRBOSS class object for a specific aircraft carrier unit. +-- @param #AIRBOSS self +-- @param carriername Name of the aircraft carrier unit as defined in the mission editor. +-- @param alias (Optional) Alias for the carrier. This will be used for radio messages and the F10 radius menu. Default is the carrier name as defined in the mission editor. +-- @return #AIRBOSS self or nil if carrier unit does not exist. +function AIRBOSS:New(carriername, alias) + + -- Inherit everthing from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #AIRBOSS + + -- Debug. + self:F2({carriername=carriername, alias=alias}) + + -- Set carrier unit. + self.carrier=UNIT:FindByName(carriername) + + -- Check if carrier unit exists. + if self.carrier==nil then + -- Error message. + local text=string.format("ERROR: Carrier unit %s could not be found! Make sure this UNIT is defined in the mission editor and check the spelling of the unit name carefully.", carriername) + MESSAGE:New(text, 120):ToAll() + self:E(text) + return nil + end + + -- Debug trace. + if false then + self.Debug=true + BASE:TraceOnOff(true) + BASE:TraceClass(self.ClassName) + BASE:TraceLevel(1) + end + + -- Set some string id for output to DCS.log file. + self.lid=string.format("AIRBOSS %s | ", carriername) + + -- Get carrier type. + self.carriertype=self.carrier:GetTypeName() + + -- Set alias. + self.alias=alias or carriername + + -- Set carrier airbase object. + self.airbase=AIRBASE:FindByName(carriername) + + -- Create carrier beacon. + self.beacon=BEACON:New(self.carrier) + + ------------- + --- Defaults: + ------------- + + -- Set up Airboss radio. + self.MarshalRadio=RADIO:New(self.carrier) + self.MarshalRadio:SetAlias("MARSHAL") + self:SetMarshalRadio() + + -- Set up LSO radio. + self.LSORadio=RADIO:New(self.carrier) + self.LSORadio:SetAlias("LSO") + self:SetLSORadio() + + -- Radio scheduler. + self.radiotimer=SCHEDULER:New() + + -- Set magnetic declination. + self:SetMagneticDeclination() + + -- Set ICSL to channel 1. + self:SetICLS() + + -- Set TACAN to channel 74X. + self:SetTACAN() + + -- Set max aircraft in landing pattern. + self:SetMaxLandingPattern() + + -- Set AI handling On. + self:SetHandleAION() + + -- Default recovery case. This sets self.defaultcase and self.case. + self:SetRecoveryCase(1) + + -- Set holding offset to 0 degrees. This set self.defaultoffset and self.holdingoffset. + self:SetHoldingOffsetAngle() + + -- Default player skill EASY. + self:SetDefaultPlayerSkill(AIRBOSS.Difficulty.EASY) + + -- CCA 50 NM radius zone around the carrier. + self:SetCarrierControlledArea() + + -- CCZ 5 NM radius zone around the carrier. + self:SetCarrierControlledZone() + + -- Carrier patrols its waypoints until the end of time. + self:SetPatrolAdInfinitum(true) + + -- Init carrier parameters. + if self.carriertype==AIRBOSS.CarrierType.STENNIS then + self:_InitStennis() + elseif self.carriertype==AIRBOSS.CarrierType.VINSON then + -- TODO: Carl Vinson parameters. + self:_InitStennis() + elseif self.carriertype==AIRBOSS.CarrierType.TARAWA then + -- TODO: Tarawa parameters. + self:_InitStennis() + elseif self.carriertype==AIRBOSS.CarrierType.KUZNETSOV then + -- Kusnetsov parameters - maybe... + self:_InitStennis() + else + self:E(self.lid.."ERROR: Unknown carrier type!") + return nil + end + + -- CASE I/II moving zone: Zone 2.75 NM astern and 0.1 NM starboard of the carrier with a diameter of 1 NM. + self.zoneInitial=ZONE_UNIT:New("Initial Zone", self.carrier, UTILS.NMToMeters(0.5), {dx=-UTILS.NMToMeters(2.75), dy=UTILS.NMToMeters(0.1), relative_to_unit=true}) + + -- Smoke zones. + if self.Debug and false then + local case=2 + self:_GetZoneBullseye(case):SmokeZone(SMOKECOLOR.White, 45) + self:_GetZoneDirtyUp(case):SmokeZone(SMOKECOLOR.Orange, 45) + self:_GetZoneArcIn(case):SmokeZone(SMOKECOLOR.Blue, 45) + self:_GetZoneArcOut(case):SmokeZone(SMOKECOLOR.Blue, 45) + self:_GetZonePlatform(case):SmokeZone(SMOKECOLOR.Red, 45) + self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green, 45) + end + + -- Carrier parameter debug tests. + if false then + -- Stern coordinate. + local FB=self:GetFinalBearing(false) + local hdg=self:GetHeading(false) + + -- Stern pos. + local stern=self:_GetSternCoord() + + -- Bow pos. + local bow=stern:Translate(self.carrierparam.totlength, hdg) + + -- End of rwy. + local rwy=stern:Translate(self.carrierparam.rwylength, FB, true) + + --- Flare points and zones. + local function flareme() + + -- Carrier pos. + self:GetCoordinate():FlareYellow() + + -- Stern + stern:FlareGreen() + + -- Bow + bow:FlareYellow() + + -- Runway half width = 10 m. + local r1=stern:Translate(self.carrierparam.rwywidth*0.5, FB+90) + local r2=stern:Translate(self.carrierparam.rwywidth*0.5, FB-90) + r1:FlareWhite() + r2:FlareWhite() + + -- End of runway. + rwy:FlareRed() + + -- Right 30 meters from stern. + local cR=stern:Translate(self.carrierparam.totwidthstarboard, hdg+90) + cR:FlareYellow() + + -- Left 40 meters from stern. + local cL=stern:Translate(self.carrierparam.totwidthport, hdg-90) + cL:FlareYellow() + + -- Flare wires. + local w1=stern:Translate(self.carrierparam.wire1, FB) + local w2=stern:Translate(self.carrierparam.wire2, FB) + local w3=stern:Translate(self.carrierparam.wire3, FB) + local w4=stern:Translate(self.carrierparam.wire4, FB) + w1:FlareWhite() + w2:FlareYellow() + w3:FlareWhite() + w4:FlareYellow() + + -- Flare carrier and landing runway. + local cbox=self:_GetZoneCarrierBox() + local rbox=self:_GetZoneRunwayBox() + cbox:FlareZone(FLARECOLOR.Green, 5, nil, self.carrierparam.deckheight) + rbox:FlareZone(FLARECOLOR.White, 5, nil, self.carrierparam.deckheight) + end + + -- Flare points every 3 seconds for 3 minutes. + SCHEDULER:New(nil, flareme, {}, 1, 3, nil, 180) + end + + -- If calls should be part of self and individual for different carriers. + --[[ + -- Init default sound files. + for _name,_sound in pairs(AIRBOSS.LSOCall) do + local sound=_sound --#AIRBOSS.RadioCall + local text=string.format() + sound.subtitle=1 + sound.loud=1 + --self.radiocall[_name]=sound + end + ]] + + -- Debug: + if false then + local text="Playing default sound files:" + for _name,_call in pairs(AIRBOSS.LSOCall) do + local call=_call --#AIRBOSS.RadioCall + + -- Debug text. + text=text..string.format("\nFile=%s.%s, duration=%.2f sec, loud=%s, subtitle=\"%s\".", call.file, call.suffix, call.duration, tostring(call.loud), call.subtitle) + + -- Radio transmission to queue. + self:RadioTransmission(self.LSORadio, call, false, 10) + + -- Also play the loud version. + if call.loud then + self:RadioTransmission(self.LSORadio, call, true, 10) + end + end + self:I(self.lid..text) + end + + + ----------------------- + --- FSM Transitions --- + ----------------------- + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Idle") -- Start AIRBOSS script. + self:AddTransition("*", "Idle", "Idle") -- Carrier is idling. + self:AddTransition("Idle", "RecoveryStart", "Recovering") -- Start recovering aircraft. + self:AddTransition("Recovering", "RecoveryStop", "Idle") -- Stop recovering aircraft. + self:AddTransition("*", "Status", "*") -- Update status of players and queues. + self:AddTransition("*", "RecoveryCase", "*") -- Switch to another case recovery. + self:AddTransition("*", "Stop", "Stopped") -- Stop AIRBOSS FMS. + + + --- Triggers the FSM event "Start" that starts the airboss. Initializes parameters and starts event handlers. + -- @function [parent=#AIRBOSS] Start + -- @param #AIRBOSS self + + --- Triggers the FSM event "Start" that starts the airboss after a delay. Initializes parameters and starts event handlers. + -- @function [parent=#AIRBOSS] __Start + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Idle" that puts the carrier into state "Idle" where no recoveries are carried out. + -- @function [parent=#AIRBOSS] Idle + -- @param #AIRBOSS self + + --- Triggers the FSM delayed event "Idle" that puts the carrier into state "Idle" where no recoveries are carried out. + -- @function [parent=#AIRBOSS] __Idle + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "RecoveryStart" that starts the recovery of aircraft. Marshalling aircraft are send to the landing pattern. + -- @function [parent=#AIRBOSS] RecoveryStart + -- @param #AIRBOSS self + -- @param #number Case Recovery case (1, 2 or 3) that is started. + -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. + + --- Triggers the FSM delayed event "RecoveryStart" that starts the recovery of aircraft. Marshalling aircraft are send to the landing pattern. + -- @function [parent=#AIRBOSS] __RecoveryStart + -- @param #number delay Delay in seconds. + -- @param #AIRBOSS self + -- @param #number Case Recovery case (1, 2 or 3) that is started. + -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. + + + --- Triggers the FSM event "RecoveryStop" that stops the recovery of aircraft. + -- @function [parent=#AIRBOSS] RecoveryStop + -- @param #AIRBOSS self + + --- Triggers the FSM delayed event "RecoveryStop" that stops the recovery of aircraft. + -- @function [parent=#AIRBOSS] __RecoveryStop + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "RecoveryCase" that switches the aircraft recovery case. + -- @function [parent=#AIRBOSS] RecoveryCase + -- @param #AIRBOSS self + -- @param #number Case The new recovery case (1, 2 or 3). + -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. + + --- Triggers the delayed FSM event "RecoveryCase" that sets the used aircraft recovery case. + -- @function [parent=#AIRBOSS] __Case + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + -- @param #number Case The new recovery case (1, 2 or 3). + -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. + + + --- Triggers the FSM event "Stop" that stops the airboss. Event handlers are stopped. + -- @function [parent=#AIRBOSS] Stop + -- @param #AIRBOSS self + + --- Triggers the FSM event "Stop" that stops the airboss after a delay. Event handlers are stopped. + -- @function [parent=#AIRBOSS] __Stop + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- USER API Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set carrier controlled area (CCA). +-- This is a large zone around the carrier, which is constantly updated wrt the carrier position. +-- @param #AIRBOSS self +-- @param #number radius Radius of zone in nautical miles (NM). Default 50 NM. +-- @return #AIRBOSS self +function AIRBOSS:SetCarrierControlledArea(radius) + + radius=UTILS.NMToMeters(radius or 50) + + self.zoneCCA=ZONE_UNIT:New("Carrier Controlled Area", self.carrier, radius) + + return self +end + +--- Set carrier controlled zone (CCZ). +-- This is a small zone (usually 5 NM radius) around the carrier, which is constantly updated wrt the carrier position. +-- @param #AIRBOSS self +-- @param #number radius Radius of zone in nautical miles (NM). Default 5 NM. +-- @return #AIRBOSS self +function AIRBOSS:SetCarrierControlledZone(radius) + + radius=UTILS.NMToMeters(radius or 5) + + self.zoneCCZ=ZONE_UNIT:New("Carrier Controlled Zone", self.carrier, radius) + + return self +end + +--- Set the default recovery case. +-- @param #AIRBOSS self +-- @param #number case Case of recovery. Either 1, 2 or 3. Default 1. +-- @return #AIRBOSS self +function AIRBOSS:SetRecoveryCase(case) + + -- Set default case or 1. + self.defaultcase=case or 1 + + -- Current case init. + self.case=self.defaultcase + + return self +end + +--- Set holding pattern offset from final bearing for Case II/III recoveries. +-- Usually, this is +-15 or +-30 degrees. You should not use and offet angle >= 90 degrees, because this will cause a devision by zero in some of the equations used to calculate the approach corridor. +-- So best stick to the defaults up to 30 degrees. +-- @param #AIRBOSS self +-- @param #number offset Offset angle in degrees. Default 0. +-- @return #AIRBOSS self +function AIRBOSS:SetHoldingOffsetAngle(offset) + + -- Set default angle or 0. + self.defaultoffset=offset or 0 + + -- Current offset init. + self.holdingoffset=self.defaultoffset + + return self +end + +--- Add aircraft recovery time window and recovery case. +-- @param #AIRBOSS self +-- @param #string starttime Start time, e.g. "8:00" for eight o'clock. Default now. +-- @param #string stoptime Stop time, e.g. "9:00" for nine o'clock. Default 90 minutes after start time. +-- @param #number case Recovery case for that time slot. Number between one and three. +-- @param #number holdingoffset Only for CASE II/III: Angle in degrees the holding pattern is offset. +-- @return #AIRBOSS self +function AIRBOSS:AddRecoveryWindow(starttime, stoptime, case, holdingoffset) + + -- Absolute mission time in seconds. + local Tnow=timer.getAbsTime() + + -- Input or now. + starttime=starttime or UTILS.SecondsToClock(Tnow) + + -- Set start time. + local Tstart=UTILS.ClockToSeconds(starttime) + + -- Set stop time. + local Tstop=UTILS.ClockToSeconds(stoptime or Tstart+90*60) + + -- Consistancy check for timing. + if Tstart>Tstop then + self:E(string.format("ERROR: Recovery stop time %s lies before recovery start time %s! Recovery windows rejected.", UTILS.SecondsToClock(Tstart), UTILS.SecondsToClock(Tstop))) + return self + end + if Tstop<=Tnow then + self:E(string.format("ERROR: Recovery stop time %s already over. Tnow=%s! Recovery windows rejected.", UTILS.SecondsToClock(Tstop), UTILS.SecondsToClock(Tnow))) + return self + end + + -- Case or default value. + case=case or self.defaultcase + + -- Holding offset or default value. + holdingoffset=holdingoffset or self.defaultoffset + + -- Offset zero for case I. + if case==1 then + holdingoffset=0 + end + + -- Recovery window. + local recovery={} --#AIRBOSS.Recovery + recovery.START=Tstart + recovery.STOP=Tstop + recovery.CASE=case + recovery.OFFSET=holdingoffset + recovery.OPEN=false + recovery.OVER=false + + -- Add to table + table.insert(self.recoverytimes, recovery) + + return self +end + +--- Disable automatic TACAN activation +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:SetTACANoff() + self.TACANon=false +end + +--- Set TACAN channel of carrier. +-- @param #AIRBOSS self +-- @param #number channel TACAN channel. Default 74. +-- @param #string mode TACAN mode, i.e. "X" or "Y". Default "X". +-- @param #string morsecode Morse code identifier. Three letters, e.g. "STN". +-- @return #AIRBOSS self +function AIRBOSS:SetTACAN(channel, mode, morsecode) + + self.TACANchannel=channel or 74 + self.TACANmode=mode or "X" + self.TACANmorse=morsecode or "STN" + self.TACANon=true + + return self +end + +--- Disable automatic ICLS activation. +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:SetICLSoff() + self.ICLSon=false +end + +--- Set ICLS channel of carrier. +-- @param #AIRBOSS self +-- @param #number channel ICLS channel. Default 1. +-- @param #string morsecode Morse code identifier. Three letters, e.g. "STN". Default "STN". +-- @return #AIRBOSS self +function AIRBOSS:SetICLS(channel, morsecode) + + self.ICLSchannel=channel or 1 + self.ICLSmorse=morsecode or "STN" + self.ICLSon=true + + return self +end + + +--- Set LSO radio frequency and modulation. Default frequency is 264 MHz AM. +-- @param #AIRBOSS self +-- @param #number frequency Frequency in MHz. Default 264 MHz. +-- @param #string modulation Modulation, i.e. "AM" (default) or "FM". +-- @return #AIRBOSS self +function AIRBOSS:SetLSORadio(frequency, modulation) + + self.LSOFreq=frequency or 264 + self.LSOModu=modulation or "AM" + + if modulation=="FM" then + self.LSOModu=radio.modulation.FM + else + self.LSOModu=radio.modulation.AM + end + + self.LSORadio:SetFrequency(self.LSOFreq) + self.LSORadio:SetModulation(self.LSOModu) + + return self +end + +--- Set carrier radio frequency and modulation. Default frequency is 305 MHz AM. +-- @param #AIRBOSS self +-- @param #number frequency Frequency in MHz. Default 305 MHz. +-- @param #string modulation Modulation, i.e. "AM" (default) or "FM". +-- @return #AIRBOSS self +function AIRBOSS:SetMarshalRadio(frequency, modulation) + + self.MarshalFreq=frequency or 305 + self.MarshalModu=modulation or "AM" + + if modulation=="FM" then + self.MarshalModu=radio.modulation.FM + else + self.MarshalModu=radio.modulation.AM + end + + self.MarshalRadio:SetFrequency(self.MarshalFreq) + self.MarshalRadio:SetModulation(self.MarshalModu) + + return self +end + +--- Set number of aircraft units which can be in the landing pattern before the pattern is full. +-- @param #AIRBOSS self +-- @param #number nmax Max number. Default 4. +-- @return #AIRBOSS self +function AIRBOSS:SetMaxLandingPattern(nmax) + self.Nmaxpattern=nmax or 4 + return self +end + +--- Handle AI aircraft. +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:SetHandleAION() + self.handleai=true + return self +end + +--- Do not handle AI aircraft. +-- @param #AIRBOSS self +-- @return #ARIBOSS self +function AIRBOSS:SetHandleAIOFF() + self.handleai=false + return self +end + + +--- Define recovery tanker associated with the carrier. +-- @param #AIRBOSS self +-- @param Ops.RecoveryTanker#RECOVERYTANKER recoverytanker Recovery tanker object. +-- @return #ARIBOSS self +function AIRBOSS:SetRecoveryTanker(recoverytanker) + self.tanker=recoverytanker + return self +end + +--- Define warehouse associated with the carrier. +-- @param #AIRBOSS self +-- @param Functional.Warehouse#WAREHOUSE warehouse Warehouse object of the carrier. +-- @return #ARIBOSS self +function AIRBOSS:SetWarehouse(warehouse) + self.warehouse=warehouse + return self +end + +--- Set default player skill. New players will be initialized with this skill. +-- +-- * "Flight Student" = @{#AIRBOSS.Difficulty.Easy} +-- * "Naval Aviator" = @{#AIRBOSS.Difficulty.Normal} +-- * "TOPGUN Graduate" = @{#AIRBOSS.Difficulty.Hard} +-- @param #AIRBOSS self +-- @param #string skill Player skill. Default "Naval Aviator". +-- @return #ARIBOSS self +function AIRBOSS:SetDefaultPlayerSkill(skill) + + -- Set skill or normal. + self.defaultskill=skill or AIRBOSS.Difficulty.NORMAL + + -- Check that defualt skill is valid. + local gotit=false + for _,_skill in pairs(AIRBOSS.Difficulty) do + if _skill==self.defaultskill then + gotit=true + end + end + + -- If invalid user input, fall back to normal. + if not gotit then + self.defaultskill=AIRBOSS.Difficulty.NORMAL + self:E(self.lid..string.format("ERROR: Invalid default skill = %s. Resetting to Naval Aviator.", tostring(skill))) + end + + return self +end + +--- Activate debug mode. Display debug messages on screen. +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:SetDebugModeON() + self.Debug=true + return self +end + +--- Carrier patrols ad inifintum. If the last waypoint is reached, it will go to waypoint one and repeat its route. +-- @param #AIRBOSS self +-- @param #boolean switch If true or nil, patrol until the end of time. If false, go along the waypoints once and stop. +-- @return #AIRBOSS self +function AIRBOSS:SetPatrolAdInfinitum(switch) + if switch==false then + self.adinfinitum=false + else + self.adinfinitum=true + end + return self +end + +--- Set the magnetic declination (or variation). By default this is set to the standard declination of the map. +-- @param #AIRBOSS self +-- @param #number declination Declination in degrees or nil for default declination of the map. +-- @return #AIRBOSS self +function AIRBOSS:SetMagneticDeclination(declination) + + self.magvar=declination or UTILS.GetMagneticDeclination() + + return self +end + +--- Deactivate debug mode. This is also the default setting. +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:SetDebugModeOFF() + self.Debug=false + return self +end + +--- Check if carrier is recovering aircraft. +-- @param #AIRBOSS self +-- @return #boolean If true, time slot for recovery is open. +function AIRBOSS:IsRecovering() + return self:is("Recovering") +end + +--- Check if carrier is idle, i.e. no operations are carried out. +-- @param #AIRBOSS self +-- @return #boolean If true, carrier is in idle state. +function AIRBOSS:IsIdle() + return self:is("Idle") +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM event functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. Starts the warehouse. Addes event handlers and schedules status updates of reqests and queue. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AIRBOSS:onafterStart(From, Event, To) + + -- Events are handled my MOOSE. + self:I(self.lid..string.format("Starting AIRBOSS v%s for carrier unit %s of type %s.", AIRBOSS.version, self.carrier:GetName(), self.carriertype)) + + -- Current map. + local theatre=env.mission.theatre + self:T2(self.lid..string.format("Theatre = %s", tostring(theatre))) + + -- Activate TACAN. + if self.TACANon then + self.beacon:ActivateTACAN(self.TACANchannel, self.TACANmode, self.TACANmorse, true) + end + + -- Activate ICLS. + if self.ICLSon then + self.beacon:ActivateICLS(self.ICLSchannel, self.ICLSmorse) + end + + -- Handle events. + self:HandleEvent(EVENTS.Birth) + self:HandleEvent(EVENTS.Land) + self:HandleEvent(EVENTS.Crash) + self:HandleEvent(EVENTS.Ejection) + + -- Time stamp for checking queues. + self.Tqueue=timer.getTime() + + -- Schedule radio queue checks. + self.RQLid=self.radiotimer:Schedule(self, self._CheckRadioQueue, {self.RQLSO, "LSO"}, 1, 0.01) + self.RQMid=self.radiotimer:Schedule(self, self._CheckRadioQueue, {self.RQMarshal, "MARSHAL"}, 1, 0.01) + + -- Initial carrier position and orientation. + self.Cposition=self:GetCoordinate() + self.Corientation=self.carrier:GetOrientationX() + self.Corientlast=self.Corientation + self.Tpupdate=timer.getTime() + + -- Init patrol route of carrier. + self:_PatrolRoute() + + -- Check if no recovery window is set. + if #self.recoverytimes==0 then + + -- Open window in 15 minutes for 3 hours. + local Topen=timer.getAbsTime()+15*60 + local Tclose=Topen+3*60*60 + + -- Add window. + self:AddRecoveryWindow(UTILS.SecondsToClock(Topen), UTILS.SecondsToClock(Tclose)) + end + + -- Start status check in 1 second. + self:__Status(1) +end + +--- On after Status event. Checks for new flights, updates queue and checks player status. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AIRBOSS:onafterStatus(From, Event, To) + + -- Get current time. + local time=timer.getTime() + + -- Update marshal and pattern queue every 30 seconds. + if time-self.Tqueue>30 then + + -- Get time. + local clock=UTILS.SecondsToClock(timer.getAbsTime()) + + -- Debug info. + local text=string.format("Time %s - Status %s (case=%d) - Speed=%.1f kts - Heading=%d - WP=%d - ETA=%s", + clock, self:GetState(), self.case, self.carrier:GetVelocityKNOTS(), self:GetHeading(), self.currentwp, UTILS.SecondsToClock(self:_GetETAatNextWP())) + self:T(self.lid..text) + + -- Check recovery times and start/stop recovery mode if necessary. + self:_CheckRecoveryTimes() + + -- Scan carrier zone for new aircraft. + self:_ScanCarrierZone() + + -- Check marshal and pattern queues. + self:_CheckQueue() + + -- Check if marshal pattern of AI needs an update. + self:_CheckPatternUpdate() + + -- Time stamp. + self.Tqueue=time + end + + -- Check player status. + self:_CheckPlayerStatus() + + -- Check AI landing pattern status + self:_CheckAIStatus() + + -- Call status every 0.5 seconds. + self:__Status(-0.5) +end + +--- Get aircraft nickname. +-- @param #AIRBOSS self +-- @param #string actype Aircraft type name. +-- @return #string Aircraft nickname. E.g. "Hornet" for the F/A-18C or "Tomcat" For the F-14A. +function AIRBOSS:_GetACNickname(actype) + + local nickname="unknown" + if actype==AIRBOSS.AircraftCarrier.A4EC then + nickname="Skyhawk" + elseif actype==AIRBOSS.AircraftCarrier.AV8B then + nickname="Harrier" + elseif actype==AIRBOSS.AircraftCarrier.E2D then + nickname="Hawkeye" + elseif actype==AIRBOSS.AircraftCarrier.F14A then + nickname="Tomcat" + elseif actype==AIRBOSS.AircraftCarrier.FA18C or actype==AIRBOSS.AircraftCarrier.HORNET then + nickname="Hornet" + elseif actype==AIRBOSS.AircraftCarrier.S3B or actype==AIRBOSS.AircraftCarrier.S3BTANKER then + nickname="Viking" + end + + return nickname +end + +--- Check AI status. Pattern queue AI in the groove? Marshal queue AI arrived in holding zone? +-- @param #AIRBOSS self +function AIRBOSS:_CheckAIStatus() + + -- Loop over all flights in landing pattern. + for _,_flight in pairs(self.Qpattern) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Only AI! + if flight.ai then + + -- Loop over all units in AI flight. + for _,_element in pairs(flight.elements) do + local element=_element --#AIRBOSS.FlightElement + + -- Unit + local unit=element.unit + + -- Get lineup and distance to carrier. + local lineup=self:_Lineup(unit, true) + + -- Distance in NM. + local distance=UTILS.MetersToNM(unit:GetCoordinate():Get2DDistance(self:GetCoordinate())) + + -- Altitude in ft. + local alt=UTILS.MetersToFeet(unit:GetAltitude()) + + -- Check if parameters are right and flight is in the groove. + if lineup<2 and distance<=0.75 and alt<500 and not element.ballcall then + + -- Paddles: Call the ball! + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.CALLTHEBALL, false, 0) + + -- Pilot: "405, Hornet Ball, 3.2" + -- TODO: Voice over. + local text=string.format("%s Ball, %.1f.", self:_GetACNickname(unit:GetTypeName()), self:_GetFuelState(unit)/1000) + self:MessageToPattern(text, element.onboard, "", 3, false, 0, true) + + -- Debug message. + MESSAGE:New(string.format("%s, %s", element.onboard, text), 15, "DEBUG"):ToAllIf(self.Debug) + + -- Paddles: Roger ball after 3 seconds. + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.ROGERBALL, false, 3) + + -- Flight element called the ball. + element.ballcall=true + + -- This is for the whole flight. Maybe we need it. + flight.ballcall=true + end + + end + end + end + +end + +--- Check if player in the landing pattern is too close to another aircarft in the pattern. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData player Player data. +function AIRBOSS:_CheckPlayerPatternDistance(player) + + -- Nothing to do since we check only in the pattern. + if #self.Qpattern==0 then + return + end + + --- Function that checks if unit1 is too close to unit2. + local function _checkclose(_unit1, _unit2) + + local unit1=_unit1 --Wrapper.Unit#UNIT + local unit2=_unit2 --Wrapper.Unit#UNIT + + if (not unit1) or (not unit2) then + return false + end + + -- Check that this is not the same unit. + if unit1:GetName()==unit2:GetName() then + return false + end + + -- Return false when unit2 is not in air? Could be on the carrier. + if not unit2:InAir() then + return false + end + + -- Positions of units. + local c1=unit1:GetCoordinate() + local c2=unit2:GetCoordinate() + + -- Vector from unit1 to unit2 + local vec12={x=c2.x-c1.x, y=0, z=c2.z-c1.z} --DCS#Vec3 + + -- Distance between units. + local dist=UTILS.VecNorm(vec12) + + -- Orientation of unit 1 in space. + local vec1=unit1:GetOrientationX() + vec1.y=0 + + -- Get angle between the two orientation vectors. Does the player aircraft nose point into the direction of the other aircraft? (Could be behind him!) + local rhdg=math.deg(math.acos(UTILS.VecDot(vec12,vec1)/UTILS.VecNorm(vec12)/UTILS.VecNorm(vec1))) + + -- Check altitude difference? + local dalt=math.abs(c2.y-c1.y) + + -- 650 feet ~= 200 meters distance between flights + local dcrit=UTILS.FeetToMeters(650) + + -- Direction in 30 degrees cone and distance < 200 meters and altitude difference <50 + -- TODO: Test parameter values. + if math.abs(rhdg)<10 and dist=recovery.START then + -- Start time has passed. + + if time1 then + text=text..string.format(" Holding offset angle %d degrees.", Offset) + end + MESSAGE:New(text, 20, self.alias):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- Set new recovery case. + self.case=Case + + -- Set holding offset. + self.holdingoffset=Offset +end + +--- On after "RecoveryStart" event. Recovery of aircraft is started and carrier switches to state "Recovering". +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number Case The recovery case (1, 2 or 3) to start. +-- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. +function AIRBOSS:onafterRecoveryStart(From, Event, To, Case, Offset) + + -- Input or default value. + Case=Case or self.defaultcase + + -- Input or default value. + Offset=Offset or self.defaultoffset + + -- Debug output. + local text=string.format("Starting aircraft recovery case %d.", Case) + if Case>1 then + text=text..string.format(" Holding offset angle %d degrees.", Offset) + end + MESSAGE:New(text, 20, self.alias):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- Message to all players in marshal stack. + -- TODO: maybe to all flights in CCA? + self:MessageToMarshal(text, "MARSHAL", "99") + + -- Switch to case. + self:RecoveryCase(Case, Offset) +end + +--- On after "RecoveryStop" event. Recovery of aircraft is stopped and carrier switches to state "Idle". +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AIRBOSS:onafterRecoveryStop(From, Event, To) + -- Debug output. + self:T(self.lid..string.format("Stopping aircraft recovery. Carrier goes to state idle.")) +end + +--- On after "Idle" event. Carrier goes to state "Idle". +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AIRBOSS:onafterIdle(From, Event, To) + -- Debug output. + self:T(self.lid..string.format("Carrier goes to idle.")) +end + +--- On after Stop event. Unhandle events. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AIRBOSS:onafterStop(From, Event, To) + self:UnHandleEvent(EVENTS.Birth) + self:UnHandleEvent(EVENTS.Land) + self:UnHandleEvent(EVENTS.Crash) + self:UnHandleEvent(EVENTS.Ejection) +end + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Parameter initialization +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Function called when a group is passing a waypoint. +--@param Wrapper.Group#GROUP group Group that passed the waypoint +--@param #AIRBOSS airboss Airboss object. +--@param #number i Waypoint number that has been reached. +--@param #number final Final waypoint number. +function AIRBOSS._PassingWaypoint(group, airboss, i, final) + + -- Debug message. + local text=string.format("Group %s passing waypoint %d of %d.", group:GetName(), i, final) + + -- Debug smoke and marker. + if airboss.Debug and false then + local pos=group:GetCoordinate() + pos:SmokeRed() + local MarkerID=pos:MarkToAll(string.format("Group %s reached waypoint %d", group:GetName(), i)) + end + + -- Debug message. + MESSAGE:New(text,10):ToAllIf(airboss.Debug) + airboss:T(airboss.lid..text) + + -- Set current waypoint. + airboss.currentwp=i + + -- If final waypoint reached, do route all over again. + if i==final and final>1 and airboss.adinfinitum then + airboss:_PatrolRoute() + end +end + +--- Function called when a group has reached the holding zone. +--@param Wrapper.Group#GROUP group Group that reached the holding zone. +--@param #AIRBOSS airboss Airboss object. +--@param #AIRBOSS.FlightGroup flight Flight group that has reached the holding zone. +function AIRBOSS._ReachedHoldingZone(group, airboss, flight) + + -- Debug message. + local text=string.format("Flight %s reached holding zone.", group:GetName()) + MESSAGE:New(text,10):ToAllIf(airboss.Debug) + airboss:T(airboss.lid..text) + + -- Debug mark. + if airboss.Debug then + group:GetCoordinate():MarkToAll(text) + end + + -- Set holding flag true and set timestamp for marshal time check. + if flight then + flight.holding=true + flight.time=timer.getAbsTime() + end +end + + +--- Patrol carrier +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:_PatrolRoute() + + -- Get carrier group. + local CarrierGroup=self.carrier:GetGroup() + + -- Waypoints of group. + local Waypoints = CarrierGroup:GetTemplateRoutePoints() + + -- NOTE: This is only necessary, if the first waypoint would already be far way, i.e. when the script is started with a large delay. + -- Calculate the new Route. + --local wp0=CarrierGroup:GetCoordinate():WaypointGround(5.5*3.6) + -- Insert current coordinate as first waypoint + --table.insert(Waypoints, 1, wp0) + + for n=1,#Waypoints do + + -- Passing waypoint taskfunction + local TaskPassingWP=CarrierGroup:TaskFunction("AIRBOSS._PassingWaypoint", self, n, #Waypoints) + + -- Call task function when carrier arrives at waypoint. + CarrierGroup:SetTaskWaypoint(Waypoints[n], TaskPassingWP) + end + + -- Set waypoint table. + local i=1 + for _,point in ipairs(Waypoints) do + + -- Coordinate of the waypoint + local coord=COORDINATE:New(point.x, point.alt, point.y) + + -- Set velocity of the coordinate. + coord:SetVelocity(point.speed) + + -- Add to table. + table.insert(self.waypoints, coord) + + -- Debug info. + if self.Debug then + coord:MarkToAll(string.format("Carrier Waypoint %d, Speed=%.1f knots", i, UTILS.MpsToKnots(point.speed))) + end + + -- Increase counter. + i=i+1 + end + + -- Current waypoint is 1. + self.currentwp=1 + + -- Route carrier group. + CarrierGroup:Route(Waypoints) +end + +--- Estimated the carrier position at some point in the future given the current waypoints and speeds. +-- @param #AIRBOSS self +-- @return DCS#time ETA abs. time in seconds. +function AIRBOSS:_GetETAatNextWP() + + -- Current waypoint + local cwp=self.currentwp + + -- Current abs. time. + local tnow=timer.getAbsTime() + + -- Current position. + local p=self:GetCoordinate() + + -- Current velocity [m/s]. + local v=self.carrier:GetVelocityMPS() + + -- Distance to next waypoint. + local s=0 + if #self.waypoints>cwp then + s=p:Get2DDistance(self.waypoints[cwp+1]) + end + + -- v=s/t <==> t=s/v + local t=s/v + + -- ETA + local eta=t+tnow + + return eta +end + + +--- Estimated the carrier position at some point in the future given the current waypoints and speeds. +-- @param #AIRBOSS self +-- @param #number time Absolute mission time at which the carrier position is requested. +-- @return Core.Point#COORDINATE Coordinate of the carrier at the given time. +function AIRBOSS:_GetCarrierFuture(time) + + local nwp=self.currentwp + + local waypoints={} + local lastwp=nil --Core.Point#COORDINATE + for i=1,#self.waypoints do + + if i>nwp then + table.insert(waypoints, self.waypoints[i]) + elseif i==nwp then + lastwp=self.waypoints[i] + end + + end + + -- Current abs. time. + local tnow=timer.getAbsTime() + + local p=self:GetCoordinate() + local v=self.carrier:GetVelocityMPS() + + local s=p:Get2DDistance(self.waypoints[nwp+1]) + + -- v=s/t <==> t=s/v + local t=s/v + + local eta=UTILS.SecondsToClock(t+tnow) + + + for _,_wp in ipairs(waypoints) do + local wp=_wp --Core.Point#COORDINATE + + end + +end + +--- Init parameters for USS Stennis carrier. +-- @param #AIRBOSS self +function AIRBOSS:_InitStennis() + + -- Carrier Parameters. + self.carrierparam.sterndist =-153 + self.carrierparam.deckheight = 19 + + -- Total size of the carrier (approx as rectangle). + self.carrierparam.totlength=310 -- Wiki says 332.8 meters overall length. + self.carrierparam.totwidthport=40 -- Wiki says 76.8 meters overall beam. + self.carrierparam.totwidthstarboard=30 + + -- Landing runway. + self.carrierparam.rwyangle = -9 + self.carrierparam.rwylength = 225 + self.carrierparam.rwywidth = 20 + + -- Wires. + self.carrierparam.wire1 = 46 -- Distance from stern to first wire. + self.carrierparam.wire2 = 46+12 + self.carrierparam.wire3 = 46+24 + self.carrierparam.wire4 = 46+35 -- Last wire is strangely one meter closer. + + + -- Platform at 5k. Reduce descent rate to 2000 ft/min to 1200 dirty up level flight. + self.Platform.name="Platform 5k" + self.Platform.Xmin=-UTILS.NMToMeters(22) -- Not more than 22 NM behind the boat. Last check was at 21 NM. + self.Platform.Xmax =nil + self.Platform.Zmin=-UTILS.NMToMeters(30) -- Not more than 30 NM port of boat. + self.Platform.Zmax= UTILS.NMToMeters(30) -- Not more than 30 NM starboard of boat. + self.Platform.LimitXmin=nil -- Limits via zone + self.Platform.LimitXmax=nil + self.Platform.LimitZmin=nil + self.Platform.LimitZmax=nil + + -- Level out at 1200 ft and dirty up. + self.DirtyUp.name="Dirty Up" + self.DirtyUp.Xmin=-UTILS.NMToMeters(21) -- Not more than 21 NM behind the boat. + self.DirtyUp.Xmax= nil + self.DirtyUp.Zmin=-UTILS.NMToMeters(30) -- Not more than 30 NM port of boat. + self.DirtyUp.Zmax= UTILS.NMToMeters(30) -- Not more than 30 NM starboard of boat. + self.DirtyUp.LimitXmin=nil -- Limits via zone + self.DirtyUp.LimitXmax=nil + self.DirtyUp.LimitZmin=nil + self.DirtyUp.LimitZmax=nil + + -- Intercept glide slope and follow bullseye. + self.Bullseye.name="Bullseye" + self.Bullseye.Xmin=-UTILS.NMToMeters(11) -- Not more than 11 NM behind the boat. Last check was at 10 NM. + self.Bullseye.Xmax= nil + self.Bullseye.Zmin=-UTILS.NMToMeters(30) -- Not more than 30 NM port. + self.Bullseye.Zmax= UTILS.NMToMeters(30) -- Not more than 30 NM starboard. + self.Bullseye.LimitXmin=nil -- Limits via zone. + self.Bullseye.LimitXmax=nil + self.Bullseye.LimitZmin=nil + self.Bullseye.LimitZmax=nil + + -- Break entry. + self.BreakEntry.name="Break Entry" + self.BreakEntry.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. Check for initial is at 3 NM with a radius of 500 m and 100 m starboard. + self.BreakEntry.Xmax= nil + self.BreakEntry.Zmin=-400 -- Not more than 400 m port of boat. Otherwise miss the zone. + self.BreakEntry.Zmax=UTILS.NMToMeters(1.5) -- Not more than 1.5 NM starboard. + self.BreakEntry.LimitXmin=0 -- Check and next step when at carrier and starboard of carrier. + self.BreakEntry.LimitXmax=nil + self.BreakEntry.LimitZmin=nil + self.BreakEntry.LimitZmax=nil + + -- Early break. + self.BreakEarly.name="Early Break" + self.BreakEarly.Xmin=-UTILS.NMToMeters(1) -- Not more than 1 NM behind the boat. Last check was at 0. + self.BreakEarly.Xmax= UTILS.NMToMeters(5) -- Not more than 5 NM in front of the boat. Enough for late breaks? + self.BreakEarly.Zmin=-UTILS.NMToMeters(2) -- Not more than 2 NM port. + self.BreakEarly.Zmax= UTILS.NMToMeters(1) -- Not more than 1 NM starboard. + self.BreakEarly.LimitXmin= 0 -- Check and next step 0.2 NM port and in front of boat. + self.BreakEarly.LimitXmax= nil + self.BreakEarly.LimitZmin=-UTILS.NMToMeters(0.2) -- -370 m port + self.BreakEarly.LimitZmax= nil + + -- Late break. + self.BreakLate.name="Late Break" + self.BreakLate.Xmin=-UTILS.NMToMeters(1) -- Not more than 1 NM behind the boat. Last check was at 0. + self.BreakLate.Xmax= UTILS.NMToMeters(5) -- Not more than 5 NM in front of the boat. Enough for late breaks? + self.BreakLate.Zmin=-UTILS.NMToMeters(2) -- Not more than 2 NM port. + self.BreakLate.Zmax= UTILS.NMToMeters(1) -- Not more than 1 NM starboard. + self.BreakLate.LimitXmin= 0 -- Check and next step 0.8 NM port and in front of boat. + self.BreakLate.LimitXmax= nil + self.BreakLate.LimitZmin=-UTILS.NMToMeters(0.8) -- -1470 m port + self.BreakLate.LimitZmax= nil + + -- Abeam position. + self.Abeam.name="Abeam Position" + self.Abeam.Xmin= nil + self.Abeam.Xmax= nil + self.Abeam.Zmin=-UTILS.NMToMeters(2) -- Not more than 2 NM port. + self.Abeam.Zmax= 100 -- Must be port! + self.Abeam.LimitXmin=-200 -- Check and next step 200 meters behind the ship. + self.Abeam.LimitXmax= nil + self.Abeam.LimitZmin= nil + self.Abeam.LimitZmax= nil + + -- At the Ninety. + self.Ninety.name="Ninety" + self.Ninety.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. LIG check anyway. + self.Ninety.Xmax= 0 -- Must be behind the boat. + self.Ninety.Zmin=-UTILS.NMToMeters(2) -- Not more than 2 NM port of boat. + self.Ninety.Zmax= nil + self.Ninety.LimitXmin=nil + self.Ninety.LimitXmax=nil + self.Ninety.LimitZmin=nil + self.Ninety.LimitZmax=-UTILS.NMToMeters(0.6) -- Check and next step when 0.6 NM port. + + -- At the Wake. + self.Wake.name="Wake" + self.Wake.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. + self.Wake.Xmax= 0 -- Must be behind the boat. + self.Wake.Zmin=-2000 -- Not more than 2 km port of boat. + self.Wake.Zmax= nil + self.Wake.LimitXmin=nil + self.Wake.LimitXmax=nil + self.Wake.LimitZmin=0 -- Check and next step when directly behind the boat. + self.Wake.LimitZmax=nil + + -- Turn to final. + self.Final.name="Final" + self.Final.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. + self.Final.Xmax= 0 -- Must be behind the boat. + self.Final.Zmin=-1000 -- Not more than 1 km port. + self.Final.Zmax= nil + self.Final.LimitXmin=nil -- No limits. Check is carried out differently. + self.Final.LimitXmax=nil + self.Final.LimitZmin=nil + self.Final.LimitZmax=nil + + -- In the Groove. + self.Groove.name="Groove" + self.Groove.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. + self.Groove.Xmax= nil + self.Groove.Zmin=-UTILS.NMToMeters(2) -- Not more than 2 NM port + self.Groove.Zmax= UTILS.NMToMeters(2) -- Not more than 2 NM starboard. + self.Groove.LimitXmin=nil -- No limits. Check is carried out differently. + self.Groove.LimitXmax=nil + self.Groove.LimitZmin=nil + self.Groove.LimitZmax=nil + +end + +--- Get optimal aircraft AoA parameters.. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @return #AIRBOSS.AircraftAoA AoA parameters for the given aircraft type. +function AIRBOSS:_GetAircraftAoA(playerData) + + -- Get AC type. + local hornet=playerData.actype==AIRBOSS.AircraftCarrier.HORNET + local skyhawk=playerData.actype==AIRBOSS.AircraftCarrier.A4EC + local harrier=playerData.actype==AIRBOSS.AircraftCarrier.AV8B + + -- Table with AoA values. + local aoa={} -- #AIRBOSS.AircraftAoA + + if hornet then + -- F/A-18C Hornet parameters + aoa.SLOW=9.8 + aoa.Slow=9.3 + aoa.OnSpeedMax=8.8 + aoa.OnSpeed=8.1 + aoa.OnSpeedMin=7.4 + aoa.Fast=6.9 + aoa.FAST=6.3 + elseif skyhawk then + -- A-4E-C Skyhawk parameters from https://forums.eagle.ru/showpost.php?p=3703467&postcount=390 + aoa.SLOW=19.0 + aoa.Slow=18.5 + aoa.OnSpeedMax=18.0 + aoa.OnSpeed=17.5 + aoa.OnSpeedMin=17.0 + aoa.Fast=16.5 + aoa.FAST=16.0 + elseif harrier then + -- AV-8B Harrier parameters. This might need further tuning. + aoa.SLOW=14.0 + aoa.Slow=13.0 + aoa.OnSpeedMax=12.0 + aoa.OnSpeed=11.0 + aoa.OnSpeedMin=10.0 + aoa.Fast=9.0 + aoa.FAST=8.0 + end + + return aoa +end + +--- Get optimal aircraft flight parameters at checkpoint. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #string step Pattern step. +-- @return #number Altitude in meters or nil. +-- @return #number Angle of Attack or nil. +-- @return #number Distance to carrier in meters or nil. +-- @return #number Speed in m/s or nil. +function AIRBOSS:_GetAircraftParameters(playerData, step) + + -- Get parameters depended on step. + step=step or playerData.step + + -- Get AC type. + local hornet=playerData.actype==AIRBOSS.AircraftCarrier.HORNET + local skyhawk=playerData.actype==AIRBOSS.AircraftCarrier.A4EC + + -- Return values. + local alt + local aoa + local dist + local speed + + -- Aircraft specific AoA. + local aoaac=self:_GetAircraftAoA(playerData) + + if step==AIRBOSS.PatternStep.PLATFORM then + + alt=UTILS.FeetToMeters(5000) + + dist=UTILS.NMToMeters(20) + + speed=UTILS.KnotsToMps(250) + + elseif step==AIRBOSS.PatternStep.ARCIN then + + speed=UTILS.KnotsToMps(250) + + elseif step==AIRBOSS.PatternStep.ARCOUT then + + speed=UTILS.KnotsToMps(250) + + elseif step==AIRBOSS.PatternStep.DIRTYUP then + + alt=UTILS.FeetToMeters(1200) + + dist=UTILS.NMToMeters(12) + + speed=UTILS.KnotsToMps(250) + + elseif step==AIRBOSS.PatternStep.BULLSEYE then + + alt=UTILS.FeetToMeters(1200) + + dist=-UTILS.NMToMeters(3) + + aoa=aoaac.OnSpeed + + elseif step==AIRBOSS.PatternStep.INITIAL then + + if hornet then + alt=UTILS.FeetToMeters(800) + speed=UTILS.KnotsToMps(350) + elseif skyhawk then + alt=UTILS.FeetToMeters(600) + speed=UTILS.KnotsToMps(250) + end + + elseif step==AIRBOSS.PatternStep.BREAKENTRY then + + if hornet then + alt=UTILS.FeetToMeters(800) + speed=UTILS.KnotsToMps(350) + elseif skyhawk then + alt=UTILS.FeetToMeters(600) + speed=UTILS.KnotsToMps(250) + end + + elseif step==AIRBOSS.PatternStep.EARLYBREAK then + + if hornet then + alt=UTILS.FeetToMeters(800) + elseif skyhawk then + alt=UTILS.FeetToMeters(600) + end + + elseif step==AIRBOSS.PatternStep.LATEBREAK then + + if hornet then + alt=UTILS.FeetToMeters(800) + elseif skyhawk then + alt=UTILS.FeetToMeters(600) + end + + elseif step==AIRBOSS.PatternStep.ABEAM then + + if hornet then + alt=UTILS.FeetToMeters(600) + elseif skyhawk then + alt=UTILS.FeetToMeters(500) + end + + aoa=aoaac.OnSpeed + + dist=UTILS.NMToMeters(1.2) + + elseif step==AIRBOSS.PatternStep.NINETY then + + if hornet then + alt=UTILS.FeetToMeters(500) + elseif skyhawk then + alt=UTILS.FeetToMeters(500) + end + + aoa=aoaac.OnSpeed + + elseif step==AIRBOSS.PatternStep.WAKE then + + if hornet then + alt=UTILS.FeetToMeters(370) + elseif skyhawk then + alt=UTILS.FeetToMeters(370) --? + end + + aoa=aoaac.OnSpeed + + elseif step==AIRBOSS.PatternStep.FINAL then + + if hornet then + alt=UTILS.FeetToMeters(300) + elseif skyhawk then + alt=UTILS.FeetToMeters(300) --? + end + + aoa=aoaac.OnSpeed + + end + + return alt, aoa, dist, speed +end + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- QUEUE Functions +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get next marshal flight which is ready to enter the landing pattern. +-- @param #AIRBOSS self +-- @return #AIRBOSS.FlightGroup Marshal flight next in line and ready to enter the pattern. Or nil if no flight is ready. +function AIRBOSS:_GetNextMarshalFight() + + -- Min 5 min in marshal before send to landing pattern. + local TmarshalMin=10*60 + + for _,_flight in pairs(self.Qmarshal) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Current stack. + local stack=flight.flag:Get() + + -- Marshal time. + local Tmarshal=timer.getAbsTime()-flight.time + + -- Check if conditions are right. + if stack==1 and flight.holding and Tmarshal>=TmarshalMin then + return flight + end + end + + return nil +end + + +--- Check marshal and pattern queues. +-- @param #AIRBOSS self +function AIRBOSS:_CheckQueue() + + -- Print queues. + self:_PrintQueue(self.flights, "All Flights") + self:_PrintQueue(self.Qmarshal, "Marshal") + self:_PrintQueue(self.Qpattern, "Pattern") + + -- Get number of aircraft units(!) currently in pattern. + local _,npattern=self:_GetQueueInfo(self.Qpattern) + + -- Get next marshal flight. + local marshalflight=self:_GetNextMarshalFight() + + -- Check if there are flights in marshal strack and if the pattern is free. + if marshalflight and npattern0 then + + -- Last flight group send to pattern. + local patternflight=self.Qpattern[#self.Qpattern] --#AIRBOSS.FlightGroup + + -- Recovery case of pattern flight. + pcase=patternflight.case + + -- Number of aircraft in this group. + local npunits=patternflight.nunits + + -- Get time in pattern. + Tpattern=timer.getAbsTime()-patternflight.time + self:T(self.lid..string.format("Pattern time of last group %s = %d seconds. # of units=%d.", patternflight.groupname, Tpattern, npunits)) + end + + -- Min time in pattern before next aircraft is allowed. + local TpatternMin + if pcase==1 then + TpatternMin=3*60*npunits --45*npunits -- 45 seconds interval per plane! + else + TpatternMin=3*60*npunits --120*npunits -- 120 seconds interval per plane! + end + + -- Check recovery window open and enough space to last pattern flight. + if self:IsRecovering() and Tpattern>TpatternMin then + self:_CheckCollapseMarshalStack(marshalflight) + end + + end +end + +--- Scan carrier zone for (new) units. +-- @param #AIRBOSS self +function AIRBOSS:_ScanCarrierZone() + + -- Carrier position. + local coord=self:GetCoordinate() + + -- Scan radius = radius of the CCA. + --local Rout=UTILS.NMToMeters(50) + local RCCZ=self.zoneCCA:GetRadius() + + -- Debug info. + self:T(self.lid..string.format("Scanning Carrier Controlled Area. Radius=%.1f NM.", UTILS.MetersToNM(RCCZ))) + + -- Scan units in carrier zone. + local _,_,_,unitscan=coord:ScanObjects(RCCZ, true, false, false) + + + -- Make a table with all groups currently in the CCA zone. + local insideCCA={} + for _,_unit in pairs(unitscan) do + local unit=_unit --Wrapper.Unit#UNIT + + -- Necessary conditions to be met: + local airborne=unit:IsAir() and unit:InAir() + local inzone=unit:IsInZone(self.zoneCCA) + local friendly=self:GetCoalition()==unit:GetCoalition() + local carrierac=self:_IsCarrierAircraft(unit) + + -- Check if this an aircraft and that it is airborne and closing in. + if airborne and inzone and friendly and carrierac then + + local group=unit:GetGroup() + local groupname=group:GetName() + + if insideCCA[groupname]==nil then + insideCCA[groupname]=group + end + + end + end + + + -- Find new flights that are inside CCA. + for groupname,_group in pairs(insideCCA) do + local group=_group --Wrapper.Group#GROUP + + -- Get flight group if possible. + local knownflight=self:_GetFlightFromGroupInQueue(group, self.flights) + + -- Get aircraft type name. + local actype=group:GetTypeName() + + -- Create a new flight group + if knownflight then + + -- Debug output. + self:T2(self.lid..string.format("Known flight group %s of type %s in CCA.", groupname, actype)) + + -- Check if flight is AI and if we want to handle it at all. + if knownflight.ai and self.handleai then + + -- Get distance to carrier. + local dist=knownflight.group:GetCoordinate():Get2DDistance(self:GetCoordinate()) + + -- Close in distance. Is >0 if AC comes closer wrt to first detected distance d0. + local closein=knownflight.dist0-dist + + -- Debug info. + self:T3(self.lid..string.format("Known AI flight group %s closed in by %.1f NM", knownflight.groupname, UTILS.MetersToNM(closein))) + + -- Send AI flight to marshal stack if group closes in more than 5 and has initial flag value. + if closein>UTILS.NMToMeters(5) and knownflight.flag:Get()==-100 then + + -- Check that we do not add a recovery tanker for marshaling. + if self.tanker and self.tanker.tanker:GetName()==groupname then + + -- Don't touch the recovery tanker! + + else + + -- Get the next free stack for current recovery case. + local stack=self:_GetFreeStack(self.case) + + -- Send AI to marshal stack. + self:_MarshalAI(knownflight, stack) + + -- Add group to marshal stack queue. + self:_AddMarshalGroup(knownflight, stack) + + end -- Tanker + end -- Closed in + end -- AI + else + -- Unknown new flight. Create a new flight group. + self:_CreateFlightGroup(group) + end + + end + + + -- Find flights that are not in CCA. + local remove={} + for _,_flight in pairs(self.flights) do + local flight=_flight --#AIRBOSS.FlightGroup + if insideCCA[flight.groupname]==nil then + -- Do not remove flights in marshal pattern. At least for case 2 & 3. If zone is set small, they might be outside in the holding pattern. + if not (flight.case>1 and self:_InQueue(self.Qmarshal, flight.group)) then + table.insert(remove, flight.group) + end + end + end + + -- Remove flight groups outside CCA. + for _,group in pairs(remove) do + self:_RemoveFlightGroup(group) + end + +end + + +--- Orbit at a specified position at a specified alititude with a specified speed. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_MarshalPlayer(playerData) + + -- Check if flight is known to the airboss already. + if playerData then + + -- Get free stack. + local mystack=self:_GetFreeStack(self.case) + + -- Add group to marshal stack. + self:_AddMarshalGroup(playerData, mystack) + + -- Set step to holding. + playerData.step=AIRBOSS.PatternStep.HOLDING + playerData.warning=nil + + -- Holding switch to nil until player arrives in the holding zone. + playerData.holding=nil + + -- Set same stack for all flights in section. + for _,_flight in pairs(playerData.section) do + local flight=_flight --#AIRBOSS.PlayerData + flight.step=AIRBOSS.PatternStep.HOLDING + flight.holding=nil + flight.flag:Set(mystack) + end + + else + + -- Flight is not registered yet. + local text="you are not yet registered inside the CCA. Marshal request denied!" + self:MessageToPlayer(playerData, text, "MARSHAL") + + end + +end + +--- Command AI flight to orbit at a specified position at a specified alititude with a specified speed. +-- If the flight is not already holding in the Marshal stack, it is guided there first. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight group. +-- @param #number nstack Stack number of group. This should be #self.Qmarshal+1 for new flight groups. +function AIRBOSS:_MarshalAI(flight, nstack) + + -- Flight group name. + local group=flight.group + local groupname=flight.groupname + + -- Get old/current stack. + local ostack=flight.flag:Get() + + -- Set new stack. + flight.flag:Set(nstack) + + -- Current carrier position. + local Carrier=self:GetCoordinate() + + -- Carrier heading. + local hdg=self:GetHeading() + + -- Recovery case. + local case=flight.case + + -- Aircraft speed 274 knots TAS ~= 250 KIAS when orbiting the pattern. (Orbit expects m/s.) + local speedOrbitMps=UTILS.KnotsToMps(274) + + -- Orbit speed in km/h for waypoints. + local speedOrbitKmh=UTILS.KnotsToKmph(274) + + -- Aircraft speed 400 knots when transiting to holding zone. (Waypoint expects km/h.) + local speedTransit=UTILS.KnotsToKmph(400) + + local altitude + local p0 --Core.Point#COORDINATE + local p1 --Core.Point#COORDINATE + local p2 --Core.Point#COORDINATE + + -- Get altitude and positions. + altitude, p1, p2=self:_GetMarshalAltitude(nstack, case) + + -- Waypoints array to be filled depending on case etc. + local wp={} + + -- If flight has not arrived in the holding zone, we guide it there. + if not flight.holding then + + ---------------------- + -- Route to Holding -- + ---------------------- + + -- Debug info. + self:T(self.lid..string.format("Guiding AI flight %s to marshal stack %d-->%d.", groupname, ostack, nstack)) + + -- Current position. Always good for as the first waypoint. + wp[1]=group:GetCoordinate():WaypointAirTurningPoint(nil, speedTransit, {}, "Current Position") + + -- Task function when arriving at the holding zone. This will set flight.holding=true. + local TaskArrivedHolding=flight.group:TaskFunction("AIRBOSS._ReachedHoldingZone", self, flight) + + -- Select case. + if case==1 then + + -- Initial point 7 NM and a bit port of carrier. + -- TODO: Test and tune! + local pE=Carrier:Translate(UTILS.NMToMeters(7), hdg-30):SetAltitude(altitude) + + -- Entry point 5 NM port and slightly astern the boat. + p0=Carrier:Translate(UTILS.NMToMeters(5), hdg-135):SetAltitude(altitude) + + -- Waypoint ahead of carrier's holding zone. + wp[#wp+1]=pE:WaypointAirTurningPoint(nil, speedTransit, {TaskArrivedHolding}, "Entering Case I Marshal Pattern") + + else + + -- Get correct radial depending on recovery case including offset. + local radial=self:GetRadial(case, false, true) + + -- Point in the middle of the race track and a 5 NM more port perpendicular. + p0=p2:Translate(UTILS.NMToMeters(5), radial+90):Translate(UTILS.NMToMeters(5), radial, true) + + -- Entering Case II/III marshal pattern waypoint. + wp[#wp+1]=p0:WaypointAirTurningPoint(nil, speedTransit, {TaskArrivedHolding}, "Entering Case II/III Marshal Pattern") + + end + + else + + ------------------------ + -- In Marshal Pattern -- + ------------------------ + + -- Debug info. + self:T(self.lid..string.format("Updating AI flight %s at marshal stack %d-->%d.", groupname, ostack, nstack)) + + -- Current position. Speed expected in km/h. + wp[1]=group:GetCoordinate():WaypointAirTurningPoint(nil, speedOrbitKmh, {}, "Current Position") + + -- Create new waypoint 0.2 Nm ahead of current positon. + p0=group:GetCoordinate():Translate(UTILS.NMToMeters(0.2), group:GetHeading(), true) + + end + + -- Set orbit task. + local taskorbit=group:TaskOrbit(p1, altitude, speedOrbitMps, p2) + + -- Orbit at waypoint. + wp[#wp+1]=p0:WaypointAirTurningPoint(nil, speedOrbitKmh, {taskorbit}, string.format("Marshal Orbit Stack %d", nstack)) + + -- Debug markers. + if self.Debug then + p0:MarkToAll("WP P0 "..groupname) + p1:MarkToAll("RT P1 "..groupname) + p2:MarkToAll("RT P2 "..groupname) + end + + -- Reinit waypoints. + group:WayPointInitialize(wp) + + -- Route group. + group:Route(wp, 0) + +end + +--- Tell AI to land on the carrier. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight group. +function AIRBOSS:_LandAI(flight) + + -- Debug info. + self:T(self.lid..string.format("Landing AI flight %s.", flight.groupname)) + + -- NOTE: Looks like the AI needs to approach at the "correct" speed. If they are too fast, they fly an unnecessary circle to bleed of speed first. + -- Unfortunately, the correct speed depends on the aircraft type! + + -- Aircraft speed when flying the pattern. + local Speed=UTILS.KnotsToKmph(200) + + if flight.actype==AIRBOSS.AircraftCarrier.HORNET or flight.actype==AIRBOSS.AircraftCarrier.FA18C then + Speed=UTILS.KnotsToKmph(200) + elseif flight.actype==AIRBOSS.AircraftCarrier.E2D then + Speed=UTILS.KnotsToKmph(150) + elseif flight.actype==AIRBOSS.AircraftCarrier.F14A then + Speed=UTILS.KnotsToKmph(175) + elseif flight.actype==AIRBOSS.AircraftCarrier.S3B or flight.actype==AIRBOSS.AircraftCarrier.S3BTANKER then + Speed=UTILS.KnotsToKmph(140) + end + + -- Carrier position. + local Carrier=self:GetCoordinate() + + -- Carrier heading. + local hdg=self:GetHeading() + + -- Waypoints array. + local wp={} + + local CurrentSpeed=flight.group:GetVelocityKMH() + + -- Current positon. + wp[#wp+1]=flight.group:GetCoordinate():WaypointAirTurningPoint(nil, CurrentSpeed, {}, "Current position") + + -- Altitude 800 ft. Looks like this works best. + local alt=UTILS.FeetToMeters(800) + + -- Landing waypoint 5 NM behind carrier at 2000 ft = 610 meters ASL. + wp[#wp+1]=Carrier:Translate(UTILS.NMToMeters(4), hdg-160):SetAltitude(alt):WaypointAirLanding(Speed, self.airbase, nil, "Landing") + --wp[#wp+1]=self:GetCoordinate():Translate(UTILS.NMToMeters(3), hdg-160):SetAltitude(alt):WaypointAirTurningPoint(nil,Speed, {}, "Before Initial") ---WaypointAirLanding(Speed, self.airbase, nil, "Landing") + -- + --wp[#wp+1]=self:GetCoordinate():WaypointAirLanding(Speed, self.airbase, nil, "Landing") + + -- Reinit waypoints. + flight.group:WayPointInitialize(wp) + + -- Route group. + flight.group:Route(wp, 0) +end + +--- Get marshal altitude and two positions of a counter-clockwise race track pattern. +-- @param #AIRBOSS self +-- @param #number stack Assigned stack number. Counting starts at one, i.e. stack=1 is the first stack. +-- @param #number case Recovery case. Default is self.case. +-- @return #number Holding altitude in meters. +-- @return Core.Point#COORDINATE First race track coordinate. +-- @return Core.Point#COORDINATE Second race track coordinate. +function AIRBOSS:_GetMarshalAltitude(stack, case) + + -- Stack <= 0. + if stack<=0 then + return 0,nil,nil + end + + -- Recovery case. + case=case or self.case + + -- Carrier position. + local Carrier=self:GetCoordinate() + + -- Altitude of first stack. Depends on recovery case. + local angels0 + local Dist + local p1=nil --Core.Point#COORDINATE + local p2=nil --Core.Point#COORDINATE + + -- Stack number. + local nstack=stack-1 + + if case==1 then + + -- CASE I: Holding at 2000 ft on a circular pattern port of the carrier. Interval +1000 ft for next stack. + angels0=2 + + -- Get true heading of carrier. + local hdg=self.carrier:GetHeading() + + -- For CCW pattern: First point astern, second ahead of the carrier. + + -- First point over carrier. + p1=Carrier + + -- Second point 1.5 NM ahead. + p2=Carrier:Translate(UTILS.NMToMeters(1.5), hdg) + + else + + -- CASE II/III: Holding at 6000 ft on a racetrack pattern astern the carrier. + angels0=6 + + -- Distance: d=n*angles0+15 NM, so first stack is at 15+6=21 NM + Dist=UTILS.NMToMeters(nstack+angels0+15) + + -- Get correct radial depending on recovery case including offset. + local radial=self:GetRadial(case, false, true) + + -- For CCW pattern: p1 further astern than p2. + + -- Length of the race track pattern. + local l=UTILS.NMToMeters(7) + + -- First point of race track pattern. + p1=Carrier:Translate(Dist+l, radial) + + -- Second point. + p2=Carrier:Translate(Dist, radial) + + end + + -- Pattern altitude. + local altitude=UTILS.FeetToMeters((nstack+angels0)*1000) + + -- Set altitude of coordinate. + p1:SetAltitude(altitude, true) + p2:SetAltitude(altitude, true) + + return altitude, p1, p2 +end + +--- Add a flight group to a specific marshal stack and to the marshal queue. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight group. +-- @param #number stack Marshal stack. This (re-)sets the flag value. +function AIRBOSS:_AddMarshalGroup(flight, stack) + + -- Set flag value. This corresponds to the stack number which starts at 1. + flight.flag:Set(stack) + + -- Set recovery case. + flight.case=self.case + + -- Pressure. + local P=UTILS.hPa2inHg(self:GetCoordinate():GetPressure()) + + -- Stack altitude. + local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack, flight.case)) + local brc=self:GetBRC() + + -- Marshal message. + -- TODO: Get charlie time estimate. + local text=string.format("Case %d, BRC is %03d, hold at %d. Expected Charlie Time XX.\n", flight.case, brc, alt) + text=text..string.format("Altimeter %.2f. Report see me.", P) + + -- Hint about TACAN bearing. + if self.TACANon and (not flight.ai) and flight.difficulty==AIRBOSS.Difficulty.EASY then + -- Get inverse magnetic radial potential offset. + local radial=self:GetRadial(flight.case, true, true, true) + text=text..string.format("\nSelect TACAN %d°, channel %d%s (%s)", radial, self.TACANchannel,self.TACANmode, self.TACANmorse) + end + + -- Message to all players. + self:MessageToAll(text, "MARSHAL", flight.onboard) + + -- Add to marshal queue. + table.insert(self.Qmarshal, flight) +end + +--- Check if marshal stack can be collapsed. +-- If next in line is an AI flight, this is done. If human player is next, we wait for "Commence" via F10 radio menu command. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight to go to pattern. +function AIRBOSS:_CheckCollapseMarshalStack(flight) + + -- Check if flight is AI or human. If AI, we collapse the stack and commence. If human, we suggest to commence. + if flight.ai then + -- Collapse stack and send AI to pattern. + self:_CollapseMarshalStack(flight) + self:_LandAI(flight) + end + + -- Inform all flights. + local text=string.format("You are cleared for Case %d recovery.", flight.case) + self:MessageToAll(text, "MARSHAL", flight.onboard) + + -- Hint for human players. + if not flight.ai then + local playerData=flight --#AIRBOSS.PlayerData + + -- Hint for easy skill. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + self:MessageToPlayer(flight, string.format("Use F10 radio menu \"Request Commence\" command when ready!"), nil, "", 5) + end + end + +end + +--- Collapse marshal stack. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight that left the marshal stack. +-- @param #boolean nopattern If true, flight does not go to pattern. +function AIRBOSS:_CollapseMarshalStack(flight, nopattern) + self:F2({flight=flight, nopattern=nopattern}) + + -- Recovery case of flight. + local case=flight.case + + -- Stack of flight. + local stack=flight.flag:Get() + + -- Memorize time when stack collapsed. Should better depend on case but for now we assume there are no two different stacks Case I or II/III. + self.Tcollapse=timer.getTime() + + -- Decrease flag values of all flight groups in marshal stack. + for _,_flight in pairs(self.Qmarshal) do + local mflight=_flight --#AIRBOSS.PlayerData + + -- Only collaps stack of which the flight left. CASE II/III stack is the same. + if (case==1 and mflight.case==1) or (case>1 and mflight.case>1) then + + -- Get current flag/stack value. + local mstack=mflight.flag:Get() + + -- Only collapse stacks above the new pattern flight. + -- This will go wrong, if patternflight is not in marshal stack because it will have value -100 and all mstacks will be larger! + -- Maybe need to set the initial value to 1000? Or check stack>0 of pattern flight? + if stack>0 and mstack>stack then + + -- New stack is old stack minus one. + -- TODO: If we include the recovery tanker, this needs to be generalized. + local newstack=mstack-1 + + -- Debug info. + self:T(self.lid..string.format("Flight %s case %d is changing marshal stack %d --> %d.", mflight.groupname, mflight.case, mstack, newstack)) + + if mflight.ai then + + -- Command AI to decrease stack. Flag is set in the routine. + self:_MarshalAI(mflight, newstack) + + else + + -- Decrease stack/flag. Human player needs to take care himself. + mflight.flag:Set(newstack) + + -- Inform players. + if mflight.difficulty~=AIRBOSS.Difficulty.HARD then + + -- Send message to all non-pros that they can descent. + local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(newstack, case)) + local text=string.format("descent to next lower stack at %d ft", alt) + self:MessageToPlayer(mflight, text, "MARSHAL") + + end + + -- Loop over section members. + for _,_sec in pairs(mflight.section) do + local sec=_sec --#AIRBOSS.PlayerData + + -- Also decrease flag for section members of flight. + sec.flag:Set(newstack) + + -- Inform section member. + if sec.difficulty~=AIRBOSS.Difficulty.HARD then + local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(newstack, case)) + local text=string.format("follow your lead to next lower stack at %d ft", alt) + self:MessageToPlayer(sec, text, "MARSHAL") + end + + end + + end + end + end + end + + + if nopattern then + + -- Debug message. + self:T(self.lid..string.format("Flight %s is leaving stack but not going to pattern.", flight.groupname)) + + -- Set flag to -1. -1 is rather arbitrary. Should not be -100 or positive. + flight.flag:Set(-1) + + else + + -- Debug message. + local Tmarshal=UTILS.SecondsToClock(timer.getAbsTime()-flight.time) + self:T(self.lid..string.format("Flight %s is leaving marshal after %s and going pattern.", flight.groupname, Tmarshal)) + + -- Decrease flag. + flight.flag:Set(stack-1) + + -- Add flight to pattern queue. + table.insert(self.Qpattern, flight) + + end + + -- New time stamp for time in pattern. + flight.time=timer.getAbsTime() + + + -- Remove flight from marshal queue. + self:_RemoveGroupFromQueue(self.Qmarshal, flight.group) + +end + +--- Get next free stack depending on recovery case. Note that here we assume one flight group per stack! +-- @param #AIRBOSS self +-- @param #number case Recovery case. Default current (self) case in progress. +-- @return #number Lowest free stack available for the given case. +function AIRBOSS:_GetFreeStack(case) + + -- Recovery case. + case=case or self.case + + -- Get stack + local nfull + if case==1 then + -- Lowest Case I stack. + nfull=self:_GetQueueInfo(self.Qmarshal, 1) + else + -- Lowest Case II or III stack. + nfull=self:_GetQueueInfo(self.Qmarshal, 23) + end + + -- Simple case without a recovery tanker for now. + local nfree=nfull+1 + + --[[ + -- Get recovery tanker stack. + local tankerstack=9999 + if self.tanker and case==1 then + tankerstack=self:_GetAngels(self.tanker.altitude) + end + + if nfull=1 + + -- No update if carrier is turning! + if turning then + self:T2(self.lid..string.format("Carrier is turning. Delta Heading = %.1f", deltaLast)) + return + end + + -- Check if orientation changed. + local Hchange=false + if math.abs(deltaHeading)>=Hupdate then + self:T(self.lid..string.format("Carrier heading changed by %d degrees. Turning=%s.", deltaHeading, tostring(turning))) + Hchange=true + end + + -- Get distance to saved position. + local dist=pos:Get2DDistance(self.Cposition) + + -- Check if carrier moved more than ~10 km. + local Dchange=false + if dist>=Dupdate then + self:T(self.lid..string.format("Carrier position changed by %.1f NM. Turning=%s.", UTILS.MetersToNM(dist), tostring(turning))) + Dchange=true + end + + -- If heading or distance changed ==> update marshal AI patterns. + if Hchange or Dchange then + + -- Loop over all marshal flights + for _,_flight in pairs(self.Qmarshal) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Update marshal pattern of AI keeping the same stack. + if flight.ai then + self:_MarshalAI(flight, flight.flag:Get()) + end + + end + + -- Inform player about new final bearing. + if Hchange then + -- 99, new final bearing XXX + local FB=self:GetFinalBearing(true) + local text=string.format("new final bearing %d.", FB) + self:MessageToAll(text, "MARSHAL", "99", 10) + end + + -- Reset parameters for next update check. + self.Corientation=vNew + self.Cposition=pos + self.Tpupdate=timer.getTime() + end + +end + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Player Status +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check current player status. +-- @param #AIRBOSS self +function AIRBOSS:_CheckPlayerStatus() + + -- Loop over all players. + for _playerName,_playerData in pairs(self.players) do + local playerData=_playerData --#AIRBOSS.PlayerData + + if playerData then + + -- Player unit. + local unit=playerData.unit + + -- Check if unit is alive and in air. + if unit:IsAlive() then + + -- Display aircraft attitude and other parameters as message text. + if playerData.attitudemonitor then + self:_AttitudeMonitor(playerData) + end + + -- Check if player is in carrier controlled area (zone with R=50 NM around the carrier). + if unit:IsInZone(self.zoneCCA) then + + -- Check if player is too close to another aircraft in the pattern. + -- TODO: At which steps is the really necessary. Case II/III? + if playerData.step==AIRBOSS.PatternStep.INITIAL or + playerData.step==AIRBOSS.PatternStep.BREAKENTRY or + playerData.step==AIRBOSS.PatternStep.EARLYBREAK or + playerData.step==AIRBOSS.PatternStep.LATEBREAK or + playerData.step==AIRBOSS.PatternStep.ABEAM or + playerData.step==AIRBOSS.PatternStep.GROOVE_XX or + playerData.step==AIRBOSS.PatternStep.GROOVE_IM then + --self:_CheckPlayerPatternDistance(playerData) + end + + if playerData.step==AIRBOSS.PatternStep.UNDEFINED then + + -- Status undefined. + local time=timer.getAbsTime() + local clock=UTILS.SecondsToClock(time) + self:T3(string.format("Player status undefined. Waiting for next step. Time %s", clock)) + + elseif playerData.step==AIRBOSS.PatternStep.REFUELING then + + -- Nothing to do here at the moment. + + elseif playerData.step==AIRBOSS.PatternStep.SPINNING then + + -- Might still be better to stay in commencing? + + elseif playerData.step==AIRBOSS.PatternStep.HOLDING then + + -- CASE I/II/III: In holding pattern. + self:_Holding(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.COMMENCING then + + -- CASE I/II/III: New approach. + self:_Commencing(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.PLATFORM then + + -- CASE II/III: Player has reached 5k "Platform". + self:_Platform(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.ARCIN then + + -- Case II/III if offset. + self:_ArcInTurn(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.ARCOUT then + + -- Case II/III if offset. + self:_ArcOutTurn(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.DIRTYUP then + + -- CASE III: Player has descended to 1200 ft and is going level from now on. + self:_DirtyUp(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.BULLSEYE then + + -- CASE III: Player has intercepted the glide slope and should follow "Bullseye" (ICLS). + self:_Bullseye(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.INITIAL then + + -- CASE I/II: Player is at the initial position entering the landing pattern. + self:_Initial(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.BREAKENTRY then + + -- CASE I/II: Break entry. + self:_BreakEntry(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.EARLYBREAK then + + -- CASE I/II: Early break. + self:_Break(playerData, AIRBOSS.PatternStep.EARLYBREAK) + + elseif playerData.step==AIRBOSS.PatternStep.LATEBREAK then + + -- CASE I/II: Late break. + self:_Break(playerData, AIRBOSS.PatternStep.LATEBREAK) + + elseif playerData.step==AIRBOSS.PatternStep.ABEAM then + + -- CASE I/II: Abeam position. + self:_Abeam(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.NINETY then + + -- CASE:I/II: Check long down wind leg. + self:_CheckForLongDownwind(playerData) + + -- At the ninety. + self:_Ninety(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.WAKE then + + -- CASE I/II: In the wake. + self:_Wake(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.FINAL then + + -- CASE I/II: Turn to final and enter the groove. + self:_Final(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.GROOVE_XX or + playerData.step==AIRBOSS.PatternStep.GROOVE_RB or + playerData.step==AIRBOSS.PatternStep.GROOVE_IM or + playerData.step==AIRBOSS.PatternStep.GROOVE_IC or + playerData.step==AIRBOSS.PatternStep.GROOVE_AR or + playerData.step==AIRBOSS.PatternStep.GROOVE_IW then + + -- CASE I/II: In the groove. + self:_Groove(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.DEBRIEF then + + -- Debriefing in 10 seconds. + SCHEDULER:New(self, self._Debrief, {playerData}, 10) + + -- Undefined status. + playerData.step=AIRBOSS.PatternStep.UNDEFINED + + else + + self:E(self.lid..string.format("ERROR: Unknown player step %s. Please report!", tostring(playerData.step))) + + end + + else + self:T(self.lid.."WARNING: Player left the CCA!") + end + + else + -- Unit not alive. + self:T(self.lid.."WARNING: Player unit is not alive!") + end + end + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- EVENT functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Airboss event handler for event birth. +-- @param #AIRBOSS self +-- @param Core.Event#EVENTDATA EventData +function AIRBOSS:OnEventBirth(EventData) + self:F3({eventbirth = EventData}) + + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + self:T3(self.lid.."BIRTH: unit = "..tostring(EventData.IniUnitName)) + self:T3(self.lid.."BIRTH: group = "..tostring(EventData.IniGroupName)) + self:T3(self.lid.."BIRTH: player = "..tostring(_playername)) + + if _unit and _playername then + + local _uid=_unit:GetID() + local _group=_unit:GetGroup() + local _callsign=_unit:GetCallsign() + + -- Debug output. + local text=string.format("AIRBOSS: Pilot %s, callsign %s entered unit %s of group %s.", _playername, _callsign, _unitName, _group:GetName()) + self:T(self.lid..text) + MESSAGE:New(text, 5):ToAllIf(self.Debug) + + -- Check if aircraft type the player occupies is carrier capable. + local rightaircraft=self:_IsCarrierAircraft(_unit) + if rightaircraft==false then + local text=string.format("Player aircraft type %s not supported by AIRBOSS class.", _unit:GetTypeName()) + MESSAGE:New(text, 30):ToAllIf(self.Debug) + self:T(self.lid..text) + return + end + + -- Add Menu commands. + self:_AddF10Commands(_unitName) + + -- Init new player data. + local playerData=self:_NewPlayer(_unitName) + + -- Init player data. + self.players[_playername]=playerData + + -- Welcome player message. + self:MessageToPlayer(playerData, string.format("Welcome, %s %s!", playerData.difficulty, playerData.name), "AIRBOSS", "", 5) + end +end + +--- Airboss event handler for event land. +-- @param #AIRBOSS self +-- @param Core.Event#EVENTDATA EventData +function AIRBOSS:OnEventLand(EventData) + self:F3({eventland = EventData}) + + -- Get unit name that landed. + local _unitName=EventData.IniUnitName + + -- Check if this was a player. + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + -- Debug output. + self:T3(self.lid.."LAND: unit = "..tostring(EventData.IniUnitName)) + self:T3(self.lid.."LAND: group = "..tostring(EventData.IniGroupName)) + self:T3(self.lid.."LAND: player = "..tostring(_playername)) + + -- This would be the closest airbase. + local airbase=EventData.Place + local airbasename=tostring(airbase:GetName()) + + -- Check if aircraft landed on the right airbase. + if airbasename==self.airbase:GetName() then + + -- Stern coordinate at the rundown. + local stern=self:_GetSternCoord() + + local zoneCarrier=self:_GetZoneCarrierBox() + + -- Check if player or AI landed. + if _unit and _playername then + -- Human Player landed. + + -- Get info. + local _uid=_unit:GetID() + local _group=_unit:GetGroup() + local _callsign=_unit:GetCallsign() + + -- Debug output. + local text=string.format("Player %s, callsign %s unit %s (ID=%d) of group %s landed at airbase %s", _playername, _callsign, _unitName, _uid, _group:GetName(), airbasename) + self:T(self.lid..text) + MESSAGE:New(text, 5, "DEBUG"):ToAllIf(self.Debug) + + -- Player data. + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + -- Check that player landed on the carrier. + if _unit:IsInZone(zoneCarrier) then + + -- Check if player already landed. We dont need a second time. + if playerData.landed then + + self:E(self.lid..string.format("Player %s just landed a second time.", _playername)) + + else + + -- Coordinate at landing event. + local coord=playerData.unit:GetCoordinate() + + -- Get distances relative to + local X,Z,rho,phi=self:_GetDistances(_unit) + + -- Landing distance wrt to stern position. + local dist=coord:Get2DDistance(stern) + + -- Debug mark of player landing coord. + if self.Debug and false then + -- Debug mark of player landing coord. + local lp=coord:MarkToAll("Landing coord.") + coord:SmokeGreen() + end + + -- Get wire. We additionally shift the landing coord back because landing event for players is unfortunately delayed. + local wire=self:_GetWire(coord, 75) + + -- No wire ==> Bolter, Bolter radio call. + -- TODO: might need a better place for this. or check + if wire>4 then + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.BOLTER) + end + + -- Get time in the groove. + local gdataX0=playerData.groove.X0 --#AIRBOSS.GrooveData + if gdataX0 then + playerData.Tgroove=timer.getTime()-gdataX0.TGroove + else + playerData.Tgroove=999 + end + + -- Debug text. + local text=string.format("Player %s AC type %s landed at dist=%.1f m. Trapped wire=%d.", playerData.name, playerData.actype, dist, wire) + text=text..string.format(" X=%.1f m, Z=%.1f m, rho=%.1f m.", X, Z, rho) + self:T(self.lid..text) + + -- We did land. + playerData.landed=true + + -- Unkonwn step until we now more. + playerData.step=AIRBOSS.PatternStep.UNDEFINED + + -- Call trapped function in 1 second to make sure we did not bolter. + SCHEDULER:New(self, self._Trapped, {playerData}, 1) + + end + + else + -- TODO: Handle case where player did not land on the carrier. + self:E(self.lid..string.format("Player %s did not land in carrier box zone. Maybe in the water near the carrier?", playerData.name)) + end + + else + + -- AI unit landed. + + -- Coordinate at landing event + local coord=EventData.IniUnit:GetCoordinate() + + -- Debug mark of player landing coord. + local dist=coord:Get2DDistance(self:GetCoordinate()) + + -- Get wire + local wire=self:_GetWire(coord, 0) + + -- Aircraft type. + local _type=EventData.IniUnit:GetTypeName() + + -- Debug text. + local text=string.format("AI unit %s of type %s landed at dist=%.1f m. Trapped wire=%d.", _unitName, _type, dist, wire) + self:T(self.lid..text) + + -- AI always lands ==> remove unit from flight group and queues. + self:_RemoveUnitFromFlight(EventData.IniUnit) + + end + end +end + +--- Airboss event handler for event crash. +-- @param #AIRBOSS self +-- @param Core.Event#EVENTDATA EventData +function AIRBOSS:OnEventCrash(EventData) + self:F3({eventland = EventData}) + + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + self:T3(self.lid.."CRASH: unit = "..tostring(EventData.IniUnitName)) + self:T3(self.lid.."CRASH: group = "..tostring(EventData.IniGroupName)) + self:T3(self.lid.."CARSH: player = "..tostring(_playername)) + + if _unit and _playername then + self:T(self.lid..string.format("Player %s crashed!",_playername)) + else + self:T2(self.lid..string.format("AI unit %s crashed!", EventData.IniUnitName)) + end + + -- Remove unit from flight and queues. + self:_RemoveUnitFromFlight(EventData.IniUnit) +end + +--- Airboss event handler for event Ejection. +-- @param #AIRBOSS self +-- @param Core.Event#EVENTDATA EventData +function AIRBOSS:OnEventEjection(EventData) + self:F3({eventland = EventData}) + + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + self:T3(self.lid.."EJECT: unit = "..tostring(EventData.IniUnitName)) + self:T3(self.lid.."EJECT: group = "..tostring(EventData.IniGroupName)) + self:T3(self.lid.."EJECT: player = "..tostring(_playername)) + + if _unit and _playername then + self:T(self.lid..string.format("Player %s ejected!",_playername)) + else + self:T2(self.lid..string.format("AI unit %s ejected!", EventData.IniUnitName)) + end + + -- Remove unit from flight and queues. + self:_RemoveUnitFromFlight(EventData.IniUnit) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- PATTERN functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Holding. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_Holding(playerData) + + -- Player unit and flight. + local unit=playerData.unit + + -- Current stack. + local stack=playerData.flag:Get() + + -- Pattern alitude. + local patternalt=self:_GetMarshalAltitude(stack, playerData.case) + + -- Player altitude. + local playeralt=unit:GetAltitude() + + -- Get holding zone of player. + local zoneHolding=self:_GetZoneHolding(playerData.case, stack) + + -- Check if player is in holding zone. + local inholdingzone=unit:IsInZone(zoneHolding) + + -- Altitude difference between player and assinged stack. + local altdiff=playeralt-patternalt + + -- Acceptable altitude depending on player skill. + local altgood=UTILS.FeetToMeters(500) + if playerData.difficulty==AIRBOSS.Difficulty.HARD then + -- Pros can be expected to be within +-100 ft. + altgood=UTILS.FeetToMeters(100) + elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then + -- Normal guys should be within +-300 ft. + altgood=UTILS.FeetToMeters(300) + elseif playerData.difficulty==AIRBOSS.Difficulty.EASY then + -- Students should be within +-500 ft. + altgood=UTILS.FeetToMeters(500) + else + -- ERROR + end + + -- Check if stack just collapsed and give the player one minute to change the alitude. + local justcollapsed=false + if self.Tcollapse then + -- Time since last stack change. + local dT=timer.getTime()-self.Tcollapse + + -- Check if less then 60 seconds. + if dT<=60 then + justcollapsed=true + end + end + + -- Check if altitude is acceptable. + local goodalt=math.abs(altdiff)altgood then + + -- Issue warning for being too high. + if not playerData.warning then + text=text..string.format("You left your assigned altitude. Descent to angels %d.", angels) + playerData.warning=true + end + + elseif altdiff<-altgood then + + -- Issue warning for being too low. + if not playerData.warning then + text=text..string.format("You left your assigned altitude. Climb to angels %d.", angels) + playerData.warning=true + end + + end + + end + + -- Back to assigned altitude. + if playerData.warning and math.abs(altdiff)<=altgood then + text=text..string.format("Altitude is looking good again.") + playerData.warning=nil + end + + elseif playerData.holding==false then + + -- Player left holding zone + if inholdingzone then + -- Player is back in the holding zone. + self:T("Player is back in the holding zone after leaving it.") + text=text..string.format("You are back in the holding zone. Now stay there!") + playerData.holding=true + else + -- Player is still outside the holding zone. + self:T2("Player still outside the holding zone. What are you doing man?!") + end + + elseif playerData.holding==nil then + -- Player did not entered the holding zone yet. + + if inholdingzone then + + -- Player arrived in holding zone. + playerData.holding=true + + -- Debug output. + self:T("Player entered the holding zone for the first time.") + + -- Inform player. + text=text..string.format("You arrived at the holding zone.") + + -- Feedback on altitude. + if goodalt then + text=text..string.format(" Altitude is good.") + else + if altdiff<0 then + text=text..string.format(" But you're too low.") + else + text=text..string.format(" But you're too high.") + end + text=text..string.format("\nCurrently assigned altitude is %d ft.", UTILS.MetersToFeet(patternalt)) + playerData.warning=true + end + + -- No info for the pros. + if playerData.difficulty==AIRBOSS.Difficulty.HARD then + text="" + end + + else + -- Player did not yet arrive in holding zone. + self:T2("Waiting for player to arrive in the holding zone.") + end + + end + + -- Send message. + self:MessageToPlayer(playerData, text, "MARSHAL", nil, 5) +end + + +--- Commence approach. This step initializes the player data. Next step depends on recovery case: +-- +-- * Case 1: Initial +-- * Case 2/3: Platform +-- +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_Commencing(playerData) + + -- Initialize player data for new approach. + self:_InitPlayer(playerData) + + -- Commencing message to player only. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + local text=string.format("Commencing. (Case %d)", playerData.case) + self:MessageToPlayer(playerData, text, playerData.onboard, "", 5) + end + + -- Next step: depends on case recovery. + if playerData.case==1 then + -- CASE I: Player has to fly to the initial which is 3 NM DME astern of the boat. + playerData.step=AIRBOSS.PatternStep.INITIAL + else + -- CASE II/III: Player has to start the descent at 4000 ft/min to the platform at 5k ft. + playerData.step=AIRBOSS.PatternStep.PLATFORM + end + + -- Next step hint. + self:_StepHint(playerData) + playerData.warning=nil +end + +--- Start pattern when player enters the initial zone in case I/II recoveries. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_Initial(playerData) + + -- Check if player is in initial zone and entering the CASE I pattern. + local inzone=playerData.unit:IsInZone(self.zoneInitial) + + -- Relative heading to carrier direction. + local relheading=self:_GetRelativeHeading(playerData.unit, false) + + -- Check if player is in zone and flying roughly in the right direction. + if inzone and math.abs(relheading)<60 then + + -- Send message for normal and easy difficulty. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + + -- Inform player. + local hint=string.format("Initial") + + -- Hook down for students. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + hint=hint.." - Hook down!" + end + + self:MessageToPlayer(playerData, hint, "MARSHAL") + end + + -- Next step: Break entry. + playerData.step=AIRBOSS.PatternStep.BREAKENTRY + playerData.warning=nil + self:_StepHint(playerData) + end + +end + +--- Check if player is in CASE II/III approach corridor. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_CheckCorridor(playerData) + + -- Check if player is in valid zone + local validzone=self:_GetZoneCorridor(playerData.case) + + -- Check if we are inside the moving zone. + local invalid=playerData.unit:IsNotInZone(validzone) + + -- Issue warning. + if invalid and (not playerData.warning) then + self:MessageToPlayer(playerData, "You left the valid approach corridor!", "MARSHAL") + playerData.warning=true + end + + -- Back in zone. + if (not invalid) and playerData.warning then + self:MessageToPlayer(playerData, "You're back in the approach corridor. Now stay there!", "MARSHAL") + playerData.warning=false + end + +end + +--- Platform at 5k ft for case II/III recoveries. Descent at 2000 ft/min. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_Platform(playerData) + + -- Check if player left or got back to the approach corridor. + self:_CheckCorridor(playerData) + + -- Check if we are inside the moving zone. + local inzone=playerData.unit:IsInZone(self:_GetZonePlatform(playerData.case)) + + -- Check if we are in zone. + if inzone then + + -- Debug message. + MESSAGE:New("Platform step reached", 5, "DEBUG"):ToAllIf(self.Debug) + + -- Get optimal altitiude. + local altitude, aoa, distance, speed =self:_GetAircraftParameters(playerData) + + -- Get altitude hint. + local hintAlt=self:_AltitudeCheck(playerData, altitude) + + -- Get altitude hint. + local hintSpeed=self:_SpeedCheck(playerData, speed) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + + -- Altitude and speed hint. + local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintSpeed) + + self:MessageToPlayer(playerData, hint, "MARSHAL", "") + end + + -- Next step: depends. + if math.abs(self.holdingoffset)>0 and playerData.case>1 then + -- Turn to BRC (case II) or FB (case III). + playerData.step=AIRBOSS.PatternStep.ARCIN + else + if playerData.case==2 then + -- Case II: Initial zone then Case I recovery. + playerData.step=AIRBOSS.PatternStep.INITIAL + elseif playerData.case==3 then + -- CASE III: Dirty up. + playerData.step=AIRBOSS.PatternStep.DIRTYUP + end + end + + -- Next step hint. + self:_StepHint(playerData) + playerData.warning=nil + end +end + + +--- Arc in turn for case II/III recoveries. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_ArcInTurn(playerData) + + -- Check if player left or got back to the approach corridor. + self:_CheckCorridor(playerData) + + -- Check if we are inside the moving zone. + local inzone=playerData.unit:IsInZone(self:_GetZoneArcIn(playerData.case)) + + if inzone then + + -- Debug message. + MESSAGE:New("Arc Turn In step reached", 5, "DEBUG"):ToAllIf(self.Debug) + + -- Get optimal altitiude. + local altitude, aoa, distance, speed=self:_GetAircraftParameters(playerData) + + -- Get speed hint. + local hintSpeed=self:_SpeedCheck(playerData, speed) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + + -- Hint speed. + local hint=string.format("%s\n%s", playerData.step, hintSpeed) + + -- Hint turn and set TACAN. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + -- Get inverse magnetic radial without offset ==> FB for Case II or BRC for Case III. + local radial=self:GetRadial(playerData.case, true, false, true) + local turn="right" + if self.holdingoffset<0 then + turn="left" + end + hint=hint..string.format("\nTurn %s and select TACAN %d°.", turn, radial) + end + + -- Message to player. + self:MessageToPlayer(playerData, hint, "MARSHAL", "") + end + + -- Next step: Arc Out Turn. + playerData.step=AIRBOSS.PatternStep.ARCOUT + playerData.warning=nil + self:_StepHint(playerData) + end +end + +--- Arc out turn for case II/III recoveries. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_ArcOutTurn(playerData) + + -- Check if player left or got back to the approach corridor. + self:_CheckCorridor(playerData) + + -- Check if we are inside the moving zone. + local inzone=playerData.unit:IsInZone(self:_GetZoneArcOut(playerData.case)) + + --if self:_CheckLimits(X, Z, self.DirtyUp) then + if inzone then + + -- Debug message. + MESSAGE:New("Arc Turn Out step reached", 5, "DEBUG"):ToAllIf(self.Debug) + + -- Get optimal altitiude. + local altitude, aoa, distance, speed=self:_GetAircraftParameters(playerData) + + -- Get speed hint. + local hintSpeed=self:_SpeedCheck(playerData, speed) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + local hint=string.format("%s\n%s", playerData.step, hintSpeed) + self:MessageToPlayer(playerData, hint, "MARSHAL", "") + end + + -- Next step: + if playerData.case==2 then + -- Case II: Initial. + playerData.step=AIRBOSS.PatternStep.INITIAL + elseif playerData.case==3 then + -- Case III: Dirty up. + playerData.step=AIRBOSS.PatternStep.DIRTYUP + else + -- ERROR! + end + + -- Next step hint. + self:_StepHint(playerData) + playerData.warning=nil + end +end + +--- Dirty up and level out at 1200 ft for case III recovery. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_DirtyUp(playerData) + + -- Check if player left or got back to the approach corridor. + self:_CheckCorridor(playerData) + + -- Check if we are inside the moving zone. + local inzone=playerData.unit:IsInZone(self:_GetZoneDirtyUp(playerData.case)) + + if inzone then + + -- Debug message. + MESSAGE:New("Dirty up step reached", 5, "DEBUG"):ToAllIf(self.Debug) + + -- Get optimal altitiude. + local altitude, aoa, distance, speed=self:_GetAircraftParameters(playerData) + + -- Get altitude hint. + local hintAlt, debrief=self:_AltitudeCheck(playerData, altitude) + + -- Get speed hint. + -- TODO: Not sure if we already need to be onspeed AoA at this point? + local hintSpeed=self:_SpeedCheck(playerData, speed) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + + -- Hint alt and speed. + local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintSpeed) + + -- Hint turn and set TACAN. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + hint=hint.."\nDirty up! Hook, gear and flaps down." + end + + self:MessageToPlayer(playerData, hint, "MARSHAL", "") + end + + -- Radio call "Say/Fly needles". Delayed by 10/15 seconds. + self:RadioTransmission(self.MarshalRadio, AIRBOSS.MarshalCall.SAYNEEDLES, false, 10) + self:RadioTransmission(self.MarshalRadio, AIRBOSS.MarshalCall.FLYNEEDLES, false, 15) + -- TODO: Make Fly Bullseye call if no automatic ICLS is active. + + -- Next step: CASE III: Intercept glide slope and follow bullseye (ICLS). + playerData.step=AIRBOSS.PatternStep.BULLSEYE + playerData.warning=nil + self:_StepHint(playerData) + end +end + +--- Intercept glide slop and follow ICLS, aka Bullseye for case III recovery. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_Bullseye(playerData) + + -- Check if player left or got back to the approach corridor. + self:_CheckCorridor(playerData) + + -- Check if we are inside the moving zone. + local inzone=playerData.unit:IsInZone(self:_GetZoneBullseye(playerData.case)) + + -- Relative heading to carrier direction of the runway. + local relheading=self:_GetRelativeHeading(playerData.unit, true) + + -- Check if player is in zone and flying roughly in the right direction. + if inzone and math.abs(relheading)<60 then + + -- Debug message. + MESSAGE:New("Bullseye step reached", 5, "DEBUG"):ToAllIf(self.Debug) + + -- Get optimal altitiude. + local altitude, aoa, distance, speed=self:_GetAircraftParameters(playerData) + + -- Get altitude hint. + local hintAlt=self:_AltitudeCheck(playerData, altitude) + + -- Get altitude hint. + local hintAoA=self:_AoACheck(playerData, aoa) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + + -- Hint alt and aoa. + local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintAoA) + + -- Hint follow the needles. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + hint=hint..string.format("Intercept glide slope and follow the needles.") + end + + self:MessageToPlayer(playerData, hint, "MARSHAL", "") + end + + -- Next step: Groove Call the ball. + playerData.step=AIRBOSS.PatternStep.GROOVE_XX + playerData.warning=nil + + -- Stephint should be empty. + self:_StepHint(playerData) + end +end + + +--- Break entry for case I/II recoveries. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_BreakEntry(playerData) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z, rho, phi=self:_GetDistances(playerData.unit) + + -- Abort condition check. + if self:_CheckAbort(X, Z, self.BreakEntry) then + self:_AbortPattern(playerData, X, Z, self.BreakEntry, true) + return + end + + -- Check if we are in front of the boat (diffX > 0). + if self:_CheckLimits(X, Z, self.BreakEntry) then + + -- Get optimal altitude, distance and speed. + local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData) + + -- Get altitude hint. + local hintAlt=self:_AltitudeCheck(playerData, alt) + + -- Get speed hint. + local hintSpeed=self:_SpeedCheck(playerData, speed) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintSpeed) + self:MessageToPlayer(playerData, hint, "MARSHAL", "") + end + + -- Next step: Early Break. + playerData.step=AIRBOSS.PatternStep.EARLYBREAK + playerData.warning=nil + self:_StepHint(playerData) + end +end + + +--- Break. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #string part Part of the break. +function AIRBOSS:_Break(playerData, part) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z, rho, phi=self:_GetDistances(playerData.unit) + + -- Early or late break. + local breakpoint = self.BreakEarly + if part==AIRBOSS.PatternStep.LATEBREAK then + breakpoint = self.BreakLate + end + + -- Check abort conditions. + if self:_CheckAbort(X, Z, breakpoint) then + self:_AbortPattern(playerData, X, Z, breakpoint, true) + return + end + + -- Check limits. + if self:_CheckLimits(X, Z, breakpoint) then + + -- Get optimal altitude, distance and speed. + local altitude=self:_GetAircraftParameters(playerData) + + -- Grade altitude. + local hint, debrief=self:_AltitudeCheck(playerData, altitude) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + + -- Hint alt. + local hint=string.format("%s %s", playerData.step, hint) + + -- Hint dirty up. + if playerData.difficult==AIRBOSS.Difficulty.EASY and part==AIRBOSS.PatternStep.LATEBREAK then + hint=hint.."\nDirty up! Gear down, flaps down. Check hook down." + end + + self:MessageToPlayer(playerData, hint, "MARSHAL", "") + end + + -- Debrief + self:_AddToDebrief(playerData, debrief) + + -- Next step: Late Break or Abeam. + if part==AIRBOSS.PatternStep.EARLYBREAK then + playerData.step=AIRBOSS.PatternStep.LATEBREAK + else + playerData.step=AIRBOSS.PatternStep.ABEAM + end + + playerData.warning=nil + self:_StepHint(playerData) + end +end + +--- Long downwind leg check. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_CheckForLongDownwind(playerData) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z=self:_GetDistances(playerData.unit) + + -- One NM from carrier is too far. + local limit=UTILS.NMToMeters(-1.5) + + -- Check we are not too far out w.r.t back of the boat. + if X90 and self:_CheckLimits(X, Z, self.Wake) then + -- Message to player. + self:MessageToPlayer(playerData, "You are already at the wake and have not passed the 90. Turn faster next time!", "LSO") + --TODO: pattern WO? + end +end + +--- At the Wake. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_Wake(playerData) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z = self:_GetDistances(playerData.unit) + + -- Check abort conditions. + if self:_CheckAbort(X, Z, self.Wake) then + self:_AbortPattern(playerData, X, Z, self.Wake, true) + return + end + + -- Right behind the wake of the carrier dZ>0. + if self:_CheckLimits(X, Z, self.Wake) then + + -- Get optimal altitude, distance and speed. + local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData) + + -- Grade altitude. + local hintAlt, debriefAlt=self:_AltitudeCheck(playerData, alt) + + -- Grade AoA. + local hintAoA, debriefAoA=self:_AoACheck(playerData, aoa) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintAoA) + self:MessageToPlayer(playerData, hint, "LSO", "") + end + + -- Debrief. + local debrief=string.format("%s\n%s", debriefAlt, debriefAoA) + + -- Add to debrief. + self:_AddToDebrief(playerData, debrief) + + -- Next step: Final. + playerData.step=AIRBOSS.PatternStep.FINAL + playerData.warning=nil + self:_StepHint(playerData) + end +end + +--- Turn to final. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_Final(playerData) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z, rho, phi = self:_GetDistances(playerData.unit) + + -- In front of carrier or more than 4 km behind carrier. + if self:_CheckAbort(X, Z, self.Final) then + self:_AbortPattern(playerData, X, Z, self.Final, true) + return + end + + -- Relative heading 0=fly parallel +-90=fly perpendicular + local relhead=self:_GetRelativeHeading(playerData.unit, true) + + -- Line up wrt runway. + local lineup=self:_Lineup(playerData.unit, true) + + -- Player's angle of bank. + local roll=playerData.unit:GetRoll() + + -- Check if player is in +-5 deg cone and flying towards the runway. + if math.abs(lineup)<5 then --and math.abs(relhead)<5 then + + -- Get optimal altitude, distance and speed. + local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData) + + -- Grade altitude. + local hintAlt, debriefAlt=self:_AltitudeCheck(playerData, alt) + + -- AoA feed back + local hintAoA, debriefAoA=self:_AoACheck(playerData, aoa) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintAoA) + self:MessageToPlayer(playerData, hint, "LSO", "") + end + + -- Add to debrief. + local debrief=string.format("%s\n%s", debriefAlt, debriefAoA) + self:_AddToDebrief(playerData, debrief) + + -- Gather pilot data. + local groovedata={} --#AIRBOSS.GrooveData + groovedata.Step=playerData.step + groovedata.Alt=alt + groovedata.AoA=aoa + groovedata.GSE=self:_Glideslope(playerData.unit, 3.5) + groovedata.LUE=self:_Lineup(playerData.unit, true) + groovedata.Roll=roll + groovedata.Rhdg=relhead + groovedata.TGroove=timer.getTime() + + -- TODO: could add angled approach if lineup<5 and relhead>5. This would mean the player has not turned in correctly! + + -- Groove data. + playerData.groove.X0=groovedata + + -- Next step: X start & call the ball. + playerData.step=AIRBOSS.PatternStep.GROOVE_XX + playerData.warning=nil + self:_StepHint(playerData) + end + +end + + +--- In the groove. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_Groove(playerData) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z = self:_GetDistances(playerData.unit) + + -- Player altitude + local alt=playerData.unit:GetAltitude() + + -- Player group. + local player=playerData.unit:GetGroup() + + -- Check abort conditions. + if self:_CheckAbort(X, Z, self.Groove) then + self:_AbortPattern(playerData, X, Z, self.Groove, true) + return + end + + -- Stern position at the rundown. + local stern=self:_GetSternCoord() + + -- Distance from rundown to player aircraft. + local rho=stern:Get2DDistance(playerData.unit:GetCoordinate()) + + -- Lineup with runway centerline. + local lineupError=self:_Lineup(playerData.unit, true) + + -- Glide slope. + local glideslopeError=self:_Glideslope(playerData.unit, 3.5) + + -- Get AoA. + local AoA=playerData.unit:GetAoA() + + -- For debugging. + MESSAGE:New(string.format("%s: LineUp=%.1f GlideSlope=%.1f AoA=%.1f", playerData.step, lineupError, glideslopeError, AoA), 3, nil, true):ToAllIf(self.Debug) + + -- Ranges in the groove. + local RXX=UTILS.NMToMeters(0.750) -- Start of groove. 0.75 = 1389 m + local RRB=UTILS.NMToMeters(0.500) -- Roger Ball! call. 0.5 = 926 m + local RIM=UTILS.NMToMeters(0.375) -- In the Middle 0.75/2. 0.375 = 695 m + local RIC=UTILS.NMToMeters(0.100) -- In Close. 0.1 = 185 m + local RAR=UTILS.NMToMeters(0.000) -- At the Ramp. + + -- Data + local groovedata={} --#AIRBOSS.GrooveData + groovedata.Step=playerData.step + groovedata.Alt=alt + groovedata.AoA=AoA + groovedata.GSE=glideslopeError + groovedata.LUE=lineupError + groovedata.Roll=playerData.unit:GetRoll() + groovedata.Rhdg=self:_GetRelativeHeading(playerData.unit, true) + + if rho<=RXX and playerData.step==AIRBOSS.PatternStep.GROOVE_XX then + + -- LSO "Call the ball" call. + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.CALLTHEBALL) + playerData.Tlso=timer.getTime() + + -- Pilot "405, Hornet Ball, 3.2" + -- Pilot output should come from pilot. + --local text=string.format("Hornet Ball, %.1f", self:_GetFuelState(playerData.unit)/1000) + --self:MessageToPlayer(playerData, text, playerData.onboard, "", 3, false, 3) + + -- Store data. + playerData.groove.XX=groovedata + + -- Next step: roger ball. + playerData.step=AIRBOSS.PatternStep.GROOVE_RB + playerData.warning=nil + + elseif rho<=RRB and playerData.step==AIRBOSS.PatternStep.GROOVE_RB then + + -- LSO "Roger ball" call. + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.ROGERBALL) + playerData.Tlso=timer.getTime()+1 + + -- Store data. + playerData.groove.RB=groovedata + + -- Next step: in the middle. + playerData.step=AIRBOSS.PatternStep.GROOVE_IM + playerData.warning=nil + + elseif rho<=RIM and playerData.step==AIRBOSS.PatternStep.GROOVE_IM then + + -- Debug. + local text=string.format("Groove IM=%d m", rho) + MESSAGE:New(text, 5):ToAllIf(self.Debug) + self:T2(self.lid..text) + + -- Store data. + playerData.groove.IM=groovedata + + -- Next step: in close. + playerData.step=AIRBOSS.PatternStep.GROOVE_IC + playerData.warning=nil + + elseif rho<=RIC and playerData.step==AIRBOSS.PatternStep.GROOVE_IC then + + -- Check if player was already waved off. + if playerData.waveoff==false then + + -- Debug + local text=string.format("Groove IC=%d m", rho) + MESSAGE:New(text, 5):ToAllIf(self.Debug) + self:T2(self.lid..text) + + -- Store data. + playerData.groove.IC=groovedata + + -- Check if player should wave off. + local waveoff=self:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) + + --local text=string.format("FF D=%.1f GLE=%.1f LUE=%.1f waveoff=%s", rho, glideslopeError, lineupError, tostring(waveoff)) + --env.info(text) + + -- Let's see.. + if waveoff then + + -- LSO Wave off! + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.WAVEOFF) + playerData.Tlso=timer.getTime() + + -- Player was waved off! + playerData.waveoff=true + + return + else + -- Next step: AR at the ramp. + playerData.step=AIRBOSS.PatternStep.GROOVE_AR + playerData.warning=nil + end + + end + + elseif rho<=RAR and playerData.step==AIRBOSS.PatternStep.GROOVE_AR then + + -- Debug. + local text=string.format("Groove AR=%d m", rho) + MESSAGE:New(text, 5):ToAllIf(self.Debug) + self:T2(self.lid..text) + + -- Store data. + playerData.groove.AR=groovedata + + -- Next step: in the wires. + playerData.step=AIRBOSS.PatternStep.GROOVE_IW + playerData.warning=nil + end + + -- Time since last LSO call. + local deltaT=timer.getTime()-playerData.Tlso + + -- Check if we are beween 3/4 NM and end of ship. Only one call every 3 seconds. + if X=RAR and rho=3 and playerData.waveoff==false then + + -- LSO call if necessary. + self:_LSOadvice(playerData, glideslopeError, lineupError) + + end + + -------------------------------------------------------- + --- Some time here the landing event MIGHT be triggered. + -------------------------------------------------------- + + -- Player infront of the carrier X>~77 m. + if X>self.carrierparam.totlength+self.carrierparam.sterndist then + + if playerData.waveoff then + + if playerData.landed then + -- This should not happen because landing event was triggered. + self:_AddToDebrief(playerData, "You were waved off but landed anyway. Airboss wants to talk to you!") + else + self:_AddToDebrief(playerData, "You were waved off.") + end + + elseif playerData.boltered then + + -- This should not happen because landing event was triggered. + self:_AddToDebrief(playerData, "You boltered.") + + else + + -- This should not happen. + self:E("What? Player was not waved off but flew past the carrier without landing. Why did waveoff not kick in?") + + -- TODO: This is more like a pilot wave off then. + self:_AddToDebrief(playerData, "Pilot wave-off.") + + playerData.waveoff=true + + end + + -- Next step: debrief. + playerData.step=AIRBOSS.PatternStep.DEBRIEF + playerData.warning=nil + + end + +end + +--- LSO check if player needs to wave off. +-- Wave off conditions are: +-- +-- * Glide slope error > 1 degree. +-- * Line up error > 3 degrees. +-- * AoA check but only for TOPGUN graduates. +-- @param #AIRBOSS self +-- @param #number glideslopeError Glide slope error in degrees. +-- @param #number lineupError Line up error in degrees. +-- @param #number AoA Angle of attack of player aircraft. +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @return #boolean If true, player should wave off! +function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) + + -- Assume we're all good. + local waveoff=false + + -- Too high or too low? + if math.abs(glideslopeError)>1 then + local text=string.format("Wave off due to glide slope error |%.1f| > 1 degree!", glideslopeError) + self:T(self.lid..string.format("%s: %s", playerData.name, text)) + self:_AddToDebrief(playerData, text) + waveoff=true + end + + -- Too far from centerline? + if math.abs(lineupError)>3 then + local text=string.format("Wave off due to line up error |%.1f| > 3 degrees!", lineupError) + self:T(self.lid..string.format("%s: %s", playerData.name, text)) + self:_AddToDebrief(playerData, text) + waveoff=true + end + + -- Too slow or too fast? Only for pros. + if playerData.difficulty==AIRBOSS.Difficulty.HARD then + -- Get aircraft specific AoA values + local aoaac=self:_GetAircraftAoA(playerData) + -- Check too slow or too fast. + if AoAaoaac.Slow then + local text=string.format("Wave off due to AoA %.1f > %.1f!", AoA, aoaac.Slow) + self:T(self.lid..string.format("%s: %s", playerData.name, text)) + self:_AddToDebrief(playerData, text) + waveoff=true + end + end + + return waveoff +end + +--- Get "stern" coordinate. +-- @param #AIRBOSS self +-- @return Core.Point#COORDINATE Coordinate at the rundown of the carrier. +function AIRBOSS:_GetSternCoord() + + -- Heading of carrier (true). + local hdg=self.carrier:GetHeading() + + -- Final bearing (true). + local FB=self:GetFinalBearing() + + -- Stern coordinate (sterndist<0). Also translate 10 meters starboard wrt Final bearing. + local stern=self:GetCoordinate():Translate(self.carrierparam.sterndist, hdg):Translate(7, FB+90) + + -- Set altitude. + stern:SetAltitude(self.carrierparam.deckheight) + + return stern +end + +--- Get wire from landing position. +-- @param #AIRBOSS self +-- @param Core.Point#COORDINATE Lcoord Landing position. +-- @param #number dc Distance correction. Shift the landing coord back if dc>0 and forward if dc<0. +-- @return #number Trapped wire (1-4) or 99 if no wire was trapped. +function AIRBOSS:_GetWire(Lcoord, dc) + + -- Final bearing (true). + local FB=self:GetFinalBearing() + + -- Stern coordinate (sterndist<0). Also translate 10 meters starboard wrt Final bearing. + local Scoord=self:_GetSternCoord() + + -- Distance to landing coord. + local Ldist=Lcoord:Get2DDistance(Scoord) + + -- For human (not AI) the lading event is delayed unfortunately. Therefore, we need another correction factor. + dc= dc or 65 + + -- Corrected landing distance wrt to stern. Landing distance needs to be reduced due to delayed landing event for human players. + local d=Ldist-dc + + -- Shift wires from stern to their correct position. + local w1=self.carrierparam.wire1 + local w2=self.carrierparam.wire2 + local w3=self.carrierparam.wire3 + local w4=self.carrierparam.wire4 + + -- Which wire was caught? + local wire + if d wire=%d (dc=%.1f)", Ldist, Ldist-dc, wire, dc)) + + return wire +end + +--- Trapped? Check if in air or not after landing event. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_Trapped(playerData) + + if playerData.unit:InAir()==false then + -- Seems we have successfully landed. + + -- Lets see if we can get a good wire. + local unit=playerData.unit + + -- Coordinate of player aircraft. + local coord=unit:GetCoordinate() + + -- Get velocity in km/h. We need to substrackt the carrier velocity. + local v=unit:GetVelocityKMH()-self.carrier:GetVelocityKMH() + + -- Stern coordinate. + local stern=self:_GetSternCoord() + + -- Distance to stern pos. + local s=stern:Get2DDistance(coord) + + -- Get current wire (estimate). This now based on the position where the player comes to a standstill which should reflect the trapped wire better. + local dcorr=100 + local wire=self:_GetWire(coord, dcorr) + + -- Debug. + local text=string.format("Player %s _Trapped: v=%.1f km/h, s=%.1f m ==> wire=%d (dcorr=%d)", playerData.name, v, s, wire, dcorr) + self:T(self.lid..text) + + -- Call this function again until v < threshold. Player comes to a standstill ==> Get wire! + if v>5 then + SCHEDULER:New(self, self._Trapped, {playerData}, 0.1) + return + end + + ---------------------------------------- + --- Form this point on we have converged + ---------------------------------------- + + -- Put some smoke and a mark. + if self.Debug then + coord:SmokeBlue() + coord:MarkToAll(text) + stern:MarkToAll("Stern") + end + + -- Set player wire. + playerData.wire=wire + + -- Message to player. + local text=string.format("Trapped %d-wire.", wire) + if wire==3 then + text=text.." Well done!" + elseif wire==2 then + text=text.." Not bad, maybe you even get the 3rd next time." + elseif wire==4 then + text=text.." That was scary. You can do better than this!" + elseif wire==1 then + text=text.." Try harder next time!" + end + + -- Message to player. + self:MessageToPlayer(playerData, text, "LSO", "") + + -- Debrief. + local hint = string.format("Trapped %d-wire.", wire) + self:_AddToDebrief(playerData, hint, "Groove: IW") + + else + + --Again in air ==> Boltered! + local text=string.format("Player %s boltered in trapped function.", playerData.name) + self:T(self.lid..text) + MESSAGE:New(text, 5, "DEBUG"):ToAllIf(self.debug) + + -- Bolter switch on. + playerData.boltered=true + + end + + -- Next step: debriefing. + playerData.step=AIRBOSS.PatternStep.DEBRIEF + playerData.warning=nil +end + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ZONE functions +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get Bullseye zone with radius 1 NM and DME 3 NM from the carrier. Radial depends on recovery case. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @return Core.Zone#ZONE_RADIUS Arc in zone. +function AIRBOSS:_GetZoneBullseye(case) + + -- Radius = 1 NM. + local radius=UTILS.NMToMeters(1) + + -- Distance = 3 NM + local distance=UTILS.NMToMeters(3) + + -- Zone depends on Case recovery. + local radial=self:GetRadial(case, false, false) + + -- Get coordinate and vec2. + local coord=self:GetCoordinate():Translate(distance, radial) + local vec2=coord:GetVec2() + + -- Create zone. + local zone=ZONE_RADIUS:New("Zone Bullseye", vec2, radius) + + return zone +end + +--- Get dirty up zone with radius 1 NM and DME 9 NM from the carrier. Radial depends on recovery case. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @return Core.Zone#ZONE_RADIUS Arc in zone. +function AIRBOSS:_GetZoneDirtyUp(case) + + -- Radius = 1 NM. + local radius=UTILS.NMToMeters(1) + + -- Distance = 9 NM + local distance=UTILS.NMToMeters(9) + + -- Zone depends on Case recovery. + local radial=self:GetRadial(case, false, false) + + -- Get coordinate and vec2. + local coord=self:GetCoordinate():Translate(distance, radial) + local vec2=coord:GetVec2() + + -- Create zone. + local zone=ZONE_RADIUS:New("Zone Dirty Up", vec2, radius) + + return zone +end + +--- Get arc out zone with radius 1 NM and DME 12 NM from the carrier. Radial depends on recovery case. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @return Core.Zone#ZONE_RADIUS Arc in zone. +function AIRBOSS:_GetZoneArcOut(case) + + -- Radius = 1 NM. + local radius=UTILS.NMToMeters(1) + + -- Distance = 12 NM + local distance=UTILS.NMToMeters(12) + + -- Zone depends on Case recovery. + local radial=self:GetRadial(case, false, false) + + -- Get coordinate of carrier and translate. + local coord=self:GetCoordinate():Translate(distance, radial) + + -- Create zone. + local zone=ZONE_RADIUS:New("Zone Arc Out", coord:GetVec2(), radius) + + return zone +end + +--- Get arc in zone with radius 1 NM and DME 14 NM from the carrier. Radial depends on recovery case. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @return Core.Zone#ZONE_RADIUS Arc in zone. +function AIRBOSS:_GetZoneArcIn(case) + + -- Radius = 1 NM. + local radius=UTILS.NMToMeters(1) + + -- Zone depends on Case recovery. + local radial=self:GetRadial(case, false, true) + + -- Angle between FB/BRC and holding zone. + local alpha=math.rad(self.holdingoffset) + + -- 12+x NM from carrier + local x=12/math.cos(alpha) + + -- Distance = 14 NM + local distance=UTILS.NMToMeters(x) + + -- Get coordinate. + local coord=self:GetCoordinate():Translate(distance, radial) + + -- Create zone. + local zone=ZONE_RADIUS:New("Zone Arc In", coord:GetVec2(), radius) + + return zone +end + +--- Get platform zone with radius 1 NM and DME 19 NM from the carrier. Radial depends on recovery case. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @return Core.Zone#ZONE_RADIUS Circular platform zone. +function AIRBOSS:_GetZonePlatform(case) + + -- Radius = 1 NM. + local radius=UTILS.NMToMeters(1) + + -- Zone depends on Case recovery. + local radial=self:GetRadial(case, false, true) + + -- Angle between FB/BRC and holding zone. + local alpha=math.rad(self.holdingoffset) + + -- Distance = 19 NM + local distance=UTILS.NMToMeters(19) --/math.cos(alpha) + + -- Get coordinate. + local coord=self:GetCoordinate():Translate(distance, radial) + + -- Create zone. + local zone=ZONE_RADIUS:New("Zone Platform", coord:GetVec2(), radius) + + return zone +end + + +--- Get approach corridor zone. Shape depends on recovery case. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @return Core.Zone#ZONE_POLYGON_BASE Box zone. +function AIRBOSS:_GetZoneCorridor(case) + + -- Radial and offset. + local radial=self:GetRadial(case, false, false) + local offset=self:GetRadial(case, false, true) + + -- Angle between radial and offset in rad. + local alpha=math.rad(self.holdingoffset) + + -- Distance shift ahead of carrier to allow for some space to bolter + local dx=2 + + -- Width of the box in NM. + local w=2 + local w2=w/2 + + -- Distance from carrier to arc out zone. + local d=12 + + -- Length of the box in NM. + local x=(d+w/2)/math.cos(alpha) + local l=28-x + --local l=15 --/math.cos(alpha) + + -- Some math... + local y1=d-w2 + local x1=y1*math.tan(alpha) + local y2=d+w2 + local x2=y2*math.tan(alpha) + local b=w2*(1/math.cos(alpha)-1) + + -- This is what we need. + local P=x1+b + local Q=x2-b + + -- Debug output. + self:T3(string.format("FF case %d radial = %d", case, radial)) + self:T3(string.format("FF case %d offset = %d", case, offset)) + self:T3(string.format("FF w = %.1f NM", w)) + self:T3(string.format("FF l = %.1f NM", l)) + self:T3(string.format("FF d = %.1f NM", d)) + self:T3(string.format("FF y1 = %.1f NM", y1)) + self:T3(string.format("FF x1 = %.1f NM", x1)) + self:T3(string.format("FF y2 = %.1f NM", y2)) + self:T3(string.format("FF x2 = %.1f NM", x2)) + self:T3(string.format("FF b = %.1f NM", b)) + self:T3(string.format("FF P = %.1f NM", P)) + self:T3(string.format("FF Q = %.1f NM", Q)) + + local c={} + c[1]=self:GetCoordinate():Translate(-UTILS.NMToMeters(dx), radial) --Carrier coordinate translated 2 NM in direction of travel to allow for bolter space. + + if math.abs(self.holdingoffset)>1 then + -- Complicated case with an angle. + c[2]=c[1]:Translate( UTILS.NMToMeters(w2), radial-90) -- 1 Right of carrier. + c[3]=c[2]:Translate( UTILS.NMToMeters(d+dx+w2), radial) -- 13 "south" @ 1 right + c[4]=c[3]:Translate( UTILS.NMToMeters(Q), radial+90) -- + c[5]=c[4]:Translate( UTILS.NMToMeters(l), offset) + c[6]=c[5]:Translate( UTILS.NMToMeters(w), offset+90) -- Back wall (angled) + c[9]=c[1]:Translate( UTILS.NMToMeters(w2), radial+90) -- 1 left of carrier. + c[8]=c[9]:Translate( UTILS.NMToMeters(d+dx-w2), radial) -- 1 left and 11 behind of carrier. + c[7]=c[8]:Translate( UTILS.NMToMeters(P), radial+90) + else + -- Easy case of a long box. + c[2]=c[1]:Translate( UTILS.NMToMeters(w2), radial-90) + c[3]=c[2]:Translate( UTILS.NMToMeters(d+dx+w2+l), radial) -- 12+1+10 = 23 NM behind the carrier. Stack 1 starts at 21 and is 7 NM. + c[4]=c[3]:Translate( UTILS.NMToMeters(w), radial+90) + c[5]=c[1]:Translate( UTILS.NMToMeters(w2), radial+90) + end + + + -- Create an array of a square! + local p={} + for _i,_c in ipairs(c) do + if self.Debug then + --_c:SmokeBlue() + end + p[_i]=_c:GetVec2() + end + + -- Square zone length=10NM width=6 NM behind the carrier starting at angels+15 NM behind the carrier. + -- So stay 0-5 NM (+1 NM error margin) port of carrier. + local zone=ZONE_POLYGON_BASE:New("CASE II/III Approach Corridor", p) + + return zone +end + + +--- Get zone of carrier. Carrier is approximated as rectangle. +-- @param #AIRBOSS self +-- @return Core.Zone#ZONE Zone surrounding the carrier. +function AIRBOSS:_GetZoneCarrierBox() + + -- Stern coordinate. + local S=self:_GetSternCoord() + + -- Current carrier heading. + local hdg=self:GetHeading(false) + + -- Coordinate array. + local p={} + + -- Starboard stern point. + p[1]=S:Translate(self.carrierparam.totwidthstarboard, hdg+90) + + -- Starboard bow point. + p[2]=p[1]:Translate(self.carrierparam.totlength, hdg) + + -- Port bow point. + p[3]=p[2]:Translate(self.carrierparam.totwidthstarboard+self.carrierparam.totwidthport, hdg-90) + + -- Port stern point. + p[4]=p[3]:Translate(self.carrierparam.totlength, hdg-180) + + -- Convert to vec2. + local vec2={} + for _,coord in ipairs(p) do + table.insert(vec2, coord:GetVec2()) + end + + -- Create polygon zone. + local zone=ZONE_POLYGON_BASE:New("Carrier Box Zone", vec2) + + return zone +end + +--- Get zone of landing runway +-- @param #AIRBOSS self +-- @return Core.Zone#ZONE Zone surrounding landing runway. +function AIRBOSS:_GetZoneRunwayBox() + + -- Stern coordinate. + local S=self:_GetSternCoord() + + -- Current carrier heading. + local FB=self:GetFinalBearing(false) + + -- Coordinate array. + local p={} + + -- Points. + p[1]=S:Translate(self.carrierparam.rwywidth, FB+90) + p[2]=p[1]:Translate(self.carrierparam.rwylength, FB) + p[3]=p[2]:Translate(self.carrierparam.rwywidth*2, FB-90) + p[4]=p[3]:Translate(self.carrierparam.rwylength, FB-180) + + -- Convert to vec2. + local vec2={} + for _,coord in ipairs(p) do + table.insert(vec2, coord:GetVec2()) + end + + -- Create polygon zone. + local zone=ZONE_POLYGON_BASE:New("Landing Runway Zone", vec2) + + return zone +end + +--- Get holding zone of player. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @param #number stack Marshal stack number. +-- @return Core.Zone#ZONE Holding zone. +function AIRBOSS:_GetZoneHolding(case, stack) + + -- Holding zone. + local zoneHolding=nil --Core.Zone#ZONE + + -- Stack is <= 0 ==> no marshal zone. + if stack<=0 then + return nil + end + + -- Pattern alitude. + local patternalt, c1, c2=self:_GetMarshalAltitude(stack, case) + + -- Select case. + if case==1 then + -- CASE I + + -- Get current carrier heading. + local hdg=self:GetHeading() + + -- Zone 2.5 NM port of carrier with a radius of 2.75 NM (holding pattern should be < 5 NM but we allow 10% error). + local R=UTILS.NMToMeters(2.5) + + -- Create zone. + local coord=self:GetCoordinate():Translate(R, hdg+270) + + zoneHolding=ZONE_RADIUS:New("CASE I Holding Zone", coord:GetVec2(), R*1.1) + + else + -- CASE II/II + + -- Get radial. + local radial=self:GetRadial(case, false, true) + + -- Create an array of a square! + local p={} + p[1]=c2:Translate(UTILS.NMToMeters(1), radial-90):GetVec2() --c2 is at (angels+15) NM directly behind the carrier. We translate it 1 NM starboard. + p[2]=c1:Translate(UTILS.NMToMeters(1), radial-90):GetVec2() --c1 is 7 NM further behind. Also translated 1 NM starboard. + p[3]=c1:Translate(UTILS.NMToMeters(7), radial+90):GetVec2() --p3 6 NM port of carrier. + p[4]=c2:Translate(UTILS.NMToMeters(7), radial+90):GetVec2() --p4 6 NM port of carrier. + + -- Square zone length=7NM width=6 NM behind the carrier starting at angels+15 NM behind the carrier. + -- So stay 0-5 NM (+1 NM error margin) port of carrier. + zoneHolding=ZONE_POLYGON_BASE:New("CASE II/III Holding Zone", p) + end + + return zoneHolding +end + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ORIENTATION functions +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Provide info about player status on the fly. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_AttitudeMonitor(playerData) + + -- Player unit. + local unit=playerData.unit + + -- Aircraft attitude. + local aoa=unit:GetAoA() + local yaw=unit:GetYaw() + local roll=unit:GetRoll() + local pitch=unit:GetPitch() + + -- Distance to the boat. + local dist=playerData.unit:GetCoordinate():Get2DDistance(self:GetCoordinate()) + local dx,dz,rho,phi=self:_GetDistances(unit) + + -- Wind vector. + local wind=unit:GetCoordinate():GetWindWithTurbulenceVec3() + + -- Aircraft veloecity vector. + local velo=unit:GetVelocityVec3() + local vabs=UTILS.VecNorm(velo) + + -- Relative heading Aircraft to Carrier. + local relhead=self:_GetRelativeHeading(playerData.unit) + + -- Output + local text=string.format("Pattern step: %s\n", playerData.step) + text=text..string.format("AoA=%.1f | |V|=%.1f knots\n", aoa, UTILS.MpsToKnots(vabs)) + text=text..string.format("Vx=%.1f Vy=%.1f Vz=%.1f m/s\n", velo.x, velo.y, velo.z) + text=text..string.format("Pitch=%.1f° | Roll=%.1f° | Yaw=%.1f°\n", pitch, roll, yaw) + text=text..string.format("Climb Angle=%.1f° | Rate=%d ft/min\n", unit:GetClimbAngle(), velo.y*196.85) + text=text..string.format("R=%.1f NM | X=%d Z=%d m\n", UTILS.MetersToNM(rho), dx, dz) + text=text..string.format("Gamma=%.1f°", relhead) + -- If in the groove, provide line up and glide slope error. + if playerData.step==AIRBOSS.PatternStep.GROOVE_XX or + playerData.step==AIRBOSS.PatternStep.GROOVE_RB or + playerData.step==AIRBOSS.PatternStep.GROOVE_IM or + playerData.step==AIRBOSS.PatternStep.GROOVE_IC or + playerData.step==AIRBOSS.PatternStep.GROOVE_AR or + playerData.step==AIRBOSS.PatternStep.GROOVE_IW then + local lineup=self:_Lineup(playerData.unit, true) + local glideslope=self:_Glideslope(playerData.unit, 3.5) + text=text..string.format("\nLU Error = %.1f° (line up)", lineup) + text=text..string.format("\nGS Error = %.1f° (glide slope)", glideslope) + end + + -- Wind (for debugging). + --text=text..string.format("Wind Vx=%.1f Vy=%.1f Vz=%.1f\n", wind.x, wind.y, wind.z) + + MESSAGE:New(text, 3, nil , true):ToClient(playerData.client) +end + +--- Get glide slope of aircraft unit. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Aircraft unit. +-- @param #number optangle (Optional) Return glide slope relative to this angle, i.e. the error from the optimal glide slope ~3.5 degrees. +-- @return #number Glide slope angle in degrees measured from the deck of the carrier and third wire. +function AIRBOSS:_Glideslope(unit, optangle) + + -- Default is 0. + optangle=optangle or 0 + + -- Stern coordinate. + local stern=self:_GetSternCoord() + + -- Ideally we want to land between 2nd and 3rd wire. + if self.carrierparam.wire3 then + local d23=self.carrierparam.wire2 --+0.5*(self.carrierparam.wire3-self.carrierparam.wire2) + stern=stern:Translate(d23, self:GetFinalBearing(false), true) + end + + -- Distance from stern to aircraft. + local x=unit:GetCoordinate():Get2DDistance(stern) + + -- Altitude of unit corrected by the deck height of the carrier. + local h=unit:GetAltitude()-self.carrierparam.deckheight + + -- Glide slope. + local glideslope=math.atan(h/x) + + -- Glide slope (error) in degrees. + local gs=math.deg(glideslope)-optangle + + --env.info(string.format("FF Glide slope error = %.1f, x=%.1f h=%.1f", gs, x, h)) + + return gs +end + +--- Get line up of player wrt to carrier. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Aircraft unit. +-- @param #boolean runway If true, include angled runway. +-- @return #number Line up with runway heading in degrees. 0 degrees = perfect line up. +1 too far left. -1 too far right. +function AIRBOSS:_Lineup(unit, runway) + + -- Vector to carrier. + local A=self:_GetSternCoord():GetVec3() + + -- Vector to player. + local B=unit:GetVec3() + + -- Vector from player to carrier. + local C=UTILS.VecSubstract(A, B) + + -- Only in 2D plane. + C.y=0 + + -- Orientation of carrier. + local X=self.carrier:GetOrientationX() + + -- Rotate orientation to angled runway. + if runway then + X=UTILS.Rotate2D(X, -self.carrierparam.rwyangle) + end + + -- Projection of player pos on x component. + local x=UTILS.VecDot(X, C) + + -- Orientation of carrier. + local Z=self.carrier:GetOrientationZ() + + -- Rotate orientation to angled runway. + if runway then + Z=UTILS.Rotate2D(Z, -self.carrierparam.rwyangle) + end + + -- Projection of player pos on z component. + local z=UTILS.VecDot(Z, C) + + --- + + -- Position of the aircraft in the new coordinate system. + local a={x=x, y=0, z=z} + + -- Stern position in the new coordinate system, which is simply the origin. + local b={x=0, y=0, z=0} + + -- Vector from plane to ref point on the boat. + local c=UTILS.VecSubstract(a, b) + + -- Current line up and error wrt to final heading of the runway. + local lineup=math.deg(math.atan2(c.z, c.x)) + + --env.info(string.format("FF lineup 2 = %.1f", lineup)) + + return lineup +end + +--- Get true (or magnetic) heading of carrier. +-- @param #AIRBOSS self +-- @param #boolean magnetic If true, calculate magnetic heading. By default true heading is returned. +-- @return #number Carrier heading in degrees. +function AIRBOSS:GetHeading(magnetic) + self:F3({magnetic=magnetic}) + + -- Carrier heading + local hdg=self.carrier:GetHeading() + + -- Include magnetic declination. + if magnetic then + hdg=hdg-self.magvar + end + + -- Adjust negative values. + if hdg<0 then + hdg=hdg+360 + end + + return hdg +end + +--- Get base recovery course (BRC) of carrier. +-- The is the magnetic heading of the carrier. +-- @param #AIRBOSS self +-- @return #number BRC in degrees. +function AIRBOSS:GetBRC() + return self:GetHeading(true) +end + + +--- Get final bearing (FB) of carrier. +-- By default, the routine returns the magnetic FB depending on the current map (Caucasus, NTTR, Normandy, Persion Gulf etc). +-- The true bearing can be obtained by setting the *TrueNorth* parameter to true. +-- @param #AIRBOSS self +-- @param #boolean magnetic If true, magnetic FB is returned. +-- @return #number FB in degrees. +function AIRBOSS:GetFinalBearing(magnetic) + + -- First get the heading. + local fb=self:GetHeading(magnetic) + + -- Final baring = BRC including angled deck. + fb=fb+self.carrierparam.rwyangle + + -- Adjust negative values. + if fb<0 then + fb=fb+360 + end + + return fb +end + +--- Get radial with respect to carrier BRC or FB and (optionally) holding offset. +-- +-- * case=1: radial=FB-180 +-- * case=2: radial=HDG-180 (+offset) +-- * case=3: radial=FB-180 (+offset) +-- +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @param #boolean magnetic If true, magnetic radial is returned. Default is true radial. +-- @param #boolean offset If true, inlcude holding offset. +-- @param #boolean inverse Return inverse, i.e. radial-180 degrees. +-- @return #number Radial in degrees. +function AIRBOSS:GetRadial(case, magnetic, offset, inverse) + + -- Case or current case. + case=case or self.case + + -- Radial. + local radial + + -- Select case. + if case==1 then + + -- Get radial. + radial=self:GetFinalBearing(magnetic)-180 + + elseif case==2 then + + -- Radial wrt to heading of carrier. + radial=self:GetHeading(magnetic)-180 + + -- Holding offset angle (+-15 or 30 degrees usually) + if offset then + radial=radial+self.holdingoffset + end + + elseif case==3 then + + -- Radial wrt angled runway. + radial=self:GetFinalBearing(magnetic)-180 + + -- Holding offset angle (+-15 or 30 degrees usually) + if offset then + radial=radial+self.holdingoffset + end + + end + + -- Adjust for negative values. + if radial<0 then + radial=radial+360 + end + + -- Inverse? + if inverse then + + -- Inverse radial + radial=radial-180 + + -- Adjust for negative values. + if radial<0 then + radial=radial+360 + end + + end + + return radial +end + +--- Get relative heading of player wrt carrier. +-- This is the angle between the direction/orientation vector of the carrier and the direction/orientation vector of the provided unit. +-- Note that this is calculated in the X-Z plane, i.e. the altitude Y is not taken into account. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Player unit. +-- @param #boolean runway (Optional) If true, return relative heading of unit wrt to angled runway of the carrier. +-- @return #number Relative heading in degrees. An angle of 0 means, unit fly parallel to carrier. An angle of + or - 90 degrees means, unit flies perpendicular to carrier. +function AIRBOSS:_GetRelativeHeading(unit, runway) + + -- Direction vector of the carrier. + local vC=self.carrier:GetOrientationX() + + -- Direction vector of the unit. + local vP=unit:GetOrientationX() + + -- We only want the X-Z plane. Aircraft could fly parallel but ballistic and we dont want the "pitch" angle. + vC.y=0 ; vP.y=0 + + -- Get angle between the two orientation vectors in rad. + local rhdg=math.deg(math.acos(UTILS.VecDot(vC,vP)/UTILS.VecNorm(vC)/UTILS.VecNorm(vP))) + + -- Include runway angle. + if runway then + rhdg=rhdg-self.carrierparam.rwyangle + end + + -- Return heading in degrees. + return rhdg +end + +--- Calculate distances between carrier and aircraft unit. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Aircraft unit. +-- @return #number Distance [m] in the direction of the orientation of the carrier. +-- @return #number Distance [m] perpendicular to the orientation of the carrier. +-- @return #number Distance [m] to the carrier. +-- @return #number Angle [Deg] from carrier to plane. Phi=0 if the plane is directly behind the carrier, phi=90 if the plane is starboard, phi=180 if the plane is in front of the carrier. +function AIRBOSS:_GetDistances(unit) + + -- Vector to carrier + local a=self.carrier:GetVec3() + + -- Vector to player + local b=unit:GetVec3() + + -- Vector from carrier to player. + local c={x=b.x-a.x, y=0, z=b.z-a.z} + + -- Orientation of carrier. + local x=self.carrier:GetOrientationX() + + -- Projection of player pos on x component. + local dx=UTILS.VecDot(x,c) + + -- Orientation of carrier. + local z=self.carrier:GetOrientationZ() + + -- Projection of player pos on z component. + local dz=UTILS.VecDot(z,c) + + -- Polar coordinates + local rho=math.sqrt(dx*dx+dz*dz) + + + -- Not exactly sure any more what I wanted to calculate here. + local phi=math.deg(math.atan2(dz,dx)) + if phi<0 then + phi=phi+360 + end + + -- phi=0 if the plane is directly behind the carrier, phi=180 if the plane is in front of the carrier + phi=phi-180 + + if phi<0 then + phi=phi+360 + end + + return dx,dz,rho,phi +end + +--- Check limits for reaching next step. +-- @param #AIRBOSS self +-- @param #number X X position of player unit. +-- @param #number Z Z position of player unit. +-- @param #AIRBOSS.Checkpoint check Checkpoint. +-- @return #boolean If true, checkpoint condition for next step was reached. +function AIRBOSS:_CheckLimits(X, Z, check) + + -- Limits + local nextXmin=check.LimitXmin==nil or (check.LimitXmin and (check.LimitXmin<0 and X<=check.LimitXmin or check.LimitXmin>=0 and X>=check.LimitXmin)) + local nextXmax=check.LimitXmax==nil or (check.LimitXmax and (check.LimitXmax<0 and X>=check.LimitXmax or check.LimitXmax>=0 and X<=check.LimitXmax)) + local nextZmin=check.LimitZmin==nil or (check.LimitZmin and (check.LimitZmin<0 and Z<=check.LimitZmin or check.LimitZmin>=0 and Z>=check.LimitZmin)) + local nextZmax=check.LimitZmax==nil or (check.LimitZmax and (check.LimitZmax<0 and Z>=check.LimitZmax or check.LimitZmax>=0 and Z<=check.LimitZmax)) + + -- Proceed to next step if all conditions are fullfilled. + local next=nextXmin and nextXmax and nextZmin and nextZmax + + -- Debug info. + local text=string.format("step=%s: next=%s: X=%d Xmin=%s Xmax=%s | Z=%d Zmin=%s Zmax=%s", + check.name, tostring(next), X, tostring(check.LimitXmin), tostring(check.LimitXmax), Z, tostring(check.LimitZmin), tostring(check.LimitZmax)) + self:T3(self.lid..text) + + return next +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- LSO functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- LSO advice radio call. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #number glideslopeError Error in degrees. +-- @param #number lineupError Error in degrees. +function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) + + -- Advice time. + local advice=0 + + -- Glideslope high/low calls. + local text="" + if glideslopeError>1 then + -- "You're high!" + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.HIGH, true) + advice=advice+AIRBOSS.LSOCall.HIGH.duration + elseif glideslopeError>0.5 then + -- "You're a little high." + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.HIGH, false) + advice=advice+AIRBOSS.LSOCall.HIGH.duration + elseif glideslopeError<-1.0 then + -- "Power!" + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.POWER, true) + advice=advice+AIRBOSS.LSOCall.POWER.duration + elseif glideslopeError<-0.5 then + -- "You're a little low." + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.POWER, false) + advice=advice+AIRBOSS.LSOCall.POWER.duration + else + text="Good altitude." + end + + text=text..string.format(" Glideslope Error = %.2f°", glideslopeError) + text=text.."\n" + + -- Lineup left/right calls. + if lineupError<-3 then + -- "Come left!" + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.COMELEFT, true) + advice=advice+AIRBOSS.LSOCall.COMELEFT.duration + elseif lineupError<-1 then + -- "Come left." + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.COMELEFT, false) + advice=advice+AIRBOSS.LSOCall.COMELEFT.duration + elseif lineupError>3 then + -- "Right for lineup!" + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.RIGHTFORLINEUP, true) + advice=advice+AIRBOSS.LSOCall.RIGHTFORLINEUP.duration + elseif lineupError>1 then + -- "Right for lineup." + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.RIGHTFORLINEUP, false) + advice=advice+AIRBOSS.LSOCall.RIGHTFORLINEUP.duration + else + text=text.."Good lineup." + end + + text=text..string.format(" Lineup Error = %.1f°\n", lineupError) + + -- Get current AoA. + local aoa=playerData.unit:GetAoA() + + -- Get aircraft AoA parameters. + local aircraftaoa=self:_GetAircraftAoA(playerData) + + -- Rate aoa. + if aoa>=aircraftaoa.Slow then + -- "Your're slow!" + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.SLOW, true) + advice=advice+AIRBOSS.LSOCall.SLOW.duration + elseif aoa>=aircraftaoa.OnSpeedMax and aoa=aircraftaoa.OnSpeedMin and aoa=aircraftaoa.Fast and aoa 24 seconds: No Grade "--" +-- +-- If you manage to be between 16.4 and and 16.6 seconds, you will even get and okay underline "\_OK\_". +-- +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @return #string LSO grade for time in groove, i.e. \_OK\_, OK, (OK), --. +function AIRBOSS:_EvalGrooveTime(playerData) + + -- Time in groove. + local t=playerData.Tgroove + + local grade="" + if t<9 then + grade="--" + elseif t<12 then + grade="(OK)" + elseif t<22 then + grade="OK" + elseif t<=24 then + grade="(OK)" + else + grade="--" + end + + -- The unicorn! + if t>=16.4 and t<=16.6 then + grade="_OK_" + end + + return grade +end + +--- Grade approach. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @return #string LSO grade, i.g. _OK_, OK, (OK), --, etc. +-- @return #number Points. +-- @return #string LSO analysis of flight path. +function AIRBOSS:_LSOgrade(playerData) + + --- Count deviations. + local function count(base, pattern) + return select(2, string.gsub(base, pattern, "")) + end + + -- Analyse flight data and conver to LSO text. + local GXX,nXX=self:_Flightdata2Text(playerData, AIRBOSS.GroovePos.XX) + local GIM,nIM=self:_Flightdata2Text(playerData, AIRBOSS.GroovePos.IM) + local GIC,nIC=self:_Flightdata2Text(playerData, AIRBOSS.GroovePos.IC) + local GAR,nAR=self:_Flightdata2Text(playerData, AIRBOSS.GroovePos.AR) + + -- Put everything together. + local G=GXX.." "..GIM.." ".." "..GIC.." "..GAR + + -- Count number of minor, normal and major deviations. + local N=nXX+nIM+nIC+nAR + local nL=count(G, '_')/2 + local nS=count(G, '%(') + local nN=N-nS-nL + + local grade + local points + if N==0 then + -- No deviations, should be REALLY RARE! + grade="_OK_" + points=5.0 + else + if nL>0 then + -- Larger deviations ==> "No grade" 2.0 points. + grade="--" + points=2.0 + elseif nN>0 then + -- No larger but average deviations ==> "Fair Pass" Pass with average deviations and corrections. + grade="(OK)" + points=3.0 + else + -- Only minor corrections + grade="OK" + points=4.0 + end + end + + -- Replace" )"( and "__" + G=G:gsub("%)%(", "") + G=G:gsub("__","") + + -- Debug info + local text="LSO grade:\n" + text=text..G.."\n" + text=text.."Grade = "..grade.." points = "..points.."\n" + text=text.."# of total deviations = "..N.."\n" + text=text.."# of large deviations _ = "..nL.."\n" + text=text.."# of normal deviations = "..nN.."\n" + text=text.."# of small deviations ( = "..nS.."\n" + self:T2(self.lid..text) + + -- Special cases. + if playerData.patternwo then + -- Pattern Wave Off + grade="PWO" + if playerData.lig then + G="LIG" + elseif playerData.patternwo then + G="n/a" + end + points=1.0 + elseif playerData.waveoff then + -- Wave Off + if playerData.landed then + --AIRBOSS wants to talk to you! + grade="CUT" + points=0.0 + else + grade="WO" + points=1.0 + end + elseif playerData.boltered then + -- Bolter + grade="-- (BOLTER)" + points=2.5 + end + + return grade, points, G +end + +--- Grade flight data. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #string groovestep Step in the groove. +-- @param #AIRBOSS.GrooveData fdata Flight data in the groove. +-- @return #string LSO grade or empty string if flight data table is nil. +-- @return #number Number of deviations from perfect flight path. +function AIRBOSS:_Flightdata2Text(playerData, groovestep) + + local function little(text) + return string.format("(%s)",text) + end + local function underline(text) + return string.format("_%s_", text) + end + + -- Data. + local fdata=playerData.groove[groovestep] + + -- No flight data ==> return empty string. + if fdata==nil then + self:T(self.lid.."Flight data is nil.") + return "", 0 + end + + -- Flight data. + local step=fdata.Step + local AOA=fdata.AoA + local GSE=fdata.GSE + local LUE=fdata.LUE + local ROL=fdata.Roll + + -- Aircraft specific AoA values. + local acaoa=self:_GetAircraftAoA(playerData) + + -- Speed. + local S=nil + if AOA>acaoa.SLOW then + S=underline("SLO") + elseif AOA>acaoa.Slow then + S="SLO" + elseif AOA>acaoa.OnSpeedMax then + S=little("SLO") + elseif AOA1 then + A=underline("H") + elseif GSE>0.5 then + A="H" + elseif GSE>0.25 then + A=little("H") + elseif GSE<-1 then + A=underline("LO") + elseif GSE<-0.5 then + A="LO" + elseif GSE<-0.25 then + A=little("LO") + end + + -- Line up. Good [-0.5, 0.5] + local D=nil + if LUE>3 then + D=underline("LUL") + elseif LUE>1 then + D="LUL" + elseif LUE>0.5 then + D=little("LUL") + elseif LUE<-3 then + D=underline("LUR") + elseif LUE<-1 then + D="LUR" + elseif LUE<-0.5 then + D=little("LUR") + end + + -- Compile. + local G="" + local n=0 + if S then + G=G..S + n=n+1 + end + if A then + G=G..A + n=n+1 + end + if D then + G=G..D + n=n+1 + end + + -- Add current step. + local step=self:_GS(step) + step=step:gsub("XX","X") + if G~="" then + G=G..step + end + + -- Debug info. + local text=string.format("LSO Grade at %s:\n", step) + text=text..string.format("AOA=%.1f\n",AOA) + text=text..string.format("GSE=%.1f\n",GSE) + text=text..string.format("LUE=%.1f\n",LUE) + text=text..string.format("ROL=%.1f\n",ROL) + text=text..G + self:T3(self.lid..text) + + return G,n +end + +--- Get short name of the grove step. +-- @param #AIRBOSS self +-- @param #number step Step +-- @return #string Shortcut name "X", "RB", "IM", "AR", "IW". +function AIRBOSS:_GS(step) + local gp + if step==AIRBOSS.PatternStep.FINAL then + gp="X0" -- Entering the groove. + elseif step==AIRBOSS.PatternStep.GROOVE_XX then + gp="X" -- Starting the groove. + elseif step==AIRBOSS.PatternStep.GROOVE_RB then + gp="RB" -- Roger ball call. + elseif step==AIRBOSS.PatternStep.GROOVE_IM then + gp="IM" -- In the middle. + elseif step==AIRBOSS.PatternStep.GROOVE_IC then + gp="IC" -- In close. + elseif step==AIRBOSS.PatternStep.GROOVE_AR then + gp="AR" -- At the ramp. + elseif step==AIRBOSS.PatternStep.GROOVE_IW then + gp="IW" -- In the wires. + end + return gp +end + +--- Check if a player is within the right area. +-- @param #AIRBOSS self +-- @param #number X X distance player to carrier. +-- @param #number Z Z distance player to carrier. +-- @param #AIRBOSS.Checkpoint pos Position data limits. +-- @return #boolean If true, approach should be aborted. +function AIRBOSS:_CheckAbort(X, Z, pos) + + local abort=false + if pos.Xmin and Xpos.Xmax then + self:T(string.format("Xmax: X=%d > %d=Xmax", X, pos.Xmax)) + abort=true + elseif pos.Zmin and Zpos.Zmax then + self:T(string.format("Zmax: Z=%d > %d=Zmax", Z, pos.Zmax)) + abort=true + end + + return abort +end + +--- Generate a text if a player is too far from where he should be. +-- @param #AIRBOSS self +-- @param #number X X distance player to carrier. +-- @param #number Z Z distance player to carrier. +-- @param #AIRBOSS.Checkpoint posData Checkpoint data. +function AIRBOSS:_TooFarOutText(X, Z, posData) + + -- Intro. + local text="you are too " + + -- X text. + local xtext=nil + if posData.Xmin and XposData.Xmax then + if posData.Xmax>=0 then + xtext="far ahead of " + else + xtext="close to " + end + end + + -- Z text. + local ztext=nil + if posData.Zmin and ZposData.Zmax then + if posData.Zmax>=0 then + ztext="far starboard of " + else + ztext="too close to " + end + end + + -- Combine X-Z text. + if xtext and ztext then + text=text..xtext.." and "..ztext + elseif xtext then + text=text..xtext + elseif ztext then + text=text..ztext + end + + -- Complete the sentence + text=text.."the carrier." + + -- If no case could be identified. + if xtext==nil and ztext==nil then + text="you are too far from where you should be!" + end + + return text +end + +--- Pattern aborted. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #number X X distance player to carrier. +-- @param #number Z Z distance player to carrier. +-- @param #AIRBOSS.Checkpoint posData Checkpoint data. +-- @param #boolean patternwo (Optional) Pattern wave off. +function AIRBOSS:_AbortPattern(playerData, X, Z, posData, patternwo) + + -- Text where we are wrong. + local text=self:_TooFarOutText(X, Z, posData) + + -- Debug. + local dtext=string.format("Abort: X=%d Xmin=%s, Xmax=%s | Z=%d Zmin=%s Zmax=%s", X, tostring(posData.Xmin), tostring(posData.Xmax), Z, tostring(posData.Zmin), tostring(posData.Zmax)) + self:E(self.lid..dtext) + + -- Message to player. + self:MessageToPlayer(playerData, text, "LSO", nil, 20) + + if patternwo then + + -- Pattern wave off! + playerData.patternwo=true + + -- Add to debrief. + self:_AddToDebrief(playerData, string.format("Pattern wave off: %s", text)) + + -- Depart and re-enter radio message. + -- TODO: Radio should depend on player step. + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.DEPARTANDREENTER, false, 3) + + -- Next step debrief. + playerData.step=AIRBOSS.PatternStep.DEBRIEF + playerData.warning=nil + end + +end + + +--- Get error margin depending on player skill. +-- +-- * Flight students: 10% and 20% +-- * Naval Aviators: 5% and 10% +-- * TOPGUN Graduates: 2.5% and 5% +-- +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @return #number Error margin for still being okay. +-- @return #number Error margin for really sucking. +function AIRBOSS:_GetGoodBadScore(playerData) + + local lowscore + local badscore + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + lowscore=10 + badscore=20 + elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then + lowscore=5 + badscore=10 + elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then + lowscore=2.5 + badscore=5 + end + + return lowscore, badscore +end + + +--- Evaluate player's altitude at checkpoint. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #number altopt Optimal altitude in meters. +-- @return #string Feedback text. +-- @return #string Debriefing text. +function AIRBOSS:_AltitudeCheck(playerData, altopt) + + if altopt==nil then + return nil, nil + end + + -- Player altitude. + local altitude=playerData.unit:GetAltitude() + + -- Get relative score. + local lowscore, badscore=self:_GetGoodBadScore(playerData) + + -- Altitude error +-X% + local _error=(altitude-altopt)/altopt*100 + + local radiocall={} --#AIRBOSS.RadioCall + + local hint + if _error>badscore then + hint=string.format("You're high.") + radiocall=AIRBOSS.LSOCall.HIGH + radiocall.loud=true + radiocall.subtitle="" + elseif _error>lowscore then + hint= string.format("You're slightly high.") + radiocall=AIRBOSS.LSOCall.HIGH + radiocall.loud=false + radiocall.subtitle="" + elseif _error<-badscore then + hint=string.format("You're low. ") + radiocall=AIRBOSS.LSOCall.LOW + radiocall.loud=true + radiocall.subtitle="" + elseif _error<-lowscore then + hint=string.format("You're slightly low.") + radiocall=AIRBOSS.LSOCall.LOW + radiocall.loud=false + radiocall.subtitle="" + else + hint=string.format("Good altitude.") + end + + -- Extend or decrease depending on skill. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + -- Also inform students about the optimal altitude. + hint=hint..string.format(" Optimal altitude is %d ft.", UTILS.MetersToFeet(altopt)) + elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then + -- We keep it short normally. + elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then + -- No hint at all for the pros. + hint="" + end + + -- Debrief text. + local debrief=string.format("Altitude %d ft = %d%% deviation from %d ft.", UTILS.MetersToFeet(altitude), _error, UTILS.MetersToFeet(altopt)) + + return hint, debrief +end + +--- Evaluate player's distance to the boat at checkpoint. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #number optdist Optimal distance in meters. +-- @return #string Feedback message text. +-- @return #string Debriefing text. +function AIRBOSS:_DistanceCheck(playerData, optdist) + + if optdist==nil then + return nil, nil + end + + -- Distance to carrier. + local distance=playerData.unit:GetCoordinate():Get2DDistance(self:GetCoordinate()) + + -- Get relative score. + local lowscore, badscore = self:_GetGoodBadScore(playerData) + + -- Altitude error +-X% + local _error=(distance-optdist)/optdist*100 + + local hint + if _error>badscore then + hint=string.format("You're too far from the boat!") + elseif _error>lowscore then + hint=string.format("You're slightly too far from the boat.") + elseif _error<-badscore then + hint=string.format( "You're too close to the boat!") + elseif _error<-lowscore then + hint=string.format("You're slightly too far from the boat.") + else + hint=string.format("Good distance to the boat.") + end + + -- Extend or decrease depending on skill. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + -- Also inform students about optimal value. + hint=hint..string.format(" Optimal distance is %.1f NM.", UTILS.MetersToNM(optdist)) + elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then + -- We keep it short normally. + elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then + -- No hint at all for the pros. + hint="" + end + + -- Debriefing text. + local debrief=string.format("Distance %.1f NM = %d%% deviation from %.1f NM.",UTILS.MetersToNM(distance), _error, UTILS.MetersToNM(optdist)) + + return hint, debrief +end + +--- Score for correct AoA. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #number optaoa Optimal AoA. +-- @return #string Feedback message text or easy and normal difficulty level or nil for hard. +-- @return #string Debriefing text. +function AIRBOSS:_AoACheck(playerData, optaoa) + + if optaoa==nil then + return nil, nil + end + + -- Get relative score. + local lowscore, badscore = self:_GetGoodBadScore(playerData) + + -- Player AoA + local aoa=playerData.unit:GetAoA() + + -- Altitude error +-X% + local _error=(aoa-optaoa)/optaoa*100 + + -- Get aircraft AoA parameters. + local aircraftaoa=self:_GetAircraftAoA(playerData) + + -- Rate aoa. + local hint="" + if aoa>=aircraftaoa.SLOW then + hint="Your're slow!" + elseif aoa>=aircraftaoa.Slow then + hint="Your're slow." + elseif aoa>=aircraftaoa.OnSpeedMax then + hint="Your're a little slow." + elseif aoa>=aircraftaoa.OnSpeedMin then + hint="You're on speed." + elseif aoa>=aircraftaoa.Fast then + hint="You're a little fast." + else + hint="You're fast!" + end + + -- Extend or decrease depending on skill. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + -- Also inform students about optimal value. + hint=hint..string.format(" Optimal AoA is %.1f.", optaoa) + elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then + -- We keep is short normally. + elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then + -- No hint at all for the pros. + hint="" + end + + -- Debriefing text. + local debrief=string.format("AoA %.1f = %d%% deviation from %.1f.", aoa, _error, optaoa) + + return hint, debrief +end + +--- Evaluate player's speed. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #number speedopt Optimal speed in m/s. +-- @return #string Feedback text. +-- @return #string Debriefing text. +function AIRBOSS:_SpeedCheck(playerData, speedopt) + + if speedopt==nil then + return nil, nil + end + + -- Player altitude. + local speed=playerData.unit:GetVelocityMPS() + + -- Get relative score. + local lowscore, badscore=self:_GetGoodBadScore(playerData) + + -- Altitude error +-X% + local _error=(speed-speedopt)/speedopt*100 + + local hint + if _error>badscore then + hint=string.format("You're fast.") + elseif _error>lowscore then + hint= string.format("You're slightly fast.") + elseif _error<-badscore then + hint=string.format("You're low.") + elseif _error<-lowscore then + hint=string.format("You're slightly slow.") + else + hint=string.format("Good speed.") + end + + -- Extend or decrease depending on skill. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + hint=hint..string.format(" Optimal speed is %d knots.", UTILS.MpsToKnots(speedopt)) + elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then + -- We keep is short normally. + elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then + -- No hint at all for pros. + hint="" + end + + -- Debrief text. + local debrief=string.format("Speed %d knots = %d%% deviation from %d knots.", UTILS.MpsToKnots(speed), _error, UTILS.MpsToKnots(speedopt)) + + return hint, debrief +end + +--- Append text to debriefing. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #string hint Debrief text of this step. +-- @param #string step (Optional) Current step in the pattern. Default from playerData. +function AIRBOSS:_AddToDebrief(playerData, hint, step) + step=step or playerData.step + table.insert(playerData.debrief, {step=step, hint=hint}) +end + +--- Debrief player and set next step. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_Debrief(playerData) + self:F2(self.lid..string.format("Debriefing of player %s.", playerData.name)) + + -- LSO grade, points, and flight data analyis. + local grade, points, analysis=self:_LSOgrade(playerData) + + -- My LSO grade. + local mygrade={} --#AIRBOSS.LSOgrade + mygrade.grade=grade + mygrade.points=points + mygrade.details=analysis + mygrade.wire=playerData.wire + mygrade.Tgroove=playerData.Tgroove + + -- Add LSO grade to table. + table.insert(playerData.grades, mygrade) + + -- LSO grade: (OK) 3.0 PT - LURIM + local text=string.format("%s %.1f PT - %s", grade, points, analysis) + + -- Wire and Groove time only if not pattern WO. + if not playerData.patternwo then + + -- Wire trapped. Not if pattern WI. + if playerData.wire and playerData.wire<=4 then + text=text..string.format(" %d-wire", playerData.wire) + end + + -- Time in the groove. Only Case I/II and not pattern WO. + if playerData.Tgroove and playerData.Tgroove<=60 and playerData.case<3 then + text=text..string.format("\nTime in the groove %d seconds: %s", playerData.Tgroove, self:_EvalGrooveTime(playerData)) + end + + end + + -- Copy debriefing text. + playerData.lastdebrief=UTILS.DeepCopy(playerData.debrief) + + -- Info text. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + text=text..string.format("\nYour detailed debriefing can be found via the F10 radio menu.") + end + + -- Message. + self:MessageToPlayer(playerData, text, "LSO", "", 30, true) + + + -- Set step to undefined and check. + playerData.step=AIRBOSS.PatternStep.UNDEFINED + + + -- Check what happened? + if playerData.patternwo then + + ---------------------- + -- Pattern Wave Off -- + ---------------------- + + -- Next step? + -- TODO: CASE I: After bolter/wo turn left and climb to 600 ft and re-enter the pattern. But do not go to initial but reenter earlier? + -- TODO: CASE I: After pattern wo? go back to initial, I guess? + -- TODO: CASE III: After bolter/wo turn left and climb to 1200 ft and re-enter pattern? + -- TODO: CASE III: After pattern wo? No idea... + + -- Can become nil when I crashed and changed to observer. Which events are captured? Nil check for unit? + + if playerData.unit:IsAlive() then + + -- Heading and distance tip. + local heading, distance + + if playerData.case==1 or playerData.case==2 then + + -- Next step: Initial again. + playerData.step=AIRBOSS.PatternStep.INITIAL + + -- Get heading and distance to initial zone ~3 NM astern. + heading=playerData.unit:GetCoordinate():HeadingTo(self.zoneInitial:GetCoordinate()) + distance=playerData.unit:GetCoordinate():Get2DDistance(self.zoneInitial:GetCoordinate()) + + elseif playerData.case==3 then + + -- Next step? Bullseye for now. + -- TODO: Could be DIRTY UP or PLATFORM or even back to MARSHAL STACK? + playerData.step=AIRBOSS.PatternStep.BULLSEYE + + -- Get heading and distance to bullseye zone ~3 NM astern. + local zone=self:_GetZoneBullseye(playerData.case) + + heading=playerData.unit:GetCoordinate():HeadingTo(zone:GetCoordinate()) + distance=playerData.unit:GetCoordinate():Get2DDistance(zone:GetCoordinate()) + + end + + -- Re-enter message. + local text=string.format("fly heading %d for %d NM to re-enter the pattern.", heading, UTILS.MetersToNM(distance)) + self:MessageToPlayer(playerData, text, "LSO", nil, 10, false, 5) + + else + + -- Unit does not seem to be alive! + -- TODO: What now? + self:T2(self.lid..string.format("Player unit not alive!")) + + end + + elseif playerData.waveoff then + + -------------- + -- Wave Off -- + -------------- + + if playerData.unit:InAir() then + + if playerData.case<3 then + + -- Next step: Abeam + playerData.step=AIRBOSS.PatternStep.ABEAM + + else + + -- Next step? Taking Bullseye for now. + playerData.step=AIRBOSS.PatternStep.BULLSEYE + + end + + else + + -- Airboss talkto! + local text=string.format("you were waved off but landed anyway. Airboss wants to talk to you!") + self:MessageToPlayer(playerData, text, "LSO", nil, 10, false, 2) + + -- Next step undefined. Player landed. + playerData.step=AIRBOSS.PatternStep.UNDEFINED + + end + + elseif playerData.boltered then + + -------------- + -- Boltered -- + -------------- + + if playerData.unit:InAir() then + + if playerData.case<3 then + + -- Next step: Abeam + playerData.step=AIRBOSS.PatternStep.ABEAM + + else + + -- Next step? Taking Bullseye for now. + playerData.step=AIRBOSS.PatternStep.BULLSEYE + + end + + else + + -- Next step undefined. Player is not in air any more. + playerData.step=AIRBOSS.PatternStep.UNDEFINED + + end + + + elseif playerData.landed then + + ------------ + -- Landed -- + ------------ + + if not playerData.unit:InAir() then + + -- Remove player unit from flight and all queues. + self:_RemoveUnitFromFlight(playerData.unit) + + -- Welcome aboard! + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.WELCOMEABOARD) + + end + + else + + -- Message to player. + self:MessageToPlayer(playerData, "Undefined state after landing! Please report.", "ERROR", nil, 10) + + -- Next step. + playerData.step=AIRBOSS.PatternStep.UNDEFINED + + end + + -- Increase number of passes. + playerData.passes=playerData.passes+1 + + -- Next step hint for students if any. + self:_StepHint(playerData) + + -- Reinitialize player data for new approach. + self:_InitPlayer(playerData, playerData.step) + + -- Debug message. + MESSAGE:New(string.format("Player step %s.", playerData.step), 5, "DEBUG"):ToAllIf(self.Debug) +end + +--- Hind for flight students about the (next) step. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #string step Step for which hint is given. +function AIRBOSS:_StepHint(playerData, step) + + -- Set step. + step=step or playerData.step + + -- Message is only for "Flight Students". + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + + -- Get optimal parameters at step. + local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData, step) + + -- Hint: + local hint="" + + -- Altitude. + if alt then + hint=hint..string.format("\nAltitude %d ft", UTILS.MetersToFeet(alt)) + end + + -- AoA. + if aoa then + hint=hint..string.format("\nAoA %.1f", aoa) + end + + -- Speed. + if speed then + hint=hint..string.format("\nSpeed %d knots", UTILS.MpsToKnots(speed)) + end + + -- Distance to the boat. + if dist then + hint=hint..string.format("\nDistance to the boat %.1f NM", UTILS.MetersToNM(dist)) + end + + -- Check if there was actually anything to tell. + if hint~="" then + + -- Compile text if any. + local text=string.format("Optimal setup at next step %s:%s", step, hint) + + -- Send hint to player. + self:MessageToPlayer(playerData, text, "AIRBOSS", "", 10, false, 2) + + end + + end +end + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- MISC functions +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get onboard number of player or client. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Aircraft group. +-- @return #string Onboard number as string. +function AIRBOSS:_GetOnboardNumberPlayer(group) + return self:_GetOnboardNumbers(group, true) +end + +--- Get onboard numbers of all units in a group. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Aircraft group. +-- @param #boolean playeronly If true, return the onboard number for player or client skill units. +-- @return #table Table of onboard numbers. +function AIRBOSS:_GetOnboardNumbers(group, playeronly) + --self:F({groupname=group:GetName}) + + -- Get group name. + local groupname=group:GetName() + + -- Debug text. + local text=string.format("Onboard numbers of group %s:", groupname) + + -- Units of template group. + local units=group:GetTemplate().units + + -- Get numbers. + local numbers={} + for _,unit in pairs(units) do + + -- Onboard number and unit name. + local n=tostring(unit.onboard_num) + local name=unit.name + local skill=unit.skill + + -- Debug text. + text=text..string.format("\n- unit %s: onboard #=%s skill=%s", name, n, skill) + + if playeronly and skill=="Client" or skill=="Player" then + -- There can be only one player in the group, so we skip everything else. + return n + end + + -- Table entry. + numbers[name]=n + end + + -- Debug info. + self:T2(self.lid..text) + + return numbers +end + +--- Check if aircraft is capable of landing on an aircraft carrier. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Aircraft unit. (Will also work with groups as given parameter.) +-- @return #boolean If true, aircraft can land on a carrier. +function AIRBOSS:_IsCarrierAircraft(unit) + local carrieraircraft=false + local aircrafttype=unit:GetTypeName() + for _,actype in pairs(AIRBOSS.AircraftCarrier) do + if actype==aircrafttype then + return true + end + end + return false +end + +--- Checks if a human player sits in the unit. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Aircraft unit. +-- @return #boolean If true, human player inside the unit. +function AIRBOSS:_IsHumanUnit(unit) + + -- Get player unit or nil if no player unit. + local playerunit=self:_GetPlayerUnitAndName(unit:GetName()) + + if playerunit then + return true + else + return false + end +end + +--- Checks if a group has a human player. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Aircraft group. +-- @return #boolean If true, human player inside group. +function AIRBOSS:_IsHuman(group) + + -- Get all units of the group. + local units=group:GetUnits() + + -- Loop over all units. + for _,_unit in pairs(units) do + -- Check if unit is human. + local human=self:_IsHumanUnit(_unit) + if human then + return true + end + end + + return false +end + +--- Get fuel state in pounds. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit The unit for which the mass is determined. +-- @return #number Fuel state in pounds. +function AIRBOSS:_GetFuelState(unit) + + -- Get relative fuel [0,1]. + local fuel=unit:GetFuel() + + -- Get max weight of fuel in kg. + local maxfuel=self:_GetUnitMasses(unit) + + -- Fuel state, i.e. what let's + local fuelstate=fuel*maxfuel + + -- Debug info. + self:T2(self.lid..string.format("Unit %s fuel state = %.1f kg = %.1f lbs", unit:GetName(), fuelstate, UTILS.kg2lbs(fuelstate))) + + return UTILS.kg2lbs(fuelstate) +end + +--- Convert altitude from meters to angels (thousands of feet). +-- @param #AIRBOSS self +-- @param alt Alitude in meters. +-- @return #number Altitude in Anglels = thousands of feet using math.floor(). +function AIRBOSS:_GetAngels(alt) + + local angels=math.floor(UTILS.MetersToFeet(alt)/1000) + + return angels +end + +--- Get unit masses especially fuel from DCS descriptor values. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit The unit for which the mass is determined. +-- @return #number Mass of fuel in kg. +-- @return #number Empty weight of unit in kg. +-- @return #number Max weight of unit in kg. +-- @return #number Max cargo weight in kg. +function AIRBOSS:_GetUnitMasses(unit) + + -- Get DCS descriptors table. + local Desc=unit:GetDesc() + + -- Mass of fuel in kg. + local massfuel=Desc.fuelMassMax or 0 + + -- Mass of empty unit in km. + local massempty=Desc.massEmpty or 0 + + -- Max weight of unit in kg. + local massmax=Desc.massMax or 0 + + -- Rest is cargo. + local masscargo=massmax-massfuel-massempty + + -- Debug info. + self:T2(self.lid..string.format("Unit %s mass fuel=%.1f kg, empty=%.1f kg, max=%.1f kg, cargo=%.1f kg", unit:GetName(), massfuel, massempty, massmax, masscargo)) + + return massfuel, massempty, massmax, masscargo +end + +--- Get player data from unit object +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Unit in question. +-- @return #AIRBOSS.PlayerData Player data or nil if not player with this name or unit exists. +function AIRBOSS:_GetPlayerDataUnit(unit) + if unit:IsAlive() then + local unitname=unit:GetName() + local playerunit,playername=self:_GetPlayerUnitAndName(unitname) + if playerunit and playername then + return self.players[playername] + end + end + return nil +end + + +--- Get player data from group object. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Group in question. +-- @return #AIRBOSS.PlayerData Player data or nil if not player with this name or unit exists. +function AIRBOSS:_GetPlayerDataGroup(group) + local units=group:GetUnits() + for _,unit in pairs(units) do + local playerdata=self:_GetPlayerDataUnit(unit) + if playerdata then + return playerdata + end + end + return nil +end + +--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +-- @return Wrapper.Unit#UNIT Unit of player or nil. +-- @return #string Name of the player or nil. +function AIRBOSS:_GetPlayerUnitAndName(_unitName) + self:F2(_unitName) + + if _unitName ~= nil then + + -- Get DCS unit from its name. + local DCSunit=Unit.getByName(_unitName) + + if DCSunit then + + local playername=DCSunit:getPlayerName() + local unit=UNIT:Find(DCSunit) + + self:T2({DCSunit=DCSunit, unit=unit, playername=playername}) + if DCSunit and unit and playername then + return unit, playername + end + + end + + end + + -- Return nil if we could not find a player. + return nil,nil +end + +--- Get carrier coalition. +-- @param #AIRBOSS self +-- @return #number Coalition side of carrier. +function AIRBOSS:GetCoalition() + return self.carrier:GetCoalition() +end + +--- Get carrier coordinate. +-- @param #AIRBOSS self +-- @return Core.Point#COORDINATE Carrier coordinate. +function AIRBOSS:GetCoordinate() + return self.carrier:GetCoordinate() +end + + +--- Get mission weather. +-- @param #AIRBOSS self +function AIRBOSS:_MissionWeather() + + -- Weather data from mission file. + local weather=env.mission.weather + + + --[[ + ["clouds"] = + { + ["thickness"] = 430, + ["density"] = 7, + ["base"] = 0, + ["iprecptns"] = 1, + }, -- end of ["clouds"] + ]] + local clouds=weather.clouds + + --[[ + ["fog"] = + { + ["thickness"] = 0, + ["visibility"] = 25, + }, -- end of ["fog"] + ]] + local fog=weather.fog + + -- Visibilty distance in meters. + local vis=weather.visibility.distance + +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- RADIO MESSAGE Functions +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Radio queue item. +-- @type AIRBOSS.Radioitem +-- @field #number Tplay Abs time when transmission should be played. +-- @field #number Tstarted Abs time when transmission began to play. +-- @field #number prio Priority 0-100. +-- @field #boolean isplaying Currently playing. +-- @field Core.Beacon#RADIO radio Radio object. +-- @field #AIRBOSS.RadioCall call Radio call. +-- @field #boolean loud If true, play loud version of file. + +--- Check radio queue for transmissions to be broadcasted. +-- @param #AIRBOSS self +-- @param #table radioqueue The radio queue. +-- @param #string name Name of the queue. +function AIRBOSS:_CheckRadioQueue(radioqueue, name) + + -- Check if queue is empty. + if #radioqueue==0 then + return + end + + -- Get current abs time. + local time=timer.getAbsTime() + + -- Sort results table wrt times they have already been engaged. + local function _sort(a, b) + return (a.Tplay < b.Tplay) or (a.Tplay==b.Tplay and a.prio < b.prio) + end + --table.sort(radioqueue, _sort) + + local playing=false + local next=nil --#AIRBOSS.Radioitem + local remove=nil + for i,_transmission in ipairs(radioqueue) do + local transmission=_transmission --#AIRBOSS.Radioitem + + -- Check if transmission time has passed. + if time>transmission.Tplay then + + -- Check if transmission is currently playing. + if transmission.isplaying then + + -- Check if transmission is finished. + if time>=transmission.Tstarted+transmission.call.duration then + + -- Transmission over. + transmission.isplaying=false + remove=i + --table.insert(remove, i) + + else -- still playing + + -- Transmission is still playing. + playing=true + + end + + else -- not playing yet + + -- Not playing ==> this will be next. + if next==nil then + next=transmission + end + + end + + else + + -- Transmission not due yet. + + end + end + + -- Found a new transmission. + if next~=nil and not playing then + self:RadioTransmit(next.radio, next.call, next.loud) + next.isplaying=true + next.Tstarted=time + end + + -- Remove completed calls from queue. + --for _,idx in pairs(remove) do + if remove then + table.remove(radioqueue, remove) + end + --end + +end + +--- Add Radio transmission to radio queue +-- @param #AIRBOSS self +-- @param Core.Radio#RADIO radio sending transmission. +-- @param #AIRBOSS.RadioCall call Radio sound files and subtitles. +-- @param #boolean loud If true, play loud sound file version. +-- @param #number delay Delay in seconds, before the message is broadcasted. +function AIRBOSS:RadioTransmission(radio, call, loud, delay) + self:F2({radio=radio, call=call, loud=loud, delay=delay}) + + -- Create a new radio transmission item. + local transmission={} --#AIRBOSS.Radioitem + + transmission.radio=radio + transmission.call=call + transmission.Tplay=timer.getAbsTime()+(delay or 0) + transmission.prio=50 + transmission.isplaying=false + transmission.Tstarted=nil + transmission.loud=loud and call.loud + + -- Add transmission to the right queue. + if radio:GetAlias()=="LSO" then + + table.insert(self.RQLSO, transmission) + + elseif radio:GetAlias()=="MARSHAL" then + + table.insert(self.RQMarshal, transmission) + + end +end + +--- Transmission radio message. +-- @param #AIRBOSS self +-- @param Core.Radio#RADIO radio sending transmission. +-- @param #AIRBOSS.RadioCall call Radio sound files and subtitles. +-- @param #boolean loud If true, play loud sound file version. +-- @param #number delay Delay in seconds, before the message is broadcasted. +function AIRBOSS:RadioTransmit(radio, call, loud, delay) + self:F2({radio=radio, call=call, loud=loud, delay=delay}) + + if (delay==nil) or (delay and delay==0) then + + -- Construct file name and subtitle. + local filename=call.file + local subtitle=call.subtitle + if loud then + if call.loud then + filename=filename.."_Loud" + end + if subtitle and subtitle~="" then + subtitle=subtitle.."!" + end + else + if subtitle and subtitle~="" then + subtitle=subtitle.."." + end + end + filename=filename.."."..(call.suffix or "ogg") + + -- New transmission. + radio:NewUnitTransmission(filename, call.subtitle, call.duration, radio.Frequency/1000000, radio.Modulation, false) + + -- Broadcast message. + radio:Broadcast(true) + + -- Workaround for the community A-4E-C as long as their radios are not functioning properly. + for _,_player in pairs(self.players) do + local playerData=_player --#AIRBOSS.PlayerData + if playerData.actype==AIRBOSS.AircraftCarrier.A4EC then + USERSOUND:New(filename):ToGroup(playerData.group) + end + end + + -- Message "Subtitle" to all players. + self:MessageToAll(subtitle, radio:GetAlias(), "", call.duration) + + else + + -- Scheduled transmission. + SCHEDULER:New(self, self.RadioTransmission, {radio, call, loud}, delay) + + end +end + +--- Send text message to player client. +-- Message format will be "SENDER: RECCEIVER, MESSAGE". +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #string message The message to send. +-- @param #string sender The person who sends the message or nil. +-- @param #string receiver The person who receives the message. Default player's onboard number. Set to "" for no receiver. +-- @param #number duration Display message duration. Default 10 seconds. +-- @param #boolean clear If true, clear screen from previous messages. +-- @param #number delay Delay in seconds, before the message is displayed. +-- @param #boolean soundoff If true, do not play boad number message. +function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration, clear, delay, soundoff) + + if playerData and message and message~="" then + + -- Default duration. + duration=duration or 10 + + -- Format message. + local text + if receiver and receiver=="" then + -- No (blank) receiver. + text=string.format("%s", message) + else + -- Default "receiver" is onboard number of player. + receiver=receiver or playerData.onboard + text=string.format("%s, %s", receiver, message) + end + self:T(self.lid..text) + + if delay and delay>0 then + -- Delayed call. + SCHEDULER:New(self, self.MessageToPlayer, {playerData, message, sender, receiver, duration, clear, 0, soundoff}, delay) + else + + if sender and not soundoff then + + if receiver=="99" then + + -- Radio message from LSO or MARSHAL to all. + if sender=="LSO" then + self:_Number2Radio(self.LSORadio, receiver, delay) + elseif sender=="MARSHAL" then + self:_Number2Radio(self.MarshalRadio, receiver, delay) + end + + elseif receiver==playerData.onboard then + + -- Sound only to player group. + if sender=="LSO" or sender=="MARSHAL" then + self:_Number2Sound(playerData, sender, receiver, delay) + end + + end + end + + -- Text message to player client. + if playerData.client then + MESSAGE:New(text, duration, sender, clear):ToClient(playerData.client) + end + + end + + end +end + +--- Send text message to all players in the CCA. +-- Message format will be "SENDER: RECCEIVER, MESSAGE". +-- @param #AIRBOSS self +-- @param #string message The message to send. +-- @param #string sender The person who sends the message or nil. +-- @param #string receiver The person who receives the message. Default player's onboard number. Set to "" for no receiver. +-- @param #number duration Display message duration. Default 10 seconds. +-- @param #boolean clear If true, clear screen from previous messages. +-- @param #number delay Delay in seconds, before the message is displayed. +-- @param #boolean soundoff If true, do not play boad number message. +function AIRBOSS:MessageToAll(message, sender, receiver, duration, clear, delay, soundoff) + + -- Make sure the onboard number sound is played only once. + local soundoff=false + + for _,_player in pairs(self.players) do + local playerData=_player --#AIRBOSS.PlayerData + + -- Message to all players in CCA. + if playerData.unit:IsInZone(self.zoneCCA) then + + -- Message to player. + self:MessageToPlayer(playerData, message, sender, receiver, duration, clear, delay, soundoff) + + -- Disable sound play of onboard number. + soundoff=true + end + end +end + + +--- Send text message to all players in the pattern queue. +-- Message format will be "SENDER: RECCEIVER, MESSAGE". +-- @param #AIRBOSS self +-- @param #string message The message to send. +-- @param #string sender The person who sends the message or nil. +-- @param #string receiver The person who receives the message. Default player's onboard number. Set to "" for no receiver. +-- @param #number duration Display message duration. Default 10 seconds. +-- @param #boolean clear If true, clear screen from previous messages. +-- @param #number delay Delay in seconds, before the message is displayed. +-- @param #boolean soundoff If true, do not play boad number message. +function AIRBOSS:MessageToPattern(message, sender, receiver, duration, clear, delay, soundoff) + + -- Make sure the onboard number sound is played only once. + local soundoff=false + + -- Loop over all flights in the pattern queue. + for _,_player in pairs(self.Qpattern) do + local playerData=_player --#AIRBOSS.PlayerData + + -- Message only to human pilots. + if not playerData.ai then + + -- Message to player. + self:MessageToPlayer(playerData, message, sender, receiver, duration, clear, delay, soundoff) + + -- Disable sound play of onboard number. + soundoff=true + end + end +end + +--- Send text message to all players in the marshal queue. +-- Message format will be "SENDER: RECCEIVER, MESSAGE". +-- @param #AIRBOSS self +-- @param #string message The message to send. +-- @param #string sender The person who sends the message or nil. +-- @param #string receiver The person who receives the message. Default player's onboard number. Set to "" for no receiver. +-- @param #number duration Display message duration. Default 10 seconds. +-- @param #boolean clear If true, clear screen from previous messages. +-- @param #number delay Delay in seconds, before the message is displayed. +-- @param #boolean soundoff If true, do not play boad number message. +function AIRBOSS:MessageToMarshal(message, sender, receiver, duration, clear, delay, soundoff) + + -- Make sure the onboard number sound is played only once. + local soundoff=false + + -- Loop over all flights in the marshal queue. + for _,_player in pairs(self.Qmarshal) do + local playerData=_player --#AIRBOSS.PlayerData + + -- Message only to human pilots. + if not playerData.ai then + + -- Message to player. + self:MessageToPlayer(playerData, message, sender, receiver, duration, clear, delay, soundoff) + + -- Disable sound play of onboard number. + soundoff=true + end + end +end + +--- Convert a number (as string) into an outsound and play it to a player group. E.g. for board number or headings. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #string sender Who is sending the call, either "LSO" or "MARSHAL". +-- @param #string number Number string, e.g. "032" or "183". +-- @param #number delay Delay before transmission in seconds. +function AIRBOSS:_Number2Sound(playerData, sender, number, delay) + + --- Split string into characters. + local function _split(str) + local chars={} + for i=1,#str do + local c=str:sub(i,i) + table.insert(chars, c) + end + return chars + end + + if delay and delay>0 then + -- Delayed call. + SCHEDULER:New(self, AIRBOSS._Number2Sound, {playerData, sender, number}, delay) + else + + -- Split string into characters. + local numbers=_split(number) + + local Sender + if sender=="LSO" then + Sender="LSOCall" + elseif sender=="MARSHAL" then + Sender="MarshalCall" + else + self:E(self.lid..string.format("ERROR: Unknown radio sender %s!", tostring(sender))) + return + end + + local wait=0 + for i=1,#numbers do + + -- Current number + local n=numbers[i] + + -- Convert to N0, N1, ... + local N=string.format("N%s", n) + + -- Radio call. + local call=AIRBOSS[Sender][N] --#AIRBOSS.RadioCall + + -- Create file name. + local filename=string.format("%s.%s", call.file, call.suffix) + + -- Play sound. + USERSOUND:New(filename):ToGroup(playerData.group, wait) + + -- Wait until this call is over before playing the next. + wait=wait+call.duration + end + + end +end + +--- Convert a number (as string) into a radio message. +-- E.g. for board number or headings. +-- @param #AIRBOSS self +-- @param Core.Radio#RADIO radio Radio used for transmission. +-- @param #string number Number string, e.g. "032" or "183". +-- @param #number delay Delay before transmission in seconds. +function AIRBOSS:_Number2Radio(radio, number, delay) + + --- Split string into characters. + local function _split(str) + local chars={} + for i=1,#str do + local c=str:sub(i,i) + table.insert(chars, c) + end + return chars + end + + -- Get radio alias. + local alias=radio:GetAlias() + + local sender="" + if alias=="LSO" then + sender="LSOCall" + elseif alias=="MARSHAL" then + sender="MarshalCall" + else + self:E(self.lid.."ERROR: Unknown radio alias!") + end + + -- Split string into characters. + local numbers=_split(number) + + for i=1,#numbers do + + -- Current number + local n=numbers[i] + + if n=="0" then + self:RadioTransmission(radio, AIRBOSS[sender].N0, false, delay) + elseif n=="1" then + self:RadioTransmission(radio, AIRBOSS[sender].N1, false, delay) + elseif n=="2" then + self:RadioTransmission(radio, AIRBOSS[sender].N2, false, delay) + elseif n=="3" then + self:RadioTransmission(radio, AIRBOSS[sender].N3, false, delay) + elseif n=="4" then + self:RadioTransmission(radio, AIRBOSS[sender].N4, false, delay) + elseif n=="5" then + self:RadioTransmission(radio, AIRBOSS[sender].N5, false, delay) + elseif n=="6" then + self:RadioTransmission(radio, AIRBOSS[sender].N6, false, delay) + elseif n=="7" then + self:RadioTransmission(radio, AIRBOSS[sender].N7, false, delay) + elseif n=="8" then + self:RadioTransmission(radio, AIRBOSS[sender].N8, false, delay) + elseif n=="9" then + self:RadioTransmission(radio, AIRBOSS[sender].N9, false, delay) + else + self:E(self.lid..string.format("ERROR: Unknown number %s!", tostring(n))) + end + end + +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- RADIO MENU Functions +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add menu commands for player. +-- @param #AIRBOSS self +-- @param #string _unitName Name of player unit. +function AIRBOSS:_AddF10Commands(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check for player unit. + if _unit and playername then + + -- Get group and ID. + local group=_unit:GetGroup() + local gid=group:GetID() + + if group and gid then + + if not self.menuadded[gid] then + + -- Enable switch so we don't do this twice. + self.menuadded[gid]=true + + -- Main F10 menu: F10/Airboss// + if AIRBOSS.MenuF10[gid]==nil then + AIRBOSS.MenuF10[gid]=missionCommands.addSubMenuForGroup(gid, "Airboss") + end + + -- F10/Airboss/ + local _rootPath=missionCommands.addSubMenuForGroup(gid, self.alias, AIRBOSS.MenuF10[gid]) + + -------------------------------- + -- F10/Airboss//F1 Help + -------------------------------- + local _helpPath=missionCommands.addSubMenuForGroup(gid, "Help", _rootPath) + -- F10/Airboss//F1 Help/F1 Mark Zones + local _markPath=missionCommands.addSubMenuForGroup(gid, "Mark Zones", _helpPath) + -- F10/Airboss//F1 Help/F1 Mark Zones/ + missionCommands.addCommandForGroup(gid, "Smoke Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, false) -- F1 + missionCommands.addCommandForGroup(gid, "Flare Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, true) -- F2 + missionCommands.addCommandForGroup(gid, "Smoke Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, false) -- F3 + missionCommands.addCommandForGroup(gid, "Flare Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, true) -- F4 + -- F10/Airboss//F1 Help/F2 Skill Level + local _skillPath=missionCommands.addSubMenuForGroup(gid, "Skill Level", _helpPath) + -- F10/Airboss//F1 Help/F2 Skill Level/ + missionCommands.addCommandForGroup(gid, "Flight Student", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.EASY) -- F1 + missionCommands.addCommandForGroup(gid, "Naval Aviator", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.NORMAL) -- F2 + missionCommands.addCommandForGroup(gid, "TOPGUN Graduate", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.HARD) -- F3 + -- F10/Airboss//F1 Help/ + missionCommands.addCommandForGroup(gid, "My Status", _helpPath, self._DisplayPlayerStatus, self, _unitName) -- F3 + missionCommands.addCommandForGroup(gid, "Attitude Monitor", _helpPath, self._AttitudeMonitor, self, playername) -- F4 + missionCommands.addCommandForGroup(gid, "Radio Check LSO", _helpPath, self._LSORadioCheck, self, _unitName) -- F5 + missionCommands.addCommandForGroup(gid, "Radio Check Marshal", _helpPath, self._MarshalRadioCheck, self, _unitName) -- F6 + missionCommands.addCommandForGroup(gid, "[Reset My Status]", _helpPath, self._ResetPlayerStatus, self, _unitName) -- F7 + + ------------------------------------- + -- F10/Airboss//F2 Kneeboard + ------------------------------------- + local _kneeboardPath=missionCommands.addSubMenuForGroup(gid, "Kneeboard", _rootPath) + -- F10/Airboss//F2 Kneeboard/F1 Results + local _resultsPath=missionCommands.addSubMenuForGroup(gid, "Results", _kneeboardPath) + -- F10/Airboss//F2 Kneeboard/F1 Results/ + missionCommands.addCommandForGroup(gid, "Greenie Board", _resultsPath, self._DisplayScoreBoard, self, _unitName) -- F1 + missionCommands.addCommandForGroup(gid, "My LSO Grades", _resultsPath, self._DisplayPlayerGrades, self, _unitName) -- F2 + missionCommands.addCommandForGroup(gid, "Last Debrief", _resultsPath, self._DisplayDebriefing, self, _unitName) -- F3 + -- F10/Airboss// + ------------------------- + missionCommands.addCommandForGroup(gid, "Request Marshal", _rootPath, self._RequestMarshal, self, _unitName) -- F3 + missionCommands.addCommandForGroup(gid, "Request Commence", _rootPath, self._RequestCommence, self, _unitName) -- F4 + missionCommands.addCommandForGroup(gid, "Request Refueling", _rootPath, self._RequestRefueling, self, _unitName) -- F5 + end + else + self:E(self.lid..string.format("ERROR: Could not find group or group ID in AddF10Menu() function. Unit name: %s.", _unitName)) + end + else + self:E(self.lid..string.format("ERROR: Player unit does not exist in AddF10Menu() function. Unit name: %s.", _unitName)) + end + +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ROOT MENU +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Reset player status. Player is removed from all queues and its status is set to undefined. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_ResetPlayerStatus(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Inform player. + local text="Status reset executed! You have been removed from all queues." + self:MessageToPlayer(playerData, text, nil, "") + + -- Remove from marhal stack can collapse stack if necessary. + if self:_InQueue(self.Qmarshal, playerData.group) then + self:_CollapseMarshalStack(playerData, true) + end + + -- Remove flight from queues. + self:_RemoveFlight(playerData) + + -- Initialize player data. + self:_InitPlayer(playerData) + + end + end +end + +--- LSO radio check. Will broadcase LSO message at given LSO frequency. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_LSORadioCheck(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + if playerData then + -- Broadcase LSO radio check message on LSO radio. + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.RADIOCHECK) + end + end +end + +--- Marshal radio check. Will broadcase Marshal message at given Marshal frequency. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_MarshalRadioCheck(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + if playerData then + -- Broadcase Marshal radio check message on Marshal radio. + self:RadioTransmission(self.MarshalRadio, AIRBOSS.MarshalCall.RADIOCHECK) + end + end +end + +--- Request marshal. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_RequestMarshal(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Check if player is in CCA + local inCCA=playerData.unit:IsInZone(self.zoneCCA) + + if inCCA then + + if self:_InQueue(self.Qmarshal, playerData.group) then + + -- Flight group is already in marhal queue. + local text=string.format("you are already in the Marshal queue. New marshal request denied!") + self:MessageToPlayer(playerData, text, "MARSHAL") + + elseif self:_InQueue(self.Qpattern, playerData.group) then + + -- Flight group is already in pattern queue. + local text=string.format("you are already in the Pattern queue. Marshal request denied!") + self:MessageToPlayer(playerData, text, "MARSHAL") + + elseif not _unit:InAir() then + + -- Flight group is already in pattern queue. + local text=string.format("you are not airborne. Marshal request denied!") + self:MessageToPlayer(playerData, text, "MARSHAL") + + else + + -- Add flight to marshal stack. + self:_MarshalPlayer(playerData) + + end + + else + + -- Flight group is not in CCA yet. + local text=string.format("you are not inside CCA yet. Marshal request denied!") + self:MessageToPlayer(playerData, text, "MARSHAL") + + end + end + end +end + +--- Request to commence approach. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_RequestCommence(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Check if unit is in CCA. + local text="" + if _unit:IsInZone(self.zoneCCA) then + + if self:_InQueue(self.Qpattern, playerData.group) then + + -- Flight group is already in pattern queue. + text=string.format("%s, you are already in the Pattern queue. Commence request denied!", playerData.name) + + elseif not _unit:InAir() then + + -- Flight group is already in pattern queue. + text=string.format("%s, you are not airborne. Commence request denied!", playerData.name) + + else + + -- Get stack value. + local stack=playerData.flag:Get() + + -- Check if player is in the lowest stack. + if stack>1 then + -- We are in a higher stack. + text="Negative ghostrider, it's not your turn yet!" + else + + -- Number of aircraft currently in pattern. + local _,npattern=self:_GetQueueInfo(self.Qpattern) + + -- Check if pattern is already full. + if npattern>=self.Nmaxpattern then + + -- Patern is full! + text=string.format("Negative ghostrider, pattern is full!\nThere are %d aircraft currently in the pattern.", npattern) + + else + + -- TODO: check if recovery window is open. + if not self:IsRecovering() then + text="Recovery window NOT open yet! However, you are cleared anyway.\n" + end + + -- Positive response. + if playerData.case==1 then + text=text.."Proceed to initial." + else + text=text.."Descent at 4k ft/min to platform at 5000 ft." + end + + -- Set player step. + playerData.step=AIRBOSS.PatternStep.COMMENCING + playerData.warning=nil + + -- Collaps marshal stack. + self:_CollapseMarshalStack(playerData, false) + end + + end + + end + else + -- This flight is not yet registered! + text="Negative ghostrider, you are not inside the CCA yet!" + end + + -- Debug + self:T(self.lid..text) + + -- Send message. + self:MessageToPlayer(playerData, text, "MARSHAL") + end + end +end + +--- Player requests refueling. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +function AIRBOSS:_RequestRefueling(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Check if there is a recovery tanker defined. + local text + if self.tanker then + + -- Check if player is in CCA. + if _unit:IsInZone(self.zoneCCA) then + + -- Check if tanker is running or refueling or returning. + if self.tanker:IsRunning() or self.tanker:IsRefueling() then + + -- Get alt of tanker in angels. + local angels=UTILS.Round(UTILS.MetersToFeet(self.tanker.altitude)/1000, 0) + + -- Tanker is up and running. + text=string.format("Proceed to tanker at angels %d.", angels) + + -- State TACAN channel of tanker if defined. + if self.tanker.TACANon then + text=text..string.format("\nTanker TACAN channel %d%s (%s)", self.tanker.TACANchannel, self.tanker.TACANmode, self.tanker.TACANmorse) + end + + -- Tanker is currently refueling. Inform player. + if self.tanker:IsRefueling() then + text=text.."\nTanker is currently refueling. You might have to queue up." + end + + -- Collapse marshal stack if player is in queue. + if self:_InQueue(self.Qmarshal, playerData.group) then + -- TODO: What if only the player and not his section wants to refuel?! + self:_CollapseMarshalStack(playerData, true) + end + elseif self.tanker:IsReturning() then + -- Tanker is RTB. + text="Tanker is RTB. Request denied!\nWait for the tanker to be back on station if you can." + end + + else + text="You are not registered inside the CCA yet. Request denied!" + end + else + text="No refueling tanker available. Request denied!" + end + + -- Send message. + self:MessageToPlayer(playerData, text, "MARSHAL") + end + end +end + +--- Set all flights within 200 meters to be part of my section. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +function AIRBOSS:_SetSection(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Coordinate of flight lead. + local mycoord=_unit:GetCoordinate() + + -- Check if player is in Marshal or pattern queue already. + local text + if self:_InQueue(self.Qmarshal,playerData.group) then + text=string.format("You are already in the Marshal queue. Setting section not possible any more!") + elseif self:_InQueue(self.Qpattern, playerData.group) then + text=string.format("You are already in the Pattern queue. Setting section not possible any more!") + else + + -- Init array + playerData.section={} + + -- Loop over all registered flights. + for _,_flight in pairs(self.flights) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Only human flight groups excluding myself. + if flight.ai==false and flight.groupname~=playerData.groupname then + + -- Distance to other group. + local distance=flight.group:GetCoordinate():Get2DDistance(mycoord) + + if distance<200 then + table.insert(playerData.section, flight) + end + + end + end + + -- Info on section members. + if #playerData.section>0 then + text=string.format("Registered flight section:") + text=text..string.format("- %s (lead)", playerData.name) + for _,_flight in paris(playerData.section) do + local flight=_flight --#AIRBOSS.PlayerData + text=text..string.format("- %s", flight.name) + flight.seclead=playerData.name + + -- Inform player that he is now part of a section. + self:MessageToPlayer(flight, string.format("Your section lead is now %s.", playerData.name), "MARSHAL") + end + else + text="No other human flights found within radius of 200 meters!" + end + end + + -- Message to section lead. + self:MessageToPlayer(playerData, text, "MARSHAL") + end + end + +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- RESULTS MENU +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Display top 10 player scores. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_DisplayScoreBoard(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + + -- Results table. + local _playerResults={} + + -- Player data of requestor. + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + -- Message text. + local text = string.format("Greenie Board:") + + for _playerName,_playerData in pairs(self.players) do + + local Paverage=0 + for _,_grade in pairs(_playerData.grades) do + Paverage=Paverage+_grade.points + end + _playerResults[_playerName]=Paverage + + end + + --Sort list! + local _sort=function(a, b) return a>b end + table.sort(_playerResults,_sort) + + local i=1 + for _playerName,_points in pairs(_playerResults) do + text=text..string.format("\n[%d] %.1f %s", i,_points,_playerName) + i=i+1 + end + + -- Send message. + if playerData.client then + MESSAGE:New(text, 30, nil, true):ToClient(playerData.client) + end + + end +end + +--- Display top 10 player scores. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_DisplayPlayerGrades(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Grades of player: + local text=string.format("Your grades, %s:", _playername) + + local p=0 + for i,_grade in pairs(playerData.grades) do + local grade=_grade --#AIRBOSS.LSOgrade + + text=text..string.format("\n[%d] %s %.1f PT - %s", i, grade.grade, grade.points, grade.details) + + -- Wire trapped if any. + if grade.wire and grade.wire<=4 then + text=text..string.format(" %d-wire", grade.wire) + end + + -- Time in the groove if any. + if grade.Tgroove and grade.Tgroove<=60 then + text=text..string.format(" Tgroove=%.1f s", grade.Tgroove) + end + + -- Add up points. + p=p+grade.points + end + + -- Number of grades. + local n=#playerData.grades + + if n>0 then + text=text..string.format("\nAverage points = %.1f", p/n) + else + text=text..string.format("\nNo data available.") + end + + -- Send message. + if playerData.client then + MESSAGE:New(text, 30, nil, true):ToClient(playerData.client) + end + end + end +end + +--- Display last debriefing. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_DisplayDebriefing(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Debriefing text. + local text=string.format("Debriefing:") + + -- Check if data is present. + if #playerData.lastdebrief>0 then + text=text..string.format("\n================================\n") + for _,_data in pairs(playerData.lastdebrief) do + local step=_data.step + local comment=_data.hint + text=text..string.format("* %s:\n",step) + text=text..string.format("%s\n", comment) + end + else + text=text.." Nothing to show yet." + end + + -- Send debrief message to player + self:MessageToPlayer(playerData, text, nil , "", 30, true) + + end + end +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- SKIL LEVEL MENU +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set difficulty level. +-- @param #AIRBOSS self +-- @param #string playername Player name. +-- @param #AIRBOSS.Difficulty difficulty Difficulty level. +function AIRBOSS:_SetDifficulty(playername, difficulty) + self:T2({difficulty=difficulty, playername=playername}) + + local playerData=self.players[playername] --#AIRBOSS.PlayerData + + if playerData then + playerData.difficulty=difficulty + local text=string.format("your difficulty level is now: %s.", difficulty) + self:MessageToPlayer(playerData, text, nil, playerData.name, 5) + else + self:E(self.lid..string.format("ERROR: Could not get player data for player %s.", playername)) + end +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- KNEEBOARD MENU +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Turn player's aircraft attitude display on or off. +-- @param #AIRBOSS self +-- @param #string playername Player name. +function AIRBOSS:_AttitudeMonitor(playername) + self:F2({playername=playername}) + + local playerData=self.players[playername] --#AIRBOSS.PlayerData + + if playerData then + playerData.attitudemonitor=not playerData.attitudemonitor + end +end + + +--- Report information about carrier. +-- @param #AIRBOSS self +-- @param #string _unitname Name of the player unit. +function AIRBOSS:_DisplayCarrierInfo(_unitname) + self:F2(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Current coordinates. + local coord=self:GetCoordinate() + + -- Carrier speed and heading. + local carrierheading=self.carrier:GetHeading() + local carrierspeed=UTILS.MpsToKnots(self.carrier:GetVelocityMPS()) + + -- Tacan/ICLS. + local tacan="unknown" + local icls="unknown" + if self.TACANon and self.TACANchannel~=nil then + tacan=string.format("%d%s (%s)", self.TACANchannel, self.TACANmode, self.TACANmorse) + end + if self.ICLSon and self.ICLSchannel~=nil then + icls=string.format("%d (%s)", self.ICLSchannel, self.ICLSmorse) + end + + -- Get groups, units in queues. + local Nmarshal,nmarshal=self:_GetQueueInfo(self.Qmarshal, playerData.case) + local Npattern,npattern=self:_GetQueueInfo(self.Qpattern) + + -- Current abs time. + local Tabs=timer.getAbsTime() + + -- Get recovery times of carrier. + local recoverytext="Recovery time windows (max 5):" + if #self.recoverytimes==0 then + recoverytext=recoverytext.." none." + else + -- Loop over recovery windows. + local rw=0 + for _,_recovery in pairs(self.recoverytimes) do + local recovery=_recovery --#AIRBOSS.Recovery + -- Only include current and future recovery windows. + if Tabs=5 then + -- Break the loop after 5 recovery times. + break + end + end + end + end + + -- Message text. + local text=string.format("%s info:\n", self.alias) + text=text..string.format("=============================================\n") + text=text..string.format("Carrier state %s\n", self:GetState()) + text=text..string.format("Case %d recovery\n", self.case) + text=text..string.format("BRC %03d°\n", self:GetBRC()) + text=text..string.format("FB %03d°\n", self:GetFinalBearing(true)) + text=text..string.format("Speed %d kts\n", carrierspeed) + text=text..string.format("Marshal radio %.3f MHz\n", self.MarshalFreq) + text=text..string.format("LSO radio %.3f MHz\n", self.LSOFreq) + text=text..string.format("TACAN Channel %s\n", tacan) + text=text..string.format("ICLS Channel %s\n", icls) + text=text..string.format("# A/C total %d\n", #self.flights) + text=text..string.format("# A/C marshal %d (%d)\n", Nmarshal, nmarshal) + text=text..string.format("# A/C pattern %d (%d)\n", Npattern, npattern) + text=text..string.format(recoverytext) + self:T2(self.lid..text) + + -- Send message. + self:MessageToPlayer(playerData, text, nil, "", 20, true) + + else + self:E(self.lid..string.format("ERROR: Could not get player data for player %s.", playername)) + end + end + +end + + +--- Report weather conditions at the carrier location. Temperature, QFE pressure and wind data. +-- @param #AIRBOSS self +-- @param #string _unitname Name of the player unit. +function AIRBOSS:_DisplayCarrierWeather(_unitname) + self:F2(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Message text. + local text="" + + -- Current coordinates. + local coord=self:GetCoordinate() + + -- Get atmospheric data at carrier location. + local T=coord:GetTemperature() + local P=coord:GetPressure() + local Wd,Ws=coord:GetWind() + + -- Get Beaufort wind scale. + local Bn,Bd=UTILS.BeaufortScale(Ws) + + local WD=string.format('%03d°', Wd) + local Ts=string.format("%d°C",T) + + local settings=_DATABASE:GetPlayerSettings(playername) or _SETTINGS --Core.Settings#SETTINGS + + local tT=string.format("%d°C",T) + local tW=string.format("%.1f m/s", Ws) + local tP=string.format("%.1f mmHg", UTILS.hPa2mmHg(P)) + if settings:IsImperial() then + tT=string.format("%d°F", UTILS.CelciusToFarenheit(T)) + tW=string.format("%.1f knots", UTILS.MpsToKnots(Ws)) + tP=string.format("%.2f inHg", UTILS.hPa2inHg(P)) + end + + -- Report text. + text=text..string.format("Weather Report at Carrier %s:\n", self.alias) + text=text..string.format("=============================================\n") + text=text..string.format("Temperature %s\n", tT) + text=text..string.format("Wind from %s at %s (%s)\n", WD, tW, Bd) + text=text..string.format("QFE %.1f hPa = %s", P, tP) + + -- Debug output. + self:T2(self.lid..text) + + -- Send message to player group. + self:MessageToPlayer(self.players[playername], text, nil, "", 30, true) + + else + self:E(self.lid..string.format("ERROR! Could not find player unit in CarrierWeather! Unit name = %s", _unitname)) + end +end + + + +--- Display player status. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +function AIRBOSS:_DisplayPlayerStatus(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Stack and stack altitude. + local stack=playerData.flag:Get() + local stackalt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack)) + + -- Fuel and fuel state. + local fuel=playerData.unit:GetFuel()*100 + local fuelstate=self:_GetFuelState(playerData.unit) + + -- Player data. + local text=string.format("Status of player %s (%s)\n", playerData.name, playerData.callsign) + text=text..string.format("=============================================\n") + text=text..string.format("Current step: %s\n", playerData.step) + text=text..string.format("Skil level: %s\n", playerData.difficulty) + text=text..string.format("Aircraft: %s\n", playerData.actype) + text=text..string.format("Board number: %s\n", playerData.onboard) + text=text..string.format("Fuel state: %.1f lbs/1000 (%.1f %%)\n", fuelstate/1000, fuel) + text=text..string.format("Stack: %d alt=%d ft\n", stack, stackalt) + text=text..string.format("Group: %s\n", playerData.group:GetName()) + text=text..string.format("# units: %d (n=%d)\n", #playerData.group:GetUnits(), playerData.nunits) + text=text..string.format("Section Lead: %s\n", tostring(playerData.seclead)) + text=text..string.format("# section: %d", #playerData.section) + for _,_sec in pairs(playerData.section) do + local sec=_sec --#AIRBOSS.PlayerData + text=text..string.format("\n- %s", sec.name) + end + + if playerData.step==AIRBOSS.PatternStep.INITIAL then + + -- Heading and distance to initial zone. + local flyhdg=playerData.unit:GetCoordinate():HeadingTo(self.zoneInitial:GetCoordinate()) + local flydist=UTILS.MetersToNM(playerData.unit:GetCoordinate():Get2DDistance(self.zoneInitial:GetCoordinate())) + local brc=self:GetBRC() + + -- Help player to find its way to the initial zone. + text=text..string.format("\nFly heading %03d° for %.1f NM and turn to BRC %03d°.", flyhdg, flydist, brc) + + elseif playerData.step==AIRBOSS.PatternStep.PLATFORM then + + -- Heading and distance to platform zone. + local flyhdg=playerData.unit:GetCoordinate():HeadingTo(self:_GetZonePlatform(playerData.case):GetCoordinate()) + local flydist=UTILS.MetersToNM(playerData.unit:GetCoordinate():Get2DDistance(self.zoneInitial:GetCoordinate())) + local fb=self:GetFinalBearing(true) + + -- Help player to find its way to the initial zone. + text=text..string.format("\nFly heading %03d° for %.1f NM and turn to FB %03d°.", flyhdg, flydist, fb) + + end + + -- Send message. + self:MessageToPlayer(playerData, text, nil, "", 30, true) + end + end + +end + +--- Mark current marshal zone of player by either smoke or flares. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +-- @param #boolean flare If true, flare the zone. If false, smoke the zone. +function AIRBOSS:_MarkMarshalZone(_unitName, flare) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Get player stack and recovery case. + local stack=playerData.flag:Get() + local case=playerData.case + + local text="" + if stack>0 then + + -- Get current holding zone. + local zone=self:_GetZoneHolding(case, stack) + + -- Pattern alitude. + local patternalt=self:_GetMarshalAltitude(stack, case) + + patternalt=0 + + if flare then + text="Marking marshal zone with WHITE flares." + zone:FlareZone(FLARECOLOR.White, 45, nil, patternalt) + else + text="Marking marshal zone with WHITE smoke." + zone:SmokeZone(SMOKECOLOR.White, 45, patternalt) + end + + else + text="You are currently not in a marshal stack. No zone to mark!" + end + + -- Send message to player. + self:MessageToPlayer(playerData, text, "MARSHAL", "") + end + end + +end + + +--- Mark CASE I or II/II zones by either smoke or flares. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +-- @param #boolean flare If true, flare the zone. If false, smoke the zone. +function AIRBOSS:_MarkCaseZones(_unitName, flare) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Player's recovery case. + local case=playerData.case + + -- Initial + local text=string.format("Marking CASE %d zones\n", case) + + -- Flare or smoke? + if flare then + + -- Case I/II: Initial + if case==1 or case==2 then + text=text.."* initial with WHITE flares\n" + self.zoneInitial:FlareZone(FLARECOLOR.White, 45) + end + + -- Case II/III: approach corridor + if case==2 or case==3 then + text=text.."* approach corridor with GREEN flares\n" + self:_GetZoneCorridor(case):FlareZone(FLARECOLOR.Green, 45) + end + + -- Case II/III: platform + if case==2 or case==3 then + text=text.."* platform with RED flares\n" + self:_GetZonePlatform(case):FlareZone(FLARECOLOR.Red, 45) + end + + -- Case III: dirty up + if case==3 then + text=text.."* dirty up with YELLOW flares\n" + self:_GetZoneDirtyUp(case):FlareZone(FLARECOLOR.Yellow, 45) + end + + -- Case II/III: arc in/out + if case==2 or case==3 then + if math.abs(self.holdingoffset)>0 then + self:_GetZoneArcIn(case):FlareZone(FLARECOLOR.Yellow, 45) + text=text.."* arc turn in with YELLOW flares\n" + self:_GetZoneArcOut(case):FlareZone(FLARECOLOR.White, 45) + text=text.."* arc trun out with WHITE flares\n" + end + end + + -- Case III: bullseye + if case==3 then + text=text.."* bullseye with WHITE flares\n" + self:_GetZoneBullseye(case):FlareZone(FLARECOLOR.White, 45) + end + + else + + -- Case I/II: Initial + if case==1 or case==2 then + text=text.."* initial with WHITE smoke\n" + self.zoneInitial:SmokeZone(SMOKECOLOR.White, 45) + end + + -- Case II/III: Approach Corridor + if case==2 or case==3 then + text=text.."* approach corridor with GREEN smoke\n" + self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green, 45) + end + + -- Case II/III: platform + if case==2 or case==3 then + text=text.."* platform with RED smoke\n" + self:_GetZonePlatform(case):SmokeZone(SMOKECOLOR.Red, 45) + end + + -- Case II/III: arc in/out if offset>0. + if case==2 or case==3 then + if math.abs(self.holdingoffset)>0 then + self:_GetZoneArcIn(case):SmokeZone(SMOKECOLOR.Blue, 45) + text=text.."* arc turn in with BLUE smoke\n" + self:_GetZoneArcOut(case):SmokeZone(SMOKECOLOR.Blue, 45) + text=text.."* arc trun out with BLUE smoke\n" + end + end + + -- Case III: dirty up + if case==3 then + text=text.."* dirty up with ORANGE smoke\n" + self:_GetZoneDirtyUp(case):SmokeZone(SMOKECOLOR.Orange, 45) + end + + -- Case III: bullseye + if case==3 then + text=text.."* bullseye with WHITE smoke\n" + self:_GetZoneBullseye(case):SmokeZone(SMOKECOLOR.White, 45) + end + + end + + -- Send message to player. + self:MessageToPlayer(playerData, text, "MARSHAL", "") + end + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua new file mode 100644 index 000000000..cdec7ebad --- /dev/null +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -0,0 +1,1279 @@ +--- **Ops** - (R2.5) - Recovery tanker for carrier operations. +-- +-- Tanker aircraft flying a racetrack pattern overhead an aircraft carrier. +-- +-- **Main Features:** +-- +-- * Regular pattern update with respect to carrier positon. +-- * No restrictions regarding carrier waypoints and heading. +-- * Automatic respawning when tanker runs out of fuel for 24/7 operations. +-- * Tanker can be spawned cold or hot on the carrier or at any other airbase or directly in air. +-- * Automatic AA TACAN beacon setting. +-- * Multiple tankers at different carriers due to object oriented approach. +-- * Finite State Machine (FSM) implementation, which allows the mission designer to hook into certain events. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- ### Special thanks to **HighwaymanEd** for testing and suggesting improvements! +-- +-- @module Ops.RecoveryTanker +-- @image MOOSE.JPG + +--- RECOVERYTANKER class. +-- @type RECOVERYTANKER +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode. +-- @field #string lid Log debug id text. +-- @field Wrapper.Unit#UNIT carrier The carrier the tanker is attached to. +-- @field #string carriertype Carrier type. +-- @field #string tankergroupname Name of the late activated tanker template group. +-- @field Wrapper.Group#GROUP tanker Tanker group. +-- @field Wrapper.Airbase#AIRBASE airbase The home airbase object of the tanker. Normally the aircraft carrier. +-- @field Core.Radio#BEACON beacon Tanker TACAN beacon. +-- @field #number TACANchannel TACAN channel. Default 1. +-- @field #string TACANmode TACAN mode, i.e. "X" or "Y". Default "Y". Use only "Y" for AA TACAN stations! +-- @field #string TACANmorse TACAN morse code. Three letters identifying the TACAN station. Default "TKR". +-- @field #boolean TACANon If true, TACAN is automatically activated. If false, TACAN is disabled. +-- @field #number speed Tanker speed when flying pattern. +-- @field #number altitude Tanker orbit pattern altitude. +-- @field #number distStern Race-track distance astern. distStern is <0. +-- @field #number distBow Race-track distance bow. distBow is >0. +-- @field #number Dupdate Pattern update when carrier changes its position by more than this distance (meters). +-- @field #number Hupdate Pattern update when carrier changes its heading by more than this number (degrees). +-- @field #number dTupdate Minimum time interval in seconds before the next pattern update can happen. +-- @field #number Tupdate Last time the pattern was updated. +-- @field #number takeoff Takeoff type (cold, hot, air). +-- @field #number lowfuel Low fuel threshold in percent. +-- @field #boolean respawn If true, tanker be respawned (default). If false, no respawning will happen. +-- @field #boolean respawninair If true, tanker will always be respawned in air. This has no impact on the initial spawn setting. +-- @field #boolean uncontrolledac If true, use and uncontrolled tanker group already present in the mission. +-- @field DCS#Vec3 orientation Orientation of the carrier. Used to monitor changes and update the pattern if heading changes significantly. +-- @field DCS#Vec3 orientlast Orientation of the carrier for checking if carrier is currently turning. +-- @field Core.Point#COORDINATE position Positon of carrier. Used to monitor if carrier significantly changed its position and then update the tanker pattern. +-- @extends Core.Fsm#FSM + +--- Recovery Tanker. +-- +-- === +-- +-- ![Banner Image](..\Presentations\RECOVERYTANKER\RecoveryTanker_Main.png) +-- +-- # Recovery Tanker +-- +-- A recovery tanker acts as refueling unit flying overhead an aircraft carrier in order to supply incoming flights with gas if they go "*Bingo on the Ball*". +-- +-- # Simple Script +-- +-- In the mission editor you have to set up a carrier unit, which will act as "mother". In the following, this unit will be named **"USS Stennis"**. +-- +-- Secondly, you need to define a recovery tanker group in the mission editor and set it to **"LATE ACTIVATED"**. The name of the group we'll use is **"Texaco"**. +-- +-- The basic script is very simple and consists of only two lines: +-- +-- TexacoStennis=RECOVERYTANKER:New(UNIT:FindByName("USS Stennis"), "Texaco") +-- TexacoStennis:Start() +-- +-- The first line will create a new RECOVERYTANKER object and the second line starts the process. +-- +-- With this setup, the tanker will be spawned on the USS Stennis with running engines. After it takes off, it will fly a position ~10 NM astern of the boat and from there start its +-- pattern. This is a counter clockwise racetrack pattern at angels 6. +-- +-- A TACAN beacon will be automatically activated at channel 1Y with morse code "TKR". See below how to change this setting. +-- +-- Note that the Tanker entry in the F10 radio menu will appear once the tanker is on station and not before. If you spawn the tanker cold or hot on the carrier, this will take ~10 minutes. +-- +-- Also note, that currently the only carrier capable aircraft in DCS is the S-3B Viking (tanker version). If you want to use another refueling aircraft, you need to activate air spawn +-- or set a different land based airport of the map. This will be explained below. +-- +-- ![Banner Image](..\Presentations\RECOVERYTANKER\RecoveryTanker_Pattern.jpg) +-- +-- The "downwind" leg of the pattern is normally used for refueling. +-- +-- Once the tanker runs out of fuel itself, it will return to the carrier, respawn with full fuel and take up its pattern again. +-- +-- # Options and Fine Tuning +-- +-- Several parameters can be customized by the mission designer via user API functions. +-- +-- ## Takeoff Type +-- +-- By default, the tanker is spawned with running engies on the carrier. The mission designer has set option to set the take off type via the @{#RECOVERYTANKER.SetTakeoff} function. +-- Or via shortcuts +-- +-- * @{#RECOVERYTANKER.SetTakeoffHot}(): Will set the takeoff to hot, which is also the default. +-- * @{#RECOVERYTANKER.SetTakeoffCold}(): Will set the takeoff type to cold, i.e. with engines off. +-- * @{#RECOVERYTANKER.SetTakeoffAir}(): Will set the takeoff type to air, i.e. the tanker will be spawned in air ~10 NM astern the carrier. +-- +-- For example, +-- TexacoStennis=RECOVERYTANKER:New(UNIT:FindByName("USS Stennis"), "Texaco") +-- TexacoStennis:SetTakeoffAir() +-- TexacoStennis:Start() +-- will spawn the tanker several nautical miles astern the carrier. From there it will start its pattern. +-- +-- Spawning in air is not as realsitic but can be useful do avoid DCS bugs and shortcomings like aircraft crashing into each other on the flight deck. +-- +-- **Note** that when spawning in air is set, the tanker will also not return to the boat, once it is out of fuel. Instead it will be respawned directly in air. +-- +-- If only the first spawning should happen on the carrier, one use the @{#RECOVERYTANKER.SetRespawnInAir}() function to command that all subsequent spawning +-- will happen in air. +-- +-- If the tanker should not be respawned at all, one can set @{#RECOVERYTANKER.SetRespawnOff}(). +-- +-- ## Pattern Parameters +-- +-- The racetrack pattern parameters can be fine tuned via the following functions: +-- +-- * @{#RECOVERYTANKER.SetAltitude}(*altitude*), where *altitude* is the pattern altitude in feet. Default 6000 ft. +-- * @{#RECOVERYTANKER.SetSpeed}(*speed*), where *speed* is the pattern speed in knots. Default is 274 knots TAS which results in ~250 KIAS. +-- * @{#RECOVERYTANKER.SetRacetrackDistances}(*distbow*, *diststern*), where *distbow* and *diststern* are the distances ahead and astern the boat (default 10 and 4 NM), respectively. +-- In principle, these number should be more like 8 and 6 NM but since the carrier is moving, we give translate the pattern points a bit forward. +-- +-- ## Home Base +-- +-- The home base is the airbase where the tanker is spawned (if not in air) and where it will go once it is running out of fuel. The default home base is the carrier itself. +-- The home base can be changed via the @{#RECOVERYTANKER.SetHomeBase}(*airbase*) function, where *airbase* can be a MOOSE @{Wrapper.Airbase#AIRBASE} object or simply the +-- name of the airbase passed as string. +-- +-- Note that only the S3B Viking is a refueling aircraft that is carrier capable. You can use other tanker aircraft types, e.g. the KC-130, but in this case you must either +-- set an airport of the map as home base or activate spawning in air via @{#RECOVERYTANKER.SetTakeoffAir}. +-- +-- ## TACAN +-- +-- A TACAN beacon for the tanker can be activated via scripting, i.e. no need to do this within the mission editor. +-- +-- The beacon is create with the @{#RECOVERYTANKER.SetTACAN}(*channel*, *morse*) function, where *channel* is the TACAN channel (a number), +-- and *morse* a three letter string that is send as morse code to identify the tanker: +-- +-- TexacoStennis:SetTACAN(10, "TKR") +-- +-- will activate a TACAN beacon 10Y with more code "TKR". +-- +-- If you do not set a TACAN beacon explicitly, it is automatically create on channel 1Y and morse code "TKR". +-- The mode is *always* "Y" for AA TACAN stations since mode "X" does not work! +-- +-- In order to completely disable the TACAN beacon, you can use the @{#RECOVERYTANKER.SetTACANoff}() function in your script. +-- +-- ## Pattern Update +-- +-- The pattern of the tanker is updated if at least one of the two following conditions apply: +-- +-- * The aircraft carrier changes its position by more than 5 NM (see @{#RECOVERYTANKER.SetPatternUpdateDistance}) and/or +-- * The aircraft carrier changes its heading by more than 5 degrees (see @{#RECOVERYTANKER.SetPatternUpdateHeading}) +-- +-- **Note** that updating the pattern often leads to a more or less small disruption of the perfect racetrack pattern of the tanker. This is because a new waypoint and new racetrack points +-- need to be set as DCS task. This is the reason why the pattern is not contantly updated but rather when the position or heading of the carrier changes significantly. +-- +-- The maximum update frequency is set to 10 minutes. You can adjust this by @{#RECOVERYTANKER.SetPatternUpdateInterval}. +-- Also the pattern will not be updated whilst the carrier is turning or the tanker is currently refueling another unit. +-- +-- # Finite State Machine +-- +-- The implementation uses a Finite State Machine (FSM). This allows the mission designer to hook in to certain events. +-- +-- * @{#RECOVERYTANKER.Start}: This event starts the FMS process and initialized parameters and spawns the tanker. DCS event handling is started. +-- * @{#RECOVERYTANKER.Status}: This event is called in regular intervals (~60 seconds) and checks the status of the tanker and carrier. It triggers other events if necessary. +-- * @{#RECOVERYTANKER.PatternUpdate}: This event commands the tanker to update its pattern +-- * @{#RECOVERYTANKER.RTB}: This events sends the tanker to its home base (usually the carrier). This is called once the tanker runs low on gas. +-- * @{#RECOVERYTANKER.RefuelStart}: This event is called when a tanker starts to refuel another unit. +-- * @{#RECOVERYTANKER.RefuelStop}: This event is called when a tanker stopped to refuel another unit. +-- * @{#RECOVERYTANKER.Run}: This event is called when the tanker resumes normal operations, e.g. after refueling stopped or tanker finished refueling. +-- * @{#RECOVERYTANKER.Stop}: This event stops the FSM by unhandling DCS events. +-- +-- The mission designer can capture these events by RECOVERYTANKER.OnAfter*Eventname* functions, e.g. @{#RECOVERYTANKER.OnAfterPatternUpdate}. +-- +-- # Debugging +-- +-- In case you have problems, it is always a good idea to have a look at your DCS log file. You find it in your "Saved Games" folder, so for example in +-- C:\Users\\Saved Games\DCS\Logs\dcs.log +-- All output concerning the @{#RECOVERYTANKER} class should have the string "RECOVERYTANKER" in the corresponding line. +-- Searching for lines that contain the string "error" or "nil" can also give you a hint what's wrong. +-- +-- The verbosity of the output can be increased by adding the following lines to your script: +-- +-- BASE:TraceOnOff(true) +-- BASE:TraceLevel(1) +-- BASE:TraceClass("RECOVERYTANKER") +-- +-- To get even more output you can increase the trace level to 2 or even 3, c.f. @{Core.Base#BASE} for more details. +-- +-- ## Debug Mode +-- +-- You have the option to enable the debug mode for this class via the @{#RECOVERYTANKER.SetDebugModeON} function. +-- If enabled, text messages about the tanker status will be displayed on screen and marks of the pattern created on the F10 map. +-- +-- @field #RECOVERYTANKER +RECOVERYTANKER = { + ClassName = "RECOVERYTANKER", + Debug = false, + lid = nil, + carrier = nil, + carriertype = nil, + tankergroupname = nil, + tanker = nil, + airbase = nil, + beacon = nil, + TACANchannel = nil, + TACANmode = nil, + TACANmorse = nil, + TACANon = nil, + altitude = nil, + speed = nil, + distStern = nil, + distBow = nil, + dTupdate = nil, + Dupdate = nil, + Hupdate = nil, + Tupdate = nil, + takeoff = nil, + lowfuel = nil, + respawn = nil, + respawninair = nil, + uncontrolledac = nil, + orientation = nil, + orientlast = nil, + position = nil, +} + +--- Class version. +-- @field #string version +RECOVERYTANKER.version="1.0.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Is alive check for tanker necessary? +-- DONE: Seamless change of position update. Get good updated waypoint and update position if tanker position is right. Not really possiple atm. +-- DONE: Check if TACAN mode "X" is allowed for AA TACAN stations. Nope +-- DONE: Check if tanker is going back to "Running" state after RTB and respawn. +-- DONE: Write documenation. +-- DONE: Trace functions self:T instead of self:I for less output. +-- DONE: Make pattern update parameters (distance, orientation) input parameters. +-- DONE: Add FSM event for pattern update. +-- DONE: Smarter pattern update function. E.g. (small) zone around carrier. Only update position when carrier leaves zone or changes heading? +-- DONE: Set AA TACAN. +-- DONE: Add refueling event/state. +-- DONE: Possibility to add already present/spawned aircraft, e.g. for warehouse. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create new RECOVERYTANKER object. +-- @param #RECOVERYTANKER self +-- @param Wrapper.Unit#UNIT carrierunit Carrier unit. +-- @param #string tankergroupname Name of the late activated tanker aircraft template group. +-- @return #RECOVERYTANKER RECOVERYTANKER object. +function RECOVERYTANKER:New(carrierunit, tankergroupname) + + -- Inherit everthing from FSM class. + local self = BASE:Inherit(self, FSM:New()) -- #RECOVERYTANKER + + if type(carrierunit)=="string" then + self.carrier=UNIT:FindByName(carrierunit) + else + self.carrier=carrierunit + end + + -- Carrier type. + self.carriertype=self.carrier:GetTypeName() + + -- Tanker group name. + self.tankergroupname=tankergroupname + + -- Save self in static object. Easier to retrieve later. + self.carrier:SetState(self.carrier, "RECOVERYTANKER", self) + + -- Debug log id. + self.lid=string.format("RECOVERYTANKER %s", self.carrier:GetName()) + + -- Init default parameters. + self:SetAltitude() + self:SetSpeed() + self:SetRacetrackDistances() + self:SetHomeBase(AIRBASE:FindByName(self.carrier:GetName())) + self:SetTakeoffHot() + self:SetLowFuelThreshold() + self:SetRespawnOnOff() + self:SetTACAN() + self:SetPatternUpdateDistance() + self:SetPatternUpdateHeading() + self:SetPatternUpdateInterval() + + -- Debug trace. + if false then + BASE:TraceOnOff(true) + BASE:TraceClass(self.ClassName) + BASE:TraceLevel(1) + end + + ----------------------- + --- FSM Transitions --- + ----------------------- + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start the FSM. + self:AddTransition("*", "RefuelStart", "Refueling") -- Tanker has started to refuel another unit. + self:AddTransition("*", "RefuelStop", "Running") -- Tanker starts to refuel. + self:AddTransition("*", "Run", "Running") -- Tanker starts normal operation again. + self:AddTransition("Running", "RTB", "Returning") -- Tanker is returning to base (for fuel). + self:AddTransition("*", "Status", "*") -- Status update. + self:AddTransition("Running", "PatternUpdate", "*") -- Update pattern wrt to carrier. + self:AddTransition("*", "Stop", "Stopped") -- Stop the FSM. + + + --- Triggers the FSM event "Start" that starts the recovery tanker. Initializes parameters and starts event handlers. + -- @function [parent=#RECOVERYTANKER] Start + -- @param #RECOVERYTANKER self + + --- Triggers the FSM event "Start" that starts the recovery tanker after a delay. Initializes parameters and starts event handlers. + -- @function [parent=#RECOVERYTANKER] __Start + -- @param #RECOVERYTANKER self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "RefuelStart" when the tanker starts refueling another aircraft. + -- @function [parent=#RECOVERYTANKER] RefuelStart + -- @param #RECOVERYTANKER self + -- @param Wrapper.Unit#UNIT receiver Unit receiving fuel from the tanker. + + --- On after "RefuelStart" event user function. Called when a the the tanker started to refuel another unit. + -- @function [parent=#RECOVERYTANKER] OnAfterRefuelStart + -- @param #RECOVERYTANKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Unit#UNIT receiver Unit receiving fuel from the tanker. + + + --- Triggers the FSM event "RefuelStop" when the tanker stops refueling another aircraft. + -- @function [parent=#RECOVERYTANKER] RefuelStop + -- @param #RECOVERYTANKER self + -- @param Wrapper.Unit#UNIT receiver Unit stoped receiving fuel from the tanker. + + --- On after "RefuelStop" event user function. Called when a the the tanker stopped to refuel another unit. + -- @function [parent=#RECOVERYTANKER] OnAfterRefuelStop + -- @param #RECOVERYTANKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Unit#UNIT receiver Unit that received fuel from the tanker. + + + --- Triggers the FSM event "Run". Simply puts the group into "Running" state. + -- @function [parent=#RECOVERYTANKER] Run + -- @param #RECOVERYTANKER self + + --- Triggers delayed the FSM event "Run". Simply puts the group into "Running" state. + -- @function [parent=#RECOVERYTANKER] __Run + -- @param #RECOVERYTANKER self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "RTB" that sends the tanker home. + -- @function [parent=#RECOVERYTANKER] RTB + -- @param #RECOVERYTANKER self + -- @param Wrapper.Airbase#AIRBASE airbase The airbase where the tanker should return to. + + --- Triggers the FSM event "RTB" that sends the tanker home after a delay. + -- @function [parent=#RECOVERYTANKER] __RTB + -- @param #RECOVERYTANKER self + -- @param #number delay Delay in seconds. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase where the tanker should return to. + + --- On after "RTB" event user function. Called when a the the tanker returns to its home base. + -- @function [parent=#RECOVERYTANKER] OnAfterRTB + -- @param #RECOVERYTANKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase where the tanker should return to. + + + --- Triggers the FSM event "Status" that updates the tanker status. + -- @function [parent=#RECOVERYTANKER] Status + -- @param #RECOVERYTANKER self + + --- Triggers the delayed FSM event "Status" that updates the tanker status. + -- @function [parent=#RECOVERYTANKER] __Status + -- @param #RECOVERYTANKER self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "PatternUpdate" that updates the pattern of the tanker wrt to the carrier position. + -- @function [parent=#RECOVERYTANKER] PatternUpdate + -- @param #RECOVERYTANKER self + + --- Triggers the delayed FSM event "PatternUpdate" that updates the pattern of the tanker wrt to the carrier position. + -- @function [parent=#RECOVERYTANKER] __PatternUpdate + -- @param #RECOVERYTANKER self + -- @param #number delay Delay in seconds. + + --- On after "PatternEvent" event user function. Called when a the pattern of the tanker is updated. + -- @function [parent=#RECOVERYTANKER] OnAfterPatternUpdate + -- @param #RECOVERYTANKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Stop" that stops the recovery tanker. Event handlers are stopped. + -- @function [parent=#RECOVERYTANKER] Stop + -- @param #RECOVERYTANKER self + + --- Triggers the FSM event "Stop" that stops the recovery tanker after a delay. Event handlers are stopped. + -- @function [parent=#RECOVERYTANKER] __Stop + -- @param #RECOVERYTANKER self + -- @param #number delay Delay in seconds. + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set the speed the tanker flys in its orbit pattern. +-- @param #RECOVERYTANKER self +-- @param #number speed True air speed (TAS) in knots. Default 274 knots, which results in ~250 KIAS. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetSpeed(speed) + self.speed=UTILS.KnotsToMps(speed or 274) + return self +end + +--- Set orbit pattern altitude of the tanker. +-- @param #RECOVERYTANKER self +-- @param #number altitude Tanker altitude in feet. Default 6000 ft. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetAltitude(altitude) + self.altitude=UTILS.FeetToMeters(altitude or 6000) + return self +end + +--- Set race-track distances. +-- @param #RECOVERYTANKER self +-- @param #number distbow Distance [NM] in front of the carrier. Default 10 NM. +-- @param #number diststern Distance [NM] behind the carrier. Default 4 NM. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetRacetrackDistances(distbow, diststern) + self.distBow=UTILS.NMToMeters(distbow or 10) + self.distStern=-UTILS.NMToMeters(diststern or 4) + return self +end + +--- Set minimum pattern update interval. After a pattern update this time interval has to pass before the next update is allowed. +-- @param #RECOVERYTANKER self +-- @param #number interval Min interval in minutes. Default is 10 minutes. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetPatternUpdateInterval(interval) + self.dTupdate=(interval or 10)*60 + return self +end + +--- Set pattern update distance threshold. Tanker will update its pattern when the carrier changes its position by more than this distance. +-- @param #RECOVERYTANKER self +-- @param #number distancechange Distance threshold in NM. Default 5 NM (=9.62 km). +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetPatternUpdateDistance(distancechange) + self.Dupdate=UTILS.NMToMeters(distancechange or 5) + return self +end + +--- Set pattern update heading threshold. Tanker will update its pattern when the carrier changes its heading by more than this value. +-- @param #RECOVERYTANKER self +-- @param #number headingchange Heading threshold in degrees. Default 5 degrees. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetPatternUpdateHeading(headingchange) + self.Hupdate=headingchange or 5 + return self +end + +--- Set low fuel state of tanker. When fuel is below this threshold, the tanker will RTB or be respawned if takeoff type is in air. +-- @param #RECOVERYTANKER self +-- @param #number fuelthreshold Low fuel threshold in percent. Default 10 % of max fuel. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetLowFuelThreshold(fuelthreshold) + self.lowfuel=fuelthreshold or 10 + return self +end + +--- Set home airbase of the tanker. This is the airbase where the tanker will go when it is out of fuel. +-- @param #RECOVERYTANKER self +-- @param Wrapper.Airbase#AIRBASE airbase The home airbase. Can be the airbase name or a Moose AIRBASE object. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetHomeBase(airbase) + if type(airbase)=="string" then + self.airbase=AIRBASE:FindByName(airbase) + else + self.airbase=airbase + end + if not self.airbase then + self:E(self.lid.."ERROR: Airbase is nil!") + end + return self +end + +--- Set takeoff type. +-- @param #RECOVERYTANKER self +-- @param #number takeofftype Takeoff type. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetTakeoff(takeofftype) + self.takeoff=takeofftype + return self +end + +--- Set takeoff with engines running (hot). +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetTakeoffHot() + self:SetTakeoff(SPAWN.Takeoff.Hot) + return self +end + +--- Set takeoff with engines off (cold). +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetTakeoffCold() + self:SetTakeoff(SPAWN.Takeoff.Cold) + return self +end + +--- Set takeoff in air at the defined pattern altitude and ~10 NM astern the carrier. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetTakeoffAir() + self:SetTakeoff(SPAWN.Takeoff.Air) + return self +end + +--- Enable respawning of tanker. Note that this is the default behaviour. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetRespawnOn() + self.respawn=true + return self +end + +--- Disable respawning of tanker. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetRespawnOff() + self.respawn=false + return self +end + +--- Set whether tanker shall be respawned or not. +-- @param #RECOVERYTANKER self +-- @param #boolean switch If true (or nil), tanker will be respawned. If false, tanker will not be respawned. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetRespawnOnOff(switch) + if switch==nil or switch==true then + self.respawn=true + else + self.respawn=false + end + return self +end + +--- Tanker will be respawned in air, even it was initially spawned on the carrier. +-- So only the first spawn will be on the carrier while all subsequent spawns will happen in air. +-- This allows for undisrupted operations and less problems on the carrier deck. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetRespawnInAir() + self.respawninair=true + return self +end + +--- Use an uncontrolled aircraft already present in the mission rather than spawning a new tanker as initial recovery thanker. +-- This can be useful when interfaced with, e.g., a MOOSE @{Functional.Warehouse#WAREHOUSE}. +-- The group name is the one specified in the @{#RECOVERYTANKER.New} function. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetUseUncontrolledAircraft() + self.uncontrolledac=true + return self +end + + +--- Disable automatic TACAN activation. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetTACANoff() + self.TACANon=false + return self +end + +--- Set TACAN channel of tanker. Note that mode is automatically set to "Y" for AA TACAN since only that works. +-- @param #RECOVERYTANKER self +-- @param #number channel TACAN channel. Default 1. +-- @param #string morse TACAN morse code identifier. Three letters. Default "TKR". +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetTACAN(channel, morse) + self.TACANchannel=channel or 1 + self.TACANmode="Y" + self.TACANmorse=morse or "TKR" + self.TACANon=true + return self +end + +--- Activate debug mode. Marks of pattern on F10 map and debug messages displayed on screen. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetDebugModeON() + self.Debug=true + return self +end + +--- Deactivate debug mode. This is also the default setting. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetDebugModeOFF() + self.Debug=false + return self +end + +--- Check if tanker is currently returning to base. +-- @param #RECOVERYTANKER self +-- @return #boolean If true, tanker is returning to base. +function RECOVERYTANKER:IsReturning() + return self:is("Returning") +end + +--- Check if tanker is currently operating. +-- @param #RECOVERYTANKER self +-- @return #boolean If true, tanker is operating. +function RECOVERYTANKER:IsRunning() + return self:is("Running") +end + +--- Check if tanker is currently refueling another aircraft. +-- @param #RECOVERYTANKER self +-- @return #boolean If true, tanker is refueling. +function RECOVERYTANKER:IsRefueling() + return self:is("Refueling") +end + +--- Check if FMS was stopped. +-- @param #RECOVERYTANKER self +-- @return #boolean If true, is stopped. +function RECOVERYTANKER:IsStopped() + return self:is("Stopped") +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM states +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. Starts the warehouse. Addes event handlers and schedules status updates of reqests and queue. +-- @param #RECOVERYTANKER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RECOVERYTANKER:onafterStart(From, Event, To) + + -- Info on start. + self:I(string.format("Starting Recovery Tanker v%s for carrier unit %s of type %s for tanker group %s.", RECOVERYTANKER.version, self.carrier:GetName(), self.carriertype, self.tankergroupname)) + + -- Handle events. + self:HandleEvent(EVENTS.EngineShutdown) + self:HandleEvent(EVENTS.Refueling, self._RefuelingStart) --Need explcit functions sice OnEventRefueling and OnEventRefuelingStop did not hook. + self:HandleEvent(EVENTS.RefuelingStop, self._RefuelingStop) + + -- Set unique alias for spawn from tanker group name and carrier unit name. + local tankergroupalias=string.format("%s_%s", self.tankergroupname, self.carrier:GetName()) + + -- Spawn tanker. We need to introduce an alias in case this class is used twice. This would confuse the spawn routine. + local Spawn=SPAWN:NewWithAlias(self.tankergroupname, tankergroupalias) + + -- Spawn on carrier. + if self.takeoff==SPAWN.Takeoff.Air then + + -- Carrier heading + local hdg=self.carrier:GetHeading() + + -- Spawn distance behind the carrier. + local dist=-self.distStern+UTILS.NMToMeters(4) + + -- Coordinate behind the carrier and slightly port. + local Carrier=self.carrier:GetCoordinate():Translate(dist, hdg+190):SetAltitude(self.altitude) + + -- Orientation of spawned group. + Spawn:InitHeading(hdg+10) + + -- Spawn at coordinate. + self.tanker=Spawn:SpawnFromCoordinate(Carrier) + + else + + -- Check if an uncontrolled tanker group was requested. + if self.useuncontrolled then + + -- Use an uncontrolled aircraft group. + self.tanker=GROUP:FindByName(self.tankergroupname) + + if self.tanker:IsAlive() then + + -- Start uncontrolled group. + self.tanker:StartUncontrolled() + + else + -- No group by that name! + self:E(string.format("ERROR: No uncontrolled (alive) tanker group with name %s could be found!", self.tankergroupname)) + return + end + + else + + -- Spawn tanker at airbase. + self.tanker=Spawn:SpawnAtAirbase(self.airbase, self.takeoff) + + end + + end + + -- Initialize route. self.distStern<0! + SCHEDULER:New(self, self._InitRoute, {-self.distStern+UTILS.NMToMeters(3)}, 1) + --self:_InitRoute(-self.distStern+UTILS.NMToMeters(3), 1) + + -- Create tanker beacon. + if self.TACANon then + self:_ActivateTACAN(2) + end + + -- Get initial orientation and position of carrier. + self.orientation=self.carrier:GetOrientationX() + self.orientlast=self.carrier:GetOrientationX() + self.position=self.carrier:GetCoordinate() + + -- Init status updates in 10 seconds. + self:__Status(10) +end + + +--- On after Status event. Checks player status. +-- @param #RECOVERYTANKER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RECOVERYTANKER:onafterStatus(From, Event, To) + + -- Get current time. + local time=timer.getTime() + + -- Get fuel of tanker. + local fuel=self.tanker:GetFuel()*100 + local text=string.format("Recovery tanker %s: state=%s fuel=%.1f", self.tanker:GetName(), self:GetState(), fuel) + self:T(self.lid..text) + + -- Check if tanker is running and not RTBing or refueling. + if self:IsRunning() then + + -- Check fuel. + if fuel 100 meters, this should be another tanker. + if dist>100 then + return + end + + -- Info message. + local text=string.format("Recovery tanker %s started refueling unit %s", self.tanker:GetName(), receiver:GetName()) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- FMS state "Refueling". + self:RefuelStart(receiver) + + end + +end + +--- Event handler for refueling stopped. +-- @param #RECOVERYTANKER self +-- @param Core.Event#EVENTDATA EventData Event data. +function RECOVERYTANKER:_RefuelingStop(EventData) + + if EventData and EventData.IniUnit and EventData.IniUnit:IsAlive() then + + -- Unit receiving fuel. + local receiver=EventData.IniUnit + + -- Get distance to tanker to check that unit is receiving fuel from this tanker. + local dist=receiver:GetCoordinate():Get2DDistance(self.tanker:GetCoordinate()) + + -- If distance > 100 meters, this should be another tanker. + if dist>100 then + return + end + + -- Info message. + local text=string.format("Recovery tanker %s stopped refueling unit %s", self.tanker:GetName(), receiver:GetName()) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- FSM state "Running". + self:RefuelStop(receiver) + end + +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- MISC functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Task function to +-- @param #RECOVERYTANKER self +function RECOVERYTANKER:_InitPatternTaskFunction() + + -- Name of the warehouse (static) object. + local carriername=self.carrier:GetName() + + -- Task script. + local DCSScript = {} + DCSScript[#DCSScript+1] = string.format('local mycarrier = UNIT:FindByName(\"%s\") ', carriername) -- The carrier unit that holds the self object. + DCSScript[#DCSScript+1] = string.format('local mytanker = mycarrier:GetState(mycarrier, \"RECOVERYTANKER\") ') -- Get the RECOVERYTANKER self object. + DCSScript[#DCSScript+1] = string.format('mytanker:PatternUpdate()') -- Call the function, e.g. mytanker.(self) + + -- Create task. + local DCSTask = CONTROLLABLE.TaskWrappedAction(self, CONTROLLABLE.CommandDoScript(self, table.concat(DCSScript))) + + return DCSTask +end + + +--- Init waypoint after spawn. Tanker is first guided to a position astern the carrier and starts its racetrack pattern from there. +-- @param #RECOVERYTANKER self +-- @param #number dist Distance [NM] of initial waypoint astern carrier. Default 8 NM. +-- @param #number delay Delay before routing in seconds. Default 1 second. +function RECOVERYTANKER:_InitRoute(dist, delay) + + -- Defaults. + dist=dist or UTILS.NMToMeters(8) + delay=delay or 1 + + -- Debug message. + self:T(self.lid..string.format("Initializing route of recovery tanker %s.", self.tanker:GetName())) + + -- Carrier position. + local Carrier=self.carrier:GetCoordinate() + + -- Carrier heading. + local hdg=self.carrier:GetHeading() + + -- First waypoint is ~10 NM behind and slightly port the boat. + local p=Carrier:Translate(dist, hdg+190):SetAltitude(self.altitude) + + -- Speed for waypoints in km/h. + -- This causes a problem, because the tanker might not be alive yet ==> We schedule the call of _InitRoute + local speed=self.tanker:GetSpeedMax()*0.8 + + -- Set to 280 knots and convert to km/h. + --local speed=280/0.539957 + + -- Debug mark. + if self.Debug then + p:MarkToAll(string.format("Enter Pattern WP: alt=%d ft, speed=%d kts", UTILS.MetersToFeet(self.altitude), speed*0.539957)) + end + + -- Task to update pattern when wp 2 is reached. + local task=self:_InitPatternTaskFunction() + + -- Waypoints. + local wp={} + if self.takeoff==SPAWN.Takeoff.Air then + wp[#wp+1]=self.tanker:GetCoordinate():SetAltitude(self.altitude):WaypointAirTurningPoint(nil, speed, {}, "Spawn Position") + else + wp[#wp+1]=Carrier:WaypointAirTakeOffParking() + end + wp[#wp+1]=p:WaypointAirTurningPoint(nil, speed, {task}, "Enter Pattern") + + -- Set route. + self.tanker:Route(wp, delay) + + -- Set state to Running. Necessary when tanker was RTB and respawned since it is probably in state "Returning". + self:__Run(1) + + -- No update yet, wait until the function is called (avoids checks if pattern update is needed). + self.Tupdate=nil +end + +--- Check if heading or position have changed significantly. +-- @param #RECOVERYTANKER self +-- @param #number dt Time since last update in seconds. +-- @return #boolean If true, heading and/or position have changed more than 5 degrees or 10 km, respectively. +function RECOVERYTANKER:_CheckPatternUpdate(dt) + + -- Get current position and orientation of carrier. + local pos=self.carrier:GetCoordinate() + + -- Current orientation of carrier. + local vNew=self.carrier:GetOrientationX() + + -- Reference orientation of carrier after the last update + local vOld=self.orientation + + -- Last orientation from 30 seconds ago. + local vLast=self.orientlast + + -- We only need the X-Z plane. + vNew.y=0 ; vOld.y=0 ; vLast.y=0 + + -- Get angle between old and new orientation vectors in rad and convert to degrees. + local deltaHeading=math.deg(math.acos(UTILS.VecDot(vNew,vOld)/UTILS.VecNorm(vNew)/UTILS.VecNorm(vOld))) + + -- Angle between current heading and last time we checked ~30 seconds ago. + local deltaLast=math.deg(math.acos(UTILS.VecDot(vNew,vLast)/UTILS.VecNorm(vNew)/UTILS.VecNorm(vLast))) + + -- Last orientation becomes new orientation + self.orientlast=vNew + + -- Carrier is turning when its heading changed by at least one degree since last check. + local turning=deltaLast>=1 + + -- Debug output if turning + if turning then + self:T2(self.lid..string.format("Carrier is turning. Delta Heading = %.1f", deltaLast)) + end + + -- Check if orientation changed. + local Hchange=false + if math.abs(deltaHeading)>=self.Hupdate then + self:T(self.lid..string.format("Carrier heading changed by %d degrees. Turning=%s.", deltaHeading, tostring(turning))) + Hchange=true + end + + -- Get distance to saved position. + local dist=pos:Get2DDistance(self.position) + + -- Check if carrier moved more than ~5 NM. + local Dchange=false + if dist>self.Dupdate then + self:T(self.lid..string.format("Carrier position changed by %.1f NM. Turning=%s.", UTILS.MetersToNM(dist), tostring(turning))) + Dchange=true + end + + -- Assume no update necessary. + local update=false + + -- No update if currently turning! Also must be running (not RTB or refueling) and T>~10 min since last position update. + if self:IsRunning() and dt>self.dTupdate and not turning then + + -- Update if heading or distance changed. + if Hchange or Dchange then + -- Debug message. + local text=string.format("Updating tanker %s pattern due to carrier position=%s or heading=%s change.", self.tanker:GetName(), tostring(Dchange), tostring(Hchange)) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- Update pos and orientation. + self.orientation=vNew + self.position=pos + update=true + end + + end + + return update +end + +--- Activate TACAN of tanker. +-- @param #RECOVERYTANKER self +-- @param #number delay Delay in seconds. +function RECOVERYTANKER:_ActivateTACAN(delay) + + if delay and delay>0 then + + -- Schedule TACAN activation. + SCHEDULER:New(self, self._ActivateTACAN, {}, delay) + + else + + -- Get tanker unit. + local unit=self.tanker:GetUnit(1) + + -- Check if unit is alive. + if unit:IsAlive() then + + -- Debug message. + local text=string.format("Activating recovery tanker TACAN beacon: channel=%d mode=%s, morse=%s.", self.TACANchannel, self.TACANmode, self.TACANmorse) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- Create a new beacon and activate TACAN. + self.beacon=BEACON:New(unit) + self.beacon:ActivateTACAN(self.TACANchannel, self.TACANmode, self.TACANmorse, true) + + else + self:E("ERROR: Recovery tanker is not alive!") + end + + end + +end + +--- Self made race track pattern. Not working as desired, since tanker changes course too rapidly after each waypoint. +-- @param #RECOVERYTANKER self +-- @return #table Table of pattern waypoints. +function RECOVERYTANKER:_Pattern() + + -- Carrier heading. + local hdg=self.carrier:GetHeading() + + -- Pattern altitude + local alt=self.altitude + + -- Carrier position. + local Carrier=self.carrier:GetCoordinate() + + local width=UTILS.NMToMeters(8) + + -- Define race-track pattern. + local p={} + p[1]=self.tanker:GetCoordinate() -- Tanker position + p[2]=Carrier:SetAltitude(alt) -- Carrier position + p[3]=p[2]:Translate(self.distBow, hdg) -- In front of carrier + p[4]=p[3]:Translate(width/math.sqrt(2), hdg-45) -- Middle front for smoother curve + -- Probably need one more to make it go -hdg at the waypoint. + p[5]=p[3]:Translate(width, hdg-90) -- In front on port + p[6]=p[5]:Translate(self.distStern-self.distBow, hdg) -- Behind on port (sterndist<0!) + p[7]=p[2]:Translate(self.distStern, hdg) -- Behind carrier + + local wp={} + for i=1,#p do + local coord=p[i] --Core.Point#COORDINATE + coord:MarkToAll(string.format("Waypoint %d", i)) + --table.insert(wp, coord:WaypointAirFlyOverPoint(nil , self.speed)) + table.insert(wp, coord:WaypointAirTurningPoint(nil , UTILS.MpsToKmph(self.speed))) + end + + return wp +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/RescueHelo.lua b/Moose Development/Moose/Ops/RescueHelo.lua new file mode 100644 index 000000000..7a39d29d7 --- /dev/null +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -0,0 +1,1093 @@ +--- **Ops** - (R2.5) - Rescue helicopter for carrier operations. +-- +-- Recue helicopter for carrier operations. +-- +-- **Main Features:** +-- +-- * Close formation with carrier. +-- * No restrictions regarding carrier waypoints and heading. +-- * Automatic respawning on empty fuel for 24/7 operations. +-- * Automatic rescuing of crashed or ejected pilots in the vicinity of the carrier. +-- * Multiple helos at different carriers due to object oriented approach. +-- * Finite State Machine (FSM) implementation. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- ### Contributions: Flightcontrol (@{AI.AI_Formation} class being used here) +-- +-- @module Ops.RescueHelo +-- @image MOOSE.JPG + +--- RESCUEHELO class. +-- @type RESCUEHELO +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode on/off. +-- @field #string lid Log debug id text. +-- @field Wrapper.Unit#UNIT carrier The carrier the helo is attached to. +-- @field #string carriertype Carrier type. +-- @field #string helogroupname Name of the late activated helo template group. +-- @field Wrapper.Group#GROUP helo Helo group. +-- @field #number takeoff Takeoff type. +-- @field Wrapper.Airbase#AIRBASE airbase The airbase object acting as home base of the helo. +-- @field Core.Set#SET_GROUP followset Follow group set. +-- @field AI.AI_Formation#AI_FORMATION formation AI_FORMATION object. +-- @field #number lowfuel Low fuel threshold of helo in percent. +-- @field #number altitude Altitude of helo in meters. +-- @field #number offsetX Offset in meters to carrier in longitudinal direction. +-- @field #number offsetZ Offset in meters to carrier in latitudinal direction. +-- @field Core.Zone#ZONE_RADIUS rescuezone Zone around the carrier in which helo will rescue crashed or ejected units. +-- @field #boolean respawn If true, helo be respawned (default). If false, no respawning will happen. +-- @field #boolean respawninair If true, helo will always be respawned in air. This has no impact on the initial spawn setting. +-- @field #boolean uncontrolledac If true, use and uncontrolled helo group already present in the mission. +-- @field #boolean rescueon If true, helo will rescue crashed pilots. If false, no recuing will happen. +-- @field #number rescueduration Time the rescue helicopter hovers over the crash site in seconds. +-- @field #number rescuespeed Speed in m/s the rescue helicopter hovers at over the crash site. +-- @field #boolean rescuestopboat If true, stop carrier during rescue operations. +-- @field #boolean carrierstop If true, route of carrier was stopped. +-- @field #number HeloFuel0 Initial fuel of helo in percent. Necessary due to DCS bug that helo with full tank does not return fuel via API function. +-- @field #boolean rtb If true, Helo will be return to base on the next status check. +-- @extends Core.Fsm#FSM + +--- Rescue Helo +-- +-- === +-- +-- ![Banner Image](..\Presentations\RESCUEHELO\RescueHelo_Main.png) +-- +-- # Recue Helo +-- +-- The rescue helo will fly in close formation with another unit, which is typically an aircraft carrier. +-- It's mission is to rescue crashed or ejected pilots. Well, and to look cool... +-- +-- # Simple Script +-- +-- In the mission editor you have to set up a carrier unit, which will act as "mother". In the following, this unit will be named "*USS Stennis*". +-- +-- Secondly, you need to define a recue helicopter group in the mission editor and set it to "**LATE ACTIVATED**". The name of the group we'll use is "*Recue Helo*". +-- +-- The basic script is very simple and consists of only two lines. +-- +-- RescueheloStennis=RESCUEHELO:New(UNIT:FindByName("USS Stennis"), "Rescue Helo") +-- RescueheloStennis:Start() +-- +-- The first line will create a new @{#RESCUEHELO} object via @{#RESCUEHELO.New} and the second line starts the process by calling @{#RESCUEHELO.Start}. +-- +-- **NOTE** that it is *very important* to define the RESCUEHELO object as **global** variable. Otherwise, the lua garbage collector will kill the formation for unknown reasons! +-- +-- By default, the helo will be spawned on the *USS Stennis* with hot engines. Then it will take off and go on station on the starboard side of the boat. +-- +-- Once the helo is out of fuel, it will return to the carrier. When the helo lands, it will be respawned immidiately and go back on station. +-- +-- If a unit crashes or a pilot ejects within a radius of 30 km from the USS Stennis, the helo will automatically fly to the crash side and +-- rescue to pilot. This will take around 5 minutes. After that, the helo will return to the Stennis, land there and bring back the poor guy. +-- When this is done, the helo will go back on station. +-- +-- # Fine Tuning +-- +-- The implementation allows to customize quite a few settings easily via user API functions. +-- +-- ## Takeoff Type +-- +-- By default, the helo is spawned with running engies on the carrier. The mission designer has set option to set the take off type via the @{#RESCUEHELO.SetTakeoff} function. +-- Or via shortcuts +-- +-- * @{#RESCUEHELO.SetTakeoffHot}(): Will set the takeoff to hot, which is also the default. +-- * @{#RESCUEHELO.SetTakeoffCold}(): Will set the takeoff type to cold, i.e. with engines off. +-- * @{#RESCUEHELO.SetTakeoffAir}(): Will set the takeoff type to air, i.e. the helo will be spawned in air near the unit which he follows. +-- +-- For example, +-- RescueheloStennis=RESCUEHELO:New(UNIT:FindByName("USS Stennis"), "Rescue Helo") +-- RescueheloStennis:SetTakeoffAir() +-- RescueheloStennis:Start() +-- will spawn the helo near the USS Stennis in air. +-- +-- Spawning in air is not as realsitic but can be useful do avoid DCS bugs and shortcomings like aircraft crashing into each other on the flight deck. +-- +-- **Note** that when spawning in air is set, the helo will also not return to the boat, once it is out of fuel. Instead it will be respawned in air. +-- +-- If only the first spawning should happen on the carrier, one use the @{#RESCUEHELO.SetRespawnInAir}() function to command that all subsequent spawning +-- will happen in air. +-- +-- If the helo should no be respawned at all, one can set @{#RESCUEHELO.SetRespawnOff}(). +-- +-- ## Home Base +-- +-- It is possible to define a "home base" other than the aircaft carrier using the @{#RESCUEHELO.SetHomeBase}(*airbase*) function, where *airbase* is +-- a @{Wrapper.Airbase#AIRBASE} object or simply the name of the airbase. +-- +-- For example, one could imagine a strike group, and the helo will be spawned from another ship which has a helo pad. +-- +-- RescueheloStennis=RESCUEHELO:New(UNIT:FindByName("USS Stennis"), "Rescue Helo") +-- RescueheloStennis:SetHomeBase(AIRBASE:FindByName("USS Normandy")) +-- RescueheloStennis:Start() +-- +-- In this case, the helo will be spawned on the USS Normandy and then make its way to the USS Stennis to establish the formation. +-- Note that the distance to the mother ship should be rather small since the helo will go there very slowly. +-- +-- Once the helo runs out of fuel, it will return to the USS Normandy and not the Stennis for respawning. +-- +-- ## Formation Positon +-- +-- The position of the helo relative to the mother ship can be tuned via the functions +-- +-- * @{#RESCUEHELO.SetAltitude}(*altitude*), where *altitude* is the altitude the helo flies at in meters. Default is 70 meters. +-- * @{#RESCUEHELO.SetOffsetX}(*distance*), where *distance is the distance in the direction of movement of the carrier. Default is 200 meters. +-- * @{#RESCUEHELO.SetOffsetZ}(*distance*), where *distance is the distance on the starboard side. Default is 100 meters. +-- +-- ## Rescue Operations +-- +-- By default the rescue helo will start a rescue operation if an aircraft crashes or a pilot ejects in the vicinity of the carrier. +-- This is rescricted to aircraft of the same coaliton as the rescue helo. Enemy (or neutral) pilots will be left on their own. +-- +-- The standard "rescue zone" has a radius of 15 NM (~28 km) around the carrier. The radius can be adjusted via the @{#RESCUEHELO.SetRescueZone}(*radius*) functions, +-- where *radius* is the radius of the zone in nautical miles. If you use multiple rescue helos in the same mission, you might want to ensure that the radii +-- are not overlapping so that two helos try to rescue the same pilot. But it should not hurt either way. +-- +-- Once the helo reaches the crash site, the rescue operation will last 5 minutes. This time can be changed by @{#RESCUEHELO.SetRescueDuration(*time*), +-- where *time* is the duration in minutes. +-- +-- During the rescue operation, the helo will hover (orbit) over the crash site at a speed of 5 knots. The speed can be set by @{#RESCUEHELO.SetRescueHoverSpeed}(*speed*), +-- where the *speed* is given in knots. +-- +-- If no rescue operations should be carried out by the helo, this option can be completely disabled by using @{#RESCUEHELO.SetRescueOff}(). +-- +-- # Finite State Machine +-- +-- The implementation uses a Finite State Machine (FSM). This allows the mission designer to hook in to certain events. +-- +-- * @{#RESCUEHELO.Start}: This eventfunction starts the FMS process and initialized parameters and spawns the helo. DCS event handling is started. +-- * @{#RESCUEHELO.Status}: This eventfunction is called in regular intervals (~60 seconds) and checks the status of the helo and carrier. It triggers other events if necessary. +-- * @{#RESCUEHELO.Rescue}: This eventfunction commands the helo to go on a rescue operation at a certain coordinate. +-- * @{#RESCUEHELO.RTB}: This eventsfunction sends the helo to its home base (usually the carrier). This is called once the helo runs low on gas. +-- * @{#RESCUEHELO.Run}: This eventfunction is called when the helo resumes normal operations and goes back on station. +-- * @{#RESCUEHELO.Stop}: This eventfunction stops the FSM by unhandling DCS events. +-- +-- The mission designer can capture these events by RESCUEHELO.OnAfter*Eventname* functions, e.g. @{#RESCUEHELO.OnAfterRescue}. +-- +-- # Debugging +-- +-- In case you have problems, it is always a good idea to have a look at your DCS log file. You find it in your "Saved Games" folder, so for example in +-- C:\Users\\Saved Games\DCS\Logs\dcs.log +-- All output concerning the @{#RESCUEHELO} class should have the string "RESCUEHELO" in the corresponding line. +-- Searching for lines that contain the string "error" or "nil" can also give you a hint what's wrong. +-- +-- The verbosity of the output can be increased by adding the following lines to your script: +-- +-- BASE:TraceOnOff(true) +-- BASE:TraceLevel(1) +-- BASE:TraceClass("RESCUEHELO") +-- +-- To get even more output you can increase the trace level to 2 or even 3, c.f. @{Core.Base#BASE} for more details. +-- +-- ## Debug Mode +-- +-- You have the option to enable the debug mode for this class via the @{#RESCUEHELO.SetDebugModeON} function. +-- If enabled, text messages about the helo status will be displayed on screen and marks of the pattern created on the F10 map. +-- +-- +-- @field #RESCUEHELO +RESCUEHELO = { + ClassName = "RESCUEHELO", + Debug = false, + lid = nil, + carrier = nil, + carriertype = nil, + helogroupname = nil, + helo = nil, + airbase = nil, + takeoff = nil, + followset = nil, + formation = nil, + lowfuel = nil, + altitude = nil, + offsetX = nil, + offsetZ = nil, + rescuezone = nil, + respawn = nil, + respawninair = nil, + uncontrolledac = nil, + rescueon = nil, + rescueduration = nil, + rescuespeed = nil, + rescuestopboat = nil, + HeloFuel0 = nil, + rtb = nil, + carrierstop = nil, +} + +--- Class version. +-- @field #string version +RESCUEHELO.version="1.0.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Add messages for rescue mission. +-- TODO: Add option to stop carrier while rescue operation is in progress? Done but NOT working! +-- DONE: Write documenation. +-- DONE: Add option to deactivate the rescueing. +-- DONE: Possibility to add already present/spawned aircraft, e.g. for warehouse. +-- DONE: Add rescue event when aircraft crashes. +-- DONE: Make offset input parameter. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new RESCUEHELO object. +-- @param #RESCUEHELO self +-- @param Wrapper.Unit#UNIT carrierunit Carrier unit object or simply the unit name. +-- @param #string helogroupname Name of the late activated rescue helo template group. +-- @return #RESCUEHELO RESCUEHELO object. +function RESCUEHELO:New(carrierunit, helogroupname) + + -- Inherit everthing from FSM class. + local self = BASE:Inherit(self, FSM:New()) -- #RESCUEHELO + + -- Catch case when just the unit name is passed. + if type(carrierunit)=="string" then + self.carrier=UNIT:FindByName(carrierunit) + else + self.carrier=carrierunit + end + + -- Carrier type. + self.carriertype=self.carrier:GetTypeName() + + -- Helo group name. + self.helogroupname=helogroupname + + -- Log ID. + self.lid=string.format("RESCUEHELO %s |", self.carrier:GetName()) + + -- Init defaults. + self:SetHomeBase(AIRBASE:FindByName(self.carrier:GetName())) + self:SetTakeoffHot() + self:SetLowFuelThreshold() + self:SetAltitude() + self:SetOffsetX() + self:SetOffsetZ() + self:SetRespawnOn() + self:SetRescueOn() + self:SetRescueZone() + self:SetRescueHoverSpeed() + self:SetRescueDuration() + self:SetRescueStopBoatOff() + + -- Some more. + self.rtb=false + self.carrierstop=false + + -- Debug trace. + if false then + BASE:TraceOnOff(true) + BASE:TraceClass(self.ClassName) + BASE:TraceLevel(1) + end + + ----------------------- + --- FSM Transitions --- + ----------------------- + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") + self:AddTransition("Running", "Rescue", "Rescuing") + self:AddTransition("Running", "RTB", "Returning") + self:AddTransition("Rescuing", "RTB", "Returning") + self:AddTransition("*", "Run", "Running") + self:AddTransition("*", "Status", "*") + self:AddTransition("*", "Stop", "Stopped") + + + --- Triggers the FSM event "Start" that starts the rescue helo. Initializes parameters and starts event handlers. + -- @function [parent=#RESCUEHELO] Start + -- @param #RESCUEHELO self + + --- Triggers the FSM event "Start" that starts the rescue helo after a delay. Initializes parameters and starts event handlers. + -- @function [parent=#RESCUEHELO] __Start + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Rescue" that sends the helo on a rescue mission to a specifc coordinate. + -- @function [parent=#RESCUEHELO] Rescue + -- @param #RESCUEHELO self + -- @param Core.Point#COORDINATE RescueCoord Coordinate where the resue mission takes place. + + --- Triggers the delayed FSM event "Rescue" that sends the helo on a rescue mission to a specifc coordinate. + -- @function [parent=#RESCUEHELO] __Rescue + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + -- @param Core.Point#COORDINATE RescueCoord Coordinate where the resue mission takes place. + + --- On after "Rescue" event user function. Called when a the the helo goes on a rescue mission. + -- @function [parent=#RESCUEHELO] OnAfterRescue + -- @param #RESCUEHELO self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Point#COORDINATE RescueCoord Crash site where the rescue operation takes place. + + + --- Triggers the FSM event "RTB" that sends the helo home. + -- @function [parent=#RESCUEHELO] RTB + -- @param #RESCUEHELO self + -- @param Wrapper.Airbase#AIRBASE airbase The airbase to return to. Default is the home base. + + --- Triggers the FSM event "RTB" that sends the helo home after a delay. + -- @function [parent=#RESCUEHELO] __RTB + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase to return to. Default is the home base. + + --- On after "RTB" event user function. Called when a the the helo returns to its home base. + -- @function [parent=#RESCUEHELO] OnAfterRTB + -- @param #RESCUEHELO self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase to return to. Default is the home base. + + + --- Triggers the FSM event "Run". + -- @function [parent=#RESCUEHELO] Run + -- @param #RESCUEHELO self + + --- Triggers the delayed FSM event "Run". + -- @function [parent=#RESCUEHELO] __Run + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Status" that updates the helo status. + -- @function [parent=#RESCUEHELO] Status + -- @param #RESCUEHELO self + + --- Triggers the delayed FSM event "Status" that updates the helo status. + -- @function [parent=#RESCUEHELO] __Status + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Stop" that stops the rescue helo. Event handlers are stopped. + -- @function [parent=#RESCUEHELO] Stop + -- @param #RESCUEHELO self + + --- Triggers the FSM event "Stop" that stops the rescue helo after a delay. Event handlers are stopped. + -- @function [parent=#RESCUEHELO] __Stop + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set low fuel state of helo. When fuel is below this threshold, the helo will RTB or be respawned if takeoff type is in air. +-- @param #RESCUEHELO self +-- @param #number threshold Low fuel threshold in percent. Default 5%. +-- @return #RESCUEHELO self +function RESCUEHELO:SetLowFuelThreshold(threshold) + self.lowfuel=threshold or 5 + return self +end + +--- Set home airbase of the helo. This is the airbase where the helo is spawned (if not in air) and will go when it is out of fuel. +-- @param #RESCUEHELO self +-- @param Wrapper.Airbase#AIRBASE airbase The home airbase. Can be the airbase name (passed as a string) or a Moose AIRBASE object. +-- @return #RESCUEHELO self +function RESCUEHELO:SetHomeBase(airbase) + if type(airbase)=="string" then + self.airbase=AIRBASE:FindByName(airbase) + else + self.airbase=airbase + end + if not self.airbase then + self:E(self.lid.."ERROR: Airbase is nil!") + end + return self +end + +--- Set rescue zone radius. Crashed or ejected units inside this radius of the carrier will be rescued if possible. +-- @param #RESCUEHELO self +-- @param #number radius Radius of rescue zone in nautical miles. Default is 15 NM. +-- @return #RESCUEHELO self +function RESCUEHELO:SetRescueZone(radius) + radius=UTILS.NMToMeters(radius or 15) + self.rescuezone=ZONE_UNIT:New("Rescue Zone", self.carrier, radius) + return self +end + +--- Set rescue hover speed. +-- @param #RESCUEHELO self +-- @param #number speed Speed in knots. Default 5 kts. +-- @return #RESCUEHELO self +function RESCUEHELO:SetRescueHoverSpeed(speed) + self.rescuespeed=UTILS.KnotsToMps(speed or 5) + return self +end + +--- Set rescue duration. This is the time it takes to rescue a pilot at the crash site. +-- @param #RESCUEHELO self +-- @param #number duration Duration in minutes. Default 5 min. +-- @return #RESCUEHELO self +function RESCUEHELO:SetRescueDuration(duration) + self.rescueduration=(duration or 5)*60 + return self +end + +--- Activate rescue option. Crashed and ejected pilots will be rescued. This is the default setting. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetRescueOn() + self.rescueon=true + return self +end + +--- Deactivate rescue option. Crashed and ejected pilots will not be rescued. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetRescueOff() + self.rescueon=false + return self +end + +--- Stop carrier during rescue operations. NOT WORKING! +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetRescueStopBoatOn() + self.rescuestopboat=true + return self +end + +--- Do not stop carrier during rescue operations. This is the default setting. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetRescueStopBoatOff() + self.rescuestopboat=false + return self +end + + +--- Set takeoff type. +-- @param #RESCUEHELO self +-- @param #number takeofftype Takeoff type. Default SPAWN.Takeoff.Hot. +-- @return #RESCUEHELO self +function RESCUEHELO:SetTakeoff(takeofftype) + self.takeoff=takeofftype or SPAWN.Takeoff.Hot + return self +end + +--- Set takeoff with engines running (hot). +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetTakeoffHot() + self:SetTakeoff(SPAWN.Takeoff.Hot) + return self +end + +--- Set takeoff with engines off (cold). +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetTakeoffCold() + self:SetTakeoff(SPAWN.Takeoff.Cold) + return self +end + +--- Set takeoff in air near the carrier. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetTakeoffAir() + self:SetTakeoff(SPAWN.Takeoff.Air) + return self +end + +--- Set altitude of helo. +-- @param #RESCUEHELO self +-- @param #number alt Altitude in meters. Default 70 m. +-- @return #RESCUEHELO self +function RESCUEHELO:SetAltitude(alt) + self.altitude=alt or 70 + return self +end + +--- Set offset parallel to orienation of carrier. +-- @param #RESCUEHELO self +-- @param #number distance Offset distance in meters. Default 200 m. +-- @return #RESCUEHELO self +function RESCUEHELO:SetOffsetX(distance) + self.offsetX=distance or 200 + return self +end + +--- Set offset perpendicular to orientation to carrier. +-- @param #RESCUEHELO self +-- @param #number distance Offset distance in meters. Default 100 m. +-- @return #RESCUEHELO self +function RESCUEHELO:SetOffsetZ(distance) + self.offsetZ=distance or 100 + return self +end + + +--- Enable respawning of helo. Note that this is the default behaviour. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetRespawnOn() + self.respawn=true + return self +end + +--- Disable respawning of helo. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetRespawnOff() + self.respawn=false + return self +end + +--- Set whether helo shall be respawned or not. +-- @param #RESCUEHELO self +-- @param #boolean switch If true (or nil), helo will be respawned. If false, helo will not be respawned. +-- @return #RESCUEHELO self +function RESCUEHELO:SetRespawnOnOff(switch) + if switch==nil or switch==true then + self.respawn=true + else + self.respawn=false + end + return self +end + +--- Helo will be respawned in air, even it was initially spawned on the carrier. +-- So only the first spawn will be on the carrier while all subsequent spawns will happen in air. +-- This allows for undisrupted operations and less problems on the carrier deck. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetRespawnInAir() + self.respawninair=true + return self +end + +--- Use an uncontrolled aircraft already present in the mission rather than spawning a new helo as initial rescue helo. +-- This can be useful when interfaced with, e.g., a warehouse. +-- The group name is the one specified in the @{#RESCUEHELO.New} function. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetUseUncontrolledAircraft() + self.uncontrolledac=true + return self +end + +--- Activate debug mode. Display debug messages on screen. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetDebugModeON() + self.Debug=true + return self +end + +--- Deactivate debug mode. This is also the default setting. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetDebugModeOFF() + self.Debug=false + return self +end + +--- Check if helo is returning to base. +-- @param #RESCUEHELO self +-- @return #boolean If true, helo is returning to base. +function RESCUEHELO:IsReturning() + return self:is("Returning") +end + +--- Check if helo is operating. +-- @param #RESCUEHELO self +-- @return #boolean If true, helo is operating. +function RESCUEHELO:IsRunning() + return self:is("Running") +end + +--- Check if helo is on a rescue mission. +-- @param #RESCUEHELO self +-- @return #boolean If true, helo is rescuing somebody. +function RESCUEHELO:IsRescuing() + return self:is("Rescuing") +end + +--- Check if FMS was stopped. +-- @param #RESCUEHELO self +-- @return #boolean If true, is stopped. +function RESCUEHELO:IsStopped() + return self:is("Stopped") +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- EVENT functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Handle landing event of rescue helo. +-- @param #RESCUEHELO self +-- @param Core.Event#EVENTDATA EventData Event data. +function RESCUEHELO:OnEventLand(EventData) + local group=EventData.IniGroup --Wrapper.Group#GROUP + + if group:IsAlive() then + + -- Group name that landed. + local groupname=group:GetName() + + -- Check that it was our helo that landed. + if groupname==self.helo:GetName() then + + -- Respawn the Helo. + local text=string.format("Respawning rescue helo group %s at home base.", groupname) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + if self:IsRescuing() then + + self:T(string.format("Rescue helo %s returned from rescue operation.", groupname)) + + end + + -- Check if takeoff air or respawn in air is set. Landing event should not happen unless the helo was on a rescue mission. + if self.takeoff==SPAWN.Takeoff.Air or self.respawninair then + + if self:IsRescuing() then + + self:T(string.format("Rescue helo %s returned from rescue operation.", groupname)) + + -- Respawn helo at current airbase. + self.helo=group:RespawnAtCurrentAirbase() + + else + + self:T2(string.format("WARNING: Rescue helo %s landed. This should not happen for Takeoff=Air or respawninair=true unless a rescue operation finished.", groupname)) + + -- Respawn helo at current airbase anyway. + if self.respawn then + self.helo=group:RespawnAtCurrentAirbase() + end + + end + + else + + -- Respawn helo at current airbase. + if self.respawn then + self.helo=group:RespawnAtCurrentAirbase() + end + + end + + -- Restart the formation. + self:__Run(10) + + end + end +end + +--- A unit crashed or a player ejected. +-- @param #RESCUEHELO self +-- @param Core.Event#EVENTDATA EventData Event data. +function RESCUEHELO:_OnEventCrashOrEject(EventData) + self:F2({eventdata=EventData}) + + -- NOTE: Careful here. Eject and crash events will probably happen for the same unit! + + -- Check that there is an initiating unit in the event data. + if EventData and EventData.IniUnit then + + -- Crashed or ejected unit. + local unit=EventData.IniUnit + local unitname=tostring(EventData.IniUnitName) + + -- Check that it was not the rescue helo itself that crashed. + if EventData.IniGroupName~=self.helo:GetName() then + + -- Debug. + local text=string.format("Unit %s crashed or ejected.", unitname) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- Unit "alive" and in our rescue zone. + if unit:IsAlive() and unit:IsInZone(self.rescuezone) then + + -- Get coordinate of crashed unit. + local coord=unit:GetCoordinate() + + -- Debug mark on map. + if self.Debug then + coord:MarkToCoalition(self.lid..string.format("Crash site of unit %s.", unitname), self.helo:GetCoalition()) + end + + -- Check that coalition is the same. + local rightcoalition=EventData.IniGroup:GetCoalition()==self.helo:GetCoalition() + + -- Only rescue if helo is "running" and not, e.g., rescuing already. + if self:IsRunning() and self.rescueon and rightcoalition then + self:Rescue(coord) + end + + end + + else + + self:E(self.lid..string.format("Rescue helo %s crashed!", unitname)) + + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM states +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. Starts the warehouse. Addes event handlers and schedules status updates of reqests and queue. +-- @param #RESCUEHELO self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RESCUEHELO:onafterStart(From, Event, To) + + -- Events are handled my MOOSE. + local text=string.format("Starting Rescue Helo Formation v%s for carrier unit %s of type %s.", RESCUEHELO.version, self.carrier:GetName(), self.carriertype) + self:I(self.lid..text) + + -- Handle events. + --self:HandleEvent(EVENTS.Birth) + self:HandleEvent(EVENTS.Land) + self:HandleEvent(EVENTS.Crash, self._OnEventCrashOrEject) + self:HandleEvent(EVENTS.Ejection, self._OnEventCrashOrEject) + + -- Delay before formation is started. + local delay=120 + + -- Set unique alias for spawn. + local helogroupalias=string.format("%s_%s", self.helogroupname, self.carrier:GetName()) + + -- Spawn helo. We need to introduce an alias in case this class is used twice. This would confuse the spawn routine. + local Spawn=SPAWN:NewWithAlias(self.helogroupname, helogroupalias) + + -- Spawn in air or at airbase. + if self.takeoff==SPAWN.Takeoff.Air then + + -- Carrier heading + local hdg=self.carrier:GetHeading() + + -- Spawn distance in front of carrier. + local dist=UTILS.NMToMeters(0.2) + + -- Coordinate behind the carrier. Altitude at least 100 meters for spawning because it drops down a bit. + local Carrier=self.carrier:GetCoordinate():Translate(dist, hdg):SetAltitude(math.max(100, self.altitude)) + + -- Orientation of spawned group. + Spawn:InitHeading(hdg) + + -- Spawn at coordinate. + self.helo=Spawn:SpawnFromCoordinate(Carrier) + + -- Start formation in 1 seconds + delay=1 + + else + + -- Check if an uncontrolled helo group was requested. + if self.useuncontrolled then + + -- Use an uncontrolled aircraft group. + self.helo=GROUP:FindByName(self.helogroupname) + + if self.helo:IsAlive() then + + -- Start uncontrolled group. + self.helo:StartUncontrolled() + + -- Delay before formation is started. + delay=60 + + else + -- No group of that name! + self:E(string.format("ERROR: No uncontrolled (alive) rescue helo group with name %s could be found!", self.helogroupname)) + return + end + + else + + -- Spawn at airbase. + self.helo=Spawn:SpawnAtAirbase(self.airbase, self.takeoff) + + -- Delay before formation is started. + if self.takeoff==SPAWN.Takeoff.Runway then + delay=5 + elseif self.takeoff==SPAWN.Takeoff.Hot then + delay=30 + elseif self.takeoff==SPAWN.Takeoff.Cold then + delay=60 + end + + end + + end + + -- Set of group(s) to follow Mother. + self.followset=SET_GROUP:New() + self.followset:AddGroup(self.helo) + + -- Get initial fuel. + self.HeloFuel0=self.helo:GetFuel() + + -- Define AI Formation object. + self.formation=AI_FORMATION:New(self.carrier, self.followset, "Helo Formation with Carrier", "Follow Carrier at given parameters.") + + -- Formation parameters. + self.formation:FormationCenterWing(-self.offsetX, 50, math.abs(self.altitude), 50, self.offsetZ, 50) + + -- Start formation FSM. + self.formation:__Start(delay) + + -- Init status check + self:__Status(1) +end + +--- On after Status event. Checks player status. +-- @param #RESCUEHELO self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RESCUEHELO:onafterStatus(From, Event, To) + + -- Get current time. + local time=timer.getTime() + + -- Get relative fuel wrt to initial fuel of helo (DCS bug https://forums.eagle.ru/showthread.php?t=223712) + local fuel=self.helo:GetFuel()/self.HeloFuel0*100 + + -- Report current fuel. + local text=string.format("Rescue Helo %s: state=%s fuel=%.1f", self.helo:GetName(), self:GetState(), fuel) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- Check if helo is running and not RTBing already or rescuing. + if self:IsRunning() then + + -- Check if fuel is low. + if fuel1 then @@ -680,3 +716,116 @@ function UTILS.VecCross(a, b) return {x=a.y*b.z - a.z*b.y, y=a.z*b.x - a.x*b.z, z=a.x*b.y - a.y*b.x} end +--- Calculate the difference between two 3D vectors by substracting the x,y,z components from each other. +-- @param DCS#Vec3 a Vector in 3D with x, y, z components. +-- @param DCS#Vec3 b Vector in 3D with x, y, z components. +-- @return DCS#Vec3 Vector c=a-b with c(i)=a(i)-b(i), i=x,y,z. +function UTILS.VecSubstract(a, b) + return {x=a.x-b.x, y=a.y-b.y, z=a.z-b.z} +end + +--- Calculate the angle between two 3D vectors. +-- @param DCS#Vec3 a Vector in 3D with x, y, z components. +-- @param DCS#Vec3 b Vector in 3D with x, y, z components. +-- @return #number Angle alpha between and b in degrees. alpha=acos(a*b)/(|a||b|), (* denotes the dot product). +function UTILS.VecAngle(a, b) + local alpha=math.acos(UTILS.VecDot(a,b)/(UTILS.VecNorm(a)*UTILS.VecNorm(b))) + return math.deg(alpha) +end + +--- Rotate 3D vector in the 2D (x,z) plane. y-component (usually altitude) unchanged. +-- @param DCS#Vec3 a Vector in 3D with x, y, z components. +-- @param #number angle Rotation angle in degrees. +-- @return DCS#Vec3 Vector rotated in the (x,z) plane. +function UTILS.Rotate2D(a, angle) + + local phi=math.rad(angle) + + local x=a.z + local y=a.x + + local Z=x*math.cos(phi)-y*math.sin(phi) + local X=x*math.sin(phi)+y*math.cos(phi) + local Y=a.y + + local A={x=X, y=Y, z=Z} + + return A +end + + + +--- Converts a TACAN Channel/Mode couple into a frequency in Hz. +-- @param #number TACANChannel The TACAN channel, i.e. the 10 in "10X". +-- @param #string TACANMode The TACAN mode, i.e. the "X" in "10X". +-- @return #number Frequency in Hz or #nil if parameters are invalid. +function UTILS.TACANToFrequency(TACANChannel, TACANMode) + + if type(TACANChannel) ~= "number" then + return nil -- error in arguments + end + if TACANMode ~= "X" and TACANMode ~= "Y" then + return nil -- error in arguments + end + +-- This code is largely based on ED's code, in DCS World\Scripts\World\Radio\BeaconTypes.lua, line 137. +-- I have no idea what it does but it seems to work + local A = 1151 -- 'X', channel >= 64 + local B = 64 -- channel >= 64 + + if TACANChannel < 64 then + B = 1 + end + + if TACANMode == 'Y' then + A = 1025 + if TACANChannel < 64 then + A = 1088 + end + else -- 'X' + if TACANChannel < 64 then + A = 962 + end + end + + return (A + TACANChannel - B) * 1000000 +end + + +--- Returns the DCS map/theatre as optained by env.mission.theatre +-- @return #string DCS map name . +function UTILS.GetDCSMap() + return env.mission.theatre +end + +--- Returns the magnetic declination of the map. +-- Returned values for the current maps are: +-- +-- * Caucasus +6 (East), year ~ 2011 +-- * NTTR +12 (East), year ~ 2011 +-- * Normandy -10 (West), year ~ 1944 +-- * Persian Gulf +2 (East), year ~ 2011 +-- @param #string map (Optional) Map for which the declination is returned. Default is from env.mission.theatre +-- @return #number Declination in degrees. +function UTILS.GetMagneticDeclination(map) + + -- Map. + map=map or UTILS.GetDCSMap() + + local declination=0 + if map==DCSMAP.Caucasus then + declination=6 + elseif map==DCSMAP.NTTR then + declination=12 + elseif map==DCSMAP.Normandy then + declination=-10 + elseif map==DCSMAP.PersianGulf then + declination=2 + else + declination=0 + end + + return declination +end + + diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index 71b020489..04fd9e93f 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -238,6 +238,8 @@ AIRBASE.Normandy = { -- * AIRBASE.PersianGulf.Sharjah_Intl -- * AIRBASE.PersianGulf.Shiraz_International_Airport -- * AIRBASE.PersianGulf.Kerman_Airport +-- * AIRBASE.PersianGulf.Jiroft_Airport +-- * AIRBASE.PersianGulf.Lavan_Island_Airport -- @field PersianGulf AIRBASE.PersianGulf = { ["Fujairah_Intl"] = "Fujairah Intl", @@ -259,6 +261,8 @@ AIRBASE.PersianGulf = { ["Sharjah_Intl"] = "Sharjah Intl", ["Shiraz_International_Airport"] = "Shiraz International Airport", ["Kerman_Airport"] = "Kerman Airport", + ["Jiroft_Airport"] = "Jiroft Airport", + ["Lavan_Island_Airport"] = "Lavan Island Airport", } --- AIRBASE.ParkingSpot ".Coordinate, ".TerminalID", ".TerminalType", ".TOAC", ".Free", ".TerminalID0", ".DistToRwy". diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index f917d2535..d037d734f 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -148,7 +148,7 @@ -- * @{#CONTROLLABLE.OptionROEReturnFirePossible} -- * @{#CONTROLLABLE.OptionROEEvadeFirePossible} -- --- ## 5.2) Rule on thread: +-- ## 5.2) Reaction On Thread: -- -- * @{#CONTROLLABLE.OptionROTNoReaction} -- * @{#CONTROLLABLE.OptionROTPassiveDefense} @@ -347,16 +347,26 @@ function CONTROLLABLE:PushTask( DCSTask, WaitTime ) local DCSControllable = self:GetDCSObject() if DCSControllable then - local Controller = self:_GetController() + + local DCSControllableName = self:GetName() -- When a controllable SPAWNs, it takes about a second to get the controllable in the simulator. Setting tasks to unspawned controllables provides unexpected results. -- Therefore we schedule the functions to set the mission and options for the Controllable. - -- Controller:pushTask( DCSTask ) + -- Controller:pushTask( DCSTask ) + + local function PushTask( Controller, DCSTask ) + if self and self:IsAlive() then + local Controller = self:_GetController() + Controller:pushTask( DCSTask ) + else + BASE:E( { DCSControllableName .. " is not alive anymore.", DCSTask = DCSTask } ) + end + end - if WaitTime then - self.TaskScheduler:Schedule( Controller, Controller.pushTask, { DCSTask }, WaitTime ) + if not WaitTime or WaitTime == 0 then + PushTask( self, DCSTask ) else - Controller:pushTask( DCSTask ) + self.TaskScheduler:Schedule( self, PushTask, { DCSTask }, WaitTime ) end return self @@ -367,7 +377,7 @@ end --- Clearing the Task Queue and Setting the Task on the queue from the controllable. -- @param #CONTROLLABLE self --- @param #DCS.Task DCSTask DCS Task array. +-- @param DCS#Task DCSTask DCS Task array. -- @param #number WaitTime Time in seconds, before the task is set. -- @return Wrapper.Controllable#CONTROLLABLE self function CONTROLLABLE:SetTask( DCSTask, WaitTime ) @@ -547,9 +557,9 @@ end ---- Executes a command action +--- Executes a command action for the CONTROLLABLE. -- @param #CONTROLLABLE self --- @param DCS#Command DCSCommand +-- @param DCS#Command DCSCommand The command to be executed. -- @return #CONTROLLABLE self function CONTROLLABLE:SetCommand( DCSCommand ) self:F2( DCSCommand ) @@ -637,9 +647,122 @@ function CONTROLLABLE:StartUncontrolled(delay) return self end +--- Give the CONTROLLABLE the command to activate a beacon. See [DCS_command_activateBeacon](https://wiki.hoggitworld.com/view/DCS_command_activateBeacon) on Hoggit. +-- For specific beacons like TACAN use the more convenient @{#BEACON} class. +-- Note that a controllable can only have one beacon activated at a time with the execption of ICLS. +-- @param #CONTROLLABLE self +-- @param Core.Radio#BEACON.Type Type Beacon type (VOR, DME, TACAN, RSBN, ILS etc). +-- @param Core.Radio#BEACON.System System Beacon system (VOR, DME, TACAN, RSBN, ILS etc). +-- @param #number Frequency Frequency in Hz the beacon is running on. Use @{#UTILS.TACANToFrequency} to generate a frequency for TACAN beacons. +-- @param #number UnitID The ID of the unit the beacon is attached to. Usefull if more units are in one group. +-- @param #number Channel Channel the beacon is using. For, e.g. TACAN beacons. +-- @param #string ModeChannel The TACAN mode of the beacon, i.e. "X" or "Y". +-- @param #boolean AA If true, create and Air-Air beacon. IF nil, automatically set if CONTROLLABLE depending on whether unit is and aircraft or not. +-- @param #string Callsign Morse code identification callsign. +-- @param #boolean Bearing If true, beacon provides bearing information - if supported by the unit the beacon is attached to. +-- @param #number Delay (Optional) Delay in seconds before the beacon is activated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandActivateBeacon(Type, System, Frequency, UnitID, Channel, ModeChannel, AA, Callsign, Bearing, Delay) + + AA=AA or self:IsAir() + UnitID=UnitID or self:GetID() + + -- Command + local CommandActivateBeacon= { + id = "ActivateBeacon", + params = { + ["type"] = Type, + ["system"] = System, + ["frequency"] = Frequency, + ["unitId"] = UnitID, + ["channel"] = Channel, + ["modeChannel"] = ModeChannel, + ["AA"] = AA, + ["callsign"] = Callsign, + ["bearing"] = Bearing, + } + } + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandActivateBeacon, {self, Type, System, Frequency, UnitID, Channel, ModeChannel, AA, Callsign, Bearing}, Delay) + else + self:SetCommand(CommandActivateBeacon) + end + + return self +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 #string Callsign Morse code identification callsign. +-- @param #number Delay (Optional) Delay in seconds before the ICLS is deactivated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandActivateICLS(Channel, UnitID, Callsign, Delay) + self:F() + + -- Command to activate ICLS system. + local CommandActivateICLS= { + id = "ActivateICLS", + params= { + ["type"] = BEACON.Type.ICLS, + ["channel"] = Channel, + ["unitId"] = UnitID, + ["callsign"] = Callsign, + } + } + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandActivateICLS, {self}, Delay) + else + self:SetCommand(CommandActivateICLS) + 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. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandDeactivateBeacon(Delay) + self:F() + + -- Command to deactivate + local CommandDeactivateBeacon={id='DeactivateBeacon', params={}} + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandActivateBeacon, {self}, Delay) + else + self:SetCommand(CommandDeactivateBeacon) + 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. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandDeactivateICLS(Delay) + self:F() + + -- Command to deactivate + local CommandDeactivateICLS={id='DeactivateICLS', params={}} + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandDeactivateICLS, {self}, Delay) + else + self:SetCommand(CommandDeactivateICLS) + end + + return self +end + + -- TASKS FOR AIR CONTROLLABLES - - --- (AIR) Attack a Controllable. -- @param #CONTROLLABLE self -- @param Wrapper.Controllable#CONTROLLABLE AttackGroup The Controllable to be attacked. @@ -877,6 +1000,38 @@ function CONTROLLABLE:TaskOrbitCircleAtVec2( Point, Altitude, Speed ) return DCSTask end +--- (AIR) Orbit at a position with at a given altitude and speed. Optionally, a race track pattern can be specified. +-- @param #CONTROLLABLE self +-- @param Core.Point#COORDINATE Coord Coordinate at which the CONTROLLABLE orbits. +-- @param #number Altitude Altitude in meters of the orbit pattern. +-- @param #number Speed Speed [m/s] flying the orbit pattern +-- @param Core.Point#COORDINATE CoordRaceTrack (Optional) If this coordinate is specified, the CONTROLLABLE will fly a race-track pattern using this and the initial coordinate. +-- @return #CONTROLLABLE self +function CONTROLLABLE:TaskOrbit(Coord, Altitude, Speed, CoordRaceTrack) + + local Pattern=AI.Task.OrbitPattern.CIRCLE + + local P1=Coord:GetVec2() + local P2=nil + if CoordRaceTrack then + Pattern=AI.Task.OrbitPattern.RACE_TRACK + P2=CoordRaceTrack:GetVec2() + end + + local Task = { + id = 'Orbit', + params = { + pattern = Pattern, + point = P1, + point2 = P2, + speed = Speed, + altitude = Altitude, + } + } + + return Task +end + --- (AIR) Orbit at the current position of the first unit of the controllable at a specified alititude. -- @param #CONTROLLABLE self -- @param #number Altitude The altitude [m] to hold the position. @@ -965,11 +1120,7 @@ function CONTROLLABLE:TaskRefueling() -- params = {} -- } - local DCSTask - DCSTask = { id = 'Refueling', - params = { - }, - }, + local DCSTask={id='Refueling', params={}} self:T3( { DCSTask } ) return DCSTask @@ -2109,7 +2260,7 @@ do -- Route methods FromCoordinate = FromCoordinate or self:GetCoordinate() -- Get path and path length on road including the end points (From and To). - local PathOnRoad, LengthOnRoad=FromCoordinate:GetPathOnRoad(ToCoordinate, true) + local PathOnRoad, LengthOnRoad, GotPath =FromCoordinate:GetPathOnRoad(ToCoordinate, true) -- Get the length only(!) on the road. local _,LengthRoad=FromCoordinate:GetPathOnRoad(ToCoordinate, false) @@ -2121,7 +2272,7 @@ do -- Route methods -- Calculate the direct distance between the initial and final points. local LengthDirect=FromCoordinate:Get2DDistance(ToCoordinate) - if PathOnRoad then + if GotPath then -- Off road part of the rout: Total=OffRoad+OnRoad. LengthOffRoad=LengthOnRoad-LengthRoad @@ -2144,7 +2295,7 @@ do -- Route methods local canroad=false -- Check if a valid path on road could be found. - if PathOnRoad and LengthDirect > 2000 then -- if the length of the movement is less than 1 km, drive directly. + if GotPath and LengthDirect > 2000 then -- if the length of the movement is less than 1 km, drive directly. -- Check whether the road is very long compared to direct path. if LongRoad and Shortcut then @@ -3073,6 +3224,3 @@ function CONTROLLABLE:IsAirPlane() return nil end - - --- Message APIs \ No newline at end of file diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index e67d61605..81b203ca9 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -325,7 +325,7 @@ end -- So all event listeners will catch the destroy event of this group for each unit in the group. -- To raise these events, provide the `GenerateEvent` parameter. -- @param #GROUP self --- @param #boolean GenerateEvent true if you want to generate a crash or dead event for each unit. +-- @param #boolean GenerateEvent If true, a crash or dead event for each unit is generated. If false, if no event is triggered. If nil, a RemoveUnit event is triggered. -- @usage -- -- Air unit example: destroy the Helicopter and generate a S_EVENT_CRASH for each unit in the Helicopter group. -- Helicopter = GROUP:FindByName( "Helicopter" ) @@ -1477,29 +1477,61 @@ end -- -- @param Wrapper.Group#GROUP self -- @param #table Template (optional) The template of the Group retrieved with GROUP:GetTemplate(). If the template is not provided, the template will be retrieved of the group itself. +-- @param #boolean Reset Reset positons if TRUE. +-- @return Wrapper.Group#GROUP self function GROUP:Respawn( Template, Reset ) - if not Template then - Template = self:GetTemplate() - end + -- Given template or get old. + Template = Template or self:GetTemplate() + + -- Get correct heading. + local function _Heading(course) + local h + if course<=180 then + h=math.rad(course) + else + h=-math.rad(360-course) + end + return h + end + -- First check if group is alive. if self:IsAlive() then + + -- Respawn zone. local Zone = self.InitRespawnZone -- Core.Zone#ZONE + + -- Zone position or current group position. local Vec3 = Zone and Zone:GetVec3() or self:GetVec3() + + -- From point of the template. local From = { x = Template.x, y = Template.y } + + -- X, Y Template.x = Vec3.x Template.y = Vec3.z + --Template.x = nil --Template.y = nil + -- Debug number of units. self:F( #Template.units ) + + -- Reset position etc? if Reset == true then + + -- Loop over units in group. for UnitID, UnitData in pairs( self:GetUnits() ) do local GroupUnit = UnitData -- Wrapper.Unit#UNIT - self:F( GroupUnit:GetName() ) + self:F(GroupUnit:GetName()) + if GroupUnit:IsAlive() then - self:F( "Alive" ) - local GroupUnitVec3 = GroupUnit:GetVec3() + self:F("Alive") + + -- Get unit position vector. + local GroupUnitVec3 = GroupUnit:GetVec3() + + -- Check if respawn zone is set. if Zone then if self.InitRespawnRandomizePositionZone then GroupUnitVec3 = Zone:GetRandomVec3() @@ -1512,17 +1544,38 @@ function GROUP:Respawn( Template, Reset ) end end + -- Altitude Template.units[UnitID].alt = self.InitRespawnHeight and self.InitRespawnHeight or GroupUnitVec3.y - Template.units[UnitID].x = ( Template.units[UnitID].x - From.x ) + GroupUnitVec3.x -- Keep the original x position of the template and translate to the new position. - Template.units[UnitID].y = ( Template.units[UnitID].y - From.y ) + GroupUnitVec3.z -- Keep the original z position of the template and translate to the new position. - Template.units[UnitID].heading = self.InitRespawnHeading and self.InitRespawnHeading or GroupUnit:GetHeading() + + -- Unit position. Why not simply take the current positon? + if Zone then + Template.units[UnitID].x = ( Template.units[UnitID].x - From.x ) + GroupUnitVec3.x -- Keep the original x position of the template and translate to the new position. + Template.units[UnitID].y = ( Template.units[UnitID].y - From.y ) + GroupUnitVec3.z -- Keep the original z position of the template and translate to the new position. + else + Template.units[UnitID].x=GroupUnitVec3.x + Template.units[UnitID].y=GroupUnitVec3.z + end + + -- Set heading. + Template.units[UnitID].heading = _Heading(self.InitRespawnHeading and self.InitRespawnHeading or GroupUnit:GetHeading()) + Template.units[UnitID].psi = -Template.units[UnitID].heading + + -- Debug. self:F( { UnitID, Template.units[UnitID], Template.units[UnitID] } ) end end - else + + else -- Reset=false or nil + + -- Loop over template units. for UnitID, TemplateUnitData in pairs( Template.units ) do + self:F( "Reset" ) + + -- Position from template. local GroupUnitVec3 = { x = TemplateUnitData.x, y = TemplateUnitData.alt, z = TemplateUnitData.y } + + -- Respawn zone position. if Zone then if self.InitRespawnRandomizePositionZone then GroupUnitVec3 = Zone:GetRandomVec3() @@ -1535,23 +1588,36 @@ function GROUP:Respawn( Template, Reset ) end end + -- Set altitude. Template.units[UnitID].alt = self.InitRespawnHeight and self.InitRespawnHeight or GroupUnitVec3.y + + -- Unit position. Template.units[UnitID].x = ( Template.units[UnitID].x - From.x ) + GroupUnitVec3.x -- Keep the original x position of the template and translate to the new position. Template.units[UnitID].y = ( Template.units[UnitID].y - From.y ) + GroupUnitVec3.z -- Keep the original z position of the template and translate to the new position. + + -- Heading Template.units[UnitID].heading = self.InitRespawnHeading and self.InitRespawnHeading or TemplateUnitData.heading + + -- Debug. self:F( { UnitID, Template.units[UnitID], Template.units[UnitID] } ) end + end end - self:Destroy() - _DATABASE:Spawn( Template ) + -- Destroy old group. Dont trigger any dead/crash events since this is a respawn. + self:Destroy(false) + self:T({Template=Template}) + + -- Spawn new group. + _DATABASE:Spawn(Template) + + -- Reset events. self:ResetEvents() return self - end @@ -1652,6 +1718,7 @@ function GROUP:RespawnAtCurrentAirbase(SpawnTemplate, Takeoff, Uncontrolled) -- -- Destroy old group. self:Destroy(false) + -- Spawn new group. _DATABASE:Spawn( SpawnTemplate ) -- Reset events. @@ -1800,8 +1867,8 @@ do -- Route methods -- -- @param #GROUP self -- @param Wrapper.Airbase#AIRBASE RTBAirbase (optional) The @{Wrapper.Airbase} to return to. If blank, the controllable will return to the nearest friendly airbase. - -- @param #number Speed (optional) The Speed, if no Speed is given, the maximum Speed of the first unit is selected. - -- @return #GROUP + -- @param #number Speed (optional) The Speed, if no Speed is given, 80% of maximum Speed of the group is selected. + -- @return #GROUP self function GROUP:RouteRTB( RTBAirbase, Speed ) self:F( { RTBAirbase:GetName(), Speed } ) @@ -1811,17 +1878,19 @@ do -- Route methods if RTBAirbase then + -- If speed is not given take 80% of max speed. + local Speed=Speed or self:GetSpeedMax()*0.8 + + --[[ local GroupPoint = self:GetVec2() - local GroupVelocity = self:GetUnit(1):GetDesc().speedMax - + local GroupVelocity = self:GetUnit(1):GetDesc().speedMax local PointFrom = {} PointFrom.x = GroupPoint.x PointFrom.y = GroupPoint.y PointFrom.type = "Turning Point" PointFrom.action = "Turning Point" PointFrom.speed = GroupVelocity - - + local PointTo = {} local AirbasePointVec2 = RTBAirbase:GetPointVec2() local AirbaseAirPoint = AirbasePointVec2:WaypointAir( @@ -1832,21 +1901,42 @@ do -- Route methods ) AirbaseAirPoint["airdromeId"] = RTBAirbase:GetID() - AirbaseAirPoint["speed_locked"] = true, + AirbaseAirPoint["speed_locked"] = true + ]] + + -- Curent (from) waypoint. + local coord=self:GetCoordinate() + local PointFrom=coord:WaypointAirTurningPoint(nil, Speed) + + -- Airbase coordinate. + --local PointAirbase=RTBAirbase:GetCoordinate():SetAltitude(coord.y):WaypointAirTurningPoint(nil ,Speed) + + -- Landing waypoint. More general than prev version since it should also work with FAPRS and ships. + local PointLanding=RTBAirbase:GetCoordinate():WaypointAirLanding(Speed, RTBAirbase) + + -- Waypoint table. + local Points={PointFrom, PointLanding} + --local Points={PointFrom, PointAirbase, PointLanding} - self:F(AirbaseAirPoint ) - - local Points = { PointFrom, AirbaseAirPoint } - - self:T3( Points ) + -- Debug info. + self:T3(Points) - local Template = self:GetTemplate() - Template.route.points = Points - self:Respawn( Template ) - - --self:Route( Points ) + -- Get group template. + local Template=self:GetTemplate() + + -- Set route points. + Template.route.points=Points + + -- Respawn the group. + self:Respawn(Template, true) + + -- Route the group or this will not work. + self:Route(Points) else + + -- Clear all tasks. self:ClearTasks() + end end diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index fef546f38..ee8c9b011 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -656,6 +656,14 @@ function POSITIONABLE:GetVelocityMPS() return 0 end +--- Returns the POSITIONABLE velocity in knots. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return #number The velocity in knots. +function POSITIONABLE:GetVelocityKNOTS() + self:F2( self.PositionableName ) + return UTILS.MpsToKnots(self:GetVelocityMPS()) +end + --- Returns the Angle of Attack of a positionable. -- @param Wrapper.Positionable#POSITIONABLE self -- @return #number Angle of attack in degrees. @@ -706,8 +714,8 @@ end --- Returns the unit's climb or descent angle. -- @param Wrapper.Positionable#POSITIONABLE self --- @return #number Climb or descent angle in degrees. -function POSITIONABLE:GetClimbAnge() +-- @return #number Climb or descent angle in degrees. Or 0 if velocity vector norm is zero (or nil). Or nil, if the position of the POSITIONABLE returns nil. +function POSITIONABLE:GetClimbAngle() -- Get position of the unit. local unitpos = self:GetPosition() @@ -719,10 +727,17 @@ function POSITIONABLE:GetClimbAnge() if unitvel and UTILS.VecNorm(unitvel)~=0 then - return math.asin(unitvel.y/UTILS.VecNorm(unitvel)) - + -- Calculate climb angle. + local angle=math.asin(unitvel.y/UTILS.VecNorm(unitvel)) + + -- Return angle in degrees. + return math.deg(angle) + else + return 0 end end + + return nil end --- Returns the pitch angle of a unit. diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index f1e5ee77c..16278596a 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -902,29 +902,31 @@ end function UNIT:InAir() self:F2( self.UnitName ) + -- Get DCS unit object. local DCSUnit = self:GetDCSObject() --DCS#Unit if DCSUnit then --- Implementation of workaround. The original code is below. --- This to simulate the landing on buildings. - - local UnitInAir = true + -- Get DCS result of whether unit is in air or not. + local UnitInAir = DCSUnit:inAir() + + -- Get unit category. local UnitCategory = DCSUnit:getDesc().category - if UnitCategory == Unit.Category.HELICOPTER then + + -- If DCS says that it is in air, check if this is really the case, since we might have landed on a building where inAir()=true but actually is not. + -- This is a workaround since DCS currently does not acknoledge that helos land on buildings. + -- Note however, that the velocity check will fail if the ground is moving, e.g. on an aircraft carrier! + if UnitInAir==true and UnitCategory == Unit.Category.HELICOPTER then local VelocityVec3 = DCSUnit:getVelocity() - local Velocity = ( VelocityVec3.x ^ 2 + VelocityVec3.y ^ 2 + VelocityVec3.z ^ 2 ) ^ 0.5 -- in meters / sec + local Velocity = UTILS.VecNorm(VelocityVec3) local Coordinate = DCSUnit:getPoint() local LandHeight = land.getHeight( { x = Coordinate.x, y = Coordinate.z } ) local Height = Coordinate.y - LandHeight if Velocity < 1 and Height <= 60 then UnitInAir = false end - else - UnitInAir = DCSUnit:inAir() end - - + self:T3( UnitInAir ) return UnitInAir end diff --git a/Moose Setup/Moose.files b/Moose Setup/Moose.files index 11d2da7db..485569cdc 100644 --- a/Moose Setup/Moose.files +++ b/Moose Setup/Moose.files @@ -59,6 +59,10 @@ Functional/Suppression.lua Functional/PseudoATC.lua Functional/Warehouse.lua +Ops/Airboss.lua +Ops/RecoveryTanker.lua +Ops/RescueHelo.lua + AI/AI_Balancer.lua AI/AI_Air.lua AI/AI_A2A.lua