From d52867c54173357094df78be7e29ab1106e8a732 Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 9 Apr 2020 18:07:41 +0200 Subject: [PATCH] Ops --- Moose Development/Moose/Functional/SWAPR.lua | 730 ------- .../Functional/{SimpleScore.lua => Score.lua} | 0 Moose Development/Moose/Modules.lua | 3 +- Moose Development/Moose/Ops/AirWing.lua | 81 +- Moose Development/Moose/Ops/Auftrag.lua | 30 +- Moose Development/Moose/Ops/FlightGroup.lua | 126 +- .../Moose/Ops/RecoveryTanker2.lua | 1698 +++++++++++++++++ Moose Development/Moose/Ops/RescueHelo2.lua | 1220 ++++++++++++ Moose Development/Moose/Ops/Squadron.lua | 47 +- Moose Development/Moose/Ops/WingCommander.lua | 26 - Moose Development/Moose/Wrapper/DCSTask.lua | 0 Moose Development/Moose/Wrapper/Mark.lua | 345 ++++ 12 files changed, 3454 insertions(+), 852 deletions(-) delete mode 100644 Moose Development/Moose/Functional/SWAPR.lua rename Moose Development/Moose/Functional/{SimpleScore.lua => Score.lua} (100%) create mode 100644 Moose Development/Moose/Ops/RecoveryTanker2.lua create mode 100644 Moose Development/Moose/Ops/RescueHelo2.lua create mode 100644 Moose Development/Moose/Wrapper/DCSTask.lua create mode 100644 Moose Development/Moose/Wrapper/Mark.lua diff --git a/Moose Development/Moose/Functional/SWAPR.lua b/Moose Development/Moose/Functional/SWAPR.lua deleted file mode 100644 index e13043acd..000000000 --- a/Moose Development/Moose/Functional/SWAPR.lua +++ /dev/null @@ -1,730 +0,0 @@ ---- **Functional** - (R2.5) - Replace client aircraft by statics until a player enters. --- --- Make the DCS world a bit more lively! --- --- **Main Features:** --- --- * Easy! --- --- ## Known (DCS) Issues --- --- * Does not support clients on ships. --- * Does not support Harriers and helicopters on parking spots below a shelter. --- --- === --- --- ### Author: **Hardcard** (aka Goreuncle on the MOOSE discord) --- ### Contributions: funkyfranky --- --- @module Functional.Swapr --- @image Functional_SWAPR.png - ---- SWAPR class. --- @type SWAPR --- @field #string ClassName Name of the class. --- @field #boolean Debug Debug mode on/off. --- @field #string lid Log debug id text. --- @field Core.Set#SET_CLIENT clientset Set of clients to be replaced. --- @field #table statics Table of static objects. --- @field #table statictemplate Table of static template --- @extends Core.Fsm#FSM - ---- Swap clients and statics --- --- === --- --- ![Banner Image](..\Presentations\SWAPR\SWAPR_Main.png) --- --- # SWAPR Concept --- --- SWAPR will enable you to easily spawn static aircraft on client slots. When a player enters a client slot, the static object is removed and the player aircraft spawned. --- This makes the airbases look a lot more alive. --- --- # Simple Script --- --- The basic script is very simple and consists of only two lines: --- --- local clientset=SET_CLIENT:New():FilterActive(false):FilterOnce() --- swapr=SWAPR:New(clientset) --- --- The first line defines a set of clients (here all) that will be replaced by statics. --- The second lines initiates the SWAPR script. That's all. --- --- **Note** that Harrier and helicopter clients are automatically removed from the client set if they are placed on a sheltered parking spot. Otherwise the statics would be spawned --- on top of the shelter roof. --- --- Similarly, clients on ships are removed as these would be spawned at sea level and not on the ship itself. --- --- All these are *DCS side restriction* when spawning statics. --- --- # 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 @{#SWAPR} class should have the string "SWAPR" 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("SWAPR") --- --- 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 @{#SWAPR.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 #SWAPR -SWAPR = { - ClassName = "SWAPR", - Debug = false, - lid = nil, - clientset = nil, - statics = {}, - statictemplate = {}, -} - ---- Class version. --- @field #string version -SWAPR.version="0.0.2" - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO list -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - --- DONE: Check for clients on ships ==> get airdrome id from first route point. --- DONE: Check that harrier and helo clients are not spawned in shelters ==> get parking spot type for these units in _Prepare() --- TODO: Check what happens if statics are destroyed. --- TODO: Check what happens if clients eject, crash or are shot down. --- TODO: Check that parking spot is not blocked by other aircraft or statics when spawning a static replacement. --- TODO: Add FSM events, e.g. static spawned, static destroyed etc. --- TODO: Add user functions, e.g. for defining the static FARP offset. --- TODO: Safe/load static templates to/from disk. - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Constructor -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Create a new SWAPR object. --- @param #SWAPR self --- @param Core.Set#SET_CLIENT clientset (Optional) Set of clients to be replaced. Default all. --- @return #SWAPR SWAPR object. -function SWAPR:New(clientset) - - -- Inherit everthing from FSM class. - local self = BASE:Inherit(self, FSM:New()) -- #SWAPR - - -- Carrier type. - self.clientset=clientset or SET_CLIENT:New():FilterActive(false):FilterOnce() - - -- Log ID. - self.lid=string.format("SWAPR | ") - - -- Debug trace. - if false then - self.Debug=true - BASE:TraceOnOff(true) - BASE:TraceClass(self.ClassName) - BASE:TraceLevel(1) - end - - -- Events are handled directly by DCS. - self:T(self.lid.."Events are handled directly by DCS.") - world.addEventHandler(self) - - self:HandleEvent(EVENTS.RemoveUnit) - - -- Prepare stuff by temporarity spawning aircraft to determine the heading. - self:_Prepare() - - ----------------------- - --- FSM Transitions --- - ----------------------- - - -- Start State. - self:SetStartState("Stopped") - - -- Add FSM transitions. - -- From State --> Event --> To State - self:AddTransition("Stopped", "Start", "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=#SWAPR] Start - -- @param #SWAPR self - - --- Triggers the FSM event "Start" that starts the rescue helo after a delay. Initializes parameters and starts event handlers. - -- @function [parent=#SWAPR] __Start - -- @param #SWAPR self - -- @param #number delay Delay in seconds. - - - --- Triggers the FSM event "Status" that updates the helo status. - -- @function [parent=#SWAPR] Status - -- @param #SWAPR self - - --- Triggers the delayed FSM event "Status" that updates the helo status. - -- @function [parent=#SWAPR] __Status - -- @param #SWAPR self - -- @param #number delay Delay in seconds. - - - --- Triggers the FSM event "Stop" that stops the rescue helo. Event handlers are stopped. - -- @function [parent=#SWAPR] Stop - -- @param #SWAPR self - - --- Triggers the FSM event "Stop" that stops the rescue helo after a delay. Event handlers are stopped. - -- @function [parent=#SWAPR] __Stop - -- @param #SWAPR self - -- @param #number delay Delay in seconds. - - return self -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- User functions -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Event handler -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - - ---- General event handler. --- @param #SWAPR self --- @param #table Event DCS event table. -function SWAPR:onEvent(Event) - self:F3(Event) - - if Event == nil or Event.initiator == nil then - self:T3("Skipping onEvent. Event or Event.initiator unknown.") - return true - end - - if Unit.getByName(Event.initiator:getName()) == nil then - self:T3("Skipping onEvent. Initiator unit name unknown.") - return true - end - - -- Get unit name and category. - local IniUnitName = Event.initiator:getName() - local IniCategory = Event.initiator:getCategory() - - -- Get client. - local client=self.clientset:FindClient(IniUnitName) - - -- This is not an event involving a client in the defined set. - if not client then - self:T3(self.lid.."Event not associated with client aircraft!") - return - end - - if Event.id==EVENTS.Birth then - - ----------------- - -- BIRTH EVENT -- - ----------------- - - if IniCategory==1 then - - --------------- - -- UNIT BORN -- - --------------- - - local IniDCSGroup = Event.initiator:getGroup() - local IniGroupName = Event.initiator:getGroup():getName() - - -- Debug info. - self:T(self.lid..string.format("Event birth of unit %s of group %s", tostring(IniUnitName), tostring(IniGroupName))) - - -- Get unit. - local unit=UNIT:FindByName(IniUnitName) - - if unit then - - --unit:SmokeGreen() - - -- Group and name. - local group=unit:GetGroup() - local groupname=group:GetName() - - -- Check if this is prepare step to determine the heading. - if string.find(groupname, "_SWAPR") then - --unit:SmokeBlue() - - -- Get info necessary for the static template. - local heading=unit:GetHeading() - local coord=unit:GetCoordinate() - local actype=unit:GetTypeName() - local livery=self:_GetLiveryFromTemplate(IniUnitName) - local airbase=self:_GetAirbaseFromTemplate(IniUnitName) - - -- FARPS suck! - if airbase:GetAirbaseCategory()==Airbase.Category.HELIPAD then - local parkingid=self:_GetParkingFromTemplate(IniUnitName) - self:T2(self.lid..string.format("FARP parking id=%s", tostring(parkingid))) - if parkingid then - local spot=airbase:GetParkingSpotData(parkingid) - coord=spot.Coordinate - coord.z=coord.z+5 - coord.x=coord.x+5 - end - end - - -- Add static template to table. - local statictemplate=self:_AddStaticTemplate(IniUnitName, actype, coord.x, coord.z, heading, unit:GetCountry(), livery) - - -- Destroy unit ==> triggers a remove unit event. - unit:Destroy() - - -- Replace aircraft by static. - --self:_Aircraft2Static(unit) - - else - self:I(self.lid..string.format("Client %s spawned!", IniUnitName)) - - -- Get static that is in place of the spawned client. - local static=self.statics[IniUnitName] --Wrapper.Static#STATIC - - -- Remove static. - if static then - self:I(self.lid..string.format("Destroying static %s!", IniUnitName)) - - -- Looks like the MOOSE Destroy function is not fast enough! - static:destroy() - self.statics[IniUnitName]=nil - else - self:E(self.lid..string.format("WARNING: No static %s to destroy!", IniUnitName)) - end - - end - - end - - elseif IniCategory==3 then - - ----------------- - -- STATIC BORN -- - ----------------- - - self:I(self.lid..string.format("Event birth of static %s", tostring(IniUnitName))) - - -- WORKS! - local static=STATIC:FindByName(IniUnitName, true) - - -- Add spawned static to table. - --self.statics[IniUnitName]=static - self.statics[IniUnitName]=Event.initiator - - end - - elseif Event.id==EVENTS.PlayerLeaveUnit then - - ----------------- - -- PLAYER LEFT -- - ----------------- - - self:I(self.lid..string.format("Event player leave unit %s", IniUnitName)) - - -- Spawn static. Needs to be delayed a tad or DCS crashes to desktop. - local statictemplate=self.statictemplate[IniUnitName] - if statictemplate then - self:ScheduleOnce(0.1, SWAPR._SpawnStaticAircraft, self, statictemplate) - end - end - -end - ---- General event handler. --- @param #SWAPR self --- @param Core.Event#EVENTDATA EventData Event data table. -function SWAPR:OnEventRemoveUnit(EventData) - self:I(EventData) - - if EventData and EventData.IniUnitName then - - -- Debug info. - self:I(self.lid..string.format("Event removed unit %s!", EventData.IniUnitName)) - - -- Spawn static aircraft. - local statictemplate=self.statictemplate[EventData.IniUnitName] - if statictemplate then - self:ScheduleOnce(0.1, SWAPR._SpawnStaticAircraft, self, statictemplate) - end - - end - -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Spawn functions -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Add template to static table. --- @param #SWAPR self --- @param #string name Name of the static. --- @param #string actype Type of the aircraft. --- @param #number x X coordinate of spawn place. --- @param #number y Y coordinate of spawn place. --- @param #number heading Heading of static. --- @param #number country Country ID of static. --- @param #string livery Livery ID of the static. --- @return #table Static template. -function SWAPR:_AddStaticTemplate(name, actype, x, y, heading, country, livery) - - -- Heading is in rad not degrees! - local headingrad=0 - if heading then - headingrad=math.rad(heading) - end - - -- Static template table. - local static={ - livery_id=livery, - heading=headingrad, - type=actype, - name=name, - y=y , - x=x , - CountryID=country, - } - - -- Debug info. - self:T2({statictemplate=static}) - - self.statictemplate[name]=static - - return static -end - ---- General event handler. --- @param #SWAPR self --- @param #table template The static template. -function SWAPR:_SpawnStaticAircraft(template) - self:I({statictemplate=template}) - - if template and not self.statics[template.name] then - - -- Spawn static. - local static=coalition.addStaticObject(template.CountryID, template) - - -- Debug info. - self:T2({spawnedstatic=static}) - self:T(self.lid..string.format("Spawned static %s", template.name)) - - else - self:T3(self.lid.."WARNING: Static template is nil!") - end - -end - ---- Replace a whole aircraft group by statics. --- @param #SWAPR self --- @param Wrapper.Group#GROUP group -function SWAPR:_AircraftGroup2Statics(group) - - -- Get the group template. - local grouptemplate=group:GetTemplate() - - -- Debug info. - self:T3({grouptemplate=grouptemplate}) - - for i,_unit in pairs(group:GetUnits()) do - local unit=_unit --Wrapper.Unit#UNIT - - -- Get unit name. - local unitname=unit:GetName() - - local statictemplate=self.statictemplate[unitname] - local static=self.statics[unitname] - - if statictemplate and not static then - - -- Destroy the unit. - unit:Destroy() - - -- Spawn static aircraft instead. - self:_SpawnStaticAircraft(statictemplate) - end - - end -end - ---- Replace a single aircraft unit by static. --- @param #SWAPR self --- @param Wrapper.Unit#UNIT unit The unit to be replaced. -function SWAPR:_Aircraft2Static(unit) - - if unit and unit:IsAlive() then - - -- Get the group template. - local grouptemplate=unit:GetGroup():GetTemplate() - - -- Debug info. - self:T3({grouptemplate=grouptemplate}) - - -- Get unit name. - local unitname=unit:GetName() - - -- Get the static template. - local statictemplate=self.statictemplate[unitname] - - -- Get the static to check if there already is one. - local static=self.statics[unitname] - - if statictemplate and not static then - - -- Destroy the unit ==> triggers a RemoveUnit event. - unit:Destroy() - - -- Spawn static aircraft instead. - self:_SpawnStaticAircraft(statictemplate) - end - - end -end - - ---- Temporarily spawn uncontrolled aircraft at all client spots to get the correct headings. --- @param #SWAPR self -function SWAPR:_Prepare() - - local remove={} - for _,_client in pairs(self.clientset:GetSet()) do - local client=_client --Wrapper.Client#CLIENT - - -- Unit name - local unitname=client.ClientName - - -- Get airbase if any. - local airbase=self:_GetAirbaseFromTemplate(unitname) - - -- First check that this is not a cliened spawned in air or on a ship. - if airbase==nil then - -- Spawned in air ==> remove! - self:I(self.lid..string.format("Removing client %s because of air start.", unitname)) - table.insert(remove, client) - elseif airbase:GetAirbaseCategory()==Airbase.Category.SHIP then - self:I(self.lid..string.format("Removing client %s because spawned on ship.", unitname)) - table.insert(remove, client) - else - - if true then - - --- - -- Spawn a group to get parameters in particular the heading on the parking spot as this is not correct in the template. - --- - - -- Check that harriers are not spawned in shelters because they would appear on top of them. - -- TODO: Need to do the same of helos? - local _continue=true - if self:_GetTypeFromTemplate(unitname)=="AV8BNA" then - local parkingid=self:_GetParkingFromTemplate(unitname) - if parkingid then - env.info(string.format("Harrier parking spot id %d", parkingid)) - local spot=airbase:GetParkingSpotData(parkingid) - if spot and spot.TerminalType==AIRBASE.TerminalType.Shelter then - _continue=false - table.insert(remove, client) - end - end - end - - if _continue then - - -- Client group name. - local groupname=_DATABASE.Templates.Units[unitname].GroupName - - -- Client group template copy. - local grouptemplate=UTILS.DeepCopy(_DATABASE:GetGroupTemplate(groupname)) - - -- Nillify the group ID. - grouptemplate.groupId=nil - - -- Set skill. - for i=1,#grouptemplate.units do - local unit=grouptemplate.units[i] - unit.skill="Good" - -- Nillify the unit ID. - unit.unitId=nil - end - - -- Uncontrolled - grouptemplate.uncontrolled=true - - -- Add _SWAPR to the group name so that we find it in birth event. - grouptemplate.name=string.format("%s_SWAPR", groupname) - - -- Debug info. - self:I({grouptemplate=grouptemplate}) - - -- Spawn group. - local group=_DATABASE:Spawn(grouptemplate) - - end - - else - - --- - -- Get all info from template. Unfortunately, heading is always 0 in the template, i.e. all statics would face due North! - --- - - local livery=self:_GetLiveryFromTemplate(unitname) - local x,y=self:_GetPositionFromTemplate(unitname) - local actype=self:_GetTypeFromTemplate(unitname) - local heading=self:_GetHeadingFromTemplate(unitname) - - -- TODO: country! - local template=self:_AddStaticTemplate(unitname, actype, x, y, heading, 1, livery) - - self:_SpawnStaticAircraft(template) - - end - - end - - end - - for _,_client in pairs(remove) do - local client=_client --Wrapper.Client#CLIENT - self.clientset:RemoveClientsByName(client.ClientName) - end - - self:I(self.lid..string.format("Number of clients left after prepare = %d", self.clientset:Count())) - -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Misc functions -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Get livery from unit. --- @param #SWAPR self --- @param #string unitname Name of the unit. --- @return #string Livery ID. -function SWAPR:_GetLiveryFromTemplate(unitname) - - local grouptemplate=_DATABASE:GetGroupTemplateFromUnitName(unitname) - - for _,unit in pairs(grouptemplate.units) do - if unit.name==unitname then - return tostring(unit.livery_id) - end - end - - return nil -end - ---- Get livery from unit. --- @param #SWAPR self --- @param #string unitname Name of the unit. --- @return #number X coordinate. --- @return #number Y coordinate. -function SWAPR:_GetPositionFromTemplate(unitname) - - local grouptemplate=_DATABASE:GetGroupTemplateFromUnitName(unitname) - - for _,unit in pairs(grouptemplate.units) do - if unit.name==unitname then - return tonumber(unit.x), tonumber(unit.y) - end - end - - return nil, nil -end - - ---- Get livery from unit. --- @param #SWAPR self --- @param #string unitname Name of the unit. --- @return #string Aircraft type. -function SWAPR:_GetTypeFromTemplate(unitname) - - local grouptemplate=_DATABASE:GetGroupTemplateFromUnitName(unitname) - - for _,unit in pairs(grouptemplate.units) do - if unit.name==unitname then - return tostring(unit.type) - end - end - - return nil -end - ---- Get livery from unit. --- @param #SWAPR self --- @param #string unitname Name of the unit. --- @return #number Heading in degrees. -function SWAPR:_GetHeadingFromTemplate(unitname) - - local grouptemplate=_DATABASE:GetGroupTemplateFromUnitName(unitname) - - for _,unit in pairs(grouptemplate.units) do - if unit.name==unitname then - return tonumber(unit.heading) - end - end - - return nil -end - ---- Get airbase from template. --- @param #SWAPR self --- @param #string unitname Name of the unit. --- @return Wrapper.Airbase#AIRBASE The airbase object or nil. -function SWAPR:_GetAirbaseFromTemplate(unitname) - - local grouptemplate=_DATABASE:GetGroupTemplateFromUnitName(unitname) - - -- First waypoint. - local wp=grouptemplate.route.points[1] - - local airbase=nil --Wrapper.Airbase#AIRBASE - local id=-666 - if wp.airdromeId then - id=tonumber(wp.airdromeId) - elseif wp.helipadId then - id=tonumber(wp.helipadId) - end - - -- Find airbase by its id. - airbase=AIRBASE:FindByID(id) - - -- Debug info. - if airbase then - self:T3(self.lid..string.format("Found airbase %s for unit %s, id=%d", airbase:GetName(), unitname, id)) - else - self:T3(self.lid..string.format("Found NO airbase for unit %s, id=%d", unitname,id)) - end - - return airbase -end - ---- Get parking id from template. --- @param #SWAPR self --- @param #string unitname Name of the unit. --- @return #number Parking id or nil. -function SWAPR:_GetParkingFromTemplate(unitname) - - local grouptemplate=_DATABASE:GetGroupTemplateFromUnitName(unitname) - - for _,unit in pairs(grouptemplate.units) do - if unit.name==unitname then - return tonumber(unit.parking) - end - end - - return nil -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - diff --git a/Moose Development/Moose/Functional/SimpleScore.lua b/Moose Development/Moose/Functional/Score.lua similarity index 100% rename from Moose Development/Moose/Functional/SimpleScore.lua rename to Moose Development/Moose/Functional/Score.lua diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index 11fb03348..c06619bc6 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -37,6 +37,8 @@ __Moose.Include( 'Scripts/Moose/Wrapper/Client.lua' ) __Moose.Include( 'Scripts/Moose/Wrapper/Static.lua' ) __Moose.Include( 'Scripts/Moose/Wrapper/Airbase.lua' ) __Moose.Include( 'Scripts/Moose/Wrapper/Scenery.lua' ) +__Moose.Include( 'Scripts/Moose/Wrapper/Mark.lua' ) +__Moose.Include( 'Scripts/Moose/Wrapper/DCSTask.lua' ) __Moose.Include( 'Scripts/Moose/Cargo/Cargo.lua' ) __Moose.Include( 'Scripts/Moose/Cargo/CargoUnit.lua' ) @@ -67,7 +69,6 @@ __Moose.Include( 'Scripts/Moose/Functional/Fox.lua' ) __Moose.Include( 'Scripts/Moose/Functional/RAT2.lua' ) __Moose.Include( 'Scripts/Moose/Functional/RatCraft.lua' ) __Moose.Include( 'Scripts/Moose/Functional/FlightModelData.lua' ) -__Moose.Include( 'Scripts/Moose/Functional/SWAPR.lua' ) __Moose.Include( 'Scripts/Moose/Ops/Airboss.lua' ) __Moose.Include( 'Scripts/Moose/Ops/RecoveryTanker.lua' ) diff --git a/Moose Development/Moose/Ops/AirWing.lua b/Moose Development/Moose/Ops/AirWing.lua index b5ed1ca9a..77a100d25 100644 --- a/Moose Development/Moose/Ops/AirWing.lua +++ b/Moose Development/Moose/Ops/AirWing.lua @@ -32,6 +32,10 @@ -- @field #table pointsTANKER Table of Tanker points. -- @field #table pointsAWACS Table of AWACS points. -- @field Ops.WingCommander#WINGCOMMANDER wingcommander The wing commander responsible for this airwing. +-- +-- @field Ops.RescueHelo#RESCUEHELO rescuehelo The rescue helo. +-- @field Ops.RecoveryTanker#RECOVERYTANKER recoverytanker The recoverytanker. +-- -- @extends Functional.Warehouse#WAREHOUSE --- Be surprised! @@ -79,11 +83,10 @@ AIRWING = { -- @type AIRWING.Payload -- @field #string unitname Name of the unit this pylon was extracted from. -- @field #string aircrafttype Type of aircraft, which can use this payload. --- @field #table missiontypes Mission types for which this payload can be used. +-- @field #table capabilities Mission types and performances for which this payload can be used. -- @field #table pylons Pylon data extracted for the unit template. -- @field #number navail Number of available payloads of this type. -- @field #boolean unlimited If true, this payload is unlimited and does not get consumed. --- @field #number priority Priority of the payload. --- Patrol data. -- @type AIRWING.PatrolData @@ -95,7 +98,7 @@ AIRWING = { --- AIRWING class version. -- @field #string version -AIRWING.version="0.1.5" +AIRWING.version="0.1.6" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ToDo list @@ -103,7 +106,6 @@ AIRWING.version="0.1.5" -- TODO: Spawn in air or hot. -- TODO: Make special request to transfer squadrons to anther airwing (or warehouse). --- TODO: Border zone or even multiple zones. -- TODO: Check that airbase has enough parking spots if a request is BIG. Alternatively, split requests. -- DONE: Add squadrons to warehouse. -- DONE: Build mission queue. @@ -145,6 +147,9 @@ function AIRWING:New(warehousename, airwingname) self.nflightsAWACS=0 self.nflightsTANKERboom=0 self.nflightsTANKERprobe=0 + + self.nflightsRecoveryTanker=0 + self.nflightsRescuehelo=0 ------------------------ --- Pseudo Functions --- @@ -240,32 +245,36 @@ function AIRWING:NewPayload(Unit, MissionTypes, Npayloads, Unlimited, Priority) MissionTypes={MissionTypes} end - -- Add ORBIT for all. - if not self:CheckMissionType(AUFTRAG.Type.ORBIT, MissionTypes) then - table.insert(MissionTypes, AUFTRAG.Type.ORBIT) - end - if Unit then local payload={} --#AIRWING.Payload + --TODO: capability + payload.unitname=Unit:GetName() payload.aircrafttype=Unit:GetTypeName() - payload.missiontypes=MissionTypes or {} + payload.capabilities=MissionTypes or {} payload.pylons=Unit:GetTemplatePayload() payload.navail=Npayloads or 99 payload.unlimited=Unlimited if Unlimited then payload.navail=1 end - payload.priority=Priority or 50 -- Add payload table.insert(self.payloads, payload) + -- Add ORBIT for all. + if not self:CheckMissionType(AUFTRAG.Type.ORBIT, MissionTypes) then + local capability={} --Ops.Auftrag#AUFTRAG.Capability + capability.MissionType=AUFTRAG.Type.ORBIT + capability.Performance=50 + --table.insert(MissionTypes, capability) + end + -- Info self:I(self.lid..string.format("Adding new payload from unit %s for aircraft type %s: N=%d (unlimited=%s), prio=%d, missions: %s", - payload.unitname, payload.aircrafttype, payload.navail, tostring(payload.unlimited), payload.priority, table.concat(payload.missiontypes, ", "))) + payload.unitname, payload.aircrafttype, payload.navail, tostring(payload.unlimited), payload.priority, table.concat(MissionTypes, ", "))) return payload end @@ -763,7 +772,7 @@ function AIRWING:CheckTANKER() for _,_mission in pairs(self.missionqueue) do local mission=_mission --Ops.Auftrag#AUFTRAG - if mission:IsNotOver() and self:CheckMissionType(mission.type, AUFTRAG.Type.TANKER) then + if mission:IsNotOver() and mission.type==AUFTRAG.Type.TANKER then if mission.refuelSystem==0 then Nboom=Nboom+1 elseif mission.refuelSystem==1 then @@ -833,6 +842,13 @@ function AIRWING:CheckAWACS() return self end +--- Check how many AWACS missions are assigned and add number of missing missions. +-- @param #AIRWING self +-- @return #AIRWING self +function AIRWING:CheckRecoveryTanker() + +end + --- Check how many AWACS missions are assigned and add number of missing missions. -- @param #AIRWING self -- @param Ops.FlightGroup#FLIGHTGROUP flightgroup The flightgroup. @@ -912,8 +928,7 @@ function AIRWING:_GetNextMission() if can then -- Optimize the asset selection. Most useful assets will come first. - -- TODO: This could be moved to AUFTRAG, right? - --self:_OptimizeAssetSelection(assets, mission) + self:_OptimizeAssetSelection(assets, mission) -- Check that mission.assets table is clean. if mission.assets and #mission.assets>0 then @@ -968,6 +983,23 @@ function AIRWING:_OptimizeAssetSelection(assets, Mission) end + local function score(Asset) + local asset=Asset --#AIRWING.SquadronAsset + + score=0 + + -- Prefer highly skilled assets. + if asset.skill==AI.Skill.GOOD then + score=score+10 + elseif asset.skill==AI.Skill.HIGH then + score=score+20 + elseif asset.skill==AI.Skill.EXCELLENT then + score=score+30 + end + + + end + -- Sort results table wrt distacance. local function optimize(a, b) local assetA=a --#AIRWING.SquadronAsset @@ -1563,7 +1595,7 @@ function AIRWING:CanMission(Mission) end ---- Returns the mission for a given mission ID (Autragsnummer). +--- Check if a mission type is contained in a list of possible types. -- @param #AIRWING self -- @param #string MissionType The requested mission type. -- @param #table PossibleTypes A table with possible mission types. @@ -1583,6 +1615,23 @@ function AIRWING:CheckMissionType(MissionType, PossibleTypes) return false end +--- Check if a mission type is contained in a list of possible capabilities. +-- @param #AIRWING self +-- @param #string MissionType The requested mission type. +-- @param #table PossibleTypes A table with possible capabilities. +-- @return #boolean If true, the requested mission type is part of the possible mission types. +function AIRWING:CheckMissionCapability(MissionType, Capabilities) + + for _,cap in pairs(Capabilities) do + local capability=cap --Ops.Auftrag#AUFTRAG.Capability + if capability.MissionType==MissionType then + return true + end + end + + return false +end + --- Returns the mission for a given mission ID (Autragsnummer). -- @param #AIRWING self -- @param #number mid Mission ID (Auftragsnummer). diff --git a/Moose Development/Moose/Ops/Auftrag.lua b/Moose Development/Moose/Ops/Auftrag.lua index 9fd5bb450..d575c050e 100644 --- a/Moose Development/Moose/Ops/Auftrag.lua +++ b/Moose Development/Moose/Ops/Auftrag.lua @@ -273,13 +273,18 @@ AUFTRAG.TargetType={ AIRBASE="Airbase", } ---- +--- Target data. -- @type AUFTRAG.TargetData -- @field Wrapper.Positionable#POSITIONABLE Target Target Object. -- @field #string Type Target type: "Group", "Unit", "Static", "Coordinate", "Airbase. -- @field #number Ninital Number of initial targets. -- @field #number Lifepoints Total life points. +--- Mission capability. +-- @type AUFTRAG.Capability +-- @field #string MissionType Type of mission. +-- @field #number Performance Number describing the performance level. The higher the better. + --- Mission success. -- @type AUFTRAG.Success -- @field #string ENGAGED Target was engaged. @@ -2296,6 +2301,29 @@ function AUFTRAG:GetDCSMissionTask() local Vec2=self.transportPickup:GetVec2() local DCStask=CONTROLLABLE.TaskEmbarking(self, Vec2, self.transportGroupSet, Duration, DistributionGroupSet) + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.RESCUEHELO then + + ------------------------- + -- RESCUE HELO Mission -- + ------------------------- + + local DCStask={} + + DCStask.id="Formation" + + local param={} + param.unitname="" + param.offsetX=20 + param.offsetY=20 + param.offsetZ=20 + param.alitude=70 + + DCStask.params=param + + table.insert(DCStasks, DCStask) else self:E(self.lid..string.format("ERROR: Unknown mission task!")) diff --git a/Moose Development/Moose/Ops/FlightGroup.lua b/Moose Development/Moose/Ops/FlightGroup.lua index 781d461dd..88872676b 100644 --- a/Moose Development/Moose/Ops/FlightGroup.lua +++ b/Moose Development/Moose/Ops/FlightGroup.lua @@ -2628,7 +2628,6 @@ function FLIGHTGROUP:onafterPassingWaypoint(From, Event, To, n, N) -- Execute waypoint tasks. if #taskswp>0 then - --self:PushTask(self.group:TaskCombo(taskswp)) self:SetTask(self.group:TaskCombo(taskswp)) end @@ -3389,38 +3388,64 @@ function FLIGHTGROUP:onafterTaskExecute(From, Event, To, Task) -- Task status executing. Task.status=FLIGHTGROUP.TaskStatus.EXECUTING + + if Task.dcstask.id=="Formation" then - -- If task is scheduled (not waypoint) set task. - if Task.type==FLIGHTGROUP.TaskType.SCHEDULED then + -- Set of group(s) to follow Mother. + local followset=SET_GROUP:New():AddGroup(self.group) - local DCStasks={} - if Task.dcstask.id=='ComboTask' then - -- Loop over all combo tasks. - for TaskID, Task in ipairs(Task.dcstask.params.tasks) do - table.insert(DCStasks, Task) - end - else - table.insert(DCStasks, Task.dcstask) - end + local param=Task.dcstask.params + + -- Define AI Formation object. + Task.formation=AI_FORMATION:New(param.carrier, followset, "Formation", "Follow X at given parameters.") + + -- Formation parameters. + Task.formation:FormationCenterWing(-param.offsetX, 50, math.abs(param.altitude), 50, param.offsetZ, 50) + + -- Set follow time interval. + Task.formation:SetFollowTimeInterval(param.dtFollow) + + -- Formation mode. + Task.formation:SetFlightModeFormation(self.group) + + -- Start formation FSM. + Task.formation:Start() + + else - -- Combo task. - local TaskCombo=self.group:TaskCombo(DCStasks) - - -- Stop condition! - local TaskCondition=self.group:TaskCondition(nil, Task.stopflag:GetName(), 1, nil, Task.duration) - - -- Controlled task. - local TaskControlled=self.group:TaskControlled(TaskCombo, TaskCondition) - - -- Task done. - local TaskDone=self.group:TaskFunction("FLIGHTGROUP._TaskDone", self, Task) - - -- Final task. - local TaskFinal=self.group:TaskCombo({TaskControlled, TaskDone}) + -- If task is scheduled (not waypoint) set task. + if Task.type==FLIGHTGROUP.TaskType.SCHEDULED then - -- Set task for group. - self:SetTask(TaskFinal, 1) + local DCStasks={} + if Task.dcstask.id=='ComboTask' then + -- Loop over all combo tasks. + for TaskID, Task in ipairs(Task.dcstask.params.tasks) do + table.insert(DCStasks, Task) + end + else + table.insert(DCStasks, Task.dcstask) + end + + -- Combo task. + local TaskCombo=self.group:TaskCombo(DCStasks) + + -- Stop condition! + local TaskCondition=self.group:TaskCondition(nil, Task.stopflag:GetName(), 1, nil, Task.duration) + + -- Controlled task. + local TaskControlled=self.group:TaskControlled(TaskCombo, TaskCondition) + + -- Task done. + local TaskDone=self.group:TaskFunction("FLIGHTGROUP._TaskDone", self, Task) + + -- Final task. + local TaskFinal=self.group:TaskCombo({TaskControlled, TaskDone}) + -- Set task for group. + self:SetTask(TaskFinal, 1) + + end + end -- Get mission of this task (if any). @@ -3489,6 +3514,11 @@ function FLIGHTGROUP:onafterTaskCancel(From, Event, To, Task) -- Set stop flag. When the flag is true, the _TaskDone function is executed and calls :TaskDone() Task.stopflag:Set(1) + + if Task.dcstask.id=="Formation" then + Task.formation:Stop() + self:TaskDone(Task) + end else @@ -3577,10 +3607,6 @@ function FLIGHTGROUP:onafterTaskDone(From, Event, To, Task) self:_CheckFlightDone(1) end - -- Update route. This is necessary because of the route task being overwritten. But we want to fly to the remaining waypoints. - -- TODO: Since TaskExecute does use PushTask now, it should not be necessary to update the route, right? - --self:__UpdateRoute(-1) - end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -3739,11 +3765,9 @@ function FLIGHTGROUP:onafterMissionCancel(From, Event, To, Mission) -- Note that two things can happen. -- 1.) Flight is still on the way to the waypoint (status should be STARTED). In this case there would not be a current task! -- 2.) Flight already passed the mission waypoint (status should be EXECUTING). + self:TaskCancel(Task) - - -- Set current mission to nil. - --self.currentmission=nil - + else -- Not the current mission. @@ -4450,35 +4474,7 @@ function FLIGHTGROUP:_UpdateWaypointTasks() -- At each waypoint report passing. local TaskPassingWaypoint=self.group:TaskFunction("FLIGHTGROUP._PassingWaypoint", self, i) - table.insert(taskswp, TaskPassingWaypoint) - - - -- For some reason THIS DOES NOT WORK if executed at the last waypoint if it is an AIR WAYPOINT. - -- I have moved it to the onafterpassingwaypoint function instead. - - if false then - - -- Get taks - local tasks=self:GetTasksWaypoint(i) - - for _,task in pairs(tasks) do - local Task=task --#FLIGHTGROUP.Task - - -- Task execute. - table.insert(taskswp, self.group:TaskFunction("FLIGHTGROUP._TaskExecute", self, Task)) - - -- Stop condition if userflag is set to 1. - local TaskCondition=self.group:TaskCondition(nil, Task.stopflag:GetName(), 1, nil, Task.duration) - - -- Controlled task. - table.insert(taskswp, self.group:TaskControlled(Task.dcstask, TaskCondition)) - - -- Task done. - table.insert(taskswp, self.group:TaskFunction("FLIGHTGROUP._TaskDone", self, Task)) - - end - - end + table.insert(taskswp, TaskPassingWaypoint) -- Waypoint task combo. wp.task=self.group:TaskCombo(taskswp) diff --git a/Moose Development/Moose/Ops/RecoveryTanker2.lua b/Moose Development/Moose/Ops/RecoveryTanker2.lua new file mode 100644 index 000000000..e43cd9cc7 --- /dev/null +++ b/Moose Development/Moose/Ops/RecoveryTanker2.lua @@ -0,0 +1,1698 @@ +--- **Ops** - Recovery tanker for carrier operations. +-- +-- Tanker aircraft flying a racetrack pattern overhead an aircraft carrier. +-- +-- **Main Features:** +-- +-- * Regular pattern update with respect to carrier position. +-- * 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 the same carrier. +-- * Multiple 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 Ops_RecoveryTanker.png + +--- 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 RadioFreq Radio frequency in MHz of the tanker. Default 251 MHz. +-- @field #string RadioModu Radio modulation "AM" or "FM". Default "AM". +-- @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 Position of carrier. Used to monitor if carrier significantly changed its position and then update the tanker pattern. +-- @field #string alias Alias of the spawn group. +-- @field #number uid Unique ID of this tanker. +-- @field #boolean awacs If true, the groups gets the enroute task AWACS instead of tanker. +-- @field #number callsignname Number for the callsign name. +-- @field #number callsignnumber Number of the callsign name. +-- @field #string modex Tail number of the tanker. +-- @field #boolean eplrs If true, enable data link, e.g. if used as AWACS. +-- @field #boolean recovery If true, tanker will recover using the AIRBOSS marshal pattern. +-- @field #number terminaltype Terminal type of used parking spots on airbases. +-- @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 engines 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 realistic 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. +-- +-- ## Radio +-- +-- The radio frequency on optionally modulation can be set via the @{#RECOVERYTANKER.SetRadio}(*frequency*, *modulation*) function. The first parameter denotes the radio frequency the tanker uses in MHz. +-- The second parameter is *optional* and sets the modulation to either AM (default) or FM. +-- +-- For example, +-- +-- TexacoStennis:SetRadio(260) +-- +-- will set the frequency of the tanker to 260 MHz AM. +-- +-- **Note** that if this is not set, the tanker frequency will be automatically set to **251 MHz AM**. +-- +-- ## 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 constantly 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. +-- +-- ## Callsign +-- +-- The callsign of the tanker can be set via the @{#RECOVERYTANKER.SetCallsign}(*callsignname*, *callsignnumber*) function. Both parameters are *numbers*. +-- The first parameter *callsignname* defines the name (1=Texaco, 2=Arco, 3=Shell). The second (optional) parameter specifies the first number and has to be between 1-9. +-- Also see [DCS_enum_callsigns](https://wiki.hoggitworld.com/view/DCS_enum_callsigns) and [DCS_command_setCallsign](https://wiki.hoggitworld.com/view/DCS_command_setCallsign). +-- +-- TexacoStennis:SetCAllsign(CALLSIGN.Tanker.Arco) +-- +-- For convenience, MOOSE has a CALLSIGN enumerator introduced. +-- +-- ## AWACS +-- +-- You can use the class also to have an AWACS orbiting overhead the carrier. This requires to add the @{#RECOVERYTANKER.SetAWACS}(*switch*, *eplrs*) function to the script, which sets the enroute tasks AWACS +-- as soon as the aircraft enters its pattern. Note that the EPLRS data link is enabled by default. To disable it, the second parameter *eplrs* must be set to *false*. +-- +-- A simple script could look like this: +-- +-- -- E-2D at USS Stennis spawning in air. +-- local awacsStennis=RECOVERYTANKER:New("USS Stennis", "E2D Group") +-- +-- -- Custom settings: +-- awacsStennis:SetAWACS() +-- awacsStennis:SetCallsign(CALLSIGN.AWACS.Wizard, 1) +-- awacsStennis:SetTakeoffAir() +-- awacsStennis:SetAltitude(20000) +-- awacsStennis:SetRadio(262) +-- awacsStennis:SetTACAN(2, "WIZ") +-- +-- -- Start AWACS. +-- awacsStennis:Start() +-- +-- # 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, + RadioFreq = nil, + RadioModu = 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, + alias = nil, + uid = 0, + awacs = nil, + callsignname = nil, + callsignnumber = nil, + modex = nil, + eplrs = nil, + recovery = nil, + terminaltype = nil, +} + +--- Unique ID (global). +-- @field #number UID Unique ID (global). +_RECOVERYTANKERID=0 + +--- Class version. +-- @field #string version +RECOVERYTANKER.version="1.0.9" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- DONE: 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 documentation. +-- 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 + + -- Increase unique ID. + _RECOVERYTANKERID=_RECOVERYTANKERID+1 + + -- Unique ID of this tanker. + self.uid=_RECOVERYTANKERID + + -- Save self in static object. Easier to retrieve later. + self.carrier:SetState(self.carrier, string.format("RECOVERYTANKER_%d", self.uid) , self) + + -- Set unique spawn alias. + self.alias=string.format("%s_%s_%02d", self.carrier:GetName(), self.tankergroupname, _RECOVERYTANKERID) + + -- Log ID. + self.lid=string.format("RECOVERYTANKER %s | ", self.alias) + + -- 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:SetRadio() + self:SetPatternUpdateDistance() + self:SetPatternUpdateHeading() + self:SetPatternUpdateInterval() + self:SetAWACS(false) + self:SetRecoveryAirboss(false) + self.terminaltype=AIRBASE.TerminalType.OpenMedOrBig + + -- 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("Returning", "Returned", "Returned") -- Tanker has returned to its airbase (i.e. landed). + 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. + + --- On after "Start" event function. Called when FSM is started. + -- @function [parent=#RECOVERYTANKER] OnAfterStart + -- @param #RECOVERYTANKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- 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 "Returned" after the tanker has landed. + -- @function [parent=#RECOVERYTANKER] Returned + -- @param #RECOVERYTANKER self + -- @param Wrapper.Airbase#AIRBASE airbase The airbase the tanker has landed. + + --- Triggers the delayed FSM event "Returned" after the tanker has landed. + -- @function [parent=#RECOVERYTANKER] __Returned + -- @param #RECOVERYTANKER self + -- @param #number delay Delay in seconds. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase the tanker has landed. + + --- On after "Returned" event user function. Called when a the the tanker has landed at an airbase. + -- @function [parent=#RECOVERYTANKER] OnAfterReturned + -- @param #RECOVERYTANKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase the tanker has landed. + + + --- 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. +-- @param #number terminaltype (Optional) The terminal type of parking spots used for spawning at airbases. Default AIRBASE.TerminalType.OpenMedOrBig. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetHomeBase(airbase, terminaltype) + 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 + if terminaltype then + self.terminaltype=terminaltype + end + return self +end + +--- Activate recovery by the AIRBOSS class. Tanker will get a Marshal stack and perform a CASE I, II or III recovery when RTB. +-- @param #RECOVERYTANKER self +-- @param #boolean switch If true or nil, recovery is done by AIRBOSS. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetRecoveryAirboss(switch) + if switch==true or switch==nil then + self.recovery=true + else + self.recovery=false + end + return self +end + +--- Set that the group takes the roll of an AWACS instead of a refueling tanker. +-- @param #RECOVERYTANKER self +-- @param #boolean switch If true or nil, set roll AWACS. +-- @param #boolean eplrs If true or nil, enable EPLRS. If false, EPLRS will be off. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetAWACS(switch, eplrs) + if switch==nil or switch==true then + self.awacs=true + else + self.awacs=false + end + if eplrs==nil or eplrs==true then + self.eplrs=true + else + self.eplrs=false + end + + return self +end + + +--- Set callsign of the tanker group. +-- @param #RECOVERYTANKER self +-- @param #number callsignname Number +-- @param #number callsignnumber Number +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetCallsign(callsignname, callsignnumber) + self.callsignname=callsignname + self.callsignnumber=callsignnumber + return self +end + +--- Set modex (tail number) of the tanker. +-- @param #RECOVERYTANKER self +-- @param #number modex Tail number. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetModex(modex) + self.modex=modex + 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 + +--- Set radio frequency and optionally modulation of the tanker. +-- @param #RECOVERYTANKER self +-- @param #number frequency Radio frequency in MHz. Default 251 MHz. +-- @param #string modulation Radio modulation, either "AM" or "FM". Default "AM". +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetRadio(frequency, modulation) + self.RadioFreq=frequency or 251 + self.RadioModu=modulation or "AM" + 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 has returned to base. +-- @param #RECOVERYTANKER self +-- @return #boolean If true, tanker has returned to base. +function RECOVERYTANKER:IsReturned() + return self:is("Returned") +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 + +--- Alias of tanker spawn group. +-- @param #RECOVERYTANKER self +-- @return #string Alias of the tanker. +function RECOVERYTANKER:GetAlias() + return self.alias +end + +--- Get unit name of the spawned tanker. +-- @param #RECOVERYTANKER self +-- @return #string Name of the tanker unit or nil if it does not exist. +function RECOVERYTANKER:GetUnitName() + local unit=self.tanker:GetUnit(1) + if unit then + return unit:GetName() + end + return nil +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.Land) + self:HandleEvent(EVENTS.Refueling, self._RefuelingStart) --Need explicit functions since OnEventRefueling and OnEventRefuelingStop did not hook! + self:HandleEvent(EVENTS.RefuelingStop, self._RefuelingStop) + self:HandleEvent(EVENTS.Crash, self._OnEventCrashOrDead) + self:HandleEvent(EVENTS.Dead, self._OnEventCrashOrDead) + + -- 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, self.alias) + + -- Set radio frequency and modulation. + Spawn:InitRadioCommsOnOff(true) + Spawn:InitRadioFrequency(self.RadioFreq) + Spawn:InitRadioModulation(self.RadioModu) + Spawn:InitModex(self.modex) + + -- 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.uncontrolledac 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, nil, self.terminaltype) + + end + + end + + -- Initialize route. self.distStern<0! + self:ScheduleOnce(1, self._InitRoute, self, -self.distStern+UTILS.NMToMeters(3)) + + -- Create tanker beacon. + if self.TACANon then + self:_ActivateTACAN(2) + end + + -- Set callsign. + if self.callsignname then + self.tanker:CommandSetCallsign(self.callsignname, self.callsignnumber, 2) + end + + -- Turn EPLRS datalink on. + if self.eplrs then + self.tanker:CommandEPLRS(true, 3) + 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() + + if self.tanker and self.tanker:IsAlive() then + + --------------------- + -- TANKER is ALIVE -- + --------------------- + + -- Get fuel of tanker. + local fuel=self.tanker:GetFuel()*100 + local life=self.tanker:GetUnit(1):GetLife() + local life0=self.tanker:GetUnit(1):GetLife0() + local lifeR=self.tanker:GetUnit(1):GetLifeRelative() + + -- Report fuel and life. + local text=string.format("Recovery tanker %s: state=%s fuel=%.1f, life=%.1f/%.1f=%d", self.tanker:GetName(), self:GetState(), fuel, life, life0, lifeR*100) + self:T(self.lid..text) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + + -- 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 + +--- A unit crashed or died. +-- @param #RECOVERYTANKER self +-- @param Core.Event#EVENTDATA EventData Event data. +function RECOVERYTANKER:_OnEventCrashOrDead(EventData) + self:F2({eventdata=EventData}) + + -- Check that there is an initiating unit in the event data. + if EventData and EventData.IniUnit then + + -- Crashed or dead unit. + local unit=EventData.IniUnit + local unitname=tostring(EventData.IniUnitName) + + -- Check that it was the tanker that crashed. + if EventData.IniGroupName==self.tanker:GetName() then + + -- Error message. + self:E(self.lid..string.format("Recovery tanker %s crashed!", unitname)) + + -- Stop FSM. + self:Stop() + + -- Restart. + if self.respawn then + self:__Start(5) + end + + end + + 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_%d\") ', self.uid) -- 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(nil, self._ActivateTACAN, {self}, delay) + self:ScheduleOnce(delay, RECOVERYTANKER._ActivateTACAN, self) + + else + + -- Get tanker unit. + local unit=self.tanker:GetUnit(1) + + -- Check if unit is alive. + if unit and unit:IsAlive() then + + -- Debug message. + local text=string.format("Activating 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(self.lid.."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/RescueHelo2.lua b/Moose Development/Moose/Ops/RescueHelo2.lua new file mode 100644 index 000000000..779f16329 --- /dev/null +++ b/Moose Development/Moose/Ops/RescueHelo2.lua @@ -0,0 +1,1220 @@ +--- **Ops** - 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. +-- +-- ## Known (DCS) Issues +-- +-- * CH-53E does only report 27.5% fuel even if fuel is set to 100% in the ME. See [bug report](https://forums.eagle.ru/showthread.php?t=223712) +-- * CH-53E does not accept USS Tarawa as landing airbase (even it can be spawned on it). +-- * Helos dont move away from their landing position on carriers. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- ### Contributions: Flightcontrol (@{AI.AI_Formation} class being used here) +-- +-- @module Ops.RescueHelo +-- @image Ops_RescueHelo.png + +--- RESCUEHELO2 class. +-- @type RESCUEHELO2 +-- @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. +-- @field #number hid Unit ID of the helo group. (Global) Running number. +-- @field #string alias Alias of the spawn group. +-- @field #number uid Unique ID of this helo. +-- @field #number modex Tail number of the helo. +-- @field #number dtFollow Follow time update interval in seconds. Default 1.0 sec. +-- @extends Ops.FlightGroup#FLIGHTGROUP + +--- Rescue Helo +-- +-- === +-- +-- ![Banner Image](..\Presentations\RESCUEHELO2\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 rescue 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=RESCUEHELO2:New(UNIT:FindByName("USS Stennis"), "Rescue Helo") +-- RescueheloStennis:Start() +-- +-- The first line will create a new @{#RESCUEHELO2} object via @{#RESCUEHELO2.New} and the second line starts the process by calling @{#RESCUEHELO2.Start}. +-- +-- **NOTE** that it is *very important* to define the RESCUEHELO2 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 engines on the carrier. The mission designer has set option to set the take off type via the @{#RESCUEHELO2.SetTakeoff} function. +-- Or via shortcuts +-- +-- * @{#RESCUEHELO2.SetTakeoffHot}(): Will set the takeoff to hot, which is also the default. +-- * @{#RESCUEHELO2.SetTakeoffCold}(): Will set the takeoff type to cold, i.e. with engines off. +-- * @{#RESCUEHELO2.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=RESCUEHELO2: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 realistic 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 @{#RESCUEHELO2.SetRespawnInAir}() function to command that all subsequent spawning +-- will happen in air. +-- +-- If the helo should no be respawned at all, one can set @{#RESCUEHELO2.SetRespawnOff}(). +-- +-- ## Home Base +-- +-- It is possible to define a "home base" other than the aircraft carrier using the @{#RESCUEHELO2.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=RESCUEHELO2: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 Position +-- +-- The position of the helo relative to the mother ship can be tuned via the functions +-- +-- * @{#RESCUEHELO2.SetAltitude}(*altitude*), where *altitude* is the altitude the helo flies at in meters. Default is 70 meters. +-- * @{#RESCUEHELO2.SetOffsetX}(*distance*), where *distance is the distance in the direction of movement of the carrier. Default is 200 meters. +-- * @{#RESCUEHELO2.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 restricted to aircraft of the same coalition 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 @{#RESCUEHELO2.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 @{#RESCUEHELO2.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 @{#RESCUEHELO2.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 @{#RESCUEHELO2.SetRescueOff}(). +-- +-- # Finite State Machine +-- +-- The implementation uses a Finite State Machine (FSM). This allows the mission designer to hook in to certain events. +-- +-- * @{#RESCUEHELO2.Start}: This eventfunction starts the FMS process and initialized parameters and spawns the helo. DCS event handling is started. +-- * @{#RESCUEHELO2.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. +-- * @{#RESCUEHELO2.Rescue}: This eventfunction commands the helo to go on a rescue operation at a certain coordinate. +-- * @{#RESCUEHELO2.RTB}: This eventsfunction sends the helo to its home base (usually the carrier). This is called once the helo runs low on gas. +-- * @{#RESCUEHELO2.Run}: This eventfunction is called when the helo resumes normal operations and goes back on station. +-- * @{#RESCUEHELO2.Stop}: This eventfunction stops the FSM by unhandling DCS events. +-- +-- The mission designer can capture these events by RESCUEHELO2.OnAfter*Eventname* functions, e.g. @{#RESCUEHELO2.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 @{#RESCUEHELO2} class should have the string "RESCUEHELO2" 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("RESCUEHELO2") +-- +-- 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 @{#RESCUEHELO2.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 #RESCUEHELO2 +RESCUEHELO2 = { + ClassName = "RESCUEHELO2", + 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, + alias = nil, + uid = 0, + modex = nil, + dtFollow = nil, +} + +--- Unique ID (global). +-- @field #number uid Unique ID (global). +_RESCUEHELO2ID=0 + +--- Class version. +-- @field #string version +RESCUEHELO2.version="2.0.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- NOPE: Add messages for rescue mission. +-- NOPE: Add option to stop carrier while rescue operation is in progress? Done but NOT working. Postponed... +-- DONE: Write documentation. +-- DONE: Add option to deactivate the rescuing. +-- 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 RESCUEHELO2 object. +-- @param #RESCUEHELO2 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 #RESCUEHELO2 RESCUEHELO2 object. +function RESCUEHELO2:New(carrierunit, helogroupname) + + -- Inherit everthing from FSM class. + local self = BASE:Inherit(self, FLIGHTGROUP:New(helogroupname, false)) -- #RESCUEHELO2 + + -- 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 + + -- Increase ID. + _RESCUEHELO2ID=_RESCUEHELO2ID+1 + + -- Unique ID of this helo. + self.uid=_RESCUEHELO2ID + + -- Save self in static object. Easier to retrieve later. + self.carrier:SetState(self.carrier, string.format("RESCUEHELO2_%d", self.uid) , self) + + -- Set unique spawn alias. + self.alias=string.format("%s_%s_%02d", self.carrier:GetName(), self.helogroupname, _RESCUEHELO2ID) + + -- Log ID. + self.lid=string.format("RESCUEHELO2 %s | ", self.alias) + + -- 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:SetFollowTimeInterval() + self:SetRescueStopBoatOff() + + -- Some more. + self.rtb=false + self.carrierstop=false + + -- Debug trace. + if false then + self.Debug=true + 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("Returning", "Returned", "Returned") + self:AddTransition("Running", "Run", "Running") + self:AddTransition("Returned", "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=#RESCUEHELO2] Start + -- @param #RESCUEHELO2 self + + --- Triggers the FSM event "Start" that starts the rescue helo after a delay. Initializes parameters and starts event handlers. + -- @function [parent=#RESCUEHELO2] __Start + -- @param #RESCUEHELO2 self + -- @param #number delay Delay in seconds. + + --- On after "Start" event function. Called when FSM is started. + -- @function [parent=#RESCUEHELO2] OnAfterStart + -- @param #RECOVERYTANKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- Triggers the FSM event "Rescue" that sends the helo on a rescue mission to a specifc coordinate. + -- @function [parent=#RESCUEHELO2] Rescue + -- @param #RESCUEHELO2 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=#RESCUEHELO2] __Rescue + -- @param #RESCUEHELO2 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=#RESCUEHELO2] OnAfterRescue + -- @param #RESCUEHELO2 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=#RESCUEHELO2] RTB + -- @param #RESCUEHELO2 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=#RESCUEHELO2] __RTB + -- @param #RESCUEHELO2 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=#RESCUEHELO2] OnAfterRTB + -- @param #RESCUEHELO2 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 "Returned" after the helo has landed. + -- @function [parent=#RESCUEHELO2] Returned + -- @param #RESCUEHELO2 self + -- @param Wrapper.Airbase#AIRBASE airbase The airbase the helo has landed. + + --- Triggers the delayed FSM event "Returned" after the helo has landed. + -- @function [parent=#RESCUEHELO2] __Returned + -- @param #RESCUEHELO2 self + -- @param #number delay Delay in seconds. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase the helo has landed. + + --- On after "Returned" event user function. Called when a the the helo has landed at an airbase. + -- @function [parent=#RESCUEHELO2] OnAfterReturned + -- @param #RESCUEHELO2 self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase the helo has landed. + + + --- Triggers the FSM event "Run". + -- @function [parent=#RESCUEHELO2] Run + -- @param #RESCUEHELO2 self + + --- Triggers the delayed FSM event "Run". + -- @function [parent=#RESCUEHELO2] __Run + -- @param #RESCUEHELO2 self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Status" that updates the helo status. + -- @function [parent=#RESCUEHELO2] Status + -- @param #RESCUEHELO2 self + + --- Triggers the delayed FSM event "Status" that updates the helo status. + -- @function [parent=#RESCUEHELO2] __Status + -- @param #RESCUEHELO2 self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Stop" that stops the rescue helo. Event handlers are stopped. + -- @function [parent=#RESCUEHELO2] Stop + -- @param #RESCUEHELO2 self + + --- Triggers the FSM event "Stop" that stops the rescue helo after a delay. Event handlers are stopped. + -- @function [parent=#RESCUEHELO2] __Stop + -- @param #RESCUEHELO2 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 #RESCUEHELO2 self +-- @param #number threshold Low fuel threshold in percent. Default 5%. +-- @return #RESCUEHELO2 self +function RESCUEHELO2: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 #RESCUEHELO2 self +-- @param Wrapper.Airbase#AIRBASE airbase The home airbase. Can be the airbase name (passed as a string) or a Moose AIRBASE object. +-- @return #RESCUEHELO2 self +function RESCUEHELO2: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 #RESCUEHELO2 self +-- @param #number radius Radius of rescue zone in nautical miles. Default is 15 NM. +-- @return #RESCUEHELO2 self +function RESCUEHELO2: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 #RESCUEHELO2 self +-- @param #number speed Speed in knots. Default 5 kts. +-- @return #RESCUEHELO2 self +function RESCUEHELO2: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 #RESCUEHELO2 self +-- @param #number duration Duration in minutes. Default 5 min. +-- @return #RESCUEHELO2 self +function RESCUEHELO2: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 #RESCUEHELO2 self +-- @return #RESCUEHELO2 self +function RESCUEHELO2:SetRescueOn() + self.rescueon=true + return self +end + +--- Deactivate rescue option. Crashed and ejected pilots will not be rescued. +-- @param #RESCUEHELO2 self +-- @return #RESCUEHELO2 self +function RESCUEHELO2:SetRescueOff() + self.rescueon=false + return self +end + +--- Set takeoff type. +-- @param #RESCUEHELO2 self +-- @param #number takeofftype Takeoff type. Default SPAWN.Takeoff.Hot. +-- @return #RESCUEHELO2 self +function RESCUEHELO2:SetTakeoff(takeofftype) + self.takeoff=takeofftype or SPAWN.Takeoff.Hot + return self +end + +--- Set takeoff with engines running (hot). +-- @param #RESCUEHELO2 self +-- @return #RESCUEHELO2 self +function RESCUEHELO2:SetTakeoffHot() + self:SetTakeoff(SPAWN.Takeoff.Hot) + return self +end + +--- Set takeoff with engines off (cold). +-- @param #RESCUEHELO2 self +-- @return #RESCUEHELO2 self +function RESCUEHELO2:SetTakeoffCold() + self:SetTakeoff(SPAWN.Takeoff.Cold) + return self +end + +--- Set takeoff in air near the carrier. +-- @param #RESCUEHELO2 self +-- @return #RESCUEHELO2 self +function RESCUEHELO2:SetTakeoffAir() + self:SetTakeoff(SPAWN.Takeoff.Air) + return self +end + +--- Set altitude of helo. +-- @param #RESCUEHELO2 self +-- @param #number alt Altitude in meters. Default 70 m. +-- @return #RESCUEHELO2 self +function RESCUEHELO2:SetAltitude(alt) + self.altitude=alt or 70 + return self +end + +--- Set offset parallel to orientation of carrier. +-- @param #RESCUEHELO2 self +-- @param #number distance Offset distance in meters. Default 200 m (~660 ft). +-- @return #RESCUEHELO2 self +function RESCUEHELO2:SetOffsetX(distance) + self.offsetX=distance or 200 + return self +end + +--- Set offset perpendicular to orientation to carrier. +-- @param #RESCUEHELO2 self +-- @param #number distance Offset distance in meters. Default 240 m (~780 ft). +-- @return #RESCUEHELO2 self +function RESCUEHELO2:SetOffsetZ(distance) + self.offsetZ=distance or 240 + return self +end + + +--- Enable respawning of helo. Note that this is the default behaviour. +-- @param #RESCUEHELO2 self +-- @return #RESCUEHELO2 self +function RESCUEHELO2:SetRespawnOn() + self.respawn=true + return self +end + +--- Disable respawning of helo. +-- @param #RESCUEHELO2 self +-- @return #RESCUEHELO2 self +function RESCUEHELO2:SetRespawnOff() + self.respawn=false + return self +end + +--- Set whether helo shall be respawned or not. +-- @param #RESCUEHELO2 self +-- @param #boolean switch If true (or nil), helo will be respawned. If false, helo will not be respawned. +-- @return #RESCUEHELO2 self +function RESCUEHELO2: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 #RESCUEHELO2 self +-- @return #RESCUEHELO2 self +function RESCUEHELO2:SetRespawnInAir() + self.respawninair=true + return self +end + +--- Set follow time update interval. +-- @param #RESCUEHELO2 self +-- @param #number dt Time interval in seconds. Default 1.0 sec. +-- @return #RESCUEHELO2 self +function RESCUEHELO2:SetFollowTimeInterval(dt) + self.dtFollow=dt or 1.0 + return self +end + +--- Check if helo is on a rescue mission. +-- @param #RESCUEHELO2 self +-- @return #boolean If true, helo is rescuing somebody. +function RESCUEHELO2:IsRescuing() + return self:is("Rescuing") +end + + +--- Get unit name of the spawned helo. +-- @param #RESCUEHELO2 self +-- @return #string Name of the helo unit or nil if it does not exist. +function RESCUEHELO2:GetUnitName() + local unit=self.helo:GetUnit(1) + if unit then + return unit:GetName() + end + return nil +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- EVENT functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Handle landing event of rescue helo. +-- @param #RESCUEHELO2 self +-- @param Core.Event#EVENTDATA EventData Event data. +function RESCUEHELO2:OnEventLand(EventData) + local group=EventData.IniGroup --Wrapper.Group#GROUP + + if group and 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 + + local airbase=nil --Wrapper.Airbase#AIRBASE + local airbasename="unknown" + if EventData.Place then + airbase=EventData.Place + airbasename=airbase:GetName() + end + + -- Respawn the Helo. + local text=string.format("Rescue helo group %s landed at airbase %s.", groupname, airbasename) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- Helo has rescued someone. + -- TODO: Add "Rescued" event. + if self:IsRescuing() then + self:T(self.lid..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 not self:IsRescuing() then + + self:E(self.lid..string.format("WARNING: Rescue helo %s landed. This should not happen for Takeoff=Air or respawninair=true and no rescue operation in progress.", groupname)) + + end + end + + -- Trigger returned event. Respawn at current airbase. + self:__Returned(3, airbase) + + end + end +end + +--- A unit crashed or a player ejected. +-- @param #RESCUEHELO2 self +-- @param Core.Event#EVENTDATA EventData Event data. +function RESCUEHELO2:_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:I(self.lid..text) + + -- Get coordinate of unit. + local coord=unit:GetCoordinate() + + if coord and self.rescuezone:IsCoordinateInZone(coord) then + + -- This does not seem to work any more. Is:Alive returns flase on ejection. + -- 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 + + -- Error message. + self:E(self.lid..string.format("Rescue helo %s crashed!", unitname)) + + -- Stop FSM. + self:Stop() + + -- Restart. + if self.respawn then + self:__Start(5) + end + + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM states +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. Starts the warehouse. Addes event handlers and schedules status updates of reqests and queue. +-- @param #RESCUEHELO2 self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RESCUEHELO2: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.", RESCUEHELO2.version, self.carrier:GetName(), self.carriertype) + self:I(self.lid..text) + + -- Handle events. + self:HandleEvent(EVENTS.Land) + self:HandleEvent(EVENTS.Crash, self._OnEventCrashOrEject) + self:HandleEvent(EVENTS.Ejection, self._OnEventCrashOrEject) + + -- Delay before formation is started. + local delay=120 + + -- 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, self.alias) + + -- Set modex for spawn. + Spawn:InitModex(self.modex) + + -- 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.uncontrolledac then + + -- Use an uncontrolled aircraft group. + self.helo=GROUP:FindByName(self.helogroupname) + + if self.helo and 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, nil, AIRBASE.TerminalType.HelicopterUsable) + + -- 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) + + -- Set follow time interval. + self.formation:SetFollowTimeInterval(self.dtFollow) + + -- Formation mode. + self.formation:SetFlightModeFormation(self.helo) + + -- Start formation FSM. + self.formation:__Start(delay) + + self.flightgroup=FLIGHTGROUP:New(self.helo) --Ops.FlightGroup#FLIGHTGROUP + + -- Init status check + self:__Status(1) +end + + +--- On after Status event. Checks player status. +-- @param #RESCUEHELO2 self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RESCUEHELO2:onafterStatus(From, Event, To) + + -- Get current time. + local time=timer.getTime() + + -- Check if helo is running and not RTBing already or rescuing. + if self.helo and self.helo:IsAlive() then + + ------------------- + -- HELO is ALIVE -- + ------------------- + + -- Get (relative) fuel wrt to initial fuel of helo (DCS bug https://forums.eagle.ru/showthread.php?t=223712) + local fuel=self.helo:GetFuel()*100 + local fuelrel=fuel/self.HeloFuel0 + local life=self.helo:GetUnit(1):GetLife() + local life0=self.helo:GetUnit(1):GetLife0() + local lifeR=self.helo:GetUnit(1):GetLifeRelative() + + -- Report current fuel. + local text=string.format("Rescue Helo %s: state=%s fuel=%.1f, rel.fuel=%.1f, life=%.1f/%.1f=%d", self.helo:GetName(), self:GetState(), fuel, fuelrel, life, life0, lifeR*100) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + if self:IsRunning() then + + -- Check if fuel is low. + if fuel Event --> To State + self:AddTransition("*", "Added", "Shown") -- Marker was added. + self:AddTransition("*", "Removed", "Shown") -- Marker was added. + + self:AddTransition("*", "Change", "*") -- Update status. + + -- Handle events. + self:HandleEvent(EVENTS.MarkAdded) + self:HandleEvent(EVENTS.MarkRemoved) + self:HandleEvent(EVENTS.MarkChange) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User API Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Marker is readonly. +-- @param #MARKER self +-- @return #MARKER self +function MARKER:ReadOnly() + + self.readonly=true + + return self +end + +--- Marker is readonly. +-- @param #MARKER self +-- @param #string Text Message displayed when the marker is added. +-- @return #MARKER self +function MARKER:Message(Text) + + self.message=Text or "" + + return self +end + +--- Place marker visible for everyone. +-- @param #MARKER self +-- @return #MARKER self +function MARKER:ToAll() + + self.toall=true + + -- First remove an existing mark. + if self.shown then + self:Remove() + end + + -- Call DCS function. + trigger.action.markToAll(self.mid, self.text, self.coordinate:GetVec3(), self.readonly, self.message) + + return self +end + +--- Place marker visible for a specific coalition only. +-- @param #MARKER self +-- @param #number Coalition Coalition 1=Red, 2=Blue, 0=Neutral. See `coaliton.side.RED`. +-- @return #MARKER self +function MARKER:ToCoalition(Coalition) + + self.coalition=Coalition + + self.tocoaliton=true + + -- First remove an existing mark. + if self.shown then + self:Remove() + end + + -- Call DCS function. + trigger.action.markToCoalition(self.mid, self.text, self.coordinate:GetVec3(), self.coalition, self.readonly, self.message) + + return self +end + +--- Place marker visible for the blue coalition only. +-- @param #MARKER self +-- @return #MARKER self +function MARKER:ToBlue() + self:ToCoalition(coalition.side.BLUE) + return self +end + +--- Place marker visible for the blue coalition only. +-- @param #MARKER self +-- @return #MARKER self +function MARKER:ToRed() + self:ToCoalition(coalition.side.RED) + return self +end + +--- Place marker visible for the neutral coalition only. +-- @param #MARKER self +-- @return #MARKER self +function MARKER:ToNeutral() + self:ToCoalition(coalition.side.NEUTRAL) + return self +end + + +--- Place marker visible for a specific group only. +-- @param #MARKER self +-- @param Wrapper.Group#GROUP Group The group to which te +-- @return #MARKER self +function MARKER:ToGroup(Group) + + -- Check if group exists. + if Group and Group:IsAlive()~=nil then + + self.groupid=Group:GetID() + + if self.groupid then + + self.groupname=Group:GetName() + + self.togroup=true + + -- First remove an existing mark. + if self.shown then + self:Remove() + end + + -- Call DCS function. + trigger.action.markToGroup(self.mid, self.text, self.coordinate:GetVec3(), self.groupid, self.readonly, self.message) + + end + + else + --TODO: Warning! + end + + return self +end + +--- Update the text displayed on the mark panel. +-- @param #MARKER self +-- @param #string Text Updated text. +-- @return #MARKER self +function MARKER:UpdateText(Text) + + self.text=Text + + self:Refresh() + +end + +--- Update the coordinate where the marker is displayed. +-- @param #MARKER self +-- @param Core.Point#COORDINATE Coordinate The new coordinate. +-- @return #MARKER self +function MARKER:UpdateCoordinate(Coordinate) + + self.coordinate=Coordinate + + self:Refresh() + +end + +--- Refresh the marker. +-- @param #MARKER self +-- @return #MARKER self +function MARKER:Refresh() + + if self.toall then + + self:ToAll() + + elseif self.tocoaliton then + + self:ToCoalition(self.coalition) + + elseif self.togroup then + + local group=GROUP:FindByName(self.groupname) + + self:ToGroup(group) + + end + +end + +--- Remove a marker. +-- @param #MARKER self +-- @return #MARKER self +function MARKER:Remove() + + -- Call DCS function. + trigger.action.removeMark(self.mid) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Event Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Event function when a MARKER is added. +-- @param #MARKER self +-- @param Core.Event#EVENTDATA EventData +function MARKER:OnEventMarkAdded(EventData) + + local MarkID=EventData.MarkID + + if MarkID==self.mid then + + self.shown=true + + end + +end + +--- Event function when a MARKER is removed. +-- @param #MARKER self +-- @param Core.Event#EVENTDATA EventData +function MARKER:OnEventMarkRemoved(EventData) + + local MarkID=EventData.MarkID + + if MarkID==self.mid then + + self.shown=false + + end + +end + +--- Event function when a MARKER changed. +-- @param #MARKER self +-- @param Core.Event#EVENTDATA EventData +function MARKER:OnEventMarkChange(EventData) + + local MarkID=EventData.MarkID + + if MarkID==self.mid then + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------