------------------------------------------------------------------
-- VEAF Skynet-IADS contacts monitoring
-- By Flogas (2023)
--
-- Features:
-- ---------
-- * This module offers tools to trigger actions when a Skynet IADS detects a target
--
-- See the documentation : https://veaf.github.io/documentation/
------------------------------------------------------------------

veafSkynetMonitor = {}

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Global settings. Stores the script constants
-------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Identifier. All output in DCS.log will start with this.
veafSkynetMonitor.Id = "SKYNET_MONITOR"

--- Version.
veafSkynetMonitor.Version = "1.1.0"

-- trace level, specific to this module
--veafSkynetMonitor.LogLevel = "trace"
veaf.loggers.new(veafSkynetMonitor.Id, veafSkynetMonitor.LogLevel)

---------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------
---  general tools
---------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------
local function tableContains(tab, element)
    for _, e in pairs(tab) do
        if e == element then
            return true
        end
    end
    return false
end

local function tableRemove(tab, element)
    for i, e in pairs(tab) do
        if e == element then
            table.remove(tab, i)
            return
        end
    end
end

local function isNullOrEmpty(s)
    return (s == nil or (type(s) == "string" and s == ""))
end

---------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------
---  VeafSkynetMonitorDescriptor class
---  Provides text descriptions to check an IADS content and structure
---------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------
VeafSkynetMonitorDescriptor = {}

VeafSkynetMonitorDescriptor._staticSring = { Separator = " | ", Indentation = "  " }
VeafSkynetMonitorDescriptor.Option =
{
    -- general
    Ewr = 1,        -- display Early Warning Radars
    Sam = 2,        -- display SAM sites
    Targets = 3,    -- display targets tracked by the IADS
    -- Elements (Sam sites, EWRs)
    ElementTargets = 4, -- display targets tracked by each sam site
    ElementDetail = 5,  -- display static element informations (go live conditions, HARM detection percentages, etc)
    ElementStructure = 6,  -- display units attached to each element (point defences, child radars, etc)
    
    NestedPointDefences = 7 -- the point defences will be displayed as part of the element they defend
}
---------------------------------------------------------------------------------------------------
---  CTOR
function VeafSkynetMonitorDescriptor:Create(iads, options)
    options = options or
        { VeafSkynetMonitorDescriptor.Option.Ewr, VeafSkynetMonitorDescriptor.Option.Sam,
            VeafSkynetMonitorDescriptor.Option.Targets }

    local this =
    {
        Iads = iads,
        Options = options,
        FilterNatoName = {}
    }
    setmetatable(this, self)
    self.__index = self

    return this
end

function VeafSkynetMonitorDescriptor:GetIndentationString(iIndentation)
    iIndentation = iIndentation or 0
    return string.rep(self._staticSring.Indentation, iIndentation)
end

function VeafSkynetMonitorDescriptor:AppendString(s, sAppend)
    sAppend = sAppend or ""

    if (isNullOrEmpty(s)) then
        return sAppend
    elseif (isNullOrEmpty(sAppend)) then
        return s
    else
        return s .. self._staticSring.Separator .. sAppend
    end
end

function VeafSkynetMonitorDescriptor:NewLine(s, iIndentation)
    return s .. "\n" .. self:GetIndentationString(iIndentation)
end

function VeafSkynetMonitorDescriptor:AppendLine(s, sAppend, iIndentation)
    sAppend = sAppend or ""

    if (isNullOrEmpty(s)) then
        return self:GetIndentationString(iIndentation) .. sAppend
    elseif (isNullOrEmpty(sAppend)) then
        return s
    else
        return self:NewLine(s, iIndentation) .. sAppend
    end
end

function VeafSkynetMonitorDescriptor:GetStringSkynetElement(skynetElement)
    local s = veafSkynet.getStringSkynetElement(skynetElement)
    local dcsGroup = veafSkynet.getDcsGroupFromSkynetElement(skynetElement)
    if (dcsGroup == nil) then
        s = s .. " (dcs group not found)"
    else
        local bActive = false
        for _, dcsUnit in pairs(dcsGroup:getUnits()) do
            if (dcsUnit:isActive()) then
                bActive = true
                break
            end
        end

        if (not bActive) then
            s = s .. " (dcs group not active)" 
        end
    end

    return s
