Version 1.2.2

New Valet Module
Wildcard refactoring
Cloner to delicates integration
This commit is contained in:
Christian Franz 2023-01-26 10:31:40 +01:00
parent 43d598f035
commit 654b782894
12 changed files with 1037 additions and 425 deletions

Binary file not shown.

Binary file not shown.

View File

@ -1,5 +1,5 @@
FARPZones = {} FARPZones = {}
FARPZones.version = "1.2.0" FARPZones.version = "1.2.1"
FARPZones.verbose = false FARPZones.verbose = false
--[[-- --[[--
Version History Version History
@ -13,6 +13,8 @@ FARPZones.verbose = false
- verbose cleanup ("FZ: something happened") - verbose cleanup ("FZ: something happened")
1.2.0 - persistence 1.2.0 - persistence
- handles contested state - handles contested state
1.2.1 - now gracefully handles a FARP Zone that does not
contain a FARP, but is placed beside it
--]]-- --]]--
@ -120,9 +122,9 @@ function FARPZones.createFARPFromZone(aZone)
local theFarp = {} local theFarp = {}
theFarp.zone = aZone theFarp.zone = aZone
theFarp.name = aZone.name theFarp.name = aZone.name
theFarp.point = cfxZones.getPoint(aZone) -- failsafe
-- find the FARPS that belong to this zone -- find the FARPS that belong to this zone
local thePoint = aZone.point local thePoint = cfxZones.getPoint(aZone)
local mapFarps = dcsCommon.getAirbasesInRangeOfPoint( local mapFarps = dcsCommon.getAirbasesInRangeOfPoint(
thePoint, thePoint,
aZone.radius, aZone.radius,
@ -132,25 +134,35 @@ function FARPZones.createFARPFromZone(aZone)
theFarp.myFarps = mapFarps theFarp.myFarps = mapFarps
theFarp.owner = 0 -- start with neutral theFarp.owner = 0 -- start with neutral
aZone.owner = 0 aZone.owner = 0
if #mapFarps == 0 then if #mapFarps == 0 then
trigger.action.outText("***Farp Zones: no FARP found for zone " .. aZone.name, 30) if aZone.verbose or FARPZones.verbose then
else trigger.action.outText("***Farp Zones: no FARP found inside zone " .. aZone.name .. ", associating closest FARP", 30)
--for idx, aFarp in pairs(mapFarps) do end
-- trigger.action.outText("Associated FARP " .. aFarp:getName() .. " with FARP Zone " .. aZone.name, 30) local closest = dcsCommon.getClosestAirbaseTo(thePoint, 1)
--end if not closest then
trigger.action.outText("***FARP Zones: unable to find a FARP to associate zone <" .. aZone.name .. "> with.", 30)
return
else
if aZone.verbose or FARPZones.verbose then
trigger.action.outText("associated FARP <" .. closest:getName() .. "> with zone <" .. aZone.name .. ">", 30)
end
end
mapFarps[1] = closest
end
theFarp.mainFarp = theFarp.myFarps[1] theFarp.mainFarp = theFarp.myFarps[1]
theFarp.point = theFarp.mainFarp:getPoint() -- this is FARP, not zone!!! theFarp.point = theFarp.mainFarp:getPoint() -- this is FARP, not zone!!!
theFarp.owner = theFarp.mainFarp:getCoalition() theFarp.owner = theFarp.mainFarp:getCoalition()
aZone.owner = theFarp.owner aZone.owner = theFarp.owner
end -- end
-- get r and phi for defenders -- get r and phi for defenders
local rPhi = cfxZones.getVectorFromZoneProperty( local rPhi = cfxZones.getVectorFromZoneProperty(
aZone, aZone,
"rPhiHDef", "rPhiHDef",
3) 3)
--trigger.action.outText("*** DEF rPhi are " .. rPhi[1] .. " and " .. rPhi[2] .. " heading " .. rPhi[3], 30)
-- get r and phi for facilities -- get r and phi for facilities
-- create a new defenderzone for this -- create a new defenderzone for this
local r = rPhi[1] local r = rPhi[1]

View File

@ -31,6 +31,7 @@ cfxObjectSpawnZones.verbose = false
-- - useDelicates link to delicate when spawned -- - useDelicates link to delicate when spawned
-- - spawned single and multi-objects can be made delicates -- - spawned single and multi-objects can be made delicates
-- 1.3.1 - baseName can be set to zone's name by giving "*" -- 1.3.1 - baseName can be set to zone's name by giving "*"
-- 1.3.2 - delicateName supports '*' to refer to own zone
-- respawn currently happens after theSpawns is deleted and cooldown seconds have passed -- respawn currently happens after theSpawns is deleted and cooldown seconds have passed
@ -123,7 +124,8 @@ function cfxObjectSpawnZones.createSpawner(inZone)
-- see if the spawn can be made brittle/delicte -- see if the spawn can be made brittle/delicte
if cfxZones.hasProperty(inZone, "useDelicates") then if cfxZones.hasProperty(inZone, "useDelicates") then
theSpawner.delicateName = cfxZones.getStringFromZoneProperty(inZone, "useDelicates", "<none>") theSpawner.delicateName = dcsCommon.trim(cfxZones.getStringFromZoneProperty(inZone, "useDelicates", "<none>"))
if theSpawner.delicateName == "*" then theSpawner.delicateName = inZone.name end
end end
-- see if it is linked to a ship to set realtive orig headiong -- see if it is linked to a ship to set realtive orig headiong

View File

@ -64,6 +64,7 @@ cfxSpawnZones.spawnedGroups = {}
-- 1.7.1 - improved verbosity -- 1.7.1 - improved verbosity
-- - spelling check -- - spelling check
-- 1.7.2 - baseName now can can be set to zone name by issuing "*" -- 1.7.2 - baseName now can can be set to zone name by issuing "*"
-- 1.7.3 - ability to hand off to delicates, useDelicates attribute
-- --
-- new version requires cfxGroundTroops, where they are -- new version requires cfxGroundTroops, where they are
-- --
@ -152,7 +153,14 @@ function cfxSpawnZones.createSpawner(inZone)
if cfxZones.hasProperty(inZone, "trackWith:") then if cfxZones.hasProperty(inZone, "trackWith:") then
inZone.trackWith = cfxZones.getStringFromZoneProperty(inZone, "trackWith:", "<None>") inZone.trackWith = cfxZones.getStringFromZoneProperty(inZone, "trackWith:", "<None>")
end end
-- interface to delicates
if cfxZones.hasProperty(inZone, "useDelicates") then
theSpawner.delicateName = dcsCommon.trim(cfxZones.getStringFromZoneProperty(inZone, "useDelicates", "<none>"))
if theSpawner.delicateName == "*" then theSpawner.delicateName = inZone.name end
end
-- connect with ME if a trigger flag is given -- connect with ME if a trigger flag is given
if cfxZones.hasProperty(inZone, "f?") then if cfxZones.hasProperty(inZone, "f?") then
theSpawner.triggerFlag = cfxZones.getStringFromZoneProperty(inZone, "f?", "none") theSpawner.triggerFlag = cfxZones.getStringFromZoneProperty(inZone, "f?", "none")
@ -411,6 +419,17 @@ function cfxSpawnZones.spawnWithSpawner(aSpawner)
end end
-- hand off to delicates
if aSpawner.delicateName and delicates then
-- pass this object to the delicate zone mentioned
local theDeli = delicates.getDelicatesByName(aSpawner.delicateName)
if theDeli then
delicates.addGroupToInventoryForZone(theDeli, newTroops)
else
trigger.action.outText("+++Spwn: spawner <" .. aZone.name .. "> can't find delicates <" .. aSpawner.delicateName .. ">", 30)
end
end
-- track this if we are have a trackwith attribute -- track this if we are have a trackwith attribute
-- note that we retrieve trackwith from ZONE, not spawner -- note that we retrieve trackwith from ZONE, not spawner
if theZone.trackWith then if theZone.trackWith then

View File

@ -1,5 +1,5 @@
cfxZones = {} cfxZones = {}
cfxZones.version = "3.0.0" cfxZones.version = "3.0.2"
-- cf/x zone management module -- cf/x zone management module
-- reads dcs zones and makes them accessible and mutable -- reads dcs zones and makes them accessible and mutable
@ -97,7 +97,6 @@ cfxZones.version = "3.0.0"
- isPointInsideZone() returns delta as well - isPointInsideZone() returns delta as well
- 2.9.0 - linked zones can useOffset and useHeading - 2.9.0 - linked zones can useOffset and useHeading
- getPoint update - getPoint update
- new getOrigin()
- pointInZone understands useOrig - pointInZone understands useOrig
- allStaticsInZone supports useOrig - allStaticsInZone supports useOrig
- dPhi for zones with useHeading - dPhi for zones with useHeading
@ -117,6 +116,9 @@ cfxZones.version = "3.0.0"
linedUnit and warning. linedUnit and warning.
- initZoneVerbosity() - initZoneVerbosity()
- 3.0.1 - updateMovingZones() better tracks linked units by name - 3.0.1 - updateMovingZones() better tracks linked units by name
- 3.0.2 - maxRadius for all zones, only differs from radius in polyZones
- re-factoring zone-base string processing from messenger module
- new processStringWildcards() that does almost all that messenger can
--]]-- --]]--
@ -244,6 +246,7 @@ function cfxZones.readFromDCS(clearfirst)
-- circular zone -- circular zone
newZone.isCircle = true newZone.isCircle = true
newZone.radius = dcsZone.radius newZone.radius = dcsZone.radius
newZone.maxRadius = newZone.radius -- same for circular
elseif zoneType == 2 then elseif zoneType == 2 then
-- polyZone -- polyZone
@ -254,6 +257,7 @@ function cfxZones.readFromDCS(clearfirst)
-- now transfer all point in the poly -- now transfer all point in the poly
-- note: DCS in 2.7 misspells vertices as 'verticies' -- note: DCS in 2.7 misspells vertices as 'verticies'
-- correct for this -- correct for this
newZone.maxRadius = 0
local verts = {} local verts = {}
if dcsZone.verticies then verts = dcsZone.verticies if dcsZone.verticies then verts = dcsZone.verticies
else else
@ -265,6 +269,10 @@ function cfxZones.readFromDCS(clearfirst)
local dcsPoint = verts[v] local dcsPoint = verts[v]
local polyPoint = cfxZones.createPointFromDCSPoint(dcsPoint) -- (x, y) --> (x, 0, y-->z) local polyPoint = cfxZones.createPointFromDCSPoint(dcsPoint) -- (x, y) --> (x, 0, y-->z)
newZone.poly[v] = polyPoint newZone.poly[v] = polyPoint
-- measure distance from zone's point, and store maxRadius
-- dcs always saves a point with the poly zone
local dist = dcsCommon.dist(newZone.point, polyPoint)
if dist > newZone.maxRadius then newZone.maxRadius = dist end
end end
else else
@ -1194,65 +1202,12 @@ function cfxZones.createGroundUnitsInZoneForCoalition (theCoalition, groupName,
return newGroup, groupDataCopy return newGroup, groupDataCopy
end end
-- parsing zone names. The first part of the name until the first blank " "
-- is the prefix and is dropped unless keepPrefix is true.
-- all others are regarded as key:value pairs and are then added
-- to the zone
-- separated by equal sign "=" AND MUST NOT CONTAIN BLANKS
-- --
-- example usage "followZone unit=rotary-1 dx=30 dy=25 rotateWithHeading=true -- ===============
-- FLAG PROCESSING
-- ===============
-- --
-- OLD DEPRECATED TECH -- TO BE DECOMMISSIONED SOON, DO NOT USE
--
--[[--
function cfxZones.parseZoneNameIntoAttributes(theZone, keepPrefix)
-- trigger.action.outText("Parsing zone: ".. theZone.name, 30)
if not keepPrefix then keepPrefix = false end -- simply for clarity
-- now split the name into space-separated strings
local attributes = dcsCommon.splitString(theZone.name, " ")
if not keepPrefix then table.remove(attributes, 1) end -- pop prefix
-- now parse all substrings and add them as attributes to theZone
for i=1, #attributes do
local a = attributes[i]
local kvp = dcsCommon.splitString(a, "=")
if #kvp == 2 then
-- we have key value pair
local theKey = kvp[1]
local theValue = kvp[2]
theZone[theKey] = theValue
-- trigger.action.outText("Zone ".. theZone.name .. " parsed: Key = " .. theKey .. ", Value = " .. theValue, 30)
else
-- trigger.action.outText("Zone ".. theZone.name .. ": dropped attribute " .. a, 30)
end
end
end
--]]--
-- OLD DEPRECATED TECH -- TO BE DECOMMISSIONED SOON, DO NOT USE
--[[--
function cfxZones.processCraterZones ()
local craters = cfxZones.zonesStartingWith("crater")
-- all these zones need to be processed and their name infor placed into attributes
for cName, cZone in pairs(craters) do
cfxZones.parseZoneNameIntoAttributes(cZone)
-- blow stuff up at the location of the zone
local cPoint = cZone.point
cPoint.y = land.getHeight({x = cPoint.x, y = cPoint.z}) -- compensate for ground level
trigger.action.explosion(cPoint, 900)
-- now interpret and act on the crater info
-- to destroy and place fire.
-- fire has small, medium, large
-- eg. fire=large
end
end
--]]--
-- --
-- Flag Pulling -- Flag Pulling
-- --
@ -1538,8 +1493,6 @@ function cfxZones.isMEFlag(inFlag)
trigger.action.outText("+++zne: warning: deprecated isMEFlag", 30) trigger.action.outText("+++zne: warning: deprecated isMEFlag", 30)
return true return true
-- returns true if inFlag is a pure positive number -- returns true if inFlag is a pure positive number
-- inFlag = dcsCommon.trim(inFlag)
-- return dcsCommon.stringIsPositiveNumber(inFlag)
end end
function cfxZones.verifyMethod(theMethod, theZone) function cfxZones.verifyMethod(theMethod, theZone)
@ -1579,7 +1532,6 @@ function cfxZones.verifyMethod(theMethod, theZone)
local op = string.sub(theMethod, 1, 1) local op = string.sub(theMethod, 1, 1)
local remainder = string.sub(theMethod, 2) local remainder = string.sub(theMethod, 2)
remainder = dcsCommon.trim(remainder) -- remove all leading and trailing spaces remainder = dcsCommon.trim(remainder) -- remove all leading and trailing spaces
-- local rNum = tonumber(remainder)
if true then if true then
-- we have a comparison = ">", "=", "<" followed by a number -- we have a comparison = ">", "=", "<" followed by a number
@ -1898,7 +1850,9 @@ function cfxZones.flagArrayFromString(inString)
end end
-- --
-- PROPERTY PROCESSING -- ===================
-- PROPERTY PROCESSING
-- ===================
-- --
function cfxZones.getAllZoneProperties(theZone, caseInsensitive) -- return as dict function cfxZones.getAllZoneProperties(theZone, caseInsensitive) -- return as dict
@ -2030,9 +1984,7 @@ function cfxZones.getPositiveRangeFromZoneProperty(theZone, theProperty, default
upperBound = lowerBound upperBound = lowerBound
lowerBound = temp lowerBound = temp
end end
-- if rndFlags.verbose then
-- trigger.action.outText("+++Zne: detected range <" .. lowerBound .. ", " .. upperBound .. ">", 30)
-- end
else else
-- bounds illegal -- bounds illegal
trigger.action.outText("+++Zne: illegal range <" .. rangeString .. ">, using " .. default .. "-" .. default, 30) trigger.action.outText("+++Zne: illegal range <" .. rangeString .. ">, using " .. default .. "-" .. default, 30)
@ -2043,7 +1995,7 @@ function cfxZones.getPositiveRangeFromZoneProperty(theZone, theProperty, default
upperBound = cfxZones.getNumberFromZoneProperty(theZone, theProperty, default) -- between pulses upperBound = cfxZones.getNumberFromZoneProperty(theZone, theProperty, default) -- between pulses
lowerBound = upperBound lowerBound = upperBound
end end
-- trigger.action.outText("+++Zne: returning <" .. lowerBound .. ", " .. upperBound .. ">", 30)
return lowerBound, upperBound return lowerBound, upperBound
end end
@ -2201,7 +2153,290 @@ function cfxZones.getSmokeColorStringFromZoneProperty(theZone, theProperty, defa
end end
-- --
-- Moving Zones. They contain a link to their unit -- Zone-based wildcard processing
--
-- process <z>
function cfxZones.processZoneStatics(inMsg, theZone)
if theZone then
inMsg = inMsg:gsub("<z>", theZone.name)
end
return inMsg
end
-- process <t>, <lat>, <lon>, <ele>, <mgrs>
function cfxZones.processSimpleZoneDynamics(inMsg, theZone, timeFormat, imperialUnits)
if not inMsg then return "<nil inMsg>" end
-- replace <t> with current mission time HMS
local absSecs = timer.getAbsTime()-- + env.mission.start_time
while absSecs > 86400 do
absSecs = absSecs - 86400 -- subtract out all days
end
if not timeFormat then timeFormat = "<:h>:<:m>:<:s>" end
local timeString = dcsCommon.processHMS(timeFormat, absSecs)
local outMsg = inMsg:gsub("<t>", timeString)
-- replace <lat> with lat of zone point and <lon> with lon of zone point
-- and <mgrs> with mgrs coords of zone point
local currPoint = cfxZones.getPoint(theZone)
local lat, lon = coord.LOtoLL(currPoint)
lat, lon = dcsCommon.latLon2Text(lat, lon)
local alt = land.getHeight({x = currPoint.x, y = currPoint.z})
if imperialUnits then
alt = math.floor(alt * 3.28084) -- feet
else
alt = math.floor(alt) -- meters
end
outMsg = outMsg:gsub("<lat>", lat)
outMsg = outMsg:gsub("<lon>", lon)
outMsg = outMsg:gsub("<ele>", alt)
local grid = coord.LLtoMGRS(coord.LOtoLL(currPoint))
local mgrs = grid.UTMZone .. ' ' .. grid.MGRSDigraph .. ' ' .. grid.Easting .. ' ' .. grid.Northing
outMsg = outMsg:gsub("<mgrs>", mgrs)
return outMsg
end
-- process <v: flag>, <rsp: flag> <rrnd>
function cfxZones.processDynamicValues(inMsg, theZone, msgResponses)
-- replace all occurences of <v: flagName> with their values
local pattern = "<v:%s*[%s%w%*%d%.%-_]+>" -- no list allowed but blanks and * and . and - and _ --> we fail on the other specials to keep this simple
local outMsg = inMsg
repeat -- iterate all patterns one by one
local startLoc, endLoc = string.find(outMsg, pattern)
if startLoc then
local theValParam = string.sub(outMsg, startLoc, endLoc)
-- strip lead and trailer
local param = string.gsub(theValParam, "<v:%s*", "")
param = string.gsub(param, ">","")
-- param = dcsCommon.trim(param) -- trim is called anyway
-- access flag
local val = cfxZones.getFlagValue(param, theZone)
val = tostring(val)
if not val then val = "NULL" end
-- replace pattern in original with new val
outMsg = string.gsub(outMsg, pattern, val, 1) -- only one sub!
end
until not startLoc
-- now process rsp
pattern = "<rsp:%s*[%s%w%*%d%.%-_]+>" -- no list allowed but blanks and * and . and - and _ --> we fail on the other specials to keep this simple
if msgResponses and (#msgResponses > 0) then -- only if this zone has an array
--trigger.action.outText("enter response proccing", 30)
repeat -- iterate all patterns one by one
local startLoc, endLoc = string.find(outMsg, pattern)
if startLoc then
--trigger.action.outText("response: found an occurence", 30)
local theValParam = string.sub(outMsg, startLoc, endLoc)
-- strip lead and trailer
local param = string.gsub(theValParam, "<rsp:%s*", "")
param = string.gsub(param, ">","")
-- access flag
local val = cfxZones.getFlagValue(param, theZone)
if not val or (val < 1) then val = 1 end
if val > msgResponses then val = msgResponses end
val = msgResponses[val]
val = dcsCommon.trim(val)
-- replace pattern in original with new val
outMsg = string.gsub(outMsg, pattern, val, 1) -- only one sub!
end
until not startLoc
-- rnd response
local rndRsp = dcsCommon.pickRandom(msgResponses)
outMsg = outMsg:gsub ("<rrnd>", rndRsp)
end
return outMsg
end
-- process <t: flag>
function cfxZones.processDynamicTime(inMsg, theZone, timeFormat)
if not timeFormat then timeFormat = "<:h>:<:m>:<:s>" end
-- replace all occurences of <t: flagName> with their values
local pattern = "<t:%s*[%s%w%*%d%.%-_]+>" -- no list allowed but blanks and * and . and - and _ --> we fail on the other specials to keep this simple
local outMsg = inMsg
repeat -- iterate all patterns one by one
local startLoc, endLoc = string.find(outMsg, pattern)
if startLoc then
local theValParam = string.sub(outMsg, startLoc, endLoc)
-- strip lead and trailer
local param = string.gsub(theValParam, "<t:%s*", "")
param = string.gsub(param, ">","")
-- access flag
local val = cfxZones.getFlagValue(param, theZone)
-- use this to process as time value
--trigger.action.outText("time: accessing <" .. param .. "> and received <" .. val .. ">", 30)
local timeString = dcsCommon.processHMS(timeFormat, val)
if not timeString then timeString = "NULL" end
-- replace pattern in original with new val
outMsg = string.gsub(outMsg, pattern, timeString, 1) -- only one sub!
end
until not startLoc
return outMsg
end
-- process <lat/lon/ele/mgrs/lle/latlon/alt/vel/hdg/rhdg/type/player: zone/unit>
function cfxZones.processDynamicLoc(inMsg, imperialUnits, responses)
local locales = {"lat", "lon", "ele", "mgrs", "lle", "latlon", "alt", "vel", "hdg", "rhdg", "type", "player"}
local outMsg = inMsg
local uHead = 0
for idx, aLocale in pairs(locales) do
local pattern = "<" .. aLocale .. ":%s*[%s%w%*%d%.%-_]+>"
repeat -- iterate all patterns one by one
local startLoc, endLoc = string.find(outMsg, pattern)
if startLoc then
local theValParam = string.sub(outMsg, startLoc, endLoc)
-- strip lead and trailer
local param = string.gsub(theValParam, "<" .. aLocale .. ":%s*", "")
param = string.gsub(param, ">","")
-- find zone or unit
param = dcsCommon.trim(param)
local thePoint = nil
local tZone = cfxZones.getZoneByName(param)
local tUnit = Unit.getByName(param)
local spd = 0
local angels = 0
local theType = "<errType>"
local playerName = "Unknown"
if tZone then
theType = "Zone"
playerName = "?zone?"
thePoint = cfxZones.getPoint(tZone)
if tZone.linkedUnit and Unit.isExist(tZone.linkedUnit) then
local lU = tZone.linkedUnit
local masterPoint = lU:getPoint()
thePoint.y = masterPoint.y
spd = dcsCommon.getUnitSpeed(lU)
spd = math.floor(spd * 3.6)
uHead = math.floor(dcsCommon.getUnitHeading(tUnit) * 57.2958) -- to degrees.
else
-- since zones always have elevation of 0,
-- now get the elevation from the map
thePoint.y = land.getHeight({x = thePoint.x, y = thePoint.z})
end
elseif tUnit then
if Unit.isExist(tUnit) then
theType = tUnit:getTypeName()
if tUnit.getPlayerName and tUnit:getPlayerName() then
playerName = tUnit:getPlayerName()
end
thePoint = tUnit:getPoint()
spd = dcsCommon.getUnitSpeed(tUnit)
-- convert m/s to km/h
spd = math.floor(spd * 3.6)
uHead = math.floor(dcsCommon.getUnitHeading(tUnit) * 57.2958) -- to degrees.
end
else
-- nothing to do, remove me.
end
local locString = "err"
if thePoint then
-- now that we have a point, we can do locale-specific
-- processing. return result in locString
local lat, lon, alt = coord.LOtoLL(thePoint)
lat, lon = dcsCommon.latLon2Text(lat, lon)
angels = math.floor(thePoint.y)
if imperialUnits then
alt = math.floor(alt * 3.28084) -- feet
spd = math.floor(spd * 0.539957) -- km/h to knots
angels = math.floor(angels * 3.28084)
else
alt = math.floor(alt) -- meters
end
if angels > 1000 then
angels = math.floor(angels / 100) * 100
end
if aLocale == "lat" then locString = lat
elseif aLocale == "lon" then locString = lon
elseif aLocale == "ele" then locString = tostring(alt)
elseif aLocale == "lle" then locString = lat .. " " .. lon .. " ele " .. tostring(alt)
elseif aLocale == "latlon" then locString = lat .. " " .. lon
elseif aLocale == "alt" then locString = tostring(angels) -- don't confuse alt and angels, bad var naming here
elseif aLocale == "vel" then locString = tostring(spd)
elseif aLocale == "hdg" then locString = tostring(uHead)
elseif aLocale == "type" then locString = theType
elseif aLocale == "player" then locString = playerName
elseif aLocale == "rhdg" and (responses) then
local offset = cfxZones.rspMapper360(uHead, #responses)
locString = dcsCommon.trim(responses[offset])
else
-- we have mgrs
local grid = coord.LLtoMGRS(coord.LOtoLL(thePoint))
locString = grid.UTMZone .. ' ' .. grid.MGRSDigraph .. ' ' .. grid.Easting .. ' ' .. grid.Northing
end
end
-- replace pattern in original with new val
outMsg = string.gsub(outMsg, pattern, locString, 1) -- only one sub!
end -- if startloc
until not startLoc
end -- for all locales
return outMsg
end
function cfxZones.rspMapper360(directionInDegrees, numResponses)
-- maps responses around a clock. Clock has 12 'responses' (12, 1, .., 11),
-- with the first (12) also mapping to the last half arc
-- this method dynamically 'winds' the responses around
-- a clock and returns the index of the message to display
if numResponses < 1 then numResponses = 1 end
directionInDegrees = math.floor(directionInDegrees)
while directionInDegrees < 0 do directionInDegrees = directionInDegrees + 360 end
while directionInDegrees >= 360 do directionInDegrees = directionInDegrees - 360 end
-- now we have 0..360
-- calculate arc per item
local arcPerItem = 360 / numResponses
local halfArc = arcPerItem / 2
-- we now map 0..360 to (0-halfArc..360-halfArc) by shifting
-- direction by half-arc and clipping back 0..360
-- and now we can directly derive the index of the response
directionInDegrees = directionInDegrees + halfArc
if directionInDegrees >= 360 then directionInDegrees = directionInDegrees - 360 end
local index = math.floor(directionInDegrees / arcPerItem) + 1 -- 1 .. numResponses
return index
end
-- replaces dcsCommon with same name
-- timeFormat is optional, default is "<:h>:<:m>:<:s>"
-- imperialUnits is optional, defaults to meters
-- responses is an array of string, defaults to {}
function cfxZones.processStringWildcards(inMsg, theZone, timeFormat, imperialUnits, responses)
if not inMsg then return "<nil inMsg>" end
local formerType = type(inMsg)
if formerType ~= "string" then inMsg = tostring(inMsg) end
if not inMsg then inMsg = "<inMsg is incompatible type " .. formerType .. ">" end
local theMsg = inMsg
-- process common DCS stuff like /n
theMsg = dcsCommon.processStringWildcards(theMsg) -- call old inherited
-- process <z>
theMsg = cfxZones.processZoneStatics(theMsg, theZone)
-- process <t>, <lat>, <lon>, <ele>, <mgrs>
theMsg = cfxZones.processSimpleZoneDynamics(theMsg, theZone, timeFormat, imperialUnits)
-- process <v: flag>, <rsp: flag> <rrnd>
theMsg = cfxZones.processDynamicValues(theMsg, theZone, responses)
-- process <t: flag>
theMsg = cfxZones.processDynamicTime(theMsg, theZone, timeFormat)
-- process <lat/lon/ele/mgrs/lle/latlon/alt/vel/hdg/rhdg/type/player: zone/unit>
theMsg = cfxZones.processDynamicLoc(theMsg, imperialUnits, responses)
return theMsg
end
--
-- ============
-- MOVING ZONES
-- ============
--
-- Moving zones contain a link to their unit
-- they are always located at an offset (x,z) or delta, phi -- they are always located at an offset (x,z) or delta, phi
-- to their master unit. delta phi allows adjustment for heading -- to their master unit. delta phi allows adjustment for heading
-- The cool thing about moving zones in cfx is that they do not -- The cool thing about moving zones in cfx is that they do not
@ -2239,13 +2474,7 @@ function cfxZones.getPoint(aZone) -- always works, even linked, returned point c
thePos.x = aZone.point.x thePos.x = aZone.point.x
thePos.y = 0 -- aZone.y thePos.y = 0 -- aZone.y
thePos.z = aZone.point.z thePos.z = aZone.point.z
--[[--
if aZone.linkedUnit then
trigger.action.outText("GetPoint: LINKED <".. aZone.name .. "> p = " .. dcsCommon.point2text(thePos) .. ", O = " .. dcsCommon.point2text(cfxZones.getDCSOrigin(aZone)), 30 )
else
trigger.action.outText("GetPoint: unlinked <".. aZone.name .. "> p = " .. dcsCommon.point2text(thePos) .. ", O = " .. dcsCommon.point2text(cfxZones.getDCSOrigin(aZone)), 30 )
end
--]]--
return thePos return thePos
end end
@ -2259,8 +2488,7 @@ function cfxZones.linkUnitToZone(theUnit, theZone, dx, dy) -- note: dy is really
local unitHeading = dcsCommon.getUnitHeading(theUnit) local unitHeading = dcsCommon.getUnitHeading(theUnit)
local bearingOffset = math.atan2(dy, dx) -- rads local bearingOffset = math.atan2(dy, dx) -- rads
if bearingOffset < 0 then bearingOffset = bearingOffset + 2 * 3.141592 end if bearingOffset < 0 then bearingOffset = bearingOffset + 2 * 3.141592 end
--trigger.action.outText("zone <" .. theZone.name .. "> is <" .. math.floor(bearingOffset * 57.2958) .. "> degrees from Unit <" .. theUnit:getName() .. ">", 30)
--trigger.action.outText("Unit <" .. theUnit:getName() .. "> has heading .. <" .. math.floor(57.2958 * unitHeading) .. ">", 30)
local dPhi = bearingOffset - unitHeading local dPhi = bearingOffset - unitHeading
if dPhi < 0 then dPhi = dPhi + 2 * 3.141592 end if dPhi < 0 then dPhi = dPhi + 2 * 3.141592 end
if (theZone.verbose and theZone.useHeading) then if (theZone.verbose and theZone.useHeading) then
@ -2349,14 +2577,12 @@ function cfxZones.updateMovingZones()
end end
function cfxZones.initLink(theZone) function cfxZones.initLink(theZone)
--trigger.action.outText("enter initlink for <" .. theZone.name .. ">")
--if true then return end
--trigger.action.outText("entry verbose check: <" .. theZone.name .. "> is verbose = " .. dcsCommon.bool2YesNo(theZone.verbose), 30)
theZone.linkBroken = true theZone.linkBroken = true
theZone.linkedUnit = nil theZone.linkedUnit = nil
theUnit = Unit.getByName(theZone.linkName) theUnit = Unit.getByName(theZone.linkName)
if theUnit then if theUnit then
--trigger.action.outText("initlink has link to <" .. theZone.linkName .. "> for <" .. theZone.name .. ">", 30)
local dx = 0 local dx = 0
local dz = 0 local dz = 0
if theZone.useOffset or theZone.useHeading then if theZone.useOffset or theZone.useHeading then
@ -2367,12 +2593,12 @@ function cfxZones.initLink(theZone)
dz = delta.z dz = delta.z
end end
cfxZones.linkUnitToZone(theUnit, theZone, dx, dz) -- also sets theZone.linkedUnit cfxZones.linkUnitToZone(theUnit, theZone, dx, dz) -- also sets theZone.linkedUnit
--trigger.action.outText("verbose check: <" .. theZone.name .. "> is verbose = " .. dcsCommon.bool2YesNo(theZone.verbose), 30)
if theZone.verbose then if theZone.verbose then
trigger.action.outText("Link established for zone <" .. theZone.name .. "> to unit <" .. theZone.linkName .. ">: dx=<" .. math.floor(dx) .. ">, dz=<" .. math.floor(dz) .. "> dist = <" .. math.floor(math.sqrt(dx * dx + dz * dz)) .. ">" , 30) trigger.action.outText("Link established for zone <" .. theZone.name .. "> to unit <" .. theZone.linkName .. ">: dx=<" .. math.floor(dx) .. ">, dz=<" .. math.floor(dz) .. "> dist = <" .. math.floor(math.sqrt(dx * dx + dz * dz)) .. ">" , 30)
end end
theZone.linkBroken = nil theZone.linkBroken = nil
--trigger.action.outText("done linking <" .. theZone.linkName .. "> to zone <" .. theZone.name .. ">", 30)
else else
if theZone.verbose then if theZone.verbose then
trigger.action.outText("Linked unit: no unit <" .. theZone.linkName .. "> to link <" .. theZone.name .. "> to", 30) trigger.action.outText("Linked unit: no unit <" .. theZone.linkName .. "> to link <" .. theZone.name .. "> to", 30)
@ -2417,26 +2643,25 @@ function cfxZones.startMovingZones()
end end
end end
--
-- ===========
-- INIT MODULE
-- ===========
--
function cfxZones.initZoneVerbosity() function cfxZones.initZoneVerbosity()
for aName,aZone in pairs(cfxZones.zones) do for aName,aZone in pairs(cfxZones.zones) do
-- support for zone-local verbose flag -- support for zone-local verbose flag
aZone.verbose = cfxZones.getBoolFromZoneProperty(aZone, "verbose", false) aZone.verbose = cfxZones.getBoolFromZoneProperty(aZone, "verbose", false)
end end
end end
--
-- init
--
function cfxZones.init() function cfxZones.init()
-- read all zones into my own db -- read all zones into my own db
cfxZones.readFromDCS(true) -- true: erase old cfxZones.readFromDCS(true) -- true: erase old
-- now, pre-read zone owner for all zones -- pre-read zone owner for all zones
-- note, all zones with this property are by definition owned zones.
-- and hence will be read anyway. this will merely ensure that the
-- ownership is established right away
-- unless owned zones module is missing, in which case
-- ownership is still established
local pZones = cfxZones.zonesWithProperty("owner") local pZones = cfxZones.zonesWithProperty("owner")
for n, aZone in pairs(pZones) do for n, aZone in pairs(pZones) do
aZone.owner = cfxZones.getCoalitionFromZoneProperty(aZone, "owner", 0) aZone.owner = cfxZones.getCoalitionFromZoneProperty(aZone, "owner", 0)

View File

@ -1,5 +1,5 @@
cloneZones = {} cloneZones = {}
cloneZones.version = "1.7.0" cloneZones.version = "1.7.1"
cloneZones.verbose = false cloneZones.verbose = false
cloneZones.requiredLibs = { cloneZones.requiredLibs = {
"dcsCommon", -- always "dcsCommon", -- always
@ -91,6 +91,8 @@ cloneZones.respawnOnGroupID = true
- <uid>, <lcl>, <i>, <g> wildcards - <uid>, <lcl>, <i>, <g> wildcards
- identical=true overrides nameScheme - identical=true overrides nameScheme
- masterOwner "*" convenience shortcut - masterOwner "*" convenience shortcut
1.7.1 - useDelicates handOff for delicates
- forcedRespawn passes zone instead of verbose
--]]-- --]]--
@ -337,6 +339,15 @@ function cloneZones.createClonerWithZone(theZone) -- has "Cloner"
--trigger.action.outText("trackwith: " .. theZone.trackWith, 30) --trigger.action.outText("trackwith: " .. theZone.trackWith, 30)
end end
-- interface to delicates
if cfxZones.hasProperty(theZone, "useDelicates") then
theZone.delicateName = dcsCommon.trim(cfxZones.getStringFromZoneProperty(theZone, "useDelicates", "<none>"))
if theZone.delicateName == "*" then theZone.delicateName = theZone.name end
if theZone.verbose then
trigger.action.outText("+++clnZ: cloner <" .. theZone.name .."> hands off delicates to <" .. theZone.delicateName .. ">", 30)
end
end
-- randomized locations on spawn -- randomized locations on spawn
theZone.rndLoc = cfxZones.getBoolFromZoneProperty(theZone, "randomizedLoc", false) theZone.rndLoc = cfxZones.getBoolFromZoneProperty(theZone, "randomizedLoc", false)
if cfxZones.hasProperty(theZone, "rndLoc") then if cfxZones.hasProperty(theZone, "rndLoc") then
@ -998,11 +1009,14 @@ function cloneZones.validateSpawnGroupData(theData, theZone, groupNames, unitNam
end end
end end
-- forcedRespan respawns a group when the previous spawn of a
-- group did not match the ID that it was supposed to match
function cloneZones.forcedRespawn(args) function cloneZones.forcedRespawn(args)
local theData = args[1] local theData = args[1]
local spawnedGroups = args[2] local spawnedGroups = args[2]
local pos = args[3] local pos = args[3]
local verbose = args[4] local theZone = args[4]
local verbose = theZone.verbose
local rawData = dcsCommon.clone(theData) local rawData = dcsCommon.clone(theData)
if verbose then if verbose then
trigger.action.outText("clnZ: enter forced respawn of <" .. theData.name .. "> to meet ID " .. theData.CZTargetID .. " (currently set for <" .. theData.groupId .. ">)", 30) trigger.action.outText("clnZ: enter forced respawn of <" .. theData.name .. "> to meet ID " .. theData.CZTargetID .. " (currently set for <" .. theData.groupId .. ">)", 30)
@ -1021,6 +1035,21 @@ function cloneZones.forcedRespawn(args)
trigger.action.outText("will replace table entry at <" .. pos .. "> with new group", 30) trigger.action.outText("will replace table entry at <" .. pos .. "> with new group", 30)
end end
spawnedGroups[pos] = theGroup spawnedGroups[pos] = theGroup
-- since we are now successful, check if we need to apply
-- delicate status
if theZone.delicateName and delicates then
-- pass this object to the delicate zone mentioned
local theDeli = delicates.getDelicatesByName(theZone.delicateName)
if theDeli then
delicates.addGroupToInventoryForZone(theDeli, theGroup)
else
trigger.action.outText("+++clnZ: spawner <" .. theZone.name .. "> can't find delicates zone <" .. theZone.delicateName .. ">", 30)
end
elseif theZone.delicateName then
trigger.action.outText("+++clnZ: WARNING - cloner <> requires 'Delicates' module.", 30)
end
else else
-- we need to try again in one second -- we need to try again in one second
if verbose then if verbose then
@ -1200,6 +1229,7 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone)
-- now spawn all raw data -- now spawn all raw data
local groupCollector = {} -- to detect cross-group conflicts local groupCollector = {} -- to detect cross-group conflicts
local unitCollector = {} -- to detect cross-group conflicts local unitCollector = {} -- to detect cross-group conflicts
local theGroup = nil -- init to empty, on this level
for idx, rawData in pairs (dataToSpawn) do for idx, rawData in pairs (dataToSpawn) do
-- now spawn and save to clones -- now spawn and save to clones
-- first norm and clone data for later save -- first norm and clone data for later save
@ -1225,7 +1255,7 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone)
end end
-- SPAWN NOW!!!! -- SPAWN NOW!!!!
local theGroup = coalition.addGroup(rawData.CZctry, rawData.CZtheCat, rawData) theGroup = coalition.addGroup(rawData.CZctry, rawData.CZtheCat, rawData)
table.insert(spawnedGroups, theGroup) table.insert(spawnedGroups, theGroup)
-- update groupXlate table from spawned group -- update groupXlate table from spawned group
@ -1261,28 +1291,49 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone)
end end
end end
-- check if our assigned ID matches the handed out by -- check if our assigned ID matches the one handed out by
-- DCS. Mismatches can happen, and are only noted -- DCS. Mismatches can happen, and are only noted
if newGroupID == rawData.CZTargetID then if newGroupID == rawData.CZTargetID then
-- we are good -- we are good, all processing correct
-- add to delicates if set
if spawnZone.delicateName and delicates then
-- pass this object to the delicate zone mentioned
local theDeli = delicates.getDelicatesByName(spawnZone.delicateName)
if theDeli then
delicates.addGroupToInventoryForZone(theDeli, theGroup)
else
trigger.action.outText("+++clnZ: spawner <" .. spawnZone.name .. "> can't find delicates zone <" .. spawnZone.delicateName .. ">", 30)
end
end
else else
if cloneZones.verbose or spawnZone.verbose then if cloneZones.verbose or spawnZone.verbose then
trigger.action.outText("clnZ: Note: GROUP ID spawn changed for <" .. rawData.name .. ">: target ID " .. rawData.CZTargetID .. " (target) returns " .. newGroupID .. " (actual) in <" .. spawnZone.name .. ">", 30) trigger.action.outText("clnZ: Note: GROUP ID spawn changed for <" .. rawData.name .. ">: target ID " .. rawData.CZTargetID .. " (target) returns " .. newGroupID .. " (actual) in <" .. spawnZone.name .. ">", 30)
--trigger.action.outText("Note: theData.groupId is <" .. theData.groupId .. ">", 30)
--if spawnZone.identical then
-- trigger.action.outText("(Identical = true detected for this zone)", 30)
--end
end end
if cloneZones.respawnOnGroupID then if cloneZones.respawnOnGroupID then
-- remove last entry in table, will be added later -- remember pos in table, will be changed after
-- respawn
local pos = #spawnedGroups local pos = #spawnedGroups
timer.scheduleFunction(cloneZones.forcedRespawn, {theData, spawnedGroups, pos, spawnZone.verbose}, timer.getTime() + 2) -- initial gap: 2 seconds for DCS to sort itself out timer.scheduleFunction(cloneZones.forcedRespawn, {theData, spawnedGroups, pos, spawnZone}, timer.getTime() + 2) -- initial gap: 2 seconds for DCS to sort itself out
-- note that this can in extreme cases result in
-- unitID mismatches, but his is extremely unlikely
else else
-- we note it in the spawn data for the group so -- we note it in the spawn data for the group so
-- persistence works fine -- persistence works fine
theData.groupId = newGroupID theData.groupId = newGroupID
-- since we keep these, we make them brittle if required
if spawnZone.delicateName and delicates then
-- pass this object to the delicate zone mentioned
local theDeli = delicates.getDelicatesByName(spawnZone.delicateName)
if theDeli then
delicates.addGroupToInventoryForZone(theDeli, theGroup)
else
trigger.action.outText("+++clnZ: spawner <" .. spawnZone.name .. "> can't find delicates zone <" .. spawnZone.delicateName .. ">", 30)
end
end
end end
end end
@ -1387,8 +1438,20 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone)
cloneZones.invokeCallbacks(theZone, "did spawn static", theStatic) cloneZones.invokeCallbacks(theZone, "did spawn static", theStatic)
if cloneZones.verbose or spawnZone.verbose then if cloneZones.verbose or spawnZone.verbose then
trigger.action.outText("Static spawn: spawned " .. aStaticName, 30) trigger.action.outText("+++clnZ: new Static clone " .. aStaticName, 30)
end end
-- processing for delicates
if spawnZone.delicateName and delicates then
-- pass this object to the delicate zone mentioned
local theDeli = delicates.getDelicatesByName(spawnZone.delicateName)
if theDeli then
delicates.addStaticObjectToInventoryForZone(theDeli, theStatic)
else
trigger.action.outText("+++cnlZ: cloner <" .. aZone.name .. "> can't find delicates <" .. spawnZone.delicateName .. ">", 30)
end
end
-- processing for cargoManager -- processing for cargoManager
if isCargo then if isCargo then
if cfxCargoManager then if cfxCargoManager then

View File

@ -1,5 +1,5 @@
dcsCommon = {} dcsCommon = {}
dcsCommon.version = "2.8.0" dcsCommon.version = "2.8.1"
--[[-- VERSION HISTORY --[[-- VERSION HISTORY
2.2.6 - compassPositionOfARelativeToB 2.2.6 - compassPositionOfARelativeToB
- clockPositionOfARelativeToB - clockPositionOfARelativeToB
@ -123,7 +123,10 @@ dcsCommon.version = "2.8.0"
- new getGroupNameByID - new getGroupNameByID
- bool2YesNo alsco can return NIL - bool2YesNo alsco can return NIL
- new getUnitStartPosByID - new getUnitStartPosByID
2.8.1 - arrayContainsString: type checking for theArray and warning
- processStringWildcards()
- new wildArrayContainsString()
- fix for stringStartsWith oddity with aircraft types
--]]-- --]]--
-- dcsCommon is a library of common lua functions -- dcsCommon is a library of common lua functions
@ -1995,9 +1998,56 @@ end
-- --
-- --
-- as arrayContainsString, except it includes wildcard matches if EITHER
-- ends on "*"
function dcsCommon.wildArrayContainsString(theArray, theString, caseSensitive)
if not theArray then return false end
if not theString then return false end
if not caseSensitive then caseSensitive = false end
if type(theArray) ~= "table" then
trigger.action.outText("***arrayContainsString: theArray is not type table but <" .. type(theArray) .. ">", 30)
end
if not caseSensitive then theString = string.upper(theString) end
--trigger.action.outText("wildACS: theString = <" .. theString .. ">, theArray contains <" .. #theArray .. "> elements", 30)
local wildIn = dcsCommon.stringEndsWith(theString, "*")
if wildIn then dcsCommon.removeEnding(thestring, "*") end
for i = 1, #theArray do
local theElement = theArray[i]
if caseSensitive then theElement = string.upper(theElement) end
local wildEle = dcsCommon.stringEndsWith(theElement, "*")
if wildEle then theElement = dcsCommon.removeEnding(theElement, "*") end
--trigger.action.outText("matching s=<" .. theString .. "> with e=<" .. theElement .. ">", 30)
if wildEle and wildIn then
-- both end on wildcards, partial match for both
if dcsCommon.stringStartsWith(theElement. theString) then return true end
if dcsCommon.stringStartsWith(theString, theElement) then return true end
--trigger.action.outText("match e* with s* failed.", 30)
elseif wildEle then
-- Element is a wildcard, partial match
if dcsCommon.stringStartsWith(theString, theElement) then return true end
--trigger.action.outText("match e* with s failed.", 30)
elseif wildIn then
-- theString is a wildcard. partial match
if dcsCommon.stringStartsWith(theElement. theString) then return true end
--trigger.action.outText("match e with s* failed.", 30)
else
-- standard: no wildcards, full match
if theArray[i] == theString then return true end
--trigger.action.outText("match e with s (straight) failed.", 30)
end
end
return false
end
function dcsCommon.arrayContainsString(theArray, theString) function dcsCommon.arrayContainsString(theArray, theString)
if not theArray then return false end if not theArray then return false end
if not theString then return false end if not theString then return false end
if type(theArray) ~= "table" then
trigger.action.outText("***arrayContainsString: theArray is not type table but <" .. type(theArray) .. ">", 30)
end
for i = 1, #theArray do for i = 1, #theArray do
if theArray[i] == theString then return true end if theArray[i] == theString then return true end
end end
@ -2117,7 +2167,35 @@ end
function dcsCommon.stringStartsWith(theString, thePrefix) function dcsCommon.stringStartsWith(theString, thePrefix)
if not theString then return false end if not theString then return false end
return theString:find(thePrefix) == 1 if not thePrefix then return false end
-- new code because old 'string.find' had some really
-- strange results with aircraft types. Prefix "A-10" did not
-- match string "A-10A" etc.
local pl = string.len(thePrefix)
if pl > string.len(theString) then return false end
if pl < 1 then return false end
for i=1, pl do
local left = string.sub(theString, i, i)
local right = string.sub(thePrefix, i, i)
if left ~= right then
return false
end
end
return true
--[[-- trigger.action.outText("---- OK???", 30)
-- strange stuff happening with some strings, let's investigate
local res = string.find(theString, thePrefix) == 1
if res then
trigger.action.outText("startswith: <" .. theString .. "> pre <" .. thePrefix .. "> --> YES", 30)
else
trigger.action.outText("startswith: <" .. theString .. "> nojoy pre <" .. thePrefix .. ">", 30)
end
return res
--]]--
end end
function dcsCommon.removePrefix(theString, thePrefix) function dcsCommon.removePrefix(theString, thePrefix)
@ -3008,6 +3086,23 @@ function dcsCommon.LSR(a, num)
return a return a
end end
--
-- string windcards
--
function dcsCommon.processStringWildcards(inMsg)
-- Replace STATIC bits of message like CR and zone name
if not inMsg then return "<nil inMsg>" end
local formerType = type(inMsg)
if formerType ~= "string" then inMsg = tostring(inMsg) end
if not inMsg then inMsg = "<inMsg is incompatible type " .. formerType .. ">" end
local outMsg = ""
-- replace line feeds
outMsg = inMsg:gsub("<n>", "\n")
return outMsg
end
-- --
-- SEMAPHORES -- SEMAPHORES
-- --

View File

@ -1,5 +1,5 @@
delicates = {} delicates = {}
delicates.version = "1.1.0" delicates.version = "1.1.1"
delicates.verbose = false delicates.verbose = false
delicates.ups = 1 delicates.ups = 1
delicates.requiredLibs = { delicates.requiredLibs = {
@ -16,7 +16,8 @@ delicates.inventory = {}
- addStaticObjectInventoryForZone - addStaticObjectInventoryForZone
- blowAll? - blowAll?
- safetyMargin - safety margin. defaults to 10% - safetyMargin - safety margin. defaults to 10%
1.1.1 - addGroupToInventoryForZone
- verbose for zone will show update event from useDelicates
--]]-- --]]--
function delicates.adddDelicates(theZone) function delicates.adddDelicates(theZone)
@ -73,9 +74,10 @@ function delicates.makeZoneInventory(theZone)
for idy, anObject in pairs(collector) do for idy, anObject in pairs(collector) do
local oName = anObject:getName() local oName = anObject:getName()
if type(oName) == 'number' then oName = tostring(oName) end if type(oName) == 'number' then oName = tostring(oName) end
local oLife = anObject:getLife() - anObject:getLife() * theZone.safetyMargin local mLife = anObject:getLife()
local oLife = mLife - mLife * theZone.safetyMargin
if theZone.verbose or delicates.verbose then if theZone.verbose or delicates.verbose then
trigger.action.outText("+++deli: cat=".. aCat .. ":<" .. oName .. "> Life=" .. oLife, 30) trigger.action.outText("+++deli: cat=".. aCat .. ":<" .. oName .. "> explodes when under " .. oLife .. " of max " .. mLife, 30)
end end
local uP = anObject:getPoint() local uP = anObject:getPoint()
if cfxZones.isPointInsideZone(uP, theZone) then if cfxZones.isPointInsideZone(uP, theZone) then
@ -83,6 +85,7 @@ function delicates.makeZoneInventory(theZone)
local desc = {} local desc = {}
desc.cat = aCat desc.cat = aCat
desc.oLife = oLife desc.oLife = oLife
desc.mLife = mLife
desc.theZone = theZone desc.theZone = theZone
desc.oName = oName desc.oName = oName
delicates.inventory[oName] = desc delicates.inventory[oName] = desc
@ -102,17 +105,34 @@ function delicates.addStaticObjectToInventoryForZone(theZone, theStatic)
local desc = {} local desc = {}
desc.cat = theStatic:getCategory() desc.cat = theStatic:getCategory()
desc.mLife = theStatic:getLife()
desc.oLife = theStatic:getLife() - theStatic:getLife() * theZone.safetyMargin desc.oLife = theStatic:getLife() - theStatic:getLife() * theZone.safetyMargin
if desc.oLife < 0 then desc.oLife = 0 end if desc.oLife < 0 then desc.oLife = 0 end
desc.theZone = theZone desc.theZone = theZone
desc.oName = theStatic:getName() desc.oName = theStatic:getName()
if theZone.verbose and delicates[desc.oName] then
trigger.action.outText("+++deli: updating existing delicate <" .. desc.oName .. "> with data from zone <" .. theZone.name .. ">", 30)
end
delicates.inventory[desc.oName] = desc delicates.inventory[desc.oName] = desc
if theZone.verbose or delicates.verbose then 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) trigger.action.outText("+++deli: added <" .. desc.oName .. "> to <" .. theZone.name .. "> blows below life = <" .. desc.oLife .. "> of <" .. desc.mLife .. "> = safety margin " .. theZone.safetyMargin * 100 .. "%", 30)
end end
end end
function delicates.addGroupToInventoryForZone(theZone, theGroup)
-- trigger.action.outText("enter addGroupToInventoryForZone", 30)
if not theZone then return end
if not theGroup then return end
-- trigger.action.outText("before itering addGroupToInventoryForZone", 30)
local allUnits = theGroup:getUnits() -- warning: we assume all alive
for idx, aUnit in pairs (allUnits) do
-- we use 'addStatic' as it also supports units
delicates.addStaticObjectToInventoryForZone(theZone, aUnit)
end
end
function delicates.createDelicatesWithZone(theZone) function delicates.createDelicatesWithZone(theZone)
theZone.power = cfxZones.getNumberFromZoneProperty(theZone, "power", 10) theZone.power = cfxZones.getNumberFromZoneProperty(theZone, "power", 10)
@ -221,14 +241,14 @@ function delicates:onEvent(theEvent)
local cLife = theObj:getLife() local cLife = theObj:getLife()
if cLife < desc.oLife then if cLife < desc.oLife then
if desc.theZone.verbose or delicates.verbose then if desc.theZone.verbose or delicates.verbose then
trigger.action.outText("+++deli: BRITTLE TRIGGER: life <" .. cLife .. "> below safety margin <" .. oDesc.oLife .. ">", 30) trigger.action.outText("+++deli: BRITTLE TRIGGER: life <" .. cLife .. "> below safety margin <" .. desc.oLife .. ">", 30)
end end
delicates.blowUpObject(desc) delicates.blowUpObject(desc)
-- remove it from further searches -- remove it from further searches
delicates.inventory[oName] = nil delicates.inventory[oName] = nil
else else
if desc.theZone.verbose or delicates.verbose then if desc.theZone.verbose or delicates.verbose then
trigger.action.outText("+++deli: CLOSE CALL, but life <" .. cLife .. "> within safety margin <" .. oDesc.oLife .. ">", 30) trigger.action.outText("+++deli: CLOSE CALL, but life <" .. cLife .. "> within safety margin <" .. desc.oLife .. ">", 30)
end end
end end
end end
@ -287,6 +307,9 @@ function delicates.update()
if theObj then if theObj then
local cLife = theObj:getLife() local cLife = theObj:getLife()
if cLife < oDesc.mLife and cLife >= oDesc.oLife and oDesc.theZone.verbose then
trigger.action.outText("+++Deli: <" .. oName .. "> was hit, <" .. cLife .. "> still above trigger <" .. oDesc.oLife .. ">", 30)
end
if cLife >= oDesc.oLife then if cLife >= oDesc.oLife then
-- all well, transfer to next iter -- all well, transfer to next iter
newInventory[oName] = oDesc newInventory[oName] = oDesc

