diff --git a/Doc/DML Documentation.pdf b/Doc/DML Documentation.pdf index a0e337e..a878581 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 e0034ff..a43ed52 100644 Binary files a/Doc/DML Quick Reference.pdf and b/Doc/DML Quick Reference.pdf differ diff --git a/modules/RNDFlags.lua b/modules/RNDFlags.lua index db50c62..95f5dad 100644 --- a/modules/RNDFlags.lua +++ b/modules/RNDFlags.lua @@ -1,5 +1,5 @@ rndFlags = {} -rndFlags.version = "1.3.1" +rndFlags.version = "1.3.2" rndFlags.verbose = false rndFlags.requiredLibs = { "dcsCommon", -- always @@ -29,8 +29,10 @@ rndFlags.requiredLibs = { - added zonal verbosity - added 'rndDone!' flag - rndMethod defaults to "inc" + 1.3.2 - moved flagArrayFromString to dcsCommon --]] + rndFlags.rndGen = {} function rndFlags.addRNDZone(aZone) @@ -38,6 +40,8 @@ function rndFlags.addRNDZone(aZone) end function rndFlags.flagArrayFromString(inString) + return dcsCommon.flagArrayFromString(inString, rndFlags.verbose) + --[[-- if string.len(inString) < 1 then trigger.action.outText("+++RND: empty flags", 30) return {} @@ -88,6 +92,7 @@ function rndFlags.flagArrayFromString(inString) trigger.action.outText("+++RND: <" .. #flags .. "> flags total", 30) end return flags + --]]-- end -- diff --git a/modules/cfxCargoReceiver.lua b/modules/cfxCargoReceiver.lua index c8279c6..417da1f 100644 --- a/modules/cfxCargoReceiver.lua +++ b/modules/cfxCargoReceiver.lua @@ -1,5 +1,5 @@ cfxCargoReceiver = {} -cfxCargoReceiver.version = "1.2.1" +cfxCargoReceiver.version = "1.2.2" cfxCargoReceiver.ups = 1 -- once a second cfxCargoReceiver.maxDirectionRange = 500 -- in m. distance when cargo manager starts talking to pilots who are carrying that cargo cfxCargoReceiver.requiredLibs = { @@ -17,6 +17,9 @@ cfxCargoReceiver.requiredLibs = { - 1.2.0 method f!, cargoReceived! - 1.2.1 cargoMethod + - 1.2.2 removed deprecated functions + corrected pollFlag bug (not passing zone along) + distance to receiver is given as distance to zone boundary CargoReceiver is a zone enhancement you use to be automatically @@ -39,13 +42,15 @@ function cfxCargoReceiver.processReceiverZone(aZone) -- process attribute and ad -- isCargoReceiver flag and we are good aZone.isCargoReceiver = true -- we can add additional processing here - aZone.autoRemove = cfxZones.getBoolFromZoneProperty(aZone, "autoRemove", false) -- maybe add a removedelay - + aZone.autoRemove = cfxZones.getBoolFromZoneProperty(aZone, "autoRemove", false) -- maybe add a removeDelay + aZone.removeDelay = cfxZones.getNumberFromZoneProperty(aZone, "removeDelay", 1) + if aZone.removeDelay < 1 then aZone.removeDelay = 1 end aZone.silent = cfxZones.getBoolFromZoneProperty(aZone, "silent", false) --trigger.action.outText("+++rcv: recognized receiver zone: " .. aZone.name , 30) -- same integration as object destruct detector for flags + --[[-- if cfxZones.hasProperty(aZone, "setFlag") then aZone.setFlag = cfxZones.getStringFromZoneProperty(aZone, "setFlag", "999") end @@ -70,7 +75,7 @@ function cfxCargoReceiver.processReceiverZone(aZone) -- process attribute and ad if cfxZones.hasProperty(aZone, "f-1") then aZone.decreaseFlag = cfxZones.getStringFromZoneProperty(aZone, "f-1", "999") end - + --]]-- -- new method support aZone.cargoMethod = cfxZones.getStringFromZoneProperty(aZone, "method", "inc") if cfxZones.hasProperty(aZone, "cargoMethod") then @@ -79,9 +84,7 @@ function cfxCargoReceiver.processReceiverZone(aZone) -- process attribute and ad if cfxZones.hasProperty(aZone, "f!") then aZone.outReceiveFlag = cfxZones.getStringFromZoneProperty(aZone, "f!", "*") - end - - if cfxZones.hasProperty(aZone, "cargoReceived!") then + elseif cfxZones.hasProperty(aZone, "cargoReceived!") then aZone.outReceiveFlag = cfxZones.getStringFromZoneProperty(aZone, "cargoReceived!", "*") end @@ -112,7 +115,25 @@ end -- -- cargo event happened. Called by Cargo Manager -- +function cfxCargoReceiver.removeCargo(args) + -- asynch call + if not args then return end + local theObject = args.theObject + local theZone = args.theZone + if not theObject then return end + if not theObject:isExist() then + -- maybe blew up? anyway, we are done + return + end + if args.theZone.verbose or cfxCargoReceiver.verbose then + trigger.action.outText("+++crgR: removed object <" .. theObject.getName() .. "> from cargo zone <" .. theZone.name .. ">", 30) + end + + theObject:destroy() +end + function cfxCargoReceiver.cargoEvent(event, object, name) + -- usually called from cargomanager --trigger.action.outText("Cargo Receiver: event <" .. event .. "> for " .. name, 30) if not event then return end if event == "grounded" then @@ -135,6 +156,7 @@ function cfxCargoReceiver.cargoEvent(event, object, name) cfxCargoReceiver.invokeCallback("deliver", object, name, aZone) -- set flags as indicated + --[[-- if aZone.setFlag then trigger.action.setUserFlag(aZone.setFlag, 1) end @@ -149,16 +171,20 @@ function cfxCargoReceiver.cargoEvent(event, object, name) local val = trigger.misc.getUserFlag(aZone.decreaseFlag) - 1 trigger.action.setUserFlag(aZone.decreaseFlag, val) end - + --]]-- if aZone.outReceiveFlag then - cfxZones.pollFlag(aZone.outReceiveFlag, aZone.cargoMethod) + cfxZones.pollFlag(aZone.outReceiveFlag, aZone.cargoMethod, aZone) end --trigger.action.outText("+++rcv: " .. name .. " delivered in zone " .. aZone.name, 30) --trigger.action.outSound("Quest Snare 3.wav") if aZone.autoRemove then - -- maybe schedule this in a few seconds? - object:destroy() + -- schedule this for in a few seconds? + local args = {} + args.theObject = object + args.theZone = aZone + timer.scheduleFunction(cfxCargoReceiver.removeCargo, args, timer.getTime() + aZone.removeDelay) + --object:destroy() end end end @@ -177,11 +203,16 @@ function cfxCargoReceiver.update() -- new we see if any of these are close to a delivery zone for idx, aCargo in pairs(liftedCargos) do local thePoint = aCargo:getPoint() - local receiver, delta = cfxZones.getClosestZone( + local receiver = cfxZones.getClosestZone( thePoint, cfxCargoReceiver.receiverZones -- must be indexed by name ) -- we now check if we are in 'speaking range' and receiver can talk + -- modify delta by distance to boundary, not + -- center + local delta = dcsCommon.distFlat(thePoint, cfxZones.getPoint(receiver)) + delta = delta - receiver.radius + if (receiver.silent == false) and (delta < cfxCargoReceiver.maxDirectionRange) then -- this cargo can be talked down. @@ -195,7 +226,7 @@ function cfxCargoReceiver.update() local theUnit = info.unit if theUnit:isExist() then local uPoint = theUnit:getPoint() - local currDelta = dcsCommon.dist(thePoint, uPoint) + local currDelta = dcsCommon.distFlat(thePoint, uPoint) if currDelta < minDelta then minDelta = currDelta closestUnit = theUnit @@ -217,7 +248,7 @@ function cfxCargoReceiver.update() receiver.point, thePoint, ownHeading) .. " o'clock" - message = receiver.name .. " is " .. math.floor(delta) .. "m at your " .. oclock + message = receiver.name .. " (r=" .. receiver.radius .. "m) is " .. math.floor(delta) .. "m at your " .. oclock end -- add agl local agl = dcsCommon.getUnitAGL(aCargo) diff --git a/modules/cfxMX.lua b/modules/cfxMX.lua index 7a03d64..94b393a 100644 --- a/modules/cfxMX.lua +++ b/modules/cfxMX.lua @@ -1,5 +1,6 @@ cfxMX = {} -cfxMX.version = "1.1.0" +cfxMX.version = "1.2.0" +cfxMX.verbose = false --[[-- Mission data decoder. Access to ME-built mission structures @@ -12,6 +13,10 @@ cfxMX.version = "1.1.0" - on start up collects a cross reference table of all original group id - add linkUnit for statics + 1.2.0 - added group name reference table + - added group type reference + - added references for allFixed, allHelo, allGround, allSea, allStatic + --]]-- @@ -19,6 +24,11 @@ cfxMX.groupNamesByID = {} cfxMX.groupIDbyName = {} cfxMX.groupDataByName = {} +cfxMX.allFixedByName = {} +cfxMX.allHeloByName = {} +cfxMX.allGroundByName = {} +cfxMX.allSeaByName = {} +cfxMX.allStaticByName ={} function cfxMX.getGroupFromDCSbyName(aName, fetchOriginal) if not fetchOriginal then fetchOriginal = false end @@ -154,7 +164,7 @@ function cfxMX.getStaticFromDCSbyName(aName, fetchOriginal) return nil, "", "", "" end -function cfxMX.createCrossReference() +function cfxMX.createCrossReferences() for coa_name_miz, coa_data in pairs(env.mission.coalition) do -- iterate all coalitions local coa_name = coa_name_miz if string.lower(coa_name_miz) == 'neutrals' then -- remove 's' at neutralS @@ -178,7 +188,7 @@ function cfxMX.createCrossReference() obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or - obj_type_name == "static" + obj_type_name == "static" -- what about "cargo"? then -- (so it's not id or name) 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! @@ -188,6 +198,21 @@ function cfxMX.createCrossReference() cfxMX.groupNamesByID[aID] = aName cfxMX.groupIDbyName[aName] = aID cfxMX.groupDataByName[aName] = group_data + -- now make the type-specific xrefs + if obj_type_name == "helicopter" then + cfxMX.allHeloByName[aName] = group_data + elseif obj_type_name == "ship" then + cfxMX.allSeaByName[aName] = group_data + elseif obj_type_name == "plane" then + cfxMX.allFixedByName[aName] = group_data + elseif obj_type_name == "vehicle" then + cfxMX.allGroundByName[aName] = group_data + elseif obj_type_name == "static" then + cfxMX.allStaticByName[aName] = group_data + else + -- should be impossible, but still + trigger.action.outText("+++MX: <" .. obj_type_name .. "> unknown type for <" .. aName .. ">", 30) + end end end --if has category data end --if plane, helo etc... category @@ -212,8 +237,10 @@ function cfxMX.catText2ID(inText) end function cfxMX.start() - cfxMX.createCrossReference() - trigger.action.outText("cfxMX: "..#cfxMX.groupNamesByID .. " groups processed successfully", 30) + cfxMX.createCrossReferences() + if cfxMX.verbose then + trigger.action.outText("cfxMX: "..#cfxMX.groupNamesByID .. " groups processed successfully", 30) + end end -- start diff --git a/modules/cfxObjectSpawnZones.lua b/modules/cfxObjectSpawnZones.lua index 4d0d303..bf2108b 100644 --- a/modules/cfxObjectSpawnZones.lua +++ b/modules/cfxObjectSpawnZones.lua @@ -1,5 +1,5 @@ cfxObjectSpawnZones = {} -cfxObjectSpawnZones.version = "1.2.1" +cfxObjectSpawnZones.version = "1.3.0" cfxObjectSpawnZones.requiredLibs = { "dcsCommon", -- common is of course needed for everything -- pretty stupid to check for this since we @@ -26,7 +26,10 @@ cfxObjectSpawnZones.verbose = false -- 1.1.5 - spawn?, spawnObjects? synonyms -- 1.2.0 - DML flag upgrade -- 1.2.1 - config zone --- - autoLink bug (zone instead of spaneer accessed) +-- - autoLink bug (zone instead of spawner accessed) +-- 1.3.0 - better synonym handling +-- - useDelicates link to delicate when spawned +-- - spawned single and multi-objects can be made delicates -- respawn currently happens after theSpawns is deleted and cooldown seconds have passed cfxObjectSpawnZones.allSpawners = {} @@ -58,13 +61,9 @@ function cfxObjectSpawnZones.createSpawner(inZone) -- connect with ME if a trigger flag is given if cfxZones.hasProperty(inZone, "f?") then theSpawner.triggerFlag = cfxZones.getStringFromZoneProperty(inZone, "f?", "none") - end - - if cfxZones.hasProperty(inZone, "spawn?") then + elseif cfxZones.hasProperty(inZone, "spawn?") then theSpawner.triggerFlag = cfxZones.getStringFromZoneProperty(inZone, "spawn?", "none") - end - - if cfxZones.hasProperty(inZone, "spawnObjects?") then + elseif cfxZones.hasProperty(inZone, "spawnObjects?") then theSpawner.triggerFlag = cfxZones.getStringFromZoneProperty(inZone, "spawnObjects?", "none") end @@ -116,6 +115,11 @@ function cfxObjectSpawnZones.createSpawner(inZone) theSpawner.requestable = cfxZones.getBoolFromZoneProperty(inZone, "requestable", false) if theSpawner.requestable then theSpawner.paused = true end + -- see if the spawn can be made brittle/delicte + if cfxZones.hasProperty(inZone, "useDelicates") then + theSpawner.delicateName = cfxZones.getStringFromZoneProperty(inZone, "useDelicates", "") + end + -- see if it is linked to a ship to set realtive orig headiong if inZone.linkedUnit then @@ -258,6 +262,16 @@ function cfxObjectSpawnZones.spawnObjectNTimes(aSpawner, theType, n, container) end end + if aSpawner.delicateName and delicates then + -- pass this object to the delicate zone mentioned + local theDeli = delicates.getDelicatesByName(aSpawner.delicateName) + if theDeli then + delicates.addStaticObjectToInventoryForZone(theDeli, theObject) + else + trigger.action.outText("+++oSpwn: spawner <" .. aZone.name .. "> can't find delicates <" .. aSpawner.delicateName .. ">", 30) + end + end + return end @@ -302,6 +316,17 @@ function cfxObjectSpawnZones.spawnObjectNTimes(aSpawner, theType, n, container) cfxCargoManager.addCargo(theObject) end end + + if aSpawner.delicateName and delicates then + -- pass this object to the delicate zone mentioned + local theDeli = delicates.getDelicatesByName(aSpawner.delicateName) + if theDeli then + delicates.addStaticObjectToInventoryForZone(theDeli, theObject) + else + trigger.action.outText("+++oSpwn: spawner <" .. aZone.name .. "> can't find delicates <" .. aSpawner.delicateName .. ">", 30) + end + end + -- update rotation currDegree = currDegree + degreeIncrement end diff --git a/modules/cfxZones.lua b/modules/cfxZones.lua index 63e91ab..e21afaf 100644 --- a/modules/cfxZones.lua +++ b/modules/cfxZones.lua @@ -1,5 +1,5 @@ cfxZones = {} -cfxZones.version = "2.8.3" +cfxZones.version = "2.8.4" -- cf/x zone management module -- reads dcs zones and makes them accessible and mutable @@ -88,6 +88,7 @@ cfxZones.version = "2.8.3" - 2.8.3 - new verifyMethod() - changed extractPropertyFromDCS() to also match attributes with blanks like "the Attr" to "theAttr" - new expandFlagName() +- 2.8.4 - fixed bug in setFlagValue() --]]-- cfxZones.verbose = false @@ -867,11 +868,12 @@ end -- get closest zone returns the zone that is closest to point function cfxZones.getClosestZone(point, theZones) if not theZones then theZones = cfxZones.zones end + local lPoint = {x=point.x, y=0, z=point.z} local currDelta = math.huge local closestZone = nil for zName, zData in pairs(theZones) do local zPoint = cfxZones.getPoint(zData) - local delta = dcsCommon.dist(point, zPoint) + local delta = dcsCommon.dist(lPoint, zPoint) -- emulate flag compare if (delta < currDelta) then currDelta = delta closestZone = zData @@ -1276,7 +1278,7 @@ end function cfxZones.setFlagValue(theFlag, theValue, theZone) local zoneName = "" if not theZone then - trigger.action.outText("+++Zne: no zone on setFlagValue") + trigger.action.outText("+++Zne: no zone on setFlagValue", 30) -- mod me for detector else zoneName = theZone.name -- for flag wildcards end diff --git a/modules/dcsCommon.lua b/modules/dcsCommon.lua index 538ab7e..d82c0ee 100644 --- a/modules/dcsCommon.lua +++ b/modules/dcsCommon.lua @@ -1,5 +1,5 @@ dcsCommon = {} -dcsCommon.version = "2.6.6" +dcsCommon.version = "2.6.8" --[[-- VERSION HISTORY 2.2.6 - compassPositionOfARelativeToB - clockPositionOfARelativeToB @@ -82,7 +82,9 @@ dcsCommon.version = "2.6.6" - new stringRemainsStartingWith() - new stripLF() - new removeBlanks() - + 2.6.7 - new menu2text() + 2.6.8 - new getMissionName() + - new flagArrayFromString() --]]-- -- dcsCommon is a library of common lua functions @@ -1882,6 +1884,21 @@ dcsCommon.version = "2.6.6" return catNum end + function dcsCommon.menu2text(inMenu) + if not inMenu then return "" end + local s = "" + for n, v in pairs(inMenu) do + if type(v) == "string" then + if s == "" then s = "[" .. v .. "]" else + s = s .. " | [" .. type(v) .. "]" end + else + if s == "" then s = "[<" .. type(v) .. ">]" else + s = s .. " | [<" .. type(v) .. ">]" end + end + end + return s + end + -- recursively show the contents of a variable function dcsCommon.dumpVar(key, value, prefix, inrecursion) if not inrecursion then @@ -2361,6 +2378,68 @@ function dcsCommon.latLon2Text(lat, lon) return lat, lon end +-- get mission name. If mission file name without ".miz" +function dcsCommon.getMissionName() + local mn = net.dostring_in("gui", "return DCS.getMissionName()") + return mn +end + +function dcsCommon.flagArrayFromString(inString, verbose) + if not verbose then verbose = false end + + if verbose then + trigger.action.outText("+++flagArray: processing <" .. inString .. ">", 30) + end + + if string.len(inString) < 1 then + trigger.action.outText("+++flagArray: empty flags", 30) + return {} + end + + + local flags = {} + local rawElements = dcsCommon.splitString(inString, ",") + -- go over all elements + for idx, anElement in pairs(rawElements) do + if dcsCommon.stringStartsWithDigit(anElement) and dcsCommon.containsString(anElement, "-") then + -- interpret this as a range + local theRange = dcsCommon.splitString(anElement, "-") + local lowerBound = theRange[1] + lowerBound = tonumber(lowerBound) + local upperBound = theRange[2] + upperBound = tonumber(upperBound) + if lowerBound and upperBound then + -- swap if wrong order + if lowerBound > upperBound then + local temp = upperBound + upperBound = lowerBound + lowerBound = temp + end + -- now add add numbers to flags + for f=lowerBound, upperBound do + table.insert(flags, f) + + end + else + -- bounds illegal + trigger.action.outText("+++flagArray: ignored range <" .. anElement .. "> (range)", 30) + end + else + -- single number + f = dcsCommon.trim(anElement) -- DML flag upgrade: accept strings tonumber(anElement) + if f then + table.insert(flags, f) + + else + trigger.action.outText("+++flagArray: ignored element <" .. anElement .. "> (single)", 30) + end + end + end + if verbose then + trigger.action.outText("+++flagArray: <" .. #flags .. "> flags total", 30) + end + return flags +end -- -- -- INIT diff --git a/modules/delicates.lua b/modules/delicates.lua index a746766..b2dc85f 100644 --- a/modules/delicates.lua +++ b/modules/delicates.lua @@ -1,5 +1,5 @@ delicates = {} -delicates.version = "1.0.0" +delicates.version = "1.1.0" delicates.verbose = false delicates.ups = 1 delicates.requiredLibs = { @@ -12,6 +12,11 @@ delicates.inventory = {} --[[-- Version History 1.0.0 - initial version + 1.1.0 - better synonym handling for f! and out! + - addStaticObjectInventoryForZone + - blowAll? + - safetyMargin - safety margin. defaults to 10% + --]]-- function delicates.adddDelicates(theZone) @@ -68,7 +73,7 @@ function delicates.makeZoneInventory(theZone) for idy, anObject in pairs(collector) do local oName = anObject:getName() if type(oName) == 'number' then oName = tostring(oName) end - local oLife = anObject:getLife() + local oLife = anObject:getLife() - anObject:getLife() * theZone.safetyMargin if theZone.verbose or delicates.verbose then trigger.action.outText("+++deli: cat=".. aCat .. ":<" .. oName .. "> Life=" .. oLife, 30) end @@ -91,25 +96,47 @@ function delicates.makeZoneInventory(theZone) end end +function delicates.addStaticObjectToInventoryForZone(theZone, theStatic) + if not theZone then return end + if not theStatic then return end + + local desc = {} + desc.cat = theStatic:getCategory() + desc.oLife = theStatic:getLife() - theStatic:getLife() * theZone.safetyMargin + if desc.oLife < 0 then desc.oLife = 0 end + desc.theZone = theZone + desc.oName = theStatic:getName() + delicates.inventory[desc.oName] = desc + + if theZone.verbose or delicates.verbose then + trigger.action.outText("+++deli: added static <" .. desc.oName .. "> to <" .. theZone.name .. "> with minimal life = <" .. desc.oLife .. "/" .. theStatic:getLife() .. "> = safety margin of " .. theZone.safetyMargin * 100 .. "%", 30) + end +end + function delicates.createDelicatesWithZone(theZone) theZone.power = cfxZones.getNumberFromZoneProperty(theZone, "power", 10) if cfxZones.hasProperty(theZone, "delicatesHit!") then theZone.delicateHit = cfxZones.getStringFromZoneProperty(theZone, "delicatesHit!", "*") - end - if cfxZones.hasProperty(theZone, "f!") then + elseif cfxZones.hasProperty(theZone, "f!") then theZone.delicateHit = cfxZones.getStringFromZoneProperty(theZone, "f!", "*") - end - if cfxZones.hasProperty(theZone, "out!") then + elseif cfxZones.hasProperty(theZone, "out!") then theZone.delicateHit = cfxZones.getStringFromZoneProperty(theZone, "out!", "*") end + -- safety margin + theZone.safetyMargin = cfxZones.getNumberFromZoneProperty(theZone, "safetyMargin", 0) + -- DML Method theZone.delicateHitMethod = cfxZones.getStringFromZoneProperty(theZone, "method", "inc") if cfxZones.hasProperty(theZone, "delicateMethod") then theZone.delicateHitMethod = cfxZones.getStringFromZoneProperty(theZone, "delicatesMethod", "inc") end + theZone.delicateTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "thriggerMethod", "change") + if cfxZones.hasProperty(theZone, "delicateTriggerMethod") then + theZone.delicateTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "delicatesMethod", "change") + end theZone.delicateRemove = cfxZones.getBoolFromZoneProperty(theZone, "remove", true) @@ -117,6 +144,11 @@ function delicates.createDelicatesWithZone(theZone) -- may want to filter by objects, can be passed in delicates delicates.makeZoneInventory(theZone) + if cfxZones.hasProperty(theZone, "blowAll?") then + theZone.blowAll = cfxZones.getStringFromZoneProperty(theZone, "blowAll?", "*") + theZone.lastBlowAll = cfxZones.getFlagValue(theZone.blowAll, theZone) + end + if delicates.verbose or theZone.verbose then trigger.action.outText("+++deli: new delicates zone <".. theZone.name ..">", 30) end @@ -185,14 +217,42 @@ function delicates:onEvent(theEvent) local oName = theObj:getName() local desc = delicates.inventory[oName] if desc then --- trigger.action.outText("+++deli: REGISTERED HIT -- removing!", 30) - delicates.blowUpObject(desc) - -- remove it from further searches - delicates.inventory[oName] = nil + -- see if damage exceeds maximum + local cLife = theObj:getLife() + if cLife < desc.oLife then + if desc.theZone.verbose or delicates.verbose then + trigger.action.outText("+++deli: BRITTLE TRIGGER: life <" .. cLife .. "> below safety margin <" .. oDesc.oLife .. ">", 30) + end + delicates.blowUpObject(desc) + -- remove it from further searches + delicates.inventory[oName] = nil + else + if desc.theZone.verbose or delicates.verbose then + trigger.action.outText("+++deli: CLOSE CALL, but life <" .. cLife .. "> within safety margin <" .. oDesc.oLife .. ">", 30) + end + end end - --- trigger.action.outText("+++deli: we hit " .. oName, 30) - + +end + +-- +-- blow entire zone +-- +function delicates.blowZone(theZone) + if not theZone then return end + local zName = theZone.name + local newInventory = {} + local delay = 0.7 + for oName, oDesc in pairs (delicates.inventory) do + if oDesc.theZone.name == zName then + delicates.blowUpObject(oDesc, delay) + delay = delay + 0.2 -- stagger explosions + else + newInventory[oName] = oDesc + end + end + + delicates.inventory = newInventory end -- @@ -228,12 +288,12 @@ function delicates.update() if theObj then local cLife = theObj:getLife() if cLife >= oDesc.oLife then - -- transfer to next iter + -- all well, transfer to next iter newInventory[oName] = oDesc else - -- blow stuff up + -- health beneath min. blow stuff up if oDesc.theZone.verbose or delicates.verbose then - trigger.action.outText(oName .. " was hit, will blow up, new health is at " .. oDesc.oLife .. ".", 30) + trigger.action.outText(oName .. " was hit, will blow up, current health is <" .. cLife .. ">, min health was " .. oDesc.oLife .. ".", 30) end delicates.blowUpObject(oDesc) end @@ -245,6 +305,13 @@ function delicates.update() end end delicates.inventory = newInventory + + -- now scan all zones for signals + for idx, theZone in pairs(delicates.theDelicates) do + if theZone.blowAll and cfxZones.testZoneFlag(theZone, theZone.blowAll, theZone.delicateTriggerMethod, "lastBlowAll") then + delicates.blowZone(theZone) + end + end end -- diff --git a/modules/impostors.lua b/modules/impostors.lua index 0a81d90..84b9551 100644 --- a/modules/impostors.lua +++ b/modules/impostors.lua @@ -566,5 +566,5 @@ end To do - reset? flag: will reset all to MX locationS - add a zone's follow ability to impostors by allowing linkedUnit to work with impostors - +- impostor on idle option. when task of group goes to idle, the group turns into impostors --]]-- \ No newline at end of file diff --git a/modules/persistence.lua b/modules/persistence.lua new file mode 100644 index 0000000..9f656eb --- /dev/null +++ b/modules/persistence.lua @@ -0,0 +1,584 @@ +persistence = {} +persistence.version = "1.0.0" +persistence.ups = 1 -- once every 1 seconds +persistence.verbose = false +persistence.active = false +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.missionData = {} -- loaded from file +persistence.requiredLibs = { + "dcsCommon", -- always + "cfxZones", -- Zones, of course +} +--[[-- + Version History + 1.0.0 - initial version + + PROVIDES LOAD/SAVE ABILITY TO MODULES + PROVIDES STANDALONE/HOSTED SERVER COMPATIOBILITY + +--]]-- + +-- in order to work, Host must desanitize lfs and io +-- only works when run as server + +-- +-- flags to save. can be added to by saveFlags attribute +-- +persistence.flagsToSave = {} -- simple table +persistence.callbacks = {} -- cbblocks, dictionary + + +-- +-- modules register here +-- +function persistence.registerModule(name, callbacks) + -- callbacks is a table with the following entries + -- callbacks.persistData - method that returns a table + -- note that name is also what the data is saved under + -- 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) + end +end + +function persistence.registerFlagsToSave(flagNames, theZone) + -- name can be single flag name or anything that + -- a zone definition has to offer, including local + -- flags. + -- flags can be passed like this: "a, 4-19, 99, kills, *lcl" + -- if you pass a local flag, you must pass the zone + -- or "persisTEMP" will be used + + if not theZone then theZone = cfxZones.createSimpleZone("persisTEMP") end + local newFlags = dcsCommon.flagArrayFromString(flagNames, persistence.verbose) + + -- mow process all new flags and add them to the list of flags + -- to save + for idx, flagName in pairs(newFlags) do + if dcsCommon.stringStartsWith(flagName, "*") then + flagName = theZone.name .. flagName + end + table.insert(persistence.flagsToSave, flagName) + end +end +-- +-- registered modules call this to get their data +-- +function persistence.getSavedDataForModule(name) + if not persistence.active then return nil end + if not persistence.hasData then return nil end + if not persistence.missionData then return end + + return persistence.missionData[name] -- simply get the modules data block +end + + +-- +-- Shared Data API +-- +function persistence.getSharedDataFor(name, item) -- not yet finalized +end + +function persistence.putSharedDataFor(data, name, item) -- not yet finalized +end + +-- +-- helper meths +-- + +function persistence.hasFile(path) --check if file exists at path +-- will also return true for a directory, follow up with isDir + + local attr = lfs.attributes(path) + if attr then + return true, attr.mode + end + + if persistence.verbose then + trigger.action.outText("isFile: attributes not found for <" .. path .. ">", 30) + end + + return false, "" +end + +function persistence.isDir(path) -- check if path is a directory + local success, mode = persistence.hasFile(path) + if success then + success = (mode == "directory") + end + return success +end + + +-- +-- Main save meths +-- +function persistence.saveText(theString, fileName, shared, append) + if not persistence.active then return false end + if not fileName then return false end + if not shared then shared = flase end + if not theString then theString = "" end + + local path = persistence.missionDir .. fileName + if shared then + -- we would now change the path + trigger.action.outText("+++persistence: NYI: shared", 30) + return + end + + local theFile = nil + + if append then + theFile = io.open(path, "a") + else + theFile = io.open(path, "w") + end + + if not theFile then + return false + end + + theFile:write(theString) + + theFile:close() + + return true +end + +function persistence.saveTable(theTable, fileName, shared, append) + if not persistence.active then return false end + if not fileName then return false end + if not theTable then return false end + if not shared then shared = false end + + local theString = net.lua2json(theTable) + + if not theString then theString = "" end + + local path = persistence.missionDir .. fileName + if shared then + -- we would now change the path + trigger.action.outText("+++persistence: NYI: shared", 30) + return + end + + local theFile = nil + + if append then + theFile = io.open(path, "a") + else + theFile = io.open(path, "w") + end + + if not theFile then + return false + end + + theFile:write(theString) + + theFile:close() + + return true +end + + +function persistence.loadText(fileName) -- load file as text + if not persistence.active then return nil end + if not fileName then return nil end + + local path = persistence.missionDir .. fileName + local theFile = io.open(path, "r") + if not theFile then return nil end + + local t = theFile:read("*a") + + theFile:close() + + return t +end + +function persistence.loadTable(fileName) -- load file as table + if not persistence.active then return nil end + if not fileName then return nil end + + local t = persistence.loadText(fileName) + + if not t then return nil end + + local tab = net.json2lua(t) + + return tab +end + + + +-- +-- Data Load on Start +-- +function persistence.initFlagsFromData(theFlags) + -- assumes that theFlags is a dictionary containing + -- flag names + local flagLog = "" + local flagCount = 0 + for flagName, value in pairs(theFlags) do + local val = tonumber(value) -- ensure number + if not val then val = 0 end + trigger.action.setUserFlag(flagName, val) + if flagLog ~= "" then + flagLog = flagLog .. ", " .. flagName .. "=" .. val + else + flagLog = flagName .. "=" .. val + end + flagCount = flagCount + 1 + end + if persistence.verbose and flagCount > 0 then + trigger.action.outText("+++persistence: loaded " .. flagCount .. " flags from storage:\n" .. flagLog .. "", 30) + elseif persistence.verbose then + trigger.action.outText("+++persistence: no flags loaded, commencing mission data load", 30) + end + + +end + +function persistence.missionStartDataLoad() + -- check one: see if we have mission data + local theData = persistence.loadTable(persistence.saveFileName) + + if not theData then + if persistence.verbose then + trigger.action.outText("+++persistence: no saved data, fresh start.", 30) + end + return + end -- there was no data to load + + if theData["freshMaker"] then + if persistence.verbose then + trigger.action.outText("+++persistence: detected fresh start.", 30) + end + return + end + + -- when we get here, we got at least some data. check it + if theData["versionID"] or persistence.versionID then + local vid = theData.versionID -- note: either may be nil! + if vid ~= persistence.versionID then + -- we pretend load never happened. + -- simply return + if persistence.verbose then + local curvid = persistence.versionID + if not curvid then curvid = "" end + if not vid then vid = "" end + trigger.action.outText("+++persistence: version mismatch\n(saved = <" .. vid .. "> vs current = <" .. curvid .. ">) - fresh start.", 30) + end + return + end + end + + -- we have valid data, and modules, after signing up + -- can init from by data + persistence.missionData = theData + persistence.hasData = true + + -- init my flags from last save + local theFlags = theData["persistence.flagData"] + if theFlags then + persistence.initFlagsFromData(theFlags) + end + + -- we are done for now. modules check in + -- after persistence and load their own data + -- when they detect that there is data to load + if persistence.verbose then + trigger.action.outText("+++persistence: basic import complete.", 30) + end +end + +-- +-- MAIN DATA WRITE +-- +function persistence.collectFlagData() + local flagData = {} + for idx, flagName in pairs (persistence.flagsToSave) do + local theNum = trigger.misc.getUserFlag(flagName) + flagData[flagName] = theNum + + end + return flagData +end + +function persistence.saveMissionData() + local myData = {} + + -- first, handle versionID and freshMaker + if persistence.freshMaker then + myData["freshMaker"] = true + end + + if persistence.versionID then + myData["versionID"] = persistence.versionID + end + + -- now handle flags + myData["persistence.flagData"] = persistence.collectFlagData() + + -- now handle all other modules + for moduleName, callbacks in pairs(persistence.callbacks) do + local moduleData = callbacks.persistData() + if moduleData then + myData[moduleName] = moduleData + if persistence.verbose then + trigger.action.outText("+++persistence: gathered data from <" .. moduleName .. ">", 30) + end + end + end + + -- now save data to file + persistence.saveTable(myData, persistence.saveFileName) +end + +-- +-- UPDATE +-- +function persistence.doSaveMission() + -- main save entry, also from API + if persistence.verbose then + trigger.action.outText("+++persistence: starting save", 30) + end + + if persistence.active then + persistence.saveMissionData() + else + if persistence.verbose then + trigger.action.outText("+++persistence: not actice. skipping save", 30) + end + return + end + + if persistence.verbose then + trigger.action.outText("+++persistence: mission saved", 30) + end +end + +function persistence.noteCleanRestart() + persistence.freshMaker = true + persistence.doSaveMission() + trigger.action.outText("\n\nYou can re-start the mission for a fresh start.\n\n",30) + +end + +function persistence.update() + -- call me in a second to poll triggers + timer.scheduleFunction(persistence.update, {}, timer.getTime() + 1/persistence.ups) + + -- check my trigger flag + if persistence.saveMission and cfxZones.testZoneFlag(persistence, persistence.saveMission, "change", "lastSaveMission") then + persistence.doSaveMission() + end + + if persistence.cleanRestart and cfxZones.testZoneFlag(persistence, persistence.cleanRestart, "change", "lastCleanRestart") then + persistence.noteCleanRestart() + end + + -- check my timer + if persistence.saveTime and persistence.saveTime < timer.getTime() then + persistence.doSaveMission() + -- start next cycle + persistence.saveTime = persistence.saveInterval * 60 + timer.getTime() + end +end +-- +-- config & start +-- + +function persistence.collectFlagsFromZone(theZone) + local theFlags = cfxZones.getStringFromZoneProperty(theZone, "saveFlags", "*dummy") + persistence.registerFlagsToSave(theFlags, theZone) +end + +function persistence.readConfigZone() + local theZone = cfxZones.getZoneByName("persistenceConfig") + local hasConfig = true + if not theZone then + hasConfig = false + theZone = cfxZones.createSimpleZone("persistenceConfig") + end + + -- serverDir is the path from the server save directory, usually "Missions/". + -- will be added to lfs.writedir(). + persistence.serverDir = cfxZones.getStringFromZoneProperty(theZone, "serverDir", "Missions\\") + + if hasConfig then + if cfxZones.hasProperty(theZone, "saveDir") then + persistence.saveDir = cfxZones.getStringFromZoneProperty(theZone, "saveDir", "") + else + -- local missname = net.dostring_in("gui", "return DCS.getMissionName()") .. " (data)" + persistence.saveDir = dcsCommon.getMissionName() .. " (data)" + end + else + persistence.saveDir = "" -- save dir is to main mission + -- so that when no config is present (standalone debugger) + -- this will not cause a separate save folder + end + + if persistence.saveDir == "" and persistence.verbose then + trigger.action.outText("*** WARNING: persistence is set to write to main mission directory!", 30) + end + + if cfxZones.hasProperty(theZone, "saveFileName") then + persistence.saveFileName = cfxZones.getStringFromZoneProperty(theZone, "saveFileName", dcsCommon.getMissionName() .. " Data.txt") + end + + if cfxZones.hasProperty(theZone, "versionID") then + persistence.versionID = cfxZones.getStringFromZoneProperty(theZone, "versionID", "") -- to check for full restart + end + + persistence.saveInterval = cfxZones.getNumberFromZoneProperty(theZone, "saveInterval", -1) -- default to manual save + if persistence.saveInterval > 0 then + persistence.saveTime = persistence.saveInterval * 60 + timer.getTime() + end + + if cfxZones.hasProperty(theZone, "cleanRestart?") then + persistence.cleanRestart = cfxZones.getStringFromZoneProperty(theZone, "cleanRestart?", "*") + persistence.lastCleanRestart = cfxZones.getFlagValue(persistence.cleanRestart, theZone) + end + + if cfxZones.hasProperty(theZone, "saveMission?") then + persistence.saveMission = cfxZones.getStringFromZoneProperty(theZone, "saveMission?", "*") + persistence.lastSaveMission = cfxZones.getFlagValue(persistence.saveMission, theZone) + end + + persistence.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) + + if persistence.verbose then + trigger.action.outText("+++persistence: read config", 30) + end + +end + +function persistence.start() + -- lib check + if not dcsCommon.libCheck then + trigger.action.outText("persistence requires dcsCommon", 30) + return false + end + if not dcsCommon.libCheck("persistence", persistence.requiredLibs) then + return false + end + + -- read config + persistence.saveFileName = dcsCommon.getMissionName() .. " Data.txt" + persistence.readConfigZone() + + -- let's see it lfs and io are online + persistence.active = false + if not _G["lfs"] then + if persistence.verbose then + trigger.action.outText("+++persistence requires 'lfs'", 30) + return false + end + end + if not _G["io"] then + if persistence.verbose then + trigger.action.outText("+++persistence requires 'io'", 30) + return false + end + end + + local mainDir = lfs.writedir() .. persistence.serverDir + if not dcsCommon.stringEndsWith(mainDir, "\\") then + mainDir = mainDir .. "\\" + end + -- lets see if we can access the server's mission directory and + -- save directory + -- we first try to access server's main mission directory, called "mainDir" which is usually /Missions/> + + if persistence.isDir(mainDir) then + if persistence.verbose then + trigger.action.outText("persistence: main dir is <" .. mainDir .. ">", 30) + end + else + if persistence.verbose then + trigger.action.outText("+++persistence: Main directory <" .. mainDir .. "> not found or not a directory", 30) + end + return false + end + persistence.mainDir = mainDir + + local missionDir = mainDir .. persistence.saveDir + if not dcsCommon.stringEndsWith(missionDir, "\\") then + missionDir = missionDir .. "\\" + end + + -- check if mission dir exists already + local success, mode = persistence.hasFile(missionDir) + if success and mode == "directory" then + -- has been allocated, and is dir + if persistence.verbose then + trigger.action.outText("+++persistence: saving mission data to <" .. missionDir .. ">", 30) + end + elseif success then + if persistence.verbose then + trigger.action.outText("+++persistence: <" .. missionDir .. "> is not a directory", 30) + end + return false + else + -- does not exist, try to allocate it + if persistence.verbose then + trigger.action.outText("+++persistence: will now create <" .. missionDir .. ">", 30) + end + local ok, mkErr = lfs.mkdir(missionDir) + if not ok then + if persistence.verbose then + trigger.action.outText("+++persistence: unable to create <" .. missionDir .. ">: <" .. mkErr .. ">", 30) + end + return false + end + if persistence.verbose then + trigger.action.outText("+++persistence: created <" .. missionDir .. "> successfully, will save mission data here", 30) + end + end + + persistence.missionDir = missionDir + + persistence.active = true -- we can load and save data + persistence.hasData = false -- we do not have save data + + -- from here on we can read and write files in the missionDir + -- read persistence attributes from all zones + local attrZones = cfxZones.getZonesWithAttributeNamed("saveFlags") + for k, aZone in pairs(attrZones) do + persistence.collectFlagsFromZone(aZone) -- process attributes + -- we do not retain the zone, it's job is done + end + + if persistence.verbose then + trigger.action.outText("+++persistence is active", 30) + end + + -- we now see if we can and need load data + persistence.missionStartDataLoad() + + -- and start updating + persistence.update() + + + return persistence.active +end + +-- +-- go! +-- + +if not persistence.start() then + if persistence.verbose then + trigger.action.outText("+++ persistence not available", 30) + end + -- we do NOT remove the methods so we don't crash +end + +-- add zones for saveFlags so authors can easily save flag values diff --git a/modules/radioMenus.lua b/modules/radioMenus.lua index 407b9fa..6154484 100644 --- a/modules/radioMenus.lua +++ b/modules/radioMenus.lua @@ -1,5 +1,5 @@ radioMenu = {} -radioMenu.version = "1.0.1" +radioMenu.version = "1.1.0" radioMenu.verbose = false radioMenu.ups = 1 radioMenu.requiredLibs = { @@ -12,6 +12,9 @@ radioMenu.menus = {} Version History 1.0.0 Initial version 1.0.1 spelling corrections + 1.1.0 removeMenu + addMenu + menuVisible --]]-- function radioMenu.addRadioMenu(theZone) @@ -32,17 +35,60 @@ end -- -- read zone -- +function radioMenu.installMenu(theZone) + if theZone.coalition == 0 then + theZone.rootMenu = missionCommands.addSubMenu(theZone.rootName, nil) + else + theZone.rootMenu = missionCommands.addSubMenuForCoalition(theZone.coalition, theZone.rootName, nil) + end + + local menuA = cfxZones.getStringFromZoneProperty(theZone, "itemA", "") + if theZone.coalition == 0 then + theZone.menuA = missionCommands.addCommand(menuA, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "A"}) + else + theZone.menuA = missionCommands.addCommandForCoalition(theZone.coalition, menuA, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "A"}) + end + + if cfxZones.hasProperty(theZone, "itemB") then + local menuB = cfxZones.getStringFromZoneProperty(theZone, "itemB", "") + if theZone.coalition == 0 then + theZone.menuB = missionCommands.addCommand(menuB, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "B"}) + else + theZone.menuB = missionCommands.addCommandForCoalition(theZone.coalition, menuB, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "B"}) + end + end + + if cfxZones.hasProperty(theZone, "itemC") then + local menuC = cfxZones.getStringFromZoneProperty(theZone, "itemC", "") + if theZone.coalition == 0 then + theZone.menuC = missionCommands.addCommand(menuC, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "C"}) + else + theZone.menuC = missionCommands.addCommandForCoalition(theZone.coalition, menuC, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "C"}) + end + end + + if cfxZones.hasProperty(theZone, "itemD") then + local menuD = cfxZones.getStringFromZoneProperty(theZone, "itemD", "") + if theZone.coalition == 0 then + theZone.menuD = missionCommands.addCommand(menuD, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "D"}) + else + theZone.menuD = missionCommands.addCommandForCoalition(theZone.coalition, menuD, theZone.rootMenu, radioMenu.redirectMenuX, {theZone, "D"}) + end + end +end + function radioMenu.createRadioMenuWithZone(theZone) - local rootName = cfxZones.getStringFromZoneProperty(theZone, "radioMenu", "") + theZone.rootName = cfxZones.getStringFromZoneProperty(theZone, "radioMenu", "") theZone.coalition = cfxZones.getCoalitionFromZoneProperty(theZone, "coalition", 0) - if theZone.coalition == 0 then - theZone.rootMenu = missionCommands.addSubMenu(rootName, nil) - else - theZone.rootMenu = missionCommands.addSubMenuForCoalition(theZone.coalition, rootName, nil) - end + theZone.menuVisible = cfxZones.getBoolFromZoneProperty(theZone, "menuVisible", true) + -- install menu if not hidden + if theZone.menuVisible then + radioMenu.installMenu(theZone) + end + --[[-- -- now do the two options local menuA = cfxZones.getStringFromZoneProperty(theZone, "itemA", "") if theZone.coalition == 0 then @@ -78,6 +124,7 @@ function radioMenu.createRadioMenuWithZone(theZone) end end + --]]-- -- get the triggers & methods here theZone.radioMethod = cfxZones.getStringFromZoneProperty(theZone, "method", "inc") @@ -85,6 +132,8 @@ function radioMenu.createRadioMenuWithZone(theZone) theZone.radioMethod = cfxZones.getStringFromZoneProperty(theZone, "radioMethod", "inc") end + theZone.radioTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "radioTriggerMethod", "change") + theZone.itemAChosen = cfxZones.getStringFromZoneProperty(theZone, "A!", "*") theZone.cooldownA = cfxZones.getNumberFromZoneProperty(theZone, "cooldownA", 0) theZone.mcdA = 0 @@ -105,6 +154,16 @@ function radioMenu.createRadioMenuWithZone(theZone) theZone.mcdD = 0 theZone.busyD = cfxZones.getStringFromZoneProperty(theZone, "busyD", "Please stand by ( seconds)") + if cfxZones.hasProperty(theZone, "removeMenu?") then + theZone.removeMenu = cfxZones.getStringFromZoneProperty(theZone, "removeMenu?", "*") + theZone.lastRemoveMenu = cfxZones.getFlagValue(theZone.removeMenu, theZone) + end + + if cfxZones.hasProperty(theZone, "addMenu?") then + theZone.addMenu = cfxZones.getStringFromZoneProperty(theZone, "addMenu?", "*") + theZone.lastAddMenu = cfxZones.getFlagValue(theZone.addMenu, theZone) + end + if radioMenu.verbose or theZone.verbose then trigger.action.outText("+++radioMenu: new radioMenu zone <".. theZone.name ..">", 30) end @@ -189,13 +248,44 @@ end -- -- Update -- required when we can enable/disable a zone's menu -- ---[[-- + function radioMenu.update() -- call me in a second to poll triggers timer.scheduleFunction(radioMenu.update, {}, timer.getTime() + 1/radioMenu.ups) + + -- iterate all menus + for idx, theZone in pairs(radioMenu.menus) do + if theZone.removeMenu + and cfxZones.testZoneFlag(theZone, theZone.removeMenu, theZone.radioTriggerMethod, "lastRemoveMenu") + and theZone.menuVisible + then + if theZone.verbose or radioMenu.verbose then + trigger.action.outText("+++menu: removing <" .. dcsCommon.menu2text(theZone.rootMenu) .. "> for <" .. theZone.name .. ">", 30) + end + + if theZone.coalition == 0 then + missionCommands.removeItem(theZone.rootMenu) + else + missionCommands.removeItemForCoalition(theZone.coalition, theZone.rootMenu) + end + + theZone.menuVisible = false + end + if theZone.addMenu + and cfxZones.testZoneFlag(theZone, theZone.addMenu, theZone.radioTriggerMethod, "lastAddMenu") + and (not theZone.menuVisible) + then + if theZone.verbose or radioMenu.verbose then + trigger.action.outText("+++menu: adding menu from <" .. theZone.name .. ">", 30) + end + + radioMenu.installMenu(theZone) -- auto-handles coalition + theZone.menuVisible = true + end + end end ---]]-- + -- -- Config & Start @@ -238,7 +328,7 @@ function radioMenu.start() end -- start update - --radioMenu.update() + radioMenu.update() trigger.action.outText("cfx radioMenu v" .. radioMenu.version .. " started.", 30) return true @@ -251,7 +341,5 @@ if not radioMenu.start() then end --[[-- - to do: turn on/off via flags callbacks for the menus - one-shot items --]]-- \ No newline at end of file diff --git a/modules/theDebugger.lua b/modules/theDebugger.lua index 7e5d497..7a80b0a 100644 --- a/modules/theDebugger.lua +++ b/modules/theDebugger.lua @@ -1,13 +1,15 @@ -- theDebugger debugger = {} -debugger.version = "1.0.1" +debugger.version = "1.1.1" debugDemon = {} -debugDemon.version = "1.0.0" +debugDemon.version = "1.1.1" debugger.verbose = false debugger.ups = 4 -- every 0.25 second debugger.name = "DML Debugger" -- for compliance with cfxZones +debugger.log = "" + --[[-- Version History 1.0.0 - Initial version @@ -15,14 +17,67 @@ debugger.name = "DML Debugger" -- for compliance with cfxZones - changed 'on' to 'active' in config zone - merged debugger and debugDemon - QoL check for 'debug' attribute (no '?') - 1.0.2 - contested! flag + 1.1.0 - logging + - trigger.action --> debugger for outText + - persistence of logs + - save + 1.1.1 - warning when trying to set a flag to a non-int + + --]]-- debugger.requiredLibs = { "dcsCommon", -- always "cfxZones", -- Zones, of course } +-- note: saving logs requires persistence module +-- will auto-abort saving if not present + + debugger.debugZones = {} +debugger.debugUnits = {} +debugger.debugGroups = {} +debugger.debugObjects = {} + +-- +-- Logging & saving +-- + +function debugger.outText(message, seconds, cls) + if not message then message = "" end + if not seconds then seconds = 20 end + if not cls then cls = false end + + -- append message to log, and add a lf + if not debugger.log then debugger.log = "" end + debugger.log = debugger.log .. message .. "\n" + + -- now hand up to trigger + trigger.action.outText(message, seconds, cls) +end + +function debugger.saveLog(name) + if not _G["persistence"] then + debugger.outText("+++debug: persistence module required to save log") + return + end + + if not persistence.active then + debugger.outText("+++debug: persistence module can't write. ensur you desanitize lfs and io") + return + end + + if persistence.saveText(debugger.log, name) then + debugger.outText("+++debug: log saved to <" .. persistence.missionDir .. name .. ">") + else + debugger.outText("+++debug: unable to save log to <" .. persistence.missionDir .. name .. ">") + end +end + + +-- +-- tracking flags +-- function debugger.addDebugger(theZone) table.insert(debugger.debugZones, theZone) @@ -33,7 +88,7 @@ function debugger.getDebuggerByName(aName) if aName == aZone.name then return aZone end end if debugger.verbose then - trigger.action.outText("+++debug: no debug zone with name <" .. aName ..">", 30) + debugger.outText("+++debug: no debug zone with name <" .. aName ..">", 30) end return nil @@ -65,7 +120,7 @@ function debugger.createDebuggerWithZone(theZone) -- say who we are and what we are monitoring if debugger.verbose or theZone.verbose then - trigger.action.outText("---debug: adding zone <".. theZone.name .."> to look for in flag(s):", 30) + debugger.outText("---debug: adding zone <".. theZone.name .."> to look for in flag(s):", 30) end -- read main debug array @@ -77,7 +132,7 @@ function debugger.createDebuggerWithZone(theZone) for idx, aFlag in pairs(flagArray) do local fVal = cfxZones.getFlagValue(aFlag, theZone) if debugger.verbose or theZone.verbose then - trigger.action.outText(" monitoring flag <" .. aFlag .. ">, inital value is <" .. fVal .. ">", 30) + debugger.outText(" monitoring flag <" .. aFlag .. ">, inital value is <" .. fVal .. ">", 30) end valueArray[aFlag] = fVal end @@ -226,7 +281,7 @@ function debugger.debugZone(theZone) -- generate the ouput message local msg = theZone.debugMsg msg = debugger.processDebugMsg(msg, theZone, aFlag, oldVal, newValue) - trigger.action.outText(msg, 30) + debugger.outText(msg, 30) end end @@ -239,7 +294,7 @@ function debugger.resetObserver(theZone) for idf, aFlag in pairs(theZone.flagArray) do local fVal = cfxZones.getFlagValue(aFlag, theZone) if debugger.verbose or theZone.verbose then - trigger.action.outText("---debug: resetting flag <" .. aFlag .. ">, to <" .. fVal .. "> for zone <" .. theZone.name .. ">", 30) + debugger.outText("---debug: resetting flag <" .. aFlag .. ">, to <" .. fVal .. "> for zone <" .. theZone.name .. ">", 30) end theZone.valueArray[aFlag] = fVal end @@ -256,39 +311,39 @@ function debugger.showObserverState(theZone) for idf, aFlag in pairs(theZone.flagArray) do local fVal = cfxZones.getFlagValue(aFlag, theZone) if debugger.verbose or theZone.verbose then - trigger.action.outText(" state of flag <" .. aFlag .. ">: <" .. theZone.valueArray[aFlag] .. ">", 30) + debugger.outText(" state of flag <" .. aFlag .. ">: <" .. theZone.valueArray[aFlag] .. ">", 30) end theZone.valueArray[aFlag] = fVal end end function debugger.showState() - trigger.action.outText("---debug: CURRENT STATE <" .. dcsCommon.nowString() .. "> --- ", 30) + debugger.outText("---debug: CURRENT STATE <" .. dcsCommon.nowString() .. "> --- ", 30) for idx, theZone in pairs(debugger.debugZones) do -- show this zone's state if #theZone.flagArray > 0 then - trigger.action.outText(" state of observer <" .. theZone.name .. "> looking for :", 30) + debugger.outText(" state of observer <" .. theZone.name .. "> looking for :", 30) debugger.showObserverState(theZone) else if theZone.verbose or debugger.verbose then - trigger.action.outText(" (empty observer <" .. theZone.name .. ">)", 30) + debugger.outText(" (empty observer <" .. theZone.name .. ">)", 30) end end end - trigger.action.outText("---debug: end of state --- ", 30) + debugger.outText("---debug: end of state --- ", 30) end function debugger.doActivate() debugger.active = true if debugger.verbose or true then - trigger.action.outText("+++ DM Debugger is now active", 30) + debugger.outText("+++ DM Debugger is now active", 30) end end function debugger.doDeactivate() debugger.active = false if debugger.verbose or true then - trigger.action.outText("+++ debugger deactivated", 30) + debugger.outText("+++ debugger deactivated", 30) end end @@ -339,7 +394,7 @@ function debugger.readConfigZone() local theZone = cfxZones.getZoneByName("debuggerConfig") if not theZone then if debugger.verbose then - trigger.action.outText("+++debug: NO config zone!", 30) + debugger.outText("+++debug: NO config zone!", 30) end theZone = cfxZones.createSimpleZone("debuggerConfig") end @@ -371,7 +426,7 @@ function debugger.readConfigZone() debugger.ups = cfxZones.getNumberFromZoneProperty(theZone, "ups", 4) if debugger.verbose then - trigger.action.outText("+++debug: read config", 30) + debugger.outText("+++debug: read config", 30) end end @@ -398,22 +453,22 @@ function debugger.start() local attrZones = cfxZones.getZonesWithAttributeNamed("debug") for k, aZone in pairs(attrZones) do - trigger.action.outText("***Warning: Zone <" .. aZone.name .. "> has a 'debug' flag. Are you perhaps missing a '?'", 30) + debugger.outText("***Warning: Zone <" .. aZone.name .. "> has a 'debug' flag. Are you perhaps missing a '?'", 30) end -- say if we are active if debugger.verbose then if debugger.active then - trigger.action.outText("+++debugger loaded and active", 30) + debugger.outText("+++debugger loaded and active", 30) else - trigger.action.outText("+++ debugger: standing by for activation", 30) + debugger.outText("+++ debugger: standing by for activation", 30) end end -- start update debugger.update() - trigger.action.outText("cfx debugger v" .. debugger.version .. " started.", 30) + debugger.outText("cfx debugger v" .. debugger.version .. " started.", 30) return true end @@ -441,6 +496,7 @@ debugDemon.verbose = false --[[-- Version History 1.0.0 - initial version + 1.1.0 - save command, requires persistence --]]-- @@ -559,7 +615,7 @@ function debugDemon.executeCommand(theCommands, event) local success = theInvoker(arguments, event) return success else - trigger.action.outText("***error: unknown command <".. cmd .. ">", 30) + debugger.outText("***error: unknown command <".. cmd .. ">", 30) return false end @@ -592,7 +648,7 @@ end -- COMMANDS -- function debugDemon.processHelpCommand(args, event) -trigger.action.outText("*** debugger: commands are:" .. +debugger.outText("*** debugger: commands are:" .. "\n " .. debugDemon.markOfDemon .. "show -- show current values for flag or observer" .. "\n " .. debugDemon.markOfDemon .. "set -- set flag to value " .. "\n " .. debugDemon.markOfDemon .. "inc -- increase flag by 1, changing it" .. @@ -614,6 +670,8 @@ trigger.action.outText("*** debugger: commands are:" .. "\n\n " .. debugDemon.markOfDemon .. "start -- starts debugger" .. "\n " .. debugDemon.markOfDemon .. "stop -- stop debugger" .. + "\n\n " .. debugDemon.markOfDemon .. "save [] -- saves debugger log to storage" .. + "\n\n " .. debugDemon.markOfDemon .. "? or -help -- this text", 30) return true end @@ -622,14 +680,14 @@ function debugDemon.processNewCommand(args, event) -- syntax new [[for] ] local observerName = args[1] if not observerName then - trigger.action.outText("*** new: missing observer name.", 30) + debugger.outText("*** new: missing observer name.", 30) return false -- allows correction end -- see if this observer already existst local theObserver = debugger.getDebuggerByName(observerName) if theObserver then - trigger.action.outText("*** new: observer <" .. observerName .. "> already exists.", 30) + debugger.outText("*** new: observer <" .. observerName .. "> already exists.", 30) return false -- allows correction end @@ -638,7 +696,7 @@ function debugDemon.processNewCommand(args, event) local remainderName = event.remainder local rObserver = debugger.getDebuggerByName(remainderName) if rObserver then - trigger.action.outText("*** new: observer <" .. remainderName .. "> already exists.", 30) + debugger.outText("*** new: observer <" .. remainderName .. "> already exists.", 30) return false -- allows correction end @@ -655,14 +713,14 @@ function debugDemon.processNewCommand(args, event) if condition == "for" then condition = args[3] end if condition then if not cfxZones.verifyMethod(condition, theZone) then - trigger.action.outText("*** new: illegal trigger condition <" .. condition .. "> for observer <" .. observerName .. ">", 30) + debugger.outText("*** new: illegal trigger condition <" .. condition .. "> for observer <" .. observerName .. ">", 30) return false end theZone.debugInputMethod = condition end debugger.addDebugger(theZone) - trigger.action.outText("*** [" .. dcsCommon.nowString() .. "] debugger: new observer <" .. observerName .. "> for <" .. theZone.debugInputMethod .. ">", 30) + debugger.outText("*** [" .. dcsCommon.nowString() .. "] debugger: new observer <" .. observerName .. "> for <" .. theZone.debugInputMethod .. ">", 30) return true end @@ -670,14 +728,14 @@ function debugDemon.processUpdateCommand(args, event) -- syntax update [[to] ] local observerName = args[1] if not observerName then - trigger.action.outText("*** update: missing observer name.", 30) + debugger.outText("*** update: missing observer name.", 30) return false -- allows correction end -- see if this observer already existst local theZone = debugger.getDebuggerByName(observerName) if not theZone then - trigger.action.outText("*** update: observer <" .. observerName .. "> does not exist exists.", 30) + debugger.outText("*** update: observer <" .. observerName .. "> does not exist exists.", 30) return false -- allows correction end @@ -685,13 +743,13 @@ function debugDemon.processUpdateCommand(args, event) if condition == "to" then condition = args[3] end if condition then if not cfxZones.verifyMethod(condition, theZone) then - trigger.action.outText("*** update: illegal trigger condition <" .. condition .. "> for observer <" .. observerName .. ">", 30) + debugger.outText("*** update: illegal trigger condition <" .. condition .. "> for observer <" .. observerName .. ">", 30) return false end theZone.debugInputMethod = condition end - trigger.action.outText("*** [" .. dcsCommon.nowString() .. "] debugger: updated observer <" .. observerName .. "> to <" .. theZone.debugInputMethod .. ">", 30) + debugger.outText("*** [" .. dcsCommon.nowString() .. "] debugger: updated observer <" .. observerName .. "> to <" .. theZone.debugInputMethod .. ">", 30) return true end @@ -699,21 +757,21 @@ function debugDemon.processDropCommand(args, event) -- syntax drop local observerName = event.remainder -- remainder if not observerName then - trigger.action.outText("*** drop: missing observer name.", 30) + debugger.outText("*** drop: missing observer name.", 30) return false -- allows correction end -- see if this observer already existst local theZone = debugger.getDebuggerByName(observerName) if not theZone then - trigger.action.outText("*** drop: observer <" .. observerName .. "> does not exist exists.", 30) + debugger.outText("*** drop: observer <" .. observerName .. "> does not exist exists.", 30) return false -- allows correction end -- now simply and irrevocable remove the observer, unless it's home, -- in which case it's simply reset if theZone == debugDemon.observer then - trigger.action.outText("*** drop: <" .. observerName .. "> is MY PRECIOUS and WILL NOT be dropped.", 30) + debugger.outText("*** drop: <" .. observerName .. "> is MY PRECIOUS and WILL NOT be dropped.", 30) -- can't really happen since it contains blanks, but -- we've seen stranger things return false -- allows correction @@ -721,7 +779,7 @@ function debugDemon.processDropCommand(args, event) debugger.removeDebugger(theZone) - trigger.action.outText("*** [" .. dcsCommon.nowString() .. "] debugger: dropped observer <" .. observerName .. ">", 30) + debugger.outText("*** [" .. dcsCommon.nowString() .. "] debugger: dropped observer <" .. observerName .. ">", 30) return true end -- observe command: add a new flag to observe @@ -730,7 +788,7 @@ function debugDemon.processObserveCommand(args, event) -- args[1] is the name of the flag local flagName = args[1] if not flagName then - trigger.action.outText("*** observe: missing flag name.", 30) + debugger.outText("*** observe: missing flag name.", 30) return false -- allows correction end @@ -738,7 +796,7 @@ function debugDemon.processObserveCommand(args, event) if args[2] == "with" then local aName = args[3] if not aName then - trigger.action.outText("*** observe: missing after 'with'.", 30) + debugger.outText("*** observe: missing after 'with'.", 30) return false -- allows correction end aName = dcsCommon.stringRemainsStartingWith(event.remainder, aName) @@ -746,12 +804,12 @@ function debugDemon.processObserveCommand(args, event) if not withTracker then -- withTracker = debugDemon.createObserver(aName) -- debugger.addDebugger(withTracker) - trigger.action.outText("*** observe: no observer <" .. aName .. "> exists", 30) + debugger.outText("*** observe: no observer <" .. aName .. "> exists", 30) return false -- allows correction end else -- not with as arg 2 if #args > 1 then - trigger.action.outText("*** observe: unknown command after flag name '" .. flagName .. "'.", 30) + debugger.outText("*** observe: unknown command after flag name '" .. flagName .. "'.", 30) return false -- allows correction end -- use own observer @@ -759,13 +817,13 @@ function debugDemon.processObserveCommand(args, event) end if debugger.isObservingWithObserver(flagName, withTracker) then - trigger.action.outText("*** observe: already observing " .. flagName .. " with <" .. withTracker.name .. ">" , 30) + debugger.outText("*** observe: already observing " .. flagName .. " with <" .. withTracker.name .. ">" , 30) return true end -- we add flag to tracker and init value debugger.addFlagToObserver(flagName, withTracker) - trigger.action.outText("*** [" .. dcsCommon.nowString() .. "] debugger: now observing <" .. flagName .. "> for value " .. withTracker.debugInputMethod .. " with <" .. withTracker.name .. ">.", 30) + debugger.outText("*** [" .. dcsCommon.nowString() .. "] debugger: now observing <" .. flagName .. "> for value " .. withTracker.debugInputMethod .. " with <" .. withTracker.name .. ">.", 30) return true end @@ -774,7 +832,7 @@ function debugDemon.processShowCommand(args, event) -- observer has precendce over flag local theName = args[1] if not theName then - trigger.action.outText("*** show: missing observer/flag name.", 30) + debugger.outText("*** show: missing observer/flag name.", 30) return false -- allows correction end @@ -785,12 +843,12 @@ function debugDemon.processShowCommand(args, event) if not theObserver then -- we directly use trigger.misc local fVal = trigger.misc.getUserFlag(theName) - trigger.action.outText("[" .. dcsCommon.nowString() .. "] flag <" .. theName .. "> : value <".. fVal .. ">", 30) + debugger.outText("[" .. dcsCommon.nowString() .. "] flag <" .. theName .. "> : value <".. fVal .. ">", 30) return true end -- if we get here, we want to show an entire observer - trigger.action.outText("*** [" .. dcsCommon.nowString() .. "] flags observed by <" .. theName .. "> looking for :", 30) + debugger.outText("*** [" .. dcsCommon.nowString() .. "] flags observed by <" .. theName .. "> looking for :", 30) local flags = theObserver.flagArray local values = theObserver.valueArray for idx, flagName in pairs(flags) do @@ -804,7 +862,7 @@ function debugDemon.processShowCommand(args, event) theMark = " ! " trailer = ", HIT!" end - trigger.action.outText(theMark .. "f:<" .. flagName .. "> = <".. fVal .. "> [current, state = <" .. values[flagName] .. ">" .. trailer .. "]", 30) + debugger.outText(theMark .. "f:<" .. flagName .. "> = <".. fVal .. "> [current, state = <" .. values[flagName] .. ">" .. trailer .. "]", 30) end return true @@ -836,7 +894,7 @@ function debugDemon.processSnapCommand(args, event) theName = dcsCommon.stringRemainsStartingWith(event.remainder, theName) theObserver = debugger.getDebuggerByName(theName) if not theObserver then - trigger.action.outText("*** snap: unknown observer name <" .. theName .. ">.", 30) + debugger.outText("*** snap: unknown observer name <" .. theName .. ">.", 30) return false -- allows correction end end @@ -861,26 +919,26 @@ function debugDemon.processSnapCommand(args, event) local sz = dcsCommon.getSizeOfTable(snapshot) debugDemon.snapshot = snapshot - trigger.action.outText("*** [" .. dcsCommon.nowString() .. "] debug: new snapshot created, " .. sz .. " flags.", 30) + debugger.outText("*** [" .. dcsCommon.nowString() .. "] debug: new snapshot created, " .. sz .. " flags.", 30) return true end function debugDemon.processCompareCommand(args, event) - trigger.action.outText("*** [" .. dcsCommon.nowString() .. "] debug: comparing snapshot with current flag values", 30) + debugger.outText("*** [" .. dcsCommon.nowString() .. "] debug: comparing snapshot with current flag values", 30) for flagName, val in pairs (debugDemon.snapshot) do local cVal = trigger.misc.getUserFlag(flagName) local mark = ' ' if cVal ~= val then mark = ' ! ' end - trigger.action.outText(mark .. "<" .. flagName .. "> snap = <" .. val .. ">, now = <" .. cVal .. "> " .. mark, 30) + debugger.outText(mark .. "<" .. flagName .. "> snap = <" .. val .. ">, now = <" .. cVal .. "> " .. mark, 30) end - trigger.action.outText("*** END", 30) + debugger.outText("*** END", 30) return true end function debugDemon.processNoteCommand(args, event) local n = event.remainder - trigger.action.outText("*** [" .. dcsCommon.nowString() .. "]: " .. n, 30) + debugger.outText("*** [" .. dcsCommon.nowString() .. "]: " .. n, 30) return true end @@ -888,7 +946,7 @@ function debugDemon.processSetCommand(args, event) -- syntax set local theName = args[1] if not theName then - trigger.action.outText("*** set: missing flag name.", 30) + debugger.outText("*** set: missing flag name.", 30) return false -- allows correction end @@ -900,13 +958,21 @@ function debugDemon.processSetCommand(args, event) end if not theVal or not (tonumber(theVal)) then - trigger.action.outText("*** set: missing or illegal value for flag <" .. theName .. ">.", 30) + debugger.outText("*** set: missing or illegal value for flag <" .. theName .. ">.", 30) return false -- allows correction end - -- we set directly, no cfxZones procing - trigger.action.outText("*** [" .. dcsCommon.nowString() .. "] debug: set flag <" .. theName .. "> to <" .. theVal .. ">", 30) + theVal = tonumber(theVal) trigger.action.setUserFlag(theName, theVal) + -- we set directly, no cfxZones proccing + local note ="" + -- flags are ints only? + if theVal ~= math.floor(theVal) then + note = " [int! " .. math.floor(theVal) .. "]" + end + + debugger.outText("*** [" .. dcsCommon.nowString() .. "] debug: set flag <" .. theName .. "> to <" .. theVal .. ">" .. note, 30) + return true end @@ -914,7 +980,7 @@ function debugDemon.processIncCommand(args, event) -- syntax inc local theName = args[1] if not theName then - trigger.action.outText("*** inc: missing flag name.", 30) + debugger.outText("*** inc: missing flag name.", 30) return false -- allows correction end @@ -922,7 +988,7 @@ function debugDemon.processIncCommand(args, event) local nVal = cVal + 1 -- we set directly, no cfxZones procing - trigger.action.outText("*** [" .. dcsCommon.nowString() .. "] debug: inc flag <" .. theName .. "> from <" .. cVal .. "> to <" .. nVal .. ">", 30) + debugger.outText("*** [" .. dcsCommon.nowString() .. "] debug: inc flag <" .. theName .. "> from <" .. cVal .. "> to <" .. nVal .. ">", 30) trigger.action.setUserFlag(theName, nVal) return true end @@ -931,7 +997,7 @@ function debugDemon.processFlipCommand(args, event) -- syntax flip local theName = args[1] if not theName then - trigger.action.outText("*** flip: missing flag name.", 30) + debugger.outText("*** flip: missing flag name.", 30) return false -- allows correction end @@ -939,7 +1005,7 @@ function debugDemon.processFlipCommand(args, event) if cVal == 0 then nVal = 1 else nVal = 0 end -- we set directly, no cfxZones procing - trigger.action.outText("*** [" .. dcsCommon.nowString() .. "] debug: flipped flag <" .. theName .. "> from <" .. cVal .. "> to <" .. nVal .. ">", 30) + debugger.outText("*** [" .. dcsCommon.nowString() .. "] debug: flipped flag <" .. theName .. "> from <" .. cVal .. "> to <" .. nVal .. ">", 30) trigger.action.setUserFlag(theName, nVal) return true end @@ -952,9 +1018,9 @@ function debugDemon.processListCommand(args, event) prefix = event.remainder -- dcsCommon.stringRemainsStartingWith(event.text, prefix) end if prefix then - trigger.action.outText("*** [" .. dcsCommon.nowString() .. "] listing observers whose name contains <" .. prefix .. ">:", 30) + debugger.outText("*** [" .. dcsCommon.nowString() .. "] listing observers whose name contains <" .. prefix .. ">:", 30) else - trigger.action.outText("*** [" .. dcsCommon.nowString() .. "] listing all observers:", 30) + debugger.outText("*** [" .. dcsCommon.nowString() .. "] listing all observers:", 30) end local allObservers = debugger.debugZones @@ -966,7 +1032,7 @@ function debugDemon.processListCommand(args, event) end if doList then - trigger.action.outText(" <" .. theName .. "> for (" .. #theZone.flagArray .. " flags)", 30) + debugger.outText(" <" .. theName .. "> for (" .. #theZone.flagArray .. " flags)", 30) end end return true @@ -976,20 +1042,20 @@ function debugDemon.processWhoCommand(args, event) -- syntax: who local flagName = event.remainder -- args[1] if not flagName or flagName:len()<1 then - trigger.action.outText("*** who: missing flag name.", 30) + debugger.outText("*** who: missing flag name.", 30) return false -- allows correction end local observers = debugger.isObserving(flagName) if not observers or #observers < 1 then - trigger.action.outText("*** [" .. dcsCommon.nowString() .. "] flag <" .. flagName .. "> is currently not observed", 30) + debugger.outText("*** [" .. dcsCommon.nowString() .. "] flag <" .. flagName .. "> is currently not observed", 30) return false end - trigger.action.outText("*** [" .. dcsCommon.nowString() .. "] flag <" .. flagName .. "> is currently observed by", 30) + debugger.outText("*** [" .. dcsCommon.nowString() .. "] flag <" .. flagName .. "> is currently observed by", 30) for idx, theZone in pairs(observers) do - trigger.action.outText(" <" .. theZone.name .. "> looking for ", 30) + debugger.outText(" <" .. theZone.name .. "> looking for ", 30) end return true @@ -1001,7 +1067,7 @@ function debugDemon.processForgetCommand(args, event) local flagName = args[1] if not flagName then - trigger.action.outText("*** forget: missing flag name.", 30) + debugger.outText("*** forget: missing flag name.", 30) return false -- allows correction end @@ -1009,19 +1075,19 @@ function debugDemon.processForgetCommand(args, event) if args[2] == "with" or args[2] == "from" then -- we also allow 'from' local aName = args[3] if not aName then - trigger.action.outText("*** forget: missing after 'with'.", 30) + debugger.outText("*** forget: missing after 'with'.", 30) return false -- allows correction end aName = dcsCommon.stringRemainsStartingWith(event.remainder, aName) withTracker = debugger.getDebuggerByName(aName) if not withTracker then - trigger.action.outText("*** forget: no observer named <" .. aName .. ">", 30) + debugger.outText("*** forget: no observer named <" .. aName .. ">", 30) return false end else -- not with as arg 2 if #args > 1 then - trigger.action.outText("*** forget: unknown command after flag name '" .. flagName .. "'.", 30) + debugger.outText("*** forget: unknown command after flag name '" .. flagName .. "'.", 30) return false -- allows correction end -- use own observer @@ -1029,13 +1095,13 @@ function debugDemon.processForgetCommand(args, event) end if not debugger.isObservingWithObserver(flagName, withTracker) then - trigger.action.outText("*** forget: observer <" .. withTracker.name .. "> does not observe flag <" .. flagName .. ">", 30) + debugger.outText("*** forget: observer <" .. withTracker.name .. "> does not observe flag <" .. flagName .. ">", 30) return false end -- we add flag to tracker and init value debugger.removeFlagFromObserver(flagName, withTracker) - trigger.action.outText("*** [" .. dcsCommon.nowString() .. "] debugger: no longer observing " .. flagName .. " with <" .. withTracker.name .. ">.", 30) + debugger.outText("*** [" .. dcsCommon.nowString() .. "] debugger: no longer observing " .. flagName .. " with <" .. withTracker.name .. ">.", 30) return true end @@ -1057,25 +1123,37 @@ function debugDemon.processResetCommand(args, event) local obsName = args[1] if not obsName then debugger.reset() -- reset all - trigger.action.outText("*** [" .. dcsCommon.nowString() .. "] debug: reset complete.", 30) + debugger.outText("*** [" .. dcsCommon.nowString() .. "] debug: reset complete.", 30) return true -- allows correction end local withTracker = nil - --if args[2] == "with" then - local aName = args[1] - aName = event.remainder -- dcsCommon.stringRemainsStartingWith(event.text, aName) + local aName = event.remainder withTracker = debugger.getDebuggerByName(aName) if not withTracker then - trigger.action.outText("*** reset: no observer <" .. aName .. ">", 30) + debugger.outText("*** reset: no observer <" .. aName .. ">", 30) return false end debugger.resetObserver(withTracker) - trigger.action.outText("*** [" .. dcsCommon.nowString() .. "] debugger:reset observer <" .. withTracker.name .. ">", 30) + debugger.outText("*** [" .. dcsCommon.nowString() .. "] debugger:reset observer <" .. withTracker.name .. ">", 30) return true end + +function debugDemon.processSaveCommand(args, event) + -- save log to file, requires persistence module + -- syntax: -save [] + local aName = event.remainder + if not aName or aName:len() < 1 then + aName = "DML Debugger Log" + end + if not dcsCommon.stringEndsWith(aName, ".txt") then + aName = aName .. ".txt" + end + debugger.saveLog(aName) + return true +end -- -- init and start -- @@ -1084,7 +1162,7 @@ function debugDemon.readConfigZone() local theZone = cfxZones.getZoneByName("debugDemonConfig") if not theZone then if debugDemon.verbose then - trigger.action.outText("+++debug: NO config zone!", 30) + debugger.outText("+++debug (daemon): NO config zone!", 30) end theZone = cfxZones.createSimpleZone("debugDemonConfig") end @@ -1099,7 +1177,7 @@ function debugDemon.readConfigZone() if debugger.verbose then - trigger.action.outText("+++debug (deamon): read config", 30) + debugger.outText("+++debug (deamon): read config", 30) end end @@ -1139,6 +1217,8 @@ function debugDemon.init() debugDemon.addCommndProcessor("start", debugDemon.processStartCommand) debugDemon.addCommndProcessor("stop", debugDemon.processStopCommand) debugDemon.addCommndProcessor("reset", debugDemon.processResetCommand) + + debugDemon.addCommndProcessor("save", debugDemon.processSaveCommand) debugDemon.addCommndProcessor("?", debugDemon.processHelpCommand) debugDemon.addCommndProcessor("help", debugDemon.processHelpCommand) @@ -1159,7 +1239,11 @@ function debugDemon.start() debugDemon.snapshot = debugDemon.createSnapshot(debugger.debugZones) debugDemon.demonID = world.addEventHandler(debugDemon) - trigger.action.outText("interactive debugDemon v" .. debugDemon.version .. " started" .. "\n enter " .. debugDemon.markOfDemon .. "? in a map mark for help", 30) + debugger.outText("interactive debugDemon v" .. debugDemon.version .. " started" .. "\n enter " .. debugDemon.markOfDemon .. "? in a map mark for help", 30) + + if not _G["persistence"] then + debugger.outText("\n note: '-save' disabled, no persistence module found", 30) + end end if debugDemon.init() then @@ -1170,6 +1254,11 @@ else end --[[-- - - track units/groups: health changes - - track players: unit change + - track units/groups/objects: health changes + - track players: unit change, enter, exit + - 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 + --]]-- diff --git a/modules/unitPersistence.lua b/modules/unitPersistence.lua new file mode 100644 index 0000000..a09e030 --- /dev/null +++ b/modules/unitPersistence.lua @@ -0,0 +1,276 @@ +unitPersistence = {} +unitPersistence.version = '1.0.0' +unitPersistence.verbose = false +unitPersistence.requiredLibs = { + "dcsCommon", -- always + "cfxZones", -- Zones, of course + "persistence", + "cfxMX", +} +--[[-- + Version History + 1.0.0 - initial version + + REQUIRES PERSISTENCE AND MX + + Persist ME-placed ground units + +--]]-- +unitPersistence.groundTroops = {} -- local buffered copy that we + -- maintain from save to save +unitPersistence.statics = {} -- locally unpacked and buffered static objects + +-- +-- Save -- Callback +-- +function unitPersistence.saveData() + local theData = {} + if unitPersistence.verbose then + trigger.action.outText("+++unitPersistence: enter saveData", 30) + end + + -- theData contains last save + -- we save GROUND units placed by ME on. we access a copy of MX data + -- for ground troups, iterate through all groups, and create + -- a replacement group here and now that is used to replace the one + -- that is there when it was spawned + for groupName, groupData in pairs(unitPersistence.groundTroops) do + -- we update this record live and save it to file + if not groupData.isDead then + local gotALiveOne = false + local allUnits = groupData.units + for idx, theUnitData in pairs(allUnits) do + if not theUnitData.isDead then + local uName = theUnitData.name + local gUnit = Unit.getByName(uName) + if gUnit and gUnit:isExist() then + -- got a live one! + gotALiveOne = true + -- update x and y and heading + theUnitData.heading = dcsCommon.getUnitHeading(gUnit) + pos = gUnit:getPoint() + theUnitData.x = pos.x + theUnitData.y = pos.z -- (!!) + -- ground units do not use alt + else + theUnitData.isDead = true + end -- is alive and exists? + end -- unit not dead + end -- iterate units in group + groupData.isDead = not gotALiveOne + end -- if group is not dead + if unitPersistence.verbose then + trigger.action.outText("unitPersistence: save - processed group <" .. groupName .. ">.", 30) + end + end + + -- process all static objects placed with ME + for oName, oData in pairs(unitPersistence.statics) do + if not oData.isDead then + -- fetch the object and see if it's still alive + local theObject = StaticObject.getByName(oName) + if theObject and theObject:isExist() then + oData.heading = dcsCommon.getUnitHeading(theObject) + pos = theObject:getPoint() + 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 + trigger.action.outText("unitPersistence: save - processed group <" .. oName .. ">. " .. note, 30) + end + end + + theData.version = unitPersistence.version + theData.ground = unitPersistence.groundTroops + theData.statics = unitPersistence.statics + return theData +end + +-- +-- Load Mission Data +-- +function unitPersistence.loadMission() + local theData = persistence.getSavedDataForModule("unitPersistence") + if not theData then + if unitPersistence.verbose then + trigger.action.outText("unitPersistence: no save date received, skipping.", 30) + end + return + end + + if theData.version ~= unitPersistence.version then + trigger.action.outText("\nWARNING!\nUnit data was saved with a different (older) version!\nProceed with caution, fresh start is recommended.\n", 30) + end + + -- we just loaded an updated version of unitPersistence.groundTroops + -- now iterate all groups, update their positions and + -- delete all dead groups or units + -- because they currently should exist is the game + -- note: if they don't exist in-game that is because mission was + -- edited after last save + local mismatchWarning = false + if theData.ground then + for groupName, groupData in pairs(theData.ground) do + local theGroup = Group.getByName(groupName) + if not theGroup then + mismatchWarning = true + elseif groupData.isDead then + theGroup:destroy() + else + local newGroup = dcsCommon.clone(groupData) + local newUnits = {} + for idx, theUnitData in pairs(groupData.units) do + -- filter all dead groups + if theUnitData.isDead then + -- skip it + + else + -- add it to new group + table.insert(newUnits, theUnitData) + end + end + -- replace old unit setup with new + newGroup.units = newUnits + + local cty = groupData.cty + local cat = groupData.cat + + -- destroy the old group + --theGroup:destroy() -- will be replaced + + -- spawn new one + theGroup = coalition.addGroup(cty, cat, newGroup) + if not theGroup then + 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) + end + end + end + else + if unitPersistence.verbose then + trigger.action.outText("+++unitPersistence: no ground unit data.", 30) + end + end + + -- 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 + else + local newStatic = dcsCommon.clone(staticData) + local cty = staticData.cty + local cat = staticData.cat + -- spawn new one, replacing same.named old, dead if required + gStatic = coalition.addStaticObject(cty, newStatic) + if not gStatic then + trigger.action.outText("+++ failed to add modified static <" .. name .. ">") + end + 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) + end + end + end + end + + if mismatchWarning then + trigger.action.outText("\n+++WARNING: \nSaved data does not match mission. You should re-start from scratch\n", 30) + end + -- set mission according to data received from last save + if unitPersistence.verbose then + trigger.action.outText("unitPersistence: units set from save data.", 30) + end +end + +-- +-- Start +-- +function unitPersistence.start() + -- lib check + if (not dcsCommon) or (not dcsCommon.libCheck) then + trigger.action.outText("unit persistence requires dcsCommon", 30) + return false + end + if not dcsCommon.libCheck("unit persistence", unitPersistence.requiredLibs) then + return false + end + + -- see if we even need to persist + if not persistence.active then + return true -- WARNING: true, but not really + end + + -- sign up for save callback + callbacks = {} + callbacks.persistData = unitPersistence.saveData + persistence.registerModule("unitPersistence", callbacks) + + -- create a local copy of the entire groundForces data that + -- we maintain internally. It's fixed, and we work on our + -- own copy for speed + for gname, data in pairs(cfxMX.allGroundByName) do + local gd = dcsCommon.clone(data) -- copy the record + gd.isDead = false -- init new field to alive + -- coalition and country + gd.cat = cfxMX.catText2ID("vehicle") + local gGroup = Group.getByName(gname) + if not gGroup then + trigger.action.outText("+++warning: group <" .. gname .. "> does not exist in-game!?", 30) + else + local firstUnit = gGroup:getUnit(1) + gd.cty = firstUnit:getCountry() + unitPersistence.groundTroops[gname] = gd + end + end + + -- make local copies of all static MX objects + -- that we also maintain internally, and convert them to game + -- spawnable objects + for name, mxData in pairs(cfxMX.allStaticByName) do + -- statics in MX are built like groups, so we have to strip + -- the outer shell and extract all 'units' which are actually + -- objects. And there is usually only one + for idx, staticData in pairs(mxData.units) do + local theStatic = dcsCommon.clone(staticData) + theStatic.isDead = false + theStatic.groupId = mxData.groupId + theStatic.cat = cfxMX.catText2ID("static") + local gameOb = StaticObject.getByName(theStatic.name) + if not gameOb then + trigger.action.outText("+++warning: static object <" .. theStatic.name .. "> does not exist in-game!?", 30) + else + theStatic.cty = gameOb:getCountry() + unitPersistence.statics[theStatic.name] = theStatic + end + end + end + + -- when we run, persistence has run and may have data ready for us + if persistence.hasData then + unitPersistence.loadMission() + end + + return true +end + +if not unitPersistence.start() then + if unitPersistence.verbose then + trigger.action.outText("+++ unit persistence not available", 30) + end + unitPersistence = nil +end \ No newline at end of file diff --git a/tutorial & demo missions/demo - Being persistent.miz b/tutorial & demo missions/demo - Being persistent.miz new file mode 100644 index 0000000..7d0f2f0 Binary files /dev/null and b/tutorial & demo missions/demo - Being persistent.miz differ