mirror of
https://github.com/weyne85/DML.git
synced 2025-10-29 16:57:49 +00:00
441 lines
15 KiB
Lua
441 lines
15 KiB
Lua
tdz = {}
|
|
tdz.version = "1.0.3"
|
|
tdz.requiredLibs = {
|
|
"dcsCommon", -- always
|
|
"cfxZones", -- Zones, of course
|
|
}
|
|
--[[--
|
|
VERSION HISTORY
|
|
1.0.0 - Initial version
|
|
1.0.1 - visible
|
|
rwFill, rwFrame
|
|
tdzFill, tdzFrame
|
|
extend, expand
|
|
left, right
|
|
multiple zone support
|
|
hops detection improvement
|
|
helo attribute
|
|
1.0.2 - manual placement option
|
|
filters FARPs
|
|
1.0.3 - "manual" now defaults to false
|
|
|
|
--]]--
|
|
|
|
tdz.allTdz = {}
|
|
tdz.watchlist = {}
|
|
tdz.watching = false
|
|
tdz.timeoutAfter = 120 -- seconds.
|
|
--
|
|
-- rwy draw procs
|
|
--
|
|
function tdz.rotateXZPolyInRads(thePoly, rads)
|
|
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)
|
|
local negCtr = {x = -center.x, y = -center.y, z = -center.z}
|
|
tdz.translatePoly(thePoly, negCtr)
|
|
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.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
|
|
end
|
|
|
|
--
|
|
-- create a tdz
|
|
--
|
|
function tdz.createTDZ(theZone)
|
|
local p = theZone:getPoint()
|
|
local theBase = dcsCommon.getClosestAirbaseTo(p, 0) -- never get FARPS
|
|
theZone.base = theBase
|
|
theZone.baseName = theBase:getName()
|
|
theZone.helos = false
|
|
|
|
local nearestRwy = nil
|
|
-- see if this is a manually placed runway
|
|
if theZone:getBoolFromZoneProperty("manual", false) then
|
|
-- construct runway from trigger zone attributes
|
|
if theZone.verbose or tdz.verbose then
|
|
trigger.action.outText("+++TDZ: runway for <" .. theZone.name .. "> is manually placed", 30)
|
|
end
|
|
nearestRwy = {}
|
|
nearestRwy.length = theZone:getNumberFromZoneProperty("length", 2500)
|
|
nearestRwy.width = theZone:getNumberFromZoneProperty("width", 60)
|
|
local hdgRaw = theZone:getNumberFromZoneProperty("course", 0) -- in degrees
|
|
hdgRaw = hdgRaw * 0.0174533 -- rads
|
|
nearestRwy.course = hdgRaw * (-1) -- why? because DCS.
|
|
nearestRwy.position = theZone:getPoint(true)
|
|
theZone.baseName = theZone.name .. "(manual placement)"
|
|
else
|
|
-- get closest runway to TDZ
|
|
-- may get a bit hairy, so let's find a good way
|
|
local allRwys = theBase:getRunways()
|
|
|
|
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
|
|
end
|
|
local bearing = nearestRwy.course * (-1)
|
|
if bearing < 0 then bearing = bearing + math.pi * 2 end
|
|
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
|
|
|
|
if tdz.verbose or theZone.verbose then
|
|
trigger.action.outText("TDZ: <" .. theZone.name .. "> attached to airfield " .. theZone.baseName .. " RW main (LEFT) is " .. rwname .. "0", 30)
|
|
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
|
|
rwLen = rwLen + 2 * theZone:getNumberFromZoneProperty("extend", 0)
|
|
local rwWid = nearestRwy.width
|
|
rwWid = rwWid + 2 * theZone:getNumberFromZoneProperty("expand", 0)
|
|
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.visible = theZone:getBoolFromZoneProperty("visible", true)
|
|
theZone.rwP1 = p1
|
|
theZone.starts = theZone:getNumberFromZoneProperty("starts", 0)
|
|
theZone.ends = theZone:getNumberFromZoneProperty("ends", 610) -- m = 2000 ft
|
|
theZone.left = theZone:getBoolFromZoneProperty("left", true)
|
|
theZone.right = theZone:getBoolFromZoneProperty("right", true)
|
|
|
|
theZone.runwayZone = tdz.calcTDZone(theZone.name .. "-" .. rwname .. "main", pos, rwLen, rwWid, bearing)
|
|
theZone.rwFrame = theZone:getRGBAVectorFromZoneProperty("rwFrame", {0, 0, 0, 1}) -- black
|
|
theZone.rwFill = theZone:getRGBAVectorFromZoneProperty("rwFill", {0, 0, 0, 0}) -- nothing
|
|
if theZone.visible then
|
|
theZone.runwayZone:drawZone(theZone.rwFrame, theZone.rwFill)
|
|
end
|
|
local theTDZone = tdz.calcTDZone(theZone.name .. "-" .. rwname, pos, rwLen, rwWid, bearing, theZone.starts / rwLen, theZone.ends/rwLen)
|
|
|
|
theZone.tdzFrame = theZone:getRGBAVectorFromZoneProperty("tdzFrame", {0, 1, 0, 1}) -- green 100%
|
|
theZone.tdzFill = theZone:getRGBAVectorFromZoneProperty("tdzFill", {0, 1, 0, 0.25}) -- 25% green
|
|
if theZone.visible and theZone.left then
|
|
theTDZone:drawZone(theZone.tdzFrame, theZone.tdzFill)
|
|
end
|
|
|
|
theZone.normTDZone = theTDZone
|
|
theTDZone = tdz.calcTDZone(theZone.name .. "-" .. opName, pos, rwLen, rwWid, bearing + math.pi, theZone.starts / rwLen, theZone.ends/rwLen)
|
|
if theZone.visible and theZone.right then
|
|
theTDZone:drawZone(theZone.tdzFrame, theZone.tdzFill)
|
|
end
|
|
theZone.opTDZone = theTDZone
|
|
theZone.opBearing = bearing + math.pi
|
|
if theZone.opBearing > 2 * math.pi then theZone.opBearing = theZone.opBearing - math.pi * 2 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.
|
|
return
|
|
end
|
|
|
|
-- we may want to filter helicopters
|
|
|
|
-- see if we touched down inside of one of our watched zones
|
|
-- and the directionality (left = landing dir, right = opDir)
|
|
-- matches
|
|
local p = theUnit:getPoint()
|
|
local theGroup = theUnit:getGroup()
|
|
local cat = theGroup:getCategory() -- DCS 2.9: no issues with groups...
|
|
local gID = theGroup:getID()
|
|
local hdg = dcsCommon.getUnitHeading(theUnit)
|
|
local msg = ""
|
|
local theZone = nil
|
|
local opposite = false
|
|
local dHdg, dOpHdg
|
|
for idx, aRunway in pairs(tdz.allTdz) do
|
|
local theRunway = aRunway.runwayZone
|
|
local allowUnit = (cat ~= 1) or aRunway.helos -- 1 = helos
|
|
if allowUnit and theRunway:pointInZone(p) then -- touched down
|
|
dHdg = math.abs(aRunway.bearing - hdg) -- 0..Pi
|
|
dOpHdg = math.abs(aRunway.opBearing - hdg)
|
|
opposite = false
|
|
if tdz.verbose or aRunway.verbose then
|
|
trigger.action.outText("TDZ: landing inside <" .. aRunway.name .. ">, myHdg = <" .. math.floor(hdg * 57.2958) .. ">, dHdg = <" .. dHdg * 57.29 .. ">, dOpHdg = <" .. dOpHdg * 57.29 .. ">, rw = <" .. math.floor(aRunway.bearing * 57.2958) .. ">, rwOp = <" .. math.floor(aRunway.opBearing * 57.2958) .. ">", 30)
|
|
end
|
|
|
|
if dOpHdg < dHdg then
|
|
opposite = true
|
|
dHdg = dOpHdg
|
|
if tdz.verbose or aRunway.verbose then
|
|
trigger.action.outText("TDZ: landing inside <" .. aRunway.name .. ">, *OPPOSING*", 30)
|
|
end
|
|
else
|
|
if tdz.verbose or aRunway.verbose then
|
|
trigger.action.outText("TDZ: landing inside <" .. aRunway.name .. ">, ---INLINE---", 30)
|
|
end
|
|
end
|
|
-- see if directionality matches
|
|
if ((opposite == false) and aRunway.left) or
|
|
((opposite == true) and aRunway.right)
|
|
then
|
|
theZone = aRunway -- FOUND!
|
|
if theZone.touchDownFlag then
|
|
theZone.pollFlag(theZone.touchDownFlag, theZone.method)
|
|
end
|
|
trigger.action.outTextForGroup(gID, "Touchdown! Come to a FULL STOP for evaluation", 30)
|
|
else
|
|
if aRunway.verbose or tdz.verbose then
|
|
trigger.action.outText("TDZ: ignored touchdown in runway for zone <" .. aRunway.name .. ">, directionality filtered.", 30)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if not theZone then
|
|
if tdz.verbose then
|
|
trigger.action.outText("TDZ: no touchdown inside zones registered", 30)
|
|
end
|
|
return
|
|
end -- no landing eval zone hit
|
|
-- Warning: finds the LAST that matches inZone and left/right
|
|
|
|
-- 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
|
|
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? Directionality was already checked
|
|
local tdZone = theZone.normTDZone
|
|
if opposite 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()
|
|
local theZone = cfxZones.getZoneByName("tdzConfig")
|
|
if not theZone then
|
|
theZone = cfxZones.createSimpleZone("tdzConfig")
|
|
end
|
|
tdz.verbose = theZone.verbose
|
|
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
|