DML/modules/dcsCommon.lua
Christian Franz 46afa49c3a Version 1.2.0
LINK UNIT support
2022-12-29 12:36:54 +01:00

3032 lines
92 KiB
Lua

dcsCommon = {}
dcsCommon.version = "2.8.0"
--[[-- 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
--]]--
-- 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
dcsCommon.coalitionSides = {0, 1, 2}
-- 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
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
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
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
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.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
-- 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)
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.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 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.rotatePointAroundOrigin(inX, inY, angle) -- angle in degrees
local rads = 3.14152 / 180 -- convert to radiants.
angle = angle * rads -- 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
-- 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
--
--
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.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 "<err in array2string: not an array>" end
local s = ""
local count = 0
for idx, ele in pairs(inArray) do
if count > 0 then s = s .. deli .. " " end
s = s .. ele
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)
if not theString then return false end
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
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 "<!NIL!>" end
local t = "[x="
if p.x then t = t .. p.x .. ", " else t = t .. "<nil>, " end
if p.y then t = t .. "y=" .. p.y .. ", " else t = t .. "y=<nil>, " end
if p.z then t = t .. "z=" .. p.z .. "]" else t = t .. "z=<nil>]" 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 "<nil>" 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)
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",
"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, "<s>", remainS)
msg = string.gsub(msg, "<m>", remainM)
msg = string.gsub(msg, "<h>", 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
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.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.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 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 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.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.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
--
-- 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()