-- cf/x zone management module -- reads dcs zones and makes them accessible and mutable -- by scripting. -- -- Copyright (c) 2021, 2022 by Christian Franz and cf/x AG -- cfxZones = {} cfxZones.version = "2.7.0" --[[-- VERSION HISTORY - 2.2.4 - getCoalitionFromZoneProperty - getStringFromZoneProperty - 2.2.5 - createGroundUnitsInZoneForCoalition corrected coalition --> country - 2.2.6 - getVectorFromZoneProperty(theZone, theProperty, defaultVal) - 2.2.7 - allow 'yes' as 'true' for boolean attribute - 2.2.8 - getBoolFromZoneProperty supports default - cfxZones.hasProperty - 2.3.0 - property names are case insensitive - 2.3.1 - getCoalitionFromZoneProperty allows 0, 1, 2 also - 2.4.0 - all zones look for owner attribute, and set it to 0 (neutral) if not present - 2.4.1 - getBoolFromZoneProperty upgraded by expected bool - markZoneWithSmoke raised by 3 meters - 2.4.2 - getClosestZone also returns delta - 2.4.3 - getCoalitionFromZoneProperty() accepts 'all' as neutral createUniqueZoneName() getStringFromZoneProperty returns default if property value = "" corrected bug in addZoneToManagedZones - 2.4.4 - getPoint(aZone) returns uip-to-date pos for linked and normal zones - linkUnit can use "useOffset" property to keep relative position - 2.4.5 - updated various methods to support getPoint when referencing zone.point - 2.4.6 - corrected spelling in markZoneWithSmoke - 2.4.7 - copy reference to dcs zone into cfx zone - 2.4.8 - getAllZoneProperties - 2.4.9 - createSimpleZone no longer requires location - parse dcs adds empty .properties = {} if none tehre - createCircleZone adds empty properties - createPolyZone adds empty properties - 2.4.10 - pickRandomZoneFrom now defaults to all cfxZones.zones - getBoolFromZoneProperty also recognizes 0, 1 - removed autostart - 2.4.11 - removed typo in get closest zone - 2.4.12 - getStringFromZoneProperty - 2.5.0 - harden getZoneProperty and all getPropertyXXXX - 2.5.1 - markZoneWithSmoke supports alt attribute - 2.5.2 - getPoint also writes through to zone itself for optimization - new method getPositiveRangeFromZoneProperty(theZone, theProperty, default) - 2.5.3 - new getAllGroupsInZone() - 2.5.4 - cleaned up getZoneProperty break on no properties - extractPropertyFromDCS trims key and property - 2.5.5 - pollFlag() centralized for banging - allStaticsInZone - 2.5.6 - flag accessor setFlagValue(), getFlagValue() - pollFlag supports theZone as final parameter - randomDelayFromPositiveRange - isMEFlag - 2.5.7 - pollFlag supports dml flags - 2.5.8 - flagArrayFromString - getFlagNumber invokes tonumber() before returning result - 2.5.9 - removed pass-back flag in getPoint() - 2.6.0 - testZoneFlag() method based flag testing - 2.6.1 - Watchflag parsing of zone condition for number-named flags - case insensitive - verbose for zone-local accepted (but not acted upon) - hasProperty now offers active information when looking for '*?' and '*!' - 2.7.0 - doPollFlag - fully support multiple flags per bang! - 2.7.1 - setFlagValueMult() --]]-- 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!!!! -- a zone has the following attributes -- x, z -- coordinate of center. note they have correct x, 0, z coordinates so no y-->z mapping -- radius (zero if quad zone) -- isCircle (true if quad zone) -- poly the quad coords are in the poly attribute and are a -- 1..n, wound counter-clockwise as (currently) in DCS: -- lower left, lower right upper left, upper right, all coords are x, 0, z -- bounds - contain the AABB coords for the zone: ul (upper left), ur, ll (lower left), lr -- for both circle and poly, all (x, 0, z) -- zones can carry information in their names that can get processed into attributes -- use -- zones can also carry information in their 'properties' tag that ME allows to -- edit. cfxZones provides an easy method to access these properties -- - getZoneProperty (returns as string) -- - getMinMaxFromZoneProperty -- - getBoolFromZoneProperty -- - getNumberFromZoneProperty -- SUPPORTED PROPERTIES -- - "linkedUnit" - zone moves with unit of that name. must be exact match -- can be combined with other attributes that extend (e.g. scar manager and -- limited pilots/airframes -- -- -- readZonesFromDCS is executed exactly once at the beginning -- from then on, use only the cfxZones.zones table -- WARNING: cfxZones is NOT case-sensitive. All zone names are -- indexed by upper case. If you have two zones with same name but -- different case, one will be replaced -- 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 = {} -- 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 local upperName = newZone.name:upper() -- location as 'point' -- WARNING: zones locs are 2D (x,y) pairs, whily 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. newZone.point = cfxZones.createPoint(dcsZone.x, 0, dcsZone.y) -- 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 elseif zoneType == 2 then -- polyZone newZone.isPoly = true newZone.radius = dcsZone.radius -- radius is still written in DCS, may change later -- now transfer all point in the poly -- note: DCS in 2.7 misspells vertices as 'verticies' -- correct vor this 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 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!!! --trigger.action.outText("znd: procced " .. newZone.name .. " with radius " .. newZone.radius, 30) 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! 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 = cfxZones.createPoint(center.x - radius, 0, center.z - radius) bounds.ur = cfxZones.createPoint(center.x + radius, 0, center.z - radius) bounds.ll = cfxZones.createPoint(center.x - radius, 0, center.z + radius) bounds.lr = cfxZones.createPoint(center.x + radius, 0, center.z + radius) elseif theZone.isPoly then local poly = theZone.poly -- ref copy! -- create the four points local ll = cfxZones.createPointFromPoint(poly[1]) local lr = cfxZones.createPointFromPoint(poly[1]) local ul = cfxZones.createPointFromPoint(poly[1]) local ur = cfxZones.createPointFromPoint(poly[1]) -- now iterate through all points and adjust bounds accordingly for v=2, #poly do local vertex = poly[v] if (vertex.x < ll.x) then ll.x = vertex.x; ul.x = vertex.x end if (vertex.x > lr.x) then lr.x = vertex.x; ur.x = vertex.x end if (vertex.z < ul.z) then ul.z = vertex.z; ur.z = vertex.z end if (vertex.z > ll.z) then ll.z = vertex.z; lr.z = vertex.z end end -- now keep the new point references -- and store them in the zone's bounds bounds.ll = ll bounds.lr = lr bounds.ul = ul bounds.ur = ur 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 cfxZones.createPoint(x, y, z) local newPoint = {} newPoint.x = x newPoint.y = y newPoint.z = z return newPoint end function cfxZones.copyPoint(inPoint) local newPoint = {} newPoint.x = inPoint.x newPoint.y = inPoint.y newPoint.z = inPoint.z return newPoint end function cfxZones.createHeightCorrectedPoint(inPoint) -- this should be in dcsCommon local cP = cfxZones.createPoint(inPoint.x, land.getHeight({x=inPoint.x, y=inPoint.z}),inPoint.z) return cP end function cfxZones.getHeightCorrectedZonePoint(theZone) return cfxZones.createHeightCorrectedPoint(theZone.point) end function cfxZones.createPointFromPoint(inPoint) return cfxZones.copyPoint(inPoint) end function cfxZones.createPointFromDCSPoint(inPoint) return cfxZones.createPoint(inPoint.x, 0, inPoint.y) end function cfxZones.createRandomPointInsideBounds(bounds) local x = math.random(bounds.ll.x, ur.x) local z = math.random(bounds.ll.z, ur.z) return cfxZones.createPoint(x, 0, z) end function cfxZones.addZoneToManagedZones(theZone) local upperName = string.upper(theZone.name) -- newZone.name:upper() cfxZones.zones[upperName] = theZone 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 = {} newZone.isCircle = true newZone.isPoly = false newZone.poly = {} newZone.bounds = {} newZone.name = name newZone.radius = radius newZone.point = cfxZones.createPoint(x, 0, z) -- props newZone.properties = {} -- calculate my bounds cfxZones.calculateZoneBounds(newZone) return newZone end function cfxZones.createPolyZone(name, poly) -- poly must be array of point type local newZone = {} 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) 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. -- trigger.action.outText("Zones: creating rZiZ with tr = " .. targetRadius .. " for " .. inZone.name .. " that as r = " .. inZone.radius, 10) 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 -- we have a poly zone. the way we do this is simple: -- generate random x, z with ranges of the bounding box -- until the point falls within the polygon. local newPoint = {} local emergencyBrake = 0 repeat newPoint = cfxZones.createRandomPointInsideBounds(inZone.bounds) emergencyBrake = emergencyBrake + 1 if (emergencyBrake > 100) then newPoint = cfxZones.copyPoint(inZone.Point) trigger.action.outText("CreateZoneInZone: mergency brake for inZone" .. inZone.name, 10) break end until cfxZones.isPointInsidePoly(newPoint, inZone.poly) -- 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 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) 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 end if (theZone.isPoly) then --trigger.action.outText("zne: isPointInside: " .. theZone.name .. " is Polyzone!", 30) return (cfxZones.isPointInsidePoly(p, theZone.poly)) end trigger.action.outText("isPointInsideZone: Unknown zone type for " .. outerZone.name, 10) end -- isZoneInZone returns true if center of innerZone is inside outerZone function cfxZones.isZoneInsideZone(innerZone, outerZone) return cfxZones.isPointInsideZone(innerZone.point, 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 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 -- -- units / groups in zone -- function cfxZones.allGroupsInZone(theZone, categ) -- categ is optional, must be code -- warning: does not check for exiting! --trigger.action.outText("Zone " .. theZone.name .. " radius " .. theZone.radius, 30) 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 cfxZones.allStaticsInZone(theZone) -- categ is optional, must be code -- warning: does not check for exiting! 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 cfxZones.pointInZone(oP, theZone) then table.insert(inZones, statO) end end end return inZones 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 aUnit:isExist() and aUnit:getLife() > 1 then local p = aUnit:getPoint() local inzone, percent, dist = cfxZones.pointInZone(p, aZone) if inzone then -- cfxZones.isPointInsideZone(p, aZone) then --trigger.action.outText("zne: YAY <" .. aUnit:getName() .. "> IS IN " .. aZone.name, 30) return true end --trigger.action.outText("zne: <" .. aUnit:getName() .. "> not in " .. aZone.name .. ", dist = " .. dist .. ", rad = ", aZone.radius, 30) 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 -- -- 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 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 cfxZones.centerZoneOnUnit(theZone, theUnit) local thePoint = theUnit:getPoint() cfxZones.moveZoneTo(theZone, thePoint.x, thePoint.z) end --[[ -- no longer makes sense with poly zones function cfxZones.isZoneEntirelyInsideZone(innerZone, outerZone) if (innerZone.radius > outerZone.radius) then return false end -- cant fit inside local d = dcsCommon.dist(innerZone.point, outerZone.point) local reducedR = outerZone.radius - innerZone.radius return d < reducedR 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.stringStartsWith(theString, thePrefix) return theString:find(thePrefix) == 1 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 -- trigger.action.outText("Enter: zonesStartingWithName for " .. prefix , 30) local prefixZones = {} prefix = prefix:upper() -- all zones have UPPERCASE NAMES! THEY SCREAM AT YOU for name, zone in pairs(searchSet) do -- trigger.action.outText("testing " .. name:upper() .. " starts with " .. prefix , 30) if cfxZones.stringStartsWith(name:upper(), prefix) then prefixZones[name] = zone -- note: ref copy! --trigger.action.outText("zone with prefix <" .. prefix .. "> found: " .. name, 10) 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 --trigger.action.outText("#debugZones is <" .. #debugZones .. ">", 10) 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 currDelta = math.huge local closestZone = nil for zName, zData in pairs(theZones) do local zPoint = cfxZones.getPoint(zData) local delta = dcsCommon.dist(point, zPoint) if (delta < currDelta) then currDelta = delta closestZone = zData end end 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 -- 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 -- 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 -- 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) if not (theZone) then return false, 0, 0 end local pflat = {x = thePoint.x, y = 0, z = thePoint.z} local zpoint = cfxZones.getPoint(theZone) -- updates zone if linked 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 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 -- 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 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] for i=2, #theUnits do local aUnit = theUnits[i] if dcsCommon.dist(theZone.point, closestUnit:getPoint()) > dcsCommon.dist(theZone.point, aUnit:getPoint()) then closestUnit = aUnit end end return closestUnit end function cfxZones.anyPlayerInZone(theZone) -- returns first player it finds for pname, pinfo in pairs(cfxPlayer.playerDB) do local playerUnit = pinfo.unit if (cfxZones.unitInZone(playerUnit, theZone)) then return true, playerUnit end end -- for all players return false, nil 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) -- theUnits can be string or table of string if not groupName then groupName = "G_"..theZone.name end -- group name will be taken from zone name and prependend with "G_" local theGroup = dcsCommon.createGroundGroupWithUnits(groupName, theUnits, theZone.radius, nil, formation) -- 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) return coalition.addGroup(theSideCJTF, Group.Category.GROUND, theGroup) end -- parsing zone names. The first part of the name until the first blank " " -- is the prefix and is dropped unless keepPrefix is true. -- all others are regarded as key:value pairs and are then added -- to the zone -- separated by equal sign "=" AND MUST NOT CONTAIN BLANKS -- -- example usage "followZone unit=rotary-1 dx=30 dy=25 rotateWithHeading=true -- -- OLD DEPRECATED TECH -- TO BE DECOMMISSIONED SOON, DO NOT USE -- --[[-- function cfxZones.parseZoneNameIntoAttributes(theZone, keepPrefix) -- trigger.action.outText("Parsing zone: ".. theZone.name, 30) if not keepPrefix then keepPrefix = false end -- simply for clarity -- now split the name into space-separated strings local attributes = dcsCommon.splitString(theZone.name, " ") if not keepPrefix then table.remove(attributes, 1) end -- pop prefix -- now parse all substrings and add them as attributes to theZone for i=1, #attributes do local a = attributes[i] local kvp = dcsCommon.splitString(a, "=") if #kvp == 2 then -- we have key value pair local theKey = kvp[1] local theValue = kvp[2] theZone[theKey] = theValue -- trigger.action.outText("Zone ".. theZone.name .. " parsed: Key = " .. theKey .. ", Value = " .. theValue, 30) else -- trigger.action.outText("Zone ".. theZone.name .. ": dropped attribute " .. a, 30) end end end --]]-- -- OLD DEPRECATED TECH -- TO BE DECOMMISSIONED SOON, DO NOT USE --[[-- function cfxZones.processCraterZones () local craters = cfxZones.zonesStartingWith("crater") -- all these zones need to be processed and their name infor placed into attributes for cName, cZone in pairs(craters) do cfxZones.parseZoneNameIntoAttributes(cZone) -- blow stuff up at the location of the zone local cPoint = cZone.point cPoint.y = land.getHeight({x = cPoint.x, y = cPoint.z}) -- compensate for ground level trigger.action.explosion(cPoint, 900) -- now interpret and act on the crater info -- to destroy and place fire. -- fire has small, medium, large -- eg. fire=large end end --]]-- -- -- Flag Pulling -- function cfxZones.pollFlag(theFlag, method, theZone) if cfxZones.verbose then trigger.action.outText("+++zones: polling flag " .. theFlag .. " with " .. method, 30) end if not theZone then trigger.action.outText("+++zones: nil theZone on pollFlag", 30) end method = method:lower() --trigger.action.outText("+++zones: polling " .. theZone.name .. " method " .. method .. " flag " .. theFlag, 30) 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 else if method ~= "on" and method ~= "f=1" then trigger.action.outText("+++zones: unknown method <" .. method .. "> - using 'on'", 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.setFlagValue(theFlag, theValue, theZone) local zoneName = "" if not theZone then trigger.action.outText("+++Zne: no zone on setFlagValue") else zoneName = theZone.name -- for flag wildcards end if type(theFlag) == "number" then -- straight set, ME flag trigger.action.setUserFlag(theFlag, theValue) return end -- we assume it's a string now theFlag = dcsCommon.trim(theFlag) -- clear leading/trailing spaces local nFlag = tonumber(theFlag) if nFlag then trigger.action.setUserFlag(theFlag, theValue) return 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 = "" if not theZone then trigger.action.outText("+++Zne: no zone 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 -- now do wildcard processing. we have alphanumeric if dcsCommon.stringStartsWith(theFlag, "*") then theFlag = zoneName .. theFlag end return tonumber(trigger.misc.getUserFlag(theFlag)) end function cfxZones.isMEFlag(inFlag) -- do NOT use me trigger.action.outText("+++zne: warning: deprecated isMEFlag", 30) return true -- returns true if inFlag is a pure positive number -- inFlag = dcsCommon.trim(inFlag) -- return dcsCommon.stringIsPositiveNumber(inFlag) end -- method-based flag testing 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 end if lMethod == "dec" or lMethod == "-1" then return currVal == lastVal-1 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 = "" if theZone then zoneName = theZone.name end trigger.action.outText("+++Zne: illegal method constraints |" .. theMethod .. "| for zone " .. zoneName, 30 ) return false end function cfxZones.testZoneFlag(theZone, theFlagName, theMethod, latchName) -- returns true if method contraints 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 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 end if not latchName then trigger.action.outText("+++Zne: no latchName for zone " .. theZone.name .. " for testZoneFlag", 30) return 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) return end -- now, test by method -- we should only test if currVal <> lastVal if currVal == lastVal then return false end --trigger.action.outText("+++Zne: about to test: c = " .. currVal .. ", l = " .. lastVal, 30) local testResult = cfxZones.testFlagByMethodForZone(currVal, lastVal, theMethod, theZone) -- update latch by method theZone[latchName] = currVal -- return result return testResult end function cfxZones.flagArrayFromString(inString) -- original code from RND flag if string.len(inString) < 1 then trigger.action.outText("+++zne: empty flags", 30) return {} end if cfxZones.verbose then trigger.action.outText("+++zne: processing <" .. inString .. ">", 30) end local flags = {} local rawElements = dcsCommon.splitString(inString, ",") -- go over all elements for idx, anElement in pairs(rawElements) do if dcsCommon.stringStartsWithDigit(anElement) and dcsCommon.containsString(anElement, "-") then -- interpret this as a range local theRange = dcsCommon.splitString(anElement, "-") local lowerBound = theRange[1] lowerBound = tonumber(lowerBound) local upperBound = theRange[2] upperBound = tonumber(upperBound) if lowerBound and upperBound then -- swap if wrong order if lowerBound > upperBound then local temp = upperBound upperBound = lowerBound lowerBound = temp end -- now add add numbers to flags for f=lowerBound, upperBound do table.insert(flags, tostring(f)) end else -- bounds illegal trigger.action.outText("+++zne: ignored range <" .. anElement .. "> (range)", 30) end else -- single number f = dcsCommon.trim(anElement) -- DML flag upgrade: accept strings tonumber(anElement) if f then table.insert(flags, f) else trigger.action.outText("+++zne: ignored element <" .. anElement .. "> (single)", 30) end end end if cfxZones.verbose then trigger.action.outText("+++zne: <" .. #flags .. "> flags total", 30) end return flags end -- -- PROPERTY PROCESSING -- function cfxZones.getAllZoneProperties(theZone, caseInsensitive) -- return as dict if not caseInsensitive then caseInsensitive = 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 props[theKey] = theProp.value end return props 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) if not cfxZones.caseSensitiveProperties then existingKey = string.lower(existingKey) end 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) -- breakme.here = 1 return end local props = cZone.properties local theVal = cfxZones.extractPropertyFromDCS(theKey, props) return theVal end function cfxZones.getStringFromZoneProperty(theZone, theProperty, default) if not default then default = "" end local p = cfxZones.getZoneProperty(theZone, theProperty) if not p then return default end if type(p) == "string" then 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 cfxZones.randomDelayFromPositiveRange(minVal, maxVal) if not maxVal then return minVal end if not minVal then return maxVal 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) -- 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 local lowerBound = default local upperBound = default 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 -- if rndFlags.verbose then -- trigger.action.outText("+++Zne: detected range <" .. lowerBound .. ", " .. upperBound .. ">", 30) -- end else -- bounds illegal trigger.action.outText("+++Zne: illegal range <" .. rangeString .. ">, using " .. default .. "-" .. default, 30) lowerBound = default upperBound = default end else upperBound = cfxZones.getNumberFromZoneProperty(theZone, theProperty, default) -- between pulses lowerBound = upperBound end -- trigger.action.outText("+++Zne: returning <" .. lowerBound .. ", " .. upperBound .. ">", 30) return lowerBound, upperBound end function cfxZones.hasProperty(theZone, theProperty) return cfxZones.getZoneProperty(theZone, theProperty) ~= nil 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() 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" return theBool end local theBool = true -- only go false if exactly no or false or "0" theBool = (p ~= 'false') and (p ~= 'no') and (p ~= "0") 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 cfxZones.getNumberFromZoneProperty(theZone, theProperty, default) --TODO: trim string if not default then default = 0 end local p = cfxZones.getZoneProperty(theZone, theProperty) p = tonumber(p) if not p then return default else return p end 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 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 -- -- Moving Zones. They 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.getPoint(aZone) -- always works, even linked, point can be reused if aZone.linkedUnit then local theUnit = aZone.linkedUnit -- has a link. is link existing? if theUnit:isExist() then -- updates zone position cfxZones.centerZoneOnUnit(aZone, theUnit) cfxZones.offsetZone(aZone, aZone.dx, aZone.dy) end end local thePos = {} thePos.x = aZone.point.x thePos.y = 0 -- aZone.y thePos.z = aZone.point.z -- update the zone as well -- that's stupid! --[[-- aZone.point = thePos local retPoint = {} -- create new copy to pass back retPoint.x = thePos.x retPoint.y = 0 retPoint.z = thePos.z --]]-- return thePos 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 end function cfxZones.updateMovingZones() cfxZones.updateSchedule = timer.scheduleFunction(cfxZones.updateMovingZones, {}, timer.getTime() + 1/cfxZones.ups) -- simply scan all cfx zones for the linkedUnit property and if there -- update the zone's points for aName,aZone in pairs(cfxZones.zones) do if aZone.linkedUnit then local theUnit = aZone.linkedUnit -- has a link. is link existing? if theUnit:isExist() then cfxZones.centerZoneOnUnit(aZone, theUnit) cfxZones.offsetZone(aZone, aZone.dx, aZone.dy) --trigger.action.outText("cf/x zones update " .. aZone.name, 30) end end end end function cfxZones.startMovingZones() -- read all zoness, and look for a property called 'linkedUnit' -- which will make them a linked zone if there is a unit that exists for aName,aZone in pairs(cfxZones.zones) do local lU = cfxZones.getZoneProperty(aZone, "linkedUnit") if lU then -- this zone is linked to a unit theUnit = Unit.getByName(lU) local useOffset = cfxZones.getBoolFromZoneProperty(aZone, "useOffset", false) if useOffset then aZone.useOffset = true end if theUnit then local dx = 0 local dz = 0 if useOffset then local delta = dcsCommon.vSub(aZone.point,theUnit:getPoint()) -- delta = B - A dx = delta.x dz = delta.z end cfxZones.linkUnitToZone(theUnit, aZone, dx, dz) --trigger.action.outText("cf/x zones: linked " .. aZone.name .. " to " .. theUnit:getName(), 30) if useOffset then --trigger.action.outText("and dx = " .. dx .. " dz = " .. dz, 30) end end end -- support for local verbose flag aZone.verbose = cfxZones.getBoolFromZoneProperty(aZone, "verbose", false) end end -- -- init -- function cfxZones.init() -- read all zones into my own db cfxZones.readFromDCS(true) -- true: erase old -- now, pre-read zone owner for all zones -- note, all zones with this property are by definition owned zones. -- and hence will be read anyway. this will merely ensure that the -- ownership is established right away -- unless owned zones module is missing, in which case -- ownership is still established local pZones = cfxZones.zonesWithProperty("owner") for n, aZone in pairs(pZones) do aZone.owner = cfxZones.getCoalitionFromZoneProperty(aZone, "owner", 0) end -- now initialize moving zones cfxZones.startMovingZones() cfxZones.updateMovingZones() -- will auto-repeat trigger.action.outText("cf/x Zones v".. cfxZones.version .. ": loaded", 10) end -- get everything rolling cfxZones.init() -- cf/x zone management module -- reads dcs zones and makes them accessible and mutable -- by scripting. -- -- Copyright (c) 2021, 2022 by Christian Franz and cf/x AG -- cfxZones = {} cfxZones.version = "2.6.1" --[[-- VERSION HISTORY - 2.2.4 - getCoalitionFromZoneProperty - getStringFromZoneProperty - 2.2.5 - createGroundUnitsInZoneForCoalition corrected coalition --> country - 2.2.6 - getVectorFromZoneProperty(theZone, theProperty, defaultVal) - 2.2.7 - allow 'yes' as 'true' for boolean attribute - 2.2.8 - getBoolFromZoneProperty supports default - cfxZones.hasProperty - 2.3.0 - property names are case insensitive - 2.3.1 - getCoalitionFromZoneProperty allows 0, 1, 2 also - 2.4.0 - all zones look for owner attribute, and set it to 0 (neutral) if not present - 2.4.1 - getBoolFromZoneProperty upgraded by expected bool - markZoneWithSmoke raised by 3 meters - 2.4.2 - getClosestZone also returns delta - 2.4.3 - getCoalitionFromZoneProperty() accepts 'all' as neutral createUniqueZoneName() getStringFromZoneProperty returns default if property value = "" corrected bug in addZoneToManagedZones - 2.4.4 - getPoint(aZone) returns uip-to-date pos for linked and normal zones - linkUnit can use "useOffset" property to keep relative position - 2.4.5 - updated various methods to support getPoint when referencing zone.point - 2.4.6 - corrected spelling in markZoneWithSmoke - 2.4.7 - copy reference to dcs zone into cfx zone - 2.4.8 - getAllZoneProperties - 2.4.9 - createSimpleZone no longer requires location - parse dcs adds empty .properties = {} if none tehre - createCircleZone adds empty properties - createPolyZone adds empty properties - 2.4.10 - pickRandomZoneFrom now defaults to all cfxZones.zones - getBoolFromZoneProperty also recognizes 0, 1 - removed autostart - 2.4.11 - removed typo in get closest zone - 2.4.12 - getStringFromZoneProperty - 2.5.0 - harden getZoneProperty and all getPropertyXXXX - 2.5.1 - markZoneWithSmoke supports alt attribute - 2.5.2 - getPoint also writes through to zone itself for optimization - new method getPositiveRangeFromZoneProperty(theZone, theProperty, default) - 2.5.3 - new getAllGroupsInZone() - 2.5.4 - cleaned up getZoneProperty break on no properties - extractPropertyFromDCS trims key and property - 2.5.5 - pollFlag() centralized for banging - allStaticsInZone - 2.5.6 - flag accessor setFlagValue(), getFlagValue() - pollFlag supports theZone as final parameter - randomDelayFromPositiveRange - isMEFlag - 2.5.7 - pollFlag supports dml flags - 2.5.8 - flagArrayFromString - getFlagNumber invokes tonumber() before returning result - 2.5.9 - removed pass-back flag in getPoint() - 2.6.0 - testZoneFlag() method based flag testing - 2.6.1 - Watchflag parsing of zone condition for number-named flags - case insensitive --]]-- 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!!!! -- a zone has the following attributes -- x, z -- coordinate of center. note they have correct x, 0, z coordinates so no y-->z mapping -- radius (zero if quad zone) -- isCircle (true if quad zone) -- poly the quad coords are in the poly attribute and are a -- 1..n, wound counter-clockwise as (currently) in DCS: -- lower left, lower right upper left, upper right, all coords are x, 0, z -- bounds - contain the AABB coords for the zone: ul (upper left), ur, ll (lower left), lr -- for both circle and poly, all (x, 0, z) -- zones can carry information in their names that can get processed into attributes -- use -- zones can also carry information in their 'properties' tag that ME allows to -- edit. cfxZones provides an easy method to access these properties -- - getZoneProperty (returns as string) -- - getMinMaxFromZoneProperty -- - getBoolFromZoneProperty -- - getNumberFromZoneProperty -- SUPPORTED PROPERTIES -- - "linkedUnit" - zone moves with unit of that name. must be exact match -- can be combined with other attributes that extend (e.g. scar manager and -- limited pilots/airframes -- -- -- readZonesFromDCS is executed exactly once at the beginning -- from then on, use only the cfxZones.zones table -- WARNING: cfxZones is NOT case-sensitive. All zone names are -- indexed by upper case. If you have two zones with same name but -- different case, one will be replaced -- 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 = {} -- 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 local upperName = newZone.name:upper() -- location as 'point' -- WARNING: zones locs are 2D (x,y) pairs, whily 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. newZone.point = cfxZones.createPoint(dcsZone.x, 0, dcsZone.y) -- 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 elseif zoneType == 2 then -- polyZone newZone.isPoly = true newZone.radius = dcsZone.radius -- radius is still written in DCS, may change later -- now transfer all point in the poly -- note: DCS in 2.7 misspells vertices as 'verticies' -- correct vor this 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 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!!! --trigger.action.outText("znd: procced " .. newZone.name .. " with radius " .. newZone.radius, 30) 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! 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 = cfxZones.createPoint(center.x - radius, 0, center.z - radius) bounds.ur = cfxZones.createPoint(center.x + radius, 0, center.z - radius) bounds.ll = cfxZones.createPoint(center.x - radius, 0, center.z + radius) bounds.lr = cfxZones.createPoint(center.x + radius, 0, center.z + radius) elseif theZone.isPoly then local poly = theZone.poly -- ref copy! -- create the four points local ll = cfxZones.createPointFromPoint(poly[1]) local lr = cfxZones.createPointFromPoint(poly[1]) local ul = cfxZones.createPointFromPoint(poly[1]) local ur = cfxZones.createPointFromPoint(poly[1]) -- now iterate through all points and adjust bounds accordingly for v=2, #poly do local vertex = poly[v] if (vertex.x < ll.x) then ll.x = vertex.x; ul.x = vertex.x end if (vertex.x > lr.x) then lr.x = vertex.x; ur.x = vertex.x end if (vertex.z < ul.z) then ul.z = vertex.z; ur.z = vertex.z end if (vertex.z > ll.z) then ll.z = vertex.z; lr.z = vertex.z end end -- now keep the new point references -- and store them in the zone's bounds bounds.ll = ll bounds.lr = lr bounds.ul = ul bounds.ur = ur 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 cfxZones.createPoint(x, y, z) local newPoint = {} newPoint.x = x newPoint.y = y newPoint.z = z return newPoint end function cfxZones.copyPoint(inPoint) local newPoint = {} newPoint.x = inPoint.x newPoint.y = inPoint.y newPoint.z = inPoint.z return newPoint end function cfxZones.createHeightCorrectedPoint(inPoint) -- this should be in dcsCommon local cP = cfxZones.createPoint(inPoint.x, land.getHeight({x=inPoint.x, y=inPoint.z}),inPoint.z) return cP end function cfxZones.getHeightCorrectedZonePoint(theZone) return cfxZones.createHeightCorrectedPoint(theZone.point) end function cfxZones.createPointFromPoint(inPoint) return cfxZones.copyPoint(inPoint) end function cfxZones.createPointFromDCSPoint(inPoint) return cfxZones.createPoint(inPoint.x, 0, inPoint.y) end function cfxZones.createRandomPointInsideBounds(bounds) local x = math.random(bounds.ll.x, ur.x) local z = math.random(bounds.ll.z, ur.z) return cfxZones.createPoint(x, 0, z) end function cfxZones.addZoneToManagedZones(theZone) local upperName = string.upper(theZone.name) -- newZone.name:upper() cfxZones.zones[upperName] = theZone 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 = {} newZone.isCircle = true newZone.isPoly = false newZone.poly = {} newZone.bounds = {} newZone.name = name newZone.radius = radius newZone.point = cfxZones.createPoint(x, 0, z) -- props newZone.properties = {} -- calculate my bounds cfxZones.calculateZoneBounds(newZone) return newZone end function cfxZones.createPolyZone(name, poly) -- poly must be array of point type local newZone = {} 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) 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. -- trigger.action.outText("Zones: creating rZiZ with tr = " .. targetRadius .. " for " .. inZone.name .. " that as r = " .. inZone.radius, 10) 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 -- we have a poly zone. the way we do this is simple: -- generate random x, z with ranges of the bounding box -- until the point falls within the polygon. local newPoint = {} local emergencyBrake = 0 repeat newPoint = cfxZones.createRandomPointInsideBounds(inZone.bounds) emergencyBrake = emergencyBrake + 1 if (emergencyBrake > 100) then newPoint = cfxZones.copyPoint(inZone.Point) trigger.action.outText("CreateZoneInZone: mergency brake for inZone" .. inZone.name, 10) break end until cfxZones.isPointInsidePoly(newPoint, inZone.poly) -- 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 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) 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 end if (theZone.isPoly) then --trigger.action.outText("zne: isPointInside: " .. theZone.name .. " is Polyzone!", 30) return (cfxZones.isPointInsidePoly(p, theZone.poly)) end trigger.action.outText("isPointInsideZone: Unknown zone type for " .. outerZone.name, 10) end -- isZoneInZone returns true if center of innerZone is inside outerZone function cfxZones.isZoneInsideZone(innerZone, outerZone) return cfxZones.isPointInsideZone(innerZone.point, 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 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 -- -- units / groups in zone -- function cfxZones.allGroupsInZone(theZone, categ) -- categ is optional, must be code -- warning: does not check for exiting! --trigger.action.outText("Zone " .. theZone.name .. " radius " .. theZone.radius, 30) 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 cfxZones.allStaticsInZone(theZone) -- categ is optional, must be code -- warning: does not check for exiting! 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 cfxZones.pointInZone(oP, theZone) then table.insert(inZones, statO) end end end return inZones 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 aUnit:isExist() and aUnit:getLife() > 1 then local p = aUnit:getPoint() local inzone, percent, dist = cfxZones.pointInZone(p, aZone) if inzone then -- cfxZones.isPointInsideZone(p, aZone) then --trigger.action.outText("zne: YAY <" .. aUnit:getName() .. "> IS IN " .. aZone.name, 30) return true end --trigger.action.outText("zne: <" .. aUnit:getName() .. "> not in " .. aZone.name .. ", dist = " .. dist .. ", rad = ", aZone.radius, 30) 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 -- -- 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 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 cfxZones.centerZoneOnUnit(theZone, theUnit) local thePoint = theUnit:getPoint() cfxZones.moveZoneTo(theZone, thePoint.x, thePoint.z) end --[[ -- no longer makes sense with poly zones function cfxZones.isZoneEntirelyInsideZone(innerZone, outerZone) if (innerZone.radius > outerZone.radius) then return false end -- cant fit inside local d = dcsCommon.dist(innerZone.point, outerZone.point) local reducedR = outerZone.radius - innerZone.radius return d < reducedR 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.stringStartsWith(theString, thePrefix) return theString:find(thePrefix) == 1 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 -- trigger.action.outText("Enter: zonesStartingWithName for " .. prefix , 30) local prefixZones = {} prefix = prefix:upper() -- all zones have UPPERCASE NAMES! THEY SCREAM AT YOU for name, zone in pairs(searchSet) do -- trigger.action.outText("testing " .. name:upper() .. " starts with " .. prefix , 30) if cfxZones.stringStartsWith(name:upper(), prefix) then prefixZones[name] = zone -- note: ref copy! --trigger.action.outText("zone with prefix <" .. prefix .. "> found: " .. name, 10) 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 --trigger.action.outText("#debugZones is <" .. #debugZones .. ">", 10) 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 currDelta = math.huge local closestZone = nil for zName, zData in pairs(theZones) do local zPoint = cfxZones.getPoint(zData) local delta = dcsCommon.dist(point, zPoint) if (delta < currDelta) then currDelta = delta closestZone = zData end end 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 -- 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 -- 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 -- 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) if not (theZone) then return false, 0, 0 end local pflat = {x = thePoint.x, y = 0, z = thePoint.z} local zpoint = cfxZones.getPoint(theZone) -- updates zone if linked 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 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 -- 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 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] for i=2, #theUnits do local aUnit = theUnits[i] if dcsCommon.dist(theZone.point, closestUnit:getPoint()) > dcsCommon.dist(theZone.point, aUnit:getPoint()) then closestUnit = aUnit end end return closestUnit end function cfxZones.anyPlayerInZone(theZone) -- returns first player it finds for pname, pinfo in pairs(cfxPlayer.playerDB) do local playerUnit = pinfo.unit if (cfxZones.unitInZone(playerUnit, theZone)) then return true, playerUnit end end -- for all players return false, nil 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) -- theUnits can be string or table of string if not groupName then groupName = "G_"..theZone.name end -- group name will be taken from zone name and prependend with "G_" local theGroup = dcsCommon.createGroundGroupWithUnits(groupName, theUnits, theZone.radius, nil, formation) -- 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) return coalition.addGroup(theSideCJTF, Group.Category.GROUND, theGroup) end -- parsing zone names. The first part of the name until the first blank " " -- is the prefix and is dropped unless keepPrefix is true. -- all others are regarded as key:value pairs and are then added -- to the zone -- separated by equal sign "=" AND MUST NOT CONTAIN BLANKS -- -- example usage "followZone unit=rotary-1 dx=30 dy=25 rotateWithHeading=true -- -- OLD DEPRECATED TECH -- TO BE DECOMMISSIONED SOON, DO NOT USE -- --[[-- function cfxZones.parseZoneNameIntoAttributes(theZone, keepPrefix) -- trigger.action.outText("Parsing zone: ".. theZone.name, 30) if not keepPrefix then keepPrefix = false end -- simply for clarity -- now split the name into space-separated strings local attributes = dcsCommon.splitString(theZone.name, " ") if not keepPrefix then table.remove(attributes, 1) end -- pop prefix -- now parse all substrings and add them as attributes to theZone for i=1, #attributes do local a = attributes[i] local kvp = dcsCommon.splitString(a, "=") if #kvp == 2 then -- we have key value pair local theKey = kvp[1] local theValue = kvp[2] theZone[theKey] = theValue -- trigger.action.outText("Zone ".. theZone.name .. " parsed: Key = " .. theKey .. ", Value = " .. theValue, 30) else -- trigger.action.outText("Zone ".. theZone.name .. ": dropped attribute " .. a, 30) end end end --]]-- -- OLD DEPRECATED TECH -- TO BE DECOMMISSIONED SOON, DO NOT USE --[[-- function cfxZones.processCraterZones () local craters = cfxZones.zonesStartingWith("crater") -- all these zones need to be processed and their name infor placed into attributes for cName, cZone in pairs(craters) do cfxZones.parseZoneNameIntoAttributes(cZone) -- blow stuff up at the location of the zone local cPoint = cZone.point cPoint.y = land.getHeight({x = cPoint.x, y = cPoint.z}) -- compensate for ground level trigger.action.explosion(cPoint, 900) -- now interpret and act on the crater info -- to destroy and place fire. -- fire has small, medium, large -- eg. fire=large end end --]]-- -- -- Flag Pulling -- function cfxZones.doPollFlag(theFlag, method, theZone) if cfxZones.verbose then trigger.action.outText("+++zones: polling flag " .. theFlag .. " with " .. method, 30) end if not theZone then trigger.action.outText("+++zones: nil theZone on pollFlag", 30) end method = method:lower() --trigger.action.outText("+++zones: polling " .. theZone.name .. " method " .. method .. " flag " .. theFlag, 30) 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 else if method ~= "on" and method ~= "f=1" then trigger.action.outText("+++zones: unknown method <" .. method .. "> - using 'on'", 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) 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 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.setFlagValue(aFlag, theValue, theZone) end end function cfxZones.setFlagValue(theFlag, theValue, theZone) local zoneName = "" if not theZone then trigger.action.outText("+++Zne: no zone on setFlagValue") else zoneName = theZone.name -- for flag wildcards end if type(theFlag) == "number" then -- straight set, ME flag trigger.action.setUserFlag(theFlag, theValue) return end -- we assume it's a string now theFlag = dcsCommon.trim(theFlag) -- clear leading/trailing spaces local nFlag = tonumber(theFlag) if nFlag then trigger.action.setUserFlag(theFlag, theValue) return 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 = "" if not theZone then trigger.action.outText("+++Zne: no zone 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 -- now do wildcard processing. we have alphanumeric if dcsCommon.stringStartsWith(theFlag, "*") then theFlag = zoneName .. theFlag end return tonumber(trigger.misc.getUserFlag(theFlag)) end function cfxZones.isMEFlag(inFlag) -- do NOT use me trigger.action.outText("+++zne: warning: deprecated isMEFlag", 30) return true -- returns true if inFlag is a pure positive number -- inFlag = dcsCommon.trim(inFlag) -- return dcsCommon.stringIsPositiveNumber(inFlag) end -- method-based flag testing 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 end if lMethod == "dec" or lMethod == "-1" then return currVal == lastVal-1 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 = "" if theZone then zoneName = theZone.name end trigger.action.outText("+++Zne: illegal method constraints |" .. theMethod .. "| for zone " .. zoneName, 30 ) return false end function cfxZones.testZoneFlag(theZone, theFlagName, theMethod, latchName) -- 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 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 end if not latchName then trigger.action.outText("+++Zne: no latchName for zone " .. theZone.name .. " for testZoneFlag", 30) return 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) return end -- now, test by method -- we should only test if currVal <> lastVal if currVal == lastVal then return false end --trigger.action.outText("+++Zne: about to test: c = " .. currVal .. ", l = " .. lastVal, 30) local testResult = cfxZones.testFlagByMethodForZone(currVal, lastVal, theMethod, theZone) -- update latch by method theZone[latchName] = currVal -- return result return testResult end function cfxZones.flagArrayFromString(inString) -- original code from RND flag if string.len(inString) < 1 then trigger.action.outText("+++zne: empty flags", 30) return {} end if cfxZones.verbose then trigger.action.outText("+++zne: processing <" .. inString .. ">", 30) end local flags = {} local rawElements = dcsCommon.splitString(inString, ",") -- go over all elements for idx, anElement in pairs(rawElements) do if dcsCommon.stringStartsWithDigit(anElement) and dcsCommon.containsString(anElement, "-") then -- interpret this as a range local theRange = dcsCommon.splitString(anElement, "-") local lowerBound = theRange[1] lowerBound = tonumber(lowerBound) local upperBound = theRange[2] upperBound = tonumber(upperBound) if lowerBound and upperBound then -- swap if wrong order if lowerBound > upperBound then local temp = upperBound upperBound = lowerBound lowerBound = temp end -- now add add numbers to flags for f=lowerBound, upperBound do table.insert(flags, tostring(f)) end else -- bounds illegal trigger.action.outText("+++zne: ignored range <" .. anElement .. "> (range)", 30) end else -- single number f = dcsCommon.trim(anElement) -- DML flag upgrade: accept strings tonumber(anElement) if f then table.insert(flags, f) else trigger.action.outText("+++zne: ignored element <" .. anElement .. "> (single)", 30) end end end if cfxZones.verbose then trigger.action.outText("+++zne: <" .. #flags .. "> flags total", 30) end return flags end -- -- PROPERTY PROCESSING -- function cfxZones.getAllZoneProperties(theZone, caseInsensitive) -- return as dict if not caseInsensitive then caseInsensitive = 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 props[theKey] = theProp.value end return props 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) if not cfxZones.caseSensitiveProperties then existingKey = string.lower(existingKey) end 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) -- breakme.here = 1 return end local props = cZone.properties local theVal = cfxZones.extractPropertyFromDCS(theKey, props) return theVal end function cfxZones.getStringFromZoneProperty(theZone, theProperty, default) if not default then default = "" end local p = cfxZones.getZoneProperty(theZone, theProperty) if not p then return default end if type(p) == "string" then 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 cfxZones.randomDelayFromPositiveRange(minVal, maxVal) if not maxVal then return minVal end if not minVal then return maxVal 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) -- 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 local lowerBound = default local upperBound = default 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 -- if rndFlags.verbose then -- trigger.action.outText("+++Zne: detected range <" .. lowerBound .. ", " .. upperBound .. ">", 30) -- end else -- bounds illegal trigger.action.outText("+++Zne: illegal range <" .. rangeString .. ">, using " .. default .. "-" .. default, 30) lowerBound = default upperBound = default end else upperBound = cfxZones.getNumberFromZoneProperty(theZone, theProperty, default) -- between pulses lowerBound = upperBound end -- trigger.action.outText("+++Zne: returning <" .. lowerBound .. ", " .. upperBound .. ">", 30) return lowerBound, upperBound end function cfxZones.hasProperty(theZone, theProperty) local foundIt = cfxZones.getZoneProperty(theZone, theProperty) if not foundIt then 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 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 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 return false end return true -- return foundIt ~= nil 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) -- intentional bug 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() 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" return theBool end local theBool = true -- only go false if exactly no or false or "0" theBool = (p ~= 'false') and (p ~= 'no') and (p ~= "0") 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 cfxZones.getNumberFromZoneProperty(theZone, theProperty, default) --TODO: trim string if not default then default = 0 end local p = cfxZones.getZoneProperty(theZone, theProperty) p = tonumber(p) if not p then return default else return p end 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 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 -- -- Moving Zones. They 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.getPoint(aZone) -- always works, even linked, point can be reused if aZone.linkedUnit then local theUnit = aZone.linkedUnit -- has a link. is link existing? if theUnit:isExist() then -- updates zone position cfxZones.centerZoneOnUnit(aZone, theUnit) cfxZones.offsetZone(aZone, aZone.dx, aZone.dy) end end local thePos = {} thePos.x = aZone.point.x thePos.y = 0 -- aZone.y thePos.z = aZone.point.z -- update the zone as well -- that's stupid! --[[-- aZone.point = thePos local retPoint = {} -- create new copy to pass back retPoint.x = thePos.x retPoint.y = 0 retPoint.z = thePos.z --]]-- return thePos 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 end function cfxZones.updateMovingZones() cfxZones.updateSchedule = timer.scheduleFunction(cfxZones.updateMovingZones, {}, timer.getTime() + 1/cfxZones.ups) -- simply scan all cfx zones for the linkedUnit property and if there -- update the zone's points for aName,aZone in pairs(cfxZones.zones) do if aZone.linkedUnit then local theUnit = aZone.linkedUnit -- has a link. is link existing? if theUnit:isExist() then cfxZones.centerZoneOnUnit(aZone, theUnit) cfxZones.offsetZone(aZone, aZone.dx, aZone.dy) --trigger.action.outText("cf/x zones update " .. aZone.name, 30) end end end end function cfxZones.startMovingZones() -- read all zoness, and look for a property called 'linkedUnit' -- which will make them a linked zone if there is a unit that exists -- also scans for 'verbose' flag. for aName,aZone in pairs(cfxZones.zones) do local lU = cfxZones.getZoneProperty(aZone, "linkedUnit") if lU then -- this zone is linked to a unit theUnit = Unit.getByName(lU) local useOffset = cfxZones.getBoolFromZoneProperty(aZone, "useOffset", false) if useOffset then aZone.useOffset = true end if theUnit then local dx = 0 local dz = 0 if useOffset then local delta = dcsCommon.vSub(aZone.point,theUnit:getPoint()) -- delta = B - A dx = delta.x dz = delta.z end cfxZones.linkUnitToZone(theUnit, aZone, dx, dz) --trigger.action.outText("cf/x zones: linked " .. aZone.name .. " to " .. theUnit:getName(), 30) if useOffset then --trigger.action.outText("and dx = " .. dx .. " dz = " .. dz, 30) end end end -- support for local verbose flag aZone.verbose = cfxZones.getBoolFromZoneProperty(aZone, "verbose", false) end end -- -- init -- function cfxZones.init() -- read all zones into my own db cfxZones.readFromDCS(true) -- true: erase old -- now, pre-read zone owner for all zones -- note, all zones with this property are by definition owned zones. -- and hence will be read anyway. this will merely ensure that the -- ownership is established right away -- unless owned zones module is missing, in which case -- ownership is still established local pZones = cfxZones.zonesWithProperty("owner") for n, aZone in pairs(pZones) do aZone.owner = cfxZones.getCoalitionFromZoneProperty(aZone, "owner", 0) end -- now initialize moving zones cfxZones.startMovingZones() cfxZones.updateMovingZones() -- will auto-repeat trigger.action.outText("cf/x Zones v".. cfxZones.version .. ": loaded", 10) end -- get everything rolling cfxZones.init()