Version 1.2.3

New ASW modules
Lots of maintenance fixes
This commit is contained in:
Christian Franz 2023-02-15 09:13:52 +01:00
parent 654b782894
commit dc81decee6
28 changed files with 2804 additions and 495 deletions

Binary file not shown.

Binary file not shown.

983
modules/asw.lua Normal file
View File

@ -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 "/<coanum>"
--[[--
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(" <complete, leaving wedge for buoy/contact: <" .. theBuoy.name .. ">/< .. 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(" <<updateBuoy, returning false")
return false
end
-- buoy is alive!
-- see if we need to resmoke
if now > 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(" <iterating buoy.contacts for lost contact done")
-- check if contact is new and add wedge if so
--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 <subname>"/"<coalition>
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(" <<done update buoy for " .. theBuoy.name .. ", returning true")
return true -- true = keep uoy alive
end
function asw.hasFix(contact)
-- determine if this sub can be fixed by the buoys
-- run down all buoys that currently see me
-- sub is only seen by opposing buoys.
local bNum = 0
local pTotal = 0
local deltaB = 0
local bearings = {}
local subName = contact.name
for bName, p in pairs(contact.trackedBy) do
local theBuoy = asw.buoys[bName]
-- CHECK FOR COALITION
-- make bnum to bnumred and bnumblue
if theBuoy.coalition == contact.coalition then
trigger.action.outText("+++Warning: same coa for buoy <" .. theBuoy.name .. "> 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("<m>:<: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
--]]--

597
modules/aswGUI.lua Normal file
View File

@ -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("< <ABEND aswGUI:onEvent: nil event")
return
end
local theID = theEvent.id
if not theID then
trigger.action.outText("+++aswGUI: nil event.ID", 30)
--env.info("< <ABEND aswGUI:onEvent: nil event ID")
return
end
local initiator = theEvent.initiator
if not initiator then
--env.info("< <ABEND aswGUI:onEvent: nil initiator")
return
end -- not interested
local theUnit = initiator
if not Unit.isExist(theUnit) then
trigger.action.outText("+++aswGUI: non-unit event filtred.", 30)
--env.info("< <ABEND aswGUI:onEvent: theUnit does not exist")
end
local name = theUnit:getName()
if not name then
trigger.action.outText("+++aswGUI: unable to access unit name in onEvent, aborting", 30)
--env.info("< <ABEND aswGUI:onEvent: theUnit not a unit/no name")
return
end
-- see if this is a player aircraft
if not theUnit.getPlayerName then
--env.info("< <LEAVE aswGUI:onEvent: not player unit A")
return
end -- not a player
if not theUnit:getPlayerName() then
--env.info("< <LEAVE aswGUI:onEvent: not player unit B")
return
end -- not a player
-- this is a player unit. Is it ASW carrier?
local uType = theUnit:getTypeName()
if not dcsCommon.isTroopCarrierType(uType, aswGUI.aswCarriers) then
if aswGUI.verbose then
trigger.action.outText("+++aswGUI: Player <" .. theUnit:getPlayerName() .. ">'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("< <LEAVE aswGUI:onEvent: not troop carrier")
return
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("< <Proccing complete asw event <" .. theID .. "")
end
--
-- Config & start
--
function aswGUI.readConfigZone()
local theZone = cfxZones.getZoneByName("aswGUIConfig")
if not theZone then
if aswGUI.verbose then
trigger.action.outText("+++aswGUI: no config zone!", 30)
end
theZone = cfxZones.createSimpleZone("aswGUIConfig")
end
aswGUI.verbose = theZone.verbose
-- read & set defaults
if cfxZones.hasProperty(theZone, "aswCarriers") then
local carr = cfxZones.getStringFromZoneProperty(theZone, "aswCarriers", "")
carr = dcsCommon.splitString(carr, ",")
aswGUI.aswCarriers = dcsCommon.trimArray(carr)
end
aswGUI.buoysPerSlot = 10
aswGUI.torpedoesPerSlot = 2
aswGUI.buoyWeight = 50 -- kg, 10x = 500, 20x = 1000
aswGUI.buoyWeight = cfxZones.getNumberFromZoneProperty(theZone, "buoyWeight", aswGUI.buoyWeight)
aswGUI.torpedoWeight = 700 -- kg
aswGUI.torpedoWeight = cfxZones.getNumberFromZoneProperty(theZone, "torpedoWeight", aswGUI.torpedoWeight)
if aswGUI.verbose then
trigger.action.outText("+++aswGUI: read config", 30)
end
end
function aswGUI.start()
--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("<<<asw GUI started")
return true
end
--
-- start up aswZones
--
if not aswGUI.start() then
trigger.action.outText("cfx aswGUI aborted: missing libraries", 30)
aswGUI = nil
end

192
modules/aswSubs.lua Normal file
View File

@ -0,0 +1,192 @@
aswSubs = {}
aswSubs.version = "1.0.0"
aswSubs.verbose = false
aswSubs.requiredLibs = {
"dcsCommon", -- always
"cfxZones", -- Zones, of course
}
--[[--
Version History
1.0.0 - initial version
--]]--
aswSubs.groupsToWatch = {} -- subs attack any group in here if they are of a different coalition and not neutral
aswSubs.unitsHit = {} -- the goners
function aswSubs.addWatchgroup(name)
if Group.getByName(name) then
aswSubs.groupsToWatch[name] = name
else
trigger.action.outText("+++aswSubs: no group named <" .. name .. "> 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

193
modules/aswZones.lua Normal file
View File

@ -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

View File

@ -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)

View File

@ -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-<some other orders>" do nothing. the "wait" prefix will be removed some time and <some other order> 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-<some other orders>" do nothing. the "wait" prefix will be removed some time and <some other order> 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

View File

@ -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

View File

@ -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)

View File

@ -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 <side> 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 <side> 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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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!", "*<none>")
--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

View File

@ -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}

View File

@ -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

View File

@ -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 - <player: unit>
- 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?", "<none>")
trigger.action.outText("+++Msg: Warning - zone <" .. theZone.name .. "> uses 'messageValue' attribute. Migrate to <v:<flag> now!")
trigger.action.outText("+++Msg: Warning - zone <" .. theZone.name .. "> uses 'messageValue' attribute. Migrate to <v:<flag> now!", 30)
end
-- time format for new <t: flagname>
@ -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
--]]--

View File

@ -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

View File

@ -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

View File

@ -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

Binary file not shown.

BIN
sound FX/submarine ping.ogg Normal file

Binary file not shown.

Binary file not shown.