mirror of
https://github.com/weyne85/DML.git
synced 2025-10-29 16:57:49 +00:00
532 lines
17 KiB
Lua
532 lines
17 KiB
Lua
-- cfxCommander - issue dcs commands to groups etc
|
|
--
|
|
-- supports scheduling
|
|
-- *** EXTENDS ZONES: 'pathing' attribute
|
|
--
|
|
cfxCommander = {}
|
|
cfxCommander.version = "2.0.0"
|
|
--[[-- VERSION HISTORY
|
|
- 1.0.5 - createWPListForGroupToPointViaRoads: detect no road found
|
|
- 1.0.6 - build in more group checks in assign wp list
|
|
- added sanity checks for doScheduledTask
|
|
- assignWPListToGroup now can schedule tasks
|
|
- makeGroupGoThere supports scheduling
|
|
- makeGroupGoTherePreferringRoads supports scheduling
|
|
- scheduleTaskForGroup supports immediate execution
|
|
- makeGroupHalt
|
|
- 1.0.7 - warning if road shorter than direct
|
|
- forceOffRoad option
|
|
- noRoadsAtAll option
|
|
- 1.1.0 - load libs
|
|
- pathing zones. Currently only supports
|
|
- offroad to override road-usage
|
|
- pathing zones are overridden by noRoadsAtAll
|
|
- CommanderConfig zones
|
|
- 1.1.1 - default pathing for pathing zone is normal, not offroad
|
|
- 1.1.2 - makeGroupTransmit
|
|
- makeGroupStopTransmitting
|
|
- verbose check before path warning
|
|
- added delay defaulting for most scheduling functions
|
|
- 1.1.3 - isExist() guard improvements for multiple methods
|
|
- cleaned up comments
|
|
- 1.1.4 - hardened makeGroupGoThere()
|
|
- 2.0.0 - dml zones
|
|
- units now can move with moveFormation
|
|
- hardened performCommands()
|
|
- createWPListForGroupToPoint() supports moveFormation
|
|
- makeGroupGoTherePreferringRoads() supports moveFormation
|
|
--]]--
|
|
|
|
cfxCommander.requiredLibs = {
|
|
"dcsCommon", -- common is of course needed for everything
|
|
"cfxZones", -- zones management for pathing zones
|
|
}
|
|
|
|
cfxCommander.verbose = false
|
|
cfxCommander.forceOffRoad = true -- if true, vehicles path follow roads, but may drive offroad (they follow vertex points from path but not the road as they are still commanded 'offroad')
|
|
cfxCommander.noRoadsAtAll = true -- if true, always go direct, overrides forceOffRoad when true. Always a two-point path. Here, there, bang!
|
|
cfxCommander.pathZones = {} -- zones that can override road settings
|
|
|
|
--
|
|
-- path zone
|
|
--
|
|
function cfxCommander.processPathingZone(aZone) -- process attribute and add to zone
|
|
local pathing = cfxZones.getStringFromZoneProperty(aZone, "pathing", "normal") -- must be "offroad" to force offroad
|
|
pathing = pathing:lower()
|
|
-- currently no validation of attribute
|
|
aZone.pathing = pathing
|
|
end
|
|
|
|
function cfxCommander.addPathingZone(aZone)
|
|
table.insert(cfxCommander.pathZones, aZone)
|
|
end
|
|
|
|
function cfxCommander.hasPathZoneFor(here, there)
|
|
for idx, aZone in pairs(cfxCommander.pathZones) do
|
|
if cfxZones.pointInZone(here, aZone) then return aZone end
|
|
if cfxZones.pointInZone(there, aZone) then return aZone end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
--
|
|
-- Config Zone Reading if present
|
|
--
|
|
function cfxCommander.readConfigZone()
|
|
-- note: must match exactly!!!!
|
|
local theZone = cfxZones.getZoneByName("CommanderConfig")
|
|
if not theZone then
|
|
theZone = cfxZones.createSimpleZone("CommanderConfig")
|
|
end
|
|
cfxCommander.verbose = theZone.verbose
|
|
cfxCommander.forceOffRoad = theZone:getBoolFromZoneProperty("forceOffRoad", false) -- if true, vehicles path follow roads, but may drive offroad
|
|
cfxCommander.noRoadsAtAll = theZone:getBoolFromZoneProperty("noRoadsAtAll", false)
|
|
end
|
|
|
|
--
|
|
-- Options are key, value pairs. Scheduler when you are creating groups
|
|
--
|
|
|
|
function cfxCommander.doOption(data)
|
|
if cfxCommander.verbose then
|
|
trigger.action.outText("Commander: setting option " .. data.key .. " --> " .. data.value, 30)
|
|
end
|
|
|
|
local theController = data.group:getController()
|
|
theController:setOption(data.key, data.value)
|
|
end
|
|
|
|
function cfxCommander.scheduleOptionForGroup(group, key, value, delay)
|
|
local data = {}
|
|
if not delay then delay = 0.1 end
|
|
data.group = group
|
|
data.key = key
|
|
data.value = value
|
|
timer.scheduleFunction(cfxCommander.doOption, data, timer.getTime() + delay)
|
|
end
|
|
|
|
--
|
|
-- performCommand is a special version of issuing a command
|
|
-- that can be easily schduled by pushing the commandData on
|
|
-- the stack with scheduling it
|
|
-- group or name must be filled to get the group,
|
|
-- and the command table is what is going to be passed to the setCommand
|
|
-- commands are given in an array, so you can stack commands
|
|
function cfxCommander.performCommands(commandData)
|
|
-- see if we have a group
|
|
if not commandData.group then
|
|
commandData.group = Group.getByName(commandData.name) -- better be inited!
|
|
end
|
|
if not Group.isExist(commandData.group) then
|
|
-- something bad is happening
|
|
return nil
|
|
end
|
|
-- get the AI
|
|
local theController = commandData.group:getController()
|
|
if not theController then return nil end
|
|
|
|
for i=1, #commandData.commands do
|
|
if cfxCommander.verbose then
|
|
trigger.action.outText("Commander: performing " .. commandData.commands[i].id, 30)
|
|
end
|
|
theController:setCommand(commandData.commands[i])
|
|
end
|
|
|
|
return nil -- a timer called us, so we return no desire to be rescheduled
|
|
end
|
|
|
|
function cfxCommander.scheduleCommands(data, delay)
|
|
if not delay then delay = 1 end
|
|
timer.scheduleFunction(cfxCommander.performCommands, data, timer.getTime() + delay)
|
|
end
|
|
|
|
function cfxCommander.scheduleSingleCommand(group, command, delay)
|
|
if not delay then delay = 1 end
|
|
local data = createCommandDataTableFor(group)
|
|
cfxCommander.addCommand(data, command)
|
|
cfxCommander.scheduleCommands(data, delay)
|
|
end
|
|
|
|
|
|
function cfxCommander.createCommandDataTableFor(group, name)
|
|
local cD = {}
|
|
if not group then
|
|
cD.name = name
|
|
else
|
|
cD.group = group
|
|
end
|
|
cD.commands={}
|
|
return cD
|
|
end
|
|
|
|
function cfxCommander.addCommand(theCD, theCommand)
|
|
if not theCD then return end
|
|
if not theCommand then return end
|
|
|
|
table.insert(theCD.commands, theCommand)
|
|
end
|
|
|
|
function cfxCommander.createSetFrequencyCommand(freq, modulator)
|
|
local theCmd = {}
|
|
if not freq then freq = 100 end
|
|
if not modulator then modulator = 0 end -- AM = 0, default
|
|
theCmd.id = 'SetFrequency'
|
|
theCmd.params = {}
|
|
theCmd.params.frequency = freq * 10000 -- 88 --> 880000. 124 --> 1.24 MHz
|
|
theCmd.params.modulation = modulator
|
|
return theCmd
|
|
end
|
|
|
|
-- oneShot is optional. if present and anything but false, will cause message to
|
|
-- me sent only once, no loops
|
|
function cfxCommander.createTransmissionCommand(filename, oneShot)
|
|
local looping = true
|
|
if not filename then filename = "dummy" end
|
|
if oneShot then looping = false end
|
|
local theCmd = {}
|
|
theCmd.id = 'TransmitMessage'
|
|
theCmd.params = {}
|
|
theCmd.params.loop = looping
|
|
theCmd.params.file = "l10n/DEFAULT/" .. filename -- need to prepend the resource string
|
|
return theCmd
|
|
end
|
|
|
|
function cfxCommander.createStopTransmissionCommand()
|
|
local theCmd = {}
|
|
theCmd.id = 'stopTransmission'
|
|
theCmd.params = {}
|
|
return theCmd
|
|
end
|
|
|
|
--
|
|
-- tasks
|
|
--
|
|
|
|
function cfxCommander.doScheduledTask(data)
|
|
if cfxCommander.verbose then
|
|
trigger.action.outText("Commander: setting task " .. data.task.id .. " for group " .. data.group:getName(), 30)
|
|
end
|
|
local theGroup = data.group
|
|
if not theGroup then return end
|
|
if not Group.isExist(theGroup) then return end
|
|
|
|
local theController = theGroup:getController()
|
|
theController:pushTask(data.task)
|
|
end
|
|
|
|
function cfxCommander.scheduleTaskForGroup(group, task, delay)
|
|
if not delay then delay = 0 end
|
|
local data = {}
|
|
data.group = group
|
|
data.task = task
|
|
if delay < 0.001 then
|
|
cfxCommander.doScheduledTask(data) -- immediate execution
|
|
return
|
|
end
|
|
timer.scheduleFunction(cfxCommander.doScheduledTask, data, timer.getTime() + delay)
|
|
end
|
|
|
|
function cfxCommander.createAttackGroupCommand(theGroupToAttack)
|
|
local task = {}
|
|
task.id = 'AttackGroup'
|
|
task.params = {}
|
|
task.params.groupID = theGroupToAttack:getID()
|
|
return task
|
|
end
|
|
|
|
function cfxCommander.createEngageGroupCommand(theGroupToAttack)
|
|
local task = {}
|
|
task.id = 'EngageGroup'
|
|
task.params = {}
|
|
task.params.groupID = theGroupToAttack:getID()
|
|
return task
|
|
end
|
|
|
|
--
|
|
-- waypoints, routes etc
|
|
--
|
|
|
|
-- basic waypoint is for ground units. point can be xyz or xy
|
|
function cfxCommander.createBasicWaypoint(point, speed, formation)
|
|
local wp = {}
|
|
wp.x = point.x
|
|
-- support xyz and xy format
|
|
if point.z then
|
|
wp.y = point.z
|
|
else
|
|
wp.y = point.y
|
|
end
|
|
|
|
if not speed then speed = 6 end -- 6 m/s = 20 kph
|
|
wp.speed = speed
|
|
|
|
if cfxCommander.forceOffRoad then
|
|
formation = "Off Road"
|
|
end
|
|
|
|
if not formation then formation = "Off Road" end
|
|
-- legal formations:
|
|
-- Off Road
|
|
-- On Road -- second letter upper case?
|
|
-- Cone
|
|
-- Rank
|
|
-- Diamond
|
|
-- Vee
|
|
-- EchelonR
|
|
-- EchelonL
|
|
wp.action = formation -- silly name, but that's how ME does it
|
|
wp.type = 'Turning Point'
|
|
return wp
|
|
|
|
end
|
|
|
|
function cfxCommander.buildTaskFromWPList(wpList)
|
|
-- build the task that will make a group follow the WP list
|
|
-- we do this by creating a "Mission" task around the WP List
|
|
-- WP list is consumed by this action
|
|
local missionTask = {}
|
|
missionTask.id = "Mission"
|
|
missionTask.params = {}
|
|
missionTask.params.route = {}
|
|
missionTask.params.route.points=wpList
|
|
return missionTask
|
|
end
|
|
|
|
function cfxCommander.assignWPListToGroup(group, wpList, delay)
|
|
if not delay then delay = 0 end
|
|
if not group then return end
|
|
if type(group) == 'string' then -- group name, nice mist trick
|
|
group = Group.getByName(group)
|
|
end
|
|
if not group then return end
|
|
if not Group.isExist(group) then return end
|
|
|
|
local theTask = cfxCommander.buildTaskFromWPList(wpList)
|
|
local ctrl = group:getController()
|
|
|
|
cfxCommander.scheduleTaskForGroup(group, theTask, delay)
|
|
end
|
|
|
|
--[[--
|
|
Formations and their "action" keywords
|
|
Line Abreast = "Rank"
|
|
Cone = "Cone"
|
|
Vee = "Vee"
|
|
Diamond = "Diamond"
|
|
Echelon Left = "EchelonL"
|
|
Echelon Right = "EchelonR"
|
|
Custom = "Custom"
|
|
|
|
--]]--
|
|
|
|
function cfxCommander.createWPListForGroupToPoint(group, point, speed, moveFormation)
|
|
if type(group) == 'string' then -- group name
|
|
group = Group.getByName(group)
|
|
end
|
|
|
|
local wpList = {}
|
|
-- here we are, and we want to go there. In DCS, this means that
|
|
-- we need to create a wp list consisting of here and there
|
|
local here = dcsCommon.getGroupLocation(group)
|
|
local wpHere = cfxCommander.createBasicWaypoint(here, speed, moveFormation)
|
|
local wpThere = cfxCommander.createBasicWaypoint(point, speed, moveFormation)
|
|
wpList[1] = wpHere
|
|
wpList[2] = wpThere
|
|
return wpList
|
|
end
|
|
|
|
-- make a ground units group head to a waypoint by replacing the entire mission
|
|
-- with a two-waypoint lsit from (here) to there at speed and formation. formation
|
|
-- default is 'off road'
|
|
function cfxCommander.makeGroupGoThere(group, there, speed, formation, delay)
|
|
if not delay then delay = 0 end
|
|
if type(group) == 'string' then -- group name
|
|
group = Group.getByName(group)
|
|
end
|
|
|
|
if not Group.isExist(group) then
|
|
trigger.action.outText("cmdr: makeGroupGoThere() - group does not exist", 30)
|
|
return
|
|
end
|
|
|
|
-- check that we can get a location for the group
|
|
local here = dcsCommon.getGroupLocation(group)
|
|
if not here then
|
|
return
|
|
end
|
|
|
|
local wp = cfxCommander.createWPListForGroupToPoint(group, there, speed, formation)
|
|
|
|
cfxCommander.assignWPListToGroup(group, wp, delay)
|
|
end
|
|
|
|
function cfxCommander.calculatePathLength(roadPoints)
|
|
local totalLen = 0
|
|
if #roadPoints < 2 then return 0 end
|
|
for i=1, #roadPoints-1 do
|
|
totalLen = totalLen + dcsCommon.dist(roadPoints[i], roadPoints[i+1])
|
|
end
|
|
return totalLen
|
|
end
|
|
|
|
-- make ground units go from here (group location) to there, using roads if possible
|
|
function cfxCommander.createWPListForGroupToPointViaRoads(group, point, speed)
|
|
if type(group) == 'string' then -- group name
|
|
group = Group.getByName(group)
|
|
end
|
|
|
|
local wpList = {}
|
|
-- here we are, and we want to go there. In DCS, this means that
|
|
-- we need to create a wp list consisting of here and there
|
|
-- when going via roads, we add to more wayoints:
|
|
-- go on-roads and leaveRoads.
|
|
-- only if we can get these two additional points, we do that, else we
|
|
-- fall back to direct route
|
|
|
|
local here = dcsCommon.getGroupLocation(group)
|
|
|
|
-- now generate a list of all points from here to there that uses roads
|
|
local rawRoadPoints = land.findPathOnRoads('roads', here.x, here.z, point.x, point.z)
|
|
-- this is the entire path. calculate the length and make
|
|
-- sure that path on-road isn't more than twice as long
|
|
-- that can happen if a bridge is out or we need to go around a hill
|
|
if not rawRoadPoints or #rawRoadPoints<3 then
|
|
trigger.action.outText("+++ no roads leading there. Taking direct approach", 30)
|
|
return cfxCommander.createWPListForGroupToPoint(group, point, speed)
|
|
end
|
|
|
|
local pathLength = cfxCommander.calculatePathLength(rawRoadPoints)
|
|
local direct = dcsCommon.dist(here, point)
|
|
if pathLength < direct and cfxCommander.verbose then
|
|
trigger.action.outText("+++dcsC: WARNING road path (" .. pathLength .. ") shorter than direct route(" .. direct .. "), will not path correctly", 30)
|
|
end
|
|
|
|
if pathLength > (2 * direct) then
|
|
-- road takes too long, take direct approach
|
|
return cfxCommander.createWPListForGroupToPoint(group, point, speed)
|
|
end
|
|
|
|
-- if we are here, the road trip is valid
|
|
for idx, wp in pairs(rawRoadPoints) do
|
|
-- createBasic... supports w.xy format
|
|
local theNewWP = cfxCommander.createBasicWaypoint(wp, speed, "On Road") -- force off road for better compatibility?
|
|
table.insert(wpList, theNewWP)
|
|
end
|
|
|
|
|
|
|
|
-- now make first and last entry OFF Road
|
|
local wpc = wpList[1]
|
|
wpc.action = "Off Road"
|
|
wpc = wpList[#wpList]
|
|
wpc.action = "Off Road"
|
|
|
|
return wpList
|
|
end
|
|
|
|
function cfxCommander.makeGroupGoTherePreferringRoads(group, there, speed, delay, moveFormation)
|
|
if type(group) == 'string' then -- group name
|
|
group = Group.getByName(group)
|
|
end
|
|
if not delay then delay = 0 end
|
|
|
|
if not moveFormation then moveFormation = "Off Road" end
|
|
|
|
if cfxCommander.noRoadsAtAll then
|
|
-- we don't even follow roads, completely forced off
|
|
cfxCommander.makeGroupGoThere(group, there, speed, moveFormation, delay)
|
|
return
|
|
end
|
|
|
|
-- see if we have an override situation
|
|
-- for one of the two points where a pathing Zone
|
|
-- overrides the roads setting
|
|
if #cfxCommander.pathZones > 0 then
|
|
local here = dcsCommon.getGroupLocation(group)
|
|
local oRide = cfxCommander.hasPathZoneFor(here, there)
|
|
if oRide and oRide.pathing == "offroad" then
|
|
-- yup, override road preference
|
|
cfxCommander.makeGroupGoThere(group, there, speed, moveFormation, delay)
|
|
return
|
|
end
|
|
end
|
|
|
|
-- viaRoads will only use roads if the road trip isn't more than twice
|
|
-- as long as the direct route
|
|
local wp = cfxCommander.createWPListForGroupToPointViaRoads(group, there, speed)
|
|
cfxCommander.assignWPListToGroup(group, wp, delay)
|
|
end
|
|
|
|
|
|
function cfxCommander.makeGroupHalt(group, delay)
|
|
if not group then return end
|
|
if not Group.isExist(group) then return end
|
|
if not delay then delay = 0 end
|
|
local theTask = {id = 'Hold', params = {}}
|
|
cfxCommander.scheduleTaskForGroup(group, theTask, delay)
|
|
end
|
|
|
|
function cfxCommander.makeGroupTransmit(group, tenKHz, filename, oneShot, delay)
|
|
if not group then return end
|
|
if not tenKHz then tenKHz = 20 end -- default to 200KHz
|
|
if not delay then delay = 1.0 end
|
|
if not filename then return end
|
|
if not oneShot then oneShot = false end
|
|
|
|
-- now build the transmission command
|
|
local theCommands = cfxCommander.createCommandDataTableFor(group)
|
|
local cmd = cfxCommander.createSetFrequencyCommand(tenKHz) -- freq in 10000 Hz
|
|
cfxCommander.addCommand(theCommands, cmd)
|
|
cmd = cfxCommander.createTransmissionCommand(filename, oneShot)
|
|
cfxCommander.addCommand(theCommands, cmd)
|
|
cfxCommander.scheduleCommands(theCommands, delay)
|
|
end
|
|
|
|
function cfxCommander.makeGroupStopTransmitting(group, delay)
|
|
if not delay then delay = 1 end
|
|
if not group then return end
|
|
local theCommands = cfxCommander.createCommandDataTableFor(group)
|
|
local cmd = cfxCommander.createStopTransmissionCommand()
|
|
cfxCommander.addCommand(theCommands, cmd)
|
|
cfxCommander.scheduleCommands(theCommands, delay)
|
|
end
|
|
|
|
|
|
function cfxCommander.start()
|
|
-- make sure we have loaded all relevant libraries
|
|
if not dcsCommon.libCheck("cfx Commander", cfxCommander.requiredLibs) then
|
|
trigger.action.outText("cf/x Commander aborted: missing libraries", 30)
|
|
return false
|
|
end
|
|
|
|
-- identify and process all 'pathing' zones
|
|
local pathZones = cfxZones.getZonesWithAttributeNamed("pathing")
|
|
for k, aZone in pairs(pathZones) do
|
|
cfxCommander.processPathingZone(aZone) -- process attribute and add to zone
|
|
cfxCommander.addPathingZone(aZone) -- remember it so we can smoke it
|
|
end
|
|
|
|
-- read config overides
|
|
cfxCommander.readConfigZone()
|
|
|
|
return true
|
|
end
|
|
|
|
if cfxCommander.start() then
|
|
trigger.action.outText("cfxCommander v" .. cfxCommander.version .. " loaded", 30)
|
|
else
|
|
trigger.action.outText("+++cfxCommander load FAILED", 30)
|
|
cfxCommander = nil
|
|
end
|
|
|
|
--[[-- known issues
|
|
|
|
- troops remain motionless until all are repaired or produced after cature
|
|
- long roads / roads not taken in persia
|
|
- all troops red and blue become motionless when one zone is occupied
|
|
- after capture, the troop capturing remains, all others can go on. one will always remain there
|
|
- rethink the factor to add to road, and simply add 100m
|
|
|
|
TODO: break long distances into smaller paths, and gravitate towards pathing zones if they have a 'gravitate' or similar attribute
|
|
--]]--
|