airtank = {} airtank.version = "1.0.0" -- Module to extinguish fires controlled by the 'inferno' module. -- For 'airtank' fire extinguisher aircraft modules. airtank.requiredLibs = { "dcsCommon", "cfxZones", } airtank.tanks = {} -- player data by GROUP name, will break with multi-unit groups -- tank attributes -- pumpArmed -- for hovering fills. -- armed -- for triggering drops below trigger alt- -- dropping -- if drop has triggered -- carrying -- how mach retardant / fluid am I carrying? -- capacity -- how much can I carry -- uName -- pName -- theUnit -- gID -- lastDeparture -- timestamp to avoid double notifications -- lastLanding -- timestamp to avoid double dip airtank.zones = {} -- come here to refill your tanks airtank.roots = {} -- roots for player by group name airtank.mainMenu = nil -- handles attachTo: airtank.types = {"Mi-8MT", "UH-1H", "Mi-24P", "OH58D", "CH-47Fbl1"} -- which helicopters/planes can firefight and get menu. can be amended with config and zone airtank.capacities = { ["UH-1H"] = 1200, ["Mi-8MT"] = 4000, ["Mi-24P"] = 2400, ["CH-47Fbl1"] = 9000, ["OH58D"] = 500, } -- how much each helo can carry. default is 500kg, can be amended with zone airtank.pumpSpeed = 100 -- liters/kg per second, for all helos same airtank.dropSpeed = 1000 -- liters per second, for all helos same airtank.releaseAlt = 100 -- m = 90 ft airtank.pumpAlt = 10 -- m = 30 feet -- -- airtank zones - land inside to refill. can have limited capa -- function airtank.addZone(theZone) airtank.zones[theZone.name] = theZone end function airtank.readZone(theZone) theZone.capacity = theZone:getNumberFromZoneProperty("airtank", 99999999) -- should be enough theZone.amount = theZone:getNumberFromZoneProperty("amount", theZone.capacity) end function airtank.refillWithZone(theZone, data) local theUnit = data.theUnit local wanted = data.capacity - data.carrying if theZone.amount > wanted then theZone.amount = theZone.amount - wanted data.carrying = data.capacity trigger.action.outTextForGroup(data.gID, "Roger, " .. data.uName .. ", topped up tanks, you now carry " .. data.carrying .. "kg of flame retardant.", 30) trigger.action.outSoundForGroup(data.gID, airtank.actionSound) trigger.action.setUnitInternalCargo(data.uName, data.carrying + 10) return true end trigger.action.outTextForGroup(data.gID, "Negative, " .. data.uName .. ", out of flame retardant.", 30) return false end -- -- event handling -- function airtank:onEvent(theEvent) -- catch birth events of helos if not theEvent then return end local theUnit = theEvent.initiator if not theUnit then return end if not theUnit.getPlayerName then return end local pName = theUnit:getPlayerName() if not pName then return end -- we have a player unit if not dcsCommon.unitIsOfLegalType(theUnit, airtank.types) then if airtank.verbose then trigger.action.outText("aTnk: unit <" .. theUnit:getName() .. ">, type <" .. theUnit:getTypeName() .. "> not an airtank.", 30) end return end local uName = theUnit:getName() local uType = theUnit:getTypeName() local theGroup = theUnit:getGroup() local gName = theGroup:getName() if theEvent.id == 15 then -- birth -- make sure this aircraft is legit airtank.installMenusForUnit(theUnit) local theData = airtank.newData(theUnit) theData.pName = pName airtank.tanks[gName] = theData if airtank.verbose then trigger.action.outText("+++aTnk: new player airtank <" .. uName .. "> type <" .. uType .. "> for <" .. pName .. ">", 30) end return end if theEvent.id == 4 or -- land theEvent.id == 55 -- runway touch then -- see if landed inside a refill zone and -- automatically top off if pumpArmed local data = airtank.tanks[gName] if data and data.lastLanding then return end -- no double dip if data and data.pumpArmed then data.lastLanding = timer.getTime() data.lastDeparture = nil local p = theUnit:getPoint() for idx, theZone in pairs (airtank.zones) do if theZone:pointInZone(p) then if airtank.refillWithZone(theZone, data) then data.armed = false data.pumpArmed = false return end -- if refill received end -- if in zone end -- for zones elseif data then data.lastLanding = timer.getTime() data.lastDeparture = nil local p = theUnit:getPoint() for idx, theZone in pairs (airtank.zones) do if theZone:pointInZone(p) then trigger.action.outTextForGroup(data.gID, "Welcome to " .. theZone.name .. ", " .. pName .. ", firefighting services are available.", 30) return end end end return end if theEvent.id == 3 or -- takeoff theEvent.id == 54 -- runway takeoff then local data = airtank.tanks[gName] local now = timer.getTime() if data then data.lastLanding = nil -- suppress double take-off notifications for 20 seconds if data.lastDeparture then -- and data.lastDeparture + 60 < now then return end data.lastDeparture = now if data.carrying < data.capacity * 0.5 then trigger.action.outTextForGroup(data.gID, "Good luck, " .. pName .. ", remember to top off your tanks before going in.", 30) else trigger.action.outTextForGroup(data.gID, "Good luck and godspeed, " .. pName .. "!", 30) end trigger.action.outSoundForGroup(data.gID, airtank.actionSound) end return end end function airtank.newData(theUnit) local theType = theUnit:getTypeName() local data = {} local capa = airtank.capacities[theType] if not capa then capa = 500 end -- default capa. data.capacity = capa data.carrying = 0 data.pumpArmed = false data.armed = false data.dropping = false data.uName = theUnit:getName() data.pName = theUnit:getPlayerName() data.theUnit = theUnit local theGroup = theUnit:getGroup() data.gID = theGroup:getID() trigger.action.setUnitInternalCargo(data.uName, data.carrying + 10) return data end -- -- comms -- function airtank.installMenusForUnit(theUnit) -- assumes all unfit types are weeded out -- if already exists, remove old if not theUnit then return end if not Unit.isExist(theUnit) then return end local theGroup = theUnit:getGroup() local uName = theUnit:getName() local uType = theUnit:getTypeName() local pName = theUnit:getPlayerName() local gName = theGroup:getName() local gID = theGroup:getID() local pRoot = airtank.roots[gName] if pRoot then missionCommands.removeItemForGroup(gID, pRoot) end -- now add the airtank menu pRoot = missionCommands.addSubMenuForGroup(gID, airtank.menuName, airtank.mainMenu) airtank.roots[gName] = pRoot -- save for later local args = {gName, uName, gID, uType, pName} -- menus: -- status: loaded, capa, armed etc -- ready pump -- turn off drop system and start sucking, if landing in zone will fully charge, else suck in water when hovering over water when low enough (below 10m) -- arm release -- get ready to drop. auto-release when alt is below 30m local m1 = missionCommands.addCommandForGroup(gID , "Tank Status" , pRoot, airtank.redirectStatus, args) local mx2 = missionCommands.addCommandForGroup(gID , "MANUAL RELEASE" , pRoot, airtank.redirectManDrop, args) local m2 = missionCommands.addCommandForGroup(gID , "*Arm*AUTODROP*trigger" , pRoot, airtank.redirectArmDrop, args) local m3 = missionCommands.addCommandForGroup(gID , "Activate/Ready intake" , pRoot, airtank.redirectArmPump, args) local m4 = missionCommands.addCommandForGroup(gID , "Secure ship" , pRoot, airtank.redirectSecure, args) end function airtank.redirectStatus(args) timer.scheduleFunction(airtank.doStatus, args, timer.getTime() + 0.1) end function airtank.doStatus(args) local gName = args[1] local uName = args[2] local gID = args[3] local uType = args[4] local pName = args[5] local ralm = airtank.releaseAlt local ralf = math.floor(ralm * 3.28084) local data = airtank.tanks[gName] local remains = data.capacity - data.carrying local msg = "\nAirtank <" .. uName .. "> (" .. uType .. "), commanded by " .. pName .. "\n capacity: " .. data.capacity .. "kg, carrying " .. data.carrying .. "kg (free " .. remains .. "kg)" -- add info to nearest refuel zone? if data.armed then msg = msg .. "\n\n *** RELEASE TRIGGER ARMED (" .. ralm .. "m/" .. ralf .. "ft AGL)***" end ralm = airtank.pumpAlt ralf = math.floor(ralm * 3.28084) if data.pumpArmed then msg = msg .. "\n\n --- intake pumps ready (below " .. ralm .. "m/" .. ralf .. "ft AGL)" end msg = msg .. "\n" trigger.action.outTextForGroup(gID, msg, 30) end function airtank.redirectManDrop(args) timer.scheduleFunction(airtank.doManDrop, args, timer.getTime() + 0.1) end function airtank.doManDrop(args) local gName = args[1] local uName = args[2] local theUnit = Unit.getByName(uName) local gID = args[3] local uType = args[4] local pName = args[5] local data = airtank.tanks[gName] local remains = data.capacity - data.carrying local alt = dcsCommon.getUnitAGL(theUnit) local ralm = math.floor(alt) local ralf = math.floor(ralm * 3.28084) local msg = "" local agl = dcsCommon.getUnitAGL(theUnit) if not theUnit:inAir() then trigger.action.outTextForGroup(data.gID, "Please get into the air before releasing flame retardant.", 30) trigger.action.outSoundForGroup(data.gID, airtank.actionSound) return end if data.carrying < 1 then msg = "\nRetard tanks empty. Your " .. uType .. " can carry up to " .. data.capacity .. "kg.\n" data.armed = false data.dropping = false data.pumpArmed = false elseif data.carrying < 100 then msg = "\nTanks empty (" .. data.carrying .. "kg left), safeties are engaged.\n" data.armed = false data.dropping = false data.pumpArmed = false else msg = "\n *** Opened drop valve at " .. ralm .. "m/" .. ralf .. "ft RALT.\n" data.armed = false data.pumpArmed = false data.dropResults = {} data.dropping = true trigger.action.outTextForGroup(gID, msg, 30) trigger.action.outSoundForGroup(gID, airtank.actionSound) airtank.dropFor(data) return end trigger.action.outTextForGroup(gID, msg, 30) trigger.action.outSoundForGroup(gID, airtank.actionSound) end function airtank.redirectArmDrop(args) timer.scheduleFunction(airtank.doArmDrop, args, timer.getTime() + 0.1) end function airtank.doArmDrop(args) local gName = args[1] local uName = args[2] local theUnit = Unit.getByName(uName) local gID = args[3] local uType = args[4] local pName = args[5] local data = airtank.tanks[gName] local remains = data.capacity - data.carrying local ralm = airtank.releaseAlt local ralf = math.floor(ralm * 3.28084) local msg = "" local agl = dcsCommon.getUnitAGL(theUnit) if data.carrying < 1 then msg = "\nRetard tanks empty. Your " .. uType .. " can carry up to " .. data.capacity .. "kg.\n" data.armed = false data.dropping = false data.pumpArmed = false elseif agl < airtank.releaseAlt then msg = "Get above " .. ralm .. "m/" .. ralf .. "ft ALG (radar) to arm trigger." elseif data.carrying < 100 then msg = "\nTank empty (" .. data.carrying .. "kg left), safeties are engaged.\n" data.armed = false data.dropping = false data.pumpArmed = false else msg = "\n *** Release valve primed to trigger below " .. ralm .. "m/" .. ralf .. "ft RALT.\n\nRelease starts automatically at or below trigger altitude.\n" data.armed = true data.dropping = false data.pumpArmed = false end trigger.action.outTextForGroup(gID, msg, 30) trigger.action.outSoundForGroup(gID, airtank.actionSound) end function airtank.redirectArmPump(args) timer.scheduleFunction(airtank.doArmPump, args, timer.getTime() + 0.1) end function airtank.doArmPump(args) local gName = args[1] local uName = args[2] local theUnit = Unit.getByName(uName) local gID = args[3] local uType = args[4] local pName = args[5] local data = airtank.tanks[gName] local remains = data.capacity - data.carrying local ralm = airtank.pumpAlt local ralf = math.floor(ralm * 3.28084) local msg = "" -- if we are on the ground, check if we are inside a -- zone that can refill us if not theUnit:inAir() then local p = theUnit:getPoint() for idx, theZone in pairs (airtank.zones) do if theZone:pointInZone(p) then if airtank.refillWithZone(theZone, data) then data.armed = false data.pumpArmed = false data.dropping = false return end end end end msg = "\n *** Intake valves ready, descend to " .. ralm .. "m/" .. ralf .. "ft RALT over water, or land at a firefighting supply base.\n" data.armed = false data.dropping = false data.pumpArmed = true trigger.action.outTextForGroup(gID, msg, 30) trigger.action.outSoundForGroup(gID, airtank.actionSound) end function airtank.redirectSecure(args) timer.scheduleFunction(airtank.doSecure, args, timer.getTime() + 0.1) end function airtank.doSecure(args) local gName = args[1] local gID = args[3] local data = airtank.tanks[gName] local msg = "" msg = "\n All valves secure and stored for cruise operation\n" data.armed = false data.dropping = false data.pumpArmed = false trigger.action.outTextForGroup(gID, msg, 30) trigger.action.outSoundForGroup(gID, airtank.actionSound) end -- -- update -- function airtank.dropFor(theData) -- drop onto ground/fire local theUnit = theData.theUnit local qty = airtank.dropSpeed if qty > theData.carrying then qty = math.floor(theData.carrying) end -- calculate position where it will hit local alt = dcsCommon.getUnitAGL(theUnit) if alt < 0 then alt = 0 end local vel = theUnit:getVelocity() -- vec 3, we only need x and z to project the point where the water will impact (we ignore vel.z) -- calculation: agl=height, no downward vel, will accelerate at G= 10 m/ss -- i.e. t = sqrt(2*agl/10) local agl = dcsCommon.getUnitAGL(theUnit) local t = math.sqrt(0.2*agl) local p = theUnit:getPoint() local impact = {x = p.x + t * vel.x, y = 0, z = p.z + t * vel.z} -- tell inferno about it, get some feedback if inferno.waterDropped then local diag = inferno.waterDropped(impact, qty, theData) if diag then table.insert(theData.dropResults, diag) end else trigger.action.outText("WARNING: airtank can't find 'inferno' module.", 30) return end -- update what we have left in tank theData.carrying = theData.carrying - qty if theData.carrying < 0 then theData.carrying = 0 end local ralm = math.floor(agl) local ralf = math.floor(ralm * 3.28084) local msg = "Dropping " .. qty .. "kg at RALT " .. ralm .. "m/" .. ralf .. "ft, " .. theData.carrying .. " kg remaining" local snd = airtank.releaseSound -- close vent if empty if theData.carrying < 100 then -- close hatch theData.dropping = false theData.armed = false msg = msg .. ", CLOSING VENTS\n\n" snd = airtank.actionSound -- add all drop diagnoses if #theData.dropResults < 1 then msg = msg .. "No discernible results.\n" else msg = msg .. "Good delivey:\n" for idx, res in pairs(theData.dropResults) do msg = msg .. " - " .. res .. "\n" end end end -- set internal cargo trigger.action.setUnitInternalCargo(theData.uName, theData.carrying + 10) -- say how much we dropped and (if so) what we are over trigger.action.outTextForGroup(theData.gID, msg, 30, true) trigger.action.outSoundForGroup(theData.gID, snd) end function airtank.updateDataFor(theData) local theUnit = theData.theUnit -- see if we are dropping if theData.dropping then -- valve is open airtank.dropFor(theData) -- drop contents of tank, sets weight elseif theData.armed then -- see if we are below 10*trigger local alt = dcsCommon.getUnitAGL(theUnit) if alt < 10 * airtank.releaseAlt then -- see if we trigger if alt <= airtank.releaseAlt then -- !! trigger flow theData.dropResults = {} theData.dropping = true -- trigger.action.outText("allocated dropResults", 30) theData.armed = false airtank.dropFor(theData) -- sets weight trigger.action.outSoundForGroup(theData.gID, airtank.releaseSound) else -- flash current alt and say when we will trigger local calm = math.floor(alt) local calf = math.floor(calm * 3.28084) local ralm = airtank.releaseAlt local ralf = math.floor(ralm * 3.28084) trigger.action.outTextForGroup(theData.gID, "Current RALT " .. calm .. "m/" .. calf .. "ft, will release at " .. ralm .. "m/" .. ralf .. "ft", 30, true) -- erase all trigger.action.outSoundForGroup(theData.gID, airtank.blipSound) end end -- see if the intake valve is open elseif theData.pumpArmed then p = theUnit:getPoint() local sType = land.getSurfaceType({x=p.x, y=p.z}) if sType ~= 2 and sType ~= 3 then -- not over water return end local alt = dcsCommon.getUnitAGL(theUnit) local calm = math.floor(alt) local calf = math.floor(calm * 3.28084) if alt < 5 * airtank.pumpAlt then local msg = "RALT " .. calm .. "m/" .. calf .. "ft, " if alt <= airtank.pumpAlt then -- in pump range theData.carrying = theData.carrying + airtank.pumpSpeed if theData.carrying > theData.capacity then theData.carrying = theData.capacity end trigger.action.setUnitInternalCargo(theData.uName, theData.carrying + 10) end msg = msg .. theData.carrying .. "/" .. theData.capacity .. "kg" if theData.carrying >= theData.capacity - 50 then theData.pumpArmed = false msg = msg .. " PUMP DISENGAGED" end trigger.action.outTextForGroup(theData.gID, msg, 30, true) trigger.action.outSoundForGroup(theData.gID, airtank.pumpSound) end end end function airtank.update() -- update all firefighters timer.scheduleFunction(airtank.update, {}, timer.getTime() + airtank.ups) -- next time local filtered = {} -- filter existing only for gName, data in pairs(airtank.tanks) do local theUnit = data.theUnit if Unit.isExist(theUnit) then if theUnit:inAir() then airtank.updateDataFor(data) end filtered[gName] = data end end airtank.tanks = filtered end -- -- start and config -- function airtank.readConfigZone() airtank.name = "airtankConfig" -- make compatible with dml zones local theZone = cfxZones.getZoneByName("airtankConfig") if not theZone then theZone = cfxZones.createSimpleZone("airtankConfig") end airtank.verbose = theZone.verbose airtank.ups = theZone:getNumberFromZoneProperty("ups", 1) airtank.menuName = theZone:getStringFromZoneProperty("menuName", "Firefighting") airtank.actionSound = theZone:getStringFromZoneProperty("actionSound", "none") airtank.blipSound = theZone:getStringFromZoneProperty("blipSound", airtank.actionSound) airtank.releaseSound = theZone:getStringFromZoneProperty("releaseSound", airtank.actionSound) airtank.pumpSound = theZone:getStringFromZoneProperty("pumpSound", airtank.actionSound) if theZone:hasProperty("attachTo:") then local attachTo = theZone:getStringFromZoneProperty("attachTo:", "") if radioMenu then -- requires optional radio menu to have loaded local mainMenu = radioMenu.mainMenus[attachTo] if mainMenu then airtank.mainMenu = mainMenu else trigger.action.outText("+++airtank: cannot find super menu <" .. attachTo .. ">", 30) end else trigger.action.outText("+++airtank: REQUIRES radioMenu to run before inferno. 'AttachTo:' ignored.", 30) end end -- add own troop carriers -- superceded by airTankSpecs if theZone:hasProperty("airtanks") then local tc = theZone:getStringFromZoneProperty("airtanks", "UH-1D") tc = dcsCommon.splitString(tc, ",") airtank.types = dcsCommon.trimArray(tc) if airtank.verbose then trigger.action.outText("+++aTnk: redefined air tanks to types:", 30) for idx, aType in pairs(airtank.types) do trigger.action.outText(aType, 30) end end end -- add capacities and types from airTankSpecs zone local capaZone = cfxZones.getZoneByName("airTankSpecs") if capaZone then if airtank.verbose then trigger.action.outText("aTnk: found and processing 'airTankSpecs' zone data.", 30) end -- read all into my types registry, replacing whatever is there local rawCapa = cfxZones.getAllZoneProperties(capaZone) local newCapas = airtank.processCapas(rawCapa) -- now types to existing types if not already there for aType, aCapa in pairs(newCapas) do airtank.capacities[aType] = aCapa dcsCommon.addToTableIfNew(airtank.types, aType) if civAir.verbose then trigger.action.outText("+++aTnk: processed aircraft <" .. aType .. "> for capacity <" .. aCapa .. ">", 30) end end end end function airtank.processCapas(rawIn) local newCapas = {} -- now iterate the input table, and generate new types and -- liveries from it for theType, capa in pairs (rawIn) do if airtank.verbose then trigger.action.outText("+++aTnk: processing type <" .. theType .. ">:<" .. capa .. ">", 30) end local pcapa = tonumber(capa) if not pcapa then capa = 0 end newCapas[theType] = pcapa end return newCapas end function airtank.start() if not dcsCommon.libCheck then trigger.action.outText("cfx airtank requires dcsCommon", 30) return false end if not dcsCommon.libCheck("cfx airtank", airtank.requiredLibs) then return false end -- read config airtank.readConfigZone() -- process airtank Zones local attrZones = cfxZones.getZonesWithAttributeNamed("airtank") for k, aZone in pairs(attrZones) do airtank.readZone(aZone) -- process attributes airtank.addZone(aZone) -- add to list end -- connect event handler world.addEventHandler(airtank) -- start update timer.scheduleFunction(airtank.update, {}, timer.getTime() + 1/airtank.ups) -- say Hi! trigger.action.outText("cf/x airtank v" .. airtank.version .. " started.", 30) return true end if not airtank.start() then trigger.action.outText("airtank failed to start up") airtank = nil end