Initial commit

This commit is contained in:
Ambroise Garel
2025-07-22 10:22:50 +02:00
parent 16f53c8a47
commit edb28205cd
422 changed files with 17001 additions and 2 deletions

View File

@@ -0,0 +1,146 @@
-- ====================================================================================
-- (DCS LUA ADD-ON) CONVERTER - UNITS CONVERSION FUNCTIONS
--
-- DCSEx.converter.celsiusToFahrenheit(t)
-- DCSEx.converter.degreesToRadians(degrees)
-- DCSEx.converter.fahrenheitToCelsius(fahrenheit)
-- DCSEx.converter.feetToMeters(feet)
-- DCSEx.converter.kelvinToCelsius(t)
-- DCSEx.converter.kelvinToFahrenheit(t)
-- DCSEx.converter.kmphToMps(kmph)
-- DCSEx.converter.knotsToMps(knots)
-- DCSEx.converter.metersToFeet(meters)
-- DCSEx.converter.metersToNM(meters)
-- DCSEx.converter.mpsToKmph(mps)
-- DCSEx.converter.mpsToKnots(mps)
-- DCSEx.converter.nmToMeters(nm)
-- DCSEx.converter.radiansToDegrees(radians)
-- ====================================================================================
DCSEx.converter = {}
-------------------------------------
-- Converts Celsius degrees to Fahrenheit
-- @param t Temperature in Celsius degrees
-- @return Temperature in Fahrenheit degrees
-------------------------------------
function DCSEx.converter.celsiusToFahrenheit(t)
return t * (9 / 5) + 32
end
-------------------------------------
-- Converts angle in degrees to radians.
-- @param degrees Angle in degrees
-- @return Angle in radians
-------------------------------------
function DCSEx.converter.degreesToRadians(degrees)
return degrees * math.pi / 180
end
-------------------------------------
-- Converts Fahrenheit degrees to Celsius
-- @param fahrenheit Temperature in Fahrenheit degrees
-- @return Temperature in Celsius degrees
-------------------------------------
function DCSEx.converter.fahrenheitToCelsius(fahrenheit)
return (fahrenheit - 32) * (5 / 9)
end
-------------------------------------
-- Converts feet to meters.
-- @param feet Distance in feet
-- @return Distance in meters
-------------------------------------
function DCSEx.converter.feetToMeters(feet)
return feet * 0.3048
end
-------------------------------------
-- Converts Kelvin degrees to Celsius
-- @param kelvin Temperature in Kelvin degrees
-- @return Temperature in Celsius degrees
-------------------------------------
function DCSEx.converter.kelvinToCelsius(t)
return t - 273.15
end
-------------------------------------
-- Converts Kelvin degrees to Fahrenheit
-- @param kelvin Temperature in Kelvin degrees
-- @return Temperature in Fahrenheit degrees
-------------------------------------
function DCSEx.converter.kelvinToFahrenheit(t)
return DCSEx.converter.celsiusToFahrenheit(DCSEx.converter.kelvinToCelsius(t))
end
-------------------------------------
-- Converts kilometers per hour to meters per second.
-- @param kmph speed in km/h
-- @return speed in m/s
-------------------------------------
function DCSEx.converter.kmphToMps(kmph)
return kmph / 3.6
end
-------------------------------------
-- Converts knots to meters per second.
-- @param knots speed in knots
-- @return speed in m/s
-------------------------------------
function DCSEx.converter.knotsToMps(knots)
return knots * 1852 / 3600
end
-------------------------------------
-- Converts meters to feet.
-- @param meters distance in meters
-- @return distance in feet
-------------------------------------
function DCSEx.converter.metersToFeet(meters)
return meters / 0.3048
end
-------------------------------------
-- Converts meters to nautical miles.
-- @param meters distance in meters
-- @return distance in nautical miles
-------------------------------------
function DCSEx.converter.metersToNM(meters)
return meters / 1852
end
-------------------------------------
-- Converts meters per second to kilometers per hour.
-- @param mps speed in m/s
-- @return speed in km/h
-------------------------------------
function DCSEx.converter.mpsToKmph(mps)
return mps * 3.6
end
-------------------------------------
-- Converts meters per second to knots.
-- @param mps speed in m/s
-- @return speed in knots
-------------------------------------
function DCSEx.converter.mpsToKnots(mps)
return mps * 3600 / 1852
end
-------------------------------------
-- Converts nautical miles to meters.
-- @param nm distance in nautical miles
-- @return distance in meters
-------------------------------------
function DCSEx.converter.nmToMeters(nm)
return nm * 1852
end
-------------------------------------
-- Converts angle in radians to degrees.
-- @param degrees Angle in radians
-- @return Angle in degrees
-------------------------------------
function DCSEx.converter.radiansToDegrees(radians)
return radians * 180 / math.pi
end

View File

@@ -0,0 +1,374 @@
-- ====================================================================================
-- DCSTOOLS - FUNCTIONS LINKED TO DCS WORLD RULES AND TABLES
-- ====================================================================================
-- DCSEx.dcs.getBRAA(point, refPoint, showAltitude, metricSystem)
-- DCSEx.dcs.getCJTFForCoalition(coalitionID)
-- DCSEx.dcs.getCoalitionAsString(coalitionID)
-- DCSEx.dcs.getCoalitionColor(coalitionID, alpha)
-- DCSEx.dcs.getGroupCenterPoint(group)
-- DCSEx.dcs.getGroupIDAsNumber(group)
-- DCSEx.dcs.getNearestObject(refPoint, objectTable)
-- DCSEx.dcs.getNearestObjects(refPoint, objectTable, maxCount)
-- DCSEx.dcs.getNearestPoints(refPoint, pointsTable, maxCount)
-- DCSEx.dcs.getOppositeCoalition(coalitionID)
-- DCSEx.dcs.getPlayerUnitsInGroup(group)
-- DCSEx.dcs.getPlayerUnitsInGroupByID(groupID)
-- DCSEx.dcs.getRadioModulationName(modulationID)
-- DCSEx.dcs.getObjectIDAsNumber(obj)
-- DCSEx.dcs.getUnitTypeFromFamily(unitFamily)
-- DCSEx.dcs.getUnitFamilyForDecade(unitFamily, decade) -- TODO: remove?
-- ====================================================================================
DCSEx.dcs = { }
-- TODO: add description and update file header
function DCSEx.dcs.doNothing()
end
-------------------------------------
-- Gets a BRAA (bearing, range, altitude, aspect) string about a point
-- Format is "[bearing to unit] for [distance] at [altitude]"
-- @param unit A unit
-- @param refPoint2 Reference point for the bearing and distance
-- @param showAltitude Should altitude be displayed?
-- @param metricSystem Should metric system units be used?
-- @return BRAA, as a string
-------------------------------------
function DCSEx.dcs.getBRAA(point, refPoint, showAltitude, metricSystem)
showAltitude = showAltitude or false
metricSystem = metricSystem or false
if not point.z then point = DCSEx.math.vec2ToVec3(point, "land") end
if not refPoint.z then refPoint = DCSEx.math.vec2ToVec3(refPoint, "land") end
local braa = tostring(DCSEx.math.getBearing(point, refPoint))
local distance = DCSEx.math.getDistance2D(point, refPoint)
if metricSystem then
distance = distance / 1000.0
else
distance = DCSEx.converter.metersToNM(distance)
end
braa = braa .. " for " .. tostring(math.ceil(distance))
if showAltitude then
local altitude = point.y
if metricSystem then
altitude = math.floor(point.y / 100) * 100
else
altitude = math.floor(DCSEx.converter.metersToFeet(point.y) / 1000) * 1000
end
braa = braa .. " at " .. tostring(altitude)
end
return braa
end
-------------------------------------
-- Returns the CJTF country for a given coalition
-- @param A coalition ID
-- @return A country ID (country.id.CJTF_BLUE or country.id.CJTF_RED)
-------------------------------------
function DCSEx.dcs.getCJTFForCoalition(coalitionID)
if coalitionID == coalition.side.RED then return country.id.CJTF_RED end
return country.id.CJTF_BLUE
end
-------------------------------------
-- Returns the name of a coalition, as a string ("blue", "red" or "neutral")
-- @param A coalition ID
-- @return A string
-------------------------------------
function DCSEx.dcs.getCoalitionAsString(coalitionID)
if coalitionID == coalition.side.NEUTRAL then return "neutral" end
if coalitionID == coalition.side.RED then return "red" end
if coalitionID == coalition.side.BLUE then return "blue" end
return nil
end
-------------------------------------
-- Returns the RGBA color table for the given coalition, accoding to NATO symbology colors
-- @param coalitionID A coalition side
-- @param alpha (optional) Alpha. Default is 1
-- @return A RGBA color table
-------------------------------------
function DCSEx.dcs.getCoalitionColor(coalitionID, alpha)
alpha = alpha or 1.0
if coalitionID == coalition.side.RED then return {0.97, 0.52, 0.51, alpha}
elseif coalitionID == coalition.side.BLUE then return {0.52, 0.87, 0.99, alpha}
else
return {0.5, 0.5, 0.5, alpha}
end
end
-- TODO: description
function DCSEx.dcs.getFirstUnitCallsign(group)
if not group then return nil end
local units = group:getUnits()
if not units then return nil end
local unit0 = units[1]
if not unit0 then return nil end
return unit0:getCallsign()
end
-------------------------------------
-- Returns a vec3 point at the center of all units of a group
-- @param group A group object
-- @return A vec3
-------------------------------------
function DCSEx.dcs.getGroupCenterPoint(group)
if not group then return nil end
local units = group:getUnits()
if not units or #units == 0 then return nil end
local centerPoint = { x = 0, y = 0, z = 0}
for _,u in pairs(units) do
local uPoint = u:getPoint()
centerPoint.x = centerPoint.x + uPoint.x
centerPoint.y = centerPoint.y + uPoint.y
centerPoint.z = centerPoint.z + uPoint.z
end
centerPoint.x = centerPoint.x / #units
centerPoint.y = centerPoint.y / #units
centerPoint.z = centerPoint.z / #units
return centerPoint
end
-------------------------------------
-- Returns the ID of a group as a number (here to fix a bug where sometimes ID is returned as a string)
-- @param group A group table
-- @return An ID (as an number) or nil if group is nil or has no ID
-------------------------------------
function DCSEx.dcs.getGroupIDAsNumber(group)
if not group then
return nil
end
return tonumber(group:getID())
end
-------------------------------------
-- Returns the object nearest (in a 2D plane) from a given point
-- @param refPoint A reference point, as a vec2 or vec3
-- @param objectTable A table of DCS objects
-- @return The object nearest from the point, or nil if no object was found
-------------------------------------
function DCSEx.dcs.getNearestObject(refPoint, objectTable)
local nearestObjects = DCSEx.dcs.getNearestObjects(refPoint, objectTable, 1)
if #nearestObjects == 0 then return nil end
return nearestObjects[1]
end
-------------------------------------
-- Returns the nearest objects (in a 2D plane) from a given point
-- @param refPoint A reference point, as a vec2 or vec3
-- @param objectTable A table of DCS objects
-- @param maxCount (optional) Maximum number of objects to return
-- @return A table of objects, sorted by distance
-------------------------------------
function DCSEx.dcs.getNearestObjects(refPoint, objectTable, maxCount)
if not refPoint then return DCSEx.table.deepCopy(objectTable) end
refPoint = DCSEx.math.vec3ToVec2(refPoint)
local sortedObjects = {}
local sortedKeys = {}
for i,o in pairs(objectTable) do
local distance = DCSEx.math.getDistance2D(refPoint, o:getPoint())
sortedObjects[distance] = i
table.insert(sortedKeys, distance)
end
table.sort(sortedKeys)
local sortedList = {}
for _,v in ipairs(sortedKeys) do
if maxCount and #sortedList >= maxCount then break end
table.insert(sortedList, objectTable[sortedObjects[v]])
end
return sortedList
end
-------------------------------------
-- Returns the nearest points (in a 2D plane) from a given point
-- @param refPoint A reference point, as a vec2 or vec3
-- @param objectTable A table of points (vec2 or vec3)
-- @param maxCount (optional) Maximum number of points to return
-- @return A table of points, sorted by distance
-------------------------------------
function DCSEx.dcs.getNearestPoints(refPoint, pointsTable, maxCount)
if not refPoint then return DCSEx.table.deepCopy(pointsTable) end
refPoint = DCSEx.math.vec3ToVec2(refPoint)
local sortedPoints = {}
local sortedKeys = {}
for i,pt in pairs(pointsTable) do
local distance = DCSEx.math.getDistance2D(refPoint, pt)
sortedPoints[distance] = i
table.insert(sortedKeys, distance)
end
table.sort(sortedKeys)
local sortedList = {}
for _,v in ipairs(sortedKeys) do
if maxCount and #sortedList >= maxCount then break end
table.insert(sortedList, pointsTable[sortedPoints[v]])
end
return sortedList
end
-------------------------------------
-- Returns the coalition opposed to the provided coalition (coalition.side.NEUTRAL still returns NEUTRAL)
-- @param group A coalition
-- @return Another coalition
-------------------------------------
function DCSEx.dcs.getOppositeCoalition(coalitionID)
if coalitionID == coalition.side.RED then return coalition.side.BLUE end
if coalitionID == coalition.side.BLUE then return coalition.side.RED end
return coalition.side.NEUTRAL
end
-------------------------------------
-- Returns all player-controlled units in a group
-- @param group A group object
-- @return A table of unit objects
-------------------------------------
function DCSEx.dcs.getPlayerUnitsInGroup(group)
if not group then return nil end
local units = group:getUnits()
if not units then return { } end
local players = {}
for _,u in pairs(units) do
if u:getPlayerName() then
table.insert(players, u)
end
end
return players
end
-------------------------------------
-- Returns all player-controlled units in the group with the given ID
-- @param groupID A group ID
-- @return A table of unit objects
-------------------------------------
function DCSEx.dcs.getPlayerUnitsInGroupByID(groupID)
return DCSEx.dcs.getPlayerUnitsInGroup(DCSEx.world.getGroupByID(groupID))
end
-------------------------------------
-- Returns a radio modulation type as a string
-- @param modulationID A modulation ID (from radio.modulation enum)
-- @return A string
-------------------------------------
function DCSEx.dcs.getRadioModulationName(modulationID)
if modulationID == radio.modulation.FM then return "FM" end
return "AM"
end
-------------------------------------
-- Returns a remplacement unit family for given family if it's not available in this decade (e.g. SAMs in the 1940s). Else returns the original family.
-- @param unitFamily An unit family
-- @param decade (optional) A decade, or the current decade from env.mission.date.Year
-- @return An unit family
-------------------------------------
function DCSEx.dcs.getUnitFamilyForDecade(unitFamily, decade)
-- TODO
-- decade = decade or envMission.getDecade()
-- if decade < 1990 then
-- if unitFamily == DCSEx.enums.unitFamily.UAVs then
-- unitFamily = DCSEx.enums.unitFamily.AttackHelicopters
-- end
-- end
-- if decade < 1970 then
-- if unitFamily == DCSEx.enums.unitFamily.AWACS then
-- unitFamily = DCSEx.enums.unitFamily.Transports
-- elseif unitFamily == DCSEx.enums.unitFamily.SAMShort then
-- unitFamily = DCSEx.enums.unitFamily.MobileAAA
-- elseif unitFamily == DCSEx.enums.unitFamily.SAMShortIR then
-- unitFamily = DCSEx.enums.unitFamily.MobileAAA
-- end
-- end
-- if decade < 1960 then
-- if unitFamily == DCSEx.enums.unitFamily.AttackHelicopters then
-- unitFamily = DCSEx.enums.unitFamily.Fighters
-- elseif unitFamily == DCSEx.enums.unitFamily.MANPADS then
-- unitFamily = DCSEx.enums.unitFamily.Infantry
-- elseif unitFamily == DCSEx.enums.unitFamily.SAMLong then
-- unitFamily = DCSEx.enums.unitFamily.StaticAAA
-- elseif unitFamily == DCSEx.enums.unitFamily.SAMMedium then
-- unitFamily = DCSEx.enums.unitFamily.StaticAAA
-- elseif unitFamily == DCSEx.enums.unitFamily.SSMissiles then
-- unitFamily = DCSEx.enums.unitFamily.Artillery
-- elseif unitFamily == DCSEx.enums.unitFamily.TransportHelicopters then
-- unitFamily = DCSEx.enums.unitFamily.Transports
-- end
-- end
-- if decade < 1950 then
-- if unitFamily == DCSEx.enums.unitFamily.MobileAAA then
-- unitFamily = DCSEx.enums.unitFamily.APC
-- elseif unitFamily == DCSEx.enums.unitFamily.Tankers then
-- unitFamily = DCSEx.enums.unitFamily.Transports
-- end
-- end
return unitFamily
end
-- TODO: description
function DCSEx.dcs.getUnitTypeFromFamily(unitFamily)
return math.floor(unitFamily / 100)
end
-------------------------------------
-- Returns the ID of an object as a number (here to fix a bug where sometimes ID is returned as a string)
-- @param obj An object (unit, static object...)
-- @return An ID (as an number) or nil if unit is nil or has no ID
-------------------------------------
function DCSEx.dcs.getObjectIDAsNumber(obj)
if not obj then return nil end
return tonumber(obj:getID())
end
-- TODO: description & file header
function DCSEx.dcs.loadMission(fileName)
net.dostring_in("mission", string.format("a_load_mission(\"%s\")", fileName))
end
-- TODO: description & file header
-- function DCSEx.dcs.isMultiplayer()
-- if #net.get_player_list() > 0 then return true end
-- if dcs and dcs.isServer() == true then return true end
-- return false
-- end
-- TODO: a_end_mission
-- TODO: description & file header
function DCSEx.dcs.outPicture(fileName, durationSeconds, clearView, startDelay, horizontalAlign, verticalAlign, size, sizeUnits)
clearView = clearView or false
startDelay = startDelay or 0
horizontalAlign = horizontalAlign or 1
verticalAlign = verticalAlign or 1
size = size or 100
sizeUnits = sizeUnits or 0
if clearView then clearView = "true" else clearView = "false" end
net.dostring_in("mission", string.format("a_out_picture(getValueResourceByKey(\"%s\"), %d, %s, %d, \"%d\", \"%d\", %d, \"%d\")", fileName, durationSeconds, clearView, startDelay, horizontalAlign, verticalAlign, size, sizeUnits))
end

