diff --git a/Doc/DML Documentation.pdf b/Doc/DML Documentation.pdf index eec4894..fc80349 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 afdb3db..fcf19ce 100644 Binary files a/Doc/DML Quick Reference.pdf and b/Doc/DML Quick Reference.pdf differ diff --git a/modules/asw.lua b/modules/asw.lua new file mode 100644 index 0000000..b6bfb70 --- /dev/null +++ b/modules/asw.lua @@ -0,0 +1,983 @@ +asw = {} +asw.version = "1.0.0" +asw.verbose = false +asw.requiredLibs = { + "dcsCommon", -- always + "cfxZones", -- Zones, of course +} +asw.ups = 0.1 -- = once every 10 seconds +asw.buoys = {} -- all buoys, by name +asw.torpedoes = {} -- all torpedoes in the water. +asw.thumpers = {} -- all current sonar amplifiers/booms that are active +asw.fixes = {} -- all subs that we have a fix on. indexed by sub name +-- fixname encodes the coalition of the fix in "/" + +--[[-- + Version History + 1.0.0 - initial version + +--]]-- + +-- +-- :::WARNING::: +-- CURRENTLY NOT CHECKING FOR COALITIONS +-- + +function asw.createTorpedo() + local t = {} + t.lifeTimer = timer.getTime() + asw.torpedoLife + t.speed = asw.torpedoSpeed + t.state = 0; -- not yet released. FSM + t.name = dcsCommon.uuid("asw.t") + return t +end + +function asw.createTorpedoForUnit(theUnit) + local t = asw.createTorpedo() + t.coalition = theUnit:getCoalition() + t.point = theUnit:getPoint() + return t +end + +function asw.createTorpedoForZone(theZone) + local t = asw.createTorpedo() + t.coalition = theZone.coalition + t.point = cfxZones.getPoint(theZone) + return t +end + +function asw.createBuoy() + local b = {} + b.markID = dcsCommon.numberUUID() -- buoy mark + b.coalition = 0 + b.point = nil + b.smokeTimer = timer.getTime() + 5 * 60 -- for refresh + b.smokeColor = nil -- + b.lifeTimer = timer.getTime() + asw.buoyLife + b.contacts = {} -- detected contacts in range. by unit name + b.timeStamps = {} + b.bearing = {} -- bearing to contact + b.lines = {} -- line art for contact (wedges) + b.lastContactNum = 0 + b.lastReportedIn = 0 -- time of last report + return b +end + +function asw.createBuoyForUnit(theUnit) + -- theUnit drops buoy, making it belong to the same coalition + -- as the dropping unit + local b = asw.createBuoy() + b.point = theUnit:getPoint() + b.point.y = 0 + b.coalition = theUnit:getCoalition() + b.smokeColor = asw.smokeColor -- needs to be done later + b.name = dcsCommon.uuid("asw-b." .. theUnit:getName()) + return b +end + +function asw.createBuoyForZone(theZone) + -- theZone drops buoy (if zone isn't linked to unit) + -- making it belong to the same coalition + -- as the dropping unit + local theUnit = cfxZones.getLinkedUnit(theZone) + if theUnit then + b = asw.createBuoyForUnit(theUnit) + return b + end + + local b = asw.createBuoy() + b.point = cfxZones.getPoint(theZone) + b.point.y = 0 + b.coalition = theZone.coalition + b.smokeColor = asw.smokeColor -- needs to be done later + b.name = dcsCommon.uuid("asw-b." .. theZone.name) + return b +end + +-- uid generation for this module. +asw.ccounter = 0 -- init to preferred value +asw.ccinc = 1 -- init to preferred increment +function asw.contactCount() + asw.ccounter = asw.ccounter + asw.ccinc + return asw.ccounter +end + +function asw.createFixForSub(theUnit, theCoalition) + if not theCoalition then + trigger.action.outText("+++ASW: createFix without coalition, assuming BLUE", 30) + theCoalition = 2 + end + + local now = timer.getTime() + local f = {} + f.coalition = theCoalition + f.theUnit = theUnit + if theCoalition == theUnit:getCoalition() then + trigger.action.outText("+++ASW: createFix - theUnit <" .. theUnit:getName() .. "> has same coalition than detection side (" .. theCoalition .. ")", 30) + end + + f.name = theUnit:getName() + f.typeName = theUnit:getTypeName() + f.desig = "SC-" .. asw.contactCount() + f.lifeTimer = now + asw.fixLife -- will be renewed whenever we hit enough signal strength + f.lines = 0 + return f +end + +-- +-- dropping buoys, torpedos and thumpers +-- + +function asw.dropBuoyFrom(theUnit) + if not theUnit or not Unit.isExist(theUnit) then return end + -- make sure we do not drop over land + local p3 = theUnit:getPoint() + local p2 = {x=p3.x, y=p3.z} + local lType = land.getSurfaceType(p2) + if lType ~= 3 then + if asw.verbose then + trigger.action.outText("+++aswZ: ASW counter-measures must be dropped over open water, not <" .. lType .. ">. Aborting deployment for <" .. theUnit:getName() .. "> failed, counter-measure lost", 30) + end + return nil + end + + local now = timer.getTime() + -- create buoy + local theBuoy = asw.createBuoyForUnit(theUnit) + + -- mark point + dcsCommon.markPointWithSmoke(theBuoy.point, theBuoy.smokeColor) + theBuoy.smokeTimer = now + 5 * 60 + + -- add buoy to my inventory + asw.buoys[theBuoy.name] = theBuoy + + -- mark on map + local info = "Buoy dropped by " .. theUnit:getName() .. " at " .. dcsCommon.nowString() + trigger.action.markToCoalition(theBuoy.markID, info, theUnit:getPoint(), theBuoy.coalition, true, "") + if asw.verbose then + trigger.action.outText("Dropping buoy " .. theBuoy.name, 30) + end + return theBuoy +end + +function asw.dropBuoyFromZone(theZone) +-- trigger.action.outText("enter asw.dropBuoyFromZone <" .. theZone.name .. ">", 30) + local theUnit = cfxZones.getLinkedUnit(theZone) + if theUnit and Unit.isExist(theUnit)then + return asw.dropBuoyFrom(theUnit) + end + + -- try and set the zone's coalition by the unit that + -- it is following + local coa = cfxZones.getLinkedUnit(theZone) + if coa then + theZone.coalition = coa + end + + if not theZone.coalition or theZone.coalition == 0 then + trigger.action.outText("+++aswZ: 0 coalition for aswZone <" .. theZone.name .. ">, aborting buoy drop.", 30) + return nil + end + + -- make sure we do not drop over land + local p3 = cfxZones.getPoint(theZone) + local p2 = {x=p3.x, y=p3.z} + local lType = land.getSurfaceType(p2) + if lType ~= 3 then + if asw.verbose then + trigger.action.outText("+++aswZ: asw measures must be dropped over open water, not <" .. lType .. ">. Aborting deployment for <" .. theZone.name .. ">", 30) + end + return nil + end + + + local now = timer.getTime() + -- create buoy + local theBuoy = asw.createBuoyForZone(theZone) + + -- mark point + dcsCommon.markPointWithSmoke(theBuoy.point, theBuoy.smokeColor) + theBuoy.smokeTimer = now + 5 * 60 + + -- add buoy to my inventory + asw.buoys[theBuoy.name] = theBuoy + + -- mark on map + local info = "Buoy dropped by " .. theZone.name .. " at " .. dcsCommon.nowString() + local pos = cfxZones.getPoint(theZone) + trigger.action.markToCoalition(theBuoy.markID, info, pos, theBuoy.coalition, true, "") + if asw.verbose then + trigger.action.outText("Dropping buoy " .. theBuoy.name, 30) + end + return theBuoy +end + +function asw.dropTorpedoFrom(theUnit) + if not theUnit or not Unit.isExist(theUnit) then + return nil + end + local p3 = theUnit:getPoint() + local p2 = {x=p3.x, y=p3.z} + local lType = land.getSurfaceType(p2) + if lType ~= 3 then + if asw.verbose then + trigger.action.outText("+++aswZ: sub counter-measures must be dropped over open water, not <" .. lType .. ">. Aborting deployment for <" .. theUnit:getName() .. "> failed, counter-measure lost", 30) + end + return nil + end + + local t = asw.createTorpedoForUnit(theUnit) + -- add to inventory + asw.torpedoes[t.name] = t + if asw.verbose then + trigger.action.outText("Launching torpedo " .. t.name, 30) + end + return t +end + +function asw.dropTorpedoFromZone(theZone) + local theUnit = cfxZones.getLinkedUnit(theZone) + if theUnit then + return asw.dropTorpedoFrom(theUnit) + end + + -- try and set the zone's coalition by the unit that + -- it is following + local coa = cfxZones.getLinkedUnit(theZone) + if coa then + theZone.coalition = coa + end + + if not theZone.coalition or theZone.coalition == 0 then + trigger.action.outText("+++aswZ: 0 coalition for aswZone <" .. theZone.name .. ">, aborting torpedo drop.", 30) + return nil + end + + -- make sure we do not drop over land + local p3 = cfxZones.getPoint(theZone) + local p2 = {x=p3.x, y=p3.z} + local lType = land.getSurfaceType(p2) + if lType ~= 3 then + if asw.verbose then + trigger.action.outText("+++aswZ: asw measures must be dropped over open water, not <" .. lType .. ">. Aborting deployment for <" .. theZone.name .. ">", 30) + end + return nil + end + + local t = asw.createTorpedoForZone(theZone) + -- add to inventory + asw.torpedoes[t.name] = t + if asw.verbose then + trigger.action.outText("Launching torpedo for zone", 30) + end + return t +end + +-- +-- UPDATE +-- +function asw.getClosestFixTo(loc, coalition) + local dist = math.huge + local closestFix = nil + for fixName, theFix in pairs(asw.fixes) do + if theFix.coalition == coalition then + local theUnit = theFix.theUnit + if Unit.isExist(theUnit) then + pos = theUnit:getPoint() + d = dcsCommon.distFlat(loc, pos) + if d < dist then + dist = d + closestFix = theFix + end + end + end + end + return closestFix, dist +end + +function asw.getClosestSubToLoc(loc, allSubs) + local dist = math.huge + local closestSub = nil + for cName, contact in pairs(allSubs) do + if Unit.isExist(contact.theUnit) then + d = dcsCommon.distFlat(loc, contact.theUnit:getPoint()) + if d < dist then + closestSub = contact.theUnit + dist = d + end + end + end + return closestSub, dist +end + +function asw.wedgeForBuoyAndContact(theBuoy, aName, p) + --env.info(" >enter wedge for buoy/contact: <" .. theBuoy.name .. ">/< .. aName .. >, p= " .. p) + if p > 1 then p = 1 end + theBuoy.lines[aName] = dcsCommon.numberUUID() + local shape = theBuoy.lines[aName] + local p1 = theBuoy.point + local deviant = asw.maxDeviation * (1-p) -- get percentage of max dev + local minDev = math.floor(5 + (deviant * 0.2)) -- one fifth + 5 is fixed + local varDev = math.floor(deviant * 0.8) -- four fifth is variable + --env.info(" |will now calculate leftD and rightD") + local leftD = math.floor(minDev + varDev * math.random()) -- dcsCommon.smallRandom(varDev) -- varDev * math.random() + local rightD = math.floor(minDev + varDev * math.random()) -- dcsCommon.smallRandom(varDev) -- varDev * math.random() + --env.info(" |will now calculate p2 and p3") + local p2 = dcsCommon.newPointAtDegreesRange(p1, theBuoy.bearing[aName] - leftD, asw.maxDetectionRange) + local p3 = dcsCommon.newPointAtDegreesRange(p1, theBuoy.bearing[aName] + rightD, asw.maxDetectionRange) + --env.info(" |will now create wedge <" .. shape .. "> ") + trigger.action.markupToAll(7, theBuoy.coalition, shape, p1, p2, p3, p1, {1, 0, 0, 0.25}, {1, 0, 0, 0.05}, 4, true, "Contact " .. tonumber(shape)) + --env.info(" /< .. aName .. >") +end + +function asw.updateBuoy(theBuoy, allSubs) + --env.info(" >>enter update buoy for " .. theBuoy.name) + -- note: buoys never see subs of their own side since it is + -- assumed that their location is known and filtered + if not theBuoy then return false end + + -- allSubs are all possible contacts + local now = timer.getTime() + if now > theBuoy.lifeTimer then + --env.info(" lifetime ran out") + -- buoy timed out: remove mark + if asw.verbose then + trigger.action.outText("+++ASW: removing mark <" .. theBuoy.markID .. "> for buoy <" .. theBuoy.name .. ">", 30) + end + --env.info(" - will remove mark " .. theBuoy.markID) + trigger.action.removeMark(theBuoy.markID) + --env.info(" - removed mark") + -- now also remove all wedges + for name, wedge in pairs(theBuoy.lines) do + if asw.verbose then + trigger.action.outText("+++ASW: removing wedge mark <" .. wedge .. "> for sub <" .. name .. ">", 30) + end + --env.info(" - will remove wedge " .. wedge) + trigger.action.removeMark(wedge) + end + --env.info(" < theBuoy.smokeTimer then + --env.info(" resmoking buoy, continue") + dcsCommon.markPointWithSmoke(theBuoy.point, theBuoy.smokeColor) + theBuoy.smokeTimer = now + 5 * 60 + --env.info(" resmoke done, continue") + end + + -- check all contacts, skip own coalition subs + -- check signal strength to all subs + local newContacts = {} -- as opposed to already in theBuoy.contacts + --env.info(" :iterating allSubs for contacts") + for contactName, contact in pairs (allSubs) do + if contact.coalition ~= theBuoy.coalition then -- not on our side + local theSub = contact.theUnit + local theSubLoc = theSub:getPoint() + local theSubName = contact.name + local p = 0 -- detection probability + local canDetect = false + local sureDetect = false + local depth = -dcsCommon.getUnitAGL(theSub) -- NOTE: INVERTED!! + if depth > 5 and depth < asw.maxDetectionDepth then + -- distance. probability recedes by square of distance + local dist = dcsCommon.distFlat(theBuoy.point, theSubLoc) + if dist > asw.maxDetectionRange then + -- will not detect + elseif dist < asw.sureDetectionRange then + canDetect = true + sureDetect = true + p = 1 + theBuoy.bearing[theSubName] = dcsCommon.bearingInDegreesFromAtoB(theBuoy.point, theSubLoc) + else + canDetect = true + p = 1 - (dist - asw.sureDetectionRange) / asw.maxDetectionRange -- percentage + p = p * p * p -- cubed, in 3D + theBuoy.bearing[theSubName] = dcsCommon.bearingInDegreesFromAtoB(theBuoy.point, theSubLoc) + end + end + if canDetect then + if sureDetect or math.random() < p then + -- we have detected sub this round! + newContacts[theSubName] = p -- remember for buoy + contact.trackedBy[theBuoy.name] = p -- remember for sub + else + -- didn't detect, do nothing + -- contact.trackedBy[theBuoy.name] = nil -- probably not required, contact is new each pass + end + else + -- contact.trackedBy[theBuoy.name] = nil -- probably not required + end + end -- if not the same coalition + end -- for all contacts + --env.info(" :iterating allSubs done") + -- now compare old contacts with new contacts + -- if contact lost, remove wedge + --env.info(" >start iterating buoy.contacts to find which contacts we lost") + for aName, aP in pairs(theBuoy.contacts) do + if newContacts[aName] then + -- exists, therefore old contact. Keep it + --[[-- code to update wedge removed + if theBuoy.timeStamps[aName] + 60 * 2 < now then + -- update map: remove wedge + local shape = theBuoy.lines[aName] + trigger.action.removeMark(shape) + -- draw a new one + local pc = newContacts[aName] -- new probability + asw.wedgeForBuoyAndContact(theBuoy, aName, pc) + end + --]]-- + else + -- contact lost. remove wedge + local shape = theBuoy.lines[aName] + if asw.verbose then + trigger.action.outText("+++ASW: will remove wedge <" .. shape .. ">", 30) + end + --env.info(" >removing wedge #" .. shape) + trigger.action.removeMark(shape) + --env.info(" >done removing wedge") + -- delete this line entry + theBuoy.lines[aName] = nil + end + end + --env.info(" start iterating newContacts for new contacts") + for aName, aP in pairs(newContacts) do + if theBuoy.contacts[aName] then + -- exists, is old contact, do nothing + else + -- new contact, draw wedge + theBuoy.timeStamps[aName] = now + theBuoy.lines[aName] = dcsCommon.numberUUID() -- new shape ID + asw.wedgeForBuoyAndContact(theBuoy, aName, aP) + -- sound, but suppress ping if we have a fix for that sub + -- fixes are indexed by "/" + if theBuoy.coalition == 1 then -- and (not asw.fixes[aName .. "/" .. "1"])then + asw.newRedBuoyContact = true + elseif theBuoy.coalition == 2 then --and (not asw.fixes[aName .. "/" .. "2"]) then + asw.newBlueBuoyContact = true + end + end + end + --env.info(" >iterating newContacts for new contacts done") + -- we may want to suppress beep if the sub is already in a fix + + -- now save the new contacts and overwrite old + theBuoy.contacts = newContacts + --env.info(" < and sub contact <" .. contact.name .. "> ", 30) + end + bNum = bNum + 1 -- count number of tracking buoys + pTotal = pTotal + p + bearings[bName] = theBuoy.bearing[subName] - 180 + if bearings[bName] < 0 then bearings[bName] = bearings[bName] + 360 end + end + + local best90 = 0 + local above30 = 0 + for bName, aBearing in pairs (bearings) do + for bbName, bBearing in pairs(bearings) do + local a = aBearing + if a > 180 then a = a - 180 end + local b = bBearing + if b > 180 then b = b - 180 end + local d = math.abs(a - b) -- 0..180 + if d > 90 then d = 90 - (d-90) end -- d = 0..90 + local this90 = d + if this90 > 30 then above30 = above30 + 1 end + if this90 > best90 then best90 = this90 end + end + end + above30 = above30 / 2 -- number of buoys that have more than 30° angle to contact, by 2 because each counts twice. + local solver = above30 * best90/90 * pTotal + if solver >= 2.0 then -- we have a fix + return true + end + return false +end + +function asw.updateFixes(allSubs) + -- in order to create or maintain a fix, we need at least x + -- buoys with a confidence level of xx for that sub + -- and their azimuth must make at least 45 degrees so we + -- can make a fix + -- remember that buoys can only see subs of *opposing* side + local now = timer.getTime() + + for subName, contact in pairs(allSubs) do + -- calculate if we have a fix on this sub + local coa = dcsCommon.getEnemyCoalitionFor(contact.coalition) + -- if coa is nil, it's a neutral sub, and we skip + if coa and asw.hasFix(contact) then + -- if new fix? Access existing ones via fix name scheme + -- fix naming scheme is to allow (later) detection of + -- same-side subs with buoys and not create a fix name + -- collision. Currently overkill + local theFix = asw.fixes[subName .. "/" .. tonumber(coa)] + if theFix then + -- exists, nothing to do + else + -- create a new fix + theFix = asw.createFixForSub(contact.theUnit, coa) + local theUnit = theFix.theUnit + local pos = theUnit:getPoint() + local lat, lon, dep = coord.LOtoLL(pos) + local lla, llb = dcsCommon.latLon2Text(lat, lon) + trigger.action.outTextForCoalition(coa, "NEW FIX " .. theFix.desig .. ": submerged contact, class <" .. theFix.typeName .. ">, location " .. lla .. ", " .. llb .. ", tracking.", 30) + if coa == 1 then asw.newRedFix = true + elseif coa == 2 then asw.newBlueFix = true + end + -- add fix to list of fixes + asw.fixes[subName .. "/" .. tonumber(coa)] = theFix + end + -- update life timer for all fixes + theFix.lifeTimer = now + asw.fixLife + trigger.action.outTextForCoalition(coa, "contact fix " .. theFix.desig .. " confirmed.", 30) + if asw.verbose then + trigger.action.outText("renewed lease for fix " .. subName .. "/" .. tonumber(coa), 30) + end + else + -- no new fix, + end + end + + -- now iterate all fixes and update them, or time out + local filtered = {} + for fixName, theFix in pairs(asw.fixes) do + if now < theFix.lifeTimer and Unit.isExist(theFix.theUnit) then + -- update the location + if theFix.lines and theFix.lines > 0 then + -- remove old + trigger.action.removeMark(theFix.lines) + end + -- allocate new fix id. we always need new fix id + theFix.lines = dcsCommon.numberUUID() + -- mark on map for coalition + local theUnit = theFix.theUnit + local pos = theUnit:getPoint() + -- assemble sub info + local vel = math.floor(1.94384 * dcsCommon.getUnitSpeed(theUnit)) + local heading = math.floor(dcsCommon.getUnitHeadingDegrees(theUnit)) + local delta = asw.fixLife - (theFix.lifeTimer - now) + local timeAgo = dcsCommon.processHMS(":<:s>", delta) + local info = "Submerged contact, identified as '" .. theFix.theUnit:getTypeName() .. "' class, moving at " .. vel .. " kts, heading " .. heading .. ", last fix " .. timeAgo .. " minutes ago." + -- note: neet to change to markToCoalition! + trigger.action.markToCoalition(theFix.lines, info, pos, theFix.coalition, true, "") + + -- add to filtered + filtered[fixName] = theFix + else + -- do not add to filtered, timed out or unit destroyed + trigger.action.outTextForCoalition(theFix.coalition, "Lost fix for contact", 30) + -- remove mark + if theFix.lines and theFix.lines > 0 then + trigger.action.removeMark(theFix.lines) + end + end + end + + asw.fixes = filtered +end + +function markTorpedo(theTorpedo) + theTorpedo.markID = dcsCommon.numberUUID() + trigger.action.markToCoalition(theTorpedo.markID, "Torpedo " .. theTorpedo.name, theTorpedo.point, theTorpedo.coalition, true, "") +end + +function asw.updateTorpedo(theTorpedo, allSubs) + -- homes in on closest torpedo, but only if it can detect it + -- else it simply runs in a random direction + + -- remove old mark + if theTorpedo.markID then + trigger.action.removeMark(theTorpedo.markID) + end + + -- outside of lethal range, torp can randomly fail and never + -- re-aquire (lostTrack is true) unless it accidentally + -- gets into lethal range + + -- see if it timed out + local now = timer.getTime() + if now > theTorpedo.lifeTimer then + trigger.action.outTextForCoalition(theTorpedo.coalition, "Torpedo " .. theTorpedo.name .. " ran out", 30) + return false + end + + -- redraw mark for torpedo. give it a new + -- uuid every time + -- during update, it gets near and if it can get close + -- enough, it will set them up the bomb and create an explosion + -- near the sub it detected. + -- uses FSM + -- state 0 = dropped into water + if theTorpedo.state == 0 then + -- state 0: dropping in the water + trigger.action.outTextForCoalition(theTorpedo.coalition, "Torpedo " .. theTorpedo.name .. " in the water!", 30) + theTorpedo.state = 1 + markTorpedo(theTorpedo) + return true + + elseif theTorpedo.state == 1 then + -- seeking. get closest fix. if we have a fix in range + -- we go to stage homing, and it's a race between time and + -- and sub + trigger.action.outTextForCoalition(theTorpedo.coalition, "Torpedo " .. theTorpedo.name .. " is seeking contact...", 30) + + -- select closest fix from same side as torpedo + local theFix, dist = asw.getClosestFixTo(theTorpedo.point, theTorpedo.coalition) + + if theFix and dist > asw.maxDetectionRange / 2 then + -- too far, forget it existed + theFix = nil + end + + if not theFix then + if asw.verbose then + trigger.action.outText("stage1: No fix/distance found for " .. theTorpedo.name, 30) + end + else + if asw.verbose then + trigger.action.outText("stage1: found fix <" .. theFix.name .. "> at dist <" .. dist .. "> for " .. theTorpedo.name, 30) + end + end + + if theFix and dist < 1700 then + -- have seeker, go to homing mode + theTorpedo.target = theFix.theUnit + if asw.verbose then + trigger.action.outText("+++asw: target found: <" .. theTorpedo.target:getName() .. ">", 30) + end + theTorpedo.state = 20 -- homing + + elseif theFix then + local B = theFix.theUnit:getPoint() + theTorpedo.course = dcsCommon.bearingFromAtoB(theTorpedo.point, B) + if asw.verbose then + trigger.action.outText("+++asw: unguided heading for <" .. theFix.theUnit:getName() .. ">", 30) + end + theTorpedo.state = 10 -- directed run + else + -- no fix anywhere in range, + -- simply pick a course and run + -- maybe we get lucky + theTorpedo.course = 2 * 3.1415 * math.random() + if asw.verbose then + trigger.action.outText("+++asw: random heading", 30) + end + theTorpedo.state = 10 -- random run + end + + markTorpedo(theTorpedo) + return true + + elseif theTorpedo.state == 10 then -- moving, not homing + -- move torpedo and see if it's close enough to a sub + -- to track or blow up + local displacement = asw.torpedoSpeed * 1/asw.ups -- meters travelled + if not theTorpedo.course then + theTorpedo.course = 0 + trigger.action.outText("+++ASW: Torpedo <" .. theTorpedo.name .. "> stage (10) with undefined course, setting 0", 30) + end + + theTorpedo.point.x = theTorpedo.point.x + displacement * math.cos(theTorpedo.course) + theTorpedo.point.z = theTorpedo.point.z + displacement * math.sin(theTorpedo.course) + + -- seeking ANY sub now. + -- warning: may go after our own subs as well, torpedo don't care! + local theSub, dist = asw.getClosestSubToLoc(theTorpedo.point, allSubs) + if dist < 1200 then + -- we lock on to this sub + theTorpedo.target = theSub + theTorpedo.state = 20 -- switch to homing + trigger.action.outTextForCoalition(theTorpedo.coalition, "Torpedo " .. theTorpedo.name .. " is going active!", 30) + end + + if dist < 1.2 * displacement then + theTorpedo.state = 99 -- go boom + end + markTorpedo(theTorpedo) + return true + + elseif theTorpedo.state == 20 then -- HOMING! + if not Unit.isExist(theTorpedo.target) then + -- target was destroyed? + if asw.verbose then + trigger.action.outText("+++asw: target lost", 30) + end + theTorpedo.course = 2 * 3.1415 * math.random() + theTorpedo.state = 10 -- switch to run free + theTorpedo.target = nil + trigger.action.outTextForCoalition(theTorpedo.coalition, "Torpedo " .. theTorpedo.name .. " lost track, searching...", 30) + return + end + + if not theTorpedo.target then + -- sanity check + theTorpedo.course = 2 * 3.1415 * math.random() + theTorpedo.state = 10 -- switch to run free + return + end + + -- we know that isExist(target) + local B = theTorpedo.target:getPoint() + theTorpedo.course = dcsCommon.bearingFromAtoB(theTorpedo.point, B) + local displacement = asw.torpedoSpeed * 1/asw.ups -- meters travelled + theTorpedo.point.x = theTorpedo.point.x + displacement * math.cos(theTorpedo.course) + theTorpedo.point.z = theTorpedo.point.z + displacement * math.sin(theTorpedo.course) + local dist = dcsCommon.distFlat(theTorpedo.point, B) + if dist < displacement then + theTorpedo.state = 99 -- boom, babe! + else + local hdg = math.floor(57.2958 * theTorpedo.course) + if hdg < 0 then hdg = hdg + 360 end + trigger.action.outTextForCoalition(theTorpedo.coalition, "Torpedo " .. theTorpedo.name .. " is homing, course " .. hdg .. ", " .. math.floor(dist) .. "m to impact", 30) + end + -- move to this torpedo and blow up + -- when close enough + markTorpedo(theTorpedo) + + return true + elseif theTorpedo.state == 99 then -- go boom + if Unit.isExist(theTorpedo.target) then + Unit.destroy(theTorpedo.target) + end + -- impact! + trigger.action.outTextForCoalition(theTorpedo.coalition, "Impact for " .. theTorpedo.name .. "! We have confirmed hit on submerged contact!", 30) + if theTorpedo.coalition == 1 then + if asw.redKill then + cfxZones.pollFlag(asw.redKill, asw.method, asw) + end + elseif theTorpedo.coalition == 2 then + if asw.blueKill then + cfxZones.pollFlag(asw.blueKill, asw.method, asw) + end + end + + -- make surface explosion + -- choose point 1m under water + local loc = theTorpedo.point + local alt = land.getHeight({x = loc.x, y = loc.z}) + loc.y = alt-1 + trigger.action.explosion(loc, 3000) + + -- we are done + return false + + else + -- we somehow ran into an unknown state + trigger.action.outText("unknown torpedo state <" .. theTorpedo.state .. "> for <" .. theTorpedo.name .. ">", 20) + return false + end + + -- return true if it should be kept in array + return true +end + +-- +-- MAIN UPDATE +-- +-- does not find subs that have surfaced +-- returns a list of 'contacts' - ready made tables +-- to track the sub: who sees them (trackedBy) and misc +-- info. +-- contacts is indexed by unit name +function asw.gatherSubs() + local allCoas = {0, 1, 2} + local subs = {} + for idx, coa in pairs(allCoas) do + local allGroups = coalition.getGroups(coa, 3) -- ships only + for idy, aGroup in pairs(allGroups) do + allUnits = aGroup:getUnits() + for idz, aUnit in pairs(allUnits) do + -- see if this unit is a sub + if aUnit and Unit.isExist(aUnit) and + (dcsCommon.getUnitAGL(aUnit) < -5) then -- yes, submerged contact. + local contact = {} + contact.theUnit = aUnit + contact.trackedBy = {} -- buoys that have a ping + contact.name = aUnit:getName() + contact.coalition = aUnit:getCoalition() + subs[contact.name] = contact + end + end + end + end + return subs +end + +function asw.update() + --env.info("-->Enter asw update") + -- first, schedule next invocation + timer.scheduleFunction(asw.update, {}, timer.getTime() + 1/asw.ups) + + local subs = asw.gatherSubs() -- ALL contacts/subs + + asw.newRedBuoyContact = false + asw.newBlueBuoyContact = false + + -- refresh all buoy detections + -- if #asw.buoys > 0 then + --env.info("Before buoy proc") + local filtered = {} + for bName, theBuoy in pairs(asw.buoys) do + if asw.updateBuoy(theBuoy, subs) then + filtered[bName] = theBuoy + end + end + asw.buoys = filtered + --env.info("Complete buoy proc") + + if asw.newRedBuoyContact then + trigger.action.outSoundForCoalition(1, asw.sonarSound) + end + if asw.newBlueBuoyContact then + trigger.action.outSoundForCoalition(2, asw.sonarSound) + end + + + -- update fixes: create if they don't exist + asw.newBlueFix = false + asw.newRedFix = false + + --env.info("Before fixes") + asw.updateFixes(subs) + --env.info("Complete fixes") + + if asw.newBlueFix then + trigger.action.outSoundForCoalition(2, asw.fixSound) + end + + if asw.newRedFix then + trigger.action.outSoundForCoalition(1, asw.fixSound) + end + + -- see if there are any torpedoes in the water + --if #asw.torpedoes > 0 then + --env.info("Before torpedoes") + local filtered = {} + for tName, theTorpedo in pairs(asw.torpedoes) do + if asw.updateTorpedo(theTorpedo, subs) then + filtered[tName] = theTorpedo + end + end + asw.torpedoes = filtered + + --env.info("Complete torpedoes") + + --end + --env.info("<--Leave asw update") +end + +-- +-- CONFIG & START +-- +function asw.readConfigZone() + local theZone = cfxZones.getZoneByName("aswConfig") + if not theZone then + if asw.verbose then + trigger.action.outText("+++asw: no config zone!", 30) + end + theZone = cfxZones.createSimpleZone("aswConfig") + end + asw.verbose = theZone.verbose + asw.name = "aswConfig" -- make compatible with cfxZones + + -- set defaults, later do the reading + asw.buoyLife = 30 * 60 -- 30 minutes life time + asw.buoyLife = cfxZones.getNumberFromZoneProperty(theZone, "buoyLife", asw.buoyLife) + if asw.buoyLife < 1 then asw.buoyLife = 999999 end -- very, very long time + + asw.maxDetectionRange = 12000 -- 12 km + asw.maxDetectionRange = cfxZones.getNumberFromZoneProperty(theZone, "detectionRange", 12000) + asw.sureDetectionRange = 1000 -- inside 1 km will always detect sub + asw.sureDetectionRange = cfxZones.getNumberFromZoneProperty(theZone, "sureDetect", 1000) + asw.torpedoLife = 7 * 60 + 30 -- 7.5 minutes, will reach max range in that time + asw.torpedoSpeed = 28.3 -- speed in m/s -- 55 knots + asw.maxDetectionDepth = 500 -- in meters. deeper than that, no detection. + asw.maxDetectionDepth = cfxZones.getNumberFromZoneProperty(theZone, "detectionDepth", 500) + asw.fixLife = 3 * 60 -- a sub "fix" lives 3 minutes past last renew + asw.fixLife = cfxZones.getNumberFromZoneProperty(theZone, "fixLife", asw.fixLife) + if asw.fixLife < 1 then asw.fixLife = 999999 end -- a long time + + asw.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) + + asw.maxDeviation = 40 -- 40 degrees + 5 = 45 degrees left and right max deviation makes a worst-case 90 degree left/right wedge + asw.fixSound = "submarine ping.ogg" + asw.fixSound = cfxZones.getStringFromZoneProperty(theZone, "fixSound", asw.fixSound) + asw.sonarSound = "beacon beep-beep.ogg" + asw.sonarSound = cfxZones.getStringFromZoneProperty(theZone, "sonarSound", asw.sonarSound) + if cfxZones.hasProperty(theZone, "redKill!") then + asw.redKill = cfxZones.getStringFromZoneProperty(theZone, "redKill!", "none") + end + if cfxZones.hasProperty(theZone, "blueKill!") then + asw.blueKill = cfxZones.getStringFromZoneProperty(theZone, "blueKill!", "none") + end + + asw.method = cfxZones.getStringFromZoneProperty(theZone, "method", "inc") + + asw.smokeColor = cfxZones.getSmokeColorStringFromZoneProperty(theZone, "smokeColor", "red") + asw.smokeColor = dcsCommon.smokeColor2Num(asw.smokeColor) + + if asw.verbose then + trigger.action.outText("+++asw: read config", 30) + end +end + +function asw.start() + if not dcsCommon.libCheck then + trigger.action.outText("cfx asw requires dcsCommon", 30) + return false + end + if not dcsCommon.libCheck("cfx asw", asw.requiredLibs) then + return false + end + + -- read config + asw.readConfigZone() + + -- start update + asw.update() + + trigger.action.outText("cfx ASW v" .. asw.version .. " started.", 30) + return true +end + +-- +-- start up asw +-- +if not asw.start() then + trigger.action.outText("cfx asw aborted: missing libraries", 30) + asw = nil +end + +--[[-- + Ideas/to do + - false positives for detections + - triangle mark for fixes, color red + - squares for torps, color yellow + - remove torpedoes when they run aground + +--]]-- diff --git a/modules/aswGUI.lua b/modules/aswGUI.lua new file mode 100644 index 0000000..9c52f74 --- /dev/null +++ b/modules/aswGUI.lua @@ -0,0 +1,597 @@ +aswGUI = {} +aswGUI.version = "1.0.0" +aswGUI.verbose = false +aswGUI.requiredLibs = { + "dcsCommon", -- always + "cfxZones", -- Zones, of course + "asw", -- needs asw module + "aswZones", -- also needs the asw zones +} + +--[[-- + Version History + 1.0.0 - initial version + +--]]-- + +aswGUI.ups = 1 -- = once every second +aswGUI.aswCraft = {} + +--[[-- +::::::::::::::::: ASSUMES SINGLE_UNIT GROUPS :::::::::::::::::: +--]]-- + + +function aswGUI.resetConf(asc) + if asc.rootMenu then + missionCommands.removeItemForGroup(asc.groupID, asc.rootMenu) + end + asc.rootMenu = missionCommands.addSubMenuForGroup(asc.groupID, "ASW") + asc.buoyNum = 0 + asc.torpedoNum = 0 + asc.coolDown = 0 -- used when waiting, currently not used +end + +-- we use lazy init whenever player enters +function aswGUI.initUnit(unitName) -- now this unit exists + local theUnit = Unit.getByName(unitName) + if not theUnit then + trigger.action.outText("+++aswGUI: <" .. unitName .. "> not a unit, aborting initUnit", 30) + return nil + end + + local theGroup = theUnit:getGroup() + local asc = {} -- set up player craft config block + --local groupData = cfxMX.playerUnit2Group[unitName] + asc.groupName = theGroup:getName() -- groupData.name + asc.name = unitName + asc.groupID = theGroup:getID() -- groupData.groupId + aswGUI.resetConf(asc) + return asc +end + + +function aswGUI.processWeightFor(conf) + -- make total weight and handle all + -- cargo for this unit + + -- hand off to DML cargo manager if implemented + if cargosuper then + trigger.action.outText("CargoSuper handling regquired, using none", 30) + return + end + + local totalWeight = conf.buoyNum * aswGUI.buoyWeight + totalWeight = totalWeight + conf.torpedoNum * aswGUI.torpedoWeight + + -- set cargo weight + trigger.action.setUnitInternalCargo(conf.name, totalWeight) + local theUnit = Unit.getByName(conf.name) + trigger.action.outTextForGroup(conf.groupID, "Total asw weight: " .. totalWeight .. "kg (" .. math.floor(totalWeight * 2.20462) .. "lbs)", 30) + return totalWeight +end + +-- +-- build unit menu +-- +function aswGUI.getBuoyCapa(conf) -- returns capa per slot + -- warning: assumes two "slots" maximum + if conf.torpedoNum > aswGUI.torpedoesPerSlot then return 0 end -- both slots are filled with torpedoes + if conf.torpedoNum > 0 then -- one slot is taken up by torpedoes + return aswGUI.buoysPerSlot - conf.buoyNum + end + if conf.buoyNum >= aswGUI.buoysPerSlot then + return 2 * aswGUI.buoysPerSlot - conf.buoyNum + end + return aswGUI.buoysPerSlot - conf.buoyNum +end + +function aswGUI.getTorpedoCapa(conf) + if conf.buoyNum > aswGUI.buoysPerSlot then return 0 end -- both slots are filled with buoys + if conf.buoyNum > 0 then -- one slot is taken up by torpedoes + return aswGUI.torpedoesPerSlot - conf.torpedoNum + end + if conf.torpedoNum >= aswGUI.torpedoesPerSlot then + return 2 * aswGUI.torpedoesPerSlot - conf.torpedoNum + end + return aswGUI.torpedoesPerSlot - conf.torpedoNum +end + +function aswGUI.setGroundMenu(conf, theUnit) + -- build menu for load stores + local loc = theUnit:getPoint() + local closestAswZone = aswZones.getClosestASWZoneTo(loc) + local inZone = cfxZones.pointInZone(loc, closestAswZone) + local bStore = 0 -- available buoys + local tStore = 0 -- available torpedoes + -- ... but only if we are in an asw zone + -- calculate how much is available + if inZone then + bStore = closestAswZone.buoyNum + if bStore < 0 then bStore = aswGUI.buoysPerSlot end + tStore = closestAswZone.torpedoNum + if tStore < 0 then tStore = aswGUI.torpedoesPerSlot end + end + + if bStore > 0 then + local bCapa = aswGUI.getBuoyCapa(conf) + if bCapa > 0 then + missionCommands.addCommandForGroup(conf.groupID, "Load <" .. bCapa .."> ASW Buoys", conf.rootMenu, aswGUI.xHandleLoadBuoys, conf) + else + missionCommands.addCommandForGroup(conf.groupID, "(No free Buoy stores)", conf.rootMenu, aswGUI.xHandleGeneric, conf) + end + else + missionCommands.addCommandForGroup(conf.groupID, "(Can't load ASW Buoys, no supplies in range)", conf.rootMenu, aswGUI.xHandleGeneric, conf) + end + + if conf.buoyNum > 0 then + local toUnload = conf.buoyNum + if toUnload > aswGUI.buoysPerSlot then toUnload = aswGUI.buoysPerSlot end + missionCommands.addCommandForGroup(conf.groupID, "Unload <" .. toUnload .. "> ASW Buoys (" .. conf.buoyNum .. " on board)", conf.rootMenu, aswGUI.xHandleUnloadBuoys, conf) + end + + -- torpedo proccing + + if tStore > 0 then + local tCapa = aswGUI.getTorpedoCapa(conf) + if tCapa > 0 then + tCapa = 1 -- one at a time + missionCommands.addCommandForGroup(conf.groupID, "Load <" .. tCapa .."> ASW Torpedoes", conf.rootMenu, aswGUI.xHandleLoadTorpedoes, conf) + else + missionCommands.addCommandForGroup(conf.groupID, "All stores filled to capacity", conf.rootMenu, aswGUI.xHandleGeneric, conf) + end + else + missionCommands.addCommandForGroup(conf.groupID, "(Can't load ASW Torpedoes, no supplies in range)", conf.rootMenu, aswGUI.xHandleGeneric, conf) + end + + if conf.torpedoNum > 0 then + local toUnload = conf.torpedoNum + if toUnload > aswGUI.torpedoesPerSlot then toUnload = aswGUI.buoysPerSlot end + missionCommands.addCommandForGroup(conf.groupID, "Unload <" .. toUnload .. "> ASW Torpedoes (" .. conf.torpedoNum .. " on board)", conf.rootMenu, aswGUI.xHandleUnloadTorpedoes, conf) + end + missionCommands.addCommandForGroup(conf.groupID, "[Stores: <" .. conf.buoyNum .. "> Buoys | <" .. conf.torpedoNum .. "> Torpedoes]", conf.rootMenu, aswGUI.xHandleGeneric, conf) +end + +function aswGUI.setAirMenu(conf, theUnit) + -- build menu for load stores + local bStore = conf.buoyNum -- available buoys + local tStore = conf.torpedoNum -- available torpedoes + + if bStore < 1 and tStore < 1 then + missionCommands.addCommandForGroup(conf.groupID, "No ASW munitions on board", conf.rootMenu, aswGUI.xHandleGeneric, conf) + return + end + + if bStore > 0 then + missionCommands.addCommandForGroup(conf.groupID, "BUOY - Drop an ASW Buoy", conf.rootMenu, aswGUI.xHandleBuoyDropoff, conf) + else + missionCommands.addCommandForGroup(conf.groupID, "No ASW Buoys on board", conf.rootMenu, aswGUI.xHandleGeneric, conf) + end + + if tStore > 0 then + missionCommands.addCommandForGroup(conf.groupID, "TORP - Drop an ASW Torpedo", conf.rootMenu, aswGUI.xHandleTorpedoDropoff, conf) + else + missionCommands.addCommandForGroup(conf.groupID, "No ASW Torpedoes on board", conf.rootMenu, aswGUI.xHandleGeneric, conf) + end + + missionCommands.addCommandForGroup(conf.groupID, "[Stores: <" .. conf.buoyNum .. "> Buoys | <" .. conf.torpedoNum .. "> Torpedoes]", conf.rootMenu, aswGUI.xHandleGeneric, conf) +end + +function aswGUI.setMenuForUnit(theUnit) + if not theUnit then return end + if not Unit.isExist(theUnit) then return end + local uName = theUnit:getName() + + -- if we get here, the unit exists. fetch unit config + local conf = aswGUI.aswCraft[uName] + -- delete old, and create new root menu + missionCommands.removeItemForGroup(conf.groupID, conf.rootMenu) + conf.rootMenu = missionCommands.addSubMenuForGroup(conf.groupID, "ASW") + + -- if we are in the air, we add menus to drop buoys or torpedoes + if theUnit:inAir() then + aswGUI.setAirMenu(conf, theUnit) + else + aswGUI.setGroundMenu(conf, theUnit) + end +end + +-- +-- comms callback handling +-- +-- +-- LOADING / UNLOADING +-- +function aswGUI.xHandleGeneric(args) + timer.scheduleFunction(aswGUI.handleGeneric, args, timer.getTime() + 0.1) +end + +function aswGUI.handleGeneric(args) + if not args then args = "*EMPTY*" end + -- do nothing +end + +function aswGUI.xHandleLoadBuoys(args) + timer.scheduleFunction(aswGUI.handleLoadBuoys, args, timer.getTime() + 0.1) +end + + +function aswGUI.handleLoadBuoys(args) + local conf = args + local theUnit = Unit.getByName(conf.name) + if not theUnit then + trigger.action.outText("+++aswG: (load buoys) can't find unit <" .. conf.name .. ">", 30) + return + end + local loc = theUnit:getPoint() + local theZone = aswZones.getClosestASWZoneTo(loc) + local inZone = cfxZones.pointInZone(loc, theZone) + local bStore = 0 -- available buoys + if inZone then + bStore = theZone.buoyNum + if bStore < 0 then bStore = aswGUI.buoysPerSlot end + else + trigger.action.outTextForGroup(conf.groupID, "Nothing loaded. Return to ASW loading zone.", 30) + aswGUI.setMenuForUnit(theUnit) + return + end + + if bStore < 1 then + trigger.action.outTextForGroup(conf.groupID, "ASW Buoy stock has run out. Sorry.", 30) + aswGUI.setMenuForUnit(theUnit) + return + end + + local capa = aswGUI.getBuoyCapa(conf) + conf.buoyNum=conf.buoyNum + capa + + if theZone.buoyNum >= 0 then + theZone.buoyNum = theZone.buoyNum - capa + if theZone.buoyNum < 0 then theZone.buoyNum = 0 end + -- proc new weight + end + + aswGUI.processWeightFor(conf) + trigger.action.outTextForGroup(conf.groupID, "Loaded <" .. capa .. "> ASW Buoys.", 30) + aswGUI.setMenuForUnit(theUnit) +end + +function aswGUI.xHandleUnloadBuoys(args) + timer.scheduleFunction(aswGUI.handleUnloadBuoys, args, timer.getTime() + 0.1) +end + +function aswGUI.handleUnloadBuoys(args) + local conf = args + local theUnit = Unit.getByName(conf.name) + if not theUnit then + trigger.action.outText("+++aswG: (unload buoys) can't find unit <" .. conf.name .. ">", 30) + return + end + local loc = theUnit:getPoint() + local theZone = aswZones.getClosestASWZoneTo(loc) + local inZone = cfxZones.pointInZone(loc, theZone) + + local amount = conf.buoyNum + while amount > aswGUI.buoysPerSlot do -- future proof, any # of slots + amount = amount - aswGUI.buoysPerSlot + end + conf.buoyNum = conf.buoyNum - amount + + if inZone then + if theZone.buoyNum >= 0 then theZone.buoyNum = theZone.buoyNum + amount end + trigger.action.outTextForGroup(conf.groupID, "Returned <" .. amount .. "> ASW Buoys to storage.", 30) + else + -- simply drop them, irrecoverable + trigger.action.outTextForGroup(conf.groupID, "Discarded <" .. amount .. "> ASW Buoys.", 30) + end + aswGUI.processWeightFor(conf) + aswGUI.setMenuForUnit(theUnit) +end + +function aswGUI.xHandleLoadTorpedoes(args) + timer.scheduleFunction(aswGUI.handleLoadTorpedoes, args, timer.getTime() + 0.1) +end + +function aswGUI.handleLoadTorpedoes(args) + local conf = args + local theUnit = Unit.getByName(conf.name) + if not theUnit then + trigger.action.outText("+++aswG: (load torps) can't find unit <" .. conf.name .. ">", 30) + return + end + local loc = theUnit:getPoint() + local theZone = aswZones.getClosestASWZoneTo(loc) + local inZone = cfxZones.pointInZone(loc, theZone) + local tStore = 0 -- available torpedoes + if inZone then + tStore = theZone.torpedoNum + if tStore < 0 then tStore = aswGUI.torpedoesPerSlot end + else + trigger.action.outTextForGroup(conf.groupID, "Nothing loaded. Return to ASW loading zone.", 30) + aswGUI.setMenuForUnit(theUnit) + return + end + + if tStore < 1 then + trigger.action.outTextForGroup(conf.groupID, "ASW Torpedo stock has run out. Sorry.", 30) + aswGUI.setMenuForUnit(theUnit) + return + end + + local capa = aswGUI.getTorpedoCapa(conf) + capa = 1 -- load one at a time + conf.torpedoNum=conf.torpedoNum + capa + if theZone.torpedoNum >= 0 then + theZone.torpedoNum = theZone.torpedoNum - capa + if theZone.torpedoNum < 0 then theZone.torpedoNum = 0 end + end + + aswGUI.processWeightFor(conf) + + trigger.action.outTextForGroup(conf.groupID, "Loaded <" .. capa .. "> asw Torpedoes.", 30) + aswGUI.setMenuForUnit(theUnit) +end + +function aswGUI.xHandleUnloadTorpedoes(args) + timer.scheduleFunction(aswGUI.handleUnloadTorpedoes, args, timer.getTime() + 0.1) +end + +function aswGUI.handleUnloadTorpedoes(args) + local conf = args + local theUnit = Unit.getByName(conf.name) + if not theUnit then + trigger.action.outText("+++aswG: (unload torpedoes) can't find unit <" .. conf.name .. ">", 30) + return + end + local loc = theUnit:getPoint() + local theZone = aswZones.getClosestASWZoneTo(loc) + local inZone = cfxZones.pointInZone(loc, theZone) + + local amount = conf.torpedoNum + while amount > aswGUI.torpedoesPerSlot do -- future proof, any # of slots + amount = amount - aswGUI.torpedoesPerSlot + end + conf.torpedoNum = conf.torpedoNum - amount + + if inZone then + if theZone.torpedoNum >= 0 then theZone.torpedoNum = theZone.torpedoNum + amount end + trigger.action.outTextForGroup(conf.groupID, "Returned <" .. amount .. "> ASW Torpedoes to storage.", 30) + else + -- simply drop them, irrecoverable + trigger.action.outTextForGroup(conf.groupID, "Discarded <" .. amount .. "> ASW Torpedoes.", 30) + end + aswGUI.processWeightFor(conf) + aswGUI.setMenuForUnit(theUnit) +end + +-- +-- LIVE DROP +-- +function aswGUI.xHandleBuoyDropoff(args) + timer.scheduleFunction(aswGUI.handleBuoyDropoff, args, timer.getTime() + 0.1) +end + +function aswGUI.hasDropoffParams(conf) + -- to be added later, can be curtailed for units + return true +end + +function aswGUI.handleBuoyDropoff(args) + local conf = args + local theUnit = Unit.getByName(conf.name) + if not theUnit or not Unit.isExist(theUnit) then + trigger.action.outText("+++aswG: (drop buoy) unit <" .. conf.name .. "> does not exits", 30) + return + end + + -- we could now make height and speed checks, but dont really do + if not aswGUI.hasDropoffParams(conf) then + trigger.action.outTextForGroup(conf.groupID, "You need to be below xxx knots and yyy ft AGL to drop ASW munitions", 30) + return + end + + -- check that we really have some buoys left + if conf.buoyNum < 1 then + trigger.action.outText("+++aswG: no buoys for <" .. conf.name .. ">.", 30) + return + end + + conf.buoyNum = conf.buoyNum - 1 + + -- do the deed + asw.dropBuoyFrom(theUnit) + trigger.action.outTextForGroup(conf.groupID, "Dropping ASW Buoy...", 30) + + -- wrap up + aswGUI.processWeightFor(conf) + aswGUI.setMenuForUnit(theUnit) +end + +function aswGUI.xHandleTorpedoDropoff(args) + timer.scheduleFunction(aswGUI.handleTorpedoDropoff, args, timer.getTime() + 0.1) +end + +function aswGUI.handleTorpedoDropoff(args) +local conf = args + local theUnit = Unit.getByName(conf.name) + if not theUnit or not Unit.isExist(theUnit) then + trigger.action.outText("+++aswG: (drop torpedo) unit <" .. conf.name .. "> does not exits", 30) + return + end + + -- we could now make height and speed checks, but dont really do + if not aswGUI.hasDropoffParams(conf) then + trigger.action.outTextForGroup(conf.groupID, "You need to be below xxx knots and yyy ft AGL to drop ASW munitions", 30) + return + end + + -- check that we really have some buoys left + if conf.torpedoNum < 1 then + trigger.action.outText("+++aswG: no torpedoes for <" .. conf.name .. ">.", 30) + return + end + + conf.torpedoNum = conf.torpedoNum - 1 + + -- do the deed + asw.dropTorpedoFrom(theUnit) + trigger.action.outTextForGroup(conf.groupID, "Dropping ASW Torpedo...", 30) + + -- wrap up + aswGUI.processWeightFor(conf) + aswGUI.setMenuForUnit(theUnit) +end + +-- +-- Event handling +-- +function aswGUI:onEvent(theEvent) + --env.info("> >ENTER aswGUI:onEvent") + if not theEvent then + trigger.action.outText("+++aswGUI: nil theEvent", 30) + --env.info("< 's unit <" .. name .. "> of type <" .. uType .. "> is not ASW-capable. ASW Types are:", 30) + for idx, aType in pairs(aswGUI.aswCarriers) do + trigger.action.outText(aType,30) + end + end + --env.info("< >Proccing aswGUI:onEvent event <" .. theID .. "") + + -- now let's access it if it was + -- used before + local conf = aswGUI.aswCraft[name] + if not conf then + -- let's init it + conf = aswGUI.initUnit(name) + if not conf then + -- something went wrong, abort + return + end + aswGUI.aswCraft[name] = conf + end + + -- if we get here, theUnit is an asw craft + if theID == 4 or -- land + theID == 3 then -- take off + aswGUI.setMenuForUnit(theUnit) + return + end + + if theID == 20 or -- player enter + theID == 15 then -- birth (server player enter) + + -- reset + aswGUI.resetConf(conf) + -- set menus + aswGUI.setMenuForUnit(theUnit) + end + + if theID == 21 then -- player leave + aswGUI.resetConf(conf) + end + --env.info("< >>ENTER asw GUI start") + if not dcsCommon.libCheck then + trigger.action.outText("cfx aswGUI requires dcsCommon", 30) + return false + end + if not dcsCommon.libCheck("cfx aswGUI", aswGUI.requiredLibs) then + return false + end + + -- read config + aswGUI.readConfigZone() + + -- subscribe to world events + world.addEventHandler(aswGUI) + + -- say Hi + trigger.action.outText("cfx ASW GUI v" .. aswGUI.version .. " started.", 30) + --env.info("<< to watch over", 30) + end +end + +function aswSubs.gatherSubs() + local allCoas = {0, 1, 2} + local subs = {} + for idx, coa in pairs(allCoas) do + local allGroups = coalition.getGroups(coa, 3) -- ships only + for idy, aGroup in pairs(allGroups) do + allUnits = aGroup:getUnits() + for idz, aUnit in pairs(allUnits) do + -- see if this unit is a sub + if aUnit and Unit.isExist(aUnit) then + if (dcsCommon.getUnitAGL(aUnit) < -5) then -- submerged contact. + local contact = {} + contact.theUnit = aUnit + contact.coalition = coa + contact.name = aUnit:getName() + contact.loc = aUnit:getPoint() + subs[contact.name] = contact + end + end + end + end + end + return subs +end + +function aswSubs.boom(args) + + local uName = args.name + local loc = args.loc + local theUnit = Unit.getByName(uName) + if theUnit and theUnit.isExist(theUnit) then + loc = theUnit:getPoint() + end + + trigger.action.explosion(loc, aswSubs.explosionDamage) +end + +function aswSubs.alert(theUnit, theContact) + -- note: we dont need theContact right now + if not theUnit or not Unit.isExist(theUnit) then + return + end + + -- see if this was hit before + local uName = theUnit:getName() + if aswSubs.unitsHit[uName] then return end + + -- mark it as hit + aswSubs.unitsHit[uName] = theContact.name + + -- schedule a few explosions + local args = {} + args.name = uName + args.loc = theUnit:getPoint() + local salvoSize = tonumber(aswSubs.salvoMin) + local varPart = tonumber(aswSubs.salvoMax) - tonumber(aswSubs.salvoMin) + if varPart > 0 then + varPart = dcsCommon.smallRandom(varPart) + salvoSize = salvoSize + varPart + end + + for i=1, tonumber(salvoSize) do + timer.scheduleFunction(aswSubs.boom, args, timer.getTime() + i*2 + 4) + end + + -- theContact has come within crit dist of theUnit + local coa = theUnit:getCoalition() + trigger.action.outTextForCoalition(coa, theUnit:getName() .. " reports " .. salvoSize .. " incoming torpedoes!", 30) +end + +function aswSubs.update() + --env.info("-->Enter asw Subs update") + timer.scheduleFunction(aswSubs.update, {}, timer.getTime() + 1) + + -- get all current subs + local allSubs = aswSubs.gatherSubs() + + -- now iterate all watch groups + for idx, name in pairs(aswSubs.groupsToWatch) do + local theGroup = Group.getByName(name) + if theGroup and Group.isExist(theGroup) then + local groupCoa = theGroup:getCoalition() + if theGroup and Group.isExist(theGroup) then + allUnits = theGroup:getUnits() + for idx, aUnit in pairs(allUnits) do + -- check against all subs + if aUnit and Unit.isExist(aUnit) then + local loc = aUnit:getPoint() + for cName, contact in pairs(allSubs) do + -- attack other side but not neutral + if groupCoa ~= contact.coalition and groupCoa ~= 0 then + -- ok, go check + local dist = dcsCommon.dist(loc, contact.loc) + if dist < aswSubs.critDist then + aswSubs.alert(aUnit, contact) + end + end + end + end + end + end + end + end + --env.info("<--Levae asw Subs update") +end + + +-- +-- Config & start +-- +function aswSubs.readConfigZone() + local theZone = cfxZones.getZoneByName("aswSubsConfig") + if not theZone then + if aswSubs.verbose then + trigger.action.outText("+++aswSubs: no config zone!", 30) + end + theZone = cfxZones.createSimpleZone("aswSubsConfig") + end + + -- read & set defaults + aswSubs.critDist = 4000 + aswSubs.critDist = cfxZones.getNumberFromZoneProperty(theZone, "critDist", aswSubs.critDist) + aswSubs.explosionDamage = 1000 + aswSubs.explosionDamage = cfxZones.getNumberFromZoneProperty(theZone, "explosionDamage", aswSubs.explosionDamage) + + aswSubs.salvoMin, aswSubs.salvoMax = cfxZones.getPositiveRangeFromZoneProperty(theZone, "salvoSize", 4, 4) + --trigger.action.outText("salvo: min <" .. aswSubs.salvoMin .. ">, max <" .. aswSubs.salvoMax .. ">", 30) + local targets = cfxZones.getStringFromZoneProperty(theZone, "targets", "") + local t2 = dcsCommon.string2Array(targets, ",") + for idx, targetName in pairs (t2) do + aswSubs.addWatchgroup(targetName) + end + + if aswSubs.verbose then + trigger.action.outText("+++aswSubs: read config", 30) + end +end + +function aswSubs.start() + if not dcsCommon.libCheck then + trigger.action.outText("cfx aswSubs requires dcsCommon", 30) + return false + end + if not dcsCommon.libCheck("cfx aswSubs", aswSubs.requiredLibs) then + return false + end + + -- read config + aswSubs.readConfigZone() + + -- start the script + aswSubs.update() + + -- all is good + trigger.action.outText("cfx ASW Subs v" .. aswGUI.version .. " started.", 30) + + return true +end + +-- +-- start up aswSubs +-- +if not aswSubs.start() then + trigger.action.outText("cfx aswSubs aborted: missing libraries", 30) + aswSubs = nil +end + diff --git a/modules/aswZones.lua b/modules/aswZones.lua new file mode 100644 index 0000000..64c67af --- /dev/null +++ b/modules/aswZones.lua @@ -0,0 +1,193 @@ +aswZones = {} +aswZones.version = "1.0.0" +aswZones.verbose = false +aswZones.requiredLibs = { + "dcsCommon", -- always + "cfxZones", -- Zones, of course + "asw", -- needs asw module +} +--[[-- + Version History + 1.0.0 - initial version + +--]]-- + +aswZones.ups = 1 -- = once every second +aswZones.zones = {} -- all zones, by name + +function aswZones.addZone(theZone) + if not theZone then + trigger.action.outText("aswZ: nil zone in addZone", 30) + return + end + aswZones.zones[theZone.name] = theZone +end + +function aswZones.getZoneNamed(theName) + if not theName then return nil end + return aswZones[theName] +end + +function aswZones.getClosestASWZoneTo(loc) + local closestZone = nil + local loDist = math.huge + for name, theZone in pairs(aswZones.zones) do + local zp = cfxZones.getPoint(theZone) + local d = dcsCommon.distFlat(zp, loc) + if d < loDist then + loDist = d + closestZone = theZone + end + end + return closestZone, loDist +end + +function aswZones.createASWZone(theZone) + -- get inventory of buoys + theZone.buoyNum = cfxZones.getNumberFromZoneProperty(theZone, "buoyS", -1) -- also used as supply for helos if they land in zone + theZone.torpedoNum = cfxZones.getNumberFromZoneProperty(theZone, "torpedoes", -1) -- also used as supply for helos if they land in zone + + theZone.coalition = cfxZones.getCoalitionFromZoneProperty(theZone, "coalition", 0) + + -- trigger method + theZone.aswTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "triggerMethod", "change") + if cfxZones.hasProperty(theZone, "aswTriggerMethod") then + theZone.aswTriggerMethod = cfxZones.getStringFromZoneProperty(theZone, "aswTriggerMethod", "change") + end + + if cfxZones.hasProperty(theZone, "buoy?") then + theZone.buoyFlag = cfxZones.getStringFromZoneProperty(theZone, "buoy?", "none") + theZone.lastBuoyValue = cfxZones.getFlagValue(theZone.buoyFlag, theZone) + end + + if cfxZones.hasProperty(theZone, "torpedo?") then + theZone.torpedoFlag = cfxZones.getStringFromZoneProperty(theZone, "torpedo?", "none") + theZone.lastTorpedoValue = cfxZones.getFlagValue(theZone.torpedoFlag, theZone) + end + + if theZone.verbose or aswZones.verbose then + trigger.action.outText("+++aswZ: new asw zone <" .. theZone.name .. ">", 30) + trigger.action.outText("has coalition " .. theZone.coalition, 30) + end +end + +-- +-- responding to triggers +-- +function aswZones.dropBuoy(theZone) + if theZone.buoyNum == 0 then + -- we are fresh out. no launch + if theZone.verbose or aswZones.verbose then + trigger.action.outText("+++aswZ: zone <" .. theZone.name .. "> is out of buoys, can't drop", 30) + end + return + end + + local theBuoy = asw.dropBuoyFromZone(theZone) + if theZone.buoyNum > 0 then + theZone.buoyNum = theZone.buoyNum - 1 + end +end + +function aswZones.dropTorpedo(theZone) + if theZone.torpedoNum == 0 then + -- we are fresh out. no launch + if theZone.verbose or aswZones.verbose then + trigger.action.outText("+++aswZ: zone <" .. theZone.name .. "> is out of torpedoes, can't drop", 30) + end + return + end + + local theTorpedo = asw.dropTorpedoFromZone(theZone) + if theZone.torpedoNum > 0 then + theZone.torpedoNum = theZone.torpedoNum - 1 + end +end +-- +-- Update +-- +function aswZones.update() + --env.info("-->Enter asw ZONES update") + -- first, schedule next invocation + timer.scheduleFunction(aswZones.update, {}, timer.getTime() + 1/aswZones.ups) + + for zName, theZone in pairs(aswZones.zones) do + if theZone.buoyFlag and cfxZones.testZoneFlag(theZone, theZone.buoyFlag, theZone.aswTriggerMethod, "lastBuoyValue") then + trigger.action.outText("zone <" .. theZone.name .. "> will now drop a buoy", 30) + aswZones.dropBuoy(theZone) + end + + if theZone.torpedoFlag and cfxZones.testZoneFlag(theZone, theZone.torpedoFlag, theZone.aswTriggerMethod, "lastTorpedoValue") then + trigger.action.outText("zone <" .. theZone.name .. "> will now drop a TORPEDO", 30) + aswZones.dropTorpedo(theZone) + end + end + + --env.info("<--Leave asw ZONES update") +end + +-- +-- Config & start +-- +function aswZones.readConfigZone() + local theZone = cfxZones.getZoneByName("aswZonesConfig") + if not theZone then + if aswZones.verbose then + trigger.action.outText("+++aswZ: no config zone!", 30) + end + theZone = cfxZones.createSimpleZone("aswZonesConfig") + end + aswZones.verbose = theZone.verbose + + -- set defaults, later do the reading + + + if aswZones.verbose then + trigger.action.outText("+++aswZ: read config", 30) + end +end + +function aswZones.start() + if not dcsCommon.libCheck then + trigger.action.outText("cfx aswZones requires dcsCommon", 30) + return false + end + if not dcsCommon.libCheck("cfx aswZones", aswZones.requiredLibs) then + return false + end + + -- read config + aswZones.readConfigZone() + + -- read zones + local attrZones = cfxZones.getZonesWithAttributeNamed("asw") + + -- collect my zones + for k, aZone in pairs(attrZones) do + aswZones.createASWZone(aZone) -- process attributes + aswZones.addZone(aZone) -- add to inventory + end + + -- start update + aswZones.update() + + -- say hi + trigger.action.outText("cfx aswZones v" .. aswZones.version .. " started.", 30) + + return true +end + +-- +-- start up aswZones +-- +if not aswZones.start() then + trigger.action.outText("cfx aswZones aborted: missing libraries", 30) + aswZones = nil +end + +-- add asw.helper with zones that can +-- drop torps +-- have inventory per zone or -1 as infinite +-- have an event when a buoy finds something +-- hav an event when a buoy times out +-- have buoyOut! and torpedoOut! events \ No newline at end of file diff --git a/modules/cfxCommander.lua b/modules/cfxCommander.lua index 2387cda..d7c8f33 100644 --- a/modules/cfxCommander.lua +++ b/modules/cfxCommander.lua @@ -4,7 +4,7 @@ -- *** EXTENDS ZONES: 'pathing' attribute -- cfxCommander = {} -cfxCommander.version = "1.1.2" +cfxCommander.version = "1.1.3" --[[-- VERSION HISTORY - 1.0.5 - createWPListForGroupToPointViaRoads: detect no road found - 1.0.6 - build in more group checks in assign wp list @@ -27,6 +27,9 @@ cfxCommander.version = "1.1.2" - 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 + --]]-- cfxCommander.requiredLibs = { @@ -199,7 +202,8 @@ function cfxCommander.doScheduledTask(data) end local theGroup = data.group if not theGroup then return end - if not theGroup.isExist 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) @@ -290,7 +294,7 @@ function cfxCommander.assignWPListToGroup(group, wpList, delay) group = Group.getByName(group) end if not group then return end - if not group:isExist() then return end + if not Group.isExist(group) then return end local theTask = cfxCommander.buildTaskFromWPList(wpList) local ctrl = group:getController() @@ -427,7 +431,6 @@ function cfxCommander.makeGroupGoTherePreferringRoads(group, there, speed, delay if oRide and oRide.pathing == "offroad" then -- yup, override road preference cfxCommander.makeGroupGoThere(group, there, speed, "Off Road", delay) - --trigger.action.outText("pathing: override offroad") return end end @@ -441,7 +444,7 @@ end function cfxCommander.makeGroupHalt(group, delay) if not group then return end - if not group:isExist() 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) diff --git a/modules/cfxGroundTroops.lua b/modules/cfxGroundTroops.lua index 552fe99..de487c5 100644 --- a/modules/cfxGroundTroops.lua +++ b/modules/cfxGroundTroops.lua @@ -21,84 +21,90 @@ cfxGroundTroops.requiredLibs = { -- module and addTroopsToPool to have them then managed by this -- module -cfxGroundTroops.deployedTroops = {} +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 +--[[-- + 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 --- an entry into the deployed troop 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 sez 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 + 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) --- + + 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 @@ -129,7 +135,7 @@ function cfxGroundTroops.readConfigZone() if cfxGroundTroops.verbose then trigger.action.outText("***gndT: NO config zone!", 30) end - return + theZone = cfxZones.createSimpleZone("groundTroopsConfig") end -- ok, for each property, load it if it exists @@ -198,9 +204,10 @@ 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() then + if not Group.isExist(group) then trigger.action.outText("+++gndT: troup don't exist, dropping", 30) return end @@ -214,10 +221,11 @@ function cfxGroundTroops.makeTroopsEngageEnemies(troop) -- we lerp to 2/3 of enemy location there = dcsCommon.vLerp(from, there, 0.66) - local speed = 10 -- m/s = 10 km/h + 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 @@ -225,7 +233,7 @@ end function cfxGroundTroops.makeTroopsEngageZone(troop) local group = troop.group if not group:isExist() then - trigger.action.outText("+++gndT: make engage zone: troops do not exist, exiting", 30) + trigger.action.outText("+++gndT: make troops engage zone: troops do not exist, exiting", 30) return end @@ -234,23 +242,15 @@ function cfxGroundTroops.makeTroopsEngageZone(troop) if not from then return end -- the group died local there = enemyZone.point -- access zone position if not there then return end - - -- we lerp to 102% of enemy location to force overshoot and engagement - --there = dcsCommon.vLerp(from, there, 1.02) - + local speed = 14 -- m/s; 10 m/s = 36 km/h - -- we prefer going over roads since we don't know - -- what is there -- 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) - -- no attack command since we don't know what is there - -- but mayhaps we should issue weapons free? - -- we'll soon test that by sticking in a troop on the way - --- local attask = cfxCommander.createAttackGroupCommand(enemies) --- cfxCommander.scheduleTaskForGroup(group, attask, 0.5) + + -- remember that we have issued a move order + troop.moving = true end function cfxGroundTroops.switchToOffroad(troops) @@ -301,13 +301,11 @@ function cfxGroundTroops.updateZoneAttackers(troop) local newTargetZone = cfxGroundTroops.getClosestEnemyZone(troop) if not newTargetZone then -- all target zones are friendly, go to guard mode --- trigger.action.outTextForCoalition(troop.side, troop.name .. " holding position", 30) troop.orders = "guard" return end if newTargetZone ~= troop.destination then --- trigger.action.outTextForCoalition(troop.side, troop.name .. " enroute to " .. newTargetZone.name, 30) troop.destination = newTargetZone cfxGroundTroops.makeTroopsEngageZone(troop) troop.lastOrderDate = timer.getTime() @@ -315,23 +313,37 @@ function cfxGroundTroops.updateZoneAttackers(troop) 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 - + -- if we get here, we need no change end --- attackers simply travel to their destination, and then switch to +-- 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 @@ -613,21 +625,18 @@ 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 = {} + 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 - -- 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 -- work with this groop according to its orders cfxGroundTroops.updateTroops(troop) --- trigger.action.outText("+++ updated troops " .. troop.name, 30) -- since group is alive remember it for next loop - --table.insert(liveTroops, troop) - liveTroops[idx] = troop -- do NOT use insert as we have indexed table + 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 @@ -958,6 +967,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.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 @@ -985,7 +995,6 @@ function cfxGroundTroops.addGroundTroopsToPool(troops) -- troops MUST be a table if cfxGroundTroops.maxManagedTroops > 0 and dcsCommon.getSizeOfTable(cfxGroundTroops.deployedTroops) >= cfxGroundTroops.maxManagedTroops then -- we need to queue table.insert(cfxGroundTroops.troopQueue, troops) - -- trigger.action.outText("enqued " .. troops.group:getName() .. " at pos ".. #cfxGroundTroops.troopQueue ..", manage cap surpassed.", 30) else -- add to deployed set cfxGroundTroops.deployedTroops[troops.group:getName()] = troops diff --git a/modules/cfxHeloTroops.lua b/modules/cfxHeloTroops.lua index 4d95c8c..4f25144 100644 --- a/modules/cfxHeloTroops.lua +++ b/modules/cfxHeloTroops.lua @@ -1,37 +1,43 @@ cfxHeloTroops = {} -cfxHeloTroops.version = "2.3.0" +cfxHeloTroops.version = "2.4.0" cfxHeloTroops.verbose = false cfxHeloTroops.autoDrop = true cfxHeloTroops.autoPickup = false cfxHeloTroops.pickupRange = 100 -- meters -- +--[[-- + VERSION HISTORY + 1.1.3 - repaired forgetting 'wait-' when loading/disembarking + 1.1.4 - corrected coalition bug in deployTroopsFromHelicopter + 2.0.0 - added weight change when troops enter and leave the helicopter + - idividual troop capa max per helicopter + 2.0.1 - lib loader verification + - uses dcsCommon.isTroopCarrier(theUnit) + 2.0.2 - can now deploy from spawners with "requestable" attribute + 2.1.0 - supports config zones + - check spawner legality by types + - updated types to include 2.7.6 additions to infantry + - updated types to include stinger/manpads + 2.2.0 - minor maintenance (dcsCommon) + - (re?) connected readConfigZone (wtf?) + - persistence support + - made legalTroops entrirely optional and defer to dcsComon else + 2.3.0 - interface with owned zones and playerScore when + - combat-dropping troops into non-owned owned zone. + - prevent auto-load from pre-empting loading csar troops + 2.3.1 - added ability to self-define troopCarriers via config + 2.4.0 - added missing support for attackZone orders (destination) + - eliminated cfxPlayer module import and all dependencies + - added support for groupTracker / limbo + - removed restriction to only apply to helicopters in anticipation of the C-130 Hercules appearing in the game + +--]]-- -- --- VERSION HISTORY --- 1.1.3 -- repaired forgetting 'wait-' when loading/disembarking --- 1.1.4 -- corrected coalition bug in deployTroopsFromHelicopter --- 2.0.0 -- added weight change when troops enter and leave the helicopter - -- idividual troop capa max per helicopter --- 2.0.1 -- lib loader verification --- -- uses dcsCommon.isTroopCarrier(theUnit) --- 2.0.2 -- can now deploy from spawners with "requestable" attribute --- 2.1.0 -- supports config zones --- -- check spawner legality by types --- -- updated types to include 2.7.6 additions to infantry --- -- updated types to include stinger/manpads --- 2.2.0 -- minor maintenance (dcsCommon) --- -- (re?) connected readConfigZone (wtf?) --- -- persistence support --- -- made legalTroops entrirely optional and defer to dcsComon else --- 2.3.0 -- interface with owned zones and playerScore when --- -- combat-dropping troops into non-owned owned zone. --- -- prevent auto-load from pre-empting loading csar troops --- --- cfxHeloTroops -- a module to pick up and drop infantry. Can be used with any helo, --- might be used to configure to only certain --- currently only supports a single helicopter per group --- only helicopters that can transport troops will have this feature --- Copyright (c) 2021, 2022 by Christian Franz and cf/x AG --- +-- cfxHeloTroops -- a module to pick up and drop infantry. +-- Can be used with ANY aircraft, configured by default to be +-- restricted to troop-carrying helicopters. +-- might be configure to apply to any type you want using the +-- configuration zone. cfxHeloTroops.requiredLibs = { @@ -39,17 +45,11 @@ cfxHeloTroops.requiredLibs = { -- pretty stupid to check for this since we -- need common to invoke the check, but anyway "cfxZones", -- Zones, of course - "cfxPlayer", -- player events "cfxCommander", -- to make troops do stuff "cfxGroundTroops", -- generic when dropping troops } cfxHeloTroops.unitConfigs = {} -- all configs are stored by unit's name -cfxHeloTroops.myEvents = {3, 4, 5} -- 3- takeoff, 4 - land, 5 - crash - --- legalTroops now optional, else check against dcsCommon.typeIsInfantry ---cfxHeloTroops.legalTroops = {"Soldier AK", "Infantry AK", "Infantry AK ver2", "Infantry AK ver3", "Infantry AK Ins", "Soldier M249", "Soldier M4 GRG", "Soldier M4", "Soldier RPG", "Paratrooper AKS-74", "Paratrooper RPG-16", "Stinger comm dsr", "Stinger comm", "Soldier stinger", "SA-18 Igla-S comm", "SA-18 Igla-S manpad", "Igla manpad INS", "SA-18 Igla comm", "SA-18 Igla manpad",} - cfxHeloTroops.troopWeight = 100 -- kg average weight per trooper -- persistence support @@ -59,13 +59,12 @@ function cfxHeloTroops.resetConfig(conf) conf.autoDrop = cfxHeloTroops.autoDrop --if true, will drop troops on-board upon touchdown conf.autoPickup = cfxHeloTroops.autoPickup -- if true will load nearest troops upon touchdown conf.pickupRange = cfxHeloTroops.pickupRange --meters, maybe make per helo? - -- maybe set up max seats by type conf.currentState = -1 -- 0 = landed, 1 = airborne, -1 undetermined conf.troopsOnBoardNum = 0 -- if not 0, we have troops and can spawnm/drop conf.troopCapacity = 8 -- should be depending on airframe + -- troopsOnBoard.name contains name of group + -- the other fields info for troops picked up conf.troopsOnBoard = {} -- table with the following - - conf.troopsOnBoard.name = "***reset***" conf.dropFormation = "circle_out" -- may be chosen later? end @@ -80,7 +79,6 @@ function cfxHeloTroops.createDefaultConfig(theUnit) end - function cfxHeloTroops.getUnitConfig(theUnit) -- will create new config if not existing if not theUnit then trigger.action.outText("+++WARNING: nil unit in get config!", 30) @@ -98,70 +96,6 @@ function cfxHeloTroops.getConfigForUnitNamed(aName) return cfxHeloTroops.unitConfigs[aName] end -function cfxHeloTroops.removeConfigForUnitNamed(aName) - if cfxHeloTroops.unitConfigs[aName] then cfxHeloTroops.unitConfigs[aName] = nil end -end - -function cfxHeloTroops.setState(theUnit, isLanded) - -- called to set the current state of the helicopter (group) - -- currently one helicopter per group max -end - - - --- --- E V E N T H A N D L I N G --- -function cfxHeloTroops.isInteresting(eventID) - -- return true if we are interested in this event, false else - for key, evType in pairs(cfxHeloTroops.myEvents) do - if evType == eventID then return true end - end - return false -end - -function cfxHeloTroops.preProcessor(event) - -- make sure it has an initiator - if not event.initiator then return false end -- no initiator - local theUnit = event.initiator - if not dcsCommon.isPlayerUnit(theUnit) then return false end -- not a player unit - local cat = theUnit:getCategory() - if cat ~= Group.Category.HELICOPTER then return false end - - return cfxHeloTroops.isInteresting(event.id) -end - -function cfxHeloTroops.postProcessor(event) - -- don't do anything -end - -function cfxHeloTroops.somethingHappened(event) - -- when this is invoked, the preprocessor guarantees that - -- it's an interesting event - -- unit is valid and player - -- airframe category is helicopter - - local theUnit = event.initiator - local ID = event.id - - - local myType = theUnit:getTypeName() - - if ID == 4 then - cfxHeloTroops.heloLanded(theUnit) - end - - if ID == 3 then - cfxHeloTroops.heloDeparted(theUnit) - end - - if ID == 5 then - cfxHeloTroops.heloCrashed(theUnit) - end - - cfxHeloTroops.setCommsMenu(theUnit) -end - -- -- -- LANDED @@ -172,9 +106,8 @@ function cfxHeloTroops.loadClosestGroup(conf) local cat = Group.Category.GROUND local unitsToLoad = dcsCommon.getLivingGroupsAndDistInRangeToPoint(p, conf.pickupRange, conf.unit:getCoalition(), cat) - -- now, the groups may contain units that are not for transport. - -- later we can filter this by weight, or other cool stuff - -- for now we simply only troopy with legal type strings + -- groups may contain units that are not for transport. + -- for now we only load troops with legal type strings unitsToLoad = cfxHeloTroops.filterTroopsByType(unitsToLoad) -- now limit the options to the five closest legal groups @@ -190,7 +123,7 @@ end function cfxHeloTroops.heloLanded(theUnit) -- when we have landed, - if not dcsCommon.isTroopCarrier(theUnit) then return end + if not dcsCommon.isTroopCarrier(theUnit, cfxHeloTroops.troopCarriers) then return end local conf = cfxHeloTroops.getUnitConfig(theUnit) conf.unit = theUnit @@ -231,7 +164,7 @@ end -- function cfxHeloTroops.heloDeparted(theUnit) - if not dcsCommon.isTroopCarrier(theUnit) then return end + if not dcsCommon.isTroopCarrier(theUnit, cfxHeloTroops.troopCarriers) then return end -- when we take off, all that needs to be done is to change the state -- to airborne, and then set the status flag @@ -248,17 +181,40 @@ end -- Helo Crashed -- -- -function cfxHeloTroops.heloCrashed(theUnit) - if not dcsCommon.isTroopCarrier(theUnit) then return end - +function cfxHeloTroops.cleanHelo(theUnit) -- clean up local conf = cfxHeloTroops.getUnitConfig(theUnit) conf.unit = theUnit conf.troopsOnBoardNum = 0 -- all dead conf.currentState = -1 -- (we don't know) - -- conf.troopsOnBoardTypes = "" -- no troops, remember? + + -- check if we need to interface with groupTracker + if conf.troopsOnBoard.name and groupTracker then + local theName = conf.troopsOnBoard.name + -- there was (possibly) a group on board. see if it was tracked + local isTracking, numTracking, trackers = groupTracker.groupNameTrackedBy(theName) + + -- if so, remove it from limbo + if isTracking then + for idx, theTracker in pairs(trackers) do + groupTracker.removeGroupNamedFromTracker(theName, theTracker) + if cfxHeloTroops.verbose then + trigger.action.outText("+++Helo: removed group <" .. theName .. "> from tracker <" .. theTracker.name .. ">", 30) + end + end + end + end + conf.troopsOnBoard = {} - cfxHeloTroops.removeComms(conf.unit) +end + +function cfxHeloTroops.heloCrashed(theUnit) + if not dcsCommon.isTroopCarrier(theUnit, cfxHeloTroops.troopCarriers) then return + end + + -- clean up + cfxHeloTroops.cleanHelo(theUnit) + end -- @@ -332,15 +288,19 @@ function cfxHeloTroops.setCommsMenu(theUnit) if not theUnit:isExist() then return end -- we only add this menu to troop carriers - if not dcsCommon.isTroopCarrier(theUnit) then return end + if not dcsCommon.isTroopCarrier(theUnit, cfxHeloTroops.troopCarriers) then + if cfxHeloTroops.verbose then + trigger.action.outText("+++heloT - player unit <" .. theUnit:getName() .. "> type <" .. theUnit:getTypeName() .. "> is not legal troop carrier.", 30) + end + return + end local group = theUnit:getGroup() local id = group:getID() - local conf = cfxHeloTroops.getUnitConfig(theUnit) --cfxHeloTroops.unitConfigs[theUnit:getName()] + local conf = cfxHeloTroops.getUnitConfig(theUnit) conf.id = id; -- we do this ALWAYS to it is current even after a crash conf.unit = theUnit -- link back - --local conf = cfxHeloTroops.getUnitConfig(theUnit) -- ok, first, if we don't have an F-10 menu, create one if not (conf.myMainMenu) then conf.myMainMenu = missionCommands.addSubMenuForGroup(id, 'Airlift Troops') @@ -548,6 +508,7 @@ function cfxHeloTroops.filterTroopsByType(unitsToLoad) end return filteredGroups end + -- -- T O G G L E S -- @@ -580,7 +541,6 @@ function cfxHeloTroops.doToggleConfig(args) end - -- -- Deploying Troops -- @@ -629,7 +589,6 @@ function cfxHeloTroops.doDeployTroops(args) -- set own troops to 0 and erase type string conf.troopsOnBoardNum = 0 conf.troopsOnBoard = {} --- conf.troopsOnBoardTypes = "" conf.troopsOnBoard.name = "***wasdeployed***" -- reset menu @@ -643,11 +602,7 @@ function cfxHeloTroops.deployTroopsFromHelicopter(conf) local unitTypes = {} -- build type names local theUnit = conf.unit local p = theUnit:getPoint() - - --for i=1, scenario.troopSize[theUnit:getName()] do - -- table.insert(unitTypes, "Soldier M4") - --end - + -- split the conf.troopsOnBoardTypes into an array of types unitTypes = dcsCommon.splitString(conf.troopsOnBoard.types, ",") if #unitTypes < 1 then @@ -656,6 +611,9 @@ function cfxHeloTroops.deployTroopsFromHelicopter(conf) local range = conf.troopsOnBoard.range local orders = conf.troopsOnBoard.orders + local dest = conf.troopsOnBoard.destination + local theName = conf.troopsOnBoard.name + if not orders then orders = "guard" end -- order processing: if the orders were pre-pended with "wait-" @@ -671,7 +629,7 @@ function cfxHeloTroops.deployTroopsFromHelicopter(conf) local theCoalition = theUnit:getGroup():getCoalition() -- make it choppers COALITION local theGroup, theData = cfxZones.createGroundUnitsInZoneForCoalition ( theCoalition, - conf.troopsOnBoard.name, -- dcsCommon.uuid("Assault"), -- maybe use config name as loaded from the group + theName, -- group name, may be tracked chopperZone, unitTypes, conf.dropFormation, @@ -682,14 +640,27 @@ function cfxHeloTroops.deployTroopsFromHelicopter(conf) troopData.orders = orders -- always set troopData.side = theCoalition troopData.range = range + troopData.destination = dest -- only for attackzone orders cfxHeloTroops.deployedTroops[theData.name] = troopData - local troop = cfxGroundTroops.createGroundTroops(theGroup, range, orders) -- use default range and orders - -- instead of scheduling tasking in one second, we add to - -- ground troops pool, and the troop pool manager will assign some enemies - cfxGroundTroops.addGroundTroopsToPool(troop) + local troop = cfxGroundTroops.createGroundTroops(theGroup, range, orders) + 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) + -- see if this is tracked by a tracker, and pass them back so + -- they can un-limbo + if groupTracker then + local isTracking, numTracking, trackers = groupTracker.groupNameTrackedBy(theName) + if isTracking then + for idx, theTracker in pairs (trackers) do + groupTracker.addGroupToTracker(theGroup, theTracker) + if cfxHeloTroops.verbose then + trigger.action.outText("+++Helo: un-limbo and tracking group <" .. theName .. "> with tracker <" .. theTracker.name .. ">", 30) + end + end + end + end end @@ -709,21 +680,38 @@ function cfxHeloTroops.doLoadGroup(args) -- get the size conf.troopsOnBoardNum = group:getSize() -- and name - conf.troopsOnBoard.name = group:getName() + local gName = group:getName() + conf.troopsOnBoard.name = gName -- and put it all into the helicopter config - -- now we need to destroy the group. First, remove it from the pool + -- now we need to destroy the group. Let's prepare: + -- if it was tracked, tell tracker to move it to limbo + -- to remember it even if it's destroyed + if groupTracker then + -- only if groupTracker is active + local isTracking, numTracking, trackers = groupTracker.groupTrackedBy(group) + if isTracking then + -- we need to put them in limbo for every tracker + for idx, aTracker in pairs(trackers) do + if cfxHeloTroops.verbose then + trigger.action.outText("+++Helo: moving group <" .. gName .. "> to limbo for tracker <" .. aTracker.name .. ">", 30) + end + groupTracker.moveGroupToLimboForTracker(group, aTracker) + end + end + end + + -- then, remove it from the pool local pooledGroup = cfxGroundTroops.getGroundTroopsForGroup(group) if pooledGroup then -- copy some important info from the troops -- if they are set conf.troopsOnBoard.orders = pooledGroup.orders conf.troopsOnBoard.range = pooledGroup.range - + conf.troopsOnBoard.destination = pooledGroup.destination -- may be nil cfxGroundTroops.removeTroopsFromPool(pooledGroup) trigger.action.outTextForGroup(conf.id, "Team '".. conf.troopsOnBoard.name .."' loaded and has orders <" .. conf.troopsOnBoard.orders .. ">", 30) else - --trigger.action.outTextForGroup(conf.id, "Team '".. conf.troopsOnBoard.name .."' loaded!", 30) if cfxHeloTroops.verbose then trigger.action.outText("+++heloT: ".. conf.troopsOnBoard.name .." was not committed to ground troops", 30) end @@ -731,9 +719,8 @@ function cfxHeloTroops.doLoadGroup(args) -- now simply destroy the group -- we'll re-assemble it when we deploy it - -- we currently can't change the weight of the helicopter -- TODO: add weight changing code - -- TODO: ensure compatibility with CSAR module + -- TODO: ensure compatibility with CSAR module group:destroy() -- now immediately run a GC so this group is removed @@ -783,47 +770,55 @@ function cfxHeloTroops.doSpawnGroup(args) end - --- --- Player event callbacks --- -function cfxHeloTroops.playerChangeEvent(evType, description, player, data) - if evType == "newGroup" then - theUnit = data.primeUnit - cfxHeloTroops.setCommsMenu(theUnit) - +-- +-- handle events +-- +function cfxHeloTroops:onEvent(theEvent) + local theID = theEvent.id + local initiator = theEvent.initiator + if not initiator then return end -- not interested + local theUnit = initiator + local name = theUnit:getName() + -- see if this is a player aircraft + if not theUnit.getPlayerName then return end -- not a player + if not theUnit:getPlayerName() then return end -- not a player + + -- only for helicopters -- overridedden by troop carriers + -- we don't check for cat any more, so any airframe + -- can be used as long as it's ok with isTroopCarrier() + + -- only for troop carriers + if not dcsCommon.isTroopCarrier(theUnit, cfxHeloTroops.troopCarriers) then return end - if evType == "removeGroup" then --- trigger.action.outText("+++Helo Troops: a group disappeared", 30) - -- data.name contains the name of the group. nil the entry in config list, so all - -- troops that group was carrying are gone - -- we must remove the comms menu for this group else we try to add another one to this group later - -- we assume a one-unit group structure, else the following may fail - local conf = cfxHeloTroops.getConfigForUnitNamed(data.primeUnitName) + if theID == 4 then -- land + cfxHeloTroops.heloLanded(theUnit) + end + + if theID == 3 then -- take off + cfxHeloTroops.heloDeparted(theUnit) + end + + if theID == 5 then -- crash + cfxHeloTroops.heloCrashed(theUnit) + end + + if theID == 20 or -- player enter + theID == 15 then -- birth + cfxHeloTroops.cleanHelo(theUnit) + end + + if theID == 21 then -- player leave + cfxHeloTroops.cleanHelo(theUnit) + local conf = cfxHeloTroops.getConfigForUnitNamed(name) if conf then cfxHeloTroops.removeCommsFromConfig(conf) end - return + return end - - if evType == "leave" then - local conf = cfxHeloTroops.getConfigForUnitNamed(player.unitName) - if conf then - cfxHeloTroops.resetConfig(conf) - end - end - - if evType == "unit" then - -- player changed units. almost never in MP, but possible in solo - -- we need to reset the conf so no troops are carried any longer - local conf = cfxHeloTroops.getConfigForUnitNamed(data.oldUnitName) - if conf then - cfxHeloTroops.resetConfig(conf) - end - end - + + cfxHeloTroops.setCommsMenu(theUnit) end -- @@ -883,6 +878,13 @@ function cfxHeloTroops.readConfigZone() cfxHeloTroops.autoPickup = cfxZones.getBoolFromZoneProperty(theZone, "autoPickup", false) cfxHeloTroops.pickupRange = cfxZones.getNumberFromZoneProperty(theZone, "pickupRange", 100) cfxHeloTroops.combatDropScore = cfxZones.getNumberFromZoneProperty(theZone, "combatDropScore", 200) + + -- add own troop carriers + if cfxZones.hasProperty(theZone, "troopCarriers") then + local tc = cfxZones.getStringFromZoneProperty(theZone, "troopCarriers", "UH-1D") + tc = dcsCommon.splitString(tc, ",") + cfxHeloTroops.troopCarriers = dcsCommon.trimArray(tc) + end end -- @@ -955,23 +957,12 @@ function cfxHeloTroops.start() -- start housekeeping cfxHeloTroops.houseKeeping() - -- install callbacks for helo-relevant events - dcsCommon.addEventHandler(cfxHeloTroops.somethingHappened, cfxHeloTroops.preProcessor, cfxHeloTroops.postProcessor) - - -- now iterate through all player groups and install the Assault Troop Menu - allPlayerGroups = cfxPlayerGroups -- cfxPlayerGroups is a global, don't fuck with it! - -- contains per group a player record, use prime unit to access player's unit - for gname, pgroup in pairs(allPlayerGroups) do - local aUnit = pgroup.primeUnit -- get any unit of that group - cfxHeloTroops.setCommsMenu(aUnit) - end - -- now install the new group notifier to install Assault Troops menu - - cfxPlayer.addMonitor(cfxHeloTroops.playerChangeEvent) + world.addEventHandler(cfxHeloTroops) trigger.action.outText("cf/x Helo Troops v" .. cfxHeloTroops.version .. " started", 30) - -- now load all save data and populate map with troops that - -- we deployed last save. + -- persistence: + -- load all save data and populate map with troops that + -- we deployed when we last saved. if persistence then -- sign up for persistence callbacks = {} @@ -990,11 +981,5 @@ if not cfxHeloTroops.start() then cfxHeloTroops = nil end ---[[-- - - interface with spawnable: request troops via comms menu if - - spawnZones defined - - spawners in range and - - spawner auf 'paused' und 'requestable' - ---]]-- + -- TODO: weight when loading troops \ No newline at end of file diff --git a/modules/cfxOwnedZones.lua b/modules/cfxOwnedZones.lua index f5cb3aa..d532a54 100644 --- a/modules/cfxOwnedZones.lua +++ b/modules/cfxOwnedZones.lua @@ -1,5 +1,5 @@ cfxOwnedZones = {} -cfxOwnedZones.version = "1.2.3" +cfxOwnedZones.version = "1.2.4" cfxOwnedZones.verbose = false cfxOwnedZones.announcer = true cfxOwnedZones.name = "cfxOwnedZones" @@ -48,6 +48,7 @@ cfxOwnedZones.name = "cfxOwnedZones" 1.2.1 - fix in load to correctly re-establish all attackers for subsequent save 1.2.2 - redCap! and blueCap! 1.2.3 - fix for persistence bug when not using conquered flag +1.2.4 - pause? and activate? inputs --]]-- @@ -235,6 +236,23 @@ function cfxOwnedZones.addOwnedZone(aZone) aZone.blueCap = cfxZones.getStringFromZoneProperty(aZone, "blueCap!", "none") end + -- pause? and activate? + if cfxZones.hasProperty(aZone, "pause?") then + aZone.pauseFlag = cfxZones.getStringFromZoneProperty(aZone, "pause?", "none") + aZone.lastPauseValue = trigger.misc.getUserFlag(aZone.pauseFlag) + end + + if cfxZones.hasProperty(aZone, "activate?") then + aZone.activateFlag = cfxZones.getStringFromZoneProperty(aZone, "activate?", "none") + aZone.lastActivateValue = trigger.misc.getUserFlag(aZone.activateFlag) + end + + aZone.ownedTriggerMethod = cfxZones.getStringFromZoneProperty(aZone, "triggerMethod", "change") + if cfxZones.hasProperty(aZone, "ownedTriggerMethod") then + aZone.ownedTriggerMethod = cfxZones.getStringFromZoneProperty(aZone, "ownedTriggerMethod", "change") + end + + aZone.unbeatable = cfxZones.getBoolFromZoneProperty(aZone, "unbeatable", false) aZone.untargetable = cfxZones.getBoolFromZoneProperty(aZone, "untargetable", false) aZone.hidden = cfxZones.getBoolFromZoneProperty(aZone, "hidden", false) @@ -855,6 +873,16 @@ function cfxOwnedZones.update() cfxOwnedZones.zoneConquered(aZone, 1, currentOwner) end end + + -- see if pause/unpause was issued + -- note that capping a zone will not change pause status + if aZone.pauseFlag and cfxZones.testZoneFlag(aZone, aZone.pauseFlag, aZone.ownedTriggerMethod, "lastPauseValue") then + aZone.paused = true + end + + if aZone.activateFlag and cfxZones.testZoneFlag(aZone, aZone.activateFlag, aZone.ownedTriggerMethod, "lastActivateValue") then + aZone.paused = false + end -- now, perhaps with their new owner call updateZone() cfxOwnedZones.updateZone(aZone) diff --git a/modules/cfxSpawnZones.lua b/modules/cfxSpawnZones.lua index 514d792..bbc6d1b 100644 --- a/modules/cfxSpawnZones.lua +++ b/modules/cfxSpawnZones.lua @@ -1,5 +1,5 @@ cfxSpawnZones = {} -cfxSpawnZones.version = "1.7.2" +cfxSpawnZones.version = "1.7.4" cfxSpawnZones.requiredLibs = { "dcsCommon", -- common is of course needed for everything -- pretty stupid to check for this since we @@ -20,112 +20,78 @@ cfxSpawnZones.spawnedGroups = {} -- Zones that conform with this requirements spawn toops automatically -- *** DOES NOT EXTEND ZONES *** LINKED OWNER via masterOwner *** -- - +--[[-- -- version history --- 1.3.0 --- - maxSpawn --- - orders --- - range --- 1.3.1 - spawnWithSpawner correct translation of country to coalition --- - createSpawner - corrected reading from properties --- 1.3.2 - createSpawner - correct reading 'owner' from properties, now --- directly reads coalition --- 1.4.0 - checks modules --- - orders 'train' or 'training' - will make the --- ground troops be issued HOLD WEAPS and --- not added to any queue. 'Training' troops --- are target dummies. --- - optional heading attribute --- - typeMult: repeate type this many time (can produce army in one call) --- 1.4.1 - 'requestable' attribute. will automatically set zone to --- - paused, so troops can be produced on call --- - getRequestableSpawnersInRange --- 1.4.2 - target attribute. used for --- - orders: attackZone --- - spawner internally copies name from cfxZone used for spawning (convenience only) --- 1.4.3 - can subscribe to callbacks. currently called when spawnForSpawner is invoked, reason is "spawned" --- - masterOwner to link ownership to other zone --- 1.4.4 - autoRemove flag to instantly start CD and respawn --- 1.4.5 - verify that maxSpawns ~= 0 on initial spawn on start-up --- 1.4.6 - getSpawnerForZoneNamed(aName) --- - nil-trapping orders before testing for 'training' --- 1.4.7 - defaulting orders to 'guard' --- - also accept 'dummy' and 'dummies' as substitute for training --- 1.4.8 - spawnWithSpawner uses getPoint to support linked spawn zones --- - update spawn count on initial spawn --- 1.5.0 - f? support to trigger spawn --- - spawnWithSpawner made string compatible --- 1.5.1 - relaxed baseName and default to dcsCommon.uuid() --- - verbose --- 1.5.2 - activate?, pause? flag --- 1.5.3 - spawn?, spawnUnits? flags --- 1.6.0 - trackwith interface for group tracker --- 1.7.0 - persistence support --- 1.7.1 - improved verbosity --- - spelling check --- 1.7.2 - baseName now can can be set to zone name by issuing "*" --- 1.7.3 - ability to hand off to delicates, useDelicates attribute --- --- new version requires cfxGroundTroops, where they are --- --- How do we recognize a spawn zone? --- contains a "spawner" attribute --- a spawner must also have the following attributes --- - spawner - anything, must be present to signal. put in 'ground' to be able to expand to other types --- - types - type strings, comma separated --- see here: https://github.com/mrSkortch/DCS-miscScripts/tree/master/ObjectDB --- - typeMult - repeat types n times to create really LOT of troops. optional, defaults to 1 --- - country - defaults to 2 (usa) -- see here https://wiki.hoggitworld.com/view/DCS_enum_country --- some important: 0 = Russia, 2 = US, 82 = UN neutral --- country is converted to coalition and then assigned to --- Joint Task Force upon spawn --- - masterOwner - optional name of master cfxZone used to determine whom the surrounding --- territory belongs to. Spwaner will only spawn if the owner coalition is the --- the same as the coalition my own county belongs to. --- if not given, spawner spawns even if inside a zone owned by opposing force --- - baseName - for naming spawned groups - MUST BE UNIQUE!!!! --- --- the following attributes are optional --- - cooldown, defaults to 60 (seconds) after troops are removed from zone, --- then the next group spawns. This means troops will only spawn after --- troops are removed and cooldown timed out --- - autoRemove - instantly removes spwaned troops, will spawn again --- again after colldown --- - formation - default is circle_out; other formations are --- - line - left lo right (west-east) facing north --- - line_V - vertical line, facing north --- - chevron - west-east, point growing to north --- - scattered, random --- - circle, circle_forward (all fact north) --- - circle-in (all face in) --- - circle-out (all face out) --- - grid, square, rect arrayed in optimal grid --- - 2deep, 2cols two columns, deep --- - 2wide 2 columns wide (2 deep) --- - heading in DEGREES (deafult 0 = north ) direction entire group is facing --- - destination - zone name to go to, no destination = stay where you are --- - paused - defaults to false. If present true, spawning will not happen --- you can then manually invoke cfxSpawnZones.spawnWithSpawner(spawner) to --- spawn the troops as they are described in the spawner --- - orders - tell them what to do. "train" makes them dummies, "guard" --- "laze", "wait-laze" etc --- other orders are as defined by cfxGroundTroops, at least --- guard - hold and defend (default) --- laze - laze targets --- wait-xxx for helo troops, stand by until dropped from helo --- attackOwnedZone - seek nearest owned zone and attack --- attackZone - move towards the named cfxZone. will generate error if zone not found --- name of zone to attack is in 'target' attribute --- - target - names a target cfxZone, used for orders. Troops will immediately --- start moving towards that zone if defined and such a zone exists --- - maxSpawns - limit number of spawn cycles. omit or -1 is unlimited --- - requestable - used with heloTroops to determine if spawning can be ordered by --- comms when in range --- respawn currently happens after theSpawn is deleted and cooldown seconds have passed + 1.3.0 + - maxSpawn + - orders + - range + 1.3.1 - spawnWithSpawner correct translation of country to coalition + - createSpawner - corrected reading from properties + 1.3.2 - createSpawner - correct reading 'owner' from properties, now + directly reads coalition + 1.4.0 - checks modules + - orders 'train' or 'training' - will make the + ground troops be issued HOLD WEAPS and + not added to any queue. 'Training' troops + are target dummies. + - optional heading attribute + - typeMult: repeate type this many time (can produce army in one call) + 1.4.1 - 'requestable' attribute. will automatically set zone to + - paused, so troops can be produced on call + - getRequestableSpawnersInRange + 1.4.2 - target attribute. used for + - orders: attackZone + - spawner internally copies name from cfxZone used for spawning (convenience only) + 1.4.3 - can subscribe to callbacks. currently called when spawnForSpawner is invoked, reason is "spawned" + - masterOwner to link ownership to other zone + 1.4.4 - autoRemove flag to instantly start CD and respawn + 1.4.5 - verify that maxSpawns ~= 0 on initial spawn on start-up + 1.4.6 - getSpawnerForZoneNamed(aName) + - nil-trapping orders before testing for 'training' + 1.4.7 - defaulting orders to 'guard' + - also accept 'dummy' and 'dummies' as substitute for training + 1.4.8 - spawnWithSpawner uses getPoint to support linked spawn zones + - update spawn count on initial spawn + 1.5.0 - f? support to trigger spawn + - spawnWithSpawner made string compatible + 1.5.1 - relaxed baseName and default to dcsCommon.uuid() + - verbose + 1.5.2 - activate?, pause? flag + 1.5.3 - spawn?, spawnUnits? flags + 1.6.0 - trackwith interface for group tracker + 1.7.0 - persistence support + 1.7.1 - improved verbosity + - spelling check + 1.7.2 - baseName now can can be set to zone name by issuing "*" + 1.7.3 - ability to hand off to delicates, useDelicates attribute + 1.7.4 - wait-attackZone fixes + + + - types - type strings, comma separated + see here: https://github.com/mrSkortch/DCS-miscScripts/tree/master/ObjectDB + + - country - defaults to 2 (usa) -- see here https://wiki.hoggitworld.com/view/DCS_enum_country + some important: 0 = Russia, 2 = US, 82 = UN neutral + country is converted to coalition and then assigned to + Joint Task Force upon spawn + + - formation - default is circle_out; other formations are + - line - left lo right (west-east) facing north + - line_V - vertical line, facing north + - chevron - west-east, point growing to north + - scattered, random + - circle, circle_forward (all fact north) + - circle-in (all face in) + - circle-out (all face out) + - grid, square, rect arrayed in optimal grid + - 2deep, 2cols two columns, deep + - 2wide 2 columns wide (2 deep) + --]]-- + cfxSpawnZones.allSpawners = {} cfxSpawnZones.callbacks = {} -- signature: cb(reason, group, spawner) - -- -- C A L L B A C K S -- @@ -225,25 +191,26 @@ function cfxSpawnZones.createSpawner(inZone) theSpawner.formation = "circle_out" theSpawner.formation = cfxZones.getStringFromZoneProperty(inZone, "formation", "circle_out") theSpawner.paused = cfxZones.getBoolFromZoneProperty(inZone, "paused", false) + -- orders are always converted to all lower case theSpawner.orders = cfxZones.getStringFromZoneProperty(inZone, "orders", "guard"):lower() - --theSpawner.orders = cfxZones.getZoneProperty(inZone, "orders") - -- used to assign special orders, default is 'guard', use "laze" to make them laze targets. can be 'wait-' which may auto-convert to 'guard' after pick-up by helo, to be handled outside. + -- used to assign orders, default is 'guard', use "laze" to make them laze targets. can be 'wait-' which may auto-convert to 'guard' after pick-up by helo, to be handled outside. -- use "train" to tell them to HOLD WEAPONS, don't move and don't participate in loop, so we have in effect target dummies -- can also use order 'dummy' or 'dummies' to switch to train if theSpawner.orders:lower() == "dummy" or theSpawner.orders:lower() == "dummies" then theSpawner.orders = "train" end if theSpawner.orders:lower() == "training" then theSpawner.orders = "train" end - theSpawner.range = cfxZones.getNumberFromZoneProperty(inZone, "range", 300) -- if we have a range, for example enemy detection for Lasing or engage range theSpawner.maxSpawns = cfxZones.getNumberFromZoneProperty(inZone, "maxSpawns", -1) -- if there is a limit on how many troops can spawn. -1 = endless spawns theSpawner.requestable = cfxZones.getBoolFromZoneProperty(inZone, "requestable", false) if theSpawner.requestable then theSpawner.paused = true end - theSpawner.target = cfxZones.getStringFromZoneProperty(inZone, "target", "") - if theSpawner.target == "" then -- this is the defaut case - theSpawner.target = nil - end + if cfxZones.hasProperty(inZone, "target") then + theSpawner.target = cfxZones.getStringFromZoneProperty(inZone, "target", "") + if theSpawner.target == "" then -- this is the defaut case + theSpawner.target = nil + end + end if cfxSpawnZones.verbose or inZone.verbose then trigger.action.outText("+++spwn: created spawner for <" .. inZone.name .. ">", 30) @@ -331,10 +298,9 @@ function cfxSpawnZones.verifySpawnOwnership(spawner) if (myCoalition ~= masterZone.owner) then -- can't spawn, surrounding area owned by enemy - --trigger.action.outText("spawner " .. spawner.name .. " - spawn suppressed: area not owned: " .. " master owner is " .. masterZone.owner .. ", we are " .. myCoalition, 30) return false end - --trigger.action.outText("spawner " .. spawner.name .. " good to go: ", 30) + return true end @@ -362,7 +328,6 @@ function cfxSpawnZones.spawnWithSpawner(aSpawner) local theCountry = aSpawner.country local theCoalition = coalition.getCountryCoalition(theCountry) --- trigger.action.outText("+++ spawn: coal <" .. theCoalition .. "> from country <" .. theCountry .. ">", 30) local theGroup, theData = cfxZones.createGroundUnitsInZoneForCoalition ( theCoalition, @@ -374,16 +339,17 @@ function cfxSpawnZones.spawnWithSpawner(aSpawner) aSpawner.theSpawn = theGroup aSpawner.count = aSpawner.count + 1 - -- isnert into collector for persistence + -- insert into collector for persistence local troopData = {} troopData.groupData = theData - troopData.orders = aSpawner.orders -- always set + troopData.orders = aSpawner.orders -- always set troopData.side = theCoalition troopData.target = aSpawner.target -- can be nil! troopData.tracker = theZone.trackWith -- taken from ZONE!!, can be nil troopData.range = aSpawner.range cfxSpawnZones.spawnedGroups[theData.name] = troopData + -- remember: orders are always lower case only if aSpawner.orders and ( aSpawner.orders:lower() == "training" or aSpawner.orders:lower() == "train" ) @@ -403,16 +369,17 @@ function cfxSpawnZones.spawnWithSpawner(aSpawner) cfxGroundTroops.addGroundTroopsToPool(newTroops) -- see if we have defined a target zone as destination + -- and set it accordingly if aSpawner.target then local destZone = cfxZones.getZoneByName(aSpawner.target) if destZone then - newTroops.destination = destZone - cfxGroundTroops.makeTroopsEngageZone(newTroops) + newTroops.destination = destZone else - trigger.action.outText("+++ spawner " .. aSpawner.name .. " has illegal target " .. aSpawner.target .. ". Pausing.", 30) + trigger.action.outText("+++ spawner " .. aSpawner.name .. " has illegal (unknown) target zone <" .. aSpawner.target .. ">. Pausing.", 30) aSpawner.paused = true end - elseif aSpawner.orders == "attackZone" then + elseif aSpawner.orders == "attackzone" then + -- attackZone command but no zone given trigger.action.outText("+++ spawner " .. aSpawner.name .. " has no target but attackZone command. Pausing.", 30) aSpawner.paused = true end @@ -459,7 +426,7 @@ function cfxSpawnZones.handoffTracking(theGroup, theZone) return end local trackerName = theZone.trackWith - --if trackerName == "*" then trackerName = theZone.name end + -- now assemble a list of all trackers if cfxSpawnZones.verbose or theZone.verbose then trigger.action.outText("+++spawner: spawn pass-off: " .. trackerName, 30) diff --git a/modules/cfxZones.lua b/modules/cfxZones.lua index dbfc4a5..05c9b68 100644 --- a/modules/cfxZones.lua +++ b/modules/cfxZones.lua @@ -1,5 +1,5 @@ cfxZones = {} -cfxZones.version = "3.0.2" +cfxZones.version = "3.0.3" -- cf/x zone management module -- reads dcs zones and makes them accessible and mutable @@ -119,6 +119,9 @@ cfxZones.version = "3.0.2" - 3.0.2 - maxRadius for all zones, only differs from radius in polyZones - re-factoring zone-base string processing from messenger module - new processStringWildcards() that does almost all that messenger can +- 3.0.3 - new getLinkedUnit() +- 3.0.4 - new createRandomPointOnZoneBoundary() +- 3.0.5 - getPositiveRangeFromZoneProperty() now also supports upper bound (optional) --]]-- @@ -392,6 +395,17 @@ function cfxZones.createRandomPointInsideBounds(bounds) return cfxZones.createPoint(x, 0, z) end +function cfxZones.createRandomPointOnZoneBoundary(theZone) + if not theZone then return nil end + if theZone.isPoly then + local loc, dx, dy = cfxZones.createRandomPointInPolyZone(theZone, true) + return loc, dx, dy + else + local loc, dx, dy = cfxZones.createRandomPointInCircleZone(theZone, true) + return loc, dx, dy + end +end + function cfxZones.createRandomPointInZone(theZone) if not theZone then return nil end if theZone.isPoly then @@ -408,7 +422,7 @@ function cfxZones.randomPointInZone(theZone) return loc, dx, dy end -function cfxZones.createRandomPointInCircleZone(theZone) +function cfxZones.createRandomPointInCircleZone(theZone, onEdge) if not theZone.isCircle then trigger.action.outText("+++Zones: warning - createRandomPointInCircleZone called for non-circle zone <" .. theZone.name .. ">", 30) return {x=theZone.point.x, y=0, z=theZone.point.z} @@ -417,7 +431,10 @@ function cfxZones.createRandomPointInCircleZone(theZone) -- ok, let's first create a random percentage value for the new radius -- now lets get a random degree local degrees = math.random() * 2 * 3.14152 -- radiants. - local r = theZone.radius * math.random() + local r = theZone.radius + if not onEdge then + r = r * math.random() + end local p = cfxZones.getPoint(theZone) -- force update of zone if linked local dx = r * math.cos(degrees) local dz = r * math.sin(degrees) @@ -426,7 +443,7 @@ function cfxZones.createRandomPointInCircleZone(theZone) return {x=px, y=0, z = pz}, dx, dz -- returns loc and offsets to theZone.point end -function cfxZones.createRandomPointInPolyZone(theZone) +function cfxZones.createRandomPointInPolyZone(theZone, onEdge) if not theZone.isPoly then trigger.action.outText("+++Zones: warning - createRandomPointInPolyZone called for non-poly zone <" .. theZone.name .. ">", 30) return cfxZones.createPoint(theZone.point.x, 0, theZone.point.z) @@ -446,6 +463,11 @@ function cfxZones.createRandomPointInPolyZone(theZone) local b = theZone.poly[lineIdxA] local randompercent = math.random() local sourceA = dcsCommon.vLerp (a, b, randompercent) + -- if all we want is a point on an edge, we are done + if onEdge then + local polyPoint = sourceA + return polyPoint, polyPoint.x - p.x, polyPoint.z - p.z -- return loc, dx, dz + end -- now get point on second line a = theZone.poly[lineIdxB] @@ -1962,13 +1984,15 @@ function cfxZones.randomDelayFromPositiveRange(minVal, maxVal) return delay end -function cfxZones.getPositiveRangeFromZoneProperty(theZone, theProperty, default) +function cfxZones.getPositiveRangeFromZoneProperty(theZone, theProperty, default, defaultmax) -- reads property as string, and interprets as range 'a-b'. -- if not a range but single number, returns both for upper and lower --trigger.action.outText("***Zne: enter with <" .. theZone.name .. ">: range for property <" .. theProperty .. ">!", 30) if not default then default = 0 end + if not defaultmax then defaultmax = default end + local lowerBound = default - local upperBound = default + local upperBound = defaultmax local rangeString = cfxZones.getStringFromZoneProperty(theZone, theProperty, "") if dcsCommon.containsString(rangeString, "-") then @@ -1987,12 +2011,12 @@ function cfxZones.getPositiveRangeFromZoneProperty(theZone, theProperty, default else -- bounds illegal - trigger.action.outText("+++Zne: illegal range <" .. rangeString .. ">, using " .. default .. "-" .. default, 30) + trigger.action.outText("+++Zne: illegal range <" .. rangeString .. ">, using " .. default .. "-" .. defaultmax, 30) lowerBound = default - upperBound = default + upperBound = defaultmax end else - upperBound = cfxZones.getNumberFromZoneProperty(theZone, theProperty, default) -- between pulses + upperBound = cfxZones.getNumberFromZoneProperty(theZone, theProperty, defaultmax) -- between pulses lowerBound = upperBound end @@ -2455,11 +2479,18 @@ function cfxZones.getDCSOrigin(aZone) return o end +function cfxZones.getLinkedUnit(theZone) + if not theZone then return nil end + if not theZone.linkedUnit then return nil end + if not Unit.isExist(theZone.linkedUnit) then return nil end + return theZone.linkedUnit +end + function cfxZones.getPoint(aZone) -- always works, even linked, returned point can be reused if aZone.linkedUnit then local theUnit = aZone.linkedUnit -- has a link. is link existing? - if theUnit:isExist() then + if Unit.isExist(theUnit) then -- updates zone position cfxZones.centerZoneOnUnit(aZone, theUnit) local dx = aZone.dx diff --git a/modules/changer.lua b/modules/changer.lua index bd6201d..484eb80 100644 --- a/modules/changer.lua +++ b/modules/changer.lua @@ -1,5 +1,5 @@ changer = {} -changer.version = "1.0.4" +changer.version = "1.0.5" changer.verbose = false changer.ups = 1 changer.requiredLibs = { @@ -14,6 +14,7 @@ changer.changers = {} 1.0.2 - on/off: verbosity 1.0.3 - NOT on/off 1.0.4 - a little bit more conversation + 1.0.5 - fixed a bug in verbosity Transmogrify an incoming signal to an output signal - not @@ -241,7 +242,7 @@ function changer.update() end else if aZone.verbose then - trigger.action.outText("+++chgr: <" .. aZone.name .. "> is paused.") + trigger.action.outText("+++chgr: <" .. aZone.name .. "> is paused.", 30) end end end diff --git a/modules/civAir.lua b/modules/civAir.lua index 99384ce..6c2bf4b 100644 --- a/modules/civAir.lua +++ b/modules/civAir.lua @@ -1,5 +1,5 @@ civAir = {} -civAir.version = "1.5.1" +civAir.version = "1.5.2" --[[-- 1.0.0 initial version 1.1.0 exclude list for airfields @@ -22,6 +22,7 @@ civAir.version = "1.5.1" massive simplifications: always between zoned airfieds exclude list and include list 1.5.1 added depart only and arrive only options for airfields + 1.5.2 fixed bugs inb verbosity --]]-- @@ -181,7 +182,7 @@ function civAir.getTwoAirbases() local filteredAB = civAir.filterAirfields(departAB, civAir.excludeAirfields) -- if none left, error if #filteredAB < 1 then - trigger.action.outText("+++civA: too few departure airfields") + trigger.action.outText("+++civA: too few departure airfields", 30) return nil, nil end @@ -195,7 +196,7 @@ function civAir.getTwoAirbases() -- if one left use it twice, boring flight. if #filteredAB < 1 then - trigger.action.outText("+++civA: too few arrival airfields") + trigger.action.outText("+++civA: too few arrival airfields", 30) return nil, nil end diff --git a/modules/cloneZone.lua b/modules/cloneZone.lua index 865ec9e..5a85717 100644 --- a/modules/cloneZone.lua +++ b/modules/cloneZone.lua @@ -93,6 +93,7 @@ cloneZones.respawnOnGroupID = true - masterOwner "*" convenience shortcut 1.7.1 - useDelicates handOff for delicates - forcedRespawn passes zone instead of verbose + 1.7.2 - onPerimeter attribute --]]-- @@ -361,6 +362,8 @@ function cloneZones.createClonerWithZone(theZone) -- has "Cloner" theZone.rndHeading = cfxZones.getBoolFromZoneProperty(theZone, "rndHeading", false) theZone.onRoad = cfxZones.getBoolFromZoneProperty(theZone, "onRoad", false) + + theZone.onPerimeter = cfxZones.getBoolFromZoneProperty(theZone, "onPerimeter", false) -- check for name scheme and / or identical if cfxZones.hasProperty(theZone, "identical") then @@ -1119,12 +1122,21 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone) -- calculate the entire group's displacement local units = rawData.units - local loc, dx, dy = cfxZones.createRandomPointInZone(spawnZone) -- also supports polygonal zones + local loc, dx, dy + if spawnZone.onPerimeter then + loc, dx, dy = cfxZones.createRandomPointOnZoneBoundary(spawnZone) + else + loc, dx, dy = cfxZones.createRandomPointInZone(spawnZone) -- also supports polygonal zones + end for idx, aUnit in pairs(units) do if not spawnZone.centerOnly then -- *every unit's displacement is randomized - loc, dx, dy = cfxZones.createRandomPointInZone(spawnZone) + if spawnZone.onPerimeter then + loc, dx, dy = cfxZones.createRandomPointOnZoneBoundary(spawnZone) + else + loc, dx, dy = cfxZones.createRandomPointInZone(spawnZone) + end aUnit.x = loc.x aUnit.y = loc.z else @@ -1363,9 +1375,14 @@ function cloneZones.spawnWithTemplateForZone(theZone, spawnZone) -- randomize if enabled if spawnZone.rndLoc then - local loc, dx, dy = cfxZones.createRandomPointInZone(spawnZone) -- also supports polygonal zones - rawData.x = rawData.x + dx - rawData.y = rawData.y + dy + local loc, dx, dy + if spawnZone.onPerimeter then + loc, dx, dy = cfxZones.createRandomPointOnZoneBoundary(spawnZone) + else + loc, dx, dy = cfxZones.createRandomPointInZone(spawnZone) -- also supports polygonal zones + end + rawData.x = rawData.x + dx -- might want to use loc + rawData.y = rawData.y + dy -- directly end if spawnZone.rndHeading then @@ -1963,7 +1980,7 @@ function cloneZones.start() -- to our watchlist for k, aZone in pairs(attrZones) do cloneZones.createClonerWithZone(aZone) -- process attribute and add to zone - cloneZones.addCloneZone(aZone) -- remember it so we can smoke it + cloneZones.addCloneZone(aZone) end -- update all cloners and spawned clones from file diff --git a/modules/csarManager2.lua b/modules/csarManager2.lua index c788958..d57fb3f 100644 --- a/modules/csarManager2.lua +++ b/modules/csarManager2.lua @@ -49,13 +49,15 @@ csarManager.ups = 1 - integration with playerScore - score global and per-mission - isCSARTarget API + - 2.2.1 - added troopCarriers attribute to config + - passes own troop carriers to dcsCommin.isTroopCarrier() --]]-- -- modules that need to be loaded BEFORE I run csarManager.requiredLibs = { "dcsCommon", -- common is of course needed for everything - "cfxZones", -- zones management foc CSAR and CSAR Mission zones + "cfxZones", -- zones management for CSAR and CSAR Mission zones "cfxPlayer", -- player monitoring and group monitoring "nameStats", -- generic data module for weight "cargoSuper", @@ -385,7 +387,7 @@ end function csarManager.heloLanded(theUnit) -- when we have landed, - if not dcsCommon.isTroopCarrier(theUnit) then return end + if not dcsCommon.isTroopCarrier(theUnit, csarManager.troopCarriers) then return end local conf = csarManager.getUnitConfig(theUnit) conf.unit = theUnit local theGroup = theUnit:getGroup() @@ -532,7 +534,7 @@ end -- -- function csarManager.heloDeparted(theUnit) - if not dcsCommon.isTroopCarrier(theUnit) then return end + if not dcsCommon.isTroopCarrier(theUnit, csarManager.troopCarriers) then return end -- if we have timed extractions (i.e. not instantaneous), -- then we need to check if we take off after the timer runs out @@ -555,7 +557,7 @@ end -- function csarManager.heloCrashed(theUnit) - if not dcsCommon.isTroopCarrier(theUnit) then return end + if not dcsCommon.isTroopCarrier(theUnit, csarManager.troopCarriers) then return end -- problem: this isn't called on network games. -- clean up @@ -573,7 +575,7 @@ end function csarManager.airframeCrashed(theUnit) -- called from airframe manager - if not dcsCommon.isTroopCarrier(theUnit) then return end + if not dcsCommon.isTroopCarrier(theUnit, csarManager.troopCarriers) then return end local conf = csarManager.getUnitConfig(theUnit) conf.unit = theUnit local theGroup = theUnit:getGroup() @@ -584,7 +586,7 @@ end function csarManager.airframeDitched(theUnit) -- called from airframe manager - if not dcsCommon.isTroopCarrier(theUnit) then return end + if not dcsCommon.isTroopCarrier(theUnit, csarManager.troopCarriers) then return end local conf = csarManager.getUnitConfig(theUnit) conf.unit = theUnit @@ -652,7 +654,7 @@ function csarManager.setCommsMenu(theUnit) -- we only add this menu to helicopter troop carriers -- will also filter out all non-helicopters as nice side effect - if not dcsCommon.isTroopCarrier(theUnit) then return end + if not dcsCommon.isTroopCarrier(theUnit, csarManager.troopCarriers) then return end local group = theUnit:getGroup() local id = group:getID() @@ -822,7 +824,7 @@ end function csarManager.playerChangeEvent(evType, description, player, data) if evType == "newGroup" then local theUnit = data.primeUnit - if not dcsCommon.isTroopCarrier(theUnit) then return end + if not dcsCommon.isTroopCarrier(theUnit, csarManager.troopCarriers) then return end csarManager.setCommsMenu(theUnit) -- allocates new config -- trigger.action.outText("+++csar: added " .. theUnit:getName() .. " to comms menu", 30) @@ -939,7 +941,7 @@ function csarManager.update() -- every second local uID = uGroup:getID() local uSide = aUnit:getCoalition() local agl = dcsCommon.getUnitAGL(aUnit) - if dcsCommon.isTroopCarrier(aUnit) then + if dcsCommon.isTroopCarrier(aUnit, csarManager.troopCarriers) then -- scan through all available csar missions to see if we are close -- enough to trigger comms for idx, csarMission in pairs (csarManager.openMissions) do @@ -1277,7 +1279,7 @@ function csarManager.readConfigZone() if cfxZones.hasProperty(theZone, "csarDelivered!") then csarManager.csarDelivered = cfxZones.getStringFromZoneProperty(theZone, "csarDelivered!", "*") - --trigger.action.outText("+++csar: will bang csarDelivered: <" .. csarManager.csarDelivered .. ">", 30) + end csarManager.rescueRadius = cfxZones.getNumberFromZoneProperty(theZone, "rescueRadius", 70) --70 -- must land within 50m to rescue @@ -1293,6 +1295,19 @@ function csarManager.readConfigZone() csarManager.actionSound = cfxZones.getStringFromZoneProperty(theZone, "actionSound", "Quest Snare 3.wav") csarManager.vectoring = cfxZones.getBoolFromZoneProperty(theZone, "vectoring", true) + -- add own troop carriers + if cfxZones.hasProperty(theZone, "troopCarriers") then + local tc = cfxZones.getStringFromZoneProperty(theZone, "troopCarriers", "UH-1D") + tc = dcsCommon.splitString(tc, ",") + csarManager.troopCarriers = dcsCommon.trimArray(tc) + if csarManager.verbose then + trigger.action.outText("+++casr: redefined troop carriers to types:", 30) + for idx, aType in pairs(csarManager.troopCarriers) do + trigger.action.outText(aType, 30) + end + end + end + if csarManager.verbose then trigger.action.outText("+++csar: read config", 30) end diff --git a/modules/dcsCommon.lua b/modules/dcsCommon.lua index 06ea23a..782da32 100644 --- a/modules/dcsCommon.lua +++ b/modules/dcsCommon.lua @@ -1,5 +1,5 @@ dcsCommon = {} -dcsCommon.version = "2.8.1" +dcsCommon.version = "2.8.2" --[[-- VERSION HISTORY 2.2.6 - compassPositionOfARelativeToB - clockPositionOfARelativeToB @@ -127,6 +127,18 @@ dcsCommon.version = "2.8.1" - processStringWildcards() - new wildArrayContainsString() - fix for stringStartsWith oddity with aircraft types + 2.8.2 - better fixes for string.find() in stringStartsWith and containsString + - dcsCommon.isTroopCarrier(theUnit, carriers) new carriers optional param + - better guards for getUnitAlt and getUnitAGL + - new newPointAtDegreesRange() + - new newPointAtAngleRange() + - new isTroopCarrierType() + - stringStartsWith now supports case insensitive match + - isTroopCarrier() supports 'any' and 'all' + - made getEnemyCoalitionFor() more resilient + - fix to smallRandom for negative numbers + - isTroopCarrierType uses wildArrayContainsString + --]]-- -- dcsCommon is a library of common lua functions @@ -139,7 +151,7 @@ dcsCommon.version = "2.8.1" -- globals dcsCommon.cbID = 0 -- callback id for simple callback scheduling - dcsCommon.troopCarriers = {"Mi-8MT", "UH-1H", "Mi-24P"} -- Ka-50 and Gazelle can't carry troops + dcsCommon.troopCarriers = {"Mi-8MT", "UH-1H", "Mi-24P"} -- Ka-50, Apache and Gazelle can't carry troops dcsCommon.coalitionSides = {0, 1, 2} -- lookup tables @@ -270,8 +282,12 @@ dcsCommon.version = "2.8.1" -- 50 items (usually some more), and only then one itemis picked from -- that array with a random number that is from a greater range (0..50+) function dcsCommon.smallRandom(theNum) -- adapted from mist, only support ints + theNum = math.floor(theNum) if theNum >= 50 then return math.random(theNum) end - + if theNum < 1 then + trigger.action.outText("smallRandom: invoke with argument < 1 (" .. theNum .. "), using 1", 30) + theNum = 1 + end -- for small randoms (<50) local lowNum, highNum highNum = theNum @@ -886,6 +902,20 @@ dcsCommon.version = "2.8.1" return thePoint, degrees end + function dcsCommon.newPointAtDegreesRange(p1, degrees, radius) + local rads = degrees * 3.14152 / 180 + local p2 = dcsCommon.newPointAtAngleRange(p1, rads, radius) + return p2 + end + + function dcsCommon.newPointAtAngleRange(p1, angle, radius) + local p2 = {} + p2.x = p1.x + radius * math.cos(angle) + p2.y = p1.y + p2.z = p1.z + radius * math.sin(angle) + return p2 + end + -- get group location: get the group's location by -- accessing the fist existing, alive member of the group that it finds function dcsCommon.getGroupLocation(group) @@ -1005,13 +1035,14 @@ dcsCommon.version = "2.8.1" end function dcsCommon.getEnemyCoalitionFor(aCoalition) - if aCoalition == 1 then return 2 end - if aCoalition == 2 then return 1 end if type(aCoalition) == "string" then aCoalition = aCoalition:lower() if aCoalition == "red" then return 2 end if aCoalition == "blue" then return 1 end + return nil end + if aCoalition == 1 then return 2 end + if aCoalition == 2 then return 1 end return nil end @@ -1818,7 +1849,7 @@ dcsCommon.version = "2.8.1" theUnit.x = theUnit.x + cx -- MOVE BACK theUnit.y = theUnit.y + cy - -- may also want to increase heading by degreess + -- may also want to increase heading by degrees theUnit.heading = theUnit.heading + rads end end @@ -1840,7 +1871,7 @@ dcsCommon.version = "2.8.1" theUnit.x = theUnit.x + cx -- MOVE BACK theUnit.y = theUnit.y + cy - -- may also want to increase heading by degreess + -- may also want to increase heading by degrees theUnit.heading = theUnit.heading + rads -- now kill psi if it existed before -- theUnit.psi = nil @@ -2011,29 +2042,30 @@ end --trigger.action.outText("wildACS: theString = <" .. theString .. ">, theArray contains <" .. #theArray .. "> elements", 30) local wildIn = dcsCommon.stringEndsWith(theString, "*") - if wildIn then dcsCommon.removeEnding(thestring, "*") end - for i = 1, #theArray do - local theElement = theArray[i] - if caseSensitive then theElement = string.upper(theElement) end + if wildIn then dcsCommon.removeEnding(theString, "*") end + for idx, theElement in pairs(theArray) do -- i = 1, #theArray do + --local theElement = theArray[i] + --trigger.action.outText("test e <" .. theElement .. "> against s <" .. theString .. ">", 30) + if not caseSensitive then theElement = string.upper(theElement) end local wildEle = dcsCommon.stringEndsWith(theElement, "*") if wildEle then theElement = dcsCommon.removeEnding(theElement, "*") end --trigger.action.outText("matching s=<" .. theString .. "> with e=<" .. theElement .. ">", 30) if wildEle and wildIn then -- both end on wildcards, partial match for both - if dcsCommon.stringStartsWith(theElement. theString) then return true end + if dcsCommon.stringStartsWith(theElement, theString) then return true end if dcsCommon.stringStartsWith(theString, theElement) then return true end --trigger.action.outText("match e* with s* failed.", 30) elseif wildEle then -- Element is a wildcard, partial match if dcsCommon.stringStartsWith(theString, theElement) then return true end - --trigger.action.outText("match e* with s failed.", 30) + --trigger.action.outText("startswith - match e* <" .. theElement .. "> with s <" .. theString .. "> failed.", 30) elseif wildIn then -- theString is a wildcard. partial match - if dcsCommon.stringStartsWith(theElement. theString) then return true end + if dcsCommon.stringStartsWith(theElement, theString) then return true end --trigger.action.outText("match e with s* failed.", 30) else -- standard: no wildcards, full match - if theArray[i] == theString then return true end + if theElement == theString then return true end --trigger.action.outText("match e with s (straight) failed.", 30) end @@ -2165,13 +2197,22 @@ end return false end - function dcsCommon.stringStartsWith(theString, thePrefix) + function dcsCommon.stringStartsWith(theString, thePrefix, caseInsensitive) if not theString then return false end if not thePrefix then return false end + if not caseInsensitive then caseInsensitive = false end + if caseInsensitive then + theString = string.upper(theString) + thePrefix = string.upper(theString) + end -- new code because old 'string.find' had some really -- strange results with aircraft types. Prefix "A-10" did not -- match string "A-10A" etc. + + -- superseded: string.find (s, pattern [, init [, plain]]) solves the problem + + --[[ local pl = string.len(thePrefix) if pl > string.len(theString) then return false end if pl < 1 then return false end @@ -2184,11 +2225,12 @@ end end return true ---[[-- trigger.action.outText("---- OK???", 30) +--]]-- trigger.action.outText("---- OK???", 30) -- strange stuff happening with some strings, let's investigate - - local res = string.find(theString, thePrefix) == 1 + local i, j = string.find(theString, thePrefix, 1, true) + return (i == 1) +--[[-- if res then trigger.action.outText("startswith: <" .. theString .. "> pre <" .. thePrefix .. "> --> YES", 30) else @@ -2222,7 +2264,7 @@ end what = string.upper(what) end if inString == what then return true end -- when entire match - return string.find(inString, what) + return string.find(inString, what, 1, true) -- 1, true means start at 1, plaintext end function dcsCommon.bool2Text(theBool) @@ -2441,6 +2483,7 @@ end end function dcsCommon.markPointWithSmoke(p, smokeColor) + if not smokeColor then smokeColor = 0 end local x = p.x local z = p.z -- do NOT change the point directly -- height-correct @@ -2628,17 +2671,35 @@ function dcsCommon.isSceneryObject(theUnit) return theUnit.getCoalition == nil -- scenery objects do not return a coalition end -function dcsCommon.isTroopCarrier(theUnit) - -- return true if conf can carry troups - if not theUnit then return false end - local uType = theUnit:getTypeName() - if dcsCommon.arrayContainsString(dcsCommon.troopCarriers, uType) then +function dcsCommon.isTroopCarrierType(theType, carriers) + if not theType then return false end + if not carriers then carriers = dcsCommon.troopCarriers + end + -- remember that arrayContainsString is case INsensitive by default + if dcsCommon.wildArrayContainsString(carriers, theType) then -- may add additional tests before returning true return true end + + -- see if user wanted 'any' or 'all' supported + if dcsCommon.arrayContainsString(carriers, "any") then + return true + end + + if dcsCommon.arrayContainsString(carriers, "all") then + return true + end + return false end +function dcsCommon.isTroopCarrier(theUnit, carriers) + -- return true if conf can carry troups + if not theUnit then return false end + local uType = theUnit:getTypeName() + return dcsCommon.isTroopCarrierType(uType, carriers) +end + function dcsCommon.isPlayerUnit(theUnit) -- new patch. simply check if getPlayerName returns something if not theUnit then return false end @@ -2664,14 +2725,14 @@ end function dcsCommon.getUnitAlt(theUnit) if not theUnit then return 0 end - if not theUnit:isExist() then return 0 end + if not Unit.isExist(theUnit) then return 0 end -- safer local p = theUnit:getPoint() return p.y end function dcsCommon.getUnitAGL(theUnit) if not theUnit then return 0 end - if not theUnit:isExist() then return 0 end + if not Unit.isExist(theUnit) then return 0 end -- safe fix local p = theUnit:getPoint() local alt = p.y local loc = {x = p.x, y = p.z} diff --git a/modules/groupTrackers.lua b/modules/groupTrackers.lua index 2db6007..fded185 100644 --- a/modules/groupTrackers.lua +++ b/modules/groupTrackers.lua @@ -29,9 +29,22 @@ groupTracker.trackers = {} - numUnits output - persistence 1.2.1 - allGone! bug removed + 1.2.2 - new groupTrackedBy() method + - limbo for storing a unit in limbo so it is + - not counted as missing when being transported --]]-- +-- 'limbo' +-- is a special storage in tracker indexed by name that is used +-- to temporarily suspend a groups tracking while it's not in +-- the mission, e.g. because it's being transported by heloTroops +-- in limbo, only the number of units is preserved +-- addGroup will automatically move a group back from limbo +-- to move into limbo, you must use moveGroupToLimboForTracker +-- to remove a group in limbo, use removeGroupNamedFromTracker +-- + function groupTracker.addTracker(theZone) table.insert(groupTracker.trackers, theZone) end @@ -54,6 +67,8 @@ end -- -- adding a group to a tracker - called by other modules and API -- +-- addGroupToTracker will automatically also move a group from +-- limbo to tracker if it already existed in limbo function groupTracker.addGroupToTracker(theGroup, theTracker) -- check if filtering is enabled for this tracker if theTracker.groupFilter then @@ -83,19 +98,30 @@ function groupTracker.addGroupToTracker(theGroup, theTracker) if gName == theName then exists = true end end end + if not exists then table.insert(theTracker.trackedGroups, theGroup) - - -- now bang/invoke addGroup! - if theTracker.tAddGroup then - cfxZones.pollFlag(theTracker.tAddGroup, "inc", theTracker) + + -- see if we merely transfer group back from limbo + -- to tracked + if theTracker.limbo[theName] then + -- group of that name is in limbo + if theTracker.verbose then + trigger.action.outText("+++gTrk: moving shelved group <" .. theName .. "> back to normal tracking for <" .. theTracker.name .. ">", 30) + end + theTracker.limbo[theName] = nil -- remove from limbo + else + -- now bang/invoke addGroup! + if theTracker.tAddGroup then + cfxZones.pollFlag(theTracker.tAddGroup, "inc", theTracker) + end end end -- now set numGroups if theTracker.tNumGroups then - cfxZones.setFlagValue(theTracker.tNumGroups, #theTracker.trackedGroups, theTracker) + cfxZones.setFlagValue(theTracker.tNumGroups, dcsCommon.getSizeOfTable(theTracker.limbo) + #theTracker.trackedGroups, theTracker) end -- count all units @@ -105,6 +131,9 @@ function groupTracker.addGroupToTracker(theGroup, theTracker) totalUnits = totalUnits + aGroup:getSize() end end + for idx, limboNum in pairs(theTracker.limbo) do + totalUnits = totalUnits + limboNum + end -- update unit count if theTracker.tNumUnits then @@ -113,6 +142,7 @@ function groupTracker.addGroupToTracker(theGroup, theTracker) -- invoke callbacks end + function groupTracker.addGroupToTrackerNamed(theGroup, trackerName) if not trackerName then trigger.action.outText("+++gTrk: nil tracker in addGroupToTrackerNamed", 30) @@ -133,6 +163,93 @@ function groupTracker.addGroupToTrackerNamed(theGroup, trackerName) groupTracker.addGroupToTracker(theGroup, theTracker) end +function groupTracker.moveGroupToLimboForTracker(theGroup, theTracker) + if not theGroup then return end + if not theTracker then return end + if not Group.isExist(theGroup) then return end + + local gName = theGroup:getName() + local filtered = {} + if theTracker.trackedGroups then + for idx, aGroup in pairs(theTracker.trackedGroups) do + if Group.isExist(aGroup) and aGroup:getName() == gName then + -- move this to limbo + theTracker.limbo[gName] = aGroup:getSize() + if theTracker.verbose then + trigger.action.outText("+++gTrk: moved group <" .. gName .. "> to limbo for <" .. theTracker.name .. ">", 30) + end + -- filtered + else + table.insert(filtered, aGroup) + end + end + theTracker.trackedGroups = filtered + end +end + +function groupTracker.removeGroupNamedFromTracker(gName, theTracker) + if not gName then return end + if not theTracker then return end + + local filteredGroups = {} + local foundOne = false + local totalUnits = 0 + if not theTracker.trackedGroups then theTracker.trackedGroups = {} end + for idx, aGroup in pairs(theTracker.trackedGroups) do + if Group.isExist(aGroup) and aGroup:getName() == gName then + -- skip and remember + foundOne = true + else + table.insert(filteredGroups, aGroup) + if Group.isExist(aGroup) then + totalUnits = totalUnits + aGroup:getSize() + end + end + end + -- also check limbo + for limboName, limboNum in pairs (theTracker.limbo) do + if gName == limboName then + -- don't count, but remember that it existed + foundOne = true + if theTracker.verbose then + trigger.action.outText("+++gTrk: removed group <" .. gName .. "> from limbo for <" .. theTracker.name .. ">", 30) + end + else + totalUnits = totalUnits + limboNum + end + end + -- remove from limbo + theTracker.limbo[gName] = nil + + if (not foundOne) and (theTracker.verbose or groupTracker.verbose) then + trigger.action.outText("+++gTrk: Removal Request Note: group <" .. gName .. "> wasn't tracked by <" .. theTracker.name .. ">", 30) + end + + -- remember the new, cleanded set + theTracker.trackedGroups = filteredGroups + + -- update number of tracked units. do it in any case + if theTracker.tNumUnits then + cfxZones.setFlagValue(theTracker.tNumUnits, totalUnits, theTracker) + end + + if foundOne then + if theTracker.verbose or groupTracker.verbose then + trigger.action.outText("+++gTrk: removed group <" .. gName .. "> from tracker <" .. theTracker.name .. ">", 30) + end + + -- now bang/invoke removeGroup! + if theTracker.tRemoveGroup then + cfxZones.pollFlag(theTracker.tRemoveGroup, "inc", theTracker) + end + + -- now set numGroups + if theTracker.tNumGroups then + cfxZones.setFlagValue(theTracker.tNumGroups, dcsCommon.getSizeOfTable(theTracker.limbo) + #theTracker.trackedGroups, theTracker) + end + end +end + function groupTracker.removeGroupNamedFromTrackerNamed(gName, trackerName) local theTracker = groupTracker.getTrackerByName(trackerName) if not theTracker then return end @@ -141,6 +258,8 @@ function groupTracker.removeGroupNamedFromTrackerNamed(gName, trackerName) return end + groupTracker.removeGroupNamedFromTracker(gName, theTracker) +--[[-- local filteredGroups = {} local foundOne = false local totalUnits = 0 @@ -156,6 +275,18 @@ function groupTracker.removeGroupNamedFromTrackerNamed(gName, trackerName) end end end + -- also check limbo + for limboName, limboNum in pairs (theTracker.limbo) do + if gName == limboName then + -- don't count, but remember that it existed + foundOne = true + else + totalUnits = totalUnits + limboNum + end + end + -- remove from limbo + theTracker.limbo[gName] = nil + if (not foundOne) and (theTracker.verbose or groupTracker.verbose) then trigger.action.outText("+++gTrk: Removal Request Note: group <" .. gName .. "> wasn't tracked by <" .. trackerName .. ">", 30) end @@ -180,16 +311,59 @@ function groupTracker.removeGroupNamedFromTrackerNamed(gName, trackerName) -- now set numGroups if theTracker.tNumGroups then - cfxZones.setFlagValue(theTracker.tNumGroups, #theTracker.trackedGroups, theTracker) + cfxZones.setFlagValue(theTracker.tNumGroups, dcsCommon.getSizeOfTable(theTracker.limbo) + #theTracker.trackedGroups, theTracker) end end + --]]-- end +-- groupTrackedBy - return trackers that track group theGroup +-- returns 3 values: true/false (is tracking), number of trackers, array of trackers +function groupTracker.groupNameTrackedBy(theName) + local isTracking = false + + -- now iterate all trackers + local tracking = {} + for idx, aTracker in pairs(groupTracker.trackers) do + -- only look at tracked groups if that tracker has an + -- initialized tracker (lazy init) + if aTracker.trackedGroups then + for idy, aGroup in pairs (aTracker.trackedGroups) do + if Group.isExist(aGroup) and aGroup:getName() == theName then + table.insert(tracking, aTracker) + isTracking = true + end + end + end + + for aName, aNum in pairs(aTracker.limbo) do + if aName == theName then + table.insert(tracking, aTracker) + isTracking = true + end + end + end + + return isTracking, #tracking, tracking +end + +function groupTracker.groupTrackedBy(theGroup) + if not theGroup then return false,0, nil end + if not Group.isExist(theGroup) then return false, 0, nil end + local theName = theGroup:getName() + local isTracking, numTracks, trackers = groupTracker.groupNameTrackedBy(theName) + return isTracking, numTracks, trackers + +end + +-- -- read zone +-- function groupTracker.createTrackerWithZone(theZone) -- init group tracking set theZone.trackedGroups = {} - + theZone.limbo = {} -- name based, for groups that are tracked + -- although technically off the map (helo etc) theZone.trackerMethod = cfxZones.getStringFromZoneProperty(theZone, "method", "inc") if cfxZones.hasProperty(theZone, "trackerMethod") then @@ -264,6 +438,10 @@ function groupTracker.destroyAllInZone(theZone) theGroup:destroy() end end + for aName, aNum in pairs(theZone.limbo) do + theZone.limbo[aName] = 0 -- <1 is special for 'remove me and detect kill on next checkGroups' + end + -- we keep all groups in trackedGroups so we -- generate a host of destroy events when we run through -- checkGroups next @@ -300,12 +478,26 @@ function groupTracker.checkGroups(theZone) end end + + local newLimbo = {} + for aName, aNum in pairs (theZone.limbo) do + if aNum < 1 then + if groupTracker.verbose or theZone.verbose then + trigger.action.outText("+++gTrk: dead group <" .. aName .. "> detected in LIMBO for " .. theZone.name .. ", removing.", 30) + end + else + newLimbo[aName] = aNum + totalUnits = totalUnits + aNum + end + end + theZone.limbo = newLimbo + -- now exchange filtered for current theZone.trackedGroups = filteredGroups --set new group value -- now set numGroups if defined if theZone.tNumGroups then - cfxZones.setFlagValue(theZone.tNumGroups, #filteredGroups, theZone) + cfxZones.setFlagValue(theZone.tNumGroups, dcsCommon.getSizeOfTable(theZone.limbo) + #filteredGroups, theZone) end -- and update unit count if defined @@ -361,7 +553,7 @@ function groupTracker.update() groupTracker.checkGroups(theZone) -- see if we need to bang on empty! - local currCount = #theZone.trackedGroups + local currCount = #theZone.trackedGroups + dcsCommon.getSizeOfTable(theZone.limbo) if theZone.allGoneFlag and currCount == 0 and currCount ~= theZone.lastGroupCount then cfxZones.pollFlag(theZone.allGoneFlag, theZone.trackerMethod, theZone) end diff --git a/modules/messenger.lua b/modules/messenger.lua index 3a96ab6..5d1678b 100644 --- a/modules/messenger.lua +++ b/modules/messenger.lua @@ -1,5 +1,5 @@ messenger = {} -messenger.version = "2.2.0" +messenger.version = "2.2.1" messenger.verbose = false messenger.requiredLibs = { "dcsCommon", -- always @@ -64,6 +64,9 @@ messenger.messengers = {} 2.2.0 - - made dynamic string gen more portable in prep for move to cfxZones - refactoring wildcard processing: moved to cfxZones + 2.2.1 - when messenger is linked to a unit, it can use the linked + unit as reference point for relative wildcards. Always broadcasts to coalition. Can be used to broadcase 'eye in the sky' type information + - fixed verbosity bug --]]-- @@ -318,7 +321,7 @@ function messenger.createMessengerWithZone(theZone) -- flag whose value can be read: to be deprecated if cfxZones.hasProperty(theZone, "messageValue?") then theZone.messageValue = cfxZones.getStringFromZoneProperty(theZone, "messageValue?", "") - trigger.action.outText("+++Msg: Warning - zone <" .. theZone.name .. "> uses 'messageValue' attribute. Migrate to now!") + trigger.action.outText("+++Msg: Warning - zone <" .. theZone.name .. "> uses 'messageValue' attribute. Migrate to now!", 30) end -- time format for new @@ -418,6 +421,20 @@ function messenger.isTriggered(theZone) end trigger.action.outSoundForUnit(ID, fileName) end + elseif cfxZones.getLinkedUnit(theZone) then + -- this only works if the zone is linked to a unit + -- and not using group or unit + -- the linked unit is then used as reference + -- outputs to all of same coalition as the linked + -- unit + local theUnit = cfxZones.getLinkedUnit(theZone) + local ID = theUnit:getID() + local coa = theUnit:getCoalition() + msg = messenger.dynamicUnitProcessing(msg, theZone, theUnit) + if #msg > 0 or theZone.clearScreen then + trigger.action.outTextForCoalition(coa, msg, theZone.duration, theZone.clearScreen) + end + trigger.action.outSoundForCoalition(coa, fileName) else -- out to all if #msg > 0 or theZone.clearScreen then @@ -518,3 +535,8 @@ if not messenger.start() then messenger = nil end + +--[[-- +Ideas: + - when messenger is ties to a unit, that unit can also be base for all relative references. Only checked if neither group nor unit is set +--]]-- diff --git a/modules/persistence.lua b/modules/persistence.lua index 8199596..5284a80 100644 --- a/modules/persistence.lua +++ b/modules/persistence.lua @@ -1,5 +1,5 @@ persistence = {} -persistence.version = "1.0.4" +persistence.version = "1.0.6" persistence.ups = 1 -- once every 1 seconds persistence.verbose = false persistence.active = false @@ -26,6 +26,7 @@ persistence.requiredLibs = { new 'saveNotification" can be off 1.0.4 - new optional 'root' property 1.0.5 - desanitize check on readConfig to early-abort + 1.0.6 - removed potential verbosity bug PROVIDES LOAD/SAVE ABILITY TO MODULES @@ -132,7 +133,7 @@ end function persistence.saveText(theString, fileName, shared, append) if not persistence.active then return false end if not fileName then - trigger.action.outText("+++persistence: saveText without fileName") + trigger.action.outText("+++persistence: saveText without fileName", 30) return false end if not shared then shared = flase end diff --git a/modules/pulseFlags.lua b/modules/pulseFlags.lua index dc511d6..aa5d830 100644 --- a/modules/pulseFlags.lua +++ b/modules/pulseFlags.lua @@ -1,5 +1,5 @@ pulseFlags = {} -pulseFlags.version = "1.3.1" +pulseFlags.version = "1.3.2" pulseFlags.verbose = false pulseFlags.requiredLibs = { "dcsCommon", -- always @@ -37,6 +37,7 @@ pulseFlags.requiredLibs = { returned onStart, defaulting to true - 1.3.0 persistence - 1.3.1 typos corrected + - 1.3.2 removed last pulse's timeID upon entry in doPulse --]]-- @@ -165,6 +166,7 @@ end function pulseFlags.doPulse(args) + local theZone = args[1] -- check if we have been paused. if so, simply -- exit with no new schedule @@ -172,6 +174,8 @@ function pulseFlags.doPulse(args) theZone.pulsing = false return end + -- erase old timerID, since we completed that + theZone.timerID = nil -- do a poll on flags -- first, we only do an initial pulse if zeroPulse is set @@ -268,7 +272,7 @@ function pulseFlags.update() -- pausePulseFlag if cfxZones.testZoneFlag(aZone, aZone.pausePulseFlag, aZone.pulseTriggerMethod, "lastPauseValue") then if pulseFlags.verbose or aZone.verbose then - trigger.action.outText("+++pulF: pausing <" .. aZone.name .. ">", 30) + trigger.action.outText("+++pulF: pausing <" .. aZone.name .. ">", 30) end aZone.pulsePaused = true -- prevents new start if aZone.timerID then diff --git a/modules/valet.lua b/modules/valet.lua index 5d5b634..11e2048 100644 --- a/modules/valet.lua +++ b/modules/valet.lua @@ -1,5 +1,5 @@ valet = {} -valet.version = "1.0.0" +valet.version = "1.0.2" valet.verbose = false valet.requiredLibs = { "dcsCommon", -- always @@ -10,6 +10,9 @@ valet.valets = {} --[[-- Version History 1.0.0 - initial version + 1.0.1 - typos in verbosity corrected + 1.0.2 - also scan birth events + --]]-- function valet.addValet(theZone) @@ -374,16 +377,20 @@ function valet.checkPlayerSpawn(playerName, theUnit) end function valet:onEvent(event) - if event.id == 20 then + if (event.id == 20) or (event.id == 15) then if not event.initiator then return end local theUnit = event.initiator - if not theUnit.getPlayerName then - trigger.action.outText("+++valet: non player event 20(?)", 30) + if not theUnit.getPlayerName then + if event.id == 20 then + trigger.action.outText("+++valet: non player event 20(?)", 30) + end -- 15 (birth can happen to all) return end local pName = theUnit:getPlayerName() if not pName then - trigger.action.outText("+++valet: nil player name on event 20 (!)", 30) + if event.id == 20 then + trigger.action.outText("+++valet: nil player name on event 20 (!)", 30) + end return end @@ -398,7 +405,7 @@ function valet.readConfigZone() local theZone = cfxZones.getZoneByName("valetConfig") if not theZone then if valet.verbose then - trigger.action.outText("+++msgr: NO config zone!", 30) + trigger.action.outText("+++valet: NO config zone!", 30) end theZone = cfxZones.createSimpleZone("valetConfig") end @@ -406,7 +413,7 @@ function valet.readConfigZone() valet.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false) if valet.verbose then - trigger.action.outText("+++msgr: read config", 30) + trigger.action.outText("+++valet: read config", 30) end end diff --git a/sound FX/beacon beep-beep.ogg b/sound FX/beacon beep-beep.ogg new file mode 100644 index 0000000..4dc32ac Binary files /dev/null and b/sound FX/beacon beep-beep.ogg differ diff --git a/sound FX/submarine ping.ogg b/sound FX/submarine ping.ogg new file mode 100644 index 0000000..6058919 Binary files /dev/null and b/sound FX/submarine ping.ogg differ diff --git a/tutorial & demo missions/demo - CSAR of Georgia.miz b/tutorial & demo missions/demo - CSAR of Georgia.miz index b103d74..d41115e 100644 Binary files a/tutorial & demo missions/demo - CSAR of Georgia.miz and b/tutorial & demo missions/demo - CSAR of Georgia.miz differ diff --git a/tutorial & demo missions/demo - Davy Jones' Rocker.miz b/tutorial & demo missions/demo - Davy Jones' Rocker.miz new file mode 100644 index 0000000..68609b3 Binary files /dev/null and b/tutorial & demo missions/demo - Davy Jones' Rocker.miz differ diff --git a/tutorial & demo missions/demo - Owned Zones ME integration.miz b/tutorial & demo missions/demo - Owned Zones ME integration.miz index 1755277..d444602 100644 Binary files a/tutorial & demo missions/demo - Owned Zones ME integration.miz and b/tutorial & demo missions/demo - Owned Zones ME integration.miz differ diff --git a/tutorial & demo missions/demo - helo trooper.miz b/tutorial & demo missions/demo - helo trooper.miz index 98b8526..b42bb12 100644 Binary files a/tutorial & demo missions/demo - helo trooper.miz and b/tutorial & demo missions/demo - helo trooper.miz differ