diff --git a/Doc/DML Documentation.pdf b/Doc/DML Documentation.pdf index 6067e2e..96cfea5 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 806b6da..fe86afc 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 7d0b8e6..e719338 100644 --- a/modules/FARPZones.lua +++ b/modules/FARPZones.lua @@ -22,8 +22,6 @@ FARPZones.verbose = false FARPZones.requiredLibs = { "dcsCommon", "cfxZones", -- Zones, of course --- "cfxCommander", -- to make troops do stuff --- "cfxGroundTroops", -- generic when dropping troops } -- *** DOES NOT EXTEND ZONES, USES OWN STRUCT *** diff --git a/modules/TDZ.lua b/modules/TDZ.lua new file mode 100644 index 0000000..e5173a9 --- /dev/null +++ b/modules/TDZ.lua @@ -0,0 +1,385 @@ +tdz = {} +tdz.version = "0.9.0dev" +tdz.requiredLibs = { + "dcsCommon", -- always + "cfxZones", -- Zones, of course +} +tdz.allTdz = {} +tdz.watchlist = {} +tdz.watching = false +tdz.timeoutAfter = 120 -- seconds. +-- +-- rwy draw procs +-- +function tdz.rotateXZPolyInRads(thePoly, rads) + if not rads then + trigger.action.outText("rotateXZPolyInRads (inner): no rads", 30) + return + end + local c = math.cos(rads) + local s = math.sin(rads) + for idx, p in pairs(thePoly) do + local nx = p.x * c - p.z * s + local nz = p.x * s + p.z * c + p.x = nx + p.z = nz + end +end + +function tdz.rotateXZPolyAroundCenterInRads(thePoly, center, rads) + if not rads then + trigger.action.outText("rotateXZPolyAroundCenterInRads: no rads", 30) + return + end + local negCtr = {x = -center.x, y = -center.y, z = -center.z} + tdz.translatePoly(thePoly, negCtr) + if not rads then + trigger.action.outText("WHOA! rotateXZPolyAroundCenterInRads: no rads", 30) + return + end + tdz.rotateXZPolyInRads(thePoly, rads) + tdz.translatePoly(thePoly, center) +end + +function tdz.rotateXZPolyAroundCenterInDegrees(thePoly, center, degrees) + tdz.rotateXZPolyAroundCenterInRads(thePoly, center, degrees * 0.0174533) +end + +function tdz.translatePoly(thePoly, v) -- straight rot, translate to 0 first + for idx, aPoint in pairs(thePoly) do + aPoint.x = aPoint.x + v.x + if aPoint.y then aPoint.y = aPoint.y + v.y end + if aPoint.z then aPoint.z = aPoint.z + v.z end + end +end +--[[-- +function tdz.frameRwy(center, length, width, rads, a, b) -- bearing in rads + if not a then a = 0 end + if not b then b = 1 end + + -- create a 0-rotated centered poly + local poly = {} + local half = length / 2 + local leftEdge = -half + poly[4] = { x = leftEdge + a * length, z = width / 2, y = 0} + poly[3] = { x = leftEdge + b * length, z = width / 2, y = 0} + poly[2] = { x = leftEdge + b * length, z = -width / 2, y = 0} + poly[1] = { x = leftEdge + a * length, z = -width / 2, y = 0} + -- move it to center in map + tdz.translatePoly(poly, center) + + -- rotate it + tdz.rotateXZPolyAroundCenterInRads(poly, center, rads) + + -- frame it + local mId = dcsCommon.numberUUID() + trigger.action.quadToAll(-1, mId, poly[1], poly[2], poly[3], poly[4], {1, 0, 0, 1}, {1, 0, 0, .5}, 3) -- dotted line, red + +end +--]]-- +function tdz.calcTDZone(name, center, length, width, rads, a, b) + if not a then a = 0 end + if not b then b = 1 end + -- create a 0-rotated centered poly + local poly = {} + local half = length / 2 + local leftEdge = -half + poly[1] = { x = leftEdge + a * length, z = width / 2, y = 0} + poly[2] = { x = leftEdge + b * length, z = width / 2, y = 0} + poly[3] = { x = leftEdge + b * length, z = -width / 2, y = 0} + poly[4] = { x = leftEdge + a * length, z = -width / 2, y = 0} + -- move it to center in map + tdz.translatePoly(poly, center) + -- rotate it + tdz.rotateXZPolyAroundCenterInRads(poly, center, rads) + -- make it a dml zone + local theNewZone = cfxZones.createSimplePolyZone(name, center, poly) + return theNewZone--, left, right +end + +-- +-- create a tdz +-- +function tdz.createTDZ(theZone) + local p = theZone:getPoint() + local theBase = dcsCommon.getClosestAirbaseTo(p) -- never get FARPS + theZone.base = theBase + theZone.baseName = theBase:getName() + + -- get closest runway to TDZ + -- may get a bit hairy, so let's find a good way + local allRwys = theBase:getRunways() + local nearestRwy = nil + local minDist = math.huge + for idx, aRwy in pairs(allRwys) do + local rp = aRwy.position + local dist = dcsCommon.distFlat(p, rp) + if dist < minDist then + nearestRwy = aRwy + minDist = dist + end + end + local bearing = nearestRwy.course * (-1) + theZone.bearing = bearing + rwname = math.floor(dcsCommon.bearing2degrees(bearing)/10 + 0.5) -- nice number + degrees = math.floor(dcsCommon.bearing2degrees(bearing) * 10) / 10 + if degrees < 0 then degrees = degrees + 360 end + if degrees > 360 then degrees = degrees - 360 end + if rwname < 0 then rwname = rwname + 36 end + if rwname > 36 then rwname = rwname - 36 end + local opName = rwname + 18 + if opName > 36 then opName = opName - 36 end + if rwname < 10 then rwname = "0"..rwname end + if opName < 10 then opName = "0" .. opName end + theZone.rwName = rwname .. "/" .. opName + theZone.opName = opName .. "/" .. rwname + local rwLen = nearestRwy.length + local rwWid = nearestRwy.width + local pos = nearestRwy.position + -- p1 is for distance to centerline calculation, defining a point + -- length away in direction bearing, setting up the line + -- theZone.rwCenter, theZone.p1 + theZone.rwCenter = pos + local p1 = {x = pos.x + math.cos(bearing) * rwLen, y = 0, z = pos.z + math.sin(bearing) * rwLen} + theZone.rwP1 = p1 + theZone.starts = theZone:getNumberFromZoneProperty("starts", 0) + theZone.ends = theZone:getNumberFromZoneProperty("ends", 610) -- m = 2000 ft + theZone.opposing = theZone:getBoolFromZoneProperty("opposing", true) + + theZone.runwayZone = tdz.calcTDZone(theZone.name .. "-" .. rwname .. "main", pos, rwLen, rwWid, bearing) + theZone.runwayZone:drawZone({0, 0, 0, 1}, {0, 0, 0, 0}) -- black outline + local theTDZone = tdz.calcTDZone(theZone.name .. "-" .. rwname, pos, rwLen, rwWid, bearing, theZone.starts / rwLen, theZone.ends/rwLen) + -- to do: mark the various zones of excellence in different colors, or at least the excellent one with more color + theTDZone:drawZone({0, 1, 0, 1}, {0, 1, 0, .25}) + theZone.normTDZone = theTDZone + if theZone.opposing then + theTDZone = tdz.calcTDZone(theZone.name .. "-" .. opName, pos, rwLen, rwWid, bearing + math.pi, theZone.starts / rwLen, theZone.ends/rwLen) + theTDZone:drawZone({0, 1, 0, 1}, {0, 1, 0, .25}) + theZone.opTDZone = theTDZone + theZone.opBearing = bearing + math.pi + end + if theZone:hasProperty("landed!") then + theZone.landedFlag = theZone:getStringFromZoneProperty("landed!", "none") + end + if theZone:hasProperty("touchdown!") then + theZone.touchDownFlag = theZone:getStringFromZoneProperty("touchDown!", "none") + end + if theZone:hasProperty("fail!") then + theZone.failFlag = theZone:getStringFromZoneProperty("fail!", "none") + end + + theZone.method = theZone:getStringFromZoneProperty("method", "inc") +end + +-- +-- event handler +-- + +function tdz.playerLanded(theUnit, playerName) + if tdz.watchlist[playerName] then + -- this is not a new landing, for now ignore, increment bump count + -- make sure unit names match? + local entry = tdz.watchlist[playerName] + entry.hops = entry.hops + 1 -- uh oh. +-- trigger.action.outText("Bump!") + end + + -- we may want to filter helicopters + + -- see if we touched down inside of one of our watched zones + local p = theUnit:getPoint() + local theGroup = theUnit:getGroup() + local gID = theGroup:getID() + local msg = "" + local theZone = nil + for idx, aRunway in pairs(tdz.allTdz) do + local theRunway = aRunway.runwayZone + if theRunway:pointInZone(p) then + -- touchdown! + theZone = aRunway + if theZone.touchDownFlag then + theZone.pollFlag(theZone.touchDownFlag, theZone.method) + end + trigger.action.outTextForGroup(gID, "Touchdown! Come to a FULL STOP for evaluation", 30) + end + end + if not theZone then return end -- no landing eval zone hit + + -- start a new watchlist entry + local entry = {} + entry.msg = "" + entry.playerName = playerName + entry.unitName = theUnit:getName() + entry.theType = theUnit:getTypeName() + entry.gID = gID + entry.theTime = timer.getTime() + entry.tdPoint = p + entry.tdVel = theUnit:getVelocity() -- vector + entry.hops = 1 + entry.theZone = theZone + + -- see if we are in main or opposite direction + local hdg = dcsCommon.getUnitHeading(theUnit) + local dHdg = math.abs(theZone.bearing - hdg) -- 0..Pi + local dOpHdg = math.abs(theZone.opBearing - hdg) + local opposite = false + if dOpHdg < dHdg then + opposite = true + dHdg = dOpHdg + trigger.action.outText("opposite rwy detected", 30) + end + if dHdg > math.pi * 1.5 then -- > 270+ + dHdg = dHdg - math.pi * 1.5 + elseif dHdg > math.pi / 2 then -- > 90+ + dHdg = dHdg - math.pi / 2 + end + dHdg = math.floor(dHdg * 572.958) / 10 -- in degrees + local lHdg = math.floor(hdg * 572.958) / 10 -- also in deg + -- now see how far off centerline. + local offcenter = dcsCommon.distanceOfPointPToLineXZ(p, theZone.rwCenter, theZone.rwP1) + offcenter = math.floor(offcenter * 10)/10 + local vel = dcsCommon.vMag(entry.tdVel) + local vkm = math.floor(vel * 36) / 10 + local kkm = math.floor(vel * 19.4383) / 10 + entry.msg = entry.msg .. "\nLanded heading " .. lHdg .. "°, diverging by " .. dHdg .. "° from runway heading, velocity at touchdown " .. vkm .. " kmh/" .. kkm .. " kts, touchdown " .. offcenter .. " m off centerline\n" + + -- inside TDZ? + local tdZone = theZone.normTDZone + if opposite and theZone.opposing then + + tdZone = theZone.opTDZone + end + if tdZone:pointInZone(p) then + -- yes, how far behind threshold + -- project point onto line to see how far inside + local distBehind = dcsCommon.distanceOfPointPToLineXZ(p, tdZone.poly[1], tdZone.poly[4]) + local zonelen = math.abs(theZone.starts-theZone.ends) + local percentile = math.floor(distBehind / zonelen * 100) + local rating = "" + if percentile < 5 or percentile > 90 then rating = "marginal" + elseif percentile < 15 or percentile > 80 then rating = "pass" + elseif percentile < 25 or percentile > 60 then rating = "good" + else rating = "excellent" end + entry.msg = entry.msg .. "Touchdown inside TD-Zone, <" .. math.floor(distBehind) .. " m> behind threshold, rating = " .. rating .. "\n" + end + + tdz.watchlist[playerName] = entry + if not tdz.watching then + tdz.watching = true + timer.scheduleFunction(tdz.watchLandings, {}, timer.getTime() + 0.2) + end +end + +function tdz:onEvent(event) + if not event.initiator then return end + local theUnit = event.initiator + if not theUnit.getPlayerName then return end + local playerName = theUnit:getPlayerName() + if not playerName then return end + if event.id == 4 then + -- player landed + tdz.playerLanded(theUnit, playerName) + end +end + +-- +-- Monitor landings in progress +-- +function tdz.watchLandings() + local filtered = {} + local count = 0 + local transfer = false + local success = false + local now = timer.getTime() + for playerName, aLanding in pairs (tdz.watchlist) do + -- see if landing timed out + local tdiff = now - aLanding.theTime + if tdiff < tdz.timeoutAfter then + local theUnit = Unit.getByName(aLanding.unitName) + if theUnit and Unit.isExist(theUnit) then + local vel = theUnit:getVelocity() + local vel = dcsCommon.vMag(vel) + local p = theUnit:getPoint() + if aLanding.theZone.runwayZone:pointInZone(p) then + -- we must slow down to below 3.6 km/h + if vel < 1 then + -- make sure that we are still inside the runway + success = true + else + transfer = true + end + else + trigger.action.outTextForGroup(aLanding.gID, "Ran off runway.", 30) + end + end + end + if transfer then + count = count + 1 + filtered[playerName] = aLanding + else + local theZone = aLanding.theZone + if success then + local theUnit = Unit.getByName(aLanding.unitName) + local p = theUnit:getPoint() + local tdist = math.floor(dcsCommon.distFlat(p, aLanding.tdPoint)) + aLanding.msg = aLanding.msg .."\nSuccessful landing for " .. aLanding.playerName .." in a " .. aLanding.theType .. ". Landing run = <" .. tdist .. " m>, <" .. math.floor(tdiff*10)/10 .. "> seconds from touch-down to standstill." + + if aLanding.hops > 1 then + aLanding.msg = aLanding.msg .. "\nNumber of hops: " .. aLanding.hops + end + if theZone.landedFlag then + theZone:pollFlag(theZone.landedFlag, theZone.method) + end + aLanding.msg = aLanding.msg .."\n" + trigger.action.outTextForGroup(aLanding.gID, aLanding.msg, 30) + else + if theZone.failFlag then + theZone:pollFlag(theZone.failFlag, theZone.method) + end + trigger.action.outTextForGroup(aLanding.gID, "Landing for " .. aLanding.playerName .." incomplete.", 30) + end + end + end + + tdz.watchlist = filtered + + if count > 0 then + timer.scheduleFunction(tdz.watchLandings, {}, timer.getTime() + 0.2) + else + tdz.watching = false + end +end +-- +-- Start +-- +function tdz.readConfigZone() +end + +function tdz.start() + if not dcsCommon.libCheck("cfx TDZ", + tdz.requiredLibs) then + return false + end + + -- read config + tdz.readConfigZone() + + -- collect all wp target zones + local attrZones = cfxZones.getZonesWithAttributeNamed("TDZ") + + for k, aZone in pairs(attrZones) do + tdz.createTDZ(aZone) -- process attribute and add to zone + table.insert(tdz.allTdz, aZone) -- remember it so we can smoke it + end + + -- add event handler + world.addEventHandler(tdz) + + trigger.action.outText("cf/x TDZ version " .. tdz.version .. " running", 30) + return true +end + +if not tdz.start() then + trigger.action.outText("cf/x TDZ aborted: missing libraries", 30) + tdz = nil +end \ No newline at end of file diff --git a/modules/bombRange.lua b/modules/bombRange.lua new file mode 100644 index 0000000..ab98e35 --- /dev/null +++ b/modules/bombRange.lua @@ -0,0 +1,564 @@ +bombRange = {} +bombRange.version = "1.0.0" +bombRange.dh = 1 -- meters above ground level burst + +bombRange.requiredLibs = { + "dcsCommon", -- always + "cfxZones", -- Zones, of course +} + +bombRange.bombs = {} -- live tracking +bombRange.ranges = {} -- all bomb ranges +bombRange.playerData = {} -- player accumulated data +bombRange.unitComms = {} -- command interface per unit +bombRange.tracking = false -- if true, we are tracking projectiles +bombRange.myStatics = {} -- indexed by id + +function bombRange.addBomb(theBomb) + table.insert(bombRange.bombs, theBomb) +end + +function bombRange.addRange(theZone) + table.insert(bombRange.ranges, theZone) +end + +function bombRange.markRange(theZone) + local newObjects = theZone:markZoneWithObjects(theZone.markType, theZone.markNum, false) + for idx, aStatic in pairs(newObjects) do + local theID = tonumber(aStatic:getID()) + bombRange.myStatics[theID] = aStatic + end +end + +function bombRange.markCenter(theZone) + local theObject = theZone:markCenterWithObject(theZone.centerType) + --table.insert(bombRange.myStatics, theObject) + local theID = tonumber(theObject:getID()) + bombRange.myStatics[theID] = theObject +end + +function bombRange.createRange(theZone) -- has bombRange attribte to mark it + theZone.usePercentage = theZone:getBoolFromZoneProperty("percentage", theZone.isCircle) + if theZone.usePercentage and theZone.isPoly then + trigger.action.outText("+++bRng: WARNING: zone <" .. theZone.name .. "> is not a circular zone but wants to use percentage scoring!", 30) + end + theZone.details = theZone:getBoolFromZoneProperty("details", false) + theZone.reporter = theZone:getBoolFromZoneProperty("reporter", true) + theZone.reportName = theZone:getBoolFromZoneProperty("reportName", true) + theZone.smokeHits = theZone:getBoolFromZoneProperty("smokeHits", false) + theZone.smokeColor = theZone:getSmokeColorStringFromZoneProperty("smokeColor", "blue") + theZone.flagHits = theZone:getBoolFromZoneProperty("flagHits", false) + theZone.flagType = theZone:getStringFromZoneProperty("flagType", "Red_Flag") + theZone.clipDist = theZone:getNumberFromZoneProperty("clipDist", 2000) -- when further way, the drop will be disregarded + + theZone.method = theZone:getStringFromZoneProperty("method", "inc") + if theZone:hasProperty("hit!") then + theZone.hitOut = theZone:getStringFromZoneProperty("hit!", "") + end + + theZone.markType = theZone:getStringFromZoneProperty("markType", "Black_Tyre_RF") + theZone.markBoundary = theZone:getBoolFromZoneProperty("markBoundary", false) + theZone.markNum = theZone:getNumberFromZoneProperty("markNum", 3) -- per quarter + theZone.markCenter = theZone:getBoolFromZoneProperty("markCenter", false) + theZone.centerType = theZone:getStringFromZoneProperty("centerType", "house2arm") + theZone.markOnMap = theZone:getBoolFromZoneProperty("markOnMap", false) + theZone.mapColor = theZone:getRGBAVectorFromZoneProperty("mapColor", {0.8, 0.8, 0.8, 1.0}) + theZone.mapFillColor = theZone:getRGBAVectorFromZoneProperty("mapFillColor", {0.8, 0.8, 0.8, 0.2}) + if theZone.markBoundary then bombRange.markRange(theZone) end + if theZone.markCenter then bombRange.markCenter(theZone) end + if theZone.markOnMap then + local markID = theZone:drawZone(theZone.mapColor, theZone.mapFillColor) + end +end + +-- +-- player data +-- +function bombRange.getPlayerData(name) + local theData = bombRange.playerData[name] + if not theData then + theData = {} + theData.aircraft = {} -- by typeDesc contains all drops per weapon type + theData.totalDrops = 0 + theData.totalHits = 0 + theData.totalPercentage = 0 -- sum, must be divided by drops + bombRange.playerData[name] = theData +-- trigger.action.outText("created new player data for " .. name, 30) + end + return theData +end + +function bombRange.addImpactForWeapon(weapon, isInside, percentage) + if not percentage then percentage = 0 end + if type(percentage) == "string" then percentage = 1 end -- handle poly + + local theData = bombRange.getPlayerData(weapon.pName) + local uType = weapon.uType + local uData = theData.aircraft[uType] + if not uData then + uData = {} + uData.wTypes = {} + theData.aircraft[uType] = uData + end + wType = weapon.type + local wData = uData.wTypes[wType] + if not wData then + wData = {shots = 0, hits = 0, percentage = 0} + uData.wTypes[wType] = wData + end + + wData.shots = wData.shots + 1 + if isInside then + wData.hits = wData.hits + 1 + wData.percentage = wData.percentage + percentage + else + end + theData.totalDrops = theData.totalDrops + 1 + if isInside then + theData.totalHits = theData.totalHits + 1 + theData.totalPercentage = theData.totalPercentage + percentage + end + +end + +function bombRange.showStatsForPlayer(pName, gID, unitName) + local theData = bombRange.getPlayerData(pName) + local msg = "\nWeapons Range Statistics for " .. pName .. "\n" + local lineCount = 0 + for aType, aircraft in pairs(theData.aircraft) do + if aircraft.wTypes then + if lineCount < 1 then + msg = msg .. " Aircraft / Munition : Drops / Hits / Quality\n" + end + for wName, wData in pairs(aircraft.wTypes) do + local pct = wData.percentage / wData.shots + pct = math.floor(pct * 10) / 10 + msg = msg .. " " .. aType .. " / " .. wName .. ": " .. wData.shots .. " / " .. wData.hits .. " / " .. pct .. "%\n" + lineCount = lineCount + 1 + end + end -- if weapon per aircraft + end + + if lineCount < 1 then + msg = msg .. "\n NO DATA\n\n" + else + msg = msg .. "\n Total ordnance drops: " .. theData.totalDrops + msg = msg .. "\n Total on target: " .. theData.totalHits + local q = math.floor(theData.totalPercentage / theData.totalDrops * 10) / 10 + msg = msg .. "\n Total Quality: " .. q .. "%\n" + end + + if bombRange.mustCheckIn then + local comms = bombRange.unitComms[unitName] + if comms.checkedIn then + msg = msg .. "\nYou are checked in with weapons range command.\n" + else + msg = msg .. "\nPLEASE CHECK IN with weapons range command.\n" + end + end + trigger.action.outTextForGroup(gID, msg, 30) + + +end +-- +-- unit UI +-- + +function bombRange.initCommsForUnit(theUnit) + local uName = theUnit:getName() + local pName = theUnit:getPlayerName() + local theGroup = theUnit:getGroup() + local gID = theGroup:getID() + local comms = bombRange.unitComms[uName] + if comms then + if bombRange.mustCheckIn then + missionCommands.removeItemForGroup(gID, comms.checkin) + end + missionCommands.removeItemForGroup(gID, comms.reset) + missionCommands.removeItemForGroup(gID, comms.getStat) + missionCommands.removeItemForGroup(gID, comms.root) + end + comms = {} + comms.checkedIn = false + comms.root = missionCommands.addSubMenuForGroup(gID, bombRange.menuTitle) + comms.getStat = missionCommands.addCommandForGroup(gID, "Get statistics for " .. pName, comms.root, bombRange.redirectComms, {"getStat", uName, pName, gID}) + comms.reset = missionCommands.addCommandForGroup(gID, "RESET statistics for " .. pName, comms.root, bombRange.redirectComms, {"reset", uName, pName, gID}) + if bombRange.mustCheckIn then + comms.checkin = missionCommands.addCommandForGroup(gID, "Check in with range", comms.root, bombRange.redirectComms, {"check", uName, pName, gID}) + end + bombRange.unitComms[uName] = comms +end + +function bombRange.redirectComms(args) + timer.scheduleFunction(bombRange.commsRequest, args, timer.getTime() + 0.1) +end + +function bombRange.commsRequest(args) + local command = args[1] -- getStat, check, + local uName = args[2] + local pName = args[3] + local theUnit = Unit.getByName(uName) + local theGroup = theUnit:getGroup() + local gID = theGroup:getID() + + if command == "getStat" then + bombRange.showStatsForPlayer(pName, gID, uName) + end + + if command == "reset" then + bombRange.playerData[pName] = nil + trigger.action.outTextForGroup(gID, "Clean slate, " .. uName .. ", all existing records have been deleted.", 30) + end + + if command == "check" then + comms = bombRange.unitComms[uName] + if comms.checkedIn then + comms.checkedIn = false -- we are now checked out + missionCommands.removeItemForGroup(gID, comms.checkin) + comms.checkin = missionCommands.addCommandForGroup(gID, "Check in with range", comms.root, bombRange.redirectComms, {"check", uName, pName, gID}) + + trigger.action.outTextForGroup(gID, "Roger, " .. uName .. ", terminating range advisory. Have a good day!", 30) + if bombRange.signOut then + cfxZones.pollFlag(bombRange.signOut, bombRange.method, bombRange) + end + else + comms.checkedIn = true + missionCommands.removeItemForGroup(gID, comms.checkin) + comms.checkin = missionCommands.addCommandForGroup(gID, "Check OUT " .. uName .. " from range", comms.root, bombRange.redirectComms, {"check", uName, pName, gID})trigger.action.outTextForGroup(gID, uName .. ", you are go for weapons deployment, observers standing by.", 30) + if bombRange.signIn then + cfxZones.pollFlag(bombRange.signIn, bombRange.method, bombRange) + end + end + end + +end + +-- +-- Event Proccing +-- +function bombRange.suspectedHit(weapon, target) + if not bombRange.tracking then + return + end + if not target then return end + local theType = target:getTypeName() + + for idx, aType in pairs(bombRange.filterTypes) do + if theType == aType then return end + end + + -- try and match target to my known statics, exit if match + if not target.getID then return end -- units have no getID! + local theID = tonumber(target:getID()) + if bombRange.myStatics[theID] then + return + end + + -- look through the tracked weapons for a match + local filtered = {} + local hasfound = false + for idx, b in pairs (bombRange.bombs) do + if b.weapon == weapon then + hasfound = true + -- update b to current position and velocity + b.pos = weapon:getPoint() + b.v = weapon:getVelocity() + bombRange.impacted(b, target) + else + table.insert(filtered, b) + end + end + if hasfound then + bombRange.bombs = filtered + end +end + +function bombRange:onEvent(event) + if not event.initiator then return end + local theUnit = event.initiator + + if event.id == 2 then -- hit + if not event.weapon then return end + bombRange.suspectedHit(event.weapon, event.target) + return + end + + local uName = nil + local pName = nil + if theUnit.getPlayerName and theUnit:getPlayerName() ~= nil then + uName = theUnit:getName() + pName = theUnit:getPlayerName() + else return end + + if event.id == 1 then -- shot event, from player + if not event.weapon then return end + local uComms = bombRange.unitComms[uName] + if bombRange.mustCheckIn and (not uComms.checkedIn) then + if bombRange.verbose then + trigger.action.outText("+++bRng: Player <" .. pName .. "> not checked in.", 30) + end + return + end + local w = event.weapon + local b = {} + local bName = w:getName() + b.name = bName + b.type = w:getTypeName() + -- may need to verify type: how do we handle clusters or flares? + b.pos = w:getPoint() + b.v = w:getVelocity() + b.pName = pName + b.uName = uName + b.uType = theUnit:getTypeName() + b.gID = theUnit:getGroup():getID() + b.weapon = w + b.released = timer.getTime() + b.relPos = b.pos + table.insert(bombRange.bombs, b) + if not bombRange.tracking then + timer.scheduleFunction(bombRange.updateBombs, {}, timer.getTime() + 1/bombRange.ups) + bombRange.tracking = true + if bombRange.verbose then + trigger.action.outText("+++bRng: start tracking.", 30) + end + end + if bombRange.verbose then + trigger.action.outText("+++bRng: Player <" .. pName .. "> fired a <" .. b.type .. ">, named <" .. b.name .. ">", 30) + end + end + + if event.id == 15 then + bombRange.initCommsForUnit(theUnit) + end + +end + +-- +-- Update +-- +function bombRange.impacted(weapon, target) + local targetName = nil + local ipos = weapon.pos -- default to weapon location + if target then + ipos = target:getPoint() + targetName = target:getDesc() + if targetName then targetName = targetName.displayName end + if not targetName then targetName = target:getTypeName() end + else + -- not an object hit, interpolate the impact point on ground: + -- calculate impact point. we use the linear equation + -- pos.y + t*velocity.y - height = 1 (height above gnd) and solve for t + local h = land.getHeight({x=weapon.pos.x, y=weapon.pos.z}) - bombRange.dh -- dh m above gnd + local t = (h-weapon.pos.y) / weapon.v.y + -- having t, we project location using pos and vel + -- impactpos = pos + t * velocity + local imod = dcsCommon.vMultScalar(weapon.v, t) + ipos = dcsCommon.vAdd(weapon.pos, imod) -- calculated impact point + end + + -- see if inside a range + if #bombRange.ranges < 1 then + trigger.action.outText("+++bRng: No Bomb Ranges detected!") + return -- no need to update anything + end + local minDist = math.huge + local theRange = nil + for idx, theZone in pairs(bombRange.ranges) do + local p = theZone:getPoint() + local dist = dcsCommon.distFlat(p, ipos) + if dist < minDist then + minDist = dist + theRange = theZone + end + end + if not theRange then + trigger.action.outText("+++bRng: nil on eval. skipping.", 30) + return + end + if minDist > theRange.clipDist then + -- no taget zone inside clip dist. disregard this one, too far off + if bombRange.reportLongMisses then + trigger.action.outTextForGroup(weapon.gID, "Impact of <" .. weapon.type .. "> released by <" .. weapon.pName .. "> outside bomb range and disregarded.", 30) + end + return + end + + if theRange.smokeHits then + trigger.action.smoke(ipos, theRange.smokeColor) + end + + if (not target) and theRange.flagHits then -- only ground imparts are flagged + local cty = dcsCommon.getACountryForCoalition(0) -- some neutral county + local p = {x=ipos.x, y=ipos.z} + local theStaticData = dcsCommon.createStaticObjectData(dcsCommon.uuid(weapon.type .. " impact"), theRange.flagType) + dcsCommon.moveStaticDataTo(theStaticData, p.x, p.y) + local theObject = coalition.addStaticObject(cty, theStaticData) + end + + if theRange.reporter and theRange.details then + local t = math.floor((timer.getTime() - weapon.released) * 10) / 10 + local v = math.floor(dcsCommon.vMag(weapon.v)) + local tDist = dcsCommon.dist(ipos, weapon.relPos)/1000 + tDist = math.floor(tDist*100) /100 + trigger.action.outTextForGroup(weapon.gID, "impact of " .. weapon.type .. " released by " .. weapon.pName .. " from " .. weapon.uType .. " after traveling " .. tDist .. " km in " .. t .. " sec, impact velocity at impact is " .. v .. " m/s!", 30) + end + + local msg = "" + if theRange:pointInZone(ipos) then + local percentage = 0 + if theRange.isPoly then + percentage = 100 + else + percentage = 1 - (minDist / theRange.radius) + percentage = math.floor(percentage * 100) + end + msg = "INSIDE target area" + if theRange.reportName then msg = msg .. " " .. theRange.name end + if (not targetName) and theRange.details then msg = msg .. ", off-center by " .. math.floor(minDist *10)/10 .. " m" end + if targetName then msg = msg .. ", hit on " .. targetName end + + if not theRange.usePercentage then + percentage = 100 + else + msg = msg .. " (Quality " .. percentage .."%)" + end + + if theRange.hitOut then + theZone:pollFlag(theRange.hitOut, theRange.method) + end + + bombRange.addImpactForWeapon(weapon, true, percentage) + else + msg = "Outside target area" + if theRange.reportName then msg = msg .. " " .. theRange.name end + if theRange.details then msg = msg .. "(off-center by " .. math.floor(minDist *10)/10 .. " m)" end + msg = msg .. ", no hit." + bombRange.addImpactForWeapon(weapon, false, 0) + end + if theRange.reporter then + trigger.action.outTextForGroup(weapon.gID,msg , 30) + end + +end + +function bombRange.updateBombs() + + local filtered = {} + for idx, theWeapon in pairs(bombRange.bombs) do + if Weapon.isExist(theWeapon.weapon) then + -- update pos and vel + theWeapon.pos = theWeapon.weapon:getPoint() + theWeapon.v = theWeapon.weapon:getVelocity() + table.insert(filtered, theWeapon) + else + -- interpolate the impact position from last position + bombRange.impacted(theWeapon) + end + end + + bombRange.bombs = filtered + if #filtered > 0 then + timer.scheduleFunction(bombRange.updateBombs, {}, timer.getTime() + 1/bombRange.ups) + bombRange.tracking = true + else + bombRange.tracking = false + if bombRange.verbose then + trigger.action.outText("+++bRng: stopped tracking.", 30) + end + end +end + +-- +-- load & save data +-- +function bombRange.saveData() + local theData = {} + -- save current score list. simple clone + local theStats = dcsCommon.clone(bombRange.playerData) + theData.theStats = theStats + return theData +end + +function bombRange.loadData() + if not persistence then return end + local theData = persistence.getSavedDataForModule("bombRange") + if not theData then + if bombRange.verbose then + trigger.action.outText("+++bRng: no save date received, skipping.", 30) + end + return + end + + local theStats = theData.theStats + bombRange.playerData = theStats +end + + +-- +-- Config & Start +-- +function bombRange.readConfigZone() + bombRange.name = "bombRangeConfig" + local theZone = cfxZones.getZoneByName("bombRangeConfig") + if not theZone then + theZone = cfxZones.createSimpleZone("bombRangeConfig") + end + local theSet = theZone:getStringFromZoneProperty("filterTypes", "house2arm, Black_Tyre_RF, Red_Flag") + theSet = dcsCommon.splitString(theSet, ",") + bombRange.filterTypes = dcsCommon.trimArray(theSet) + bombRange.reportLongMisses = theZone:getBoolFromZoneProperty("reportLongMisses", false) + bombRange.mustCheckIn = theZone:getBoolFromZoneProperty("mustCheckIn", false) + bombRange.ups = theZone:getNumberFromZoneProperty("ups", 20) + bombRange.menuTitle = theZone:getStringFromZoneProperty("menuTitle","Contact BOMB RANGE") + if theZone:hasProperty("signIn!") then + bombRange.signIn = theZone:getStringFromZoneProperty("signIn!", 30) + end + if theZone:hasProperty("signOut!") then + bombRange.signOut = theZone:getStringFromZoneProperty("signOut!", 30) + end + bombRange.method = theZone:getStringFromZoneProperty("method", "inc") + bombRange.verbose = theZone.verbose +end + + +function bombRange.start() + if not dcsCommon.libCheck("cfx bombRange", + bombRange.requiredLibs) then + return false + end + + -- read config + bombRange.readConfigZone() + + -- collect all wp target zones + local attrZones = cfxZones.getZonesWithAttributeNamed("bombRange") + + for k, aZone in pairs(attrZones) do + bombRange.createRange(aZone) -- process attribute and add to zone + bombRange.addRange(aZone) -- remember it so we can smoke it + end + + -- load data + if persistence then + -- sign up for persistence + callbacks = {} + callbacks.persistData = bombRange.saveData + persistence.registerModule("bombRange", callbacks) + -- now load my data + bombRange.loadData() + end + + -- add event handler + world.addEventHandler(bombRange) + + return true +end + +if not bombRange.start() then + trigger.action.outText("cf/x Bomb Range aborted: missing libraries", 30) + bombRange = nil +end + +-- +-- add persistence +-- \ No newline at end of file diff --git a/modules/cfxNDB.lua b/modules/cfxNDB.lua index e76661a..f9de988 100644 --- a/modules/cfxNDB.lua +++ b/modules/cfxNDB.lua @@ -1,5 +1,5 @@ cfxNDB = {} -cfxNDB.version = "1.2.1" +cfxNDB.version = "1.3.0" --[[-- cfxNDB: @@ -26,6 +26,7 @@ cfxNDB.version = "1.2.1" - update only when moving and delta > maxDelta - zone-local verbosity support - better config defaulting + 1.3.0 - dmlZones --]]-- @@ -104,42 +105,42 @@ function cfxNDB.stopNDB(theNDB) end function cfxNDB.createNDBWithZone(theZone) - theZone.freq = cfxZones.getNumberFromZoneProperty(theZone, "NDB", 124) -- in MHz + theZone.freq = theZone:getNumberFromZoneProperty("NDB", 124) -- in MHz -- convert MHz to Hz theZone.freq = theZone.freq * 1000000 -- Hz - theZone.fm = cfxZones.getBoolFromZoneProperty(theZone, "fm", false) - theZone.ndbSound = cfxZones.getStringFromZoneProperty(theZone, "soundFile", "") - theZone.power = cfxZones.getNumberFromZoneProperty(theZone, "watts", cfxNDB.power) + theZone.fm = theZone:getBoolFromZoneProperty("fm", false) + theZone.ndbSound = theZone:getStringFromZoneProperty("soundFile", "") + theZone.power = theZone:getNumberFromZoneProperty("watts", cfxNDB.power) theZone.loop = true -- always. NDB always loops -- UNSUPPORTED refresh. Although read individually, it only works -- when LARGER than module's refresh. - theZone.ndbRefresh = cfxZones.getNumberFromZoneProperty(theZone, "ndbRefresh", cfxNDB.refresh) -- only used if linked + theZone.ndbRefresh = theZone:getNumberFromZoneProperty("ndbRefresh", cfxNDB.refresh) -- only used if linked theZone.ndbRefreshTime = timer.getTime() + theZone.ndbRefresh -- only used with linkedUnit, but set up nonetheless -- paused - theZone.paused = cfxZones.getBoolFromZoneProperty(theZone, "paused", false) + theZone.paused = theZone:getBoolFromZoneProperty("paused", false) -- watchflags - theZone.ndbTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "triggerMethod", "change") - if cfxZones.hasProperty(theZone, "ndbTriggerMethod") then - theZone.ndbTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "ndbTriggerMethod", "change") + theZone.ndbTriggerMethod = theZone:getStringFromZoneProperty( "triggerMethod", "change") + if theZone:hasProperty("ndbTriggerMethod") then + theZone.ndbTriggerMethod = theZone:getStringFromZoneProperty("ndbTriggerMethod", "change") end -- on/offf query flags - if cfxZones.hasProperty(theZone, "on?") then - theZone.onFlag = cfxZones.getStringFromZoneProperty(theZone, "on?", "none") + if theZone:hasProperty("on?") then + theZone.onFlag = theZone:getStringFromZoneProperty("on?", "none") end if theZone.onFlag then - theZone.onFlagVal = cfxZones.getFlagValue(theZone.onFlag, theZone) -- trigger.misc.getUserFlag(theZone.onFlag) -- save last value + theZone.onFlagVal = theZone:getFlagValue(theZone.onFlag) -- save last value end - if cfxZones.hasProperty(theZone, "off?") then - theZone.offFlag = cfxZones.getStringFromZoneProperty(theZone, "off?", "none") + if theZone:hasProperty("off?") then + theZone.offFlag = theZone:getStringFromZoneProperty("off?", "none") end if theZone.offFlag then - theZone.offFlagVal = cfxZones.getFlagValue(theZone.offFlag, theZone) --trigger.misc.getUserFlag(theZone.offFlag) -- save last value + theZone.offFlagVal = theZone:getFlagValue(theZone.offFlag) -- save last value end -- start it @@ -170,7 +171,7 @@ function cfxNDB.update() if not theNDB.lastLoc then cfxNDB.startNDB(theNDB) -- never was started else - local loc = cfxZones.getPoint(theNDB) -- y === 0 + local loc = theNDB:getPoint() -- y === 0 loc.y = land.getHeight({x = loc.x, y = loc.z}) -- get y from land local delta = dcsCommon.dist(loc, theNDB.lastLoc) if delta > cfxNDB.maxDist then @@ -182,17 +183,15 @@ function cfxNDB.update() end -- now check triggers to start/stop - if cfxZones.testZoneFlag(theNDB, theNDB.onFlag, theNDB.ndbTriggerMethod, "onFlagVal") then + if theNDB:testZoneFlag(theNDB.onFlag, theNDB.ndbTriggerMethod, "onFlagVal") then -- yupp, trigger start cfxNDB.startNDB(theNDB) end - - if cfxZones.testZoneFlag(theNDB, theNDB.offFlag, theNDB.ndbTriggerMethod, "offFlagVal") then + if theNDB:testZoneFlag(theNDB.offFlag, theNDB.ndbTriggerMethod, "offFlagVal") then -- yupp, trigger start cfxNDB.stopNDB(theNDB) end - end end @@ -205,10 +204,10 @@ function cfxNDB.readConfig() theZone = cfxZones.createSimpleZone("ndbConfig") end - cfxNDB.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) - cfxNDB.ndbRefresh = cfxZones.getNumberFromZoneProperty(theZone, "ndbRefresh", 10) + cfxNDB.verbose = theZone.verbose + cfxNDB.ndbRefresh = theZone:getNumberFromZoneProperty("ndbRefresh", 10) - cfxNDB.maxDist = cfxZones.getNumberFromZoneProperty(theZone, "maxDist", 50) -- max 50m error for movement + cfxNDB.maxDist = theZone:getNumberFromZoneProperty("maxDist", 50) -- max 50m error for movement if cfxNDB.verbose then trigger.action.outText("***ndb: read config", 30) diff --git a/modules/cfxObjectSpawnZones.lua b/modules/cfxObjectSpawnZones.lua index 126b338..43a536e 100644 --- a/modules/cfxObjectSpawnZones.lua +++ b/modules/cfxObjectSpawnZones.lua @@ -1,5 +1,5 @@ cfxObjectSpawnZones = {} -cfxObjectSpawnZones.version = "1.3.1" +cfxObjectSpawnZones.version = "2.0.0" cfxObjectSpawnZones.requiredLibs = { "dcsCommon", -- common is of course needed for everything -- pretty stupid to check for this since we @@ -8,31 +8,33 @@ cfxObjectSpawnZones.requiredLibs = { } cfxObjectSpawnZones.ups = 1 cfxObjectSpawnZones.verbose = false --- --- Zones that conform with this requirements spawn toops automatically --- *** DOES NOT EXTEND ZONES *** --- --- version history --- 1.0.0 - based on 1.4.6 version from cfxSpawnZones --- 1.1.0 - uses linkedUnit, so spawnming can occur on ships --- make sure you also enable useOffset to place the --- statics away from the center of the ship --- 1.1.1 - also processes paused flag --- - despawnRemaining(spawner) --- 1.1.2 - autoRemove option re-installed --- - added possibility to autoUnlink --- 1.1.3 - ME-triggered flag via f? and triggerFlag --- 1.1.4 - activate?, pause? attributes --- 1.1.5 - spawn?, spawnObjects? synonyms --- 1.2.0 - DML flag upgrade --- 1.2.1 - config zone --- - autoLink bug (zone instead of spawner accessed) --- 1.3.0 - better synonym handling --- - useDelicates link to delicate when spawned --- - spawned single and multi-objects can be made delicates --- 1.3.1 - baseName can be set to zone's name by giving "*" --- 1.3.2 - delicateName supports '*' to refer to own zone +--[[-- + Zones that conform with this requirements spawn objects automatically + *** DOES NOT EXTEND ZONES *** + version history + 1.0.0 - based on 1.4.6 version from cfxSpawnZones + 1.1.0 - uses linkedUnit, so spawnming can occur on ships + make sure you also enable useOffset to place the + statics away from the center of the ship + 1.1.1 - also processes paused flag + - despawnRemaining(spawner) + 1.1.2 - autoRemove option re-installed + - added possibility to autoUnlink + 1.1.3 - ME-triggered flag via f? and triggerFlag + 1.1.4 - activate?, pause? attributes + 1.1.5 - spawn?, spawnObjects? synonyms + 1.2.0 - DML flag upgrade + 1.2.1 - config zone + - autoLink bug (zone instead of spawner accessed) + 1.3.0 - better synonym handling + - useDelicates link to delicate when spawned + - spawned single and multi-objects can be made delicates + 1.3.1 - baseName can be set to zone's name by giving "*" + 1.3.2 - delicateName supports '*' to refer to own zone + 2.0.0 - dmlZones + +--]]-- -- respawn currently happens after theSpawns is deleted and cooldown seconds have passed cfxObjectSpawnZones.allSpawners = {} @@ -59,72 +61,70 @@ end function cfxObjectSpawnZones.createSpawner(inZone) local theSpawner = {} theSpawner.zone = inZone - theSpawner.name = inZone.name + theSpawner.name = inZone.name -- provide compat with cfxZones (not dmlZones, though) -- connect with ME if a trigger flag is given - if cfxZones.hasProperty(inZone, "f?") then - theSpawner.triggerFlag = cfxZones.getStringFromZoneProperty(inZone, "f?", "none") - elseif cfxZones.hasProperty(inZone, "spawn?") then - theSpawner.triggerFlag = cfxZones.getStringFromZoneProperty(inZone, "spawn?", "none") - elseif cfxZones.hasProperty(inZone, "spawnObjects?") then - theSpawner.triggerFlag = cfxZones.getStringFromZoneProperty(inZone, "spawnObjects?", "none") + if inZone:hasProperty("f?") then + theSpawner.triggerFlag = inZone:getStringFromZoneProperty("f?", "none") + elseif inZone:hasProperty("spawn?") then + theSpawner.triggerFlag = inZone:getStringFromZoneProperty("spawn?", "none") + elseif inZone:hasProperty("spawnObjects?") then + theSpawner.triggerFlag = inZone:getStringFromZoneProperty( "spawnObjects?", "none") end if theSpawner.triggerFlag then - theSpawner.lastTriggerValue = cfxZones.getFlagValue(theSpawner.triggerFlag, theSpawner) -- trigger.misc.getUserFlag(theSpawner.triggerFlag) + theSpawner.lastTriggerValue = cfxZones.getFlagValue(theSpawner.triggerFlag, theSpawner) end - if cfxZones.hasProperty(inZone, "activate?") then - theSpawner.activateFlag = cfxZones.getStringFromZoneProperty(inZone, "activate?", "none") + if inZone:hasProperty("activate?") then + theSpawner.activateFlag = inZone:getStringFromZoneProperty( "activate?", "none") theSpawner.lastActivateValue = cfxZones.getFlagValue(theSpawner.activateFlag, theSpawner) --trigger.misc.getUserFlag(theSpawner.activateFlag) end - if cfxZones.hasProperty(inZone, "pause?") then - theSpawner.pauseFlag = cfxZones.getStringFromZoneProperty(inZone, "pause?", "none") - theSpawner.lastPauseValue = cfxZones.getFlagValue(theSpawner.lastPauseValue, theSpawner) -- trigger.misc.getUserFlag(theSpawner.pauseFlag) + if inZone:hasProperty("pause?") then + theSpawner.pauseFlag = inZone:getStringFromZoneProperty("pause?", "none") + theSpawner.lastPauseValue = cfxZones.getFlagValue(theSpawner.lastPauseValue, theSpawner) end - theSpawner.types = cfxZones.getStringFromZoneProperty(inZone, "types", "White_Tyre") - local n = cfxZones.getNumberFromZoneProperty(inZone, "count", 1) -- DO NOT CONFUSE WITH OWN PROPERTY COUNT for unique names!!! + theSpawner.types = inZone:getStringFromZoneProperty("types", "White_Tyre") + local n = inZone:getNumberFromZoneProperty("count", 1) -- DO NOT CONFUSE WITH OWN PROPERTY COUNT for unique names!!! if n < 1 then n = 1 end -- sanity check. - theSpawner.numObj = n - theSpawner.country = cfxZones.getNumberFromZoneProperty(inZone, "country", 2) -- coalition2county(theSpawner.owner) - + theSpawner.country = inZone:getNumberFromZoneProperty("country", 2) theSpawner.rawOwner = coalition.getCountryCoalition(theSpawner.country) - theSpawner.baseName = cfxZones.getStringFromZoneProperty(inZone, "baseName", dcsCommon.uuid("objSpwn")) + theSpawner.baseName = inZone:getStringFromZoneProperty("baseName", "*") theSpawner.baseName = dcsCommon.trim(theSpawner.baseName) if theSpawner.baseName == "*" then theSpawner.baseName = inZone.name -- convenience shortcut end --cfxZones.getZoneProperty(inZone, "baseName") - theSpawner.cooldown = cfxZones.getNumberFromZoneProperty(inZone, "cooldown", 60) + theSpawner.cooldown = inZone:getNumberFromZoneProperty("cooldown", 60) theSpawner.lastSpawnTimeStamp = -10000 -- just init so it will always work - theSpawner.autoRemove = cfxZones.getBoolFromZoneProperty(inZone, "autoRemove", false) - theSpawner.autoLink = cfxZones.getBoolFromZoneProperty(inZone, "autoLink", true) + theSpawner.autoRemove = inZone:getBoolFromZoneProperty("autoRemove", false) + theSpawner.autoLink = inZone:getBoolFromZoneProperty("autoLink", true) - theSpawner.heading = cfxZones.getNumberFromZoneProperty(inZone, "heading", 0) - theSpawner.weight = cfxZones.getNumberFromZoneProperty(inZone, "weight", 0) + theSpawner.heading = inZone:getNumberFromZoneProperty("heading", 0) + theSpawner.weight = inZone:getNumberFromZoneProperty("weight", 0) if theSpawner.weight < 0 then theSpawner.weight = 0 end - theSpawner.isCargo = cfxZones.getBoolFromZoneProperty(inZone, "isCargo", false) + theSpawner.isCargo = inZone:getBoolFromZoneProperty("isCargo", false) if theSpawner.isCargo == true and theSpawner.weight < 100 then theSpawner.weight = 100 end - theSpawner.managed = cfxZones.getBoolFromZoneProperty(inZone, "managed", true) -- defaults to managed cargo + theSpawner.managed = inZone:getBoolFromZoneProperty("managed", true) -- defaults to managed cargo theSpawner.cdTimer = 0 -- used for cooldown. if timer.getTime < this value, don't spawn - -- theSpawner.cdStarted = false -- used to initiate cooldown when all items in theSpawns disappear + theSpawner.count = 1 -- used to create names, and count how many groups created theSpawner.theSpawns = {} -- all items that are spawned. re-spawn happens if they are all out - theSpawner.maxSpawns = cfxZones.getNumberFromZoneProperty(inZone, "maxSpawns", 1) - theSpawner.paused = cfxZones.getBoolFromZoneProperty(inZone, "paused", false) - theSpawner.requestable = cfxZones.getBoolFromZoneProperty(inZone, "requestable", false) + theSpawner.maxSpawns = inZone:getNumberFromZoneProperty("maxSpawns", 1) + theSpawner.paused = inZone:getBoolFromZoneProperty("paused", false) + theSpawner.requestable = inZone:getBoolFromZoneProperty("requestable", false) if theSpawner.requestable then theSpawner.paused = true end -- see if the spawn can be made brittle/delicte - if cfxZones.hasProperty(inZone, "useDelicates") then - theSpawner.delicateName = dcsCommon.trim(cfxZones.getStringFromZoneProperty(inZone, "useDelicates", "")) + if inZone:hasProperty("useDelicates") then + theSpawner.delicateName = dcsCommon.trim(inZone:getStringFromZoneProperty("useDelicates", "")) if theSpawner.delicateName == "*" then theSpawner.delicateName = inZone.name end end diff --git a/modules/cfxOwnedZones.lua b/modules/cfxOwnedZones.lua index 180e57a..afc0c8a 100644 --- a/modules/cfxOwnedZones.lua +++ b/modules/cfxOwnedZones.lua @@ -43,11 +43,7 @@ cfxOwnedZones.initialized = false owned zones is a module that manages conquerable zones and keeps a record of who owns the zone based on rules - - *** EXTENTDS ZONES ***, so compatible with cfxZones, pilotSafe (limited airframes), may conflict with FARPZones - - - owned zones are identified by the 'owner' property. It can be initially set to nothing (default), NEUTRAL, RED or BLUE + *** EXTENTDS ZONES *** when a zone changes hands, a callback can be installed to be told of that fact callback has the format (zone, newOwner, formerOwner) with zone being the Zone, and new owner and former owners diff --git a/modules/cfxZones.lua b/modules/cfxZones.lua index 5140281..9d7a568 100644 --- a/modules/cfxZones.lua +++ b/modules/cfxZones.lua @@ -1,5 +1,5 @@ cfxZones = {} -cfxZones.version = "4.0.7" +cfxZones.version = "4.0.9" -- cf/x zone management module -- reads dcs zones and makes them accessible and mutable @@ -56,7 +56,15 @@ cfxZones.version = "4.0.7" - processDynamicValueVU - 4.0.6 - hash mark forgotten QoL - 4.0.7 - drawZone() - +- 4.0.8 - markZoneWithObjects() + - cleanup + - markCenterWithObject + - markPointWithObject +- 4.0.9 - createPolyZone now correctly returns new zone + - createSimplePolyZone correctly passes location to createPolyZone + - createPolyZone now correctly sets zone.point + - createPolyZone now correctly inits dcsOrigin + - createCircleZone noew correctly inits dcsOrigin --]]-- -- @@ -304,7 +312,7 @@ function cfxZones.calculateZoneBounds(theZone) -- we may need to ascertain why we need ul, ur, ll, lr instead of just ll and ur -- store pRad theZone.pRad = pRad -- not sure we'll ever need that, but at least we have it --- trigger.action.outText("+++Zones: poly zone <" .. theZone.name .. "> has pRad = " .. pRad, 30) -- remember to remove me + else -- huston, we have a problem if cfxZones.verbose then @@ -319,28 +327,11 @@ function dmlZone:calculateZoneBounds() end function cfxZones.createPoint(x, y, z) -- bridge to dcsCommon, backward comp. - return dcsCommon.createPoint(x, y, z) ---[[-- - local newPoint = {} - newPoint.x = x - newPoint.y = y - newPoint.z = z - return newPoint --]]-- + return dcsCommon.createPoint(x, y, z) end function cfxZones.copyPoint(inPoint) -- bridge to dcsCommon, backward comp. return dcsCommon.copyPoint(inPoint) ---[[-- - local newPoint = {} - newPoint.x = inPoint.x - newPoint.y = inPoint.y - -- handle xz only - if inPoint.z then - newPoint.z = inPoint.z - else - newPoint.z = inPoint.y - end - return newPoint --]]-- end function cfxZones.createHeightCorrectedPoint(inPoint) -- this should be in dcsCommon @@ -535,7 +526,8 @@ function cfxZones.createCircleZone(name, x, z, radius) newZone.name = name newZone.radius = radius newZone.point = dcsCommon.createPoint(x, 0, z) - + newZone.dcsOrigin = dcsCommon.createPoint(x, 0, z) + -- props newZone.properties = {} @@ -552,8 +544,9 @@ function cfxZones.createSimplePolyZone(name, location, points, addToManaged) end if not location.x then location.x = 0 end if not location.z then location.z = 0 end + if not location.y then location.y = 0 end - local newZone = cfxZones.createPolyZone(name, points) + local newZone = cfxZones.createPolyZone(name, points, location) if addToManaged then cfxZones.addZoneToManagedZones(newZone) @@ -593,8 +586,11 @@ function cfxZones.createSimpleQuadZone(name, location, points, addToManaged) return cfxZones.createSimplePolyZone(name, location, points, addToManaged) end -function cfxZones.createPolyZone(name, poly) -- poly must be array of point type +function cfxZones.createPolyZone(name, poly, location) -- poly must be array of point type local newZone = dmlZone:new(nil) -- {} OOP compatibility + if not location then location = {x=0, y=0, z=0} end + newZone.point = dcsCommon.createPoint(location.x, 0, location.z) + newZone.dcsOrigin = dcsCommon.createPoint(location.x, 0, location.z) newZone.isCircle = false newZone.isPoly = true newZone.poly = {} @@ -612,10 +608,9 @@ function cfxZones.createPolyZone(name, poly) -- poly must be array of point type newZone.properties = {} cfxZones.calculateZoneBounds(newZone) + return newZone end - - function cfxZones.createRandomZoneInZone(name, inZone, targetRadius, entirelyInside) -- create a new circular zone with center placed inside inZone -- if entirelyInside is false, only the zone's center is guaranteed to be inside @@ -641,23 +636,6 @@ function cfxZones.createRandomZoneInZone(name, inZone, targetRadius, entirelyIns return newZone elseif inZone.isPoly then - -- we have a poly zone. the way we do this is simple: - -- generate random x, z with ranges of the bounding box - -- until the point falls within the polygon. - --[[ replaced by new code - - local newPoint = {} - local emergencyBrake = 0 - repeat - newPoint = cfxZones.createRandomPointInsideBounds(inZone.bounds) - emergencyBrake = emergencyBrake + 1 - if (emergencyBrake > 100) then - newPoint = cfxZones.copyPoint(inZone.Point) - trigger.action.outText("CreateZoneInZone: emergency brake for inZone" .. inZone.name, 10) - break - end - until cfxZones.isPointInsidePoly(newPoint, inZone.poly) - --]]-- local newPoint = cfxZones.createRandomPointInPolyZone(inZone) -- construct new zone local newZone = cfxZones.createCircleZone(name, newPoint.x, newPoint.z, targetRadius) @@ -833,7 +811,6 @@ function cfxZones.getZoneVolume(theZone) } return vol elseif (theZone.isPoly) then - --trigger.action.outText("zne: isPointInside: " .. theZone.name .. " is Polyzone!", 30) -- build the box volume, using the zone's bounds ll and ur points local lowerLeft = {} -- we build x = westerm y = southern, Z = alt @@ -870,17 +847,11 @@ end function cfxZones.declutterZone(theZone) if not theZone then return end local theVol = cfxZones.getZoneVolume(theZone) --- if theZone.verbose then --- dcsCommon.dumpVar2Str("vol", theVol) --- end world.removeJunk(theVol) end function dmlZone:declutterZone() local theVol = cfxZones.getZoneVolume(self) --- if self.verbose then --- dcsCommon.dumpVar2Str("vol", theVol) --- end world.removeJunk(theVol) end @@ -888,8 +859,7 @@ end -- units / groups in zone -- function cfxZones.allGroupsInZone(theZone, categ) -- categ is optional, must be code - -- warning: does not check for exiting! - --trigger.action.outText("Zone " .. theZone.name .. " radius " .. theZone.radius, 30) + -- warning: does not check for existing! local inZones = {} local coals = {0, 1, 2} -- all coalitions for idx, coa in pairs(coals) do @@ -908,8 +878,7 @@ function dmlZone:allGroupsInZone(categ) end function cfxZones.allGroupNamesInZone(theZone, categ) -- categ is optional, must be code - -- warning: does not check for exiting! - --trigger.action.outText("Zone " .. theZone.name .. " radius " .. theZone.radius, 30) + -- warning: does not check for existing! local inZones = {} local coals = {0, 1, 2} -- all coalitions for idx, coa in pairs(coals) do @@ -928,7 +897,7 @@ function dmlZone:allGroupNamesInZone(categ) end function cfxZones.allStaticsInZone(theZone, useOrigin) -- categ is optional, must be code - -- warning: does not check for exiting! + -- warning: does not check for existing! local inZones = {} local coals = {0, 1, 2} -- all coalitions for idx, coa in pairs(coals) do @@ -976,11 +945,9 @@ function cfxZones.isGroupPartiallyInZone(aGroup, aZone) if aUnit:isExist() and aUnit:getLife() > 1 then local p = aUnit:getPoint() local inzone, percent, dist = cfxZones.pointInZone(p, aZone) - if inzone then -- cfxZones.isPointInsideZone(p, aZone) then - --trigger.action.outText("zne: YAY <" .. aUnit:getName() .. "> IS IN " .. aZone.name, 30) + if inzone then return true end - --trigger.action.outText("zne: <" .. aUnit:getName() .. "> not in " .. aZone.name .. ", dist = " .. dist .. ", rad = ", aZone.radius, 30) end end return false @@ -1073,11 +1040,7 @@ function cfxZones.dumpZones(zoneTable) end trigger.action.outText("Zones end", 10) end ---[[-- moved to dcsCommon -function cfxZones.stringStartsWith(theString, thePrefix) - return theString:find(thePrefix) == 1 -end ---]]-- + function cfxZones.keysForTable(theTable) local keyset={} local n=0 @@ -1113,17 +1076,12 @@ end -- return all zones from the zone table that begin with string prefix -- function cfxZones.zonesStartingWithName(prefix, searchSet) - if not searchSet then searchSet = cfxZones.zones end - --- trigger.action.outText("Enter: zonesStartingWithName for " .. prefix , 30) local prefixZones = {} prefix = prefix:upper() -- all zones have UPPERCASE NAMES! THEY SCREAM AT YOU for name, zone in pairs(searchSet) do --- trigger.action.outText("testing " .. name:upper() .. " starts with " .. prefix , 30) if dcsCommon.stringStartsWith(name:upper(), prefix) then prefixZones[name] = zone -- note: ref copy! - --trigger.action.outText("zone with prefix <" .. prefix .. "> found: " .. name, 10) end end @@ -1396,19 +1354,6 @@ function dmlZone:closestUnitToZoneCenter(theUnits) return cfxZones.closestUnitToZoneCenter(theUnits, self) end ---[[ -function cfxZones.anyPlayerInZone(theZone) -- returns first player it finds - for pname, pinfo in pairs(cfxPlayer.playerDB) do - local playerUnit = pinfo.unit - if (cfxZones.unitInZone(playerUnit, theZone)) then - return true, playerUnit - end - end -- for all players - return false, nil -end - ---]]-- - -- grow zone function cfxZones.growZone() -- circular zones simply increase radius @@ -1776,14 +1721,6 @@ end function dmlZone:getFlagValue(theFlag) return cfxZones.getFlagValue(theFlag, self) end ---[[-- -function cfxZones.isMEFlag(inFlag) - -- do NOT use me - trigger.action.outText("+++zne: warning: deprecated isMEFlag", 30) - return true - -- returns true if inFlag is a pure positive number -end ---]]-- function cfxZones.verifyMethod(theMethod, theZone) local lMethod = string.lower(theMethod) @@ -2082,7 +2019,6 @@ function cfxZones.testZoneFlag(theZone, theFlagName, theMethod, latchName) return false, currVal end - --trigger.action.outText("+++Zne: about to test: c = " .. currVal .. ", l = " .. lastVal, 30) local testResult = cfxZones.testFlagByMethodForZone(currVal, lastVal, theMethod, theZone) -- update latch by method @@ -2197,12 +2133,10 @@ end function cfxZones.getZoneProperty(cZone, theKey) if not cZone then trigger.action.outText("+++zone: no zone in getZoneProperty", 30) --- breek.here.noew = 1 return nil end if not theKey then trigger.action.outText("+++zone: no property key in getZoneProperty for zone " .. cZone.name, 30) --- breakme.here = 1 return end @@ -2224,7 +2158,6 @@ end function cfxZones.getStringFromZoneProperty(theZone, theProperty, default) if not default then default = "" end --- local p = cfxZones.getZoneProperty(theZone, theProperty) -- OOP heavy duty test here local p = theZone:getZoneProperty(theProperty) if not p then return default end @@ -2378,7 +2311,6 @@ function cfxZones.hasProperty(theZone, theProperty) return false end return true --- return foundIt ~= nil end function dmlZone:hasProperty(theProperty) @@ -2539,30 +2471,22 @@ function dmlZone:getCoalitionFromZoneProperty(theProperty, default) end function cfxZones.getNumberFromZoneProperty(theZone, theProperty, default) ---TODO: trim string if not default then default = 0 end default = tonumber(default) if not default then default = 0 end -- enforce default numbner as well local p = cfxZones.getZoneProperty(theZone, theProperty) p = tonumber(p) if not p then p = default end --- if theZone.verbose then --- trigger.action.outText("+++zne: getNumberFromZoneProperty returns <" .. p .. "> for prop <" .. theProperty .. "> in zone <" .. theZone.name .. ">", 30) --- end return p end -function dmlZone:getNumberFromZoneProperty(theProperty, default) ---TODO: trim string +function dmlZone:getNumberFromZoneProperty(theProperty, default) if not default then default = 0 end default = tonumber(default) if not default then default = 0 end -- enforce default numbner as well local p = self:getZoneProperty(theProperty) p = tonumber(p) if not p then p = default end --- if self.verbose then --- trigger.action.outText("+++zne OOP: getNumberFromZoneProperty returns <" .. p .. "> for prop <" .. theProperty .. "> in zone <" .. self.name .. ">", 30) --- end return p end @@ -2883,7 +2807,6 @@ function cfxZones.processDynamicValues(inMsg, theZone, msgResponses) 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, " found, val = <" .. val .. ">, A = <" .. left .. ">, B = <" .. right .. ">", 30) local yesno = false -- see if unit exists local theUnit = Unit.getByName(val) @@ -3336,7 +3258,6 @@ function cfxZones.linkUnitToZone(theUnit, theZone, dx, dy) -- note: dy is really -- direction to zone theZone.uHdg = unitHeading -- original unit heading to turn other -- units if need be - --trigger.action.outText("Link setup: dx=<" .. dx .. ">, dy=<" .. dy .. "> unit original hdg = <" .. math.floor(57.2958 * unitHeading) .. ">", 30) end function dmlZone:linkUnitToZone(theUnit, dx, dy) -- note: dy is really Z, don't get confused!!!! @@ -3386,8 +3307,6 @@ function cfxZones.calcHeadingOffset(aZone, theUnit) -- in DCS, positive x is north (wtf?) and positive z is east local dy = (-aZone.rxy) * math.sin(zoneBearing) local dx = aZone.rxy * math.cos(zoneBearing) - - --trigger.action.outText("zone bearing is " .. math.floor(zoneBearing * 57.2958) .. " dx = <" .. dx .. "> , dy = <" .. dy .. ">", 30) return dx, -dy -- note: dy is z coord!!!! end @@ -3542,8 +3461,101 @@ function cfxZones.startMovingZones() end end +-- +-- marking zones +-- +function cfxZones.spreadNObjectsOverLine(theZone, n, objType, left, right, cty) -- leaves last position free + trigger.action.outText("left = " .. dcsCommon.point2text(left) .. ", right = " .. dcsCommon.point2text(right),30) + + local a = {x=left.x, y=left.z} + local b = {x=right.x, y=right.z} + local dir = dcsCommon.vSub(b,a) -- vector from left to right + local dirInc = dcsCommon.vMultScalar(dir, 1/n) + local count = 0 + local p = {x=left.x, y = left.z} + local baseName = dcsCommon.uuid(theZone.name) + while count < n do + local theStaticData = dcsCommon.createStaticObjectData(dcsCommon.uuid(theZone.name), objType) + dcsCommon.moveStaticDataTo(theStaticData, p.x, p.y) + local theObject = coalition.addStaticObject(cty, theStaticData) + p = dcsCommon.vAdd(p, dirInc) + count = count + 1 + end +end +function cfxZones.markZoneWithObjects(theZone, objType, qtrNum, markCenter, cty) -- returns set + if not objType then objType = "Black_Tyre_RF" end + if not qtrNum then qtrNum = 3 end -- +1 for number of marks per quarter + if not cty then cty = dcsCommon.getACountryForCoalition(0) end -- some neutral county + local p = theZone:getPoint() + local newObjects = {} + + if theZone.isPoly then + -- we place 4 * (qtrnum + 1) objects around the edge of the zone + -- we mark each poly along v-->v+1, placing ip and qtrNum additional points + local o = cfxZones.spreadNObjectsOverLine(theZone, qtrNum + 1, objType, theZone.poly[1], theZone.poly[2], cty) + local p = cfxZones.spreadNObjectsOverLine(theZone, qtrNum + 1, objType, theZone.poly[2], theZone.poly[3], cty) + local q = cfxZones.spreadNObjectsOverLine(theZone, qtrNum + 1, objType, theZone.poly[3], theZone.poly[4], cty) + local r = cfxZones.spreadNObjectsOverLine(theZone, qtrNum + 1, objType, theZone.poly[4], theZone.poly[1], cty) + o = dcsCommon.combineTables(o,p) + p = dcsCommon.combineTables(q,r) + newObjects = dcsCommon.combineTables(o,p) + + else + local numObjects = (qtrNum + 1) * 4 + local degrees = 3.14157 / 180 + local degreeIncrement = (360 / numObjects) * degrees + local currDegree = 0 + local radius = theZone.radius + for i=1, numObjects do + local ox = p.x + math.cos(currDegree) * radius + local oy = p.z + math.sin(currDegree) * radius -- note: z! + local theStaticData = dcsCommon.createStaticObjectData(dcsCommon.uuid(theZone.name), objType) + dcsCommon.moveStaticDataTo(theStaticData, ox, oy) + local theObject = coalition.addStaticObject(cty, theStaticData) + table.insert(newObjects, theObject) + currDegree = currDegree + degreeIncrement + end + end + + if markCenter then + -- also mark the center + local theObject = cfxZones.markPointWithObject(p, objType, cty) + table.insert(newObjects, theObject) + end + + return newObjects +end + +function dmlZone:markZoneWithObjects(objType, qtrNum, markCenter, cty) -- returns set + return cfxZones.markZoneWithObjects(self, objType, qtrNum, markCenter) +end + +function cfxZones.markCenterWithObject(theZone, objType, cty) -- returns object + local p = cfxZones.getPoint(theZone) + local theObject = cfxZones.markPointWithObject(theZone, p, objType, cty) + return theObject +end + +function dmlZone:markCenterWithObject(objType, cty) -- returns object + return cfxZones.markCenterWithObject(self, objType, cty) +end + +function cfxZones.markPointWithObject(theZone, p, theType, cty) -- returns object + if not cty then cty = dcsCommon.getACountryForCoalition(0) end + local ox = p.x + local oy = p.y + if p.z then oy = p.z end -- support vec 2 and vec 3 + local theStaticData = dcsCommon.createStaticObjectData(dcsCommon.uuid(theZone.name), theType) + dcsCommon.moveStaticDataTo(theStaticData, ox, oy) + local theObject = coalition.addStaticObject(cty, theStaticData) + return theObject +end + +function dmlZone:markPointWithObject(p, theType, cty) -- returns object + return cfxZones.markPointWithObject(self, p, theType, cty) +end -- -- =========== -- INIT MODULE diff --git a/modules/civAir.lua b/modules/civAir.lua index 0d4b740..f50b4a4 100644 --- a/modules/civAir.lua +++ b/modules/civAir.lua @@ -98,10 +98,10 @@ function civAir.readConfigZone() civAir.aircraftTypes = typeArray end - if theZone:hasProperty("ups") then +-- if theZone:hasProperty("ups") then civAir.ups = theZone:getNumberFromZoneProperty("ups", 0.05) if civAir.ups < .0001 then civAir.ups = 0.05 end - end +-- end if theZone:hasProperty("maxTraffic") then civAir.maxTraffic = theZone:getNumberFromZoneProperty( "maxTraffic", 10) @@ -109,15 +109,15 @@ function civAir.readConfigZone() civAir.maxTraffic = theZone:getNumberFromZoneProperty( "maxFlights", 10) end - if theZone:hasProperty("maxIdle") then +-- if theZone:hasProperty("maxIdle") then civAir.maxIdle = theZone:getNumberFromZoneProperty("maxIdle", 8 * 60) - end +-- end - if theZone:hasProperty("initialAirSpawns") then +-- if theZone:hasProperty("initialAirSpawns") then civAir.initialAirSpawns = theZone:getBoolFromZoneProperty( "initialAirSpawns", true) - end +-- end - civAir.verbose = theZone.verbose -- cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) + civAir.verbose = theZone.verbose end function civAir.processZone(theZone) diff --git a/modules/csarManager2.lua b/modules/csarManager2.lua index cfd0480..3cde9cb 100644 --- a/modules/csarManager2.lua +++ b/modules/csarManager2.lua @@ -1,5 +1,5 @@ csarManager = {} -csarManager.version = "2.2.6" +csarManager.version = "2.3.1" csarManager.verbose = false csarManager.ups = 1 @@ -65,6 +65,14 @@ csarManager.ups = 1 - 2.2.5 - manual freq for CSAR was off by a factor of 10 - Corrected - 2.2.6 - useFlare, now also launches a flare in addition to smoke - zone testing uses getPoint for zones, supports moving csar zones + - 2.3.0 - dmlZones + - onRoad attribute for CSAR mission Zones + - rndLoc support + - triggerMethod support + - 2.3.1 - addPrefix option + - delay asynch OK (message only) + - offset zone on randomized soldier + - smokeDist INTEGRATES AUTOMATICALLY WITH playerScore IF INSTALLED @@ -133,6 +141,9 @@ function csarManager.createDownedPilot(theMission, existingUnit) -- a radius of <1, this is only for ejecting pilots if newTargetZone.radius > 1 then aLocation, aHeading = dcsCommon.randomPointOnPerimeter(newTargetZone.radius / 2 + 3, newTargetZone.point.x, newTargetZone.point.z) + -- we now move the entire zone so it centers on unit + newTargetZone.point.x = aLocation.x + newTargetZone.point.z = aLocation.z else aLocation.x = newTargetZone.point.x aLocation.z = newTargetZone.point.z @@ -188,8 +199,10 @@ function csarManager.createCSARMissionData(point, theSide, freq, name, numCrew, end end if not inRadius then inRadius = csarManager.rescueRadius end - - newMission.name = "downed " .. name .. "-" .. csarManager.missionID -- make it uuid-capable + newMission.name = name .. "-" .. csarManager.missionID -- make it uuid-capable + if csarManager.addPrefix then + newMission.name = "downed " .. newMission.name + end newMission.zone = cfxZones.createSimpleZone(newMission.name, point, inRadius) --csarManager.rescueRadius) newMission.marker = mapMarker -- so it can be removed later newMission.isHot = false -- creating adversaries will make it hot, or when units are near. maybe implement a search later? @@ -468,15 +481,11 @@ function csarManager.heloLanded(theUnit) "Evacuees", theMassObject) msn = theMassObject.ref - -- csarManager.successMission(myName, base.name, msn) - -- to be done when we remove troopsOnBoard + end -- reset weight local totalMass = cargoSuper.calculateTotalMassFor(myName) trigger.action.setUnitInternalCargo(myName, totalMass) -- super recalcs --- trigger.action.outText("+++csar: delivered - set internal weight for " .. myName .. " to " .. totalMass, 30) - --- trigger.action.setUnitInternalCargo(myName, 10) -- 10 kg as empty conf.troopsOnBoard = {} -- empty out troops on board -- we do *not* return so we can pick up troops on -- a CSARBASE if they were dropped there @@ -521,6 +530,12 @@ function csarManager.heloLanded(theUnit) 30) didPickup = true; + local args = {} + args.theName = theMission.name + args.mySide = mySide + args.unitName = myName + timer.scheduleFunction(csarManager.asynchSuccess, args, timer.getTime() + 3) + csarManager.removeMission(theMission) table.insert(conf.troopsOnBoard, theMission) theMission.group:destroy() -- will shut up radio as well @@ -536,16 +551,24 @@ function csarManager.heloLanded(theUnit) theMassObject) end if didPickup then - trigger.action.outSoundForCoalition(mySide, csarManager.actionSound) -- "Quest Snare 3.wav") + local args = {} + args.mySide = mySide + timer.scheduleFunction(csarManager.asynchSound, args, timer.getTime() + 3) end -- reset unit's weight based on people on board local totalMass = cargoSuper.calculateTotalMassFor(myName) - -- WAS: trigger.action.setUnitInternalCargo(myName, 10 + #conf.troopsOnBoard * csarManager.pilotWeight) -- 10 kg as empty + per-unit time people trigger.action.setUnitInternalCargo(myName, totalMass) -- 10 kg as empty + per-unit time people --- trigger.action.outText("+++csar: set internal weight for " .. myName .. " to " .. totalMass, 30) end +function csarManager.asynchSuccess(args) + -- currently, we always say "OK", will check for fail later + trigger.action.outTextForCoalition(args.mySide, args.unitName .. " has loaded " .. args.theName .. "!", 30) +end + +function csarManager.asynchSound(args) + trigger.action.outSoundForCoalition(args.mySide, csarManager.actionSound) +end -- -- -- Helo took off @@ -747,7 +770,6 @@ function csarManager.doListCSARRequests(args) local point = theUnit:getPoint() local theSide = theUnit:getCoalition() - --trigger.action.outText("+++csar: ".. theUnit:getName() .." issued csar status request", 30) local report = "\nCrews requesting evacuation\n" local openMissions = csarManager.openMissionsForSide(theSide) @@ -772,11 +794,10 @@ function csarManager.doListCSARRequests(args) if #myBases < 1 then report = report .. "\n\nWARNING: NO CSAR BASES TO DELIVER EVACUEES TO" end - report = report .. "\n" trigger.action.outTextForGroup(conf.id, report, 30) - trigger.action.outSoundForGroup(conf.id, csarManager.actionSound) -- "Quest Snare 3.wav") + trigger.action.outSoundForGroup(conf.id, csarManager.actionSound) end function csarManager.redirectStatusCarrying(args) @@ -788,8 +809,6 @@ function csarManager.doStatusCarrying(args) local param = args[2] local theUnit = conf.unit - --trigger.action.outText("+++csar: ".. theUnit:getName() .." wants to know how their rescued troops are doing", 30) - -- build status report local report = "\nCrew Rescue Status:\n" if #conf.troopsOnBoard < 1 then @@ -809,7 +828,7 @@ function csarManager.doStatusCarrying(args) report = report .. "\n" trigger.action.outTextForGroup(conf.id, report, 30) - trigger.action.outSoundForGroup(conf.id, csarManager.actionSound) -- "Quest Snare 3.wav") + trigger.action.outSoundForGroup(conf.id, csarManager.actionSound) end function csarManager.redirectUnloadOne(args) @@ -825,7 +844,7 @@ function csarManager.unloadOne(args) local report = "NYI: unload one" if theUnit:inAir() then - report = "STRONGLY recommend we land first, Sir!" + report = "STRONGLY recommend we land first, sir!" trigger.action.outTextForGroup(conf.id, report, 30) trigger.action.outSoundForGroup(conf.id, csarManager.actionSound) -- "Quest Snare 3.wav") return @@ -961,22 +980,16 @@ end function csarManager.addCSARBase(aZone) local csarBase = {} csarBase.zone = aZone - ---[[-- - local bName = cfxZones.getStringFromZoneProperty(aZone, "CSARBASE", "XXX") - if bName == "XXX" then bName = aZone.name end - csarBase.name = cfxZones.getStringFromZoneProperty(aZone, "name", bName) ---]]-- - -- CSARBASE now carries the coalition in the CSARBASE attribute - csarBase.side = cfxZones.getCoalitionFromZoneProperty(aZone, "CSARBASE", 0) + -- CSARBASE carries the coalition in the CSARBASE attribute + csarBase.side = aZone:getCoalitionFromZoneProperty("CSARBASE", 0) -- backward-compatibility to older versions. -- will be deprecated - if cfxZones.hasProperty(aZone, "coalition") then - csarBase.side = cfxZones.getCoalitionFromZoneProperty(aZone, "CSARBASE", 0) + if aZone:hasProperty("coalition") then + csarBase.side = aZone:getCoalitionFromZoneProperty("CSARBASE", 0) end -- see if we have provided a name field, default zone name - csarBase.name = cfxZones.getStringFromZoneProperty(aZone, "name", aZone.name) + csarBase.name = aZone:getStringFromZoneProperty("name", aZone.name) table.insert(csarManager.csarBases, csarBase) @@ -1003,12 +1016,11 @@ function csarManager.getCSARBasesForSide(theSide) end return bases end --- + -- -- U P D A T E -- =========== -- --- -- -- updateCSARMissions: make sure evacuees are still alive @@ -1106,7 +1118,7 @@ function csarManager.update() -- every second -- also pop smoke if not popped already, or more than 5 minutes ago if csarManager.useSmoke and (timer.getTime() - csarMission.lastSmokeTime) >= 5 * 60 then local smokePoint = dcsCommon.randomPointOnPerimeter( - 50, csarMission.zone.point.x, csarMission.zone.point.z) --cfxZones.createHeightCorrectedPoint(csarMission.zone.point) + csarManager.smokeDist, csarMission.zone.point.x, csarMission.zone.point.z) --cfxZones.createHeightCorrectedPoint(csarMission.zone.point) -- trigger.action.smoke(smokePoint, 4 ) dcsCommon.markPointWithSmoke(smokePoint, csarManager.smokeColor) csarMission.lastSmokeTime = timer.getTime() @@ -1120,11 +1132,11 @@ function csarManager.update() -- every second local ep = evacuee:getPoint() d = dcsCommon.distFlat(uPoint, ep) d = math.floor(d * 10) / 10 - if d < csarManager.hoverRadius * 2 then + if d < csarManager.rescueTriggerRange * 0.5 then --csarManager.hoverRadius * 2 then local ownHeading = dcsCommon.getUnitHeadingDegrees(aUnit) local oclock = dcsCommon.clockPositionOfARelativeToB(ep, uPoint, ownHeading) .. " o'clock" -- log distance - local hoverMsg = "Closing on " .. csarMission.name .. ", " .. d * 3 .. "ft on your " .. oclock .. " o'clock" + local hoverMsg = "Closing on " .. csarMission.name .. ", " .. d * 1 .. "m on your " .. oclock .. " o'clock" if d < csarManager.hoverRadius then if (agl <= csarManager.hoverAlt) and (agl > 3) then @@ -1137,7 +1149,7 @@ function csarManager.update() -- every second hoverTime = timer.getTime() - hoverTime -- calculate number of seconds local remainder = math.floor(csarManager.hoverDuration - hoverTime) if remainder < 1 then remainder = 1 end - hoverMsg = "Steady... " .. d * 3 .. "ft to your " .. oclock .. " o'clock, winching... (" .. remainder .. ")" + hoverMsg = "Steady... " .. d * 1 .. "m to your " .. oclock .. " o'clock, winching... (" .. remainder .. ")" if hoverTime > csarManager.hoverDuration then -- we rescued the guy! hoverMsg = "We have " .. csarMission.name .. " safely on board!" @@ -1172,7 +1184,7 @@ function csarManager.update() -- every second trigger.action.outTextForGroup(uID, hoverMsg, 30, true) return -- only ever one winch op else -- too high for hover - hoverMsg = "Evacuee " .. d * 3 .. "ft on your " .. oclock .. " o'clock; land or descend to between 10 and 90 AGL for winching" + hoverMsg = "Evacuee " .. d * 1 .. "m on your " .. oclock .. " o'clock; land or descend to between 10 and 90 AGL for winching" csarMission.hoveringUnits[uName] = nil -- reset timer end else -- not inside hover dist @@ -1199,12 +1211,17 @@ function csarManager.update() -- every second -- check if their flag value has changed if theZone.startCSAR then -- this should always be true, but you never know - local currVal = cfxZones.getFlagValue(theZone.startCSAR, theZone) - if currVal ~= theZone.lastCSARVal then +-- local currVal = theZone:getFlagValue(theZone.startCSAR) +-- if currVal ~= theZone.lastCSARVal then + if theZone:testZoneFlag(theZone.startCSAR, theZone.triggerMethod, "lastCSARVal") then -- set up random point in zone - local mPoint = cfxZones.createRandomPointInZone(theZone) + local mPoint = theZone:getPoint() + if theZone.rndLoc then mPoint = theZone:createRandomPointInZone() end + if theZone.onRoad then + mPoint.x, mPoint.z = land.getClosestPointOnRoads('roads',mPoint.x, mPoint.z) + end local theMission = csarManager.createCSARMissionData( - mPoint, --cfxZones.getPoint(theZone), -- point + mPoint, theZone.csarSide, -- theSide theZone.csarFreq, -- freq theZone.csarName, -- name @@ -1214,7 +1231,7 @@ function csarManager.update() -- every second 0.1, --theZone.radius) -- radius nil) -- parashoo unit csarManager.addMission(theMission) - theZone.lastCSARVal = currVal + --theZone.lastCSARVal = currVal if csarManager.verbose then trigger.action.outText("+++csar: started CSAR mission " .. theZone.csarName, 30) end @@ -1235,23 +1252,9 @@ function csarManager.createCSARforUnit(theUnit, pilotName, radius, silent, score local coal = theUnit:getCoalition() local csarPoint = dcsCommon.randomPointInCircle(radius, radius/2, point.x, point.z) - - -- check the ground- water will kill the pilot - -- not any more! pilot can float - + csarPoint.y = csarPoint.z - local surf = land.getSurfaceType(csarPoint) - - --[[-- - if surf == 2 or surf == 3 then - if not silent then - trigger.action.outTextForCoalition(coal, "Bad chute! Bad chute! ".. pilotName .. " did not survive ejection out of their " .. theUnit:getTypeName(), 30) - trigger.action.outSoundForGroup(coal, csarManager.actionSound) -- "Quest Snare 3.wav") - end - return - end - --]]-- - + local surf = land.getSurfaceType(csarPoint) csarPoint.y = land.getHeight(csarPoint) -- when we get here, the terrain is ok, so let's drop the pilot @@ -1278,10 +1281,8 @@ function csarManager.createCSARForParachutist(theUnit, name) -- invoked with par -- create a CSAR mission now local theMission = csarManager.createCSARMissionData(pos, coa, nil, name, nil, nil, nil, 0.1, nil) csarManager.addMission(theMission) --- if not silent then trigger.action.outTextForCoalition(coa, "MAYDAY MAYDAY MAYDAY! ".. name .. " requesting extraction after eject!", 30) - trigger.action.outSoundForGroup(coa, csarManager.actionSound) -- "Quest Snare 3.wav") --- end + trigger.action.outSoundForGroup(coa, csarManager.actionSound) end -- @@ -1290,7 +1291,6 @@ end function csarManager.processCSARBASE() local csarBases = cfxZones.zonesWithProperty("CSARBASE") - -- now add all zones to my zones table, and init additional info -- from properties for k, aZone in pairs(csarBases) do @@ -1305,54 +1305,58 @@ end function csarManager.readCSARZone(theZone) -- zones have attribute "CSAR" -- gather data, and then create a mission from this - local mName = cfxZones.getStringFromZoneProperty(theZone, "CSAR", "Lt. Unknown") - if mName == "" then mName = theZone.name end - local theSide = cfxZones.getCoalitionFromZoneProperty(theZone, "coalition", 0) + local mName = theZone:getStringFromZoneProperty("CSAR", theZone.name) +-- if mName == "" then mName = theZone.name end + local theSide = theZone:getCoalitionFromZoneProperty("coalition", 0) theZone.csarSide = theSide theZone.csarName = mName -- now deprecating name attributes - if cfxZones.hasProperty(theZone, "name") then - theZone.csarName = cfxZones.getStringFromZoneProperty(theZone, "name", "") - elseif cfxZones.hasProperty(theZone, "csarName") then - theZone.csarName = cfxZones.getStringFromZoneProperty(theZone, "csarName", "") - elseif cfxZones.hasProperty(theZone, "pilotName") then - theZone.csarName = cfxZones.getStringFromZoneProperty(theZone, "pilotName", "") - elseif cfxZones.hasProperty(theZone, "victimName") then - theZone.csarName = cfxZones.getStringFromZoneProperty(theZone, "victimName", "") + if theZone:hasProperty("name") then + theZone.csarName = theZone:getStringFromZoneProperty("name", "") + elseif theZone:hasProperty("csarName") then + theZone.csarName = theZone:getStringFromZoneProperty("csarName", "") + elseif theZone:hasProperty("pilotName") then + theZone.csarName = theZone:getStringFromZoneProperty("pilotName", "") + elseif theZone:hasProperty("victimName") then + theZone.csarName = theZone:getStringFromZoneProperty("victimName", "") end - theZone.csarFreq = cfxZones.getNumberFromZoneProperty(theZone, "freq", 0) + theZone.csarFreq = theZone:getNumberFromZoneProperty("freq", 0) -- since freqs are set in 10kHz multiplier by DML -- we have to divide the feq given here by 10 theZone.csarFreq = theZone.csarFreq / 10 if theZone.csarFreq < 0.01 then theZone.csarFreq = nil end theZone.numCrew = 1 theZone.csarMapMarker = nil - theZone.timeLimit = cfxZones.getNumberFromZoneProperty(theZone, "timeLimit", 0) + theZone.timeLimit = theZone:getNumberFromZoneProperty("timeLimit", 0) if theZone.timeLimit == 0 then theZone.timeLimit = nil else theZone.timeLimit = timeLimit * 60 end - local deferred = cfxZones.getBoolFromZoneProperty(theZone, "deferred", false) + local deferred = theZone:getBoolFromZoneProperty("deferred", false) - if cfxZones.hasProperty(theZone, "in?") then - theZone.startCSAR = cfxZones.getStringFromZoneProperty(theZone, "in?", "*none") - theZone.lastCSARVal = cfxZones.getFlagValue(theZone.startCSAR, theZone) + if theZone:hasProperty("in?") then + theZone.startCSAR = theZone:getStringFromZoneProperty("in?", "*none") + theZone.lastCSARVal = theZone:getFlagValue(theZone.startCSAR) + elseif theZone:hasProperty("start?") then + theZone.startCSAR = theZone:getStringFromZoneProperty("start?", "*none") + theZone.lastCSARVal = theZone:getFlagValue(theZone.startCSAR) + elseif theZone:hasProperty("startCSAR?") then + theZone.startCSAR = theZone:getStringFromZoneProperty("startCSAR?", "*none") + theZone.lastCSARVal = theZone:getFlagValue(theZone.startCSAR) end - if cfxZones.hasProperty(theZone, "start?") then - theZone.startCSAR = cfxZones.getStringFromZoneProperty(theZone, "start?", "*none") - theZone.lastCSARVal = cfxZones.getFlagValue(theZone.startCSAR, theZone) - end - - if cfxZones.hasProperty(theZone, "startCSAR?") then - theZone.startCSAR = cfxZones.getStringFromZoneProperty(theZone, "startCSAR?", "*none") - theZone.lastCSARVal = cfxZones.getFlagValue(theZone.startCSAR, theZone) - end - - if cfxZones.hasProperty(theZone, "score") then - theZone.score = cfxZones.getNumberFromZoneProperty(theZone, "score", 100) + if theZone:hasProperty("score") then + theZone.score = theZone:getNumberFromZoneProperty("score", 100) end + theZone.triggerMethod = theZone:getStringFromZoneProperty("triggerMethod", "change") + theZone.rndLoc = theZone:getBoolFromZoneProperty("rndLoc", true) + theZone.onRoad = theZone:getBoolFromZoneProperty("onRoad", false) + if (not deferred) then - local mPoint = cfxZones.createRandomPointInZone(theZone) + local mPoint = theZone:getPoint() + if theZone.rndLoc then mPoint = theZone:createRandomPointInZone() end + if theZone.onRoad then + mPoint.x, mPoint.z = land.getClosestPointOnRoads('roads',mPoint.x, mPoint.z) + end local theMission = csarManager.createCSARMissionData( mPoint, theZone.csarSide, @@ -1379,13 +1383,10 @@ end function csarManager.processCSARZones() local csarBases = cfxZones.zonesWithProperty("CSAR") - -- now add all zones to my zones table, and init additional info -- from properties for k, aZone in pairs(csarBases) do - csarManager.readCSARZone(aZone) - end end @@ -1406,6 +1407,7 @@ function csarManager.installCallback(theCB) end function csarManager.readConfigZone() + csarManager.name = "csarManagerConfig" -- compat with cfxZones local theZone = cfxZones.getZoneByName("csarManagerConfig") if not theZone then if csarManager.verbose then @@ -1415,48 +1417,46 @@ function csarManager.readConfigZone() end csarManager.configZone = theZone -- save for flag banging compatibility - csarManager.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) + csarManager.verbose = theZone.verbose + csarManager.ups = theZone:getNumberFromZoneProperty("ups", 1) - csarManager.ups = cfxZones.getNumberFromZoneProperty(theZone, "ups", 1) - - csarManager.useSmoke = cfxZones.getBoolFromZoneProperty(theZone, "useSmoke", true) - csarManager.smokeColor = cfxZones.getSmokeColorStringFromZoneProperty(theZone, "smokeColor", "blue") + csarManager.useSmoke = theZone:getBoolFromZoneProperty("useSmoke", true) + csarManager.smokeColor = theZone:getSmokeColorStringFromZoneProperty("smokeColor", "blue") + csarManager.smokeDist = theZone:getNumberFromZoneProperty("smokeDist", 30) csarManager.smokeColor = dcsCommon.smokeColor2Num(csarManager.smokeColor) - csarManager.useFlare = cfxZones.getBoolFromZoneProperty(theZone, "useFlare", true) - csarManager.flareColor = cfxZones.getFlareColorStringFromZoneProperty(theZone, "flareColor", "red") + csarManager.useFlare = theZone:getBoolFromZoneProperty("useFlare", true) + csarManager.flareColor = theZone:getFlareColorStringFromZoneProperty("flareColor", "red") csarManager.flareColor = dcsCommon.flareColor2Num(csarManager.flareColor) - - if cfxZones.hasProperty(theZone, "csarRedDelivered!") then - csarManager.csarRedDelivered = cfxZones.getStringFromZoneProperty(theZone, "csarRedDelivered!", "*") + if theZone:hasProperty("csarRedDelivered!") then + csarManager.csarRedDelivered = theZone:getStringFromZoneProperty("csarRedDelivered!", "*") end - if cfxZones.hasProperty(theZone, "csarBlueDelivered!") then - csarManager.csarBlueDelivered = cfxZones.getStringFromZoneProperty(theZone, "csarBlueDelivered!", "*") + if theZone:hasProperty("csarBlueDelivered!") then + csarManager.csarBlueDelivered = theZone:getStringFromZoneProperty("csarBlueDelivered!", "*") end - if cfxZones.hasProperty(theZone, "csarDelivered!") then - csarManager.csarDelivered = cfxZones.getStringFromZoneProperty(theZone, "csarDelivered!", "*") - + if theZone:hasProperty("csarDelivered!") then + csarManager.csarDelivered = theZone:getStringFromZoneProperty("csarDelivered!", "*") end - csarManager.rescueRadius = cfxZones.getNumberFromZoneProperty(theZone, "rescueRadius", 70) --70 -- must land within 50m to rescue - csarManager.hoverRadius = cfxZones.getNumberFromZoneProperty(theZone, "hoverRadius", 30) -- 30 -- must hover within 10m of unit - csarManager.hoverAlt = cfxZones.getNumberFromZoneProperty(theZone, "hoverAlt", 40) -- 40 -- must hover below this alt - csarManager.hoverDuration = cfxZones.getNumberFromZoneProperty(theZone, "hoverDuration", 20) -- 20 -- must hover for this duration - csarManager.rescueTriggerRange = cfxZones.getNumberFromZoneProperty(theZone, "rescueTriggerRange", 2000) -- 2000 -- when the unit pops smoke and radios - csarManager.beaconSound = cfxZones.getStringFromZoneProperty(theZone, "beaconSound", "Radio_beacon_of_distress_on_121.ogg") --"Radio_beacon_of_distress_on_121,5_MHz.ogg" - csarManager.pilotWeight = cfxZones.getNumberFromZoneProperty(theZone, "pilotWeight", 120) -- 120 + csarManager.rescueRadius = theZone:getNumberFromZoneProperty( "rescueRadius", 70) + csarManager.hoverRadius = theZone:getNumberFromZoneProperty( "hoverRadius", 30) + csarManager.hoverAlt = theZone:getNumberFromZoneProperty("hoverAlt", 40) + csarManager.hoverDuration = theZone:getNumberFromZoneProperty( "hoverDuration", 20) + csarManager.rescueTriggerRange = theZone:getNumberFromZoneProperty("rescueTriggerRange", 2000) + csarManager.beaconSound = theZone:getStringFromZoneProperty( "beaconSound", "Radio_beacon_of_distress_on_121,5_MHz.ogg") + csarManager.pilotWeight = theZone:getNumberFromZoneProperty("pilotWeight", 120) - csarManager.rescueScore = cfxZones.getNumberFromZoneProperty(theZone, "rescueScore", 100) + csarManager.rescueScore = theZone:getNumberFromZoneProperty( "rescueScore", 100) - csarManager.actionSound = cfxZones.getStringFromZoneProperty(theZone, "actionSound", "Quest Snare 3.wav") - csarManager.vectoring = cfxZones.getBoolFromZoneProperty(theZone, "vectoring", true) + csarManager.actionSound = theZone:getStringFromZoneProperty( "actionSound", "Quest Snare 3.wav") + csarManager.vectoring = theZone:getBoolFromZoneProperty("vectoring", true) -- add own troop carriers - if cfxZones.hasProperty(theZone, "troopCarriers") then - local tc = cfxZones.getStringFromZoneProperty(theZone, "troopCarriers", "UH-1D") + if theZone:hasProperty("troopCarriers") then + local tc = theZone:getStringFromZoneProperty("troopCarriers", "UH-1D") tc = dcsCommon.splitString(tc, ",") csarManager.troopCarriers = dcsCommon.trimArray(tc) if csarManager.verbose then @@ -1467,6 +1467,8 @@ function csarManager.readConfigZone() end end + csarManager.addPrefix = theZone:getBoolFromZoneProperty("addPrefix", true) + if csarManager.verbose then trigger.action.outText("+++csar: read config", 30) end @@ -1536,5 +1538,5 @@ end -- allow any airfied to be csarsafe by default, no longer *requires* csarbase - -- support quad zones and optionally non-random placement + -- remove cfxPlayer dependency --]]-- \ No newline at end of file diff --git a/modules/dcsCommon.lua b/modules/dcsCommon.lua index 5fb8c21..e67eb2a 100644 --- a/modules/dcsCommon.lua +++ b/modules/dcsCommon.lua @@ -1,5 +1,5 @@ dcsCommon = {} -dcsCommon.version = "2.9.3" +dcsCommon.version = "2.9.5" --[[-- VERSION HISTORY 2.2.6 - compassPositionOfARelativeToB - clockPositionOfARelativeToB @@ -173,6 +173,9 @@ dcsCommon.version = "2.9.3" - new getCountriesForCoalition() 2.9.2 - updated event2text 2.9.3 - getAirbasesWhoseNameContains now supports category tables for filtering +2.9.4 - new bearing2degrees() +2.9.5 - distanceOfPointPToLineXZ(p, p1, p2) + --]]-- -- dcsCommon is a library of common lua functions @@ -845,6 +848,13 @@ dcsCommon.version = "2.9.3" return "North" end + function dcsCommon.bearing2degrees(inRad) + local degrees = inRad / math.pi * 180 + if degrees < 0 then degrees = degrees + 360 end + if degrees > 360 then degrees = degrees - 360 end + return degrees + end + function dcsCommon.bearing2compass(inrad) local bearing = math.floor(inrad / math.pi * 180) if bearing < 0 then bearing = bearing + 360 end @@ -977,6 +987,19 @@ dcsCommon.version = "2.9.3" -- note: no separate case for straight in front or behind end + -- Distance of point p to line defined by p1,p2 + -- only on XZ map + function dcsCommon.distanceOfPointPToLineXZ(p, p1, p2) + local x21 = p2.x - p1.x + local y10 = p1.z - p.z + local x10 = p1.x - p.x + local y21 = p2.z - p1.z + local numer = math.abs((x21*y10) - (x10 * y21)) + local denom = math.sqrt(x21 * x21 + y21 * y21) + local dist = numer/denom + return dist + end + function dcsCommon.randomDegrees() local degrees = math.random(360) * 3.14152 / 180 return degrees diff --git a/modules/delayFlags.lua b/modules/delayFlags.lua index fc1cebb..ddd944b 100644 --- a/modules/delayFlags.lua +++ b/modules/delayFlags.lua @@ -1,5 +1,5 @@ delayFlag = {} -delayFlag.version = "1.3.0" +delayFlag.version = "1.4.0" delayFlag.verbose = false delayFlag.requiredLibs = { "dcsCommon", -- always @@ -38,6 +38,8 @@ delayFlag.flags = {} - continueDelay - delayLeft 1.3.0 - persistence + 1.4.0 - dmlZones + - delayLeft# --]]-- @@ -64,70 +66,70 @@ end -- function delayFlag.createTimerWithZone(theZone) -- delay - theZone.delayMin, theZone.delayMax = cfxZones.getPositiveRangeFromZoneProperty(theZone, "timeDelay", 1) -- same as zone signature + theZone.delayMin, theZone.delayMax = theZone:getPositiveRangeFromZoneProperty("timeDelay", 1) -- same as zone signature if delayFlag.verbose or theZone.verbose then trigger.action.outText("+++dlyF: time delay is <" .. theZone.delayMin .. ", " .. theZone.delayMax .. "> seconds", 30) end -- watchflags: -- triggerMethod - theZone.delayTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "triggerMethod", "change") + theZone.delayTriggerMethod = theZone:getStringFromZoneProperty("triggerMethod", "change") - if cfxZones.hasProperty(theZone, "delayTriggerMethod") then - theZone.delayTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "delayTriggerMethod", "change") + if theZone:hasProperty("delayTriggerMethod") then + theZone.delayTriggerMethod = theZone:getStringFromZoneProperty("delayTriggerMethod", "change") end -- trigger flag - if cfxZones.hasProperty(theZone, "f?") then - theZone.triggerDelayFlag = cfxZones.getStringFromZoneProperty(theZone, "f?", "none") - end - - if cfxZones.hasProperty(theZone, "in?") then - theZone.triggerDelayFlag = cfxZones.getStringFromZoneProperty(theZone, "in?", "none") - end - - if cfxZones.hasProperty(theZone, "startDelay?") then - theZone.triggerDelayFlag = cfxZones.getStringFromZoneProperty(theZone, "startDelay?", "none") + if theZone:hasProperty("f?") then + theZone.triggerDelayFlag = theZone:getStringFromZoneProperty("f?", "none") + elseif theZone:hasProperty("in?") then + theZone.triggerDelayFlag = theZone:getStringFromZoneProperty("in?", "none") + elseif theZone:hasProperty("startDelay?") then + theZone.triggerDelayFlag = theZone:getStringFromZoneProperty("startDelay?", "none") end if theZone.triggerDelayFlag then - theZone.lastDelayTriggerValue = cfxZones.getFlagValue(theZone.triggerDelayFlag, theZone) -- trigger.misc.getUserFlag(theZone.triggerDelayFlag) -- save last value + theZone.lastDelayTriggerValue = theZone:getFlagValue(theZone.triggerDelayFlag) end - theZone.delayMethod = cfxZones.getStringFromZoneProperty(theZone, "method", "inc") + theZone.delayMethod = theZone:getStringFromZoneProperty("method", "inc") - if cfxZones.hasProperty(theZone, "delayMethod") then - theZone.delayMethod = cfxZones.getStringFromZoneProperty(theZone, "delayMethod", "inc") + if theZone:hasProperty("delayMethod") then + theZone.delayMethod = theZone:getStringFromZoneProperty( "delayMethod", "inc") end -- out flag - theZone.delayDoneFlag = cfxZones.getStringFromZoneProperty(theZone, "out!", "*") + theZone.delayDoneFlag = theZone:getStringFromZoneProperty("out!", "*") - if cfxZones.hasProperty(theZone, "delayDone!") then - theZone.delayDoneFlag = cfxZones.getStringFromZoneProperty(theZone, "delayDone!", "*") + if theZone:hasProperty("delayDone!") then + theZone.delayDoneFlag = theZone:getStringFromZoneProperty( "delayDone!", "*") end -- stop the press! - if cfxZones.hasProperty(theZone, "stopDelay?") then - theZone.triggerStopDelay = cfxZones.getStringFromZoneProperty(theZone, "stopDelay?", "none") - theZone.lastTriggerStopValue = cfxZones.getFlagValue(theZone.triggerStopDelay, theZone) + if theZone:hasProperty("stopDelay?") then + theZone.triggerStopDelay = theZone:getStringFromZoneProperty("stopDelay?", "none") + theZone.lastTriggerStopValue = theZone:getFlagValue(theZone.triggerStopDelay) end -- pause and continue - if cfxZones.hasProperty(theZone, "pauseDelay?") then - theZone.triggerPauseDelay = cfxZones.getStringFromZoneProperty(theZone, "pauseDelay?", "none") - theZone.lastTriggerPauseValue = cfxZones.getFlagValue(theZone.triggerPauseDelay, theZone) + if theZone:hasProperty("pauseDelay?") then + theZone.triggerPauseDelay = theZone:getStringFromZoneProperty("pauseDelay?", "none") + theZone.lastTriggerPauseValue = theZone:getFlagValue(theZone.triggerPauseDelay) end - if cfxZones.hasProperty(theZone, "continueDelay?") then - theZone.triggerContinueDelay = cfxZones.getStringFromZoneProperty(theZone, "continueDelay?", "none") - theZone.lastTriggerContinueValue = cfxZones.getFlagValue(theZone.triggerContinueDelay, theZone) + if theZone:hasProperty("continueDelay?") then + theZone.triggerContinueDelay = theZone:getStringFromZoneProperty("continueDelay?", "none") + theZone.lastTriggerContinueValue = theZone:getFlagValue(theZone.triggerContinueDelay) end -- timeInfo - theZone.delayTimeLeft = cfxZones.getStringFromZoneProperty(theZone, "delayLeft", "*cfxIgnored") + if theZone:hasProperty("delayLeft") then + theZone.delayTimeLeft = theZone:getStringFromZoneProperty("delayLeft", "*cfxIgnored") + else + theZone.delayTimeLeft = theZone:getStringFromZoneProperty("delayLeft#", "*cfxIgnored") + end -- init theZone.delayRunning = false @@ -136,7 +138,7 @@ function delayFlag.createTimerWithZone(theZone) theZone.timeLeft = -1 -- in seconds, always kept up to date -- but not really used - cfxZones.setFlagValue(theZone.delayTimeLeft, -1, theZone) + theZone:setFlagValue(theZone.delayTimeLeft, -1) end @@ -167,7 +169,7 @@ function delayFlag.startDelay(theZone) end theZone.timeLimit = timer.getTime() + delay - cfxZones.setFlagValue(theZone.delayTimeLeft, delay, theZone) + theZone:setFlagValue(theZone.delayTimeLeft, delay) end function delayFlag.pauseDelay(theZone) @@ -194,7 +196,7 @@ function delayFlag.update() if remaining < 0 then remaining = -1 end -- see if we need to stop - if cfxZones.testZoneFlag(aZone, aZone.triggerStopDelay, aZone.delayTriggerMethod, "lastTriggerStopValue") then + if aZone:testZoneFlag(aZone.triggerStopDelay, aZone.delayTriggerMethod, "lastTriggerStopValue") then aZone.delayRunning = false -- simply stop. if delayFlag.verbose or aZone.verbose then trigger.action.outText("+++dlyF: stopped delay " .. aZone.name, 30) @@ -202,7 +204,7 @@ function delayFlag.update() end - if cfxZones.testZoneFlag(aZone, aZone.triggerDelayFlag, aZone.delayTriggerMethod, "lastDelayTriggerValue") then + if aZone:testZoneFlag(aZone.triggerDelayFlag, aZone.delayTriggerMethod, "lastDelayTriggerValue") then if delayFlag.verbose or aZone.verbose then if aZone.delayRunning then trigger.action.outText("+++dlyF: re-starting timer " .. aZone.name, 30) @@ -216,7 +218,7 @@ function delayFlag.update() if not aZone.delayPaused then - if aZone.delayRunning and cfxZones.testZoneFlag(aZone, aZone.triggerPauseDelay, aZone.delayTriggerMethod, "lastTriggerPauseValue") then + if aZone.delayRunning and aZone:testZoneFlag( aZone.triggerPauseDelay, aZone.delayTriggerMethod, "lastTriggerPauseValue") then if delayFlag.verbose or aZone.verbose then trigger.action.outText("+++dlyF: pausing timer <" .. aZone.name .. "> with <" .. remaining .. "> remaining", 30) end @@ -232,14 +234,14 @@ function delayFlag.update() if delayFlag.verbose or aZone.verbose then trigger.action.outText("+++dlyF: banging on " .. aZone.delayDoneFlag, 30) end - cfxZones.pollFlag(aZone.delayDoneFlag, aZone.delayMethod, aZone) + aZone:pollFlag(aZone.delayDoneFlag, aZone.delayMethod) end end - cfxZones.setFlagValue(aZone.delayTimeLeft, remaining, aZone) + aZone:setFlagValue(aZone.delayTimeLeft, remaining) else -- we are paused. Check for 'continue' - if aZone.delayRunning and cfxZones.testZoneFlag(aZone, aZone.triggerContinueDelay, aZone.delayTriggerMethod, "lastTriggerContinueValue") then + if aZone.delayRunning and aZone:testZoneFlag( aZone.triggerContinueDelay, aZone.delayTriggerMethod, "lastTriggerContinueValue") then if delayFlag.verbose or aZone.verbose then trigger.action.outText("+++dlyF: continuing timer <" .. aZone.name .. "> with <" .. aZone.remainingTime .. "> seconds remaining", 30) end @@ -312,13 +314,10 @@ end function delayFlag.readConfigZone() local theZone = cfxZones.getZoneByName("delayFlagsConfig") if not theZone then - if delayFlag.verbose then - trigger.action.outText("+++dlyF: NO config zone!", 30) - end - return + theZone = cfxZones.createSimpleZone("delayFlagsConfig") end - delayFlag.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) + delayFlag.verbose = theZone.verbose -- cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) if delayFlag.verbose then trigger.action.outText("+++dlyF: read config", 30) diff --git a/modules/mxObjects.lua b/modules/mxObjects.lua index 9287d21..2506a1a 100644 --- a/modules/mxObjects.lua +++ b/modules/mxObjects.lua @@ -111,7 +111,7 @@ function mxObjects.showNClosestTextObjectsToUnit(n, theUnit, numbered) -- dist msg = msg .. " " .. dist .. units msg = msg .. "\n" -- add line feed - if mxObjects.doubleLine then msg = msg .. "\n" end + if mxObjects.doubleLine and (i < n) then msg = msg .. "\n" end end end return msg diff --git a/modules/ownAll.lua b/modules/ownAll.lua index 1063c2c..51bd25c 100644 --- a/modules/ownAll.lua +++ b/modules/ownAll.lua @@ -51,7 +51,7 @@ function ownAll.ownAllForZone(theZone) theZone:setFlagValue(theZone.totalNum, #filtered) end - theZone.ownershipUplink = theZone:getBoolFromZoneProperty("uplink", true) + theZone.ownershipUplink = theZone:getBoolFromZoneProperty("uplink", true) -- not documented local redNum, blueNum theZone.state, redNum, blueNum = ownAll.calcState(theZone) diff --git a/modules/radioMenus.lua b/modules/radioMenus.lua index 86923f1..70f61d0 100644 --- a/modules/radioMenus.lua +++ b/modules/radioMenus.lua @@ -1,5 +1,5 @@ radioMenu = {} -radioMenu.version = "2.1.0" +radioMenu.version = "2.1.1" radioMenu.verbose = false radioMenu.ups = 1 radioMenu.requiredLibs = { @@ -31,6 +31,7 @@ radioMenu.menus = {} ackA, ackB, ackC, ackD attributes valA-D now define full method, not just values full wildcard support for ack and cooldown + 2.1.1 - outMessage now works correctly --]]-- function radioMenu.addRadioMenu(theZone) @@ -355,7 +356,7 @@ function radioMenu.radioOutMsg(ack, gid, theZone) -- group processing. only if gid>0 and cfxMX local theMsg = ack if (gid > 0) and cfxMX then - local gName = cfxMX.cfxMX.groupNamesByID[gid] + local gName = cfxMX.groupNamesByID[gid] theMsg = theMsg:gsub("", gName) end diff --git a/tutorial & demo missions/demo - bombs away.miz b/tutorial & demo missions/demo - bombs away.miz new file mode 100644 index 0000000..b9c1c71 Binary files /dev/null and b/tutorial & demo missions/demo - bombs away.miz differ