diff --git a/Doc/DML Documentation.pdf b/Doc/DML Documentation.pdf index 417b5ce..68abda0 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 b58e70b..dfceb0e 100644 Binary files a/Doc/DML Quick Reference.pdf and b/Doc/DML Quick Reference.pdf differ diff --git a/modules/WHpersistence.lua b/modules/WHpersistence.lua new file mode 100644 index 0000000..802a4cc --- /dev/null +++ b/modules/WHpersistence.lua @@ -0,0 +1,96 @@ +WHpersistence = {} +WHpersistence.version = "1.0.0" +WHpersistence.requiredLibs = { + "dcsCommon", + "cfxZones", + "persistence", +} +-- +-- load / save (game data) +-- +function WHpersistence.saveData() + local theData = {} + local theWH = {} + -- generate all WH data from all my airfields + local allMyBase = world:getAirbases() + for idx, theBase in pairs(allMyBase) do + local name = theBase:getName() + local WH = theBase:getWarehouse() + local inv = WH:getInventory() + theWH[name] = inv + end + theData.theWH = theWH + return theData, WHpersistence.sharedData -- second val currently nil +end + +function WHpersistence.loadData() + if not persistence then return end + local shared = nil + local theData = persistence.getSavedDataForModule("WHpersistence") + if (not theData) or not (theData.theWH) then + if WHpersistence.verbose then + trigger.action.outText("+++WHp: no save date received, skipping.", 30) + end + return + end + -- set up all warehouses from data loaded + for name, inv in pairs(theData.theWH) do + trigger.action.outText("+++restoring <" .. name .. ">", 30) + local theBase = Airbase.getByName(name) + if theBase then + local theWH = theBase:getWarehouse() + if theWH then + -- we go through weapon, liquids and aircraft + for idx, liq in pairs(inv.liquids) do + theWH:setLiquidAmount(idx, liq) + trigger.action.outText(name .. ": Liq <" .. idx .. "> : <" .. liq .. ">", 30) + end + for ref, num in pairs(inv.weapon) do + theWH:setItem(ref, num) + end + for ref, num in pairs(inv.aircraft) do + theWH:setItem(ref, num) + end + else + trigger.action.outText(name .. ": no warehouse") + end + else + trigger.action.outText(name .. ": no airbase") + end + end +end +-- +-- config +-- +function WHpersistence.readConfigZone() + local theZone = cfxZones.getZoneByName("WHpersistenceConfig") + if not theZone then + theZone = cfxZones.createSimpleZone("WHpersistenceConfig") + end + WHpersistence.verbose = theZone.verbose +end +-- +-- GO +-- +function WHpersistence.start() + if not dcsCommon.libCheck then + trigger.action.outText("cfx WHpersistence requires dcsCommon", 30) + return false + end + if not dcsCommon.libCheck("cfx Raise Flag", WHpersistence.requiredLibs)then return false end + WHpersistence.readConfigZone() + if persistence then + callbacks = {} + callbacks.persistData = WHpersistence.saveData + persistence.registerModule("WHpersistence", callbacks) + -- now load my data + WHpersistence.loadData() + end + trigger.action.outText("cfx WHpersistence v" .. WHpersistence.version .. " started.", 30) + return true +end + +if not WHpersistence.start() then + trigger.action.outText("cfx WHpersistence aborted: missing libraries", 30) + WHpersistence = nil +end \ No newline at end of file diff --git a/modules/airfield.lua b/modules/airfield.lua index 41f2769..89f9a89 100644 --- a/modules/airfield.lua +++ b/modules/airfield.lua @@ -1,5 +1,5 @@ airfield = {} -airfield.version = "2.2.0" +airfield.version = "2.3.0" airfield.requiredLibs = { "dcsCommon", "cfxZones", @@ -22,6 +22,12 @@ airfield.allAirfields = {} -- inexed by af name, db entries: base, cat 2.1.0 - added support for makeNeutral? 2.1.1 - bug fixing for DCS 2.9x airfield retrofit 2.2.0 - dmlZone:getCoalition() / masterowner adaptation for owner + 2.3.0 - increased verbosity on persistence + - airfield delayed release after data load + - locked owner on start + - release after start + - redrawing airfields when releasing + - cleanup --]]-- -- init all airfields DB @@ -31,7 +37,6 @@ function airfield.collectAll() local dropped = 0 for idx, aBase in pairs(allBases) do local entry = {} - --local cat = Airbase.getCategory(aBase) -- DCS 2.9 hardened -- ho! dcs 2.9.x retrofit screwed with Airfield.getCategory. local cat = aBase:getDesc().category -- cats: 0 = airfield, 1 = farp, 2 = ship @@ -44,20 +49,18 @@ function airfield.collectAll() count = count + 1 else dropped = dropped + 1 --- trigger.action.outText("***dropped airbase <" .. aBase:getName() .. ">, cat = <" .. cat .. ">", 30) end end if airfield.verbose then trigger.action.outText("+++airF: init - count = <" .. count .. ">, dropped = <" .. dropped .. ">", 30) end end - -- -- setting up airfield -- +airfield.collector = {} function airfield.createAirFieldFromZone(theZone) theZone.farps = theZone:getBoolFromZoneProperty("farps", false) - local filterCat = 0 if (theZone.farps) then filterCat = {0, 1} end -- bases and farps local p = theZone:getPoint() @@ -65,17 +68,15 @@ function airfield.createAirFieldFromZone(theZone) theZone.airfield = theBase theZone.afName = theBase:getName() - -- set zone's owner + -- set zone's owner theZone.owner = theBase:getCoalition() theZone.mismatchCount = airfield.gracePeriod if theZone.verbose or airfield.verbose then trigger.action.outText("+++airF: airfield zone <" .. theZone.name .. "> associates with <" .. theZone.afName .. ">, current owner is <" .. theZone.owner .. ">", 30) end - -- methods theZone.method = theZone:getStringFromZoneProperty("method", "inc") - theZone.triggerMethod = theZone:getStringFromZoneProperty("triggerMethod", "change") - + theZone.triggerMethod = theZone:getStringFromZoneProperty("triggerMethod", "change") if theZone:hasProperty("red!") then theZone.redCap = theZone:getStringFromZoneProperty("red!") end @@ -92,23 +93,20 @@ function airfield.createAirFieldFromZone(theZone) theZone.makeBlue = theZone:getStringFromZoneProperty("makeBlue?", "") theZone.lastMakeBlue = trigger.misc.getUserFlag(theZone.makeBlue) end - if theZone:hasProperty("makeNeutral?") then theZone.makeNeutral = theZone:getStringFromZoneProperty("makeNeutral?", "") theZone.lastMakeNeutral = trigger.misc.getUserFlag(theZone.makeNeutral) end - if theZone:hasProperty("autoCap?") then theZone.autoCap = theZone:getStringFromZoneProperty("autoCap?", "") theZone.lastAutoCap = trigger.misc.getUserFlag(theZone.autoCap) end theZone.directControl = theZone:getBoolFromZoneProperty("directControl", false) - if theZone.directControl then airfield.assumeControl(theZone) end - + -- if fixed attribute, we switch to that color and keep it fixed. -- can be overridden by either makeXX or autoCap. if theZone:hasProperty("fixed") then @@ -144,6 +142,14 @@ function airfield.createAirFieldFromZone(theZone) airfield.showAirfield(theZone) + theBase:autoCapture(false) -- lock down owner to avoid contested at beginning + -- set up collector to free ownership later + if theZone:hasProperty("fixed") then + airfield.collector[theBase] = false -- autocap off after delay + else + airfield.collector[theBase] = true + end + -- now mark this zone as handled local entry = airfield.allAirfields[theZone.afName] if not entry then @@ -179,7 +185,6 @@ function airfield.showAirfield(theZone) theZone.ownerMark = nil end if not theZone.show then return end -- we don't show in map - local lineColor = theZone.redLine -- {1.0, 0, 0, 1.0} -- red local fillColor = theZone.redFill -- {1.0, 0, 0, 0.2} -- red local owner = theZone:getCoalition() -- .owner @@ -190,10 +195,7 @@ function airfield.showAirfield(theZone) lineColor = theZone.neutralLine -- {0.8, 0.8, 0.8, 1.0} fillColor = theZone.neutralFill -- {0.8, 0.8, 0.8, 0.2} end - theZone.ownerMark = airfield.markAirfieldOnMap(theZone.airfield, lineColor, fillColor) - - end function airfield.assumeControl(theZone) @@ -211,7 +213,6 @@ function airfield.relinquishControl(theZone) end theBase:autoCapture(true) -- turn off autocap end - -- -- event handling -- @@ -252,7 +253,6 @@ function airfield.airfieldCaptured(theBase) if theZone.verbose or airfield.verbose then trigger.action.outText("+++airF: capturing <" .. bName .. "> for zone <" .. theZone.name .. ">", 30) end - local newCoa = theBase:getCoalition() theZone.owner = newCoa @@ -272,7 +272,6 @@ function airfield.airfieldCaptured(theBase) if theZone.blueCap and newCoa == 2 then theZone:pollFlag(theZone.blueCap, theZone.method) end - end function airfield:onEvent(event) @@ -347,7 +346,6 @@ function airfield.update() end theZone.owner = 0 end - if theZone.autoCap and theZone:testZoneFlag(theZone.autoCap, theZone.triggerMethod, "lastAutoCap") then if theAirfield:autoCaptureIsOn() then @@ -386,16 +384,12 @@ function airfield.GC() trigger.action.outText("+++airF: corrected ownership after grace period", 30) end end - end end - end - -- -- LOAD / SAVE -- - function airfield.saveData() local theData = {} local allAF = {} @@ -411,6 +405,21 @@ function airfield.saveData() return theData end +function airfield.releaseFields(releaseMe) + for theAF, rel in pairs(releaseMe) do + theAF:autoCapture(rel) + if airfield.verbose then + trigger.action.outText("+++airF: releasing AF <" .. theAF:getName() .. "> to saved cap state <" .. dcsCommon.bool2Text(rel) .. ">", 30) + end + end + for name, theZone in pairs(airfield.myAirfields) do + airfield.showAirfield(theZone) + if airfield.verbose or theZone.verbose then + trigger.action.outText("+++airF: redrawing <" .. theZone.name .. ">", 30) + end + end +end + function airfield.loadData() if not persistence then return end local theData = persistence.getSavedDataForModule("airfield") @@ -418,6 +427,7 @@ function airfield.loadData() if airfield.verbose then trigger.action.outText("+++airF persistence: no save data received, skipping.", 30) end + timer.scheduleFunction(airfield.releaseFields, airfield.collector, timer.getTime() + 2) return end @@ -426,9 +436,12 @@ function airfield.loadData() if airfield.verbose then trigger.action.outText("+++airF persistence: no airfield data, skipping", 30) end + timer.scheduleFunction(airfield.releaseFields, airfield.collector, timer.getTime() + 2) return end + airfield.collector = {} -- overwrite existing + for theName, AFData in pairs(allAF) do local theZone = airfield.myAirfields[theName] if theZone then @@ -438,20 +451,22 @@ function airfield.loadData() theAirfield:autoCapture(false) theAirfield:setCoalition(AFData.owner) theZone.owner = AFData.owner + if airfield.verbose or theZone.verbose then + trigger.action.outText("+++airF: setting AF Zone <" .. theZone.name .. ">, owner from file to <" .. theZone.owner .. ">", 30) + end -- set ownedBy# if theZone.ownedBy then trigger.action.setUserFlag(theZone.ownedBy, theZone.owner) end -- set owning mode: autocap or direct - theAirfield:autoCapture(AFData.autocapActive) - + --theAirfield:autoCapture(AFData.autocapActive) + airfield.collector[theAirfield] = AFData.autocapActive else trigger.action.outText("+++airF persistence: cannot synch airfield <" .. theName .. ">, skipping", 40) end end + timer.scheduleFunction(airfield.releaseFields, airfield.collector, timer.getTime() + 2) end - - -- -- start up -- @@ -460,9 +475,7 @@ function airfield.readConfig() if not theZone then theZone = cfxZones.createSimpleZone("airfieldConfig") end - airfield.verbose = theZone.verbose --- airfield.farps = theZone:getBoolFromZoneProperty("farps", false) - + airfield.verbose = theZone.verbose -- colors for line and fill airfield.redLine = theZone:getRGBAVectorFromZoneProperty("redLine", {1.0, 0, 0, 1.0}) airfield.redFill = theZone:getRGBAVectorFromZoneProperty("redFill", {1.0, 0, 0, 0.2}) @@ -470,7 +483,6 @@ function airfield.readConfig() airfield.blueFill = theZone:getRGBAVectorFromZoneProperty("blueFill", {0.0, 0, 1.0, 0.2}) airfield.neutralLine = theZone:getRGBAVectorFromZoneProperty("neutralLine", {0.8, 0.8, 0.8, 1.0}) airfield.neutralFill = theZone:getRGBAVectorFromZoneProperty("neutralFill", {0.8, 0.8, 0.8, 0.2}) - airfield.showAll = theZone:getBoolFromZoneProperty("show", false) end @@ -485,13 +497,10 @@ end function airfield.start() if not dcsCommon.libCheck("cfx airfield", airfield.requiredLibs) then return false end - -- set up DB airfield.collectAll() - -- read config airfield.readConfig() - -- read bases local abZones = cfxZones.zonesWithProperty("airfield") for idx, aZone in pairs(abZones) do @@ -512,6 +521,8 @@ function airfield.start() persistence.registerModule("airfield", callbacks) -- now load my data airfield.loadData() + else + timer.scheduleFunction(airfield.releaseFields, airfield.collector, timer.getTime() + 2) -- release airfields when not loaded from storage end -- start update in 1 second diff --git a/modules/autoCSAR.lua b/modules/autoCSAR.lua index 7b5ec0a..51053c1 100644 --- a/modules/autoCSAR.lua +++ b/modules/autoCSAR.lua @@ -1,5 +1,5 @@ autoCSAR = {} -autoCSAR.version = "2.2.0" +autoCSAR.version = "2.2.1" autoCSAR.requiredLibs = { "dcsCommon", -- always "cfxZones", -- Zones, of course @@ -19,7 +19,8 @@ autoCSAR.trackedEjects = {} -- we start tracking on eject 2.1.0 - persistence support 2.2.0 - new noExploit option in config - no csar mission if pilot lands too close to airbase or farp - and noExploit is on + and noExploit is on + 2.2.1 - DCS hardening for isExist --]]-- autoCSAR.forbidden = {} -- indexed by name, contains point autoCSAR.killDist = 2100 -- meters from center of forbidden @@ -173,7 +174,9 @@ function autoCSAR:onEvent(event) local coa = event.initiator:getCoalition() -- see if pilot has ejector seat and prepare to connect one with the other local info = nil - if event.target and event.target:isExist() then + if event.target + and event.target.isExist + and event.target:isExist() then -- DCS hardening info = {} info.coa = coa info.seat = event.target diff --git a/modules/cloneZone.lua b/modules/cloneZone.lua index ef03d40..c6a5ede 100644 --- a/modules/cloneZone.lua +++ b/modules/cloneZone.lua @@ -1,5 +1,5 @@ cloneZones = {} -cloneZones.version = "2.4.0" +cloneZones.version = "2.5.0" cloneZones.verbose = false cloneZones.requiredLibs = { "dcsCommon", -- always @@ -57,6 +57,8 @@ cloneZones.respawnOnGroupID = true path) with wiper module - using "wipe?" will now create a warning 2.4.0 - reworked masterOwner to fit with dmlZone + 2.5.0 - re-establish spawn zone in persistence to provide + empty! detection through saves (missed hasClones) --]]-- -- @@ -90,7 +92,6 @@ function cloneZones.invokeCallbacks(theZone, reason, args) if not theZone then return end if not reason then reason = "" end if not args then args = {} end - -- invoke anyone who wants to know that a group -- of people was rescued. for idx, cb in pairs(cloneZones.callbacks) do @@ -105,7 +106,6 @@ function cloneZones.partOfGroupDataInZone(theZone, theUnits) local zP = cfxZones.getPoint(theZone) zP = theZone:getDCSOrigin() -- don't use getPoint now. zP.y = 0 - for idx, aUnit in pairs(theUnits) do local uP = {} uP.x = aUnit.x @@ -197,7 +197,6 @@ function cloneZones.createClonerWithZone(theZone) -- has "Cloner" table.insert(theZone.myStatics, aStatic) end end - cloneZones.despawnAll(theZone) if (#theZone.cloneNames + #theZone.staticNames) < 1 then if cloneZones.verbose then @@ -210,18 +209,12 @@ function cloneZones.createClonerWithZone(theZone) -- has "Cloner" trigger.action.outText(theZone.name .. " clone template saved", 30) end end - - -- declutter theZone.declutter = theZone:getBoolFromZoneProperty("declutter", false) - - -- watchflags theZone.cloneTriggerMethod = theZone:getStringFromZoneProperty("triggerMethod", "change") - if theZone:hasProperty("cloneTriggerMethod") then theZone.cloneTriggerMethod = theZone:getStringFromZoneProperty("cloneTriggerMethod", "change") end - -- f? and spawn? and other synonyms map to the same if theZone:hasProperty("f?") then theZone.spawnFlag = theZone:getStringFromZoneProperty("f?", "none") elseif theZone:hasProperty("in?") then @@ -231,12 +224,10 @@ function cloneZones.createClonerWithZone(theZone) -- has "Cloner" elseif theZone:hasProperty("clone?") then theZone.spawnFlag = theZone:getStringFromZoneProperty("clone?", "none") end - if theZone.spawnFlag then theZone.lastSpawnValue = theZone:getFlagValue(theZone.spawnFlag) end - -- deSpawn? if theZone:hasProperty("deSpawn?") then theZone.deSpawnFlag = theZone:getStringFromZoneProperty( "deSpawn?", "none") elseif theZone:hasProperty("deClone?") then @@ -259,7 +250,7 @@ function cloneZones.createClonerWithZone(theZone) -- has "Cloner" theZone.onStart = theZone:getBoolFromZoneProperty("onStart", false) theZone.moveRoute = theZone:getBoolFromZoneProperty("moveRoute", false) theZone.preWipe = theZone:getBoolFromZoneProperty("preWipe", false) - + if theZone:hasProperty("empty!") then theZone.emptyBangFlag = theZone:getStringFromZoneProperty("empty!", "") -- note string on number default end @@ -267,30 +258,7 @@ function cloneZones.createClonerWithZone(theZone) -- has "Cloner" theZone.cloneMethod = theZone:getStringFromZoneProperty("cloneMethod", "inc") if theZone:hasProperty("method") then theZone.cloneMethod = theZone:getStringFromZoneProperty("method", "inc") -- note string on number default - end - - --[[-- - if theZone:hasProperty("masterOwner") then - theZone.masterOwner = theZone:getStringFromZoneProperty( "masterOwner", "*") - theZone.masterOwner = dcsCommon.trim(theZone.masterOwner) - if theZone.masterOwner == "*" then - theZone.masterOwner = theZone.name - if theZone.verbose then - trigger.action.outText("+++clnZ: masterOwner for <" .. theZone.name .. "> set successfully to to itself, currently owned by faction <" .. theZone.owner .. ">", 30) - end - end - if theZone.verbose or cloneZones.verbose then - trigger.action.outText("+++clnZ: ownership of <" .. theZone.name .. "> tied to zone <" .. theZone.masterOwner .. ">", 30) - end - -- check that the zone exists in DCS - local theMaster = cfxZones.getZoneByName(theZone.masterOwner) - if not theMaster then - trigger.action.outText("clnZ: WARNING: cloner's <" .. theZone.name .. "> master owner named <" .. theZone.masterOwner .. "> does not exist!", 30) - end - theZone.masterOwner = theMaster - end - --]]-- - + end theZone.turn = theZone:getNumberFromZoneProperty("turn", 0) -- interface to groupTracker @@ -329,7 +297,6 @@ function cloneZones.createClonerWithZone(theZone) -- has "Cloner" theZone.inBuiltup = theZone:getNumberFromZoneProperty("inBuiltup", 10) -- 10 meter radius must be free -- small houses end theZone.rndHeading = theZone:getBoolFromZoneProperty("rndHeading", false) - theZone.onRoad = theZone:getBoolFromZoneProperty("onRoad", false) theZone.onPerimeter = theZone:getBoolFromZoneProperty("onPerimeter", false) @@ -338,15 +305,12 @@ function cloneZones.createClonerWithZone(theZone) -- has "Cloner" theZone.identical = theZone:getBoolFromZoneProperty("identical", false) if theZone.identical == false then theZone.identical = nil end end - if theZone:hasProperty("nameScheme") then theZone.nameScheme = theZone:getStringFromZoneProperty( "nameScheme", "-") -- default to [ "-" ] end - if theZone:hasProperty("groupScheme") then theZone.groupScheme = theZone:getStringFromZoneProperty("groupScheme", "-") end - if theZone.identical and theZone.nameScheme then trigger.action.outText("+++clnZ: WARNING - clone zone <" .. theZone.name .. "> has both IDENTICAL and NAMESCHEME/GROUPSCHEME attributes. nameScheme is ignored.", 30) theZone.nameScheme = nil @@ -539,7 +503,6 @@ function cloneZones.updateLocationsInGroupData(theData, zoneDelta, adjustAllWayp -- now process departing slot if given if departFromAerodrome then -- we may need alt from land to add here, maybe later - -- now process parking slots, and choose closest slot -- per unit's location if fromParking then @@ -658,11 +621,9 @@ function cloneZones.uniqueNameStaticData(theData, theCloneZone, sourcename) if theCloneZone and theCloneZone.nameScheme then local schema = theCloneZone.nameScheme newName, iterCount = cloneZones.nameFromSchema(schema, theData.name, theCloneZone, sourceName, iterCount) - if theCloneZone.verbose then trigger.action.outText("clnZ: zone <" .. theCloneZone.name .. "> static schema <" .. schema .. ">: <" .. theData.name .. "> --> <" .. newName .. ">", 30) end - theData.name = newName -- dcsCommon.uuid(theData.name) else -- default naming scheme: - @@ -697,19 +658,6 @@ end function cloneZones.resolveOwnership(spawnZone, ctry) if not spawnZone.masterOwner then return ctry end -- old code ---[[-- - local masterZone = cfxZones.getZoneByName(spawnZone.masterOwner) - if not masterZone then - trigger.action.outText("+++clnZ: cloner " .. spawnZone.name .. " could not find master owner <" .. spawnZone.masterOwner .. ">", 30) - return ctry - end - - if not masterZone.owner then - return ctry - end ---]]-- - --- ctry = dcsCommon.getACountryForCoalition(masterZone.owner) ctry = dcsCommon.getACountryForCoalition(spawnZone:getCoalition()) return ctry end @@ -1249,6 +1197,8 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone) -- clone for persistence local theData = dcsCommon.clone(rawData) + -- remember the zone that spawned this particular group + theData.CZspawner = spawnZone.name cloneZones.allClones[rawData.name] = theData if cloneZones.verbose or spawnZone.verbose then @@ -1273,7 +1223,6 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone) info.timeLimit = now + timeLimit info.cloneZone = spawnZone table.insert(cloneZones.despawnPlan, info) --- trigger.action.outText("+++clne: scheduled auto-despawn for <" .. info.name .. "> in <" .. timeLimit .. "> secs", 30) end -- turn off AI if disabled @@ -1446,6 +1395,7 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone) rawData.cty = ctry -- save for persistence local theData = dcsCommon.clone(rawData) + theData.CZspawner = spawnZone.name -- remember spawner cloneZones.allCObjects[rawData.name] = theData if cloneZones.verbose or spawnZone.verbose then @@ -1466,7 +1416,6 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone) info.timeLimit = now + timeLimit info.cloneZone = spawnZone table.insert(cloneZones.despawnPlan, info) --- trigger.action.outText("+++clne: scheduled auto-despawn for OBJECT <" .. info.name .. "> in <" .. timeLimit .. "> secs", 30) end -- we don't mix groups with units, so no lookup tables for -- statics @@ -1528,9 +1477,7 @@ end function cloneZones.spawnWithSpawner(theZone) -- analog to cfxSpawnZones.spawnWithSpawner(theSpawner) -- glue code for helo troops and other modules - -- we may want to check if cloner isn't emtpy first - cloneZones.spawnWithCloner(theZone) end @@ -1702,15 +1649,6 @@ end function cloneZones.resolveOwningCoalition(theZone) return theZone:getCoalition() ---[[-- - if not theZone.masterOwner then return theZone.owner end - local masterZone = cfxZones.getZoneByName(theZone.masterOwner) - if not masterZone then - trigger.action.outText("+++clnZ: cloner " .. theZone.name .. " could not find master owner <" .. theZone.masterOwner .. ">", 30) - return theZone.owner - end - return masterZone.owner ---]]-- end function cloneZones.getRequestableClonersInRange(aPoint, aRange, aSide) @@ -1821,7 +1759,6 @@ function cloneZones.update() local filtered = {} for idx, theInfo in pairs(cloneZones.despawnPlan) do if theInfo.timeLimit < now then --- trigger.action.outText("+++clne: auto-despawning <" .. theInfo.name .. ">", 30) if theInfo.isObject then -- dealloc static object local theObject = theInfo.theGroup @@ -1857,7 +1794,6 @@ function cloneZones.doOnStart() end end end - -- -- Regular GC and housekeeping -- @@ -1884,7 +1820,6 @@ function cloneZones.GC() end end cloneZones.allClones = filteredAttackers - filteredAttackers = {} for gName, gData in pairs (cloneZones.allCObjects) do -- all we need to do is get the group of that name @@ -1904,8 +1839,6 @@ function cloneZones.houseKeeping() timer.scheduleFunction(cloneZones.houseKeeping, {}, timer.getTime() + 5 * 60) -- every 5 minutes cloneZones.GC() end - - -- -- LOAD / SAVE -- @@ -1969,7 +1902,7 @@ function cloneZones.saveData() cData.myUniqueCounter = theCloner.myUniqueCounter cData.oSize = theCloner.oSize cData.lastSize = theCloner.lastSize - -- mySpawns: all groups i'm curently observing for empty! + -- mySpawns: all groups I'm curently observing for empty! -- myStatics: dto for objects local mySpawns = {} for idx, aGroup in pairs(theCloner.mySpawns) do @@ -2071,6 +2004,7 @@ function cloneZones.loadData() local theGroup = Group.getByName(aName) if theGroup then table.insert(mySpawns, theGroup) + theCloner.hasClones = true -- was missing! else trigger.action.outText("+++clnZ - persistence: can't reconnect cloner <" .. cName .. "> with clone group <".. aName .. ">", 30) end @@ -2082,6 +2016,7 @@ function cloneZones.loadData() local theStatic = StaticObject.getByName(aName) if theStatic then table.insert(myStatics, theStatic) + theCloner.hasClones = true -- was missing!S else trigger.action.outText("+++clnZ - persistence: can't reconnect cloner <" .. cName .. "> with static <".. aName .. ">", 30) end @@ -2115,24 +2050,16 @@ function cloneZones.readConfigZone() end theZone = cfxZones.createSimpleZone("cloneZonesConfig") end - if theZone:hasProperty("uniqueCount") then cloneZones.uniqueCounter = theZone:getNumberFromZoneProperty("uniqueCount", cloneZone.uniqueCounter) end - if theZone:hasProperty("localCount") then cloneZones.lclUniqueCounter = theZone:getNumberFromZoneProperty("localCount", cloneZone.lclUniqueCounter) end - if theZone:hasProperty("globalCount") then cloneZones.globalCounter = theZone:getNumberFromZoneProperty("globalCount", cloneZone.globalCounter) end - - cloneZones.verbose = theZone:getBoolFromZoneProperty("verbose", false) - - if cloneZones.verbose then - trigger.action.outText("+++clnZ: read config", 30) - end + cloneZones.verbose = theZone.verbose end function cloneZones.start() @@ -2201,4 +2128,5 @@ end make example where transport can be different plane types but have same name support 'orders' to complete replace routes, and pass to groundCommander like spawner. only for ground troops + maxCycles - maximum number of clone cycles. emulate with countdown? --]]-- \ No newline at end of file diff --git a/modules/heloTroops.lua b/modules/heloTroops.lua index 05a180b..ebed7b3 100644 --- a/modules/heloTroops.lua +++ b/modules/heloTroops.lua @@ -1,5 +1,5 @@ cfxHeloTroops = {} -cfxHeloTroops.version = "3.1.3" +cfxHeloTroops.version = "3.1.5" cfxHeloTroops.verbose = false cfxHeloTroops.autoDrop = true cfxHeloTroops.autoPickup = false @@ -23,7 +23,8 @@ cfxHeloTroops.requestRange = 500 -- meters 3.1.3 - decycled structures (destination zone) on save - upcycled structures (destination) on load - loadSound and disembarkSound - + 3.1.4 - guarding destination access in save + 3.1.5 - more guarding of destination access --]]-- @@ -926,9 +927,14 @@ function cfxHeloTroops.saveData() for gName, gData in pairs(cfxHeloTroops.deployedTroops) do local sData = dcsCommon.clone(gData) dcsCommon.synchGroupData(sData.groupData) - if sData.destination then - net.log("cfxHeloTroops: decycling troop 'destination' for <" .. sData.destination:getName() .. ">") - sData.destination = sData.destination:getName() + if sData.destination then + if type(sData.destination) == "table" and (sData.destination.name) then + net.log("cfxHeloTroops: decycling troop 'destination' for <" .. sData.destination.name .. ">") + sData.destination = sData.destination.name + else + sData.destination = nil + net.log("cfxHeloTroops: decycling deployed troops 'destination' nilling for safety") + end end allTroopData[gName] = sData end diff --git a/modules/impostors.lua b/modules/impostors.lua index 095b41f..944f7b6 100644 --- a/modules/impostors.lua +++ b/modules/impostors.lua @@ -1,6 +1,6 @@ impostors={} -impostors.version = "1.1.0" +impostors.version = "1.2.0" impostors.verbose = false impostors.ups = 1 impostors.requiredLibs = { @@ -21,7 +21,7 @@ impostors.uniqueCounter = 8200000 -- clones start at 9200000 1.1.0 - filtered dead units during spawns cleanup some performance boost for mx lookup - + 1.2.0 - filters dead groups entirely LIMITATIONS: must be on ground (or would be very silly does not work with any units deployed on ships @@ -296,7 +296,7 @@ function impostors.spawnGroupsFromImpostor(theZone) local deadUnits = {} -- collect all dead units for immediate delete -- after spawning - + local filtered = {} for idx, groupName in pairs(theZone.groupNames) do -- get my group data from MX based on my name -- we get from MX so we get all path and order info @@ -306,67 +306,75 @@ function impostors.spawnGroupsFromImpostor(theZone) local cat = cfxMX.groupCatByName[groupName] local ctry = cfxMX.countryByName[groupName] local impostorGroup = theZone.myImpostors[groupName] - local relinkZones = {} - -- now iterate all units in that group, and remove their impostors - for idy, theUnit in pairs(rawData.units) do - if theUnit and theUnit.name then - local impName = impostorGroup[theUnit.name] - if not impName then - if theZone.verbose then - trigger.action.outText("group <" .. groupName .. ">: no impostor for <" .. theUnit.name .. ">", 30) - end - else - local impStat = StaticObject.getByName(impName) - if impStat and impStat:isExist() and impStat:getLife() > 1 then - -- still alive. read x, y and heading - local sp = impStat:getPoint() - theUnit.x = sp.x - theUnit.y = sp.z -- !!! - theUnit.heading = dcsCommon.getUnitHeading(impStat) -- should also work for statics - -- should automatically handle ["livery_id"] - relinkZones[theUnit.name] = cfxZones.zonesLinkedToUnit(impStat) + if impostorGroup then + table.insert(filtered, groupName) + local relinkZones = {} + -- now iterate all units in that group, and remove their impostors + for idy, theUnit in pairs(rawData.units) do + if theUnit and theUnit.name then + local impName = impostorGroup[theUnit.name] + if not impName then + if theZone.verbose then + trigger.action.outText("group <" .. groupName .. ">: no impostor for <" .. theUnit.name .. ">", 30) + end else - -- dead - table.insert(deadUnits, theUnit.name) - end - -- destroy imp - if impStat and impStat:isExist() then - impStat:destroy() + local impStat = StaticObject.getByName(impName) + if impStat and impStat:isExist() and impStat:getLife() > 1 then + -- still alive. read x, y and heading + local sp = impStat:getPoint() + theUnit.x = sp.x + theUnit.y = sp.z -- !!! + theUnit.heading = dcsCommon.getUnitHeading(impStat) -- should also work for statics + -- should automatically handle ["livery_id"] + relinkZones[theUnit.name] = cfxZones.zonesLinkedToUnit(impStat) + else + -- dead + table.insert(deadUnits, theUnit.name) + end + -- destroy imp + if impStat and impStat:isExist() then + impStat:destroy() + end end end end - end - - -- destroy impostor info - theZone.myImpostors[groupName] = nil - theZone.impostor = false - - -- now create the group - if theZone.blinkTime <= 0 then - -- immediate spawn - --local newGroup = coalition.addGroup(ctry, cfxMX.catText2ID(cat), rawData) - local newGroup = coalition.addGroup(ctry, cat, rawData) - impostors.relinkZonesForGroup(relinkZones, newGroup) - if theZone.trackWith and groupTracker.addGroupToTrackerNamed then - -- add these groups to the group tracker - if theZone.verbose or impostors.verbose then - trigger.action.outText("+++ipst: attempting to add group <" .. newGroup:getName() .. "> to tracker <" .. theZone.trackWith .. ">", 30) - end - groupTracker.addGroupToTrackerNamed(newGroup, theZone.trackWith) + + -- destroy impostor info + theZone.myImpostors[groupName] = nil + theZone.impostor = false -- is this good? + + -- now create the group + if theZone.blinkTime <= 0 then + -- immediate spawn + --local newGroup = coalition.addGroup(ctry, cfxMX.catText2ID(cat), rawData) + local newGroup = coalition.addGroup(ctry, cat, rawData) + impostors.relinkZonesForGroup(relinkZones, newGroup) + if theZone.trackWith and groupTracker.addGroupToTrackerNamed then + -- add these groups to the group tracker + if theZone.verbose or impostors.verbose then + trigger.action.outText("+++ipst: attempting to add group <" .. newGroup:getName() .. "> to tracker <" .. theZone.trackWith .. ">", 30) + end + groupTracker.addGroupToTrackerNamed(newGroup, theZone.trackWith) + end + else + -- scheduled spawn + theZone.blinkCount = theZone.blinkCount + 1 -- so healthcheck avoids false positives + local args = {} + args.ctry = ctry + args.cat = cat -- cfxMX.catText2ID(cat) + args.rawData = rawData + args.theZone = theZone + args.relinkZones = relinkZones + timer.scheduleFunction(impostors.delayedSpawn, args, timer.getTime() + theZone.blinkTime) end else - -- scheduled spawn - theZone.blinkCount = theZone.blinkCount + 1 -- so healthcheck avoids false positives - local args = {} - args.ctry = ctry - args.cat = cat -- cfxMX.catText2ID(cat) - args.rawData = rawData - args.theZone = theZone - args.relinkZones = relinkZones - timer.scheduleFunction(impostors.delayedSpawn, args, timer.getTime() + theZone.blinkTime) + if theZone.verbose or impostors.verbose then + trigger.action.outText("No impostor group named <" .. groupName .. "> any more, skipped.", 30) + end +-- theZone.myImpostors[groupName] = nil end end - + theZone.groupNames = filtered -- filter out non-existing -- now remove all dead units if theZone.blinkTime <= 0 then for idx, unitName in pairs(deadUnits) do diff --git a/modules/noGap.lua b/modules/noGap.lua index 393002a..52a0cdc 100644 --- a/modules/noGap.lua +++ b/modules/noGap.lua @@ -1,5 +1,5 @@ noGap = {} -noGap.version = "1.0.1" +noGap.version = "1.3.1" noGap.verbose = false noGap.ignoreMe = "-ng" -- ignore altogether @@ -20,11 +20,7 @@ noGap.requiredLibs = { works on unit-level (stop-Gap works on group level) Advantage: multiple-ship player groups look better, less code Disadvantage: incompatibe with SSB/slotBlock - - What it does: - Replace all player units with static aircraft until the first time - that a player slots into that plane. Static is then replaced with live player unit. - + DOES NOT SUPPORT SHIP-BASED AIRCRAFT For multiplayer, NoGapGUI must run on the server (only server) @@ -38,12 +34,18 @@ noGap.requiredLibs = { Version History 1.0.0 - Initial version 1.0.1 - added "from runway" - + 1.3.1 - in line with stopGap 1.3.0 + - allNeutral attribute + - DCS dynamic player spawn compatibility + - shallow water also qualifies as carrier based + - noParking + - kickTheDead + - refreshInterval, reinforced guards --]]-- noGap.standInUnits = {} -- static replacement, if filled; indexed by name noGap.liveUnits = {} -- live in-game units, checked regularly -noGap.allPlayerUnits = {} -- for update check to get server notification +noGap.allPlayerUnits = {} -- for update check to get server notification, excludes dynamically spawned units noGap.noGapZones = {} -- DML only function noGap.staticMXFromUnitMX(theGroup, theUnit) @@ -57,6 +59,12 @@ function noGap.staticMXFromUnitMX(theGroup, theUnit) theStatic.type = theUnit.type theStatic.name = theUnit.name -- same as ME unit theStatic.cty = cfxMX.countryByName[theGroup.name] + theStatic.payload = theUnit.payload -- not supported (yet) by DCS + theStatic.onboard_num = theUnit.onboard_num -- not supported + -- DML only: allNeutral + if noGap.allNeutral then + theStatic.cty = dcsCommon.getACountryForCoalition(0) + end return theStatic end @@ -82,10 +90,20 @@ function noGap.isGroundStart(theGroup) if action == "Turning Point" then return false end if action == "Landing" then return false end if action == "From Runway" then return false end + if noGap.noParking then + local loAct = string.lower(action) + if loAct == "from parking area" or + loAct == "from parking area hot" then + if noGap.verbose then + trigger.action.outText("StopG: Player Group <" .. theGroup.name .. "> NOPARKING: [" .. action .. "] must be skipped.", 30) + end + return false + end + end -- aircraft is on the ground - but is it in water (carrier)? local u1 = theGroup.units[1] local sType = land.getSurfaceType(u1) -- has fields x and y - if sType == 3 then return false end + if sType == 3 or sType == 2 then return false end if noGap.verbose then trigger.action.outText("noG: Player Group <" .. theGroup.name .. "> GROUND BASED: " .. action .. ", land type " .. sType, 30) end @@ -107,7 +125,7 @@ function noGap.ignoreMXUnit(theUnit) -- DML-only return false end -function noGap.createStandInForMXData(group, theUnit) -- group, theUnit are MX data blocks +function noGap.createStandInForMXData(group, theUnit) -- WARNING: group and theUnit are MX data blocks local sgMatch = theUnit.name:sub(-#noGap.ignoreMe) == noGap.ignoreMe or group.name:sub(-#noGap.ignoreMe) == noGap.ignoreMe local spMatch = theUnit.name:sub(-#noGap.spIgnore) == noGap.spIgnore or group.name:sub(-#noGap.spIgnore) == noGap.spIgnore local zoneIgnore = noGap.ignoreMXUnit(theUnit) @@ -139,7 +157,6 @@ function noGap.createStandInForMXData(group, theUnit) -- group, theUnit are MX d end end end - end function noGap.fillGaps() @@ -175,6 +192,7 @@ function noGap.turnOff() StaticObject.destroy(standIn) end noGap.standInUnits = {} + noGap.running = false end function noGap.turnOn() @@ -183,6 +201,23 @@ function noGap.turnOn() end -- populate all empty (non-taken) slots with stand-ins noGap.fillGaps() + noGap.running = true +end + +function noGap.refreshAll() -- restore all statics + if noGap.refreshInterval > 0 then + -- re-schedule invocation + timer.scheduleFunction(noGap.refreshAll, {}, timer.getTime() + noGap.refreshInterval) + if not noGap.enabled then return end + if noGap.running then + noGap.turnOff() -- kill all statics + -- turn back on in half a second + timer.scheduleFunction(noGap.turnOn, {}, timer.getTime() + 0.5) + end + if stopGap.verbose then + noGap.action.outText("+++noG: refreshing all static", 30) + end + end end -- @@ -193,11 +228,18 @@ function noGap:onEvent(event) if not event.id then return end if not event.initiator then return end local theUnit = event.initiator - + if (not theUnit.getPlayerName) or (not theUnit:getPlayerName()) then + return + end -- no player unit. + if cfxMX.isDynamicPlayer(theUnit) then + if noGap.verbose then + trigger.action.outText("+++noG: unit <" .. theUnit:getName() .. "> controlled by <" .. theUnit:getPlayerName() .. "> is dynamically spawned, ignoring.", 30) + end + return + end -- ignore all dynamically spawned aircraft + if event.id == 15 then -- we act on player unit birth - if (not theUnit.getPlayerName) or (not theUnit:getPlayerName()) then - return - end -- no player unit. + local uName = theUnit:getName() if noGap.standInUnits[uName] then @@ -208,11 +250,35 @@ function noGap:onEvent(event) trigger.action.outText("+++noG: removed static for <" ..uName .. ">, player inbound", 30) end end - noGap.liveUnits[uName] = theUnit + noGap.liveUnits[uName] = theUnit -- dynamic never show up here -- reset noGapGUI flag, it has done its job. Unit is live -- we can reset it for next iteration trigger.action.setUserFlag("NG"..uName, 0) end + + if id == 6 then -- eject, ignore for now + end + if (id == 9) or (id == 30) or (id == 5) then -- dead, lost, crash + local pName = theUnit:getPlayerName() + timer.scheduleFunction(noGap.kickplayer, pName, timer.getTime() + 1) + end + +end + +noGap.kicks = {} +function noGap.kickplayer(args) + if not noGap.kickTheDead then return end + local pName = args + for i,slot in pairs(net.get_player_list()) do + local nn = net.get_name(slot) + if nn == pName then + if noGap.kicks[nn] then + if timer.getTime() < noGap.kicks[nn] then return end + end + net.force_player_slot(slot, 0, '') + noGap.kicks[nn] = timer.getTime() + 5 -- avoid too many kicks in 5 seconds + end + end end -- @@ -312,7 +378,7 @@ function noGap.update() end -- --- read stopGapZone (DML only) +-- read noGap Zone (DML only) -- function noGap.createNoGapZone(theZone) local ng = theZone:getBoolFromZoneProperty("noGap", true) @@ -331,8 +397,11 @@ noGap.name = "noGapConfig" -- cfxZones compatibility here function noGap.readConfigZone(theZone) -- currently nothing to do noGap.verbose = theZone.verbose + noGap.ssbEnabled = theZone:getBoolFromZoneProperty("ssb", true) noGap.enabled = theZone:getBoolFromZoneProperty("onStart", true) noGap.timeOut = theZone:getNumberFromZoneProperty("timeOut", 0) -- default to off + noGap.noParking = theZone:getBoolFromZoneProperty("noParking", false) + if theZone:hasProperty("on?") then noGap.turnOnFlag = theZone:getStringFromZoneProperty("on?", "*") noGap.lastTurnOnFlag = trigger.misc.getUserFlag(noGap.turnOnFlag) @@ -350,6 +419,10 @@ function noGap.readConfigZone(theZone) trigger.action.outText("+++noG: turned off", 30) end end + + noGap.refreshInterval = theZone:getNumberFromZoneProperty("refresh", -1) -- default: no refresh + noGap.kickTheDead = theZone:getBoolFromZoneProperty("kickDead", true) + noGap.allNeutral = theZone:getBoolFromZoneProperty("allNeutral", false) end -- @@ -394,6 +467,11 @@ function noGap.start() -- start update in 1 second timer.scheduleFunction(noGap.update, {}, timer.getTime() + 1) + -- start refresh cycle if refresh (>0) + if noGap.refreshInterval > 0 then + timer.scheduleFunction(noGap.refreshAll, {}, timer.getTime() + noGap.refreshInterval) + end + -- say hi! local mp = " (SP - <" .. sgDetect .. ">)" if sgDetect > 0 then mp = " -- MP GUI Detected (" .. sgDetect .. ")!" end diff --git a/modules/radioMenus.lua b/modules/radioMenus.lua index 436edce..457495a 100644 --- a/modules/radioMenus.lua +++ b/modules/radioMenus.lua @@ -1,5 +1,5 @@ radioMenu = {} -radioMenu.version = "4.0.1" +radioMenu.version = "4.0.2" radioMenu.verbose = false radioMenu.ups = 1 radioMenu.requiredLibs = { @@ -19,7 +19,8 @@ radioMenu.lateGroups = {} -- dict by ID detect cyclic references 4.0.0 - added infrastructure for dynamic players (DCS 2.9.6) - added dynamic player support - 4.0.1 - MX no longer optional, so ask for it + 4.0.1 - MX no longer optional, so ask for it + 4.0.2 - ackSnd now also broadcasts to all if no group --]]-- function radioMenu.addRadioMenu(theZone) @@ -770,7 +771,11 @@ function radioMenu.doMenuX(args) radioMenu.radioOutMsg(ack, gid, theZone) end if ackSnd then - trigger.action.outSoundForGroup(gid, ackSnd) + if (not gid) or gid == 0 then + trigger.action.outSound(ackSnd) + else + trigger.action.outSoundForGroup(gid, ackSnd) + end end end diff --git a/modules/raiseFlag.lua b/modules/raiseFlag.lua index 0224ade..38be06e 100644 --- a/modules/raiseFlag.lua +++ b/modules/raiseFlag.lua @@ -1,107 +1,144 @@ raiseFlag = {} -raiseFlag.version = "2.0.0" -raiseFlag.verbose = false +raiseFlag.version = "3.0.0" raiseFlag.requiredLibs = { "dcsCommon", -- always "cfxZones", -- Zones, of course } -raiseFlag.flags = {} +raiseFlag.flags = {} -- my minions --[[-- -Raise A Flag module -- (c) 2022-23 by Christian Franz and cf/x AG + (c) 2022-23 by Christian Franz and cf/x AG Version History - 1.0.0 - initial release - 1.0.1 - synonym "raiseFlag!" - 1.1.0 - DML update - 1.2.0 - Watchflag update - 1.2.1 - support for 'inc', 'dec', 'flip' - 2.0.0 - dmlZones - - full method support - - full DML upgrade - - method attribute (synonym to 'value') + 3.0.0 - switched to polling + - remains# attribute + - support for persistence + - zone-individual verbosity + - code cleanup, removed deprecated attributes --]]-- + function raiseFlag.addRaiseFlag(theZone) - table.insert(raiseFlag.flags, theZone) + raiseFlag.flags[theZone.name] = theZone end - -function raiseFlag.getRaiseFlagByName(aName) - for idx, aZone in pairs(raiseFlag.flags) do - if aName == aZone.name then return aZone end - end - if raiseFlag.verbose then - trigger.action.outText("+++rFlg: no raiseFlag with name <" .. aName ..">", 30) - end - return nil -end - -- -- read attributes -- function raiseFlag.createRaiseFlagWithZone(theZone) - -- get flag from faiseFlag itself - if theZone:hasProperty("raiseFlag") then - theZone.raiseFlag = theZone:getStringFromZoneProperty("raiseFlag", "") -- the flag to raise - else - theZone.raiseFlag = theZone:getStringFromZoneProperty("raiseFlag!", "") -- the flag to raise - end - - -- pre-method DML raiseFlag is now upgraded to method. - -- flagValue now carries the method - if theZone:hasProperty("value") then -- backward compatibility - theZone.flagValue = theZone:getStringFromZoneProperty("value", "inc") -- value to set to. default is command 'inc' - else - theZone.flagValue = theZone:getStringFromZoneProperty("method", "inc") - end + theZone.raiseFlag = theZone:getStringFromZoneProperty("raiseFlag!", "") -- the flag to raise + theZone.flagValue = theZone:getStringFromZoneProperty("method", "inc") theZone.flagValue = theZone.flagValue:lower() theZone.minAfterTime, theZone.maxAfterTime = theZone:getPositiveRangeFromZoneProperty("afterTime", -1) theZone.raiseTriggerMethod = theZone:getStringFromZoneProperty( "triggerMethod", "change") if theZone:hasProperty("raiseTriggerMethod") then theZone.raiseTriggerMethod = theZone:getStringFromZoneProperty("raiseTriggerMethod", "change") end - if theZone:hasProperty("stopFlag?") then theZone.triggerStopFlag = theZone:getStringFromZoneProperty( "stopFlag?", "none") theZone.lastTriggerStopValue = theZone:getFlagValue(theZone.triggerStopFlag) -- save last value end - theZone.scheduleID = nil + if theZone:hasProperty("timeLeft#") then + theZone.timeLeft = theZone:getStringFromZoneProperty("timeLeft#", "none") + end theZone.raiseStopped = false - -- now simply schedule for invocation - local args = {} - args.theZone = theZone if theZone.minAfterTime < 1 then - timer.scheduleFunction(raiseFlag.triggered, args, timer.getTime() + 0.5) + theZone.deadLine = -1 -- will always trigger else local delay = cfxZones.randomDelayFromPositiveRange(theZone.minAfterTime, theZone.maxAfterTime) - timer.scheduleFunction(raiseFlag.triggered, args, timer.getTime() + delay) + theZone.deadLine = timer.getTime() + delay end end -function raiseFlag.triggered(args) - local theZone = args.theZone - if theZone.raiseStopped then return end - -- if we get here, we aren't stopped and do the flag pull +function raiseFlag.triggered(theZone) + if theZone.raiseStopped then return end -- should be filtered local command = theZone.flagValue theZone:pollFlag(theZone.raiseFlag, command) if raiseFlag.verbose or theZone.verbose then - trigger.action.outText("+++rFlg - raising <" .. theZone.raiseFlag .. "> with method '" .. command .. "'" ,30) + trigger.action.outText("+++rFlg - raising <" .. theZone.raiseFlag .. "> with method '" .. command .. "'" .. " for zone <" .. theZone.name .. ">" ,30) end end - -- -- update -- -function raiseFlag.update() - -- call me in a second to poll triggers - timer.scheduleFunction(raiseFlag.update, {}, timer.getTime() + 1) - for idx, aZone in pairs(raiseFlag.flags) do - -- make sure to re-start before reading time limit - if aZone:testZoneFlag(aZone.triggerStopFlag, aZone.raiseTriggerMethod, "lastTriggerStopValue") then - theZone.raiseStopped = true -- we are done, no flag! +function raiseFlag.update(firstUpdate) + local now = timer.getTime() + timer.scheduleFunction(raiseFlag.update, false, now + 1) -- we always beat on .5 offset! + local filtered = {} + for zName, theZone in pairs(raiseFlag.flags) do + -- see if this timer has run out + if theZone.deadLine < now then + if theZone.verbose then + trigger.action.outText("+++rFlg: will raise flag <" .. theZone.name .. ">", 30) + end + raiseFlag.triggered(theZone) + if theZone.timeLeft then theZone:setFlagValue(theZone.timeLeft, 0) end + theZone.raiseStopped = true -- will filter + elseif theZone.timeLeft then + local rem = theZone.deadLine - now + theZone:setFlagValue(theZone.timeLeft, rem) + if theZone.verbose then + trigger.action.outText("+++rFlg: time left on <" .. theZone.name .. ">:" .. math.floor(rem) .. " secs", 30) + end end + + if theZone:testZoneFlag(theZone.triggerStopFlag, theZone.raiseTriggerMethod, "lastTriggerStopValue") then + theZone.raiseStopped = true -- filter + end + + if not theZone.raiseStopped then + filtered[theZone.name] = theZone + end end + raiseFlag.flags = filtered +end +-- +-- load / save (game data) +-- +function raiseFlag.saveData() + local theData = {} + local theFlags = {} + local now = timer.getTime() + for name, theZone in pairs (raiseFlag.flags) do + -- note: we only process existing! + theFlags[name] = theZone.deadLine - now + end + -- save current deadlines + theData.theFlags = theFlags + return theData, raiseFlag.sharedData -- second val currently nil end +function raiseFlag.loadData() + if not persistence then return end + local shared = nil + local theData = persistence.getSavedDataForModule("raiseFlag") + if (not theData) then + if raiseFlag.verbose then + trigger.action.outText("+++rFlg: no save date received, skipping.", 30) + end + return + end + local theFlags = theData.theFlags + -- filter and reset timers + local filtered = {} + local now = timer.getTime() + for name, deadLine in pairs(theFlags) do + local theZone = raiseFlag.flags[name] + if theZone then + theZone.deadLine = now + deadLine + filtered[name] = theZone + if theZone.verbose then + trigger.action.outText("+++rFlg: (persistence) reset zone <" .. name .. "> to <" .. theZone.deadLine .. "> (<" .. deadLine .. ">)", 30) + end + else + trigger.action.outText("+++rFlag: (persistence) filtered <" .. name .. ">, does not exist in miz.", 30) + end + end + for name, theZone in pairs(raiseFlag.flags) do + if not filtered[name] and theZone.verbose then + trigger.action.outText("+++rFlg: (persistence) filtered spent/non-saved <" .. name .. ">.", 30) + end + end + raiseFlag.flags = filtered +end -- -- config & go! -- @@ -117,39 +154,30 @@ function raiseFlag.readConfigZone() end function raiseFlag.start() - -- lib check if not dcsCommon.libCheck then trigger.action.outText("cfx raise flag requires dcsCommon", 30) return false end - if not dcsCommon.libCheck("cfx Raise Flag", raiseFlag.requiredLibs) then - return false - end - - -- read config + if not dcsCommon.libCheck("cfx Raise Flag", raiseFlag.requiredLibs)then return false end raiseFlag.readConfigZone() - - -- process raise Flags Zones - local attrZones = cfxZones.getZonesWithAttributeNamed("raiseFlag") - for k, aZone in pairs(attrZones) do - raiseFlag.createRaiseFlagWithZone(aZone) -- process attributes - raiseFlag.addRaiseFlag(aZone) -- add to list - end - -- try synonym attrZones = cfxZones.getZonesWithAttributeNamed("raiseFlag!") for k, aZone in pairs(attrZones) do raiseFlag.createRaiseFlagWithZone(aZone) -- process attributes raiseFlag.addRaiseFlag(aZone) -- add to list end - - -- start update - raiseFlag.update() - + if persistence then + callbacks = {} + callbacks.persistData = raiseFlag.saveData + persistence.registerModule("raiseFlag", callbacks) + -- now load my data + raiseFlag.loadData() + end + -- start update at 0.5 secs mark + timer.scheduleFunction(raiseFlag.update, true, timer.getTime() + 0.5) trigger.action.outText("cfx raiseFlag v" .. raiseFlag.version .. " started.", 30) return true end --- let's go! if not raiseFlag.start() then trigger.action.outText("cfx Raise Flag aborted: missing libraries", 30) raiseFlag = nil diff --git a/modules/slotty.lua b/modules/slotty.lua index 8d41f5b..f3f08f1 100644 --- a/modules/slotty.lua +++ b/modules/slotty.lua @@ -1,5 +1,5 @@ slotty = {} -slotty.version = "1.1.0" +slotty.version = "1.2.0" --[[-- Single-player slot blocking and slot blocking fallback for multiplayer when SSB is not installed on server. @@ -12,7 +12,7 @@ slotty.version = "1.1.0" Version history 1.0.0 - Initial version 1.1.0 - "noSlotty" global disable flag, anti-mirror SSB flag -1.2.0 - better (delayed) nilling for cfxSSB ocupied clients to +1.2.0 - better (delayed) nilling for cfxSSB occupied clients to - avoid a race condition --]]-- diff --git a/modules/stopGaps.lua b/modules/stopGaps.lua index 95054a7..a28db4e 100644 --- a/modules/stopGaps.lua +++ b/modules/stopGaps.lua @@ -58,7 +58,7 @@ stopGap.requiredLibs = { required 1.3.0 - carriers in shallow waters also no longer handled as viable - noParking option - + 1.3.1 - stronger guards on refresh --]]-- stopGap.standInGroups = {} -- idx by name, if set has a static @@ -239,6 +239,7 @@ function stopGap.refreshAll() -- restore all statics if stopGap.refreshInterval > 0 then -- re-schedule invocation timer.scheduleFunction(stopGap.refreshAll, {}, timer.getTime() + stopGap.refreshInterval) + if not stopGap.enabled then return end -- why do we have enabled and running? if stopGap.running then stopGap.turnOff() -- kill all statics -- turn back on in half a second