------------------------------------------------------------------ -- VEAF missile guardian functions for DCS World -- By zip (2020) -- -- Features: -- --------- -- * GUARDIAN objects that are configured to warn and protect specific units (helos, airplanes) of weapons fired in their direction -- * ANGEL objects following these weapons, warning their targets (distance, aspect, danger) and optionnaly destroy weapons in the air before impact (training, protection) -- * Works with all current and future maps (Caucasus, NTTR, Normandy, PG, ...) -- -- See the documentation : https://veaf.github.io/documentation/ ------------------------------------------------------------------ veafMissileGuardian = {} ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Global settings. Stores the script constants ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Identifier. All output in DCS.log will start with this. veafMissileGuardian.Id = "MISSILEGUARDIAN" --- Version. veafMissileGuardian.Version = "0.0.1" -- trace level, specific to this module --veafMissileGuardian.LogLevel = "trace" veaf.loggers.new(veafMissileGuardian.Id, veafMissileGuardian.LogLevel) --- Number of seconds between each check of the WIDE ZONE watchdog function veafMissileGuardian.SecondsBetweenWideZoneWatchdogChecks = 5 --- Number of seconds between each check of the DANGER ZONE watchdog function veafMissileGuardian.SecondsBetweenDangerZoneWatchdogChecks = 0.5 veafMissileGuardian.RadioMenuName = "GUARDIAN" veafMissileGuardian.RemoteCommandParser = "([[a-zA-Z0-9]+)%s?([^%s]*)%s?(.*)" ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Do not change anything below unless you know what you are doing! ------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Radio menus paths veafMissileGuardian.rootPath = nil ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Utility methods ------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- VeafMG_Weapon object ; represents a weapon in flight, detected by a Guardian and managed by an Protector ------------------------------------------------------------------------------------------------------------------------------------------------------------- VeafMG_Weapon = { -- technical name name = nil, -- DCS weapon object dcsWeapon = nil, -- shooter shooter = nil, -- shooter name (if the shooter unit gets destroyed, keep its name) shooterName = nil, } VeafMG_Weapon.__index = VeafMG_Weapon function VeafMG_Weapon:new() veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("VeafMG_Weapon:new()")) local self = setmetatable({}, VeafMG_Weapon) self.name = nil self.dcsWeapon = nil self.shooter = nil self.shooterName = nil return self end function VeafMG_Weapon:copy() local copy = VeafMG_Weapon:new() -- copy the attributes copy.name = self.name copy.dcsWeapon = self.dcsWeapon copy.shooter = self.shooter copy.shooterName = self.shooterName return copy end --- --- setters and getters --- function VeafMG_Weapon:setName(value) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("VeafMG_Weapon.setName([%s])",value or "")) self.name = value return self end function VeafMG_Weapon:getName() return self.name end function VeafMG_Weapon:setDcsWeapon(value) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("VeafMG_Weapon[%s].setDcsWeapon()",self:getName() or "")) self.dcsWeapon = value if self.dcsWeapon then self.shooter = self.dcsWeapon:getLauncher() self.shooterName = veafMissileGuardian.getUnitName(self.shooter) end return self end function VeafMG_Weapon:getDcsWeapon() return self.dcsWeapon end function VeafMG_Weapon:getShooter() return self.shooter end function VeafMG_Weapon:getShooterName() return self.shooterName end --- --- other methods --- function VeafMG_Weapon:getCurrentPosition() local _result = nil if self:getDcsWeapon() then _result = self:getDcsWeapon():getPoint() end return _result end function VeafMG_Weapon:getCurrentTarget() local _result = nil if self:getDcsWeapon() then _result = self:getDcsWeapon():getTarget() end return _result end function VeafMG_Weapon:getCurrentEnergy() local _result = nil if self:getDcsWeapon() then local _mass = 250 -- let's say the missile weights 250kg local _vector = self:getDcsWeapon():getVelocity() local _absVelocity = mist.vec.mag(_vector) local _kinetic = (_mass / 2) * _absVelocity * _absVelocity local _alt = self:getDcsWeapon():getPoint().z local _potential = _mass * 9.81 * _alt _result = _kinetic + _potential end return _result end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- VeafMG_Guardian object -- The Guardian observes units and reacts if one of them fires a weapon by encapsulating it into a VeafMG_Weapon and passing it to a VeafMG_Protector for observation ------------------------------------------------------------------------------------------------------------------------------------------------------------- VeafMG_Guardian = { -- technical name name = nil, -- human-friendly name friendlyName = nil, -- list of units that should be protected from missiles protectedUnits = nil, -- polygon describing a zone when the units should be to be protected protectedZone = nil, } VeafMG_Guardian.__index = VeafMG_Guardian VeafMG_Guardian.WARNING_MESSAGE = "Warning, %s : you've been attacked by %s and a missile is in the air" VeafMG_Guardian.WARNING_MESSAGE_TIME = 10 function VeafMG_Guardian:new() veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("VeafMG_Guardian:new()")) local self = setmetatable({}, VeafMG_Guardian) self.name = nil self.friendlyName = nil self.protectedUnits = {} self.protectedZone = {} return self end function VeafMG_Guardian:copy() local copy = VeafMG_Guardian:new() -- copy the attributes copy.name = self.name copy.friendlyName = self.friendlyName -- deep copy the collections copy.protectedUnits = {} for unitName, value in pairs(self.protectedUnits) do copy.protectedZone[unitName] = value end copy.protectedZone = {} for _, value in pairs(self.protectedZone) do table.insert(copy.protectedZone, value) end return copy end --- --- setters and getters --- function VeafMG_Guardian:setName(value) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("VeafMG_Guardian[]:setName([%s])",value or "")) self.name = value return self end function VeafMG_Guardian:getName() return self.name end function VeafMG_Guardian:setFriendlyName(value) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("VeafMG_Guardian[%s]:setFriendlyName()",self:getName() or "")) self.friendlyName = value return self end function VeafMG_Guardian:getFriendlyName() return self.friendlyName end function VeafMG_Guardian:addProtectedUnit(value) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("VeafMG_Guardian[%s]:addProtectedUnit()",self:getName() or "")) if type(value) ~= "string" then value = value:getName() end self.protectedUnits[value] = "protected" return self end function VeafMG_Guardian:setProtectedZone(value) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("VeafMG_Guardian[%s]:setProtectedZone()",self:getName() or "")) self.protectedZone = value return self end --- --- other methods --- --- event handler function VeafMG_Guardian:onEvent(event) -- only react to S_EVENT_SHOT events if event and event.id == world.event.S_EVENT_SHOT then veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("VeafMG_Protector:onEvent(S_EVENT_SHOT) : %s", veaf.p(event))) if event.weapon then -- check if the target is one of the protected units local _target = event.weapon:getTarget() if _target then local _targetName = _target:getName() veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("_targetName = %s", veaf.p(_targetName))) if self.protectedUnits[_targetName] then -- check if the target is in the protected zone local _inZone = mist.pointInPolygon(_target:getPoint(), self.protectedZone) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("_inZone = %s", veaf.p(_inZone))) if _inZone then -- encapsulate the event weapon local _weapon = VeafMG_Weapon:new():setDcsWeapon(event.weapon) -- message the target unit local _groupId = _target:getGroup():getID() local _playername = _target:getPlayerName() if _playername then veaf.loggers.get(veafMissileGuardian.Id):debug(string.format("Issuing a warning to unit %s", veaf.p(_playername))) trigger.action.outTextForGroup(_groupId, string.format(VeafMG_Guardian.WARNING_MESSAGE, _playername, _weapon:getShooterName()), VeafMG_Guardian.WARNING_MESSAGE_TIME) end -- pass the weapon to the large-scale protector veafMissileGuardian.getLargeScaleProtector():setWeapon(_weapon) end end end end end end function VeafMG_Guardian:start() -- register event handler world.addEventHandler(self) end function VeafMG_Guardian:stop() -- deregister event handler world.removeEventHandler(self) end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- VeafMG_Protector object -- Responsible for observing and reporting VeafMG_Weapons behavior, and protecting the registered units by destroying the weapons in flight when needed ------------------------------------------------------------------------------------------------------------------------------------------------------------- VeafMG_Protector = { -- technical name name = nil, -- seconds between watchdog checks secondsBetweenWatchdogChecks = nil, -- weapon we're trying to protect from weapon = nil, } VeafMG_Protector.__index = VeafMG_Protector function VeafMG_Protector:new() veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("VeafMG_Protector:new()")) local self = setmetatable({}, VeafMG_Protector) self.name = nil self.secondsBetweenWatchdogChecks = nil self.weapon = nil return self end function VeafMG_Protector:copy() local copy = VeafMG_Protector:new() -- copy the attributes copy.name = self.name copy.secondsBetweenWatchdogChecks = self.secondsBetweenWatchdogChecks copy.weapon = self.weapon -- deep copy the collections -- copy.parameters = {} -- for name, value in pairs(self.parameters) do -- veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("copying parameter %s : ",tostring(name))) -- copy.parameters[name]=value -- end return copy end --- --- setters and getters --- function VeafMG_Protector:setName(value) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("VeafMG_Protector[]:setName([%s])",value or "")) self.name = value return self end function VeafMG_Protector:getName() return self.name end function VeafMG_Protector:setSecondsBetweenWatchdogChecks(value) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("VeafMG_Protector[%s]:setSecondsBetweenWatchdogChecks()",self:getName() or "")) self.secondsBetweenWatchdogChecks = value return self end function VeafMG_Protector:getSecondsBetweenWatchdogChecks() return self.secondsBetweenWatchdogChecks end function VeafMG_Protector:setWeapon(value) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("VeafMG_Protector[%s]:setWeapon()",self:getName() or "")) self.weapon = value return self end --- --- other methods --- --- function VeafMG_Protector:start() -- schedule the watchdog function end function VeafMG_Protector:stop() -- unschedule the watchdog function end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- local functions ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafMissileGuardian.getUnitName(unit) return unit:getName() -- TODO make this useful (add the player name if possible) end function veafMissileGuardian.getLargeScaleProtector() -- TODO end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- global functions ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- add a new guardian function veafMissileGuardian.AddGuardian(guardian) veaf.loggers.get(veafMissileGuardian.Id):debug(string.format("veafMissileGuardian.AddGuardian([%s])",guardian:getName() or "")) return guardian end -- activate a guardian function veafMissileGuardian.ActivateGuardian(name, silent) veaf.loggers.get(veafMissileGuardian.Id):debug(string.format("veafMissileGuardian.ActivateGuardian([%s])",name or "")) local guardian = veafMissileGuardian.GetGuardian(name) local result = guardian:activate(silent) if not silent and not guardian:isSilent() then if result then trigger.action.outText("VeafMG_Guardian "..guardian:getFriendlyName().." has been activated.", 10) else trigger.action.outText("VeafMG_Guardian "..guardian:getFriendlyName().." was already active.", 10) end end veafMissileGuardian.buildRadioMenu() end -- desactivate a guardian function veafMissileGuardian.DesactivateGuardian(name, silent) veaf.loggers.get(veafMissileGuardian.Id):debug(string.format("veafMissileGuardian.DesactivateGuardian([%s])",name or "")) local guardian = veafMissileGuardian.GetGuardian(name) local result = guardian:desactivate(silent) if not silent and not guardian:isSilent() then if result then trigger.action.outText("VeafMG_Guardian "..guardian:getFriendlyName().." has been desactivated.", 10) else trigger.action.outText("VeafMG_Guardian "..guardian:getFriendlyName().." was already inactive.", 10) end end veafMissileGuardian.buildRadioMenu() end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Radio menu and help ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafMissileGuardian._buildMissionRadioMenu(menu, title, element) local missions = element.missions if #missions == 1 then -- one simple mission local mission = missions[1] if mission:isActive() then title = "* "..title end mission.radioRootPath = veafRadio.addSubMenu(title, menu) mission:updateRadioMenu(true) else -- group by skill and scale veaf.loggers.get(veafMissileGuardian.Id):trace("group by skill and scale") local skills = {} for _, mission in pairs(missions) do local regex = ("^([^/]+)/([^/]+)/(%d+)$") local name, skill, scale = mission:getName():match(regex) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("missionName=[%s], name=%s, skill=%s, scale=%s", tostring(mission:getName()), tostring(name), tostring(skill), tostring(scale))) if not skills[skill] then skills[skill] = {} end skills[skill][scale] = mission end veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("skills=%s", veaf.p(skills))) -- create the radio menus local title = title if element.activeGroups then title = "* "..title end local missionPath = veafRadio.addSubMenu(title, menu) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format(" %s", title)) local skillsNames = {} for skill, _ in pairs(skills) do table.insert(skillsNames, skill) end table.sort(skillsNames) for _, skill in pairs(skillsNames) do local scales = skills[skill] local skillTitle = skill if element.activeGroups and element.activeGroups[skill] then skillTitle = "* "..skillTitle end local skillPath = veafRadio.addSubMenu(skillTitle, missionPath) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format(" %s", skill)) local scalesNames = {} for scale, _ in pairs(scales) do table.insert(scalesNames, scale) end table.sort(scalesNames) for _, scale in pairs(scalesNames) do local mission = scales[scale] local scaleTitle = "scale "..scale if element.activeGroups and element.activeGroups[skill] and element.activeGroups[skill][scale] then scaleTitle = "* "..scaleTitle end local scalePath = veafRadio.addSubMenu(scaleTitle, skillPath) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format(" %s", scale)) mission.radioRootPath = scalePath mission:updateRadioMenu(true) end end end end --- Build the initial radio menu function veafMissileGuardian.buildRadioMenu() veaf.loggers.get(veafMissileGuardian.Id):debug("buildRadioMenu()") if veafMissileGuardian.rootPath then veafRadio.clearSubmenu(veafMissileGuardian.rootPath) else veafMissileGuardian.rootPath = veafRadio.addMenu(veafMissileGuardian.RadioMenuName) end if not(veafRadio.skipHelpMenus) then veafRadio.addCommandToSubmenu("HELP", veafMissileGuardian.rootPath, veafMissileGuardian.help, nil, veafRadio.USAGE_ForGroup) end veafRadio.refreshRadioMenu() end function veafMissileGuardian.listGuardians() -- sort the missions alphabetically local sortedMissions = {} table.sort(sortedMissions) local text = 'List of all available guardians:\n' for _, missionName in pairs(sortedMissions) do text = text .. " - " .. missionName .. "\n" end trigger.action.outText(text, 20) end function veafMissileGuardian.listActiveMissions() -- sort the missions alphabetically local sortedMissions = {} for _, mission in pairs(veafMissileGuardian.missionsDict) do if mission:isActive() then table.insert(sortedMissions, mission:getName() .. ' : ' .. mission:getRemainingEnemiesString()) end end table.sort(sortedMissions) local text = 'No active combat mission !' if #sortedMissions > 0 then text = 'List of active combat missions:\n' for _, missionName in pairs(sortedMissions) do text = text .. " - " .. missionName .. "\n" end end trigger.action.outText(text, 20) end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- remote interface ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- execute command from the remote interface function veafMissileGuardian.executeCommandFromRemote(parameters) veaf.loggers.get(veafMissileGuardian.Id):debug(string.format("veafMissileGuardian.executeCommandFromRemote()")) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("parameters= %s", veaf.p(parameters))) local _pilot, _pilotName, _unitName, _command = unpack(parameters) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("_pilot= %s", veaf.p(_pilot))) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("_pilotName= %s", veaf.p(_pilotName))) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("_unitName= %s", veaf.p(_unitName))) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("_command= %s", veaf.p(_command))) if not _pilot or not _command then return false end if _command then -- parse the command local _action, _missionName, _parameters = _command:match(veafMissileGuardian.RemoteCommandParser) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("_action=%s",veaf.p(_action))) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("_guardianName=%s",veaf.p(_missionName))) veaf.loggers.get(veafMissileGuardian.Id):trace(string.format("_parameters=%s",veaf.p(_parameters))) if _action and _action:lower() == "list" then veaf.loggers.get(veafMissileGuardian.Id):info(string.format("[%s] is listing air missions)",veaf.p(_pilot.name))) veafMissileGuardian.listAvailableMissions() return true elseif _action and _action:lower() == "start" and _missionName then local _silent = _parameters and _parameters:lower() == "silent" veaf.loggers.get(veafMissileGuardian.Id):info(string.format("[%s] is starting air mission [%s] %s)",veaf.p(_pilot.name), veaf.p(_missionName), veaf.p(_parameters))) veafMissileGuardian.ActivateMission(_missionName, _silent) return true elseif _action and _action:lower() == "stop" then local _silent = _parameters and _parameters:lower() == "silent" veaf.loggers.get(veafMissileGuardian.Id):info(string.format("[%s] is stopping air mission [%s] %s)",veaf.p(_pilot.name), veaf.p(_missionName), veaf.p(_parameters))) veafMissileGuardian.DesactivateMission(_missionName, _silent) return true end end return false end ------------------------------------------------------------------------------------------------------------------------------------------------------------- -- initialisation ------------------------------------------------------------------------------------------------------------------------------------------------------------- function veafMissileGuardian.initialize() veaf.loggers.get(veafMissileGuardian.Id):info("Initializing module") veafMissileGuardian.buildRadioMenu() veafMissileGuardian.dumpMissionsList(veaf.config.MISSION_EXPORT_PATH) end veaf.loggers.get(veafMissileGuardian.Id):info(string.format("Loading version %s", veafMissileGuardian.Version))