diff --git a/Doc/DML Documentation.pdf b/Doc/DML Documentation.pdf index 7535ea0..eec4894 100644 Binary files a/Doc/DML Documentation.pdf and b/Doc/DML Documentation.pdf differ diff --git a/Doc/DML Quick Reference.pdf b/Doc/DML Quick Reference.pdf index fe9ba03..afdb3db 100644 Binary files a/Doc/DML Quick Reference.pdf and b/Doc/DML Quick Reference.pdf differ diff --git a/modules/FARPZones.lua b/modules/FARPZones.lua index aa7af08..7d0b8e6 100644 --- a/modules/FARPZones.lua +++ b/modules/FARPZones.lua @@ -1,5 +1,5 @@ FARPZones = {} -FARPZones.version = "1.2.0" +FARPZones.version = "1.2.1" FARPZones.verbose = false --[[-- Version History @@ -13,6 +13,8 @@ FARPZones.verbose = false - verbose cleanup ("FZ: something happened") 1.2.0 - persistence - 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 = {} theFarp.zone = aZone theFarp.name = aZone.name - + theFarp.point = cfxZones.getPoint(aZone) -- failsafe -- find the FARPS that belong to this zone - local thePoint = aZone.point + local thePoint = cfxZones.getPoint(aZone) local mapFarps = dcsCommon.getAirbasesInRangeOfPoint( thePoint, aZone.radius, @@ -132,25 +134,35 @@ function FARPZones.createFARPFromZone(aZone) theFarp.myFarps = mapFarps theFarp.owner = 0 -- start with neutral aZone.owner = 0 - if #mapFarps == 0 then - trigger.action.outText("***Farp Zones: no FARP found for zone " .. aZone.name, 30) - else - --for idx, aFarp in pairs(mapFarps) do --- trigger.action.outText("Associated FARP " .. aFarp:getName() .. " with FARP Zone " .. aZone.name, 30) - --end + if #mapFarps == 0 then + if aZone.verbose or FARPZones.verbose then + trigger.action.outText("***Farp Zones: no FARP found inside zone " .. aZone.name .. ", associating closest FARP", 30) + end + local closest = dcsCommon.getClosestAirbaseTo(thePoint, 1) + 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.point = theFarp.mainFarp:getPoint() -- this is FARP, not zone!!! - theFarp.owner = theFarp.mainFarp:getCoalition() - aZone.owner = theFarp.owner - end + theFarp.mainFarp = theFarp.myFarps[1] + theFarp.point = theFarp.mainFarp:getPoint() -- this is FARP, not zone!!! + theFarp.owner = theFarp.mainFarp:getCoalition() + aZone.owner = theFarp.owner +-- end -- get r and phi for defenders local rPhi = cfxZones.getVectorFromZoneProperty( aZone, "rPhiHDef", 3) - --trigger.action.outText("*** DEF rPhi are " .. rPhi[1] .. " and " .. rPhi[2] .. " heading " .. rPhi[3], 30) + -- get r and phi for facilities -- create a new defenderzone for this local r = rPhi[1] diff --git a/modules/cfxObjectSpawnZones.lua b/modules/cfxObjectSpawnZones.lua index d0bf760..126b338 100644 --- a/modules/cfxObjectSpawnZones.lua +++ b/modules/cfxObjectSpawnZones.lua @@ -31,6 +31,7 @@ cfxObjectSpawnZones.verbose = false -- - useDelicates link to delicate when spawned -- - spawned single and multi-objects can be made delicates -- 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 @@ -123,7 +124,8 @@ function cfxObjectSpawnZones.createSpawner(inZone) -- see if the spawn can be made brittle/delicte if cfxZones.hasProperty(inZone, "useDelicates") then - theSpawner.delicateName = cfxZones.getStringFromZoneProperty(inZone, "useDelicates", "") + theSpawner.delicateName = dcsCommon.trim(cfxZones.getStringFromZoneProperty(inZone, "useDelicates", "")) + if theSpawner.delicateName == "*" then theSpawner.delicateName = inZone.name end end -- see if it is linked to a ship to set realtive orig headiong diff --git a/modules/cfxSpawnZones.lua b/modules/cfxSpawnZones.lua index e4f6832..514d792 100644 --- a/modules/cfxSpawnZones.lua +++ b/modules/cfxSpawnZones.lua @@ -64,6 +64,7 @@ cfxSpawnZones.spawnedGroups = {} -- 1.7.1 - improved verbosity -- - spelling check -- 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 -- @@ -152,7 +153,14 @@ function cfxSpawnZones.createSpawner(inZone) if cfxZones.hasProperty(inZone, "trackWith:") then inZone.trackWith = cfxZones.getStringFromZoneProperty(inZone, "trackWith:", "") end - + + -- interface to delicates + if cfxZones.hasProperty(inZone, "useDelicates") then + theSpawner.delicateName = dcsCommon.trim(cfxZones.getStringFromZoneProperty(inZone, "useDelicates", "")) + if theSpawner.delicateName == "*" then theSpawner.delicateName = inZone.name end + end + + -- connect with ME if a trigger flag is given if cfxZones.hasProperty(inZone, "f?") then theSpawner.triggerFlag = cfxZones.getStringFromZoneProperty(inZone, "f?", "none") @@ -411,6 +419,17 @@ function cfxSpawnZones.spawnWithSpawner(aSpawner) 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 -- note that we retrieve trackwith from ZONE, not spawner if theZone.trackWith then diff --git a/modules/cfxZones.lua b/modules/cfxZones.lua index 21d91ec..dbfc4a5 100644 --- a/modules/cfxZones.lua +++ b/modules/cfxZones.lua @@ -1,5 +1,5 @@ cfxZones = {} -cfxZones.version = "3.0.0" +cfxZones.version = "3.0.2" -- cf/x zone management module -- reads dcs zones and makes them accessible and mutable @@ -97,7 +97,6 @@ cfxZones.version = "3.0.0" - isPointInsideZone() returns delta as well - 2.9.0 - linked zones can useOffset and useHeading - getPoint update - - new getOrigin() - pointInZone understands useOrig - allStaticsInZone supports useOrig - dPhi for zones with useHeading @@ -117,6 +116,9 @@ cfxZones.version = "3.0.0" linedUnit and warning. - initZoneVerbosity() - 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 newZone.isCircle = true newZone.radius = dcsZone.radius + newZone.maxRadius = newZone.radius -- same for circular elseif zoneType == 2 then -- polyZone @@ -254,6 +257,7 @@ function cfxZones.readFromDCS(clearfirst) -- now transfer all point in the poly -- note: DCS in 2.7 misspells vertices as 'verticies' -- correct for this + newZone.maxRadius = 0 local verts = {} if dcsZone.verticies then verts = dcsZone.verticies else @@ -265,6 +269,10 @@ function cfxZones.readFromDCS(clearfirst) local dcsPoint = verts[v] local polyPoint = cfxZones.createPointFromDCSPoint(dcsPoint) -- (x, y) --> (x, 0, y-->z) 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 else @@ -1194,65 +1202,12 @@ function cfxZones.createGroundUnitsInZoneForCoalition (theCoalition, groupName, return newGroup, groupDataCopy 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 -- @@ -1538,8 +1493,6 @@ function cfxZones.isMEFlag(inFlag) trigger.action.outText("+++zne: warning: deprecated isMEFlag", 30) return true -- returns true if inFlag is a pure positive number --- inFlag = dcsCommon.trim(inFlag) --- return dcsCommon.stringIsPositiveNumber(inFlag) end function cfxZones.verifyMethod(theMethod, theZone) @@ -1579,7 +1532,6 @@ function cfxZones.verifyMethod(theMethod, theZone) local op = string.sub(theMethod, 1, 1) local remainder = string.sub(theMethod, 2) remainder = dcsCommon.trim(remainder) -- remove all leading and trailing spaces --- local rNum = tonumber(remainder) if true then -- we have a comparison = ">", "=", "<" followed by a number @@ -1898,7 +1850,9 @@ function cfxZones.flagArrayFromString(inString) end -- --- PROPERTY PROCESSING +-- =================== +-- PROPERTY PROCESSING +-- =================== -- function cfxZones.getAllZoneProperties(theZone, caseInsensitive) -- return as dict @@ -2030,9 +1984,7 @@ function cfxZones.getPositiveRangeFromZoneProperty(theZone, theProperty, default upperBound = lowerBound lowerBound = temp end --- if rndFlags.verbose then --- trigger.action.outText("+++Zne: detected range <" .. lowerBound .. ", " .. upperBound .. ">", 30) --- end + else -- bounds illegal 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 lowerBound = upperBound end --- trigger.action.outText("+++Zne: returning <" .. lowerBound .. ", " .. upperBound .. ">", 30) + return lowerBound, upperBound end @@ -2201,7 +2153,290 @@ function cfxZones.getSmokeColorStringFromZoneProperty(theZone, theProperty, defa end -- --- Moving Zones. They contain a link to their unit +-- Zone-based wildcard processing +-- + +-- process +function cfxZones.processZoneStatics(inMsg, theZone) + if theZone then + inMsg = inMsg:gsub("", theZone.name) + end + return inMsg +end + +-- process , , , , +function cfxZones.processSimpleZoneDynamics(inMsg, theZone, timeFormat, imperialUnits) + if not inMsg then return "" end + -- replace 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("", timeString) + + -- replace with lat of zone point and with lon of zone point + -- and 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) + outMsg = outMsg:gsub("", lon) + outMsg = outMsg:gsub("", alt) + local grid = coord.LLtoMGRS(coord.LOtoLL(currPoint)) + local mgrs = grid.UTMZone .. ' ' .. grid.MGRSDigraph .. ' ' .. grid.Easting .. ' ' .. grid.Northing + outMsg = outMsg:gsub("", mgrs) + return outMsg +end + +-- process , +function cfxZones.processDynamicValues(inMsg, theZone, msgResponses) + -- replace all occurences of with their values + local pattern = "" -- 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, "","") + -- 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 = "" -- 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, "","") + + -- 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 ("", rndRsp) + end + + return outMsg +end + +-- process +function cfxZones.processDynamicTime(inMsg, theZone, timeFormat) + if not timeFormat then timeFormat = "<:h>:<:m>:<:s>" end + -- replace all occurences of with their values + local pattern = "" -- 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, "","") + -- 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 +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 = "" + 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 "" end + local formerType = type(inMsg) + if formerType ~= "string" then inMsg = tostring(inMsg) end + if not inMsg then inMsg = "" end + local theMsg = inMsg + -- process common DCS stuff like /n + theMsg = dcsCommon.processStringWildcards(theMsg) -- call old inherited + -- process + theMsg = cfxZones.processZoneStatics(theMsg, theZone) + -- process , , , , + theMsg = cfxZones.processSimpleZoneDynamics(theMsg, theZone, timeFormat, imperialUnits) + -- process , + theMsg = cfxZones.processDynamicValues(theMsg, theZone, responses) + -- process + theMsg = cfxZones.processDynamicTime(theMsg, theZone, timeFormat) + -- process + 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 -- to their master unit. delta phi allows adjustment for heading -- 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.y = 0 -- aZone.y 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 end @@ -2259,8 +2488,7 @@ function cfxZones.linkUnitToZone(theUnit, theZone, dx, dy) -- note: dy is really local unitHeading = dcsCommon.getUnitHeading(theUnit) local bearingOffset = math.atan2(dy, dx) -- rads 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 if dPhi < 0 then dPhi = dPhi + 2 * 3.141592 end if (theZone.verbose and theZone.useHeading) then @@ -2349,14 +2577,12 @@ function cfxZones.updateMovingZones() end 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.linkedUnit = nil theUnit = Unit.getByName(theZone.linkName) if theUnit then - --trigger.action.outText("initlink has link to <" .. theZone.linkName .. "> for <" .. theZone.name .. ">", 30) + local dx = 0 local dz = 0 if theZone.useOffset or theZone.useHeading then @@ -2367,12 +2593,12 @@ function cfxZones.initLink(theZone) dz = delta.z end 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 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 theZone.linkBroken = nil - --trigger.action.outText("done linking <" .. theZone.linkName .. "> to zone <" .. theZone.name .. ">", 30) + else if theZone.verbose then trigger.action.outText("Linked unit: no unit <" .. theZone.linkName .. "> to link <" .. theZone.name .. "> to", 30) @@ -2417,26 +2643,25 @@ function cfxZones.startMovingZones() end end + +-- +-- =========== +-- INIT MODULE +-- =========== +-- + function cfxZones.initZoneVerbosity() for aName,aZone in pairs(cfxZones.zones) do -- support for zone-local verbose flag aZone.verbose = cfxZones.getBoolFromZoneProperty(aZone, "verbose", false) end end --- --- init --- function cfxZones.init() -- read all zones into my own db cfxZones.readFromDCS(true) -- true: erase old - - -- now, 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 + + -- pre-read zone owner for all zones local pZones = cfxZones.zonesWithProperty("owner") for n, aZone in pairs(pZones) do aZone.owner = cfxZones.getCoalitionFromZoneProperty(aZone, "owner", 0) diff --git a/modules/cloneZone.lua b/modules/cloneZone.lua index d0d0fea..865ec9e 100644 --- a/modules/cloneZone.lua +++ b/modules/cloneZone.lua @@ -1,5 +1,5 @@ cloneZones = {} -cloneZones.version = "1.7.0" +cloneZones.version = "1.7.1" cloneZones.verbose = false cloneZones.requiredLibs = { "dcsCommon", -- always @@ -91,6 +91,8 @@ cloneZones.respawnOnGroupID = true - , , , wildcards - identical=true overrides nameScheme - 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) end + -- interface to delicates + if cfxZones.hasProperty(theZone, "useDelicates") then + theZone.delicateName = dcsCommon.trim(cfxZones.getStringFromZoneProperty(theZone, "useDelicates", "")) + 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 theZone.rndLoc = cfxZones.getBoolFromZoneProperty(theZone, "randomizedLoc", false) if cfxZones.hasProperty(theZone, "rndLoc") then @@ -998,11 +1009,14 @@ function cloneZones.validateSpawnGroupData(theData, theZone, groupNames, unitNam 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) local theData = args[1] local spawnedGroups = args[2] local pos = args[3] - local verbose = args[4] + local theZone = args[4] + local verbose = theZone.verbose local rawData = dcsCommon.clone(theData) if verbose then 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) end 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 -- we need to try again in one second if verbose then @@ -1200,6 +1229,7 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone) -- now spawn all raw data local groupCollector = {} -- 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 -- now spawn and save to clones -- first norm and clone data for later save @@ -1225,7 +1255,7 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone) end -- SPAWN NOW!!!! - local theGroup = coalition.addGroup(rawData.CZctry, rawData.CZtheCat, rawData) + theGroup = coalition.addGroup(rawData.CZctry, rawData.CZtheCat, rawData) table.insert(spawnedGroups, theGroup) -- update groupXlate table from spawned group @@ -1261,28 +1291,49 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone) 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 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 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("Note: theData.groupId is <" .. theData.groupId .. ">", 30) - --if spawnZone.identical then - -- trigger.action.outText("(Identical = true detected for this zone)", 30) - --end + end 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 - 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 -- we note it in the spawn data for the group so -- persistence works fine 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 @@ -1387,8 +1438,20 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone) cloneZones.invokeCallbacks(theZone, "did spawn static", theStatic) 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 + + -- 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 if isCargo then if cfxCargoManager then diff --git a/modules/dcsCommon.lua b/modules/dcsCommon.lua index d755f01..06ea23a 100644 --- a/modules/dcsCommon.lua +++ b/modules/dcsCommon.lua @@ -1,5 +1,5 @@ dcsCommon = {} -dcsCommon.version = "2.8.0" +dcsCommon.version = "2.8.1" --[[-- VERSION HISTORY 2.2.6 - compassPositionOfARelativeToB - clockPositionOfARelativeToB @@ -123,7 +123,10 @@ dcsCommon.version = "2.8.0" - new getGroupNameByID - bool2YesNo alsco can return NIL - 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 @@ -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) if not theArray 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 if theArray[i] == theString then return true end end @@ -2117,7 +2167,35 @@ end function dcsCommon.stringStartsWith(theString, thePrefix) 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 function dcsCommon.removePrefix(theString, thePrefix) @@ -3008,6 +3086,23 @@ function dcsCommon.LSR(a, num) return a end +-- +-- string windcards +-- +function dcsCommon.processStringWildcards(inMsg) + -- Replace STATIC bits of message like CR and zone name + if not inMsg then return "" end + local formerType = type(inMsg) + if formerType ~= "string" then inMsg = tostring(inMsg) end + if not inMsg then inMsg = "" end + local outMsg = "" + -- replace line feeds + outMsg = inMsg:gsub("", "\n") + + return outMsg +end + + -- -- SEMAPHORES -- diff --git a/modules/delicates.lua b/modules/delicates.lua index b2dc85f..9f8d1bb 100644 --- a/modules/delicates.lua +++ b/modules/delicates.lua @@ -1,5 +1,5 @@ delicates = {} -delicates.version = "1.1.0" +delicates.version = "1.1.1" delicates.verbose = false delicates.ups = 1 delicates.requiredLibs = { @@ -16,7 +16,8 @@ delicates.inventory = {} - addStaticObjectInventoryForZone - blowAll? - safetyMargin - safety margin. defaults to 10% - + 1.1.1 - addGroupToInventoryForZone + - verbose for zone will show update event from useDelicates --]]-- function delicates.adddDelicates(theZone) @@ -73,9 +74,10 @@ function delicates.makeZoneInventory(theZone) for idy, anObject in pairs(collector) do local oName = anObject:getName() if type(oName) == 'number' then oName = tostring(oName) end - local oLife = anObject:getLife() - anObject:getLife() * theZone.safetyMargin + local mLife = anObject:getLife() + local oLife = mLife - mLife * theZone.safetyMargin 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 local uP = anObject:getPoint() if cfxZones.isPointInsideZone(uP, theZone) then @@ -83,6 +85,7 @@ function delicates.makeZoneInventory(theZone) local desc = {} desc.cat = aCat desc.oLife = oLife + desc.mLife = mLife desc.theZone = theZone desc.oName = oName delicates.inventory[oName] = desc @@ -102,17 +105,34 @@ function delicates.addStaticObjectToInventoryForZone(theZone, theStatic) local desc = {} desc.cat = theStatic:getCategory() + desc.mLife = theStatic:getLife() desc.oLife = theStatic:getLife() - theStatic:getLife() * theZone.safetyMargin if desc.oLife < 0 then desc.oLife = 0 end desc.theZone = theZone 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 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 +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) theZone.power = cfxZones.getNumberFromZoneProperty(theZone, "power", 10) @@ -221,14 +241,14 @@ function delicates:onEvent(theEvent) local cLife = theObj:getLife() if cLife < desc.oLife then if desc.theZone.verbose or delicates.verbose then - trigger.action.outText("+++deli: BRITTLE TRIGGER: life <" .. cLife .. "> below safety margin <" .. oDesc.oLife .. ">", 30) + trigger.action.outText("+++deli: BRITTLE TRIGGER: life <" .. cLife .. "> below safety margin <" .. desc.oLife .. ">", 30) end delicates.blowUpObject(desc) -- remove it from further searches delicates.inventory[oName] = nil else if desc.theZone.verbose or delicates.verbose then - trigger.action.outText("+++deli: CLOSE CALL, but life <" .. cLife .. "> within safety margin <" .. oDesc.oLife .. ">", 30) + trigger.action.outText("+++deli: CLOSE CALL, but life <" .. cLife .. "> within safety margin <" .. desc.oLife .. ">", 30) end end end @@ -287,6 +307,9 @@ function delicates.update() if theObj then 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 -- all well, transfer to next iter newInventory[oName] = oDesc diff --git a/modules/messenger.lua b/modules/messenger.lua index 3a95ad2..3a96ab6 100644 --- a/modules/messenger.lua +++ b/modules/messenger.lua @@ -1,5 +1,5 @@ messenger = {} -messenger.version = "2.1.0" +messenger.version = "2.2.0" messenger.verbose = false messenger.requiredLibs = { "dcsCommon", -- always @@ -61,6 +61,9 @@ messenger.messengers = {} - fix to messageMute - 2.1.1 - cosmetic: only output text if len>0 and not cls + 2.2.0 - + - 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 end --- --- read attributes --- -function messenger.preProcMessage(inMsg, theZone) - -- Replace STATIC bits of message like CR and zone name - if not inMsg then return "" end - local formerType = type(inMsg) - if formerType ~= "string" then inMsg = tostring(inMsg) end - if not inMsg then inMsg = "" end - local outMsg = "" - -- replace line feeds - outMsg = inMsg:gsub("", "\n") - if theZone then - outMsg = outMsg:gsub("", theZone.name) - end - return outMsg -end - --- Old-school processing to replace wildcards --- repalce with current time --- replace with zone's current lonlat --- replace with zone's current mgrs -function messenger.dynamicProcessClassic(inMsg, theZone) - - if not inMsg then return "" end - -- replace 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("", timeString) - - -- replace with lat of zone point and with lon of zone point - -- and 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) - outMsg = outMsg:gsub("", lon) - outMsg = outMsg:gsub("", alt) - local grid = coord.LLtoMGRS(coord.LOtoLL(currPoint)) - local mgrs = grid.UTMZone .. ' ' .. grid.MGRSDigraph .. ' ' .. grid.Easting .. ' ' .. grid.Northing - outMsg = outMsg:gsub("", mgrs) - return outMsg -end -- --- new dynamic flag processing --- -function messenger.processDynamicValues(inMsg, theZone) - -- replace all occurences of with their values - local pattern = "" -- 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, "","") - -- 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 = "" -- 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, "","") - - -- 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 ("", rndRsp) - end - - return outMsg -end - -function messenger.processDynamicTime(inMsg, theZone) - -- replace all occurences of with their values - local pattern = "" -- 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, "","") - -- 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 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 = "" - 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 - +-- Dynamic Group and Dynamic Unit processing are +-- unique to messenger, and are not available via +-- cfxZones or dcsCommon +-- function messenger.dynamicGroupProcessing(msg, theZone, theGroup) 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 == "sde" then locString = dcsCommon.getNauticalDirection(direction) 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]) else locString = "" end @@ -469,27 +216,14 @@ function messenger.dynamicUnitProcessing(inMsg, theZone, theUnit) end -function messenger.dynamicFlagProcessing(inMsg, theZone) - if not inMsg then return "No in message" end - if not theZone then return "Nil zone" end - - -- process - local msg = messenger.processDynamicValues(inMsg, theZone) - - -- process - msg = messenger.processDynamicTime(msg, theZone) - - -- process lat / lon / ele / mgrs - msg = messenger.processDynamicLoc(msg, theZone) - - return msg -end - +-- +-- reat attributes +-- function messenger.createMessengerWithZone(theZone) -- start val - a range 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.spaceAfter = cfxZones.getBoolFromZoneProperty(theZone, "spaceAfter", false) @@ -584,6 +318,7 @@ function messenger.createMessengerWithZone(theZone) -- flag whose value can be read: to be deprecated if cfxZones.hasProperty(theZone, "messageValue?") then theZone.messageValue = cfxZones.getStringFromZoneProperty(theZone, "messageValue?", "") + trigger.action.outText("+++Msg: Warning - zone <" .. theZone.name .. "> uses 'messageValue' attribute. Migrate to now!") end -- time format for new @@ -606,7 +341,7 @@ function messenger.createMessengerWithZone(theZone) end 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 @@ -620,29 +355,23 @@ function messenger.getMessage(theZone) if not zName then zName = "" end local zVal = "" if theZone.messageValue then + trigger.action.outText("+++Msg: Warning - zone <" .. theZone.name .. "> uses 'messageValue' attribute. Migrate to now!") zVal = cfxZones.getFlagValue(theZone.messageValue, theZone) zVal = tostring(zVal) if not zVal then zVal = "" end end - - -- replace *zone and *value wildcards - --msg = string.gsub(msg, "*name", zName)-- deprecated - --msg = string.gsub(msg, "*value", zVal) -- deprecated -- old-school to provide value from messageValue + -- to be removed mid-2023 msg = string.gsub(msg, "", zVal) local z = tonumber(zVal) if not z then z = 0 end msg = dcsCommon.processHMS(msg, z) - -- process [classic format], and - msg = messenger.dynamicProcessClassic(msg, theZone) + -- remainder hand-off to cfxZones (refactoring of messenger code + msg = cfxZones.processStringWildcards(msg, theZone, theZone.msgTimeFormat, theZone.imperialUnits, theZone.msgResponses) - -- now add new processing of access - msg = messenger.dynamicFlagProcessing(msg, theZone) - -- now add new processinf of - -- also handles , , return msg end @@ -789,7 +518,3 @@ if not messenger.start() then messenger = nil end ---[[-- - - ---]]-- \ No newline at end of file diff --git a/modules/valet.lua b/modules/valet.lua new file mode 100644 index 0000000..5d5b634 --- /dev/null +++ b/modules/valet.lua @@ -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", "") + if cfxZones.hasProperty(theZone, "firstInSoundFile") then + theZone.firstInSoundFile = cfxZones.getStringFromZoneProperty(theZone, "firstInSoundFile", "") + end + + theZone.outSoundFile = cfxZones.getStringFromZoneProperty(theZone, "outSoundFile", "") + + -- 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!", "*") + end + + if cfxZones.hasProperty(theZone, "bye!") then + theZone.valetBye = cfxZones.getStringFromZoneProperty(theZone, "bye!", "*") + 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", "") + theZone.valetGroups = dcsCommon.string2Array(groups, ",") + elseif cfxZones.hasProperty(theZone, "valetGroups") then + local groups = cfxZones.getStringFromZoneProperty(theZone, "valetGroups", "") + theZone.valetGroups = dcsCommon.string2Array(groups, ",") + end + + if cfxZones.hasProperty(theZone, "units") then + local units = cfxZones.getStringFromZoneProperty(theZone, "units", "") + theZone.valetUnits = dcsCommon.string2Array(units, ",") + elseif cfxZones.hasProperty(theZone, "valetUnits") then + local units = cfxZones.getStringFromZoneProperty(theZone, "valetUnits", "") + 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("", pName) + theMsg = theMsg:gsub("", aUnit:getName()) + theMsg = theMsg:gsub("", aUnit:getTypeName()) + theMsg = theMsg:gsub("", aUnit:getGroup():getName()) + theMsg = theMsg:gsub("", tostring(theDesc.greets + 1) ) + theMsg = theMsg:gsub("", 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 == "" 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 == "" 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 \ No newline at end of file diff --git a/tutorial & demo missions/demo - I say hello goodbye.miz b/tutorial & demo missions/demo - I say hello goodbye.miz new file mode 100644 index 0000000..4b55104 Binary files /dev/null and b/tutorial & demo missions/demo - I say hello goodbye.miz differ