DML/modules/heloTroops.lua
Christian Franz 59d8bb30cf Version 2.4.5
csarFX, dropFormation in heloTroops
2025-03-13 09:49:23 +01:00

1313 lines
45 KiB
Lua

cfxHeloTroops = {}
cfxHeloTroops.version = "5.0.0"
cfxHeloTroops.verbose = false
cfxHeloTroops.autoDrop = true
cfxHeloTroops.autoPickup = false
cfxHeloTroops.pickupRange = 100 -- meters
cfxHeloTroops.requestRange = 500 -- meters
--
--[[--
VERSION HISTORY
4.0.0 - added dropZones
- enforceDropZones
- coalition for drop zones
4.1.0 - troops dropped in dropZones with active autodespawn are
filtered from load menu
- updated eventhandler to new events and unitLost
- timeStamp to avoid double-dipping
- auto-pickup restricted as well
- code cleanup
4.2.0 - support for individual lase codes
- support for drivable
4.2.1 - increased verbosity
- also supports 'pickupRang" for reverse-compatibility with manual typo.
4.2.2 - support for attachTo:
4.2.3 - dropZone supports 'keepWait' attribute
- dropZone supports 'setWait' attribute
4.2.4 - dropWait is always active outside of drop zones
5.0.0 - drop options and formation menus. Supported formations
circle_out
chevron
line left
line right
scattered behind
--]]--
cfxHeloTroops.minTime = 3 -- seconds beween tandings
cfxHeloTroops.requiredLibs = {
"dcsCommon", -- common is of course needed for everything
"cfxZones", -- Zones, of course
"cfxCommander", -- to make troops do stuff
"cfxGroundTroops", -- generic when dropping troops
}
cfxHeloTroops.unitConfigs = {} -- all configs are stored by unit's name
cfxHeloTroops.troopWeight = 100 -- kg average weight per trooper
cfxHeloTroops.dropZones = {} -- dict
-- persistence support
cfxHeloTroops.deployedTroops = {}
--
-- drop formation helpers
--
function cfxHeloTroops.formation2text(inFormation)
if inFormation == "circle_out" then return "Circle Around" end
if inFormation == "chevron" then return "Chevron in Front" end
if inFormation == "lineLeft" then return "Line to Port" end
if inFormation == "lineRight" then return "Line to Starboard" end
if inFormation == "gaggle" then return "Gaggle Behind" end
return "ErrFormation"
end
function cfxHeloTroops.formation2dml(inFormation) -- returns formation, delta, phi
if inFormation == "circle_out" then return "circle_out", 0, 0 end
if inFormation == "chevron" then return "chevron", 0, 0 end
if inFormation == "lineLeft" then return "line_v", 12, 4.71239 end -- 4.71239 is 270 degrees
if inFormation == "lineRight" then return "line_v", 12, 1.57079633 end -- 1.57079633 is 90 degrees
if inFormation == "gaggle" then return "scattered", 24, 3.14159265 end -- 3.14159265 is pi is 180 degrees
trigger.action.outText("+++heloT: unknown drop formation <>, using circle_out, 0 ,0", 30)
return "circle_out", 0, 0
end
--
-- drop zones
--
function cfxHeloTroops.processDropZone(theZone)
theZone.droppedFlag = theZone:getStringFromZoneProperty("dropZone!", "cfxNone")
theZone.dropMethod = theZone:getStringFromZoneProperty("dropMethod", "inc")
theZone.dropCoa = theZone:getCoalitionFromZoneProperty("coalition", 0)
theZone.autoDespawn = theZone:getNumberFromZoneProperty("autoDespawn", -1)
theZone.keepWait = theZone:getBoolFromZoneProperty("keepWait", false)
theZone.setWait = theZone:getBoolFromZoneProperty("setWait", false)
end
--
-- comms
--
function cfxHeloTroops.resetConfig(conf)
conf.autoDrop = cfxHeloTroops.autoDrop --if true, will drop troops on-board upon touchdown
conf.autoPickup = cfxHeloTroops.autoPickup -- if true will load nearest troops upon touchdown
conf.pickupRange = cfxHeloTroops.pickupRange --meters, maybe make per helo?
conf.currentState = -1 -- 0 = landed, 1 = airborne, -1 undetermined
conf.troopsOnBoardNum = 0 -- if not 0, we have troops and can spawnm/drop
conf.troopCapacity = 8 -- should be depending on airframe
-- troopsOnBoard.name contains name of group
-- the other fields info for troops picked up
conf.troopsOnBoard = {} -- table with the following
conf.troopsOnBoard.name = "***reset***"
conf.dropFormation = "circle_out" -- used to derive formation, delta, phi in formation2dml, use formation2text for text representation
conf.timeStamp = timer.getTime() -- to avoid double-dipping
end
function cfxHeloTroops.createDefaultConfig(theUnit)
local conf = {}
cfxHeloTroops.resetConfig(conf)
conf.myMainMenu = nil -- this is where the main menu for group will be stored
conf.myCommands = nil -- this is where we put all commands in. Why?
return conf
end
function cfxHeloTroops.getUnitConfig(theUnit) -- will create new config if not existing
if not theUnit then
trigger.action.outText("+++WARNING: nil unit in get config!", 30)
return nil
end
local c = cfxHeloTroops.unitConfigs[theUnit:getName()]
if not c then
c = cfxHeloTroops.createDefaultConfig(theUnit)
cfxHeloTroops.unitConfigs[theUnit:getName()] = c
end
return c
end
function cfxHeloTroops.getConfigForUnitNamed(aName)
return cfxHeloTroops.unitConfigs[aName]
end
--
-- LANDED
--
function cfxHeloTroops.loadClosestGroup(conf)
local p = conf.unit:getPosition().p
local cat = Group.Category.GROUND
local unitsToLoad = dcsCommon.getLivingGroupsAndDistInRangeToPoint(p, conf.pickupRange, conf.unit:getCoalition(), cat)
-- groups may contain units that are not for transport.
-- for now we only load troops with legal type strings
unitsToLoad = cfxHeloTroops.filterTroopsByType(unitsToLoad)
-- filter all groups that are inside a dropZone with a
-- positive autoDespawn attribute
local mySide = conf.unit:getCoalition()
unitsToLoad = cfxHeloTroops.filterTroopsFromDropZones(unitsToLoad, mySide)
-- now limit the options to the five closest legal groups
local numUnits = #unitsToLoad
if numUnits < 1 then return false end -- on false will drop through
local aTeam = unitsToLoad[1] -- get first (closest) entry
local dist = aTeam.dist
local group = aTeam.group
cfxHeloTroops.doLoadGroup({conf, group})
return true -- will have loaded and reset menu
end
function cfxHeloTroops.heloLanded(theUnit)
-- when we have landed,
if not dcsCommon.isTroopCarrier(theUnit, cfxHeloTroops.troopCarriers) then return end
local conf = cfxHeloTroops.getUnitConfig(theUnit)
-- prevent double-dipping on land and depart
local now = timer.getTime()
local diff = now - conf.timeStamp
if diff < cfxHeloTroops.minTime then
if cfxHeloTroops.verbose then
trigger.action.outText("+++heloT-heloLanded: filtered for time restraint <" .. diff .. ">", 30)
end
return
end
if cfxHeloTroops.verbose then
trigger.action.outText("+++heloT-heloLanded: resetting timeStamp for delta <" .. diff .. ">", 30)
end
conf.timeStamp = now
conf.unit = theUnit
conf.currentState = 0
-- auto-unload
if conf.autoDrop then
if conf.troopsOnBoardNum > 0 then
cfxHeloTroops.doDeployTroops({conf, "autodrop"})
-- doDeployTroops() invokes set menu and empties troopsOnBoard
return
end
-- no troops to drop on board
end
if conf.autoPickup then
if conf.troopsOnBoardNum < 1 then
-- load the closest group
if cfxHeloTroops.loadClosestGroup(conf) then
return
end
end
end
-- reset menu
cfxHeloTroops.removeComms(conf.unit)
cfxHeloTroops.setCommsMenu(conf.unit)
end
--
-- Helo took off
--
function cfxHeloTroops.heloDeparted(theUnit)
if not dcsCommon.isTroopCarrier(theUnit, cfxHeloTroops.troopCarriers) then return end
-- change the state to airborne, and update menus
local conf = cfxHeloTroops.getUnitConfig(theUnit)
-- prevent double-dipping on land and depart
local now = timer.getTime()
local diff = now - conf.timeStamp
if cfxHeloTroops.verbose then
trigger.action.outText("+++heloT-heloDeparted: resetting timeStamp for delta <" .. diff .. ">", 30)
end
conf.timeStamp = now
conf.currentState = 1 -- in the air
cfxHeloTroops.removeComms(conf.unit)
cfxHeloTroops.setCommsMenu(conf.unit)
end
--
-- Helo Crashed
--
function cfxHeloTroops.cleanHelo(theUnit)
-- clean up
local conf = cfxHeloTroops.getUnitConfig(theUnit)
conf.unit = theUnit
conf.troopsOnBoardNum = 0 -- all dead
conf.currentState = -1 -- (we don't know)
-- check if we need to interface with groupTracker
if conf.troopsOnBoard.name and groupTracker then
local theName = conf.troopsOnBoard.name
-- there was (possibly) a group on board. see if it was tracked
local isTracking, numTracking, trackers = groupTracker.groupNameTrackedBy(theName)
-- if so, remove it from limbo
if isTracking then
for idx, theTracker in pairs(trackers) do
groupTracker.removeGroupNamedFromTracker(theName, theTracker)
if cfxHeloTroops.verbose then
trigger.action.outText("+++Helo: removed group <" .. theName .. "> from tracker <" .. theTracker.name .. ">", 30)
end
end
end
end
conf.troopsOnBoard = {}
end
function cfxHeloTroops.heloCrashed(theUnit)
if not dcsCommon.isTroopCarrier(theUnit, cfxHeloTroops.troopCarriers) then return
end
-- clean up
cfxHeloTroops.cleanHelo(theUnit)
end
--
-- M E N U H A N D L I N G & R E S P O N S E
--
function cfxHeloTroops.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 cfxHeloTroops.removeCommsFromConfig(conf)
cfxHeloTroops.clearCommsSubmenus(conf)
if conf.myMainMenu then
missionCommands.removeItemForGroup(conf.id, conf.myMainMenu)
conf.myMainMenu = nil
end
end
function cfxHeloTroops.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 = cfxHeloTroops.getUnitConfig(theUnit)
conf.id = id
conf.unit = theUnit
cfxHeloTroops.removeCommsFromConfig(conf)
end
function cfxHeloTroops.addConfigMenu(conf)
-- we add a menu for auto-drop-off and drop formation
-- trigger.action.outText("enter addConfigMenu for <" .. conf.unit:getName() .. ">", 30)
if conf.myDeployMenu then
missionCommands.removeItemForGroup(conf.id, conf.myDeployMenu)
end
conf.myDeployMenu = missionCommands.addSubMenuForGroup(conf.id, 'Deployment Options', conf.myMainMenu)
local onOff = "OFF"
if conf.autoDrop then onOff = "ON" end
local theCommand = missionCommands.addCommandForGroup(
conf.id,
'Auto-Drop: ' .. onOff .. ' - Select to change',
conf.myDeployMenu,
cfxHeloTroops.redirectToggleConfig,
{conf, "drop"}
)
table.insert(conf.myCommands, theCommand)
onOff = "OFF"
if conf.autoPickup then onOff = "ON" end
theCommand = missionCommands.addCommandForGroup(
conf.id,
'Auto-Pickup: ' .. onOff .. ' - Select to change',
conf.myDeployMenu,
cfxHeloTroops.redirectToggleConfig,
{conf, "pickup"}
)
table.insert(conf.myCommands, theCommand)
if conf.myFormationMenu then
missionCommands.removeItemForGroup(conf.id, conf.myFormationMenu)
end
conf.myFormationMenu = missionCommands.addSubMenuForGroup(conf.id, "Set Formation (" .. cfxHeloTroops.formation2text(conf.dropFormation) .. ")", conf.myDeployMenu)
theCommand = missionCommands.addCommandForGroup(
conf.id,
'Circle Around',
conf.myFormationMenu,
cfxHeloTroops.redirectDropFormation,
{conf, "circle_out"}
)
theCommand = missionCommands.addCommandForGroup(
conf.id,
'Chevron in Front',
conf.myFormationMenu,
cfxHeloTroops.redirectDropFormation,
{conf, "chevron"}
)
theCommand = missionCommands.addCommandForGroup(
conf.id,
'Line to Port',
conf.myFormationMenu,
cfxHeloTroops.redirectDropFormation,
{conf, "lineLeft"}
)
theCommand = missionCommands.addCommandForGroup(
conf.id,
'Line to Starboard',
conf.myFormationMenu,
cfxHeloTroops.redirectDropFormation,
{conf, "lineRight"}
)
theCommand = missionCommands.addCommandForGroup(
conf.id,
'Gaggle Behind',
conf.myFormationMenu,
cfxHeloTroops.redirectDropFormation,
{conf, "gaggle"}
)
end
function cfxHeloTroops.setCommsMenu(theUnit)
-- compatible with DCS 2.9.6 dynamic spawns
if cfxHeloTroops.verbose then
trigger.action.outText("+++heloT: setComms for player unit <" .. theUnit:getName() .. ">: ENTER.", 30)
end
if not theUnit then return end
if not theUnit:isExist() then return end
-- we only add this menu to troop carriers
if not dcsCommon.isTroopCarrier(theUnit, cfxHeloTroops.troopCarriers) then
if cfxHeloTroops.verbose then
trigger.action.outText("+++heloT - player unit <" .. theUnit:getName() .. "> type <" .. theUnit:getTypeName() .. "> is not legal troop carrier.", 30)
end
return
end
local group = theUnit:getGroup()
local id = group:getID()
local conf = cfxHeloTroops.getUnitConfig(theUnit)
-- set time stamp to avoid double-dipping later
--conf.timeStamp = timer.getTime() -- to avoid double-dipping
conf.id = id; -- we ALWAYS do this so it is current even after a crash
conf.unit = theUnit -- link back
-- if we don't have an F-10 menu, create one
if not (conf.myMainMenu) then
local mainMenu = nil
if cfxHeloTroops.mainMenu then
mainMenu = radioMenu.getMainMenuFor(cfxHeloTroops.mainMenu)
end
conf.myMainMenu = missionCommands.addSubMenuForGroup(id, 'Airlift Troops', mainMenu)
end
-- clear out existing commands, add new
cfxHeloTroops.clearCommsSubmenus(conf)
cfxHeloTroops.addConfigMenu(conf)
-- now see if we are on the ground or in the air
-- or unknown
if conf.currentState < 0 then
conf.currentState = 0 -- landed
if theUnit:inAir() then
conf.currentState = 1
end
end
if conf.currentState == 0 then
cfxHeloTroops.addGroundMenu(conf)
else
cfxHeloTroops.addAirborneMenu(conf)
end
end
function cfxHeloTroops.addAirborneMenu(conf)
-- while airborne, add a status menu
local commandTxt = "(To load troops, land in proximity to them)"
if conf.troopsOnBoardNum > 0 then
commandTxt = "(You are carrying " .. conf.troopsOnBoardNum .. " Infantry. Land to deploy them)"
end
local theCommand = missionCommands.addCommandForGroup(
conf.id,
commandTxt,
conf.myMainMenu,
cfxHeloTroops.redirectNoAction,
{conf, "none"}
)
table.insert(conf.myCommands, theCommand)
end
function cfxHeloTroops.redirectNoAction(args)
-- we do not redirect since there is nothing to do
end
function cfxHeloTroops.addGroundMenu(conf)
-- Player can deploy troops when loaded
-- or load troops when they are in proximity
if cfxHeloTroops.verbose then
trigger.action.outText("+++heloT: ENTER addGroundMenu for unit <" .. conf.unit:getName() .. "> with <" .. conf.troopsOnBoardNum .. "> troops on board", 30)
end
-- case 1: troops aboard
if conf.troopsOnBoardNum > 0 then
if cfxHeloTroops.verbose then
trigger.action.outText("+++heloT: unit <" .. conf.unit:getName() .. "> has <" .. conf.troopsOnBoardNum .. "> troops on board", 30)
end
local theCommand = missionCommands.addCommandForGroup(
conf.id,
"Deploy Team <" .. conf.troopsOnBoard.name .. ">",
conf.myMainMenu,
cfxHeloTroops.redirectDeployTroops,
{conf, "deploy"}
)
table.insert(conf.myCommands, theCommand)
return -- no loading
end
-- case 2A: no troops aboard. requestable spawners/cloners in range?
local p = conf.unit:getPosition().p
local mySide = conf.unit:getCoalition()
-- collect available spawn zones
local availableSpawners = {}
if cfxSpawnZones then -- only if SpawnZones is implemented
local availableSpawnersRaw = cfxSpawnZones.getRequestableSpawnersInRange(p, cfxHeloTroops.requestRange, mySide)
for idx, aSpawner in pairs(availableSpawnersRaw) do
-- filter all spawners that spawn "illegal" troops
local theTypes = aSpawner.types
local typeArray = dcsCommon.splitString(theTypes, ',')
typeArray = dcsCommon.trimArray(typeArray)
local allLegal = true
-- check agianst default (dcsCommon) or own definition (if exists)
for idy, aType in pairs(typeArray) do
if cfxHeloTroops.legalTroops then
if not dcsCommon.arrayContainsString(cfxHeloTroops.legalTroops, aType) then
allLegal = false
-- trigger.action.outText("spawner <" .. aSpawner.name .. ">: troop type <" .. aType .. "> is illegal", 30)
end
else
if not dcsCommon.typeIsInfantry(aType) then
allLegal = false
-- trigger.action.outText("spawner <" .. aSpawner.name .. ">: troop type <" .. aType .. "> is not infantry", 30)
end
end
end
if allLegal then
table.insert(availableSpawners, aSpawner)
end
end
end
-- collect available clone zones
if cloneZones then
local availableSpawnersRaw = cloneZones.getRequestableClonersInRange(p, cfxHeloTroops.requestRange, mySide)
for idx, aSpawner in pairs(availableSpawnersRaw) do
-- filter all spawners that spawn "illegal" troops or have none
local theTypes = aSpawner.allTypes
local allLegal = true
local numTypes = dcsCommon.getSizeOfTable(theTypes)
if numTypes > 0 then
for aType, cnt in pairs(theTypes) do
if cfxHeloTroops.legalTroops then
if not dcsCommon.arrayContainsString(cfxHeloTroops.legalTroops, aType) then
allLegal = false
end
else
if not dcsCommon.typeIsInfantry(aType) then
allLegal = false
end
end
end
else
allegal = false
end
if allLegal then
table.insert(availableSpawners, aSpawner)
end
end
end
local numSpawners = #availableSpawners
if numSpawners > 5 then numSpawners = 5 end
while numSpawners > 0 do
-- for each spawner in range, create a menu item
local spawner = availableSpawners[numSpawners]
local theName = spawner.baseName
local comm = "Request <" .. theName .. "> troops for transport"
local theCommand = missionCommands.addCommandForGroup(
conf.id,
comm,
conf.myMainMenu,
cfxHeloTroops.redirectSpawnGroup,
{conf, spawner}
)
table.insert(conf.myCommands, theCommand)
numSpawners = numSpawners - 1
end
-- Collect troops in range that we can load up
local cat = Group.Category.GROUND
local unitsToLoad = dcsCommon.getLivingGroupsAndDistInRangeToPoint(p, conf.pickupRange, conf.unit:getCoalition(), cat)
-- the groups may contain units that are not for transport.
-- later we can filter this by weight, or other cool stuff
-- for now we simply only troopy with legal type strings
-- TODO: add weight filtering
unitsToLoad = cfxHeloTroops.filterTroopsByType(unitsToLoad)
-- filter all groups that are inside a dropZone with a
-- positive autoDespawn attribute
unitsToLoad = cfxHeloTroops.filterTroopsFromDropZones(unitsToLoad, mySide)
-- now limit the options to the five closest legal groups
local numUnits = #unitsToLoad
if numUnits > 5 then numUnits = 5 end
if numUnits < 1 then
local theCommand = missionCommands.addCommandForGroup(
conf.id,
"(No units in range)",
conf.myMainMenu,
cfxHeloTroops.redirectNoAction,
{conf, "none"}
)
table.insert(conf.myCommands, theCommand)
return
end
-- add an entry for each group in units to load
for i=1, numUnits do
local aTeam = unitsToLoad[i]
local dist = aTeam.dist
local group = aTeam.group
local tNum = group:getSize()
local comm = "Load <" .. group:getName() .. "> " .. tNum .. " Members"
local theCommand = missionCommands.addCommandForGroup(
conf.id,
comm,
conf.myMainMenu,
cfxHeloTroops.redirectLoadGroup,
{conf, group}
)
table.insert(conf.myCommands, theCommand)
end
end
function cfxHeloTroops.filterTroopsByType(unitsToLoad)
if cfxHeloTroops.verbose then trigger.action.outText("+++heloT: enter filterTroops", 30) end
local filteredGroups = {}
for idx, aTeam in pairs(unitsToLoad) do
local group = aTeam.group
if cfxHeloTroops.verbose then trigger.action.outText("+++heloT: testing group <" .. group:getName() .. ">", 30) end
local theTypes = dcsCommon.getGroupTypeString(group)
local aT = dcsCommon.splitString(theTypes, ",")
local pass = true
for iT, sT in pairs(aT) do
-- check if this is a valid type
if cfxHeloTroops.legalTroops then
if not dcsCommon.arrayContainsString(cfxHeloTroops.legalTroops, sT) then
pass = false
if cfxHeloTroops.verbose then
local gName = group:getName()
trigger.action.outText("+++heloT[legal]: type <" .. sT .. "> not in legalTroops, group <" .. gName .. "> discarded.", 30)
end
break
end
else
if not dcsCommon.typeIsInfantry(sT) then
pass = false
if cfxHeloTroops.verbose then
local gName = group:getName()
trigger.action.outText("+++heloT[common]: type <" .. sT .. "> not in dcsC.infantry, group <" .. gName .. "> discarded.", 30)
end
break
end
end
end
-- check if we are about to pre-empt a CSAR mission
if csarManager then
if csarManager.isCSARTarget(group) then
-- this one is managed by csarManager,
-- don't load it for helo troops
pass = false
if cfxHeloTroops.verbose then
local gName = group:getName()
trigger.action.outText("+++heloT[csar]: group <" .. gName .. "> filtered: is CSAR target.", 30)
end
end
end
if pass then
table.insert(filteredGroups, aTeam)
if cfxHeloTroops.verbose then
local gName = group:getName()
trigger.action.outText("+++heloT[menu]: group <" .. gName .. "> added to available troops.", 30)
end
end
end
if cfxHeloTroops.verbose then
trigger.action.outText("+++heloT[menu]: returning with <" .. #filteredGroups .. "> available groups", 30)
end
return filteredGroups
end
function cfxHeloTroops.filterTroopsFromDropZones(allTroops, mySide)
-- quick-out: no dropZones
if dcsCommon.getSizeOfTable(cfxHeloTroops.dropZones) < 1 then return allTroops end
local filtered = {}
for idx, theTeam in pairs(allTroops) do
-- theTeam is a table {group, dist}
local theGroup = theTeam.group
local firstUnit = theGroup:getUnit(1)
local include = true
if firstUnit and Unit.isExist(firstUnit) then
local p = firstUnit:getPoint()
for idy, theZone in pairs(cfxHeloTroops.dropZones) do
if theZone.autoDespawn > 0 and
(theZone:getCoalition() == 0 or theZone:getCoalition() == mySide)
then
-- see if the unit is inside this zone
if theZone:isPointInsideZone(p) then
include = false -- filter out
if theZone.verbose then
trigger.action.outText("+++helo: filtered group <" .. theGroup:getName() .. "> from 'load' menu. Reason: autoDespawn active in deploy zone <" .. theZone.name .. ">", 30)
end
end
end
end
end
if include then table.insert(filtered, theTeam) end
end
return filtered
end
--
-- T O G G L E S
--
function cfxHeloTroops.redirectToggleConfig(args)
timer.scheduleFunction(cfxHeloTroops.doToggleConfig, args, timer.getTime() + 0.1)
end
function cfxHeloTroops.doToggleConfig(args)
local conf = args[1]
local what = args[2]
if what == "drop" then
conf.autoDrop = not conf.autoDrop
if conf.autoDrop then
trigger.action.outTextForGroup(conf.id, "Now deploying troops immediately after landing", 30)
else
trigger.action.outTextForGroup(conf.id, "Troops will now only deploy when told to", 30)
end
else
conf.autoPickup = not conf.autoPickup
if conf.autoPickup then
trigger.action.outTextForGroup(conf.id, "Nearest troops will now automatically board after landing", 30)
else
trigger.action.outTextForGroup(conf.id, "Troops will now board only after being ordered to do so", 30)
end
end
cfxHeloTroops.setCommsMenu(conf.unit)
end
--
-- set formation
--
function cfxHeloTroops.redirectDropFormation(args)
timer.scheduleFunction(cfxHeloTroops.doDropFormation, args, timer.getTime() + 0.1)
end
function cfxHeloTroops.doDropFormation(args)
local conf = args[1]
local newFormation = args[2]
conf.dropFormation = newFormation
if cfxHeloTroops.verbose then
trigger.action.outText("Switching <" .. conf.unit:getName() .. ">'s troop deploy formation to <" .. newFormation .. ">", 40)
end
cfxHeloTroops.setCommsMenu(conf.unit)
end
--
-- Deploying Troops
--
function cfxHeloTroops.redirectDeployTroops(args)
timer.scheduleFunction(cfxHeloTroops.doDeployTroops, args, timer.getTime() + 0.1)
end
function cfxHeloTroops.scoreWhenCapturing(theUnit)
if theUnit and Unit.isExist(theUnit) and theUnit.getPlayerName then
-- see if we are inside a non-alinged zone (incl. neutral)
local coa = theUnit:getCoalition()
local p = theUnit:getPoint()
local theGroup = theUnit:getGroup()
local ID = theGroup:getID()
local nearestZone, dist = cfxOwnedZones.getNearestOwnedZoneToPoint(p)
if nearestZone and nearestZone:pointInZone(p) then
-- we are inside an owned zone!
if nearestZone.owner ~= coa then
-- yup, combat drop!
local theScore = cfxHeloTroops.combatDropScore
local pName = theUnit:getPlayerName()
if pName then
cfxPlayerScore.updateScoreForPlayer(pName, theScore)
cfxPlayerScore.logFeatForPlayer(pName, "Combat Troop Insertion at " .. nearestZone.name, coa)
end
end
end
end
end
function cfxHeloTroops.isInsideDropZone(theUnit)
local p = theUnit:getPoint()
for idx, theZone in pairs (cfxHeloTroops.dropZones) do
if theZone:isPointInsideZone(p) then return true end
end
return false
end
function cfxHeloTroops.doDeployTroops(args)
local conf = args[1]
local what = args[2]
local theUnit = conf.unit
local theGroup = theUnit:getGroup()
local gid = theGroup:getID()
local inside = cfxHeloTroops.isInsideDropZone(theUnit)
if (not inside) and cfxHeloTroops.enforceDropZones then
trigger.action.outTextForGroup(gid, "You are outside an disembark/drop zone.", 30)
return
end
-- deploy the troops I have on board
cfxHeloTroops.deployTroopsFromHelicopter(conf)
-- interface with playerscore if we dropped
-- inside an enemy-owned zone
if cfxPlayerScore and cfxOwnedZones then
--local theUnit = conf.unit
cfxHeloTroops.scoreWhenCapturing(theUnit)
end
-- set own troops to 0 and erase type string
conf.troopsOnBoardNum = 0
conf.troopsOnBoard = {}
conf.troopsOnBoard.name = "***wasdeployed***"
cfxHeloTroops.unitConfigs[theUnit:getName()] = conf -- forced write-back (strange...)
if cfxHeloTroops.verbose then
trigger.action.outText("+++heloT: doDeployTroops unit <" .. conf.unit:getName() .. "> reset to <" .. conf.troopsOnBoardNum .. "> troops on board", 30)
end
-- reset menu
cfxHeloTroops.removeComms(conf.unit)
cfxHeloTroops.setCommsMenu(conf.unit)
end
function cfxHeloTroops.deployTroopsFromHelicopter(conf)
local unitTypes = {} -- build type names
local theUnit = conf.unit
local p = theUnit:getPoint()
-- split the conf.troopsOnBoardTypes into an array of types
unitTypes = dcsCommon.splitString(conf.troopsOnBoard.types, ",")
if #unitTypes < 1 then
table.insert(unitTypes, "Soldier M4") -- fallback
end
local range = conf.troopsOnBoard.range
local orders = conf.troopsOnBoard.orders
local dest = conf.troopsOnBoard.destination
local theName = conf.troopsOnBoard.name
local moveFormation = conf.troopsOnBoard.moveFormation
local code = conf.troopsOnBoard.code
local canDrive = conf.troopsOnBoard.canDrive
local theCoalition = theUnit:getGroup():getCoalition() -- my coa
if not orders then orders = "guard" end
orders = string.lower(orders)
-- order "wait" processing if not in special drop zone:
-- if the orders were pre-pended with "wait-"
-- remove that, so after dropping they do what their
-- orders where AFTER removing "wait"
-- or if setWait is true, add it
local closestDropZone = cfxZones.getClosestZone(p, cfxHeloTroops.dropzones)
if dcsCommon.stringStartsWith(orders, "wait-") then
local dropWait = false
if closestDropZone and closestDropZone:pointInZone(p) then
if closestDropZone.dropCoa == 0 or closestDropZone.dropCoa == theCoalition then
if not closestDropZone.keepWait then dropWait = true end
end
else
dropWait = true -- outside of any drop zones
end
-- see if we are in a drop zone
if dropWait then
orders = dcsCommon.removePrefix(orders, "wait-")
trigger.action.outTextForGroup(conf.id, "+++ <" .. conf.troopsOnBoard.name .. "> revoke 'wait' orders, proceed with <".. orders .. ">", 30)
else trigger.action.outTextForGroup(conf.id, "+++ <" .. conf.troopsOnBoard.name .. "> keeping 'wait' orders (".. orders .. ")", 30) end
end
if not dcsCommon.stringStartsWith(orders, "wait-") then
local setWait = false
if closestDropZone and closestDropZone:pointInZone(p) then
if closestDropZone.dropCoa == 0 or closestDropZone.dropCoa == theCoalition then
if not closestDropZone.setWait then setWait = true end
end
end
-- see if we are in a drop zone
if setWait then
orders = "wait-" .. orders
trigger.action.outTextForGroup(conf.id, "+++ <" .. conf.troopsOnBoard.name .. "> added 'wait' orders: <".. orders .. ">", 30)
else trigger.action.outTextForGroup(conf.id, "+++ <" .. conf.troopsOnBoard.name .. "> keeping orders (".. orders .. ")", 30) end
end
-- calculate drop point using delta, phi and unit heading
local f = "circle_out"
local delta = 0
local phi = 0
f, delta, phi = cfxHeloTroops.formation2dml(conf.dropFormation)
if cfxHeloTroops.verbose then
trigger.action.outText("formation: <" .. f .. ">, delta <" .. delta .. ">, phi <" .. phi .. ">", 30)
end
local uh = dcsCommon.getUnitHeading(theUnit)
p = dcsCommon.pointInDirectionOfPointXYY(uh + phi, delta, p)
local chopperZone = cfxZones.createSimpleZone("choppa", p, 12) -- 12 m radius around choppa
local theGroup, theData = cfxZones.createGroundUnitsInZoneForCoalition (
theCoalition,
theName, -- group name, may be tracked
chopperZone,
unitTypes,
f, --conf.dropFormation,
uh * 57.2958, -- heading in degrees, may need a formation offset like 90
nil, -- liveries not yet supported
canDrive)
-- persistence management
local troopData = {}
troopData.groupData = theData
troopData.orders = orders -- always set
troopData.side = theCoalition
troopData.range = range
troopData.destination = dest -- only for attackzone orders
cfxHeloTroops.deployedTroops[theData.name] = troopData
local troop = cfxGroundTroops.createGroundTroops(theGroup, range, orders, moveFormation, code, canDrive)
if orders == "captureandhold" then
-- we get the target zone NOW!!! before we flip the zone and
-- and make them run to the wrong zone
dest = cfxGroundTroops.getClosestEnemyZone(troop)
troopData.destination = dest
if dest then
trigger.action.outText("Inserting troops to capture zone <" .. dest.name .. ">", 30)
else
trigger.action.outText("+++heloT: WARNING: cap&hold: can't find a zone to cap.", 30)
end
end
troop.destination = dest -- transfer target zone for attackzone oders
cfxGroundTroops.addGroundTroopsToPool(troop) -- will schedule move orders
trigger.action.outTextForGroup(conf.id, "<" .. theGroup:getName() .. "> have deployed to the ground with orders " .. orders .. "!", 30)
trigger.action.outSoundForGroup(conf.id, cfxHeloTroops.disembarkSound)
-- if tracked by a tracker, and pass them back for un-limbo
if groupTracker then
local isTracking, numTracking, trackers = groupTracker.groupNameTrackedBy(theName)
if isTracking then
for idx, theTracker in pairs (trackers) do
groupTracker.addGroupToTracker(theGroup, theTracker)
if cfxHeloTroops.verbose then
trigger.action.outText("+++Helo: un-limbo and tracking group <" .. theName .. "> with tracker <" .. theTracker.name .. ">", 30)
end
end
end
end
-- bang on dropZones
for name, theZone in pairs(cfxHeloTroops.dropZones) do
-- can employ coalition test here as well, maybe later?
if theZone:isPointInsideZone(p) then
if theZone.dropCoa == 0 or theCoalition == theZone.dropCoa then
if cfxHeloTroops.verbose or theZone.verbose then
trigger.action.outText("+++Helo: will bang! on dropZone <" .. theZone.name .. "> output dropZone! <" .. theZone.droppedFlag .. "> with method <" .. theZone.dropMethod .. ">", 30)
end
theZone:pollFlag(theZone.droppedFlag, theZone.dropMethod)
end
if theZone.autoDespawn and theZone.autoDespawn > 0 then
args = {}
args.theZone = theZone
args.theGroup = theGroup
timer.scheduleFunction(cfxHeloTroops.autoDespawn, args, timer.getTime() + theZone.autoDespawn)
end
end
end
end
function cfxHeloTroops.autoDespawn(args)
if not args then return end
local theZone = args.theZone
local theGroup = args.theGroup
if theZone.verbose then
trigger.action.outText("+++Helo: auto-despawning drop in drop zone <" .. theZone.name .. ">", 30)
end
if not theGroup then return end
if Group.isExist(theGroup) then
Group.destroy(theGroup)
end
end
--
-- Loading Troops
--
function cfxHeloTroops.redirectLoadGroup(args)
timer.scheduleFunction(cfxHeloTroops.doLoadGroup, args, timer.getTime() + 0.1)
end
function cfxHeloTroops.doLoadGroup(args)
local conf = args[1]
local group = args[2]
if not group then return end
if not Group.isExist(group) then return end -- edge case: group died in the past 0.1 seconds
conf.troopsOnBoard = {}
-- all we need to do is disassemble the group into type
conf.troopsOnBoard.types = dcsCommon.getGroupTypeString(group)
-- get the size
conf.troopsOnBoardNum = group:getSize()
-- and name
local gName = group:getName()
conf.troopsOnBoard.name = gName
-- and put it all into the helicopter config
-- destroy the group:
-- if it was tracked, tell tracker to move it to limbo
-- to remember it
if groupTracker then
-- only if groupTracker is active
local isTracking, numTracking, trackers = groupTracker.groupTrackedBy(group)
if isTracking then
-- we need to put them in limbo for every tracker
for idx, aTracker in pairs(trackers) do
if cfxHeloTroops.verbose then
trigger.action.outText("+++Helo: moving group <" .. gName .. "> to limbo for tracker <" .. aTracker.name .. ">", 30)
end
groupTracker.moveGroupToLimboForTracker(group, aTracker)
end
end
end
-- then, remove it from the pool
local pooledGroup = cfxGroundTroops.getGroundTroopsForGroup(group)
if pooledGroup then
-- copy important info from the troops
-- if they are set
conf.troopsOnBoard.orders = pooledGroup.orders
conf.troopsOnBoard.range = pooledGroup.range
conf.troopsOnBoard.destination = pooledGroup.destination -- may be nil
conf.troopsOnBoard.moveFormation = pooledGroup.moveFormation
if pooledGroup.orders and pooledGroup.orders == "captureandhold" then
conf.troopsOnBoard.destination = nil -- forget last destination so they can be helo-redeployed
end
conf.troopsOnBoard.code = pooledGroup.code
conf.troopsOnBoard.canDrive = pooledGroup.canDrive
cfxGroundTroops.removeTroopsFromPool(pooledGroup)
trigger.action.outTextForGroup(conf.id, "Team '".. conf.troopsOnBoard.name .."' loaded and has orders <" .. conf.troopsOnBoard.orders .. ">", 30)
else
if cfxHeloTroops.verbose then
trigger.action.outText("+++heloT: ".. conf.troopsOnBoard.name .." was not committed to ground troops", 30)
end
end
-- TODO: add weight changing code
-- TODO: ensure compatibility with CSAR module
group:destroy()
-- now immediately run a GC so this group is removed
-- from any save data
cfxHeloTroops.GC()
-- say so
trigger.action.outTextForGroup(conf.id, "Team '".. conf.troopsOnBoard.name .."' aboard, ready to go!", 30)
trigger.action.outSoundForGroup(conf.id, cfxHeloTroops.loadSound)
-- reset menu
cfxHeloTroops.removeComms(conf.unit)
cfxHeloTroops.setCommsMenu(conf.unit)
end
--
-- spawning troops
--
function cfxHeloTroops.redirectSpawnGroup(args)
timer.scheduleFunction(cfxHeloTroops.doSpawnGroup, args, timer.getTime() + 0.1)
end
function cfxHeloTroops.delayedCommsResetForUnit(args)
local theUnit = args[1]
cfxHeloTroops.removeComms(theUnit)
cfxHeloTroops.setCommsMenu(theUnit)
end
function cfxHeloTroops.doSpawnGroup(args)
local conf = args[1]
local theSpawner = args[2]
-- theSpawner can be of type cfxSpawnZone !!!OR!!! cfxCloneZones
-- make sure cooldown on spawner has timed out, else notify that you have to wait
local now = timer.getTime()
if now < (theSpawner.lastSpawnTimeStamp + theSpawner.cooldown) then
local delta = math.floor(theSpawner.lastSpawnTimeStamp + theSpawner.cooldown - now)
trigger.action.outTextForGroup(conf.id, "Still redeploying (" .. delta .. " seconds left)", 30)
return
end
theSpawner.spawnWithSpawner(theSpawner) -- can be both spawner and cloner (Lua "polymorphism"
trigger.action.outTextForGroup(conf.id, "Deploying <" .. theSpawner.baseName .. "> now...", 30)
-- reset all comms so we can include new troops
-- into load menu
timer.scheduleFunction(cfxHeloTroops.delayedCommsResetForUnit, {conf.unit, "ignore"}, now + 1.0)
end
--
-- handle events
--
function cfxHeloTroops:onEvent(theEvent)
local theID = theEvent.id
local initiator = theEvent.initiator
if not initiator then return end -- not interested
local theUnit = initiator
-- see if this is a player aircraft
if not theUnit.getPlayerName then return end -- not a player
if not theUnit:getPlayerName() then return end -- not a player
local name = theUnit:getName() -- moved to a later
-- only for troop carriers (not just helos any more)
if not dcsCommon.isTroopCarrier(theUnit, cfxHeloTroops.troopCarriers) then
return
end
if theID == 4 or theID == 55 then -- land
cfxHeloTroops.heloLanded(theUnit)
end
if theID == 3 or theID == 54 then -- take off
cfxHeloTroops.heloDeparted(theUnit)
end
if theID == 5 or theID == 30 then -- crash or unitLost
cfxHeloTroops.heloCrashed(theUnit)
end
if theID == 20 or -- player enter
theID == 15 then -- birth
cfxHeloTroops.cleanHelo(theUnit)
end
if theID == 21 then -- player leave
cfxHeloTroops.cleanHelo(theUnit)
local conf = cfxHeloTroops.getConfigForUnitNamed(name)
if conf then
cfxHeloTroops.removeCommsFromConfig(conf)
end
return
end
cfxHeloTroops.setCommsMenu(theUnit)
end
--
-- Regular GC and housekeeping
--
function cfxHeloTroops.GC()
-- GC run. remove all my dead remembered troops
local filteredAttackers = {}
local before = #cfxHeloTroops.deployedTroops
for gName, gData in pairs (cfxHeloTroops.deployedTroops) do
-- all we need to do is get the group of that name
-- and if it still returns units we are fine
local gameGroup = Group.getByName(gName)
if gameGroup and gameGroup:isExist() and gameGroup:getSize() > 0 then
filteredAttackers[gName] = gData
end
end
cfxHeloTroops.deployedTroops = filteredAttackers
if cfxHeloTroops.verbose then
trigger.action.outText("helo troops GC ran: before <" .. before .. ">, after <" .. #cfxHeloTroops.deployedTroops .. ">", 30)
end
end
function cfxHeloTroops.houseKeeping()
timer.scheduleFunction(cfxHeloTroops.houseKeeping, {}, timer.getTime() + 5 * 60) -- every 5 minutes
cfxHeloTroops.GC()
end
--
-- read config zone
--
function cfxHeloTroops.readConfigZone()
-- note: must match exactly!!!!
local theZone = cfxZones.getZoneByName("heloTroopsConfig")
if not theZone then
theZone = cfxZones.createSimpleZone("heloTroopsConfig")
end
cfxHeloTroops.verbose = theZone.verbose
if theZone:hasProperty("legalTroops") then
local theTypesString = theZone:getStringFromZoneProperty("legalTroops", "")
local unitTypes = dcsCommon.splitString(theTypesString, ",")
if #unitTypes < 1 then
unitTypes = {"Soldier AK", "Infantry AK", "Infantry AK ver2", "Infantry AK ver3", "Infantry AK Ins", "Soldier M249", "Soldier M4 GRG", "Soldier M4", "Soldier RPG", "Paratrooper AKS-74", "Paratrooper RPG-16", "Stinger comm dsr", "Stinger comm", "Soldier stinger", "SA-18 Igla-S comm", "SA-18 Igla-S manpad", "Igla manpad INS", "SA-18 Igla comm", "SA-18 Igla manpad",} -- default
else
unitTypes = dcsCommon.trimArray(unitTypes)
end
cfxHeloTroops.legalTroops = unitTypes
end
cfxHeloTroops.troopWeight = theZone:getNumberFromZoneProperty("troopWeight", 100) -- kg average weight per trooper
cfxHeloTroops.autoDrop = theZone:getBoolFromZoneProperty("autoDrop", false)
cfxHeloTroops.autoPickup = theZone:getBoolFromZoneProperty("autoPickup", false)
cfxHeloTroops.pickupRange = theZone:getNumberFromZoneProperty("pickupRange", 100)
if theZone:hasProperty("pickupRang") then
cfxHeloTroops.pickupRange = theZone:getNumberFromZoneProperty("pickupRang", 100)
end
cfxHeloTroops.combatDropScore = theZone:getNumberFromZoneProperty( "combatDropScore", 200)
cfxHeloTroops.actionSound = theZone:getStringFromZoneProperty("actionSound", "Quest Snare 3.wav")
cfxHeloTroops.loadSound = theZone:getStringFromZoneProperty("loadSound", cfxHeloTroops.actionSound)
cfxHeloTroops.disembarkSound = theZone:getStringFromZoneProperty("disembarkSound", cfxHeloTroops.actionSound)
cfxHeloTroops.requestRange = theZone:getNumberFromZoneProperty("requestRange", 500)
cfxHeloTroops.enforceDropZones = theZone:getBoolFromZoneProperty("enforceDropZones", false)
-- add own troop carriers
if theZone:hasProperty("troopCarriers") then
local tc = theZone:getStringFromZoneProperty("troopCarriers", "UH-1D")
tc = dcsCommon.splitString(tc, ",")
cfxHeloTroops.troopCarriers = dcsCommon.trimArray(tc)
end
if theZone:hasProperty("attachTo:") then
local attachTo = theZone:getStringFromZoneProperty("attachTo:", "<none>")
if radioMenu then -- requires optional radio menu to have loaded
local mainMenu = radioMenu.mainMenus[attachTo]
if mainMenu then
cfxHeloTroops.mainMenu = mainMenu
else
trigger.action.outText("+++heloT: cannot find super menu <" .. attachTo .. ">", 30)
end
else
trigger.action.outText("+++heloT: REQUIRES radioMenu to run before cfxHeloTroops. 'AttachTo:' ignored.", 30)
end
end
end
--
-- Load / Save data
--
function cfxHeloTroops.saveData()
local theData = {}
local allTroopData = {}
-- run a GC pre-emptively
cfxHeloTroops.GC()
-- now simply iterate and save all deployed troops
for gName, gData in pairs(cfxHeloTroops.deployedTroops) do
local sData = dcsCommon.clone(gData)
dcsCommon.synchGroupData(sData.groupData)
if sData.destination then
if type(sData.destination) == "table" and (sData.destination.name) then
net.log("cfxHeloTroops: decycling troop 'destination' for <" .. sData.destination.name .. ">")
sData.destination = sData.destination.name
else
sData.destination = nil
net.log("cfxHeloTroops: decycling deployed troops 'destination' nilling for safety")
end
end
allTroopData[gName] = sData
end
theData.troops = allTroopData
return theData
end
function cfxHeloTroops.loadData()
if not persistence then return end
local theData = persistence.getSavedDataForModule("cfxHeloTroops")
if not theData then
if cfxHeloTroops.verbose then
trigger.action.outText("+++heloT: no save date received, skipping.", 30)
end
return
end
-- simply spawn all troops that we have carried around and
-- were still alive when we saved. Troops that were picked
-- up by helos never made it to the save file
local allTroopData = theData.troops
for gName, gdTroop in pairs (allTroopData) do
local gData = gdTroop.groupData
local orders = gdTroop.orders
local side = gdTroop.side
local range = gdTroop.range
local cty = gData.cty
local cat = gData.cat
local dest = nil
local code = gdTroop.code
local canDrive = gdTroop.canDrive
local formation = gdTroop.moveFormation
local code = gdTroop.code
local canDrive = gdTroop.canDrive
if canDrive then -- restore canDrive to all units
local units = gData.units
for idx, theUnit in pairs(units) do
theUnit.playerCanDrive = drivable
end
end
-- synch destination from name to real zone
if gdTroop.destination then
dest = cfxZones.getZoneByName(gdTroop.destination)
net.log("cfxHeloTroops: attempting to restore troop destination zone <" .. gdTroop.destination .. ">")
end
-- now spawn, but first
-- add to my own deployed queue so we can save later
local gdClone = dcsCommon.clone(gdTroop)
cfxHeloTroops.deployedTroops[gName] = gdClone
local theGroup = coalition.addGroup(cty, cat, gData)
-- post-proccing for cfxGroundTroops
-- add to groundTroops
local newTroops = cfxGroundTroops.createGroundTroops(theGroup, range, orders, moveFormation, code, canDrive)
newTroops.destination = dest
cfxGroundTroops.addGroundTroopsToPool(newTroops)
end
end
--
-- Start
--
function cfxHeloTroops.start()
-- check libs
if not dcsCommon.libCheck("cfx Helo Troops",
cfxHeloTroops.requiredLibs) then
return false
end
-- read config zone
cfxHeloTroops.readConfigZone()
-- read drop zones
local attrZones = cfxZones.getZonesWithAttributeNamed("dropZone!")
for k, aZone in pairs(attrZones) do
cfxHeloTroops.processDropZone(aZone)
cfxHeloTroops.dropZones[aZone.name] = aZone
end
-- start housekeeping
cfxHeloTroops.houseKeeping()
world.addEventHandler(cfxHeloTroops)
trigger.action.outText("cf/x Helo Troops v" .. cfxHeloTroops.version .. " started", 30)
-- persistence:
-- load all save data and populate map with troops that
-- we deployed when we last saved.
if persistence then
-- sign up for persistence
callbacks = {}
callbacks.persistData = cfxHeloTroops.saveData
persistence.registerModule("cfxHeloTroops", callbacks)
-- now load my data
cfxHeloTroops.loadData()
end
return true
end
-- let's get rolling
if not cfxHeloTroops.start() then
trigger.action.outText("cf/x Helo Troops aborted: missing libraries", 30)
cfxHeloTroops = nil
end
-- TODO: weight when loading troops
-- TODO: keepWait for dropzones: troops keep their "wait" orders when dropzone has that tag