diff --git a/Doc/DML Documentation.pdf b/Doc/DML Documentation.pdf index 7593082..6cc4ff6 100644 Binary files a/Doc/DML Documentation.pdf and b/Doc/DML Documentation.pdf differ diff --git a/Doc/DML Quick Reference.pdf b/Doc/DML Quick Reference.pdf index 9f01367..ff3ab4b 100644 Binary files a/Doc/DML Quick Reference.pdf and b/Doc/DML Quick Reference.pdf differ diff --git a/modules/FARPZones.lua b/modules/FARPZones.lua index 90733ae..cff1a6c 100644 --- a/modules/FARPZones.lua +++ b/modules/FARPZones.lua @@ -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 diff --git a/modules/LZ.lua b/modules/LZ.lua index 3b88816..6c9ae66 100644 --- a/modules/LZ.lua +++ b/modules/LZ.lua @@ -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 diff --git a/modules/airtank.lua b/modules/airtank.lua new file mode 100644 index 0000000..b1e8ff9 --- /dev/null +++ b/modules/airtank.lua @@ -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:", "") + 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 + diff --git a/modules/civHelo.lua b/modules/civHelo.lua index ff02a94..0b1cd0d 100644 --- a/modules/civHelo.lua +++ b/modules/civHelo.lua @@ -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 diff --git a/modules/convoy.lua b/modules/convoy.lua index 70454ba..05f0e25 100644 --- a/modules/convoy.lua +++ b/modules/convoy.lua @@ -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 --]]-- \ No newline at end of file diff --git a/modules/dcsCommon.lua b/modules/dcsCommon.lua index 04ddac7..305c415 100644 --- a/modules/dcsCommon.lua +++ b/modules/dcsCommon.lua @@ -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 diff --git a/modules/heloTroops.lua b/modules/heloTroops.lua index f8684a6..dd120f0 100644 --- a/modules/heloTroops.lua +++ b/modules/heloTroops.lua @@ -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) diff --git a/modules/inferno.lua b/modules/inferno.lua new file mode 100644 index 0000000..13053d2 --- /dev/null +++ b/modules/inferno.lua @@ -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:", "") + 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 +--]]-- \ No newline at end of file diff --git a/modules/limitedAirframes.lua b/modules/limitedAirframes.lua index 1efe929..e0c6b07 100644 --- a/modules/limitedAirframes.lua +++ b/modules/limitedAirframes.lua @@ -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() diff --git a/modules/objectSpawnZones.lua b/modules/objectSpawnZones.lua index 9197009..d83893b 100644 --- a/modules/objectSpawnZones.lua +++ b/modules/objectSpawnZones.lua @@ -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 diff --git a/modules/ownedZones (legacy 1.3.0).lua b/modules/ownedZones (legacy 1.3.0).lua index 792bdd8..e728074 100644 --- a/modules/ownedZones (legacy 1.3.0).lua +++ b/modules/ownedZones (legacy 1.3.0).lua @@ -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 diff --git a/modules/ownedZones.lua b/modules/ownedZones.lua index bfd1b00..0e0d7e4 100644 --- a/modules/ownedZones.lua +++ b/modules/ownedZones.lua @@ -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 diff --git a/modules/radioMenus.lua b/modules/radioMenus.lua index 39e0a14..436edce 100644 --- a/modules/radioMenus.lua +++ b/modules/radioMenus.lua @@ -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) diff --git a/modules/reconMode.lua b/modules/reconMode.lua index 62172d2..819359f 100644 --- a/modules/reconMode.lua +++ b/modules/reconMode.lua @@ -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) diff --git a/modules/slotty.lua b/modules/slotty.lua index 7dfc146..8d41f5b 100644 --- a/modules/slotty.lua +++ b/modules/slotty.lua @@ -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 diff --git a/modules/stopGaps.lua b/modules/stopGaps.lua index bad3ca4..95054a7 100644 --- a/modules/stopGaps.lua +++ b/modules/stopGaps.lua @@ -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?", "*") stopGap.lastTurnOnFlag = trigger.misc.getUserFlag(stopGap.turnOnFlag) diff --git a/tutorial & demo missions/demo - artillery zones triggered.miz b/tutorial & demo missions/demo - artillery zones triggered.miz index fe88f65..bec3773 100644 Binary files a/tutorial & demo missions/demo - artillery zones triggered.miz and b/tutorial & demo missions/demo - artillery zones triggered.miz differ