View File

@@ -0,0 +1,123 @@
DCSEx.enums = {}
-------------------------------------
-- Line types for map markers. The enum is missing from DCS
-------------------------------------
DCSEx.enums.lineType = {
NO_LINE = 0,
SOLID = 1,
DASHED = 2,
DOTTED = 3,
DOT_DASH = 4,
LONG_DASH = 5,
TWO_DASH = 6,
}
-------------------------------------
-- Event to check to see if a task/objective is complete
-------------------------------------
DCSEx.enums.spawnPointType = {
LAND_LARGE = 1,
LAND_MEDIUM = 2,
LAND_SMALL = 3,
SEA = 4,
}
-------------------------------------
-- Event to check to see if a task/objective is complete
-------------------------------------
DCSEx.enums.taskEvent = {
DESTROY = 1,
DESTROY_SCENERY = 2,
LAND = 3,
}
-------------------------------------
-- Families of mission tasks
-------------------------------------
DCSEx.enums.taskFamily = {
ANTISHIP = 1,
-- CAP = 2, -- TODO
-- CAS = 3, -- TODO
GROUND_ATTACK = 2, -- 4
-- HELICOPTER = XXX, -- 5
-- HELO_HUNT = XXX, -- 6
INTERCEPTION = 3, -- 7
-- OCA = XXX, -- 8
SEAD = 4, --9
STRIKE = 5, -- 10
}
-------------------------------------
-- Special events for tasks
-------------------------------------
DCSEx.enums.taskFlag = {
ALLOW_JTAC = 1,
DESTROY_TRACK_RADARS_ONLY = 2,
MOVING = 3,
ON_ROADS = 4,
SCENERY_TARGET = 5
}
-------------------------------------
-- Enumerates the various time periods during which DCS World missions can take place
-------------------------------------
DCSEx.enums.timePeriod = {
WORLD_WAR_2 = 1,
KOREA_WAR = 2,
VIETNAM_WAR = 3,
COLD_WAR = 4,
MODERN = 5
}
-------------------------------------
-- Type of unit families. Hundreds digit must match the value in the Unit.Category enum
-------------------------------------
DCSEx.enums.unitFamily = {
AIRDEFENSE_MANPADS = 251,
AIRDEFENSE_AAA_MOBILE = 252,
AIRDEFENSE_AAA_STATIC = 253,
AIRDEFENSE_SAM_LONG = 254,
AIRDEFENSE_SAM_MEDIUM = 255,
AIRDEFENSE_SAM_SHORT = 256,
AIRDEFENSE_SAM_SHORT_IR = 257,
PLANE_ATTACK = 001,
PLANE_AWACS = 002,
PLANE_BOMBER = 003,
PLANE_FIGHTER = 004,
PLANE_INTERCEPTOR = 005,
PLANE_TANKER = 006,
PLANE_TRANSPORT = 007,
PLANE_UAV = 008,
GROUND_APC = 201,
GROUND_ARTILLERY = 202,
GROUND_INFANTRY = 203,
GROUND_MBT = 204,
GROUND_SS_MISSILE = 205,
GROUND_UNARMED = 206,
HELICOPTER_ATTACK = 101,
HELICOPTER_TRANSPORT = 102,
SHIP_CARGO = 301,
SHIP_CARRIER = 302,
SHIP_CRUISER = 303,
SHIP_FRIGATE = 304,
SHIP_LIGHT = 305,
SHIP_MISSILE_BOAT = 306,
SHIP_SUBMARINE = 307,
STATIC_SCENERY = 401,
STATIC_STRUCTURE = 402
}
DCSEx.enums.victoryCondition = {
DESTROY = 1,
DESTROY_NO_AIR_DEFENSE = 2,
DESTROY_SCENERY = 3,
DESTROY_TRACK_RADARS_ONLY = 4, -- for SEAD tasks
LAND_NEAR = 5,
}

View File

@@ -0,0 +1,131 @@
-- ====================================================================================
-- (DCS LUA ADD-ON) ENVMISSION - FUNCTIONS RELATED TO THE ENV.MISSION TABLE
--
-- DCSEx.envMission.getDecade(yearOffset)
-- DCSEx.envMission.getDistanceToNearestPlayerSpawnPoint(point)
-- DCSEx.envMission.getGroup(groupID)
-- DCSEx.envMission.getGroups(sideID)
-- DCSEx.envMission.getPlayerGroups(coalitionId)
-- ====================================================================================
DCSEx.envMission = {}
-------------------------------------
-- Returns the decade during which the mission takes place (1940 to 2010)
-- @param yearOffset An offset to apply to the actual year
-- @return The decade, as a number
-------------------------------------
function DCSEx.envMission.getDecade(yearOffset)
return DCSEx.math.clamp(math.floor((env.mission.date.Year + (yearOffset or 0)) / 10) * 10, 1940, 2010)
end
-------------------------------------
-- Returns the distance to the nearest player spawn point
-- @param coalition Coalition the players belong to
-- @param point A vec3 or vec2
-- @return The distance, in meters, to the nearest player spawn point, or nil if no player spawn points are present
-------------------------------------
function DCSEx.envMission.getDistanceToNearestPlayerSpawnPoint(coalition, point)
point = DCSEx.math.vec3ToVec2(point)
local playerSlots = DCSEx.envMission.getPlayerGroups(coalition)
if #playerSlots == 0 then return nil end
local nearestPlayer = 999999999999999999999
for _,p in pairs(playerSlots) do
local dist = DCSEx.math.getDistance2D(point, p)
if dist < nearestPlayer then nearestPlayer = dist end
end
return nearestPlayer
end
-------------------------------------
-- Gets information about a group
-- @param groupID Group ID
-- @return Missiondata group table or nil if ID doesn't exist
-------------------------------------
function DCSEx.envMission.getGroup(groupID)
local allGroups = DCSEx.envMission.getGroups()
for _, g in ipairs(allGroups) do
if g.groupId == groupID then
return g
end
end
return nil
end
-------------------------------------
-- Gets all unit groups
-- @param sideID Coalition ID (coalition.side.*), or nil to return unit groups from all coalitions
-- @return Table of missiondata group tables
-------------------------------------
function DCSEx.envMission.getGroups(sideID)
-- Group data are located in coalition[COAL_ID_AS_STRING].country[N][UNIT_CATEGORY_AS_STRING].group[]
-- (e.g. coalition["blue"].country[1]["plane"].group[1])
local validCoalitions = { [0] = "neutrals", [1] = "red", [2] = "blue"} -- by default, search all coalitions
if sideID == coalition.side.BLUE then
validCoalitions = { [2] = "blue"}
elseif sideID == coalition.side.RED then
validCoalitions = { [1] = "red" }
elseif sideID == coalition.side.NEUTRAL then
validCoalitions = { [0] = "neutrals" }
end
local groups = {}
for coalID, coalName in pairs(validCoalitions) do -- In each coalition...
if env.mission.coalition[coalName] then -- (if coalition exists)
for _, countryData in pairs(env.mission.coalition[coalName].country) do -- search each country
for _, uType in pairs({"helicopter", "plane", "ship", "static", "vehicle"}) do -- for each unit category
if countryData[uType] then -- (if unit category exists)
for _,g in pairs(countryData[uType].group) do -- for each group
local groupInfo = DCSEx.table.deepCopy(g)
groupInfo.coalition = coalID -- store coalition side for later reference
table.insert(groups, groupInfo)
end
end
end
end
end
end
return groups
end
-------------------------------------
-- Gets all player groups
-- @param coalitionId Coalition ID (coalition.side.*), or nil to return unit groups from all coalitions
-- @return Table of missiondata group tables
-------------------------------------
function DCSEx.envMission.getPlayerGroups(coalitionId)
local allGroups = DCSEx.envMission.getGroups(coalitionId)
local playerGroups = {}
for _, g in ipairs(allGroups) do
local isPlayerGroup = false
for _, u in pairs(g.units) do
if u.skill == "Player" or u.skill == "Client" then
isPlayerGroup = true
end
end
if isPlayerGroup then
table.insert(playerGroups, DCSEx.table.deepCopy(g))
end
end
return playerGroups
end
-- TODO: description & file header
function DCSEx.envMission.setBriefing(side, text, picture)
text = text or ""
text = text:gsub("\n", "\\n")
picture = picture or ""
net.dostring_in("mission", string.format("a_set_briefing(\"%s\", getValueResourceByKey(\"%s\"), \"%s\")", DCSEx.dcs.getCoalitionAsString(side):lower(), picture, text))
end

