diff --git a/Doc/DML Documentation.pdf b/Doc/DML Documentation.pdf index 7b73647..ced1b94 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 badc390..47614d0 100644 Binary files a/Doc/DML Quick Reference.pdf and b/Doc/DML Quick Reference.pdf differ diff --git a/modules/cfxZones.lua b/modules/cfxZones.lua index ad69560..55c759c 100644 --- a/modules/cfxZones.lua +++ b/modules/cfxZones.lua @@ -1,5 +1,5 @@ cfxZones = {} -cfxZones.version = "4.4.4" +cfxZones.version = "4.5.0" -- cf/x zone management module -- reads dcs zones and makes them accessible and mutable @@ -34,6 +34,12 @@ cfxZones.version = "4.4.4" -4.4.2 - twn support for wildcards and -4.4.3 - property name is trimmed (double check) -4.4.4 - createGroundUnitsInZoneForCoalition supports drivable +-4.4.5 - corrected startMovingZones() for linked zones via ME's LINKZONE drop-down +-4.4.6 - corrected pattern bug in processDynamicAB() +-4.5.0 - corrected bug in getBoolFromZoneProperty for default = false and "rnd" + - rnd in bool can have = xxx param for percentage + - getSmokeColorNumberFromZoneProperty() + --]]-- -- @@ -2467,20 +2473,40 @@ function cfxZones.getBoolFromZoneProperty(theZone, theProperty, defaultVal) if type(defaultVal) ~= "boolean" then defaultVal = false end - if not theZone then trigger.action.outText("WARNING: NIL Zone in getBoolFromZoneProperty", 30) return defaultVal end - - local p = cfxZones.getZoneProperty(theZone, theProperty) if not p then return defaultVal end - -- make sure we compare so default always works when -- answer isn't exactly the opposite p = p:lower() p = dcsCommon.trim(p) + local p1 = p:find("=") -- pre-proccing for random + if p1 then + local r = p:sub(p1+1, -1) + p = p:sub(1,p1-1) + if r and string.len(r) > 0 then p1 = math.floor(tonumber(r)) else p1 = nil end + p = dcsCommon.trim(p) + end + + -- special: return a random value if p == "rnd" or "?" or "maybe" + if (p == "?") or (p == "rnd") or (p == "random") or (p == "maybe") then + local matchVal = 500 -- 50% + local theRnd = math.random(1000) + if p1 then + -- we have a numeric rnd=xxx. + matchVal = p1 * 10 + end + if theZone.verbose then + trigger.action.outText("+++Zne: zone <" .. theZone.name .. "> getBool RND resolve for attr <" .. theProperty .. ">", 30) + if p1 then trigger.action.outText("rnd range set to <" .. p1 .. ">%", 30) end + trigger.action.outText("is rnd <" .. theRnd .. "> < match <" .. matchVal .. ">?", 30) + end + return (theRnd < matchVal) + end + if defaultVal == false then -- only go true if exact match to yes or true theBool = false @@ -2488,11 +2514,6 @@ function cfxZones.getBoolFromZoneProperty(theZone, theProperty, defaultVal) return theBool end - -- special: return a random value if p == "rnd" or "?" or "maybe" - if (p == "?") or (p == "rnd") or (p == "random") or (p == "maybe") then - return (math.random(1000) < 500) -- 50:50 - end - local theBool = true -- only go false if exactly no or false or "0" theBool = (p ~= 'false') and (p ~= 'no') and (p ~= "0") and (p~="off") @@ -2504,24 +2525,40 @@ function dmlZone:getBoolFromZoneProperty(theProperty, defaultVal) if type(defaultVal) ~= "boolean" then defaultVal = false end - local p = self:getZoneProperty(theProperty) if not p then return defaultVal end - -- make sure we compare so default always works when -- answer isn't exactly the opposite p = p:lower() p = dcsCommon.trim(p) + local p1 = p:find("=") -- pre-proccing for random + if p1 then + local r = p:sub(p1+1, -1) + p = p:sub(1,p1-1) + if r and string.len(r) > 0 then p1 = math.floor(tonumber(r)) else p1 = nil end + p = dcsCommon.trim(p) + end + -- special: return a random value if p == "rnd" or "?" or "maybe" + if (p == "?") or (p == "rnd") or (p == "random") or (p == "maybe") then + local matchVal = 500 -- 50% + local theRnd = math.random(1000) + if p1 then + -- we have a numeric rnd=xxx. + matchVal = p1 * 10 + end + if self.verbose then + trigger.action.outText("+++Zne: zone <" .. self.name .. "> getBool RND resolve for attr <" .. theProperty .. ">", 30) + if p1 then trigger.action.outText("rnd range set to <" .. p1 .. ">%", 30) end + trigger.action.outText("is rnd <" .. theRnd .. "> < match <" .. matchVal .. ">?", 30) + end + return (theRnd < matchVal) + end + if defaultVal == false then -- only go true if exact match to yes or true theBool = false theBool = (p == 'true') or (p == 'yes') or (p == "1") or (p=="on") return theBool - end - - -- special: return a random value if p == "rnd" or "?" or "maybe" - if (p == "?") or (p == "rnd") or (p == "random") or (p == "maybe") then - return (math.random(1000) < 500) -- 50:50 end local theBool = true @@ -2776,6 +2813,8 @@ function cfxZones.getSmokeColorStringFromZoneProperty(theZone, theProperty, defa end function dmlZone:getSmokeColorStringFromZoneProperty(theProperty, default) -- smoke as 'red', 'green', or 1..5 + return cfxZones.getSmokeColorStringFromZoneProperty(self, theProperty, default) + --[[ if not default then default = "red" end local s = self:getStringFromZoneProperty(theProperty, default) s = s:lower() @@ -2794,8 +2833,39 @@ function dmlZone:getSmokeColorStringFromZoneProperty(theProperty, default) -- sm s == "blue" then return s end return default + --]] end +function cfxZones.getSmokeColorNumberFromZoneProperty(theZone, theProperty, default) -- smoke as 'red', 'green', or 1..5 +-- NOT identical to smoke numbers used in ctf!!!! + if not default then default = "red" end + local s = cfxZones.getStringFromZoneProperty(theZone, theProperty, default) + if not s or string.len(s) < 1 then s = default end + + s = s:lower() + s = dcsCommon.trim(s) + -- check numbers + local n = tonumber(s) + if n then + if n >= 0 and n < 5 then return math.floor(n) end + return -1 -- random + end + + if s == "green" then return 0 + elseif s == "red" then return 1 + elseif s == "white" then return 2 + elseif s == "orange" then return 3 + elseif s == "blue" then return 4 + elseif s == "?" or s == "rnd" or s == "random" then return -1 + else return -1 -- should NEVER happen + end +end + +function dmlZone:getSmokeColorNumberFromZoneProperty(theProperty, default) + return cfxZones.getSmokeColorNumberFromZoneProperty(self, theProperty, default) +end + + function cfxZones.getFlareColorStringFromZoneProperty(theZone, theProperty, default) -- smoke as 'red', 'green', or 1..5 if not default then default = "red" end local s = cfxZones.getStringFromZoneProperty(theZone, theProperty, default) @@ -2820,6 +2890,8 @@ function cfxZones.getFlareColorStringFromZoneProperty(theZone, theProperty, defa end function dmlZone:getFlareColorStringFromZoneProperty(theProperty, default) -- smoke as 'red', 'green', or 1..5 + return cfxZones.getFlareColorStringFromZoneProperty(self, theProperty, default) + --[[-- if not default then default = "red" end local s = self:getStringFromZoneProperty(theProperty, default) s = s:lower() @@ -2840,6 +2912,7 @@ function dmlZone:getFlareColorStringFromZoneProperty(theProperty, default) -- sm return s end return default + --]]-- end -- @@ -3166,21 +3239,37 @@ end function cfxZones.processDynamicAB(inMsg, locale) local outMsg = inMsg + local startLoc + local endLoc + local iter = 0 if not locale then locale = "A/B" end - - -- - local replacerValPattern = "<".. locale .. ":%s*[%s%w%*%d%.%-_]+" .. "%[[%s%w]+|[%s%w]+%]"..">" + -- FULL REWORK: find has bugs in grep + -- + local replacerValPattern = "<".. locale .. ":%s*[%s%w%*%d%.%-_]+" .. + "%[[%s%w%p%-%._]+" .. "|" .. + "[%s%w%p%-%._]+%]" .. ">" -- now with captures repeat - local startLoc, endLoc = string.find(outMsg, replacerValPattern) + startLoc, endLoc = string.find(outMsg, replacerValPattern, 1) -- note "1" if startLoc then - local rp = string.sub(outMsg, startLoc, endLoc) - -- get val/unit name + iter = iter + 1 + -- let's find the "real" endloc, since find returns all if more than one hit + local e1 = string.find(outMsg, "|", startLoc) + -- from there, find the end "]>" -- no blanks between them + if not e1 then trigger.action.outText("wildcard a/B : no delim | in <" .. outMsg .. "> after <" .. startLoc .. ">, returning", 30); return "err1" end + local e2 = string.find(outMsg, "%]>", e1) + if not e2 then trigger.action.outText("wildcard A/B: no lim %]> in <" .. outMsg .. "> after <" .. e2 .. ">, returning", 30); return "err3" end + local e3 = e2 + 1 + local rp = string.sub(outMsg, startLoc, e3) -- endLoc) -- whole shebang + local asmLeft = "" -- instead of gsub we re-assemble + if startLoc > 1 then asmLeft = string.sub(outMsg, 1, startLoc-1) end -- left side + local asmRight = string.sub(outMsg, e3 + 1, -1) -- right side + -- get flag/unit name local valA, valB = string.find(rp, ":%s*[%s%w%*%d%.%-_]+%[") local val = string.sub(rp, valA+1, valB-1) val = dcsCommon.trim(val) -- get left and right - local leftA, leftB = string.find(rp, "%[[%s%w]+|" ) -- from "[" to "|" - local rightA, rightB = string.find(rp, "|[%s%w]+%]") -- from "|" to "]" + local leftA, leftB = string.find(rp, "%[[%s%w%p%-%._]+|" ) -- from "[" to "|" + local rightA, rightB = string.find(rp, "|[%s%w%p%-%._]+%]") -- from "|" to "]" left = string.sub(rp, leftA+1, leftB-1) left = dcsCommon.trim(left) right = string.sub(rp, rightA+1, rightB-1) @@ -3196,9 +3285,10 @@ function cfxZones.processDynamicAB(inMsg, locale) local locString = left if yesno then locString = right end - outMsg = string.gsub(outMsg, replacerValPattern, locString, 1) + local tmp = asmLeft .. locString .. asmRight + outMsg = tmp end - until not startLoc + until (not startLoc) or (iter > 10) -- max 2 iters return outMsg end @@ -3528,8 +3618,15 @@ function cfxZones.initLink(theZone) local dz = 0 if theZone.useOffset or theZone.useHeading then local A = cfxZones.getDCSOrigin(theZone) + if not A.x then + trigger.action.outText("+++ zones: initlink - can't access orig pos.x for A", 30) + return + end local B = dcsCommon.getOrigPositionByID(theZone.linkedUID) - + if not B.x then + trigger.action.outText("+++ zones: initlink - can't access orig.x unit for B", 30) + return + end local delta = dcsCommon.vSub(A,B) dx = delta.x dz = delta.z @@ -3592,7 +3689,8 @@ function cfxZones.startMovingZones() trigger.action.outText("WARNING: Zone <" .. aZone.name .. ">: cannot resolve linked unit ID <" .. theID .. ">", 30) lU = "***DML link err***" end - aZone.linkedUID = lU + --aZone.linkedUID = lU -- wrong! must be UID, not name + aZone.linkedUID = theID elseif aZone:hasProperty("linkedUnit") then lU = aZone:getZoneProperty("linkedUnit") -- getString: name of unit local luid = dcsCommon.unitName2ID[lU] diff --git a/modules/dcsCommon.lua b/modules/dcsCommon.lua index 3916f6b..1aea678 100644 --- a/modules/dcsCommon.lua +++ b/modules/dcsCommon.lua @@ -1,5 +1,5 @@ dcsCommon = {} -dcsCommon.version = "3.1.4" +dcsCommon.version = "3.1.5" --[[-- VERSION HISTORY 3.0.0 - removed bad bug in stringStartsWith, only relevant if caseSensitive is false - point2text new intsOnly option @@ -33,6 +33,8 @@ dcsCommon.version = "3.1.4" - new DCS Patch section 3.1.4 - new processStringWildcardsForUnit - integrated into std wildcard proccing, unit optional +3.1.5 - more verbosity on unitID2X + --]]-- -- dcsCommon is a library of common lua functions @@ -155,6 +157,9 @@ end end function dcsCommon.getOrigPositionByID(theID) + if not dcsCommon.unitID2X[theID] then + trigger.action.outText("common: getOrigPos - no unit by id for <" .. theID .. ">, type is <" .. type(theID) .. ">", 30) + end local p = {x=dcsCommon.unitID2X[theID], y=0, z=dcsCommon.unitID2Y[theID]} return p end diff --git a/modules/fogger.lua b/modules/fogger.lua index dad2e08..aa6f877 100644 --- a/modules/fogger.lua +++ b/modules/fogger.lua @@ -1,5 +1,5 @@ fogger = {} -fogger.version = "1.0.0" +fogger.version = "1.1.0" fogger.requiredLibs = { "dcsCommon", "cfxZones", @@ -9,7 +9,9 @@ fogger.zones = {} --[[-- Version history A DML module (c) 2024 by Christian FRanz -- 1.0.0 - Initial version +- 1.0.0 - Initial version +- 1.1.0 - added lcl attribute + - added onStart --]]-- function fogger.createFogZone(theZone) @@ -22,7 +24,25 @@ function fogger.createFogZone(theZone) if theZone:hasProperty("thickness") then theZone.thickMin, theZone.thickMax = theZone:getPositiveRangeFromZoneProperty("thickness", 0,0) end + theZone.lcl = theZone:getBoolFromZoneProperty("lcl", false) theZone.durMin, theZone.durMax = theZone:getPositiveRangeFromZoneProperty ("duration", 1, 1) + if theZone:hasProperty("onStart") then + --trigger.action.outText("+++fog: zone <" .. theZone.name .. "> HAS 'onStart' attribute", 30) + theZone.onStart = theZone:getBoolFromZoneProperty("onStart", false) + if theZone.onStart then + if theZone.verbose then + trigger.action.outText("+++fog: will schedule onStart fog in zone <" .. theZone.name .. ">", 30) + end + timer.scheduleFunction(fogger.doFog, theZone, timer.getTime() + 0.5) + else + --trigger.action.outText("+++ fog: onstart turned OFF", 30) + end + else + --trigger.action.outText("+++fog: zone <" .. theZone.name .. "> no 'onStart' attribute, turned off", 30) + end + if theZone.verbose then + trigger.action.outText("+++fog: zone <" .. theZone.name .. "> processed.", 30) + end end function fogger.doFog(theZone) @@ -31,7 +51,11 @@ function fogger.doFog(theZone) if vis < 100 then vis = 0 end local thick = world.weather.getFogThickness() if theZone.thickMin then thick = dcsCommon.randomBetween(theZone.thickMin, theZone.thickMax) end - if thick < 100 then thick = 0 end + if thick < 100 then thick = 0 + elseif theZone.lcl then + local p = theZone:getPoint() + thick = thick + land.getHeight({x = p.x, y = p.z}) + end local dur = dcsCommon.randomBetween(theZone.durMin, theZone.durMax) if theZone.verbose or fogger.verbose then trigger.action.outText("+++fog: will set fog vis = <" .. vis .. ">, thick = <" .. thick .. ">, transition <" .. dur .. "> secs", 30) diff --git a/modules/smoking.lua b/modules/smoking.lua new file mode 100644 index 0000000..5315b59 --- /dev/null +++ b/modules/smoking.lua @@ -0,0 +1,172 @@ +smoking = {} +smoking.version = "1.0.0" +smoking.requiredLibs = { -- a DML module (c) 2025 by Christian Franz + "dcsCommon", + "cfxZones", +} +smoking.zones = {} +smoking.roots = {} -- groups that have already been inited + +--[[-- VERSION HISTORY + - 1.0.0 initial version + +--]]-- + +-- FOR NOW REQUIRES SINGLE-UNIT PLAYER GROUPS +function smoking.createSmokingZone(theZone) + theZone.smColor = theZone:getSmokeColorNumberFromZoneProperty("smoking", "white") + if theZone.smColor > 0 then theZone.smColor = theZone.smColor + 1 end + theZone.smAlt = theZone:getNumberFromZoneProperty("alt", 0) +end + +-- event handler +function smoking:onEvent(theEvent) + if not theEvent then return end + if not theEvent.initiator then return end + local theUnit = theEvent.initiator + if not theUnit.getName then return end + if not theUnit.getPlayerName then return end + if not theUnit:getPlayerName() then return end + if not theUnit.getGroup then return end + local theGroup = theUnit:getGroup() + if not theGroup then return end + if theEvent.id == 15 and smoking.hasGUI then -- birth and gui on + local theColor = nil + local theAlt = smoking.smAlt -- default to global + -- see if we even want to install a menu + if dcsCommon.getSizeOfTable(smoking.zones) > 0 then + p = theUnit:getPoint() + for idx, theZone in pairs(smoking.zones) do + if theZone:pointInZone(p) then + theColor = theZone.smColor + theAlt = theZone.smAlt + end + end + if not theColor then return end + else + theColor = smoking.color -- use global color + end + if theColor < 1 then theColor = math.random(1, 5) end + local gName = theGroup:getName() + if smoking.roots[gName] then return end -- already inited + local uName = theUnit:getName() + local gID = theGroup:getID() + + -- remove old group menu + if smoking.roots[gName] then + missionCommands.removeItemForGroup(gID, smoking.roots[gName]) + end + + -- handle main menu + local mainMenu = nil + if smoking.mainMenu then + mainMenu = radioMenu.getMainMenuFor(smoking.mainMenu) + end + + local root = missionCommands.addSubMenuForGroup(gID, smoking.menuName, mainMenu) + smoking.roots[gName] = root + + local args = {} + args.theUnit = theUnit + args.uName = uName + args.gID = gID + args.gName = gName + args.coa = theGroup:getCoalition() + args.smAlt = theAlt + args.smColor = theColor + -- now add the submenus for convoys + local m = missionCommands.addCommandForGroup(gID, "Smoke ON", root, smoking.redirectSmoke, args) + args = {} -- create new!! ref + args.theUnit = theUnit + args.uName = uName + args.gID = gID + args.gName = gName + args.coa = theGroup:getCoalition() + args.smAlt = 0 + args.smColor = 0 -- color 0 = turn off + m = missionCommands.addCommandForGroup(gID, "Turn OFF smoke", root, smoking.redirectSmoke, args) + end -- if birth +end + +function smoking.redirectSmoke(args) -- escape debug confines + timer.scheduleFunction(smoking.doSmoke, args, timer.getTime() + 0.1) +end + +function smoking.doSmoke(args) + local uName = args.uName + local theColor = args.smColor + local theAlt = args.smAlt + trigger.action.ctfColorTag(uName, theColor, 0) -- , theAlt) + if smoking.verbose then + trigger.action.outText("+++smk: turning smoke trail for <" .. uName .. "> to <" .. theColor .. ">", 30) + end +end + + +-- config +function smoking.readConfigZone() + -- note: must match exactly!!!! + local theZone = cfxZones.getZoneByName("smokingConfig") + if not theZone then + theZone = cfxZones.createSimpleZone("smokingConfig") + end + smoking.ups = theZone:getNumberFromZoneProperty("ups", 1) + smoking.name = "smoking" + smoking.verbose = theZone.verbose + smoking.color = theZone:getSmokeColorNumberFromZoneProperty("color", "white" ) + if smoking.color >= 0 then smoking.color = smoking.color + 1 end -- yeah, ctf aircraft smoke and ground smoke are NOT the same, ctf is gnd + 1. huzzah! + smoking.smAlt = theZone:getNumberFromZoneProperty("alt", 0) + smoking.menuName = theZone:getStringFromZoneProperty("menuName", "Smoke Trail") + smoking.hasGUI = theZone:getBoolFromZoneProperty("GUI", true) + if theZone:hasProperty("attachTo:") then + local attachTo = theZone:getStringFromZoneProperty("attachTo:", "") + if radioMenu then -- requires optional radio menu to have loaded + local mainMenu = radioMenu.mainMenus[attachTo] + if mainMenu then + smoking.mainMenu = mainMenu + else + trigger.action.outText("+++smoking: cannot find super menu <" .. attachTo .. ">", 30) + end + else + trigger.action.outText("+++smoking: REQUIRES radioMenu to run before smoking. 'AttachTo:' ignored.", 30) + end + end +end + + +-- go go go +function smoking.start() + -- lib check + if not dcsCommon.libCheck then + trigger.action.outText("smoking requires dcsCommon", 30) + return false + end + if not dcsCommon.libCheck("smoking", smoking.requiredLibs) then + return false + end + -- read config + smoking.readConfigZone() + -- process "fog?" Zones + local attrZones = cfxZones.getZonesWithAttributeNamed("smoking") + for k, aZone in pairs(attrZones) do + smoking.createSmokingZone(aZone) + smoking.zones[aZone.name] = aZone + end + -- hook into events + world.addEventHandler(smoking) + + trigger.action.outText("smoking v" .. smoking.version .. " started.", 30) + return true +end + +-- let's go! +if not smoking.start() then + trigger.action.outText("smoking aborted: error on start", 30) + smoking = nil +end + +--[[-- + To Do: + - smoking zones where aircraft automatically turn on/off their smoke + - different smoke colors for red and blue in autosmoke zones +--]]-- \ No newline at end of file diff --git a/tutorial & demo missions/demo - The smoke is ON.miz b/tutorial & demo missions/demo - The smoke is ON.miz new file mode 100644 index 0000000..6fcc718 Binary files /dev/null and b/tutorial & demo missions/demo - The smoke is ON.miz differ