dcsCommon = {} dcsCommon.version = "2.6.5" --[[-- 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 undertsands '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 --]]-- -- dcsCommon is a library of common lua functions -- for easy access and simple mission programming -- (c) 2021, 2022 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 and Gazelle can't carry troops -- 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 -- 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 if theNum >= 50 then return math.random(theNum) 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) local delta = math.huge local allYourBase = dcsCommon.getAirbasesWhoseNameContains("*", filterCat, filterCoalition) -- get em all and filter 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 -- -- 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 dx = B.x - A.x dz = B.z - A.z 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 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.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 --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 local x = x + (innerRadius + r) * math.cos(degrees) local z = z + (innerRadius + r) * math.sin(degrees) local thePoint = {} thePoint.x = x thePoint.y = 0 thePoint.z = z return thePoint, degrees 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 (theUnit:isExist() 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 aCoalition == 1 then return 2 end if aCoalition == 2 then return 1 end if type(aCoalition) == "string" then aCoalition = aCoalition:lower() if aCoalition == "red" then return 2 end if aCoalition == "blue" then return 1 end 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 < 86 do if i ~= 14 then if (coalition.getCountryCoalition(i) == aCoalition) then return i end end i = i + 1 end return nil 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) 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) 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 setmetatable(copy, dcsCommon.clone(getmetatable(orig))) else -- number, string, boolean, etc copy = orig end return copy end function dcsCommon.copyArray(inArray) -- 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.createOverheadAirdromeRoutPintData(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.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 = 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.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 intil mindistance to all is kept or emergencybreak for idx, rUnit in pairs(processedUnits) do -- get min dist to all positioned units thePoint = dcsCommon.randomPointInCircle(radius, innerRadius) -- returns x, 0, z uPoint.x = rUnit.x uPoint.y = 0 uPoint.z = rUnit.y 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.rotatePointAroundOrigin(inX, inY, angle) -- angle in degrees local degrees = 3.14152 / 180 -- ok, it's actually radiants. angle = angle * degrees -- turns into rads 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.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 degreess 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 degreess theUnit.heading = theUnit.heading + rads 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 -- -- -- M I S C M E T H O D S -- -- function dcsCommon.arrayContainsString(theArray, theString) if not theArray then return false end if not theString then return false 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.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) return theString:find(thePrefix) == 1 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) 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 end if theBool then return "yes" end return "no" 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 .. p.y .. ", " else t = t .. ", " end if p.z then t = t .. p.z .. "]" else t = t .. "]" 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 -- 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) 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 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", "pilot dead", "base captured", "mission start", "mission end", -- 12 "took control", "refuel stop", "birth", "human failure", "det. failure", "engine start", "engine stop", "player enter unit", "player leave unit", "player comment", "start shoot", "end shoot", "mark add", "mark changed", "makr removed", "kill", "score", "unit lost", "land after eject", "Paratrooper land", "chair discard after eject", "weapon add", "trigger zone", "landing quality mark", "BDA", "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.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.markPointWithSmoke(p, smokeColor) 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 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 -- -- -- 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 r.z = a.z + b.z 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 r.z = a.z - b.z 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 r.z = a.z * f 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.isTroopCarrier(theUnit) -- return true if conf can carry troups if not theUnit then return false end local uType = theUnit:getTypeName() if dcsCommon.arrayContainsString(dcsCommon.troopCarriers, uType) then -- may add additional tests before returning true return true end return false end function dcsCommon.getUnitAlt(theUnit) if not theUnit then return 0 end if not theUnit:isExist() then return 0 end local p = theUnit:getPoint() return p.y end function dcsCommon.getUnitAGL(theUnit) if not theUnit then return 0 end if not theUnit:isExist() then return 0 end 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 theUnit:isExist() 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.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 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.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 -- -- -- INIT -- -- -- init any variables the lib requires internally function dcsCommon.init() cbID = 0 --dcsCommon.uuIdent = 0 if (dcsCommon.verbose) or true then trigger.action.outText("dcsCommon v" .. dcsCommon.version .. " loaded", 10) end end -- do init. dcsCommon.init() --[[-- to do: - formation 2Column - formation 3Column -]]--