diff --git a/Moose Development/Moose/Functional/AICSAR.lua b/Moose Development/Moose/Functional/AICSAR.lua new file mode 100644 index 000000000..84bb745fc --- /dev/null +++ b/Moose Development/Moose/Functional/AICSAR.lua @@ -0,0 +1,622 @@ +--- **Functional** - AI CSAR system +-- +-- ## Main Features: +-- +-- * Send out helicopters to downed pilots +-- * Rescues players and AI alike +-- * Coalition specific +-- * Starting from a FARP or Airbase +-- * Dedicated MASH zone +-- * Some FSM functions to include in your mission scripts +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/). +-- +-- === +-- +-- ### Author: **applevangelist** +-- +-- === +-- @module Functional.AICSAR +-- @image MOOSE.JPG + + + +--- AI CSAR class. +-- @type AICSAR +-- @field #string ClassName Name of this class. +-- @field #string version Versioning. +-- @field #string lid LID for log entries. +-- @field #number coalition Colition side. +-- @field #string template Template for pilot. +-- @field #string helotemplate Template for CSAR helo. +-- @field #string alias Alias Name. +-- @field Wrapper.Airbase#AIRBASE farp FARP object from where to start. +-- @field Core.Zone#ZONE farpzone MASH zone to drop rescued pilots. +-- @field #number maxdistance Max distance to go for a rescue. +-- @field #table pilotqueue Queue of pilots to rescue. +-- @field #number pilotindex Table index to bind pilot to helo. +-- @field #table helos Table of Ops.FlightGroup#FLIGHTGROUP objects +-- @field #boolean verbose Switch more output. +-- @field #number rescuezoneradius Radius around downed pilot for the helo to land in. +-- @field #table rescued Track number of rescued pilot. +-- @field #boolean autoonoff Only send a helo when no human heli pilots are available. +-- @field Core.Set#SET_CLIENT playerset Track if alive heli pilots are available. +-- +-- @extends Core.Fsm#FSM + + +--- *I once donated a pint of my finest red corpuscles to the great American Red Cross and the doctor opined my blood was very helpful; contained so much alcohol they could use it to sterilize their instruments.* +-- W.C. Fields +-- +-- === +-- +-- # AICSAR Concept +-- +-- For an AI or human pilot landing with a parachute, a rescue mission will be spawned. The helicopter will fly to the pilot, pick him or her up, +-- and fly back to a designated MASH (medical) zone, drop the pilot and then return to base. +-- Operational maxdistance can be set as well as the landing radius around the downed pilot. +-- Keep in mind that AI helicopters cannot hover-load at the time of writing, so rescue operations over water or in the mountains might not +-- work. +-- Optionally, if you have a CSAR operation with human pilots in your mission, you can set AICSAR to ignore missions when humna helicopter +-- pilots are around. +-- +-- ## Setup +-- +-- Setup is a one-liner: +-- +-- -- @param #string Alias Name of this instance. +-- -- @param #number Coalition Coalition as in coalition.side.BLUE, can also be passed as "blue", "red" or "neutral" +-- -- @param #string Pilottemplate Pilot template name. +-- -- @param #string Helotemplate Helicopter template name. +-- -- @param Wrapper.Airbase#AIRBASE FARP FARP object or Airbase from where to start. +-- -- @param Core.Zone#ZONE MASHZone Zone where to drop pilots after rescue. +-- local my_aicsar=AICSAR:New("Luftrettung",coalition.side.BLUE,"Downed Pilot","Rescue Helo",AIRBASE:FindByName("Test FARP"),ZONE:New("MASH")) +-- +-- ## Options are +-- +-- my_aicsar.maxdistance -- maximum operational distance in meters. Defaults to 50NM or 92.6km +-- my_aicsar.rescuezoneradius -- landing zone around downed pilot. Defaults to 200m +-- my_aicsar.autoonoff -- stop operations when human helicopter pilots are around. Defaults to true. +-- my_aicsar.verbose -- messages coalition side about ongoing operations. Defaults to true. +-- +-- === +--- +-- +-- @field #AICSAR +AICSAR = { + ClassName = "AICSAR", + version = "0.0.1", + lid = "", + coalition = coalition.side.BLUE, + template = "", + helotemplate = "", + alias = "", + farp = nil, + farpzone = nil, + maxdistance = UTILS.NMToMeters(50), + pilotqueue = {}, + pilotindex = 0, + helos = {}, + verbose = true, + rescuezoneradius = 200, + rescued = {}, + autoonoff = true, + playerset = nil, +} + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Function to create a new AICSAR object +-- @param #AICSAR self +-- @param #string Alias Name of this instance. +-- @param #number Coalition Coalition as in coalition.side.BLUE, can also be passed as "blue", "red" or "neutral" +-- @param #string Pilottemplate Pilot template name. +-- @param #string Helotemplate Helicopter template name. +-- @param Wrapper.Airbase#AIRBASE FARP FARP object or Airbase from where to start. +-- @param Core.Zone#ZONE MASHZone Zone where to drop pilots after rescue. +-- @return #AICSAR self +function AICSAR:New(Alias,Coalition,Pilottemplate,Helotemplate,FARP,MASHZone) + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) + + --set Coalition + if Coalition and type(Coalition)=="string" then + if Coalition=="blue" then + self.coalition=coalition.side.BLUE + self.coalitiontxt = Coalition + elseif Coalition=="red" then + self.coalition=coalition.side.RED + self.coalitiontxt = Coalition + elseif Coalition=="neutral" then + self.coalition=coalition.side.NEUTRAL + self.coalitiontxt = Coalition + else + self:E("ERROR: Unknown coalition in CSAR!") + end + else + self.coalition = Coalition + self.coalitiontxt = string.lower(UTILS.GetCoalitionName(self.coalition)) + end + + -- Set alias. + if Alias then + self.alias=tostring(Alias) + else + self.alias="Red Cross" + if self.coalition then + if self.coalition==coalition.side.RED then + self.alias="IFRC" + elseif self.coalition==coalition.side.BLUE then + self.alias="CSAR" + end + end + end + + self.template = Pilottemplate + self.helotemplate = Helotemplate + self.farp = FARP + self.farpzone = MASHZone + self.playerset = SET_CLIENT:New():FilterActive(true):FilterCategories("helicopter"):FilterStart() + + -- Set some string id for output to DCS.log file. + self.lid=string.format("%s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown") + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start FSM. + self:AddTransition("*", "Status", "*") -- CSAR status update. + self:AddTransition("*", "PilotDown", "*") -- Pilot down + self:AddTransition("*", "PilotPickedUp", "*") -- Pilot in helo + self:AddTransition("*", "PilotRescued", "*") -- Pilot Rescued + self:AddTransition("*", "PilotKIA", "*") -- Pilot dead + self:AddTransition("*", "HeloDown", "*") -- Helo dead + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + + self:HandleEvent(EVENTS.LandingAfterEjection) + + self:__Start(math.random(2,5)) + + self:I(self.lid .. " AI CSAR Starting") + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Status". + -- @function [parent=#AICSAR] Status + -- @param #AICSAR self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#AICSAR] __Status + -- @param #AICSAR self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". + -- @function [parent=#AICSAR] Stop + -- @param #AICSAR self + + --- Triggers the FSM event "Stop" after a delay. + -- @function [parent=#AICSAR] __Stop + -- @param #AICSAR self + -- @param #number delay Delay in seconds. + + --- On after "PilotDown" event. + -- @function [parent=#AICSAR] OnAfterPilotDown + -- @param #AICSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Point#COORDINATE Coordinate Location of the pilot. + -- @param #boolean InReach True if in maxdistance else false. + + --- On after "PilotPickedUp" event. + -- @function [parent=#AICSAR] OnAfterPilotPickedUp + -- @param #AICSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.FlightGroup#FLIGHTGROUP Helo + -- @param #table CargoTable of Ops.OpsGroup#OPSGROUP Cargo objects + -- @param #number Index + + --- On after "PilotRescued" event. + -- @function [parent=#AICSAR] OnAfterPilotRescued + -- @param #AICSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On after "PilotKIA" event. + -- @function [parent=#AICSAR] OnAfterPilotKIA + -- @param #AICSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On after "HeloDown" event. + -- @function [parent=#AICSAR] OnAfterHeloDown + -- @param #AICSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.FlightGroup#FLIGHTGROUP Helo + -- @param #number Index + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- [Internal] Catch the landing after ejection and spawn a pilot in situ. +-- @param #AICSAR self +-- @param Core.Event#EVENTDATA EventData +-- @return #AICSAR self +function AICSAR:OnEventLandingAfterEjection(EventData) + self:T(self.lid .. "OnEventLandingAfterEjection ID=" .. EventData.id) + + -- autorescue on off? + if self.autoonoff then + if self.playerset:CountAlive() > 0 then + return self + end + end + + local _event = EventData -- Core.Event#EVENTDATA + -- get position and spawn in a template pilot + local _LandingPos = COORDINATE:NewFromVec3(_event.initiator:getPosition().p) + local _country = _event.initiator:getCountry() + local _coalition = coalition.getCountryCoalition( _country ) + + -- DONE: add distance check + local distancetofarp = _LandingPos:Get2DDistance(self.farp:GetCoordinate()) + + if _coalition == self.coalition and distancetofarp <= self.maxdistance then + self:T(self.lid .. "Spawning new Pilot") + self.pilotindex = self.pilotindex + 1 + local newpilot = SPAWN:NewWithAlias(self.template,string.format("%s-AICSAR-%d",self.template, self.pilotindex)) + newpilot:InitDelayOff() + newpilot:OnSpawnGroup( + function (grp) + self.pilotqueue[self.pilotindex] = grp + end + ) + newpilot:SpawnFromCoordinate(_LandingPos) + --self.pilotqueue[self.pilotindex] = newpilot + Unit.destroy(_event.initiator) -- shagrat remove static Pilot model + self:__PilotDown(2,_LandingPos,true) + if self.verbose then + local text = "Roger, Pilot, we hear you. Stay where you are, a helo is on the way!" + MESSAGE:New(text,15,"AICSAR"):ToCoalition(self.coalition) + end + elseif _coalition == self.coalition and distancetofarp > self.maxdistance then + -- apologies, too far off + self:T(self.lid .. "Pilot out of reach") + self:__PilotDown(2,_LandingPos,false) + if self.verbose then + local text = "Sorry, Pilot. You're behind maximum operational distance! Good Luck!" + MESSAGE:New(text,15,"AICSAR"):ToCoalition(self.coalition) + end + end + return self +end + +--- [Internal] Get (available?) FlightGroup +-- @param #AICSAR self +-- @return Ops.FlightGroup#FLIGHTGROUP The FlightGroup +function AICSAR:_GetFlight() + self:T(self.lid .. "_GetFlight") + -- Helo Carrier. + local newhelo = SPAWN:NewWithAlias(self.helotemplate,self.helotemplate..math.random(1,10000)) + :InitDelayOff() + :InitUnControlled(true) + :Spawn() + + local nhelo=FLIGHTGROUP:New(newhelo) + nhelo:SetHomebase(self.farp) + nhelo:Activate() + return nhelo +end + +--- [Internal] Create a new rescue mission +-- @param #AICSAR self +-- @param Wrapper.Group#GROUP Pilot The pilot to be rescued. +-- @param #number Index Index number of this pilot +-- @return #AICSAR self +function AICSAR:_InitMission(Pilot,Index) + self:T(self.lid .. "_InitMission") + + local pickupzone = ZONE_GROUP:New(Pilot:GetName(),Pilot,self.rescuezoneradius) + --local pilotset = SET_GROUP:New() + --pilotset:AddGroup(Pilot) + + -- Cargo transport assignment. + local opstransport=OPSTRANSPORT:New(Pilot, pickupzone, self.farpzone) + + local helo = self:_GetFlight() + -- inject reservation + helo.AICSARReserved = true + + -- Cargo transport assignment to first Huey group. + helo:AddOpsTransport(opstransport) + + -- callback functions + local function AICPickedUp(Helo,Cargo,Index) + self:__PilotPickedUp(2,Helo,Cargo,Index) + end + + local function AICHeloDead(Helo,Index) + self:__HeloDown(2,Helo,Index) + end + + function helo:OnAfterLoadingDone(From,Event,To) + AICPickedUp(helo,helo:GetCargoGroups(),Index) + end + + function helo:OnAfterDead(From,Event,To) + AICHeloDead(helo,Index) + end + + self.helos[Index] = helo + + return self +end + +--- [Internal] Check if pilot arrived in rescue zone (MASH) +-- @param #AICSAR self +-- @param Wrapper.Group#GROUP Pilot The pilot to be rescued. +-- @return #boolean outcome +function AICSAR:_CheckInMashZone(Pilot) + self:T(self.lid .. "_CheckQueue") + if Pilot:IsInZone(self.farpzone) then + return true + else + return false + end +end + +--- [Internal] Check helo queue +-- @param #AICSAR self +-- @return #AICSAR self +function AICSAR:_CheckHelos() + self:T(self.lid .. "_CheckHelos") + for _index,_helo in pairs(self.helos) do + local helo = _helo -- Ops.FlightGroup#FLIGHTGROUP + if helo and helo.ClassName == "FLIGHTGROUP" then + local state = helo:GetState() + local name = helo:GetName() + self:T("Helo group "..name.." in state "..state) + if state == "Arrived" then + helo:__Stop(1) + self.helos[_index] = nil + end + else + self.helos[_index] = nil + end + end + return self +end + +--- [Internal] Check pilot queue for next mission +-- @param #AICSAR self +-- @return #AICSAR self +function AICSAR:_CheckQueue() + self:T(self.lid .. "_CheckQueue") + for _index, _pilot in pairs(self.pilotqueue) do + local classname = _pilot.ClassName and _pilot.ClassName or "NONE" + local name = _pilot.GroupName and _pilot.GroupName or "NONE" + --self:T("Looking at " .. classname .. " " .. name) + -- find one w/o mission + if _pilot and _pilot.ClassName and _pilot.ClassName == "GROUP" then + -- has no mission assigned? + if not _pilot.AICSAR then + _pilot.AICSAR = {} + _pilot.AICSAR.Status = "Initiated" + _pilot.AICSAR.Boarded = false + self:_InitMission(_pilot,_index) + break + else + -- update status from OPSGROUP + local flightgroup = self.helos[_index] -- Ops.FlightGroup#FLIGHTGROUP + if flightgroup then + local state = flightgroup:GetState() + _pilot.AICSAR.Status = state + end + --self:T("Flight for " .. _pilot.GroupName .. " in state " .. state) + if self:_CheckInMashZone(_pilot) then + self:T("Pilot" .. _pilot.GroupName .. " rescued!") + _pilot:Destroy(false) + self.pilotqueue[_index] = nil + self.rescued[_index] = true + self:__PilotRescued(2) + flightgroup.AICSARReserved = false + end + end + end + end + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- [Internal] onafterStart +-- @param #AICSAR self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AICSAR self +function AICSAR:onafterStart(From, Event, To) + self:T({From, Event, To}) + self:__Status(3) + return self +end + +--- [Internal] onafterStatus +-- @param #AICSAR self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AICSAR self +function AICSAR:onafterStatus(From, Event, To) + self:T({From, Event, To}) + self:_CheckQueue() + self:_CheckHelos() + self:__Status(30) + return self +end + +--- [Internal] onafterStop +-- @param #AICSAR self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AICSAR self +function AICSAR:onafterStop(From, Event, To) + self:T({From, Event, To}) + self:UnHandleEvent(EVENTS.LandingAfterEjection) + return self +end + +--- [Internal] onafterPilotDown +-- @param #AICSAR self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Core.Point#COORDINATE Coordinate Location of the pilot. +-- @param #boolean InReach True if in maxdistance else false. +-- @return #AICSAR self +function AICSAR:onafterPilotDown(From, Event, To, Coordinate, InReach) + self:T({From, Event, To}) + local CoordinateText = Coordinate:ToStringMGRS() + local inreach = tostring(InReach) + local text = string.format("Pilot down at %s. In reach = %s",CoordinateText,inreach) + self:T(text) + if self.verbose then + MESSAGE:New(text,15,"AICSAR"):ToCoalition(self.coalition) + end + return self +end + +--- [Internal] onafterPilotKIA +-- @param #AICSAR self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AICSAR self +function AICSAR:onafterPilotKIA(From, Event, To) + self:T({From, Event, To}) + if self.verbose then + MESSAGE:New("Pilot KIA!",15,"AICSAR"):ToCoalition(self.coalition) + end + return self +end + +--- [Internal] onafterHeloDown +-- @param #AICSAR self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.FlightGroup#FLIGHTGROUP Helo +-- @param #number Index +-- @return #AICSAR self +function AICSAR:onafterHeloDown(From, Event, To, Helo, Index) + self:T({From, Event, To}) + if self.verbose then + MESSAGE:New("CSAR Helo Down!",15,"AICSAR"):ToCoalition(self.coalition) + end + local findex = 0 + local fhname = Helo:GetName() + -- find index of Helo + if Index and Index > 0 then + findex=Index + else + for _index, _helo in pairs(self.helos) do + local helo = _helo -- Ops.FlightGroup#FLIGHTGROUP + local hname = helo:GetName() + if fhname == hname then + findex = _index + break + end + end + end + -- find pilot + if findex > 0 and not self.rescued[findex] then + local pilot = self.pilotqueue[findex] + self.helos[findex] = nil + if pilot.AICSAR.Boarded then + self:T("Helo Down: Found DEAD Pilot ID " .. findex .. " with name " .. pilot:GetName()) + -- pilot also dead + self:__PilotKIA(2) + self.pilotqueue[findex] = nil + else + -- initiate new mission + self:T("Helo Down: Found ALIVE Pilot ID " .. findex .. " with name " .. pilot:GetName()) + self:_InitMission(pilot,findex) + end + end + return self +end + +--- [Internal] onafterPilotRescued +-- @param #AICSAR self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AICSAR self +function AICSAR:onafterPilotRescued(From, Event, To) + self:T({From, Event, To}) + if self.verbose then + MESSAGE:New("Pilot rescued!",15,"AICSAR"):ToCoalition(self.coalition) + end + return self +end + +--- [Internal] onafterPilotPickedUp +-- @param #AICSAR self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.FlightGroup#FLIGHTGROUP Helo +-- @param #table CargoTable of Ops.OpsGroup#OPSGROUP Cargo objects +-- @param #number Index +-- @return #AICSAR self +function AICSAR:onafterPilotPickedUp(From, Event, To, Helo, CargoTable, Index) + self:T({From, Event, To}) + if self.verbose then + MESSAGE:New("Pilot picked up!",15,"AICSAR"):ToCoalition(self.coalition) + end + local findex = 0 + local fhname = Helo:GetName() + if Index and Index > 0 then + findex = Index + else + -- find index of Helo + for _index, _helo in pairs(self.helos) do + local helo = _helo -- Ops.FlightGroup#FLIGHTGROUP + local hname = helo:GetName() + if fhname == hname then + findex = _index + break + end + end + end + -- find pilot + if findex > 0 then + local pilot = self.pilotqueue[findex] + self:T("Boarded: Found Pilot ID " .. findex .. " with name " .. pilot:GetName()) + pilot.AICSAR.Boarded = true -- mark as boarded + end + return self +end diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index 30f02b45d..02289d6dd 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -71,6 +71,7 @@ __Moose.Include( 'Scripts/Moose/Functional/Fox.lua' ) __Moose.Include( 'Scripts/Moose/Functional/Mantis.lua' ) __Moose.Include( 'Scripts/Moose/Functional/Shorad.lua' ) __Moose.Include( 'Scripts/Moose/Functional/Autolase.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/AICSAR.lua' ) __Moose.Include( 'Scripts/Moose/Ops/Airboss.lua' ) __Moose.Include( 'Scripts/Moose/Ops/RecoveryTanker.lua' )