-- Transpiled at (UTC): 2026-09-21T21:00:07.438Z local ScriptGlobals = {} do -- classes.util.util local UTIL = {} do function UTIL.split_string(input, separator) if separator == nil then separator = " " end local result = {} if input == nil then return result end for str in string.gmatch(input, "[^" .. separator .. "]+") do table.insert(result, str) end return result end function UTIL.tableLength(table) if table == nil then return 0 end local count = 0 for _ in pairs(table) do count = count + 1 end return count end function UTIL.deepCopyTable(orig) local function deepCopy(original) local orig_type = type(original) local copy if orig_type == 'table' then original = original copy = {} for orig_key, orig_value in pairs(original) do local copiedKey = deepCopy(orig_key) copy[copiedKey] = deepCopy(orig_value) end setmetatable(copy, deepCopy(getmetatable(original))) else copy = original end return copy end return deepCopy(orig) end function UTIL.randomFromList(list) local max = #list if max == 0 or max == nil then return nil end local random = math.random(0, max) if random == 0 then random = 1 end return list[random] end function UTIL.sublist(list, start, n) local result = {} for i = start, n do result[#result + 1] = list[i] end return result end function UTIL.replaceString(str, find, replace) if str == nil then return "" end if find == nil or replace == nil then return str end local result = str:gsub(find, replace) return result end local function table_print(tt, indent, done) done = done or {} indent = indent or 0 if type(tt) == "table" then tt = tt local sb = {} for key, value in pairs(tt) do table.insert(sb, string.rep(" ", indent)) if type(value) == "table" and not done[value] then done[value] = true table.insert(sb, "[\"" ..key .. "\"]" .. " = {\n"); table.insert(sb, table_print(value, indent + 2, done)) table.insert(sb, string.rep(" ", indent)) table.insert(sb, "},\n"); elseif "number" == type(key) then table.insert(sb, string.format("\"%s\",\n", tostring(value))) else table.insert(sb, string.format( "[\"%s\"] = \"%s\",\n", tostring(key), tostring(value))) end end return table.concat(sb) else return tt .. "\n" end end UTIL.startsWith = function(str, findable, ignoreCase) if ignoreCase == true then return string.lower(str):find('^' .. string.lower(findable)) ~= nil end return str:find('^' .. findable) ~= nil end UTIL.endsWith = function(str, findable, ignoreCase) if ignoreCase == true then return string.lower(str):find(string.lower(findable) .. '$') ~= nil end return str:find(findable .. '$') ~= nil end UTIL.strContains = function(str, findable) return str:find(findable) ~= nil end UTIL.startswithAny = function(str, findableTable) for _, value in pairs(findableTable) do if type(value) == "string" and UTIL.startsWith(str, value) then return true end end return false end function UTIL.toString(something) if something == nil then return "nil" elseif "table" == type(something) then return table_print(something) elseif "string" == type(something) then return something else return tostring(something) end end function UTIL.VectorDistance2d(a, b) return math.sqrt((b.x - a.x) ^ 2 + (b.y - a.y) ^ 2) end function UTIL.VectorDistance3d(a, b) return UTIL.vectorMagnitude({ x = a.x - b.x, y = a.y - b.y, z = a.z - b.z }) end function UTIL.vectorMagnitude(vec) return (vec.x ^ 2 + vec.y ^ 2 + vec.z ^ 2) ^ 0.5 end function UTIL.vectorNormalize(vec) local magnitude = UTIL.vectorMagnitude(vec) if magnitude == 0 then return { x = 0, y = 0, z = 0 } end return { x = vec.x / magnitude, y = vec.y / magnitude, z = vec.z / magnitude } end function UTIL.vectorMove(vec, direction, distance) local rad = math.rad(direction) local x = vec.x + (math.cos(rad) * distance) local y = vec.y + (math.sin(rad) * distance) return { x = x, y = y} end function UTIL.vectorHeadingFromTo(vec1, vec2) local dx = vec2.x - vec1.x local dy = vec2.y - vec1.y local heading = math.deg(math.atan2(dy, dx)) if heading < 0 then heading = heading + 360 end return heading end function UTIL.vectorAlignment(vec1, vec2) local vec1Norm = UTIL.vectorNormalize(vec1) local vec2Norm = UTIL.vectorNormalize(vec2) return ((vec1Norm.x * vec2Norm.x) + (vec1Norm.y * vec2Norm.y) + (vec1Norm.z * vec2Norm.z)) end local function isInComplexPolygon(polygon, x, y) local function getEdges(poly) local result = {} for i = 1, #poly do local point1 = poly[i] local point2Index = i + 1 if point2Index > #poly then point2Index = 1 end local point2 = poly[point2Index] local edge = { x1 = point1.x, z1 = point1.y, x2 = point2.x, z2 = point2.y } table.insert(result, edge) end return result end local edges = getEdges(polygon) local count = 0; for _, edge in pairs(edges) do if (x < edge.x1) ~= (x < edge.x2) and y < edge.z1 + ((x - edge.x1) / (edge.x2 - edge.x1)) * (edge.z2 - edge.z1) then count = count + 1 end end return count % 2 == 1 end function UTIL.IsPointInPolygon(polygon, x, y) return isInComplexPolygon(polygon, x, y) end function UTIL.is3dPointInZone(point, zone) if zone.zone_type == "Polygon" and zone.verts then if UTIL.IsPointInPolygon(zone.verts, point.x, point.z) == true then return true end else if (((point.x - zone.location.x) ^ 2 + (point.z - zone.location.y) ^ 2) ^ 0.5 <= zone.radius) then return true end end return false end function UTIL.is2dPointInZone(point, zone) if zone.zone_type == "Polygon" and zone.verts then if UTIL.IsPointInPolygon(zone.verts, point.x, point.y) == true then return true end else if (((point.x - zone.location.x) ^ 2 + (point.y - zone.location.y) ^ 2) ^ 0.5 <= zone.radius) then return true end end return false end function UTIL.getConvexHull3d(points) if #points == 0 then return {} end local function ccw(a, b, c) return (b.z - a.z) * (c.x - a.x) > (b.x - a.x) * (c.z - a.z) end table.sort(points, function(left, right) return left.z < right.z end) local hull = {} for _, point in pairs(points) do while #hull >= 2 and not ccw(hull[#hull - 1], hull[#hull], point) do table.remove(hull, #hull) end table.insert(hull, point) end local t = #hull + 1 for i = #points, 1, -1 do local point = points[i] while #hull >= t and not ccw(hull[#hull - 1], hull[#hull], point) do table.remove(hull, #hull) end table.insert(hull, point) end table.remove(hull, #hull) return hull end function UTIL.getConvexHull(points) if #points == 0 then return {} end local function ccw(a, b, c) return (b.y - a.y) * (c.x - a.x) > (b.x - a.x) * (c.y - a.y) end table.sort(points, function(left, right) return left.y < right.y end) local hull = {} for _, point in pairs(points) do while #hull >= 2 and not ccw(hull[#hull - 1], hull[#hull], point) do table.remove(hull, #hull) end table.insert(hull, point) end local t = #hull + 1 for i = #points, 1, -1 do local point = points[i] while #hull >= t and not ccw(hull[#hull - 1], hull[#hull], point) do table.remove(hull, #hull) end table.insert(hull, point) end table.remove(hull, #hull) return hull end function UTIL.getSeparatedConvexHulls(points, minSeparation) if #points == 0 then return {} end local clusters = {} local assigned = {} for i, p in ipairs(points) do if not assigned[i] then local cluster = { p } assigned[i] = true local queue = { i } while #queue > 0 do local idx = table.remove(queue) local base = points[idx] for j, q in ipairs(points) do if not assigned[j] then local dx = base.x - q.x local dy = base.y - q.y if (dx * dx + dy * dy) <= (minSeparation * minSeparation) then table.insert(cluster, q) assigned[j] = true table.insert(queue, j) end end end end table.insert(clusters, cluster) end end local hulls = {} for _, cluster in ipairs(clusters) do local hull = UTIL.getConvexHull(cluster) if #hull > 0 then table.insert(hulls, hull) end end return hulls end function UTIL.enlargeConvexHull(points, meters) if points == nil or #points == 0 then return {} end local allpoints = {} for _, point in pairs(points) do table.insert(allpoints, point) allpoints[#allpoints + 1] = point allpoints[#allpoints+1] = { x = point.x + meters, y = point.y, } allpoints[#allpoints+1] = { x = point.x - meters, y = point.y, } allpoints[#allpoints+1] = { x = point.x, y = point.y + meters, } allpoints[#allpoints+1] = { x = point.x, y = point.y - meters, } allpoints[#allpoints+1] = { x = point.x + math.cos(math.rad(45)) * meters, y = point.y + math.sin(math.rad(45)) * meters, } allpoints[#allpoints+1] = { x = point.x - math.cos(math.rad(45)) * meters, y = point.y - math.sin(math.rad(45)) * meters, } allpoints[#allpoints+1] = { x = point.x - math.cos(math.rad(45)) * meters, y = point.y + math.sin(math.rad(45)) * meters, } allpoints[#allpoints+1] = { x = point.x + math.cos(math.rad(45)) * meters, y = point.y - math.sin(math.rad(45)) * meters, } end return UTIL.getConvexHull(allpoints) end function UTIL.GetVisibleHullPointsFromOrigin(hull, origin) local function segmentsIntersect(a, b, c, d) local function ccw(p1, p2, p3) return (p3.y - p1.y) * (p2.x - p1.x) > (p2.y - p1.y) * (p3.x - p1.x) end return (ccw(a, c, d) ~= ccw(b, c, d)) and (ccw(a, b, c) ~= ccw(a, b, d)) end local n = #hull local visible = {} for i = 1, n do local p = hull[i] local isVisible = true for j = 1, n do local a = hull[j] local b = hull[(j % n) + 1] if (a ~= p and b ~= p) then if segmentsIntersect(origin, p, a, b) then isVisible = false break end end end if isVisible then table.insert(visible, p) end end return visible end function UTIL.GetTangentHullPointsFromOrigin(hull, origin) if hull == nil or #hull <= 0 then return {} end local function orientation(a, b, c) return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x) end local n = #hull if n == 0 then return {} end if n == 1 then return { hull[1] } end local function findTangent(isLeft) local best = 1 for i = 2, n do local o = orientation(origin, hull[best], hull[i]) if (isLeft and o < 0) or (not isLeft and o > 0) then best = i end end return hull[best] end local leftTangent = findTangent(true) local rightTangent = findTangent(false) if leftTangent.x == rightTangent.x and leftTangent.y == rightTangent.y then return { leftTangent } else return { leftTangent, rightTangent } end end end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.util then ScriptGlobals.classes.util = {} end ScriptGlobals.classes.util.util = UTIL end -- classes.util.util do -- classes.configuration.globalconfig local GlobalConfig = {} GlobalConfig.__index = GlobalConfig; function GlobalConfig.New() local self = setmetatable({}, GlobalConfig) self._briefingTime = 30 if SpearheadConfig then if SpearheadConfig.briefingMessageDuration then self._briefingTime = SpearheadConfig.briefingMessageDuration end if SpearheadConfig.debugEnabled ~= nil then self._debugEnabled = SpearheadConfig.debugEnabled else self._debugEnabled = false end self._debugMenuEnabled = SpearheadConfig.debugMenuEnabled == true end return self end function GlobalConfig:getBriefingTime() return self._briefingTime or 60 end function GlobalConfig:isDebugEnabled() return self._debugEnabled or false end function GlobalConfig:isDebugMenuEnabled() return self._debugMenuEnabled or false end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.configuration then ScriptGlobals.classes.configuration = {} end ScriptGlobals.classes.configuration.globalconfig = GlobalConfig end -- classes.configuration.globalconfig do -- classes.util.logger local Util = ScriptGlobals.classes.util.util local SpearheadConfig = ScriptGlobals.classes.configuration.globalconfig local defaultLogLevel = "INFO" if SpearheadConfig then if SpearheadConfig:isDebugEnabled() then defaultLogLevel = "DEBUG" end end local LOGGER = {} do local PreFix = "Spearhead" function LOGGER.new(logger_name, logLevel) LOGGER.__index = LOGGER local self = setmetatable({}, LOGGER) self.LoggerName = logger_name or "(loggername not set)" self.LogLevel = logLevel or defaultLogLevel return self end function LOGGER:info(message) if message == nil then return end message = Util.toString(message) if self.LogLevel == "INFO" or self.LogLevel == "DEBUG" then env.info("[" .. PreFix .. "]" .. "[" .. self.LoggerName .. "] " .. message) end end function LOGGER:warn(message) if message == nil then return end message = Util.toString(message) if self.LogLevel == "INFO" or self.LogLevel == "DEBUG" or self.LogLevel == "WARN" then env.warning("[" .. PreFix .. "]" .. "[" .. self.LoggerName .. "] " .. message) end end function LOGGER:error(message) if message == nil then return end message = Util.toString(message) if self.LogLevel == "INFO" or self.LogLevel == "DEBUG" or self.LogLevel == "WARN" or self.LogLevel == "ERROR" then env.error("[" .. PreFix .. "]" .. "[" .. self.LoggerName .. "] " .. message) end end function LOGGER:debug(message) if message == nil then return end message = Util.toString(message) if self.LogLevel == "DEBUG" then env.info("[" .. PreFix .. "]" .. "[" .. self.LoggerName .. "][DEBUG] " .. message) end end end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.util then ScriptGlobals.classes.util = {} end ScriptGlobals.classes.util.logger = LOGGER end -- classes.util.logger do -- classes.persistence.persistence local Util = ScriptGlobals.classes.util.util local Persistence = {} do local EXTENSION = "spearhead" local persistanceWriteIntervalSeconds = 15 local enabled = false local version = "1.0.0" local tables = { version = version, unitsStates = {}, random_missions = {}, deliveredKilos = {}, activeStage = nil, -- Backwards compatibility for previous version without lanes activeStageInStageLane = {} } local logger = {} if SpearheadConfig == nil then SpearheadConfig = {} end if SpearheadConfig.Persistence == nil then SpearheadConfig.Persistence = {} end local createFileIfNotExists = function(path) if not path or path == "" then logger:error("Persistence file path is not set, cannot create file") return end local f = io.open(path, "r") if f == nil then logger:info("Persistence file does not exist, creating new one at: " .. path) f = io.open(path, "w+") if f == nil then logger:error("Could not create a file") else f:write("{}") f:close() end logger:info("Created new persistence file at: " .. path) else f:close() end end local loadTablesFromFile = function(path) if not path then return end logger:info("Loading data from persistance file...") local f = io.open(path, "r") if f == nil then logger:error("Could not open persistence file for reading: " .. path) return end local json = f:read("*a") f:close() logger:debug("Loaded persistence file content: " .. json) local lua = net.json2lua(json) if lua then tables = lua logger:debug("Loaded persistence data: " .. Util.toString(lua)) else logger:error("Could not load persistence file, using default tables") end if tables.version == nil then tables.version = version end end local writeToFile = function() if not Persistence._path then return end local path = Persistence._path local f = io.open(path, "w+") if f == nil then error("Could not open file for writing") return end local jsonString = net.lua2json(tables) f:write(jsonString) if f ~= nil then f:close() end Persistence._updateRequired = false logger:info("Wrote persistence data to file") end local UpdateContinuous = function(_, time) env.info("[Spearhead][Persistence] Checking up on persistence state...") if Persistence._updateRequired == true then local status, result = pcall(writeToFile) if status == false then env.error("[Spearhead][Persistence] Could not write state to file: " .. result) end end return time + persistanceWriteIntervalSeconds end Persistence.UpdateNow = function() if enabled == true then writeToFile() end end Persistence.isEnabled = function() return enabled end local warnForNonPersistenceContinous = function(_, time) trigger.action.outText("Persistence was enabeld, however, io and lfs are not available and no persistence will be done. Make sure to either disable persistence or fix the issues before continuing.", 10) return time + 9 end local getLastFileOrDefault = function(dir, startsWith, default) local latestFile, lastNumber = default, 0 for file in lfs.dir(dir) do local split = Util.split_string(file, ".") local doesStartWith = Util.startsWith(file, startsWith, true) if split and #split > 0 and split[#split] == EXTENSION and doesStartWith == true then local numberString = split[#split-1] local number = tonumber(numberString) if number ~= nil and number > lastNumber then lastNumber = number latestFile = file end end end return latestFile end Persistence.Init = function(persistenceLogger) logger = persistenceLogger logger:info("Initiating Persistence Manager") logger:debug("Initiating Persistence Manager") if lfs == nil or io == nil then logger:error("lfs and io seem to be sanitized. Persistence is skipped and disabled") enabled = false timer.scheduleFunction(warnForNonPersistenceContinous, nil, timer.getTime() + 10) return end local dir = lfs.writedir() .. "\\Data" local fileName = "Spearhead_Persistence.0.spearhead" if SpearheadConfig and SpearheadConfig.Persistence then if SpearheadConfig.Persistence.fileName then if type(SpearheadConfig.Persistence.fileName) ~= "string" then SpearheadConfig.Persistence.fileName = "Spearhead_Persistence.0.spearhead" end local userFileName = SpearheadConfig.Persistence.fileName local split = Util.split_string(userFileName, ".") if not split or #split < 3 then split = split or {} split[#split+1] = "0" split[#split+1] = "spearhead" end if tonumber(split[#split-1]) == nil then split[#split+1] = "0" split[#split+1] = "spearhead" end fileName = table.concat(split, ".") end if SpearheadConfig.Persistence.directory then if type(SpearheadConfig.Persistence.directory) ~= "string" then SpearheadConfig.Persistence.directory = lfs.writedir() .. "\\Data" end dir = SpearheadConfig.Persistence.directory end end local split = Util.split_string(fileName, ".") local matchingPart = table.concat(Util.sublist(split, 1, #split-2), ".") local lastFile = getLastFileOrDefault(dir, matchingPart, fileName) if lastFile == nil then lastFile = fileName end local fileSplit = Util.split_string(lastFile, ".") fileSplit[#fileSplit-1] = tostring(tonumber(fileSplit[#fileSplit-1]) + 1) fileName = table.concat(fileSplit, ".") if lastFile ~= fileName then logger:info("Found last persistence file: " .. lastFile) else logger:info("No previous persistence file found, using default: " .. fileName) end logger:info("New Persistence file name: " .. tostring(fileName)) local lastPath = dir .. "\\" .. lastFile local path = dir .. "\\" .. fileName Persistence._path = path createFileIfNotExists(path) loadTablesFromFile(lastPath) timer.scheduleFunction(UpdateContinuous, nil, timer.getTime() + 120) enabled = true Persistence.UpdateNow() end Persistence.SetActiveStage = function(stageLane, stageNumber) stageLane = stageLane or "nil" tables.activeStageInStageLane[stageLane] = stageNumber Persistence._updateRequired = true end Persistence.GetActiveStage = function(stageLane) stageLane = stageLane or "nil" if stageLane == "nil" and tables.activeStageInStageLane[stageLane] == nil then tables.activeStageInStageLane[stageLane] = tables.activeStage tables.activeStage = nil end return tables.activeStageInStageLane[stageLane] end Persistence.RegisterPickedRandomMission = function(missionName, pickedZone) if enabled == false then return end if tables.random_missions == nil then tables.random_missions = {} end tables.random_missions[string.lower(missionName)] = pickedZone Persistence._updateRequired = true end Persistence.GetPickedRandomMission = function(missionName) if enabled == false then return nil end return tables.random_missions[string.lower(missionName)] end Persistence.SetZoneDeliveredKilos = function(zoneName, kilos) if enabled == false then return end if tables.deliveredKilos == nil then tables.deliveredKilos = {} end tables.deliveredKilos[zoneName] = kilos Persistence._updateRequired = true end Persistence.GetZoneDeliveredKilos = function(zoneName) if enabled == false then return 0 end if tables.deliveredKilos == nil then tables.deliveredKilos = {} end return tables.deliveredKilos[zoneName] or 0 end Persistence.UnitState = function(unitName) if Persistence.isEnabled() == false then return nil end local entry = tables.unitsStates[unitName] if entry then return entry else local state = { isDead = false } return state end end Persistence.UnitKilled = function (name, position, heading, type) if enabled == false then return end logger:debug("Unit killed: " .. name .. ".. => persistenting") tables.unitsStates[name] = { isDead = true, pos = position, heading = heading, type = type, isCleaned = false } Persistence._updateRequired = true end end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.persistence then ScriptGlobals.classes.persistence = {} end ScriptGlobals.classes.persistence.persistence = Persistence end -- classes.persistence.persistence do -- classes.stageclasses.groups.spearheadsceneryobject local Persistence = ScriptGlobals.classes.persistence.persistence local SpearheadSceneryObject = {} SpearheadSceneryObject.__index = SpearheadSceneryObject function SpearheadSceneryObject.New(objectID) local self = setmetatable({}, SpearheadSceneryObject) if objectID == nil then return nil end self.persistentName = "SpearheadSceneryObject_" .. objectID self.objectID = objectID self.isDead = false self.internalObj = { ["id_"] = objectID } return self end function SpearheadSceneryObject:IsAlive() if Object.isExist(self.internalObj) == false then self:MarkDead() return false end if SceneryObject.getLife(self.internalObj) <= 0.10 then self:MarkDead() return false end return true end function SpearheadSceneryObject:MarkDead() if self.isDead == true then return end self.isDead = true Persistence.UnitKilled(self.persistentName, self:GetPoint(), 0, "Scenery") end function SpearheadSceneryObject:UpdateStatePersistently() if self.isDead == true then return end local state = Persistence.UnitState(self.persistentName) if state and state.isDead == true then trigger.action.explosion(self:GetPoint(), 1000) self.isDead = true end end function SpearheadSceneryObject:GetPersistentName() return self.persistentName end function SpearheadSceneryObject:GetPoint() return Object.getPoint(self.internalObj) end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.stageclasses then ScriptGlobals.classes.stageclasses = {} end if not ScriptGlobals.classes.stageclasses.groups then ScriptGlobals.classes.stageclasses.groups = {} end ScriptGlobals.classes.stageclasses.groups.spearheadsceneryobject = SpearheadSceneryObject end -- classes.stageclasses.groups.spearheadsceneryobject do -- classes.util.dcsutil local Util = ScriptGlobals.classes.util.util local SpearheadSceneryObject = ScriptGlobals.classes.stageclasses.groups.spearheadsceneryobject local DCS_UTIL = {} do do DCS_UTIL.__trigger_zones = {} end DCS_UTIL.Coalition = { NEUTRAL = 0, RED = 1, BLUE = 2 } DCS_UTIL.ZoneType = { Cilinder = 0, Polygon = 2 } DCS_UTIL.GroupCategory = { AIRPLANE = 0, HELICOPTER = 1, GROUND = 2, SHIP = 3, TRAIN = 4, STATIC = 5 --CUSTOM CATEGORY } DCS_UTIL.__airbaseNamesById = {} DCS_UTIL.__airbaseZonesByName = {} DCS_UTIL.__airportsStartingCoalition = {} DCS_UTIL.__warehouseStartingCoalition = {} function DCS_UTIL.__INIT() do do for _, trigger_zone in pairs(env.mission.triggers.zones) do local verts = {} if Util.tableLength(trigger_zone.verticies) >= 4 then table.insert(verts, { x = trigger_zone.verticies[4].x, y = trigger_zone.verticies[4].y }) table.insert(verts, { x = trigger_zone.verticies[3].x, y = trigger_zone.verticies[3].y }) table.insert(verts, { x = trigger_zone.verticies[2].x, y = trigger_zone.verticies[2].y }) table.insert(verts, { x = trigger_zone.verticies[1].x, y = trigger_zone.verticies[1].y }) end local zoneType = "Cilinder" if trigger_zone.type == DCS_UTIL.ZoneType.Polygon then zoneType = "Polygon" end local zone = { name = trigger_zone.name, zone_type = zoneType, location = { x = trigger_zone.x, y = trigger_zone.y }, radius = trigger_zone.radius, verts = verts, properties = {} } if trigger_zone.properties then for _, kvPair in pairs(trigger_zone.properties) do local key = kvPair.key local value = kvPair.value zone.properties[#zone.properties + 1] = { key = key, value = value } end end DCS_UTIL.__trigger_zones[zone.name] = zone end end do if env.warehouses.airports then env.warehouses.airports = env.warehouses.airports for warehouse_id, value in pairs(env.warehouses.airports) do if warehouse_id ~= nil then warehouse_id = tostring(warehouse_id) or "nil" local coalitionNumber = DCS_UTIL.stringToCoalition(value.coalition) DCS_UTIL.__airportsStartingCoalition[warehouse_id] = coalitionNumber end end end if env.warehouses.warehouses then env.warehouses.warehouses = env.warehouses.warehouses for warehouse_id, value in pairs(env.warehouses.warehouses) do if warehouse_id ~= nil then local warehouse_id_str = tostring(warehouse_id) or "nil" local coalitionNumber = DCS_UTIL.stringToCoalition(value.coalition) DCS_UTIL.__warehouseStartingCoalition[warehouse_id_str] = coalitionNumber end end end end do local airbases = world.getAirbases() if airbases then for _, airbase in pairs(airbases) do local name = airbase:getName() airbase:autoCapture(false) DCS_UTIL.__airbaseNamesById[tostring(airbase:getID())] = name if name then local relevantPoints = {} for _, x in pairs(airbase:getRunways()) do if x.position and x.position.x and x.position.z then table.insert(relevantPoints, { x = x.position.x, y = x.position.z }) end end for _, x in pairs(airbase:getParking()) do if x.vTerminalPos and x.vTerminalPos.x and x.vTerminalPos.z then table.insert(relevantPoints, { x = x.vTerminalPos.x, y = x.vTerminalPos.z }) end end local points = Util.getConvexHull(relevantPoints) local enlargedPoints = Util.enlargeConvexHull(points, 750) local triggerZone = { name = name, location = { x = airbase:getPoint().x, y = airbase:getPoint().z }, zone_type = "Polygon", radius = 0, verts = enlargedPoints } DCS_UTIL.__airbaseZonesByName[name] = triggerZone end end end end end end function DCS_UTIL.stringToCoalition(input) input = string.lower(input) if input == 'neutrals' or input == "neutral" or input == "0" then return DCS_UTIL.Coalition.NEUTRAL end if input == 'red' or input == "1" then return DCS_UTIL.Coalition.RED end if input == 'blue' or input == "2" then return DCS_UTIL.Coalition.BLUE end return -1 end function DCS_UTIL.DestroyUnit(unitName) local unit = Unit.getByName(unitName) if unit and unit:isExist() then unit:destroy() end end function DCS_UTIL.getUnitsInZones(unit_names, zone_names) local units = {} local zones = {} for k = 1, #unit_names do local unit = Unit.getByName(unit_names[k]) or StaticObject.getByName(unit_names[k]) if unit and unit:isExist() == true then units[#units + 1] = unit end end for _, zone_name in pairs(zone_names) do local zone = DCS_UTIL.__trigger_zones[zone_name] if zone then zones[#zones + 1] = zone end end local in_zone_units = {} for units_ind = 1, #units do local lUnit = units[units_ind] local isActive = true local lCat = Object.getCategory(lUnit) if lCat == Object.Category.UNIT then local unit = lUnit isActive = unit:isActive() == true end local unit_pos = lUnit:getPosition().p for _, zone in pairs(zones) do if unit_pos and isActive == true then local isInZone = Util.is3dPointInZone(unit_pos, zone) if isInZone == true then in_zone_units[#in_zone_units + 1] = { unit = lUnit, zone_name = zone.name } end end end end return in_zone_units end function DCS_UTIL.getGroupsInZone(group_names, zone_name) local zone = DCS_UTIL.__trigger_zones[zone_name] if zone == nil then return {} end return DCS_UTIL.areGroupsInCustomZone(group_names, zone) end function DCS_UTIL.areGroupsInCustomZone(group_names, zone) local units = {} if Util.tableLength(group_names) < 1 then return {} end for k = 1, #group_names do local entry local group = Group.getByName(group_names[k]) if group ~= nil then entry = { unit = group:getUnit(1), groupname = group_names[k] } else entry = { unit = StaticObject.getByName(group_names[k]), groupname = group_names[k] } end if entry and entry.unit and entry.unit:isExist() == true then units[#units + 1] = { groupname = entry.groupname, unit = entry.unit } end end local result_groups = {} for _, entry in pairs(units) do local pos = entry.unit:getPoint() local isInZone = Util.is3dPointInZone(pos, zone) if isInZone == true then table.insert(result_groups, entry.groupname) end end return result_groups end function DCS_UTIL.isPositionInZones(x, z, zone_names) local zones = {} for _, zone_name in pairs(zone_names) do local zone = DCS_UTIL.__trigger_zones[zone_name] if zone then zones[#zones + 1] = zone end end local result_zones = {} for _, zone in pairs(zones) do if Util.is3dPointInZone({ x = x, z = z, y = 0 }, zone) == true then result_zones[#result_zones + 1] = zone.name end end return result_zones end function DCS_UTIL.isPositionInZone(x, z, zone_name) local zone = DCS_UTIL.__trigger_zones[zone_name] if Util.is3dPointInZone({ x = x, y = 0, z = z }, zone) then return true end return false end function DCS_UTIL.isZoneInZone(zone_name, parent_zone_name) local zoneA = DCS_UTIL.__trigger_zones[zone_name] if zoneA == nil then return false end local zoneB = DCS_UTIL.__trigger_zones[parent_zone_name] if zoneB == nil then return false end return Util.is3dPointInZone({ x = zoneA.location.x, y = 0, z = zoneA.location.y }, zoneB) end function DCS_UTIL.getZoneByName(zone_name) if zone_name == nil then return nil end return DCS_UTIL.__trigger_zones[zone_name] end function DCS_UTIL.getAirbaseZoneByName(airbaseName) if airbaseName == nil then return nil end return DCS_UTIL.__airbaseZonesByName[airbaseName] end function DCS_UTIL.stringToGroupCategory(input) input = string.lower(input) if input == 'airplane' or input == 'plane' then return DCS_UTIL.GroupCategory.AIRPLANE end if input == 'helicopter' then return DCS_UTIL.GroupCategory.HELICOPTER end if input == 'ground' or input == 'vehicle' then return DCS_UTIL.GroupCategory.GROUND end if input == 'ship' then return DCS_UTIL.GroupCategory.SHIP end if input == 'train' then return DCS_UTIL.GroupCategory.TRAIN end if input == "static" then return DCS_UTIL.GroupCategory.STATIC end return nil; end function DCS_UTIL.getAllGroupCategoryNames() return { "airplane", "helicopter", "ground", "ship", "train", "static" } end local config = { ["ah-64d_blk_ii"] = "MGRS", ["fa-18c_hornet"] = "DMS", ["av8bna"] = "DMS", ["f-14b"] = "DMS", ["f-14a-135-gr"] = "DMS" } function DCS_UTIL.convertVec2ToUnitUsableType(location, unitType) local height = land.getHeight(location) local vec3 = { x = location.x, y = height, z = location.y } unitType = string.lower(unitType or "") local conversionType = config[unitType] if not conversionType then conversionType = "DDM" end return DCS_UTIL.convertToDisplayCoord(vec3, conversionType) end function DCS_UTIL.convertToDisplayCoord(location, coordType) local lattitude, longitude, altitude = coord.LOtoLL(location) if coordType == "MGRS" then local mgrs = coord.LLtoMGRS(lattitude, longitude) return string.format("%s %s %s %s", mgrs.UTMZone, mgrs.MGRSDigraph, mgrs.Easting, mgrs.Northing) end local function dd_to_ddm(dd) local degrees = math.floor(math.abs(dd)) local minutes = (math.abs(dd) - degrees) * 60 local sign = dd >= 0 and 1 or -1 return degrees * sign, minutes end local lat_deg, lat_min = dd_to_ddm(lattitude) local lon_deg, lon_min = dd_to_ddm(longitude) local lat_hemisphere = lattitude >= 0 and "N" or "S" local lon_hemisphere = longitude >= 0 and "E" or "W" if coordType == "DDM" then return string.format("%s%02d°%06.3f' %s%03d°%06.3f' %dft", lat_hemisphere, math.abs(lat_deg), lat_min, lon_hemisphere, math.abs(lon_deg), lon_min, altitude * 3,28084) end if coordType == "DMS" then local lat_min_display = math.floor(lat_min) local lon_min_display = math.floor(lon_min) local lat_sec_display = math.floor((lat_min - lat_min_display) * 60) local lon_sec_display = math.floor((lon_min - lon_min_display) * 60) return string.format("%s%02d°%02d'%02d %s%03d°%02d'%02d %dft", lat_hemisphere, math.abs(lat_deg), lat_min_display, lat_sec_display, lon_hemisphere, math.abs(lon_deg), lon_min_display, lon_sec_display, altitude * 3,28084) end end function DCS_UTIL.getSceneryObjectsInZone(zone) local volume if(zone.zone_type == "Cilinder") then local y = land.getHeight({ x = zone.location.x, y = zone.location.y }) local sphere = { id = world.VolumeType.SPHERE, params = { point = { x = zone.location.x, y = y, z = zone.location.y }, radius = zone.radius } } volume = sphere else local minX = nil local maxX = nil local minZ = nil local maxZ = nil for _, point in pairs(zone.verts) do if minX == nil or point.x < minX then minX = point.x end if maxX == nil or point.x > maxX then maxX = point.x end if minZ == nil or point.y < minZ then minZ = point.y end if maxZ == nil or point.y > maxZ then maxZ = point.y end end if(minX == nil or maxX == nil or minZ == nil or maxZ == nil) then return {} end local min = { x = minX, y = land.getHeight({ x = minX, y = minZ }) - 100, z = minZ } local max = { x = maxX, y = land.getHeight({ x = maxX, y = maxZ }) + 500, z = maxZ } local box = { id = world.VolumeType.BOX, params = { min = min, max = max } } volume = box end local sceneryObjects = {} local onFound = function(object) if object and object:isExist() and object:hasAttribute("Buildings") then local obj = SpearheadSceneryObject.New(object["id_"]) table.insert(sceneryObjects, obj) end end world.searchObjects(Object.Category.SCENERY, volume, onFound) return sceneryObjects end function DCS_UTIL.getUnitTypeFromGroup(group) for _, unit in pairs(group:getUnits()) do if unit and unit:isExist() then return unit:getTypeName() end end end function DCS_UTIL.getAllPlayerUnits() local units = {} for i = 0, 2 do local players = coalition.getPlayers(i) for _, unit in pairs(players) do units[#units + 1] = unit end end return units end function DCS_UTIL.getAirbaseName(baseId) local stringified = tostring(baseId) return DCS_UTIL.__airbaseNamesById[stringified] end function DCS_UTIL.getAirbaseById(baseId) local name = DCS_UTIL.getAirbaseName(baseId) if name == nil then return nil end return Airbase.getByName(name) end function DCS_UTIL.getStartingCoalition(airbase) if airbase == nil then return nil end local baseId = tostring(airbase:getID()) local result = DCS_UTIL.__airportsStartingCoalition[baseId] if result == nil then result = DCS_UTIL.__warehouseStartingCoalition[baseId] end return result end function DCS_UTIL.CleanCorpse(unitName) unitName = "dead_" .. unitName local object = StaticObject.getByName(unitName) if object then object:destroy() end end local __drawID = 4210 function DCS_UTIL.GetNextDrawID() __drawID = __drawID + 1 return __drawID end function DCS_UTIL.AddMarkToGroup(groupID, text, location) local nextId = DCS_UTIL.GetNextDrawID() trigger.action.markToGroup(nextId, text, location, groupID, true, nil) return nextId end function DCS_UTIL.AddMarkToAll(text, location) local nextId = DCS_UTIL.GetNextDrawID() trigger.action.markToAll(nextId, text, location, true, nil) return nextId end function DCS_UTIL.RemoveMark(markId) if markId ~= nil then trigger.action.removeMark(markId) end end function DCS_UTIL.SetLineColor(drawID, lineColor) local lineColorMapped = { lineColor.r or 0, lineColor.g or 0, lineColor.b or 0, lineColor.a or 1 } trigger.action.setMarkupColor(drawID, lineColorMapped) end function DCS_UTIL.SetFillColor(drawID, fillColor) if fillColor == nil then return end local lineColorMapped = { fillColor.r or 0, fillColor.g or 0, fillColor.b or 0, fillColor.a or 1 } trigger.action.setMarkupColorFill(drawID, lineColorMapped) end function DCS_UTIL.RemoveZoneDraw(drawID) if drawID ~= nil then trigger.action.removeMark(drawID) end end function DCS_UTIL.GetNeutralCountry() for _, id in pairs(country.id) do if coalition.getCountryCoalition(id) == DCS_UTIL.Coalition.NEUTRAL then return id end end end function DCS_UTIL.NeedsRTBInTen(groupName, fuelOffset) local isBingo = DCS_UTIL.IsBingoFuel(groupName, fuelOffset) if isBingo then return true end local aliveUnits = 0 local group = Group.getByName(groupName) if group then for _ , unit in pairs(group:getUnits()) do if unit and unit:isExist() == true and unit:inAir() == true then aliveUnits = aliveUnits + 1 end end if aliveUnits / group:getInitialSize() <= 0.5 then return true end end return false end function DCS_UTIL.IsBingoFuel(groupName, offset) if offset == nil then offset = 0 end local bingoSetting = 0.20 bingoSetting = bingoSetting + offset local group = Group.getByName(groupName) if group then for _, unit in pairs(group:getUnits()) do if unit and unit:isExist() == true and unit:inAir() == true and unit:getFuel() < bingoSetting then return true end end end return false end function DCS_UTIL.GetPlayerGroupByGroupID(groupId) for i = 0, 2 do local players = coalition.getPlayers(i) for _, unit in pairs(players) do if unit and unit:isExist() == true then local group = unit:getGroup() if group and group:getID() == groupId then return group end end end end end function DCS_UTIL.GetPlayerUnitByID(unitID) for i = 0, 2 do local players = coalition.getPlayers(i) for _, unit in pairs(players) do if unit and unit:getID() == unitID then return unit end end end end DCS_UTIL.__INIT(); end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.util then ScriptGlobals.classes.util = {} end ScriptGlobals.classes.util.dcsutil = DCS_UTIL end -- classes.util.dcsutil do -- classes.util.missioneditorwarnings local MissionEditingWarnings = {} MissionEditingWarnings.warnings = {} function MissionEditingWarnings.Add(warningMessage) table.insert(MissionEditingWarnings.warnings, warningMessage or "skip") end function MissionEditingWarnings.WriteAll(logger) if not logger then return end if not MissionEditingWarnings.warnings or #MissionEditingWarnings.warnings == 0 then return end logger:warn("Mission Editor Warnings:") for _, warning in ipairs(MissionEditingWarnings.warnings) do logger:warn("- " .. warning) end end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.util then ScriptGlobals.classes.util = {} end ScriptGlobals.classes.util.missioneditorwarnings = MissionEditingWarnings end -- classes.util.missioneditorwarnings do -- classes.helpers.mizgroupsmanager local DcsUtil = ScriptGlobals.classes.util.dcsutil local MizGroupsManager = {} MizGroupsManager._groupNames = {} MizGroupsManager._spawnTemplateData = {} do local getCoalitionData = function(coalition_name) return env.mission.coalition[coalition_name] end local getGroupsData = function(country, key) if key == "planes" then return country.plane end if key == "helicopters" then return country.helicopter end if key == "ground" then return country.vehicle end if key == "ships" then return country.ship end if key == "statics" then return country.static end return nil end for _, coalition_name in ipairs({"neutral", "blue", "red"}) do local coalition_data = getCoalitionData(coalition_name) if coalition_data.country then for _, country_data in pairs(coalition_data.country) do for _, category_name in pairs(DcsUtil.getAllGroupCategoryNames()) do local category_id = DcsUtil.stringToGroupCategory(category_name) local groups_data = getGroupsData(country_data, category_name) if category_id ~= nil and type(groups_data) == "table" and groups_data.group ~= nil and type(groups_data.group) == "table" then for _, group in pairs(groups_data.group) do local name = group.name local skippable = false local isStatic = false if category_id == DcsUtil.GroupCategory.STATIC then isStatic = true local unit = group.units[1] if unit and unit.category == "Heliports" then skippable = true elseif unit and unit.name then name = unit.name else env.error("Group " .. name .. " has no units, skipping it.") skippable = true end end if skippable == false then MizGroupsManager._spawnTemplateData[name] = { isStatic = isStatic, country = country_data.id, category = category_id, groupTemplate = group } table.insert(MizGroupsManager._groupNames, name) end end end end end end end end function MizGroupsManager.getAllGroupNames() return MizGroupsManager._groupNames end function MizGroupsManager.IsGroupStatic(groupName) local spawnData = MizGroupsManager._spawnTemplateData[groupName] if spawnData then return spawnData.isStatic end return nil end function MizGroupsManager.getSpawnTemplateData(groupName) return MizGroupsManager._spawnTemplateData[groupName] end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.helpers then ScriptGlobals.classes.helpers = {} end ScriptGlobals.classes.helpers.mizgroupsmanager = MizGroupsManager end -- classes.helpers.mizgroupsmanager do -- classes.stageclasses.drawings.helper.drawinghelper local DcsUtil = ScriptGlobals.classes.util.dcsutil local Logger = ScriptGlobals.classes.util.logger local GlobalConfig = ScriptGlobals.classes.configuration.globalconfig local level = "INFO" if GlobalConfig.New():isDebugEnabled() then level = "DEBUG" end local logger = Logger.new("DrawingHelper", level) local DrawingHelper = {} DrawingHelper.__index = DrawingHelper function DrawingHelper.Draw(object) if object == nil then if logger then logger:warn("DrawingHelper.Draw called with nil object") end return {} end if(object.primitiveType == "Polygon") then return DrawingHelper.DrawPolygon(object) elseif(object.primitiveType == "Line") then return DrawingHelper.DrawLine(object) elseif(object.primitiveType == "TextBox") then return DrawingHelper.DrawTextBox(object) else logger:warn("Unknown primitive type: " .. tostring(object.primitiveType)) end return {} end function DrawingHelper.GetAndAddId() return DcsUtil.GetNextDrawID() end local function MarkupToAll(shapeID, drawID, points, fillColor, lineColor, lineStyle, lineThickness) if lineThickness == nil or lineThickness <= 0 then lineStyle = 0 end local functionString = "trigger.action.markupToAll(" .. shapeID .. ", -1, " .. drawID .. "," for _, point in pairs(points) do functionString = functionString .. " { x=" .. point.x .. ", y=0,z=" .. point.z .. "}," end functionString = functionString .. "{0,1,0,1}, {0,1,0,1}, " .. lineStyle .. ")" logger:debug("Drawing complex drawing with ID " .. tostring(drawID) .. " and function string: " .. functionString) local f, err = loadstring(functionString) if f then f() else logger:error("Something failed when drawing complex drawing" .. err) end if fillColor then trigger.action.setMarkupColorFill(drawID, fillColor) end trigger.action.setMarkupColor(drawID, lineColor) trigger.action.setMarkupTypeLine(drawID, lineStyle) end function DrawingHelper.DrawPolygon(object) if object == nil then return {} end local id = DrawingHelper.GetAndAddId() local function DrawCircle(circle) local vec3 = { x = circle.mapX, y = 0, z = circle.mapY } local fillColor = DrawingHelper.ColorToColorTable(circle.fillColorString) local colorString = DrawingHelper.ColorToColorTable(circle.colorString) local style = DrawingHelper.ToLineStyleInteger(circle.style) trigger.action.circleToAll(-1, id, vec3, circle.radius, colorString, fillColor, style, true) end local function DrawOval(oval) local points = {} local pointsNo = 30 local angleStep = (2 * math.pi) / pointsNo local fillColor = DrawingHelper.ColorToColorTable(oval.fillColorString) local color = DrawingHelper.ColorToColorTable(oval.colorString) local lineStyle = DrawingHelper.ToLineStyleInteger(oval.style) for i = 1, pointsNo do local angle = i * angleStep local x = oval.mapX + (oval.r1 * math.cos(angle)) local y = oval.mapY + (oval.r2 * math.sin(angle)) table.insert(points, { x = x, y = 0, z = y } ) end MarkupToAll(7, id, points, fillColor, color, lineStyle, oval.thickness) end local function DrawFree(free) local fillColor = DrawingHelper.ColorToColorTable(free.fillColorString) local color = DrawingHelper.ColorToColorTable(free.colorString) local lineStyle = DrawingHelper.ToLineStyleInteger(free.style) local keys = {} for k, _ in pairs(free.points) do table.insert(keys, k) end table.sort(keys, function(a, b) return a < b end) local points = {} for _, k in ipairs(keys) do local point = free.points[k] local newPoint = { x = free.mapX + point.x, y = 0, z = free.mapY + point.y } local firstPoint = points[1] if firstPoint == nil or newPoint.x ~= firstPoint.x or newPoint.z ~= firstPoint.z then table.insert(points, newPoint) end end MarkupToAll(7, id, points, fillColor, color, lineStyle, free.thickness) end local function DrawRect(rect) local fillColor = DrawingHelper.ColorToColorTable(rect.fillColorString) local color = DrawingHelper.ColorToColorTable(rect.colorString) local lineStyle = DrawingHelper.ToLineStyleInteger(rect.style) local pointA = { x = rect.mapX, y = 0, z = rect.mapY } local pointB = { x = rect.mapX + rect.width, y = 0, z = rect.mapY + rect.height } trigger.action.rectToAll(-1, id, pointA, pointB, color, fillColor, lineStyle, true) end local function DrawArrow(arrow) local fillColor = DrawingHelper.ColorToColorTable(arrow.fillColorString) local color = DrawingHelper.ColorToColorTable(arrow.colorString) local lineStyle = DrawingHelper.ToLineStyleInteger(arrow.style) logger:debug("Drawing arrow with start point: " .. tostring(arrow.mapX) .. ", " .. tostring(arrow.mapY) .. " and angle: " .. tostring(arrow.angle) .. " and length: " .. tostring(arrow.length)) local endPoint = { x = arrow.mapX, y = 0, z = arrow.mapY } local rad = math.rad(arrow.angle or 0) local length = arrow.length or 100 local startPoint = { x = arrow.mapX - length * math.sin(rad), y = 0, z = arrow.mapY + length * math.cos(rad) } trigger.action.arrowToAll(-1, id, startPoint, endPoint, color, fillColor, lineStyle, true) end if logger then logger:debug("Drawing polygon with ID " .. tostring(id) .. " and polygon mode: " .. tostring(object.polygonMode)) end if object.polygonMode == "circle" then DrawCircle(object) elseif object.polygonMode == "oval" then DrawOval(object) elseif object.polygonMode == "free" then DrawFree(object) elseif object.polygonMode == "rect" then DrawRect(object) elseif object.polygonMode == "arrow" then DrawArrow(object) end return {id} end function DrawingHelper.DrawLine(object) local points = {} local ids = {} for _, point in ipairs(object.points) do table.insert(points, { x = object.mapX + point.x, y = 0, z = object.mapY + point.y } ) end local color = DrawingHelper.ColorToColorTable(object.colorString) local lineStyle = DrawingHelper.ToLineStyleInteger(object.style) for i = 1, #points - 1 do local id = DrawingHelper.GetAndAddId() trigger.action.lineToAll(-1, id, points[i], points[i + 1], color, lineStyle, true) table.insert(ids, id) end return ids end function DrawingHelper.DrawTextBox(object) local id = DrawingHelper.GetAndAddId() trigger.action.textToAll(-1, id, { x= object.mapX, y = 0, z = object.mapY }, DrawingHelper.ColorToColorTable(object.colorString), DrawingHelper.ColorToColorTable(object.fillColorString), object.fontSize or 12, true, object.text or "") return {id} end function DrawingHelper.Remove(id) trigger.action.removeMark(id) end function DrawingHelper.ColorToColorTable(hexStr) if hexStr == nil then logger:warn("ColorToColorTable called with nil hexStr, returning default color {0, 0, 0, 0}") return { 0, 0, 0, 0 } end hexStr = hexStr:gsub("0x", "") local r = tonumber(hexStr:sub(1, 2), 16) / 255 local g = tonumber(hexStr:sub(3, 4), 16) / 255 local b = tonumber(hexStr:sub(5, 6), 16) / 255 local a = tonumber(hexStr:sub(7, 8), 16) / 255 return { r, g , b , a } end function DrawingHelper.ColorTableToColorString(rgba) if rgba == nil or #rgba < 4 or rgba[1] == nil or rgba[2] == nil or rgba[3] == nil or rgba[4] == nil then logger:warn("ColorTableToColorString called with invalid rgba table, returning default color string '0x00000000'") return "0x00000000" end local r = string.format("%02X", math.floor(rgba[1] * 255)) local g = string.format("%02X", math.floor(rgba[2] * 255)) local b = string.format("%02X", math.floor(rgba[3] * 255)) local a = string.format("%02X", math.floor(rgba[4] * 255)) return "0x" .. r .. g .. b .. a end function DrawingHelper.ToLineStyleInteger(lineStyle) lineStyle = lineStyle:lower() if lineStyle == "no line" then return 0 elseif lineStyle == "solid" then return 1 elseif lineStyle == "dashed" then return 2 elseif lineStyle == "dotted" then return 3 elseif lineStyle == "dotdash" then return 4 elseif lineStyle == "longdash" then return 5 elseif lineStyle == "twodash" then return 6 else return 0 end end function DrawingHelper.ToLineStyleString(lineStyle) if lineStyle == 0 then return "no line" elseif lineStyle == 1 then return "solid" elseif lineStyle == 2 then return "dashed" elseif lineStyle == 3 then return "dotted" elseif lineStyle == 4 then return "dotdash" elseif lineStyle == 5 then return "longdash" elseif lineStyle == 6 then return "twodash" else return "no line" end end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.stageclasses then ScriptGlobals.classes.stageclasses = {} end if not ScriptGlobals.classes.stageclasses.drawings then ScriptGlobals.classes.stageclasses.drawings = {} end if not ScriptGlobals.classes.stageclasses.drawings.helper then ScriptGlobals.classes.stageclasses.drawings.helper = {} end ScriptGlobals.classes.stageclasses.drawings.helper.drawinghelper = DrawingHelper end -- classes.stageclasses.drawings.helper.drawinghelper do -- classes.stageclasses.drawings.customdrawing local DrawingHelper = ScriptGlobals.classes.stageclasses.drawings.helper.drawinghelper local Logger = ScriptGlobals.classes.util.logger local drawingLogger = Logger.new("CustomDrawing") local CustomDrawing = {} CustomDrawing.__index = CustomDrawing function CustomDrawing.New(drawingObject) local self = setmetatable({}, CustomDrawing) self._drawingObject = drawingObject self._IDs = {} self._name = drawingObject.name return self end function CustomDrawing.FromPoints(points, colorString, lineStyle, lineThickness) if points == nil or #points < 3 then drawingLogger:warn("CustomDrawing.FromPoints called with nil or less than 3 points") return nil end local drawingObject = { mapX = 0, mapY = 0, points = points, lineMode = "free", name = "custom_drawing_" .. tostring(math.random(1000000)), primitiveType = "Polygon", polygonMode = "free", visible = true, style = DrawingHelper.ToLineStyleString(lineStyle), colorString = colorString, thickness = lineThickness, closed = false } return CustomDrawing.New(drawingObject) end function CustomDrawing.FromZone(zone, colorString, fillColorString, lineStyle, lineThickness) if zone == nil then drawingLogger:warn("CustomDrawing.FromZone called with nil zone") return nil end if zone.zone_type == "Cilinder" then local drawingObject = { mapX = zone.location.x, mapY = zone.location.y, radius = zone.radius, name = zone.name .. "_drawing", primitiveType = "Polygon", polygonMode = "circle", visible = true, style = DrawingHelper.ToLineStyleString(lineStyle), colorString = colorString, fillColorString = fillColorString, thickness = lineThickness, } return CustomDrawing.New(drawingObject) end if zone.zone_type == "Polygon" then local drawingObject = { mapX = 0, mapY = 0, points = zone.verts, name = zone.name .. "_drawing", primitiveType = "Polygon", polygonMode = "free", visible = true, style = DrawingHelper.ToLineStyleString(lineStyle), colorString = colorString, fillColorString = fillColorString, thickness = lineThickness, } return CustomDrawing.New(drawingObject) end end function CustomDrawing:GetName() return self._drawingObject.name end function CustomDrawing:Draw() drawingLogger:debug("Drawing custom drawing with IDs " .. table.concat(self._IDs, ", ")) if self._IDs ~= nil then for _, id in ipairs(self._IDs) do DrawingHelper.Remove(id) end end self._IDs = DrawingHelper.Draw(self._drawingObject) end function CustomDrawing:Remove() if self._IDs ~= nil then for _, id in ipairs(self._IDs) do drawingLogger:debug("Removing custom drawing with ID " .. tostring(id)) DrawingHelper.Remove(id) end self._IDs = {} end end function CustomDrawing:UpdateDrawingObject(updateFunc) self._drawingObject = updateFunc(self._drawingObject) end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.stageclasses then ScriptGlobals.classes.stageclasses = {} end if not ScriptGlobals.classes.stageclasses.drawings then ScriptGlobals.classes.stageclasses.drawings = {} end ScriptGlobals.classes.stageclasses.drawings.customdrawing = CustomDrawing end -- classes.stageclasses.drawings.customdrawing do -- classes.stageclasses.drawings.stagedrawing local CustomDrawing = ScriptGlobals.classes.stageclasses.drawings.customdrawing local Util = ScriptGlobals.classes.util.util local MissionEditorWarnings = ScriptGlobals.classes.util.missioneditorwarnings local StageDrawing = {} StageDrawing.__index = StageDrawing setmetatable(StageDrawing, CustomDrawing) function StageDrawing.New(drawingObject) local super = CustomDrawing.New(drawingObject) if not super then return nil end local self = setmetatable(super, StageDrawing) local split = Util.split_string(self._name or "", "_") local secondPart = split[2] or "1" local splitPart = Util.split_string(secondPart, ":") if tonumber(splitPart[1]) == nil then local laneIdentifier = string.sub(splitPart[1], 1, 1) if tonumber(laneIdentifier) == nil then self._stageLaneIdentifier = laneIdentifier end local startNumber = string.sub(splitPart[1], 2) local startNumberVal = tonumber(startNumber) if startNumberVal == nil then MissionEditorWarnings.Add("Start number for " .. self._name .. " is not a valid number: " .. tostring(startNumber)) return nil end self._startingStage = startNumberVal else local startNumber = tonumber(splitPart[1]) if startNumber == nil then MissionEditorWarnings.Add("Start number for " .. self._name .. " is not a valid number: " .. tostring(splitPart[1])) return nil end self._startingStage = startNumber end if tonumber(splitPart[2]) == nil then local laneIdentifier = string.sub(splitPart[2], 1, 1) if tonumber(laneIdentifier) == nil then if self._stageLaneIdentifier and self._stageLaneIdentifier ~= laneIdentifier then MissionEditorWarnings.Add("Lane identifiers do not match for " .. self._name .. ": " .. self._stageLaneIdentifier .. " vs " .. laneIdentifier .. ". Will only use stage " .. self._stageLaneIdentifier) end end local stopNumber = string.sub(splitPart[2], 2) local stopNumberVal = tonumber(stopNumber) if stopNumberVal == nil then MissionEditorWarnings.Add("Stop number for " .. self._name .. " is not a valid number: " .. tostring(stopNumber)) return nil end self._removeAtStage = stopNumberVal else local stopNumber = tonumber(splitPart[2]) if stopNumber == nil then MissionEditorWarnings.Add("Stop number for " .. self._name .. " is not a valid number: " .. tostring(splitPart[2])) return nil end self._removeAtStage = stopNumber end return self end function StageDrawing:GetStageLaneIdentifier() return self._stageLaneIdentifier end function StageDrawing:GetStartingStage() return self._startingStage end function StageDrawing:GetRemoveAtStage() return self._removeAtStage end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.stageclasses then ScriptGlobals.classes.stageclasses = {} end if not ScriptGlobals.classes.stageclasses.drawings then ScriptGlobals.classes.stageclasses.drawings = {} end ScriptGlobals.classes.stageclasses.drawings.stagedrawing = StageDrawing end -- classes.stageclasses.drawings.stagedrawing do -- classes.spearhead_db local Util = ScriptGlobals.classes.util.util local DcsUtil = ScriptGlobals.classes.util.dcsutil local MissionEditorWarnings = ScriptGlobals.classes.util.missioneditorwarnings local MizGroupsManager = ScriptGlobals.classes.helpers.mizgroupsmanager local StageDrawing = ScriptGlobals.classes.stageclasses.drawings.stagedrawing local Database = {} function Database.New(Logger) local tables = { AllZoneNames = {}, BlueSams = {}, AllCapRoutes = {}, capZonesByCapZoneID = {}, AllInterceptZones = {}, AllSceneryObjects = {}, interceptZonesByZoneID = {}, CarrierRouteZones = {}, MissionZones = {}, MissionZonesLocations = {}, StageZoneNames = {}, RandomMissionZones = {}, StageZones = {}, StageZonesByNumber = {}, AllFarpZones = {}, AirbaseDataPerAirfield = {}, BlueSamDataPerZone = {}, MissionZoneData = {}, FarpZoneData = {}, missionCodes = {}, SupplyHubZones = {}, StageDrawings = {} } Database.__index = Database local self = setmetatable({}, Database) self._logger = Logger self._tables = tables self._logger:debug("Initiating tables") do for _, zone_data in pairs(DcsUtil.__trigger_zones) do local zone_name = zone_data.name local zoneLocation = { x = zone_data.location.x, y = zone_data.location.y } local split_string = Util.split_string(zone_name, "_") table.insert(self._tables.AllZoneNames, zone_name) if string.lower(split_string[1]) == "missionstage" then table.insert(self._tables.StageZoneNames, zone_name) if split_string[2] then local stringified = tostring(split_string[2]) or "unknown" if self._tables.StageZonesByNumber[stringified] == nil then self._tables.StageZonesByNumber[stringified] = {} end table.insert(self._tables.StageZonesByNumber[stringified], zone_name) local stageData = { StageZoneName = zone_name, StageIndex = stringified, AirbaseNames = {}, BlueSamZones = {}, FarpZones = {}, MissionZones = {}, RandomMissionZones = {}, MiscGroups = {}, SupplyHubZones = {}, SupplyHubZonesInFarp = {} } self._tables.StageZones[zone_name] = stageData end end local lowered = string.lower(split_string[1]) if lowered == "waitingstage" then table.insert(self._tables.StageZoneNames, zone_name) end if lowered == "mission" then table.insert(self._tables.MissionZones, zone_name) self._tables.MissionZonesLocations[zone_name] = zoneLocation end if lowered == "randommission" then table.insert(self._tables.RandomMissionZones, zone_name) self._tables.MissionZonesLocations[zone_name] = zoneLocation end if lowered == "farp" then table.insert(self._tables.AllFarpZones, zone_name) end if lowered == "caproute" then table.insert(self._tables.AllCapRoutes, zone_name) end if lowered == "interceptzone" then table.insert(self._tables.AllInterceptZones, zone_name) end if lowered == "carrierroute" then table.insert(self._tables.CarrierRouteZones, zone_name) end if lowered == "bluesam" then table.insert(self._tables.BlueSams, zone_name) end if lowered == "supplyhub" then table.insert(self._tables.SupplyHubZones, zone_name) end if lowered == "scenerytarget" or lowered == "scenerytargets" then local sceneryObjects = DcsUtil.getSceneryObjectsInZone(zone_data) for _, sceneryObject in pairs(sceneryObjects) do table.insert(self._tables.AllSceneryObjects, sceneryObject) end end end end self._logger:debug("initiated zone tables, continuing with descriptions") do if env.mission.drawings and env.mission.drawings.layers then for _, layer in pairs(env.mission.drawings.layers) do if string.lower(layer.name) == "author" then for _, layer_object in pairs(layer.objects) do if Util.startsWith(string.lower(layer_object.name), "buildable", true) == true then local airbaseData = self:getAirbaseDataForDrawLayer(layer_object) if airbaseData then self._logger:debug("found airbase data for " .. layer_object.name) if layer_object.primitiveType == "TextBox" then layer_object = layer_object local number = tonumber(layer_object.text) airbaseData.buildingKilos = number end end end end end end end end do if env.mission.drawings and env.mission.drawings.layers then for _, layer in pairs(env.mission.drawings.layers) do if string.lower(layer.name) == "author" then for _, layer_object in pairs(layer.objects) do if Util.startsWith(layer_object.name, "drawing_", true) then local object = layer_object local stageDrawing = StageDrawing.New(object) if stageDrawing then table.insert(self._tables.StageDrawings, stageDrawing) end end end end end end end local availableSupplyHubs = {} for _, supplyHubZoneName in pairs(self._tables.SupplyHubZones) do availableSupplyHubs[supplyHubZoneName] = true end for _, stageZoneName in pairs(self._tables.StageZoneNames) do local stageData = self._tables.StageZones[stageZoneName] if stageData then if env.mission.drawings and env.mission.drawings.layers then for _, layer in pairs(env.mission.drawings.layers) do if string.lower(layer.name) == "author" then for _, layer_object in pairs(layer.objects) do if Util.startsWith(string.lower(layer_object.name), "stagebriefing_", true) == true then local zone = DcsUtil.getZoneByName(stageZoneName) local vec2 = { x = layer_object.mapX, y = layer_object.mapY } if zone and Util.is2dPointInZone(vec2, zone) == true then if layer_object.primitiveType == "TextBox" then layer_object = layer_object local description = layer_object.text if description and description ~= "" then stageData.StageBriefing = description end end end end end end end end for _, blueSamStageName in pairs(self._tables.BlueSams) do if DcsUtil.isZoneInZone(blueSamStageName, stageZoneName) == true then table.insert(stageData.BlueSamZones, blueSamStageName) end end for _, farpZoneName in pairs(self._tables.AllFarpZones) do if DcsUtil.isZoneInZone(farpZoneName, stageZoneName) then table.insert(stageData.FarpZones, farpZoneName) for hubZoneName, available in pairs(availableSupplyHubs) do if available == true and DcsUtil.isZoneInZone(hubZoneName, farpZoneName) == true then local farpZoneData = self:getOrCreateFarpDataForZone(farpZoneName) if farpZoneData then table.insert(farpZoneData.supplyHubNames, hubZoneName) availableSupplyHubs[hubZoneName] = false end end end end end for _, airbase in pairs(world.getAirbases()) do local point = airbase:getPoint() if DcsUtil.isPositionInZone(point.x, point.z, stageZoneName) == true then if airbase:getDesc().category == 0 then table.insert(stageData.AirbaseNames, airbase:getName()) local airbaseZone = DcsUtil.getAirbaseZoneByName(airbase:getName()) for hubZoneName, available in pairs(availableSupplyHubs) do local zone = DcsUtil.getZoneByName(hubZoneName) if zone and airbaseZone then if available == true and Util.is2dPointInZone(zone.location, airbaseZone) == true then local airbaseData = self:getOrCreateAirbaseData(airbase:getName()) if airbaseData then table.insert(airbaseData.supplyHubNames, hubZoneName) availableSupplyHubs[hubZoneName] = false end end end end end end end for supplyHubZone, available in pairs(availableSupplyHubs) do if available == true and DcsUtil.isZoneInZone(supplyHubZone, stageZoneName) == true then table.insert(stageData.SupplyHubZones, supplyHubZone) end end for _, farpZoneName in pairs(stageData.FarpZones) do for _, supplyHubZone in pairs(self._tables.SupplyHubZones) do if DcsUtil.isZoneInZone(supplyHubZone, farpZoneName) == true then stageData.SupplyHubZonesInFarp[supplyHubZone] = farpZoneName end end end for _, missionZone in pairs(self._tables.MissionZones) do if DcsUtil.isZoneInZone(missionZone, stageZoneName) == true then table.insert(stageData.MissionZones, missionZone) end end for _, missionZone in pairs(self._tables.RandomMissionZones) do if DcsUtil.isZoneInZone(missionZone, stageZoneName) == true then table.insert(stageData.RandomMissionZones, missionZone) end end end end for _, farpZoneName in pairs(self._tables.AllFarpZones) do for _, airbase in pairs(world.getAirbases()) do if airbase:getDesc().category == Airbase.Category.HELIPAD then local name = airbase:getName() if self._tables.FarpZoneData[farpZoneName] == nil then self._tables.FarpZoneData[farpZoneName] = { groups = {}, padNames = {}, supplyHubNames = {} } end local position = airbase:getPoint() if DcsUtil.isPositionInZone(position.x, position.z, farpZoneName) == true then table.insert(self._tables.FarpZoneData[farpZoneName].padNames, name) end end end end self:initAvailableUnits() self:loadCapUnits() self:loadBlueSamUnits() self:loadMissionzoneUnits() self:loadRandomMissionzoneUnits() self:loadFarpData() self:loadAirbaseGroups() self:loadMiscGroupsInStages() for _, cap_route_zone in pairs(self._tables.AllCapRoutes) do local split = Util.split_string(cap_route_zone, "_") local zoneID = split[2] if zoneID then if tables.capZonesByCapZoneID[zoneID] == nil then tables.capZonesByCapZoneID[zoneID] = { zones = {}, current = 1 } end local zone = DcsUtil.getZoneByName(cap_route_zone) if zone then table.insert(tables.capZonesByCapZoneID[zoneID].zones, zone) end end end for _, interceptZone in pairs(self._tables.AllInterceptZones) do local split = Util.split_string(interceptZone, "_") local zoneID = split[2] if zoneID then if tables.interceptZonesByZoneID[zoneID] == nil then tables.interceptZonesByZoneID[zoneID] = {} end local zone = DcsUtil.getZoneByName(interceptZone) if zone then table.insert(tables.interceptZonesByZoneID[zoneID], zone) end end end local totalUnits = 0 local missions = 0 for _, data in pairs(self._tables.MissionZoneData) do missions = missions + 1 for _, groupName in pairs(data.RedGroups) do local group = Group.getByName(groupName) if group then totalUnits = totalUnits + group:getInitialSize() end end for _, groupName in pairs(data.BlueGroups) do local group = Group.getByName(groupName) if group then totalUnits = totalUnits + group:getInitialSize() end end end do for _, missionZone in pairs(self._tables.MissionZones) do if self._tables.MissionZoneData[missionZone] == nil or self._tables.MissionZoneData[missionZone].description == nil then MissionEditorWarnings.Add("Mission with zonename: " .. missionZone .. " does not have a briefing") end end for _, missionZone in pairs(self._tables.RandomMissionZones) do if self._tables.MissionZoneData[missionZone] == nil or self._tables.MissionZoneData[missionZone].description == nil then MissionEditorWarnings.Add("Mission with zonename: " .. missionZone .. " does not have a briefing") end end end if missions == 0 then missions = 1 end self._logger:info("initiated the database with amount of zones: ") self._logger:info("Stages: " .. Util.tableLength(self._tables.StageZones)) self._logger:info("Total Missions: " .. Util.tableLength(self._tables.MissionZoneData)) self._logger:info("Average units per mission: " .. totalUnits / missions) self._logger:info("Random Missions: " .. Util.tableLength(self._tables.RandomMissionZones)) self._logger:info("Farps: " .. Util.tableLength(self._tables.AllFarpZones)) self._logger:info("Airbases: " .. Util.tableLength(self._tables.AirbaseDataPerAirfield)) return self end local is_group_taken = {} local getAvailableGroups = function() local result = {} for name, value in pairs(is_group_taken) do if value == false then table.insert(result, name) end end return result end local getAvailableCAPGroups = function() local result = {} for name, value in pairs(is_group_taken) do if value == false and Util.startsWith(name, "CAP") then table.insert(result, name) end end return result end function Database:initAvailableUnits() do local all_groups = MizGroupsManager.getAllGroupNames() for _, value in pairs(all_groups) do is_group_taken[value] = false end end end function Database:getAirbaseDataForDrawLayer(layer_object) for _, airbase in pairs(world.getAirbases()) do local zone = DcsUtil.getAirbaseZoneByName(airbase:getName()) if zone and Util.is2dPointInZone({ x = layer_object.mapX, y = layer_object.mapY }, zone) == true then return self:getOrCreateAirbaseData(airbase:getName()) end end return nil end function Database:getOrCreateBlueSamDataForZone(zoneName) local blueSamData = self._tables.BlueSamDataPerZone[zoneName] if blueSamData == nil then blueSamData = { groups = {}, } self._tables.BlueSamDataPerZone[zoneName] = blueSamData end return blueSamData end function Database:getOrCreateFarpDataForZone(zoneName) local farpData = self._tables.FarpZoneData[zoneName] if farpData == nil then farpData = { padNames = {}, groups = {}, supplyHubNames = {} } self._tables.FarpZoneData[zoneName] = farpData end return farpData end function Database:getOrCreateAirbaseData(baseName) local baseData = self._tables.AirbaseDataPerAirfield[baseName] if baseData == nil then baseData = { CapGroups = {}, InterceptGroups = {}, SweepGroups = {}, RedGroups = {}, BlueGroups = {}, supplyHubNames = {} } self._tables.AirbaseDataPerAirfield[baseName] = baseData end return baseData end function Database:loadCapUnits() local all_groups = getAvailableCAPGroups() local airbases = world.getAirbases() for _, airbase in pairs(airbases) do local point = airbase:getPoint() local zone = DcsUtil.getAirbaseZoneByName(airbase:getName()) if zone == nil then zone = { location = { x = point.x, y = point.z }, radius = 4000, name = "temp_zone", verts = {}, zone_type = "Cilinder" } end local baseData = self:getOrCreateAirbaseData(airbase:getName()) local groups = DcsUtil.areGroupsInCustomZone(all_groups, zone) for _, groupName in pairs(groups) do is_group_taken[groupName] = true if Util.startsWith(groupName, "CAP_A", true) or Util.startsWith(groupName, "CAP_B", true) then table.insert(baseData.CapGroups, groupName) elseif Util.startsWith(groupName, "CAP_I", true) then table.insert(baseData.InterceptGroups, groupName) elseif Util.startsWith(groupName, "CAP_S", true) then table.insert(baseData.SweepGroups, groupName) end end self._tables.AirbaseDataPerAirfield[airbase:getName()] = baseData end end function Database:loadBlueSamUnits() local all_groups = MizGroupsManager.getAllGroupNames() for _, blueSamZone in pairs(self._tables.BlueSams) do local samData = self:getOrCreateBlueSamDataForZone(blueSamZone) local groups = DcsUtil.getGroupsInZone(all_groups, blueSamZone) for _, groupName in pairs(groups) do is_group_taken[groupName] = true table.insert(samData.groups, groupName) end local triggerZone = DcsUtil.getZoneByName(blueSamZone) if triggerZone then for _, kvPair in pairs(triggerZone.properties) do if kvPair.key and Util.startsWith(kvPair.key, "buildable") then local number = tonumber(kvPair.value) if number and number > 0 then samData.buildingKilos = number if env.mission.drawings and env.mission.drawings.layers then for _, layer in pairs(env.mission.drawings.layers) do if string.lower(layer.name) == "author" then for _, layer_object in pairs(layer.objects) do local vec2 = { x = layer_object.mapX, y = layer_object.mapY } if layer_object.primitiveType == "TextBox" and triggerZone and Util.is2dPointInZone(vec2, triggerZone) then if layer_object.name and Util.startsWith(layer_object.name, "supplybriefing_", true) then layer_object = layer_object local description = layer_object.text if description and description ~= "" then samData.briefing = description end end end end end end end else MissionEditorWarnings.Add("Buildable number for " .. blueSamZone .. " is invalid") end end end end end end function Database:getStageDrawings() if self._tables.StageDrawings == nil then return {} end return self._tables.StageDrawings end function Database:LoadZoneData(missionZoneName) local all_groups = getAvailableGroups() self._tables.MissionZoneData[missionZoneName] = { RedGroups = {}, BlueGroups = {}, SceneryTargets = {}, ZoneName = missionZoneName, dependsOn = {} } local groups = DcsUtil.getGroupsInZone(all_groups, missionZoneName) for _, groupName in pairs(groups) do if MizGroupsManager.IsGroupStatic(groupName) == true then local object = StaticObject.getByName(groupName) if object and object:getCoalition() == coalition.side.RED then table.insert(self._tables.MissionZoneData[missionZoneName].RedGroups, groupName) elseif object then table.insert(self._tables.MissionZoneData[missionZoneName].BlueGroups, groupName) end else local group = Group.getByName(groupName) if group and group:getCoalition() == coalition.side.RED then table.insert(self._tables.MissionZoneData[missionZoneName].RedGroups, groupName) elseif group then table.insert(self._tables.MissionZoneData[missionZoneName].BlueGroups, groupName) end end is_group_taken[groupName] = true end for _, sceneryObject in pairs(self._tables.AllSceneryObjects) do local point = sceneryObject:GetPoint() if point then if DcsUtil.isPositionInZone(point.x, point.z, missionZoneName) == true then table.insert(self._tables.MissionZoneData[missionZoneName].SceneryTargets, sceneryObject) end end end local triggerZone = DcsUtil.getZoneByName(missionZoneName) if triggerZone and triggerZone.properties then for _, kvPair in pairs(triggerZone.properties) do local key = kvPair.key if Util.startsWith(key, "dependson", true) == true then table.insert(self._tables.MissionZoneData[missionZoneName].dependsOn, kvPair.value) elseif Util.startsWith(key, "completeat") == true then local value = tonumber(kvPair.value) if value then if value > 1 and value <= 100 then value = value / 100 elseif value > 100 then MissionEditorWarnings.Add("Mission with zonename: " .. missionZoneName .. " has a complete at value of " .. value .. " which is higher than 100, this will not work as intended") end self._tables.MissionZoneData[missionZoneName].completeAt = value end elseif Util.startsWith(key, "primary", true) == true then if type(kvPair.value) == "string" then if kvPair.value == "true" then self._tables.MissionZoneData[missionZoneName].primaryOverwrite = true elseif kvPair.value == "false" then self._tables.MissionZoneData[missionZoneName].primaryOverwrite = false end end end end end if env.mission.drawings and env.mission.drawings.layers then for _, layer in pairs(env.mission.drawings.layers) do if string.lower(layer.name) == "author" then for _, layer_object in pairs(layer.objects) do local vec2 = { x = layer_object.mapX, y = layer_object.mapY } if layer_object.primitiveType == "TextBox" and triggerZone and Util.is2dPointInZone(vec2, triggerZone) then if layer_object.name and Util.startsWith(layer_object.name, "briefing_", true) then layer_object = layer_object local description = layer_object.text if description and description ~= "" then self._tables.MissionZoneData[missionZoneName].description = description self._tables.MissionZoneData[missionZoneName].descriptionLocation = vec2 end end end end end end end end function Database:loadMissionzoneUnits() for _, missionZoneName in pairs(self._tables.MissionZones) do self:LoadZoneData(missionZoneName) end end function Database:loadRandomMissionzoneUnits() for _, missionZoneName in pairs(self._tables.RandomMissionZones) do self:LoadZoneData(missionZoneName) end end function Database:loadFarpData() local all_groups = getAvailableGroups() for _, farpZone in pairs(self._tables.AllFarpZones) do local farpzoneData = self:getOrCreateFarpDataForZone(farpZone) local groups = DcsUtil.getGroupsInZone(all_groups, farpZone) for _, groupName in pairs(groups) do is_group_taken[groupName] = true table.insert(farpzoneData.groups, groupName) end local triggerZone = DcsUtil.getZoneByName(farpZone) if triggerZone then for _, kvPair in pairs(triggerZone.properties) do if kvPair.key and Util.startsWith(kvPair.key, "buildable", true) == true then local number = tonumber(kvPair.value) if number and number > 0 then farpzoneData.buildingKilos = number if env.mission.drawings and env.mission.drawings.layers then for _, layer in pairs(env.mission.drawings.layers) do if string.lower(layer.name) == "author" then for _, layer_object in pairs(layer.objects) do local vec2 = { x = layer_object.mapX, y = layer_object.mapY } if layer_object.primitiveType == "TextBox" and triggerZone and Util.is2dPointInZone(vec2, triggerZone) then layer_object = layer_object if layer_object.name and Util.startsWith(layer_object.name, "supplybriefing_", true) then local description = layer_object.text if description and description ~= "" then farpzoneData.briefing = description end end end end end end end else MissionEditorWarnings.Add("Buildable number for " .. farpZone .. " is invalid.") end end end end end end function Database:loadAirbaseGroups() local all_groups = getAvailableGroups() for _, stageZone in pairs(self._tables.StageZones) do for _, baseName in pairs(stageZone.AirbaseNames) do local base = Airbase.getByName(baseName) if base then local basedata = self:getOrCreateAirbaseData(baseName) local point = base:getPoint() local airbaseZone = DcsUtil.getAirbaseZoneByName(baseName) if airbaseZone == nil then airbaseZone = { location = { x = point.x, y = point.z }, radius = 4000, name = "temp_zone", verts = {}, zone_type = "Cilinder" } end if airbaseZone and base:getDesc().category == Airbase.Category.AIRDROME then local groups = DcsUtil.areGroupsInCustomZone(all_groups, airbaseZone) for _, groupName in pairs(groups) do if MizGroupsManager.IsGroupStatic(groupName) == true then local object = StaticObject.getByName(groupName) if object then if object:getCoalition() == coalition.side.RED then table.insert(basedata.RedGroups, groupName) is_group_taken[groupName] = true elseif object:getCoalition() == coalition.side.BLUE then table.insert(basedata.BlueGroups, groupName) is_group_taken[groupName] = true end end else local group = Group.getByName(groupName) if group then if group:getCoalition() == coalition.side.RED then table.insert(basedata.RedGroups, groupName) is_group_taken[groupName] = true elseif group:getCoalition() == coalition.side.BLUE then table.insert(basedata.BlueGroups, groupName) is_group_taken[groupName] = true end end end end end end end end end function Database:loadMiscGroupsInStages() local all_groups = getAvailableGroups() for _, stageZone in pairs(self._tables.StageZones) do stageZone.MiscGroups = {} local groups = DcsUtil.getGroupsInZone(all_groups, stageZone.StageZoneName) for _, groupName in pairs(groups) do if MizGroupsManager.IsGroupStatic(groupName) == true then local object = StaticObject.getByName(groupName) if object and object:getCoalition() ~= coalition.side.NEUTRAL then is_group_taken[groupName] = true table.insert(stageZone.MiscGroups, groupName) end else local group = Group.getByName(groupName) if group and group:getCoalition() ~= coalition.side.NEUTRAL then is_group_taken[groupName] = true table.insert(stageZone.MiscGroups, groupName) end end end end end function Database:GetLocationForMissionZone(missionZoneName) if self._tables.MissionZoneData[missionZoneName] and self._tables.MissionZoneData[missionZoneName].descriptionLocation then return self._tables.MissionZoneData[missionZoneName].descriptionLocation end return self._tables.MissionZonesLocations[missionZoneName] end function Database:GetCapZoneForZoneID(zoneID) zoneID = tostring(zoneID) or "nothing" local capZonesForID = self._tables.capZonesByCapZoneID[zoneID] if capZonesForID and capZonesForID.zones then local count = Util.tableLength(capZonesForID.zones) if count == 0 then self._logger:warn("Tried to get cap zone for zoneID: " .. zoneID .. " but cap zones were empty for this ID") return nil end capZonesForID.current = capZonesForID.current + 1 if Util.tableLength(capZonesForID.zones) < capZonesForID.current then capZonesForID.current = 1 end return capZonesForID.zones[capZonesForID.current] else self._logger:warn("Tried to get cap zone for zoneID: " .. zoneID .. " but no cap zones were found for this ID") end return nil end function Database:GetInterceptZonesForZoneID(zoneID) zoneID = tostring(zoneID) or "nothing" local interceptZonesForID = self._tables.interceptZonesByZoneID[zoneID] if not interceptZonesForID then return {} end return interceptZonesForID end function Database:getStagezoneNames() return self._tables.StageZoneNames end function Database:getCarrierRouteZones() return self._tables.CarrierRouteZones end function Database:getBriefingForStage(stagename) local stageZone = self._tables.StageZones[stagename] if not stageZone then return "" end return stageZone.StageBriefing or "" end function Database:getMissionsForStage(stagename) local stageZone = self._tables.StageZones[stagename] if not stageZone then return {} end return stageZone.MissionZones end function Database:getRandomMissionsForStage(stagename) local stageZone = self._tables.StageZones[stagename] if not stageZone then return {} end return stageZone.RandomMissionZones end function Database:getMissionDataForZone(missionZoneName) return self._tables.MissionZoneData[missionZoneName] end function Database:getAirbaseNamesInStage(stageName) local stageData = self._tables.StageZones[stageName] if not stageData then return {} end return stageData.AirbaseNames or {} end function Database:getFarpNamesInStage(stageName) local stageData = self._tables.StageZones[stageName] if not stageData then return {} end return stageData.FarpZones or {} end function Database:getFarpDataForZone(farpZoneName) local farpData = self._tables.FarpZoneData[farpZoneName] if not farpData then return nil end return farpData end function Database:getAirbaseDataForZone(baseName) local baseData = self._tables.AirbaseDataPerAirfield[baseName] if not baseData then return nil end return baseData end function Database:getStageBriefingForStage(stageName) local stageData = self._tables.StageZones[stageName] if not stageData then return nil end return stageData.StageBriefing or nil end function Database:getBlueSamsInStage(stageName) local stageData = self._tables.StageZones[stageName] if not stageData then return {} end return stageData.BlueSamZones end function Database:getSupplyHubsInStage(stageName) local stageData = self._tables.StageZones[stageName] if not stageData then return {} end return stageData.SupplyHubZones end function Database:getFarpDependencyForSupplyHub(stageName, supplyZoneName) local stageData = self._tables.StageZones[stageName] if not stageData then return nil end return stageData.SupplyHubZonesInFarp[supplyZoneName] end function Database:getBlueSamDataForZone(samZone) return self._tables.BlueSamDataPerZone[samZone] end function Database:getMiscGroupsAtStage(stageName) local stageZone = self._tables.StageZones[stageName] if not stageZone then return {} end return stageZone.MiscGroups end function Database:GetNewMissionCode() local code = nil local tries = 0 while code == nil and tries < 10 do local random = math.random(1000, 9999) if self._tables.missionCodes[random] == nil then code = random end tries = tries + 1 end return code end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end ScriptGlobals.classes.spearhead_db = Database end -- classes.spearhead_db do -- classes.spearhead_events local Logger = ScriptGlobals.classes.util.logger local Persistence = ScriptGlobals.classes.persistence.persistence local DcsUtil = ScriptGlobals.classes.util.dcsutil local SpearheadEvents = {} do local logger = nil SpearheadEvents.Init = function(logLevel) logger = Logger.new("Events", logLevel) end local warn = function(text) if logger then logger:warn(text) end end local logError = function(text) if logger then logger:error(text) end end local logDebug = function(text) if logger then logger:debug(text) end end do local OnStageNumberChangedListeners = {} local OnStageNumberChangedHandlers = {} SpearheadEvents.AddStageNumberChangedListener = function(listener) if type(listener) ~= "table" or type(listener.OnStageNumberChanged) ~= "function" then warn("Event handler not of type table/object with function OnStageNumberChanged(self, number, stageLaneIdentifier)") return end table.insert(OnStageNumberChangedListeners, listener) end local OnStageNumberChangeCompleteListeners = {} SpearheadEvents.AddStageNumberChangeCompleteListener = function(listener) if type(listener) ~= "table" or type(listener.OnStageNumberChangeComplete) ~= "function" then warn("Event handler not of type table/object with function OnStageNumberChangeComplete(self, number, laneIdentifier)") return end table.insert(OnStageNumberChangeCompleteListeners, listener) end SpearheadEvents.PublishStageNumberChanged = function(newStageNumber, laneIdentifier) pcall(function () Persistence.SetActiveStage(laneIdentifier, newStageNumber) end) for _, callable in pairs(OnStageNumberChangedListeners) do local _, err = pcall(function() callable:OnStageNumberChanged(newStageNumber, laneIdentifier) end) if err then logError(err) end end for _, callable in pairs(OnStageNumberChangedHandlers) do local _, err = pcall(callable, newStageNumber, laneIdentifier) if err then logError(err) end end for _, callable in pairs(OnStageNumberChangeCompleteListeners) do local _, err = pcall(function() callable:OnStageNumberChangeComplete(newStageNumber, laneIdentifier) end) if err then logError(err) end end Logger.new("Events", "INFO"):info("Published stage number changed to: " .. tostring(newStageNumber)) end end local onWeaponFiredListeners = {} SpearheadEvents.AddWeaponFiredListener = function(weaponFiredListener) if type(weaponFiredListener) ~= "table" then warn("Event handler not of type table/object") return end table.insert(onWeaponFiredListeners, weaponFiredListener) end local triggerWeaponFired = function(unit, weapon, target) for _, callable in pairs(onWeaponFiredListeners) do local _, err = pcall(function() callable:OnWeaponFired(unit, weapon, target) end) if err then logError(err) end end end local onLandEventListeners = {} SpearheadEvents.addOnUnitLandEventListener = function(unitName, landListener) if type(landListener) ~= "table" then warn("Event handler not of type table/object") return end if onLandEventListeners[unitName] == nil then onLandEventListeners[unitName] = {} end table.insert(onLandEventListeners[unitName], landListener) end local OnUnitLostListeners = {} SpearheadEvents.addOnUnitLostEventListener = function(unitName, unitLostListener) if type(unitLostListener) ~= "table" then warn("Unit lost Event listener not of type table/object") return end if OnUnitLostListeners[unitName] == nil then OnUnitLostListeners[unitName] = {} end table.insert(OnUnitLostListeners[unitName], unitLostListener) end do local OnGroupRTBListeners = {} SpearheadEvents.addOnGroupRTBListener = function(groupName, handlingObject) if type(handlingObject) ~= "table" then warn("Event handler not of type table/object") return end if OnGroupRTBListeners[groupName] == nil then OnGroupRTBListeners[groupName] = {} end table.insert(OnGroupRTBListeners[groupName], handlingObject) end SpearheadEvents.PublishRTB = function(groupName) if groupName ~= nil then if OnGroupRTBListeners[groupName] then for _, callable in pairs(OnGroupRTBListeners[groupName]) do local _, err = pcall(function() callable:OnGroupRTB(groupName) end) if err then logError(err) end end end end end local OnGroupRTBInTenListeners = {} SpearheadEvents.addOnGroupRTBInTenListener = function(groupName, handlingObject) if type(handlingObject) ~= "table" then warn("Event handler not of type table/object") return end if OnGroupRTBInTenListeners[groupName] == nil then OnGroupRTBInTenListeners[groupName] = {} end table.insert(OnGroupRTBInTenListeners[groupName], handlingObject) end SpearheadEvents.PublishRTBInTen = function(groupName) if groupName ~= nil then if OnGroupRTBInTenListeners[groupName] then for _, callable in pairs(OnGroupRTBInTenListeners[groupName]) do local _, err = pcall(function() callable:OnGroupRTBInTen(groupName) end) if err then logError(err) end end end end end end do local OnGroupOnStationListeners = {} SpearheadEvents.addOnGroupOnStationListener = function(groupName, handlingObject) if type(handlingObject) ~= "table" then warn("Event handler not of type table/object") return end if OnGroupOnStationListeners[groupName] == nil then OnGroupOnStationListeners[groupName] = {} end table.insert(OnGroupOnStationListeners[groupName], handlingObject) end SpearheadEvents.PublishOnStation = function(groupName) if groupName ~= nil then if OnGroupOnStationListeners[groupName] then for _, callable in pairs(OnGroupOnStationListeners[groupName]) do local _, err = pcall(function() callable:OnGroupOnStation(groupName) end) if err then logError(err) end end end end end end do local playerEnterUnitListeners = {} SpearheadEvents.AddOnPlayerEnterUnitListener = function(listener) if type(listener) ~= "table" then warn("Unit lost Event listener not of type table/object") return end table.insert(playerEnterUnitListeners, listener) end SpearheadEvents.TriggerPlayerEntersUnit = function(unit) if unit ~= nil then if playerEnterUnitListeners then for _, callable in pairs(playerEnterUnitListeners) do local _, err = pcall(function() callable:OnPlayerEntersUnit(unit) end) if err then logError(err) end end end end end end do local unitEjectListeners = {} SpearheadEvents.AddOnUnitEjectedListener = function(listener) if type(listener) ~= "table" then warn("Unit lost Event listener not of type table/object") return end table.insert(unitEjectListeners, listener) end end local e = {} function e:onEvent(event) if event.id == world.event.S_EVENT_LAND or event.id == world.event.S_EVENT_RUNWAY_TOUCH then local unit = event.initiator local airbase = event.place if unit ~= nil then local name = unit:getName() if onLandEventListeners[name] then for _, callable in pairs(onLandEventListeners[name]) do local _, err = pcall(function() callable:OnUnitLanded(unit, airbase) end) if err then logError(err) end end end end end if event.id == world.event.S_EVENT_DEAD or event.id == world.event.S_EVENT_CRASH or event.id == world.event.S_EVENT_EJECTION or event.id == world.event.S_EVENT_UNIT_LOST then local object = event.initiator if object and object.getName then logDebug("Receiving death event from: " .. object:getName()) end if object and object.getName and OnUnitLostListeners[object:getName()] then for _, callable in pairs(OnUnitLostListeners[object:getName()]) do local _, err = pcall(function() callable:OnUnitLost(object) end) if err then logError(err) end end end end if event.id == world.event.S_EVENT_SHOT then local shooter = event.initiator local weapon = event.weapon local target = event.target triggerWeaponFired(shooter, weapon, target) end if event.id == world.event.S_EVENT_MISSION_END then Persistence.UpdateNow() end local AI_GROUPS = {} local function CheckAndTriggerSpawnAsync(unit, _) local function isPlayer(checkUnit) if checkUnit == nil then return false, "unit is nil" end if checkUnit.getGroup == nil then return false, 'no get group function in unit object, most likely static' end if Object.getCategory(checkUnit) ~= Object.Category.UNIT then return false, "object is not a unit" end if checkUnit:isExist() ~= true then return false, "unit does not exist" end local group = checkUnit:getGroup() if group ~= nil then if AI_GROUPS[group:getName()] == true then return false end local players = DcsUtil.getAllPlayerUnits() local unitName = checkUnit:getName() for _, playerUnit in pairs(players) do if playerUnit:getName() == unitName then return true end end AI_GROUPS[group:getName()] = true end return false, "unit is nil or does not exist" end if isPlayer(unit) == true then SpearheadEvents.TriggerPlayerEntersUnit(unit) end end if event.id == world.event.S_EVENT_BIRTH then timer.scheduleFunction(CheckAndTriggerSpawnAsync, event.initiator, timer.getTime() + 3) end end world.addEventHandler(e) end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end ScriptGlobals.classes.spearhead_events = SpearheadEvents end -- classes.spearhead_events do -- classes.stageclasses.helpers.supplyconfighelper local Util = ScriptGlobals.classes.util.util local SupplyConfig = { ["FARP_CRATE_500"] = { type = "FARP_CRATE", weight = 500, displayName = "FARP Crate (500)", staticType = "container_cargo", }, ["FARP_CRATE_1000"] = { type = "FARP_CRATE", weight = 1000, displayName = "FARP Crate (1000)", staticType = "container_cargo", }, ["FARP_CRATE_2000"] = { type = "FARP_CRATE", weight = 2000, displayName = "FARP Crate (2000)", staticType = "container_cargo", }, ["SAM_CRATE_500"] = { type = "SAM_CRATE", weight = 1000, displayName = "SAM Crate (500)", staticType = "container_cargo", }, ["SAM_CRATE_1000"] = { type = "SAM_CRATE", weight = 1000, displayName = "SAM Crate (1000)", staticType = "container_cargo", }, ["SAM_CRATE_2000"] = { type = "SAM_CRATE", weight = 2000, displayName = "SAM Crate (2000)", staticType = "container_cargo", }, ["AIRBASE_CRATE_2000"] = { type = "AIRBASE_CRATE", weight = 2000, displayName = "Airbase Crate (2000)", staticType = "container_cargo", }, } local SupplyConfigHelper = {} function SupplyConfigHelper.fromObjectName(name) for configName, config in pairs(SupplyConfig) do if Util.startsWith(name, configName, true) == true then return config end end return nil end function SupplyConfigHelper.getSupplyConfig(type) return SupplyConfig[type] end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.stageclasses then ScriptGlobals.classes.stageclasses = {} end if not ScriptGlobals.classes.stageclasses.helpers then ScriptGlobals.classes.stageclasses.helpers = {} end ScriptGlobals.classes.stageclasses.helpers.supplyconfighelper = SupplyConfigHelper end -- classes.stageclasses.helpers.supplyconfighelper do -- classes.stageclasses.helpers.supplyloadconfig local SupplyLoadConfig = { ["Mi-8MT"] = { maxInternalLoad = 4000, dropZones = { { centerAngle = 180, angleWidth = 60, minRadius = 20, maxRadius = 50, spacing = 5 }, } }, ["CH-47Fbl1"] = { maxInternalLoad = 10000, dropZones = { { centerAngle = 180, angleWidth = 30, minRadius = 20, maxRadius = 75, spacing = 5 }, } }, ["Mi-24P"] = { maxInternalLoad = 2000, dropZones = { { centerAngle = 270, angleWidth = 60, minRadius = 15, maxRadius = 50, spacing = 5 }, { centerAngle = 90, angleWidth = 60, minRadius = 15, maxRadius = 50, spacing = 5 }, } }, ["UH-1H"] = { maxInternalLoad = 2000, dropZones = { { centerAngle = 270, angleWidth = 60, minRadius = 15, maxRadius = 50, spacing = 5 }, { centerAngle = 90, angleWidth = 60, minRadius = 15, maxRadius = 50, spacing = 5 }, } } } if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.stageclasses then ScriptGlobals.classes.stageclasses = {} end if not ScriptGlobals.classes.stageclasses.helpers then ScriptGlobals.classes.stageclasses.helpers = {} end ScriptGlobals.classes.stageclasses.helpers.supplyloadconfig = SupplyLoadConfig end -- classes.stageclasses.helpers.supplyloadconfig do -- classes.stageclasses.helpers.supplyunitstracker local Logger = ScriptGlobals.classes.util.logger local Util = ScriptGlobals.classes.util.util local DcsUtil = ScriptGlobals.classes.util.dcsutil local SpearheadEvents = ScriptGlobals.classes.spearhead_events local SupplyConfigHelper = ScriptGlobals.classes.stageclasses.helpers.supplyconfighelper local SupplyLoadConfig = ScriptGlobals.classes.stageclasses.helpers.supplyloadconfig local SupplyUnitsTracker = {} SupplyUnitsTracker.__index = SupplyUnitsTracker local singleton = nil function SupplyUnitsTracker.getOrCreate() if singleton == nil then singleton = setmetatable({}, SupplyUnitsTracker) singleton._logger = Logger.new("SupplyUnitsTracker") singleton._unitPositions = {} singleton._cargoInUnits = {} singleton._supplyUnitsByName = {} singleton._droppedCrates = {} singleton._registeredHubs = {} singleton._supplyUnitEventsListeners = {} singleton._unitInSupplyHub = {} SpearheadEvents.AddOnPlayerEnterUnitListener(singleton) local function updateTask(selfA, time) selfA:Update() return time + 15 end timer.scheduleFunction(updateTask, singleton, timer.getTime() + 15) local function checkUnitsInZone(selfA, time) pcall(function() selfA:CheckUnitsInZones() end) return time + 5 end timer.scheduleFunction(checkUnitsInZone, singleton, timer.getTime() + 5) end return singleton end function SupplyUnitsTracker:OnPlayerEntersUnit(unit) if unit == nil then return end if self:IsSupplyUnit(unit) == true then self._supplyUnitsByName[unit:getName()] = unit self._cargoInUnits[tostring(unit:getID())] = nil self._unitInSupplyHub[tostring(unit:getID())] = false self._unitPositions[tostring(unit:getID())] = unit:getPoint() end for _, listener in pairs(self._supplyUnitEventsListeners) do pcall(function() if listener.supplyUnitSpawned then listener:supplyUnitSpawned(unit) end end) end end function SupplyUnitsTracker:AddOnSupplyUnitEventListener(listener) if listener == nil then return end if self._supplyUnitEventsListeners == nil then self._supplyUnitEventsListeners = {} end table.insert(self._supplyUnitEventsListeners, listener) end function SupplyUnitsTracker:IsUnitInSupplyHub(unit) if unit == nil then return false end local unitIDStr = tostring(unit:getID()) return self._unitInSupplyHub[unitIDStr] == true end function SupplyUnitsTracker:IsGroupLeadInSupplyHub(groupID) local group = DcsUtil.GetPlayerGroupByGroupID(groupID) if group == nil then return false end local unit = group:getUnit(1) if unit == nil then return false end return self:IsUnitInSupplyHub(unit) end function SupplyUnitsTracker:Update() local players = DcsUtil.getAllPlayerUnits() for _, player in pairs(players) do if player ~= nil and player:isExist() and self:IsSupplyUnit(player) == true then self._supplyUnitsByName[player:getName()] = player end end end function SupplyUnitsTracker:IsSupplyUnit(unit) if unit == nil then return false end if unit:hasAttribute("Transport helicopters") then return true end if unit:hasAttribute("Helicopters") and unit:hasAttribute("Transports") then return true end return false end function SupplyUnitsTracker:AddCargoToUnit(unitID, crateType) if unitID == nil or crateType == nil then return end local unit = DcsUtil.GetPlayerUnitByID(unitID) if unit == nil then return end local unitIdStr = tostring(unitID) if self._cargoInUnits[unitIdStr] == nil then self._cargoInUnits[unitIdStr] = {} end if self._cargoInUnits[unitIdStr][crateType] == nil then self._cargoInUnits[unitIdStr][crateType] = 0 end self._cargoInUnits[unitIdStr][crateType] = self._cargoInUnits[unitIdStr][crateType] + 1 end function SupplyUnitsTracker:RemoveCargoFromUnit(unitID, crateType) if unitID == nil or crateType == nil then return end local unitIDStr = tostring(unitID) if self._cargoInUnits[unitIDStr] == nil then return end if self._cargoInUnits[unitIDStr][crateType] == nil then return end self._cargoInUnits[unitIDStr][crateType] = self._cargoInUnits[unitIDStr][crateType] - 1 local hasCargo = false for _, count in pairs(self._cargoInUnits[unitIDStr]) do if count > 0 then hasCargo = true break end end if hasCargo == false then self._cargoInUnits[unitIDStr] = nil end end function SupplyUnitsTracker:UpdateWeightForUnit(unit) local weight = 0 if self._cargoInUnits[tostring(unit:getID())] then for crateType, count in pairs(self._cargoInUnits[tostring(unit:getID())]) do local crateConfig = SupplyConfigHelper.getSupplyConfig(crateType) if crateConfig and count then weight = weight + (crateConfig.weight * count) end end end trigger.action.setUnitInternalCargo(unit:getName(), weight) end function SupplyUnitsTracker:CheckUnitsInZones() for _, unit in pairs(self._supplyUnitsByName) do if unit ~= nil and unit:isExist() == true then self._logger:debug("Checking unit: " .. unit:getName()) local pos = unit:getPoint() for hub, enabled in pairs(self._registeredHubs) do if enabled == true then local zone = hub:GetZone() if zone ~= nil then if Util.is3dPointInZone(pos, zone) then if self._unitInSupplyHub[tostring(unit:getID())] ~= true then self._unitInSupplyHub[tostring(unit:getID())] = true for _, listener in pairs(self._supplyUnitEventsListeners) do pcall(function() if listener.enteredSupplyHub then listener:enteredSupplyHub(unit, hub) end end) end end else if self._unitInSupplyHub[tostring(unit:getID())] == true then self._unitInSupplyHub[tostring(unit:getID())] = false for _, listener in pairs(self._supplyUnitEventsListeners) do pcall(function() if listener.exitedSupplyHub then listener:exitedSupplyHub(unit, hub) end end) end end end end end end self._unitPositions[tostring(unit:getID())] = pos end end end function SupplyUnitsTracker:RegisterHub(hub) if hub == nil then return end if self._registeredHubs[hub] == nil then self._registeredHubs[hub] = true end end function SupplyUnitsTracker:GetCargoInUnit(unitID) if unitID == nil then return end local unitIDStr = tostring(unitID) if self._cargoInUnits[unitIDStr] == nil then return end return self._cargoInUnits[unitIDStr] end function SupplyUnitsTracker:GetUnits() return self._supplyUnitsByName end local cargoCount = 0 function SupplyUnitsTracker:UnloadRequested(unitID, crateType, missionCommandsHelper) self._logger:debug("Unload requested for unit: " .. unitID .. " crateType: " .. crateType) local unit = DcsUtil.GetPlayerUnitByID(unitID) if unit == nil or unit:isExist() == false then self._logger:warn("Unload requested for non-existent unit: " .. unitID) return end local group = unit:getGroup() if group == nil then self._logger:warn("Unload requested for unit with no group: " .. unit:getName()) return end local cargoConfig = SupplyConfigHelper.getSupplyConfig(crateType) if cargoConfig == nil then self._logger:error("Invalid crate type: " .. crateType) return end local cargoPos = self:GetCargoPlacePosition(unit, cargoConfig.staticType) if cargoPos == nil then self._logger:warn("No valid position found to drop cargo for unit: " .. unit:getName()) trigger.action.outTextForUnit(unit:getID(), "No valid position to drop cargo. Unloading area is too crowded.", 10) return end self:RemoveCargoFromUnit(unitID, crateType) self:UpdateWeightForUnit(unit) cargoCount = cargoCount + 1 local cargoSpawnObject = { name = crateType .. "_" .. cargoCount, type = cargoConfig.staticType, x = cargoPos.x, y = cargoPos.z, } self._logger:debug("Spawning crate #" .. cargoCount .. " at (" .. string.format("%.2f", cargoPos.x) .. ", " .. string.format("%.2f", cargoPos.y) .. ", " .. string.format("%.2f", cargoPos.z) .. ")") local spawned = coalition.addStaticObject(unit:getCoalition(), cargoSpawnObject) self._droppedCrates[cargoSpawnObject.name] = spawned missionCommandsHelper:updateCommandsForGroup(group:getID()) self._logger:debug("Cargo dropped for unit: " .. unit:getName() .. " crateType: " .. crateType) end function SupplyUnitsTracker:GetCargoCratesDropped() return self._droppedCrates end function SupplyUnitsTracker:UnitRequestCrateLoading(groupID, crateType, missionCommandsHelper) self._logger:debug("UnitRequestCrateLoading called with groupID: " .. groupID .. " and crateType: " .. crateType) local group = DcsUtil.GetPlayerGroupByGroupID(groupID) if group ~= nil then local crateConfig = SupplyConfigHelper.getSupplyConfig(crateType) if crateConfig == nil then self._logger:error("Invalid crate type: " .. crateType) return end local unit = group:getUnit(1) if unit == nil then return end if unit:isExist() == false then return end if unit:inAir() == true then trigger.action.outTextForUnit(unit:getID(), "Land first before crates can be loaded", 10) return end trigger.action.outTextForUnit(unit:getID(), "Loading crate of type " .. crateType, 13) local LoadCrateTask = function(params) local loaded = params.self:TryLoadCrateInUnit(params.unit, params.crateType, params.commandHelper) if loaded ~= false then trigger.action.outTextForUnit(unit:getID(), "Loaded crate :" .. params.crateType, 10) end end local params = { self = self, unit = unit, crateType = crateType, groupID = groupID, commandHelper = missionCommandsHelper } timer.scheduleFunction(LoadCrateTask, params, timer.getTime() + 15) end end function SupplyUnitsTracker:TryLoadCrateInUnit(unit, crateType, commandHelper) local crateConfigA = SupplyConfigHelper.getSupplyConfig(crateType) if crateConfigA == nil then trigger.action.outTextForUnit(unit:getID(), "Invalid crate type: " .. crateType, 5) return false end local currentWeight = 0 for _, cargo in pairs(self._cargoInUnits) do if cargo[crateType] ~= nil then currentWeight = currentWeight + (cargo[crateType] * crateConfigA.weight) end end local unitConfig = SupplyLoadConfig[unit:getTypeName()] if unitConfig == nil then trigger.action.outTextForUnit(unit:getID(), "Your unit type is not configured for logistics: " .. crateType, 5) self._logger:error("Invalid unit type: " .. unit:getTypeName()) return false end local maxWeight = unitConfig.maxInternalLoad if currentWeight + crateConfigA.weight > maxWeight then trigger.action.outTextForUnit(unit:getID(), "Failed to load crate due to it overloading your max weight of: " .. maxWeight .. "kg", 5) return false end self:AddCargoToUnit(unit:getID(), crateType) self:UpdateWeightForUnit(unit) local group = unit:getGroup() if group == nil then return false end local groupID = group:getID() commandHelper:updateCommandsForGroup(groupID) return true end function SupplyUnitsTracker:UnitRequestCrateSpawn(groupID, crateType) local group = DcsUtil.GetPlayerGroupByGroupID(groupID) if group == nil then local crateConfig = SupplyConfigHelper.getSupplyConfig(crateType) if crateConfig == nil then self._logger:error("Invalid crate type: " .. crateType) return end end end function SupplyUnitsTracker:GetBoundingBoxes(foundObject) local desc = foundObject:getDesc() if foundObject:getCategory() == Object.Category.SCENERY then desc = SceneryObject.getDescByName(foundObject:getTypeName()) end if desc == nil or desc.box == nil then return nil end local objPos = foundObject:getPoint() local box = desc.box local heading = 0 pcall(function() local objPosition = foundObject:getPosition() if objPosition and objPosition.x then heading = math.atan2(objPosition.x.z, objPosition.x.x) end end) local minX = box.min.x local maxX = box.max.x local minZ = box.min.z local maxZ = box.max.z if math.abs(heading) > 0.1 then local corners = { {minX, minZ}, {minX, maxZ}, {maxX, minZ}, {maxX, maxZ} } minX, maxX = math.huge, -math.huge minZ, maxZ = math.huge, -math.huge for _, corner in ipairs(corners) do local rotX = corner[1] * math.cos(heading) - corner[2] * math.sin(heading) local rotZ = corner[1] * math.sin(heading) + corner[2] * math.cos(heading) minX = math.min(minX, rotX) maxX = math.max(maxX, rotX) minZ = math.min(minZ, rotZ) maxZ = math.max(maxZ, rotZ) end end return { min = { x = objPos.x + minX, y = objPos.y + box.min.y, z = objPos.z + minZ }, max = { x = objPos.x + maxX, y = objPos.y + box.max.y, z = objPos.z + maxZ }, heading = heading } end function SupplyUnitsTracker:CheckBBoxCollision(crateBBox, objBBox, safetyMargin) local objMin = { x = objBBox.min.x - safetyMargin, y = objBBox.min.y - safetyMargin, z = objBBox.min.z - safetyMargin } local objMax = { x = objBBox.max.x + safetyMargin, y = objBBox.max.y + safetyMargin, z = objBBox.max.z + safetyMargin } return crateBBox.min.x <= objMax.x and crateBBox.max.x >= objMin.x and crateBBox.min.y <= objMax.y and crateBBox.max.y >= objMin.y and crateBBox.min.z <= objMax.z and crateBBox.max.z >= objMin.z end function SupplyUnitsTracker:GetCargoPlacePosition(unit, crateTypeName) local unitPos = unit:getPosition() local unitHeading = math.atan2(unitPos.x.z, unitPos.x.x) local crateDesc = StaticObject.getDescByName(crateTypeName) if crateDesc == nil or crateDesc.box == nil then self._logger:error("Could not get bbox for crate type: " .. crateTypeName) return nil end local crateRelativeBBox = crateDesc.box local dropZones = { { centerAngle = 180, angleWidth = 60, minRadius = 15, maxRadius = 50, spacing = 5 } } if SupplyLoadConfig[unit:getTypeName()] ~= nil then dropZones = SupplyLoadConfig[unit:getTypeName()].dropZones end local searchVolume = { id = world.VolumeType.SPHERE, params = { point = { x = unitPos.p.x, y = unitPos.p.y, z = unitPos.p.z }, radius = 100 -- Search a large area } } local occupiedObjects = {} local found = function(foundItem, _) local bbox = self:GetBoundingBoxes(foundItem) if bbox then self._logger:debug("Found object: " .. foundItem:getTypeName() .. " at (" .. foundItem:getPoint().x .. ", " .. foundItem:getPoint().z .. ")") table.insert(occupiedObjects, { pos = foundItem:getPoint(), bbox = bbox }) else self._logger:debug("Found object without bbox: " .. foundItem:getTypeName()) end end local searchCategories = {} for key, value in pairs(Object.Category) do self._logger:debug("Adding category to search: " .. tostring(value) .. " (" .. tostring(key) .. ")") table.insert(searchCategories, value) end world.searchObjects(searchCategories, searchVolume, found) local safetyMargin = 3 for _, zone in ipairs(dropZones) do local minAngle = zone.centerAngle - (zone.angleWidth / 2) local maxAngle = zone.centerAngle + (zone.angleWidth / 2) for distance = zone.minRadius, zone.maxRadius, zone.spacing do local angleStep = math.min(15, zone.angleWidth / 3) for angle = minAngle, maxAngle, angleStep do local radians = math.rad(angle) local worldAngle = radians + unitHeading local candidateX = unitPos.p.x + distance * math.sin(worldAngle) local candidateZ = unitPos.p.z + distance * math.cos(worldAngle) local candidateY = land.getHeight({ x = candidateX, y = candidateZ }) local crateBBoxWorldSpace = { min = { x = candidateX + crateRelativeBBox.min.x, y = candidateY + crateRelativeBBox.min.y, z = candidateZ + crateRelativeBBox.min.z }, max = { x = candidateX + crateRelativeBBox.max.x, y = candidateY + crateRelativeBBox.max.y, z = candidateZ + crateRelativeBBox.max.z } } local collides = false for _, obj in ipairs(occupiedObjects) do if self:CheckBBoxCollision(crateBBoxWorldSpace, obj.bbox, safetyMargin) then collides = true self._logger:debug("Collision at angle=" .. angle .. ", distance=" .. distance) break end end if not collides then self._logger:debug("Valid position found at angle=" .. angle .. ", distance=" .. distance .. ", pos=(" .. string.format("%.2f", candidateX) .. ", " .. string.format("%.2f", candidateY) .. ", " .. string.format("%.2f", candidateZ) .. ")") return { x = candidateX, y = candidateY, z = candidateZ } end end end end return nil end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.stageclasses then ScriptGlobals.classes.stageclasses = {} end if not ScriptGlobals.classes.stageclasses.helpers then ScriptGlobals.classes.stageclasses.helpers = {} end ScriptGlobals.classes.stageclasses.helpers.supplyunitstracker = SupplyUnitsTracker end -- classes.stageclasses.helpers.supplyunitstracker do -- classes.configuration.stageconfig local StageConfig = {}; StageConfig.__index = StageConfig local Logger = ScriptGlobals.classes.util.logger local _logger = Logger.new("StageConfig", Logger.LogLevel) local function new() if SpearheadConfig == nil then _logger:warn("SpearheadConfig is nil, creating default SpearheadConfig") SpearheadConfig = {} end if SpearheadConfig.StageConfig == nil then _logger:warn("SpearheadConfig.StageConfig is nil, creating default StageConfig") SpearheadConfig.StageConfig = {} end local self = setmetatable({}, StageConfig) self.isEnabled = SpearheadConfig.StageConfig.enabled ~= false self.isDrawStagesEnabled = SpearheadConfig.StageConfig.drawStages ~= false self.isAutoStages = SpearheadConfig.StageConfig.autoStages ~= false self.startingStage = SpearheadConfig.StageConfig.startingStage or 1 self.maxMissionsPerStage = SpearheadConfig.StageConfig.maxMissionStage or 10 self.isDrawPreActivatedEnabled = SpearheadConfig.StageConfig.drawPreActivated ~= false self.AmountPreactivateStage = SpearheadConfig.StageConfig.preactivateStage or 1 self.briefingOnSpawnEnabled = SpearheadConfig.StageConfig.briefingOnSpawn ~= false _logger:info("Successfully created StageConfig Object") return self; end local config = new(); function StageConfig:getInstance() return config end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.configuration then ScriptGlobals.classes.configuration = {} end ScriptGlobals.classes.configuration.stageconfig = StageConfig end -- classes.configuration.stageconfig do -- classes.stageclasses.helpers.missioncommandshelper local Util = ScriptGlobals.classes.util.util local DcsUtil = ScriptGlobals.classes.util.dcsutil local Logger = ScriptGlobals.classes.util.logger local SpearheadEvents = ScriptGlobals.classes.spearhead_events local SupplyUnitsTracker = ScriptGlobals.classes.stageclasses.helpers.supplyunitstracker local SupplyConfigHelper = ScriptGlobals.classes.stageclasses.helpers.supplyconfighelper local StageConfig = ScriptGlobals.classes.configuration.stageconfig local MissionCommandsHelper = {} MissionCommandsHelper.__index = MissionCommandsHelper local function sortMissions(list, groupPos) table.sort(list, function(a, b) local distA = Util.VectorDistance2d(groupPos, a.location or { x = 0, y = 0 }) local distB = Util.VectorDistance2d(groupPos, b.location or { x = 0, y = 0 }) return distA < distB; end) end local instance = nil function MissionCommandsHelper.getOrCreate() if instance == nil then instance = setmetatable({}, MissionCommandsHelper) instance._logger = Logger.new("MissionCommandsHelper") instance._logger:info("Creating MissionCommandsHelper instance") instance.missionsByCode = {} instance.enabledByCode = {} instance.updateNeeded = false instance.pinnedByGroup = {} instance.lastUpdate = 0 instance._stageBriefings = {} instance._stageConfig = StageConfig:getInstance() instance._supplyUnitsTracker = SupplyUnitsTracker.getOrCreate() instance._supplyUnitsTracker:AddOnSupplyUnitEventListener( { enteredSupplyHub = function(_, unit) if unit == nil then return end instance.updateNeeded = true instance:updateCommandsForGroup(unit:getGroup():getID()) end, exitedSupplyHub = function(_, unit) instance.updateNeeded = true instance:updateCommandsForGroup(unit:getGroup():getID()) end, supplyUnitSpawned = function(_, unit) instance.updateNeeded = true instance:updateCommandsForGroup(unit:getGroup():getID()) end } ) instance.updateContinuous = function(selfA, time) if selfA.updateNeeded == false then return time + 10 end for _, unit in pairs(DcsUtil.getAllPlayerUnits()) do if unit and unit:isExist() then local group = unit:getGroup() if group then selfA:updateCommandsForGroup(group:getID()) end end end selfA.lastUpdate = timer.getTime() selfA.updateNeeded = false return time + 10 end timer.scheduleFunction(instance.updateContinuous, instance, timer.getTime() + 5) SpearheadEvents.AddOnPlayerEnterUnitListener(instance) end return instance end function MissionCommandsHelper:AddStageBriefing(stageName, briefing) self._stageBriefings[stageName] = briefing end function MissionCommandsHelper:RemoveStageBriefing(stageName) self._stageBriefings[stageName] = nil end function MissionCommandsHelper:AddMissionToCommands(mission) self._logger:debug("Adding mission to commands: [" .. mission.code .. "]" .. mission.name) self.missionsByCode[tostring(mission.code)] = mission self.enabledByCode[tostring(mission.code)] = true self.updateNeeded = true end function MissionCommandsHelper:RemoveMissionToCommands(mission) self.enabledByCode[tostring(mission.code)] = false self.updateNeeded = true end function MissionCommandsHelper:OnPlayerEntersUnit(unit) if unit then local group = unit:getGroup() if group then self:updateCommandsForGroup(group:getID()) if self._stageConfig.briefingOnSpawnEnabled == true then self:OverviewToGroup(group:getID()) end end end end local missionBriefingRequested = function(args) local mission = args.mission local groupID = args.groupId mission:ShowBriefing(groupID) end local pinMissionCommand = function(args) local self = args.self local groupID = args.groupId local mission = args.mission if mission then self:PinMission(mission, groupID) end end function MissionCommandsHelper:OverviewToGroup(groupID) local text = "Missions Overview\n\n" local group = DcsUtil.GetPlayerGroupByGroupID(groupID) local groupPos = { x = 0, y = 0 } if group then local pos = group:getUnit(1):getPosition().p groupPos = { x = pos.x, y = pos.z } end local function formatLine(mission) local distanceText = "?" if group then local lead = group:getUnit(1) if lead and lead:isExist() == true then local pos = lead:getPoint() local Vec2Pos = { x = pos.x, y = pos.z } local distance = Util.VectorDistance2d(Vec2Pos, mission.location) / 1852 distanceText = string.format("~%d", math.floor(distance)) end end return string.format("[%s]\t%s \t%s \t%s %% \t%s nM\n", mission.code, mission.missionTypeDisplay, mission.name, mission:PercentageComplete(), distanceText) end for _, briefing in pairs(self._stageBriefings) do text = text .. briefing .. "\n\n" end text = text .. "Primary Missions\n" local primaryMissions = {} for code, enabled in pairs(self.enabledByCode) do if enabled == true then local mission = self.missionsByCode[code] if mission and mission:getState() == "ACTIVE" and mission:GetPriority() == "primary" then table.insert(primaryMissions, mission) end end end sortMissions(primaryMissions, groupPos) for _, mission in pairs(primaryMissions) do text = text .. formatLine(mission) end text = text .. "\nSecondary Missions\n" local secondaryMissions = {} for code, enabled in pairs(self.enabledByCode) do if enabled == true then local mission = self.missionsByCode[code] if mission and mission:getState() == "ACTIVE" and mission:GetPriority() == "secondary" then table.insert(secondaryMissions, mission) end end end sortMissions(secondaryMissions, groupPos) for _, mission in pairs(secondaryMissions) do text = text .. formatLine(mission) end trigger.action.outTextForGroup(groupID, text, 20, true) end function MissionCommandsHelper:AddOverviewCommand(groupID) local MissionOverViewToGroup = function(args) args.self:OverviewToGroup(args.groupId) end local overviewToGroupCommandArgs = { self = self, groupId = groupID } missionCommands.removeItemForGroup(groupID, { "Overview" }) missionCommands.addCommandForGroup(groupID, "Overview", nil, MissionOverViewToGroup, overviewToGroupCommandArgs) end function MissionCommandsHelper:AddPinnedMission(groupID) local pinndedMission = self.pinnedByGroup[tostring(groupID)] missionCommands.removeItemForGroup(groupID, { "Pinned Mission" }) if pinndedMission and self.enabledByCode[tostring(pinndedMission.code)] == true then missionCommands.addCommandForGroup(groupID, "Pinned Mission", nil, missionBriefingRequested, { groupId = groupID, mission = pinndedMission }) end end function MissionCommandsHelper:updateCommandsForGroup(groupID) self._logger:debug("Updating commands for group: " .. tostring(groupID)) self:AddPinnedMission(groupID) self:AddOverviewCommand(groupID) self:ResetFolders(groupID) self:AddAllMissionCommandsToGroup(groupID) self:AddSupplyHubCommandsIfApplicable(groupID) self:AddCargoCommands(groupID) local clearView = function(id) trigger.action.outTextForGroup(id, "clearing...", 1, true) end missionCommands.removeItemForGroup(groupID, { "Clear View" }) missionCommands.addCommandForGroup(groupID, "Clear View", nil, clearView, groupID) missionCommands.removeItemForGroup(groupID, { "Refresh Missions" }) missionCommands.addCommandForGroup(groupID, "Refresh Missions", nil, function(refresh_mission_id) self._logger:debug("Manual refresh of missions for group: " .. tostring(refresh_mission_id)) self:updateCommandsForGroup(refresh_mission_id) end, groupID) end local folderNames = { primary = "Primary Missions", secondary = "Secondary Missions", supplyHub = "Supply Hub", cargo = "Cargo" } function MissionCommandsHelper:PinMission(mission, groupID) self._logger:debug("Pinning mission: [" .. mission.code .. "]" .. mission.name) self.pinnedByGroup[tostring(groupID)] = mission trigger.action.outTextForGroup(groupID, "Pinned mission: [" .. mission.code .. "]" .. mission.name, 3, true) self:updateCommandsForGroup(groupID) mission:ShowBriefing(groupID) end function MissionCommandsHelper:AddAllMissionCommandsToGroup(groupID) local perFolder = 9 local group = DcsUtil.GetPlayerGroupByGroupID(groupID) local groupPos = { x = 0, y = 0 } if group then local pos = group:getUnit(1):getPosition().p groupPos = { x = pos.x, y = pos.z } end do local count = 0 local path = { [1] = folderNames.primary } local primaryMissions = {} for code, enabled in pairs(self.enabledByCode) do if enabled == true then local mission = self.missionsByCode[code] if mission and mission:GetPriority() == "primary" then table.insert(primaryMissions, mission) end end end sortMissions(primaryMissions, groupPos) for _, mission in pairs(primaryMissions) do count = count + 1 if count <= perFolder then local copied = Util.deepCopyTable(path) self:addMissionCommands(groupID, copied, mission) else local name = "Next Menu ..." missionCommands.addSubMenuForGroup(groupID, name, path) path[#path + 1] = name count = 0 end end end do local count = 0 local path = { [1] = folderNames.secondary } local secondaryMissions = {} for code, enabled in pairs(self.enabledByCode) do if enabled == true then local mission = self.missionsByCode[code] if mission and mission:GetPriority() == "secondary" then table.insert(secondaryMissions, mission) end end end sortMissions(secondaryMissions, groupPos) for _, mission in pairs(secondaryMissions) do count = count + 1 if count <= perFolder then local copied = Util.deepCopyTable(path) self:addMissionCommands(groupID, copied, mission) else local name = "Next Menu ..." missionCommands.addSubMenuForGroup(groupID, name, path) path[#path + 1] = name count = 0 end end end end function MissionCommandsHelper:addMissionCommands(groupId, path, mission) if path then local group = DcsUtil.GetPlayerGroupByGroupID(groupId) local distance = "[?]" if group then local lead = group:getUnit(1) if lead and lead:isExist() == true then local pos = lead:getPoint() local Vec2Pos = { x = pos.x, y = pos.z } local dist = Util.VectorDistance2d(Vec2Pos, mission.location) / 1852 distance = "[" .. string.format("~%dnM", math.floor(dist)) .. "]" end end local missionFolderName = "[" .. mission.code .. "]" .. distance .. mission.name .. "(" .. mission.missionTypeDisplay .. ")" missionCommands.addSubMenuForGroup(groupId, missionFolderName, path) table.insert(path, missionFolderName) local missionBriefingRequestedArgs = { groupId = groupId, mission = mission } missionCommands.addCommandForGroup(groupId, "Briefing", path, missionBriefingRequested, missionBriefingRequestedArgs) local pinMissionCommandArgs = { self = self, groupId = groupId, mission = mission } missionCommands.addCommandForGroup(groupId, "Pin", path, pinMissionCommand, pinMissionCommandArgs) end end function MissionCommandsHelper:AddSupplyHubCommandsIfApplicable(groupID) if self._supplyUnitsTracker:IsGroupLeadInSupplyHub(groupID) ~= true then return end self._logger:debug("Adding supply hub commands for group: " .. tostring(groupID)) local group = DcsUtil.GetPlayerGroupByGroupID(groupID) if group == nil then return end local unit = group:getUnit(1) if unit == nil then return end local loadCargoCommand = function(params) local crateType = params.crateType local supplyUnitsTracker = params.supplyUnitsTracker if supplyUnitsTracker then supplyUnitsTracker:UnitRequestCrateLoading(params.groupID, crateType, params.commandHelper) end end local path = { [1] = folderNames.supplyHub } local farpParams1000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "FARP_CRATE_1000", supplyUnitsTracker = self._supplyUnitsTracker, commandHelper = self } missionCommands.addCommandForGroup(groupID, "Load FARP Crate (1000)", path, loadCargoCommand, farpParams1000) local farpParams2000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "FARP_CRATE_2000", supplyUnitsTracker = self._supplyUnitsTracker, commandHelper = self } missionCommands.addCommandForGroup(groupID, "Load FARP Crate (2000)", path, loadCargoCommand, farpParams2000) local samParms1000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "SAM_CRATE_2000", supplyUnitsTracker = self._supplyUnitsTracker, commandHelper = self } missionCommands.addCommandForGroup(groupID, "Load SAM Crate (1000)", path, loadCargoCommand, samParms1000) local samParms2000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "SAM_CRATE_2000", supplyUnitsTracker = self._supplyUnitsTracker, commandHelper = self } missionCommands.addCommandForGroup(groupID, "Load SAM Crate (2000)", path, loadCargoCommand, samParms2000) local airbaseParms2000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "AIRBASE_CRATE_2000", supplyUnitsTracker = self._supplyUnitsTracker, commandHelper = self } missionCommands.addCommandForGroup(groupID, "Airbase Crate (2000)", path, loadCargoCommand, airbaseParms2000) end function MissionCommandsHelper:AddCargoCommands(groupID) local group = DcsUtil.GetPlayerGroupByGroupID(groupID) if group == nil then return end local unit = group:getUnit(1) if unit == nil then return end local unloadCargoCommand = function(params) local unitID = params.unitID local crateType = params.crateType params.supplyUnitsTracker:UnloadRequested(unitID, crateType, params.commandHelper) end local cargo = self._supplyUnitsTracker:GetCargoInUnit(unit:getID()) if cargo then for cargoType, amount in pairs(cargo) do local cargoConfig = SupplyConfigHelper.getSupplyConfig(cargoType) if cargoConfig then for _ = 1, amount do local path = { [1] = folderNames.cargo } local params = { unitID = unit:getID(), crateType = cargoType, supplyUnitsTracker = self ._supplyUnitsTracker, commandHelper = self } missionCommands.addCommandForGroup(groupID, "Unload " .. cargoConfig.displayName, path, unloadCargoCommand, params) end end end end end function MissionCommandsHelper:addMissionFolders(groupId) missionCommands.addSubMenuForGroup(groupId, folderNames.primary) missionCommands.addSubMenuForGroup(groupId, folderNames.secondary) if self._supplyUnitsTracker:IsGroupLeadInSupplyHub(groupId) == true then missionCommands.addSubMenuForGroup(groupId, folderNames.supplyHub) end local group = DcsUtil.GetPlayerGroupByGroupID(groupId) if group == nil then return end local unit = group:getUnit(1) if unit == nil then return end local cargo = self._supplyUnitsTracker:GetCargoInUnit(unit:getID()) if cargo ~= nil then missionCommands.addSubMenuForGroup(groupId, folderNames.cargo) end end function MissionCommandsHelper:removeMissionFolders(groupId) missionCommands.removeItemForGroup(groupId, { folderNames.primary }) missionCommands.removeItemForGroup(groupId, { folderNames.secondary }) missionCommands.removeItemForGroup(groupId, { folderNames.supplyHub }) missionCommands.removeItemForGroup(groupId, { folderNames.cargo }) end function MissionCommandsHelper:ResetFolders(groupID) self:removeMissionFolders(groupID) self:addMissionFolders(groupID) end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.stageclasses then ScriptGlobals.classes.stageclasses = {} end if not ScriptGlobals.classes.stageclasses.helpers then ScriptGlobals.classes.stageclasses.helpers = {} end ScriptGlobals.classes.stageclasses.helpers.missioncommandshelper = MissionCommandsHelper end -- classes.stageclasses.helpers.missioncommandshelper do -- classes.configuration.capconfig local CapConfig = {}; CapConfig.__index = CapConfig function CapConfig.new() local self = setmetatable({}, CapConfig) if SpearheadConfig == nil then SpearheadConfig = {} end if SpearheadConfig.CapConfig == nil then SpearheadConfig.CapConfig = {} end if type(SpearheadConfig.CapConfig.enabled) ~= "boolean" then SpearheadConfig.CapConfig.enabled = true end local enabled = SpearheadConfig.CapConfig.enabled if enabled == nil then enabled = true end self._isEnabled = enabled self._minSpeed = (tonumber(SpearheadConfig.CapConfig.minSpeed) or 400) * 0.514444 self._maxSpeed = (tonumber(SpearheadConfig.CapConfig.maxSpeed) or 400) * 0.514444 self._minAlt = (tonumber(SpearheadConfig.CapConfig.minAlt) or 18000) * 0.3048 self._maxAlt = (tonumber(SpearheadConfig.CapConfig.maxAlt) or 28000) * 0.3048 self._minDurationOnStation = 1200 self._maxDurationOnStation = 2700 self._maxDeviationRange = (tonumber(SpearheadConfig.CapConfig.maxCommitRange) or 35) * 1852 self._rearmDelay = tonumber(SpearheadConfig.CapConfig.rearmDelay) or 600 self._repairDelay = tonumber(SpearheadConfig.CapConfig.repairDelay) or 600 self._deathDelay = tonumber(SpearheadConfig.CapConfig.deathDelay) or 1800 return self; end function CapConfig:isEnabled() return self._isEnabled end function CapConfig:getMinSpeed() return self._minSpeed end function CapConfig:getMaxSpeed() return self._maxSpeed end function CapConfig:getMinAlt() return self._minAlt end function CapConfig:getMaxAlt() return self._maxAlt end function CapConfig:getMinDurationOnStation() return self._minDurationOnStation end function CapConfig:getMaxDurationOnStation() return self._maxDurationOnStation end function CapConfig:getMaxDeviationRange() return self._maxDeviationRange end function CapConfig:getRearmDelay() return self._rearmDelay end function CapConfig:getRepairDelay() return self._repairDelay end function CapConfig:getDeathDelay() return self._deathDelay end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.configuration then ScriptGlobals.classes.configuration = {} end ScriptGlobals.classes.configuration.capconfig = CapConfig end -- classes.configuration.capconfig do -- classes.configuration.persistenceconfig local PersistenceConfig = {} function PersistenceConfig.new() local self = setmetatable({}, { __index = PersistenceConfig }) if not SpearheadConfig then SpearheadConfig = {} end if not SpearheadConfig.Persistence then SpearheadConfig.Persistence = {} end self._enabled = SpearheadConfig.Persistence.enabled == true self.directory = SpearheadConfig.Persistence.directory self.fileName = SpearheadConfig.Persistence.fileName return self end function PersistenceConfig:isEnabled() return self._enabled == true end function PersistenceConfig:getDirectory() return self.directory end function PersistenceConfig:getFileName() return self.fileName end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.configuration then ScriptGlobals.classes.configuration = {} end ScriptGlobals.classes.configuration.persistenceconfig = PersistenceConfig end -- classes.configuration.persistenceconfig do -- classes.helpers.spawnmanager local MizGroupsManager = ScriptGlobals.classes.helpers.mizgroupsmanager local Persistence = ScriptGlobals.classes.persistence.persistence local SpearheadEvents = ScriptGlobals.classes.spearhead_events local Util = ScriptGlobals.classes.util.util local SpawnManager = {} SpawnManager.__index = SpawnManager function SpawnManager.new(logger) local self = setmetatable({}, SpawnManager) self._logger = logger self._persistedUnits = {} return self end function SpawnManager:SpawnGroup(groupName, overrides, isGroupPersistant) local spawnData = MizGroupsManager.getSpawnTemplateData(groupName) if spawnData == nil then env.error("SpawnManager:SpawnGroup - No spawn template found for group: " .. groupName) return nil, false end if spawnData.isStatic == true then return self:SpawnStaticInternal(groupName, spawnData, overrides, isGroupPersistant), true else return self:SpawnGroupInternal(spawnData, overrides, isGroupPersistant), false end end function SpawnManager:DestroyGroup(groupName) if groupName == nil then return end if self:IsGroupStatic(groupName) == true then local object = StaticObject.getByName(groupName) if object ~= nil then object:destroy() else env.error("SpawnManager:DestroyGroup - Static object not found: " .. groupName) end else local group = Group.getByName(groupName) if group and group:isExist() then group:destroy() else env.error("SpawnManager:DestroyGroup - Group not found or does not exist: " .. groupName) end end end function SpawnManager:IsGroupStatic(groupName) local isStatic = MizGroupsManager.IsGroupStatic(groupName) if isStatic ~= nil then return isStatic end return StaticObject.getByName(groupName) ~= nil end function SpawnManager:OnUnitLost(object) local name = object:getName() if self._persistedUnits[name] then local heading = 0 local pos = object:getPosition() if pos then heading = math.atan2(pos.x.z, pos.x.x) if heading < 0 then heading = heading + 2*math.pi end heading = heading end Persistence.UnitKilled( name, object:getPoint(), heading, object:getTypeName() ) end end function SpawnManager:SpawnCorpsesOnly(groupName) if groupName == nil then return end end do function SpawnManager:SpawnGroupInternal(spawnData, override, isPersistent) if not spawnData then return end local country = spawnData.country if override then country = override.countryID or spawnData.country end local spawnTemplate = Util.deepCopyTable(spawnData.groupTemplate) if spawnTemplate and spawnTemplate["units"] then local units = spawnTemplate["units"] for _, unit in pairs(units) do local name = unit["name"] SpearheadEvents.addOnUnitLostEventListener(name, self) if override and override.emptyLoadouts == true then if unit["payload"] and unit["payload"]["pylons"] then local payload = unit["payload"] payload["pylons"] = {} end end if unit["parking"] then unit["parking_landing"] = unit["parking"] end if unit["parking_id"] then unit["parking_landing_id"] = unit["parking_id"] end end if override and override.route ~= nil then spawnTemplate["route"] = override.route end if override and override.uncontrolled ~= nil then spawnTemplate["uncontrolled"] = override.uncontrolled end local group = coalition.addGroup(country, spawnData.category, spawnTemplate) for _, unit in pairs(group:getUnits()) do self:CheckUnitAndReplaceIfPersistentDead(unit) if isPersistent == true then self._persistedUnits[unit:getName()] = true end SpearheadEvents.addOnUnitLostEventListener(unit:getName(), self) end return group end return nil end function SpawnManager:SpawnStaticInternal(groupName, spawnData, overrides, isPersistent) if not spawnData then return end local country = spawnData.country if overrides then country = overrides.countryID or spawnData.country end local spawnTemplate = Util.deepCopyTable(spawnData.groupTemplate) local persistentState = Persistence.UnitState(groupName) if persistentState then if persistentState.isDead == true then spawnTemplate["dead"] = true if persistentState.pos then spawnTemplate["x"] = persistentState.pos.x spawnTemplate["y"] = persistentState.pos.z if spawnTemplate["units"] and spawnTemplate["units"][1] then local units = spawnTemplate["units"] local firstUnit = units[1] firstUnit["x"] = persistentState.pos.x firstUnit["y"] = persistentState.pos.z firstUnit["heading"] = persistentState.heading or 0 end end end end coalition.addGroup(country, -1, spawnTemplate) local object = StaticObject.getByName(groupName) if object == nil then env.error("Could not retrieve spawned static object after spawning with name: " .. groupName) return nil end if isPersistent == true then self._persistedUnits[groupName] = true SpearheadEvents.addOnUnitLostEventListener(groupName, self) end return object end function SpawnManager:CheckUnitAndReplaceIfPersistentDead(unit) if not unit then return end local deadState = Persistence.UnitState(unit:getName()) if deadState and deadState.isDead == true then unit:destroy() local staticObject = { ["heading"] = deadState.heading or 0, ["type"] = deadState.type or unit:getTypeName(), ["name"] = unit:getName() .. "_dead", ["x"] = deadState.pos.x, ["y"] = deadState.pos.z, ["dead"] = true, } coalition.addStaticObject(unit:getCountry(), staticObject) end end end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.helpers then ScriptGlobals.classes.helpers = {} end ScriptGlobals.classes.helpers.spawnmanager = SpawnManager end -- classes.helpers.spawnmanager do -- classes.capclasses.detection.detectionmanager local DetectionManager = {} DetectionManager.__index = DetectionManager function DetectionManager.New(logger) local self = setmetatable({}, DetectionManager) self._logger = logger self._detectedUnits = { [tostring(coalition.side.RED)] = {}, [tostring(coalition.side.BLUE)] = {} } self._detectingUnits = { [tostring(coalition.side.RED)] = {}, [tostring(coalition.side.BLUE)] = {} } local updateDetectingUnitsTask = function(selfA, time) selfA:UpdateDetectingUnits() return time + 120 end timer.scheduleFunction(updateDetectingUnitsTask, self, timer.getTime() + 120) local updateDetected = function(selfA, time) selfA:UpdateDetectedUnits() return time + 10 end timer.scheduleFunction(updateDetected, self, timer.getTime() + 130) return self end function DetectionManager:IsUnitDetectedBy(unitName, coalitionSide) local coalitionString = tostring(coalitionSide) if not self._detectedUnits[coalitionString] then return false end if not self._detectedUnits[coalitionString][unitName] then return false end return timer.getTime() - self._detectedUnits[coalitionString][unitName] < 20 end function DetectionManager:GetDetectedUnitsBy(coalitionSide) local coalitionString = tostring(coalitionSide) if not self._detectedUnits[coalitionString] then return {} end local detectedUnits = {} for unitName, _ in pairs(self._detectedUnits[coalitionString]) do if self:IsUnitDetectedBy(unitName, coalitionSide) then table.insert(detectedUnits, unitName) end end return detectedUnits end function DetectionManager:UpdateDetectingUnits() self:UpdateDetectingInCoalition(coalition.side.RED) self:UpdateDetectingInCoalition(coalition.side.BLUE) self._logger:debug("Updated detecting units") end function DetectionManager:UpdateDetectedUnits() self:UpdateDetectedUnitsByCoalition(coalition.side.RED) self:UpdateDetectedUnitsByCoalition(coalition.side.BLUE) self._logger:debug("Updated detected units") end function DetectionManager:IsDetectingType(unit) return unit:hasAttribute("EWR") or unit:hasAttribute("AWACS") or unit:hasAttribute("SAM SR") end function DetectionManager:UpdateDetectingInCoalition(coalitionSide) local coalitionString = tostring(coalitionSide) local airGroups = coalition.getGroups(coalitionSide, Group.Category.AIRPLANE) for _, group in ipairs(airGroups) do if group and group:isExist() then for _, unit in ipairs(group:getUnits()) do if unit and self:IsDetectingType(unit) == true then self._detectingUnits[coalitionString][unit:getName()] = unit end end end end local groundGroups = coalition.getGroups(coalitionSide, Group.Category.GROUND) for _, group in ipairs(groundGroups) do if group and group:isExist() then for _, unit in ipairs(group:getUnits()) do if unit and self:IsDetectingType(unit) == true then self._detectingUnits[coalitionString][unit:getName()] = unit end end end end local ships = coalition.getGroups(coalitionSide, Group.Category.SHIP) for _, group in ipairs(ships) do if group and group:isExist() then for _, unit in ipairs(group:getUnits()) do if unit and self:IsDetectingType(unit) == true then self._detectingUnits[coalitionString][unit:getName()] = unit end end end end end function DetectionManager:UpdateDetectedUnitsByCoalition(coalitionSide) local coalitionString = tostring(coalitionSide) if not self._detectingUnits[coalitionString] then return end if not self._detectedUnits[coalitionString] then self._detectedUnits[coalitionString] = {} end for _, detectingUnit in pairs(self._detectingUnits[coalitionString]) do if detectingUnit and detectingUnit:isExist() then local controller = detectingUnit:getController() if controller then local targets = controller:getDetectedTargets(Controller.Detection.RADAR) for _, target in pairs(targets) do if target and target.object ~= nil and target.distance == true then local targetUnit = target.object local name = targetUnit:getName() self._detectedUnits[coalitionString][name] = timer.getTime() end end end end end end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.capclasses then ScriptGlobals.classes.capclasses = {} end if not ScriptGlobals.classes.capclasses.detection then ScriptGlobals.classes.capclasses.detection = {} end ScriptGlobals.classes.capclasses.detection.detectionmanager = DetectionManager end -- classes.capclasses.detection.detectionmanager do -- classes.capclasses.runwaybombing.runwaybombingtracker local SpearheadEvents = ScriptGlobals.classes.spearhead_events local Util = ScriptGlobals.classes.util.util local RunwayBombingTracker = {} RunwayBombingTracker.__index = RunwayBombingTracker function RunwayBombingTracker.new(logger) local self = setmetatable({}, RunwayBombingTracker) self._logger = logger SpearheadEvents.AddWeaponFiredListener(self) return self end function RunwayBombingTracker:OnWeaponFired(_, weapon, _) if weapon == nil then return end local desc = weapon:getDesc() local isTrackable = desc.category == Weapon.Category.BOMB or (desc.category == Weapon.Category.MISSILE and desc.missileCategory == Weapon.MissileCategory.CRUISE) if isTrackable == true then local weaponTrackingArgs = { weapon = weapon, self = self } timer.scheduleFunction(RunwayBombingTracker.trackWeaponTask, weaponTrackingArgs, timer.getTime() + 1) end end function RunwayBombingTracker:RegisterRunway(runway, strikeMission) if not self.trackedRunways then self.trackedRunways = {} end if not self.trackedRunways[runway] then self.trackedRunways[runway] = strikeMission end end function RunwayBombingTracker.trackWeaponTask(weaponTrackingArgs, time) local weapon = weaponTrackingArgs.weapon local self = weaponTrackingArgs.self if not weapon or weapon:isExist() == false then return nil end local pos = weapon:getPoint() local velocity = weapon:getVelocity() local ground = land.getHeight({ x = pos.x, y = pos.z }) local MpS = velocity.y if MpS > 0 then return time + 3 end local nextInterval = (pos.y - ground) / math.abs(MpS) if nextInterval < 1 then local impactPoint = { x = pos.x + velocity.x * nextInterval, y = pos.z + velocity.z * nextInterval } self:OnWeaponImpact(weapon:getDesc(), impactPoint) return nil end if nextInterval > 5 then nextInterval = 5 end return time + (nextInterval /2) end function RunwayBombingTracker:OnWeaponImpact(weaponDesc, impactPoint) self._logger:debug("RunwayBombingTracker:OnWeaponImpact") local warhead = weaponDesc.warhead local explosiveMass = (warhead.explosiveMass or warhead.shapedExplosiveMass) for _, strikeMission in pairs(self.trackedRunways) do local zone= strikeMission:GetRunwayZone() if Util.is3dPointInZone({ x = impactPoint.x, z = impactPoint.y, y = 0 }, zone) then strikeMission:RunwayHit(impactPoint, explosiveMass) end end end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.capclasses then ScriptGlobals.classes.capclasses = {} end if not ScriptGlobals.classes.capclasses.runwaybombing then ScriptGlobals.classes.capclasses.runwaybombing = {} end ScriptGlobals.classes.capclasses.runwaybombing.runwaybombingtracker = RunwayBombingTracker end -- classes.capclasses.runwaybombing.runwaybombingtracker do -- classes.capclasses.taskings.rtb local Util = ScriptGlobals.classes.util.util local RTB = {} function RTB.getAsMission(airbase, missionPoint, capConfig) return { id = "Mission", params = { airborne = true, route = { points = { [1] = RTB.getApproachPoint(airbase, missionPoint, capConfig), [2] = RTB.getInitialPoint(airbase), [3] = RTB.getLandingPoint(airbase) } } } } end local getRunwayIntoWindCourseRad = function(airbase) local activeCourse = nil local minAlignment = nil local runways = airbase:getRunways() local windVec = atmosphere.getWind(airbase:getPoint()) local mPerS = Util.vectorMagnitude(windVec) if mPerS > 0 then for i = 1, #runways do local runway = runways[i] do local rad = runway.course if rad < 0 then rad = math.abs(rad) else rad = 0 - rad end local runwayVec = {x = math.cos(rad), z = math.sin(rad), y = 0} local alignment = Util.vectorAlignment(windVec, runwayVec) if minAlignment == nil or alignment < minAlignment then activeCourse = i minAlignment = alignment end end do local degree = math.deg(runway.course) degree = (degree + 180) % 360 local rad = math.rad(degree) if rad < 0 then rad = math.abs(rad) else rad = 0 - rad end local runwayVec = {x = math.cos(rad), z = math.sin(rad), y = 0} local alignment = Util.vectorAlignment(windVec, runwayVec) if minAlignment == nil or alignment < minAlignment then activeCourse = i minAlignment = alignment end end end end local rad = runways[1].course if activeCourse ~= nil then rad = runways[activeCourse].course end if rad < 0 then return math.abs(rad) else return 0 - rad end end local function calcInitialPoint(airbase) local runwayCourseRad = getRunwayIntoWindCourseRad(airbase) local heading = math.deg(runwayCourseRad) local flipped = (heading + 180) % 360 local basePoint = airbase:getPoint() local basePointVec2 = {x = basePoint.x, y = basePoint.z} local point = Util.vectorMove(basePointVec2, flipped, 22000) return point, flipped end function RTB.getApproachPoint(airbase, missionPoint, capConfig) local initialPoint, headingFromRunway = calcInitialPoint(airbase) local pointA = Util.vectorMove(initialPoint, headingFromRunway - 45, 9000) local pointB = Util.vectorMove(initialPoint, headingFromRunway + 45, 9000) if Util.VectorDistance2d(missionPoint, pointA) > Util.VectorDistance2d(missionPoint, pointB) then pointA = pointB end return { alt = 3000, action = "Turning Point", alt_type = "BARO", speed = capConfig:getMinSpeed(), ETA = 0, ETA_locked = false, x = pointA.x, y = pointA.y, speed_locked = true, formation_template = "", type = "Turning Point", task = { id = "ComboTask", params = { tasks = {} } } } end function RTB.getInitialPoint(airbase) local point = calcInitialPoint(airbase) return { alt = 600, action = "Turning Point", alt_type = "BARO", speed = 180, ETA = 0, ETA_locked = false, x = point.x, y = point.y, speed_locked = true, formation_template = "", type = "Turning Point", task = { id = "ComboTask", params = { tasks = {} } } } end function RTB.getLandingPoint(airbase) local basePoint = airbase:getPoint() return { alt = basePoint.y, action = "Landing", alt_type = "BARO", speed = 70, ETA = 0, ETA_locked = false, x = basePoint.x, y = basePoint.z, speed_locked = true, formation_template = "", airdromeId = airbase:getID(), type = "Land", task = { id = "ComboTask", params = { tasks = {} } } } end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.capclasses then ScriptGlobals.classes.capclasses = {} end if not ScriptGlobals.classes.capclasses.taskings then ScriptGlobals.classes.capclasses.taskings = {} end ScriptGlobals.classes.capclasses.taskings.rtb = RTB end -- classes.capclasses.taskings.rtb do -- classes.capclasses.airgroups.airgroup local SpearheadEvents = ScriptGlobals.classes.spearhead_events local RTBMission = ScriptGlobals.classes.capclasses.taskings.rtb local Util = ScriptGlobals.classes.util.util local GlobalConfig = ScriptGlobals.classes.configuration.globalconfig local CustomDrawing = ScriptGlobals.classes.stageclasses.drawings.customdrawing local DrawingHelper = ScriptGlobals.classes.stageclasses.drawings.helper.drawinghelper local AirGroup = {} AirGroup.__index = AirGroup local globalConfig = GlobalConfig.New() function AirGroup:New(groupName, groupType, config, logger, spawnManager) self._groupName = groupName self._groupType = groupType self._isSpawned = false self._state = "UnSpawned" self._config = config self._logger = logger self._spawnManager = spawnManager self._routeDrawings = {} local group = Group.getByName(self._groupName) if group then SpearheadEvents.addOnGroupRTBListener(self._groupName, self) SpearheadEvents.addOnGroupRTBInTenListener(self._groupName, self) SpearheadEvents.addOnGroupOnStationListener(self._groupName, self) for _, unit in pairs(group:getUnits()) do SpearheadEvents.addOnUnitLostEventListener(unit:getName(), self) SpearheadEvents.addOnUnitLandEventListener(unit:getName(), self) end spawnManager:DestroyGroup(groupName) end end function AirGroup:GetState() return self._state end function AirGroup:GetName() return self._groupName end function AirGroup:MarkRearmComplete() self:Respawn(false) if self._state == "Rearming" then self:SetState("ReadyOnTheRamp") end end function AirGroup:SetMission(mission) self:SetState("InTransit") local group = Group.getByName(self._groupName) if group then local controller = group:getController() if controller then controller:setCommand({ id = 'Start', params = {} }) end end local setMissionDelayed = function(data, _) data.self:SetMissionPrivate(data.mission) end local data = { self = self, mission = mission } timer.scheduleFunction(setMissionDelayed, data, timer.getTime() + 5) end function AirGroup:SetMissionPrivate(mission) self:SetState("InTransit") local group = Group.getByName(self._groupName) if group and mission then group:getController():setTask(mission) self._logger:debug("mission - Task set for group: " .. self._groupName) if globalConfig:isDebugMenuEnabled() == true then if self._routeDrawing then self._routeDrawing:Remove() end local points = {} if mission and mission.params and mission.params.route and mission.params.route.points then local routePoints = mission.params.route.points for _, wp in pairs(routePoints) do if wp and wp.x and wp.y then table.insert(points, { x = wp.x, y = wp.y }) end end end local colorString = DrawingHelper.ColorTableToColorString({ 1, 0, 0, 1 }) self._routeDrawing = CustomDrawing.FromPoints(points, colorString, 5, 2) self._routeDrawing:Draw() end end end function AirGroup:SendRTB(airbase) self._logger:debug("AirGroup:SendRTB called for group: " .. self._groupName) self:SetState("Rtb") local group = Group.getByName(self._groupName) if group then local location = nil for _, unit in pairs(group:getUnits()) do if unit and unit:isExist() == true and unit:inAir() == true then location = unit:getPoint() break end end if location then local mission = RTBMission.getAsMission(airbase, { x= location.x, y= location.z }, self._config) group:getController():setTask(mission) self._logger:debug("AirGroup:SendRTB - Task set for group: " .. self._groupName) end end end function AirGroup:Spawn() if self._isSpawned then return end self:SpawnInternal(false) end function AirGroup:IsInAir() local group = Group.getByName(self._groupName) if group then local units = group:getUnits() for _, unit in pairs(units) do if unit:inAir() == true then return true end end end return false end function AirGroup:SpawnInternal(force, withoutLoadout) if withoutLoadout == nil then withoutLoadout = false end if self._isSpawned and force ~= true then return end local overrides = { emptyLoadouts = withoutLoadout, uncontrolled = true } local group, isStatic = self._spawnManager:SpawnGroup(self._groupName, overrides, false) if isStatic == true then self._state = "UnSpawned" return end group = group if group then self._group = group self._isSpawned = true self._initialSize = #group:getUnits() self._liveState = {} else self._logger:error("Failed to spawn group: " .. self._groupName) end if self._state == "UnSpawned" then self:SetState("ReadyOnTheRamp") end local function CheckLivenessTask(selfA, time) local interval = selfA:CheckLiveness() if not interval then return end return time + interval end if self._checkLivenessNumber then pcall(function() timer.removeFunction(self._checkLivenessNumber) end) end self._checkLivenessNumber = timer.scheduleFunction(CheckLivenessTask, self, timer.getTime() + 5) end function AirGroup:Respawn(withoutLoadout) self:SpawnInternal(true, withoutLoadout) end function AirGroup:SetState(state) if self._state == state then return end self._state = state self._logger:debug("AirGroup:State changed for group: " .. self._groupName .. " to state: " .. state) end function AirGroup:CheckLiveness() local isAlive = false local group = Group.getByName(self._groupName) if group and group:isExist() == true then for _, unit in pairs(group:getUnits()) do if unit and unit:isExist() == true and unit:getLife() > (unit:getLife0() * 0.3) then isAlive = true end end end if isAlive == false then self:SetState("Dead") self:CheckStateAndStartRepairRearm() return nil end return 10 end function AirGroup:OnLastUnitLanded() local checkGroupForRestart = function(data, time) local group = Group.getByName(data.self:GetName()) if not group then self:CheckStateAndStartRepairRearm() return end for _, unit in pairs(group:getUnits()) do if unit and unit:isExist() then local pos = unit:getPoint() if data.lastLocations[unit:getName()] == nil then data.lastLocations[unit:getName()] = pos return time + 5 end if pos and Util.VectorDistance3d(pos, data.lastLocations[unit:getName()]) > 10 then data.lastChangeTime = time end data.lastLocations[unit:getName()] = pos end end if data.lastChangeTime + 30 < time then local withoutLoadout = true data.self:Respawn(withoutLoadout) data.self:CheckStateAndStartRepairRearm() return end return time + 5 end local data = { self = self, lastLocations = {}, lastChangeTime = timer.getTime() } local group = Group.getByName(self._groupName) if group then for _, unit in pairs(group:getUnits()) do data.lastLocations[unit:getName()] = unit:getPoint() end end timer.scheduleFunction(checkGroupForRestart, data, timer.getTime() + 5) self._logger:debug("AirGroup:OnLastUnitLanded - Monitoring group: " .. self._groupName) end function AirGroup:CheckStateAndStartRepairRearm() self._logger:debug("AirGroup:CheckStateAndStartRepairRearm called for group: " .. self._groupName) local group = Group.getByName(self._groupName) local anyAlive = false local allAlive = true if group then for _, unit in pairs(group:getUnits()) do if unit and unit:isExist() == true and unit:getLife() > (unit:getLife0() * 0.3) then anyAlive = true else allAlive = false end end end if anyAlive == false then self:StartRespawn() return end if allAlive == false then self:StartRepair() return end self:StartRearm() end do function AirGroup:StartRespawn() self:SetState("Dead") local respawnTask = function(selfA, _) selfA:StartRepair() end local delay = self._config:getDeathDelay() if delay < 2 then delay = 2 end return timer.scheduleFunction(respawnTask, self, timer.getTime() + delay) end function AirGroup:StartRepair() self:SetState("Repairing") if self._isSpawned == false then self:Spawn() end local rearmTask = function(_, _) self:StartRearm() end local delay = self._config:getRepairDelay() if delay < 2 then delay = 2 end return timer.scheduleFunction(rearmTask, self, timer.getTime() + delay) end function AirGroup:StartRearm() self:SetState("Rearming") if self._isSpawned == false then self:Spawn() end local rearmDelay = self._config:getRearmDelay() if rearmDelay < 2 then rearmDelay = 2 end local rearmTask = function(selfA, _) selfA:MarkRearmComplete() end return timer.scheduleFunction(rearmTask, self, timer.getTime() + rearmDelay) end end do function AirGroup:OnUnitLost(unit) if unit == nil then return end self:CheckLiveness() end function AirGroup:OnGroupRTBInTen(groupName) if self._groupName == groupName then self._logger:debug("AirGroup:OnGroupRTBInTen called for group: " .. self._groupName) self:SetState("RtbInTen") end end function AirGroup:OnGroupRTB(groupName) if self._groupName == groupName then self._logger:debug("AirGroup:OnGroupRTB called for group: " .. self._groupName) self:SetState("Rtb") end end function AirGroup:OnUnitLanded(_, _) local anyInAir = false local group = Group.getByName(self._groupName) if group then for _, u in pairs(group:getUnits()) do if u and u:isExist() == true and u:inAir() == true then anyInAir = true break end end end if not anyInAir then self:OnLastUnitLanded() end end function AirGroup:OnGroupOnStation(groupName) if self._groupName == groupName then self:SetState("OnStation") end end end function AirGroup:Destroy() self._spawnManager:DestroyGroup(self._groupName) end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.capclasses then ScriptGlobals.classes.capclasses = {} end if not ScriptGlobals.classes.capclasses.airgroups then ScriptGlobals.classes.capclasses.airgroups = {} end ScriptGlobals.classes.capclasses.airgroups.airgroup = AirGroup end -- classes.capclasses.airgroups.airgroup do -- classes.capclasses.taskings.cap local Util = ScriptGlobals.classes.util.util local RTB = ScriptGlobals.classes.capclasses.taskings.rtb local CAP = {} local function GetCAPTargetTypes(attackHelos) local targetTypes = { [1] = "Planes", } if attackHelos then targetTypes[2] = "Helicopters" end return targetTypes end local function GetCAPPointFromTriggerZone(airBase, capZone) local furthestA = nil local furthestB = nil local furthestDistance = 0 for indexA, pointA in ipairs(capZone.verts) do for indexB, pointB in ipairs(capZone.verts) do if pointA ~= pointB then local distance = Util.VectorDistance2d(pointA, pointB) if distance > furthestDistance then furthestDistance = distance furthestA = indexA furthestB = indexB end end end end local baseVec3 = airBase:getPoint() local baseVec2 = { x = baseVec3.x, y = baseVec3.z } local pointA = capZone.verts[furthestA] local pointB = capZone.verts[furthestB] local furthest = pointA local closest = pointB local heading = Util.vectorHeadingFromTo(pointA, pointB) if Util.VectorDistance2d(baseVec2, pointB) > Util.VectorDistance2d(baseVec2, pointA) then furthest = pointB closest = pointA heading = Util.vectorHeadingFromTo(pointB, pointA) end local distance = furthestDistance if distance > 15000 then distance = distance - 10000 end return { width = 10000, furthest = furthest, closest = closest, legLength = distance, hotLegDir = math.rad(heading), orbitOriginPoint = closest } end local GetOutboundTask = function(airbase, capZone, capConfig) local airbaseVec3 = airbase:getPoint() local airbaseVec2 = { x = airbaseVec3.x, y = airbaseVec3.z } local heading = Util.vectorHeadingFromTo(airbaseVec2, capZone.location) local point = Util.vectorMove(airbaseVec2, heading, 18520) return { alt = 2000, action = "Fly Over Point", type = "Turning Point", alt_type = "BARO", speed = capConfig:getMinSpeed(), ETA = 0, ETA_locked = false, x = point.x, y = point.y, speed_locked = false, formation_template = "", task = { id = "ComboTask", params = { tasks = { [1] = { id = 'EngageTargets', params = { maxDist = capConfig:getMaxDeviationRange() + 10 * 1852, maxDistEnabled = capConfig:getMaxDeviationRange() > 0, -- required to check maxDist targetTypes = GetCAPTargetTypes(false), priority = 0 } }, } } } } end function CAP.getAsMissionFromAirbase(groupName, airbase, capZone, capConfig) local points = { [1] = GetOutboundTask(airbase, capZone, capConfig), [2] = GetOutboundTask(airbase, capZone, capConfig), [3] = CAP.getAsTasking(groupName, airbase, capZone, capConfig), [4] = RTB.getApproachPoint(airbase, capZone.location, capConfig), [5] = RTB.getInitialPoint(airbase), [6] = RTB.getLandingPoint(airbase) } local mission = { id = 'Mission', params = { airborne = true, route = { points = points } } } return mission end function CAP.getAsMission(groupName, airbase, capZone, capConfig) local points = { [1] = CAP.getAsTasking(groupName, airbase, capZone, capConfig), [2] = RTB.getApproachPoint(airbase, capZone.location, capConfig), [3] = RTB.getInitialPoint(airbase), [4] = RTB.getLandingPoint(airbase) } local mission = { id = 'Mission', params = { airborne = true, route = { points = points } } } return mission end function CAP.getAsTasking(groupName, airbase, capZone, capConfig) local duration = math.random(capConfig:getMinDurationOnStation(), capConfig:getMaxDurationOnStation()) or 1500 local capTaskingOptions = GetCAPPointFromTriggerZone(airbase, capZone) local durationBefore10 = duration - 600 if durationBefore10 < 0 then durationBefore10 = 0 end local durationAfter10 = 600 if duration < 600 then durationAfter10 = duration end local alt = math.random(capConfig:getMinAlt(), capConfig:getMaxAlt()) local speed = math.random(capConfig:getMinSpeed(), capConfig:getMaxSpeed()) return { alt = alt, action = "Turning Point", alt_type = "BARO", speed = speed, ETA = 0, ETA_locked = false, x = capTaskingOptions.closest.x, y = capTaskingOptions.closest.y, speed_locked = true, formation_template = "", task = { id = "ComboTask", params = { tasks = { [1] = { number = 1, auto = false, id = "WrappedAction", enabled = "true", params = { action = { id = "Script", params = { command = "pcall(GlobalCapCallBacks.PublishOnStation, \"" .. groupName .. "\")" } } } }, [2] = { id = 'EngageTargets', params = { maxDist = capConfig:getMaxDeviationRange(), maxDistEnabled = capConfig:getMaxDeviationRange() >= 0, -- required to check maxDist targetTypes = GetCAPTargetTypes(false), priority = 0 } }, [3] = { number = 3, auto = false, id = "ControlledTask", enabled = true, params = { task = { id = "Orbit", params = { altitude = alt, pattern = "Anchored", speed = speed, point = { x = capTaskingOptions.closest.x, y = capTaskingOptions.closest.y }, speedEdited = true, clockWise = false, hotLegDir = capTaskingOptions.hotLegDir, legLength = capTaskingOptions.legLength, width = capTaskingOptions.width, } }, stopCondition = { duration = durationBefore10, condition = "return GlobalCapCallBacks.NeedsRTBInTen(\"" .. groupName .. "\", 0.10)", } } }, [4] = { number = 4, auto = false, id = "WrappedAction", enabled = "true", params = { action = { id = "Script", params = { command = "pcall(GlobalCapCallBacks.PublishRTBInTen, \"" .. groupName .. "\")" } } } }, [5] = { number = 5, auto = false, id = "ControlledTask", enabled = true, params = { task = { id = "Orbit", params = { altitude = alt, pattern = "Circle", speed = speed, -- speedEdited = true, -- clockWise = false, -- hotLegDir = capTaskingOptions.hotLegDir, -- legLength = capTaskingOptions.legLength, -- width = capTaskingOptions.width, } }, stopCondition = { duration = durationAfter10, condition = "return GlobalCapCallBacks.IsBingoFuel(\"" .. groupName .. "\", 0.10)", } } }, [6] = { number = 6, auto = false, id = "WrappedAction", enabled = "true", params = { action = { id = "Script", params = { command = "pcall(GlobalCapCallBacks.PublishRTB, \"" .. groupName .. "\")" } } } } } } } } end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.capclasses then ScriptGlobals.classes.capclasses = {} end if not ScriptGlobals.classes.capclasses.taskings then ScriptGlobals.classes.capclasses.taskings = {} end ScriptGlobals.classes.capclasses.taskings.cap = CAP end -- classes.capclasses.taskings.cap do -- classes.capclasses.airgroups.capgroup local AirGroup = ScriptGlobals.classes.capclasses.airgroups.airgroup local CAP = ScriptGlobals.classes.capclasses.taskings.cap local Util = ScriptGlobals.classes.util.util local MissionEditorWarner = ScriptGlobals.classes.util.missioneditorwarnings local CapGroup = {} CapGroup.__index = CapGroup function CapGroup.New(groupName, config, logger, spawnManager) setmetatable(CapGroup, AirGroup) local self = setmetatable({}, CapGroup) AirGroup.New(self, groupName, "CAP", config, logger, spawnManager) self._targetZoneIdPerStage = {} self:InitWithName(groupName) return self end function CapGroup:IsBackup() return self._isBackup end function CapGroup:GetZoneIDWhenStageID(stageID) return self._targetZoneIdPerStage[tostring(stageID)] end function CapGroup:GetCurrentTargetZoneID() return self._currentTargetZoneID end function CapGroup:SendToZone(zone, targetZoneID, airbase) self._logger:debug("Airgroup " .. self._groupName .. " called to zone: " .. zone.name) self._currentTargetZoneID = targetZoneID local group = Group.getByName(self._groupName) local isInAir = false if group then local units = group:getUnits() for _, unit in pairs(units) do if unit:inAir() == true then isInAir = true break end end else self._logger:debug("CapGroup:SendToZone - Group not found: " .. self._groupName) return end if isInAir == true then local mission = CAP.getAsMission(self._groupName, airbase, zone, self._config) self:SetMission(mission) else local mission = CAP.getAsMissionFromAirbase(self._groupName, airbase, zone, self._config) if mission then self:SetMission(mission) else self._logger:warn("CapGroup:SendToZone - Mission could not be created for group: " .. self._groupName) return end end end function CapGroup:InitWithName(groupName) local split_string = Util.split_string(groupName, "_") local partCount = Util.tableLength(split_string) if partCount >= 3 then local configPart = split_string[2] local first = configPart:sub(1, 1) if first == "A" then self._isBackup = false configPart = string.sub(configPart, 2, #configPart) elseif first == "B" then configPart = string.sub(configPart, 2, #configPart) self._isBackup = true elseif first == "[" then self._isBackup = false else MissionEditorWarner.Add("Could not parse the CAP config for group: " .. groupName) return end local subsplit = Util.split_string(configPart, "|") if subsplit then for _, value in pairs(subsplit) do local keySplit = Util.split_string(value, "]") local targetZone = keySplit[2] local allActives = string.sub(keySplit[1], 2, #keySplit[1]) local commaSeperated = Util.split_string(allActives, ",") for _, childValue in pairs(commaSeperated) do local dashSeperated = Util.split_string(childValue, "-") if Util.tableLength(dashSeperated) > 1 then local from = tonumber(dashSeperated[1]) local till = tonumber(dashSeperated[2]) for i = from, till do if Util.strContains(targetZone, "A") == true then self._targetZoneIdPerStage[tostring(i)] = string.gsub(targetZone, "A", tostring(i)) else self._targetZoneIdPerStage[tostring(i)] = targetZone end end else if Util.strContains(targetZone, "A") == true then self._targetZoneIdPerStage[tostring(dashSeperated[1])] = string.gsub(targetZone, "A", tostring(dashSeperated[1])) else self._targetZoneIdPerStage[tostring(dashSeperated[1])] = targetZone end end end end end env.info("Capgroup parsed with table: " .. Util.toString(self._targetZoneIdPerStage)) else MissionEditorWarner.Add("CAP Group with name: " .. groupName .. "should have at least 3 parts, but has " .. partCount) end end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.capclasses then ScriptGlobals.classes.capclasses = {} end if not ScriptGlobals.classes.capclasses.airgroups then ScriptGlobals.classes.capclasses.airgroups = {} end ScriptGlobals.classes.capclasses.airgroups.capgroup = CapGroup end -- classes.capclasses.airgroups.capgroup do -- classes.capclasses.taskings.sweep local Util = ScriptGlobals.classes.util.util local RTB = ScriptGlobals.classes.capclasses.taskings.rtb local SWEEP = {} local function GetCAPTargetTypes(attackHelos) local targetTypes = { [1] = "Planes", } if attackHelos then targetTypes[2] = "Helicopters" end return targetTypes end local function GetCAPPointFromTriggerZone(airBase, capZone) local furthestA = nil local furthestB = nil local furthestDistance = 0 for indexA, pointA in ipairs(capZone.verts) do for indexB, pointB in ipairs(capZone.verts) do if pointA ~= pointB then local distance = Util.VectorDistance2d(pointA, pointB) if distance > furthestDistance then furthestDistance = distance furthestA = indexA furthestB = indexB end end end end local baseVec3 = airBase:getPoint() local baseVec2 = { x = baseVec3.x, y = baseVec3.z } local pointA = capZone.verts[furthestA] local pointB = capZone.verts[furthestB] local furthest = pointA local closest = pointB local heading = Util.vectorHeadingFromTo(pointA, pointB) if Util.VectorDistance2d(baseVec2, pointB) > Util.VectorDistance2d(baseVec2, pointA) then furthest = pointB closest = pointA heading = Util.vectorHeadingFromTo(pointB, pointA) end local distance = furthestDistance if distance > 15000 then distance = distance - 10000 end return { width = 10000, furthest = furthest, closest = closest, legLength = distance, hotLegDir = math.rad(heading), orbitOriginPoint = closest } end local GetOutboundTask = function(airbase, capZone, capConfig) local airbaseVec3 = airbase:getPoint() local airbaseVec2 = { x = airbaseVec3.x, y = airbaseVec3.z } local heading = Util.vectorHeadingFromTo(airbaseVec2, capZone.location) local point = Util.vectorMove(airbaseVec2, heading, 18520) return { alt = 2000, action = "Fly Over Point", type = "Turning Point", alt_type = "BARO", speed = capConfig:getMinSpeed(), ETA = 0, ETA_locked = false, x = point.x, y = point.y, speed_locked = false, formation_template = "", task = { id = "ComboTask", params = { tasks = { [1] = { id = 'EngageTargets', params = { maxDist = capConfig:getMaxDeviationRange(), maxDistEnabled = capConfig:getMaxDeviationRange() > 0, -- required to check maxDist targetTypes = GetCAPTargetTypes(false), priority = 0 } }, } } } } end function SWEEP.getAsMissionFromAirbase(groupName, airbase, capZone, capConfig) local pointA, pointB, pointC = SWEEP.getAsTasking(groupName, airbase, capZone, capConfig) local points = { [1] = GetOutboundTask(airbase, capZone, capConfig), [2] = GetOutboundTask(airbase, capZone, capConfig), [3] = pointA, [4] = pointB, [5] = pointC, [6] = RTB.getApproachPoint(airbase, capZone.location, capConfig), [7] = RTB.getInitialPoint(airbase), [8] = RTB.getLandingPoint(airbase) } local mission = { id = 'Mission', params = { airborne = true, route = { points = points } } } return mission end function SWEEP.getAsTasking(groupName, airbase, capZone, capConfig) local capTaskingOptions = GetCAPPointFromTriggerZone(airbase, capZone) local alt = math.random(capConfig:getMinAlt(), capConfig:getMaxAlt()) local speed = capConfig:getMaxSpeed() local pointA = { alt = alt, action = "Turning Point", alt_type = "BARO", speed = speed, ETA = 0, ETA_locked = false, x = capTaskingOptions.closest.x, y = capTaskingOptions.closest.y, speed_locked = true, formation_template = "", task = { id = "ComboTask", params = { tasks = { [1] = { number = 1, auto = false, id = "WrappedAction", enabled = "true", params = { action = { id = "Script", params = { command = "pcall(GlobalCapCallBacks.PublishOnStation, \"" .. groupName .. "\")" } } } }, [2] = { id = 'EngageTargets', params = { maxDist = capConfig:getMaxDeviationRange(), maxDistEnabled = capConfig:getMaxDeviationRange() >= 0, -- required to check maxDist targetTypes = GetCAPTargetTypes(false), priority = 0 } } } } } } local pointB = { alt = alt, action = "Turning Point", alt_type = "BARO", speed = speed, ETA = 0, ETA_locked = false, x = capTaskingOptions.furthest.x, y = capTaskingOptions.furthest.y, speed_locked = true, formation_template = "", task = { id = "ComboTask", params = { tasks = { [1] = { id = 'EngageTargets', params = { maxDist = capConfig:getMaxDeviationRange(), maxDistEnabled = capConfig:getMaxDeviationRange() >= 0, -- required to check maxDist targetTypes = GetCAPTargetTypes(false), priority = 0 } } } } } } local pointC = { alt = alt, action = "Turning Point", alt_type = "BARO", speed = speed, ETA = 0, ETA_locked = false, x = capTaskingOptions.closest.x, y = capTaskingOptions.closest.y, speed_locked = true, formation_template = "", task = { id = "ComboTask", params = { tasks = { [1] = { id = 'EngageTargets', params = { maxDist = capConfig:getMaxDeviationRange(), maxDistEnabled = capConfig:getMaxDeviationRange() >= 0, -- required to check maxDist targetTypes = GetCAPTargetTypes(false), priority = 0 } }, [2] = { number = 2, auto = false, id = "WrappedAction", enabled = "true", params = { action = { id = "Script", params = { command = "pcall(GlobalCapCallBacks.PublishRTB, \"" .. groupName .. "\")" } } } } } } } } return pointA, pointB, pointC end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.capclasses then ScriptGlobals.classes.capclasses = {} end if not ScriptGlobals.classes.capclasses.taskings then ScriptGlobals.classes.capclasses.taskings = {} end ScriptGlobals.classes.capclasses.taskings.sweep = SWEEP end -- classes.capclasses.taskings.sweep do -- classes.capclasses.airgroups.sweepgroup local AirGroup = ScriptGlobals.classes.capclasses.airgroups.airgroup local SWEEP = ScriptGlobals.classes.capclasses.taskings.sweep local Util = ScriptGlobals.classes.util.util local MissionEditorWarner = ScriptGlobals.classes.util.missioneditorwarnings local SweepGroup = {} SweepGroup.__index = SweepGroup function SweepGroup.New(groupName, config, logger, spawnManager) setmetatable(SweepGroup, AirGroup) local self = setmetatable({}, SweepGroup) AirGroup.New(self, groupName, "SWEEP", config, logger, spawnManager) self._targetZoneIdPerStage = {} self:InitWithName(groupName) return self end function SweepGroup:GetZoneIDWhenStageID(stageID) return self._targetZoneIdPerStage[tostring(stageID)] end function SweepGroup:GetCurrentTargetZoneID() return self._currentTargetZoneID end local setMissionDelayedTask = function(params, _) params.self:SetMissionPrivate(params.task) end function SweepGroup:SendToZone(zone, targetZoneID, airbase) self._logger:debug("Airgroup " .. self._groupName .. " called to zone: " .. zone.name) self._currentTargetZoneID = targetZoneID local mission = SWEEP.getAsMissionFromAirbase(self._groupName, airbase, zone, self._config) if mission then local params = { task = mission, self = self } local delay = math.random(120, 600) timer.scheduleFunction(setMissionDelayedTask, params, timer.getTime() + delay) else self._logger:error("SweepGroup:SendToZone - Mission could not be created for group: " .. self._groupName) end end function SweepGroup:SendToZoneInternal() end function SweepGroup:InitWithName(groupName) local split_string = Util.split_string(groupName, "_") local partCount = Util.tableLength(split_string) if partCount >= 3 then local configPart = split_string[2] configPart = string.sub(configPart, 2, #configPart) local subsplit = Util.split_string(configPart, "|") if subsplit then for _, value in pairs(subsplit) do local keySplit = Util.split_string(value, "]") local targetZone = keySplit[2] local allActives = string.sub(keySplit[1], 2, #keySplit[1]) local commaSeperated = Util.split_string(allActives, ",") for _, childValue in pairs(commaSeperated) do local dashSeperated = Util.split_string(childValue, "-") if Util.tableLength(dashSeperated) > 1 then local from = tonumber(dashSeperated[1]) local till = tonumber(dashSeperated[2]) for i = from, till do if Util.strContains(targetZone, "A") == true then self._targetZoneIdPerStage[tostring(i)] = string.gsub(targetZone, "A", tostring(i)) else self._targetZoneIdPerStage[tostring(i)] = targetZone end end else if Util.strContains(targetZone, "A") == true then self._targetZoneIdPerStage[tostring(dashSeperated[1])] = string.gsub(targetZone, "A", tostring(dashSeperated[1])) else self._targetZoneIdPerStage[tostring(dashSeperated[1])] = targetZone end end end end end env.info("SweepGroup parsed with table: " .. Util.toString(self._targetZoneIdPerStage)) else MissionEditorWarner.Add("SWEEP Group with name: " .. groupName .. "should have at least 3 parts, but has " .. partCount) end end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.capclasses then ScriptGlobals.classes.capclasses = {} end if not ScriptGlobals.classes.capclasses.airgroups then ScriptGlobals.classes.capclasses.airgroups = {} end ScriptGlobals.classes.capclasses.airgroups.sweepgroup = SweepGroup end -- classes.capclasses.airgroups.sweepgroup do -- classes.capclasses.taskings.intercept local Util = ScriptGlobals.classes.util.util local RTB = ScriptGlobals.classes.capclasses.taskings.rtb local INTERCEPT = {} function INTERCEPT.getMissionFromAirbase(groupName, interceptPoint, airbase, config, speed, alt) local airbaseVec3 = airbase:getPoint() local airbaseVec2 = { x = airbaseVec3.x, y = airbaseVec3.z } local heading = Util.vectorHeadingFromTo(airbaseVec2, interceptPoint) env.info("BLAAT: heading from runway to first point: " .. heading) local point = Util.vectorMove(airbaseVec2, heading, 3*1852) local pointA, pointB, pointC, pointD = INTERCEPT.getInterceptTaskPoint(groupName, point, interceptPoint, airbaseVec2, config, speed, alt) local points = { [1] = pointA, [2] = pointA, [3] = pointB, [4] = pointC, [5] = pointD, [6] = RTB.getApproachPoint(airbase, interceptPoint, config), [7] = RTB.getInitialPoint(airbase), [8] = RTB.getLandingPoint(airbase) } local mission = { id = 'Mission', params = { airborne = true, route = { points = points } } } return mission end function INTERCEPT.getMissionFromInAir(groupName, currentPosition, interceptPoint, airbase, config, speed, alt) local pointAirbasev3 = airbase:getPoint() local airbase2 = { x = pointAirbasev3.x, y = pointAirbasev3.z } local pointA, pointB, pointC, pointD = INTERCEPT.getInterceptTaskPoint(groupName, currentPosition, interceptPoint, airbase2, config, speed, alt) local points = { [1] = pointA, [2] = pointA, [3] = pointB, [4] = pointC, [5] = pointD, [6] = RTB.getApproachPoint(airbase, interceptPoint, config), [7] = RTB.getInitialPoint(airbase), [8] = RTB.getLandingPoint(airbase) } local mission = { id = 'Mission', params = { airborne = true, route = { points = points } } } return mission end function INTERCEPT.getUnitInterceptMissionFromAir(groupName, currentPoint, targetPosition, targetUnit, airbase, config, speed, alt) local pointAirbasev3 = airbase:getPoint() local airbase2 = { x = pointAirbasev3.x, y = pointAirbasev3.z } local pointA, pointB, pointC, pointD = INTERCEPT.getUnitInterceptTaskPoint(groupName, currentPoint, targetPosition, targetUnit, airbase2, config, speed, alt) local points = { [1] = pointA, [2] = pointA, [3] = pointB, [4] = pointC, [5] = pointD, [6] = RTB.getApproachPoint(airbase, currentPoint, config), [7] = RTB.getInitialPoint(airbase), [8] = RTB.getLandingPoint(airbase) } local mission = { id = 'Mission', params = { airborne = true, route = { points = points } } } return mission end function INTERCEPT.getInterceptTaskPoint(groupName, currentPoint, targetPoint, airbasePoint, config, speed, alt) local pointA = { alt = alt or config:getMinAlt(), action = "Turning Point", alt_type = "BARO", speed = speed or config:getMaxSpeed(), ETA = 0, ETA_locked = false, x = currentPoint.x, y = currentPoint.y, speed_locked = true, formation_template = "", task = { id = "ComboTask", params = { tasks = { id = 'EngageTargetsInZone', params = { point = targetPoint, zoneRadius = 10 * 1852, -- 10 NM, point will be updated, so target should be in this zone. targetTypes = { [1] = "Planes", }, priority = 0 } } } } } local pointB = { alt = alt or config:getMinAlt(), action = "Turning Point", alt_type = "BARO", speed = speed or config:getMaxSpeed(), ETA = 0, ETA_locked = false, x = targetPoint.x, y = targetPoint.y, speed_locked = true, formation_template = "", task = { id = "ComboTask", params = { tasks = { id = 'EngageTargetsInZone', params = { point = targetPoint, zoneRadius = 10 * 1852, -- 10 NM, point will be updated, so target should be in this zone. targetTypes = { [1] = "Planes", }, priority = 0 } } } } } local midPoint = { x = (targetPoint.x + airbasePoint.x) / 2, y = (targetPoint.y + airbasePoint.y) / 2 } local pointC = { alt = config:getMinAlt(), action = "Fly Over Point", alt_type = "BARO", speed = config:getMaxSpeed(), ETA = 0, ETA_locked = false, x = midPoint.x, y = midPoint.y, speed_locked = true, formation_template = "", task = { id = "ComboTask", params = {} } } local pointD = { alt = config:getMinAlt(), action = "Turning Point", alt_type = "BARO", speed = config:getMaxSpeed(), ETA = 0, ETA_locked = false, x = midPoint.x, y = midPoint.y, speed_locked = true, formation_template = "", task = { id = "ComboTask", params = { tasks = { [1] = { number = 1, auto = false, id = "WrappedAction", enabled = "true", params = { action = { id = "Script", params = { command = "pcall(GlobalCapCallBacks.PublishRTB, \"" .. groupName .. "\")" } } } } } } } } return pointA, pointB, pointC, pointD end function INTERCEPT.getUnitInterceptTaskPoint(groupName, currentPoint, targetPosition, targetUnit, airbasePoint, config, speed, alt) local pointA = { alt = alt or config:getMinAlt(), action = "Turning Point", alt_type = "BARO", speed = speed or config:getMaxSpeed(), ETA = 0, ETA_locked = false, x = currentPoint.x, y = currentPoint.y, speed_locked = true, formation_template = "", task = { id = "ComboTask", params = { tasks = { [1] = { id = 'EngageUnit', params = { unitId = targetUnit:getID(), groupAttack = true, priority = 0 } } } } } } local pointB = { alt = alt or config:getMinAlt(), action = "Turning Point", alt_type = "BARO", speed = speed or config:getMaxSpeed(), ETA = 0, ETA_locked = false, x = targetPosition.x, y = targetPosition.y, speed_locked = true, formation_template = "", task = { id = "ComboTask", params = { tasks = { [1] = { id = 'EngageUnit', params = { unitId = targetUnit:getID(), groupAttack = true, priority = 0 } } } } } } local midPoint = { x = (targetPosition.x + airbasePoint.x) / 2, y = (targetPosition.y + airbasePoint.y) / 2 } local pointC = { alt = config:getMinAlt(), action = "Fly Over Point", alt_type = "BARO", speed = config:getMaxSpeed(), ETA = 0, ETA_locked = false, x = midPoint.x, y = midPoint.y, speed_locked = true, formation_template = "", task = { id = "ComboTask", params = {} } } local pointD = { alt = config:getMinAlt(), action = "Turning Point", alt_type = "BARO", speed = config:getMaxSpeed(), ETA = 0, ETA_locked = false, x = midPoint.x, y = midPoint.y, speed_locked = true, formation_template = "", task = { id = "ComboTask", params = { tasks = { [1] = { number = 1, auto = false, id = "WrappedAction", enabled = "true", params = { action = { id = "Script", params = { command = "pcall(GlobalCapCallBacks.PublishRTB, \"" .. groupName .. "\")" } } } } } } } } return pointA, pointB, pointC, pointD end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.capclasses then ScriptGlobals.classes.capclasses = {} end if not ScriptGlobals.classes.capclasses.taskings then ScriptGlobals.classes.capclasses.taskings = {} end ScriptGlobals.classes.capclasses.taskings.intercept = INTERCEPT end -- classes.capclasses.taskings.intercept do -- classes.capclasses.airgroups.interceptgroup local AirGroup = ScriptGlobals.classes.capclasses.airgroups.airgroup local INTERCEPT = ScriptGlobals.classes.capclasses.taskings.intercept local Util = ScriptGlobals.classes.util.util local DcsUtil = ScriptGlobals.classes.util.dcsutil local MissionEditorWarner = ScriptGlobals.classes.util.missioneditorwarnings local InterceptGroup = {} InterceptGroup.__index = InterceptGroup function InterceptGroup.New(groupName, config, logger, detectionManager, spawnManager) setmetatable(InterceptGroup, AirGroup) local self = setmetatable({}, InterceptGroup) AirGroup.New(self, groupName, "INTERCEPT", config, logger, spawnManager) local group = Group.getByName(groupName) if not group then logger:error("InterceptGroup: Group " .. groupName .. " does not exist") return nil end local unit = group:getUnit(1) if unit then local desc = unit:getDesc() if desc["speedMax10K"] then self._maxSpeed = desc["speedMax10K"] * 0.75 self._logger:debug("InterceptGroup: Max speed for group " .. groupName .. " is set to " .. self._maxSpeed .. " m/s") end end self._coalitionSide = group:getCoalition() self._detectionManager = detectionManager self._config = config self._targetNames = {} self._targetZoneIdPerStage = {} self:InitWithName(groupName) return self end function InterceptGroup:SendToInterceptUnits(units, zoneName, homeAirbase) self._airbase = homeAirbase self._targetZoneName = zoneName self:SetTargetUnits(units) end function InterceptGroup:GetZoneIDWhenStageID(stageID) return self._targetZoneIdPerStage[tostring(stageID)] end function InterceptGroup:GetCurrentTargetZone() return self._targetZoneName end function InterceptGroup:SetTargetUnits(unitNames) self._targetNames = unitNames self:UpdateTask() if self._updateTaskID then pcall(function() timer.removeFunction(self._updateTaskID) end) end local updateContinous = function(selfA, time) local next = selfA:UpdateTask() if next then return time + next else return nil end end self._updateTaskID = timer.scheduleFunction(updateContinous, self, timer.getTime() + 30) end function InterceptGroup:RemoveTargetUnit(unit) if not unit then return end local name = unit:getName() for key, value in pairs(self._targetNames) do if value == "name" then table.remove(self._targetNames, key) self._logger:debug("InterceptGroup: Removed target unit " .. name .. " from group " .. self._groupName) break end end end function InterceptGroup:GetClosestTarget() local group = Group.getByName(self._groupName) if not group then return nil end local groupPoint = nil for _, unit in pairs(group:getUnits()) do if unit and unit:isExist() then groupPoint = unit:getPoint() break end end if not groupPoint then return nil end local closestUnit = nil local closestDistance = math.huge for _, targetName in pairs(self._targetNames) do local targetUnit = Unit.getByName(targetName) if targetUnit and targetUnit:isExist() then local pos = targetUnit:getPoint() local distance = Util.VectorDistance3d(groupPoint, pos) if distance < closestDistance then closestDistance = distance closestUnit = targetUnit end end end return closestUnit, groupPoint end function InterceptGroup:UpdateTask() local group = Group.getByName(self._groupName) if not group then return end local closestUnit, groupPoint = self:GetClosestTarget() if not closestUnit then return 15 end if not groupPoint then return 15 end local closesUnitVec = closestUnit:getPoint() local alt = closesUnitVec.y if alt < 1000 then alt = 1000 end local selfDetected = false for _, unit in pairs(group:getUnits()) do if unit and unit:isExist() then local controller = unit:getController() if controller then for _, detected in pairs(controller:getDetectedTargets(Controller.Detection.VISUAL, Controller.Detection.OPTIC, Controller.Detection.RADAR)) do if detected and detected.object and detected.distance == true then if detected.object:getName() == closestUnit:getName() then selfDetected = true break end end end end end end if DcsUtil.IsBingoFuel(self._groupName) then self._logger:debug("InterceptGroup: " .. self._groupName .. " is at bingo fuel, returning to base") self:SendRTB(self._airbase) return nil end if selfDetected and self:IsInAir() == true then if self._currentTargetName and self._currentTargetName == closestUnit:getName() then local distance = Util.VectorDistance3d(closestUnit:getPoint(), groupPoint) if distance < 30 * 1852 then self._logger:debug("InterceptGroup: " .. self._groupName .. " continues attacking target " .. closestUnit:getName()) return 15 end end self._logger:debug("InterceptGroup: " .. self._groupName .. " has detected target " .. closestUnit:getName() .. ", creating intercept mission") local vec3 = closestUnit:getPoint() local vec2 = { x = vec3.x, y = vec3.z } local groupPointVec2 = { x = groupPoint.x, y = groupPoint.z } local mission = INTERCEPT.getUnitInterceptMissionFromAir(self._groupName, groupPointVec2, vec2, closestUnit, self._airbase, self._config, self._maxSpeed, alt) self:SetMission(mission) self._currentTargetName = closestUnit:getName() return 15 else self._currentTargetName = nil end local speed = self._config:getMaxSpeed() local interceptPoint = self:GetInterceptPoint(groupPoint, speed, closestUnit) if interceptPoint == nil then return 30 end local mission if self:IsInAir() == true then mission = INTERCEPT.getMissionFromInAir( self._groupName, { x = groupPoint.x, y = groupPoint.z }, interceptPoint, self._airbase, self._config, self._maxSpeed, alt ) else mission = INTERCEPT.getMissionFromAirbase( self._groupName, interceptPoint, self._airbase, self._config, self._maxSpeed, alt ) end if mission then self:SetMission(mission) else self._logger:warn("InterceptGroup: Could not create mission for group " .. self._groupName) return 30 end return 15 end function InterceptGroup:GetInterceptPoint(originatingUnit, speed, targetUnit) if not targetUnit or not targetUnit:isExist() then return nil end local targetPos = targetUnit:getPoint() local targetVel = targetUnit:getVelocity() local relPos = { x = targetPos.x - originatingUnit.x, y = targetPos.y - originatingUnit.y, z = targetPos.z - originatingUnit.z } local relVel = { x = targetVel.x, y = targetVel.y, z = targetVel.z } local relPos2 = relPos.x^2 + relPos.y^2 + relPos.z^2 local relVel2 = relVel.x^2 + relVel.y^2 + relVel.z^2 local speed2 = speed^2 local dot = relPos.x * relVel.x + relPos.y * relVel.y + relPos.z * relVel.z local a = relVel2 - speed2 local b = 2 * dot local c = relPos2 local discriminant = b^2 - 4*a*c if discriminant < 0 or a == 0 then return { x = targetPos.x, y = targetPos.z } end local sqrtDisc = math.sqrt(discriminant) local t1 = (-b + sqrtDisc) / (2*a) local t2 = (-b - sqrtDisc) / (2*a) local t = math.min(t1, t2) if t < 0 then t = math.max(t1, t2) end if t < 0 then return { x = targetPos.x, y = targetPos.z } end local intercept = { x = targetPos.x + targetVel.x * t, y = targetPos.z + targetVel.z * t } return intercept end function InterceptGroup:InitWithName(groupName) local split_string = Util.split_string(groupName, "_") local partCount = Util.tableLength(split_string) if partCount >= 3 then local configPart = split_string[2] configPart = string.sub(configPart, 2, #configPart) local subsplit = Util.split_string(configPart, "|") if subsplit then for _, value in pairs(subsplit) do local keySplit = Util.split_string(value, "]") local targetZone = keySplit[2] local allActives = string.sub(keySplit[1], 2, #keySplit[1]) local commaSeperated = Util.split_string(allActives, ",") for _, childValue in pairs(commaSeperated) do local dashSeperated = Util.split_string(childValue, "-") if Util.tableLength(dashSeperated) > 1 then local from = tonumber(dashSeperated[1]) local till = tonumber(dashSeperated[2]) for i = from, till do if Util.strContains(targetZone, "A") == true then self._targetZoneIdPerStage[tostring(i)] = string.gsub(targetZone, "A", tostring(i)) else self._targetZoneIdPerStage[tostring(i)] = targetZone end end else if Util.strContains(targetZone, "A") == true then self._targetZoneIdPerStage[tostring(dashSeperated[1])] = string.gsub(targetZone, "A", tostring(dashSeperated[1])) else self._targetZoneIdPerStage[tostring(dashSeperated[1])] = targetZone end end end end end env.info("interceptGroup parsed with table: " .. Util.toString(self._targetZoneIdPerStage)) else MissionEditorWarner.Add("CAP Group with name: " .. groupName .. "should have at least 3 parts, but has " .. partCount) end end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.capclasses then ScriptGlobals.classes.capclasses = {} end if not ScriptGlobals.classes.capclasses.airgroups then ScriptGlobals.classes.capclasses.airgroups = {} end ScriptGlobals.classes.capclasses.airgroups.interceptgroup = InterceptGroup end -- classes.capclasses.airgroups.interceptgroup do -- classes.stageclasses.missions.basemissions.mission local MissionCommandsHelper = ScriptGlobals.classes.stageclasses.helpers.missioncommandshelper local DcsUtil = ScriptGlobals.classes.util.dcsutil local Util = ScriptGlobals.classes.util.util local GlobalConfig = ScriptGlobals.classes.configuration.globalconfig local Mission = {} Mission.__index = Mission function Mission.newSuper(self, zoneName, missionName, missionType, missionBriefing, priority, database, logger) self.zoneName = zoneName self.name = missionName self.missionType = missionType self._priority = priority self._state = "NEW" self._logger = logger self._database = database self._missionBriefing = missionBriefing self.code = tostring(database:GetNewMissionCode()) self._completeListeners = {} self.location = database:GetLocationForMissionZone(zoneName) self.missionTypeDisplay = self.missionType self._missionCommandsHelper = MissionCommandsHelper.getOrCreate() return true, "success" end function Mission:getState() return self._state end function Mission:GetPriority() return self._priority end function Mission:SpawnPersistedState() end function Mission:SpawnActive() end function Mission:UpdateState(_checkHealth, _messageIfDone) end function Mission:StartCheckingContinuous() end function Mission:PercentageComplete() return 0 end function Mission:ShowBriefing(groupId) local group = DcsUtil.GetPlayerGroupByGroupID(groupId) if group == nil then return end local unitType = DcsUtil.getUnitTypeFromGroup(group) local coords = DcsUtil.convertVec2ToUnitUsableType(self.location, unitType) if coords == nil then coords = "Could not make conversion" end self._logger:debug("Coords converted: " .. coords) local stateString = self:ToStateString() if self._missionBriefing == nil or self._missionBriefing == "" then self._missionBriefing = "No briefing available" end local briefing = self._missionBriefing briefing = Util.replaceString(briefing, "{{coords}}", coords) briefing = Util.replaceString(briefing, "{{ coords }}", coords) local text = "Mission [" .. self.code .. "] " .. self.name .. "\n \n" .. briefing .. " \n \n" .. stateString trigger.action.outTextForGroup(groupId, text, GlobalConfig:getBriefingTime()); end function Mission:AddMissionCompleteListener(listener) if type(listener) ~= "table" then return end table.insert(self._completeListeners, listener) end function Mission:NotifyMissionComplete() self._missionCommandsHelper:RemoveMissionToCommands(self) self._logger:info("Mission Completed: " .. self.zoneName) trigger.action.outText("Mission " .. self.name .. " [" .. self.code .. "] was completed successfully", 20) for _, listener in pairs(self._completeListeners) do pcall(function() listener:OnMissionComplete(self) end) end local _, _ = pcall(function() SpearheadAPI.Internal.notifyMissionComplete(self.zoneName) end) end function Mission:ForceMissionComplete() self._state = "COMPLETED" self:NotifyMissionComplete() end function Mission:MarkMissionAreaToGroup(_groupId) end function Mission:ToStateString() return "status: in progress" end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.stageclasses then ScriptGlobals.classes.stageclasses = {} end if not ScriptGlobals.classes.stageclasses.missions then ScriptGlobals.classes.stageclasses.missions = {} end if not ScriptGlobals.classes.stageclasses.missions.basemissions then ScriptGlobals.classes.stageclasses.missions.basemissions = {} end ScriptGlobals.classes.stageclasses.missions.basemissions.mission = Mission end -- classes.stageclasses.missions.basemissions.mission do -- classes.stageclasses.missions.runwaystrikemission local Mission = ScriptGlobals.classes.stageclasses.missions.basemissions.mission local Util = ScriptGlobals.classes.util.util local DcsUtil = ScriptGlobals.classes.util.dcsutil local DrawingHelper = ScriptGlobals.classes.stageclasses.drawings.helper.drawinghelper local RunwayStrikeMission = {} function RunwayStrikeMission.new(runway, airbaseName, database, logger, runwayBombingTracker) RunwayStrikeMission.__index = RunwayStrikeMission setmetatable(RunwayStrikeMission, Mission) local self = setmetatable({}, RunwayStrikeMission) self._airportName = airbaseName local missionBriefing = "Bomb runway " .. runway.Name .. " at " .. airbaseName .. "to delay the CAP effort" local success, error = Mission.newSuper(self, "noZone", runway.Name, "OCA", missionBriefing, "secondary", database, logger) self.runwayBombingTracker = runwayBombingTracker self._runway = runway self._runwayZone = self:RunwayToSpearheadZone(runway) self._repairInProgress = false self._minKilosForDamage = 100 local sections = self:ToSections(runway, 5) self._runwaySections = { sections[2], sections[3], sections[4], } if not success then logger:error("Failed to create RunwayBombingMission " .. runway.Name .. " => " .. error) return nil end self.location = { x= runway.position.x, y = runway.position.z } runwayBombingTracker:RegisterRunway(runway, self) return self end function RunwayStrikeMission:SpawnActive() self._missionCommandsHelper:AddMissionToCommands(self) self:Draw() self:UpdateState() end function RunwayStrikeMission:GetRunwayZone() return self._runwayZone end function RunwayStrikeMission:RunwayHit(impactPoint, explosiveMass) self._logger:debug("Runway hit: " .. self._airportName .. ":" .. self._runway.Name) for _, section in pairs(self._runwaySections) do local zone = self:SectionToSpearheadZone(section) if Util.is3dPointInZone({ x = impactPoint.x, z = impactPoint.y, y = 0 }, zone) then if section.kilosHit == nil then section.kilosHit = 0 end section.kilosHit = section.kilosHit + explosiveMass end end local updateState = function(selfA, _) selfA:UpdateState() end timer.scheduleFunction(updateState, self, timer.getTime() + 5) end function RunwayStrikeMission:UpdateState() self._logger:debug("Updating state of runway strike mission " .. self._airportName .. ":" .. self._runway.Name) self:Draw() for _, section in pairs(self._runwaySections) do if section.kilosHit > self._minKilosForDamage then self:StartRepair() self._missionCommandsHelper:RemoveMissionToCommands(self) break end end end local healthyAreaColor = { r=0, g=1, b=0, a=0.5 } local healthyAreaLineColor = { r=0, g=1, b=0, a=1 } local damagedAreaColor ={ r=1, g=165/255, b=0, a=0.5 } local damagedAreaLineColor = { r=1, g=165/255, b=0, a=1 } local destroyedAreaColor = { r=1, g=0, b=0, a=0.5 } local destroyedAreaLineColor = { r=1, g=0, b=0, a=1 } function RunwayStrikeMission:Draw() local function drawSection(runwaySection) local lineColor = healthyAreaLineColor local fillColor = healthyAreaColor local orangeDamage = self._minKilosForDamage - 100 if orangeDamage < 0 then orangeDamage = self._minKilosForDamage * 0.8 end if runwaySection.kilosHit > self._minKilosForDamage then lineColor = destroyedAreaLineColor fillColor = destroyedAreaColor elseif runwaySection.kilosHit > orangeDamage then lineColor = damagedAreaLineColor fillColor = damagedAreaColor end if runwaySection.drawID == nil then local zone = self:SectionToSpearheadZone(runwaySection) local drawObject = { primitiveType = "Polygon", polygonMode = "free", mapX = 0, mapY = 0, points = zone.verts, name = zone.name, fillColorString = DrawingHelper.ColorTableToColorString(fillColor), colorString = DrawingHelper.ColorTableToColorString(lineColor), style = "solid", thickness = 1, visible = true, } runwaySection.drawID = DrawingHelper.Draw(drawObject) else for _, drawID in pairs(runwaySection.drawID) do DcsUtil.SetFillColor(drawID, fillColor) DcsUtil.SetLineColor(drawID, lineColor) end end end for _, section in pairs(self._runwaySections) do drawSection(section) end end function RunwayStrikeMission:SectionToSpearheadZone(section) return { location = { x = self._runway.position.x, y = self._runway.position.z }, radius = self._runway.width, name = self._runway.Name, verts = section.corners, zone_type = "Polygon", } end function RunwayStrikeMission:StartRepair() if self._repairInProgress == true then return end self._repairInProgress = true self._logger:debug("Starting repair of runway strike mission " .. self._airportName .. ":" .. self._runway.Name) local repairTask = function (selfA, time) local interval = selfA:DoRepairCycle() if interval == nil then return nil end return time + interval end timer.scheduleFunction(repairTask, self, timer.getTime() + 5) end function RunwayStrikeMission:DoRepairCycle() local interval = 5 local repairPerSecond = 5 self._logger:debug("Repair cycle for" .. self._airportName .. ":" .. self._runway.Name) local isHealed = true for _, section in pairs(self._runwaySections) do section.kilosHit = section.kilosHit - interval * repairPerSecond if section.kilosHit > self._minKilosForDamage * 0.1 then self:AddOrUpdateRepairStatics(section) isHealed = false else self:RemoveRepairStatics(section) end end self:Draw() if isHealed == true then self._repairInProgress = false self:FullRepairRunway() self._logger:debug("Repair complete for runway strike mission " .. self._airportName .. ":" .. self._runway.Name) return nil end return interval end local counter = 1 local repairStaticConfigs = { [1] = { [1] = { ["category"] = "Unarmed", ["type"] = "ZIL-135", ["y"] = -4, ["x"] = 10, ["heading"] = 6.2133721370998, }, [2] = { ["category"] = "Unarmed", ["type"] = "Tigr_233036", ["y"] = 0, ["x"] = 10, ["heading"] = 6.2133721370998, }, [3] = { ["category"] = "Unarmed", ["type"] = "Infantry AK ver3", ["y"] = 10, ["x"] = 7, ["heading"] = 4.4331363000656, }, [4] = { ["category"] = "Unarmed", ["type"] = "Infantry AK ver3", ["y"] = 12, ["x"] = 1, ["heading"] = 4.4331363000656, }, [5] = { ["category"] = "Unarmed", ["type"] = "Infantry AK ver3", ["y"] = -10, ["x"] = -2, ["heading"] = 4.1538836197465, }, [6] = { ["category"] = "Unarmed", ["type"] = "CV_59_Large_Forklift", ["y"] = -11, ["x"] = 0, ["heading"] = 1.535889741755, }, [7] = { ["category"] = "Unarmed", ["type"] = "ZiL-131 APA-80", ["y"] = 11, ["x"] = 5, ["heading"] = 0.62831853071796, }, [8] = { ["category"] = "Unarmed", ["type"] = "CV_59_NS60", ["y"] = -4, ["x"] = -15, ["heading"] = 0.62831853071796, } }, [2] = { [1] = { ["category"] = "Air Defence", ["type"] = "generator_5i57", ["y"] = 2.7889551542015, ["x"] = -4.7698247930386, ["heading"] = 4.0666171571468, }, [2] = { ["category"] = "Infantry", ["type"] = "Infantry AK ver3", ["y"] = 3.639392285097, ["x"] = 1.4300755972836, ["heading"] = 5.0440015382636, }, [3] = { ["category"] = "Infantry", ["type"] = "Infantry AK ver3", ["y"] = 3.5139072826724, ["x"] = -0.32671443666074, ["heading"] = 6.16101225954, }, [4] = { ["category"] = "Unarmed", ["type"] = "ATMZ-5", ["y"] = -6.9556010010953, ["x"] = 3.007681753102, ["heading"] = 4.0666171571468, }, [5] = { ["category"] = "Unarmed", ["type"] = "GAZ-66", ["y"] = 5.9304017026008, ["x"] = 0.80323129595153, ["heading"] = 6.2308254296198, } } } function RunwayStrikeMission:AddOrUpdateRepairStatics(section) if Util.tableLength(section.repairGroups) > 0 then return end local location = { --Can randomise a little later x = section.center.x, y = section.center.y, } local repairGroup = Util.randomFromList(repairStaticConfigs) for _, repairStatic in pairs(repairGroup) do repairStatic.x = location.x + repairStatic.x repairStatic.y = location.y + repairStatic.y repairStatic.hidden = true repairStatic.name = "runway_repairunit_" .. counter counter = counter + 1 coalition.addStaticObject(country.id.RUSSIA, repairStatic) table.insert(section.repairGroups, repairStatic.name) end end function RunwayStrikeMission:RemoveRepairStatics(section) if Util.tableLength(section.repairGroups) == 0 then return end for _, repairGroup in pairs(section.repairGroups) do local static = StaticObject.getByName(repairGroup) if static then static:destroy() end end section.repairGroups = {} end function RunwayStrikeMission:FullRepairRunway() for _, section in pairs(self._runwaySections) do section.kilosHit = 0 self:RemoveRepairStatics(section) end local minX = self._runwayZone.verts[1].x local minY = self._runwayZone.verts[1].y local maxX = self._runwayZone.verts[1].x local maxY = self._runwayZone.verts[1].y for _, vert in pairs(self._runwayZone.verts) do if vert.x < minX then minX = vert.x end if vert.x > maxX then maxX = vert.x end if vert.y < minY then minY = vert.y end if vert.y > maxY then maxY = vert.y end end local box = { id = world.VolumeType.BOX, params = { min = { x = minX, z = minY, y = 0 }, max = { x = maxX, z = maxY, y = 10000 }, } } world.removeJunk(box) end function RunwayStrikeMission:ToSections(runway, numSections) local sections = {} local center = runway.position local heading = runway.course local width = runway.width local length = runway.length / numSections if heading < 0 then heading = math.abs(heading) else heading = 0 - heading end local cosH = math.cos(heading) local sinH = math.sin(heading) for i = 0, numSections - 1 do local sectionCenterOffset = (i - (numSections / 2) + 0.5) * length local sectionCenter = { x = center.x + sectionCenterOffset * cosH, y = center.z + sectionCenterOffset * sinH } local halfWidth = width / 2 local halfLength = length / 2 local corners = { { x = sectionCenter.x + (-halfLength * cosH - halfWidth * sinH), y = sectionCenter.y + (-halfLength * sinH + halfWidth * cosH) }, { x = sectionCenter.x + (-halfLength * cosH + halfWidth * sinH), y = sectionCenter.y + (-halfLength * sinH - halfWidth * cosH) }, { x = sectionCenter.x + (halfLength * cosH + halfWidth * sinH), y = sectionCenter.y + (halfLength * sinH - halfWidth * cosH) }, { x = sectionCenter.x + (halfLength * cosH - halfWidth * sinH), y = sectionCenter.y + (halfLength * sinH + halfWidth * cosH) } } local section = { center = sectionCenter, corners = corners, kilosHit = 0, repairGroups = {}, drawID = {}, } table.insert(sections, section) end return sections end function RunwayStrikeMission:RunwayToSpearheadZone(runway) local radHeading = runway.course if radHeading < 0 then radHeading = math.abs(radHeading) else radHeading = 0 - radHeading end local cosH = math.cos(radHeading) local sinH = math.sin(radHeading) local halfWidth = runway.width / 2 local halfHeight = runway.length / 2 local corners = { { x = runway.position.x + (-halfHeight * cosH - halfWidth * sinH), y = runway.position.z + (-halfHeight * sinH + halfWidth * cosH) }, { x = runway.position.x + (-halfHeight * cosH + halfWidth * sinH), y = runway.position.z + (-halfHeight * sinH - halfWidth * cosH) }, { x = runway.position.x + (halfHeight * cosH + halfWidth * sinH), y = runway.position.z + (halfHeight * sinH - halfWidth * cosH) }, { x = runway.position.x + (halfHeight * cosH - halfWidth * sinH), y = runway.position.z + (halfHeight * sinH + halfWidth * cosH) } } return { location = { x = runway.position.x, y = runway.position.z }, radius = runway.width, name = runway.Name, verts = corners, zone_type = "Polygon", } end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.stageclasses then ScriptGlobals.classes.stageclasses = {} end if not ScriptGlobals.classes.stageclasses.missions then ScriptGlobals.classes.stageclasses.missions = {} end ScriptGlobals.classes.stageclasses.missions.runwaystrikemission = RunwayStrikeMission end -- classes.stageclasses.missions.runwaystrikemission do -- classes.capclasses.capairbase local Util = ScriptGlobals.classes.util.util local CapGroup = ScriptGlobals.classes.capclasses.airgroups.capgroup local SweepGroup = ScriptGlobals.classes.capclasses.airgroups.sweepgroup local InterceptGroup = ScriptGlobals.classes.capclasses.airgroups.interceptgroup local SpearheadEvents = ScriptGlobals.classes.spearhead_events local RunwayStrikeMission = ScriptGlobals.classes.stageclasses.missions.runwaystrikemission local CapBase = {} local CheckStateContinuous = function(self, time) self:CheckAndScheduleCAP() self:CheckAndScheduleSweep() self:CheckAndScheduleIntercept() return time + 15 end function CapBase.new(airbaseName, database, logger, capConfig, runwayBombingTracker, detectionManager, spawnManager) CapBase.__index = CapBase local self = setmetatable({}, { __index = CapBase }) self.runwayBombingTracker = runwayBombingTracker self.runwayStrikeMissions = {} self.airbaseName = airbaseName self.logger = logger self.activeStage = 0 self.capConfig = capConfig self.database = database self.capGroupsByName = {} self.sweepGroupsByName = {} self.interceptGroupsByName = {} self.detectionManager = detectionManager local baseData = database:getAirbaseDataForZone(airbaseName) if baseData and baseData.CapGroups then for _, name in pairs(baseData.CapGroups) do local capGroup = CapGroup.New(name, capConfig, logger, spawnManager) if capGroup then self.capGroupsByName[name] = capGroup end end end if baseData and baseData.SweepGroups then for _, name in pairs(baseData.SweepGroups) do local sweepGroup = SweepGroup.New(name, capConfig, logger, spawnManager) if sweepGroup then self.sweepGroupsByName[name] = sweepGroup end end end if baseData and baseData.InterceptGroups then for _, name in pairs(baseData.InterceptGroups) do local interceptGroup = InterceptGroup.New(name, capConfig, logger, detectionManager, spawnManager) if interceptGroup then self.interceptGroupsByName[name] = interceptGroup end end end local capFlights = Util.tableLength(self.capGroupsByName) local sweepFlights = Util.tableLength(self.sweepGroupsByName) local interceptFlights = Util.tableLength(self.interceptGroupsByName) logger:info(airbaseName .. " : " .. capFlights .." CAP | " .. sweepFlights .. " SWEEP | " .. interceptFlights .. " INTERCEPT") self:CreateRunwayStrikeMission(database) SpearheadEvents.AddStageNumberChangedListener(self) timer.scheduleFunction(CheckStateContinuous, self, timer.getTime() + 15) return self end function CapBase:CreateRunwayStrikeMission(database) local airbase = Airbase.getByName(self.airbaseName) if not airbase then self.logger:debug("Could not find a airbase with name to create runway mission" .. self.airbaseName) return end for _, runway in pairs(airbase:getRunways()) do if runway then self.logger:debug("Runway " .. runway.Name .. " at airbase " .. self.airbaseName .. " with heading " .. runway.course .. " and length " .. runway.length .. " and width " .. runway.width) local mission = RunwayStrikeMission.new(runway, self.airbaseName, database, self.logger, self.runwayBombingTracker) self.runwayStrikeMissions[runway.Name] = mission end end end function CapBase:SpawnIfApplicable() self.logger:debug("Check spawns for airbase " .. self.airbaseName) for _, capGroup in pairs(self.capGroupsByName) do local targetStage = capGroup:GetZoneIDWhenStageID(tostring(self.activeStage)) if targetStage ~= nil and capGroup:GetState() == "UnSpawned" then capGroup:Spawn() end end for _, sweepGroup in pairs(self.sweepGroupsByName) do local targetStage = sweepGroup:GetZoneIDWhenStageID(tostring(self.activeStage)) if targetStage ~= nil and sweepGroup:GetState() == "UnSpawned" then sweepGroup:Spawn() end end for _, interceptGroup in pairs(self.interceptGroupsByName) do local targetStage = interceptGroup:GetZoneIDWhenStageID(tostring(self.activeStage)) if targetStage ~= nil and interceptGroup:GetState() == "UnSpawned" then interceptGroup:Spawn() end end end function CapBase:CheckAndScheduleCAP() self.logger:debug("Check taskings for airbase " .. self.airbaseName) local countPerStage = {} local requiredPerStage = {} local airbase = Airbase.getByName(self.airbaseName) if not airbase then return nil end local activeStageID = tostring(self.activeStage) for _, group in pairs(self.capGroupsByName) do if group:IsBackup() == true then local state = group:GetState() if state == "InTransit" or state == "OnStation" or state == "RtbInTen" then local supposedTargetZoneID = group:GetZoneIDWhenStageID(activeStageID) local currentTargetZone = group:GetCurrentTargetZoneID() if supposedTargetZoneID == nil then self.logger:debug("CapGroup " .. group:GetName() .. " has no target zone for stage " .. activeStageID) group:SendRTB(airbase) else if supposedTargetZoneID and supposedTargetZoneID ~= currentTargetZone then if state == "RtbInTen" then self.logger:debug("CapGroup " .. group:GetName() .. " is RTB in 10 minutes, sending to RTB already") group:SendRTB(airbase) else local triggerZone = self.database:GetCapZoneForZoneID(supposedTargetZoneID) if triggerZone then group:SendToZone(triggerZone, supposedTargetZoneID, airbase) else self.logger:debug("CapGroup " .. group:GetName() .. " has no trigger zone for stage " .. activeStageID) group:SendRTB(airbase) end end end if countPerStage[supposedTargetZoneID] == nil then countPerStage[supposedTargetZoneID] = 0 end if supposedTargetZoneID == group:GetCurrentTargetZoneID() and (state == "OnStation" or state =="InTransit") then countPerStage[supposedTargetZoneID] = countPerStage[supposedTargetZoneID] + 1 end end end end end for _, group in pairs(self.capGroupsByName) do if group:IsBackup() == false then local state = group:GetState() local supposedZone = group:GetZoneIDWhenStageID(activeStageID) if supposedZone then if requiredPerStage[supposedZone] == nil then requiredPerStage[supposedZone] = 0 end if countPerStage[supposedZone] == nil then countPerStage[supposedZone] = 0 end requiredPerStage[supposedZone] = requiredPerStage[supposedZone] + 1 if state == "ReadyOnTheRamp" then if countPerStage[supposedZone] < requiredPerStage[supposedZone] then local triggerZone = self.database:GetCapZoneForZoneID(supposedZone) if triggerZone then group:SendToZone(triggerZone, supposedZone, airbase) end countPerStage[supposedZone] = countPerStage[supposedZone] + 1 end elseif state == "InTransit" or state == "OnStation" then if supposedZone ~= group:GetCurrentTargetZoneID() then if countPerStage[supposedZone] < requiredPerStage[supposedZone] then local triggerZone = self.database:GetCapZoneForZoneID(supposedZone) if triggerZone then group:SendToZone(triggerZone, supposedZone, airbase) else group:SendRTB(airbase) end end end countPerStage[supposedZone] = countPerStage[supposedZone] + 1 elseif state == "RtbInTen" and supposedZone ~= group:GetCurrentTargetZoneID() then group:SendRTB(airbase) end else if state == "InTransit" or state == "OnStation" or state == "RtbInTen" then group:SendRTB(airbase) end end end end for _, group in pairs(self.capGroupsByName) do if group:IsBackup() == true then if group:GetState() == "ReadyOnTheRamp" then local supposedZone = group:GetZoneIDWhenStageID(activeStageID) if supposedZone then if countPerStage[supposedZone] == nil then countPerStage[supposedZone] = 0 end if requiredPerStage[supposedZone] == nil then requiredPerStage[supposedZone] = 0 end if countPerStage[supposedZone] < requiredPerStage[supposedZone] then local triggerZone = self.database:GetCapZoneForZoneID(supposedZone) if triggerZone then group:SendToZone(triggerZone, supposedZone, airbase) end countPerStage[supposedZone] = countPerStage[supposedZone] + 1 end end end end end end function CapBase:CheckAndScheduleSweep() self.logger:debug("Check sweep taskings for airbase " .. self.airbaseName) local airbase = Airbase.getByName(self.airbaseName) if not airbase then return nil end local activeStageID = tostring(self.activeStage) for _, group in pairs(self.sweepGroupsByName) do local targetZoneID = group:GetZoneIDWhenStageID(activeStageID) if targetZoneID then local triggerZone = self.database:GetCapZoneForZoneID(targetZoneID) if triggerZone then if group:GetState() == "ReadyOnTheRamp" then group:SendToZone(triggerZone, targetZoneID, airbase) end else self.logger:debug("SweepGroup " .. group:GetName() .. " has no trigger zone for stage " .. activeStageID) group:SendRTB(airbase) end end end end function CapBase:CheckAndScheduleIntercept() self.logger:debug("Check intercept taskings for airbase " .. self.airbaseName) local interceptZoneIDs = {} local airbase = Airbase.getByName(self.airbaseName) if not airbase then return nil end for _, group in pairs(self.interceptGroupsByName) do local targetZoneID = group:GetZoneIDWhenStageID(tostring(self.activeStage)) if targetZoneID then interceptZoneIDs[targetZoneID] = true end end local unitsToInterceptPerZone = {} local detectedUnits = self.detectionManager:GetDetectedUnitsBy(coalition.side.RED) for targetZoneID, _ in pairs(interceptZoneIDs) do local zones = self.database:GetInterceptZonesForZoneID(targetZoneID) if zones then for _, zone in pairs(zones) do self.logger:debug("Check intercept zone " .. zone.name .. " for airbase " .. self.airbaseName) for _, unitName in pairs(detectedUnits) do self.logger:debug("Check unit " .. unitName .. " for intercept in zone " .. zone.name) local unit = Unit.getByName(unitName) if unit and unit:isExist() then local unitPos = unit:getPoint() if Util.is3dPointInZone(unitPos, zone) == true then if not unitsToInterceptPerZone[zone.name] then unitsToInterceptPerZone[zone.name] = {} end table.insert(unitsToInterceptPerZone[zone.name], unitName) end end end end end end local ratio = 4 for name, targets in pairs(unitsToInterceptPerZone) do local required = 0 local nrTargets = Util.tableLength(targets) if targets and nrTargets > 0 then required = math.ceil(nrTargets / ratio) end local total = 0 for _, group in pairs(self.interceptGroupsByName) do if group:GetState() == "OnStation" and group:GetCurrentTargetZone() == name then group:SetTargetUnits(targets) total = total + 1 end end if total < required then for _, group in pairs(self.interceptGroupsByName) do if total < required then if group:GetState() == "ReadyOnTheRamp" then group:SendToInterceptUnits(targets, name, airbase) total = total + 1 self.logger:debug("Intercept group " .. group:GetName() .. " sent to intercept zone " .. name ) elseif group:GetState() == "InTransit" or group:GetState() == "OnStation" then group:SetTargetUnits(targets) end end end end end end function CapBase:OnStageNumberChanged(number, laneIdentifier) if laneIdentifier ~= nil then return end self.activeStage = number if self:IsBaseActiveWhenStageIsActive(number) == true then for _, mission in pairs(self.runwayStrikeMissions) do mission:SpawnActive() end end self:SpawnIfApplicable() end function CapBase:IsBaseActiveWhenStageIsActive(stageNumber) for _, group in pairs(self.capGroupsByName) do local target = group:GetZoneIDWhenStageID(tostring(stageNumber)) if group:IsBackup() == false and target ~= nil then return true end end for _, group in pairs(self.sweepGroupsByName) do local target = group:GetZoneIDWhenStageID(tostring(stageNumber)) if target ~= nil then return true end end for _, group in pairs(self.interceptGroupsByName) do local target = group:GetZoneIDWhenStageID(tostring(stageNumber)) if target ~= nil then return true end end return false end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.capclasses then ScriptGlobals.classes.capclasses = {} end ScriptGlobals.classes.capclasses.capairbase = CapBase end -- classes.capclasses.capairbase do -- classes.capclasses.globalcapmanager local Logger = ScriptGlobals.classes.util.logger local Util = ScriptGlobals.classes.util.util local RunwayBombingTracker = ScriptGlobals.classes.capclasses.runwaybombing.runwaybombingtracker local CapAirbase = ScriptGlobals.classes.capclasses.capairbase local GlobalCapManager = {} do local airbasesPerStage = {} local allAirbasesByName = {} local initiated = false function GlobalCapManager.start(database, capConfig, detectionManager, logLevel, spawnManager) if initiated == true then return end local logger = Logger.new("AirbaseManager", logLevel) local bombTrackLogger = Logger.new("RunwayBombingTracker", logLevel) local runwayBombingTracker = RunwayBombingTracker.new(bombTrackLogger) local zones = database:getStagezoneNames() if zones then for _, stageName in pairs(zones) do if airbasesPerStage[stageName] == nil then airbasesPerStage[stageName] = {} end local airbaseNames = database:getAirbaseNamesInStage(stageName) if airbaseNames then for _, airbaseName in pairs(airbaseNames) do if airbaseName then local airbaseSpecificLogger = Logger.new("CAP_" .. airbaseName, logLevel) local airbase = CapAirbase.new(airbaseName, database, airbaseSpecificLogger, capConfig, runwayBombingTracker, detectionManager, spawnManager) if airbase then table.insert(airbasesPerStage[stageName], airbase) allAirbasesByName[airbaseName] = airbase end end end end end end logger:info("Initiated " .. Util.tableLength(allAirbasesByName) .. " airbases for cap") initiated = true GlobalCapManager.IsCapActiveWhenZoneIsActive = function(zoneName, activeZoneNumber) for _, airbase in pairs(airbasesPerStage[zoneName]) do if airbase:IsBaseActiveWhenStageIsActive(activeZoneNumber) == true then return true end end return false end end end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.capclasses then ScriptGlobals.classes.capclasses = {} end ScriptGlobals.classes.capclasses.globalcapmanager = GlobalCapManager end -- classes.capclasses.globalcapmanager do -- classes.stageclasses.groups.spearheadgroup local DcsUtil = ScriptGlobals.classes.util.dcsutil local SpearheadGroup = {} SpearheadGroup.__index = SpearheadGroup function SpearheadGroup.New(groupName, spawnManager, isPersistent) local self = setmetatable({}, SpearheadGroup) if isPersistent == nil then isPersistent = false end self._spawnManager = spawnManager self._isStatic = spawnManager:IsGroupStatic(groupName) == true self._groupName = groupName self._isSpawned = false self._isPersistent = isPersistent return self end function SpearheadGroup:GetName() return self._groupName end function SpearheadGroup:IsSpawned() return self._isSpawned end function SpearheadGroup:SpawnCorpsesOnly() if self._isSpawned == true then return end self._spawnManager:SpawnCorpsesOnly(self._groupName) self._isSpawned = true end function SpearheadGroup:Spawn(lateStart) if self._isSpawned == true then return end local overrides = { uncontrolled = lateStart, } local _, isStatic = self._spawnManager:SpawnGroup(self._groupName, overrides, self._isPersistent) self._isStatic = isStatic self._isSpawned = true end function SpearheadGroup:Destroy() self._isSpawned = false self._spawnManager:DestroyGroup(self._groupName) end function SpearheadGroup:IsStatic() return self._isStatic end function SpearheadGroup:GetCoalition() if self._isStatic == true then local object = StaticObject.getByName(self._groupName) if object == nil then return 0 end return object:getCoalition() else local group = Group.getByName(self._groupName) if group == nil then return 0 end return group:getCoalition() end end function SpearheadGroup:GetObjects() local result = {} if self._isStatic == true then local staticObject = StaticObject.getByName(self._groupName) if staticObject then table.insert(result, staticObject) end else local group = Group.getByName(self._groupName) if not group then return {} end for _, unit in pairs(group:getUnits()) do table.insert(result, unit) end end return result end function SpearheadGroup:GetAsUnits() if self._isStatic == true then return {} end local result = {} local group = Group.getByName(self._groupName) if not group then return {} end for _, unit in pairs(group:getUnits()) do table.insert(result, unit) end return result end function SpearheadGroup:GetAllUnitPositions() local result = {} if self._isStatic == true then local staticObject = StaticObject.getByName(self._groupName) if staticObject then table.insert(result, staticObject:getPoint()) end else local group = Group.getByName(self._groupName) if not group then return {} end for _, unit in pairs(group:getUnits()) do table.insert(result, unit:getPoint()) end end return result end function SpearheadGroup:SetInvisible() if self._isStatic == true then local country = DcsUtil.GetNeutralCountry() local overrides = { countryID = country } self._spawnManager:SpawnGroup(self._groupName, overrides, self._isPersistent) else local group = Group.getByName(self._groupName) if group then local setInvisible = { id = 'SetInvisible', params = { value = true } } group:getController():setCommand(setInvisible) end end end function SpearheadGroup:SetVisible() local group = Group.getByName(self._groupName) if group then local setInvisible = { id = 'SetInvisible', params = { value = false } } group:getController():setCommand(setInvisible) end end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.stageclasses then ScriptGlobals.classes.stageclasses = {} end if not ScriptGlobals.classes.stageclasses.groups then ScriptGlobals.classes.stageclasses.groups = {} end ScriptGlobals.classes.stageclasses.groups.spearheadgroup = SpearheadGroup end -- classes.stageclasses.groups.spearheadgroup do -- classes.stageclasses.missions.buildablemission local Mission = ScriptGlobals.classes.stageclasses.missions.basemissions.mission local Util = ScriptGlobals.classes.util.util local DcsUtil = ScriptGlobals.classes.util.dcsutil local SupplyUnitsTracker = ScriptGlobals.classes.stageclasses.helpers.supplyunitstracker local MissionCommandsHelper = ScriptGlobals.classes.stageclasses.helpers.missioncommandshelper local GlobalConfig = ScriptGlobals.classes.configuration.globalconfig local SupplyConfigHelper = ScriptGlobals.classes.stageclasses.helpers.supplyconfighelper local CustomDrawing = ScriptGlobals.classes.stageclasses.drawings.customdrawing local DrawingHelper = ScriptGlobals.classes.stageclasses.drawings.helper.drawinghelper local BuildableMission = {} BuildableMission.__index = BuildableMission local function getDefaultBriefing(siteType, coords) return "We've dispatched forward units to find a proper spot for a new " .. siteType .. "." .. "\nYou will need to drop off supplies so they can start building." .. "\nThe coords are: " .. coords .. "\n\n" end function BuildableMission.new(database, logger, targetZone, noLandingZone, requiredKilos, requiredCrateType, briefing) setmetatable(BuildableMission, Mission) local self = setmetatable({}, { __index = BuildableMission }) self._targetZone = targetZone self._database = database self._requiredKilos = requiredKilos self._droppedKilos = 0 self._briefing = briefing self._noLandingZone = noLandingZone if noLandingZone then local verts = noLandingZone.verts local enlarged = Util.enlargeConvexHull(verts, 300) local dropOfZone = { name = targetZone.name .. "_dropZone", zone_type = "Polygon", radius = 0, verts = enlarged, location = noLandingZone.location, } self._dropOffZone = dropOfZone end self.code = tostring(database:GetNewMissionCode()) local splitTargetZoneName = Util.split_string(targetZone.name, "_") if splitTargetZoneName and splitTargetZoneName[3] and splitTargetZoneName[3] ~= "" then self.name = splitTargetZoneName[3] else self.name = "Resupply" end self.zoneName = targetZone.name .. "_supply" self._logger = logger self._onCrateDroppedOfListeners = {} self._completeListeners = {} self._markIDsPerGroup = {} self._supplyUnitsTracker = SupplyUnitsTracker.getOrCreate() self._state = "NEW" self.location = targetZone.location self.missionType = "LOGISTICS" self.missionTypeDisplay = "LOGISTICS" self.priority = "secondary" self._missionCommandsHelper = MissionCommandsHelper.getOrCreate() self._crateType = requiredCrateType return self end function BuildableMission:AddOnCrateDroppedOfListener(listener) table.insert(self._onCrateDroppedOfListeners, listener) end function BuildableMission:ShowBriefing(groupID) local group = DcsUtil.GetPlayerGroupByGroupID(groupID) if group == nil then return end local unitType = DcsUtil.getUnitTypeFromGroup(group) local coords = DcsUtil.convertVec2ToUnitUsableType(self.location, unitType) if coords == nil then coords = "Could not make conversion" end local siteType = "FARP" if self._crateType == "SAM_CRATE" then siteType = "SAM site" elseif self._crateType == "AIRBASE_CRATE" then siteType = "airbase" end local briefingPart = self._briefing if briefingPart then briefingPart = Util.replaceString(briefingPart, "{{coords}}", coords) else briefingPart = getDefaultBriefing(siteType, coords) end local briefing = "Mission [" .. self.code .. "] " .. self.name .. "\n \n" .. briefingPart .. "\n\n" .. "\nKilos still required: " .. self._requiredKilos - self._droppedKilos .. "\n\n" .. "NOTE: Do not land in the orange construction zone!" trigger.action.outTextForGroup(groupID, briefing, GlobalConfig:getBriefingTime()) end function BuildableMission:MarkMissionAreaToGroup(groupID) local groupIdStr = tostring(groupID) if self._markIDsPerGroup[groupIdStr] then DcsUtil.RemoveMark(self._markIDsPerGroup[groupIdStr]) end local text = "[" .. self.code .. "] " .. self.name .. " | " .. self._crateType local location = { x= self.location.x, y=land.getHeight(self.location), z=self.location.y } local markID = DcsUtil.AddMarkToGroup(groupID, text, location) self._markIDsPerGroup[groupIdStr] = markID end function BuildableMission:NotifyCrateDroppedOf(crate) for _, listener in ipairs(self._onCrateDroppedOfListeners) do if listener.OnCrateDroppedOff then listener:OnCrateDroppedOff(self, crate.weight) end end end function BuildableMission:SpawnActive() if self._state ~= "NEW" then self._logger:debug("Mission already spawned: " .. self.code) return end self._logger:debug("Spawning buildable mission: " .. self.code) if self._noLandingZone == nil then self._logger:error("No nolanding zone found for mission: " .. self.code) return end local lineColor = DrawingHelper.ColorTableToColorString({ 230/255, 93/255, 49/255, 1}) local fillColor = DrawingHelper.ColorTableToColorString({ 230/255, 93/255, 49/255, 0.2}) self._noLandingZoneDrawing = CustomDrawing.FromZone(self._noLandingZone, lineColor, fillColor, 2, 6) self._noLandingZoneDrawing:Draw() if self._dropOffZone == nil then self._logger:error("No drop off zone found for mission: " .. self.code) return end local lineColor2 = DrawingHelper.ColorTableToColorString({ 0, 0, 1, 1}) local fillColor2 = DrawingHelper.ColorTableToColorString({ 0, 0, 1, 0}) self._dropOffZoneDrawing = CustomDrawing.FromZone(self._dropOffZone, lineColor2, fillColor2, 2, 6) self._dropOffZoneDrawing:Draw() local checkForCrateTasks = function (selfA, time) selfA:CheckCratesInZone() if selfA:getState() == "COMPLETED" then return nil end return time + 10 end timer.scheduleFunction(checkForCrateTasks, self, timer.getTime() + 10) self:SpawnForwardUnits() self._state = "ACTIVE" self._missionCommandsHelper:AddMissionToCommands(self) self._supplyUnitsTracker:AddOnSupplyUnitEventListener(self) local units = self._supplyUnitsTracker:GetUnits() if units then for _, unit in pairs(units) do if unit and unit:isExist() then local group = unit:getGroup() if group then self:MarkMissionAreaToGroup(group:getID()) end end end end end function BuildableMission:SpawnForwardUnits() end function BuildableMission:SupplyUnitSpawned(unit) if self._state ~= "ACTIVE" then return end local group = unit:getGroup() if group == nil then return end self:MarkMissionAreaToGroup(unit:getGroup():getID()) end function BuildableMission:CheckCratesInZone() local foundCrates = {} local crates = self._supplyUnitsTracker:GetCargoCratesDropped() for _, staticObject in pairs(crates) do if staticObject and staticObject:isExist() and Util.startsWith(staticObject:getName(), self._crateType, true) then local pos = staticObject:getPoint() if Util.is3dPointInZone(pos, self._dropOffZone) then table.insert(foundCrates, staticObject) end end end for _, foundCrate in pairs(foundCrates) do local crateConfig = SupplyConfigHelper.fromObjectName(foundCrate:getName()) if crateConfig then self._droppedKilos = self._droppedKilos + crateConfig.weight foundCrate:destroy() self:NotifyCrateDroppedOf(crateConfig) end end if self._droppedKilos >= self._requiredKilos then self._dropOffZoneDrawing:Remove() self._noLandingZoneDrawing:Remove() self:NotifyMissionComplete() self._state = "COMPLETED" end if self._state == "COMPLETED" then for groupID, markID in pairs(self._markIDsPerGroup) do if markID then DcsUtil.RemoveMark(markID) self._markIDsPerGroup[groupID] = nil end end end end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.stageclasses then ScriptGlobals.classes.stageclasses = {} end if not ScriptGlobals.classes.stageclasses.missions then ScriptGlobals.classes.stageclasses.missions = {} end ScriptGlobals.classes.stageclasses.missions.buildablemission = BuildableMission end -- classes.stageclasses.missions.buildablemission do -- classes.stageclasses.specialzones.abstract.buildablezone local Util = ScriptGlobals.classes.util.util local Persistence = ScriptGlobals.classes.persistence.persistence local BuildableMission = ScriptGlobals.classes.stageclasses.missions.buildablemission local BuildableZone = {} BuildableZone.__index = BuildableZone function BuildableZone:New(targetZone, kilosRequired, crateType, buildableGroups, logger, database, briefing) self._targetZone = targetZone self._requiredKilos = kilosRequired or 0 self._buildableGroups = buildableGroups or {} self._buildableLogger = logger local totalGroups = Util.tableLength(self._buildableGroups) self._groupsPerKilo = totalGroups / self._requiredKilos self._receivedBuildingKilos = 0 local persistedKilos = Persistence.GetZoneDeliveredKilos(targetZone.name) if persistedKilos and persistedKilos > 0 then self._buildableLogger:debug("Zone " .. targetZone.name .. " already has " .. persistedKilos .. " kilos delivered") self._receivedBuildingKilos = persistedKilos local startUnpackingCrate = function(params, time) local unpacked = params.unpackedKilos + (params.kilosPerSecond * 2) local alreadySpawned = params.unpackedItems / params.groupsPerKilo local diff = unpacked - alreadySpawned local amount = math.floor(diff * params.groupsPerKilo) local spawned = params.self:SpawnAmount(amount) params.unpackedItems = params.unpackedItems + amount params.unpackedKilos = unpacked if params.unpackedKilos >= params.kilos or spawned == false then return end return time + 0.5 end local params = { self = self, groupsPerKilo = self._groupsPerKilo, unpackedItems = 0, kilosPerSecond = persistedKilos/30, unpackedKilos = 0, kilos = persistedKilos } timer.scheduleFunction(startUnpackingCrate, params, timer.getTime() + 5) kilosRequired = kilosRequired - persistedKilos end local noLandingZone = self:GetNoLandingZone() if kilosRequired and kilosRequired > 0 then self._buildableMission = BuildableMission.new(database, logger, targetZone, noLandingZone, kilosRequired, crateType, briefing) self._buildableMission:AddOnCrateDroppedOfListener(self) else self._buildableMission = nil end if self._buildableMission == nil then self._buildableLogger:debug("No buildable mission for zone: " .. targetZone.name) end end function BuildableZone:StartBuildable() self._buildableMission:SpawnActive() end function BuildableZone:OnBuildingComplete() end function BuildableZone:OnCrateDroppedOff(_, kilos) self._buildableLogger:debug("Crate dropped off in zone: " .. self._targetZone.name) local startUnpackingCrate = function(params, time) local unpacked = params.unpackedKilos + (params.kilosPerSecond * 2) local alreadySpawned = params.unpackedItems / params.groupsPerKilo local diff = unpacked - alreadySpawned local amount = math.floor(diff * params.groupsPerKilo) local spawned = params.self:SpawnAmount(amount) params.unpackedItems = params.unpackedItems + amount params.unpackedKilos = unpacked if params.unpackedKilos >= params.kilos or spawned == false then params.self:FinaliseCrate(params.kilos) return end return time + 2 end local timeToUnpack = (kilos / 500) * 15 local params = { self = self, groupsPerKilo = self._groupsPerKilo, unpackedItems = 0, kilosPerSecond = kilos/timeToUnpack, unpackedKilos = 0, kilos = kilos } timer.scheduleFunction(startUnpackingCrate, params, timer.getTime() + 2) end function BuildableZone:FinaliseCrate(kilos) self._receivedBuildingKilos = self._receivedBuildingKilos + kilos Persistence.SetZoneDeliveredKilos(self._targetZone.name, self._receivedBuildingKilos) if self._receivedBuildingKilos >= self._requiredKilos then self:OnBuildingComplete() end end function BuildableZone:GetNoLandingZone() local points = {} for _, group in pairs(self._buildableGroups) do for _, unitPos in pairs(group:GetAllUnitPositions()) do table.insert(points, { x = unitPos.x, y = unitPos.z }) end end local vecs = Util.getConvexHull(points) local spearheadZone = { name = self._targetZone.name .. "_noland", location = self._targetZone.location, verts = vecs, radius = 0, zone_type = "Polygon" } return spearheadZone end function BuildableZone:SpawnAmount(amount) local function spawnOne() for _, group in pairs(self._buildableGroups) do if group:IsSpawned() == false then group:Spawn() return true end end return nil end for _ = 1, amount do local spawned = spawnOne() if spawned ~= true then self._buildableLogger:debug("No more groups to spawn in zone: " .. self._targetZone.name) return false end end return true end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.stageclasses then ScriptGlobals.classes.stageclasses = {} end if not ScriptGlobals.classes.stageclasses.specialzones then ScriptGlobals.classes.stageclasses.specialzones = {} end if not ScriptGlobals.classes.stageclasses.specialzones.abstract then ScriptGlobals.classes.stageclasses.specialzones.abstract = {} end ScriptGlobals.classes.stageclasses.specialzones.abstract.buildablezone = BuildableZone end -- classes.stageclasses.specialzones.abstract.buildablezone do -- classes.stageclasses.specialzones.supplyhub local Util = ScriptGlobals.classes.util.util local DcsUtil = ScriptGlobals.classes.util.dcsutil local SupplyUnitsTracker = ScriptGlobals.classes.stageclasses.helpers.supplyunitstracker local MissionCommandsHelper = ScriptGlobals.classes.stageclasses.helpers.missioncommandshelper local CustomDrawing = ScriptGlobals.classes.stageclasses.drawings.customdrawing local DrawingHelper = ScriptGlobals.classes.stageclasses.drawings.helper.drawinghelper local SupplyHub = {} function SupplyHub.new(database, logger, zoneName) SupplyHub.__index = SupplyHub local self = setmetatable({}, SupplyHub) self._database = database self._logger = logger self._zoneName = zoneName self._customDrawing = nil local split = Util.split_string(zoneName, "_") if string.lower(split[2]) == "a" then self._activeAtStart = true else self._activeAtStart = false end self._zone = DcsUtil.getZoneByName(zoneName) self._supplyUnitsTracker = SupplyUnitsTracker.getOrCreate() self._inZone = {} self._missionCommandsHelper = MissionCommandsHelper.getOrCreate() self._logger:debug("Creating Supply Hub zone: " .. self._zoneName) return self end function SupplyHub:IsActiveFromStart() return self._activeAtStart end function SupplyHub:GetZoneName() return self._zoneName end function SupplyHub:GetZone() return self._zone end function SupplyHub:Activate() if self._active == true then return end self._active = true self._logger:debug("Activating Supply Hub zone: " .. self._zoneName) local zone = DcsUtil.getZoneByName(self._zoneName) if zone and self._customDrawing == nil then local fillColor = DrawingHelper.ColorTableToColorString({ 0, 1, 0, 0.2 }) local lineColor = DrawingHelper.ColorTableToColorString({ 0, 1, 0, 1}) local lineStyle = 1 self._customDrawing = CustomDrawing.FromZone(zone, lineColor, fillColor, lineStyle, 6) self._customDrawing:Draw() end self._supplyUnitsTracker:RegisterHub(self) end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.stageclasses then ScriptGlobals.classes.stageclasses = {} end if not ScriptGlobals.classes.stageclasses.specialzones then ScriptGlobals.classes.stageclasses.specialzones = {} end ScriptGlobals.classes.stageclasses.specialzones.supplyhub = SupplyHub end -- classes.stageclasses.specialzones.supplyhub do -- classes.stageclasses.specialzones.farpzone local BuildableZone = ScriptGlobals.classes.stageclasses.specialzones.abstract.buildablezone local Util = ScriptGlobals.classes.util.util local DcsUtil = ScriptGlobals.classes.util.dcsutil local SupplyHub = ScriptGlobals.classes.stageclasses.specialzones.supplyhub local SpearheadGroup = ScriptGlobals.classes.stageclasses.groups.spearheadgroup local FarpZone = {} FarpZone.__index = FarpZone function FarpZone.New(database, logger, zoneName, spawnManager) setmetatable(FarpZone, BuildableZone) local self = setmetatable({}, FarpZone) self._database = database self._logger = logger self._zoneName = zoneName local split = Util.split_string(zoneName, "_") if string.lower(split[2]) == "a" then self._startingFarp = true else self._startingFarp = false end logger:debug("FARP zone name: " .. zoneName .. " startingFarp" .. tostring(self._startingFarp)) local farpData = database:getFarpDataForZone(zoneName) self._groups = {} self._padNames = {} self._supplyHubs = {} if farpData then self._padNames = farpData.padNames for _, supplyHubName in pairs(farpData.supplyHubNames) do local supplyHub = SupplyHub.new(database, logger, supplyHubName) if supplyHub then table.insert(self._supplyHubs, supplyHub) end end for _, groupName in pairs(farpData.groups) do local group = SpearheadGroup.New(groupName, spawnManager, true) table.insert(self._groups, group) group:Destroy() end local zone = DcsUtil.getZoneByName(zoneName) if zone then self._logger:debug("Creating Buildable zone: " .. zoneName .. " with " .. (farpData.buildingKilos or "nil") .. " kilos") BuildableZone.New(self, zone, farpData.buildingKilos or 0, "FARP_CRATE", self._groups, logger, database, farpData.briefing) end end self:Deactivate() return self end function FarpZone:IsStartingFarp() return self._startingFarp end function FarpZone:Activate() self._logger:info("Activating FARP zone: " .. self._zoneName) if self._buildableMission == nil then self:BuildUp() self:SetPadsBlue() self:ActivateSupplyHubs() else self:StartBuildable() end end function FarpZone:Deactivate() self:NeutralisePads() end function FarpZone:OnBuildingComplete() self:BuildUp() self:SetPadsBlue() self:ActivateSupplyHubs() end function FarpZone:BuildUp() for _, group in pairs(self._groups) do group:Spawn() end end function FarpZone:ActivateSupplyHubs() for _, supplyHub in pairs(self._supplyHubs) do supplyHub:Activate() end end function FarpZone:NeutralisePads() for _, name in pairs(self._padNames) do local base = Airbase.getByName(name) if base then base:autoCapture(false) base:setCoalition(1) end end end function FarpZone:SetPadsBlue() for _, name in pairs(self._padNames) do local base = Airbase.getByName(name) if base then base:autoCapture(false) base:setCoalition(2) end end end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.stageclasses then ScriptGlobals.classes.stageclasses = {} end if not ScriptGlobals.classes.stageclasses.specialzones then ScriptGlobals.classes.stageclasses.specialzones = {} end ScriptGlobals.classes.stageclasses.specialzones.farpzone = FarpZone end -- classes.stageclasses.specialzones.farpzone do -- classes.stageclasses.helpers.battlemanager local Logger = ScriptGlobals.classes.util.logger local Util = ScriptGlobals.classes.util.util local BattleManager = {} BattleManager.__index = BattleManager function BattleManager.New(redGroups, blueGroups, name, logLevel) local self = setmetatable({}, BattleManager) self._isActive = false self._name = name self._logger = Logger.new("BattleManager_" .. name, logLevel) self._redGroups = redGroups self._blueGroups = blueGroups self._logger:debug("BattleManager created with name: " .. self._name .. ", red groups: " .. #self._redGroups .. ", blue groups: " .. #self._blueGroups) return self end local function CheckTask(self, time) local interval = self:Update() if not interval then return end return time + interval end function BattleManager:Start() self._logger:info("BattleManager started: " .. self._name) self._isActive = true self:SetAllInvisible() timer.scheduleFunction(CheckTask, self, timer.getTime() + 5) end function BattleManager:Stop() self._isActive = false self:SetAllVisible() end function BattleManager:SetAllInvisible() for _, group in pairs(self._redGroups) do group:SetInvisible() end for _, group in pairs(self._blueGroups) do group:SetInvisible() end end function BattleManager:SetAllVisible() for _, group in pairs(self._redGroups) do group:SetVisible() end for _, group in pairs(self._blueGroups) do group:SetVisible() end end function BattleManager:Update() if self._isActive == false then return nil end self._logger:debug("BattleManager Update called for " .. self._name) self:LetUnitsShoot(self._redGroups, self._blueGroups) self:LetUnitsShoot(self._blueGroups, self._redGroups) return math.random(4, 10) end function BattleManager:LetUnitsShoot(groups, targetGroups) local shootChance = math.random(3, 7) / 10 local targetHulls = self:ToShootingHulls(targetGroups) for _, group in pairs(groups) do local units = group:GetAsUnits() for _, unit in pairs(units) do if self:IsUnitApplicable(unit) == true then if unit:hasAttribute("Infantry") == true then shootChance = 0.8 end if math.random() <= shootChance then local unitPos = unit:getPoint() local point = self:GetRandomPoint({x = unitPos.x, y = unitPos.z }, targetHulls) if point then local ammo, qty = self:getBestAmmo(unit) local shootTask = { id = "FireAtPoint", params = { point = point, radius = 1, expendQty = qty, weaponType = ammo, expendQtyEnabled = true } } local controller = unit:getController() if controller then controller:setTask(shootTask) end end end end end end end function BattleManager:getBestAmmo(unit) local ammo = unit:getAmmo() if not ammo then return 3221225470, 1 end local shells = {} for _, entry in pairs(ammo) do if entry.count and entry.count > 0 then if entry.desc.category == Weapon.Category.SHELL then table.insert(shells, entry) end end end local entry = Util.randomFromList(shells) if entry and entry.desc and entry.desc.warhead then local caliber = entry.desc.warhead.caliber if caliber > 50 then return 258503344128, 1 else return 258503344129, 25 end end return 3221225470, 1 end function BattleManager:IsUnitApplicable(unit) if not unit or not unit:isExist() then return false end if unit:hasAttribute("AAA") == true or unit:hasAttribute("Air Defence") == true or unit:hasAttribute("Mobile AAA") == true then return false end return true end function BattleManager:ToShootingHulls(groups) local result = {} local points = {} for _, group in pairs(groups) do for _, unit in pairs(group:GetObjects()) do local pos = unit:getPoint() table.insert(points, {x = pos.x, y = pos.z}) end end local hulls = Util.getSeparatedConvexHulls(points, 50) local enlargedHulls = {} for _, hull in pairs(hulls) do local enlarged = Util.enlargeConvexHull(hull, 25) if enlarged then table.insert(enlargedHulls, enlarged) end end for _, hull in pairs(enlargedHulls) do if #hull > 2 then table.insert(result, hull) end end return result end function BattleManager:GetRandomPoint(origin, groupHulls) local hull = Util.randomFromList(groupHulls) if not hull then return nil end local shootPoints = Util.GetTangentHullPointsFromOrigin(hull, origin) return Util.randomFromList(shootPoints) end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.stageclasses then ScriptGlobals.classes.stageclasses = {} end if not ScriptGlobals.classes.stageclasses.helpers then ScriptGlobals.classes.stageclasses.helpers = {} end ScriptGlobals.classes.stageclasses.helpers.battlemanager = BattleManager end -- classes.stageclasses.helpers.battlemanager do -- classes.stageclasses.missions.zonemission local Util = ScriptGlobals.classes.util.util local MissionEditorWarnings = ScriptGlobals.classes.util.missioneditorwarnings local Mission = ScriptGlobals.classes.stageclasses.missions.basemissions.mission local SpearheadGroup = ScriptGlobals.classes.stageclasses.groups.spearheadgroup local Events = ScriptGlobals.classes.spearhead_events local BattleManager = ScriptGlobals.classes.stageclasses.helpers.battlemanager local DcsUtil = ScriptGlobals.classes.util.dcsutil local ZoneMission = {} local function ParseZoneName(input) local split_name = Util.split_string(input, "_") local split_length = Util.tableLength(split_name) if Util.startsWith(input, "RANDOMMISSION") == true and split_length < 4 then MissionEditorWarnings.Add("Random Mission with zonename " .. input .. " not in right format") return nil elseif split_length < 3 then MissionEditorWarnings.Add("Mission with zonename" .. input .. " not in right format") return nil end local parsedType = "nil" local inputType = string.lower(split_name[2]) if inputType == "dead" then parsedType = "DEAD" end if inputType == "strike" then parsedType = "STRIKE" end if inputType == "cas" then parsedType = "CAS" end if inputType == "bai" then parsedType = "BAI" end if inputType == "sam" then parsedType = "SAM" end if inputType == "deepstrike" then parsedType = "DEEPSTRIKE" end if parsedType == "nil" then MissionEditorWarnings.Add("Mission with zoneName '" .. input .. "' has an unsupported type '" .. (type or "nil")) return nil end local name = split_name[3] return { missionName = name, type = parsedType } end function ZoneMission.new(zoneName, priority, database, logger, parentStage, spawnManager) ZoneMission.__index = ZoneMission setmetatable(ZoneMission, Mission) local self = setmetatable({}, ZoneMission) local parsed = ParseZoneName(zoneName) if not parsed then logger:error("Failed to create ZoneMission " .. zoneName .. " => invalid name") return nil end local missionData = database:getMissionDataForZone(zoneName) if not missionData then return end local missionBriefing = missionData.description or "No briefing available" local success, error = Mission.newSuper(self, zoneName, parsed.missionName, parsed.type, missionBriefing, priority, database, logger) if not success then logger:error("Failed to create ZoneMission " .. zoneName .. " => " .. error) return nil end if self.missionType == "SAM" then self.missionTypeDisplay = "DEAD" end self._missionGroups = { redGroups = {}, blueGroups = {}, unitsAlive = {}, targetsAlive = {}, hasTargets = false, groupNamesPerUnit = {}, sceneryTargets = {} } self._parentStage = parentStage self._dependencies = {} if missionData.dependsOn then for _, dependency in pairs(missionData.dependsOn) do self._dependencies[dependency] = false end end if missionData.completeAt == nil and (self.missionType == "BAI" or self.missionType == "CAS") then self._completeAtIndex = 0.8 elseif missionData.completeAt == nil then self._completeAtIndex = 1 else self._completeAtIndex = missionData.completeAt end self._missionGroups.sceneryTargets = missionData.SceneryTargets or {} if Util.tableLength(self._missionGroups.sceneryTargets) > 0 then self._missionGroups.hasTargets = true end for _, groupName in pairs(missionData.BlueGroups) do local spearheadGroup = SpearheadGroup.New(groupName, spawnManager, true) if spearheadGroup then table.insert(self._missionGroups.blueGroups, spearheadGroup) end spearheadGroup:Destroy() end for _, groupName in pairs(missionData.RedGroups) do local spearheadGroup = SpearheadGroup.New(groupName, spawnManager, true) table.insert(self._missionGroups.redGroups, spearheadGroup) local isGroupTarget = Util.startsWith(string.lower(groupName), "tgt_") for _, unit in pairs(spearheadGroup:GetObjects()) do local unitName = unit:getName() local isUnitTarget = Util.startsWith(string.lower(unitName), "tgt_") if self._missionGroups.unitsAlive[groupName] == nil then self._missionGroups.unitsAlive[groupName] = {} end self._missionGroups.unitsAlive[groupName][unitName] = true self._missionGroups.groupNamesPerUnit[unitName] = groupName if isGroupTarget == true or isUnitTarget == true then self._missionGroups.hasTargets = true if self._missionGroups.targetsAlive[groupName] == nil then self._missionGroups.targetsAlive[groupName] = {} end self._missionGroups.targetsAlive[groupName][unitName] = true end Events.addOnUnitLostEventListener(unitName, self) end spearheadGroup:Destroy() end if self.missionType == "CAS" then self._battleManager = BattleManager.New(self._missionGroups.redGroups, self._missionGroups.blueGroups, self.zoneName, self._logger.LogLevel) end self._logger:debug("Mission " .. self.name .. " group count: " .. Util.tableLength(missionData.RedGroups)) return self end function ZoneMission:StartCheckingDependencies() self._state = "WAITING" local function CheckDependencies(mission, time) if mission:AllDependenciesMet() == true then mission:SpawnActive() return nil end return time + 15 end timer.scheduleFunction(CheckDependencies, self, timer.getTime() + 15) end function ZoneMission:AllDependenciesMet() local allDependenciesMet = true for missionName, _ in pairs(self._dependencies) do if self._parentStage:IsMissionComplete(missionName) == false then allDependenciesMet = false self._dependencies[missionName] = false else self._dependencies[missionName] = true end end if allDependenciesMet == true then self._logger:info("All dependencies met for " .. self.name) end return allDependenciesMet end function ZoneMission:UpdateState(checkHealth, _) if checkHealth == nil then checkHealth = false end if checkHealth == true then local function unitAliveState(unitName) local staticObject = StaticObject.getByName(unitName) if staticObject then if staticObject:isExist() == true then local life0 = staticObject:getDesc().life if staticObject:getLife() / life0 < 0.3 then self._logger:debug("exploding unit") trigger.action.explosion(staticObject:getPoint(), 100) return false end return true else return false end else local unit = Unit.getByName(unitName) if unit and unit:isExist() then if unit:getLife() / unit:getLife0() < 0.2 then self._logger:debug("exploding unit") trigger.action.explosion(unit:getPoint(), 100) return false end return true else return false end end end if self._missionGroups.hasTargets == true then for groupName, unitNameDict in pairs(self._missionGroups.targetsAlive) do for unitName, isAlive in pairs(unitNameDict) do if isAlive == true then self._missionGroups.targetsAlive[groupName][unitName] = unitAliveState(unitName) end end end else for groupName, unitNameDict in pairs(self._missionGroups.unitsAlive) do for unitName, isAlive in pairs(unitNameDict) do if isAlive == true then self._missionGroups.unitsAlive[groupName][unitName] = unitAliveState(unitName) end end end end end if self._missionGroups.hasTargets == true then local total = 0 local alive = 0 for _, units in pairs(self._missionGroups.targetsAlive) do for _, isAlive in pairs(units) do total = total + 1 if isAlive == true then alive = alive + 1 end end end for _, sceneryObject in pairs(self._missionGroups.sceneryTargets) do total = total + 1 if sceneryObject:IsAlive() == true then alive = alive + 1 end end local deadRatio = (total - alive) / total if deadRatio >= self._completeAtIndex then self._logger:debug("Dead ratio " .. self.zoneName .. deadRatio .. " >= " .. self._completeAtIndex) self._state = "COMPLETED" end else local total = 0 local alive = 0 for _, units in pairs(self._missionGroups.unitsAlive) do for _, isAlive in pairs(units) do total = total + 1 if isAlive == true then alive = alive + 1 end end end local deadRatio = (total - alive) / total if deadRatio >= self._completeAtIndex then self._logger:debug("Dead ratio " .. self.zoneName .. deadRatio .. " >= " .. self._completeAtIndex) self._state = "COMPLETED" end end if self._state == "COMPLETED" and self._lastContactMarkerID then DcsUtil.RemoveMark(self._lastContactMarkerID) end if self._state == "COMPLETED" and self._battleManager then self._battleManager:Stop() end end function ZoneMission:SpawnPersistedState() for _, group in pairs(self._missionGroups.redGroups) do group:Spawn() end for _, object in pairs(self._missionGroups.sceneryTargets) do object:UpdateStatePersistently() end end function ZoneMission:SpawnInactive() self._logger:info("PreActivating " .. self.name) if self.missionType == "DEEPSTRIKE" then local missionData = self._database:getMissionDataForZone(self.zoneName) local priority = "secondary" if missionData and missionData.primaryOverwrite ~= nil then if missionData.primaryOverwrite == true then priority = "primary" end end self._priority = priority self._missionCommandsHelper:AddMissionToCommands(self) end for _, group in pairs(self._missionGroups.redGroups) do group:Spawn() end end function ZoneMission:SpawnActive() if self:AllDependenciesMet() == false then self:SpawnInactive() self:StartCheckingDependencies() return end self._logger:info("Activating " .. self.name) if self._state == "COMPLETED" or self._state == "ACTIVE" then self._logger:debug("Mission already completed, not spawning") return end self._state = "ACTIVE" for _, group in pairs(self._missionGroups.redGroups) do group:Spawn() end for _, group in pairs(self._missionGroups.blueGroups) do group:Spawn() end for _, sceneryObject in pairs(self._missionGroups.sceneryTargets) do sceneryObject:UpdateStatePersistently() end if self._battleManager then self._battleManager:Start() end self._missionCommandsHelper:RemoveMissionToCommands(self) if self.missionType == "DEEPSTRIKE" then local missionData = self._database:getMissionDataForZone(self.zoneName) local priority = "primary" if missionData and missionData.primaryOverwrite ~= nil then if missionData.primaryOverwrite == false then priority = "secondary" end end self._priority = priority end self._missionCommandsHelper:AddMissionToCommands(self) self:StartCheckingContinuous() end function ZoneMission:StartCheckingContinuous() local Check = function(mission, time) mission:UpdateState(true, true) if mission:getState() == "COMPLETED" then mission:NotifyMissionComplete() return nil end return time + 30 end timer.scheduleFunction(Check, self, timer.getTime() + 30) end function ZoneMission:PercentageComplete() if self._missionGroups.hasTargets == true then local dead = 0 local total = 0 if self._missionGroups.targetsAlive then for _, group in pairs(self._missionGroups.targetsAlive) do for _, isAlive in pairs(group) do total = total + 1 if isAlive == false then dead = dead + 1 end end end end for _, sceneryObject in pairs(self._missionGroups.sceneryTargets) do total = total + 1 if sceneryObject:IsAlive() == false then dead = dead + 1 end end if total > 0 then return math.floor((dead / total) * 100) end else local dead = 0 local total = 0 if self._missionGroups.unitsAlive then for _, group in pairs(self._missionGroups.unitsAlive) do for _, isAlive in pairs(group) do total = total + 1 if isAlive == false then dead = dead + 1 end end end end if total > 0 then return math.floor((dead / total) * 100) end end return 0 end function ZoneMission:ToStateString() return "Units Destroyed: " .. self:PercentageComplete() .. "%" end function ZoneMission:OnUnitLost(object) self._logger:debug("Getting on unit lost event") if SpearheadConfig and SpearheadConfig.StageConfig and SpearheadConfig.StageConfig.markLastContact == true then self:MarkLastContact(object) end local category = Object.getCategory(object) if category == Object.Category.UNIT then object = object local unitName = object:getName() self._logger:debug("UnitName:" .. unitName) local groupName = self._missionGroups.groupNamesPerUnit[unitName] self._missionGroups.unitsAlive[groupName][unitName] = false if self._missionGroups.targetsAlive[groupName] and self._missionGroups.targetsAlive[groupName][unitName] then self._missionGroups.targetsAlive[groupName][unitName] = false end elseif category == Object.Category.STATIC then object = object local name = object:getName() self._missionGroups.unitsAlive[name][name] = false self._logger:debug("Name " .. name) if self._missionGroups.targetsAlive[name] and self._missionGroups.targetsAlive[name][name] then self._missionGroups.targetsAlive[name][name] = false end end self:UpdateState(false, true) end function ZoneMission:MarkLastContact(unit) if not unit then self._logger:error("MarkLastContact called with nil unit") return end local point = unit:getPoint() if not point then self._logger:error("MarkLastContact called with unit without point") return end if self._lastContactMarkerID then DcsUtil.RemoveMark(self._lastContactMarkerID) end self._lastContactMarkerID = DcsUtil.AddMarkToAll("Last Contact: " .. self.name .. " [" .. self.code .. "]", point) end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.stageclasses then ScriptGlobals.classes.stageclasses = {} end if not ScriptGlobals.classes.stageclasses.missions then ScriptGlobals.classes.stageclasses.missions = {} end ScriptGlobals.classes.stageclasses.missions.zonemission = ZoneMission end -- classes.stageclasses.missions.zonemission do -- classes.stageclasses.specialzones.stagebase local BuildableZone = ScriptGlobals.classes.stageclasses.specialzones.abstract.buildablezone local DcsUtil = ScriptGlobals.classes.util.dcsutil local SpearheadGroup = ScriptGlobals.classes.stageclasses.groups.spearheadgroup local SupplyHub = ScriptGlobals.classes.stageclasses.specialzones.supplyhub local Util = ScriptGlobals.classes.util.util local StageBase = {} StageBase.__index = StageBase function StageBase.New(databaseManager, logger, airbaseName, spawnManager) setmetatable(StageBase, BuildableZone) local self = setmetatable({}, StageBase) self._database = databaseManager self._logger = logger self._red_groups = {} self._blue_groups = {} self._cleanup_units = {} self._supplyHubs = {} self._airbase = Airbase.getByName(airbaseName) self._initialSide = DcsUtil.getStartingCoalition(self._airbase) do local airbaseData = databaseManager:getAirbaseDataForZone(airbaseName) if airbaseData == nil then logger:error("Airbase data not found for airbase: " .. airbaseName) return nil end local redUnitsPos = {} local blueUnitsPos = {} for _, groupName in pairs(airbaseData.RedGroups) do local shGroup = SpearheadGroup.New(groupName, spawnManager, true) table.insert(self._red_groups, shGroup) for _, unit in pairs(shGroup:GetObjects()) do redUnitsPos[unit:getName()] = unit:getPoint() end shGroup:Destroy() end for _, groupName in pairs(airbaseData.BlueGroups) do local shGroup = SpearheadGroup.New(groupName, spawnManager, true) table.insert(self._blue_groups, shGroup) for _, unit in pairs(shGroup:GetObjects()) do blueUnitsPos[unit:getName()] = unit:getPoint() end shGroup:Destroy() end for _, supplyHubName in pairs(airbaseData.supplyHubNames) do local supplyHub = SupplyHub.new(databaseManager, logger, supplyHubName) if supplyHub then table.insert(self._supplyHubs, supplyHub) end end do local cleanup_distance = 5 for _, blueUnitPos in pairs(blueUnitsPos) do for redUnitName, redUnitPos in pairs(redUnitsPos) do local distance = Util.VectorDistance3d(blueUnitPos, redUnitPos) if distance <= cleanup_distance then self._cleanup_units[redUnitName] = true end end end end local zone = DcsUtil.getAirbaseZoneByName(airbaseName) if zone then BuildableZone.New(self, zone, airbaseData.buildingKilos or 0, "AIRBASE_CRATE", self._blue_groups, logger, databaseManager) end end return self end function StageBase:SpawnRedUnits() local spawnAsync = function(groups) for _, group in pairs(groups) do group:Spawn() end return nil end timer.scheduleFunction(spawnAsync, self._red_groups, timer.getTime() + 3) end function StageBase:CleanRedUnits() for _, value in pairs(self._red_groups) do value:SpawnCorpsesOnly() end for unitName, shouldClean in pairs(self._cleanup_units) do if shouldClean == true then DcsUtil.DestroyUnit(unitName) DcsUtil.CleanCorpse(unitName) end end end function StageBase:SpawnBlueUnits() local spawnAsync = function(groups) for _, group in pairs(groups) do group:Spawn() end return nil end timer.scheduleFunction(spawnAsync, self._blue_groups, timer.getTime() + 3) end function StageBase:ActivateRedStage() self._logger:debug("Activate red stage for airbase: " .. self._airbase:getName()) if self._airbase and (self._initialSide == 2 or self._initialSide == 1) then self._airbase:setCoalition(coalition.side.RED) self._airbase:autoCapture(false) end self:SpawnRedUnits() end function StageBase:ActivateBlueStage() self._logger:debug("Activate blue stage for airbase: " .. self._airbase:getName()) self:CleanRedUnits() if self._buildableMission then self:StartBuildable() else self:FinaliseBlueStage() end end function StageBase:FinaliseBlueStage() if self._initialSide == 2 and self._airbase then self._airbase:setCoalition(coalition.side.BLUE) self._airbase:autoCapture(false) end self:SpawnBlueUnits() for _, hub in pairs(self._supplyHubs) do hub:Activate() end end function StageBase:OnBuildingComplete() self:FinaliseBlueStage() end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.stageclasses then ScriptGlobals.classes.stageclasses = {} end if not ScriptGlobals.classes.stageclasses.specialzones then ScriptGlobals.classes.stageclasses.specialzones = {} end ScriptGlobals.classes.stageclasses.specialzones.stagebase = StageBase end -- classes.stageclasses.specialzones.stagebase do -- classes.stageclasses.specialzones.bluesam local BuildableZone = ScriptGlobals.classes.stageclasses.specialzones.abstract.buildablezone local SpearheadGroup = ScriptGlobals.classes.stageclasses.groups.spearheadgroup local Util = ScriptGlobals.classes.util.util local DcsUtil = ScriptGlobals.classes.util.dcsutil local BlueSam = {} BlueSam.__index = BlueSam function BlueSam.New(database, logger, zoneName, spawnManager) setmetatable(BlueSam, BuildableZone) local self = setmetatable({}, BlueSam) self._database = database self._logger = logger self._zoneName = zoneName self._blueGroups = {} self._cleanupUnits = {} local blueSamData = database:getBlueSamDataForZone(zoneName) if blueSamData == nil then logger:error("Blue SAM data not found for zone: " .. zoneName) return nil end self._buildableCrateKilos = blueSamData.buildingKilos self._receivedKilos = 0 local blueUnitsPos = {} local redUnitsPos = {} for _, groupName in pairs(blueSamData.groups) do local spearheadGroup = SpearheadGroup.New(groupName, spawnManager, true) if spearheadGroup then if spearheadGroup:GetCoalition() == 2 or spearheadGroup:GetCoalition() == 0 then table.insert(self._blueGroups, spearheadGroup) end for _, unit in pairs(spearheadGroup:GetObjects()) do if spearheadGroup:GetCoalition() == 1 then table.insert(blueUnitsPos, unit:getPoint()) elseif spearheadGroup:GetCoalition() == 2 then table.insert(redUnitsPos, unit:getPoint()) end end end spearheadGroup:Destroy() end local cleanup_distance = 5 for _, blueUnitPos in pairs(blueUnitsPos) do for redUnitName, redUnitPos in pairs(redUnitsPos) do local distance = Util.VectorDistance3d(blueUnitPos, redUnitPos) if distance <= cleanup_distance then self._cleanupUnits[redUnitName] = true end end end local zone = DcsUtil.getZoneByName(zoneName) if zone then BuildableZone.New(self, zone, self._buildableCrateKilos or 0, "SAM_CRATE", self._blueGroups, logger, database, blueSamData.briefing) end return self end function BlueSam:GetNoLandingZone() local points = {} for _, group in pairs(self._blueGroups) do for _, unitPos in pairs(group:GetAllUnitPositions()) do table.insert(points, { x = unitPos.x, y = unitPos.z }) end end local vecs = Util.getConvexHull(points) local zone = DcsUtil.getZoneByName(self._zoneName) if zone == nil then self._logger:error("Zone not found: " .. self._zoneName) return nil end local spearheadZone = { name = self._zoneName .. "_noland", location = zone.location, verts = vecs, radius = 0, zone_type = "Polygon" } return spearheadZone end function BlueSam:Activate() if self._buildableMission == nil then self:SpawnGroups() else self:StartBuildable() end end function BlueSam:SpawnGroups() for unitName, needsCleanup in pairs(self._cleanupUnits) do if needsCleanup then DcsUtil.DestroyUnit(unitName) end end for _, group in pairs(self._blueGroups) do group:Spawn() end end function BlueSam:OnBuildingComplete() self:SpawnGroups() end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.stageclasses then ScriptGlobals.classes.stageclasses = {} end if not ScriptGlobals.classes.stageclasses.specialzones then ScriptGlobals.classes.stageclasses.specialzones = {} end ScriptGlobals.classes.stageclasses.specialzones.bluesam = BlueSam end -- classes.stageclasses.specialzones.bluesam do -- classes.stageclasses.stagelane local StageLane = {} StageLane.__index = StageLane StageLane.DefaultLaneKey = nil function StageLane.New(laneIdentifier) if not laneIdentifier then laneIdentifier = StageLane.DefaultLaneKey end local self = setmetatable({}, { __index = StageLane }) self._isDefaultStageLane = laneIdentifier == StageLane.DefaultLaneKey self._stageLaneIdentifier = laneIdentifier self._activeStageIndex = nil self._stagesInLaneByIndex = {} self._chapterStarts = nil self._stageLaneState = "BetweenChapters" return self end function StageLane:AddStage(stage) local stageIndex = stage:GetStageIndex() if not self._stagesInLaneByIndex[tostring(stageIndex)] then self._stagesInLaneByIndex[tostring(stageIndex)] = {} end if not self._maxStageIndex or stageIndex > self._maxStageIndex then self._maxStageIndex = stageIndex end table.insert(self._stagesInLaneByIndex[tostring(stageIndex)], stage) end function StageLane:IsStageIndexComplete(stageNumber) local stages = self._stagesInLaneByIndex[tostring(stageNumber)] if not stages then return nil end for _, stage in ipairs(stages) do if not stage:IsComplete() then return false end end return true end function StageLane:IsCurrentStageIndexComplete() return self:IsStageIndexComplete(self._activeStageIndex) == true end function StageLane:GetStageLaneIdentifier() return self._stageLaneIdentifier end function StageLane:IsDefaultStageLane() return self._isDefaultStageLane end function StageLane:GetActiveStageIndex() return self._activeStageIndex end function StageLane:SetActiveStageIndex(stageNumber) if self._stagesInLaneByIndex[tostring(stageNumber)] == nil then self._stageLaneState = "BetweenChapters" elseif stageNumber > self._maxStageIndex then self._stageLaneState = "Completed" else self._stageLaneState = "InChapter" end self._activeStageIndex = stageNumber end function StageLane:GetStageLaneState() return self._stageLaneState end function StageLane:IsChapterStart(stageNumber) if self._chapterStarts == nil then self:FillChapterStarts() end return self._chapterStarts[tostring(stageNumber)] == true end function StageLane:FillChapterStarts() local previousIndex = nil local stageIndices = {} for stageIndex, _ in pairs(self._stagesInLaneByIndex) do table.insert(stageIndices, tonumber(stageIndex)) end table.sort(stageIndices) if self._chapterStarts == nil then self._chapterStarts = {} end for _, stageIndex in ipairs(stageIndices) do if previousIndex == nil then self._chapterStarts[tostring(stageIndex)] = true elseif stageIndex > previousIndex + 1 then self._chapterStarts[tostring(stageIndex)] = true else self._chapterStarts[tostring(stageIndex)] = false end previousIndex = stageIndex end end function StageLane:GetAllStageIndices() local indices = {} for stageIndex, _ in pairs(self._stagesInLaneByIndex) do table.insert(indices, tonumber(stageIndex)) end table.sort(indices) return indices end function StageLane:GetStagesAtIndex(stageIndex) return self._stagesInLaneByIndex[tostring(stageIndex)] end function StageLane:GetNextChapterStart() if self._chapterStarts == nil then self:FillChapterStarts() end for stageIndex, _ in pairs(self._stagesInLaneByIndex) do if tonumber(stageIndex) > self._activeStageIndex and self._chapterStarts[tostring(stageIndex)] == true then return tonumber(stageIndex) end end return nil end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.stageclasses then ScriptGlobals.classes.stageclasses = {} end ScriptGlobals.classes.stageclasses.stagelane = StageLane end -- classes.stageclasses.stagelane do -- classes.stageclasses.stagerepository local StageLane = ScriptGlobals.classes.stageclasses.stagelane local StageRepository = {} StageRepository.__index = StageRepository local instance = nil function StageRepository.new() local self = setmetatable({}, StageRepository) self.StageLanes = {} self.StageLanes[tostring(StageLane.DefaultLaneKey)] = StageLane.New(StageLane.DefaultLaneKey) instance = self return self end StageRepository.getInstance = function() if instance == nil then instance = StageRepository.new() end return instance end function StageRepository:getStageLane(laneName) if not laneName then laneName = StageLane.DefaultLaneKey end return self.StageLanes[tostring(laneName)] end function StageRepository:getAllStageLanes() local lanes = {} for _, lane in pairs(self.StageLanes) do table.insert(lanes, lane) end return lanes end function StageRepository:AddStage(stage) local stageLaneIdentifier = stage:GetStageLaneIdentifier() or StageLane.DefaultLaneKey if self.StageLanes[tostring(stageLaneIdentifier)] == nil then self.StageLanes[tostring(stageLaneIdentifier)] = StageLane.New(stageLaneIdentifier) end self.StageLanes[tostring(stageLaneIdentifier)]:AddStage(stage) end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.stageclasses then ScriptGlobals.classes.stageclasses = {} end ScriptGlobals.classes.stageclasses.stagerepository = StageRepository end -- classes.stageclasses.stagerepository do -- classes.stageclasses.stages.basestage.stagestate local StageState = { Inactive = 0, PreActivated = 1, Activated = 2, Blue = 3 } if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.stageclasses then ScriptGlobals.classes.stageclasses = {} end if not ScriptGlobals.classes.stageclasses.stages then ScriptGlobals.classes.stageclasses.stages = {} end if not ScriptGlobals.classes.stageclasses.stages.basestage then ScriptGlobals.classes.stageclasses.stages.basestage = {} end ScriptGlobals.classes.stageclasses.stages.basestage.stagestate = StageState end -- classes.stageclasses.stages.basestage.stagestate do -- classes.stageclasses.stages.basestage.stage local SpearheadGroup = ScriptGlobals.classes.stageclasses.groups.spearheadgroup local MissionCommandsHelper = ScriptGlobals.classes.stageclasses.helpers.missioncommandshelper local DcsUtil = ScriptGlobals.classes.util.dcsutil local FarpZone = ScriptGlobals.classes.stageclasses.specialzones.farpzone local SupplyHub = ScriptGlobals.classes.stageclasses.specialzones.supplyhub local Util = ScriptGlobals.classes.util.util local ZoneMission = ScriptGlobals.classes.stageclasses.missions.zonemission local MissionEditorWarnings = ScriptGlobals.classes.util.missioneditorwarnings local Persistence = ScriptGlobals.classes.persistence.persistence local StageBase = ScriptGlobals.classes.stageclasses.specialzones.stagebase local BlueSam = ScriptGlobals.classes.stageclasses.specialzones.bluesam local Events = ScriptGlobals.classes.spearhead_events local GlobalCapManager = ScriptGlobals.classes.capclasses.globalcapmanager local CustomDrawing = ScriptGlobals.classes.stageclasses.drawings.customdrawing local DrawingHelper = ScriptGlobals.classes.stageclasses.drawings.helper.drawinghelper local StageRepository = ScriptGlobals.classes.stageclasses.stagerepository local StageState = ScriptGlobals.classes.stageclasses.stages.basestage.stagestate local Stage = {} Stage.__index = Stage Stage.StageColors = { INVISIBLE = { 0, 0, 0, 0 }, RED_ACTIVE = { 1, 0, 0, 0.20 }, RED_PREACTIVE = { 1, 0, 0, 0.05}, BLUE = { 0, 0, 1, 0.10}, GRAY = { 80/255, 80/255, 80/255, 0.10 } } function Stage:superNew(database, stageConfig, logger, initData, stageType, missionPriority, spawnManager) logger:debug("[BaseStage] Initiating stage with name: " .. initData.stageZoneName) self._currentStageState = StageState.Inactive self.zoneName = initData.stageZoneName self.stageNumber = initData.stageNumber self._stageRepository = StageRepository.getInstance() if initData.stageLaneIdentifier then self._stageLaneIdentifier = string.lower(initData.stageLaneIdentifier) else self._stageLaneIdentifier = nil end self.stageName = initData.stageDisplayName self._stageType = stageType self.OnPostStageComplete = nil self.OnPostBlueActivated = nil self._database = database self._logger = logger self._db = { stageBriefing = nil, missionsByCode = {}, missions = {}, sams ={}, blueSams = {}, airbases ={}, miscGroups = {}, maxMissions = stageConfig.maxMissionsPerStage, farps = {}, missionsByName = {}, supplyHubs = {} } self._stageConfig = stageConfig or {} self._missionCommandsHelper = MissionCommandsHelper.getOrCreate() local zone = DcsUtil.getZoneByName(self.zoneName) if zone then local colorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.INVISIBLE) local fillColorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.INVISIBLE) local customDrawing = CustomDrawing.FromZone(zone, colorString, fillColorString, 1, 5) if customDrawing then self._customDrawing = customDrawing end end self._spawnedGroups = {} self._missionPriority = missionPriority self._stageCompleteListeners = {} local farpNames = database:getFarpNamesInStage(self.zoneName) for _, farpName in pairs(farpNames) do local farp = FarpZone.New(database, logger, farpName, spawnManager) table.insert(self._db.farps, farp) end local supplyHubNames = database:getSupplyHubsInStage(self.zoneName) for _, supplyHubName in pairs(supplyHubNames) do local supplyHub = SupplyHub.new(database, logger, supplyHubName) table.insert(self._db.supplyHubs, supplyHub) end self._db.stageBriefing = database:getStageBriefingForStage(self.zoneName) self._logger:info("Initiating new Stage with name: " .. self.zoneName) self.CheckContinuousAsync = function (selfA, time) selfA:CheckAndUpdateSelf() if selfA:IsComplete() == true then selfA:NotifyComplete() return nil end return time + 20 end do local missionZones = database:getMissionsForStage(self.zoneName) self._logger:debug("Found " .. Util.tableLength(missionZones) .. " mission zones for stage: " .. self.zoneName) for _, missionZone in pairs(missionZones) do local mission = ZoneMission.new(missionZone, self._missionPriority, database, logger, self, spawnManager) if mission then self._db.missionsByCode[mission.code] = mission if mission.name and self._db.missionsByName[mission.name] == nil then self._db.missionsByName[mission.name] = mission else MissionEditorWarnings.Add("DUPLICATE MISSION NAME ALERT: " .. mission.name .. " in zone: " .. self.zoneName) end if mission.missionType == "SAM" then table.insert(self._db.sams, mission) else table.insert(self._db.missions, mission) end end end local randomMissionNames = database:getRandomMissionsForStage(self.zoneName) local randomMissionByName = {} for _, missionZoneName in pairs(randomMissionNames) do local mission = ZoneMission.new(missionZoneName, self._missionPriority, database, logger, self, spawnManager) if mission then if randomMissionByName[mission.name] == nil then randomMissionByName[mission.name] = {} end table.insert(randomMissionByName[mission.name], mission) end end for missionName, missions in pairs(randomMissionByName) do local missionZonePicked = Persistence.GetPickedRandomMission(missionName) if missionZonePicked == nil then local mission = Util.randomFromList(missions) if mission then Persistence.RegisterPickedRandomMission(mission.name, mission.zoneName) self._db.missionsByCode[mission.code] = mission if mission.name and self._db.missionsByName[mission.name] == nil then self._db.missionsByName[mission.name] = mission else MissionEditorWarnings.Add("DUPLICATE MISSION NAME ALERT: " .. mission.name .. " in zone: " .. self.zoneName) end if mission.missionType == "SAM" then table.insert(self._db.sams, mission) else table.insert(self._db.missions, mission) end end else self._logger:info("Using persisted random mission with name: " .. missionName .. " and zone: " .. missionZonePicked) for _, mission in pairs(missions) do if string.lower(mission.zoneName) == string.lower(missionZonePicked) then self._db.missionsByCode[mission.code] = mission if mission.missionType == "SAM" then table.insert(self._db.sams, mission) else table.insert(self._db.missions, mission) end end end end end for _, mission in pairs(self._db.missionsByCode) do mission:AddMissionCompleteListener(self) end local airbaseNames = database:getAirbaseNamesInStage(self.zoneName) if airbaseNames ~= nil and type(airbaseNames) == "table" then for _, airbaseName in pairs(airbaseNames) do local airbase = StageBase.New(database, logger, airbaseName, spawnManager) table.insert(self._db.airbases, airbase) end end for _, samZoneName in pairs(database:getBlueSamsInStage(self.zoneName)) do local blueSam = BlueSam.New(database, logger, samZoneName, spawnManager) table.insert(self._db.blueSams, blueSam) end local miscGroups = database:getMiscGroupsAtStage(self.zoneName) for _, groupName in pairs(miscGroups) do local miscGroup = SpearheadGroup.New(groupName, spawnManager, true) table.insert(self._db.miscGroups, miscGroup) miscGroup:Destroy() end end Events.AddStageNumberChangedListener(self) return self end function Stage:IsComplete() if self._currentStageState >= StageState.Blue then return true end for _, mission in pairs(self._db.sams) do local state = mission:getState() if state == "ACTIVE" or state == "NEW" or state =="WAITING" then return false end end for _, mission in pairs(self._db.missions) do local state = mission:getState() if state == "ACTIVE" or state == "NEW" then return false end end return true end function Stage:IsActive() return self._currentStageState == StageState.Activated end function Stage:GetStageType() return self._stageType end function Stage:GetStageLaneIdentifier() return self._stageLaneIdentifier end function Stage:GetStageIndex() return self.stageNumber end function Stage:GetStageName() return self.stageName end function Stage:GetMissions() local missions = {} for _, mission in pairs(self._db.missions) do table.insert(missions, mission) end for _, sam in pairs(self._db.sams) do table.insert(missions, sam) end return missions end function Stage:CheckAndUpdateSelf() self._logger:debug("Checking on Stage: " .. self.zoneName) local dbTables = self:GetStageTables() local getAvailableMissions = function () local availableMissions = {} for _, mission in pairs(dbTables.missionsByCode) do if mission:getState() == "NEW" then table.insert(availableMissions, mission) end end return availableMissions end local getActiveMissionsCount = function () local result = 0 for _, mission in pairs(dbTables.missionsByCode) do if mission:getState() == "ACTIVE" then result = result + 1 end end return result end local max = dbTables.maxMissions local availableMissionsCount = Util.tableLength(getAvailableMissions()) local activeCount = getActiveMissionsCount() if activeCount < max and availableMissionsCount > 0 then for _ = activeCount+1, max do if availableMissionsCount == 0 then break else local mission = Util.randomFromList(getAvailableMissions()) if mission then mission:SpawnActive() activeCount = activeCount + 1; else return end availableMissionsCount = availableMissionsCount - 1 end end end end function Stage:IsMissionComplete(missionName) local mission = self._db.missionsByName[missionName] if not mission then return true end return mission:getState() == "COMPLETED" end function Stage:NotifyComplete() self._logger:info("Stage complete: " .. (self.stageName or self.stageNumber or "unknown")) for _, listener in pairs(self._stageCompleteListeners) do pcall(function() listener:OnStageComplete(self) end) end if self.OnPostStageComplete then timer.scheduleFunction(self.OnPostStageComplete, self, timer.getTime() + 3) end end function Stage:AddStageCompleteListener(listener) table.insert(self._stageCompleteListeners, listener) end function Stage:PreActivate() if self._currentStageState >= StageState.PreActivated then return end self._currentStageState = StageState.PreActivated for _, mission in pairs(self._db.sams) do if mission then mission:SpawnInactive() end end for _, mission in pairs(self._db.missions) do if mission and mission.missionType == "DEEPSTRIKE" then mission:SpawnInactive() end end for _, airbase in pairs(self._db.airbases) do airbase:ActivateRedStage() end self:MarkStage() end function Stage:MarkStage() self._logger:debug("Marking stage '" .. Util.toString(self.zoneName) .. "' with state: " .. self._currentStageState) if self._customDrawing then self._customDrawing:Remove() end if self._stageConfig.isDrawStagesEnabled == false then return end if self._stageConfig.isDrawPreActivatedEnabled == false and self._currentStageState == StageState.PreActivated then return end if self._customDrawing then self._customDrawing:UpdateDrawingObject(function(drawingObject) local drawing = drawingObject if self._currentStageState == StageState.Activated then drawing.fillColorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.RED_ACTIVE) drawing.colorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.RED_ACTIVE) drawing.style = "dot dash" elseif self._currentStageState == StageState.PreActivated then drawing.fillColorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.RED_PREACTIVE) drawing.colorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.RED_PREACTIVE) drawing.style = "no line" elseif self._currentStageState == StageState.Blue then drawing.fillColorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.BLUE) drawing.colorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.BLUE) drawing.style = "two dash" else drawing.fillColorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.INVISIBLE) drawing.colorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.INVISIBLE) drawing.style = "no line" end return drawing end) self._customDrawing:Draw() end end function Stage:ActivateStage() if self._currentStageState >= StageState.Activated then return end self:PreActivate() self._currentStageState = StageState.Activated pcall(function() self:MarkStage() end) self._logger:debug("Activating Misc groups for zone. Count: " .. Util.tableLength(self._db.miscGroups)) for _, miscGroup in pairs(self._db.miscGroups) do miscGroup:Spawn() end for _, mission in pairs(self._db.missions) do if mission.missionType == "DEAD" then mission:SpawnActive() end end for _, farp in pairs(self._db.farps) do if farp:IsStartingFarp() == true then farp:Activate() end end for _, supplyHub in pairs(self._db.supplyHubs) do if supplyHub:IsActiveFromStart() == true then supplyHub:Activate() end end if self._db and self._db.stageBriefing then self._missionCommandsHelper:AddStageBriefing(self.zoneName, self._db.stageBriefing) end timer.scheduleFunction(self.CheckContinuousAsync, self, timer.getTime() + 3) end function Stage:GetStageTables() return self._db end function Stage:OnStageNumberChanged(number, stageLaneIdentifier) local needsPreActivation = function() if self._stageLaneIdentifier == stageLaneIdentifier then if self.stageNumber <= number + self._stageConfig.AmountPreactivateStage then return true end elseif self._stageLaneIdentifier == nil then if self._stageRepository:getStageLane(self._stageLaneIdentifier):IsChapterStart(self.stageNumber) and self.stageNumber < number + self._stageConfig.AmountPreactivateStage then return true end if GlobalCapManager.IsCapActiveWhenZoneIsActive(self.zoneName, number) == true then return true end end return false end local needsActivation = function() if self._stageLaneIdentifier == stageLaneIdentifier and self.stageNumber == number then return true end return false end local needsBlueActivation = function() if self._stageLaneIdentifier == stageLaneIdentifier and self.stageNumber < number then return true end return false end if needsPreActivation() == true then self:PreActivate() end if needsActivation() == true then self:ActivateStage() end if needsBlueActivation() == true then self:ActivateBlueStage() end end Stage.OnMissionComplete = function(self, _) self:CheckAndUpdateSelf() end function Stage:ActivateBlueGroups() for _, blueSam in pairs(self._db.blueSams) do blueSam:Activate() end for _, airbase in pairs(self._db.airbases) do airbase:ActivateBlueStage() end if self.OnPostBlueActivated then pcall(function() self:OnPostBlueActivated() end) end for _, farp in pairs(self._db.farps) do if farp:IsStartingFarp() == true then farp:Activate() end end for _, supplyHub in pairs(self._db.supplyHubs) do supplyHub:Activate() end end function Stage:GetStageStats() local strike = 0 local dead = 0 local bai = 0 local cas = 0 for _, mission in pairs(self._db.missions) do if mission.missionType == "STRIKE" then strike = strike + 1 elseif mission.missionType == "DEAD" or mission.missionType == "SAM" then dead = dead + 1 elseif mission.missionType == "BAI" then bai = bai + 1 elseif mission.missionType == "CAS" then cas = cas + 1 end end for _, _ in pairs(self._db.sams) do dead = dead + 1 end return strike, dead, bai, cas end function Stage:ActivateBlueStage() if self._currentStageState >= StageState.Blue then return end self._logger:debug("Setting stage '" .. Util.toString(self.zoneName) .. "' to blue") self._currentStageState = StageState.Blue for _, mission in pairs(self._db.missions) do mission:SpawnPersistedState() end for _, mission in pairs(self._db.sams) do mission:SpawnPersistedState() end for _, miscGroup in pairs(self._db.miscGroups) do miscGroup:Spawn() end local ActivateBlueAsync = function(selfA) pcall(function() selfA:MarkStage() end) selfA:ActivateBlueGroups() return nil end self._missionCommandsHelper:RemoveStageBriefing(self.zoneName) timer.scheduleFunction(ActivateBlueAsync, self, timer.getTime() + 3) end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.stageclasses then ScriptGlobals.classes.stageclasses = {} end if not ScriptGlobals.classes.stageclasses.stages then ScriptGlobals.classes.stageclasses.stages = {} end if not ScriptGlobals.classes.stageclasses.stages.basestage then ScriptGlobals.classes.stageclasses.stages.basestage = {} end ScriptGlobals.classes.stageclasses.stages.basestage.stage = Stage end -- classes.stageclasses.stages.basestage.stage do -- classes.stageclasses.stages.extrastage local Stage = ScriptGlobals.classes.stageclasses.stages.basestage.stage local GlobalCapManager = ScriptGlobals.classes.capclasses.globalcapmanager local StageState = ScriptGlobals.classes.stageclasses.stages.basestage.stagestate local ExtraStage = {} ExtraStage.__index = ExtraStage function ExtraStage.New(database, stageConfig, logger, initData, spawnManager) setmetatable(ExtraStage, Stage) local self = setmetatable({}, { __index = ExtraStage }) self:superNew(database, stageConfig, logger, initData, "ExtraStage", "secondary", spawnManager) self.OnPostBlueActivated = function (selfStage) selfStage:MarkStage() end self.OnPostStageComplete = function (selfStage) selfStage:ActivateBlueStage() end return self end function ExtraStage:OnStageNumberChanged(number, stageLaneIdentifier) if stageLaneIdentifier ~= self._stageLaneIdentifier then return end if self._activeStage == number then return end self._activeStage = number if self.stageNumber - self._activeStage == self._stageConfig.AmountPreactivateStage then self._logger:debug("Pre-activating stage: " .. self.zoneName .. " with number: " .. number) self:PreActivate() elseif GlobalCapManager.IsCapActiveWhenZoneIsActive(self.zoneName, number) == true then self:PreActivate() end if number == self.stageNumber then self:ActivateStage() end if self._currentStageState == StageState.Blue then self:ActivateBlueStage() end end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.stageclasses then ScriptGlobals.classes.stageclasses = {} end if not ScriptGlobals.classes.stageclasses.stages then ScriptGlobals.classes.stageclasses.stages = {} end ScriptGlobals.classes.stageclasses.stages.extrastage = ExtraStage end -- classes.stageclasses.stages.extrastage do -- classes.stageclasses.stages.primarystage local Stage = ScriptGlobals.classes.stageclasses.stages.basestage.stage local PrimaryStage = {} PrimaryStage.__index = PrimaryStage function PrimaryStage.New(database, stageConfig, logger, initData, spawnManager) setmetatable(PrimaryStage, Stage) local self = setmetatable({}, { __index = PrimaryStage }) self:superNew(database, stageConfig, logger, initData, "PrimaryStage", "primary", spawnManager) return self end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.stageclasses then ScriptGlobals.classes.stageclasses = {} end if not ScriptGlobals.classes.stageclasses.stages then ScriptGlobals.classes.stageclasses.stages = {} end ScriptGlobals.classes.stageclasses.stages.primarystage = PrimaryStage end -- classes.stageclasses.stages.primarystage do -- classes.stageclasses.stages.waitingstage local Stage = ScriptGlobals.classes.stageclasses.stages.basestage.stage local WaitingStage = {} WaitingStage.__index = WaitingStage function WaitingStage.New(database, stageConfig, logger, initData, waitingSeconds, spawnManager) setmetatable(WaitingStage, Stage) local self = setmetatable({}, { __index = WaitingStage }) self:superNew(database, stageConfig, logger, initData, "WaitingStage", "none", spawnManager) self._waitTimeSeconds = 5 if waitingSeconds and waitingSeconds > 5 then self._waitTimeSeconds = waitingSeconds end self._startTime = nil self.CheckContinuousAsync = function (selfA, time) if selfA:IsComplete() == true then selfA:NotifyComplete() return nil end return time + 2 end return self end function WaitingStage:ActivateStage() self._logger:info("Starting Waiting Stage '" .. self.zoneName .. "' which will complete in about " .. self._waitTimeSeconds .. " seconds") self._isActive = true self._startTime = timer.getTime() timer.scheduleFunction(self.CheckContinuousAsync, self, self._startTime + self._waitTimeSeconds) end function WaitingStage:IsComplete() if timer.getTime() > (self._startTime + self._waitTimeSeconds) then return true end return false end function WaitingStage:OnStageNumberChanged() self._logger:debug("Waiting Stage OnStageNumberChanged override") end function WaitingStage:MarkStage(_) self._logger:debug("Waiting Stage MarkStage override") end function WaitingStage:GetExpectedTime() return self._startTime + self._waitTimeSeconds end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.stageclasses then ScriptGlobals.classes.stageclasses = {} end if not ScriptGlobals.classes.stageclasses.stages then ScriptGlobals.classes.stageclasses.stages = {} end ScriptGlobals.classes.stageclasses.stages.waitingstage = WaitingStage end -- classes.stageclasses.stages.waitingstage do -- classes.stageclasses.globalstagemanager local Events = ScriptGlobals.classes.spearhead_events local Util = ScriptGlobals.classes.util.util local DcsUtil = ScriptGlobals.classes.util.dcsutil local Logger = ScriptGlobals.classes.util.logger local PersistenceConfig = ScriptGlobals.classes.configuration.persistenceconfig local ExtraStage = ScriptGlobals.classes.stageclasses.stages.extrastage local PrimaryStage = ScriptGlobals.classes.stageclasses.stages.primarystage local WaitingStage = ScriptGlobals.classes.stageclasses.stages.waitingstage local MissionCommandsHelper = ScriptGlobals.classes.stageclasses.helpers.missioncommandshelper local StageRepository = ScriptGlobals.classes.stageclasses.stagerepository local StageLane = ScriptGlobals.classes.stageclasses.stagelane local Persistence = ScriptGlobals.classes.persistence.persistence local currentStage = -99 local GlobalStageManager = {} GlobalStageManager.__index = GlobalStageManager GlobalStageManager.getCurrentStage = function() return currentStage end local singletonInstance = nil function GlobalStageManager.new(database, stageConfig, logLevel, spawnManager) if singletonInstance ~= nil then return singletonInstance end local logger = Logger.new("StageManager", logLevel) logger:info("Using Stage Log Level: " .. logLevel) local self = setmetatable({}, GlobalStageManager) singletonInstance = self self.database = database self.stageConfig = stageConfig self._missionCommandsHelper = MissionCommandsHelper.getOrCreate() self._stageRepository = StageRepository.getInstance() self.logger = logger if stageConfig.isAutoStages ~= true then logger:warn( "Spearhead will not automatically progress stages due to the given settings. If you manually have implemented this, please ignore this message") end Events.AddStageNumberChangedListener(self) for _, stageName in pairs(database:getStagezoneNames()) do logger:debug("Found stage zone with name: " .. stageName) local parseResult = self:ParseStageName(stageName) if parseResult.isValid == false then logger:warn("Stage zone with name " .. stageName .. " is not valid: " .. parseResult.invalidReason) else local initData = { stageZoneName = parseResult.stageZoneName, stageNumber = parseResult.orderNumber, stageDisplayName = parseResult.stageDisplayName, stageLaneIdentifier = parseResult.stageLaneIdentifier } if parseResult.stageType == "PrimaryStage" then local stage = PrimaryStage.New(database, stageConfig, logger, initData, spawnManager) stage:AddStageCompleteListener(self) self._stageRepository:AddStage(stage) elseif parseResult.stageType == "ExtraStage" then local stage = ExtraStage.New(database, stageConfig, logger, initData, spawnManager) stage:AddStageCompleteListener(self) self._stageRepository:AddStage(stage) elseif parseResult.stageType == "WaitingStage" then local waitingStage = WaitingStage.New(database, stageConfig, logger, initData, parseResult.waitingStageSeconds, spawnManager) waitingStage:AddStageCompleteListener(self) self._stageRepository:AddStage(waitingStage) end end end singletonInstance = self return self end function GlobalStageManager:Start() self.logger:info("Starting GlobalStageManager") local startingStage = self.stageConfig.startingStage or 1 local persistenceConfig = PersistenceConfig.new() if persistenceConfig:isEnabled() == true then self.logger:info("Persistence is enabled, loading stage state from persistence") local stageLanes = self._stageRepository:getAllStageLanes() for _, stageLane in pairs(stageLanes) do local stageLaneIdentifier = stageLane:GetStageLaneIdentifier() local persistedStage = Persistence.GetActiveStage(stageLaneIdentifier) if persistedStage then self.logger:info("Loaded persisted stage " .. persistedStage .. " for lane " .. (stageLaneIdentifier or "default")) stageLane:SetActiveStageIndex(persistedStage) Events.PublishStageNumberChanged(persistedStage, stageLaneIdentifier) else stageLane:SetActiveStageIndex(startingStage) Events.PublishStageNumberChanged(startingStage, stageLaneIdentifier) end end else self.logger:info("Persistence is disabled, starting at stage " .. startingStage) local stageLanes = self._stageRepository:getAllStageLanes() for _, stageLane in pairs(stageLanes) do stageLane:SetActiveStageIndex(startingStage) Events.PublishStageNumberChanged(startingStage, stageLane:GetStageLaneIdentifier()) end end end function GlobalStageManager:ParseStageName(stageName) local split = Util.split_string(stageName, "_") if Util.tableLength(split) < 3 then return { isValid = false, invalidReason = "Stage zone with name " .. stageName .. " does not have a order number or valid format" } end local typePart = string.lower(split[1]) if typePart == "missionstage" then local orderNumberString = string.lower(split[2]) local stageType = "PrimaryStage" if Util.endsWith(orderNumberString, "x", true) == true then stageType = "ExtraStage" orderNumberString = orderNumberString:sub(1, -1) end local stageLaneIdentifier = nil local first = orderNumberString:sub(1, 1) if tonumber(first) == nil then stageLaneIdentifier = string.lower(first) orderNumberString = orderNumberString:sub(2) end local orderNumber = tonumber(orderNumberString) if orderNumber == nil then return { isValid = false, invalidReason = "Stage zone with name " .. stageName .. " does not have a valid order number : " .. orderNumberString } end local stageDisplayName = split[3] local result = { isValid = true, orderNumber = orderNumber, stageLaneIdentifier = stageLaneIdentifier, stageDisplayName = stageDisplayName, stageZoneName = stageName, stageType = stageType } return result elseif typePart == "waitingstage" then local stageType = "WaitingStage" local orderNumberString = split[2] local orderNumber = tonumber(orderNumberString) if orderNumber == nil then return { isValid = false, invalidReason = "Waiting Stage zone with name " .. stageName .. " does not have a valid order number : " .. orderNumberString } end local waitingSecondsString = split[3] local waitingSeconds = tonumber(waitingSecondsString) if waitingSeconds == nil then return { isValid = false, invalidReason = "Waiting Stage zone with name " .. stageName .. " does not have a valid amount of seconds parameter : " .. waitingSecondsString } end local stageDisplayName = "Waiting Stage " .. orderNumber local result = { isValid = true, orderNumber = orderNumber, stageLaneIdentifier = nil, stageDisplayName = stageDisplayName, stageZoneName = stageName, stageType = stageType, waitingStageSeconds = waitingSeconds } return result end return { isValid = false, invalidReason = "Stage zone with name " .. stageName .. " has an unrecognized type: " .. typePart } end function GlobalStageManager:OnStageComplete(stage) self.logger:debug("Receiving stage complete event from: " .. stage.zoneName) local stageIndex = stage:GetStageIndex() local laneIdentifier = stage:GetStageLaneIdentifier() local stageLane = self._stageRepository:getStageLane(laneIdentifier) if not stageLane or not stageLane:IsCurrentStageIndexComplete() then self.logger:debug("Stage lane " .. (laneIdentifier or "default") .. " is not complete for stage index " .. stageIndex) return end local nextStageIndex = stageIndex + 1 Events.PublishStageNumberChanged(nextStageIndex, laneIdentifier) stageLane:SetActiveStageIndex(nextStageIndex) if stageLane:IsDefaultStageLane() then local allLanes = self._stageRepository:getAllStageLanes() for _, lane in ipairs(allLanes) do if lane:GetStageLaneIdentifier() ~= StageLane.DefaultLaneKey and lane:GetStageLaneState() == "BetweenChapters" then local nextChapterStart = lane:GetNextChapterStart() if nextChapterStart == nextStageIndex then self.logger:debug("Lane " .. (lane:GetStageLaneIdentifier() or "default") .. " is at chapter start for next stage index " .. nextStageIndex) Events.PublishStageNumberChanged(nextStageIndex, lane:GetStageLaneIdentifier()) lane:SetActiveStageIndex(nextStageIndex) end end end else local defaultStageLane = self._stageRepository:getStageLane(StageLane.DefaultLaneKey) if defaultStageLane and defaultStageLane:GetStageLaneState() == "BetweenChapters" then local nextChapterStart = defaultStageLane:GetNextChapterStart() if nextChapterStart == nextStageIndex then local allLanes = self._stageRepository:getAllStageLanes() local allSideLanesReady = true for _, lane in ipairs(allLanes) do if lane:GetStageLaneIdentifier() ~= StageLane.DefaultLaneKey then if lane:GetStageLaneState() == "InChapter" and lane:GetActiveStageIndex() < nextStageIndex then allSideLanesReady = false self.logger:debug("Lane " .. (lane:GetStageLaneIdentifier() or "default") .. " is not ready for next stage index " .. nextStageIndex) break elseif lane:GetStageLaneState() == "BetweenChapters" then local nextChapter = lane:GetNextChapterStart() if nextChapter and nextChapter < nextStageIndex then allSideLanesReady = false self.logger:debug("Lane " .. (lane:GetStageLaneIdentifier() or "default") .. " is not ready for next stage index " .. nextStageIndex) break end end end end if allSideLanesReady then self.logger:debug("All side lanes are ready for next stage index " .. nextStageIndex .. ", activating default lane") Events.PublishStageNumberChanged(nextStageIndex, nil) defaultStageLane:SetActiveStageIndex(nextStageIndex) else self.logger:debug("Not all side lanes are ready for next stage index " .. nextStageIndex .. ", default lane will not be activated") end end end end end function GlobalStageManager:IsStageComplete(stageNumber, stageLaneIdentifier) local stageLane = self._stageRepository:getStageLane(stageLaneIdentifier) if not stageLane then self.logger:warn("Stage lane " .. (stageLaneIdentifier or "default") .. " does not exist") return nil end return stageLane:IsStageIndexComplete(stageNumber) end function GlobalStageManager:OnStageNumberChanged(stageNumber, stageLaneIdentifier) if stageLaneIdentifier ~= nil then return end self.logger:debug("Stage number changed to: " .. tostring(stageNumber)) self:UpdateDrawings(stageNumber) end function GlobalStageManager:OnStageNumberChangeComplete(stageNumber, stageLaneIdentifier) if stageLaneIdentifier ~= nil then return end self.logger:debug("Stage number change complete to: " .. tostring(stageNumber)) local groups = {} for _, player in pairs(DcsUtil.getAllPlayerUnits()) do local group = player:getGroup() if group then table.insert(groups, group) end end for _, group in pairs(groups) do self._missionCommandsHelper:OverviewToGroup(group:getID()) end end function GlobalStageManager:UpdateDrawings(stageNumber, stageLaneIdentifier) self.logger:debug("Updating custom drawings for stage number: " .. tostring(stageNumber)) local drawings = self.database:getStageDrawings() for _, drawing in pairs(drawings) do local startStage = drawing:GetStartingStage() local stopStage = drawing:GetRemoveAtStage() local laneIdentifier = drawing:GetStageLaneIdentifier() if laneIdentifier == stageLaneIdentifier then if stageNumber >= startStage and stageNumber < stopStage then self.logger:debug("Drawing " .. drawing:GetName() .. " is active for stage number: " .. tostring(stageNumber)) drawing:Draw() else self.logger:debug("Drawing " .. drawing:GetName() .. " is not active for stage number: " .. tostring(stageNumber)) drawing:Remove() end end end end function GlobalStageManager:PrintMermaidStage() local lanes = self._stageRepository:getAllStageLanes() local nodes = {} local edges = {} local stageIndicesByLane = {} local mainLaneId = "default" local laneColors = { "#FF6B6B", -- 1: Red "#4ECDC4", -- 2: Teal "#45B7D1", -- 3: Blue "#FFA502", -- 4: Orange "#95E1D3", -- 5: Mint "#F38181", -- 6: Coral "#AA96DA", -- 7: Purple "#FCBAD3", -- 8: Pink "#A8E6CF", -- 9: Light green "#FFD3B6", -- 10: Peach "#FFAAA5", -- 11: Light red "#FF8B94", -- 12: Rose "#FFEAA7", -- 13: Butter "#DFE6E9", -- 14: Gray "#00B894", -- 15: Emerald "#0984E3", -- 16: Cobalt "#6C5CE7", -- 17: Indigo "#A29BFE", -- 18: Lavender "#FD79A8", -- 19: Magenta "#FDCB6E", -- 20: Gold "#6C757D", -- 21: Slate "#20C997", -- 22: Seafoam "#E74C3C", -- 23: Scarlet "#3498DB", -- 24: Dodger blue "#9B59B6", -- 25: Amethyst "#1ABC9C", -- 26: Turquoise } local laneColorMap = {} local colorIndex = 1 for _, lane in ipairs(lanes) do local laneId = lane:GetStageLaneIdentifier() or "default" local stageIndices = lane:GetAllStageIndices() stageIndicesByLane[laneId] = stageIndices laneColorMap[laneId] = laneColors[colorIndex] colorIndex = colorIndex + 1 if colorIndex > #laneColors then colorIndex = 1 end for _, stageIndex in ipairs(stageIndices) do local stages = lane:GetStagesAtIndex(stageIndex) if stages then for _, stage in ipairs(stages) do local nodeId = laneId .. "_" .. stageIndex local stageType = stage:GetStageType() local stageName = stage.stageName or stage.zoneName local bracketLabel if laneId == "default" then bracketLabel = "[" .. stageIndex .. "]" else bracketLabel = "[" .. laneId .. stageIndex .. "]" end local label = bracketLabel .. " " .. stageName local nodeShape = "[" local nodeEnd = "]" if stageType == "ExtraStage" then nodeShape = "([" nodeEnd = "])" elseif stageType == "WaitingStage" then nodeShape = "[[" nodeEnd = "]]" end nodes[nodeId] = string.format(' %s%s"%s"%s', nodeId, nodeShape, label, nodeEnd) end end end end for laneId, stageIndices in pairs(stageIndicesByLane) do for i = 1, #stageIndices - 1 do local fromIdx = stageIndices[i] local toIdx = stageIndices[i + 1] local fromNodeId = laneId .. "_" .. fromIdx local toNodeId = laneId .. "_" .. toIdx local isChapter = false for _, lane in ipairs(lanes) do local normalizedLaneId = lane:GetStageLaneIdentifier() or "default" if normalizedLaneId == laneId then isChapter = lane:IsChapterStart(toIdx) break end end local label = isChapter and "|chapter|" or "" table.insert(edges, string.format(' %s -->%s %s', fromNodeId, label, toNodeId)) end end local mainStageLane = nil for _, lane in ipairs(lanes) do if lane:IsDefaultStageLane() then mainStageLane = lane break end end if mainStageLane then for _, lane in ipairs(lanes) do if lane:IsDefaultStageLane() == false then local sideId = lane:GetStageLaneIdentifier() local sideIndices = stageIndicesByLane[sideId] or {} for _, stageIdx in ipairs(sideIndices) do if lane:IsChapterStart(stageIdx) then local fromNodeId = mainLaneId .. "_" .. (stageIdx - 1) local toNodeId = sideId .. "_" .. stageIdx table.insert(edges, string.format(' %s -->|unlock| %s', fromNodeId, toNodeId)) end end end end end if mainStageLane then local mainIndices = stageIndicesByLane[mainLaneId] for i = 2, #mainIndices do local nextIdx = mainIndices[i] if mainStageLane:IsChapterStart(nextIdx) then for _, lane in ipairs(lanes) do if lane:IsDefaultStageLane() == false then local sideId = lane:GetStageLaneIdentifier() local sideIndices = stageIndicesByLane[sideId] or {} local gateStageIdx = nil for _, idx in ipairs(sideIndices) do if idx >= nextIdx then gateStageIdx = idx break end end if gateStageIdx == nil and #sideIndices > 0 then gateStageIdx = sideIndices[#sideIndices] end if gateStageIdx then local fromNodeId = sideId .. "_" .. gateStageIdx local toNodeId = mainLaneId .. "_" .. nextIdx table.insert(edges, string.format(' %s -->|gate| %s', fromNodeId, toNodeId)) end end end end end end local diagramLines = { "graph TD" } for laneId, color in pairs(laneColorMap) do table.insert(diagramLines, string.format(' classDef lane_%s fill:%s,stroke:#333,stroke-width:2px,color:#000', laneId, color)) end for _, nodeStr in pairs(nodes) do table.insert(diagramLines, nodeStr) end for _, edgeStr in pairs(edges) do table.insert(diagramLines, edgeStr) end for nodeId in pairs(nodes) do local laneId = nodeId:match("(.+)_[0-9]+$") if laneId then table.insert(diagramLines, string.format(' class %s lane_%s', nodeId, laneId)) end end local diagram = "========== STAGE FLOW DIAGRAM ==========\n" .. table.concat(diagramLines, "\n") .. "\n========== END DIAGRAM ==========" self.logger:info(diagram) end GlobalStageManager.isStageComplete = function(stageNumber, stageLaneIdentifier) if singletonInstance == nil then Logger.new("StageManager", "INFO"):warn( "GlobalStageManager.isStageComplete called before GlobalStageManager was initialized. Returning nil") return nil end if stageLaneIdentifier then stageLaneIdentifier = string.lower(stageLaneIdentifier) end return singletonInstance:IsStageComplete(stageNumber, stageLaneIdentifier) end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.stageclasses then ScriptGlobals.classes.stageclasses = {} end ScriptGlobals.classes.stageclasses.globalstagemanager = GlobalStageManager end -- classes.stageclasses.globalstagemanager do -- classes.spearhead_routeutil local DcsUtil = ScriptGlobals.classes.util.dcsutil local Util = ScriptGlobals.classes.util.util local ROUTE_UTIL = {} do local function GetCAPTargetTypes(attackHelos) local targetTypes = { [1] = "Planes", } if attackHelos then targetTypes[2] = "Helicopters" end return targetTypes end local RtbTask = function(airdromeId, basePoint, speed) if basePoint == nil then basePoint = DcsUtil.getAirbaseById(airdromeId):getPoint() end return { alt = basePoint.y, action = "Landing", alt_type = "BARO", speed = speed, ETA = 0, ETA_locked = false, x = basePoint.x, y = basePoint.z, speed_locked = true, formation_template = "", airdromeId = airdromeId, type = "Land", task = { id = "ComboTask", params = { tasks = {} } } } end local CapTask = function(groupName, position, altitude, speed, duration, engageHelos, deviationdistance, pattern) local durationBefore10 = duration - 600 if durationBefore10 < 0 then durationBefore10 = 0 end local durationAfter10 = 600 if duration < 600 then durationAfter10 = duration end return { alt = altitude, action = "Turning Point", alt_type = "BARO", speed = speed, ETA = 0, ETA_locked = false, x = position.x, y = position.z, speed_locked = true, formation_template = "", task = { id = "ComboTask", params = { tasks = { [1] = { number = 1, auto = false, id = "WrappedAction", enabled = "true", params = { action = { id = "Script", params = { command = "pcall(GlobalCapCallBacks.PublishOnStation, \"" .. groupName .. "\")" } } } }, [2] = { id = 'EngageTargets', params = { maxDist = deviationdistance, maxDistEnabled = deviationdistance >= 0, -- required to check maxDist targetTypes = GetCAPTargetTypes(engageHelos), priority = 0 } }, [3] = { number = 3, auto = false, id = "ControlledTask", enabled = true, params = { task = { id = "Orbit", params = { altitude = altitude, pattern = pattern, speed = speed, } }, stopCondition = { duration = durationBefore10, condition = "return GlobalCapCallBacks.IsBingoFuel(\"" .. groupName .. "\", 0.10)", } } }, [4] = { number = 4, auto = false, id = "WrappedAction", enabled = "true", params = { action = { id = "Script", params = { command = "pcall(GlobalCapCallBacks.PublishRTBInTen, \"" .. groupName .. "\")" } } } }, [5] = { number = 5, auto = false, id = "ControlledTask", enabled = true, params = { task = { id = "Orbit", params = { altitude = altitude, pattern = pattern, speed = speed, } }, stopCondition = { duration = durationAfter10, condition = "return GlobalCapCallBacks.IsBingoFuel(\"" .. groupName .. "\", 0.10)", } } }, [6] = { number = 6, auto = false, id = "WrappedAction", enabled = "true", params = { action = { id = "Script", params = { command = "pcall(GlobalCapCallBacks.PublishRTB, \"" .. groupName .. "\")" } } } } } } } } end local FlyToPointTask = function(position, altitude, speed, childTasks) return { alt = altitude, action = "Turning Point", alt_type = "BARO", speed = speed, ETA = 0, ETA_locked = false, x = position.x, y = position.z, speed_locked = true, formation_template = "", task = { id = "ComboTask", params = { tasks = childTasks or {} } } } end ROUTE_UTIL.createCapMission = function(groupName, airdromeId, capPoint, racetrackSecondPoint, altitude, speed, durationOnStation, attackHelos, deviationDistance) local baseName = DcsUtil.getAirbaseName(airdromeId) if baseName == nil then return nil end durationOnStation = durationOnStation or 1800 altitude = altitude or 3000 speed = speed or 130 attackHelos = attackHelos or false deviationDistance = deviationDistance or 32186 local base = Airbase.getByName(baseName) if base == nil then return nil end local additionalFlyOverTasks = { { enabled = true, auto = false, id = "WrappedAction", number = 1, params = { action = { id = "Option", params = { variantIndex = 2, name = AI.Option.Air.id.FORMATION, formationIndex = 2, value = 131074 } } } } } local orbitType = "Circle" if racetrackSecondPoint then orbitType = "Race-Track" end local basePoint = base:getPoint() local points if racetrackSecondPoint == nil then points = { [1] = FlyToPointTask(capPoint, altitude, speed, additionalFlyOverTasks), [2] = CapTask(groupName, capPoint, altitude, speed, durationOnStation, attackHelos, deviationDistance, orbitType), [3] = RtbTask(airdromeId, basePoint, speed) } else points = { [1] = FlyToPointTask(capPoint, altitude, speed, additionalFlyOverTasks), [2] = CapTask(groupName, capPoint, altitude, speed, durationOnStation, attackHelos, deviationDistance, orbitType), [3] = FlyToPointTask(racetrackSecondPoint, altitude, speed, {}), [4] = RtbTask(airdromeId, basePoint, speed) } end return { id = 'Mission', params = { airborne = true, route = { points = points } } } end ROUTE_UTIL.CreateRTBMission = function(groupName, airdromeId, speed) local base = DcsUtil.getAirbaseById(airdromeId) if base == nil then return nil, "No airbase found for ID " .. tostring(airdromeId) end local group = Group.getByName(groupName) local pos; local i = 1 if group == nil then return nil, "No group found for name " .. groupName end local units = group:getUnits() while pos == nil and i <= Util.tableLength(units) do local unit = units[i] if unit and unit:isExist() == true and unit:inAir() == true then pos = unit:getPoint() end i = i + 1 end speed = speed or 130 if pos == nil then return nil, "Could not find any unit in the air to set the RTB task" end local additionalFlyOverTasks = { { enabled = true, auto = false, id = "WrappedAction", number = 1, params = { action = { id = "Option", params = { variantIndex = 2, name = AI.Option.Air.id.FORMATION, formationIndex = 2, value = 131074 } } } } } return { id = "Mission", params = { airborne = true, -- RTB mission generally are given to airborne units route = { points = { [1] = { alt = pos.y, action = "Turning Point", alt_type = "BARO", speed = speed, ETA = 0, ETA_locked = false, x = pos.x, y = pos.z, speed_locked = true, formation_template = "", task = { id = "ComboTask", params = { tasks = { [1] = { number = 1, auto = false, id = "WrappedAction", enabled = "true", params = { action = { id = "Script", params = { command = "pcall(GlobalCapCallBacks.PublishRTB, \"" .. groupName .. "\")" } } } } } } } }, [2] = FlyToPointTask(base:getPoint(), 600, speed, additionalFlyOverTasks), [3] = RtbTask(airdromeId, base:getPoint(), speed) } } } }, "" end ROUTE_UTIL.CreateCarrierRacetrack = function(pointA, pointB) return { id = "Mission", params = { airborne = false, route = { points = { [1] = { ["alt"] = -0, ["type"] = "Turning Point", ["ETA"] = 0, ["alt_type"] = "BARO", ["formation_template"] = "", ["y"] = pointA.z, ["x"] = pointA.x, ["ETA_locked"] = false, ["speed"] = 13.88888, ["action"] = "Turning Point", ["task"] = { ["id"] = "ComboTask", ["params"] = { ["tasks"] = {}, }, -- end of ["params"] }, -- end of ["task"] ["speed_locked"] = true, }, [2] = { ["alt"] = -0, ["type"] = "Turning Point", ["ETA"] = -0, ["alt_type"] = "BARO", ["formation_template"] = "", ["y"] = pointB.z, ["x"] = pointB.x, ["ETA_locked"] = false, ["speed"] = 13.88888, ["action"] = "Turning Point", ["task"] = { ["id"] = "ComboTask", ["params"] = { ["tasks"] = { [1] = { ["enabled"] = true, ["auto"] = false, ["id"] = "GoToWaypoint", ["number"] = 1, ["params"] = { ["fromWaypointIndex"] = 2, ["nWaypointIndx"] = 1, }, }, }, }, }, ["speed_locked"] = true, } } } } }, nil end end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end ScriptGlobals.classes.spearhead_routeutil = ROUTE_UTIL end -- classes.spearhead_routeutil do -- classes.fleetclasses.fleetgroup local Util = ScriptGlobals.classes.util.util local DcsUtil = ScriptGlobals.classes.util.dcsutil local MissionEditorWarning = ScriptGlobals.classes.util.missioneditorwarnings local RouteUtil = ScriptGlobals.classes.spearhead_routeutil local SpearheadEvents = ScriptGlobals.classes.spearhead_events local FleetGroup = {} FleetGroup.__index = FleetGroup function FleetGroup.new(fleetGroupName, database, logger) local self = setmetatable({}, FleetGroup) self.fleetGroupName = fleetGroupName self.logger = logger local split_name = Util.split_string(fleetGroupName, "_") if Util.tableLength(split_name) < 2 then MissionEditorWarning.Add("CARRIERGROUP should have at least 2 parts. CARRIERGROUP_") return nil end self.fleetNameIdentifier = split_name[2] self.targetZonePerStage = {} self.currentTargetZone = nil self.pointsPerZone = {} do local carrierRouteZones = database:getCarrierRouteZones() for _, zoneName in pairs(carrierRouteZones) do if Util.strContains(string.lower(zoneName), "_".. string.lower(self.fleetNameIdentifier) .. "_" ) == true then local zone = DcsUtil.getZoneByName(zoneName) if zone and zone.zone_type == DcsUtil.ZoneType.Polygon then local split_string = Util.split_string(zoneName, "_") if Util.tableLength(split_string) < 3 then MissionEditorWarning.Add( "CARRIERROUTE should at least have 3 parts. Check the documentation for: " .. zoneName) else local function GetTwoFurthestPoints(zoneA) local biggest = nil local biggestA = zoneA.verts[1] local biggestB = zoneA.verts[2] for i = 1, 3 do for ii = i + 1, 4 do local a = zoneA.verts[i] local b = zoneA.verts[ii] local dist = Util.VectorDistance2d(a, b) if biggest == nil or dist > biggest then biggestA = a biggestB = b biggest = dist end end end return { x = biggestA.x, y = biggestA.y }, { x = biggestB.x, y = biggestB.y } end local function getMinMaxStage(namePart) if namePart == nil then return nil, nil end if Util.startsWith(namePart, "%[") == true then namePart = Util.split_string(namePart, "[")[1] end if Util.strContains(namePart, "%]") == true then namePart = Util.split_string(namePart, "]")[1] end local split_numbers = Util.split_string(namePart, "-") if Util.tableLength(split_numbers) < 2 then MissionEditorWarning.Add("CARRIERROUTE zone stage numbers not in the format _[-]: " .. zoneName) return nil, nil end local first = tonumber(split_numbers[1]) local second = tonumber(split_numbers[2]) if first == nil or second == nil then MissionEditorWarning.Add("CARRIERROUTE zone stage numbers not in the format _[-]: " .. zoneName) return nil, nil end return first, second end local pointA, pointB = GetTwoFurthestPoints(zone) local first, second = getMinMaxStage(split_string[3]) if first ~= nil and second ~= nil then for i = first, second do self.targetZonePerStage[tostring(i)] = zoneName end local entry = { pointA = {x = pointA.x, z = pointA.y, y = 0}, pointB = {x = pointB.x, z = pointB.y, y = 0} } self.pointsPerZone[zoneName] = entry else MissionEditorWarning.Add("CARRIERROUTE zone stage numbers not in the format _[-]: " .. zoneName) end end else MissionEditorWarning.Add("CARRIERROUTE cannot be a cilinder: " .. zoneName) end end end end SpearheadEvents.AddStageNumberChangedListener(self) return self end local SetTaskAsync = function(input, _) local targetZone = input.targetZone local task = input.task local groupName = input.groupName local l_logger = input.logger local group = Group.getByName(groupName) if group then l_logger:info("Sending " .. groupName .. " to " .. targetZone) group:getController():setTask(task) end end function FleetGroup:OnStageNumberChanged(number, laneIdentifier) if laneIdentifier ~= nil then return end local targetZone = self.targetZonePerStage[tostring(number)] if targetZone and targetZone ~= self.currentTargetZone then local points = self.pointsPerZone[targetZone] local task = RouteUtil.CreateCarrierRacetrack(points.pointA, points.pointB) timer.scheduleFunction(SetTaskAsync, { task = task, targetZone = targetZone, groupName = self.fleetGroupName, logger = self.logger }, timer.getTime() + 5) end end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.fleetclasses then ScriptGlobals.classes.fleetclasses = {} end ScriptGlobals.classes.fleetclasses.fleetgroup = FleetGroup end -- classes.fleetclasses.fleetgroup do -- classes.fleetclasses.globalfleetmanager local Logger = ScriptGlobals.classes.util.logger local Util = ScriptGlobals.classes.util.util local FleetGroup = ScriptGlobals.classes.fleetclasses.fleetgroup local MizGroupsManager = ScriptGlobals.classes.helpers.mizgroupsmanager local GlobalFleetManager = {} local fleetGroups = {} GlobalFleetManager.start = function(database) local logger = Logger.new("CARRIERFLEET", "INFO") local all_groups = MizGroupsManager.getAllGroupNames() for _, groupName in pairs(all_groups) do if Util.startsWith(string.lower(groupName), "carriergroup" ) == true then logger:info("Registering " .. groupName .. " as a managed fleet") local carrierGroup = FleetGroup.new(groupName, database, logger) table.insert(fleetGroups, carrierGroup) end end end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.fleetclasses then ScriptGlobals.classes.fleetclasses = {} end ScriptGlobals.classes.fleetclasses.globalfleetmanager = GlobalFleetManager end -- classes.fleetclasses.globalfleetmanager do -- classes.debug.debugmenu local Logger = ScriptGlobals.classes.util.logger local StageRepository = ScriptGlobals.classes.stageclasses.stagerepository local DebugMenu = {} DebugMenu.__index = DebugMenu function DebugMenu.new() local self = setmetatable({}, DebugMenu) self._logger = Logger.new("DebugMenu") self._stageRepository = StageRepository.getInstance() return self end local menuName = "Spearhead Debug" local debugMenuPath = { [1] = menuName } function DebugMenu:RegisterMenus() missionCommands.addSubMenu(menuName, {}) local refresh = function(params) local selfA = params.self selfA:RefreshMenu() end missionCommands.addCommand("Refresh Menu", debugMenuPath, refresh, { self = self }) self:AddStageOptions() end function DebugMenu:RefreshMenu() missionCommands.removeItem(debugMenuPath) self:RegisterMenus() end function DebugMenu:AddStageOptions() local stageMenuTable = missionCommands.addSubMenu("Stages", debugMenuPath) local stageLanes = self._stageRepository:getAllStageLanes() for _, stageLane in pairs(stageLanes) do local stages = stageLane:GetStagesAtIndex(stageLane:GetActiveStageIndex()) if stages then for _, stage in pairs(stages) do local stageLaneId = stageLane:GetStageLaneIdentifier() or "" local stageIndex = stage:GetStageIndex() local stageName = stage:GetStageName() local stageMenuName = stageLaneId .. stageIndex .. "_" .. stageName local currentStageMenuTable = missionCommands.addSubMenu(stageMenuName, stageMenuTable) local completeStage = function(params) local stageA = params.stage local missions = stageA:GetMissions() for _, mission in pairs(missions) do mission:ForceMissionComplete() end end local params = { self = self, stage = stage } missionCommands.addCommand("Complete Stage", currentStageMenuTable, completeStage, params) end else self._logger:info("No stages found for lane: " .. (stageLane:GetStageLaneIdentifier() or "default") .. " at index: " .. (stageLane:GetActiveStageIndex() or "nil")) end end end if not ScriptGlobals.classes then ScriptGlobals.classes = {} end if not ScriptGlobals.classes.debug then ScriptGlobals.classes.debug = {} end ScriptGlobals.classes.debug.debugmenu = DebugMenu end -- classes.debug.debugmenu do -- main local Logger = ScriptGlobals.classes.util.logger local Database = ScriptGlobals.classes.spearhead_db local SpearheadEvents = ScriptGlobals.classes.spearhead_events local MissionCommandsHelper = ScriptGlobals.classes.stageclasses.helpers.missioncommandshelper local GlobalConfig = ScriptGlobals.classes.configuration.globalconfig local CapConfig = ScriptGlobals.classes.configuration.capconfig local StageConfig = ScriptGlobals.classes.configuration.stageconfig local Persistence = ScriptGlobals.classes.persistence.persistence local PersistenceConfig = ScriptGlobals.classes.configuration.persistenceconfig local SpawnManager = ScriptGlobals.classes.helpers.spawnmanager local DetectionManager = ScriptGlobals.classes.capclasses.detection.detectionmanager local GlobalCapManager = ScriptGlobals.classes.capclasses.globalcapmanager local GlobalStageManager = ScriptGlobals.classes.stageclasses.globalstagemanager local GlobalFleetManager = ScriptGlobals.classes.fleetclasses.globalfleetmanager local MissionEditorWarnings = ScriptGlobals.classes.util.missioneditorwarnings local defaultLogLevel = "INFO" if SpearheadConfig and SpearheadConfig.debugEnabled == true then defaultLogLevel = "DEBUG" end local startTime = timer.getTime() * 1000 SpearheadEvents.Init(defaultLogLevel) local dbLogger = Logger.new("database", defaultLogLevel) local standardLogger = Logger.new("", defaultLogLevel) local databaseManager = Database.New(dbLogger) MissionCommandsHelper.getOrCreate() local capConfig = CapConfig:new(); local stageConfig = StageConfig:getInstance(); local persistenceConfig = PersistenceConfig.new() if persistenceConfig and persistenceConfig:isEnabled() == true then local persistenceLogger = Logger.new("Persistence", defaultLogLevel) Persistence.Init(persistenceLogger) end local spawnLogger = Logger.new("SpawnManager", defaultLogLevel) local spawnManager = SpawnManager.new(spawnLogger) local detectionLogger = Logger.new("DetectionManager", defaultLogLevel) local detectionManager = DetectionManager.New(detectionLogger) GlobalCapManager.start(databaseManager, capConfig, detectionManager, defaultLogLevel, spawnManager) local globalStageManager = GlobalStageManager.new(databaseManager, stageConfig, defaultLogLevel, spawnManager) GlobalFleetManager.start(databaseManager) env.info(startTime .. "ms / " .. timer.getTime() * 1000 .. "ms") local duration = (timer.getTime() * 1000) - startTime standardLogger:info("Spearhead Initialisation duration: " .. tostring(duration) .. "ms") local missionEditorWarningsLogger = Logger.new("MissionEditorWarnings", defaultLogLevel) MissionEditorWarnings.WriteAll(missionEditorWarningsLogger) globalStageManager:PrintMermaidStage() local startDelayed = function() globalStageManager:Start() return nil end timer.scheduleFunction(startDelayed, nil, timer.getTime() + 5) local globalConfig = GlobalConfig.New() if globalConfig and globalConfig:isDebugMenuEnabled() == true then local DebugMenu = ScriptGlobals.classes.debug.debugmenu local debugMenu = DebugMenu.new() debugMenu:RegisterMenus() end end -- main do -- classes.api.spearheadapi local SpearheadEvents = ScriptGlobals.classes.spearhead_events local GlobalStageManager = ScriptGlobals.classes.stageclasses.globalstagemanager local MissionCompleteListeners = {} SpearheadAPI = { Stages = { changeStage = function(stageNumber) if type(stageNumber) ~= "number" then return false, "stageNumber " .. stageNumber .. " is not a valid number" end SpearheadEvents.PublishStageNumberChanged(stageNumber) return true, "" end, getCurrentStage = function() return GlobalStageManager.getCurrentStage() or nil end, isStageComplete = function(stageNumber, stageLaneIdentifier) if type(stageNumber) ~= "number" then return false, "stageNumber " .. stageNumber .. " is not a valid number" end local isComplete = GlobalStageManager.isStageComplete(stageNumber, stageLaneIdentifier) if isComplete == nil then return nil, "no stage found with number " .. stageNumber end return isComplete, "" end }, Missions = { addOnMissionCompleteListener = function(listener) if type(listener) ~= "table" or type(listener.onMissionComplete) ~= "function" then error("listener is not a valid OnMissionCompleteListener") end table.insert(MissionCompleteListeners, listener) end }, --- Internal Functions for the API that can be called through the rest of the Framework Internal = { notifyMissionComplete = function(zone_name) for _, listener in ipairs(MissionCompleteListeners) do pcall(function() listener:onMissionComplete(zone_name) end) end end, } } end -- classes.api.spearheadapi do -- classes.api.spearheadapidoc SpearheadAPI = SpearheadAPI end -- classes.api.spearheadapidoc do -- classes.capclasses.taskings.callbacks.globalcallbacks local DcsUtil = ScriptGlobals.classes.util.dcsutil local Events = ScriptGlobals.classes.spearhead_events GlobalCapCallBacks = {} function GlobalCapCallBacks.IsBingoFuel(groupName, fuelPercent) return DcsUtil.IsBingoFuel(groupName, fuelPercent) end function GlobalCapCallBacks.NeedsRTBInTen(groupName, fuelOffset) return DcsUtil.NeedsRTBInTen(groupName, fuelOffset) end function GlobalCapCallBacks.PublishRTBInTen(groupName) return Events.PublishRTBInTen(groupName) end function GlobalCapCallBacks.PublishRTB(groupName) return Events.PublishRTB(groupName) end function GlobalCapCallBacks.PublishOnStation(groupName) return Events.PublishOnStation(groupName) end end -- classes.capclasses.taskings.callbacks.globalcallbacks