diff --git a/Doc/DML Documentation.pdf b/Doc/DML Documentation.pdf index a878581..5054466 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 a43ed52..e7dea8e 100644 Binary files a/Doc/DML Quick Reference.pdf and b/Doc/DML Quick Reference.pdf differ diff --git a/modules/FARPZones.lua b/modules/FARPZones.lua index faa56e4..ae5b590 100644 --- a/modules/FARPZones.lua +++ b/modules/FARPZones.lua @@ -1,5 +1,5 @@ FARPZones = {} -FARPZones.version = "1.1.0" +FARPZones.version = "1.2.0" FARPZones.verbose = false --[[-- Version History @@ -11,6 +11,9 @@ FARPZones.verbose = false - rFormation attribute added - verbose flag - verbose cleanup ("FZ: something happened") + 1.2.0 - persistence + - handles contested state + --]]-- @@ -75,7 +78,7 @@ FARPZones.spinUpDelay = 30 -- seconds until FARP becomes operational after captu FARPZones.allFARPZones = {} -FARPZones.startingUp = false +FARPZones.startingUp = false -- not needed / read anywhere -- FARP ZONE ACCESS function FARPZones.addFARPZone(aFARP) @@ -90,6 +93,15 @@ function FARPZones.getFARPForZone(aZone) return FARPZones.allFARPZones[aZone] end +function FARPZones.getFARPZoneByName(aName) + for aZone, aFarp in pairs(FARPZones.allFARPZones) do + if aZone.name == aName then return aFarp end + -- we assume zone.name == farp.name + end + trigger.action.outText("Unable to find FARP <" .. aName .. ">", 30) + return nil +end + function FARPZones.getFARPZoneForFARP(aFarp) -- find the first FARP zone that associates with -- aFARP (an airField) @@ -123,9 +135,9 @@ function FARPZones.createFARPFromZone(aZone) if #mapFarps == 0 then trigger.action.outText("***Farp Zones: no FARP found for zone " .. aZone.name, 30) else - for idx, aFarp in pairs(mapFarps) do + --for idx, aFarp in pairs(mapFarps) do -- trigger.action.outText("Associated FARP " .. aFarp:getName() .. " with FARP Zone " .. aZone.name, 30) - end + --end theFarp.mainFarp = theFarp.myFarps[1] theFarp.point = theFarp.mainFarp:getPoint() -- this is FARP, not zone!!! @@ -317,7 +329,7 @@ function FARPZones.produceVehicles(theFarp) local theCoalition = theFarp.owner if theTypes ~= "none" then - local theGroup = cfxZones.createGroundUnitsInZoneForCoalition ( + local theGroup, theData = cfxZones.createGroundUnitsInZoneForCoalition ( theCoalition, theFarp.name .. "-D" .. theFarp.count, -- must be unique theFarp.defZone, @@ -326,10 +338,11 @@ function FARPZones.produceVehicles(theFarp) theFarp.defHeading) -- we do not add these troops to ground troop management theFarp.defenders = theGroup -- but we retain a handle just in case + theFarp.defenderData = theData end unitTypes = FARPZones.resourceTypes - local theGroup = cfxZones.createGroundUnitsInZoneForCoalition ( + local theGroup, theData = cfxZones.createGroundUnitsInZoneForCoalition ( theCoalition, theFarp.name .. "-R" .. theFarp.count, -- must be unique theFarp.resZone, @@ -337,7 +350,7 @@ function FARPZones.produceVehicles(theFarp) "line_v", theFarp.resHeading) theFarp.resources = theGroup - + theFarp.resourceData = theData -- update unique counter theFarp.count = theFarp.count + 1 end @@ -382,6 +395,23 @@ function FARPZones.somethingHappened(event) end local newOwner = aFarp:getCoalition() + -- now, because we can load from file, we may get a notice + -- that a newly loaded state disagrees with new game state + -- if so, we simply wink and exit + if newOwner == zonedFarp.owner then + trigger.action.outText("FARP <" .. zonedFarp.name .. "> aligned with persisted data", 30) + return + end + + -- let's ignore the owner = 3 (contested). Usually does not + -- happen with an event, but let's be prepared + if newOwner == 3 then + if FARPZones.verbose then + trigger.action.outText("FARP <" .. zonedFarp.name .. "> has become contested", 30) + end + return + end + local blueRed = "Red" if newOwner == 2 then blueRed = "Blue" end trigger.action.outText("FARP " .. zonedFarp.zone.name .. " captured by " .. blueRed .."!", 30) @@ -408,6 +438,80 @@ function FARPZones.somethingHappened(event) end +-- +-- LOAD / SAVE +-- +function FARPZones.saveData() + local theData = {} + if FARPZones.verbose then + trigger.action.outText("+++frpZ: enter saveData", 30) + end + + local farps = {} + -- iterate all farp data and put them into a container each + for theZone, theFARP in pairs(FARPZones.allFARPZones) do + fName = theZone.name + trigger.action.outText("frpZ persistence: processing FARP <" .. fName .. ">", 30) + local fData = {} + fData.owner = theFARP.owner + fData.defenderData = dcsCommon.clone(theFARP.defenderData) + fData.resourceData = dcsCommon.clone(theFARP.resourceData) + dcsCommon.synchGroupData(fData.defenderData) + if fData.defenderData and #fData.defenderData.units<1 then + fData.defenderData = nil + end + dcsCommon.synchGroupData(fData.resourceData) + if fData.resourceData and #fData.resourceData.units<1 then + fData.resourceData = nil + end + farps[fName] = fData + end + + theData.farps = farps + return theData +end + +function FARPZones.loadMission() + local theData = persistence.getSavedDataForModule("FARPZones") + if not theData then + if FARPZones.verbose then + trigger.action.outText("frpZ: no save date received, skipping.", 30) + end + return + end + + local farps = theData.farps + if farps then + for fName, fData in pairs(farps) do + local theFARP = FARPZones.getFARPZoneByName(fName) + if theFARP then + theFARP.owner = fData.owner + theFARP.zone.owner = fData.owner + theFARP.defenderData = dcsCommon.clone(fData.defenderData) + local groupData = fData.defenderData + if groupData and #groupData.units > 0 then + local cty = groupData.cty + local cat = groupData.cat + theFARP.defenders = coalition.addGroup(cty, cat, groupData) + end + + groupData = fData.resourceData + if groupData and #groupData.units > 0 then + local cty = groupData.cty + local cat = groupData.cat + theFARP.resources = coalition.addGroup(cty, cat, groupData) + end + FARPZones.drawFARPCircleInMap(theFARP) -- mark in map + if (not theFARP.defenders) and (not theFARP.resources) then + -- we instigate a resource and defender drop + FARPZones.produceVehicles(theFARP) + end + else + trigger.action.outText("frpZ: persistence: FARP <" .. fName .. "> no longer exists in mission, skipping", 30) + end + end + end +end -- -- Start @@ -439,7 +543,7 @@ function FARPZones.start() return false end - FARPZones.startingUp = true + FARPZones.startingUp = true -- not needed / read anywhere -- read config zone FARPZones.readConfig() @@ -449,19 +553,41 @@ function FARPZones.start() FARPZones.preProcessor, FARPZones.postProcessor) + -- set up persistence BEFORE we read zones, so weh know the + -- score during init phase + local hasSaveData = false + if persistence then + -- sign up for persistence + callbacks = {} + callbacks.persistData = FARPZones.saveData + persistence.registerModule("FARPZones", callbacks) + hasSaveData = persistence.hasData + end + -- collect all FARP Zones local theZones = cfxZones.getZonesWithAttributeNamed("FARP") for k, aZone in pairs(theZones) do local aFARP = FARPZones.createFARPFromZone(aZone) -- read attributes from DCS FARPZones.addFARPZone(aFARP) -- add to managed zones - FARPZones.drawFARPCircleInMap(aFARP) -- mark in map - FARPZones.produceVehicles(aFARP) -- allocate initial vehicles + -- moved FARPZones.drawFARPCircleInMap(aFARP) -- mark in map + -- moved FARPZones.produceVehicles(aFARP) -- allocate initial vehicles if FARPZones.verbose then trigger.action.outText("processed FARP <" .. aZone.name .. "> now owned by " .. aZone.owner, 30) end end - FARPZones.startingUp = false + -- now produce all vehicles - whether from + -- save, or clean from start + if hasSaveData then + FARPZones.loadMission() + else + for idx, aFARP in pairs (FARPZones.allFARPZones) do + FARPZones.drawFARPCircleInMap(aFARP) -- mark in map + FARPZones.produceVehicles(aFARP) -- allocate initial vehicles + end + end + + FARPZones.startingUp = false -- not needed / read anywhere trigger.action.outText("cf/x FARP Zones v" .. FARPZones.version .. " started", 30) return true @@ -479,5 +605,7 @@ Improvements: per FARP/Helipad in zone: create resources (i.e. support multi 4-Pad FARPS out of the box make hidden farps only appear for owning side + + make farps repair their service vehicles after a time, or simply refresh them every x minutes, to make the algo simpler --]]-- \ No newline at end of file diff --git a/modules/cfxArtilleryZones.lua b/modules/cfxArtilleryZones.lua index c8ecb89..cbc21cb 100644 --- a/modules/cfxArtilleryZones.lua +++ b/modules/cfxArtilleryZones.lua @@ -297,6 +297,10 @@ function cfxArtilleryZones.doFireAt(aZone, maxDistFromCenter) cfxArtilleryZones.invokeCallbacksFor('fire', aZone, {}) end +-- +-- API main entry call for firing at zone +-- invokes doFireAt() +-- function cfxArtilleryZones.simFireAtZone(aZone, aGroup, dist) if not dist then dist = aZone.spotRange end diff --git a/modules/cfxMX.lua b/modules/cfxMX.lua index 94b393a..93e8759 100644 --- a/modules/cfxMX.lua +++ b/modules/cfxMX.lua @@ -1,5 +1,5 @@ cfxMX = {} -cfxMX.version = "1.2.0" +cfxMX.version = "1.2.1" cfxMX.verbose = false --[[-- Mission data decoder. Access to ME-built mission structures @@ -16,14 +16,15 @@ cfxMX.verbose = false 1.2.0 - added group name reference table - added group type reference - added references for allFixed, allHelo, allGround, allSea, allStatic - + 1.2.1 - added countryByName + - added linkByName - --]]-- cfxMX.groupNamesByID = {} cfxMX.groupIDbyName = {} cfxMX.groupDataByName = {} - +cfxMX.countryByName ={} +cfxMX.linkByName = {} cfxMX.allFixedByName = {} cfxMX.allHeloByName = {} cfxMX.allGroundByName = {} @@ -193,11 +194,20 @@ function cfxMX.createCrossReferences() local category = obj_type_name if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's at least one group! for group_num, group_data in pairs(obj_type_data.group) do + local aName = group_data.name local aID = group_data.groupId + -- get linkUnit info if it exists + local linkUnit = nil + if group_data and group_data.route and group_data.route and group_data.route.points[1] then + linkUnit = group_data.route.points[1].linkUnit + cfxMX.linkByName[aName] = linkUnit + end + cfxMX.groupNamesByID[aID] = aName cfxMX.groupIDbyName[aName] = aID cfxMX.groupDataByName[aName] = group_data + cfxMX.countryByName[aName] = cntry_id -- now make the type-specific xrefs if obj_type_name == "helicopter" then cfxMX.allHeloByName[aName] = group_data diff --git a/modules/cfxObjectDestructDetector.lua b/modules/cfxObjectDestructDetector.lua index 98f9ed9..9fbcc0a 100644 --- a/modules/cfxObjectDestructDetector.lua +++ b/modules/cfxObjectDestructDetector.lua @@ -1,22 +1,24 @@ cfxObjectDestructDetector = {} -cfxObjectDestructDetector.version = "1.2.0" +cfxObjectDestructDetector.version = "1.3.0" +cfxObjectDestructDetector.verbose = false cfxObjectDestructDetector.requiredLibs = { "dcsCommon", -- always "cfxZones", -- Zones, of course } -cfxObjectDestructDetector.verbose = false --[[-- VERSION HISTORY 1.0.0 initial version, based on parashoo, arty zones 1.0.1 fixed bug: trigger.MISC.getUserFlag() 1.1.0 added support for method, f! and destroyed! 1.2.0 DML / Watchflag support + 1.3.0 Persistence support Detect when an object with OBJECT ID as assigned in ME dies *** EXTENDS ZONES --]]-- + cfxObjectDestructDetector.objectZones = {} -- @@ -41,6 +43,14 @@ function cfxObjectDestructDetector.addObjectDetectZone(aZone) table.insert(cfxObjectDestructDetector.objectZones, aZone) end +function cfxObjectDestructDetector.getObjectDetectZoneByName(aName) + for idx, aZone in pairs(cfxObjectDestructDetector.objectZones) do + if aZone.name == aName then return aZone end + end + -- add landHeight to this zone + return nil +end + -- -- processing of zones -- @@ -48,6 +58,10 @@ function cfxObjectDestructDetector.processObjectDestructZone(aZone) aZone.name = cfxZones.getStringFromZoneProperty(aZone, "NAME", aZone.name) -- aZone.coalition = cfxZones.getCoalitionFromZoneProperty(aZone, "coalition", 0) aZone.ID = cfxZones.getNumberFromZoneProperty(aZone, "OBJECT ID", 1) -- THIS! + -- persistence interface + aZone.isDestroyed = false + + --[[-- old code, to be decom'd --]]-- if cfxZones.hasProperty(aZone, "setFlag") then aZone.setFlag = cfxZones.getStringFromZoneProperty(aZone, "setFlag", "999") end @@ -73,16 +87,16 @@ function cfxObjectDestructDetector.processObjectDestructZone(aZone) aZone.decreaseFlag = cfxZones.getStringFromZoneProperty(aZone, "f-1", "999") end - -- new method support - aZone.oddMethod = cfxZones.getStringFromZoneProperty(aZone, "method", "flip") + -- DML method support + aZone.oddMethod = cfxZones.getStringFromZoneProperty(aZone, "method", "inc") if cfxZones.hasProperty(aZone, "oddMethod") then - aZone.oddMethod = cfxZones.getStringFromZoneProperty(aZone, "oddMethod", "flip") + aZone.oddMethod = cfxZones.getStringFromZoneProperty(aZone, "oddMethod", "inc") end - if cfxZones.hasProperty(aZone, "f!") then - aZone.outDestroyFlag = cfxZones.getStringFromZoneProperty(aZone, "f!", "*none") - end + -- we now always have that property + aZone.outDestroyFlag = cfxZones.getStringFromZoneProperty(aZone, "f!", "*none") + if cfxZones.hasProperty(aZone, "destroyed!") then aZone.outDestroyFlag = cfxZones.getStringFromZoneProperty(aZone, "destroyed!", "*none") end @@ -102,7 +116,7 @@ function cfxObjectDestructDetector:onEvent(event) if not id then return end for idx, aZone in pairs(cfxObjectDestructDetector.objectZones) do - if aZone.ID == id then + if (not aZone.isDestroyed) and aZone.ID == id then -- flag manipulation -- OLD FLAG SUPPORT, SOON TO BE REMOVED if aZone.setFlag then @@ -128,7 +142,7 @@ function cfxObjectDestructDetector:onEvent(event) -- invoke callbacks cfxObjectDestructDetector.invokeCallbacksFor(aZone) - if cfxObjectDestructDetector.verbose then + if aZone.verbose or cfxObjectDestructDetector.verbose then trigger.action.outText("OBJECT KILL: " .. id, 30) end @@ -136,6 +150,9 @@ function cfxObjectDestructDetector:onEvent(event) -- for better performance since it cant -- die twice + -- save state for persistence + aZone.isDestroyed = true + return end end @@ -143,8 +160,85 @@ function cfxObjectDestructDetector:onEvent(event) end end --- add event handler +-- +-- persistence: save and load data +-- +function cfxObjectDestructDetector.saveData() -- invoked by persistence + local theData = {} + local zoneInfo = {} + for idx, aZone in pairs(cfxObjectDestructDetector.objectZones) do + -- save all pertinent info. in our case, it's just + -- the isDestroyed and flag info info + info = {} + info.isDestroyed = aZone.isDestroyed + info.outDestroyVal = cfxZones.getFlagValue(aZone.outDestroyFlag, aZone) + zoneInfo[aZone.name] = info + end + -- expasion proof: assign as own field + theData.zoneInfo = zoneInfo + return theData +end + +function cfxObjectDestructDetector.loadMission() + if cfxObjectDestructDetector.verbose then + trigger.action.outText("+++oDDet: persistence - loading data", 30) + end + + local theData = persistence.getSavedDataForModule("cfxObjectDestructDetector") + if not theData then + return + end + + -- iterate the data, and fail graciously if + -- we can't find a zone. it's probably beed edited out + local zoneInfo = theData.zoneInfo + if not zoneInfo then return end + if cfxObjectDestructDetector.verbose then + trigger.action.outText("+++oDDet: persistence - processing data", 30) + end + + for zName, info in pairs (zoneInfo) do + local theZone = cfxObjectDestructDetector.getObjectDetectZoneByName(zName) + if theZone then + theZone.isDestroyed = info.isDestroyed + cfxZones.setFlagValue(theZone.outDestroyFlag, info.outDestroyVal, theZone) + if cfxObjectDestructDetector.verbose or theZone.verbose then + trigger.action.outText("+++oDDet: persistence setting flag <" .. theZone.outDestroyFlag .. "> to <" .. info.outDestroyVal .. ">",30) + end + local theName = tostring(theZone.ID) + if info.isDestroyed then + -- We now get the scenery object in that zone + -- and remove it + -- note that dcsCommon methods use DCS zones, not cfx + local theObject = dcsCommon.getSceneryObjectInZoneByName(theName, theZone.dcsZone) + if theObject then + if cfxObjectDestructDetector.verbose or theZone.verbose then + trigger.action.outText("+++oDDet: persistence removing dead scenery object <" .. theName .. ">",30) + end + theObject:destroy() + else + if cfxObjectDestructDetector.verbose or theZone.verbose then + trigger.action.outText("+++oDDet: persistence - can't find scenery objects <" .. theName .. ">, skipped destruction",30) + end + end + else + if cfxObjectDestructDetector.verbose or theZone.verbose then + trigger.action.outText("+++oDDet: persistence - scenery objects <" .. theName .. "> is healthy",30) + end + end + else + trigger.action.outText("+++oDDet: persistence - can't find detector <" .. zName .. "> on load. skipping", 30) + end + end + if cfxObjectDestructDetector.verbose then + trigger.action.outText("+++oDDet: persistence - processing complete", 30) + end +end + +-- +-- start +-- function cfxObjectDestructDetector.start() if not dcsCommon.libCheck("cfx Object Destruct Detector", @@ -152,20 +246,38 @@ function cfxObjectDestructDetector.start() return false end - -- collect all zones with 'smoke' attribute + -- collect all zones with 'OBJECT id' attribute -- collect all spawn zones local attrZones = cfxZones.getZonesWithAttributeNamed("OBJECT ID") - -- now create a spawner for all, add them to the spawner updater, and spawn for all zones that are not - -- paused + for k, aZone in pairs(attrZones) do cfxObjectDestructDetector.processObjectDestructZone(aZone) -- process attribute and add to zone properties (extend zone) - cfxObjectDestructDetector.addObjectDetectZone(aZone) -- remember it so we can smoke it + cfxObjectDestructDetector.addObjectDetectZone(aZone) end -- add myself as event handler world.addEventHandler(cfxObjectDestructDetector) + -- persistence: see if we have any data to process + -- for all our zones, and sign up for data saving + if persistence and persistence.active then + -- sign up for saves + callbacks = {} + callbacks.persistData = cfxObjectDestructDetector.saveData + persistence.registerModule("cfxObjectDestructDetector", callbacks) + + if persistence.hasData then + cfxObjectDestructDetector.loadMission() + end + else + if cfxObjectDestructDetector.verbose then + trigger.action.outText("no persistence for cfxObjectDestructDetector", 30) + end + end + + + -- say hi trigger.action.outText("cfx Object Destruct Zones v" .. cfxObjectDestructDetector.version .. " started.", 30) return true diff --git a/modules/cfxOwnedZones.lua b/modules/cfxOwnedZones.lua index 59a8890..1d21922 100644 --- a/modules/cfxOwnedZones.lua +++ b/modules/cfxOwnedZones.lua @@ -1,7 +1,8 @@ cfxOwnedZones = {} -cfxOwnedZones.version = "1.1.2" +cfxOwnedZones.version = "1.2.0" cfxOwnedZones.verbose = false cfxOwnedZones.announcer = true +cfxOwnedZones.name = "cfxOwnedZones" --[[-- VERSION HISTORY 1.0.3 - added getNearestFriendlyZone @@ -21,7 +22,7 @@ cfxOwnedZones.announcer = true - support of 'none' type string to indicate no attackers/defenders - updated property access - module check - - cfxOwnedTroop.usesDefenders(aZone) + - cfxOwnedZones.usesDefenders(aZone) - verifyZone 1.0.8 - repairDefenders trims types to allow blanks in type separator @@ -41,6 +42,9 @@ cfxOwnedZones.announcer = true - announcer 1.1.1 - conq+1 flag 1.1.2 - corrected type bug in zoneConquered +1.2.0 - support for persistence + - conq+1 --> conquered! + - no cfxGroundTroop bug (no delay) --]]-- cfxOwnedZones.requiredLibs = { @@ -60,6 +64,10 @@ cfxOwnedZones.attackingTime = 300 -- 300 seconds until new attackers are produce cfxOwnedZones.shockTime = 200 -- 200 -- 'shocked' period of inactivity cfxOwnedZones.repairTime = 200 -- 200 -- time until we raplace one lost unit, also repairs all other units to 100% +-- persistence: all attackers we ever sent out. +-- is regularly verified and cut to size +cfxOwnedZones.spawnedAttackers = {} + -- owned zones is a module that managers 'conquerable' zones and keeps a -- record of who owns the zone -- based on some simple rules that are regularly checked @@ -166,6 +174,12 @@ function cfxOwnedZones.drawZoneInMap(aZone) end +function cfxOwnedZones.getOwnedZoneByName(zName) + for zKey, theZone in pairs (cfxOwnedZones.zones) do + if theZone.name == zName then return theZone end + end + return nil +end function cfxOwnedZones.addOwnedZone(aZone) local owner = cfxZones.getCoalitionFromZoneProperty(aZone, "owner", 0) -- is already readm read it again @@ -203,8 +217,9 @@ function cfxOwnedZones.addOwnedZone(aZone) local paused = cfxZones.getBoolFromZoneProperty(aZone, "paused", false) aZone.paused = paused + aZone.conqueredFlag = cfxZones.getStringFromZoneProperty(aZone, "conquered!", "*") if cfxZones.hasProperty(aZone, "conq+1") then - cfxOwnedZones.conqueredFlag = cfxZones.getNumberFromZoneProperty(theZone, "conq+1", -1) + aZone.conqueredFlag = cfxZones.getStringFromZoneProperty(aZone, "conq+1", "*") end aZone.unbeatable = cfxZones.getBoolFromZoneProperty(aZone, "unbeatable", false) @@ -218,7 +233,7 @@ end function cfxOwnedZones.verifyZone(aZone) -- do some sanity checks if not cfxGroundTroops and (aZone.attackersRED ~= "none" or aZone.attackersBLUE ~= "none") then - trigger.action.outText("+++owdZ: " .. aZone.name .. " attackers need cfxGroundTroops to function") + trigger.action.outText("+++owdZ: " .. aZone.name .. " attackers need cfxGroundTroops to function", 30) end end @@ -375,14 +390,14 @@ function cfxOwnedZones.spawnAttackTroops(theTypes, aZone, aCoalition, aFormation local spawnZone = cfxZones.createSimpleZone("attkSpawnZone", spawnPoint, aZone.attackRadius) - local theGroup = cfxZones.createGroundUnitsInZoneForCoalition ( + local theGroup, theData = cfxZones.createGroundUnitsInZoneForCoalition ( aCoalition, -- theCountry, aZone.name .. " (A) " .. dcsCommon.numberUUID(), -- must be unique spawnZone, unitTypes, aFormation, -- outward facing 0) - return theGroup + return theGroup, theData end function cfxOwnedZones.spawnDefensiveTroops(theTypes, aZone, aCoalition, aFormation) @@ -400,13 +415,13 @@ function cfxOwnedZones.spawnDefensiveTroops(theTypes, aZone, aCoalition, aFormat --local theCountry = dcsCommon.coalition2county(aCoalition) local spawnZone = cfxZones.createSimpleZone("spawnZone", aZone.point, aZone.spawnRadius) - local theGroup = cfxZones.createGroundUnitsInZoneForCoalition ( + local theGroup, theData = cfxZones.createGroundUnitsInZoneForCoalition ( aCoalition, --theCountry, aZone.name .. " (D) " .. dcsCommon.numberUUID(), -- must be unique spawnZone, unitTypes, aFormation, -- outward facing 0) - return theGroup + return theGroup, theData end -- -- U P D A T E @@ -432,7 +447,13 @@ function cfxOwnedZones.sendOutAttackers(aZone) if attackers == "none" then return end - local theGroup = cfxOwnedZones.spawnAttackTroops(attackers, aZone, aZone.owner, aZone.attackFormation) + local theGroup, theData = cfxOwnedZones.spawnAttackTroops(attackers, aZone, aZone.owner, aZone.attackFormation) + + local troopData = {} + troopData.groupData = theData + troopData.orders = "attackOwnedZone" -- lazy coding! + troopData.side = aZone.owner + cfxOwnedZones.spawnedAttackers[theData.name] = troopData -- submit them to ground troops handler as zoneseekers -- and our groundTroops module will handle the rest @@ -496,10 +517,11 @@ function cfxOwnedZones.zoneConquered(aZone, theSide, formerOwner) -- 0 = neutral end end -- increase conq flag - if aZone.conqueredFlag then - local lastVal = trigger.misc.getUserFlag(aZone.conqueredFlag) - trigger.action.setUserFlag(aZone.conqueredFlag, lastVal + 1) - end +-- if aZone.conqueredFlag then + -- local lastVal = trigger.misc.getUserFlag(aZone.conqueredFlag) + -- trigger.action.setUserFlag(aZone.conqueredFlag, lastVal + 1) + cfxZones.pollFlag(aZone.conqueredFlag, "inc", aZone) +-- end -- invoke callbacks now cfxOwnedZones.invokeConqueredCallbacks(aZone, theSide, formerOwner) @@ -567,7 +589,7 @@ function cfxOwnedZones.repairDefenders(aZone) -- now livingTypes holds the full array of units we need to spawn local theCountry = dcsCommon.getACountryForCoalition(aZone.owner) local spawnZone = cfxZones.createSimpleZone("spawnZone", aZone.point, aZone.spawnRadius) - local theGroup = cfxZones.createGroundUnitsInZoneForCoalition ( + local theGroup, theData = cfxZones.createGroundUnitsInZoneForCoalition ( aZone.owner, -- was wrongly: theCountry aZone.name .. dcsCommon.numberUUID(), -- must be unique spawnZone, @@ -599,15 +621,17 @@ function cfxOwnedZones.spawnDefenders(aZone) -- if 'none', simply exit if defenders == "none" then return end - local theGroup = cfxOwnedZones.spawnDefensiveTroops(defenders, aZone, aZone.owner, aZone.formation) + local theGroup, theData = cfxOwnedZones.spawnDefensiveTroops(defenders, aZone, aZone.owner, aZone.formation) -- the troops reamin, so no orders to move, no handing off to ground troop manager - aZone.defenders = theGroup; + aZone.defenders = theGroup + aZone.defenderData = theData -- used for persistence if theGroup then - aZone.defenderMax = theGroup:getInitialSize() -- so we can determine if some units were destroyed - aZone.lastDefenders = aZone.defenderMax -- if this is larger than current number, someone bit the dust + --aZone.defenderMax = theGroup:getInitialSize() -- so we can determine if some units were destroyed + aZone.lastDefenders = theGroup:getInitialSize() --- aZone.defenderMax -- if this is larger than current number, someone bit the dust --trigger.action.outText("+++ spawned defenders for ".. aZone.name, 30) else trigger.action.outText("+++owdZ: WARNING: spawned no defenders for ".. aZone.name, 30) + aZone.defenderData = nil end end @@ -661,6 +685,10 @@ function cfxOwnedZones.updateZone(aZone) -- we have defenders if aZone.defenders:isExist() then -- isee if group was damaged + if not aZone.lastDefenders then + -- fresh group, probably from persistence, needs init + aZone.lastDefenders = -1 + end if aZone.defenders:getSize() < aZone.lastDefenders then -- yes, at least one unit destroyed aZone.timeStamp = timer.getTime() @@ -671,6 +699,8 @@ function cfxOwnedZones.updateZone(aZone) aZone.state = "shocked" return + else + aZone.lastDefenders = aZone.defenders:getSize() end else @@ -725,17 +755,19 @@ function cfxOwnedZones.updateZone(aZone) -- we are currently rebuilding defenders unit by unit if timer.getTime() > aZone.timeStamp + cfxOwnedZones.repairTime then aZone.timeStamp = timer.getTime() + -- wait's up, repair one defender, then check if full strength cfxOwnedZones.repairDefenders(aZone) - if aZone.defenders:getSize() >= aZone.defenderMax then --- + -- see if we are full strenght and if so go to attack, else set timer to reair the next unit + if aZone.defenders and aZone.defenders:isExist() and aZone.defenders:getSize() >= aZone.defenders:getInitialSize() then -- we are at max size, time to produce some attackers + -- progress to next state nextState = "attacking" aZone.timeStamp = timer.getTime() if cfxOwnedZones.verbose then trigger.action.outText("+++owdZ: State " .. aZone.state .. " to " .. nextState .. " for " .. aZone.name, 30) end end - -- see if we are full strenght and if so go to attack, else set timer to reair the next unit + end elseif aZone.state == "shocked" then @@ -763,6 +795,20 @@ function cfxOwnedZones.updateZone(aZone) aZone.state = nextState end +function cfxOwnedZones.GC() + -- GC run. remove all my dead remembered troops + local filteredAttackers = {} + for gName, gData in pairs (cfxOwnedZones.spawnedAttackers) do + -- all we need to do is get the group of that name + -- and if it still returns units we are fine + local gameGroup = Group.getByName(gName) + if gameGroup and gameGroup:isExist() and gameGroup:getSize() > 0 then + filteredAttackers[gName] = gData + end + end + cfxOwnedZones.spawnedAttackers = filteredAttackers +end + function cfxOwnedZones.update() cfxOwnedZones.updateSchedule = timer.scheduleFunction(cfxOwnedZones.update, {}, timer.getTime() + 1/cfxOwnedZones.ups) @@ -792,6 +838,11 @@ function cfxOwnedZones.update() end +function cfxOwnedZones.houseKeeping() + timer.scheduleFunction(cfxOwnedZones.houseKeeping, {}, timer.getTime() + 5 * 60) -- every 5 minutes + cfxOwnedZones.GC() +end + function cfxOwnedZones.sideOwnsAll(theSide) for key, aZone in pairs(cfxOwnedZones.zones) do if aZone.owner ~= theSide then @@ -810,19 +861,150 @@ function cfxOwnedZones.hasOwnedZones() return false end + +-- +-- load / save data +-- + +function cfxOwnedZones.saveData() + -- this is called from persistence when it's time to + -- save data. returns a table with all my data + local theData = {} + local allZoneData = {} + -- iterate all my zones and create data + for idx, theZone in pairs(cfxOwnedZones.zones) do + local zoneData = {} + if theZone.defenderData then + zoneData.defenderData = dcsCommon.clone(theZone.defenderData) + dcsCommon.synchGroupData(zoneData.defenderData) + end + zoneData.conquered = cfxZones.getFlagValue(theZone.conqueredFlag, theZone) + zoneData.owner = theZone.owner + zoneData.state = theZone.state -- will prevent immediate spawn + -- since new zones are spawned with 'init' + allZoneData[theZone.name] = zoneData + end + + -- now iterate all attack groups that we have spawned and that + -- (maybe) are still alive + cfxOwnedZones.GC() -- start with a GC run to remove all dead + local livingAttackers = {} + for gName, gData in pairs (cfxOwnedZones.spawnedAttackers) do + -- all we need to do is get the group of that name + -- and if it still returns units we are fine + -- spawnedAttackers is a [groupName] table with {.groupData, .orders, .side} + local gameGroup = Group.getByName(gName) + if gameGroup and gameGroup:isExist() then + if gameGroup:getSize() > 0 then + local sData = dcsCommon.clone(gData) + dcsCommon.synchGroupData(sData.groupData) + livingAttackers[gName] = sData + end + end + end + + -- now write the info for the flags that we output for #red, etc + local flagInfo = {} + flagInfo.neutral = cfxZones.getFlagValue(cfxOwnedZones.neutralTriggerFlag, cfxOwnedZones) + flagInfo.red = cfxZones.getFlagValue(cfxOwnedZones.redTriggerFlag, cfxOwnedZones) + flagInfo.blue = cfxZones.getFlagValue(cfxOwnedZones.blueTriggerFlag, cfxOwnedZones) + -- assemble the data + theData.zoneData = allZoneData + theData.attackers = livingAttackers + theData.flagInfo = flagInfo + + -- return it + return theData +end + +function cfxOwnedZones.loadData() + -- remember to draw in map with new owner + if not persistence then return end + local theData = persistence.getSavedDataForModule("cfxOwnedZones") + if not theData then + if cfxOwnedZones.verbose then + trigger.action.outText("owdZ: no save date received, skipping.", 30) + end + return + end + -- theData contains the following tables: + -- zoneData: per-zone data + -- flagInfo: module-global flags + -- attackers: all spawned attackers that we feed to groundTroops + local allZoneData = theData.zoneData + for zName, zData in pairs(allZoneData) do + -- access zone + local theZone = cfxOwnedZones.getOwnedZoneByName(zName) + if theZone then + if zData.defenderData then + if theZone.defenders and theZone.defenders:isExist() then + -- should not happen, but so be it + theZone.defenders:destroy() + end + local gData = zData.defenderData + local cty = gData.cty + local cat = gData.cat + theZone.defenders = coalition.addGroup(cty, cat, gData) + theZone.defenderData = zData.defenderData + end + theZone.owner = zData.owner + theZone.state = zData.state + cfxZones.setFlagValue(theZone.conqueredFlag, zData.conquered, theZone) + -- update mark in map + cfxOwnedZones.drawZoneInMap(theZone) + else + trigger.action.outText("owdZ: load - data mismatch: cannot find zone <" .. zName .. ">, skipping zone.", 30) + end + end + + -- now process all attackers + local allAttackers = theData.attackers + for gName, gdTroop in pairs(allAttackers) do + -- table is {.groupData, .orders, .side} + local gData = gdTroop.groupData + local orders = gdTroop.orders + local side = gdTroop.side + local cty = gData.cty + local cat = gData.cat + -- add to my own attacker queue so we can save later + local dClone = dcsCommon.clone(gData) + cfxOwnedZones.spawnedAttackers[gName] = dClone + local theGroup = coalition.addGroup(cty, cat, gData) + if cfxGroundTroops then + local troops = cfxGroundTroops.createGroundTroops(theGroup) + troops.orders = orders + troops.side = side + cfxGroundTroops.addGroundTroopsToPool(troops) -- hand off to ground troops + end + end + + -- now process module global flags + local flagInfo = theData.flagInfo + if flagInfo then + cfxZones.setFlagValue(cfxOwnedZones.neutralTriggerFlag, flagInfo.neutral, cfxOwnedZones) + cfxZones.setFlagValue(cfxOwnedZones.redTriggerFlag, flagInfo.red, cfxOwnedZones) + cfxZones.setFlagValue(cfxOwnedZones.blueTriggerFlag, flagInfo.blue, cfxOwnedZones) + end +end + + +-- function cfxOwnedZones.readConfigZone(theZone) + if not theZone then theZone = cfxZones.createSimpleZone("ownedZonesConfig") end + + cfxOwnedZones.name = "cfxOwnedZones" -- just in case, so we can access with cfxZones cfxOwnedZones.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) cfxOwnedZones.announcer = cfxZones.getBoolFromZoneProperty(theZone, "announcer", true) - if cfxZones.hasProperty(theZone, "r!") then - cfxOwnedZones.redTriggerFlag = cfxZones.getStringFromZoneProperty(theZone, "r!", "") - end - if cfxZones.hasProperty(theZone, "b!") then - cfxOwnedZones.blueTriggerFlag = cfxZones.getStringFromZoneProperty(theZone, "b!", "") - end - if cfxZones.hasProperty(theZone, "n!") then - cfxOwnedZones.neutralTriggerFlag = cfxZones.getStringFromZoneProperty(theZone, "n!", "") - end +-- if cfxZones.hasProperty(theZone, "r!") then + cfxOwnedZones.redTriggerFlag = cfxZones.getStringFromZoneProperty(theZone, "r!", "*") +-- end +-- if cfxZones.hasProperty(theZone, "b!") then + cfxOwnedZones.blueTriggerFlag = cfxZones.getStringFromZoneProperty(theZone, "b!", "*") +-- end +-- if cfxZones.hasProperty(theZone, "n!") then + cfxOwnedZones.neutralTriggerFlag = cfxZones.getStringFromZoneProperty(theZone, "n!", "*") +-- end cfxOwnedZones.defendingTime = cfxZones.getNumberFromZoneProperty(theZone, "defendingTime", 100) cfxOwnedZones.attackingTime = cfxZones.getNumberFromZoneProperty(theZone, "attackingTime", 300) cfxOwnedZones.shockTime = cfxZones.getNumberFromZoneProperty(theZone, "shockTime", 200) @@ -838,11 +1020,8 @@ function cfxOwnedZones.init() -- read my config zone local theZone = cfxZones.getZoneByName("ownedZonesConfig") - if not theZone then - trigger.action.outText("+++ownZ: no config", 30) - else - cfxOwnedZones.readConfigZone(theZone) - end + cfxOwnedZones.readConfigZone(theZone) + -- collect all owned zones by their 'owner' property -- start the process @@ -854,9 +1033,21 @@ function cfxOwnedZones.init() cfxOwnedZones.addOwnedZone(aZone) end + if persistence then + -- sign up for persistence + callbacks = {} + callbacks.persistData = cfxOwnedZones.saveData + persistence.registerModule("cfxOwnedZones", callbacks) + -- now load my data + cfxOwnedZones.loadData() + end + initialized = true cfxOwnedZones.updateSchedule = timer.scheduleFunction(cfxOwnedZones.update, {}, timer.getTime() + 1/cfxOwnedZones.ups) + -- start housekeeping + cfxOwnedZones.houseKeeping() + trigger.action.outText("cx/x owned zones v".. cfxOwnedZones.version .. " started", 30) return true diff --git a/modules/cfxReconMode.lua b/modules/cfxReconMode.lua index 4369c7a..12fc38b 100644 --- a/modules/cfxReconMode.lua +++ b/modules/cfxReconMode.lua @@ -1,5 +1,5 @@ cfxReconMode = {} -cfxReconMode.version = "2.1.0" +cfxReconMode.version = "2.1.1" cfxReconMode.verbose = false -- set to true for debug info cfxReconMode.reconSound = "UI_SCI-FI_Tone_Bright_Dry_20_stereo.wav" -- to be played when somethiong discovered @@ -77,7 +77,9 @@ VERSION HISTORY 2.1.0 - processZoneMessage uses group's position, not zone - silent attribute for priority targets - activate / deactivate by flags - + 2.1.1 - Lat Lon and MGRS also give Elevation + - cfxReconMode.reportTime + cfxReconMode is a script that allows units to perform reconnaissance missions and, after detecting units, marks them on the map with markers for their coalition and some text @@ -421,13 +423,14 @@ function cfxReconMode.getLocation(theGroup) local msg = "" local theUnit = theGroup:getUnit(1) local currPoint = theUnit:getPoint() + local ele = math.floor(land.getHeight({x = currPoint.x, y = currPoint.z})) if cfxReconMode.mgrs then local grid = coord.LLtoMGRS(coord.LOtoLL(currPoint)) - msg = grid.UTMZone .. ' ' .. grid.MGRSDigraph .. ' ' .. grid.Easting .. ' ' .. grid.Northing + msg = grid.UTMZone .. ' ' .. grid.MGRSDigraph .. ' ' .. grid.Easting .. ' ' .. grid.Northing .. " Ele " .. ele .."m" else local lat, lon, alt = coord.LOtoLL(currPoint) lat, lon = dcsCommon.latLon2Text(lat, lon) - msg = "Lat " .. lat .. " Lon " .. lon + msg = "Lat " .. lat .. " Lon " .. lon .. " Ele " .. ele .."m" end return msg end @@ -516,7 +519,7 @@ function cfxReconMode.detectedGroup(mySide, theScout, theGroup, theLoc) -- say something if not silent and cfxReconMode.announcer then local msg = cfxReconMode.generateSALT(theScout, theGroup) - trigger.action.outTextForCoalition(mySide, msg, 30) + trigger.action.outTextForCoalition(mySide, msg, cfxReconMode.reportTime) -- trigger.action.outTextForCoalition(mySide, theScout:getName() .. " reports new ground contact " .. theGroup:getName(), 30) if cfxReconMode.verbose then trigger.action.outText("+++rcn: announced for side " .. mySide, 30) @@ -549,7 +552,7 @@ function cfxReconMode.detectedGroup(mySide, theScout, theGroup, theLoc) -- AND EVEN WHEN SILENT!!! local msg = zInfo.prioMessage msg = cfxReconMode.processZoneMessage(msg, zInfo.theZone, theGroup) - trigger.action.outTextForCoalition(mySide, msg, 30) + trigger.action.outTextForCoalition(mySide, msg, cfxReconMode.reportTime) if cfxReconMode.verbose or zInfo.theZone.verbose then trigger.action.outText("+++rcn: prio message sent for prio target zone <" .. zInfo.theZone.name .. ">",30) end @@ -965,7 +968,8 @@ function cfxReconMode.readConfigZone() cfxReconMode.greyScouts = cfxZones.getBoolFromZoneProperty(theZone, "greyScouts", false) cfxReconMode.playerOnlyRecon = cfxZones.getBoolFromZoneProperty(theZone, "playerOnlyRecon", false) cfxReconMode.reportNumbers = cfxZones.getBoolFromZoneProperty(theZone, "reportNumbers", true) - + cfxReconMode.reportTime = cfxZones.getNumberFromZoneProperty(theZone, "reportTime", 30) + cfxReconMode.detectionMinRange = cfxZones.getNumberFromZoneProperty(theZone, "detectionMinRange", 3000) cfxReconMode.detectionMaxRange = cfxZones.getNumberFromZoneProperty(theZone, "detectionMaxRange", 12000) cfxReconMode.maxAlt = cfxZones.getNumberFromZoneProperty(theZone, "maxAlt", 9000) diff --git a/modules/cfxZones.lua b/modules/cfxZones.lua index e21afaf..e38efbd 100644 --- a/modules/cfxZones.lua +++ b/modules/cfxZones.lua @@ -1,5 +1,5 @@ cfxZones = {} -cfxZones.version = "2.8.4" +cfxZones.version = "2.8.5" -- cf/x zone management module -- reads dcs zones and makes them accessible and mutable @@ -89,6 +89,9 @@ cfxZones.version = "2.8.4" - changed extractPropertyFromDCS() to also match attributes with blanks like "the Attr" to "theAttr" - new expandFlagName() - 2.8.4 - fixed bug in setFlagValue() +- 2.8.5 - createGroundUnitsInZoneForCoalition() now always passes back a copy of the group data + - data also contains cty = country and cat = category for easy spawn + - getFlagValue additional zone name guards --]]-- cfxZones.verbose = false @@ -1047,8 +1050,17 @@ function cfxZones.createGroundUnitsInZoneForCoalition (theCoalition, groupName, -- first we need to translate the coalition to a legal -- country. we use UN for neutral, cjtf for red and blue local theSideCJTF = dcsCommon.coalition2county(theCoalition) - return coalition.addGroup(theSideCJTF, Group.Category.GROUND, theGroup) + -- store cty and cat for later access. DCS doesn't need it, but we may + + theGroup.cty = theSideCJTF + theGroup.cat = Group.Category.GROUND + + -- create a copy of the group data for + -- later reference + local groupDataCopy = dcsCommon.clone(theGroup) + local newGroup = coalition.addGroup(theSideCJTF, Group.Category.GROUND, theGroup) + return newGroup, groupDataCopy end -- parsing zone names. The first part of the name until the first blank " " @@ -1311,8 +1323,8 @@ end function cfxZones.getFlagValue(theFlag, theZone) local zoneName = "" - if not theZone then - trigger.action.outText("+++Zne: no zone on getFlagValue", 30) + if not theZone or not theZone.name then + trigger.action.outText("+++Zne: no zone or zone name on getFlagValue") else zoneName = theZone.name -- for flag wildcards end diff --git a/modules/changer.lua b/modules/changer.lua index c2852c0..8bc3b45 100644 --- a/modules/changer.lua +++ b/modules/changer.lua @@ -258,7 +258,7 @@ function changer.start() -- read config changer.readConfigZone() - -- process cloner Zones + -- process changer Zones local attrZones = cfxZones.getZonesWithAttributeNamed("change?") for k, aZone in pairs(attrZones) do changer.createChangerWithZone(aZone) -- process attributes diff --git a/modules/cloneZone.lua b/modules/cloneZone.lua index 8191a39..780d146 100644 --- a/modules/cloneZone.lua +++ b/modules/cloneZone.lua @@ -1,1120 +1,1186 @@ - cloneZones = {} - cloneZones.version = "1.4.8" - 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.9" +cloneZones.verbose = false +cloneZones.requiredLibs = { + "dcsCommon", -- always + "cfxZones", -- Zones, of course + "cfxMX", +} +cloneZones.minSep = 10 -- minimal separation for onRoad auto-pos +cloneZones.maxIter = 100 -- maximum number of attempts to resolve + -- a too-close separation + +-- groupTracker is OPTIONAL! and required only with trackWith attribute - cloneZones.cloners = {} - cloneZones.callbacks = {} - cloneZones.unitXlate = {} - cloneZones.groupXlate = {} -- used to translate original groupID to cloned. only holds last spawned group id - cloneZones.uniqueCounter = 9200000 -- we start group numbering here - --[[-- - Clones Groups from ME mission data - Copyright (c) 2022 by Christian Franz and cf/x AG - - Version History - 1.0.0 - initial version - 1.0.1 - preWipe attribute - 1.1.0 - support for static objects - - despawn? attribute - 1.1.1 - despawnAll: isExist guard - - map in? to f? - 1.2.0 - Lua API integration: callbacks - - groupXlate struct - - unitXlate struct - - resolveReferences - - getGroupsInZone rewritten for data - - static resolve - - linkUnit resolve - - clone? synonym - - empty! and method attributes - 1.3.0 - DML flag upgrade - 1.3.1 - groupTracker interface - - trackWith: attribute - 1.4.0 - Watchflags - 1.4.1 - trackWith: accepts list of trackers - 1.4.2 - onstart delays for 0.1 s to prevent static stacking - - turn bug for statics (bug in dcsCommon, resolved) - 1.4.3 - embark/disembark now works with cloners - 1.4.4 - removed some debugging verbosity - 1.4.5 - randomizeLoc, rndLoc keyword - - cargo manager integration - pass cargo objects when present - 1.4.6 - removed some verbosity for spawned aircraft with airfields on their routes - 1.4.7 - DML watchflag and DML Flag polish, method-->cloneMethod - 1.4.8 - added 'wipe?' synonym - - - --]]-- +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 + 1.4.5 - randomizeLoc, rndLoc keyword + - cargo manager integration - pass cargo objects when present + 1.4.6 - removed some verbosity for spawned aircraft with airfields on their routes + 1.4.7 - DML watchflag and DML Flag polish, method-->cloneMethod + 1.4.8 - added 'wipe?' synonym + 1.4.9 - onRoad option + - rndHeading option + + +--]]-- - -- - -- adding / removing from list - -- - function cloneZones.addCloneZone(theZone) - table.insert(cloneZones.cloners, theZone) +-- +-- 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 - 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 +-- +-- 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 + + 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 - - 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 + 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 - 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 - theZone.cloneNames = nil - theZone.staticNames = nil - end + cloneZones.despawnAll(theZone) + if (#theZone.cloneNames + #theZone.staticNames) < 1 then if cloneZones.verbose then - trigger.action.outText(theZone.name .. " clone template saved", 30) - end + trigger.action.outText("+++clnZ: WARNING - Template in clone zone <" .. theZone.name .. "> is empty", 30) + end + theZone.cloneNames = nil + theZone.staticNames = nil end - - -- watchflag: - -- triggerMethod - theZone.cloneTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "triggerMethod", "change") - - if cfxZones.hasProperty(theZone, "cloneTriggerMethod") then - theZone.cloneTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "cloneTriggerMethod", "change") - end - - -- f? and spawn? and other synonyms map to the same - if cfxZones.hasProperty(theZone, "f?") then - theZone.spawnFlag = cfxZones.getStringFromZoneProperty(theZone, "f?", "none") - end - - if cfxZones.hasProperty(theZone, "in?") then - theZone.spawnFlag = cfxZones.getStringFromZoneProperty(theZone, "in?", "none") - end - - if cfxZones.hasProperty(theZone, "spawn?") then - theZone.spawnFlag = cfxZones.getStringFromZoneProperty(theZone, "spawn?", "none") - end - - if cfxZones.hasProperty(theZone, "clone?") then - theZone.spawnFlag = cfxZones.getStringFromZoneProperty(theZone, "clone?", "none") - end - - if theZone.spawnFlag then - theZone.lastSpawnValue = cfxZones.getFlagValue(theZone.spawnFlag, theZone) - end - - -- deSpawn? - if cfxZones.hasProperty(theZone, "deSpawn?") then - theZone.deSpawnFlag = cfxZones.getStringFromZoneProperty(theZone, "deSpawn?", "none") - end - - if cfxZones.hasProperty(theZone, "deClone?") then - theZone.deSpawnFlag = cfxZones.getStringFromZoneProperty(theZone, "deClone?", "none") - end - - if cfxZones.hasProperty(theZone, "wipe?") then - theZone.deSpawnFlag = cfxZones.getStringFromZoneProperty(theZone, "wipe?", "none") - end - - if theZone.deSpawnFlag then - theZone.lastDeSpawnValue = cfxZones.getFlagValue(theZone.deSpawnFlag, theZone) - end - - theZone.onStart = cfxZones.getBoolFromZoneProperty(theZone, "onStart", false) - - theZone.moveRoute = cfxZones.getBoolFromZoneProperty(theZone, "moveRoute", false) - - theZone.preWipe = cfxZones.getBoolFromZoneProperty(theZone, "preWipe", false) - - -- to be deprecated - if cfxZones.hasProperty(theZone, "empty+1") then - theZone.emptyFlag = cfxZones.getStringFromZoneProperty(theZone, "empty+1", "") -- note string on number default - end - - if cfxZones.hasProperty(theZone, "empty!") then - theZone.emptyBangFlag = cfxZones.getStringFromZoneProperty(theZone, "empty!", "") -- note string on number default - end - - theZone.cloneMethod = cfxZones.getStringFromZoneProperty(theZone, "cloneMethod", "inc") - if cfxZones.hasProperty(theZone, "method") then - theZone.cloneMethod = cfxZones.getStringFromZoneProperty(theZone, "method", "inc") -- note string on number default - end - - if cfxZones.hasProperty(theZone, "masterOwner") then - theZone.masterOwner = cfxZones.getStringFromZoneProperty(theZone, "masterOwner", "") - end - - theZone.turn = cfxZones.getNumberFromZoneProperty(theZone, "turn", 0) - - -- interface to groupTracker - if cfxZones.hasProperty(theZone, "trackWith:") then - theZone.trackWith = cfxZones.getStringFromZoneProperty(theZone, "trackWith:", "") - end - - -- randomized locations on spawn - theZone.rndLoc = cfxZones.getBoolFromZoneProperty(theZone, "randomizedLoc", false) - if cfxZones.hasProperty(theZone, "rndLoc") then - theZone.rndLoc = cfxZones.getBoolFromZoneProperty(theZone, "rndLoc", false) - end - if theZone.rndLoc and theZone.verbose then - trigger.action.outText("+++ rndloc on for " .. theZone.name, 30) - end - - -- we end with clear plate - end - - -- - -- 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 + trigger.action.outText(theZone.name .. " clone template saved", 30) 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 + + -- watchflag: + -- triggerMethod + theZone.cloneTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "triggerMethod", "change") + + if cfxZones.hasProperty(theZone, "cloneTriggerMethod") then + theZone.cloneTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "cloneTriggerMethod", "change") + end + + -- f? and spawn? and other synonyms map to the same + if cfxZones.hasProperty(theZone, "f?") then + theZone.spawnFlag = cfxZones.getStringFromZoneProperty(theZone, "f?", "none") + end + + if cfxZones.hasProperty(theZone, "in?") then + theZone.spawnFlag = cfxZones.getStringFromZoneProperty(theZone, "in?", "none") + end + + if cfxZones.hasProperty(theZone, "spawn?") then + theZone.spawnFlag = cfxZones.getStringFromZoneProperty(theZone, "spawn?", "none") + end + + if cfxZones.hasProperty(theZone, "clone?") then + theZone.spawnFlag = cfxZones.getStringFromZoneProperty(theZone, "clone?", "none") + end + + if theZone.spawnFlag then + theZone.lastSpawnValue = cfxZones.getFlagValue(theZone.spawnFlag, theZone) + end + + -- deSpawn? + if cfxZones.hasProperty(theZone, "deSpawn?") then + theZone.deSpawnFlag = cfxZones.getStringFromZoneProperty(theZone, "deSpawn?", "none") + end + + if cfxZones.hasProperty(theZone, "deClone?") then + theZone.deSpawnFlag = cfxZones.getStringFromZoneProperty(theZone, "deClone?", "none") + end + + if cfxZones.hasProperty(theZone, "wipe?") then + theZone.deSpawnFlag = cfxZones.getStringFromZoneProperty(theZone, "wipe?", "none") + end + + if theZone.deSpawnFlag then + theZone.lastDeSpawnValue = cfxZones.getFlagValue(theZone.deSpawnFlag, theZone) + end + + theZone.onStart = cfxZones.getBoolFromZoneProperty(theZone, "onStart", false) + + theZone.moveRoute = cfxZones.getBoolFromZoneProperty(theZone, "moveRoute", false) + + theZone.preWipe = cfxZones.getBoolFromZoneProperty(theZone, "preWipe", false) + + -- to be deprecated + if cfxZones.hasProperty(theZone, "empty+1") then + theZone.emptyFlag = cfxZones.getStringFromZoneProperty(theZone, "empty+1", "") -- note string on number default + end + + if cfxZones.hasProperty(theZone, "empty!") then + theZone.emptyBangFlag = cfxZones.getStringFromZoneProperty(theZone, "empty!", "") -- note string on number default + end + + theZone.cloneMethod = cfxZones.getStringFromZoneProperty(theZone, "cloneMethod", "inc") + if cfxZones.hasProperty(theZone, "method") then + theZone.cloneMethod = cfxZones.getStringFromZoneProperty(theZone, "method", "inc") -- note string on number default + end + + if cfxZones.hasProperty(theZone, "masterOwner") then + theZone.masterOwner = cfxZones.getStringFromZoneProperty(theZone, "masterOwner", "") + end + + theZone.turn = cfxZones.getNumberFromZoneProperty(theZone, "turn", 0) + + -- interface to groupTracker + if cfxZones.hasProperty(theZone, "trackWith:") then + theZone.trackWith = cfxZones.getStringFromZoneProperty(theZone, "trackWith:", "") 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 -- (!!) + -- randomized locations on spawn + theZone.rndLoc = cfxZones.getBoolFromZoneProperty(theZone, "randomizedLoc", false) + if cfxZones.hasProperty(theZone, "rndLoc") then + theZone.rndLoc = cfxZones.getBoolFromZoneProperty(theZone, "rndLoc", false) + end + theZone.rndHeading = cfxZones.getBoolFromZoneProperty(theZone, "rndHeading", false) + + theZone.onRoad = cfxZones.getBoolFromZoneProperty(theZone, "onRoad", false) + + if theZone.rndLoc and theZone.verbose then + trigger.action.outText("+++ rndloc on for " .. theZone.name, 30) + end + + -- we end with clear plate +end - 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 +-- +-- 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 + 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 -- (!!) + + 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() + 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 + 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() + 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 end end - function cloneZones.uniqueID() - local uid = cloneZones.uniqueCounter - cloneZones.uniqueCounter = cloneZones.uniqueCounter + 1 - return uid - 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 +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 - function cloneZones.uniqueIDGroupData(theData) - theData.groupId = cloneZones.uniqueID() - end +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 +function cloneZones.resolveOwnership(spawnZone, ctry) + if not spawnZone.masterOwner then return ctry 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) + 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 - -- +-- +-- 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) +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 - - -- 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 + 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 + + -- 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.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 +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 - 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 +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 - -- resolve EMBARK/DISEMBARK groupd references - if taskData.id and taskData.params and taskData.params.groupsForEmbarking - then - -- build new groupsForEmbarking - local embarkers = taskData.params.groupsForEmbarking + 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 embark group id <" .. gID .. "> to <" .. resolvedID .. ">", 30) + trigger.action.outText("+++clnZ: resolved distribute unit/group id <" .. aUnit .. "/" .. gID .. "> to <".. newUnit .. "/" .. resolvedID .. ">", 30) end - -- replace old with new table - taskData.params.groupsForEmbarking = newEmbarkers + -- store this as new group for + -- translated transportID + newDist[newUnit] = 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 + -- 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.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) +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 - 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) + -- now use raw data to spawn and see if it works outabox + local theCat = cfxMX.catText2ID(cat) + rawData.CZtheCat = theCat -- save cat - local spawnedGroups = {} - local spawnedStatics = {} - local dataToSpawn = {} + -- update their position if not spawning to exact same location + cloneZones.updateLocationsInGroupData(rawData, zoneDelta, spawnZone.moveRoute) - 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 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 - - -- 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 - - 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 + -- 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) - rawData.x = rawData.x + dx - rawData.y = rawData.y + dy + aUnit.x = aUnit.x + dx + aUnit.y = aUnit.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 + + if spawnZone.rndHeading then + local units = rawData.units + for idx, aUnit in pairs(units) do + local phi = 6.2831 * math.random() -- that's 2Pi, folx + aUnit.heading = phi 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 + + -- apply onRoad option if selected + if spawnZone.onRoad then + local units = rawData.units + local iterCount = 0 + local otherLocs = {} -- resolved locs + for idx, aUnit in pairs(units) do + local cx = aUnit.x + local cy = aUnit.y + -- we now iterate until there is enough separation or too many iters + local tooClose + local np, nx, ny + repeat + nx, ny = land.getClosestPointOnRoads("roads", cx, cy) + -- compare this with all other locs + np = {x=nx, y=ny} + tooClose = false + for idc, op in pairs(otherLocs) do + local d = dcsCommon.dist(np, op) + if d < cloneZones.minSep then + tooClose = true + cx = cx + cloneZones.minSep + cy = cy + cloneZones.minSep + iterCount = iterCount + 1 + -- trigger.action.outText("d fail for <" .. aUnit.name.. ">: d= <" .. d .. ">, iters = <" .. iterCount .. ">", 30) + end + end + until (iterCount > cloneZones.maxIter) or (not tooClose) + -- trigger.action.outText("separation iters for <" .. aUnit.name.. ">:<" .. iterCount .. ">", 30) + table.insert(otherLocs, np) + aUnit.x = nx + aUnit.y = ny 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 + 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 + + -- 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 - 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) + trigger.action.outText("clnZ: post-clone verification failed for unit <" .. uName .. ">: ÎD mismatch: " .. uID .. " -- " .. aUnit.CZTargetID, 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) + cloneZones.unitXlate[aUnit.CZorigID] = uID + else + trigger.action.outText("clnZ: post-clone verifiaction failed for unit <" .. uName .. ">: not found", 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 + -- check if our assigned ID matches the handed out by + -- DCS + if newGroupID == rawData.CZTargetID then + -- we are good 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 + trigger.action.outText("clnZ: MISMATCH " .. rawData.name .. " target ID " .. rawData.CZTargetID .. " does not match " .. newGroupID, 30) 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.cloneMethod, aZone) - if cloneZones.verbose then - trigger.action.outText("+++clnZ: bang! on " .. aZone.emptyBangFlag, 30) - end - end - -- invoke callbacks - cloneZones.invokeCallbacks(aZone, "empty", {}) - - -- prevent isEmpty next pass - aZone.hasClones = false - end - + cloneZones.invokeCallbacks(theZone, "did spawn group", theGroup) + -- interface to groupTracker + if theZone.trackWith then + cloneZones.handoffTracking(theGroup, theZone) 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) + -- 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 + + if spawnZone.rndHeading then + local phi = 6.2831 * math.random() -- that's 2Pi, folx + rawData.heading = phi + end + + if spawnZone.onRoad then + local cx = rawData.x + local cy = rawData.y + local nx, ny = land.getClosestPointOnRoads("roads", cx, cy) + rawData.x = nx + rawData.y = ny + end + + -- apply turning + dcsCommon.rotateUnitData(rawData, spawnZone.turn, 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 - 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 - -- - -- START - -- - function cloneZones.readConfigZone() - local theZone = cfxZones.getZoneByName("cloneZonesConfig") - if not theZone then +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: NO config zone!", 30) + 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 - - cloneZones.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) - + end + templateZone = newTemplate + end + + -- make sure our template is filled + if not templateZone.cloneNames then if cloneZones.verbose then - trigger.action.outText("+++clnZ: read config", 30) + trigger.action.outText("+++clnZ: clone source template <".. templateZone.name .. "> for clone zone <" .. theZone.name .."> is empty", 30) end + return 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 + -- 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 - -- 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 + -- 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 - -- 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) + -- empty handling + local isEmpty = cloneZones.countLiveUnits(aZone) < 1 and aZone.hasClones + if isEmpty then + -- see if we need to bang a flag + if aZone.emptyFlag then + --cloneZones.pollFlag(aZone.emptyFlag) + cfxZones.pollFlag(aZone.emptyFlag, 'inc', aZone) + end + + if aZone.emptyBangFlag then + cfxZones.pollFlag(aZone.emptyBangFlag, aZone.cloneMethod, aZone) + if cloneZones.verbose then + trigger.action.outText("+++clnZ: bang! on " .. aZone.emptyBangFlag, 30) + end + end + -- invoke callbacks + cloneZones.invokeCallbacks(aZone, "empty", {}) + + -- prevent isEmpty next pass + aZone.hasClones = false + end - -- start update - cloneZones.update() - - trigger.action.outText("cfx Clone Zones v" .. cloneZones.version .. " started.", 30) - return true end +end - - -- let's go! - if not cloneZones.start() then - trigger.action.outText("cf/x Clone Zones aborted: missing libraries", 30) - cloneZones = nil +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: read config", 30) + end +end + +function cloneZones.start() + -- lib check + if not dcsCommon.libCheck then + trigger.action.outText("cfx Clone Zones requires dcsCommon", 30) + return false + end + if not dcsCommon.libCheck("cfx Clone Zones", + cloneZones.requiredLibs) then + return false + end + + -- read config + cloneZones.readConfigZone() + + -- process cloner Zones + local attrZones = cfxZones.getZonesWithAttributeNamed("cloner") + + -- now create an rnd gen for each one and add them + -- to our watchlist + for k, aZone in pairs(attrZones) do + cloneZones.createClonerWithZone(aZone) -- process attribute and add to zone + cloneZones.addCloneZone(aZone) -- remember it so we can smoke it + end + + -- 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 - --[[-- - to resolve tasks +-- let's go! +if not cloneZones.start() then + trigger.action.outText("cf/x Clone Zones aborted: missing libraries", 30) + cloneZones = nil +end - - AFAC - - FAC Assign group - - set freq for unit - --]]-- \ No newline at end of file + +--[[-- + to resolve tasks + + - AFAC + - FAC Assign group + - set freq for unit +--]]-- \ No newline at end of file diff --git a/modules/dcsCommon.lua b/modules/dcsCommon.lua index d82c0ee..d7958f2 100644 --- a/modules/dcsCommon.lua +++ b/modules/dcsCommon.lua @@ -1,5 +1,5 @@ dcsCommon = {} -dcsCommon.version = "2.6.8" +dcsCommon.version = "2.7.0" --[[-- VERSION HISTORY 2.2.6 - compassPositionOfARelativeToB - clockPositionOfARelativeToB @@ -85,6 +85,11 @@ dcsCommon.version = "2.6.8" 2.6.7 - new menu2text() 2.6.8 - new getMissionName() - new flagArrayFromString() + 2.6.9 - new getSceneryObjectsInZone() + - new getSceneryObjectInZoneByName() + 2.7.0 - new synchGroupData() + clone, topClone and copyArray now all nil-trap + --]]-- -- dcsCommon is a library of common lua functions @@ -456,6 +461,7 @@ dcsCommon.version = "2.6.8" return dcsCommon.dist(point1, point2) end + -- distance between points function dcsCommon.dist(point1, point2) -- returns distance between two points -- supports xyz and xy notations @@ -882,6 +888,7 @@ dcsCommon.version = "2.6.8" -- topClone is a shallow clone of orig, only top level is iterated, -- all values are ref-copied function dcsCommon.topClone(orig) + if not orig then return nil end local orig_type = type(orig) local copy if orig_type == 'table' then @@ -898,6 +905,7 @@ dcsCommon.version = "2.6.8" -- clone is a recursive clone which will also clone -- deeper levels, as used in units function dcsCommon.clone(orig) + if not orig then return nil end local orig_type = type(orig) local copy if orig_type == 'table' then @@ -913,6 +921,8 @@ dcsCommon.version = "2.6.8" end function dcsCommon.copyArray(inArray) + if not inArray then return nil end + -- warning: this is a ref copy! local theCopy = {} for idx, element in pairs(inArray) do @@ -1686,7 +1696,29 @@ dcsCommon.version = "2.6.8" end end - + +function dcsCommon.synchGroupData(inGroupData) -- update group data block by +-- comparing it to spawned group and update units by x, y, heding and isExist +-- modifies inGroupData! + if not inGroupData then return end + -- groupdata from game, NOT MX DATA! + -- we synch the units and their coords + local livingUnits = {} + for idx, unitData in pairs(inGroupData.units) do + local theUnit = Unit.getByName(unitData.name) + if theUnit and theUnit:isExist() and theUnit:getLife()>1 then + -- update x and y and heading + local pos = theUnit:getPoint() + unitData.unitId = theUnit:getID() + unitData.x = pos.x + unitData.y = pos.z -- !!!! + unitData.heading = dcsCommon.getUnitHeading(gUnit) + table.insert(livingUnits, unitData) + end + end + inGroupData.units = livingUnits +end + -- -- -- M I S C M E T H O D S @@ -2440,6 +2472,42 @@ function dcsCommon.flagArrayFromString(inString, verbose) end return flags end + +function dcsCommon.objectHandler(theObject, theCollector) + table.insert(theCollector, theObject) + return true +end + +function dcsCommon.getSceneryObjectsInZone(theZone) -- DCS ZONE!!! + local aCat = 5 -- scenery + -- WARNING: WE ARE USING DCS ZONES, NOT CFX!!! + local p = {x=theZone.x, y=0, z=theZone.y} + local lp = {x = p.x, y = p.z} + p.y = land.getHeight(lp) + local collector = {} + + -- now build the search argument + local args = { + id = world.VolumeType.SPHERE, + params = { + point = p, + radius = theZone.radius + } + } + + -- now call search + world.searchObjects(aCat, args, dcsCommon.objectHandler, collector) + return collector +end + +function dcsCommon.getSceneryObjectInZoneByName(theName, theZone) -- DCS ZONE!!! + local allObs = dcsCommon.getSceneryObjectsInZone(theZone) + for idx, anObject in pairs(allObs) do + if tostring(anObject:getName()) == theName then return anObject end + end + return nil +end + -- -- -- INIT diff --git a/modules/persistence.lua b/modules/persistence.lua index 9f656eb..8d487cf 100644 --- a/modules/persistence.lua +++ b/modules/persistence.lua @@ -1,5 +1,5 @@ persistence = {} -persistence.version = "1.0.0" +persistence.version = "1.0.1" persistence.ups = 1 -- once every 1 seconds persistence.verbose = false persistence.active = false @@ -7,7 +7,7 @@ persistence.saveFileName = nil -- "mission data.txt" persistence.sharedDir = nil -- not yet implemented persistence.missionDir = nil -- set at start persistence.saveDir = nil -- set at start - +persistence.name = "persistence" -- for cfxZones persistence.missionData = {} -- loaded from file persistence.requiredLibs = { "dcsCommon", -- always @@ -16,6 +16,12 @@ persistence.requiredLibs = { --[[-- Version History 1.0.0 - initial version + 1.0.1 - when available, module sets flag "cfxPersistence" to 1 + - when data availabe, cfxPersistenceHasData is set to 1 + - spelling + - cfxZones interface + - always output save location + PROVIDES LOAD/SAVE ABILITY TO MODULES PROVIDES STANDALONE/HOSTED SERVER COMPATIOBILITY @@ -42,7 +48,7 @@ function persistence.registerModule(name, callbacks) -- and must be the one given when you retrieve it later persistence.callbacks[name] = callbacks if persistence.verbose then - trigger.action.outText("+++persistence: module <" .. name .. "> registred itself", 30) + trigger.action.outText("+++persistence: module <" .. name .. "> registered itself", 30) end end @@ -120,7 +126,10 @@ end -- function persistence.saveText(theString, fileName, shared, append) if not persistence.active then return false end - if not fileName then return false end + if not fileName then + trigger.action.outText("+++persistence: saveText without fileName") + return false + end if not shared then shared = flase end if not theString then theString = "" end @@ -140,6 +149,7 @@ function persistence.saveText(theString, fileName, shared, append) end if not theFile then + trigger.action.outText("+++persistence: saveText - unable to open " .. path, 30) return false end @@ -283,6 +293,7 @@ function persistence.missionStartDataLoad() -- can init from by data persistence.missionData = theData persistence.hasData = true + trigger.action.setUserFlag("cfxPersistenceHasData", 1) -- init my flags from last save local theFlags = theData["persistence.flagData"] @@ -359,9 +370,9 @@ function persistence.doSaveMission() return end - if persistence.verbose then - trigger.action.outText("+++persistence: mission saved", 30) - end +-- if persistence.verbose then + trigger.action.outText("+++persistence: mission saved to\n" .. persistence.missionDir .. persistence.saveFileName, 30) +-- end end function persistence.noteCleanRestart() @@ -546,6 +557,7 @@ function persistence.start() persistence.missionDir = missionDir persistence.active = true -- we can load and save data + trigger.action.setUserFlag("cfxPersistence", 1) persistence.hasData = false -- we do not have save data -- from here on we can read and write files in the missionDir diff --git a/modules/theDebugger.lua b/modules/theDebugger.lua index 7a80b0a..0948c8f 100644 --- a/modules/theDebugger.lua +++ b/modules/theDebugger.lua @@ -1,8 +1,8 @@ -- theDebugger debugger = {} -debugger.version = "1.1.1" +debugger.version = "1.1.2" debugDemon = {} -debugDemon.version = "1.1.1" +debugDemon.version = "1.1.2" debugger.verbose = false debugger.ups = 4 -- every 0.25 second @@ -22,6 +22,7 @@ debugger.log = "" - persistence of logs - save 1.1.1 - warning when trying to set a flag to a non-int + 1.1.2 - remove command --]]-- @@ -478,11 +479,6 @@ if not debugger.start() then debugger = nil end ---[[-- - debug on and off. globally, not per zone - ---]]-- - -- -- DEBUG DEMON @@ -625,15 +621,6 @@ end -- -- Helpers -- ---[[-- -function debugDemon.isObserving(flagName) - -- for now, we simply scan out own - for idx, aName in pairs(debugDemon.observer.flagArray) do - if aName == flagName then return true end - end - return false -end ---]]-- function debugDemon.createObserver(aName) local observer = cfxZones.createSimpleZone(aName) @@ -667,6 +654,7 @@ debugger.outText("*** debugger: commands are:" .. "\n\n " .. debugDemon.markOfDemon .. "snap [] -- create new snapshot of flags" .. "\n " .. debugDemon.markOfDemon .. "compare -- compare snapshot flag values with current" .. "\n " .. debugDemon.markOfDemon .. "note -- add to the text log" .. + "\n\n " .. debugDemon.markOfDemon .. "remove -- remove named item from mission" .. "\n\n " .. debugDemon.markOfDemon .. "start -- starts debugger" .. "\n " .. debugDemon.markOfDemon .. "stop -- stop debugger" .. @@ -1154,6 +1142,40 @@ function debugDemon.processSaveCommand(args, event) debugger.saveLog(aName) return true end + +function debugDemon.processRemoveCommand(args, event) + -- remove a group, unit or object + -- try group first + local aName = event.remainder + if not aName or aName:len() < 1 then + debugger.outText("*** remove: no remove target", 30) + return false + end + + aName = dcsCommon.trim(aName) + local theGroup = Group.getByName(aName) + if theGroup and theGroup:isExist() then + theGroup:destroy() + debugger.outText("*** remove: removed group <" .. aName .. ">", 30) + return true + end + + local theUnit = Unit.getByName(aName) + if theUnit and theUnit:isExist() then + theUnit:destroy() + debugger.outText("*** remove: removed unit <" .. aName .. ">", 30) + return true + end + + local theStatic = StaticObject.getByName(aName) + if theStatic and theStatic:isExist() then + theStatic:destroy() + debugger.outText("*** remove: removed static object <" .. aName .. ">", 30) + return true + end + debugger.outText("*** remove: did not find anything called <" .. aName .. "> to remove", 30) + return true +end -- -- init and start -- @@ -1223,6 +1245,7 @@ function debugDemon.init() debugDemon.addCommndProcessor("?", debugDemon.processHelpCommand) debugDemon.addCommndProcessor("help", debugDemon.processHelpCommand) + debugDemon.addCommndProcessor("remove", debugDemon.processRemoveCommand) return true end @@ -1259,6 +1282,11 @@ end - inspect objects, dumping category, life, if it's tasking, latLon, alt, speed, direction - exec files. save all commands and then run them from script - - remove units via delete and explode + + - query objects: -q persistence.active returns boolean, true + -q x.y returns table, 12 elements + -q a.b.x returns number 12 + -q d.e.f returns string "asdasda..." + -q sada reuturs --]]-- diff --git a/modules/unitPersistence.lua b/modules/unitPersistence.lua index a09e030..56414f0 100644 --- a/modules/unitPersistence.lua +++ b/modules/unitPersistence.lua @@ -1,6 +1,7 @@ unitPersistence = {} -unitPersistence.version = '1.0.0' +unitPersistence.version = '1.0.1' unitPersistence.verbose = false +unitPersistence.updateTime = 60 -- seconds. Once every minute check statics unitPersistence.requiredLibs = { "dcsCommon", -- always "cfxZones", -- Zones, of course @@ -10,6 +11,10 @@ unitPersistence.requiredLibs = { --[[-- Version History 1.0.0 - initial version + 1.0.1 - handles late activation + - handles linked static objects + - does no longer mess with heliports + - update statics once a minute, not second REQUIRES PERSISTENCE AND MX @@ -66,7 +71,7 @@ function unitPersistence.saveData() -- process all static objects placed with ME for oName, oData in pairs(unitPersistence.statics) do - if not oData.isDead then + if not oData.isDead or oData.lateActivation then -- fetch the object and see if it's still alive local theObject = StaticObject.getByName(oName) if theObject and theObject:isExist() then @@ -75,17 +80,16 @@ function unitPersistence.saveData() oData.x = pos.x oData.y = pos.z -- (!!) oData.isDead = theObject:getLife() < 1 --- trigger.action.outText("deadcheck: " .. oName .. " has health=" .. theObject:getLife(), 30) oData.dead = oData.isDead else oData.isDead = true oData.dead = true --- trigger.action.outText("deadcheck: " .. oName .. " certified dead", 30) end end if unitPersistence.verbose then local note = "(ok)" if oData.isDead then note = "(dead)" end + if oData.lateActivation then note = "(late active)" end trigger.action.outText("unitPersistence: save - processed group <" .. oName .. ">. " .. note, 30) end end @@ -154,7 +158,7 @@ function unitPersistence.loadMission() trigger.action.outText("+++ failed to add modified group <" .. groupName .. ">") end if unitPersistence.verbose then - trigger.action.outText("+++unitPersistence: updated group <" .. groupName .. "> of cat <" .. cat .. "> for cty <" .. cty .. ">", 30) + -- trigger.action.outText("+++unitPersistence: updated group <" .. groupName .. "> of cat <" .. cat .. "> for cty <" .. cty .. ">", 30) end end end @@ -167,11 +171,27 @@ function unitPersistence.loadMission() -- and now the same for static objects if theData.statics then for name, staticData in pairs(theData.statics) do - local theStatic = StaticObject.getByName(name) - if not theStatic then - mismatchWarning = true + --local theStatic = StaticObject.getByName(name) + if staticData.lateActivation then + -- this one will not be in the game now, skip + if unitPersistence.verbose then + trigger.action.outText("+++unitPersistence: static <" .. name .. "> is late activate, no update", 30) + end + --elseif not theStatic then + -- mismatchWarning = true + elseif staticData.category == "Heliports" then + -- FARPS are static objects that HATE to be + -- messed with, so we don't + if unitPersistence.verbose then + trigger.action.outText("+++unitPersistence: static <" .. name .. "> is Heliport, no update", 30) + end else local newStatic = dcsCommon.clone(staticData) + -- add link info if it exists + newStatic.linkUnit = cfxMX.linkByName[name] + if newStatic.linkUnit and unitPersistence.verbose then + trigger.action.outText("+++unitPersistence: linked static <" .. name .. "> to unit <" .. newStatic.linkUnit .. ">", 30) + end local cty = staticData.cty local cat = staticData.cat -- spawn new one, replacing same.named old, dead if required @@ -182,7 +202,7 @@ function unitPersistence.loadMission() if unitPersistence.verbose then local note = "" if newStatic.dead then note = " (dead)" end - trigger.action.outText("+++unitPersistence: updated static <" .. name .. "> for cty <" .. cty .. ">" .. note, 30) + -- trigger.action.outText("+++unitPersistence: updated static <" .. name .. "> for cty <" .. cty .. ">" .. note, 30) end end end @@ -197,6 +217,31 @@ function unitPersistence.loadMission() end end +-- +-- Update +-- +function unitPersistence.update() + -- we check every minute + timer.scheduleFunction(unitPersistence.update, {}, timer.getTime() + unitPersistence.updateTime) + -- do a quick scan for all late activated static objects and if they + -- are suddently visible, remove their late activate state + --for groupName, groupdata in pairs(unitPersistence.groundTroops) do + -- currently not needed + --end + + for objName, objData in pairs(unitPersistence.statics) do + if objData.lateActivation then + local theStatic = StaticObject.getByName(objData.name) + if theStatic then + objData.lateActivation = false + if unitPersistence.verbose then + trigger.action.outText("+++unitPersistence: <" .. objData.name .. "> has activated", 30) + end + end + end + end +end + -- -- Start -- @@ -250,21 +295,29 @@ function unitPersistence.start() theStatic.isDead = false theStatic.groupId = mxData.groupId theStatic.cat = cfxMX.catText2ID("static") + theStatic.cty = cfxMX.countryByName[name] local gameOb = StaticObject.getByName(theStatic.name) if not gameOb then - trigger.action.outText("+++warning: static object <" .. theStatic.name .. "> does not exist in-game!?", 30) + if unitPersistence.verbose then + trigger.action.outText("+++unitPersistence: static object <" .. theStatic.name .. "> has late activation", 30) + end + theStatic.lateActivation = true else - theStatic.cty = gameOb:getCountry() - unitPersistence.statics[theStatic.name] = theStatic + --theStatic.cty = gameOb:getCountry() + --unitPersistence.statics[theStatic.name] = theStatic end + unitPersistence.statics[theStatic.name] = theStatic end end - + -- when we run, persistence has run and may have data ready for us if persistence.hasData then unitPersistence.loadMission() end + -- start update + unitPersistence.update() + return true end @@ -273,4 +326,8 @@ if not unitPersistence.start() then trigger.action.outText("+++ unit persistence not available", 30) end unitPersistence = nil -end \ No newline at end of file +end +--[[-- + ToDo: linked statics and linked units on restore + +--]]-- diff --git a/tutorial & demo missions/demo - recon mode - reloaded.miz b/tutorial & demo missions/demo - recon mode - reloaded.miz index ff19c71..277b821 100644 Binary files a/tutorial & demo missions/demo - recon mode - reloaded.miz and b/tutorial & demo missions/demo - recon mode - reloaded.miz differ