View File

@@ -0,0 +1,106 @@
-- ====================================================================================
-- DCSEx.IO - HANDLES READING/WRITING FILES
-- ====================================================================================
-- DCSEx.io.canReadAndWrite()
-- DCSEx.io.load(fileName)
-- DCSEx.io.save(fileName, values)
-- ====================================================================================
DCSEx.io = {}
do
-------------------------------------
-- Returns true if the IO table has been unsanitized (allowing IO operations)
-- and false if it hasn't been
--
-- @return A boolean
-------------------------------------
function DCSEx.io.canReadAndWrite()
return io ~= nil
end
-------------------------------------
-- Loads a table from a text file
--
-- @param fileName Name of the file to read
-- @param obfuscate Should the file contents be obfuscated?
-- @return A table, or nil if something went wrong
-------------------------------------
-- function DCSEx.io.load(fileName, obfuscate)
-- obfuscate = obfuscate or false -- TODO: obfuscation
-- -- IO table is sanitized, cannot read/write to disk
-- if not DCSEx.io.canReadAndWrite() then return nil end
-- local saveFile = io.open(fileName, "r")
-- if not saveFile then return nil end
-- local values = {}
-- local rawText = saveFile:read("*all")
-- for k, v in string.gmatch(rawText, "(%w+)=(%w+)") do
-- local numval = tonumber(v)
-- if numval then
-- values[k] = tonumber(v)
-- else
-- values[k] = v
-- -- trigger.action.outText("GET value \""..k.."\" AT \""..tostring(v).."\"", 1)
-- end
-- end
-- saveFile:close()
-- return values
-- end
function DCSEx.io.load(fileName)
-- IO table is sanitized, cannot read/write to disk
if not DCSEx.io.canReadAndWrite() then return nil end
local saveFile = io.open(fileName, "r")
if not saveFile then return nil end
local str = saveFile:read("*all")
saveFile:close()
return str
end
-------------------------------------
-- Saves a table to a text file
--
-- @param fileName Name of the file to write to
-- @param values Key/value table containing the values to save
-- @param obfuscate Should the file contents be obfuscated?
-- @return True if everything went right, false otherwise
-------------------------------------
-- function DCSEx.io.save(fileName, values, obfuscate)
-- obfuscate = obfuscate or false -- TODO: obfuscation
-- -- IO table is sanitized, cannot read/write to disk
-- if not DCSEx.io.canReadAndWrite() then return false end
-- -- No values or not a table
-- if values == nil then return false end
-- if type(values) ~= "table" then return false end
-- local saveFile = io.open(fileName, "w")
-- if not saveFile then return false end
-- for k,v in pairs(values) do
-- saveFile:write(k.."="..tostring(v).."\n")
-- -- trigger.action.outText("SET value \""..k.."\" TO \""..tostring(v).."\"", 1)
-- end
-- saveFile:close()
-- return true
-- end
function DCSEx.io.save(fileName, str)
-- IO table is sanitized, cannot read/write to disk
if not DCSEx.io.canReadAndWrite() then return false end
local saveFile = io.open(fileName, "w")
if not saveFile then return false end
saveFile:write(str)
saveFile:close()
return true
end
end

View File

