Version 2.3.1

- Code hardening
- Experimental: inferno and airtank modules
This commit is contained in:
Christian Franz 2024-08-29 10:44:52 +02:00
parent f7a8705aa5
commit 695303940f
19 changed files with 1509 additions and 89 deletions

Binary file not shown.

Binary file not shown.

View File

@ -560,6 +560,10 @@ end
-- Start
--
function FARPZones.releaseFARPS()
if FARPZones.verbose then
trigger.action.outText("FARPz: releasing FARP ownership to mission in progress", 30)
end
for idx, aFarp in pairs(FARPZones.lockup) do
aFarp:autoCapture(true)
end

View File

@ -1,5 +1,5 @@
LZ = {}
LZ.version = "1.1.0"
LZ.version = "1.2.1"
LZ.verbose = false
LZ.ups = 1
LZ.requiredLibs = {
@ -15,7 +15,8 @@ LZ.LZs = {}
Version History
1.0.0 - initial version
1.1.0 - persistence
1.2.0 - dcs 2024-07-11 and dcs 2024-07-22 updates (new events)
1.2.1 - theZone --> aZone typo at input management
--]]--
function LZ.addLZ(theZone)
@ -216,7 +217,9 @@ function LZ:onEvent(event)
-- only interested in S_EVENT_TAKEOFF and events
if event.id ~= world.event.S_EVENT_TAKEOFF and
event.id ~= world.event.S_EVENT_LAND then
event.id ~= world.event.S_EVENT_LAND and
event.id ~= world.event.S_EVENT_RUNWAY_TAKEOFF and
event.id ~= world.event.S_EVENT_RUNWAY_TOUCH then
return
end
@ -231,14 +234,20 @@ function LZ:onEvent(event)
-- see if this unit interests us at all
if LZ.unitIsInterestingForZone(theUnit, aZone) then
-- interesting unit in zone triggered the event
if aZone.lzDeparted and event.id == world.event.S_EVENT_TAKEOFF then
if aZone.lzDeparted and
(event.id == world.event.S_EVENT_TAKEOFF or
event.id == world.event.S_EVENT_RUNWAY_TAKEOFF)
then
if LZ.verbose or aZone.verbose then
trigger.action.outText("+++LZ: detected departure from <" .. aZone.name .. ">", 30)
end
cfxZones.pollFlag(aZone.lzDeparted, aZone.lzMethod, aZone)
end
if aZone.lzLanded and event.id == world.event.S_EVENT_LAND then
if aZone.lzLanded and
(event.id == world.event.S_EVENT_LAND or
event.id == world.event.S_EVENT_RUNWAY_TOUCH)
then
if LZ.verbose or aZone.verbose then
trigger.action.outText("+++LZ: detected landing in <" .. aZone.name .. ">", 30)
end
@ -264,14 +273,14 @@ function LZ.update()
for idx, aZone in pairs(LZ.LZs) do
-- see if we are being paused or unpaused
if cfxZones.testZoneFlag(aZone, aZone.lzPause, aZone.LZTriggerMethod, "lzLastPause") then
if LZ.verbose or theZone.verbose then
if LZ.verbose or aZone.verbose then
trigger.action.outText("+++LZ: triggered pause? for <".. aZone.name ..">", 30)
end
aZone.isPaused = true
end
if cfxZones.testZoneFlag(aZone, aZone.lzContinue, aZone.LZTriggerMethod, "lzLastContinue") then
if LZ.verbose or theZone.verbose then
if LZ.verbose or aZone.verbose then
trigger.action.outText("+++LZ: triggered continue? for <".. aZone.name ..">", 30)
end
aZone.isPaused = false

631
modules/airtank.lua Normal file
View File

@ -0,0 +1,631 @@
airtank = {}
airtank.version = "0.9.9"
-- 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:", "<none>")
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
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

View File

@ -1,5 +1,5 @@
civHelo = {}
civHelo.version = "1.0.0"
civHelo.version = "1.0.1"
civHelo.requiredLibs = {
"dcsCommon", -- always
"cfxZones",
@ -7,7 +7,7 @@ civHelo.requiredLibs = {
--[[--
Version History
1.0.0 - Initial version
1.0.1 - livery hardening
--]]--
civHelo.flights = {} -- currently active flights
@ -282,10 +282,24 @@ function civHelo.getType(theZone)
end
function civHelo.getLiveryForType(theType, theData)
-- trigger.action.outText("picking liviery for helo type <" .. theType .. ">", 30)
if civHelo.liveries[theType] then
local available = civHelo.liveries[theType]
local chosen = dcsCommon.pickRandom(available)
if available then
-- trigger.action.outText("We have a livery selection available for helo", 30)
else
-- trigger.action.outText("No liveries for type.", 30)
end
local chosen = dcsCommon.pickRandom(available)
if chosen then
-- trigger.action.outText("a fine livery choice: <" .. chosen .. "> for type <" .. theType .. ">", 30)
else
-- trigger.action.outText("No choice, no livery.", 30)
end
theData.livery_id = chosen
else
-- trigger.action.outText("No livery for type <" .. theType .. ">", 30)
end
end

View File

@ -1050,17 +1050,13 @@ destinationReached! -- adds script to last waypoint to hit this signal, also ini
dead! signal and cb. only applies to ground troops? can they disembark troops when hit?
attacked signal each time a unit is destroyed
importantType - type that must survive=
coalition / masterOwner
isActive# 0/1
coalition / masterOwner tie-in
doWipe? to wipe all my convoys?
tacTypes = desinate units types that must survive. Upon start, ensure that at least one tac type is pressenr
when arriving, verify that it still is, or fail earlier when all tactypes are destroyed.
convoy status UI
csar integration. when losing a vehicle, pSCAR match (say, 50%), whole convoy pushes a HOLD order to defend until time runs out or csar mission picks up evacuee (pop task).
do:
when escort engages, send notice
when escort damaged, send notice
mark source and dest of convoy on map for same side
?mark source and dest of convoy on map for same side
make routes interchangeable between convoys?
make inf units disembark when convoy attacked
--]]--

View File

