--- DEFAULT Values if Spearhead == nil then Spearhead = {} end local UTIL = {} do -- INIT UTIL ---splits a string in sub parts by seperator ---@param input string ---@param seperator string ---@return table result list of strings function UTIL.split_string(input, seperator) if seperator == nil then seperator = " " end local result = {} if input == nil then return result end for str in string.gmatch(input, "[^" .. seperator .. "]+") do table.insert(result, str) end return result end ---comment ---@param table any ---@return number 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 ---@param orig table ---@return table copy function UTIL.deepCopyTable(orig) local orig_type = type(orig) local copy if orig_type == 'table' then copy = {} for orig_key, orig_value in next, orig, nil do copy[UTIL.deepCopyTable(orig_key)] = UTIL.deepCopyTable(orig_value) end setmetatable(copy, UTIL.deepCopyTable(getmetatable(orig))) else -- number, string, boolean, etc copy = orig end return copy end ---Gets a random from the list ---@param list Array ---@return any @random element from the list 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 ---@param list Array ---@param start number start ---@param n number length ---@return Array function UTIL.sublist(list, start, n) local result = {} for i = start, n do result[#result + 1] = list[i] end return result end ---@param str string ---@param find string ---@param replace string ---@return string 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 local sb = {} for key, value in pairs(tt) do table.insert(sb, string.rep(" ", indent)) -- indent it 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)) -- indent it 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 ---comment ---@param str string ---@param findable string ---@param ignoreCase boolean? ---@return boolean 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 ---comment ---@param str string ---@param findable string ---@return boolean UTIL.strContains = function(str, findable) return str:find(findable) ~= nil end ---comment ---@param str string ---@param findableTable table ---@return boolean UTIL.startswithAny = function(str, findableTable) for key, 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 ---comment ---@param a Vec2 ---@param b Vec2 ---@return number function UTIL.VectorDistance2d(a, b) return math.sqrt((b.x - a.x) ^ 2 + (b.y - a.y) ^ 2) end ---comment ---@param a Vec3 ---@param b Vec3 ---@return number function UTIL.VectorDistance3d(a, b) return UTIL.vectorMagnitude({ x = a.x - b.x, y = a.y - b.y, z = a.z - b.z }) end ---comment ---@param vec Vec3 ---@return number function UTIL.vectorMagnitude(vec) return (vec.x ^ 2 + vec.y ^ 2 + vec.z ^ 2) ^ 0.5 end ---@param vec Vec3 ---@return Vec3 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 ---@param vec Vec2 ---@param direction number @in degrees ---@param distance number ---@return Vec2 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 ---comment ---@param vec1 Vec2 ---@param vec2 Vec2 ---@return number in degrees 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 ---@param vec1 Vec3 ---@param vec2 Vec3 ---@return number alignment a number in range [-1,1]. > 0 same direction. < 0 opposite direction 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 -- if (yp < y1) != (yp < y2) and xp < x1 + ((yp-y1)/(y2-y1))*(x2-x1) then -- count = count + 1 end end return count % 2 == 1 end ---comment ---@param polygon Array of pairs { x, y } ---@param x number X location ---@param y number Y location ---@return boolean function UTIL.IsPointInPolygon(polygon, x, y) return isInComplexPolygon(polygon, x, y) end ---@param point Vec3 ---@param zone SpearheadTriggerZone 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 ---@param point Vec2 ---@param zone SpearheadTriggerZone 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 ---comment ---@param points Array points ---@return Array hullPoints function UTIL.getConvexHull(points) if #points == 0 then return {} end ---comment ---@param a Vec2 ---@param b Vec2 ---@param c Vec2 ---@return boolean 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 = {} -- lower 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 -- upper hull 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 ---Splits points into clusters with at least minSeparation between clusters, returns convex hull for each cluster ---@param points Array ---@param minSeparation number ---@return Array> hulls function UTIL.getSeparatedConvexHulls(points, minSeparation) if #points == 0 then return {} end -- Simple clustering: group points that are within minSeparation of each other local clusters = {} local assigned = {} for i, p in ipairs(points) do if not assigned[i] then local cluster = { p } assigned[i] = true -- Find all points close to p (BFS) 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 -- Compute convex hull for each cluster 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 ---@param points Array function UTIL.enlargeConvexHull(points, meters) if points == nil or #points == 0 then return {} end ---@type Array 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 ---Returns hull points visible from origin (not blocked by hull edges) ---@param hull Array ---@param origin Vec2 ---@return Array function UTIL.GetVisibleHullPointsFromOrigin(hull, origin) local function segmentsIntersect(a, b, c, d) -- Helper: returns true if segment ab intersects cd (excluding endpoints) 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 -- Check against all hull edges except those incident to p for j = 1, n do local a = hull[j] local b = hull[(j % n) + 1] -- skip edges incident to p 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 ---Returns the two tangent points ("scratch points") from origin to the convex hull ---@param hull Array ---@param origin Vec2 ---@return Array function UTIL.GetTangentHullPointsFromOrigin(hull, origin) if hull == nil or #hull <= 0 then return {} end local function orientation(a, b, c) -- Returns >0 if c is to the left of ab, <0 if to the right, 0 if colinear 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 -- Find left tangent: the hull point where all other points are to the right of the line from origin to that point 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 tangents are the same (can happen if origin is colinear), only return one if leftTangent.x == rightTangent.x and leftTangent.y == rightTangent.y then return { leftTangent } else return { leftTangent, rightTangent } end end end Spearhead.Util = UTIL ---DCS UTIL Takes inspiration from MIST but only takes the things it needs, changes for DCS updates and different vision for advanced mission scripting stuff. ---It also adds functions that make the other TDCS scripts easier without taking too much "control" away like MOOSE can sometimes. local DCS_UTIL = {} do -- INIT DCS_UTIL do -- local databases --[[ groupdata = { category, country_id, group_template } ]] -- --[[ zone = { name, zone_type, x, y, radius verts, } ]] -- ---@alias SpearheadTriggerZoneType ---| "Cilinder" ---| "Polygon" ---@class SpearheadTriggerZone ---@field name string ---@field location Vec2 ---@field radius number ---@field verts Array ---@field zone_type SpearheadTriggerZoneType ---@field properties Array? ---@type Array DCS_UTIL.__trigger_zones = {} end DCS_UTIL.Coalition = { NEUTRAL = 0, RED = 1, BLUE = 2 } DCS_UTIL.ZoneType = { Cilinder = 0, Polygon = 2 } ---@enum SpearheadGroupCategory DCS_UTIL.GroupCategory = { AIRPLANE = 0, HELICOPTER = 1, GROUND = 2, SHIP = 3, TRAIN = 4, STATIC = 5 --CUSTOM CATEGORY } DCS_UTIL.__airbaseNamesById = {} ---@type table DCS_UTIL.__airbaseZonesByName = {} DCS_UTIL.__airportsStartingCoalition = {} DCS_UTIL.__warehouseStartingCoalition = {} function DCS_UTIL.__INIT() do -- INITS ALL TABLES WITH DATA THAT's from the MIZ environment do --init trigger zones for i, trigger_zone in pairs(env.mission.triggers.zones) do -- reorder verts as they are not ordered correctly in the ME local verts = {} if Spearhead.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 ---@type SpearheadTriggerZone 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 -- init airports and warehouses if env.warehouses.airports then 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 DCS_UTIL.__warehouseStartingCoalition[-1] = "placeholder" for warehouse_id, value in pairs(env.warehouses.warehouses) do if warehouse_id ~= nil then warehouse_id = tostring(warehouse_id) or "nil" local coalitionNumber = DCS_UTIL.stringToCoalition(value.coalition) DCS_UTIL.__warehouseStartingCoalition[warehouse_id] = coalitionNumber end end end end do -- fill airbaseNames and zones 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 ---@type Array 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 } if SpearheadConfig and SpearheadConfig.debugEnabled == true then DCS_UTIL.DrawZone(triggerZone, { r = 0, g = 1, b = 0, a = 1 }, { a = 0, r = 0, g = 1, b = 0 }, 1) end DCS_UTIL.__airbaseZonesByName[name] = triggerZone end end end end end end ---maps the coalition name to the DCS coalition integer ---@param input string the name ---@return integer function DCS_UTIL.stringToCoalition(input) --[[ coalition.side = { NEUTRAL = 0 RED = 1 BLUE = 2 } ]] -- local 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 ---destroy the given unit ---@param unitName string function DCS_UTIL.DestroyUnit(unitName) local unit = Unit.getByName(unitName) if unit and unit:isExist() then unit:destroy() end end --- takes a list of units and returns all the units that are in any of the zones ---@param unit_names table unit names ---@param zone_names table zone names ---@return table unit list of objects { unit = UNIT, zone_name = zoneName} function DCS_UTIL.getUnitsInZones(unit_names, zone_names) local units = {} ---@type Array 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 index, 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 unit_pos = lUnit:getPosition().p local lCat = Object.getCategory(lUnit) for zone_name, zone in pairs(zones) do if unit_pos and ((lCat == 1 and lUnit:isActive() == true) or lCat ~= 1) then -- it is a unit and is active or it is not a unit 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 --- takes a list of groups and returns all the group leaders that are in any of the zones ---@param group_names table unit names ---@param zone_name string zone names ---@return table groupnames list of group names 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 --- takes a x, y poistion and checks if it is inside any of the zones ---@param group_names Array North South position ---@param zone SpearheadTriggerZone ---@return Array groupnames list of groups that are in the zone function DCS_UTIL.areGroupsInCustomZone(group_names, zone) local units = {} if Spearhead.Util.tableLength(group_names) < 1 then return {} end for k = 1, #group_names do local entry = nil 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] = entry 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 --- takes a x, y poistion and checks if it is inside any of the zones ---@param x number North South position ---@param z number West East position ---@param zone_names table zone names ---@return table zones list of objects { zone_name = zoneName} function DCS_UTIL.isPositionInZones(x, z, zone_names) ---@type Array local zones = {} for index, 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_name, 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 --- takes a x, y poistion and checks if it is inside any of the zones ---@param x number North South position ---@param z number West East position ---@param zone_name string zone name ---@return boolean result 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 --- takes a x, y poistion and checks if it is inside any of the zones ---@param zone_name string ---@param parent_zone_name string ---@return boolean result function DCS_UTIL.isZoneInZone(zone_name, parent_zone_name) local zoneA = DCS_UTIL.__trigger_zones[zone_name] --[[@as SpearheadTriggerZone]] if zoneA == nil then return false end local zoneB = DCS_UTIL.__trigger_zones[parent_zone_name] --[[@as SpearheadTriggerZone]] if zoneB == nil then return false end return UTIL.is3dPointInZone({ x = zoneA.location.x, y = 0, z = zoneA.location.y }, zoneB) end ---comment ---@param zone_name any ---@return SpearheadTriggerZone? zone { name,b zone_type, x, z, radius, verts } function DCS_UTIL.getZoneByName(zone_name) if zone_name == nil then return nil end return DCS_UTIL.__trigger_zones[zone_name] end ---comment ---@param airbaseName string ---@return SpearheadTriggerZone? zone function DCS_UTIL.getAirbaseZoneByName(airbaseName) if airbaseName == nil then return nil end return DCS_UTIL.__airbaseZonesByName[airbaseName] end ---maps the category name to the DCS group category ---@param input string the name ---@return integer? 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 ---@type table local config = { ["ah-64d_blk_ii"] = "MGRS", ["fa-18c_hornet"] = "DMS", ["av8bna"] = "DMS", ["f-14b"] = "DMS", ["f-14a-135-gr"] = "DMS" } ---@param location Vec2 ---@param unitType string function DCS_UTIL.convertVec2ToUnitUsableType(location, unitType) local height = land.getHeight(location) local vec3 = { x = location.x, y = height, z = location.y } local unitType = string.lower(unitType or "") local conversionType = config[unitType] if not conversionType then conversionType = "DDM" end return DCS_UTIL.convertToDisplayCoord(vec3, conversionType) end ---@alias CoordType ---| "MGRS" MGRS ---| "DMS" DECIMAL SECONDS ---| "DDM" DECIMAL MINUTES ---@private ---@param location Vec3 ---@param coordType CoordType 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 -- Convert DD to DDM (Degrees Decimal Minutes) 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 ---@param zone SpearheadTriggerZone ---@return Array sceneryObjects function DCS_UTIL.getSceneryObjectsInZone(zone) ---@type Volume local volume if(zone.zone_type == "Cilinder") then local y = land.getHeight({ x = zone.location.x, y = zone.location.y }) ---@type Sphere 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 ---@type Vec3 local min = { x = minX, y = land.getHeight({ x = minX, y = minZ }) - 100, z = minZ } ---@type Vec3 local max = { x = maxX, y = land.getHeight({ x = maxX, y = maxZ }) + 500, z = maxZ } ---@type Box local box = { id = world.VolumeType.BOX, params = { min = min, max = max } } volume = box end ---@type Array local sceneryObjects = {} ---@param object SceneryObject local onFound = function(object) if object and object:isExist() and object:hasAttribute("Buildings") then local obj = Spearhead.classes.stageClasses.Groups.SpearheadSceneryObject.New(object["id_"]) table.insert(sceneryObjects, obj) end end world.searchObjects(Object.Category.SCENERY, volume, onFound) return sceneryObjects end ---@param group Group function DCS_UTIL.getUnitTypeFromGroup(group) for _, unit in pairs(group:getUnits()) do if unit and unit:isExist() then return unit:getTypeName() end end end ---comment Get all units that are players ---@return Array units function DCS_UTIL.getAllPlayerUnits() local units = {} for i = 0, 2 do local players = coalition.getPlayers(i) for key, unit in pairs(players) do units[#units + 1] = unit end end return units end ---get base name from ID ---@param baseId number ---@return string? name function DCS_UTIL.getAirbaseName(baseId) local stringified = tostring(baseId) return DCS_UTIL.__airbaseNamesById[stringified] end ---get base from id ---@param baseId number ---@return Airbase? table function DCS_UTIL.getAirbaseById(baseId) local name = DCS_UTIL.getAirbaseName(baseId) if name == nil then return nil end return Airbase.getByName(name) end ---Get the starting coalition of a farp or airbase ---@param airbase Airbase ---@return number? coalition function DCS_UTIL.getStartingCoalition(airbase) if airbase == nil then return nil end --STRING based dictionary otherwise it'll be a string/collapsed array 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) local unitName = "dead_" .. unitName local object = StaticObject.getByName(unitName) if object then object:destroy() end end ---@class DrawColor ---@field r number ---@field g number ---@field b number ---@field a number local drawID = 400 ---@param zone SpearheadTriggerZone ---@param lineColor DrawColor ---@param fillColor DrawColor ---@param lineStyle LineType ---@return number drawID function DCS_UTIL.DrawZone(zone, lineColor, fillColor, lineStyle) if lineStyle == nil then lineStyle = 4 end drawID = drawID + 1 if zone.zone_type == "Cilinder" then trigger.action.circleToAll(-1, drawID, { x = zone.location.x, y = 0, z = zone.location.y }, zone.radius, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, lineStyle, true) else local functionString = "trigger.action.markupToAll(7, -1, " .. drawID .. "," for _, vecpoint in pairs(zone.verts) do functionString = functionString .. " { x=" .. vecpoint.x .. ", y=0,z=" .. vecpoint.y .. "}," end functionString = functionString .. "{0,1,0,1}, {0,1,0,1}, " .. lineStyle .. ")" ---@diagnostic disable-next-line: deprecated local f, err = loadstring(functionString) if f then f() else env.error("Something failed when drawing complex drawing" .. err) end end local fillColorMapped = { fillColor.r or 0, fillColor.g or 0, fillColor.b or 0, fillColor.a or 0.5 } local lineColorMapped = { lineColor.r or 0, lineColor.g or 0, lineColor.b or 0, lineColor.a or 1 } trigger.action.setMarkupColorFill(drawID, fillColorMapped) trigger.action.setMarkupColor(drawID, lineColorMapped) return drawID end ---@param start Vec3 ---@param finish Vec3 ---@param lineColor DrawColor ---@param lineStyle LineType function DCS_UTIL.DrawLine(start, finish, lineColor, lineStyle) if lineStyle == nil then lineStyle = 4 end drawID = drawID + 1 local lineColorMapped = { lineColor.r or 0, lineColor.g or 0, lineColor.b or 0, lineColor.a or 1 } trigger.action.lineToAll(-1, drawID, start, finish, lineColorMapped, lineStyle) return drawID end ---@param groupID number ---@param text string ---@param location Vec3 ---@return number markID function DCS_UTIL.AddMarkToGroup(groupID, text, location) drawID = drawID + 1 trigger.action.markToGroup(drawID, text, location, groupID, true, nil) return drawID end ---comment ---@param text any ---@param location Vec3 ---@return integer function DCS_UTIL.AddMarkToAll(text, location) drawID = drawID + 1 trigger.action.markToAll(drawID, text, location, true, nil) return drawID end ---@param markId number function DCS_UTIL.RemoveMark(markId) if markId ~= nil then trigger.action.removeMark(markId) end end ---comment ---@param drawID number ---@param lineColor DrawColor 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 ---comment ---@param drawID number ---@param fillColor DrawColor function DCS_UTIL.SetFillColor(drawID, fillColor) 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 ---@return number? id function DCS_UTIL.GetNeutralCountry() for name, 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 ---@return boolean 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 ---comment ---@param groupId number ---@return Group? function DCS_UTIL.GetPlayerGroupByGroupID(groupId) for i = 0, 2 do local players = coalition.getPlayers(i) for key, 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 ---@param unitID number ---@return Unit? function DCS_UTIL.GetPLayerUnitByID(unitID) for i = 0, 2 do local players = coalition.getPlayers(i) for key, unit in pairs(players) do if unit and unit:getID() == unitID then return unit end end end end DCS_UTIL.__INIT(); end Spearhead.DcsUtil = DCS_UTIL --- @class Logger --- @field LoggerName string the name of the logger --- @field LogLevel string the log level of the logger local LOGGER = {} do local PreFix = "Spearhead" ---comment ---@param logger_name any ---@param logLevel LogLevel ---@return Logger 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 "INFO" return self end ---@param message any the message 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 ---comment ---@param message string 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 ---@param message any -- the message 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 ---@param message any the message 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 Spearhead.LoggerTemplate = LOGGER Spearhead.MissionEditingWarnings = {} function Spearhead.AddMissionEditorWarning(warningMessage) table.insert(Spearhead.MissionEditingWarnings, warningMessage or "skip") end local loadDone = false Spearhead.LoadingDone = function() if loadDone == true then return end local warningLogger = Spearhead.LoggerTemplate.new("MISSIONPARSER", "INFO") if Spearhead.Util.tableLength(Spearhead.MissionEditingWarnings) > 0 then for key, message in pairs(Spearhead.MissionEditingWarnings) do warningLogger:warn(message) end else warningLogger:info("No issues detected") end loadDone = true end