dcsCommon = {} dcsCommon.version = "2.9.2" --[[-- VERSION HISTORY 2.2.6 - compassPositionOfARelativeToB - clockPositionOfARelativeToB 2.2.7 - isTroopCarrier - distFlat 2.2.8 - fixed event2text 2.2.9 - getUnitAGL - getUnitAlt - getUnitSpeed - getUnitHeading - getUnitHeadingDegrees - mag - clockPositionOfARelativeToB with own heading 2.3.0 - unitIsInfantry 2.3.1 - bool2YesNo - bool2Text 2.3.2 - getGroupAvgSpeed - getGroupMaxSpeed 2.3.3 - getSizeOfTable 2.3.4 - isSceneryObject coalition2county 2.3.5 - smallRandom pickRandom uses smallRandom airfield handling, parking flight waypoint handling landing waypoint creation take-off waypoint creation 2.3.6 - createOverheadAirdromeRoutPintData(aerodrome) 2.3.7 - coalition2county - warning when creating UN 2.3.8 - improved headingOfBInDegrees, new getClockDirection 2.3.9 - getClosingVelocity - dot product - magSquare - vMag 2.4.0 - libCheck 2.4.1 - grid/square/rect formation - arrangeGroupInNColumns formation - 2Columns formation deep and wide formation 2.4.2 - getAirbasesInRangeOfPoint 2.4.3 - lerp 2.4.4 - getClosestAirbaseTo - fixed bug in containsString when strings equal 2.4.5 - added cargo and mass options to createStaticObjectData 2.4.6 - fixed randompercent 2.4.7 - smokeColor2Num(smokeColor) 2.4.8 - linkStaticDataToUnit() 2.4.9 - trim functions - createGroundUnitData uses trim function to remove leading/trailing blanks so now we can use blanks after comma to separate types - dcsCommon.trimArray( - createStaticObjectData uses trim for type - getEnemyCoalitionFor understands strings, still returns number - coalition2county also understands 'red' and 'blue' 2.5.0 - "Line" formation with one unit places unit at center 2.5.1 - vNorm(a) 2.5.1 - added SA-18 Igla manpad to unitIsInfantry() 2.5.2 - added copyArray method - corrected heading in createStaticObjectData 2.5.3 - corrected rotateGroupData bug for cz - removed forced error in failed pickRandom 2.5.4 - rotateUnitData() - randomBetween() 2.5.5 - stringStartsWithDigit() - stringStartsWithLetter() - stringIsPositiveNumber() 2.5.6 - corrected stringEndsWith() bug with str 2.5.7 - point2text(p) 2.5.8 - string2GroupCat() 2.5.9 - string2ObjectCat() 2.6.0 - unified uuid, removed uuIdent 2.6.1 - removed bug in rotateUnitData: cy --> cz param passing 2.6.2 - new combineTables() 2.6.3 - new tacan2freq() 2.6.4 - new processHMS() 2.6.5 - new bearing2compass() - new bearingdegrees2compass() - new latLon2Text() - based on mist 2.6.6 - new nowString() - new str2num() - new stringRemainsStartingWith() - new stripLF() - new removeBlanks() 2.6.7 - new menu2text() 2.6.8 - new getMissionName() - new flagArrayFromString() 2.6.9 - new getSceneryObjectsInZone() - new getSceneryObjectInZoneByName() 2.7.0 - new synchGroupData() clone, topClone and copyArray now all nil-trap 2.7.1 - new isPlayerUnit() -- moved from cfxPlayer new getAllExistingPlayerUnitsRaw - from cfxPlayer new typeIsInfantry() 2.7.2 - new rangeArrayFromString() fixed leading blank bug in flagArrayFromString new incFlag() new decFlag() nil trap in stringStartsWith() new getClosestFreeSlotForCatInAirbaseTo() 2.7.3 - new string2Array() - additional guard for isPlayerUnit 2.7.4 - new array2string() 2.7.5 - new bitAND32() - new LSR() - new num2bin() 2.7.6 - new getObjectsForCatAtPointWithRadius() 2.7.7 - clone() has new stripMeta option. pass true to remove all meta tables - dumpVar2Str detects meta tables - rotateGroupData kills unit's psi value if it existed since it messes with heading - rotateGroupData - changes psi to -heading if it exists rather than nilling 2.7.8 - new getGeneralDirection() - new getNauticalDirection() - more robust guards for getUnitSpeed 2.7.9 - new bool2Num(theBool) - new aspectByDirection() - createGroundGroupWithUnits corrected spelling of minDist, crashed scattered formation - randomPointInCircle fixed erroneous local for x, z - "scattered" formation repaired 2.7.10- semaphore groundwork 2.8.0 - new collectMissionIDs at start-up - new getUnitNameByID - new getGroupNameByID - bool2YesNo alsco can return NIL - new getUnitStartPosByID 2.8.1 - arrayContainsString: type checking for theArray and warning - processStringWildcards() - new wildArrayContainsString() - fix for stringStartsWith oddity with aircraft types 2.8.2 - better fixes for string.find() in stringStartsWith and containsString - dcsCommon.isTroopCarrier(theUnit, carriers) new carriers optional param - better guards for getUnitAlt and getUnitAGL - new newPointAtDegreesRange() - new newPointAtAngleRange() - new isTroopCarrierType() - stringStartsWith now supports case insensitive match - isTroopCarrier() supports 'any' and 'all' - made getEnemyCoalitionFor() more resilient - fix to smallRandom for negative numbers - isTroopCarrierType uses wildArrayContainsString 2.8.3 - small optimizations in bearingFromAtoB() - new whichSideOfMine() 2.8.4 - new rotatePointAroundOriginRad() - new rotatePointAroundPointDeg() - new rotatePointAroundPointRad() - getClosestAirbaseTo() now supports passing list of air bases 2.8.5 - better guard in getGroupUnit() 2.8.6 - phonetic helpers new spellString() 2.8.7 - new flareColor2Num() - new flareColor2Text() - new iteratePlayers() 2.8.8 - new hexString2RGBA() - new playerName2Coalition() - new coalition2Text() 2.8.9 - vAdd supports xy and xyz - vSub supports xy and xyz - vMultScalar supports xy and xyz 2.8.10 - tacan2freq now integrated with module (blush) - array2string cosmetic default - vMultScalar corrected bug in accessing b.z - new randomLetter() - new getPlayerUnit() - new getMapName() - new getMagDeclForPoint() 2.9.0 - createPoint() moved from cfxZones - copyPoint() moved from cfxZones - numberArrayFromString() moved from cfxZones 2.9.1 - new createSimpleRoutePointData() - createOverheadAirdromeRoutPintData corrected and legacy support added - new bearingFromAtoBusingXY() - corrected verbosity for bearingFromAtoB - new getCountriesForCoalition() 2.9.2 - updated task2text --]]-- -- dcsCommon is a library of common lua functions -- for easy access and simple mission programming -- (c) 2021 - 2023 by Chritian 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.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 -- -- 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) -- if no name given or aName = "*", then all bases are returned prior to filtering function dcsCommon.getAirbasesWhoseNameContains(aName, filterCat, filterCoalition) --trigger.action.outText("getAB(name): enter with " .. aName, 30) 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 --if aName ~= "*" then -- trigger.action.outText("getAB(name): matched " .. airBaseName, 30) --end local doAdd = true if filterCat then -- make sure the airbase is of that category local airCat = dcsCommon.getAirbaseCat(aBase) doAdd = doAdd and airCat == filterCat 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) --trigger.action.outText("slot <" .. aSlot.Term_Index .. "> has dist " .. math.floor(currDist) .. " and _0 of <" .. aSlot.Term_Index_0 .. ">", 30) if currDist < closestDist then closestSlot = aSlot closestDist = currDist end end --trigger.action.outText("slot <" .. closestSlot.Term_Index .. "> has closest dist <" .. math.floor(closestDist) .. ">", 30) 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.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 -- trigger.action.outText("+++comm: oclock - bearing = " .. bearing .. " and inHeading = " .. headingOfBInDegrees, 30) 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 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) -- 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() -- 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 --trigger.action.outText("+++cmn: A group has no live units. returning nil", 10) 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 --trigger.action.outText("+++cmn A group has no live units. returning nil", 10) 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 --dcsCommon.cbID = dcsCommon.cbID + 1 -- increment unique count 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 -- trigger.action.outText("event " .. event.id .. " discarded by pre-processor", 10) 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 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 rp.speed = knots * 0.514444 -- we use 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.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; -- in m/s? If so, 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) 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 = 133; -- 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 -- trigger.action.outText("dcsCommon - processing formation " .. formation .. " with radius = " .. radius, 30) 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] -- trigger.action.outText("formation unit " .. u.name .. " currX = " .. currX, 30) 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] -- trigger.action.outText("formation unit " .. u.name .. " currX = " .. currY, 30) 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] -- trigger.action.outText("formation unit " .. u.name .. " currX = " .. currX .. " currY = " .. currY, 30) 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 = {} 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 -- trigger.action.outText("formation circle detected", 30) 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) --[[-- local h = math.floor(num / w) --trigger.action.outText("AdcsC: num=" .. num .. " w=" .. w .. "h=" .. h .. " -- num%w=" .. num%w, 30) if (num % w) > 0 then h = h + 1 end --trigger.action.outText("BdcsC: num=" .. num .. " w=" .. w .. "h=" .. h, 30) -- now w * h always >= num and num items fir in that grid -- w is width, h is height, of course :) -- now calculat xInc and yInc 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 --]]-- 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) 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 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 -- trigger.action.outText("dcsCommon - i am here", 30) -- trigger.action.outText("dcsCommon - name " .. name, 30) -- trigger.action.outText("dcsCommon - unit type " .. theUnitTypes, 30) local aUnit = {} aUnit = dcsCommon.createGroundUnitData(name .. "-1", theUnitTypes, false) -- trigger.action.outText("dcsCommon - unit name retval " .. aUnit.name, 30) 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) 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 -- trigger.action.outText("dcsCommon - unknown category: " .. cat, 30) -- return nil -- we also got all we need end end 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 --trigger.action.outText("+++dcsC:rotGrp cy,cy = "..cx .. "," .. cy, 30) 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 --trigger.action.outText("+++dcsC:rotGrp cy,cy = "..cx .. "," .. cy, 30) 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 -- now kill psi if it existed before -- theUnit.psi = nil -- better code: psi is always -heading. Nobody knows what psi is, though 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) 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) return theStatic 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("***arrayContainsString: theArray is not type table but <" .. type(theArray) .. ">", 30) end if not caseSensitive then theString = string.upper(theString) end --trigger.action.outText("wildACS: theString = <" .. theString .. ">, theArray contains <" .. #theArray .. "> elements", 30) local wildIn = dcsCommon.stringEndsWith(theString, "*") if wildIn then dcsCommon.removeEnding(theString, "*") end for idx, theElement in pairs(theArray) do -- i = 1, #theArray do --local theElement = theArray[i] --trigger.action.outText("test e <" .. theElement .. "> against s <" .. theString .. ">", 30) if not caseSensitive then theElement = string.upper(theElement) end local wildEle = dcsCommon.stringEndsWith(theElement, "*") if wildEle then theElement = dcsCommon.removeEnding(theElement, "*") end --trigger.action.outText("matching s=<" .. theString .. "> with e=<" .. theElement .. ">", 30) 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 --trigger.action.outText("match e* with s* failed.", 30) elseif wildEle then -- Element is a wildcard, partial match if dcsCommon.stringStartsWith(theString, theElement) then return true end --trigger.action.outText("startswith - match e* <" .. theElement .. "> with s <" .. theString .. "> failed.", 30) elseif wildIn then -- theString is a wildcard. partial match if dcsCommon.stringStartsWith(theElement, theString) then return true end --trigger.action.outText("match e with s* failed.", 30) else -- standard: no wildcards, full match if theElement == theString then return true end --trigger.action.outText("match e with s (straight) failed.", 30) 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 table but <" .. type(theArray) .. ">", 30) end for i = 1, #theArray do if theArray[i] == theString then return true end end return false end function dcsCommon.splitString(inputstr, sep) if sep == nil then sep = "%s" end if inputstr == nil then inputstr = "" end local t={} for str in string.gmatch(inputstr, "([^"..sep.."]+)") do table.insert(t, str) end return t end function dcsCommon.trimFront(inputstr) if not inputstr then return nil end local s = inputstr while string.len(s) > 1 and string.sub(s, 1, 1) == " " do local snew = string.sub(s, 2) -- all except first s = snew end return s end function dcsCommon.trimBack(inputstr) if not inputstr then return nil end local s = inputstr while string.len(s) > 1 and string.sub(s, -1) == " " do local snew = string.sub(s, 1, -2) -- all except last s = snew end return s end function dcsCommon.trim(inputstr) local t1 = dcsCommon.trimFront(inputstr) local t2 = dcsCommon.trimBack(t1) return t2 end function dcsCommon.trimArray(theArray) local trimmedArray = {} for idx, element in pairs(theArray) do local tel = dcsCommon.trim(element) table.insert(trimmedArray, tel) end return trimmedArray end function dcsCommon.string2Array(inString, deli, uCase) if not inString then return {} end if not deli then return {} end if not uCase then uCase = false end if uCase then inString = string.upper(inString) end inString = dcsCommon.trim(inString) if dcsCommon.containsString(inString, deli) then local a = dcsCommon.splitString(inString, deli) a = dcsCommon.trimArray(a) return a else return {inString} end end function dcsCommon.array2string(inArray, deli) if not deli then deli = ", " end if type(inArray) ~= "table" then return "" end local s = "" local count = 0 for idx, ele in pairs(inArray) do if count > 0 then s = s .. deli .. " " end s = s .. ele count = count + 1 end return s end function dcsCommon.stripLF(theString) return theString:gsub("[\r\n]", "") end function dcsCommon.removeBlanks(theString) return theString:gsub("%s", "") end function dcsCommon.stringIsPositiveNumber(theString) -- only full integer positive numbers supported if not theString then return false end -- if theString == "" then return false end for i = 1, #theString do local c = theString:sub(i,i) if c < "0" or c > "9" then return false end end return true end function dcsCommon.stringStartsWithDigit(theString) if #theString < 1 then return false end local c = string.sub(theString, 1, 1) return c >= "0" and c <= "9" end function dcsCommon.stringStartsWithLetter(theString) if #theString < 1 then return false end local c = string.sub(theString, 1, 1) if c >= "a" and c <= "z" then return true end if c >= "A" and c <= "Z" then return true end return false end function dcsCommon.stringStartsWith(theString, thePrefix, caseInsensitive) if not theString then return false end if not thePrefix then return false end if not caseInsensitive then caseInsensitive = false end if caseInsensitive then theString = string.upper(theString) thePrefix = string.upper(theString) end -- new code because old 'string.find' had some really -- strange results with aircraft types. Prefix "A-10" did not -- match string "A-10A" etc. -- superseded: string.find (s, pattern [, init [, plain]]) solves the problem --[[ local pl = string.len(thePrefix) if pl > string.len(theString) then return false end if pl < 1 then return false end for i=1, pl do local left = string.sub(theString, i, i) local right = string.sub(thePrefix, i, i) if left ~= right then return false end end return true --]]-- trigger.action.outText("---- OK???", 30) -- strange stuff happening with some strings, let's investigate local i, j = string.find(theString, thePrefix, 1, true) return (i == 1) --[[-- if res then trigger.action.outText("startswith: <" .. theString .. "> pre <" .. thePrefix .. "> --> YES", 30) else trigger.action.outText("startswith: <" .. theString .. "> nojoy pre <" .. thePrefix .. ">", 30) end return res --]]-- end function dcsCommon.removePrefix(theString, thePrefix) if not dcsCommon.stringStartsWith(theString, thePrefix) then return theString end; return theString:sub(1 + #thePrefix) end function dcsCommon.stringEndsWith(theString, theEnding) return theEnding == "" or theString:sub(-#theEnding) == theEnding end function dcsCommon.removeEnding(theString, theEnding) if not dcsCommon.stringEndsWith(theString, theEnding) then return theString end return theString:sub(1, #theString - #theEnding) end function dcsCommon.containsString(inString, what, caseSensitive) if (not caseSensitive) then inString = string.upper(inString) what = string.upper(what) end if inString == what then return true end -- when entire match return string.find(inString, what, 1, true) -- 1, true means start at 1, plaintext end function dcsCommon.bool2Text(theBool) if not theBool then theBool = false end if theBool then return "true" end return "false" end function dcsCommon.bool2YesNo(theBool) if not theBool then theBool = false return "NIL" end if theBool then return "yes" end return "no" end function dcsCommon.bool2Num(theBool) if not theBool then theBool = false end if theBool then return 1 end return 0 end function dcsCommon.point2text(p) if not p then return "" end local t = "[x=" if p.x then t = t .. p.x .. ", " else t = t .. ", " end if p.y then t = t .. "y=" .. p.y .. ", " else t = t .. "y=, " end if p.z then t = t .. "z=" .. p.z .. "]" else t = t .. "z=]" end return t end function dcsCommon.string2GroupCat(inString) if not inString then return 2 end -- default ground inString = inString:lower() inString = dcsCommon.trim(inString) local catNum = tonumber(inString) if catNum then if catNum < 0 then catNum = 0 end if catNum > 4 then catNum = 4 end return catNum end catNum = 2 -- ground default if dcsCommon.stringStartsWith(inString, "grou") then catNum = 2 end if dcsCommon.stringStartsWith(inString, "air") then catNum = 0 end if dcsCommon.stringStartsWith(inString, "hel") then catNum = 1 end if dcsCommon.stringStartsWith(inString, "shi") then catNum = 3 end if dcsCommon.stringStartsWith(inString, "trai") then catNum = 4 end return catNum end function dcsCommon.string2ObjectCat(inString) if not inString then return 3 end -- default static inString = inString:lower() inString = dcsCommon.trim(inString) local catNum = tonumber(inString) if catNum then if catNum < 0 then catNum = 0 end if catNum > 6 then catNum = 6 end return catNum end catNum = 3 -- static default if dcsCommon.stringStartsWith(inString, "uni") then catNum = 1 end if dcsCommon.stringStartsWith(inString, "wea") then catNum = 2 end if dcsCommon.stringStartsWith(inString, "bas") then catNum = 4 end if dcsCommon.stringStartsWith(inString, "sce") then catNum = 5 end if dcsCommon.stringStartsWith(inString, "car") then catNum = 6 end return catNum end function dcsCommon.menu2text(inMenu) if not inMenu then return "" end local s = "" for n, v in pairs(inMenu) do if type(v) == "string" then if s == "" then s = "[" .. v .. "]" else s = s .. " | [" .. type(v) .. "]" end else if s == "" then s = "[<" .. type(v) .. ">]" else s = s .. " | [<" .. type(v) .. ">]" end end end return s end -- recursively show the contents of a variable function dcsCommon.dumpVar(key, value, prefix, inrecursion) if not inrecursion then -- output a marker to find in the log / screen env.info("*** dcsCommon vardump START") end if not value then value = "nil" end if not prefix then prefix = "" end prefix = " " .. prefix if type(value) == "table" then env.info(prefix .. key .. ": [ ") -- iterate through all kvp for k,v in pairs (value) do dcsCommon.dumpVar(k, v, prefix, true) end env.info(prefix .. " ] - end " .. key) elseif type(value) == "boolean" then local b = "false" if value then b = "true" end env.info(prefix .. key .. ": " .. b) else -- simple var, show contents, ends recursion env.info(prefix .. key .. ": " .. value) end if not inrecursion then -- output a marker to find in the log / screen trigger.action.outText("=== dcsCommon vardump end", 30) env.info("=== dcsCommon vardump end") end end function dcsCommon.dumpVar2Str(key, value, prefix, inrecursion) -- dumps to screen, not string if not inrecursion then -- output a marker to find in the log / screen trigger.action.outText("*** dcsCommon vardump START",30) end if not value then value = "nil" end if not prefix then prefix = "" end prefix = " " .. prefix if getmetatable(value) then if type(value) == "string" then else trigger.action.outText(prefix .. key (" .. type(value) .. ") .. " HAS META", 30) end end if type(value) == "table" then trigger.action.outText(prefix .. key .. ": [ ", 30) -- iterate through all kvp for k,v in pairs (value) do dcsCommon.dumpVar2Str(k, v, prefix, true) end trigger.action.outText(prefix .. " ] - end " .. key, 30) elseif type(value) == "boolean" then local b = "false" if value then b = "true" end trigger.action.outText(prefix .. key .. ": " .. b, 30) else -- simple var, show contents, ends recursion trigger.action.outText(prefix .. key .. ": " .. value, 30) end if not inrecursion then -- output a marker to find in the log / screen trigger.action.outText("=== dcsCommon vardump end", 30) --env.info("=== dcsCommon vardump end") end end function dcsCommon.numberUUID() dcsCommon.simpleUUID = dcsCommon.simpleUUID + 1 return dcsCommon.simpleUUID end function dcsCommon.uuid(prefix) --dcsCommon.uuIdent = dcsCommon.uuIdent + 1 if not prefix then prefix = dcsCommon.uuidStr end return prefix .. "-" .. dcsCommon.numberUUID() -- dcsCommon.uuIdent end function dcsCommon.event2text(id) if not id then return "error" end if id == 0 then return "invalid" end -- translate the event id to text local events = {"shot", "hit", "takeoff", "land", "crash", "eject", "refuel", "dead", -- 8 "pilot dead", "base captured", "mission start", "mission end", -- 12 "took control", "refuel stop", "birth", "human failure", -- 16 "det. failure", "engine start", "engine stop", "player enter unit", -- 20 "player leave unit", "player comment", "start shoot", "end shoot", -- 24 "mark add", "mark changed", "mark removed", "kill", -- 28 "score", "unit lost", "land after eject", "Paratrooper land", -- 32 "chair discard after eject", "weapon add", "trigger zone", "landing quality mark", -- 36 "BDA", "AI Abort Mission", "DayNight", "Flight Time", -- 40 "Pilot Suicide", "player cap airfield", "emergency landing", "unit create task", -- 44 "unit delete task", "Simulation start", "weapon rearm", "weapon drop", -- 48 "unit task timeout", "unit task stage", "max"} if id > #events then return "Unknown (ID=" .. id .. ")" end return events[id] end function dcsCommon.smokeColor2Text(smokeColor) if (smokeColor == 0) then return "Green" end if (smokeColor == 1) then return "Red" end if (smokeColor == 2) then return "White" end if (smokeColor == 3) then return "Orange" end if (smokeColor == 4) then return "Blue" end return ("unknown: " .. smokeColor) end function dcsCommon.flareColor2Text(flareColor) if (flareColor == 0) then return "Green" end if (flareColor == 1) then return "Red" end if (flareColor == 2) then return "White" end if (flareColor == 3) then return "Yellow" end if (flareColor < 0) then return "Random" end return ("unknown: " .. flareColor) end function dcsCommon.smokeColor2Num(smokeColor) if not smokeColor then smokeColor = "green" end if type(smokeColor) ~= "string" then return 0 end smokeColor = smokeColor:lower() if (smokeColor == "green") then return 0 end if (smokeColor == "red") then return 1 end if (smokeColor == "white") then return 2 end if (smokeColor == "orange") then return 3 end if (smokeColor == "blue") then return 4 end return 0 end function dcsCommon.flareColor2Num(flareColor) if not flareColor then flareColor = "green" end if type(flareColor) ~= "string" then return 0 end flareColor = flareColor:lower() if (flareColor == "green") then return 0 end if (flareColor == "red") then return 1 end if (flareColor == "white") then return 2 end if (flareColor == "yellow") then return 3 end if (flareColor == "random") then return -1 end if (flareColor == "rnd") then return -1 end return 0 end function dcsCommon.markPointWithSmoke(p, smokeColor) if not smokeColor then smokeColor = 0 end local x = p.x local z = p.z -- do NOT change the point directly -- height-correct local y = land.getHeight({x = x, y = z}) local newPoint= {x = x, y = y + 2, z = z} trigger.action.smoke(newPoint, smokeColor) end -- based on buzzer1977's idea, channel is number, eg in 74X, channel is 74, mode is "X" function dcsCommon.tacan2freq(channel, mode) if not mode then mode = "X" end if not channel then channel = 1 end if type(mode) ~= "string" then mode = "X" end mode = mode:upper() local offset = 1000000 * channel if channel < 64 then if mode == "Y" then return 1087000000 + offset end return 961000000 + offset -- mode x end if mode == "Y" then return 961000000 + offset end return 1087000000 + offset -- mode x end function dcsCommon.processHMS(msg, delta) local rS = math.floor(delta) local remainS = tostring(rS) local rM = math.floor(delta/60) local remainM = tostring(rM) local rH = math.floor(delta/3600) local remainH = tostring(rH) local hmsH = remainH if rH < 10 then hmsH = "0" .. hmsH end local hmsCount = delta - (rH * 3600) -- mins left local mins = math.floor (hmsCount / 60) local hmsM = tostring(mins) if mins < 10 then hmsM = "0" .. hmsM end hmsCount = hmsCount - (mins * 60) local secs = math.floor(hmsCount) local hmsS = tostring(secs) if secs < 10 then hmsS = "0" .. hmsS end msg = string.gsub(msg, "", remainS) msg = string.gsub(msg, "", remainM) msg = string.gsub(msg, "", remainH) msg = string.gsub(msg, "<:s>", hmsS) msg = string.gsub(msg, "<:m>", hmsM) msg = string.gsub(msg, "<:h>", hmsH) return msg end function dcsCommon.nowString() local absSecs = timer.getAbsTime()-- + env.mission.start_time while absSecs > 86400 do absSecs = absSecs - 86400 -- subtract out all days end return dcsCommon.processHMS("<:h>:<:m>:<:s>", absSecs) end function dcsCommon.str2num(inVal, default) if not default then default = 0 end if not inVal then return default end if type(inVal) == "number" then return inVal end local num = nil if type(inVal) == "string" then num = tonumber(inVal) end if not num then return default end return num end function dcsCommon.stringRemainsStartingWith(theString, startingWith) -- find the first position where startingWith starts local pos = theString:find(startingWith) if not pos then return theString end -- now return the entire remainder of the string from pos local nums = theString:len() - pos + 1 return theString:sub(-nums) end -- -- -- V E C T O R M A T H -- -- function dcsCommon.vAdd(a, b) local r = {} if not a then a = {x = 0, y = 0, z = 0} end if not b then b = {x = 0, y = 0, z = 0} end r.x = a.x + b.x r.y = a.y + b.y if a.z and b.z then r.z = a.z + b.z end return r end function dcsCommon.vSub(a, b) local r = {} if not a then a = {x = 0, y = 0, z = 0} end if not b then b = {x = 0, y = 0, z = 0} end r.x = a.x - b.x r.y = a.y - b.y if a.z and b.z then r.z = a.z - b.z end return r end function dcsCommon.vMultScalar(a, f) local r = {} if not a then a = {x = 0, y = 0, z = 0} end if not f then f = 0 end r.x = a.x * f r.y = a.y * f if a.z then r.z = a.z * f end return r end function dcsCommon.vLerp (a, b, t) if not a then a = {x = 0, y = 0, z = 0} end if not b then b = {x = 0, y = 0, z = 0} end local d = dcsCommon.vSub(b, a) local dt = dcsCommon.vMultScalar(d, t) local r = dcsCommon.vAdd(a, dt) return r end function dcsCommon.mag(x, y, z) if not x then x = 0 end if not y then y = 0 end if not z then z = 0 end return (x * x + y * y + z * z)^0.5 end function dcsCommon.vMag(a) if not a then return 0 end if not a.x then a.x = 0 end if not a.y then a.y = 0 end if not a.z then a.z = 0 end return dcsCommon.mag(a.x, a.y, a.z) end function dcsCommon.magSquare(x, y, z) if not x then x = 0 end if not y then y = 0 end if not z then z = 0 end return (x * x + y * y + z * z) end function dcsCommon.vNorm(a) if not a then return {x = 0, y = 0, z = 0} end m = dcsCommon.vMag(a) if m <= 0 then return {x = 0, y = 0, z = 0} end local r = {} r.x = a.x / m r.y = a.y / m r.z = a.z / m return r end function dcsCommon.dot (a, b) if not a then a = {} end if not a.x then a.x = 0 end if not a.y then a.y = 0 end if not a.z then a.z = 0 end if not b then b = {} end if not b.x then b.x = 0 end if not b.y then b.y = 0 end if not b.z then b.z = 0 end return a.x * b.x + a.y * b.y + a.z * b.z end -- -- UNIT MISC -- function dcsCommon.isSceneryObject(theUnit) if not theUnit then return false end return theUnit.getCoalition == nil -- scenery objects do not return a coalition end function dcsCommon.isTroopCarrierType(theType, carriers) if not theType then return false end if not carriers then carriers = dcsCommon.troopCarriers end -- remember that arrayContainsString is case INsensitive by default if dcsCommon.wildArrayContainsString(carriers, theType) then -- may add additional tests before returning true return true end -- see if user wanted 'any' or 'all' supported if dcsCommon.arrayContainsString(carriers, "any") then return true end if dcsCommon.arrayContainsString(carriers, "all") then return true end return false end function dcsCommon.isTroopCarrier(theUnit, carriers) -- return true if conf can carry troups if not theUnit then return false end local uType = theUnit:getTypeName() return dcsCommon.isTroopCarrierType(uType, carriers) end function dcsCommon.getAllExistingPlayerUnitsRaw() local apu = {} for idx, theSide in pairs(dcsCommon.coalitionSides) do local thePlayers = coalition.getPlayers(theSide) for idy, theUnit in pairs (thePlayers) do if theUnit and theUnit:isExist() then table.insert(apu, theUnit) end end end return apu end function dcsCommon.getUnitAlt(theUnit) if not theUnit then return 0 end if not Unit.isExist(theUnit) then return 0 end -- safer local p = theUnit:getPoint() return p.y end function dcsCommon.getUnitAGL(theUnit) if not theUnit then return 0 end if not Unit.isExist(theUnit) then return 0 end -- safe fix local p = theUnit:getPoint() local alt = p.y local loc = {x = p.x, y = p.z} local landElev = land.getHeight(loc) return alt - landElev end function dcsCommon.getUnitSpeed(theUnit) if not theUnit then return 0 end if not Unit.isExist(theUnit) then return 0 end local v = theUnit:getVelocity() return dcsCommon.mag(v.x, v.y, v.z) end -- closing velocity of u1 and u2, seen from u1 function dcsCommon.getClosingVelocity(u1, u2) if not u1 then return 0 end if not u2 then return 0 end if not u1:isExist() then return 0 end if not u2:isExist() then return 0 end local v1 = u1:getVelocity() local v2 = u2:getVelocity() local dV = dcsCommon.vSub(v1,v2) local a = u1:getPoint() local b = u2:getPoint() local aMinusB = dcsCommon.vSub(a,b) -- vector from u2 to u1 local abMag = dcsCommon.vMag(aMinusB) -- distance u1 to u2 if abMag < .0001 then return 0 end -- project deltaV onto vector from u2 to u1 local vClose = dcsCommon.dot(dV, aMinusB) / abMag return vClose end function dcsCommon.getGroupAvgSpeed(theGroup) if not theGroup then return 0 end if not dcsCommon.isGroupAlive(theGroup) then return 0 end local totalSpeed = 0 local cnt = 0 local livingUnits = theGroup:getUnits() for idx, theUnit in pairs(livingUnits) do cnt = cnt + 1 totalSpeed = totalSpeed + dcsCommon.getUnitSpeed(theUnit) end if cnt == 0 then return 0 end return totalSpeed / cnt end function dcsCommon.getGroupMaxSpeed(theGroup) if not theGroup then return 0 end if not dcsCommon.isGroupAlive(theGroup) then return 0 end local maxSpeed = 0 local livingUnits = theGroup:getUnits() for idx, theUnit in pairs(livingUnits) do currSpeed = dcsCommon.getUnitSpeed(theUnit) if currSpeed > maxSpeed then maxSpeed = currSpeed end end return maxSpeed end function dcsCommon.getUnitHeading(theUnit) if not theUnit then return 0 end if not theUnit:isExist() then return 0 end local pos = theUnit:getPosition() -- returns three vectors, p is location local heading = math.atan2(pos.x.z, pos.x.x) -- make sure positive only, add 360 degrees if heading < 0 then heading = heading + 2 * math.pi -- put heading in range of 0 to 2*pi end return heading end function dcsCommon.getUnitHeadingDegrees(theUnit) local heading = dcsCommon.getUnitHeading(theUnit) return heading * 57.2958 -- 180 / math.pi end function dcsCommon.typeIsInfantry(theType) local isInfantry = dcsCommon.containsString(theType, "infantry", false) or dcsCommon.containsString(theType, "paratrooper", false) or dcsCommon.containsString(theType, "stinger", false) or dcsCommon.containsString(theType, "manpad", false) or dcsCommon.containsString(theType, "soldier", false) or dcsCommon.containsString(theType, "SA-18 Igla", false) return isInfantry end function dcsCommon.unitIsInfantry(theUnit) if not theUnit then return false end if not theUnit:isExist() then return end local theType = theUnit:getTypeName() --[[-- local isInfantry = dcsCommon.containsString(theType, "infantry", false) or dcsCommon.containsString(theType, "paratrooper", false) or dcsCommon.containsString(theType, "stinger", false) or dcsCommon.containsString(theType, "manpad", false) or dcsCommon.containsString(theType, "soldier", false) or dcsCommon.containsString(theType, "SA-18 Igla", false) return isInfantry --]]-- return dcsCommon.typeIsInfantry(theType) end function dcsCommon.coalition2county(inCoalition) -- simply return UN troops for 0 neutral, -- joint red for 1 red -- joint blue for 2 blue if inCoalition == 1 then return 81 end -- cjtf red if inCoalition == 2 then return 80 end -- blue if type(inCoalition) == "string" then inCoalition = inCoalition:lower() if inCoalition == "red" then return 81 end if inCoalition == "blue" then return 80 end end trigger.action.outText("+++dcsC: coalition2county in (" .. inCoalition .. ") converts to UN (82)!", 30) return 82 -- UN end function dcsCommon.coalition2Text(coa) if not coa then return "!nil!" end if coa == 0 then return "NEUTRAL" end if coa == 1 then return "RED" end if coa == 2 then return "BLUE" end return "?UNKNOWN?" end function dcsCommon.latLon2Text(lat, lon) -- inspired by mist, thanks Grimes! -- returns two strings: lat and lon -- determine hemispheres by sign local latHemi, lonHemi if lat > 0 then latHemi = 'N' else latHemi = 'S' end if lon > 0 then lonHemi = 'E' else lonHemi = 'W' end -- remove sign since we have hemi lat = math.abs(lat) lon = math.abs(lon) -- calc deg / mins local latDeg = math.floor(lat) local latMin = (lat - latDeg) * 60 local lonDeg = math.floor(lon) local lonMin = (lon - lonDeg) * 60 -- calc seconds local rawLatMin = latMin latMin = math.floor(latMin) local latSec = (rawLatMin - latMin) * 60 local rawLonMin = lonMin lonMin = math.floor(lonMin) local lonSec = (rawLonMin - lonMin) * 60 -- correct for rounding errors if latSec >= 60 then latSec = latSec - 60 latMin = latMin + 1 end if lonSec >= 60 then lonSec = lonSec - 60 lonMin = lonMin + 1 end -- prepare string output local secFrmtStr = '%06.3f' local lat = string.format('%02d', latDeg) .. '°' .. string.format('%02d', latMin) .. "'" .. string.format(secFrmtStr, latSec) .. '"' .. latHemi local lon = string.format('%02d', lonDeg) .. '°' .. string.format('%02d', lonMin) .. "'" .. string.format(secFrmtStr, lonSec) .. '"' .. lonHemi return lat, lon end -- get mission name. If mission file name without ".miz" function dcsCommon.getMissionName() local mn = net.dostring_in("gui", "return DCS.getMissionName()") return mn end function dcsCommon.numberArrayFromString(inString, default) -- moved from cfxZones if not default then default = 0 end if string.len(inString) < 1 then trigger.action.outText("+++dcsCommon: empty numbers", 30) return {default, } end local flags = {} local rawElements = dcsCommon.splitString(inString, ",") -- go over all elements for idx, anElement in pairs(rawElements) do anElement = dcsCommon.trim(anElement) if dcsCommon.stringStartsWithDigit(anElement) and dcsCommon.containsString(anElement, "-") then -- interpret this as a range local theRange = dcsCommon.splitString(anElement, "-") local lowerBound = theRange[1] lowerBound = tonumber(lowerBound) local upperBound = theRange[2] upperBound = tonumber(upperBound) if lowerBound and upperBound then -- swap if wrong order if lowerBound > upperBound then local temp = upperBound upperBound = lowerBound lowerBound = temp end -- now add add numbers to flags for f=lowerBound, upperBound do table.insert(flags, tostring(f)) end else -- bounds illegal trigger.action.outText("+++dcsCommon: ignored range <" .. anElement .. "> (range)", 30) end else -- single number f = dcsCommon.trim(anElement) f = tonumber(f) if f then table.insert(flags, f) end end end if #flags < 1 then flags = {default, } end return flags end function dcsCommon.flagArrayFromString(inString, verbose) if not verbose then verbose = false end if verbose then trigger.action.outText("+++flagArray: processing <" .. inString .. ">", 30) end if string.len(inString) < 1 then trigger.action.outText("+++flagArray: empty flags", 30) return {} end local flags = {} local rawElements = dcsCommon.splitString(inString, ",") -- go over all elements for idx, anElement in pairs(rawElements) do anElement = dcsCommon.trim(anElement) if dcsCommon.stringStartsWithDigit(anElement) and dcsCommon.containsString(anElement, "-") then -- interpret this as a range local theRange = dcsCommon.splitString(anElement, "-") local lowerBound = theRange[1] lowerBound = tonumber(lowerBound) local upperBound = theRange[2] upperBound = tonumber(upperBound) if lowerBound and upperBound then -- swap if wrong order if lowerBound > upperBound then local temp = upperBound upperBound = lowerBound lowerBound = temp end -- now add add numbers to flags for f=lowerBound, upperBound do table.insert(flags, f) end else -- bounds illegal trigger.action.outText("+++flagArray: ignored range <" .. anElement .. "> (range)", 30) end else -- single number local f = dcsCommon.trim(anElement) -- DML flag upgrade: accept strings tonumber(anElement) if f then table.insert(flags, f) else trigger.action.outText("+++flagArray: ignored element <" .. anElement .. "> (single)", 30) end end end if verbose then trigger.action.outText("+++flagArray: <" .. #flags .. "> flags total", 30) end return flags end function dcsCommon.rangeArrayFromString(inString, verbose) if not verbose then verbose = false end if verbose then trigger.action.outText("+++rangeArray: processing <" .. inString .. ">", 30) end if string.len(inString) < 1 then trigger.action.outText("+++rangeArray: empty ranges", 30) return {} end local ranges = {} local rawElements = dcsCommon.splitString(inString, ",") -- go over all elements for idx, anElement in pairs(rawElements) do anElement = dcsCommon.trim(anElement) local outRange = {} if dcsCommon.stringStartsWithDigit(anElement) and dcsCommon.containsString(anElement, "-") then -- interpret this as a range local theRange = dcsCommon.splitString(anElement, "-") local lowerBound = theRange[1] lowerBound = tonumber(lowerBound) local upperBound = theRange[2] upperBound = tonumber(upperBound) if lowerBound and upperBound then -- swap if wrong order if lowerBound > upperBound then local temp = upperBound upperBound = lowerBound lowerBound = temp end -- now add to ranges outRange[1] = lowerBound outRange[2] = upperBound table.insert(ranges, outRange) if verbose then trigger.action.outText("+++rangeArray: new range <" .. lowerBound .. "> to <" .. upperBound .. ">", 30) end else -- bounds illegal trigger.action.outText("+++rangeArray: ignored range <" .. anElement .. "> (range)", 30) end else -- single number local f = dcsCommon.trim(anElement) f = tonumber(f) if f then outRange[1] = f outRange[2] = f table.insert(ranges, outRange) if verbose then trigger.action.outText("+++rangeArray: new (single-val) range <" .. f .. "> to <" .. f .. ">", 30) end else trigger.action.outText("+++rangeArray: ignored element <" .. anElement .. "> (single)", 30) end end end if verbose then trigger.action.outText("+++rangeArray: <" .. #ranges .. "> ranges total", 30) end return ranges end function dcsCommon.incFlag(flagName) local v = trigger.misc.getUserFlag(flagName) trigger.action.setUserFlag(flagName, v + 1) end function dcsCommon.decFlag(flagName) local v = trigger.misc.getUserFlag(flagName) trigger.action.setUserFlag(flagName, v - 1) end function dcsCommon.objectHandler(theObject, theCollector) table.insert(theCollector, theObject) return true end function dcsCommon.getObjectsForCatAtPointWithRadius(aCat, thePoint, theRadius) if not aCat then aCat = Object.Category.UNIT end local p = {x=thePoint.x, y=thePoint.y, z=thePoint.z} local collector = {} -- now build the search argument local args = { id = world.VolumeType.SPHERE, params = { point = p, radius = theRadius } } -- now call search world.searchObjects(aCat, args, dcsCommon.objectHandler, collector) return collector end function dcsCommon.getSceneryObjectsInZone(theZone) -- DCS ZONE!!! local aCat = 5 -- scenery -- WARNING: WE ARE USING DCS ZONES, NOT CFX!!! local p = {x=theZone.x, y=0, z=theZone.y} local lp = {x = p.x, y = p.z} p.y = land.getHeight(lp) local collector = {} -- now build the search argument local args = { id = world.VolumeType.SPHERE, params = { point = p, radius = theZone.radius } } -- now call search world.searchObjects(aCat, args, dcsCommon.objectHandler, collector) return collector end function dcsCommon.getSceneryObjectInZoneByName(theName, theZone) -- DCS ZONE!!! local allObs = dcsCommon.getSceneryObjectsInZone(theZone) for idx, anObject in pairs(allObs) do if tostring(anObject:getName()) == theName then return anObject end end return nil end -- -- bitwise operators -- function dcsCommon.bitAND32(a, b) if not a then a = 0 end if not b then b = 0 end local z = 0 local e = 1 for i = 0, 31 do local a1 = a % 2 -- 0 or 1 local b1 = b % 2 -- 0 or 1 if a1 == 1 and b1 == 1 then a = a - 1 -- remove bit b = b - 1 z = z + e else if a1 == 1 then a = a - 1 end -- remove bit if b1 == 1 then b = b - 1 end end a = a / 2 -- shift right b = b / 2 e = e * 2 -- raise e by 1 end return z end function dcsCommon.num2bin(a) if not a then a = 0 end local z = "" for i = 0, 31 do local a1 = a % 2 -- 0 or 1 if a1 == 1 then a = a - 1 -- remove bit z = "1"..z else z = "0"..z end a = a / 2 -- shift right end return z end function dcsCommon.LSR(a, num) if not a then a = 0 end if not num then num = 16 end for i = 1, num do local a1 = a % 2 -- 0 or 1 if a1 == 1 then a = a - 1 -- remove bit end a = a / 2 -- shift right end return a end -- -- string wildcards -- function dcsCommon.processStringWildcards(inMsg) -- Replace STATIC bits of message like CR and zone name if not inMsg then return "" end local formerType = type(inMsg) if formerType ~= "string" then inMsg = tostring(inMsg) end if not inMsg then inMsg = "" end local outMsg = "" -- replace line feeds outMsg = inMsg:gsub("", "\n") return outMsg end -- -- phonetic alphabet -- dcsCommon.alphabet = { a = "alpha", b = "bravo", c = "charlie", d = "delta", e = "echo", f = "foxtrot", g = "golf", h = "hotel", i = "india", j = "juliet", k = "kilo", l = "lima", m = "mike", n = "november", o = "oscar", p = "papa", q = "quebec", r = "romeo", s = "sierra", t = "tango", u = "uniform", v = "victor", w = "whiskey", x = "x-ray", y = "yankee", z = "zulu", ["0"] = "zero", ["1"] = "wun", ["2"] = "too", ["3"] = "tree", ["4"] = "fower", ["5"] = "fife" , ["6"] = "six", ["7"] = "seven", ["8"] = "att", ["9"] = "niner", [" "] = "break", } function dcsCommon.letter(inChar) local theChar = "" if type(inChar == "string") then if #inChar < 1 then return "#ERROR0#" end inChar = string.lower(inChar) theChar = string.sub(inChar, 1, 1) elseif type(inChar == "number") then if inChar > 255 then return "#ERROR>#" end if inChar < 0 then return "#ERROR<#" end theChar = char(inChar) else return "#ERRORT#" end -- trigger.action.outText("doing <" .. theChar .. ">", 30) local a = dcsCommon.alphabet[theChar] if a == nil then a = "#ERROR?#" end return a end function dcsCommon.spellString(inString) local res = "" local first = true for i = 1, #inString do local c = inString:sub(i,i) if first then res = dcsCommon.letter(c) first = false else res = res .. " " .. dcsCommon.letter(c) end end return res end dcsCommon.letters = {"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", } function dcsCommon.randomLetter(lowercase) local theLetter = dcsCommon.pickRandom(dcsCommon.letters) if lowercase then theLetter = string.lower(theLetter) end return theLetter end -- -- RGBA from hex -- function dcsCommon.hexString2RGBA(inString) -- enter with "#FF0020" (RGB) or "#FF00AB99" RGBA -- check if it starts with # if not inString then return nil end if #inString ~= 7 and #inString ~=9 then return nil end if inString:sub(1, 1) ~= "#" then return nil end inString = inString:lower() local red = tonumber("0x" .. inString:sub(2,3)) if not red then red = 0 end local green = tonumber("0x" .. inString:sub(4,5)) if not green then green = 0 end local blue = tonumber("0x" .. inString:sub(6,7)) if not blue then blue = 0 end local alpha = 255 if #inString == 9 then alpha = tonumber("0x" .. inString:sub(8,9)) end if not alpha then alpha = 0 end return {red/255, green/255, blue/255, alpha/255} end -- -- Player handling -- function dcsCommon.playerName2Coalition(playerName) if not playerName then return 0 end local factions = {1,2} for idx, theFaction in pairs(factions) do local players = coalition.getPlayers(theFaction) for idy, theUnit in pairs(players) do local upName = theUnit:getPlayerName() if upName == playerName then return theFaction end end end return 0 end function dcsCommon.isPlayerUnit(theUnit) -- new patch. simply check if getPlayerName returns something if not theUnit then return false end if not Unit.isExist(theUnit) then return end if not theUnit.getPlayerName then return false end -- map/static object local pName = theUnit:getPlayerName() if pName then return true end return false end function dcsCommon.getPlayerUnit(name) for coa = 1, 2 do local players = coalition.getPlayers(coa) for idx, theUnit in pairs(players) do if theUnit:getPlayerName() == name then return theUnit end end end return nil end -- -- theater and theater-related stuff -- function dcsCommon.getMapName() return env.mission.theatre end dcsCommon.magDecls = {Caucasus = 6.5, MarianaIslands = 1, Nevada = 12, PersianGulf = 2, Syria = 4, Normandy = -12 -- 1944, -1 in 2016 -- SinaiMap still missing -- Falklands still missing, big differences } function dcsCommon.getMagDeclForPoint(point) -- WARNING! Approximations only, map-wide, not adjusted for year nor location! -- serves as a stub for the day when DCS provides correct info local map = dcsCommon.getMapName() local decl = dcsCommon.magDecls[map] if not decl then trigger.action.outText("+++dcsC: unknown map <" .. map .. ">, using dclenation 0", 30) decl = 0 end return decl end -- -- iterators -- -- iteratePlayers - call callback for all player units -- callback is of signature callback(playerUnit) -- function dcsCommon.iteratePlayers(callBack) local factions = {0, 1, 2} for idx, theFaction in pairs(factions) do local players = coalition.getPlayers(theFaction) for idy, theUnit in pairs(players) do callBack(theUnit) end end end -- -- MISC POINT CREATION -- function dcsCommon.createPoint(x, y, z) local newPoint = {} newPoint.x = x newPoint.y = y newPoint.z = z -- works even if Z == nil return newPoint end function dcsCommon.copyPoint(inPoint) local newPoint = {} newPoint.x = inPoint.x newPoint.y = inPoint.y -- handle xz only if inPoint.z then newPoint.z = inPoint.z else newPoint.z = inPoint.y end return newPoint end -- -- SEMAPHORES -- dcsCommon.semaphores = {} -- replacement for trigger.misc.getUserFlag function dcsCommon.getUserFlag(flagName) if dcsCommon.semaphores[flagName] then return dcsCommon.semaphores[flagName] end return trigger.misc.getUserFlag(flagName) end -- replacement for trigger.action.setUserFlag function dcsCommon.setUserFlag(flagName, theValue) -- not yet connected: semaphores -- forget semaphore content if new value is old-school if type(theValue) == "number" then dcsCommon.semaphores[theValue] = nil --return to old-school end trigger.action.setUserFlag(flagName, theValue) end -- -- -- INIT -- -- -- init any variables, tables etc that the lib requires internally function dcsCommon.init() cbID = 0 -- create ID tables dcsCommon.collectMissionIDs() --dcsCommon.uuIdent = 0 if (dcsCommon.verbose) or true then trigger.action.outText("dcsCommon v" .. dcsCommon.version .. " loaded", 10) end end -- do init. dcsCommon.init()