@ -1,5 +1,5 @@
dcsCommon = {}
dcsCommon.version = "3.1.2"
dcsCommon.version = "3.1.3"
--[[-- VERSION HISTORY
3.0.0 - removed bad bug in stringStartsWith, only relevant if caseSensitive is false
- point2text new intsOnly option
@ -29,19 +29,37 @@ dcsCommon.version = "3.1.2"
3.1.0 - updates to events, DCS update 7-11 2024 hardening
3.1.1 - added Chinook to troop carriers
3.1.2 - isTroopCarrier() hardening against DCS sillieness
3.1.3 - new dcsCommon.unitIsOfLegalType() analogue to isTroopCarrier
- new DCS Patch section
--]]--
-- dcsCommon is a library of common lua functions
-- for easy access and simple mission programming
-- (c) 2021 - 2024 by Christian Franz and cf/x AG
--
-- DCS API PATCHES FOR DCS-INTERNAL BUGS
--
Group.getByNameBase = Group.getByName -- thanks, Dzsekeb(?)
function Group.getByName(name) -- patch getByName to protect against empty groups
local g = Group.getByNameBase(name)
if not g then return nil end
if g:getSize() == 0 then return nil end
return g
end
--
-- DML FOUNDATION
--
dcsCommon.verbose = false -- set to true to see debug messages. Lots of them
dcsCommon.uuidStr = "uuid-"
dcsCommon.simpleUUID = 76543 -- a number to start. as good as any
-- globals
dcsCommon.cbID = 0 -- callback id for simple callback scheduling
dcsCommon.troopCarriers = {"Mi-8MT", "UH-1H", "Mi-24P", "OH58D", "CH-47Fbl1"} -- Ka-50, Apache and Gazelle can't carry troops
dcsCommon.troopCarriers = {"Mi-8MT", "UH-1H", "Mi-24P", "OH58D", "CH-47Fbl1"} -- Ka-50, Apache and Gazelle can't carry troops, the Kiowa can!
dcsCommon.coalitionSides = {0, 1, 2}
dcsCommon.maxCountry = 86 -- number of countries defined in total
@ -2839,6 +2857,10 @@ function dcsCommon.isTroopCarrierType(theType, carriers)
return false
end
function dcsCommon.unitIsOfLegalType(theUnit, types)
return dcsCommon.isTroopCarrier(theUnit, types)
end
function dcsCommon.isTroopCarrier(theUnit, carriers)
-- return true if conf can carry troups
if not theUnit then return false end

View File

@ -1,5 +1,5 @@
cfxHeloTroops = {}
cfxHeloTroops.version = "3.1.0"
cfxHeloTroops.version = "3.1.2"
cfxHeloTroops.verbose = false
cfxHeloTroops.autoDrop = true
cfxHeloTroops.autoPickup = false
@ -18,7 +18,8 @@ cfxHeloTroops.requestRange = 500 -- meters
3.0.4 - also handles picking up troops with orders "captureandhold"
3.0.5 - worked around a new issues accessing a unit's name
3.1.0 - compatible with DCS 2.9.6 dynamic spawning
3.1.1 - deployTroopsFromHelicopter() captureandhold
3.1.2 - doLoadGroup - test if group is still alive edge case handling
--]]--
@ -613,6 +614,7 @@ function cfxHeloTroops.deployTroopsFromHelicopter(conf)
local moveFormation = conf.troopsOnBoard.moveFormation
if not orders then orders = "guard" end
orders = string.lower(orders)
-- order processing: if the orders were pre-pended with "wait-"
-- we now remove that, so after dropping they do what their
@ -646,7 +648,11 @@ function cfxHeloTroops.deployTroopsFromHelicopter(conf)
-- and make them run to the wrong zone
dest = cfxGroundTroops.getClosestEnemyZone(troop)
troopData.destination = dest
trigger.action.outText("Inserting troops to capture zone <" .. dest.name .. ">", 30)
if dest then
trigger.action.outText("Inserting troops to capture zone <" .. dest.name .. ">", 30)
else
trigger.action.outText("+++heloT: WARNING: cap&hold: can't find a zone to cap.", 30)
end
end
troop.destination = dest -- transfer target zone for attackzone oders
@ -678,6 +684,8 @@ end
function cfxHeloTroops.doLoadGroup(args)
local conf = args[1]
local group = args[2]
if not group then return end
if not Group.isExist(group) then return end -- edge case: group died in the past 0.1 seconds
conf.troopsOnBoard = {}
-- all we need to do is disassemble the group into type
conf.troopsOnBoard.types = dcsCommon.getGroupTypeString(group)

734
modules/inferno.lua Normal file
View File

