diff --git a/Doc/DML Documentation.pdf b/Doc/DML Documentation.pdf index f4a26d2..fb0cef7 100644 Binary files a/Doc/DML Documentation.pdf and b/Doc/DML Documentation.pdf differ diff --git a/Doc/DML Quick Reference.pdf b/Doc/DML Quick Reference.pdf index 32b40ae..ed69d73 100644 Binary files a/Doc/DML Quick Reference.pdf and b/Doc/DML Quick Reference.pdf differ diff --git a/modules/FARPZones.lua b/modules/FARPZones.lua index 99f4f08..05e348a 100644 --- a/modules/FARPZones.lua +++ b/modules/FARPZones.lua @@ -1,5 +1,5 @@ FARPZones = {} -FARPZones.version = "2.1.0" +FARPZones.version = "2.1.1" FARPZones.verbose = false --[[-- Version History @@ -24,7 +24,7 @@ FARPZones.verbose = false 2.0.2 - clean-up verbosity enhancements 2.1.0 - integration with camp: needs repairs, produceResourceVehicles() - + 2.1.1 - loading a farp from data respaws all defenders and resource vehicles --]]-- @@ -547,6 +547,8 @@ function FARPZones.loadMission() local theAB = theFARP.mainFarp theAB:setCoalition(theFARP.owner) -- FARP is in lockup. theFARP.defenderData = dcsCommon.clone(fData.defenderData) + + --[[-- local groupData = fData.defenderData if groupData and #groupData.units > 0 then local cty = groupData.cty @@ -560,11 +562,13 @@ function FARPZones.loadMission() local cat = groupData.cat theFARP.resources = coalition.addGroup(cty, cat, groupData) end + --]]-- + FARPZones.produceVehicles(theFARP) -- do full defender and resource cycle FARPZones.drawFARPCircleInMap(theFARP) -- mark in map - if (not theFARP.defenders) and (not theFARP.resources) then +-- if (not theFARP.defenders) and (not theFARP.resources) then -- we instigate a resource and defender drop - FARPZones.produceVehicles(theFARP) - end +-- FARPZones.produceVehicles(theFARP) +-- end else trigger.action.outText("frpZ: persistence: FARP <" .. fName .. "> no longer exists in mission, skipping", 30) end diff --git a/modules/SSBClient.lua b/modules/SSBClient.lua index 9610667..9d14230 100644 --- a/modules/SSBClient.lua +++ b/modules/SSBClient.lua @@ -1,5 +1,5 @@ cfxSSBClient = {} -cfxSSBClient.version = "4.0.0" +cfxSSBClient.version = "4.0.1" cfxSSBClient.verbose = false cfxSSBClient.singleUse = false -- set to true to block crashed planes -- NOTE: singleUse (true) requires SSB to disable immediate respawn after kick @@ -16,6 +16,8 @@ cfxSSBClient.requiredLibs = { Version History 4.0.0 - dmlZones - cfxMX instead of cfxGroups + 4.0.1 - check slot availability immediately upon start + - ssb autoenable option --]]-- cfxSSBClient.enabledFlagValue = 0 -- DO NOT CHANGE, MUST MATCH SSB @@ -490,33 +492,31 @@ function cfxSSBClient.readConfigZone() -- note: must match exactly!!!! local theZone = cfxZones.getZoneByName("SSBClientConfig") if not theZone then - trigger.action.outText("+++SSBC: no config zone!", 30) - return - end - - trigger.action.outText("+++SSBC: found config zone!", 30) - - cfxSSBClient.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) + theZone = cfxZones.createSimpleZone("SSBClientConfig") + end + cfxSSBClient.verbose = theZone.verbose -- single-use - cfxSSBClient.singleUse = cfxZones.getBoolFromZoneProperty(theZone, "singleUse", false) -- use airframes only once? respawn after kick must be disabled in ssb - cfxSSBClient.reUseAfter = cfxZones.getNumberFromZoneProperty(theZone, "reUseAfter", -1) + cfxSSBClient.singleUse = theZone:getBoolFromZoneProperty("singleUse", false) -- use airframes only once? respawn after kick must be disabled in ssb + cfxSSBClient.reUseAfter = theZone:getNumberFromZoneProperty( "reUseAfter", -1) -- airfield availability - cfxSSBClient.allowNeutralFields = cfxZones.getBoolFromZoneProperty(theZone, "allowNeutralFields", false) + cfxSSBClient.allowNeutralFields = theZone:getBoolFromZoneProperty( "allowNeutralFields", false) - cfxSSBClient.maxAirfieldRange = cfxZones.getNumberFromZoneProperty(theZone, "maxAirfieldRange", 3000) -- meters, to find attached airfield + cfxSSBClient.maxAirfieldRange = theZone:getNumberFromZoneProperty("maxAirfieldRange", 3000) -- meters, to find attached airfield -- optimization - cfxSSBClient.keepInAirGroups = cfxZones.getBoolFromZoneProperty(theZone, "keepInAirGroups", false) + cfxSSBClient.keepInAirGroups = theZone:getBoolFromZoneProperty("keepInAirGroups", false) -- SSB direct control. -- USE ONLY WHEN YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING - cfxSSBClient.enabledFlagValue = cfxZones.getNumberFromZoneProperty(theZone, "enabledFlagValue", 0) + cfxSSBClient.enabledFlagValue = theZone:getNumberFromZoneProperty("enabledFlagValue", 0) - cfxSSBClient.disabledFlagValue = cfxZones.getNumberFromZoneProperty(theZone, "disabledFlagValue", cfxSSBClient.enabledFlagValue + 100) + cfxSSBClient.disabledFlagValue = theZone:getNumberFromZoneProperty("disabledFlagValue", cfxSSBClient.enabledFlagValue + 100) + + cfxSSBClient.ssbAutoenable = theZone:getBoolFromZoneProperty("ssbAutoenable", true) end -- @@ -601,13 +601,18 @@ function cfxSSBClient.start() -- install a timed update just to make sure -- and start NOW - timer.scheduleFunction(cfxSSBClient.update, {}, timer.getTime() + 1) + -- timer.scheduleFunction(cfxSSBClient.update, {}, timer.getTime() + 1) + cfxSSBClient.update() -- start dml update (on a different timer cfxSSBClient.dmlUpdate() -- now turn on ssb - trigger.action.setUserFlag("SSB",100) + if cfxSSBClient.ssbAutoenable then + trigger.action.setUserFlag("SSB",100) + else + trigger.action.outText("WARNING: cfxSSBClient did !!NOT!! auto-enable SSB for mission.", 30) + end -- persistence: load states if persistence then diff --git a/modules/bombRange.lua b/modules/bombRange.lua index f7729d4..8c6815b 100644 --- a/modules/bombRange.lua +++ b/modules/bombRange.lua @@ -1,5 +1,5 @@ bombRange = {} -bombRange.version = "1.1.2" +bombRange.version = "1.1.3" bombRange.dh = 1 -- meters above ground level burst bombRange.requiredLibs = { @@ -21,6 +21,7 @@ VERSION HISTORY 1.1.1 - fixed reading smoke color for zone minor clean-up 1.1.2 - corrected bug when no bomb range is detected +1.1.3 - added meters/feet distance when reporting impact --]]-- bombRange.bombs = {} -- live tracking @@ -543,7 +544,8 @@ function bombRange.impacted(weapon, target, finalPass) tDist = math.floor(tDist*100) /100 trigger.action.outTextForGroup(weapon.gID, "impact of " .. weapon.type .. " released by " .. weapon.pName .. " from " .. weapon.uType .. " after traveling " .. tDist .. " km in " .. t .. " sec, impact velocity at impact is " .. v .. " m/s!", 30) end - + local meters = math.floor(minDist * 10) / 10 + local feet = math.floor(minDist * 3.28084 * 10) / 10 local msg = "" if impactInside then local percentage = 0 @@ -553,15 +555,17 @@ function bombRange.impacted(weapon, target, finalPass) percentage = 1 - (minDist / theRange.radius) percentage = math.floor(percentage * 100) end + + msg = "INSIDE target area" if theRange.reportName then msg = msg .. " " .. theRange.name end - if (not targetName) and theRange.details then msg = msg .. ", off-center by " .. math.floor(minDist *10)/10 .. " m" end + if (not targetName) and theRange.details then msg = msg .. ", off-center by " .. meters .. "m/" .. feet .. "ft" end--math.floor(minDist *10)/10 .. " m" end if targetName then msg = msg .. ", hit on " .. targetName end if not theRange.usePercentage then percentage = 100 else - msg = msg .. " (Quality " .. percentage .."%)" + msg = msg .. " (Quality " .. percentage .."%)" --, off-center by " .. meters .. "m/" .. feet .. "ft)" end if theRange.hitOut then @@ -572,7 +576,7 @@ function bombRange.impacted(weapon, target, finalPass) else msg = "Outside target area" if theRange.reportName then msg = msg .. " " .. theRange.name end - if theRange.details then msg = msg .. " (off-center by " .. math.floor(minDist *10)/10 .. " m)" end + if theRange.details then msg = msg .. " (off-center by " .. meters .. "m/" .. feet .. "ft)" end --math.floor(minDist *10)/10 .. " m)" end msg = msg .. ", no hit." bombRange.addImpactForWeapon(weapon, false, 0) end diff --git a/modules/commander.lua b/modules/commander.lua index f36c254..715793c 100644 --- a/modules/commander.lua +++ b/modules/commander.lua @@ -4,7 +4,7 @@ -- *** EXTENDS ZONES: 'pathing' attribute -- cfxCommander = {} -cfxCommander.version = "1.1.4" +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 @@ -30,7 +30,11 @@ cfxCommander.version = "1.1.4" - 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 = { @@ -72,16 +76,11 @@ function cfxCommander.readConfigZone() -- note: must match exactly!!!! local theZone = cfxZones.getZoneByName("CommanderConfig") if not theZone then - trigger.action.outText("+++cmdr: no config zone!", 30) - return + theZone = cfxZones.createSimpleZone("CommanderConfig") end - - trigger.action.outText("+++cmdr: found config zone!", 30) - - cfxCommander.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) - cfxCommander.forceOffRoad = cfxZones.getBoolFromZoneProperty(theZone, "forceOffRoad", false) -- if true, vehicles path follow roads, but may drive offroad - cfxCommander.noRoadsAtAll = cfxZones.getBoolFromZoneProperty(theZone, "noRoadsAtAll", false) - + 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 -- @@ -118,8 +117,14 @@ function cfxCommander.performCommands(commandData) 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) @@ -204,7 +209,6 @@ function cfxCommander.doScheduledTask(data) local theGroup = data.group if not theGroup then return end if not Group.isExist(theGroup) then return end --- if not theGroup.isExist then return end local theController = theGroup:getController() theController:pushTask(data.task) @@ -262,7 +266,7 @@ function cfxCommander.createBasicWaypoint(point, speed, formation) if not formation then formation = "Off Road" end -- legal formations: - -- Off road + -- Off Road -- On Road -- second letter upper case? -- Cone -- Rank @@ -300,21 +304,22 @@ function cfxCommander.assignWPListToGroup(group, wpList, delay) local theTask = cfxCommander.buildTaskFromWPList(wpList) local ctrl = group:getController() ---[[-- - if delay < 0.001 then -- immediate action - if ctrl then - ctrl:setTask(theTask) - end - else - -- delay execution of this command by the specified amount - -- of seconds - cfxCommander.scheduleTaskForGroup(group, theTask, delay) - end ---]]-- cfxCommander.scheduleTaskForGroup(group, theTask, delay) end -function cfxCommander.createWPListForGroupToPoint(group, point, speed, formation) +--[[-- + 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 @@ -323,8 +328,8 @@ function cfxCommander.createWPListForGroupToPoint(group, point, speed, formation -- 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, formation) - local wpThere = cfxCommander.createBasicWaypoint(point, speed, formation) + local wpHere = cfxCommander.createBasicWaypoint(here, speed, moveFormation) + local wpThere = cfxCommander.createBasicWaypoint(point, speed, moveFormation) wpList[1] = wpHere wpList[2] = wpThere return wpList @@ -398,12 +403,9 @@ function cfxCommander.createWPListForGroupToPointViaRoads(group, point, speed) if pathLength > (2 * direct) then -- road takes too long, take direct approach - --trigger.action.outText("+++ road path (" .. pathLength .. ") > twice direct route(" .. direct .. "), commencing direct off-road", 30) return cfxCommander.createWPListForGroupToPoint(group, point, speed) end - --trigger.action.outText("+++ ".. group:getName() .. ": choosing road path l=" .. pathLength .. " over direct route d=" .. direct, 30) - -- if we are here, the road trip is valid for idx, wp in pairs(rawRoadPoints) do -- createBasic... supports w.xy format @@ -422,16 +424,17 @@ function cfxCommander.createWPListForGroupToPointViaRoads(group, point, speed) return wpList end -function cfxCommander.makeGroupGoTherePreferringRoads(group, there, speed, delay) +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, "Off Road", delay) + cfxCommander.makeGroupGoThere(group, there, speed, moveFormation, delay) return end @@ -443,7 +446,7 @@ function cfxCommander.makeGroupGoTherePreferringRoads(group, there, speed, delay local oRide = cfxCommander.hasPathZoneFor(here, there) if oRide and oRide.pathing == "offroad" then -- yup, override road preference - cfxCommander.makeGroupGoThere(group, there, speed, "Off Road", delay) + cfxCommander.makeGroupGoThere(group, there, speed, moveFormation, delay) return end end @@ -498,9 +501,6 @@ function cfxCommander.start() -- identify and process all 'pathing' zones local pathZones = cfxZones.getZonesWithAttributeNamed("pathing") - - -- now create a spawner for all, add them to the spawner updater, and spawn for all zones that are not - -- paused 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 diff --git a/modules/csarManager2.lua b/modules/csarManager2.lua index 21a6c17..2217b94 100644 --- a/modules/csarManager2.lua +++ b/modules/csarManager2.lua @@ -1075,7 +1075,7 @@ function csarManager.updateCSARMissions() else local msg = aMission.name .. " confirmed KIA, repeat KIA. Abort CSAR." trigger.action.outTextForCoalition(aMission.side, msg, 30) - trigger.action.outSoundForCoalition(aMission.side, csarManager.actionSound) + trigger.action.outSoundForCoalition(aMission.side, csarManager.lostSound) csarManager.invokeCallbacks(aMission.side, false, 1, "KIA", aMission) end end @@ -1765,4 +1765,6 @@ end -- may want to change if time limit was exceeded on return to tell player that they did not survive the transport + + - randomize smoke color if smoke color has more than one entries --]]-- \ No newline at end of file diff --git a/modules/dcsCommon.lua b/modules/dcsCommon.lua index e42ea94..c7dbbe2 100644 --- a/modules/dcsCommon.lua +++ b/modules/dcsCommon.lua @@ -1,5 +1,5 @@ dcsCommon = {} -dcsCommon.version = "3.0.6" +dcsCommon.version = "3.0.7" --[[-- VERSION HISTORY 3.0.0 - removed bad bug in stringStartsWith, only relevant if caseSensitive is false - point2text new intsOnly option diff --git a/modules/groundTroops.lua b/modules/groundTroops.lua index cab5984..b9123d7 100644 --- a/modules/groundTroops.lua +++ b/modules/groundTroops.lua @@ -1,6 +1,6 @@ cfxGroundTroops = {} -cfxGroundTroops.version = "2.0.1" -cfxGroundTroops.ups = 1 +cfxGroundTroops.version = "2.2.0" +cfxGroundTroops.ups = 0.25 -- every 4 seconds cfxGroundTroops.verbose = false cfxGroundTroops.requiredLibs = { "dcsCommon", -- common is of course needed for everything @@ -29,10 +29,11 @@ cfxGroundTroops.jtacCB = {} -- jtac callbacks, to be implemented 2.0.0 - dmlZones - jtacSound - - clanup + - cleanup - jtacVerbose 2.0.1 - small fiex ti checkPileUp() - + 2.1.0 - captureandhold - oneshot attackowned + 2.2.0 - moveFormation support an entry into the deployed troop table has the following attributes - group - the group @@ -41,6 +42,8 @@ cfxGroundTroops.jtacCB = {} -- jtac callbacks, to be implemented "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 + "captureandhold" - interface to ownedZones, seeks out nearest enemy + or neutral owned zone. once captured, it stays there "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 @@ -53,6 +56,7 @@ cfxGroundTroops.jtacCB = {} -- jtac callbacks, to be implemented - 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 + -- reduced ups to 0.24, updating troops every 4 seconds is fast enough usage: @@ -169,9 +173,9 @@ function cfxGroundTroops.makeTroopsEngageEnemies(troop) -- we lerp to 2/3 of enemy location there = dcsCommon.vLerp(from, there, 0.66) - + local moveFormation = troop.moveFormation local speed = 10 -- m/s = 10 km/h -- wait. 10 m/s is 36 km/h - cfxCommander.makeGroupGoThere(group, there, speed) + cfxCommander.makeGroupGoThere(group, there, speed, moveFormation) local attask = cfxCommander.createAttackGroupCommand(enemies) cfxCommander.scheduleTaskForGroup(group, attask, 0.5) troop.moving = true @@ -189,15 +193,20 @@ function cfxGroundTroops.makeTroopsEngageZone(troop) 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 + local there = enemyZone:getPoint() -- 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) - + if troop.orders == "captureandhold" then + -- direct capture never uses roads + cfxCommander.makeGroupGoThere(group, there, speed, troop.moveFormation, 5) + else + -- when we attack any owned zone, we prefer roads + cfxCommander.makeGroupGoTherePreferringRoads(group, there, speed, 5, troop.moveFormation) + end -- remember that we have issued a move order troop.moving = true end @@ -236,6 +245,10 @@ end -- 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) + if not cfxOwnedZones then + trigger.action.outText("+++groundT: WARNING! ownedZones is not loaded, which is required.", 30) + return nil + end local p = dcsCommon.getGroupLocation(troop.group) local tempZone = cfxZones.createSimpleZone("tz", p, 100) tempZone.owner = troop.side @@ -250,6 +263,17 @@ function cfxGroundTroops.updateZoneAttackers(troop) return end troop.insideDestination = false -- mark as not inside + + -- we *have* a destination, but not yet isued move orders, + -- meaning that we just spawned, probably from helo. + -- do not look for new location, issue move orders instead + if not troop.hasMovedOrders and troop.destination then + troop.hasMovedOrders = true + cfxGroundTroops.makeTroopsEngageZone(troop) + troop.lastOrderDate = timer.getTime() + troop.speedWarning = 0 + return + end local newTargetZone = cfxGroundTroops.getClosestEnemyZone(troop) if not newTargetZone then @@ -259,6 +283,12 @@ function cfxGroundTroops.updateZoneAttackers(troop) end if newTargetZone ~= troop.destination then + if troop.destination and troop.orders == "captureandhold" then + troop.lastOrderDate = timer.getTime() -- we may even dismiss them + -- from troop array. But orders should remain when picked up by helo + -- we never change target. Stay. + return + end troop.destination = newTargetZone cfxGroundTroops.makeTroopsEngageZone(troop) troop.lastOrderDate = timer.getTime() @@ -532,6 +562,10 @@ function cfxGroundTroops.updateWait(troop) end function cfxGroundTroops.updateTroops(troop) + if cfxGroundTroops.verbose then + trigger.action.outText("+++GTroop: enter updateTroopps for <" .. troop.name .. ">", 30) + end + -- if orders start with "wait-" then the troops -- simply do nothing if dcsCommon.stringStartsWith(troop.orders, "wait-") then @@ -547,6 +581,9 @@ function cfxGroundTroops.updateTroops(troop) elseif troop.orders == "attackownedzone" then cfxGroundTroops.updateZoneAttackers(troop) + elseif troop.orders == "captureandhold" then + cfxGroundTroops.updateZoneAttackers(troop) + elseif troop.orders == "laze" then cfxGroundTroops.updateLaze(troop) @@ -880,14 +917,16 @@ end -- createGroundTroop -- use this to create a cfxGroundTroops from a dcs group -- -function cfxGroundTroops.createGroundTroops(inGroup, range, orders) +function cfxGroundTroops.createGroundTroops(inGroup, range, orders, moveFormation) local newTroops = {} if not orders then orders = "guard" end + if not moveFormation then moveFormation = "Custom" end if orders:lower() == "lase" then orders = "laze" -- we use WRONG spelling here, cause we're cool. yeah, right. end + trigger.action.outText("Enter createGT group <" .. inGroup:getName() .. "> with o=<" .. orders .. ">, mf=<" .. moveFormation .. ">", 30) newTroops.insideDestination = false newTroops.unscheduleCount = 0 -- will count up as we aren't scheduled newTroops.speedWarning = 0 @@ -897,6 +936,7 @@ function cfxGroundTroops.createGroundTroops(inGroup, range, orders) newTroops.coalition = inGroup:getCoalition() newTroops.side = newTroops.coalition -- because we'e been using both. newTroops.name = inGroup:getName() + newTroops.moveFormation = moveFormation 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 @@ -912,6 +952,7 @@ function cfxGroundTroops.addGroundTroopsToPool(troops) -- troops MUST be a table end if not troops.orders then troops.orders = "guard" end troops.orders = troops.orders:lower() + if not troops.moveFormation then troops.moveFormation = "Custom" end 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 diff --git a/modules/heloTroops.lua b/modules/heloTroops.lua index 781362c..66c146a 100644 --- a/modules/heloTroops.lua +++ b/modules/heloTroops.lua @@ -1,5 +1,5 @@ cfxHeloTroops = {} -cfxHeloTroops.version = "3.0.3" +cfxHeloTroops.version = "3.0.4" cfxHeloTroops.verbose = false cfxHeloTroops.autoDrop = true cfxHeloTroops.autoPickup = false @@ -40,6 +40,7 @@ cfxHeloTroops.requestRange = 500 -- meters 3.0.1 - fixed a bug with legalTroops attribute 3.0.2 - fixed a typo in in-air menu 3.0.3 - pointInZone check for insertion rather than radius + 3.0.4 - also handles picking up troops with orders "captureandhold" --]]-- -- @@ -52,8 +53,6 @@ cfxHeloTroops.requestRange = 500 -- meters cfxHeloTroops.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 "cfxZones", -- Zones, of course "cfxCommander", -- to make troops do stuff "cfxGroundTroops", -- generic when dropping troops @@ -641,6 +640,7 @@ function cfxHeloTroops.deployTroopsFromHelicopter(conf) local orders = conf.troopsOnBoard.orders local dest = conf.troopsOnBoard.destination local theName = conf.troopsOnBoard.name + local moveFormation = conf.troopsOnBoard.moveFormation if not orders then orders = "guard" end @@ -670,7 +670,15 @@ function cfxHeloTroops.deployTroopsFromHelicopter(conf) troopData.destination = dest -- only for attackzone orders cfxHeloTroops.deployedTroops[theData.name] = troopData - local troop = cfxGroundTroops.createGroundTroops(theGroup, range, orders) + local troop = cfxGroundTroops.createGroundTroops(theGroup, range, orders, moveFormation) + 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 + trigger.action.outText("Inserting troops to capture zone <" .. dest.name .. ">", 30) + 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) @@ -735,8 +743,13 @@ function cfxHeloTroops.doLoadGroup(args) 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 cfxGroundTroops.removeTroopsFromPool(pooledGroup) trigger.action.outTextForGroup(conf.id, "Team '".. conf.troopsOnBoard.name .."' loaded and has orders <" .. conf.troopsOnBoard.orders .. ">", 30) +-- trigger.action.outText("and mf = <" .. conf.troopsOnBoard.moveFormation .. ">", 30) --trigger.action.outSoundForGroup(conf.id, cfxHeloTroops.actionSound) -- "Quest Snare 3.wav") else if cfxHeloTroops.verbose then diff --git a/modules/income.lua b/modules/income.lua index 17a63c0..cb7b68b 100644 --- a/modules/income.lua +++ b/modules/income.lua @@ -65,10 +65,12 @@ function income.update() has, balance = bank.getBalance(1) tick = string.gsub(income.tickMessage, "", redI) trigger.action.outTextForCoalition(1, "\n" .. tick .. "\nNew balance: §" .. balance .. "\n", 30) + trigger.action.outSoundForCoalition(1, income.reportSound) has, balance = bank.getBalance(2) tick = string.gsub(income.tickMessage, "", blueI) trigger.action.outTextForCoalition(2, "\n" .. tick .. "\nNew balance: §" .. balance .. "\n", 30) + trigger.action.outSoundForCoalition(2, income.reportSound) end end @@ -88,6 +90,8 @@ function income.readConfigZone() income.interval = theZone:getNumberFromZoneProperty("interval", 10 * 60) -- every 10 minutes income.tickMessage = theZone:getStringFromZoneProperty("tickMessage", "New funds from income available: §") income.announceTicks = theZone:getBoolFromZoneProperty("announceTicks", true) + income.reportSound = theZone:getStringFromZoneProperty("reportSound", "") + income.verbose = theZone.verbose end diff --git a/modules/launchPlatform.lua b/modules/launchPlatform.lua new file mode 100644 index 0000000..5f9bd2d --- /dev/null +++ b/modules/launchPlatform.lua @@ -0,0 +1,254 @@ +launchPlatform = {} +launchPlatform.version = "0.0.0" +launchPlatform.requiredLibs = { + "dcsCommon", + "cfxZones", +} +launchPlatform.zones = {} +launchPlatform.redLaunchers = {} +launchPlatform.blueLaunchers = {} + +-- weapon types currently known +-- 52613349374 = tomahawk + +function launchPlatform.addLaunchPlatform(theZone) + launchPlatform.zones[theZone.name] = theZone + if theZone.coa == 1 or theZone.coa == 0 then + launchPlatform.redLaunchers[theZone.name] = theZone + end + if theZone.coa == 2 or theZone.coa == 0 then + launchPlatform.blueLaunchers[theZone.name] = theZone + end + +end + +function launchPlatform.readLaunchPlatform(theZone) + theZone.coa = theZone:getCoalitionFromZoneProperty("coalition", 0) + theZone.impactRadius = theZone:getNumberFromZoneProperty("radius", 1000) + if theZone:hasProperty("salvos") then + theZone.num = theZone:getNumberFromZoneProperty("salvos", 1) + end + if theZone:hasProperty("salvo") then + theZone.num = theZone:getNumberFromZoneProperty("salvo", 1) + end + -- possible extensions: missile. currently tomahawk launched from a missile cruiser that beams in and vanishes later + -- later versions could support SCUDS and some long-range arty, + -- perhaps even aircraft + +end + +-- note - the tomahawks don't care who they belong to, we do not +-- need them to belong to anyone, it may be a visibility thing though + +function launchPlatform.launchForPlatform(coa, theZone, tgtPoint, tgtZone) + local launchPoint = theZone:createRandomPointInZone() + local gData = launchPlatform.createData(launchPoint, tgtPoint, tgtZone, theZone.impactRadius, theZone.name, theZone.num) + return gData +end + +function launchPlatform.launchAtTargetZone(coa, tgtZone, theType) -- gets closest platform for target + -- type currently not supported + local platforms = launchPlatform.redLaunchers + if coa == 2 then platforms = launchPlatform.blueLaunchers end + local cty = dcsCommon.getACountryForCoalition(coa) + + -- get closest launcher for target + local tgtPoint = tgtZone:getPoint() + local src, dist = cfxZones.getClosestZone(tgtPoint, platforms) + trigger.action.outText("+++LP: chosen <" .. src.name .. "> as launch platform", 30) + + local theLauncher = launchPlatform.launchForPlatform(coa, src, tgtPoint, tgtZone) + if not theLauncher then + trigger.action.outText("NO LAUNCHER", 30) + return nil + end + -- if type is tomahawk, the platform is ship = 3 + local theGroup = coalition.addGroup(cty, 3, theLauncher) + if not theGroup then + trigger.action.outText("!!!!!!!!!!!!!NOPE", 30) + return + end + -- we remove the group in some time + local now = timer.getTime() + timer.scheduleFunction(launchPlatform.asynchRemovePlatform, theGroup:getName(), now + 300) +end + +function launchPlatform.asynchRemovePlatform(args) + trigger.action.outText("LP: asynch remove for group <" .. args .. ">", 30) + local theGroup = Group.getByName(args) + if not theGroup then return end + Group.destroy(theGroup) +end + +function launchPlatform.createData(thePoint, theTarget, targetZone, radius, name, num, wType) + -- if present, we can use targetZone with some intelligence + if not thePoint then + trigger.action.outText("NO POINT", 30) + return nil + end + if not theTarget then + trigger.action.outText("NO TARGET", 30) + return nil + end + + if not wType then wType = 52613349374 end + if not radius then radius = 1000 end + local useQty = true + if not num then num = 15 end + if num > 30 then num = 30 end -- max 30 missiles + + if not name then name = "launcherDML" end + local gData = { + ["visible"] = false, + ["tasks"] = {}, + ["uncontrollable"] = false, + ["route"] = { + ["points"] = { + [1] = { + ["alt"] = 0, + ["type"] = "Turning Point", + ["ETA"] = 0, + ["alt_type"] = "BARO", + ["formation_template"] = "", + ["y"] = thePoint.z, + ["x"] = thePoint.x, + ["ETA_locked"] = true, + ["speed"] = 0, + ["action"] = "Turning Point", + ["task"] = { + ["id"] = "ComboTask", + ["params"] = { + ["tasks"] = { + [1] = { + ["number"] = 1, + ["auto"] = false, + ["id"] = "FireAtPoint", + ["enabled"] = true, + ["params"] = { + ["y"] = theTarget.z, + ["x"] = theTarget.x, + ["expendQtyEnabled"] = true, + ["alt_type"] = 1, + ["templateId"] = "", + ["expendQty"] = 2, + ["weaponType"] = wType, + ["zoneRadius"] = radius, + }, -- end of ["params"] + }, -- end of [1] + }, -- end of ["tasks"] + }, -- end of ["params"] + }, -- end of ["task"] + ["speed_locked"] = true, + }, -- end of [1] + }, -- end of ["points"] + }, -- end of ["route"] + ["hidden"] = false, + ["units"] = { + [1] = { + ["modulation"] = 0, + ["skill"] = "Average", + ["type"] = "USS_Arleigh_Burke_IIa", + ["y"] = thePoint.z, + ["x"] = thePoint.x, + ["name"] = dcsCommon.uuid(name), + ["heading"] = 2.2925180610373, + ["frequency"] = 127500000, + }, -- end of [1] + }, -- end of ["units"] + ["y"] = thePoint.z, + ["x"] = thePoint.x, + ["name"] = dcsCommon.uuid(name), + ["start_time"] = 0, + } + + -- now create the tasks block replacements + -- create random target locations inside + -- target point with radius and launch 2 per salvo + -- perhaps add some inteligence to target resource points + -- if inside camp + local hiPrioTargets + if targetZone and targetZone.cloners and #targetZone.cloners > 0 then + trigger.action.outText("+++LP: detected <" .. targetZone.name .. "> is camp with <" .. #targetZone.cloners .. "> res-points, re-targeting hi-prio", 30) + hiPrioTargets = targetZone.cloners + radius = radius / 10 -- much smaller error + end + local tasks = {} + for i=1, num do + local dp = dcsCommon.randomPointInCircle(radius, 0) + if hiPrioTargets then + -- choose one of the + local thisCloner = dcsCommon.pickRandom(hiPrioTargets) + local tp = thisCloner:getPoint() + dp.x = dp.x + tp.x + dp.z = dp.z + tp.z + + else + dp.x = dp.x + theTarget.x + dp.z = dp.z + theTarget.z + end + local telem = { + ["number"] = i, + ["auto"] = false, + ["id"] = "FireAtPoint", + ["enabled"] = true, + ["params"] = { + ["y"] = dp.z, + ["x"] = dp.x, + ["expendQtyEnabled"] = true, + ["alt_type"] = 1, + ["templateId"] = "", + ["expendQty"] = 1, + ["weaponType"] = wType, + ["zoneRadius"] = radius, + }, -- end of ["params"] + } -- end of [1] + -- table.insert(tasks, telem) + tasks[i] = telem + end + + -- now replace old task with new + gData.route.points[1].task.params.tasks = tasks + return gData +end + +-- +-- start up +-- +function launchPlatform.readConfigZone() + local theZone = cfxZones.getZoneByName("launchPlatformConfig") + if not theZone then + theZone = cfxZones.createSimpleZone("launchPlatformConfig") + end + launchPlatform.verbose = theZone.verbose +end + + +function launchPlatform.start() +-- lib check + if not dcsCommon.libCheck then + trigger.action.outText("cfx launchPlatform requires dcsCommon", 30) + return false + end + if not dcsCommon.libCheck("cfx launchPlatform", launchPlatform.requiredLibs) then + return false + end + + -- read config + launchPlatform.readConfigZone() + + -- process launchPlatform Zones + local attrZones = cfxZones.getZonesWithAttributeNamed("launchPlatform") + for k, aZone in pairs(attrZones) do + launchPlatform.readLaunchPlatform(aZone) -- process attributes + launchPlatform.addLaunchPlatform(aZone) -- add to list + end + + -- say hi + trigger.action.outText("launchPlatform v" .. launchPlatform.version .. " started.", 30) + return true +end + +if not launchPlatform.start() then + trigger.action.outText("launchPlatform failed to start.", 30) + launchPlatform = nil +end diff --git a/modules/milHelo.lua b/modules/milHelo.lua index 04a3d96..f4c04e1 100644 --- a/modules/milHelo.lua +++ b/modules/milHelo.lua @@ -678,7 +678,9 @@ end function milHelo.GCcollected(gName) -- do some housekeeping? - trigger.action.outText("removed flight <" .. gName .. ">", 30) + if milHelo.verbose then + trigger.action.outText("removed flight <" .. gName .. ">", 30) + end end function milHelo.GC() diff --git a/modules/noGap.lua b/modules/noGap.lua index c4b5c7d..393002a 100644 --- a/modules/noGap.lua +++ b/modules/noGap.lua @@ -1,5 +1,5 @@ noGap = {} -noGap.version = "1.0.0" +noGap.version = "1.0.1" noGap.verbose = false noGap.ignoreMe = "-ng" -- ignore altogether @@ -37,6 +37,7 @@ noGap.requiredLibs = { Version History 1.0.0 - Initial version + 1.0.1 - added "from runway" --]]-- @@ -80,6 +81,7 @@ function noGap.isGroundStart(theGroup) if action == "Fly Over Point" then return false end if action == "Turning Point" then return false end if action == "Landing" then return false end + if action == "From Runway" then return false end -- aircraft is on the ground - but is it in water (carrier)? local u1 = theGroup.units[1] local sType = land.getSurfaceType(u1) -- has fields x and y diff --git a/modules/slotty.lua b/modules/slotty.lua new file mode 100644 index 0000000..7dfc146 --- /dev/null +++ b/modules/slotty.lua @@ -0,0 +1,74 @@ +slotty = {} +slotty.version = "1.1.0" +--[[-- + Single-player slot blocking and slot blocking fallback + for multiplayer when SSB is not installed on server. + Uses SSB's method of marking groups with flags + (c) 2024 by Christian Franz + + Slotty can be disabled by setting the value of the flag named "noSlotty" to + a value greater than zero + +Version history +1.0.0 - Initial version +1.1.0 - "noSlotty" global disable flag, anti-mirror SSB flag + +--]]-- + +function slotty:onEvent(event) + if not event.initiator then return end + local theUnit = event.initiator + if not theUnit.getPlayerName then return end + local pName = theUnit:getPlayerName() + if not pName then return end + local uName = theUnit:getName() + local theGroup = theUnit:getGroup() + local gName = theGroup:getName() + if event.id == 15 then -- birth + if trigger.misc.getUserFlag("noSlotty") > 0 then return end + local np = net.get_player_list() -- retruns a list of PID + local isSP = false + if not np or (#np < 1) then + isSP = true -- we are in single-player mode + end + -- now see if that group name is currently blocked + local blockstate = false + if trigger.misc.getUserFlag(gName) > 0 then + trigger.action.outText("Group <" .. gName .. "> is currently blocked and can't be entered", 30) + blockstate = true + end + + if not blockstate then return end -- nothing left to do, all is fine + + -- interface with SSBClient for compatibility + if cfxSSBClient and cfxSSBClient.occupiedUnits then + cfxSSBClient.occupiedUnits[uName] = nil + end + + if isSP then + theUnit:destroy() -- SP kill, works only in Single-player + return + end + + -- we would leave the rest to SSB, but if we get here, SSB is + -- not installed on host, so we proceed with invoking netAPI + for idx,pid in pairs(np) do + local netName = net.get_name(pid) + if netName == pName then + timer.scheduleFunction(slotty.kick, pid, timer.getTime() + 0.1) + return + end + end + end +end + +function slotty.kick(pid) + net.force_player_slot(pid, 0, '') -- '', thanks Dz! +end + +function slotty.start() + world.addEventHandler(slotty) + trigger.action.outText("slotty v " .. slotty.version .. " running.", 30) +end + +slotty.start() diff --git a/modules/spawnZones.lua b/modules/spawnZones.lua index 48ac9a0..f24ea57 100644 --- a/modules/spawnZones.lua +++ b/modules/spawnZones.lua @@ -1,5 +1,5 @@ cfxSpawnZones = {} -cfxSpawnZones.version = "2.0.1" +cfxSpawnZones.version = "2.0.2" cfxSpawnZones.requiredLibs = { "dcsCommon", -- common is of course needed for everything -- pretty stupid to check for this since we @@ -27,6 +27,8 @@ cfxSpawnZones.spawnedGroups = {} - baseName defaults to zone name, as it is safe for naming - spawnWithSpawner direct link in spawner to spawnZones 2.0.1 - fix in verifySpawnOwnership() when not master zone found + 2.0.2 - new "moveFormation" attribute + --]]-- cfxSpawnZones.allSpawners = {} @@ -128,6 +130,13 @@ function cfxSpawnZones.createSpawner(inZone) theSpawner.count = 1 -- used to create names, and count how many groups created theSpawner.theSpawn = nil -- link to last spawned group theSpawner.formation = inZone:getStringFromZoneProperty("formation", "circle_out") + theSpawner.moveFormation = inZone:getStringFromZoneProperty("moveFormation", "Custom") + + if theSpawner.moveFormation == "Custom" or theSpawner.moveFormation == "EchelonR" or theSpawner.moveFormation == "EchelonL" or theSpawner.moveFormation == "Diamond" or theSpawner.moveFormation == "Vee" or theSpawner.moveFormation == "Cone" or theSpawner.moveFormation == "Rank" then -- all fine, do nothing + else + trigger.action.outText("+++SpwZ: unknown moveFormation <" .. theSpawner.moveFormation .. "> in spawn zone <" .. inZone.name .. ">, defaulting to 'Custom'", 30) + theSpawner.moveFormation = "Custom" + end theSpawner.paused = inZone:getBoolFromZoneProperty("paused", false) -- orders are always converted to all lower case theSpawner.orders = inZone:getStringFromZoneProperty("orders", "guard"):lower() @@ -297,6 +306,7 @@ function cfxSpawnZones.spawnWithSpawner(aSpawner) troopData.groupData = theData troopData.orders = aSpawner.orders -- always set troopData.side = theCoalition + troopData.moveFormation = aSpawner.moveFormation troopData.target = aSpawner.target -- can be nil! troopData.tracker = theZone.trackWith -- taken from ZONE!!, can be nil troopData.range = aSpawner.range @@ -318,7 +328,7 @@ function cfxSpawnZones.spawnWithSpawner(aSpawner) AI.Option.Ground.val.ROE.WEAPON_HOLD, 1.0) else - local newTroops = cfxGroundTroops.createGroundTroops(theGroup, aSpawner.range, aSpawner.orders) + local newTroops = cfxGroundTroops.createGroundTroops(theGroup, aSpawner.range, aSpawner.orders, aSpawner.moveFormation) cfxGroundTroops.addGroundTroopsToPool(newTroops) -- see if we have defined a target zone as destination @@ -576,6 +586,7 @@ function cfxSpawnZones.loadData() for gName, gdTroop in pairs (allTroopData) do local gData = gdTroop.groupData local orders = gdTroop.orders + local moveFormation = gdTroop.moveFormation local target = gdTroop.target local tracker = gdTroop.tracker local side = gdTroop.side @@ -598,7 +609,7 @@ function cfxSpawnZones.loadData() 1.0) else -- add to groundTroops - local newTroops = cfxGroundTroops.createGroundTroops(theGroup, range, orders) + local newTroops = cfxGroundTroops.createGroundTroops(theGroup, range, orders, moveFormation) cfxGroundTroops.addGroundTroopsToPool(newTroops) -- engage a target zone if target then diff --git a/modules/theDebugger.lua b/modules/theDebugger.lua index 78530fd..3e771fb 100644 --- a/modules/theDebugger.lua +++ b/modules/theDebugger.lua @@ -1,6 +1,6 @@ -- theDebugger 2.x debugger = {} -debugger.version = "2.1.0" +debugger.version = "2.1.1" debugDemon = {} debugDemon.version = "2.1.0" @@ -39,6 +39,7 @@ debugger.log = "" debug invocation on clone of data structure readback verification of flag set fixed getProperty() in debugger with zone + 2.1.1 - removed bug that skipped events? when zone not verbose --]]-- @@ -267,11 +268,13 @@ function debugger.createEventMonWithZone(theZone) end for idx, aFlag in pairs(flagArray) do local evt = tonumber(aFlag) - if evt and (debugger.verbose or theZone.verbose) then + if evt then if evt < 0 then evt = 0 end if evt > 57 then evt = 57 end debugger.showEvents[evt] = debugDemon.eventList[tostring(evt)] - debugger.outText(" monitoring event <" .. debugger.showEvents[evt] .. ">", 30) + if (debugger.verbose or theZone.verbose) then + debugger.outText(" monitoring event <" .. debugger.showEvents[evt] .. ">", 30) + end end end end diff --git a/modules/unitPersistence.lua b/modules/unitPersistence.lua index 7517498..57af492 100644 --- a/modules/unitPersistence.lua +++ b/modules/unitPersistence.lua @@ -1,5 +1,5 @@ unitPersistence = {} -unitPersistence.version = '2.0.0' +unitPersistence.version = '2.0.1' unitPersistence.verbose = false unitPersistence.updateTime = 60 -- seconds. Once every minute check statics unitPersistence.requiredLibs = { @@ -21,6 +21,7 @@ unitPersistence.requiredLibs = { - fixed air spawn (fixed wing) 2.0.0 - dmlZones, OOP cleanup + 2.0.1 - cosmetic verbosity during save REQUIRES PERSISTENCE AND MX @@ -162,7 +163,9 @@ function unitPersistence.saveData() end else theUnitData.isDead = true - trigger.action.outText("+++unitPersistence - unit <" .. uName .. "> of group <" .. groupName .. "> is dead or non-existant", 30) + if unitPersistence.verbose then + trigger.action.outText("+++unitPersistence - unit <" .. uName .. "> of group <" .. groupName .. "> is dead or non-existant", 30) + end end -- is alive and exists? end -- unit maybe not dead end -- iterate units in group diff --git a/tutorial & demo missions/demo - helo trooper.miz b/tutorial & demo missions/demo - helo trooper.miz index 591d7cf..80fc339 100644 Binary files a/tutorial & demo missions/demo - helo trooper.miz and b/tutorial & demo missions/demo - helo trooper.miz differ diff --git a/tutorial & demo missions/demo - non SSB & SP slot blocking.miz b/tutorial & demo missions/demo - non SSB & SP slot blocking.miz new file mode 100644 index 0000000..329bbb7 Binary files /dev/null and b/tutorial & demo missions/demo - non SSB & SP slot blocking.miz differ