DML/modules/csarManager2.lua
Christian Franz 7e4c147071 Version 2.2.2
Maintenance update
2024-04-18 11:31:07 +02:00

1663 lines
58 KiB
Lua

csarManager = {}
csarManager.version = "3.2.7"
csarManager.ups = 1
--[[-- VERSION HISTORY
- 2.3.0 - dmlZones
- onRoad attribute for CSAR mission Zones
- rndLoc support
- triggerMethod support
- 2.3.1 - addPrefix option
- delay asynch OK (message only)
- offset zone on randomized soldier
- smokeDist
- 2.3.2 - DCS 2.9 getCategory() fix
- 3.0.0 - moved mission creation out of update loop into own
- removed cfxPlayer dependency
- new event manager
- no longer single-proccing pilots
- can also handle aircraft - isTroopCarrier
- 3.1.0 - integration with scribe
- expanded internal API: newMissionCB, invoked at addMission
- expanded internal API: removedMissionCB
- added *rnd as option for csarName (requires "names")
- missions are sorted by distance
- mission timeLimit range implemented
- update handles time limit (pickup only)
- inflight status reflects time limit but will not time out
- pickupSound option
- lostSound option
- 3.1.1 - birth clears troopsOnBoard
- 3.2.0 - inPopulated csar option
- clearance csar attribute
- maxTries csar attribute
- 3.2.1 - comsRange attribute when mission times out
- rescueTypes option in config
3.2.2 - reset helicopter weight on birth
- cleanup
3.2.3 - hardening against *accidental* multi-unit player groups.
3.2.4 - pass theZone with missionCreateCB when created from zone
3.2.5 - smoke callbacks
- useRanks option
3.2.6 - inBuiltup analogon to cloner
3.2.7 - createCSARForParachutist now supports optional coa (autoCSAR)
INTEGRATES AUTOMATICALLY WITH playerScore
INTEGRATES WITH LIMITED AIRFRAMES
INTEGRATES AUTOMATICALLY WITH SCRIBE
--]]--
-- modules that need to be loaded BEFORE I run
csarManager.requiredLibs = {
"dcsCommon", -- common is of course needed for everything
"cfxZones", -- zones management for CSAR and CSAR Mission zones
"nameStats", -- generic data module for weight
"cargoSuper",
}
-- unitConfigs contain the config data for any helicopter
-- currently in the game. The Array is indexed by unit name
csarManager.unitConfigs = {}
--
-- CASR MISSION
--
csarManager.openMissions = {} -- all currently available missions
csarManager.csarBases = {} -- all bases where we can drop off rescued pilots. NOT a zone!
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 = {}
csarManager.csarCreatedCB = {}
csarManager.csarRemoveCB = {}
csarManager.csarPickupCB = {}
csarManager.csarSmokeCB = {}
--
-- CREATING A CSAR
--
function csarManager.createDownedPilot(theMission, existingUnit)
if not cfxCommander then
trigger.action.outText("+++CSAR: can't create mission, module cfxCommander is missing.", 30)
return
end
if not existingUnit then
local aLocation = {}
local aHeading = 0 -- in rads
local newTargetZone = theMission.zone
-- if mission.radius is < 1 we do not randomize location
-- else location is somewhere in the middle of zone
-- csar mission zones randomize by themselves, and pass
-- a radius of <1, this is only for ejecting pilots
if newTargetZone.radius > 1 then
aLocation, aHeading = dcsCommon.randomPointOnPerimeter(newTargetZone.radius / 2 + 3, newTargetZone.point.x, newTargetZone.point.z)
-- we now move the entire zone so it centers on unit
newTargetZone.point.x = aLocation.x
newTargetZone.point.z = aLocation.z
else
aLocation.x = newTargetZone.point.x
aLocation.z = newTargetZone.point.z
aHeading = math.random(360)/360 * 2 * 3.1415
end
theMission.locations = {}
-- get a typeName for the unit to create from my list of
-- types
local rType = dcsCommon.pickRandom(csarManager.rescueTypes)
local theBoyGroup = dcsCommon.createSingleUnitGroup(theMission.name,
rType, -- "Soldier M4 GRG",
aLocation.x,
aLocation.z,
-aHeading + 1.5) -- + 1.5 to turn inwards
local theSideCJTF = dcsCommon.getACountryForCoalition(theMission.side)
theMission.group = coalition.addGroup(theSideCJTF,
Group.Category.GROUND,
theBoyGroup)
if theBoyGroup then
table.insert(theMission.locations, aLocation)
else
trigger.action.outText("+++csar: FAILED to create csar!", 30)
end
else
theMission.group = existingUnit:getGroup()
local allUnits = theMission.group:getUnits()
for idx, aUnit in pairs(allUnits) do
local loc = aUnit:getPoint() -- warning: won't work if group newly allocated!
table.insert(theMission.locations, loc)
end
end
-- we now use commands to send radio transmissions
local ADF = 20 + math.random(150) -- create a random number between 20 and 110 --> 200'000 .. 1'700'000 KHz = 200KHz .. 1'700 KHz
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 10'000 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, parashootUnit) -- if parashootUnit is set, will not allocate new
-- create a type
-- if not timeLimit then timeLimit = -1 end
if not point then return nil end
local newMission = {}
newMission.side = theSide
-- if "names" module active, allow random names if name
-- equals "*rnd"
if names and names.uniqueFullName and name == "*rnd" then
name = names.uniqueFullName()
elseif name == "*rnd" then
trigger.action.outText("+++scar: '*rnd' name requires the 'name' module for randomization. Using 'John Smith'", 30)
name = "John Smith"
end
if dcsCommon.stringStartsWith(name, "downed ") then
-- remove "downed" - it will be added again later
name = dcsCommon.removePrefix(name, "downed ")
if csarManager.verbose then
trigger.action.outText("+++csar: 'downed' procced for <" .. name .. ">", 30)
end
end
if csarManager.useRanks then
-- local ranks = csarManager.ranks -- {"Lt", "Lt", "Lt", "Col", "Cpt", "WO", "WO"}
local myRank = dcsCommon.pickRandom(csarManager.ranks)
name = myRank .. " " .. name
end
if not inRadius then inRadius = csarManager.rescueRadius end
newMission.name = name .. " (ID#" .. csarManager.missionID .. ")" -- make it uuid-capable
if csarManager.addPrefix then
newMission.name = "downed " .. newMission.name
end
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, parashootUnit)
newMission.timeStamp = timer.getTime() -- now
-- set timeLimit if enabled
if timeLimit then
local theLimit = cfxZones.randomDelayFromPositiveRange(timeLimit[1], timeLimit[2]) * 60
newMission.expires = timer.getTime() + theLimit
end
-- update counter and return
csarManager.missionID = csarManager.missionID + 1
return newMission
end
function csarManager.addMission(theMission, theZone)
table.insert(csarManager.openMissions, theMission)
csarManager.invokeNewMissionCallbacks(theMission, theZone)
end
function csarManager.removeMission(theMission, pickup)
if not pickup then pickup = false end
-- invoked when evacuee is PICKED UP!
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
-- csarManager.invokeRemovedMissionCallbacks(theMission)
if pickup then
csarManager.invokePickUpCallbacks(theMission)
end
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
function csarManager.isCSARTarget(theGroup)
for idx, theMission in pairs(csarManager.openMissions) do
if theMission.group == theGroup then return true end
end
return false
end
--
-- UNIT CONFIG
--
function csarManager.resetConfig(conf)
-- reset only overwrites 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:onEvent(event)
-- make sure it has an initiator
if not event.initiator then return end
local theUnit = event.initiator
if not dcsCommon.isPlayerUnit(theUnit) then return end -- not a player unit
-- only proceed if troop carrier (no more helo checks, all troop carriers, so osprey and harrier can be used if so desired)
if not dcsCommon.isTroopCarrier(theUnit, csarManager.troopCarriers) then return end
local ID = event.id
if ID == 4 then -- landed
csarManager.heloLanded(theUnit)
end
if ID == 3 or ID == 55 then -- take off, postponed take-off
csarManager.heloDeparted(theUnit)
end
if ID == 5 then -- crash
csarManager.heloCrashed(theUnit)
-- note: maybe not called in network missions.
-- correction: is called in 2.9
end
if ID == 15 then -- player helicopter birth
-- we need to set up comms for this unit
csarManager.setCommsMenu(theUnit)
-- we also need to make sure that there are no
-- more troopsOnBoard
local myName = theUnit:getName()
local conf = csarManager.getUnitConfig(theUnit)
conf.unit = theUnit
conf.troopsOnBoard = {}
local totalMass = cargoSuper.calculateTotalMassFor(myName)
-- now also set cargo weight for the unit
cargoSuper.removeAllMassForCargo(myName, "Evacuees") -- will allocate new empty table
totalMass = cargoSuper.calculateTotalMassFor(myName)
trigger.action.setUnitInternalCargo(myName, totalMass) -- super recalcs
end
end
--
-- CSAR LANDED
--
function csarManager.successMission(who, where, theMission)
-- who is
-- where is
-- theMission is mission table
-- playerScore integration
if cfxPlayerScore then
local theScore = theMission.score
if not theScore then theScore = csarManager.rescueScore end
local theUnit = Unit.getByName(who)
if theUnit and theUnit.getPlayerName then
local pName = theUnit:getPlayerName()
if pName then
cfxPlayerScore.updateScoreForPlayer(pName, theScore)
cfxPlayerScore.logFeatForPlayer(pName, "Evacuated " .. theMission.name)
end
end
end
-- scribe.integration
if scribe then
local theUnit = Unit.getByName(who)
if theUnit and theUnit.getPlayerName then
local pName = theUnit:getPlayerName()
scribe.playerRescueComplete(pName)
end
end
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, theMission)
csarManager.invokeCallbacks(theMission.side, true, 1, "success", theMission)
trigger.action.outSoundForCoalition(theMission.side, csarManager.successSound)
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)
if csarManager.verbose then
trigger.action.outText("+++csar: banging csarDelivered: <" .. csarManager.csarDelivered .. ">", 30)
end
end
end
function csarManager.heloLanded(theUnit)
-- when we have landed,
if not dcsCommon.isTroopCarrier(theUnit, csarManager.troopCarriers) then return end
local conf = csarManager.getUnitConfig(theUnit)
conf.unit = theUnit
local theGroup = theUnit:getGroup()
conf.id = theGroup: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 csarManager.verbose then
trigger.action.outText("+++csar: helo <" .. myName .. "> landed with <" .. #allEvacuees .. "> evacuees on board.",30)
end
if #allEvacuees > 0 then -- wasif #conf.troopsOnBoard > 0 then
if csarManager.verbose then
trigger.action.outText("+++csar: checking bases:", 30)
end
for idx, base in pairs(csarManager.csarBases) do
if csarManager.verbose then
trigger.action.outText("+++csar: base <" .. base.zone.name .. ">", 30)
end
-- 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
if csarManager.verbose or base.zone.verbose then
trigger.action.outText("+++csar: <" .. myName .. "> touch down in CSAR drop-off zone <" .. base.zone.name .. ">", 30)
end
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
end
-- reset weight
local totalMass = cargoSuper.calculateTotalMassFor(myName)
trigger.action.setUnitInternalCargo(myName, totalMass) -- super recalcs
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
else
if csarManager.verbose or base.zone.verbose then
trigger.action.outText("+++csar: touchdown of <" .. myName .. "> occured outside of csar zone <" .. base.zone.name .. ">", 30)
end
end
else -- not on my side
if csarManager.verbose or base.zone.verbose then
trigger.action.outText("+++csar: base <" .. base.zone.name .. "> is on side <" .. currentBaseSide .. ">, which is not on my side <" .. mySide .. ">.", 30)
end
end -- my side?
end -- for all bases
if csarManager.verbose then
trigger.action.outText("+++csar: complete bases check", 30)
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 and 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;
local args = {}
args.theName = theMission.name
args.mySide = mySide
args.unitName = myName
timer.scheduleFunction(csarManager.asynchSuccess, args, timer.getTime() + 3)
csarManager.removeMission(theMission, true) -- picked up
table.insert(conf.troopsOnBoard, theMission)
theMission.group:destroy() -- will shut up radio as well
theMission.group = nil
-- now adapt for cargoSuper
local theMassObject = cargoSuper.createMassObject(
csarManager.pilotWeight,
theMission.name,
theMission)
cargoSuper.addMassObjectTo(
myName,
"Evacuees",
theMassObject)
end
if didPickup then
local args = {}
args.mySide = mySide
timer.scheduleFunction(csarManager.asynchSound, args, timer.getTime() + 3)
end
-- reset unit's weight based on people on board
local totalMass = cargoSuper.calculateTotalMassFor(myName)
trigger.action.setUnitInternalCargo(myName, totalMass) -- 10 kg as empty + per-unit time people
end
function csarManager.asynchSuccess(args)
-- currently, we always say "OK", will check for fail later
trigger.action.outTextForCoalition(args.mySide, args.unitName .. " has loaded " .. args.theName .. "!", 30)
end
function csarManager.asynchSound(args)
trigger.action.outSoundForCoalition(args.mySide, csarManager.pickupSound)
end
--
--
-- Helo took off
--
--
function csarManager.heloDeparted(theUnit)
if not dcsCommon.isTroopCarrier(theUnit, csarManager.troopCarriers) 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, csarManager.troopCarriers) 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, csarManager.troopCarriers) 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, csarManager.troopCarriers) 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, csarManager.troopCarriers) 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)
commandTxt = "Direction to nearest safe zone"
theCommand = missionCommands.addCommandForGroup(
conf.id,
commandTxt,
conf.myMainMenu,
csarManager.redirectDirections,
{conf, "redirect"}
)
table.insert(conf.myCommands, theCommand)
end
function csarManager.redirectListCSARRequests(args)
timer.scheduleFunction(csarManager.doListCSARRequests, args, timer.getTime() + 0.1)
end
function csarManager.openMissionsForSide(theSide)
local theMissions = {}
for idx, aMission in pairs(csarManager.openMissions) do
if aMission.side == theSide or aMission.side == 0 then
table.insert(theMissions, aMission)
end
end
return theMissions
end
function csarManager.doListCSARRequests(args)
local now = timer.getTime()
local conf = args[1]
local param = args[2]
local theUnit = conf.unit
if not theUnit then return end -- ??
if not Unit.isExist(theUnit) then return end
local point = theUnit:getPoint()
local theSide = theUnit:getCoalition()
local report = "\nCrews requesting evacuation\n"
local openMissions = csarManager.openMissionsForSide(theSide)
if #openMissions < 1 then
report = report .. "\nNo requests, all crew are safe."
else
-- iterate through all missions and calc distance
for idx, mission in pairs(openMissions) do
local d = dcsCommon.distFlat(point, mission.zone.point) * 0.000539957
d = math.floor(d * 10) / 10
mission.dist = d
end
-- sort openMissions by dist
table.sort(openMissions,
function (e1, e2) return e1.dist < e2.dist end
)
-- we may want to limit to n nearest missions
local maxM = #openMissions
if maxM > csarManager.maxMissions then maxM = csarManager.maxMissions end
for i=1,maxM do --in pairs(openMissions) do
local mission = openMissions[i]
local b = dcsCommon.bearingInDegreesFromAtoB(point, mission.zone.point)
local status = "alive"
if mission.expires then
delta = math.floor ((mission.expires - now) / 60)
if delta < 10 then status = "+deteriorating+" end
if delta < 5 then status = "*critical*" end
if csarManager.verbose then
status = status .. "[" .. delta .. "]" -- remove me
end
end
if csarManager.vectoring then
report = report .. "\n".. mission.name .. ", bearing " .. b .. ", " ..mission.dist .."nm, " .. " ADF " .. mission.freq * 10 .. " kHz - " .. status
else
-- leave out vectoring
report = report .. "\n".. mission.name .. " ADF " .. mission.freq * 10 .. " kHz - " .. status
end
end
end
local myBases = csarManager.getCSARBasesForSide(theSide)
if #myBases < 1 then
report = report .. "\n\nWARNING: NO CSAR BASES TO DELIVER EVACUEES TO"
end
report = report .. "\n"
trigger.action.outTextForGroup(conf.id, report, 30)
trigger.action.outSoundForGroup(conf.id, csarManager.actionSound)
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
if not theUnit then return end -- ??
if not Unit.isExist(theUnit) then return end
local now = timer.getTime()
-- 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
if evacMission.expires then
delta = math.floor ((evacMission.expires - now) / 60)
if delta > 20 then
report = report .. " is hurt but stable"
elseif delta > 10 then
report = report .. " is badly hurt"
else
report = report .. " is in critical condition" -- or 'beat up, but will live'
end
else
report = report .. " is stable" -- or 'beat up, but will live'
end
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, csarManager.actionSound)
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
if not theUnit then return end -- ??
if not Unit.isExist(theUnit) then return end
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, csarManager.actionSound) -- "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, csarManager.actionSound) -- "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
--TODO: remove weight for this pilot!
trigger.action.outTextForCoalition(theSide, myName .. " has aborted evacuating " .. msn.name .. ". New CSAR available.", 30)
trigger.action.outSoundForCoalition(theSide, csarManager.actionSound) -- "Quest Snare 3.wav")
-- recalc weight
local totalMass = 10 + #conf.troopsOnBoard * csarManager.pilotWeight
trigger.action.setUnitInternalCargo(myName, totalMass) -- 10 kg as empty + per-unit time people
--trigger.action.outText("unit <" .. myName .. ">, internal cargo now <" .. totalMass .. ">kg", 30)
end
end
function csarManager.redirectDirections(args)
timer.scheduleFunction(csarManager.directions, args, timer.getTime() + 0.1)
end
function csarManager.directions(args)
local conf = args[1]
local param = args[2]
local theUnit = conf.unit
if not theUnit then return end -- ??
if not Unit.isExist(theUnit) then return end
local myName = theUnit:getName()
local theSide = theUnit:getCoalition()
local report = "Nothing to report."
-- get all safe zones
local myBases = csarManager.getCSARBasesForSide(theSide)
if #myBases < 1 then
report = "\n\nWARNING: NO CSAR BASES TO DELIVER EVACUEES TO"
else
-- find nearest zone
local p = theUnit:getPoint()
p.y = 0
local dMin = math.huge
local theBase = nil
local dP = nil
for idx, aBase in pairs(myBases) do
local z = aBase.zone
local zp = cfxZones.getPoint(z)
zp.y = 0
local d = dcsCommon.dist(p, zp)
if d < dMin then
theBase = aBase
dP = zp
dMin = d
end
end
-- see if we are inside
if cfxZones.isPointInsideZone(p, theBase.zone) then
report = "\nYou are inside safe zone " .. theBase.name .. "."
else
-- get bearing and distance
dMin = dMin / 1000 * 0.539957 -- in nm
dMin = math.floor(dMin * 10) / 10
local bearing = dcsCommon.bearingInDegreesFromAtoB(p, dP)
report = "\nClosest safe zone for " .. myName .. " is " .. theBase.name ..", bearing " .. bearing .. " at " .. dMin .. "nm"
end
end
report = report .. "\n"
trigger.action.outTextForGroup(conf.id, report, 30)
trigger.action.outSoundForGroup(conf.id, csarManager.actionSound)
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
-- CSARBASE carries the coalition in the CSARBASE attribute
csarBase.side = aZone:getCoalitionFromZoneProperty("CSARBASE", 0)
-- backward-compatibility to older versions.
-- will be deprecated
if aZone:hasProperty("coalition") then
csarBase.side = aZone:getCoalitionFromZoneProperty("CSARBASE", 0)
end
-- see if we have provided a name field, default zone name
csarBase.name = aZone:getStringFromZoneProperty("name", aZone.name)
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
function csarManager.getCSARBasesForSide(theSide)
local bases = {}
for idx, aBase in pairs(csarManager.csarBases) do
if aBase.side == 0 or aBase.side == theSide then
table.insert(bases, aBase)
end
end
return bases
end
--
-- U P D A T E
-- ===========
--
--
-- updateCSARMissions: make sure evacuees are still alive
--
function csarManager.updateCSARMissions()
local newMissions = {}
local now = timer.getTime()
for idx, aMission in pairs (csarManager.openMissions) do
-- see if mission timed out
local now = timer.getTime()
local stillRunning = true
if aMission.expires then stillRunning = aMission.expires > now end
local stillAlive = dcsCommon.isGroupAlive(aMission.group)
-- if dead, set stillAlive to false
if stillRunning and stillAlive then
table.insert(newMissions, aMission)
elseif stillAlive then
-- expired.
local p = aMission.zone:getPoint() -- all missions have a zone
p.y = 0
local thePlayers = coalition.getPlayers(aMission.side)
local msg = aMission.name .. " is no longer responding. Abort rescue."
for idx, theUnit in pairs (thePlayers) do
local up = theUnit:getPoint()
up.y = 0
local dist = math.floor (dcsCommon.dist(up, p))
if dist < csarManager.comsRange then
local ID = theUnit:getID()
trigger.action.outTextForUnit(ID, msg, 30)
trigger.action.outSoundForUnit(ID, csarManager.lostSound)
end
end
csarManager.invokeCallbacks(aMission.side, false, 1, "lost", aMission)
if aMission.group and Group.isExist(aMission.group) then
Group.destroy(aMission.group)
end
else
local msg = aMission.name .. " confirmed KIA, repeat KIA. Abort CSAR."
trigger.action.outTextForCoalition(aMission.side, msg, 30)
trigger.action.outSoundForCoalition(aMission.side, csarManager.actionSound)
csarManager.invokeCallbacks(aMission.side, false, 1, "KIA", aMission)
end
end
csarManager.openMissions = newMissions -- this is the new batch
end
function csarManager.launchFlare(args)
local color = args.color
if color < 0 then color = math.random(4) - 1 end
local loc = args.loc -- with height
if csarManager.verbose then
trigger.action.outText("+++csarM: launching flare, c = " .. color .. " (" .. dcsCommon.flareColor2Text(color) .. ")", 30)
end
trigger.action.outTextForGroup(args.uID, "Launching flare!", 30)
loc.y = loc.y + 3 -- launch 3 meters above ground
trigger.action.signalFlare(loc, color, 0)
end
-- WE ASSUME MISSIONS AREN'T TOO CLOSE TOGETHER TO
-- MESS UP MESSAGING OR PICKUP
-- if they are less than 2d apart, they can crosstalk each other
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 allPlayerUnits = dcsCommon.getAllExistingPlayersAndUnits() -- indexed by player name
for pname, aUnit in pairs(allPlayerUnits) do
if aUnit:inAir() and
dcsCommon.isTroopCarrier(aUnit, csarManager.troopCarriers)
then -- troop carrier 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)
local needsGC = false
-- local hasMessaged = false
for idx, csarMission in pairs (csarManager.openMissions) do
-- check if we are inside trigger range on the same side
local mp = cfxZones.getPoint(csarMission.zone, true)
local d = dcsCommon.distFlat(uPoint, mp)
if ((uSide == csarMission.side) or (csarMission.side == 0) )
and (d < csarManager.rescueTriggerRange) then
-- we are in trigger distance. if we did not notify before
-- do it now, we ever only do this once for a unit for any mission
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
if csarManager.useFlare then
if csarManager.useSmoke then
msg = msg .. " and will launch flare in a few seconds"
else
msg = msg .. " - preparing flare"
end
-- schedule flare launch in 5-10 seconds
local args = {}
args.loc = mp
args.color = csarManager.flareColor
args.uID = uID
timer.scheduleFunction(csarManager.launchFlare, args, timer.getTime() + math.random(5))
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, csarManager.actionSound) -- "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 5 minutes ago
if csarManager.useSmoke and (timer.getTime() - csarMission.lastSmokeTime) >= 5 * 60 then
if csarMission.lastSmokeTime < 0 then
-- this is the first time that this mission pops smoke
-- trigger.action.outText("***will invoke smoke cb", 30)
csarManager.invokeSmokeCallbacks(csarMission, uName)
else
--trigger.action.outText("nope smoke's a dope", 30)
end
local smokePoint = dcsCommon.randomPointOnPerimeter(
csarManager.smokeDist, csarMission.zone.point.x, csarMission.zone.point.z)
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
-- if competition picked up, we skip this loop
local evacuee = nil
if csarMission.group then evacuee = csarMission.group:getUnit(1) end
if evacuee then
local ep = evacuee:getPoint()
d = dcsCommon.distFlat(uPoint, ep)
d = math.floor(d * 10) / 10
if d < csarManager.rescueTriggerRange * 0.5 then
local ownHeading = dcsCommon.getUnitHeadingDegrees(aUnit)
local oclock = dcsCommon.clockPositionOfARelativeToB(ep, uPoint, ownHeading) .. " o'clock"
-- log distance
local hoverMsg = "Closing on " .. csarMission.name .. ", " .. d * 1 .. "m 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
local remainder = math.floor(csarManager.hoverDuration - hoverTime)
if remainder < 1 then remainder = 1 end
hoverMsg = "Steady... " .. d * 1 .. "m 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)
-- mission now GC's after iteration csarManager.removeMission(csarMission)
table.insert(conf.troopsOnBoard, csarMission)
csarMission.group:destroy() -- will shut up radio as well
csarMission.group = nil -- no more evacuees
needsGC = true -- need filtering missions
-- now handle weight using cargoSuper
local theMassObject = cargoSuper.createMassObject(
csarManager.pilotWeight,
csarMission.name,
csarMission)
cargoSuper.addMassObjectTo(
uName,
"Evacuees",
theMassObject)
local totalMass = cargoSuper.calculateTotalMassFor(uName)
trigger.action.setUnitInternalCargo(uName, totalMass)
if csarManager.verbose then
local allEvacuees = cargoSuper.getManifestFor(myName, "Evacuees") -- returns unlinked array
trigger.action.outText("+++csar: <" .. uName .. "> now has <" .. #allEvacuees .. "> groups of evacuees on board, totalling " .. totalMass .. "kg", 30)
end
trigger.action.outSoundForGroup(uID, csarManager.pickupSound)
--return -- we only ever rescue one
end -- hovered long enough
-- return -- only ever one winch op
else -- too high for hover
hoverMsg = "Evacuee " .. d * 1 .. "m 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?
else
-- somebody snatched the evacuee
end -- if has evacuee
end -- if in range
end -- for all missions
-- now GC all missions if we lifted a pilot up (we no longer return after first succesful)
if needsGC then
local filtered = {}
for idx, csarMission in pairs(csarManager.openMissions) do
if csarMission.group then
table.insert(filtered, csarMission)
end
end
csarManager.openMissions = filtered
end
end -- if in Air
end -- for all player units
-- 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 = theZone:getFlagValue(theZone.startCSAR)
-- if currVal ~= theZone.lastCSARVal then
if theZone:testZoneFlag(theZone.startCSAR, theZone.triggerMethod, "lastCSARVal") then
local theMission = csarManager.createCSARMissionFromZone(theZone)
csarManager.addMission(theMission, theZone)
--theZone.lastCSARVal = currVal
if csarManager.verbose or theZone.verbose then
trigger.action.outText("+++csar: started CSAR mission for <" .. theZone.csarName .. ">", 30)
end
end
end
end
end
function csarManager.createCSARMissionFromZone(theZone)
-- set up random point in zone
local mPoint = theZone:getPoint()
if theZone.rndLoc then mPoint = theZone:createRandomPointInZone() end
if theZone.onRoad then
mPoint.x, mPoint.z = land.getClosestPointOnRoads('roads',mPoint.x, mPoint.z)
elseif theZone.inPopulated then
local aPoint = theZone:createRandomPointInPopulatedZone(theZone.clearance) -- no more maxTries: theZone.maxTries)
mPoint = aPoint -- safety in case we need to mod aPoint
end
local theMission = csarManager.createCSARMissionData(
mPoint,
theZone.csarSide, -- theSide
theZone.csarFreq, -- freq
theZone.csarName, -- name
theZone.numCrew, -- numCrew
theZone.timeLimit, -- timeLimit
theZone.csarMapMarker, -- mapMarker
0.1, --theZone.radius) -- radius
nil) -- parashoo unit
theMission.inPopulated = theZone.inPopulated -- transfer for csarFX
return theMission
end
--
-- create a CSAR Mission for a unit
--
function csarManager.createCSARforUnit(theUnit, pilotName, radius, silent, score) -- invoked with aircraft as theUnit, usually still in air
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)
csarPoint.y = csarPoint.z
local surf = land.getSurfaceType(csarPoint)
csarPoint.y = land.getHeight(csarPoint)
-- when we get here, the terrain is ok, so let's drop the pilot
local theMission = csarManager.createCSARMissionData(
csarPoint, -- point
coal, -- side
nil, -- freq
pilotName, -- name
1, -- num crew
nil, -- time limit
nil) -- map mark, inRadius, parashooUnit
theMission.score = score
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, csarManager.actionSound) -- "Quest Snare 3.wav")
end
end
function csarManager.createCSARForParachutist(theUnit, name, coa) -- invoked with parachute guy on ground as theUnit
if not coa then coa = theUnit:getCoalition() end
local pos = theUnit:getPoint()
-- unit DOES NOT HAVE GROUP!!! (unless water splashdown)
-- create a CSAR mission now
local theMission = csarManager.createCSARMissionData(pos, coa, nil, name, nil, nil, nil, 0.1, nil)
csarManager.addMission(theMission)
trigger.action.outTextForCoalition(coa, "MAYDAY MAYDAY MAYDAY! ".. name .. " requesting extraction after eject!", 30)
trigger.action.outSoundForGroup(coa, csarManager.actionSound)
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 mName = theZone:getStringFromZoneProperty("CSAR", theZone.name)
-- if mName == "" then mName = theZone.name end
local theSide = theZone:getCoalitionFromZoneProperty("coalition", 0)
theZone.csarSide = theSide
theZone.csarName = mName -- now deprecating name attributes
if theZone:hasProperty("name") then
theZone.csarName = theZone:getStringFromZoneProperty("name", "<none>")
elseif theZone:hasProperty("csarName") then
theZone.csarName = theZone:getStringFromZoneProperty("csarName", "<none>")
elseif theZone:hasProperty("pilotName") then
theZone.csarName = theZone:getStringFromZoneProperty("pilotName", "<none>")
elseif theZone:hasProperty("victimName") then
theZone.csarName = theZone:getStringFromZoneProperty("victimName", "<none>")
end
theZone.csarFreq = theZone:getNumberFromZoneProperty("freq", 0)
-- since freqs are set in 10kHz multiplier by DML
-- we have to divide the feq given here by 10
theZone.csarFreq = theZone.csarFreq / 10
if theZone.csarFreq < 0.01 then theZone.csarFreq = nil end
theZone.numCrew = 1
theZone.csarMapMarker = nil
if theZone:hasProperty("timeLimit") then
local tmin, tmax = theZone:getPositiveRangeFromZoneProperty("timeLimit", 1)
theZone.timeLimit = {tmin, tmax}
else
theZone.timeLimit = nil
end
local deferred = theZone:getBoolFromZoneProperty("deferred", false)
if theZone:hasProperty("in?") then
theZone.startCSAR = theZone:getStringFromZoneProperty("in?", "*none")
theZone.lastCSARVal = theZone:getFlagValue(theZone.startCSAR)
elseif theZone:hasProperty("start?") then
theZone.startCSAR = theZone:getStringFromZoneProperty("start?", "*none")
theZone.lastCSARVal = theZone:getFlagValue(theZone.startCSAR)
elseif theZone:hasProperty("startCSAR?") then
theZone.startCSAR = theZone:getStringFromZoneProperty("startCSAR?", "*none")
theZone.lastCSARVal = theZone:getFlagValue(theZone.startCSAR)
end
if theZone:hasProperty("score") then
theZone.score = theZone:getNumberFromZoneProperty("score", 100)
end
theZone.triggerMethod = theZone:getStringFromZoneProperty("triggerMethod", "change")
theZone.rndLoc = theZone:getBoolFromZoneProperty("rndLoc", true)
theZone.onRoad = theZone:getBoolFromZoneProperty("onRoad", false)
if theZone:hasProperty("onRoads") then
theZone.onRoad = theZone:getBoolFromZoneProperty("onRoads", false)
end
-- emulate inBuiltup from clone Zone
if theZone:hasProperty("inPopulated") or theZone:hasProperty("clearance") or theZone:hasProperty("inBuiltup")then
if theZone:hasProperty("inPopulated") then
theZone.inPopulated = theZone:getBoolFromZoneProperty("inPopulated", false)
else
theZone.inPopulated = true -- presence of clearance forces it to true
end
theZone.clearance = theZone:getNumberFromZoneProperty("clearance", 10)
if theZone:hasProperty("inBuiltUp") then
theZone.clearance = theZone:getNumberFromZoneProperty("inBuiltup", 10)
end
end
-- maxTries is decommed
-- theZone.maxTries = theZone:getNumberFromZoneProperty("maxTries", 20)
if theZone.onRoad and theZone.inPopulated then
trigger.action.outText("warning: competing 'onRoad' and 'inPopulated' attributes in zone <" .. theZone.name .. ">. Using 'onRoad'.", 30)
theZone.inPopulated = false
end
-- add to list of startable csar
if theZone.startCSAR then
csarManager.addCSARZone(theZone)
end
if (not deferred) then
local theMission = csarManager.createCSARMissionFromZone(theZone)
csarManager.addMission(theMission, theZone)
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
--
-- mission complete cs(coalition, success, numberRescued, notes, data)
function csarManager.invokeCallbacks(theCoalition, success, numRescued, notes, theMission)
-- invoke anyone who wants to know that a group
-- of people was rescued.
for idx, cb in pairs(csarManager.csarCompleteCB) do
-- notes =
-- "KIA" when evacuee is killed (success = false)
-- "success" when mission done (success = true)
-- "lost" when evacuee timed out (success = false)
cb(theCoalition, success, numRescued, notes, theMission)
end
end
-- mission created cb(theMission)
function csarManager.invokeNewMissionCallbacks(theMission, theZone)
-- invoke anyone who wants to know that a new mission was created
for idx, cb in pairs(csarManager.csarCreatedCB) do
cb(theMission, theZone)
end
end
-- mission: picking up the evacuee
function csarManager.invokePickUpCallbacks(theMission)
-- invoke anyone who wants to know that a new mission was created
for idx, cb in pairs(csarManager.csarPickupCB) do
cb(theMission)
end
end
function csarManager.invokeSmokeCallbacks(theMission, uName)
for idx, cb in pairs(csarManager.csarSmokeCB) do
cb(theMission, uName)
end
end
function csarManager.installCallback(theCB)
table.insert(csarManager.csarCompleteCB, theCB)
end
function csarManager.installNewMissionCallback(theCB)
table.insert(csarManager.csarCreatedCB, theCB)
end
function csarManager.installPickupCallback(theCB)
table.insert(csarManager.csarPickupCB, theCB)
end
function csarManager.installSmokeCallback(theCB)
table.insert(csarManager.csarSmokeCB, theCB)
end
function csarManager.readConfigZone()
csarManager.name = "csarManagerConfig" -- compat with cfxZones
local theZone = cfxZones.getZoneByName("csarManagerConfig")
if not theZone then
theZone = cfxZones.createSimpleZone("csarManagerConfig")
end
csarManager.configZone = theZone -- save for flag banging compatibility
csarManager.verbose = theZone.verbose
csarManager.ups = theZone:getNumberFromZoneProperty("ups", 1)
csarManager.useSmoke = theZone:getBoolFromZoneProperty("useSmoke", true)
csarManager.smokeColor = theZone:getSmokeColorStringFromZoneProperty("smokeColor", "blue")
csarManager.smokeDist = theZone:getNumberFromZoneProperty("smokeDist", 30)
csarManager.smokeColor = dcsCommon.smokeColor2Num(csarManager.smokeColor)
csarManager.useFlare = theZone:getBoolFromZoneProperty("useFlare", true)
csarManager.flareColor = theZone:getFlareColorStringFromZoneProperty("flareColor", "red")
csarManager.flareColor = dcsCommon.flareColor2Num(csarManager.flareColor)
if theZone:hasProperty("csarRedDelivered!") then
csarManager.csarRedDelivered = theZone:getStringFromZoneProperty("csarRedDelivered!", "*<none>")
end
if theZone:hasProperty("csarBlueDelivered!") then
csarManager.csarBlueDelivered = theZone:getStringFromZoneProperty("csarBlueDelivered!", "*<none>")
end
if theZone:hasProperty("csarDelivered!") then
csarManager.csarDelivered = theZone:getStringFromZoneProperty("csarDelivered!", "*<none>")
end
csarManager.rescueRadius = theZone:getNumberFromZoneProperty( "rescueRadius", 70)
csarManager.hoverRadius = theZone:getNumberFromZoneProperty( "hoverRadius", 30)
csarManager.hoverAlt = theZone:getNumberFromZoneProperty("hoverAlt", 40)
csarManager.hoverDuration = theZone:getNumberFromZoneProperty( "hoverDuration", 20)
csarManager.rescueTriggerRange = theZone:getNumberFromZoneProperty("rescueTriggerRange", 2000)
csarManager.beaconSound = theZone:getStringFromZoneProperty( "beaconSound", "Radio_beacon_of_distress_on_121,5_MHz.ogg")
csarManager.pilotWeight = theZone:getNumberFromZoneProperty("pilotWeight", 120)
csarManager.rescueScore = theZone:getNumberFromZoneProperty( "rescueScore", 100)
csarManager.actionSound = theZone:getStringFromZoneProperty( "actionSound", "Quest Snare 3.wav")
csarManager.successSound = theZone:getStringFromZoneProperty("successSound", csarManager.actionSound)
csarManager.pickupSound = theZone:getStringFromZoneProperty("pickupSound", csarManager.actionSound)
csarManager.vectoring = theZone:getBoolFromZoneProperty("vectoring", true)
csarManager.lostSound = theZone:getStringFromZoneProperty("lostSound", csarManager.actionSound)
csarManager.comsRange = theZone:getNumberFromZoneProperty("comsRange", 40000)
-- add own troop carriers
if theZone:hasProperty("troopCarriers") then
local tc = theZone:getStringFromZoneProperty("troopCarriers", "UH-1D")
tc = dcsCommon.splitString(tc, ",")
csarManager.troopCarriers = dcsCommon.trimArray(tc)
if csarManager.verbose then
trigger.action.outText("+++casr: redefined troop carriers to types:", 30)
for idx, aType in pairs(csarManager.troopCarriers) do
trigger.action.outText(aType, 30)
end
end
end
csarManager.addPrefix = theZone:getBoolFromZoneProperty("addPrefix", true)
csarManager.useRanks = theZone:getBoolFromZoneProperty("useRanks", false)
local lRanks= theZone:getStringFromZoneProperty("ranks", "Lt, Lt, Lt, Col, Cpt, WO, WO")
local typeArray = dcsCommon.splitString(lRanks, ",")
typeArray = dcsCommon.trimArray(typeArray)
csarManager.ranks = typeArray
csarManager.maxMissions = theZone:getNumberFromZoneProperty("maxMissions", 15)
-- add types to use for the rescuee
local hTypes = theZone:getStringFromZoneProperty("rescueTypes", "Soldier M4 GRG")
local typeArray = dcsCommon.splitString(hTypes, ",")
typeArray = dcsCommon.trimArray(typeArray)
csarManager.rescueTypes = typeArray
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()
-- 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()
world.addEventHandler(csarManager)
-- now iterate through all player groups and install the CSAR Menu
local allPlayerUnits = dcsCommon.getAllExistingPlayerUnitsRaw()
for pName, aUnit in pairs(allPlayerUnits) do
csarManager.setCommsMenu(aUnit)
end
-- start updating and track all helicopters in the air against missions
csarManager.update()
-- say hi!
trigger.action.outText("cf/x CSAR Manager v" .. csarManager.version .. " started", 30)
return true
end
-- let's get rolling
if not csarManager.start() then
trigger.action.outText("cf/x CSAR Manager v" .. csarManager.version .. " FAILED to run", 30)
csarManager = nil
end
--[[--
improvements
- need to stay on ground for x seconds to load troops
- hot lz
- limit on troops aboard for transport
- delay for drop-off
- repair o'clock
- compatibility: side/owner - make sure it is compatible
with FARP, and landing on a FARP with opposition ownership
will not disembark
- when unloading one by menu, update weight!!!
-- allow any airfied to be csarsafe by default, no longer *requires* csarbase
-- minFreq, maxFreq settings for config and mission-individual
-- may want to change if time limit was exceeded on return to tell
player that they did not survive the transport
--]]--