diff --git a/README.md b/README.md
index ee94ccb..482b122 100644
--- a/README.md
+++ b/README.md
@@ -1,46 +1,53 @@
-# Spearhead
-
-
-## Contributing.
-
-I'm always happy to see contributions!
-Especially if you've found an issue, annoyance or feature that you know how to solve, fix or create!
-
-### Keep it small.
-
-Keeping your contributions small and to the point helps both me and yourself.
-It'll help the PR review and the speed of improvements.
-If you have a big feature in mind? Go for it!
-Does it require an entire refactor of parts or all of the program? Maybe make sure to do step 0 first.
-
-### Steps (for first timers)
-0. Reach out (Optional)
-
-If you want to let me know you want to do something to contribute, please do!
-Maybe I'm already working on the thing you wanted to build, or someone else is.
-Creating an issue is even better!
-
-
-1. Fork the repository
-
-By forking you can create your own working set of code.
-Whether you keep that fork public or private is up to you!
-
-Keep in mind, forking is alright, but it's best with the intent to contribute back.
-After all, instead of having 5 slightly different versions and products, it might be nicer to have 1 much better version.
-
-2. Create a Draft PR as soon as possible!
-
-As soon as you've found some times to create an initial version, please create a draft PR.
-That way you can let me and everyone know it's being worked on and people can see what conflicts might arise with their own changes.
-
-3. Test, test, test
-
-With DCS and Lua there's a bunch of edge cases that are really hard to get to.
-However, you kinda need to make sure most if not all cases are caught and tested.
-Since automated testing for now doesn't seem feasible, please make sure to verify and test functionality after touching stuff.
-
-4. Finalise and publish the PR.
-
-Finalise the PR and let the maintainers know!
-We can all have a look and discuss the changes.
+# Spearhead
+
+
+## Contributing.
+
+I'm always happy to see contributions!
+Especially if you've found an issue, annoyance or feature that you know how to solve, fix or create!
+
+### Keep it small.
+
+Keeping your contributions small and to the point helps both me and yourself.
+It'll help the PR review and the speed of improvements.
+If you have a big feature in mind? Go for it!
+Does it require an entire refactor of parts or all of the program? Maybe make sure to do step 0 first.
+
+### Steps (for first timers)
+0. Reach out (Optional)
+
+If you want to let me know you want to do something to contribute, please do!
+Maybe I'm already working on the thing you wanted to build, or someone else is.
+Creating an issue is even better!
+
+
+1. Fork the repository
+
+By forking you can create your own working set of code.
+Whether you keep that fork public or private is up to you!
+
+Keep in mind, forking is alright, but it's best with the intent to contribute back.
+After all, instead of having 5 slightly different versions and products, it might be nicer to have 1 much better version.
+
+2. Create a Draft PR as soon as possible!
+
+As soon as you've found some times to create an initial version, please create a draft PR.
+That way you can let me and everyone know it's being worked on and people can see what conflicts might arise with their own changes.
+
+3. Test, test, test
+
+With DCS and Lua there's a bunch of edge cases that are really hard to get to.
+However, you kinda need to make sure most if not all cases are caught and tested.
+Since automated testing for now doesn't seem feasible, please make sure to verify and test functionality after touching stuff.
+
+4. Finalise and publish the PR.
+
+Finalise the PR and let the maintainers know!
+We can all have a look and discuss the changes.
+
+
+
+
+# TODO:
+
+- Would it be possible to generate optional markers on mission briefing locations for Tomcat / Phantom navigation? Then Jester can type it in for the pilots automatically.
diff --git a/classes/configuration/GlobalConfig.lua b/classes/configuration/GlobalConfig.lua
new file mode 100644
index 0000000..011efe2
--- /dev/null
+++ b/classes/configuration/GlobalConfig.lua
@@ -0,0 +1,34 @@
+
+
+
+local briefingMessageTime = nil
+
+---@class GlobalConfig
+---@field private _briefingTime number ;
+local GlobalConfig = {}
+GlobalConfig.__index = GlobalConfig;
+
+---@return GlobalConfig
+function GlobalConfig.New()
+
+ local self = setmetatable({}, GlobalConfig)
+
+ self._briefingTime = 30
+
+ if SpearheadConfig then
+ if SpearheadConfig.briefingMessageDuration then
+ self._briefingTime = SpearheadConfig.briefingMessageDuration
+ end
+ end
+
+ return self
+end
+
+function GlobalConfig:getBriefingTime()
+ return self._briefingTime or 30
+end
+
+
+if Spearhead == nil then Spearhead = {} end
+Spearhead.GlobalConfig = GlobalConfig.New()
+
diff --git a/classes/spearhead_base.lua b/classes/spearhead_base.lua
index 8617ac2..3cb8336 100644
--- a/classes/spearhead_base.lua
+++ b/classes/spearhead_base.lua
@@ -1,1359 +1,1359 @@
---- 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 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
+--- 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 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
diff --git a/classes/stageClasses/helpers/MissionCommandsHelper.lua b/classes/stageClasses/helpers/MissionCommandsHelper.lua
index 40fc50e..5c78a58 100644
--- a/classes/stageClasses/helpers/MissionCommandsHelper.lua
+++ b/classes/stageClasses/helpers/MissionCommandsHelper.lua
@@ -1,485 +1,568 @@
-
----@class MissionCommandsHelper
----@field missionsByCode table @table of missions by their code
----@field enabledByCode table @table of enabled missions by their code
----@field updateNeeded boolean @flag to indicate if an update is needed
----@field lastUpdate number @last update time
----@field updateContinuous fun(self: MissionCommandsHelper, time: number): number @function to update commands continuously
----@field pinnedByGroup table @table of pinned missions by group ID
----@field private _stageBriefings table @table of stage briefings by stage name
----@field private _supplyHubGroups table @table of supply hub groups by their ID
----@field private _logger Logger @logger instance for logging
----@field private _supplyUnitsTracker SupplyUnitsTracker @supply units tracker instance
-local MissionCommandsHelper = {}
-MissionCommandsHelper.__index = MissionCommandsHelper
-
-local id = 0
-
-local instance = nil
-
----@return MissionCommandsHelper
----@param logLevel string @log level for the logger
-function MissionCommandsHelper.getOrCreate(logLevel)
- if instance == nil then
- instance = setmetatable({}, MissionCommandsHelper)
-
- instance._logger = Spearhead.LoggerTemplate.new("MissionCommandsHelper", logLevel)
-
- instance._logger:info("Creating MissionCommandsHelper instance")
-
- instance.missionsByCode = {}
- instance.enabledByCode = {}
- instance.updateNeeded = false
- instance.pinnedByGroup = {}
- instance.lastUpdate = 0
- instance._supplyHubGroups = {}
- instance._stageBriefings = {}
-
- instance._supplyUnitsTracker = Spearhead.classes.stageClasses.helpers.SupplyUnitsTracker.getOrCreate(logLevel)
-
- ---comment
- ---@param selfA MissionCommandsHelper
- ---@param time number
- ---@return number
- instance.updateContinuous = function(selfA, time)
- if selfA.updateNeeded == false then
- return time + 10
- end
-
- for _, unit in pairs(Spearhead.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)
- Spearhead.Events.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
-
----@param mission Mission
-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
-
----Removes a mission from the F10 commands menu
----@param mission Mission
-function MissionCommandsHelper:RemoveMissionToCommands(mission)
- self.enabledByCode[tostring(mission.code)] = false
- self.updateNeeded = true
-end
-
----@param groupID number
-function MissionCommandsHelper:MarkUnitInSupplyHub(groupID)
- self._logger:debug("Marking unit in supply hub: " .. tostring(groupID))
- local updateNeeded = false
- if self._supplyHubGroups[tostring(groupID)] ~= true then
- updateNeeded = true
- end
-
- self._supplyHubGroups[tostring(groupID)] = true
- if updateNeeded == true then self:updateCommandsForGroup(groupID) end
-end
-
-
----@param groupID number
-function MissionCommandsHelper:MarkUnitOutsideSupplyHub(groupID)
- self._logger:debug("Marking unit outide supply hub: " .. tostring(groupID))
- local updateNeeded = false
- if self._supplyHubGroups[tostring(groupID)] == true then
- updateNeeded = true
- end
-
- self._supplyHubGroups[tostring(groupID)] = false
- if updateNeeded == true then self:updateCommandsForGroup(groupID) end
-end
-
-
-
----@param unit Unit
-function MissionCommandsHelper:OnPlayerEntersUnit(unit)
- if unit then
- local group = unit:getGroup()
- if group then self:updateCommandsForGroup(group:getID()) end
- end
-end
-
----@class MissionBriefingRequestedArgs
----@field mission Mission @the mission object
----@field groupId integer @the group ID of the player requesting the briefing
-
----comment
----@param args MissionBriefingRequestedArgs
-local missionBriefingRequested = function(args)
- ---@type Mission
- local mission = args.mission
- local groupID = args.groupId
-
- mission:ShowBriefing(groupID)
-end
-
----@class PinMissionCommandArgs
----@field self MissionCommandsHelper @the MissionCommandsHelper instance
----@field groupId integer @the group ID of the player requesting the briefing
----@field mission Mission @the mission object
-
-local pinMissionCommand = function(args)
- ---@type MissionCommandsHelper
- local self = args.self
- local groupID = args.groupId
- local mission = args.mission
-
- if mission then
- self:PinMission(mission, groupID)
- end
-end
-
----@private
-function MissionCommandsHelper:AddOverviewCommand(groupID)
-
- local MissionsOverviewToGroup = function (id)
-
- local text = "Missions Overview\n\n"
-
- local group = Spearhead.DcsUtil.GetPlayerGroupByGroupID(id)
-
- ---comment
- ---@param mission Mission
- ---@return string
- 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 = Spearhead.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
-
- ---Primary missions
- text = text .. "Primary Missions\n"
- 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.priority == "primary" then
- text = text .. formatLine(mission)
- end
- end
- end
-
- ---Secondary missions
- text = text .. "\nSecondary Missions\n"
- 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.priority == "secondary" then
- text = text .. formatLine(mission)
- end
- end
- end
-
- trigger.action.outTextForGroup(id, text, 20, true)
- end
-
- missionCommands.removeItemForGroup(groupID, { "Overview" } )
- missionCommands.addCommandForGroup(groupID, "Overview", nil, MissionsOverviewToGroup, groupID)
-end
-
----@private
----@param groupID number
-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
-
----@param groupID number
-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)
-
- ---@param id number
- 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)
-
-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)
-end
-
-function MissionCommandsHelper:AddAllMissionCommandsToGroup(groupID)
-
- local perFolder = 9
-
- do --- primary missions
- local count = 0
- local path = { [1] = folderNames.primary }
- for code, enabled in pairs(self.enabledByCode) do
- if enabled == true then
- local mission = self.missionsByCode[code]
- if mission and mission.priority == "primary" then
- count = count + 1
- if count <= perFolder then
- local copied = Spearhead.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
- end
-
- do --- secondary missions
- local count = 0
- local path = { [1] = folderNames.secondary }
- for code, enabled in pairs(self.enabledByCode) do
- if enabled == true then
- local mission = self.missionsByCode[code]
- if mission and mission.priority == "secondary" then
- count = count + 1
- if count <= perFolder then
- local copied = Spearhead.Util.deepCopyTable(path)
- self:addMissionCommands(groupID, copied, mission)
- else
- local name = "Next Menu ..."
- missionCommands.addSubMenuForGroup(groupID, name, path)
- path[#path+1] = "more missions ..."
- end
- end
- end
- end
- end
-end
-
----comment
----@private
----@param groupId integer
----@param path Array
----@param mission Mission
-function MissionCommandsHelper:addMissionCommands(groupId, path, mission)
-
- if path then
- local missionFolderName = "[" .. mission.code .. "]" .. mission.name
- missionCommands.addSubMenuForGroup(groupId, missionFolderName, path)
- table.insert(path, missionFolderName)
-
- ---@type MissionBriefingRequestedArgs
- local missionBriefingRequestedArgs = { groupId = groupId, mission = mission }
- missionCommands.addCommandForGroup(groupId, "Briefing", path, missionBriefingRequested,missionBriefingRequestedArgs)
-
- ---@type PinMissionCommandArgs
- local pinMissionCommandArgs = { self = self, groupId = groupId, mission = mission }
- missionCommands.addCommandForGroup(groupId, "Pin", path, pinMissionCommand, pinMissionCommandArgs)
- end
-end
-
----@private
----@param groupID integer
-function MissionCommandsHelper:AddSupplyHubCommandsIfApplicable(groupID)
-
- if self._supplyHubGroups[tostring(groupID)] ~= true then return end
-
- self._logger:debug("Adding supply hub commands for group: " .. tostring(groupID))
-
- local group = Spearhead.DcsUtil.GetPlayerGroupByGroupID(groupID)
- if group == nil then return end
-
- local unit = group:getUnit(1)
- if unit == nil then return end
-
- ---@class LoadCargoCommandParams
- ---@field unitID number
- ---@field groupID number
- ---@field crateType CrateType
- ---@field supplyUnitsTracker SupplyUnitsTracker
-
- ---comment
- ---@param params LoadCargoCommandParams
- local loadCargoCommand = function(params)
- local crateType = params.crateType
- local supplyUnitsTracker = params.supplyUnitsTracker
- if supplyUnitsTracker then
- supplyUnitsTracker:UnitRequestCrateLoading(params.groupID, crateType)
- end
- end
-
- local path = { [1] = folderNames.supplyHub }
-
-
- ---@type LoadCargoCommandParams
- local farpParams1000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "FARP_CRATE_1000", supplyUnitsTracker = self._supplyUnitsTracker }
- missionCommands.addCommandForGroup(groupID, "Load FARP Crate (1000)", path, loadCargoCommand, farpParams1000)
-
- ---@type LoadCargoCommandParams
- local farpParams2000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "FARP_CRATE_2000", supplyUnitsTracker = self._supplyUnitsTracker }
- missionCommands.addCommandForGroup(groupID, "Load FARP Crate (2000)", path, loadCargoCommand, farpParams2000)
-
- ---@type LoadCargoCommandParams
- local samParms1000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "SAM_CRATE_2000", supplyUnitsTracker = self._supplyUnitsTracker }
- missionCommands.addCommandForGroup(groupID, "Load SAM Crate (1000)", path, loadCargoCommand, samParms1000)
-
- ---@type LoadCargoCommandParams
- local samParms2000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "SAM_CRATE_2000", supplyUnitsTracker = self._supplyUnitsTracker }
- missionCommands.addCommandForGroup(groupID, "Load SAM Crate (2000)", path, loadCargoCommand, samParms2000)
-
- ---@type LoadCargoCommandParams
- local samParms2000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "AIRBASE_CRATE_2000", supplyUnitsTracker = self._supplyUnitsTracker }
- missionCommands.addCommandForGroup(groupID, "Airbase Crate (2000)", path, loadCargoCommand, samParms2000)
-end
-
-function MissionCommandsHelper:AddCargoCommands(groupID)
-
- local group = Spearhead.DcsUtil.GetPlayerGroupByGroupID(groupID)
- if group == nil then return end
-
- local unit = group:getUnit(1)
- if unit == nil then return end
-
- ---@class UnloadCargoCommandParams
- ---@field unitID number
- ---@field crateType CrateType
- ---@field supplyUnitsTracker SupplyUnitsTracker
-
- ---comment
- ---@param params UnloadCargoCommandParams
- local unloadCargoCommand = function(params)
- local unitID = params.unitID
- local crateType = params.crateType
- params.supplyUnitsTracker:UnloadRequested(unitID, crateType)
- end
-
- local cargo = self._supplyUnitsTracker:GetCargoInUnit(unit:getID())
- if cargo then
- for cargoType, amount in pairs(cargo) do
- local cargoConfig = Spearhead.classes.stageClasses.helpers.supplies.SupplyConfigHelper.getSupplyConfig(cargoType)
- if cargoConfig then
- for i = 1, amount do
- local path = { [1] = folderNames.cargo }
- ---@type UnloadCargoCommandParams
- local params = { unitID = unit:getID(), crateType = cargoType, supplyUnitsTracker = self._supplyUnitsTracker }
- missionCommands.addCommandForGroup(groupID, "Unload " .. cargoConfig.displayName, path, unloadCargoCommand, params)
- end
- end
- end
- end
-end
-
-
-
----@private
----@param groupId integer
-function MissionCommandsHelper:addMissionFolders(groupId)
-
- missionCommands.addSubMenuForGroup(groupId, folderNames.primary)
- missionCommands.addSubMenuForGroup(groupId, folderNames.secondary)
-
- if self._supplyHubGroups[tostring(groupId)] == true then
- self._logger:debug("Adding supply hub commands folder for group: " .. tostring(groupId))
- missionCommands.addSubMenuForGroup(groupId, folderNames.supplyHub)
- end
-
- local group = Spearhead.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
-
----@private
----@param groupId integer
-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
-
----@private
-function MissionCommandsHelper:ResetFolders(groupID)
- -- Cleanup mission folder
- self:removeMissionFolders(groupID)
-
- -- Add mission folders
- self:addMissionFolders(groupID)
-end
-
-if Spearhead == nil then Spearhead = {} end
-if Spearhead.classes == nil then Spearhead.classes = {} end
-if Spearhead.classes.stageClasses == nil then Spearhead.classes.stageClasses = {} end
-if Spearhead.classes.stageClasses.helpers == nil then Spearhead.classes.stageClasses.helpers = {} end
-Spearhead.classes.stageClasses.helpers.MissionCommandsHelper = MissionCommandsHelper
+
+---@class MissionCommandsHelper
+---@field missionsByCode table @table of missions by their code
+---@field enabledByCode table @table of enabled missions by their code
+---@field updateNeeded boolean @flag to indicate if an update is needed
+---@field lastUpdate number @last update time
+---@field updateContinuous fun(self: MissionCommandsHelper, time: number): number @function to update commands continuously
+---@field pinnedByGroup table @table of pinned missions by group ID
+---@field private _stageBriefings table @table of stage briefings by stage name
+---@field private _supplyHubGroups table @table of supply hub groups by their ID
+---@field private _logger Logger @logger instance for logging
+---@field private _supplyUnitsTracker SupplyUnitsTracker @supply units tracker instance
+local MissionCommandsHelper = {}
+MissionCommandsHelper.__index = MissionCommandsHelper
+
+---@param list Array
+---@param groupPos Vec2
+local function sortMissions(list, groupPos)
+ table.sort(list, function(a, b)
+ local distA = Spearhead.Util.VectorDistance2d(groupPos, a.location or {x=0, y=0})
+ local distB = Spearhead.Util.VectorDistance2d(groupPos, b.location or {x=0, y=0})
+ return distA < distB;
+ end)
+end
+
+local id = 0
+
+local instance = nil
+
+---@return MissionCommandsHelper
+---@param logLevel string @log level for the logger
+function MissionCommandsHelper.getOrCreate(logLevel)
+ if instance == nil then
+ instance = setmetatable({}, MissionCommandsHelper)
+
+ instance._logger = Spearhead.LoggerTemplate.new("MissionCommandsHelper", logLevel)
+
+ instance._logger:info("Creating MissionCommandsHelper instance")
+
+ instance.missionsByCode = {}
+ instance.enabledByCode = {}
+ instance.updateNeeded = false
+ instance.pinnedByGroup = {}
+ instance.lastUpdate = 0
+ instance._supplyHubGroups = {}
+ instance._stageBriefings = {}
+
+ instance._supplyUnitsTracker = Spearhead.classes.stageClasses.helpers.SupplyUnitsTracker.getOrCreate(logLevel)
+
+ ---comment
+ ---@param selfA MissionCommandsHelper
+ ---@param time number
+ ---@return number
+ instance.updateContinuous = function(selfA, time)
+ if selfA.updateNeeded == false then
+ return time + 10
+ end
+
+ for _, unit in pairs(Spearhead.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)
+ Spearhead.Events.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
+
+---@param mission Mission
+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
+
+---Removes a mission from the F10 commands menu
+---@param mission Mission
+function MissionCommandsHelper:RemoveMissionToCommands(mission)
+ self.enabledByCode[tostring(mission.code)] = false
+ self.updateNeeded = true
+end
+
+---@param groupID number
+function MissionCommandsHelper:MarkUnitInSupplyHub(groupID)
+ self._logger:debug("Marking unit in supply hub: " .. tostring(groupID))
+ local updateNeeded = false
+ if self._supplyHubGroups[tostring(groupID)] ~= true then
+ updateNeeded = true
+ end
+
+ self._supplyHubGroups[tostring(groupID)] = true
+ if updateNeeded == true then self:updateCommandsForGroup(groupID) end
+end
+
+
+---@param groupID number
+function MissionCommandsHelper:MarkUnitOutsideSupplyHub(groupID)
+ self._logger:debug("Marking unit outide supply hub: " .. tostring(groupID))
+ local updateNeeded = false
+ if self._supplyHubGroups[tostring(groupID)] == true then
+ updateNeeded = true
+ end
+
+ self._supplyHubGroups[tostring(groupID)] = false
+ if updateNeeded == true then self:updateCommandsForGroup(groupID) end
+end
+
+
+
+---@param unit Unit
+function MissionCommandsHelper:OnPlayerEntersUnit(unit)
+ if unit then
+ local group = unit:getGroup()
+ if group then self:updateCommandsForGroup(group:getID()) end
+ end
+end
+
+---@class MissionBriefingRequestedArgs
+---@field mission Mission @the mission object
+---@field groupId integer @the group ID of the player requesting the briefing
+
+---comment
+---@param args MissionBriefingRequestedArgs
+local missionBriefingRequested = function(args)
+ ---@type Mission
+ local mission = args.mission
+ local groupID = args.groupId
+
+ mission:ShowBriefing(groupID)
+end
+
+---@class PinMissionCommandArgs
+---@field self MissionCommandsHelper @the MissionCommandsHelper instance
+---@field groupId integer @the group ID of the player requesting the briefing
+---@field mission Mission @the mission object
+
+---@param args PinMissionCommandArgs
+local pinMissionCommand = function(args)
+ ---@type MissionCommandsHelper
+ local self = args.self
+ local groupID = args.groupId
+ local mission = args.mission
+
+ if mission then
+ self:PinMission(mission, groupID)
+ end
+end
+
+---@private
+function MissionCommandsHelper:AddOverviewCommand(groupID)
+
+ local MissionsOverviewToGroup = function (id)
+
+ local text = "Missions Overview\n\n"
+
+ local group = Spearhead.DcsUtil.GetPlayerGroupByGroupID(id)
+ ---@type Vec2
+ local groupPos = { x=0, y=0 }
+ if group then
+ local pos = group:getUnit(1):getPosition().p
+ groupPos = { x= pos.x, y=pos.z }
+ end
+
+ ---comment
+ ---@param mission Mission
+ ---@return string
+ 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 = Spearhead.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
+
+
+
+
+ ---Primary missions
+ text = text .. "Primary Missions\n"
+
+ ---@type Array
+ 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.priority == "primary" then
+ table.insert(primaryMissions, mission)
+ end
+ end
+ end
+
+ sortMissions(primaryMissions, groupPos)
+
+ for _, mission in pairs(primaryMissions) do
+ text = text .. formatLine(mission)
+ end
+
+ ---Secondary missions
+ text = text .. "\nSecondary Missions\n"
+
+ ---@type Array
+ 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.priority == "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(id, text, 20, true)
+ end
+
+ missionCommands.removeItemForGroup(groupID, { "Overview" } )
+ missionCommands.addCommandForGroup(groupID, "Overview", nil, MissionsOverviewToGroup, groupID)
+end
+
+---@private
+---@param groupID number
+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
+
+---@param groupID number
+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)
+
+ ---@param id number
+ 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"
+}
+
+
+---@param mission Mission
+---@param groupID integer
+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 = Spearhead.DcsUtil.GetPlayerGroupByGroupID(groupID)
+ ---@type Vec2
+ 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 --- primary missions
+ local count = 0
+ local path = { [1] = folderNames.primary }
+
+ ---@type Array
+ local primaryMissions = {}
+
+ for code, enabled in pairs(self.enabledByCode) do
+ if enabled == true then
+ local mission = self.missionsByCode[code]
+ if mission and mission.priority == "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 = Spearhead.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 --- secondary missions
+ 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.priority == "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 = Spearhead.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
+
+---comment
+---@private
+---@param groupId integer
+---@param path Array
+---@param mission Mission
+function MissionCommandsHelper:addMissionCommands(groupId, path, mission)
+
+ if path then
+
+ local group = Spearhead.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 = Spearhead.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)
+
+ ---@type MissionBriefingRequestedArgs
+ local missionBriefingRequestedArgs = { groupId = groupId, mission = mission }
+ missionCommands.addCommandForGroup(groupId, "Briefing", path, missionBriefingRequested,missionBriefingRequestedArgs)
+
+ ---@type PinMissionCommandArgs
+ local pinMissionCommandArgs = { self = self, groupId = groupId, mission = mission }
+ missionCommands.addCommandForGroup(groupId, "Pin", path, pinMissionCommand, pinMissionCommandArgs)
+ end
+end
+
+---@private
+---@param groupID integer
+function MissionCommandsHelper:AddSupplyHubCommandsIfApplicable(groupID)
+
+ if self._supplyHubGroups[tostring(groupID)] ~= true then return end
+
+ self._logger:debug("Adding supply hub commands for group: " .. tostring(groupID))
+
+ local group = Spearhead.DcsUtil.GetPlayerGroupByGroupID(groupID)
+ if group == nil then return end
+
+ local unit = group:getUnit(1)
+ if unit == nil then return end
+
+ ---@class LoadCargoCommandParams
+ ---@field unitID number
+ ---@field groupID number
+ ---@field crateType CrateType
+ ---@field supplyUnitsTracker SupplyUnitsTracker
+
+ ---comment
+ ---@param params LoadCargoCommandParams
+ local loadCargoCommand = function(params)
+ local crateType = params.crateType
+ local supplyUnitsTracker = params.supplyUnitsTracker
+ if supplyUnitsTracker then
+ supplyUnitsTracker:UnitRequestCrateLoading(params.groupID, crateType)
+ end
+ end
+
+ local path = { [1] = folderNames.supplyHub }
+
+ ---@type LoadCargoCommandParams
+ local farpParams1000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "FARP_CRATE_1000", supplyUnitsTracker = self._supplyUnitsTracker }
+ missionCommands.addCommandForGroup(groupID, "Load FARP Crate (1000)", path, loadCargoCommand, farpParams1000)
+
+ ---@type LoadCargoCommandParams
+ local farpParams2000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "FARP_CRATE_2000", supplyUnitsTracker = self._supplyUnitsTracker }
+ missionCommands.addCommandForGroup(groupID, "Load FARP Crate (2000)", path, loadCargoCommand, farpParams2000)
+
+ ---@type LoadCargoCommandParams
+ local samParms1000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "SAM_CRATE_2000", supplyUnitsTracker = self._supplyUnitsTracker }
+ missionCommands.addCommandForGroup(groupID, "Load SAM Crate (1000)", path, loadCargoCommand, samParms1000)
+
+ ---@type LoadCargoCommandParams
+ local samParms2000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "SAM_CRATE_2000", supplyUnitsTracker = self._supplyUnitsTracker }
+ missionCommands.addCommandForGroup(groupID, "Load SAM Crate (2000)", path, loadCargoCommand, samParms2000)
+
+ ---@type LoadCargoCommandParams
+ local samParms2000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "AIRBASE_CRATE_2000", supplyUnitsTracker = self._supplyUnitsTracker }
+ missionCommands.addCommandForGroup(groupID, "Airbase Crate (2000)", path, loadCargoCommand, samParms2000)
+end
+
+function MissionCommandsHelper:AddCargoCommands(groupID)
+
+ local group = Spearhead.DcsUtil.GetPlayerGroupByGroupID(groupID)
+ if group == nil then return end
+
+ local unit = group:getUnit(1)
+ if unit == nil then return end
+
+ ---@class UnloadCargoCommandParams
+ ---@field unitID number
+ ---@field crateType CrateType
+ ---@field supplyUnitsTracker SupplyUnitsTracker
+
+ ---comment
+ ---@param params UnloadCargoCommandParams
+ local unloadCargoCommand = function(params)
+ local unitID = params.unitID
+ local crateType = params.crateType
+ params.supplyUnitsTracker:UnloadRequested(unitID, crateType)
+ end
+
+ local cargo = self._supplyUnitsTracker:GetCargoInUnit(unit:getID())
+ if cargo then
+ for cargoType, amount in pairs(cargo) do
+ local cargoConfig = Spearhead.classes.stageClasses.helpers.supplies.SupplyConfigHelper.getSupplyConfig(cargoType)
+ if cargoConfig then
+ for i = 1, amount do
+ local path = { [1] = folderNames.cargo }
+ ---@type UnloadCargoCommandParams
+ local params = { unitID = unit:getID(), crateType = cargoType, supplyUnitsTracker = self._supplyUnitsTracker }
+ missionCommands.addCommandForGroup(groupID, "Unload " .. cargoConfig.displayName, path, unloadCargoCommand, params)
+ end
+ end
+ end
+ end
+end
+
+
+
+---@private
+---@param groupId integer
+function MissionCommandsHelper:addMissionFolders(groupId)
+
+ missionCommands.addSubMenuForGroup(groupId, folderNames.primary)
+ missionCommands.addSubMenuForGroup(groupId, folderNames.secondary)
+
+ if self._supplyHubGroups[tostring(groupId)] == true then
+ self._logger:debug("Adding supply hub commands folder for group: " .. tostring(groupId))
+ missionCommands.addSubMenuForGroup(groupId, folderNames.supplyHub)
+ end
+
+ local group = Spearhead.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
+
+---@private
+---@param groupId integer
+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
+
+---@private
+function MissionCommandsHelper:ResetFolders(groupID)
+ -- Cleanup mission folder
+ self:removeMissionFolders(groupID)
+
+ -- Add mission folders
+ self:addMissionFolders(groupID)
+end
+
+if Spearhead == nil then Spearhead = {} end
+if Spearhead.classes == nil then Spearhead.classes = {} end
+if Spearhead.classes.stageClasses == nil then Spearhead.classes.stageClasses = {} end
+if Spearhead.classes.stageClasses.helpers == nil then Spearhead.classes.stageClasses.helpers = {} end
+Spearhead.classes.stageClasses.helpers.MissionCommandsHelper = MissionCommandsHelper
diff --git a/classes/stageClasses/missions/BuildableMission.lua b/classes/stageClasses/missions/BuildableMission.lua
index de35ce9..745d855 100644
--- a/classes/stageClasses/missions/BuildableMission.lua
+++ b/classes/stageClasses/missions/BuildableMission.lua
@@ -119,7 +119,7 @@ function BuildableMission:ShowBriefing(groupID)
"\n\n" ..
"NOTE: Do not land in the orange construction zone!"
- trigger.action.outTextForGroup(groupID, briefing, 30)
+ trigger.action.outTextForGroup(groupID, briefing, Spearhead.GlobalConfig:getBriefingTime())
end
function BuildableMission:MarkMissionAreaToGroup(groupID)
diff --git a/classes/stageClasses/missions/baseMissions/Mission.lua b/classes/stageClasses/missions/baseMissions/Mission.lua
index 5434ee2..0acaac9 100644
--- a/classes/stageClasses/missions/baseMissions/Mission.lua
+++ b/classes/stageClasses/missions/baseMissions/Mission.lua
@@ -94,7 +94,7 @@ function Mission:ShowBriefing(groupId)
local text = "Mission [" ..
self.code .. "] " .. self.name .. "\n \n" .. briefing .. " \n \n" .. stateString
- trigger.action.outTextForGroup(groupId, text, 30);
+ trigger.action.outTextForGroup(groupId, text, Spearhead.GlobalConfig:getBriefingTime());
end
@@ -143,9 +143,6 @@ if not Spearhead.classes.stageClasses.missions then Spearhead.classes.stageClass
if not Spearhead.classes.stageClasses.missions.baseMissions then Spearhead.classes.stageClasses.missions.baseMissions = {} end
Spearhead.classes.stageClasses.missions.baseMissions.Mission = Mission
-
-
-
do --aliases
--- @alias MissionPriority
diff --git a/config.lua b/config.lua
index 458c890..44014cf 100644
--- a/config.lua
+++ b/config.lua
@@ -4,6 +4,10 @@ SpearheadConfig = {
---DEBUG LOGGING
debugEnabled = false, -- default false
+ --- The time briefings should be displayed by default.
+ --- Players can always "Clear Messages" through the F10 menu, so setting it to a high value can be
+ briefingMessageDuration = 30, --default 30
+
CapConfig = {
--quickly enable of disable the entire CAP Logic
--(you can also just rename all units to not be named "CAP_")