cfxGroundTroops = {} cfxGroundTroops.version = "1.7.8" cfxGroundTroops.ups = 1 cfxGroundTroops.verbose = false cfxGroundTroops.requiredLibs = { "dcsCommon", -- common is of course needed for everything -- pretty stupid to check for this since we -- need common to invoke the check, but anyway "cfxCommander", -- generic data module for weight -- cfxOwnedZones is optional } -- ground troops: a module to manage ground toops. makes groups of ground troops -- patrol and engage enemies and signal idle -- understands cfxOwnedZones orders 'attackOwnedZone' and will re-direct -- troops when a zone was captured by interacting with cfxOwnedZones to -- find the nearest non-owned zone and direct the group there -- USAGE -- Allocate a group in game and issue them marching orders towars a goal -- then createGroundTroops to allocate a structure used by this -- module and addTroopsToPool to have them then managed by this -- module cfxGroundTroops.deployedTroops = {} -- indexed by group name --[[-- version history 1.3.0 - added "wait-" prefix to have toops do nothing - added lazing 1.3.1 - sound for lazing msg is "UI_SCI-FI_Tone_Bright_Dry_20_stereo.wav" - lazing --> lasing in text 1.3.2 - set ups to 2 1.4.0 - queued updates except for lazers 1.4.1 - makeTroopsEngageZone now issues hold before moving on 5 seconds later - getTroopReport - include size of group 1.4.2 - uses unitIsInfantry from dcsCommon 1.5.0 - new scheduled updates per troop to reduce processor load - tiebreak code 1.5.1 - small bugfix in scheduled code 1.5.2 - checkSchedule - speed warning in scheduler - go off road when speed warning too much 1.5.3 - monitor troops - managed queue for ground troops - on second switch to offroad now removed from MQ 1.5.4 - removed debugging messages 1.5.5 - removed bug in troop report reading nil destination 1.6.0 - check modules 1.6.1 - troopsCallback management so you can be informed if a troop you have added to the pool is dead or has achieved a goal. callback will list reasons "dead" and "arrived" updateAttackers 1.6.2 - also accept 'lase' as 'laze', translate directly 1.7.0 - now can use groundTroopsConfig zone 1.7.1 - addTroopsDeadCallback() renamed to addTroopsCallback() - invokeCallbacksFor also accepts and passes on data block - troops is always passed in data block as .troops 1.7.2 - callback when group is neutralized on guard orders - callback when group is being engaged under guard orders 1.7.3 - callbacks for lase:tracking and lase:stop 1.7.4 - verbose flag, warnings suppressed 1.7.5 - some troop.group hardening with isExist() 1.7.6 - fixed switchToOffroad 1.7.7 - no longer case sensitive for orders 1.7.7 - updateAttackers() now inspects 'moving' status and invokes makeTroopsEngageZone - makeTroopsEngageZone() sets 'moving' status to true - createGroundTroops() sets moving status to false - updateZoneAttackers() uses moving 1.7.8 - better guards before invoking ownedZones an entry into the deployed troop table has the following attributes - group - the group - orders: "guard" - will guard the spot and look for enemies in range "patrol" - will walk between way points back and forth "laze" - will stay in place and try to laze visible vehicles in range "attackOwnedZone" - interface to cfxOwnedZones module, seeks out enemy zones to attack and capture them "wait-" do nothing. the "wait" prefix will be removed some time and then revealed. Used at least by heloTroops "train" - target dummies. ROE=HOLD, no ground loop "attack" - transition to destination, once there, stop and switch to guard. requires destination zone be set to a valid cfxZone - coalition - the coalition from the group - enemy - if set, the group this group it is engaging. this means the group is fighting and not idle - name - name of group, dan be freely changed - signature - "cfx" to tell apart from dcs groups - range = range to look for enemies. default is 300m. In "laze" orders, range to laze - lazeTarget - target currently lazing - lazeCode - laser code. default is 1688 - moving - has been given orders to move somewhere already. used for first movement order with attack orders usage: take a dcs group of ground troops and create a cfx ground troop record with createGroundTroops() then add this to the manager with addGroundTroopsToPool() you can control what the group is to do by changing the cfx troop attribute orders you can install a callback that will notify you if a troop reached a goal or was killed with addTroopsCallback() which will also give a reason callback pattern is myCallback(reason, theGroup, orders, data) with troop being the group, and orders the original orders, and reason a string containing why the callback was invoked. Currently defined reasons are - "dead" - entire group was killed - "arrived" - at least a part of group arrived at destination (only with some orders) --]]-- -- -- UPDATE MODELS -- standard is update all every time: fastest, but may cause -- performance issues -- queued will work one every pass (except for lazed), distributing the load much better -- schedueld installs a callback for each group separately and thus distributes the load over time much better cfxGroundTroops.queuedUpdates = false -- set to true to process one group per turn. To work this way, scheduledUpdates must be false cfxGroundTroops.scheduledUpdates = true -- set to false to allow queing of standard updates. overrides queuedUpdates cfxGroundTroops.monitorNumbers = false -- set to true to debug managed group size cfxGroundTroops.standardScheduleInterval = 30 -- 30 seconds between calls cfxGroundTroops.guardUpdateInterval = 30 -- every 30 seconds we check up on guards cfxGroundTroops.trackingUpdateInterval = 0.5 -- 0.5 seconds for lazer tracking etc cfxGroundTroops.maxManagedTroops = 67 -- -1 is infinite, any positive number turn on cap on managed troops and palces excess troops in queue cfxGroundTroops.troopQueue = {} -- FIFO stack -- return the best tracking interval for this type of orders -- -- READ CONFIG ZONE TO OVERRIDE SETTING -- function cfxGroundTroops.readConfigZone() -- note: must match exactly!!!! local theZone = cfxZones.getZoneByName("groundTroopsConfig") if not theZone then if cfxGroundTroops.verbose then trigger.action.outText("***gndT: NO config zone!", 30) end theZone = cfxZones.createSimpleZone("groundTroopsConfig") end -- ok, for each property, load it if it exists if cfxZones.hasProperty(theZone, "queuedUpdates") then cfxGroundTroops.queuedUpdates = cfxZones.getBoolFromZoneProperty(theZone, "queuedUpdates", false) end if cfxZones.hasProperty(theZone, "scheduledUpdates") then cfxGroundTroops.scheduledUpdates = cfxZones.getBoolFromZoneProperty(theZone, "scheduledUpdates", false) end if cfxZones.hasProperty(theZone, "maxManagedTroops") then cfxGroundTroops.maxManagedTroops = cfxZones.getNumberFromZoneProperty(theZone, "maxManagedTroops", 65) end if cfxZones.hasProperty(theZone, "monitorNumbers") then cfxGroundTroops.monitorNumbers = cfxZones.getBoolFromZoneProperty(theZone, "monitorNumbers", false) end if cfxZones.hasProperty(theZone, "standardScheduleInterval") then cfxGroundTroops.standardScheduleInterval = cfxZones.getNumberFromZoneProperty(theZone, "standardScheduleInterval", 30) end if cfxZones.hasProperty(theZone, "guardUpdateInterval") then cfxGroundTroops.guardUpdateInterval = cfxZones.getNumberFromZoneProperty(theZone, "guardUpdateInterval", 30) end if cfxZones.hasProperty(theZone, "trackingUpdateInterval") then cfxGroundTroops.trackingUpdateInterval = cfxZones.getNumberFromZoneProperty(theZone, "trackingUpdateInterval", 0.5) end if cfxZones.hasProperty(theZone, "verbose") then cfxGroundTroops.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) end if cfxGroundTroops.verbose then trigger.action.outText("+++gndT: read config zone!", 30) end end -- -- Callback handling -- cfxGroundTroops.troopsCallback = {} function cfxGroundTroops.addTroopsCallback(theCallback) table.insert(cfxGroundTroops.troopsCallback, theCallback) end function cfxGroundTroops.invokeCallbacksFor(reason, troops, data) if not data then data = {} end data.troops = troops for idx, theCB in pairs (cfxGroundTroops.troopsCallback) do theCB(reason, troops.group, troops.orders, data) end end function cfxGroundTroops.getScheduleInterval(orders) if orders == "laze" then return cfxGroundTroops.trackingUpdateInterval end return cfxGroundTroops.standardScheduleInterval end -- create controller commands to attack a group "enemies" -- enemies are an attribute of the troop structure -- usually called from a group on guard when idling function cfxGroundTroops.makeTroopsEngageEnemies(troop) local group = troop.group if not Group.isExist(group) then trigger.action.outText("+++gndT: troup don't exist, dropping", 30) return end local enemies = troop.enemy local from = dcsCommon.getGroupLocation(group) if not from then return end -- the commandos died local there = dcsCommon.getGroupLocation(enemies) if not there then return end -- we lerp to 2/3 of enemy location there = dcsCommon.vLerp(from, there, 0.66) local speed = 10 -- m/s = 10 km/h -- wait. 10 m/s is 36 km/h cfxCommander.makeGroupGoThere(group, there, speed) local attask = cfxCommander.createAttackGroupCommand(enemies) cfxCommander.scheduleTaskForGroup(group, attask, 0.5) troop.moving = true end -- make the troops engage a cfxZone passed in the destination -- attribute function cfxGroundTroops.makeTroopsEngageZone(troop) local group = troop.group if not group:isExist() then trigger.action.outText("+++gndT: make troops engage zone: troops do not exist, exiting", 30) return end local enemyZone = troop.destination -- must be cfxZone local from = dcsCommon.getGroupLocation(group) if not from then return end -- the group died local there = enemyZone.point -- access zone position if not there then return end local speed = 14 -- m/s; 10 m/s = 36 km/h -- make troops stop in 1 second, then start in 5 seconds to give AI respite cfxCommander.makeGroupHalt(group, 1) -- 1 second delay cfxCommander.makeGroupGoTherePreferringRoads(group, there, speed, 5) -- remember that we have issued a move order troop.moving = true end function cfxGroundTroops.switchToOffroad(troops) -- we may need to test if we already did this, -- but not for now -- this is called when troops are stuck -- on their route for longer than allowed -- we now force a direct approach local group = troops.group if not group:isExist() then return end local enemies = troops.destination local from = dcsCommon.getGroupLocation(group) if not from then return end -- the commandos died local there = enemies.point if not there then return end local speed = 14 -- m/s; 10 m/s = 36 km/h cfxCommander.makeGroupHalt(group, 0) -- no delay, halt now cfxCommander.makeGroupGoThere(group, there, speed, "Off Road", 5) troops.lastOrderDate = timer.getTime() troops.speedWarning = 0 end -- -- update loop for troops that have 'attackOwnedZones' as -- their orders -- if they have no destination zone, or the zone they are -- are heading for is already owned by their side, then look for -- the closest enemy zone, and cut attack orders to move there function cfxGroundTroops.getClosestEnemyZone(troop) local p = dcsCommon.getGroupLocation(troop.group) local tempZone = cfxZones.createSimpleZone("tz", p, 100) tempZone.owner = troop.side local newTarget = cfxOwnedZones.getNearestEnemyOwnedZone(tempZone, true) -- 'true' will also target neutral zones return newTarget end function cfxGroundTroops.updateZoneAttackers(troop) if not troop then return end if not cfxOwnedZones then trigger.action.outText("+++gndT: update zone attackers requires ownedZones", 30) return end troop.insideDestination = false -- mark as not inside local newTargetZone = cfxGroundTroops.getClosestEnemyZone(troop) if not newTargetZone then -- all target zones are friendly, go to guard mode troop.orders = "guard" return end if newTargetZone ~= troop.destination then troop.destination = newTargetZone cfxGroundTroops.makeTroopsEngageZone(troop) troop.lastOrderDate = timer.getTime() troop.speedWarning = 0 return end -- if we get here, we should be under way to our nearest enemy zone if not troop.moving then cfxGroundTroops.makeTroopsEngageZone(troop) return end -- if we get here, we are under way to troop.destination -- check if we are inside the zone, and if so, set variable to true local p = dcsCommon.getGroupLocation(troop.group) troop.insideDestination = cfxZones.isPointInsideZone(p, troop.destination) -- if we get here, we need no change end -- attackers simply travel to their destination (zone), and then switch to -- guard orders once they arrive function cfxGroundTroops.updateAttackers(troop) if not troop then return end if not troop.destination then return end if not troop.group:isExist() then return end -- if we are not moving, we need to issue move oders now -- this can happen if previously, there was a 'wait' command -- and this now was removed so we end up in the method if not troop.moving then cfxGroundTroops.makeTroopsEngageZone(troop) return end if cfxZones.isGroupPartiallyInZone(troop.group, troop.destination) then -- we have arrived -- we could now also initiate a general callback with reason cfxGroundTroops.invokeCallbacksFor("arrived", troop) troop.orders = "guard" return end -- if we get here, we need no change end -- update loop for a group that has "guard" orders. -- basically it stands around and looks for enemies -- until it finds a group, and then engages the enemy -- when engaged, it is not looking for other enemies -- 'engaged' means that the troop.enemy attribute is set function cfxGroundTroops.updateGuards(troop) if not troop.group:isExist() then return end local theEnemy = troop.enemy if theEnemy then -- see if enemy is dead if not dcsCommon.isGroupAlive(theEnemy) then troop.enemy = nil -- yup, zed's dead. next time around, we won't be checking this again trigger.action.outText(troop.name .. " has neutralized enemy forces", 30) --DONE: invoke callback for defeating troops local data = {} data.enemy = theEnemy cfxGroundTroops.invokeCallbacksFor("neutralized", troop, data) return end -- yes, we are still engaged return end -- we are currently unengaged. look for an enemy if not troop.range then troop.range = 300 end troop.coalition = troop.group:getCoalition() local enemyCoal = dcsCommon.getEnemyCoalitionFor(troop.coalition) local cat = Group.Category.GROUND local p = dcsCommon.getGroupLocation(troop.group) local enemies, enemyDist = dcsCommon.getClosestLivingGroupToPoint(p, enemyCoal, cat) local maxRange = troop.range -- meters -- if we have enemies then schedule a path to go there if enemies and (enemyDist < maxRange) then troop.enemy = enemies --timer.scheduleFunction(cfxGroundTroops.makeGroupEngageEnemies, troop, timer.getTime() + 1.0) cfxGroundTroops.makeTroopsEngageEnemies(troop) trigger.action.outText(troop.name .. " is engaging enemy forces at range " .. math.floor(enemyDist) .. "meters", 30) --DONE: invoke callback for engaging troops, pass data local data = {} data.enemy = enemies cfxGroundTroops.invokeCallbacksFor("engaging", troop, data) elseif enemies then --trigger.action.outText(troop.name .. " enemiy out of range: " .. math.floor(enemyDist) .. "meters", 30) else --trigger.action.outText(troop.name .. " no enemies", 30) end end -- -- update loop for units that laze targets. -- they can only laze if they are alive, but update -- will take care of that, so when we are here, there -- is at least one of them alive -- function cfxGroundTroops.findLazeTarget(troop) local here = troop.group:getUnit(1):getPoint() troop.coalition = troop.group:getCoalition() local enemyCoal = dcsCommon.getEnemyCoalitionFor(troop.coalition) --local enemySide = dcsCommon.getEnemyCoalitionFor(troop.side) local cat = Group.Category.GROUND local enemyGroups = dcsCommon.getLivingGroupsAndDistInRangeToPoint(here, troop.range, enemyCoal, cat) -- we now have a list of possible targets in range if #enemyGroups < 1 then -- no targets in range return nil end here = {x = here.x, y = here.y + 2.0, z = here.z} -- raise by 2.0m -- iterate through the list until we find the first target -- that fits the bill and return it -- trigger.action.outText("+++ looking at " .. #enemyGroups .. " laze groups", 30) for i=1, #enemyGroups do -- get all units for this group local aGroup = enemyGroups[i].group -- remember, they are in a {dist, group} tuple local theUnits = aGroup:getUnits() -- iterate all units for udx, aUnit in pairs(theUnits) do if (aUnit:isExist() and aUnit:getLife() > 1) then -- unit lives -- now, we need to filter infantry. we do this by -- pre-fetching the typeString --troop.lazeTargetType = aUnit:getTypeName() -- and checking if the name contains some infantry- -- typical strings. Idea taken from JTAC script local isInfantry = dcsCommon.unitIsInfantry(theUnit) if not isInfantry then -- this is a vehicle, is it in line of sight? -- raise the point 2m above ground for both points -- as done in jtac script local there = aUnit:getPoint() there = {x = there.x, y = there.y + 2.0, z = there.z} if land.isVisible(here, there) then -- we found a visible vehicle in -- the nearest group to us in range -- that is visible! return aUnit else --trigger.action.outText("+++ ".. aUnit:getName() .."cant be seen", 30) end -- if visible else -- trigger.action.outText("+++ ".. aUnit:getName() .." (".. troop.lazeTargetType .. ") is infantry", 30) end -- if not infantry end -- if alive end -- for all units end -- for all enemy groups --trigger.action.outText("+++ find nearest laze target did not find anything to laze", 30) return nil -- no unit found end function cfxGroundTroops.lazerOff(troop) if troop.lazerPointer then troop.lazerPointer:destroy() end troop.lazerPointer = nil troop.lazingUnit = nil end function cfxGroundTroops.trackLazer(troop) -- the only thing that must be set when entering here is -- lazeTarget. We set up the rest if not troop.lazingUnit then troop.lazingUnit = troop.group:getUnit(1) -- get first unit if troop.lazingUnit:getLife() < 1 then trigger.action.outText("+++ LazingUnit is dead, getUnit works differently from what docs say, need to filter for lively units", 30) end end if not troop.lazerPointer then local there = troop.lazeTarget:getPoint() troop.lazerPointer = Spot.createLaser(troop.lazingUnit,{x = 0, y = 2, z = 0}, there, 1688) troop.lazeTargetType = troop.lazeTarget:getTypeName() trigger.action.outTextForCoalition(troop.side, troop.name .. " tally target - lasing " .. troop.lazeTargetType .. "!", 30) trigger.action.outSoundForCoalition(troop.side, "UI_SCI-FI_Tone_Bright_Dry_20_stereo.wav") troop.lastLazerSpot = there -- remember last spot local data = {} data.enemy = troop.lazeTarget data.tracker = troop.lazingUnit cfxGroundTroops.invokeCallbacksFor("lase:tracking", troop, data) return end -- if true then return end -- if we get here, we update the lazerPointer local there = troop.lazeTarget:getPoint() -- we may only want to update the laser spot when dist > trigger troop.lazerPointer:setPoint(there) -- we may want to report dist troop.lastLazerSpot = there end function cfxGroundTroops.updateLaze(troop) -- check if we have a laze target. -- check if lazing unit was killed, and therefore lost target if troop.lazingUnit then -- check that unit still alive if troop.lazingUnit:isExist() and troop.lazingUnit:getLife() >= 1 then else cfxGroundTroops.lazerOff(troop) troop.lazeTarget = nil trigger.action.outTextForCoalition(troop.side, troop.name .. " reports lasing " .. troop.lazeTargetType .. " interrupted. Re-acquiring.", 30) trigger.action.outSoundForCoalition(troop.side, "UI_SCI-FI_Tone_Bright_Dry_20_stereo.wav") troop.lazingUnit = nil cfxGroundTroops.invokeCallbacksFor("lase:stop", troop) return -- we'll re-acquire through a new unit next round end end -- if we get here, a lazing unit --local here = troop.lazingUnit:getPoint() if troop.lazeTarget then -- check if that target is alive and in range if troop.lazeTarget:isExist() and troop.lazeTarget:getLife() >= 1 then -- note: when we laze a target, we know that we have a lazing unit local here = troop.lazingUnit:getPoint() -- check if it has moved out of range local there = troop.lazeTarget:getPoint() if dcsCommon.dist(here, there) > troop.range then -- troop out of range trigger.action.outTextForCoalition(troop.side, troop.name .. " lost sight of lazed target " .. troop.lazeTargetType, 30) trigger.action.outSoundForCoalition(troop.side, "UI_SCI-FI_Tone_Bright_Dry_20_stereo.wav") troop.lazeTarget = nil cfxGroundTroops.lazerOff(troop) troop.lazingUnit = nil cfxGroundTroops.invokeCallbacksFor("lase:stop", troop) return end -- if we get here, we need to update the target point cfxGroundTroops.trackLazer(troop) return else -- target died trigger.action.outTextForCoalition(troop.side, troop.name .. " confirms kill for " .. troop.lazeTargetType, 30) trigger.action.outSoundForCoalition(troop.side, "UI_SCI-FI_Tone_Bright_Dry_20_stereo.wav") troop.lazeTarget = nil cfxGroundTroops.lazerOff(troop) troop.lazingUnit = nil cfxGroundTroops.invokeCallbacksFor("lase:stop", troop) return end end -- if we get here, we must look for a laze target troop.lazeTarget = cfxGroundTroops.findLazeTarget(troop) if troop.lazeTarget then cfxGroundTroops.trackLazer(troop) -- will also set up lazing unit end end function cfxGroundTroops.updateWait(troop) -- currently nothing to do end function cfxGroundTroops.updateTroops(troop) -- if orders start with "wait-" then the troops -- simply do nothing if dcsCommon.stringStartsWith(troop.orders, "wait-") then -- the troops are waiting to be picked update -- when they are dropped again, thre prefix to -- their order is removed, and the 'real' orders -- are revealed. For now, do nothing cfxGroundTroops.updateWait(troop) --REMEMBER: LOWER CASE ONLY! elseif troop.orders == "guard" then cfxGroundTroops.updateGuards(troop) elseif troop.orders == "attackownedzone" then cfxGroundTroops.updateZoneAttackers(troop) elseif troop.orders == "laze" then cfxGroundTroops.updateLaze(troop) elseif troop.orders == "attackzone" then cfxGroundTroops.updateAttackers(troop) else trigger.action.outText("+++ updated troops " .. troop.name .. " have unknown orders " .. troop.orders, 30) end end -- -- we have to systems to process during update: -- once all, and one per turn, with the exception -- of lazers, who get updated every turn -- -- -- all at once -- function cfxGroundTroops.update() cfxGroundTroops.updateSchedule = timer.scheduleFunction(cfxGroundTroops.update, {}, timer.getTime() + 1/cfxGroundTroops.ups) -- iterate all my troops and build next -- versions pool local liveTroops = {} -- filtered table, indexed by name for idx, troop in pairs(cfxGroundTroops.deployedTroops) do local group = troop.group if not dcsCommon.isGroupAlive(group) then -- group dead. remove from pool -- this happens by not copying it into the poos cfxGroundTroops.invokeCallbacksFor("dead", troop) -- notify anyone who is interested that we are no longer proccing these else -- work with this groop according to its orders cfxGroundTroops.updateTroops(troop) -- since group is alive remember it for next loop liveTroops[idx] = troop -- do NOT use insert as we have indexed table by name end end -- liveTroops holds all troops that are still alive and will -- be revisited next loop cfxGroundTroops.deployedTroops = liveTroops end -- -- UpdateQueued looks for the first unordered (.receivedOrders == false) group -- and processes them. if orders are 'laze', it will always be ordered -- function cfxGroundTroops.updateQueued() cfxGroundTroops.updateSchedule = timer.scheduleFunction(cfxGroundTroops.updateQueued, {}, timer.getTime() + 1/cfxGroundTroops.ups) -- iterate all my troops and build next -- versions pool local liveTroops = {} local hasOrdered = false -- so far, no orders have been given for idx, troop in pairs(cfxGroundTroops.deployedTroops) do local group = troop.group if not dcsCommon.isGroupAlive(group) then -- group dead. remove from pool -- this happens by not copying it to liveTroops -- trigger.action.outText("+++ removing ground troops " .. troop.name, 30) cfxGroundTroops.invokeCallbacksFor("dead", troop) -- notify anyone who is interested that we are no longer proccing these else -- check if this is a lazer if troop.orders == "laze" then -- lazers are updated each turn cfxGroundTroops.updateLaze(troop) else if not hasOrdered and not (troop.receivedOrders) then -- work with this groop according to its orders cfxGroundTroops.updateTroops(troop) troop.receivedOrders = true -- this one has received orders hasOrdered = true end end liveTroops[idx] = troop -- do NOT use insert as we have indexed table end end -- liveTroops holds all troops that are still alive and will -- be revisited next loop cfxGroundTroops.deployedTroops = liveTroops -- if no orders have been passed, clear all troop's .receivedOrders flag -- and the loop starts anew next loop if not hasOrdered then for idx, troop in pairs(cfxGroundTroops.deployedTroops) do troop.receivedOrders = nil end end end -- -- in updateCheckOnly we simply check the ground queue -- if there are troops added that need scheduling (i.e. have -- been passed in by addTroops and schedule them -- function cfxGroundTroops.updateCheckOnly() -- re-schedule myself in 1 second timer.scheduleFunction(cfxGroundTroops.updateCheckOnly, {}, timer.getTime() + 1) -- iterate through all troops, and -- see if there are any that have not been scheduled -- to schedule them for updates in 1 second -- that will be the first time that they are scheduled, -- all others will be self-scheduled for idx, troop in pairs(cfxGroundTroops.deployedTroops) do if not troop.hasBeenScheduled then local params = {troop} troop.hasBeenScheduled = true troop.updateID = timer.scheduleFunction(cfxGroundTroops.updateSingleScheduled, params, timer.getTime() + 1) --trigger.action.outText("+++groundT: scheduling troops <".. troop.group:getName() .."> with orders <" .. troop.orders .. ">", 30) end end -- note that alive checks are now done during the scheduled -- update, not every time for all end function cfxGroundTroops.updateSingleScheduled(params) local troops = params[1] troops.updateID = nil -- erase update id if not troops then trigger.action.outText("+++groundT WARNING: nil troop in updateSingle", 30) return -- no further action required, no longer updates end local group = troops.group -- see if we have been taken out of the pool or updated -- if so, exit if not group:isExist() then -- simply never again look at it. return end if cfxGroundTroops.deployedTroops[troops.group:getName()] ~= troops then -- trigger.action.outText("+++groundT NOTE: troops <".. troops.group:getName() .."> was removed from pool. Cancel Update", 30) return -- no further reschedule end -- see if scheduling is turned off if not troops.reschedule then trigger.action.outText("+++groundT NOTE: no longer updating <".. troops.group:getName() .."> per reschedule param", 30) return end -- now, check if still alive if not dcsCommon.isGroupAlive(group) then -- group dead, no longer updates --trigger.action.outText("+++groundT NOTE: <".. troops.group:getName() .."> dead, removing", 30) cfxGroundTroops.invokeCallbacksFor("dead", troops) -- notify anyone who is interested that we are no longer proccing these cfxGroundTroops.removeTroopsFromPool(troops) return -- nothing else to do end -- now, execute the update itself, standard update --trigger.action.outText("+++groundT: singleU troop <".. troops.group:getName() .."> with orders <" .. troops.orders .. ">", 30) cfxGroundTroops.updateTroops(troops) -- check max speed of group. if < 0.1 then note and increase -- speedWarning. if not, reset speed warning if troops.orders == "attackownedzone" and dcsCommon.getGroupMaxSpeed(troops.group) < 0.1 then if not troops.speedWarning then troops.speedWarning = 0 end troops.speedWarning = troops.speedWarning + 1 else troops.speedWarning = 0 -- reset end if troops.speedWarning > 5 then -- make me 5 lastOrder = timer.getTime() - troops.lastOrderDate --trigger.action.outText("+++groundT WARNING: <".. troops.group:getName() .."> (S:".. troops.side .. ") to " .. troops.destination.name .. ": stopped for " .. troops.speedWarning .. " iters, orderage=" .. lastOrder, 30) -- this may be a matter of too many waypoints. -- maybe issue orders to go to their destination directly? -- now force an order to go directly. if troops.speedWarning > 5 then if troops.isOffroad then -- we already switched to off-road. take me -- out of the managed queue, I'm not going -- anywhere -- trigger.action.outText("+++groundT <".. troops.group:getName() .."> is going nowhere. Removed from managed troops", 30) cfxGroundTroops.removeTroopsFromPool(troops) else cfxGroundTroops.switchToOffroad(troops) -- trigger.action.outText("+++groundT <".. troops.group:getName() .."> SWITCHED TO OFFROAD", 30) troops.isOffroad = true -- so we know that we already did that end end end -- now reschedule updte for my best time local updateTime = cfxGroundTroops.getScheduleInterval(troops.orders) troops.updateID = timer.scheduleFunction(cfxGroundTroops.updateSingleScheduled, params, timer.getTime() + updateTime) end -- -- PILEUP and TIE BRAKERS -- -- there may come a situation where troops gather in -- one zone because the zone isn't won - some other troops -- are there and noone moves. -- a tie-break is required -- -- checkpile up: every so often, we test if we have run into a -- pileup-situation. this happens if there are more than n -- units with group-attacker order in the same zone, and that -- zone is their destination -- this can be easily detected by the insideDestination flag -- checkPileUp should be run every minute or so function cfxGroundTroops.checkPileUp() -- schedule my next call --trigger.action.outText("+++groundT: pileup check", 30) timer.scheduleFunction(cfxGroundTroops.checkPileUp, {}, timer.getTime() + 60) local thePiles = {} if not cfxOwnedZones then -- trigger.action.outText("+++groundT: pileUp - owned zones not yet ready", 30) return end -- create a list of all piles for idx, oz in pairs(cfxOwnedZones.zones) do local newPile = {} newPile[1] = 0 -- no red inZone here newPile[2] = 0 -- no blue inZone here newPile.zone = oz -- the zone we are looking at thePiles[oz] = newPile end -- now iterate through all currently alive groups and -- attribute them to their piles for idx, troop in pairs(cfxGroundTroops.deployedTroops) do -- get each group and count them if they are inside -- their destination if troop.insideDestination and troop.group:isExist() then local side = troop.group:getCoalition() local thePile = thePiles[troop.destination] local theSide = troop.group:getCoalition() thePile[theSide] = thePile[theSide] + 1 -- we count groups, not units end end -- a pileup happens, if there are more than 3 groups in destination zone -- with NO other troops present (usually the case) -- or when there are 5 groups more than the number for the other side -- so now scan all piles for idx, thePile in pairs(thePiles) do -- check red pileup if thePile[1] > 3 and thePile[2] == 0 then -- simple pileup. 3 groups, no others except defenders and -- perhaps transients cfxGroundTroops.breakTie(thePile, 1) elseif thePile[1] >= thePile[2] + 5 then -- numerical pileup cfxGroundTroops.breakTie(thePile, 1) end -- check blue loside if thePile[2] >= 3 and thePile[1] == 0 then -- simple pileup. 3 groups, no others except defenders and -- perhaps transients cfxGroundTroops.breakTie(thePile, 2) elseif thePile[2] >= thePile[1] + 5 then -- numerical pileup cfxGroundTroops.breakTie(thePile, 2) end end end function cfxGroundTroops.breakTie(thePile, winner) trigger.action.outText("+++ groundT: TIEBREAK - winner is " .. winner .. " in zone " .. thePile.zone.name .. ": " .. thePile[1] .. ":" .. thePile[2] , 30) -- now add some code to do the actual tie breaking: remove all units that -- are inside the zone and who belong to the other side local loser = 1 -- red default local theZone = thePile.zone if winner == 1 then loser = 2 end -- now get all ground groups for the losing side local losingGround = coalition.getGroups(loser, Group.Category.GROUND) for idx, theGroup in pairs(losingGround) do -- if alive, check if inside the zone if theGroup:isExist() and dcsCommon.isGroupAlive(theGroup) then -- make sure it's not a transient if not isDeployedGroundTroop(theGroup) then local p = dcsCommon.getGroupLocation(theGroup) if cfxZones.isPointInsideZone(p, theZone) then trigger.action.outText("+++ groundT: TIEBREAK - destroying group " .. theGroup:getName() , 30) -- we delete this group now theGroup:destroy() end end end end end -- -- sanity checks for rescheduling -- function cfxGroundTroops.checkSchedules() timer.scheduleFunction(cfxGroundTroops.checkSchedules, {}, timer.getTime() + 10) for idx, troop in pairs(cfxGroundTroops.deployedTroops) do -- check if troop is not scheduled -- if this happens to a group more than a certain times, -- it has somehow dropped out of the reschedule -- plan and needs to be scheduled if troop.updateID == nil then troop.unscheduleCount = troop.unscheduleCount + 1 if (troop.unscheduleCount > 1) and troop.group:isExist() then trigger.action.outText("+++ groundT: unscheduled group " .. troop.group:getName() .. " cnt=" .. troop.unscheduleCount , 30) end end end end -- -- REPORTING -- -- -- get a report of troops as string -- function cfxGroundTroops.getTroopReport(theSide, ignoreInfantry) if not ignoreInfantry then ignoreInfantry = false end local report = "GROUND FORCES REPORT" for idx, troop in pairs(cfxGroundTroops.deployedTroops) do if troop.side == theSide and troop.group:isExist() then local unitNum = troop.group:getSize() report = report .. "\n" .. troop.name .. " (".. unitNum .."): <" .. troop.orders .. ">" if troop.orders == "attackownedzone" then if troop.destination then report = report .. " move towards " .. troop.destination.name else report = report .. " (selecting destination)" end end end end report = report .. "\n---END REPORT\n" return report end -- -- CREATE / ADD / REMOVE -- -- -- createGroundTroop -- use this to create a cfxGroundTroops from a dcs group -- function cfxGroundTroops.createGroundTroops(inGroup, range, orders) local newTroops = {} if not orders then orders = "guard" end if orders:lower() == "lase" then orders = "laze" -- we use WRONG spelling here, cause we're cool. yeah, right. end newTroops.insideDestination = false newTroops.unscheduleCount = 0 -- will count up as we aren't scheduled newTroops.speedWarning = 0 newTroops.isOffroad = false -- if true, we switched to direct orders, not roads, after standstill newTroops.group = inGroup newTroops.orders = orders:lower() newTroops.coalition = inGroup:getCoalition() newTroops.side = newTroops.coalition -- because we'e been using both. newTroops.name = inGroup:getName() newTroops.moving = false -- set to not have received move orders yet newTroops.signature = "cfx" -- to verify this is groundTroop group, not dcs groups if not range then range = 300 end newTroops.range = range return newTroops end function cfxGroundTroops.addGroundTroopsToPool(troops) -- troops MUST be a table that I understand, with if not troops then return end if troops.signature ~= "cfx" then trigger.action.outText("+++ adding ground troops with unsupported troop signature", 30) return end if not troops.orders then troops.orders = "guard" end troops.orders = troops.orders:lower() troops.reschedule = true -- in case we use scheduled update -- we now add to internal array. this is worked on by all -- update meths, on scheduled upadtes, it is only used to -- pick up, and do the initial schedule, after that they -- all re-schedule themselves troops.hasBeenScheduled = false -- so far, no updates -- hasBeenScheduled is used by updateCheckOnly when scheduled -- updates are used. -- now add to actively managed table or queue it if enabled if cfxGroundTroops.maxManagedTroops > 0 and dcsCommon.getSizeOfTable(cfxGroundTroops.deployedTroops) >= cfxGroundTroops.maxManagedTroops then -- we need to queue table.insert(cfxGroundTroops.troopQueue, troops) else -- add to deployed set cfxGroundTroops.deployedTroops[troops.group:getName()] = troops end end function cfxGroundTroops.removeTroopsFromPool(troops) if not troops then return end if troops.signature ~= "cfx" then return end if not troops.group:isExist() then trigger.action.outText("warning: removeFromPool called with inexistant group", 30) return end if cfxGroundTroops.deployedTroops[troops.group:getName()] then local troop = cfxGroundTroops.deployedTroops[troops.group:getName()] troops.reschedule = false -- so a reschedule wont update any more cfxGroundTroops.deployedTroops[troops.group:getName()] = nil return end -- if we get here, we need to check if perhaps the troops -- are in the queue for i=1, #cfxGroundTroops.troopQueue do if cfxGroundTroops.troopQueue[i] == troops then table.remove(cfxGroundTroops.troopQueue, i) return end end end function isDeployedGroundTroop(aGroup) if not aGroup then return false end -- see if its already managed if cfxGroundTroops.deployedTroops[aGroup:getName()] ~= nil then return true end -- see if it's in the queue for i=1, #cfxGroundTroops.troopQueue do if cfxGroundTroops.troopQueue[i] == troops then return true end end -- if we get here, it's neither managed nor queued return false -- return cfxGroundTroops.deployedTroops[aGroup:getName()] ~= nil end function cfxGroundTroops.getGroundTroopsForGroup(aGroup) if not (cfxGroundTroops.deployedTroops[aGroup:getName()]) then -- see if it's queued for i=1, #cfxGroundTroops.troopQueue do local troops = cfxGroundTroops.troopQueue[i] if troops.group == aGroup then return troops end end if cfxGroundTroops.verbose then trigger.action.outText("+++gndT - WARNING: cannot find group " .. aGroup:getName() .. " for troop retrieval. Known troops are:", 30) end for k,v in pairs(cfxGroundTroops.deployedTroops) do trigger.action.outText("+++ ".. k .. ": has v: " .. v.name, 30) end return nil end return cfxGroundTroops.deployedTroops[aGroup:getName()] end function cfxGroundTroops.monitorQueues() timer.scheduleFunction(cfxGroundTroops.monitorQueues, {}, timer.getTime() + 5) -- calculate the numbers local num = dcsCommon.getSizeOfTable(cfxGroundTroops.deployedTroops) local msg = "+++ gndT - Groups Managed: <" .. num .. ">" -- display the numbers if cfxGroundTroops.maxManagedTroops > 0 then msg = msg .. " capped at " .. cfxGroundTroops.maxManagedTroops .. ", q size is <" .. #cfxGroundTroops.troopQueue .. ">" end trigger.action.outText(msg, 30) end -- manageQueue: if depth of deployedTroops is below max and we have -- items in queue, pop off first one and put in managed table -- checked once every 2 seconds function cfxGroundTroops.manageQueues() timer.scheduleFunction(cfxGroundTroops.manageQueues, {}, timer.getTime() + 2) if cfxGroundTroops.maxManagedTroops < 1 then return end -- if we get here, we have a limit on managed -- items if #cfxGroundTroops.troopQueue < 1 then return end -- if we here, there are items waiting in the queue while dcsCommon.getSizeOfTable(cfxGroundTroops.deployedTroops) < cfxGroundTroops.maxManagedTroops and #cfxGroundTroops.troopQueue > 0 do -- trnasfer items from the front to the managed queue local theTroops = cfxGroundTroops.troopQueue[1] table.remove(cfxGroundTroops.troopQueue, 1) if theTroops.group:isExist() then cfxGroundTroops.deployedTroops[theTroops.group:getName()] = theTroops end -- trigger.action.outText("+++gT: dequed and activaed " .. theTroops.group:getName(), 30) end end function cfxGroundTroops.start() if not dcsCommon.libCheck("cfx Ground Troops", cfxGroundTroops.requiredLibs) then trigger.action.outText("cf/x Ground Troops aborted: missing libraries", 30) return false end -- read optional config zone cfxGroundTroops.readConfigZone() if cfxGroundTroops.scheduledUpdates then cfxGroundTroops.queuedUpdates = false cfxGroundTroops.updateCheckOnly() cfxGroundTroops.checkSchedules() -- check regularly if all troops have been updated by checking their ID elseif cfxGroundTroops.queuedUpdates then cfxGroundTroops.updateQueued() else cfxGroundTroops.update() end -- now install a regular pileup check timer.scheduleFunction(cfxGroundTroops.checkPileUp, {}, timer.getTime() + 60) if cfxGroundTroops.monitorNumbers then timer.scheduleFunction(cfxGroundTroops.monitorQueues, {}, timer.getTime() + 5) end if cfxGroundTroops.maxManagedTroops > 0 then timer.scheduleFunction(cfxGroundTroops.manageQueues, {}, timer.getTime() + 1) end trigger.action.outText("cf/x Ground Troops v" .. cfxGroundTroops.version .. " started", 30) if not cfxOwnedZones then --trigger.action.outText("+++groundT: pileUp - owned zones not yet ready", 30) end return true end if not cfxGroundTroops.start() then cfxGroundTroops = nil trigger.action.outText("cfxGroundTroops aborted load", 30) end --[[-- TO DO - implement 'patrol' orders!!! when ordering a new route, issue a command to stop in 1 second and another with new marching orders in 5 seconds look at setTask() and resetTask() for controller - change group logic to set itself up to 'requestOrders' with group as parameter, so they can decide themselves how quickly they want to be re-tasked - DONE enqueue and dequeue methods with capped ground troops size - named locs have strategic values attached (default = 1), and distance is divided by strat value to get at priority when rerouting - difficulty increase: make enemy troops better by raining their spawned level - check out simple slot block SSB (pre-moose) to see if we can implement slot blocking for downed pilots - new 'wanda' (wander) module to make airports more lively: zone, have individuals/single vehicle wander around. two waypoints (start and stop), that are zones, and whenever they reach one or are at speed 0, they get a new one. may have pause before they go to next. variant on above: selection of zones that are somehow connected, and destinations are made between these for patrolling zone. can force order, loop, and ping-pong. --]]--