@ -0,0 +1,734 @@
inferno = {}
inferno.version = "0.9.9"
inferno.requiredLibs = {
"dcsCommon",
"cfxZones",
}
--
-- Inferno models fires inside inferno zones. Fires can spread and
-- be extinguished by aircraft from the airtank module
-- (c) 2024 by Christian "cfrag" Franz
--
inferno.zones = {}
inferno.maxStages = 10 -- tied to fuel consumption, 1 burns small, maxStages at max size. A burning fire untended increases in stage by one each tick until it reaches maxStages
inferno.threshold = 4.9 -- when fire spreads to another field
inferno.fireExtinguishedCB = {} -- CB for other modules / scripts
--
-- CB
--
function inferno.installExtinguishedCB(theCB)
table.insert(inferno.fireExtinguishedCB, theCB)
end
function inferno.invokeFireExtinguishedCB(theZone)
for idx, cb in pairs(inferno.fireExtinguishedCB) do
cb(theZone, theZone.heroes)
end
end
--
-- Reading zones
--
function inferno.addZone(theZone)
inferno.zones[theZone.name] = theZone
end
function inferno.buildGrid(theZone)
-- default for circular zone
local radius = theZone.radius
local side = radius * 2
local p = theZone:getPoint()
local minx = p.x - radius
local minz = p.z - radius
local xside = side
local zside = side
local xradius = radius
local zradius = radius
if theZone.isPoly then
-- build the params for a (rectangular) zone from a
-- quad zone, makes area an AABB (axis-aligned bounding box)
minx = theZone.bounds.ll.x
minz = theZone.bounds.ll.z
xside = theZone.bounds.ur.x - theZone.bounds.ll.x
zside = theZone.bounds.ur.z - theZone.bounds.ll.z
end
local cellx = theZone.cellSize -- square cells assumed
local cellz = theZone.cellSize
local numX = math.floor(xside / cellx)
if numX < 1 then numX = 1 end
if numX > 100 then
trigger.action.outText("***inferno: limited x from <" .. numX .. "> to 100", 30)
numX = 100
end
local numZ = math.floor(zside / cellz)
if numX > 100 then
trigger.action.outText("***inferno: limited z from <" .. numZ .. "> to 100", 30)
numZ = 100
end
if numZ < 1 then numZ = 1 end
if theZone.verbose then
trigger.action.outText("infernal zone <" .. theZone.name .. ">: cellSize <" .. theZone.cellSize .. "> --> x <" .. numX .. ">, z <" .. numZ .. ">", 30)
end
local grid = {}
local goodCells = {}
-- Remember that in DCS
-- X is North/South, with positive X being North/South
-- and Z is East/West with positve Z being EAST
local fCount = 0
theZone.burning = false
for x=1, numX do -- "up/down"
grid[x] = {}
for z=1, numZ do -- "left/right"
local ele = {}
-- calculate center for each cell
local xc = minx + (x-1) * cellx + cellx/2
local zc = minz + (z-1) * cellz + cellz/2
local xf = xc
local zf = zc
local lp = {x=xc, y=zc}
local yc = land.getHeight(lp)
ele.center = {x=xc, y=yc, z=zc}
if theZone.markCell then
dcsCommon.createStaticObjectForCoalitionAtLocation(0, ele.center, dcsCommon.uuid(theZone.name), "Black_Tyre_RF", 0, false)
dcsCommon.createStaticObjectForCoalitionAtLocation(0, {x=ele.center.x - cellx/2, y=0, z=ele.center.z - cellz/2}, dcsCommon.uuid(theZone.name), "Windsock", 0, false)
dcsCommon.createStaticObjectForCoalitionAtLocation(0, {x=ele.center.x + cellx/2, y=0, z=ele.center.z - cellz/2}, dcsCommon.uuid(theZone.name), "Windsock", 0, false)
dcsCommon.createStaticObjectForCoalitionAtLocation(0, {x=ele.center.x - cellx/2, y=0, z=ele.center.z + cellz/2}, dcsCommon.uuid(theZone.name), "Windsock", 0, false)
dcsCommon.createStaticObjectForCoalitionAtLocation(0, {x=ele.center.x + cellx/2, y=0, z=ele.center.z + cellz/2}, dcsCommon.uuid(theZone.name), "Windsock", 0, false)
end
ele.fxpos = {x=xf, y=yc, z=zf}
ele.myType = land.getSurfaceType(lp) -- LAND=1, SHALLOW_WATER=2, WATER=3, ROAD=4, RUNWAY=5
-- we don not burn if a cell has shallow or deep water, or roads or runways
ele.inside = theZone:pointInZone(ele.center)
if theZone.freeBorder then
if x == 1 or x == numX then ele.inside = false end
if z == 1 or z == numZ then ele.inside = false end
end
ele.myStage = 0 -- not burning
if ele.inside and ele.myType == 1 then -- land only
-- create a position for the fire -- visual only
if theZone.stagger then
repeat
xf = xc + (0.5 - math.random()) * cellx
zf = zc + (0.5 - math.random()) * cellz
lp = {x=xf, y=zf}
until land.getSurfaceType(lp) == 1
ele.fxpos = {x=xf, y=yc, z=zf}
end
-- place a fire on the cell
if theZone.fullBlaze then
ele.fxname = dcsCommon.uuid(theZone.name)
trigger.action.effectSmokeBig(ele.fxpos, 4, 0.5 , ele.fxname)
ele.myStage = inferno.maxStages
ele.fxsize = 4
theZone.burning = true
end
local sparkable = {x=x, z=z}
table.insert(goodCells, sparkable)
fCount = fCount + 1
else
ele.inside = false -- optim: not in poly or burnable
end
ele.fuel = theZone.fuel -- use rnd?
ele.eternal = theZone.eternal -- unlimited fuel
grid[x][z] = ele
end -- for z
end -- for x
if theZone.verbose then
trigger.action.outText("inferno: zone <" .. theZone.name .. "> has <" .. fCount .. "> hot spots", 30)
end
if fCount < 1 then
trigger.action.outText("WARNING: <" .. theZone.name .. "> has no good burn cells!", 30)
end
theZone.numX = numX
theZone.numZ = numZ
theZone.minx = minx
theZone.minz = minz
theZone.xside = xside
theZone.grid = grid
theZone.goodCells = goodCells
-- find bestCell from goodCells closest to center
local bestdist = math.huge
local bestCell = nil
for idx, aCell in pairs(goodCells) do
local x = aCell.x
local z = aCell.z
local ele = grid[x][z]
local cp = ele.center
local d = dcsCommon.dist(cp, p)
if d < bestdist then
bestCell = aCell
bestdist = d
end
end
theZone.bestCell = bestCell
end
function inferno.readZone(theZone)
theZone.cellSize = theZone:getNumberFromZoneProperty("cellSize", inferno.cellSize)
-- FUEL: amount of fuel to burn PER CELL. when at zero, fire goes out
-- expansion: make it a random range
theZone.fuel = theZone:getNumberFromZoneProperty("fuel", 100)
theZone.rndLoc = theZone:getBoolFromZoneProperty("rndLoc", false)
theZone.freeBorder = theZone:getBoolFromZoneProperty("freeBorder", true) -- ring zone with non-burning zones
theZone.eternal = theZone:getBoolFromZoneProperty("eternal", true)
theZone.stagger = theZone:getBoolFromZoneProperty("stagger", true) -- randomize inside cell
theZone.fullBlaze = theZone:getBoolFromZoneProperty("fullBlaze", false )
theZone.canSpread = theZone:getBoolFromZoneProperty("canSpread", true)
theZone.maxSpread = theZone:getNumberFromZoneProperty("maxSpread", 999999)
theZone.impactSmoke = theZone:getBoolFromZoneProperty("impactSmoke", inferno.impactSmoke)
theZone.markCell = theZone:getBoolFromZoneProperty("markCell", false)
inferno.buildGrid(theZone)
theZone.heroes = {} -- remembers all who dropped into zone
theZone.onStart = theZone:getBoolFromZoneProperty("onStart", false)
if theZone:hasProperty("ignite?") then
theZone.ignite = theZone:getStringFromZoneProperty("ignite?", "none")
theZone.lastIgnite = trigger.misc.getUserFlag(theZone.ignite)
end
if theZone:hasProperty("douse?") then
theZone.douse = theZone:getStringFromZoneProperty("douse?", "none")
theZone.lastDouse = trigger.misc.getUserFlag(theZone.douse)
end
if theZone:hasProperty("extinguished!") then
theZone.extinguished = theZone:getStringFromZoneProperty("extinguished", "none")
end
end
--
-- API for water droppers
--
function inferno.surroundDelta(theZone, p, x, z)
if x < 1 then return math.huge end
if z < 1 then return math.huge end
if x > theZone.numX then return math.huge end
if z > theZone.numZ then return math.huge end
local ele = theZone.grid[x][z]
if not ele then return math.huge end
if not ele.inside then return math.huge end
if not ele.sparked then return math.huge end
if ele.myStage < 1 then return math.huge end
return dcsCommon.dist(p, ele.fxpos)
end
function inferno.waterInZone(theZone, p, amount)
-- water dropped (as point source) into a inferno zone.
-- find the cell that it was dropped in
local x = p.x - theZone.minx
local z = p.z - theZone.minz
local xc = math.floor(x / theZone.cellSize) + 1 -- square cells!
local zc = math.floor(z / theZone.cellSize) + 1
local ele = theZone.grid[xc][zc]
if not ele then
trigger.action.outText("Inferno: no ele for <" .. theZone.name .. ">: x<" .. x .. ">z<" .. z .. ">", 30)
return "NIL ele for x" .. xc .. ",z" .. zc .. " in " .. theZone.name
end
-- empty ele pre-proccing:
-- if not burning, see if we find a better burning cell nearby
if (not ele.sparked) or ele.extinguished then -- not burning, note that we do NOT test inside here!
local hitDelta = math.sqrt(2) * theZone.cellSize -- dcsCommon.dist(p, ele.center) + 0.5 * theZone.cellSize -- give others a chance
local bestDelta = hitDelta
local ofx = 0
local ofz = 0
for dx = -1, 1 do
for dz = -1, 1 do
if dx == 0 and dz == 0 then -- skip this one
else
local newDelta = inferno.surroundDelta(theZone, p, xc + dx, zc + dz)
if newDelta < bestDelta then
bestDelta = newDelta
ofx = dx
ofz = dz
end
end
end
end
xc = xc + ofx
zc = zc + ofz
ele = theZone.grid[xc][zc]
-- else
-- trigger.action.outText("inferno dropper: NO ALIGNMENT, beds are burning.", 30)
end
if theZone.impactSmoke then
if inferno.verbose then
trigger.action.smoke(ele.center, 1) -- red is ele center
end
trigger.action.smoke(p, 4) -- blue is actual impact
end
-- inside?
if ele.inside then
-- force this cell's eternal to OFF, now it consumes fuel
ele.eternal = false -- from now on, this cell burns own fuel
if not ele.sparked then
-- not burning. remove all fuel, make it
-- extinguished so it won't catch fire in the future
ele.extinguished = true -- will now negatively contribute
ele.fuel = 0
return "Good peripheral delivery, will prevent spread."
end
-- calculate dispersal of water. the higher, the more dispersed
-- and less fuel on the ground is 'removed'
local dispAmount = amount -- currently no dispersal, full amount hits ground
ele.fuel = ele.fuel - dispAmount
-- we can restage fx to smaller fire and reset stage
-- so fire consumes less fuel ?
-- NYI, later.
return "Direct delivery into fire cell!"
end
-- not inside or a water tile
return nil
end
function inferno.waterDropped(p, amount, data) -- if returns non-nil, has hit a cell
-- p is (x, 0, z) of where the water hits the ground
for name, theZone in pairs(inferno.zones) do
if theZone:pointInZone(p) then
if inferno.verbose then
trigger.action.outText("inferno: INSIDE <" .. theZone.name .. ">", 30)
end
-- if available, remember and increase the number of drops in
-- zone for player
if data and data.pName then
if not theZone.heroes then theZone.heroes = {} end
if theZone.heroes[data.pName] then
theZone.heroes[data.pName] = theZone.heroes[data.pName] + 1
else
theZone.heroes[data.pName] = 1
end
-- trigger.action.outText("registering <" .. data.pName .. "> for drop in <" .. theZone.name .. ">", 30)
-- else
-- trigger.action.outText("Not registered, no data", 30)
end
return inferno.waterInZone(theZone, p, amount)
end
end
if inferno.impactSmoke then
-- mark the position with a blue smoke
trigger.action.smoke(p, 4)
end
if inferno.verbose then
trigger.action.outText("water drop outside any inferno zone", 30)
end
return nil
end
--
-- IGNITE & DOUSE
--
function inferno.sparkCell(theZone, x, z)
local ele = theZone.grid[x][z]
if not ele.inside then
if theZone.verbose then
trigger.action.outText("ele x<" .. x .. ">z<" .. z .. "> is outside, no spark!", 30)
end
return
false end
ele.fxname = dcsCommon.uuid(theZone.name)
trigger.action.effectSmokeBig(ele.fxpos, 1, 0.5 , ele.fxname)
ele.myStage = 1
ele.fxsize = 1
ele.sparked = true
return true
end
function inferno.ignite(theZone)
if theZone.burning then
-- later expansion: add more fires
-- will give error when fullblaze is set
trigger.action.outText("Zone <" .. theZone.name .. "> already burning", 30)
return
end
if theZone.verbose then
trigger.action.outText("igniting <" .. theZone.name .. ">", 30)
end
local midNum = math.floor((#theZone.goodCells + 1)/ 2)
if midNum < 1 then midNum = 1 end
local midCell = theZone.bestCell
if not midCell then midCell = theZone.goodCells[midNum] end
if theZone.rndLoc then midCell = dcsCommon.pickRandom(theZone.goodCells) end
local x = midCell.x
local z = midCell.z
if inferno.sparkCell(theZone, x, z) then
if theZone.verbose then
trigger.action.outText("Sparking cell x<" .. x .. ">z<" .. z .. "> for <" .. theZone.name .. ">", 30)
end
else
trigger.action.outText("Inferno: fire in <" .. theZone.name .. "> @ center x<" .. x .. ">z<" .. z .. "> didin't catch", 30)
end
theZone.hasSpread = 0 -- how many times we have spread
theZone.burning = true
end
function inferno.startFire(theZone)
if theZone.burning then
return
end
inferno.ignite(theZone)
end
function inferno.douseFire(theZone)
-- if not theZone.burning then
-- return
-- end
-- walk the grid, and kill all flames, set all eles
-- to end state
for x=1, theZone.numX do
for z=1, theZone.numZ do
local ele = theZone.grid[x][z]
if ele.inside then
if ele.fxname then
trigger.action.effectSmokeStop(ele.fxname)
end
end
end
end
inferno.buildGrid(theZone) -- prep next fire in here
theZone.heroes = {}
theZone.burning = false
end
--
-- Fire Tick: progress/grow fire, burn fuel, expand conflagration etc.
--
function inferno.fireUpdate() -- update all burning fires
--[[--
every tick, we progress the fire status of all cells
fire stages (per cell):
< 1 : not burning, perhaps dying
0 : not burning, not smoking
-1..-5 : smoking. the closer to 0, less smoke
1..10 : burning, number = flame size, contib. heat and fuel consumption
--]]--
timer.scheduleFunction(inferno.fireUpdate, {}, timer.getTime() + inferno.fireTick) -- next time
for zName, theZone in pairs(inferno.zones) do
if theZone.fullBlaze then
-- do nothing, just testing layout
elseif theZone.burning then
inferno.burnOneTick(theZone) -- expand fuel, see if it spreads
end
end
end
function inferno.burnOneTick(theZone)
-- iterate all cells and see if the fire spreads
local isBurning = false
local grid = theZone.grid
local newStage = {} -- new states
local numX = theZone.numX
local numZ = theZone.numZ
-- pass 1:
-- calculate new stages
for x = 1, numX do -- up
newStage[x] = {}
for z = 1, numZ do
local ele = grid[x][z]
if ele.inside then
local stage = ele.myStage
-- we will only continue burning if we have fuel
if ele.extinguished then
-- we are drenched and can't re-ignite
newStage[x][z] = 0
elseif ele.fuel > 0 then -- we have fuel
if stage > 0 then -- it's already burning
if not ele.eternal then
ele.fuel = ele.fuel - stage -- consume fuel if burning and not eternal
if theZone.verbose then
trigger.action.outText(stage .. " fuel consumed. remain: "..ele.fuel, 30)
end
end
stage = stage + 1
if stage > inferno.maxStages then
stage = inferno.maxStages
end
newStage[x][z] = stage -- fire is growing
elseif stage < 0 then
-- fire is dying, can't be here if fuel > 0
newStage[x][z] = stage
else -- not burning. see if the surrounding sides are contributing
if theZone.canSpread and (theZone.hasSpread < theZone.maxSpread) then
local accu = 0
-- now do all surrounding 8 fields
-- NOTE: use wind direction to modify below if we use wind - NYI
accu = accu + inferno.contribute(x-1, z-1, theZone)
accu = accu + inferno.contribute(x, z-1, theZone)
accu = accu + inferno.contribute(x+1, z-1, theZone)
accu = accu + inferno.contribute(x-1, z, theZone)
accu = accu + inferno.contribute(x+1, z, theZone)
accu = accu + inferno.contribute(x-1, z+1, theZone)
accu = accu + inferno.contribute(x, z+1, theZone)
accu = accu + inferno.contribute(x+1, z+1, theZone)
accu = accu / 2 -- half intensity
-- 10% chance to spread when above threshold
if accu > inferno.threshold and math.random() < 0.1 then
stage = 1 -- start small fire
theZone.hasSpread = theZone.hasSpread + 1
end
end
newStage[x][z] = stage
end
else -- fuel is spent let flames die down if they exist
if stage == 0 then -- wasn't burning before
newStage[x][z] = 0
else
if stage > 0 then
newStage[x][z] = stage - 1
if newStage[x][z] == 0 then
newStage[x][z] = - 5
end
else
newStage[x][z] = stage + 1
end
end
end
else
newStage[x][z] = 0 -- outside, will always be 0
end
end -- for z
end -- for x
-- pass 2:
-- see what changed and handle accordingly
for x = 1, numX do -- up
for z = 1, numZ do
local ele = grid[x][z]
if ele.inside then
local stage = ele.myStage
local ns = newStage[x][z]
if not ns then ns = 0 end
if theZone.verbose and ele.sparked then
trigger.action.outText("x<" .. x .. ">z<" .. z .. "> - next stage is " .. ns, 30)
end
if ns ~= stage then
-- fire has changed: spread or dying down
if stage == 0 then -- fire has spread!
if theZone.verbose then
trigger.action.outText("Fire in <" .. theZone.name .. "> has spread to x<" .. x .. ">z<" .. z .. ">", 30)
end
ele.sparked = true
elseif ns == 0 then -- fire has died down fully
if theZone.verbose then
trigger.action.outText("Fire in <" .. theZone.name .. "> at x<" .. x .. ">z<" .. z .. "> has been extinguished", 30)
end
end
-- handle fire fx
-- determine fx number
local fx = 0
if stage > 0 then
fx = math.floor(stage / 2) -- 1..10--> 1..4
if fx < 1 then fx = 1 end
if fx > 4 then fx = 4 end
isBurning = true
elseif stage < 0 then
fx = 4-stage -- -5 .. -1 --> 6..10
if fx < 5 then fx = 5 end
if fx > 8 then fx = 8 end
isBurning = true -- keep as 'burning'
end
if fx ~= ele.fxsize then
if ele.fxname then
if theZone.verbose then
trigger.action.outText("removing old fx <" .. ele.fxsize .. "> [" .. ele.fxname .. "] for <" .. fx .. "> in <" .. theZone.name .. "> x<" .. x .. ">z<" .. z .. ">", 30)
end
-- remove old fx
trigger.action.effectSmokeStop(ele.fxname)
end
-- start new
if fx > 0 then
ele.fxname = dcsCommon.uuid(theZone.name)
trigger.action.effectSmokeBig(ele.fxpos, fx, 0.5 , ele.fxname)
else
if theZone.verbose then
trigger.action.outText("expiring <" .. theZone.name .. "> x<" .. x .. ">z<" .. z .. ">", 30)
end
end
ele.fxsize = fx
end
-- save new stage
ele.myStage = ns
else
if not ele.sparked then
-- not yet ignited, ignore
elseif ele.extinguished then
-- ignore, we are already off
elseif stage ~= 0 then
-- still burning bright
isBurning = true
else
-- remove last fx
trigger.action.effectSmokeStop(ele.fxname)
-- clear this zone or add debris now?
ele.extinguished = true -- now can't re-ignite
end
end -- if ns <> stage
end -- if inside
end -- for z
end -- for x
if not isBurning then
trigger.action.outText("inferno in <" .. theZone.name .. "> has been fully extinguished", 30)
theZone.burning = false
if theZone.extinguished then
theZone:pollFlag(theZone.extinguished, "inc")
end
inferno.invokeFireExtinguishedCB(theZone)
-- also fully douse this one, so we can restart it later
inferno.douseFire(theZone)
end
end
function inferno.contribute(x, z, theZone)
-- a cell starts burning if there is fuel
-- and the total contribution of all surrounding cells is
-- 10 or more, meaning that a 10 fire will ignite all surrounding
-- fields
-- bounds check
if x < 1 then return 0 end
if z < 1 then return 0 end
local numX = theZone.numX
if x > numX then return 0 end
local numZ = theZone.numZ
if z > numZ then return 0 end
local ele = theZone.grid[x][z]
if not ele.inside then return 0 end
if not ele.sparked then return 0 end -- not burning
if ele.extinguished then return -2 end -- water spill dampens
-- return stage that we are in if > 0
if ele.myStage >= 0 then
return ele.myStage -- mystage is positive int, "heat" 1..10
end
return 0
end
--
-- UPDATE
--
function inferno.update() -- for flag polling etc
timer.scheduleFunction(inferno.update, {}, timer.getTime() + 1/inferno.ups)
for idx, theZone in pairs (inferno.zones) do
if theZone.ignite and
theZone:testZoneFlag(theZone.ignite, "change", "lastIgnite") then
inferno.startFire(theZone)
end
if theZone.douse and theZone:testZoneFlag(theZone.douse, "change", "lastDouse") then
inferno.douseFire(theZone)
end
end
end
--
-- CONFIG & START
--
function inferno.readConfigZone()
inferno.name = "infernoConfig" -- make compatible with dml zones
local theZone = cfxZones.getZoneByName("infernoConfig")
if not theZone then
theZone = cfxZones.createSimpleZone("infernoConfig")
end
inferno.verbose = theZone.verbose
inferno.ups = theZone:getNumberFromZoneProperty("ups", 1)
inferno.fireTick = theZone:getNumberFromZoneProperty("fireTick", 10)
inferno.cellSize = theZone:getNumberFromZoneProperty("cellSize", 100)
inferno.menuName = theZone:getStringFromZoneProperty("menuName", "Firefighting")
inferno.impactSmoke = theZone:getBoolFromZoneProperty("impactSmoke", false)
if theZone:hasProperty("attachTo:") then
local attachTo = theZone:getStringFromZoneProperty("attachTo:", "<none>")
if radioMenu then -- requires optional radio menu to have loaded
local mainMenu = radioMenu.mainMenus[attachTo]
if mainMenu then
inferno.mainMenu = mainMenu
else
trigger.action.outText("+++inferno: cannot find super menu <" .. attachTo .. ">", 30)
end
else
trigger.action.outText("+++inferno: REQUIRES radioMenu to run before inferno. 'AttachTo:' ignored.", 30)
end
end
end
function inferno.start()
if not dcsCommon.libCheck then
trigger.action.outText("cfx inferno requires dcsCommon", 30)
return false
end
if not dcsCommon.libCheck("cfx inferno", inferno.requiredLibs) then
return false
end
-- read config
inferno.readConfigZone()
-- process inferno Zones
local attrZones = cfxZones.getZonesWithAttributeNamed("inferno")
for k, aZone in pairs(attrZones) do
inferno.readZone(aZone) -- process attributes
inferno.addZone(aZone) -- add to list
end
-- start update (DML)
timer.scheduleFunction(inferno.update, {}, timer.getTime() + 1/inferno.ups)
-- start fire tick update
timer.scheduleFunction(inferno.fireUpdate, {}, timer.getTime() + inferno.fireTick)
-- start all zones that have onstart
for gName, theZone in pairs(inferno.zones) do
if theZone.onStart then
inferno.ignite(theZone)
end
end
-- say Hi!
trigger.action.outText("cf/x inferno v" .. inferno.version .. " started.", 30)
return true
end
if not inferno.start() then
trigger.action.outText("inferno failed to start up")
inferno = nil
end
--[[--
"ele" structure in grid
- fuel amount of fuel to burn. when < 0 the fire starves over the next cycles. By dumping water helicopters/planes reduce amount of available fuel
- extinguished if true, can't re-ignite
- myStage -- FSM for flames: >0 == burning, consumes fuel, <0 is starved of fuel and get smaller each tick
- fxname to reference flame fx
- eternal if true does not consume fuel. goes false when the first drop of water enters the cell from players
- center point of center
- fxpos - point in cell that has the fx
- mytype land type. only 1 burns
- inside is it inside the zone and can burn? true if so. used to make cells unburnable
- sparked if false this isn't burning
to do:
OK - callback for extinguis
OK - remember who contributes dropped inside successful and then receives ack when zone fully doused
Possible enhancements
- random range fuel
- wind for contribute (can precalc into table)
- boom in ignite
- clear after burn out
- leave debris after burn out, mabe place in ignite
--]]--

View File

@ -1,5 +1,5 @@
limitedAirframes = {}
limitedAirframes.version = "1.6.0"
limitedAirframes.version = "1.7.0"
limitedAirframes.warningSound = "Quest Snare 3.wav"
limitedAirframes.loseSound = "Death PIANO.wav"
@ -11,33 +11,6 @@ limitedAirframes.requiredLibs = {
}
--[[-- VERSION HISTORY
- 1.0.0 - initial version
- 1.0.1 - out text to coalition for switch
- less verbose
- win/lose sound by coalition
- 1.0.2 - corrected some to to-->for Groups typos
- 1.0.3 - renamed to 'pilot' instead of airframe:
pilotSafe attribute
- fixed MP bug, switched to EventMonII code
- 1.0.4 - added CSAR integration: create CSAR and callback
- safe ditch only at less than 7 kmh and 10m agl
- 1.0.5 - replaced 5 (crash) check for helos only
- 1.0.6 - changed alt and speed tests to inAir
reduced verbosity
made reporting of new units for coalition side only
- 1.0.7 - if unlimited pilots it says so when you return one
to base
- 1.0.8 - now can query remaining pilots
- 1.0.9 - better formatted remaining pilots
- 1.1.0 - module manager
- separated out settings
- hand change in pilotsafe zones that can be landed in
- 1.2.0 - limitedAirframesConfig zone
- 1.3.0 - added network dead override logic via unitFlownByPlayer
- 1.4.0 - DML integration, verbosity, clean-up, QoL improvements
redSafe, blueSafe with attribute, backward compatible
currRed
- 1.4.1 - removed dependency to cfxPlayer
- 1.5.0 - persistence support
- 1.5.1 - new "announcer" attribute
- 1.5.2 - integration with autoCSAR: prevent limitedAF from creating csar
@ -49,28 +22,17 @@ limitedAirframes.requiredLibs = {
- new hasUI attribute
- minor clean-up
- set numRed and numBlue on startup
1.7.0 - dcs jul-11, jul-22 bug prevention
- some cleanup
--]]--
-- limitedAirframes manages the number of available player airframes
-- per scenario and side. Each time a player crashes the plane
-- outside of safe zones, the number is decreased for that side
-- when the number reaches -1 or smaller, other side wins
-- !!!Only affects player planes!!
-- safe zones must have a property "pilotSafe"
-- - pilotSafe - this is a zone to safely change airframes in
-- - can also carry 'red' or 'blue' to enable
-- if zone can change ownership, player's coalition
-- is checked against current zone ownership
-- zone owner.
limitedAirframes.safeZones = {} -- safezones are zones where a crash or change plane does not
limitedAirframes.myEvents = {5, 9, 30, 6, 20, 21, 15 } -- 5 = crash, 9 - dead, 30 - unit lost, 6 - eject, 20 - enter unit, 21 - leave unit, 15 - birth
-- guarantee a min of 2 seconds between events
-- guarantee a minimum of 2 seconds between events
-- for this we save last event per player
limitedAirframes.lastEvents = {}
-- each time a plane crashes or is abandoned check
@ -196,10 +158,16 @@ end
-- and also adds the player if unknown
function limitedAirframes.addPlayerUnit(theUnit)
local theSide = theUnit:getCoalition()
local uName = theUnit:getName()
if not uName then uName = "**XXXX**" end
local pName = theUnit:getPlayerName()
if not pName then pName = "**????**" end
local uName = "**XXXX**"
if theUnit.getName then
uName = theUnit:getName()
end
-- if not uName then uName = "**XXXX**" end
local pName = "**????**"
if theUnit.getPlayerName then
pName = theUnit:getPlayerName()
end
-- if not pName then pName = "**????**" end
limitedAirframes.updatePlayer(pName, "alive")
local desc = "unit <" .. uName .. "> controlled by <" .. pName .. ">"
@ -273,6 +241,7 @@ end
--
-- E V E N T H A N D L I N G
--
--[[--
function limitedAirframes.XXXisKnownPlayerUnit(theUnit)
if not theUnit then return false end
local aName = theUnit:getName()
@ -281,6 +250,7 @@ function limitedAirframes.XXXisKnownPlayerUnit(theUnit)
end
return false
end
--]]--
function limitedAirframes.isInteresting(eventID)
-- return true if we are interested in this event, false else
@ -294,6 +264,7 @@ function limitedAirframes.preProcessor(event)
-- make sure it has an initiator
if not event.initiator then return false end -- no initiator
local theUnit = event.initiator
if not theUnit.getName then return false end -- DCS Jul-22 bug
local uName = theUnit:getName()
@ -353,6 +324,7 @@ function limitedAirframes.somethingHappened(event)
end
local theUnit = event.initiator
if not theUnit.getName then return end
local unitName = theUnit:getName()
local ID = event.id
local myType = theUnit:getTypeName()

View File

@ -1,5 +1,5 @@
cfxObjectSpawnZones = {}
cfxObjectSpawnZones.version = "2.1.0"
cfxObjectSpawnZones.version = "2.1.1"
cfxObjectSpawnZones.requiredLibs = {
"dcsCommon", -- common is of course needed for everything
-- pretty stupid to check for this since we
@ -15,7 +15,7 @@ cfxObjectSpawnZones.verbose = false
version history
2.0.0 - dmlZones
2.1.0 - autoTurn attribute
2.1.1 - now spawns the correct country
--]]--
-- respawn currently happens after theSpawns is deleted and cooldown seconds have passed
@ -246,7 +246,8 @@ function cfxObjectSpawnZones.spawnObjectNTimes(aSpawner, theType, n, container)
end
-- spawn in dcs
local theObject = coalition.addStaticObject(aSpawner.rawOwner, theStaticData) -- create in dcs
--local theObject = coalition.addStaticObject(aSpawner.rawOwner, theStaticData) -- create in dcs
local theObject = coalition.addStaticObject(aSpawner.country, theStaticData) -- create in dcs
table.insert(container, theObject) -- add to collection
if aSpawner.isCargo and aSpawner.managed then
if cfxCargoManager then
@ -303,7 +304,8 @@ function cfxObjectSpawnZones.spawnObjectNTimes(aSpawner, theType, n, container)
end
-- spawn in dcs
local theObject = coalition.addStaticObject(aSpawner.rawOwner, theStaticData) -- this will generate an event!
-- local theObject = coalition.addStaticObject(aSpawner.rawOwner, theStaticData) -- this will generate an event!
local theObject = coalition.addStaticObject(aSpawner.country, theStaticData) -- this will generate an event!
table.insert(container, theObject)
-- see if it is managed cargo
if aSpawner.isCargo and aSpawner.managed then

View File

@ -1,5 +1,5 @@
cfxOwnedZones = {}
cfxOwnedZones.version = "1.3.0"
cfxOwnedZones.version = "1.3.0gamma"
cfxOwnedZones.verbose = false
cfxOwnedZones.announcer = true
cfxOwnedZones.name = "cfxOwnedZones"
@ -61,6 +61,7 @@ cfxOwnedZones.name = "cfxOwnedZones"
- blueLost! zone output
- ownedBy direct zone output
- neutral! zone output
1.3.0g - DML 2.x compatibility
--]]--
cfxOwnedZones.requiredLibs = {
@ -1193,7 +1194,7 @@ function cfxOwnedZones.loadData()
end
theZone.owner = zData.owner
theZone.state = zData.state
if zData.conquered then
if zData.conquered and theZone.conqueredFlag then
cfxZones.setFlagValue(theZone.conqueredFlag, zData.conquered, theZone)
end
-- update mark in map

View File

@ -1,5 +1,5 @@
cfxOwnedZones = {}
cfxOwnedZones.version = "2.4.0"
cfxOwnedZones.version = "2.4.1"
cfxOwnedZones.verbose = false
cfxOwnedZones.announcer = true
cfxOwnedZones.name = "cfxOwnedZones"
@ -44,6 +44,7 @@ cfxOwnedZones.name = "cfxOwnedZones"
- code clean-up
2.3.1 - restored getNearestOwnedZoneToPoint
2.4.0 - dmlZones masterOwner update
2.4.1 - conquered flag now correctly guarded in loadData()
--]]--
cfxOwnedZones.requiredLibs = {
@ -761,7 +762,7 @@ function cfxOwnedZones.loadData()
local theZone = cfxOwnedZones.getOwnedZoneByName(zName)
if theZone then
theZone.owner = zData.owner
if zData.conquered then
if zData.conquered and theZone.conqueredFlag then
theZone:setFlagValue(theZone.conqueredFlag, zData.conquered)
end
-- update mark in map

View File

@ -1,10 +1,11 @@
radioMenu = {}
radioMenu.version = "4.0.0"
radioMenu.version = "4.0.1"
radioMenu.verbose = false
radioMenu.ups = 1
radioMenu.requiredLibs = {
"dcsCommon", -- always
"cfxZones", -- Zones, of course
"cfxMX",
}
-- note: cfxMX is optional unless using types or groups attributes
radioMenu.menus = {}
@ -18,6 +19,7 @@ radioMenu.lateGroups = {} -- dict by ID
detect cyclic references
4.0.0 - added infrastructure for dynamic players (DCS 2.9.6)
- added dynamic player support
4.0.1 - MX no longer optional, so ask for it
--]]--
function radioMenu.addRadioMenu(theZone)

