------------------------------------------------------------------ -- VEAF Quick Reaction Alert for DCS World -- https://en.wikipedia.org/wiki/Quick_Reaction_Alert -- By Zip (2020) and Rex (2022) -- -- Features: -- --------- -- * Define zones that are defended by an AI flight -- * Default behavior: when an ennemy aircraft enters the zone, QRA patrol is spawned; then, when it is destroyed, the zone is not defended anymore; when all enemy aircrafts have left the zone, it resets and can respawn a new QRA -- -- See the documentation : https://veaf.github.io/documentation/ ------------------------------------------------------------------ veafQraManager = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global settings. Stores the script constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. veafQraManager.Id = "QRA" --- Version. veafQraManager.Version = "1.2.4" -- trace level, specific to this module --veafQraManager.LogLevel = "trace" veaf.loggers.new(veafQraManager.Id, veafQraManager.LogLevel) function veafQraManager.statusToString(status) if status == veafQraManager.STATUS_WILLREARM then return "STATUS_WILLREARM" end if status == veafQraManager.STATUS_READY then return "STATUS_READY" end if status == veafQraManager.STATUS_READY_WAITINGFORMORE then return "STATUS_READY_WAITINGFORMORE" end if status == veafQraManager.STATUS_ACTIVE then return "STATUS_ACTIVE" end if status == veafQraManager.STATUS_DEAD then return "STATUS_DEAD" end return "" end veafQraManager.STATUS_WILLREARM = 0 veafQraManager.STATUS_READY = 1 veafQraManager.STATUS_READY_WAITINGFORMORE = 1.5 veafQraManager.STATUS_ACTIVE = 2 veafQraManager.STATUS_DEAD = 3 --scheduled states veafQraManager.STATUS_OUT = 4 veafQraManager.STATUS_NOAIRBASE = 5 veafQraManager.STATUS_STOP = 6 veafQraManager.WATCHDOG_DELAY = 5 veafQraManager.MINIMUM_LIFE_FOR_QRA_IN_PERCENT = 10 veafQraManager.DEFAULT_airbaseMinLifePercent = 0.9 veafQraManager.AllSilence = false --value to set all spawned QRAs to silent if true. By default it's false but this value can be set in the missionConfig veafQraManager.DEFAULT_MESSAGE_START = "%s is online" veafQraManager.DEFAULT_MESSAGE_DEPLOY = "%s is deploying" veafQraManager.DEFAULT_MESSAGE_DESTROYED = "%s has been destroyed" veafQraManager.DEFAULT_MESSAGE_READY = "%s is ready" veafQraManager.DEFAULT_MESSAGE_OUT = "%s is out of aircrafts" veafQraManager.DEFAULT_MESSAGE_RESUPPLIED = "%s has been resupplied" veafQraManager.DEFAULT_MESSAGE_AIRBASE_DOWN = "%s lost it's airbase" veafQraManager.DEFAULT_MESSAGE_AIRBASE_UP = "%s now has an airbase" veafQraManager.DEFAULT_MESSAGE_STOP = "%s is offline" ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Do not change anything below unless you know what you are doing! ------------------------------------------------------------------------------------------------------------------------------------------------------------- veafQraManager.qras = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- VeafQRA class methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- VeafQRA = {} function VeafQRA.init(object) -- technical name (QRA instance name) object.name = nil -- trigger zone name (if set, we'll use a DCS trigger zone) object.triggerZoneName = nil -- center (point in the center of the circle, when not using a DCS trigger zone) object.zoneCenter = nil -- radius (size of the circle, when not using a zone) object.zoneRadius = nil -- draw the zone on screen object.drawZone = false -- description for the briefing object.description = nil -- aircraft groups forming the QRA object.groups = {} -- aircraft groups forming the QRA, in a table by enemy quantity (i.e. if this number of enemies are in the zone, spawn these groups) object.groupsToDeployByEnemyQuantity = {} -- coalition for the QRA object.coalition = nil -- coalitions the QRA is defending against object.enemyCoalitions = {} -- message when the QRA is started object.messageStart = veafQraManager.DEFAULT_MESSAGE_START -- event when the QRA is started object.onStart = nil -- message when the QRA is triggered object.messageDeploy = veafQraManager.DEFAULT_MESSAGE_DEPLOY -- event when the QRA is triggered object.onDeploy = nil -- message when the QRA is destroyed object.messageDestroyed = veafQraManager.DEFAULT_MESSAGE_DESTROYED -- event when the QRA is destroyed object.onDestroyed = nil -- message when the QRA is ready object.messageReady = veafQraManager.DEFAULT_MESSAGE_READY -- event when the QRA is ready object.onReady = nil -- message when the QRA is out of aircrafts object.messageOut = veafQraManager.DEFAULT_MESSAGE_OUT -- event when the QRA is out of aircrafts object.onOut = nil -- message when the QRA has been resupplied and will start operations against object.messageResupplied = veafQraManager.DEFAULT_MESSAGE_RESUPPLIED -- event when the QRA has been resupplied and will start operations against object.onResupplied = nil -- message when the QRA has lost the airbase it operates from object.messageAirbaseDown = veafQraManager.DEFAULT_MESSAGE_AIRBASE_DOWN -- event when the QRA has lost the airbase it operates from object.onAirbaseDown = nil -- message when the QRA has retrieved the airbase it operates from and will start operations again object.messageAirbaseUp = veafQraManager.DEFAULT_MESSAGE_AIRBASE_UP -- event when the QRA has retrieved the airbase it operates from and will start operations again object.onAirbaseUp = nil -- message when the QRA is stopped object.messageStop = veafQraManager.DEFAULT_MESSAGE_STOP -- event when the QRA is stopped object.onStop = nil -- silent means no message is emitted object.silent = veafQraManager.AllSilence -- default position for respawns (im meters, lat/lon, relative to the zone center) object.respawnDefaultOffset = {latDelta=0, lonDelta=0} -- radius of the defenders groups spawn object.respawnRadius = 250 -- reacts when helicopters enter the zone object.reactOnHelicopters = false -- delay before activating object.delayBeforeActivating = -1 -- delay before rearming object.delayBeforeRearming = -1 -- the enemy does not have to leave the zone before the QRA is rearmed object.noNeedToLeaveZoneBeforeRearming = false -- reset the QRA immediately if all the enemy units leave the zone object.resetWhenLeavingZone = false -- maximum number of QRA ready for action at once, -1 indicates infinite object.QRAmaxCount = -1 -- number of groups of aircrafts that can be spawned for this QRA in total, -1 indicates infinite. object.QRAcount = -1 -- delay in minutes before the QRA counter is increased by one, simulating some sort of logistic chain of aircrafts. object.delayBeforeQRAresupply = 0 -- maximum number of resupplies at a given time, simulating some sort of warehousing, -1 indicates infinite. Is decremented every time a resupply happens. 0 indicates no resupply. object.QRAresupplyMax = -1 -- minimum QRAcount that will trigger a resupply, -1 indicates as soon as an aircraft is lost object.QRAminCountforResupply = -1 -- how many aircraft groups are resupplied at once object.resupplyAmount = 1 -- indicator to know if the QRA is being resupplied or not object.isResupplying = false -- name of the airport to which the QRA is linked, QRAs will be deployed only if this is set and the airport is captured by the QRA's coalition or if this is not set object.airportLink = nil -- minimum linked airbase life percentage (from 0 to 1) for the QRA to have it's airbase available object.airportMinLifePercent = veafQraManager.DEFAULT_airbaseMinLifePercent -- boolean to know if the status OUT was announced or not object.outAnnounced = false -- boolean to know if the status NOAIRBASE was announced or not object.noAB_announced = false -- minimum number of enemies in the zone to trigger deployment; updated automatically by setGroupsToDeployByEnemyQuantity object.minimumNbEnemyPlanes = -1 -- planes in the zone will only be detected below this altitude (in feet) object.minimumAltitude = -999999 -- planes in the zone will only be detected above this altitude (in feet) object.maximumAltitude = 999999 object.timer = nil object.state = nil object.scheduled_state = nil object._enemyHumanUnits = nil object.spawnedGroupsNames = {} end function VeafQRA.ToggleAllSilence(state) if state then veafQraManager.AllSilence = true else veafQraManager.AllSilence = false end end function VeafQRA:new(objectToCopy) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA:new()") local objectToCreate = objectToCopy or {} -- create object if user does not provide one setmetatable(objectToCreate, self) self.__index = self -- init the new object VeafQRA.init(objectToCreate) return objectToCreate end function VeafQRA:setName(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[]:setName(%s)", veaf.p(value)) self.name = value return veafQraManager.add(self) -- add the QRA to the QRA list as soon as a name is available to index it end function VeafQRA:setTriggerZone(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setTriggerZone(%s)", veaf.p(self.name), veaf.p(value)) self.triggerZoneName = value return self end function VeafQRA:setZoneCenter(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setZoneCenter(%s)", veaf.p(self.name), veaf.p(value)) self.zoneCenter = value return self end function VeafQRA:setZoneCenterFromCoordinates(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setZoneCenterFromCoordinates(%s)", veaf.p(self.name), veaf.p(value)) local _lat, _lon = veaf.computeLLFromString(value) local vec3 = coord.LLtoLO(_lat, _lon) return self:setZoneCenter(vec3) end function VeafQRA:setZoneRadius(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setZoneRadius(%s)", veaf.p(self.name), veaf.p(value)) self.zoneRadius = value return self end function VeafQRA:setDescription(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setDescription(%s)", veaf.p(self.name), veaf.p(value)) self.description = value return veafQraManager.add(self) -- add the QRA to the QRA list as soon as a name is available to index it end function VeafQRA:getDescription() return self.description or self.name end function VeafQRA:getName() return self.name or self.description end function VeafQRA:addGroup(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:addGroup(%s)", veaf.p(self.name), veaf.p(value)) if not self.groupsToDeployByEnemyQuantity[1] then self.groupsToDeployByEnemyQuantity[1] = {} end table.insert(self.groupsToDeployByEnemyQuantity[1], value) return self end function VeafQRA:addRandomGroup(groups, number, bias) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:addRandomGroup(%s, %s, %s)", veaf.p(self.name), veaf.p(groups), veaf.p(number), veaf.p(bias)) return self:addGroup({groups, number or 1, bias or 0}) end function VeafQRA:setGroupsToDeployByEnemyQuantity(enemyNb, groupsToDeploy) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setGroupsToDeployByEnemyQuantity(%s) -> %s", veaf.p(self.name), veaf.p(enemyNb), veaf.p(groupsToDeploy)) self.groupsToDeployByEnemyQuantity[enemyNb] = groupsToDeploy if self.minimumNbEnemyPlanes == -1 or self.minimumNbEnemyPlanes > enemyNb then self.minimumNbEnemyPlanes = enemyNb end return self end function VeafQRA:setRandomGroupsToDeployByEnemyQuantity(enemyNb, groups, number, bias) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setRandomGroupsToDeployByEnemyQuantity(%s, %s, %s, %s)", veaf.p(self.name), veaf.p(enemyNb), veaf.p(groups), veaf.p(number), veaf.p(bias)) return self:setGroupsToDeployByEnemyQuantity(enemyNb, {groups, number or 1, bias or 0}) end function VeafQRA:setCoalition(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setCoalition(%s)", veaf.p(self.name), veaf.p(value)) self.coalition = value return self end function VeafQRA:addEnnemyCoalition(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:addEnnemyCoalition(%s)", veaf.p(self.name), veaf.p(value)) self.enemyCoalitions[value] = value return self end function VeafQRA:getEnnemyCoalition() local result = nil for coalition, _ in pairs(self.enemyCoalitions) do result = coalition break end return result end function VeafQRA:setMessageStart(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setMessageStart(%s)", veaf.p(self.name), veaf.p(value)) self.messageStart = value return self end function VeafQRA:setOnStart(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setOnStart()", veaf.p(self.name)) self.onStart = value return self end function VeafQRA:setMessageDeploy(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setMessageDeploy(%s)", veaf.p(self.name), veaf.p(value)) self.messageDeploy = value return self end function VeafQRA:setOnDeploy(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setOnDeploy()", veaf.p(self.name)) self.onDeploy = value return self end function VeafQRA:setMessageDestroyed(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setMessageDestroyed(%s)", veaf.p(self.name), veaf.p(value)) self.messageDestroyed = value return self end function VeafQRA:setOnDestroyed(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setOnDestroyed()", veaf.p(self.name)) self.onDestroyed = value return self end function VeafQRA:setMessageReady(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setMessageReady(%s)", veaf.p(self.name), veaf.p(value)) self.messageReady = value return self end function VeafQRA:setOnReady(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setOnReady()", veaf.p(self.name)) self.onReady = value return self end function VeafQRA:setMessageOut(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setMessageOut(%s)", veaf.p(self.name), veaf.p(value)) self.messageOut = value return self end function VeafQRA:setOnOut(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setOnOut()", veaf.p(self.name)) self.onOut = value return self end function VeafQRA:setMessageResupplied(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setMessageResupplied(%s)", veaf.p(self.name), veaf.p(value)) self.messageResupplied = value return self end function VeafQRA:setOnResupplied(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setOnResupplied()", veaf.p(self.name)) self.onResupplied = value return self end function VeafQRA:setMessageAirbaseDown(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setMessageAirbaseDown(%s)", veaf.p(self.name), veaf.p(value)) self.messageAirbaseDown = value return self end function VeafQRA:setOnAirbaseDown(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setOnAirbaseDown()", veaf.p(self.name)) self.onAirbaseDown = value return self end function VeafQRA:setMessageAirbaseUp(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setMessageAirbaseUp(%s)", veaf.p(self.name), veaf.p(value)) self.messageAirbaseUp = value return self end function VeafQRA:setOnAirbaseUp(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setOnAirbaseUp()", veaf.p(self.name)) self.onAirbaseUp = value return self end function VeafQRA:setMessageStop(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setMessageStop(%s)", veaf.p(self.name), veaf.p(value)) self.messageStop = value return self end function VeafQRA:setOnStop(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setOnStop()", veaf.p(self.name)) self.onStop = value return self end function VeafQRA:setSilent(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setSilent(%s)", veaf.p(self.name), veaf.p(value)) self.silent = value or false return self end function VeafQRA:setDrawZone(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setDrawZone(%s)", veaf.p(self.name), veaf.p(value)) self.drawZone = value or false return self end --TODO, warehousing for each group within a QRA and not just the whole QRA function VeafQRA:setQRAcount(count) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setQRAcount(%s)", veaf.p(self.name), veaf.p(count)) if count and type(count) == 'number' and count >= -1 then self.QRAcount = count end return self end function VeafQRA:setQRAmaxCount(maxCount) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setQRAmaxCount(%s)", veaf.p(self.name), veaf.p(maxCount)) if maxCount and type(maxCount) == 'number' and maxCount >= -1 then self.QRAmaxCount = maxCount end return self end function VeafQRA:setQRAresupplyDelay(resupplyDelay) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setQRAresupplyDelay(%s)", veaf.p(self.name), veaf.p(resupplyDelay)) if resupplyDelay and type(resupplyDelay) == 'number' and resupplyDelay >= 0 then self.delayBeforeQRAresupply = resupplyDelay end return self end function VeafQRA:setQRAmaxResupplyCount(maxResupplyCount) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setQRAmaxResupplyCount(%s)", veaf.p(self.name), veaf.p(maxResupplyCount)) if maxResupplyCount and type(maxResupplyCount) == 'number' and maxResupplyCount >= -1 then self.QRAresupplyMax = maxResupplyCount end return self end function VeafQRA:setQRAminCountforResupply(minCountforResupply) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setQRAminCountforResupply(%s)", veaf.p(self.name), veaf.p(minCountforResupply)) if minCountforResupply and type(minCountforResupply) == 'number' and minCountforResupply >= -1 and minCountforResupply ~= 0 then self.QRAminCountforResupply = minCountforResupply end return self end function VeafQRA:setResupplyAmount(resupplyAmount) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setResupplyAmount(%s)", veaf.p(self.name), veaf.p(resupplyAmount)) if resupplyAmount and type(resupplyAmount) == 'number' and resupplyAmount >= 1 then self.resupplyAmount = resupplyAmount end return self end function VeafQRA:setAirportLink(airport_name) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setAirportLink(%s)", veaf.p(self.name), veaf.p(airport_name)) if airport_name and type(airport_name) == 'string' and Airbase.getByName(airport_name) then self.airportLink = airport_name end return self end function VeafQRA:setAirportMinLifePercent(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setAirportMinLifePercent(%s)", veaf.p(self.name), veaf.p(value)) if value and value >= 0 and value <= 1 then self.airportMinLifePercent = value end return self end function VeafQRA:setReactOnHelicopters() veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setReactOnHelicopters()", veaf.p(self.name)) self.reactOnHelicopters = true return self end ---set the default respawn offset (in meters, relative to the zone center) ---@param defaultOffsetLatitude any in meters ---@param defaultOffsetLongitude any in meters ---@return table self function VeafQRA:setRespawnDefaultOffset(defaultOffsetLatitude, defaultOffsetLongitude) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setRespawnDefaultOffset(%s, %s)", veaf.p(self.name), veaf.p(defaultOffsetLatitude), veaf.p(defaultOffsetLongitude)) self.respawnDefaultOffset = { latDelta = defaultOffsetLatitude, lonDelta = defaultOffsetLongitude} return self end function VeafQRA:setRespawnRadius(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setRespawnRadius(%s)", veaf.p(self.name), veaf.p(value)) self.respawnRadius = value if self.respawnRadius < 250 then self.respawnRadius = 250 end return self end function VeafQRA:setDelayBeforeRearming(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setDelayBeforeRearming(%s)", veaf.p(self.name), veaf.p(value)) self.delayBeforeRearming = value return self end function VeafQRA:setNoNeedToLeaveZoneBeforeRearming() veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setNoNeedToLeaveZoneBeforeRearming()", veaf.p(self.name)) self.noNeedToLeaveZoneBeforeRearming = true return self end function VeafQRA:setResetWhenLeavingZone() veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setResetWhenLeavingZone()", veaf.p(self.name)) self.resetWhenLeavingZone = true return self end function VeafQRA:setDelayBeforeActivating(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setDelayBeforeActivating(%s)", veaf.p(self.name), veaf.p(value)) self.delayBeforeActivating = value return self end function VeafQRA:setMinimumAltitudeInFeet(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setMinimumAltitudeInFeet(%s)", veaf.p(self.name), veaf.p(value)) self.minimumAltitude = value * 0.3048 -- convert from feet return self end function VeafQRA:getMinimumAltitudeInMeters() return self.minimumAltitude end function VeafQRA:setMaximumAltitudeInFeet(value) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setMaximumAltitudeInFeet(%s)", veaf.p(self.name), veaf.p(value)) self.maximumAltitude = value * 0.3048 -- convert from feet return self end function VeafQRA:getMaximumAltitudeInMeters() return self.maximumAltitude end function VeafQRA:humanBornEvent(unit) veaf.loggers.get(veafQraManager.Id):trace("VeafQRA[%s]:humanBornEvent(%s)", self.name, unit) if not self._enemyHumanUnits then return -- do this later ^^ end local coalitionId = 0 if unit.unitCoalition then coalitionId = unit.unitCoalition end if self.enemyCoalitions[coalitionId] then veaf.loggers.get(veafQraManager.Id):trace("VeafQRA[%s]:humanBornEvent() - unit being born is an enemy (coalition %s)", self.name, coalitionId) if unit.unitCategory then if (unit.unitCategory == Unit.Category.AIRPLANE) or (unit.unitCategory == Unit.Category.HELICOPTER and self.reactOnHelicopters) then -- check if the unit is already in the list for _, unitName in pairs(self._enemyHumanUnits) do if unitName == unit.unitName then return end end veaf.loggers.get(veafQraManager.Id):trace("adding unit to enemy human units for QRA") table.insert(self._enemyHumanUnits, unit.unitName) end end end end function VeafQRA:_getEnemyHumanUnits() if not self._enemyHumanUnits then veaf.loggers.get(veafQraManager.Id):trace("VeafQRA[%s]:_getEnemyHumanUnits() - computing", veaf.p(self.name)) self._enemyHumanUnits = {} for _, unit in pairs(mist.DBs.humansByName) do local coalitionId = 0 if unit.coalition then if unit.coalition:lower() == "red" then coalitionId = coalition.side.RED elseif unit.coalition:lower() == "blue" then coalitionId = coalition.side.BLUE end end if self.enemyCoalitions[coalitionId] then if unit.category then if (unit.category == "plane") or (unit.category == "helicopter" and self.reactOnHelicopters) then veaf.loggers.get(veafQraManager.Id):trace("adding unit to enemy human units for QRA") table.insert(self._enemyHumanUnits, unit.unitName) end end end end end return self._enemyHumanUnits end function VeafQRA:check() veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:check()", veaf.p(self.name)) veaf.loggers.get(veafQraManager.Id):debug("self.state=%s", veaf.p(veafQraManager.statusToString(self.state))) veaf.loggers.get(veafQraManager.Id):trace("timer.getTime()=%s", veaf.p(timer.getTime())) --scheduled state application is attempted regardless of airportlink checks etc. to take into account user requested states which go through scheduled_states as well --Stop scheduled is checked before even running the check function as it has the highest priority self:applyScheduledState() if self.state ~= veafQraManager.STATUS_STOP then --if the QRA is linked to an airbase. Airport is checked before even trying to deploy a group and check warehousing which has a lower priority if self.airportLink then veaf.loggers.get(veafQraManager.Id):trace("Checking Airport link : %s", veaf.p(self.airportLink)) self:checkAirport() self:applyScheduledState() end if self.state ~= veafQraManager.STATUS_NOAIRBASE then --if warehousing is activated. Warehousing is checked before even trying to deploy a group if self.QRAcount ~= -1 then veaf.loggers.get(veafQraManager.Id):trace("Checking Warehousing...") veaf.loggers.get(veafQraManager.Id):trace("QRACount : %s", veaf.p(self.QRAcount)) self:checkWarehousing() self:applyScheduledState() end if self.state ~= veafQraManager.STATUS_OUT then local unitNames = self:_getEnemyHumanUnits() local unitsInZone = nil local triggerZone = veaf.getTriggerZone(self.triggerZoneName) if (not veaf.isNullOrEmpty(self.triggerZoneName) and triggerZone == nil) then veaf.loggers.get(veafQraManager.Id):error("QRA has a non-existant zone: " .. self.triggerZoneName) end unitsInZone = {} if triggerZone then if triggerZone.type == 0 then -- circular unitsInZone = mist.getUnitsInZones(unitNames, {self.triggerZoneName}) elseif triggerZone.type == 2 then -- quad point unitsInZone = mist.getUnitsInPolygon(unitNames, triggerZone.verticies) end elseif self.zoneCenter then unitsInZone = veaf.findUnitsInCircle(self.zoneCenter, self.zoneRadius, false, unitNames) else veaf.loggers.get(veafQraManager.Id):error("QRA [%s] has no zone defined, cannot check for units in zone", self.name) return end veaf.loggers.get(veafQraManager.Id):trace("unitsInZone=%s", unitsInZone) local nbUnitsInZone = 0 for _, unit in pairs(unitsInZone) do -- check the unit altitude against the ceiling and floor if unit:inAir() then -- never count a landed aircraft local alt = unit:getPoint().y if alt >= self:getMinimumAltitudeInMeters() and alt <= self:getMaximumAltitudeInMeters() then nbUnitsInZone = nbUnitsInZone + 1 end end end veaf.loggers.get(veafQraManager.Id):trace("nbUnitsInZone=%s", nbUnitsInZone) if (self.state == veafQraManager.STATUS_READY) and (unitsInZone and nbUnitsInZone > 0) then veaf.loggers.get(veafQraManager.Id):debug("self.state set to veafQraManager.STATUS_READY_WAITINGFORMORE at timer.getTime()=%s", timer.getTime()) self.state = veafQraManager.STATUS_READY_WAITINGFORMORE self.timeSinceReady = timer.getTime() elseif (self.state == veafQraManager.STATUS_READY_WAITINGFORMORE) and (unitsInZone and nbUnitsInZone > 0) and (timer.getTime() - self.timeSinceReady > self.delayBeforeActivating) then -- trigger the QRA self:deploy(nbUnitsInZone) self.timeSinceReady = -1 elseif (self.state == veafQraManager.STATUS_DEAD) and (self.noNeedToLeaveZoneBeforeRearming or (not unitsInZone or nbUnitsInZone == 0)) then -- rearm the QRA after a delay (if set) if self.delayBeforeRearming > 0 then mist.scheduleFunction(VeafQRA.rearm, {self}, timer.getTime()+self.delayBeforeRearming) self.state = veafQraManager.STATUS_WILLREARM else self:rearm() end elseif (self.state == veafQraManager.STATUS_ACTIVE) then local qraAlive = false local qraInAir = false for _, groupName in pairs(self.spawnedGroupsNames) do local group = Group.getByName(groupName) if group then local groupAtLeastOneUnitAlive = false local groupAtLeastOneUnitInAir = false local category = group:getCategory() local units = group:getUnits() if units then for _,unit in pairs(units) do if unit then local unitLife = unit:getLife() local unitLife0 = 0 if unit.getLife0 then -- statics have no life0 unitLife0 = unit:getLife0() end local unitLifePercent = unitLife if unitLife0 > 0 then unitLifePercent = 100 * unitLife / unitLife0 end if unitLifePercent >= veafQraManager.MINIMUM_LIFE_FOR_QRA_IN_PERCENT then groupAtLeastOneUnitAlive = true end if category == 0 --[[airplanes]] or category == 1 --[[helicopters]] then -- check if at least one unit is still airborne if unit:inAir() then groupAtLeastOneUnitInAir = true end else -- consider that ground units have never landed groupAtLeastOneUnitInAir = true end end end end qraAlive = qraAlive or groupAtLeastOneUnitAlive qraInAir = qraInAir or groupAtLeastOneUnitInAir veaf.loggers.get(veafQraManager.Id):trace("qraAlive=%s", veaf.p(qraAlive)) veaf.loggers.get(veafQraManager.Id):trace("qraInAir=%s", veaf.p(qraInAir)) end end if not qraAlive then -- signal QRA destroyed self:destroyed() elseif (self.resetWhenLeavingZone and nbUnitsInZone == 0) or not qraInAir then -- QRA reset self:rearm() end end end end mist.scheduleFunction(VeafQRA.check, {self}, timer.getTime() + veafQraManager.WATCHDOG_DELAY) end end function VeafQRA:setScheduledState(scheduledState) --priority level 1 veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:setScheduledState(%s)", veaf.p(self.name), veaf.p(scheduledState)) if scheduledState == veafQraManager.STATUS_STOP then self.scheduled_state = veafQraManager.STATUS_STOP veaf.loggers.get(veafQraManager.Id):debug("QRA STOP scheduled") --priority level 2 elseif scheduledState == veafQraManager.STATUS_NOAIRBASE and self.scheduled_state ~= veafQraManager.STATUS_STOP then self.scheduled_state = veafQraManager.STATUS_NOAIRBASE veaf.loggers.get(veafQraManager.Id):debug("QRA NOAIRBASE scheduled") --priority level 3 elseif scheduledState == veafQraManager.STATUS_OUT and self.scheduled_state ~= veafQraManager.STATUS_STOP and self.scheduled_state ~= veafQraManager.STATUS_NOAIRBASE then self.scheduled_state = veafQraManager.STATUS_OUT veaf.loggers.get(veafQraManager.Id):debug("QRA OUT scheduled") end return self end function VeafQRA:applyScheduledState() if self.scheduled_state and self.state ~= veafQraManager.STATUS_ACTIVE then veaf.loggers.get(veafQraManager.Id):debug("QRA taking scheduled status : %s", veaf.p(self.scheduled_state)) self.state = self.scheduled_state end end function VeafQRA:checkAirport() local QRA_airportObject = veaf.getAirbaseForCoalition(self.airportLink, self.coalition) local airport_life_percent = nil if QRA_airportObject then airport_life_percent = veaf.getAirbaseLife(self.airportLink, true) end veaf.loggers.get(veafQraManager.Id):trace("VeafQRA[%s] is linked to airbase %s", veaf.p(self.name), veaf.p(self.airportLink)) if not QRA_airportObject or airport_life_percent < self.airportMinLifePercent then veaf.loggers.get(veafQraManager.Id):trace("QRA lost it's airbase") self:setScheduledState(veafQraManager.STATUS_NOAIRBASE) if not self.silent and not self.noAB_announced then local msg = string.format(self.messageAirbaseDown, self:getDescription()) for coalition, _ in pairs(self.enemyCoalitions) do trigger.action.outTextForCoalition(coalition, msg, 15) end end if self.onAirbaseDown then self.onAirbaseDown(QRA_airportObject) end self.noAB_announced = true elseif self.state == veafQraManager.STATUS_NOAIRBASE then veaf.loggers.get(veafQraManager.Id):trace("QRA has it's airbase %s", veaf.p(QRA_airportObject:getName())) if not self.silent then local msg = string.format(self.messageAirbaseUp, self:getDescription()) for coalition, _ in pairs(self.enemyCoalitions) do trigger.action.outTextForCoalition(coalition, msg, 15) end end if self.onAirbaseUp then self.onAirbaseUp(QRA_airportObject) end self.noAB_announced = false self.state = veafQraManager.STATUS_DEAD --QRA that have just been recommisionned act as if they were dead since they need to be rearmed after a delay if self.scheduled_state == veafQraManager.STATUS_NOAIRBASE then self.scheduled_state = nil end --make sure you reset the scheduled state if you are within the bounds of this method end end -- -- maximum number of QRA ready for action at once, -1 indicates infinite -- QRAmaxCount = -1 -- -- number of groups of aircrafts that can be spawned for this QRA in total, -1 indicates infinite -- QRAcount = -1 -- -- delay in minutes before the QRA counter is increased by one, simulating some sort of logistic chain of aircrafts. -- delayBeforeQRAresupply = 0 -- -- maximum number of resupplies at a given time, simulating some sort of warehousing, -1 indicates infinite. Is decremented every time a resupply happens if not equal to -1 originally. 0 indicated no resupply. -- QRAresupplyMax = -1 -- -- minimum QRAcount that will trigger a resupply, -1 indicates as soon as an aircraft is lost -- QRAminCountforResupply = -1 -- -- how many aircraft groups are resupplied at once --resupplyAmount = 1 -- -- indicator to know if the QRA is being resupplied or not --isResupplying = false function VeafQRA:checkWarehousing() veaf.loggers.get(veafQraManager.Id):trace("VeafQRA[%s] resupply state is %s", veaf.p(self.name), veaf.p(self.isResupplying)) --if a resupply is not already on the way and if there are aircrafts in stock and if the available aircraft count is below the threshold or if an aircraft was just lost and the resupply mode indicates to resupply whenever an aircraft is lost if not self.isResupplying and self.QRAresupplyMax ~= 0 and (self.QRAcount < self.QRAminCountforResupply or (self.QRAcount < self.QRAmaxCount or (self.QRAmaxCount == -1 and self.state == veafQraManager.STATUS_DEAD)) and self.QRAminCountforResupply == -1) then veaf.loggers.get(veafQraManager.Id):trace("QRA has %s/%s aircraft groups available", veaf.p(self.QRAcount), veaf.p(self.QRAmaxCount)) veaf.loggers.get(veafQraManager.Id):trace("QRA has %s aircraft groups ready for resupply (-1 for infinite)", veaf.p(self.QRAresupplyMax)) veaf.loggers.get(veafQraManager.Id):trace("QRA resupply asks for %s aircraft groups", veaf.p(self.resupplyAmount)) local resupplyAmount = self.resupplyAmount --take into account the maximum number of QRA groups as to not oversupply it if self.QRAmaxCount ~= -1 and resupplyAmount > self.QRAmaxCount - self.QRAcount then resupplyAmount = self.QRAmaxCount - self.QRAcount veaf.loggers.get(veafQraManager.Id):trace("There are only %s available aircraft group slots for this QRA", veaf.p(self.QRAmaxCount - self.QRAcount)) end --take into account the maximum number of QRA groups that can be supplied by the stock if self.QRAresupplyMax ~= -1 and resupplyAmount > self.QRAresupplyMax then resupplyAmount = self.QRAresupplyMax veaf.loggers.get(veafQraManager.Id):trace("QRA can only be resupplied by %s aircraft groups", veaf.p(self.QRAresupplyMax)) end veaf.loggers.get(veafQraManager.Id):trace("%s aircraft groups will be handled for resupply", veaf.p(resupplyAmount)) if resupplyAmount > 0 then self.isResupplying = true if self.delayBeforeQRAresupply > 0 then veaf.loggers.get(veafQraManager.Id):trace("QRA will be resupplied in %s seconds", veaf.p(self.delayBeforeQRAresupply)) mist.scheduleFunction(VeafQRA.resupply, {self, resupplyAmount}, timer.getTime()+self.delayBeforeQRAresupply) else veaf.loggers.get(veafQraManager.Id):trace("QRA is being resupplied...") self:resupply(resupplyAmount) end end end if self.QRAcount == 0 then veaf.loggers.get(veafQraManager.Id):trace("QRA is out of aircraft groups") if not self.silent and not self.outAnnounced then local msg = string.format(self.messageOut, self:getDescription()) for coalition, _ in pairs(self.enemyCoalitions) do trigger.action.outTextForCoalition(coalition, msg, 15) end self.outAnnounced = true end if self.onOut then self.onOut() end self:setScheduledState(veafQraManager.STATUS_OUT) end end function VeafQRA:resupply(resupplyAmount) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:resupply(%s)", veaf.p(self.name), veaf.p(resupplyAmount)) --if the QRA can still operate, then execute the resupply, the list would need to be expanded if new scheduled status blocking operations were added if self.scheduled_state ~= veafQraManager.STATUS_NOAIRBASE and self.scheduled_state ~= veafQraManager.STATUS_STOP then if resupplyAmount and type(resupplyAmount) == 'number' and resupplyAmount > 0 then veaf.loggers.get(veafQraManager.Id):trace("QRA is going to be resupplied, old count is : %s", veaf.p(self.QRAcount)) self.QRAcount = self.QRAcount + resupplyAmount veaf.loggers.get(veafQraManager.Id):trace("QRA was resupplied, new count is : %s", veaf.p(self.QRAcount)) veaf.loggers.get(veafQraManager.Id):trace("QRA previously had %s aircraft groups ready for resupply (-1 for infinite)", veaf.p(self.QRAresupplyMax)) if self.QRAresupplyMax ~= -1 then self.QRAresupplyMax = self.QRAresupplyMax - resupplyAmount if self.QRAresupplyMax < 0 then self.QRAresupplyMax = 0 end end veaf.loggers.get(veafQraManager.Id):trace("QRA now only has %s aircraft groups ready for resupply (-1 for infinite)", veaf.p(self.QRAresupplyMax)) if self.state == veafQraManager.STATUS_OUT then veaf.loggers.get(veafQraManager.Id):trace("QRA now has at least one aircraft group ready for action, resuming service...") if not self.silent then local msg = string.format(self.messageResupplied, self:getDescription()) for coalition, _ in pairs(self.enemyCoalitions) do trigger.action.outTextForCoalition(coalition, msg, 15) end end if self.onResupplied then self.onResupplied() end self.outAnnounced = false self.state = veafQraManager.STATUS_DEAD --QRA that have just arrived act as if the QRA had just died, they need to be rearmed if self.scheduled_state == veafQraManager.STATUS_OUT then self.scheduled_state = nil end --make sure you reset the scheduled state if you are within the bounds of this method end end else veaf.loggers.get(veafQraManager.Id):trace("QRA is no longer operating, resupply did not take place") end self.isResupplying = false end function VeafQRA:chooseGroupsToDeploy(nbUnitsInZone) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:chooseGroupsToDeploy(%s)", veaf.p(self.name), veaf.p(nbUnitsInZone)) local biggestNumberLowerThanUnitsInZone = -1 local groupsToDeploy = nil for enemyNb, groups in pairs(self.groupsToDeployByEnemyQuantity) do if nbUnitsInZone >= enemyNb then biggestNumberLowerThanUnitsInZone = enemyNb groupsToDeploy = groups end end if groupsToDeploy then -- process a random group definition local groupsToChooseFrom = groupsToDeploy[1] local numberOfGroups = groupsToDeploy[2] local bias = groupsToDeploy[3] if groupsToChooseFrom and type(groupsToChooseFrom) == "table" and numberOfGroups and type(numberOfGroups) == "number" and bias and type(bias) == "number" then local result = {} for _ = 1, numberOfGroups do local group = veaf.randomlyChooseFrom(groupsToChooseFrom, bias) veaf.loggers.get(veafQraManager.Id):trace("group=%s", veaf.p(group)) table.insert(result, group) end groupsToDeploy = result end end return groupsToDeploy end function VeafQRA:deploy(nbUnitsInZone) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:deploy()", veaf.p(self.name)) veaf.loggers.get(veafQraManager.Id):trace("nbUnitsInZone=[%s]", veaf.p(nbUnitsInZone)) if self.minimumNbEnemyPlanes ~= -1 and self.minimumNbEnemyPlanes > nbUnitsInZone then veaf.loggers.get(veafQraManager.Id):trace("not enough enemies in zone, min=%s", veaf.p(self.minimumNbEnemyPlanes)) return end if not self.silent then local msg = string.format(self.messageDeploy, self:getDescription()) for coalition, _ in pairs(self.enemyCoalitions) do trigger.action.outTextForCoalition(coalition, msg, 15) end end local groupsToDeploy = self:chooseGroupsToDeploy(nbUnitsInZone) self.spawnedGroupsNames = {} if groupsToDeploy then local zoneCenter = {} if self.triggerZoneName then local triggerZone = veaf.getTriggerZone(self.triggerZoneName) zoneCenter.x = triggerZone.x zoneCenter.z = triggerZone.y zoneCenter.y = 0 elseif self.zoneCenter then zoneCenter = self.zoneCenter end for _, groupNameOrCommand in pairs(groupsToDeploy) do -- check if this is a DCS group or a VEAF command if veaf.startsWith(groupNameOrCommand, "[") or veaf.startsWith(groupNameOrCommand, "-") then -- this is a command local command = groupNameOrCommand local latDelta = self.respawnDefaultOffset.latDelta local lonDelta = self.respawnDefaultOffset.lonDelta if veaf.startsWith(groupNameOrCommand, "[") then -- extract relative coordinates and the actual command local coords coords, command = groupNameOrCommand:match("%[(.*)%](.*)") veaf.loggers.get(veafQraManager.Id):trace("coords=%s", veaf.p(coords)) veaf.loggers.get(veafQraManager.Id):trace("command=%s", veaf.p(command)) if coords then latDelta, lonDelta = coords:match("([%+-%d]+),%s*([%+-%d]+)") end end veaf.loggers.get(veafQraManager.Id):debug("running command [%s]", veaf.p(command)) veaf.loggers.get(veafQraManager.Id):trace("latDelta = [%s]", veaf.p(latDelta)) veaf.loggers.get(veafQraManager.Id):trace("lonDelta = [%s]", veaf.p(lonDelta)) local position = {x = zoneCenter.x - lonDelta, y = zoneCenter.y, z = zoneCenter.z + latDelta} local randomPosition = mist.getRandPointInCircle(position, self.respawnRadius) local spawnedGroupsNames = {} veafInterpreter.execute(command, randomPosition, self.coalition, nil, spawnedGroupsNames) for _, newGroupName in pairs(spawnedGroupsNames) do table.insert(self.spawnedGroupsNames, newGroupName) end else -- this is a DCS group local groupName = groupNameOrCommand veaf.loggers.get(veafQraManager.Id):debug("spawning group [%s]", veaf.p(groupName)) local group = Group.getByName(groupName) if not group then veaf.loggers.get(veafQraManager.Id):error("group [%s] does not exist in the mission!", veaf.p(groupName)) else veaf.loggers.get(veafQraManager.Id):debug("group=%s", veaf.p(group)) veaf.loggers.get(veafQraManager.Id):debug("group:getUnits()=%s", veaf.p(group:getUnits())) local spawnSpot = {x = zoneCenter.x - self.respawnDefaultOffset.lonDelta, y = zoneCenter.y, z = zoneCenter.z + self.respawnDefaultOffset.latDelta} -- Try and set the spawn spot at the place the group has been set in the Mission Editor. -- Unfortunately this is sometimes not possible because DCS is not returning the group units for some reason. -- When this happens we'll default to the default spawn offset (same as spawning with VEAF commands) if not group:getUnit(1) then veaf.loggers.get(veafQraManager.Id):warn("group [%s] does not have any unit!", veaf.p(groupName)) else spawnSpot = group:getUnit(1):getPoint() end local vars = {} vars.point = mist.getRandPointInCircle(spawnSpot, self.respawnRadius) vars.point.z = vars.point.y vars.point.y = spawnSpot.y vars.gpName = groupName vars.action = 'clone' vars.route = mist.getGroupRoute(groupName, 'task') local newGroup = mist.teleportToPoint(vars) -- respawn with radius if newGroup then table.insert(self.spawnedGroupsNames, newGroup.name) end end end end veaf.loggers.get(veafQraManager.Id):trace("self.spawnedGroups=%s", veaf.p(self.spawnedGroupsNames)) self.state = veafQraManager.STATUS_ACTIVE end if self.onDeploy then self.onDeploy(nbUnitsInZone) end end function VeafQRA:destroyed() veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:destroyed()", veaf.p(self.name)) if not self.silent then local msg = string.format(self.messageDestroyed, self:getDescription()) for coalition, _ in pairs(self.enemyCoalitions) do trigger.action.outTextForCoalition(coalition, msg, 15) end end if self.onDestroyed then self.onDestroyed() end self.state = veafQraManager.STATUS_DEAD if self.QRAcount > 0 then veaf.loggers.get(veafQraManager.Id):trace("QRA will now see one of it's aicraft groups removed") self.QRAcount = self.QRAcount - 1 end end function VeafQRA:rearm(silent) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:rearm()", veaf.p(self.name)) if not self.silent and not silent then local msg = string.format(self.messageReady, self:getDescription()) for coalition, _ in pairs(self.enemyCoalitions) do trigger.action.outTextForCoalition(coalition, msg, 15) end end if self.spawnedGroupsNames then for _, groupName in pairs(self.spawnedGroupsNames) do local group = Group.getByName(groupName) if group then group:destroy() end end end if self.onReady then self.onReady() end self.state = veafQraManager.STATUS_READY end function VeafQRA:start() veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:start()", veaf.p(self.name)) self.scheduled_state = nil --make sure you reset the scheduled state if you are within the bounds of this method self:rearm() self:check() -- draw the zone if self.drawZone then if self.triggerZoneName then self.zoneDrawing = mist.marker.drawZone(self.triggerZoneName, {message=self:getDescription(), readOnly=true}) else self.zoneDrawing = VeafCircleOnMap:new() :setName(self:getName()) :setCoalition(self:getEnnemyCoalition()) :setCenter(self.zoneCenter) :setRadius(self.zoneRadius) :setLineType("dashed") :setColor("white") :setFillColor("transparent") :draw() end end if not self.silent then local msg = string.format(self.messageStart, self:getDescription()) for coalition, _ in pairs(self.enemyCoalitions) do trigger.action.outTextForCoalition(coalition, msg, 15) end end if self.onStart then self.onStart() end return self end function VeafQRA:stop(silent) veaf.loggers.get(veafQraManager.Id):debug("VeafQRA[%s]:stop()", veaf.p(self.name)) self:setScheduledState(veafQraManager.STATUS_STOP) -- just in case, despawn the spawned groups if self.spawnedGroupsNames then for _, groupName in pairs(self.spawnedGroupsNames) do local group = Group.getByName(groupName) if group then group:destroy() end end end -- erase the zone if self.zoneDrawing then if self.triggerZoneName then mist.marker.remove(self.zoneDrawing.markId) else self.zoneDrawing:erase() end self.zoneDrawing = nil end if not self.silent and not silent then local msg = string.format(self.messageStop, self:getDescription()) for coalition, _ in pairs(self.enemyCoalitions) do trigger.action.outTextForCoalition(coalition, msg, 15) end end if self.onStop then self.onStop() end return self end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Utility methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafQraManager.add(aQraObject, aName) local name = aName or aQraObject:getName() veafQraManager.qras[name] = aQraObject return aQraObject end function veafQraManager.get(aNameString) return veafQraManager.qras[aNameString] end --- --- called from veafEventHandler when a unit is created function veafQraManager.eventHandler(event) -- find the originator unit local unitName = event.initiator and event.initiator.unitName if not unitName then return end if mist.DBs.humansByName[unitName] then -- it's a human unit local unit = event.initiator if unit ~= nil then -- handle the event on all QRAs for _, qra in pairs(veafQraManager.qras) do qra:humanBornEvent(unit) end end end end function veafQraManager.initialize() veaf.loggers.get(veafQraManager.Id):debug("veafQraManager.initialize()") veafEventHandler.addCallback("veafQraManager.eventHandler", {"S_EVENT_BIRTH", "S_EVENT_PLAYER_ENTER_UNIT"}, veafQraManager.eventHandler) end veaf.loggers.get(veafQraManager.Id):info("Loading version %s", veafQraManager.Version)