--[[ #### Why? For Spearhead there's a lot of stages and states the mission can be in.
To make sure CAP units will be at the place where the mission maker expects them to be there's a naming convention that should help.
You as a mission maker will have full controll over where they are supposed to be, the script will take care of getting them there. The CAP manager's goal is to provide dedicated aircraft scheduling that doesn't reset every stage reset. Naming: CAP\_\<"A" | B"\>\_\ #### Config: ``` 1 at x: [] n and n at x: [,] n till n at x: [-] n till n and n at x: [-,] n till n at Active: [-]A divider: | examples: CAP_A[1-4,6]7|[5,7]8_SomeName => Will fly CAP at stage 7 when stages 1 through 4 and 6 are active and will fly CAP at 8 when 5 and 7 are active CAP_A[2-5]5|[6]6_SomeName => Will fly CAP at stage 5 when stages 2 through 5 active and will fly CAP at 6 when 6 is active CAP_A[1-5]A|[6]7_SomeName => Will fly CAP at the ACTIVE stage if Stages 1-5 are active. Basically following the active stages. Then when 6 is active it will fly in 7 CAP_B[1-5]A|[6]7_SomeName => Will fly BACKUP CAP for the active zones 1 through 5 and back up for 7 when 6 is active. ``` ### How many? And how to add backups? To fascilitate a nice flow of the mission and also make sure it doesn't oversaturate the zones with aircraft the script works with a Active/Backup system in the naming.
This really doesn't mean much per se once the mission runs, but most importantly is that the A units define how many groups there should be max in a zone at a time.
The B units will simply be used to fill that amount if the A units can't due to RTB, Death, Rearming etc.
#### Example Take the units: ``` CAP_A[1-5]A_SomeName1 CAP_A[1-5]A_SomeName2 CAP_B[1-3,5]A_SomeName ``` `CAP_A...` units are primary units where the `CAP_B...` units are the backups.
In this case the CAP manager sees that for stages 1 through 5 this configuration requires 2 groups in the active zone.
If one of those 2 groups dies or is going back to base the B group will be used to top up the CAP units at that zone.
After scheduling the B units the A units that are back at base ready on the ramp will also not be scheduled until the CAP units that are active in the zone (inlcuding B units) drop below the required CAP unit (of 2 in this example) In this example there is no Backup unit for zone 4. This might quiet down the CAP a little as the Active groups will have to rearm and refuel without there being any backup. ### What the cap manager does: - Spawn aircraft on the ramp (or despawn when they are not needed anymore for culling) - Send out aircraft based on where they are supposed to be - Send Aircraft RTB after X time.
RTB in this sense means back to its base of origin. Not the closest friendly base like DCS does. - Simulates Rearming and then sending them out when needed. - Delays aircraft for X amount of time before spawning and rearming after a groups demise. - Aircraft are spawned on the ramp so OCA does have effect. (Be sure to also take a look at the Airbase and SAM spawning for defences) ### Future Ideas - Aircraft rearm hubs with finite spawns on other airbases that get replenished by aircraft flying in. ]] -- local CapHelper = {} do ---comment ---@param groupName string ---@return table? CapHelper.ParseGroupName = function(groupName) local split_string = Spearhead.Util.split_string(groupName, "_") local partCount = Spearhead.Util.tableLength(split_string) if partCount >= 3 then local result = {} result.zonesConfig = {} -- CAP_[1-5]5|[6]6|[7]7_Sukhoi -- CAP_[1-5,7]A|[6]7_Sukhoi local configPart = split_string[2] local first = configPart:sub(1, 1) if first == "A" then result.isBackup = false configPart = string.sub(configPart, 2, #configPart) elseif first == "B" then configPart = string.sub(configPart, 2, #configPart) result.isBackup = true elseif first == "[" then result.isBackup = false else table.insert(Spearhead.MissionEditingWarnings, "Could not parse the CAP config for group: " .. groupName) return nil end local subsplit = Spearhead.Util.split_string(configPart, "|") if subsplit then for key, value in pairs(subsplit) do local keySplit = Spearhead.Util.split_string(value, "]") local targetZone = keySplit[2] local allActives = string.sub(keySplit[1], 2, #keySplit[1]) local commaSeperated = Spearhead.Util.split_string(allActives, ",") for _, value in pairs(commaSeperated) do local dashSeperated = Spearhead.Util.split_string(value, "-") if Spearhead.Util.tableLength(dashSeperated) > 1 then local from = tonumber(dashSeperated[1]) local till = tonumber(dashSeperated[2]) for i = from, till do if targetZone == "A" then result.zonesConfig[tostring(i)] = tostring(i) else result.zonesConfig[tostring(i)] = tostring(targetZone) end end else if targetZone == "A" then result.zonesConfig[tostring(dashSeperated[1])] = tostring(dashSeperated[1]) else result.zonesConfig[tostring(dashSeperated[1])] = tostring(targetZone) end end end end end return result else table.insert(Spearhead.MissionEditingWarnings, "CAP Group with name: " .. groupName .. "should have at least 3 parts, but has " .. partCount) return nil end end end ---comment ---@param input table { groupName, task, logger } ---@param time number ---@return nil local function setTaskAsync(input, time) local task = input.task local groupName = input.groupName local group = Group.getByName(groupName) if task then group:getController():setTask(task) if input.logger ~= nil then input.logger:debug("task set succesfully to group " .. groupName) end end return nil end local CapGroup = {} CapGroup.GroupState = { UNSPAWNED = 0, READYONRAMP = 1, INTRANSIT = 2, ONSTATION = 3, RTBINTEN = 4, RTB = 5, DEAD = 6, REARMING = 7, DESPAWNED = 8 } local function SetReadyOnRampAsync(self, time) self:SetState(CapGroup.GroupState.READYONRAMP) end ---comment ---@param groupName string ---@param airbaseId number ---@param logger table logger dependency injection ---@param database table database dependency injection ---@param capConfig table config dependency injection ---@return table? o function CapGroup:new(groupName, airbaseId, logger, database, capConfig) local o = {} setmetatable(o, { __index = self }) local RESPAWN_AFTER_TOUCHDOWN_SECONDS = 180 -- initials o.groupName = groupName o.airbaseId = airbaseId o.logger = logger o.database = database local parsed = CapHelper.ParseGroupName(groupName) if parsed == nil then return nil end o.capZonesConfig = parsed.zonesConfig o.isBackup = parsed.isBackup --vars o.assignedStageName = nil o.assignedStageNumber = nil o.state = CapGroup.GroupState.UNSPAWNED o.aliveUnits = {} o.landedUnits = {} o.unitCount = 0 o.onStationSince = 0 o.currentCapTaskingDuration = 0 --config o.capConfig = {} if not capConfig then capConfig = {} end o.capConfig.maxDeviationRange = capConfig.maxDeviationRange o.capConfig.minSpeed = (capConfig.minSpeed or 400) * 0.514444 o.capConfig.maxSpeed = (capConfig.maxSpeed or 500) * 0.514444 o.capConfig.minAlt = (capConfig.minAlt or 8000) * 0.3048 o.capConfig.maxAlt = (capConfig.maxAlt or 27000) * 0.3048 o.capConfig.minDurationOnStation = capConfig.minDurationOnStation or 600 o.capConfig.maxDurationOnstation = capConfig.maxDurationOnStation or 1800 o.capConfig.rearmDelay = capConfig.rearmDelay or 300 ---comment ---@param self table ---@param currentActive number ---@return table o.GetTargetZone = function (self, currentActive) return self.capZonesConfig[tostring(currentActive)] end o.SetState = function(self, state) self.state = state self:PublishUnitUpdatedEvent() end o.StartRearm = function(self) self:SpawnOnTheRamp() self:SetState(CapGroup.GroupState.REARMING) timer.scheduleFunction(SetReadyOnRampAsync, self, timer.getTime() + self.capConfig.rearmDelay - RESPAWN_AFTER_TOUCHDOWN_SECONDS) end o.SpawnOnTheRamp = function(self) self.aliveUnits = {} self.landedUnits = {} self.onStationSince = 0 local group = Spearhead.DcsUtil.SpawnGroupTemplate(self.groupName, nil, nil, true) if group then self.unitCount = group:getInitialSize() if self.state == CapGroup.GroupState.UNSPAWNED then self:SetState(CapGroup.GroupState.READYONRAMP) end for _, unit in pairs(group:getUnits()) do local name = unit:getName() self.aliveUnits[name] = true self.landedUnits[name] = false end end end o.Despawn = function(self) Spearhead.DcsUtil.DestroyGroup(self.groupName) end o.SendRTB = function(self) local group = Group.getByName(self.groupName) if group and group:isExist() then local speed = math.random(self.capConfig.minSpeed, self.capConfig.maxSpeed) local rtbTask, errormessage = Spearhead.RouteUtil.CreateRTBMission(self.groupName, self.airbaseId, speed) if rtbTask then timer.scheduleFunction(setTaskAsync, { task = rtbTask, groupName = self.groupName, logger = self.logger }, timer.getTime() + 3) else self.logger:error("No RTB task could be created for group: " .. self.groupName .. " due to " .. errormessage) end end end ---Starts and send this group to perform CAP at a stage ---@param self any ---@param stageZoneNumber string o.SendToStage = function(self, stageZoneNumber) if self.state == CapGroup.GroupState.DEAD or self.state == CapGroup.GroupState.RTB then return --Can't task a unit that's dead or RTB end local stageZoneName = self.database:getStageZoneByStageNumber(stageZoneNumber) self.assignedStageNumber = stageZoneNumber self.assignedStageName = stageZoneName local group = Group.getByName(self.groupName) if group and group:isExist() then local zone = Spearhead.DcsUtil.getZoneByName(stageZoneName) if zone then self.logger:debug("Sending group out " .. self.groupName) local controller = group:getController() local capPoints = database:getCapRouteInZone(stageZoneName, self.airbaseId) or { point1 = { x = zone.x, z = zone.z }, point2 = nil } local altitude = math.random(self.capConfig.minAlt, self.capConfig.maxAlt) local speed = math.random(self.capConfig.minSpeed, self.capConfig.maxSpeed) local attackHelos = false local deviationDistance = self.capConfig.maxDeviationRange local capTask if self.state == CapGroup.GroupState.ONRAMP or self.onStationSince == 0 then controller:setCommand({ id = 'Start', params = {} }) local duration = math.random(self.capConfig.minDurationOnStation, self.capConfig .maxDurationOnstation) self.logger:debug("random schedule min: " .. tostring(self.capConfig.minDurationOnStation or "nil") .. " max: " .. tostring(self.capConfig.maxDurationOnstation or "nil") .. " actual " .. duration) self.currentCapTaskingDuration = duration capTask = Spearhead.RouteUtil.createCapMission(self.groupName, self.airbaseId, capPoints.point1, capPoints.point2, altitude, speed, duration, attackHelos, deviationDistance) else local duration = self.currentCapTaskingDuration - (timer.getTime() - o.onStationSince) capTask = Spearhead.RouteUtil.createCapMission(self.groupName, self.airbaseId, capPoints.point1, capPoints.point2, altitude, speed, duration, attackHelos, deviationDistance) end if capTask then timer.scheduleFunction(setTaskAsync, { task = capTask, groupName = self.groupName, logger = self.logger }, timer.getTime() + 3) end self:SetState(CapGroup.GroupState.INTRANSIT) end end end ---Starts and send a unit to another airbase ---@param self table ---@param airdomeId any o.SendToAirbase = function(self, airdomeId) self.airbaseId = airdomeId local speed = math.random(self.capConfig.minSpeed, self.capConfig.maxSpeed) local rtbTask = Spearhead.RouteUtil.CreateRTBMission(self.groupName, airdomeId, speed) local group = Group.getByName(self.groupName) local controller = group:getController() controller:setCommand({ id = 'Start', params = {} }) timer.scheduleFunction(setTaskAsync, { task = rtbTask, groupName = self.groupName, logger = self.logger }, timer.getTime() + 5) end o.OnGroupRTB = function(self, groupName) if groupName == self.groupName then self.logger:debug("Setting group " .. groupName .. " to state RTB after a total of " .. timer.getTime() - self.onStationSince .. "s of the " .. self.currentCapTaskingDuration .. "s") self:SetState(CapGroup.GroupState.RTB) end end o.OnGroupRTBInTen = function(self, groupName) if groupName == self.groupName then self:SetState(CapGroup.GroupState.RTBINTEN) end end o.OnGroupOnStation = function(self, groupName) if groupName == self.groupName then self.onStationSince = timer.getTime() self.logger:debug("Setting group " .. groupName .. " to state Onstation") self:SetState(CapGroup.GroupState.ONSTATION) end end ---comment ---@param self table ---@param proActive boolean Will check all units in group for aliveness o.UpdateState = function(self, proActive) local landed = false local landedCount = 0 for name, landedBool in pairs(self.landedUnits) do if landedBool == true then landedCount = landedCount + 1 landed = true end end local deadCount = 0 for name, isAlive in pairs(self.aliveUnits) do if isAlive == false then deadCount = deadCount + 1 end end local function DelayedStartRearm(input, time) local capGroup = input.self capGroup:StartRearm() end if landedCount + deadCount == self.unitCount then if landed then timer.scheduleFunction(DelayedStartRearm, { self = self }, timer.getTime() + RESPAWN_AFTER_TOUCHDOWN_SECONDS) else local delay = self.capConfig.deathDelay - self.capConfig.rearmDelay + RESPAWN_AFTER_TOUCHDOWN_SECONDS timer.scheduleFunction(DelayedStartRearm, { self = self }, timer.getTime() + delay) end end end o.eventListeners = {} ---comment ---@param self table ---@param listener table object with function OnGroupStateUpdated(capGroupTable) o.AddOnStateUpdatedListener = function(self, listener) if type(listener) ~= "table" then self.logger:error("Listener not of type table for AddOnStateUpdatedListener") return end if listener.OnGroupStateUpdated == nil then self.logger:error("Listener does not implement OnGroupStateUpdated") return end table.insert(self.eventListeners, listener) end o.PublishUnitUpdatedEvent = function(self) for _, callable in pairs(self.eventListeners) do local _, error = pcall(function() callable:OnGroupStateUpdated(self) end) if error then self.logger:error(error) end end end o.OnUnitLanded = function(self, initiatorUnit, airbase) if airbase then local airdomeId = airbase:getID() self.airbaseId = airdomeId end local name = initiatorUnit:getName() self.logger:debug("Received unit land event for unit " .. name .. " of group " .. self.groupName) self.landedUnits[name] = true self:UpdateState(false) end o.OnUnitLost = function(self, initiatorUnit) self.logger:debug("Received unit lost event for group " .. self.groupName) if initiatorUnit then self.aliveUnits[initiatorUnit:getName()] = false end self:UpdateState(false) end Spearhead.Events.addOnGroupRTBListener(o.groupName, o) Spearhead.Events.addOnGroupRTBInTenListener(o.groupName, o) Spearhead.Events.addOnGroupOnStationListener(o.groupName, o) local units = Group.getByName(groupName):getUnits() for key, unit in pairs(units) do Spearhead.Events.addOnUnitLandEventListener(unit:getName(), o) Spearhead.Events.addOnUnitLostEventListener(unit:getName(), o) end return o end if not Spearhead.internal then Spearhead.internal = {} end Spearhead.internal.CapGroup = CapGroup