View File

@ -930,14 +930,7 @@ function cfxReconMode.readConfigZone()
-- note: must match exactly!!!!
local theZone = cfxZones.getZoneByName("reconModeConfig")
if not theZone then
if cfxReconMode.verbose then
trigger.action.outText("+++rcn: no config zone!", 30)
end
theZone = cfxZones.createSimpleZone("reconModeConfig")
else
if cfxReconMode.verbose then
trigger.action.outText("+++rcn: found config zone!", 30)
end
end
cfxReconMode.verbose = theZone.verbose --cfxZones.getBoolFromZoneProperty(theZone, "verbose", false)

View File

@ -12,8 +12,17 @@ slotty.version = "1.1.0"
Version history
1.0.0 - Initial version
1.1.0 - "noSlotty" global disable flag, anti-mirror SSB flag
1.2.0 - better (delayed) nilling for cfxSSB ocupied clients to
- avoid a race condition
--]]--
function slotty.delayedSSBNil(args)
uName = args
if cfxSSBClient.verbose then
trigger.action.outText("slotty-->SSBClient: nilling <" .. args .. ">, now unoccupied", 30)
end
cfxSSBClient.occupiedUnits[uName] = nil
end
function slotty:onEvent(event)
if not event.initiator then return end
@ -42,7 +51,10 @@ function slotty:onEvent(event)
-- interface with SSBClient for compatibility
if cfxSSBClient and cfxSSBClient.occupiedUnits then
cfxSSBClient.occupiedUnits[uName] = nil
-- to resolve a race condition, we schedule nilling the
-- ssbClientSlot in 1/2 second
-- cfxSSBClient.occupiedUnits[uName] = nil
timer.scheduleFunction(slotty.delayedSSBNil, uName, timer.getTime() + 0.5)
end
if isSP then

