Version 2.4.0

Happy new year!
smoking smoke trails module
This commit is contained in:
Christian Franz 2025-01-01 14:48:46 +01:00
parent bb301ecb30
commit 2274ba930d
7 changed files with 332 additions and 33 deletions

Binary file not shown.

Binary file not shown.

View File

@ -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 <twn: > and <loc:>
-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
-- <A/B: flagOrUnitName [val A | val B]>
local replacerValPattern = "<".. locale .. ":%s*[%s%w%*%d%.%-_]+" .. "%[[%s%w]+|[%s%w]+%]"..">"
-- FULL REWORK: find has bugs in grep
-- <A/B: flagOrUnitName [text A | text B]>
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]

View File

@ -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

View File

@ -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)

172
modules/smoking.lua Normal file
View File

@ -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:", "<none>")
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
--]]--

Binary file not shown.