cloneZones = {} cloneZones.version = "1.6.1" 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.allClones = {} -- all clones spawned, regularly GC'd cloneZones.allCObjects = {} -- all clones objects --[[-- Clones Groups from ME mission data Copyright (c) 2022 by Christian Franz and cf/x AG Version History 1.0.0 - initial version 1.0.1 - preWipe attribute 1.1.0 - support for static objects - despawn? attribute 1.1.1 - despawnAll: isExist guard - map in? to f? 1.2.0 - Lua API integration: callbacks - groupXlate struct - unitXlate struct - resolveReferences - getGroupsInZone rewritten for data - static resolve - linkUnit resolve - clone? synonym - empty! and method attributes 1.3.0 - DML flag upgrade 1.3.1 - groupTracker interface - trackWith: attribute 1.4.0 - Watchflags 1.4.1 - trackWith: accepts list of trackers 1.4.2 - onstart delays for 0.1 s to prevent static stacking - turn bug for statics (bug in dcsCommon, resolved) 1.4.3 - embark/disembark now works with cloners 1.4.4 - removed some debugging verbosity 1.4.5 - randomizeLoc, rndLoc keyword - cargo manager integration - pass cargo objects when present 1.4.6 - removed some verbosity for spawned aircraft with airfields on their routes 1.4.7 - DML watchflag and DML Flag polish, method-->cloneMethod 1.4.8 - added 'wipe?' synonym 1.4.9 - onRoad option - rndHeading option 1.5.0 - persistence 1.5.1 - fixed static data cloning bug (load & save) 1.5.2 - fixed bug in trackWith: referencing wrong cloner 1.5.3 - centerOnly/wholeGroups attribute for rndLoc, rndHeading and onRoad 1.5.4 - parking for aircraft processing when cloning from template 1.5.5 - removed some verbosity 1.6.0 - fixed issues with cloning for zones with linked units - cloning with useHeading - major declutter 1.6.1 - removed some verbosity when not rotating routes - updateTaskLocations () - cloning groups now also adjusts tasks like search and engage in zone - cloning with rndLoc supports polygons - corrected rndLoc without centerOnly to not include individual offsets - ensure support of recovery tanker resolve cloned group --]]-- -- -- 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 -- reasons for callback -- "will despawn group" - args is the group about to be despawned -- "did spawn group" -- args is group that was spawned -- "will despawn static" -- "did spawn static" -- "spawned" -- completed spawn cycle. args contains .groups and .statics spawned -- "empty" -- all spawns have been killed, args is empty -- "wiped" -- preWipe executed -- "", 30) end local localZones = cloneZones.allGroupsInZoneByData(theZone) local localObjects = cfxZones.allStaticsInZone(theZone, 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 = {} -- update: getPoint is bad if it's a moving zone. -- use getDCSOrigin instead theZone.origin = cfxZones.getDCSOrigin(theZone) -- 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 cfxZones.hasProperty(theZone, "source") then theZone.source = cfxZones.getStringFromZoneProperty(theZone, "source", "") if theZone.source == "" then theZone.source = nil end end 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) 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 -- watchflags theZone.cloneTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "triggerMethod", "change") if cfxZones.hasProperty(theZone, "cloneTriggerMethod") then theZone.cloneTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "cloneTriggerMethod", "change") end -- f? and spawn? and other synonyms map to the same if cfxZones.hasProperty(theZone, "f?") then theZone.spawnFlag = cfxZones.getStringFromZoneProperty(theZone, "f?", "none") end if cfxZones.hasProperty(theZone, "in?") then theZone.spawnFlag = cfxZones.getStringFromZoneProperty(theZone, "in?", "none") end if cfxZones.hasProperty(theZone, "spawn?") then theZone.spawnFlag = cfxZones.getStringFromZoneProperty(theZone, "spawn?", "none") end if cfxZones.hasProperty(theZone, "clone?") then theZone.spawnFlag = cfxZones.getStringFromZoneProperty(theZone, "clone?", "none") end if theZone.spawnFlag then theZone.lastSpawnValue = cfxZones.getFlagValue(theZone.spawnFlag, theZone) end -- deSpawn? if cfxZones.hasProperty(theZone, "deSpawn?") then theZone.deSpawnFlag = cfxZones.getStringFromZoneProperty(theZone, "deSpawn?", "none") end if cfxZones.hasProperty(theZone, "deClone?") then theZone.deSpawnFlag = cfxZones.getStringFromZoneProperty(theZone, "deClone?", "none") end if cfxZones.hasProperty(theZone, "wipe?") then theZone.deSpawnFlag = cfxZones.getStringFromZoneProperty(theZone, "wipe?", "none") end if theZone.deSpawnFlag then theZone.lastDeSpawnValue = cfxZones.getFlagValue(theZone.deSpawnFlag, theZone) end theZone.onStart = cfxZones.getBoolFromZoneProperty(theZone, "onStart", false) theZone.moveRoute = cfxZones.getBoolFromZoneProperty(theZone, "moveRoute", false) theZone.preWipe = cfxZones.getBoolFromZoneProperty(theZone, "preWipe", false) -- to be deprecated if cfxZones.hasProperty(theZone, "empty+1") then theZone.emptyFlag = cfxZones.getStringFromZoneProperty(theZone, "empty+1", "") -- note string on number default end if cfxZones.hasProperty(theZone, "empty!") then theZone.emptyBangFlag = cfxZones.getStringFromZoneProperty(theZone, "empty!", "") -- note string on number default end theZone.cloneMethod = cfxZones.getStringFromZoneProperty(theZone, "cloneMethod", "inc") if cfxZones.hasProperty(theZone, "method") then theZone.cloneMethod = cfxZones.getStringFromZoneProperty(theZone, "method", "inc") -- note string on number default end if cfxZones.hasProperty(theZone, "masterOwner") then theZone.masterOwner = cfxZones.getStringFromZoneProperty(theZone, "masterOwner", "") end theZone.turn = cfxZones.getNumberFromZoneProperty(theZone, "turn", 0) -- interface to groupTracker if cfxZones.hasProperty(theZone, "trackWith:") then theZone.trackWith = cfxZones.getStringFromZoneProperty(theZone, "trackWith:", "") --trigger.action.outText("trackwith: " .. theZone.trackWith, 30) end -- randomized locations on spawn theZone.rndLoc = cfxZones.getBoolFromZoneProperty(theZone, "randomizedLoc", false) if cfxZones.hasProperty(theZone, "rndLoc") then theZone.rndLoc = cfxZones.getBoolFromZoneProperty(theZone, "rndLoc", false) end theZone.centerOnly = cfxZones.getBoolFromZoneProperty(theZone, "centerOnly", false) if cfxZones.hasProperty(theZone, "wholeGroups") then theZone.centerOnly = cfxZones.getBoolFromZoneProperty(theZone, "wholeGroups", false) end theZone.rndHeading = cfxZones.getBoolFromZoneProperty(theZone, "rndHeading", false) theZone.onRoad = cfxZones.getBoolFromZoneProperty(theZone, "onRoad", false) -- we end with clear plate 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 for idx, aGroup in pairs(theZone.mySpawns) do --trigger.action.outText("++clnZ: despawn all " .. aGroup.name, 30) 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.uniqueID() local uid = cloneZones.uniqueCounter cloneZones.uniqueCounter = cloneZones.uniqueCounter + 1 return uid end function cloneZones.uniqueNameGroupData(theData) theData.name = dcsCommon.uuid(theData.name) local units = theData.units for idx, aUnit in pairs(units) do aUnit.name = dcsCommon.uuid(aUnit.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.resolveOwnership(spawnZone, ctry) if not spawnZone.masterOwner then return ctry end local masterZone = cfxZones.getZoneByName(spawnZone.masterOwner) if not masterZone then trigger.action.outText("+++clnZ: cloner " .. spawnZone.name .. " could not find master owner <" .. spawnZone.masterOwner .. ">", 30) return ctry end if not masterZone.owner then return ctry end ctry = dcsCommon.getACountryForCoalition(masterZone.owner) 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) --trigger.action.outText("+++clnZ: resolved embark group id <" .. gID .. "> to <" .. resolvedID .. ">", 30) 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) --trigger.action.outText("+++clnZ: resolved distribute unit/group id <" .. aUnit .. "/" .. gID .. "> to <".. newUnit .. "/" .. resolvedID .. ">", 30) end -- store this as new group for -- translated transportID newDist[newUnit] = newEmbarkers end -- replace old distribution with new taskData.params.distribution = newDist --trigger.action.outText("+++clnZ: rebuilt distribution", 30) 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 --trigger.action.outText("+++clnZ: resolved selected transport <" .. tID .. "> to <" .. newTID .. ">", 30) 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("+++clne: <" .. theZone.name .. "> trackWith requires groupTracker module", 30) return end local trackerName = theZone.trackWith --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 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 spawner with settings (target location) local newCenter = cfxZones.getPoint(spawnZone) -- includes zone following updates local oCenter = cfxZones.getDCSOrigin(theZone) -- get original coords on map for cloning offsets -- calculate zoneDelta, is added to all vectors local zoneDelta = dcsCommon.vSub(newCenter, theZone.origin) -- oCenter) --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 = cfxZones.getPoint(spawnZone) 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) rawData.CZorigName = rawData.name -- save original group name local origID = rawData.groupId -- save original group ID rawData.CZorigID = origID cloneZones.uniqueIDGroupData(rawData) -- assign unique ID we know cloneZones.uniqueIDUnitData(rawData) -- assign unique ID for units -- saves old unitId as CZorigID 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 their position if not spawning to exact same location if cloneZones.verbose or theZone.verbose or spawnZone.verbose then trigger.action.outText("+++clnZ: tmpl delta x = <" .. math.floor(zoneDelta.x) .. ">, y = <" .. math.floor(zoneDelta.z) .. "> for tmpl <" .. theZone.name .. "> to cloner <" .. spawnZone.name .. ">", 30) end -- 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 r = math.random() * spawnZone.radius local phi = 6.2831 * math.random() -- that's 2Pi, folx local dx = r * math.cos(phi) local dy = r * math.sin(phi) --]] local loc, dx, dy = cfxZones.createRandomPointInZone(spawnZone) -- also supports polygonal zones for idx, aUnit in pairs(units) do if not spawnZone.centerOnly then -- *every unit's displacement is randomized -- r = math.random() * spawnZone.radius -- phi = 6.2831 * math.random() -- that's 2Pi, folx -- dx = r * math.cos(phi) -- dy = r * math.sin(phi) loc, dx, dy = cfxZones.createRandomPointInZone(spawnZone) 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: r = " .. r .. ", 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 cloneZones.uniqueNameGroupData(rawData) -- see what country we spawn for ctry = cloneZones.resolveOwnership(spawnZone, ctry) rawData.CZctry = ctry -- save ctry 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 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 -- trigger.action.outText("matching route point 1 and group with unit 1", 30) 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) cloneZones.allClones[rawData.name] = theData local theGroup = coalition.addGroup(rawData.CZctry, rawData.CZtheCat, rawData) table.insert(spawnedGroups, theGroup) -- 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 trigger.action.outText("clnZ: post-clone verification failed for unit <" .. uName .. ">: ÎD mismatch: " .. uID .. " -- " .. aUnit.CZTargetID, 30) end cloneZones.unitXlate[aUnit.CZorigID] = uID else trigger.action.outText("clnZ: post-clone verifiaction failed for unit <" .. uName .. ">: not found", 30) end end -- check if our assigned ID matches the handed out by -- DCS if newGroupID == rawData.CZTargetID then -- we are good else trigger.action.outText("clnZ: MISMATCH " .. rawData.name .. " target ID " .. rawData.CZTargetID .. " does not match " .. newGroupID, 30) 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 r = math.random() * spawnZone.radius --local phi = 6.2831 * math.random() -- that's 2Pi, folx --local dx = r * math.cos(phi) --local dy = r * math.sin(phi) local loc, dx, dy = cfxZones.createRandomPointInZone(spawnZone) -- also supports polygonal zones rawData.x = rawData.x + dx rawData.y = rawData.y + dy 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) -- make sure static name is unique and remember original rawData.name = dcsCommon.uuid(rawData.name) rawData.unitId = cloneZones.uniqueID() 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) cloneZones.allCObjects[rawData.name] = theData local theStatic = coalition.addStaticObject(ctry, rawData) local newStaticID = tonumber(theStatic:getID()) table.insert(spawnedStatics, theStatic) -- we don't mix groups with units, so no lookup tables for -- statics if newStaticID == rawData.CZTargetID then else trigger.action.outText("Static ID mismatch: " .. newStaticID .. " vs (target) " .. rawData.CZTargetID .. " for " .. rawData.name, 30) 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("Static spawn: spawned " .. aStaticName, 30) end -- processing for cargoManager if isCargo then if cfxCargoManager then cfxCargoManager.addCargo(theStatic) if cloneZones.verbose or spawnZone.verbose then trigger.action.outText("+++clne: added CARGO " .. theStatic:getName() .. " to cargo manager ", 30) end else if cloneZones.verbose or spawnZone.verbose then trigger.action.outText("+++clne: CARGO " .. theStatic:getName() .. " detected, not managerd", 30) end end end end local args = {} args.groups = spawnedGroups args.statics = spawnedStatics cloneZones.invokeCallbacks(theZone, "spawned", args) return spawnedGroups, spawnedStatics 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 -- force spawn with this spawner local templateZone = theZone if theZone.source then -- we use a different zone for templates -- souce 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? if theZone.preWipe then cloneZones.despawnAll(theZone) cloneZones.invokeCallbacks(theZone, "wiped", {}) end 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.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 aGroup:isExist() then local allUnits = aGroup:getUnits() for idy, aUnit in pairs(allUnits) do if aUnit:isExist() and aUnit:getLife() >= 1 then count = count + 1 end end 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 allUnits = aGroup:getUnits() for idy, aUnit in pairs(allUnits) do if aUnit:isExist() and aUnit:getLife() >= 1 then return true end 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 -- -- 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 = cfxZones.getFlagValue(aZone.deSpawnFlag, aZone) -- trigger.misc.getUserFlag(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 if cfxZones.testZoneFlag(aZone, aZone.spawnFlag, aZone.cloneTriggerMethod, "lastSpawnValue") then if cloneZones.verbose then trigger.action.outText("+++clnZ: spawn triggered for <" .. aZone.name .. ">", 30) end cloneZones.spawnWithCloner(aZone) end -- empty handling local isEmpty = cloneZones.countLiveUnits(aZone) < 1 and aZone.hasClones if isEmpty then -- see if we need to bang a flag if aZone.emptyFlag then --cloneZones.pollFlag(aZone.emptyFlag) cfxZones.pollFlag(aZone.emptyFlag, 'inc', aZone) end if aZone.emptyBangFlag then cfxZones.pollFlag(aZone.emptyBangFlag, aZone.cloneMethod, aZone) if cloneZones.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 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 -- 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 -- 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 -- 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) 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 -- local cat = staticData.cat -- 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 -- ALWAYS TRUE WHEN WE COME HERE! cData.isStarted local mySpawns = {} for idx, aName in pairs(cData.mySpawns) do local theGroup = Group.getByName(aName) if theGroup then table.insert(mySpawns, theGroup) 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) 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 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 return end cloneZones.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) if cloneZones.verbose then trigger.action.outText("+++clnZ: read config", 30) end 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") -- now create an rnd gen for each one and add them -- to our watchlist for k, aZone in pairs(attrZones) do cloneZones.createClonerWithZone(aZone) -- process attribute and add to zone cloneZones.addCloneZone(aZone) -- remember it so we can smoke it 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 --]]--