DML/modules/taxiPolice.lua
Christian Franz 52985a2d4c Version 1.2.6
TaxiPolice 1.0.0
Small bug fixes
2023-03-30 08:20:18 +02:00

379 lines
14 KiB
Lua

taxiPolice = {}
taxiPolice.version = "1.0.0"
taxiPolice.verbose = true
taxiPolice.ups = 1 -- checks per second
taxiPolice.requiredLibs = {
"dcsCommon", -- always
"cfxZones", -- Zones, of course
}
--[[--
Version History
1.0.0 - Initial version
--]]--
taxiPolice.speedLimit = 14 -- m/s . 14 m/s = 50 km/h, 10 m/s = 36 kmh
taxiPolice.triggerTime = 3 -- seconds until we register a speeding violation
taxiPolice.rwyLeeway = 5 -- meters on each side
taxiPolice.rwyExtend = 500 -- meters in front and at end
taxiPolice.airfieldMaxDist = 3000 -- radius around airfield in which we operate
taxiPolice.runways = {} -- indexed by airbase name, then by rwName
-- if nil, that base is not policed
taxiPolice.suspects = {} -- units that are currently behaving naughty
taxiPolice.tickets = {} -- number of warnings per player
taxiPolice.maxTickets = 3 -- number of tickes without retribution
taxiPolice.lastMessageTo = {} -- used to suppress messages if too soon
function taxiPolice.buildRunways()
local bases = world.getAirbases()
local mId = 0
for idb, aBase in pairs (bases) do -- i = 1, #base do
local name = aBase:getName()
local rny = aBase:getRunways()
-- Note that Airbase.Category values are not obtained by calling airbase:getCategory() - that calls Object.getCategory(airbase) and will always return the value Object.Category.BASE. Instead you need to use airbase:getDesc().category to obtain the Airbase.Category!
local cat = aBase:getDesc().category
if rny and (cat == 0) then
local runways = {}
for idx, rwy in pairs(rny) do -- j = 1, #rny do
-- calcualte quad that encloses taxiway
local points = {} -- quad
local init = rwy.position
local bearing = rwy.course * -1 -- "*-1 to make meaningful"
local rwName = bearing * 57.2958 -- rads to degree
if rwName < 0 then rwName = rwName + 360 end
rwName = math.floor(rwName / 10)
rwName = tostring(rwName)
-- calculate start and end point of RWY, "heading 0"
local radius = rwy.length/2 + taxiPolice.rwyExtend
local pStart = {y=0, x = init.x + radius, z = init.z }
local pEnd = {y=0, x = init.x - radius, z = init.z}
-- Build runway with width; at 0 heading (trivial case)
local width = rwy.width/2 + taxiPolice.rwyLeeway
local dz1 = width
local dz2 = - width
points[1] = {y = 0, x = pStart.x, z = pStart.z + dz1}
points[2] = {y = 0, x = pStart.x, z = pStart.z + dz2}
points[3] = {y = 0, x = pEnd.x, z = pEnd.z + dz2}
points[4] = {y = 0, x = pEnd.x, z = pEnd.z + dz1}
-- rotate RWY "0" to RW "bearing"
local poly = dcsCommon.rotatePoly3AroundVec3Rad(points, init, bearing)
mId = mId + 1
-- draw on map
if taxiPolice.verbose then
trigger.action.quadToAll(-1, mId, poly[1], poly[2], poly[3], poly[4], {0, 0, 0, 1}, {0, 0, 0, .5}, 3)
end
-- save runway under name
runways[rwName] = poly
-- build a 100x100 quad to show base's center
end
taxiPolice.runways[name] = runways
if taxiPolice.verbose then
-- mark center of airbase in a red 200x200 square
local points = {}
local pStart = aBase:getPoint()
local pEnd = aBase:getPoint()
local dx1 = 100
local dz1 = 100
local dx2 = -100
local dz2 = -100
points[1] = {y = 0, x = pStart.x + dx1, z = pStart.z + dz1}
points[2] = {y = 0, x = pStart.x + dx2, z = pStart.z + dz1}
points[3] = {y = 0, x = pEnd.x + dx2, z = pEnd.z + dz2}
points[4] = {y = 0, x = pEnd.x + dx1, z = pEnd.z + dz2}
mId = mId + 1
trigger.action.quadToAll(-1, mId, points[1], points[2], points[3], points[4], {1, 0, 0, 1}, {1, 0, 0, .5}, 3)
end
else
if taxiPolice.verbose then
trigger.action.outText("No runways proccing for base <" .. name .. ">, cat = <" .. cat .. ">", 30)
end
end
end
end
--
-- Checking and Policing
--
function taxiPolice.retributeAgainst(theUnit)
-- player did not learn.
local player = theUnit:getPlayerName()
trigger.action.outText("Player <" .. player .. "> behaves reckless and is being reprimanded", 30)
-- do some harsh stuff
local pGrp = theUnit:getGroup()
local gID = pGrp:getID()
trigger.action.outTextForGroup(gID, "We don't appreciate your behavior. Stop it NOW. Here's something to think about...", 30)
trigger.action.setUnitInternalCargo(theUnit:getName() , 1000000 ) -- add 1000t
end
function taxiPolice.checkUnit(theUnit, allAirfields)
if not theUnit.getPlayerName then return end
local theGroup = theUnit:getGroup()
local cat = theGroup:getCategory()
if cat ~= 0 then return end -- not a fixed wing, disregard
local player = theUnit:getPlayerName()
if not player then return end
local p = theUnit:getPoint()
p.y = 0
local base, dist = dcsCommon.getClosestAirbaseTo(p, nil, nil, allAirfields)
if dist > taxiPolice.airfieldMaxDist then
taxiPolice.suspects[player] = nil -- remove watched status
return
end -- not interesting
local vel = dcsCommon.getUnitSpeed(theUnit)
-- if we get here, player is on the ground, in proximity to airfield
if vel < taxiPolice.speedLimit then
taxiPolice.suspects[player] = nil -- remove watched status
return
end -- not speeding
-- if we get here, we also exceed the speed limit
-- check if we are on a runway
local myRunways = taxiPolice.runways[base:getName()]
if not myRunways then
-- this base is not policed
taxiPolice.suspects[player] = nil
return
end
for rwName, aRunway in pairs(myRunways) do
if cfxZones.isPointInsidePoly(p, aRunway) then
taxiPolice.suspects[player] = nil -- remove watched status
return
end
end
-- if we get here, player is speeding on airfield
local speedingSince = taxiPolice.suspects[player] -- time since speeding started
if not speedingSince then
-- we start watching now. At least one second will be grace period
taxiPolice.suspects[player] = timer.getTime()
return
end
if timer.getTime() - speedingSince < taxiPolice.triggerTime then
-- we are watching, but not acting
--trigger.action.outText(player .. ", you are being watched: <" .. timer.getTime() - speedingSince .. ">", 30)
return
end
-- when we get here, player is in violation.
-- make sure we will not trigger again by setting future speedingsince to negative
taxiPolice.suspects[player] = timer.getTime() + 10000 -- 10000 seconds in the future
local vioNum = taxiPolice.tickets[player]
if not vioNum then vioNum = 0 end
vioNum = vioNum + 1
taxiPolice.tickets[player] = vioNum
local pGrp = theUnit:getGroup()
local gID = pGrp:getID()
if vioNum <= taxiPolice.maxTickets then
-- just post a warning
trigger.action.outTextForGroup(gID, player .. ", your taxi speed is reckless. Stop it. Violations registered against you: " .. vioNum, 30)
return
end
-- we have reached retribution stage
taxiPolice.retributeAgainst(theUnit)
end
---
--- UPDATE
---
function taxiPolice.update() -- every second/ups
-- schedule next invocation
timer.scheduleFunction(taxiPolice.update, {}, timer.getTime() + 1/taxiPolice.ups)
--trigger.action.outText("onpatrol flag is " .. taxiPolice.onPatrol .. " with val = " .. trigger.misc.getUserFlag(taxiPolice.onPatrol), 30)
-- see if this has been turned on or offDuty
if taxiPolice.onPatrol and
cfxZones.testZoneFlag(taxiPolice, taxiPolice.onPatrol, "change", "lastOnPatrol") then
taxiPolice.active = true
local knots = math.floor(taxiPolice.speedLimit * 1.94384)
local kmh = math.floor(taxiPolice.speedLimit * 3.6)
trigger.action.outText("NOTAM:\ntarmac and taxiway speed limit of " .. knots .. " knots/" .. kmh .. " km/h is enforced on all air fields!", 30)
end
if taxiPolice.offDuty and
cfxZones.testZoneFlag(taxiPolice, taxiPolice.offDuty, "change", "lastOffDuty") then
taxiPolice.active = false
trigger.action.outText("NOTAM:\ntarmac and taxiway speed limit rescinded. Taxi responsibly!", 30)
end
if not taxiPolice.active then return end
local allAirfields = dcsCommon.getAirbasesWhoseNameContains("*", 0) -- all fixed bases, no FARP nor ships. Pre-collect
-- check all player units
local playerFactions = {1, 2}
for idx, aFaction in pairs(playerFactions) do
local allPlayers = coalition.getPlayers(aFaction)
for idy, aPlayer in pairs(allPlayers) do -- returns UNITS!
if Unit.isActive(aPlayer) and not aPlayer:inAir() then
taxiPolice.checkUnit(aPlayer, allAirfields)
end
end
end
end
--
-- ONEVENT
--
function taxiPolice:onEvent(theEvent)
--trigger.action.outText("txP event: <" .. theEvent.id .. ">", 30)
if not taxiPolice.greetings then return end -- no warnings
if not taxiPolice.active then return end -- no policing active
local ID = theEvent.id
if not ID then return end
if (ID ~= 15) and (ID ~= 4) then return end -- not birth nor land
local theUnit = theEvent.initiator
if not theUnit then return end
if theUnit:inAir() then return end
-- make sure it's a plane. Helos are ignored
local theGroup = theUnit:getGroup()
local cat = theGroup:getCategory()
if cat ~= 0 then return end
if not theUnit.getPlayerName then return end
local pName = theUnit:getPlayerName()
if not pName then return end
local base, dist = dcsCommon.getClosestAirbaseTo(theUnit:getPoint(), 0)
local bName = base:getName()
if dist > taxiPolice.airfieldMaxDist then return end
local UID = theUnit:getID()
-- check if this airfield is exempt
local rwys = taxiPolice.runways[bName]
local now = timer.getTime()
local last = taxiPolice.lastMessageTo[pName] -- remember timestamp
if not last then last = 0 end
local tdiff = now - last
-- make sure palyer receives only one such notice within 15 seconds
-- but always when now = 0 (mission startup)
if (now ~= 0) and (tdiff < 15) then return end -- to soon
taxiPolice.lastMessageTo[pName] = now
if not rwys then
trigger.action.outTextForUnit(UID, "Welcome to " .. bName .. ", " .. pName .. "!\nAlthough a general taxiway speed limit is in effect, it does not apply here.", 30)
return
end
local knots = math.floor(taxiPolice.speedLimit * 1.94384)
local kmh = math.floor(taxiPolice.speedLimit * 3.6)
trigger.action.outTextForUnit(UID, "Welcome to " .. bName .. ", " .. pName .. "!\nBe advised: a speed limit of " .. knots .. " knots/" .. kmh .. " km/h is enforced on tarmac and taxiways.", 30)
end
--
-- START
--
function taxiPolice.processTaxiZones()
local taxiZones = cfxZones.zonesWithProperty("taxiPolice")
local allAirfields = dcsCommon.getAirbasesWhoseNameContains("*", 0)
for idx, theZone in pairs(taxiZones) do
local isPoliced = cfxZones.getBoolFromZoneProperty(theZone, "taxiPolice", "true")
if not isPoliced then
local p = cfxZones.getPoint(theZone)
local base, dist = dcsCommon.getClosestAirbaseTo(p, nil, nil, allAirfields)
local name = base:getName()
taxiPolice.runways[name] = nil
if taxiPolice.verbose then
trigger.action.outText("txPol: base <" .. name .. "> taxiways not policed.", 30)
end
end
end
end
function taxiPolice.readConfigZone()
local theZone = cfxZones.getZoneByName("taxiPoliceConfig")
if not theZone then
theZone = cfxZones.createSimpleZone("taxiPoliceConfig")
if taxiPolice.verbose then
trigger.action.outText("+++txPol: no config zone!", 30)
end
end
taxiPolice.name = "taxiPoliceConfig" -- cfxZones compatibility
taxiPolice.verbose = theZone.verbose
taxiPolice.speedLimit = cfxZones.getNumberFromZoneProperty(theZone, "speedLimit", 14) -- 14 -- m/s. 14 m/s = 50 km/h, 10 m/s = 36 kmh
taxiPolice.triggerTime = cfxZones.getNumberFromZoneProperty(theZone, "triggerTime", 3) --3 -- seconds until we register a speeding violation
taxiPolice.rwyLeeway = cfxZones.getNumberFromZoneProperty(theZone, "leeway", 5) -- 5 -- meters on each side
taxiPolice.rwyExtend = cfxZones.getNumberFromZoneProperty(theZone, "extend", 500) --500 -- meters in front and at end
taxiPolice.airfieldMaxDist = cfxZones.getNumberFromZoneProperty(theZone, "radius", 3000) -- 3000 -- radius around airfield in which we operate
taxiPolice.maxTickets = cfxZones.getNumberFromZoneProperty(theZone, "maxTickets", 3) -- 3
taxiPolice.active = cfxZones.getBoolFromZoneProperty(theZone, "active", true)
taxiPolice.greetings = cfxZones.getBoolFromZoneProperty(theZone, "greetings", true)
if cfxZones.hasProperty(theZone, "onPatrol") then
taxiPolice.onPatrol = cfxZones.getStringFromZoneProperty(theZone, "onPatrol", "<none>")
taxiPolice.lastOnPatrol = cfxZones.getFlagValue(taxiPolice.onPatrol, taxiPolice)
end
if cfxZones.hasProperty(theZone, "offDuty") then
taxiPolice.offDuty = cfxZones.getStringFromZoneProperty(theZone, "offDuty", "<none>")
taxiPolice.lastOffDuty = cfxZones.getFlagValue(taxiPolice.offDuty, taxiPolice)
end
end
function taxiPolice.start()
-- lib check
if not dcsCommon.libCheck then
trigger.action.outText("cfx taxiPolice requires dcsCommon", 30)
return false
end
if not dcsCommon.libCheck("cfx taxiPolice", taxiPolice.requiredLibs) then
return false
end
-- read config
taxiPolice.readConfigZone()
-- build taxiway db
taxiPolice.buildRunways()
-- read taxiPolice attributes
taxiPolice.processTaxiZones()
-- start update
taxiPolice.update()
-- install envent handler to greet pilots on airfields
world.addEventHandler(taxiPolice)
-- say hi!
trigger.action.outText("cfx taxiPolice v" .. taxiPolice.version .. " started.", 30)
return true
end
-- let's go!
if not taxiPolice.start() then
trigger.action.outText("cfx taxiPolice aborted: missing libraries", 30)
taxiPolice = nil
end
--[[--
Possible improvements
- other sanctions on violations like kick, ban etc
- call nearest airfield for open rwys (needs 'commandForUnit' first
- ability to persist offenders
--]]--