diff --git a/Doc/DML Documentation.pdf b/Doc/DML Documentation.pdf index 319d29b..607b5d2 100644 Binary files a/Doc/DML Documentation.pdf and b/Doc/DML Documentation.pdf differ diff --git a/Doc/DML Quick Reference.pdf b/Doc/DML Quick Reference.pdf index c714e5f..ed8695f 100644 Binary files a/Doc/DML Quick Reference.pdf and b/Doc/DML Quick Reference.pdf differ diff --git a/modules/cfxNDB.lua b/modules/cfxNDB.lua index d5eb0d2..62c585b 100644 --- a/modules/cfxNDB.lua +++ b/modules/cfxNDB.lua @@ -1,9 +1,9 @@ cfxNDB = {} -cfxNDB.version = "1.1.0" +cfxNDB.version = "1.2.0" --[[-- cfxNDB: - Copyright (c) 2021 by Christian Franz and cf/x AG + Copyright (c) 2021, 2022 by Christian Franz and cf/x AG Zone enhancement that simulates an NDB for a zone. If zone is linked, the NDB's location is updated @@ -21,6 +21,7 @@ cfxNDB.version = "1.1.0" - paused flag, paused handling - startNDB() can accept string - stopNDB() can accept string + 1.2.0 - DML full integration --]]-- @@ -112,13 +113,19 @@ function cfxNDB.createNDBWithZone(theZone) -- paused theZone.paused = cfxZones.getBoolFromZoneProperty(theZone, "paused", false) + -- watchflags + theZone.ndbTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "triggerMethod", "change") + if cfxZones.hasProperty(theZone, "ndbTriggerMethod") then + theZone.ndbTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "ndbTriggerMethod", "change") + end + -- on/offf query flags if cfxZones.hasProperty(theZone, "on?") then theZone.onFlag = cfxZones.getStringFromZoneProperty(theZone, "on?", "none") end if theZone.onFlag then - theZone.onFlagVal = trigger.misc.getUserFlag(theZone.onFlag) -- save last value + theZone.onFlagVal = cfxZones.getFlagValue(theZone.onFlag, theZone) -- trigger.misc.getUserFlag(theZone.onFlag) -- save last value end if cfxZones.hasProperty(theZone, "off?") then @@ -126,7 +133,7 @@ function cfxNDB.createNDBWithZone(theZone) end if theZone.offFlag then - theZone.offFlagVal = trigger.misc.getUserFlag(theZone.offFlag) -- save last value + theZone.offFlagVal = cfxZones.getFlagValue(theZone.offFlag, theZone) --trigger.misc.getUserFlag(theZone.offFlag) -- save last value end -- start it @@ -158,24 +165,17 @@ function cfxNDB.update() end -- now check triggers to start/stop - if theNDB.onFlagVal then - -- see if this changed - local currTriggerVal = trigger.misc.getUserFlag(theNDB.onFlag) - if currTriggerVal ~= theNDB.onFlagVal then - -- yupp, trigger start - cfxNDB.startNDB(theNDB) - theNDB.onFlagVal = currTriggerVal - end + if cfxZones.testZoneFlag(theNDB, theNDB.onFlag, theNDB.ndbTriggerMethod, "onFlagVal") then + -- yupp, trigger start + cfxNDB.startNDB(theNDB) end - if theNDB.offFlagVal then - local currTriggerVal = trigger.misc.getUserFlag(theNDB.offFlag) - if currTriggerVal ~= theNDB.offFlagVal then - -- yupp, trigger start - cfxNDB.stopNDB(theNDB) - theNDB.offFlagVal = currTriggerVal - end - end + + if cfxZones.testZoneFlag(theNDB, theNDB.offFlag, theNDB.ndbTriggerMethod, "offFlagVal") then + -- yupp, trigger start + cfxNDB.stopNDB(theNDB) + end + end end diff --git a/modules/cfxObjectDestructDetector.lua b/modules/cfxObjectDestructDetector.lua index 7c0ab7e..98f9ed9 100644 --- a/modules/cfxObjectDestructDetector.lua +++ b/modules/cfxObjectDestructDetector.lua @@ -1,5 +1,5 @@ cfxObjectDestructDetector = {} -cfxObjectDestructDetector.version = "1.1.0" +cfxObjectDestructDetector.version = "1.2.0" cfxObjectDestructDetector.requiredLibs = { "dcsCommon", -- always "cfxZones", -- Zones, of course @@ -10,6 +10,7 @@ cfxObjectDestructDetector.verbose = false 1.0.0 initial version, based on parashoo, arty zones 1.0.1 fixed bug: trigger.MISC.getUserFlag() 1.1.0 added support for method, f! and destroyed! + 1.2.0 DML / Watchflag support Detect when an object with OBJECT ID as assigned in ME dies @@ -73,13 +74,21 @@ function cfxObjectDestructDetector.processObjectDestructZone(aZone) end -- new method support - aZone.method = cfxZones.getStringFromZoneProperty(aZone, "method", "flip") + aZone.oddMethod = cfxZones.getStringFromZoneProperty(aZone, "method", "flip") + if cfxZones.hasProperty(aZone, "oddMethod") then + aZone.oddMethod = cfxZones.getStringFromZoneProperty(aZone, "oddMethod", "flip") + end + if cfxZones.hasProperty(aZone, "f!") then - aZone.outDestroyFlag = cfxZones.getNumberFromZoneProperty(aZone, "f!", -1) + aZone.outDestroyFlag = cfxZones.getStringFromZoneProperty(aZone, "f!", "*none") end - if cfxZones.hasProperty(aZone, "destroyed!") then - aZone.outDestroyFlag = cfxZones.getNumberFromZoneProperty(aZone, "destroyed!", -1) + if cfxZones.hasProperty(aZone, "destroyed!") then + aZone.outDestroyFlag = cfxZones.getStringFromZoneProperty(aZone, "destroyed!", "*none") + end + + if cfxZones.hasProperty(aZone, "objectDestroyed!") then + aZone.outDestroyFlag = cfxZones.getStringFromZoneProperty(aZone, "objectDestroyed!", "*none") end end -- @@ -95,6 +104,7 @@ function cfxObjectDestructDetector:onEvent(event) for idx, aZone in pairs(cfxObjectDestructDetector.objectZones) do if aZone.ID == id then -- flag manipulation + -- OLD FLAG SUPPORT, SOON TO BE REMOVED if aZone.setFlag then trigger.action.setUserFlag(aZone.setFlag, 1) end @@ -109,10 +119,11 @@ function cfxObjectDestructDetector:onEvent(event) local val = trigger.misc.getUserFlag(aZone.decreaseFlag) - 1 trigger.action.setUserFlag(aZone.decreaseFlag, val) end + -- END OF OLD CODE, TO BE REMOVED -- support for banging if aZone.outDestroyFlag then - cfxZones.pollFlag(aZone.outDestroyFlag, aZone.method) + cfxZones.pollFlag(aZone.outDestroyFlag, aZone.oddMethod, aZone) end -- invoke callbacks diff --git a/modules/cfxReconMode.lua b/modules/cfxReconMode.lua index 8c22787..ffb51d7 100644 --- a/modules/cfxReconMode.lua +++ b/modules/cfxReconMode.lua @@ -1,11 +1,14 @@ cfxReconMode = {} -cfxReconMode.version = "1.4.1" +cfxReconMode.version = "1.5.0" cfxReconMode.verbose = false -- set to true for debug info cfxReconMode.reconSound = "UI_SCI-FI_Tone_Bright_Dry_20_stereo.wav" -- to be played when somethiong discovered cfxReconMode.prioList = {} -- group names that are high prio and generate special event cfxReconMode.blackList = {} -- group names that are NEVER detected. Comma separated strings, e.g. {"Always Hidden", "Invisible Group"} +cfxReconMode.removeWhenDestroyed = true +cfxReconMode.activeMarks = {} -- all marks and their groups, indexed by groupName + cfxReconMode.requiredLibs = { "dcsCommon", -- always "cfxZones", -- Zones, of course @@ -42,7 +45,9 @@ VERSION HISTORY - recon sound - read all flight groups at start to get rid of the - late activation work-around - + 1.5.0 - removeWhenDestroyed() + - autoRemove() + - readConfigZone creates default config zone so we get correct defaulting cfxReconMode is a script that allows units to perform reconnaissance missions and, after detecting units, marks them on the map with @@ -279,14 +284,16 @@ function cfxReconMode.removeMarkForArgs(args) local theID = args[4] local theName = args[5] --- if not theGroup then return end --- if not theGroup:isExist then return end + -- only remove if it wasn't already removed. + -- this method is called async *and* sync! + if cfxReconMode.activeMarks[theName] then + trigger.action.removeMark(theID) + -- invoke callbacks + cfxReconMode.invokeCallbacks("removed", theSide, theScout, theGroup, theName) + cfxReconMode.activeMarks[theName] = nil -- also remove from list of groups being checked + end - trigger.action.removeMark(theID) - cfxReconMode.detectedGroups[theName] = nil - - -- invoke callbacks - cfxReconMode.invokeCallbacks("removed", theSide, theScout, theGroup, theName) + cfxReconMode.detectedGroups[theName] = nil -- some housekeeping. end @@ -294,18 +301,23 @@ function cfxReconMode.detectedGroup(mySide, theScout, theGroup, theLoc) -- put a mark on the map if cfxReconMode.applyMarks then local theID = cfxReconMode.placeMarkForUnit(theLoc, mySide, theGroup) - + local gName = theGroup:getName() + local args = {mySide, theScout, theGroup, theID, gName} + cfxReconMode.activeMarks[gName] = args -- schedule removal if desired - if cfxReconMode.marksFadeAfter > 0 then - args = {mySide, theScout, theGroup, theID, theGroup:getName()} + if cfxReconMode.marksFadeAfter > 0 then timer.scheduleFunction(cfxReconMode.removeMarkForArgs, args, timer.getTime() + cfxReconMode.marksFadeAfter) end end + -- say something if cfxReconMode.announcer then trigger.action.outTextForCoalition(mySide, theScout:getName() .. " reports new ground contact " .. theGroup:getName(), 30) + trigger.action.outText("+++recon: announced for side " .. mySide, 30) -- play a sound trigger.action.outSoundForCoalition(mySide, cfxReconMode.reconSound) + else + --trigger.action.outText("+++recon: announcer off", 30) end -- see if it was a prio target @@ -420,6 +432,38 @@ function cfxReconMode.updateQueues() end end +function cfxReconMode.isGroupStillAlive(gName) + local theGroup = Group.getByName(gName) + if not theGroup then return false end + if not theGroup:isExist() then return false end + local allUnits = theGroup:getUnits() + for idx, aUnit in pairs (allUnits) do + if aUnit:getLife() >= 1 then return true end + end + return false +end + +function cfxReconMode.autoRemove() + -- schedule next call + timer.scheduleFunction(cfxReconMode.autoRemove, {}, timer.getTime() + 1/cfxReconMode.ups) + + local toRemove = {} + -- scan all marked groups, and when they no longer exist, remove them + for idx, args in pairs (cfxReconMode.activeMarks) do + -- args = {mySide, theScout, theGroup, theID, gName} + local gName = args[5] + if not cfxReconMode.isGroupStillAlive(gName) then + -- remove mark, remove group from set + table.insert(toRemove, args) + end + end + + for idx, args in pairs(toRemove) do + cfxReconMode.removeMarkForArgs(args) + trigger.action.outText("+++recn: removed mark: " .. args[5], 30) + end +end + -- event handler function cfxReconMode:onEvent(event) if not event then return end @@ -509,10 +553,11 @@ function cfxReconMode.readConfigZone() if cfxReconMode.verbose then trigger.action.outText("+++rcn: no config zone!", 30) end - return - end - if cfxReconMode.verbose then - trigger.action.outText("+++rcn: found config zone!", 30) + theZone = cfxZones.createSimpleZone("reconModeConfig") + else + if cfxReconMode.verbose then + trigger.action.outText("+++rcn: found config zone!", 30) + end end cfxReconMode.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) @@ -539,10 +584,13 @@ function cfxReconMode.readConfigZone() cfxReconMode.applyMarks = cfxZones.getBoolFromZoneProperty(theZone, "applyMarks", true) cfxReconMode.announcer = cfxZones.getBoolFromZoneProperty(theZone, "announcer", true) - + -- trigger.action.outText("recon: announcer is " .. dcsCommon.bool2Text(cfxReconMode.announcer), 30) -- announced if cfxZones.hasProperty(theZone, "reconSound") then cfxReconMode.reconSound = cfxZones.getStringFromZoneProperty(theZone, "reconSound", "") end + + cfxReconMode.removeWhenDestroyed = cfxZones.getBoolFromZoneProperty(theZone, "autoRemove", true) + end -- @@ -563,7 +611,13 @@ function cfxReconMode.start() -- start update cycle cfxReconMode.updateQueues() - + + -- if dead groups are removed from map, + -- schedule housekeeping + if cfxReconMode.removeWhenDestroyed then + cfxReconMode.autoRemove() + end + if cfxReconMode.autoRecon then -- install own event handler to detect -- when a unit takes off and add it to scout diff --git a/modules/cfxZones.lua b/modules/cfxZones.lua index 73e860a..9e7b88f 100644 --- a/modules/cfxZones.lua +++ b/modules/cfxZones.lua @@ -6,7 +6,7 @@ -- cfxZones = {} -cfxZones.version = "2.6.1" +cfxZones.version = "2.7.0" --[[-- VERSION HISTORY - 2.2.4 - getCoalitionFromZoneProperty - getStringFromZoneProperty @@ -60,7 +60,10 @@ cfxZones.version = "2.6.1" - 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 + - 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! --]]-- cfxZones.verbose = false @@ -1704,8 +1707,1800 @@ function cfxZones.startMovingZones() --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.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) + 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 diff --git a/modules/cloneZone.lua b/modules/cloneZone.lua index 23a6e1b..485cdf8 100644 --- a/modules/cloneZone.lua +++ b/modules/cloneZone.lua @@ -1,5 +1,5 @@ cloneZones = {} -cloneZones.version = "1.4.0" +cloneZones.version = "1.4.1" cloneZones.verbose = false cloneZones.requiredLibs = { "dcsCommon", -- always @@ -37,6 +37,7 @@ cloneZones.uniqueCounter = 9200000 -- we start group numbering here 1.3.1 - groupTracker interface - trackWith: attribute 1.4.0 - Watchflags + 1.4.1 - trackWith: accepts list of trackers --]]-- @@ -548,14 +549,31 @@ function cloneZones.handoffTracking(theGroup, theZone) return end local trackerName = theZone.trackWith - if trackerName == "*" then trackerName = theZone.name end - local theTracker = groupTracker.getTrackerByName(trackerName) - if not theTracker then - trigger.action.outText("+++clne: <" .. theZone.name .. ">: cannot find tracker named <".. trackerName .. ">", 30) - return + --if trackerName == "*" then trackerName = theZone.name end + -- now assemble a list of all trackers + if cloneZones.verbose or theZone.verbose then + trigger.action.outText("+++clne: clone pass-off: " .. trackerName, 30) + end + + local trackerNames = {} + if dcsCommon.containsString(trackerName, ',') then + trackerNames = dcsCommon.splitString(trackerName, ',') + else + table.insert(trackerNames, trackerName) + end + for idx, aTrk in pairs(trackerNames) do + local theName = dcsCommon.trim(aTrk) + if theName == "*" then theName = theZone.name end + local theTracker = groupTracker.getTrackerByName(theName) + if not theTracker then + trigger.action.outText("+++clne: <" .. theZone.name .. ">: cannot find tracker named <".. theName .. ">", 30) + else + groupTracker.addGroupToTracker(theGroup, theTracker) + if cloneZones.verbose or theZone.verbose then + trigger.action.outText("+++clne: added " .. theGroup:getName() .. " to tracker " .. theName, 30) + end + end end - - groupTracker.addGroupToTracker(theGroup, theTracker) end function cloneZones.spawnWithTemplateForZone(theZone, spawnZone) diff --git a/modules/dcsCommon.lua b/modules/dcsCommon.lua index d8590e6..f8921e1 100644 --- a/modules/dcsCommon.lua +++ b/modules/dcsCommon.lua @@ -1,5 +1,5 @@ dcsCommon = {} -dcsCommon.version = "2.5.7" +dcsCommon.version = "2.5.8" --[[-- VERSION HISTORY 2.2.6 - compassPositionOfARelativeToB - clockPositionOfARelativeToB @@ -67,6 +67,7 @@ dcsCommon.version = "2.5.7" - stringIsPositiveNumber() 2.5.6 - corrected stringEndsWith() bug with str 2.5.7 - point2text(p) + 2.5.8 - string2GroupCat() --]]-- @@ -1782,6 +1783,30 @@ dcsCommon.version = "2.5.7" return t end + function dcsCommon.string2GroupCat(inString) + + if not inString then return 2 end -- default ground + inString = inString:lower() + inString = dcsCommon.trim(inString) + + local catNum = tonumber(inString) + if catNum then + if catNum < 0 then catNum = 0 end + if catNum > 4 then catNum = 4 end + return catNum + end + + catNum = 2 -- ground default + if dcsCommon.stringStartsWith(inString, "grou") then catNum = 2 end + if dcsCommon.stringStartsWith(inString, "air") then catNum = 0 end + if dcsCommon.stringStartsWith(inString, "hel") then catNum = 1 end + if dcsCommon.stringStartsWith(inString, "shi") then catNum = 3 end + if dcsCommon.stringStartsWith(inString, "trai") then catNum = 4 end + + return catNum + end + + -- recursively show the contents of a variable function dcsCommon.dumpVar(key, value, prefix, inrecursion) if not inrecursion then diff --git a/modules/delayFlags.lua b/modules/delayFlags.lua index e69edc1..c1b7360 100644 --- a/modules/delayFlags.lua +++ b/modules/delayFlags.lua @@ -1,5 +1,5 @@ delayFlag = {} -delayFlag.version = "1.2.0" +delayFlag.version = "1.2.1" delayFlag.verbose = false delayFlag.requiredLibs = { "dcsCommon", -- always @@ -29,6 +29,8 @@ delayFlag.flags = {} - pauseDelay? - unpauseDelay? 1.2.0 - Watchflags + 1.2.1 - method goes to dlyMethod + - delay done is correctly inited --]]-- @@ -86,15 +88,18 @@ function delayFlag.createTimerWithZone(theZone) end - theZone.method = cfxZones.getStringFromZoneProperty(theZone, "method", "flip") + theZone.delayMethod = cfxZones.getStringFromZoneProperty(theZone, "method", "flip") - -- out flag - if cfxZones.hasProperty(theZone, "out!") then - theZone.delayDoneFlag = cfxZones.getStringFromZoneProperty(theZone, "out!", -1) + if cfxZones.hasProperty(theZone, "delayMethod") then + theZone.delayMethod = cfxZones.getStringFromZoneProperty(theZone, "delayMethod", "flip") end + -- out flag + theZone.delayDoneFlag = cfxZones.getStringFromZoneProperty(theZone, "out!", "*") + + if cfxZones.hasProperty(theZone, "delayDone!") then - theZone.delayDoneFlag = cfxZones.getStringFromZoneProperty(theZone, "delayDone!", -1) + theZone.delayDoneFlag = cfxZones.getStringFromZoneProperty(theZone, "delayDone!", "*") end -- stop the press! @@ -205,10 +210,11 @@ function delayFlag.update() -- end timer aZone.delayRunning = false -- poll flag - cfxZones.pollFlag(aZone.delayDoneFlag, aZone.method, aZone) - if delayFlag.verbose then + if delayFlag.verbose or aZone.verbose then trigger.action.outText("+++dlyF: banging on " .. aZone.delayDoneFlag, 30) - end + end + cfxZones.pollFlag(aZone.delayDoneFlag, aZone.delayMethod, aZone) + end end diff --git a/modules/groupTrackers.lua b/modules/groupTrackers.lua index 0e976d9..85e5d53 100644 --- a/modules/groupTrackers.lua +++ b/modules/groupTrackers.lua @@ -1,5 +1,5 @@ groupTracker = {} -groupTracker.version = "1.0.0" +groupTracker.version = "1.1.0" groupTracker.verbose = false groupTracker.ups = 1 groupTracker.requiredLibs = { @@ -11,6 +11,9 @@ groupTracker.trackers = {} --[[-- Version History 1.0.0 - Initial version + 1.1.0 - filtering added + - array support for trackers + - array support for trackers --]]-- @@ -37,7 +40,19 @@ end -- adding a group to a tracker - called by other modules and API -- function groupTracker.addGroupToTracker(theGroup, theTracker) - if groupTracker.verbose then + -- check if filtering is enabled for this tracker + if theTracker.groupFilter then + cat = theGroup:getCategory() + if not cat then return end -- strange, but better safe than sorry + if cat ~= theTracker.groupFilter then + if groupTracker.verbose then + trigger.action.outText("+++gTrk: Tracker <" .. theTracker.name .. "> rejected <" .. theGroup:getName() .. "> for class mismatch. Expect: " .. theTracker.groupFilter .. " received: " .. cat , 30) + end + return + end + end + + if groupTracker.verbose or theTracker.verbose then trigger.action.outText("+++gTrk: will add group <" .. theGroup:getName() .. "> to tracker " .. theTracker.name, 30) end @@ -96,6 +111,13 @@ function groupTracker.createTrackerWithZone(theZone) -- we may need to zero this flag end + if cfxZones.hasProperty(theZone, "groupFilter") then + local filterString = cfxZones.getStringFromZoneProperty(theZone, "groupFilter", "2") -- ground + theZone.groupFilter = dcsCommon.string2GroupCat(filterString) + if groupTracker.verbose or theZone.verbose then + trigger.action.outText("+++gTrck: filtering " .. theZone.groupFilter .. " in " .. theZone.name, 30) + end + end end -- @@ -121,7 +143,7 @@ function groupTracker.checkGroups(theZone) if isDead then -- bang deceased - if groupTracker.verbose then + if groupTracker.verbose or theZone.verbose then trigger.action.outText("+++gTrk: dead group detected in " .. theZone.name .. ", discarding.", 30) end if theZone.tRemoveGroup then @@ -157,6 +179,35 @@ end function groupTracker.trackGroupsInZone(theZone) local trackerName = cfxZones.getStringFromZoneProperty(theZone, "addToTracker:", "") + + local theGroups = cfxZones.allGroupsInZone(theZone, nil) + + -- now init array processing + local trackerNames = {} + if dcsCommon.containsString(trackerName, ',') then + trackerNames = dcsCommon.splitString(trackerName, ',') + else + table.insert(trackerNames, trackerName) + end + + for idx, aTrk in pairs(trackerNames) do + local theName = dcsCommon.trim(aTrk) + if theName == "*" then theName = theZone.name end + local theTracker = groupTracker.getTrackerByName(theName) + if not theTracker then + trigger.action.outText("+++gTrk-TW: <" .. theZone.name .. ">: cannot find tracker named <".. theName .. ">", 30) + else + for idy, aGroup in pairs(theGroups) do + groupTracker.addGroupToTracker(aGroup, theTracker) + if cloneZones.verbose or theZone.verbose then + trigger.action.outText("+++gTrk-TW: added " .. theGroup:getName() .. " to tracker " .. theName, 30) + end + end + end + end + + -- old code, non-array capable + --[[-- if trackerName == "*" then trackerName = theZone.name end local theTracker = groupTracker.getTrackerByName(trackerName) @@ -172,7 +223,7 @@ function groupTracker.trackGroupsInZone(theZone) end groupTracker.addGroupToTracker(aGroup, theTracker) end - + --]]-- end @@ -215,7 +266,7 @@ function groupTracker.start() end -- find and process all zones that want me to immediately add - -- units to the tracker + -- units to the tracker. Must run AFTER we have gathered all trackers local attrZones = cfxZones.getZonesWithAttributeNamed("addToTracker:") for k, aZone in pairs(attrZones) do groupTracker.trackGroupsInZone(aZone) -- process attributes @@ -234,9 +285,3 @@ if not groupTracker.start() then messenger = nil end ---[[-- - add a pass to immediately add units in zones with 'addToTracker' - after zone pass - - add an output flag for the total number of units it watches, so when that changes, a change is instigated automatically ---]]-- \ No newline at end of file diff --git a/modules/messenger.lua b/modules/messenger.lua index 9eb26a3..4f63f9e 100644 --- a/modules/messenger.lua +++ b/modules/messenger.lua @@ -1,5 +1,5 @@ messenger = {} -messenger.version = "1.2.0" +messenger.version = "1.2.1" messenger.verbose = false messenger.requiredLibs = { "dcsCommon", -- always @@ -18,8 +18,8 @@ messenger.messengers = {} 1.1.1 - firewalled coalition to msgCoalition - messageOn? - messageOff? - 1.2.0 - triggerMethod (original Watchflag integration) - + 1.2.0 - msgTriggerMethod (original Watchflag integration) + 1.2.1 - qoL: = newline, = zone name, = value --]]-- function messenger.addMessenger(theZone) @@ -40,9 +40,24 @@ end -- -- read attributes -- +function messenger.preProcMessage(inMsg, theZone) + if not inMsg then return "" end + local formerType = type(inMsg) + if formerType ~= "string" then inMsg = tostring(inMsg) end + if not inMsg then inMsg = "" end + local outMsg = "" + -- replace line feeds + outMsg = inMsg:gsub("", "\n") + if theZone then + outMsg = outMsg:gsub("", theZone.name) + end + return outMsg +end + function messenger.createMessengerWithZone(theZone) -- start val - a range - theZone.message = cfxZones.getStringFromZoneProperty(theZone, "message", "") + local aMessage = cfxZones.getStringFromZoneProperty(theZone, "message", "") + theZone.message = messenger.preProcMessage(aMessage, theZone) theZone.spaceBefore = cfxZones.getBoolFromZoneProperty(theZone, "spaceBefore", false) theZone.spaceAfter = cfxZones.getBoolFromZoneProperty(theZone, "spaceAfter", false) @@ -55,8 +70,11 @@ function messenger.createMessengerWithZone(theZone) theZone.duration = cfxZones.getNumberFromZoneProperty(theZone, "duration", 30) - -- triggerMethod - theZone.triggerMethod = cfxZones.getStringFromZoneProperty(theZone, "triggerMethod", "change") + -- msgTriggerMethod + theZone.msgTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "triggerMethod", "change") + if cfxZones.hasProperty(theZone, "msgTriggerMethod") then + theZone.msgTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "msgTriggerMethod", "change") + end -- trigger flag f? in? messageOut? if cfxZones.hasProperty(theZone, "f?") then @@ -124,6 +142,7 @@ function messenger.getMessage(theZone) -- replace *zone and *value wildcards msg = string.gsub(msg, "*name", zName) msg = string.gsub(msg, "*value", zVal) + msg = string.gsub(msg, "", zVal) return msg end @@ -163,7 +182,7 @@ function messenger.update() for idx, aZone in pairs(messenger.messengers) do -- make sure to re-start before reading time limit -- new trigger code - if cfxZones.testZoneFlag(aZone, aZone.triggerMessagerFlag, aZone.triggerMethod, "lastMessageTriggerValue") then + if cfxZones.testZoneFlag(aZone, aZone.triggerMessagerFlag, aZone.msgTriggerMethod, "lastMessageTriggerValue") then if messenger.verbose then trigger.action.outText("+++msgr: triggered on in? for <".. aZone.name ..">", 30) end @@ -171,14 +190,14 @@ function messenger.update() end -- old trigger code - if cfxZones.testZoneFlag(aZone, aZone.messageOffFlag, "change", "lastMessageOff") then + if cfxZones.testZoneFlag(aZone, aZone.messageOffFlag, aZone.msgTriggerMethod, "lastMessageOff") then aZone.messageOff = true if messenger.verbose then trigger.action.outText("+++msg: messenger <" .. aZone.name .. "> turned ***OFF***", 30) end end - if cfxZones.testZoneFlag(aZone, aZone.messageOnFlag, "change", "lastMessageOn") then + if cfxZones.testZoneFlag(aZone, aZone.messageOnFlag, aZone.msgTriggerMethod, "lastMessageOn") then aZone.messageOff = false if messenger.verbose then trigger.action.outText("+++msg: messenger <" .. aZone.name .. "> turned ON", 30) diff --git a/modules/raiseFlag.lua b/modules/raiseFlag.lua index d7638df..ee8fbf6 100644 --- a/modules/raiseFlag.lua +++ b/modules/raiseFlag.lua @@ -1,5 +1,5 @@ raiseFlag = {} -raiseFlag.version = "1.2.0" +raiseFlag.version = "1.2.1" raiseFlag.verbose = false raiseFlag.requiredLibs = { "dcsCommon", -- always @@ -14,6 +14,7 @@ raiseFlag.flags = {} 1.0.1 - synonym "raiseFlag!" 1.1.0 - DML update 1.2.0 - Watchflag update + 1.2.1 - support for 'inc', 'dec', 'flip' --]]-- function raiseFlag.addRaiseFlag(theZone) @@ -42,7 +43,8 @@ function raiseFlag.createRaiseFlagWithZone(theZone) theZone.raiseFlag = cfxZones.getStringFromZoneProperty(theZone, "raiseFlag!", "") -- the flag to raise end - theZone.flagValue = cfxZones.getNumberFromZoneProperty(theZone, "value", 1) -- value to set to + theZone.flagValue = cfxZones.getStringFromZoneProperty(theZone, "value", "inc") -- value to set to. default is command 'inc' + theZone.flagValue = theZone.flagValue:lower() theZone.minAfterTime, theZone.maxAfterTime = cfxZones.getPositiveRangeFromZoneProperty(theZone, "afterTime", -1) @@ -77,7 +79,19 @@ function raiseFlag.triggered(args) local theZone = args.theZone if theZone.raiseStopped then return end -- if we get here, we aren't stopped and do the flag pull - cfxZones.setFlagValue(theZone.raiseFlag, theZone.flagValue, theZone) + local command = theZone.flagValue + command = dcsCommon.trim(command) + if command == "inc" or command == "dec" or command == "flip" then + cfxZones.pollFlag(theZone.raiseFlag, command, theZone) + if raiseFlag.verbose or theZone.verbose then + trigger.action.outText("+++rFlg - raising <" .. theZone.raiseFlag .. "> with method " .. command ,30) + end + else + cfxZones.setFlagValue(theZone.raiseFlag, theZone.flagValue, theZone) + if raiseFlag.verbose or theZone.verbose then + trigger.action.outText("+++rFlg - raising <" .. theZone.raiseFlag .. "> to value: " .. theZone.flagValue ,30) + end + end end -- @@ -163,4 +177,7 @@ end if not raiseFlag.start() then trigger.action.outText("cfx Raise Flag aborted: missing libraries", 30) raiseFlag = nil -end \ No newline at end of file +end + +-- add rnd(a,b) support to value +-- better: if value is a range, make it a random. problem: negative values are legal, so we need formula \ No newline at end of file diff --git a/modules/unitZone.lua b/modules/unitZone.lua index 11f0093..889aaab 100644 --- a/modules/unitZone.lua +++ b/modules/unitZone.lua @@ -1,11 +1,19 @@ unitZone={} -unitZone.version = "1.0.0" +unitZone.version = "1.1.0" unitZone.verbose = false unitZone.ups = 1 unitZone.requiredLibs = { "dcsCommon", -- always "cfxZones", -- Zones, of course } +--[[-- + Version History + 1.0.0 - Initial Version + 1.1.0 - DML flag integration + - method/uzMethod + +--]]-- + unitZone.unitZones = {} function unitZone.addUnitZone(theZone) @@ -62,7 +70,17 @@ function unitZone.createUnitZone(theZone) -- coalition theZone.uzCoalition = cfxZones.getCoalitionFromZoneProperty(theZone, "coalition", 0) -- 0 = all if cfxZones.hasProperty(theZone, "uzCoalition") then - cfxZones.uzCoalition = cfxZones.getCoalitionFromZoneProperty(theZone, "uzCoalition", 0) + theZone.uzCoalition = cfxZones.getCoalitionFromZoneProperty(theZone, "uzCoalition", 0) + end + + if unitZone.verbose then + trigger.action.outText("+++uZne: set coa " .. theZone.uzCoalition .. " for <" .. theZone.name .. ">", 30) + end + + -- DML M;ethod + theZone.uzMethod = cfxZones.getStringFromZoneProperty(theZone, "method", "inc") + if cfxZones.hasProperty(theZone, "uzMethod") then + theZone.uzMethod = cfxZones.getStringFromZoneProperty(theZone, "uzMethod", "inc") end theZone.enterZone = cfxZones.getStringFromZoneProperty(theZone, "enterZone!", "") @@ -184,16 +202,16 @@ end -- function unitZone.bangState(theZone, newState) - cfxZones.pollFlag(theZone.changeZone, "inc", theZone) + cfxZones.pollFlag(theZone.changeZone, theZone.uzMethod, theZone) if newState then - cfxZones.pollFlag(theZone.enterZone, "inc", theZone) + cfxZones.pollFlag(theZone.enterZone, theZone.uzMethod, theZone) if unitZone.verbose then - trigger.action.outText("+++uZone: banging enter! on <" .. theZone.enterZone .. "> for " .. theZone.name, 30) + trigger.action.outText("+++uZone: banging enter! with <" .. theZone.uzMethod .. "> on <" .. theZone.enterZone .. "> for " .. theZone.name, 30) end else - cfxZones.pollFlag(theZone.exitZone, "inc", theZone) + cfxZones.pollFlag(theZone.exitZone, theZone.uzMethod, theZone) if unitZone.verbose then - trigger.action.outText("+++uZone: banging exit! on <" .. theZone.exitZone .. "> for " .. theZone.name, 30) + trigger.action.outText("+++uZone: banging exit! with <" .. theZone.uzMethod .. "> on <" .. theZone.exitZone .. "> for " .. theZone.name, 30) end end end diff --git a/modules/xFlags.lua b/modules/xFlags.lua index a0bbc57..2cf1fc9 100644 --- a/modules/xFlags.lua +++ b/modules/xFlags.lua @@ -1,7 +1,7 @@ xFlags = {} xFlags.version = "1.0.1" xFlags.verbose = false -xFlags.ups = 1 -- overwritten in get config! +xFlags.ups = 1 -- overwritten in get config when configZone is present xFlags.requiredLibs = { "dcsCommon", -- always "cfxZones", -- Zones, of course @@ -12,7 +12,8 @@ xFlags.requiredLibs = { Version History 1.0.0 - Initial version 1.0.1 - allow flags names for ops as well - modelled on cfxZones.testFlagByMethodForZone() + 1.1.0 - Watchflags harmonization + --]]-- xFlags.xFlagZones = {} @@ -75,9 +76,13 @@ function xFlags.createXFlagsWithZone(theZone) theZone.matchNum = cfxZones.getNumberFromZoneProperty(theZone, "#hits", 0) - theZone.lookFor = cfxZones.getStringFromZoneProperty(theZone, "lookFor", "change") -- (<>=[number or reference flag], off, on, yes, no, true, false, change - theZone.lookFor = string.lower(theZone.lookFor) - theZone.lookFor = dcsCommon.trim(theZone.lookFor) + theZone.xTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "xTriggerMethod", "change") -- (<>=[number or reference flag], off, on, yes, no, true, false, change + if cfxZones.hasProperty(theZone, "xTrigger") then + theZone.xTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "xTrigger", "change") + end + + theZone.xTriggerMethod = string.lower(theZone.xTriggerMethod) + theZone.xTriggerMethod = dcsCommon.trim(theZone.xTriggerMethod) if cfxZones.hasProperty(theZone, "xReset?") then theZone.xReset = cfxZones.getStringFromZoneProperty(theZone, "xReset?", "") @@ -104,7 +109,7 @@ function xFlags.evaluateFlags(theZone) end -- now perform comparison flag by flag - local op = theZone.lookFor + local op = theZone.xTriggerMethod local hits = 0 local checkSum = "" local firstChar = string.sub(op, 1, 1) @@ -116,6 +121,9 @@ function xFlags.evaluateFlags(theZone) rNum = cfxZones.getFlagValue(remainder, theZone) end + -- this mimics cfxZones.testFlagByMethodForZone method (and is + -- that method's genesis), but is different enough not to invoke that + -- method for i = 1, #theZone.flagNames do local lastHits = hits if op == "change" then @@ -167,7 +175,7 @@ function xFlags.evaluateFlags(theZone) end else - trigger.action.outText("+++xF: unknown lookFor: <" .. op .. ">", 30) + trigger.action.outText("+++xF: unknown xTriggerMethod: <" .. op .. ">", 30) return 0, "" end if xFlags.verbose and lastHits ~= hits then diff --git a/tutorial & demo missions/demo - ADF and NDB fun.miz b/tutorial & demo missions/demo - ADF and NDB fun.miz index 3ef08e0..33ab8be 100644 Binary files a/tutorial & demo missions/demo - ADF and NDB fun.miz and b/tutorial & demo missions/demo - ADF and NDB fun.miz differ diff --git a/tutorial & demo missions/demo - The Zonal Countdown.miz b/tutorial & demo missions/demo - The Zonal Countdown.miz index 6fb0091..ad46114 100644 Binary files a/tutorial & demo missions/demo - The Zonal Countdown.miz and b/tutorial & demo missions/demo - The Zonal Countdown.miz differ diff --git a/tutorial & demo missions/demo - Watchflags demo.miz b/tutorial & demo missions/demo - Watchflags demo.miz index 013c429..b38acff 100644 Binary files a/tutorial & demo missions/demo - Watchflags demo.miz and b/tutorial & demo missions/demo - Watchflags demo.miz differ diff --git a/tutorial & demo missions/demo - object destruct detection.miz b/tutorial & demo missions/demo - object destruct detection.miz index b937f67..558478f 100644 Binary files a/tutorial & demo missions/demo - object destruct detection.miz and b/tutorial & demo missions/demo - object destruct detection.miz differ