View File

@ -1,9 +1,10 @@
stopGap = {}
stopGap.version = "1.2.0"
stopGap.version = "1.3.0"
stopGap.verbose = false
stopGap.ssbEnabled = true
stopGap.ignoreMe = "-sg"
stopGap.spIgnore = "-sp" -- only single-player ignored
stopGap.noParking = false -- turn on to ignore all 'from parking'
stopGap.isMP = false
stopGap.running = true
stopGap.refreshInterval = -1 -- seconds to refresh all statics. -1 = never, 3600 = once every hour
@ -15,7 +16,7 @@ stopGap.requiredLibs = {
"cfxMX",
}
--[[--
Written and (c) 2023 by Christian Franz
Written and (c) 2023-2024 by Christian Franz
Replace all player units with static aircraft until the first time
that a player slots into that plane. Static is then replaced with live player unit.
@ -55,6 +56,8 @@ stopGap.requiredLibs = {
1.2.0 - DCS dynamic player spawn compatibility
stopGaps only works with MX data, so we are good, no changes
required
1.3.0 - carriers in shallow waters also no longer handled as viable
- noParking option
--]]--
@ -102,11 +105,26 @@ function stopGap.isGroundStart(theGroup)
if action == "Turning Point" then return false end
if action == "Landing" then return false end
if action == "From Runway" then return false end
if stopGap.noParking then
local loAct = string.lower(action)
if loAct == "from parking area" or
loAct == "from parking area hot" then
if stopGap.verbose then
trigger.action.outText("StopG: Player Group <" .. theGroup.name .. "> NOPARKING: [" .. action .. "] must be skipped.", 30)
end
return false
end
end
-- looks like aircraft is on the ground
-- but is it in water (carrier)?
local u1 = theGroup.units[1]
local sType = land.getSurfaceType(u1) -- has fields x and y
if sType == 3 then return false end
if sType == 3 or sType == 2 then
if stopGap.verbose then
trigger.action.outText("StopG: Player Group <" .. theGroup.name .. "> SEA BASED: [" .. action .. "], land type " .. sType .. " should be skipped.", 30)
end
return false
end -- water & shallow water a no-go
if stopGap.verbose then
trigger.action.outText("StopG: Player Group <" .. theGroup.name .. "> GROUND BASED: " .. action .. ", land type " .. sType, 30)
end
@ -422,6 +440,7 @@ function stopGap.readConfigZone(theZone)
stopGap.verbose = theZone.verbose
stopGap.ssbEnabled = theZone:getBoolFromZoneProperty("ssb", true)
stopGap.enabled = theZone:getBoolFromZoneProperty("onStart", true)
stopGap.noParking = theZone:getBoolFromZoneProperty("noParking", false)
if theZone:hasProperty("on?") then
stopGap.turnOnFlag = theZone:getStringFromZoneProperty("on?", "*<none>")
stopGap.lastTurnOnFlag = trigger.misc.getUserFlag(stopGap.turnOnFlag)