end

function VeafSkynetMonitorDescriptor:GetStringElementStructure(details, sDetailType)
    local s = ""

    if (details == nil or #details <= 0) then
        s = self:AppendString(s, "No " .. sDetailType)
    else
        local iMaximumRangeMeters = nil
        for i = 1, #details do
            local detail = details[i]
            if (detail.maximumRange and (iMaximumRangeMeters == nil or iMaximumRangeMeters < detail.maximumRange))then
                iMaximumRangeMeters = detail.maximumRange
            end
        end

        local sMaximumRange = ""
        if (iMaximumRangeMeters) then
            iMaximumRangeMeters = veaf.round(mist.utils.metersToNM(iMaximumRangeMeters), 1)
            sMaximumRange = " range:" .. iMaximumRangeMeters .. "nm"
        end
        s = self:AppendString(s, sDetailType .. ":" .. #details .. sMaximumRange)
    end

    return s
end

function VeafSkynetMonitorDescriptor:GetStringEwr(ewr, iIndentation)
    local s = ""

    -- Site description
    s = self:AppendLine(s, "EWR : " .. VeafSkynetMonitorDescriptor:GetStringSkynetElement(ewr), iIndentation)

    -- Site detail
    if (tableContains(self.Options, VeafSkynetMonitorDescriptor.Option.ElementDetail)) then
        s = self:NewLine(s, iIndentation + 1)

        if (ewr.harmDetectionChance) then
            s = self:AppendString(s, "HARM detection " .. ewr.harmDetectionChance .. "%")
        end
        if (ewr.autonomousBehaviour == SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DCS_AI) then
            s = self:AppendString(s, "Autonomous:DCS AI")
        elseif (ewr.autonomousBehaviour == SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK) then
            s = self:AppendString(s, "Autonomous:dark")
        end
        if (ewr.goLiveRange == SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE) then
            s = self:AppendString(s, "Go live:kill zone")
        elseif (ewr.goLiveRange == SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE) then
            s = self:AppendString(s, "Go live:search range")
        end
    end

    -- Site structure
    local pointDefences = ewr:getPointDefences()
    if (tableContains(self.Options, VeafSkynetMonitorDescriptor.Option.ElementStructure)) then
        local radars = ewr:getRadars()

        s = self:NewLine(s, iIndentation + 1)
        s = self:AppendString(s, self:GetStringElementStructure(radars, "Radars"))
        s = self:AppendString(s, self:GetStringElementStructure(pointDefences, "Point defences"))
    end

    -- Site state
    s = self:NewLine(s, iIndentation + 1)
    if (ewr:isActive()) then
        s = self:AppendString(s, "Active")
    else
        s = self:AppendString(s, "Not active")
    end
    if (ewr:getAutonomousState()) then
        s = self:AppendString(s, "Autonomous")
    end
    if (ewr.harmSilenceID ~= nil) then
        s = self:AppendString(s, "Defending HARM (shutdown time " .. ewr.harmShutdownTime .. "s)")
    end

    if (tableContains(self.Options, VeafSkynetMonitorDescriptor.Option.ElementTargets)) then
        local targets = ewr:getDetectedTargets()
        if (targets == nil or #targets <= 0) then
            s = self:AppendLine(s, "No targets", iIndentation + 1)
        else
            s = self:AppendLine(s, "Targets :", iIndentation + 1)

            for i = 1, #targets do
                local target = targets[i]
                s = self:AppendLine(s, target:getName(), iIndentation + 2)
                s = self:AppendString(s, target:getTypeName())
            end
        end
    end

    if (tableContains(self.Options, VeafSkynetMonitorDescriptor.Option.NestedPointDefences)) then
        if (pointDefences and #pointDefences > 0) then
            s = self:AppendLine(s, "Point defences :", iIndentation + 1)

            for i = 1, #pointDefences do
                local pointDefence = pointDefences[i]
                s = self:AppendLine(s, self:GetStringSam(pointDefence, iIndentation + 2))
            end
        end
    end

    return s
end

function VeafSkynetMonitorDescriptor:GetStringSam(samSite, iIndentation)
    local s = ""

    -- Site description
    s = self:AppendLine(s, "Sam site : " .. VeafSkynetMonitorDescriptor:GetStringSkynetElement(samSite), iIndentation)
    if (samSite.isAPointDefence) then
        s = self:AppendString(s, "**Point defence**")
    end

    -- Site detail
    if (tableContains(self.Options, VeafSkynetMonitorDescriptor.Option.ElementDetail)) then
        s = self:NewLine(s, iIndentation + 1)

        if (samSite:getCanEngageAirWeapons()) then
            s = self:AppendString(s, "Can engage air weapons")
        end
        if (samSite:getCanEngageHARM()) then
            s = self:AppendString(s, "Can engage HARMs")
        end
        if (samSite.harmDetectionChance) then
            s = self:AppendString(s, "HARM detection " .. samSite.harmDetectionChance .. "%")
        end
        if (samSite.autonomousBehaviour == SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DCS_AI) then
            s = self:AppendString(s, "Autonomous:DCS AI")
        elseif (samSite.autonomousBehaviour == SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK) then
            s = self:AppendString(s, "Autonomous:dark")
        end
        if (samSite.goLiveRange == SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE) then
            s = self:AppendString(s, "Go live:kill zone " .. samSite.firingRangePercent .. "% of max range")
        elseif (samSite.goLiveRange == SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_SEARCH_RANGE) then
            s = self:AppendString(s, "Go live:search range")
        end
    end

    local iRangeMeters = 0
    local launchers = samSite:getLaunchers()


    -- Site structure
    local pointDefences = samSite:getPointDefences()
    if (tableContains(self.Options, VeafSkynetMonitorDescriptor.Option.ElementStructure)) then
        local searchRadars = samSite:getSearchRadars()
        local trackRadars = samSite:getTrackingRadars()
        local launchers = samSite:getLaunchers()

        s = self:NewLine(s, iIndentation + 1)
        s = self:AppendString(s, self:GetStringElementStructure(searchRadars, "Search radars"))
        s = self:AppendString(s, self:GetStringElementStructure(trackRadars, "Track radars"))
        s = self:AppendString(s, self:GetStringElementStructure(launchers, "Launchers"))
        s = self:AppendString(s, self:GetStringElementStructure(pointDefences, "Point defences"))
    end

    -- Site state
    s = self:NewLine(s, iIndentation + 1)
    if (samSite:isActive()) then
        s = self:AppendString(s, "Active")
    else
        s = self:AppendString(s, "Not active")
    end
    if (samSite:getAutonomousState()) then
        s = self:AppendString(s, "Autonomous")
    end
    if (samSite:getActAsEW()) then
        s = self:AppendString(s, "Acting as EW")
    end
    if (not samSite:hasRemainingAmmo()) then
        s = self:AppendString(s, "No ammo")
    end
    if (samSite.harmSilenceID ~= nil) then
        s = self:AppendString(s, "Defending HARM (shutdown time " .. samSite.harmShutdownTime .. "s)")
    end

    if (tableContains(self.Options, VeafSkynetMonitorDescriptor.Option.ElementTargets)) then
        local targets = samSite:getDetectedTargets()
        if (targets == nil or #targets <= 0) then
            s = self:AppendLine(s, "No targets", iIndentation + 1)
        else
            s = self:AppendLine(s, "Targets :", iIndentation + 1)

            for i = 1, #targets do
                local target = targets[i]
                s = self:AppendLine(s, target:getName(), iIndentation + 2)
                s = self:AppendString(s, target:getTypeName())
            end
        end
    end

    if (tableContains(self.Options, VeafSkynetMonitorDescriptor.Option.NestedPointDefences)) then
        if (pointDefences and #pointDefences > 0) then
            s = self:AppendLine(s, "Point defences :", iIndentation + 1)

            for i = 1, #pointDefences do
                local pointDefence = pointDefences[i]
                s = self:AppendLine(s, self:GetStringSam(pointDefence, iIndentation + 2))
            end
        end
    end

    return s
end

function VeafSkynetMonitorDescriptor:GetStringDescription()
    local s = ""
    s = self:AppendLine("IADS : " .. self.Iads:getCoalitionString())
    local iIndentation = 1

    if (tableContains(self.Options, VeafSkynetMonitorDescriptor.Option.Ewr)) then
        local ewrs = self.Iads:getEarlyWarningRadars()
        if (#ewrs <= 0) then
            s = self:AppendLine(s, "No EWR", iIndentation)
        else
            s = self:AppendLine(s, "EWRs :", iIndentation)
            for i = 1, #ewrs do
                local ewr = ewrs[i]
                
                local bAddEwr = true
                
                if(self.FilterNatoName and #self.FilterNatoName > 0 and not tableContains(self.FilterNatoName, ewr:getNatoName())) then
                    bAddEwr = false
                end

                if (bAddEwr) then
                    s = self:AppendLine(s, self:GetStringEwr(ewr, iIndentation + 1))
                end
            end
        end
    end

    if (tableContains(self.Options, VeafSkynetMonitorDescriptor.Option.Sam)) then
        local samSites = self.Iads:getSAMSites()
        if (#samSites <= 0) then
            s = self:AppendLine(s, "No sam sites", iIndentation)
        else
            s = self:AppendLine(s, "Sam sites :", iIndentation)
            for i = 1, #samSites do
                local samSite = samSites[i]
                
                local bAddSamSite = true

                if (tableContains(self.Options, VeafSkynetMonitorDescriptor.Option.NestedPointDefences) and samSite.isAPointDefence) then
                    bAddSamSite = false
                end
                
                if(self.FilterNatoName and #self.FilterNatoName > 0 and not tableContains(self.FilterNatoName, samSite:getNatoName())) then
                    bAddSamSite = false
                end

                if (bAddSamSite) then
                    s = self:AppendLine(s, self:GetStringSam(samSite, iIndentation + 1))
                end
            end
        end
    end

    if (tableContains(self.Options, VeafSkynetMonitorDescriptor.Option.Targets)) then

    end

    return s
end

---------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------
---  VeafSkynetMonitorTask base class
---  Named monitoring task
---  A monitoring task represents a task to be executed by the monitoring thread
---------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------
VeafSkynetMonitorTask = {}
---------------------------------------------------------------------------------------------------
---  CTOR
function VeafSkynetMonitorTask:Create(sName)
    local this =
    {
        Name = sName
    }
    setmetatable(this, self)
    self.__index = self

    return this
end

function VeafSkynetMonitorTask:ToString()
    return self.Name
end

function VeafSkynetMonitorTask:Execute()
    veaf.loggers.get(veafSkynetMonitor.Id):trace("Executing VeafSkynetMonitorTask")
end

---------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------
---  VeafSkynetMonitorTaskContacts class
---  Named monitoring task to check for contacts detected and lost by an iads
---------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------
VeafSkynetMonitorTaskContacts = {}
VeafSkynetMonitorTaskContacts = inheritsFrom(VeafSkynetMonitorTask) -- oop functions are from Skynet
---------------------------------------------------------------------------------------------------
---  CTOR
function VeafSkynetMonitorTaskContacts:Create(sName, iads, unitsToMonitor, onDetectedAction, onLostAction)
    local this = self:superClass():Create(sName, iads)
    setmetatable(this, self)
    self.__index = self

    this.Iads = iads
    this.UnitsToMonitor = unitsToMonitor
    this.TrackedUnits = {}
    this.OnDetectedAction = onDetectedAction
    this.OnLostAction = onLostAction

    return this
end

function VeafSkynetMonitorTaskContacts:ToString()
    return self.Name .. " | IADS [" .. self.Iads:getCoalitionString() .. "]"
end

function VeafSkynetMonitorTaskContacts:Execute()
    veaf.loggers.get(veafSkynetMonitor.Id):trace("Executing VeafSkynetMonitorTaskContacts")
    local currentContacts = self:GetIadsCurrentContacts()
    veaf.loggers.get(veafSkynetMonitor.Id):trace(self:ToString() .. " - currently tracking " .. #self.TrackedUnits .. " monitored units")

    for _, sContactName in pairs(currentContacts) do
        if (self:ContactIsToMonitor(sContactName) and not self:ContactIsTracked(sContactName)) then
            veaf.loggers.get(veafSkynetMonitor.Id):trace("Monitored contact detected: " .. sContactName)
            self:AddTrackedContact(sContactName)
            local err, errmsg = pcall(self.OnDetectedAction, sContactName)
        end
    end

    for _, sContactName in pairs(self.TrackedUnits) do
        if (not tableContains(currentContacts, sContactName)) then
            veaf.loggers.get(veafSkynetMonitor.Id):trace("Monitored contact lost: " .. sContactName)
            self:RemoveTrackedContact(sContactName)
            local err, errmsg = pcall(self.OnLostAction, sContactName)
        end
    end
end

function VeafSkynetMonitorTaskContacts:ContactIsToMonitor(sContactName)
    return self.UnitsToMonitor and tableContains(self.UnitsToMonitor, sContactName)
end

function VeafSkynetMonitorTaskContacts:GetIadsCurrentContacts()
    local currentContacts = {}
    for _, contact in pairs(self.Iads:getContacts()) do
        table.insert(currentContacts, contact:getName())
    end

    return currentContacts
end

function VeafSkynetMonitorTaskContacts:ContactIsTracked(sContactName)
    return tableContains(self.TrackedUnits, sContactName)
end

function VeafSkynetMonitorTaskContacts:AddTrackedContact(sContactName)
    table.insert(self.TrackedUnits, sContactName)
end

function VeafSkynetMonitorTaskContacts:RemoveTrackedContact(sContactName)
    tableRemove(self.TrackedUnits, sContactName)
end

---------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------
---  VeafSkynetMonitorTaskDescriptor class
---  Named monitoring task to display informations regarding an iads
---------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------
VeafSkynetMonitorTaskDescriptor = {}
VeafSkynetMonitorTaskDescriptor = inheritsFrom(VeafSkynetMonitorTask) -- oop functions are from Skynet

VeafSkynetMonitorTaskDescriptor.OutputDcsTextRecipients = { All = "*All*", Coalition = "*Coalition*" }
---------------------------------------------------------------------------------------------------
---  CTOR
function VeafSkynetMonitorTaskDescriptor:Create(sName, descriptors, sOutputLogLevel, outputDcsTextRecipients)
    local this = self:superClass():Create(sName)
    setmetatable(this, self)
    self.__index = self

    this.Descriptors = descriptors
    this.OutputLogLevel = sOutputLogLevel
    this.OutputDcsTextRecipients = outputDcsTextRecipients

    return this
end

function VeafSkynetMonitorTaskDescriptor:Output(sInformation)
    if (self.OutputLogLevel == "error") then
        veaf.loggers.get(veafSkynetMonitor.Id):error(sInformation)
    elseif (self.OutputLogLevel == "warning") then
        veaf.loggers.get(veafSkynetMonitor.Id):warning(sInformation)
    elseif (self.OutputLogLevel == "info") then
        veaf.loggers.get(veafSkynetMonitor.Id):info(sInformation)
    elseif (self.OutputLogLevel == "debug") then
        veaf.loggers.get(veafSkynetMonitor.Id):debug(sInformation)
    elseif (self.OutputLogLevel == "trace") then
        veaf.loggers.get(veafSkynetMonitor.Id):trace(sInformation)
    end

    if (self.OutputDcsTextRecipients and type(self.OutputDcsTextRecipients) == "table" and #self.OutputDcsTextRecipients > 0) then
        local iDuration = 4
        for _, recipient in pairs(self.OutputDcsTextRecipients) do
            if (recipient == VeafSkynetMonitorTaskDescriptor.OutputDcsTextRecipients.All) then
                trigger.action.outText(sInformation, iDuration)
            elseif (tableContains (coalition.side, recipient)) then
                trigger.action.outTextForCoalition(recipient, sInformation, iDuration)
            else
                local group = Group.getByName(recipient)
                if (group) then
                    trigger.action.outTextForGroup(group.id_, sInformation, iDuration)
                end
            end
        end
    end
end

function VeafSkynetMonitorTaskDescriptor:Execute()
    veaf.loggers.get(veafSkynetMonitor.Id):trace("Executing VeafSkynetMonitorTaskInformations")

    self:Output("----- Task Descriptor [ " .. self.Name .. " ] -----")
    for _, descriptor in pairs(self.Descriptors) do
        local sInformation = descriptor:GetStringDescription()
        self:Output(sInformation)
    end
end

---------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------
---  Skynet monitoring thread management
---------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------
veafSkynetMonitor._monitoringTasks = {}
veafSkynetMonitor._monitoringThreadId = nil
veafSkynetMonitor._interval = 5 -- iads.contactUpdateInterval

function veafSkynetMonitor.AddMonitoringTask(task)
    if (task == nil) then
        return
    end

    if (task.Name == nil or task.Name == "") then
        veaf.loggers.get(veafSkynetMonitor.Id):error("Monitoring task with incorrect name cannot be added")
        return
    end
    if (veafSkynetMonitor._monitoringTasks[task.Name] ~= nil) then
        veaf.loggers.get(veafSkynetMonitor.Id):error("Monitoring task with name [" .. task.Name .. "] already added")
        return
    end

    veaf.loggers.get(veafSkynetMonitor.Id):trace("Adding monitoring task: " .. task:ToString())
    veafSkynetMonitor._monitoringTasks[task.Name] = task

    if (veafSkynetMonitor._monitoringThreadId == nil) then
        veaf.loggers.get(veafSkynetMonitor.Id):trace("Starting mist thread")
        veafSkynetMonitor._monitoringThreadId = mist.scheduleFunction(veafSkynetMonitor.ExecuteMonitoringTasks, {},
            timer.getTime() + veafSkynetMonitor._interval, veafSkynetMonitor._interval, timer.getTime() + 3600)
    end
end

function veafSkynetMonitor.AddMonitoringTaskContacts(sTaskName, iads, unitsToMonitor, onDetectedAction, onLostAction)
    local task = VeafSkynetMonitorTaskContacts:Create(sTaskName, iads, unitsToMonitor, onDetectedAction, onLostAction)
    veafSkynetMonitor.AddMonitoringTask(task)
end

function veafSkynetMonitor.RemoveMonitoringTask(sTaskName)
    sTaskName = sTaskName or ""
    if (sTaskName ~= "" and veafSkynetMonitor._monitoringTasks[sTaskName]) then
        veaf.loggers.get(veafSkynetMonitor.Id):trace("Removing monitoring task: " .. veafSkynetMonitor._monitoringTasks[sTaskName]:ToString())
        veafSkynetMonitor._monitoringTasks[sTaskName] = nil
    end

    if (veaf.length(veafSkynetMonitor._monitoringTasks) <= 0) then
        veaf.loggers.get(veafSkynetMonitor.Id):trace("Nothing to monitor, stopping mist thread")
        mist.removeFunction(veafSkynetMonitor._monitoringThreadId)
    end
end

function veafSkynetMonitor.ExecuteMonitoringTasks()
    for _, task in pairs(veafSkynetMonitor._monitoringTasks) do
        task:Execute()
    end
end

---------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------
---  Skynet iads informations
---------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------

veaf.loggers.get(veafSkynetMonitor.Id):info(string.format("Loading version %s", veafSkynetMonitor.Version))
