mirror of
https://github.com/weyne85/DML.git
synced 2025-10-29 16:57:49 +00:00
2717 lines
82 KiB
Lua
2717 lines
82 KiB
Lua
dcsCommon = {}
|
|
dcsCommon.version = "2.7.2"
|
|
--[[-- VERSION HISTORY
|
|
2.2.6 - compassPositionOfARelativeToB
|
|
- clockPositionOfARelativeToB
|
|
2.2.7 - isTroopCarrier
|
|
- distFlat
|
|
2.2.8 - fixed event2text
|
|
2.2.9 - getUnitAGL
|
|
- getUnitAlt
|
|
- getUnitSpeed
|
|
- getUnitHeading
|
|
- getUnitHeadingDegrees
|
|
- mag
|
|
- clockPositionOfARelativeToB with own heading
|
|
2.3.0 - unitIsInfantry
|
|
2.3.1 - bool2YesNo
|
|
- bool2Text
|
|
2.3.2 - getGroupAvgSpeed
|
|
- getGroupMaxSpeed
|
|
2.3.3 - getSizeOfTable
|
|
2.3.4 - isSceneryObject
|
|
coalition2county
|
|
2.3.5 - smallRandom
|
|
pickRandom uses smallRandom
|
|
airfield handling, parking
|
|
flight waypoint handling
|
|
landing waypoint creation
|
|
take-off waypoint creation
|
|
2.3.6 - createOverheadAirdromeRoutPintData(aerodrome)
|
|
2.3.7 - coalition2county - warning when creating UN
|
|
2.3.8 - improved headingOfBInDegrees, new getClockDirection
|
|
2.3.9 - getClosingVelocity
|
|
- dot product
|
|
- magSquare
|
|
- vMag
|
|
2.4.0 - libCheck
|
|
2.4.1 - grid/square/rect formation
|
|
- arrangeGroupInNColumns formation
|
|
- 2Columns formation deep and wide formation
|
|
2.4.2 - getAirbasesInRangeOfPoint
|
|
2.4.3 - lerp
|
|
2.4.4 - getClosestAirbaseTo
|
|
- fixed bug in containsString when strings equal
|
|
2.4.5 - added cargo and mass options to createStaticObjectData
|
|
2.4.6 - fixed randompercent
|
|
2.4.7 - smokeColor2Num(smokeColor)
|
|
2.4.8 - linkStaticDataToUnit()
|
|
2.4.9 - trim functions
|
|
- createGroundUnitData uses trim function to remove leading/trailing blanks
|
|
so now we can use blanks after comma to separate types
|
|
- dcsCommon.trimArray(
|
|
- createStaticObjectData uses trim for type
|
|
- getEnemyCoalitionFor understands strings, still returns number
|
|
- coalition2county also understands 'red' and 'blue'
|
|
2.5.0 - "Line" formation with one unit places unit at center
|
|
2.5.1 - vNorm(a)
|
|
2.5.1 - added SA-18 Igla manpad to unitIsInfantry()
|
|
2.5.2 - added copyArray method
|
|
- corrected heading in createStaticObjectData
|
|
2.5.3 - corrected rotateGroupData bug for cz
|
|
- removed forced error in failed pickRandom
|
|
2.5.4 - rotateUnitData()
|
|
- randomBetween()
|
|
2.5.5 - stringStartsWithDigit()
|
|
- stringStartsWithLetter()
|
|
- stringIsPositiveNumber()
|
|
2.5.6 - corrected stringEndsWith() bug with str
|
|
2.5.7 - point2text(p)
|
|
2.5.8 - string2GroupCat()
|
|
2.5.9 - string2ObjectCat()
|
|
2.6.0 - unified uuid, removed uuIdent
|
|
2.6.1 - removed bug in rotateUnitData: cy --> cz param passing
|
|
2.6.2 - new combineTables()
|
|
2.6.3 - new tacan2freq()
|
|
2.6.4 - new processHMS()
|
|
2.6.5 - new bearing2compass()
|
|
- new bearingdegrees2compass()
|
|
- new latLon2Text() - based on mist
|
|
2.6.6 - new nowString()
|
|
- new str2num()
|
|
- new stringRemainsStartingWith()
|
|
- new stripLF()
|
|
- new removeBlanks()
|
|
2.6.7 - new menu2text()
|
|
2.6.8 - new getMissionName()
|
|
- new flagArrayFromString()
|
|
2.6.9 - new getSceneryObjectsInZone()
|
|
- new getSceneryObjectInZoneByName()
|
|
2.7.0 - new synchGroupData()
|
|
clone, topClone and copyArray now all nil-trap
|
|
2.7.1 - new isPlayerUnit() -- moved from cfxPlayer
|
|
new getAllExistingPlayerUnitsRaw - from cfxPlayer
|
|
new typeIsInfantry()
|
|
2.7.2 - new rangeArrayFromString()
|
|
fixed leading blank bug in flagArrayFromString
|
|
new incFlag()
|
|
new decFlag()
|
|
nil trap in stringStartsWith()
|
|
new getClosestFreeSlotForCatInAirbaseTo()
|
|
|
|
--]]--
|
|
|
|
-- 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}
|
|
|
|
-- 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
|
|
|
|
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
|
|
|
|
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)
|
|
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)
|
|
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
|
|
setmetatable(copy, dcsCommon.clone(getmetatable(orig)))
|
|
else -- number, string, boolean, etc
|
|
copy = orig
|
|
end
|
|
return copy
|
|
end
|
|
|
|
function dcsCommon.copyArray(inArray)
|
|
if not inArray then return nil end
|
|
|
|
-- warning: this is a ref copy!
|
|
local theCopy = {}
|
|
for idx, element in pairs(inArray) do
|
|
table.insert(theCopy, element)
|
|
end
|
|
return theCopy
|
|
end
|
|
--
|
|
--
|
|
-- S P A W N I N G
|
|
--
|
|
--
|
|
|
|
function dcsCommon.createEmptyGroundGroupData (name)
|
|
local theGroup = {} -- empty group
|
|
theGroup.visible = false
|
|
theGroup.taskSelected = true
|
|
-- theGroup.route = {}
|
|
-- theGroup.groupId = id
|
|
theGroup.tasks = {}
|
|
-- theGroup.hidden = false -- hidden on f10?
|
|
|
|
theGroup.units = { } -- insert units here! -- use addUnitToGroupData
|
|
|
|
theGroup.x = 0
|
|
theGroup.y = 0
|
|
theGroup.name = name
|
|
-- theGroup.start_time = 0
|
|
theGroup.task = "Ground Nothing"
|
|
|
|
return theGroup
|
|
end;
|
|
|
|
function dcsCommon.createEmptyAircraftGroupData (name)
|
|
local theGroup = dcsCommon.createEmptyGroundGroupData(name)--{} -- empty group
|
|
|
|
theGroup.task = "Nothing" -- can be others, like Transport, CAS, etc
|
|
-- returns with empty route
|
|
theGroup.route = dcsCommon.createEmptyAircraftRouteData() -- we can add points here
|
|
return theGroup
|
|
end;
|
|
|
|
function dcsCommon.createAircraftRoutePointData(x, z, altitudeInFeet, knots, altType, action)
|
|
local rp = {}
|
|
rp.x = x
|
|
rp.y = z
|
|
rp.action = "Turning Point"
|
|
rp.type = "Turning Point"
|
|
if action then rp.action = action; rp.type = action end -- warning: may not be correct, need to verify later
|
|
rp.alt = altitudeInFeet * 0.3048
|
|
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
|
|
|
|
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.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 end
|
|
if theBool then return "yes" end
|
|
return "no"
|
|
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 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 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 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.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.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
|
|
|
|
--
|
|
--
|
|
-- 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
|
|
|
|
-]]--
|