cloneZones = {} cloneZones.version = "2.6.0" cloneZones.verbose = false cloneZones.requiredLibs = { "dcsCommon", -- always "cfxZones", -- Zones, of course "cfxMX", } cloneZones.minSep = 10 -- minimal separation for onRoad auto-pos cloneZones.maxIter = 100 -- maximum number of attempts to resolve -- a too-close separation -- groupTracker is OPTIONAL! and required only with trackWith attribute cloneZones.cloners = {} cloneZones.callbacks = {} cloneZones.unitXlate = {} cloneZones.groupXlate = {} -- used to translate original groupID to cloned. only holds last spawned group id cloneZones.uniqueCounter = 9200000 -- we start group numbering here cloneZones.lclUniqueCounter = 1 -- zone-local init value, can be config'dHeading cloneZones.globalCounter = 1 -- module-global count cloneZones.allClones = {} -- all clones spawned, regularly GC'd -- contains DATA blocks! cloneZones.allCObjects = {} -- all clones objects cloneZones.despawnPlan = {} -- used with despawnIn cloneZones.respawnOnGroupID = true --[[-- Clones Groups from ME mission data Copyright (c) 2022-2024 by Christian Franz and cf/x AG Version History 1.9.0 - minor clean-up for synonyms - spawnWithSpawner alias for HeloTroops etc requestable SPAWN - requestable attribute - cooldown attribute - cloner collects all types used - groupScheme attribute 1.9.1 - useAI attribute 2.0.0 - clean-up 2.0.1 - improved empty! logic to account for deferred spawn when pre-wipe is active 2.1.0 - despawnIn option - inBuiltup option for rndLoc 2.2.0 - oSize - countLiveUnits() performace optimization - new countLiveAIUnits() - damaged! output - health# output - persistence: persist oSize and set lastSize 2.2.1 - verbosity updates for post-check - if cloned group is late activation, turn it off 2.3.0 - added optional cWipe? attribute to resolve possible conflict (undocumented, just to provide lazy people with a migration path) with wiper module - using "wipe?" will now create a warning 2.4.0 - reworked masterOwner to fit with dmlZone 2.5.0 - re-establish spawn zone in persistence to provide empty! detection through saves (missed hasClones) 2.5.1 - f? and in? put on notice for depreciation 2.5.2 - removed bug when checking damaged! and no units cloned 2.6.0 - maxCycles attribute - increased vorbosity during persistance:loadData --]]-- -- -- adding / removing from list -- function cloneZones.addCloneZone(theZone) table.insert(cloneZones.cloners, theZone) end function cloneZones.getCloneZoneByName(aName) for idx, aZone in pairs(cloneZones.cloners) do if aName == aZone.name then return aZone end end if cloneZones.verbose then trigger.action.outText("+++clnZ: no clone with name <" .. aName ..">", 30) end return nil end -- -- callbacks -- function cloneZones.addCallback(theCallback) if not theCallback then return end table.insert(cloneZones.callbacks, theCallback) end function cloneZones.invokeCallbacks(theZone, reason, args) if not theZone then return end if not reason then reason = "" end if not args then args = {} end -- invoke anyone who wants to know that a group -- of people was rescued. for idx, cb in pairs(cloneZones.callbacks) do cb(theZone, reason, args) end end -- -- reading zones -- function cloneZones.partOfGroupDataInZone(theZone, theUnits) local zP = cfxZones.getPoint(theZone) zP = theZone:getDCSOrigin() -- don't use getPoint now. zP.y = 0 for idx, aUnit in pairs(theUnits) do local uP = {} uP.x = aUnit.x uP.y = 0 uP.z = aUnit.y -- !! y-z if theZone:pointInZone(uP) then return true end end return false end function cloneZones.allGroupsInZoneByData(theZone) local theGroupsInZone = {} for groupName, groupData in pairs(cfxMX.groupDataByName) do if groupData.units then if cloneZones.partOfGroupDataInZone(theZone, groupData.units) then theGroup = Group.getByName(groupName) table.insert(theGroupsInZone, theGroup) end end end return theGroupsInZone end function cloneZones.createClonerWithZone(theZone) -- has "Cloner" if cloneZones.verbose or theZone.verbose then trigger.action.outText("+++clnZ: new cloner <" .. theZone.name ..">", 30) end theZone.spawnWithSpawner = cloneZones.spawnWithSpawner theZone.myUniqueCounter = cloneZones.lclUniqueCounter -- init local counter local localZones = cloneZones.allGroupsInZoneByData(theZone) local localObjects = theZone:allStaticsInZone(true) -- true = use DCS origin, not moved zone if theZone.verbose then trigger.action.outText("+++clnZ: building cloner <" .. theZone.name .. "> TMPL: >>>", 30) for idx, theGroup in pairs (localZones) do trigger.action.outText("Zone <" .. theZone.name .. ">: group <" .. theGroup:getName() .. "> in template", 30) end for idx, theObj in pairs(localObjects) do trigger.action.outText("Zone <" .. theZone.name .. ">: static object <" .. theObj:getName() .. "> in template", 30) end trigger.action.outText("END cloner <" .. theZone.name .. "> TMPL: <<<", 30) end theZone.cloner = true -- this is a cloner zoner theZone.mySpawns = {} theZone.myStatics = {} -- use getDCSOrigin instead theZone.origin = theZone:getDCSOrigin() -- source tells us which template to use. it can be the following: -- nothing (no attribute) - then we use whatever groups are in zone to -- spawn as template -- name of another spawner that provides the template -- we can't simply use a group name as we lack the reference -- location for delta if theZone:hasProperty("source") then theZone.source = theZone:getStringFromZoneProperty("source", "") if theZone.source == "" then theZone.source = nil end end theZone.allTypes = {} -- names of all types if not theZone.source then theZone.cloneNames = {} -- names of the groups. only present in template spawners theZone.staticNames = {} -- names of all statics. only present in templates for idx, aGroup in pairs(localZones) do local gName = aGroup:getName() if gName then table.insert(theZone.cloneNames, gName) table.insert(theZone.mySpawns, aGroup) -- collect them for initial despawn -- now get group data and save a lookup for -- resolving internal references local rawData, cat, ctry = cfxMX.getGroupFromDCSbyName(gName) -- iterate all units and save their individual types for idy, aUnit in pairs(rawData.units) do local theType = aUnit.type if not theZone.allTypes[theType] then theZone.allTypes[theType] = 1 -- first one else theZone.allTypes[theType] = theZone.allTypes[theType] + 1 -- increment end end local origID = rawData.groupId end end for idx, aStatic in pairs (localObjects) do local sName = aStatic:getName() if sName then table.insert(theZone.staticNames, sName) table.insert(theZone.myStatics, aStatic) end end cloneZones.despawnAll(theZone) if (#theZone.cloneNames + #theZone.staticNames) < 1 then if cloneZones.verbose then trigger.action.outText("+++clnZ: WARNING - Template in clone zone <" .. theZone.name .. "> is empty", 30) end theZone.cloneNames = nil theZone.staticNames = nil end if cloneZones.verbose then trigger.action.outText(theZone.name .. " clone template saved", 30) end end theZone.declutter = theZone:getBoolFromZoneProperty("declutter", false) theZone.cloneTriggerMethod = theZone:getStringFromZoneProperty("triggerMethod", "change") if theZone:hasProperty("cloneTriggerMethod") then theZone.cloneTriggerMethod = theZone:getStringFromZoneProperty("cloneTriggerMethod", "change") end if theZone:hasProperty("f?") then -- deprecated! theZone.spawnFlag = theZone:getStringFromZoneProperty("f?", "none") elseif theZone:hasProperty("in?") then -- deprecated theZone.spawnFlag = theZone:getStringFromZoneProperty("in?", "none") elseif theZone:hasProperty("spawn?") then theZone.spawnFlag = theZone:getStringFromZoneProperty("spawn?", "none") elseif theZone:hasProperty("clone?") then theZone.spawnFlag = theZone:getStringFromZoneProperty("clone?", "none") end if theZone.spawnFlag then theZone.lastSpawnValue = theZone:getFlagValue(theZone.spawnFlag) end if theZone:hasProperty("deSpawn?") then theZone.deSpawnFlag = theZone:getStringFromZoneProperty( "deSpawn?", "none") elseif theZone:hasProperty("deClone?") then theZone.deSpawnFlag = theZone:getStringFromZoneProperty( "deClone?", "none") elseif theZone:hasProperty("wipe?") then theZone.deSpawnFlag = theZone:getStringFromZoneProperty("wipe?", "none") trigger.action.outText("+++clnZ: WARNING - Clone Zone <" .. theZone.name .. ">: attribute 'wipe?' is deprecated for clone zones!", 30) -- note possible conflict with wiper module, so we add the new -- cWipe? attribute elseif theZone:hasProperty("cWipe?") then theZone.deSpawnFlag = theZone:getStringFromZoneProperty("cWipe?", "none") end if theZone.deSpawnFlag then theZone.lastDeSpawnValue = theZone:getFlagValue(theZone.deSpawnFlag) end theZone.cooldown = theZone:getNumberFromZoneProperty("cooldown", -1) -- anything > 0 activates cd theZone.lastSpawnTimeStamp = -10000 theZone.onStart = theZone:getBoolFromZoneProperty("onStart", false) theZone.moveRoute = theZone:getBoolFromZoneProperty("moveRoute", false) theZone.preWipe = theZone:getBoolFromZoneProperty("preWipe", false) if theZone:hasProperty("empty!") then theZone.emptyBangFlag = theZone:getStringFromZoneProperty("empty!", "") -- note string on number default end theZone.cloneMethod = theZone:getStringFromZoneProperty("cloneMethod", "inc") if theZone:hasProperty("method") then theZone.cloneMethod = theZone:getStringFromZoneProperty("method", "inc") -- note string on number default end theZone.turn = theZone:getNumberFromZoneProperty("turn", 0) -- interface to groupTracker if theZone:hasProperty("trackWith:") then theZone.trackWith = theZone:getStringFromZoneProperty( "trackWith:", "") end -- interface to delicates if theZone:hasProperty("useDelicates") then theZone.delicateName = dcsCommon.trim(theZone:getStringFromZoneProperty("useDelicates", "")) if theZone.delicateName == "*" then theZone.delicateName = theZone.name end if theZone.verbose then trigger.action.outText("+++clnZ: cloner <" .. theZone.name .."> hands off delicates to <" .. theZone.delicateName .. ">", 30) end end -- interface to requestable, must be unsourced! if theZone:hasProperty("requestable") then theZone.requestable = theZone:getBoolFromZoneProperty( "requestable", false) theZone.baseName = theZone.name -- backward compatibility with HeloTroops if theZone.source then trigger.action.outText("WARNING: cloner <" .. theZone.name .. "> has 'source' attribute and is marked 'requestable' - this can result in unrequestable clones", 30) end end -- randomized locations on spawn theZone.rndLoc = theZone:getBoolFromZoneProperty("randomizedLoc", false) if theZone:hasProperty("rndLoc") then theZone.rndLoc = theZone:getBoolFromZoneProperty("rndLoc", false) end theZone.centerOnly = theZone:getBoolFromZoneProperty("centerOnly", false) if theZone:hasProperty("wholeGroups") then theZone.centerOnly = theZone:getBoolFromZoneProperty( "wholeGroups", false) end if theZone:hasProperty("inBuiltup") then theZone.inBuiltup = theZone:getNumberFromZoneProperty("inBuiltup", 10) -- 10 meter radius must be free -- small houses end theZone.rndHeading = theZone:getBoolFromZoneProperty("rndHeading", false) theZone.onRoad = theZone:getBoolFromZoneProperty("onRoad", false) theZone.onPerimeter = theZone:getBoolFromZoneProperty("onPerimeter", false) -- check for name scheme and / or identical if theZone:hasProperty("identical") then theZone.identical = theZone:getBoolFromZoneProperty("identical", false) if theZone.identical == false then theZone.identical = nil end end if theZone:hasProperty("nameScheme") then theZone.nameScheme = theZone:getStringFromZoneProperty( "nameScheme", "-") -- default to [ "-" ] end if theZone:hasProperty("groupScheme") then theZone.groupScheme = theZone:getStringFromZoneProperty("groupScheme", "-") end if theZone.identical and theZone.nameScheme then trigger.action.outText("+++clnZ: WARNING - clone zone <" .. theZone.name .. "> has both IDENTICAL and NAMESCHEME/GROUPSCHEME attributes. nameScheme is ignored.", 30) theZone.nameScheme = nil theZone.groupScheme = nil end theZone.useAI = theZone:getBoolFromZoneProperty("useAI", true) if theZone:hasProperty("despawnIn") then theZone.despawnInMin, theZone.despawnInMax = theZone:getPositiveRangeFromZoneProperty("despawnIn", 2,2) end -- damaged and health interface if theZone:hasProperty("damaged!") then theZone.damaged = theZone:getStringFromZoneProperty("damaged!") end if theZone:hasProperty("health#") then theZone.health = theZone:getStringFromZoneProperty("health#") end -- maxCycles if theZone:hasProperty("maxCycles") then theZone.maxCycles = theZone:getNumberFromZoneProperty("maxCycles", 99999) end if theZone:hasProperty("maxCycle") then theZone.maxCycles = theZone:getNumberFromZoneProperty("maxCycle", 99999) end -- we end with clear plate theZone.lastSize = 0 -- no units here end -- -- spawning, despawning -- function cloneZones.despawnAll(theZone) if cloneZones.verbose or theZone.verbose then trigger.action.outText("+++clnZ: despawn all - wiping zone <" .. theZone.name .. ">", 30) end theZone.oSize = 0 -- original spawn size for idx, aGroup in pairs(theZone.mySpawns) do if aGroup:isExist() then if theZone.verbose then trigger.action.outText("+++clnZ: will destroy <" .. aGroup:getName() .. ">", 30) end cloneZones.invokeCallbacks(theZone, "will despawn group", aGroup) Group.destroy(aGroup) end end for idx, aStatic in pairs(theZone.myStatics) do -- warning! may be mismatch because we are looking at groups -- not objects. let's see if aStatic:isExist() then if cloneZones.verbose or theZone.verbose then trigger.action.outText("Destroying static <" .. aStatic:getName() .. ">", 30) end cloneZones.invokeCallbacks(theZone, "will despawn static", aStatic) Object.destroy(aStatic) -- we don't aStatio:destroy() to find out what it is end end theZone.mySpawns = {} theZone.myStatics = {} end function cloneZones.assignClosestParking(theData) -- on enter: theData has units with updated x, y -- and waypoint 1 action is From Parking -- and it has at least one unit -- let's get the airbase local theRoute = theData.route -- we know it exists local thePoints = theRoute.points local firstPoint = thePoints[1] local loc = {} loc.x = firstPoint.x loc.y = 0 loc.z = firstPoint.y local theAirbase = dcsCommon.getClosestAirbaseTo(loc) -- now let's assign free slots closest to unit local slotsTaken = {} local units = theData.units local cat = cfxMX.groupTypeByName[theData.name] for idx, theUnit in pairs(units) do local newSlot = dcsCommon.getClosestFreeSlotForCatInAirbaseTo(cat, theUnit.x, theUnit.y, theAirbase, slotsTaken) if newSlot then local slotNo = newSlot.Term_Index theUnit.parking_id = nil -- !! or you b screwed theUnit.parking = slotNo -- !! screw parking_ID, they don't match theUnit.x = newSlot.vTerminalPos.x theUnit.y = newSlot.vTerminalPos.z -- !!! table.insert(slotsTaken, slotNo) end end end function cloneZones.rotateWPAroundCenter(thePoint, center, angle) -- angle in rads -- move to center thePoint.x = thePoint.x - center.x thePoint.y = thePoint.y - center.z -- !! -- rotate local c = math.cos(angle) local s = math.sin(angle) local px = thePoint.x * c - thePoint.y * s local py = thePoint.x * s + thePoint.y * c -- apply and move back thePoint.x = px + center.x thePoint.y = py + center.z -- !! end function cloneZones.updateTaskLocations(thePoint, zoneDelta) -- parse tasks for x and y and update them by zoneDelta if thePoint and thePoint.task and thePoint.task.params and thePoint.task.params.tasks then local theTasks = thePoint.task.params.tasks for idx, aTask in pairs(theTasks) do -- EngageTargetsInZone task has x & y in params if aTask.params and aTask.params.x and aTask.params.y then aTask.params.x = aTask.params.x + zoneDelta.x aTask.params.y = aTask.params.y + zoneDelta.z --!! -- trigger.action.outText("moved search & engage zone", 30) end end end end function cloneZones.updateLocationsInGroupData(theData, zoneDelta, adjustAllWaypoints, center, angle) -- enter with theData being group's data block -- remember that zoneDelta's [z] modifies theData's y!! local units = theData.units local departFromAerodrome = false local fromParking = false for idx, aUnit in pairs(units) do aUnit.x = aUnit.x + zoneDelta.x aUnit.y = aUnit.y + zoneDelta.z -- again!!!! end -- now modifiy waypoints. we ALWAYS adjust the -- first waypoint, all others only if asked -- to by moveRoute attribute (adjustAllWaypoints) local theRoute = theData.route if theRoute then local thePoints = theRoute.points if thePoints and #thePoints > 0 then if adjustAllWaypoints then for i=1, #thePoints do thePoints[i].x = thePoints[i].x + zoneDelta.x thePoints[i].y = thePoints[i].y + zoneDelta.z -- (!!) -- rotate around center by angle if given if center and angle then cloneZones.rotateWPAroundCenter(thePoints[i], center, angle) else -- trigger.action.outText("not rotating route", 30) end cloneZones.updateTaskLocations(thePoints[i], zoneDelta) end else -- only first point thePoints[1].x = thePoints[1].x + zoneDelta.x thePoints[1].y = thePoints[1].y + zoneDelta.z -- (!!) if center and angle then cloneZones.rotateWPAroundCenter(thePoints[1], center, angle) end cloneZones.updateTaskLocations(thePoints[i], zoneDelta) end -- if there is an airodrome id given in first waypoint, -- adjust for closest location local firstPoint = thePoints[1] if firstPoint.airdromeId then local loc = {} loc.x = firstPoint.x loc.y = 0 loc.z = firstPoint.y local bestAirbase = dcsCommon.getClosestAirbaseTo(loc) --departingAerodrome = bestAirbase firstPoint.airdromeId = bestAirbase:getID() departFromAerodrome = true fromParking = dcsCommon.stringStartsWith(firstPoint.action, "From Parking") end -- adjust last point (landing) if #thePoints > 1 then local lastPoint = thePoints[#thePoints] if firstPoint.airdromeId then local loc = {} loc.x = lastPoint.x loc.y = 0 loc.z = lastPoint.y local bestAirbase = dcsCommon.getClosestAirbaseTo(loc) lastPoint.airdromeId = bestAirbase:getID() end end end -- if points in route end -- if route -- now process departing slot if given if departFromAerodrome then -- we may need alt from land to add here, maybe later -- now process parking slots, and choose closest slot -- per unit's location if fromParking then cloneZones.assignClosestParking(theData) end end end function cloneZones.nameFromSchema(schema, inName, theZone, sourceName, i) -- default schema (classic) is "-" local outName = schema local iter = i -- replace all occurences of with original name outName = outName:gsub("", inName) -- replace all occurences of with zone name outName = outName:gsub("", theZone.name) -- replace all occurences of with source zone name outName = outName:gsub("", sourceName) -- uid (uuid) with auto-increment local pos = string.find(outName, "") while (pos and pos > 0) do local uid = tostring(dcsCommon.numberUUID()) outName = outName:gsub("", uid, 1) -- only first substitution pos = string.find(outName, "") end -- i (iter) with increment pos = string.find(outName, "") while (pos and pos > 0) do local uid = tostring(iter) outName = outName:gsub("", uid, 1) -- only first substitution iter = iter + 1 pos = string.find(outName, "") end -- lcl local (zonal) count with increment pos = string.find(outName, "") while (pos and pos > 0) do local uid = tostring(theZone.myUniqueCounter) outName = outName:gsub("", uid, 1) -- only first substitution theZone.myUniqueCounter = theZone.myUniqueCounter + 1 pos = string.find(outName, "") end -- g global (module) count with increment pos = string.find(outName, "") while (pos and pos > 0) do local uid = tostring(cloneZones.globalCounter) outName = outName:gsub("", uid, 1) -- only first substitution cloneZones.globalCounter = cloneZones.globalCounter + 1 pos = string.find(outName, "") end return outName, iter end function cloneZones.uniqueID() local uid = cloneZones.uniqueCounter cloneZones.uniqueCounter = cloneZones.uniqueCounter + 1 return uid end function cloneZones.uniqueNameGroupData(theData, theCloneZone, sourceName) if not sourceName then sourceName = theCloneZone.name end if not theCloneZone.groupScheme then theData.name = dcsCommon.uuid(theData.name) else theData.name = cloneZones.nameFromSchema(theCloneZone.groupScheme, theData.name, theCloneZone, sourceName, 1) end local schema = theCloneZone.nameScheme local units = theData.units local iterCount = 1 local newName = "none" local allNames = {} -- enforce unique names inside group for idx, aUnit in pairs(units) do if theCloneZone and theCloneZone.nameScheme then newName, iterCount = cloneZones.nameFromSchema(schema, aUnit.name, theCloneZone, sourceName, iterCount) -- make sure that this name is has not been generated yet -- inside the same group local hasChanged = false local schemeName = newName while dcsCommon.arrayContainsString(allNames, newName) do newName = newName .. "x" hasChanged = true end if theCloneZone.verbose and hasChanged then trigger.action.outText("cnlz: nameScheme [" .. theCloneZone.nameScheme .. "] failsafe: changed <" .. schemeName .. "> to <" .. newName .. ">", 30) end table.insert(allNames, newName) if theCloneZone.verbose then trigger.action.outText("clnZ: zone <" .. theCloneZone.name .. "> unit schema <" .. schema .. ">: <" .. aUnit.name .. "> --> <" .. newName .. ">", 30) end aUnit.name = newName -- dcsCommon.uuid(aUnit.name) else -- default naming scheme: - aUnit.name = dcsCommon.uuid(aUnit.name) end end end function cloneZones.uniqueNameStaticData(theData, theCloneZone, sourcename) if not sourceName then sourceName = theCloneZone.name end -- WARNING: unlike GroupData enters with UNIT data local iterCount = 1 local newName = "none" if theCloneZone and theCloneZone.nameScheme then local schema = theCloneZone.nameScheme newName, iterCount = cloneZones.nameFromSchema(schema, theData.name, theCloneZone, sourceName, iterCount) if theCloneZone.verbose then trigger.action.outText("clnZ: zone <" .. theCloneZone.name .. "> static schema <" .. schema .. ">: <" .. theData.name .. "> --> <" .. newName .. ">", 30) end theData.name = newName -- dcsCommon.uuid(theData.name) else -- default naming scheme: - theData.name = dcsCommon.uuid(theData.name) end end function cloneZones.uniqueIDGroupData(theData) theData.groupId = cloneZones.uniqueID() end function cloneZones.uniqueIDUnitData(theData) if not theData then return end if not theData.units then return end local units = theData.units for idx, aUnit in pairs(units) do aUnit.CZorigID = aUnit.unitId aUnit.unitId = cloneZones.uniqueID() aUnit.CZTargetID = aUnit.unitId end end function cloneZones.sameIDUnitData(theData) if not theData then return end if not theData.units then return end local units = theData.units for idx, aUnit in pairs(units) do aUnit.CZorigID = aUnit.unitId aUnit.CZTargetID = aUnit.unitId end end function cloneZones.resolveOwnership(spawnZone, ctry) if not spawnZone.masterOwner then return ctry end -- old code ctry = dcsCommon.getACountryForCoalition(spawnZone:getCoalition()) return ctry end -- -- resolve external group references -- function cloneZones.resolveGroupID(gID, rawData, dataTable, reason) if not reason then reason = "" end local resolvedID = gID local myOName = rawData.CZorigName local groupName = cfxMX.groupNamesByID[gID] -- first, check if this an internal reference, i.e. inside the same -- zone template for idx, otherData in pairs(dataTable) do -- look in own data table if otherData.CZorigName == groupName then -- using cfxMX for clarity only (name access) resolvedID = otherData.CZTargetID return resolvedID end end -- now check if we have spawned this before local lastClone = cloneZones.groupXlate[gID] if lastClone then resolvedID = lastClone return resolvedID end -- if we get here, reference is not to a cloned item return resolvedID end function cloneZones.resolveUnitID(uID, rawData, dataTable, reason) -- also resolves statics as they share ID with units local resolvedID = uID -- first, check if this an internal reference, i.e. inside the same -- zone template for idx, otherData in pairs(dataTable) do -- iterate all units for idy, aUnit in pairs(otherData.units) do if aUnit.CZorigID == uID then resolvedID = aUnit.CZTargetID return resolvedID end end end -- now check if we have spawned this before local lastClone = cloneZones.unitXlate[uID] if lastClone then resolvedID = lastClone return resolvedID end -- if we get here, reference is not to a cloned item return resolvedID end function cloneZones.resolveStaticLinkUnit(uID) local resolvedID = uID local lastClone = cloneZones.unitXlate[uID] if lastClone then resolvedID = lastClone return resolvedID end return resolvedID end function cloneZones.resolveWPReferences(rawData, theZone, dataTable) -- check to see if we really need data table, as we have theZone -- perform a check of route for group or unit references if not rawData then return end local myOName = rawData.CZorigName if rawData.route and rawData.route.points then local points = rawData.route.points for idx, aPoint in pairs(points) do -- check if there is a link unit here and resolve if aPoint.linkUnit then local gID = aPoint.linkUnit local resolvedID = cloneZones.resolveUnitID(gID, rawData, dataTable, "linkUnit") aPoint.linkUnit = resolvedID end -- iterate all tasks assigned to point local task = aPoint.task if task and task.params and task.params.tasks then local tasks = task.params.tasks -- iterate all tasks for this waypoint for idy, taskData in pairs(tasks) do -- resolve group references in TASKS -- also covers recovery tanke etc if taskData.id and taskData.params and taskData.params.groupId then -- we resolve group reference local gID = taskData.params.groupId local resolvedID = cloneZones.resolveGroupID(gID, rawData, dataTable, taskData.id) taskData.params.groupId = resolvedID end -- resolve EMBARK/DISEMBARK group references if taskData.id and taskData.params and taskData.params.groupsForEmbarking then -- build new groupsForEmbarking local embarkers = taskData.params.groupsForEmbarking local newEmbarkers = {} for grpIdx, gID in pairs(embarkers) do local resolvedID = cloneZones.resolveGroupID(gID, rawData, dataTable, "embark") table.insert(newEmbarkers, resolvedID) end -- replace old with new table taskData.params.groupsForEmbarking = newEmbarkers end -- resolve DISTRIBUTION (embark) unit/group refs if taskData.id and taskData.params and taskData.params.distribution then local newDist = {} -- will replace old for aUnit, aList in pairs(taskData.params.distribution) do -- first, translate this unit's number local newUnit = cloneZones.resolveUnitID(aUnit, rawData, dataTable, "transportID") local embarkers = aList local newEmbarkers = {} for grpIdx, gID in pairs(embarkers) do -- translate old to new local resolvedID = cloneZones.resolveGroupID(gID, rawData, dataTable, "embark") table.insert(newEmbarkers, resolvedID) end -- store this as new group for -- translated transportID newDist[newUnit] = newEmbarkers end -- replace old distribution with new taskData.params.distribution = newDist end -- resolve selectedTransport unit reference if taskData.id and taskData.params and taskData.params.selectedTransportt then local tID = taskData.params.selectedTransport local newTID = cloneZones.resolveUnitID(tID, rawData, dataTable, "transportID") taskData.params.selectedTransport = newTID end -- note: we may need to process x and y as well -- resolve UNIT references in TASKS if taskData.id and taskData.params and taskData.params.unitId then -- we don't look for keywords, we simply resolve local uID = taskData.params.unitId local resolvedID = cloneZones.resolveUnitID(uID, rawData, dataTable, taskData.id) taskData.params.unitId = resolvedID end -- resolve unit references in ACTIONS -- for example TACAN if taskData.params and taskData.params.action and taskData.params.action.params and taskData.params.action.params.unitId then local uID = taskData.params.action.params.unitId local resolvedID = cloneZones.resolveUnitID(uID, rawData, dataTable, "Action") taskData.params.action.params.unitId = resolvedID end end end end end end function cloneZones.resolveReferences(theZone, dataTable) -- when an action refers to another group, we check if -- the group referred to is also a clone, and update -- the reference to the newest incardnation for idx, rawData in pairs(dataTable) do -- resolve references in waypoints cloneZones.resolveWPReferences(rawData, theZone, dataTable) end end function cloneZones.handoffTracking(theGroup, theZone) if not groupTracker then trigger.action.outText("+++clnZ: <" .. theZone.name .. "> trackWith requires groupTracker module", 30) return end local trackerName = theZone.trackWith -- now assemble a list of all trackers if cloneZones.verbose or theZone.verbose then trigger.action.outText("+++clnZ: 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("+++clnZ: <" .. theZone.name .. ">: cannot find tracker named <".. theName .. ">", 30) else groupTracker.addGroupToTracker(theGroup, theTracker) if cloneZones.verbose or theZone.verbose then trigger.action.outText("+++clnZ: added " .. theGroup:getName() .. " to tracker " .. theName, 30) end end end end function cloneZones.validateSpawnUnitData(aUnit, theZone, unitNames) -- entry with unit data construct -- also used for static objects! if not aUnit then return end if not theZone then return end -- we only verify replacement if identical or name sheme attribute if not (theZone.identical or theZone.nameScheme) then return end if unitNames[aUnit.name] then trigger.action.outText("clnZ: <" .. theZone.name .. "> validation warning - Unit/Object name <" .. aUnit.name .. ">: duplicate name within spawn cycle, will be repaced", 30) else unitNames[aUnit.name] = true end local theUnit = Unit.getByName(aUnit.name) if theUnit and Unit.isExist(theUnit) then if cloneZones.verbose or theZone.verbose then trigger.action.outText("+++clnZ: cloner <" .. theZone.name .. "> will replace existing UNIT <" .. aUnit.name .. ">", 30) end -- since we are about to replace a unit, we also steal the ID local stolenID = theUnit:getID() aUnit.unitId = stolenID else -- now check if we are about to grab an MX data ID -- and ned to steal that local stolenID = cfxMX.unitIDbyName[aUnit.name] if stolenID then if cloneZones.verbose or theZone.verbose then trigger.action.outText("+++clnZ: cloner <" .. theZone.name .. "> will replace MX UNIT ID <" .. aUnit.name .. "> by appropriating ID <" .. stolenID .. ">", 30) end aUnit.unitId = stolenID end end -- check against static objects. local theStatic = StaticObject.getByName(aUnit.name) if theStatic and StaticObject.isExist(theStatic) then trigger.action.outText("+++clnZ: cloner <" .. theZone.name .. "> will replace existing STATIC <" .. aUnit.name .. ">", 30) end end function cloneZones.validateSpawnGroupData(theData, theZone, groupNames, unitNames) -- entry with group construct if not theData then return end if not theZone then return end -- we only verify replacement if identical or name sheme attribute if not (theZone.identical or theZone.nameScheme) then return end if groupNames[theData.name] then trigger.action.outText("clnZ: <" .. theZone.name .. "> validation warning - group name <" .. theData.name .. ">: duplicate within spawn, previous spawn will be removed", 30) else groupNames[theData.name] = true end local theGroup = Group.getByName(theData.name) if theGroup and Group.isExist(theGroup) and theGroup:getSize() > 0 then trigger.action.outText("+++clnZ: cloner <" .. theZone.name .. "> will replace existing GROUP <" .. theData.name .. ">", 30) end if not theData.units then return end local units = theData.units for idx, aUnit in pairs(units) do cloneZones.validateSpawnUnitData(aUnit, theZone, unitNames) end end -- forcedRespan respawns a group when the previous spawn of a -- group did not match the ID that it was supposed to match function cloneZones.forcedRespawn(args) local theData = args[1] local spawnedGroups = args[2] local pos = args[3] local theZone = args[4] local verbose = theZone.verbose local rawData = dcsCommon.clone(theData) if verbose then trigger.action.outText("clnZ: enter forced respawn of <" .. theData.name .. "> to meet ID " .. theData.CZTargetID .. " (currently set for <" .. theData.groupId .. ">)", 30) end -- we now try to spawn again, with hopes of receiving the -- correct id local theGroup = coalition.addGroup(rawData.CZctry, rawData.CZtheCat, rawData) -- make sure that this time the id matches local newGroupID = theGroup:getID() if newGroupID == theData.CZTargetID then if verbose then trigger.action.outText("GOOD REPLACEMENT new ID <" .. newGroupID .. "> matches target <" .. theData.CZTargetID .. "> for <" .. theData.name .. ">", 30) trigger.action.outText("will replace table entry at <" .. pos .. "> with new group", 30) end spawnedGroups[pos] = theGroup -- since we are now successful, check if we need to apply -- delicate status if theZone.delicateName and delicates then -- pass this object to the delicate zone mentioned local theDeli = delicates.getDelicatesByName(theZone.delicateName) if theDeli then delicates.addGroupToInventoryForZone(theDeli, theGroup) else trigger.action.outText("+++clnZ: spawner <" .. theZone.name .. "> can't find delicates zone <" .. theZone.delicateName .. ">", 30) end elseif theZone.delicateName then trigger.action.outText("+++clnZ: WARNING - cloner <> requires 'Delicates' module.", 30) end else -- we need to try again in one second if verbose then trigger.action.outText("FAIL: new ID <" .. newGroupID .. "> does not match target <" .. theData.CZTargetID .. "> for <" .. theData.name .. ">. Will re-try in 1s", 30) end spawnedGroups[pos] = theGroup -- replace so we don't fail checks timer.scheduleFunction(cloneZones.forcedRespawn, args, timer.getTime() + 1) end end function cloneZones.spawnWithTemplateForZone(theZone, spawnZone) if cloneZones.verbose or spawnZone.verbose then trigger.action.outText("+++clnZ: spawning with template <" .. theZone.name .. "> for spawner <" .. spawnZone.name .. ">", 30) end -- theZone is the cloner with the TEMPLATE (source) -- spawnZone is the actual spawner with SETTINGS and DESTINATION (target location) where the clones are poofed into existence local newCenter = spawnZone:getPoint() -- includes zone following updates local oCenter = theZone:getDCSOrigin() -- get original coords on map for cloning offsets -- calculate zoneDelta, is added to all vectors local zoneDelta = dcsCommon.vSub(newCenter, theZone.origin) -- precalc turn value for linked rotation local dHeading = 0 -- for linked zones local rotCenter = nil if spawnZone.linkedUnit and spawnZone.uHdg and spawnZone.useHeading and Unit.isExist(spawnZone.linkedUnit) then local theUnit = spawnZone.linkedUnit local currHeading = dcsCommon.getUnitHeading(theUnit) dHeading = currHeading - spawnZone.uHdg rotCenter = spawnZone:getPoint() end local spawnedGroups = {} local spawnedStatics = {} local dataToSpawn = {} -- temp save so we can connect in-group references for idx, aGroupName in pairs(theZone.cloneNames) do local rawData, cat, ctry = cfxMX.getGroupFromDCSbyName(aGroupName) -- fetches a clone! -- sanity checks: lateActivation etc if rawData.lateActivation then trigger.action.outText("+++clnZ: WARNING - clone group <" .. rawData.name .. "> in cloner <" .. theZone.name .. "> is set to 'late activation'. Ignored.", 30) rawData.lateActivation = false end rawData.CZorigName = rawData.name -- save original group name local origID = rawData.groupId -- save original group ID rawData.CZorigID = origID if spawnZone.identical then cloneZones.sameIDUnitData(rawData) -- set up CZTargetID for units to be same as in template else -- only assign new ids when 'identical' flag is not active cloneZones.uniqueIDGroupData(rawData) -- assign unique ID we know cloneZones.uniqueIDUnitData(rawData) -- assign unique ID for units -- saves old unitId as CZorigID end rawData.CZTargetID = rawData.groupId -- save if rawData.name ~= aGroupName then trigger.action.outText("Clone: FAILED name check", 30) end local theCat = cfxMX.catText2ID(cat) rawData.CZtheCat = theCat -- save category -- update routes when not spawning same location cloneZones.updateLocationsInGroupData(rawData, zoneDelta, spawnZone.moveRoute, rotCenter, spawnZone.turn / 57.2958 + dHeading) -- apply randomizer if selected if spawnZone.rndLoc then -- calculate the entire group's displacement local units = rawData.units local loc, dx, dy if spawnZone.onPerimeter then loc, dx, dy = spawnZone:createRandomPointOnZoneBoundary() elseif spawnZone.inBuiltup then loc, dx, dy = spawnZone:createRandomPointInPopulatedZone(spawnZone.inBuiltup) else loc, dx, dy = spawnZone:createRandomPointInZone() -- also supports polygonal zones end for idx, aUnit in pairs(units) do if not spawnZone.centerOnly then -- *every unit's displacement is randomized if spawnZone.onPerimeter then loc, dx, dy = spawnZone:createRandomPointOnZoneBoundary() elseif spawnZone.inBuiltup then loc, dx, dy = spawnZone:createRandomPointInPopulatedZone(spawnZone.inBuiltup) else loc, dx, dy = spawnZone:createRandomPointInZone() end aUnit.x = loc.x aUnit.y = loc.z else aUnit.x = aUnit.x + dx aUnit.y = aUnit.y + dy end if spawnZone.verbose or cloneZones.verbose then trigger.action.outText("+++clnZ: <" .. spawnZone.name .. "> R = " .. spawnZone.radius .. ":G<" .. rawData.name .. "/" .. aUnit.name .. "> - rndLoc: dx = " .. dx .. ", dy= " .. dy .. ".", 30) end end end if spawnZone.rndHeading then local units = rawData.units if spawnZone.centerOnly and units and units[1] then -- rotate entire group around unit 1 local cx = units[1].x local cy = units[1].y local degrees = 360 * math.random() -- rotateGroupData uses degrees dcsCommon.rotateGroupData(rawData, degrees, cx, cy) else for idx, aUnit in pairs(units) do local phi = 6.2831 * math.random() -- that's 2Pi, folx aUnit.heading = phi end end end -- apply onRoad option if selected if spawnZone.onRoad then local units = rawData.units if spawnZone.centerOnly then -- only place the first unit in group on roads -- and displace all other with the same offset local hasOffset = false local dx, dy, cx, cy for idx, aUnit in pairs(units) do cx = aUnit.x cy = aUnit.y if not hasOffset then local nx, ny = land.getClosestPointOnRoads("roads", cx, cy) dx = nx - cx dy = ny - cy hasOffset = true end aUnit.x = cx + dx aUnit.y = cy + dy end else local iterCount = 0 local otherLocs = {} -- resolved locs for idx, aUnit in pairs(units) do local cx = aUnit.x local cy = aUnit.y -- we now iterate until there is enough separation or too many iters local tooClose local np, nx, ny repeat nx, ny = land.getClosestPointOnRoads("roads", cx, cy) -- compare this with all other locs np = {x=nx, y=ny} tooClose = false for idc, op in pairs(otherLocs) do local d = dcsCommon.dist(np, op) if d < cloneZones.minSep then tooClose = true cx = cx + cloneZones.minSep cy = cy + cloneZones.minSep iterCount = iterCount + 1 end end until (iterCount > cloneZones.maxIter) or (not tooClose) table.insert(otherLocs, np) aUnit.x = nx aUnit.y = ny end end -- else centerOnly end -- apply turning dcsCommon.rotateGroupData(rawData, spawnZone.turn + 57.2958 *dHeading, newCenter.x, newCenter.z) -- make sure unit and group names are unique unless -- we have identical active if not spawnZone.identical then cloneZones.uniqueNameGroupData(rawData, spawnZone, theZone.name) end -- see what country we spawn for ctry = cloneZones.resolveOwnership(spawnZone, ctry) rawData.CZctry = ctry -- save ctry -- set AI on or off rawData.useAI = spawnZone.useAI table.insert(dataToSpawn, rawData) end -- now resolve references to other cloned units for all raw data -- we must do this BEFORE we spawn cloneZones.resolveReferences(theZone, dataToSpawn) -- now spawn all raw data spawnZone.oSize = 0 -- original size reset local groupCollector = {} -- to detect cross-group conflicts local unitCollector = {} -- to detect cross-group conflicts local theGroup = nil -- init to empty, on this level for idx, rawData in pairs (dataToSpawn) do -- now spawn and save to clones -- first norm and clone data for later save rawData.cty = rawData.CZctry rawData.cat = rawData.CZtheCat -- make group, unit[1] and route point [1] all match up if rawData.route and rawData.units[1] then rawData.route.points[1].x = rawData.units[1].x rawData.route.points[1].y = rawData.units[1].y rawData.x = rawData.units[1].x rawData.y = rawData.units[1].y end -- clone for persistence local theData = dcsCommon.clone(rawData) -- remember the zone that spawned this particular group theData.CZspawner = spawnZone.name cloneZones.allClones[rawData.name] = theData if cloneZones.verbose or spawnZone.verbose then -- optional spawn validation report before we spawn cloneZones.validateSpawnGroupData(rawData, spawnZone, groupCollector, unitCollector) end -- SPAWN NOW!!!! theGroup = coalition.addGroup(rawData.CZctry, rawData.CZtheCat, rawData) table.insert(spawnedGroups, theGroup) -- increment oSize by number of spawns spawnZone.oSize = spawnZone.oSize + theGroup:getSize() -- see if this is an auto-despawner if spawnZone.despawnInMin then local now = timer.getTime() local timeLimit = dcsCommon.randomBetween(spawnZone.despawnInMin, spawnZone.despawnInMax) local info = {} info.theGroup = theGroup info.name = theData.name info.isObject = false info.timeLimit = now + timeLimit info.cloneZone = spawnZone table.insert(cloneZones.despawnPlan, info) end -- turn off AI if disabled if not rawData.useAI then cloneZones.turnOffAI({theGroup}) end -- update groupXlate table from spawned group -- so we can later reference them with other clones local newGroupID = theGroup:getID() -- new ID assigned by DCS local origID = rawData.CZorigID -- before we materialized cloneZones.groupXlate[origID] = newGroupID -- now also save all units for references -- and verify assigned vs target ID for idx, aUnit in pairs(rawData.units) do -- access the proposed name local uName = aUnit.name local gUnit = Unit.getByName(uName) if gUnit then -- unit exists. compare planned and assigned ID local uID = tonumber(gUnit:getID()) if uID == aUnit.CZTargetID then -- all good else -- mismatch. may happen when namingScheme causes -- unit to be reallocated to existing unit. if spawnZone.verbose then if spawnZone.nameScheme then trigger.action.outText("clnZ: nameScheme - unit <" .. uName .. ">: ÎD mapped to existing: " .. uID , 30) else trigger.action.outText("clnZ: post-clone verification failed for unit <" .. uName .. ">: ÎD mismatch: " .. uID .. " -- " .. aUnit.CZTargetID, 30) end end end cloneZones.unitXlate[aUnit.CZorigID] = uID else if spawnZone.verbose then trigger.action.outText("clnZ: post-clone verifiaction failed for unit <" .. uName .. ">: not found", 30) end end end -- check if our assigned ID matches the one handed out by -- DCS. Mismatches can happen, and are only noted if newGroupID == rawData.CZTargetID then -- we are good, all processing correct -- add to delicates if set if spawnZone.delicateName and delicates then -- pass this object to the delicate zone mentioned local theDeli = delicates.getDelicatesByName(spawnZone.delicateName) if theDeli then delicates.addGroupToInventoryForZone(theDeli, theGroup) else trigger.action.outText("+++clnZ: spawner <" .. spawnZone.name .. "> can't find delicates zone <" .. spawnZone.delicateName .. ">", 30) end end else if cloneZones.verbose or spawnZone.verbose then trigger.action.outText("clnZ: Note: GROUP ID spawn changed for <" .. rawData.name .. ">: target ID " .. rawData.CZTargetID .. " (target) returns " .. newGroupID .. " (actual) in <" .. spawnZone.name .. ">", 30) end if cloneZones.respawnOnGroupID then -- remember pos in table, will be changed after -- respawn local pos = #spawnedGroups timer.scheduleFunction(cloneZones.forcedRespawn, {theData, spawnedGroups, pos, spawnZone}, timer.getTime() + 2) -- initial gap: 2 seconds for DCS to sort itself out -- note that this can in extreme cases result in -- unitID mismatches, but his is extremely unlikely else -- we note it in the spawn data for the group so -- persistence works fine theData.groupId = newGroupID -- since we keep these, we make them brittle if required if spawnZone.delicateName and delicates then -- pass this object to the delicate zone mentioned local theDeli = delicates.getDelicatesByName(spawnZone.delicateName) if theDeli then delicates.addGroupToInventoryForZone(theDeli, theGroup) else trigger.action.outText("+++clnZ: spawner <" .. spawnZone.name .. "> can't find delicates zone <" .. spawnZone.delicateName .. ">", 30) end end end end cloneZones.invokeCallbacks(spawnZone, "did spawn group", theGroup) -- interface to groupTracker if spawnZone.trackWith then cloneZones.handoffTracking(theGroup, spawnZone) end end -- static spawns for idx, aStaticName in pairs(theZone.staticNames) do local rawData, cat, ctry, parent = cfxMX.getStaticFromDCSbyName(aStaticName) -- returns a UNIT data block if not rawData then trigger.action.outText("Static Clone: no such group <"..aStaticName .. ">", 30) elseif rawData.name == aStaticName then -- all good else trigger.action.outText("Static Clone: FAILED name check for <" .. aStaticName .. ">", 30) end local origID = rawData.unitId -- save original unit ID rawData.CZorigID = origID rawData.x = rawData.x + zoneDelta.x rawData.y = rawData.y + zoneDelta.z -- !!! -- randomize if enabled if spawnZone.rndLoc then local loc, dx, dy if spawnZone.onPerimeter then loc, dx, dy = spawnZone:createRandomPointOnZoneBoundary() elseif spawnZone.inBuiltup then loc, dx, dy = spawnZone:createRandomPointInPopulatedZone(spawnZone.inBuiltup) else loc, dx, dy = spawnZone:createRandomPointInZone() -- also supports polygonal zones end rawData.x = rawData.x + dx -- might want to use loc rawData.y = rawData.y + dy -- directly end if spawnZone.rndHeading then local phi = 6.2831 * math.random() -- that's 2Pi, folx rawData.heading = phi end if spawnZone.onRoad then local cx = rawData.x local cy = rawData.y local nx, ny = land.getClosestPointOnRoads("roads", cx, cy) rawData.x = nx rawData.y = ny end -- apply turning dcsCommon.rotateUnitData(rawData, spawnZone.turn + 57.2958 * dHeading, newCenter.x, newCenter.z) if not spawnZone.identical then -- make sure static name is unique and remember original cloneZones.uniqueNameStaticData(rawData, spawnZone, theZone.name) rawData.unitId = cloneZones.uniqueID() end rawData.CZTargetID = rawData.unitId -- see what country we spawn for ctry = cloneZones.resolveOwnership(spawnZone, ctry) -- handle linkUnit if provided if false and rawData.linkUnit then local lU = cloneZones.resolveStaticLinkUnit(rawData.linkUnit) rawData.linkUnit = lU if not rawData.offsets then rawData.offsets = {} rawData.offsets.angle = 0 rawData.offsets.x = 0 rawData.offsets.y = 0 end rawData.offsets.y = rawData.offsets.y - zoneDelta.z rawData.offsets.x = rawData.offsets.x - zoneDelta.x rawData.offsets.angle = rawData.offsets.angle + spawnZone.turn rawData.linkOffset = true end local isCargo = rawData.canCargo rawData.cty = ctry -- save for persistence local theData = dcsCommon.clone(rawData) theData.CZspawner = spawnZone.name -- remember spawner cloneZones.allCObjects[rawData.name] = theData if cloneZones.verbose or spawnZone.verbose then -- optional spawn validation report before we spawn cloneZones.validateSpawnUnitData(rawData, spawnZone, unitCollector) end local theStatic = coalition.addStaticObject(ctry, rawData) local newStaticID = tonumber(theStatic:getID()) table.insert(spawnedStatics, theStatic) if spawnZone.despawnInMin then local now = timer.getTime() local timeLimit = dcsCommon.randomBetween(spawnZone.despawnInMin, spawnZone.despawnInMax) local info = {} info.theGroup = theStatic info.name = theData.name info.isObject = true info.timeLimit = now + timeLimit info.cloneZone = spawnZone table.insert(cloneZones.despawnPlan, info) end -- we don't mix groups with units, so no lookup tables for -- statics if newStaticID == rawData.CZTargetID then else if cloneZones.verbose or spawnZone.verbose then trigger.action.outText("Static ID mismatch: " .. newStaticID .. " vs (target) " .. rawData.CZTargetID .. " for " .. rawData.name, 30) end end cloneZones.unitXlate[origID] = newStaticID -- same as units cloneZones.invokeCallbacks(theZone, "did spawn static", theStatic) if cloneZones.verbose or spawnZone.verbose then trigger.action.outText("+++clnZ: new Static clone " .. aStaticName, 30) end -- processing for delicates if spawnZone.delicateName and delicates then -- pass this object to the delicate zone mentioned local theDeli = delicates.getDelicatesByName(spawnZone.delicateName) if theDeli then delicates.addStaticObjectToInventoryForZone(theDeli, theStatic) else trigger.action.outText("+++cnlZ: cloner <" .. aZone.name .. "> can't find delicates <" .. spawnZone.delicateName .. ">", 30) end end -- processing for cargoManager if isCargo then if cfxCargoManager then cfxCargoManager.addCargo(theStatic) if cloneZones.verbose or spawnZone.verbose then trigger.action.outText("+++clnZ: added CARGO " .. theStatic:getName() .. " to cargo manager ", 30) end else if cloneZones.verbose or spawnZone.verbose then trigger.action.outText("+++clnZ: CARGO " .. theStatic:getName() .. " detected, not managerd", 30) end end end end -- reset lastSize to oSize spawnZone.lastSize = spawnZone.oSize local args = {} args.groups = spawnedGroups args.statics = spawnedStatics cloneZones.invokeCallbacks(theZone, "spawned", args) return spawnedGroups, spawnedStatics end function cloneZones.turnOffAI(args) local theGroup = args[1] local theController = theGroup:getController() theController:setOnOff(false) end -- retro-fit for helo troops and others to provide 'requestable' support function cloneZones.spawnWithSpawner(theZone) -- analog to cfxSpawnZones.spawnWithSpawner(theSpawner) -- glue code for helo troops and other modules -- we may want to check if cloner isn't emtpy first cloneZones.spawnWithCloner(theZone) end function cloneZones.spawnWithCloner(theZone) if not theZone then trigger.action.outText("+++clnZ: nil zone on spawnWithCloner", 30) return end if not theZone.cloner then trigger.action.outText("+++clnZ: spawnWithCloner invoked with non-cloner <" .. theZone.name .. ">", 30) return end -- see if we are on cooldown. If so, exit if theZone.cooldown > 0 then local now = timer.getTime() if now < theZone.lastSpawnTimeStamp + theZone.cooldown then if theZone.verbose or cloneZones.verbose then trigger.action.outText("+++clnZ: cloner <" .. theZone.name .. "> still on cool-down, no clone cycle", 30) end return else theZone.lastSpawnTimeStamp = now end end -- see if we have spawned enough if theZone.maxCycles then if theZone.maxCycles > 0 then -- update and spawn theZone.maxCycles = theZone.maxCycles - 1 else -- no spawn if theZone.verbose then trigger.action.outText("+++clnZ: cloner <" .. theZone.name .. "> out of cycles. No clone cycle", 30) end return -- no spawning, out of cycles end end -- force spawn with this spawner local templateZone = theZone if theZone.source then -- we use a different zone for templates -- source can be a comma separated list local templateName = theZone.source if dcsCommon.containsString(templateName, ",") then local allNames = templateName local templates = dcsCommon.splitString(templateName, ",") templateName = dcsCommon.pickRandom(templates) templateName = dcsCommon.trim(templateName) if cloneZones.verbose or theZone.verbose then trigger.action.outText("+++clnZ: picked random template <" .. templateName .."> for from <" .. allNames .. "> for cloner " .. theZone.name, 30) end end if cloneZones.verbose or theZone.verbose then trigger.action.outText("+++clnZ: spawning - picked <" .. templateName .. "> as template", 30) end local newTemplate = cloneZones.getCloneZoneByName(templateName) if not newTemplate then if cloneZones.verbose or theZone.verbose then trigger.action.outText("+++clnZ: no clone source with name <" .. templateName .."> for cloner " .. theZone.name, 30) end return end templateZone = newTemplate end -- make sure our template is filled if not templateZone.cloneNames then if cloneZones.verbose then trigger.action.outText("+++clnZ: clone source template <".. templateZone.name .. "> for clone zone <" .. theZone.name .."> is empty", 30) end return end -- pre-Wipe? local didPrewipe = false if theZone.preWipe then cloneZones.despawnAll(theZone) cloneZones.invokeCallbacks(theZone, "wiped", {}) didPrewipe = true end -- declutter? if theZone.declutter then theZone:declutterZone() if theZone.verbose then trigger.action.outText("+++clnZ: cloner <" .. theZone.name .. "> declutter complete.", 30) end end local args = {theZone = theZone, templateZone = templateZone} if didPrewipe then -- delay spawning to allow revoval to take place timer.scheduleFunction(cloneZones.doClone, args, timer.getTime() + 0.5) else -- can do immediately cloneZones.doClone(args) end end -- deferrable clone method. function cloneZones.doClone(args) local theZone = args.theZone local templateZone = args.templateZone local theClones, theStatics = cloneZones.spawnWithTemplateForZone(templateZone, theZone) -- reset hasClones so we know our spawns are full and we can -- detect complete destruction if (theClones and #theClones > 0) or (theStatics and #theStatics > 0) then theZone.hasClones = true theZone.mySpawns = theClones theZone.myStatics = theStatics else theZone.hasClones = false theZone.mySpawns = {} theZone.myStatics = {} end end function cloneZones.countLiveAIUnits(theZone) -- like countLiveUnits, but disregards statics if not theZone then return 0 end local count = 0 if not theZone.mySpawns then return 0 end -- count units if theZone.mySpawns then for idx, aGroup in pairs(theZone.mySpawns) do if Group.isExist(aGroup) then count = count + aGroup:getSize() end end end return count end function cloneZones.countLiveUnits(theZone) if not theZone then return 0 end local count = 0 -- count units if theZone.mySpawns then for idx, aGroup in pairs(theZone.mySpawns) do if Group.isExist(aGroup) then --aGroup:isExist() then count = count + aGroup:getSize() end end end -- count statics if theZone.myStatics then for idx, aStatic in pairs(theZone.myStatics) do if aStatic:isExist() and aStatic:getLife() >= 1 then count = count + 1 end end end return count end function cloneZones.hasLiveUnits(theZone) if not theZone then return 0 end if theZone.mySpawns then for idx, aGroup in pairs(theZone.mySpawns) do if aGroup:isExist() then local uNum = aGroup:getSize() if uNum > 0 then return true end end end end if theZone.myStatics then for idx, aStatic in pairs(theZone.myStatics) do if aStatic:isExist() and aStatic.getLife() >= 1 then return true end end end return false end function cloneZones.resolveOwningCoalition(theZone) return theZone:getCoalition() end function cloneZones.getRequestableClonersInRange(aPoint, aRange, aSide) if not aSide then aSide = 0 end if not aRange then aRange = 200 end if not aPoint then return {} end local theSpawners = {} for idx, aZone in pairs(cloneZones.cloners) do -- iterate all zones and collect those that match local hasMatch = true local delta = dcsCommon.distFlat(aPoint, aZone:getPoint()) if delta > aRange then hasMatch = false end if aSide ~= 0 then -- check if side is correct for owned zone local resolved = cloneZones.resolveOwningCoalition(aZone) if resolved == 0 or resolved ~= aSide then -- failed ownership test. must match and not be zero hasMatch = false end end if not aZone.requestable then hasMatch = false end if hasMatch then table.insert(theSpawners, aZone) end end return theSpawners end -- -- UPDATE -- function cloneZones.update() timer.scheduleFunction(cloneZones.update, {}, timer.getTime() + 1) for idx, aZone in pairs(cloneZones.cloners) do -- see if deSpawn was pulled. Must run before spawn if aZone.deSpawnFlag then local currTriggerVal = aZone:getFlagValue(aZone.deSpawnFlag) if currTriggerVal ~= aZone.lastDeSpawnValue then if cloneZones.verbose or aZone.verbose then trigger.action.outText("+++clnZ: DEspawn triggered for <" .. aZone.name .. ">", 30) end cloneZones.despawnAll(aZone) aZone.lastDeSpawnValue = currTriggerVal end end -- see if we got spawn? command local willSpawn = false -- init to false. if aZone:testZoneFlag(aZone.spawnFlag, aZone.cloneTriggerMethod, "lastSpawnValue") then if cloneZones.verbose or aZone.verbose then trigger.action.outText("+++clnZ: spawn triggered for <" .. aZone.name .. "> on flag <" .. aZone.spawnFlag .. ">", 30) end cloneZones.spawnWithCloner(aZone) willSpawn = true -- in case prewipe, we delay -- can mess with empty, so we tell empty to skip end -- handling of damaged! and #health if aZone.hasClones and (aZone.damaged or aZone.health) then -- calculate current health local currSize = cloneZones.countLiveAIUnits(aZone) if not aZone.oSize then aZone.oSize = 0 end if aZone.oSize < 1 then if aZone.verbose or cloneZones.verbose then trigger.action.outText("+++clnZ: Warning: zero oZize for cloner <" .. aZone.name .. ">, no health info, no damage alert", 30) end else local percent = math.floor(currSize * 100 / aZone.oSize) if aZone.health then aZone:setFlagValue(aZone.health, percent) end if aZone.lastSize > currSize then if aZone.damaged then aZone:pollFlag(aZone.damaged, aZone.cloneMethod) end end end aZone.lastSize = currSize end -- empty handling local isEmpty = cloneZones.countLiveUnits(aZone) < 1 and aZone.hasClones if isEmpty and (willSpawn == false) then -- see if we need to bang a flag if aZone.emptyBangFlag then aZone:pollFlag(aZone.emptyBangFlag, aZone.cloneMethod) if cloneZones.verbose or aZone.verbose then trigger.action.outText("+++clnZ: bang! on " .. aZone.emptyBangFlag, 30) end end -- invoke callbacks cloneZones.invokeCallbacks(aZone, "empty", {}) -- prevent isEmpty next pass aZone.hasClones = false end end -- now remove all scheduled despawns local now = timer.getTime() local filtered = {} for idx, theInfo in pairs(cloneZones.despawnPlan) do if theInfo.timeLimit < now then if theInfo.isObject then -- dealloc static object local theObject = theInfo.theGroup if theObject and StaticObject.isExist(theObject) then StaticObject.destroy(theObject) end else local theGroup = theInfo.theGroup if theGroup and Group.isExist(theGroup) then Group.destroy(theGroup) end end else table.insert(filtered, theInfo) end end cloneZones.despawnPlan = filtered end function cloneZones.doOnStart() for idx, theZone in pairs(cloneZones.cloners) do if theZone.onStart then if theZone.isStarted then if cloneZones.verbose or theZone.verbose then trigger.action.outText("+++clnZ: onStart pre-empted for <" .. theZone.name .. "> by persistence", 30) end else if cloneZones.verbose or theZone.verbose then trigger.action.outText("+++clnZ: onStart spawing for <"..theZone.name .. ">", 30) end cloneZones.spawnWithCloner(theZone) end end end end -- -- Regular GC and housekeeping -- function cloneZones.GC() -- GC run. remove all my dead remembered troops local filteredAttackers = {} for gName, gData in pairs (cloneZones.allClones) do -- all we need to do is get the group of that name -- and if it still returns units we are fine local gameGroup = Group.getByName(gName) if gameGroup and gameGroup:isExist() and gameGroup:getSize() > 0 then -- we now filter for categories. we currently only let -- ground units pass -- better make this configurabele by option later if gData.cat == 0 and false then -- block aircraft elseif gData.cat == 1 and false then -- block helos elseif gData.cat == 2 and false then -- block ground elseif gData.cat == 3 and false then -- block ship elseif gData.cat == 4 and false then -- block trains else -- not filtered, persist filteredAttackers[gName] = gData end end end cloneZones.allClones = filteredAttackers filteredAttackers = {} for gName, gData in pairs (cloneZones.allCObjects) do -- all we need to do is get the group of that name -- and if it still returns units we are fine local theObject = StaticObject.getByName(gName) if theObject and theObject:isExist() then filteredAttackers[gName] = gData if theObject:getLife() < 1 then gData.dead = true end end end cloneZones.allCObjects = filteredAttackers end function cloneZones.houseKeeping() timer.scheduleFunction(cloneZones.houseKeeping, {}, timer.getTime() + 5 * 60) -- every 5 minutes cloneZones.GC() end -- -- LOAD / SAVE -- function cloneZones.synchGroupMXData(theData) -- we iterate the group's units one by one and update them local newUnits = {} local allUnits = theData.units for idx, unitData in pairs(allUnits) do local uName = unitData.name local gUnit = Unit.getByName(uName) if gUnit and gUnit:isExist() then unitData.heading = dcsCommon.getUnitHeading(gUnit) pos = gUnit:getPoint() unitData.x = pos.x unitData.y = pos.z -- (!!) -- add aircraft handling here (alt, speed etc) -- perhaps even curtail route table.insert(newUnits, unitData) end end theData.units = newUnits end function cloneZones.synchMXObjData(theData) local oName = theData.name local theObject = StaticObject.getByName(oName) theData.heading = dcsCommon.getUnitHeading(theObject) pos = theObject:getPoint() theData.x = pos.x theData.y = pos.z -- (!!) theData.isDead = theObject:getLife() < 1 theData.dead = theData.isDead end function cloneZones.saveData() local theData = {} local allCloneData = {} local allSOData = {} -- run a GC pre-emptively cloneZones.GC() -- now simply iterate and save all deployed clones for gName, gData in pairs(cloneZones.allClones) do local sData = dcsCommon.clone(gData) cloneZones.synchGroupMXData(sData) allCloneData[gName] = sData end -- now simply iterate and save all deployed clones for gName, gData in pairs(cloneZones.allCObjects) do local sData = dcsCommon.clone(gData) cloneZones.synchMXObjData(sData) allSOData[gName] = sData end -- now save all cloner stati local cloners = {} for idx, theCloner in pairs(cloneZones.cloners) do local cData = {} local cName = theCloner.name cData.myUniqueCounter = theCloner.myUniqueCounter if theCloner.maxCycles then cData.maxCycles = theCloner.maxCycles end cData.oSize = theCloner.oSize cData.lastSize = theCloner.lastSize -- mySpawns: all groups I'm curently observing for empty! -- myStatics: dto for objects local mySpawns = {} for idx, aGroup in pairs(theCloner.mySpawns) do if aGroup and aGroup:isExist() and aGroup:getSize() > 0 then table.insert(mySpawns, aGroup:getName()) end end cData.mySpawns = mySpawns local myStatics = {} for idx, aStatic in pairs(theCloner.myStatics) do table.insert(myStatics, aStatic:getName()) end cData.myStatics = myStatics cData.isStarted = theCloner.isStarted -- to prevent onStart cloners[cName] = cData end -- save globals theData.cuid = cloneZones.uniqueCounter -- replace whatever is larger theData.uuid = dcsCommon.simpleUUID -- replace whatever is larger theData.globalCount = cloneZones.globalCount -- save to struct and pass back theData.clones = allCloneData theData.objects = allSOData theData.cloneZones = cloners return theData end function cloneZones.loadData() if not persistence then return end local theData = persistence.getSavedDataForModule("cloneZones") if not theData then if cloneZones.verbose then trigger.action.outText("+++clnZ: no save date received, skipping.", 30) end return end local gSpawn = 0 local uSpawn = 0 -- spawn all units local allClones = theData.clones for gName, gData in pairs (allClones) do local cty = gData.cty local cat = gData.cat -- now spawn, but first -- add to my own deployed queue so we can save later local gdClone = dcsCommon.clone(gData) cloneZones.allClones[gName] = gdClone local theGroup = coalition.addGroup(cty, cat, gData) uSpawn = uSpawn + #theGroup:getUnits() gSpawn = gSpawn + 1 -- turn off AI if disabled if not gData.useAI then cloneZones.turnOffAI({theGroup}) end end if cloneZones.verbose then trigger.action.outText("load: loaded <" .. gSpawn .. "> groups, <" .. uSpawn .. "> units total.", 30) end -- spawn all static objects local allObjects = theData.objects for oName, oData in pairs(allObjects) do local newStatic = dcsCommon.clone(oData) -- add link info if it exists newStatic.linkUnit = cfxMX.linkByName[oName] if newStatic.linkUnit and cloneZones.verbose then trigger.action.outText("+++clnZ: linked static <" .. oName .. "> to unit <" .. newStatic.linkUnit .. ">", 30) end local cty = newStatic.cty -- spawn new one, replacing same.named old, dead if required gStatic = coalition.addStaticObject(cty, newStatic) -- processing for cargoManager if oData.canCargo then if cfxCargoManager then cfxCargoManager.addCargo(gStatic) end end -- add the original data block to be remembered -- for next save cloneZones.allCObjects[oName] = oData end -- now update all spawners and reconnect them with their spawns local allCloners = theData.cloneZones for cName, cData in pairs(allCloners) do local theCloner = cloneZones.getCloneZoneByName(cName) if theCloner then theCloner.isStarted = true -- init myUniqueCounter if it exists if cData.myUniqueCounter then theCloner.myUniqueCounter = cData.myUniqueCounter end if cData.oSize then theCloner.oSize = cData.oSize end if cData.lastSize then theCloner.lastSize = cData.lastSize end if cData.maxCycles then theCloner.maxCycles = cData.maxCycles end local mySpawns = {} for idx, aName in pairs(cData.mySpawns) do local theGroup = Group.getByName(aName) if theGroup then table.insert(mySpawns, theGroup) theCloner.hasClones = true -- was missing! else trigger.action.outText("+++clnZ - persistence: can't reconnect cloner <" .. cName .. "> with clone group <".. aName .. ">", 30) end end theCloner.mySpawns = mySpawns local myStatics = {} for idx, aName in pairs(cData.myStatics) do local theStatic = StaticObject.getByName(aName) if theStatic then table.insert(myStatics, theStatic) theCloner.hasClones = true -- was missing!S else trigger.action.outText("+++clnZ - persistence: can't reconnect cloner <" .. cName .. "> with static <".. aName .. ">", 30) end end theCloner.myStatics = myStatics else trigger.action.outText("+++clnZ - persistence: cannot synch cloner <" .. cName .. ">, does not exist", 30) end end -- finally, synch uid and uuid if theData.cuid and theData.cuid > cloneZones.uniqueCounter then cloneZones.uniqueCounter = theData.cuid end if theData.uuiD and theData.uuid > dcsCommon.simpleUUID then dcsCommon.simpleUUID = theData.uuid end if theData.globalCount and theData.globalCount > cloneZones.globalCount then cloneZones.globalCount = theData.globalCount end end -- -- START -- function cloneZones.readConfigZone() local theZone = cfxZones.getZoneByName("cloneZonesConfig") if not theZone then if cloneZones.verbose then trigger.action.outText("+++clnZ: NO config zone!", 30) end theZone = cfxZones.createSimpleZone("cloneZonesConfig") end if theZone:hasProperty("uniqueCount") then cloneZones.uniqueCounter = theZone:getNumberFromZoneProperty("uniqueCount", cloneZone.uniqueCounter) end if theZone:hasProperty("localCount") then cloneZones.lclUniqueCounter = theZone:getNumberFromZoneProperty("localCount", cloneZone.lclUniqueCounter) end if theZone:hasProperty("globalCount") then cloneZones.globalCounter = theZone:getNumberFromZoneProperty("globalCount", cloneZone.globalCounter) end cloneZones.verbose = theZone.verbose end function cloneZones.start() -- lib check if not dcsCommon.libCheck then trigger.action.outText("cfx Clone Zones requires dcsCommon", 30) return false end if not dcsCommon.libCheck("cfx Clone Zones", cloneZones.requiredLibs) then return false end -- read config cloneZones.readConfigZone() -- process cloner Zones local attrZones = cfxZones.getZonesWithAttributeNamed("cloner") for k, aZone in pairs(attrZones) do cloneZones.createClonerWithZone(aZone) -- process attribute and add to zone cloneZones.addCloneZone(aZone) end -- update all cloners and spawned clones from file if persistence then -- sign up for persistence callbacks = {} callbacks.persistData = cloneZones.saveData persistence.registerModule("cloneZones", callbacks) -- now load my data cloneZones.loadData() end -- schedule onStart, and leave at least a few -- cycles to go through object removal -- persistencey has loaded isStarted if a cloner was -- already started timer.scheduleFunction(cloneZones.doOnStart, {}, timer.getTime() + 1.0) -- start update cloneZones.update() -- start housekeeping cloneZones.houseKeeping() trigger.action.outText("cfx Clone Zones v" .. cloneZones.version .. " started.", 30) return true end -- let's go! if not cloneZones.start() then trigger.action.outText("cf/x Clone Zones aborted: missing libraries", 30) cloneZones = nil end --[[-- to resolve tasks - AFAC - FAC Assign group - set freq for unit nameTest - optional safety / debug feature that will name-test each unit that is about to be spawned for replacement. Maybe auto turn on when verbose is set? make example where transport can be different plane types but have same name support 'orders' to complete replace routes, and pass to groundCommander like spawner. only for ground troops maxCycles - maximum number of clone cycles. emulate with countdown? --]]--