dcs_liberation/resources/plugins/skynetiads/skynet-iads-compiled.lua
2020-10-22 18:39:54 +02:00

2964 lines
87 KiB
Lua

env.info("--- SKYNET VERSION: 1.1.3 | BUILD TIME: 30.09.2020 1816Z ---")
do
--this file contains the required units per sam type
samTypesDB = {
['S-300'] = {
['type'] = 'complex',
['searchRadar'] = {
['S-300PS 40B6MD sr'] = {
},
['S-300PS 64H6E sr'] = {
},
},
['trackingRadar'] = {
['S-300PS 40B6M tr'] = {
},
},
['launchers'] = {
['S-300PS 5P85D ln'] = {
},
['S-300PS 5P85C ln'] = {
},
},
['misc'] = {
['S-300PS 54K6 cp'] = {
['required'] = true,
},
},
['name'] = {
['NATO'] = 'SA-10 Grumble',
},
['harm_detection_chance'] = 90
},
['Buk'] = {
['type'] = 'complex',
['searchRadar'] = {
['SA-11 Buk SR 9S18M1'] = {
},
},
['launchers'] = {
['SA-11 Buk LN 9A310M1'] = {
},
},
['misc'] = {
['SA-11 Buk CC 9S470M1'] = {
['required'] = true,
},
},
['name'] = {
['NATO'] = 'SA-11 Gadfly',
},
['harm_detection_chance'] = 70
},
['s-125'] = {
['type'] = 'complex',
['searchRadar'] = {
['p-19 s-125 sr'] = {
},
},
['trackingRadar'] = {
['snr s-125 tr'] = {
},
},
['launchers'] = {
['5p73 s-125 ln'] = {
},
},
['name'] = {
['NATO'] = 'SA-3 Goa',
},
['harm_detection_chance'] = 40
},
['s-75'] = {
['type'] = 'complex',
['searchRadar'] = {
['p-19 s-125 sr'] = {
},
},
['trackingRadar'] = {
['SNR_75V'] = {
},
},
['launchers'] = {
['S_75M_Volhov'] = {
},
},
['name'] = {
['NATO'] = 'SA-2 Guideline',
},
['harm_detection_chance'] = 30
},
['Kub'] = {
['type'] = 'complex',
['searchRadar'] = {
['Kub 1S91 str'] = {
},
},
['launchers'] = {
['Kub 2P25 ln'] = {
},
},
['name'] = {
['NATO'] = 'SA-6 Gainful',
},
['harm_detection_chance'] = 40
},
['Patriot'] = {
['type'] = 'complex',
['searchRadar'] = {
['Patriot str'] = {
},
},
['launchers'] = {
['Patriot ln'] = {
},
},
['misc'] = {
['Patriot cp'] = {
['required'] = false,
},
['Patriot EPP'] = {
['required'] = false,
},
['Patriot ECS'] = {
['required'] = true,
},
['Patriot AMG'] = {
['required'] = false,
},
},
['name'] = {
['NATO'] = 'Patriot',
},
['harm_detection_chance'] = 90
},
['Hawk'] = {
['type'] = 'complex',
['searchRadar'] = {
['Hawk sr'] = {
},
},
['trackingRadar'] = {
['Hawk tr'] = {
},
},
['launchers'] = {
['Hawk ln'] = {
},
},
['name'] = {
['NATO'] = 'Hawk',
},
['harm_detection_chance'] = 40
},
['Roland ADS'] = {
['type'] = 'single',
['searchRadar'] = {
['Roland ADS'] = {
},
},
['launchers'] = {
['Roland ADS'] = {
},
},
['name'] = {
['NATO'] = 'Roland ADS',
},
['harm_detection_chance'] = 60
},
['2S6 Tunguska'] = {
['type'] = 'single',
['searchRadar'] = {
['2S6 Tunguska'] = {
},
},
['launchers'] = {
['2S6 Tunguska'] = {
},
},
['name'] = {
['NATO'] = 'SA-19 Grison',
},
},
['Osa'] = {
['type'] = 'single',
['searchRadar'] = {
['Osa 9A33 ln'] = {
},
},
['launchers'] = {
['Osa 9A33 ln'] = {
},
},
['name'] = {
['NATO'] = 'SA-8 Gecko',
},
['harm_detection_chance'] = 20
},
['Strela-10M3'] = {
['type'] = 'single',
['searchRadar'] = {
['Strela-10M3'] = {
['trackingRadar'] = true,
},
},
['launchers'] = {
['Strela-10M3'] = {
},
},
['name'] = {
['NATO'] = 'SA-13 Gopher',
},
},
['Strela-1 9P31'] = {
['type'] = 'single',
['searchRadar'] = {
['Strela-1 9P31'] = {
},
},
['launchers'] = {
['Strela-1 9P31'] = {
},
},
['name'] = {
['NATO'] = 'SA-9 Gaskin',
},
['harm_detection_chance'] = 20
},
['Tor'] = {
['type'] = 'single',
['searchRadar'] = {
['Tor 9A331'] = {
},
},
['launchers'] = {
['Tor 9A331'] = {
},
},
['name'] = {
['NATO'] = 'SA-15 Gauntlet',
},
},
['Gepard'] = {
['type'] = 'single',
['searchRadar'] = {
['Gepard'] = {
},
},
['launchers'] = {
['Gepard'] = {
},
},
['name'] = {
['NATO'] = 'Gepard',
},
['harm_detection_chance'] = 10
},
['Rapier'] = {
['searchRadar'] = {
['rapier_fsa_blindfire_radar'] = {
},
},
['launchers'] = {
['rapier_fsa_launcher'] = {
['trackingRadar'] = true,
},
},
['misc'] = {
['rapier_fsa_optical_tracker_unit'] = {
['required'] = true,
},
},
['name'] = {
['NATO'] = 'Rapier',
},
['harm_detection_chance'] = 10
},
['ZSU-23-4 Shilka'] = {
['type'] = 'single',
['searchRadar'] = {
['ZSU-23-4 Shilka'] = {
},
},
['launchers'] = {
['ZSU-23-4 Shilka'] = {
},
},
['name'] = {
['NATO'] = 'Zues',
},
['harm_detection_chance'] = 10
},
['HQ-7'] = {
['searchRadar'] = {
['HQ-7_STR_SP'] = {
},
},
['launchers'] = {
['HQ-7_LN_SP'] = {
},
},
['name'] = {
['NATO'] = 'CSA-4',
},
['harm_detection_chance'] = 30
},
--- Start of EW radars:
['1L13 EWR'] = {
['type'] = 'ewr',
['searchRadar'] = {
['1L13 EWR'] = {
},
},
['name'] = {
['NATO'] = '1L13 EWR',
},
['harm_detection_chance'] = 60
},
['55G6 EWR'] = {
['type'] = 'ewr',
['searchRadar'] = {
['55G6 EWR'] = {
},
},
['name'] = {
['NATO'] = '55G6 EWR',
},
['harm_detection_chance'] = 60
},
['Dog Ear'] = {
['type'] = 'ewr',
['searchRadar'] = {
['Dog Ear radar'] = {
},
},
['name'] = {
['NATO'] = 'Dog Ear',
},
['harm_detection_chance'] = 20
},
['Roland Radar'] = {
['type'] = 'ewr',
['searchRadar'] = {
['Roland Radar'] = {
},
},
['name'] = {
['NATO'] = 'Roland EWR',
},
['harm_detection_chance'] = 60
},
['p-19 s-125 sr'] = {
['searchRadar'] = {
['p-19 s-125 sr'] = {
},
},
['name'] = {
['NATO'] = 'Flat Face',
},
['harm_detection_chance'] = 40
},
['Patriot str'] = {
['searchRadar'] = {
['Patriot str'] = {
},
},
['name'] = {
['NATO'] = 'Patriot str',
},
['harm_detection_chance'] = 80
},
['EW S-300'] = {
['searchRadar'] = {
['S-300PS 40B6MD sr'] = {
},
['S-300PS 64H6E sr'] = {
},
},
['name'] = {
['NATO'] = 'Big Bird',
},
['harm_detection_chance'] = 90
},
['SA-11 Buk SR 9S18M1'] = {
['searchRadar'] = {
['SA-11 Buk SR 9S18M1'] = {
},
},
['name'] = {
['NATO'] = 'Snow Drift',
},
['harm_detection_chance'] = 70
},
['Kub 1S91 str'] = {
['searchRadar'] = {
['Kub 1S91 str'] = {
},
},
['name'] = {
['NATO'] = 'Straight Flush',
},
['harm_detection_chance'] = 40
},
['Hawk str'] = {
['searchRadar'] = {
['Hawk sr'] = {
},
},
['name'] = {
['NATO'] = 'Hawk str',
},
['harm_detection_chance'] = 40
},
}
end
do
SkynetIADS = {}
SkynetIADS.__index = SkynetIADS
SkynetIADS.database = samTypesDB
function SkynetIADS:create(name)
local iads = {}
setmetatable(iads, SkynetIADS)
iads.radioMenu = nil
iads.earlyWarningRadars = {}
iads.samSites = {}
iads.commandCenters = {}
iads.ewRadarScanMistTaskID = nil
iads.samSetupMistTaskID = nil
iads.coalition = nil
iads.contacts = {}
iads.maxTargetAge = 32
iads.name = name
if iads.name == nil then
iads.name = ""
end
iads.contactUpdateInterval = 5
iads.samSetupTime = 60
iads.destroyedUnitResponsibleForUpdateAutonomousStateOfSAMSite = nil
iads.debugOutput = {}
iads.debugOutput.IADSStatus = false
iads.debugOutput.samWentDark = false
iads.debugOutput.contacts = false
iads.debugOutput.radarWentLive = false
iads.debugOutput.ewRadarNoConnection = false
iads.debugOutput.samNoConnection = false
iads.debugOutput.jammerProbability = false
iads.debugOutput.addedEWRadar = false
iads.debugOutput.hasNoPower = false
iads.debugOutput.addedSAMSite = false
iads.debugOutput.warnings = true
iads.debugOutput.harmDefence = false
iads.debugOutput.samSiteStatusEnvOutput = false
iads.debugOutput.earlyWarningRadarStatusEnvOutput = false
return iads
end
function SkynetIADS:setUpdateInterval(interval)
self.contactUpdateInterval = interval
end
function SkynetIADS:setCoalition(item)
if item then
local coalitionID = item:getCoalition()
if self.coalitionID == nil then
self.coalitionID = coalitionID
end
if self.coalitionID ~= coalitionID then
self:printOutput("element: "..item:getName().." has a different coalition than the IADS", true)
end
end
end
function SkynetIADS:addJammer(jammer)
table.insert(self.jammers, jammer)
end
function SkynetIADS:getCoalition()
return self.coalitionID
end
function SkynetIADS:getDestroyedEarlyWarningRadars()
local destroyedSites = {}
for i = 1, #self.earlyWarningRadars do
local ewSite = self.earlyWarningRadars[i]
if ewSite:isDestroyed() then
table.insert(destroyedSites, ewSite)
end
end
return destroyedSites
end
function SkynetIADS:getUsableAbstractRadarElemtentsOfTable(abstractRadarTable)
local usable = {}
for i = 1, #abstractRadarTable do
local abstractRadarElement = abstractRadarTable[i]
if abstractRadarElement:hasActiveConnectionNode() and abstractRadarElement:hasWorkingPowerSource() and abstractRadarElement:isDestroyed() == false then
table.insert(usable, abstractRadarElement)
end
end
return usable
end
function SkynetIADS:getUsableEarlyWarningRadars()
return self:getUsableAbstractRadarElemtentsOfTable(self.earlyWarningRadars)
end
function SkynetIADS:createTableDelegator(units)
local sites = SkynetIADSTableDelegator:create()
for i = 1, #units do
local site = units[i]
table.insert(sites, site)
end
return sites
end
function SkynetIADS:addEarlyWarningRadarsByPrefix(prefix)
self:deactivateEarlyWarningRadars()
self.earlyWarningRadars = {}
for unitName, unit in pairs(mist.DBs.unitsByName) do
local pos = self:findSubString(unitName, prefix)
--somehow the MIST unit db contains StaticObject, we check to see we only add Units
local unit = Unit.getByName(unitName)
if pos and pos == 1 and unit then
self:addEarlyWarningRadar(unitName)
end
end
return self:createTableDelegator(self.earlyWarningRadars)
end
function SkynetIADS:addEarlyWarningRadar(earlyWarningRadarUnitName)
local earlyWarningRadarUnit = Unit.getByName(earlyWarningRadarUnitName)
if earlyWarningRadarUnit == nil then
self:printOutput("you have added an EW Radar that does not exist, check name of Unit in Setup and Mission editor: "..earlyWarningRadarUnitName, true)
return
end
self:setCoalition(earlyWarningRadarUnit)
local ewRadar = nil
local category = earlyWarningRadarUnit:getDesc().category
if category == Unit.Category.AIRPLANE or category == Unit.Category.SHIP then
ewRadar = SkynetIADSAWACSRadar:create(earlyWarningRadarUnit, self)
else
ewRadar = SkynetIADSEWRadar:create(earlyWarningRadarUnit, self)
end
ewRadar:setupElements()
ewRadar:setCachedTargetsMaxAge(self:getCachedTargetsMaxAge())
-- for performance improvement, if iads is not scanning no update coverage update needs to be done, will be executed once when iads activates
if self.ewRadarScanMistTaskID ~= nil then
self:updateIADSCoverage()
end
ewRadar:goLive()
table.insert(self.earlyWarningRadars, ewRadar)
if self:getDebugSettings().addedEWRadar then
self:printOutput(ewRadar:getDescription().." added to IADS")
end
return ewRadar
end
function SkynetIADS:getCachedTargetsMaxAge()
return self.contactUpdateInterval
end
function SkynetIADS:getEarlyWarningRadars()
return self:createTableDelegator(self.earlyWarningRadars)
end
function SkynetIADS:getEarlyWarningRadarByUnitName(unitName)
for i = 1, #self.earlyWarningRadars do
local ewRadar = self.earlyWarningRadars[i]
if ewRadar:getDCSName() == unitName then
return ewRadar
end
end
end
function SkynetIADS:findSubString(haystack, needle)
return string.find(haystack, needle, 1, true)
end
function SkynetIADS:addSAMSitesByPrefix(prefix)
self:deativateSAMSites()
self.samSites = {}
for groupName, groupData in pairs(mist.DBs.groupsByName) do
local pos = self:findSubString(groupName, prefix)
if pos and pos == 1 then
--mist returns groups, units and, StaticObjects
local dcsObject = Group.getByName(groupName)
if dcsObject then
self:addSAMSite(groupName)
end
end
end
return self:createTableDelegator(self.samSites)
end
function SkynetIADS:getSAMSitesByPrefix(prefix)
local returnSams = {}
for i = 1, #self.samSites do
local samSite = self.samSites[i]
local groupName = samSite:getDCSName()
local pos = self:findSubString(groupName, prefix)
if pos and pos == 1 then
table.insert(returnSams, samSite)
end
end
return self:createTableDelegator(returnSams)
end
function SkynetIADS:addSAMSite(samSiteName)
local samSiteDCS = Group.getByName(samSiteName)
if samSiteDCS == nil then
self:printOutput("you have added an SAM Site that does not exist, check name of Group in Setup and Mission editor: "..tostring(samSiteName), true)
return
end
self:setCoalition(samSiteDCS)
local samSite = SkynetIADSSamSite:create(samSiteDCS, self)
samSite:setupElements()
-- for performance improvement, if iads is not scanning no update coverage update needs to be done, will be executed once when iads activates
if self.ewRadarScanMistTaskID ~= nil then
self:updateIADSCoverage()
end
samSite:setCachedTargetsMaxAge(self:getCachedTargetsMaxAge())
samSite:goLive()
if samSite:getNatoName() == "UNKNOWN" then
self:printOutput("you have added an SAM Site that Skynet IADS can not handle: "..samSite:getDCSName(), true)
samSite:cleanUp()
else
samSite:goDark()
table.insert(self.samSites, samSite)
if self:getDebugSettings().addedSAMSite then
self:printOutput(samSite:getDescription().." added to IADS")
end
return samSite
end
end
function SkynetIADS:getUsableSAMSites()
return self:getUsableAbstractRadarElemtentsOfTable(self.samSites)
end
function SkynetIADS:getDestroyedSAMSites()
local destroyedSites = {}
for i = 1, #self.samSites do
local samSite = self.samSites[i]
if samSite:isDestroyed() then
table.insert(destroyedSites, samSite)
end
end
return destroyedSites
end
function SkynetIADS:getSAMSites()
return self:createTableDelegator(self.samSites)
end
function SkynetIADS:getActiveSAMSites()
local activeSAMSites = {}
for i = 1, #self.samSites do
if self.samSites[i]:isActive() then
table.insert(activeSAMSites, self.samSites[i])
end
end
return activeSAMSites
end
function SkynetIADS:getSAMSiteByGroupName(groupName)
for i = 1, #self.samSites do
local samSite = self.samSites[i]
if samSite:getDCSName() == groupName then
return samSite
end
end
end
function SkynetIADS:getSAMSitesByNatoName(natoName)
local selectedSAMSites = SkynetIADSTableDelegator:create()
for i = 1, #self.samSites do
local samSite = self.samSites[i]
if samSite:getNatoName() == natoName then
table.insert(selectedSAMSites, samSite)
end
end
return selectedSAMSites
end
function SkynetIADS:addCommandCenter(commandCenter)
self:setCoalition(commandCenter)
local comCenter = SkynetIADSCommandCenter:create(commandCenter, self)
table.insert(self.commandCenters, comCenter)
return comCenter
end
function SkynetIADS:isCommandCenterUsable()
local hasWorkingCommandCenter = (#self.commandCenters == 0)
for i = 1, #self.commandCenters do
local comCenter = self.commandCenters[i]
if comCenter:isDestroyed() == false and comCenter:hasWorkingPowerSource() then
hasWorkingCommandCenter = true
break
else
hasWorkingCommandCenter = false
end
end
return hasWorkingCommandCenter
end
function SkynetIADS:getCommandCenters()
return self.commandCenters
end
function SkynetIADS:setSAMSitesToAutonomousMode()
for i= 1, #self.samSites do
samSite = self.samSites[i]
samSite:goAutonomous()
end
end
function SkynetIADS.evaluateContacts(self)
if self:isCommandCenterUsable() == false then
if self:getDebugSettings().noWorkingCommmandCenter then
self:printOutput("No Working Command Center")
end
self:setSAMSitesToAutonomousMode()
return
end
local ewRadars = self:getUsableEarlyWarningRadars()
local samSites = self:getUsableSAMSites()
-- rewrote this part of the code to keep loops to a minimum
--will add SAM Sites acting as EW Rardars to the ewRadars array:
for i = 1, #samSites do
local samSite = samSites[i]
--We inform SAM sites that a target update is about to happen. If they have no targets in range after the cycle they go dark
samSite:targetCycleUpdateStart()
if samSite:getActAsEW() then
table.insert(ewRadars, samSite)
end
--if the sam site is not in ew mode and active we grab the detected targets right here
if samSite:isActive() and samSite:getActAsEW() == false then
local contacts = samSite:getDetectedTargets()
for j = 1, #contacts do
local contact = contacts[j]
self:mergeContact(contact)
end
end
end
local samSitesToTrigger = {}
for i = 1, #ewRadars do
local ewRadar = ewRadars[i]
--call go live in case ewRadar had to shut down (HARM attack)
ewRadar:goLive()
-- if an awacs has traveled more than a predeterminded distance we update the autonomous state of the sams
if getmetatable(ewRadar) == SkynetIADSAWACSRadar and ewRadar:isUpdateOfAutonomousStateOfSAMSitesRequired() then
self:updateAutonomousStatesOfSAMSites()
end
local ewContacts = ewRadar:getDetectedTargets()
if #ewContacts > 0 then
local samSitesUnderCoverage = ewRadar:getSAMSitesInCoveredArea()
for j = 1, #samSitesUnderCoverage do
local samSiteUnterCoverage = samSitesUnderCoverage[j]
-- only if a SAM site is not active we add it to the hash of SAM sites to be iterated later on
if samSiteUnterCoverage:isActive() == false then
--we add them to a hash to make sure each SAM site is in the collection only once, reducing the number of loops we conduct later on
samSitesToTrigger[samSiteUnterCoverage:getDCSName()] = samSiteUnterCoverage
end
end
for j = 1, #ewContacts do
local contact = ewContacts[j]
self:mergeContact(contact)
end
end
end
self:cleanAgedTargets()
for samName, samToTrigger in pairs(samSitesToTrigger) do
for j = 1, #self.contacts do
local contact = self.contacts[j]
-- the DCS Radar only returns enemy aircraft, if that should change a coalition check will be required
-- currently every type of object in the air is handed of to the SAM site, including missiles
local description = contact:getDesc()
local category = description.category
if category and category ~= Unit.Category.GROUND_UNIT and category ~= Unit.Category.SHIP and category ~= Unit.Category.STRUCTURE then
samToTrigger:informOfContact(contact)
end
end
end
for i = 1, #samSites do
local samSite = samSites[i]
samSite:targetCycleUpdateEnd()
end
self:printSystemStatus()
end
function SkynetIADS:cleanAgedTargets()
local contactsToKeep = {}
for i = 1, #self.contacts do
local contact = self.contacts[i]
if contact:getAge() < self.maxTargetAge then
table.insert(contactsToKeep, contact)
end
end
self.contacts = contactsToKeep
end
function SkynetIADS:buildSAMSitesInCoveredArea()
local samSites = self:getUsableSAMSites()
for i = 1, #samSites do
local samSite = samSites[i]
samSite:updateSAMSitesInCoveredArea()
end
local ewRadars = self:getUsableEarlyWarningRadars()
for i = 1, #ewRadars do
local ewRadar = ewRadars[i]
ewRadar:updateSAMSitesInCoveredArea()
end
end
function SkynetIADS:updateIADSCoverage()
self:buildSAMSitesInCoveredArea()
self:enforceRebuildAutonomousStateOfSAMSites()
--update moose connector with radar group names Skynet is able to use
self:getMooseConnector():update()
end
function SkynetIADS:updateAutonomousStatesOfSAMSites(deadUnit)
--deat unit is to prevent multiple calls via the event handling of SkynetIADSAbstractElement when a units power source or connection node is destroyed
if deadUnit == nil or self.destroyedUnitResponsibleForUpdateAutonomousStateOfSAMSite ~= deadUnit then
self:updateIADSCoverage()
self.destroyedUnitResponsibleForUpdateAutonomousStateOfSAMSite = deadUnit
end
end
function SkynetIADS:enforceRebuildAutonomousStateOfSAMSites()
local ewRadars = self:getUsableEarlyWarningRadars()
local samSites = self:getUsableSAMSites()
for i = 1, #samSites do
local samSite = samSites[i]
if samSite:getActAsEW() then
table.insert(ewRadars, samSite)
end
end
for i = 1, #samSites do
local samSite = samSites[i]
local inRange = false
for j = 1, #ewRadars do
if samSite:isInRadarDetectionRangeOf(ewRadars[j]) then
inRange = true
end
end
if inRange == false then
samSite:goAutonomous()
else
samSite:resetAutonomousState()
end
end
end
function SkynetIADS:mergeContact(contact)
local existingContact = false
for i = 1, #self.contacts do
local iadsContact = self.contacts[i]
if iadsContact:getName() == contact:getName() then
iadsContact:refresh()
existingContact = true
end
end
if existingContact == false then
table.insert(self.contacts, contact)
end
end
function SkynetIADS:getContacts()
return self.contacts
end
function SkynetIADS:printOutput(output, typeWarning)
if typeWarning == true and self.debugOutput.warnings or typeWarning == nil then
if typeWarning == true then
output = "WARNING: "..output
end
trigger.action.outText(output, 4)
end
end
function SkynetIADS:getDebugSettings()
return self.debugOutput
end
-- will start going through the Early Warning Radars and SAM sites to check what targets they have detected
function SkynetIADS.activate(self)
mist.removeFunction(self.ewRadarScanMistTaskID)
mist.removeFunction(self.samSetupMistTaskID)
self.ewRadarScanMistTaskID = mist.scheduleFunction(SkynetIADS.evaluateContacts, {self}, 1, self.contactUpdateInterval)
self:updateIADSCoverage()
end
function SkynetIADS:setupSAMSitesAndThenActivate(setupTime)
if setupTime then
self.samSetupTime = setupTime
end
local samSites = self:getSAMSites()
for i = 1, #samSites do
local sam = samSites[i]
sam:goLive()
--stop harm scan, because this function will shut down point defences
sam:stopScanningForHARMs()
--point defences will go dark after sam:goLive() call on the SAM they are protecting, so we load them and call a separate goLive call here, some SAMs will therefore receive 2 goLive calls
-- this should not have a negative impact on performance
local pointDefences = sam:getPointDefences()
for j = 1, #pointDefences do
pointDefence = pointDefences[j]
pointDefence:goLive()
end
end
self.samSetupMistTaskID = mist.scheduleFunction(SkynetIADS.postSetupSAMSites, {self}, timer.getTime() + self.samSetupTime)
end
function SkynetIADS.postSetupSAMSites(self)
local samSites = self:getSAMSites()
for i = 1, #samSites do
local sam = samSites[i]
--turn on the scan again otherwise SAMs that fired a missile while in setup will not turn off anymore
sam:scanForHarms()
end
self:activate()
end
function SkynetIADS:deactivate()
mist.removeFunction(self.ewRadarScanMistTaskID)
self:deativateSAMSites()
self:deactivateEarlyWarningRadars()
self:deactivateCommandCenters()
end
function SkynetIADS:deactivateCommandCenters()
for i = 1, #self.commandCenters do
local comCenter = self.commandCenters[i]
comCenter:cleanUp()
end
end
function SkynetIADS:deativateSAMSites()
for i = 1, #self.samSites do
local samSite = self.samSites[i]
samSite:cleanUp()
end
end
function SkynetIADS:deactivateEarlyWarningRadars()
for i = 1, #self.earlyWarningRadars do
local ewRadar = self.earlyWarningRadars[i]
ewRadar:cleanUp()
end
end
function SkynetIADS:addRadioMenu()
self.radioMenu = missionCommands.addSubMenu('SKYNET IADS '..self:getCoalitionString())
local displayIADSStatus = missionCommands.addCommand('show IADS Status', self.radioMenu, SkynetIADS.updateDisplay, {self = self, value = true, option = 'IADSStatus'})
local displayIADSStatus = missionCommands.addCommand('hide IADS Status', self.radioMenu, SkynetIADS.updateDisplay, {self = self, value = false, option = 'IADSStatus'})
local displayIADSStatus = missionCommands.addCommand('show contacts', self.radioMenu, SkynetIADS.updateDisplay, {self = self, value = true, option = 'contacts'})
local displayIADSStatus = missionCommands.addCommand('hide contacts', self.radioMenu, SkynetIADS.updateDisplay, {self = self, value = false, option = 'contacts'})
end
function SkynetIADS:removeRadioMenu()
missionCommands.removeItem(self.radioMenu)
end
function SkynetIADS.updateDisplay(params)
local option = params.option
local self = params.self
local value = params.value
if option == 'IADSStatus' then
self:getDebugSettings()[option] = value
elseif option == 'contacts' then
self:getDebugSettings()[option] = value
end
end
function SkynetIADS:getCoalitionString()
local coalitionStr = "RED"
if self.coalitionID == coalition.side.BLUE then
coalitionStr = "BLUE"
elseif self.coalitionID == coalition.side.NEUTRAL then
coalitionStr = "NEUTRAL"
end
if self.name then
coalitionStr = coalitionStr.." "..self.name
end
return coalitionStr
end
function SkynetIADS:getMooseConnector()
if self.mooseConnector == nil then
self.mooseConnector = SkynetMooseA2ADispatcherConnector:create(self)
end
return self.mooseConnector
end
function SkynetIADS:addMooseSetGroup(mooseSetGroup)
self:getMooseConnector():addMooseSetGroup(mooseSetGroup)
end
function SkynetIADS:printDetailedEarlyWarningRadarStatus()
local ewRadars = self:getEarlyWarningRadars()
env.info("------------------------------------------ EW RADAR STATUS: "..self:getCoalitionString().." -------------------------------")
for i = 1, #ewRadars do
local ewRadar = ewRadars[i]
local numConnectionNodes = #ewRadar:getConnectionNodes()
local numPowerSources = #ewRadar:getPowerSources()
local isActive = ewRadar:isActive()
local connectionNodes = ewRadar:getConnectionNodes()
local firstRadar = nil
local radars = ewRadar:getRadars()
--get the first existing radar to prevent issues in calculating the distance later on:
for i = 1, #radars do
if radars[i]:isExist() then
firstRadar = radars[i]
break
end
end
local numDamagedConnectionNodes = 0
for j = 1, #connectionNodes do
local connectionNode = connectionNodes[j]
if connectionNode:isExist() == false then
numDamagedConnectionNodes = numDamagedConnectionNodes + 1
end
end
local intactConnectionNodes = numConnectionNodes - numDamagedConnectionNodes
local powerSources = ewRadar:getPowerSources()
local numDamagedPowerSources = 0
for j = 1, #powerSources do
local powerSource = powerSources[j]
if powerSource:isExist() == false then
numDamagedPowerSources = numDamagedPowerSources + 1
end
end
local intactPowerSources = numPowerSources - numDamagedPowerSources
local detectedTargets = ewRadar:getDetectedTargets()
local samSitesInCoveredArea = ewRadar:getSAMSitesInCoveredArea()
local unitName = "DESTROYED"
if ewRadar:getDCSRepresentation():isExist() then
unitName = ewRadar:getDCSName()
end
env.info("UNIT: "..unitName.." | TYPE: "..ewRadar:getNatoName())
env.info("ACTIVE: "..tostring(isActive).."| DETECTED TARGETS: "..#detectedTargets.." | DEFENDING HARM: "..tostring(ewRadar:isDefendingHARM()))
if numConnectionNodes > 0 then
env.info("CONNECTION NODES: "..numConnectionNodes.." | DAMAGED: "..numDamagedConnectionNodes.." | INTACT: "..intactConnectionNodes)
else
env.info("NO CONNECTION NODES SET")
end
if numPowerSources > 0 then
env.info("POWER SOURCES : "..numPowerSources.." | DAMAGED:"..numDamagedPowerSources.." | INTACT: "..intactPowerSources)
else
env.info("NO POWER SOURCES SET")
end
env.info("SAM SITES IN COVERED AREA: "..#samSitesInCoveredArea)
for j = 1, #samSitesInCoveredArea do
local samSiteCovered = samSitesInCoveredArea[j]
env.info(samSiteCovered:getDCSName())
end
for j = 1, #detectedTargets do
local contact = detectedTargets[j]
if firstRadar ~= nil and firstRadar:isExist() then
local distance = mist.utils.round(mist.utils.metersToNM(ewRadar:getDistanceInMetersToContact(firstRadar:getDCSRepresentation(), contact:getPosition().p)), 2)
env.info("CONTACT: "..contact:getName().." | TYPE: "..contact:getTypeName().." | DISTANCE NM: "..distance)
end
end
env.info("---------------------------------------------------")
end
end
function SkynetIADS:printDetailedSAMSiteStatus()
local samSites = self:getSAMSites()
env.info("------------------------------------------ SAM STATUS: "..self:getCoalitionString().." -------------------------------")
for i = 1, #samSites do
local samSite = samSites[i]
local numConnectionNodes = #samSite:getConnectionNodes()
local numPowerSources = #samSite:getPowerSources()
local isAutonomous = samSite:getAutonomousState()
local isActive = samSite:isActive()
local connectionNodes = samSite:getConnectionNodes()
local firstRadar = samSite:getRadars()[1]
local numDamagedConnectionNodes = 0
for j = 1, #connectionNodes do
local connectionNode = connectionNodes[j]
if connectionNode:isExist() == false then
numDamagedConnectionNodes = numDamagedConnectionNodes + 1
end
end
local intactConnectionNodes = numConnectionNodes - numDamagedConnectionNodes
local powerSources = samSite:getPowerSources()
local numDamagedPowerSources = 0
for j = 1, #powerSources do
local powerSource = powerSources[j]
if powerSource:isExist() == false then
numDamagedPowerSources = numDamagedPowerSources + 1
end
end
local intactPowerSources = numPowerSources - numDamagedPowerSources
local detectedTargets = samSite:getDetectedTargets()
local samSitesInCoveredArea = samSite:getSAMSitesInCoveredArea()
env.info("GROUP: "..samSite:getDCSName().." | TYPE: "..samSite:getNatoName())
env.info("ACTIVE: "..tostring(isActive).." | AUTONOMOUS: "..tostring(isAutonomous).." | IS ACTING AS EW: "..tostring(samSite:getActAsEW()).." | DETECTED TARGETS: "..#detectedTargets.." | DEFENDING HARM: "..tostring(samSite:isDefendingHARM()).." | MISSILES IN FLIGHT:"..tostring(samSite:getNumberOfMissilesInFlight()))
if numConnectionNodes > 0 then
env.info("CONNECTION NODES: "..numConnectionNodes.." | DAMAGED: "..numDamagedConnectionNodes.." | INTACT: "..intactConnectionNodes)
else
env.info("NO CONNECTION NODES SET")
end
if numPowerSources > 0 then
env.info("POWER SOURCES : "..numPowerSources.." | DAMAGED:"..numDamagedPowerSources.." | INTACT: "..intactPowerSources)
else
env.info("NO POWER SOURCES SET")
end
env.info("SAM SITES IN COVERED AREA: "..#samSitesInCoveredArea)
for j = 1, #samSitesInCoveredArea do
local samSiteCovered = samSitesInCoveredArea[j]
env.info(samSiteCovered:getDCSName())
end
for j = 1, #detectedTargets do
local contact = detectedTargets[j]
if firstRadar ~= nil and firstRadar:isExist() then
local distance = mist.utils.round(mist.utils.metersToNM(samSite:getDistanceInMetersToContact(firstRadar:getDCSRepresentation(), contact:getPosition().p)), 2)
env.info("CONTACT: "..contact:getName().." | TYPE: "..contact:getTypeName().." | DISTANCE NM: "..distance)
end
end
env.info("---------------------------------------------------")
end
end
function SkynetIADS:printSystemStatus()
if self:getDebugSettings().IADSStatus or self:getDebugSettings().contacts then
local coalitionStr = self:getCoalitionString()
self:printOutput("---- IADS: "..coalitionStr.." ------")
end
if self:getDebugSettings().IADSStatus then
local numComCenters = #self.commandCenters
local numIntactComCenters = 0
local numDestroyedComCenters = 0
local numComCentersNoPower = 0
local numComCentersServingIADS = 0
for i = 1, #self.commandCenters do
local commandCenter = self.commandCenters[i]
if commandCenter:hasWorkingPowerSource() == false then
numComCentersNoPower = numComCentersNoPower + 1
end
if commandCenter:isDestroyed() == false then
numIntactComCenters = numIntactComCenters + 1
end
if commandCenter:isDestroyed() == false and commandCenter:hasWorkingPowerSource() then
numComCentersServingIADS = numComCentersServingIADS + 1
end
end
numDestroyedComCenters = numComCenters - numIntactComCenters
self:printOutput("COMMAND CENTERS: Serving IADS: "..numComCentersServingIADS.." | Total: "..numComCenters.." | Intact: "..numIntactComCenters.." | Destroyed: "..numDestroyedComCenters.." | NoPower: "..numComCentersNoPower)
local ewNoPower = 0
local ewTotal = #self:getEarlyWarningRadars()
local ewNoConnectionNode = 0
local ewActive = 0
local ewRadarsInactive = 0
for i = 1, #self.earlyWarningRadars do
local ewRadar = self.earlyWarningRadars[i]
if ewRadar:hasWorkingPowerSource() == false then
ewNoPower = ewNoPower + 1
end
if ewRadar:hasActiveConnectionNode() == false then
ewNoConnectionNode = ewNoConnectionNode + 1
end
if ewRadar:isActive() then
ewActive = ewActive + 1
end
end
ewRadarsInactive = ewTotal - ewActive
local numEWRadarsDestroyed = #self:getDestroyedEarlyWarningRadars()
self:printOutput("EW: "..ewTotal.." | Act: "..ewActive.." | Inact: "..ewRadarsInactive.." | Destroyed: "..numEWRadarsDestroyed.." | NoPowr: "..ewNoPower.." | NoCon: "..ewNoConnectionNode)
local samSitesInactive = 0
local samSitesActive = 0
local samSitesTotal = #self:getSAMSites()
local samSitesNoPower = 0
local samSitesNoConnectionNode = 0
local samSitesOutOfAmmo = 0
local samSiteAutonomous = 0
local samSiteRadarDestroyed = 0
for i = 1, #self.samSites do
local samSite = self.samSites[i]
if samSite:hasWorkingPowerSource() == false then
samSitesNoPower = samSitesNoPower + 1
end
if samSite:hasActiveConnectionNode() == false then
samSitesNoConnectionNode = samSitesNoConnectionNode + 1
end
if samSite:isActive() then
samSitesActive = samSitesActive + 1
end
if samSite:hasRemainingAmmo() == false then
samSitesOutOfAmmo = samSitesOutOfAmmo + 1
end
if samSite:getAutonomousState() == true then
samSiteAutonomous = samSiteAutonomous + 1
end
if samSite:hasWorkingRadar() == false then
samSiteRadarDestroyed = samSiteRadarDestroyed + 1
end
end
samSitesInactive = samSitesTotal - samSitesActive
self:printOutput("SAM: "..samSitesTotal.." | Act: "..samSitesActive.." | Inact: "..samSitesInactive.." | Autonm: "..samSiteAutonomous.." | Raddest: "..samSiteRadarDestroyed.." | NoPowr: "..samSitesNoPower.." | NoCon: "..samSitesNoConnectionNode.." | NoAmmo: "..samSitesOutOfAmmo)
end
if self:getDebugSettings().contacts then
for i = 1, #self.contacts do
local contact = self.contacts[i]
self:printOutput("CONTACT: "..contact:getName().." | TYPE: "..contact:getTypeName().." | GS: "..tostring(contact:getGroundSpeedInKnots()).." | LAST SEEN: "..contact:getAge())
end
end
if self:getDebugSettings().earlyWarningRadarStatusEnvOutput then
self:printDetailedEarlyWarningRadarStatus()
end
if self:getDebugSettings().samSiteStatusEnvOutput then
self:printDetailedSAMSiteStatus()
end
end
end
do
SkynetMooseA2ADispatcherConnector = {}
function SkynetMooseA2ADispatcherConnector:create(iads)
local instance = {}
setmetatable(instance, self)
self.__index = self
instance.iadsCollection = {}
instance.mooseGroups = {}
instance.ewRadarGroupNames = {}
instance.samSiteGroupNames = {}
table.insert(instance.iadsCollection, iads)
return instance
end
function SkynetMooseA2ADispatcherConnector:addIADS(iads)
table.insert(self.iadsCollection, iads)
end
function SkynetMooseA2ADispatcherConnector:addMooseSetGroup(mooseSetGroup)
table.insert(self.mooseGroups, mooseSetGroup)
self:update()
end
function SkynetMooseA2ADispatcherConnector:getEarlyWarningRadarGroupNames()
self.ewRadarGroupNames = {}
for i = 1, #self.iadsCollection do
local ewRadars = self.iadsCollection[i]:getUsableEarlyWarningRadars()
for j = 1, #ewRadars do
local ewRadar = ewRadars[j]
table.insert(self.ewRadarGroupNames, ewRadar:getDCSRepresentation():getGroup():getName())
end
end
return self.ewRadarGroupNames
end
function SkynetMooseA2ADispatcherConnector:getSAMSiteGroupNames()
self.samSiteGroupNames = {}
for i = 1, #self.iadsCollection do
local samSites = self.iadsCollection[i]:getUsableSAMSites()
for j = 1, #samSites do
local samSite = samSites[j]
table.insert(self.samSiteGroupNames, samSite:getDCSName())
end
end
return self.samSiteGroupNames
end
function SkynetMooseA2ADispatcherConnector:update()
--mooseGroup elements are type of:
--https://flightcontrol-master.github.io/MOOSE_DOCS_DEVELOP/Documentation/Core.Set.html##(SET_GROUP)
--remove previously set group names:
for i = 1, #self.mooseGroups do
local mooseGroup = self.mooseGroups[i]
mooseGroup:RemoveGroupsByName(self.ewRadarGroupNames)
mooseGroup:RemoveGroupsByName(self.samSiteGroupNames)
end
--add group names of IADS radars that are currently usable by the IADS:
for i = 1, #self.mooseGroups do
local mooseGroup = self.mooseGroups[i]
mooseGroup:AddGroupsByName(self:getEarlyWarningRadarGroupNames())
mooseGroup:AddGroupsByName(self:getSAMSiteGroupNames())
end
end
end
do
SkynetIADSTableDelegator = {}
function SkynetIADSTableDelegator:create()
local instance = {}
local forwarder = {}
forwarder.__index = function(tbl, name)
tbl[name] = function(self, ...)
for i = 1, #self do
self[i][name](self[i], ...)
end
return self
end
return tbl[name]
end
setmetatable(instance, forwarder)
instance.__index = forwarder
return instance
end
end
do
SkynetIADSAbstractDCSObjectWrapper = {}
function SkynetIADSAbstractDCSObjectWrapper:create(dcsObject)
local instance = {}
setmetatable(instance, self)
self.__index = self
instance.dcsObject = dcsObject
if dcsObject and dcsObject:isExist() and getmetatable(dcsObject) == Unit then
--we store inital life here, because getLife0() returs a value that is lower that getLife() when no damage has happened...
instance.initialLife = dcsObject:getLife()
end
return instance
end
function SkynetIADSAbstractDCSObjectWrapper:getName()
return self.dcsObject:getName()
end
function SkynetIADSAbstractDCSObjectWrapper:getTypeName()
return self.dcsObject:getTypeName()
end
function SkynetIADSAbstractDCSObjectWrapper:getPosition()
return self.dcsObject:getPosition()
end
function SkynetIADSAbstractDCSObjectWrapper:isExist()
if self.dcsObject then
return self.dcsObject:isExist()
else
return false
end
end
function SkynetIADSAbstractDCSObjectWrapper:getLifePercentage()
if self.dcsObject and self.dcsObject:isExist() then
return self.dcsObject:getLife() / self.initialLife * 100
else
return 0
end
end
function SkynetIADSAbstractDCSObjectWrapper:getDCSRepresentation()
return self.dcsObject
end
end
do
SkynetIADSAbstractElement = {}
function SkynetIADSAbstractElement:create(dcsRepresentation, iads)
local instance = {}
setmetatable(instance, self)
self.__index = self
instance.connectionNodes = {}
instance.powerSources = {}
instance.iads = iads
instance.natoName = "UNKNOWN"
instance.dcsName = ""
instance:setDCSRepresentation(dcsRepresentation)
world.addEventHandler(instance)
return instance
end
function SkynetIADSAbstractElement:removeEventHandlers()
world.removeEventHandler(self)
end
function SkynetIADSAbstractElement:cleanUp()
self:removeEventHandlers()
end
function SkynetIADSAbstractElement:isDestroyed()
return self:getDCSRepresentation():isExist() == false
end
function SkynetIADSAbstractElement:addPowerSource(powerSource)
table.insert(self.powerSources, powerSource)
return self
end
function SkynetIADSAbstractElement:getPowerSources()
return self.powerSources
end
function SkynetIADSAbstractElement:addConnectionNode(connectionNode)
table.insert(self.connectionNodes, connectionNode)
self.iads:updateAutonomousStatesOfSAMSites()
return self
end
function SkynetIADSAbstractElement:getConnectionNodes()
return self.connectionNodes
end
function SkynetIADSAbstractElement:hasActiveConnectionNode()
local connectionNode = self:genericCheckOneObjectIsAlive(self.connectionNodes)
if connectionNode == false and self.iads:getDebugSettings().samNoConnection then
self.iads:printOutput(self:getDescription().." no connection to Command Center")
end
return connectionNode
end
function SkynetIADSAbstractElement:hasWorkingPowerSource()
local power = self:genericCheckOneObjectIsAlive(self.powerSources)
if power == false and self.iads:getDebugSettings().hasNoPower then
self.iads:printOutput(self:getDescription().." has no power")
end
return power
end
function SkynetIADSAbstractElement:getDCSName()
return self.dcsName
end
-- generic function to theck if power plants, command centers, connection nodes are still alive
function SkynetIADSAbstractElement:genericCheckOneObjectIsAlive(objects)
local isAlive = (#objects == 0)
for i = 1, #objects do
local object = objects[i]
--if we find one object that is not fully destroyed we assume the IADS is still working
if object:isExist() then
isAlive = true
break
end
end
return isAlive
end
function SkynetIADSAbstractElement:setDCSRepresentation(representation)
self.dcsRepresentation = representation
if self.dcsRepresentation then
self.dcsName = self:getDCSRepresentation():getName()
end
end
function SkynetIADSAbstractElement:getDCSRepresentation()
return self.dcsRepresentation
end
function SkynetIADSAbstractElement:getNatoName()
return self.natoName
end
function SkynetIADSAbstractElement:getDescription()
return "IADS ELEMENT: "..self:getDCSName().." | Type : "..tostring(self:getNatoName())
end
function SkynetIADSAbstractElement:onEvent(event)
--if a unit is destroyed we check to see if its a power plant powering the unit or a connection node
if event.id == world.event.S_EVENT_DEAD then
if self:hasWorkingPowerSource() == false or self:isDestroyed() then
self:goDark()
self.iads:updateAutonomousStatesOfSAMSites(event.initiator)
end
if self:hasActiveConnectionNode() == false then
self:goAutonomous()
self.iads:updateAutonomousStatesOfSAMSites(event.initiator)
end
end
if event.id == world.event.S_EVENT_SHOT then
self:weaponFired(event)
end
end
--placeholder method, can be implemented by subclasses
function SkynetIADSAbstractElement:weaponFired(event)
end
--placeholder method, can be implemented by subclasses
function SkynetIADSAbstractElement:goDark()
end
--placeholder method, can be implemented by subclasses
function SkynetIADSAbstractElement:goAutonomous()
end
-- helper code for class inheritance
function inheritsFrom( baseClass )
local new_class = {}
local class_mt = { __index = new_class }
function new_class:create()
local newinst = {}
setmetatable( newinst, class_mt )
return newinst
end
if nil ~= baseClass then
setmetatable( new_class, { __index = baseClass } )
end
-- Implementation of additional OO properties starts here --
-- Return the class object of the instance
function new_class:class()
return new_class
end
-- Return the super class object of the instance
function new_class:superClass()
return baseClass
end
-- Return true if the caller is an instance of theClass
function new_class:isa( theClass )
local b_isa = false
local cur_class = new_class
while ( nil ~= cur_class ) and ( false == b_isa ) do
if cur_class == theClass then
b_isa = true
else
cur_class = cur_class:superClass()
end
end
return b_isa
end
return new_class
end
end
do
SkynetIADSAbstractRadarElement = {}
SkynetIADSAbstractRadarElement = inheritsFrom(SkynetIADSAbstractElement)
SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DCS_AI = 1
SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK = 2
SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE = 1
SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE = 2
function SkynetIADSAbstractRadarElement:create(dcsElementWithRadar, iads)
local instance = self:superClass():create(dcsElementWithRadar, iads)
setmetatable(instance, self)
self.__index = self
instance.aiState = false
instance.harmScanID = nil
instance.harmSilenceID = nil
instance.lastJammerUpdate = 0
instance.objectsIdentifiedAsHarms = {}
instance.objectsIdentifiedAsHarmsMaxTargetAge = 60
instance.launchers = {}
instance.trackingRadars = {}
instance.searchRadars = {}
instance.samSitesInCoveredArea = {}
instance.missilesInFlight = {}
instance.pointDefences = {}
instance.ingnoreHARMSWhilePointDefencesHaveAmmo = false
instance.autonomousBehaviour = SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DCS_AI
instance.goLiveRange = SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE
instance.isAutonomous = false
instance.harmDetectionChance = 0
instance.minHarmShutdownTime = 0
instance.maxHarmShutDownTime = 0
instance.minHarmPresetShutdownTime = 30
instance.maxHarmPresetShutdownTime = 180
instance.firingRangePercent = 100
instance.actAsEW = false
instance.cachedTargets = {}
instance.cachedTargetsMaxAge = 1
instance.cachedTargetsCurrentAge = 0
instance.goLiveTime = 0
-- 5 seconds seems to be a good value for the sam site to find the target with its organic radar
instance.noCacheActiveForSecondsAfterGoLive = 5
return instance
end
--TODO: this method could be updated to only return Radar weapons fired, this way a SAM firing an IR weapon could go dark faster in the goDark() method
function SkynetIADSAbstractRadarElement:weaponFired(event)
if event.id == world.event.S_EVENT_SHOT then
local weapon = event.weapon
local launcherFired = event.initiator
for i = 1, #self.launchers do
local launcher = self.launchers[i]
if launcher:getDCSRepresentation() == launcherFired then
table.insert(self.missilesInFlight, weapon)
end
end
end
end
function SkynetIADSAbstractRadarElement:setCachedTargetsMaxAge(maxAge)
self.cachedTargetsMaxAge = maxAge
end
function SkynetIADSAbstractRadarElement:cleanUp()
for i = 1, #self.pointDefences do
local pointDefence = self.pointDefences[i]
pointDefence:cleanUp()
end
mist.removeFunction(self.harmScanID)
mist.removeFunction(self.harmSilenceID)
--call method from super class
self:removeEventHandlers()
end
function SkynetIADSAbstractRadarElement:addPointDefence(pointDefence)
table.insert(self.pointDefences, pointDefence)
return self
end
function SkynetIADSAbstractRadarElement:getPointDefences()
return self.pointDefences
end
function SkynetIADSAbstractRadarElement:updateSAMSitesInCoveredArea()
local samSites = self.iads:getUsableSAMSites()
self.samSitesInCoveredArea = {}
for i = 1, #samSites do
local samSite = samSites[i]
if samSite:isInRadarDetectionRangeOf(self) and samSite ~= self then
table.insert(self.samSitesInCoveredArea, samSite)
end
end
return self.samSitesInCoveredArea
end
function SkynetIADSAbstractRadarElement:getSAMSitesInCoveredArea()
return self.samSitesInCoveredArea
end
function SkynetIADSAbstractRadarElement:pointDefencesHaveRemainingAmmo(minNumberOfMissiles)
local remainingMissiles = 0
for i = 1, #self.pointDefences do
local pointDefence = self.pointDefences[i]
remainingMissiles = remainingMissiles + pointDefence:getRemainingNumberOfMissiles()
end
local returnValue = false
if ( remainingMissiles > 0 and remainingMissiles >= minNumberOfMissiles ) then
returnValue = true
end
return returnValue
end
function SkynetIADSAbstractElement:pointDefencesHaveEnoughLaunchers(minNumberOfLaunchers)
local numOfLaunchers = 0
for i = 1, #self.pointDefences do
local pointDefence = self.pointDefences[i]
numOfLaunchers = numOfLaunchers + #pointDefence:getLaunchers()
end
local returnValue = false
if ( numOfLaunchers > 0 and numOfLaunchers >= minNumberOfLaunchers ) then
returnValue = true
end
return returnValue
end
function SkynetIADSAbstractElement:setIgnoreHARMSWhilePointDefencesHaveAmmo(state)
if state == true or state == false then
self.ingnoreHARMSWhilePointDefencesHaveAmmo = state
end
return self
end
function SkynetIADSAbstractRadarElement:hasMissilesInFlight()
return #self.missilesInFlight > 0
end
function SkynetIADSAbstractRadarElement:getNumberOfMissilesInFlight()
return #self.missilesInFlight
end
-- DCS does not send an event, when a missile is destroyed, so this method needs to be polled so that the missiles in flight are current, polling is done in the HARM Search call: evaluateIfTargetsContainHARMs
function SkynetIADSAbstractRadarElement:updateMissilesInFlight()
local missilesInFlight = {}
for i = 1, #self.missilesInFlight do
local missile = self.missilesInFlight[i]
if missile:isExist() then
table.insert(missilesInFlight, missile)
end
end
self.missilesInFlight = missilesInFlight
self:goDarkIfOutOfAmmo()
end
function SkynetIADSAbstractRadarElement:goDarkIfOutOfAmmo()
if self:hasRemainingAmmo() == false and self:getActAsEW() == false then
self:goDark()
end
end
function SkynetIADSAbstractRadarElement:getActAsEW()
return self.actAsEW
end
function SkynetIADSAbstractRadarElement:setActAsEW(ewState)
if ewState == true or ewState == false then
self.actAsEW = ewState
end
if self.actAsEW == true then
self:goLive()
else
self:goDark()
end
return self
end
function SkynetIADSAbstractRadarElement:getUnitsToAnalyse()
local units = {}
table.insert(units, self:getDCSRepresentation())
if getmetatable(self:getDCSRepresentation()) == Group then
units = self:getDCSRepresentation():getUnits()
end
return units
end
function SkynetIADSAbstractRadarElement:getRemainingNumberOfMissiles()
local remainingNumberOfMissiles = 0
for i = 1, #self.launchers do
local launcher = self.launchers[i]
remainingNumberOfMissiles = remainingNumberOfMissiles + launcher:getRemainingNumberOfMissiles()
end
return remainingNumberOfMissiles
end
function SkynetIADSAbstractRadarElement:getInitialNumberOfMissiles()
local initalNumberOfMissiles = 0
for i = 1, #self.launchers do
local launcher = self.launchers[i]
initalNumberOfMissiles = launcher:getInitialNumberOfMissiles() + initalNumberOfMissiles
end
return initalNumberOfMissiles
end
function SkynetIADSAbstractRadarElement:getRemainingNumberOfShells()
local remainingNumberOfShells = 0
for i = 1, #self.launchers do
local launcher = self.launchers[i]
remainingNumberOfShells = remainingNumberOfShells + launcher:getRemainingNumberOfShells()
end
return remainingNumberOfShells
end
function SkynetIADSAbstractRadarElement:getInitialNumberOfShells()
local initialNumberOfShells = 0
for i = 1, #self.launchers do
local launcher = self.launchers[i]
initialNumberOfShells = initialNumberOfShells + launcher:getInitialNumberOfShells()
end
return initialNumberOfShells
end
function SkynetIADSAbstractRadarElement:hasRemainingAmmo()
--the launcher check is due to ew radars they have no launcher and no ammo and therefore are never out of ammo
return ( #self.launchers == 0 ) or ((self:getRemainingNumberOfMissiles() > 0 ) or ( self:getRemainingNumberOfShells() > 0 ) )
end
function SkynetIADSAbstractRadarElement:getHARMDetectionChance()
return self.harmDetectionChance
end
function SkynetIADSAbstractRadarElement:setHARMDetectionChance(chance)
self.harmDetectionChance = chance
return self
end
function SkynetIADSAbstractRadarElement:setupElements()
local numUnits = #self:getUnitsToAnalyse()
for typeName, dataType in pairs(SkynetIADS.database) do
local hasSearchRadar = false
local hasTrackingRadar = false
local hasLauncher = false
self.searchRadars = {}
self.trackingRadars = {}
self.launchers = {}
for entry, unitData in pairs(dataType) do
if entry == 'searchRadar' then
self:analyseAndAddUnit(SkynetIADSSAMSearchRadar, self.searchRadars, unitData)
hasSearchRadar = true
end
if entry == 'launchers' then
self:analyseAndAddUnit(SkynetIADSSAMLauncher, self.launchers, unitData)
hasLauncher = true
end
if entry == 'trackingRadar' then
self:analyseAndAddUnit(SkynetIADSSAMTrackingRadar, self.trackingRadars, unitData)
hasTrackingRadar = true
end
end
local numElementsCreated = #self.searchRadars + #self.trackingRadars + #self.launchers
--this check ensures a unit or group has all required elements for the specific sam or ew type:
if (hasLauncher and hasSearchRadar and hasTrackingRadar and #self.launchers > 0 and #self.searchRadars > 0 and #self.trackingRadars > 0 )
or (hasSearchRadar and hasLauncher and #self.searchRadars > 0 and #self.launchers > 0)
or (hasSearchRadar and hasLauncher == false and hasTrackingRadar == false and #self.searchRadars > 0 and numUnits == 1) then
local harmDetection = dataType['harm_detection_chance']
if harmDetection then
self.harmDetectionChance = harmDetection
end
local natoName = dataType['name']['NATO']
--we shorten the SA-XX names and don't return their code names eg goa, gainful..
local pos = natoName:find(" ")
local prefix = natoName:sub(1, 2)
if string.lower(prefix) == 'sa' and pos ~= nil then
self.natoName = natoName:sub(1, (pos-1))
else
self.natoName = natoName
end
break
end
end
end
function SkynetIADSAbstractRadarElement:analyseAndAddUnit(class, tableToAdd, unitData)
local units = self:getUnitsToAnalyse()
for i = 1, #units do
local unit = units[i]
local unitTypeName = unit:getTypeName()
for unitName, unitPerformanceData in pairs(unitData) do
if unitName == unitTypeName then
samElement = class:create(unit)
samElement:setupRangeData()
table.insert(tableToAdd, samElement)
end
end
end
end
function SkynetIADSAbstractRadarElement:getController()
local dcsRepresentation = self:getDCSRepresentation()
if dcsRepresentation:isExist() then
return dcsRepresentation:getController()
else
return nil
end
end
function SkynetIADSAbstractRadarElement:getLaunchers()
return self.launchers
end
function SkynetIADSAbstractRadarElement:getSearchRadars()
return self.searchRadars
end
function SkynetIADSAbstractRadarElement:getTrackingRadars()
return self.trackingRadars
end
function SkynetIADSAbstractRadarElement:getRadars()
local radarUnits = {}
for i = 1, #self.searchRadars do
table.insert(radarUnits, self.searchRadars[i])
end
for i = 1, #self.trackingRadars do
table.insert(radarUnits, self.trackingRadars[i])
end
return radarUnits
end
function SkynetIADSAbstractRadarElement:setGoLiveRangeInPercent(percent)
if percent ~= nil then
self.firingRangePercent = percent
for i = 1, #self.launchers do
local launcher = self.launchers[i]
launcher:setFiringRangePercent(self.firingRangePercent)
end
for i = 1, #self.searchRadars do
local radar = self.searchRadars[i]
radar:setFiringRangePercent(self.firingRangePercent)
end
end
return self
end
function SkynetIADSAbstractRadarElement:getGoLiveRangeInPercent()
return self.firingRangePercent
end
function SkynetIADSAbstractRadarElement:setEngagementZone(engagementZone)
if engagementZone == SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE then
self.goLiveRange = engagementZone
elseif engagementZone == SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE then
self.goLiveRange = engagementZone
end
return self
end
function SkynetIADSAbstractRadarElement:getEngagementZone()
return self.goLiveRange
end
function SkynetIADSAbstractRadarElement:goLive()
if ( self.aiState == false and self:hasWorkingPowerSource() and self.harmSilenceID == nil)
and ( (self.isAutonomous == false) or (self.isAutonomous == true and self.autonomousBehaviour == SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DCS_AI ) )
and (self:hasRemainingAmmo() == true )
then
if self:isDestroyed() == false then
local cont = self:getController()
cont:setOnOff(true)
cont:setOption(AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.RED)
cont:setOption(AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_FREE)
self.goLiveTime = timer.getTime()
end
self.aiState = true
self:pointDefencesStopActingAsEW()
if self.iads:getDebugSettings().radarWentLive then
self.iads:printOutput(self:getDescription().." going live")
end
self:scanForHarms()
end
end
function SkynetIADSAbstractRadarElement:pointDefencesStopActingAsEW()
for i = 1, #self.pointDefences do
local pointDefence = self.pointDefences[i]
pointDefence:setActAsEW(false)
end
end
function SkynetIADSAbstractRadarElement:noDamageToRadars()
local radars = self:getRadars()
for i = 1, #radars do
local radar = radars[i]
if radar:getLifePercentage() < 100 then
return false
end
end
return true
end
function SkynetIADSAbstractRadarElement:goDark()
if ( self.aiState == true )
and (self.harmSilenceID ~= nil or ( self.harmSilenceID == nil and #self:getDetectedTargets() == 0 and self:hasMissilesInFlight() == false) or ( self.harmSilenceID == nil and #self:getDetectedTargets() > 0 and self:hasMissilesInFlight() == false and self:hasRemainingAmmo() == false ) )
and ( self.isAutonomous == false or ( self.isAutonomous == true and self.autonomousBehaviour == SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK ) )
then
if self:isDestroyed() == false then
local controller = self:getController()
-- if the SAM site still has ammo we turn off the controller, this prevents rearming, however this way the SAM site is frozen in a red state, on the next actication it will be up and running much faster, therefore it will instantaneously engage targets
-- also this is a better way to get the HARM to miss the target, if not set to false the HARM often sticks to the target
if self:hasRemainingAmmo() then
controller:setOnOff(false)
--if the SAM is out of ammo we set the state to green, and ROE to weapon hold, this way it will shut down its radar and it can be rearmed
else
controller:setOption(AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.GREEN)
controller:setOption(AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD)
end
end
-- point defence will only go live if the Radar Emitting site it is protecting goes dark and this is due to a it defending against a HARM
if (self.harmSilenceID ~= nil) then
self:pointDefencesGoLive()
end
self.aiState = false
self:stopScanningForHARMs()
if self.iads:getDebugSettings().samWentDark then
self.iads:printOutput(self:getDescription().." going dark")
end
end
end
function SkynetIADSAbstractRadarElement:pointDefencesGoLive()
for i = 1, #self.pointDefences do
local pointDefence = self.pointDefences[i]
pointDefence:setActAsEW(true)
end
end
function SkynetIADSAbstractRadarElement:isActive()
return self.aiState
end
function SkynetIADSAbstractRadarElement:isTargetInRange(target)
local isSearchRadarInRange = false
local isTrackingRadarInRange = false
local isLauncherInRange = false
local isSearchRadarInRange = ( #self.searchRadars == 0 )
for i = 1, #self.searchRadars do
local searchRadar = self.searchRadars[i]
if searchRadar:isInRange(target) then
isSearchRadarInRange = true
break
end
end
if self.goLiveRange == SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE then
isLauncherInRange = ( #self.launchers == 0 )
for i = 1, #self.launchers do
local launcher = self.launchers[i]
if launcher:isInRange(target) then
isLauncherInRange = true
break
end
end
isTrackingRadarInRange = ( #self.trackingRadars == 0 )
for i = 1, #self.trackingRadars do
local trackingRadar = self.trackingRadars[i]
if trackingRadar:isInRange(target) then
isTrackingRadarInRange = true
break
end
end
else
isLauncherInRange = true
isTrackingRadarInRange = true
end
return (isSearchRadarInRange and isTrackingRadarInRange and isLauncherInRange )
end
function SkynetIADSAbstractRadarElement:isInRadarDetectionRangeOf(abstractRadarElement)
local radars = self:getRadars()
local abstractRadarElementRadars = abstractRadarElement:getRadars()
for i = 1, #radars do
local radar = radars[i]
for j = 1, #abstractRadarElementRadars do
local abstractRadarElementRadar = abstractRadarElementRadars[j]
if abstractRadarElementRadar:isExist() and radar:isExist() then
local distance = self:getDistanceToUnit(radar:getDCSRepresentation():getPosition().p, abstractRadarElementRadar:getDCSRepresentation():getPosition().p)
if abstractRadarElementRadar:getMaxRangeFindingTarget() >= distance then
return true
end
end
end
end
return false
end
function SkynetIADSAbstractRadarElement:getDistanceToUnit(unitPosA, unitPosB)
return mist.utils.round(mist.utils.get2DDist(unitPosA, unitPosB, 0))
end
function SkynetIADSAbstractRadarElement:setAutonomousBehaviour(mode)
if mode ~= nil then
self.autonomousBehaviour = mode
end
return self
end
function SkynetIADSAbstractRadarElement:getAutonomousBehaviour()
return self.autonomousBehaviour
end
function SkynetIADSAbstractRadarElement:resetAutonomousState()
if self.isAutonomous == true then
self.isAutonomous = false
self:goDark()
end
end
function SkynetIADSAbstractRadarElement:goAutonomous()
if self.isAutonomous == false then
self.isAutonomous = true
self:goDark()
self:goLive()
end
end
function SkynetIADSAbstractRadarElement:getAutonomousState()
return self.isAutonomous
end
function SkynetIADSAbstractRadarElement:hasWorkingRadar()
local radars = self:getRadars()
for i = 1, #radars do
local radar = radars[i]
if radar:isRadarWorking() == true then
return true
end
end
return false
end
function SkynetIADSAbstractRadarElement:jam(successProbability)
if self:isDestroyed() == false then
local controller = self:getController()
local probability = math.random(1, 100)
if self.iads:getDebugSettings().jammerProbability then
self.iads:printOutput("JAMMER: "..self:getDescription()..": Probability: "..successProbability)
end
if successProbability > probability then
controller:setOption(AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD)
if self.iads:getDebugSettings().jammerProbability then
self.iads:printOutput("JAMMER: "..self:getDescription()..": jammed, setting to weapon hold")
end
else
controller:setOption(AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_FREE)
if self.iads:getDebugSettings().jammerProbability then
self.iads:printOutput("Jammer: "..self:getDescription()..": jammed, setting to weapon free")
end
end
self.lastJammerUpdate = timer:getTime()
end
end
function SkynetIADSAbstractRadarElement:scanForHarms()
self:stopScanningForHARMs()
self.harmScanID = mist.scheduleFunction(SkynetIADSAbstractRadarElement.evaluateIfTargetsContainHARMs, {self}, 1, 2)
end
function SkynetIADSAbstractElement:isScanningForHARMs()
return self.harmScanID ~= nil
end
function SkynetIADSAbstractElement:isDefendingHARM()
return self.harmSilenceID ~= nil
end
function SkynetIADSAbstractRadarElement:stopScanningForHARMs()
mist.removeFunction(self.harmScanID)
self.harmScanID = nil
end
function SkynetIADSAbstractRadarElement:goSilentToEvadeHARM(timeToImpact)
self:finishHarmDefence(self)
self.objectsIdentifiedAsHarms = {}
local harmTime = self:getHarmShutDownTime()
if self.iads:getDebugSettings().harmDefence then
self.iads:printOutput("HARM DEFENCE: "..self:getDCSName().." shutting down | FOR: "..harmTime.." seconds | TTI: "..timeToImpact)
end
self.harmSilenceID = mist.scheduleFunction(SkynetIADSAbstractRadarElement.finishHarmDefence, {self}, timer.getTime() + harmTime, 1)
self:goDark()
end
function SkynetIADSAbstractRadarElement:getHarmShutDownTime()
local shutDownTime = math.random(self.minHarmShutdownTime, self.maxHarmShutDownTime)
return shutDownTime
end
function SkynetIADSAbstractRadarElement.finishHarmDefence(self)
mist.removeFunction(self.harmSilenceID)
self.harmSilenceID = nil
end
function SkynetIADSAbstractRadarElement:getDetectedTargets()
if ( timer.getTime() - self.cachedTargetsCurrentAge > self.cachedTargetsMaxAge ) or ( timer.getTime() - self.goLiveTime < self.noCacheActiveForSecondsAfterGoLive ) then
self.cachedTargets = {}
self.cachedTargetsCurrentAge = timer.getTime()
if self:hasWorkingPowerSource() and self:isDestroyed() == false then
local targets = self:getController():getDetectedTargets(Controller.Detection.RADAR)
for i = 1, #targets do
local target = targets[i]
-- there are cases when a destroyed object is still visible as a target to the radar, don't add it, will cause errors everywhere the dcs object is accessed
if target.object then
local iadsTarget = SkynetIADSContact:create(target)
iadsTarget:refresh()
if self:isTargetInRange(iadsTarget) then
table.insert(self.cachedTargets, iadsTarget)
end
end
end
end
end
return self.cachedTargets
end
function SkynetIADSAbstractRadarElement:getSecondsToImpact(distanceNM, speedKT)
local tti = 0
if speedKT > 0 then
tti = mist.utils.round((distanceNM / speedKT) * 3600, 0)
if tti < 0 then
tti = 0
end
end
return tti
end
function SkynetIADSAbstractRadarElement:getDistanceInMetersToContact(radarUnit, point)
return mist.utils.round(mist.utils.get3DDist(radarUnit:getPosition().p, point), 0)
end
function SkynetIADSAbstractRadarElement:calculateMinimalShutdownTimeInSeconds(timeToImpact)
return timeToImpact + self.minHarmPresetShutdownTime
end
function SkynetIADSAbstractRadarElement:calculateMaximalShutdownTimeInSeconds(minShutdownTime)
return minShutdownTime + mist.random(1, self.maxHarmPresetShutdownTime)
end
function SkynetIADSAbstractRadarElement:calculateImpactPoint(target, distanceInMeters)
-- distance needs to be incremented by a certain value for ip calculation to work, check why presumably due to rounding errors in the previous distance calculation
return land.getIP(target:getPosition().p, target:getPosition().x, distanceInMeters + 50)
end
function SkynetIADSAbstractRadarElement:shallReactToHARM()
return self.harmDetectionChance >= math.random(1, 100)
end
-- will only check for missiles, if DCS ads AAA than can engage HARMs then this code must be updated:
function SkynetIADSAbstractRadarElement:shallIgnoreHARMShutdown()
local numOfHarms = self:getNumberOfObjectsItentifiedAsHARMS()
return ( self:pointDefencesHaveRemainingAmmo(numOfHarms) and self:pointDefencesHaveEnoughLaunchers(numOfHarms) and self.ingnoreHARMSWhilePointDefencesHaveAmmo == true)
end
function SkynetIADSAbstractRadarElement:getNumberOfObjectsItentifiedAsHARMS()
local numFound = 0
for unitName, unit in pairs(self.objectsIdentifiedAsHarms) do
numFound = numFound + 1
end
return numFound
end
function SkynetIADSAbstractRadarElement:cleanUpOldObjectsIdentifiedAsHARMS()
local validObjects = {}
for unitName, unit in pairs(self.objectsIdentifiedAsHarms) do
local harm = unit['target']
if harm:getAge() <= self.objectsIdentifiedAsHarmsMaxTargetAge then
validObjects[harm:getName()] = {}
validObjects[harm:getName()]['target'] = harm
validObjects[harm:getName()]['count'] = unit['count']
end
end
self.objectsIdentifiedAsHarms = validObjects
--stop point defences acting as ew (always on), will occur of activated via shallIgnoreHARMShutdown() in evaluateIfTargetsContainHARMs
if self:getNumberOfObjectsItentifiedAsHARMS() == 0 then
self:pointDefencesStopActingAsEW()
end
end
function SkynetIADSAbstractRadarElement.evaluateIfTargetsContainHARMs(self)
--if an emitter dies the SAM site being jammed will revert back to normal operation:
if self.lastJammerUpdate > 0 and ( timer:getTime() - self.lastJammerUpdate ) > 10 then
self:jam(0)
self.lastJammerUpdate = 0
end
--we use the regular interval of this method to update to other states:
self:updateMissilesInFlight()
self:cleanUpOldObjectsIdentifiedAsHARMS()
local targets = self:getDetectedTargets()
for i = 1, #targets do
local target = targets[i]
local radars = self:getRadars()
for j = 1, #radars do
local radar = radars[j]
if radar:isExist() == true then
local distance = self:getDistanceInMetersToContact(radar, target:getPosition().p)
local impactPoint = self:calculateImpactPoint(target, distance)
if impactPoint then
local harmImpactPointDistanceToSAM = self:getDistanceInMetersToContact(radar, impactPoint)
if harmImpactPointDistanceToSAM <= 100 then
if self.objectsIdentifiedAsHarms[target:getName()] then
self.objectsIdentifiedAsHarms[target:getName()]['count'] = self.objectsIdentifiedAsHarms[target:getName()]['count'] + 1
else
self.objectsIdentifiedAsHarms[target:getName()] = {}
self.objectsIdentifiedAsHarms[target:getName()]['target'] = target
self.objectsIdentifiedAsHarms[target:getName()]['count'] = 1
end
local savedTarget = self.objectsIdentifiedAsHarms[target:getName()]['target']
savedTarget:refresh()
local numDetections = self.objectsIdentifiedAsHarms[target:getName()]['count']
local speed = savedTarget:getGroundSpeedInKnots()
local timeToImpact = self:getSecondsToImpact(mist.utils.metersToNM(distance), speed)
local shallReactToHarm = self:shallReactToHARM()
-- if self:getNumberOfObjectsItentifiedAsHARMS() > 0 then
-- env.info("detect as HARM: "..self:getDCSName().." "..self:getNumberOfObjectsItentifiedAsHARMS())
-- end
-- we use 2 detection cycles so a random object in the air pointing at the SAM site for a spilt second will not trigger a shutdown. shallReactToHarm adds some salt otherwise the SAM will always shut down 100% of the time.
if numDetections == 2 and shallReactToHarm then
if self:shallIgnoreHARMShutdown() == false then
self.minHarmShutdownTime = self:calculateMinimalShutdownTimeInSeconds(timeToImpact)
self.maxHarmShutDownTime = self:calculateMaximalShutdownTimeInSeconds(self.minHarmShutdownTime)
self:goSilentToEvadeHARM(timeToImpact)
else
self:pointDefencesGoLive()
end
end
if numDetections == 2 and shallReactToHarm == false then
if self.iads:getDebugSettings().harmDefence then
self.iads:printOutput("HARM DEFENCE: "..self:getDCSName().." will not react")
end
end
end
end
end
end
end
end
end
do
--this class is currently used for AWACS and Ships, at a latter date a separate class for ships could be created, currently not needed
SkynetIADSAWACSRadar = {}
SkynetIADSAWACSRadar = inheritsFrom(SkynetIADSAbstractRadarElement)
function SkynetIADSAWACSRadar:create(radarUnit, iads)
local instance = self:superClass():create(radarUnit, iads)
setmetatable(instance, self)
self.__index = self
instance.lastUpdatePosition = nil
return instance
end
function SkynetIADSAWACSRadar:setupElements()
local unit = self:getDCSRepresentation()
local radar = SkynetIADSSAMSearchRadar:create(unit)
radar:setupRangeData()
table.insert(self.searchRadars, radar)
end
function SkynetIADSAWACSRadar:getNatoName()
return self:getDCSRepresentation():getTypeName()
end
-- AWACs will not scan for HARMS
function SkynetIADSAWACSRadar:scanForHarms()
end
function SkynetIADSAWACSRadar:getMaxAllowedMovementForAutonomousUpdateInNM()
local radarRange = mist.utils.metersToNM(self.searchRadars[1]:getMaxRangeFindingTarget())
return mist.utils.round(radarRange / 10)
end
function SkynetIADSAWACSRadar:isUpdateOfAutonomousStateOfSAMSitesRequired()
return self:getDistanceTraveledSinceLastUpdate() > self:getMaxAllowedMovementForAutonomousUpdateInNM()
end
function SkynetIADSAWACSRadar:getDistanceTraveledSinceLastUpdate()
local currentPosition = nil
if self.lastUpdatePosition == nil and self:getDCSRepresentation():isExist() then
self.lastUpdatePosition = self:getDCSRepresentation():getPosition().p
end
if self:getDCSRepresentation():isExist() then
currentPosition = self:getDCSRepresentation():getPosition().p
end
return mist.utils.round(mist.utils.metersToNM(self:getDistanceToUnit(self.lastUpdatePosition, currentPosition)))
end
end
do
SkynetIADSCommandCenter = {}
SkynetIADSCommandCenter = inheritsFrom(SkynetIADSAbstractElement)
function SkynetIADSCommandCenter:create(commandCenter, iads)
local instance = self:superClass():create(commandCenter, iads)
setmetatable(instance, self)
self.__index = self
instance.natoName = "Command Center"
return instance
end
end
do
SkynetIADSContact = {}
SkynetIADSContact = inheritsFrom(SkynetIADSAbstractDCSObjectWrapper)
function SkynetIADSContact:create(dcsRadarTarget)
local instance = self:superClass():create(dcsRadarTarget.object)
setmetatable(instance, self)
self.__index = self
instance.firstContactTime = timer.getAbsTime()
instance.lastTimeSeen = 0
instance.dcsRadarTarget = dcsRadarTarget
instance.name = instance.dcsObject:getName()
instance.typeName = instance.dcsObject:getTypeName()
instance.position = instance.dcsObject:getPosition()
instance.numOfTimesRefreshed = 0
instance.speed = 0
return instance
end
function SkynetIADSContact:getName()
return self.name
end
function SkynetIADSContact:getTypeName()
return self.typeName
end
function SkynetIADSContact:isTypeKnown()
return self.dcsRadarTarget.type
end
function SkynetIADSContact:isDistanceKnown()
return self.dcsRadarTarget.distance
end
function SkynetIADSContact:getPosition()
return self.position
end
function SkynetIADSContact:getGroundSpeedInKnots(decimals)
if decimals == nil then
decimals = 2
end
return mist.utils.round(self.speed, decimals)
end
function SkynetIADSContact:getDesc()
if self.dcsObject:isExist() then
return self.dcsObject:getDesc()
else
return {}
end
end
function SkynetIADSContact:getNumberOfTimesHitByRadar()
return self.numOfTimesRefreshed
end
function SkynetIADSContact:refresh()
self.numOfTimesRefreshed = self.numOfTimesRefreshed + 1
if self.dcsObject and self.dcsObject:isExist() then
local distance = mist.utils.metersToNM(mist.utils.get2DDist(self.position.p, self.dcsObject:getPosition().p))
local timeDelta = (timer.getAbsTime() - self.lastTimeSeen)
if timeDelta > 0 then
local hours = timeDelta / 3600
self.speed = (distance / hours)
end
self.position = self.dcsObject:getPosition()
end
self.lastTimeSeen = timer.getAbsTime()
end
function SkynetIADSContact:getAge()
return mist.utils.round(timer.getAbsTime() - self.lastTimeSeen)
end
end
do
SkynetIADSEWRadar = {}
SkynetIADSEWRadar = inheritsFrom(SkynetIADSAbstractRadarElement)
function SkynetIADSEWRadar:create(radarUnit, iads)
local instance = self:superClass():create(radarUnit, iads)
setmetatable(instance, self)
self.__index = self
instance.autonomousBehaviour = SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK
return instance
end
end
do
SkynetIADSJammer = {}
SkynetIADSJammer.__index = SkynetIADSJammer
function SkynetIADSJammer:create(emitter, iads)
local jammer = {}
setmetatable(jammer, SkynetIADSJammer)
jammer.radioMenu = nil
jammer.emitter = emitter
jammer.jammerTaskID = nil
jammer.iads = {iads}
jammer.maximumEffectiveDistanceNM = 200
--jammer probability settings are stored here, visualisation, see: https://docs.google.com/spreadsheets/d/16rnaU49ZpOczPEsdGJ6nfD0SLPxYLEYKmmo4i2Vfoe0/edit#gid=0
jammer.jammerTable = {
['SA-2'] = {
['function'] = function(distanceNauticalMiles) return ( 1.4 ^ distanceNauticalMiles ) + 90 end,
['canjam'] = true,
},
['SA-3'] = {
['function'] = function(distanceNauticalMiles) return ( 1.4 ^ distanceNauticalMiles ) + 80 end,
['canjam'] = true,
},
['SA-6'] = {
['function'] = function(distanceNauticalMiles) return ( 1.4 ^ distanceNauticalMiles ) + 23 end,
['canjam'] = true,
},
['SA-8'] = {
['function'] = function(distanceNauticalMiles) return ( 1.35 ^ distanceNauticalMiles ) + 30 end,
['canjam'] = true,
},
['SA-10'] = {
['function'] = function(distanceNauticalMiles) return ( 1.07 ^ (distanceNauticalMiles / 1.13) ) + 5 end,
['canjam'] = true,
},
['SA-11'] = {
['function'] = function(distanceNauticalMiles) return ( 1.25 ^ distanceNauticalMiles ) + 15 end,
['canjam'] = true,
},
['SA-15'] = {
['function'] = function(distanceNauticalMiles) return ( 1.15 ^ distanceNauticalMiles ) + 5 end,
['canjam'] = true,
},
}
return jammer
end
function SkynetIADSJammer:masterArmOn()
self:masterArmSafe()
self.jammerTaskID = mist.scheduleFunction(SkynetIADSJammer.runCycle, {self}, 1, 10)
end
function SkynetIADSJammer:addFunction(natoName, jammerFunction)
self.jammerTable[natoName] = {
['function'] = jammerFunction,
['canjam'] = true
}
end
function SkynetIADSJammer:setMaximumEffectiveDistance(distance)
self.maximumEffectiveDistanceNM = distance
end
function SkynetIADSJammer:disableFor(natoName)
self.jammerTable[natoName]['canjam'] = false
end
function SkynetIADSJammer:isKnownRadarEmitter(natoName)
local isActive = false
for unitName, unit in pairs(self.jammerTable) do
if unitName == natoName and unit['canjam'] == true then
isActive = true
end
end
return isActive
end
function SkynetIADSJammer:addIADS(iads)
table.insert(self.iads, iads)
end
function SkynetIADSJammer:getSuccessProbability(distanceNauticalMiles, natoName)
local probability = 0
local jammerSettings = self.jammerTable[natoName]
if jammerSettings ~= nil then
probability = jammerSettings['function'](distanceNauticalMiles)
end
return probability
end
function SkynetIADSJammer:getDistanceNMToRadarUnit(radarUnit)
return mist.utils.metersToNM(mist.utils.get3DDist(self.emitter:getPosition().p, radarUnit:getPosition().p))
end
function SkynetIADSJammer.runCycle(self)
if self.emitter:isExist() == false then
self:masterArmSafe()
return
end
for i = 1, #self.iads do
local iads = self.iads[i]
local samSites = iads:getActiveSAMSites()
for j = 1, #samSites do
local samSite = samSites[j]
local radars = samSite:getRadars()
local hasLOS = false
local distance = 0
local natoName = samSite:getNatoName()
for l = 1, #radars do
local radar = radars[l]
distance = self:getDistanceNMToRadarUnit(radar)
-- I try to emulate the system as it would work in real life, so a jammer can only jam a SAM site if has line of sight to at least one radar in the group
if self:isKnownRadarEmitter(natoName) and self:hasLineOfSightToRadar(radar) and distance <= self.maximumEffectiveDistanceNM then
if iads:getDebugSettings().jammerProbability then
iads:printOutput("JAMMER: Distance: "..distance)
end
samSite:jam(self:getSuccessProbability(distance, natoName))
end
end
end
end
end
function SkynetIADSJammer:hasLineOfSightToRadar(radar)
local radarPos = radar:getPosition().p
--lift the radar 30 meters off the ground, some 3d models are dug in to the ground, creating issues in calculating LOS
radarPos.y = radarPos.y + 30
return land.isVisible(radarPos, self.emitter:getPosition().p)
end
function SkynetIADSJammer:masterArmSafe()
mist.removeFunction(self.jammerTaskID)
end
--TODO: Remove Menu when emitter dies:
function SkynetIADSJammer:addRadioMenu()
self.radioMenu = missionCommands.addSubMenu('Jammer: '..self.emitter:getName())
missionCommands.addCommand('Master Arm On', self.radioMenu, SkynetIADSJammer.updateMasterArm, {self = self, option = 'masterArmOn'})
missionCommands.addCommand('Master Arm Safe', self.radioMenu, SkynetIADSJammer.updateMasterArm, {self = self, option = 'masterArmSafe'})
end
function SkynetIADSJammer.updateMasterArm(params)
local option = params.option
local self = params.self
if option == 'masterArmOn' then
self:masterArmOn()
elseif option == 'masterArmSafe' then
self:masterArmSafe()
end
end
function SkynetIADSJammer:removeRadioMenu()
missionCommands.removeItem(self.radioMenu)
end
end
do
SkynetIADSSAMSearchRadar = {}
SkynetIADSSAMSearchRadar = inheritsFrom(SkynetIADSAbstractDCSObjectWrapper)
function SkynetIADSSAMSearchRadar:create(unit)
local instance = self:superClass():create(unit)
setmetatable(instance, self)
self.__index = self
instance.firingRangePercent = 100
instance.maximumRange = 0
instance.initialNumberOfMissiles = 0
instance.remainingNumberOfMissiles = 0
instance.initialNumberOfShells = 0
instance.remainingNumberOfShells = 0
instance.triedSensors = 0
return instance
end
--override in subclasses to match different datastructure of getSensors()
function SkynetIADSSAMSearchRadar:setupRangeData()
if self:isExist() then
local data = self:getDCSRepresentation():getSensors()
if data == nil then
--this is to prevent infinite calls between launcher and search radar
self.triedSensors = self.triedSensors + 1
--the SA-13 does not have any sensor data, but is has launcher data, so we use the stuff from the launcher for the radar range.
SkynetIADSSAMLauncher.setupRangeData(self)
return
end
for i = 1, #data do
local subEntries = data[i]
for j = 1, #subEntries do
local sensorInformation = subEntries[j]
-- some sam sites have IR and passive EWR detection, we are just interested in the radar data
-- investigate if upperHemisphere and headOn is ok, I guess it will work for most detection cases
if sensorInformation.type == Unit.SensorType.RADAR then
local upperHemisphere = sensorInformation['detectionDistanceAir']['upperHemisphere']['headOn']
local lowerHemisphere = sensorInformation['detectionDistanceAir']['lowerHemisphere']['headOn']
self.maximumRange = upperHemisphere
if lowerHemisphere > upperHemisphere then
self.maximumRange = lowerHemisphere
end
end
end
end
end
end
function SkynetIADSSAMSearchRadar:getMaxRangeFindingTarget()
return self.maximumRange
end
function SkynetIADSSAMSearchRadar:isRadarWorking()
-- the ammo check is for the SA-13 which does not return any sensor data:
return (self:isExist() == true and ( self:getDCSRepresentation():getSensors() ~= nil or self:getDCSRepresentation():getAmmo() ~= nil ) )
end
function SkynetIADSSAMSearchRadar:setFiringRangePercent(percent)
self.firingRangePercent = percent
end
function SkynetIADSSAMSearchRadar:getDistance(target)
return mist.utils.get2DDist(target:getPosition().p, self.dcsObject:getPosition().p)
end
function SkynetIADSSAMSearchRadar:getHeight(target)
local radarElevation = self:getDCSRepresentation():getPosition().p.y
local targetElevation = target:getPosition().p.y
return math.abs(targetElevation - radarElevation)
end
function SkynetIADSSAMSearchRadar:isInHorizontalRange(target)
return (self:getMaxRangeFindingTarget() / 100 * self.firingRangePercent) >= self:getDistance(target)
end
function SkynetIADSSAMSearchRadar:isInRange(target)
if self:isExist() == false then
return false
end
return self:isInHorizontalRange(target)
end
end
do
SkynetIADSSamSite = {}
SkynetIADSSamSite = inheritsFrom(SkynetIADSAbstractRadarElement)
function SkynetIADSSamSite:create(samGroup, iads)
local sam = self:superClass():create(samGroup, iads)
setmetatable(sam, self)
self.__index = self
sam.targetsInRange = false
return sam
end
function SkynetIADSSamSite:isDestroyed()
local isDestroyed = true
for i = 1, #self.launchers do
local launcher = self.launchers[i]
if launcher:isExist() == true then
isDestroyed = false
end
end
local radars = self:getRadars()
for i = 1, #radars do
local radar = radars[i]
if radar:isExist() == true then
isDestroyed = false
end
end
return isDestroyed
end
function SkynetIADSSamSite:targetCycleUpdateStart()
self.targetsInRange = false
end
function SkynetIADSSamSite:targetCycleUpdateEnd()
if self.targetsInRange == false and self.actAsEW == false then
self:goDark()
end
end
function SkynetIADSSamSite:informOfContact(contact)
-- we make sure isTargetInRange (expensive call) is only triggered if no previous calls to this method resulted in targets in range
if self.targetsInRange == false and self:isTargetInRange(contact) then
self:goLive()
self.targetsInRange = true
end
end
end
do
SkynetIADSSAMTrackingRadar = {}
SkynetIADSSAMTrackingRadar = inheritsFrom(SkynetIADSSAMSearchRadar)
function SkynetIADSSAMTrackingRadar:create(unit)
local instance = self:superClass():create(unit)
setmetatable(instance, self)
self.__index = self
return instance
end
end
do
SkynetIADSSAMLauncher = {}
SkynetIADSSAMLauncher = inheritsFrom(SkynetIADSSAMSearchRadar)
function SkynetIADSSAMLauncher:create(unit)
local instance = self:superClass():create(unit)
setmetatable(instance, self)
self.__index = self
instance.maximumFiringAltitude = 0
return instance
end
function SkynetIADSSAMLauncher:setupRangeData()
self.remainingNumberOfMissiles = 0
self.remainingNumberOfShells = 0
if self:isExist() then
local data = self:getDCSRepresentation():getAmmo()
local initialNumberOfMissiles = 0
local initialNumberOfShells = 0
--data becomes nil, when all missiles are fired
if data then
for i = 1, #data do
local ammo = data[i]
--we ignore checks on radar guidance types, since we are not interested in how exactly the missile is guided by the SAM site.
if ammo.desc.category == Weapon.Category.MISSILE then
--TODO: see what the difference is between Max and Min values, SA-3 has higher Min value than Max?, most likely it has to do with the box parameters supplied by launcher
--to simplyfy we just use the larger value, sam sites need a few seconds of tracking time to fire, by that time contact has most likely closed in on the SAM site.
local altMin = ammo.desc.rangeMaxAltMin
local altMax = ammo.desc.rangeMaxAltMax
self.maximumRange = altMin
if altMin < altMax then
self.maximumRange = altMax
end
self.maximumFiringAltitude = ammo.desc.altMax
self.remainingNumberOfMissiles = self.remainingNumberOfMissiles + ammo.count
initialNumberOfMissiles = self.remainingNumberOfMissiles
end
if ammo.desc.category == Weapon.Category.SHELL then
self.remainingNumberOfShells = self.remainingNumberOfShells + ammo.count
initialNumberOfShells = self.remainingNumberOfShells
end
--if no distance was detected we run the code for the search radar. This happens when all in one units are passed like the shilka
if self.maximumRange == 0 then
--this is to prevent infinite calls between launcher and search radar
if self.triedSensors <= 2 then
SkynetIADSSAMSearchRadar.setupRangeData(self)
end
end
end
-- conditions here are because setupRangeData() is called multiple times in the code to update ammo status, we set initial values only the first time the method is called
if self.initialNumberOfMissiles == 0 then
self.initialNumberOfMissiles = initialNumberOfMissiles
end
if self.initialNumberOfShells == 0 then
self.initialNumberOfShells = initialNumberOfShells
end
end
end
end
function SkynetIADSSAMLauncher:getInitialNumberOfShells()
return self.initialNumberOfShells
end
function SkynetIADSSAMLauncher:getRemainingNumberOfShells()
self:setupRangeData()
return self.remainingNumberOfShells
end
function SkynetIADSSAMLauncher:getInitialNumberOfMissiles()
return self.initialNumberOfMissiles
end
function SkynetIADSSAMLauncher:getRemainingNumberOfMissiles()
self:setupRangeData()
return self.remainingNumberOfMissiles
end
function SkynetIADSSAMLauncher:getRange()
return self.maximumRange
end
function SkynetIADSSAMLauncher:getMaximumFiringAltitude()
return self.maximumFiringAltitude
end
function SkynetIADSSAMLauncher:isWithinFiringHeight(target)
-- if no max firing height is set (radar quided AAA) then we use the vertical range, bit of a hack but probably ok for AAA
if self:getMaximumFiringAltitude() > 0 then
return self:getMaximumFiringAltitude() >= self:getHeight(target)
else
return self:getRange() >= self:getHeight(target)
end
end
function SkynetIADSSAMLauncher:isInRange(target)
if self:isExist() == false then
return false
end
return self:isWithinFiringHeight(target) and self:isInHorizontalRange(target)
end
end
--[[
SA-2 Launcher:
{
count=1,
desc={
Nmax=17,
RCS=0.39669999480247,
_origin="",
altMax=25000,
altMin=100,
box={
max={x=4.7303376197815, y=0.84564626216888, z=0.84564626216888},
min={x=-5.8387970924377, y=-0.84564626216888, z=-0.84564626216888}
},
category=1,
displayName="SA2V755",
fuseDist=20,
guidance=4,
life=2,
missileCategory=2,
rangeMaxAltMax=30000,
rangeMaxAltMin=40000,
rangeMin=7000,
typeName="SA2V755",
warhead={caliber=500, explosiveMass=196, mass=196, type=1}
}
}
}
--]]