@@ -0,0 +1,351 @@
-- ====================================================================================
-- (DCS LUA ADD-ON) MATH - EXTENSION TO THE "MATH" TABLE
--
-- (Constant) DCSEx.math.TWO_PI
-- DCSEx.math.addVec(vecA, vecB)
-- DCSEx.math.clamp(val, min, max)
-- DCSEx.math.getBearing(point, refPoint)
-- DCSEx.math.getDistance2D(vec2a, vec2b)
-- DCSEx.math.getDistance3D(vec3a, vec3b)
-- DCSEx.math.getRelativeHeading(point, refObject)
-- DCSEx.math.getVec2FromAngle(angle)
-- DCSEx.math.isPointInsideCircle(center, radius, vec2)
-- DCSEx.math.isPointInsidePolygon(polygon, vec2)
-- DCSEx.math.isSamePoint(pointA, pointB)
-- DCSEx.math.lerp(val0, val1, t)
-- DCSEx.math.multVec2(vec2, mult)
-- DCSEx.math.normalizeVec2(vec2)
-- DCSEx.math.randomBoolean()
-- DCSEx.math.randomFloat(min, max)
-- DCSEx.math.randomPointAtDistance(point, distance)
-- DCSEx.math.randomPointInCircle(center, radius, minRadius, surfaceType)
-- DCSEx.math.randomSign()
-- DCSEx.math.toBoolean(val)
-- DCSEx.math.vec2ToVec3(vec2, y)
-- DCSEx.math.vec3ToVec2(vec3)
-- ====================================================================================
DCSEx.math = {}
-------------------------------------
-- Constants
-------------------------------------
DCSEx.math.TWO_PI = math.pi * 2
-------------------------------------
-- Returns the sum of two vec2 or vec3
-- @param vecA A vector
-- @param vecB Another vector
-- @return The sum of both vectors
-------------------------------------
function DCSEx.math.addVec(vecA, vecB)
if vecA.z then
return { x = vecA.x + vecB.x, y = vecA.y + vecB.y, z = vecA.z + vecB.z }
end
return { x = vecA.x + vecB.x, y = vecA.y + vecB.y }
end
-------------------------------------
-- Clamp a number value between min and max
-- @param value The value to clamp
-- @param min Minimum allowed value
-- @param max Maximum allowed value
-- @return Clamped value
-------------------------------------
function DCSEx.math.clamp(val, min, max)
return math.max(min, math.min(max, val))
end
-------------------------------------
-- Gets the bearing between two vectors, in degrees
-- @param point A vec2/vec3
-- @param refPoint Vec2/vec3 to use as a reference point
-- @return Bearing, as a number, in degrees
-------------------------------------
function DCSEx.math.getBearing(point, refPoint)
refPoint = refPoint or {x = 0, y = 0}
if point.z then point = DCSEx.math.vec3ToVec2(point) end -- Point is a vec3, convert it
if refPoint.z then refPoint = DCSEx.math.vec3ToVec2(refPoint) end -- Point is a vec3, convert it
local bearing = math.atan2(point.y - refPoint.y, point.x - refPoint.x)
if bearing < 0 then
bearing = bearing + DCSEx.math.TWO_PI
end
return math.floor(bearing * (180 / math.pi))
end
-------------------------------------
-- Returns the pythagorean distance between two 2D points or the length of a single vector
-- @param vec2a A 2D point
-- @param vec2b (optional) Another 2D point
-- @return Distance between the points
-------------------------------------
function DCSEx.math.getDistance2D(vec2a, vec2b)
vec2b = vec2b or { x = 0, y = 0 }
if vec2a.z then vec2a = DCSEx.math.vec3ToVec2(vec2a) end
if vec2b.z then vec2b = DCSEx.math.vec3ToVec2(vec2b) end
return math.sqrt((vec2a.x - vec2b.x) ^ 2 + (vec2a.y - vec2b.y) ^ 2)
end
-------------------------------------
-- Returns the pythagorean distance between two 3D points or the length of a single vector
-- @param vec3a A 3D point
-- @param vec3b (optional) Another 3D point
-- @return Distance between the points
-------------------------------------
function DCSEx.math.getDistance3D(vec3a, vec3b)
vec3b = vec3b or { x = 0, y = 0, z = 0 }
return math.sqrt((vec3a.x - vec3b.x) ^ 2 + (vec3a.y - vec3b.y) ^ 2 + (vec3a.z - vec3b.z) ^ 2)
end
-------------------------------------
-- Returns the relative heading difference between refObject and a given point
-- @param point The point for which to check the relative heading
-- @param refObject The reference object against which relative heading should be measured
-- @param format (optional) Return format. Possible formats are "clock" (1 o'clock...) or "cardinal" (NNW...)
-- @return A number in degrees, or a string if a special format was provided
-------------------------------------
function DCSEx.math.getRelativeHeading(point, refObject, format)
if not point then return nil end
if not refObject then return nil end
local unitpos = refObject:getPosition()
local bearing = DCSEx.math.getBearing(point, unitpos:getPoint())
local unitBearing = math.atan2(unitpos.x.z, unitpos.x.x)
if unitBearing < 0 then unitBearing = unitBearing + DCSEx.math.TWO_PI end
unitBearing = math.floor(unitBearing * (180 / math.pi))
local relBearing = bearing - unitBearing
if relBearing < 0 then relBearing = relBearing + 360 end
if format == "oclock" then
local oClock = math.floor(relBearing / 30)
if oClock <= 0 then oClock = 12 end
return tostring(oClock).." o'clock"
elseif format == "cardinal" then
return "TODO" -- TODO: cardinal
end
return math.floor(relBearing)
end
-------------------------------------
-- Returns an normalized vec2 from an angle/bearing in radians
-- @param unit Angle/bearing in radians
-- @return A normalized vec2
-------------------------------------
function DCSEx.math.getVec2FromAngle(angle)
return {x = math.cos(angle), y = math.sin(angle)}
end
-------------------------------------
-- Is a point inside a circle?
-- @param center The center of the circle, as a vec2
-- @param radius The radius of the circle
-- @param vec2 A vec2
-- @return True if vec2 is inside the circle, false otherwise
-------------------------------------
function DCSEx.math.isPointInsideCircle(center, radius, vec2)
return (vec2.x - center.x) ^ 2 + (vec2.y - center.y) ^ 2 < radius ^ 2
end
-------------------------------------
-- Is a point inside a polygon?
-- @param vec2[] A polygon, as a table of vec2
-- @param vec2 A vec2
-- @return True if vec2 is inside the polygon, false otherwise
-------------------------------------
function DCSEx.math.isPointInsidePolygon(polygon, vec2)
if not polygon or not vec2 then return false end
local oddNodes = false
local j = #polygon
for i = 1, #polygon do
if (polygon[i].y < vec2.y and polygon[j].y >= vec2.y or polygon[j].y < vec2.y and polygon[i].y >= vec2.y) then
if
(polygon[i].x + (vec2.y - polygon[i].y) / (polygon[j].y - polygon[i].y) * (polygon[j].x - polygon[i].x) <
vec2.x)
then
oddNodes = not oddNodes
end
end
j = i
end
return oddNodes
end
-------------------------------------
-- Compares two 2D or 3D points
-- @param pointA a Point2 or Point3
-- @param pointB another Point2 or Point3
-- @return True if points are the same, false otherwise
-------------------------------------
function DCSEx.math.isSamePoint(pointA, pointB)
if pointA.x ~= pointB.x then return false end
if pointA.y ~= pointB.y then return false end
if pointA.z or pointB.z then
if pointA.z ~= pointB.z then return false end
end
return true
end
-------------------------------------
-- Linearly interpolates between two numbers
-- @param val0 Value vers l=0
-- @param val1 Value vers l=1
-- @param t Interpolation between 0 and 1
-- @return A number value
-------------------------------------
function DCSEx.math.lerp(val0, val1, t)
return val0 + t * (val1 - val0);
end
-------------------------------------
-- Multiplies both the x and y components of a vec2 by a floating-point value
-- @param vec2 A vec2
-- @param mult A floating-point value
-- @return A vec2
-------------------------------------
function DCSEx.math.multVec2(vec2, mult)
return {x = vec2.x * mult, y = vec2.y * mult}
end
-------------------------------------
-- Returns an normalized vec2
-- @param unit A vec2
-- @return A normalized vec2
-------------------------------------
function DCSEx.math.normalizeVec2(vec2)
local length = DCSEx.math.getDistance2D(vec2)
return {x = vec2.x / length, y = vec2.y / length}
end
-------------------------------------
-- Returns a random boolean
-- @return A boolean
-------------------------------------
function DCSEx.math.randomBoolean()
return DCSEx.math.random(1, 2) == 1
end
-------------------------------------
-- Returns a random floating-point number between min and max
-- @param min Minimum floating-point value
-- @param max Maximum floating-point value
-- @return A number
-------------------------------------
function DCSEx.math.randomFloat(min, max)
if min >= max then
return min
end
return min + math.random() * (max - min)
end
-------------------------------------
-- Returns a random vec2 at a given distance of another vec2
-- @param point Reference point
-- @param distance Distance from the reference point
-- @return A vec2
-------------------------------------
function DCSEx.math.randomPointAtDistance(point, distance)
local angle = math.random() * math.pi * 2.0
local x = point.x + math.cos(angle) * distance
local y = point.y + math.sin(angle) * distance
return { x = x, y = y }
end
-------------------------------------
-- Returns a random vec2 in circle of a given center and radius
-- @param center Center of the circle as a vec2
-- @param radius Radius of the circle
-- @param minRadius (optional) Minimum inner radius circle in which points should not be spawned
-- @param surfaceType (optional) If not nil, point must be of given surface type
-- @return A vec2 or nil if no point was found
-------------------------------------
function DCSEx.math.randomPointInCircle(center, radius, minRadius, surfaceType)
local minRadius = minRadius or 0
local dist = minRadius + math.random() * (radius - minRadius)
local angle = math.random() * math.pi * 2.0
local point = nil
for i=1,36 do
local x = center.x + math.cos(angle) * dist
local y = center.y + math.sin(angle) * dist
point = { x = x, y = y }
if not surfaceType then return point end
if land.getSurfaceType(point) == surfaceType then return point end
angle = angle + 0.174533 -- Increment angle by 10° (in radians)
end
return nil
end
-------------------------------------
-- Returns a random sign as a number, -1 or 1
-- @return -1 50% of the time, 1 50% of the time
-------------------------------------
function DCSEx.math.randomSign()
if math.random() < .5 then
return -1
end
return 1
end
-------------------------------------
-- Converts a value to a boolean
-- @param val Value to convert
-- @return A boolean, or nil if val was nil
-------------------------------------
function DCSEx.math.toBoolean(val)
if val == nil then return nil end
if not val then return false end
if val == 0 then return false end
if val:lower() == "false" or val:lower() == "no" or val:lower() == "off" then return false end
return true
end
-------------------------------------
-- Converts a vec2 to a vec3
-- @param vec2 A vec2
-- @param y (Optional) A value for the vec3's y component or "land" to use land height
-- @return A vec3 where v3.x=v2.x, v3.y=y and v3.z=v2.y
-------------------------------------
function DCSEx.math.vec2ToVec3(vec2, y)
-- Value was already a vec3
if vec2.z then
return {x = vec2.x, y = vec2.y, z = vec2.z}
end
y = y or 0
if y == "land" then y = land.getHeight(vec2) end
return {x = vec2.x, y = y, z = vec2.y}
end
-------------------------------------
-- Converts a vec3 to a vec2
-- @param vec3 A vec3
-- @return A vec2 where v2.x=v3.x and v2.y=v3.z
-------------------------------------
function DCSEx.math.vec3ToVec2(vec3)
-- Value was already a vec2
if not vec3.z then
return {x = vec3.x, y = vec3.y}
end
return {x = vec3.x, y = vec3.z}
end

View File

@@ -0,0 +1,87 @@
-- ====================================================================================
-- (DCS LUA ADD-ON) STRING - EXTENSION TO THE "STRING" TABLE
--
-- DCSEx.string.firstToUpper(str)
-- DCSEx.string.getReadingTime(message)
-- DCSEx.string.split(str, separator)
-- DCSEx.string.startsWith(haystack, needle)
-- DCSEx.string.trim(str)
-- ====================================================================================
DCSEx.string = {}
-------------------------------------
-- Uppercases the fist letter of a string
-- @param str A string
-- @return A string, with the first letter cast to upper case
-------------------------------------
function DCSEx.string.firstToUpper(str)
return (str:gsub("^%l", DCSEx.string.upper))
end
-------------------------------------
-- Estimates the time (in seconds) required to read a string
-- @param message A text message
-- @return A duration in seconds
-------------------------------------
function DCSEx.string.getReadingTime(message)
message = message or ""
message = tostring(message)
return DCSEx.math.clamp(#message / 8.7, 3.0, 15.0) -- 10.7 letters per second, minimum length 3 seconds, max length 15 seconds
end
-- TODO: description, update file header
function DCSEx.string.join(table, separator)
local joinedString = ""
for i=1,#table do
joinedString = joinedString..tostring(table[i])
if i < #table then
joinedString = joinedString..separator
end
end
return joinedString
end
-- TODO: description, file header
-- Code from https://stackoverflow.com/questions/10989788/format-integer-in-lua
function DCSEx.string.toStringThousandsSeparator(number)
local i, j, minus, int, fraction = tostring(number):find('([-]?)(%d+)([.]?%d*)')
int = int:reverse():gsub("(%d%d%d)", "%1,")
return minus .. int:reverse():gsub("^,", "") .. fraction
end
-------------------------------------
-- Splits a string
-- @param str The string to split
-- @param separator The string to split
-- @return A table of split strings
-------------------------------------
function DCSEx.string.split(str, separator)
separator = separator or "%s"
local t = {}
for s in DCSEx.string.gmatch(str, "([^"..separator.."]+)") do table.insert(t, s) end
return t
end
-------------------------------------
-- Does a string starts with the given substring?
-- @param haystack The string
-- @param needle The substring to look for
-- @return True if it starts with the substring, false otherwise
-------------------------------------
function DCSEx.string.startsWith(haystack, needle)
return haystack:sub(1, #needle) == needle
end
-------------------------------------
-- Trims a string
-- @param str A string
-- @return A string
-------------------------------------
function DCSEx.string.trim(str)
return (str:gsub("^%s*(.-)%s*$", "%1"))
end

View File

@@ -0,0 +1,222 @@
-- ====================================================================================
-- (DCS LUA ADD-ON) TABLE - EXTENSION TO THE "TABLE" TABLE
--
-- DCSEx.table.contains(t, val)
-- DCSEx.table.containsKey(t, k)
-- DCSEx.table.containsAll(t, values)
-- DCSEx.table.containsAny(t, values)
-- DCSEx.table.countNonNils(t)
-- DCSEx.table.deepCopy(orig)
-- DCSEx.table.dump(t)
-- DCSEx.table.getKeys(t)
-- DCSEx.table.getKeyFromValue(t, val)
-- DCSEx.table.getRandom(t)
-- DCSEx.table.getRandomIndex(t)
-- DCSEx.table.shuffle(t)
-- ====================================================================================
DCSEx.table = {}
-------------------------------------
-- Returns true if table t contains value val
-- @param t A table
-- @param val A value
-- @return True if the table contains the value, false otherwise
-------------------------------------
function DCSEx.table.contains(t, val)
if not t then return false end
for _,v in pairs(t) do
if v == val then return true end
end
return false
end
-------------------------------------
-- Returns true if table t contains key k
-- @param t A table
-- @param k A key
-- @return True if the table contains the key, false otherwise
-------------------------------------
function DCSEx.table.containsKey(t, k)
if not t then return false end
if t[k] then return true end
return false
end
-------------------------------------
-- Returns true if table t contains all values in table values
-- @param t A table
-- @param values A table of values
-- @return True if the table contains all values, false otherwise
-------------------------------------
function DCSEx.table.containsAll(t, values)
if not t then return false end
for _,v in pairs(values) do
if not DCSEx.table.contains(t, v) then
return false
end
end
return true
end
-------------------------------------
-- Returns true if table t contains at least one value in table values
-- @param t A table
-- @param values A table of values
-- @return True if the table contains at least one value, false otherwise
-------------------------------------
function DCSEx.table.containsAny(t, values)
if not t then return false end
for _,v in pairs(values) do
if DCSEx.table.contains(t, v) then
return true
end
end
return false
end
function DCSEx.table.containsAllKeys(t, keys)
if not t then return false end
for _,k in pairs(keys) do
if not DCSEx.table.containsKey(t, k) then
return false
end
end
return true
end
function DCSEx.table.containsAnyKeys(t, keys)
if not t then return false end
for _,k in pairs(keys) do
if DCSEx.table.containsKey(t, k) then
return true
end
end
return false
end
-------------------------------------
-- Returns the number of non-nils elements in a table
-- @param t A table
-- @return A number
-------------------------------------
function DCSEx.table.countNonNils(t)
local count = 0
for _,v in pairs(t) do
if v ~= nil then count = count + 1 end
end
return count
end
-------------------------------------
-- Returns a deep copy of the table, doesn't work with recursive tables (code from http://lua-users.org/wiki/CopyTable)
-- @param orig A table
-- @return A deep copied clone of the table
-------------------------------------
function DCSEx.table.deepCopy(orig)
-- Not a table
if type(orig) ~= "table" then
return orig
end
local copy = {}
for orig_key, orig_value in next, orig, nil do
copy[DCSEx.table.deepCopy(orig_key)] = DCSEx.table.deepCopy(orig_value)
end
setmetatable(copy, DCSEx.table.deepCopy(getmetatable(orig)))
return copy
end
-------------------------------------
-- Dumps the content of a table as a string
-- @param orig A table
-- @return A string representaton of the table
-------------------------------------
function DCSEx.table.dump(t)
if type(t) == 'table' then
local s = '{ '
for k,v in pairs(t) do
if type(k) ~= 'number' then k = '"'..k..'"' end
s = s .. '['..k..'] = ' .. DCSEx.table.dump(v) .. ','
end
return s .. '} '
else
return tostring(t)
end
end
-------------------------------------
-- Returns the key associated to a value in a table, or nil if not found
-- @param t A table
-- @param val A value
-- @return The key associated to this value in the table, or nil
-------------------------------------
function DCSEx.table.getKeyFromValue(t, val)
for k, v in pairs(t) do
if v == val then return k end
end
return nil
end
-------------------------------------
-- Returns all the keys in an associative table
-- @param t A table
-- @return An array of keys
-------------------------------------
function DCSEx.table.getKeys(t)
local keys = {}
local n = 1
for k,_ in pairs(t) do
keys[n] = k
n = n + 1
end
table.sort(keys)
return keys
end
-------------------------------------
-- Returns a random value from a numerically-indexed table
-- @param t A table
-- @return A random element from the table
-------------------------------------
function DCSEx.table.getRandom(t)
return t[math.random(#t)]
end
-------------------------------------
-- Returns a random index from a numerically-indexed table
-- @param t A table
-- @return A random index from the table
-------------------------------------
function DCSEx.table.getRandomIndex(t)
return math.random(#t)
end
-------------------------------------
-- Randomly shuffles a numerically-indexed table
-- @param t A table
-- @return A table with shuffled values
-------------------------------------
function DCSEx.table.shuffle(t)
local len, random = #t, math.random
for i = len, 2, -1 do
local j = random(1, i)
t[i], t[j] = t[j], t[i]
end
return t
end

View File

@@ -0,0 +1,213 @@
DCSEx.unitCallsignMaker = {}
do
local CALLSIGN_TYPE = {
DEFAULT = 1,
A10 = 2,
AH64 = 3,
AWACS = 4,
B1B = 5,
B52 = 6,
F15E = 7,
F16 = 8,
F18 = 9,
JTAC = 10,
TANKER = 11,
TRANSPORT = 12,
}
local CALLSIGNS = {
[CALLSIGN_TYPE.DEFAULT] = {
[1] = "Enfield",
[2] = "Springfield",
[3] = "Uzi",
[4] = "Colt",
[5] = "Dodge",
[6] = "Ford",
[7] = "Chevy",
[8] = "Pontiac",
},
[CALLSIGN_TYPE.A10] = {
[9] = "Hawg",
[10] = "Boar",
[11] = "Pig",
[12] = "Tusk",
},
[CALLSIGN_TYPE.AH64] = {
[9] = "Army Air",
[10] = "Apache",
[11] = "Crow",
[12] = "Sioux",
[13] = "Gatling",
[14] = "Gunslinger",
[15] = "Hammerhead",
[16] = "Bootleg",
[17] = "Palehorse",
[18] = "Carnivor",
[19] = "Saber",
},
[CALLSIGN_TYPE.AWACS] = {
[1] = "Overlord",
[2] = "Magic",
[3] = "Wizard",
[4] = "Focus",
[5] = "Darkstar",
},
[CALLSIGN_TYPE.B1B] = {
[9] = "Bone",
[10] = "Dark",
[11] = "Vader",
},
[CALLSIGN_TYPE.B52] = {
[9] = "Buff",
[10] = "Dump",
[11] = "Kenworth",
},
[CALLSIGN_TYPE.F15E] = {
[9] = "Dude",
[10] = "Thud",
[11] = "Gunny",
[12] = "Trek",
[13] = "Sniper",
[14] = "Sled",
[15] = "Best",
[16] = "Jazz",
[17] = "Rage",
[18] = "Tahoe",
},
[CALLSIGN_TYPE.F16] = {
[9] = "Viper",
[10] = "Venom",
[11] = "Lobo",
[12] = "Cowboy",
[13] = "Python",
[14] = "Rattler",
[15] = "Panther",
[16] = "Wolf",
[17] = "Weasel",
[18] = "Wild",
[19] = "Ninja",
[20] = "Jedi",
},
[CALLSIGN_TYPE.F18] = {
[9] = "Hornet",
[10] = "Squid",
[11] = "Ragin",
[12] = "Roman",
[13] = "Sting",
[14] = "Jury",
[15] = "Joker",
[16] = "Ram",
[17] = "Hawk",
[18] = "Devil",
[19] = "Check",
[20] = "Snake",
},
[CALLSIGN_TYPE.JTAC] = {
[1] = "Axeman",
[2] = "Darknight",
[3] = "Warrior",
[4] = "Pointer",
[5] = "Eyeball",
[6] = "Moonbeam",
[7] = "Whiplash",
[8] = "Finger",
[9] = "Pinpoint",
[10] = "Ferret",
[11] = "Shaba",
[12] = "Playboy",
[13] = "Hammer",
[14] = "Jaguar",
[15] = "Deathstar",
[16] = "Anvil",
[17] = "Firefly",
[18] = "Mantis",
[19] = "Badger"
},
[CALLSIGN_TYPE.TANKER] = {
[1] = "Texaco",
[2] = "Arco",
[3] = "Shell",
},
[CALLSIGN_TYPE.TRANSPORT] = {
[9] = "Heavy",
[10] = "Trash",
[11] = "Cargo",
[12] = "Ascot",
},
}
local currentCallsigns = {}
for _,i in pairs(CALLSIGN_TYPE) do
currentCallsigns[i] = { 1, 1, 1, 1, 1, 1, 1, 1, 1 }
end
local function getCallsignTypeByUnitType(unitType)
if not unitType then return CALLSIGN_TYPE.DEFAULT end
-- TODO: complete
if unitType == "AH-64A" or unitType == "AH-64D" or unitType == "AH-64D_BLK_II" then return CALLSIGN_TYPE.A10 end
if unitType == "A-10A" or unitType == "A-10C" or unitType == "A-10C_2" then return CALLSIGN_TYPE.A10 end
if unitType == "F-15E" or unitType == "F-15ESE" then return CALLSIGN_TYPE.F15E end
if unitType == "F-16A" or unitType == "F-16A MLU" or unitType == "F-16C bl.50" or unitType == "F-16C bl.52d" or unitType == "F-16C_50" then return CALLSIGN_TYPE.F16 end
if unitType == "FA-18C" or unitType == "FA-18C_hornet" then return CALLSIGN_TYPE.F18 end
-- if unitType == "A-50" or unitType == "E-2C" or unitType == "E-3A" then return CALLSIGN_TYPE.AWACS end
local unitDesc = Unit.getDescByName(unitType)
if not unitDesc then return CALLSIGN_TYPE.DEFAULT end
if unitDesc.attributes["AWACS"] then return CALLSIGN_TYPE.AWACS end
if unitDesc.attributes["Tankers"] then return CALLSIGN_TYPE.TANKER end
return CALLSIGN_TYPE.DEFAULT
end
local function incrementCallsign(callsignName, callsignNumber)
if not callsignName then return end
callsignNumber = math.max(1, callsignNumber or 1)
for k,_ in pairs(CALLSIGNS) do
for i,_ in pairs(CALLSIGNS[k]) do
if CALLSIGNS[k][i]:lower() == callsignName:lower() then
currentCallsigns[k][i] = math.max(currentCallsigns[k][i], callsignNumber + 1)
if (currentCallsigns[k][i] > 9) then currentCallsigns[k][i] = 1 end -- More than 9? Loop back to 1.
-- TUM.log("Callsign "..CALLSIGNS[k][i].." set to "..tostring(callsignNumber))
return
end
end
end
end
function DCSEx.unitCallsignMaker.getCallsign(unitType)
local csType = getCallsignTypeByUnitType(unitType)
local csNameIndex = DCSEx.table.getRandom(DCSEx.table.getKeys(CALLSIGNS[csType]))
if not currentCallsigns[csType][csNameIndex] then currentCallsigns[csType][csNameIndex] = 1 end
local csNumber = currentCallsigns[csType][csNameIndex]
currentCallsigns[csType][csNameIndex] = currentCallsigns[csType][csNameIndex] + 1
if currentCallsigns[csType][csNameIndex] > 9 then currentCallsigns[csType][csNameIndex] = 1 end
local callsignTable = {
[1] = csNameIndex,
[2] = csNumber,
-- [3] = 1,
["name"] = CALLSIGNS[csType][csNameIndex]..tostring(csNumber),
}
return callsignTable
end
do
local missionGroups = DCSEx.envMission.getGroups()
for _,g in ipairs(missionGroups) do
if g.units and g.units[1] then
local unit = g.units[1]
if unit.callsign and unit.callsign[1] and unit.callsign.name then
local callsignName = unit.callsign.name:sub(1, #unit.callsign.name - 2)
incrementCallsign(callsignName, unit.callsign[2])
end
end
end
end
end

View File

@@ -0,0 +1,453 @@
-- ====================================================================================
-- DCSEX.UNITGROUMAKER - CREATES AND ADDS GROUPS TO THE GAME WORLD
--
-- (local) createGroupTable(groupID, groupCategory, options)
-- (local) getDefaultUnitSpread(groupCategory)
-- (local) getNextGroupID()
-- (local) getNextUnitID()
-- (local) setAircraftTaskAwacs(groupTable)
-- (local) setAircraftTaskOrbit(groupTable, options)
-- (local) setCommand(groupTable, actionID, actionValue)
-- (local) setOption(groupTable, optionID, optionValue)
-- DCSEx.unitGroupMaker.create(coalitionID, groupCategory, vec2, unitTypes, options)
-- DCSEx.unitGroupMaker.initialize()
-- ====================================================================================
DCSEx.unitGroupMaker = {}
do
local nextGroupID = 1 -- ID of the next generated group
local nextUnitID = 1 -- ID of the next generated unit
local function createGroupTable(groupID, groupCategory, options)
local groupTable = {
groupId = groupID,
hidden = options.hidden,
name = options.name,
route = {points = {}},
start_time = 0,
taskSelected = true,
uncontrollable = true,
visible = false,
units = {},
}
if groupCategory == Group.Category.GROUND then
groupTable.task = "Ground Nothing"
groupTable.tasks = {}
elseif groupCategory == Group.Category.AIRPLANE or groupCategory == Group.Category.HELICOPTER then
groupTable.uncontrolled = options.uncontrolled or false
end
return groupTable
end
local function getDefaultUnitSpread(groupCategory)
if groupCategory == Group.Category.AIRPLANE or groupCategory == Group.Category.HELICOPTER then
return 150 -- TODO: improve
elseif groupCategory == Group.Category.SHIP then
return math.random(250, 400)
else
return math.random(20, 30)
end
end
-- Returns the next available group ID and increment the number
local function getNextGroupID()
nextGroupID = nextGroupID + 1
return nextGroupID - 1
end
-- Returns the next available unit ID and increment the number
local function getNextUnitID()
nextUnitID = nextUnitID + 1
return nextUnitID - 1
end
local function setAircraftTaskAwacs(groupTable)
groupTable.frequency = 251000000
groupTable.task = "AWACS"
table.insert(groupTable.route.points[1].task.params.tasks,
{
["enabled"] = true,
["auto"] = true,
["id"] = "AWACS",
["number"] = #groupTable.route.points[1].task.params.tasks + 1,
["params"] = { },
})
return groupTable
end
local function setAircraftTaskCAP(groupTable)
groupTable.task = "CAP"
table.insert(groupTable.route.points[1].task.params.tasks,
{
["enabled"] = true,
["auto"] = true,
["id"] = "EngageTargets",
["number"] = #groupTable.route.points[1].task.params.tasks + 1,
["params"] = {
maxDist = DCSEx.converter.nmToMeters(60),
maxDistEnabled = false,
-- targetTypes = { "Planes", "Helicopters" },
targetTypes = { "Fighters", "Interceptors", "Multirole fighters" },
priority = 0
},
})
return groupTable
end
local function setAircraftTaskOrbit(groupTable, options)
-- TODO: oval orbit
table.insert(groupTable.route.points[#groupTable.route.points].task.params.tasks,
{
["enabled"] = true,
["auto"] = false,
["id"] = "Orbit",
["number"] = #groupTable.route.points[#groupTable.route.points].task.params.tasks + 1,
["params"] =
{
["altitude"] = options.altitude,
["pattern"] = "Circle",
["speed"] = options.speed,
},
})
return groupTable
end
local function setCommand(groupTable, actionID, actionValue)
table.insert(
groupTable.route.points[1].task.params.tasks,
{
["auto"] = false,
["enabled"] = true,
["id"] = "WrappedAction",
["number"] = #groupTable.route.points[1].task.params.tasks + 1,
["params"] = { ["action"] = { ["id"] = actionID, ["params"] = { ["value"] = actionValue, }, }, },
})
end
local function setOption(groupTable, optionID, optionValue)
table.insert(
groupTable.route.points[1].task.params.tasks,
{
["auto"] = false,
["enabled"] = true,
["id"] = "WrappedAction",
["number"] = #groupTable.route.points[1].task.params.tasks + 1,
["params"] = { ["action"] = { ["id"] = "Option", ["params"] = { ["name"] = optionID, ["value"] = optionValue, }, }, },
})
end
function DCSEx.unitGroupMaker.createStatic(side, point2, typeName, shapeName, heading, dead)
heading = heading or DCSEx.converter.degreesToRadians(math.random(0, 359))
dead = dead or false
local unitID = getNextUnitID()
local staticObj = {
["heading"] = 0,
["groupId"] = getNextGroupID(),
["shape_name"] = shapeName,
["type"] = typeName,
["unitId"] = unitID,
["rate"] = 100,
["name"] = "Static #"..tostring(unitID),
["category"] = "Fortifications",
["y"] = point2.y,
["x"] = point2.x,
["dead"] = dead,
}
coalition.addStaticObject(DCSEx.dcs.getCJTFForCoalition(side), staticObj)
return unitID
end
function DCSEx.unitGroupMaker.create(coalitionID, groupCategory, vec2, unitTypes, options)
if not unitTypes or #unitTypes == 0 then return nil end -- No unit types provided
if type(unitTypes) == "string" then unitTypes = { unitTypes } end -- Single unit type provided, make it a table of size 1
local aircraftDB = nil -- Aircraft entry in the DB for airplane/helicopter units
local destVec2 = nil -- Destination point (for moving units)
local groupID = getNextGroupID() -- Get a new unique ID for the group
local isAirUnit = false
local hidden = false
if not TUM.DEBUG_MODE and coalitionID ~= TUM.settings.getPlayerCoalition() then
hidden = true
end
-- Setup options
options = options or {}
--options.heading = nil
options.altitude = options.altitude or 0
options.altitudeType = options.altitudeType or "BARO"
options.hidden = hidden
options.isMoving = false
options.livery = options.livery or "default"
options.name = DCSEx.unitNamesMaker.getName(groupID, unitTypes)
options.pointAction = "Turning Point"
options.skill = options.skill or "Average"
options.speed = 5.5555556
options.spreadDistance = options.spreadDistance or getDefaultUnitSpread(groupCategory)
options.spreadOffset = options.spreadOffset or 0
-- Movement point (for units with a second WP)
if options.moveTo then
options.isMoving = true
destVec2 = DCSEx.table.deepCopy(options.moveTo)
elseif options.moveBy then
options.isMoving = true
local angle = DCSEx.converter.degreesToRadians(math.random(0, 359))
destVec2 = { x = vec2.x + math.cos(angle) * options.moveBy, y = vec2.y + math.sin(angle) * options.moveBy }
end
-- Category specific options
if groupCategory == Group.Category.GROUND then
options.pointAction = "Off Road"
local desc = Unit.getDescByName(unitTypes[1])
if desc and desc.attributes and desc.attributes.Infantry then options.speed = 1.66667 end
-- Check position and formation for moving ground units
if options.isMoving then
if options.onRoad then
options.pointAction = "On Road"
vec2 = DCSEx.world.getClosestPointOnRoadsVec2(vec2)
destVec2 = DCSEx.world.getClosestPointOnRoadsVec2(destVec2)
else
options.pointAction = options.formation or DCSEx.table.getRandom({"Rank", "Cone", "Vee", "Diamond", "EchelonL", "EchelonR"})
end
end
options.livery = "default" -- TODO: getSeasonalLivery()
elseif groupCategory == Group.Category.AIRPLANE or groupCategory == Group.Category.HELICOPTER then
isAirUnit = true
-- Plane/helicopter groups always use a single unit type
for i=1,#unitTypes do
unitTypes[i] = unitTypes[1]
end
-- Get unit info from aircraft database
aircraftDB = Library.aircraft[unitTypes[1]]
if not aircraftDB then return nil end -- Unit wasn't found in the database, abort group creation
-- Pick a random livery if available
-- if aircraftDB.liveries and aircraftDB.liveries[coalitionID] then
-- options.livery = table.getRandom(aircraftDB.liveries[coalitionID])
-- end
options.altitude = aircraftDB.altitude or DCSEx.converter.feetToMeters(15000)
options.altitudeType = "BARO"
options.speed = DCSEx.math.randomFloat(0.9, 1.1) * (aircraftDB.speed or 250)
end
-- First unit of the group is a template, use a group template instead of enumerating the types in unitTypes
local groupTemplate = nil
if Library.groupTemplates[unitTypes[1]] then
groupTemplate = DCSEx.table.deepCopy(Library.groupTemplates[unitTypes[1]])
unitTypes = {}
for i=1,#groupTemplate do
table.insert(unitTypes, groupTemplate[i].name)
end
end
-- Create group table
local groupTable = createGroupTable(groupID, groupCategory, options)
groupTable.x = vec2.x
groupTable.y = vec2.y
-- Initial waypoint
table.insert(
groupTable.route.points,
{
action = options.pointAction,
alt = options.altitude,
alt_type = options.altitudeType,
ETA = 0.0,
ETA_locked = false,
formation_template = "",
name = "WP1",
speed = options.speed,
speed_locked = true,
task = {id = "ComboTask", params = {tasks = {}}},
type = "Turning Point",
x = vec2.x,
y = vec2.y
}
)
if options.takeOff and (groupCategory == Group.Category.AIRPLANE or groupCategory == Group.Category.HELICOPTER) then
groupTable.route.points[1].alt = 250
groupTable.route.points[1].alt_type = "RADIO"
end
if destVec2 then -- There's a destination, add a second waypoint
local destPoint = {
action = options.pointAction,
alt_type = options.altitudeType,
alt = options.altitude,
ETA = 0.0,
ETA_locked = false,
formation_template = "",
name = "WP2",
speed = options.speed,
task = {
id = "ComboTask",
params = { tasks = { } }
},
type = "Turning Point",
x = destVec2.x,
y = destVec2.y,
speed_locked = true
}
-- Ground/ship groups loop between their waypoints
if groupCategory == Group.Category.GROUND or groupCategory == Group.Category.SHIP then
if not options.noLoop then
table.insert(
destPoint.task.params.tasks,
{
enabled = true,
auto = false,
id = "GoToWaypoint",
number = 1,
params = {
fromWaypointIndex = 2,
nWaypointIndx = 1
}
})
end
end
table.insert(groupTable.route.points, destPoint)
end
-- Add various options/commands
if options.disableWeapons then setOption(groupTable, AI.Option.Ground.id.ROE, AI.Option.Ground.val.ROE.WEAPON_HOLD) end -- Values are from the AI.Option.Ground tables, but they're the same for all (ROE=0, WEAPON_HOLD=4)
if options.immortal then setCommand(groupTable, "SetImmortal", true) end
if options.invisible then setCommand(groupTable, "SetInvisible", true) end
if options.silenced then setOption(groupTable, AI.Option.Air.id.SILENCE, true) end
if options.unlimitedFuel then setCommand(groupTable, "SetUnlimitedFuel", true) end
local groupCallsign = nil
if isAirUnit then
if options.taskAwacs then setAircraftTaskAwacs(groupTable) end
if options.taskCAP then setAircraftTaskCAP(groupTable) end
setAircraftTaskOrbit(groupTable, options)
groupCallsign = DCSEx.unitCallsignMaker.getCallsign(unitTypes[1])
groupTable.name = groupCallsign.name
end
local unitsID = {}
-----------------------
-- Create units
-----------------------
for i=1,#unitTypes do
local unitID = getNextUnitID()
table.insert(unitsID, unitID)
local unitHeading, unitOffset
if groupTemplate then -- Use offset and heading from the group
unitHeading = groupTemplate[i].heading
unitOffset = { x = groupTemplate[i].dx, y = groupTemplate[i].dy }
else -- Create offset and heading
unitHeading = options.heading or DCSEx.math.randomFloat(0, DCSEx.math.TWO_PI)
unitOffset =
DCSEx.math.multVec2(
DCSEx.math.getVec2FromAngle(unitHeading),
options.spreadDistance * (options.spreadOffset + i - 1)
)
end
local unitType = unitTypes[i]
local unitTable = {
coldAtStart = false,
heading = unitHeading,
livery = options.livery,
name = options.name.." #"..tostring(i),
playerCanDrive = false,
skill = options.skill,
transportable = {randomTransportable = false},
type = unitType,
unitId = unitID,
x = vec2.x + unitOffset.x,
y = vec2.y + unitOffset.y
}
if isAirUnit and aircraftDB then
unitTable.hardpoint_racks = true
unitTable.psi = 1.7703702498393
unitTable.callsign = DCSEx.table.deepCopy(groupCallsign)
unitTable.callsign.name = unitTable.callsign.name..tostring(i)
unitTable.callsign[3] = i
unitTable.name = unitTable.callsign.name
-- Special properties for unit
if aircraftDB.properties then
unitTable.AddPropAircraft = DCSEx.table.deepCopy(aircraftDB.properties)
end
-- Common payload (fuel, gun ammo, etc)
if aircraftDB.payload then
unitTable.payload = DCSEx.table.deepCopy(aircraftDB.payload)
end
if aircraftDB.pylons then
-- TODO: pylons according to task
if options.taskAttack and aircraftDB.pylons.attack then
unitTable.payload.pylons = DCSEx.table.deepCopy(aircraftDB.pylons.attack)
elseif options.taskCAP and aircraftDB.pylons.cap then
unitTable.payload.pylons = DCSEx.table.deepCopy(aircraftDB.pylons.cap)
elseif options.taskSEAD and aircraftDB.pylons.sead then
unitTable.payload.pylons = DCSEx.table.deepCopy(aircraftDB.pylons.sead)
elseif options.taskStrike and aircraftDB.pylons.strike then
unitTable.payload.pylons = DCSEx.table.deepCopy(aircraftDB.pylons.strike)
else
unitTable.payload.pylons = DCSEx.table.deepCopy(aircraftDB.pylons.default)
end
end
end
table.insert(groupTable.units, unitTable)
end
coalition.addGroup(DCSEx.dcs.getCJTFForCoalition(coalitionID), groupCategory, groupTable)
return {
callsign = groupCallsign,
groupID = groupID,
unitsID = unitsID,
unitTypeNames = DCSEx.table.deepCopy(unitTypes) -- Not always the same as the unitTypes input parameter, because of group templates
}
end
------------------------------------------------------
-- INITIALIZE THE GROUP MAKER
-- Look for maximum groupID and unitID already in use to make sure we don't use an existing ID when spawning units
------------------------------------------------------
local groupData = DCSEx.envMission.getGroups()
for _,g in ipairs(groupData) do
if g.groupId >= nextGroupID then
nextGroupID = g.groupId + 1
end
for __,u in ipairs(g.units) do
if u.unitId >= nextUnitID then
nextUnitID = u.unitId + 1
end
end
end
end

View File

@@ -0,0 +1,92 @@
DCSEx.unitNamesMaker = {}
do
local function getSectionNameSuffixAir()
return DCSEx.table.getRandom({"squadron", "wing"})
end
local function getSectionNameSuffixGround()
return DCSEx.table.getRandom({"bataillon", "brigade", "corps", "division", "regiment"})
end
local function getSectionNameSuffixNaval()
return DCSEx.table.getRandom({"fleet", "force"})
end
local function getDefaultSectionName()
return "unit "..getSectionNameSuffixGround()
end
local function getNameByDesc(unitDesc)
if not unitDesc then return getDefaultSectionName() end
if unitDesc.attributes["UAVs"] then
return DCSEx.table.getRandom({"unmanned"}).." "..getSectionNameSuffixAir()
end
if unitDesc.attributes["Bombers"] then
return DCSEx.table.getRandom({"bomber"}).." "..getSectionNameSuffixAir()
end
if unitDesc.attributes["Fighters"] or unitDesc.attributes["Interceptors"] or unitDesc.attributes["Battle airplanes"] then
return DCSEx.table.getRandom({"fighter", "fighting"}).." "..getSectionNameSuffixAir()
end
if unitDesc.attributes["Planes"] then
return DCSEx.table.getRandom({"air", "cargo", "transport"}).." "..getSectionNameSuffixAir()
end
if unitDesc.attributes["Attack helicopters"] then
return DCSEx.table.getRandom({"assault", "rotary"}).." "..getSectionNameSuffixAir()
end
if unitDesc.attributes["Helicopters"] then
return DCSEx.table.getRandom({"tactical"}).." "..getSectionNameSuffixAir()
end
if unitDesc.attributes["Aircraft Carriers"] then
return DCSEx.table.getRandom({"carrier"}).." "..getSectionNameSuffixNaval()
end
if unitDesc.attributes["Unarmed ships"] then
return DCSEx.table.getRandom({"cargo", "logistical"}).." "..getSectionNameSuffixNaval()
end
if unitDesc.attributes["Ships"] then
return DCSEx.table.getRandom({"expeditionary", "assault"}).." "..getSectionNameSuffixNaval()
end
if unitDesc.attributes["MANPADS AUX"] or unitDesc.attributes["MANPADS"] or unitDesc.attributes["Armed Air Defence"] or unitDesc.attributes["SAM elements"] or unitDesc.attributes["SAM related"] or unitDesc.attributes["SAM"] then
return DCSEx.table.getRandom({"air defense"}).." "..getSectionNameSuffixGround()
end
if unitDesc.attributes["Tanks"] or unitDesc.attributes["HeavyArmoredUnits"] or unitDesc.attributes["LightArmoredUnits"] then
return DCSEx.table.getRandom({"armor", "heavy"}).." "..getSectionNameSuffixGround()
end
if unitDesc.attributes["LightArmoredUnits"] then
return DCSEx.table.getRandom({"armored", "mechanized"}).." "..getSectionNameSuffixGround()
end
if unitDesc.attributes["Ground vehicles"] then
return DCSEx.table.getRandom({"engineer","logistical","support","tactical"}).." "..getSectionNameSuffixGround()
end
if unitDesc.attributes["Infantry"] then
return DCSEx.table.getRandom({"infantry"}).." "..getSectionNameSuffixGround()
end
return getDefaultSectionName()
end
function DCSEx.unitNamesMaker.getName(groupID, unitTypes)
local nameNumber = tostring(groupID)
if (nameNumber:sub(-1) == "1") then nameNumber = nameNumber.."st"
elseif (nameNumber:sub(-1) == "2") then nameNumber = nameNumber.."nd"
elseif (nameNumber:sub(-1) == "3") then nameNumber = nameNumber.."rd"
else nameNumber = nameNumber.."th"
end
local nameSection = getDefaultSectionName()
if unitTypes and #unitTypes > 0 then
local desc = Unit.getDescByName(unitTypes[1])
if desc then
nameSection = getNameByDesc(desc)
end
end
return nameNumber.." "..nameSection
end
end

View File

@@ -0,0 +1,497 @@
-- ====================================================================================
-- WORLDTOOLS - FUNCTIONS RELATED TO THE GAME WORLD
-- ====================================================================================
-- DCSEx.world.collidesWithScenery(vec2, radius)
-- DCSEx.world.findSpawnPoint(vec2, minRadius, maxRadius, surfaceType, radiusWithoutScenery)
-- DCSEx.world.getAllPlayers()
-- DCSEx.world.getAllSceneryBuildings(minHealth)
-- DCSEx.world.getAllUnits(unitCategory)
-- DCSEx.world.getClosestPointOnRoadsVec2(vec2)
-- DCSEx.world.getCoordinatesAsString(point)
-- DCSEx.world.getCurrentMarkerID()
-- DCSEx.world.getGroupByID(groupID)
-- DCSEx.world.getNextMarkerID()
-- DCSEx.world.getSceneriesInZone(center, radius, minHealth)
-- DCSEx.world.getTerrainHeightDiff(coord, searchRadius)
-- DCSEx.world.getUnitByID(unitID)
-- DCSEx.world.isGroupAlive(g, unitsMustBeInAir)
-- DCSEx.world.setUnitLifePercent(unitID, life)
-- ====================================================================================
DCSEx.world = { }
do
-- TODO: get max marker already in use from envMission
local nextMarkerId = 1 -- Next map marker ID
function DCSEx.world.collidesWithScenery(vec2, radius)
local foundOne = false
radius = radius or 8
local volS = {
id = world.VolumeType.SPHERE,
params = {
point = DCSEx.math.vec2ToVec3(vec2, "land"),
radius = radius
}
}
local ifFound = function(foundItem, val)
foundOne = true
return true
end
world.searchObjects(Object.Category.SCENERY, volS, ifFound)
return foundOne
end
-- function DCSEx.world.findSpawnPoint(vec2, minRadius, maxRadius, surfaceType, radiusWithoutScenery, territorySide, expandSearch)
-- expandSearch = expandSearch or true
-- for _=0,16 do
-- for _=0,16 do
-- local spawnPoint = nil
-- spawnPoint = DCSEx.math.randomPointInCircle(
-- vec2,
-- DCSEx.converter.nmToMeters(maxRadius),
-- DCSEx.converter.nmToMeters(minRadius),
-- surfaceType)
-- if spawnPoint and radiusWithoutScenery then
-- if DCSEx.world.collidesWithScenery(spawnPoint, radiusWithoutScenery) then
-- spawnPoint = nil
-- end
-- end
-- if spawnPoint and territorySide then
-- if scramble.territories.getOwner(spawnPoint) ~= territorySide then
-- spawnPoint = nil
-- end
-- end
-- if spawnPoint then return spawnPoint end
-- end
-- if not expandSearch then return nil end
-- minRadius = minRadius * 0.9
-- maxRadius = maxRadius * 1.2
-- end
-- return nil
-- end
-------------------------------------
-- Returns a table of all player-controlled units currently in the game
-- @return A table of unit objects
-------------------------------------
function DCSEx.world.getAllPlayers()
local players = {}
for coalitionID=0,2 do
local coalPlayers = coalition.getPlayers(coalitionID)
for _,p in pairs(coalPlayers) do
table.insert(players, p)
end
end
return players
end
-------------------------------------
-- Returns a table of all map scenery buildings.
-- This function is rather CPU-consuming, better run it once on mission start and store the result in table.
-- @param minHealth Minimum health a building must have to be included in the table
-- @return A table of scenery objects
-------------------------------------
function DCSEx.world.getAllSceneryBuildings(minHealth)
minHealth = minHealth or 0
local sceneries = {}
local scenerySearchvolume = {
id = world.VolumeType.SPHERE,
params = { point = { x = env.mission.map.centerX, y = 0, z = env.mission.map.centerY }, radius = 10000000 }
}
local function ifSceneryFound(foundScenery, val)
local desc = foundScenery:getDesc()
if desc and desc.attributes and desc.attributes.Buildings then
if desc.life >= minHealth then
table.insert(sceneries, foundScenery)
end
end
return true
end
world.searchObjects(Object.Category.SCENERY, scenerySearchvolume, ifSceneryFound)
return sceneries
end
-------------------------------------
-- Returns all units belonging to a given category
-- @param coalitionID Coalition ID (coalition.side.XXX) or nil to search all coalitions
-- @param unitCategory An unit category (Group.Category.XXX)
-- @return A table of unit tables
-------------------------------------
function DCSEx.world.getAllUnits(coalitionID, unitCategory)
local units = {}
local searchedCoalitions = { 0, 1, 2 }
if coalitionID then searchedCoalitions = { coalitionID } end
for _,c in pairs(searchedCoalitions) do -- Enumerate coalitions
for _, g in pairs(coalition.getGroups(c, unitCategory)) do
for __, u in pairs(g:getUnits()) do
table.insert(units, u)
end
end
end
return units
end
-------------------------------------
-- Returns the closest point to roads as a vec2
-- @param vec2 Coordinates to look for
-- @return A vec2 with the closest point on roads
-------------------------------------
function DCSEx.world.getClosestPointOnRoadsVec2(vec2)
local roadX, roadY = land.getClosestPointOnRoads("roads", vec2.x, vec2.y)
return {x = roadX, y = roadY}
end
-------------------------------------
-- Returns the LL/MGRS coordinates of a point, as a string
-- Based on code by Bushmanni - https://forums.eagle.ru/showthread.php?t=99480
-- @param point The point, as a vec2 or vec3
-- @param hideElevation (optional) Show elevation NOT be displayed? Default: false
-- @return A string
-------------------------------------
function DCSEx.world.getCoordinatesAsString(point, hideElevation)
hideElevation = hideElevation or false
if not point.z then point = DCSEx.math.vec2ToVec3(point, "land") end
local cooString = ""
local LLposN, LLposE, alt = coord.LOtoLL(point)
local LLposfixN, LLposdegN = math.modf(LLposN)
LLposdegN = LLposdegN * 60
local LLposdegN2, LLposdegN3 = math.modf(LLposdegN)
local LLposdegN3Decimal = LLposdegN3 * 1000
LLposdegN3 = LLposdegN3 * 60
local LLposfixE, LLposdegE = math.modf(LLposE)
LLposdegE = LLposdegE * 60
local LLposdegE2, LLposdegE3 = math.modf(LLposdegE)
local LLposdegE3Decimal = LLposdegE3 * 1000
LLposdegE3 = LLposdegE3 * 60
local LLns = "N"
if LLposfixN < 0 then
LLns = "S"
end
local LLew = "E"
if LLposfixE < 0 then
LLew = "W"
end
local LLposNstring = LLns .. " " .. string.format("%.2i°%.2i'%.2i''", LLposfixN, LLposdegN2, LLposdegN3)
local LLposEstring = LLew .. " " .. string.format("%.3i°%.2i'%.2i''", LLposfixE, LLposdegE2, LLposdegE3)
cooString = "L/L: " .. LLposNstring .. " " .. LLposEstring
local LLposNstring = LLns .. " " .. string.format("%.2i°%.2i.%.3i", LLposfixN, LLposdegN2, LLposdegN3Decimal)
local LLposEstring = LLew .. " " .. string.format("%.3i°%.2i.%.3i", LLposfixE, LLposdegE2, LLposdegE3Decimal)
cooString = cooString .. "\nL/L: " .. LLposNstring .. " " .. LLposEstring
local mgrs = coord.LLtoMGRS(LLposN, LLposE)
local mgrsString =
mgrs.MGRSDigraph .. " " .. mgrs.UTMZone .. " " .. tostring(mgrs.Easting) .. " " .. tostring(mgrs.Northing)
cooString = cooString .. "\nMGRS: " .. mgrsString
if not hideElevation then
cooString = cooString .. "\nELEVATION: " .. math.floor(alt * 3.281) .. "ft / " .. math.floor(alt) .. " meters"
end
return cooString
end
-------------------------------------
-- Returns the last map marker ID generated by DCSEx.world.getNextMarkerID(), if any
-- @return A numeric ID, or nil
-------------------------------------
function DCSEx.world.getCurrentMarkerID()
if nextMarkerId == 1 then return nil end
return nextMarkerId - 1
end
-------------------------------------
-- Searches and return a group by its ID
-- @param groupID ID of the group
-- @return A group table, or nil if no group with this ID was found
-------------------------------------
function DCSEx.world.getGroupByID(groupID)
for coalitionID = 1, 2 do
for _, grp in pairs(coalition.getGroups(coalitionID)) do
if DCSEx.dcs.getGroupIDAsNumber(grp) == groupID then
return grp
end
end
end
return nil
end
-- TODO: description
function DCSEx.world.getMarkerByText(text, coalition)
if not text then return nil end
text = text:lower()
local markers = DCSEx.world.getMarkPanels()
for _,m in pairs(markers) do
local markerText = m.text or ""
markerText = markerText:lower()
if markerText == text then
if coalition == nil or m.coalition == coalition then
return m
end
end
end
return nil
end
-------------------------------------
-- Returns a new, unique, map marker ID
-- @return A numeric ID
-------------------------------------
function DCSEx.world.getNextMarkerID()
nextMarkerId = nextMarkerId + 1
return nextMarkerId - 1
end
-- TODO: description, file header
function DCSEx.world.getPlayersInAir(side)
local players = {}
if side then
players = coalition.getPlayers(side)
else
players = DCSEx.world.getAllPlayers()
end
local playersInAir = {}
for _,p in ipairs(players) do
if p:inAir() then
table.insert(playersInAir, p)
end
end
return playersInAir
end
-- TODO: description, file header
function DCSEx.world.getSpawnPoint(zone, surfaceType, safeRadius)
safeRadius = safeRadius or 100
-- Only two surface types are really useful to us: land and water
if surfaceType == land.SurfaceType.SHALLOW_WATER then
surfaceType = land.SurfaceType.WATER
elseif surfaceType == land.SurfaceType.ROAD or surfaceType == land.SurfaceType.RUNWAY then
surfaceType = land.SurfaceType.LAND
end
local loopsLeft = 512
while loopsLeft > 0 do
local basePoint = DCSEx.zones.getRandomPointInside(zone)
if not surfaceType then -- Nil surfaceType means: any point is fine
return basePoint
end
if surfaceType == land.SurfaceType.WATER then
if basePoint and land.getSurfaceType(basePoint) == land.SurfaceType.WATER then
return basePoint
end
else
if safeRadius <= 0 then
return basePoint
end
basePoint = DCSEx.math.vec2ToVec3(basePoint, "land")
local spawnPoints = Disposition.getSimpleZones(basePoint, math.max(1852, safeRadius * 5), safeRadius, 1)
if #spawnPoints > 0 and land.getSurfaceType(spawnPoints[1]) == land.SurfaceType.LAND then
return spawnPoints[1]
end
end
loopsLeft = loopsLeft - 1
end
return nil
end
-- TODO: description
function DCSEx.world.getSceneriesInZone(center, radius, minHealth)
minHealth = minHealth or 0
local sceneries = {}
local scenerySearchvolume = {
id = world.VolumeType.SPHERE,
params = { point = { x = center.x, y = 0, z = center.y }, radius = radius }
}
local function ifSceneryFound(foundScenery, val)
local desc = foundScenery:getDesc()
if desc and desc.attributes and desc.attributes.Buildings then
if desc.life >= minHealth then
table.insert(sceneries, foundScenery)
end
end
return true
end
world.searchObjects(Object.Category.SCENERY, scenerySearchvolume, ifSceneryFound)
return sceneries
end
-- TODO: description
function DCSEx.world.getTerrainHeightDiff(coord, searchRadius)
local samples = {}
searchRadius = searchRadius or 5
samples[#samples + 1] = land.getHeight(coord)
for i = 0, 360, 30 do
samples[#samples + 1] = land.getHeight({x = (coord.x + (math.sin(math.rad(i))*searchRadius)), y = (coord.y + (math.cos(math.rad(i))*searchRadius))})
if searchRadius >= 20 then -- if search radius is sorta large, take a sample halfway between center and outer edge
samples[#samples + 1] = land.getHeight({x = (coord.x + (math.sin(math.rad(i))*(searchRadius/2))), y = (coord.y + (math.cos(math.rad(i))*(searchRadius/2)))})
end
end
local tMax, tMin = 0, 1000000
for _, height in pairs(samples) do
if height > tMax then
tMax = height
end
if height < tMin then
tMin = height
end
end
return tMax - tMin
end
function DCSEx.world.getUnitsCenter(units)
if not units or #units == 0 then return { x = 0, y = 0 } end
local center = { x = 0, y = 0 }
for _,u in pairs(units) do
local uPt2 = DCSEx.math.vec3ToVec2(u:getPoint())
center.x = center.x + uPt2.x
center.y = center.y + uPt2.y
end
center.x = center.x / #units
center.y = center.y / #units
return center
end
-------------------------------------
-- Searches and return an unit by its ID
-- @param unitID ID of the unit
-- @return An unit, or nil if no unit with this ID was found
-------------------------------------
function DCSEx.world.getUnitByID(unitID)
for coalitionID = 1, 2 do
for _, g in pairs(coalition.getGroups(coalitionID)) do
local units = g:getUnits()
for _, u in pairs(units) do
if DCSEx.dcs.getObjectIDAsNumber(u) == unitID then
return u
end
end
end
end
return nil
end
-------------------------------------
-- Searches and return a coalition static object by its ID
-- @param unitID ID of the static object
-- @return An static object, or nil if no unit with this ID was found
-------------------------------------
function DCSEx.world.getStaticObjectByID(staticID)
for coalitionID = 1,2 do
for _,s in pairs(coalition.getStaticObjects(coalitionID)) do
if DCSEx.dcs.getObjectIDAsNumber(s) == staticID then
return s
end
end
end
return nil
end
-- TODO: description
function DCSEx.world.isGroupAlive(g, unitsMustBeInAir)
if not g then return false end
if not g:isExist() then return false end
if g:getSize() == 0 then return false end
unitsMustBeInAir = unitsMustBeInAir or false
local atLeastOneActiveUnit = false
local units = g:getUnits()
if not units or #units == 0 then return false end
for _,u in pairs(units) do
if u:isExist() and u:getLife() > 0 then
if u:inAir() or not unitsMustBeInAir then
atLeastOneActiveUnit = true
end
end
end
return atLeastOneActiveUnit
end
-- TODO: description & file header
function DCSEx.world.setUnitLifePercent(unitID, life)
net.dostring_in("mission", string.format("a_unit_set_life_percentage(%d, %f)", unitID, life))
end
-- TODO: description & file header
function DCSEx.world.explodeUnit(unitID, amount)
net.dostring_in("mission", string.format("a_explosion_unit(%d, %f)", unitID, amount))
end
function DCSEx.world.destroyGroupByID(groupID)
if not groupID then return end
local g = DCSEx.world.getGroupByID(groupID)
if g then g:destroy() end
end
-- function DCSEx.world.destroySceneryInZone(zone, destructionPercent)
-- destructionPercent = destructionPercent or 0
-- net.dostring_in("mission", string.format("a_scenery_destruction_zone(%d, %f)", zone.zoneId, destructionPercent))
-- end
-- function DCSEx.world.highlightUnit(unitID, enabled)
-- if enabled then
-- enabled = "true"
-- else
-- enabled = "false"
-- end
-- net.dostring_in("mission", string.format("a_unit_highlight(%d, %s)", unitID, enabled))
-- end
-- function DCSEx.world.shellingZone(zone, tnt, shellsCount)
-- net.dostring_in("mission", string.format("a_shelling_zone(%d, %f, %d)", zone.zoneId, tnt, shellsCount))
-- end
end

View File

@@ -0,0 +1,305 @@
-- ====================================================================================
-- ZONETOOLS - FUNCTIONS RELATED TO MAP TRIGGER ZONES
-- ====================================================================================
-- DCSEx.zones.drawOnMap(zoneTable, lineColor, fillColor, lineType, drawName, readonly)
-- DCSEx.zones.getAll()
-- DCSEx.zones.getByName(name)
-- DCSEx.zones.getCenter(zoneTable)
-- DCSEx.zones.getProperty(zoneTable, propertyName)
-- DCSEx.zones.getPropertyBoolean(zoneTable, propertyName, defaultValue)
-- DCSEx.zones.getPropertyFloat(zoneTable, propertyName, defaultValue, min, max)
-- DCSEx.zones.getPropertyInt(zoneTable, propertyName, defaultValue, min, max)
-- DCSEx.zones.getPropertyParse(zoneTable, propertyName, stringTable, valueTable, defaultValue)
-- DCSEx.zones.getPropertyTable(zoneTable, propertyName)
-- DCSEx.zones.getRadius(zoneTable, useMaxForQuads)
-- DCSEx.zones.getSurfaceArea(zoneTable)
-- DCSEx.zones.isPointInside(zoneTable, point)
-- ====================================================================================
DCSEx.zones = { }
-- TODO: function description
function DCSEx.zones.drawOnMap(zoneTable, lineColor, fillColor, lineType, drawName, readOnly)
drawName = drawName or false
readOnly = readOnly or true
if not zoneTable then return end
local markerID = DCSEx.world.getNextMarkerID()
-- Draw shapes on the F10 map
if zoneTable.type == 2 then -- Zone is a quad
trigger.action.quadToAll(
-1,
markerID,
DCSEx.math.vec2ToVec3(zoneTable.verticies[1]),
DCSEx.math.vec2ToVec3(zoneTable.verticies[2]),
DCSEx.math.vec2ToVec3(zoneTable.verticies[3]),
DCSEx.math.vec2ToVec3(zoneTable.verticies[4]),
lineColor,
fillColor,
lineType,
readOnly
)
else -- Zone is a circle
trigger.action.circleToAll(
-1,
markerID,
DCSEx.math.vec2ToVec3(zoneTable),
zoneTable.radius,
lineColor,
fillColor,
lineType,
readOnly
)
end
if drawName then
local markerIDText = DCSEx.world.getNextMarkerID()
trigger.action.textToAll(-1, markerIDText, DCSEx.math.vec2ToVec3(zoneTable), { 1, 1, 1, 1 }, { 0, 0, 0, .5 }, 18, readOnly, zoneTable.name)
return { markerID, markerIDText }
end
return markerID
end
-------------------------------------
-- Returns all trigger zones
-- @return Table of zones
-------------------------------------
function DCSEx.zones.getAll()
if not env.mission.triggers then
return {}
end
if not env.mission.triggers.zones then
return {}
end
return DCSEx.table.deepCopy(env.mission.triggers.zones)
end
-------------------------------------
-- Finds and return a trigger zone by a certain name
-- @param name Case-insensitive name of the zone
-- @return Zone table or nil if no zone with this name was found
-------------------------------------
function DCSEx.zones.getByName(name)
if not name then return nil end
if not env.mission.triggers then return nil end
if not env.mission.triggers.zones then return nil end
name = name:lower()
for _, z in pairs(env.mission.triggers.zones) do
if z.name:lower() == name then
return DCSEx.table.deepCopy(z)
end
end
return nil
end
-------------------------------------
-- Returns the center of a zone
-- @param zoneTable The zone table, returned by TMMissionData.getZones() or TMMissionData.getZoneByName(name)
-- @return A vec2
-------------------------------------
function DCSEx.zones.getCenter(zoneTable)
if not zoneTable then return nil end
local x = zoneTable.x or 0
local y = zoneTable.y or 0
if zoneTable.type == 2 then -- Zone is a quad
x = (zoneTable.verticies[1].x + zoneTable.verticies[2].x + zoneTable.verticies[3].x + zoneTable.verticies[4].x) / 4
y = (zoneTable.verticies[1].y + zoneTable.verticies[2].y + zoneTable.verticies[3].y + zoneTable.verticies[4].y) / 4
end
return { x = x, y = y }
end
-------------------------------------
-- Returns the value of the property of a trigger zone, as a string
-- @param zoneTable The zone table, returned by DCSEx.zones.getAll() or DCSEx.zones.getByName(name)
-- @param propertyName Case-insensitive name of the property
-- @return The value of the property or nil if it doesn't exist
-------------------------------------
function DCSEx.zones.getProperty(zoneTable, propertyName, defaultValue)
if not propertyName then return defaultValue end
if not zoneTable then return defaultValue end
if not zoneTable.properties then return defaultValue end
propertyName = propertyName:lower()
for _, p in pairs(zoneTable.properties) do
if p.key:lower() == propertyName then
return (p.value or defaultValue):lower()
end
end
return defaultValue
end
-------------------------------------
-- Returns the value of the property of a trigger zone, parsed against a case-insensitive table of strings
-- @param zoneTable The zone table, returned by DCSEx.zones.getAll() or DCSEx.zones.getByName(name)
-- @param propertyName Case-insensitive name of the property
-- @param defaultValue Default value to return if no match was found
-- @return A boolean
-------------------------------------
function DCSEx.zones.getPropertyBoolean(zoneTable, propertyName, defaultValue)
return DCSEx.zones.getPropertyParse(
zoneTable,
propertyName,
{"true", "yes", "1", "on", "enabled", "false", "no", "0", "off", "disabled"},
{true, true, true, true, true, false, false, false, false, false},
defaultValue)
end
-------------------------------------
-- Returns the value of the property of a trigger zone, as a float
-- @param zoneTable The zone table, returned by DCSEx.zones.getAll() or DCSEx.zones.getByName(name)
-- @param propertyName Case-insensitive name of the property
-- @param defaultValue Default value to return if no match was found
-- @param min Minimum value
-- @param max Maximum value
-- @return A float
-------------------------------------
function DCSEx.zones.getPropertyFloat(zoneTable, propertyName, defaultValue, min, max)
local value = tonumber(DCSEx.zones.getProperty(zoneTable, propertyName))
if not value then return defaultValue end
if min then value = math.max(min, value) end
if max then value = math.min(max, value) end
return value
end
-------------------------------------
-- Returns the value of the property of a trigger zone, as an integer
-- @param zoneTable The zone table, returned by DCSEx.zones.getAll() or DCSEx.zones.getByName(name)
-- @param propertyName Case-insensitive name of the property
-- @param defaultValue Default value to return if no match was found
-- @param min Minimum value
-- @param max Maximum value
-- @return An integer
-------------------------------------
function DCSEx.zones.getPropertyInt(zoneTable, propertyName, defaultValue, min, max)
local value = DCSEx.zones.getPropertyFloat(zoneTable, propertyName, defaultValue, min, max)
if not value then return nil end
return math.floor(value)
end
-------------------------------------
-- Gets the value of a property of a trigger zone and parse it according to two correspondance tables
-- @param zoneTable The zone table, returned by DCSEx.zones.getAll() or DCSEx.zones.getByName(name)
-- @param propertyName Case-insensitive name of the property
-- @param stringTable A table of strings
-- @param valueTable A values, matching the strings table's indices
-- @param defaultValue Default value to return if no match was found
-- @return A value
-------------------------------------
function DCSEx.zones.getPropertyParse(zoneTable, propertyName, stringTable, valueTable, defaultValue)
local value = DCSEx.zones.getProperty(zoneTable, propertyName) or ""
value = value:lower()
for i,_ in ipairs(stringTable) do
if value == stringTable[i]:lower() then
return valueTable[i]
end
end
return defaultValue
end
-------------------------------------
-- Returns the value of the property of a trigger zone, as a table of comma-separated lowercase strings
-- @param zoneTable The zone table, returned by DCSEx.zones.getAll() or DCSEx.zones.getByName(name)
-- @param propertyName Case-insensitive name of the property
-- @return An table
-------------------------------------
function DCSEx.zones.getPropertyTable(zoneTable, propertyName)
local value = DCSEx.zones.getProperty(zoneTable, propertyName)
if not value then return {} end
return string.split(value:lower(), ",")
end
-------------------------------------
-- Returns the radius of a zone, in meter
-- @param zoneTable The zone table, returned by DCSEx.zones.getAll() or DCSEx.zones.getByName(name)
-- @param useMaxForQuads If true, return largest distance between the center and a vertex. If false (default value), returns the mean distance. Only used if the zone is a quad.
-- @return An table
-------------------------------------
function DCSEx.zones.getRadius(zoneTable, useMaxForQuads)
if not zoneTable then return 0 end
useMaxForQuads = useMaxForQuads or false
local radius = 0
if zoneTable.type == 2 then -- Zone is a quad
for _,v in ipairs(zoneTable.verticies) do
if useMaxForQuads then
radius = math.max(radius, DCSEx.math.getDistance2D(zoneTable, v))
else
radius = radius + DCSEx.math.getDistance2D(zoneTable, v)
end
end
if #zoneTable.verticies > 0 and not useMaxForQuads then
radius = radius / #zoneTable.verticies
end
else
radius = zoneTable.radius
end
return radius
end
-- TODO: description + file header
function DCSEx.zones.getRandomPointInside(zoneTable, surfaceType)
local radius = DCSEx.zones.getRadius(zoneTable)
for _=1,64 do
local point = DCSEx.math.randomPointInCircle(zoneTable, radius, surfaceType)
if zoneTable.type == 2 then
if not DCSEx.zones.isPointInside(zoneTable, point) then point = nil end
end
if point then return point end
end
return nil
end
-------------------------------------
-- Returns the surface area of a zone
-- @param zoneTable The zone table, returned by TMMissionData.getZones() or TMMissionData.getZoneByName(name)
-- @return A number, in squared meters
-------------------------------------
function DCSEx.zones.getSurfaceArea(zoneTable)
if not zoneTable then return 0 end
if zoneTable.type == 2 then -- Zone is a quad
if not zoneTable.verticies then return 0 end
local area = zoneTable.verticies[1].x * zoneTable.verticies[2].y + zoneTable.verticies[2].x * zoneTable.verticies[3].y + zoneTable.verticies[3].x * zoneTable.verticies[4].y + zoneTable.verticies[4].x * zoneTable.verticies[1].y - zoneTable.verticies[2].x * zoneTable.verticies[1].y - zoneTable.verticies[3].x * zoneTable.verticies[2].y - zoneTable.verticies[4].x * zoneTable.verticies[3].y - zoneTable.verticies[1].x * zoneTable.verticies[4].y
return math.abs(area) / 2
else -- Zone is a circle
if not zoneTable.radius then return 0 end
return (zoneTable.radius ^ 2) * math.pi
end
end
-------------------------------------
-- Returns true if a point is inside a zone
-- @param zoneTable The zone table, returned by TMMissionData.getZones() or TMMissionData.getZoneByName(name)
-- @param point A point, as a vec3 or vec2
-- @return True if the point is inside the zone, false otherwise
-------------------------------------
function DCSEx.zones.isPointInside(zoneTable, point)
if not point then return false end
if point.z then point = DCSEx.math.vec3ToVec2(point) end -- Point was a vec3, convert to vec2
if zoneTable.type == 2 then -- Zone is a quad
return DCSEx.math.isPointInsidePolygon(zoneTable.verticies, point)
else -- Zone is a circle
return DCSEx.math.isPointInsideCircle({x = zoneTable.x, y = zoneTable.y}, zoneTable.radius, point)
end
end