mirror of
https://github.com/weyne85/DML.git
synced 2025-10-29 16:57:49 +00:00
3768 lines
120 KiB
Lua
3768 lines
120 KiB
Lua
cfxZones = {}
|
|
cfxZones.version = "4.4.4"
|
|
|
|
-- cf/x zone management module
|
|
-- reads dcs zones and makes them accessible and mutable
|
|
-- by scripting.
|
|
--
|
|
-- Copyright (c) 2021 - 2024 by Christian Franz and cf/x AG
|
|
--
|
|
|
|
--[[-- VERSION HISTORY
|
|
- 4.1.0 - getBoolFromZoneProperty 'on/off' support for dml variant as well
|
|
- 4.1.1 - evalRemainder() updates
|
|
- 4.1.2 - hash property missing warning
|
|
- 4.2.0 - new createRandomPointInPopulatedZone()
|
|
- 4.3.0 - boolean supports maybe, random, rnd, ?
|
|
- small optimization for randomInRange()
|
|
- randomDelayFromPositiveRange also allows 0
|
|
- 4.3.1 - new drawText() for zones
|
|
- dmlZone:getClosestZone() bridge
|
|
- 4.3.2 - new getListFromZoneProperty()
|
|
- 4.3.3 - hardened calculateZoneBounds
|
|
- 4.3.4 - rewrote zone bounds for poly zones
|
|
- 4.3.5 - hardened getStringFromZoneProperty against number value returns (WebEd bug)
|
|
- 4.3.6 - tiny optimization in isPointInsideQuad
|
|
- moving zone - hardening code for static objects
|
|
- moving zones - now deriving dx, dy,uHeading from dcsCommon xref for linked zones
|
|
- 4.3.7 - corrected bug in processDynamicValues for lookup table
|
|
- 4.4.0 - dmlZone:getCoalition()
|
|
- dmlZone:getTypeName()
|
|
- dmlZone supports masterOwner by default
|
|
- dmlZone:getCoalition() dereferences masterOwner once
|
|
-4.4.1 - better verbosity for error in doPollFlag()
|
|
-4.4.2 - twn support for wildcards <twn: > and <loc:>
|
|
-4.4.3 - property name is trimmed (double check)
|
|
-4.4.4 - createGroundUnitsInZoneForCoalition supports drivable
|
|
--]]--
|
|
|
|
--
|
|
-- ====================
|
|
-- OOP dmlZone API HERE
|
|
-- ====================
|
|
--
|
|
|
|
dmlZone = {}
|
|
function dmlZone:new(o)
|
|
o = o or {}
|
|
setmetatable(o, self)
|
|
self.__index = self
|
|
self.name = "dmlZone raw"
|
|
self.isCircle = false
|
|
self.isPoly = false
|
|
self.radius = 0
|
|
self.poly = {}
|
|
self.bounds = {}
|
|
self.properties = {}
|
|
return o
|
|
end
|
|
|
|
-- dmlZone compatibility with DCS MSE objects:
|
|
-- dmlZone:getName() -- returns zone.name attribute (from ME)
|
|
-- dmlZone:getPoint() -- returns current point or dmlPoint
|
|
-- dmlZone:getTypeName() -- returns "dmlZone"
|
|
-- dmlZone:getCoalition -- returns owner
|
|
|
|
|
|
--
|
|
-- CLASSIC INTERFACE
|
|
--
|
|
cfxZones.verbose = false
|
|
cfxZones.caseSensitiveProperties = false -- set to true to make property names case sensitive
|
|
cfxZones.ups = 1 -- updates per second. updates moving zones
|
|
|
|
cfxZones.zones = {} -- these are the zone as retrieved from the mission.
|
|
-- ALWAYS USE THESE, NEVER DCS's ZONES!!!!
|
|
|
|
function cfxZones.readFromDCS(clearfirst)
|
|
if (clearfirst) then
|
|
cfxZones.zones = {}
|
|
end
|
|
-- not all missions have triggers or zones
|
|
if not env.mission.triggers then
|
|
if cfxZones.verbose then
|
|
trigger.action.outText("cf/x zones: no env.triggers defined", 10)
|
|
end
|
|
return
|
|
end
|
|
|
|
if not env.mission.triggers.zones then
|
|
if cfxZones.verbose then
|
|
trigger.action.outText("cf/x zones: no zones defined", 10)
|
|
end
|
|
return;
|
|
end
|
|
|
|
-- we only retrieve the data we need. At this point it is name, location and radius
|
|
-- and put this in our own little structure. we also convert to all upper case name for index
|
|
-- and assume that the name may also carry meaning, e.g. 'LZ:' defines a landing zone
|
|
-- so we can quickly create other sets from this
|
|
-- zone object. DCS 2.7 introduced quads, so this is supported as well
|
|
-- name - name in upper case
|
|
-- isCircle - true if circular zone
|
|
-- isPoly - true if zone is defined by convex polygon, e.g. quad
|
|
-- point - vec3 (x 0 z) - zone's in-world center, used to place the coordinate
|
|
-- radius - number, zero when quad
|
|
-- bounds - aabb with attributes ul, ur, ll, lr (upper left .. lower right) as (x, 0, z)
|
|
-- poly - array 1..n of poly points, wound counter-clockwise
|
|
|
|
for i, dcsZone in pairs(env.mission.triggers.zones) do
|
|
if type(dcsZone) == 'table' then -- hint taken from MIST: verify type when reading from dcs
|
|
-- dcs data is like a box of chocolates...
|
|
local newZone = dmlZone:new(nil) -- WAS: {} -- OOP introduction July 2023
|
|
-- name, converted to upper is used only for indexing
|
|
-- the original name remains untouched
|
|
newZone.dcsZone = dcsZone
|
|
newZone.name = dcsZone.name
|
|
newZone.isCircle = false
|
|
newZone.isPoly = false
|
|
newZone.radius = 0
|
|
newZone.poly = {}
|
|
newZone.bounds = {}
|
|
newZone.properties = {} -- dcs has this too, copy if present
|
|
if dcsZone.properties then
|
|
newZone.properties = dcsZone.properties
|
|
else
|
|
newZone.properties = {}
|
|
end -- WARNING: REF COPY. May need to clone
|
|
--[[--
|
|
trigger.action.outText("zone <> properties:trimmed", 30)
|
|
local msg = "["
|
|
for idx, val in pairs(newZone.properties) do
|
|
msg = msg .. "<" .. val.key .. ">:<" .. dcsCommon.trim(val.key) .. ">, "
|
|
end
|
|
trigger.action.outText(msg, 30)
|
|
--]]--
|
|
local upperName = newZone.name:upper()
|
|
|
|
-- location as 'point'
|
|
-- WARNING: zones locs are 2D (x,y) pairs, while y in DCS is altitude.
|
|
-- so we need to change (x,y) into (x, 0, z). Since Zones have no
|
|
-- altitude (they are an infinite cylinder) this works. Remember to
|
|
-- drop y from zone calculations to see if inside.
|
|
-- WARNING: ME linked zones have a relative x any y
|
|
-- to the linked unit
|
|
if dcsZone.linkUnit then
|
|
-- calculate the zone's real position by accessing the unit's MX data
|
|
-- as precached by dcsCommon
|
|
local ux, uy = dcsCommon.getUnitStartPosByID(dcsZone.linkUnit)
|
|
newZone.point = dcsCommon.createPoint(ux + dcsZone.x, 0, uy + dcsZone.y)
|
|
newZone.dcsOrigin = dcsCommon.createPoint(ux + dcsZone.x, 0, uy + dcsZone.y)
|
|
else
|
|
newZone.point = dcsCommon.createPoint(dcsZone.x, 0, dcsZone.y)
|
|
newZone.dcsOrigin = dcsCommon.createPoint(dcsZone.x, 0, dcsZone.y)
|
|
end
|
|
|
|
-- start type processing. if zone.type exists, we have a mission
|
|
-- created with 2.7 or above, else earlier
|
|
local zoneType = 0
|
|
if (dcsZone.type) then
|
|
zoneType = dcsZone.type
|
|
end
|
|
|
|
if zoneType == 0 then
|
|
-- circular zone
|
|
newZone.isCircle = true
|
|
newZone.radius = dcsZone.radius
|
|
newZone.maxRadius = newZone.radius -- same for circular
|
|
|
|
elseif zoneType == 2 then
|
|
-- polyZone
|
|
newZone.isPoly = true
|
|
newZone.radius = dcsZone.radius -- radius is still written in DCS, may change later. The radius has no meaning and is the last radius written before zone changed to poly.
|
|
-- note that newZone.point is only inside the tone for
|
|
-- convex polys, and DML only correctly works with convex polys
|
|
-- now transfer all point in the poly
|
|
-- note: DCS in 2.7 misspells vertices as 'verticies'
|
|
-- correct for this
|
|
newZone.maxRadius = 0
|
|
local verts = {}
|
|
if dcsZone.verticies then verts = dcsZone.verticies
|
|
else
|
|
-- in later versions, this was corrected
|
|
verts = dcsZone.vertices -- see if this is ever called
|
|
end
|
|
|
|
for v=1, #verts do
|
|
local dcsPoint = verts[v]
|
|
local polyPoint = cfxZones.createPointFromDCSPoint(dcsPoint) -- (x, y) --> (x, 0, y-->z)
|
|
newZone.poly[v] = polyPoint
|
|
-- measure distance from zone's point, and store maxRadius
|
|
-- dcs always saves a point with the poly zone
|
|
local dist = dcsCommon.dist(newZone.point, polyPoint)
|
|
if dist > newZone.maxRadius then newZone.maxRadius = dist end
|
|
end
|
|
else
|
|
|
|
trigger.action.outText("cf/x zones: malformed zone #" .. i .. " unknown type " .. zoneType, 10)
|
|
end
|
|
|
|
|
|
-- calculate bounds
|
|
cfxZones.calculateZoneBounds(newZone)
|
|
|
|
-- add to my table
|
|
cfxZones.zones[upperName] = newZone -- WARNING: UPPER ZONE!!!
|
|
|
|
else
|
|
if cfxZones.verbose then
|
|
trigger.action.outText("cf/x zones: malformed zone #" .. i .. " dropped", 10)
|
|
end
|
|
end -- else var not a table
|
|
|
|
end -- for all zones kvp
|
|
end -- readFromDCS
|
|
|
|
function cfxZones.calculateZoneBounds(theZone)
|
|
if not (theZone) then return
|
|
end
|
|
|
|
local bounds = theZone.bounds -- copy ref! -- DON'T BELIEVE THIS!
|
|
|
|
if theZone.isCircle then
|
|
-- aabb are easy: center +/- radius
|
|
local center = theZone.point
|
|
local radius = theZone.radius
|
|
-- dcs uses z+ is down on map
|
|
-- upper left is center - radius
|
|
bounds.ul = dcsCommon.createPoint(center.x - radius, 0, center.z - radius)
|
|
bounds.ur = dcsCommon.createPoint(center.x + radius, 0, center.z - radius)
|
|
bounds.ll = dcsCommon.createPoint(center.x - radius, 0, center.z + radius)
|
|
bounds.lr = dcsCommon.createPoint(center.x + radius, 0, center.z + radius)
|
|
|
|
-- write back
|
|
theZone.bounds = bounds
|
|
elseif theZone.isPoly then
|
|
local poly = theZone.poly -- ref copy!
|
|
-- create the four points
|
|
local p = cfxZones.createPointFromPoint(poly[1])
|
|
local pRad = dcsCommon.dist(theZone.point, poly[1]) -- rRad is radius for polygon from theZone.point
|
|
|
|
-- now iterate through all points and adjust bounds accordingly
|
|
local lx, ly, mx, my = p.x, p.z, p.x, p.z
|
|
for vtx=1, #poly do
|
|
local v = poly[vtx]
|
|
if v.x < lx then lx = v.x end
|
|
if v.x > mx then mx = v.x end
|
|
if v.z < ly then ly = v.z end
|
|
if v.z > my then my = v.z end
|
|
local dp = dcsCommon.dist(theZone.point, v)
|
|
if dp > pRad then pRad = dp end -- find largst distance to vertex
|
|
end
|
|
|
|
theZone.bounds.ul = dcsCommon.createPoint(lx, 0, my)
|
|
theZone.bounds.ur = dcsCommon.createPoint(mx, 0, my)
|
|
theZone.bounds.ll = dcsCommon.createPoint(lx, 0, ly)
|
|
theZone.bounds.lr = dcsCommon.createPoint(mx, 0, ly)
|
|
-- store pRad
|
|
theZone.pRad = pRad -- not sure we'll ever need that, but at least we have it
|
|
else
|
|
-- huston, we have a problem
|
|
if cfxZones.verbose then
|
|
trigger.action.outText("cf/x zones: calc bounds: zone " .. theZone.name .. " has unknown type", 30)
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
function dmlZone:calculateZoneBounds()
|
|
cfxZones.calculateZoneBounds(self)
|
|
end
|
|
|
|
function cfxZones.createPoint(x, y, z) -- bridge to dcsCommon, backward comp.
|
|
return dcsCommon.createPoint(x, y, z)
|
|
end
|
|
|
|
function cfxZones.copyPoint(inPoint) -- bridge to dcsCommon, backward comp.
|
|
return dcsCommon.copyPoint(inPoint)
|
|
end
|
|
|
|
function cfxZones.createHeightCorrectedPoint(inPoint) -- this should be in dcsCommon
|
|
local cP = dcsCommon.createPoint(inPoint.x, land.getHeight({x=inPoint.x, y=inPoint.z}),inPoint.z)
|
|
return cP
|
|
end
|
|
|
|
function cfxZones.getHeightCorrectedZonePoint(theZone)
|
|
local thePoint = cfxZone.getPoint(theZone)
|
|
return cfxZones.createHeightCorrectedPoint(thePoint)
|
|
end
|
|
|
|
function dmlZone:getHeightCorrectedZonePoint()
|
|
local thePoint = self:getPoint()
|
|
return dcsCommon.createPoint(thePoint.x, land.getHeight({x=thePoint.x, y=thePoint.z}),thePoint.z)
|
|
end
|
|
|
|
function cfxZones.createPointFromPoint(inPoint)
|
|
return cfxZones.copyPoint(inPoint)
|
|
end
|
|
|
|
function cfxZones.createPointFromDCSPoint(inPoint)
|
|
return dcsCommon.createPoint(inPoint.x, 0, inPoint.y)
|
|
end
|
|
|
|
|
|
function cfxZones.createRandomPointInsideBounds(bounds)
|
|
-- warning: bounds do not move woth zone! may have to be updated
|
|
local x = math.random(bounds.ll.x, ur.x)
|
|
local z = math.random(bounds.ll.z, ur.z)
|
|
return dcsCommon.createPoint(x, 0, z)
|
|
end
|
|
|
|
function cfxZones.createRandomPointOnZoneBoundary(theZone)
|
|
if not theZone then return nil end
|
|
if theZone.isPoly then
|
|
local loc, dx, dy = cfxZones.createRandomPointInPolyZone(theZone, true)
|
|
return loc, dx, dy
|
|
else
|
|
local loc, dx, dy = cfxZones.createRandomPointInCircleZone(theZone, true)
|
|
return loc, dx, dy
|
|
end
|
|
end
|
|
|
|
function dmlZone:createRandomPointOnZoneBoundary()
|
|
return cfxZones.createRandomPointOnZoneBoundary(self)
|
|
end
|
|
|
|
function cfxZones.createRandomPointInZone(theZone)
|
|
if not theZone then return nil end
|
|
if theZone.isPoly then
|
|
local loc, dx, dy = cfxZones.createRandomPointInPolyZone(theZone)
|
|
return loc, dx, dy
|
|
else
|
|
local loc, dx, dy = cfxZones.createRandomPointInCircleZone(theZone)
|
|
return loc, dx, dy
|
|
end
|
|
end
|
|
|
|
function dmlZone:createRandomPointInZone()
|
|
local loc, dx, dy = cfxZones.createRandomPointInZone(self)
|
|
return loc, dx, dy
|
|
end
|
|
|
|
|
|
function cfxZones.randomPointInZone(theZone)
|
|
local loc, dx, dy = cfxZones.createRandomPointInZone(theZone)
|
|
return loc, dx, dy
|
|
end
|
|
|
|
function dmlZone:randomPointInZone()
|
|
local loc, dx, dy = cfxZones.createRandomPointInZone(self)
|
|
return loc, dx, dy
|
|
end
|
|
|
|
function cfxZones.createRandomPointInCircleZone(theZone, onEdge)
|
|
if not theZone.isCircle then
|
|
trigger.action.outText("+++Zones: warning - createRandomPointInCircleZone called for non-circle zone <" .. theZone.name .. ">", 30)
|
|
return {x=theZone.point.x, y=0, z=theZone.point.z}
|
|
end
|
|
|
|
-- ok, let's first create a random percentage value for the new radius
|
|
-- now lets get a random degree
|
|
local degrees = math.random() * 2 * 3.14152 -- radiants.
|
|
local r = theZone.radius
|
|
if not onEdge then
|
|
r = r * math.random()
|
|
end
|
|
local p = cfxZones.getPoint(theZone) -- force update of zone if linked
|
|
local dx = r * math.cos(degrees)
|
|
local dz = r * math.sin(degrees)
|
|
local px = p.x + dx -- r * math.cos(degrees)
|
|
local pz = p.z + dz -- r * math.sin(degrees)
|
|
return {x=px, y=0, z = pz}, dx, dz -- returns loc and offsets to theZone.point
|
|
end
|
|
|
|
function dmlZone:createRandomPointInCircleZone(theZone, onEdge)
|
|
local p, dx, dz = cfxZones.createRandomPointInCircleZone(self, onEdge)
|
|
return p, dx, dz
|
|
end
|
|
|
|
function cfxZones.createRandomPointInPolyZone(theZone, onEdge)
|
|
if not theZone.isPoly then
|
|
trigger.action.outText("+++Zones: warning - createRandomPointInPolyZone called for non-poly zone <" .. theZone.name .. ">", 30)
|
|
return dcsCommon.createPoint(theZone.point.x, 0, theZone.point.z)
|
|
end
|
|
-- force update of all points
|
|
local p = cfxZones.getPoint(theZone)
|
|
|
|
-- point in convex poly: choose two different lines from that polygon
|
|
local lineIdxA = dcsCommon.smallRandom(#theZone.poly)
|
|
repeat lineIdxB = dcsCommon.smallRandom(#theZone.poly) until (lineIdxA ~= lineIdxB)
|
|
|
|
-- we now have two different lines. pick a random point on each.
|
|
-- we use lerp to pick any point between a and b
|
|
local a = theZone.poly[lineIdxA]
|
|
lineIdxA = lineIdxA + 1 -- get next point in poly and wrap around
|
|
if lineIdxA > #theZone.poly then lineIdxA = 1 end
|
|
local b = theZone.poly[lineIdxA]
|
|
local randompercent = math.random()
|
|
local sourceA = dcsCommon.vLerp (a, b, randompercent)
|
|
-- if all we want is a point on an edge, we are done
|
|
if onEdge then
|
|
local polyPoint = sourceA
|
|
return polyPoint, polyPoint.x - p.x, polyPoint.z - p.z -- return loc, dx, dz
|
|
end
|
|
|
|
-- now get point on second line
|
|
a = theZone.poly[lineIdxB]
|
|
lineIdxB = lineIdxB + 1 -- get next point in poly and wrap around
|
|
if lineIdxB > #theZone.poly then lineIdxB = 1 end
|
|
b = theZone.poly[lineIdxB]
|
|
randompercent = math.random()
|
|
local sourceB = dcsCommon.vLerp (a, b, randompercent)
|
|
|
|
-- now take a random point on that line that entirely
|
|
-- runs through the poly
|
|
randompercent = math.random()
|
|
local polyPoint = dcsCommon.vLerp (sourceA, sourceB, randompercent)
|
|
return polyPoint, polyPoint.x - p.x, polyPoint.z - p.z -- return loc, dx, dz
|
|
end
|
|
|
|
function dmlZone:createRandomPointInPolyZone(onEdge)
|
|
local p, dx, dz = cfxZones.createRandomPointInPolyZone(self, onEdge)
|
|
return p, dx, dz
|
|
end
|
|
|
|
function dmlZone:createRandomPointInPopulatedZone(radius, maxTries)
|
|
if not maxTries then maxTries = 20 end
|
|
if not radius then radius = 10 end -- meters
|
|
local cnt = 0
|
|
local p, dx, dz
|
|
repeat
|
|
p, dx, dz = self:createRandomPointInZone() -- p is x, 0, z
|
|
local hits, collector = cfxZones.objectsInRange(p, radius)
|
|
if hits < 1 then return p, dx, dz end
|
|
if hits == 1 then
|
|
local o = collector[1]
|
|
local op = o:getPoint()
|
|
d = dcsCommon.distFlat(op, p)
|
|
-- trigger.action.outText("singleDist = " .. d, 30)
|
|
if d > radius/2 then
|
|
-- trigger.action.outText("good enough, will use", 30)
|
|
return p, dx, dz
|
|
end
|
|
end
|
|
cnt = cnt + 1
|
|
-- trigger.action.outText(hits .. "hits --> failed try " .. cnt, 30)
|
|
until cnt > maxTries
|
|
return p, dx, dz
|
|
end
|
|
|
|
function cfxZones.createRandomPointInPopulatedZone(theZone, radius, maxTries)
|
|
if not theZone then return nil, nil, nil end
|
|
local p, dx, dz = theZone:createRandomPointInPopulatedZone(radius, maxTries)
|
|
return p, dx, dz
|
|
end
|
|
|
|
--[[--
|
|
function dmlZone:createRandomPointInPopulatedZone(radius, maxTries)
|
|
if not maxTries then maxTries = 20 end
|
|
local cnt = 0
|
|
local p, dx, dz
|
|
p, dx, dz = self:createRandomPointInZone() -- p is x, 0, z
|
|
repeat
|
|
local hits = cfxZones.objectsInRange(p, radius)
|
|
if hits < 1 then return p, dx, dz end
|
|
-- move to the right by radius
|
|
p.z = p.z + radius
|
|
dz = dz + radius
|
|
cnt = cnt + 1
|
|
trigger.action.outText("failed try " .. cnt, 30)
|
|
until cnt > maxTries
|
|
return p, dx, dz
|
|
end
|
|
--]]--
|
|
function cfxZones.objectHandler(theObject, theCollector) -- for world.search
|
|
table.insert(theCollector, theObject)
|
|
return true
|
|
end
|
|
|
|
function cfxZones.objectsInRange(pt, range)
|
|
if not range then range = 100 end -- meters
|
|
local allCats = {1, 2, 3, 4, 5, 6} -- all cats
|
|
local lp = {x = pt.x, y = pt.z}
|
|
pt.y = land.getHeight(lp)
|
|
local collector = {}
|
|
-- now build the search argument
|
|
local args = {
|
|
id = world.VolumeType.SPHERE,
|
|
params = {
|
|
point = pt,
|
|
radius = range -- range
|
|
}
|
|
}
|
|
-- now call search
|
|
world.searchObjects(allCats, args, cfxZones.objectHandler, collector)
|
|
-- now filter for distance because search finds too many
|
|
local filtered = {}
|
|
for idx, anObject in pairs(collector) do
|
|
-- calc dist and filter
|
|
local op = anObject:getPoint()
|
|
local dist = dcsCommon.dist(pt, op)
|
|
if dist < range then
|
|
-- local e = {
|
|
-- dist = dist,
|
|
-- o = anObject
|
|
-- }
|
|
-- table.insert(filtered, e)
|
|
table.insert(filtered, anObject)
|
|
end
|
|
end
|
|
|
|
return #filtered, filtered
|
|
end
|
|
|
|
function cfxZones.addZoneToManagedZones(theZone)
|
|
local upperName = string.upper(theZone.name) -- newZone.name:upper()
|
|
cfxZones.zones[upperName] = theZone
|
|
end
|
|
|
|
function dmlZone:addZoneToManagedZones()
|
|
local upperName = string.upper(self.name) -- newZone.name:upper()
|
|
cfxZones.zones[upperName] = self
|
|
end
|
|
|
|
function cfxZones.createUniqueZoneName(inName, searchSet)
|
|
if not inName then return nil end
|
|
if not searchSet then searchSet = cfxZones.zones end
|
|
inName = inName:upper()
|
|
while searchSet[inName] ~= nil do
|
|
inName = inName .. "X"
|
|
end
|
|
return inName
|
|
end
|
|
|
|
function cfxZones.createSimpleZone(name, location, radius, addToManaged)
|
|
if not radius then radius = 10 end
|
|
if not addToManaged then addToManaged = false end
|
|
if not location then
|
|
location = {}
|
|
end
|
|
if not location.x then location.x = 0 end
|
|
if not location.z then location.z = 0 end
|
|
|
|
local newZone = cfxZones.createCircleZone(name, location.x, location.z, radius)
|
|
|
|
if addToManaged then
|
|
cfxZones.addZoneToManagedZones(newZone)
|
|
end
|
|
return newZone
|
|
end
|
|
|
|
function cfxZones.createCircleZone(name, x, z, radius)
|
|
local newZone = dmlZone:new(nil) -- {} OOP compatibility
|
|
newZone.isCircle = true
|
|
newZone.isPoly = false
|
|
newZone.poly = {}
|
|
newZone.bounds = {}
|
|
|
|
newZone.name = name
|
|
newZone.radius = radius
|
|
newZone.point = dcsCommon.createPoint(x, 0, z)
|
|
newZone.dcsOrigin = dcsCommon.createPoint(x, 0, z)
|
|
|
|
-- props
|
|
newZone.properties = {}
|
|
|
|
-- calculate my bounds
|
|
cfxZones.calculateZoneBounds(newZone)
|
|
|
|
return newZone
|
|
end
|
|
|
|
function cfxZones.createSimplePolyZone(name, location, points, addToManaged)
|
|
if not addToManaged then addToManaged = false end
|
|
if not location then
|
|
location = {}
|
|
end
|
|
if not location.x then location.x = 0 end
|
|
if not location.z then location.z = 0 end
|
|
if not location.y then location.y = 0 end
|
|
|
|
local newZone = cfxZones.createPolyZone(name, points, location)
|
|
|
|
if addToManaged then
|
|
cfxZones.addZoneToManagedZones(newZone)
|
|
end
|
|
return newZone
|
|
end
|
|
|
|
function cfxZones.createSimpleQuadZone(name, location, points, addToManaged)
|
|
if not location then
|
|
location = {}
|
|
end
|
|
if not location.x then location.x = 0 end
|
|
if not location.z then location.z = 0 end
|
|
|
|
-- synthesize 4 points if they don't exist
|
|
-- remember: in DCS positive x is up, positive z is right
|
|
if not points then
|
|
points = {}
|
|
end
|
|
if not points[1] then
|
|
-- upper left
|
|
points[1] = {x = location.x-1, y = 0, z = location.z-1}
|
|
end
|
|
if not points[2] then
|
|
-- upper right
|
|
points[2] = {x = location.x-1, y = 0, z = location.z+1}
|
|
end
|
|
if not points[3] then
|
|
-- lower right
|
|
points[3] = {x = location.x+1, y = 0, z = location.z+1}
|
|
end
|
|
if not points[4] then
|
|
-- lower left
|
|
points[4] = {x = location.x+1, y = 0, z = location.z-1}
|
|
end
|
|
|
|
return cfxZones.createSimplePolyZone(name, location, points, addToManaged)
|
|
end
|
|
|
|
function cfxZones.createPolyZone(name, poly, location) -- poly must be array of point type
|
|
local newZone = dmlZone:new(nil) -- {} OOP compatibility
|
|
if not location then location = {x=0, y=0, z=0} end
|
|
newZone.point = dcsCommon.createPoint(location.x, 0, location.z)
|
|
newZone.dcsOrigin = dcsCommon.createPoint(location.x, 0, location.z)
|
|
newZone.isCircle = false
|
|
newZone.isPoly = true
|
|
newZone.poly = {}
|
|
newZone.bounds = {}
|
|
|
|
newZone.name = name
|
|
newZone.radius = 0
|
|
-- copy poly
|
|
for v=1, #poly do
|
|
local theVertex = poly[v]
|
|
newZone.poly[v] = cfxZones.createPointFromPoint(theVertex)
|
|
end
|
|
|
|
-- properties
|
|
newZone.properties = {}
|
|
|
|
cfxZones.calculateZoneBounds(newZone)
|
|
return newZone
|
|
end
|
|
|
|
function cfxZones.createRandomZoneInZone(name, inZone, targetRadius, entirelyInside)
|
|
-- create a new circular zone with center placed inside inZone
|
|
-- if entirelyInside is false, only the zone's center is guaranteed to be inside
|
|
-- inZone.
|
|
-- entirelyInside is not guaranteed for polyzones
|
|
|
|
if inZone.isCircle then
|
|
local sourceRadius = inZone.radius
|
|
if entirelyInside and targetRadius > sourceRadius then targetRadius = sourceRadius end
|
|
if entirelyInside then sourceRadius = sourceRadius - targetRadius end
|
|
|
|
-- ok, let's first create a random percentage value for the new radius
|
|
local percent = 1 / math.random(100)
|
|
-- now lets get a random degree
|
|
local degrees = math.random(360) * 3.14152 / 180 -- ok, it's actually radiants.
|
|
local r = sourceRadius * percent
|
|
local x = inZone.point.x + r * math.cos(degrees)
|
|
local z = inZone.point.z + r * math.sin(degrees)
|
|
-- construct new zone
|
|
local newZone = cfxZones.createCircleZone(name, x, z, targetRadius)
|
|
return newZone
|
|
|
|
elseif inZone.isPoly then
|
|
local newPoint = cfxZones.createRandomPointInPolyZone(inZone)
|
|
-- construct new zone
|
|
local newZone = cfxZones.createCircleZone(name, newPoint.x, newPoint.z, targetRadius)
|
|
return newZone
|
|
|
|
else
|
|
-- zone type unknown
|
|
trigger.action.outText("CreateZoneInZone: unknown zone type for inZone =" .. inZone.name , 10)
|
|
return nil
|
|
end
|
|
end
|
|
|
|
-- polygon inside zone calculations
|
|
|
|
|
|
-- isleft returns true if point P is to the left of line AB
|
|
-- by determining the sign (up or down) of the normal vector of
|
|
-- the two vectors PA and PB in the y coordinate. We arbitrarily define
|
|
-- left as being > 0, so right is <= 0. As long as we always use the
|
|
-- same comparison, it does not matter what up or down mean.
|
|
-- this is important because we don't know if dcs always winds quads
|
|
-- the same way, we must simply assume that they are wound as a polygon
|
|
function cfxZones.isLeftXZ(A, B, P)
|
|
return ((B.x - A.x)*(P.z - A.z) - (B.z - A.z)*(P.x - A.x)) > 0
|
|
end
|
|
|
|
-- returns true/false for inside
|
|
function cfxZones.isPointInsideQuad(thePoint, A, B, C, D)
|
|
-- Inside test (only convex polygons):
|
|
-- point lies on the same side of each quad's vertex AB, BC, CD, DA
|
|
-- how do we find out which side a point lies on? via the cross product
|
|
-- see isLeft below
|
|
|
|
-- so all we need to do is make sure all results of isLeft for all
|
|
-- four sides are the same
|
|
local mustMatch = isLeftXZ(A, B, thePoint) -- all test results must be the same and we are ok
|
|
-- they just must be the same side.
|
|
if (cfxZones.isLeftXZ(B, C, thePoint ~= mustMatch)) then return false end -- on other side than all before
|
|
if (cfxZones.isLeftXZ(C, D, thePoint ~= mustMatch)) then return false end
|
|
if (cfxZones.isLeftXZ(D, A, thePoint ~= mustMatch)) then return false end
|
|
return true
|
|
end
|
|
|
|
-- generalized version of insideQuad, assumes winding of poly, poly convex, poly closed
|
|
function cfxZones.isPointInsidePoly(thePoint, poly)
|
|
local mustMatch = cfxZones.isLeftXZ(poly[1], poly[2], thePoint)
|
|
for v=2, #poly-1 do
|
|
if cfxZones.isLeftXZ(poly[v], poly[v+1], thePoint) ~= mustMatch then return false end
|
|
end
|
|
-- final test
|
|
if cfxZones.isLeftXZ(poly[#poly], poly[1], thePoint) ~= mustMatch then return false end
|
|
|
|
return true
|
|
end;
|
|
|
|
function cfxZones.isPointInsideZone(thePoint, theZone, radiusIncrease)
|
|
-- radiusIncrease only works for circle zones
|
|
if not radiusIncrease then radiusIncrease = 0 end
|
|
local p = {x=thePoint.x, y = 0, z = thePoint.z} -- zones have no altitude
|
|
if (theZone.isCircle) then
|
|
local zp = cfxZones.getPoint(theZone)
|
|
local d = dcsCommon.dist(p, theZone.point)
|
|
return d < theZone.radius + radiusIncrease, d
|
|
end
|
|
|
|
if (theZone.isPoly) then
|
|
--trigger.action.outText("zne: isPointInside: " .. theZone.name .. " is Polyzone!", 30)
|
|
return (cfxZones.isPointInsidePoly(p, theZone.poly)), 0 -- always returns delta 0
|
|
end
|
|
|
|
trigger.action.outText("isPointInsideZone: Unknown zone type for " .. outerZone.name, 10)
|
|
end
|
|
|
|
function dmlZone:isPointInsideZone(thePoint, radiusIncrease) -- warning: param order!
|
|
return cfxZones.isPointInsideZone(thePoint, self, radiusIncrease)
|
|
end
|
|
|
|
-- isZoneInZone returns true if center of innerZone is inside outerZone
|
|
function cfxZones.isZoneInsideZone(innerZone, outerZone)
|
|
local p = cfxZones.getPoint(innerZone)
|
|
return cfxZones.isPointInsideZone(p, outerZone)
|
|
end
|
|
|
|
function dmlZone:isZoneInsideZone(outerZone)
|
|
return cfxZones.isPointInsideZone(self:getPoint(), outerZone)
|
|
end
|
|
|
|
function cfxZones.getZonesContainingPoint(thePoint, testZones) -- return array
|
|
if not testZones then
|
|
testZones = cfxZones.zones
|
|
end
|
|
|
|
local containerZones = {}
|
|
for tName, tData in pairs(testZones) do
|
|
if cfxZones.isPointInsideZone(thePoint, tData) then
|
|
table.insert(containerZones, tData)
|
|
end
|
|
end
|
|
|
|
return containerZones
|
|
end
|
|
|
|
function cfxZones.getFirstZoneContainingPoint(thePoint, testZones)
|
|
if not testZones then
|
|
testZones = cfxZones.zones
|
|
end
|
|
|
|
for tName, tData in pairs(testZones) do
|
|
if cfxZones.isPointInsideZone(thePoint, tData) then
|
|
return tData
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
function cfxZones.getAllZonesInsideZone(superZone, testZones) -- returnes array!
|
|
if not testZones then
|
|
testZones = cfxZones.zones
|
|
end
|
|
|
|
local containedZones = {}
|
|
for zName, zData in pairs(testZones) do
|
|
if cfxZones.isZoneInsideZone(zData, superZone) then
|
|
if zData ~= superZone then
|
|
-- we filter superzone because superzone usually resides
|
|
-- inside itself
|
|
table.insert(containedZones, zData)
|
|
end
|
|
end
|
|
end
|
|
return containedZones
|
|
end
|
|
|
|
function dmlZone:getAllZonesInsideZone(testZones)
|
|
return cfxZones.getAllZonesInsideZone(self, testZones)
|
|
end
|
|
|
|
|
|
function cfxZones.getZonesWithAttributeNamed(attributeName, testZones)
|
|
if not testZones then testZones = cfxZones.zones end
|
|
|
|
local attributZones = {}
|
|
for aName,aZone in pairs(testZones) do
|
|
local attr = cfxZones.getZoneProperty(aZone, attributeName)
|
|
if attr then
|
|
-- this zone has the requested attribute
|
|
table.insert(attributZones, aZone)
|
|
end
|
|
end
|
|
return attributZones
|
|
end
|
|
|
|
--
|
|
-- zone volume management
|
|
--
|
|
|
|
function cfxZones.getZoneVolume(theZone)
|
|
if not theZone then return nil end
|
|
|
|
if (theZone.isCircle) then
|
|
-- create a sphere volume
|
|
local p = cfxZones.getPoint(theZone)
|
|
p.y = land.getHeight({x = p.x, y = p.z})
|
|
local r = theZone.radius
|
|
if r < 10 then r = 10 end
|
|
local vol = {
|
|
id = world.VolumeType.SPHERE,
|
|
params = {
|
|
point = p,
|
|
radius = r
|
|
}
|
|
}
|
|
return vol
|
|
elseif (theZone.isPoly) then
|
|
-- build the box volume, using the zone's bounds ll and ur points
|
|
local lowerLeft = {}
|
|
-- we build x = westerm y = southern, Z = alt
|
|
local alt = land.getHeight({x=theZone.bounds.ll.x, y = theZone.bounds.ll.z}) - 10
|
|
lowerLeft.x = theZone.bounds.ll.x
|
|
lowerLeft.z = theZone.bounds.ll.z
|
|
lowerLeft.y = alt -- we go lower
|
|
|
|
local upperRight = {}
|
|
alt = land.getHeight({x=theZone.bounds.ur.x, y = theZone.bounds.ur.z}) + 10
|
|
upperRight.x = theZone.bounds.ur.x
|
|
upperRight.z = theZone.bounds.ur.z
|
|
upperRight.y = alt -- we go higher
|
|
|
|
-- construct volume
|
|
local vol = {
|
|
id = world.VolumeType.BOX,
|
|
params = {
|
|
min = lowerLeft,
|
|
max = upperRight
|
|
}
|
|
}
|
|
return vol
|
|
else
|
|
trigger.action.outText("zne: unknown zone type for <" .. theZone.name .. ">", 30)
|
|
end
|
|
end
|
|
|
|
function dmlZone:getZoneVolume()
|
|
return cfxZones.getZoneVolume(self)
|
|
end
|
|
|
|
|
|
function cfxZones.declutterZone(theZone)
|
|
if not theZone then return end
|
|
local theVol = cfxZones.getZoneVolume(theZone)
|
|
world.removeJunk(theVol)
|
|
end
|
|
|
|
function dmlZone:declutterZone()
|
|
local theVol = cfxZones.getZoneVolume(self)
|
|
world.removeJunk(theVol)
|
|
end
|
|
|
|
--
|
|
-- units / groups in zone
|
|
--
|
|
function cfxZones.allGroupsInZone(theZone, categ) -- categ is optional, must be code
|
|
-- warning: does not check for existing!
|
|
local inZones = {}
|
|
local coals = {0, 1, 2} -- all coalitions
|
|
for idx, coa in pairs(coals) do
|
|
local allGroups = coalition.getGroups(coa, categ)
|
|
for key, group in pairs(allGroups) do -- iterate all groups
|
|
if cfxZones.isGroupPartiallyInZone(group, theZone) then
|
|
table.insert(inZones, group)
|
|
end
|
|
end
|
|
end
|
|
return inZones
|
|
end
|
|
|
|
function dmlZone:allGroupsInZone(categ)
|
|
return cfxZones.allGroupsInZone(self, categ)
|
|
end
|
|
|
|
function cfxZones.allGroupNamesInZone(theZone, categ) -- categ is optional, must be code
|
|
-- warning: does not check for existing!
|
|
local inZones = {}
|
|
local coals = {0, 1, 2} -- all coalitions
|
|
for idx, coa in pairs(coals) do
|
|
local allGroups = coalition.getGroups(coa, categ)
|
|
for key, group in pairs(allGroups) do -- iterate all groups
|
|
if cfxZones.isGroupPartiallyInZone(group, theZone) then
|
|
table.insert(inZones, group:getName())
|
|
end
|
|
end
|
|
end
|
|
return inZones
|
|
end
|
|
|
|
function dmlZone:allGroupNamesInZone(categ)
|
|
return cfxZones.allGroupNamesInZone(self, categ)
|
|
end
|
|
|
|
function cfxZones.allStaticsInZone(theZone, useOrigin) -- categ is optional, must be code
|
|
-- warning: does not check for existing!
|
|
local inZones = {}
|
|
local coals = {0, 1, 2} -- all coalitions
|
|
for idx, coa in pairs(coals) do
|
|
local allStats = coalition.getStaticObjects(coa)
|
|
for key, statO in pairs(allStats) do -- iterate all groups
|
|
local oP = statO:getPoint()
|
|
if useOrigin then
|
|
if cfxZones.pointInZone(oP, theZone, true) then
|
|
-- use DCS original coords
|
|
table.insert(inZones, statO)
|
|
end
|
|
elseif cfxZones.pointInZone(oP, theZone) then
|
|
table.insert(inZones, statO)
|
|
end
|
|
end
|
|
end
|
|
return inZones
|
|
end
|
|
|
|
function dmlZone:allStaticsInZone(useOrigin)
|
|
return cfxZones.allStaticsInZone(self, useOrigin)
|
|
end
|
|
|
|
|
|
function cfxZones.groupsOfCoalitionPartiallyInZone(coal, theZone, categ) -- categ is optional
|
|
local groupsInZone = {}
|
|
local allGroups = coalition.getGroups(coal, categ)
|
|
for key, group in pairs(allGroups) do -- iterate all groups
|
|
if group:isExist() then
|
|
if cfxZones.isGroupPartiallyInZone(group, theZone) then
|
|
table.insert(groupsInZone, group)
|
|
end
|
|
end
|
|
end
|
|
return groupsInZone
|
|
end
|
|
|
|
function cfxZones.isGroupPartiallyInZone(aGroup, aZone)
|
|
if not aGroup then return false end
|
|
if not aZone then return false end
|
|
|
|
if not aGroup:isExist() then return false end
|
|
local allUnits = aGroup:getUnits()
|
|
for uk, aUnit in pairs (allUnits) do
|
|
if Unit.isExist(aUnit) and aUnit:getLife() > 1 then
|
|
local p = aUnit:getPoint()
|
|
local inzone, percent, dist = cfxZones.pointInZone(p, aZone)
|
|
if inzone then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
function cfxZones.isEntireGroupInZone(aGroup, aZone)
|
|
if not aGroup then return false end
|
|
if not aZone then return false end
|
|
if not aGroup:isExist() then return false end
|
|
local allUnits = aGroup:getUnits()
|
|
for uk, aUnit in pairs (allUnits) do
|
|
if aUnit:isExist() and aUnit:getLife() > 1 then
|
|
local p = aUnit:getPoint()
|
|
if not cfxZones.isPointInsideZone(p, aZone) then
|
|
return false
|
|
end
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
function dmlZone:isEntireGroupInZone(aGroup)
|
|
return cfxZones.isEntireGroupInZone(aGroup, self)
|
|
end
|
|
|
|
--
|
|
-- Zone Manipulation
|
|
--
|
|
|
|
function cfxZones.offsetZone(theZone, dx, dz)
|
|
-- first, update center
|
|
theZone.point.x = theZone.point.x + dx
|
|
theZone.point.z = theZone.point.z + dz
|
|
|
|
-- now process all polygon points - it's empty for circular, so don't worry
|
|
for v=1, #theZone.poly do
|
|
theZone.poly[v].x = theZone.poly[v].x + dx
|
|
theZone.poly[v].z = theZone.poly[v].z + dz
|
|
end
|
|
|
|
-- update zone bounds
|
|
theZone.bounds.ll.x = theZone.bounds.ll.x + dx
|
|
theZone.bounds.lr.x = theZone.bounds.lr.x + dx
|
|
theZone.bounds.ul.x = theZone.bounds.ul.x + dx
|
|
theZone.bounds.ur.x = theZone.bounds.ur.x + dx
|
|
|
|
theZone.bounds.ll.z = theZone.bounds.ll.z + dz
|
|
theZone.bounds.lr.z = theZone.bounds.lr.z + dz
|
|
theZone.bounds.ul.z = theZone.bounds.ul.z + dz
|
|
theZone.bounds.ur.z = theZone.bounds.ur.z + dz
|
|
|
|
end
|
|
|
|
function dmlZone:offsetZone(dx, dz)
|
|
cfxZones.offsetZone(self, dx, dz)
|
|
end
|
|
|
|
|
|
function cfxZones.moveZoneTo(theZone, x, z)
|
|
local dx = x - theZone.point.x
|
|
local dz = z - theZone.point.z
|
|
cfxZones.offsetZone(theZone, dx, dz)
|
|
end;
|
|
|
|
function dmlZone:moveZoneTo(x, z)
|
|
cfxZones.moveZoneTo(self, x, z)
|
|
end
|
|
|
|
function cfxZones.centerZoneOnUnit(theZone, theUnit)
|
|
local thePoint = theUnit:getPoint()
|
|
cfxZones.moveZoneTo(theZone, thePoint.x, thePoint.z)
|
|
end
|
|
|
|
function dmlZone:centerZoneOnUnit(theUnit)
|
|
local thePoint = theUnit:getPoint()
|
|
self:moveZoneTo(thePoint.x, thePoint.z)
|
|
end
|
|
|
|
|
|
function cfxZones.dumpZones(zoneTable)
|
|
if not zoneTable then zoneTable = cfxZones.zones end
|
|
|
|
trigger.action.outText("Zones START", 10)
|
|
for i, zone in pairs(zoneTable) do
|
|
local myType = "unknown"
|
|
if zone.isCircle then myType = "Circle" end
|
|
if zone.isPoly then myType = "Poly" end
|
|
|
|
trigger.action.outText("#".. i .. ": " .. zone.name .. " of type " .. myType, 10)
|
|
end
|
|
trigger.action.outText("Zones end", 10)
|
|
end
|
|
|
|
function cfxZones.keysForTable(theTable)
|
|
local keyset={}
|
|
local n=0
|
|
|
|
for k,v in pairs(tab) do
|
|
n=n+1
|
|
keyset[n]=k
|
|
end
|
|
return keyset
|
|
end
|
|
|
|
|
|
--
|
|
-- return all zones that have a specific named property
|
|
--
|
|
function cfxZones.zonesWithProperty(propertyName, searchSet)
|
|
if not searchSet then searchSet = cfxZones.zones end
|
|
local theZones = {}
|
|
for k, aZone in pairs(searchSet) do
|
|
if not aZone then
|
|
trigger.action.outText("+++zone: nil aZone for " .. k, 30)
|
|
else
|
|
local lU = cfxZones.getZoneProperty(aZone, propertyName)
|
|
if lU then
|
|
table.insert(theZones, aZone)
|
|
end
|
|
end
|
|
end
|
|
return theZones
|
|
end
|
|
|
|
--
|
|
-- return all zones from the zone table that begin with string prefix
|
|
--
|
|
function cfxZones.zonesStartingWithName(prefix, searchSet)
|
|
if not searchSet then searchSet = cfxZones.zones end
|
|
local prefixZones = {}
|
|
prefix = prefix:upper() -- all zones have UPPERCASE NAMES! THEY SCREAM AT YOU
|
|
for name, zone in pairs(searchSet) do
|
|
if dcsCommon.stringStartsWith(name:upper(), prefix) then
|
|
prefixZones[name] = zone -- note: ref copy!
|
|
end
|
|
end
|
|
|
|
return prefixZones
|
|
end
|
|
|
|
--
|
|
-- return all zones from the zone table that begin with the string or set of strings passed in prefix
|
|
-- if you pass 'true' as second (optional) parameter, it will first look for all zones that begin
|
|
-- with '+' and return only those. Use during debugging to force finding a specific zone
|
|
--
|
|
function cfxZones.zonesStartingWith(prefix, searchSet, debugging)
|
|
-- you can force zones by having their name start with "+"
|
|
-- which will force them to return immediately if debugging is true for this call
|
|
|
|
if (debugging) then
|
|
local debugZones = cfxZones.zonesStartingWithName("+", searchSet)
|
|
if not (next(debugZones) == nil) then -- # operator only works on array elements
|
|
--trigger.action.outText("returning zones with prefix <" .. prefix .. ">", 10)
|
|
return debugZones
|
|
end
|
|
end
|
|
|
|
if (type(prefix) == "string") then
|
|
return cfxZones.zonesStartingWithName(prefix, searchSet)
|
|
end
|
|
|
|
local allZones = {}
|
|
for i=1, #prefix do
|
|
-- iterate through all names in prefix set
|
|
local theName = prefix[i]
|
|
local newZones = cfxZones.zonesStartingWithName(theName, searchSet)
|
|
-- add them all to current table
|
|
for zName, zInfo in pairs(newZones) do
|
|
allZones[zName] = zInfo -- will also replace doublets
|
|
end
|
|
end
|
|
|
|
return allZones
|
|
end
|
|
|
|
function cfxZones.getZoneByName(aName, searchSet)
|
|
if not searchSet then searchSet = cfxZones.zones end
|
|
aName = aName:upper()
|
|
return searchSet[aName] -- the joys of key value pairs
|
|
end
|
|
|
|
function cfxZones.getZonesContainingString(aString, searchSet)
|
|
if not searchSet then searchSet = cfxZones.zones end
|
|
aString = string.upper(aString)
|
|
resultSet = {}
|
|
for zName, zData in pairs(searchSet) do
|
|
if aString == string.upper(zData.name) then
|
|
resultSet[zName] = zData
|
|
end
|
|
end
|
|
|
|
end;
|
|
|
|
-- filter zones by range to a point. returns indexed set
|
|
function cfxZones.getZonesInRange(point, range, theZones)
|
|
if not theZones then theZones = cfxZones.zones end
|
|
|
|
local inRangeSet = {}
|
|
for zName, zData in pairs (theZones) do
|
|
if dcsCommon.dist(point, zData.point) < range then
|
|
table.insert(inRangeSet, zData)
|
|
end
|
|
end
|
|
return inRangeSet
|
|
end
|
|
|
|
-- get closest zone returns the zone that is closest to point
|
|
function cfxZones.getClosestZone(point, theZones)
|
|
if not theZones then theZones = cfxZones.zones end
|
|
local lPoint = {x=point.x, y=0, z=point.z}
|
|
local currDelta = math.huge
|
|
local closestZone = nil
|
|
for zName, zData in pairs(theZones) do
|
|
local zPoint = cfxZones.getPoint(zData)
|
|
local delta = dcsCommon.dist(lPoint, zPoint) -- emulate flag compare
|
|
if (delta < currDelta) then
|
|
currDelta = delta
|
|
closestZone = zData
|
|
end
|
|
end
|
|
return closestZone, currDelta
|
|
end
|
|
|
|
function dmlZone:getClosestZone(theZones)
|
|
local closestZone, currDelta = cfxZones.getClosestZone(self:getPoint(), theZones)
|
|
return closestZone, currDelta
|
|
end
|
|
|
|
-- return a random zone from the table passed in zones
|
|
function cfxZones.pickRandomZoneFrom(zones)
|
|
if not zones then zones = cfxZones.zones end
|
|
local indexedZones = dcsCommon.enumerateTable(zones)
|
|
local r = math.random(#indexedZones)
|
|
return indexedZones[r]
|
|
end
|
|
|
|
-- return an zone element by index
|
|
function cfxZones.getZoneByIndex(theZones, theIndex)
|
|
local enumeratedZones = dcsCommon.enumerateTable(theZones)
|
|
if (theIndex > #enumeratedZones) then
|
|
trigger.action.outText("WARNING: zone index " .. theIndex .. " out of bounds - max = " .. #enumeratedZones, 30)
|
|
return nil end
|
|
if (theIndex < 1) then return nil end
|
|
|
|
return enumeratedZones[theIndex]
|
|
end
|
|
|
|
-- place a smoke marker in center of zone, offset by dx, dy
|
|
function cfxZones.markZoneWithSmoke(theZone, dx, dz, smokeColor, alt)
|
|
if not alt then alt = 5 end
|
|
local point = cfxZones.getPoint(theZone) --{} -- theZone.point
|
|
point.x = point.x + dx -- getpoint updates and returns copy
|
|
point.z = point.z + dz
|
|
-- get height at point
|
|
point.y = land.getHeight({x = point.x, y = point.z}) + alt
|
|
-- height-correct
|
|
--local newPoint= {x = point.x, y = land.getHeight({x = point.x, y = point.z}) + 3, z= point.z}
|
|
trigger.action.smoke(point, smokeColor)
|
|
end
|
|
|
|
function dmlZone:markZoneWithSmoke(dx, dz, smokeColor, alt)
|
|
cfxZones.markZoneWithSmoke(self, dx, dz, smokeColor, alt)
|
|
end
|
|
|
|
-- place a smoke marker in center of zone, offset by radius and degrees
|
|
function cfxZones.markZoneWithSmokePolar(theZone, radius, degrees, smokeColor, alt)
|
|
local rads = degrees * math.pi / 180
|
|
local dx = radius * math.sin(rads)
|
|
local dz = radius * math.cos(rads)
|
|
cfxZones.markZoneWithSmoke(theZone, dx, dz, smokeColor, alt)
|
|
end
|
|
|
|
function dmlZone:markZoneWithSmokePolar(radius, degrees, smokeColor, alt)
|
|
cfxZones.markZoneWithSmokePolar(self, radius, degrees, smokeColor, alt)
|
|
end
|
|
|
|
-- place a smoke marker in center of zone, offset by radius and randomized degrees
|
|
function cfxZones.markZoneWithSmokePolarRandom(theZone, radius, smokeColor)
|
|
local degrees = math.random(360)
|
|
cfxZones.markZoneWithSmokePolar(theZone, radius, degrees, smokeColor)
|
|
end
|
|
|
|
function dmlZone:markZoneWithSmokePolarRandom(radius, smokeColor)
|
|
local degrees = math.random(360)
|
|
self:markZoneWithSmokePolar(radius, degrees, smokeColor)
|
|
end
|
|
|
|
function cfxZones.pointInOneOfZones(thePoint, zoneArray, useOrig)
|
|
if not zoneArray then zoneArray = cfxZones.zones end
|
|
for idx, theZone in pairs(zoneArray) do
|
|
local isIn, percent, dist = cfxZones.pointInZone(thePoint, theZone, useOrig)
|
|
if isIn then return isIn, percent, dist, theZone end
|
|
end
|
|
return false, 0, 0, nil
|
|
end
|
|
|
|
|
|
-- unitInZone returns true if theUnit is inside the zone
|
|
-- the second value returned is the percentage of distance
|
|
-- from center to rim, with 100% being entirely in center, 0 = outside
|
|
-- the third value returned is the distance to center
|
|
function cfxZones.pointInZone(thePoint, theZone, useOrig)
|
|
|
|
if not (theZone) then return false, 0, 0 end
|
|
|
|
local pflat = {x = thePoint.x, y = 0, z = thePoint.z}
|
|
|
|
local zpoint
|
|
if useOrig then
|
|
zpoint = cfxZones.getDCSOrigin(theZone)
|
|
else
|
|
zpoint = cfxZones.getPoint(theZone) -- updates zone if linked
|
|
end
|
|
local ppoint = thePoint -- xyz
|
|
local pflat = {x = ppoint.x, y = 0, z = ppoint.z}
|
|
local dist = dcsCommon.dist(zpoint, pflat)
|
|
|
|
if theZone.isCircle then
|
|
if theZone.radius <= 0 then
|
|
return false, 0, 0
|
|
end
|
|
|
|
local success = dist < theZone.radius
|
|
local percentage = 0
|
|
if (success) then
|
|
percentage = 1 - dist / theZone.radius
|
|
end
|
|
return success, percentage, dist
|
|
|
|
elseif theZone.isPoly then
|
|
local success = cfxZones.isPointInsidePoly(pflat, theZone.poly)
|
|
return success, 0, dist
|
|
else
|
|
trigger.action.outText("pointInZone: Unknown zone type for " .. theZone.name, 10)
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
function dmlZone:pointInZone(thePoint, useOrig)
|
|
return cfxZones.pointInZone(thePoint, self, useOrig)
|
|
end
|
|
|
|
|
|
function cfxZones.unitInZone(theUnit, theZone)
|
|
if not (theUnit) then return false, 0, 0 end
|
|
if not (theUnit:isExist()) then return false, 0, 0 end
|
|
-- force zone update if it is linked to another zone
|
|
-- pointInZone does update
|
|
local thePoint = theUnit:getPoint()
|
|
return cfxZones.pointInZone(thePoint, theZone)
|
|
end
|
|
|
|
function dmlZone:unitInZone(theUnit)
|
|
if not (theUnit) then return false, 0, 0 end
|
|
if not (theUnit:isExist()) then return false, 0, 0 end
|
|
-- force zone update if it is linked to another zone
|
|
-- pointInZone does update
|
|
local thePoint = theUnit:getPoint()
|
|
return self:pointInZone(thePoint)
|
|
end
|
|
|
|
-- returns all units of the input set that are inside the zone
|
|
function cfxZones.unitsInZone(theUnits, theZone)
|
|
if not theUnits then return {} end
|
|
if not theZone then return {} end
|
|
|
|
local zoneUnits = {}
|
|
for index, aUnit in pairs(theUnits) do
|
|
if cfxZones.unitInZone(aUnit, theZone) then
|
|
table.insert( zoneUnits, aUnit)
|
|
end
|
|
end
|
|
return zoneUnits
|
|
end
|
|
|
|
function dmlZone:unitsInZone(theUnits)
|
|
if not theUnits then return {} end
|
|
local zoneUnits = {}
|
|
for index, aUnit in pairs(theUnits) do
|
|
if self:unitInZone(aUnit) then
|
|
table.insert(zoneUnits, aUnit)
|
|
end
|
|
end
|
|
return zoneUnits
|
|
end
|
|
|
|
function cfxZones.closestUnitToZoneCenter(theUnits, theZone)
|
|
-- does not care if they really are in zone. call unitsInZone first
|
|
-- if you need to have them filtered
|
|
-- theUnits MUST BE ARRAY
|
|
if not theUnits then return nil end
|
|
if #theUnits == 0 then return nil end
|
|
local closestUnit = theUnits[1]
|
|
local zP = cfxZones.getPoint(theZone)
|
|
local smallestDist = math.huge
|
|
for i=2, #theUnits do
|
|
local aUnit = theUnits[i]
|
|
local currDist = dcsCommon.dist(zP, aUnit:getPoint())
|
|
if smallestDist > currDelta then
|
|
closestUnit = aUnit
|
|
smallestDist = currDist
|
|
end
|
|
end
|
|
return closestUnit
|
|
end
|
|
|
|
function dmlZone:closestUnitToZoneCenter(theUnits)
|
|
return cfxZones.closestUnitToZoneCenter(theUnits, self)
|
|
end
|
|
|
|
-- grow zone
|
|
function cfxZones.growZone()
|
|
-- circular zones simply increase radius
|
|
-- poly zones: not defined
|
|
|
|
end
|
|
|
|
|
|
-- creating units in a zone
|
|
function cfxZones.createGroundUnitsInZoneForCoalition (theCoalition, groupName, theZone, theUnits, formation, heading, liveries, drivable)
|
|
-- theUnits can be string or table of string
|
|
if not groupName then groupName = "G_"..theZone.name end
|
|
if not drivable then drivable = false end
|
|
-- group name will be taken from zone name and prependend with "G_"
|
|
local theGroup = dcsCommon.createGroundGroupWithUnits(groupName, theUnits, theZone.radius, nil, formation, nil, liveries)
|
|
|
|
theGroup.uncontrollable = false -- just for completeness
|
|
if drivable then
|
|
local units = theGroup.units
|
|
for idx, theUnit in pairs(units) do
|
|
theUnit.playerCanDrive = drivable
|
|
end
|
|
end
|
|
|
|
-- turn the entire formation to heading
|
|
if (not heading) then heading = 0 end
|
|
dcsCommon.rotateGroupData(theGroup, heading) -- currently, group is still at origin, no cx, cy
|
|
|
|
|
|
-- now move the group to center of theZone
|
|
dcsCommon.moveGroupDataTo(theGroup,
|
|
theZone.point.x,
|
|
theZone.point.z) -- watchit: Z!!!
|
|
|
|
-- create the group in the world and return it
|
|
-- first we need to translate the coalition to a legal
|
|
-- country. we use UN for neutral, cjtf for red and blue
|
|
local theSideCJTF = dcsCommon.coalition2county(theCoalition)
|
|
-- store cty and cat for later access. DCS doesn't need it, but we may
|
|
|
|
theGroup.cty = theSideCJTF
|
|
theGroup.cat = Group.Category.GROUND
|
|
|
|
-- create a copy of the group data for
|
|
-- later reference
|
|
local groupDataCopy = dcsCommon.clone(theGroup)
|
|
|
|
local newGroup = coalition.addGroup(theSideCJTF, Group.Category.GROUND, theGroup)
|
|
return newGroup, groupDataCopy
|
|
end
|
|
|
|
--
|
|
-- ===============
|
|
-- FLAG PROCESSING
|
|
-- ===============
|
|
--
|
|
|
|
--
|
|
-- Flag Pulling
|
|
--
|
|
function cfxZones.pulseFlag(theFlag, method, theZone)
|
|
local args = {}
|
|
args.theFlag = theFlag
|
|
args.method = method
|
|
args.theZone = theZone
|
|
local delay = 3
|
|
if dcsCommon.containsString(method, ",") then
|
|
local parts = dcsCommon.splitString(method, ",")
|
|
delay = parts[2]
|
|
if delay then delay = tonumber(delay) end
|
|
end
|
|
if not delay then delay = 3 end
|
|
if theZone.verbose then
|
|
trigger.action.outText("+++zne: RAISING pulse t="..delay.." for flag <" .. theFlag .. "> in zone <" .. theZone.name ..">", 30)
|
|
end
|
|
local newVal = 1
|
|
cfxZones.setFlagValue(theFlag, newVal, theZone)
|
|
|
|
-- schedule second half of pulse
|
|
timer.scheduleFunction(cfxZones.unPulseFlag, args, timer.getTime() + delay)
|
|
end
|
|
|
|
function dmlZone:pulseFlag(theFlag, method)
|
|
cfxZones.pulseFlag(theFlag, method, self)
|
|
end
|
|
|
|
function cfxZones.unPulseFlag(args)
|
|
local theZone = args.theZone
|
|
local method = args.method
|
|
local theFlag = args.theFlag
|
|
local newVal = 0
|
|
-- we may later use method to determine pulse direction / newVal
|
|
-- for now, we always go low
|
|
if theZone.verbose then
|
|
trigger.action.outText("+++zne: DOWNPULSE pulse for flag <" .. theFlag .. "> in zone <" .. theZone.name ..">", 30)
|
|
end
|
|
cfxZones.setFlagValue(theFlag, newVal, theZone)
|
|
end
|
|
|
|
function cfxZones.evalRemainder(remainder, theZone)
|
|
local rNum = tonumber(remainder)
|
|
if not rNum then
|
|
-- we use remainder as name for flag
|
|
-- PROCESS ESCAPE SEQUENCES
|
|
local esc = string.sub(remainder, 1, 1)
|
|
local last = string.sub(remainder, -1)
|
|
if esc == "@" then
|
|
remainder = string.sub(remainder, 2)
|
|
remainder = dcsCommon.trim(remainder)
|
|
end
|
|
|
|
if esc == "(" and last == ")" and string.len(remainder) > 2 then
|
|
-- note: iisues with startswith("(") ???
|
|
remainder = string.sub(remainder, 2, -2)
|
|
remainder = dcsCommon.trim(remainder)
|
|
end
|
|
if esc == "\"" and last == "\"" and string.len(remainder) > 2 then
|
|
remainder = string.sub(remainder, 2, -2)
|
|
remainder = dcsCommon.trim(remainder)
|
|
end
|
|
if cfxZones.verbose then
|
|
trigger.action.outText("+++zne: accessing flag <" .. remainder .. ">", 30)
|
|
end
|
|
rNum = cfxZones.getFlagValue(remainder, theZone)
|
|
end
|
|
return rNum
|
|
end
|
|
|
|
function dmlZone:evalRemainder(remainder)
|
|
return cfxZones.evalRemainder(remainder, self)
|
|
end
|
|
|
|
function cfxZones.doPollFlag(theFlag, method, theZone) -- no OOP equivalent
|
|
-- WARNING:
|
|
-- if method is a number string, it will be interpreted as follows:
|
|
-- positive number: set immediate
|
|
-- negative: decrement by amouint
|
|
if not theZone then
|
|
trigger.action.outText("+++zones: nil theZone on pollFlag", 30)
|
|
end
|
|
|
|
local mt = type(method)
|
|
if mt == "number" then
|
|
method = "#" .. method -- convert to immediate
|
|
mt = "string"
|
|
elseif mt ~= "string" then
|
|
trigger.action.outText("+++zne: warning: zone <" .. theZone.name .. "> method type <" .. mt .. "> received. Ignoring", 30)
|
|
return
|
|
end
|
|
|
|
local val = nil
|
|
method = method:lower()
|
|
method = dcsCommon.trim(method)
|
|
val = tonumber(method) -- see if val can be directly converted
|
|
if dcsCommon.stringStartsWith(method, "+") or
|
|
dcsCommon.stringStartsWith(method, "-")
|
|
then
|
|
-- skip this processing, a legal method can start with "+" or "-"
|
|
-- and we interpret it as a method to increase or decrease by amount
|
|
elseif (val ~= nil) then
|
|
-- provision to handle direct (positive) numbers (legacy support)
|
|
-- method can be converted to number but does not start with - or +
|
|
-- since all negative numbers start with '-' above guard will skip, positive will end up here
|
|
cfxZones.setFlagValue(theFlag, val, theZone)
|
|
if cfxZones.verbose or theZone.verbose then
|
|
trigger.action.outText("+++zones: flag <" .. theFlag .. "> changed to #" .. val, 30)
|
|
end
|
|
return
|
|
else
|
|
end
|
|
|
|
if dcsCommon.stringStartsWith(method, "#") then
|
|
-- immediate value command. remove # and eval remainder
|
|
local remainder = dcsCommon.removePrefix(method, "#")
|
|
val = cfxZones.evalRemainder(remainder) -- always returens a number
|
|
cfxZones.setFlagValue(theFlag, val, theZone)
|
|
if theZone.verbose then
|
|
trigger.action.outText("+++zones: poll setting immediate <" .. theFlag .. "> in <" .. theZone.name .. "> to <" .. val .. ">", 30)
|
|
end
|
|
return
|
|
end
|
|
|
|
local currVal = cfxZones.getFlagValue(theFlag, theZone)
|
|
if method == "inc" or method == "f+1" then
|
|
--trigger.action.setUserFlag(theFlag, currVal + 1)
|
|
cfxZones.setFlagValue(theFlag, currVal+1, theZone)
|
|
|
|
elseif method == "dec" or method == "f-1" then
|
|
-- trigger.action.setUserFlag(theFlag, currVal - 1)
|
|
cfxZones.setFlagValue(theFlag, currVal-1, theZone)
|
|
|
|
elseif method == "off" or method == "f=0" then
|
|
-- trigger.action.setUserFlag(theFlag, 0)
|
|
cfxZones.setFlagValue(theFlag, 0, theZone)
|
|
|
|
elseif method == "flip" or method == "xor" then
|
|
if currVal ~= 0 then
|
|
-- trigger.action.setUserFlag(theFlag, 0)
|
|
cfxZones.setFlagValue(theFlag, 0, theZone)
|
|
|
|
else
|
|
--trigger.action.setUserFlag(theFlag, 1)
|
|
cfxZones.setFlagValue(theFlag, 1, theZone)
|
|
end
|
|
|
|
elseif dcsCommon.stringStartsWith(method, "pulse") then
|
|
cfxZones.pulseFlag(theFlag, method, theZone)
|
|
|
|
elseif dcsCommon.stringStartsWith(method, "+") then
|
|
-- we add whatever is to the right
|
|
local remainder = dcsCommon.removePrefix(method, "+")
|
|
local adder = cfxZones.evalRemainder(remainder)
|
|
cfxZones.setFlagValue(theFlag, currVal+adder, theZone)
|
|
if theZone.verbose then
|
|
trigger.action.outText("+++zones: (poll) updating with '+' flag <" .. theFlag .. "> in <" .. theZone.name .. "> by <" .. adder .. "> to <" .. adder + currVal .. ">", 30)
|
|
end
|
|
|
|
elseif dcsCommon.stringStartsWith(method, "-") then
|
|
-- we subtract whatever is to the right
|
|
local remainder = dcsCommon.removePrefix(method, "-")
|
|
local adder = cfxZones.evalRemainder(remainder)
|
|
cfxZones.setFlagValue(theFlag, currVal-adder, theZone)
|
|
|
|
else
|
|
if method ~= "on" and method ~= "f=1" then
|
|
trigger.action.outText("+++zones: unknown method <" .. method .. "> for flag <" .. theFlag .. "> in zone <" .. theZone.name .. "> - setting to 1", 30)
|
|
end
|
|
-- default: on.
|
|
-- trigger.action.setUserFlag(theFlag, 1)
|
|
cfxZones.setFlagValue(theFlag, 1, theZone)
|
|
end
|
|
|
|
if cfxZones.verbose then
|
|
local newVal = cfxZones.getFlagValue(theFlag, theZone)
|
|
trigger.action.outText("+++zones: flag <" .. theFlag .. "> changed from " .. currVal .. " to " .. newVal, 30)
|
|
end
|
|
end
|
|
|
|
function cfxZones.pollFlag(theFlag, method, theZone)
|
|
--trigger.action.outText("enter pollflag for flag <" .. theFlag .. "> of zone <" .. theZone.name .. ">", 30)
|
|
local allFlags = {}
|
|
if dcsCommon.containsString(theFlag, ",") then
|
|
if cfxZones.verbose then
|
|
trigger.action.outText("+++zones: will poll flag set <" .. theFlag .. "> with " .. method, 30)
|
|
end
|
|
allFlags = dcsCommon.splitString(theFlag, ",")
|
|
else
|
|
table.insert(allFlags, theFlag)
|
|
end
|
|
|
|
for idx, aFlag in pairs(allFlags) do
|
|
aFlag = dcsCommon.trim(aFlag)
|
|
-- note: mey require range preprocessing, but that's not
|
|
-- a priority
|
|
cfxZones.doPollFlag(aFlag, method, theZone)
|
|
end
|
|
end
|
|
|
|
function dmlZone:pollFlag(theFlag, method)
|
|
cfxZones.pollFlag(theFlag, method, self)
|
|
end
|
|
|
|
function cfxZones.expandFlagName(theFlag, theZone)
|
|
if not theFlag then return "!NIL" end
|
|
local zoneName = "<dummy>"
|
|
if theZone then
|
|
zoneName = theZone.name -- for flag wildcards
|
|
end
|
|
|
|
if type(theFlag) == "number" then
|
|
-- straight number, return
|
|
return theFlag
|
|
end
|
|
|
|
-- we assume it's a string now
|
|
theFlag = dcsCommon.trim(theFlag) -- clear leading/trailing spaces
|
|
local nFlag = tonumber(theFlag)
|
|
if nFlag then -- a number, legal
|
|
return theFlag
|
|
end
|
|
|
|
-- now do wildcard processing. we have alphanumeric
|
|
if dcsCommon.stringStartsWith(theFlag, "*") then
|
|
theFlag = zoneName .. theFlag
|
|
end
|
|
return theFlag
|
|
end
|
|
|
|
function dmlZone:setFlagValue(theFlag, theValue)
|
|
cfxZones.setFlagValueMult(theFlag, theValue, self)
|
|
end
|
|
|
|
function cfxZones.setFlagValue(theFlag, theValue, theZone)
|
|
cfxZones.setFlagValueMult(theFlag, theValue, theZone)
|
|
end
|
|
|
|
function cfxZones.setFlagValueMult(theFlag, theValue, theZone)
|
|
local allFlags = {}
|
|
if dcsCommon.containsString(theFlag, ",") then
|
|
if cfxZones.verbose then
|
|
trigger.action.outText("+++zones: will multi-set flags <" .. theFlag .. "> to " .. theValue, 30)
|
|
end
|
|
allFlags = dcsCommon.splitString(theFlag, ",")
|
|
else
|
|
table.insert(allFlags, theFlag)
|
|
end
|
|
|
|
for idx, aFlag in pairs(allFlags) do
|
|
aFlag = dcsCommon.trim(aFlag)
|
|
-- note: mey require range preprocessing, but that's not
|
|
-- a priority
|
|
cfxZones.doSetFlagValue(aFlag, theValue, theZone)
|
|
end
|
|
end
|
|
|
|
function cfxZones.doSetFlagValue(theFlag, theValue, theZone)
|
|
local zoneName = "<dummy>"
|
|
if not theZone then
|
|
trigger.action.outText("+++Zne: no zone on doSetFlagValue", 30) -- mod me for detector
|
|
else
|
|
zoneName = theZone.name -- for flag wildcards
|
|
end
|
|
|
|
if type(theFlag) == "number" then
|
|
-- straight set, oldschool ME flag
|
|
trigger.action.setUserFlag(theFlag, theValue)
|
|
return
|
|
end
|
|
|
|
-- we assume it's a string now
|
|
theFlag = dcsCommon.trim(theFlag) -- clear leading/trailing spaces
|
|
-- some QoL: detect "<none>"
|
|
if dcsCommon.containsString(theFlag, "<none>") then
|
|
trigger.action.outText("+++Zone: warning - setFlag has '<none>' flag name in zone <" .. zoneName .. ">", 30) -- if error, intended break
|
|
end
|
|
|
|
-- now do wildcard processing. we have alphanumeric
|
|
if dcsCommon.stringStartsWith(theFlag, "*") then
|
|
theFlag = zoneName .. theFlag
|
|
end
|
|
trigger.action.setUserFlag(theFlag, theValue)
|
|
end
|
|
|
|
|
|
|
|
function cfxZones.getFlagValue(theFlag, theZone)
|
|
local zoneName = "<dummy>"
|
|
if not theZone or not theZone.name then
|
|
trigger.action.outText("+++Zne: no zone or zone name on getFlagValue", 30)
|
|
else
|
|
zoneName = theZone.name -- for flag wildcards
|
|
end
|
|
|
|
if type(theFlag) == "number" then
|
|
-- straight get, ME flag
|
|
return tonumber(trigger.misc.getUserFlag(theFlag))
|
|
end
|
|
|
|
-- we assume it's a string now
|
|
theFlag = dcsCommon.trim(theFlag) -- clear leading/trailing spaces
|
|
local nFlag = tonumber(theFlag)
|
|
if nFlag then
|
|
return tonumber(trigger.misc.getUserFlag(theFlag))
|
|
end
|
|
|
|
-- some QoL: detect "<none>"
|
|
if dcsCommon.containsString(theFlag, "<none>") then
|
|
trigger.action.outText("+++Zone: warning - getFlag has '<none>' flag name in zone <" .. zoneName .. ">", 30) -- break here
|
|
end
|
|
|
|
-- now do wildcard processing. we have alphanumeric
|
|
if dcsCommon.stringStartsWith(theFlag, "*") then
|
|
theFlag = zoneName .. theFlag
|
|
end
|
|
return tonumber(trigger.misc.getUserFlag(theFlag))
|
|
end
|
|
|
|
function dmlZone:getFlagValue(theFlag)
|
|
return cfxZones.getFlagValue(theFlag, self)
|
|
end
|
|
|
|
function cfxZones.verifyMethod(theMethod, theZone)
|
|
local lMethod = string.lower(theMethod)
|
|
if lMethod == "#" or lMethod == "change" then
|
|
return true
|
|
end
|
|
|
|
if lMethod == "0" or lMethod == "no" or lMethod == "false"
|
|
or lMethod == "off" then
|
|
return true
|
|
end
|
|
|
|
if lMethod == "1" or lMethod == "yes" or lMethod == "true"
|
|
or lMethod == "on" then
|
|
return true
|
|
end
|
|
|
|
if lMethod == "inc" or lMethod == "+1" then
|
|
return true
|
|
end
|
|
|
|
if lMethod == "dec" or lMethod == "-1" then
|
|
return true
|
|
end
|
|
|
|
if lMethod == "lohi" or lMethod == "pulse" then
|
|
return true
|
|
end
|
|
|
|
if lMethod == "hilo" then
|
|
return true
|
|
end
|
|
|
|
-- number constraints
|
|
-- or flag constraints -- ONLY RETURN TRUE IF CHANGE AND CONSTRAINT MET
|
|
local op = string.sub(theMethod, 1, 1)
|
|
local remainder = string.sub(theMethod, 2)
|
|
remainder = dcsCommon.trim(remainder) -- remove all leading and trailing spaces
|
|
|
|
if true then
|
|
-- we have a comparison = ">", "=", "<" followed by a number
|
|
-- THEY TRIGGER EACH TIME lastVal <> currVal AND condition IS MET
|
|
if op == "=" then
|
|
return true
|
|
end
|
|
|
|
if op == "#" or op == "~" then
|
|
return true
|
|
end
|
|
|
|
if op == "<" then
|
|
return true
|
|
end
|
|
|
|
if op == ">" then
|
|
return true
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
function dmlZone:verifyMethod(theMethod)
|
|
return cfxZones.verifyMethod(theMethod, self)
|
|
end
|
|
|
|
-- method-based flag testing
|
|
function cfxZones.evalFlagMethodImmediate(currVal, theMethod, theZone)
|
|
-- immediate eval - does not look at last val.
|
|
-- return true/false/value based on theMethod's contraints
|
|
-- simple constraints
|
|
local lMethod = string.lower(theMethod)
|
|
if lMethod == "#" or lMethod == "change" then
|
|
-- ALWAYS RETURNS TRUE for currval <> 0, flase if currval = 0
|
|
return currVal ~= 0
|
|
end
|
|
|
|
if lMethod == "0" or lMethod == "no" or lMethod == "false"
|
|
or lMethod == "off" then
|
|
-- WARNING: ALWAYS RETURNS FALSE
|
|
return false
|
|
end
|
|
|
|
if lMethod == "1" or lMethod == "yes" or lMethod == "true"
|
|
or lMethod == "on" then
|
|
-- WARNING: ALWAYS RETURNS TRUE
|
|
return true
|
|
end
|
|
|
|
if lMethod == "inc" or lMethod == "+1" then
|
|
return currVal+1 -- this may be unexpected
|
|
end
|
|
|
|
if lMethod == "dec" or lMethod == "-1" then
|
|
return currVal-1 -- this may be unexpectd
|
|
end
|
|
|
|
-- number constraints
|
|
-- or flag constraints
|
|
-- ONLY RETURN TRUE IF CHANGE AND CONSTRAINT MET
|
|
local op = string.sub(theMethod, 1, 1)
|
|
local remainder = string.sub(theMethod, 2)
|
|
remainder = dcsCommon.trim(remainder) -- remove all leading and trailing spaces
|
|
local rNum = tonumber(remainder)
|
|
if not rNum then
|
|
-- we use remainder as name for flag
|
|
-- PROCESS ESCAPE SEQUENCES
|
|
local esc = string.sub(remainder, 1, 1)
|
|
local last = string.sub(remainder, -1)
|
|
if esc == "@" then
|
|
remainder = string.sub(remainder, 2)
|
|
remainder = dcsCommon.trim(remainder)
|
|
end
|
|
|
|
if esc == "(" and last == ")" and string.len(remainder) > 2 then
|
|
-- note: iisues with startswith("(") ???
|
|
remainder = string.sub(remainder, 2, -2)
|
|
remainder = dcsCommon.trim(remainder)
|
|
end
|
|
if esc == "\"" and last == "\"" and string.len(remainder) > 2 then
|
|
remainder = string.sub(remainder, 2, -2)
|
|
remainder = dcsCommon.trim(remainder)
|
|
end
|
|
if cfxZones.verbose then
|
|
trigger.action.outText("+++zne: accessing flag <" .. remainder .. ">", 30)
|
|
end
|
|
rNum = cfxZones.getFlagValue(remainder, theZone)
|
|
end
|
|
if rNum then
|
|
-- we have a comparison = ">", "=", "<" followed by a number
|
|
if op == "=" then
|
|
return currVal == rNum
|
|
end
|
|
|
|
if op == "#" or op == "~" then
|
|
return currVal ~= rNum
|
|
end
|
|
|
|
if op == "<" then
|
|
return currVal < rNum
|
|
end
|
|
|
|
if op == ">" then
|
|
return currVal > rNum
|
|
end
|
|
end
|
|
|
|
-- if we get here, we have an error
|
|
local zoneName = "<NIL>"
|
|
if theZone then zoneName = theZone.name end
|
|
trigger.action.outText("+++Zne: illegal |" .. theMethod .. "| in eval for zone " .. zoneName, 30 )
|
|
return false
|
|
end
|
|
|
|
function dmlZone:evalFlagMethodImmediate(currVal, theMethod, theZone)
|
|
return cfxZones.evalFlagMethodImmediate(currVal, theMethod, self)
|
|
end
|
|
|
|
|
|
function cfxZones.testFlagByMethodForZone(currVal, lastVal, theMethod, theZone)
|
|
-- return true/false based on theMethod's contraints
|
|
-- simple constraints
|
|
-- ONLY RETURN TRUE IF CHANGE AND CONSTRAINT MET
|
|
local lMethod = string.lower(theMethod)
|
|
if lMethod == "#" or lMethod == "change" then
|
|
-- check if currVal different from lastVal
|
|
return currVal ~= lastVal
|
|
end
|
|
|
|
if lMethod == "0" or lMethod == "no" or lMethod == "false"
|
|
or lMethod == "off" then
|
|
-- WARNING: ONLY RETURNS TRUE IF FALSE AND lastval not zero!
|
|
return currVal == 0 and currVal ~= lastVal
|
|
end
|
|
|
|
if lMethod == "1" or lMethod == "yes" or lMethod == "true"
|
|
or lMethod == "on" then
|
|
-- WARNING: only returns true if lastval was false!!!!
|
|
return (currVal ~= 0 and lastVal == 0)
|
|
end
|
|
|
|
if lMethod == "inc" or lMethod == "+1" then
|
|
-- return currVal == lastVal+1 -- better: test for greater than
|
|
return currVal > lastVal
|
|
end
|
|
|
|
if lMethod == "dec" or lMethod == "-1" then
|
|
--return currVal == lastVal-1
|
|
return currVal < lastVal
|
|
end
|
|
|
|
if lMethod == "lohi" or lMethod == "pulse" then
|
|
return (lastVal <= 0 and currVal > 0)
|
|
end
|
|
|
|
if lMethod == "hilo" then
|
|
return (lastVal > 0 and currVal <= 0)
|
|
end
|
|
|
|
-- number constraints
|
|
-- or flag constraints
|
|
-- ONLY RETURN TRUE IF CHANGE AND CONSTRAINT MET
|
|
local op = string.sub(theMethod, 1, 1)
|
|
local remainder = string.sub(theMethod, 2)
|
|
remainder = dcsCommon.trim(remainder) -- remove all leading and trailing spaces
|
|
local rNum = tonumber(remainder)
|
|
if not rNum then
|
|
-- we use remainder as name for flag
|
|
-- PROCESS ESCAPE SEQUENCES
|
|
local esc = string.sub(remainder, 1, 1)
|
|
local last = string.sub(remainder, -1)
|
|
if esc == "@" then
|
|
remainder = string.sub(remainder, 2)
|
|
remainder = dcsCommon.trim(remainder)
|
|
end
|
|
|
|
if esc == "(" and last == ")" and string.len(remainder) > 2 then
|
|
-- note: iisues with startswith("(") ???
|
|
remainder = string.sub(remainder, 2, -2)
|
|
remainder = dcsCommon.trim(remainder)
|
|
end
|
|
if esc == "\"" and last == "\"" and string.len(remainder) > 2 then
|
|
remainder = string.sub(remainder, 2, -2)
|
|
remainder = dcsCommon.trim(remainder)
|
|
end
|
|
if cfxZones.verbose then
|
|
trigger.action.outText("+++zne: accessing flag <" .. remainder .. ">", 30)
|
|
end
|
|
rNum = cfxZones.getFlagValue(remainder, theZone)
|
|
end
|
|
if rNum then
|
|
-- we have a comparison = ">", "=", "<" followed by a number
|
|
-- THEY TRIGGER EACH TIME lastVal <> currVal AND condition IS MET
|
|
if op == "=" then
|
|
return currVal == rNum and lastVal ~= currVal
|
|
end
|
|
|
|
if op == "#" or op == "~" then
|
|
return currVal ~= rNum and lastVal ~= currVal
|
|
end
|
|
|
|
if op == "<" then
|
|
return currVal < rNum and lastVal ~= currVal
|
|
end
|
|
|
|
if op == ">" then
|
|
return currVal > rNum and lastVal ~= currVal
|
|
end
|
|
end
|
|
|
|
-- if we get here, we have an error
|
|
local zoneName = "<NIL>"
|
|
if theZone then zoneName = theZone.name end
|
|
trigger.action.outText("+++Zne: illegal method constraints |" .. theMethod .. "| for zone " .. zoneName, 30 )
|
|
return false
|
|
end
|
|
|
|
-- WARNING: testZoneFlag must also support non-dmlZone!!!
|
|
function cfxZones.testZoneFlag(theZone, theFlagName, theMethod, latchName)
|
|
-- returns two values: true/false method result, and curr value
|
|
-- returns true if method constraints are met for flag theFlagName
|
|
-- as defined by theMethod
|
|
if not theMethod then
|
|
theMethod = "change"
|
|
end
|
|
|
|
-- will read and update theZone[latchName] as appropriate
|
|
if not theZone then
|
|
trigger.action.outText("+++Zne: no zone for testZoneFlag", 30)
|
|
return nil, nil
|
|
end
|
|
if not theFlagName then
|
|
-- this is common, no error, only on verbose
|
|
if cfxZones.verbose then
|
|
trigger.action.outText("+++Zne: no flagName for zone " .. theZone.name .. " for testZoneFlag", 30)
|
|
end
|
|
return nil, nil
|
|
end
|
|
if not latchName then
|
|
trigger.action.outText("+++Zne: no latchName for zone " .. theZone.name .. " for testZoneFlag", 30)
|
|
return nil, nil
|
|
end
|
|
-- get current value
|
|
local currVal = cfxZones.getFlagValue(theFlagName, theZone)
|
|
|
|
-- get last value from latch
|
|
local lastVal = theZone[latchName]
|
|
if not lastVal then
|
|
trigger.action.outText("+++Zne: latch <" .. latchName .. "> not valid for zone " .. theZone.name, 30) -- intentional break here
|
|
return nil, nil
|
|
end
|
|
|
|
-- now, test by method
|
|
-- we should only test if currVal <> lastVal
|
|
if currVal == lastVal then
|
|
return false, currVal
|
|
end
|
|
|
|
local testResult = cfxZones.testFlagByMethodForZone(currVal, lastVal, theMethod, theZone)
|
|
|
|
-- update latch by method
|
|
theZone[latchName] = currVal
|
|
|
|
-- return result
|
|
return testResult, currVal
|
|
end
|
|
|
|
function dmlZone:testZoneFlag(theFlagName, theMethod, latchName)
|
|
local r, v = cfxZones.testZoneFlag(self, theFlagName, theMethod, latchName)
|
|
return r, v
|
|
end
|
|
|
|
function cfxZones.numberArrayFromString(inString, default) -- bridge
|
|
return dcsCommon.numberArrayFromString(inString, default)
|
|
end
|
|
|
|
|
|
function cfxZones.flagArrayFromString(inString) -- dcsCommon bridge
|
|
return dcsCommon.flagArrayFromString(inString)
|
|
end
|
|
|
|
|
|
--
|
|
-- Drawing a Zone
|
|
--
|
|
|
|
function cfxZones.drawZone(theZone, lineColor, fillColor, markID)
|
|
if not theZone then return 0 end
|
|
if not lineColor then lineColor = {0.8, 0.8, 0.8, 1.0} end
|
|
if not fillColor then fillColor = {0.8, 0.8, 0.8, 0.0} end
|
|
if not markID then markID = dcsCommon.numberUUID() end
|
|
|
|
if theZone.isCircle then
|
|
trigger.action.circleToAll(-1, markID, theZone.point, theZone.radius, lineColor, fillColor, 1, true, "")
|
|
else
|
|
local poly = theZone.poly
|
|
trigger.action.quadToAll(-1, markID, poly[4], poly[3], poly[2], poly[1], lineColor, fillColor, 1, true, "") -- note: left winding to get fill color
|
|
end
|
|
|
|
return markID
|
|
end
|
|
|
|
function dmlZone:drawZone(lineColor, fillColor, markID)
|
|
return cfxZones.drawZone(self, lineColor, fillColor, markID)
|
|
end
|
|
|
|
function cfxZones.drawText(theZone, theText, fSize, lineColor, fillColor)
|
|
if not theZone then return end
|
|
if not fSize then fSize = 12 end
|
|
if not lineColor then lineColor = {0.8, 0.8, 0.8, 1.0} end
|
|
if not fillColor then fillColor = {0, 0, 0, 0} end
|
|
local markID = dcsCommon.numberUUID()
|
|
local p = theZone:getPoint()
|
|
local offset = {x = p.x, y = 0, z = p.z}
|
|
trigger.action.textToAll(-1, markID, offset, lineColor , fillColor , fSize, true , theText)
|
|
return markID
|
|
end
|
|
|
|
function dmlZone:drawText(theText, fSize, lineColor, fillColor)
|
|
return cfxZones.drawText(self, theText, fSize, lineColor, fillColor)
|
|
end
|
|
--
|
|
-- ===================
|
|
-- PROPERTY PROCESSING
|
|
-- ===================
|
|
--
|
|
|
|
function cfxZones.getAllZoneProperties(theZone, caseInsensitive, numbersOnly) -- return as dict
|
|
if not caseInsensitive then caseInsensitive = false end
|
|
if not numbersOnly then numbersOnly = false end
|
|
if not theZone then return {} end
|
|
|
|
local dcsProps = theZone.properties -- zone properties in dcs format
|
|
local props = {}
|
|
-- dcs has all properties as array with values .key and .value
|
|
-- so convert them into a dictionary
|
|
for i=1, #dcsProps do
|
|
local theProp = dcsProps[i]
|
|
local theKey = "dummy"
|
|
if string.len(theProp.key) > 0 then theKey = theProp.key end
|
|
if caseInsensitive then theKey = theKey:upper() end
|
|
local v = theProp.value
|
|
if numbersOnly then
|
|
v = tonumber(v)
|
|
if not v then v = 0 end
|
|
end
|
|
props[theKey] = v
|
|
end
|
|
return props
|
|
end
|
|
|
|
function dmlZone:getAllZoneProperties(caseInsensitive, numbersOnly)
|
|
return cfxZones.getAllZoneProperties(self, caseInsensitive, numbersOnly)
|
|
end
|
|
|
|
function cfxZones.extractPropertyFromDCS(theKey, theProperties)
|
|
-- trim
|
|
theKey = dcsCommon.trim(theKey)
|
|
-- make lower case conversion if not case sensitive
|
|
if not cfxZones.caseSensitiveProperties then
|
|
theKey = string.lower(theKey)
|
|
end
|
|
|
|
-- iterate all keys and compare to what we are looking for
|
|
for i=1, #theProperties do
|
|
local theP = theProperties[i]
|
|
|
|
local existingKey = dcsCommon.trim(theP.key) -- does not work if ends on "#!?" - why?
|
|
--while len(existingKey) > 1 and string.sub(existingKey, -1) == " " do
|
|
-- existingKey = existingKey:sub(1, -2) -- trim manually
|
|
--end
|
|
if string.sub(existingKey, -1) == " " then
|
|
trigger.action.outText("+++ZONES: warning: unable to trim blanks from attribute name <" .. existingKey .. ">", 30)
|
|
end
|
|
-- trigger.action.outText("procced attribute name <" .. existingKey .. ">", 30)
|
|
if not cfxZones.caseSensitiveProperties then
|
|
existingKey = string.lower(existingKey)
|
|
end
|
|
if existingKey == theKey then
|
|
return theP.value
|
|
end
|
|
|
|
-- now check after removing all blanks
|
|
existingKey = dcsCommon.removeBlanks(existingKey)
|
|
if existingKey == theKey then
|
|
return theP.value
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function cfxZones.getZoneProperty(cZone, theKey)
|
|
if not cZone then
|
|
trigger.action.outText("+++zone: no zone in getZoneProperty", 30)
|
|
return nil
|
|
end
|
|
if not theKey then
|
|
trigger.action.outText("+++zone: no property key in getZoneProperty for zone " .. cZone.name, 30)
|
|
return
|
|
end
|
|
|
|
local props = cZone.properties
|
|
local theVal = cfxZones.extractPropertyFromDCS(theKey, props)
|
|
return theVal
|
|
end
|
|
|
|
function dmlZone:getZoneProperty(theKey)
|
|
if not theKey then
|
|
trigger.action.outText("+++zone: no property key in OOP getZoneProperty for zone " .. self.name, 30)
|
|
return nil
|
|
end
|
|
local props = self.properties
|
|
local theVal = cfxZones.extractPropertyFromDCS(theKey, props)
|
|
return theVal
|
|
end
|
|
|
|
|
|
function cfxZones.getStringFromZoneProperty(theZone, theProperty, default)
|
|
if not default then default = "" end
|
|
-- OOP heavy duty test here
|
|
local p = theZone:getZoneProperty(theProperty)
|
|
if not p then return default end
|
|
if type(p) == "number" then
|
|
p = tostring(p)
|
|
end
|
|
if type(p) == "string" then
|
|
p = dcsCommon.trim(p)
|
|
if p == "" then p = default end
|
|
return p
|
|
end
|
|
return default -- warning. what if it was a number first?
|
|
end
|
|
|
|
function dmlZone:getStringFromZoneProperty(theProperty, default)
|
|
if not default then default = "" end
|
|
local p = self:getZoneProperty(theProperty)
|
|
if not p then return default end
|
|
if type(p) == "number" then
|
|
p = tostring(p)
|
|
end
|
|
if type(p) == "string" then
|
|
p = dcsCommon.trim(p)
|
|
if p == "" then p = default end
|
|
return p
|
|
end
|
|
return default -- warning. what if it was a number first?
|
|
end
|
|
|
|
function cfxZones.getMinMaxFromZoneProperty(theZone, theProperty)
|
|
local p = cfxZones.getZoneProperty(theZone, theProperty)
|
|
local theNumbers = dcsCommon.splitString(p, " ")
|
|
return tonumber(theNumbers[1]), tonumber(theNumbers[2])
|
|
end
|
|
|
|
function dmlZone:getMinMaxFromZoneProperty(theProperty)
|
|
local p = self:getZoneProperty(theProperty)
|
|
local theNumbers = dcsCommon.splitString(p, " ")
|
|
return tonumber(theNumbers[1]), tonumber(theNumbers[2])
|
|
end
|
|
|
|
function cfxZones.randomInRange(minVal, maxVal) -- should be moved to dcsCommon
|
|
if maxVal < minVal then
|
|
local t = minVal
|
|
minVal = maxVal
|
|
maxVal = t
|
|
end
|
|
return cfxZones.randomDelayFromPositiveRange(minVal, maxVal)
|
|
end
|
|
|
|
function cfxZones.randomDelayFromPositiveRange(minVal, maxVal) -- should be moved to dcsCommon
|
|
if not maxVal then return minVal end
|
|
if not minVal then return maxVal end
|
|
if minVal == maxVal then return minVal end
|
|
local delay = maxVal
|
|
if minVal >= 0 and minVal < delay then
|
|
-- we want a randomized from time from minTime .. delay
|
|
local varPart = delay - minVal + 1
|
|
varPart = dcsCommon.smallRandom(varPart) - 1
|
|
delay = minVal + varPart
|
|
end
|
|
return delay
|
|
end
|
|
|
|
function cfxZones.getPositiveRangeFromZoneProperty(theZone, theProperty, default, defaultmax)
|
|
-- reads property as string, and interprets as range 'a-b'.
|
|
-- if not a range but single number, returns both for upper and lower
|
|
--trigger.action.outText("***Zne: enter with <" .. theZone.name .. ">: range for property <" .. theProperty .. ">!", 30)
|
|
if not default then default = 0 end
|
|
if not defaultmax then defaultmax = default end
|
|
|
|
local lowerBound = default
|
|
local upperBound = defaultmax
|
|
|
|
local rangeString = cfxZones.getStringFromZoneProperty(theZone, theProperty, "")
|
|
if dcsCommon.containsString(rangeString, "-") then
|
|
local theRange = dcsCommon.splitString(rangeString, "-")
|
|
lowerBound = theRange[1]
|
|
lowerBound = tonumber(lowerBound)
|
|
upperBound = theRange[2]
|
|
upperBound = tonumber(upperBound)
|
|
if lowerBound and upperBound then
|
|
-- swap if wrong order
|
|
if lowerBound > upperBound then
|
|
local temp = upperBound
|
|
upperBound = lowerBound
|
|
lowerBound = temp
|
|
end
|
|
|
|
else
|
|
-- bounds illegal
|
|
trigger.action.outText("+++Zne: illegal range <" .. rangeString .. ">, using " .. default .. "-" .. defaultmax, 30)
|
|
lowerBound = default
|
|
upperBound = defaultmax
|
|
end
|
|
else
|
|
upperBound = cfxZones.getNumberFromZoneProperty(theZone, theProperty, defaultmax) -- between pulses
|
|
lowerBound = upperBound
|
|
end
|
|
|
|
return lowerBound, upperBound
|
|
end
|
|
|
|
function dmlZone:getPositiveRangeFromZoneProperty(theProperty, default, defaultmax)
|
|
local lo, up = cfxZones.getPositiveRangeFromZoneProperty(self, theProperty, default, defaultmax)
|
|
return lo, up
|
|
end
|
|
|
|
function cfxZones.getListFromZoneProperty(theZone, theProperty, defaultItem) -- comma delimited
|
|
if not defaultItem then defaultItem = "default" end
|
|
|
|
local theString = theZone:getStringFromZoneProperty(theProperty, defaultItem)
|
|
if dcsCommon.containsString(theString, ",") then
|
|
local theArray = dcsCommon.splitString(theString, ',')
|
|
theArray = dcsCommon.trimArray(theArray)
|
|
return theArray
|
|
else
|
|
return {theString}
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
function dmlZone:getListFromZoneProperty(theProperty, defaultItem)
|
|
return cfxZones.getListFromZoneProperty(self, theProperty, defaultItem)
|
|
end
|
|
|
|
function cfxZones.hasProperty(theZone, theProperty)
|
|
if not theProperty then
|
|
trigger.action.outText("+++zne: WARNING - hasProperty called with nil theProperty for zone <" .. theZone.name .. ">", 30)
|
|
return false
|
|
end
|
|
local foundIt = cfxZones.getZoneProperty(theZone, theProperty)
|
|
if not foundIt then
|
|
-- check for possible forgotten or exchanged IO flags
|
|
if string.sub(theProperty, -1) == "?" then
|
|
local lessOp = theProperty:sub(1,-2)
|
|
if cfxZones.getZoneProperty(theZone, lessOp) ~= nil then
|
|
trigger.action.outText("*** NOTE: " .. theZone.name .. "'s property <" .. lessOp .. "> may be missing a Query ('?') symbol", 30)
|
|
end
|
|
local lessPlus = lessOp .. "!"
|
|
if cfxZones.getZoneProperty(theZone, lessPlus) ~= nil then
|
|
trigger.action.outText("*** NOTE: " .. theZone.name .. "'s property <" .. lessOp .. "> may be using '!' instead of '?' for input", 30)
|
|
end
|
|
return false
|
|
end
|
|
|
|
if string.sub(theProperty, -1) == "!" then
|
|
local lessOp = theProperty:sub(1,-2)
|
|
if cfxZones.getZoneProperty(theZone, lessOp) ~= nil then
|
|
trigger.action.outText("*** NOTE: " .. theZone.name .. "'s property <" .. lessOp .. "> may be missing a Bang! ('!') symbol", 30)
|
|
end
|
|
local lessPlus = lessOp .. "?"
|
|
if cfxZones.getZoneProperty(theZone, lessPlus) ~= nil then
|
|
trigger.action.outText("*** NOTE: " .. theZone.name .. "'s property <" .. lessOp .. "> may be using '!' instead of '?' for input", 30)
|
|
end
|
|
return false
|
|
end
|
|
|
|
if string.sub(theProperty, -1) == ":" then
|
|
local lessOp = theProperty:sub(1,-2)
|
|
if cfxZones.getZoneProperty(theZone, lessOp) ~= nil then
|
|
trigger.action.outText("*** NOTE: " .. theZone.name .. "'s property <" .. lessOp .. "> may be missing a colon (':') at end", 30)
|
|
end
|
|
return false
|
|
end
|
|
|
|
if string.sub(theProperty, -1) == "#" then
|
|
local lessOp = theProperty:sub(1,-2)
|
|
if cfxZones.getZoneProperty(theZone, lessOp) ~= nil then
|
|
trigger.action.outText("*** NOTE: " .. theZone.name .. "'s property <" .. lessOp .. "> may be missing a hash mark ('#') at end", 30)
|
|
end
|
|
return false
|
|
end
|
|
|
|
return false
|
|
end
|
|
return true
|
|
end
|
|
|
|
function dmlZone:hasProperty(theProperty)
|
|
if not theProperty then
|
|
trigger.action.outText("+++zne: WARNING - hasProperty called with nil theProperty for zone <" .. self.name .. ">", 30)
|
|
return false
|
|
end
|
|
theProperty = dcsCommon.trim(theProperty) -- strip leading and training blanks
|
|
local foundIt = self:getZoneProperty(theProperty)
|
|
if not foundIt then
|
|
-- check for possible forgotten or exchanged IO flags
|
|
if string.sub(theProperty, -1) == "?" then
|
|
local lessOp = theProperty:sub(1,-2)
|
|
if self:getZoneProperty(lessOp) ~= nil then
|
|
trigger.action.outText("*** NOTE: " .. self.name .. "'s property <" .. lessOp .. "> may be missing a Query ('?') symbol", 30)
|
|
end
|
|
local lessPlus = lessOp .. "!"
|
|
if self:getZoneProperty(lessPlus) ~= nil then
|
|
trigger.action.outText("*** NOTE: " .. self.name .. "'s property <" .. lessOp .. "> may be using '!' instead of '?' for input", 30)
|
|
end
|
|
return false
|
|
end
|
|
|
|
if string.sub(theProperty, -1) == "!" then
|
|
local lessOp = theProperty:sub(1,-2)
|
|
if self:getZoneProperty(lessOp) ~= nil then
|
|
trigger.action.outText("*** NOTE: " .. self.name .. "'s property <" .. lessOp .. "> may be missing a Bang! ('!') symbol", 30)
|
|
end
|
|
local lessPlus = lessOp .. "?"
|
|
if self:getZoneProperty(lessPlus) ~= nil then
|
|
trigger.action.outText("*** NOTE: " .. self.name .. "'s property <" .. lessOp .. "> may be using '!' instead of '?' for input", 30)
|
|
end
|
|
return false
|
|
end
|
|
|
|
if string.sub(theProperty, -1) == ":" then
|
|
local lessOp = theProperty:sub(1,-2)
|
|
if self:getZoneProperty(lessOp) ~= nil then
|
|
trigger.action.outText("*** NOTE: " .. self.name .. "'s property <" .. lessOp .. "> may be missing a colon (':') at end", 30)
|
|
end
|
|
return false
|
|
end
|
|
|
|
if string.sub(theProperty, -1) == "#" then
|
|
local lessOp = theProperty:sub(1,-2)
|
|
if self:getZoneProperty(lessOp) ~= nil then
|
|
trigger.action.outText("*** NOTE: " .. self.name .. "'s property <" .. lessOp .. "> may be missing a hash mark ('#') at end", 30)
|
|
end
|
|
return false
|
|
end
|
|
|
|
return false
|
|
end
|
|
return true
|
|
end
|
|
|
|
function cfxZones.getBoolFromZoneProperty(theZone, theProperty, defaultVal)
|
|
if not defaultVal then defaultVal = false end
|
|
if type(defaultVal) ~= "boolean" then
|
|
defaultVal = false
|
|
end
|
|
|
|
if not theZone then
|
|
trigger.action.outText("WARNING: NIL Zone in getBoolFromZoneProperty", 30)
|
|
return defaultVal
|
|
end
|
|
|
|
|
|
local p = cfxZones.getZoneProperty(theZone, theProperty)
|
|
if not p then return defaultVal end
|
|
|
|
-- make sure we compare so default always works when
|
|
-- answer isn't exactly the opposite
|
|
p = p:lower()
|
|
p = dcsCommon.trim(p)
|
|
if defaultVal == false then
|
|
-- only go true if exact match to yes or true
|
|
theBool = false
|
|
theBool = (p == 'true') or (p == 'yes') or (p == "1") or (p == 'on')
|
|
return theBool
|
|
end
|
|
|
|
-- special: return a random value if p == "rnd" or "?" or "maybe"
|
|
if (p == "?") or (p == "rnd") or (p == "random") or (p == "maybe") then
|
|
return (math.random(1000) < 500) -- 50:50
|
|
end
|
|
|
|
local theBool = true
|
|
-- only go false if exactly no or false or "0"
|
|
theBool = (p ~= 'false') and (p ~= 'no') and (p ~= "0") and (p~="off")
|
|
return theBool
|
|
end
|
|
|
|
function dmlZone:getBoolFromZoneProperty(theProperty, defaultVal)
|
|
if not defaultVal then defaultVal = false end
|
|
if type(defaultVal) ~= "boolean" then
|
|
defaultVal = false
|
|
end
|
|
|
|
local p = self:getZoneProperty(theProperty)
|
|
if not p then return defaultVal end
|
|
|
|
-- make sure we compare so default always works when
|
|
-- answer isn't exactly the opposite
|
|
p = p:lower()
|
|
p = dcsCommon.trim(p)
|
|
if defaultVal == false then
|
|
-- only go true if exact match to yes or true
|
|
theBool = false
|
|
theBool = (p == 'true') or (p == 'yes') or (p == "1") or (p=="on")
|
|
return theBool
|
|
end
|
|
|
|
-- special: return a random value if p == "rnd" or "?" or "maybe"
|
|
if (p == "?") or (p == "rnd") or (p == "random") or (p == "maybe") then
|
|
return (math.random(1000) < 500) -- 50:50
|
|
end
|
|
|
|
local theBool = true
|
|
-- only go false if exactly no or false or "0"
|
|
theBool = (p ~= 'false') and (p ~= 'no') and (p ~= "0") and (p ~= "off")
|
|
return theBool
|
|
end
|
|
|
|
function cfxZones.getCoalitionFromZoneProperty(theZone, theProperty, default)
|
|
if not default then default = 0 end
|
|
local p = cfxZones.getZoneProperty(theZone, theProperty)
|
|
if not p then return default end
|
|
if type(p) == "number" then -- can't currently really happen
|
|
if p == 1 then return 1 end
|
|
if p == 2 then return 2 end
|
|
return 0
|
|
end
|
|
|
|
if type(p) == "string" then
|
|
if p == "1" then return 1 end
|
|
if p == "2" then return 2 end
|
|
if p == "0" then return 0 end
|
|
|
|
p = p:lower()
|
|
|
|
if p == "red" then return 1 end
|
|
if p == "blue" then return 2 end
|
|
if p == "neutral" then return 0 end
|
|
if p == "all" then return 0 end
|
|
return default
|
|
end
|
|
|
|
return default
|
|
end
|
|
|
|
function dmlZone:getCoalitionFromZoneProperty(theProperty, default)
|
|
if not default then default = 0 end
|
|
local p = self:getZoneProperty(theProperty)
|
|
if not p then return default end
|
|
if type(p) == "number" then -- can't currently really happen
|
|
if p == 1 then return 1 end
|
|
if p == 2 then return 2 end
|
|
return 0
|
|
end
|
|
|
|
if type(p) == "string" then
|
|
if p == "1" then return 1 end
|
|
if p == "2" then return 2 end
|
|
if p == "0" then return 0 end
|
|
|
|
p = p:lower()
|
|
|
|
if p == "red" then return 1 end
|
|
if p == "blue" then return 2 end
|
|
if p == "neutral" then return 0 end
|
|
if p == "all" then return 0 end
|
|
return default
|
|
end
|
|
|
|
return default
|
|
end
|
|
|
|
function cfxZones.getNumberFromZoneProperty(theZone, theProperty, default)
|
|
if not default then default = 0 end
|
|
default = tonumber(default)
|
|
if not default then default = 0 end -- enforce default numbner as well
|
|
local p = cfxZones.getZoneProperty(theZone, theProperty)
|
|
p = tonumber(p)
|
|
if not p then p = default end
|
|
return p
|
|
end
|
|
|
|
function dmlZone:getNumberFromZoneProperty(theProperty, default)
|
|
if not default then default = 0 end
|
|
default = tonumber(default)
|
|
if not default then default = 0 end -- enforce default numbner as well
|
|
local p = self:getZoneProperty(theProperty)
|
|
p = tonumber(p)
|
|
if not p then p = default end
|
|
return p
|
|
end
|
|
|
|
function cfxZones.getVectorFromZoneProperty(theZone, theProperty, minDims, defaultVal)
|
|
if not minDims then minDims = 0 end
|
|
if not defaultVal then defaultVal = 0 end
|
|
local s = cfxZones.getStringFromZoneProperty(theZone, theProperty, "")
|
|
local sVec = dcsCommon.splitString(s, ",")
|
|
local nVec = {}
|
|
for idx, numString in pairs (sVec) do
|
|
local n = tonumber(numString)
|
|
if not n then n = defaultVal end
|
|
table.insert(nVec, n)
|
|
end
|
|
-- make sure vector contains at least minDims values
|
|
while #nVec < minDims do
|
|
table.insert(nVec, defaultVal)
|
|
end
|
|
|
|
return nVec
|
|
end
|
|
|
|
function dmlZone:getVectorFromZoneProperty(theProperty, minDims, defaultVal)
|
|
if not minDims then minDims = 0 end
|
|
if not defaultVal then defaultVal = 0 end
|
|
local s = self:getStringFromZoneProperty(theProperty, "")
|
|
local sVec = dcsCommon.splitString(s, ",")
|
|
local nVec = {}
|
|
for idx, numString in pairs (sVec) do
|
|
local n = tonumber(numString)
|
|
if not n then n = defaultVal end
|
|
table.insert(nVec, n)
|
|
end
|
|
-- make sure vector contains at least minDims values
|
|
while #nVec < minDims do
|
|
table.insert(nVec, defaultVal)
|
|
end
|
|
|
|
return nVec
|
|
end
|
|
|
|
function cfxZones.getRGBVectorFromZoneProperty(theZone, theProperty, defaultVal)
|
|
if not defaultVal then defaultVal = {1.0, 1.0, 1.0} end
|
|
if #defaultVal ~=3 then defaultVal = {1.0, 1.0, 1.0} end
|
|
local s = cfxZones.getStringFromZoneProperty(theZone, theProperty, "")
|
|
local sVec = dcsCommon.splitString(s, ",")
|
|
local nVec = {}
|
|
for i = 1, 3 do
|
|
n = sVec[i]
|
|
if n then n = tonumber(n) end
|
|
if not n then n = defaultVal[i] end
|
|
if n > 1.0 then n = 1.0 end
|
|
if n < 0 then n = 0 end
|
|
nVec[i] = n
|
|
end
|
|
return nVec
|
|
end
|
|
|
|
function dmlZone:getRGBVectorFromZoneProperty(theProperty, defaultVal)
|
|
if not defaultVal then defaultVal = {1.0, 1.0, 1.0} end
|
|
if #defaultVal ~=3 then defaultVal = {1.0, 1.0, 1.0} end
|
|
local s = self:getStringFromZoneProperty(theProperty, "")
|
|
local sVec = dcsCommon.splitString(s, ",")
|
|
local nVec = {}
|
|
for i = 1, 3 do
|
|
n = sVec[i]
|
|
if n then n = tonumber(n) end
|
|
if not n then n = defaultVal[i] end
|
|
if n > 1.0 then n = 1.0 end
|
|
if n < 0 then n = 0 end
|
|
nVec[i] = n
|
|
end
|
|
return nVec
|
|
end
|
|
|
|
|
|
function cfxZones.getRGBAVectorFromZoneProperty(theZone, theProperty, defaultVal)
|
|
if not defaultVal then defaultVal = {1.0, 1.0, 1.0, 1.0} end
|
|
if #defaultVal ~=4 then defaultVal = {1.0, 1.0, 1.0, 1.0} end
|
|
local s = cfxZones.getStringFromZoneProperty(theZone, theProperty, "")
|
|
s = dcsCommon.trim(s)
|
|
if s:sub(1,1) == "#" then
|
|
-- it's probably a "#RRGGBBAA" format hex string
|
|
local hVec = dcsCommon.hexString2RGBA(s)
|
|
if hVec then return hVec end
|
|
end
|
|
|
|
local sVec = dcsCommon.splitString(s, ",")
|
|
local nVec = {}
|
|
for i = 1, 4 do
|
|
n = sVec[i]
|
|
if n then n = tonumber(n) end
|
|
if not n then n = defaultVal[i] end
|
|
if n > 1.0 then n = 1.0 end
|
|
if n < 0 then n = 0 end
|
|
nVec[i] = n
|
|
end
|
|
|
|
return nVec
|
|
end
|
|
|
|
function dmlZone:getRGBAVectorFromZoneProperty(theProperty, defaultVal)
|
|
if not defaultVal then defaultVal = {1.0, 1.0, 1.0, 1.0} end
|
|
if #defaultVal ~=4 then defaultVal = {1.0, 1.0, 1.0, 1.0} end
|
|
local s = self:getStringFromZoneProperty(theProperty, "")
|
|
s = dcsCommon.trim(s)
|
|
if s:sub(1,1) == "#" then
|
|
-- it's probably a "#RRGGBBAA" format hex string
|
|
local hVec = dcsCommon.hexString2RGBA(s)
|
|
if hVec then return hVec end
|
|
end
|
|
|
|
local sVec = dcsCommon.splitString(s, ",")
|
|
local nVec = {}
|
|
for i = 1, 4 do
|
|
n = sVec[i]
|
|
if n then n = tonumber(n) end
|
|
if not n then n = defaultVal[i] end
|
|
if n > 1.0 then n = 1.0 end
|
|
if n < 0 then n = 0 end
|
|
nVec[i] = n
|
|
end
|
|
|
|
return nVec
|
|
end
|
|
|
|
function cfxZones.getRGBFromZoneProperty(theZone, theProperty, default)
|
|
--if not default then default = {1.0, 1.0, 1.0} end -- white
|
|
local rawRGB = cfxZones.getVectorFromZoneProperty(theZone, theProperty, 3, 1.0)
|
|
local retVal = {}
|
|
for i = 1, 3 do
|
|
local cp = rawRGB[i]
|
|
if cp > 1.0 then cp = 1.0 end
|
|
if cp < 0 then cp = 0 end
|
|
retVal[i] = cp
|
|
end
|
|
return retVal
|
|
end
|
|
|
|
function dmlZone:getRGBFromZoneProperty(theProperty, default)
|
|
--if not default then default = {1.0, 1.0, 1.0} end -- white
|
|
local rawRGB = self:getVectorFromZoneProperty(theProperty, 3, 1.0)
|
|
local retVal = {}
|
|
for i = 1, 3 do
|
|
local cp = rawRGB[i]
|
|
if cp > 1.0 then cp = 1.0 end
|
|
if cp < 0 then cp = 0 end
|
|
retVal[i] = cp
|
|
end
|
|
return retVal
|
|
end
|
|
|
|
|
|
function cfxZones.getSmokeColorStringFromZoneProperty(theZone, theProperty, default) -- smoke as 'red', 'green', or 1..5
|
|
if not default then default = "red" end
|
|
local s = cfxZones.getStringFromZoneProperty(theZone, theProperty, default)
|
|
s = s:lower()
|
|
s = dcsCommon.trim(s)
|
|
-- check numbers
|
|
if (s == "0") then return "green" end
|
|
if (s == "1") then return "red" end
|
|
if (s == "2") then return "white" end
|
|
if (s == "3") then return "orange" end
|
|
if (s == "4") then return "blue" end
|
|
|
|
if s == "green" or
|
|
s == "red" or
|
|
s == "white" or
|
|
s == "orange" or
|
|
s == "blue" then return s end
|
|
|
|
return default
|
|
end
|
|
|
|
function dmlZone:getSmokeColorStringFromZoneProperty(theProperty, default) -- smoke as 'red', 'green', or 1..5
|
|
if not default then default = "red" end
|
|
local s = self:getStringFromZoneProperty(theProperty, default)
|
|
s = s:lower()
|
|
s = dcsCommon.trim(s)
|
|
-- check numbers
|
|
if (s == "0") then return "green" end
|
|
if (s == "1") then return "red" end
|
|
if (s == "2") then return "white" end
|
|
if (s == "3") then return "orange" end
|
|
if (s == "4") then return "blue" end
|
|
|
|
if s == "green" or
|
|
s == "red" or
|
|
s == "white" or
|
|
s == "orange" or
|
|
s == "blue" then return s end
|
|
|
|
return default
|
|
end
|
|
|
|
function cfxZones.getFlareColorStringFromZoneProperty(theZone, theProperty, default) -- smoke as 'red', 'green', or 1..5
|
|
if not default then default = "red" end
|
|
local s = cfxZones.getStringFromZoneProperty(theZone, theProperty, default)
|
|
s = s:lower()
|
|
s = dcsCommon.trim(s)
|
|
-- check numbers
|
|
if (s == "rnd") then return "random" end
|
|
if (s == "0") then return "green" end
|
|
if (s == "1") then return "red" end
|
|
if (s == "2") then return "white" end
|
|
if (s == "3") then return "yellow" end
|
|
if (s == "-1") then return "random" end
|
|
|
|
if s == "green" or
|
|
s == "red" or
|
|
s == "white" or
|
|
s == "yellow" or
|
|
s == "random" then
|
|
return s end
|
|
|
|
return default
|
|
end
|
|
|
|
function dmlZone:getFlareColorStringFromZoneProperty(theProperty, default) -- smoke as 'red', 'green', or 1..5
|
|
if not default then default = "red" end
|
|
local s = self:getStringFromZoneProperty(theProperty, default)
|
|
s = s:lower()
|
|
s = dcsCommon.trim(s)
|
|
-- check numbers
|
|
if (s == "rnd") then return "random" end
|
|
if (s == "0") then return "green" end
|
|
if (s == "1") then return "red" end
|
|
if (s == "2") then return "white" end
|
|
if (s == "3") then return "yellow" end
|
|
if (s == "-1") then return "random" end
|
|
|
|
if s == "green" or
|
|
s == "red" or
|
|
s == "white" or
|
|
s == "yellow" or
|
|
s == "random" then
|
|
return s end
|
|
|
|
return default
|
|
end
|
|
|
|
--
|
|
-- Zone-based wildcard processing
|
|
--
|
|
|
|
-- process <z>
|
|
function cfxZones.processZoneStatics(inMsg, theZone)
|
|
if theZone then
|
|
inMsg = inMsg:gsub("<z>", theZone.name)
|
|
end
|
|
return inMsg
|
|
end
|
|
|
|
function dmlZone:processZoneStatics(inMsg, theZone)
|
|
inMsg = inMsg:gsub("<z>", self.name)
|
|
return inMsg
|
|
end
|
|
|
|
-- process <t>, <lat>, <lon>, <ele>, <mgrs>
|
|
function cfxZones.processSimpleZoneDynamics(inMsg, theZone, timeFormat, imperialUnits)
|
|
if not inMsg then return "<nil inMsg>" end
|
|
-- replace <t> with current mission time HMS
|
|
local absSecs = timer.getAbsTime()-- + env.mission.start_time
|
|
while absSecs > 86400 do
|
|
absSecs = absSecs - 86400 -- subtract out all days
|
|
end
|
|
if not timeFormat then timeFormat = "<:h>:<:m>:<:s>" end
|
|
local timeString = dcsCommon.processHMS(timeFormat, absSecs)
|
|
local outMsg = inMsg:gsub("<t>", timeString)
|
|
|
|
-- replace <lat> with lat of zone point and <lon> with lon of zone point
|
|
-- and <mgrs> with mgrs coords of zone point
|
|
local currPoint = cfxZones.getPoint(theZone)
|
|
local lat, lon = coord.LOtoLL(currPoint)
|
|
lat, lon = dcsCommon.latLon2Text(lat, lon)
|
|
local alt = land.getHeight({x = currPoint.x, y = currPoint.z})
|
|
if imperialUnits then
|
|
alt = math.floor(alt * 3.28084) -- feet
|
|
else
|
|
alt = math.floor(alt) -- meters
|
|
end
|
|
outMsg = outMsg:gsub("<lat>", lat)
|
|
outMsg = outMsg:gsub("<lon>", lon)
|
|
outMsg = outMsg:gsub("<ele>", alt)
|
|
local grid = coord.LLtoMGRS(coord.LOtoLL(currPoint))
|
|
local mgrs = grid.UTMZone .. ' ' .. grid.MGRSDigraph .. ' ' .. grid.Easting .. ' ' .. grid.Northing
|
|
outMsg = outMsg:gsub("<mgrs>", mgrs)
|
|
return outMsg
|
|
end
|
|
|
|
-- process <v: flag>, <rsp: flag> <rrnd>
|
|
function cfxZones.processDynamicValues(inMsg, theZone, msgResponses)
|
|
-- replace all occurences of <v: flagName> with their values
|
|
local pattern = "<v:%s*[%s%w%*%d%.%-_]+>" -- no list allowed but blanks and * and . and - and _ --> we fail on the other specials to keep this simple
|
|
local outMsg = inMsg
|
|
repeat -- iterate all patterns one by one
|
|
local startLoc, endLoc = string.find(outMsg, pattern)
|
|
if startLoc then
|
|
local theValParam = string.sub(outMsg, startLoc, endLoc)
|
|
-- strip lead and trailer
|
|
local param = string.gsub(theValParam, "<v:%s*", "")
|
|
param = string.gsub(param, ">","")
|
|
-- param = dcsCommon.trim(param) -- trim is called anyway
|
|
-- access flag
|
|
local val = cfxZones.getFlagValue(param, theZone)
|
|
val = tostring(val)
|
|
if not val then val = "NULL" end
|
|
-- replace pattern in original with new val
|
|
outMsg = string.gsub(outMsg, pattern, val, 1) -- only one sub!
|
|
end
|
|
until not startLoc
|
|
|
|
-- now process rsp
|
|
pattern = "<rsp:%s*[%s%w%*%d%.%-_]+>" -- no list allowed but blanks and * and . and - and _ --> we fail on the other specials to keep this simple
|
|
|
|
if msgResponses and (#msgResponses > 0) then -- only if this zone has an array
|
|
--trigger.action.outText("enter response proccing", 30)
|
|
repeat -- iterate all patterns one by one
|
|
local startLoc, endLoc = string.find(outMsg, pattern)
|
|
if startLoc then
|
|
local theValParam = string.sub(outMsg, startLoc, endLoc)
|
|
-- strip lead and trailer
|
|
local param = string.gsub(theValParam, "<rsp:%s*", "")
|
|
param = string.gsub(param, ">","")
|
|
|
|
-- access flag
|
|
local val = cfxZones.getFlagValue(param, theZone)
|
|
if not val or (val < 1) then val = 1 end
|
|
if val > #msgResponses then val = #msgResponses end
|
|
|
|
val = msgResponses[val]
|
|
val = dcsCommon.trim(val)
|
|
-- replace pattern in original with new val
|
|
outMsg = string.gsub(outMsg, pattern, val, 1) -- only one sub!
|
|
end
|
|
until not startLoc
|
|
|
|
-- rnd response
|
|
local rndRsp = dcsCommon.pickRandom(msgResponses)
|
|
outMsg = outMsg:gsub ("<rrnd>", rndRsp)
|
|
end
|
|
|
|
return outMsg
|
|
end
|
|
|
|
-- process <t: flag>
|
|
function cfxZones.processDynamicTime(inMsg, theZone, timeFormat)
|
|
if not timeFormat then timeFormat = "<:h>:<:m>:<:s>" end
|
|
-- replace all occurences of <t: flagName> with their values
|
|
local pattern = "<t:%s*[%s%w%*%d%.%-_]+>" -- no list allowed but blanks and * and . and - and _ --> we fail on the other specials to keep this simple
|
|
local outMsg = inMsg
|
|
repeat -- iterate all patterns one by one
|
|
local startLoc, endLoc = string.find(outMsg, pattern)
|
|
if startLoc then
|
|
local theValParam = string.sub(outMsg, startLoc, endLoc)
|
|
-- strip lead and trailer
|
|
local param = string.gsub(theValParam, "<t:%s*", "")
|
|
param = string.gsub(param, ">","")
|
|
-- access flag
|
|
local val = cfxZones.getFlagValue(param, theZone)
|
|
-- use this to process as time value
|
|
--trigger.action.outText("time: accessing <" .. param .. "> and received <" .. val .. ">", 30)
|
|
local timeString = dcsCommon.processHMS(timeFormat, val)
|
|
|
|
if not timeString then timeString = "NULL" end
|
|
-- replace pattern in original with new val
|
|
outMsg = string.gsub(outMsg, pattern, timeString, 1) -- only one sub!
|
|
end
|
|
until not startLoc
|
|
return outMsg
|
|
end
|
|
|
|
-- process <lat/lon/ele/mgrs/lle/latlon/alt/vel/hdg/rhdg/type/player: zone/unit>
|
|
function cfxZones.processDynamicLoc(inMsg, imperialUnits, responses)
|
|
local locales = {"lat", "lon", "ele", "mgrs", "lle", "latlon", "alt", "vel", "hdg", "rhdg", "type", "player", "twn", "loc"}
|
|
local outMsg = inMsg
|
|
local uHead = 0
|
|
for idx, aLocale in pairs(locales) do
|
|
local pattern = "<" .. aLocale .. ":%s*[%s%w%*%d%.%-_]+>"
|
|
repeat -- iterate all patterns one by one
|
|
local startLoc, endLoc = string.find(outMsg, pattern)
|
|
if startLoc then
|
|
local theValParam = string.sub(outMsg, startLoc, endLoc)
|
|
-- strip lead and trailer
|
|
local param = string.gsub(theValParam, "<" .. aLocale .. ":%s*", "")
|
|
param = string.gsub(param, ">","")
|
|
-- find zone or unit
|
|
param = dcsCommon.trim(param)
|
|
local thePoint = nil
|
|
local tZone = cfxZones.getZoneByName(param)
|
|
local tUnit = Unit.getByName(param)
|
|
local spd = 0
|
|
local angels = 0
|
|
local theType = "<errType>"
|
|
local playerName = "Unknown"
|
|
if tZone then
|
|
theType = "Zone"
|
|
playerName = "?zone?"
|
|
thePoint = cfxZones.getPoint(tZone)
|
|
if tZone.linkedUnit and Unit.isExist(tZone.linkedUnit) then
|
|
local lU = tZone.linkedUnit
|
|
local masterPoint = lU:getPoint()
|
|
thePoint.y = masterPoint.y
|
|
spd = dcsCommon.getUnitSpeed(lU)
|
|
spd = math.floor(spd * 3.6)
|
|
uHead = math.floor(dcsCommon.getUnitHeading(tUnit) * 57.2958) -- to degrees.
|
|
else
|
|
-- since zones always have elevation of 0,
|
|
-- now get the elevation from the map
|
|
thePoint.y = land.getHeight({x = thePoint.x, y = thePoint.z})
|
|
end
|
|
elseif tUnit then
|
|
if Unit.isExist(tUnit) then
|
|
theType = tUnit:getTypeName()
|
|
if tUnit.getPlayerName and tUnit:getPlayerName() then
|
|
playerName = tUnit:getPlayerName()
|
|
end
|
|
thePoint = tUnit:getPoint()
|
|
spd = dcsCommon.getUnitSpeed(tUnit)
|
|
-- convert m/s to km/h
|
|
spd = math.floor(spd * 3.6)
|
|
uHead = math.floor(dcsCommon.getUnitHeading(tUnit) * 57.2958) -- to degrees.
|
|
end
|
|
else
|
|
-- nothing to do, remove me.
|
|
end
|
|
|
|
local locString = "err"
|
|
if thePoint then
|
|
-- now that we have a point, we can do locale-specific
|
|
-- processing. return result in locString
|
|
local lat, lon, alt = coord.LOtoLL(thePoint)
|
|
lat, lon = dcsCommon.latLon2Text(lat, lon)
|
|
angels = math.floor(thePoint.y)
|
|
if imperialUnits then
|
|
alt = math.floor(alt * 3.28084) -- feet
|
|
spd = math.floor(spd * 0.539957) -- km/h to knots
|
|
angels = math.floor(angels * 3.28084)
|
|
else
|
|
alt = math.floor(alt) -- meters
|
|
end
|
|
|
|
if angels > 1000 then
|
|
angels = math.floor(angels / 100) * 100
|
|
end
|
|
|
|
if aLocale == "lat" then locString = lat
|
|
elseif aLocale == "lon" then locString = lon
|
|
elseif aLocale == "ele" then locString = tostring(alt)
|
|
elseif aLocale == "lle" then locString = lat .. " " .. lon .. " ele " .. tostring(alt)
|
|
elseif aLocale == "latlon" then locString = lat .. " " .. lon
|
|
elseif aLocale == "alt" then locString = tostring(angels) -- don't confuse alt and angels, bad var naming here
|
|
elseif aLocale == "vel" then locString = tostring(spd)
|
|
elseif aLocale == "hdg" then locString = tostring(uHead)
|
|
elseif aLocale == "type" then locString = theType
|
|
elseif aLocale == "player" then locString = playerName
|
|
elseif aLocale == "rhdg" and (responses) then
|
|
local offset = cfxZones.rspMapper360(uHead, #responses)
|
|
locString = dcsCommon.trim(responses[offset])
|
|
elseif aLocale == "twn" then
|
|
if twn and towns then locString = twn.closestTownTo(thePoint)
|
|
else locString = "!twn!" end
|
|
elseif aLocale == "loc" then
|
|
if twn and towns then
|
|
local name, data, dist = twn.closestTownTo(thePoint)
|
|
local units = "km"
|
|
local mdist= dist * 0.539957
|
|
dist = math.floor(dist/100) / 10
|
|
mdist = math.floor(mdist/100) / 10
|
|
if imperialUnits then
|
|
dist = mdist
|
|
units = "nm"
|
|
end
|
|
local bear = dcsCommon.compassPositionOfARelativeToB(thePoint, data.p)
|
|
locString = dist .. units .. " " .. bear .. " of " .. name
|
|
else locString = "!twn!" end
|
|
else
|
|
-- we have mgrs
|
|
local grid = coord.LLtoMGRS(coord.LOtoLL(thePoint))
|
|
locString = grid.UTMZone .. ' ' .. grid.MGRSDigraph .. ' ' .. grid.Easting .. ' ' .. grid.Northing
|
|
end
|
|
end
|
|
-- replace pattern in original with new val
|
|
outMsg = string.gsub(outMsg, pattern, locString, 1) -- only one sub!
|
|
end -- if startloc
|
|
until not startLoc
|
|
end -- for all locales
|
|
return outMsg
|
|
end
|
|
|
|
-- process reference that can be flag, Zone, or unit.
|
|
-- i.e. <coa: xyz>
|
|
function cfxZones.processDynamicVZU(inMsg)
|
|
local locales = {"coa",}
|
|
local outMsg = inMsg
|
|
local uHead = 0
|
|
for idx, aLocale in pairs(locales) do
|
|
local pattern = "<" .. aLocale .. ":%s*[%s%w%*%d%.%-_]+>" -- e.g. "<coa: flag Name>
|
|
repeat -- iterate all patterns one by one
|
|
local startLoc, endLoc = string.find(outMsg, pattern)
|
|
if startLoc then
|
|
local theValParam = string.sub(outMsg, startLoc, endLoc)
|
|
-- strip lead and trailer
|
|
local param = string.gsub(theValParam, "<" .. aLocale .. ":%s*", "") -- remove "<coa:"
|
|
param = string.gsub(param, ">","") -- remove trailing ">"
|
|
-- find zone or unit
|
|
param = dcsCommon.trim(param) -- param = "flag Name"
|
|
local tZone = cfxZones.getZoneByName(param)
|
|
local tUnit = Unit.getByName(param)
|
|
|
|
local locString = "err"
|
|
if aLocale == "coa" then
|
|
coa = trigger.misc.getUserFlag(param)
|
|
if tZone then coa = tZone.owner end
|
|
if tUnit and Unit:isExist(tUnit) then coa = tUnit:getCoalition() end
|
|
locString = dcsCommon.coalition2Text(coa)
|
|
end
|
|
|
|
outMsg = string.gsub(outMsg, pattern, locString, 1) -- only one sub!
|
|
end -- if startloc
|
|
until not startLoc
|
|
end -- for all locales
|
|
return outMsg
|
|
end
|
|
|
|
-- process two-value vars that can be flag or unit and return interpreted value
|
|
-- i.e. <alive: Aerial-1-1>
|
|
function cfxZones.processDynamicValueVU(inMsg)
|
|
local locales = {"yes", "true", "alive", "in"}
|
|
local outMsg = inMsg
|
|
local uHead = 0
|
|
for idx, aLocale in pairs(locales) do
|
|
local pattern = "<" .. aLocale .. ":%s*[%s%w%*%d%.%-_]+>" -- e.g. "<yes: flagOrUnitName>
|
|
repeat -- iterate all patterns one by one
|
|
local startLoc, endLoc = string.find(outMsg, pattern)
|
|
if startLoc then
|
|
local theValParam = string.sub(outMsg, startLoc, endLoc)
|
|
-- strip lead and trailer
|
|
local param = string.gsub(theValParam, "<" .. aLocale .. ":%s*", "") -- remove "<alive:"
|
|
param = string.gsub(param, ">","") -- remove trailing ">"
|
|
-- find zone or unit
|
|
param = dcsCommon.trim(param) -- param = "flagOrUnitName"
|
|
local tUnit = Unit.getByName(param)
|
|
local yesNo = trigger.misc.getUserFlag(param) ~= 0
|
|
if tUnit then yesNo = Unit.isExist(tUnit) end
|
|
local locString = "err"
|
|
if aLocale == "yes" then
|
|
if yesNo then locString = "yes" else locString = "no" end
|
|
elseif aLocale == "true" then
|
|
if yesNo then locString = "true" else locString = "false" end
|
|
elseif aLocale == "alive" then
|
|
if yesNo then locString = "alive" else locString = "dead" end
|
|
elseif aLocale == "in" then
|
|
if yesNo then locString = "in" else locString = "out" end
|
|
end
|
|
|
|
outMsg = string.gsub(outMsg, pattern, locString, 1) -- only one sub!
|
|
end -- if startloc
|
|
until not startLoc
|
|
end -- for all locales
|
|
return outMsg
|
|
end
|
|
|
|
function cfxZones.processDynamicAB(inMsg, locale)
|
|
local outMsg = inMsg
|
|
if not locale then locale = "A/B" end
|
|
|
|
-- <A/B: flagOrUnitName [val A | val B]>
|
|
local replacerValPattern = "<".. locale .. ":%s*[%s%w%*%d%.%-_]+" .. "%[[%s%w]+|[%s%w]+%]"..">"
|
|
repeat
|
|
local startLoc, endLoc = string.find(outMsg, replacerValPattern)
|
|
if startLoc then
|
|
local rp = string.sub(outMsg, startLoc, endLoc)
|
|
-- get val/unit name
|
|
local valA, valB = string.find(rp, ":%s*[%s%w%*%d%.%-_]+%[")
|
|
local val = string.sub(rp, valA+1, valB-1)
|
|
val = dcsCommon.trim(val)
|
|
-- get left and right
|
|
local leftA, leftB = string.find(rp, "%[[%s%w]+|" ) -- from "[" to "|"
|
|
local rightA, rightB = string.find(rp, "|[%s%w]+%]") -- from "|" to "]"
|
|
left = string.sub(rp, leftA+1, leftB-1)
|
|
left = dcsCommon.trim(left)
|
|
right = string.sub(rp, rightA+1, rightB-1)
|
|
right = dcsCommon.trim(right)
|
|
local yesno = false
|
|
-- see if unit exists
|
|
local theUnit = Unit.getByName(val)
|
|
if theUnit then
|
|
yesno = Unit:isExist(theUnit)
|
|
else
|
|
yesno = trigger.misc.getUserFlag(val) ~= 0
|
|
end
|
|
|
|
local locString = left
|
|
if yesno then locString = right end
|
|
outMsg = string.gsub(outMsg, replacerValPattern, locString, 1)
|
|
end
|
|
until not startLoc
|
|
return outMsg
|
|
end
|
|
|
|
function cfxZones.rspMapper360(directionInDegrees, numResponses)
|
|
-- maps responses around a clock. Clock has 12 'responses' (12, 1, .., 11),
|
|
-- with the first (12) also mapping to the last half arc
|
|
-- this method dynamically 'winds' the responses around
|
|
-- a clock and returns the index of the message to display
|
|
if numResponses < 1 then numResponses = 1 end
|
|
directionInDegrees = math.floor(directionInDegrees)
|
|
while directionInDegrees < 0 do directionInDegrees = directionInDegrees + 360 end
|
|
while directionInDegrees >= 360 do directionInDegrees = directionInDegrees - 360 end
|
|
-- now we have 0..360
|
|
-- calculate arc per item
|
|
local arcPerItem = 360 / numResponses
|
|
local halfArc = arcPerItem / 2
|
|
|
|
-- we now map 0..360 to (0-halfArc..360-halfArc) by shifting
|
|
-- direction by half-arc and clipping back 0..360
|
|
-- and now we can directly derive the index of the response
|
|
directionInDegrees = directionInDegrees + halfArc
|
|
if directionInDegrees >= 360 then directionInDegrees = directionInDegrees - 360 end
|
|
|
|
local index = math.floor(directionInDegrees / arcPerItem) + 1 -- 1 .. numResponses
|
|
|
|
return index
|
|
end
|
|
|
|
-- replaces dcsCommon with same name
|
|
-- timeFormat is optional, default is "<:h>:<:m>:<:s>"
|
|
-- imperialUnits is optional, defaults to meters
|
|
-- responses is an array of string, defaults to {}
|
|
function cfxZones.processStringWildcards(inMsg, theZone, timeFormat, imperialUnits, responses)
|
|
if not inMsg then return "<nil inMsg>" end
|
|
local formerType = type(inMsg)
|
|
if formerType ~= "string" then inMsg = tostring(inMsg) end
|
|
if not inMsg then inMsg = "<inMsg is incompatible type " .. formerType .. ">" end
|
|
local theMsg = inMsg
|
|
-- process common DCS stuff like /n
|
|
theMsg = dcsCommon.processStringWildcards(theMsg) -- call old inherited
|
|
-- process <z>
|
|
theMsg = cfxZones.processZoneStatics(theMsg, theZone)
|
|
-- process <t>, <lat>, <lon>, <ele>, <mgrs>
|
|
theMsg = cfxZones.processSimpleZoneDynamics(theMsg, theZone, timeFormat, imperialUnits)
|
|
-- process <v: flag>, <rsp: flag> <rrnd>
|
|
theMsg = cfxZones.processDynamicValues(theMsg, theZone, responses)
|
|
-- process <t: flag>
|
|
theMsg = cfxZones.processDynamicTime(theMsg, theZone, timeFormat)
|
|
-- process <lat/lon/ele/mgrs/lle/latlon/alt/vel/hdg/rhdg/type/player: zone/unit>
|
|
theMsg = cfxZones.processDynamicLoc(theMsg, imperialUnits, responses)
|
|
-- process values that can be derived from flag (default), zone or unit
|
|
theMsg = cfxZones.processDynamicVZU(theMsg)
|
|
theMsg = cfxZones.processDynamicAB(theMsg)
|
|
theMsg = cfxZones.processDynamicValueVU(theMsg)
|
|
return theMsg
|
|
end
|
|
|
|
--
|
|
-- ============
|
|
-- MOVING ZONES
|
|
-- ============
|
|
--
|
|
-- Moving zones contain a link to their unit
|
|
-- they are always located at an offset (x,z) or delta, phi
|
|
-- to their master unit. delta phi allows adjustment for heading
|
|
-- The cool thing about moving zones in cfx is that they do not
|
|
-- require special handling, they are always updated
|
|
-- and work with 'pointinzone' etc automatically
|
|
|
|
-- Always works on cfx Zones, NEVER on DCS zones.
|
|
--
|
|
-- requires that readFromDCS has been done
|
|
--
|
|
function cfxZones.getDCSOrigin(aZone)
|
|
local o = {}
|
|
o.x = aZone.dcsOrigin.x
|
|
o.y = 0
|
|
o.z = aZone.dcsOrigin.z
|
|
return o
|
|
end
|
|
|
|
function dmlZone:getDCSOrigin()
|
|
local o = {}
|
|
if not self.dcsOrigin then
|
|
trigger.action.outText("dmlZone (OOP): no dcsOrigin defined for zone <" .. self.name .. ">", 30)
|
|
o.x = 0
|
|
o.y = 0
|
|
o.z = 0
|
|
else
|
|
o.x = self.dcsOrigin.x
|
|
o.y = 0
|
|
o.z = self.dcsOrigin.z
|
|
end
|
|
return o
|
|
end
|
|
|
|
function cfxZones.getLinkedUnit(theZone)
|
|
if not theZone then return nil end
|
|
if not theZone.linkedUnit then return nil end
|
|
if not Unit.isExist(theZone.linkedUnit) then return nil end
|
|
return theZone.linkedUnit
|
|
end
|
|
|
|
function dmlZone:getLinkedUnit()
|
|
if not self.linkedUnit then return nil end
|
|
if not Unit.isExist(self.linkedUnit) then return nil end
|
|
return self.linkedUnit
|
|
end
|
|
|
|
function cfxZones.getPoint(aZone, getHeight) -- always works, even linked, returned point can be reused
|
|
-- returned y (when using getHeight) is that of the land, else 0
|
|
if not getHeight then getHeight = false end
|
|
if aZone.linkedUnit then
|
|
local theUnit = aZone.linkedUnit
|
|
-- has a link. is link existing?
|
|
if Unit.isExist(theUnit) then
|
|
-- updates zone position
|
|
cfxZones.centerZoneOnUnit(aZone, theUnit)
|
|
local dx = aZone.dx
|
|
local dy = aZone.dy
|
|
if aZone.useHeading then
|
|
dx, dy = cfxZones.calcHeadingOffset(aZone, theUnit)
|
|
end
|
|
cfxZones.offsetZone(aZone, dx, dy)
|
|
end
|
|
end
|
|
local thePos = {}
|
|
thePos.x = aZone.point.x
|
|
thePos.z = aZone.point.z
|
|
if not getHeight then
|
|
thePos.y = 0 -- aZone.y
|
|
else
|
|
thePos.y = land.getHeight({x = thePos.x, y = thePos.z})
|
|
end
|
|
|
|
return thePos
|
|
end
|
|
|
|
function dmlZone:getPoint(getHeight)
|
|
if not getHeight then getHeight = false end
|
|
if self.linkedUnit then
|
|
local theUnit = self.linkedUnit
|
|
-- has a link. is link existing?
|
|
if Unit.isExist(theUnit) then
|
|
-- updates zone position
|
|
self:centerZoneOnUnit(theUnit)
|
|
local dx = self.dx
|
|
local dy = self.dy
|
|
if self.useHeading then
|
|
dx, dy = self:calcHeadingOffset(theUnit)
|
|
end
|
|
self:offsetZone(dx, dy)
|
|
end
|
|
end
|
|
local thePos = {}
|
|
thePos.x = self.point.x
|
|
thePos.z = self.point.z
|
|
if not getHeight then
|
|
thePos.y = 0 -- aZone.y
|
|
else
|
|
thePos.y = land.getHeight({x = thePos.x, y = thePos.z})
|
|
end
|
|
|
|
return thePos
|
|
end
|
|
|
|
function dmlZone:getName() -- no cfxZones.bridge!
|
|
return self.name
|
|
end
|
|
|
|
function dmlZone:getCoalition()
|
|
-- automatically support masterOwner. Warning: cloners etc can reference itself!
|
|
if self.masterOwner then return self.masterOwner.owner end -- zone must exist
|
|
return self.owner
|
|
end
|
|
|
|
function cfxZones.getCoalition(theZone)
|
|
return theZone:getCoalition()
|
|
end
|
|
|
|
function dmlZone:getTypeName()
|
|
return "dmlZone"
|
|
end
|
|
|
|
function cfxZones.getTypeName(theZone)
|
|
return theZone:getTypeName()
|
|
end
|
|
|
|
function cfxZones.linkUnitToZone(theUnit, theZone, dx, dy) -- note: dy is really Z, don't get confused!!!!
|
|
theZone.linkedUnit = theUnit
|
|
if not dx then dx = 0 end
|
|
if not dy then dy = 0 end
|
|
theZone.dx = dx
|
|
theZone.dy = dy
|
|
theZone.rxy = math.sqrt(dx * dx + dy * dy) -- radius
|
|
local uName = theUnit:getName()
|
|
-- local unitHeading = dcsCommon.getUnitHeading(theUnit)
|
|
local unitHeading = dcsCommon.unitName2Heading[uName] -- get original unit's heading from ME
|
|
local bearingOffset = math.atan2(dy, dx) -- rads
|
|
if bearingOffset < 0 then bearingOffset = bearingOffset + 2 * 3.141592 end
|
|
|
|
local dPhi = bearingOffset - unitHeading
|
|
if dPhi < 0 then dPhi = dPhi + 2 * 3.141592 end
|
|
if (theZone.verbose and theZone.useHeading) then
|
|
trigger.action.outText("Zone is at <" .. math.floor(57.2958 * dPhi) .. "> relative to unit heading", 30)
|
|
end
|
|
theZone.dPhi = dPhi -- constant delta between unit heading and
|
|
-- direction to zone
|
|
theZone.uHdg = unitHeading -- original unit heading to turn other
|
|
-- units if need be
|
|
end
|
|
|
|
function dmlZone:linkUnitToZone(theUnit, dx, dy) -- note: dy is really Z, don't get confused!!!!
|
|
self.linkedUnit = theUnit
|
|
if not dx then dx = 0 end
|
|
if not dy then dy = 0 end
|
|
self.dx = dx
|
|
self.dy = dy
|
|
self.rxy = math.sqrt(dx * dx + dy * dy) -- radius
|
|
local unitHeading = dcsCommon.getUnitHeading(theUnit)
|
|
local bearingOffset = math.atan2(dy, dx) -- rads
|
|
if bearingOffset < 0 then bearingOffset = bearingOffset + 2 * 3.141592 end
|
|
|
|
local dPhi = bearingOffset - unitHeading
|
|
if dPhi < 0 then dPhi = dPhi + 2 * 3.141592 end
|
|
if (self.verbose and self.useHeading) then
|
|
trigger.action.outText("Zone <" .. self.name .. "> is at <" .. math.floor(57.2958 * dPhi) .. "> relative to unit heading", 30)
|
|
end
|
|
self.dPhi = dPhi -- constant delta between unit heading and
|
|
-- direction to zone
|
|
self.uHdg = unitHeading -- original unit heading to turn other
|
|
-- units if need be
|
|
end
|
|
|
|
function cfxZones.zonesLinkedToUnit(theUnit) -- returns all zones linked to this unit
|
|
if not theUnit then return {} end
|
|
local linkedZones = {}
|
|
for idx, theZone in pairs (cfxZones.zones) do
|
|
if theZone.linkedUnit == theUnit then
|
|
table.insert(linkedZones, theZone)
|
|
end
|
|
end
|
|
return linkedZones
|
|
end
|
|
|
|
function cfxZones.calcHeadingOffset(aZone, theUnit)
|
|
-- recalc dx and dy based on ry and current heading
|
|
-- since 0 degrees is [0,1] = [0,r] the calculation of
|
|
-- rotated coords can be simplified from
|
|
-- xr = x cos phi - y sin phi = -r sin phi
|
|
-- yr = y cos phi + x sin phi = r cos phi
|
|
local unitHeading = dcsCommon.getUnitHeading(theUnit)
|
|
-- add heading offset
|
|
local zoneBearing = unitHeading + aZone.dPhi
|
|
if zoneBearing > 2 * 3.141592 then zoneBearing = zoneBearing - 2 * 3.141592 end
|
|
|
|
-- in DCS, positive x is north (wtf?) and positive z is east
|
|
local dy = (-aZone.rxy) * math.sin(zoneBearing)
|
|
local dx = aZone.rxy * math.cos(zoneBearing)
|
|
return dx, -dy -- note: dy is z coord!!!!
|
|
end
|
|
|
|
function dmlZone:calcHeadingOffset(theUnit)
|
|
local unitHeading = dcsCommon.getUnitHeading(theUnit)
|
|
local zoneBearing = unitHeading + self.dPhi
|
|
if zoneBearing > 2 * 3.141592 then zoneBearing = zoneBearing - 2 * 3.141592 end
|
|
-- in DCS, positive x is north (wtf?) and positive z is east
|
|
local dy = (-self.rxy) * math.sin(zoneBearing)
|
|
local dx = self.rxy * math.cos(zoneBearing)
|
|
return dx, -dy -- note: dy is z coord!!!!
|
|
end
|
|
|
|
|
|
function cfxZones.updateMovingZones()
|
|
cfxZones.updateSchedule = timer.scheduleFunction(cfxZones.updateMovingZones, {}, timer.getTime() + 1/cfxZones.ups)
|
|
-- simply scan all cfx zones for the linkName property, and if present
|
|
-- update the zone's points
|
|
for aName,aZone in pairs(cfxZones.zones) do
|
|
-- only do this if ther is a linkName property,
|
|
-- else this zone isn't linked. link name is harmonized from
|
|
-- both linkUnit non-DML and linedUnit DML
|
|
if aZone.linkName then
|
|
if aZone.linkBroken then
|
|
-- try to relink
|
|
cfxZones.initLink(aZone)
|
|
else --if aZone.linkName then
|
|
-- always re-acquire linkedUnit via Unit.getByName()
|
|
-- this way we gloss over any replacements via spawns/clones
|
|
aZone.linkedUnit = Unit.getByName(aZone.linkName)
|
|
if not aZone.linkUnit then aZone.linkUnit = StaticObject.getByName(aZone.linkName) end
|
|
end
|
|
|
|
if aZone.linkedUnit then
|
|
local theUnit = aZone.linkedUnit
|
|
-- has a link. is link existing?
|
|
if theUnit:isExist() then
|
|
cfxZones.centerZoneOnUnit(aZone, theUnit)
|
|
local dx = aZone.dx
|
|
local dy = aZone.dy -- this is actually z
|
|
if aZone.useHeading then
|
|
dx, dy = cfxZones.calcHeadingOffset(aZone, theUnit)
|
|
end
|
|
cfxZones.offsetZone(aZone, dx, dy)
|
|
else
|
|
-- we lost link (track level)
|
|
aZone.linkBroken = true
|
|
aZone.linkedUnit = nil
|
|
end
|
|
else
|
|
-- we lost link (top level)
|
|
aZone.linkBroken = true
|
|
aZone.linkedUnit = nil
|
|
end
|
|
else
|
|
-- this zone isn't linked
|
|
end
|
|
end
|
|
end
|
|
|
|
function cfxZones.initLink(theZone)
|
|
theZone.linkBroken = true
|
|
theZone.linkedUnit = nil
|
|
theUnit = Unit.getByName(theZone.linkName) -- unit or static
|
|
if not theUnit then theUnit = StaticObject.getByName(theZone.linkName) end
|
|
if theUnit then
|
|
local dx = 0
|
|
local dz = 0
|
|
if theZone.useOffset or theZone.useHeading then
|
|
local A = cfxZones.getDCSOrigin(theZone)
|
|
local B = dcsCommon.getOrigPositionByID(theZone.linkedUID)
|
|
|
|
local delta = dcsCommon.vSub(A,B)
|
|
dx = delta.x
|
|
dz = delta.z
|
|
end
|
|
cfxZones.linkUnitToZone(theUnit, theZone, dx, dz) -- also sets theZone.linkedUnit
|
|
|
|
if theZone.verbose then
|
|
trigger.action.outText("Link established for zone <" .. theZone.name .. "> to unit <" .. theZone.linkName .. ">: dx=<" .. math.floor(dx) .. ">, dz=<" .. math.floor(dz) .. "> dist = <" .. math.floor(math.sqrt(dx * dx + dz * dz)) .. ">" , 30)
|
|
end
|
|
theZone.linkBroken = nil
|
|
else
|
|
if theZone.verbose then
|
|
trigger.action.outText("Linked unit: no unit <" .. theZone.linkName .. "> to link <" .. theZone.name .. "> to", 30)
|
|
end
|
|
end
|
|
end
|
|
|
|
function dmlZone:initLink()
|
|
self.linkBroken = true
|
|
self.linkedUnit = nil
|
|
theUnit = Unit.getByName(self.linkName) -- unit or static
|
|
if not theUnit then theUnit = StaticObject.getByName(self.linkName) end
|
|
if theUnit then
|
|
local dx = 0
|
|
local dz = 0
|
|
if self.useOffset or self.useHeading then
|
|
local A = self:getDCSOrigin()
|
|
local B = dcsCommon.getOrigPositionByID(self.linkedUID)
|
|
local delta = dcsCommon.vSub(A,B)
|
|
dx = delta.x
|
|
dz = delta.z
|
|
end
|
|
self:linkUnitToZone(theUnit, dx, dz) -- also sets theZone.linkedUnit
|
|
|
|
if self.verbose then
|
|
trigger.action.outText("DML:Link established for zone <" .. self.name .. "> to unit <" .. self.linkName .. ">: dx=<" .. math.floor(dx) .. ">, dz=<" .. math.floor(dz) .. "> dist = <" .. math.floor(math.sqrt(dx * dx + dz * dz)) .. ">" , 30)
|
|
end
|
|
self.linkBroken = nil
|
|
|
|
else
|
|
if self.verbose then
|
|
trigger.action.outText("Linked unit: no unit <" .. self.linkName .. "> to link <" .. self.name .. "> to", 30)
|
|
end
|
|
end
|
|
end
|
|
|
|
function cfxZones.startMovingZones()
|
|
-- read all zones, and look for a property called 'linkedUnit'
|
|
-- which will make them a linked zone if there is a unit that exists
|
|
-- also suppors 'useOffset' and 'useHeading'
|
|
for aName,aZone in pairs(cfxZones.zones) do
|
|
|
|
local lU = nil
|
|
-- check if DCS zone has the linkUnit new attribute introduced in
|
|
-- late 2022 with 2.8
|
|
if aZone.dcsZone.linkUnit then
|
|
local theID = aZone.dcsZone.linkUnit
|
|
lU = dcsCommon.getUnitNameByID(theID) -- can be unit OR STATIC OBJECT
|
|
if not lU then
|
|
trigger.action.outText("WARNING: Zone <" .. aZone.name .. ">: cannot resolve linked unit ID <" .. theID .. ">", 30)
|
|
lU = "***DML link err***"
|
|
end
|
|
aZone.linkedUID = lU
|
|
elseif aZone:hasProperty("linkedUnit") then
|
|
lU = aZone:getZoneProperty("linkedUnit") -- getString: name of unit
|
|
local luid = dcsCommon.unitName2ID[lU]
|
|
if luid then
|
|
aZone.linkedUID = luid
|
|
else
|
|
trigger.action.outText("WARNING: zone <" .. aZone.name .. "> linked unit (by attribute) <" .. lU .. "> does not exist!", 30)
|
|
lU = nil
|
|
end
|
|
end
|
|
|
|
-- sanity check
|
|
if aZone.dcsZone.linkUnit and aZone:hasProperty("linkedUnit") then
|
|
trigger.action.outText("WARNING: Zone <" .. aZone.name .. "> has dual unit link definition. Will use link to unit <" .. lU .. ">", 30)
|
|
end
|
|
|
|
if lU then
|
|
aZone.linkName = lU
|
|
aZone.useOffset = aZone:getBoolFromZoneProperty("useOffset", false)
|
|
aZone.useHeading = aZone:getBoolFromZoneProperty("useHeading", false)
|
|
cfxZones.initLink(aZone)
|
|
end
|
|
|
|
end
|
|
end
|
|
|
|
--
|
|
-- marking zones
|
|
--
|
|
|
|
function cfxZones.spreadNObjectsOverLine(theZone, n, objType, left, right, cty) -- leaves last position free
|
|
trigger.action.outText("left = " .. dcsCommon.point2text(left) .. ", right = " .. dcsCommon.point2text(right),30)
|
|
|
|
local a = {x=left.x, y=left.z}
|
|
local b = {x=right.x, y=right.z}
|
|
local dir = dcsCommon.vSub(b,a) -- vector from left to right
|
|
local dirInc = dcsCommon.vMultScalar(dir, 1/n)
|
|
local count = 0
|
|
local p = {x=left.x, y = left.z}
|
|
local baseName = dcsCommon.uuid(theZone.name)
|
|
while count < n do
|
|
local theStaticData = dcsCommon.createStaticObjectData(dcsCommon.uuid(theZone.name), objType)
|
|
dcsCommon.moveStaticDataTo(theStaticData, p.x, p.y)
|
|
local theObject = coalition.addStaticObject(cty, theStaticData)
|
|
p = dcsCommon.vAdd(p, dirInc)
|
|
count = count + 1
|
|
end
|
|
end
|
|
|
|
function cfxZones.markZoneWithObjects(theZone, objType, qtrNum, markCenter, cty) -- returns set
|
|
if not objType then objType = "Black_Tyre_RF" end
|
|
if not qtrNum then qtrNum = 3 end -- +1 for number of marks per quarter
|
|
if not cty then cty = dcsCommon.getACountryForCoalition(0) end -- some neutral county
|
|
local p = theZone:getPoint()
|
|
local newObjects = {}
|
|
|
|
if theZone.isPoly then
|
|
-- we place 4 * (qtrnum + 1) objects around the edge of the zone
|
|
-- we mark each poly along v-->v+1, placing ip and qtrNum additional points
|
|
local o = cfxZones.spreadNObjectsOverLine(theZone, qtrNum + 1, objType, theZone.poly[1], theZone.poly[2], cty)
|
|
local p = cfxZones.spreadNObjectsOverLine(theZone, qtrNum + 1, objType, theZone.poly[2], theZone.poly[3], cty)
|
|
local q = cfxZones.spreadNObjectsOverLine(theZone, qtrNum + 1, objType, theZone.poly[3], theZone.poly[4], cty)
|
|
local r = cfxZones.spreadNObjectsOverLine(theZone, qtrNum + 1, objType, theZone.poly[4], theZone.poly[1], cty)
|
|
o = dcsCommon.combineTables(o,p)
|
|
p = dcsCommon.combineTables(q,r)
|
|
newObjects = dcsCommon.combineTables(o,p)
|
|
|
|
else
|
|
local numObjects = (qtrNum + 1) * 4
|
|
local degrees = 3.14157 / 180
|
|
local degreeIncrement = (360 / numObjects) * degrees
|
|
local currDegree = 0
|
|
local radius = theZone.radius
|
|
for i=1, numObjects do
|
|
local ox = p.x + math.cos(currDegree) * radius
|
|
local oy = p.z + math.sin(currDegree) * radius -- note: z!
|
|
local theStaticData = dcsCommon.createStaticObjectData(dcsCommon.uuid(theZone.name), objType)
|
|
dcsCommon.moveStaticDataTo(theStaticData, ox, oy)
|
|
local theObject = coalition.addStaticObject(cty, theStaticData)
|
|
table.insert(newObjects, theObject)
|
|
currDegree = currDegree + degreeIncrement
|
|
end
|
|
end
|
|
|
|
if markCenter then
|
|
-- also mark the center
|
|
local theObject = cfxZones.markPointWithObject(p, objType, cty)
|
|
table.insert(newObjects, theObject)
|
|
end
|
|
|
|
return newObjects
|
|
end
|
|
|
|
function dmlZone:markZoneWithObjects(objType, qtrNum, markCenter, cty) -- returns set
|
|
return cfxZones.markZoneWithObjects(self, objType, qtrNum, markCenter)
|
|
end
|
|
|
|
function cfxZones.markCenterWithObject(theZone, objType, cty) -- returns object
|
|
local p = cfxZones.getPoint(theZone)
|
|
local theObject = cfxZones.markPointWithObject(theZone, p, objType, cty)
|
|
return theObject
|
|
end
|
|
|
|
function dmlZone:markCenterWithObject(objType, cty) -- returns object
|
|
return cfxZones.markCenterWithObject(self, objType, cty)
|
|
end
|
|
|
|
function cfxZones.markPointWithObject(theZone, p, theType, cty) -- returns object
|
|
if not cty then cty = dcsCommon.getACountryForCoalition(0) end
|
|
local ox = p.x
|
|
local oy = p.y
|
|
if p.z then oy = p.z end -- support vec 2 and vec 3
|
|
local theStaticData = dcsCommon.createStaticObjectData(dcsCommon.uuid(theZone.name), theType)
|
|
dcsCommon.moveStaticDataTo(theStaticData, ox, oy)
|
|
local theObject = coalition.addStaticObject(cty, theStaticData)
|
|
return theObject
|
|
end
|
|
|
|
function dmlZone:markPointWithObject(p, theType, cty) -- returns object
|
|
return cfxZones.markPointWithObject(self, p, theType, cty)
|
|
end
|
|
--
|
|
-- ===========
|
|
-- INIT MODULE
|
|
-- ===========
|
|
--
|
|
|
|
function cfxZones.initZoneVerbosity()
|
|
for aName,aZone in pairs(cfxZones.zones) do
|
|
-- support for zone-local verbose flag
|
|
aZone.verbose = cfxZones.getBoolFromZoneProperty(aZone, "verbose", false)
|
|
end
|
|
end
|
|
|
|
function cfxZones.init()
|
|
-- read all zones into my own db
|
|
cfxZones.readFromDCS(true) -- true: erase old
|
|
|
|
-- pre-read zone owner for all zones
|
|
-- much like verbose, all zones have owner
|
|
for n, aZone in pairs(cfxZones.zones) do
|
|
aZone.owner = cfxZones.getCoalitionFromZoneProperty(aZone, "owner", 0)
|
|
|
|
if aZone:hasProperty("masterOwner") then
|
|
local mo = aZone:getStringFromZoneProperty("masterOwner", "forgotten master")
|
|
mo = dcsCommon.trim(mo)
|
|
if mo == "*" then mo = aZone.name end
|
|
local mz = cfxZones.getZoneByName(mo)
|
|
if not mz then
|
|
trigger.action.outText("+++fcxZones: WARNING: Master Owner <" .. mo .. "> for zone <" .. aZone.name .. "> does not exist!", 30)
|
|
else
|
|
aZone.masterOwner = mz
|
|
aZone.owner = mz.owner
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
-- enable all zone's verbose flags if present
|
|
-- must be done BEFORE we start the moving zones
|
|
cfxZones.initZoneVerbosity()
|
|
|
|
-- now initialize moving zones
|
|
cfxZones.startMovingZones()
|
|
cfxZones.updateMovingZones() -- will auto-repeat
|
|
|
|
trigger.action.outText("cf/x Zones v".. cfxZones.version .. ": loaded, zones:" .. dcsCommon.getSizeOfTable(cfxZones.zones), 30)
|
|
|
|
end
|
|
|
|
-- get everything rolling
|
|
cfxZones.init()
|