DML/modules/csarManager2.lua
Christian Franz cc46e5fb10 Version 0.9987
Limited Airframes
cloneZones randomizer, cargo
2022-05-05 13:55:03 +02:00

1314 lines
45 KiB
Lua

csarManager = {}
csarManager.version = "2.1.1"
csarManager.verbose = false
csarManager.ups = 1
--[[-- VERSION HISTORY
- 1.0.0 initial version
- 1.0.1 - smoke optional
- airframeCrashed method for airframe manager
- removed '(downed )' when re-picked up
- fixed oclock
- 1.0.2 - hover retrieval
- 1.0.3 - corrected a bug in oclock during hovering
- 1.0.4 - now correctly allocates pilot to coalition via dcscommon.coalition2county
- 1.1.0 - pilot adds weight to unit
- module check
- 2.0.0 - weight managed via cargoSuper
- 2.0.1 - getCSARBaseforZone()
- check if zone landed in has owner attribute
to provide compatibility with owned zones,
FARPZones etc that keep zone.owner up to date
- 2.0.2 - use parametric csarManager.hoverAlt
- use hoverDuration
- 2.0.3 - corrected bug in hoverDuration
- 2.0.4 - guard in createCSARMission for cfxCommander
- 2.1.0 - startCSAR?
- deferrable missions
- verbose
- ups
- useSmoke
- smokeColor
- reworked smoking the loc
- config zone
- csarRedDelivered
- csarBlueDelivered
- finally fixed smoke performance bug
- csarManager.vectoring optional
- 2.1.1 - zone-local verbosity
--]]--
-- modules that need to be loaded BEFORE I run
csarManager.requiredLibs = {
"dcsCommon", -- common is of course needed for everything
"cfxZones", -- zones management foc CSAR and CSAR Mission zones
"cfxPlayer", -- player monitoring and group monitoring
"nameStats", -- generic data module for weight
"cargoSuper",
-- "cfxCommander", -- needed if you want to hand-create CSAR missions
}
-- *** DOES NOT EXTEND ZONES *** BUT USES OWN STRUCT
-- *** extends zones for csarMission zones though
--[[--
CSAR MANAGER
============
This module can create and manage CSAR missions, i.e.
create a unit on the ground, mark it on the map, handle
if the unit is killed, create enemies in the vicinity
It will install a menu in any troop helicopter as
determined by dcsCommon.isTroopCarrier() with the
option to list available csar mission. for each created mission
it will give range and frequency for ADF
When a helicopter is in range, it will set smoke to better
visually identify the location.
When the helicopter lands close enough to a downed pilot,
the pilot is picket up automatically. Their weight is added
to the unit, so it may overload!
When the helicopter than lands in a CSARBASE Zone, the mission is
a success and a success callback is invoked automatically for
all picked up groups. All zones that have the CSARBASE property are
CSAR Bases, but their coalition must be either neutral or match the
one of the unit that landed
On start, it scans all zones for a CSAR property, and creates
a CSAR mission with data taken from the properties in the
zone so you can easily create CSAR missions in ME
WARNING: ASSUMES SINGLE UNIT PLAYER GROUPS
==========================================
Main Interface
- createCSARMission(location, side, numCrew, mark, clearing, timeout)
creates a csar mission that can be tracked.
location is the position on the map
side is the side the unit is on (neutal is for any side)
numCrew the number of people (1-4)
mark true if marked on map
clearing will create a clearing
timeout - time in seconds until pilots die. timer stops on pickup
RETURNS true, "ok" -- false, "fail reason" (string)
- createCSARAdversaries(location, side, numEnemies, radius, maxRadius)
creates some random infantery randomized on a circle around the location
location - center, usually where the downed pilot is
side - side of the enemy red/blue
numEnemies - number of infantry
radius[, maxRadius] distance of the enemy troops
- in ME, create at least one zone with a property named "CSARBASE" for
each side that supports csar missions. This is where the players
can drop off pilots that they rescued. If you have no CSARBASE zone
defined, you'll receive a warning for that side when you attempt a
rescue
- in ME you can place zones with a CSAR attribute that will generate
a scar mission. Further attributes are "coalition" (red/blue), "name" (any name you like) and "freq" (for elt ADR, leave empty for random)
NOTE:
CSARBASE is compatible with the FARP Attribute of
FARP Zones
--]]--
--
-- OPTIONS
--
csarManager.useSmoke = false -- smoke is a performance killer, so you can turn it off
csarManager.smokeColor = 4 -- when using smoke
-- unitConfigs contain the config data for any helicopter
-- currently in the game. The Array is indexed by unit name
csarManager.unitConfigs = {}
csarManager.myEvents = {3, 4, 5} -- 3 = take off, 4 = land, 5 = crash
--
-- CASR MISSION
--
csarManager.openMissions = {} -- all currently available missions
csarManager.csarBases = {} -- all bases where we can drop off rescued pilots
csarManager.csarZones = {} -- zones for spawning
csarManager.missionID = 1 -- to create uuid
csarManager.rescueRadius = 70 -- must land within 50m to rescue
csarManager.hoverRadius = 30 -- must hover within 10m of unit
csarManager.hoverAlt = 40 -- must hover below this alt
csarManager.hoverDuration = 20 -- must hover for this duration
csarManager.rescueTriggerRange = 2000 -- when the unit pops smoke and radios
csarManager.beaconSound = "Radio_beacon_of_distress_on_121,5_MHz.ogg"
csarManager.pilotWeight = 120 -- kg for the rescued person. added to the unit's weight
csarManager.vectoring = true -- provide bearing and range
--
-- callbacks
--
csarManager.csarCompleteCB = {}
--
-- CREATING A CSAR
--
function csarManager.createDownedPilot(theMission)
if not cfxCommander then
trigger.action.outText("+++CSAR: can't create mission, module cfxCommander is missing.", 30)
return
end
local aLocation = {}
local aHeading = 0 -- in rads
local newTargetZone = theMission.zone
aLocation, aHeading = dcsCommon.randomPointOnPerimeter(newTargetZone.radius / 2 + 3, newTargetZone.point.x, newTargetZone.point.z)
local theBoyGroup = dcsCommon.createSingleUnitGroup(theMission.name,
"Soldier M4 GRG", -- "Soldier M4 GRG",
aLocation.x,
aLocation.z,
-aHeading + 1.5) -- + 1.5 to turn inwards
-- WARNING:
-- coalition.addGroup takes the COUNTRY of the group, and derives the
-- coalition from that. So if mission.sie is 0, we use UN, if it is 1 (red) it
-- is joint red, if 2 it is joint blue
local theSideCJTF = dcsCommon.coalition2county(theMission.side) -- get the correct county CJTF
theMission.group = coalition.addGroup(theSideCJTF,
Group.Category.GROUND,
theBoyGroup)
if theBoyGroup then
else
trigger.action.outText("+++csar: FAILED to create csar!", 30)
end
-- we now use commands to send radio transmissions
local ADF = 20 + math.random(90)
if theMission.freq then ADF = theMission.freq else theMission.freq = ADF end
local theCommands = cfxCommander.createCommandDataTableFor(theMission.group)
local cmd = cfxCommander.createSetFrequencyCommand(ADF) -- freq in 10000 Hz
cfxCommander.addCommand(theCommands, cmd)
cmd = cfxCommander.createTransmissionCommand(csarManager.beaconSound)
cfxCommander.addCommand(theCommands, cmd)
cfxCommander.scheduleCommands(theCommands, 2) -- in 2 seconds, so unit has time to percolate through DCS
end
function csarManager.createCSARMissionData(point, theSide, freq, name, numCrew, timeLimit, mapMarker, inRadius)
-- create a type
if not timeLimit then timeLimit = -1 end
if not point then return nil end
local newMission = {}
newMission.side = theSide
if dcsCommon.stringStartsWith(name, "(downed) ") then
-- remove "downed" - it will be added again later
name = dcsCommon.removePrefix(name, "(downed) ")
end
if not inRadius then inRadius = csarManager.rescueRadius end
newMission.name = "(downed) " .. name .. "-" .. csarManager.missionID -- make it uuid-capable
newMission.zone = cfxZones.createSimpleZone(newMission.name, point, inRadius) --csarManager.rescueRadius)
newMission.marker = mapMarker -- so it can be removed later
newMission.isHot = false -- creating adversaries will make it hot, or when units are near. maybe implement a search later?
-- detection and load stuff
newMission.lastSmokeTime = -1000 -- so it will smoke immediately
newMission.messagedUnits = {} -- so we remember whom the unit radioed
newMission.hoveringUnits = {} -- used when hovering
newMission.freq = freq -- if nil will make random
-- allocate units
csarManager.createDownedPilot(newMission)
-- update counter and return
csarManager.missionID = csarManager.missionID + 1
return newMission
end
function csarManager.addMission(theMission)
table.insert(csarManager.openMissions, theMission)
end
function csarManager.removeMission(theMission)
if not theMission then return end
local newMissions = {}
for idx, aMission in pairs (csarManager.openMissions) do
if aMission ~= theMission then
table.insert(newMissions, aMission)
else
end
end
csarManager.openMissions = newMissions -- this is the new batch
end
function csarManager.removeMissionForGroup(theDownedGroup)
if not theDownedGroup then return end
local newMissions = {}
for idx, aMission in pairs (csarManager.openMissions) do
if aMission.group ~= theDownedGroup then
table.insert(newMissions, aMission)
else
end
end
csarManager.openMissions = newMissions -- this is the new batch
end
--
-- UNIT CONFIG
--
function csarManager.resetConfig(conf)
-- reset only ovberwrites mission-relevant data
conf.troopsOnBoard = {} -- number of rescued missions
local myName = conf.name
cargoSuper.removeAllMassForCargo(myName, "Evacuees") -- will allocate new empty table
conf.currentState = -1 -- indetermined, 0 = landed 1 = airborne
conf.timeStamp = timer.getTime()
end
function csarManager.createDefaultConfig(theUnit)
local conf = {}
conf.theUnit = theUnit
conf.name = theUnit:getName()
csarManager.resetConfig(conf)
--conf.unit = {} -- the unit this is linked to
conf.myMainMenu = nil -- this is the main menu for group
conf.myCommands = nil -- all commands in sub menu
conf.id = theUnit:getID()
return conf
end
function csarManager.getUnitConfig(theUnit) -- will create new config if not existing
if not theUnit then
trigger.action.outText("+++csar: nil unit in get config!", 30)
return nil
end
local uName = theUnit:getName()
local c = csarManager.getConfigForUnitNamed(uName)
if not c then
c = csarManager.createDefaultConfig(theUnit)
csarManager.unitConfigs[uName] = c
end
return c
end
function csarManager.getConfigForUnitNamed(aName)
return csarManager.unitConfigs[aName]
end
function csarManager.removeConfigForUnitNamed(aName)
if not aName then return end
if csarManager.unitConfigs[aName] then csarManager.unitConfigs[aName] = nil end
end
--
-- E V E N T H A N D L I N G
--
function csarManager.isInteresting(eventID)
-- return true if we are interested in this event, false else
for key, evType in pairs(csarManager.myEvents) do
if evType == eventID then return true end
end
return false
end
function csarManager.preProcessor(event)
-- make sure it has an initiator
if not event.initiator then return false end -- no initiator
local theUnit = event.initiator
local cat = theUnit:getCategory()
if cat ~= Unit.Category.HELICOPTER then
return false
end
--trigger.action.outText("+++csar: event " .. event.id .. " for cat = " .. cat .. " (helicopter?) unit " .. theUnit:getName(), 30)
if not cfxPlayer.isPlayerUnit(theUnit) then
--trigger.action.outText("+++csar: rejected event: " .. theUnit:getName() .. " not a player helo", 30)
return false
end -- not a player unit
return csarManager.isInteresting(event.id)
end
function csarManager.postProcessor(event)
-- don't do anything for now
end
function csarManager.somethingHappened(event)
-- when this is invoked, the preprocessor guarantees that
-- it's an interesting event
-- unit is valid and player
-- airframe category is helicopter
local theUnit = event.initiator
local ID = event.id
local myType = theUnit:getTypeName()
if ID == 4 then -- landed
csarManager.heloLanded(theUnit)
end
if ID == 3 then -- take off
csarManager.heloDeparted(theUnit)
end
if ID == 5 then -- crash
csarManager.heloCrashed(theUnit)
end
csarManager.setCommsMenu(theUnit)
end
--
--
-- CSAR LANDED
--
--
function csarManager.successMission(who, where, theMission)
trigger.action.outTextForCoalition(theMission.side,
who .. " successfully evacuated " .. theMission.name .. " to " .. where .. "!",
30)
-- now call callback for coalition side
-- callback has format callback(coalition, success true/false, numberSaved, descriptionText)
csarManager.invokeCallbacks(theMission.side, true, 1, "success")
trigger.action.outSoundForCoalition(theMission.side, "Quest Snare 3.wav")
if csarManager.csarRedDelivered and theMission.side == 1 then
cfxZones.pollFlag(csarManager.csarRedDelivered, "inc", csarManager.configZone)
end
if csarManager.csarBlueDelivered and theMission.side == 2 then
cfxZones.pollFlag(csarManager.csarBlueDelivered, "inc", csarManager.configZone)
end
if csarManager.csarDelivered then
cfxZones.pollFlag(csarManager.csarDelivered, "inc", csarManager.configZone)
trigger.action.outText("+++csar: banging csarDelivered: <" .. csarManager.csarDelivered .. ">", 30)
end
end
function csarManager.heloLanded(theUnit)
-- when we have landed,
if not dcsCommon.isTroopCarrier(theUnit) then return end
local conf = csarManager.getUnitConfig(theUnit)
conf.unit = theUnit
local theGroup = theUnit:getGroup()
conf.id = theGroup:getID()
--conf.id = theUnit:getID()
conf.currentState = 0
local thePoint = theUnit:getPoint()
local mySide = theUnit:getCoalition()
local myName = theUnit:getName()
-- first, check if we have landed in a CSAR dropoff zone
-- if so, drop off all loaded csar troops and award the
-- points or airframes
local allEvacuees = cargoSuper.getManifestFor(myName, "Evacuees") -- returns unlinked array
if #allEvacuees > 0 then -- wasif #conf.troopsOnBoard > 0 then
for idx, base in pairs(csarManager.csarBases) do
-- check if the attached zone has changed hands
-- this can happen if zone has its own owner
-- attribute and is conquered by another side
local currentBaseSide = base.side
if base.zone.owner then
-- this zone is shared with capturable (owned)
-- zone extensions like owned zone, FARP etc.
-- use current owner
currentBaseSide = base.zone.owner
end
if currentBaseSide == mySide or
currentBaseSide == 0
then -- can always land in neutral
if cfxZones.pointInZone(thePoint, base.zone) then
for idx, msn in pairs(conf.troopsOnBoard) do
-- each troopsOnboard is actually the
-- csar mission that I picked up
csarManager.successMission(myName, base.name, msn)
end
-- now use cargoSuper to retrieve all evacuees
-- and deliver them to safety
for idx, theMassObject in pairs(allEvacuees) do
cargoSuper.removeMassObjectFrom(
myName,
"Evacuees",
theMassObject)
msn = theMassObject.ref
-- csarManager.successMission(myName, base.name, msn)
-- to be done when we remove troopsOnBoard
end
-- reset weight
local totalMass = cargoSuper.calculateTotalMassFor(myName)
trigger.action.setUnitInternalCargo(myName, totalMass) -- super recalcs
-- trigger.action.outText("+++csar: delivered - set internal weight for " .. myName .. " to " .. totalMass, 30)
-- trigger.action.setUnitInternalCargo(myName, 10) -- 10 kg as empty
conf.troopsOnBoard = {} -- empty out troops on board
-- we do *not* return so we can pick up troops on
-- a CSARBASE if they were dropped there
end
end -- my side?
end
end -- check only if I'm carrying evacuees
-- if not in a csar dropoff zone, check if we are
-- landed in a csar pickup zone, and start loading
local pickups = {}
for idx, mission in pairs(csarManager.openMissions) do
if mySide == mission.side then
-- see if we are inside the mission's rescue range
local d = dcsCommon.distFlat(thePoint, mission.zone.point)
if d < csarManager.rescueRadius then
-- pick up this mission an remove it from the
table.insert(pickups, mission)
end
end
end
-- now process the missions that I've picked up, transfer them to troopsOnBoard, and remove the dudes
local didPickup = false
for idx, theMission in pairs(pickups) do
trigger.action.outTextForCoalition(mySide,
myName .. " is extracting " .. theMission.name .. "!",
30)
didPickup = true;
csarManager.removeMission(theMission)
table.insert(conf.troopsOnBoard, theMission)
theMission.group:destroy() -- will shut up radio as well
theMission.group = nil
-- now adapt for cargoSuper
theMassObject = cargoSuper.createMassObject(
csarManager.pilotWeight,
theMission.name,
theMission)
cargoSuper.addMassObjectTo(
myName,
"Evacuees",
theMassObject)
end
if didPickup then
trigger.action.outSoundForCoalition(mySide, "Quest Snare 3.wav")
end
-- reset unit's weight based on people on board
local totalMass = cargoSuper.calculateTotalMassFor(myName)
-- WAS: trigger.action.setUnitInternalCargo(myName, 10 + #conf.troopsOnBoard * csarManager.pilotWeight) -- 10 kg as empty + per-unit time people
trigger.action.setUnitInternalCargo(myName, totalMass) -- 10 kg as empty + per-unit time people
-- trigger.action.outText("+++csar: set internal weight for " .. myName .. " to " .. totalMass, 30)
end
--
--
-- Helo took off
--
--
function csarManager.heloDeparted(theUnit)
if not dcsCommon.isTroopCarrier(theUnit) then return end
-- if we have timed extractions (i.e. not instantaneous),
-- then we need to check if we take off after the timer runs out
-- when we take off, all that needs to be done is to change the state
-- to airborne, and then set the status flag
local conf = csarManager.getUnitConfig(theUnit)
conf.unit = theUnit
local theGroup = theUnit:getGroup()
conf.id = theGroup:getID()
conf.currentState = 1 -- in the air
end
--
--
-- Helo Crashed
--
--
function csarManager.heloCrashed(theUnit)
if not dcsCommon.isTroopCarrier(theUnit) then return end
-- problem: this isn't called on network games.
-- clean up
local conf = csarManager.getUnitConfig(theUnit)
conf.unit = theUnit
local theGroup = theUnit:getGroup()
conf.id = theGroup:getID()
conf.currentState = -1 -- (we don't know)
conf.troopsOnBoard = {}
local myName = conf.name
cargoSuper.removeAllMassForCargo(myName, "Evacuees") -- will allocate new empty table
csarManager.removeComms(conf.unit)
end
function csarManager.airframeCrashed(theUnit)
-- called from airframe manager
if not dcsCommon.isTroopCarrier(theUnit) then return end
local conf = csarManager.getUnitConfig(theUnit)
conf.unit = theUnit
local theGroup = theUnit:getGroup()
conf.id = theGroup:getID()
-- may want to do something, for now just nothing
end
function csarManager.airframeDitched(theUnit)
-- called from airframe manager
if not dcsCommon.isTroopCarrier(theUnit) then return end
local conf = csarManager.getUnitConfig(theUnit)
conf.unit = theUnit
local theGroup = theUnit:getGroup()
conf.id = theGroup:getID()
local theSide = theUnit:getCoalition()
if #conf.troopsOnBoard > 0 then
-- this is where we can create a new CSAR mission
trigger.action.outTextForCoalition(theSide, theUnit:getName() .. " abandoned while evacuating " .. #conf.troopsOnBoard .. " pilots. There many be survivors.", 30)
-- trigger.action.outSoundForCoalition(conf.id, "Quest Snare 3.wav")
for i=1, #conf.troopsOnBoard do
local msn = conf.troopsOnBoard[i] -- picked up unit(s)
local theRescuedPilot = msn.name
-- create x new missions in 50m radius
-- except for pilot, that will be called
-- from limitedAirframes
csarManager.createCSARforUnit(theUnit, theRescuedPilot, 50, true)
end
end
-- NYI: re-populate from cargo
local myName = conf.name
cargoSuper.removeAllMassForCargo(myName, "Evacuees") -- will allocate new empty table
end
--
--
-- M E N U H A N D L I N G & R E S P O N S E
--
--
function csarManager.clearCommsSubmenus(conf)
if conf.myCommands then
for i=1, #conf.myCommands do
missionCommands.removeItemForGroup(conf.id, conf.myCommands[i])
end
end
conf.myCommands = {}
end
function csarManager.removeCommsFromConfig(conf)
csarManager.clearCommsSubmenus(conf)
if conf.myMainMenu then
missionCommands.removeItemForGroup(conf.id, conf.myMainMenu)
conf.myMainMenu = nil
end
end
function csarManager.removeComms(theUnit)
if not theUnit then return end
if not theUnit:isExist() then return end
local group = theUnit:getGroup()
local id = group:getID()
local conf = csarManager.getUnitConfig(theUnit)
conf.id = id
conf.unit = theUnit
csarManager.removeCommsFromConfig(conf)
end
function csarManager.setCommsMenu(theUnit)
if not theUnit then return end
if not theUnit:isExist() then return end
-- we only add this menu to helicopter troop carriers
-- will also filter out all non-helicopters as nice side effect
if not dcsCommon.isTroopCarrier(theUnit) then return end
local group = theUnit:getGroup()
local id = group:getID()
local conf = csarManager.getUnitConfig(theUnit) -- will allocate if new. This is important since a group event can call this as well
conf.id = id; -- we do this ALWAYS to it is current even after a crash
conf.unit = theUnit -- link back
-- reset all coms now
csarManager.removeCommsFromConfig(conf)
-- ok, first, if we don't have an F-10 menu, create one
conf.myMainMenu = missionCommands.addSubMenuForGroup(id, 'CSAR Missions')
-- now we have a menu without submenus.
-- add our own submenus
local commandTxt = "List active CSAR requests"
local theCommand = missionCommands.addCommandForGroup(
conf.id,
commandTxt,
conf.myMainMenu,
csarManager.redirectListCSARRequests,
{conf, "hi there"}
)
table.insert(conf.myCommands, theCommand)
commandTxt = "Status of rescued crew aboard"
theCommand = missionCommands.addCommandForGroup(
conf.id,
commandTxt,
conf.myMainMenu,
csarManager.redirectStatusCarrying,
{conf, "hi there"}
)
table.insert(conf.myCommands, theCommand)
commandTxt = "Unload one evacuee here (rescue later)"
theCommand = missionCommands.addCommandForGroup(
conf.id,
commandTxt,
conf.myMainMenu,
csarManager.redirectUnloadOne,
{conf, "unload one"}
)
table.insert(conf.myCommands, theCommand)
end
function csarManager.redirectListCSARRequests(args)
timer.scheduleFunction(csarManager.doListCSARRequests, args, timer.getTime() + 0.1)
end
function csarManager.doListCSARRequests(args)
local conf = args[1]
local param = args[2]
local theUnit = conf.unit
local point = theUnit:getPoint()
--trigger.action.outText("+++csar: ".. theUnit:getName() .." issued csar status request", 30)
local report = "\nCrews requesting evacuation\n"
if #csarManager.openMissions < 1 then
report = report .. "\nNo requests, all crew are safe."
else
-- iterate through all troops onboard to get their status
for idx, mission in pairs(csarManager.openMissions) do
local d = dcsCommon.distFlat(point, mission.zone.point) * 0.000539957
d = math.floor(d * 10) / 10
local b = dcsCommon.bearingInDegreesFromAtoB(point, mission.zone.point)
local status = "alive"
if csarManager.vectoring then
report = report .. "\n".. mission.name .. ", bearing " .. b .. ", " ..d .."nm, " .. " ADF " .. mission.freq .. "0 kHz - " .. status
else
-- leave out vectoring
report = report .. "\n".. mission.name .. " ADF " .. mission.freq .. "0 kHz - " .. status
end
end
end
if #csarManager.csarBases < 1 then
report = report .. "\n\nWARNING: NO CSAR BASES TO DELIVER EVACUEES"
end
report = report .. "\n"
trigger.action.outTextForGroup(conf.id, report, 30)
trigger.action.outSoundForGroup(conf.id, "Quest Snare 3.wav")
end
function csarManager.redirectStatusCarrying(args)
timer.scheduleFunction(csarManager.doStatusCarrying, args, timer.getTime() + 0.1)
end
function csarManager.doStatusCarrying(args)
local conf = args[1]
local param = args[2]
local theUnit = conf.unit
--trigger.action.outText("+++csar: ".. theUnit:getName() .." wants to know how their rescued troops are doing", 30)
-- build status report
local report = "\nCrew Rescue Status:\n"
if #conf.troopsOnBoard < 1 then
report = report .. "\nWe have no evacuees on board"
else
-- iterate through all troops onboard to get their status
for i=1, #conf.troopsOnBoard do
local evacMission = conf.troopsOnBoard[i]
report = report .. "\n".. i .. ") " .. evacMission.name
report = report .. " is stable" -- or 'beat up, but will live'
end
report = report .. "\n\nTotal added weigth: " .. 10 + #conf.troopsOnBoard * csarManager.pilotWeight .. "kg"
end
report = report .. "\n"
trigger.action.outTextForGroup(conf.id, report, 30)
trigger.action.outSoundForGroup(conf.id, "Quest Snare 3.wav")
end
function csarManager.redirectUnloadOne(args)
timer.scheduleFunction(csarManager.unloadOne, args, timer.getTime() + 0.1)
end
function csarManager.unloadOne(args)
local conf = args[1]
local param = args[2]
local theUnit = conf.unit
local myName = theUnit:getName()
local report = "NYI: unload one"
if theUnit:inAir() then
report = "STRONGLY recommend we land first, Sir!"
trigger.action.outTextForGroup(conf.id, report, 30)
trigger.action.outSoundForGroup(conf.id, "Quest Snare 3.wav")
return
end
if #conf.troopsOnBoard < 1 then
report = "No evacuees on board."
trigger.action.outTextForGroup(conf.id, report, 30)
trigger.action.outSoundForGroup(conf.id, "Quest Snare 3.wav")
else
-- simulate a crash but for one unit
local theSide = theUnit:getCoalition()
-- this is where we can create a new CSAR mission
i= #conf.troopsOnBoard
local msn = conf.troopsOnBoard[i] -- picked up unit(s)
local theRescuedPilot = msn.name
-- create a new missions in 50m radius
csarManager.createCSARforUnit(theUnit, theRescuedPilot, 50, true)
conf.troopsOnBoard[i] = nil -- remove this mission
trigger.action.outTextForCoalition(theSide, myName .. " has aborted evacuating " .. msn.name .. ". New CSAR available.", 30)
trigger.action.outSoundForCoalition(theSide, "Quest Snare 3.wav")
-- recalc weight
trigger.action.setUnitInternalCargo(myName, 10 + #conf.troopsOnBoard * csarManager.pilotWeight) -- 10 kg as empty + per-unit time people
end
end
--
-- Player event callbacks
--
function csarManager.playerChangeEvent(evType, description, player, data)
if evType == "newGroup" then
local theUnit = data.primeUnit
if not dcsCommon.isTroopCarrier(theUnit) then return end
csarManager.setCommsMenu(theUnit) -- allocates new config
-- trigger.action.outText("+++csar: added " .. theUnit:getName() .. " to comms menu", 30)
return
end
if evType == "removeGroup" then
-- trigger.action.outText("+++csar: a group disappeared", 30)
local conf = csarManager.getConfigForUnitNamed(data.primeUnitName)
if conf then
csarManager.removeCommsFromConfig(conf)
end
return
end
if evType == "leave" then
local conf = csarManager.getConfigForUnitNamed(player.unitName)
if conf then
csarManager.resetConfig(conf)
end
end
if evType == "unit" then
-- player changed units. almost never in MP, but possible in solo
-- because of 1 seconds timing loop
-- will result in a new group appearing and a group disappearing, so we are good,
-- except we need to reset the conf so no troops are carried any longer
local conf = csarManager.getConfigForUnitNamed(data.oldUnitName)
if conf then
csarManager.resetConfig(conf)
end
end
end
--
-- CSAR Bases
--
-- properties:
-- - zone : the zone
-- - side : coalition
-- - name : name of base, can be overriden with property
function csarManager.addCSARBase(aZone)
local csarBase = {}
csarBase.zone = aZone
local bName = cfxZones.getStringFromZoneProperty(aZone, "CSARBASE", "XXX")
if bName == "XXX" then bName = aZone.name end
csarBase.name = cfxZones.getStringFromZoneProperty(aZone, "name", bName)
-- read further properties like facilities that may
-- need to be matched
csarBase.side = cfxZones.getCoalitionFromZoneProperty(aZone, "coalition", 0)
table.insert(csarManager.csarBases, csarBase)
if csarManager.verbose or aZone.verbose then
trigger.action.outText("+++csar: zone <" .. csarBase.name .. "> safe for side " .. csarBase.side, 30)
end
end
function csarManager.getCSARBaseforZone(aZone)
for idx, aCsarBase in pairs(csarManager.csarBases) do
if aCsarBase.zone == aZone then
return aCsarBase
end
end
return nil
end
--
--
-- U P D A T E
-- ===========
--
--
--
-- updateCSARMissions: make sure evacuees are still alive
--
function csarManager.updateCSARMissions()
local newMissions = {}
for idx, aMission in pairs (csarManager.openMissions) do
local stillAlive = dcsCommon.isGroupAlive(aMission.group)
-- now check if a timer was running to rescue this group
-- if dead, set stillAlive to false
if stillAlive then
table.insert(newMissions, aMission)
else
local msg = aMission.name .. " confirmed KIA, repeat KIA. Abort CSAR."
trigger.action.outTextForCoalition(aMission.side, msg, 30)
trigger.action.outSoundForCoalition(aMission.side, "Quest Snare 3.wav")
end
end
csarManager.openMissions = newMissions -- this is the new batch
end
function csarManager.update() -- every second
-- schedule next invocation
timer.scheduleFunction(csarManager.update, {}, timer.getTime() + 1/csarManager.ups)
-- first, check the health of all csar misions and update the table of live units
csarManager.updateCSARMissions()
-- now scan through all helo groups and see if they are close to a
-- CSAR zone and initiate the help sequence
local allPlayerGroups = cfxPlayerGroups -- cfxPlayerGroups is a global, don't fuck with it!
-- contains per group a player record, use prime unit to access player's unit
for gname, pgroup in pairs(allPlayerGroups) do
local aUnit = pgroup.primeUnit -- get prime unit of that group
if aUnit:isExist() and aUnit:inAir() then -- exists and is flying
local uPoint = aUnit:getPoint()
local uName = aUnit:getName()
local uGroup = aUnit:getGroup()
local uID = uGroup:getID()
local uSide = aUnit:getCoalition()
local agl = dcsCommon.getUnitAGL(aUnit)
if dcsCommon.isTroopCarrier(aUnit) then
-- scan through all available csar missions to see if we are close
-- enough to trigger comms
for idx, csarMission in pairs (csarManager.openMissions) do
-- check if we are inside trigger range on the same side
local d = dcsCommon.distFlat(uPoint, csarMission.zone.point)
if (uSide == csarMission.side) and (d < csarManager.rescueTriggerRange) then
-- we are in trigger distance. if we did not notify before
-- do it now
if not dcsCommon.arrayContainsString(csarMission.messagedUnits, uName) then
-- radio this unit with oclock and tell it they are in 2k range
-- also note if LZ is hot
local ownHeading = dcsCommon.getUnitHeadingDegrees(aUnit)
local oclock = dcsCommon.clockPositionOfARelativeToB(csarMission.zone.point, uPoint, ownHeading) .. " o'clock"
local msg = "\n" .. uName ..", " .. csarMission.name .. ". We can hear you, check your " .. oclock
if csarManager.useSmoke then msg = msg .. " - popping smoke" end
msg = msg .. "."
if csarMission.isHot then
msg = msg .. " Be advised: LZ is hot."
end
msg = msg .. "\n"
trigger.action.outTextForGroup(uID, msg, 30)
trigger.action.outSoundForGroup(uID, "Quest Snare 3.wav")
table.insert(csarMission.messagedUnits, uName) -- remember that we messaged them so we don't do again
end
-- also pop smoke if not popped already, or more than 3 minutes ago
if csarManager.useSmoke and timer.getTime() - csarMission.lastSmokeTime > 179 then
local smokePoint = dcsCommon.randomPointOnPerimeter(
50, csarMission.zone.point.x, csarMission.zone.point.z) --cfxZones.createHeightCorrectedPoint(csarMission.zone.point)
-- trigger.action.smoke(smokePoint, 4 )
dcsCommon.markPointWithSmoke(smokePoint, csarManager.smokeColor)
csarMission.lastSmokeTime = timer.getTime()
end
-- now check if we are inside hover range and alt
-- in order to simultate winch ops
-- WARNING: WE ALWAYS ONLY CHECK A SINGLE UNIT - the first alive
local evacuee = csarMission.group:getUnit(1)
if evacuee then
ep = evacuee:getPoint()
d = dcsCommon.distFlat(uPoint, ep)
d = math.floor(d * 10) / 10
if d < csarManager.hoverRadius * 2 then
local ownHeading = dcsCommon.getUnitHeadingDegrees(aUnit)
local oclock = dcsCommon.clockPositionOfARelativeToB(ep, uPoint, ownHeading) .. " o'clock"
-- log distance
local hoverMsg = "Closing on " .. csarMission.name .. ", " .. d * 3 .. "ft on your " .. oclock .. " o'clock"
if d < csarManager.hoverRadius then
if (agl <= csarManager.hoverAlt) and (agl > 3) then
local hoverTime = csarMission.hoveringUnits[uName]
if not hoverTime then
-- create new entry
hoverTime = timer.getTime()
csarMission.hoveringUnits[uName] = timer.getTime()
end
hoverTime = timer.getTime() - hoverTime -- calculate number of seconds
remainder = math.floor(csarManager.hoverDuration - hoverTime)
if remainder < 1 then remainder = 1 end
hoverMsg = "Steady... " .. d * 3 .. "ft to your " .. oclock .. " o'clock, winching... (" .. remainder .. ")"
if hoverTime > csarManager.hoverDuration then
-- we rescued the guy!
hoverMsg = "We have " .. csarMission.name .. " safely on board!"
local conf = csarManager.getUnitConfig(aUnit)
csarManager.removeMission(csarMission)
table.insert(conf.troopsOnBoard, csarMission)
csarMission.group:destroy() -- will shut up radio as well
csarMission.group = nil
trigger.action.outTextForGroup(uID, hoverMsg, 30, true)
trigger.action.outSoundForGroup(uID, "Quest Snare 3.wav")
return -- we only ever rescue one
end -- hovered long enough
trigger.action.outTextForGroup(uID, hoverMsg, 30, true)
return -- only ever one winch op
else -- too high for hover
hoverMsg = "Evacuee " .. d * 3 .. "ft on your " .. oclock .. " o'clock; land or descend to between 10 and 90 AGL for winching"
csarMission.hoveringUnits[uName] = nil -- reset timer
end
else -- not inside hover dist
-- remove the hover indicator for this
csarMission.hoveringUnits[uName] = nil
end
trigger.action.outTextForGroup(uID, hoverMsg, 30, true)
return -- only ever one winch op
else
-- remove the hover indicator for this unit
csarMission.hoveringUnits[uName] = nil
end -- inside 2 * hover dist?
end -- has evacuee
end -- if in range
end -- for all missions
end -- if troop carrier
end -- if exists
end -- for all players
-- now see and check if we need to spawn from a csar zone
-- that has been told to spawn
for idx, theZone in pairs(csarManager.csarZones) do
-- check if their flag value has changed
if theZone.startCSAR then
-- this should always be true, but you never know
local currVal = cfxZones.getFlagValue(theZone.startCSAR, theZone)
if currVal ~= theZone.lastCSARVal then
local theMission = csarManager.createCSARMissionData(
cfxZones.getPoint(theZone),
theZone.csarSide,
theZone.csarFreq,
theZone.csarName,
theZone.numCrew,
theZone.timeLimit,
theZone.csarMapMarker,
theZone.radius)
csarManager.addMission(theMission)
theZone.lastCSARVal = currVal
if csarManager.verbose then
trigger.action.outText("+++csar: started CSAR mission " .. theZone.csarName, 30)
end
end
end
end
end
--
-- create a CSAR Mission for a unit
--
function csarManager.createCSARforUnit(theUnit, pilotName, radius, silent)
if not silent then silent = false end
if not radius then radius = 1000 end
if not pilotName then pilotName = "Eddie" end
local point = theUnit:getPoint()
local coal = theUnit:getCoalition()
local csarPoint = dcsCommon.randomPointInCircle(radius, radius/2, point.x, point.z)
-- check the ground- water will kill the pilot
csarPoint.y = csarPoint.z
local surf = land.getSurfaceType(csarPoint)
if surf == 2 or surf == 3 then
if not silent then
trigger.action.outTextForCoalition(coal, "Bad chute! Bad chute! ".. pilotName .. " did not survive ejection out of their " .. theUnit:getTypeName(), 30)
trigger.action.outSoundForGroup(coal, "Quest Snare 3.wav")
end
return
end
csarPoint.y = land.getHeight(csarPoint)
-- when we get here, the terrain is ok, so let's drop the pilot
local theMission = csarManager.createCSARMissionData(
csarPoint,
coal,
nil,
pilotName,
1,
nil,
nil)
csarManager.addMission(theMission)
if not silent then
trigger.action.outTextForCoalition(coal, "MAYDAY MAYDAY MAYDAY! ".. pilotName .. " in " .. theUnit:getTypeName() .. " ejected, report good chute. Prepare CSAR!", 30)
trigger.action.outSoundForGroup(coal, "Quest Snare 3.wav")
end
end
--
-- csar (mission) zones
--
function csarManager.processCSARBASE()
local csarBases = cfxZones.zonesWithProperty("CSARBASE")
-- now add all zones to my zones table, and init additional info
-- from properties
for k, aZone in pairs(csarBases) do
csarManager.addCSARBase(aZone)
end
end
function csarManager.addCSARZone(theZone)
table.insert(csarManager.csarZones, theZone)
end
function csarManager.readCSARZone(theZone)
-- zones have attribute "CSAR"
-- gather data, and then create a mission from this
local theSide = cfxZones.getCoalitionFromZoneProperty(theZone, "coalition", 0)
theZone.csarSide = theSide
theZone.csarName = cfxZones.getStringFromZoneProperty(theZone, "name", "<none>")
if cfxZones.hasProperty(theZone, "csarName") then
theZone.csarName = cfxZones.getStringFromZoneProperty(theZone, "csarName", "<none>")
end
if cfxZones.hasProperty(theZone, "pilotName") then
theZone.csarName = cfxZones.getStringFromZoneProperty(theZone, "pilotName", "<none>")
end
if cfxZones.hasProperty(theZone, "victimName") then
theZone.csarName = cfxZones.getStringFromZoneProperty(theZone, "victimName", "<none>")
end
theZone.csarFreq = cfxZones.getNumberFromZoneProperty(theZone, "freq", 0)
if theZone.csarFreq == 0 then theZone.csarFreq = nil end
theZone.numCrew = 1
theZone.csarMapMarker = nil
theZone.timeLimit = cfxZones.getNumberFromZoneProperty(theZone, "timeLimit", 0)
if theZone.timeLimit == 0 then theZone.timeLimit = nil else theZone.timeLimit = timeLimit * 60 end
local deferred = cfxZones.getBoolFromZoneProperty(theZone, "deferred", false)
if cfxZones.hasProperty(theZone, "in?") then
theZone.startCSAR = cfxZones.getStringFromZoneProperty(theZone, "in?", "*none")
theZone.lastCSARVal = cfxZones.getFlagValue(theZone.startCSAR, theZone)
end
if cfxZones.hasProperty(theZone, "startCSAR?") then
theZone.startCSAR = cfxZones.getStringFromZoneProperty(theZone, "startCSAR?", "*none")
theZone.lastCSARVal = cfxZones.getFlagValue(theZone.startCSAR, theZone)
end
if (not deferred) then
local theMission = csarManager.createCSARMissionData(
theZone.point,
theZone.csarSide,
theZone.csarFreq,
theZone.csarName,
theZone.numCrew,
theZone.timeLimit,
theZone.csarMapMarker,
theZone.radius)
csarManager.addMission(theMission)
end
-- add to list of startable csar
if theZone.startCSAR then
csarManager.addCSARZone(theZone)
trigger.action.outText("csar: added <".. theZone.name .."> to deferred csar missions", 30)
end
if deferred and not theZone.startCSAR then
trigger.action.outText("+++csar: warning - CSAR Mission in Zone <" .. theZone.name .. "> can't be started", 30)
end
end
function csarManager.processCSARZones()
local csarBases = cfxZones.zonesWithProperty("CSAR")
-- now add all zones to my zones table, and init additional info
-- from properties
for k, aZone in pairs(csarBases) do
csarManager.readCSARZone(aZone)
end
end
--
-- Init & Start
--
function csarManager.invokeCallbacks(theCoalition, success, numRescued, notes)
-- invoke anyone who wants to know that a group
-- of people was rescued.
for idx, cb in pairs(csarManager.csarCompleteCB) do
cb(theCoalition, success, numRescued, notes)
end
end
function csarManager.installCallback(theCB)
table.insert(csarManager.csarCompleteCB, theCB)
end
function csarManager.readConfigZone()
local theZone = cfxZones.getZoneByName("csarManagerConfig")
if not theZone then
if csarManager.verbose then
trigger.action.outText("+++csar: NO config zone!", 30)
end
return
end
csarManager.configZone = theZone -- save for flag banging compatibility
csarManager.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false)
csarManager.ups = cfxZones.getNumberFromZoneProperty(theZone, "ups", 1)
csarManager.useSmoke = cfxZones.getBoolFromZoneProperty(theZone, "useSmoke", true)
csarManager.smokeColor = cfxZones.getSmokeColorStringFromZoneProperty(theZone, "smokeColor", "blue")
csarManager.smokeColor = dcsCommon.smokeColor2Num(csarManager.smokeColor)
if cfxZones.hasProperty(theZone, "csarRedDelivered!") then
csarManager.csarRedDelivered = cfxZones.getStringFromZoneProperty(theZone, "csarRedDelivered!", "*<none>")
end
if cfxZones.hasProperty(theZone, "csarBlueDelivered!") then
csarManager.csarBlueDelivered = cfxZones.getStringFromZoneProperty(theZone, "csarBlueDelivered!", "*<none>")
end
if cfxZones.hasProperty(theZone, "csarDelivered!") then
csarManager.csarDelivered = cfxZones.getStringFromZoneProperty(theZone, "csarDelivered!", "*<none>")
trigger.action.outText("+++csar: will bang csarDelivered: <" .. csarManager.csarDelivered .. ">", 30)
end
csarManager.rescueRadius = cfxZones.getNumberFromZoneProperty(theZone, "rescueRadius", 70) --70 -- must land within 50m to rescue
csarManager.hoverRadius = cfxZones.getNumberFromZoneProperty(theZone, "hoverRadius", 30) -- 30 -- must hover within 10m of unit
csarManager.hoverAlt = cfxZones.getNumberFromZoneProperty(theZone, "hoverAlt", 40) -- 40 -- must hover below this alt
csarManager.hoverDuration = cfxZones.getNumberFromZoneProperty(theZone, "hoverDuration", 20) -- 20 -- must hover for this duration
csarManager.rescueTriggerRange = cfxZones.getNumberFromZoneProperty(theZone, "rescueTriggerRange", 2000) -- 2000 -- when the unit pops smoke and radios
csarManager.beaconSound = cfxZones.getStringFromZoneProperty(theZone, "beaconSound", "Radio_beacon_of_distress_on_121.ogg") --"Radio_beacon_of_distress_on_121,5_MHz.ogg"
csarManager.pilotWeight = cfxZones.getNumberFromZoneProperty(theZone, "pilotWeight", 120) -- 120
csarManager.vectoring = cfxZones.getBoolFromZoneProperty(theZone, "vectoring", true)
if csarManager.verbose then
trigger.action.outText("+++csar: read config", 30)
end
end
function csarManager.start()
-- make sure we have loaded all relevant libraries
if not dcsCommon.libCheck("cfx CSAR", csarManager.requiredLibs) then
trigger.action.outText("cf/x CSAR aborted: missing libraries", 30)
return false
end
-- read config
csarManager.readConfigZone()
-- install callbacks for helo-relevant events
dcsCommon.addEventHandler(csarManager.somethingHappened, csarManager.preProcessor, csarManager.postProcessor)
-- now iterate through all player groups and install the CSAR Menu
local allPlayerGroups = cfxPlayerGroups -- cfxPlayerGroups is a global, don't fuck with it!
-- contains per group a player record, use prime unit to access player's unit
for gname, pgroup in pairs(allPlayerGroups) do
local aUnit = pgroup.primeUnit -- get prime unit of that group
csarManager.setCommsMenu(aUnit)
end
-- now install the new group notifier for new groups so we can remove and add CSAR menus
cfxPlayer.addMonitor(csarManager.playerChangeEvent)
-- now scan all zones that are CSAR drop-off for quick access
csarManager.processCSARBASE()
-- now scan all zones to create ME-placed CSAR missions
-- and populate the available mission.
csarManager.processCSARZones()
-- now call update so we can monitor progress of all helos, and alert them
-- when they are close to a CSAR
csarManager.update()
-- say hi!
trigger.action.outText("cf/x CSAR v" .. csarManager.version .. " started", 30)
return true
end
-- let's get rolling
if not csarManager.start() then
csarManager = nil
end
--[[--
improvements
- need to stay on ground for x seconds to load troops
- hot lz
- hover recover
- limit on troops aboard for transport
- delay for drop-off
- csar when: always, only on eject,
- repair o'clock
- nearest csarBase
- red/blue csarbases
- weight
- compatibility: side/owner - make sure it is compatible
with FARP, and landing on a FARP with opposition ownership
will not disembark
--]]--