diff --git a/Doc/DML Documentation.pdf b/Doc/DML Documentation.pdf index 2bc4caa..57d88f5 100644 Binary files a/Doc/DML Documentation.pdf and b/Doc/DML Documentation.pdf differ diff --git a/Doc/DML Quick Reference.pdf b/Doc/DML Quick Reference.pdf index 110c5e6..348b714 100644 Binary files a/Doc/DML Quick Reference.pdf and b/Doc/DML Quick Reference.pdf differ diff --git a/modules/RNDFlags.lua b/modules/RNDFlags.lua index 3bbce15..310e38b 100644 --- a/modules/RNDFlags.lua +++ b/modules/RNDFlags.lua @@ -1,5 +1,5 @@ rndFlags = {} -rndFlags.version = "1.2.0" +rndFlags.version = "1.3.0" rndFlags.verbose = false rndFlags.requiredLibs = { "dcsCommon", -- always @@ -23,6 +23,9 @@ rndFlags.requiredLibs = { some code clean-up rndMethod synonym 1.2.0 - Watchflag integration + 1.3.0 - DML simplification: RND! + zone-local verbosity + --]] rndFlags.rndGen = {} @@ -87,18 +90,28 @@ end -- create rnd gen from zone -- function rndFlags.createRNDWithZone(theZone) - local flags = cfxZones.getStringFromZoneProperty(theZone, "flags!", "") - if flags == "" then - -- let's try alternate spelling without "!" - flags = cfxZones.getStringFromZoneProperty(theZone, "flags", "") + local flags = "" + if cfxZones.hasProperty(theZone, "RND!") then + flags = cfxZones.getStringFromZoneProperty(theZone, "RND!", "") + elseif cfxZones.hasProperty(theZone, "flags!") then + trigger.action.outText("+++RND: warning - zone <" .. theZone.name .. ">: deprecated 'flags!' usage, use 'RND!' instead.", 30) + flags = cfxZones.getStringFromZoneProperty(theZone, "flags!", "") + elseif cfxZones.hasProperty(theZone, "flags") then + trigger.action.outText("+++RND: warning - zone <" .. theZone.name .. ">: deprecated 'flags' (no bang) usage, use 'RND!' instead.", 30) + flags = cfxZones.getStringFromZoneProperty(theZone, "flags", "") + else + trigger.action.outText("+++RND: warning - zone <" .. theZone.name .. ">: no flags defined!", 30) end + -- now build the flag array from strings local theFlags = rndFlags.flagArrayFromString(flags) theZone.myFlags = theFlags - + if rndFlags.verbose or theZone.verbose then + trigger.action.outText("+++RND: output set for <" .. theZone.name .. "> is <" .. flags .. ">",30) + end theZone.pollSizeMin, theZone.pollSize = cfxZones.getPositiveRangeFromZoneProperty(theZone, "pollSize", 1) - if rndFlags.verbose then + if rndFlags.verbose or theZone.verbose then trigger.action.outText("+++RND: pollSize is <" .. theZone.pollSizeMin .. ", " .. theZone.pollSize .. ">", 30) end @@ -179,8 +192,8 @@ function rndFlags.fire(theZone) if #availableFlags < 1 then - if rndFlags.verbose then - trigger.action.outText("+++RND: RND " .. theZone.name .. " ran out of flags. aborting fire", 30) + if rndFlags.verbose or theZone.verbose then + trigger.action.outText("+++RND: RND " .. theZone.name .. " ran out of flags. Will fire 'done' instead ", 30) end if theZone.doneFlag then @@ -190,7 +203,7 @@ function rndFlags.fire(theZone) return end - if rndFlags.verbose then + if rndFlags.verbose or theZone.verbose then trigger.action.outText("+++RND: firing RND " .. theZone.name .. " with pollsize " .. pollSize .. " on " .. #availableFlags .. " set size", 30) end @@ -212,7 +225,7 @@ function rndFlags.fire(theZone) local theFlag = table.remove(availableFlags,theFlagIndex) --rndFlags.pollFlag(theFlag, theZone.rndMethod) - if rndFlags.verbose then + if rndFlags.verbose or theZone.verbose then trigger.action.outText("+++RND: polling " .. theFlag .. " with " .. theZone.rndMethod, 30) end @@ -234,25 +247,12 @@ function rndFlags.update() for idx, aZone in pairs(rndFlags.rndGen) do if cfxZones.testZoneFlag(aZone, aZone.triggerFlag, aZone.rndTriggerMethod, "lastTriggerValue") then - if rndFlags.verbose then + if rndFlags.verbose or aZone.verbose then trigger.action.outText("+++RND: triggering " .. aZone.name, 30) end rndFlags.fire(aZone) end ---[[-- old code pre watchflag - if aZone.triggerFlag then - local currTriggerVal = cfxZones.getFlagValue(aZone.triggerFlag, aZone) -- trigger.misc.getUserFlag(aZone.triggerFlag) - if currTriggerVal ~= aZone.lastTriggerValue - then - if rndFlags.verbose then - trigger.action.outText("+++RND: triggering " .. aZone.name, 30) - end - rndFlags.fire(aZone) - aZone.lastTriggerValue = currTriggerVal - end - end ---]]-- end end @@ -306,7 +306,9 @@ function rndFlags.start() rndFlags.readConfigZone() -- process RND Zones - local attrZones = cfxZones.getZonesWithAttributeNamed("RND") + local attrZones = cfxZones.getZonesWithAttributeNamed("RND!") + a = dcsCommon.getSizeOfTable(attrZones) + trigger.action.outText("RND! zones: " .. a, 30) -- now create an rnd gen for each one and add them -- to our watchlist @@ -314,6 +316,15 @@ function rndFlags.start() rndFlags.createRNDWithZone(aZone) -- process attribute and add to zone rndFlags.addRNDZone(aZone) -- remember it so we can smoke it end + + -- obsolete here + attrZones = cfxZones.getZonesWithAttributeNamed("RND") + -- now create an rnd gen for each one and add them + -- to our watchlist + for k, aZone in pairs(attrZones) do + rndFlags.createRNDWithZone(aZone) -- process attribute and add to zone + rndFlags.addRNDZone(aZone) -- remember it so we can smoke it + end -- start cycle timer.scheduleFunction(rndFlags.startCycle, {}, timer.getTime() + 0.25) diff --git a/modules/cfxZones.lua b/modules/cfxZones.lua index 3e4fdbc..b08854a 100644 --- a/modules/cfxZones.lua +++ b/modules/cfxZones.lua @@ -4,9 +4,8 @@ -- -- Copyright (c) 2021, 2022 by Christian Franz and cf/x AG -- - cfxZones = {} -cfxZones.version = "2.7.6" +cfxZones.version = "2.7.7" --[[-- VERSION HISTORY - 2.2.4 - getCoalitionFromZoneProperty - getStringFromZoneProperty @@ -65,12 +64,14 @@ cfxZones.version = "2.7.6" - hasProperty now offers active information when looking for '*?' and '*!' - 2.7.0 - doPollFlag - fully support multiple flags per bang! - 2.7.1 - setFlagValueMult() - - 2.7.2 - '161 repair' + - 2.7.2 - '261 repair' - 2.7.3 - testZoneFlag returns mathodResult, lastVal - evalFlagMethodImmediate() - 2.7.4 - doPollFlag supports immediate number setting - 2.7.5 - more QoL checks when mixing up ? and ! for attributes - 2.7.6 - trim for getBoolFromZoneProperty and getStringFromZoneProperty + - 2.7.7 - randomInRange() + - show number of zones --]]-- cfxZones.verbose = false @@ -558,6 +559,7 @@ end function cfxZones.getZonesWithAttributeNamed(attributeName, testZones) if not testZones then testZones = cfxZones.zones end + local attributZones = {} for aName,aZone in pairs(testZones) do local attr = cfxZones.getZoneProperty(aZone, attributeName) @@ -1416,6 +1418,7 @@ function cfxZones.testFlagByMethodForZone(currVal, lastVal, theMethod, theZone) return false end + function cfxZones.testZoneFlag(theZone, theFlagName, theMethod, latchName) -- returns two values: true/false method result, and curr value -- returns true if method constraints are met for flag theFlagName @@ -1603,6 +1606,15 @@ function cfxZones.getMinMaxFromZoneProperty(theZone, theProperty) end +function cfxZones.randomInRange(minVal, maxVal) + if maxVal < minVal then + local t = minVal + minVal = maxVal + maxVal = t + end + return cfxZones.randomDelayFromPositiveRange(minVal, maxVal) +end + function cfxZones.randomDelayFromPositiveRange(minVal, maxVal) if not maxVal then return minVal end if not minVal then return maxVal end @@ -1697,7 +1709,6 @@ function cfxZones.hasProperty(theZone, theProperty) -- return foundIt ~= nil end - function cfxZones.getBoolFromZoneProperty(theZone, theProperty, defaultVal) if not defaultVal then defaultVal = false end if type(defaultVal) ~= "boolean" then @@ -1920,7 +1931,8 @@ function cfxZones.init() cfxZones.startMovingZones() cfxZones.updateMovingZones() -- will auto-repeat - trigger.action.outText("cf/x Zones v".. cfxZones.version .. ": loaded", 10) + trigger.action.outText("cf/x Zones v".. cfxZones.version .. ": loaded, zones:" .. dcsCommon.getSizeOfTable(cfxZones.zones), 30) + end -- get everything rolling diff --git a/modules/cloneZone.lua b/modules/cloneZone.lua index 651dfcf..48ff736 100644 --- a/modules/cloneZone.lua +++ b/modules/cloneZone.lua @@ -1,1064 +1,1112 @@ -cloneZones = {} -cloneZones.version = "1.4.4" -cloneZones.verbose = false -cloneZones.requiredLibs = { - "dcsCommon", -- always - "cfxZones", -- Zones, of course - "cfxMX", -} --- groupTracker is OPTIONAL! and required only with trackWith attribute + cloneZones = {} + cloneZones.version = "1.4.5" + cloneZones.verbose = false + cloneZones.requiredLibs = { + "dcsCommon", -- always + "cfxZones", -- Zones, of course + "cfxMX", + } + -- 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 ---[[-- - 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 - - ---]]-- - --- --- 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 --- "") - 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 + 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 + --[[-- + Clones Groups from ME mission data + Copyright (c) 2022 by Christian Franz and cf/x AG - 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 --- cloneZones.templateGroups[gName] = origID --- cloneZones.templateGroupsReverse[origID] = gName - end - end + 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 - 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 + --]]-- + + -- + -- 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(theZone.name .. " clone template saved", 30) + 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 + -- "") -- 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.method = cfxZones.getStringFromZoneProperty(theZone, "method", "inc") - - 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:", "") - end - - -- we end with clear plate -end + -- group translation orig id --- --- spawning, despawning --- - -function cloneZones.despawnAll(theZone) - if cloneZones.verbose then - trigger.action.outText("wiping <" .. theZone.name .. ">", 30) - end - for idx, aGroup in pairs(theZone.mySpawns) do - --trigger.action.outText("++clnZ: despawn all " .. aGroup.name, 30) + -- + -- reading zones + -- + function cloneZones.partOfGroupDataInZone(theZone, theUnits) + local zP = cfxZones.getPoint(theZone) + zP.y = 0 - if aGroup:isExist() then - cloneZones.invokeCallbacks(theZone, "will despawn group", aGroup) - Group.destroy(aGroup) + for idx, aUnit in pairs(theUnits) do + local uP = {} + uP.x = aUnit.x + uP.y = 0 + uP.z = aUnit.y -- !! y-z + local dist = dcsCommon.dist(uP, zP) + if dist <= theZone.radius then return true end end + return false 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 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.updateLocationsInGroupData(theData, zoneDelta, adjustAllWaypoints) - - -- remember that zoneDelta's [z] modifies theData's y!! - theData.x = theData.x + zoneDelta.x - theData.y = theData.y + zoneDelta.z -- !!! - local units = theData.units - 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, but only all others if asked - -- to - 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 -- (!!) + function cloneZones.allGroupsInZoneByData(theZone) + local theGroupsInZone = {} + local radius = theZone.radius + 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 - else - -- only first point - thePoints[1].x = thePoints[1].x + zoneDelta.x - thePoints[1].y = thePoints[1].y + zoneDelta.z -- (!!) - end - - -- if there is an airodrome id given in first waypoint, - -- adjust for closest location - local firstPoint = thePoints[1] - if firstPoint.airdromeId then - trigger.action.outText("first: airdrome adjust for " .. theData.name .. " now is " .. firstPoint.airdromeId, 30) - local loc = {} - loc.x = firstPoint.x - loc.y = 0 - loc.z = firstPoint.y - local bestAirbase = dcsCommon.getClosestAirbaseTo(loc) - firstPoint.airdromeId = bestAirbase:getID() - trigger.action.outText("first: adjusted to " .. firstPoint.airdromeId, 30) - end - - -- adjust last point (landing) - if #thePoints > 1 then - local lastPoint = thePoints[#thePoints] - if firstPoint.airdromeId then - trigger.action.outText("last: airdrome adjust for " .. theData.name .. " now is " .. lastPoint.airdromeId, 30) - local loc = {} - loc.x = lastPoint.x - loc.y = 0 - loc.z = lastPoint.y - local bestAirbase = dcsCommon.getClosestAirbaseTo(loc) - lastPoint.airdromeId = bestAirbase:getID() - trigger.action.outText("last: adjusted to " .. lastPoint.airdromeId, 30) - end - end end + return theGroupsInZone 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 fine 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] - --trigger.action.outText("Resolve for <" .. myOName .. "> the external ID: " .. gID .. " --> " .. groupName .. " for <" .. reason.. "> task", 30) - - -- 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 - --trigger.action.outText("resolved (internally) " .. gID .. " to " .. resolvedID, 30) - return resolvedID + function cloneZones.createClonerWithZone(theZone) -- has "Cloner" + if cloneZones.verbose then + trigger.action.outText("+++clnZ: new cloner " .. theZone.name, 30) end - end - - -- now check if we have spawned this before - local lastClone = cloneZones.groupXlate[gID] - if lastClone then - resolvedID = lastClone - --trigger.action.outText("resolved (EXT) " .. gID .. " to " .. resolvedID, 30) - return resolvedID - end - - -- if we get here, reference is not to a cloned item - --trigger.action.outText("resolved " .. gID .. " to " .. resolvedID, 30) - return resolvedID -end -function cloneZones.resolveUnitID(uID, rawData, dataTable, reason) --- also resolves statics as they share ID with units - local resolvedID = uID - --trigger.action.outText("Resolve reference to unitId <" .. uID .. "> for <" .. reason.. "> task", 30) - - -- 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 - --trigger.action.outText("resolved (internally) " .. uID .. " to " .. resolvedID, 30) - return resolvedID - end - end - - end - - -- now check if we have spawned this before - local lastClone = cloneZones.unitXlate[uID] - if lastClone then - resolvedID = lastClone - --trigger.action.outText("resolved (U-EXT) " .. uID .. " to " .. resolvedID, 30) - return resolvedID - end - - -- if we get here, reference is not to a cloned item - --trigger.action.outText("resolved G-" .. uID .. " to " .. resolvedID, 30) - return resolvedID -end - -function cloneZones.resolveStaticLinkUnit(uID) - local resolvedID = uID - local lastClone = cloneZones.unitXlate[uID] - if lastClone then - resolvedID = lastClone - --trigger.action.outText("resolved (U-EXT) " .. uID .. " to " .. resolvedID, 30) - 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 - --trigger.action.outText("resolved link unit to "..resolvedID .. " for " .. rawData.name, 30) - 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 - for idy, taskData in pairs(tasks) do - -- resolve group references in TASKS - 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 groupd 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 - rigger.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 - 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 + local localZones = cloneZones.allGroupsInZoneByData(theZone) + local localObjects = cfxZones.allStaticsInZone(theZone) + theZone.cloner = true -- this is a cloner zoner + theZone.mySpawns = {} + theZone.myStatics = {} + theZone.origin = cfxZones.getPoint(theZone) -- save reference point for all groupVectors + + -- 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 - end -end - -function cloneZones.spawnWithTemplateForZone(theZone, spawnZone) - -- theZone is the cloner with the template - -- spawnZone is the spawner with settings - --if not spawnZone then spawnZone = theZone end - local newCenter = cfxZones.getPoint(spawnZone) - -- calculate zoneDelta, is added to all vectors - local zoneDelta = dcsCommon.vSub(newCenter, theZone.origin) - - local spawnedGroups = {} - local spawnedStatics = {} - local dataToSpawn = {} - - 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 - -- now use raw data to spawn and see if it works outabox - local theCat = cfxMX.catText2ID(cat) - rawData.CZtheCat = theCat -- save cat - - -- update their position if not spawning to exact same location - cloneZones.updateLocationsInGroupData(rawData, zoneDelta, spawnZone.moveRoute) - - -- apply turning - dcsCommon.rotateGroupData(rawData, spawnZone.turn, 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 - local theGroup = coalition.addGroup(rawData.CZctry, rawData.CZtheCat, rawData) - table.insert(spawnedGroups, theGroup) - - --trigger.action.outText("spawned group " .. rawData.name .. "consisting of", 30) - - -- update groupXlate table - 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 - --trigger.action.outText("unit " .. uName .. "#"..uID, 30) - -- all good - else - trigger.action.outText("clnZ: post-clone verification failed for unit <" .. uName .. ">: ÎD mismatch: " .. uID .. " -- " .. aUnit.CZTargetID, 30) + 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 + -- cloneZones.templateGroups[gName] = origID + -- cloneZones.templateGroupsReverse[origID] = gName + 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 - cloneZones.unitXlate[aUnit.CZorigID] = uID - else - trigger.action.outText("clnZ: post-clone verifiaction failed for unit <" .. uName .. ">: not found", 30) - end + theZone.cloneNames = nil + theZone.staticNames = nil + end + if cloneZones.verbose then + trigger.action.outText(theZone.name .. " clone template saved", 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 + -- watchflag: + -- triggerMethod + theZone.cloneTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "triggerMethod", "change") - cloneZones.invokeCallbacks(theZone, "did spawn group", theGroup) + 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 theZone.deSpawnFlag then + theZone.lastDeSpawnValue = cfxZones.getFlagValue(theZone.deSpawnFlag, theZone) + end + + -- to be deprecated + 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.method = cfxZones.getStringFromZoneProperty(theZone, "method", "inc") + + if cfxZones.hasProperty(theZone, "masterOwner") then + theZone.masterOwner = cfxZones.getStringFromZoneProperty(theZone, "masterOwner", "") + end + + theZone.turn = cfxZones.getNumberFromZoneProperty(theZone, "turn", 0) + -- interface to groupTracker - if theZone.trackWith then - cloneZones.handoffTracking(theGroup, theZone) + if cfxZones.hasProperty(theZone, "trackWith:") then + theZone.trackWith = cfxZones.getStringFromZoneProperty(theZone, "trackWith:", "") 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 - - -- now use raw data to spawn and see if it works outabox - --local theCat = cfxMX.catText2ID(cat) -- will be "static" - - -- trigger.action.outText("static object proccing", 30) - rawData.x = rawData.x + zoneDelta.x - rawData.y = rawData.y + zoneDelta.z -- !!! - - -- apply turning - dcsCommon.rotateUnitData(rawData, spawnZone.turn, 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 - --trigger.action.outText("has link to " .. rawData.linkUnit, 30) - local lU = cloneZones.resolveStaticLinkUnit(rawData.linkUnit) - --trigger.action.outText("resolved to " .. lU, 30) - rawData.linkUnit = lU - if not rawData.offsets then - rawData.offsets = {} - rawData.offsets.angle = 0 - rawData.offsets.x = 0 - rawData.offsets.y = 0 - --trigger.action.outText("clnZ: link required offset for " .. rawData.name, 30) - 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 --- trigger.action.outText("zone deltas are " .. zoneDelta.x .. ", " .. zoneDelta.y, 30) - end - - 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 --- trigger.action.outText("Static ID OK: " .. newStaticID .. " for " .. rawData.name, 30) - 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 then - trigger.action.outText("Static spawn: spawned " .. aStaticName, 30) + -- 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 - 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 + if theZone.rndLoc and theZone.verbose then + trigger.action.outText("+++ rndloc on for " .. theZone.name, 30) + end + + -- we end with clear plate 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 then - trigger.action.outText("+++clnZ: picked random template <" .. templateName .."> for from <" .. allNames .. "> for cloner " .. theZone.name, 30) + + -- + -- spawning, despawning + -- + + function cloneZones.despawnAll(theZone) + if cloneZones.verbose then + trigger.action.outText("wiping <" .. 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 + cloneZones.invokeCallbacks(theZone, "will despawn group", aGroup) + Group.destroy(aGroup) end end - - local newTemplate = cloneZones.getCloneZoneByName(templateName) - if not newTemplate then - if cloneZones.verbose then - trigger.action.outText("+++clnZ: no clone source with name <" .. templateName .."> for cloner " .. theZone.name, 30) - end - return + 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 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 - 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 + function cloneZones.updateLocationsInGroupData(theData, zoneDelta, adjustAllWaypoints) + + -- remember that zoneDelta's [z] modifies theData's y!! + theData.x = theData.x + zoneDelta.x + theData.y = theData.y + zoneDelta.z -- !!! + local units = theData.units + 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, but only all others if asked + -- to + 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 -- (!!) + 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 then - trigger.action.outText("+++clnZ: DEspawn triggered for <" .. aZone.name .. ">", 30) + else + -- only first point + thePoints[1].x = thePoints[1].x + zoneDelta.x + thePoints[1].y = thePoints[1].y + zoneDelta.z -- (!!) 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.method, aZone) - if cloneZones.verbose then - trigger.action.outText("+++clnZ: bang! on " .. aZone.emptyBangFlag, 30) + + -- if there is an airodrome id given in first waypoint, + -- adjust for closest location + local firstPoint = thePoints[1] + if firstPoint.airdromeId then + trigger.action.outText("first: airdrome adjust for " .. theData.name .. " now is " .. firstPoint.airdromeId, 30) + local loc = {} + loc.x = firstPoint.x + loc.y = 0 + loc.z = firstPoint.y + local bestAirbase = dcsCommon.getClosestAirbaseTo(loc) + firstPoint.airdromeId = bestAirbase:getID() + trigger.action.outText("first: adjusted to " .. firstPoint.airdromeId, 30) + end + + -- adjust last point (landing) + if #thePoints > 1 then + local lastPoint = thePoints[#thePoints] + if firstPoint.airdromeId then + trigger.action.outText("last: airdrome adjust for " .. theData.name .. " now is " .. lastPoint.airdromeId, 30) + local loc = {} + loc.x = lastPoint.x + loc.y = 0 + loc.z = lastPoint.y + local bestAirbase = dcsCommon.getClosestAirbaseTo(loc) + lastPoint.airdromeId = bestAirbase:getID() + trigger.action.outText("last: adjusted to " .. lastPoint.airdromeId, 30) + end + end end - -- invoke callbacks - cloneZones.invokeCallbacks(aZone, "empty", {}) - - -- prevent isEmpty next pass - aZone.hasClones = false + 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 fine master owner <" .. spawnZone.masterOwner .. ">", 30) + return ctry end + if not masterZone.owner then + return ctry + end + + ctry = dcsCommon.getACountryForCoalition(masterZone.owner) + return ctry end -end -function cloneZones.onStart() - --trigger.action.outText("+++clnZ: Enter atStart", 30) - for idx, theZone in pairs(cloneZones.cloners) do - if theZone.onStart then - if cloneZones.verbose then - trigger.action.outText("+++clnZ: atStart will spawn for <"..theZone.name .. ">", 30) + -- + -- 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] + --trigger.action.outText("Resolve for <" .. myOName .. "> the external ID: " .. gID .. " --> " .. groupName .. " for <" .. reason.. "> task", 30) + + -- 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 + --trigger.action.outText("resolved (internally) " .. gID .. " to " .. resolvedID, 30) + return resolvedID + end + end + + -- now check if we have spawned this before + local lastClone = cloneZones.groupXlate[gID] + if lastClone then + resolvedID = lastClone + --trigger.action.outText("resolved (EXT) " .. gID .. " to " .. resolvedID, 30) + return resolvedID + end + + -- if we get here, reference is not to a cloned item + --trigger.action.outText("resolved " .. gID .. " to " .. resolvedID, 30) + return resolvedID + end + + function cloneZones.resolveUnitID(uID, rawData, dataTable, reason) + -- also resolves statics as they share ID with units + local resolvedID = uID + --trigger.action.outText("Resolve reference to unitId <" .. uID .. "> for <" .. reason.. "> task", 30) + + -- 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 + --trigger.action.outText("resolved (internally) " .. uID .. " to " .. resolvedID, 30) + return resolvedID + end + end + + end + + -- now check if we have spawned this before + local lastClone = cloneZones.unitXlate[uID] + if lastClone then + resolvedID = lastClone + --trigger.action.outText("resolved (U-EXT) " .. uID .. " to " .. resolvedID, 30) + return resolvedID + end + + -- if we get here, reference is not to a cloned item + --trigger.action.outText("resolved G-" .. uID .. " to " .. resolvedID, 30) + return resolvedID + end + + function cloneZones.resolveStaticLinkUnit(uID) + local resolvedID = uID + local lastClone = cloneZones.unitXlate[uID] + if lastClone then + resolvedID = lastClone + --trigger.action.outText("resolved (U-EXT) " .. uID .. " to " .. resolvedID, 30) + 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 + --trigger.action.outText("resolved link unit to "..resolvedID .. " for " .. rawData.name, 30) + 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 + for idy, taskData in pairs(tasks) do + -- resolve group references in TASKS + 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 groupd 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 + rigger.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 + 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) + -- theZone is the cloner with the template + -- spawnZone is the spawner with settings + --if not spawnZone then spawnZone = theZone end + local newCenter = cfxZones.getPoint(spawnZone) + -- calculate zoneDelta, is added to all vectors + local zoneDelta = dcsCommon.vSub(newCenter, theZone.origin) + + local spawnedGroups = {} + local spawnedStatics = {} + local dataToSpawn = {} + + 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 - cloneZones.spawnWithCloner(theZone) + -- now use raw data to spawn and see if it works outabox + local theCat = cfxMX.catText2ID(cat) + rawData.CZtheCat = theCat -- save cat + + -- update their position if not spawning to exact same location + cloneZones.updateLocationsInGroupData(rawData, zoneDelta, spawnZone.moveRoute) + + -- apply randomizer if selected + if spawnZone.rndLoc then + local units = rawData.units + for idx, aUnit in pairs(units) do + 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) + aUnit.x = aUnit.x + dx + aUnit.y = aUnit.y + dy + end + end + + -- apply turning + dcsCommon.rotateGroupData(rawData, spawnZone.turn, 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 - end -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 + local theGroup = coalition.addGroup(rawData.CZctry, rawData.CZtheCat, rawData) + table.insert(spawnedGroups, theGroup) + + --trigger.action.outText("spawned group " .. rawData.name .. "consisting of", 30) + + -- update groupXlate table + 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 + --trigger.action.outText("unit " .. uName .. "#"..uID, 30) + -- 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 --- --- START --- -function cloneZones.readConfigZone() - local theZone = cfxZones.getZoneByName("cloneZonesConfig") - if not theZone then + cloneZones.invokeCallbacks(theZone, "did spawn group", theGroup) + -- interface to groupTracker + if theZone.trackWith then + cloneZones.handoffTracking(theGroup, theZone) + 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 + + -- now use raw data to spawn and see if it works outabox + --local theCat = cfxMX.catText2ID(cat) -- will be "static" + + -- trigger.action.outText("static object proccing", 30) + 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) + rawData.x = rawData.x + dx + rawData.y = rawData.y + dy + end + + -- apply turning + dcsCommon.rotateUnitData(rawData, spawnZone.turn, 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 + --trigger.action.outText("has link to " .. rawData.linkUnit, 30) + local lU = cloneZones.resolveStaticLinkUnit(rawData.linkUnit) + --trigger.action.outText("resolved to " .. lU, 30) + rawData.linkUnit = lU + if not rawData.offsets then + rawData.offsets = {} + rawData.offsets.angle = 0 + rawData.offsets.x = 0 + rawData.offsets.y = 0 + --trigger.action.outText("clnZ: link required offset for " .. rawData.name, 30) + 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 + -- trigger.action.outText("zone deltas are " .. zoneDelta.x .. ", " .. zoneDelta.y, 30) + end + + local isCargo = rawData.canCargo + 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 + -- trigger.action.outText("Static ID OK: " .. newStaticID .. " for " .. rawData.name, 30) + 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 then + trigger.action.outText("+++clnZ: picked random template <" .. templateName .."> for from <" .. allNames .. "> for cloner " .. theZone.name, 30) + end + end + + local newTemplate = cloneZones.getCloneZoneByName(templateName) + if not newTemplate then + if cloneZones.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 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.method, 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.onStart() + --trigger.action.outText("+++clnZ: Enter atStart", 30) + for idx, theZone in pairs(cloneZones.cloners) do + if theZone.onStart then + if cloneZones.verbose then + trigger.action.outText("+++clnZ: atStart will spawn for <"..theZone.name .. ">", 30) + end + cloneZones.spawnWithCloner(theZone) + + end + 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: NO config zone!", 30) + trigger.action.outText("+++clnZ: read config", 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 + + 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 + + -- run through onStart, but leave at least a few + -- cycles to go through object removal so statics + -- can spawn on ground. onStart is being deprecated, the + -- raiseFlag module covers this since the first time + -- raiseFlag is run is t0 + 0.5s + timer.scheduleFunction(cloneZones.onStart, {}, timer.getTime() + 0.1) + + -- start update + cloneZones.update() + + trigger.action.outText("cfx Clone Zones v" .. cloneZones.version .. " started.", 30) + return true end - - -- run through onStart, but leave at least a few - -- cycles to go through object removal so statics - -- can spawn on ground. onStart is being deprecated, the - -- raiseFlag module covers this since the first time - -- raiseFlag is run is t0 + 0.5s - timer.scheduleFunction(cloneZones.onStart, {}, timer.getTime() + 0.1) - - -- start update - cloneZones.update() - - 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 + -- 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 + --[[-- + to resolve tasks - - AFAC - - FAC Assign group - - set freq for unit - - embark / disembark ---]]-- \ No newline at end of file + - AFAC + - FAC Assign group + - set freq for unit + - embark / disembark + --]]-- \ No newline at end of file diff --git a/modules/csarManager2.lua b/modules/csarManager2.lua index 668ee78..f41bbf2 100644 --- a/modules/csarManager2.lua +++ b/modules/csarManager2.lua @@ -1,5 +1,5 @@ csarManager = {} -csarManager.version = "2.1.0" +csarManager.version = "2.1.1" csarManager.verbose = false csarManager.ups = 1 @@ -35,6 +35,7 @@ csarManager.ups = 1 - csarBlueDelivered - finally fixed smoke performance bug - csarManager.vectoring optional + - 2.1.1 - zone-local verbosity --]]-- -- modules that need to be loaded BEFORE I run @@ -178,7 +179,7 @@ function csarManager.createDownedPilot(theMission) theBoyGroup) if theBoyGroup then --- trigger.action.outText("+++csar: created csar!", 30) + else trigger.action.outText("+++csar: FAILED to create csar!", 30) end @@ -343,7 +344,6 @@ function csarManager.somethingHappened(event) local ID = event.id local myType = theUnit:getTypeName() --- trigger.action.outText("+++csar: event " .. ID .. " for player unit " .. theUnit:getName() .. " of type " .. myType, 30) if ID == 4 then -- landed csarManager.heloLanded(theUnit) @@ -375,9 +375,7 @@ function csarManager.successMission(who, where, theMission) -- callback has format callback(coalition, success true/false, numberSaved, descriptionText) csarManager.invokeCallbacks(theMission.side, true, 1, "success") --- for idx, callback in pairs(csarManager.csarCompleteCB) do --- callback(theMission.side, true, 1, "test") --- end + trigger.action.outSoundForCoalition(theMission.side, "Quest Snare 3.wav") if csarManager.csarRedDelivered and theMission.side == 1 then @@ -420,13 +418,10 @@ function csarManager.heloLanded(theUnit) local currentBaseSide = base.side if base.zone.owner then - -- this zone is shared with capturable + -- this zone is shared with capturable (owned) -- zone extensions like owned zone, FARP etc. -- use current owner currentBaseSide = base.zone.owner --- trigger.action.outText("+++csar: overriding base.side with zone owner = " .. currentBaseSide .. " for csarB " .. base.name .. ", requiring " .. mySide .. " or 0 to land", 30) - else --- trigger.action.outText("+++csar: base " .. base.name .. " has no owner - proceeding with side = " .. base.side .. " looking for " .. mySide, 30) end if currentBaseSide == mySide or @@ -551,21 +546,7 @@ function csarManager.heloCrashed(theUnit) local theGroup = theUnit:getGroup() conf.id = theGroup:getID() conf.currentState = -1 -- (we don't know) - --[[-- - if #conf.troopsOnBoard > 0 then - -- this is where we can create a new CSAR mission - trigger.action.outSoundForCoalition(conf.id, theUnit:getName() .. " crashed while evacuating " .. #conf.troopsOnBoard .. " pilots. Survivors possible.", 30) - trigger.action.outSoundForCoalition(conf.id, "Quest Snare 3.wav") - for i=1, #conf.troopsOnBoard do - local msn = conf.troopsOnBoard[i] -- picked up unit(s) - local theRescuedPilot = msn.name - -- create x new missions in 50m radius - -- except for pilot, that will be called - -- from limitedAirframes - csarManager.createCSARforUnit(theUnit, theRescuedPilot, 50, true) - end - end - --]]-- + conf.troopsOnBoard = {} local myName = conf.name cargoSuper.removeAllMassForCargo(myName, "Evacuees") -- will allocate new empty table @@ -876,7 +857,10 @@ function csarManager.addCSARBase(aZone) csarBase.side = cfxZones.getCoalitionFromZoneProperty(aZone, "coalition", 0) table.insert(csarManager.csarBases, csarBase) --- trigger.action.outText("+++csar: found base " .. csarBase.name .. " for side " .. csarBase.side, 30) + + if csarManager.verbose or aZone.verbose then + trigger.action.outText("+++csar: zone <" .. csarBase.name .. "> safe for side " .. csarBase.side, 30) + end end function csarManager.getCSARBaseforZone(aZone) @@ -895,7 +879,6 @@ end -- -- - -- -- updateCSARMissions: make sure evacuees are still alive -- @@ -1191,28 +1174,6 @@ function csarManager.processCSARZones() for k, aZone in pairs(csarBases) do csarManager.readCSARZone(aZone) - --[[-- - -- gather data, and then create a mission from this - local theSide = cfxZones.getCoalitionFromZoneProperty(aZone, "coalition", 0) - aZone.csarSide = theSide - local name = cfxZones.getZoneProperty(aZone, "name") - aZone. - local freq = cfxZones.getNumberFromZoneProperty(aZone, "freq", 0) - if freq == 0 then freq = nil end - local numCrew = 1 - local mapMarker = nil - local timeLimit = cfxZones.getNumberFromZoneProperty(aZone, "timeLimit", 0) - if timeLimit == 0 then timeLimit = nil else timeLimit = timeLimit * 60 end - - local theMission = csarManager.createCSARMissionData(aZone.point, - theSide, - freq, - name, - numCrew, - timeLimit, - mapMarker) - csarManager.addMission(theMission) - --]]-- end end diff --git a/modules/limitedAirframes.lua b/modules/limitedAirframes.lua index 3e56d3d..be88ac3 100644 --- a/modules/limitedAirframes.lua +++ b/modules/limitedAirframes.lua @@ -1,19 +1,24 @@ limitedAirframes = {} -limitedAirframes.version = "1.3.0" +limitedAirframes.version = "1.4.0" +limitedAirframes.verbose = false limitedAirframes.enabled = true -- can be turned off -limitedAirframes.userCanToggle = true -- F-10 menu? +limitedAirframes.userCanToggle = true -- F10 menu? +limitedAirframes.onlyOwnSide = true -- F10 query only shows own side count, for later expansion limitedAirframes.maxRed = -1 -- -1 == infinite -limitedAirframes.maxBlue = 6 -- -1 = infinite +limitedAirframes.maxBlue = -1 -- = infinite limitedAirframes.redWinsFlag = "999" limitedAirframes.blueWinsFlag = "998" - +limitedAirframes.method = "inc" +limitedAirframes.warningSound = "Quest Snare 3.wav" +limitedAirframes.loseSound = "Death PIANO.wav" +limitedAirframes.winSound = "Triumphant Victory.wav" + limitedAirframes.requiredLibs = { "dcsCommon", -- common is of course needed for everything -- pretty stupid to check for this since we -- need common to invoke the check, but anyway "cfxZones", -- Zones, of course for safe landings - "cfxPlayer", -- callbacks --- "cfxGroundTroops", -- generic data module for weight + "cfxPlayer", } --[[-- VERSION HISTORY @@ -40,6 +45,10 @@ limitedAirframes.requiredLibs = { - hand change in pilotsafe zones that can be landed in - 1.2.0 - limitedAirframesConfig zone - 1.3.0 - added network dead override logic via unitFlownByPlayer + - 1.4.0 - DML integration, verbosity, clean-up, QoL improvements + redSafe, blueSafe with attribute, backward compatible + currRed + --]]-- @@ -52,6 +61,7 @@ limitedAirframes.requiredLibs = { -- *** EXTENDS ZONES *** -- safe zones must have a property "pilotSafe" -- - pilotSafe - this is a zone to safely change airframes in +-- - can also carry 'red' or 'blue' to enable -- - redSafe (optional, defaults to true) -- - blueSafe (optional, defaults to true) -- set to "false" or "no" to disallow that side to change @@ -80,7 +90,7 @@ limitedAirframes.lastEvents = {} -- update the side's airframe credit limitedAirframes.currRed = 0 -limitedAirframes.currRed = 0 +limitedAirframes.currBlue = 0 -- we record all unit names that contain a player -- so that we can check against these when we receive @@ -106,11 +116,19 @@ function limitedAirframes.readConfigZone() -- note: must match exactly!!!! local theZone = cfxZones.getZoneByName("limitedAirframesConfig") if not theZone then - trigger.action.outText("***LimA: NO config zone!", 30) - return + if limitedAirframes.verbose then + trigger.action.outText("+++limA: NO config zone!", 30) + end + theZone = cfxZones.createSimpleZone("limitedAirframesConfig") end + -- remember me + limitedAirframes.config = theZone - trigger.action.outText("LimA: found config zone!", 30) + limitedAirframes.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) + + if limitedAirframes.verbose then + trigger.action.outText("+++limA: found config zone!", 30) + end -- ok, for each property, load it if it exists if cfxZones.hasProperty(theZone, "enabled") then @@ -130,14 +148,34 @@ function limitedAirframes.readConfigZone() limitedAirframes.maxBlue = cfxZones.getNumberFromZoneProperty(theZone, "maxBlue", -1) end + limitedAirframes.numRed = cfxZones.getStringFromZoneProperty(theZone, "#red", "*none") + limitedAirframes.numBlue = cfxZones.getStringFromZoneProperty(theZone, "#blue", "*none") - if cfxZones.hasProperty(theZone, "redWinsFlag") then - limitedAirframes.redWinsFlag = cfxZones.getStringFromZoneProperty(theZone, "redWinsFlag", "999") + + limitedAirframes.redWinsFlag = cfxZones.getStringFromZoneProperty(theZone, "redWins!", "*none") + + if cfxZones.hasProperty(theZone, "redWinsFlag!") then + limitedAirframes.redWinsFlag = cfxZones.getStringFromZoneProperty(theZone, "redWinsFlag!", "*none") end - if cfxZones.hasProperty(theZone, "blueWinsFlag") then - limitedAirframes.blueWinsFlag = cfxZones.getStringFromZoneProperty(theZone, "blueWinsFlag", "998") + limitedAirframes.blueWinsFlag = cfxZones.getStringFromZoneProperty(theZone, "blueWins!", "*none") + if cfxZones.hasProperty(theZone, "blueWinsFlag!") then + limitedAirframes.blueWinsFlag = cfxZones.getStringFromZoneProperty(theZone, "blueWinsFlag!", "*none") end + + limitedAirframes.method = cfxZones.getStringFromZoneProperty(theZone, "method", "inc") + + if cfxZones.hasProperty(theZone, "warningSound") then + limitedAirframes.warningSound = cfxZones.getStringFromZoneProperty(theZone, "warningSound", "none") + end + + if cfxZones.hasProperty(theZone, "winSound") then + limitedAirframes.winSound = cfxZones.getStringFromZoneProperty(theZone, "winSound", "none") + end + + if cfxZones.hasProperty(theZone, "loseSound") then + limitedAirframes.loseSound = cfxZones.getStringFromZoneProperty(theZone, "loseSound", "none") + end end -- @@ -206,17 +244,17 @@ end function limitedAirframes.updatePlayer(pName, status) if not pName then - trigger.action.outText("+++lim: WARNING - NIL pName in updatePlayer for status " .. status, 30) + trigger.action.outText("+++limA: WARNING - NIL pName in updatePlayer for status " .. status, 30) return end local desc = "" if not limitedAirframes.players[pName] then - desc = "+++lim: NEW player " .. pName .. ": " .. status + desc = "+++limA: NEW player " .. pName .. ": " .. status else if limitedAirframes.players[pName] ~= status then - desc = "+++lim: CHANGE player " .. pName .. " " .. limitedAirframes.players[pName] .. " -> " .. status + desc = "+++limA: CHANGE player " .. pName .. " " .. limitedAirframes.players[pName] .. " -> " .. status else - desc = "+++: player " .. pName .. " no change (" .. status .. ")" + desc = "+++limA: player " .. pName .. " no change (" .. status .. ")" end end @@ -267,16 +305,13 @@ function limitedAirframes.preProcessor(event) if event.id == 6 then -- Eject, plane already divorced from player if limitedAirframes.isKnownUnitName(uName) then - --trigger.action.outText("limAir: detected EJECT for player unit " .. uName .. " player " .. limitedAirframes.getKnownUnitPilotByUnitName(uName), 30) return true end return false -- no longer of interest end if event.id == 5 then -- crash, plane no longer attached to player - if limitedAirframes.isKnownUnitName(uName) then - --trigger.action.outText("limAir: detected CRASH for player unit " .. uName .. " player " .. limitedAirframes.getKnownUnitPilotByUnitName(uName), 30) return true end return false -- no longer of interest @@ -329,19 +364,10 @@ function limitedAirframes.somethingHappened(event) local ID = event.id local myType = theUnit:getTypeName() - -- "20" event (player enter): always processed ---[[-- if ID == 20 or ID == 15 then -- player entered unit - limitedAirframes.addPlayerUnit(theUnit) -- will also update player and player status to 'alive' - -- now procc a 'cheater' since we entered a new airframe/pilot - limitedAirframes.checkPlayerFrameAvailability(event) - return - end ---]]-- if ID == 20 then -- 20 ENTER UNIT local pName = limitedAirframes.getKnownUnitPilotByUnit(theUnit) if not pName then pName = "***UNKNOWN***" end - --trigger.action.outText("limAir: Received ENTER UNIT (20) for " .. pName .. " in " .. unitName , 30) return end @@ -354,7 +380,9 @@ function limitedAirframes.somethingHappened(event) limitedAirframes.unitFlownByPlayer[unitName] = playerName -- TODO: make sure this is the ONLY plane the player -- is registered under, and mark mismatches - trigger.action.outText("limAir: 15 -- player " .. playerName .. " now in " .. unitName, 30) + if limitedAirframes.verbose then + trigger.action.outText("limAir: 15 -- player " .. playerName .. " now in " .. unitName, 30) + end return end @@ -391,7 +419,9 @@ function limitedAirframes.somethingHappened(event) limitedAirframes.pilotDied(theUnit) return else - trigger.action.outText("limAir: Crash of airframe detected - but player status wasn't alive (" .. pStatus .. ")", 30) + if limitedAirframes.verbose then + trigger.action.outText("limAir: Crash of airframe detected - but player status wasn't alive (" .. pStatus .. ")", 30) + end return end end @@ -402,8 +432,9 @@ function limitedAirframes.somethingHappened(event) -- remove pilot name from unit name limitedAirframes.unitFlownByPlayer[unitName] = nil --trigger.action.outText("limAir: 21 -- unit " .. unitName .. " unoccupied", 30) - - trigger.action.outText("limAir: 21 (player left) for unit " .. unitName , 30) + if limitedAirframes.verbose then + trigger.action.outText("limAir: 21 (player left) for unit " .. unitName , 30) + end -- player left unit. Happens twice -- check if player alive, else we have a ditch. limitedAirframes.handlePlayerLeftUnit(event) @@ -415,7 +446,9 @@ function limitedAirframes.somethingHappened(event) --trigger.action.outText("limAir: 9 (PILOT DEAD) for unit " .. unitName , 30) local thePilot = limitedAirframes.unitFlownByPlayer[unitName] if not thePilot then - trigger.action.outText("+++limAir: 9 O'RIDE -- unit " .. unitName .. " was legally vacated before!", 30) + if limitedAirframes.verbose then + trigger.action.outText("+++limAir: 9 O'RIDE -- unit " .. unitName .. " was legally vacated before!", 30) + end return end limitedAirframes.pilotDied(theUnit) @@ -426,8 +459,6 @@ function limitedAirframes.somethingHappened(event) return end - - trigger.action.outText("limAir: WARNING unhandled: " .. ID .. " for player unit " .. theUnit:getName() .. " of type " .. myType, 30) end @@ -474,9 +505,11 @@ function limitedAirframes.handlePlayerLeftUnit(event) if theSafeZone.owner then -- owned zone. olny allow in neutral or owned by same side isSafe = isSafe and (mySide == theSafeZone.owner or theSafeZone.owner == 0) - trigger.action.outText("+++: Lim - " .. theSafeZone.name .. " ownership: myside = " .. mySide .. " zone owner is " .. theSafeZone.owner, 30) + if limitedAirframes.verbose then + trigger.action.outText("+++limA: " .. theSafeZone.name .. " ownership: myside = " .. mySide .. " zone owner is " .. theSafeZone.owner, 30) + end else - -- trigger.action.outText("+++: Zone " .. theSafeZone.name .. " has no ownership, skipping check", 30) + end -- check we are at rest below 10m height. agl may give @@ -492,7 +525,9 @@ function limitedAirframes.handlePlayerLeftUnit(event) if isInAir then isSafe = false end if isSafe then - trigger.action.outTextForCoalition(mySide, "limAir: Pilot " .. theUnit:getPlayerName() .. " left unit " .. theUnit:getName() .. " legally in zone " .. theSafeZone.name, 30) +-- if limitedAirframes.verbose then + trigger.action.outTextForCoalition(mySide, "Pilot " .. theUnit:getPlayerName() .. " left unit " .. theUnit:getName() .. " legally in zone " .. theSafeZone.name, 30) +-- end -- remove from known player planes -- no more limitedAirframes.removePlayerUnit(theUnit) return; @@ -500,7 +535,10 @@ function limitedAirframes.handlePlayerLeftUnit(event) end -- ditched outside safe harbour - trigger.action.outTextForCoalition(mySide, "Pilot " .. theUnit:getPlayerName() .. " DITCHED unit " .. theUnit:getName() .. " -- PILOT LOSS (MIA)", 30) +-- if limitedAirframes.verbose then + trigger.action.outTextForCoalition(mySide, "Pilot " .. theUnit:getPlayerName() .. " DITCHED unit " .. theUnit:getName() .. " -- PILOT is considered MIA", 30) +-- end + limitedAirframes.pilotLost(theUnit) if csarManager and csarManager.airframeDitched then csarManager.airframeDitched(theUnit) @@ -560,19 +598,22 @@ function limitedAirframes.pilotLost(theUnit) if limitedAirframes.maxRed < 0 then return false end -- disabled/infinite limitedAirframes.currRed = limitedAirframes.currRed - 1 + -- pass it along + cfxZones.setFlagValueMult(limitedAirframes.numRed, limitedAirframes.currRed, limitedAirframes.config) if limitedAirframes.currRed == 0 then trigger.action.outTextForCoalition(theSide, "\nYou have lost almost all of your pilots.\n\nWARNING: Losing any more pilots WILL FAIL THE MISSION\n", 30) - trigger.action.outSoundForCoalition(theSide, "Quest Snare 3.wav") + trigger.action.outSoundForCoalition(theSide, limitedAirframes.warningSound)--"Quest Snare 3.wav") return false end if limitedAirframes.currRed < 0 then -- red have lost all airframes trigger.action.outText("\nREDFORCE has lost all of their pilots.\n\nBLUEFORCE WINS!\n", 30) - trigger.action.outSoundForCoalition(theSide, "Death PIANO.wav") - trigger.action.outSoundForCoalition(theOtherSide, "Triumphant Victory.wav") - trigger.action.setUserFlag(limitedAirframes.blueWinsFlag, 1 ) + trigger.action.outSoundForCoalition(theSide, limitedAirframes.loseSound) --"Death PIANO.wav") + trigger.action.outSoundForCoalition(theOtherSide, limitedAirframes.winSound)--"Triumphant Victory.wav") +-- trigger.action.setUserFlag(limitedAirframes.blueWinsFlag, 1 ) + cfxZones.pollFlag(limitedAirframes.blueWinsFlag, limitedAirframes.method, limitedAirframes.config) return true end @@ -581,20 +622,24 @@ function limitedAirframes.pilotLost(theUnit) theOtherSide = 1 if limitedAirframes.maxBlue < 0 then return false end -- disabled/infinite limitedAirframes.currBlue = limitedAirframes.currBlue - 1 + -- pass it along + cfxZones.setFlagValueMult(limitedAirframes.numBlue, limitedAirframes.currBlue, limitedAirframes.config) + if limitedAirframes.currBlue == 0 then trigger.action.outTextForCoalition(theSide, "\nYou have lost almost all of your pilots.\n\nWARNING: Losing any more pilots WILL FAIL THE MISSION\n", 30) - trigger.action.outSoundForCoalition(theSide, "Quest Snare 3.wav") + trigger.action.outSoundForCoalition(theSide, limitedAirframes.warningSound)--"Quest Snare 3.wav") return false end if limitedAirframes.currBlue < 0 then -- red have lost all airframes trigger.action.outText("\nBLUEFORCE has lost all of their pilots.\n\nREDFORCE WINS!\n", 30) - trigger.action.setUserFlag(limitedAirframes.redWinsFlag, 1 ) - trigger.action.outSoundForCoalition(theSide, "Death PIANO.wav") - trigger.action.outSoundForCoalition(theOtherSide, "Triumphant Victory.wav") +-- trigger.action.setUserFlag(limitedAirframes.redWinsFlag, 1 ) + cfxZones.pollFlag(limitedAirframes.redWinsFlag, limitedAirframes.method, limitedAirframes.config) + trigger.action.outSoundForCoalition(theSide, limitedAirframes.loseSound)--"Death PIANO.wav") + trigger.action.outSoundForCoalition(theOtherSide, limitedAirframes.winSound)--"Triumphant Victory.wav") return true end - trigger.action.outSoundForCoalition(theSide, "Quest Snare 3.wav") + trigger.action.outSoundForCoalition(theSide, limitedAirframes.warningSound)--"Quest Snare 3.wav") trigger.action.outTextForCoalition(theSide, "You have lost a pilot! Remaining: " .. limitedAirframes.currBlue, 30) end return false @@ -643,12 +688,31 @@ function limitedAirframes.addSafeZone(aZone) -- add zone to my list limitedAirframes.safeZones[aZone] = aZone - aZone.redSafe = true - aZone.redSafe = cfxZones.getBoolFromZoneProperty(aZone, "redSafe", true) - aZone.blueSafe = true - aZone.blueSafe = cfxZones.getBoolFromZoneProperty(aZone, "blueSafe", true) - trigger.action.outText("limAir: added safeZone " .. aZone.name, 30) + -- deprecated old code. new code contains 'red, blue' in value for pilotsafe + local safeSides = cfxZones.getStringFromZoneProperty(aZone, "pilotsafe", "") + safeSides = safeSides:lower() + if dcsCommon.containsString(safeSides, "red") or dcsCommon.containsString(safeSides, "blue") then + aZone.redSafe = dcsCommon.containsString(safeSides, "red") + aZone.blueSafe = dcsCommon.containsString(safeSides, "blue") + else + --aZone.redSafe = true + aZone.redSafe = cfxZones.getBoolFromZoneProperty(aZone, "redSafe", true) + --aZone.blueSafe = true + aZone.blueSafe = cfxZones.getBoolFromZoneProperty(aZone, "blueSafe", true) + end + + if limitedAirframes.verbose or aZone.verbose then + if aZone.redSafe then + trigger.action.outText("+++limA: <" .. aZone.name .. "> is safe for RED pilots", 30) + end + if aZone.blueSafe then + trigger.action.outText("+++limA: <" .. aZone.name .. "> is safe for BLUE pilots", 30) + end + + trigger.action.outText("+++limA: added safeZone " .. aZone.name, 30) + + end end @@ -704,7 +768,7 @@ function limitedAirframes.doAirframeScore(args) local msg = "\nRED has " .. redRemaining .. " pilots left,\nBLUE has " .. blueRemaining .. " pilots left\n" trigger.action.outText(msg, 30, true) - trigger.action.outSound("Quest Snare 3.wav") + trigger.action.outSound(limitedAirframes.warningSound)--"Quest Snare 3.wav") end function limitedAirframes.redirectToggleAirFrames(args) @@ -717,23 +781,26 @@ function limitedAirframes.doToggleAirFrames(args) local desc = "\n\nPilot Count rule NOW IN EFFECT\n\n" if limitedAirframes.enabled then - trigger.action.outSound("Quest Snare 3.wav") + trigger.action.outSound(limitedAirframes.warningSound)--"Quest Snare 3.wav") else desc = "\n\nYou cowardly disabled Pilot Count\n\n" - trigger.action.outSound("Death PIANO.wav") + trigger.action.outSound(limitedAirframes.loseSound)--"Death PIANO.wav") end trigger.action.outText(desc, 30) limitedAirframes.setCommsMenu() end -- --- CSAR CALLBACK +-- CSAR CALLBACK (called by CSAR Manager) -- function limitedAirframes.pilotsRescued(theCoalition, success, numRescued, notes) local availablePilots = 0 if theCoalition == 1 then -- red limitedAirframes.currRed = limitedAirframes.currRed + numRescued + -- pass it along + cfxZones.setFlagValueMult(limitedAirframes.numRed, limitedAirframes.currRed, limitedAirframes.config) + if limitedAirframes.currRed > limitedAirframes.maxRed then limitedAirframes.currRed = limitedAirframes.maxRed end @@ -745,6 +812,10 @@ function limitedAirframes.pilotsRescued(theCoalition, success, numRescued, notes if theCoalition == 2 then -- blue limitedAirframes.currBlue = limitedAirframes.currBlue + numRescued + -- pass it along + cfxZones.setFlagValueMult(limitedAirframes.numBlue, limitedAirframes.currBlue, limitedAirframes.config) + + if limitedAirframes.currBlue > limitedAirframes.maxBlue then limitedAirframes.currBlue = limitedAirframes.maxBlue end @@ -754,7 +825,7 @@ function limitedAirframes.pilotsRescued(theCoalition, success, numRescued, notes end end trigger.action.outTextForCoalition(theCoalition, "\nPilots returned to flight line, you now have " .. availablePilots..".\n", 30) - trigger.action.outSoundForCoalition(theCoalition, "Quest Snare 3.wav") + trigger.action.outSoundForCoalition(theCoalition, limitedAirframes.warningSound)--"Quest Snare 3.wav") end -- @@ -770,6 +841,10 @@ function limitedAirframes.start() -- override config settings if defined as zone limitedAirframes.readConfigZone() + -- set output flags + cfxZones.setFlagValueMult(limitedAirframes.numBlue, limitedAirframes.currBlue, limitedAirframes.config) + cfxZones.setFlagValueMult(limitedAirframes.numRed, limitedAirframes.currRed, limitedAirframes.config) + -- collect all zones that are airframe safe local afsZones = cfxZones.zonesWithProperty("pilotSafe") @@ -779,6 +854,27 @@ function limitedAirframes.start() limitedAirframes.addSafeZone(aZone) end + -- check that sides with limited airframes also have at least one + -- pilotsafe zone + if limitedAirframes.maxRed > 0 then + local safeAndSound = false + for idx, theZone in pairs(limitedAirframes.safeZones) do + if theZone.redSafe then safeAndSound = true end + end + if not safeAndSound then + trigger.action.outText("+++limA: WARNING - RED has no safe zone to change air frames", 30) + end + end + if limitedAirframes.maxBlue > 0 then + local safeAndSound = false + for idx, theZone in pairs(limitedAirframes.safeZones) do + if theZone.blueSafe then safeAndSound = true end + end + if not safeAndSound then + trigger.action.outText("+++limA: WARNING - BLUE has no safe zone to change air frames", 30) + end + end + -- connect player callback -- install callbacks for airframe-related events dcsCommon.addEventHandler(limitedAirframes.somethingHappened, limitedAirframes.preProcessor, limitedAirframes.postProcessor) @@ -805,13 +901,13 @@ function limitedAirframes.start() -- connect to csarManager if present if csarManager and csarManager.installCallback then csarManager.installCallback(limitedAirframes.pilotsRescued) - trigger.action.outText("+++lim: connected to csar manager", 30) + trigger.action.outText("+++limA: connected to csar manager", 30) else - trigger.action.outText("+++lim: NO CSAR integration", 30) + trigger.action.outText("+++limA: NO CSAR integration", 30) end -- say hi - trigger.action.outText("limitedAirframes v" .. limitedAirframes.version .. " started: R:".. limitedAirframes.maxRed .. "/B:" .. limitedAirframes.maxBlue, 30) + trigger.action.outText("cf/x Limited Airframes v" .. limitedAirframes.version .. " started: R:".. limitedAirframes.maxRed .. "/B:" .. limitedAirframes.maxBlue, 30) return true end @@ -825,4 +921,5 @@ end --[[-- safe ditch: check airspeed and altitude. ditch only counts if less than 10m and 2 kts report number of airframes left via second instance in switch off menu + so it can report only one side --]]-- \ No newline at end of file diff --git a/modules/pulseFlags.lua b/modules/pulseFlags.lua index 6b0db74..29dbc6d 100644 --- a/modules/pulseFlags.lua +++ b/modules/pulseFlags.lua @@ -1,5 +1,5 @@ pulseFlags = {} -pulseFlags.version = "1.2.0" +pulseFlags.version = "1.2.1" pulseFlags.verbose = false pulseFlags.requiredLibs = { "dcsCommon", -- always @@ -29,6 +29,9 @@ pulseFlags.requiredLibs = { pulseStopped synonym - 1.2.0 DML Watchflag integration corrected bug in loading last pulse value for paused + - 1.2.1 pulseInterval synonym for time + pulses now supports range + zone-local verbosity --]]-- @@ -53,11 +56,30 @@ function pulseFlags.createPulseWithZone(theZone) -- time can be number, or number-number range theZone.minTime, theZone.time = cfxZones.getPositiveRangeFromZoneProperty(theZone, "time", 1) - if pulseFlags.verbose then - trigger.action.outText("***PulF: zone <" .. theZone.name .. "> time is <".. theZone.minTime ..", " .. theZone.time .. "!", 30) + if cfxZones.hasProperty(theZone, "pulseInterval") then + theZone.minTime, theZone.time = cfxZones.getPositiveRangeFromZoneProperty(theZone, "pulseInterval", 1) + end + + if pulseFlags.verbose or theZone.verbose then + trigger.action.outText("+++pulF: zone <" .. theZone.name .. "> time is <".. theZone.minTime ..", " .. theZone.time .. "!", 30) end - theZone.pulses = cfxZones.getNumberFromZoneProperty(theZone, "pulses", -1) + + theZone.pulses = -1 -- set to infinite + if cfxZones.hasProperty(theZone, "pulses") then + local minP + local maxP + minP, maxP = cfxZones.getPositiveRangeFromZoneProperty(theZone, "pulses", 1) + if minP == maxP then theZone.pulses = minP + else + theZone.pulses = cfxZones.randomInRange(minP, maxP) + end + end + + if pulseFlags.verbose or theZone.verbose then + trigger.action.outText("+++pulF: zone <" .. theZone.name .. "> set to <" .. theZone.pulses .. "> pulses", 30) + end + theZone.pulsesLeft = 0 -- will start new cycle -- watchflag: @@ -133,7 +155,7 @@ function pulseFlags.doPulse(args) -- do a poll on flags -- first, we only do an initial pulse if zeroPulse is set if theZone.hasPulsed or theZone.zeroPulse then - if pulseFlags.verbose then + if pulseFlags.verbose or theZone.verbose then trigger.action.outText("+++pulF: will bang " .. theZone.pulseFlag, 30); end @@ -151,8 +173,8 @@ function pulseFlags.doPulse(args) --local currVal = cfxZones.getFlagValue(theZone.pulseDoneFlag, theZone)-- trigger.misc.getUserFlag(theZone.pulseDoneFlag) cfxZones.pollFlag(theZone.pulseDoneFlag, "inc", theZone) -- trigger.action.setUserFlag(theZone.pulseDoneFlag, currVal + 1) end - if pulseFlags.verbose then - trigger.action.outText("***PulF: pulse <" .. theZone.name .. "> ended!", 30) + if pulseFlags.verbose or theZone.verbose then + trigger.action.outText("+++pulF: pulse <" .. theZone.name .. "> ended!", 30) end theZone.pulsing = false theZone.pulsePaused = true @@ -160,8 +182,8 @@ function pulseFlags.doPulse(args) end end else - if pulseFlags.verbose then - trigger.action.outText("***PulF: pulse <" .. theZone.name .. "> delaying zero pulse!", 30) + if pulseFlags.verbose or theZone.verbose then + trigger.action.outText("+++pulF: pulse <" .. theZone.name .. "> delaying zero pulse!", 30) end end @@ -173,8 +195,8 @@ function pulseFlags.doPulse(args) -- schedule in delay time theZone.timerID = timer.scheduleFunction(pulseFlags.doPulse, args, timer.getTime() + delay) - if pulseFlags.verbose then - trigger.action.outText("+++PulF: pulse <" .. theZone.name .. "> rescheduled in " .. delay, 30) + if pulseFlags.verbose or theZone.verbose then + trigger.action.outText("+++pulF: pulse <" .. theZone.name .. "> rescheduled in " .. delay, 30) end end @@ -184,8 +206,8 @@ function pulseFlags.startNewPulse(theZone) theZone.pulsesLeft = theZone.pulses local args = {theZone} theZone.pulsing = true - if pulseFlags.verbose then - trigger.action.outText("+++PulF: starting pulse <" .. theZone.name .. ">", 30) + if pulseFlags.verbose or theZone.verbose then + trigger.action.outText("+++pulF: starting pulse <" .. theZone.name .. ">", 30) end pulseFlags.doPulse(args) end @@ -214,30 +236,16 @@ function pulseFlags.update() -- see if we got a pause or activate command -- activatePulseFlag if cfxZones.testZoneFlag(aZone, aZone.activatePulseFlag, aZone.pulseTriggerMethod, "lastActivateValue") then - if pulseFlags.verbose then - trigger.action.outText("+++PulF: activating <" .. aZone.name .. ">", 30) + if pulseFlags.verbose or aZone.verbose then + trigger.action.outText("+++pulF: activating <" .. aZone.name .. ">", 30) end aZone.pulsePaused = false -- will start anew end - ---[[-- -- old code - if aZone.activatePulseFlag then - local currTriggerVal = cfxZones.getFlagValue(aZone.activatePulseFlag, aZone) -- trigger.misc.getUserFlag(aZone.activatePulseFlag) - if currTriggerVal ~= aZone.lastActivateValue - then - if pulseFlags.verbose then - trigger.action.outText("+++PulF: activating <" .. aZone.name .. ">", 30) - end - aZone.lastActivateValue = currTriggerVal - aZone.pulsePaused = false -- will start anew - end - end ---]]-- -- pausePulseFlag if cfxZones.testZoneFlag(aZone, aZone.pausePulseFlag, aZone.pulseTriggerMethod, "lastPauseValue") then - if pulseFlags.verbose then - trigger.action.outText("+++PulF: pausing <" .. aZone.name .. ">", 30) + if pulseFlags.verbose or aZone.verbose then + trigger.action.outText("+++pulF: pausing <" .. aZone.name .. ">", 30) end aZone.pulsePaused = true -- prevents new start if aZone.timerID then @@ -245,24 +253,7 @@ function pulseFlags.update() aZone.timerID = nil end end ---[[-- - -- old colde - if aZone.pausePulseFlag then - local currTriggerVal = cfxZones.getFlagValue(aZone.pausePulseFlag, aZone)-- trigger.misc.getUserFlag(aZone.pausePulseFlag) - if currTriggerVal ~= aZone.lastPauseValue - then - if pulseFlags.verbose then - trigger.action.outText("+++PulF: pausing <" .. aZone.name .. ">", 30) - end - aZone.lastPauseValue = currTriggerVal - aZone.pulsePaused = true -- prevents new start - if aZone.timerID then - timer.removeFunction(aZone.timerID) - aZone.timerID = nil - end - end - end ---]]-- + end end @@ -274,7 +265,7 @@ function pulseFlags.readConfigZone() local theZone = cfxZones.getZoneByName("pulseFlagsConfig") if not theZone then if pulseFlags.verbose then - trigger.action.outText("+++PulF: NO config zone!", 30) + trigger.action.outText("+++pulF: NO config zone!", 30) end return end @@ -282,7 +273,7 @@ function pulseFlags.readConfigZone() pulseFlags.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) if pulseFlags.verbose then - trigger.action.outText("+++PulF: read config", 30) + trigger.action.outText("+++pulF: read config", 30) end end @@ -302,7 +293,8 @@ function pulseFlags.start() -- process RND Zones local attrZones = cfxZones.getZonesWithAttributeNamed("pulse") - +-- local a = dcsCommon.getSizeOfTable(attrZones) +-- trigger.action.outText("pulse zones: " .. a, 30) -- now create a pulse gen for each one and add them -- to our watchlist for k, aZone in pairs(attrZones) do @@ -310,8 +302,9 @@ function pulseFlags.start() pulseFlags.addPulse(aZone) -- remember it so we can pulse it end - local attrZones = cfxZones.getZonesWithAttributeNamed("pulse!") - + attrZones = cfxZones.getZonesWithAttributeNamed("pulse!") + a = dcsCommon.getSizeOfTable(attrZones) + trigger.action.outText("pulse! zones: " .. a, 30) -- now create a pulse gen for each one and add them -- to our watchlist for k, aZone in pairs(attrZones) do diff --git a/tutorial & demo missions/demo - helo cargo.miz b/tutorial & demo missions/demo - helo cargo.miz index 09f7552..902901f 100644 Binary files a/tutorial & demo missions/demo - helo cargo.miz and b/tutorial & demo missions/demo - helo cargo.miz differ diff --git a/tutorial & demo missions/demo - pilots at their limit.miz b/tutorial & demo missions/demo - pilots at their limit.miz new file mode 100644 index 0000000..4c9a7b4 Binary files /dev/null and b/tutorial & demo missions/demo - pilots at their limit.miz differ