View File

@ -1,5 +1,5 @@
messenger = {} messenger = {}
messenger.version = "2.1.0" messenger.version = "2.2.0"
messenger.verbose = false messenger.verbose = false
messenger.requiredLibs = { messenger.requiredLibs = {
"dcsCommon", -- always "dcsCommon", -- always
@ -61,6 +61,9 @@ messenger.messengers = {}
- fix to messageMute - fix to messageMute
- <type: unit> - <type: unit>
2.1.1 - cosmetic: only output text if len>0 and not cls 2.1.1 - cosmetic: only output text if len>0 and not cls
2.2.0 - <player: unit>
- made dynamic string gen more portable in prep for move to cfxZones
- refactoring wildcard processing: moved to cfxZones
--]]-- --]]--
@ -79,268 +82,12 @@ function messenger.getMessengerByName(aName)
return nil return nil
end end
--
-- read attributes
--
function messenger.preProcMessage(inMsg, theZone)
-- Replace STATIC bits of message like CR and zone name
if not inMsg then return "<nil inMsg>" end
local formerType = type(inMsg)
if formerType ~= "string" then inMsg = tostring(inMsg) end
if not inMsg then inMsg = "<inMsg is incompatible type " .. formerType .. ">" end
local outMsg = ""
-- replace line feeds
outMsg = inMsg:gsub("<n>", "\n")
if theZone then
outMsg = outMsg:gsub("<z>", theZone.name)
end
return outMsg
end
-- Old-school processing to replace wildcards
-- repalce <t> with current time
-- replace <lat> with zone's current lonlat
-- replace <mgrs> with zone's current mgrs
function messenger.dynamicProcessClassic(inMsg, theZone)
if not inMsg then return "<nil inMsg>" end
-- replace <t> with current mission time HMS
local absSecs = timer.getAbsTime()-- + env.mission.start_time
while absSecs > 86400 do
absSecs = absSecs - 86400 -- subtract out all days
end
local timeString = dcsCommon.processHMS(theZone.msgTimeFormat, absSecs)
local outMsg = inMsg:gsub("<t>", timeString)
-- replace <lat> with lat of zone point and <lon> with lon of zone point
-- and <mgrs> with mgrs coords of zone point
local currPoint = cfxZones.getPoint(theZone)
local lat, lon = coord.LOtoLL(currPoint)
lat, lon = dcsCommon.latLon2Text(lat, lon)
local alt = land.getHeight({x = currPoint.x, y = currPoint.z})
if theZone.imperialUnits then
alt = math.floor(alt * 3.28084) -- feet
else
alt = math.floor(alt) -- meters
end
outMsg = outMsg:gsub("<lat>", lat)
outMsg = outMsg:gsub("<lon>", lon)
outMsg = outMsg:gsub("<ele>", alt)
local grid = coord.LLtoMGRS(coord.LOtoLL(currPoint))
local mgrs = grid.UTMZone .. ' ' .. grid.MGRSDigraph .. ' ' .. grid.Easting .. ' ' .. grid.Northing
outMsg = outMsg:gsub("<mgrs>", mgrs)
return outMsg
end
-- --
-- new dynamic flag processing -- Dynamic Group and Dynamic Unit processing are
-- -- unique to messenger, and are not available via
function messenger.processDynamicValues(inMsg, theZone) -- cfxZones or dcsCommon
-- replace all occurences of <v: flagName> with their values --
local pattern = "<v:%s*[%s%w%*%d%.%-_]+>" -- no list allowed but blanks and * and . and - and _ --> we fail on the other specials to keep this simple
local outMsg = inMsg
repeat -- iterate all patterns one by one
local startLoc, endLoc = string.find(outMsg, pattern)
if startLoc then
local theValParam = string.sub(outMsg, startLoc, endLoc)
-- strip lead and trailer
local param = string.gsub(theValParam, "<v:%s*", "")
param = string.gsub(param, ">","")
-- param = dcsCommon.trim(param) -- trim is called anyway
-- access flag
local val = cfxZones.getFlagValue(param, theZone)
val = tostring(val)
if not val then val = "NULL" end
-- replace pattern in original with new val
outMsg = string.gsub(outMsg, pattern, val, 1) -- only one sub!
end
until not startLoc
-- now process rsp
pattern = "<rsp:%s*[%s%w%*%d%.%-_]+>" -- no list allowed but blanks and * and . and - and _ --> we fail on the other specials to keep this simple
if theZone.msgResponses and (#theZone.msgResponses > 0) then -- only if this zone has an array
--trigger.action.outText("enter response proccing", 30)
repeat -- iterate all patterns one by one
local startLoc, endLoc = string.find(outMsg, pattern)
if startLoc then
--trigger.action.outText("response: found an occurence", 30)
local theValParam = string.sub(outMsg, startLoc, endLoc)
-- strip lead and trailer
local param = string.gsub(theValParam, "<rsp:%s*", "")
param = string.gsub(param, ">","")
-- access flag
local val = cfxZones.getFlagValue(param, theZone)
if not val or (val < 1) then val = 1 end
if val > #theZone.msgResponses then val = #theZone.msgResponses end
val = theZone.msgResponses[val]
val = dcsCommon.trim(val)
-- replace pattern in original with new val
outMsg = string.gsub(outMsg, pattern, val, 1) -- only one sub!
end
until not startLoc
-- rnd response
local rndRsp = dcsCommon.pickRandom(theZone.msgResponses)
outMsg = outMsg:gsub ("<rrnd>", rndRsp)
end
return outMsg
end
function messenger.processDynamicTime(inMsg, theZone)
-- replace all occurences of <v: flagName> with their values
local pattern = "<t:%s*[%s%w%*%d%.%-_]+>" -- no list allowed but blanks and * and . and - and _ --> we fail on the other specials to keep this simple
local outMsg = inMsg
repeat -- iterate all patterns one by one
local startLoc, endLoc = string.find(outMsg, pattern)
if startLoc then
local theValParam = string.sub(outMsg, startLoc, endLoc)
-- strip lead and trailer
local param = string.gsub(theValParam, "<t:%s*", "")
param = string.gsub(param, ">","")
-- access flag
local val = cfxZones.getFlagValue(param, theZone)
-- use this to process as time value
--trigger.action.outText("time: accessing <" .. param .. "> and received <" .. val .. ">", 30)
local timeString = dcsCommon.processHMS(theZone.msgTimeFormat, val)
if not timeString then timeString = "NULL" end
-- replace pattern in original with new val
outMsg = string.gsub(outMsg, pattern, timeString, 1) -- only one sub!
end
until not startLoc
return outMsg
end
function messenger.processDynamicLoc(inMsg, theZone)
-- replace all occurences of <lat/lon/ele/mgrs: flagName> with their values
-- agl = angels
-- vel = velocity (speed)
-- hdg = heading
-- rhdg = heading, response-mapped
local locales = {"lat", "lon", "ele", "mgrs", "lle", "latlon", "alt", "vel", "hdg", "rhdg", "type"}
local outMsg = inMsg
local uHead = 0
for idx, aLocale in pairs(locales) do
local pattern = "<" .. aLocale .. ":%s*[%s%w%*%d%.%-_]+>"
repeat -- iterate all patterns one by one
local startLoc, endLoc = string.find(outMsg, pattern)
if startLoc then
local theValParam = string.sub(outMsg, startLoc, endLoc)
-- strip lead and trailer
local param = string.gsub(theValParam, "<" .. aLocale .. ":%s*", "")
param = string.gsub(param, ">","")
-- find zone or unit
param = dcsCommon.trim(param)
local thePoint = nil
local tZone = cfxZones.getZoneByName(param)
local tUnit = Unit.getByName(param)
local spd = 0
local angels = 0
local theType = "<errType>"
if tZone then
theType = "Zone"
thePoint = cfxZones.getPoint(tZone)
if tZone.linkedUnit and Unit.isExist(tZone.linkedUnit) then
local lU = tZone.linkedUnit
local masterPoint = lU:getPoint()
thePoint.y = masterPoint.y
spd = dcsCommon.getUnitSpeed(lU)
spd = math.floor(spd * 3.6)
uHead = math.floor(dcsCommon.getUnitHeading(tUnit) * 57.2958) -- to degrees.
else
-- since zones always have elevation of 0,
-- now get the elevation from the map
thePoint.y = land.getHeight({x = thePoint.x, y = thePoint.z})
end
elseif tUnit then
if Unit.isExist(tUnit) then
theType = tUnit:getTypeName()
thePoint = tUnit:getPoint()
spd = dcsCommon.getUnitSpeed(tUnit)
-- convert m/s to km/h
spd = math.floor(spd * 3.6)
uHead = math.floor(dcsCommon.getUnitHeading(tUnit) * 57.2958) -- to degrees.
end
else
-- nothing to do, remove me.
end
local locString = theZone.errString
if thePoint then
-- now that we have a point, we can do locale-specific
-- processing. return result in locString
local lat, lon, alt = coord.LOtoLL(thePoint)
lat, lon = dcsCommon.latLon2Text(lat, lon)
angels = math.floor(thePoint.y)
if theZone.imperialUnits then
alt = math.floor(alt * 3.28084) -- feet
spd = math.floor(spd * 0.539957) -- km/h to knots
angels = math.floor(angels * 3.28084)
else
alt = math.floor(alt) -- meters
end
if angels > 1000 then
angels = math.floor(angels / 100) * 100
end
if aLocale == "lat" then locString = lat
elseif aLocale == "lon" then locString = lon
elseif aLocale == "ele" then locString = tostring(alt)
elseif aLocale == "lle" then locString = lat .. " " .. lon .. " ele " .. tostring(alt)
elseif aLocale == "latlon" then locString = lat .. " " .. lon
elseif aLocale == "alt" then locString = tostring(angels) -- don't confuse alt and angels, bad var naming here
elseif aLocale == "vel" then locString = tostring(spd)
elseif aLocale == "hdg" then locString = tostring(uHead)
elseif aLocale == "type" then locString = theType
elseif aLocale == "rhdg" and (theZone.msgResponses) then
local offset = messenger.rspMapper360(uHead, #theZone.msgResponses)
locString = dcsCommon.trim(theZone.msgResponses[offset])
else
-- we have mgrs
local grid = coord.LLtoMGRS(coord.LOtoLL(thePoint))
locString = grid.UTMZone .. ' ' .. grid.MGRSDigraph .. ' ' .. grid.Easting .. ' ' .. grid.Northing
end
end
-- replace pattern in original with new val
outMsg = string.gsub(outMsg, pattern, locString, 1) -- only one sub!
end -- if startloc
until not startLoc
end -- for all locales
return outMsg
end
function messenger.rspMapper360(directionInDegrees, numResponses)
-- maps responses like clock. Clock has 12 'responses' (12, 1, .., 11),
-- with the first (12) also mapping to the last half arc
-- this method dynamically 'winds' the responses around
-- a clock and returns the index of the message to display
if numResponses < 1 then numResponses = 1 end
directionInDegrees = math.floor(directionInDegrees)
while directionInDegrees < 0 do directionInDegrees = directionInDegrees + 360 end
while directionInDegrees >= 360 do directionInDegrees = directionInDegrees - 360 end
-- now we have 0..360
-- calculate arc per item
local arcPerItem = 360 / numResponses
local halfArc = arcPerItem / 2
-- we now map 0..360 to (0-halfArc..360-halfArc) by shifting
-- direction by half-arc and clipping back 0..360
-- and now we can directly derive the index of the response
directionInDegrees = directionInDegrees + halfArc
if directionInDegrees >= 360 then directionInDegrees = directionInDegrees - 360 end
local index = math.floor(directionInDegrees / arcPerItem) + 1 -- 1 .. numResponses
return index
end
function messenger.dynamicGroupProcessing(msg, theZone, theGroup) function messenger.dynamicGroupProcessing(msg, theZone, theGroup)
if not theGroup then return msg end if not theGroup then return msg end
@ -455,7 +202,7 @@ function messenger.dynamicUnitProcessing(inMsg, theZone, theUnit)
elseif aLocale == "hnd" then locString = dcsCommon.getGeneralDirection(direction) elseif aLocale == "hnd" then locString = dcsCommon.getGeneralDirection(direction)
elseif aLocale == "sde" then locString = dcsCommon.getNauticalDirection(direction) elseif aLocale == "sde" then locString = dcsCommon.getNauticalDirection(direction)
elseif aLocale == "rbea" and (theZone.msgResponses) then elseif aLocale == "rbea" and (theZone.msgResponses) then
local offset = messenger.rspMapper360(direction, #theZone.msgResponses) local offset = cfxZones.rspMapper360(direction, #theZone.msgResponses)
locString = dcsCommon.trim(theZone.msgResponses[offset]) locString = dcsCommon.trim(theZone.msgResponses[offset])
else locString = "<locale " .. aLocale .. " err: undefined params>" else locString = "<locale " .. aLocale .. " err: undefined params>"
end end
@ -469,27 +216,14 @@ function messenger.dynamicUnitProcessing(inMsg, theZone, theUnit)
end end
function messenger.dynamicFlagProcessing(inMsg, theZone) --
if not inMsg then return "No in message" end -- reat attributes
if not theZone then return "Nil zone" end --
-- process <v: xxx>
local msg = messenger.processDynamicValues(inMsg, theZone)
-- process <t: xxx>
msg = messenger.processDynamicTime(msg, theZone)
-- process lat / lon / ele / mgrs
msg = messenger.processDynamicLoc(msg, theZone)
return msg
end
function messenger.createMessengerWithZone(theZone) function messenger.createMessengerWithZone(theZone)
-- start val - a range -- start val - a range
local aMessage = cfxZones.getStringFromZoneProperty(theZone, "message", "") local aMessage = cfxZones.getStringFromZoneProperty(theZone, "message", "")
theZone.message = messenger.preProcMessage(aMessage, theZone) theZone.message = aMessage -- refactoring: messenger.preProcMessage(aMessage, theZone) removed
theZone.spaceBefore = cfxZones.getBoolFromZoneProperty(theZone, "spaceBefore", false) theZone.spaceBefore = cfxZones.getBoolFromZoneProperty(theZone, "spaceBefore", false)
theZone.spaceAfter = cfxZones.getBoolFromZoneProperty(theZone, "spaceAfter", false) theZone.spaceAfter = cfxZones.getBoolFromZoneProperty(theZone, "spaceAfter", false)
@ -584,6 +318,7 @@ function messenger.createMessengerWithZone(theZone)
-- flag whose value can be read: to be deprecated -- flag whose value can be read: to be deprecated
if cfxZones.hasProperty(theZone, "messageValue?") then if cfxZones.hasProperty(theZone, "messageValue?") then
theZone.messageValue = cfxZones.getStringFromZoneProperty(theZone, "messageValue?", "<none>") theZone.messageValue = cfxZones.getStringFromZoneProperty(theZone, "messageValue?", "<none>")
trigger.action.outText("+++Msg: Warning - zone <" .. theZone.name .. "> uses 'messageValue' attribute. Migrate to <v:<flag> now!")
end end
-- time format for new <t: flagname> -- time format for new <t: flagname>
@ -606,7 +341,7 @@ function messenger.createMessengerWithZone(theZone)
end end
if messenger.verbose or theZone.verbose then if messenger.verbose or theZone.verbose then
trigger.action.outText("+++Msg: new zone <".. theZone.name .."> will say <".. theZone.message .. ">", 30) trigger.action.outText("+++Msg: new messenger in <".. theZone.name .."> will say '".. theZone.message .. "' (raw)", 30)
end end
end end
@ -620,29 +355,23 @@ function messenger.getMessage(theZone)
if not zName then zName = "<strange!>" end if not zName then zName = "<strange!>" end
local zVal = "<n/a>" local zVal = "<n/a>"
if theZone.messageValue then if theZone.messageValue then
trigger.action.outText("+++Msg: Warning - zone <" .. theZone.name .. "> uses 'messageValue' attribute. Migrate to <v:<flag> now!")
zVal = cfxZones.getFlagValue(theZone.messageValue, theZone) zVal = cfxZones.getFlagValue(theZone.messageValue, theZone)
zVal = tostring(zVal) zVal = tostring(zVal)
if not zVal then zVal = "<err>" end if not zVal then zVal = "<err>" end
end end
-- replace *zone and *value wildcards
--msg = string.gsub(msg, "*name", zName)-- deprecated
--msg = string.gsub(msg, "*value", zVal) -- deprecated
-- old-school <v> to provide value from messageValue -- old-school <v> to provide value from messageValue
-- to be removed mid-2023
msg = string.gsub(msg, "<v>", zVal) msg = string.gsub(msg, "<v>", zVal)
local z = tonumber(zVal) local z = tonumber(zVal)
if not z then z = 0 end if not z then z = 0 end
msg = dcsCommon.processHMS(msg, z) msg = dcsCommon.processHMS(msg, z)
-- process <t> [classic format], <latlon> and <mrgs> -- remainder hand-off to cfxZones (refactoring of messenger code
msg = messenger.dynamicProcessClassic(msg, theZone) msg = cfxZones.processStringWildcards(msg, theZone, theZone.msgTimeFormat, theZone.imperialUnits, theZone.msgResponses)
-- now add new processing of <x: flagname> access
msg = messenger.dynamicFlagProcessing(msg, theZone)
-- now add new processinf of <lat: flagname>
-- also handles <lon:x>, <ele:x>, <mgrs:x>
return msg return msg
end end
@ -789,7 +518,3 @@ if not messenger.start() then
messenger = nil messenger = nil
end end
--[[--
--]]--

448
modules/valet.lua Normal file
View File

@ -0,0 +1,448 @@
valet = {}
valet.version = "1.0.0"
valet.verbose = false
valet.requiredLibs = {
"dcsCommon", -- always
"cfxZones", -- Zones, of course
}
valet.valets = {}
--[[--
Version History
1.0.0 - initial version
--]]--
function valet.addValet(theZone)
table.insert(valet.valets, theZone)
end
function valet.getValetByName(aName)
for idx, aZone in pairs(valet.valets) do
if aName == aZone.name then return aZone end
end
if valet.verbose then
trigger.action.outText("+++valet: no valet with name <" .. aName ..">", 30)
end
return nil
end
--
-- read attributes
--
function valet.createValetWithZone(theZone)
-- start val - a range
theZone.inSoundFile = cfxZones.getStringFromZoneProperty(theZone, "inSoundFile", "<none>")
if cfxZones.hasProperty(theZone, "firstInSoundFile") then
theZone.firstInSoundFile = cfxZones.getStringFromZoneProperty(theZone, "firstInSoundFile", "<none>")
end
theZone.outSoundFile = cfxZones.getStringFromZoneProperty(theZone, "outSoundFile", "<none>")
-- greeting/first greeting, handle if "" = no text out
if cfxZones.hasProperty(theZone, "firstGreeting") then
theZone.firstGreeting = cfxZones.getStringFromZoneProperty(theZone, "firstGreeting", "")
end
theZone.greeting = cfxZones.getStringFromZoneProperty(theZone, "greeting", "")
theZone.greetSpawns = cfxZones.getBoolFromZoneProperty(theZone, "greetSpawns", false)
-- goodbye
theZone.goodbye = cfxZones.getStringFromZoneProperty(theZone, "goodbye", "")
theZone.duration = cfxZones.getNumberFromZoneProperty(theZone, "duration", 30) -- warning: crossover from messenger. Intentional
-- valetMethod for outputs
theZone.valetMethod = cfxZones.getStringFromZoneProperty(theZone, "method", "inc")
if cfxZones.hasProperty(theZone, "valetMethod") then
theZone.valetMethod = cfxZones.getStringFromZoneProperty(theZone, "valetMethod", "inc")
end
-- outputs
if cfxZones.hasProperty(theZone, "hi!") then
theZone.valetHi = cfxZones.getStringFromZoneProperty(theZone, "hi!", "*<none>")
end
if cfxZones.hasProperty(theZone, "bye!") then
theZone.valetBye = cfxZones.getStringFromZoneProperty(theZone, "bye!", "*<none>")
end
-- reveiver: coalition, group, unit
if cfxZones.hasProperty(theZone, "coalition") then
theZone.valetCoalition = cfxZones.getCoalitionFromZoneProperty(theZone, "coalition", 0)
elseif cfxZones.hasProperty(theZone, "valetCoalition") then
theZone.valetCoalition = cfxZones.getCoalitionFromZoneProperty(theZone, "valetCoalition", 0)
end
if cfxZones.hasProperty(theZone, "types") then
local types = cfxZones.getStringFromZoneProperty(theZone, "types", "")
theZone.valetTypes = dcsCommon.string2Array(types, ",")
elseif cfxZones.hasProperty(theZone, "valetTypes") then
local types = cfxZones.getStringFromZoneProperty(theZone, "valetTypes", "")
theZone.valetTypes = dcsCommon.string2Array(groups, ",")
end
if cfxZones.hasProperty(theZone, "groups") then
local groups = cfxZones.getStringFromZoneProperty(theZone, "groups", "<none>")
theZone.valetGroups = dcsCommon.string2Array(groups, ",")
elseif cfxZones.hasProperty(theZone, "valetGroups") then
local groups = cfxZones.getStringFromZoneProperty(theZone, "valetGroups", "<none>")
theZone.valetGroups = dcsCommon.string2Array(groups, ",")
end
if cfxZones.hasProperty(theZone, "units") then
local units = cfxZones.getStringFromZoneProperty(theZone, "units", "<none>")
theZone.valetUnits = dcsCommon.string2Array(units, ",")
elseif cfxZones.hasProperty(theZone, "valetUnits") then
local units = cfxZones.getStringFromZoneProperty(theZone, "valetUnits", "<none>")
theZone.valetUnits = dcsCommon.string2Array(units, ",")
end
if (theZone.valetGroups and theZone.valetUnits) or
(theZone.valetGroups and theZone.valetCoalition) or
(theZone.valetUnits and theZone.valetCoalition)
then
trigger.action.outText("+++valet: WARNING - valet in <" .. theZone.name .. "> may have coalition, group or unit. Use only one.", 30)
end
theZone.imperialUnits = cfxZones.getBoolFromZoneProperty(theZone, "imperial", false)
if cfxZones.hasProperty(theZone, "imperialUnits") then
theZone.imperialUnits = cfxZones.getBoolFromZoneProperty(theZone, "imperialUnits", false)
end
theZone.valetTimeFormat = cfxZones.getStringFromZoneProperty(theZone, "timeFormat", "<:h>:<:m>:<:s>")
-- collect all players currently in-zone.
-- since we start the game, there is no player in-game, can skip
theZone.playersInZone = {}
end
--
-- Update
--
function valet.preprocessWildcards(inMsg, aUnit, theDesc)
local theMsg = inMsg
local pName = "Unknown"
if aUnit.getPlayerName then
pN = aUnit:getPlayerName()
if pN then pName = pN end
end
theMsg = theMsg:gsub("<player>", pName)
theMsg = theMsg:gsub("<unit>", aUnit:getName())
theMsg = theMsg:gsub("<type>", aUnit:getTypeName())
theMsg = theMsg:gsub("<group>", aUnit:getGroup():getName())
theMsg = theMsg:gsub("<in>", tostring(theDesc.greets + 1) )
theMsg = theMsg:gsub("<out>", tostring(theDesc.byes + 1))
return theMsg
end
function valet.greetPlayer(playerName, aPlayerUnit, theZone, theDesc)
--trigger.action.outText("valet.greetPlayer <" .. theZone.name .. "> enter", 30)
-- player has just entred zone
local msg = theZone.greeting
local dur = theZone.duration
local fileName = "l10n/DEFAULT/" .. theZone.inSoundFile
local ID = aPlayerUnit:getID()
-- see if this was the first time, and if so, if we have a special first message
if theDesc.greets < 1 then
if theZone.firstGreeting then
msg = theZone.firstGreeting
end
if theZone.firstInSoundFile then
fileName = "l10n/DEFAULT/" .. theZone.firstInSoundFile
end
end
if msg == "<none>" then msg = "" end
if not msg then msg = "" end
-- an empty string suppresses message/sound
if msg ~= "" then
if theZone.verbose then
trigger.action.outText("+++valet: <" .. theZone.name .. "> - 'greet' triggers for player <" .. playerName .. "> in <" .. aPlayerUnit:getName() .. ">", 30)
end
-- process and say meessage
msg = valet.preprocessWildcards(msg, aPlayerUnit, theDesc)
msg = cfxZones.processStringWildcards(msg, theZone, theZone.valetTimeFormat, theZone.imperialUnits) -- nil responses
-- now always output only to the player
trigger.action.outTextForUnit(ID, msg, dur)
end
-- always play, if no sound file found it will have no effect
trigger.action.outSoundForUnit(ID, fileName)
-- update desc
theDesc.currentlyIn = true
theDesc.greets = theDesc.greets + 1
-- bang output
if theZone.valetHi then
cfxZones.pollFlag(theZone.valetHi, theZone.valetMethod, theZone)
if theZone.verbose or valet.verbose then
trigger.action.outText("+++valet: banging output 'hi!' with <" .. theZone.valetMethod .. "> on <" .. theZone.valetHi .. "> for zone " .. theZone.name, 30)
end
end
end
function valet.sendOffPlayer(playerName, aPlayerUnit, theZone, theDesc)
-- player has left the area
local msg = theZone.goodbye or ""
local dur = theZone.duration
local fileName = "l10n/DEFAULT/" .. theZone.inSoundFile
local ID = aPlayerUnit:getID()
if msg == "<none>" then msg = "" end
-- an empty string suppresses message/sound
if msg ~= "" then
-- process and say meessage
msg = valet.preprocessWildcards(msg, aPlayerUnit, theDesc)
msg = cfxZones.processStringWildcards(msg, theZone, theZone.valetTimeFormat, theZone.imperialUnits) -- nil responses
trigger.action.outTextForUnit(ID, msg, dur)
end
-- always play sound
trigger.action.outSoundForUnit(ID, fileName)
-- update desc
theDesc.currentlyIn = false
theDesc.byes = theDesc.byes + 1
-- bang output
if theZone.valetBye then
cfxZones.pollFlag(theZone.valetBye, theZone.valetMethod, theZone)
if theZone.verbose or valet.verbose then
trigger.action.outText("+++valet: banging output 'bye!' with <" .. theZone.valetMethod .. "> on <" .. theZone.valetBye .. "> for zone " .. theZone.name, 30)
end
end
end
function valet.checkZoneAgainstPlayers(theZone, allPlayers)
-- check status of all players if they are inside or
-- outside the zone (done during update)
-- when a change happens, react to it
local p = cfxZones.getPoint(theZone)
p.y = 0 -- sanity first
local maxRad = theZone.maxRadius
-- set up hysteresis
local outside = maxRad * 1.2
for playerName, aPlayerUnit in pairs (allPlayers) do
local unitName = aPlayerUnit:getName()
local uP = aPlayerUnit:getPoint()
local uCoa = aPlayerUnit:getCoalition()
local uGroup = aPlayerUnit:getGroup()
local groupName = uGroup:getName()
--local cat = aPlayerUnit:getDesc().category -- note indirection!
local uType = aPlayerUnit:getTypeName()
if theZone.valetCoalition and theZone.valetCoalition ~= uCoa then
-- coalition mismatch -- no checks required
elseif theZone.valetGroups and not dcsCommon.wildArrayContainsString(theZone.valetGroups, groupName) then
-- group name mismatch, skip checks
elseif theZone.valetUnits and not dcsCommon.wildArrayContainsString(theZone.valetUnits, unitName) then
-- unit name mismatch, skip checks
elseif theZone.valetTypes and not dcsCommon.wildArrayContainsString(theZone.valetTypes, uType) then
-- types dont match
else
local theDesc = theZone.playersInZone[playerName] -- may be nil
uP.y = 0 -- mask out y
local dist = dcsCommon.dist(p, uP) -- get distance
if cfxZones.pointInZone(uP, theZone) then
-- the unit is inside the zone.
-- see if it was inside last time
-- if new player, create new record, start as outside
if not theDesc then
theDesc = {}
theDesc.currentlyIn = false
theDesc.greets = 0
theDesc.byes = 0
theDesc.unitName = unitName
theZone.playersInZone[playerName] = theDesc
else
if theDesc.unitName == unitName then
else
-- ha!!! player changed planes!
theDesc.currentlyIn = false
theDesc.greets = 0
theDesc.byes = 0
theDesc.unitName = unitName
end
end
if not theDesc.currentlyIn then
-- we detect a change. Need to greet
valet.greetPlayer(playerName, aPlayerUnit, theZone, theDesc)
end
elseif (dist > outside) and theDesc then
if theDesc.unitName == unitName then
else
-- ha!!! player changed planes!
theDesc.currentlyIn = false
theDesc.greets = 0
theDesc.byes = 0
theDesc.unitName = unitName
end
if theDesc.currentlyIn then
-- unit is definitely outside and was inside before
-- (there's a record in this zone's playersInZone
valet.sendOffPlayer(playerName, aPlayerUnit, theZone, theDesc)
else
-- was outside before
end
else
-- we are in the twilight zone (hysteresis). Do nothing.
end
end -- else do checks
end
end
function valet.update()
-- call me in a second to poll triggers
timer.scheduleFunction(valet.update, {}, timer.getTime() + 1)
-- collect all players
local allPlayers = {}
-- single-player first
local sp = world.getPlayer() -- returns unit
if sp then
local playerName = sp:getPlayerName()
if playerName then
allPlayers[playerName] = sp
end
end
-- now clients
local coalitions = {0, 1, 2}
for isx, aCoa in pairs (coalitions) do
local coaClients = coalition.getPlayers(aCoa)
for idy, aUnit in pairs (coaClients) do
if aUnit.getPlayerName and aUnit:getPlayerName() then
allPlayers[aUnit:getPlayerName()] = aUnit
end
end
end
for idx, theZone in pairs(valet.valets) do
valet.checkZoneAgainstPlayers(theZone, allPlayers)
end
end
--
-- OnEvent - detecting player enter unit
--
function valet.checkPlayerSpawn(playerName, theUnit)
-- see if player spawned in a valet zone
if not playerName then return end
if not theUnit then return end
local pos = theUnit:getPoint()
--trigger.action.outText("+++valet: spawn event", 30)
for idx, theZone in pairs(valet.valets) do
-- erase any old records
theZone.playersInZone[playerName] = nil
-- create new if in that valet zone
if cfxZones.pointInZone(pos, theZone) then
theDesc = {}
theDesc.currentlyIn = true -- suppress messages
if theZone.greetSpawns then
theDesc.currentlyIn = false
end
theDesc.greets = 0
theDesc.byes = 0
theDesc.unitName = theUnit:getName()
theZone.playersInZone[playerName] = theDesc
if theZone.verbose then
trigger.action.outText("+++valet: spawning player <" .. playerName .. "> / <" .. theUnit:getName() .. "> in valet <" .. theZone.name .. ">", 40)
end
end
end
end
function valet:onEvent(event)
if event.id == 20 then
if not event.initiator then return end
local theUnit = event.initiator
if not theUnit.getPlayerName then
trigger.action.outText("+++valet: non player event 20(?)", 30)
return
end
local pName = theUnit:getPlayerName()
if not pName then
trigger.action.outText("+++valet: nil player name on event 20 (!)", 30)
return
end
valet.checkPlayerSpawn(pName, theUnit)
end
end
--
-- Config & Start
--
function valet.readConfigZone()
local theZone = cfxZones.getZoneByName("valetConfig")
if not theZone then
if valet.verbose then
trigger.action.outText("+++msgr: NO config zone!", 30)
end
theZone = cfxZones.createSimpleZone("valetConfig")
end
valet.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false)
if valet.verbose then
trigger.action.outText("+++msgr: read config", 30)
end
end
function valet.start()
-- lib check
if not dcsCommon.libCheck then
trigger.action.outText("cfx valet requires dcsCommon", 30)
return false
end
if not dcsCommon.libCheck("cfx valet", valet.requiredLibs) then
return false
end
-- read config
valet.readConfigZone()
-- process valet Zones
-- old style
local attrZones = cfxZones.getZonesWithAttributeNamed("valet")
for k, aZone in pairs(attrZones) do
valet.createValetWithZone(aZone) -- process attributes
valet.addValet(aZone) -- add to list
end
-- register event handler
world.addEventHandler(valet)
-- start update
timer.scheduleFunction(valet.update, {}, timer.getTime() + 1)
trigger.action.outText("cfx valet v" .. valet.version .. " started.", 30)
return true
end
-- let's go!
if not valet.start() then
trigger.action.outText("cfx valet aborted: missing libraries", 30)
valet = nil
end