diff --git a/Moose Development/Moose/Functional/Tiresias.lua b/Moose Development/Moose/Functional/Tiresias.lua new file mode 100644 index 000000000..078bfda56 --- /dev/null +++ b/Moose Development/Moose/Functional/Tiresias.lua @@ -0,0 +1,590 @@ +--- **Functional** - TIRESIAS - manages AI behaviour. +-- +-- === +-- +-- The @{#TIRESIAS} class is working in the back to keep your large-scale ground units in check. +-- +-- ## Features: +-- +-- * Designed to keep CPU and Network usage lower on missions with a lot of ground units. +-- * Does not affect ships to keep the Navy guys happy. +-- * Does not affect OpsGroup type groups. +-- * Distinguishes between SAM groups, AAA groups and other ground groups. +-- * Exceptions can be defined to keep certain actions going. +-- * Works coalition-independent in the back +-- * Easy setup. +-- +-- === +-- +-- ## Missions: +-- +-- ### [TIRESIAS](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master) +-- +-- === +-- +-- ### Author : **applevangelist ** +-- +-- @module Functional.Tiresias +-- @image Functional.Tiresias.jpg +-- +-- Last Update: Dec 2023 + +------------------------------------------------------------------------- +--- **TIRESIAS** class, extends Core.Base#BASE +-- @type TIRESIAS +-- @field #string ClassName +-- @field #booelan debug +-- @field #string version +-- @field #number Interval +-- @field Core.Set#SET_GROUP GroundSet +-- @field #number Coalition +-- @field Core.Set#SET_GROUP VehicleSet +-- @field Core.Set#SET_GROUP AAASet +-- @field Core.Set#SET_GROUP SAMSet +-- @field Core.Set#SET_GROUP ExceptionSet +-- @field Core.Set#SET_OPSGROUP OpsGroupSet +-- @field #number AAARange +-- @field #number HeloSwitchRange +-- @field #number PlaneSwitchRange +-- @field Core.Set#SET_GROUP FlightSet +-- @field #boolean SwitchAAA +-- @extends Core.Fsm#FSM + +--- +-- @type TIRESIAS.Data +-- @field #string type +-- @field #number range +-- @field #boolean invisible +-- @field #boolean AIOff +-- @field #boolean exception + + +--- *Tiresias, Greek demi-god and shapeshifter, blinded by the Gods, works as oracle for you.* (Wiki) +-- +-- === +-- +-- ## TIRESIAS Concept +-- +-- * Designed to keep CPU and Network usage lower on missions with a lot of ground units. +-- * Does not affect ships to keep the Navy guys happy. +-- * Does not affect OpsGroup type groups. +-- * Distinguishes between SAM groups, AAA groups and other ground groups. +-- * Exceptions can be defined in SET_GROUP objects to keep certain actions going. +-- * Works coalition-independent in the back +-- * Easy setup. +-- +-- ## Setup +-- +-- Setup is a one-liner: +-- +-- local blinder = TIRESIAS:New() +-- +-- Optionally you can set up exceptions, e.g. for convoys driving around +-- +-- local exceptionset = SET_GROUP:New():FilterCoalitions("red"):FilterPrefixes("Convoy"):FilterStart() +-- local blinder = TIRESIAS:New() +-- blinder:AddExceptionSet(exceptionset) +-- +-- Options +-- +-- -- Setup different radius for activation around helo and airplane groups (applies to AI and humans) +-- blinder:SetActivationRanges(10,25) -- defaults are 10, and 25 +-- +-- -- Setup engagement ranges for AAA (non-advanced SAM units like Flaks etc) and if you want them to be AIOff +-- blinder:SetAAARanges(60,true) -- defaults are 60, and true +-- +-- @field #TIRESIAS +TIRESIAS = { + ClassName = "TIRESIAS", + debug = false, + version = "0.0.4", + Interval = 20, + GroundSet = nil, + VehicleSet = nil, + AAASet = nil, + SAMSet = nil, + ExceptionSet = nil, + AAARange = 60, -- 60% + HeloSwitchRange = 10, -- NM + PlaneSwitchRange = 25, -- NM + SwitchAAA = true, +} + +--- [USER] Create a new Tiresias object and start it up. +-- @param #TIRESIAS self +-- @return #TIRESIAS self +function TIRESIAS:New() + + -- Inherit everything from FSM class. + local self = BASE:Inherit(self, FSM:New()) -- #TIRESIAS + + --- FSM Functions --- + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start FSM. + self:AddTransition("*", "Status", "*") -- TIRESIAS status update. + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + + self.ExceptionSet = SET_GROUP:New():Clear(false) + + self:HandleEvent(EVENTS.PlayerEnterAircraft,self._EventHandler) + + self.lid = string.format("TIRESIAS %s | ",self.version) + + self:I(self.lid.."Managing ground groups!") + + --- Triggers the FSM event "Stop". Stops TIRESIAS and all its event handlers. + -- @function [parent=#TIRESIAS] Stop + -- @param #TIRESIAS self + + --- Triggers the FSM event "Stop" after a delay. Stops TIRESIAS and all its event handlers. + -- @function [parent=#TIRESIAS] __Stop + -- @param #TIRESIAS self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Start". Starts TIRESIAS and all its event handlers. Note - `:New()` already starts the instance. + -- @function [parent=#TIRESIAS] Start + -- @param #TIRESIAS self + + --- Triggers the FSM event "Start" after a delay. Starts TIRESIAS and all its event handlers. Note - `:New()` already starts the instance. + -- @function [parent=#TIRESIAS] __Start + -- @param #TIRESIAS self + -- @param #number delay Delay in seconds. + + self:__Start(1) + return self +end + +------------------------------------------------------------------------------------------------------------- +-- +-- Helper Functions +-- +------------------------------------------------------------------------------------------------------------- + +---[USER] Set activation radius for Helos and Planes in Nautical Miles. +-- @param #TIRESIAS self +-- @param #number HeloMiles Radius around a Helicopter in which AI ground units will be activated. Defaults to 10NM. +-- @param #number PlaneMiles Radius around an Airplane in which AI ground units will be activated. Defaults to 25NM. +-- @return #TIRESIAS self +function TIRESIAS:SetActivationRanges(HeloMiles,PlaneMiles) + self.HeloSwitchRange = HeloMiles or 10 + self.PlaneSwitchRange = PlaneMiles or 25 + return self +end + +---[USER] Set AAA Ranges - AAA equals non-SAM systems which qualify as AAA in DCS world. +-- @param #TIRESIAS self +-- @param #number FiringRange The engagement range that AAA units will be set to. Can be 0 to 100 (percent). Defaults to 60. +-- @param #boolean SwitchAAA Decide if these system will have their AI switched off, too. Defaults to true. +-- @return #TIRESIAS self +function TIRESIAS:SetAAARanges(FiringRange,SwitchAAA) + self.AAARange = FiringRange or 60 + self.SwitchAAA = (SwitchAAA == false) and false or true + return self +end + +--- [USER] Add a SET_GROUP of GROUP objects as exceptions. Can be done multiple times. +-- @param #TIRESIAS self +-- @param Core.Set#SET_GROUP Set to add to the exception list. +-- @return #TIRESIAS self +function TIRESIAS:AddExceptionSet(Set) + self:T(self.lid.."AddExceptionSet") + local exceptions = self.ExceptionSet + Set:ForEachGroupAlive( + function(grp) + if not grp.Tiresias then + grp.Tiresias = { -- #TIRESIAS.Data + type = "Exception", + exception = true, + } + exceptions:AddGroup(grp,true) + end + BASE:I("TIRESIAS: Added exception group: "..grp:GetName()) + end + ) + return self +end + +--- [INTERNAL] Filter Function +-- @param Wrapper.Group#GROUP Group +-- @return #boolean isin +function TIRESIAS._FilterNotAAA(Group) + local grp = Group -- Wrapper.Group#GROUP + local isaaa = grp:IsAAA() + if isaaa == true and grp:IsGround() and not grp:IsShip() then + return false -- remove from SET + else + return true -- keep in SET + end +end + +--- [INTERNAL] Filter Function +-- @param Wrapper.Group#GROUP Group +-- @return #boolean isin +function TIRESIAS._FilterNotSAM(Group) + local grp = Group -- Wrapper.Group#GROUP + local issam = grp:IsSAM() + if issam == true and grp:IsGround() and not grp:IsShip() then + return false -- remove from SET + else + return true -- keep in SET + end +end + +--- [INTERNAL] Filter Function +-- @param Wrapper.Group#GROUP Group +-- @return #boolean isin +function TIRESIAS._FilterAAA(Group) + local grp = Group -- Wrapper.Group#GROUP + local isaaa = grp:IsAAA() + if isaaa == true and grp:IsGround() and not grp:IsShip() then + return true -- remove from SET + else + return false -- keep in SET + end +end + +--- [INTERNAL] Filter Function +-- @param Wrapper.Group#GROUP Group +-- @return #boolean isin +function TIRESIAS._FilterSAM(Group) + local grp = Group -- Wrapper.Group#GROUP + local issam = grp:IsSAM() + if issam == true and grp:IsGround() and not grp:IsShip() then + return true -- remove from SET + else + return false -- keep in SET + end +end + +--- [INTERNAL] Init Groups +-- @param #TIRESIAS self +-- @return #TIRESIAS self +function TIRESIAS:_InitGroups() + self:T(self.lid.."_InitGroups") + -- Set all groups invisible/motionless + local EngageRange = self.AAARange + local SwitchAAA = self.SwitchAAA + --- AAA + self.AAASet:ForEachGroupAlive( + function(grp) + if not grp.Tiresias then + grp:OptionEngageRange(EngageRange) + grp:SetCommandInvisible(true) + if SwitchAAA then + grp:SetAIOff() + grp:EnableEmission(false) + end + grp.Tiresias = { -- #TIRESIAS.Data + type = "AAA", + invisible = true, + range = EngageRange, + exception = false, + AIOff = SwitchAAA, + } + end + if grp.Tiresias and (not grp.Tiresias.exception == true) then + if grp.Tiresias.invisible and grp.Tiresias.invisible == false then + grp:SetCommandInvisible(true) + grp.Tiresias.invisible = true + if SwitchAAA then + grp:SetAIOff() + grp:EnableEmission(false) + grp.Tiresias.AIOff = true + end + end + end + --BASE:I(string.format("Init/Switch off AAA %s (Exception %s)",grp:GetName(),tostring(grp.Tiresias.exception))) + end + ) + --- Vehicles + self.VehicleSet:ForEachGroupAlive( + function(grp) + if not grp.Tiresias then + grp:SetAIOff() + grp:SetCommandInvisible(true) + grp.Tiresias = { -- #TIRESIAS.Data + type = "Vehicle", + invisible = true, + AIOff = true, + exception = false, + } + end + if grp.Tiresias and (not grp.Tiresias.exception == true) then + if grp.Tiresias and grp.Tiresias.invisible and grp.Tiresias.invisible == false then + grp:SetCommandInvisible(true) + grp:SetAIOff() + grp.Tiresias.invisible = true + end + end + --BASE:I(string.format("Init/Switch off Vehicle %s (Exception %s)",grp:GetName(),tostring(grp.Tiresias.exception))) + end + ) + --- SAM + self.SAMSet:ForEachGroupAlive( + function(grp) + if not grp.Tiresias then + grp:SetCommandInvisible(true) + grp.Tiresias = { -- #TIRESIAS.Data + type = "SAM", + invisible = true, + exception = false, + } + end + if grp.Tiresias and (not grp.Tiresias.exception == true) then + if grp.Tiresias and grp.Tiresias.invisible and grp.Tiresias.invisible == false then + grp:SetCommandInvisible(true) + grp.Tiresias.invisible = true + end + end + --BASE:I(string.format("Init/Switch off SAM %s (Exception %s)",grp:GetName(),tostring(grp.Tiresias.exception))) + end + ) + return self +end + +--- [INTERNAL] Event handler function +-- @param #TIRESIAS self +-- @param Core.Event#EVENTDATA EventData +-- @return #TIRESIAS self +function TIRESIAS:_EventHandler(EventData) + self:T(string.format("%s Event = %d",self.lid, EventData.id)) + local event = EventData -- Core.Event#EVENTDATA + if event.id == EVENTS.PlayerEnterAircraft or event.id == EVENTS.PlayerEnterUnit then + --local _coalition = event.IniCoalition + --if _coalition ~= self.Coalition then + -- return --ignore! + --end + local unitname = event.IniUnitName or "none" + local _unit = event.IniUnit + local _group = event.IniGroup + if _group and _group:IsAlive() then + local radius = self.PlaneSwitchRange + if _group:IsHelicopter() then + radius = self.HeloSwitchRange + end + self:_SwitchOnGroups(_group,radius) + end + end + return self +end + +--- [INTERNAL] Switch Groups Behaviour +-- @param #TIRESIAS self +-- @param Wrapper.Group#GROUP group +-- @param #number radius Radius in NM +-- @return #TIRESIAS self +function TIRESIAS:_SwitchOnGroups(group,radius) + self:T(self.lid.."_SwitchOnGroups "..group:GetName().." Radius "..radius.." NM") + local zone = ZONE_GROUP:New("Zone-"..group:GetName(),group,UTILS.NMToMeters(radius)) + local ground = SET_GROUP:New():FilterCategoryGround():FilterZones({zone}):FilterOnce() + local count = ground:CountAlive() + if self.debug then + local text = string.format("There are %d groups around this plane or helo!",count) + self:I(text) + end + local SwitchAAA = self.SwitchAAA + if ground:CountAlive() > 0 then + ground:ForEachGroupAlive( + function(grp) + if grp.Tiresias and grp.Tiresias.type and (not grp.Tiresias.exception == true ) then + if grp.Tiresias.invisible == true then + grp:SetCommandInvisible(false) + grp.Tiresias.invisible = false + end + if grp.Tiresias.type == "Vehicle" and grp.Tiresias.AIOff and grp.Tiresias.AIOff == true then + grp:SetAIOn() + grp.Tiresias.AIOff = false + end + if SwitchAAA and grp.Tiresias.type == "AAA" and grp.Tiresias.AIOff and grp.Tiresias.AIOff == true then + grp:SetAIOn() + grp:EnableEmission(true) + grp.Tiresias.AIOff = false + end + --BASE:I(string.format("TIRESIAS - Switch on %s %s (Exception %s)",tostring(grp.Tiresias.type),grp:GetName(),tostring(grp.Tiresias.exception))) + else + BASE:E("TIRESIAS - This group has not been initialized or is an exception!") + end + end + ) + end + return self +end + +------------------------------------------------------------------------------------------------------------- +-- +-- FSM Functions +-- +------------------------------------------------------------------------------------------------------------- + +--- [INTERNAL] FSM Function +-- @param #TIRESIAS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #TIRESIAS self +function TIRESIAS:onafterStart(From, Event, To) + self:T({From, Event, To}) + + local VehicleSet = SET_GROUP:New():FilterCategoryGround():FilterFunction(TIRESIAS._FilterNotAAA):FilterFunction(TIRESIAS._FilterNotSAM):FilterStart() + local AAASet = SET_GROUP:New():FilterCategoryGround():FilterFunction(TIRESIAS._FilterAAA):FilterStart() + local SAMSet = SET_GROUP:New():FilterCategoryGround():FilterFunction(TIRESIAS._FilterSAM):FilterStart() + local OpsGroupSet = SET_OPSGROUP:New():FilterActive(true):FilterStart() + self.FlightSet = SET_GROUP:New():FilterCategories({"plane","helicopter"}):FilterStart() + + local EngageRange = self.AAARange + + local ExceptionSet = self.ExceptionSet + if self.ExceptionSet then + function ExceptionSet:OnAfterAdded(From,Event,To,ObjectName,Object) + BASE:I("TIRESIAS: EXCEPTION Object Added: "..Object:GetName()) + if Object and Object:IsAlive() then + Object.Tiresias = { -- #TIRESIAS.Data + type = "Exception", + exception = true, + } + Object:SetAIOn() + Object:SetCommandInvisible(false) + Object:EnableEmission(true) + end + end + + local OGS = OpsGroupSet:GetAliveSet() + for _,_OG in pairs(OGS or {}) do + local OG = _OG -- Ops.OpsGroup#OPSGROUP + local grp = OG:GetGroup() + ExceptionSet:AddGroup(grp,true) + end + + function OpsGroupSet:OnAfterAdded(From,Event,To,ObjectName,Object) + local grp = Object:GetGroup() + ExceptionSet:AddGroup(grp,true) + end + end + + function VehicleSet:OnAfterAdded(From,Event,To,ObjectName,Object) + BASE:I("TIRESIAS: VEHCILE Object Added: "..Object:GetName()) + if Object and Object:IsAlive() then + Object:SetAIOff() + Object:SetCommandInvisible(true) + Object.Tiresias = { -- #TIRESIAS.Data + type = "Vehicle", + invisible = true, + AIOff = true, + exception = false, + } + end + end + + local SwitchAAA = self.SwitchAAA + + function AAASet:OnAfterAdded(From,Event,To,ObjectName,Object) + if Object and Object:IsAlive() then + BASE:I("TIRESIAS: AAA Object Added: "..Object:GetName()) + Object:OptionEngageRange(EngageRange) + Object:SetCommandInvisible(true) + if SwitchAAA then + Object:SetAIOff() + Object:EnableEmission(false) + end + Object.Tiresias = { -- #TIRESIAS.Data + type = "AAA", + invisible = true, + range = EngageRange, + exception = false, + AIOff = SwitchAAA, + } + end + end + + function SAMSet:OnAfterAdded(From,Event,To,ObjectName,Object) + if Object and Object:IsAlive() then + BASE:I("TIRESIAS: SAM Object Added: "..Object:GetName()) + Object:SetCommandInvisible(true) + Object.Tiresias = { -- #TIRESIAS.Data + type = "SAM", + invisible = true, + exception = false, + } + end + end + + self.VehicleSet = VehicleSet + self.AAASet = AAASet + self.SAMSet = SAMSet + self.OpsGroupSet = OpsGroupSet + + self:_InitGroups() + + self:__Status(1) + return self +end + +--- [INTERNAL] FSM Function +-- @param #TIRESIAS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #TIRESIAS self +function TIRESIAS:onbeforeStatus(From, Event, To) + self:T({From, Event, To}) + if self:GetState() == "Stopped" then + return false + end + return self +end + +--- [INTERNAL] FSM Function +-- @param #TIRESIAS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #TIRESIAS self +function TIRESIAS:onafterStatus(From, Event, To) + self:T({From, Event, To}) + if self.debug then + local count = self.VehicleSet:CountAlive() + local AAAcount = self.AAASet:CountAlive() + local SAMcount = self.SAMSet:CountAlive() + local text = string.format("Overall: %d | Vehicles: %d | AAA: %d | SAM: %d",count+AAAcount+SAMcount,count,AAAcount,SAMcount) + self:I(text) + end + self:_InitGroups() + if self.FlightSet:CountAlive() > 0 then + local Set = self.FlightSet:GetAliveSet() + for _,_plane in pairs(Set) do + local plane = _plane -- Wrapper.Group#GROUP + local radius = self.PlaneSwitchRange + if plane:IsHelicopter() then + radius = self.HeloSwitchRange + end + self:_SwitchOnGroups(_plane,radius) + end + end + if self:GetState() ~= "Stopped" then + self:__Status(self.Interval) + end + return self +end + +--- [INTERNAL] FSM Function +-- @param #TIRESIAS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #TIRESIAS self +function TIRESIAS:onafterStop(From, Event, To) + self:T({From, Event, To}) + self:UnHandleEvent(EVENTS.PlayerEnterAircraft) + return self +end + +------------------------------------------------------------------------------------------------------------- +-- +-- End +-- +------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index 99ca565a1..7ab4ad2e8 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -83,6 +83,7 @@ __Moose.Include( 'Scripts/Moose/Functional/ZoneCaptureCoalition.lua' ) __Moose.Include( 'Scripts/Moose/Functional/ZoneGoal.lua' ) __Moose.Include( 'Scripts/Moose/Functional/ZoneGoalCargo.lua' ) __Moose.Include( 'Scripts/Moose/Functional/ZoneGoalCoalition.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/Tiresias.lua' ) __Moose.Include( 'Scripts/Moose/Ops/Airboss.lua' ) __Moose.Include( 'Scripts/Moose/Ops/AirWing.lua' ) diff --git a/Moose Setup/Moose.files b/Moose Setup/Moose.files index 90a6e761d..a863db719 100644 --- a/Moose Setup/Moose.files +++ b/Moose Setup/Moose.files @@ -83,6 +83,7 @@ Functional/Autolase.lua Functional/AICSAR.lua Functional/AmmoTruck.lua Functional/ZoneGoalCargo.lua +Functional/Tiresias.lua Ops/Airboss.lua Ops/RecoveryTanker.lua