dcsCommon = {} dcsCommon.version = "3.0.7" --[[-- VERSION HISTORY 3.0.0 - removed bad bug in stringStartsWith, only relevant if caseSensitive is false - point2text new intsOnly option - arrangeGroupDataIntoFormation minDist harden - cleanup - new pointInDirectionOfPointXYY() - createGroundGroupWithUnits now supports liveries - new getAllExistingPlayersAndUnits() 3.0.1 - clone: better handling of string type 3.0.2 - new getPlayerUnit() 3.0.3 - createStaticObjectForCoalitionInRandomRing() returns x and z - isTroopCarrier() also supports 'helo' keyword - new createTakeOffFromGroundRoutePointData() 3.0.4 - getGroupLocation() hardened, optional verbose 3.0.5 - new getNthItem() - new getFirstItem() - arrayContainsString() can handle dicts - new pointXpercentYdegOffAB() 3.0.6 - new arrayContainsStringCaseInsensitive() 3.0.7 - fixed small bug in wildArrayContainsString --]]-- -- dcsCommon is a library of common lua functions -- for easy access and simple mission programming -- (c) 2021 - 2024 by Christian Franz and cf/x AG dcsCommon.verbose = false -- set to true to see debug messages. Lots of them dcsCommon.uuidStr = "uuid-" dcsCommon.simpleUUID = 76543 -- a number to start. as good as any -- globals dcsCommon.cbID = 0 -- callback id for simple callback scheduling dcsCommon.troopCarriers = {"Mi-8MT", "UH-1H", "Mi-24P"} -- Ka-50, Apache and Gazelle can't carry troops dcsCommon.coalitionSides = {0, 1, 2} dcsCommon.maxCountry = 86 -- number of countries defined in total -- lookup tables dcsCommon.groupID2Name = {} dcsCommon.unitID2Name = {} dcsCommon.unitID2X = {} dcsCommon.unitID2Y = {} -- verify that a module is loaded. obviously not required -- for dcsCommon, but all higher-order modules function dcsCommon.libCheck(testingFor, requiredLibs) local canRun = true for idx, libName in pairs(requiredLibs) do if not _G[libName] then trigger.action.outText("*** " .. testingFor .. " requires " .. libName, 30) canRun = false end end return canRun end -- read all groups and units from miz and build a reference table function dcsCommon.collectMissionIDs() -- create cross reference tables to be able to get a group or -- unit's name by ID for coa_name_miz, coa_data in pairs(env.mission.coalition) do -- iterate all coalitions local coa_name = coa_name_miz if string.lower(coa_name_miz) == 'neutrals' then -- remove 's' at neutralS coa_name = 'neutral' end -- directly convert coalition into number for easier access later local coaNum = 0 if coa_name == "red" then coaNum = 1 end if coa_name == "blue" then coaNum = 2 end if type(coa_data) == 'table' then -- coalition = {bullseye, nav_points, name, county}, -- with county being an array if coa_data.country then -- make sure there a country table for this coalition for cntry_id, cntry_data in pairs(coa_data.country) do -- iterate all countries for this -- per country = {id, name, vehicle, helicopter, plane, ship, static} local countryName = string.lower(cntry_data.name) local countryID = cntry_data.id if type(cntry_data) == 'table' then -- filter strings .id and .name for obj_type_name, obj_type_data in pairs(cntry_data) do -- only look at helos, ships, planes and vehicles if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" -- what about "cargo"? then -- (so it's not id or name) local category = obj_type_name if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's at least one group! for group_num, group_data in pairs(obj_type_data.group) do local aName = group_data.name local aID = group_data.groupId -- store this reference dcsCommon.groupID2Name[aID] = aName -- now iterate all units in this group -- for player into for unit_num, unit_data in pairs(group_data.units) do if unit_data.name and unit_data.unitId then -- store this reference dcsCommon.unitID2Name[unit_data.unitId] = unit_data.name dcsCommon.unitID2X[unit_data.unitId] = unit_data.x dcsCommon.unitID2Y[unit_data.unitId] = unit_data.y end end -- for all units end -- for all groups end --if has category data end --if plane, helo etc... category end --for all objects in country end --if has country data end --for all countries in coalition end --if coalition has country table end -- if there is coalition data end --for all coalitions in mission end function dcsCommon.getUnitNameByID(theID) -- accessor function for later expansion return dcsCommon.unitID2Name[theID] end function dcsCommon.getGroupNameByID(theID) -- accessor function for later expansion return dcsCommon.groupID2Name[theID] end function dcsCommon.getUnitStartPosByID(theID) local x = dcsCommon.unitID2X[theID] local y = dcsCommon.unitID2Y[theID] return x, y end -- returns only positive values, lo must be >0 and <= hi function dcsCommon.randomBetween(loBound, hiBound) if not loBound then loBound = 1 end if not hiBound then hiBound = 1 end if loBound == hiBound then return loBound end local delayMin = loBound local delayMax = hiBound local delay = delayMax if delayMin ~= delayMax then -- pick random in range , say 3-7 --> 5 s! local delayDiff = (delayMax - delayMin) + 1 -- 7-3 + 1 delay = dcsCommon.smallRandom(delayDiff) - 1 --> 0-4 delay = delay + delayMin if delay > delayMax then delay = delayMax end if delay < 1 then delay = 1 end if dcsCommon.verbose then trigger.action.outText("+++dcsC: delay range " .. delayMin .. "-" .. delayMax .. ": selected " .. delay, 30) end end return delay end -- taken inspiration from mist, as dcs lua has issues with -- random numbers smaller than 50. Given a range of x numbers 1..x, it is -- repeated a number of times until it fills an array of at least -- 50 items (usually some more), and only then one itemis picked from -- that array with a random number that is from a greater range (0..50+) function dcsCommon.smallRandom(theNum) -- adapted from mist, only support ints theNum = math.floor(theNum) if theNum >= 50 then return math.random(theNum) end if theNum < 1 then trigger.action.outText("smallRandom: invoke with argument < 1 (" .. theNum .. "), using 1", 30) theNum = 1 end -- for small randoms (<50) local lowNum, highNum highNum = theNum lowNum = 1 local total = 1 if math.abs(highNum - lowNum + 1) < 50 then -- if total values is less than 50 total = math.modf(50/math.abs(highNum - lowNum + 1)) -- number of times to repeat whole range to get above 50. e.g. 11 would be 5 times 1 .. 11, giving us 55 items total end local choices = {} for i = 1, total do -- iterate required number of times for x = lowNum, highNum do -- iterate between the range choices[#choices +1] = x -- add each entry to a table end end local rtnVal; -- = math.random(#choices) -- will now do a math.random of at least 50 choices for i = 1, 15 do rtnVal = math.random(#choices) -- iterate 15 times for randomization end return choices[rtnVal] -- return indexed end function dcsCommon.getNthItem(theSet, n) local count = 1 for key, value in pairs(theSet) do if count == n then return value end count = count + 1 end return nil end function dcsCommon.getFirstItem(theSet) return dcsCommon.getNthItem(theSet, 1) end function dcsCommon.getSizeOfTable(theTable) local count = 0 for _ in pairs(theTable) do count = count + 1 end return count end function dcsCommon.findAndRemoveFromTable(theTable, theElement) -- assumes array if not theElement then return false end if not theTable then return false end for i=1, #theTable do if theTable[i] == theElement then -- this element found. remove from table table.remove(theTable, i) return true end end end function dcsCommon.pickRandom(theTable) if not theTable then trigger.action.outText("*** warning: nil table in pick random", 30) end if #theTable < 1 then trigger.action.outText("*** warning: zero choice in pick random", 30) --local k = i.ll return nil end if #theTable == 1 then return theTable[1] end r = dcsCommon.smallRandom(#theTable) --r = math.random(#theTable) return theTable[r] end -- enumerateTable - make an array out of a table for indexed access function dcsCommon.enumerateTable(theTable) if not theTable then theTable = {} end local array = {} for key, value in pairs(theTable) do table.insert(array, value) end return array end -- combine table. creates new function dcsCommon.combineTables(inOne, inTwo) local outTable = {} for idx, element in pairs(inOne) do table.insert(outTable, element) end for idx, element in pairs(inTwo) do table.insert(outTable, element) end return outTable end function dcsCommon.addToTableIfNew(theTable, theElement) for idx, anElement in pairs(theTable) do if anElement == theElement then return end end table.insert(theTable, theElement) end -- -- A I R F I E L D S A N D F A R P S -- -- airfield management function dcsCommon.getAirbaseCat(aBase) if not aBase then return nil end local airDesc = aBase:getDesc() if not airDesc then return nil end local airCat = airDesc.category return airCat end -- get free parking slot. optional parkingType can be used to -- filter for a scpecific type, e.g. 104 = open field function dcsCommon.getFirstFreeParkingSlot(aerodrome, parkingType) if not aerodrome then return nil end local freeSlots = aerodrome:getParking(true) for idx, theSlot in pairs(freeSlots) do if not parkingType then -- simply return the first we come across return theSlot end if theSlot.Term_Type == parkingType then return theSlot end end return nil end -- getAirbasesInRangeOfPoint: get airbases that are in range of point function dcsCommon.getAirbasesInRangeOfPoint(center, range, filterCat, filterCoalition) if not center then return {} end if not range then range = 500 end -- 500m default local basesInRange = {} local allAB = dcsCommon.getAirbasesWhoseNameContains("*", filterCat, filterCoalition) for idx, aBase in pairs(allAB) do local delta = dcsCommon.dist(center, aBase:getPoint()) if delta <= range then table.insert(basesInRange, aBase) end end return basesInRange end -- getAirbasesInRangeOfAirbase returns all airbases that -- are in range of the given airbase function dcsCommon.getAirbasesInRangeOfAirbase(airbase, includeCenter, range, filterCat, filterCoalition) if not airbase then return {} end if not range then range = 150000 end local center = airbase:getPoint() local centerName = airbase:getName() local ABinRange = {} local allAB = dcsCommon.getAirbasesWhoseNameContains("*", filterCat, filterCoalition) for idx, aBase in pairs(allAB) do if aBase:getName() ~= centerName then local delta = dcsCommon.dist(center, aBase:getPoint()) if delta <= range then table.insert(ABinRange, aBase) end end end if includeCenter then table.insert(ABinRange, airbase) end return ABinRange end function dcsCommon.getAirbasesInRangeOfAirbaseList(theCenterList, includeList, range, filterCat, filterCoalition) local collectorDict = {} for idx, aCenter in pairs(theCenterList) do -- get all surrounding airbases. returns list of airfields local surroundingAB = dcsCommon.getAirbasesInRangeOfAirbase(airbase, includeList, range, filterCat, filterCoalition) for idx2, theAirField in pairs (surroundingAB) do collectorDict[airField] = theAirField end end -- make result an array local theABList = dcsCommon.enumerateTable(collectorDict) return theABList end -- getAirbasesWhoseNameContains - get all airbases containing -- a name. filterCat is optional and can be aerodrome (0), farp (1), ship (2) -- filterCoalition is optional and can be 0 (neutral), 1 (red), 2 (blue) or -- a table containing categories, e.g. {0, 2} = airfields and ships but not farps -- if no name given or aName = "*", then all bases are returned prior to filtering function dcsCommon.getAirbasesWhoseNameContains(aName, filterCat, filterCoalition) if not aName then aName = "*" end local allYourBase = world.getAirbases() -- get em all local areBelongToUs = {} -- now iterate all bases for idx, aBase in pairs(allYourBase) do local airBaseName = aBase:getName() -- get display name if aName == "*" or dcsCommon.containsString(airBaseName, aName) then -- containsString is case insesitive unless told otherwise local doAdd = true if filterCat then local aCat = dcsCommon.getAirbaseCat(aBase) if type(filterCat) == "table" then local hit = false for idx, fCat in pairs(filterCat) do if fCat == aCat then hit = true end end doAdd = doAdd and hit else -- make sure the airbase is of that category local airCat = aCat doAdd = doAdd and airCat == filterCat end end if filterCoalition then doAdd = doAdd and filterCoalition == aBase:getCoalition() end if doAdd then -- all good, add to table table.insert(areBelongToUs, aBase) end end end return areBelongToUs end function dcsCommon.getFirstAirbaseWhoseNameContains(aName, filterCat, filterCoalition) local allBases = dcsCommon.getAirbasesWhoseNameContains(aName, filterCat, filterCoalition) for idx, aBase in pairs (allBases) do -- simply return first return aBase end return nil end function dcsCommon.getClosestAirbaseTo(thePoint, filterCat, filterCoalition, allYourBase) local delta = math.huge if not allYourBase then allYourBase = dcsCommon.getAirbasesWhoseNameContains("*", filterCat, filterCoalition) -- get em all and filter end local closestBase = nil for idx, aBase in pairs(allYourBase) do -- iterate them all local abPoint = aBase:getPoint() newDelta = dcsCommon.dist(thePoint, {x=abPoint.x, y = 0, z=abPoint.z}) if newDelta < delta then delta = newDelta closestBase = aBase end end return closestBase, delta end function dcsCommon.getClosestFreeSlotForCatInAirbaseTo(cat, x, y, theAirbase, ignore) if not theAirbase then return nil end if not ignore then ignore = {} end if not cat then return nil end if (not cat == "helicopter") and (not cat == "plane") then trigger.action.outText("+++common-getslotforcat: wrong cat <" .. cat .. ">", 30) return nil end local allFree = theAirbase:getParking(true) -- only free slots local filterFreeByType = {} for idx, aSlot in pairs(allFree) do local termT = aSlot.Term_Type if termT == 104 or (termT == 72 and cat == "plane") or (termT == 68 and cat == "plane") or (termT == 40 and cat == "helicopter") then table.insert(filterFreeByType, aSlot) else -- we skip this slot, not good for type end end if #filterFreeByType == 0 then return nil end local reallyFree = {} for idx, aSlot in pairs(filterFreeByType) do local slotNum = aSlot.Term_Index isTaken = false for idy, taken in pairs(ignore) do if taken == slotNum then isTaken = true end end if not isTaken then table.insert(reallyFree, aSlot) end end if #reallyFree < 1 then reallyFree = filterFreeByType end local closestDist = math.huge local closestSlot = nil local p = {x = x, y = 0, z = y} -- !! for idx, aSlot in pairs(reallyFree) do local sp = {x = aSlot.vTerminalPos.x, y = 0, z = aSlot.vTerminalPos.z} local currDist = dcsCommon.distFlat(p, sp) if currDist < closestDist then closestSlot = aSlot closestDist = currDist end end return closestSlot end -- -- U N I T S M A N A G E M E N T -- -- number of living units in group function dcsCommon.livingUnitsInGroup(group) local living = 0 local allUnits = group:getUnits() for key, aUnit in pairs(allUnits) do if aUnit:isExist() and aUnit:getLife() >= 1 then living = living + 1 end end return living end -- closest living unit in group to a point function dcsCommon.getClosestLivingUnitToPoint(group, p) if not p then return nil end if not group then return nil end local closestUnit = nil local closestDist = math.huge local allUnits = group:getUnits() for key, aUnit in pairs(allUnits) do if aUnit:isExist() and aUnit:getLife() >= 1 then local thisDist = dcsCommon.dist(p, aUnit:getPoint()) if thisDist < closestDist then closestDist = thisDist closestUnit = aUnit end end end return closestUnit, closestDist end -- closest living group to a point - cat can be nil or one of Group.Category = { AIRPLANE = 0, HELICOPTER = 1, GROUND = 2, SHIP = 3, TRAIN = 4} function dcsCommon.getClosestLivingGroupToPoint(p, coal, cat) if not cat then cat = 2 end -- ground is default local closestGroup = nil; local closestGroupDist = math.huge local allGroups = coalition.getGroups(coal, cat) -- get all groups from this coalition, perhaps filtered by cat for key, grp in pairs(allGroups) do local closestUnit, dist = dcsCommon.getClosestLivingUnitToPoint(grp, p) if closestUnit then if dist < closestGroupDist then closestGroup = grp closestGroupDist = dist end end end return closestGroup, closestGroupDist end function dcsCommon.getLivingGroupsAndDistInRangeToPoint(p, range, coal, cat) if not cat then cat = 2 end -- ground is default local groupsInRange = {}; local allGroups = coalition.getGroups(coal, cat) -- get all groups from this coalition, perhaps filtered by cat for key, grp in pairs(allGroups) do local closestUnit, dist = dcsCommon.getClosestLivingUnitToPoint(grp, p) if closestUnit then if dist < range then table.insert(groupsInRange, {group = grp, dist = dist}) -- array end end end -- sort the groups by distance table.sort(groupsInRange, function (left, right) return left.dist < right.dist end ) return groupsInRange end -- distFlat ignores y, input must be xyz points, NOT xy points function dcsCommon.distFlat(p1, p2) local point1 = {x = p1.x, y = 0, z=p1.z} local point2 = {x = p2.x, y = 0, z=p2.z} return dcsCommon.dist(point1, point2) end -- distance between points function dcsCommon.dist(point1, point2) -- returns distance between two points -- supports xyz and xy notations if not point1 then trigger.action.outText("+++ warning: nil point1 in common:dist", 30) point1 = {x=0, y=0, z=0} end if not point2 then trigger.action.outText("+++ warning: nil point2 in common:dist", 30) point2 = {x=0, y=0, z=0} stop.here.now = 1 end local p1 = {x = point1.x, y = point1.y} if not point1.z then p1.z = p1.y p1.y = 0 else p1.z = point1.z end local p2 = {x = point2.x, y = point2.y} if not point2.z then p2.z = p2.y p2.y = 0 else p2.z = point2.z end local x = p1.x - p2.x local y = p1.y - p2.y local z = p1.z - p2.z return (x*x + y*y + z*z)^0.5 end function dcsCommon.delta(name1, name2) -- returns distance (in meters) of two named objects local n1Pos = Unit.getByName(name1):getPosition().p local n2Pos = Unit.getByName(name2):getPosition().p return dcsCommon.dist(n1Pos, n2Pos) end -- lerp between a and b, x being 0..1 (percentage), clipped to [0..1] function dcsCommon.lerp(a, b, x) if not a then return 0 end if not b then return 0 end if not x then return a end if x < 0 then x = 0 end if x > 1 then x = 1 end return a + (b - a ) * x end function dcsCommon.bearingFromAtoB(A, B) -- coords in x, z if not A then trigger.action.outText("WARNING: no 'A' in bearingFromAtoB", 30) return 0 end if not B then trigger.action.outText("WARNING: no 'B' in bearingFromAtoB", 30) return 0 end if not A.x then trigger.action.outText("WARNING: no 'A.x' (type A =<" .. type(A) .. ">)in bearingFromAtoB", 30) return 0 end if not A.z then trigger.action.outText("WARNING: no 'A.z' (type A =<" .. type(A) .. ">)in bearingFromAtoB", 30) return 0 end if not B.x then trigger.action.outText("WARNING: no 'B.x' (type B =<" .. type(B) .. ">)in bearingFromAtoB", 30) return 0 end if not B.z then trigger.action.outText("WARNING: no 'B.z' (type B =<" .. type(B) .. ">)in bearingFromAtoB", 30) return 0 end local dx = B.x - A.x local dz = B.z - A.z local bearing = math.atan2(dz, dx) -- in radiants return bearing end function dcsCommon.bearingFromAtoBusingXY(A, B) -- coords in x, y if not A then trigger.action.outText("WARNING: no 'A' in bearingFromAtoBXY", 30) return 0 end if not B then trigger.action.outText("WARNING: no 'B' in bearingFromAtoBXY", 30) return 0 end if not A.x then trigger.action.outText("WARNING: no 'A.x' (type A =<" .. type(A) .. ">)in bearingFromAtoBXY", 30) return 0 end if not A.y then trigger.action.outText("WARNING: no 'A.y' (type A =<" .. type(A) .. ">)in bearingFromAtoBXY", 30) return 0 end if not B.x then trigger.action.outText("WARNING: no 'B.x' (type B =<" .. type(B) .. ">)in bearingFromAtoBXY", 30) return 0 end if not B.y then trigger.action.outText("WARNING: no 'B.y' (type B =<" .. type(B) .. ">)in bearingFromAtoBXY", 30) return 0 end local dx = B.x - A.x local dz = B.y - A.y local bearing = math.atan2(dz, dx) -- in radiants return bearing end function dcsCommon.bearingInDegreesFromAtoB(A, B) local bearing = dcsCommon.bearingFromAtoB(A, B) -- in rads bearing = math.floor(bearing / math.pi * 180) if bearing < 0 then bearing = bearing + 360 end if bearing > 360 then bearing = bearing - 360 end return bearing end function dcsCommon.compassPositionOfARelativeToB(A, B) -- warning: is REVERSE in order for bearing, returns a string like 'Sorth', 'Southwest' if not A then return "***error:A***" end if not B then return "***error:B***" end local bearing = dcsCommon.bearingInDegreesFromAtoB(B, A) -- returns 0..360 if bearing < 23 then return "North" end if bearing < 68 then return "NE" end if bearing < 112 then return "East" end if bearing < 158 then return "SE" end if bearing < 202 then return "South" end if bearing < 248 then return "SW" end if bearing < 292 then return "West" end if bearing < 338 then return "NW" end return "North" end function dcsCommon.bearing2degrees(inRad) local degrees = inRad / math.pi * 180 if degrees < 0 then degrees = degrees + 360 end if degrees > 360 then degrees = degrees - 360 end return degrees end function dcsCommon.bearing2compass(inrad) local bearing = math.floor(inrad / math.pi * 180) if bearing < 0 then bearing = bearing + 360 end if bearing > 360 then bearing = bearing - 360 end return dcsCommon.bearingdegrees2compass(bearing) end function dcsCommon.bearingdegrees2compass(bearing) if bearing < 23 then return "North" end if bearing < 68 then return "NE" end if bearing < 112 then return "East" end if bearing < 158 then return "SE" end if bearing < 202 then return "South" end if bearing < 248 then return "SW" end if bearing < 292 then return "West" end if bearing < 338 then return "NW" end return "North" end function dcsCommon.clockPositionOfARelativeToB(A, B, headingOfBInDegrees) -- o'clock notation if not A then return "***error:A***" end if not B then return "***error:B***" end if not headingOfBInDegrees then headingOfBInDegrees = 0 end local bearing = dcsCommon.bearingInDegreesFromAtoB(B, A) -- returns 0..360 bearing = bearing - headingOfBInDegrees return dcsCommon.getClockDirection(bearing) end -- given a heading, return clock with 0 being 12, 180 being 6 etc. function dcsCommon.getClockDirection(direction) -- inspired by cws, improvements my own if not direction then return 0 end direction = math.fmod (direction, 360) while direction < 0 do direction = direction + 360 end while direction >= 360 do direction = direction - 360 end if direction < 15 then -- special case 12 o'clock past 12 o'clock return 12 end direction = direction + 15 -- add offset so we get all other times correct return math.floor(direction/30) end function dcsCommon.getGeneralDirection(direction) -- inspired by cws, improvements my own if not direction then return "unkown" end direction = math.fmod (direction, 360) while direction < 0 do direction = direction + 360 end while direction >= 360 do direction = direction - 360 end if direction < 45 then return "ahead" end if direction < 135 then return "right" end if direction < 225 then return "behind" end if direction < 315 then return "left" end return "ahead" end function dcsCommon.getNauticalDirection(direction) -- inspired by cws, improvements my own if not direction then return "unkown" end direction = math.fmod (direction, 360) while direction < 0 do direction = direction + 360 end while direction >= 360 do direction = direction - 360 end if direction < 45 then return "ahead" end if direction < 135 then return "starboard" end if direction < 225 then return "aft" end if direction < 315 then return "port" end return "ahead" end function dcsCommon.aspectByDirection(direction) -- inspired by cws, improvements my own if not direction then return "unkown" end direction = math.fmod (direction, 360) while direction < 0 do direction = direction + 360 end while direction >= 360 do direction = direction - 360 end if direction < 45 then return "hot" end if direction < 135 then return "beam" end if direction < 225 then return "drag" end if direction < 315 then return "beam" end return "hot" end function dcsCommon.whichSideOfMine(theUnit, target) -- returs two values: -1/1 = left/right and "left"/"right" if not theUnit then return nil end if not target then return nil end local uDOF = theUnit:getPosition() -- returns p, x, y, z Vec3 -- with x, y, z being the normalised vectors for right, up, forward local heading = math.atan2(uDOF.x.z, uDOF.x.x) -- returns rads if heading < 0 then heading = heading + 2 * math.pi -- put heading in range of 0 to 2*pi end -- heading now runs from 0 through 2Pi local A = uDOF.p local B = target:getPoint() -- now get bearing from theUnit to target local dx = B.x - A.x local dz = B.z - A.z local bearing = math.atan2(dz, dx) -- in rads if bearing < 0 then bearing = bearing + 2 * math.pi -- make bearing 0 to 2*pi end -- we now have bearing to B, and own heading. -- subtract own heading from bearing to see at what -- bearing target would be if we 'turned the world' so -- that theUnit is heading 0 local dBearing = bearing - heading -- if result < 0 or > Pi (=180°), target is left from us if dBearing < 0 or dBearing > math.pi then return -1, "left" end return 1, "right" -- note: no separate case for straight in front or behind end -- Distance of point p to line defined by p1,p2 -- only on XZ map function dcsCommon.distanceOfPointPToLineXZ(p, p1, p2) local x21 = p2.x - p1.x local y10 = p1.z - p.z local x10 = p1.x - p.x local y21 = p2.z - p1.z local numer = math.abs((x21*y10) - (x10 * y21)) local denom = math.sqrt(x21 * x21 + y21 * y21) local dist = numer/denom return dist end function dcsCommon.randomDegrees() local degrees = math.random(360) * 3.14152 / 180 return degrees end function dcsCommon.randomPercent() local percent = math.random(100)/100 return percent end function dcsCommon.randomPointOnPerimeter(sourceRadius, x, z) return dcsCommon.randomPointInCircle(sourceRadius, sourceRadius-1, x, z) end function dcsCommon.randomPointInCircle(sourceRadius, innerRadius, x, z) if not x then x = 0 end if not z then z = 0 end --local y = 0 if not innerRadius then innerRadius = 0 end if innerRadius < 0 then innerRadius = 0 end local percent = dcsCommon.randomPercent() -- 1 / math.random(100) -- now lets get a random degree local degrees = dcsCommon.randomDegrees() -- math.random(360) * 3.14152 / 180 -- ok, it's actually radiants. local r = (sourceRadius-innerRadius) * percent x = x + (innerRadius + r) * math.cos(degrees) z = z + (innerRadius + r) * math.sin(degrees) local thePoint = {} thePoint.x = x thePoint.y = 0 thePoint.z = z return thePoint, degrees end function dcsCommon.newPointAtDegreesRange(p1, degrees, radius) local rads = degrees * 3.14152 / 180 local p2 = dcsCommon.newPointAtAngleRange(p1, rads, radius) return p2 end function dcsCommon.newPointAtAngleRange(p1, angle, radius) local p2 = {} p2.x = p1.x + radius * math.cos(angle) p2.y = p1.y p2.z = p1.z + radius * math.sin(angle) return p2 end -- get group location: get the group's location by -- accessing the fist existing, alive member of the group that it finds function dcsCommon.getGroupLocation(group, verbose, gName) if not verbose then verbose = false end -- nifty trick from mist: make this work with group and group name if type(group) == 'string' then -- group name group = Group.getByName(group) end -- get all units local allUnits = group:getUnits() if not allUnits then if verbose then trigger.action.outText("++++common: no group location for <" .. gName .. ">, skipping.", 30) end return nil end -- iterate through all members of group until one is alive and exists for index, theUnit in pairs(allUnits) do if (theUnit:isExist() and theUnit:getLife() > 0) then return theUnit:getPosition().p end end -- if we get here, there was no live unit return nil end -- get the group's first Unit that exists and is -- alive function dcsCommon.getGroupUnit(group) if not group then return nil end -- nifty trick from mist: make this work with group and group name if type(group) == 'string' then -- group name group = Group.getByName(group) end if not group:isExist() then return nil end -- get all units local allUnits = group:getUnits() -- iterate through all members of group until one is alive and exists for index, theUnit in pairs(allUnits) do if Unit.isExist(theUnit) and theUnit:getLife() > 0 then return theUnit end; end -- if we get here, there was no live unit return nil end -- and here the alias function dcsCommon.getFirstLivingUnit(group) return dcsCommon.getGroupUnit(group) end -- isGroupAlive returns true if there is at least one unit in the group that isn't dead function dcsCommon.isGroupAlive(group) return (dcsCommon.getGroupUnit(group) ~= nil) end function dcsCommon.getLiveGroupUnits(group) -- nifty trick from mist: make this work with group and group name if type(group) == 'string' then -- group name group = Group.getByName(group) end local liveUnits = {} -- get all units local allUnits = group:getUnits() -- iterate through all members of group until one is alive and exists for index, theUnit in pairs(allUnits) do if (theUnit:isExist() and theUnit:getLife() > 0) then table.insert(liveUnits, theUnit) end; end -- if we get here, there was no live unit return liveUnits end function dcsCommon.getGroupTypeString(group) -- convert into comma separated types if not group then trigger.action.outText("+++cmn getGroupTypeString: nil group", 30) return "" end if not dcsCommon.isGroupAlive(group) then trigger.action.outText("+++cmn getGroupTypeString: dead group", 30) return "" end local theTypes = "" local liveUnits = dcsCommon.getLiveGroupUnits(group) for i=1, #liveUnits do if i > 1 then theTypes = theTypes .. "," end theTypes = theTypes .. liveUnits[i]:getTypeName() end return theTypes end function dcsCommon.getGroupTypes(group) if not group then trigger.action.outText("+++cmn getGroupTypes: nil group", 30) return {} end if not dcsCommon.isGroupAlive(group) then trigger.action.outText("+++cmn getGroupTypes: dead group", 30) return {} end local liveUnits = dcsCommon.getLiveGroupUnits(group) local unitTypes = {} for i=1, #liveUnits do table.insert(unitTypes, liveUnits[i]:getTypeName()) end return unitTypes end function dcsCommon.getEnemyCoalitionFor(aCoalition) if type(aCoalition) == "string" then aCoalition = aCoalition:lower() if aCoalition == "red" then return 2 end if aCoalition == "blue" then return 1 end return nil end if aCoalition == 1 then return 2 end if aCoalition == 2 then return 1 end return nil end function dcsCommon.getACountryForCoalition(aCoalition) -- scan the table of countries and get the first country that is part of aCoalition -- this is useful if you want to create troops for a coalition but don't know the -- coalition's countries -- we start with id=0 (Russia), go to id=85 (Slovenia), but skip id = 14 local i = 0 while i < dcsCommon.maxCountry do -- 86 do if i ~= 14 then if (coalition.getCountryCoalition(i) == aCoalition) then return i end end i = i + 1 end return nil end function dcsCommon.getCountriesForCoalition(aCoalition) if not aCoalition then aCoalition = 0 end local allCty = {} local i = 0 while i < dcsCommon.maxCountry do if i ~= 14 then -- there is no county 14 if (coalition.getCountryCoalition(i) == aCoalition) then table.insert(allCty, i) end end i = i + 1 end return allCty end -- -- -- C A L L B A C K H A N D L E R -- -- -- installing callbacks -- based on mist, with optional additional hooks for pre- and post- -- processing of the event -- when filtering occurs in pre, an alternative 'rejected' handler can be called function dcsCommon.addEventHandler(f, pre, post, rejected) -- returns ID local handler = {} -- build a wrapper and connect the onEvent handler.id = dcsCommon.uuid("eventHandler") handler.f = f -- the callback itself if (rejected) then handler.rejected = rejected end -- now set up pre- and post-processors. defaults are set in place -- so pre and post are optional. If pre returns false, the callback will -- not be invoked if (pre) then handler.pre = pre else handler.pre = dcsCommon.preCall end if (post) then handler.post = post else handler.post = dcsCommon.postCall end function handler:onEvent(event) if not self.pre(event) then if dcsCommon.verbose then end if (self.rejected) then self.rejected(event) end return end self.f(event) -- call the handler self.post(event) -- do post-processing end world.addEventHandler(handler) return handler.id end function dcsCommon.preCall(e) -- we can filter here -- if we return false, the call is abortet if dcsCommon.verbose then trigger.action.outText("event " .. e.id .. " received: PRE-PROCESSING", 10) end return true; end; function dcsCommon.postCall(e) -- we do pos proccing here if dcsCommon.verbose then trigger.action.outText("event " .. e.id .. " received: post proc", 10) end end -- highly specific eventhandler for one event only -- based on above, with direct filtering built in; skips pre -- but does post function dcsCommon.addEventHandlerForEventTypes(f, evTypes, post, rejected) -- returns ID local handler = {} -- build a wrapper and connect the onEvent dcsCommon.cbID = dcsCommon.cbID + 1 -- increment unique count handler.id = dcsCommon.cbID handler.what = evTypes if (rejected) then handler.rejected = rejected end handler.f = f -- set the callback itself -- now set up post-processor. pre is hard-coded to match evType -- post is optional. If event.id is not in evTypes, the callback will -- not be invoked if (post) then handler.post = post else handler.post = dcsCommon.postCall end function handler:onEvent(event) hasMatch = false; for key, evType in pairs(self.what) do if evType == event.id then hasMatch = true; break; end; end; if not hasMatch then if dcsCommon.verbose then trigger.action.outText("event " .. e.id .. " discarded - not in whitelist evTypes", 10) end if (self.rejected) then self.rejected(event) end return; end; self.f(event) -- call the actual handler as passed to us self.post(event) -- do post-processing end world.addEventHandler(handler) -- add to event handlers return handler.id end -- remove event handler / callback, identical to Mist -- note we don't call world.removeEventHandler, but rather directly -- access world.eventHandlers directly and remove kvp directly. function dcsCommon.removeEventHandler(id) for key, handler in pairs(world.eventHandlers) do if handler.id and handler.id == id then world.eventHandlers[key] = nil return true end end return false end -- -- -- C L O N I N G -- -- -- topClone is a shallow clone of orig, only top level is iterated, -- all values are ref-copied function dcsCommon.topClone(orig) if not orig then return nil end local orig_type = type(orig) local copy if orig_type == 'table' then copy = {} for orig_key, orig_value in pairs(orig) do copy[orig_key] = orig_value end else -- number, string, boolean, etc copy = orig end return copy end -- clone is a recursive clone which will also clone -- deeper levels, as used in units function dcsCommon.clone(orig, stripMeta) if not orig then return nil end local orig_type = type(orig) local copy if orig_type == 'table' then copy = {} for orig_key, orig_value in next, orig, nil do copy[dcsCommon.clone(orig_key)] = dcsCommon.clone(orig_value) end if not stripMeta then -- also connect meta data setmetatable(copy, dcsCommon.clone(getmetatable(orig))) else -- strip all except string, and for strings use a fresh string if type(copy) == "string" then local tmp = "" tmp = tmp .. copy -- will get rid of any foreign metas for string copy = tmp end end elseif orig_type == "string" then local tmp = "" copy = tmp .. orig else -- number, string, boolean, etc copy = orig end return copy end function dcsCommon.copyArray(inArray) if not inArray then return nil end -- warning: this is a ref copy! local theCopy = {} for idx, element in pairs(inArray) do table.insert(theCopy, element) end return theCopy end -- -- -- S P A W N I N G -- -- function dcsCommon.createEmptyGroundGroupData (name) local theGroup = {} -- empty group theGroup.visible = false theGroup.taskSelected = true -- theGroup.route = {} -- theGroup.groupId = id theGroup.tasks = {} -- theGroup.hidden = false -- hidden on f10? theGroup.units = { } -- insert units here! -- use addUnitToGroupData theGroup.x = 0 theGroup.y = 0 theGroup.name = name -- theGroup.start_time = 0 theGroup.task = "Ground Nothing" return theGroup end; function dcsCommon.createEmptyAircraftGroupData (name) local theGroup = dcsCommon.createEmptyGroundGroupData(name)--{} -- empty group theGroup.task = "Nothing" -- can be others, like Transport, CAS, etc -- returns with empty route theGroup.route = dcsCommon.createEmptyAircraftRouteData() -- we can add points here return theGroup end; function dcsCommon.createAircraftRoutePointData(x, z, altitudeInFeet, knots, altType, action) local rp = {} rp.x = x rp.y = z rp.action = "Turning Point" rp.type = "Turning Point" if action then rp.action = action; rp.type = action end -- warning: may not be correct, need to verify later rp.alt = altitudeInFeet * 0.3048 -- in m rp.speed = knots * 0.514444 -- we use m/s rp.alt_type = "BARO" if (altType) then rp.alt_type = altType end return rp end function dcsCommon.addRoutePointDataToRouteData(inRoute, x, z, altitudeInFeet, knots, altType, action) local p = dcsCommon.createAircraftRoutePointData(x, z, altitudeInFeet, knots, altType, action) local thePoints = inRoute.points table.insert(thePoints, p) end function dcsCommon.addRoutePointDataToGroupData(group, x, z, altitudeInFeet, knots, altType, action) if not group.route then group.route = dcsCommon.createEmptyAircraftRouteData() end local theRoute = group.route dcsCommon.addRoutePointDataToRouteData(theRoute, x, z, altitudeInFeet, knots, altType, action) end function dcsCommon.addRoutePointForGroupData(theGroup, theRP) if not theGroup then return end if not theGroup.route then theGroup.route = dcsCommon.createEmptyAircraftRouteData() end local theRoute = theGroup.route local thePoints = theRoute.points table.insert(thePoints, theRP) end function dcsCommon.createEmptyAircraftRouteData() local route = {} route.points = {} return route end function dcsCommon.createTakeOffFromGroundRoutePointData(pt, isHot) -- vec 3! if not pt then return nil end local rp = {} rp.x = pt.x rp.y = pt.z rp.alt = pt.y if isHot then rp.action = "From Ground Area Hot" rp.type = "TakeOffGroundHot" else rp.action = "From Ground Area" -- add " Hot" if hot rp.type = "TakeOffGround" -- add "Hot" (NO blank) if hot end rp.speed = 10 -- that's 36 km/h rp.alt_type = "BARO" return rp end function dcsCommon.createTakeOffFromParkingRoutePointData(aerodrome) if not aerodrome then return nil end local rp = {} local freeParkingSlot = dcsCommon.getFirstFreeParkingSlot(aerodrome, 104) -- get big slot first if not freeParkingSlot then freeParkingSlot = dcsCommon.getFirstFreeParkingSlot(aerodrome) -- try any size end if not freeParkingSlot then trigger.action.outText("civA: no free parking at " .. aerodrome:getName(), 30) return nil end local p = freeParkingSlot.vTerminalPos rp.airdromeId = aerodrome:getID() rp.x = p.x rp.y = p.z rp.alt = p.y rp.action = "From Parking Area" rp.type = "TakeOffParking" rp.speed = 100 -- that's 360 km/h rp.alt_type = "BARO" return rp end function dcsCommon.createOverheadAirdromeRoutePointData(aerodrome) if not aerodrome then return nil end local rp = {} local p = aerodrome:getPoint() rp.x = p.x rp.y = p.z rp.alt = p.y + 2000 -- 6000 ft overhead rp.action = "Turning Point" rp.type = "Turning Point" rp.speed = 133; -- in m/s? If so, that's 360 km/h rp.alt_type = "BARO" return rp end function dcsCommon.createOverheadAirdromeRoutPintData(aerodrome) -- backwards-compat to typo return dcsCommon.createOverheadAirdromeRoutePointData(aerodrome) end function dcsCommon.createLandAtAerodromeRoutePointData(aerodrome) if not aerodrome then return nil end local rp = {} local p = aerodrome:getPoint() rp.airdromeId = aerodrome:getID() rp.x = p.x rp.y = p.z rp.alt = land.getHeight({x=p.x, y=p.z}) --p.y rp.action = "Landing" rp.type = "Land" rp.speed = 100; -- in m/s? If so, that's 360 km/h rp.alt_type = "BARO" return rp end function dcsCommon.createSimpleRoutePointData(p, alt, speed) if not speed then speed = 133 end if not alt then alt = 8000 end -- 24'000 feet local rp = {} rp.x = p.x rp.y = p.z rp.alt = alt rp.action = "Turning Point" rp.type = "Turning Point" rp.speed = speed; -- in m/s? If so, that's 360 km/h rp.alt_type = "BARO" return rp end function dcsCommon.createRPFormationData(findex) -- must be added as "task" to an RP. use 4 for Echelon right local task = {} task.id = "ComboTask" local params = {} task.params = params local tasks = {} params.tasks = tasks local t1 = {} tasks[1] = t1 t1.number = 1 t1.auto = false t1.id = "WrappedAction" t1.enabled = true local t1p = {} t1.params = t1p local action = {} t1p.action = action action.id = "Option" local ap = {} action.params = ap ap.variantIndex = 3 ap.name = 5 -- AI.Option.Air.ID 5 = Formation ap.formationIndex = findex -- 4 is echelon_right ap.value = 262147 return task end function dcsCommon.addTaskDataToRP(theTask, theGroup, rpIndex) local theRoute = theGroup.route local thePoints = theRoute.points local rp = thePoints[rpIndex] rp.task = theTask end -- create a minimal payload table that is compatible with creating -- a unit. you may need to alter this before adding the unit to -- the mission. all params optional function dcsCommon.createPayload(fuel, flare, chaff, gun) local payload = {} payload.pylons = {} if not fuel then fuel = 1000 end -- in kg. check against fuelMassMax in type desc if not flare then flare = 0 end if not chaff then chaff = 0 end if not gun then gun = 0 end return payload end function dcsCommon.createCallsign(cs) local callsign = {} callsign[1] = 1 callsign[2] = 1 callsign[3] = 1 if not cs then cs = "Enfield11" end callsign.name = cs return callsign end -- create the data table required to spawn a unit. -- unit types are defined in https://github.com/mrSkortch/DCS-miscScripts/tree/master/ObjectDB function dcsCommon.createGroundUnitData(name, unitType, transportable) local theUnit = {} unitType = dcsCommon.trim(unitType) theUnit.type = unitType -- e.g. "LAV-25", if not transportable then transportable = false end -- elaborate, not requried code theUnit.transportable = {["randomTransportable"] = transportable} -- theUnit.unitId = id theUnit.skill = "Average" -- always average theUnit.x = 0 -- make it zero, zero! theUnit.y = 0 theUnit.name = name theUnit.playerCanDrive = false theUnit.heading = 0 return theUnit end function dcsCommon.createAircraftUnitData(name, unitType, transportable, altitude, speed, heading) local theAirUnit = dcsCommon.createGroundUnitData(name, unitType, transportable) theAirUnit.alt = 100 -- make it 100m if altitude then theAirUnit.alt = altitude end theAirUnit.alt_type = "RADIO" -- AGL theAirUnit.speed = 77 -- m/s --> 150 knots if speed then theAirUnit.speed = speed end if heading then theAirUnit.heading = heading end theAirUnit.payload = dcsCommon.createPayload() theAirUnit.callsign = dcsCommon.createCallsign() return theAirUnit end function dcsCommon.addUnitToGroupData(theUnit, theGroup, dx, dy, heading) -- add a unit to a group, and place it at dx, dy of group's position, -- taking into account unit's own current location if not dx then dx = 0 end if not dy then dy = 0 end if not heading then heading = 0 end theUnit.x = theUnit.x + dx + theGroup.x theUnit.y = theUnit.y + dy + theGroup.y theUnit.heading = heading table.insert(theGroup.units, theUnit) end; function dcsCommon.createSingleUnitGroup(name, theUnitType, x, z, heading) -- create the container local theNewGroup = dcsCommon.createEmptyGroundGroupData(name) local aUnit = {} aUnit = dcsCommon.createGroundUnitData(name .. "-1", theUnitType, false) -- trigger.action.outText("dcsCommon - unit name retval " .. aUnit.name, 30) dcsCommon.addUnitToGroupData(aUnit, theNewGroup, x, z, heading) return theNewGroup end function dcsCommon.arrangeGroupDataIntoFormation(theNewGroup, radius, minDist, formation, innerRadius) -- formations: -- (default) "line" (left to right along x) -- that is Y direction -- "line_v" a line top to bottom -- that is X direction -- "chevron" - left to right middle too top -- "scattered", "random" -- random, innerRadius used to clear area in center -- "circle", "circle_forward" -- circle, forward facing -- "circle_in" -- circle, inwarf facing -- "circle_out" -- circle, outward facing -- "grid", "square", "rect" -- optimal rectangle -- "2cols", "2deep" -- 2 columns, n deep -- "2wide" -- 2 columns wide, 2 deep local num = #theNewGroup.units -- now do the formation stuff -- make sure that they keep minimum distance if formation == "LINE_V" then -- top to bottom in zone (heding 0). -- will run through x-coordinate -- use entire radius top to bottom local currX = -radius local increment = radius * 2/(num - 1) -- MUST NOT TRY WITH 1 UNIT! for i=1, num do local u = theNewGroup.units[i] u.x = currX currX = currX + increment end elseif formation == "LINE" then -- left to right in zone. runs through Y -- left and right are y because at heading 0, forward is x (not y as expected) -- if only one, place in middle of circle and be done if num == 1 then -- nothing. just stay in the middle else local currY = -radius local increment = radius * 2/(num - 1) -- MUST NOT TRY WITH 1 UNIT! for i=1, num do local u = theNewGroup.units[i] u.y = currY currY = currY + increment end end elseif formation == "CHEVRON" then -- left to right in zone. runs through Y -- left and right are y because at heading 0, forward is x (not y as expected) local currY = -radius local currX = 0 local incrementY = radius * 2/(num - 1) -- MUST NOT TRY WITH 1 UNIT! local incrementX = radius * 2/(num - 1) -- MUST NOT TRY WITH 1 UNIT! for i=1, num do local u = theNewGroup.units[i] u.x = currX u.y = currY -- calc coords for NEXT iteration currY = currY + incrementY -- march left to right if i < num / 2 then -- march up currX = currX + incrementX elseif i == num / 2 then -- even number, keep height currX = currX + 0 else currX = currX - incrementX -- march down end -- note: when unit number even, the wedge is sloped. may need an odd/even test for better looks end elseif formation == "SCATTERED" or formation == "RANDOM" then -- use randomPointInCircle and tehn iterate over all vehicles for mindelta processedUnits = {} if not minDist then minDist = 10 end for i=1, num do local emergencyBreak = 1 -- prevent endless loop local lowDist = 10000 local uPoint = {} local thePoint = {} repeat -- get random point until mindistance to all is kept or emergencybreak thePoint = dcsCommon.randomPointInCircle(radius, innerRadius) -- returns x, 0, z -- check if too close to others for idx, rUnit in pairs(processedUnits) do -- get min dist to all positioned units --trigger.action.outText("rPnt: thePoint = " .. dcsCommon.point2text(thePoint), 30) uPoint.x = rUnit.x uPoint.y = 0 uPoint.z = rUnit.y --trigger.action.outText("rPnt: uPoint = " .. dcsCommon.point2text(uPoint), 30) local dist = dcsCommon.dist(thePoint, uPoint) -- measure distance to unit if (dist < lowDist) then lowDist = dist end end emergencyBreak = emergencyBreak + 1 until (emergencyBreak > 20) or (lowDist > minDist) -- we have random x, y local u = theNewGroup.units[i] -- get unit to position u.x = thePoint.x u.y = thePoint.z -- z --> y mapping! -- now add the unit to the 'processed' set table.insert(processedUnits, u) end elseif dcsCommon.stringStartsWith(formation, "CIRCLE") then -- units are arranged on perimeter of circle defined by radius local currAngle = 0 local angleInc = 2 * 3.14157 / num -- increase per spoke for i=1, num do local u = theNewGroup.units[i] -- get unit u.x = radius * math.cos(currAngle) u.y = radius * math.sin(currAngle) -- now baldower out heading -- circle, circle_forward no modifier of heading if dcsCommon.stringStartsWith(formation, "CIRCLE_IN") then -- make the heading inward faceing - that's angle + pi u.heading = u.heading + currAngle + 3.14157 elseif dcsCommon.stringStartsWith(formation, "CIRCLE_OUT") then u.heading = u.heading + currAngle + 0 end currAngle = currAngle + angleInc end elseif formation == "GRID" or formation == "SQUARE" or formation == "RECT" then if num < 2 then return end -- arrange units in an w x h grid -- e-g- 12 units = 4 x 3. -- calculate w local w = math.floor(num^(0.5) + 0.5) dcsCommon.arrangeGroupInNColumns(theNewGroup, w, radius) elseif formation == "2DEEP" or formation == "2COLS" then if num < 2 then return end -- arrange units in an 2 x h grid local w = 2 dcsCommon.arrangeGroupInNColumnsDeep(theNewGroup, w, radius) elseif formation == "2WIDE" then if num < 2 then return end -- arrange units in an 2 x h grid local w = 2 dcsCommon.arrangeGroupInNColumns(theNewGroup, w, radius) else trigger.action.outText("dcsCommon - unknown formation: " .. formation, 30) end end function dcsCommon.arrangeGroupInNColumns(theNewGroup, w, radius) local num = #theNewGroup.units local h = math.floor(num / w) if (num % w) > 0 then h = h + 1 end local i = 1 local xInc = 0 if w > 1 then xInc = 2 * radius / (w-1) end local yInc = 0 if h > 1 then yInc = 2 * radius / (h-1) end local currY = radius if h < 2 then currY = 0 end -- special:_ place in Y middle if only one row) while h > 0 do local currX = radius local wCnt = w while wCnt > 0 and (i <= num) do local u = theNewGroup.units[i] -- get unit u.x = currX u.y = currY currX = currX - xInc wCnt = wCnt - 1 i = i + 1 end currY = currY - yInc h = h - 1 end end function dcsCommon.arrangeGroupInNColumnsDeep(theNewGroup, w, radius) local num = #theNewGroup.units local h = math.floor(num / w) if (num % w) > 0 then h = h + 1 end local i = 1 local yInc = 0 if w > 1 then yInc = 2 * radius / (w-1) end local xInc = 0 if h > 1 then xInc = 2 * radius / (h-1) end local currX = radius if h < 2 then currX = 0 end -- special:_ place in Y middle if only one row) while h > 0 do local currY = radius local wCnt = w while wCnt > 0 and (i <= num) do local u = theNewGroup.units[i] -- get unit u.x = currX u.y = currY currY = currY - yInc wCnt = wCnt - 1 i = i + 1 end currX = currX - xInc h = h - 1 end end function dcsCommon.createGroundGroupWithUnits(name, theUnitTypes, radius, minDist, formation, innerRadius, liveries) -- liveries is indexed by typeName and provides alternate livery names -- from default. if not minDist then minDist = 4 end -- meters if not formation then formation = "line" end if not radius then radius = 30 end -- meters if not innerRadius then innerRadius = 0 end if not liveries then liveries = {} end formation = formation:upper() -- theUnitTypes can be either a single string or a table of strings -- see here for TypeName https://github.com/mrSkortch/DCS-miscScripts/tree/master/ObjectDB -- formation defines how the units are going to be arranged in the -- formation specified. -- formations: -- (default) "line" (left to right along x) -- that is Y direction -- "line_V" a line top to bottom -- that is X direction -- "chevron" - left to right middle too top -- "scattered", "random" -- random, innerRadius used to clear area in center -- "circle", "circle_forward" -- circle, forward facing -- "circle_in" -- circle, inwarf facing -- "circle_out" -- circle, outward facing -- first, we create a group local theNewGroup = dcsCommon.createEmptyGroundGroupData(name) -- now add a single unit or multiple units if type(theUnitTypes) ~= "table" then local aUnit = {} aUnit = dcsCommon.createGroundUnitData(name .. "-1", theUnitTypes, false) dcsCommon.addUnitToGroupData(aUnit, theNewGroup, 0, 0) -- create with data at location (0,0) return theNewGroup end -- if we get here, theUnitTypes is a table -- now loop and create a unit for each table local num = 1 for key, theType in pairs(theUnitTypes) do -- trigger.action.outText("+++dcsC: creating unit " .. name .. "-" .. num .. ": " .. theType, 30) local aUnit = dcsCommon.createGroundUnitData(name .. "-"..num, theType, false) local theLivery = liveries[theType] if theLivery then aUnit.livery_id = theLivery end dcsCommon.addUnitToGroupData(aUnit, theNewGroup, 0, 0) num = num + 1 end dcsCommon.arrangeGroupDataIntoFormation(theNewGroup, radius, minDist, formation, innerRadius) return theNewGroup end -- create a new group, based on group in mission. Groups coords are 0,0 for group and all -- x,y and heading function dcsCommon.createGroupDataFromLiveGroup(name, newName) if not newName then newName = dcsCommon.uuid("uniqName") end -- get access to the group local liveGroup = Group.getByName(name) if not liveGroup then return nil end -- get the categorty local cat = liveGroup:getCategory() local theNewGroup = {} -- create a new empty group at (0,0) if cat == Group.Category.AIRPLANE or cat == Group.Category.HELICOPTER then theNewGroup = dcsCommon.createEmptyAircraftGroupData(newName) elseif cat == Group.Category.GROUND then theNewGroup = dcsCommon.createEmptyGroudGroupData(newName) else trigger.action.outText("dcsCommon - unknown category: " .. cat, 30) return nil end -- now get all units from live group and create data units -- note that unit data for group has x=0, y=0 liveUnits = liveGroup:getUnits() for index, theUnit in pairs(liveUnits) do -- for each unit we get the desc local desc = theUnit:getDesc() -- of interest is only typename local newUnit = dcsCommon.createGroundUnitData(dcsCommon.uuid(newName), desc.typeName, false) -- we now basically have a ground unit at (0,0) -- add mandatory fields by type if cat == Group.Category.AIRPLANE or cat == Group.Category.HELICOPTER then newUnit.alt = 100 -- make it 100m newUnit.alt_type = "RADIO" -- AGL newUnit.speed = 77 -- m/s --> 150 knots newUnit.payload = dcsCommon.createPayload() -- empty payload newUnit.callsign = dcsCommon.createCallsign() -- 'enfield11' elseif cat == Group.Category.GROUND then -- we got all we need else end end end; function dcsCommon.pointInDirectionOfPointXYY(dir, dist, p) -- dir in rad, p in XYZ returns XZZ local fx = math.cos(dir) local fy = math.sin(dir) local p2 = {} p2.x = p.x + dist * fx p2.y = p.z + dist * fy p2.z = p2.y -- make p2 XYY vec2/3 upcast return p2 end function dcsCommon.pointXpercentYdegOffAB(A, B, xPer, yDeg) -- rets xzz point local bearingRad = dcsCommon.bearingFromAtoB(A, B) local dist = dcsCommon.dist(A, B) local deviation = bearingRad + yDeg * 0.0174533 local newDist = dist * xPer/100 local newPoint = dcsCommon.pointInDirectionOfPointXYY(deviation, newDist, A) return newPoint end function dcsCommon.rotatePointAroundOriginRad(inX, inY, angle) -- angle in degrees local c = math.cos(angle) local s = math.sin(angle) local px local py px = inX * c - inY * s py = inX * s + inY * c return px, py end function dcsCommon.rotatePointAroundOrigin(inX, inY, angle) -- angle in degrees local rads = 3.14152 / 180 -- convert to radiants. angle = angle * rads -- turns into rads local px, py = dcsCommon.rotatePointAroundOriginRad(inX, inY, angle) return px, py end function dcsCommon.rotatePointAroundPointRad(x, y, px, py, angle) x = x - px y = y - py x, y = dcsCommon.rotatePointAroundOriginRad(x, y, angle) x = x + px y = y + py return x, y end function dcsCommon.rotatePointAroundPointDeg(x, y, px, py, degrees) x, y = dcsCommon.rotatePointAroundPointRad(x, y, px, py, degrees * 3.14152 / 180) return x, y end -- rotates a Vec3-base inPoly on XZ pane around inPoint on XZ pane function dcsCommon.rotatePoly3AroundVec3Rad(inPoly, inPoint, rads) local outPoly = {} for idx, aVertex in pairs(inPoly) do local x, z = dcsCommon.rotatePointAroundPointRad(aVertex.x, aVertex.z, inPoint.x, inPoint.z, rads) local v3 = {x = x, y = aVertex.y, z = z} outPoly[idx] = v3 end return outPoly end function dcsCommon.rotateUnitData(theUnit, degrees, cx, cz) if not cx then cx = 0 end if not cz then cz = 0 end local cy = cz local rads = degrees * 3.14152 / 180 do theUnit.x = theUnit.x - cx -- MOVE TO ORIGIN OF ROTATION theUnit.y = theUnit.y - cy theUnit.x, theUnit.y = dcsCommon.rotatePointAroundOrigin(theUnit.x, theUnit.y, degrees) theUnit.x = theUnit.x + cx -- MOVE BACK theUnit.y = theUnit.y + cy -- may also want to increase heading by degrees theUnit.heading = theUnit.heading + rads end end function dcsCommon.rotateGroupData(theGroup, degrees, cx, cz) if not cx then cx = 0 end if not cz then cz = 0 end local cy = cz local rads = degrees * 3.14152 / 180 -- turns all units in group around the group's center by degrees. -- may also need to turn individual units by same amount for i, theUnit in pairs (theGroup.units) do theUnit.x = theUnit.x - cx -- MOVE TO ORIGIN OF ROTATION theUnit.y = theUnit.y - cy theUnit.x, theUnit.y = dcsCommon.rotatePointAroundOrigin(theUnit.x, theUnit.y, degrees) theUnit.x = theUnit.x + cx -- MOVE BACK theUnit.y = theUnit.y + cy -- may also want to increase heading by degrees theUnit.heading = theUnit.heading + rads if theUnit.psi then theUnit.psi = -theUnit.heading end end end function dcsCommon.offsetGroupData(theGroup, dx, dy) -- add dx and dy to group's and all unit's coords for i, theUnit in pairs (theGroup.units) do theUnit.x = theUnit.x + dx theUnit.y = theUnit.y + dy end theGroup.x = theGroup.x + dx theGroup.y = theGroup.y + dy end function dcsCommon.moveGroupDataTo(theGroup, xAbs, yAbs) local dx = xAbs-theGroup.x local dy = yAbs-theGroup.y dcsCommon.offsetGroupData(theGroup, dx, dy) end -- static objectr shapes and types are defined here -- https://github.com/mrSkortch/DCS-miscScripts/tree/master/ObjectDB/Statics function dcsCommon.createStaticObjectData(name, objType, heading, dead, cargo, mass) local staticObj = {} if not heading then heading = 0 end if not dead then dead = false end if not cargo then cargo = false end objType = dcsCommon.trim(objType) staticObj.heading = heading -- staticObj.groupId = 0 -- staticObj.shape_name = shape -- e.g. H-Windsock_RW staticObj.type = objType -- e.g. Windsock -- ["unitId"] = 3, staticObj.rate = 1 -- score when killed staticObj.name = name -- staticObj.category = "Fortifications", staticObj.y = 0 staticObj.x = 0 staticObj.dead = dead staticObj.canCargo = cargo -- to cargo if cargo then if not mass then mass = 1234 end staticObj.mass = mass -- to cargo end return staticObj end function dcsCommon.createStaticObjectDataAt(loc, name, objType, heading, dead) local theData = dcsCommon.createStaticObjectData(name, objType, heading, dead) theData.x = loc.x theData.y = loc.z return theData end function dcsCommon.createStaticObjectForCoalitionAtLocation(theCoalition, loc, name, objType, heading, dead) if not heading then heading = math.random(360) * 3.1415 / 180 end local theData = dcsCommon.createStaticObjectDataAt(loc, name, objType, heading, dead) local theStatic = coalition.addStaticObject(theCoalition, theData) -- warning! coalition is not country! return theStatic end function dcsCommon.createStaticObjectForCoalitionInRandomRing(theCoalition, objType, x, z, innerRadius, outerRadius, heading, alive) if not outerRadius then outerRadius = innerRadius end if not heading then heading = math.random(360) * 3.1415 / 180 end local dead = not alive local p = dcsCommon.randomPointInCircle(outerRadius, innerRadius, x, z) local theData = dcsCommon.createStaticObjectData(dcsCommon.uuid("static"), objType, heading, dead) theData.x = p.x theData.y = p.z local theStatic = coalition.addStaticObject(theCoalition, theData) -- warning! coalition is not country return theStatic, p.x, p.z end function dcsCommon.linkStaticDataToUnit(theStatic, theUnit, dx, dy, heading) if not theStatic then trigger.action.OutText("+++dcsC: NIL theStatic on linkStatic!", 30) return end -- NOTE: we may get current heading and subtract/add -- to original heading local rotX, rotY = dcsCommon.rotatePointAroundOrigin(dx, dy, -heading) if not theUnit then return end if not theUnit:isExist() then return end theStatic.linkOffset = true theStatic.linkUnit = theUnit:getID() local unitPos = theUnit:getPoint() local offsets = {} offsets.x = rotX offsets.y = rotY offsets.angle = 0 theStatic.offsets = offsets end function dcsCommon.offsetStaticData(theStatic, dx, dy) theStatic.x = theStatic.x + dx theStatic.y = theStatic.y + dy -- now check if thre is a route (for linked objects) if theStatic.route then -- access points[1] x and y and copy from main theStatic.route.points[1].x = theStatic.x theStatic.route.points[1].y = theStatic.y end end function dcsCommon.moveStaticDataTo(theStatic, x, y) theStatic.x = x theStatic.y = y -- now check if thre is a route (for linked objects) if theStatic.route then -- access points[1] x and y and copy from main theStatic.route.points[1].x = theStatic.x theStatic.route.points[1].y = theStatic.y end end function dcsCommon.synchGroupData(inGroupData) -- update group data block by -- comparing it to spawned group and update units by x, y, heding and isExist -- modifies inGroupData! if not inGroupData then return end -- groupdata from game, NOT MX DATA! -- we synch the units and their coords local livingUnits = {} for idx, unitData in pairs(inGroupData.units) do local theUnit = Unit.getByName(unitData.name) if theUnit and theUnit:isExist() and theUnit:getLife()>1 then -- update x and y and heading local pos = theUnit:getPoint() unitData.unitId = theUnit:getID() unitData.x = pos.x unitData.y = pos.z -- !!!! unitData.heading = dcsCommon.getUnitHeading(gUnit) table.insert(livingUnits, unitData) end end inGroupData.units = livingUnits end -- -- -- M I S C M E T H O D S -- -- -- as arrayContainsString, except it includes wildcard matches if EITHER -- ends on "*" function dcsCommon.wildArrayContainsString(theArray, theString, caseSensitive) if not theArray then return false end if not theString then return false end if not caseSensitive then caseSensitive = false end if type(theArray) ~= "table" then trigger.action.outText("***wildArrayContainsString: theArray is not type table but <" .. type(theArray) .. ">", 30) end if not caseSensitive then theString = string.upper(theString) end local wildIn = dcsCommon.stringEndsWith(theString, "*") if wildIn then theString = dcsCommon.removeEnding(theString, "*") end for idx, theElement in pairs(theArray) do -- i = 1, #theArray do if not caseSensitive then theElement = string.upper(theElement) end local wildEle = dcsCommon.stringEndsWith(theElement, "*") if wildEle then theElement = dcsCommon.removeEnding(theElement, "*") end if wildEle and wildIn then -- both end on wildcards, partial match for both if dcsCommon.stringStartsWith(theElement, theString) then return true end if dcsCommon.stringStartsWith(theString, theElement) then return true end elseif wildEle then -- Element is a wildcard, partial match if dcsCommon.stringStartsWith(theString, theElement) then return true end elseif wildIn then -- theString is a wildcard. partial match if dcsCommon.stringStartsWith(theElement, theString) then return true end else -- standard: no wildcards, full match if theElement == theString then return true end end end return false end function dcsCommon.arrayContainsString(theArray, theString) if not theArray then return false end if not theString then return false end if type(theArray) ~= "table" then trigger.action.outText("***arrayContainsString: theArray is not type